summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
-rw-r--r--.eslintignore2
-rw-r--r--.eslintrc.yml11
-rw-r--r--.gitlab-ci.yml13
-rw-r--r--.gitlab/CODEOWNERS8
-rw-r--r--.gitlab/ci/cache-repo.gitlab-ci.yml1
-rw-r--r--.gitlab/ci/cng.gitlab-ci.yml1
-rw-r--r--.gitlab/ci/dev-fixtures.gitlab-ci.yml14
-rw-r--r--.gitlab/ci/docs.gitlab-ci.yml3
-rw-r--r--.gitlab/ci/frontend.gitlab-ci.yml89
-rw-r--r--.gitlab/ci/global.gitlab-ci.yml74
-rw-r--r--.gitlab/ci/memory.gitlab-ci.yml6
-rw-r--r--.gitlab/ci/pages.gitlab-ci.yml1
-rw-r--r--.gitlab/ci/qa.gitlab-ci.yml7
-rw-r--r--.gitlab/ci/rails.gitlab-ci.yml223
-rw-r--r--.gitlab/ci/reports.gitlab-ci.yml29
-rw-r--r--.gitlab/ci/review.gitlab-ci.yml85
-rw-r--r--.gitlab/ci/rules.gitlab-ci.yml204
-rw-r--r--.gitlab/ci/setup.gitlab-ci.yml4
-rw-r--r--.gitlab/ci/test-metadata.gitlab-ci.yml13
-rw-r--r--.gitlab/issue_templates/Feature proposal.md27
-rw-r--r--.gitlab/issue_templates/Productivity Improvement.md5
-rw-r--r--.gitlab/issue_templates/Security developer workflow.md4
-rw-r--r--.gitlab/issue_templates/Technical Evaluation.md4
-rw-r--r--.gitlab/merge_request_templates/Documentation.md23
-rw-r--r--.gitlab/merge_request_templates/Security Release.md2
-rw-r--r--.haml-lint_todo.yml799
-rw-r--r--.markdownlint.json9
-rw-r--r--.rubocop.yml74
-rw-r--r--.rubocop_todo.yml24
-rw-r--r--CHANGELOG-EE.md41
-rw-r--r--CHANGELOG.md62
-rw-r--r--GITALY_SERVER_VERSION2
-rw-r--r--GITLAB_ELASTICSEARCH_INDEXER_VERSION2
-rw-r--r--GITLAB_PAGES_VERSION2
-rw-r--r--GITLAB_SHELL_VERSION2
-rw-r--r--GITLAB_WORKHORSE_VERSION2
-rw-r--r--Gemfile45
-rw-r--r--Gemfile.lock193
-rw-r--r--Guardfile51
-rw-r--r--README.md2
-rw-r--r--app/assets/images/cluster_app_logos/fluentd.pngbin0 -> 2480 bytes
-rw-r--r--app/assets/javascripts/access_tokens/components/expires_at_field.vue14
-rw-r--r--app/assets/javascripts/access_tokens/index.js12
-rw-r--r--app/assets/javascripts/actioncable_consumer.js3
-rw-r--r--app/assets/javascripts/alert_management/components/alert_details.vue236
-rw-r--r--app/assets/javascripts/alert_management/components/alert_management_list.vue303
-rw-r--r--app/assets/javascripts/alert_management/constants.js46
-rw-r--r--app/assets/javascripts/alert_management/details.js47
-rw-r--r--app/assets/javascripts/alert_management/graphql/fragments/detailItem.fragment.graphql11
-rw-r--r--app/assets/javascripts/alert_management/graphql/fragments/listItem.fragment.graphql9
-rw-r--r--app/assets/javascripts/alert_management/graphql/mutations/update_alert_status.graphql9
-rw-r--r--app/assets/javascripts/alert_management/graphql/queries/details.query.graphql11
-rw-r--r--app/assets/javascripts/alert_management/graphql/queries/getAlerts.query.graphql11
-rw-r--r--app/assets/javascripts/alert_management/list.js55
-rw-r--r--app/assets/javascripts/alert_management/services/index.js7
-rw-r--r--app/assets/javascripts/alerts_service_settings/components/alerts_service_form.vue4
-rw-r--r--app/assets/javascripts/api.js57
-rw-r--r--app/assets/javascripts/autosave.js13
-rw-r--r--app/assets/javascripts/awards_handler.js8
-rw-r--r--app/assets/javascripts/behaviors/copy_to_clipboard.js3
-rw-r--r--app/assets/javascripts/behaviors/markdown/marks/inline_html.js4
-rw-r--r--app/assets/javascripts/behaviors/markdown/paste_markdown_table.js3
-rw-r--r--app/assets/javascripts/behaviors/markdown/render_gfm.js2
-rw-r--r--app/assets/javascripts/behaviors/markdown/render_mermaid.js12
-rw-r--r--app/assets/javascripts/behaviors/markdown/render_metrics.js27
-rw-r--r--app/assets/javascripts/behaviors/shortcuts/shortcuts_blob.js2
-rw-r--r--app/assets/javascripts/blob/blob_fork_suggestion.js2
-rw-r--r--app/assets/javascripts/blob/components/blob_content.vue17
-rw-r--r--app/assets/javascripts/blob/components/blob_content_error.vue71
-rw-r--r--app/assets/javascripts/blob/components/blob_edit_header.vue4
-rw-r--r--app/assets/javascripts/blob/components/blob_header.vue2
-rw-r--r--app/assets/javascripts/blob/components/blob_header_filepath.vue14
-rw-r--r--app/assets/javascripts/blob/components/blob_header_viewer_switcher.vue2
-rw-r--r--app/assets/javascripts/blob/components/constants.js56
-rw-r--r--app/assets/javascripts/blob/suggest_gitlab_ci_yml/components/popover.vue3
-rw-r--r--app/assets/javascripts/blob/utils.js19
-rw-r--r--app/assets/javascripts/boards/components/board.js3
-rw-r--r--app/assets/javascripts/boards/components/board_column.vue6
-rw-r--r--app/assets/javascripts/boards/components/board_form.vue8
-rw-r--r--app/assets/javascripts/boards/components/board_sidebar.js5
-rw-r--r--app/assets/javascripts/boards/components/boards_selector.vue6
-rw-r--r--app/assets/javascripts/boards/components/issue_card_inner.vue4
-rw-r--r--app/assets/javascripts/boards/constants.js8
-rw-r--r--app/assets/javascripts/boards/filtered_search_boards.js2
-rw-r--r--app/assets/javascripts/boards/icons/fullscreen_collapse.svg1
-rw-r--r--app/assets/javascripts/boards/icons/fullscreen_expand.svg1
-rw-r--r--app/assets/javascripts/boards/index.js116
-rw-r--r--app/assets/javascripts/boards/mixins/sortable_default_options.js5
-rw-r--r--app/assets/javascripts/boards/models/assignee.js2
-rw-r--r--app/assets/javascripts/boards/models/issue.js28
-rw-r--r--app/assets/javascripts/boards/models/list.js87
-rw-r--r--app/assets/javascripts/boards/queries/board_list.fragment.graphql5
-rw-r--r--app/assets/javascripts/boards/queries/board_list_shared.fragment.graphql15
-rw-r--r--app/assets/javascripts/boards/queries/group_board.query.graphql13
-rw-r--r--app/assets/javascripts/boards/queries/project_board.query.graphql13
-rw-r--r--app/assets/javascripts/boards/stores/boards_store.js114
-rw-r--r--app/assets/javascripts/boards/stores/state.js4
-rw-r--r--app/assets/javascripts/boards/toggle_focus.js46
-rw-r--r--app/assets/javascripts/broadcast_notification.js4
-rw-r--r--app/assets/javascripts/ci_variable_list/ajax_variable_list.js4
-rw-r--r--app/assets/javascripts/ci_variable_list/components/ci_variable_modal.vue8
-rw-r--r--app/assets/javascripts/ci_variable_list/constants.js2
-rw-r--r--app/assets/javascripts/ci_variable_list/index.js4
-rw-r--r--app/assets/javascripts/ci_variable_list/store/actions.js4
-rw-r--r--app/assets/javascripts/ci_variable_list/store/mutation_types.js1
-rw-r--r--app/assets/javascripts/ci_variable_list/store/mutations.js4
-rw-r--r--app/assets/javascripts/ci_variable_list/store/state.js1
-rw-r--r--app/assets/javascripts/close_reopen_report_toggle.js2
-rw-r--r--app/assets/javascripts/clusters/clusters_bundle.js13
-rw-r--r--app/assets/javascripts/clusters/components/applications.vue73
-rw-r--r--app/assets/javascripts/clusters/components/fluentd_output_settings.vue241
-rw-r--r--app/assets/javascripts/clusters/components/ingress_modsecurity_settings.vue4
-rw-r--r--app/assets/javascripts/clusters/components/remove_cluster_confirmation.vue4
-rw-r--r--app/assets/javascripts/clusters/constants.js2
-rw-r--r--app/assets/javascripts/clusters/event_hub.js4
-rw-r--r--app/assets/javascripts/clusters/services/clusters_service.js1
-rw-r--r--app/assets/javascripts/clusters/stores/clusters_store.js19
-rw-r--r--app/assets/javascripts/clusters_list/components/clusters.vue162
-rw-r--r--app/assets/javascripts/clusters_list/constants.js3
-rw-r--r--app/assets/javascripts/clusters_list/store/actions.js33
-rw-r--r--app/assets/javascripts/clusters_list/store/mutation_types.js1
-rw-r--r--app/assets/javascripts/clusters_list/store/mutations.js10
-rw-r--r--app/assets/javascripts/clusters_list/store/state.js6
-rw-r--r--app/assets/javascripts/code_navigation/components/app.vue8
-rw-r--r--app/assets/javascripts/code_navigation/components/popover.vue33
-rw-r--r--app/assets/javascripts/code_navigation/store/actions.js9
-rw-r--r--app/assets/javascripts/code_navigation/store/mutations.js3
-rw-r--r--app/assets/javascripts/code_navigation/store/state.js1
-rw-r--r--app/assets/javascripts/comment_type_toggle.js2
-rw-r--r--app/assets/javascripts/commit/image_file.js5
-rw-r--r--app/assets/javascripts/commons/index.js1
-rw-r--r--app/assets/javascripts/compare_autocomplete.js5
-rw-r--r--app/assets/javascripts/confidential_merge_request/components/dropdown.vue2
-rw-r--r--app/assets/javascripts/create_cluster/eks_cluster/components/eks_cluster_configuration_form.vue6
-rw-r--r--app/assets/javascripts/create_cluster/eks_cluster/components/service_credentials_form.vue6
-rw-r--r--app/assets/javascripts/create_cluster/gke_cluster/components/gke_project_id_dropdown.vue4
-rw-r--r--app/assets/javascripts/create_item_dropdown.js8
-rw-r--r--app/assets/javascripts/create_merge_request_dropdown.js2
-rw-r--r--app/assets/javascripts/cycle_analytics/cycle_analytics_store.js2
-rw-r--r--app/assets/javascripts/deploy_keys/eventhub.js4
-rw-r--r--app/assets/javascripts/design_management/components/app.vue3
-rw-r--r--app/assets/javascripts/design_management/components/delete_button.vue64
-rw-r--r--app/assets/javascripts/design_management/components/design_destroyer.vue66
-rw-r--r--app/assets/javascripts/design_management/components/design_note_pin.vue61
-rw-r--r--app/assets/javascripts/design_management/components/design_notes/design_discussion.vue169
-rw-r--r--app/assets/javascripts/design_management/components/design_notes/design_note.vue148
-rw-r--r--app/assets/javascripts/design_management/components/design_notes/design_reply_form.vue137
-rw-r--r--app/assets/javascripts/design_management/components/design_overlay.vue279
-rw-r--r--app/assets/javascripts/design_management/components/design_presentation.vue314
-rw-r--r--app/assets/javascripts/design_management/components/design_scaler.vue65
-rw-r--r--app/assets/javascripts/design_management/components/image.vue110
-rw-r--r--app/assets/javascripts/design_management/components/list/item.vue174
-rw-r--r--app/assets/javascripts/design_management/components/toolbar/index.vue126
-rw-r--r--app/assets/javascripts/design_management/components/toolbar/pagination.vue83
-rw-r--r--app/assets/javascripts/design_management/components/toolbar/pagination_button.vue48
-rw-r--r--app/assets/javascripts/design_management/components/upload/button.vue58
-rw-r--r--app/assets/javascripts/design_management/components/upload/design_dropzone.vue134
-rw-r--r--app/assets/javascripts/design_management/components/upload/design_version_dropdown.vue76
-rw-r--r--app/assets/javascripts/design_management/constants.js14
-rw-r--r--app/assets/javascripts/design_management/graphql.js45
-rw-r--r--app/assets/javascripts/design_management/graphql/fragments/design.fragment.graphql22
-rw-r--r--app/assets/javascripts/design_management/graphql/fragments/designList.fragment.graphql8
-rw-r--r--app/assets/javascripts/design_management/graphql/fragments/designNote.fragment.graphql28
-rw-r--r--app/assets/javascripts/design_management/graphql/fragments/diffRefs.fragment.graphql5
-rw-r--r--app/assets/javascripts/design_management/graphql/fragments/note_permissions.fragment.graphql3
-rw-r--r--app/assets/javascripts/design_management/graphql/fragments/version.fragment.graphql4
-rw-r--r--app/assets/javascripts/design_management/graphql/mutations/createImageDiffNote.mutation.graphql21
-rw-r--r--app/assets/javascripts/design_management/graphql/mutations/createNote.mutation.graphql10
-rw-r--r--app/assets/javascripts/design_management/graphql/mutations/destroyDesign.mutation.graphql10
-rw-r--r--app/assets/javascripts/design_management/graphql/mutations/updateImageDiffNote.mutation.graphql10
-rw-r--r--app/assets/javascripts/design_management/graphql/mutations/update_active_discussion.mutation.graphql3
-rw-r--r--app/assets/javascripts/design_management/graphql/mutations/update_note.mutation.graphql10
-rw-r--r--app/assets/javascripts/design_management/graphql/mutations/uploadDesign.mutation.graphql21
-rw-r--r--app/assets/javascripts/design_management/graphql/queries/active_discussion.query.graphql6
-rw-r--r--app/assets/javascripts/design_management/graphql/queries/appData.query.graphql4
-rw-r--r--app/assets/javascripts/design_management/graphql/queries/design_permissions.query.graphql10
-rw-r--r--app/assets/javascripts/design_management/graphql/queries/getDesign.query.graphql31
-rw-r--r--app/assets/javascripts/design_management/graphql/queries/get_design_list.query.graphql26
-rw-r--r--app/assets/javascripts/design_management/graphql/typedefs.graphql12
-rw-r--r--app/assets/javascripts/design_management/index.js58
-rw-r--r--app/assets/javascripts/design_management/mixins/all_designs.js49
-rw-r--r--app/assets/javascripts/design_management/mixins/all_versions.js62
-rw-r--r--app/assets/javascripts/design_management/pages/design/index.vue400
-rw-r--r--app/assets/javascripts/design_management/pages/index.vue323
-rw-r--r--app/assets/javascripts/design_management/router/constants.js3
-rw-r--r--app/assets/javascripts/design_management/router/index.js22
-rw-r--r--app/assets/javascripts/design_management/router/routes.js44
-rw-r--r--app/assets/javascripts/design_management/utils/cache_update.js272
-rw-r--r--app/assets/javascripts/design_management/utils/design_management_utils.js125
-rw-r--r--app/assets/javascripts/design_management/utils/error_messages.js95
-rw-r--r--app/assets/javascripts/design_management/utils/tracking.js28
-rw-r--r--app/assets/javascripts/diff_notes/components/comment_resolve_btn.js21
-rw-r--r--app/assets/javascripts/diff_notes/components/jump_to_discussion.js11
-rw-r--r--app/assets/javascripts/diffs/components/app.vue23
-rw-r--r--app/assets/javascripts/diffs/components/commit_item.vue75
-rw-r--r--app/assets/javascripts/diffs/components/diff_discussions.vue8
-rw-r--r--app/assets/javascripts/diffs/components/diff_file.vue4
-rw-r--r--app/assets/javascripts/diffs/components/diff_file_header.vue6
-rw-r--r--app/assets/javascripts/diffs/components/edit_button.vue36
-rw-r--r--app/assets/javascripts/diffs/components/no_changes.vue6
-rw-r--r--app/assets/javascripts/diffs/store/actions.js80
-rw-r--r--app/assets/javascripts/diffs/store/getters_versions_dropdowns.js5
-rw-r--r--app/assets/javascripts/diffs/store/mutation_types.js2
-rw-r--r--app/assets/javascripts/diffs/store/mutations.js5
-rw-r--r--app/assets/javascripts/diffs/store/utils.js53
-rw-r--r--app/assets/javascripts/dropzone_input.js6
-rw-r--r--app/assets/javascripts/editor/editor_lite.js4
-rw-r--r--app/assets/javascripts/environments/components/confirm_rollback_modal.vue10
-rw-r--r--app/assets/javascripts/environments/components/container.vue34
-rw-r--r--app/assets/javascripts/environments/components/delete_environment_modal.vue2
-rw-r--r--app/assets/javascripts/environments/components/environment_item.vue13
-rw-r--r--app/assets/javascripts/environments/components/environments_app.vue33
-rw-r--r--app/assets/javascripts/environments/components/environments_table.vue33
-rw-r--r--app/assets/javascripts/environments/components/stop_environment_modal.vue2
-rw-r--r--app/assets/javascripts/environments/event_hub.js4
-rw-r--r--app/assets/javascripts/environments/folder/environments_folder_bundle.js2
-rw-r--r--app/assets/javascripts/environments/folder/environments_folder_view.vue28
-rw-r--r--app/assets/javascripts/environments/index.js2
-rw-r--r--app/assets/javascripts/environments/mixins/canary_callout_mixin.js23
-rw-r--r--app/assets/javascripts/environments/mixins/container_mixin.js34
-rw-r--r--app/assets/javascripts/environments/mixins/environment_item_mixin.js13
-rw-r--r--app/assets/javascripts/environments/mixins/environments_app_mixin.js32
-rw-r--r--app/assets/javascripts/environments/mixins/environments_folder_view_mixin.js29
-rw-r--r--app/assets/javascripts/environments/mixins/environments_table_mixin.js10
-rw-r--r--app/assets/javascripts/environments/stores/environments_store.js20
-rw-r--r--app/assets/javascripts/error_tracking/components/error_tracking_list.vue6
-rw-r--r--app/assets/javascripts/error_tracking/components/stacktrace_entry.vue4
-rw-r--r--app/assets/javascripts/error_tracking/store/actions.js2
-rw-r--r--app/assets/javascripts/filtered_search/constants.js5
-rw-r--r--app/assets/javascripts/filtered_search/dropdown_operator.js10
-rw-r--r--app/assets/javascripts/filtered_search/event_hub.js4
-rw-r--r--app/assets/javascripts/filtered_search/filtered_search_dropdown.js41
-rw-r--r--app/assets/javascripts/filtered_search/filtered_search_dropdown_manager.js2
-rw-r--r--app/assets/javascripts/filtered_search/filtered_search_manager.js17
-rw-r--r--app/assets/javascripts/filtered_search/stores/recent_searches_store.js14
-rw-r--r--app/assets/javascripts/filtered_search/visual_token_value.js4
-rw-r--r--app/assets/javascripts/flash.js6
-rw-r--r--app/assets/javascripts/frequent_items/components/app.vue2
-rw-r--r--app/assets/javascripts/frequent_items/event_hub.js4
-rw-r--r--app/assets/javascripts/gfm_auto_complete.js18
-rw-r--r--app/assets/javascripts/gl_dropdown.js15
-rw-r--r--app/assets/javascripts/gl_form.js8
-rw-r--r--app/assets/javascripts/groups/components/app.vue7
-rw-r--r--app/assets/javascripts/groups/event_hub.js4
-rw-r--r--app/assets/javascripts/groups/new_group_child.js2
-rw-r--r--app/assets/javascripts/header.js23
-rw-r--r--app/assets/javascripts/helpers/avatar_helper.js5
-rw-r--r--app/assets/javascripts/helpers/event_hub_factory.js20
-rw-r--r--app/assets/javascripts/ide/components/commit_sidebar/actions.vue4
-rw-r--r--app/assets/javascripts/ide/components/commit_sidebar/editor_header.vue6
-rw-r--r--app/assets/javascripts/ide/components/commit_sidebar/form.vue37
-rw-r--r--app/assets/javascripts/ide/components/commit_sidebar/list.vue18
-rw-r--r--app/assets/javascripts/ide/components/ide.vue24
-rw-r--r--app/assets/javascripts/ide/components/ide_review.vue2
-rw-r--r--app/assets/javascripts/ide/components/ide_side_bar.vue2
-rw-r--r--app/assets/javascripts/ide/components/ide_tree.vue18
-rw-r--r--app/assets/javascripts/ide/components/jobs/detail.vue4
-rw-r--r--app/assets/javascripts/ide/components/nav_form.vue4
-rw-r--r--app/assets/javascripts/ide/components/new_dropdown/index.vue7
-rw-r--r--app/assets/javascripts/ide/components/new_dropdown/modal.vue112
-rw-r--r--app/assets/javascripts/ide/components/panes/collapsible_sidebar.vue3
-rw-r--r--app/assets/javascripts/ide/components/pipelines/list.vue11
-rw-r--r--app/assets/javascripts/ide/components/repo_commit_section.vue29
-rw-r--r--app/assets/javascripts/ide/components/repo_editor.vue32
-rw-r--r--app/assets/javascripts/ide/components/resizable_panel.vue31
-rw-r--r--app/assets/javascripts/ide/constants.js1
-rw-r--r--app/assets/javascripts/ide/eventhub.js4
-rw-r--r--app/assets/javascripts/ide/lib/diff/diff.js13
-rw-r--r--app/assets/javascripts/ide/lib/editor.js3
-rw-r--r--app/assets/javascripts/ide/lib/languages/index.js5
-rw-r--r--app/assets/javascripts/ide/lib/languages/vue.js306
-rw-r--r--app/assets/javascripts/ide/lib/themes/index.js20
-rw-r--r--app/assets/javascripts/ide/lib/themes/monokai.js169
-rw-r--r--app/assets/javascripts/ide/lib/themes/none.js17
-rw-r--r--app/assets/javascripts/ide/lib/themes/solarized_dark.js1110
-rw-r--r--app/assets/javascripts/ide/lib/themes/solarized_light.js1101
-rw-r--r--app/assets/javascripts/ide/services/index.js8
-rw-r--r--app/assets/javascripts/ide/stores/actions.js20
-rw-r--r--app/assets/javascripts/ide/stores/actions/project.js6
-rw-r--r--app/assets/javascripts/ide/stores/actions/tree.js2
-rw-r--r--app/assets/javascripts/ide/stores/modules/commit/actions.js36
-rw-r--r--app/assets/javascripts/ide/stores/mutation_types.js3
-rw-r--r--app/assets/javascripts/ide/stores/mutations.js19
-rw-r--r--app/assets/javascripts/ide/stores/mutations/project.js4
-rw-r--r--app/assets/javascripts/ide/stores/mutations/tree.js5
-rw-r--r--app/assets/javascripts/ide/stores/state.js2
-rw-r--r--app/assets/javascripts/ide/stores/utils.js43
-rw-r--r--app/assets/javascripts/ide/utils.js16
-rw-r--r--app/assets/javascripts/image_diff/helpers/badge_helper.js4
-rw-r--r--app/assets/javascripts/image_diff/helpers/comment_indicator_helper.js4
-rw-r--r--app/assets/javascripts/image_diff/helpers/dom_helper.js11
-rw-r--r--app/assets/javascripts/image_diff/image_diff.js6
-rw-r--r--app/assets/javascripts/import_projects/event_hub.js4
-rw-r--r--app/assets/javascripts/importer_status.js6
-rw-r--r--app/assets/javascripts/integrations/edit/components/active_toggle.vue11
-rw-r--r--app/assets/javascripts/integrations/edit/components/integration_form.vue50
-rw-r--r--app/assets/javascripts/integrations/edit/components/jira_trigger_fields.vue99
-rw-r--r--app/assets/javascripts/integrations/edit/components/trigger_fields.vue73
-rw-r--r--app/assets/javascripts/integrations/edit/event_hub.js4
-rw-r--r--app/assets/javascripts/integrations/edit/index.js40
-rw-r--r--app/assets/javascripts/issuable_bulk_update_actions.js8
-rw-r--r--app/assets/javascripts/issuable_bulk_update_sidebar.js2
-rw-r--r--app/assets/javascripts/issuable_sidebar/queries/issue_sidebar.query.graphql15
-rw-r--r--app/assets/javascripts/issuables_list/components/issuable_list_root_app.vue96
-rw-r--r--app/assets/javascripts/issuables_list/eventhub.js6
-rw-r--r--app/assets/javascripts/issuables_list/index.js55
-rw-r--r--app/assets/javascripts/issuables_list/queries/get_issues_list_details.query.graphql22
-rw-r--r--app/assets/javascripts/issue.js58
-rw-r--r--app/assets/javascripts/issue_show/components/app.vue13
-rw-r--r--app/assets/javascripts/issue_show/components/fields/description.vue32
-rw-r--r--app/assets/javascripts/issue_show/event_hub.js4
-rw-r--r--app/assets/javascripts/jira_import/components/jira_import_app.vue70
-rw-r--r--app/assets/javascripts/jira_import/components/jira_import_form.vue23
-rw-r--r--app/assets/javascripts/jira_import/components/jira_import_progress.vue5
-rw-r--r--app/assets/javascripts/jira_import/components/jira_import_setup.vue6
-rw-r--r--app/assets/javascripts/jira_import/index.js1
-rw-r--r--app/assets/javascripts/jira_import/queries/get_jira_import_details.query.graphql2
-rw-r--r--app/assets/javascripts/jira_import/utils.js49
-rw-r--r--app/assets/javascripts/jobs/components/environments_block.vue8
-rw-r--r--app/assets/javascripts/jobs/store/actions.js4
-rw-r--r--app/assets/javascripts/jobs/store/state.js2
-rw-r--r--app/assets/javascripts/labels_select.js50
-rw-r--r--app/assets/javascripts/lib/graphql.js1
-rw-r--r--app/assets/javascripts/lib/utils/datetime_utility.js13
-rw-r--r--app/assets/javascripts/lib/utils/downloader.js20
-rw-r--r--app/assets/javascripts/lib/utils/keycodes.js3
-rw-r--r--app/assets/javascripts/lib/utils/keys.js4
-rw-r--r--app/assets/javascripts/lib/utils/text_markdown.js7
-rw-r--r--app/assets/javascripts/lib/utils/url_utility.js66
-rw-r--r--app/assets/javascripts/line_highlighter.js9
-rw-r--r--app/assets/javascripts/locale/sprintf.js2
-rw-r--r--app/assets/javascripts/main.js5
-rw-r--r--app/assets/javascripts/member_expiration_date.js3
-rw-r--r--app/assets/javascripts/merge_request_tabs.js4
-rw-r--r--app/assets/javascripts/milestone_select.js41
-rw-r--r--app/assets/javascripts/milestones/project_milestone_combobox.vue228
-rw-r--r--app/assets/javascripts/mirrors/ssh_mirror.js4
-rw-r--r--app/assets/javascripts/monitoring/components/alert_widget.vue286
-rw-r--r--app/assets/javascripts/monitoring/components/alert_widget_form.vue307
-rw-r--r--app/assets/javascripts/monitoring/components/charts/anomaly.vue6
-rw-r--r--app/assets/javascripts/monitoring/components/charts/heatmap.vue2
-rw-r--r--app/assets/javascripts/monitoring/components/charts/time_series.vue13
-rw-r--r--app/assets/javascripts/monitoring/components/dashboard.vue277
-rw-r--r--app/assets/javascripts/monitoring/components/dashboard_panel.vue393
-rw-r--r--app/assets/javascripts/monitoring/components/dashboards_dropdown.vue55
-rw-r--r--app/assets/javascripts/monitoring/components/embeds/metric_embed.vue8
-rw-r--r--app/assets/javascripts/monitoring/components/panel_type.vue324
-rw-r--r--app/assets/javascripts/monitoring/components/variables/custom_variable.vue50
-rw-r--r--app/assets/javascripts/monitoring/components/variables/text_variable.vue39
-rw-r--r--app/assets/javascripts/monitoring/components/variables_section.vue56
-rw-r--r--app/assets/javascripts/monitoring/constants.js84
-rw-r--r--app/assets/javascripts/monitoring/monitoring_bundle.js2
-rw-r--r--app/assets/javascripts/monitoring/monitoring_bundle_with_alerts.js13
-rw-r--r--app/assets/javascripts/monitoring/services/alerts_service.js32
-rw-r--r--app/assets/javascripts/monitoring/stores/actions.js84
-rw-r--r--app/assets/javascripts/monitoring/stores/getters.js28
-rw-r--r--app/assets/javascripts/monitoring/stores/mutation_types.js10
-rw-r--r--app/assets/javascripts/monitoring/stores/mutations.js36
-rw-r--r--app/assets/javascripts/monitoring/stores/state.js17
-rw-r--r--app/assets/javascripts/monitoring/stores/utils.js2
-rw-r--r--app/assets/javascripts/monitoring/stores/variable_mapping.js167
-rw-r--r--app/assets/javascripts/monitoring/utils.js201
-rw-r--r--app/assets/javascripts/monitoring/validators.js44
-rw-r--r--app/assets/javascripts/namespace_select.js8
-rw-r--r--app/assets/javascripts/new_branch_form.js5
-rw-r--r--app/assets/javascripts/notebook/cells/markdown.vue34
-rw-r--r--app/assets/javascripts/notebook/cells/output/index.vue3
-rw-r--r--app/assets/javascripts/notes.js19
-rw-r--r--app/assets/javascripts/notes/components/comment_form.vue83
-rw-r--r--app/assets/javascripts/notes/components/discussion_counter.vue19
-rw-r--r--app/assets/javascripts/notes/components/note_form.vue6
-rw-r--r--app/assets/javascripts/notes/components/note_header.vue93
-rw-r--r--app/assets/javascripts/notes/components/noteable_note.vue10
-rw-r--r--app/assets/javascripts/notes/components/notes_app.vue5
-rw-r--r--app/assets/javascripts/notes/index.js4
-rw-r--r--app/assets/javascripts/notes/mixins/discussion_navigation.js2
-rw-r--r--app/assets/javascripts/notes/stores/actions.js17
-rw-r--r--app/assets/javascripts/notes/stores/getters.js2
-rw-r--r--app/assets/javascripts/notes/stores/index.js6
-rw-r--r--app/assets/javascripts/notes/stores/modules/index.js2
-rw-r--r--app/assets/javascripts/notes/stores/mutation_types.js1
-rw-r--r--app/assets/javascripts/notes/stores/mutations.js4
-rw-r--r--app/assets/javascripts/pages/admin/application_settings/general/index.js2
-rw-r--r--app/assets/javascripts/pages/admin/impersonation_tokens/index.js4
-rw-r--r--app/assets/javascripts/pages/admin/projects/index/components/delete_project_modal.vue8
-rw-r--r--app/assets/javascripts/pages/admin/users/components/delete_user_modal.vue8
-rw-r--r--app/assets/javascripts/pages/milestones/shared/components/delete_milestone_modal.vue2
-rw-r--r--app/assets/javascripts/pages/milestones/shared/components/promote_milestone_modal.vue2
-rw-r--r--app/assets/javascripts/pages/milestones/shared/event_hub.js4
-rw-r--r--app/assets/javascripts/pages/profiles/personal_access_tokens/index.js4
-rw-r--r--app/assets/javascripts/pages/projects/alert_management/details/index.js5
-rw-r--r--app/assets/javascripts/pages/projects/alert_management/index/index.js5
-rw-r--r--app/assets/javascripts/pages/projects/blob/new/index.js11
-rw-r--r--app/assets/javascripts/pages/projects/blob/show/index.js7
-rw-r--r--app/assets/javascripts/pages/projects/environments/metrics/index.js2
-rw-r--r--app/assets/javascripts/pages/projects/issues/index/index.js5
-rw-r--r--app/assets/javascripts/pages/projects/issues/show.js10
-rw-r--r--app/assets/javascripts/pages/projects/labels/components/promote_label_modal.vue4
-rw-r--r--app/assets/javascripts/pages/projects/labels/event_hub.js4
-rw-r--r--app/assets/javascripts/pages/projects/pipeline_schedules/shared/components/interval_pattern_input.vue139
-rw-r--r--app/assets/javascripts/pages/projects/pipeline_schedules/shared/components/pipeline_schedules_callout.vue3
-rw-r--r--app/assets/javascripts/pages/projects/pipeline_schedules/shared/init_form.js4
-rw-r--r--app/assets/javascripts/pages/projects/pipelines/dag/index.js2
-rw-r--r--app/assets/javascripts/pages/projects/pipelines/index/index.js1
-rw-r--r--app/assets/javascripts/pages/projects/settings/access_tokens/index.js3
-rw-r--r--app/assets/javascripts/pages/projects/shared/permissions/components/settings_panel.vue49
-rw-r--r--app/assets/javascripts/pages/projects/wikis/components/delete_wiki_modal.vue5
-rw-r--r--app/assets/javascripts/pages/projects/wikis/wikis.js13
-rw-r--r--app/assets/javascripts/pages/search/init_filtered_search.js2
-rw-r--r--app/assets/javascripts/performance_bar/components/detailed_metric.vue4
-rw-r--r--app/assets/javascripts/persistent_user_callout.js5
-rw-r--r--app/assets/javascripts/pipelines/components/graph/job_group_dropdown.vue4
-rw-r--r--app/assets/javascripts/pipelines/components/graph/job_name_component.vue4
-rw-r--r--app/assets/javascripts/pipelines/components/graph/stage_column_component.vue4
-rw-r--r--app/assets/javascripts/pipelines/components/pipelines.vue44
-rw-r--r--app/assets/javascripts/pipelines/components/pipelines_filtered_search.vue91
-rw-r--r--app/assets/javascripts/pipelines/components/pipelines_table_row.vue5
-rw-r--r--app/assets/javascripts/pipelines/components/stage.vue2
-rw-r--r--app/assets/javascripts/pipelines/components/test_reports/test_reports.vue3
-rw-r--r--app/assets/javascripts/pipelines/components/test_reports/test_summary.vue9
-rw-r--r--app/assets/javascripts/pipelines/components/test_reports/test_summary_table.vue18
-rw-r--r--app/assets/javascripts/pipelines/components/tokens/pipeline_branch_name_token.vue70
-rw-r--r--app/assets/javascripts/pipelines/components/tokens/pipeline_trigger_author_token.vue114
-rw-r--r--app/assets/javascripts/pipelines/constants.js10
-rw-r--r--app/assets/javascripts/pipelines/pipeline_details_bundle.js65
-rw-r--r--app/assets/javascripts/pipelines/services/pipelines_service.js14
-rw-r--r--app/assets/javascripts/pipelines/stores/pipeline_store.js2
-rw-r--r--app/assets/javascripts/profile/account/components/delete_account_modal.vue2
-rw-r--r--app/assets/javascripts/profile/account/components/update_username.vue10
-rw-r--r--app/assets/javascripts/project_select.js23
-rw-r--r--app/assets/javascripts/projects/commits/components/author_select.vue1
-rw-r--r--app/assets/javascripts/projects/commits/store/actions.js6
-rw-r--r--app/assets/javascripts/projects/pipelines/charts/components/app.vue3
-rw-r--r--app/assets/javascripts/registry/explorer/components/image_list.vue124
-rw-r--r--app/assets/javascripts/registry/explorer/constants.js44
-rw-r--r--app/assets/javascripts/registry/explorer/index.js2
-rw-r--r--app/assets/javascripts/registry/explorer/pages/details.vue168
-rw-r--r--app/assets/javascripts/registry/explorer/pages/index.vue39
-rw-r--r--app/assets/javascripts/registry/explorer/pages/list.vue172
-rw-r--r--app/assets/javascripts/registry/explorer/router.js12
-rw-r--r--app/assets/javascripts/registry/explorer/stores/actions.js11
-rw-r--r--app/assets/javascripts/registry/explorer/stores/index.js1
-rw-r--r--app/assets/javascripts/registry/settings/registry_settings_bundle.js2
-rw-r--r--app/assets/javascripts/registry/settings/store/getters.js1
-rw-r--r--app/assets/javascripts/registry/settings/store/mutations.js2
-rw-r--r--app/assets/javascripts/registry/shared/components/expiration_policy_fields.vue116
-rw-r--r--app/assets/javascripts/registry/shared/constants.js32
-rw-r--r--app/assets/javascripts/releases/components/app_edit.vue74
-rw-r--r--app/assets/javascripts/releases/components/app_index.vue11
-rw-r--r--app/assets/javascripts/releases/components/asset_links_form.vue2
-rw-r--r--app/assets/javascripts/releases/components/evidence_block.vue8
-rw-r--r--app/assets/javascripts/releases/components/release_block_footer.vue7
-rw-r--r--app/assets/javascripts/releases/components/release_block_header.vue13
-rw-r--r--app/assets/javascripts/releases/components/release_block_metadata.vue15
-rw-r--r--app/assets/javascripts/releases/components/release_block_milestone_info.vue12
-rw-r--r--app/assets/javascripts/releases/stores/modules/detail/actions.js11
-rw-r--r--app/assets/javascripts/releases/stores/modules/detail/mutation_types.js1
-rw-r--r--app/assets/javascripts/releases/stores/modules/detail/mutations.js4
-rw-r--r--app/assets/javascripts/releases/stores/modules/detail/state.js4
-rw-r--r--app/assets/javascripts/reports/accessibility_report/components/accessibility_issue_body.vue30
-rw-r--r--app/assets/javascripts/reports/accessibility_report/grouped_accessibility_reports_app.vue64
-rw-r--r--app/assets/javascripts/reports/accessibility_report/store/actions.js79
-rw-r--r--app/assets/javascripts/reports/accessibility_report/store/getters.js48
-rw-r--r--app/assets/javascripts/reports/accessibility_report/store/index.js16
-rw-r--r--app/assets/javascripts/reports/accessibility_report/store/mutation_types.js5
-rw-r--r--app/assets/javascripts/reports/accessibility_report/store/mutations.js20
-rw-r--r--app/assets/javascripts/reports/accessibility_report/store/state.js28
-rw-r--r--app/assets/javascripts/reports/components/grouped_issues_list.vue93
-rw-r--r--app/assets/javascripts/reports/components/grouped_test_reports_app.vue15
-rw-r--r--app/assets/javascripts/reports/components/issue_status_icon.vue3
-rw-r--r--app/assets/javascripts/reports/components/report_section.vue2
-rw-r--r--app/assets/javascripts/reports/constants.js3
-rw-r--r--app/assets/javascripts/reports/store/mutations.js3
-rw-r--r--app/assets/javascripts/repository/components/breadcrumbs.vue2
-rw-r--r--app/assets/javascripts/repository/components/last_commit.vue5
-rw-r--r--app/assets/javascripts/repository/components/table/row.vue5
-rw-r--r--app/assets/javascripts/repository/queries/commit.fragment.graphql1
-rw-r--r--app/assets/javascripts/repository/queries/pathLastCommit.query.graphql1
-rw-r--r--app/assets/javascripts/repository/router.js21
-rw-r--r--app/assets/javascripts/repository/utils/commit.js1
-rw-r--r--app/assets/javascripts/right_sidebar.js3
-rw-r--r--app/assets/javascripts/search_autocomplete.js4
-rw-r--r--app/assets/javascripts/serverless/components/area.vue4
-rw-r--r--app/assets/javascripts/serverless/event_hub.js4
-rw-r--r--app/assets/javascripts/set_status_modal/event_hub.js4
-rw-r--r--app/assets/javascripts/sidebar/components/assignees/assignees.vue4
-rw-r--r--app/assets/javascripts/sidebar/components/assignees/assignees_realtime.vue75
-rw-r--r--app/assets/javascripts/sidebar/components/assignees/sidebar_assignees.vue24
-rw-r--r--app/assets/javascripts/sidebar/components/assignees/uncollapsed_assignee_list.vue2
-rw-r--r--app/assets/javascripts/sidebar/components/confidential/confidential_issue_sidebar.vue23
-rw-r--r--app/assets/javascripts/sidebar/components/confidential/edit_form_buttons.vue7
-rw-r--r--app/assets/javascripts/sidebar/components/participants/participants.vue18
-rw-r--r--app/assets/javascripts/sidebar/lib/sidebar_move_issue.js4
-rw-r--r--app/assets/javascripts/sidebar/mount_sidebar.js21
-rw-r--r--app/assets/javascripts/sidebar/stores/sidebar_store.js4
-rw-r--r--app/assets/javascripts/snippet/snippet_bundle.js24
-rw-r--r--app/assets/javascripts/snippets/components/edit.vue18
-rw-r--r--app/assets/javascripts/snippets/components/snippet_blob_edit.vue6
-rw-r--r--app/assets/javascripts/snippets/components/snippet_blob_view.vue36
-rw-r--r--app/assets/javascripts/snippets/components/snippet_description_edit.vue4
-rw-r--r--app/assets/javascripts/snippets/components/snippet_description_view.vue21
-rw-r--r--app/assets/javascripts/snippets/components/snippet_header.vue40
-rw-r--r--app/assets/javascripts/snippets/components/snippet_title.vue10
-rw-r--r--app/assets/javascripts/snippets/fragments/snippetBase.fragment.graphql2
-rw-r--r--app/assets/javascripts/static_site_editor/components/app.vue3
-rw-r--r--app/assets/javascripts/static_site_editor/components/edit_area.vue51
-rw-r--r--app/assets/javascripts/static_site_editor/components/publish_toolbar.vue15
-rw-r--r--app/assets/javascripts/static_site_editor/components/saved_changes_message.vue7
-rw-r--r--app/assets/javascripts/static_site_editor/components/skeleton_loader.vue19
-rw-r--r--app/assets/javascripts/static_site_editor/components/static_site_editor.vue95
-rw-r--r--app/assets/javascripts/static_site_editor/constants.js7
-rw-r--r--app/assets/javascripts/static_site_editor/graphql/index.js39
-rw-r--r--app/assets/javascripts/static_site_editor/graphql/mutations/submit_content_changes.mutation.graphql7
-rw-r--r--app/assets/javascripts/static_site_editor/graphql/queries/app_data.query.graphql9
-rw-r--r--app/assets/javascripts/static_site_editor/graphql/queries/saved_content_meta.query.graphql3
-rw-r--r--app/assets/javascripts/static_site_editor/graphql/queries/source_content.query.graphql9
-rw-r--r--app/assets/javascripts/static_site_editor/graphql/resolvers/file.js11
-rw-r--r--app/assets/javascripts/static_site_editor/graphql/resolvers/submit_content_changes.js24
-rw-r--r--app/assets/javascripts/static_site_editor/graphql/typedefs.graphql43
-rw-r--r--app/assets/javascripts/static_site_editor/index.js33
-rw-r--r--app/assets/javascripts/static_site_editor/pages/home.vue120
-rw-r--r--app/assets/javascripts/static_site_editor/pages/success.vue35
-rw-r--r--app/assets/javascripts/static_site_editor/router/constants.js2
-rw-r--r--app/assets/javascripts/static_site_editor/router/index.js15
-rw-r--r--app/assets/javascripts/static_site_editor/router/routes.js21
-rw-r--r--app/assets/javascripts/static_site_editor/services/submit_content_changes.js15
-rw-r--r--app/assets/javascripts/static_site_editor/store/actions.js37
-rw-r--r--app/assets/javascripts/static_site_editor/store/getters.js2
-rw-r--r--app/assets/javascripts/static_site_editor/store/index.js19
-rw-r--r--app/assets/javascripts/static_site_editor/store/mutation_types.js8
-rw-r--r--app/assets/javascripts/static_site_editor/store/mutations.js36
-rw-r--r--app/assets/javascripts/static_site_editor/store/state.js23
-rw-r--r--app/assets/javascripts/syntax_highlight.js13
-rw-r--r--app/assets/javascripts/terminal/terminal.js13
-rw-r--r--app/assets/javascripts/tracking.js47
-rw-r--r--app/assets/javascripts/tree.js5
-rw-r--r--app/assets/javascripts/users_select.js774
-rw-r--r--app/assets/javascripts/users_select/constants.js18
-rw-r--r--app/assets/javascripts/users_select/index.js764
-rw-r--r--app/assets/javascripts/users_select/utils.js27
-rw-r--r--app/assets/javascripts/vue_merge_request_widget/components/deployment/deployment_info.vue2
-rw-r--r--app/assets/javascripts/vue_merge_request_widget/components/mr_collapsible_extension.vue2
-rw-r--r--app/assets/javascripts/vue_merge_request_widget/components/mr_widget_header.vue4
-rw-r--r--app/assets/javascripts/vue_merge_request_widget/components/mr_widget_pipeline.vue15
-rw-r--r--app/assets/javascripts/vue_merge_request_widget/components/mr_widget_pipeline_container.vue3
-rw-r--r--app/assets/javascripts/vue_merge_request_widget/components/mr_widget_terraform_plan.vue19
-rw-r--r--app/assets/javascripts/vue_merge_request_widget/components/states/commits_header.vue4
-rw-r--r--app/assets/javascripts/vue_merge_request_widget/components/states/mr_widget_auto_merge_enabled.vue3
-rw-r--r--app/assets/javascripts/vue_merge_request_widget/components/states/mr_widget_closed.vue2
-rw-r--r--app/assets/javascripts/vue_merge_request_widget/components/states/mr_widget_conflicts.vue4
-rw-r--r--app/assets/javascripts/vue_merge_request_widget/components/states/mr_widget_merged.vue2
-rw-r--r--app/assets/javascripts/vue_merge_request_widget/components/states/mr_widget_pipeline_tour.vue21
-rw-r--r--app/assets/javascripts/vue_merge_request_widget/components/states/ready_to_merge.vue25
-rw-r--r--app/assets/javascripts/vue_merge_request_widget/components/states/squash_before_merge.vue2
-rw-r--r--app/assets/javascripts/vue_merge_request_widget/event_hub.js4
-rw-r--r--app/assets/javascripts/vue_merge_request_widget/mixins/ready_to_merge.js6
-rw-r--r--app/assets/javascripts/vue_merge_request_widget/mr_widget_options.vue19
-rw-r--r--app/assets/javascripts/vue_merge_request_widget/stores/get_state_key.js2
-rw-r--r--app/assets/javascripts/vue_merge_request_widget/stores/mr_widget_store.js16
-rw-r--r--app/assets/javascripts/vue_shared/components/awards_list.vue17
-rw-r--r--app/assets/javascripts/vue_shared/components/blob_viewers/rich_viewer.vue6
-rw-r--r--app/assets/javascripts/vue_shared/components/blob_viewers/simple_viewer.vue3
-rw-r--r--app/assets/javascripts/vue_shared/components/ci_icon.vue2
-rw-r--r--app/assets/javascripts/vue_shared/components/clipboard_button.vue1
-rw-r--r--app/assets/javascripts/vue_shared/components/clone_dropdown.vue16
-rw-r--r--app/assets/javascripts/vue_shared/components/code_block.vue21
-rw-r--r--app/assets/javascripts/vue_shared/components/commit.vue2
-rw-r--r--app/assets/javascripts/vue_shared/components/content_viewer/content_viewer.vue12
-rw-r--r--app/assets/javascripts/vue_shared/components/content_viewer/lib/viewer_utils.js4
-rw-r--r--app/assets/javascripts/vue_shared/components/content_viewer/viewers/markdown_viewer.vue30
-rw-r--r--app/assets/javascripts/vue_shared/components/date_time_picker/date_time_picker.vue16
-rw-r--r--app/assets/javascripts/vue_shared/components/file_finder/item.vue4
-rw-r--r--app/assets/javascripts/vue_shared/components/file_icon/file_icon_map.js2
-rw-r--r--app/assets/javascripts/vue_shared/components/file_row.vue13
-rw-r--r--app/assets/javascripts/vue_shared/components/form/title.vue1
-rw-r--r--app/assets/javascripts/vue_shared/components/gl_mentions.vue2
-rw-r--r--app/assets/javascripts/vue_shared/components/identicon.vue2
-rw-r--r--app/assets/javascripts/vue_shared/components/issue/issue_warning.vue6
-rw-r--r--app/assets/javascripts/vue_shared/components/issue/related_issuable_item.vue40
-rw-r--r--app/assets/javascripts/vue_shared/components/markdown/field.vue4
-rw-r--r--app/assets/javascripts/vue_shared/components/markdown/field_view.vue19
-rw-r--r--app/assets/javascripts/vue_shared/components/recaptcha_eventhub.js4
-rw-r--r--app/assets/javascripts/vue_shared/components/rich_content_editor/constants.js37
-rw-r--r--app/assets/javascripts/vue_shared/components/rich_content_editor/rich_content_editor.vue65
-rw-r--r--app/assets/javascripts/vue_shared/components/rich_content_editor/toolbar_item.vue20
-rw-r--r--app/assets/javascripts/vue_shared/components/rich_content_editor/toolbar_service.js32
-rw-r--r--app/assets/javascripts/vue_shared/components/sidebar/labels_select/base.vue7
-rw-r--r--app/assets/javascripts/vue_shared/components/sidebar/labels_select/dropdown_button.vue6
-rw-r--r--app/assets/javascripts/vue_shared/components/sidebar/labels_select/dropdown_value.vue6
-rw-r--r--app/assets/javascripts/vue_shared/components/sidebar/labels_select_vue/constants.js5
-rw-r--r--app/assets/javascripts/vue_shared/components/sidebar/labels_select_vue/dropdown_button.vue28
-rw-r--r--app/assets/javascripts/vue_shared/components/sidebar/labels_select_vue/dropdown_contents_create_view.vue44
-rw-r--r--app/assets/javascripts/vue_shared/components/sidebar/labels_select_vue/dropdown_contents_labels_view.vue66
-rw-r--r--app/assets/javascripts/vue_shared/components/sidebar/labels_select_vue/dropdown_value.vue8
-rw-r--r--app/assets/javascripts/vue_shared/components/sidebar/labels_select_vue/label_item.vue52
-rw-r--r--app/assets/javascripts/vue_shared/components/sidebar/labels_select_vue/labels_select_root.vue73
-rw-r--r--app/assets/javascripts/vue_shared/components/sidebar/labels_select_vue/store/getters.js22
-rw-r--r--app/assets/javascripts/vue_shared/components/sidebar/labels_select_vue/store/mutations.js26
-rw-r--r--app/assets/javascripts/vue_shared/components/sidebar/labels_select_vue/store/state.js4
-rw-r--r--app/assets/javascripts/vue_shared/components/user_avatar/badges/gitlab_team_member_badge.vue27
-rw-r--r--app/assets/javascripts/vue_shared/components/user_avatar/user_avatar_svg.vue38
-rw-r--r--app/assets/javascripts/vue_shared/mixins/ci_pagination_api_mixin.js29
-rw-r--r--app/assets/stylesheets/application.scss8
-rw-r--r--app/assets/stylesheets/bootstrap_migration.scss2
-rw-r--r--app/assets/stylesheets/components/dashboard_skeleton.scss2
-rw-r--r--app/assets/stylesheets/components/design_management/design.scss140
-rw-r--r--app/assets/stylesheets/components/design_management/design_list_item.scss19
-rw-r--r--app/assets/stylesheets/components/design_management/design_version_dropdown.scss3
-rw-r--r--app/assets/stylesheets/components/milestone_combobox.scss13
-rw-r--r--app/assets/stylesheets/components/related_items_list.scss123
-rw-r--r--app/assets/stylesheets/components/rich_content_editor.scss11
-rw-r--r--app/assets/stylesheets/framework/animations.scss2
-rw-r--r--app/assets/stylesheets/framework/buttons.scss4
-rw-r--r--app/assets/stylesheets/framework/common.scss59
-rw-r--r--app/assets/stylesheets/framework/dropdowns.scss21
-rw-r--r--app/assets/stylesheets/framework/header.scss1
-rw-r--r--app/assets/stylesheets/framework/secondary_navigation_elements.scss10
-rw-r--r--app/assets/stylesheets/framework/tables.scss23
-rw-r--r--app/assets/stylesheets/framework/typography.scss13
-rw-r--r--app/assets/stylesheets/framework/variables.scss8
-rw-r--r--app/assets/stylesheets/page_bundles/_ide_mixins.scss1
-rw-r--r--app/assets/stylesheets/page_bundles/_ide_monaco_overrides.scss23
-rw-r--r--app/assets/stylesheets/page_bundles/_ide_theme_overrides.scss308
-rw-r--r--app/assets/stylesheets/page_bundles/ide.scss191
-rw-r--r--app/assets/stylesheets/page_bundles/ide_themes/README.md53
-rw-r--r--app/assets/stylesheets/page_bundles/ide_themes/_dark.scss50
-rw-r--r--app/assets/stylesheets/pages/alert_management/details.scss42
-rw-r--r--app/assets/stylesheets/pages/alert_management/list.scss83
-rw-r--r--app/assets/stylesheets/pages/alert_management/severity-icons.scss26
-rw-r--r--app/assets/stylesheets/pages/boards.scss24
-rw-r--r--app/assets/stylesheets/pages/commits.scss12
-rw-r--r--app/assets/stylesheets/pages/cycle_analytics.scss1
-rw-r--r--app/assets/stylesheets/pages/error_list.scss2
-rw-r--r--app/assets/stylesheets/pages/issuable.scss13
-rw-r--r--app/assets/stylesheets/pages/issues.scss10
-rw-r--r--app/assets/stylesheets/pages/labels.scss5
-rw-r--r--app/assets/stylesheets/pages/milestone.scss4
-rw-r--r--app/assets/stylesheets/pages/notes.scss24
-rw-r--r--app/assets/stylesheets/pages/pipeline_schedules.scss5
-rw-r--r--app/assets/stylesheets/pages/prometheus.scss14
-rw-r--r--app/assets/stylesheets/pages/settings.scss10
-rw-r--r--app/assets/stylesheets/snippets.scss6
-rw-r--r--app/assets/stylesheets/utilities.scss34
-rw-r--r--app/channels/application_cable/channel.rb6
-rw-r--r--app/channels/application_cable/connection.rb22
-rw-r--r--app/channels/issues_channel.rb13
-rw-r--r--app/controllers/admin/appearances_controller.rb1
-rw-r--r--app/controllers/admin/application_settings_controller.rb4
-rw-r--r--app/controllers/admin/ci/variables_controller.rb48
-rw-r--r--app/controllers/admin/dashboard_controller.rb1
-rw-r--r--app/controllers/admin/logs_controller.rb24
-rw-r--r--app/controllers/admin/sessions_controller.rb1
-rw-r--r--app/controllers/admin/users_controller.rb2
-rw-r--r--app/controllers/application_controller.rb67
-rw-r--r--app/controllers/boards/issues_controller.rb3
-rw-r--r--app/controllers/clusters/applications_controller.rb2
-rw-r--r--app/controllers/clusters/clusters_controller.rb40
-rw-r--r--app/controllers/concerns/boards_actions.rb3
-rw-r--r--app/controllers/concerns/impersonation.rb43
-rw-r--r--app/controllers/concerns/issuable_actions.rb3
-rw-r--r--app/controllers/concerns/issuable_collections_action.rb4
-rw-r--r--app/controllers/concerns/known_sign_in.rb31
-rw-r--r--app/controllers/concerns/members_presentation.rb1
-rw-r--r--app/controllers/concerns/metrics_dashboard.rb29
-rw-r--r--app/controllers/concerns/notes_actions.rb2
-rw-r--r--app/controllers/concerns/preview_markdown.rb2
-rw-r--r--app/controllers/concerns/record_user_last_activity.rb1
-rw-r--r--app/controllers/concerns/renders_ldap_servers.rb19
-rw-r--r--app/controllers/concerns/service_params.rb1
-rw-r--r--app/controllers/concerns/snippets_actions.rb15
-rw-r--r--app/controllers/concerns/spammable_actions.rb2
-rw-r--r--app/controllers/dashboard/projects_controller.rb6
-rw-r--r--app/controllers/dashboard_controller.rb1
-rw-r--r--app/controllers/google_api/authorizations_controller.rb3
-rw-r--r--app/controllers/graphql_controller.rb14
-rw-r--r--app/controllers/groups/group_links_controller.rb5
-rw-r--r--app/controllers/groups/group_members_controller.rb47
-rw-r--r--app/controllers/groups/registry/repositories_controller.rb4
-rw-r--r--app/controllers/groups/settings/repository_controller.rb2
-rw-r--r--app/controllers/groups_controller.rb6
-rw-r--r--app/controllers/help_controller.rb4
-rw-r--r--app/controllers/ide_controller.rb4
-rw-r--r--app/controllers/import/github_controller.rb2
-rw-r--r--app/controllers/import/google_code_controller.rb4
-rw-r--r--app/controllers/jwt_controller.rb11
-rw-r--r--app/controllers/ldap/omniauth_callbacks_controller.rb4
-rw-r--r--app/controllers/omniauth_callbacks_controller.rb11
-rw-r--r--app/controllers/projects/alert_management_controller.rb16
-rw-r--r--app/controllers/projects/artifacts_controller.rb15
-rw-r--r--app/controllers/projects/branches_controller.rb1
-rw-r--r--app/controllers/projects/ci/daily_build_group_report_results_controller.rb77
-rw-r--r--app/controllers/projects/design_management/designs/raw_images_controller.rb30
-rw-r--r--app/controllers/projects/design_management/designs/resized_image_controller.rb46
-rw-r--r--app/controllers/projects/design_management/designs_controller.rb21
-rw-r--r--app/controllers/projects/environments_controller.rb15
-rw-r--r--app/controllers/projects/graphs_controller.rb22
-rw-r--r--app/controllers/projects/import/jira_controller.rb2
-rw-r--r--app/controllers/projects/issues_controller.rb23
-rw-r--r--app/controllers/projects/mattermosts_controller.rb2
-rw-r--r--app/controllers/projects/merge_requests/creations_controller.rb8
-rw-r--r--app/controllers/projects/merge_requests/diffs_controller.rb2
-rw-r--r--app/controllers/projects/merge_requests_controller.rb23
-rw-r--r--app/controllers/projects/pipelines_controller.rb47
-rw-r--r--app/controllers/projects/project_members_controller.rb14
-rw-r--r--app/controllers/projects/refs_controller.rb39
-rw-r--r--app/controllers/projects/registry/repositories_controller.rb3
-rw-r--r--app/controllers/projects/settings/access_tokens_controller.rb71
-rw-r--r--app/controllers/projects/settings/repository_controller.rb2
-rw-r--r--app/controllers/projects/snippets_controller.rb11
-rw-r--r--app/controllers/projects/tree_controller.rb2
-rw-r--r--app/controllers/projects/usage_ping_controller.rb6
-rw-r--r--app/controllers/projects/wikis_controller.rb27
-rw-r--r--app/controllers/projects_controller.rb12
-rw-r--r--app/controllers/registrations_controller.rb3
-rw-r--r--app/controllers/repositories/git_http_controller.rb8
-rw-r--r--app/controllers/search_controller.rb11
-rw-r--r--app/controllers/sessions_controller.rb14
-rw-r--r--app/controllers/snippets_controller.rb25
-rw-r--r--app/controllers/user_callouts_controller.rb2
-rw-r--r--app/finders/alert_management/alerts_finder.rb55
-rw-r--r--app/finders/artifacts_finder.rb24
-rw-r--r--app/finders/ci/daily_build_group_report_results_finder.rb37
-rw-r--r--app/finders/ci/job_artifacts_finder.rb26
-rw-r--r--app/finders/clusters/knative_services_finder.rb1
-rw-r--r--app/finders/container_repositories_finder.rb13
-rw-r--r--app/finders/design_management/designs_finder.rb57
-rw-r--r--app/finders/design_management/versions_finder.rb58
-rw-r--r--app/finders/freeze_periods_finder.rb14
-rw-r--r--app/finders/group_members_finder.rb2
-rw-r--r--app/finders/issuable_finder.rb133
-rw-r--r--app/finders/issuable_finder/params.rb2
-rw-r--r--app/finders/issues_finder/params.rb2
-rw-r--r--app/finders/members_finder.rb16
-rw-r--r--app/finders/metrics/users_starred_dashboards_finder.rb35
-rw-r--r--app/finders/projects/serverless/functions_finder.rb1
-rw-r--r--app/finders/projects_finder.rb4
-rw-r--r--app/finders/releases_finder.rb20
-rw-r--r--app/finders/todos_finder.rb2
-rw-r--r--app/graphql/mutations/alert_management/base.rb40
-rw-r--r--app/graphql/mutations/alert_management/create_alert_issue.rb30
-rw-r--r--app/graphql/mutations/alert_management/update_alert_status.rb35
-rw-r--r--app/graphql/mutations/base_mutation.rb2
-rw-r--r--app/graphql/mutations/branches/create.rb51
-rw-r--r--app/graphql/mutations/design_management/base.rb23
-rw-r--r--app/graphql/mutations/design_management/delete.rb66
-rw-r--r--app/graphql/mutations/design_management/upload.rb38
-rw-r--r--app/graphql/mutations/metrics/dashboard/annotations/create.rb106
-rw-r--r--app/graphql/mutations/snippets/base.rb2
-rw-r--r--app/graphql/mutations/snippets/create.rb9
-rw-r--r--app/graphql/resolvers/alert_management/alert_status_counts_resolver.rb13
-rw-r--r--app/graphql/resolvers/alert_management_alert_resolver.rb31
-rw-r--r--app/graphql/resolvers/board_lists_resolver.rb38
-rw-r--r--app/graphql/resolvers/branch_commit_resolver.rb17
-rw-r--r--app/graphql/resolvers/design_management/design_at_version_resolver.rb46
-rw-r--r--app/graphql/resolvers/design_management/design_resolver.rb57
-rw-r--r--app/graphql/resolvers/design_management/designs_resolver.rb50
-rw-r--r--app/graphql/resolvers/design_management/version/design_at_version_resolver.rb95
-rw-r--r--app/graphql/resolvers/design_management/version/designs_at_version_resolver.rb60
-rw-r--r--app/graphql/resolvers/design_management/version_in_collection_resolver.rb45
-rw-r--r--app/graphql/resolvers/design_management/version_resolver.rb25
-rw-r--r--app/graphql/resolvers/design_management/versions_resolver.rb76
-rw-r--r--app/graphql/resolvers/issues_resolver.rb18
-rw-r--r--app/graphql/resolvers/metrics/dashboards/annotation_resolver.rb1
-rw-r--r--app/graphql/resolvers/milestone_resolver.rb31
-rw-r--r--app/graphql/resolvers/namespace_projects_resolver.rb2
-rw-r--r--app/graphql/resolvers/projects_resolver.rb31
-rw-r--r--app/graphql/resolvers/release_resolver.rb25
-rw-r--r--app/graphql/resolvers/releases_resolver.rb21
-rw-r--r--app/graphql/types/alert_management/alert_sort_enum.rb25
-rw-r--r--app/graphql/types/alert_management/alert_status_counts_type.rb30
-rw-r--r--app/graphql/types/alert_management/alert_type.rb88
-rw-r--r--app/graphql/types/alert_management/severity_enum.rb14
-rw-r--r--app/graphql/types/alert_management/status_enum.rb14
-rw-r--r--app/graphql/types/board_list_type.rb26
-rw-r--r--app/graphql/types/board_type.rb7
-rw-r--r--app/graphql/types/branch_type.rb18
-rw-r--r--app/graphql/types/commit_type.rb1
-rw-r--r--app/graphql/types/design_management/design_at_version_type.rb37
-rw-r--r--app/graphql/types/design_management/design_collection_type.rb44
-rw-r--r--app/graphql/types/design_management/design_fields.rb78
-rw-r--r--app/graphql/types/design_management/design_type.rb44
-rw-r--r--app/graphql/types/design_management/design_version_event_enum.rb18
-rw-r--r--app/graphql/types/design_management/version_type.rb37
-rw-r--r--app/graphql/types/design_management_type.rb18
-rw-r--r--app/graphql/types/grafana_integration_type.rb2
-rw-r--r--app/graphql/types/issuable_sort_enum.rb7
-rw-r--r--app/graphql/types/issue_sort_enum.rb6
-rw-r--r--app/graphql/types/issue_type.rb8
-rw-r--r--app/graphql/types/jira_import_type.rb5
-rw-r--r--app/graphql/types/metrics/dashboard_type.rb3
-rw-r--r--app/graphql/types/metrics/dashboards/annotation_type.rb4
-rw-r--r--app/graphql/types/mutation_type.rb6
-rw-r--r--app/graphql/types/notes/noteable_type.rb4
-rw-r--r--app/graphql/types/permission_types/issue.rb8
-rw-r--r--app/graphql/types/permission_types/project.rb4
-rw-r--r--app/graphql/types/project_type.rb32
-rw-r--r--app/graphql/types/query_type.rb16
-rw-r--r--app/graphql/types/release_type.rb47
-rw-r--r--app/graphql/types/snippet_type.rb2
-rw-r--r--app/graphql/types/snippets/blob_type.rb10
-rw-r--r--app/graphql/types/snippets/blob_viewer_type.rb6
-rw-r--r--app/graphql/types/todo_target_enum.rb1
-rw-r--r--app/graphql/types/todo_type.rb2
-rw-r--r--app/graphql/types/user_type.rb4
-rw-r--r--app/helpers/access_tokens_helper.rb7
-rw-r--r--app/helpers/appearances_helper.rb4
-rw-r--r--app/helpers/application_helper.rb4
-rw-r--r--app/helpers/application_settings_helper.rb13
-rw-r--r--app/helpers/auth_helper.rb6
-rw-r--r--app/helpers/blob_helper.rb21
-rw-r--r--app/helpers/boards_helper.rb3
-rw-r--r--app/helpers/button_helper.rb4
-rw-r--r--app/helpers/clusters_helper.rb11
-rw-r--r--app/helpers/commits_helper.rb2
-rw-r--r--app/helpers/environment_helper.rb2
-rw-r--r--app/helpers/environments_helper.rb89
-rw-r--r--app/helpers/events_helper.rb14
-rw-r--r--app/helpers/export_helper.rb13
-rw-r--r--app/helpers/form_helper.rb4
-rw-r--r--app/helpers/groups_helper.rb4
-rw-r--r--app/helpers/icons_helper.rb2
-rw-r--r--app/helpers/issuables_helper.rb28
-rw-r--r--app/helpers/issues_helper.rb16
-rw-r--r--app/helpers/members_helper.rb13
-rw-r--r--app/helpers/milestones_helper.rb2
-rw-r--r--app/helpers/namespaces_helper.rb4
-rw-r--r--app/helpers/nav_helper.rb5
-rw-r--r--app/helpers/preferences_helper.rb2
-rw-r--r--app/helpers/projects/alert_management_helper.rb21
-rw-r--r--app/helpers/projects_helper.rb19
-rw-r--r--app/helpers/releases_helper.rb4
-rw-r--r--app/helpers/search_helper.rb6
-rw-r--r--app/helpers/services_helper.rb12
-rw-r--r--app/helpers/snippets_helper.rb76
-rw-r--r--app/helpers/sorting_helper.rb4
-rw-r--r--app/helpers/system_note_helper.rb8
-rw-r--r--app/helpers/todos_helper.rb19
-rw-r--r--app/helpers/workhorse_helper.rb4
-rw-r--r--app/helpers/x509_helper.rb4
-rw-r--r--app/mailers/emails/groups.rb19
-rw-r--r--app/mailers/emails/notes.rb12
-rw-r--r--app/mailers/emails/profile.rb10
-rw-r--r--app/mailers/notify.rb1
-rw-r--r--app/mailers/previews/notify_preview.rb4
-rw-r--r--app/models/active_session.rb4
-rw-r--r--app/models/alert_management/alert.rb146
-rw-r--r--app/models/appearance.rb3
-rw-r--r--app/models/application_setting.rb12
-rw-r--r--app/models/application_setting_implementation.rb5
-rw-r--r--app/models/blob.rb6
-rw-r--r--app/models/blob_viewer/dependency_manager.rb2
-rw-r--r--app/models/broadcast_message.rb5
-rw-r--r--app/models/ci/bridge.rb4
-rw-r--r--app/models/ci/build.rb67
-rw-r--r--app/models/ci/daily_build_group_report_result.rb20
-rw-r--r--app/models/ci/daily_report_result.rb22
-rw-r--r--app/models/ci/freeze_period.rb18
-rw-r--r--app/models/ci/freeze_period_status.rb47
-rw-r--r--app/models/ci/group.rb2
-rw-r--r--app/models/ci/instance_variable.rb76
-rw-r--r--app/models/ci/job_artifact.rb61
-rw-r--r--app/models/ci/legacy_stage.rb4
-rw-r--r--app/models/ci/persistent_ref.rb12
-rw-r--r--app/models/ci/pipeline.rb55
-rw-r--r--app/models/ci/pipeline_schedule.rb4
-rw-r--r--app/models/ci/processable.rb12
-rw-r--r--app/models/ci/stage.rb4
-rw-r--r--app/models/clusters/applications/elastic_stack.rb47
-rw-r--r--app/models/clusters/applications/fluentd.rb20
-rw-r--r--app/models/clusters/applications/ingress.rb7
-rw-r--r--app/models/clusters/applications/jupyter.rb2
-rw-r--r--app/models/clusters/applications/knative.rb4
-rw-r--r--app/models/clusters/applications/runner.rb2
-rw-r--r--app/models/clusters/cluster.rb92
-rw-r--r--app/models/clusters/concerns/application_status.rb9
-rw-r--r--app/models/commit_status.rb12
-rw-r--r--app/models/concerns/async_devise_email.rb14
-rw-r--r--app/models/concerns/awardable.rb43
-rw-r--r--app/models/concerns/cache_markdown_field.rb1
-rw-r--r--app/models/concerns/ci/contextable.rb8
-rw-r--r--app/models/concerns/diff_positionable_note.rb4
-rw-r--r--app/models/concerns/has_repository.rb1
-rw-r--r--app/models/concerns/has_user_type.rb45
-rw-r--r--app/models/concerns/has_wiki.rb44
-rw-r--r--app/models/concerns/issuable.rb29
-rw-r--r--app/models/concerns/issue_resource_event.rb13
-rw-r--r--app/models/concerns/limitable.rb27
-rw-r--r--app/models/concerns/merge_request_resource_event.rb11
-rw-r--r--app/models/concerns/milestoneable.rb2
-rw-r--r--app/models/concerns/noteable.rb14
-rw-r--r--app/models/concerns/prometheus_adapter.rb1
-rw-r--r--app/models/concerns/protected_ref_access.rb4
-rw-r--r--app/models/concerns/reactive_caching.rb15
-rw-r--r--app/models/concerns/redis_cacheable.rb6
-rw-r--r--app/models/concerns/spammable.rb33
-rw-r--r--app/models/concerns/state_eventable.rb9
-rw-r--r--app/models/concerns/storage/legacy_project_wiki.rb11
-rw-r--r--app/models/concerns/timebox.rb204
-rw-r--r--app/models/concerns/update_project_statistics.rb14
-rw-r--r--app/models/container_repository.rb2
-rw-r--r--app/models/cycle_analytics/group_level.rb29
-rw-r--r--app/models/deploy_token.rb5
-rw-r--r--app/models/design_management.rb13
-rw-r--r--app/models/design_management/action.rb44
-rw-r--r--app/models/design_management/design.rb266
-rw-r--r--app/models/design_management/design_action.rb64
-rw-r--r--app/models/design_management/design_at_version.rb119
-rw-r--r--app/models/design_management/design_collection.rb30
-rw-r--r--app/models/design_management/repository.rb51
-rw-r--r--app/models/design_management/version.rb144
-rw-r--r--app/models/design_user_mention.rb6
-rw-r--r--app/models/diff_note.rb10
-rw-r--r--app/models/email.rb14
-rw-r--r--app/models/environment.rb9
-rw-r--r--app/models/epic.rb2
-rw-r--r--app/models/error_tracking/project_error_tracking_setting.rb1
-rw-r--r--app/models/event.rb15
-rw-r--r--app/models/global_milestone.rb5
-rw-r--r--app/models/group.rb35
-rw-r--r--app/models/group_import_state.rb34
-rw-r--r--app/models/group_milestone.rb3
-rw-r--r--app/models/hooks/project_hook.rb3
-rw-r--r--app/models/internal_id_enums.rb13
-rw-r--r--app/models/issue.rb22
-rw-r--r--app/models/iteration.rb100
-rw-r--r--app/models/jira_import_state.rb7
-rw-r--r--app/models/list.rb14
-rw-r--r--app/models/member.rb1
-rw-r--r--app/models/members/project_member.rb5
-rw-r--r--app/models/members_preloader.rb4
-rw-r--r--app/models/merge_request.rb54
-rw-r--r--app/models/merge_request_diff.rb30
-rw-r--r--app/models/metrics/users_starred_dashboard.rb18
-rw-r--r--app/models/milestone.rb195
-rw-r--r--app/models/milestone_note.rb2
-rw-r--r--app/models/namespace.rb25
-rw-r--r--app/models/namespace/root_storage_size.rb31
-rw-r--r--app/models/note.rb14
-rw-r--r--app/models/pages_domain.rb11
-rw-r--r--app/models/performance_monitoring/prometheus_dashboard.rb2
-rw-r--r--app/models/personal_access_token.rb21
-rw-r--r--app/models/personal_snippet.rb4
-rw-r--r--app/models/plan.rb42
-rw-r--r--app/models/plan_limits.rb23
-rw-r--r--app/models/project.rb127
-rw-r--r--app/models/project_authorization.rb3
-rw-r--r--app/models/project_ci_cd_setting.rb2
-rw-r--r--app/models/project_feature.rb2
-rw-r--r--app/models/project_repository_storage_move.rb58
-rw-r--r--app/models/project_services/chat_message/merge_message.rb4
-rw-r--r--app/models/project_services/chat_message/pipeline_message.rb48
-rw-r--r--app/models/project_services/jira_service.rb66
-rw-r--r--app/models/project_services/mattermost_slash_commands_service.rb4
-rw-r--r--app/models/project_services/mock_monitoring_service.rb2
-rw-r--r--app/models/project_services/webex_teams_service.rb57
-rw-r--r--app/models/project_services/youtrack_service.rb4
-rw-r--r--app/models/project_statistics.rb3
-rw-r--r--app/models/project_wiki.rb220
-rw-r--r--app/models/release.rb14
-rw-r--r--app/models/remote_mirror.rb26
-rw-r--r--app/models/repository.rb11
-rw-r--r--app/models/resource_label_event.rb6
-rw-r--r--app/models/resource_milestone_event.rb11
-rw-r--r--app/models/resource_state_event.rb15
-rw-r--r--app/models/resource_weight_event.rb4
-rw-r--r--app/models/sent_notification.rb4
-rw-r--r--app/models/service.rb14
-rw-r--r--app/models/snippet.rb31
-rw-r--r--app/models/snippet_repository.rb30
-rw-r--r--app/models/ssh_host_key.rb1
-rw-r--r--app/models/state_note.rb19
-rw-r--r--app/models/storage/hashed.rb1
-rw-r--r--app/models/system_note_metadata.rb1
-rw-r--r--app/models/timelog.rb4
-rw-r--r--app/models/todo.rb6
-rw-r--r--app/models/user.rb105
-rw-r--r--app/models/user_type_enums.rb13
-rw-r--r--app/models/wiki.rb233
-rw-r--r--app/models/wiki_page.rb59
-rw-r--r--app/models/wiki_page/meta.rb108
-rw-r--r--app/models/x509_certificate.rb6
-rw-r--r--app/models/x509_commit_signature.rb4
-rw-r--r--app/policies/alert_management/alert_policy.rb7
-rw-r--r--app/policies/ci/build_policy.rb10
-rw-r--r--app/policies/ci/freeze_period_policy.rb7
-rw-r--r--app/policies/concerns/policy_actor.rb37
-rw-r--r--app/policies/design_management/design_at_version_policy.rb8
-rw-r--r--app/policies/design_management/design_collection_policy.rb7
-rw-r--r--app/policies/design_management/design_policy.rb8
-rw-r--r--app/policies/design_management/version_policy.rb8
-rw-r--r--app/policies/global_policy.rb10
-rw-r--r--app/policies/group_policy.rb35
-rw-r--r--app/policies/issue_policy.rb16
-rw-r--r--app/policies/project_policy.rb101
-rw-r--r--app/policies/wiki_page_policy.rb2
-rw-r--r--app/presenters/README.md16
-rw-r--r--app/presenters/ci/build_runner_presenter.rb35
-rw-r--r--app/presenters/clusterable_presenter.rb4
-rw-r--r--app/presenters/commit_status_presenter.rb8
-rw-r--r--app/presenters/instance_clusterable_presenter.rb4
-rw-r--r--app/presenters/pages_domain_presenter.rb2
-rw-r--r--app/presenters/projects/prometheus/alert_presenter.rb61
-rw-r--r--app/presenters/projects/settings/deploy_keys_presenter.rb4
-rw-r--r--app/presenters/release_presenter.rb7
-rw-r--r--app/presenters/snippet_presenter.rb10
-rw-r--r--app/serializers/accessibility_error_entity.rb12
-rw-r--r--app/serializers/accessibility_reports_comparer_entity.rb15
-rw-r--r--app/serializers/accessibility_reports_comparer_serializer.rb5
-rw-r--r--app/serializers/analytics_summary_entity.rb4
-rw-r--r--app/serializers/ci/basic_variable_entity.rb13
-rw-r--r--app/serializers/ci/dag_job_entity.rb11
-rw-r--r--app/serializers/ci/dag_job_group_entity.rb9
-rw-r--r--app/serializers/ci/dag_pipeline_entity.rb20
-rw-r--r--app/serializers/ci/dag_pipeline_serializer.rb7
-rw-r--r--app/serializers/ci/dag_stage_entity.rb9
-rw-r--r--app/serializers/ci/instance_variable_serializer.rb7
-rw-r--r--app/serializers/cluster_application_entity.rb2
-rw-r--r--app/serializers/cluster_entity.rb9
-rw-r--r--app/serializers/cluster_serializer.rb15
-rw-r--r--app/serializers/diff_file_base_entity.rb23
-rw-r--r--app/serializers/diffs_entity.rb4
-rw-r--r--app/serializers/group_variable_entity.rb9
-rw-r--r--app/serializers/issuable_sidebar_basic_entity.rb2
-rw-r--r--app/serializers/issuable_sidebar_extras_entity.rb2
-rw-r--r--app/serializers/merge_request_assignee_entity.rb2
-rw-r--r--app/serializers/merge_request_poll_widget_entity.rb12
-rw-r--r--app/serializers/merge_request_serializer.rb2
-rw-r--r--app/serializers/note_user_entity.rb4
-rw-r--r--app/serializers/service_event_entity.rb44
-rw-r--r--app/serializers/service_event_serializer.rb5
-rw-r--r--app/serializers/test_suite_comparer_entity.rb2
-rw-r--r--app/serializers/test_suite_entity.rb3
-rw-r--r--app/serializers/variable_entity.rb9
-rw-r--r--app/services/alert_management/create_alert_issue_service.rb70
-rw-r--r--app/services/alert_management/process_prometheus_alert_service.rb86
-rw-r--r--app/services/alert_management/update_alert_status_service.rb63
-rw-r--r--app/services/audit_event_service.rb5
-rw-r--r--app/services/auth/container_registry_authentication_service.rb29
-rw-r--r--app/services/authorized_project_update/project_create_service.rb34
-rw-r--r--app/services/base_container_service.rb12
-rw-r--r--app/services/base_service.rb73
-rw-r--r--app/services/boards/issues/list_service.rb9
-rw-r--r--app/services/boards/lists/list_service.rb6
-rw-r--r--app/services/branches/create_service.rb2
-rw-r--r--app/services/ci/compare_accessibility_reports_service.rb17
-rw-r--r--app/services/ci/create_job_artifacts_service.rb17
-rw-r--r--app/services/ci/create_pipeline_service.rb21
-rw-r--r--app/services/ci/daily_build_group_report_result_service.rb40
-rw-r--r--app/services/ci/daily_report_result_service.rb39
-rw-r--r--app/services/ci/destroy_expired_job_artifacts_service.rb8
-rw-r--r--app/services/ci/generate_terraform_reports_service.rb29
-rw-r--r--app/services/ci/pipeline_processing/atomic_processing_service.rb2
-rw-r--r--app/services/ci/pipeline_processing/atomic_processing_service/status_collection.rb19
-rw-r--r--app/services/ci/pipeline_schedule_service.rb14
-rw-r--r--app/services/ci/process_pipeline_service.rb13
-rw-r--r--app/services/ci/register_job_service.rb4
-rw-r--r--app/services/ci/retry_build_service.rb5
-rw-r--r--app/services/ci/retry_pipeline_service.rb2
-rw-r--r--app/services/ci/update_instance_variables_service.rb72
-rw-r--r--app/services/clusters/applications/base_service.rb20
-rw-r--r--app/services/clusters/applications/check_installation_progress_service.rb2
-rw-r--r--app/services/clusters/applications/check_uninstall_progress_service.rb2
-rw-r--r--app/services/clusters/applications/check_upgrade_progress_service.rb2
-rw-r--r--app/services/clusters/applications/ingress_modsecurity_usage_service.rb69
-rw-r--r--app/services/clusters/applications/schedule_update_service.rb6
-rw-r--r--app/services/clusters/gcp/verify_provision_status_service.rb2
-rw-r--r--app/services/clusters/kubernetes/configure_istio_ingress_service.rb4
-rw-r--r--app/services/clusters/management/create_project_service.rb7
-rw-r--r--app/services/clusters/parse_cluster_applications_artifact_service.rb95
-rw-r--r--app/services/concerns/base_service_utility.rb72
-rw-r--r--app/services/concerns/git/logger.rb10
-rw-r--r--app/services/concerns/measurable.rb61
-rw-r--r--app/services/concerns/spam_check_methods.rb4
-rw-r--r--app/services/deployments/older_deployments_drop_service.rb4
-rw-r--r--app/services/design_management/delete_designs_service.rb66
-rw-r--r--app/services/design_management/design_service.rb31
-rw-r--r--app/services/design_management/design_user_notes_count_service.rb34
-rw-r--r--app/services/design_management/generate_image_versions_service.rb99
-rw-r--r--app/services/design_management/on_success_callbacks.rb23
-rw-r--r--app/services/design_management/runs_design_actions.rb35
-rw-r--r--app/services/design_management/save_designs_service.rb114
-rw-r--r--app/services/emails/base_service.rb2
-rw-r--r--app/services/event_create_service.rb28
-rw-r--r--app/services/git/branch_hooks_service.rb2
-rw-r--r--app/services/git/wiki_push_service.rb57
-rw-r--r--app/services/git/wiki_push_service/change.rb67
-rw-r--r--app/services/grafana/proxy_service.rb1
-rw-r--r--app/services/groups/create_service.rb4
-rw-r--r--app/services/groups/import_export/export_service.rb24
-rw-r--r--app/services/groups/import_export/import_service.rb34
-rw-r--r--app/services/groups/update_service.rb1
-rw-r--r--app/services/incident_management/create_issue_service.rb8
-rw-r--r--app/services/issuable/clone/attributes_rewriter.rb55
-rw-r--r--app/services/issuable/clone/base_service.rb2
-rw-r--r--app/services/issuable/common_system_notes_service.rb18
-rw-r--r--app/services/issuable_base_service.rb14
-rw-r--r--app/services/issues/build_service.rb10
-rw-r--r--app/services/issues/related_branches_service.rb20
-rw-r--r--app/services/issues/update_service.rb4
-rw-r--r--app/services/jira_import/start_import_service.rb2
-rw-r--r--app/services/lfs/file_transformer.rb3
-rw-r--r--app/services/members/request_access_service.rb2
-rw-r--r--app/services/merge_requests/base_service.rb31
-rw-r--r--app/services/merge_requests/rebase_service.rb8
-rw-r--r--app/services/merge_requests/refresh_service.rb4
-rw-r--r--app/services/merge_requests/squash_service.rb18
-rw-r--r--app/services/metrics/dashboard/base_service.rb2
-rw-r--r--app/services/metrics/dashboard/grafana_metric_embed_service.rb3
-rw-r--r--app/services/metrics/dashboard/transient_embed_service.rb8
-rw-r--r--app/services/metrics/users_starred_dashboards/create_service.rb74
-rw-r--r--app/services/metrics/users_starred_dashboards/delete_service.rb33
-rw-r--r--app/services/namespaces/check_storage_size_service.rb94
-rw-r--r--app/services/notes/post_process_service.rb8
-rw-r--r--app/services/notification_service.rb20
-rw-r--r--app/services/pages_domains/obtain_lets_encrypt_certificate_service.rb2
-rw-r--r--app/services/pod_logs/base_service.rb3
-rw-r--r--app/services/pod_logs/elasticsearch_service.rb10
-rw-r--r--app/services/pod_logs/kubernetes_service.rb9
-rw-r--r--app/services/post_receive_service.rb17
-rw-r--r--app/services/projects/alerting/notify_service.rb17
-rw-r--r--app/services/projects/container_repository/cleanup_tags_service.rb2
-rw-r--r--app/services/projects/create_service.rb32
-rw-r--r--app/services/projects/gitlab_projects_import_service.rb8
-rw-r--r--app/services/projects/hashed_storage/base_attachment_service.rb2
-rw-r--r--app/services/projects/hashed_storage/base_repository_service.rb28
-rw-r--r--app/services/projects/import_export/export_service.rb33
-rw-r--r--app/services/projects/import_service.rb25
-rw-r--r--app/services/projects/lfs_pointers/lfs_download_link_list_service.rb2
-rw-r--r--app/services/projects/lsif_data_service.rb2
-rw-r--r--app/services/projects/prometheus/alerts/notify_service.rb9
-rw-r--r--app/services/projects/propagate_service_template.rb54
-rw-r--r--app/services/projects/transfer_service.rb24
-rw-r--r--app/services/projects/update_remote_mirror_service.rb14
-rw-r--r--app/services/projects/update_repository_storage_service.rb69
-rw-r--r--app/services/prometheus/proxy_service.rb1
-rw-r--r--app/services/prometheus/proxy_variable_substitution_service.rb48
-rw-r--r--app/services/releases/create_service.rb6
-rw-r--r--app/services/resource_access_tokens/create_service.rb111
-rw-r--r--app/services/resource_access_tokens/revoke_service.rb65
-rw-r--r--app/services/resource_events/base_synthetic_notes_builder_service.rb2
-rw-r--r--app/services/resource_events/change_milestone_service.rb7
-rw-r--r--app/services/resources/create_access_token_service.rb111
-rw-r--r--app/services/search/snippet_service.rb2
-rw-r--r--app/services/search_service.rb19
-rw-r--r--app/services/snippets/base_service.rb41
-rw-r--r--app/services/snippets/create_service.rb40
-rw-r--r--app/services/snippets/update_service.rb47
-rw-r--r--app/services/spam/akismet_service.rb2
-rw-r--r--app/services/spam/spam_action_service.rb91
-rw-r--r--app/services/spam/spam_check_service.rb68
-rw-r--r--app/services/spam/spam_constants.rb9
-rw-r--r--app/services/spam/spam_verdict_service.rb26
-rw-r--r--app/services/system_note_service.rb28
-rw-r--r--app/services/system_notes/design_management_service.rb83
-rw-r--r--app/services/tags/destroy_service.rb14
-rw-r--r--app/services/template_engines/liquid_service.rb48
-rw-r--r--app/services/terraform/remote_state_handler.rb2
-rw-r--r--app/services/user_project_access_changed_service.rb13
-rw-r--r--app/services/users/migrate_to_ghost_user_service.rb6
-rw-r--r--app/services/verify_pages_domain_service.rb4
-rw-r--r--app/services/wiki_pages/base_service.rb13
-rw-r--r--app/services/wiki_pages/create_service.rb4
-rw-r--r--app/services/wiki_pages/event_create_service.rb30
-rw-r--r--app/services/wikis/create_attachment_service.rb11
-rw-r--r--app/uploaders/design_management/design_v432x230_uploader.rb45
-rw-r--r--app/validators/cron_freeze_period_timezone_validator.rb13
-rw-r--r--app/validators/cron_validator.rb15
-rw-r--r--app/views/admin/appearances/_form.html.haml35
-rw-r--r--app/views/admin/application_settings/_influx.html.haml60
-rw-r--r--app/views/admin/application_settings/_prometheus.html.haml6
-rw-r--r--app/views/admin/application_settings/_repository_mirrors_form.html.haml2
-rw-r--r--app/views/admin/application_settings/_repository_storage.html.haml4
-rw-r--r--app/views/admin/application_settings/_signup.html.haml27
-rw-r--r--app/views/admin/application_settings/_visibility_and_access.html.haml1
-rw-r--r--app/views/admin/application_settings/general.html.haml4
-rw-r--r--app/views/admin/application_settings/integrations.html.haml2
-rw-r--r--app/views/admin/application_settings/metrics_and_profiling.html.haml11
-rw-r--r--app/views/admin/impersonation_tokens/index.html.haml28
-rw-r--r--app/views/admin/logs/show.html.haml24
-rw-r--r--app/views/admin/projects/show.html.haml28
-rw-r--r--app/views/admin/services/_deprecated_message.html.haml3
-rw-r--r--app/views/admin/services/edit.html.haml2
-rw-r--r--app/views/admin/sessions/_new_base.html.haml2
-rw-r--r--app/views/admin/sessions/_signin_box.html.haml19
-rw-r--r--app/views/admin/sessions/_tabs_normal.html.haml3
-rw-r--r--app/views/admin/sessions/new.html.haml25
-rw-r--r--app/views/admin/sessions/two_factor.html.haml2
-rw-r--r--app/views/ci/status/_dropdown_graph_badge.html.haml4
-rw-r--r--app/views/ci/variables/_index.html.haml2
-rw-r--r--app/views/clusters/clusters/_cluster.html.haml3
-rw-r--r--app/views/clusters/clusters/index.html.haml2
-rw-r--r--app/views/clusters/clusters/show.html.haml1
-rw-r--r--app/views/dashboard/snippets/index.html.haml2
-rw-r--r--app/views/devise/registrations/new.html.haml2
-rw-r--r--app/views/devise/sessions/_new_ldap.html.haml8
-rw-r--r--app/views/devise/shared/_omniauth_box.html.haml4
-rw-r--r--app/views/devise/shared/_signin_box.html.haml2
-rw-r--r--app/views/devise/shared/_tabs_ldap.html.haml11
-rw-r--r--app/views/devise/shared/_tabs_normal.html.haml7
-rw-r--r--app/views/groups/_flash_messages.html.haml2
-rw-r--r--app/views/groups/_home_panel.html.haml46
-rw-r--r--app/views/groups/edit.html.haml4
-rw-r--r--app/views/groups/group_members/index.html.haml22
-rw-r--r--app/views/groups/settings/_advanced.html.haml2
-rw-r--r--app/views/groups/settings/_default_branch_protection.html.haml3
-rw-r--r--app/views/groups/settings/_export.html.haml28
-rw-r--r--app/views/groups/settings/_permissions.html.haml2
-rw-r--r--app/views/groups/settings/integrations/index.html.haml2
-rw-r--r--app/views/groups/settings/repository/show.html.haml2
-rw-r--r--app/views/groups/show.html.haml6
-rw-r--r--app/views/groups/sidebar/_packages.html.haml4
-rw-r--r--app/views/help/_shortcuts.html.haml162
-rw-r--r--app/views/import/google_code/new_user_map.html.haml2
-rw-r--r--app/views/kaminari/gitlab/_page.html.haml2
-rw-r--r--app/views/layouts/_head.html.haml1
-rw-r--r--app/views/layouts/_page.html.haml1
-rw-r--r--app/views/layouts/devise.html.haml4
-rw-r--r--app/views/layouts/header/_current_user_dropdown.html.haml1
-rw-r--r--app/views/layouts/header/_help_dropdown.html.haml1
-rw-r--r--app/views/layouts/nav/sidebar/_admin.html.haml11
-rw-r--r--app/views/layouts/nav/sidebar/_group.html.haml2
-rw-r--r--app/views/layouts/nav/sidebar/_profile.html.haml4
-rw-r--r--app/views/layouts/nav/sidebar/_project.html.haml16
-rw-r--r--app/views/layouts/nav/sidebar/_project_packages_link.html.haml4
-rw-r--r--app/views/notify/group_was_exported_email.html.haml9
-rw-r--r--app/views/notify/group_was_exported_email.text.erb6
-rw-r--r--app/views/notify/group_was_not_exported_email.html.haml10
-rw-r--r--app/views/notify/group_was_not_exported_email.text.erb7
-rw-r--r--app/views/notify/issues_csv_email.html.haml9
-rw-r--r--app/views/notify/issues_csv_email.text.erb4
-rw-r--r--app/views/notify/note_design_email.html.haml1
-rw-r--r--app/views/notify/note_design_email.text.erb1
-rw-r--r--app/views/notify/unknown_sign_in_email.html.haml14
-rw-r--r--app/views/notify/unknown_sign_in_email.text.haml10
-rw-r--r--app/views/profiles/keys/_form.html.haml2
-rw-r--r--app/views/profiles/personal_access_tokens/index.html.haml24
-rw-r--r--app/views/profiles/show.html.haml5
-rw-r--r--app/views/projects/alert_management/details.html.haml4
-rw-r--r--app/views/projects/alert_management/index.html.haml3
-rw-r--r--app/views/projects/blob/_header.html.haml8
-rw-r--r--app/views/projects/blob/_template_selectors.html.haml2
-rw-r--r--app/views/projects/buttons/_download.html.haml15
-rw-r--r--app/views/projects/commit/_signature.html.haml2
-rw-r--r--app/views/projects/commit/_signature_badge.html.haml6
-rw-r--r--app/views/projects/commit/x509/_signature_badge_user.html.haml2
-rw-r--r--app/views/projects/commits/_commit.html.haml2
-rw-r--r--app/views/projects/cycle_analytics/show.html.haml5
-rw-r--r--app/views/projects/deploy_keys/_form.html.haml24
-rw-r--r--app/views/projects/deploy_keys/_index.html.haml15
-rw-r--r--app/views/projects/graphs/charts.html.haml21
-rw-r--r--app/views/projects/graphs/show.html.haml2
-rw-r--r--app/views/projects/import/jira/show.html.haml1
-rw-r--r--app/views/projects/issues/_design_management.html.haml15
-rw-r--r--app/views/projects/issues/_issue.html.haml5
-rw-r--r--app/views/projects/issues/_related_branches.html.haml8
-rw-r--r--app/views/projects/issues/_tabs.html.haml14
-rw-r--r--app/views/projects/issues/export_csv/_modal.html.haml1
-rw-r--r--app/views/projects/issues/index.html.haml6
-rw-r--r--app/views/projects/issues/show.html.haml10
-rw-r--r--app/views/projects/labels/index.html.haml4
-rw-r--r--app/views/projects/merge_requests/_how_to_merge.html.haml2
-rw-r--r--app/views/projects/merge_requests/_merge_request.html.haml2
-rw-r--r--app/views/projects/merge_requests/_widget.html.haml1
-rw-r--r--app/views/projects/merge_requests/creations/update_branches.html.haml3
-rw-r--r--app/views/projects/mirrors/_mirror_repos.html.haml2
-rw-r--r--app/views/projects/mirrors/_mirror_repos_push.html.haml12
-rw-r--r--app/views/projects/new.html.haml2
-rw-r--r--app/views/projects/pipelines/_with_tabs.html.haml10
-rw-r--r--app/views/projects/pipelines/index.html.haml1
-rw-r--r--app/views/projects/services/_deprecated_message.html.haml3
-rw-r--r--app/views/projects/services/_form.html.haml8
-rw-r--r--app/views/projects/services/_index.html.haml30
-rw-r--r--app/views/projects/services/edit.html.haml3
-rw-r--r--app/views/projects/services/prometheus/_custom_metrics.html.haml4
-rw-r--r--app/views/projects/services/slack_slash_commands/_help.html.haml2
-rw-r--r--app/views/projects/settings/_general.html.haml2
-rw-r--r--app/views/projects/settings/access_tokens/index.html.haml34
-rw-r--r--app/views/projects/settings/ci_cd/_autodevops_form.html.haml2
-rw-r--r--app/views/projects/settings/ci_cd/_form.html.haml4
-rw-r--r--app/views/projects/settings/ci_cd/show.html.haml2
-rw-r--r--app/views/projects/settings/integrations/show.html.haml4
-rw-r--r--app/views/projects/settings/operations/_incidents.html.haml8
-rw-r--r--app/views/projects/settings/repository/show.html.haml2
-rw-r--r--app/views/projects/snippets/show.html.haml2
-rw-r--r--app/views/projects/tags/_tag.html.haml3
-rw-r--r--app/views/projects/tags/show.html.haml2
-rw-r--r--app/views/projects/wikis/_sidebar.html.haml2
-rw-r--r--app/views/projects/wikis/git_access.html.haml5
-rw-r--r--app/views/search/_category.html.haml2
-rw-r--r--app/views/search/results/_blob_data.html.haml2
-rw-r--r--app/views/search/results/_snippet_blob.html.haml50
-rw-r--r--app/views/search/results/_snippet_title.html.haml2
-rw-r--r--app/views/shared/_auto_devops_callout.html.haml2
-rw-r--r--app/views/shared/_broadcast_message.html.haml2
-rw-r--r--app/views/shared/_clone_panel.html.haml2
-rw-r--r--app/views/shared/_commit_message_container.html.haml7
-rw-r--r--app/views/shared/_delete_label_modal.html.haml11
-rw-r--r--app/views/shared/_field.html.haml13
-rw-r--r--app/views/shared/_group_form.html.haml4
-rw-r--r--app/views/shared/_group_tips.html.haml8
-rw-r--r--app/views/shared/_label_row.html.haml2
-rw-r--r--app/views/shared/_milestone_expired.html.haml6
-rw-r--r--app/views/shared/_milestones_filter.html.haml6
-rw-r--r--app/views/shared/_mini_pipeline_graph.html.haml2
-rw-r--r--app/views/shared/_no_ssh.html.haml2
-rw-r--r--app/views/shared/_personal_access_tokens_created_container.html.haml15
-rw-r--r--app/views/shared/_personal_access_tokens_form.html.haml31
-rw-r--r--app/views/shared/_personal_access_tokens_table.html.haml36
-rw-r--r--app/views/shared/_project_limit.html.haml6
-rw-r--r--app/views/shared/_recaptcha_form.html.haml2
-rw-r--r--app/views/shared/_ref_dropdown.html.haml4
-rw-r--r--app/views/shared/_service_settings.html.haml35
-rw-r--r--app/views/shared/access_tokens/_created_container.html.haml12
-rw-r--r--app/views/shared/access_tokens/_form.html.haml34
-rw-r--r--app/views/shared/access_tokens/_table.html.haml37
-rw-r--r--app/views/shared/boards/components/_board.html.haml3
-rw-r--r--app/views/shared/boards/components/sidebar/_labels.html.haml3
-rw-r--r--app/views/shared/deploy_keys/_index.html.haml14
-rw-r--r--app/views/shared/deploy_keys/_project_group_form.html.haml24
-rw-r--r--app/views/shared/deploy_tokens/_form.html.haml10
-rw-r--r--app/views/shared/file_hooks/_index.html.haml4
-rw-r--r--app/views/shared/hook_logs/_content.html.haml2
-rw-r--r--app/views/shared/integrations/_form.html.haml2
-rw-r--r--app/views/shared/integrations/_index.html.haml27
-rw-r--r--app/views/shared/integrations/_integrations.html.haml26
-rw-r--r--app/views/shared/issuable/_bulk_update_sidebar.html.haml38
-rw-r--r--app/views/shared/issuable/_close_reopen_button.html.haml17
-rw-r--r--app/views/shared/issuable/_close_reopen_report_toggle.html.haml23
-rw-r--r--app/views/shared/issuable/_label_dropdown.html.haml7
-rw-r--r--app/views/shared/issuable/_label_page_default.html.haml2
-rw-r--r--app/views/shared/issuable/_nav.html.haml8
-rw-r--r--app/views/shared/issuable/_search_bar.html.haml4
-rw-r--r--app/views/shared/issuable/_sidebar.html.haml4
-rw-r--r--app/views/shared/issuable/_sidebar_assignees.html.haml2
-rw-r--r--app/views/shared/issuable/form/_branch_chooser.html.haml2
-rw-r--r--app/views/shared/members/_badge.html.haml4
-rw-r--r--app/views/shared/members/_blocked_badge.html.haml3
-rw-r--r--app/views/shared/members/_its_you_badge.html.haml3
-rw-r--r--app/views/shared/members/_member.html.haml29
-rw-r--r--app/views/shared/members/_two_factor_auth_badge.html.haml3
-rw-r--r--app/views/shared/milestones/_issuable.html.haml2
-rw-r--r--app/views/shared/milestones/_issues_tab.html.haml6
-rw-r--r--app/views/shared/milestones/_labels_tab.html.haml4
-rw-r--r--app/views/shared/milestones/_merge_requests_tab.haml8
-rw-r--r--app/views/shared/milestones/_milestone.html.haml16
-rw-r--r--app/views/shared/milestones/_top.html.haml12
-rw-r--r--app/views/shared/notes/_form.html.haml4
-rw-r--r--app/views/shared/notes/_note.html.haml7
-rw-r--r--app/views/shared/snippets/_form.html.haml10
-rw-r--r--app/views/shared/snippets/_header.html.haml4
-rw-r--r--app/views/shared/snippets/_snippet.html.haml5
-rw-r--r--app/views/shared/tokens/_scopes_form.html.haml2
-rw-r--r--app/views/snippets/show.html.haml2
-rw-r--r--app/views/users/_deletion_guidance.html.haml7
-rw-r--r--app/views/users/calendar_activities.html.haml4
-rw-r--r--app/workers/all_queues.yml73
-rw-r--r--app/workers/authorized_project_update/project_create_worker.rb19
-rw-r--r--app/workers/authorized_project_update/user_refresh_with_low_urgency_worker.rb11
-rw-r--r--app/workers/ci/daily_build_group_report_results_worker.rb16
-rw-r--r--app/workers/ci/daily_report_results_worker.rb16
-rw-r--r--app/workers/cluster_configure_worker.rb10
-rw-r--r--app/workers/cluster_project_configure_worker.rb12
-rw-r--r--app/workers/concerns/application_worker.rb17
-rw-r--r--app/workers/concerns/chaos_queue.rb2
-rw-r--r--app/workers/concerns/reactive_cacheable_worker.rb33
-rw-r--r--app/workers/create_commit_signature_worker.rb4
-rw-r--r--app/workers/design_management/new_version_worker.rb31
-rw-r--r--app/workers/external_service_reactive_caching_worker.rb7
-rw-r--r--app/workers/gitlab/jira_import/import_issue_worker.rb28
-rw-r--r--app/workers/group_import_worker.rb11
-rw-r--r--app/workers/incident_management/process_alert_worker.rb25
-rw-r--r--app/workers/irker_worker.rb2
-rw-r--r--app/workers/merge_request_mergeability_check_worker.rb3
-rw-r--r--app/workers/new_release_worker.rb2
-rw-r--r--app/workers/pages_domain_ssl_renewal_cron_worker.rb5
-rw-r--r--app/workers/process_commit_worker.rb4
-rw-r--r--app/workers/project_update_repository_storage_worker.rb16
-rw-r--r--app/workers/reactive_caching_worker.rb32
-rw-r--r--app/workers/stage_update_worker.rb4
-rw-r--r--app/workers/update_head_pipeline_for_merge_request_worker.rb4
-rw-r--r--app/workers/x509_issuer_crl_check_worker.rb76
-rwxr-xr-xbin/background_jobs2
-rwxr-xr-xbin/background_jobs_sk2
-rwxr-xr-xbin/background_jobs_sk_cluster2
-rw-r--r--cable/config.ru2
-rw-r--r--changelogs/add-name-parameter-to-project-environments-api.yml5
-rw-r--r--changelogs/unreleased/10343-remove-a-lonely-dot-in-batch-comments.yml5
-rw-r--r--changelogs/unreleased/118609-design-comment-edit-comment-text.yml5
-rw-r--r--changelogs/unreleased/14108-instance-level-ci-variables-api.yml5
-rw-r--r--changelogs/unreleased/14108-instance-level-ci-variables-controller.yml5
-rw-r--r--changelogs/unreleased/14108-instance-level-ci-variables-logic.yml5
-rw-r--r--changelogs/unreleased/14108-instance-level-ci-variables.yml5
-rw-r--r--changelogs/unreleased/1600-remove-jenkinsdeprecatedservice.yml5
-rw-r--r--changelogs/unreleased/195887-jira-comment-details-column-migration.yml5
-rw-r--r--changelogs/unreleased/195887-jira-comment-details-include-branch-name.yml5
-rw-r--r--changelogs/unreleased/195889-integrations-list-views-should-use-recommended-page-layout.yml5
-rw-r--r--changelogs/unreleased/196862-drop-plan-id.yml5
-rw-r--r--changelogs/unreleased/197170-issues-api-endpoint-missmatch-web-ui.yml5
-rw-r--r--changelogs/unreleased/197344-use-cookies-with-metadata.yml5
-rw-r--r--changelogs/unreleased/198324-avoid_subqueries.yml5
-rw-r--r--changelogs/unreleased/198603-add-foreign-key-on-projects-namespace-id-and-clean-up-ghost-projec.yml5
-rw-r--r--changelogs/unreleased/199046-text-for-future-release-date-grammatically-incorrect.yml5
-rw-r--r--changelogs/unreleased/199053-inconsistent-help-icon-styling.yml5
-rw-r--r--changelogs/unreleased/199428-update-the-main-left-side-navigation-for-the-package-area-to-bette.yml5
-rw-r--r--changelogs/unreleased/199843-fix-viewing-nil-blobs.yml5
-rw-r--r--changelogs/unreleased/199912-ff-enable-by-default.yml5
-rw-r--r--changelogs/unreleased/201872-partitioning-implement-cascading-deletes-without-foreign-keys.yml6
-rw-r--r--changelogs/unreleased/201927-solarized-dark.yml5
-rw-r--r--changelogs/unreleased/201930-webide-solarized.yml5
-rw-r--r--changelogs/unreleased/202143-fix-not-enough-data-in-vsa.yml6
-rw-r--r--changelogs/unreleased/202525-add-test-report-api-route.yml5
-rw-r--r--changelogs/unreleased/204839-registry-application-settings.yml5
-rw-r--r--changelogs/unreleased/205513-create-subgroup-without-project-permission.yml5
-rw-r--r--changelogs/unreleased/205570-sprint_initial_migrations.yml5
-rw-r--r--changelogs/unreleased/205570-sprint_relationships.yml5
-rw-r--r--changelogs/unreleased/207235-snippets-vue-ff-on-by-default.yml5
-rw-r--r--changelogs/unreleased/207267-expiration-policy-copy.yml5
-rw-r--r--changelogs/unreleased/207267-regex-keep-frontend.yml5
-rw-r--r--changelogs/unreleased/207324-search-api-scoped-to-blobs-does-not-honor-per_page.yml5
-rw-r--r--changelogs/unreleased/207471-expose-note-confidential-attribute-in-apis-and-display-on-frontend.yml5
-rw-r--r--changelogs/unreleased/207934-snippet-embed-scrolling.yml5
-rw-r--r--changelogs/unreleased/208132-add-ecs-to-autodeploy.yml5
-rw-r--r--changelogs/unreleased/208171-clicking-on-search-results-does-not-follow-link.yml5
-rw-r--r--changelogs/unreleased/208223-provider-icon-haml.yml5
-rw-r--r--changelogs/unreleased/208250-collect-object-store-config-in-usage-data.yml5
-rw-r--r--changelogs/unreleased/208255-editing-markdown-fields-don-t-work.yml5
-rw-r--r--changelogs/unreleased/208715-bump-tslint-search-depth.yml5
-rw-r--r--changelogs/unreleased/208897-remove-bot-type-column.yml5
-rw-r--r--changelogs/unreleased/208920-jira-import-usage-data.yml5
-rw-r--r--changelogs/unreleased/209808-webide-dark-theme.yml5
-rw-r--r--changelogs/unreleased/210018-remove-admin-ability-not-to-use-hashed-storage.yml5
-rw-r--r--changelogs/unreleased/210045-make-ci-ci-variables-protected-by-default.yml5
-rw-r--r--changelogs/unreleased/210232-select-the-first-option-if-there-is-only-one-metric-option-on-a.yml5
-rw-r--r--changelogs/unreleased/211460-annotations-clusters-endpoint.yml5
-rw-r--r--changelogs/unreleased/211461-create-annotations-graphql-endpoint.yml5
-rw-r--r--changelogs/unreleased/211519-hide-default-award-emojis.yml6
-rw-r--r--changelogs/unreleased/211637-blob-path-name.yml5
-rw-r--r--changelogs/unreleased/211944-provide-instance-level-setting-to-enable-or-disable-default-branch-add-policies.yml5
-rw-r--r--changelogs/unreleased/211984-jira-import-connection-test.yml5
-rw-r--r--changelogs/unreleased/212213-cablett-allowlist-gitlab-team-member.yml5
-rw-r--r--changelogs/unreleased/212279-webide-vue-files.yml5
-rw-r--r--changelogs/unreleased/212331-move-features-to-core-issue-board-focus-mode.yml5
-rw-r--r--changelogs/unreleased/212470-change-text-on-self-managed-sign-on-page.yml5
-rw-r--r--changelogs/unreleased/212549-padding-in-snippet-blob-header.yml5
-rw-r--r--changelogs/unreleased/212592-disable-binary-edit.yml5
-rw-r--r--changelogs/unreleased/212816-container-registry-missing-elipsis-on-tag-name.yml5
-rw-r--r--changelogs/unreleased/212948-jira-importer-labels.yml5
-rw-r--r--changelogs/unreleased/213238-fix-ide-discard-empty-file.yml5
-rw-r--r--changelogs/unreleased/213282-add-support-for-vue-routes.yml5
-rw-r--r--changelogs/unreleased/213282-update-template-for-sse.yml5
-rw-r--r--changelogs/unreleased/213324-fix-table-colors.yml5
-rw-r--r--changelogs/unreleased/213341-project-snippet-delete-redirect.yml5
-rw-r--r--changelogs/unreleased/213382-use-not-valid-to-immediately-enforce-a-not-null-constraint.yml5
-rw-r--r--changelogs/unreleased/213392-pagination-blue-background.yml5
-rw-r--r--changelogs/unreleased/213424-update-info-text-when-export-is-started.yml5
-rw-r--r--changelogs/unreleased/213473-move-custom-metric-alerts.yml5
-rw-r--r--changelogs/unreleased/213531-fix-init-d-web-server-pid.yml5
-rw-r--r--changelogs/unreleased/213556-move-the-hashed-storage-checks-to-gitlab-app-check-task-instead-of.yml5
-rw-r--r--changelogs/unreleased/213566-deploy-token-basic-auth.yml6
-rw-r--r--changelogs/unreleased/213566-deploy-token-conan.yml5
-rw-r--r--changelogs/unreleased/213566-deploy-token-npm.yml5
-rw-r--r--changelogs/unreleased/213566-package-deploy-token-auth.yml5
-rw-r--r--changelogs/unreleased/213566-package-deploy-tokens.yml5
-rw-r--r--changelogs/unreleased/213571-drop-lm-artifact-from-ci.yml5
-rw-r--r--changelogs/unreleased/213572-drop-lm-template-support.yml5
-rw-r--r--changelogs/unreleased/213652-don-t-lose-user-content-in-wiki.yml5
-rw-r--r--changelogs/unreleased/213678-API-documentation.yml5
-rw-r--r--changelogs/unreleased/213678-remove-deprecated-release-evidence-endpoints.yml5
-rw-r--r--changelogs/unreleased/213808-add-scheduled-at-field-to-jira-imports.yml5
-rw-r--r--changelogs/unreleased/213853-client-side-evaluation-to-live-preview-app-updates.yml5
-rw-r--r--changelogs/unreleased/213876.yml5
-rw-r--r--changelogs/unreleased/213877.yml5
-rw-r--r--changelogs/unreleased/213880-alert-management-list-graphql.yml5
-rw-r--r--changelogs/unreleased/213884-alert-management-plain-text-search.yml5
-rw-r--r--changelogs/unreleased/213890-alerts-loading-state.yml5
-rw-r--r--changelogs/unreleased/213892-Alerts-empty-state.yml5
-rw-r--r--changelogs/unreleased/213894-northstar-metric-monitor-health.yml5
-rw-r--r--changelogs/unreleased/214007-example-expiration-regex.yml5
-rw-r--r--changelogs/unreleased/214065-increase-width-of-label-column-on-labels-page.yml5
-rw-r--r--changelogs/unreleased/214242-remove-deprecated-buttons-in-release-page.yml5
-rw-r--r--changelogs/unreleased/214243.yml5
-rw-r--r--changelogs/unreleased/214283-add-updated-at-to-todos-api.yml5
-rw-r--r--changelogs/unreleased/214301-change-placeholder-in-search-input-for-analytics-features.yml5
-rw-r--r--changelogs/unreleased/214322-remove-token-from-runners-api.yml5
-rw-r--r--changelogs/unreleased/214382-remove-project_list_show_issue_count-feature-flag.yml5
-rw-r--r--changelogs/unreleased/214382-remove-project_list_show_mr_count-feature-flag.yml5
-rw-r--r--changelogs/unreleased/214382-remove-registrations_recaptcha-feature-flag.yml5
-rw-r--r--changelogs/unreleased/214382-remove-set_user_last_activity-feature-flag.yml5
-rw-r--r--changelogs/unreleased/214477-reactor-toast-to-alert-in-image-repository-details-page.yml5
-rw-r--r--changelogs/unreleased/214478-add-image-repository-search.yml5
-rw-r--r--changelogs/unreleased/214518-database-table-for-alert-management-alerts.yml5
-rw-r--r--changelogs/unreleased/214522-list.yml5
-rw-r--r--changelogs/unreleased/214528.yml5
-rw-r--r--changelogs/unreleased/214542-graphql-status-mutation.yml5
-rw-r--r--changelogs/unreleased/214547_expose_web_url.yml5
-rw-r--r--changelogs/unreleased/214581-star-dashboard-btn.yml6
-rw-r--r--changelogs/unreleased/214582-revert-sort-order-change-BE.yml5
-rw-r--r--changelogs/unreleased/214582-show-starred-dashboards-in-metrics-dashboard-dropdown.yml6
-rw-r--r--changelogs/unreleased/214583-add-ability-to-query-projects.yml5
-rw-r--r--changelogs/unreleased/214678-add-an-api-for-flipper-percentage-of-actors-rollout.yml5
-rw-r--r--changelogs/unreleased/214710-rename-configure-to-enable.yml5
-rw-r--r--changelogs/unreleased/214773-metrics-dashboadr-heatmap-styles.yml5
-rw-r--r--changelogs/unreleased/214777-match-jira-keys-with-trailing-characters.yml5
-rw-r--r--changelogs/unreleased/214834-propagate-service-tempate.yml5
-rw-r--r--changelogs/unreleased/214882-esc-key-handler.yml6
-rw-r--r--changelogs/unreleased/214882-render-single-panel.yml5
-rw-r--r--changelogs/unreleased/214983-remove_additional_rows_in_application_settings.yml5
-rw-r--r--changelogs/unreleased/215121-add-wysiwyg-documentation.yml5
-rw-r--r--changelogs/unreleased/215147-drop-feature-flag.yml5
-rw-r--r--changelogs/unreleased/215164-show-correct-import-label-and-import-count-on-jira-import-form.yml5
-rw-r--r--changelogs/unreleased/215308-follow-up-from-add-issues_create_limit-to-settings-api.yml5
-rw-r--r--changelogs/unreleased/215326-moving-an-issue-referencing-a-group-in-a-note-can-fail.yml5
-rw-r--r--changelogs/unreleased/215352-update-cancel-comment-note-text.yml5
-rw-r--r--changelogs/unreleased/215472-single-chart-from-url.yml5
-rw-r--r--changelogs/unreleased/215473-url-update-single-panel.yml5
-rw-r--r--changelogs/unreleased/215542-add-jira-import-finished-alert.yml5
-rw-r--r--changelogs/unreleased/215542-show-flash-alert-on-issues-page-which-displays-the-jira-import-sta.yml5
-rw-r--r--changelogs/unreleased/215563-migration-to-import-common-metrics.yml6
-rw-r--r--changelogs/unreleased/215563-remove-ruby-syntax.yml6
-rw-r--r--changelogs/unreleased/215569-jira-import-fix-default-author.yml5
-rw-r--r--changelogs/unreleased/215598-alert-management-graphql-sort-backend.yml5
-rw-r--r--changelogs/unreleased/215617_mutation_to_add_a_new_branch.yml5
-rw-r--r--changelogs/unreleased/215700-fix-incorrect-commits-number-in-commits-list.yml5
-rw-r--r--changelogs/unreleased/215902-ci-job-jwt-handle-signing-key-issues.yml5
-rw-r--r--changelogs/unreleased/215919-email-validation.yml5
-rw-r--r--changelogs/unreleased/215955-redirect-loop-when-logging-in-for-the-experimental-sign_up-flow.yml5
-rw-r--r--changelogs/unreleased/215975-remove-monaco-snippets-flag.yml5
-rw-r--r--changelogs/unreleased/216001-fix-copy-button-hover.yml5
-rw-r--r--changelogs/unreleased/216031-show-accurate-error-message-when-pipelines-disabled-mr-must-succee.yml7
-rw-r--r--changelogs/unreleased/216035-clone-dropdown-icons-alignment.yml5
-rw-r--r--changelogs/unreleased/216046-snippet-search-results-page-styling-issue.yml5
-rw-r--r--changelogs/unreleased/216091-align-author-dropdown-height.yml5
-rw-r--r--changelogs/unreleased/216122-use-search-to-quickly-find-and-discover-images-hosted-in-the-gitla.yml5
-rw-r--r--changelogs/unreleased/216157-add-usage-ping-recording_finished_at-field.yml5
-rw-r--r--changelogs/unreleased/216216-fix-mr-diffs-external-diffs-store-bug.yml5
-rw-r--r--changelogs/unreleased/216243.yml5
-rw-r--r--changelogs/unreleased/216300-allow-for-mr-creation.yml5
-rw-r--r--changelogs/unreleased/216453-large-files-snippet.yml5
-rw-r--r--changelogs/unreleased/216472-move-embeding-metrics-to-core.yml5
-rw-r--r--changelogs/unreleased/216477-gllink-updates.yml5
-rw-r--r--changelogs/unreleased/216505-public-dashboard-visibility.yml5
-rw-r--r--changelogs/unreleased/216509-ide-new-file-trim.yml5
-rw-r--r--changelogs/unreleased/216543-none-syntax-highlighting-theme-for-web-ide.yml5
-rw-r--r--changelogs/unreleased/216584-drop-ruby-memory-bytes.yml5
-rw-r--r--changelogs/unreleased/216597-drop-uss-pss-env-var.yml5
-rw-r--r--changelogs/unreleased/216618-remove-liquid.yml6
-rw-r--r--changelogs/unreleased/216728-actionview-template-error-undefined-method-pages_domain-for-pagesd.yml5
-rw-r--r--changelogs/unreleased/216750-open-single-panel-new-tab.yml5
-rw-r--r--changelogs/unreleased/216851-graphql-externallypaginatedarrayconnection-can-return-incorrect-nu.yml5
-rw-r--r--changelogs/unreleased/216920-bug-restore-exact-time-tooltip-on-last-updated-tag-column.yml5
-rw-r--r--changelogs/unreleased/216970-add-instance-to-service-if-missing.yml5
-rw-r--r--changelogs/unreleased/217455-skip-mergeability-check-on-list-endpoint-by-default.yml5
-rw-r--r--changelogs/unreleased/217602-file-uploads-on-local-storage-with-nil-secret-in-the-db-are-broken.yml5
-rw-r--r--changelogs/unreleased/217708-nomethoderror-undefined-method-admin-for-nil-nilclass.yml6
-rw-r--r--changelogs/unreleased/217992.yml5
-rw-r--r--changelogs/unreleased/22691-externalize-i18n-strings-from---app-views-shared-issuable-_bulk_upd.yml5
-rw-r--r--changelogs/unreleased/22691-externalize-i18n-strings-from---app-views-shared-issuable-_close_re.yml5
-rw-r--r--changelogs/unreleased/22691-externalize-i18n-strings-from---app-views-shared-issuable-_label_.yml5
-rw-r--r--changelogs/unreleased/22691-externalize-i18n-strings-from---app-views-shared-issuable-_nav-html.yml5
-rw-r--r--changelogs/unreleased/22691-externalize-i18n-strings-from---app-views-shared-issuable-_sidebar-.yml5
-rw-r--r--changelogs/unreleased/22691-externalize-i18n-strings-from---app-views-shared-milestones-_issuab.yml5
-rw-r--r--changelogs/unreleased/22691-externalize-i18n-strings-from---app-views-shared-milestones-_issues.yml5
-rw-r--r--changelogs/unreleased/22691-externalize-i18n-strings-from---app-views-shared-milestones-_labels.yml5
-rw-r--r--changelogs/unreleased/22691-externalize-i18n-strings-from---app-views-shared-milestones-_merge_.yml5
-rw-r--r--changelogs/unreleased/22691-externalize-i18n-strings-from---app-views-shared-milestones-_milest.yml5
-rw-r--r--changelogs/unreleased/22691-externalize-i18n-strings-from---views-shared-issuable-_close_re-btn.yml5
-rw-r--r--changelogs/unreleased/22691-externelize-i18n-strings-from---app-views-shared--aria-label.yml5
-rw-r--r--changelogs/unreleased/22691-externelize-i18n-strings-from---app-views-shared-_commit_message_co.yml5
-rw-r--r--changelogs/unreleased/22691-externelize-i18n-strings-from---app-views-shared-_delete_label_moda.yml5
-rw-r--r--changelogs/unreleased/22691-externelize-i18n-strings-from---app-views-shared-_field-html-haml.yml5
-rw-r--r--changelogs/unreleased/22691-externelize-i18n-strings-from---app-views-shared-_group_form-html-h.yml5
-rw-r--r--changelogs/unreleased/22691-externelize-i18n-strings-from---app-views-shared-_group_tips-html-h.yml5
-rw-r--r--changelogs/unreleased/22691-externelize-i18n-strings-from---app-views-shared-_label_row-html-ha.yml5
-rw-r--r--changelogs/unreleased/22691-externelize-i18n-strings-from---app-views-shared-_milestone_expired.yml5
-rw-r--r--changelogs/unreleased/22691-externelize-i18n-strings-from---app-views-shared-_milestones_filter.yml5
-rw-r--r--changelogs/unreleased/22691-externelize-i18n-strings-from---app-views-shared-_personal_access_t.yml5
-rw-r--r--changelogs/unreleased/22691-externelize-i18n-strings-from---app-views-shared-_project_limit-htm.yml5
-rw-r--r--changelogs/unreleased/22691-externelize-i18n-strings-from---app-views-shared-_recaptcha_form-ht.yml5
-rw-r--r--changelogs/unreleased/22691-externelize-i18n-strings-from---app-views-shared-_ref_dropdown-html.yml5
-rw-r--r--changelogs/unreleased/22691-externelize-i18n-strings-from---app-views-shared-milestones-_top-ht.yml5
-rw-r--r--changelogs/unreleased/22691-externelize-i18n-strings-from---app-views-users-_deletion_guidance-.yml5
-rw-r--r--changelogs/unreleased/22691-i18n-externelize-strings-from---app-views-users-calendar_activities.yml5
-rw-r--r--changelogs/unreleased/23847-download-reports.yml5
-rw-r--r--changelogs/unreleased/24295-freeze-period-API-changes.yml5
-rw-r--r--changelogs/unreleased/24295-freeze-period-db-changes.yml5
-rw-r--r--changelogs/unreleased/24295-freeze-period-service.yml5
-rw-r--r--changelogs/unreleased/24525-ide-renaming-issue.yml5
-rw-r--r--changelogs/unreleased/25375-webide-markdown-broken-images.yml5
-rw-r--r--changelogs/unreleased/25807-status-tooltip-overlaps-extended-tooltip.yml5
-rw-r--r--changelogs/unreleased/25875-Add-Webex-Teams-project-integration-service.yml5
-rw-r--r--changelogs/unreleased/27481-remove-unused-cluster-workers.yml5
-rw-r--r--changelogs/unreleased/28560_cleanup_optimistic_locking_db_pt2-second-try.yml5
-rw-r--r--changelogs/unreleased/28566-add-sec-binaries-template.yml5
-rw-r--r--changelogs/unreleased/28617-add-global-sec-prefix.yml5
-rw-r--r--changelogs/unreleased/29713-graphql-add-issue-priority-sort.yml5
-rw-r--r--changelogs/unreleased/30146-let-s-encrypt-errors.yml5
-rw-r--r--changelogs/unreleased/30526-c-be-wiki-activity-Pushes.yml5
-rw-r--r--changelogs/unreleased/31810-markdown-images.yml5
-rw-r--r--changelogs/unreleased/32358-add-initial-stats-for-modsecurity-installations.yml5
-rw-r--r--changelogs/unreleased/33098-distribute-daily-cron-schedules-out-over-the-hour.yml5
-rw-r--r--changelogs/unreleased/33161-highlight-focused-design-discussion-in-image-markers.yml5
-rw-r--r--changelogs/unreleased/33200-add-empty-needs-to-auto-devops-test.yml5
-rw-r--r--changelogs/unreleased/35069-protect-builds.yml5
-rw-r--r--changelogs/unreleased/36758-graphql-query-one-or-all-lists-in-an-issue-board.yml5
-rw-r--r--changelogs/unreleased/36810-webide-branch-with-path.yml5
-rw-r--r--changelogs/unreleased/37278-DS_DISABLE_DIND-true.yml5
-rw-r--r--changelogs/unreleased/37278-SAST_DISABLE_DIND-true.yml5
-rw-r--r--changelogs/unreleased/38022-add-remove_label-quick-action-more-intuitive-than-unlabel.yml5
-rw-r--r--changelogs/unreleased/38080-scala-gitlab-ci-template-does-not-work.yml5
-rw-r--r--changelogs/unreleased/38096-add-index-on-resource-weight-events-created-at-pd.yml5
-rw-r--r--changelogs/unreleased/38096-resource-state-events-pd.yml5
-rw-r--r--changelogs/unreleased/38296-graphql-add-milestone_due_asc-sort-for-issuables.yml5
-rw-r--r--changelogs/unreleased/38355-user-popovers-don-t-work-in-system-notes.yml5
-rw-r--r--changelogs/unreleased/38358-update-migration-helpers-to-use-check-constraints-instead-of-change.yml5
-rw-r--r--changelogs/unreleased/39467-allow-a-release-s-associated-milestones-to-be-edited-through-the-ed.yml5
-rw-r--r--changelogs/unreleased/55241-add-issues-create-limit-to-settings-api.yml5
-rw-r--r--changelogs/unreleased/Remove-addIssue-function-logic-from-list-js.yml5
-rw-r--r--changelogs/unreleased/Remove-getIssues-function-logic-from-list-js.yml5
-rw-r--r--changelogs/unreleased/Remove-update-function-logic-from-issue-js.yml5
-rw-r--r--changelogs/unreleased/aalakkad-fix-mirro-repo-docs-link.yml5
-rw-r--r--changelogs/unreleased/ab-fix-not-null-inconsistency.yml5
-rw-r--r--changelogs/unreleased/ab-north-star-usage-ping-2.yml5
-rw-r--r--changelogs/unreleased/add-ci-kubernetes-active-pipeline-variable.yml6
-rw-r--r--changelogs/unreleased/add-epic-id-to-resource-state-event-pd.yml5
-rw-r--r--changelogs/unreleased/add-inherit-from-to-services.yml5
-rw-r--r--changelogs/unreleased/add-missing-cluster-usage-data-index.yml5
-rw-r--r--changelogs/unreleased/add-ops-strategies-user-lists-table.yml5
-rw-r--r--changelogs/unreleased/add_confidential_issue_url_param.yml5
-rw-r--r--changelogs/unreleased/add_fluentd_into_cluster_app_page.yml5
-rw-r--r--changelogs/unreleased/add_waf_and_cilium_flags_into_fluentd_table.yml5
-rw-r--r--changelogs/unreleased/ajk-212987-nil-wiki-page.yml5
-rw-r--r--changelogs/unreleased/ajk-fix-extra-sleep-exclusive-lease.yml5
-rw-r--r--changelogs/unreleased/ak-official-helm-chart.yml5
-rw-r--r--changelogs/unreleased/ak-update-es.yml5
-rw-r--r--changelogs/unreleased/al-214347-web-ide-pipelines-usage.yml5
-rw-r--r--changelogs/unreleased/al-214695-improve-response-in-snippets-api.yml5
-rw-r--r--changelogs/unreleased/alert-management-colour.yml5
-rw-r--r--changelogs/unreleased/allow_public_view_of_pipeline_schedules.yml5
-rw-r--r--changelogs/unreleased/app-helpers.yml5
-rw-r--r--changelogs/unreleased/app-models-1.yml5
-rw-r--r--changelogs/unreleased/app-models-2.yml5
-rw-r--r--changelogs/unreleased/app-models-3.yml5
-rw-r--r--changelogs/unreleased/async-secondary-email-devise-mailers.yml5
-rw-r--r--changelogs/unreleased/bvl-remove-admin-logs.yml5
-rw-r--r--changelogs/unreleased/bvl-remove-sidekiq-request-store-env.yml5
-rw-r--r--changelogs/unreleased/cat-duplicate-ci-pipelines-index-215790.yml5
-rw-r--r--changelogs/unreleased/change-var-to-variable.yml5
-rw-r--r--changelogs/unreleased/chore-migrate-models-policies-specs-admin-mode.yml5
-rw-r--r--changelogs/unreleased/chore-mitt-migration-issuables-list.yml5
-rw-r--r--changelogs/unreleased/chore-vue-event-hub-to-mitt-migration.yml5
-rw-r--r--changelogs/unreleased/cl-update-indexer-230.yml5
-rw-r--r--changelogs/unreleased/cl-wiki-result.yml5
-rw-r--r--changelogs/unreleased/cleanup_user_highest_roles_background_migration.yml5
-rw-r--r--changelogs/unreleased/cluster-applications-artifact.yml5
-rw-r--r--changelogs/unreleased/cngo-fix-jira-importer-urls.yml5
-rw-r--r--changelogs/unreleased/cngo-make-edit-board-text-sentence-case.yml5
-rw-r--r--changelogs/unreleased/correctly-count-wiki-pages-in-sidebar.yml5
-rw-r--r--changelogs/unreleased/dblessing-email-on-new-signin.yml5
-rw-r--r--changelogs/unreleased/dblessing-google-oauth2-timeout.yml5
-rw-r--r--changelogs/unreleased/dblessing-remove-app-settings-redirect.yml5
-rw-r--r--changelogs/unreleased/dblessing-scim-identitites-new-user-flow.yml5
-rw-r--r--changelogs/unreleased/dbodicherla-add-dropdown-input-based-on-dashboard-file.yml6
-rw-r--r--changelogs/unreleased/dbodicherla-add-prefix-for-templating-variables.yml5
-rw-r--r--changelogs/unreleased/dbodicherla-update-urls-params-only-update-existing-variables.yml6
-rw-r--r--changelogs/unreleased/deprecate-x-y-stable-secure-images.yml5
-rw-r--r--changelogs/unreleased/design-management-ee-to-ce-fe-designs-tab.yml5
-rw-r--r--changelogs/unreleased/display-db-deprecation-notice.yml5
-rw-r--r--changelogs/unreleased/dmishunov-clone-btn-margin.yml5
-rw-r--r--changelogs/unreleased/dmishunov-fix-project-snippet-redirect.yml5
-rw-r--r--changelogs/unreleased/dmishunov-js-error-anon-view-snippet.yml5
-rw-r--r--changelogs/unreleased/dmishunov-snippet-edit-cancellation.yml5
-rw-r--r--changelogs/unreleased/dont-support-raw-text-on-filter.yml5
-rw-r--r--changelogs/unreleased/dz-deprecate-plugins-dir.yml5
-rw-r--r--changelogs/unreleased/dz-redirect-unscoped-issue-routes.yml5
-rw-r--r--changelogs/unreleased/dz-remove-legacy-routes.yml5
-rw-r--r--changelogs/unreleased/dz-scope-autocomplete-routing.yml5
-rw-r--r--changelogs/unreleased/dz-scope-pipeline-routes.yml5
-rw-r--r--changelogs/unreleased/dz-update-android-template.yml5
-rw-r--r--changelogs/unreleased/e2300-cs-template.yml5
-rw-r--r--changelogs/unreleased/e2300-dast-template.yml5
-rw-r--r--changelogs/unreleased/e2300-ds-template.yml5
-rw-r--r--changelogs/unreleased/e2300-lm-template.yml5
-rw-r--r--changelogs/unreleased/e2300-sast-template.yml5
-rw-r--r--changelogs/unreleased/eb-download-daily-coverage-csv.yml6
-rw-r--r--changelogs/unreleased/ee-app-1.yml5
-rw-r--r--changelogs/unreleased/emilyring-cluster-list-refactor-pagination.yml5
-rw-r--r--changelogs/unreleased/emilyring-terraform-mr-fix.yml5
-rw-r--r--changelogs/unreleased/enable-group-export-ndjson-by-default.yml5
-rw-r--r--changelogs/unreleased/enable-group-import-ndjson-by-default.yml5
-rw-r--r--changelogs/unreleased/enable-uploadpack-filters.yml5
-rw-r--r--changelogs/unreleased/error-list-colour.yml5
-rw-r--r--changelogs/unreleased/error-tracking-link-target.yml5
-rw-r--r--changelogs/unreleased/exclude_html_entities_from_haml_lint.yml5
-rw-r--r--changelogs/unreleased/expose-issue-iid-in-alert-management-alert-graphql.yml5
-rw-r--r--changelogs/unreleased/expose-more-fields-in-alert-management-alert-graphql.yml5
-rw-r--r--changelogs/unreleased/fe-ide-fix-scroll-in-right-pane.yml5
-rw-r--r--changelogs/unreleased/feat-add-expunge-option-mailroom.yml5
-rw-r--r--changelogs/unreleased/feat-add-intermediate-cas-to-smime.yml5
-rw-r--r--changelogs/unreleased/feat-ldap-auth-admin-mode.yml5
-rw-r--r--changelogs/unreleased/feat-profile-image-guidelines.yml5
-rw-r--r--changelogs/unreleased/feat-x509-tag.yml5
-rw-r--r--changelogs/unreleased/feat-x509_issuer_crl_check.yml5
-rw-r--r--changelogs/unreleased/feature-next-prev-commit-buttons.yml5
-rw-r--r--changelogs/unreleased/feature-refactor-keyboard-shortcuts-modal.yml5
-rw-r--r--changelogs/unreleased/ff-iid-routing.yml5
-rw-r--r--changelogs/unreleased/ff-lists-api.yml5
-rw-r--r--changelogs/unreleased/filter-pipeline-by-branch.yml5
-rw-r--r--changelogs/unreleased/fix-36628-create-a-rake-task-to-cleanup-unused-lfs-files.yml5
-rw-r--r--changelogs/unreleased/fix-admin-mode-graphiql-explorer.yml5
-rw-r--r--changelogs/unreleased/fix-approvals-given-count.yml5
-rw-r--r--changelogs/unreleased/fix-branch-dot-txt.yml5
-rw-r--r--changelogs/unreleased/fix-broken-heading-of-vue-3-migration-guide-doc.yml5
-rw-r--r--changelogs/unreleased/fix-cop-inject-multiple.yml5
-rw-r--r--changelogs/unreleased/fix-how-to-checkout-mr-link.yml5
-rw-r--r--changelogs/unreleased/fix-issue-details-row-emoji-block.yml5
-rw-r--r--changelogs/unreleased/fix-istio-url-should-be-raw.yml5
-rw-r--r--changelogs/unreleased/fix-mentions-personal-snippet.yml5
-rw-r--r--changelogs/unreleased/fix-missing-rss-feed-events.yml5
-rw-r--r--changelogs/unreleased/fix-nested-search-for-matching-ds-files.yml5
-rw-r--r--changelogs/unreleased/fix-new-mr-project-commit-link.yml5
-rw-r--r--changelogs/unreleased/fix-protected-vars-by-default.yml6
-rw-r--r--changelogs/unreleased/fix-started-guide-link-201724.yml5
-rw-r--r--changelogs/unreleased/fix-subgroup-milestone-links.yml5
-rw-r--r--changelogs/unreleased/fix-wiki-activity-icons.yml5
-rw-r--r--changelogs/unreleased/fj-208904-add-snippet-backfilling-repository-migration.yml5
-rw-r--r--changelogs/unreleased/fj-214692-avoid-commit-when-content-file-name-not-updated.yml5
-rw-r--r--changelogs/unreleased/fj-215861-add-rake-tasks-to-migrate-snippets.yml5
-rw-r--r--changelogs/unreleased/fj-add-files-param-snippet-create-mutation.yml5
-rw-r--r--changelogs/unreleased/fj-add-migration-user.yml5
-rw-r--r--changelogs/unreleased/fj-add-presence-validation-rest-snippet-endpoints.yml5
-rw-r--r--changelogs/unreleased/fj-avoid-repository-size-checks-if-migration-bot.yml5
-rw-r--r--changelogs/unreleased/fj-expand-graphql-snippet-blob-type.yml5
-rw-r--r--changelogs/unreleased/fj-fix-bug-snippet-blob-viewer-graphql.yml5
-rw-r--r--changelogs/unreleased/fj-fix-migration-when-user-invalid-commit-name.yml5
-rw-r--r--changelogs/unreleased/fj-fix-raw-snippet-endpoint.yml5
-rw-r--r--changelogs/unreleased/fj-fix-templates-endpoint-when-project-has-dots.yml5
-rw-r--r--changelogs/unreleased/fj-move-raw-url-to-snippet-entity.yml5
-rw-r--r--changelogs/unreleased/fj-remove-file-name-in-snippet-lists.yml5
-rw-r--r--changelogs/unreleased/fj-remove-flag-version-snippets.yml5
-rw-r--r--changelogs/unreleased/fj-return-proper-file-name-snippet-entity.yml5
-rw-r--r--changelogs/unreleased/fj-update-snippet-backfilling-with-migrate-bot.yml5
-rw-r--r--changelogs/unreleased/georgekoltsov-add-group-export-email-nofitication.yml5
-rw-r--r--changelogs/unreleased/graphql_log_user_last_activity_on.yml5
-rw-r--r--changelogs/unreleased/group-shared-projects-api.yml5
-rw-r--r--changelogs/unreleased/id-cache-logs-tree.yml5
-rw-r--r--changelogs/unreleased/id-fetch-raw-artifact-of-any-type.yml5
-rw-r--r--changelogs/unreleased/id-fix-diff-link-on-search.yml5
-rw-r--r--changelogs/unreleased/id-remove-diff-compare-with-head-feature-flag.yml5
-rw-r--r--changelogs/unreleased/ifrenkel-remove-pipfilelock-from-ds-tpl.yml5
-rw-r--r--changelogs/unreleased/improve_add_remove_labels_api.yml5
-rw-r--r--changelogs/unreleased/initial_repository_storage_move.yml5
-rw-r--r--changelogs/unreleased/ipython_katex_fixes.yml5
-rw-r--r--changelogs/unreleased/jh-group_export_ui_frontend.yml5
-rw-r--r--changelogs/unreleased/jira-user-importer-prep.yml5
-rw-r--r--changelogs/unreleased/jira-user-importer.yml5
-rw-r--r--changelogs/unreleased/jivanvl-ensure-links-generated-manage-variables.yml5
-rw-r--r--changelogs/unreleased/jivanvl-remove-feature-flag-visibility-toggle-monitoring.yml5
-rw-r--r--changelogs/unreleased/jivanvl-use-metrics-url-query-param.yml5
-rw-r--r--changelogs/unreleased/json-gem-upgrade.yml5
-rw-r--r--changelogs/unreleased/kc-escape-dashboard-path-annotations.yml5
-rw-r--r--changelogs/unreleased/kubeclient-create-or-update.yml5
-rw-r--r--changelogs/unreleased/leaky-constant-fix-1.yml5
-rw-r--r--changelogs/unreleased/leaky-constant-fix-10.yml5
-rw-r--r--changelogs/unreleased/leaky-constant-fix-12.yml5
-rw-r--r--changelogs/unreleased/leaky-constant-fix-13.yml5
-rw-r--r--changelogs/unreleased/leaky-constant-fix-15.yml5
-rw-r--r--changelogs/unreleased/leaky-constant-fix-16.yml5
-rw-r--r--changelogs/unreleased/leaky-constant-fix-17.yml5
-rw-r--r--changelogs/unreleased/leaky-constant-fix-18.yml5
-rw-r--r--changelogs/unreleased/leaky-constant-fix-19.yml5
-rw-r--r--changelogs/unreleased/leaky-constant-fix-2.yml5
-rw-r--r--changelogs/unreleased/leaky-constant-fix-20.yml5
-rw-r--r--changelogs/unreleased/leaky-constant-fix-21.yml5
-rw-r--r--changelogs/unreleased/leaky-constant-fix-23.yml5
-rw-r--r--changelogs/unreleased/leaky-constant-fix-24.yml5
-rw-r--r--changelogs/unreleased/leaky-constant-fix-25.yml5
-rw-r--r--changelogs/unreleased/leaky-constant-fix-26.yml5
-rw-r--r--changelogs/unreleased/leaky-constant-fix-27.yml5
-rw-r--r--changelogs/unreleased/leaky-constant-fix-35.yml5
-rw-r--r--changelogs/unreleased/leaky-constant-fix-4.yml5
-rw-r--r--changelogs/unreleased/leaky-constant-fix-6.yml5
-rw-r--r--changelogs/unreleased/leaky-constant-fix-7.yml5
-rw-r--r--changelogs/unreleased/leaky-constant-fix-8.yml5
-rw-r--r--changelogs/unreleased/lib-gitlab-refactoring-2.yml5
-rw-r--r--changelogs/unreleased/lib-gitlab-refactoring-3.yml5
-rw-r--r--changelogs/unreleased/lib-gitlab-refactoring-4.yml5
-rw-r--r--changelogs/unreleased/lib-gitlab-refactoring-5.yml5
-rw-r--r--changelogs/unreleased/lib-gitlab-refactoring.yml5
-rw-r--r--changelogs/unreleased/markdown-toolbar-spaces.yml5
-rw-r--r--changelogs/unreleased/mattermost-chat-responder.yml5
-rw-r--r--changelogs/unreleased/mc-feature-keep-latest-artifact-for-ref.yml5
-rw-r--r--changelogs/unreleased/merge-requests-spec.yml5
-rw-r--r--changelogs/unreleased/mg-dedupe-monaco-chunks.yml5
-rw-r--r--changelogs/unreleased/move-browser-performance-testing-to-rules-syntax.yml5
-rw-r--r--changelogs/unreleased/move-build-template-to-rules-syntax.yml5
-rw-r--r--changelogs/unreleased/move-code-quality-template-to-rules-syntax.yml5
-rw-r--r--changelogs/unreleased/move-deploy-template-to-rules-syntax.yml5
-rw-r--r--changelogs/unreleased/move_daily_users_statistics_cronjob_to_ce.yml5
-rw-r--r--changelogs/unreleased/mv-to-service.yml5
-rw-r--r--changelogs/unreleased/mw-ca-add-title.yml5
-rw-r--r--changelogs/unreleased/mw-cicd-analytics-add-title.yml5
-rw-r--r--changelogs/unreleased/mw-cr-title-margin.yml5
-rw-r--r--changelogs/unreleased/mw-ia-add-title.yml5
-rw-r--r--changelogs/unreleased/mw-ia-group-level-add-title.yml5
-rw-r--r--changelogs/unreleased/mw-insights-add-title.yml5
-rw-r--r--changelogs/unreleased/mw-pa-title-cleanup.yml5
-rw-r--r--changelogs/unreleased/mw-ra-add-title.yml5
-rw-r--r--changelogs/unreleased/mw-vsa-title-cleanup.yml5
-rw-r--r--changelogs/unreleased/mwam-214582-add-starred-dashboard-to-metrics-dashboard-endpoint.yml5
-rw-r--r--changelogs/unreleased/mwaw-214581-delete-star-metrics-dashboard-endpoint.yml5
-rw-r--r--changelogs/unreleased/mwaw-214581-star-metrics-dashboard-endpoint.yml5
-rw-r--r--changelogs/unreleased/mwaw-215224-make-dashboard-annotations-generally-available.yml6
-rw-r--r--changelogs/unreleased/mwaw-216963-annotations-missing-from-metric-charts.yml6
-rw-r--r--changelogs/unreleased/nicolasdular-hide-broadcast-notification-until-end-date.yml5
-rw-r--r--changelogs/unreleased/nicolasdular-improve-target-path-regex.yml5
-rw-r--r--changelogs/unreleased/nicolasdular-remove-email-restrictions-ff.yml5
-rw-r--r--changelogs/unreleased/ntepluhina-edit-note-fix.yml5
-rw-r--r--changelogs/unreleased/package-deploy-tokens-fe.yml5
-rw-r--r--changelogs/unreleased/pages-1-18.yml5
-rw-r--r--changelogs/unreleased/pedroms-threads-counter-wording.yml5
-rw-r--r--changelogs/unreleased/ph-207121-shaMismatchCanMerge.yml5
-rw-r--r--changelogs/unreleased/ph-212669-commitTypeMarkdownHTML.yml5
-rw-r--r--changelogs/unreleased/ph-213189-codeNavSameFile.yml5
-rw-r--r--changelogs/unreleased/ph-214480-copyUploadLinks.yml5
-rw-r--r--changelogs/unreleased/ph-215917-escapeRef.yml5
-rw-r--r--changelogs/unreleased/ph-33423-fixCommitReplyForm.yml5
-rw-r--r--changelogs/unreleased/ph-33586-confirmCancelReply.yml5
-rw-r--r--changelogs/unreleased/pipelines-spec.yml5
-rw-r--r--changelogs/unreleased/psi-dropdown-item-check.yml5
-rw-r--r--changelogs/unreleased/rc-add_metrics_dashboard_policy.yml5
-rw-r--r--changelogs/unreleased/rc-remove_influx_views_docs.yml5
-rw-r--r--changelogs/unreleased/rc-remove_influxdb_supporting_libs.yml5
-rw-r--r--changelogs/unreleased/re-enable_boards_negative_filters.yml5
-rw-r--r--changelogs/unreleased/recreate_foreign_key_with_nullify.yml5
-rw-r--r--changelogs/unreleased/refactor-ee-app-services.yml5
-rw-r--r--changelogs/unreleased/remove-create_confidential_merge_request.yml5
-rw-r--r--changelogs/unreleased/remove-deprecated-container-scanning-report-format.yml5
-rw-r--r--changelogs/unreleased/remove-exceptions-from-junit-parser.yml5
-rw-r--r--changelogs/unreleased/remove-experimental-indexer-column.yml5
-rw-r--r--changelogs/unreleased/remove-obsolete-resource-milestone-events-columns.yml5
-rw-r--r--changelogs/unreleased/remove-sidekiq-rake-tasks.yml5
-rw-r--r--changelogs/unreleased/remove_admin_settings_geo_navigation.yml5
-rw-r--r--changelogs/unreleased/remove_ci_expose_arbitrary_artifacts_in_mr_feature_flag.yml5
-rw-r--r--changelogs/unreleased/remove_git_archive_path_feature_flag.yml5
-rw-r--r--changelogs/unreleased/remove_kwargs_from_storage_move_worker.yml5
-rw-r--r--changelogs/unreleased/revert-0ddee28a.yml5
-rw-r--r--changelogs/unreleased/rs-enable-keep-divergent-refs.yml5
-rw-r--r--changelogs/unreleased/rubocop-flag-kwargs-in-sidekiq-workers-cop.yml5
-rw-r--r--changelogs/unreleased/rw-exclude-skipped-tests-from-success.yml4
-rw-r--r--changelogs/unreleased/rz_fix_api_statistics_permission.yml5
-rw-r--r--changelogs/unreleased/set-multiple-custom-metrics-edit-path.yml5
-rw-r--r--changelogs/unreleased/sh-clean-up-public-visiblity-level-check.yml5
-rw-r--r--changelogs/unreleased/sh-cleanup-mwps-refresh.yml5
-rw-r--r--changelogs/unreleased/sh-destroy-dropzone-hidden-input.yml5
-rw-r--r--changelogs/unreleased/sh-disable-schema-dump-prod.yml5
-rw-r--r--changelogs/unreleased/sh-enable-ref-caching-diffs-controller.yml5
-rw-r--r--changelogs/unreleased/sh-fix-create-projects-with-prometheus-service.yml5
-rw-r--r--changelogs/unreleased/sh-fix-lfs-badge-feature-flag.yml5
-rw-r--r--changelogs/unreleased/sh-fix-merge-request-stickiness.yml5
-rw-r--r--changelogs/unreleased/sh-fix-orphaned-invited-members.yml5
-rw-r--r--changelogs/unreleased/sh-fix-overwrite-import-export-check.yml5
-rw-r--r--changelogs/unreleased/sh-fix-paste-markdown-from-diff.yml5
-rw-r--r--changelogs/unreleased/sh-fix-render-rst.yml5
-rw-r--r--changelogs/unreleased/sh-fix-squash-error-handling.yml5
-rw-r--r--changelogs/unreleased/sh-fix-user-logging-for-jwt-controller.yml5
-rw-r--r--changelogs/unreleased/sh-handle-deployments-no-pods.yml5
-rw-r--r--changelogs/unreleased/sh-handle-invalid-gitattributes.yml5
-rw-r--r--changelogs/unreleased/sh-log-api-errors.yml5
-rw-r--r--changelogs/unreleased/sh-log-cloudflare-headers.yml5
-rw-r--r--changelogs/unreleased/sh-log-container-registry-access-warnings.yml5
-rw-r--r--changelogs/unreleased/sh-revert-codeowners-check.yml5
-rw-r--r--changelogs/unreleased/sh-update-nokogiri-1-10-9.yml5
-rw-r--r--changelogs/unreleased/sh-update-plantutml-gem.yml5
-rw-r--r--changelogs/unreleased/sh-update-rails-6-0-2-2.yml5
-rw-r--r--changelogs/unreleased/sh-upgrade-unicorn.yml5
-rw-r--r--changelogs/unreleased/sh-use-gitlab-markdown-in-wiki.yml5
-rw-r--r--changelogs/unreleased/shard_move_capture_all_errors.yml5
-rw-r--r--changelogs/unreleased/show_unpersisted_services_not_active.yml5
-rw-r--r--changelogs/unreleased/sk-216910-validate-package-type.yml5
-rw-r--r--changelogs/unreleased/sk-217056-relocate-nuget-presenter.yml5
-rw-r--r--changelogs/unreleased/sk-add-validation-on-params-for-podlogs-service.yml5
-rw-r--r--changelogs/unreleased/sk-fix-500-on-snippet-graphql-mutations.yml5
-rw-r--r--changelogs/unreleased/sk-fix-tag-deletion-duplicate-events.yml5
-rw-r--r--changelogs/unreleased/sk-organize-package-models.yml5
-rw-r--r--changelogs/unreleased/sk-refactor-count-queries-in-environments-controller.yml5
-rw-r--r--changelogs/unreleased/sluongngoc-decouple-partial-clone.yml5
-rw-r--r--changelogs/unreleased/sort-issues-by-label-prio.yml5
-rw-r--r--changelogs/unreleased/storage_move_api.yml5
-rw-r--r--changelogs/unreleased/switch-thread-to-process-memory-cache.yml5
-rw-r--r--changelogs/unreleased/sy-fix-transient-embed-again.yml5
-rw-r--r--changelogs/unreleased/sy-publish-status.yml5
-rw-r--r--changelogs/unreleased/sy-status-counts.yml5
-rw-r--r--changelogs/unreleased/topics-optional.yml5
-rw-r--r--changelogs/unreleased/tr-alert-detail-remaining-fields.yml5
-rw-r--r--changelogs/unreleased/tr-alert-management-feature-flag.yml5
-rw-r--r--changelogs/unreleased/tr-fix-space-char.yml5
-rw-r--r--changelogs/unreleased/tr-remove-alert-detail-feature-flag.yml5
-rw-r--r--changelogs/unreleased/tr-remove-alert-mgmt-feature-flag.yml5
-rw-r--r--changelogs/unreleased/update-ado-image-to-0-14-0.yml6
-rw-r--r--changelogs/unreleased/update-ado-image-to-0-15-0.yml6
-rw-r--r--changelogs/unreleased/update-css-loader.yml5
-rw-r--r--changelogs/unreleased/update-deprecated-slot-syntax-in---app-assets-javascripts-boards-componen.yml5
-rw-r--r--changelogs/unreleased/update-deprecated-slot-syntax-in---app-assets-javascripts-confidential_me.yml5
-rw-r--r--changelogs/unreleased/update-deprecated-slot-syntax-in---app-assets-javascripts-diffs-component.yml5
-rw-r--r--changelogs/unreleased/update-deprecated-slot-syntax-in---app-assets-javascripts-pages-admin-use.yml5
-rw-r--r--changelogs/unreleased/update-deprecated-slot-syntax-in---app-assets-javascripts-pages-milestone.yml5
-rw-r--r--changelogs/unreleased/update-deprecated-slot-syntax-in---app-assets-javascripts-performance_bar.yml5
-rw-r--r--changelogs/unreleased/update-deprecated-slot-syntax-in---app-assets-javascripts-profile-account.yml5
-rw-r--r--changelogs/unreleased/update-deprecated-slot-syntax-in---app-assets-javascripts-releases-compon.yml5
-rw-r--r--changelogs/unreleased/update-deprecated-slot-syntax-in---app-assets-javascripts-repository-comp.yml5
-rw-r--r--changelogs/unreleased/update-deprecated-slot-syntax-in---app-assets-javascripts-serverless-comp.yml5
-rw-r--r--changelogs/unreleased/update-deprecated-slot-syntax-in---assets-javascripts-pages-milestone-pro.yml5
-rw-r--r--changelogs/unreleased/update-deprecated-slot-syntax-in---environments-delete_environment_modal-vue.yml5
-rw-r--r--changelogs/unreleased/update-deprecated-slot-syntax-in---javascripts-ide-ide_tree-vue.yml5
-rw-r--r--changelogs/unreleased/update-deprecated-slot-syntax-in---javascripts-ide-pipelines-list-vue.yml5
-rw-r--r--changelogs/unreleased/update-deprecated-slot-syntax-in---releases-evidence_block-vue.yml5
-rw-r--r--changelogs/unreleased/update-deprecated-slot-syntax-in-app-assets-javascripts-issue_show-compon.yml5
-rw-r--r--changelogs/unreleased/update-doorkeeper-503.yml5
-rw-r--r--changelogs/unreleased/update-gitlab-managed-helm-to-2-16-6.yml6
-rw-r--r--changelogs/unreleased/update-gitlab-runner-helm-chart-to-0-16-1.yml5
-rw-r--r--changelogs/unreleased/update-jupyter-to-0-9-0.yml5
-rw-r--r--changelogs/unreleased/update-recursive-open-struct-gem.yml5
-rw-r--r--changelogs/unreleased/update_fluentd_model_to_include_log_flags.yml5
-rw-r--r--changelogs/unreleased/update_ui_to_support_cilium_log_checkbox.yml5
-rw-r--r--changelogs/unreleased/user-api-spec.yml5
-rw-r--r--changelogs/unreleased/user-can-enable-alert-management.yml5
-rw-r--r--changelogs/unreleased/vij-fix-update-bug.yml5
-rw-r--r--changelogs/unreleased/vij-invalid-path-error.yml5
-rw-r--r--changelogs/unreleased/vij-snippet-blob-search-views.yml5
-rw-r--r--changelogs/unreleased/vij-snippet-code-references.yml5
-rw-r--r--changelogs/unreleased/vij-snippet-create-update-errors.yml5
-rw-r--r--config/application.rb1
-rw-r--r--config/cable.yml.example14
-rw-r--r--config/environments/test.rb7
-rw-r--r--config/feature_categories.yml6
-rw-r--r--config/gitlab.yml.example13
-rw-r--r--config/helpers/is_eslint.js18
-rw-r--r--config/initializers/0_thread_cache.rb3
-rw-r--r--config/initializers/1_settings.rb16
-rw-r--r--config/initializers/7_prometheus_metrics.rb6
-rw-r--r--config/initializers/action_cable.rb9
-rw-r--r--config/initializers/actioncable.rb8
-rw-r--r--config/initializers/active_record_fix_insert_all.rb26
-rw-r--r--config/initializers/cookies_serializer.rb2
-rw-r--r--config/initializers/gettext_rails_i18n_patch.rb2
-rw-r--r--config/initializers/measuring.rb3
-rw-r--r--config/initializers/rack_attack.rb.example29
-rw-r--r--config/initializers/rack_attack_new.rb10
-rw-r--r--config/initializers/sidekiq.rb19
-rw-r--r--config/initializers/zz_metrics.rb7
-rw-r--r--config/initializers_before_autoloader/002_sidekiq.rb20
-rw-r--r--config/initializers_before_autoloader/100_patch_omniauth_oauth2.rb17
-rw-r--r--config/locales/doorkeeper.en.yml13
-rw-r--r--config/mail_room.yml1
-rw-r--r--config/prometheus/common_metrics.yml45
-rw-r--r--config/pseudonymizer.yml1
-rw-r--r--config/puma_actioncable.example.development.rb2
-rw-r--r--config/redis.cache.yml.example6
-rw-r--r--config/redis.queues.yml.example6
-rw-r--r--config/redis.shared_state.yml.example6
-rw-r--r--config/resque.yml.example6
-rw-r--r--config/routes.rb42
-rw-r--r--config/routes/admin.rb9
-rw-r--r--config/routes/issues.rb1
-rw-r--r--config/routes/merge_requests.rb2
-rw-r--r--config/routes/pipelines.rb37
-rw-r--r--config/routes/project.rb109
-rw-r--r--config/routes/repository_scoped.rb4
-rw-r--r--config/sidekiq_queues.yml4
-rw-r--r--config/smime_signature_settings.rb1
-rw-r--r--config/webpack.config.js41
-rw-r--r--config/webpack.vendor.config.js1
-rw-r--r--danger/changelog/Dangerfile8
-rw-r--r--danger/gitlab_ui_wg/Dangerfile62
-rw-r--r--danger/roulette/Dangerfile9
-rw-r--r--danger/specs/Dangerfile6
-rw-r--r--danger/telemetry/Dangerfile27
-rw-r--r--danger/utility_css/Dangerfile51
-rw-r--r--db/migrate/20171230123729_init_schema.rb6
-rw-r--r--db/migrate/20180101160629_create_prometheus_metrics.rb4
-rw-r--r--db/migrate/20180115094742_add_default_project_creation_setting.rb2
-rw-r--r--db/migrate/20180116193854_create_lfs_file_locks.rb2
-rw-r--r--db/migrate/20180122162010_add_auto_devops_domain_to_application_settings.rb4
-rw-r--r--db/migrate/20180129193323_add_uploads_builder_context.rb4
-rw-r--r--db/migrate/20180209115333_create_chatops_tables.rb2
-rw-r--r--db/migrate/20180212030105_add_external_ip_to_clusters_applications_ingress.rb4
-rw-r--r--db/migrate/20180214093516_create_badges.rb5
-rw-r--r--db/migrate/20180214155405_create_clusters_applications_runners.rb4
-rw-r--r--db/migrate/20180216120000_add_pages_domain_verification.rb2
-rw-r--r--db/migrate/20180222043024_add_ip_address_to_runner.rb2
-rw-r--r--db/migrate/20180223144945_add_allow_local_requests_from_hooks_and_services_to_application_settings.rb2
-rw-r--r--db/migrate/20180305144721_add_privileged_to_runner.rb2
-rw-r--r--db/migrate/20180308125206_add_user_internal_regex_to_application_setting.rb4
-rw-r--r--db/migrate/20180314145917_add_header_and_footer_banners_to_appearances_table.rb2
-rw-r--r--db/migrate/20180315160435_add_external_auth_mutual_tls_fields_to_project_settings.rb6
-rw-r--r--db/migrate/20180319190020_create_deploy_tokens.rb4
-rw-r--r--db/migrate/20180424134533_create_application_setting_terms.rb2
-rw-r--r--db/migrate/20180502122856_create_project_mirror_data.rb6
-rw-r--r--db/migrate/20180503131624_create_remote_mirrors.rb6
-rw-r--r--db/migrate/20180503175053_ensure_missing_columns_to_project_mirror_data.rb8
-rw-r--r--db/migrate/20180503193953_add_mirror_available_to_application_settings.rb2
-rw-r--r--db/migrate/20180511131058_create_clusters_applications_jupyter.rb10
-rw-r--r--db/migrate/20180515121227_create_notes_diff_files.rb7
-rw-r--r--db/migrate/20180529093006_ensure_remote_mirror_columns.rb9
-rw-r--r--db/migrate/20180531185349_add_repository_languages.rb4
-rw-r--r--db/migrate/20180601213245_add_deploy_strategy_to_project_auto_devops.rb2
-rw-r--r--db/migrate/20180607071808_add_push_events_branch_filter_to_web_hooks.rb2
-rw-r--r--db/migrate/20180612103626_add_columns_for_helm_tiller_certificates.rb2
-rw-r--r--db/migrate/20180613081317_create_ci_builds_runner_session.rb4
-rw-r--r--db/migrate/20180625113853_create_import_export_uploads.rb2
-rw-r--r--db/migrate/20180704204006_add_hide_third_party_offers_to_application_settings.rb2
-rw-r--r--db/migrate/20180713092803_create_user_statuses.rb4
-rw-r--r--db/migrate/20180718005113_add_instance_statistics_visibility_to_application_setting.rb2
-rw-r--r--db/migrate/20180723135214_add_web_ide_client_side_preview_enabled_to_application_settings.rb2
-rw-r--r--db/migrate/20180808162000_add_user_show_add_ssh_key_message_to_application_settings.rb2
-rw-r--r--db/migrate/20180814153625_add_commit_email_to_users.rb4
-rw-r--r--db/migrate/20180824202952_add_outbound_requests_whitelist_to_application_settings.rb2
-rw-r--r--db/migrate/20180831164905_add_common_to_prometheus_metrics.rb2
-rw-r--r--db/migrate/20180831164908_add_identifier_to_prometheus_metric.rb2
-rw-r--r--db/migrate/20180901200537_add_resource_label_event_reference_fields.rb2
-rw-r--r--db/migrate/20180907015926_add_legacy_abac_to_cluster_providers_gcp.rb2
-rw-r--r--db/migrate/20180910115836_add_attr_encrypted_columns_to_web_hook.rb4
-rw-r--r--db/migrate/20180910153412_add_token_digest_to_personal_access_tokens.rb2
-rw-r--r--db/migrate/20180912111628_add_knative_application.rb6
-rw-r--r--db/migrate/20180924141949_add_diff_max_patch_bytes_to_application_settings.rb2
-rw-r--r--db/migrate/20181009190428_create_clusters_kubernetes_namespaces.rb6
-rw-r--r--db/migrate/20181017001059_add_cluster_type_to_clusters.rb2
-rw-r--r--db/migrate/20181019032400_add_shards_table.rb2
-rw-r--r--db/migrate/20181019032408_add_repositories_table.rb2
-rw-r--r--db/migrate/20181025115728_add_private_commit_email_hostname_to_application_settings.rb4
-rw-r--r--db/migrate/20181028120717_add_first_day_of_week_to_application_settings.rb2
-rw-r--r--db/migrate/20181031145139_add_protected_ci_variables_to_application_settings.rb2
-rw-r--r--db/migrate/20181031190559_drop_gcp_clusters_table.rb2
-rw-r--r--db/migrate/20181101191341_create_clusters_applications_cert_manager.rb6
-rw-r--r--db/migrate/20181115140140_add_encrypted_runners_token_to_settings.rb4
-rw-r--r--db/migrate/20181116050532_knative_external_ip.rb4
-rw-r--r--db/migrate/20181116141415_add_encrypted_runners_token_to_namespaces.rb4
-rw-r--r--db/migrate/20181116141504_add_encrypted_runners_token_to_projects.rb4
-rw-r--r--db/migrate/20181120151656_add_token_encrypted_to_ci_runners.rb2
-rw-r--r--db/migrate/20181122160027_create_project_repositories.rb2
-rw-r--r--db/migrate/20181123144235_create_suggestions.rb6
-rw-r--r--db/migrate/20181128123704_add_state_to_pool_repository.rb10
-rw-r--r--db/migrate/20181129104854_add_token_encrypted_to_ci_builds.rb4
-rw-r--r--db/migrate/20181203002526_add_project_bfg_object_map_column.rb4
-rw-r--r--db/migrate/20181211092510_add_name_author_id_and_sha_to_releases.rb6
-rw-r--r--db/migrate/20181212171634_create_error_tracking_settings.rb4
-rw-r--r--db/migrate/20181228175414_create_releases_link_table.rb4
-rw-r--r--db/migrate/20190109153125_add_merge_request_external_diffs.rb2
-rw-r--r--db/migrate/20190114172110_add_domain_to_cluster.rb2
-rw-r--r--db/migrate/20190115092821_add_columns_project_error_tracking_settings.rb6
-rw-r--r--db/migrate/20190116234221_add_sorting_fields_to_user_preference.rb6
-rw-r--r--db/migrate/20190218134158_add_masked_to_ci_variables.rb2
-rw-r--r--db/migrate/20190218134209_add_masked_to_ci_group_variables.rb2
-rw-r--r--db/migrate/20190219201635_add_asset_proxy_settings.rb8
-rw-r--r--db/migrate/20190220142344_add_email_header_and_footer_enabled_flag_to_appearances_table.rb2
-rw-r--r--db/migrate/20190228192410_add_multi_line_attributes_to_suggestion.rb6
-rw-r--r--db/migrate/20190301182457_add_external_hostname_to_ingress_and_knative.rb6
-rw-r--r--db/migrate/20190320174702_add_lets_encrypt_notification_email_to_application_settings.rb4
-rw-r--r--db/migrate/20190322164830_add_auto_ssl_enabled_to_pages_domain.rb2
-rw-r--r--db/migrate/20190325105715_add_fields_to_user_preferences.rb2
-rw-r--r--db/migrate/20190325165127_add_managed_to_cluster.rb2
-rw-r--r--db/migrate/20190327163904_add_notification_email_to_notification_settings.rb4
-rw-r--r--db/migrate/20190329085614_add_lets_encrypt_terms_of_service_accepted_to_application_settings.rb2
-rw-r--r--db/migrate/20190402150158_backport_enterprise_schema.rb8
-rw-r--r--db/migrate/20190409224933_add_name_to_geo_nodes.rb2
-rw-r--r--db/migrate/20190414185432_add_comment_to_vulnerability_feedback.rb2
-rw-r--r--db/migrate/20190415030217_add_variable_type_to_ci_variables.rb2
-rw-r--r--db/migrate/20190416213556_add_variable_type_to_ci_group_variables.rb2
-rw-r--r--db/migrate/20190416213631_add_variable_type_to_ci_pipeline_schedule_variables.rb2
-rw-r--r--db/migrate/20190422082247_create_project_metrics_settings.rb2
-rw-r--r--db/migrate/20190429082448_create_pages_domain_acme_orders.rb6
-rw-r--r--db/migrate/20190430131225_create_issue_tracker_data.rb4
-rw-r--r--db/migrate/20190430142025_create_jira_tracker_data.rb4
-rw-r--r--db/migrate/20190514105711_create_ip_restriction.rb2
-rw-r--r--db/migrate/20190516151857_add_lets_encrypt_private_key_to_application_settings.rb2
-rw-r--r--db/migrate/20190520200123_add_rule_type_to_approval_merge_request_approval_rules.rb2
-rw-r--r--db/migrate/20190527011309_add_required_template_name_to_application_settings.rb4
-rw-r--r--db/migrate/20190529142545_add_dns_rebinding_protection_enabled_to_application_settings.rb2
-rw-r--r--db/migrate/20190604091310_add_ldap_membership_lock.rb2
-rw-r--r--db/migrate/20190605104727_add_default_project_deletion_protection_to_application_settings.rb2
-rw-r--r--db/migrate/20190606054742_add_token_encrypted_to_operations_feature_flags_clients.rb4
-rw-r--r--db/migrate/20190606202100_add_name_to_badges.rb2
-rw-r--r--db/migrate/20190607085356_add_source_to_pages_domains.rb2
-rw-r--r--db/migrate/20190611090827_add_time_tracking_limit_to_hours_to_application_settings.rb2
-rw-r--r--db/migrate/20190611161641_add_target_project_id_to_merge_trains.rb8
-rw-r--r--db/migrate/20190613044655_add_username_to_deploy_tokens.rb2
-rw-r--r--db/migrate/20190613073003_create_project_aliases.rb2
-rw-r--r--db/migrate/20190617123615_add_grafana_to_settings.rb2
-rw-r--r--db/migrate/20190621151636_add_merge_request_rebase_jid.rb2
-rw-r--r--db/migrate/20190624123615_add_grafana_url_to_settings.rb6
-rw-r--r--db/migrate/20190625115224_add_description_to_services.rb2
-rw-r--r--db/migrate/20190628145246_add_strategies_to_operations_feature_flag_scopes.rb2
-rw-r--r--db/migrate/20190709204413_add_rule_type_to_approval_project_rules.rb2
-rw-r--r--db/migrate/20190711124721_create_job_variables.rb6
-rw-r--r--db/migrate/20190711200508_add_token_encrypted_to_deploy_tokens.rb2
-rw-r--r--db/migrate/20190712064021_add_namespace_per_environment_flag_to_clusters.rb2
-rw-r--r--db/migrate/20190715140740_add_event_type_to_design_management_designs_versions.rb1
-rw-r--r--db/migrate/20190715173819_add_object_storage_flag_to_geo_node.rb2
-rw-r--r--db/migrate/20190716144222_create_analytics_cycle_analytics_project_stages.rb2
-rw-r--r--db/migrate/20190722104947_add_static_object_token_to_users.rb6
-rw-r--r--db/migrate/20190722132830_add_static_objects_external_storage_columns_to_application_settings.rb2
-rw-r--r--db/migrate/20190723153247_create_allowed_email_domains_for_groups.rb2
-rw-r--r--db/migrate/20190729062536_create_analytics_cycle_analytics_group_stages.rb2
-rw-r--r--db/migrate/20190729180447_add_merge_requests_require_code_owner_approval_to_protected_branches.rb2
-rw-r--r--db/migrate/20190731084415_add_build_need.rb2
-rw-r--r--db/migrate/20190801142441_add_throttle_protected_path_columns.rb2
-rw-r--r--db/migrate/20190806071559_remove_epic_issues_default_relative_position.rb2
-rw-r--r--db/migrate/20190808152507_add_projects_sorting_field_to_user_preferences.rb2
-rw-r--r--db/migrate/20190816151221_add_active_jobs_limit_to_plans.rb2
-rw-r--r--db/migrate/20190820163320_add_first_last_name_to_user.rb2
-rw-r--r--db/migrate/20190821040941_create_cluster_providers_aws.rb4
-rw-r--r--db/migrate/20190827222124_add_sourcegraph_configuration_to_application_settings.rb2
-rw-r--r--db/migrate/20190828172831_create_package_tag.rb2
-rw-r--r--db/migrate/20190829131130_create_external_pull_requests.rb2
-rw-r--r--db/migrate/20190901174200_add_max_issue_count_to_list.rb2
-rw-r--r--db/migrate/20190903150358_create_analytics_repository_files_table.rb2
-rw-r--r--db/migrate/20190905140605_add_cloud_run_to_clusters_providers_gcp.rb2
-rw-r--r--db/migrate/20190906104555_create_alerts_service_data.rb2
-rw-r--r--db/migrate/20190907184714_add_show_whitespace_in_diffs_to_user_preferences.rb2
-rw-r--r--db/migrate/20190910211526_create_packages_conan_file_metadata.rb2
-rw-r--r--db/migrate/20190913174707_add_spdx_id_to_software_licenses.rb2
-rw-r--r--db/migrate/20190918102042_create_grafana_integrations.rb2
-rw-r--r--db/migrate/20190918104731_add_cleanup_status_to_cluster.rb2
-rw-r--r--db/migrate/20190918121135_add_cleanup_status_reason_to_cluster.rb2
-rw-r--r--db/migrate/20190920224341_create_merge_request_context_commits_and_diffs.rb4
-rw-r--r--db/migrate/20190924124627_add_pull_mirror_branch_prefix_to_projects.rb6
-rw-r--r--db/migrate/20190926225633_create_x509_signatures.rb2
-rw-r--r--db/migrate/20190927055500_create_description_versions.rb2
-rw-r--r--db/migrate/20190929180751_create_vulnerabilities.rb4
-rw-r--r--db/migrate/20190930153535_create_zoom_meetings.rb2
-rw-r--r--db/migrate/20191001170300_create_ci_ref.rb2
-rw-r--r--db/migrate/20191002123516_create_clusters_applications_elastic_stack.rb4
-rw-r--r--db/migrate/20191003015155_add_self_managed_prometheus_alerts.rb2
-rw-r--r--db/migrate/20191003060227_add_push_event_hooks_limit_to_application_settings.rb2
-rw-r--r--db/migrate/20191003064615_create_aws_roles.rb2
-rw-r--r--db/migrate/20191003195218_add_pendo_enabled_to_application_settings.rb2
-rw-r--r--db/migrate/20191003195620_add_pendo_url_to_application_settings.rb2
-rw-r--r--db/migrate/20191008013056_add_push_event_activities_limit_to_application_settings.rb2
-rw-r--r--db/migrate/20191009222222_add_custom_http_clone_url_root_to_application_settings.rb2
-rw-r--r--db/migrate/20191010174846_add_snowplow_iglu_registry_url_to_application_settings.rb2
-rw-r--r--db/migrate/20191014123159_add_expire_notification_delivered_to_personal_access_tokens.rb2
-rw-r--r--db/migrate/20191017191341_create_clusters_applications_crossplane.rb4
-rw-r--r--db/migrate/20191025092748_add_secret_token_to_snippet.rb2
-rw-r--r--db/migrate/20191028130054_add_max_issue_weight_to_list.rb2
-rw-r--r--db/migrate/20191029125305_create_packages_conan_metadata.rb2
-rw-r--r--db/migrate/20191029191901_add_enabled_to_grafana_integrations.rb2
-rw-r--r--db/migrate/20191103202505_add_eks_credentials_to_application_settings.rb4
-rw-r--r--db/migrate/20191105134413_create_service_desk_settings.rb2
-rw-r--r--db/migrate/20191105155113_add_secret_to_snippet.rb2
-rw-r--r--db/migrate/20191106144901_add_state_to_merge_trains.rb2
-rw-r--r--db/migrate/20191111121500_default_ci_config_path.rb2
-rw-r--r--db/migrate/20191112090226_add_artifacts_to_ci_build_need.rb2
-rw-r--r--db/migrate/20191119231621_create_container_expiration_policies.rb2
-rw-r--r--db/migrate/20191120084627_add_encrypted_fields_to_application_settings.rb4
-rw-r--r--db/migrate/20191121111621_create_packages_dependencies.rb2
-rw-r--r--db/migrate/20191121193110_add_issue_links_type.rb2
-rw-r--r--db/migrate/20191125133353_add_target_path_to_broadcast_message.rb2
-rw-r--r--db/migrate/20191125140458_create_import_failures.rb2
-rw-r--r--db/migrate/20191127030005_create_serverless_domain_cluster.rb2
-rw-r--r--db/migrate/20191127163053_add_confidential_to_doorkeeper_application.rb2
-rw-r--r--db/migrate/20191127221608_add_wildcard_and_domain_type_to_pages_domains.rb4
-rw-r--r--db/migrate/20191128145231_add_ci_resource_groups.rb2
-rw-r--r--db/migrate/20191129134844_add_broadcast_type_to_broadcast_message.rb2
-rw-r--r--db/migrate/20191202181924_add_environment_auto_stop_in_to_ci_builds_metadata.rb2
-rw-r--r--db/migrate/20191204192726_add_design_disk_path_to_geo_hashed_storage_migrated_events.rb2
-rw-r--r--db/migrate/20191206014412_add_image_to_design_management_designs_versions.rb2
-rw-r--r--db/migrate/20191208110214_add_suggestion_commit_message_to_projects.rb6
-rw-r--r--db/migrate/20191212140117_change_commit_user_mentions_commit_id_column_type.rb2
-rw-r--r--db/migrate/20191213104838_add_service_desk_username.rb2
-rw-r--r--db/migrate/20191213143656_create_ci_pipelines_config.rb2
-rw-r--r--db/migrate/20191217165641_add_saml_provider_prohibited_outer_forks.rb2
-rw-r--r--db/migrate/20191218084115_add_updating_name_disabled_for_users_to_application_settings.rb2
-rw-r--r--db/migrate/20191218124915_add_repository_storage_to_snippets.rb4
-rw-r--r--db/migrate/20191218125015_add_storage_version_to_snippets.rb2
-rw-r--r--db/migrate/20200102140148_add_expanded_environment_name_to_ci_build_metadata.rb2
-rw-r--r--db/migrate/20200121192942_create_geo_events.rb2
-rw-r--r--db/migrate/20200122161638_add_deploy_token_type_to_deploy_tokens.rb2
-rw-r--r--db/migrate/20200124053531_add_source_to_import_failures.rb2
-rw-r--r--db/migrate/20200128184209_add_usage_to_pages_domains.rb2
-rw-r--r--db/migrate/20200129133716_add_resource_milestone_events_table.rb2
-rw-r--r--db/migrate/20200130134335_add_cert_and_key_to_serverless_domain_cluster.rb4
-rw-r--r--db/migrate/20200202100932_add_service_desk_project_key.rb2
-rw-r--r--db/migrate/20200204131831_create_daily_report_results.rb6
-rw-r--r--db/migrate/20200206112850_create_snippet_repository_table.rb2
-rw-r--r--db/migrate/20200207151640_create_deployment_clusters.rb2
-rw-r--r--db/migrate/20200210184410_create_operations_strategies_table.rb2
-rw-r--r--db/migrate/20200210184420_create_operations_scopes_table.rb2
-rw-r--r--db/migrate/20200213093702_add_email_restrictions_to_application_settings.rb2
-rw-r--r--db/migrate/20200213100530_add_verification_columns_to_packages.rb2
-rw-r--r--db/migrate/20200213155311_add_npm_package_requests_forwarding_to_application_settings.rb2
-rw-r--r--db/migrate/20200213224220_add_sprints.rb35
-rw-r--r--db/migrate/20200214025454_add_canonical_emails.rb2
-rw-r--r--db/migrate/20200215222507_drop_forked_project_links_fk.rb5
-rw-r--r--db/migrate/20200218113721_add_indexes_to_package_file.rb19
-rw-r--r--db/migrate/20200219105209_add_filepath_to_release_links.rb2
-rw-r--r--db/migrate/20200219135440_add_limit_metric_type_to_list.rb2
-rw-r--r--db/migrate/20200224020219_add_status_page_settings.rb2
-rw-r--r--db/migrate/20200224163804_add_version_to_feature_flags_table.rb2
-rw-r--r--db/migrate/20200226100614_create_requirements.rb4
-rw-r--r--db/migrate/20200227164113_create_scim_identities.rb2
-rw-r--r--db/migrate/20200227165129_create_user_details.rb2
-rw-r--r--db/migrate/20200302152516_add_wiki_slug.rb2
-rw-r--r--db/migrate/20200304023245_add_sprint_to_issues.rb20
-rw-r--r--db/migrate/20200304023851_add_sprint_to_merge_requests.rb20
-rw-r--r--db/migrate/20200304024025_add_sprint_id_index_to_issues.rb21
-rw-r--r--db/migrate/20200304024042_add_sprint_id_index_to_merge_requests.rb21
-rw-r--r--db/migrate/20200305200641_create_terraform_states.rb2
-rw-r--r--db/migrate/20200309162244_add_open_project_tracker_data.rb2
-rw-r--r--db/migrate/20200310145304_add_runtime_created_to_ci_job_variables.rb2
-rw-r--r--db/migrate/20200311141053_add_ci_pipeline_schedules_to_plan_limits.rb2
-rw-r--r--db/migrate/20200311154110_create_vulnerability_exports.rb2
-rw-r--r--db/migrate/20200311165635_create_project_export_jobs.rb2
-rw-r--r--db/migrate/20200311214912_add_modsecurity_mode_to_ingress_application.rb2
-rw-r--r--db/migrate/20200318162148_add_external_key_to_issues_table.rb2
-rw-r--r--db/migrate/20200318164448_add_external_key_to_epics_table.rb2
-rw-r--r--db/migrate/20200318183553_create_pypi_package_metadata.rb2
-rw-r--r--db/migrate/20200319124127_create_metrics_dashboard_annotations.rb4
-rw-r--r--db/migrate/20200320112455_add_cost_factor_fileds_to_ci_runners.rb4
-rw-r--r--db/migrate/20200320123839_add_letsencrypt_errors_to_pages_domains.rb2
-rw-r--r--db/migrate/20200320212400_add_project_show_default_award_emojis.rb11
-rw-r--r--db/migrate/20200323071918_add_bio_to_user_details.rb6
-rw-r--r--db/migrate/20200324115359_add_namespace_storage_size_limit_to_application_settings.rb2
-rw-r--r--db/migrate/20200325152327_add_seat_link_enabled_to_application_settings.rb2
-rw-r--r--db/migrate/20200326114443_create_jira_imports_table.rb2
-rw-r--r--db/migrate/20200326122700_create_diff_note_positions.rb4
-rw-r--r--db/migrate/20200330121000_add_confidential_attribute_to_epics.rb2
-rw-r--r--db/migrate/20200331195952_add_container_expiration_policies_enable_historic_entries_to_application_settings.rb2
-rw-r--r--db/migrate/20200401211005_create_operations_user_lists.rb4
-rw-r--r--db/migrate/20200402001106_add_cluster_type_index_to_clusters.rb19
-rw-r--r--db/migrate/20200402115013_add_index_on_modsecurity_to_ingress.rb18
-rw-r--r--db/migrate/20200402115623_add_index_on_successful_deployment_and_environment_id_to_deployments.rb18
-rw-r--r--db/migrate/20200402124802_add_correlation_id_to_project_import_state.rb2
-rw-r--r--db/migrate/20200402185044_create_clusters_applications_fluentd.rb4
-rw-r--r--db/migrate/20200406095930_add_needs_ssl_renewal_user_provided_pages_domains_index.rb20
-rw-r--r--db/migrate/20200406100909_add_needs_ssl_renewal_valid_not_after_pages_domains_index.rb20
-rw-r--r--db/migrate/20200406132529_add_resource_state_events_table.rb20
-rw-r--r--db/migrate/20200406141452_add_index_to_issue_id_and_created_at_on_resource_weight_events.rb19
-rw-r--r--db/migrate/20200406192059_add_write_registry_to_deploy_tokens.rb2
-rw-r--r--db/migrate/20200407171133_add_protected_tag_create_access_levels_user_id_foreign_key.rb2
-rw-r--r--db/migrate/20200407182205_create_partitioned_foreign_keys.rb31
-rw-r--r--db/migrate/20200407222647_create_project_repository_storage_moves.rb31
-rw-r--r--db/migrate/20200408125046_create_ci_freeze_periods.rb30
-rw-r--r--db/migrate/20200408154331_add_protected_branch_merge_access_levels_user_id_foreign_key.rb2
-rw-r--r--db/migrate/20200408154411_add_path_locks_user_id_foreign_key.rb2
-rw-r--r--db/migrate/20200408154455_add_protected_branch_push_access_levels_user_id_foreign_key.rb2
-rw-r--r--db/migrate/20200408154604_add_u2f_registrations_user_id_foreign_key.rb2
-rw-r--r--db/migrate/20200409105455_change_verification_checksum_field_type_in_package_file.rb17
-rw-r--r--db/migrate/20200409105456_add_checksum_index_to_package_file.rb17
-rw-r--r--db/migrate/20200410104828_add_comment_detail_to_services.rb13
-rw-r--r--db/migrate/20200411125656_add_package_scopes_to_deploy_tokens.rb19
-rw-r--r--db/migrate/20200413072059_add_group_owners_can_manage_default_branch_protection_to_application_settings.rb2
-rw-r--r--db/migrate/20200413230056_add_waf_and_cilium_logs_to_applications_fluentd.rb29
-rw-r--r--db/migrate/20200414112444_add_group_id_to_vulnerability_exports.rb10
-rw-r--r--db/migrate/20200414114611_add_group_id_index_and_fk_to_vulnerability_exports.rb20
-rw-r--r--db/migrate/20200414115801_change_project_index_on_vulnerability_exports.rb21
-rw-r--r--db/migrate/20200415153154_add_unique_index_on_plan_name.rb19
-rw-r--r--db/migrate/20200415203024_add_offset_pagination_plan_limit.rb9
-rw-r--r--db/migrate/20200416005331_create_status_page_published_incidents.rb20
-rw-r--r--db/migrate/20200416120128_add_columns_to_terraform_state.rb2
-rw-r--r--db/migrate/20200417044453_create_alert_management_alerts.rb44
-rw-r--r--db/migrate/20200417075843_remove_and_add_foreign_key_to_project_settings.rb19
-rw-r--r--db/migrate/20200417145946_add_locked_to_ci_job_artifact.rb19
-rw-r--r--db/migrate/20200420092011_add_profile_image_guidelines_to_appearances.rb21
-rw-r--r--db/migrate/20200420104303_add_group_import_states_table.rb25
-rw-r--r--db/migrate/20200420104323_add_text_limit_to_group_import_states.rb19
-rw-r--r--db/migrate/20200420115948_create_metrics_users_starred_dashboard.rb25
-rw-r--r--db/migrate/20200420141733_add_index_on_enabled_clusters.rb18
-rw-r--r--db/migrate/20200420172113_add_text_limit_to_sprints_title.rb19
-rw-r--r--db/migrate/20200420172752_add_sprints_foreign_key_to_projects.rb19
-rw-r--r--db/migrate/20200420172927_add_sprints_foreign_key_to_groups.rb19
-rw-r--r--db/migrate/20200420201933_add_check_constraint_to_sprint_must_belong_to_project_or_group.rb19
-rw-r--r--db/migrate/20200421092907_add_index_container_repository_on_name_trigram_to_container_repository.rb18
-rw-r--r--db/migrate/20200421111005_create_daily_build_group_report_results.rb21
-rw-r--r--db/migrate/20200421233150_add_foreign_keys_for_alert_management_alerts.rb19
-rw-r--r--db/migrate/20200422091541_create_ci_instance_variables.rb31
-rw-r--r--db/migrate/20200422213749_create_operations_strategies_user_lists.rb14
-rw-r--r--db/migrate/20200423075720_add_user_id_foreign_key_to_resource_state_events.rb19
-rw-r--r--db/migrate/20200423080334_add_issue_id_foreign_key_to_resource_state_events.rb19
-rw-r--r--db/migrate/20200423080607_add_merge_request_id_foreign_key_to_resource_state_events.rb19
-rw-r--r--db/migrate/20200423081409_add_constraint_to_resource_state_events_must_belong_to_issue_or_merge_request.rb19
-rw-r--r--db/migrate/20200423081441_add_foreign_key_from_users_to_metrics_users_starred_dashboars.rb18
-rw-r--r--db/migrate/20200423081519_add_foreign_key_from_projects_to_metrics_users_starred_dashboars.rb18
-rw-r--r--db/migrate/20200423101529_add_scheduled_at_to_jira_imports.rb9
-rw-r--r--db/migrate/20200424050250_remove_orphaned_invited_members.rb13
-rw-r--r--db/migrate/20200424101920_add_text_limit_to_metrics_users_starred_dashboards_dashboard_path.rb16
-rw-r--r--db/migrate/20200424135319_create_nuget_dependency_link_metadata.rb26
-rw-r--r--db/migrate/20200429001827_sprint_rename_state_to_state_enum.rb17
-rw-r--r--db/migrate/20200429015603_add_fk_to_project_repository_storage_moves.rb19
-rw-r--r--db/migrate/20200429181335_add_default_value_for_file_store_to_lfs_objects.rb19
-rw-r--r--db/migrate/20200429181955_add_default_value_for_file_store_to_ci_job_artifacts.rb19
-rw-r--r--db/migrate/20200429182245_add_default_value_for_store_to_uploads.rb19
-rw-r--r--db/migrate/20200430103158_create_group_wiki_repositories.rb15
-rw-r--r--db/migrate/20200430130048_create_packages_nuget_metadata.rb34
-rw-r--r--db/migrate/20200505164958_add_registry_settings_to_application_settings.rb30
-rw-r--r--db/migrate/20200505171834_add_text_limit_to_container_registry_vendor.rb17
-rw-r--r--db/migrate/20200505172405_add_text_limit_to_container_registry_version.rb17
-rw-r--r--db/migrate/20200507221434_add_container_registry_features_to_application_settings.rb13
-rw-r--r--db/migrate/20200511092246_add_epic_id_to_resource_state_events.rb17
-rw-r--r--db/migrate/20200511092505_add_foreign_key_to_epic_id_on_resource_state_events.rb19
-rw-r--r--db/migrate/20200511115430_add_inherit_from_to_services.rb9
-rw-r--r--db/migrate/20200511115431_add_index_inherit_from_id_to_services.rb21
-rw-r--r--db/migrate/20200511121549_add_group_wiki_repositories_shard_id_foreign_key.rb19
-rw-r--r--db/migrate/20200511121610_add_group_wiki_repositories_group_id_foreign_key.rb19
-rw-r--r--db/migrate/20200511121620_add_group_wiki_repositories_disk_path_limit.rb18
-rw-r--r--db/migrate/20200511130129_remove_deprecated_jenkins_service_records.rb17
-rw-r--r--db/migrate/20200512085150_change_default_value_of_protected_ci_variables_of_application_settings_to_true.rb9
-rw-r--r--db/migrate/20200512164334_sprint_make_state_enum_not_null_and_default.rb15
-rw-r--r--db/migrate/20200513160930_fix_not_null_check_constraint_inconsistency.rb20
-rw-r--r--db/migrate/20200513224143_add_section_to_approval_merge_request_rule.rb21
-rw-r--r--db/migrate/20200513234502_fill_file_store_lfs_objects.rb19
-rw-r--r--db/migrate/20200513235347_fill_store_uploads.rb19
-rw-r--r--db/migrate/20200513235532_fill_file_store_ci_job_artifacts.rb21
-rw-r--r--db/migrate/20200515155620_add_index_non_requested_project_members_on_source_id_source_type.rb17
-rw-r--r--db/post_migrate/20181008200441_remove_circuit_breaker.rb2
-rw-r--r--db/post_migrate/20181013005024_remove_koding_from_application_settings.rb2
-rw-r--r--db/post_migrate/20190404231137_remove_alternate_url_from_geo_nodes.rb2
-rw-r--r--db/post_migrate/20190625184066_remove_sentry_from_application_settings.rb4
-rw-r--r--db/post_migrate/20191030193050_remove_pendo_from_application_settings.rb2
-rw-r--r--db/post_migrate/20191128162854_drop_project_ci_cd_settings_merge_trains_enabled.rb2
-rw-r--r--db/post_migrate/20191202031812_drop_operations_feature_flags_clients_token.rb2
-rw-r--r--db/post_migrate/20200120083607_remove_storage_version_column_from_snippets.rb2
-rw-r--r--db/post_migrate/20200128210353_cleanup_optimistic_locking_nulls.rb2
-rw-r--r--db/post_migrate/20200212052620_readd_template_column_to_services.rb2
-rw-r--r--db/post_migrate/20200217210353_cleanup_optimistic_locking_nulls_pt2.rb7
-rw-r--r--db/post_migrate/20200219193058_remove_state_from_issues.rb2
-rw-r--r--db/post_migrate/20200219193117_remove_state_from_merge_requests.rb2
-rw-r--r--db/post_migrate/20200221142216_remove_repository_storage_from_snippets.rb2
-rw-r--r--db/post_migrate/20200310215714_migrate_saml_identities_to_scim_identities.rb29
-rw-r--r--db/post_migrate/20200312134637_backfill_environment_id_on_deployment_merge_requests.rb27
-rw-r--r--db/post_migrate/20200401091051_remove_reference_columns_from_resource_milestone_events.rb11
-rw-r--r--db/post_migrate/20200403132349_remove_old_index_pages_domains_need_auto_ssl_renewal.rb21
-rw-r--r--db/post_migrate/20200420094444_backfill_snippet_repositories.rb30
-rw-r--r--db/post_migrate/20200420162730_remove_additional_application_settings_rows.rb17
-rw-r--r--db/post_migrate/20200424043515_drop_namespaces_plan_id.rb26
-rw-r--r--db/post_migrate/20200427064130_cleanup_optimistic_locking_nulls_pt2_fixed.rb47
-rw-r--r--db/post_migrate/20200428134356_remove_elastic_experimental_indexer_from_application_settings.rb8
-rw-r--r--db/post_migrate/20200429002150_cleanup_sprints_state_rename.rb17
-rw-r--r--db/post_migrate/20200506085748_update_undefined_confidence_from_occurrences.rb35
-rw-r--r--db/post_migrate/20200506125731_cleanup_user_highest_roles_population.rb23
-rw-r--r--db/post_migrate/20200506154421_migrate_scim_identities_to_saml_for_new_users.rb37
-rw-r--r--db/post_migrate/20200508091106_remove_bot_type.rb29
-rw-r--r--db/post_migrate/20200511080113_add_projects_foreign_key_to_namespaces.rb27
-rw-r--r--db/post_migrate/20200511083541_cleanup_projects_with_missing_namespace.rb263
-rw-r--r--db/post_migrate/20200511092714_update_undefined_confidence_from_vulnerabilities.rb35
-rw-r--r--db/post_migrate/20200511130130_ensure_deprecated_jenkins_service_records_removal.rb17
-rw-r--r--db/post_migrate/20200511145545_change_variable_interpolation_format_in_common_metrics.rb15
-rw-r--r--db/post_migrate/20200511220023_validate_projects_foreign_key_to_namespaces.rb21
-rw-r--r--db/post_migrate/20200513171959_enable_hashed_storage.rb17
-rw-r--r--db/post_migrate/20200514000009_add_not_null_constraint_on_file_store_to_lfs_objects.rb17
-rw-r--r--db/post_migrate/20200514000132_add_not_null_constraint_on_store_to_uploads.rb17
-rw-r--r--db/post_migrate/20200514000340_add_not_null_constraint_on_file_store_to_ci_jobs_artifacts.rb17
-rw-r--r--db/structure.sql733
-rw-r--r--doc/.vale/gitlab/BadgeCapitalization.yml42
-rw-r--r--doc/.vale/gitlab/British.yml106
-rw-r--r--doc/.vale/gitlab/Contractions.yml2
-rw-r--r--doc/.vale/gitlab/Profanity.yml30
-rw-r--r--doc/.vale/gitlab/ReferenceLinks.yml10
-rw-r--r--doc/.vale/gitlab/SubstitutionWarning.yml16
-rw-r--r--doc/.vale/gitlab/Substitutions.yml9
-rw-r--r--doc/.vale/gitlab/spelling-exceptions.txt44
-rw-r--r--doc/README.md20
-rw-r--r--doc/administration/audit_events.md31
-rw-r--r--doc/administration/auditor_users.md10
-rw-r--r--doc/administration/auth/crowd.md5
-rw-r--r--doc/administration/auth/google_secure_ldap.md7
-rw-r--r--doc/administration/auth/how_to_configure_ldap_gitlab_ce/index.md5
-rw-r--r--doc/administration/auth/how_to_configure_ldap_gitlab_ee/index.md6
-rw-r--r--doc/administration/auth/jwt.md5
-rw-r--r--doc/administration/auth/ldap-ee.md3
-rw-r--r--doc/administration/auth/ldap-troubleshooting.md4
-rw-r--r--doc/administration/auth/oidc.md2
-rw-r--r--doc/administration/auth/smartcard.md4
-rw-r--r--doc/administration/availability/index.md138
-rw-r--r--doc/administration/database_load_balancing.md37
-rw-r--r--doc/administration/external_database.md30
-rw-r--r--doc/administration/external_pipeline_validation.md2
-rw-r--r--doc/administration/feature_flags.md99
-rw-r--r--doc/administration/file_hooks.md14
-rw-r--r--doc/administration/geo/disaster_recovery/background_verification.md4
-rw-r--r--doc/administration/geo/disaster_recovery/index.md39
-rw-r--r--doc/administration/geo/disaster_recovery/planned_failover.md2
-rw-r--r--doc/administration/geo/replication/configuration.md10
-rw-r--r--doc/administration/geo/replication/database.md20
-rw-r--r--doc/administration/geo/replication/datatypes.md5
-rw-r--r--doc/administration/geo/replication/external_database.md40
-rw-r--r--doc/administration/geo/replication/geo_validation_tests.md100
-rw-r--r--doc/administration/geo/replication/high_availability.md463
-rw-r--r--doc/administration/geo/replication/index.md12
-rw-r--r--doc/administration/geo/replication/multiple_servers.md459
-rw-r--r--doc/administration/geo/replication/security_review.md4
-rw-r--r--doc/administration/geo/replication/troubleshooting.md35
-rw-r--r--doc/administration/geo/replication/updating_the_geo_nodes.md3
-rw-r--r--doc/administration/geo/replication/using_a_geo_server.md2
-rw-r--r--doc/administration/geo/replication/version_specific_updates.md10
-rw-r--r--doc/administration/git_annex.md33
-rw-r--r--doc/administration/gitaly/img/praefect_architecture_v12_10.pngbin29067 -> 29189 bytes
-rw-r--r--doc/administration/gitaly/index.md140
-rw-r--r--doc/administration/gitaly/praefect.md343
-rw-r--r--doc/administration/gitaly/reference.md4
-rw-r--r--doc/administration/high_availability/README.md6
-rw-r--r--doc/administration/high_availability/consul.md9
-rw-r--r--doc/administration/high_availability/database.md16
-rw-r--r--doc/administration/high_availability/gitaly.md12
-rw-r--r--doc/administration/high_availability/gitlab.md23
-rw-r--r--doc/administration/high_availability/load_balancer.md2
-rw-r--r--doc/administration/high_availability/monitoring_node.md8
-rw-r--r--doc/administration/high_availability/nfs.md14
-rw-r--r--doc/administration/high_availability/nfs_host_client_setup.md6
-rw-r--r--doc/administration/high_availability/pgbouncer.md4
-rw-r--r--doc/administration/high_availability/redis.md138
-rw-r--r--doc/administration/high_availability/redis_source.md42
-rw-r--r--doc/administration/high_availability/sidekiq.md7
-rw-r--r--doc/administration/incoming_email.md15
-rw-r--r--doc/administration/index.md17
-rw-r--r--doc/administration/instance_limits.md37
-rw-r--r--doc/administration/instance_review.md5
-rw-r--r--doc/administration/integration/plantuml.md31
-rw-r--r--doc/administration/integration/terminal.md2
-rw-r--r--doc/administration/issue_closing_pattern.md4
-rw-r--r--doc/administration/job_artifacts.md6
-rw-r--r--doc/administration/job_logs.md4
-rw-r--r--doc/administration/lfs/index.md68
-rw-r--r--doc/administration/libravatar.md4
-rw-r--r--doc/administration/logs.md220
-rw-r--r--doc/administration/monitoring/gitlab_self_monitoring_project/index.md10
-rw-r--r--doc/administration/monitoring/performance/gitlab_configuration.md10
-rw-r--r--doc/administration/monitoring/performance/grafana_configuration.md56
-rw-r--r--doc/administration/monitoring/performance/index.md18
-rw-r--r--doc/administration/monitoring/performance/influxdb_configuration.md192
-rw-r--r--doc/administration/monitoring/performance/influxdb_schema.md102
-rw-r--r--doc/administration/monitoring/prometheus/gitlab_exporter.md6
-rw-r--r--doc/administration/monitoring/prometheus/gitlab_metrics.md165
-rw-r--r--doc/administration/monitoring/prometheus/index.md12
-rw-r--r--doc/administration/monitoring/prometheus/node_exporter.md6
-rw-r--r--doc/administration/monitoring/prometheus/pgbouncer_exporter.md6
-rw-r--r--doc/administration/monitoring/prometheus/postgres_exporter.md6
-rw-r--r--doc/administration/monitoring/prometheus/redis_exporter.md6
-rw-r--r--doc/administration/monitoring/prometheus/registry_exporter.md6
-rw-r--r--doc/administration/object_storage.md12
-rw-r--r--doc/administration/operations/extra_sidekiq_processes.md157
-rw-r--r--doc/administration/operations/fast_ssh_key_lookup.md10
-rw-r--r--doc/administration/operations/index.md1
-rw-r--r--doc/administration/operations/puma.md10
-rw-r--r--doc/administration/operations/sidekiq_memory_killer.md8
-rw-r--r--doc/administration/operations/ssh_certificates.md6
-rw-r--r--doc/administration/operations/unicorn.md4
-rw-r--r--doc/administration/packages/container_registry.md46
-rw-r--r--doc/administration/pages/index.md105
-rw-r--r--doc/administration/pages/source.md4
-rw-r--r--doc/administration/pseudonymizer.md4
-rw-r--r--doc/administration/raketasks/check.md12
-rw-r--r--doc/administration/raketasks/geo.md4
-rw-r--r--doc/administration/raketasks/github_import.md4
-rw-r--r--doc/administration/raketasks/ldap.md6
-rw-r--r--doc/administration/raketasks/maintenance.md56
-rw-r--r--doc/administration/raketasks/praefect.md2
-rw-r--r--doc/administration/raketasks/project_import_export.md6
-rw-r--r--doc/administration/raketasks/storage.md278
-rw-r--r--doc/administration/raketasks/uploads/migrate.md108
-rw-r--r--doc/administration/raketasks/uploads/sanitize.md55
-rw-r--r--doc/administration/reference_architectures/10k_users.md79
-rw-r--r--doc/administration/reference_architectures/1k_users.md82
-rw-r--r--doc/administration/reference_architectures/25k_users.md79
-rw-r--r--doc/administration/reference_architectures/2k_users.md90
-rw-r--r--doc/administration/reference_architectures/3k_users.md82
-rw-r--r--doc/administration/reference_architectures/50k_users.md79
-rw-r--r--doc/administration/reference_architectures/5k_users.md76
-rw-r--r--doc/administration/reference_architectures/img/reference-architectures.pngbin0 -> 47459 bytes
-rw-r--r--doc/administration/reference_architectures/index.md225
-rw-r--r--doc/administration/reply_by_email_postfix_setup.md6
-rw-r--r--doc/administration/repository_checks.md20
-rw-r--r--doc/administration/repository_storage_paths.md16
-rw-r--r--doc/administration/repository_storage_types.md172
-rw-r--r--doc/administration/restart_gitlab.md31
-rw-r--r--doc/administration/scaling/index.md255
-rw-r--r--doc/administration/server_hooks.md33
-rw-r--r--doc/administration/smime_signing_email.md8
-rw-r--r--doc/administration/static_objects_external_storage.md2
-rw-r--r--doc/administration/terraform_state.md13
-rw-r--r--doc/administration/timezone.md4
-rw-r--r--doc/administration/troubleshooting/elasticsearch.md5
-rw-r--r--doc/administration/troubleshooting/gitlab_rails_cheat_sheet.md50
-rw-r--r--doc/administration/troubleshooting/kubernetes_cheat_sheet.md42
-rw-r--r--doc/administration/troubleshooting/log_parsing.md34
-rw-r--r--doc/administration/troubleshooting/postgresql.md10
-rw-r--r--doc/administration/troubleshooting/sidekiq.md3
-rw-r--r--doc/administration/troubleshooting/ssl.md38
-rw-r--r--doc/administration/uploads.md39
-rw-r--r--doc/api/README.md54
-rw-r--r--doc/api/api_resources.md14
-rw-r--r--doc/api/appearance.md3
-rw-r--r--doc/api/applications.md8
-rw-r--r--doc/api/avatar.md2
-rw-r--r--doc/api/award_emoji.md16
-rw-r--r--doc/api/boards.md4
-rw-r--r--doc/api/branches.md15
-rw-r--r--doc/api/broadcast_messages.md8
-rw-r--r--doc/api/commits.md76
-rw-r--r--doc/api/container_registry.md5
-rw-r--r--doc/api/deploy_keys.md2
-rw-r--r--doc/api/deploy_tokens.md4
-rw-r--r--doc/api/epics.md6
-rw-r--r--doc/api/events.md34
-rw-r--r--doc/api/feature_flag_specs.md12
-rw-r--r--doc/api/feature_flag_user_lists.md181
-rw-r--r--doc/api/feature_flags.md2
-rw-r--r--doc/api/features.md44
-rw-r--r--doc/api/freeze_periods.md168
-rw-r--r--doc/api/geo_nodes.md8
-rw-r--r--doc/api/graphql/getting_started.md2
-rw-r--r--doc/api/graphql/reference/gitlab_schema.graphql2033
-rw-r--r--doc/api/graphql/reference/gitlab_schema.json5358
-rw-r--r--doc/api/graphql/reference/index.md384
-rw-r--r--doc/api/group_badges.md2
-rw-r--r--doc/api/group_import_export.md8
-rw-r--r--doc/api/group_milestones.md73
-rw-r--r--doc/api/groups.md159
-rw-r--r--doc/api/import.md4
-rw-r--r--doc/api/instance_level_ci_variables.md162
-rw-r--r--doc/api/issues.md13
-rw-r--r--doc/api/issues_statistics.md6
-rw-r--r--doc/api/jobs.md12
-rw-r--r--doc/api/keys.md4
-rw-r--r--doc/api/labels.md4
-rw-r--r--doc/api/lint.md4
-rw-r--r--doc/api/managed_licenses.md2
-rw-r--r--doc/api/members.md72
-rw-r--r--doc/api/merge_request_approvals.md2
-rw-r--r--doc/api/merge_requests.md45
-rw-r--r--doc/api/metrics_dashboard_annotations.md14
-rw-r--r--doc/api/metrics_user_starred_dashboards.md61
-rw-r--r--doc/api/oauth2.md2
-rw-r--r--doc/api/packages.md23
-rw-r--r--doc/api/pages.md2
-rw-r--r--doc/api/pages_domains.md12
-rw-r--r--doc/api/pipeline_schedules.md20
-rw-r--r--doc/api/pipeline_triggers.md6
-rw-r--r--doc/api/pipelines.md65
-rw-r--r--doc/api/project_badges.md2
-rw-r--r--doc/api/project_import_export.md13
-rw-r--r--doc/api/project_repository_storage_moves.md80
-rw-r--r--doc/api/project_snippets.md4
-rw-r--r--doc/api/projects.md27
-rw-r--r--doc/api/releases/index.md29
-rw-r--r--doc/api/releases/links.md16
-rw-r--r--doc/api/remote_mirrors.md17
-rw-r--r--doc/api/repositories.md2
-rw-r--r--doc/api/repository_files.md20
-rw-r--r--doc/api/runners.md14
-rw-r--r--doc/api/scim.md27
-rw-r--r--doc/api/search.md35
-rw-r--r--doc/api/services.md82
-rw-r--r--doc/api/settings.md32
-rw-r--r--doc/api/snippets.md52
-rw-r--r--doc/api/templates/gitignores.md2
-rw-r--r--doc/api/todos.md9
-rw-r--r--doc/api/users.md23
-rw-r--r--doc/api/vulnerability_exports.md151
-rw-r--r--doc/api/wikis.md4
-rw-r--r--doc/ci/README.md30
-rw-r--r--doc/ci/caching/index.md5
-rw-r--r--doc/ci/ci_cd_for_external_repos/bitbucket_integration.md2
-rw-r--r--doc/ci/cloud_deployment/index.md5
-rw-r--r--doc/ci/directed_acyclic_graph/index.md3
-rw-r--r--doc/ci/docker/using_docker_build.md2
-rw-r--r--doc/ci/docker/using_docker_images.md36
-rw-r--r--doc/ci/docker/using_kaniko.md13
-rw-r--r--doc/ci/environments.md983
-rw-r--r--doc/ci/environments/environments_dashboard.md3
-rw-r--r--doc/ci/environments/incremental_rollouts.md10
-rw-r--r--doc/ci/environments/index.md991
-rw-r--r--doc/ci/environments/protected_environments.md5
-rw-r--r--doc/ci/examples/authenticating-with-hashicorp-vault/index.md6
-rw-r--r--doc/ci/examples/devops_and_game_dev_with_gitlab_ci_cd/index.md16
-rw-r--r--doc/ci/examples/laravel_with_gitlab_and_envoy/index.md2
-rw-r--r--doc/ci/examples/test-and-deploy-python-application-to-heroku.md11
-rw-r--r--doc/ci/examples/test-and-deploy-ruby-application-to-heroku.md18
-rw-r--r--doc/ci/examples/test-scala-application.md4
-rw-r--r--doc/ci/examples/test_phoenix_app_with_gitlab_ci_cd/index.md2
-rw-r--r--doc/ci/img/metrics_reports.pngbin19450 -> 0 bytes
-rw-r--r--doc/ci/img/metrics_reports_v13_0.pngbin0 -> 17996 bytes
-rw-r--r--doc/ci/introduction/index.md11
-rw-r--r--doc/ci/jenkins/index.md17
-rw-r--r--doc/ci/junit_test_reports.md35
-rw-r--r--doc/ci/large_repositories/index.md6
-rw-r--r--doc/ci/merge_request_pipelines/img/merge_request_pipelines_doubled_MR_v12_09.pngbin0 -> 29650 bytes
-rw-r--r--doc/ci/merge_request_pipelines/img/merge_request_pipelines_doubled_branch_v12_09.pngbin0 -> 29680 bytes
-rw-r--r--doc/ci/merge_request_pipelines/index.md128
-rw-r--r--doc/ci/merge_request_pipelines/pipelines_for_merged_results/index.md17
-rw-r--r--doc/ci/merge_request_pipelines/pipelines_for_merged_results/merge_trains/index.md21
-rw-r--r--doc/ci/metrics_reports.md4
-rw-r--r--doc/ci/parent_child_pipelines.md6
-rw-r--r--doc/ci/pipelines/img/pipelines_index.pngbin14896 -> 0 bytes
-rw-r--r--doc/ci/pipelines/img/pipelines_index_v13_0.pngbin0 -> 46101 bytes
-rw-r--r--doc/ci/pipelines/index.md42
-rw-r--r--doc/ci/pipelines/job_artifacts.md227
-rw-r--r--doc/ci/pipelines/schedules.md9
-rw-r--r--doc/ci/pipelines/settings.md21
-rw-r--r--doc/ci/review_apps/index.md21
-rw-r--r--doc/ci/services/mysql.md2
-rw-r--r--doc/ci/services/postgres.md2
-rw-r--r--doc/ci/variables/README.md451
-rw-r--r--doc/ci/variables/img/ci_job_stage_output_example.pngbin68964 -> 156322 bytes
-rw-r--r--doc/ci/variables/img/inherited_group_variables_v12_5.pngbin21349 -> 58215 bytes
-rw-r--r--doc/ci/variables/img/override_value_via_manual_pipeline_output.pngbin72420 -> 310224 bytes
-rw-r--r--doc/ci/variables/img/override_variable_manual_pipeline.pngbin13885 -> 52678 bytes
-rw-r--r--doc/ci/variables/img/variable_types_usage_example.pngbin21983 -> 0 bytes
-rw-r--r--doc/ci/variables/predefined_variables.md13
-rw-r--r--doc/ci/yaml/README.md2471
-rw-r--r--doc/ci/yaml/includes.md213
-rw-r--r--doc/development/README.md28
-rw-r--r--doc/development/api_graphql_styleguide.md16
-rw-r--r--doc/development/api_styleguide.md18
-rw-r--r--doc/development/application_limits.md4
-rw-r--r--doc/development/architecture.md2
-rw-r--r--doc/development/auto_devops.md6
-rw-r--r--doc/development/background_migrations.md11
-rw-r--r--doc/development/changelog.md20
-rw-r--r--doc/development/cicd/img/ci_architecture.pngbin0 -> 102944 bytes
-rw-r--r--doc/development/cicd/index.md75
-rw-r--r--doc/development/code_review.md64
-rw-r--r--doc/development/contributing/index.md100
-rw-r--r--doc/development/contributing/issue_workflow.md12
-rw-r--r--doc/development/contributing/merge_request_workflow.md8
-rw-r--r--doc/development/contributing/style_guides.md2
-rw-r--r--doc/development/creating_enums.md89
-rw-r--r--doc/development/dangerbot.md6
-rw-r--r--doc/development/database/add_foreign_key_to_existing_column.md2
-rw-r--r--doc/development/database_debugging.md30
-rw-r--r--doc/development/database_review.md8
-rw-r--r--doc/development/diffs.md7
-rw-r--r--doc/development/documentation/feature_flags.md188
-rw-r--r--doc/development/documentation/index.md112
-rw-r--r--doc/development/documentation/site_architecture/release_process.md14
-rw-r--r--doc/development/documentation/structure.md10
-rw-r--r--doc/development/documentation/styleguide.md224
-rw-r--r--doc/development/documentation/workflow.md382
-rw-r--r--doc/development/ee_features.md13
-rw-r--r--doc/development/elasticsearch.md2
-rw-r--r--doc/development/emails.md9
-rw-r--r--doc/development/event_tracking/backend.md4
-rw-r--r--doc/development/event_tracking/frontend.md4
-rw-r--r--doc/development/event_tracking/index.md4
-rw-r--r--doc/development/experiment_guide/index.md22
-rw-r--r--doc/development/fe_guide/accessibility.md13
-rw-r--r--doc/development/fe_guide/axios.md18
-rw-r--r--doc/development/fe_guide/design_patterns.md4
-rw-r--r--doc/development/fe_guide/droplab/droplab.md18
-rw-r--r--doc/development/fe_guide/droplab/plugins/ajax.md2
-rw-r--r--doc/development/fe_guide/droplab/plugins/filter.md2
-rw-r--r--doc/development/fe_guide/droplab/plugins/input_setter.md2
-rw-r--r--doc/development/fe_guide/event_tracking.md4
-rw-r--r--doc/development/fe_guide/frontend_faq.md2
-rw-r--r--doc/development/fe_guide/graphql.md24
-rw-r--r--doc/development/fe_guide/icons.md17
-rw-r--r--doc/development/fe_guide/index.md23
-rw-r--r--doc/development/fe_guide/performance.md15
-rw-r--r--doc/development/fe_guide/security.md31
-rw-r--r--doc/development/fe_guide/style/javascript.md5
-rw-r--r--doc/development/fe_guide/style/scss.md51
-rw-r--r--doc/development/fe_guide/style/vue.md6
-rw-r--r--doc/development/fe_guide/tooling.md4
-rw-r--r--doc/development/fe_guide/vue.md216
-rw-r--r--doc/development/fe_guide/vue3_migration.md124
-rw-r--r--doc/development/fe_guide/vuex.md85
-rw-r--r--doc/development/feature_flags/controls.md115
-rw-r--r--doc/development/feature_flags/index.md8
-rw-r--r--doc/development/feature_flags/process.md3
-rw-r--r--doc/development/file_storage.md26
-rw-r--r--doc/development/filtering_by_label.md4
-rw-r--r--doc/development/geo.md6
-rw-r--r--doc/development/geo/framework.md254
-rw-r--r--doc/development/gitaly.md4
-rw-r--r--doc/development/go_guide/index.md74
-rw-r--r--doc/development/hash_indexes.md2
-rw-r--r--doc/development/i18n/externalization.md32
-rw-r--r--doc/development/i18n/proofreader.md2
-rw-r--r--doc/development/img/snowplow_flow.pngbin0 -> 16589 bytes
-rw-r--r--doc/development/img/telemetry_system_overview.pngbin0 -> 429082 bytes
-rw-r--r--doc/development/import_export.md75
-rw-r--r--doc/development/import_project.md3
-rw-r--r--doc/development/instrumentation.md2
-rw-r--r--doc/development/integrations/example_vuln.pngbin0 -> 102950 bytes
-rw-r--r--doc/development/integrations/secure.md164
-rw-r--r--doc/development/integrations/secure_partner_integration.md2
-rw-r--r--doc/development/interacting_components.md2
-rw-r--r--doc/development/internal_api.md10
-rw-r--r--doc/development/lfs.md2
-rw-r--r--doc/development/licensed_feature_availability.md4
-rw-r--r--doc/development/licensing.md64
-rw-r--r--doc/development/logging.md7
-rw-r--r--doc/development/mass_insert.md6
-rw-r--r--doc/development/merge_request_performance_guidelines.md2
-rw-r--r--doc/development/migration_style_guide.md95
-rw-r--r--doc/development/multi_version_compatibility.md62
-rw-r--r--doc/development/namespaces_storage_statistics.md12
-rw-r--r--doc/development/new_fe_guide/development/accessibility.md23
-rw-r--r--doc/development/new_fe_guide/modules/dirty_submit.md2
-rw-r--r--doc/development/newlines_styleguide.md2
-rw-r--r--doc/development/ordering_table_columns.md90
-rw-r--r--doc/development/packages.md12
-rw-r--r--doc/development/performance.md4
-rw-r--r--doc/development/permissions.md6
-rw-r--r--doc/development/pipelines.md535
-rw-r--r--doc/development/policies.md4
-rw-r--r--doc/development/polymorphic_associations.md2
-rw-r--r--doc/development/profiling.md2
-rw-r--r--doc/development/query_recorder.md130
-rw-r--r--doc/development/rake_tasks.md58
-rw-r--r--doc/development/reactive_caching.md12
-rw-r--r--doc/development/repository_mirroring.md13
-rw-r--r--doc/development/scalability.md6
-rw-r--r--doc/development/secure_coding_guidelines.md79
-rw-r--r--doc/development/shell_scripting_guide/index.md20
-rw-r--r--doc/development/sidekiq_style_guide.md51
-rw-r--r--doc/development/telemetry/index.md165
-rw-r--r--doc/development/telemetry/snowplow.md393
-rw-r--r--doc/development/telemetry/usage_ping.md489
-rw-r--r--doc/development/testing_guide/best_practices.md60
-rw-r--r--doc/development/testing_guide/end_to_end/beginners_guide.md340
-rw-r--r--doc/development/testing_guide/end_to_end/img/gl-devops-lifecycle-by-stage-numbers_V12_10.pngbin0 -> 28571 bytes
-rw-r--r--doc/development/testing_guide/end_to_end/index.md2
-rw-r--r--doc/development/testing_guide/end_to_end/page_objects.md47
-rw-r--r--doc/development/testing_guide/end_to_end/quick_start_guide.md621
-rw-r--r--doc/development/testing_guide/end_to_end/rspec_metadata_tests.md2
-rw-r--r--doc/development/testing_guide/end_to_end/style_guide.md1
-rw-r--r--doc/development/testing_guide/flaky_tests.md2
-rw-r--r--doc/development/testing_guide/frontend_testing.md60
-rw-r--r--doc/development/testing_guide/index.md12
-rw-r--r--doc/development/testing_guide/review_apps.md41
-rw-r--r--doc/development/testing_guide/testing_levels.md56
-rw-r--r--doc/development/uploads.md2
-rw-r--r--doc/development/value_stream_analytics.md2
-rw-r--r--doc/development/verifying_database_capabilities.md4
-rw-r--r--doc/development/what_requires_downtime.md67
-rw-r--r--doc/development/windows.md139
-rw-r--r--doc/downgrade_ee_to_ce/README.md7
-rw-r--r--doc/gitlab-basics/command-line-commands.md2
-rw-r--r--doc/gitlab-basics/create-project.md2
-rw-r--r--doc/gitlab-basics/create-your-ssh-keys.md2
-rw-r--r--doc/gitlab-basics/start-using-git.md137
-rw-r--r--doc/gitlab-geo/ha.md4
-rw-r--r--doc/install/README.md12
-rw-r--r--doc/install/aws/index.md234
-rw-r--r--doc/install/azure/index.md47
-rw-r--r--doc/install/google_cloud_platform/index.md19
-rw-r--r--doc/install/installation.md39
-rw-r--r--doc/install/openshift_and_gitlab/index.md27
-rw-r--r--doc/install/relative_url.md9
-rw-r--r--doc/install/requirements.md81
-rw-r--r--doc/integration/README.md2
-rw-r--r--doc/integration/azure.md2
-rw-r--r--doc/integration/elasticsearch.md89
-rw-r--r--doc/integration/github.md7
-rw-r--r--doc/integration/gitlab.md5
-rw-r--r--doc/integration/img/gitlab_app.pngbin32020 -> 0 bytes
-rw-r--r--doc/integration/img/jenkins_gitlab_service_settings.pngbin24094 -> 0 bytes
-rw-r--r--doc/integration/jenkins.md210
-rw-r--r--doc/integration/jenkins_deprecated.md3
-rw-r--r--doc/integration/jira_development_panel.md9
-rw-r--r--doc/integration/oauth_provider.md2
-rw-r--r--doc/integration/omniauth.md2
-rw-r--r--doc/integration/recaptcha.md2
-rw-r--r--doc/integration/slash_commands.md6
-rw-r--r--doc/integration/sourcegraph.md4
-rw-r--r--doc/integration/vault.md3
-rw-r--r--doc/monitoring/performance/influxdb_configuration.md4
-rw-r--r--doc/monitoring/performance/influxdb_schema.md4
-rw-r--r--doc/push_rules/push_rules.md41
-rw-r--r--doc/raketasks/README.md46
-rw-r--r--doc/raketasks/backup_hrz.pngbin11441 -> 0 bytes
-rw-r--r--doc/raketasks/backup_restore.md118
-rw-r--r--doc/raketasks/cleanup.md13
-rw-r--r--doc/raketasks/features.md18
-rw-r--r--doc/raketasks/generate_sample_prometheus_data.md20
-rw-r--r--doc/raketasks/import.md90
-rw-r--r--doc/raketasks/list_repos.md21
-rw-r--r--doc/raketasks/migrate_snippets.md96
-rw-r--r--doc/raketasks/user_management.md117
-rw-r--r--doc/raketasks/web_hooks.md52
-rw-r--r--doc/raketasks/x509_signatures.md21
-rw-r--r--doc/security/README.md4
-rw-r--r--doc/security/asset_proxy.md2
-rw-r--r--doc/security/crime_vulnerability.md24
-rw-r--r--doc/security/rack_attack.md28
-rw-r--r--doc/security/webhooks.md2
-rw-r--r--doc/subscriptions/index.md24
-rw-r--r--doc/telemetry/backend.md34
-rw-r--r--doc/telemetry/frontend.md167
-rw-r--r--doc/telemetry/index.md75
-rw-r--r--doc/telemetry/snowplow.md5
-rw-r--r--doc/tools/email.md3
-rw-r--r--doc/topics/airgap/index.md74
-rw-r--r--doc/topics/authentication/index.md3
-rw-r--r--doc/topics/autodevops/customize.md48
-rw-r--r--doc/topics/autodevops/index.md406
-rw-r--r--doc/topics/autodevops/quick_start_guide.md8
-rw-r--r--doc/topics/autodevops/stages.md404
-rw-r--r--doc/topics/autodevops/upgrading_postgresql.md44
-rw-r--r--doc/topics/git/how_to_install_git/index.md49
-rw-r--r--doc/topics/git/index.md19
-rw-r--r--doc/topics/git/lfs/index.md10
-rw-r--r--doc/topics/git/lfs/migrate_from_git_annex_to_git_lfs.md33
-rw-r--r--doc/topics/git/numerous_undo_possibilities_in_git/index.md20
-rw-r--r--doc/topics/git/partial_clone.md23
-rw-r--r--doc/topics/git/troubleshooting_git.md16
-rw-r--r--doc/topics/git/useful_git_commands.md2
-rw-r--r--doc/topics/gitlab_flow.md2
-rw-r--r--doc/topics/web_application_firewall/img/guide_waf_ingress_disabled_settings_v12_10.pngbin0 -> 51416 bytes
-rw-r--r--doc/university/support/README.md6
-rw-r--r--doc/university/training/end-user/README.md2
-rw-r--r--doc/university/training/topics/agile_git.md2
-rw-r--r--doc/update/README.md32
-rw-r--r--doc/update/patch_versions.md2
-rw-r--r--doc/update/restore_after_failure.md4
-rw-r--r--doc/update/upgrading_from_ce_to_ee.md4
-rw-r--r--doc/user/admin_area/abuse_reports.md2
-rw-r--r--doc/user/admin_area/broadcast_messages.md11
-rw-r--r--doc/user/admin_area/index.md19
-rw-r--r--doc/user/admin_area/license.md7
-rw-r--r--doc/user/admin_area/monitoring/health_check.md26
-rw-r--r--doc/user/admin_area/settings/account_and_limit_settings.md2
-rw-r--r--doc/user/admin_area/settings/email.md5
-rw-r--r--doc/user/admin_area/settings/external_authorization.md7
-rw-r--r--doc/user/admin_area/settings/index.md9
-rw-r--r--doc/user/admin_area/settings/instance_template_repository.md2
-rw-r--r--doc/user/admin_area/settings/protected_paths.md21
-rw-r--r--doc/user/admin_area/settings/rate_limit_on_issues_creation.md2
-rw-r--r--doc/user/admin_area/settings/sign_up_restrictions.md7
-rw-r--r--doc/user/admin_area/settings/terms.md4
-rw-r--r--doc/user/admin_area/settings/usage_statistics.md374
-rw-r--r--doc/user/admin_area/settings/visibility_and_access_controls.md22
-rw-r--r--doc/user/analytics/code_review_analytics.md5
-rw-r--r--doc/user/analytics/img/repository_analytics_v13_0.pngbin0 -> 91755 bytes
-rw-r--r--doc/user/analytics/index.md9
-rw-r--r--doc/user/analytics/productivity_analytics.md7
-rw-r--r--doc/user/analytics/repository_analytics.md40
-rw-r--r--doc/user/analytics/value_stream_analytics.md46
-rw-r--r--doc/user/application_security/configuration/index.md2
-rw-r--r--doc/user/application_security/container_scanning/img/container_scanning_v12_9.pngbin23030 -> 0 bytes
-rw-r--r--doc/user/application_security/container_scanning/img/container_scanning_v13_0.pngbin0 -> 33010 bytes
-rw-r--r--doc/user/application_security/container_scanning/index.md49
-rw-r--r--doc/user/application_security/dast/img/dast_all_v12_9.pngbin12130 -> 0 bytes
-rw-r--r--doc/user/application_security/dast/img/dast_all_v13_0.pngbin0 -> 32346 bytes
-rw-r--r--doc/user/application_security/dast/img/dast_single_v12_9.pngbin96419 -> 0 bytes
-rw-r--r--doc/user/application_security/dast/img/dast_single_v13_0.pngbin0 -> 211373 bytes
-rw-r--r--doc/user/application_security/dast/index.md256
-rw-r--r--doc/user/application_security/dependency_scanning/analyzers.md2
-rw-r--r--doc/user/application_security/dependency_scanning/img/dependency_scanning.pngbin16167 -> 0 bytes
-rw-r--r--doc/user/application_security/dependency_scanning/img/dependency_scanning_v13_0.pngbin0 -> 44921 bytes
-rw-r--r--doc/user/application_security/dependency_scanning/index.md267
-rw-r--r--doc/user/application_security/img/adding_a_dismissal_reason_v13_0.pngbin0 -> 109979 bytes
-rw-r--r--doc/user/application_security/img/dismissed_info_v12_3.pngbin35439 -> 0 bytes
-rw-r--r--doc/user/application_security/img/interacting_with_vulnerability_v13_0.pngbin0 -> 90299 bytes
-rw-r--r--doc/user/application_security/img/interactive_reports.pngbin29814 -> 0 bytes
-rw-r--r--doc/user/application_security/img/security_configuration_page_v12_9.pngbin51545 -> 0 bytes
-rw-r--r--doc/user/application_security/img/security_configuration_page_v13_1.pngbin0 -> 63337 bytes
-rw-r--r--doc/user/application_security/index.md168
-rw-r--r--doc/user/application_security/offline_deployments/index.md9
-rw-r--r--doc/user/application_security/sast/analyzers.md4
-rw-r--r--doc/user/application_security/sast/img/sast_v12_9.pngbin13983 -> 0 bytes
-rw-r--r--doc/user/application_security/sast/img/sast_v13_0.pngbin0 -> 29907 bytes
-rw-r--r--doc/user/application_security/sast/index.md231
-rw-r--r--doc/user/application_security/secret_detection/img/secret-detection-merge-request-ui.pngbin0 -> 100409 bytes
-rw-r--r--doc/user/application_security/secret_detection/index.md67
-rw-r--r--doc/user/application_security/security_dashboard/img/group_security_dashboard_v12_6.pngbin69145 -> 0 bytes
-rw-r--r--doc/user/application_security/security_dashboard/img/group_security_dashboard_v13_0.pngbin0 -> 212401 bytes
-rw-r--r--doc/user/application_security/security_dashboard/img/instance_security_dashboard_export_csv_v13_0.pngbin0 -> 5563 bytes
-rw-r--r--doc/user/application_security/security_dashboard/img/instance_security_dashboard_with_projects_v12_8.pngbin87617 -> 0 bytes
-rw-r--r--doc/user/application_security/security_dashboard/img/instance_security_dashboard_with_projects_v13_0.pngbin0 -> 58505 bytes
-rw-r--r--doc/user/application_security/security_dashboard/img/project_security_dashboard_export_csv_v12_10.png (renamed from doc/user/application_security/security_dashboard/img/project_security_dashboard_export_csv_v12.10.png)bin9766 -> 9766 bytes
-rw-r--r--doc/user/application_security/security_dashboard/img/project_security_dashboard_v12_3.pngbin48767 -> 0 bytes
-rw-r--r--doc/user/application_security/security_dashboard/img/project_security_dashboard_v13_0.pngbin0 -> 199457 bytes
-rw-r--r--doc/user/application_security/security_dashboard/index.md53
-rw-r--r--doc/user/application_security/threat_monitoring/index.md6
-rw-r--r--doc/user/application_security/vulnerabilities/index.md6
-rw-r--r--doc/user/clusters/applications.md97
-rw-r--r--doc/user/clusters/crossplane.md10
-rw-r--r--doc/user/clusters/environments.md8
-rw-r--r--doc/user/clusters/img/fluentd_v12_10.pngbin26438 -> 0 bytes
-rw-r--r--doc/user/clusters/img/fluentd_v13_0.pngbin0 -> 32445 bytes
-rw-r--r--doc/user/clusters/management_project.md6
-rw-r--r--doc/user/compliance/license_compliance/img/license_compliance.pngbin5184 -> 0 bytes
-rw-r--r--doc/user/compliance/license_compliance/img/license_compliance_add_license_v12_3.pngbin28440 -> 0 bytes
-rw-r--r--doc/user/compliance/license_compliance/img/license_compliance_add_license_v13_0.pngbin0 -> 61862 bytes
-rw-r--r--doc/user/compliance/license_compliance/img/license_compliance_decision.pngbin5975 -> 0 bytes
-rw-r--r--doc/user/compliance/license_compliance/img/license_compliance_decision_v13_0.pngbin0 -> 40646 bytes
-rw-r--r--doc/user/compliance/license_compliance/img/license_compliance_pipeline_tab_v12_3.pngbin16435 -> 0 bytes
-rw-r--r--doc/user/compliance/license_compliance/img/license_compliance_pipeline_tab_v13_0.pngbin0 -> 51906 bytes
-rw-r--r--doc/user/compliance/license_compliance/img/license_compliance_search_v12_3.pngbin26074 -> 0 bytes
-rw-r--r--doc/user/compliance/license_compliance/img/license_compliance_search_v13_0.pngbin0 -> 29857 bytes
-rw-r--r--doc/user/compliance/license_compliance/img/license_compliance_settings_v12_3.pngbin14766 -> 0 bytes
-rw-r--r--doc/user/compliance/license_compliance/img/license_compliance_settings_v13_0.pngbin0 -> 17567 bytes
-rw-r--r--doc/user/compliance/license_compliance/img/license_compliance_v13_0.pngbin0 -> 85525 bytes
-rw-r--r--doc/user/compliance/license_compliance/img/license_list_v12_6.pngbin30154 -> 0 bytes
-rw-r--r--doc/user/compliance/license_compliance/img/license_list_v13_0.pngbin0 -> 89930 bytes
-rw-r--r--doc/user/compliance/license_compliance/img/policies_maintainer_add_v12_9.pngbin6745 -> 0 bytes
-rw-r--r--doc/user/compliance/license_compliance/img/policies_maintainer_add_v13_0.pngbin0 -> 22079 bytes
-rw-r--r--doc/user/compliance/license_compliance/img/policies_maintainer_edit_v12_9.pngbin10751 -> 0 bytes
-rw-r--r--doc/user/compliance/license_compliance/img/policies_maintainer_edit_v13_0.pngbin0 -> 40712 bytes
-rw-r--r--doc/user/compliance/license_compliance/img/policies_v12_9.pngbin18966 -> 0 bytes
-rw-r--r--doc/user/compliance/license_compliance/img/policies_v13_0.pngbin0 -> 69562 bytes
-rw-r--r--doc/user/compliance/license_compliance/index.md183
-rw-r--r--doc/user/feature_highlight.md4
-rw-r--r--doc/user/gitlab_com/index.md50
-rw-r--r--doc/user/group/clusters/index.md81
-rw-r--r--doc/user/group/contribution_analytics/index.md5
-rw-r--r--doc/user/group/epics/img/epic_view_v12.3.pngbin61402 -> 0 bytes
-rw-r--r--doc/user/group/epics/img/epic_view_v13.0.pngbin0 -> 54891 bytes
-rw-r--r--doc/user/group/epics/index.md257
-rw-r--r--doc/user/group/epics/manage_epics.md302
-rw-r--r--doc/user/group/index.md35
-rw-r--r--doc/user/group/insights/index.md4
-rw-r--r--doc/user/group/issues_analytics/index.md4
-rw-r--r--doc/user/group/roadmap/img/roadmap_view_v12_10.pngbin46736 -> 0 bytes
-rw-r--r--doc/user/group/roadmap/img/roadmap_view_v13_0.pngbin0 -> 55012 bytes
-rw-r--r--doc/user/group/roadmap/index.md5
-rw-r--r--doc/user/group/saml_sso/index.md10
-rw-r--r--doc/user/group/saml_sso/scim_setup.md82
-rw-r--r--doc/user/group/settings/img/export_panel.pngbin0 -> 200162 bytes
-rw-r--r--doc/user/group/settings/import_export.md98
-rw-r--r--doc/user/group/subgroups/index.md5
-rw-r--r--doc/user/img/gitlab_snippet.pngbin34355 -> 0 bytes
-rw-r--r--doc/user/img/gitlab_snippet_v13_0.pngbin0 -> 28925 bytes
-rw-r--r--doc/user/img/snippet_clone_button_v13_0.pngbin0 -> 33081 bytes
-rw-r--r--doc/user/incident_management/index.md163
-rw-r--r--doc/user/infrastructure/img/terraform_plan_log_v13_0.pngbin0 -> 23683 bytes
-rw-r--r--doc/user/infrastructure/img/terraform_plan_widget_v13_0.pngbin0 -> 10986 bytes
-rw-r--r--doc/user/infrastructure/index.md345
-rw-r--r--doc/user/instance/clusters/index.md6
-rw-r--r--doc/user/instance_statistics/user_cohorts.md1
-rw-r--r--doc/user/markdown.md124
-rw-r--r--doc/user/packages/conan_repository/index.md17
-rw-r--r--doc/user/packages/container_registry/img/container_registry_group_repositories_v12_10.pngbin48791 -> 0 bytes
-rw-r--r--doc/user/packages/container_registry/img/container_registry_group_repositories_v13_0.pngbin0 -> 41813 bytes
-rw-r--r--doc/user/packages/container_registry/img/container_registry_repositories_v12_10.pngbin24128 -> 0 bytes
-rw-r--r--doc/user/packages/container_registry/img/container_registry_repositories_v13_0.pngbin0 -> 44925 bytes
-rw-r--r--doc/user/packages/container_registry/img/container_registry_repositories_with_quickstart_v12_10.pngbin61732 -> 0 bytes
-rw-r--r--doc/user/packages/container_registry/img/container_registry_repositories_with_quickstart_v13_0.pngbin0 -> 48708 bytes
-rw-r--r--doc/user/packages/container_registry/img/container_registry_repository_details_v12.10.pngbin47218 -> 0 bytes
-rw-r--r--doc/user/packages/container_registry/img/container_registry_repository_details_v13.0.pngbin0 -> 32673 bytes
-rw-r--r--doc/user/packages/container_registry/img/container_registry_tags_v12_10.pngbin37743 -> 0 bytes
-rw-r--r--doc/user/packages/container_registry/img/expiration-policy-app.pngbin32054 -> 0 bytes
-rw-r--r--doc/user/packages/container_registry/img/expiration_policy_app_v13_0.pngbin0 -> 61601 bytes
-rw-r--r--doc/user/packages/container_registry/index.md106
-rw-r--r--doc/user/packages/dependency_proxy/img/group_dependency_proxy.pngbin58663 -> 29334 bytes
-rw-r--r--doc/user/packages/dependency_proxy/index.md4
-rw-r--r--doc/user/packages/img/group_packages_list_v12_10.pngbin41637 -> 0 bytes
-rw-r--r--doc/user/packages/img/group_packages_list_v13_0.pngbin0 -> 50889 bytes
-rw-r--r--doc/user/packages/img/package_detail_v12_10.pngbin63529 -> 0 bytes
-rw-r--r--doc/user/packages/img/package_detail_v13_0.pngbin0 -> 46047 bytes
-rw-r--r--doc/user/packages/img/project_packages_list_v12_10.pngbin39163 -> 0 bytes
-rw-r--r--doc/user/packages/img/project_packages_list_v13_0.pngbin0 -> 52752 bytes
-rw-r--r--doc/user/packages/index.md27
-rw-r--r--doc/user/packages/maven_repository/index.md368
-rw-r--r--doc/user/packages/npm_registry/index.md33
-rw-r--r--doc/user/packages/nuget_repository/index.md20
-rw-r--r--doc/user/packages/pypi_repository/index.md198
-rw-r--r--doc/user/permissions.md34
-rw-r--r--doc/user/profile/img/change_password_v13_0.pngbin0 -> 15919 bytes
-rw-r--r--doc/user/profile/img/unknown_sign_in_email_v13_0.pngbin0 -> 20047 bytes
-rw-r--r--doc/user/profile/index.md18
-rw-r--r--doc/user/profile/notifications.md25
-rw-r--r--doc/user/profile/personal_access_tokens.md13
-rw-r--r--doc/user/profile/preferences.md5
-rw-r--r--doc/user/profile/unknown_sign_in_notification.md16
-rw-r--r--doc/user/project/clusters/add_eks_clusters.md6
-rw-r--r--doc/user/project/clusters/add_gke_clusters.md6
-rw-r--r--doc/user/project/clusters/add_remove_clusters.md6
-rw-r--r--doc/user/project/clusters/index.md42
-rw-r--r--doc/user/project/clusters/kubernetes_pod_logs.md58
-rw-r--r--doc/user/project/clusters/runbooks/index.md172
-rw-r--r--doc/user/project/clusters/serverless/aws.md8
-rw-r--r--doc/user/project/clusters/serverless/index.md6
-rw-r--r--doc/user/project/code_owners.md43
-rw-r--r--doc/user/project/deploy_boards.md15
-rw-r--r--doc/user/project/deploy_tokens/index.md30
-rw-r--r--doc/user/project/description_templates.md20
-rw-r--r--doc/user/project/file_lock.md7
-rw-r--r--doc/user/project/git_attributes.md4
-rw-r--r--doc/user/project/img/service_desk_custom_email_address_v13_0.pngbin0 -> 89721 bytes
-rw-r--r--doc/user/project/import/bitbucket.md14
-rw-r--r--doc/user/project/import/bitbucket_server.md4
-rw-r--r--doc/user/project/import/github.md6
-rw-r--r--doc/user/project/import/jira.md9
-rw-r--r--doc/user/project/index.md2
-rw-r--r--doc/user/project/integrations/bamboo.md2
-rw-r--r--doc/user/project/integrations/generic_alerts.md12
-rw-r--r--doc/user/project/integrations/gitlab_slack_application.md29
-rw-r--r--doc/user/project/integrations/img/metrics_dashboard_annotations_ui_v13.0.pngbin0 -> 31654 bytes
-rw-r--r--doc/user/project/integrations/img/panel_context_menu_v12_10.pngbin21057 -> 0 bytes
-rw-r--r--doc/user/project/integrations/img/panel_context_menu_v13_0.pngbin0 -> 34737 bytes
-rw-r--r--doc/user/project/integrations/img/prometheus_dashboard_select_v_13_0.pngbin0 -> 14284 bytes
-rw-r--r--doc/user/project/integrations/img/toggle_metrics_user_starred_dashboard_v13_0.pngbin0 -> 14922 bytes
-rw-r--r--doc/user/project/integrations/img/webex_teams_configuration.pngbin0 -> 250628 bytes
-rw-r--r--doc/user/project/integrations/mattermost_slash_commands.md15
-rw-r--r--doc/user/project/integrations/overview.md17
-rw-r--r--doc/user/project/integrations/prometheus.md255
-rw-r--r--doc/user/project/integrations/prometheus_library/cloudwatch.md6
-rw-r--r--doc/user/project/integrations/prometheus_library/haproxy.md6
-rw-r--r--doc/user/project/integrations/prometheus_library/index.md6
-rw-r--r--doc/user/project/integrations/prometheus_library/kubernetes.md6
-rw-r--r--doc/user/project/integrations/prometheus_library/nginx.md6
-rw-r--r--doc/user/project/integrations/prometheus_library/nginx_ingress.md6
-rw-r--r--doc/user/project/integrations/prometheus_library/nginx_ingress_vts.md6
-rw-r--r--doc/user/project/integrations/prometheus_units.md77
-rw-r--r--doc/user/project/integrations/slack.md2
-rw-r--r--doc/user/project/integrations/webex_teams.md24
-rw-r--r--doc/user/project/integrations/webhooks.md2
-rw-r--r--doc/user/project/integrations/youtrack.md4
-rw-r--r--doc/user/project/issue_board.md461
-rw-r--r--doc/user/project/issues/csv_export.md2
-rw-r--r--doc/user/project/issues/design_management.md6
-rw-r--r--doc/user/project/issues/due_dates.md5
-rw-r--r--doc/user/project/issues/index.md5
-rw-r--r--doc/user/project/issues/managing_issues.md27
-rw-r--r--doc/user/project/issues/related_issues.md6
-rw-r--r--doc/user/project/labels.md19
-rw-r--r--doc/user/project/members/index.md2
-rw-r--r--doc/user/project/merge_requests/accessibility_testing.md22
-rw-r--r--doc/user/project/merge_requests/browser_performance_testing.md164
-rw-r--r--doc/user/project/merge_requests/code_quality.md26
-rw-r--r--doc/user/project/merge_requests/creating_merge_requests.md2
-rw-r--r--doc/user/project/merge_requests/img/accessibility_mr_widget_v13_0.pngbin0 -> 130072 bytes
-rw-r--r--doc/user/project/merge_requests/img/code_quality.pngbin94062 -> 511302 bytes
-rw-r--r--doc/user/project/merge_requests/reviewing_and_managing_merge_requests.md11
-rw-r--r--doc/user/project/merge_requests/test_coverage_visualization.md4
-rw-r--r--doc/user/project/merge_requests/versions.md22
-rw-r--r--doc/user/project/new_ci_build_permissions_model.md2
-rw-r--r--doc/user/project/operations/alert_management.md79
-rw-r--r--doc/user/project/operations/error_tracking.md8
-rw-r--r--doc/user/project/operations/feature_flags.md79
-rw-r--r--doc/user/project/operations/img/alert_detail_v13_0.pngbin0 -> 24097 bytes
-rw-r--r--doc/user/project/operations/img/alert_management_1_v13_0.pngbin0 -> 19152 bytes
-rw-r--r--doc/user/project/operations/img/alert_management_1_v13_1.pngbin0 -> 57133 bytes
-rw-r--r--doc/user/project/operations/img/alert_management_severity_v13_0.pngbin0 -> 10972 bytes
-rw-r--r--doc/user/project/operations/index.md2
-rw-r--r--doc/user/project/operations/linking_to_an_external_dashboard.md2
-rw-r--r--doc/user/project/operations/tracing.md6
-rw-r--r--doc/user/project/pages/custom_domains_ssl_tls_certification/dns_concepts.md3
-rw-r--r--doc/user/project/pages/custom_domains_ssl_tls_certification/index.md8
-rw-r--r--doc/user/project/pages/custom_domains_ssl_tls_certification/lets_encrypt_integration.md3
-rw-r--r--doc/user/project/pages/custom_domains_ssl_tls_certification/ssl_tls_concepts.md3
-rw-r--r--doc/user/project/pages/getting_started/fork_sample_project.md3
-rw-r--r--doc/user/project/pages/getting_started/new_or_existing_website.md3
-rw-r--r--doc/user/project/pages/getting_started/pages_bundled_template.md3
-rw-r--r--doc/user/project/pages/getting_started_part_four.md11
-rw-r--r--doc/user/project/pages/getting_started_part_one.md3
-rw-r--r--doc/user/project/pages/index.md3
-rw-r--r--doc/user/project/pages/introduction.md14
-rw-r--r--doc/user/project/pages/pages_access_control.md6
-rw-r--r--doc/user/project/protected_tags.md4
-rw-r--r--doc/user/project/push_options.md8
-rw-r--r--doc/user/project/quick_actions.md2
-rw-r--r--doc/user/project/releases/img/edit_release_page_v13_0.pngbin0 -> 285708 bytes
-rw-r--r--doc/user/project/releases/img/release_milestone_dropdown_v13_0.pngbin0 -> 138986 bytes
-rw-r--r--doc/user/project/releases/index.md61
-rw-r--r--doc/user/project/repository/file_finder.md17
-rw-r--r--doc/user/project/repository/forking_workflow.md2
-rw-r--r--doc/user/project/repository/img/file_finder_find_button.pngbin14565 -> 0 bytes
-rw-r--r--doc/user/project/repository/img/file_finder_find_button_v12_10.pngbin0 -> 70732 bytes
-rw-r--r--doc/user/project/repository/img/file_finder_find_file.pngbin19478 -> 0 bytes
-rw-r--r--doc/user/project/repository/img/file_finder_find_file_v12_10.pngbin0 -> 59474 bytes
-rw-r--r--doc/user/project/repository/index.md6
-rw-r--r--doc/user/project/repository/repository_mirroring.md22
-rw-r--r--doc/user/project/repository/x509_signed_commits/index.md71
-rw-r--r--doc/user/project/requirements/index.md3
-rw-r--r--doc/user/project/service_desk.md72
-rw-r--r--doc/user/project/settings/import_export.md27
-rw-r--r--doc/user/project/settings/index.md23
-rw-r--r--doc/user/project/settings/project_access_tokens.md55
-rw-r--r--doc/user/project/static_site_editor/img/static_site_editor_v12_10.pngbin50679 -> 0 bytes
-rw-r--r--doc/user/project/static_site_editor/img/wysiwyg_editor_v13_0.pngbin0 -> 49012 bytes
-rw-r--r--doc/user/project/static_site_editor/index.md14
-rw-r--r--doc/user/project/status_page/index.md28
-rw-r--r--doc/user/project/web_ide/img/admin_clientside_evaluation.pngbin9342 -> 0 bytes
-rw-r--r--doc/user/project/web_ide/img/admin_live_preview_v13_0.pngbin0 -> 5508 bytes
-rw-r--r--doc/user/project/web_ide/img/dark_theme_v13.0.pngbin0 -> 852854 bytes
-rw-r--r--doc/user/project/web_ide/img/live_preview_v13_0.png (renamed from doc/user/project/web_ide/img/clientside_evaluation.png)bin60256 -> 60256 bytes
-rw-r--r--doc/user/project/web_ide/img/solarized_light_theme_v13.0.pngbin0 -> 790377 bytes
-rw-r--r--doc/user/project/web_ide/index.md42
-rw-r--r--doc/user/project/wiki/index.md23
-rw-r--r--doc/user/search/advanced_global_search.md8
-rw-r--r--doc/user/search/advanced_search_syntax.md8
-rw-r--r--doc/user/search/img/filter_approved_by_merge_requests_v13_0.pngbin0 -> 46764 bytes
-rw-r--r--doc/user/search/index.md16
-rw-r--r--doc/user/snippets.md67
-rw-r--r--doc/user/todos.md4
-rw-r--r--haml_lint/linter/no_plain_nodes.rb8
-rw-r--r--jest.config.base.js92
-rw-r--r--jest.config.integration.js5
-rw-r--r--jest.config.js103
-rw-r--r--jest.config.unit.js17
-rw-r--r--lib/api/admin/ci/variables.rb137
-rw-r--r--lib/api/api.rb16
-rw-r--r--lib/api/api_guard.rb9
-rw-r--r--lib/api/appearance.rb3
-rw-r--r--lib/api/branches.rb2
-rw-r--r--lib/api/deploy_tokens.rb6
-rw-r--r--lib/api/entities/appearance.rb1
-rw-r--r--lib/api/entities/branch.rb6
-rw-r--r--lib/api/entities/design_management/design.rb16
-rw-r--r--lib/api/entities/freeze_period.rb11
-rw-r--r--lib/api/entities/job_request/artifacts.rb1
-rw-r--r--lib/api/entities/merge_request_basic.rb6
-rw-r--r--lib/api/entities/metrics/user_starred_dashboard.rb11
-rw-r--r--lib/api/entities/personal_snippet.rb3
-rw-r--r--lib/api/entities/project_repository_storage_move.rb14
-rw-r--r--lib/api/entities/release.rb2
-rw-r--r--lib/api/entities/remote_mirror.rb4
-rw-r--r--lib/api/entities/runner_details.rb9
-rw-r--r--lib/api/entities/snippet.rb10
-rw-r--r--lib/api/entities/todo.rb13
-rw-r--r--lib/api/entities/user_basic.rb2
-rw-r--r--lib/api/entities/user_path.rb2
-rw-r--r--lib/api/features.rb22
-rw-r--r--lib/api/freeze_periods.rb107
-rw-r--r--lib/api/groups.rb61
-rw-r--r--lib/api/helpers.rb18
-rw-r--r--lib/api/helpers/internal_helpers.rb2
-rw-r--r--lib/api/helpers/merge_requests_helpers.rb1
-rw-r--r--lib/api/helpers/pagination_strategies.rb34
-rw-r--r--lib/api/helpers/projects_helpers.rb1
-rw-r--r--lib/api/helpers/search_helpers.rb2
-rw-r--r--lib/api/helpers/services_helpers.rb9
-rw-r--r--lib/api/helpers/snippets_helpers.rb17
-rw-r--r--lib/api/internal/base.rb19
-rw-r--r--lib/api/issues.rb2
-rw-r--r--lib/api/job_artifacts.rb4
-rw-r--r--lib/api/members.rb2
-rw-r--r--lib/api/merge_requests.rb7
-rw-r--r--lib/api/metrics/dashboard/annotations.rb45
-rw-r--r--lib/api/metrics/user_starred_dashboards.rb46
-rw-r--r--lib/api/pipelines.rb15
-rw-r--r--lib/api/project_repository_storage_moves.rb34
-rw-r--r--lib/api/project_snippets.rb34
-rw-r--r--lib/api/project_templates.rb12
-rw-r--r--lib/api/projects.rb4
-rw-r--r--lib/api/remote_mirrors.rb2
-rw-r--r--lib/api/search.rb3
-rw-r--r--lib/api/settings.rb16
-rw-r--r--lib/api/snippets.rb31
-rw-r--r--lib/api/wikis.rb14
-rw-r--r--lib/banzai/filter/issue_reference_filter.rb20
-rw-r--r--lib/banzai/filter/upload_link_filter.rb4
-rw-r--r--lib/banzai/filter/user_reference_filter.rb2
-rw-r--r--lib/banzai/pipeline.rb2
-rw-r--r--lib/banzai/reference_parser/design_parser.rb31
-rw-r--r--lib/banzai/renderer.rb7
-rw-r--r--lib/container_registry/client.rb21
-rw-r--r--lib/container_registry/config.rb2
-rw-r--r--lib/csv_builder.rb11
-rw-r--r--lib/csv_builders/single_batch.rb11
-rw-r--r--lib/declarative_policy.rb17
-rw-r--r--lib/feature.rb6
-rw-r--r--lib/gitlab/alert_management/alert_params.rb41
-rw-r--r--lib/gitlab/alert_management/alert_status_counts.rb53
-rw-r--r--lib/gitlab/alerting/alert.rb12
-rw-r--r--lib/gitlab/alerting/notification_payload_parser.rb8
-rw-r--r--lib/gitlab/analytics/cycle_analytics/median.rb6
-rw-r--r--lib/gitlab/auth.rb4
-rw-r--r--lib/gitlab/auth/auth_finders.rb27
-rw-r--r--lib/gitlab/auth/ldap/access.rb4
-rw-r--r--lib/gitlab/auth/ldap/adapter.rb4
-rw-r--r--lib/gitlab/auth/ldap/config.rb4
-rw-r--r--lib/gitlab/auth/ldap/person.rb4
-rw-r--r--lib/gitlab/auth/ldap/user.rb4
-rw-r--r--lib/gitlab/auth/o_auth/auth_hash.rb4
-rw-r--r--lib/gitlab/auth/o_auth/provider.rb14
-rw-r--r--lib/gitlab/auth/o_auth/user.rb4
-rw-r--r--lib/gitlab/auth/result.rb4
-rw-r--r--lib/gitlab/auth/saml/config.rb4
-rw-r--r--lib/gitlab/auth/saml/user.rb4
-rw-r--r--lib/gitlab/background_migration/backfill_environment_id_deployment_merge_requests.rb47
-rw-r--r--lib/gitlab/background_migration/backfill_snippet_repositories.rb94
-rw-r--r--lib/gitlab/background_migration/migrate_issue_trackers_sensitive_data.rb2
-rw-r--r--lib/gitlab/background_migration/populate_user_highest_roles_table.rb2
-rw-r--r--lib/gitlab/background_migration/remove_undefined_occurrence_confidence_level.rb13
-rw-r--r--lib/gitlab/background_migration/remove_undefined_vulnerability_confidence_level.rb13
-rw-r--r--lib/gitlab/blob_helper.rb16
-rw-r--r--lib/gitlab/chat/responder/mattermost.rb131
-rw-r--r--lib/gitlab/chat_name_token.rb2
-rw-r--r--lib/gitlab/checks/base_checker.rb3
-rw-r--r--lib/gitlab/checks/change_access.rb4
-rw-r--r--lib/gitlab/checks/diff_check.rb3
-rw-r--r--lib/gitlab/ci/ansi2html.rb2
-rw-r--r--lib/gitlab/ci/ansi2json/state.rb2
-rw-r--r--lib/gitlab/ci/build/artifacts/metadata.rb4
-rw-r--r--lib/gitlab/ci/build/artifacts/metadata/entry.rb2
-rw-r--r--lib/gitlab/ci/config/entry/artifacts.rb10
-rw-r--r--lib/gitlab/ci/config/entry/reports.rb4
-rw-r--r--lib/gitlab/ci/config/entry/trigger.rb9
-rw-r--r--lib/gitlab/ci/cron_parser.rb11
-rw-r--r--lib/gitlab/ci/features.rb18
-rw-r--r--lib/gitlab/ci/parsers.rb8
-rw-r--r--lib/gitlab/ci/parsers/accessibility/pa11y.rb33
-rw-r--r--lib/gitlab/ci/parsers/terraform/tfplan.rb35
-rw-r--r--lib/gitlab/ci/parsers/test/junit.rb8
-rw-r--r--lib/gitlab/ci/pipeline/chain/command.rb15
-rw-r--r--lib/gitlab/ci/pipeline/chain/sequence.rb3
-rw-r--r--lib/gitlab/ci/pipeline/seed/build/resource_group.rb1
-rw-r--r--lib/gitlab/ci/reports/accessibility_reports.rb46
-rw-r--r--lib/gitlab/ci/reports/accessibility_reports_comparer.rb55
-rw-r--r--lib/gitlab/ci/reports/terraform_reports.rb27
-rw-r--r--lib/gitlab/ci/reports/test_reports.rb6
-rw-r--r--lib/gitlab/ci/reports/test_suite.rb19
-rw-r--r--lib/gitlab/ci/status/build/failed.rb4
-rw-r--r--lib/gitlab/ci/templates/Auto-DevOps.gitlab-ci.yml3
-rw-r--r--lib/gitlab/ci/templates/Deploy-ECS.gitlab-ci.yml31
-rw-r--r--lib/gitlab/ci/templates/Jobs/Browser-Performance-Testing.gitlab-ci.yml14
-rw-r--r--lib/gitlab/ci/templates/Jobs/Build.gitlab-ci.yml5
-rw-r--r--lib/gitlab/ci/templates/Jobs/Code-Quality.gitlab-ci.yml11
-rw-r--r--lib/gitlab/ci/templates/Jobs/DAST-Default-Branch-Deploy.gitlab-ci.yml44
-rw-r--r--lib/gitlab/ci/templates/Jobs/Deploy.gitlab-ci.yml154
-rw-r--r--lib/gitlab/ci/templates/Jobs/Deploy/ECS.gitlab-ci.yml30
-rw-r--r--lib/gitlab/ci/templates/Jobs/Test.gitlab-ci.yml16
-rw-r--r--lib/gitlab/ci/templates/Scala.gitlab-ci.yml8
-rw-r--r--lib/gitlab/ci/templates/Security/Container-Scanning.gitlab-ci.yml21
-rw-r--r--lib/gitlab/ci/templates/Security/DAST.gitlab-ci.yml26
-rw-r--r--lib/gitlab/ci/templates/Security/Dependency-Scanning.gitlab-ci.yml120
-rw-r--r--lib/gitlab/ci/templates/Security/License-Management.gitlab-ci.yml34
-rw-r--r--lib/gitlab/ci/templates/Security/License-Scanning.gitlab-ci.yml25
-rw-r--r--lib/gitlab/ci/templates/Security/SAST.gitlab-ci.yml214
-rw-r--r--lib/gitlab/ci/templates/Security/Secure-Binaries.gitlab-ci.yml246
-rw-r--r--lib/gitlab/ci/templates/Terraform.gitlab-ci.yml2
-rw-r--r--lib/gitlab/ci/templates/Verify/Accessibility.gitlab-ci.yml4
-rw-r--r--lib/gitlab/ci/templates/Workflows/Branch-Pipelines.gitlab-ci.yml7
-rw-r--r--lib/gitlab/ci/templates/Workflows/MergeRequest-Pipelines.gitlab-ci.yml8
-rw-r--r--lib/gitlab/ci/yaml_processor.rb2
-rw-r--r--lib/gitlab/cleanup/orphan_lfs_file_references.rb2
-rw-r--r--lib/gitlab/code_navigation_path.rb16
-rw-r--r--lib/gitlab/config_checker/external_database_checker.rb27
-rw-r--r--lib/gitlab/cycle_analytics/group_stage_summary.rb54
-rw-r--r--lib/gitlab/cycle_analytics/stage_summary.rb5
-rw-r--r--lib/gitlab/cycle_analytics/summary/commit.rb6
-rw-r--r--lib/gitlab/cycle_analytics/summary/deploy.rb16
-rw-r--r--lib/gitlab/cycle_analytics/summary/deployment_frequency.rb3
-rw-r--r--lib/gitlab/cycle_analytics/summary/group/base.rb26
-rw-r--r--lib/gitlab/cycle_analytics/summary/group/deploy.rb31
-rw-r--r--lib/gitlab/cycle_analytics/summary/group/deployment_frequency.rb33
-rw-r--r--lib/gitlab/cycle_analytics/summary/group/issue.rb44
-rw-r--r--lib/gitlab/cycle_analytics/summary/issue.rb11
-rw-r--r--lib/gitlab/cycle_analytics/summary/value.rb42
-rw-r--r--lib/gitlab/cycle_analytics/summary_helper.rb5
-rw-r--r--lib/gitlab/danger/changelog.rb4
-rw-r--r--lib/gitlab/danger/commit_linter.rb2
-rw-r--r--lib/gitlab/danger/emoji_checker.rb6
-rw-r--r--lib/gitlab/danger/helper.rb17
-rw-r--r--lib/gitlab/danger/request_helper.rb2
-rw-r--r--lib/gitlab/danger/teammate.rb31
-rw-r--r--lib/gitlab/data_builder/wiki_page.rb3
-rw-r--r--lib/gitlab/database/batch_count.rb8
-rw-r--r--lib/gitlab/database/count/reltuples_count_strategy.rb2
-rw-r--r--lib/gitlab/database/migration_helpers.rb182
-rw-r--r--lib/gitlab/database/partitioning_migration_helpers.rb122
-rw-r--r--lib/gitlab/database/partitioning_migration_helpers/partitioned_foreign_key.rb13
-rw-r--r--lib/gitlab/database/partitioning_migration_helpers/partitioned_foreign_key_validator.rb28
-rw-r--r--lib/gitlab/database/rename_reserved_paths_migration/v1/rename_base.rb2
-rw-r--r--lib/gitlab/database/schema_helpers.rb52
-rw-r--r--lib/gitlab/database/with_lock_retries.rb14
-rw-r--r--lib/gitlab/dependency_linker/json_linker.rb2
-rw-r--r--lib/gitlab/diff/file.rb17
-rw-r--r--lib/gitlab/diff/formatters/text_formatter.rb3
-rw-r--r--lib/gitlab/diff/highlight_cache.rb2
-rw-r--r--lib/gitlab/diff/position.rb2
-rw-r--r--lib/gitlab/discussions_diff/highlight_cache.rb2
-rw-r--r--lib/gitlab/elasticsearch/logs/lines.rb9
-rw-r--r--lib/gitlab/email/handler.rb10
-rw-r--r--lib/gitlab/email/hook/smime_signature_interceptor.rb7
-rw-r--r--lib/gitlab/email/message/repository_push.rb2
-rw-r--r--lib/gitlab/email/smime/certificate.rb40
-rw-r--r--lib/gitlab/email/smime/signer.rb25
-rw-r--r--lib/gitlab/emoji.rb4
-rw-r--r--lib/gitlab/exclusive_lease.rb20
-rw-r--r--lib/gitlab/exclusive_lease_helpers.rb39
-rw-r--r--lib/gitlab/exclusive_lease_helpers/sleeping_lock.rb50
-rw-r--r--lib/gitlab/experimentation.rb74
-rw-r--r--lib/gitlab/external_authorization/response.rb2
-rw-r--r--lib/gitlab/git/attributes_parser.rb2
-rw-r--r--lib/gitlab/git/commit.rb14
-rw-r--r--lib/gitlab/git/tag.rb21
-rw-r--r--lib/gitlab/git_access_design.rb28
-rw-r--r--lib/gitlab/git_access_snippet.rb16
-rw-r--r--lib/gitlab/gitaly_client.rb7
-rw-r--r--lib/gitlab/github_import/parallel_importer.rb4
-rw-r--r--lib/gitlab/gl_repository.rb11
-rw-r--r--lib/gitlab/gl_repository/repo_type.rb6
-rw-r--r--lib/gitlab/gon_helper.rb4
-rw-r--r--lib/gitlab/grape_logging/loggers/cloudflare_logger.rb18
-rw-r--r--lib/gitlab/grape_logging/loggers/context_logger.rb14
-rw-r--r--lib/gitlab/grape_logging/loggers/exception_logger.rb28
-rw-r--r--lib/gitlab/graphql/authorize/authorize_field_service.rb8
-rw-r--r--lib/gitlab/graphql/pagination/keyset/connection.rb2
-rw-r--r--lib/gitlab/graphql/pagination/keyset/order_info.rb2
-rw-r--r--lib/gitlab/graphql/query_analyzers/logger_analyzer.rb4
-rw-r--r--lib/gitlab/graphql/variables.rb2
-rw-r--r--lib/gitlab/group_search_results.rb4
-rw-r--r--lib/gitlab/health_checks/puma_check.rb2
-rw-r--r--lib/gitlab/i18n.rb40
-rw-r--r--lib/gitlab/import_export.rb8
-rw-r--r--lib/gitlab/import_export/after_export_strategies/web_upload_strategy.rb5
-rw-r--r--lib/gitlab/import_export/design_repo_restorer.rb15
-rw-r--r--lib/gitlab/import_export/design_repo_saver.rb19
-rw-r--r--lib/gitlab/import_export/group/group_restorer.rb71
-rw-r--r--lib/gitlab/import_export/group/import_export.yml2
-rw-r--r--lib/gitlab/import_export/group/legacy_import_export.yml86
-rw-r--r--lib/gitlab/import_export/group/legacy_tree_restorer.rb2
-rw-r--r--lib/gitlab/import_export/group/legacy_tree_saver.rb2
-rw-r--r--lib/gitlab/import_export/group/tree_restorer.rb140
-rw-r--r--lib/gitlab/import_export/group/tree_saver.rb72
-rw-r--r--lib/gitlab/import_export/importer.rb26
-rw-r--r--lib/gitlab/import_export/project/base_task.rb20
-rw-r--r--lib/gitlab/import_export/project/export_task.rb13
-rw-r--r--lib/gitlab/import_export/project/import_export.yml27
-rw-r--r--lib/gitlab/import_export/project/import_task.rb34
-rw-r--r--lib/gitlab/import_export/project/object_builder.rb8
-rw-r--r--lib/gitlab/import_export/project/relation_factory.rb9
-rw-r--r--lib/gitlab/import_export/project/tree_restorer.rb2
-rw-r--r--lib/gitlab/import_export/project/tree_saver.rb2
-rw-r--r--lib/gitlab/import_export/relation_tree_restorer.rb4
-rw-r--r--lib/gitlab/import_export/snippets_repo_restorer.rb1
-rw-r--r--lib/gitlab/import_export/snippets_repo_saver.rb2
-rw-r--r--lib/gitlab/instrumentation/redis.rb3
-rw-r--r--lib/gitlab/instrumentation_helper.rb9
-rw-r--r--lib/gitlab/jira_import/base_importer.rb5
-rw-r--r--lib/gitlab/jira_import/handle_labels_service.rb39
-rw-r--r--lib/gitlab/jira_import/issue_serializer.rb62
-rw-r--r--lib/gitlab/jira_import/issues_importer.rb2
-rw-r--r--lib/gitlab/jira_import/labels_importer.rb26
-rw-r--r--lib/gitlab/jira_import/metadata_collector.rb57
-rw-r--r--lib/gitlab/jira_import/user_mapper.rb53
-rw-r--r--lib/gitlab/json.rb34
-rw-r--r--lib/gitlab/kubernetes/helm.rb2
-rw-r--r--lib/gitlab/kubernetes/helm/api.rb6
-rw-r--r--lib/gitlab/kubernetes/helm/delete_command.rb2
-rw-r--r--lib/gitlab/kubernetes/helm/parsers/list_v2.rb37
-rw-r--r--lib/gitlab/kubernetes/kube_client.rb72
-rw-r--r--lib/gitlab/kubernetes/network_policy.rb91
-rw-r--r--lib/gitlab/logging/cloudflare_helper.rb23
-rw-r--r--lib/gitlab/lograge/custom_options.rb8
-rw-r--r--lib/gitlab/mail_room.rb3
-rw-r--r--lib/gitlab/markdown_cache.rb2
-rw-r--r--lib/gitlab/metrics.rb93
-rw-r--r--lib/gitlab/metrics/dashboard/url.rb2
-rw-r--r--lib/gitlab/metrics/exporter/sidekiq_exporter.rb2
-rw-r--r--lib/gitlab/metrics/influx_db.rb183
-rw-r--r--lib/gitlab/metrics/method_call.rb13
-rw-r--r--lib/gitlab/metrics/metric.rb54
-rw-r--r--lib/gitlab/metrics/rack_middleware.rb4
-rw-r--r--lib/gitlab/metrics/samplers/database_sampler.rb58
-rw-r--r--lib/gitlab/metrics/samplers/influx_sampler.rb49
-rw-r--r--lib/gitlab/metrics/samplers/puma_sampler.rb2
-rw-r--r--lib/gitlab/metrics/samplers/ruby_sampler.rb26
-rw-r--r--lib/gitlab/metrics/sidekiq_middleware.rb2
-rw-r--r--lib/gitlab/metrics/subscribers/action_view.rb8
-rw-r--r--lib/gitlab/metrics/system.rb81
-rw-r--r--lib/gitlab/metrics/transaction.rb56
-rw-r--r--lib/gitlab/middleware/multipart.rb1
-rw-r--r--lib/gitlab/middleware/read_only/controller.rb4
-rw-r--r--lib/gitlab/omniauth_initializer.rb83
-rw-r--r--lib/gitlab/pagination/keyset.rb11
-rw-r--r--lib/gitlab/patch/draw_route.rb4
-rw-r--r--lib/gitlab/performance_bar.rb2
-rw-r--r--lib/gitlab/phabricator_import/conduit/response.rb2
-rw-r--r--lib/gitlab/project_search_results.rb36
-rw-r--r--lib/gitlab/prometheus/metric_group.rb3
-rw-r--r--lib/gitlab/prometheus/queries/query_additional_metrics.rb4
-rw-r--r--lib/gitlab/prometheus_client.rb2
-rw-r--r--lib/gitlab/quick_actions/issuable_actions.rb2
-rw-r--r--lib/gitlab/regex.rb8
-rw-r--r--lib/gitlab/request_context.rb7
-rw-r--r--lib/gitlab/rugged_instrumentation.rb3
-rw-r--r--lib/gitlab/runtime.rb22
-rw-r--r--lib/gitlab/sanitizers/exif.rb2
-rw-r--r--lib/gitlab/search/parsed_query.rb4
-rw-r--r--lib/gitlab/search_results.rb13
-rw-r--r--lib/gitlab/services/logger.rb11
-rw-r--r--lib/gitlab/setup_helper.rb3
-rw-r--r--lib/gitlab/sidekiq_config/cli_methods.rb3
-rw-r--r--lib/gitlab/sidekiq_daemon/monitor.rb2
-rw-r--r--lib/gitlab/sidekiq_logging/json_formatter.rb15
-rw-r--r--lib/gitlab/sidekiq_logging/structured_logger.rb11
-rw-r--r--lib/gitlab/sidekiq_middleware.rb5
-rw-r--r--lib/gitlab/sidekiq_middleware/admin_mode/client.rb8
-rw-r--r--lib/gitlab/sidekiq_middleware/arguments_logger.rb2
-rw-r--r--lib/gitlab/sidekiq_middleware/duplicate_jobs/duplicate_job.rb2
-rw-r--r--lib/gitlab/sidekiq_middleware/extra_done_log_metadata.rb25
-rw-r--r--lib/gitlab/slash_commands/presenters/issue_base.rb4
-rw-r--r--lib/gitlab/snippet_search_results.rb34
-rw-r--r--lib/gitlab/static_site_editor/config.rb7
-rw-r--r--lib/gitlab/task_helpers.rb8
-rw-r--r--lib/gitlab/testing/clear_process_memory_cache_middleware.rb17
-rw-r--r--lib/gitlab/testing/clear_thread_memory_cache_middleware.rb17
-rw-r--r--lib/gitlab/thread_memory_cache.rb15
-rw-r--r--lib/gitlab/tracking.rb7
-rw-r--r--lib/gitlab/tree_summary.rb36
-rw-r--r--lib/gitlab/uploads/migration_helper.rb3
-rw-r--r--lib/gitlab/url_builder.rb30
-rw-r--r--lib/gitlab/usage_data.rb203
-rw-r--r--lib/gitlab/usage_data_counters/base_counter.rb10
-rw-r--r--lib/gitlab/usage_data_counters/designs_counter.rb42
-rw-r--r--lib/gitlab/usage_data_counters/note_counter.rb10
-rw-r--r--lib/gitlab/usage_data_counters/search_counter.rb4
-rw-r--r--lib/gitlab/usage_data_counters/web_ide_counter.rb52
-rw-r--r--lib/gitlab/user_access_snippet.rb14
-rw-r--r--lib/gitlab/utils.rb6
-rw-r--r--lib/gitlab/utils/measuring.rb58
-rw-r--r--lib/gitlab/wiki_pages.rb3
-rw-r--r--lib/gitlab/with_request_store.rb14
-rw-r--r--lib/gitlab/workhorse.rb15
-rw-r--r--lib/gitlab/x509/signature.rb4
-rw-r--r--lib/gitlab/x509/tag.rb41
-rw-r--r--lib/gitlab_danger.rb2
-rw-r--r--lib/google_api/auth.rb7
-rw-r--r--lib/mattermost/client.rb2
-rw-r--r--lib/quality/helm3_client.rb6
-rw-r--r--lib/quality/helm_client.rb114
-rw-r--r--lib/quality/test_level.rb1
-rw-r--r--lib/rspec_flaky/listener.rb2
-rw-r--r--lib/static_model.rb6
-rwxr-xr-xlib/support/init.d/gitlab26
-rw-r--r--lib/support/init.d/gitlab.default.example6
-rw-r--r--lib/system_check/app/hashed_storage_all_projects_check.rb22
-rw-r--r--lib/system_check/app/hashed_storage_enabled_check.rb23
-rw-r--r--lib/system_check/rake_task/app_task.rb4
-rw-r--r--lib/tasks/file_hooks.rake5
-rw-r--r--lib/tasks/gemojione.rake6
-rw-r--r--lib/tasks/gitlab/gitaly.rake7
-rw-r--r--lib/tasks/gitlab/import_export/export.rake6
-rw-r--r--lib/tasks/gitlab/import_export/import.rake6
-rw-r--r--lib/tasks/gitlab/shell.rake2
-rw-r--r--lib/tasks/gitlab/snippets.rake91
-rw-r--r--lib/tasks/gitlab/track_deployment.rake9
-rw-r--r--lib/tasks/gitlab/workhorse.rake2
-rw-r--r--lib/tasks/sidekiq.rake38
-rw-r--r--locale/am_ET/gitlab.po1007
-rw-r--r--locale/ar_SA/gitlab.po1027
-rw-r--r--locale/bg/gitlab.po1011
-rw-r--r--locale/bn_BD/gitlab.po1007
-rw-r--r--locale/bn_IN/gitlab.po1007
-rw-r--r--locale/bs_BA/gitlab.po1216
-rw-r--r--locale/ca_ES/gitlab.po1011
-rw-r--r--locale/cs_CZ/gitlab.po1017
-rw-r--r--locale/cy_GB/gitlab.po1027
-rw-r--r--locale/da_DK/gitlab.po1007
-rw-r--r--locale/de/gitlab.po1069
-rw-r--r--locale/el_GR/gitlab.po1007
-rw-r--r--locale/eo/gitlab.po1007
-rw-r--r--locale/es/gitlab.po1135
-rw-r--r--locale/et_EE/gitlab.po1007
-rw-r--r--locale/fa_IR/gitlab.po1007
-rw-r--r--locale/fi_FI/gitlab.po1007
-rw-r--r--locale/fil_PH/gitlab.po1007
-rw-r--r--locale/fr/gitlab.po1049
-rw-r--r--locale/gitlab.pot1953
-rw-r--r--locale/gl_ES/gitlab.po1007
-rw-r--r--locale/he_IL/gitlab.po1017
-rw-r--r--locale/hi_IN/gitlab.po1007
-rw-r--r--locale/hr_HR/gitlab.po1012
-rw-r--r--locale/hu_HU/gitlab.po1007
-rw-r--r--locale/id_ID/gitlab.po1002
-rw-r--r--locale/it/gitlab.po1007
-rw-r--r--locale/ja/gitlab.po3320
-rw-r--r--locale/ka_GE/gitlab.po1007
-rw-r--r--locale/ko/gitlab.po1022
-rw-r--r--locale/ku_TR/gitlab.po1007
-rw-r--r--locale/mn_MN/gitlab.po1007
-rw-r--r--locale/nb_NO/gitlab.po1007
-rw-r--r--locale/nl_NL/gitlab.po1011
-rw-r--r--locale/pa_IN/gitlab.po1007
-rw-r--r--locale/pl_PL/gitlab.po1017
-rw-r--r--locale/pt_BR/gitlab.po1053
-rw-r--r--locale/pt_PT/gitlab.po1011
-rw-r--r--locale/ro_RO/gitlab.po1012
-rw-r--r--locale/ru/gitlab.po3775
-rw-r--r--locale/sk_SK/gitlab.po1017
-rw-r--r--locale/sl_SI/gitlab.po1017
-rw-r--r--locale/sq_AL/gitlab.po1007
-rw-r--r--locale/sr_CS/gitlab.po1012
-rw-r--r--locale/sr_SP/gitlab.po1012
-rw-r--r--locale/sv_SE/gitlab.po1007
-rw-r--r--locale/sw_KE/gitlab.po1007
-rw-r--r--locale/tr_TR/gitlab.po1055
-rw-r--r--locale/uk/gitlab.po1583
-rw-r--r--locale/ur_PK/gitlab.po1007
-rw-r--r--locale/uz_UZ/gitlab.po1007
-rw-r--r--locale/vi_VN/gitlab.po1002
-rw-r--r--locale/zh_CN/gitlab.po1156
-rw-r--r--locale/zh_HK/gitlab.po1002
-rw-r--r--locale/zh_TW/gitlab.po1004
-rw-r--r--package.json26
-rw-r--r--qa/Gemfile7
-rw-r--r--qa/Gemfile.lock63
-rw-r--r--qa/README.md2
-rw-r--r--qa/Rakefile12
-rw-r--r--qa/qa.rb29
-rw-r--r--qa/qa/fixtures/monitored_auto_devops/.gitlab-ci.yml5
-rw-r--r--qa/qa/git/repository.rb62
-rw-r--r--qa/qa/page/base.rb9
-rw-r--r--qa/qa/page/component/breadcrumbs.rb4
-rw-r--r--qa/qa/page/component/ci_badge_link.rb4
-rw-r--r--qa/qa/page/component/clone_panel.rb4
-rw-r--r--qa/qa/page/component/confirm_modal.rb4
-rw-r--r--qa/qa/page/component/custom_metric.rb49
-rw-r--r--qa/qa/page/component/design_management.rb82
-rw-r--r--qa/qa/page/component/groups_filter.rb4
-rw-r--r--qa/qa/page/component/issuable/common.rb4
-rw-r--r--qa/qa/page/component/lazy_loader.rb4
-rw-r--r--qa/qa/page/component/legacy_clone_panel.rb4
-rw-r--r--qa/qa/page/component/note.rb4
-rw-r--r--qa/qa/page/component/project/templates.rb15
-rw-r--r--qa/qa/page/component/select2.rb8
-rw-r--r--qa/qa/page/component/web_ide/alert.rb8
-rw-r--r--qa/qa/page/dashboard/snippet/edit.rb36
-rw-r--r--qa/qa/page/dashboard/snippet/new.rb26
-rw-r--r--qa/qa/page/dashboard/snippet/show.rb65
-rw-r--r--qa/qa/page/file/shared/commit_button.rb4
-rw-r--r--qa/qa/page/file/shared/commit_message.rb4
-rw-r--r--qa/qa/page/file/shared/editor.rb4
-rw-r--r--qa/qa/page/group/sub_menus/common.rb3
-rw-r--r--qa/qa/page/main/terms.rb24
-rw-r--r--qa/qa/page/page_concern.rb16
-rw-r--r--qa/qa/page/profile/personal_access_tokens.rb14
-rw-r--r--qa/qa/page/profile/ssh_keys.rb18
-rw-r--r--qa/qa/page/project/issue/index.rb21
-rw-r--r--qa/qa/page/project/issue/show.rb18
-rw-r--r--qa/qa/page/project/job/show.rb72
-rw-r--r--qa/qa/page/project/new.rb6
-rw-r--r--qa/qa/page/project/operations/kubernetes/index.rb4
-rw-r--r--qa/qa/page/project/operations/metrics/show.rb24
-rw-r--r--qa/qa/page/project/pipeline/index.rb76
-rw-r--r--qa/qa/page/project/pipeline/show.rb112
-rw-r--r--qa/qa/page/project/settings/advanced.rb4
-rw-r--r--qa/qa/page/project/settings/ci_cd.rb9
-rw-r--r--qa/qa/page/project/settings/ci_variables.rb2
-rw-r--r--qa/qa/page/project/settings/common.rb13
-rw-r--r--qa/qa/page/project/settings/deploy_keys.rb2
-rw-r--r--qa/qa/page/project/settings/general_pipelines.rb23
-rw-r--r--qa/qa/page/project/settings/incidents.rb37
-rw-r--r--qa/qa/page/project/settings/integrations.rb19
-rw-r--r--qa/qa/page/project/settings/main.rb2
-rw-r--r--qa/qa/page/project/settings/merge_request.rb2
-rw-r--r--qa/qa/page/project/settings/operations.rb23
-rw-r--r--qa/qa/page/project/settings/repository.rb4
-rw-r--r--qa/qa/page/project/settings/services/prometheus.rb36
-rw-r--r--qa/qa/page/project/sub_menus/ci_cd.rb6
-rw-r--r--qa/qa/page/project/sub_menus/common.rb1
-rw-r--r--qa/qa/page/project/sub_menus/issues.rb6
-rw-r--r--qa/qa/page/project/sub_menus/operations.rb12
-rw-r--r--qa/qa/page/project/sub_menus/project.rb6
-rw-r--r--qa/qa/page/project/sub_menus/repository.rb8
-rw-r--r--qa/qa/page/project/sub_menus/settings.rb15
-rw-r--r--qa/qa/page/search/results.rb76
-rw-r--r--qa/qa/resource/api_fabricator.rb33
-rw-r--r--qa/qa/resource/kubernetes_cluster.rb68
-rw-r--r--qa/qa/resource/kubernetes_cluster/base.rb40
-rw-r--r--qa/qa/resource/kubernetes_cluster/project_cluster.rb72
-rw-r--r--qa/qa/resource/pipeline.rb53
-rw-r--r--qa/qa/resource/project.rb34
-rw-r--r--qa/qa/resource/runner.rb27
-rw-r--r--qa/qa/resource/ssh_key.rb10
-rw-r--r--qa/qa/resource/visibility.rb17
-rw-r--r--qa/qa/runtime/api/client.rb2
-rw-r--r--qa/qa/runtime/project.rb35
-rw-r--r--qa/qa/scenario/test/integration/gitaly_ha.rb13
-rw-r--r--qa/qa/service/docker_run/gitlab_runner.rb59
-rw-r--r--qa/qa/service/praefect_manager.rb27
-rw-r--r--qa/qa/specs/features/api/1_manage/rate_limits_spec.rb2
-rw-r--r--qa/qa/specs/features/api/1_manage/users_spec.rb2
-rw-r--r--qa/qa/specs/features/api/2_plan/closes_issue_via_pushing_a_commit_spec.rb2
-rw-r--r--qa/qa/specs/features/api/3_create/repository/files_spec.rb1
-rw-r--r--qa/qa/specs/features/api/3_create/repository/project_archive_compare_spec.rb1
-rw-r--r--qa/qa/specs/features/browser_ui/2_plan/email/trigger_email_notification_spec.rb2
-rw-r--r--qa/qa/specs/features/browser_ui/2_plan/issue/create_issue_spec.rb2
-rw-r--r--qa/qa/specs/features/browser_ui/2_plan/issue_boards/focus_mode_spec.rb28
-rw-r--r--qa/qa/specs/features/browser_ui/3_create/design_management/add_design_add_annotation.rb31
-rw-r--r--qa/qa/specs/features/browser_ui/3_create/gitaly/high_availability_spec.rb63
-rw-r--r--qa/qa/specs/features/browser_ui/3_create/merge_request/merge_merge_request_from_fork_spec.rb2
-rw-r--r--qa/qa/specs/features/browser_ui/3_create/repository/add_ssh_key_spec.rb2
-rw-r--r--qa/qa/specs/features/browser_ui/3_create/repository/move_project_create_fork_spec.rb4
-rw-r--r--qa/qa/specs/features/browser_ui/3_create/repository/push_over_http_file_size_spec.rb10
-rw-r--r--qa/qa/specs/features/browser_ui/3_create/snippet/clone_push_pull_personal_snippet_spec.rb102
-rw-r--r--qa/qa/specs/features/browser_ui/3_create/snippet/create_snippet_spec.rb2
-rw-r--r--qa/qa/specs/features/browser_ui/3_create/web_ide/review_merge_request_spec.rb2
-rw-r--r--qa/qa/specs/features/browser_ui/4_verify/runner/register_runner_spec.rb1
-rw-r--r--qa/qa/specs/features/browser_ui/4_verify/testing/view_code_coverage_spec.rb61
-rw-r--r--qa/qa/specs/features/browser_ui/6_release/pipeline/parent_child_pipelines_dependent_relationship_spec.rb2
-rw-r--r--qa/qa/specs/features/browser_ui/6_release/pipeline/parent_child_pipelines_independent_relationship_spec.rb2
-rw-r--r--qa/qa/specs/features/browser_ui/7_configure/auto_devops/create_project_with_auto_devops_spec.rb2
-rw-r--r--qa/qa/specs/features/browser_ui/7_configure/kubernetes/kubernetes_integration_spec.rb10
-rw-r--r--qa/qa/specs/features/browser_ui/8_monitor/all_monitor_core_features_spec.rb155
-rw-r--r--qa/qa/specs/features/browser_ui/8_monitor/apm/dashboards_spec.rb97
-rw-r--r--qa/qa/specs/features/sanity/framework_spec.rb2
-rw-r--r--qa/qa/specs/helpers/quarantine.rb130
-rw-r--r--qa/qa/specs/runner.rb2
-rw-r--r--qa/qa/support/api.rb3
-rw-r--r--qa/qa/tools/delete_test_ssh_keys.rb80
-rw-r--r--qa/qa/vendor/jenkins/page/configure_job.rb15
-rw-r--r--qa/qa/vendor/jenkins/page/last_job_console.rb18
-rw-r--r--qa/spec/factory/resource/user_spec.rb8
-rw-r--r--qa/spec/git/repository_spec.rb127
-rw-r--r--qa/spec/resource/ssh_key_spec.rb21
-rw-r--r--qa/spec/service/docker_run/gitlab_runner_spec.rb173
-rw-r--r--qa/spec/specs/helpers/quarantine_spec.rb8
-rw-r--r--qa/spec/specs/runner_spec.rb14
-rw-r--r--rubocop/cop/avoid_keyword_arguments_in_sidekiq_workers.rb20
-rw-r--r--rubocop/cop/gitlab/change_timzone.rb20
-rw-r--r--rubocop/cop/gitlab/json.rb36
-rw-r--r--rubocop/cop/inject_enterprise_edition_module.rb10
-rw-r--r--rubocop/cop/migration/add_column.rb50
-rw-r--r--rubocop/cop/migration/add_column_with_default.rb28
-rw-r--r--rubocop/cop/migration/add_columns_to_wide_tables.rb1
-rw-r--r--rubocop/cop/migration/add_concurrent_foreign_key.rb14
-rw-r--r--rubocop/cop/migration/add_limit_to_string_columns.rb59
-rw-r--r--rubocop/cop/migration/add_limit_to_text_columns.rb121
-rw-r--r--rubocop/cop/migration/prevent_strings.rb52
-rw-r--r--rubocop/cop/migration/reversible_add_column_with_default.rb35
-rw-r--r--rubocop/cop/migration/with_lock_retries_disallowed_method.rb58
-rw-r--r--rubocop/cop/migration/with_lock_retries_without_ddl_transaction.rb36
-rw-r--r--rubocop/cop/performance/ar_exists_and_present_blank.rb57
-rw-r--r--rubocop/cop/rspec/empty_line_after_shared_example.rb64
-rw-r--r--rubocop/migration_helpers.rb6
-rwxr-xr-xscripts/build_assets_image20
-rwxr-xr-xscripts/clean-old-cached-assets2
-rw-r--r--scripts/create_postgres_user.sh2
-rwxr-xr-xscripts/frontend/webpack_dev_server.js68
-rw-r--r--scripts/gitaly_test.rb2
-rw-r--r--scripts/prepare_build.sh12
-rwxr-xr-xscripts/prepare_postgres_fdw.sh2
-rwxr-xr-xscripts/regenerate-schema182
-rwxr-xr-xscripts/review_apps/automated_cleanup.rb21
-rwxr-xr-xscripts/review_apps/gcp_cleanup.sh9
-rwxr-xr-xscripts/review_apps/review-apps.sh46
-rw-r--r--scripts/rspec_helpers.sh44
-rwxr-xr-xscripts/schema_changed.sh4
-rwxr-xr-xscripts/security-harness2
-rw-r--r--scripts/utils.sh35
-rw-r--r--spec/channels/application_cable/connection_spec.rb47
-rw-r--r--spec/channels/issues_channel_spec.rb36
-rw-r--r--spec/config/application_spec.rb12
-rw-r--r--spec/config/mail_room_spec.rb3
-rw-r--r--spec/config/smime_signature_settings_spec.rb9
-rw-r--r--spec/controllers/admin/ci/variables_controller_spec.rb70
-rw-r--r--spec/controllers/admin/clusters_controller_spec.rb25
-rw-r--r--spec/controllers/admin/requests_profiles_controller_spec.rb6
-rw-r--r--spec/controllers/admin/users_controller_spec.rb2
-rw-r--r--spec/controllers/application_controller_spec.rb73
-rw-r--r--spec/controllers/concerns/issuable_actions_spec.rb2
-rw-r--r--spec/controllers/concerns/metrics_dashboard_spec.rb33
-rw-r--r--spec/controllers/dashboard/projects_controller_spec.rb51
-rw-r--r--spec/controllers/google_api/authorizations_controller_spec.rb26
-rw-r--r--spec/controllers/graphql_controller_spec.rb65
-rw-r--r--spec/controllers/groups/clusters_controller_spec.rb25
-rw-r--r--spec/controllers/groups/group_links_controller_spec.rb36
-rw-r--r--spec/controllers/groups/registry/repositories_controller_spec.rb30
-rw-r--r--spec/controllers/groups/settings/integrations_controller_spec.rb4
-rw-r--r--spec/controllers/groups/settings/repository_controller_spec.rb4
-rw-r--r--spec/controllers/groups_controller_spec.rb59
-rw-r--r--spec/controllers/help_controller_spec.rb7
-rw-r--r--spec/controllers/import/bitbucket_controller_spec.rb2
-rw-r--r--spec/controllers/ldap/omniauth_callbacks_controller_spec.rb51
-rw-r--r--spec/controllers/oauth/token_info_controller_spec.rb10
-rw-r--r--spec/controllers/omniauth_callbacks_controller_spec.rb11
-rw-r--r--spec/controllers/profiles/emails_controller_spec.rb24
-rw-r--r--spec/controllers/projects/alert_management_controller_spec.rb59
-rw-r--r--spec/controllers/projects/artifacts_controller_spec.rb42
-rw-r--r--spec/controllers/projects/branches_controller_spec.rb56
-rw-r--r--spec/controllers/projects/ci/daily_build_group_report_results_controller_spec.rb80
-rw-r--r--spec/controllers/projects/clusters_controller_spec.rb27
-rw-r--r--spec/controllers/projects/cycle_analytics/events_controller_spec.rb6
-rw-r--r--spec/controllers/projects/design_management/designs/raw_images_controller_spec.rb153
-rw-r--r--spec/controllers/projects/design_management/designs/resized_image_controller_spec.rb148
-rw-r--r--spec/controllers/projects/environments/prometheus_api_controller_spec.rb4
-rw-r--r--spec/controllers/projects/environments_controller_spec.rb36
-rw-r--r--spec/controllers/projects/grafana_api_controller_spec.rb3
-rw-r--r--spec/controllers/projects/graphs_controller_spec.rb20
-rw-r--r--spec/controllers/projects/import/jira_controller_spec.rb15
-rw-r--r--spec/controllers/projects/issues_controller_spec.rb281
-rw-r--r--spec/controllers/projects/logs_controller_spec.rb111
-rw-r--r--spec/controllers/projects/merge_requests_controller_spec.rb307
-rw-r--r--spec/controllers/projects/mirrors_controller_spec.rb2
-rw-r--r--spec/controllers/projects/pipelines_controller_spec.rb119
-rw-r--r--spec/controllers/projects/prometheus/alerts_controller_spec.rb2
-rw-r--r--spec/controllers/projects/refs_controller_spec.rb34
-rw-r--r--spec/controllers/projects/registry/repositories_controller_spec.rb28
-rw-r--r--spec/controllers/projects/service_hook_logs_controller_spec.rb2
-rw-r--r--spec/controllers/projects/settings/access_tokens_controller_spec.rb190
-rw-r--r--spec/controllers/projects/settings/repository_controller_spec.rb4
-rw-r--r--spec/controllers/projects/snippets_controller_spec.rb38
-rw-r--r--spec/controllers/projects/static_site_editor_controller_spec.rb14
-rw-r--r--spec/controllers/projects/usage_ping_controller_spec.rb68
-rw-r--r--spec/controllers/projects/wikis_controller_spec.rb26
-rw-r--r--spec/controllers/projects_controller_spec.rb26
-rw-r--r--spec/controllers/registrations_controller_spec.rb32
-rw-r--r--spec/controllers/repositories/git_http_controller_spec.rb47
-rw-r--r--spec/controllers/search_controller_spec.rb10
-rw-r--r--spec/controllers/sessions_controller_spec.rb13
-rw-r--r--spec/controllers/snippets_controller_spec.rb90
-rw-r--r--spec/db/schema_spec.rb18
-rw-r--r--spec/factories/alert_management/alerts.rb81
-rw-r--r--spec/factories/appearances.rb1
-rw-r--r--spec/factories/ci/builds.rb18
-rw-r--r--spec/factories/ci/daily_build_group_report_results.rb14
-rw-r--r--spec/factories/ci/daily_report_results.rb13
-rw-r--r--spec/factories/ci/freeze_periods.rb10
-rw-r--r--spec/factories/ci/instance_variables.rb13
-rw-r--r--spec/factories/ci/job_artifacts.rb74
-rw-r--r--spec/factories/ci/pipelines.rb24
-rw-r--r--spec/factories/ci/test_case.rb2
-rw-r--r--spec/factories/clusters/applications/helm.rb24
-rw-r--r--spec/factories/deploy_tokens.rb8
-rw-r--r--spec/factories/design_management/actions.rb13
-rw-r--r--spec/factories/design_management/design_at_version.rb23
-rw-r--r--spec/factories/design_management/designs.rb128
-rw-r--r--spec/factories/design_management/versions.rb142
-rw-r--r--spec/factories/events.rb14
-rw-r--r--spec/factories/git_wiki_commit_details.rb15
-rw-r--r--spec/factories/groups.rb10
-rw-r--r--spec/factories/identities.rb2
-rw-r--r--spec/factories/iterations.rb60
-rw-r--r--spec/factories/merge_requests.rb24
-rw-r--r--spec/factories/metrics/users_starred_dasboards.rb9
-rw-r--r--spec/factories/notes.rb19
-rw-r--r--spec/factories/plan_limits.rb11
-rw-r--r--spec/factories/plans.rb13
-rw-r--r--spec/factories/project_repository_storage_moves.rb14
-rw-r--r--spec/factories/project_wikis.rb11
-rw-r--r--spec/factories/projects.rb6
-rw-r--r--spec/factories/remote_mirrors.rb5
-rw-r--r--spec/factories/resource_state_event.rb10
-rw-r--r--spec/factories/sequences.rb1
-rw-r--r--spec/factories/services.rb7
-rw-r--r--spec/factories/uploads.rb6
-rw-r--r--spec/factories/usage_data.rb26
-rw-r--r--spec/factories/users.rb6
-rw-r--r--spec/factories/wiki_pages.rb44
-rw-r--r--spec/factories/wikis.rb21
-rw-r--r--spec/features/admin/admin_appearance_spec.rb18
-rw-r--r--spec/features/admin/admin_browses_logs_spec.rb20
-rw-r--r--spec/features/admin/admin_hooks_spec.rb18
-rw-r--r--spec/features/admin/admin_mode/login_spec.rb77
-rw-r--r--spec/features/admin/admin_settings_spec.rb40
-rw-r--r--spec/features/admin/admin_users_impersonation_tokens_spec.rb4
-rw-r--r--spec/features/boards/boards_spec.rb54
-rw-r--r--spec/features/boards/focus_mode_spec.rb17
-rw-r--r--spec/features/boards/sidebar_spec.rb2
-rw-r--r--spec/features/commits/user_view_commits_spec.rb22
-rw-r--r--spec/features/dashboard/help_spec.rb21
-rw-r--r--spec/features/dashboard/issues_spec.rb2
-rw-r--r--spec/features/dashboard/snippets_spec.rb43
-rw-r--r--spec/features/dashboard/todos/todos_spec.rb42
-rw-r--r--spec/features/error_tracking/user_filters_errors_by_status_spec.rb2
-rw-r--r--spec/features/error_tracking/user_sees_error_index_spec.rb2
-rw-r--r--spec/features/explore/groups_spec.rb2
-rw-r--r--spec/features/global_search_spec.rb2
-rw-r--r--spec/features/groups/import_export/export_file_spec.rb59
-rw-r--r--spec/features/groups/issues_spec.rb23
-rw-r--r--spec/features/groups/members/leave_group_spec.rb2
-rw-r--r--spec/features/groups/members/manage_groups_spec.rb85
-rw-r--r--spec/features/groups/members/master_adds_member_with_expiration_date_spec.rb68
-rw-r--r--spec/features/groups/navbar_spec.rb52
-rw-r--r--spec/features/groups_spec.rb36
-rw-r--r--spec/features/help_pages_spec.rb32
-rw-r--r--spec/features/issues/bulk_assignment_labels_spec.rb2
-rw-r--r--spec/features/issues/filtered_search/filter_issues_spec.rb47
-rw-r--r--spec/features/issues/filtered_search/visual_tokens_spec.rb18
-rw-r--r--spec/features/issues/spam_issues_spec.rb121
-rw-r--r--spec/features/issues/update_issues_spec.rb2
-rw-r--r--spec/features/issues/user_creates_branch_and_merge_request_spec.rb12
-rw-r--r--spec/features/issues/user_sees_sidebar_updates_in_realtime_spec.rb32
-rw-r--r--spec/features/markdown/copy_as_gfm_spec.rb6
-rw-r--r--spec/features/markdown/metrics_spec.rb2
-rw-r--r--spec/features/merge_request/maintainer_edits_fork_spec.rb2
-rw-r--r--spec/features/merge_request/user_interacts_with_batched_mr_diffs_spec.rb2
-rw-r--r--spec/features/merge_request/user_posts_diff_notes_spec.rb4
-rw-r--r--spec/features/merge_request/user_posts_notes_spec.rb5
-rw-r--r--spec/features/merge_request/user_resolves_conflicts_spec.rb4
-rw-r--r--spec/features/merge_request/user_resolves_diff_notes_and_discussions_resolve_spec.rb54
-rw-r--r--spec/features/merge_request/user_sees_notes_from_forked_project_spec.rb1
-rw-r--r--spec/features/merge_requests/user_mass_updates_spec.rb2
-rw-r--r--spec/features/milestones/user_creates_milestone_spec.rb6
-rw-r--r--spec/features/milestones/user_views_milestone_spec.rb6
-rw-r--r--spec/features/profiles/emails_spec.rb11
-rw-r--r--spec/features/profiles/personal_access_tokens_spec.rb4
-rw-r--r--spec/features/projects/activity/user_sees_design_comment_spec.rb51
-rw-r--r--spec/features/projects/branches/user_creates_branch_spec.rb10
-rw-r--r--spec/features/projects/commit/comments/user_edits_comments_spec.rb6
-rw-r--r--spec/features/projects/environments_pod_logs_spec.rb2
-rw-r--r--spec/features/projects/files/user_browses_files_spec.rb39
-rw-r--r--spec/features/projects/graph_spec.rb2
-rw-r--r--spec/features/projects/import_export/export_file_spec.rb4
-rw-r--r--spec/features/projects/issues/design_management/user_paginates_designs_spec.rb40
-rw-r--r--spec/features/projects/issues/design_management/user_permissions_upload_spec.rb24
-rw-r--r--spec/features/projects/issues/design_management/user_uploads_designs_spec.rb63
-rw-r--r--spec/features/projects/issues/design_management/user_views_design_images_spec.rb41
-rw-r--r--spec/features/projects/issues/design_management/user_views_design_spec.rb29
-rw-r--r--spec/features/projects/issues/design_management/user_views_designs_spec.rb47
-rw-r--r--spec/features/projects/issues/design_management/user_views_designs_with_svg_xss_spec.rb55
-rw-r--r--spec/features/projects/members/list_spec.rb19
-rw-r--r--spec/features/projects/navbar_spec.rb21
-rw-r--r--spec/features/projects/pipelines/pipeline_spec.rb72
-rw-r--r--spec/features/projects/serverless/functions_spec.rb2
-rw-r--r--spec/features/projects/services/disable_triggers_spec.rb10
-rw-r--r--spec/features/projects/services/prometheus_external_alerts_spec.rb20
-rw-r--r--spec/features/projects/services/user_activates_issue_tracker_spec.rb59
-rw-r--r--spec/features/projects/services/user_activates_jira_spec.rb42
-rw-r--r--spec/features/projects/services/user_activates_mattermost_slash_command_spec.rb204
-rw-r--r--spec/features/projects/services/user_activates_slack_slash_command_spec.rb9
-rw-r--r--spec/features/projects/services/user_activates_youtrack_spec.rb91
-rw-r--r--spec/features/projects/settings/access_tokens_spec.rb93
-rw-r--r--spec/features/projects/settings/operations_settings_spec.rb2
-rw-r--r--spec/features/projects/settings/project_settings_spec.rb30
-rw-r--r--spec/features/projects/settings/registry_settings_spec.rb2
-rw-r--r--spec/features/projects/settings/repository_settings_spec.rb16
-rw-r--r--spec/features/projects/settings/user_interacts_with_deploy_keys_spec.rb10
-rw-r--r--spec/features/projects/settings/user_sees_revoke_deploy_token_modal_spec.rb2
-rw-r--r--spec/features/projects/snippets/create_snippet_spec.rb17
-rw-r--r--spec/features/projects/snippets/user_updates_snippet_spec.rb20
-rw-r--r--spec/features/projects/user_sees_user_popover_spec.rb18
-rw-r--r--spec/features/projects/wiki/markdown_preview_spec.rb2
-rw-r--r--spec/features/projects/wiki/shortcuts_spec.rb2
-rw-r--r--spec/features/projects/wiki/user_creates_wiki_page_spec.rb35
-rw-r--r--spec/features/projects/wiki/user_git_access_wiki_page_spec.rb2
-rw-r--r--spec/features/projects/wiki/user_updates_wiki_page_spec.rb4
-rw-r--r--spec/features/projects/wiki/user_views_wiki_in_project_page_spec.rb2
-rw-r--r--spec/features/projects/wiki/user_views_wiki_page_spec.rb2
-rw-r--r--spec/features/projects/wiki/user_views_wiki_pages_spec.rb6
-rw-r--r--spec/features/projects/wiki/users_views_asciidoc_page_with_includes_spec.rb2
-rw-r--r--spec/features/search/user_searches_for_code_spec.rb4
-rw-r--r--spec/features/search/user_searches_for_wiki_pages_spec.rb2
-rw-r--r--spec/features/security/project/internal_access_spec.rb6
-rw-r--r--spec/features/security/project/private_access_spec.rb2
-rw-r--r--spec/features/security/project/public_access_spec.rb10
-rw-r--r--spec/features/snippets/search_snippets_spec.rb2
-rw-r--r--spec/features/snippets/spam_snippets_spec.rb76
-rw-r--r--spec/features/snippets/user_creates_snippet_spec.rb19
-rw-r--r--spec/features/snippets/user_edits_snippet_spec.rb20
-rw-r--r--spec/features/static_site_editor_spec.rb4
-rw-r--r--spec/features/users/signup_spec.rb4
-rw-r--r--spec/finders/alert_management/alerts_finder_spec.rb298
-rw-r--r--spec/finders/artifacts_finder_spec.rb31
-rw-r--r--spec/finders/ci/daily_build_group_report_results_finder_spec.rb72
-rw-r--r--spec/finders/ci/job_artifacts_finder_spec.rb31
-rw-r--r--spec/finders/container_repositories_finder_spec.rb29
-rw-r--r--spec/finders/design_management/designs_finder_spec.rb105
-rw-r--r--spec/finders/design_management/versions_finder_spec.rb129
-rw-r--r--spec/finders/fork_projects_finder_spec.rb2
-rw-r--r--spec/finders/freeze_periods_finder_spec.rb59
-rw-r--r--spec/finders/issues_finder_spec.rb50
-rw-r--r--spec/finders/members_finder_spec.rb4
-rw-r--r--spec/finders/merge_requests_finder_spec.rb11
-rw-r--r--spec/finders/metrics/users_starred_dashboards_finder_spec.rb55
-rw-r--r--spec/finders/projects/serverless/functions_finder_spec.rb1
-rw-r--r--spec/finders/releases_finder_spec.rb11
-rw-r--r--spec/finders/todos_finder_spec.rb4
-rw-r--r--spec/fixtures/accessibility/pa11y_with_errors.json109
-rw-r--r--spec/fixtures/accessibility/pa11y_with_invalid_url.json12
-rw-r--r--spec/fixtures/accessibility/pa11y_without_errors.json8
-rw-r--r--spec/fixtures/api/schemas/cluster_list.json14
-rw-r--r--spec/fixtures/api/schemas/cluster_status.json2
-rw-r--r--spec/fixtures/api/schemas/entities/accessibility_error.json40
-rw-r--r--spec/fixtures/api/schemas/entities/accessibility_reports_comparer.json43
-rw-r--r--spec/fixtures/api/schemas/entities/discussion.json11
-rw-r--r--spec/fixtures/api/schemas/entities/note_user_entity.json3
-rw-r--r--spec/fixtures/api/schemas/entities/user.json3
-rw-r--r--spec/fixtures/api/schemas/pipeline_schedule.json4
-rw-r--r--spec/fixtures/api/schemas/public_api/v4/branch.json6
-rw-r--r--spec/fixtures/api/schemas/public_api/v4/freeze_period.json20
-rw-r--r--spec/fixtures/api/schemas/public_api/v4/freeze_periods.json5
-rw-r--r--spec/fixtures/api/schemas/public_api/v4/issue.json9
-rw-r--r--spec/fixtures/api/schemas/public_api/v4/members.json5
-rw-r--r--spec/fixtures/api/schemas/public_api/v4/notes.json4
-rw-r--r--spec/fixtures/api/schemas/public_api/v4/project_repository_storage_move.json20
-rw-r--r--spec/fixtures/api/schemas/public_api/v4/project_repository_storage_moves.json6
-rw-r--r--spec/fixtures/api/schemas/public_api/v4/snippets.json7
-rw-r--r--spec/fixtures/config/mail_room_enabled.yml2
-rw-r--r--spec/fixtures/config/redis_cache_new_format_host.yml8
-rw-r--r--spec/fixtures/config/redis_new_format_host.yml8
-rw-r--r--spec/fixtures/config/redis_queues_new_format_host.yml8
-rw-r--r--spec/fixtures/config/redis_shared_state_new_format_host.yml8
-rw-r--r--spec/fixtures/group_export.tar.gzbin3546 -> 2921 bytes
-rw-r--r--spec/fixtures/group_export_invalid_subrelations.tar.gzbin3602 -> 2868 bytes
-rw-r--r--spec/fixtures/helm/helm_list_v2_prometheus_deployed.json.gzbin0 -> 338 bytes
-rw-r--r--spec/fixtures/helm/helm_list_v2_prometheus_failed.json.gzbin0 -> 339 bytes
-rw-r--r--spec/fixtures/helm/helm_list_v2_prometheus_missing.json.gzbin0 -> 320 bytes
-rw-r--r--spec/fixtures/legacy_group_export.tar.gzbin0 -> 3546 bytes
-rw-r--r--spec/fixtures/legacy_group_export_invalid_subrelations.tar.gzbin0 -> 3602 bytes
-rw-r--r--spec/fixtures/legacy_symlink_export.tar.gzbin0 -> 435 bytes
-rw-r--r--spec/fixtures/lib/elasticsearch/query.json2
-rw-r--r--spec/fixtures/lib/elasticsearch/query_with_container.json2
-rw-r--r--spec/fixtures/lib/elasticsearch/query_with_cursor.json2
-rw-r--r--spec/fixtures/lib/elasticsearch/query_with_end_time.json2
-rw-r--r--spec/fixtures/lib/elasticsearch/query_with_filebeat_6.json40
-rw-r--r--spec/fixtures/lib/elasticsearch/query_with_search.json2
-rw-r--r--spec/fixtures/lib/elasticsearch/query_with_start_time.json2
-rw-r--r--spec/fixtures/lib/elasticsearch/query_with_times.json2
-rw-r--r--spec/fixtures/lib/gitlab/import_export/complex/project.json40
-rw-r--r--spec/fixtures/lib/gitlab/import_export/complex/tree.tar.gzbin32595 -> 0 bytes
-rw-r--r--spec/fixtures/lib/gitlab/import_export/complex/tree/project.json1
-rw-r--r--spec/fixtures/lib/gitlab/import_export/complex/tree/project/auto_devops.ndjson1
-rw-r--r--spec/fixtures/lib/gitlab/import_export/complex/tree/project/boards.ndjson1
-rw-r--r--spec/fixtures/lib/gitlab/import_export/complex/tree/project/ci_cd_settings.ndjson1
-rw-r--r--spec/fixtures/lib/gitlab/import_export/complex/tree/project/ci_pipelines.ndjson7
-rw-r--r--spec/fixtures/lib/gitlab/import_export/complex/tree/project/container_expiration_policy.ndjson1
-rw-r--r--spec/fixtures/lib/gitlab/import_export/complex/tree/project/custom_attributes.ndjson2
-rw-r--r--spec/fixtures/lib/gitlab/import_export/complex/tree/project/error_tracking_setting.ndjson1
-rw-r--r--spec/fixtures/lib/gitlab/import_export/complex/tree/project/external_pull_requests.ndjson1
-rw-r--r--spec/fixtures/lib/gitlab/import_export/complex/tree/project/issues.ndjson10
-rw-r--r--spec/fixtures/lib/gitlab/import_export/complex/tree/project/labels.ndjson2
-rw-r--r--spec/fixtures/lib/gitlab/import_export/complex/tree/project/merge_requests.ndjson9
-rw-r--r--spec/fixtures/lib/gitlab/import_export/complex/tree/project/milestones.ndjson3
-rw-r--r--spec/fixtures/lib/gitlab/import_export/complex/tree/project/pipeline_schedules.ndjson1
-rw-r--r--spec/fixtures/lib/gitlab/import_export/complex/tree/project/project_badges.ndjson2
-rw-r--r--spec/fixtures/lib/gitlab/import_export/complex/tree/project/project_feature.ndjson1
-rw-r--r--spec/fixtures/lib/gitlab/import_export/complex/tree/project/project_members.ndjson4
-rw-r--r--spec/fixtures/lib/gitlab/import_export/complex/tree/project/protected_branches.ndjson1
-rw-r--r--spec/fixtures/lib/gitlab/import_export/complex/tree/project/protected_environments.ndjson1
-rw-r--r--spec/fixtures/lib/gitlab/import_export/complex/tree/project/protected_tags.ndjson1
-rw-r--r--spec/fixtures/lib/gitlab/import_export/complex/tree/project/releases.ndjson1
-rw-r--r--spec/fixtures/lib/gitlab/import_export/complex/tree/project/services.ndjson19
-rw-r--r--spec/fixtures/lib/gitlab/import_export/complex/tree/project/snippets.ndjson1
-rw-r--r--spec/fixtures/lib/gitlab/import_export/complex/tree/project/triggers.ndjson2
-rw-r--r--spec/fixtures/lib/gitlab/import_export/designs/project.json502
-rw-r--r--spec/fixtures/lib/gitlab/import_export/group/tree.tar.gzbin1246 -> 0 bytes
-rw-r--r--spec/fixtures/lib/gitlab/import_export/group/tree/project.json1
-rw-r--r--spec/fixtures/lib/gitlab/import_export/group/tree/project/issues.ndjson3
-rw-r--r--spec/fixtures/lib/gitlab/import_export/group/tree/project/labels.ndjson1
-rw-r--r--spec/fixtures/lib/gitlab/import_export/group/tree/project/milestones.ndjson1
-rw-r--r--spec/fixtures/lib/gitlab/import_export/group_exports/child_with_no_parent/tree/groups/4351.json1
-rw-r--r--spec/fixtures/lib/gitlab/import_export/group_exports/child_with_no_parent/tree/groups/4352.json1
-rw-r--r--spec/fixtures/lib/gitlab/import_export/group_exports/child_with_no_parent/tree/groups/_all.ndjson2
-rw-r--r--spec/fixtures/lib/gitlab/import_export/group_exports/complex/tree/groups/4351.json1
-rw-r--r--spec/fixtures/lib/gitlab/import_export/group_exports/complex/tree/groups/4351/badges.ndjson1
-rw-r--r--spec/fixtures/lib/gitlab/import_export/group_exports/complex/tree/groups/4351/boards.ndjson1
-rw-r--r--spec/fixtures/lib/gitlab/import_export/group_exports/complex/tree/groups/4351/epics.ndjson5
-rw-r--r--spec/fixtures/lib/gitlab/import_export/group_exports/complex/tree/groups/4351/labels.ndjson10
-rw-r--r--spec/fixtures/lib/gitlab/import_export/group_exports/complex/tree/groups/4351/members.ndjson6
-rw-r--r--spec/fixtures/lib/gitlab/import_export/group_exports/complex/tree/groups/4351/milestones.ndjson5
-rw-r--r--spec/fixtures/lib/gitlab/import_export/group_exports/complex/tree/groups/4352.json1
-rw-r--r--spec/fixtures/lib/gitlab/import_export/group_exports/complex/tree/groups/4352/badges.ndjson1
-rw-r--r--spec/fixtures/lib/gitlab/import_export/group_exports/complex/tree/groups/4352/boards.ndjson2
-rw-r--r--spec/fixtures/lib/gitlab/import_export/group_exports/complex/tree/groups/4352/epics.ndjson5
-rw-r--r--spec/fixtures/lib/gitlab/import_export/group_exports/complex/tree/groups/4352/labels.ndjson9
-rw-r--r--spec/fixtures/lib/gitlab/import_export/group_exports/complex/tree/groups/4352/members.ndjson6
-rw-r--r--spec/fixtures/lib/gitlab/import_export/group_exports/complex/tree/groups/4352/milestones.ndjson5
-rw-r--r--spec/fixtures/lib/gitlab/import_export/group_exports/complex/tree/groups/4353.json1
-rw-r--r--spec/fixtures/lib/gitlab/import_export/group_exports/complex/tree/groups/4353/badges.ndjson1
-rw-r--r--spec/fixtures/lib/gitlab/import_export/group_exports/complex/tree/groups/4353/boards.ndjson2
-rw-r--r--spec/fixtures/lib/gitlab/import_export/group_exports/complex/tree/groups/4353/epics.ndjson5
-rw-r--r--spec/fixtures/lib/gitlab/import_export/group_exports/complex/tree/groups/4353/labels.ndjson9
-rw-r--r--spec/fixtures/lib/gitlab/import_export/group_exports/complex/tree/groups/4353/members.ndjson6
-rw-r--r--spec/fixtures/lib/gitlab/import_export/group_exports/complex/tree/groups/4353/milestones.ndjson5
-rw-r--r--spec/fixtures/lib/gitlab/import_export/group_exports/complex/tree/groups/_all.ndjson3
-rw-r--r--spec/fixtures/lib/gitlab/import_export/group_exports/no_children/tree/groups/4353.json41
-rw-r--r--spec/fixtures/lib/gitlab/import_export/group_exports/no_children/tree/groups/4353/badges.ndjson1
-rw-r--r--spec/fixtures/lib/gitlab/import_export/group_exports/no_children/tree/groups/4353/boards.ndjson2
-rw-r--r--spec/fixtures/lib/gitlab/import_export/group_exports/no_children/tree/groups/4353/epics.ndjson5
-rw-r--r--spec/fixtures/lib/gitlab/import_export/group_exports/no_children/tree/groups/4353/labels.ndjson10
-rw-r--r--spec/fixtures/lib/gitlab/import_export/group_exports/no_children/tree/groups/4353/members.ndjson6
-rw-r--r--spec/fixtures/lib/gitlab/import_export/group_exports/no_children/tree/groups/4353/milestones.ndjson5
-rw-r--r--spec/fixtures/lib/gitlab/import_export/group_exports/no_children/tree/groups/_all.ndjson1
-rw-r--r--spec/fixtures/lib/gitlab/import_export/group_exports/visibility_levels/internal/tree/groups/283.json1
-rw-r--r--spec/fixtures/lib/gitlab/import_export/group_exports/visibility_levels/internal/tree/groups/284.json1
-rw-r--r--spec/fixtures/lib/gitlab/import_export/group_exports/visibility_levels/internal/tree/groups/285.json1
-rw-r--r--spec/fixtures/lib/gitlab/import_export/group_exports/visibility_levels/internal/tree/groups/286.json1
-rw-r--r--spec/fixtures/lib/gitlab/import_export/group_exports/visibility_levels/internal/tree/groups/_all.ndjson4
-rw-r--r--spec/fixtures/lib/gitlab/import_export/group_exports/visibility_levels/private/tree/groups/283.json1
-rw-r--r--spec/fixtures/lib/gitlab/import_export/group_exports/visibility_levels/private/tree/groups/284.json1
-rw-r--r--spec/fixtures/lib/gitlab/import_export/group_exports/visibility_levels/private/tree/groups/285.json1
-rw-r--r--spec/fixtures/lib/gitlab/import_export/group_exports/visibility_levels/private/tree/groups/286.json1
-rw-r--r--spec/fixtures/lib/gitlab/import_export/group_exports/visibility_levels/private/tree/groups/_all.ndjson4
-rw-r--r--spec/fixtures/lib/gitlab/import_export/group_exports/visibility_levels/public/tree/groups/283.json1
-rw-r--r--spec/fixtures/lib/gitlab/import_export/group_exports/visibility_levels/public/tree/groups/284.json1
-rw-r--r--spec/fixtures/lib/gitlab/import_export/group_exports/visibility_levels/public/tree/groups/285.json1
-rw-r--r--spec/fixtures/lib/gitlab/import_export/group_exports/visibility_levels/public/tree/groups/286.json1
-rw-r--r--spec/fixtures/lib/gitlab/import_export/group_exports/visibility_levels/public/tree/groups/_all.ndjson4
-rw-r--r--spec/fixtures/lib/gitlab/import_export/invalid_json/tree.tar.gzbin191 -> 0 bytes
-rw-r--r--spec/fixtures/lib/gitlab/import_export/invalid_json/tree/project.json1
-rw-r--r--spec/fixtures/lib/gitlab/import_export/light/tree.tar.gzbin1435 -> 0 bytes
-rw-r--r--spec/fixtures/lib/gitlab/import_export/light/tree/project.json1
-rw-r--r--spec/fixtures/lib/gitlab/import_export/light/tree/project/custom_attributes.ndjson2
-rw-r--r--spec/fixtures/lib/gitlab/import_export/light/tree/project/issues.ndjson1
-rw-r--r--spec/fixtures/lib/gitlab/import_export/light/tree/project/labels.ndjson1
-rw-r--r--spec/fixtures/lib/gitlab/import_export/light/tree/project/milestones.ndjson1
-rw-r--r--spec/fixtures/lib/gitlab/import_export/light/tree/project/services.ndjson2
-rw-r--r--spec/fixtures/lib/gitlab/import_export/milestone-iid/tree.tar.gzbin714 -> 0 bytes
-rw-r--r--spec/fixtures/lib/gitlab/import_export/milestone-iid/tree/project.json1
-rw-r--r--spec/fixtures/lib/gitlab/import_export/milestone-iid/tree/project/issues.ndjson2
-rw-r--r--spec/fixtures/lib/gitlab/import_export/multi_pipeline_ref_one_external_pr/tree.tar.gzbin1172 -> 0 bytes
-rw-r--r--spec/fixtures/lib/gitlab/import_export/multi_pipeline_ref_one_external_pr/tree/project.json1
-rw-r--r--spec/fixtures/lib/gitlab/import_export/multi_pipeline_ref_one_external_pr/tree/project/ci_cd_settings.ndjson1
-rw-r--r--spec/fixtures/lib/gitlab/import_export/multi_pipeline_ref_one_external_pr/tree/project/ci_pipelines.ndjson2
-rw-r--r--spec/fixtures/lib/gitlab/import_export/multi_pipeline_ref_one_external_pr/tree/project/external_pull_requests.ndjson1
-rw-r--r--spec/fixtures/lib/gitlab/import_export/multi_pipeline_ref_one_external_pr/tree/project/project_feature.ndjson1
-rw-r--r--spec/fixtures/lib/gitlab/import_export/with_invalid_records/tree.tar.gzbin513 -> 0 bytes
-rw-r--r--spec/fixtures/lib/gitlab/import_export/with_invalid_records/tree/project.json1
-rw-r--r--spec/fixtures/lib/gitlab/import_export/with_invalid_records/tree/project/milestones.ndjson2
-rw-r--r--spec/fixtures/lib/gitlab/metrics/dashboard/development_metrics.yml39
-rw-r--r--spec/fixtures/lsif.json.zipbin0 -> 2178 bytes
-rw-r--r--spec/fixtures/sample_doc.md1
-rw-r--r--spec/fixtures/terraform/tfplan.json1
-rw-r--r--spec/fixtures/terraform/tfplan_with_corrupted_data.json1
-rw-r--r--spec/fixtures/trace/sample_trace4
-rw-r--r--spec/fixtures/x509/ZZZZZZA6.crlbin0 -> 205280 bytes
-rw-r--r--spec/frontend/.eslintrc.yml17
-rw-r--r--spec/frontend/__mocks__/@toast-ui/vue-editor/index.js29
-rw-r--r--spec/frontend/ajax_loading_spinner_spec.js57
-rw-r--r--spec/frontend/alert_management/components/alert_management_detail_spec.js242
-rw-r--r--spec/frontend/alert_management/components/alert_management_list_spec.js325
-rw-r--r--spec/frontend/alert_management/mocks/alerts.json29
-rw-r--r--spec/frontend/api_spec.js2
-rw-r--r--spec/frontend/autosave_spec.js90
-rw-r--r--spec/frontend/avatar_helper_spec.js110
-rw-r--r--spec/frontend/behaviors/markdown/paste_markdown_table_spec.js12
-rw-r--r--spec/frontend/behaviors/markdown/render_metrics_spec.js36
-rw-r--r--spec/frontend/blob/components/__snapshots__/blob_edit_header_spec.js.snap2
-rw-r--r--spec/frontend/blob/components/__snapshots__/blob_header_filepath_spec.js.snap11
-rw-r--r--spec/frontend/blob/components/__snapshots__/blob_header_spec.js.snap2
-rw-r--r--spec/frontend/blob/components/blob_content_error_spec.js51
-rw-r--r--spec/frontend/blob/components/blob_content_spec.js36
-rw-r--r--spec/frontend/blob/components/blob_header_filepath_spec.js10
-rw-r--r--spec/frontend/blob/components/blob_header_spec.js2
-rw-r--r--spec/frontend/blob/components/mock_data.js2
-rw-r--r--spec/frontend/blob/pipeline_tour_success_modal_spec.js2
-rw-r--r--spec/frontend/blob/suggest_gitlab_ci_yml/components/popover_spec.js9
-rw-r--r--spec/frontend/blob/utils_spec.js42
-rw-r--r--spec/frontend/boards/board_list_spec.js2
-rw-r--r--spec/frontend/boards/boards_store_spec.js137
-rw-r--r--spec/frontend/boards/issue_spec.js22
-rw-r--r--spec/frontend/bootstrap_linked_tabs_spec.js67
-rw-r--r--spec/frontend/broadcast_notification_spec.js35
-rw-r--r--spec/frontend/ci_variable_list/ci_variable_list/ajax_variable_list_spec.js203
-rw-r--r--spec/frontend/ci_variable_list/ci_variable_list/ci_variable_list_spec.js282
-rw-r--r--spec/frontend/ci_variable_list/ci_variable_list/native_form_variable_list_spec.js (renamed from spec/javascripts/ci_variable_list/native_form_variable_list_spec.js)0
-rw-r--r--spec/frontend/ci_variable_list/components/ci_variable_modal_spec.js7
-rw-r--r--spec/frontend/ci_variable_list/services/mock_data.js12
-rw-r--r--spec/frontend/ci_variable_list/store/actions_spec.js10
-rw-r--r--spec/frontend/ci_variable_list/store/mutations_spec.js10
-rw-r--r--spec/frontend/close_reopen_report_toggle_spec.js288
-rw-r--r--spec/frontend/clusters/components/applications_spec.js33
-rw-r--r--spec/frontend/clusters/components/fluentd_output_settings_spec.js186
-rw-r--r--spec/frontend/clusters/components/knative_domain_editor_spec.js2
-rw-r--r--spec/frontend/clusters/services/mock_data.js1
-rw-r--r--spec/frontend/clusters/stores/clusters_store_spec.js18
-rw-r--r--spec/frontend/clusters_list/components/clusters_spec.js110
-rw-r--r--spec/frontend/clusters_list/mock_data.js18
-rw-r--r--spec/frontend/clusters_list/store/actions_spec.js29
-rw-r--r--spec/frontend/code_navigation/components/__snapshots__/popover_spec.js.snap11
-rw-r--r--spec/frontend/code_navigation/components/app_spec.js1
-rw-r--r--spec/frontend/code_navigation/components/popover_spec.js41
-rw-r--r--spec/frontend/code_navigation/store/actions_spec.js28
-rw-r--r--spec/frontend/commit/pipelines/pipelines_spec.js44
-rw-r--r--spec/frontend/commit_merge_requests_spec.js (renamed from spec/javascripts/commit_merge_requests_spec.js)0
-rw-r--r--spec/frontend/commits_spec.js98
-rw-r--r--spec/frontend/contributors/store/actions_spec.js3
-rw-r--r--spec/frontend/contributors/store/getters_spec.js3
-rw-r--r--spec/frontend/create_cluster/eks_cluster/services/aws_services_facade_spec.js14
-rw-r--r--spec/frontend/create_item_dropdown_spec.js (renamed from spec/javascripts/create_item_dropdown_spec.js)0
-rw-r--r--spec/frontend/custom_metrics/components/custom_metrics_form_fields_spec.js1
-rw-r--r--spec/frontend/deploy_keys/components/action_btn_spec.js54
-rw-r--r--spec/frontend/deploy_keys/components/app_spec.js142
-rw-r--r--spec/frontend/deploy_keys/components/key_spec.js161
-rw-r--r--spec/frontend/deploy_keys/components/keys_panel_spec.js63
-rw-r--r--spec/frontend/design_management/components/__snapshots__/design_note_pin_spec.js.snap42
-rw-r--r--spec/frontend/design_management/components/__snapshots__/design_presentation_spec.js.snap104
-rw-r--r--spec/frontend/design_management/components/__snapshots__/design_scaler_spec.js.snap115
-rw-r--r--spec/frontend/design_management/components/__snapshots__/image_spec.js.snap68
-rw-r--r--spec/frontend/design_management/components/delete_button_spec.js51
-rw-r--r--spec/frontend/design_management/components/design_note_pin_spec.js49
-rw-r--r--spec/frontend/design_management/components/design_notes/__snapshots__/design_note_spec.js.snap61
-rw-r--r--spec/frontend/design_management/components/design_notes/__snapshots__/design_reply_form_spec.js.snap15
-rw-r--r--spec/frontend/design_management/components/design_notes/design_discussion_spec.js133
-rw-r--r--spec/frontend/design_management/components/design_notes/design_note_spec.js170
-rw-r--r--spec/frontend/design_management/components/design_notes/design_reply_form_spec.js182
-rw-r--r--spec/frontend/design_management/components/design_overlay_spec.js393
-rw-r--r--spec/frontend/design_management/components/design_presentation_spec.js546
-rw-r--r--spec/frontend/design_management/components/design_scaler_spec.js67
-rw-r--r--spec/frontend/design_management/components/image_spec.js133
-rw-r--r--spec/frontend/design_management/components/list/__snapshots__/item_spec.js.snap472
-rw-r--r--spec/frontend/design_management/components/list/item_spec.js168
-rw-r--r--spec/frontend/design_management/components/toolbar/__snapshots__/index_spec.js.snap61
-rw-r--r--spec/frontend/design_management/components/toolbar/__snapshots__/pagination_button_spec.js.snap28
-rw-r--r--spec/frontend/design_management/components/toolbar/__snapshots__/pagination_spec.js.snap29
-rw-r--r--spec/frontend/design_management/components/toolbar/index_spec.js123
-rw-r--r--spec/frontend/design_management/components/toolbar/pagination_button_spec.js61
-rw-r--r--spec/frontend/design_management/components/toolbar/pagination_spec.js79
-rw-r--r--spec/frontend/design_management/components/upload/__snapshots__/button_spec.js.snap79
-rw-r--r--spec/frontend/design_management/components/upload/__snapshots__/design_dropzone_spec.js.snap455
-rw-r--r--spec/frontend/design_management/components/upload/__snapshots__/design_version_dropdown_spec.js.snap111
-rw-r--r--spec/frontend/design_management/components/upload/button_spec.js59
-rw-r--r--spec/frontend/design_management/components/upload/design_dropzone_spec.js132
-rw-r--r--spec/frontend/design_management/components/upload/design_version_dropdown_spec.js114
-rw-r--r--spec/frontend/design_management/components/upload/mock_data/all_versions.js14
-rw-r--r--spec/frontend/design_management/mock_data/all_versions.js8
-rw-r--r--spec/frontend/design_management/mock_data/design.js54
-rw-r--r--spec/frontend/design_management/mock_data/designs.js17
-rw-r--r--spec/frontend/design_management/mock_data/no_designs.js11
-rw-r--r--spec/frontend/design_management/mock_data/notes.js32
-rw-r--r--spec/frontend/design_management/pages/__snapshots__/index_spec.js.snap263
-rw-r--r--spec/frontend/design_management/pages/design/__snapshots__/index_spec.js.snap184
-rw-r--r--spec/frontend/design_management/pages/design/index_spec.js301
-rw-r--r--spec/frontend/design_management/pages/index_spec.js533
-rw-r--r--spec/frontend/design_management/router_spec.js81
-rw-r--r--spec/frontend/design_management/utils/cache_update_spec.js44
-rw-r--r--spec/frontend/design_management/utils/design_management_utils_spec.js176
-rw-r--r--spec/frontend/design_management/utils/error_messages_spec.js62
-rw-r--r--spec/frontend/design_management/utils/tracking_spec.js53
-rw-r--r--spec/frontend/diff_comments_store_spec.js136
-rw-r--r--spec/frontend/diffs/components/app_spec.js212
-rw-r--r--spec/frontend/diffs/components/commit_item_spec.js144
-rw-r--r--spec/frontend/diffs/components/diff_content_spec.js2
-rw-r--r--spec/frontend/diffs/components/diff_discussions_spec.js2
-rw-r--r--spec/frontend/diffs/components/diff_expansion_cell_spec.js2
-rw-r--r--spec/frontend/diffs/components/diff_gutter_avatars_spec.js2
-rw-r--r--spec/frontend/diffs/components/diff_line_note_form_spec.js2
-rw-r--r--spec/frontend/diffs/components/edit_button_spec.js19
-rw-r--r--spec/frontend/diffs/components/inline_diff_expansion_row_spec.js2
-rw-r--r--spec/frontend/diffs/components/inline_diff_view_spec.js4
-rw-r--r--spec/frontend/diffs/components/parallel_diff_expansion_row_spec.js2
-rw-r--r--spec/frontend/diffs/components/parallel_diff_view_spec.js2
-rw-r--r--spec/frontend/diffs/store/actions_spec.js184
-rw-r--r--spec/frontend/diffs/store/getters_spec.js4
-rw-r--r--spec/frontend/diffs/store/getters_versions_dropdowns_spec.js9
-rw-r--r--spec/frontend/diffs/store/mutations_spec.js30
-rw-r--r--spec/frontend/diffs/store/utils_spec.js84
-rw-r--r--spec/frontend/dirty_submit/dirty_submit_collection_spec.js22
-rw-r--r--spec/frontend/dirty_submit/dirty_submit_factory_spec.js (renamed from spec/javascripts/dirty_submit/dirty_submit_factory_spec.js)0
-rw-r--r--spec/frontend/dirty_submit/dirty_submit_form_spec.js97
-rw-r--r--spec/frontend/dirty_submit/helper.js43
-rw-r--r--spec/frontend/editor/editor_lite_spec.js177
-rw-r--r--spec/frontend/emoji_spec.js485
-rw-r--r--spec/frontend/feature_highlight/feature_highlight_helper_spec.js62
-rw-r--r--spec/frontend/feature_highlight/feature_highlight_options_spec.js44
-rw-r--r--spec/frontend/feature_highlight/feature_highlight_spec.js120
-rw-r--r--spec/frontend/filtered_search/dropdown_utils_spec.js374
-rw-r--r--spec/frontend/filtered_search/filtered_search_manager_spec.js587
-rw-r--r--spec/frontend/filtered_search/filtered_search_tokenizer_spec.js (renamed from spec/javascripts/filtered_search/filtered_search_tokenizer_spec.js)0
-rw-r--r--spec/frontend/filtered_search/issues_filtered_search_token_keys_spec.js (renamed from spec/javascripts/filtered_search/issues_filtered_search_token_keys_spec.js)0
-rw-r--r--spec/frontend/filtered_search/recent_searches_root_spec.js32
-rw-r--r--spec/frontend/filtered_search/services/recent_searches_service_spec.js161
-rw-r--r--spec/frontend/filtered_search/visual_token_value_spec.js389
-rw-r--r--spec/frontend/fixtures/test_report.rb2
-rw-r--r--spec/frontend/flash_spec.js233
-rw-r--r--spec/frontend/frequent_items/components/app_spec.js251
-rw-r--r--spec/frontend/frequent_items/mock_data.js127
-rw-r--r--spec/frontend/frequent_items/store/actions_spec.js228
-rw-r--r--spec/frontend/frequent_items/store/mutations_spec.js (renamed from spec/javascripts/frequent_items/store/mutations_spec.js)0
-rw-r--r--spec/frontend/frequent_items/utils_spec.js130
-rw-r--r--spec/frontend/groups/components/app_spec.js507
-rw-r--r--spec/frontend/groups/components/group_folder_spec.js65
-rw-r--r--spec/frontend/groups/components/group_item_spec.js215
-rw-r--r--spec/frontend/groups/components/groups_spec.js72
-rw-r--r--spec/frontend/groups/components/item_actions_spec.js84
-rw-r--r--spec/frontend/groups/components/item_caret_spec.js38
-rw-r--r--spec/frontend/groups/components/item_stats_spec.js119
-rw-r--r--spec/frontend/groups/components/item_stats_value_spec.js82
-rw-r--r--spec/frontend/groups/components/item_type_icon_spec.js53
-rw-r--r--spec/frontend/groups/mock_data.js (renamed from spec/javascripts/groups/mock_data.js)0
-rw-r--r--spec/frontend/groups/service/groups_service_spec.js42
-rw-r--r--spec/frontend/groups/store/groups_store_spec.js123
-rw-r--r--spec/frontend/header_spec.js16
-rw-r--r--spec/frontend/helpers/class_spec_helper.js1
-rw-r--r--spec/frontend/helpers/event_hub_factory_spec.js94
-rw-r--r--spec/frontend/helpers/filtered_search_spec_helper.js69
-rw-r--r--spec/frontend/helpers/fixtures.js5
-rw-r--r--spec/frontend/helpers/set_window_location_helper.js40
-rw-r--r--spec/frontend/helpers/set_window_location_helper_spec.js40
-rw-r--r--spec/frontend/helpers/vue_mount_component_helper.js25
-rw-r--r--spec/frontend/helpers/web_worker_mock.js10
-rw-r--r--spec/frontend/ide/components/activity_bar_spec.js72
-rw-r--r--spec/frontend/ide/components/commit_sidebar/editor_header_spec.js50
-rw-r--r--spec/frontend/ide/components/commit_sidebar/form_spec.js111
-rw-r--r--spec/frontend/ide/components/commit_sidebar/list_spec.js2
-rw-r--r--spec/frontend/ide/components/commit_sidebar/radio_group_spec.js134
-rw-r--r--spec/frontend/ide/components/file_row_extra_spec.js170
-rw-r--r--spec/frontend/ide/components/file_templates/bar_spec.js117
-rw-r--r--spec/frontend/ide/components/ide_review_spec.js73
-rw-r--r--spec/frontend/ide/components/ide_side_bar_spec.js57
-rw-r--r--spec/frontend/ide/components/ide_spec.js125
-rw-r--r--spec/frontend/ide/components/ide_status_bar_spec.js127
-rw-r--r--spec/frontend/ide/components/ide_tree_list_spec.js77
-rw-r--r--spec/frontend/ide/components/ide_tree_spec.js34
-rw-r--r--spec/frontend/ide/components/jobs/detail/description_spec.js (renamed from spec/javascripts/ide/components/jobs/detail/description_spec.js)0
-rw-r--r--spec/frontend/ide/components/jobs/item_spec.js (renamed from spec/javascripts/ide/components/jobs/item_spec.js)0
-rw-r--r--spec/frontend/ide/components/merge_requests/item_spec.js63
-rw-r--r--spec/frontend/ide/components/nav_dropdown_button_spec.js93
-rw-r--r--spec/frontend/ide/components/nav_dropdown_spec.js102
-rw-r--r--spec/frontend/ide/components/new_dropdown/button_spec.js65
-rw-r--r--spec/frontend/ide/components/new_dropdown/index_spec.js84
-rw-r--r--spec/frontend/ide/components/new_dropdown/modal_spec.js175
-rw-r--r--spec/frontend/ide/components/new_dropdown/upload_spec.js112
-rw-r--r--spec/frontend/ide/components/pipelines/list_spec.js17
-rw-r--r--spec/frontend/ide/components/preview/clientside_spec.js8
-rw-r--r--spec/frontend/ide/components/repo_commit_section_spec.js1
-rw-r--r--spec/frontend/ide/components/repo_tab_spec.js185
-rw-r--r--spec/frontend/ide/components/repo_tabs_spec.js (renamed from spec/javascripts/ide/components/repo_tabs_spec.js)0
-rw-r--r--spec/frontend/ide/components/shared/tokened_input_spec.js133
-rw-r--r--spec/frontend/ide/lib/common/model_manager_spec.js126
-rw-r--r--spec/frontend/ide/lib/common/model_spec.js137
-rw-r--r--spec/frontend/ide/lib/decorations/controller_spec.js143
-rw-r--r--spec/frontend/ide/lib/diff/controller_spec.js215
-rw-r--r--spec/frontend/ide/lib/editor_spec.js302
-rw-r--r--spec/frontend/ide/lib/languages/vue_spec.js92
-rw-r--r--spec/frontend/ide/services/index_spec.js63
-rw-r--r--spec/frontend/ide/stores/mutations_spec.js41
-rw-r--r--spec/frontend/ide/stores/utils_spec.js71
-rw-r--r--spec/frontend/ide/utils_spec.js92
-rw-r--r--spec/frontend/image_diff/helpers/badge_helper_spec.js130
-rw-r--r--spec/frontend/image_diff/helpers/comment_indicator_helper_spec.js144
-rw-r--r--spec/frontend/image_diff/helpers/dom_helper_spec.js120
-rw-r--r--spec/frontend/image_diff/helpers/utils_helper_spec.js (renamed from spec/javascripts/image_diff/helpers/utils_helper_spec.js)0
-rw-r--r--spec/frontend/image_diff/image_badge_spec.js84
-rw-r--r--spec/frontend/image_diff/image_diff_spec.js361
-rw-r--r--spec/frontend/image_diff/mock_data.js (renamed from spec/javascripts/image_diff/mock_data.js)0
-rw-r--r--spec/frontend/image_diff/replaced_image_diff_spec.js356
-rw-r--r--spec/frontend/import_projects/components/import_projects_table_spec.js5
-rw-r--r--spec/frontend/import_projects/components/provider_repo_table_row_spec.js4
-rw-r--r--spec/frontend/integrations/edit/components/active_toggle_spec.js8
-rw-r--r--spec/frontend/integrations/edit/components/integration_form_spec.js99
-rw-r--r--spec/frontend/integrations/edit/components/jira_trigger_fields_spec.js97
-rw-r--r--spec/frontend/integrations/edit/components/trigger_fields_spec.js136
-rw-r--r--spec/frontend/integrations/integration_settings_form_spec.js268
-rw-r--r--spec/frontend/issuable_spec.js64
-rw-r--r--spec/frontend/issuables_list/components/issuable_list_root_app_spec.js121
-rw-r--r--spec/frontend/issue_show/components/app_spec.js497
-rw-r--r--spec/frontend/issue_show/components/description_spec.js188
-rw-r--r--spec/frontend/issue_show/components/edited_spec.js (renamed from spec/javascripts/issue_show/components/edited_spec.js)0
-rw-r--r--spec/frontend/issue_show/components/fields/description_template_spec.js41
-rw-r--r--spec/frontend/issue_show/components/form_spec.js99
-rw-r--r--spec/frontend/issue_show/components/title_spec.js95
-rw-r--r--spec/frontend/jira_import/components/jira_import_app_spec.js102
-rw-r--r--spec/frontend/jira_import/components/jira_import_form_spec.js31
-rw-r--r--spec/frontend/jira_import/components/jira_import_progress_spec.js21
-rw-r--r--spec/frontend/jira_import/components/jira_import_setup_spec.js18
-rw-r--r--spec/frontend/jira_import/utils_spec.js65
-rw-r--r--spec/frontend/jobs/components/artifacts_block_spec.js (renamed from spec/javascripts/jobs/components/artifacts_block_spec.js)0
-rw-r--r--spec/frontend/jobs/components/commit_block_spec.js89
-rw-r--r--spec/frontend/jobs/components/empty_state_spec.js (renamed from spec/javascripts/jobs/components/empty_state_spec.js)0
-rw-r--r--spec/frontend/jobs/components/environments_block_spec.js (renamed from spec/javascripts/jobs/components/environments_block_spec.js)0
-rw-r--r--spec/frontend/jobs/components/job_container_item_spec.js101
-rw-r--r--spec/frontend/jobs/components/job_log_spec.js65
-rw-r--r--spec/frontend/jobs/components/jobs_container_spec.js (renamed from spec/javascripts/jobs/components/jobs_container_spec.js)0
-rw-r--r--spec/frontend/jobs/components/log/line_header_spec.js2
-rw-r--r--spec/frontend/jobs/components/manual_variables_form_spec.js (renamed from spec/javascripts/jobs/components/manual_variables_form_spec.js)0
-rw-r--r--spec/frontend/jobs/components/sidebar_spec.js166
-rw-r--r--spec/frontend/jobs/components/stages_dropdown_spec.js163
-rw-r--r--spec/frontend/jobs/components/trigger_block_spec.js (renamed from spec/javascripts/jobs/components/trigger_block_spec.js)0
-rw-r--r--spec/frontend/jobs/components/unmet_prerequisites_block_spec.js (renamed from spec/javascripts/jobs/components/unmet_prerequisites_block_spec.js)0
-rw-r--r--spec/frontend/jobs/mixins/delayed_job_mixin_spec.js79
-rw-r--r--spec/frontend/jobs/store/actions_spec.js512
-rw-r--r--spec/frontend/jobs/store/helpers.js (renamed from spec/javascripts/jobs/store/helpers.js)0
-rw-r--r--spec/frontend/jobs/store/mutations_spec.js2
-rw-r--r--spec/frontend/labels_select_spec.js15
-rw-r--r--spec/frontend/landing_spec.js184
-rw-r--r--spec/frontend/lib/utils/axios_utils_spec.js1
-rw-r--r--spec/frontend/lib/utils/common_utils_spec.js2
-rw-r--r--spec/frontend/lib/utils/csrf_token_spec.js57
-rw-r--r--spec/frontend/lib/utils/downloader_spec.js40
-rw-r--r--spec/frontend/lib/utils/navigation_utility_spec.js23
-rw-r--r--spec/frontend/lib/utils/poll_spec.js225
-rw-r--r--spec/frontend/lib/utils/sticky_spec.js77
-rw-r--r--spec/frontend/lib/utils/text_markdown_spec.js8
-rw-r--r--spec/frontend/lib/utils/url_utility_spec.js147
-rw-r--r--spec/frontend/milestones/mock_data.js82
-rw-r--r--spec/frontend/milestones/project_milestone_combobox_spec.js150
-rw-r--r--spec/frontend/mocks/ce/diffs/workers/tree_worker.js9
-rw-r--r--spec/frontend/mocks/ce/ide/lib/diff/diff_worker.js1
-rw-r--r--spec/frontend/mocks_spec.js13
-rw-r--r--spec/frontend/monitoring/__snapshots__/alert_widget_spec.js.snap43
-rw-r--r--spec/frontend/monitoring/alert_widget_spec.js422
-rw-r--r--spec/frontend/monitoring/components/__snapshots__/dashboard_template_spec.js.snap25
-rw-r--r--spec/frontend/monitoring/components/alert_widget_form_spec.js220
-rw-r--r--spec/frontend/monitoring/components/charts/single_stat_spec.js14
-rw-r--r--spec/frontend/monitoring/components/charts/time_series_spec.js43
-rw-r--r--spec/frontend/monitoring/components/dashboard_panel_spec.js576
-rw-r--r--spec/frontend/monitoring/components/dashboard_spec.js549
-rw-r--r--spec/frontend/monitoring/components/dashboard_template_spec.js15
-rw-r--r--spec/frontend/monitoring/components/dashboard_url_time_spec.js2
-rw-r--r--spec/frontend/monitoring/components/dashboards_dropdown_spec.js127
-rw-r--r--spec/frontend/monitoring/components/duplicate_dashboard_form_spec.js26
-rw-r--r--spec/frontend/monitoring/components/embeds/metric_embed_spec.js10
-rw-r--r--spec/frontend/monitoring/components/panel_type_spec.js408
-rw-r--r--spec/frontend/monitoring/components/variables/custom_variable_spec.js52
-rw-r--r--spec/frontend/monitoring/components/variables/text_variable_spec.js59
-rw-r--r--spec/frontend/monitoring/components/variables_section_spec.js126
-rw-r--r--spec/frontend/monitoring/mock_data.js231
-rw-r--r--spec/frontend/monitoring/store/actions_spec.js180
-rw-r--r--spec/frontend/monitoring/store/getters_spec.js84
-rw-r--r--spec/frontend/monitoring/store/mutations_spec.js92
-rw-r--r--spec/frontend/monitoring/store/utils_spec.js6
-rw-r--r--spec/frontend/monitoring/store/variable_mapping_spec.js22
-rw-r--r--spec/frontend/monitoring/store_utils.js43
-rw-r--r--spec/frontend/monitoring/stubs/modal_stub.js11
-rw-r--r--spec/frontend/monitoring/utils_spec.js302
-rw-r--r--spec/frontend/monitoring/validators_spec.js80
-rw-r--r--spec/frontend/notebook/cells/code_spec.js90
-rw-r--r--spec/frontend/notebook/cells/markdown_spec.js167
-rw-r--r--spec/frontend/notebook/cells/output/html_sanitize_tests.js (renamed from spec/javascripts/notebook/cells/output/html_sanitize_tests.js)0
-rw-r--r--spec/frontend/notebook/cells/output/html_spec.js (renamed from spec/javascripts/notebook/cells/output/html_spec.js)0
-rw-r--r--spec/frontend/notebook/cells/output/index_spec.js115
-rw-r--r--spec/frontend/notebook/cells/prompt_spec.js56
-rw-r--r--spec/frontend/notebook/index_spec.js100
-rw-r--r--spec/frontend/notes/components/comment_form_spec.js7
-rw-r--r--spec/frontend/notes/components/discussion_actions_spec.js2
-rw-r--r--spec/frontend/notes/components/discussion_counter_spec.js9
-rw-r--r--spec/frontend/notes/components/discussion_filter_spec.js4
-rw-r--r--spec/frontend/notes/components/discussion_notes_spec.js2
-rw-r--r--spec/frontend/notes/components/note_form_spec.js8
-rw-r--r--spec/frontend/notes/components/note_header_spec.js94
-rw-r--r--spec/frontend/notes/components/noteable_discussion_spec.js2
-rw-r--r--spec/frontend/notes/components/notes_app_spec.js2
-rw-r--r--spec/frontend/notes/mixins/discussion_navigation_spec.js4
-rw-r--r--spec/frontend/notes/mock_data.js1
-rw-r--r--spec/frontend/notes/old_notes_spec.js52
-rw-r--r--spec/frontend/notes/stores/actions_spec.js31
-rw-r--r--spec/frontend/notes/stores/collapse_utils_spec.js4
-rw-r--r--spec/frontend/notes/stores/mutation_spec.js56
-rw-r--r--spec/frontend/oauth_remember_me_spec.js (renamed from spec/javascripts/oauth_remember_me_spec.js)0
-rw-r--r--spec/frontend/pages/admin/application_settings/account_and_limits_spec.js (renamed from spec/javascripts/pages/admin/application_settings/account_and_limits_spec.js)0
-rw-r--r--spec/frontend/pages/admin/jobs/index/components/stop_jobs_modal_spec.js64
-rw-r--r--spec/frontend/pages/admin/users/components/__snapshots__/delete_user_modal_spec.js.snap1
-rw-r--r--spec/frontend/pages/admin/users/new/index_spec.js (renamed from spec/javascripts/pages/admin/users/new/index_spec.js)0
-rw-r--r--spec/frontend/pages/labels/components/promote_label_modal_spec.js103
-rw-r--r--spec/frontend/pages/milestones/shared/components/delete_milestone_modal_spec.js109
-rw-r--r--spec/frontend/pages/milestones/shared/components/promote_milestone_modal_spec.js98
-rw-r--r--spec/frontend/pages/projects/pipeline_schedules/shared/components/interval_pattern_input_spec.js154
-rw-r--r--spec/frontend/pages/projects/pipeline_schedules/shared/components/pipeline_schedule_callout_spec.js114
-rw-r--r--spec/frontend/pages/projects/shared/permissions/components/settings_panel_spec.js29
-rw-r--r--spec/frontend/pages/sessions/new/preserve_url_fragment_spec.js (renamed from spec/javascripts/pages/sessions/new/preserve_url_fragment_spec.js)0
-rw-r--r--spec/frontend/pipelines/components/pipelines_filtered_search_spec.js97
-rw-r--r--spec/frontend/pipelines/graph/stage_column_component_spec.js2
-rw-r--r--spec/frontend/pipelines/header_component_spec.js116
-rw-r--r--spec/frontend/pipelines/linked_pipelines_mock.json3536
-rw-r--r--spec/frontend/pipelines/mock_data.js568
-rw-r--r--spec/frontend/pipelines/pipeline_details_mediator_spec.js36
-rw-r--r--spec/frontend/pipelines/pipelines_actions_spec.js142
-rw-r--r--spec/frontend/pipelines/pipelines_artifacts_spec.js46
-rw-r--r--spec/frontend/pipelines/pipelines_spec.js710
-rw-r--r--spec/frontend/pipelines/pipelines_table_row_spec.js2
-rw-r--r--spec/frontend/pipelines/pipelines_table_spec.js66
-rw-r--r--spec/frontend/pipelines/stage_spec.js156
-rw-r--r--spec/frontend/pipelines/stores/pipeline_store_spec.js135
-rw-r--r--spec/frontend/pipelines/test_reports/stores/mutations_spec.js6
-rw-r--r--spec/frontend/pipelines/test_reports/test_summary_spec.js18
-rw-r--r--spec/frontend/pipelines/test_reports/test_summary_table_spec.js36
-rw-r--r--spec/frontend/pipelines/time_ago_spec.js67
-rw-r--r--spec/frontend/pipelines/tokens/pipeline_branch_name_token_spec.js89
-rw-r--r--spec/frontend/pipelines/tokens/pipeline_trigger_author_token_spec.js98
-rw-r--r--spec/frontend/pipelines_spec.js (renamed from spec/javascripts/pipelines_spec.js)0
-rw-r--r--spec/frontend/prometheus_metrics/custom_metrics_spec.js2
-rw-r--r--spec/frontend/prometheus_metrics/mock_data.js44
-rw-r--r--spec/frontend/prometheus_metrics/prometheus_metrics_spec.js178
-rw-r--r--spec/frontend/registry/explorer/components/image_list_spec.js74
-rw-r--r--spec/frontend/registry/explorer/mock_data.js8
-rw-r--r--spec/frontend/registry/explorer/pages/details_spec.js198
-rw-r--r--spec/frontend/registry/explorer/pages/index_spec.js36
-rw-r--r--spec/frontend/registry/explorer/pages/list_spec.js269
-rw-r--r--spec/frontend/registry/explorer/stores/actions_spec.js20
-rw-r--r--spec/frontend/registry/explorer/stubs.js5
-rw-r--r--spec/frontend/registry/settings/store/getters_spec.js14
-rw-r--r--spec/frontend/registry/shared/components/__snapshots__/expiration_policy_fields_spec.js.snap18
-rw-r--r--spec/frontend/registry/shared/components/expiration_policy_fields_spec.js37
-rw-r--r--spec/frontend/related_merge_requests/components/related_merge_requests_spec.js94
-rw-r--r--spec/frontend/related_merge_requests/store/actions_spec.js111
-rw-r--r--spec/frontend/related_merge_requests/store/mutations_spec.js (renamed from spec/javascripts/related_merge_requests/store/mutations_spec.js)0
-rw-r--r--spec/frontend/releases/components/app_edit_spec.js9
-rw-r--r--spec/frontend/releases/components/release_block_footer_spec.js81
-rw-r--r--spec/frontend/releases/components/release_block_metadata_spec.js67
-rw-r--r--spec/frontend/releases/components/release_block_milestone_info_spec.js4
-rw-r--r--spec/frontend/releases/components/release_block_spec.js13
-rw-r--r--spec/frontend/releases/stores/modules/detail/actions_spec.js12
-rw-r--r--spec/frontend/releases/stores/modules/detail/mutations_spec.js31
-rw-r--r--spec/frontend/reports/accessibility_report/grouped_accessibility_reports_app_spec.js126
-rw-r--r--spec/frontend/reports/accessibility_report/mock_data.js55
-rw-r--r--spec/frontend/reports/accessibility_report/store/actions_spec.js121
-rw-r--r--spec/frontend/reports/accessibility_report/store/getters_spec.js149
-rw-r--r--spec/frontend/reports/accessibility_report/store/mutations_spec.js64
-rw-r--r--spec/frontend/reports/components/__snapshots__/grouped_issues_list_spec.js.snap25
-rw-r--r--spec/frontend/reports/components/__snapshots__/issue_status_icon_spec.js.snap37
-rw-r--r--spec/frontend/reports/components/grouped_issues_list_spec.js86
-rw-r--r--spec/frontend/reports/components/grouped_test_reports_app_spec.js260
-rw-r--r--spec/frontend/reports/components/issue_status_icon_spec.js29
-rw-r--r--spec/frontend/reports/components/modal_open_name_spec.js47
-rw-r--r--spec/frontend/reports/components/modal_spec.js (renamed from spec/javascripts/reports/components/modal_spec.js)0
-rw-r--r--spec/frontend/reports/components/summary_row_spec.js37
-rw-r--r--spec/frontend/reports/components/test_issue_body_spec.js72
-rw-r--r--spec/frontend/reports/mock_data/mock_data.js24
-rw-r--r--spec/frontend/reports/mock_data/new_and_fixed_failures_report.json (renamed from spec/javascripts/reports/mock_data/new_and_fixed_failures_report.json)0
-rw-r--r--spec/frontend/reports/mock_data/new_errors_report.json (renamed from spec/javascripts/reports/mock_data/new_errors_report.json)0
-rw-r--r--spec/frontend/reports/mock_data/new_failures_report.json (renamed from spec/javascripts/reports/mock_data/new_failures_report.json)0
-rw-r--r--spec/frontend/reports/mock_data/no_failures_report.json (renamed from spec/javascripts/reports/mock_data/no_failures_report.json)0
-rw-r--r--spec/frontend/reports/mock_data/resolved_failures.json (renamed from spec/javascripts/reports/mock_data/resolved_failures.json)0
-rw-r--r--spec/frontend/reports/store/actions_spec.js171
-rw-r--r--spec/frontend/reports/store/mutations_spec.js (renamed from spec/javascripts/reports/store/mutations_spec.js)0
-rw-r--r--spec/frontend/repository/components/__snapshots__/last_commit_spec.js.snap8
-rw-r--r--spec/frontend/repository/components/last_commit_spec.js1
-rw-r--r--spec/frontend/repository/utils/commit_spec.js2
-rw-r--r--spec/frontend/settings_panels_spec.js (renamed from spec/javascripts/settings_panels_spec.js)0
-rw-r--r--spec/frontend/sidebar/__snapshots__/confidential_issue_sidebar_spec.js.snap12
-rw-r--r--spec/frontend/sidebar/assignees_realtime_spec.js102
-rw-r--r--spec/frontend/sidebar/components/time_tracking/time_tracker_spec.js279
-rw-r--r--spec/frontend/sidebar/confidential/edit_form_buttons_spec.js41
-rw-r--r--spec/frontend/sidebar/confidential/edit_form_spec.js45
-rw-r--r--spec/frontend/sidebar/confidential_edit_buttons_spec.js35
-rw-r--r--spec/frontend/sidebar/confidential_edit_form_buttons_spec.js35
-rw-r--r--spec/frontend/sidebar/confidential_issue_sidebar_spec.js25
-rw-r--r--spec/frontend/sidebar/lock/edit_form_buttons_spec.js31
-rw-r--r--spec/frontend/sidebar/lock/lock_issue_sidebar_spec.js99
-rw-r--r--spec/frontend/sidebar/participants_spec.js206
-rw-r--r--spec/frontend/sidebar/sidebar_assignees_spec.js46
-rw-r--r--spec/frontend/sidebar/sidebar_mediator_spec.js135
-rw-r--r--spec/frontend/sidebar/sidebar_move_issue_spec.js167
-rw-r--r--spec/frontend/sidebar/sidebar_subscriptions_spec.js36
-rw-r--r--spec/frontend/sidebar/subscriptions_spec.js106
-rw-r--r--spec/frontend/smart_interval_spec.js2
-rw-r--r--spec/frontend/snippet/snippet_bundle_spec.js141
-rw-r--r--spec/frontend/snippets/components/__snapshots__/snippet_blob_edit_spec.js.snap1
-rw-r--r--spec/frontend/snippets/components/__snapshots__/snippet_description_edit_spec.js.snap4
-rw-r--r--spec/frontend/snippets/components/__snapshots__/snippet_description_view_spec.js.snap16
-rw-r--r--spec/frontend/snippets/components/edit_spec.js16
-rw-r--r--spec/frontend/snippets/components/snippet_blob_view_spec.js38
-rw-r--r--spec/frontend/snippets/components/snippet_description_view_spec.js27
-rw-r--r--spec/frontend/snippets/components/snippet_header_spec.js86
-rw-r--r--spec/frontend/snippets/components/snippet_title_spec.js6
-rw-r--r--spec/frontend/static_site_editor/components/edit_area_spec.js76
-rw-r--r--spec/frontend/static_site_editor/components/publish_toolbar_spec.js17
-rw-r--r--spec/frontend/static_site_editor/components/saved_changes_message_spec.js7
-rw-r--r--spec/frontend/static_site_editor/components/static_site_editor_spec.js247
-rw-r--r--spec/frontend/static_site_editor/graphql/resolvers/file_spec.js25
-rw-r--r--spec/frontend/static_site_editor/graphql/resolvers/submit_content_changes_spec.js37
-rw-r--r--spec/frontend/static_site_editor/mock_data.js5
-rw-r--r--spec/frontend/static_site_editor/pages/home_spec.js211
-rw-r--r--spec/frontend/static_site_editor/pages/success_spec.js78
-rw-r--r--spec/frontend/static_site_editor/services/submit_content_changes_spec.js32
-rw-r--r--spec/frontend/static_site_editor/store/actions_spec.js152
-rw-r--r--spec/frontend/static_site_editor/store/getters_spec.js19
-rw-r--r--spec/frontend/static_site_editor/store/mutations_spec.js54
-rw-r--r--spec/frontend/tracking_spec.js57
-rw-r--r--spec/frontend/users_select/utils_spec.js33
-rw-r--r--spec/frontend/vue_mr_widget/components/mr_collapsible_extension_spec.js4
-rw-r--r--spec/frontend/vue_mr_widget/components/mr_widget_pipeline_container_spec.js100
-rw-r--r--spec/frontend/vue_mr_widget/components/mr_widget_terraform_plan_spec.js18
-rw-r--r--spec/frontend/vue_mr_widget/stores/artifacts_list/actions_spec.js165
-rw-r--r--spec/frontend/vue_mr_widget/stores/get_state_key_spec.js24
-rw-r--r--spec/frontend/vue_mr_widget/stores/mr_widget_store_spec.js112
-rw-r--r--spec/frontend/vue_shared/components/__snapshots__/clone_dropdown_spec.js.snap38
-rw-r--r--spec/frontend/vue_shared/components/__snapshots__/code_block_spec.js.snap16
-rw-r--r--spec/frontend/vue_shared/components/__snapshots__/identicon_spec.js.snap12
-rw-r--r--spec/frontend/vue_shared/components/awards_list_spec.js42
-rw-r--r--spec/frontend/vue_shared/components/blob_viewers/__snapshots__/simple_viewer_spec.js.snap3
-rw-r--r--spec/frontend/vue_shared/components/blob_viewers/rich_viewer_spec.js5
-rw-r--r--spec/frontend/vue_shared/components/ci_badge_link_spec.js100
-rw-r--r--spec/frontend/vue_shared/components/ci_icon_spec.js122
-rw-r--r--spec/frontend/vue_shared/components/code_block_spec.js29
-rw-r--r--spec/frontend/vue_shared/components/content_viewer/content_viewer_spec.js21
-rw-r--r--spec/frontend/vue_shared/components/content_viewer/lib/viewer_utils_spec.js20
-rw-r--r--spec/frontend/vue_shared/components/content_viewer/viewers/download_viewer_spec.js28
-rw-r--r--spec/frontend/vue_shared/components/content_viewer/viewers/image_viewer_spec.js59
-rw-r--r--spec/frontend/vue_shared/components/content_viewer/viewers/markdown_viewer_spec.js114
-rw-r--r--spec/frontend/vue_shared/components/date_time_picker/date_time_picker_lib_spec.js9
-rw-r--r--spec/frontend/vue_shared/components/diff_viewer/diff_viewer_spec.js98
-rw-r--r--spec/frontend/vue_shared/components/dropdown/dropdown_button_spec.js81
-rw-r--r--spec/frontend/vue_shared/components/dropdown/dropdown_hidden_input_spec.js36
-rw-r--r--spec/frontend/vue_shared/components/dropdown/mock_data.js (renamed from spec/javascripts/vue_shared/components/dropdown/mock_data.js)0
-rw-r--r--spec/frontend/vue_shared/components/file_finder/item_spec.js140
-rw-r--r--spec/frontend/vue_shared/components/file_row_spec.js8
-rw-r--r--spec/frontend/vue_shared/components/filtered_search_dropdown_spec.js190
-rw-r--r--spec/frontend/vue_shared/components/gl_countdown_spec.js83
-rw-r--r--spec/frontend/vue_shared/components/header_ci_component_spec.js93
-rw-r--r--spec/frontend/vue_shared/components/identicon_spec.js37
-rw-r--r--spec/frontend/vue_shared/components/issue/issue_milestone_spec.js44
-rw-r--r--spec/frontend/vue_shared/components/issue/related_issuable_item_spec.js5
-rw-r--r--spec/frontend/vue_shared/components/markdown/field_spec.js34
-rw-r--r--spec/frontend/vue_shared/components/markdown/field_view_spec.js26
-rw-r--r--spec/frontend/vue_shared/components/markdown/suggestions_spec.js102
-rw-r--r--spec/frontend/vue_shared/components/markdown/toolbar_spec.js35
-rw-r--r--spec/frontend/vue_shared/components/navigation_tabs_spec.js64
-rw-r--r--spec/frontend/vue_shared/components/pikaday_spec.js30
-rw-r--r--spec/frontend/vue_shared/components/project_avatar/default_spec.js58
-rw-r--r--spec/frontend/vue_shared/components/project_selector/project_list_item_spec.js109
-rw-r--r--spec/frontend/vue_shared/components/project_selector/project_selector_spec.js112
-rw-r--r--spec/frontend/vue_shared/components/rich_content_editor/rich_content_editor_spec.js59
-rw-r--r--spec/frontend/vue_shared/components/rich_content_editor/toolbar_item_spec.js44
-rw-r--r--spec/frontend/vue_shared/components/rich_content_editor/toolbar_service_spec.js29
-rw-r--r--spec/frontend/vue_shared/components/sidebar/labels_select/base_spec.js5
-rw-r--r--spec/frontend/vue_shared/components/sidebar/labels_select/dropdown_button_spec.js20
-rw-r--r--spec/frontend/vue_shared/components/sidebar/labels_select/dropdown_create_label_spec.js2
-rw-r--r--spec/frontend/vue_shared/components/sidebar/labels_select/dropdown_footer_spec.js2
-rw-r--r--spec/frontend/vue_shared/components/sidebar/labels_select/dropdown_value_collapsed_spec.js2
-rw-r--r--spec/frontend/vue_shared/components/sidebar/labels_select/dropdown_value_spec.js5
-rw-r--r--spec/frontend/vue_shared/components/sidebar/labels_select/mock_data.js (renamed from spec/javascripts/vue_shared/components/sidebar/labels_select/mock_data.js)0
-rw-r--r--spec/frontend/vue_shared/components/sidebar/labels_select_vue/dropdown_button_spec.js25
-rw-r--r--spec/frontend/vue_shared/components/sidebar/labels_select_vue/dropdown_contents_create_view_spec.js14
-rw-r--r--spec/frontend/vue_shared/components/sidebar/labels_select_vue/dropdown_contents_labels_view_spec.js91
-rw-r--r--spec/frontend/vue_shared/components/sidebar/labels_select_vue/label_item_spec.js111
-rw-r--r--spec/frontend/vue_shared/components/sidebar/labels_select_vue/labels_select_root_spec.js24
-rw-r--r--spec/frontend/vue_shared/components/sidebar/labels_select_vue/mock_data.js3
-rw-r--r--spec/frontend/vue_shared/components/sidebar/labels_select_vue/store/actions_spec.js2
-rw-r--r--spec/frontend/vue_shared/components/sidebar/labels_select_vue/store/getters_spec.js24
-rw-r--r--spec/frontend/vue_shared/components/sidebar/labels_select_vue/store/mutations_spec.js5
-rw-r--r--spec/frontend/vue_shared/components/stacked_progress_bar_spec.js104
-rw-r--r--spec/frontend/vue_shared/components/tabs/tab_spec.js32
-rw-r--r--spec/frontend/vue_shared/components/tabs/tabs_spec.js61
-rw-r--r--spec/frontend/vue_shared/components/toggle_button_spec.js101
-rw-r--r--spec/frontend/wikis_spec.js26
-rw-r--r--spec/frontend_integration/.eslintrc.yml6
-rw-r--r--spec/frontend_integration/README.md17
-rw-r--r--spec/frontend_integration/ide/__snapshots__/ide_integration_spec.js.snap136
-rw-r--r--spec/frontend_integration/ide/ide_integration_spec.js100
-rw-r--r--spec/graphql/gitlab_schema_spec.rb16
-rw-r--r--spec/graphql/mutations/alert_management/create_alert_issue_spec.rb60
-rw-r--r--spec/graphql/mutations/alert_management/update_alert_status_spec.rb73
-rw-r--r--spec/graphql/mutations/branches/create_spec.rb55
-rw-r--r--spec/graphql/mutations/design_management/delete_spec.rb145
-rw-r--r--spec/graphql/mutations/design_management/upload_spec.rb136
-rw-r--r--spec/graphql/mutations/issues/set_confidential_spec.rb2
-rw-r--r--spec/graphql/mutations/issues/set_due_date_spec.rb2
-rw-r--r--spec/graphql/mutations/issues/update_spec.rb2
-rw-r--r--spec/graphql/mutations/merge_requests/set_labels_spec.rb2
-rw-r--r--spec/graphql/mutations/merge_requests/set_locked_spec.rb2
-rw-r--r--spec/graphql/mutations/merge_requests/set_milestone_spec.rb2
-rw-r--r--spec/graphql/mutations/merge_requests/set_subscription_spec.rb2
-rw-r--r--spec/graphql/mutations/merge_requests/set_wip_spec.rb2
-rw-r--r--spec/graphql/mutations/todos/mark_all_done_spec.rb2
-rw-r--r--spec/graphql/mutations/todos/mark_done_spec.rb2
-rw-r--r--spec/graphql/mutations/todos/restore_spec.rb2
-rw-r--r--spec/graphql/resolvers/alert_management/alert_status_counts_resolver_spec.rb24
-rw-r--r--spec/graphql/resolvers/alert_management_alert_resolver_spec.rb63
-rw-r--r--spec/graphql/resolvers/board_lists_resolver_spec.rb82
-rw-r--r--spec/graphql/resolvers/branch_commit_resolver_spec.rb26
-rw-r--r--spec/graphql/resolvers/design_management/design_at_version_resolver_spec.rb69
-rw-r--r--spec/graphql/resolvers/design_management/design_resolver_spec.rb88
-rw-r--r--spec/graphql/resolvers/design_management/designs_resolver_spec.rb93
-rw-r--r--spec/graphql/resolvers/design_management/version/design_at_version_resolver_spec.rb93
-rw-r--r--spec/graphql/resolvers/design_management/version/designs_at_version_resolver_spec.rb86
-rw-r--r--spec/graphql/resolvers/design_management/version_in_collection_resolver_spec.rb64
-rw-r--r--spec/graphql/resolvers/design_management/version_resolver_spec.rb43
-rw-r--r--spec/graphql/resolvers/design_management/versions_resolver_spec.rb117
-rw-r--r--spec/graphql/resolvers/issues_resolver_spec.rb78
-rw-r--r--spec/graphql/resolvers/milestone_resolver_spec.rb28
-rw-r--r--spec/graphql/resolvers/projects/jira_imports_resolver_spec.rb8
-rw-r--r--spec/graphql/resolvers/projects_resolver_spec.rb77
-rw-r--r--spec/graphql/resolvers/release_resolver_spec.rb51
-rw-r--r--spec/graphql/resolvers/releases_resolver_spec.rb42
-rw-r--r--spec/graphql/types/alert_management/alert_status_count_type_spec.rb20
-rw-r--r--spec/graphql/types/alert_management/alert_type_spec.rb31
-rw-r--r--spec/graphql/types/alert_management/severity_enum_spec.rb11
-rw-r--r--spec/graphql/types/alert_management/status_enum_spec.rb24
-rw-r--r--spec/graphql/types/award_emojis/award_emoji_type_spec.rb6
-rw-r--r--spec/graphql/types/blob_viewers/type_enum_spec.rb2
-rw-r--r--spec/graphql/types/board_list_type_spec.rb13
-rw-r--r--spec/graphql/types/board_type_spec.rb4
-rw-r--r--spec/graphql/types/branch_type_spec.rb9
-rw-r--r--spec/graphql/types/ci/detailed_status_type_spec.rb2
-rw-r--r--spec/graphql/types/ci/pipeline_type_spec.rb4
-rw-r--r--spec/graphql/types/commit_type_spec.rb6
-rw-r--r--spec/graphql/types/design_management/design_at_version_type_spec.rb16
-rw-r--r--spec/graphql/types/design_management/design_collection_type_spec.rb13
-rw-r--r--spec/graphql/types/design_management/design_type_spec.rb13
-rw-r--r--spec/graphql/types/design_management/design_version_event_enum_spec.rb11
-rw-r--r--spec/graphql/types/design_management/version_type_spec.rb13
-rw-r--r--spec/graphql/types/design_management_type_spec.rb7
-rw-r--r--spec/graphql/types/diff_refs_type_spec.rb10
-rw-r--r--spec/graphql/types/environment_type_spec.rb4
-rw-r--r--spec/graphql/types/error_tracking/sentry_detailed_error_type_spec.rb4
-rw-r--r--spec/graphql/types/error_tracking/sentry_error_collection_type_spec.rb4
-rw-r--r--spec/graphql/types/error_tracking/sentry_error_stack_trace_entry_type_spec.rb2
-rw-r--r--spec/graphql/types/error_tracking/sentry_error_stack_trace_type_spec.rb4
-rw-r--r--spec/graphql/types/error_tracking/sentry_error_type_spec.rb2
-rw-r--r--spec/graphql/types/grafana_integration_type_spec.rb6
-rw-r--r--spec/graphql/types/group_type_spec.rb6
-rw-r--r--spec/graphql/types/issuable_sort_enum_spec.rb15
-rw-r--r--spec/graphql/types/issuable_state_enum_spec.rb2
-rw-r--r--spec/graphql/types/issue_sort_enum_spec.rb6
-rw-r--r--spec/graphql/types/issue_state_enum_spec.rb2
-rw-r--r--spec/graphql/types/issue_type_spec.rb11
-rw-r--r--spec/graphql/types/jira_import_type_spec.rb4
-rw-r--r--spec/graphql/types/label_type_spec.rb2
-rw-r--r--spec/graphql/types/merge_request_state_enum_spec.rb2
-rw-r--r--spec/graphql/types/merge_request_type_spec.rb6
-rw-r--r--spec/graphql/types/metadata_type_spec.rb4
-rw-r--r--spec/graphql/types/metrics/dashboard_type_spec.rb2
-rw-r--r--spec/graphql/types/metrics/dashboards/annotation_type_spec.rb4
-rw-r--r--spec/graphql/types/milestone_type_spec.rb4
-rw-r--r--spec/graphql/types/namespace_type_spec.rb4
-rw-r--r--spec/graphql/types/notes/discussion_type_spec.rb4
-rw-r--r--spec/graphql/types/notes/note_type_spec.rb4
-rw-r--r--spec/graphql/types/notes/noteable_type_spec.rb3
-rw-r--r--spec/graphql/types/permission_types/issue_spec.rb5
-rw-r--r--spec/graphql/types/permission_types/merge_request_type_spec.rb2
-rw-r--r--spec/graphql/types/permission_types/project_spec.rb2
-rw-r--r--spec/graphql/types/project_type_spec.rb23
-rw-r--r--spec/graphql/types/projects/base_service_type_spec.rb4
-rw-r--r--spec/graphql/types/projects/jira_service_type_spec.rb4
-rw-r--r--spec/graphql/types/projects/service_type_spec.rb2
-rw-r--r--spec/graphql/types/projects/services_enum_spec.rb2
-rw-r--r--spec/graphql/types/query_type_spec.rb2
-rw-r--r--spec/graphql/types/release_type_spec.rb37
-rw-r--r--spec/graphql/types/repository_type_spec.rb8
-rw-r--r--spec/graphql/types/root_storage_statistics_type_spec.rb4
-rw-r--r--spec/graphql/types/snippet_type_spec.rb10
-rw-r--r--spec/graphql/types/snippets/blob_type_spec.rb16
-rw-r--r--spec/graphql/types/snippets/blob_viewer_type_spec.rb81
-rw-r--r--spec/graphql/types/time_type_spec.rb2
-rw-r--r--spec/graphql/types/todo_type_spec.rb2
-rw-r--r--spec/graphql/types/tree/blob_type_spec.rb4
-rw-r--r--spec/graphql/types/tree/submodule_type_spec.rb4
-rw-r--r--spec/graphql/types/tree/tree_entry_type_spec.rb4
-rw-r--r--spec/graphql/types/tree/tree_type_spec.rb4
-rw-r--r--spec/graphql/types/tree/type_enum_spec.rb2
-rw-r--r--spec/graphql/types/user_type_spec.rb6
-rw-r--r--spec/haml_lint/linter/no_plain_nodes_spec.rb38
-rw-r--r--spec/helpers/access_tokens_helper_spec.rb18
-rw-r--r--spec/helpers/application_helper_spec.rb27
-rw-r--r--spec/helpers/auth_helper_spec.rb36
-rw-r--r--spec/helpers/boards_helper_spec.rb4
-rw-r--r--spec/helpers/clusters_helper_spec.rb26
-rw-r--r--spec/helpers/commits_helper_spec.rb28
-rw-r--r--spec/helpers/environments_helper_spec.rb18
-rw-r--r--spec/helpers/events_helper_spec.rb14
-rw-r--r--spec/helpers/export_helper_spec.rb11
-rw-r--r--spec/helpers/groups_helper_spec.rb27
-rw-r--r--spec/helpers/issuables_helper_spec.rb42
-rw-r--r--spec/helpers/markup_helper_spec.rb39
-rw-r--r--spec/helpers/members_helper_spec.rb11
-rw-r--r--spec/helpers/milestones_helper_spec.rb15
-rw-r--r--spec/helpers/nav_helper_spec.rb23
-rw-r--r--spec/helpers/preferences_helper_spec.rb2
-rw-r--r--spec/helpers/projects/alert_management_helper_spec.rb82
-rw-r--r--spec/helpers/projects_helper_spec.rb42
-rw-r--r--spec/helpers/releases_helper_spec.rb4
-rw-r--r--spec/helpers/search_helper_spec.rb1
-rw-r--r--spec/helpers/snippets_helper_spec.rb31
-rw-r--r--spec/helpers/todos_helper_spec.rb71
-rw-r--r--spec/helpers/visibility_level_helper_spec.rb26
-rw-r--r--spec/helpers/x509_helper_spec.rb18
-rw-r--r--spec/initializers/action_mailer_hooks_spec.rb4
-rw-r--r--spec/initializers/lograge_spec.rb2
-rw-r--r--spec/initializers/secret_token_spec.rb9
-rw-r--r--spec/initializers/zz_metrics_spec.rb4
-rw-r--r--spec/javascripts/ajax_loading_spinner_spec.js57
-rw-r--r--spec/javascripts/avatar_helper_spec.js98
-rw-r--r--spec/javascripts/bootstrap_linked_tabs_spec.js67
-rw-r--r--spec/javascripts/ci_variable_list/ajax_variable_list_spec.js231
-rw-r--r--spec/javascripts/ci_variable_list/ci_variable_list_spec.js294
-rw-r--r--spec/javascripts/close_reopen_report_toggle_spec.js272
-rw-r--r--spec/javascripts/commits_spec.js98
-rw-r--r--spec/javascripts/deploy_keys/components/action_btn_spec.js72
-rw-r--r--spec/javascripts/deploy_keys/components/app_spec.js155
-rw-r--r--spec/javascripts/deploy_keys/components/key_spec.js157
-rw-r--r--spec/javascripts/deploy_keys/components/keys_panel_spec.js63
-rw-r--r--spec/javascripts/diff_comments_store_spec.js141
-rw-r--r--spec/javascripts/diffs/create_diffs_store.js5
-rw-r--r--spec/javascripts/diffs/mock_data/diff_discussions.js5
-rw-r--r--spec/javascripts/diffs/mock_data/diff_file.js5
-rw-r--r--spec/javascripts/diffs/mock_data/diff_file_unreadable.js5
-rw-r--r--spec/javascripts/diffs/mock_data/diff_with_commit.js7
-rw-r--r--spec/javascripts/diffs/mock_data/merge_request_diffs.js7
-rw-r--r--spec/javascripts/dirty_submit/dirty_submit_collection_spec.js29
-rw-r--r--spec/javascripts/dirty_submit/dirty_submit_form_spec.js114
-rw-r--r--spec/javascripts/dirty_submit/helper.js48
-rw-r--r--spec/javascripts/editor/editor_lite_spec.js160
-rw-r--r--spec/javascripts/emoji_spec.js486
-rw-r--r--spec/javascripts/feature_highlight/feature_highlight_helper_spec.js75
-rw-r--r--spec/javascripts/feature_highlight/feature_highlight_spec.js141
-rw-r--r--spec/javascripts/filtered_search/dropdown_utils_spec.js374
-rw-r--r--spec/javascripts/filtered_search/filtered_search_manager_spec.js580
-rw-r--r--spec/javascripts/filtered_search/recent_searches_root_spec.js30
-rw-r--r--spec/javascripts/filtered_search/services/recent_searches_service_spec.js158
-rw-r--r--spec/javascripts/filtered_search/visual_token_value_spec.js389
-rw-r--r--spec/javascripts/flash_spec.js236
-rw-r--r--spec/javascripts/frequent_items/components/app_spec.js257
-rw-r--r--spec/javascripts/frequent_items/mock_data.js168
-rw-r--r--spec/javascripts/frequent_items/store/actions_spec.js228
-rw-r--r--spec/javascripts/frequent_items/utils_spec.js130
-rw-r--r--spec/javascripts/gl_dropdown_spec.js22
-rw-r--r--spec/javascripts/groups/components/app_spec.js533
-rw-r--r--spec/javascripts/groups/components/group_folder_spec.js67
-rw-r--r--spec/javascripts/groups/components/group_item_spec.js218
-rw-r--r--spec/javascripts/groups/components/groups_spec.js76
-rw-r--r--spec/javascripts/groups/components/item_actions_spec.js84
-rw-r--r--spec/javascripts/groups/components/item_caret_spec.js38
-rw-r--r--spec/javascripts/groups/components/item_stats_spec.js128
-rw-r--r--spec/javascripts/groups/components/item_stats_value_spec.js82
-rw-r--r--spec/javascripts/groups/components/item_type_icon_spec.js58
-rw-r--r--spec/javascripts/groups/service/groups_service_spec.js42
-rw-r--r--spec/javascripts/groups/store/groups_store_spec.js123
-rw-r--r--spec/javascripts/helpers/filtered_search_spec_helper.js70
-rw-r--r--spec/javascripts/helpers/init_vue_mr_page_helper.js2
-rw-r--r--spec/javascripts/helpers/vue_mount_component_helper.js40
-rw-r--r--spec/javascripts/ide/components/activity_bar_spec.js72
-rw-r--r--spec/javascripts/ide/components/commit_sidebar/radio_group_spec.js139
-rw-r--r--spec/javascripts/ide/components/file_row_extra_spec.js170
-rw-r--r--spec/javascripts/ide/components/file_templates/bar_spec.js117
-rw-r--r--spec/javascripts/ide/components/ide_review_spec.js69
-rw-r--r--spec/javascripts/ide/components/ide_side_bar_spec.js57
-rw-r--r--spec/javascripts/ide/components/ide_spec.js125
-rw-r--r--spec/javascripts/ide/components/ide_status_bar_spec.js129
-rw-r--r--spec/javascripts/ide/components/ide_tree_list_spec.js77
-rw-r--r--spec/javascripts/ide/components/ide_tree_spec.js34
-rw-r--r--spec/javascripts/ide/components/merge_requests/item_spec.js63
-rw-r--r--spec/javascripts/ide/components/nav_dropdown_button_spec.js93
-rw-r--r--spec/javascripts/ide/components/nav_dropdown_spec.js80
-rw-r--r--spec/javascripts/ide/components/new_dropdown/button_spec.js65
-rw-r--r--spec/javascripts/ide/components/new_dropdown/index_spec.js84
-rw-r--r--spec/javascripts/ide/components/new_dropdown/modal_spec.js150
-rw-r--r--spec/javascripts/ide/components/new_dropdown/upload_spec.js112
-rw-r--r--spec/javascripts/ide/components/repo_editor_spec.js43
-rw-r--r--spec/javascripts/ide/components/repo_tab_spec.js185
-rw-r--r--spec/javascripts/ide/components/shared/tokened_input_spec.js133
-rw-r--r--spec/javascripts/ide/lib/common/model_manager_spec.js126
-rw-r--r--spec/javascripts/ide/lib/common/model_spec.js137
-rw-r--r--spec/javascripts/ide/lib/decorations/controller_spec.js143
-rw-r--r--spec/javascripts/ide/lib/diff/controller_spec.js215
-rw-r--r--spec/javascripts/ide/lib/editor_spec.js287
-rw-r--r--spec/javascripts/ide/stores/actions/tree_spec.js3
-rw-r--r--spec/javascripts/image_diff/helpers/badge_helper_spec.js130
-rw-r--r--spec/javascripts/image_diff/helpers/comment_indicator_helper_spec.js144
-rw-r--r--spec/javascripts/image_diff/helpers/dom_helper_spec.js120
-rw-r--r--spec/javascripts/image_diff/image_badge_spec.js96
-rw-r--r--spec/javascripts/image_diff/image_diff_spec.js361
-rw-r--r--spec/javascripts/image_diff/replaced_image_diff_spec.js355
-rw-r--r--spec/javascripts/integrations/integration_settings_form_spec.js301
-rw-r--r--spec/javascripts/issuable_spec.js64
-rw-r--r--spec/javascripts/issue_show/components/app_spec.js568
-rw-r--r--spec/javascripts/issue_show/components/description_spec.js210
-rw-r--r--spec/javascripts/issue_show/components/fields/description_template_spec.js43
-rw-r--r--spec/javascripts/issue_show/components/form_spec.js98
-rw-r--r--spec/javascripts/issue_show/components/title_spec.js105
-rw-r--r--spec/javascripts/issue_show/helpers.js1
-rw-r--r--spec/javascripts/issue_show/mock_data.js1
-rw-r--r--spec/javascripts/jobs/components/commit_block_spec.js89
-rw-r--r--spec/javascripts/jobs/components/job_container_item_spec.js99
-rw-r--r--spec/javascripts/jobs/components/job_log_spec.js65
-rw-r--r--spec/javascripts/jobs/components/sidebar_spec.js169
-rw-r--r--spec/javascripts/jobs/components/stages_dropdown_spec.js163
-rw-r--r--spec/javascripts/jobs/mixins/delayed_job_mixin_spec.js93
-rw-r--r--spec/javascripts/jobs/store/actions_spec.js512
-rw-r--r--spec/javascripts/landing_spec.js166
-rw-r--r--spec/javascripts/lib/utils/csrf_token_spec.js50
-rw-r--r--spec/javascripts/lib/utils/navigation_utility_spec.js23
-rw-r--r--spec/javascripts/lib/utils/poll_spec.js222
-rw-r--r--spec/javascripts/lib/utils/sticky_spec.js66
-rw-r--r--spec/javascripts/line_highlighter_spec.js17
-rw-r--r--spec/javascripts/monitoring/components/dashboard_resize_browser_spec.js94
-rw-r--r--spec/javascripts/monitoring/components/dashboard_resize_spec.js87
-rw-r--r--spec/javascripts/notebook/cells/code_spec.js74
-rw-r--r--spec/javascripts/notebook/cells/markdown_spec.js105
-rw-r--r--spec/javascripts/notebook/cells/output/index_spec.js115
-rw-r--r--spec/javascripts/notebook/cells/prompt_spec.js56
-rw-r--r--spec/javascripts/notebook/index_spec.js100
-rw-r--r--spec/javascripts/pages/admin/jobs/index/components/stop_jobs_modal_spec.js61
-rw-r--r--spec/javascripts/pages/labels/components/promote_label_modal_spec.js103
-rw-r--r--spec/javascripts/pages/milestones/shared/components/delete_milestone_modal_spec.js106
-rw-r--r--spec/javascripts/pages/milestones/shared/components/promote_milestone_modal_spec.js98
-rw-r--r--spec/javascripts/pages/projects/pipeline_schedules/shared/components/interval_pattern_input_spec.js192
-rw-r--r--spec/javascripts/pages/projects/pipeline_schedules/shared/components/pipeline_schedule_callout_spec.js106
-rw-r--r--spec/javascripts/pipelines/header_component_spec.js108
-rw-r--r--spec/javascripts/pipelines/linked_pipelines_mock.json3535
-rw-r--r--spec/javascripts/pipelines/mock_data.js423
-rw-r--r--spec/javascripts/pipelines/pipeline_details_mediator_spec.js36
-rw-r--r--spec/javascripts/pipelines/pipelines_actions_spec.js128
-rw-r--r--spec/javascripts/pipelines/pipelines_artifacts_spec.js38
-rw-r--r--spec/javascripts/pipelines/pipelines_spec.js783
-rw-r--r--spec/javascripts/pipelines/pipelines_table_spec.js86
-rw-r--r--spec/javascripts/pipelines/stage_spec.js136
-rw-r--r--spec/javascripts/pipelines/stores/pipeline.json167
-rw-r--r--spec/javascripts/pipelines/stores/pipeline_store.js165
-rw-r--r--spec/javascripts/pipelines/stores/pipeline_with_triggered.json381
-rw-r--r--spec/javascripts/pipelines/stores/pipeline_with_triggered_by.json379
-rw-r--r--spec/javascripts/pipelines/stores/pipeline_with_triggered_triggered_by.json452
-rw-r--r--spec/javascripts/pipelines/time_ago_spec.js64
-rw-r--r--spec/javascripts/prometheus_metrics/mock_data.js41
-rw-r--r--spec/javascripts/prometheus_metrics/prometheus_metrics_spec.js178
-rw-r--r--spec/javascripts/related_merge_requests/components/related_merge_requests_spec.js88
-rw-r--r--spec/javascripts/related_merge_requests/store/actions_spec.js110
-rw-r--r--spec/javascripts/reports/components/grouped_test_reports_app_spec.js239
-rw-r--r--spec/javascripts/reports/components/modal_open_name_spec.js47
-rw-r--r--spec/javascripts/reports/components/summary_row_spec.js37
-rw-r--r--spec/javascripts/reports/components/test_issue_body_spec.js72
-rw-r--r--spec/javascripts/reports/mock_data/mock_data.js8
-rw-r--r--spec/javascripts/reports/store/actions_spec.js171
-rw-r--r--spec/javascripts/search_autocomplete_spec.js24
-rw-r--r--spec/javascripts/sidebar/components/time_tracking/time_tracker_spec.js278
-rw-r--r--spec/javascripts/sidebar/lock/edit_form_buttons_spec.js32
-rw-r--r--spec/javascripts/sidebar/lock/lock_issue_sidebar_spec.js99
-rw-r--r--spec/javascripts/sidebar/mock_data.js7
-rw-r--r--spec/javascripts/sidebar/participants_spec.js202
-rw-r--r--spec/javascripts/sidebar/sidebar_mediator_spec.js134
-rw-r--r--spec/javascripts/sidebar/sidebar_move_issue_spec.js166
-rw-r--r--spec/javascripts/sidebar/sidebar_subscriptions_spec.js38
-rw-r--r--spec/javascripts/sidebar/subscriptions_spec.js100
-rw-r--r--spec/javascripts/vue_mr_widget/components/mr_widget_header_spec.js4
-rw-r--r--spec/javascripts/vue_mr_widget/components/mr_widget_pipeline_container_spec.js99
-rw-r--r--spec/javascripts/vue_mr_widget/components/mr_widget_pipeline_spec.js13
-rw-r--r--spec/javascripts/vue_mr_widget/components/states/mr_widget_ready_to_merge_spec.js7
-rw-r--r--spec/javascripts/vue_mr_widget/stores/artifacts_list/actions_spec.js165
-rw-r--r--spec/javascripts/vue_mr_widget/stores/mr_widget_store_spec.js112
-rw-r--r--spec/javascripts/vue_shared/components/ci_badge_link_spec.js100
-rw-r--r--spec/javascripts/vue_shared/components/ci_icon_spec.js122
-rw-r--r--spec/javascripts/vue_shared/components/content_viewer/content_viewer_spec.js123
-rw-r--r--spec/javascripts/vue_shared/components/diff_viewer/diff_viewer_spec.js105
-rw-r--r--spec/javascripts/vue_shared/components/dropdown/dropdown_button_spec.js81
-rw-r--r--spec/javascripts/vue_shared/components/dropdown/dropdown_hidden_input_spec.js36
-rw-r--r--spec/javascripts/vue_shared/components/file_finder/item_spec.js140
-rw-r--r--spec/javascripts/vue_shared/components/filtered_search_dropdown_spec.js190
-rw-r--r--spec/javascripts/vue_shared/components/gl_countdown_spec.js77
-rw-r--r--spec/javascripts/vue_shared/components/header_ci_component_spec.js93
-rw-r--r--spec/javascripts/vue_shared/components/markdown/suggestions_spec.js106
-rw-r--r--spec/javascripts/vue_shared/components/markdown/toolbar_spec.js40
-rw-r--r--spec/javascripts/vue_shared/components/navigation_tabs_spec.js64
-rw-r--r--spec/javascripts/vue_shared/components/pikaday_spec.js30
-rw-r--r--spec/javascripts/vue_shared/components/project_avatar/default_spec.js58
-rw-r--r--spec/javascripts/vue_shared/components/project_selector/project_list_item_spec.js109
-rw-r--r--spec/javascripts/vue_shared/components/project_selector/project_selector_spec.js142
-rw-r--r--spec/javascripts/vue_shared/components/stacked_progress_bar_spec.js107
-rw-r--r--spec/javascripts/vue_shared/components/tabs/tab_spec.js32
-rw-r--r--spec/javascripts/vue_shared/components/tabs/tabs_spec.js68
-rw-r--r--spec/javascripts/vue_shared/components/toggle_button_spec.js101
-rw-r--r--spec/javascripts/vue_shared/components/user_avatar/user_avatar_svg_spec.js29
-rw-r--r--spec/lib/api/entities/branch_spec.rb28
-rw-r--r--spec/lib/api/entities/design_management/design_spec.rb19
-rw-r--r--spec/lib/api/entities/project_repository_storage_move_spec.rb21
-rw-r--r--spec/lib/api/entities/snippet_spec.rb94
-rw-r--r--spec/lib/api/helpers/pagination_strategies_spec.rb77
-rw-r--r--spec/lib/banzai/filter/issue_reference_filter_spec.rb55
-rw-r--r--spec/lib/banzai/filter/upload_link_filter_spec.rb17
-rw-r--r--spec/lib/banzai/filter/user_reference_filter_spec.rb2
-rw-r--r--spec/lib/banzai/pipeline/wiki_pipeline_spec.rb8
-rw-r--r--spec/lib/banzai/reference_parser/design_parser_spec.rb91
-rw-r--r--spec/lib/banzai/renderer_spec.rb57
-rw-r--r--spec/lib/bitbucket_server/representation/activity_spec.rb2
-rw-r--r--spec/lib/bitbucket_server/representation/comment_spec.rb2
-rw-r--r--spec/lib/bitbucket_server/representation/pull_request_comment_spec.rb2
-rw-r--r--spec/lib/bitbucket_server/representation/pull_request_spec.rb2
-rw-r--r--spec/lib/bitbucket_server/representation/repo_spec.rb2
-rw-r--r--spec/lib/container_registry/client_spec.rb52
-rw-r--r--spec/lib/declarative_policy_spec.rb38
-rw-r--r--spec/lib/feature_spec.rb8
-rw-r--r--spec/lib/gitlab/alert_management/alert_params_spec.rb94
-rw-r--r--spec/lib/gitlab/alert_management/alert_status_counts_spec.rb55
-rw-r--r--spec/lib/gitlab/alerting/alert_spec.rb24
-rw-r--r--spec/lib/gitlab/alerting/notification_payload_parser_spec.rb29
-rw-r--r--spec/lib/gitlab/analytics/cycle_analytics/median_spec.rb42
-rw-r--r--spec/lib/gitlab/app_json_logger_spec.rb4
-rw-r--r--spec/lib/gitlab/application_context_spec.rb14
-rw-r--r--spec/lib/gitlab/auth/auth_finders_spec.rb160
-rw-r--r--spec/lib/gitlab/auth/o_auth/provider_spec.rb12
-rw-r--r--spec/lib/gitlab/auth_spec.rb8
-rw-r--r--spec/lib/gitlab/background_migration/backfill_environment_id_deployment_merge_requests_spec.rb46
-rw-r--r--spec/lib/gitlab/background_migration/backfill_snippet_repositories_spec.rb187
-rw-r--r--spec/lib/gitlab/background_migration/migrate_issue_trackers_sensitive_data_spec.rb73
-rw-r--r--spec/lib/gitlab/chat/responder/mattermost_spec.rb117
-rw-r--r--spec/lib/gitlab/checks/push_file_count_check_spec.rb4
-rw-r--r--spec/lib/gitlab/ci/config/entry/artifacts_spec.rb48
-rw-r--r--spec/lib/gitlab/ci/config/entry/reports_spec.rb2
-rw-r--r--spec/lib/gitlab/ci/config/entry/trigger_spec.rb13
-rw-r--r--spec/lib/gitlab/ci/cron_parser_spec.rb314
-rw-r--r--spec/lib/gitlab/ci/parsers/accessibility/pa11y_spec.rb118
-rw-r--r--spec/lib/gitlab/ci/parsers/terraform/tfplan_spec.rb51
-rw-r--r--spec/lib/gitlab/ci/parsers/test/junit_spec.rb60
-rw-r--r--spec/lib/gitlab/ci/parsers_spec.rb16
-rw-r--r--spec/lib/gitlab/ci/pipeline/chain/sequence_spec.rb10
-rw-r--r--spec/lib/gitlab/ci/reports/accessibility_reports_comparer_spec.rb270
-rw-r--r--spec/lib/gitlab/ci/reports/accessibility_reports_spec.rb232
-rw-r--r--spec/lib/gitlab/ci/reports/terraform_reports_spec.rb34
-rw-r--r--spec/lib/gitlab/ci/reports/test_case_spec.rb2
-rw-r--r--spec/lib/gitlab/ci/reports/test_reports_spec.rb25
-rw-r--r--spec/lib/gitlab/ci/reports/test_suite_spec.rb27
-rw-r--r--spec/lib/gitlab/ci/templates/Jobs/browser_performance_testing_gitlab_ci_yaml_spec.rb85
-rw-r--r--spec/lib/gitlab/ci/templates/Jobs/build_gitlab_ci_yaml_spec.rb58
-rw-r--r--spec/lib/gitlab/ci/templates/Jobs/code_quality_gitlab_ci_yaml_spec.rb86
-rw-r--r--spec/lib/gitlab/ci/templates/Jobs/deploy_gitlab_ci_yaml_spec.rb222
-rw-r--r--spec/lib/gitlab/ci/templates/Jobs/test_gitlab_ci_yaml_spec.rb86
-rw-r--r--spec/lib/gitlab/ci/templates/auto_devops_gitlab_ci_yaml_spec.rb100
-rw-r--r--spec/lib/gitlab/ci/yaml_processor_spec.rb22
-rw-r--r--spec/lib/gitlab/code_navigation_path_spec.rb31
-rw-r--r--spec/lib/gitlab/config_checker/external_database_checker_spec.rb56
-rw-r--r--spec/lib/gitlab/cycle_analytics/group_stage_summary_spec.rb176
-rw-r--r--spec/lib/gitlab/cycle_analytics/summary/value_spec.rb33
-rw-r--r--spec/lib/gitlab/danger/changelog_spec.rb8
-rw-r--r--spec/lib/gitlab/danger/helper_spec.rb65
-rw-r--r--spec/lib/gitlab/danger/teammate_spec.rb13
-rw-r--r--spec/lib/gitlab/database/batch_count_spec.rb8
-rw-r--r--spec/lib/gitlab/database/migration_helpers_spec.rb391
-rw-r--r--spec/lib/gitlab/database/partitioning_migration_helpers/partitioned_foreign_key_spec.rb48
-rw-r--r--spec/lib/gitlab/database/partitioning_migration_helpers_spec.rb230
-rw-r--r--spec/lib/gitlab/database/rename_reserved_paths_migration/v1/rename_base_spec.rb4
-rw-r--r--spec/lib/gitlab/database/with_lock_retries_spec.rb21
-rw-r--r--spec/lib/gitlab/diff/file_spec.rb55
-rw-r--r--spec/lib/gitlab/diff/formatters/text_formatter_spec.rb18
-rw-r--r--spec/lib/gitlab/diff/position_spec.rb4
-rw-r--r--spec/lib/gitlab/elasticsearch/logs/lines_spec.rb24
-rw-r--r--spec/lib/gitlab/elasticsearch/logs/pods_spec.rb4
-rw-r--r--spec/lib/gitlab/email/handler_spec.rb12
-rw-r--r--spec/lib/gitlab/email/hook/smime_signature_interceptor_spec.rb23
-rw-r--r--spec/lib/gitlab/email/smime/certificate_spec.rb55
-rw-r--r--spec/lib/gitlab/email/smime/signer_spec.rb35
-rw-r--r--spec/lib/gitlab/exclusive_lease_helpers/sleeping_lock_spec.rb102
-rw-r--r--spec/lib/gitlab/exclusive_lease_helpers_spec.rb31
-rw-r--r--spec/lib/gitlab/exclusive_lease_spec.rb82
-rw-r--r--spec/lib/gitlab/experimentation_spec.rb53
-rw-r--r--spec/lib/gitlab/gfm/reference_rewriter_spec.rb12
-rw-r--r--spec/lib/gitlab/git/attributes_parser_spec.rb8
-rw-r--r--spec/lib/gitlab/git/blob_spec.rb12
-rw-r--r--spec/lib/gitlab/git/commit_spec.rb20
-rw-r--r--spec/lib/gitlab/git/tag_spec.rb30
-rw-r--r--spec/lib/gitlab/git_access_design_spec.rb45
-rw-r--r--spec/lib/gitlab/git_access_snippet_spec.rb153
-rw-r--r--spec/lib/gitlab/git_access_wiki_spec.rb10
-rw-r--r--spec/lib/gitlab/gl_repository/repo_type_spec.rb37
-rw-r--r--spec/lib/gitlab/gl_repository_spec.rb15
-rw-r--r--spec/lib/gitlab/google_code_import/client_spec.rb2
-rw-r--r--spec/lib/gitlab/google_code_import/importer_spec.rb2
-rw-r--r--spec/lib/gitlab/grape_logging/formatters/lograge_with_timestamp_spec.rb2
-rw-r--r--spec/lib/gitlab/grape_logging/loggers/cloudflare_logger_spec.rb31
-rw-r--r--spec/lib/gitlab/grape_logging/loggers/exception_logger_spec.rb69
-rw-r--r--spec/lib/gitlab/graphql/authorize/authorize_field_service_spec.rb10
-rw-r--r--spec/lib/gitlab/graphql/pagination/keyset/connection_spec.rb15
-rw-r--r--spec/lib/gitlab/graphql_logger_spec.rb6
-rw-r--r--spec/lib/gitlab/health_checks/master_check_spec.rb5
-rw-r--r--spec/lib/gitlab/hook_data/issuable_builder_spec.rb1
-rw-r--r--spec/lib/gitlab/import_export/all_models.yml22
-rw-r--r--spec/lib/gitlab/import_export/attribute_configuration_spec.rb3
-rw-r--r--spec/lib/gitlab/import_export/design_repo_restorer_spec.rb42
-rw-r--r--spec/lib/gitlab/import_export/design_repo_saver_spec.rb37
-rw-r--r--spec/lib/gitlab/import_export/fast_hash_serializer_spec.rb2
-rw-r--r--spec/lib/gitlab/import_export/group/legacy_tree_restorer_spec.rb2
-rw-r--r--spec/lib/gitlab/import_export/group/tree_restorer_spec.rb184
-rw-r--r--spec/lib/gitlab/import_export/group/tree_saver_spec.rb140
-rw-r--r--spec/lib/gitlab/import_export/import_export_equivalence_spec.rb4
-rw-r--r--spec/lib/gitlab/import_export/import_test_coverage_spec.rb13
-rw-r--r--spec/lib/gitlab/import_export/importer_spec.rb79
-rw-r--r--spec/lib/gitlab/import_export/json/legacy_reader/file_spec.rb2
-rw-r--r--spec/lib/gitlab/import_export/json/legacy_reader/hash_spec.rb4
-rw-r--r--spec/lib/gitlab/import_export/json/ndjson_reader_spec.rb14
-rw-r--r--spec/lib/gitlab/import_export/lfs_saver_spec.rb2
-rw-r--r--spec/lib/gitlab/import_export/project/export_task_spec.rb43
-rw-r--r--spec/lib/gitlab/import_export/project/import_task_spec.rb49
-rw-r--r--spec/lib/gitlab/import_export/project/tree_restorer_spec.rb108
-rw-r--r--spec/lib/gitlab/import_export/project/tree_saver_spec.rb25
-rw-r--r--spec/lib/gitlab/import_export/relation_tree_restorer_spec.rb14
-rw-r--r--spec/lib/gitlab/import_export/safe_model_attributes.yml2
-rw-r--r--spec/lib/gitlab/instrumentation_helper_spec.rb6
-rw-r--r--spec/lib/gitlab/jira_import/base_importer_spec.rb20
-rw-r--r--spec/lib/gitlab/jira_import/handle_labels_service_spec.rb53
-rw-r--r--spec/lib/gitlab/jira_import/issue_serializer_spec.rb150
-rw-r--r--spec/lib/gitlab/jira_import/issues_importer_spec.rb36
-rw-r--r--spec/lib/gitlab/jira_import/labels_importer_spec.rb83
-rw-r--r--spec/lib/gitlab/jira_import/metadata_collector_spec.rb178
-rw-r--r--spec/lib/gitlab/jira_import/user_mapper_spec.rb80
-rw-r--r--spec/lib/gitlab/json_logger_spec.rb4
-rw-r--r--spec/lib/gitlab/json_spec.rb152
-rw-r--r--spec/lib/gitlab/kubernetes/helm/api_spec.rb25
-rw-r--r--spec/lib/gitlab/kubernetes/helm/base_command_spec.rb80
-rw-r--r--spec/lib/gitlab/kubernetes/helm/delete_command_spec.rb41
-rw-r--r--spec/lib/gitlab/kubernetes/helm/init_command_spec.rb73
-rw-r--r--spec/lib/gitlab/kubernetes/helm/install_command_spec.rb84
-rw-r--r--spec/lib/gitlab/kubernetes/helm/parsers/list_v2_spec.rb100
-rw-r--r--spec/lib/gitlab/kubernetes/helm/patch_command_spec.rb68
-rw-r--r--spec/lib/gitlab/kubernetes/helm/pod_spec.rb2
-rw-r--r--spec/lib/gitlab/kubernetes/helm/reset_command_spec.rb33
-rw-r--r--spec/lib/gitlab/kubernetes/kube_client_spec.rb108
-rw-r--r--spec/lib/gitlab/kubernetes/network_policy_spec.rb224
-rw-r--r--spec/lib/gitlab/legacy_github_import/importer_spec.rb1
-rw-r--r--spec/lib/gitlab/logging/cloudflare_helper_spec.rb52
-rw-r--r--spec/lib/gitlab/lograge/custom_options_spec.rb33
-rw-r--r--spec/lib/gitlab/mail_room/mail_room_spec.rb3
-rw-r--r--spec/lib/gitlab/metrics/background_transaction_spec.rb6
-rw-r--r--spec/lib/gitlab/metrics/dashboard/stages/grafana_formatter_spec.rb6
-rw-r--r--spec/lib/gitlab/metrics/dashboard/url_spec.rb32
-rw-r--r--spec/lib/gitlab/metrics/exporter/sidekiq_exporter_spec.rb2
-rw-r--r--spec/lib/gitlab/metrics/method_call_spec.rb19
-rw-r--r--spec/lib/gitlab/metrics/metric_spec.rb71
-rw-r--r--spec/lib/gitlab/metrics/rack_middleware_spec.rb25
-rw-r--r--spec/lib/gitlab/metrics/samplers/database_sampler_spec.rb49
-rw-r--r--spec/lib/gitlab/metrics/samplers/influx_sampler_spec.rb105
-rw-r--r--spec/lib/gitlab/metrics/samplers/ruby_sampler_spec.rb32
-rw-r--r--spec/lib/gitlab/metrics/sidekiq_middleware_spec.rb7
-rw-r--r--spec/lib/gitlab/metrics/subscribers/action_view_spec.rb6
-rw-r--r--spec/lib/gitlab/metrics/system_spec.rb117
-rw-r--r--spec/lib/gitlab/metrics/transaction_spec.rb165
-rw-r--r--spec/lib/gitlab/metrics/web_transaction_spec.rb144
-rw-r--r--spec/lib/gitlab/metrics_spec.rb78
-rw-r--r--spec/lib/gitlab/middleware/multipart_spec.rb11
-rw-r--r--spec/lib/gitlab/omniauth_initializer_spec.rb16
-rw-r--r--spec/lib/gitlab/pagination/keyset_spec.rb12
-rw-r--r--spec/lib/gitlab/path_regex_spec.rb15
-rw-r--r--spec/lib/gitlab/performance_bar_spec.rb59
-rw-r--r--spec/lib/gitlab/phabricator_import/conduit/response_spec.rb4
-rw-r--r--spec/lib/gitlab/phabricator_import/conduit/tasks_response_spec.rb2
-rw-r--r--spec/lib/gitlab/phabricator_import/conduit/users_response_spec.rb2
-rw-r--r--spec/lib/gitlab/phabricator_import/issues/importer_spec.rb2
-rw-r--r--spec/lib/gitlab/project_search_results_spec.rb113
-rw-r--r--spec/lib/gitlab/prometheus_client_spec.rb4
-rw-r--r--spec/lib/gitlab/regex_spec.rb33
-rw-r--r--spec/lib/gitlab/repository_url_builder_spec.rb2
-rw-r--r--spec/lib/gitlab/request_context_spec.rb4
-rw-r--r--spec/lib/gitlab/runtime_spec.rb13
-rw-r--r--spec/lib/gitlab/search_results_spec.rb10
-rw-r--r--spec/lib/gitlab/sidekiq_config/cli_methods_spec.rb8
-rw-r--r--spec/lib/gitlab/sidekiq_logging/json_formatter_spec.rb27
-rw-r--r--spec/lib/gitlab/sidekiq_logging/structured_logger_spec.rb25
-rw-r--r--spec/lib/gitlab/sidekiq_middleware/duplicate_jobs/duplicate_job_spec.rb19
-rw-r--r--spec/lib/gitlab/sidekiq_middleware/extra_done_log_metadata_spec.rb35
-rw-r--r--spec/lib/gitlab/sidekiq_middleware_spec.rb24
-rw-r--r--spec/lib/gitlab/snippet_search_results_spec.rb29
-rw-r--r--spec/lib/gitlab/static_site_editor/config_spec.rb28
-rw-r--r--spec/lib/gitlab/throttle_spec.rb78
-rw-r--r--spec/lib/gitlab/tracking_spec.rb8
-rw-r--r--spec/lib/gitlab/tree_summary_spec.rb16
-rw-r--r--spec/lib/gitlab/url_builder_spec.rb13
-rw-r--r--spec/lib/gitlab/usage_data_counters/designs_counter_spec.rb14
-rw-r--r--spec/lib/gitlab/usage_data_counters/web_ide_counter_spec.rb47
-rw-r--r--spec/lib/gitlab/usage_data_spec.rb344
-rw-r--r--spec/lib/gitlab/user_access_snippet_spec.rb52
-rw-r--r--spec/lib/gitlab/utils/measuring_spec.rb40
-rw-r--r--spec/lib/gitlab/utils_spec.rb34
-rw-r--r--spec/lib/gitlab/view/presenter/factory_spec.rb6
-rw-r--r--spec/lib/gitlab/wiki_pages/front_matter_parser_spec.rb6
-rw-r--r--spec/lib/gitlab/with_request_store_spec.rb30
-rw-r--r--spec/lib/gitlab/workhorse_spec.rb88
-rw-r--r--spec/lib/gitlab/x509/signature_spec.rb160
-rw-r--r--spec/lib/gitlab/x509/tag_spec.rb42
-rw-r--r--spec/lib/gitlab_danger_spec.rb2
-rw-r--r--spec/lib/google_api/auth_spec.rb14
-rw-r--r--spec/lib/grafana/validator_spec.rb4
-rw-r--r--spec/lib/omni_auth/strategies/jwt_spec.rb4
-rw-r--r--spec/lib/quality/helm_client_spec.rb132
-rw-r--r--spec/lib/quality/test_level_spec.rb4
-rw-r--r--spec/lib/rspec_flaky/flaky_example_spec.rb2
-rw-r--r--spec/lib/rspec_flaky/report_spec.rb6
-rw-r--r--spec/lib/sentry/client/event_spec.rb2
-rw-r--r--spec/lib/sentry/client/issue_link_spec.rb4
-rw-r--r--spec/lib/sentry/client/issue_spec.rb4
-rw-r--r--spec/lib/sentry/client/projects_spec.rb2
-rw-r--r--spec/lib/sentry/client/repo_spec.rb2
-rw-r--r--spec/lib/serializers/json_spec.rb2
-rw-r--r--spec/lib/system_check/app/hashed_storage_all_projects_check_spec.rb24
-rw-r--r--spec/lib/system_check/app/hashed_storage_enabled_check_spec.rb24
-rw-r--r--spec/lib/system_check/simple_executor_spec.rb140
-rw-r--r--spec/lib/system_check_spec.rb21
-rw-r--r--spec/mailers/emails/groups_spec.rb41
-rw-r--r--spec/mailers/emails/profile_spec.rb40
-rw-r--r--spec/mailers/notify_spec.rb43
-rw-r--r--spec/migrations/20200511145545_change_variable_interpolation_format_in_common_metrics_spec.rb34
-rw-r--r--spec/migrations/backfill_snippet_repositories_spec.rb44
-rw-r--r--spec/migrations/cleanup_optimistic_locking_nulls_pt2_fixed_spec.rb45
-rw-r--r--spec/migrations/cleanup_optimistic_locking_nulls_spec.rb9
-rw-r--r--spec/migrations/cleanup_projects_with_missing_namespace_spec.rb134
-rw-r--r--spec/migrations/encrypt_plaintext_attributes_on_application_settings_spec.rb10
-rw-r--r--spec/migrations/fill_file_store_ci_job_artifacts_spec.rb44
-rw-r--r--spec/migrations/fill_file_store_lfs_objects_spec.rb36
-rw-r--r--spec/migrations/fill_store_uploads_spec.rb48
-rw-r--r--spec/migrations/remove_additional_application_settings_rows_spec.rb27
-rw-r--r--spec/migrations/remove_deprecated_jenkins_service_records_spec.rb28
-rw-r--r--spec/migrations/remove_orphaned_invited_members_spec.rb55
-rw-r--r--spec/models/ability_spec.rb46
-rw-r--r--spec/models/alert_management/alert_spec.rb320
-rw-r--r--spec/models/application_setting_spec.rb14
-rw-r--r--spec/models/blob_spec.rb512
-rw-r--r--spec/models/blob_viewer/readme_spec.rb2
-rw-r--r--spec/models/broadcast_message_spec.rb18
-rw-r--r--spec/models/ci/build_spec.rb353
-rw-r--r--spec/models/ci/daily_build_group_report_result_spec.rb59
-rw-r--r--spec/models/ci/daily_report_result_spec.rb62
-rw-r--r--spec/models/ci/freeze_period_spec.rb50
-rw-r--r--spec/models/ci/freeze_period_status_spec.rb62
-rw-r--r--spec/models/ci/instance_variable_spec.rb93
-rw-r--r--spec/models/ci/job_artifact_spec.rb120
-rw-r--r--spec/models/ci/persistent_ref_spec.rb12
-rw-r--r--spec/models/ci/pipeline_schedule_spec.rb8
-rw-r--r--spec/models/ci/pipeline_spec.rb145
-rw-r--r--spec/models/ci/processable_spec.rb159
-rw-r--r--spec/models/ci/runner_spec.rb8
-rw-r--r--spec/models/ci/stage_spec.rb26
-rw-r--r--spec/models/clusters/applications/elastic_stack_spec.rb70
-rw-r--r--spec/models/clusters/applications/fluentd_spec.rb36
-rw-r--r--spec/models/clusters/applications/ingress_spec.rb6
-rw-r--r--spec/models/clusters/applications/jupyter_spec.rb4
-rw-r--r--spec/models/clusters/cluster_spec.rb98
-rw-r--r--spec/models/commit_status_spec.rb44
-rw-r--r--spec/models/concerns/awardable_spec.rb41
-rw-r--r--spec/models/concerns/blocks_json_serialization_spec.rb7
-rw-r--r--spec/models/concerns/cache_markdown_field_spec.rb52
-rw-r--r--spec/models/concerns/cacheable_attributes_spec.rb4
-rw-r--r--spec/models/concerns/has_user_type_spec.rb86
-rw-r--r--spec/models/concerns/mentionable_spec.rb52
-rw-r--r--spec/models/concerns/noteable_spec.rb2
-rw-r--r--spec/models/concerns/reactive_caching_spec.rb100
-rw-r--r--spec/models/concerns/redis_cacheable_spec.rb6
-rw-r--r--spec/models/concerns/spammable_spec.rb91
-rw-r--r--spec/models/container_repository_spec.rb12
-rw-r--r--spec/models/cycle_analytics/code_spec.rb2
-rw-r--r--spec/models/cycle_analytics/group_level_spec.rb44
-rw-r--r--spec/models/cycle_analytics/issue_spec.rb2
-rw-r--r--spec/models/cycle_analytics/plan_spec.rb2
-rw-r--r--spec/models/cycle_analytics/production_spec.rb2
-rw-r--r--spec/models/cycle_analytics/project_level_spec.rb2
-rw-r--r--spec/models/cycle_analytics/review_spec.rb2
-rw-r--r--spec/models/cycle_analytics/staging_spec.rb2
-rw-r--r--spec/models/cycle_analytics/test_spec.rb2
-rw-r--r--spec/models/deploy_token_spec.rb4
-rw-r--r--spec/models/design_management/action_spec.rb105
-rw-r--r--spec/models/design_management/design_action_spec.rb98
-rw-r--r--spec/models/design_management/design_at_version_spec.rb426
-rw-r--r--spec/models/design_management/design_collection_spec.rb82
-rw-r--r--spec/models/design_management/design_spec.rb575
-rw-r--r--spec/models/design_management/repository_spec.rb58
-rw-r--r--spec/models/design_management/version_spec.rb342
-rw-r--r--spec/models/design_user_mention_spec.rb12
-rw-r--r--spec/models/diff_note_spec.rb18
-rw-r--r--spec/models/email_spec.rb20
-rw-r--r--spec/models/environment_spec.rb21
-rw-r--r--spec/models/event_spec.rb171
-rw-r--r--spec/models/group_spec.rb168
-rw-r--r--spec/models/hooks/project_hook_spec.rb4
-rw-r--r--spec/models/issue_spec.rb132
-rw-r--r--spec/models/iteration_spec.rb170
-rw-r--r--spec/models/jira_import_state_spec.rb1
-rw-r--r--spec/models/member_spec.rb22
-rw-r--r--spec/models/merge_request_diff_spec.rb59
-rw-r--r--spec/models/merge_request_spec.rb168
-rw-r--r--spec/models/metrics/users_starred_dashboard_spec.rb39
-rw-r--r--spec/models/milestone_note_spec.rb10
-rw-r--r--spec/models/milestone_spec.rb163
-rw-r--r--spec/models/namespace/root_storage_size_spec.rb67
-rw-r--r--spec/models/note_spec.rb40
-rw-r--r--spec/models/pages_domain_spec.rb6
-rw-r--r--spec/models/performance_monitoring/prometheus_dashboard_spec.rb102
-rw-r--r--spec/models/performance_monitoring/prometheus_metric_spec.rb59
-rw-r--r--spec/models/performance_monitoring/prometheus_panel_group_spec.rb54
-rw-r--r--spec/models/performance_monitoring/prometheus_panel_spec.rb77
-rw-r--r--spec/models/personal_access_token_spec.rb23
-rw-r--r--spec/models/personal_snippet_spec.rb1
-rw-r--r--spec/models/plan_limits_spec.rb74
-rw-r--r--spec/models/plan_spec.rb17
-rw-r--r--spec/models/project_ci_cd_setting_spec.rb12
-rw-r--r--spec/models/project_feature_spec.rb74
-rw-r--r--spec/models/project_repository_storage_move_spec.rb63
-rw-r--r--spec/models/project_services/chat_message/pipeline_message_spec.rb605
-rw-r--r--spec/models/project_services/irker_service_spec.rb2
-rw-r--r--spec/models/project_services/jira_service_spec.rb93
-rw-r--r--spec/models/project_services/mattermost_slash_commands_service_spec.rb7
-rw-r--r--spec/models/project_services/microsoft_teams_service_spec.rb2
-rw-r--r--spec/models/project_services/webex_teams_service_spec.rb10
-rw-r--r--spec/models/project_snippet_spec.rb1
-rw-r--r--spec/models/project_spec.rb279
-rw-r--r--spec/models/project_wiki_spec.rb453
-rw-r--r--spec/models/release_spec.rb52
-rw-r--r--spec/models/remote_mirror_spec.rb52
-rw-r--r--spec/models/repository_spec.rb76
-rw-r--r--spec/models/resource_label_event_spec.rb3
-rw-r--r--spec/models/resource_milestone_event_spec.rb17
-rw-r--r--spec/models/resource_state_event_spec.rb14
-rw-r--r--spec/models/sent_notification_spec.rb22
-rw-r--r--spec/models/service_spec.rb32
-rw-r--r--spec/models/snippet_repository_spec.rb32
-rw-r--r--spec/models/snippet_spec.rb54
-rw-r--r--spec/models/spam_log_spec.rb27
-rw-r--r--spec/models/state_note_spec.rb29
-rw-r--r--spec/models/timelog_spec.rb6
-rw-r--r--spec/models/todo_spec.rb32
-rw-r--r--spec/models/tree_spec.rb21
-rw-r--r--spec/models/user_spec.rb165
-rw-r--r--spec/models/user_type_enums_spec.rb13
-rw-r--r--spec/models/wiki_page/meta_spec.rb87
-rw-r--r--spec/models/wiki_page_spec.rb161
-rw-r--r--spec/models/x509_commit_signature_spec.rb32
-rw-r--r--spec/policies/alert_management/alert_policy_spec.rb25
-rw-r--r--spec/policies/base_policy_spec.rb2
-rw-r--r--spec/policies/blob_policy_spec.rb2
-rw-r--r--spec/policies/ci/build_policy_spec.rb18
-rw-r--r--spec/policies/clusters/cluster_policy_spec.rb11
-rw-r--r--spec/policies/clusters/instance_policy_spec.rb20
-rw-r--r--spec/policies/deploy_key_policy_spec.rb18
-rw-r--r--spec/policies/design_management/design_policy_spec.rb181
-rw-r--r--spec/policies/environment_policy_spec.rb32
-rw-r--r--spec/policies/global_policy_spec.rb78
-rw-r--r--spec/policies/group_policy_spec.rb30
-rw-r--r--spec/policies/issue_policy_spec.rb22
-rw-r--r--spec/policies/merge_request_policy_spec.rb6
-rw-r--r--spec/policies/namespace_policy_spec.rb8
-rw-r--r--spec/policies/note_policy_spec.rb12
-rw-r--r--spec/policies/personal_snippet_policy_spec.rb10
-rw-r--r--spec/policies/project_policy_spec.rb369
-rw-r--r--spec/policies/project_snippet_policy_spec.rb15
-rw-r--r--spec/policies/user_policy_spec.rb8
-rw-r--r--spec/policies/wiki_page_policy_spec.rb2
-rw-r--r--spec/presenters/ci/build_presenter_spec.rb26
-rw-r--r--spec/presenters/ci/build_runner_presenter_spec.rb142
-rw-r--r--spec/presenters/clusterable_presenter_spec.rb16
-rw-r--r--spec/presenters/pages_domain_presenter_spec.rb8
-rw-r--r--spec/presenters/projects/prometheus/alert_presenter_spec.rb142
-rw-r--r--spec/requests/api/admin/ci/variables_spec.rb210
-rw-r--r--spec/requests/api/appearance_spec.rb5
-rw-r--r--spec/requests/api/branches_spec.rb9
-rw-r--r--spec/requests/api/deployments_spec.rb2
-rw-r--r--spec/requests/api/features_spec.rb36
-rw-r--r--spec/requests/api/freeze_periods_spec.rb475
-rw-r--r--spec/requests/api/graphql/boards/board_lists_query_spec.rb137
-rw-r--r--spec/requests/api/graphql/current_user/todos_query_spec.rb7
-rw-r--r--spec/requests/api/graphql/gitlab_schema_spec.rb2
-rw-r--r--spec/requests/api/graphql/group/milestones_spec.rb48
-rw-r--r--spec/requests/api/graphql/metrics/dashboard/annotations_spec.rb72
-rw-r--r--spec/requests/api/graphql/mutations/alert_management/alerts/update_alert_status_spec.rb42
-rw-r--r--spec/requests/api/graphql/mutations/award_emojis/add_spec.rb2
-rw-r--r--spec/requests/api/graphql/mutations/award_emojis/remove_spec.rb2
-rw-r--r--spec/requests/api/graphql/mutations/award_emojis/toggle_spec.rb2
-rw-r--r--spec/requests/api/graphql/mutations/branches/create_spec.rb45
-rw-r--r--spec/requests/api/graphql/mutations/design_management/delete_spec.rb127
-rw-r--r--spec/requests/api/graphql/mutations/design_management/upload_spec.rb99
-rw-r--r--spec/requests/api/graphql/mutations/jira_import/start_spec.rb5
-rw-r--r--spec/requests/api/graphql/mutations/metrics/dashboard/annotations/create_spec.rb231
-rw-r--r--spec/requests/api/graphql/mutations/snippets/create_spec.rb52
-rw-r--r--spec/requests/api/graphql/mutations/snippets/destroy_spec.rb13
-rw-r--r--spec/requests/api/graphql/mutations/snippets/mark_as_spam_spec.rb8
-rw-r--r--spec/requests/api/graphql/mutations/snippets/update_spec.rb25
-rw-r--r--spec/requests/api/graphql/project/alert_management/alert_status_counts_spec.rb61
-rw-r--r--spec/requests/api/graphql/project/alert_management/alerts_spec.rb139
-rw-r--r--spec/requests/api/graphql/project/grafana_integration_spec.rb10
-rw-r--r--spec/requests/api/graphql/project/issue/design_collection/version_spec.rb216
-rw-r--r--spec/requests/api/graphql/project/issue/design_collection/versions_spec.rb113
-rw-r--r--spec/requests/api/graphql/project/issue/designs/designs_spec.rb388
-rw-r--r--spec/requests/api/graphql/project/issue/designs/notes_spec.rb70
-rw-r--r--spec/requests/api/graphql/project/issue_spec.rb189
-rw-r--r--spec/requests/api/graphql/project/issues_spec.rb209
-rw-r--r--spec/requests/api/graphql/project/jira_import_spec.rb3
-rw-r--r--spec/requests/api/graphql/query_spec.rb95
-rw-r--r--spec/requests/api/graphql_spec.rb2
-rw-r--r--spec/requests/api/groups_spec.rb271
-rw-r--r--spec/requests/api/helpers_spec.rb4
-rw-r--r--spec/requests/api/internal/base_spec.rb95
-rw-r--r--spec/requests/api/issues/get_group_issues_spec.rb51
-rw-r--r--spec/requests/api/issues/issues_spec.rb26
-rw-r--r--spec/requests/api/issues/post_projects_issues_spec.rb2
-rw-r--r--spec/requests/api/issues/put_projects_issues_spec.rb9
-rw-r--r--spec/requests/api/merge_requests_spec.rb68
-rw-r--r--spec/requests/api/metrics/dashboard/annotations_spec.rb140
-rw-r--r--spec/requests/api/metrics/user_starred_dashboards_spec.rb164
-rw-r--r--spec/requests/api/pipeline_schedules_spec.rb2
-rw-r--r--spec/requests/api/pipelines_spec.rb99
-rw-r--r--spec/requests/api/project_export_spec.rb4
-rw-r--r--spec/requests/api/project_milestones_spec.rb8
-rw-r--r--spec/requests/api/project_repository_storage_moves_spec.rb89
-rw-r--r--spec/requests/api/project_snippets_spec.rb98
-rw-r--r--spec/requests/api/project_statistics_spec.rb30
-rw-r--r--spec/requests/api/project_templates_spec.rb28
-rw-r--r--spec/requests/api/projects_spec.rb6
-rw-r--r--spec/requests/api/remote_mirrors_spec.rb23
-rw-r--r--spec/requests/api/runner_spec.rb58
-rw-r--r--spec/requests/api/runners_spec.rb93
-rw-r--r--spec/requests/api/search_spec.rb252
-rw-r--r--spec/requests/api/settings_spec.rb14
-rw-r--r--spec/requests/api/snippets_spec.rb136
-rw-r--r--spec/requests/api/statistics_spec.rb4
-rw-r--r--spec/requests/api/terraform/state_spec.rb16
-rw-r--r--spec/requests/api/todos_spec.rb41
-rw-r--r--spec/requests/api/users_spec.rb6
-rw-r--r--spec/requests/api/wikis_spec.rb4
-rw-r--r--spec/requests/jwt_controller_spec.rb42
-rw-r--r--spec/requests/rack_attack_global_spec.rb57
-rw-r--r--spec/requests/user_activity_spec.rb4
-rw-r--r--spec/routing/admin_routing_spec.rb7
-rw-r--r--spec/routing/project_routing_spec.rb55
-rw-r--r--spec/routing/routing_spec.rb24
-rw-r--r--spec/rubocop/cop/avoid_keyword_arguments_in_sidekiq_workers_spec.rb49
-rw-r--r--spec/rubocop/cop/gitlab/change_timezone_spec.rb21
-rw-r--r--spec/rubocop/cop/gitlab/json_spec.rb39
-rw-r--r--spec/rubocop/cop/inject_enterprise_edition_module_spec.rb11
-rw-r--r--spec/rubocop/cop/migration/add_column_with_default_spec.rb45
-rw-r--r--spec/rubocop/cop/migration/add_columns_to_wide_tables_spec.rb4
-rw-r--r--spec/rubocop/cop/migration/add_concurrent_foreign_key_spec.rb6
-rw-r--r--spec/rubocop/cop/migration/add_limit_to_string_columns_spec.rb268
-rw-r--r--spec/rubocop/cop/migration/add_limit_to_text_columns_spec.rb160
-rw-r--r--spec/rubocop/cop/migration/prevent_strings_spec.rb143
-rw-r--r--spec/rubocop/cop/migration/reversible_add_column_with_default_spec.rb43
-rw-r--r--spec/rubocop/cop/migration/with_lock_retries_disallowed_method_spec.rb68
-rw-r--r--spec/rubocop/cop/migration/with_lock_retries_without_ddl_transaction_spec.rb46
-rw-r--r--spec/rubocop/cop/performance/ar_exists_and_present_blank_spec.rb111
-rw-r--r--spec/rubocop/cop/rspec/empty_line_after_shared_example_spec.rb86
-rw-r--r--spec/rubocop/cop/rspec/env_assignment_spec.rb12
-rw-r--r--spec/serializers/accessibility_error_entity_spec.rb37
-rw-r--r--spec/serializers/accessibility_reports_comparer_entity_spec.rb87
-rw-r--r--spec/serializers/accessibility_reports_comparer_serializer_spec.rb65
-rw-r--r--spec/serializers/ci/dag_job_entity_spec.rb43
-rw-r--r--spec/serializers/ci/dag_job_group_entity_spec.rb58
-rw-r--r--spec/serializers/ci/dag_pipeline_entity_spec.rb112
-rw-r--r--spec/serializers/ci/dag_pipeline_serializer_spec.rb17
-rw-r--r--spec/serializers/ci/dag_stage_entity_spec.rb31
-rw-r--r--spec/serializers/cluster_application_entity_spec.rb12
-rw-r--r--spec/serializers/cluster_entity_spec.rb6
-rw-r--r--spec/serializers/cluster_serializer_spec.rb38
-rw-r--r--spec/serializers/diff_file_base_entity_spec.rb58
-rw-r--r--spec/serializers/diffs_metadata_entity_spec.rb2
-rw-r--r--spec/serializers/environment_entity_spec.rb50
-rw-r--r--spec/serializers/merge_request_poll_widget_entity_spec.rb44
-rw-r--r--spec/serializers/merge_request_sidebar_basic_entity_spec.rb2
-rw-r--r--spec/serializers/service_event_entity_spec.rb41
-rw-r--r--spec/serializers/test_case_entity_spec.rb4
-rw-r--r--spec/serializers/test_suite_entity_spec.rb50
-rw-r--r--spec/services/alert_management/create_alert_issue_service_spec.rb152
-rw-r--r--spec/services/alert_management/process_prometheus_alert_service_spec.rb136
-rw-r--r--spec/services/alert_management/update_alert_status_service_spec.rb66
-rw-r--r--spec/services/application_settings/update_service_spec.rb2
-rw-r--r--spec/services/auth/container_registry_authentication_service_spec.rb20
-rw-r--r--spec/services/authorized_project_update/project_create_service_spec.rb142
-rw-r--r--spec/services/base_container_service_spec.rb23
-rw-r--r--spec/services/boards/issues/list_service_spec.rb2
-rw-r--r--spec/services/branches/create_service_spec.rb30
-rw-r--r--spec/services/ci/compare_accessibility_reports_service_spec.rb62
-rw-r--r--spec/services/ci/compare_test_reports_service_spec.rb7
-rw-r--r--spec/services/ci/create_job_artifacts_service_spec.rb67
-rw-r--r--spec/services/ci/create_pipeline_service/custom_config_content_spec.rb4
-rw-r--r--spec/services/ci/daily_build_group_report_result_service_spec.rb158
-rw-r--r--spec/services/ci/daily_report_result_service_spec.rb161
-rw-r--r--spec/services/ci/destroy_expired_job_artifacts_service_spec.rb26
-rw-r--r--spec/services/ci/generate_terraform_reports_service_spec.rb71
-rw-r--r--spec/services/ci/pipeline_processing/atomic_processing_service/status_collection_spec.rb15
-rw-r--r--spec/services/ci/pipeline_processing/atomic_processing_service_spec.rb6
-rw-r--r--spec/services/ci/pipeline_processing/legacy_processing_service_spec.rb19
-rw-r--r--spec/services/ci/pipeline_processing/shared_processing_service.rb25
-rw-r--r--spec/services/ci/pipeline_processing/shared_processing_service_tests_with_yaml.rb57
-rw-r--r--spec/services/ci/pipeline_processing/test_cases/dag_build_allow_failure_test_on_failure.yml47
-rw-r--r--spec/services/ci/pipeline_processing/test_cases/dag_build_fails.yml39
-rw-r--r--spec/services/ci/pipeline_processing/test_cases/dag_build_fails_deploy_needs_test.yml39
-rw-r--r--spec/services/ci/pipeline_processing/test_cases/dag_build_fails_deploy_needs_test_when_always.yml43
-rw-r--r--spec/services/ci/pipeline_processing/test_cases/dag_build_fails_other_build_succeeds.yml62
-rw-r--r--spec/services/ci/pipeline_processing/test_cases/dag_build_fails_other_build_succeeds_deploy_always.yml63
-rw-r--r--spec/services/ci/pipeline_processing/test_cases/dag_build_fails_test_allow_failure.yml40
-rw-r--r--spec/services/ci/pipeline_processing/test_cases/dag_build_fails_test_always.yml35
-rw-r--r--spec/services/ci/pipeline_processing/test_cases/dag_build_fails_test_on_failure.yml35
-rw-r--r--spec/services/ci/pipeline_processing/test_cases/dag_build_succeeds_test_on_failure.yml35
-rw-r--r--spec/services/ci/pipeline_processing/test_cases/dag_builds_succeed_test_on_failure.yml63
-rw-r--r--spec/services/ci/pipeline_processing/test_cases/dag_builds_succeed_test_on_failure_deploy_always.yml64
-rw-r--r--spec/services/ci/pipeline_processing/test_cases/dag_test_allow_failure_true.yml43
-rw-r--r--spec/services/ci/pipeline_processing/test_cases/dag_test_manual_allow_failure_false.yml66
-rw-r--r--spec/services/ci/pipeline_processing/test_cases/dag_test_manual_allow_failure_true.yml58
-rw-r--r--spec/services/ci/pipeline_processing/test_cases/dag_test_manual_allow_failure_true_deploy_always.yml27
-rw-r--r--spec/services/ci/pipeline_processing/test_cases/dag_test_manual_allow_failure_true_deploy_on_failure.yml48
-rw-r--r--spec/services/ci/pipeline_processing/test_cases/dag_test_manual_allow_failure_true_other_test_succeeds.yml42
-rw-r--r--spec/services/ci/pipeline_processing/test_cases/dag_test_on_failure_with_failure.yml66
-rw-r--r--spec/services/ci/pipeline_processing/test_cases/dag_test_on_failure_with_success.yml40
-rw-r--r--spec/services/ci/pipeline_processing/test_cases/stage_build_allow_failure_test_on_failure.yml53
-rw-r--r--spec/services/ci/pipeline_processing/test_cases/stage_build_fails.yml38
-rw-r--r--spec/services/ci/pipeline_processing/test_cases/stage_build_fails_test_allow_failure.yml39
-rw-r--r--spec/services/ci/pipeline_processing/test_cases/stage_test_manual_allow_failure_false.yml65
-rw-r--r--spec/services/ci/pipeline_processing/test_cases/stage_test_manual_allow_failure_true.yml54
-rw-r--r--spec/services/ci/pipeline_processing/test_cases/stage_test_manual_allow_failure_true_deploy_on_failure.yml44
-rw-r--r--spec/services/ci/pipeline_processing/test_cases/stage_test_on_failure_with_failure.yml52
-rw-r--r--spec/services/ci/pipeline_processing/test_cases/stage_test_on_failure_with_success.yml52
-rw-r--r--spec/services/ci/pipeline_schedule_service_spec.rb32
-rw-r--r--spec/services/ci/process_pipeline_service_spec.rb19
-rw-r--r--spec/services/ci/register_job_service_spec.rb2
-rw-r--r--spec/services/ci/retry_build_service_spec.rb50
-rw-r--r--spec/services/ci/retry_pipeline_service_spec.rb19
-rw-r--r--spec/services/ci/update_instance_variables_service_spec.rb230
-rw-r--r--spec/services/clusters/applications/check_upgrade_progress_service_spec.rb4
-rw-r--r--spec/services/clusters/applications/ingress_modsecurity_usage_service_spec.rb196
-rw-r--r--spec/services/clusters/applications/schedule_update_service_spec.rb6
-rw-r--r--spec/services/clusters/gcp/finalize_creation_service_spec.rb3
-rw-r--r--spec/services/clusters/kubernetes/configure_istio_ingress_service_spec.rb4
-rw-r--r--spec/services/clusters/kubernetes/create_or_update_namespace_service_spec.rb1
-rw-r--r--spec/services/clusters/kubernetes/create_or_update_service_account_service_spec.rb12
-rw-r--r--spec/services/clusters/parse_cluster_applications_artifact_service_spec.rb200
-rw-r--r--spec/services/cohorts_service_spec.rb2
-rw-r--r--spec/services/deployments/older_deployments_drop_service_spec.rb37
-rw-r--r--spec/services/design_management/delete_designs_service_spec.rb195
-rw-r--r--spec/services/design_management/design_user_notes_count_service_spec.rb43
-rw-r--r--spec/services/design_management/generate_image_versions_service_spec.rb77
-rw-r--r--spec/services/design_management/save_designs_service_spec.rb356
-rw-r--r--spec/services/emails/confirm_service_spec.rb6
-rw-r--r--spec/services/event_create_service_spec.rb13
-rw-r--r--spec/services/git/branch_push_service_spec.rb10
-rw-r--r--spec/services/git/wiki_push_service/change_spec.rb109
-rw-r--r--spec/services/git/wiki_push_service_spec.rb338
-rw-r--r--spec/services/grafana/proxy_service_spec.rb2
-rw-r--r--spec/services/groups/create_service_spec.rb21
-rw-r--r--spec/services/groups/import_export/export_service_spec.rb40
-rw-r--r--spec/services/groups/import_export/import_service_spec.rb254
-rw-r--r--spec/services/groups/update_service_spec.rb20
-rw-r--r--spec/services/incident_management/create_issue_service_spec.rb24
-rw-r--r--spec/services/issuable/clone/attributes_rewriter_spec.rb28
-rw-r--r--spec/services/issues/close_service_spec.rb2
-rw-r--r--spec/services/issues/create_service_spec.rb86
-rw-r--r--spec/services/issues/related_branches_service_spec.rb102
-rw-r--r--spec/services/issues/resolve_discussions_spec.rb19
-rw-r--r--spec/services/issues/update_service_spec.rb30
-rw-r--r--spec/services/jira_import/start_import_service_spec.rb122
-rw-r--r--spec/services/lfs/file_transformer_spec.rb17
-rw-r--r--spec/services/merge_requests/create_service_spec.rb13
-rw-r--r--spec/services/merge_requests/merge_service_spec.rb2
-rw-r--r--spec/services/merge_requests/rebase_service_spec.rb19
-rw-r--r--spec/services/merge_requests/refresh_service_spec.rb40
-rw-r--r--spec/services/merge_requests/squash_service_spec.rb40
-rw-r--r--spec/services/merge_requests/update_service_spec.rb3
-rw-r--r--spec/services/metrics/dashboard/clone_dashboard_service_spec.rb4
-rw-r--r--spec/services/metrics/dashboard/grafana_metric_embed_service_spec.rb4
-rw-r--r--spec/services/metrics/dashboard/transient_embed_service_spec.rb6
-rw-r--r--spec/services/metrics/users_starred_dashboards/create_service_spec.rb72
-rw-r--r--spec/services/metrics/users_starred_dashboards/delete_service_spec.rb41
-rw-r--r--spec/services/namespaces/check_storage_size_service_spec.rb159
-rw-r--r--spec/services/note_summary_spec.rb6
-rw-r--r--spec/services/notes/create_service_spec.rb56
-rw-r--r--spec/services/notes/post_process_service_spec.rb27
-rw-r--r--spec/services/notification_service_spec.rb66
-rw-r--r--spec/services/pages_domains/obtain_lets_encrypt_certificate_service_spec.rb2
-rw-r--r--spec/services/pod_logs/base_service_spec.rb30
-rw-r--r--spec/services/pod_logs/elasticsearch_service_spec.rb32
-rw-r--r--spec/services/pod_logs/kubernetes_service_spec.rb20
-rw-r--r--spec/services/post_receive_service_spec.rb35
-rw-r--r--spec/services/projects/alerting/notify_service_spec.rb96
-rw-r--r--spec/services/projects/create_service_spec.rb98
-rw-r--r--spec/services/projects/fork_service_spec.rb8
-rw-r--r--spec/services/projects/hashed_storage/base_attachment_service_spec.rb2
-rw-r--r--spec/services/projects/hashed_storage/migrate_repository_service_spec.rb10
-rw-r--r--spec/services/projects/hashed_storage/rollback_repository_service_spec.rb8
-rw-r--r--spec/services/projects/import_export/export_service_spec.rb28
-rw-r--r--spec/services/projects/import_service_spec.rb22
-rw-r--r--spec/services/projects/prometheus/alerts/create_events_service_spec.rb6
-rw-r--r--spec/services/projects/prometheus/alerts/notify_service_spec.rb33
-rw-r--r--spec/services/projects/propagate_service_template_spec.rb36
-rw-r--r--spec/services/projects/transfer_service_spec.rb271
-rw-r--r--spec/services/projects/update_remote_mirror_service_spec.rb37
-rw-r--r--spec/services/projects/update_repository_storage_service_spec.rb46
-rw-r--r--spec/services/prometheus/proxy_service_spec.rb2
-rw-r--r--spec/services/prometheus/proxy_variable_substitution_service_spec.rb156
-rw-r--r--spec/services/quick_actions/interpret_service_spec.rb4
-rw-r--r--spec/services/releases/create_service_spec.rb3
-rw-r--r--spec/services/repository_archive_clean_up_service_spec.rb2
-rw-r--r--spec/services/resource_access_tokens/create_service_spec.rb163
-rw-r--r--spec/services/resource_access_tokens/revoke_service_spec.rb111
-rw-r--r--spec/services/resource_events/change_milestone_service_spec.rb10
-rw-r--r--spec/services/resource_events/merge_into_notes_service_spec.rb2
-rw-r--r--spec/services/resources/create_access_token_service_spec.rb163
-rw-r--r--spec/services/search/snippet_service_spec.rb50
-rw-r--r--spec/services/search_service_spec.rb86
-rw-r--r--spec/services/snippets/create_service_spec.rb154
-rw-r--r--spec/services/snippets/update_service_spec.rb90
-rw-r--r--spec/services/spam/spam_action_service_spec.rb215
-rw-r--r--spec/services/spam/spam_check_service_spec.rb170
-rw-r--r--spec/services/spam/spam_verdict_service_spec.rb65
-rw-r--r--spec/services/system_note_service_spec.rb28
-rw-r--r--spec/services/system_notes/design_management_service_spec.rb155
-rw-r--r--spec/services/template_engines/liquid_service_spec.rb126
-rw-r--r--spec/services/todo_service_spec.rb30
-rw-r--r--spec/services/update_merge_request_metrics_service_spec.rb4
-rw-r--r--spec/services/user_project_access_changed_service_spec.rb9
-rw-r--r--spec/services/users/destroy_service_spec.rb14
-rw-r--r--spec/services/users/migrate_to_ghost_user_service_spec.rb6
-rw-r--r--spec/services/verify_pages_domain_service_spec.rb2
-rw-r--r--spec/services/wiki_pages/base_service_spec.rb2
-rw-r--r--spec/services/wiki_pages/create_service_spec.rb93
-rw-r--r--spec/services/wiki_pages/destroy_service_spec.rb49
-rw-r--r--spec/services/wiki_pages/event_create_service_spec.rb87
-rw-r--r--spec/services/wiki_pages/update_service_spec.rb97
-rw-r--r--spec/services/wikis/create_attachment_service_spec.rb67
-rw-r--r--spec/spec_helper.rb52
-rw-r--r--spec/support/capybara.rb13
-rw-r--r--spec/support/cycle_analytics_helpers/test_generation.rb4
-rw-r--r--spec/support/database_cleaner.rb2
-rw-r--r--spec/support/helpers/admin_mode_helpers.rb3
-rw-r--r--spec/support/helpers/concurrent_helpers.rb40
-rw-r--r--spec/support/helpers/design_management_test_helpers.rb45
-rw-r--r--spec/support/helpers/exclusive_lease_helpers.rb4
-rw-r--r--spec/support/helpers/fake_blob_helpers.rb6
-rw-r--r--spec/support/helpers/graphql_helpers.rb9
-rw-r--r--spec/support/helpers/jira_service_helper.rb5
-rw-r--r--spec/support/helpers/kubernetes_helpers.rb112
-rw-r--r--spec/support/helpers/login_helpers.rb2
-rw-r--r--spec/support/helpers/query_recorder.rb4
-rw-r--r--spec/support/helpers/reactive_caching_helpers.rb11
-rw-r--r--spec/support/helpers/smime_helper.rb14
-rw-r--r--spec/support/helpers/stub_feature_flags.rb34
-rw-r--r--spec/support/helpers/stub_gitlab_calls.rb8
-rw-r--r--spec/support/helpers/stub_object_storage.rb2
-rw-r--r--spec/support/helpers/test_env.rb20
-rw-r--r--spec/support/helpers/usage_data_helpers.rb95
-rw-r--r--spec/support/helpers/wiki_helpers.rb7
-rw-r--r--spec/support/helpers/workhorse_helpers.rb2
-rw-r--r--spec/support/helpers/x509_helpers.rb137
-rw-r--r--spec/support/import_export/common_util.rb19
-rw-r--r--spec/support/import_export/configuration_helper.rb4
-rw-r--r--spec/support/kubeclient.rb10
-rw-r--r--spec/support/matchers/disallow_request_matchers.rb2
-rw-r--r--spec/support/matchers/graphql_matchers.rb10
-rw-r--r--spec/support/rails/test_case_patch.rb53
-rw-r--r--spec/support/redis/redis_shared_examples.rb2
-rw-r--r--spec/support/renameable_upload.rb15
-rw-r--r--spec/support/shared_contexts/cache_allowed_users_in_namespace_shared_context.rb31
-rw-r--r--spec/support/shared_contexts/design_management_shared_contexts.rb38
-rw-r--r--spec/support/shared_contexts/features/error_tracking_shared_context.rb4
-rw-r--r--spec/support/shared_contexts/issuable/merge_request_shared_context.rb49
-rw-r--r--spec/support/shared_contexts/issuable/project_shared_context.rb16
-rw-r--r--spec/support/shared_contexts/json_response_shared_context.rb2
-rw-r--r--spec/support/shared_contexts/lib/gitlab/import_export/project/rake_task_object_storage_shared_context.rb17
-rw-r--r--spec/support/shared_contexts/navbar_structure_context.rb2
-rw-r--r--spec/support/shared_contexts/policies/group_policy_shared_context.rb10
-rw-r--r--spec/support/shared_contexts/policies/project_policy_shared_context.rb3
-rw-r--r--spec/support/shared_contexts/project_service_shared_context.rb5
-rw-r--r--spec/support/shared_contexts/services_shared_context.rb3
-rw-r--r--spec/support/shared_contexts/spam_constants.rb7
-rw-r--r--spec/support/shared_examples/controllers/instance_statistics_controllers_shared_examples.rb20
-rw-r--r--spec/support/shared_examples/controllers/known_sign_in_shared_examples.rb59
-rw-r--r--spec/support/shared_examples/controllers/variables_shared_examples.rb25
-rw-r--r--spec/support/shared_examples/features/error_tracking_shared_example.rb14
-rw-r--r--spec/support/shared_examples/features/variable_list_shared_examples.rb3
-rw-r--r--spec/support/shared_examples/graphql/design_fields_shared_examples.rb80
-rw-r--r--spec/support/shared_examples/graphql/jira_import/jira_import_resolver_shared_examples.rb4
-rw-r--r--spec/support/shared_examples/graphql/sorted_paginated_query_shared_examples.rb92
-rw-r--r--spec/support/shared_examples/helm_commands_shared_examples.rb131
-rw-r--r--spec/support/shared_examples/lib/gitlab/cycle_analytics/base_stage_shared_examples.rb2
-rw-r--r--spec/support/shared_examples/lib/gitlab/helm_generated_script_shared_examples.rb15
-rw-r--r--spec/support/shared_examples/lib/gitlab/import_export/project/rake_task_object_storage_shared_examples.rb22
-rw-r--r--spec/support/shared_examples/lib/gitlab/migration_helpers_shared_examples.rb3
-rw-r--r--spec/support/shared_examples/models/chat_service_shared_examples.rb2
-rw-r--r--spec/support/shared_examples/models/cluster_application_status_shared_examples.rb61
-rw-r--r--spec/support/shared_examples/models/concerns/blob_replicator_strategy_shared_examples.rb4
-rw-r--r--spec/support/shared_examples/models/concerns/has_repository_shared_examples.rb20
-rw-r--r--spec/support/shared_examples/models/concerns/has_wiki_shared_examples.rb79
-rw-r--r--spec/support/shared_examples/models/concerns/limitable_shared_examples.rb36
-rw-r--r--spec/support/shared_examples/models/concerns/timebox_shared_examples.rb242
-rw-r--r--spec/support/shared_examples/models/diff_positionable_note_shared_examples.rb24
-rw-r--r--spec/support/shared_examples/models/email_format_shared_examples.rb41
-rw-r--r--spec/support/shared_examples/models/issuable_hook_data_shared_examples.rb1
-rw-r--r--spec/support/shared_examples/models/mentionable_shared_examples.rb4
-rw-r--r--spec/support/shared_examples/models/slack_mattermost_notifications_shared_examples.rb2
-rw-r--r--spec/support/shared_examples/models/wiki_shared_examples.rb423
-rw-r--r--spec/support/shared_examples/policies/project_policy_shared_examples.rb14
-rw-r--r--spec/support/shared_examples/policies/wiki_policies_shared_examples.rb228
-rw-r--r--spec/support/shared_examples/quick_actions/issuable/issuable_quick_actions_shared_examples.rb22
-rw-r--r--spec/support/shared_examples/requests/api/award_emoji_todo_shared_examples.rb2
-rw-r--r--spec/support/shared_examples/requests/api/graphql/group_and_project_boards_query_shared_examples.rb2
-rw-r--r--spec/support/shared_examples/requests/api/graphql/mutations/snippets_shared_examples.rb10
-rw-r--r--spec/support/shared_examples/requests/snippet_shared_examples.rb70
-rw-r--r--spec/support/shared_examples/requires_variables_shared_example.rb13
-rw-r--r--spec/support/shared_examples/resource_events.rb18
-rw-r--r--spec/support/shared_examples/serializers/diff_file_entity_shared_examples.rb2
-rw-r--r--spec/support/shared_examples/services/boards/lists_list_service_shared_examples.rb4
-rw-r--r--spec/support/shared_examples/services/measurable_service_shared_examples.rb32
-rw-r--r--spec/support/shared_examples/services/metrics/dashboard_shared_examples.rb4
-rw-r--r--spec/support/shared_examples/services/projects/update_repository_storage_service_shared_examples.rb26
-rw-r--r--spec/support/shared_examples/services/resource_events/change_milestone_service_shared_examples.rb7
-rw-r--r--spec/support/shared_examples/services/snippets_shared_examples.rb42
-rw-r--r--spec/support/shared_examples/services/wiki_pages/create_service_shared_examples.rb94
-rw-r--r--spec/support/shared_examples/services/wiki_pages/destroy_service_shared_examples.rb50
-rw-r--r--spec/support/shared_examples/services/wiki_pages/update_service_shared_examples.rb98
-rw-r--r--spec/support/shared_examples/services/wikis/create_attachment_service_shared_examples.rb62
-rw-r--r--spec/support/shared_examples/tasks/gitlab/import_export/measurable_shared_examples.rb31
-rw-r--r--spec/support/shared_examples/workers/authorized_projects_worker_shared_example.rb42
-rw-r--r--spec/support/shared_examples/workers/gitlab/jira_import/jira_import_workers_shared_examples.rb2
-rw-r--r--spec/support/shared_examples/workers/pages_domain_cron_worker_shared_examples.rb10
-rw-r--r--spec/support/shared_examples/workers/reactive_cacheable_shared_examples.rb59
-rw-r--r--spec/support/sidekiq.rb13
-rw-r--r--spec/support/sidekiq_middleware.rb13
-rw-r--r--spec/support/unicorn.rb27
-rw-r--r--spec/support/webmock.rb18
-rw-r--r--spec/support_specs/helpers/active_record/query_recorder_spec.rb8
-rw-r--r--spec/support_specs/helpers/stub_feature_flags_spec.rb130
-rw-r--r--spec/tasks/gitlab/artifacts/migrate_rake_spec.rb12
-rw-r--r--spec/tasks/gitlab/gitaly_rake_spec.rb10
-rw-r--r--spec/tasks/gitlab/snippets_rake_spec.rb114
-rw-r--r--spec/tasks/gitlab/task_helpers_spec.rb15
-rw-r--r--spec/tasks/gitlab/uploads/migrate_rake_spec.rb12
-rw-r--r--spec/tasks/gitlab/workhorse_rake_spec.rb2
-rw-r--r--spec/uploaders/content_type_whitelist_spec.rb18
-rw-r--r--spec/uploaders/design_management/design_v432x230_uploader_spec.rb86
-rw-r--r--spec/uploaders/records_uploads_spec.rb4
-rw-r--r--spec/uploaders/workers/object_storage/migrate_uploads_worker_spec.rb12
-rw-r--r--spec/validators/cron_freeze_period_timezone_validator_spec.rb24
-rw-r--r--spec/validators/cron_validator_spec.rb47
-rw-r--r--spec/views/admin/sessions/new.html.haml_spec.rb59
-rw-r--r--spec/views/admin/users/_user.html.haml_spec.rb12
-rw-r--r--spec/views/devise/sessions/new.html.haml_spec.rb4
-rw-r--r--spec/views/devise/shared/_signin_box.html.haml_spec.rb2
-rw-r--r--spec/views/help/index.html.haml_spec.rb12
-rw-r--r--spec/views/help/show.html.haml_spec.rb18
-rw-r--r--spec/views/layouts/nav/sidebar/_admin.html.haml_spec.rb9
-rw-r--r--spec/views/layouts/nav/sidebar/_project.html.haml_spec.rb62
-rw-r--r--spec/views/profiles/show.html.haml_spec.rb44
-rw-r--r--spec/views/projects/issues/_related_branches.html.haml_spec.rb24
-rw-r--r--spec/views/projects/issues/show.html.haml_spec.rb27
-rw-r--r--spec/views/projects/merge_requests/show.html.haml_spec.rb60
-rw-r--r--spec/views/projects/services/_form.haml_spec.rb17
-rw-r--r--spec/views/projects/settings/ci_cd/_autodevops_form.html.haml_spec.rb2
-rw-r--r--spec/workers/authorized_project_update/project_create_worker_spec.rb50
-rw-r--r--spec/workers/authorized_project_update/user_refresh_with_low_urgency_worker_spec.rb11
-rw-r--r--spec/workers/authorized_projects_worker_spec.rb37
-rw-r--r--spec/workers/ci/daily_build_group_report_results_worker_spec.rb34
-rw-r--r--spec/workers/ci/daily_report_results_worker_spec.rb34
-rw-r--r--spec/workers/concerns/application_worker_spec.rb15
-rw-r--r--spec/workers/create_commit_signature_worker_spec.rb19
-rw-r--r--spec/workers/design_management/new_version_worker_spec.rb72
-rw-r--r--spec/workers/external_service_reactive_caching_worker_spec.rb7
-rw-r--r--spec/workers/gitlab/jira_import/import_issue_worker_spec.rb39
-rw-r--r--spec/workers/gitlab/jira_import/stage/finish_import_worker_spec.rb4
-rw-r--r--spec/workers/gitlab/jira_import/stage/import_attachments_worker_spec.rb4
-rw-r--r--spec/workers/gitlab/jira_import/stage/import_issues_worker_spec.rb7
-rw-r--r--spec/workers/gitlab/jira_import/stage/import_labels_worker_spec.rb11
-rw-r--r--spec/workers/gitlab/jira_import/stage/import_notes_worker_spec.rb4
-rw-r--r--spec/workers/group_import_worker_spec.rb59
-rw-r--r--spec/workers/incident_management/process_alert_worker_spec.rb67
-rw-r--r--spec/workers/merge_request_mergeability_check_worker_spec.rb11
-rw-r--r--spec/workers/namespaceless_project_destroy_worker_spec.rb14
-rw-r--r--spec/workers/new_release_worker_spec.rb2
-rw-r--r--spec/workers/post_receive_spec.rb44
-rw-r--r--spec/workers/process_commit_worker_spec.rb24
-rw-r--r--spec/workers/project_export_worker_spec.rb10
-rw-r--r--spec/workers/project_update_repository_storage_worker_spec.rb36
-rw-r--r--spec/workers/reactive_caching_worker_spec.rb44
-rw-r--r--spec/workers/stage_update_worker_spec.rb9
-rw-r--r--spec/workers/update_head_pipeline_for_merge_request_worker_spec.rb65
-rw-r--r--spec/workers/update_highest_role_worker_spec.rb2
-rw-r--r--spec/workers/x509_issuer_crl_check_worker_spec.rb90
-rw-r--r--vendor/elastic_stack/values.yaml89
-rwxr-xr-xvendor/elastic_stack/wait-for-elasticsearch.sh2
-rw-r--r--vendor/project_templates/android.tar.gzbin132592 -> 133429 bytes
-rw-r--r--vendor/project_templates/sse_middleman.tar.gzbin7099 -> 7368 bytes
-rw-r--r--yarn.lock320
5619 files changed, 203494 insertions, 80513 deletions
diff --git a/.eslintignore b/.eslintignore
index 9a5e15c86ae..f364771e54f 100644
--- a/.eslintignore
+++ b/.eslintignore
@@ -9,6 +9,6 @@
/scripts/
/tmp/
/vendor/
-jest.config.js
+jest.config.*.js
karma.config.js
webpack.config.js
diff --git a/.eslintrc.yml b/.eslintrc.yml
index 0639228fe51..f8bc2a3ae94 100644
--- a/.eslintrc.yml
+++ b/.eslintrc.yml
@@ -22,17 +22,16 @@ rules:
- allow:
- __
- _links
- # Disabled for now, to make the airbnb-base 12.1.0 -> 13.1.0 update smoother
- no-else-return:
- - error
- - allowElseIf: true
import/no-unresolved:
- error
- ignore:
# https://gitlab.com/gitlab-org/gitlab/issues/38226
- '^ee_component/'
- import/no-useless-path-segments: off
import/order: off
+ # Disabled for now, to make the airbnb-base 12.1.0 -> 13.1.0 update smoother
+ no-else-return:
+ - error
+ - allowElseIf: true
lines-between-class-members: off
# Disabled for now, to make the plugin-vue 4.5 -> 5.0 update smoother
vue/no-confusing-v-for-v-if: error
@@ -45,8 +44,6 @@ rules:
no-jquery/no-serialize: error
promise/always-return: off
promise/no-callback-in-promise: off
- # Make update to eslint@6 smoother:
- prefer-object-spread: off
overrides:
- files:
- '**/spec/**/*'
diff --git a/.gitlab-ci.yml b/.gitlab-ci.yml
index 9e808cc7a9b..21dfd6563e2 100644
--- a/.gitlab-ci.yml
+++ b/.gitlab-ci.yml
@@ -1,8 +1,7 @@
-image: "registry.gitlab.com/gitlab-org/gitlab-build-images:ruby-2.6.5-golang-1.14-git-2.26-lfs-2.9-chrome-73.0-node-12.x-yarn-1.21-postgresql-10-graphicsmagick-1.3.34"
-
stages:
- sync
- prepare
+ - build-images
- fixtures
- test
- post-test
@@ -16,6 +15,7 @@ stages:
# in cases where jobs require Docker-in-Docker, the job
# definition must be extended with `.use-docker-in-docker`
default:
+ image: "registry.gitlab.com/gitlab-org/gitlab-build-images:ruby-2.6.6-golang-1.14-git-2.26-lfs-2.9-chrome-73.0-node-12.x-yarn-1.21-postgresql-9.6-graphicsmagick-1.3.34"
tags:
- gitlab-org
# All jobs are interruptible by default
@@ -25,10 +25,17 @@ workflow:
rules:
# If `$FORCE_GITLAB_CI` is set, create a pipeline.
- if: '$FORCE_GITLAB_CI'
+ # As part of the process of creating RCs automatically, we update stable
+ # branches with the changes of the most recent production deployment. The
+ # merge requests used for this merge a branch release-tools/X into a stable
+ # branch. For these merge requests we don't want to run any pipelines, as
+ # they serve no purpose and will run anyway when the changes are merged.
+ - if: '$CI_MERGE_REQUEST_SOURCE_BRANCH_NAME =~ /^release-tools\/\d+\.\d+\.\d+-rc\d+$/ && $CI_MERGE_REQUEST_TARGET_BRANCH_NAME =~ /^[\d-]+-stable(-ee)?$/ && $CI_PROJECT_PATH == "gitlab-org/gitlab"'
+ when: never
# For merge requests, create a pipeline.
- if: '$CI_MERGE_REQUEST_IID'
# For `master` branch, create a pipeline (this includes on schedules, pushes, merges, etc.).
- - if: '$CI_COMMIT_BRANCH == "master"'
+ - if: '$CI_COMMIT_BRANCH == $CI_DEFAULT_BRANCH'
# For tags, create a pipeline.
- if: '$CI_COMMIT_TAG'
# If `$GITLAB_INTERNAL` isn't set, don't create a pipeline.
diff --git a/.gitlab/CODEOWNERS b/.gitlab/CODEOWNERS
index 28ad2b41921..e71e74fd4d3 100644
--- a/.gitlab/CODEOWNERS
+++ b/.gitlab/CODEOWNERS
@@ -7,8 +7,12 @@
*.rake @gitlab-org/maintainers/rails-backend
# Technical writing team are the default reviewers for all markdown docs
-*.md @gl-docsteam
/doc/ @gl-docsteam
+# Dev and Doc guidelines
+/doc/development/ @marcia @mjang1
+/doc/development/documentation/ @mikelewis
+/doc/ci @marcel.amirault @sselhorn
+/doc/.linting @marcel.amirault @eread @aqualls @mikelewis
# Frontend maintainers should see everything in `app/assets/`
*.scss @annabeldunstone @gitlab-org/maintainers/frontend
@@ -33,7 +37,7 @@
/ee/app/finders/ @gitlab-org/maintainers/database
# Feature specific owners
-/ee/lib/gitlab/code_owners/ @reprazent
+/ee/lib/gitlab/code_owners/ @reprazent @kerrizor
/ee/lib/ee/gitlab/auth/ldap/ @dblessing @mkozono
/lib/gitlab/auth/ldap/ @dblessing @mkozono
/lib/gitlab/ci/templates/ @nolith @zj
diff --git a/.gitlab/ci/cache-repo.gitlab-ci.yml b/.gitlab/ci/cache-repo.gitlab-ci.yml
index ecbed0ed6c8..a091785dec3 100644
--- a/.gitlab/ci/cache-repo.gitlab-ci.yml
+++ b/.gitlab/ci/cache-repo.gitlab-ci.yml
@@ -21,7 +21,6 @@ cache-repo:
extends: .cache-repo:rules
image: gcr.io/google.com/cloudsdktool/cloud-sdk:alpine
stage: sync
- allow_failure: true
variables:
GIT_STRATEGY: none
TAR_FILENAME: /tmp/gitlab-master.tar
diff --git a/.gitlab/ci/cng.gitlab-ci.yml b/.gitlab/ci/cng.gitlab-ci.yml
index 2450e346569..d7699de74e2 100644
--- a/.gitlab/ci/cng.gitlab-ci.yml
+++ b/.gitlab/ci/cng.gitlab-ci.yml
@@ -3,7 +3,6 @@ cloud-native-image:
image: ruby:2.6-alpine
dependencies: []
stage: post-test
- allow_failure: true
variables:
GIT_DEPTH: "1"
script:
diff --git a/.gitlab/ci/dev-fixtures.gitlab-ci.yml b/.gitlab/ci/dev-fixtures.gitlab-ci.yml
index 27ceb6f37db..fc3678a7d17 100644
--- a/.gitlab/ci/dev-fixtures.gitlab-ci.yml
+++ b/.gitlab/ci/dev-fixtures.gitlab-ci.yml
@@ -1,11 +1,11 @@
.run-dev-fixtures:
extends:
- .default-retry
- - .default-cache
+ - .rails-cache
- .default-before_script
- .use-pg11
stage: test
- needs: ["setup-test-env pg11"]
+ needs: ["setup-test-env"]
variables:
FIXTURE_PATH: "db/fixtures/development"
SEED_CYCLE_ANALYTICS: "true"
@@ -19,8 +19,9 @@ run-dev-fixtures:
- .run-dev-fixtures
- .dev-fixtures:rules:ee-and-foss
script:
- - scripts/gitaly-test-spawn
- - RAILS_ENV=test bundle exec rake db:seed_fu
+ - run_timed_command "scripts/gitaly-test-build"
+ - run_timed_command "scripts/gitaly-test-spawn"
+ - run_timed_command "RAILS_ENV=test bundle exec rake db:seed_fu"
run-dev-fixtures-ee:
extends:
@@ -28,6 +29,7 @@ run-dev-fixtures-ee:
- .dev-fixtures:rules:ee-only
- .use-pg11-ee
script:
- - scripts/gitaly-test-spawn
+ - run_timed_command "scripts/gitaly-test-build"
+ - run_timed_command "scripts/gitaly-test-spawn"
- cp ee/db/fixtures/development/* $FIXTURE_PATH
- - RAILS_ENV=test bundle exec rake db:seed_fu
+ - run_timed_command "RAILS_ENV=test bundle exec rake db:seed_fu"
diff --git a/.gitlab/ci/docs.gitlab-ci.yml b/.gitlab/ci/docs.gitlab-ci.yml
index b8a66decbb7..50dbef44598 100644
--- a/.gitlab/ci/docs.gitlab-ci.yml
+++ b/.gitlab/ci/docs.gitlab-ci.yml
@@ -2,7 +2,6 @@
extends:
- .default-retry
- .docs:rules:review-docs
- allow_failure: true
image: ruby:2.6-alpine
stage: review
dependencies: []
@@ -68,7 +67,7 @@ graphql-reference-verify:
- .docs:rules:graphql-reference-verify
- .use-pg11
stage: test
- needs: ["setup-test-env pg11"]
+ needs: ["setup-test-env"]
script:
- bundle exec rake gitlab:graphql:check_docs
- bundle exec rake gitlab:graphql:check_schema
diff --git a/.gitlab/ci/frontend.gitlab-ci.yml b/.gitlab/ci/frontend.gitlab-ci.yml
index 2b22162b0c2..6e9119f295a 100644
--- a/.gitlab/ci/frontend.gitlab-ci.yml
+++ b/.gitlab/ci/frontend.gitlab-ci.yml
@@ -15,8 +15,7 @@
- .default-retry
- .default-before_script
- .assets-compile-cache
- - .use-docker-in-docker
- image: registry.gitlab.com/gitlab-org/gitlab-build-images:ruby-2.6.5-git-2.26-lfs-2.9-chrome-73.0-node-12.x-yarn-1.21-graphicsmagick-1.3.34-docker-19.03.1
+ image: registry.gitlab.com/gitlab-org/gitlab-build-images:ruby-2.6.6-git-2.26-lfs-2.9-chrome-73.0-node-12.x-yarn-1.21-graphicsmagick-1.3.34-docker-19.03.1
stage: prepare
variables:
NODE_ENV: "production"
@@ -34,20 +33,16 @@
paths:
- webpack-report/
- assets-compile.log
- # We consume these files in GitLab UI for integration tests:
- # https://gitlab.com/gitlab-org/gitlab-ui/-/blob/e88493b3c855aea30bf60baee692a64606b0eb1e/.storybook/preview-head.pug#L1
- - public/assets/application-*.css
- - public/assets/application-*.css.gz
+ # These assets are used in multiple locations:
+ # - in `build-assets-image` job to create assets image for packaging systems
+ # - GitLab UI for integration tests: https://gitlab.com/gitlab-org/gitlab-ui/-/blob/e88493b3c855aea30bf60baee692a64606b0eb1e/.storybook/preview-head.pug#L1
+ - public/assets
when: always
script:
- node --version
- retry yarn install --frozen-lockfile --production --cache-folder .yarn-cache --prefer-offline
- free -m
- time bin/rake gitlab:assets:compile > assets-compile.log 2>&1
- # TODO: Change the image tag to be the MD5 of assets files and skip image building if the image exists
- # We'll also need to pass GITLAB_ASSETS_TAG to the trigerred omnibus-gitlab pipeline similarly to how we do it for trigerred CNG pipelines
- # https://gitlab.com/gitlab-org/gitlab/issues/208389
- - time scripts/build_assets_image
- scripts/clean-old-cached-assets
- rm -f /etc/apt/sources.list.d/google*.list # We don't need to update Chrome here
@@ -65,6 +60,20 @@ gitlab:assets:compile pull-cache:
cache:
policy: pull
+build-assets-image:
+ extends:
+ - .use-kaniko
+ - .frontend:rules:gitlab-assets-compile-pull-cache
+ stage: build-images
+ needs: ["gitlab:assets:compile pull-cache"]
+ variables:
+ GIT_DEPTH: "1"
+ script:
+ # TODO: Change the image tag to be the MD5 of assets files and skip image building if the image exists
+ # We'll also need to pass GITLAB_ASSETS_TAG to the trigerred omnibus-gitlab pipeline similarly to how we do it for trigerred CNG pipelines
+ # https://gitlab.com/gitlab-org/gitlab/issues/208389
+ - scripts/build_assets_image
+
.compile-assets-metadata:
extends:
- .default-retry
@@ -127,16 +136,15 @@ compile-assets pull-cache as-if-foss:
.frontend-fixtures-base:
extends:
- .default-retry
- - .default-cache
+ - .rails-cache
- .default-before_script
- .use-pg11
stage: fixtures
- needs: ["setup-test-env pg11", "compile-assets pull-cache"]
+ needs: ["setup-test-env", "compile-assets pull-cache"]
script:
- - date
- - scripts/gitaly-test-spawn
- - date
- - bundle exec rake frontend:fixtures
+ - run_timed_command "scripts/gitaly-test-build"
+ - run_timed_command "scripts/gitaly-test-spawn"
+ - run_timed_command "bundle exec rake frontend:fixtures"
artifacts:
name: frontend-fixtures
expire_in: 31d
@@ -154,7 +162,7 @@ frontend-fixtures:
frontend-fixtures-as-if-foss:
extends:
- .frontend-fixtures-base
- - .frontend:rules:default-frontend-jobs-no-foss
+ - .frontend:rules:default-frontend-jobs-as-if-foss
- .as-if-foss
.frontend-job-base:
@@ -197,7 +205,7 @@ karma:
karma-as-if-foss:
extends:
- .karma-base
- - .frontend:rules:default-frontend-jobs-no-foss
+ - .frontend:rules:default-frontend-jobs-as-if-foss
- .as-if-foss
needs: ["frontend-fixtures-as-if-foss"]
@@ -229,10 +237,24 @@ jest:
junit: junit_jest.xml
parallel: 2
+jest-integration:
+ extends:
+ - .frontend-job-base
+ - .frontend:rules:default-frontend-jobs
+ script:
+ - date
+ - yarn jest:integration --ci
+ needs: ["frontend-fixtures"]
+ cache:
+ key: jest-integration
+ paths:
+ - tmp/cache/jest/
+ policy: pull-push
+
jest-as-if-foss:
extends:
- .jest-base
- - .frontend:rules:default-frontend-jobs-no-foss
+ - .frontend:rules:default-frontend-jobs-as-if-foss
- .as-if-foss
needs: ["frontend-fixtures-as-if-foss"]
cache:
@@ -241,11 +263,13 @@ jest-as-if-foss:
coverage-frontend:
extends:
- .default-retry
- - .frontend:rules:default-frontend-jobs-no-foss
+ - .yarn-cache
+ - .frontend:rules:ee-mr-and-master-only
needs: ["jest"]
stage: post-test
before_script:
- - yarn install --frozen-lockfile --cache-folder .yarn-cache --prefer-offline
+ - source scripts/utils.sh
+ - retry yarn install --frozen-lockfile
script:
- yarn node scripts/frontend/merge_coverage_frontend.js
artifacts:
@@ -254,8 +278,7 @@ coverage-frontend:
paths:
- coverage-frontend/
cache:
- paths:
- - .yarn-cache/
+ policy: pull
.qa-frontend-node:
extends:
@@ -279,22 +302,32 @@ qa-frontend-node:10:
image: node:dubnium
qa-frontend-node:latest:
- extends: .qa-frontend-node
+ extends:
+ - .qa-frontend-node
+ - .frontend:rules:qa-frontend-node-latest
image: node:latest
- allow_failure: true
webpack-dev-server:
extends:
- .default-retry
- - .default-cache
- .frontend:rules:default-frontend-jobs
stage: test
- needs: ["setup-test-env pg11", "compile-assets pull-cache"]
+ needs: []
variables:
WEBPACK_MEMORY_TEST: "true"
WEBPACK_VENDOR_DLL: "true"
+ cache:
+ key:
+ files:
+ - yarn.lock
+ prefix: "v1"
+ paths:
+ - node_modules/
+ - tmp/cache/webpack-dlls/
script:
- - yarn webpack-vendor
+ - source scripts/utils.sh
+ - retry yarn install --frozen-lockfile
+ - retry yarn webpack-vendor
- node --expose-gc node_modules/.bin/webpack-dev-server --config config/webpack.config.js
artifacts:
name: webpack-dev-server
diff --git a/.gitlab/ci/global.gitlab-ci.yml b/.gitlab/ci/global.gitlab-ci.yml
index 66b7c47efcf..e6619ff2b6d 100644
--- a/.gitlab/ci/global.gitlab-ci.yml
+++ b/.gitlab/ci/global.gitlab-ci.yml
@@ -21,7 +21,7 @@
# Jobs that only need to pull cache
.default-cache:
cache:
- key: "debian-stretch-ruby-2.6.5-pg11-node-12.x"
+ key: "debian-stretch-ruby-2.6.6-pg11-node-12.x"
paths:
- .go/pkg/mod
- vendor/ruby
@@ -29,65 +29,39 @@
- vendor/gitaly-ruby
policy: pull
-.use-pg9:
- image: "registry.gitlab.com/gitlab-org/gitlab-build-images:ruby-2.6.5-golang-1.14-git-2.26-lfs-2.9-chrome-73.0-node-12.x-yarn-1.21-postgresql-9.6-graphicsmagick-1.3.34"
- services:
- - name: postgres:9.6.17
- command: ["postgres", "-c", "fsync=off", "-c", "synchronous_commit=off", "-c", "full_page_writes=off"]
- - name: redis:alpine
- variables:
- POSTGRES_HOST_AUTH_METHOD: trust
+.rails-cache:
cache:
- key: "debian-stretch-ruby-2.6.5-pg9-node-12.x"
+ key:
+ files:
+ - Gemfile.lock
+ - GITALY_SERVER_VERSION
+ prefix: "ruby-go-cache-v1"
+ paths:
+ - vendor/ruby
+ - vendor/gitaly-ruby
+ - .go/pkg/mod
+ policy: pull
-.use-pg10:
- image: "registry.gitlab.com/gitlab-org/gitlab-build-images:ruby-2.6.5-golang-1.14-git-2.26-lfs-2.9-chrome-73.0-node-12.x-yarn-1.21-postgresql-10-graphicsmagick-1.3.34"
- services:
- - name: postgres:10.12
- command: ["postgres", "-c", "fsync=off", "-c", "synchronous_commit=off", "-c", "full_page_writes=off"]
- - name: redis:alpine
- variables:
- POSTGRES_HOST_AUTH_METHOD: trust
+.yarn-cache:
cache:
- key: "debian-stretch-ruby-2.6.5-pg10-node-12.x"
+ key:
+ files:
+ - yarn.lock
+ prefix: "v1"
+ paths:
+ - node_modules/
.use-pg11:
- image: "registry.gitlab.com/gitlab-org/gitlab-build-images:ruby-2.6.5-golang-1.14-git-2.26-lfs-2.9-chrome-73.0-node-12.x-yarn-1.21-postgresql-11-graphicsmagick-1.3.34"
+ image: "registry.gitlab.com/gitlab-org/gitlab-build-images:ruby-2.6.6-golang-1.14-git-2.26-lfs-2.9-chrome-73.0-node-12.x-yarn-1.21-postgresql-11-graphicsmagick-1.3.34"
services:
- name: postgres:11.6
command: ["postgres", "-c", "fsync=off", "-c", "synchronous_commit=off", "-c", "full_page_writes=off"]
- name: redis:alpine
variables:
POSTGRES_HOST_AUTH_METHOD: trust
- cache:
- key: "debian-stretch-ruby-2.6.5-pg11-node-12.x"
-
-.use-pg9-ee:
- image: "registry.gitlab.com/gitlab-org/gitlab-build-images:ruby-2.6.5-golang-1.14-git-2.26-lfs-2.9-chrome-73.0-node-12.x-yarn-1.21-postgresql-9.6-graphicsmagick-1.3.34"
- services:
- - name: postgres:9.6.17
- command: ["postgres", "-c", "fsync=off", "-c", "synchronous_commit=off", "-c", "full_page_writes=off"]
- - name: redis:alpine
- - name: elasticsearch:6.4.2
- variables:
- POSTGRES_HOST_AUTH_METHOD: trust
- cache:
- key: "debian-stretch-ruby-2.6.5-pg9-node-12.x"
-
-.use-pg10-ee:
- image: "registry.gitlab.com/gitlab-org/gitlab-build-images:ruby-2.6.5-golang-1.14-git-2.26-lfs-2.9-chrome-73.0-node-12.x-yarn-1.21-postgresql-10-graphicsmagick-1.3.34"
- services:
- - name: postgres:10.12
- command: ["postgres", "-c", "fsync=off", "-c", "synchronous_commit=off", "-c", "full_page_writes=off"]
- - name: redis:alpine
- - name: elasticsearch:6.4.2
- variables:
- POSTGRES_HOST_AUTH_METHOD: trust
- cache:
- key: "debian-stretch-ruby-2.6.5-pg10-node-12.x"
.use-pg11-ee:
- image: "registry.gitlab.com/gitlab-org/gitlab-build-images:ruby-2.6.5-golang-1.14-git-2.26-lfs-2.9-chrome-73.0-node-12.x-yarn-1.21-postgresql-11-graphicsmagick-1.3.34"
+ image: "registry.gitlab.com/gitlab-org/gitlab-build-images:ruby-2.6.6-golang-1.14-git-2.26-lfs-2.9-chrome-73.0-node-12.x-yarn-1.21-postgresql-11-graphicsmagick-1.3.34"
services:
- name: postgres:11.6
command: ["postgres", "-c", "fsync=off", "-c", "synchronous_commit=off", "-c", "full_page_writes=off"]
@@ -95,15 +69,13 @@
- name: elasticsearch:6.4.2
variables:
POSTGRES_HOST_AUTH_METHOD: trust
- cache:
- key: "debian-stretch-ruby-2.6.5-pg11-node-12.x"
-# Pin kaniko to v0.16.0 due to https://github.com/GoogleContainerTools/kaniko/issues/1162
.use-kaniko:
image:
- name: gcr.io/kaniko-project/executor:debug-v0.16.0
+ name: gcr.io/kaniko-project/executor:debug-v0.20.0
entrypoint: [""]
before_script:
+ - mkdir -p /kaniko/.docker
- echo "{\"auths\":{\"$CI_REGISTRY\":{\"username\":\"$CI_REGISTRY_USER\",\"password\":\"$CI_REGISTRY_PASSWORD\"}}}" > /kaniko/.docker/config.json
.as-if-foss:
diff --git a/.gitlab/ci/memory.gitlab-ci.yml b/.gitlab/ci/memory.gitlab-ci.yml
index af75ff257ea..79dfc88d132 100644
--- a/.gitlab/ci/memory.gitlab-ci.yml
+++ b/.gitlab/ci/memory.gitlab-ci.yml
@@ -8,7 +8,7 @@
memory-static:
extends: .only-code-memory-job-base
stage: test
- needs: ["setup-test-env pg11"]
+ needs: ["setup-test-env"]
variables:
SETUP_DB: "false"
script:
@@ -28,6 +28,7 @@ memory-static:
- tmp/memory_*.txt
reports:
metrics: tmp/memory_metrics.txt
+ expire_in: 31d
# Show memory usage caused by invoking require per gem.
# Unlike `memory-static`, it hits the app with one request to ensure that any last minute require-s have been called.
@@ -38,7 +39,7 @@ memory-on-boot:
- .only-code-memory-job-base
- .use-pg11
stage: test
- needs: ["setup-test-env pg11", "compile-assets pull-cache"]
+ needs: ["setup-test-env", "compile-assets pull-cache"]
variables:
NODE_ENV: "production"
RAILS_ENV: "production"
@@ -54,3 +55,4 @@ memory-on-boot:
- tmp/memory_*.txt
reports:
metrics: tmp/memory_on_boot_metrics.txt
+ expire_in: 31d
diff --git a/.gitlab/ci/pages.gitlab-ci.yml b/.gitlab/ci/pages.gitlab-ci.yml
index 38d79ddb090..218ec7043d9 100644
--- a/.gitlab/ci/pages.gitlab-ci.yml
+++ b/.gitlab/ci/pages.gitlab-ci.yml
@@ -15,3 +15,4 @@ pages:
artifacts:
paths:
- public
+ expire_in: 31d
diff --git a/.gitlab/ci/qa.gitlab-ci.yml b/.gitlab/ci/qa.gitlab-ci.yml
index 8a8f66a4643..40ef13dd92b 100644
--- a/.gitlab/ci/qa.gitlab-ci.yml
+++ b/.gitlab/ci/qa.gitlab-ci.yml
@@ -23,7 +23,7 @@ qa:internal:
qa:internal-as-if-foss:
extends:
- .qa-job-base
- - .qa:rules:ee-only
+ - .qa:rules:as-if-foss
- .as-if-foss
script:
- bundle exec rspec
@@ -38,7 +38,7 @@ qa:selectors:
qa:selectors-as-if-foss:
extends:
- qa:selectors
- - .qa:rules:ee-only
+ - .qa:rules:as-if-foss
- .as-if-foss
.package-and-qa-base:
@@ -58,6 +58,5 @@ package-and-qa:
needs:
- job: build-qa-image
artifacts: false
- - job: gitlab:assets:compile pull-cache
+ - job: build-assets-image
artifacts: false
- allow_failure: true
diff --git a/.gitlab/ci/rails.gitlab-ci.yml b/.gitlab/ci/rails.gitlab-ci.yml
index 28ec96b838a..e8087aebcef 100644
--- a/.gitlab/ci/rails.gitlab-ci.yml
+++ b/.gitlab/ci/rails.gitlab-ci.yml
@@ -1,48 +1,43 @@
.rails:needs:setup-and-assets:
- needs: ["setup-test-env pg11", "compile-assets pull-cache"]
+ needs: ["setup-test-env", "compile-assets pull-cache"]
.rails-job-base:
extends:
- .default-retry
- - .default-cache
- .default-before_script
+ - .rails-cache
-####################
-# EE and FOSS jobs #
-.base-setup-test-env:
+#######################################################
+# EE/FOSS: default refs (MRs, master, schedules) jobs #
+setup-test-env:
extends:
- .rails-job-base
+ - .rails:rules:default-refs-code-backstage-qa
+ - .use-pg11
stage: prepare
+ variables:
+ GITLAB_TEST_EAGER_LOAD: "0"
script:
- - bundle exec ruby -Ispec -e 'require "spec_helper" ; TestEnv.init'
- - scripts/gitaly-test-build # Do not use 'bundle exec' here
+ - run_timed_command "bundle exec ruby -I. -e 'require \"config/environment\"; TestEnv.init'"
+ - run_timed_command "scripts/gitaly-test-build" # Do not use 'bundle exec' here
+ - rm tmp/tests/gitaly/.ruby-bundle # This file prevents gems from being installed even if vendor/gitaly-ruby is missing
artifacts:
expire_in: 7d
paths:
- - tmp/tests
- config/secrets.yml
- - vendor/gitaly-ruby
+ - tmp/tests/gitaly
+ - tmp/tests/gitlab-elasticsearch-indexer
+ - tmp/tests/gitlab-shell
+ - tmp/tests/gitlab-test-fork
+ - tmp/tests/gitlab-test-fork_bare
+ - tmp/tests/gitlab-test
+ - tmp/tests/gitlab-workhorse
+ - tmp/tests/repositories
+ - tmp/tests/second_storage
+ when: always
cache:
policy: pull-push
-setup-test-env pg11:
- extends:
- - .base-setup-test-env
- - .rails:rules:default-refs-code-backstage-qa
- - .use-pg11
-
-setup-test-env pg10:
- extends:
- - .base-setup-test-env
- - .rails:rules:master-refs-code-backstage
- - .use-pg10
-
-setup-test-env pg9:
- extends:
- - .base-setup-test-env
- - .rails:rules:nightly-master-refs-code-backstage
- - .use-pg9
-
static-analysis:
extends:
- .rails-job-base
@@ -55,7 +50,7 @@ static-analysis:
script:
- scripts/static-analysis
cache:
- key: "ruby-2.6.5-pg11-rubocop"
+ key: "ruby-2.6.6-pg11-rubocop"
paths:
- vendor/ruby
- tmp/rubocop_cache
@@ -64,8 +59,8 @@ static-analysis:
downtime_check:
extends:
- .rails-job-base
- - .rails:needs:setup-and-assets
- .rails:rules:downtime_check
+ needs: ["setup-test-env"]
stage: test
variables:
SETUP_DB: "false"
@@ -75,8 +70,10 @@ downtime_check:
.rspec-base:
extends: .rails-job-base
stage: test
- needs: ["setup-test-env pg11", "retrieve-tests-metadata", "compile-assets pull-cache"]
+ needs: ["setup-test-env", "retrieve-tests-metadata", "compile-assets pull-cache"]
script:
+ - run_timed_command "scripts/gitaly-test-build"
+ - run_timed_command "scripts/gitaly-test-spawn"
- source scripts/rspec_helpers.sh
- rspec_paralellized_job "--tag ~quarantine --tag ~geo --tag ~level:migration"
artifacts:
@@ -89,7 +86,7 @@ downtime_check:
- rspec_profiling/
- tmp/capybara/
- tmp/memory_test/
- - junit_rspec.xml
+ - log/*.log
reports:
junit: junit_rspec.xml
@@ -101,6 +98,8 @@ downtime_check:
.rspec-base-migration:
script:
+ - run_timed_command "scripts/gitaly-test-build"
+ - run_timed_command "scripts/gitaly-test-spawn"
- source scripts/rspec_helpers.sh
- rspec_paralellized_job "--tag ~quarantine --tag ~geo --tag level:migration"
@@ -133,7 +132,7 @@ rspec fast_spec_helper:
- .rails:rules:ee-and-foss
- .use-pg11
stage: test
- needs: ["setup-test-env pg11"]
+ needs: ["setup-test-env"]
db:migrate:reset:
extends: .db-job-base
@@ -141,28 +140,28 @@ db:migrate:reset:
- bundle exec rake db:migrate:reset
db:check-schema:
- extends: .db-job-base
+ extends:
+ - .db-job-base
+ - .rails:rules:ee-mr-and-master-only
script:
- source scripts/schema_changed.sh
-db:migrate-from-v11.11.0:
+db:migrate-from-v12.10.0:
extends: .db-job-base
variables:
SETUP_DB: "false"
script:
- - export PROJECT_TO_CHECKOUT="gitlab-foss"
- - export TAG_TO_CHECKOUT="v11.11.0"
- - '[[ ! -d "ee/" ]] || export PROJECT_TO_CHECKOUT="gitlab"'
- - '[[ ! -d "ee/" ]] || export TAG_TO_CHECKOUT="v11.11.0-ee"'
+ - export PROJECT_TO_CHECKOUT="gitlab"
+ - export TAG_TO_CHECKOUT="v12.10.0-ee"
+ - '[[ -d "ee/" ]] || export PROJECT_TO_CHECKOUT="gitlab-foss"'
+ - '[[ -d "ee/" ]] || export TAG_TO_CHECKOUT="v12.10.0"'
- git fetch https://gitlab.com/gitlab-org/$PROJECT_TO_CHECKOUT.git $TAG_TO_CHECKOUT
- git checkout -f FETCH_HEAD
- - sed -i "s/gem 'oj', '~> 2.17.4'//" Gemfile
- - sed -i "s/gem 'bootsnap', '~> 1.0.0'/gem 'bootsnap'/" Gemfile
- bundle update google-protobuf grpc bootsnap
- bundle install $BUNDLE_INSTALL_FLAGS
- date
- cp config/gitlab.yml.example config/gitlab.yml
- - bundle exec rake db:drop db:create db:schema:load db:seed_fu
+ - bundle exec rake db:drop db:create db:structure:load db:seed_fu
- date
- git checkout -f $CI_COMMIT_SHA
- bundle install $BUNDLE_INSTALL_FLAGS
@@ -186,23 +185,24 @@ gitlab:setup:
# db/fixtures/development/04_project.rb thanks to SIZE=1 below
- git clone https://gitlab.com/gitlab-org/gitlab-test.git
/home/git/repositories/gitlab-org/gitlab-test.git
- - scripts/gitaly-test-spawn
+ - run_timed_command "scripts/gitaly-test-build"
+ - run_timed_command "scripts/gitaly-test-spawn"
- force=yes SIZE=1 FIXTURE_PATH="db/fixtures/development" bundle exec rake gitlab:setup
artifacts:
when: on_failure
expire_in: 1d
paths:
- - log/development.log
+ - log/*.log
rspec:coverage:
extends:
- .rails-job-base
- - .rails:rules:ee-only
+ - .rails:rules:ee-mr-and-master-only
stage: post-test
# We cannot use needs since it would mean needing 84 jobs (since most are parallelized)
# so we use `dependencies` here.
dependencies:
- - setup-test-env pg11
+ - setup-test-env
- rspec migration pg11
- rspec unit pg11
- rspec integration pg11
@@ -231,104 +231,11 @@ rspec:coverage:
- coverage/index.html
- coverage/assets/
- tmp/memory_test/
-# EE and FOSS jobs #
-####################
-
-####################
-# master-only jobs #
-.rspec-base-pg10:
- extends:
- - .rspec-base
- - .rails:rules:master-refs-code-backstage
- - .use-pg10
- needs: ["setup-test-env pg10", "retrieve-tests-metadata", "compile-assets pull-cache"]
+# EE/FOSS: default refs (MRs, master, schedules) jobs #
+#######################################################
-rspec migration pg10:
- extends:
- - .rspec-base-pg10
- - .rspec-base-migration
- parallel: 5
-
-rspec unit pg10:
- extends: .rspec-base-pg10
- parallel: 20
-
-rspec integration pg10:
- extends: .rspec-base-pg10
- parallel: 8
-
-rspec system pg10:
- extends: .rspec-base-pg10
- parallel: 24
-# master-only jobs #
-####################
-
-######################
-# nightly-only jobs #
-.rspec-base-pg9:
- extends:
- - .rspec-base
- - .rails:rules:nightly-master-refs-code-backstage
- - .use-pg9
- needs: ["setup-test-env pg9", "retrieve-tests-metadata", "compile-assets pull-cache"]
-
-rspec migration pg9:
- extends:
- - .rspec-base-pg9
- - .rspec-base-migration
- parallel: 5
-
-rspec unit pg9:
- extends: .rspec-base-pg9
- parallel: 20
-
-rspec integration pg9:
- extends: .rspec-base-pg9
- parallel: 8
-
-rspec system pg9:
- extends: .rspec-base-pg9
- parallel: 24
-# nightly-only jobs #
-#####################
-
-#######################
-# EE master-only jobs #
-.rspec-ee-base-pg10:
- extends:
- - .rspec-base-ee
- - .use-pg10-ee
- needs: ["setup-test-env pg10", "retrieve-tests-metadata", "compile-assets pull-cache"]
-
-rspec-ee migration pg10:
- extends:
- - .rspec-ee-base-pg10
- - .rspec-base-migration
- - .rails:rules:master-refs-code-backstage
- parallel: 2
-
-rspec-ee unit pg10:
- extends:
- - .rspec-ee-base-pg10
- - .rails:rules:master-refs-code-backstage
- parallel: 10
-
-rspec-ee integration pg10:
- extends:
- - .rspec-ee-base-pg10
- - .rails:rules:master-refs-code-backstage
- parallel: 4
-
-rspec-ee system pg10:
- extends:
- - .rspec-ee-base-pg10
- - .rails:rules:master-refs-code-backstage
- parallel: 6
-# EE master-only jobs #
-#######################
-
-################
-# EE-only jobs #
+##################################################
+# EE: default refs (MRs, master, schedules) jobs #
.rspec-base-ee:
extends:
- .rspec-base
@@ -336,10 +243,11 @@ rspec-ee system pg10:
.rspec-base-pg11-as-if-foss:
extends:
- - .rspec-base-ee
+ - .rspec-base
+ - .rails:rules:as-if-foss
- .as-if-foss
- .use-pg11
- needs: ["setup-test-env pg11", "retrieve-tests-metadata", "compile-assets pull-cache as-if-foss"]
+ needs: ["setup-test-env", "retrieve-tests-metadata", "compile-assets pull-cache as-if-foss"]
.rspec-ee-base-pg11:
extends:
@@ -385,6 +293,8 @@ rspec-ee system pg11:
.rspec-ee-base-geo:
extends: .rspec-base-ee
script:
+ - run_timed_command "scripts/gitaly-test-build"
+ - run_timed_command "scripts/gitaly-test-spawn"
- source scripts/rspec_helpers.sh
- scripts/prepare_postgres_fdw.sh
- rspec_paralellized_job "--tag ~quarantine --tag geo"
@@ -411,26 +321,5 @@ db:rollback geo:
script:
- bundle exec rake geo:db:migrate VERSION=20170627195211
- bundle exec rake geo:db:migrate
-# EE-only jobs #
-################
-
-########################
-# EE nightly-only jobs #
-.rspec-ee-base-geo-pg9:
- extends:
- - .rspec-ee-base-geo
- - .use-pg9-ee
- - .rails:rules:nightly-master-refs-code-backstage-ee-only
- needs: ["setup-test-env pg9", "retrieve-tests-metadata", "compile-assets pull-cache"]
-
-rspec-ee unit pg9 geo:
- extends: .rspec-ee-base-geo-pg9
- parallel: 2
-
-rspec-ee integration pg9 geo:
- extends: .rspec-ee-base-geo-pg9
-
-rspec-ee system pg9 geo:
- extends: .rspec-ee-base-geo-pg9
-# EE nightly-only jobs #
-########################
+# EE: default refs (MRs, master, schedules) jobs #
+##################################################
diff --git a/.gitlab/ci/reports.gitlab-ci.yml b/.gitlab/ci/reports.gitlab-ci.yml
index 61915aa798e..153334e1aff 100644
--- a/.gitlab/ci/reports.gitlab-ci.yml
+++ b/.gitlab/ci/reports.gitlab-ci.yml
@@ -14,11 +14,7 @@ code_quality:
- .use-docker-in-docker
stage: test
needs: []
- allow_failure: true
variables:
- # emptying DOCKER_HOST so it can be detected properly on kubernetes executor
- # with the script below
- DOCKER_HOST: ""
CODE_QUALITY_IMAGE: "registry.gitlab.com/gitlab-org/ci-cd/codequality:0.85.9"
script:
- |
@@ -47,12 +43,10 @@ code_quality:
extends:
- .default-retry
- .reports:rules:sast
- - .use-docker-in-docker
stage: test
# `needs: []` starts the job immediately in the pipeline
# https://docs.gitlab.com/ee/ci/yaml/README.html#needs
needs: []
- allow_failure: true
artifacts:
paths:
- gl-sast-report.json # GitLab-specific
@@ -60,10 +54,6 @@ code_quality:
sast: gl-sast-report.json
expire_in: 1 week # GitLab-specific
variables:
- # emptying DOCKER_HOST so it can be detected properly on kubernetes executor
- # with the script below
- DOCKER_HOST: ""
- DOCKER_DRIVER: overlay2
DOCKER_TLS_CERTDIR: ""
SAST_ANALYZER_IMAGE_PREFIX: "registry.gitlab.com/gitlab-org/security-products/analyzers"
SAST_ANALYZER_IMAGE_TAG: 2
@@ -82,15 +72,11 @@ eslint-sast:
image:
name: "$SAST_ANALYZER_IMAGE_PREFIX/eslint:$SAST_ANALYZER_IMAGE_TAG"
-kubesec-sast:
- extends: .sast
- image:
- name: "$SAST_ANALYZER_IMAGE_PREFIX/kubesec:$SAST_ANALYZER_IMAGE_TAG"
-
-nodejs-scan-sast:
- extends: .sast
- image:
- name: "$SAST_ANALYZER_IMAGE_PREFIX/nodejs-scan:$SAST_ANALYZER_IMAGE_TAG"
+# Temporary disabled as it's constantly failing. See https://gitlab.com/gitlab-org/gitlab/-/issues/213769.
+# nodejs-scan-sast:
+# extends: .sast
+# image:
+# name: "$SAST_ANALYZER_IMAGE_PREFIX/nodejs-scan:$SAST_ANALYZER_IMAGE_TAG"
secrets-sast:
extends: .sast
@@ -108,11 +94,7 @@ dependency_scanning:
stage: test
needs: []
variables:
- # emptying DOCKER_HOST so it can be detected properly on kubernetes executor
- # with the script below
- DOCKER_HOST: ""
DS_EXCLUDED_PATHS: "qa/qa/ee/fixtures/secure_premade_reports,spec,ee/spec" # GitLab-specific
- allow_failure: true
script:
- export DS_VERSION=${SP_VERSION:-$(echo "$CI_SERVER_VERSION" | sed 's/^\([0-9]*\)\.\([0-9]*\).*/\1-\2-stable/')}
- |
@@ -183,7 +165,6 @@ dast:
# DAST_USERNAME_FIELD: "user[login]"
# DAST_PASSWORD_FIELD: "user[passowrd]"
DAST_VERSION: 1
- allow_failure: true
script:
- 'export DAST_WEBSITE="${DAST_WEBSITE:-$(cat environment_url.txt)}"'
# To be done in a later iteration
diff --git a/.gitlab/ci/review.gitlab-ci.yml b/.gitlab/ci/review.gitlab-ci.yml
index 06710b3b9dd..46a281cd48f 100644
--- a/.gitlab/ci/review.gitlab-ci.yml
+++ b/.gitlab/ci/review.gitlab-ci.yml
@@ -1,70 +1,36 @@
-.review-docker:
- extends:
- - .default-retry
- - .use-docker-in-docker
- image: registry.gitlab.com/gitlab-org/gitlab-build-images:gitlab-qa-alpine-ruby-2.6
- variables:
- GITLAB_EDITION: "ce"
-
build-qa-image:
extends:
- .use-kaniko
- .default-retry
- - .review:rules:mr-and-schedule-auto
- stage: prepare
+ - .review:rules:build-qa-image
+ stage: build-images
+ needs: []
script:
- - '[[ ! -d "ee/" ]] || export GITLAB_EDITION="ee"'
- - export QA_IMAGE="${CI_REGISTRY}/${CI_PROJECT_PATH}/gitlab-${GITLAB_EDITION}-qa:${CI_COMMIT_REF_SLUG}"
+ - export QA_IMAGE="${CI_REGISTRY}/${CI_PROJECT_PATH}/gitlab-ee-qa:${CI_COMMIT_REF_SLUG}"
- /kaniko/executor --context=${CI_PROJECT_DIR} --dockerfile=${CI_PROJECT_DIR}/qa/Dockerfile --destination=${QA_IMAGE} --cache=true
-.review-cleanup-base:
+review-cleanup:
extends:
- .default-retry
- .review:rules:review-cleanup
+ image: registry.gitlab.com/gitlab-org/gitlab-build-images:gitlab-helm3-kubectl1.14
stage: prepare
- allow_failure: true
environment:
name: review/auto-cleanup
action: stop
before_script:
- source scripts/utils.sh
+ - source scripts/review_apps/gcp_cleanup.sh
- install_gitlab_gem
+ - setup_gcp_dependencies
script:
- ruby -rrubygems scripts/review_apps/automated_cleanup.rb
-
-review-cleanup:
- extends:
- - .review-cleanup-base
- image: registry.gitlab.com/gitlab-org/gitlab-build-images:gitlab-charts-build-base
-
-review-cleanup-helm3:
- extends:
- - .review-cleanup-base
- variables:
- HELM_3: 1
- image: registry.gitlab.com/gitlab-org/gitlab-build-images:gitlab-helm3-kubectl1.14
-
-review-gcp-cleanup:
- extends:
- - .review:rules:review-gcp-cleanup
- stage: prepare
- image: gcr.io/google.com/cloudsdktool/cloud-sdk:latest
- allow_failure: true
- environment:
- name: review/auto-gcp-cleanup
- action: stop
- before_script:
- - gcloud auth activate-service-account --key-file=$REVIEW_APPS_GCP_CREDENTIALS
- - gcloud config set project $REVIEW_APPS_GCP_PROJECT
- - apt-get install -y jq
- - source scripts/review_apps/gcp_cleanup.sh
- script:
- gcp_cleanup
review-build-cng:
extends:
- .default-retry
- - .review:rules:mr-and-schedule-auto-if-frontend-manual-otherwise
+ - .review:rules:review-build-cng
image: ruby:2.6-alpine
stage: review-prepare
before_script:
@@ -87,8 +53,7 @@ review-build-cng:
variables:
HOST_SUFFIX: "${CI_ENVIRONMENT_SLUG}"
DOMAIN: "-${CI_ENVIRONMENT_SLUG}.${REVIEW_APPS_DOMAIN}"
- GITLAB_HELM_CHART_REF: "v3.2.2"
- GITLAB_EDITION: "ce"
+ GITLAB_HELM_CHART_REF: "v3.3.3"
environment:
name: review/${CI_COMMIT_REF_NAME}
url: https://gitlab-${CI_ENVIRONMENT_SLUG}.${REVIEW_APPS_DOMAIN}
@@ -102,9 +67,7 @@ review-deploy:
stage: review
dependencies: []
resource_group: "review/${CI_COMMIT_REF_NAME}"
- allow_failure: true
before_script:
- - '[[ ! -d "ee/" ]] || export GITLAB_EDITION="ee"'
- export GITLAB_SHELL_VERSION=$(<GITLAB_SHELL_VERSION)
- export GITALY_VERSION=$(<GITALY_SERVER_VERSION)
- export GITLAB_WORKHORSE_VERSION=$(<GITLAB_WORKHORSE_VERSION)
@@ -146,28 +109,28 @@ review-deploy:
review-stop-failed-deployment:
extends:
- .review-stop-base
- - .review:rules:mr-only-auto
+ - .review:rules:review-stop-failed-deployment
stage: prepare
script:
- delete_failed_release
- - delete_helm2_release
review-stop:
extends:
- .review-stop-base
- .review:rules:mr-only-manual
stage: review
- allow_failure: true
script:
- delete_release
.review-qa-base:
- extends: .review-docker
+ extends:
+ - .default-retry
+ - .use-docker-in-docker
+ image: registry.gitlab.com/gitlab-org/gitlab-build-images:gitlab-qa-alpine-ruby-2.6
stage: qa
# This is needed so that manual jobs with needs don't block the pipeline.
# See https://gitlab.com/gitlab-org/gitlab/-/issues/199979.
dependencies: ["review-deploy"]
- allow_failure: true
variables:
QA_ARTIFACTS_DIR: "${CI_PROJECT_DIR}/qa"
QA_CAN_TEST_GIT_PROTOCOL_V2: "false"
@@ -179,8 +142,7 @@ review-stop:
GITHUB_ACCESS_TOKEN: "${REVIEW_APPS_QA_GITHUB_ACCESS_TOKEN}"
EE_LICENSE: "${REVIEW_APPS_EE_LICENSE}"
before_script:
- - '[[ ! -d "ee/" ]] || export GITLAB_EDITION="ee"'
- - export QA_IMAGE="${CI_REGISTRY}/${CI_PROJECT_PATH}/gitlab-${GITLAB_EDITION}-qa:${CI_COMMIT_REF_SLUG}"
+ - export QA_IMAGE="${CI_REGISTRY}/${CI_PROJECT_PATH}/gitlab-ee-qa:${CI_COMMIT_REF_SLUG}"
- export CI_ENVIRONMENT_URL="$(cat environment_url.txt)"
- echo "${CI_ENVIRONMENT_URL}"
- echo "${QA_IMAGE}"
@@ -196,7 +158,7 @@ review-stop:
review-qa-smoke:
extends:
- .review-qa-base
- - .review:rules:mr-only-auto-if-frontend-manual-otherwise
+ - .review:rules:review-qa-smoke
script:
- gitlab-qa Test::Instance::Smoke "${QA_IMAGE}" "${CI_ENVIRONMENT_URL}"
@@ -221,7 +183,6 @@ review-performance:
# This is needed so that manual jobs with needs don't block the pipeline.
# See https://gitlab.com/gitlab-org/gitlab/-/issues/199979.
dependencies: ["review-deploy"]
- allow_failure: true
before_script:
- export CI_ENVIRONMENT_URL="$(cat environment_url.txt)"
- echo "${CI_ENVIRONMENT_URL}"
@@ -237,6 +198,7 @@ review-performance:
- sitespeed-results/
reports:
performance: performance.json
+ expire_in: 31d
parallel-spec-reports:
extends:
@@ -244,7 +206,6 @@ parallel-spec-reports:
image: ruby:2.6-alpine
stage: post-qa
dependencies: ["review-qa-all"]
- allow_failure: true
variables:
NEW_PARALLEL_SPECS_REPORT: qa/report-new.html
BASE_ARTIFACT_URL: "${CI_PROJECT_URL}/-/jobs/${CI_JOB_ID}/artifacts/file/qa/"
@@ -263,17 +224,19 @@ parallel-spec-reports:
- qa/gitlab-qa-run-*
reports:
junit: qa/gitlab-qa-run-*/**/rspec-*.xml
+ expire_in: 31d
danger-review:
extends:
- .default-retry
- - .default-cache
+ - .yarn-cache
- .review:rules:danger
image: registry.gitlab.com/gitlab-org/gitlab-build-images:danger
stage: test
needs: []
script:
- - git version
- - node --version
- - yarn install --frozen-lockfile --cache-folder .yarn-cache --prefer-offline
+ - source scripts/utils.sh
+ - retry yarn install --frozen-lockfile
- danger --fail-on-errors=true --verbose
+ cache:
+ policy: pull
diff --git a/.gitlab/ci/rules.gitlab-ci.yml b/.gitlab/ci/rules.gitlab-ci.yml
index 79ae7823853..383aca0043b 100644
--- a/.gitlab/ci/rules.gitlab-ci.yml
+++ b/.gitlab/ci/rules.gitlab-ci.yml
@@ -16,6 +16,15 @@
.if-master-refs: &if-master-refs
if: '$CI_COMMIT_REF_NAME == "master"'
+.if-master-push: &if-master-push
+ if: '$CI_COMMIT_BRANCH == "master" && $CI_PIPELINE_SOURCE == "push"'
+
+.if-master-schedule-2-hourly: &if-master-schedule-2-hourly
+ if: '$CI_COMMIT_BRANCH == "master" && $CI_PIPELINE_SOURCE == "schedule" && $FREQUENCY == "2-hourly"'
+
+.if-master-schedule-nightly: &if-master-schedule-nightly
+ if: '$CI_COMMIT_BRANCH == "master" && $CI_PIPELINE_SOURCE == "schedule" && $FREQUENCY == "nightly"'
+
.if-auto-deploy-branches: &if-auto-deploy-branches
if: '$CI_COMMIT_BRANCH =~ /^\d+-\d+-auto-deploy-\d+$/'
@@ -25,8 +34,11 @@
.if-merge-request: &if-merge-request
if: '$CI_MERGE_REQUEST_IID'
-.if-nightly-master-schedule: &if-nightly-master-schedule
- if: '$NIGHTLY && $CI_COMMIT_REF_NAME == "master" && $CI_PIPELINE_SOURCE == "schedule"'
+.if-merge-request-title-as-if-foss: &if-merge-request-title-as-if-foss
+ if: '$CI_MERGE_REQUEST_TITLE =~ /RUN AS-IF-FOSS/'
+
+.if-security-merge-request: &if-security-merge-request
+ if: '$CI_PROJECT_NAMESPACE == "gitlab-org/security" && $CI_MERGE_REQUEST_IID'
.if-dot-com-gitlab-org-schedule: &if-dot-com-gitlab-org-schedule
if: '$CI_SERVER_HOST == "gitlab.com" && $CI_PROJECT_NAMESPACE == "gitlab-org" && $CI_PIPELINE_SOURCE == "schedule"'
@@ -37,9 +49,6 @@
.if-dot-com-gitlab-org-merge-request: &if-dot-com-gitlab-org-merge-request
if: '$CI_SERVER_HOST == "gitlab.com" && $CI_PROJECT_NAMESPACE == "gitlab-org" && $CI_MERGE_REQUEST_IID'
-.if-dot-com-gitlab-org-and-security-merge-request: &if-dot-com-gitlab-org-and-security-merge-request
- if: '$CI_SERVER_HOST == "gitlab.com" && $CI_PROJECT_NAMESPACE =~ /^gitlab-org($|\/security$)/ && $CI_MERGE_REQUEST_IID'
-
.if-dot-com-gitlab-org-and-security-tag: &if-dot-com-gitlab-org-and-security-tag
if: '$CI_SERVER_HOST == "gitlab.com" && $CI_PROJECT_NAMESPACE =~ /^gitlab-org($|\/security$)/ && $CI_COMMIT_TAG'
@@ -52,6 +61,10 @@
####################
# Changes patterns #
####################
+.ci-patterns: &ci-patterns
+ - ".gitlab-ci.yml"
+ - ".gitlab/ci/**/*"
+
.yaml-patterns: &yaml-patterns
- "**/*.yml"
@@ -77,7 +90,6 @@
- "{,ee/}fixtures/**/*"
- "{,ee/}rubocop/**/*"
- "{,ee/}spec/**/*"
- - "doc/README.md" # Some RSpec test rely on this file
.code-patterns: &code-patterns
- "{package.json,yarn.lock}"
@@ -120,7 +132,6 @@
- "{,ee/}fixtures/**/*"
- "{,ee/}rubocop/**/*"
- "{,ee/}spec/**/*"
- - "doc/README.md" # Some RSpec test rely on this file
.code-qa-patterns: &code-qa-patterns
- "{package.json,yarn.lock}"
@@ -162,7 +173,6 @@
- "{,ee/}fixtures/**/*"
- "{,ee/}rubocop/**/*"
- "{,ee/}spec/**/*"
- - "doc/README.md" # Some RSpec test rely on this file
# QA changes
- ".dockerignore"
- "qa/**/*"
@@ -173,7 +183,7 @@
.cache-repo:rules:
rules:
- <<: *if-cache-credentials-schedule
- when: on_success
+ allow_failure: true
#############
# CNG rules #
@@ -182,6 +192,7 @@
rules:
- <<: *if-dot-com-gitlab-org-and-security-tag
when: manual
+ allow_failure: true
######################
# Dev fixtures rules #
@@ -208,6 +219,7 @@
- <<: *if-dot-com-gitlab-org-merge-request
changes: *docs-patterns
when: manual
+ allow_failure: true
.docs:rules:docs-lint:
rules:
@@ -226,6 +238,7 @@
##################
# Frontend rules #
##################
+# This job only runs on `master` since it pushes to the cache.
.frontend:rules:gitlab-assets-compile-pull-push-cache:
rules:
- <<: *if-not-canonical-namespace
@@ -248,13 +261,14 @@
changes: *code-backstage-qa-patterns
when: on_success
+# This job only runs on `master` since it pushes to the cache.
.frontend:rules:compile-assets-pull-push-cache-as-if-foss:
rules:
- <<: *if-not-ee
when: never
- - <<: *if-master-refs
+ - <<: *if-master-push
changes: *code-backstage-qa-patterns
- when: on_success
+ - <<: *if-master-schedule-2-hourly
.frontend:rules:compile-assets-pull-cache:
rules:
@@ -266,9 +280,14 @@
rules:
- <<: *if-not-ee
when: never
- - <<: *if-default-refs
+ - <<: *if-master-push
changes: *code-backstage-qa-patterns
- when: on_success
+ - <<: *if-master-schedule-2-hourly
+ - <<: *if-security-merge-request
+ changes: *code-backstage-qa-patterns
+ - <<: *if-merge-request-title-as-if-foss
+ - <<: *if-merge-request
+ changes: *ci-patterns
.frontend:rules:default-frontend-jobs:
rules:
@@ -276,13 +295,27 @@
changes: *code-backstage-patterns
when: on_success
-.frontend:rules:default-frontend-jobs-no-foss:
+.frontend:rules:default-frontend-jobs-as-if-foss:
rules:
- <<: *if-not-ee
when: never
- - <<: *if-default-refs
+ - <<: *if-master-push
+ changes: *code-backstage-patterns
+ - <<: *if-master-schedule-2-hourly
+ - <<: *if-security-merge-request
+ changes: *code-backstage-patterns
+ - <<: *if-merge-request-title-as-if-foss
+ - <<: *if-merge-request
+ changes: *ci-patterns
+
+.frontend:rules:ee-mr-and-master-only:
+ rules:
+ - <<: *if-not-ee
+ when: never
+ - <<: *if-merge-request
+ changes: *code-backstage-patterns
+ - <<: *if-master-refs
changes: *code-backstage-patterns
- when: on_success
.frontend:rules:qa-frontend-node:
rules:
@@ -293,6 +326,15 @@
changes: *frontend-dependency-patterns
when: on_success
+.frontend:rules:qa-frontend-node-latest:
+ rules:
+ - <<: *if-master-refs
+ changes: *frontend-dependency-patterns
+ allow_failure: true
+ - <<: *if-merge-request
+ changes: *frontend-dependency-patterns
+ allow_failure: true
+
################
# Memory rules #
################
@@ -322,24 +364,33 @@
changes: *code-qa-patterns
when: on_success
-.qa:rules:ee-only:
+.qa:rules:as-if-foss:
rules:
- <<: *if-not-ee
when: never
- - <<: *if-default-refs
+ - <<: *if-master-push
changes: *code-qa-patterns
- when: on_success
+ - <<: *if-master-schedule-2-hourly
+ - <<: *if-security-merge-request
+ changes: *code-qa-patterns
+ - <<: *if-merge-request-title-as-if-foss
+ - <<: *if-merge-request
+ changes: *ci-patterns
.qa:rules:package-and-qa:
rules:
- <<: *if-dot-com-gitlab-org-merge-request
+ changes: *ci-patterns
+ allow_failure: true
+ - <<: *if-dot-com-gitlab-org-merge-request
changes: *qa-patterns
- when: on_success
+ allow_failure: true
- <<: *if-dot-com-gitlab-org-merge-request
changes: *code-patterns
when: manual
+ allow_failure: true
- <<: *if-dot-com-gitlab-org-schedule
- when: on_success
+ allow_failure: true
###############
# Rails rules #
@@ -348,50 +399,45 @@
rules:
- <<: *if-default-refs
changes: *code-backstage-patterns
- when: on_success
.rails:rules:default-refs-code-backstage-qa:
rules:
- <<: *if-default-refs
changes: *code-backstage-qa-patterns
- when: on_success
-
-.rails:rules:master-refs-code-backstage:
- rules:
- - <<: *if-master-refs
- changes: *code-backstage-patterns
- when: on_success
- - changes: [".gitlab/ci/rails.gitlab-ci.yml"]
-.rails:rules:nightly-master-refs-code-backstage:
+.rails:rules:ee-only:
rules:
- - <<: *if-nightly-master-schedule
+ - <<: *if-not-ee
+ when: never
+ - <<: *if-default-refs
changes: *code-backstage-patterns
- when: on_success
- - changes: [".gitlab/ci/rails.gitlab-ci.yml"]
-.rails:rules:nightly-master-refs-code-backstage-ee-only:
+.rails:rules:as-if-foss:
rules:
- <<: *if-not-ee
when: never
- - <<: *if-nightly-master-schedule
+ - <<: *if-master-push
changes: *code-backstage-patterns
- when: on_success
- - changes: [".gitlab/ci/rails.gitlab-ci.yml"]
+ - <<: *if-master-schedule-2-hourly
+ - <<: *if-security-merge-request
+ changes: *code-backstage-patterns
+ - <<: *if-merge-request-title-as-if-foss
+ - <<: *if-merge-request
+ changes: *ci-patterns
-.rails:rules:ee-only:
+.rails:rules:ee-mr-and-master-only:
rules:
- <<: *if-not-ee
when: never
- - <<: *if-default-refs
+ - <<: *if-merge-request
+ changes: *code-backstage-patterns
+ - <<: *if-master-refs
changes: *code-backstage-patterns
- when: on_success
.rails:rules:downtime_check:
rules:
- <<: *if-merge-request
changes: *code-backstage-patterns
- when: on_success
##################
# Releases rules #
@@ -414,6 +460,7 @@
# - <<: *if-master-refs # To be done in a later iteration: https://gitlab.com/gitlab-org/gitlab/issues/31160#note_278188255
- <<: *if-default-refs
changes: *code-backstage-patterns
+ allow_failure: true
.reports:rules:sast:
rules:
@@ -422,6 +469,7 @@
# - <<: *if-master-refs # To be done in a later iteration: https://gitlab.com/gitlab-org/gitlab/issues/31160#note_278188255
- <<: *if-default-refs
changes: *code-backstage-qa-patterns
+ allow_failure: true
.reports:rules:dependency_scanning:
rules:
@@ -430,6 +478,7 @@
# - <<: *if-master-refs # To be done in a later iteration: https://gitlab.com/gitlab-org/gitlab/issues/31160#note_278188255
- <<: *if-default-refs
changes: *code-backstage-qa-patterns
+ allow_failure: true
.reports:rules:dast:
rules:
@@ -437,10 +486,11 @@
when: never
- <<: *if-dot-com-gitlab-org-merge-request
changes: *frontend-patterns
- when: on_success
+ allow_failure: true
- <<: *if-dot-com-gitlab-org-merge-request
changes: *code-qa-patterns
when: manual
+ allow_failure: true
.reports:schedule-dast:
rules:
@@ -451,67 +501,86 @@
################
# Review rules #
################
-.review:rules:mr-and-schedule-auto:
+.review:rules:build-qa-image:
rules:
+ - <<: *if-not-ee
+ when: never
- <<: *if-dot-com-gitlab-org-merge-request
changes: *code-qa-patterns
- when: on_success
- <<: *if-dot-com-gitlab-org-schedule
- when: on_success
+
+.review:rules:review-build-cng:
+ rules:
+ - <<: *if-dot-com-gitlab-org-merge-request
+ changes: *ci-patterns
+ - <<: *if-dot-com-gitlab-org-merge-request
+ changes: *frontend-patterns
+ - <<: *if-dot-com-gitlab-org-merge-request
+ changes: *code-qa-patterns
+ when: manual
+ allow_failure: true
+ - <<: *if-dot-com-gitlab-org-schedule
.review:rules:mr-and-schedule-auto-if-frontend-manual-otherwise:
rules:
+ - <<: *if-not-ee
+ when: never
+ - <<: *if-dot-com-gitlab-org-merge-request
+ changes: *ci-patterns
- <<: *if-dot-com-gitlab-org-merge-request
changes: *frontend-patterns
- when: on_success
+ allow_failure: true
- <<: *if-dot-com-gitlab-org-merge-request
changes: *code-qa-patterns
when: manual
allow_failure: true
- <<: *if-dot-com-gitlab-org-schedule
- when: on_success
+ allow_failure: true
-.review:rules:mr-only-auto:
+.review:rules:review-stop-failed-deployment:
rules:
+ - <<: *if-not-ee
+ when: never
- <<: *if-dot-com-gitlab-org-merge-request
changes: *code-qa-patterns
- when: on_success
-.review:rules:mr-only-auto-if-frontend-manual-otherwise:
+.review:rules:review-qa-smoke:
rules:
+ - <<: *if-not-ee
+ when: never
+ - <<: *if-dot-com-gitlab-org-merge-request
+ changes: *ci-patterns
- <<: *if-dot-com-gitlab-org-merge-request
changes: *frontend-patterns
- when: on_success
+ allow_failure: true
- <<: *if-dot-com-gitlab-org-merge-request
changes: *code-qa-patterns
when: manual
+ allow_failure: true
.review:rules:mr-only-manual:
rules:
+ - <<: *if-not-ee
+ when: never
- <<: *if-dot-com-gitlab-org-merge-request
changes: *code-qa-patterns
when: manual
+ allow_failure: true
.review:rules:review-cleanup:
rules:
+ - <<: *if-not-ee
+ when: never
- <<: *if-dot-com-gitlab-org-merge-request
changes: *code-qa-patterns
when: manual
+ allow_failure: true
- <<: *if-dot-com-gitlab-org-schedule
- when: on_success
-
-.review:rules:review-gcp-cleanup:
- rules:
- - <<: *if-dot-com-gitlab-org-merge-request
- changes: *code-qa-patterns
- when: manual
- - <<: *if-dot-com-gitlab-org-schedule
- when: on_success
+ allow_failure: true
.review:rules:danger:
rules:
- if: '$DANGER_GITLAB_API_TOKEN && $CI_MERGE_REQUEST_IID'
- when: on_success
###############
# Setup rules #
@@ -527,10 +596,11 @@
.setup:rules:dont-interrupt-me:
rules:
- <<: *if-master-or-tag
- when: on_success
+ allow_failure: true
- <<: *if-auto-deploy-branches
- when: on_success
+ allow_failure: true
- when: manual
+ allow_failure: true
.setup:rules:gitlab_git_test:
rules:
@@ -557,9 +627,13 @@
.test-metadata:rules:update-tests-metadata:
rules:
+ - <<: *if-not-ee
+ when: never
+ - changes:
+ - ".gitlab/ci/test-metadata.gitlab-ci.yml"
+ - "scripts/rspec_helpers.sh"
- <<: *if-dot-com-ee-schedule
changes: *code-backstage-patterns
- when: on_success
##############
# YAML rules #
diff --git a/.gitlab/ci/setup.gitlab-ci.yml b/.gitlab/ci/setup.gitlab-ci.yml
index e79f3939bc7..9be495f1ef2 100644
--- a/.gitlab/ci/setup.gitlab-ci.yml
+++ b/.gitlab/ci/setup.gitlab-ci.yml
@@ -7,7 +7,7 @@ cache gems:
- .default-before_script
- .setup:rules:cache-gems
stage: test
- needs: ["setup-test-env pg11"]
+ needs: ["setup-test-env"]
variables:
SETUP_DB: "false"
script:
@@ -15,6 +15,7 @@ cache gems:
artifacts:
paths:
- vendor/cache
+ expire_in: 31d
.minimal-job:
extends:
@@ -26,7 +27,6 @@ dont-interrupt-me:
stage: sync
image: alpine:edge
interruptible: false
- allow_failure: true
variables:
GIT_STRATEGY: none
script:
diff --git a/.gitlab/ci/test-metadata.gitlab-ci.yml b/.gitlab/ci/test-metadata.gitlab-ci.yml
index cda6d996bdb..65cce76fc48 100644
--- a/.gitlab/ci/test-metadata.gitlab-ci.yml
+++ b/.gitlab/ci/test-metadata.gitlab-ci.yml
@@ -31,6 +31,19 @@ update-tests-metadata:
- .tests-metadata-state
- .test-metadata:rules:update-tests-metadata
stage: post-test
+ dependencies:
+ - setup-test-env
+ - rspec migration pg11
+ - rspec unit pg11
+ - rspec integration pg11
+ - rspec system pg11
+ - rspec-ee migration pg11
+ - rspec-ee unit pg11
+ - rspec-ee integration pg11
+ - rspec-ee system pg11
+ - rspec-ee unit pg11 geo
+ - rspec-ee integration pg11 geo
+ - rspec-ee system pg11 geo
cache:
policy: push
script:
diff --git a/.gitlab/issue_templates/Feature proposal.md b/.gitlab/issue_templates/Feature proposal.md
index 4b98b4e7ac6..45b5fc85cd1 100644
--- a/.gitlab/issue_templates/Feature proposal.md
+++ b/.gitlab/issue_templates/Feature proposal.md
@@ -1,4 +1,4 @@
-<!-- The first three sections: "Problem to solve", "Intended users" and "Proposal", are strongly recommended, while the rest of the sections can be filled out during the problem validation or breakdown phase. However, keep in mind that providing complete and relevant information early helps our product team validate the problem and start working on a solution. -->
+<!-- The first four sections: "Problem to solve", "Intended users", "User experience goal", and "Proposal", are strongly recommended, while the rest of the sections can be filled out during the problem validation or breakdown phase. However, keep in mind that providing complete and relevant information early helps our product team validate the problem and start working on a solution. -->
### Problem to solve
@@ -10,33 +10,44 @@
Personas are described at https://about.gitlab.com/handbook/marketing/product-marketing/roles-personas/
-* [Rachel (Release Manager)](https://about.gitlab.com/handbook/marketing/product-marketing/roles-personas/#rachel-release-manager)
+* [Cameron (Compliance Manager)](https://about.gitlab.com/handbook/marketing/product-marketing/roles-personas/#cameron-compliance-manager)
* [Parker (Product Manager)](https://about.gitlab.com/handbook/marketing/product-marketing/roles-personas/#parker-product-manager)
* [Delaney (Development Team Lead)](https://about.gitlab.com/handbook/marketing/product-marketing/roles-personas/#delaney-development-team-lead)
-* [Sasha (Software Developer)](https://about.gitlab.com/handbook/marketing/product-marketing/roles-personas/#sasha-software-developer)
* [Presley (Product Designer)](https://about.gitlab.com/handbook/marketing/product-marketing/roles-personas/#presley-product-designer)
+* [Sasha (Software Developer)](https://about.gitlab.com/handbook/marketing/product-marketing/roles-personas/#sasha-software-developer)
* [Devon (DevOps Engineer)](https://about.gitlab.com/handbook/marketing/product-marketing/roles-personas/#devon-devops-engineer)
* [Sidney (Systems Administrator)](https://about.gitlab.com/handbook/marketing/product-marketing/roles-personas/#sidney-systems-administrator)
* [Sam (Security Analyst)](https://about.gitlab.com/handbook/marketing/product-marketing/roles-personas/#sam-security-analyst)
-* [Dana (Data Analyst)](https://about.gitlab.com/handbook/marketing/product-marketing/roles-personas/#dana-data-analyst)
+* [Rachel (Release Manager)](https://about.gitlab.com/handbook/marketing/product-marketing/roles-personas/#rachel-release-manager)
+* [Alex (Security Operations Engineer)](https://about.gitlab.com/handbook/marketing/product-marketing/roles-personas/#alex-security-operations-engineer)
* [Simone (Software Engineer in Test)](https://about.gitlab.com/handbook/marketing/product-marketing/roles-personas/#simone-software-engineer-in-test)
-* [Allison (Application Ops)](https://about.gitlab.com/handbook/marketing/product-marketing/roles-personas/#allison-application-ops) -->
+* [Allison (Application Ops)](https://about.gitlab.com/handbook/marketing/product-marketing/roles-personas/#allison-application-ops)
+* [Priyanka (Platform Engineer)](https://about.gitlab.com/handbook/marketing/product-marketing/roles-personas/#priyanka-platform-engineer)
+* [Dana (Data Analyst)](https://about.gitlab.com/handbook/marketing/product-marketing/roles-personas/#dana-data-analyst)
+-->
-### Further details
+### User experience goal
+
+<!-- What is the single user experience workflow this problem addresses?
+For example, "The user should be able to use the UI/API/.gitlab-ci.yml with GitLab to <perform a specific task>"
+https://about.gitlab.com/handbook/engineering/ux/ux-research-training/user-story-mapping/ -->
-<!-- Include use cases, benefits, goals, or any other details that will help us understand the problem better. -->
### Proposal
<!-- How are we going to solve the problem? Try to include the user journey! https://about.gitlab.com/handbook/journeys/#user-journey -->
+### Further details
+
+<!-- Include use cases, benefits, goals, or any other details that will help us understand the problem better. -->
+
### Permissions and Security
<!-- What permissions are required to perform the described actions? Are they consistent with the existing permissions as documented for users, groups, and projects as appropriate? Is the proposed behavior consistent between the UI, API, and other access methods (e.g. email replies)?-->
### Documentation
-<!-- See the Feature Change Documentation Workflow https://docs.gitlab.com/ee/development/documentation/feature-change-workflow.html
+<!-- See the Feature Change Documentation Workflow https://docs.gitlab.com/ee/development/documentation/workflow.html#for-a-product-change
* Add all known Documentation Requirements in this section. See https://docs.gitlab.com/ee/development/documentation/feature-change-workflow.html#documentation-requirements
* If this feature requires changing permissions, update the permissions document. See https://docs.gitlab.com/ee/user/permissions.html -->
diff --git a/.gitlab/issue_templates/Productivity Improvement.md b/.gitlab/issue_templates/Productivity Improvement.md
index 79e1461392e..974f11f6da3 100644
--- a/.gitlab/issue_templates/Productivity Improvement.md
+++ b/.gitlab/issue_templates/Productivity Improvement.md
@@ -1,7 +1,7 @@
-## What is the productivity problem to solve?
+## What is the GitLab engineering productivity problem to solve?
<!--
-Please describe the productivity problem that needs to be solved backed by charts from
+Please describe the engineering productivity problem that needs to be solved backed by charts from
https://about.gitlab.com/handbook/engineering/quality/engineering-productivity-team/#engineering-productivity-team-metrics.
-->
@@ -37,4 +37,3 @@ after the implementation is merged/deployed/released.
- Otherwise, create a new "Productivity Improvement" issue. You can re-use the description from this issue, but obviously another solution should be chosen this time.
/label ~"Engineering Productivity" ~meta
-/cc @gl-quality/eng-prod
diff --git a/.gitlab/issue_templates/Security developer workflow.md b/.gitlab/issue_templates/Security developer workflow.md
index 2c80360d32d..695f0167ad4 100644
--- a/.gitlab/issue_templates/Security developer workflow.md
+++ b/.gitlab/issue_templates/Security developer workflow.md
@@ -36,7 +36,8 @@ After your merge request has been approved according to our [approval guidelines
## Documentation and final details
- [ ] Ensure the [Links section](#links) is completed.
-- [ ] Find out the versions affected (the Git history of the files affected may help you with this) and add them to the [details section](#details)
+- [ ] Add the GitLab [versions](https://gitlab.com/gitlab-org/release/docs/-/blob/master/general/security/developer.md#versions-affected) and editions affected to the [details section](#details)
+ * The Git history of the files affected may help you associate the issue with a [release](https://about.gitlab.com/releases/)
- [ ] Fill in any upgrade notes that users may need to take into account in the [details section](#details)
- [ ] Add Yes/No and further details if needed to the migration and settings columns in the [details section](#details)
- [ ] Add the nickname of the external user who found the issue (and/or HackerOne profile) to the Thanks row in the [details section](#details)
@@ -56,6 +57,7 @@ After your merge request has been approved according to our [approval guidelines
| Description | Details | Further details|
| -------- | -------- | -------- |
| Versions affected | X.Y | |
+| GitLab EE only | Yes/No | |
| Upgrade notes | | |
| GitLab Settings updated | Yes/No| |
| Migration required | Yes/No | |
diff --git a/.gitlab/issue_templates/Technical Evaluation.md b/.gitlab/issue_templates/Technical Evaluation.md
index f603d88a764..533a1343820 100644
--- a/.gitlab/issue_templates/Technical Evaluation.md
+++ b/.gitlab/issue_templates/Technical Evaluation.md
@@ -7,7 +7,7 @@
### Tasks to Evaluate
-<!-- Outline the tasks with issues that you need evaluate as a part of the implementation issue -->
+<!-- Outline the tasks with issues that you need to evaluate as a part of the implementation issue -->
- [ ] Determine feasibility of the feature
- [ ] Create issue for implementation or update existing implementation issue description with implementation proposal
@@ -18,7 +18,7 @@
### Risks and Implementation Considerations
-<!-- Idenitfy any risks found in the research, whether this is performance, impacts to other functionality or other bugs -->
+<!-- Identify any risks found in the research, whether this is performance, impacts to other functionality or other bugs -->
### Team
diff --git a/.gitlab/merge_request_templates/Documentation.md b/.gitlab/merge_request_templates/Documentation.md
index 901228ee77e..72bfd2cdec4 100644
--- a/.gitlab/merge_request_templates/Documentation.md
+++ b/.gitlab/merge_request_templates/Documentation.md
@@ -13,12 +13,25 @@
<!-- Link related issues below. Insert the issue link or reference after the word "Closes" if merging this should automatically close it. -->
-## Author's checklist
+## Author's checklist (required)
- [ ] Follow the [Documentation Guidelines](https://docs.gitlab.com/ee/development/documentation/) and [Style Guide](https://docs.gitlab.com/ee/development/documentation/styleguide.html).
-- [ ] If applicable, update the [permissions table](https://docs.gitlab.com/ee/user/permissions.html).
+- If you have `developer` access or higher (for example, GitLab team members or [Core Team](https://about.gitlab.com/community/core-team/) members)
+ - [ ] Apply the ~documentation label, plus:
+ - The corresponding DevOps stage and group label, if applicable.
+ - ~"development guidelines" when changing docs under `doc/development/*`, `CONTRIBUTING.md`, or `README.md`.
+ - ~"development guidelines" and ~"Documentation guidelines" when changing docs under `development/documentation/*`.
+ - ~"development guidelines" and ~"Description templates (.gitlab/\*)" when creating/updating issue and MR description templates.
+ - [ ] Assign the [designated Technical Writer](https://about.gitlab.com/handbook/engineering/ux/technical-writing/#assignments).
+
+When applicable:
+
+- [ ] Update the [permissions table](https://docs.gitlab.com/ee/user/permissions.html).
- [ ] Link docs to and from the higher-level index page, plus other related docs where helpful.
-- [ ] Apply the ~documentation label.
+- [ ] Add [GitLab's version history note(s)](https://docs.gitlab.com/ee/development/documentation/styleguide.html#text-for-documentation-requiring-version-text).
+- [ ] Add the [product tier badge](https://docs.gitlab.com/ee/development/documentation/styleguide.html#product-badges).
+- [ ] Add/update the [feature flag section](https://docs.gitlab.com/ee/development/documentation/feature_flags.html).
+- [ ] If you're changing document headings, search `doc/*`, `app/views/*`, and `ee/app/views/*` for old headings replacing with the new ones to [avoid broken anchors](https://docs.gitlab.com/ee/development/documentation/styleguide.html#anchor-links).
## Review checklist
@@ -30,7 +43,9 @@ All reviewers can help ensure accuracy, clarity, completeness, and adherence to
**2. Technical Writer**
-* [ ] Optional: Technical writer review. If not requested for this MR, must be scheduled post-merge. To request for this MR, assign the writer listed for the applicable [DevOps stage](https://about.gitlab.com/handbook/product/categories/#devops-stages).
+- [ ] Optional: Technical writer review. If not requested for this MR, must be scheduled post-merge. To request for this MR, assign the writer listed for the applicable [DevOps stage](https://about.gitlab.com/handbook/product/categories/#devops-stages).
+ - [ ] Add ~"Technical Writing" and `docs::` workflow label.
+ - [ ] Add ~docs-only when the only files changed are under `doc/*`.
**3. Maintainer**
diff --git a/.gitlab/merge_request_templates/Security Release.md b/.gitlab/merge_request_templates/Security Release.md
index af3839a96a4..f852bebae95 100644
--- a/.gitlab/merge_request_templates/Security Release.md
+++ b/.gitlab/merge_request_templates/Security Release.md
@@ -19,7 +19,7 @@ See [the general developer security release guidelines](https://gitlab.com/gitla
- [ ] A [CHANGELOG entry](https://docs.gitlab.com/ee/development/changelog.html) is added without a `merge_request` value, with `type` set to `security`
- [ ] Assign to a reviewer and maintainer, per our [Code Review process].
- [ ] For the MR targeting `master`:
- - [ ] Ping appsec team member who created the issue and ask for a non-blocking review with `Please review this MR`.
+ - [ ] Ask for a non-blocking review from the AppSec team member associated to the issue in the [Canonical repository](https://gitlab.com/gitlab-org/gitlab). If you're unsure who to ping, ask on `#sec-appsec` Slack channel.
- [ ] Ensure it's approved according to our [Approval Guidelines].
- [ ] Merge request _must not_ close the corresponding security issue, _unless_ it targets `master`.
diff --git a/.haml-lint_todo.yml b/.haml-lint_todo.yml
index 1417f37f76d..a79bba24edb 100644
--- a/.haml-lint_todo.yml
+++ b/.haml-lint_todo.yml
@@ -1,431 +1,390 @@
# This configuration was generated by
# `haml-lint --auto-gen-config`
-# on 2020-03-04 13:16:29 +0100 using Haml-Lint version 0.34.0.
+# on 2020-04-20 07:11:26 +0000 using Haml-Lint version 0.34.0.
# The point is for the user to remove these configuration records
# one by one as the lints are removed from the code base.
# Note that changes in the inspected code, or installation of new
# versions of Haml-Lint, may require this file to be generated again.
linters:
- # Offense count: 1646
+
+ # Offense count: 1552
NoPlainNodes:
enabled: true
exclude:
- - 'app/views/admin/abuse_reports/_abuse_report.html.haml'
- - 'app/views/admin/abuse_reports/index.html.haml'
- - 'app/views/admin/appearances/_form.html.haml'
- - 'app/views/admin/application_settings/_abuse.html.haml'
- - 'app/views/admin/application_settings/_diff_limits.html.haml'
- - 'app/views/admin/application_settings/_gitaly.html.haml'
- - 'app/views/admin/application_settings/_influx.html.haml'
- - 'app/views/admin/application_settings/_ip_limits.html.haml'
- - 'app/views/admin/application_settings/_performance.html.haml'
- - 'app/views/admin/application_settings/_plantuml.html.haml'
- - 'app/views/admin/application_settings/_prometheus.html.haml'
- - 'app/views/admin/application_settings/_realtime.html.haml'
- - 'app/views/admin/application_settings/_repository_check.html.haml'
- - 'app/views/admin/application_settings/_signin.html.haml'
- - 'app/views/admin/application_settings/_signup.html.haml'
- - 'app/views/admin/application_settings/_spam.html.haml'
- - 'app/views/admin/application_settings/_terminal.html.haml'
- - 'app/views/admin/application_settings/_usage.html.haml'
- - 'app/views/admin/application_settings/_visibility_and_access.html.haml'
- - 'app/views/admin/applications/_delete_form.html.haml'
- - 'app/views/admin/applications/_form.html.haml'
- - 'app/views/admin/applications/edit.html.haml'
- - 'app/views/admin/applications/index.html.haml'
- - 'app/views/admin/applications/new.html.haml'
- - 'app/views/admin/applications/show.html.haml'
- - 'app/views/admin/background_jobs/show.html.haml'
- - 'app/views/admin/broadcast_messages/index.html.haml'
- - 'app/views/admin/dashboard/index.html.haml'
- - 'app/views/admin/deploy_keys/new.html.haml'
- - 'app/views/admin/health_check/show.html.haml'
- - 'app/views/admin/hook_logs/_index.html.haml'
- - 'app/views/admin/hook_logs/show.html.haml'
- - 'app/views/admin/hooks/_form.html.haml'
- - 'app/views/admin/hooks/edit.html.haml'
- - 'app/views/admin/labels/_form.html.haml'
- - 'app/views/admin/logs/show.html.haml'
- - 'app/views/admin/projects/_projects.html.haml'
- - 'app/views/admin/projects/show.html.haml'
- - 'app/views/admin/requests_profiles/index.html.haml'
- - 'app/views/admin/runners/_runner.html.haml'
- - 'app/views/admin/runners/index.html.haml'
- - 'app/views/admin/runners/show.html.haml'
- - 'app/views/admin/services/_form.html.haml'
- - 'app/views/admin/services/index.html.haml'
- - 'app/views/admin/spam_logs/_spam_log.html.haml'
- - 'app/views/admin/spam_logs/index.html.haml'
- - 'app/views/admin/system_info/show.html.haml'
- - 'app/views/admin/users/_access_levels.html.haml'
- - 'app/views/admin/users/_form.html.haml'
- - 'app/views/admin/users/_head.html.haml'
- - 'app/views/admin/users/_profile.html.haml'
- - 'app/views/admin/users/_projects.html.haml'
- - 'app/views/admin/users/new.html.haml'
- - 'app/views/admin/users/projects.html.haml'
- - 'app/views/admin/users/show.html.haml'
- - 'app/views/clusters/clusters/_cluster.html.haml'
- - 'app/views/clusters/clusters/_form.html.haml'
- - 'app/views/clusters/clusters/_gcp_signup_offer_banner.html.haml'
- - 'app/views/clusters/clusters/gcp/_form.html.haml'
- - 'app/views/clusters/clusters/new.html.haml'
- - 'app/views/dashboard/milestones/index.html.haml'
- - 'app/views/dashboard/projects/_blank_state_admin_welcome.html.haml'
- - 'app/views/dashboard/projects/_blank_state_welcome.html.haml'
- - 'app/views/dashboard/todos/_todo.html.haml'
- - 'app/views/dashboard/todos/index.html.haml'
- - 'app/views/devise/confirmations/almost_there.haml'
- - 'app/views/devise/mailer/_confirmation_instructions_account.html.haml'
- - 'app/views/devise/mailer/_confirmation_instructions_secondary.html.haml'
- - 'app/views/devise/mailer/email_changed.html.haml'
- - 'app/views/devise/mailer/password_change.html.haml'
- - 'app/views/devise/mailer/reset_password_instructions.html.haml'
- - 'app/views/devise/mailer/unlock_instructions.html.haml'
- - 'app/views/devise/passwords/edit.html.haml'
- - 'app/views/devise/sessions/_new_base.html.haml'
- - 'app/views/devise/sessions/_new_crowd.html.haml'
- - 'app/views/devise/sessions/_new_ldap.html.haml'
- - 'app/views/devise/sessions/new.html.haml'
- - 'app/views/devise/sessions/two_factor.html.haml'
- - 'app/views/devise/shared/_omniauth_box.html.haml'
- - 'app/views/devise/shared/_sign_in_link.html.haml'
- - 'app/views/devise/shared/_tabs_normal.html.haml'
- - 'app/views/discussions/_discussion.html.haml'
- - 'app/views/discussions/_headline.html.haml'
- - 'app/views/discussions/_notes.html.haml'
- - 'app/views/doorkeeper/applications/_delete_form.html.haml'
- - 'app/views/doorkeeper/authorized_applications/_delete_form.html.haml'
- - 'app/views/errors/encoding.html.haml'
- - 'app/views/errors/git_not_found.html.haml'
- - 'app/views/errors/omniauth_error.html.haml'
- - 'app/views/errors/precondition_failed.html.haml'
- - 'app/views/events/_commit.html.haml'
- - 'app/views/events/_event_push.atom.haml'
- - 'app/views/events/event/_push.html.haml'
- - 'app/views/groups/_create_chat_team.html.haml'
- - 'app/views/groups/_group_admin_settings.html.haml'
- - 'app/views/groups/labels/edit.html.haml'
- - 'app/views/groups/labels/new.html.haml'
- - 'app/views/groups/milestones/edit.html.haml'
- - 'app/views/groups/milestones/index.html.haml'
- - 'app/views/groups/milestones/new.html.haml'
- - 'app/views/groups/projects.html.haml'
- - 'app/views/groups/runners/edit.html.haml'
- - 'app/views/groups/settings/_advanced.html.haml'
- - 'app/views/groups/settings/_lfs.html.haml'
- - 'app/views/help/_shortcuts.html.haml'
- - 'app/views/help/index.html.haml'
- - 'app/views/help/instance_configuration.html.haml'
- - 'app/views/help/instance_configuration/_gitlab_ci.html.haml'
- - 'app/views/help/instance_configuration/_gitlab_pages.html.haml'
- - 'app/views/help/ui.html.haml'
- - 'app/views/import/bitbucket/status.html.haml'
- - 'app/views/import/bitbucket_server/status.html.haml'
- - 'app/views/invites/show.html.haml'
- - 'app/views/layouts/_mailer.html.haml'
- - 'app/views/layouts/header/_default.html.haml'
- - 'app/views/layouts/header/_new_dropdown.haml'
- - 'app/views/layouts/mailer/devise.html.haml'
- - 'app/views/layouts/notify.html.haml'
- - 'app/views/notify/_failed_builds.html.haml'
- - 'app/views/notify/_reassigned_issuable_email.html.haml'
- - 'app/views/notify/_removal_notification.html.haml'
- - 'app/views/notify/_successful_pipeline.html.haml'
- - 'app/views/notify/autodevops_disabled_email.html.haml'
- - 'app/views/notify/changed_milestone_email.html.haml'
- - 'app/views/notify/import_issues_csv_email.html.haml'
- - 'app/views/notify/issue_moved_email.html.haml'
- - 'app/views/notify/member_access_denied_email.html.haml'
- - 'app/views/notify/member_invite_accepted_email.html.haml'
- - 'app/views/notify/member_invited_email.html.haml'
- - 'app/views/notify/new_gpg_key_email.html.haml'
- - 'app/views/notify/new_mention_in_issue_email.html.haml'
- - 'app/views/notify/new_ssh_key_email.html.haml'
- - 'app/views/notify/new_user_email.html.haml'
- - 'app/views/notify/pages_domain_disabled_email.html.haml'
- - 'app/views/notify/pages_domain_enabled_email.html.haml'
- - 'app/views/notify/pages_domain_verification_failed_email.html.haml'
- - 'app/views/notify/pages_domain_verification_succeeded_email.html.haml'
- - 'app/views/notify/pipeline_failed_email.html.haml'
- - 'app/views/notify/project_was_exported_email.html.haml'
- - 'app/views/notify/project_was_moved_email.html.haml'
- - 'app/views/notify/project_was_not_exported_email.html.haml'
- - 'app/views/notify/push_to_merge_request_email.html.haml'
- - 'app/views/notify/remote_mirror_update_failed_email.html.haml'
- - 'app/views/notify/removed_milestone_issue_email.html.haml'
- - 'app/views/notify/removed_milestone_merge_request_email.html.haml'
- - 'app/views/notify/repository_push_email.html.haml'
- - 'app/views/profiles/chat_names/_chat_name.html.haml'
- - 'app/views/profiles/chat_names/index.html.haml'
- - 'app/views/profiles/chat_names/new.html.haml'
- - 'app/views/profiles/keys/_key.html.haml'
- - 'app/views/profiles/show.html.haml'
- - 'app/views/projects/_bitbucket_import_modal.html.haml'
- - 'app/views/projects/_customize_workflow.html.haml'
- - 'app/views/projects/_deletion_failed.html.haml'
- - 'app/views/projects/_fork_suggestion.html.haml'
- - 'app/views/projects/_gitlab_import_modal.html.haml'
- - 'app/views/projects/_home_panel.html.haml'
- - 'app/views/projects/_import_project_pane.html.haml'
- - 'app/views/projects/_issuable_by_email.html.haml'
- - 'app/views/projects/_md_preview.html.haml'
- - 'app/views/projects/_readme.html.haml'
- - 'app/views/projects/artifacts/_artifact.html.haml'
- - 'app/views/projects/artifacts/_tree_file.html.haml'
- - 'app/views/projects/artifacts/browse.html.haml'
- - 'app/views/projects/blame/_age_map_legend.html.haml'
- - 'app/views/projects/blame/show.html.haml'
- - 'app/views/projects/blob/_editor.html.haml'
- - 'app/views/projects/blob/_header_content.html.haml'
- - 'app/views/projects/blob/_new_dir.html.haml'
- - 'app/views/projects/blob/_remove.html.haml'
- - 'app/views/projects/blob/_render_error.html.haml'
- - 'app/views/projects/blob/_upload.html.haml'
- - 'app/views/projects/blob/edit.html.haml'
- - 'app/views/projects/blob/new.html.haml'
- - 'app/views/projects/blob/preview.html.haml'
- - 'app/views/projects/blob/viewers/_empty.html.haml'
- - 'app/views/projects/blob/viewers/_stl.html.haml'
- - 'app/views/projects/branches/_branch.html.haml'
- - 'app/views/projects/branches/_commit.html.haml'
- - 'app/views/projects/branches/_delete_protected_modal.html.haml'
- - 'app/views/projects/branches/new.html.haml'
- - 'app/views/projects/ci/builds/_build.html.haml'
- - 'app/views/projects/ci/lints/_create.html.haml'
- - 'app/views/projects/commit/_change.html.haml'
- - 'app/views/projects/commits/_commit.html.haml'
- - 'app/views/projects/commits/_inline_commit.html.haml'
- - 'app/views/projects/compare/_form.html.haml'
- - 'app/views/projects/compare/index.html.haml'
- - 'app/views/projects/cycle_analytics/_empty_stage.html.haml'
- - 'app/views/projects/cycle_analytics/_no_access.html.haml'
- - 'app/views/projects/cycle_analytics/_overview.html.haml'
- - 'app/views/projects/cycle_analytics/show.html.haml'
- - 'app/views/projects/deploy_keys/_form.html.haml'
- - 'app/views/projects/deploy_keys/_index.html.haml'
- - 'app/views/projects/deploy_keys/edit.html.haml'
- - 'app/views/projects/deployments/_deployment.html.haml'
- - 'app/views/projects/diffs/_file_header.html.haml'
- - 'app/views/projects/diffs/_replaced_image_diff.html.haml'
- - 'app/views/projects/diffs/_stats.html.haml'
- - 'app/views/projects/empty.html.haml'
- - 'app/views/projects/environments/show.html.haml'
- - 'app/views/projects/forks/error.html.haml'
- - 'app/views/projects/generic_commit_statuses/_generic_commit_status.html.haml'
- - 'app/views/projects/hook_logs/_index.html.haml'
- - 'app/views/projects/hook_logs/show.html.haml'
- - 'app/views/projects/hooks/edit.html.haml'
- - 'app/views/projects/imports/new.html.haml'
- - 'app/views/projects/imports/show.html.haml'
- - 'app/views/projects/issues/_issue.html.haml'
- - 'app/views/projects/issues/_new_branch.html.haml'
- - 'app/views/projects/issues/import_csv/_modal.html.haml'
- - 'app/views/projects/issues/show.html.haml'
- - 'app/views/projects/jobs/_header.html.haml'
- - 'app/views/projects/jobs/_table.html.haml'
- - 'app/views/projects/jobs/index.html.haml'
- - 'app/views/projects/labels/edit.html.haml'
- - 'app/views/projects/labels/new.html.haml'
- - 'app/views/projects/mattermosts/_no_teams.html.haml'
- - 'app/views/projects/mattermosts/_team_selection.html.haml'
- - 'app/views/projects/mattermosts/new.html.haml'
- - 'app/views/projects/merge_requests/_commits.html.haml'
- - 'app/views/projects/merge_requests/_discussion.html.haml'
- - 'app/views/projects/merge_requests/_how_to_merge.html.haml'
- - 'app/views/projects/merge_requests/_merge_request.html.haml'
- - 'app/views/projects/merge_requests/_mr_title.html.haml'
- - 'app/views/projects/merge_requests/conflicts/_commit_stats.html.haml'
- - 'app/views/projects/merge_requests/conflicts/_file_actions.html.haml'
- - 'app/views/projects/merge_requests/conflicts/_submit_form.html.haml'
- - 'app/views/projects/merge_requests/conflicts/components/_diff_file_editor.html.haml'
- - 'app/views/projects/merge_requests/conflicts/components/_inline_conflict_lines.html.haml'
- - 'app/views/projects/merge_requests/conflicts/show.html.haml'
- - 'app/views/projects/merge_requests/creations/_diffs.html.haml'
- - 'app/views/projects/merge_requests/creations/_new_compare.html.haml'
- - 'app/views/projects/merge_requests/creations/_new_submit.html.haml'
- - 'app/views/projects/merge_requests/diffs/_different_base.html.haml'
- - 'app/views/projects/merge_requests/diffs/_diffs.html.haml'
- - 'app/views/projects/merge_requests/diffs/_version_controls.html.haml'
- - 'app/views/projects/merge_requests/invalid.html.haml'
- - 'app/views/projects/merge_requests/widget/open/_error.html.haml'
- - 'app/views/projects/mirrors/_regenerate_public_ssh_key_confirm_modal.html.haml'
- - 'app/views/projects/mirrors/_ssh_host_keys.html.haml'
- - 'app/views/projects/no_repo.html.haml'
- - 'app/views/projects/pipeline_schedules/_pipeline_schedule.html.haml'
- - 'app/views/projects/pipelines/_info.html.haml'
- - 'app/views/projects/protected_branches/shared/_dropdown.html.haml'
- - 'app/views/projects/protected_branches/shared/_index.html.haml'
- - 'app/views/projects/protected_branches/shared/_matching_branch.html.haml'
- - 'app/views/projects/protected_branches/shared/_protected_branch.html.haml'
- - 'app/views/projects/protected_branches/show.html.haml'
- - 'app/views/projects/protected_tags/shared/_create_protected_tag.html.haml'
- - 'app/views/projects/protected_tags/shared/_dropdown.html.haml'
- - 'app/views/projects/protected_tags/shared/_index.html.haml'
- - 'app/views/projects/protected_tags/shared/_matching_tag.html.haml'
- - 'app/views/projects/protected_tags/shared/_protected_tag.html.haml'
- - 'app/views/projects/protected_tags/shared/_tags_list.html.haml'
- - 'app/views/projects/protected_tags/show.html.haml'
- - 'app/views/projects/registry/repositories/_tag.html.haml'
- - 'app/views/projects/repositories/_feed.html.haml'
- - 'app/views/projects/runners/_shared_runners.html.haml'
- - 'app/views/projects/runners/edit.html.haml'
- - 'app/views/projects/services/_form.html.haml'
- - 'app/views/projects/services/mattermost_slash_commands/_detailed_help.html.haml'
- - 'app/views/projects/services/mattermost_slash_commands/_help.html.haml'
- - 'app/views/projects/services/prometheus/_metrics.html.haml'
- - 'app/views/projects/services/slack_slash_commands/_help.html.haml'
- - 'app/views/projects/settings/ci_cd/_badge.html.haml'
- - 'app/views/projects/settings/ci_cd/_form.html.haml'
- - 'app/views/projects/stage/_stage.html.haml'
- - 'app/views/projects/tags/index.html.haml'
- - 'app/views/projects/tags/new.html.haml'
- - 'app/views/projects/tags/releases/edit.html.haml'
- - 'app/views/projects/tree/_tree_row.html.haml'
- - 'app/views/projects/tree/_truncated_notice_tree_row.html.haml'
- - 'app/views/projects/triggers/_form.html.haml'
- - 'app/views/projects/triggers/_index.html.haml'
- - 'app/views/projects/triggers/_trigger.html.haml'
- - 'app/views/projects/triggers/edit.html.haml'
- - 'app/views/projects/wikis/_pages_wiki_page.html.haml'
- - 'app/views/projects/wikis/edit.html.haml'
- - 'app/views/projects/wikis/history.html.haml'
- - 'app/views/search/results/_issue.html.haml'
- - 'app/views/search/results/_note.html.haml'
- - 'app/views/search/results/_snippet_blob.html.haml'
- - 'app/views/search/results/_snippet_title.html.haml'
- - 'app/views/shared/_auto_devops_implicitly_enabled_banner.html.haml'
- - 'app/views/shared/_commit_message_container.html.haml'
- - 'app/views/shared/_confirm_fork_modal.html.haml'
- - 'app/views/shared/_confirm_modal.html.haml'
- - 'app/views/shared/_delete_label_modal.html.haml'
- - 'app/views/shared/_group_form.html.haml'
- - 'app/views/shared/_group_tips.html.haml'
- - 'app/views/shared/_milestone_expired.html.haml'
- - 'app/views/shared/_no_password.html.haml'
- - 'app/views/shared/_no_ssh.html.haml'
- - 'app/views/shared/_ping_consent.html.haml'
- - 'app/views/shared/_project_limit.html.haml'
- - 'app/views/shared/boards/components/_board.html.haml'
- - 'app/views/shared/boards/components/_sidebar.html.haml'
- - 'app/views/shared/boards/components/sidebar/_due_date.html.haml'
- - 'app/views/shared/boards/components/sidebar/_labels.html.haml'
- - 'app/views/shared/boards/components/sidebar/_milestone.html.haml'
- - 'app/views/shared/deploy_tokens/_revoke_modal.html.haml'
- - 'app/views/shared/hook_logs/_content.html.haml'
- - 'app/views/shared/issuable/_assignees.html.haml'
- - 'app/views/shared/issuable/_board_create_list_dropdown.html.haml'
- - 'app/views/shared/issuable/_bulk_update_sidebar.html.haml'
- - 'app/views/shared/issuable/_close_reopen_report_toggle.html.haml'
- - 'app/views/shared/issuable/_form.html.haml'
- - 'app/views/shared/issuable/_search_bar.html.haml'
- - 'app/views/shared/issuable/_sidebar.html.haml'
- - 'app/views/shared/issuable/form/_default_templates.html.haml'
- - 'app/views/shared/issuable/form/_issue_assignee.html.haml'
- - 'app/views/shared/issuable/form/_template_selector.html.haml'
- - 'app/views/shared/issuable/form/_title.html.haml'
- - 'app/views/shared/labels/_form.html.haml'
- - 'app/views/shared/members/_member.html.haml'
- - 'app/views/shared/milestones/_form_dates.html.haml'
- - 'app/views/shared/milestones/_issuable.html.haml'
- - 'app/views/shared/milestones/_milestone.html.haml'
- - 'app/views/shared/milestones/_sidebar.html.haml'
- - 'app/views/shared/milestones/_top.html.haml'
- - 'app/views/shared/notes/_hints.html.haml'
- - 'app/views/shared/notes/_note.html.haml'
- - 'app/views/shared/notifications/_button.html.haml'
- - 'app/views/shared/notifications/_custom_notifications.html.haml'
- - 'app/views/shared/notifications/_new_button.html.haml'
- - 'app/views/shared/runners/_runner_description.html.haml'
- - 'app/views/shared/runners/show.html.haml'
- - 'app/views/shared/snippets/_header.html.haml'
- - 'app/views/shared/snippets/_snippet.html.haml'
- - 'app/views/shared/web_hooks/_form.html.haml'
- - 'app/views/shared/web_hooks/_hook.html.haml'
- - 'app/views/u2f/_authenticate.html.haml'
- - 'app/views/u2f/_register.html.haml'
- - 'app/views/users/_deletion_guidance.html.haml'
- - 'ee/app/views/admin/_namespace_plan_info.html.haml'
- - 'ee/app/views/admin/application_settings/_templates.html.haml'
- - 'ee/app/views/admin/audit_logs/index.html.haml'
- - 'ee/app/views/admin/emails/show.html.haml'
- - 'ee/app/views/admin/geo/nodes/edit.html.haml'
- - 'ee/app/views/admin/geo/nodes/new.html.haml'
- - 'ee/app/views/admin/geo/projects/_registry_failed.html.haml'
- - 'ee/app/views/admin/geo/projects/_registry_never.html.haml'
- - 'ee/app/views/admin/licenses/_upload_trial_license.html.haml'
- - 'ee/app/views/admin/licenses/new.html.haml'
- - 'ee/app/views/admin/licenses/show.html.haml'
- - 'ee/app/views/admin/monitoring/ee/_nav.html.haml'
- - 'ee/app/views/admin/projects/_shared_runner_status.html.haml'
- - 'ee/app/views/admin/users/_auditor_access_level_radio.html.haml'
- - 'ee/app/views/admin/users/_auditor_user_badge.html.haml'
- - 'ee/app/views/admin/users/_limits.html.haml'
- - 'ee/app/views/admin/users/_user_detail_note.html.haml'
- - 'ee/app/views/dashboard/projects/_blank_state_ee_trial.html.haml'
- - 'ee/app/views/errors/kerberos_denied.html.haml'
- - 'ee/app/views/groups/ee/_settings_nav.html.haml'
- - 'ee/app/views/groups/epics/_epic.html.haml'
- - 'ee/app/views/groups/group_members/_ldap_sync.html.haml'
- - 'ee/app/views/groups/group_members/_sync_button.html.haml'
- - 'ee/app/views/groups/hooks/edit.html.haml'
- - 'ee/app/views/groups/ldap_group_links/index.html.haml'
- - 'ee/app/views/jira_connect/subscriptions/index.html.haml'
- - 'ee/app/views/layouts/jira_connect.html.haml'
- - 'ee/app/views/layouts/nav/ee/admin/_new_monitoring_sidebar.html.haml'
- - 'ee/app/views/layouts/service_desk.html.haml'
- - 'ee/app/views/ldap_group_links/_form.html.haml'
- - 'ee/app/views/ldap_group_links/_ldap_group_link.html.haml'
- - 'ee/app/views/ldap_group_links/_ldap_group_links.html.haml'
- - 'ee/app/views/ldap_group_links/_ldap_group_links_show.html.haml'
- - 'ee/app/views/ldap_group_links/_ldap_group_links_synchronizations.html.haml'
- - 'ee/app/views/namespaces/_shared_runner_status.html.haml'
- - 'ee/app/views/namespaces/_shared_runners_minutes_setting.html.haml'
- - 'ee/app/views/namespaces/pipelines_quota/_extra_shared_runners_minutes_quota.html.haml'
- - 'ee/app/views/namespaces/pipelines_quota/_list.haml'
- - 'ee/app/views/notify/approved_merge_request_email.html.haml'
- - 'ee/app/views/notify/epic_status_changed_email.html.haml'
- - 'ee/app/views/notify/issues_csv_email.html.haml'
- - 'ee/app/views/notify/new_review_email.html.haml'
- - 'ee/app/views/notify/send_admin_notification.html.haml'
- - 'ee/app/views/notify/send_unsubscribed_notification.html.haml'
- - 'ee/app/views/notify/unapproved_merge_request_email.html.haml'
- - 'ee/app/views/oauth/geo_auth/error.html.haml'
- - 'ee/app/views/profiles/pipeline_quota/index.haml'
- - 'ee/app/views/projects/blob/_owners.html.haml'
- - 'ee/app/views/projects/commits/_mirror_status.html.haml'
- - 'ee/app/views/projects/issues/_issue_weight.html.haml'
- - 'ee/app/views/projects/issues/export_csv/_modal.html.haml'
- - 'ee/app/views/projects/jobs/_shared_runner_limit_warning.html.haml'
- - 'ee/app/views/projects/merge_requests/_approvals_count.html.haml'
- - 'ee/app/views/projects/merge_requests/widget/open/_geo.html.haml'
- - 'ee/app/views/projects/mirrors/_mirrored_repositories_count.html.haml'
- - 'ee/app/views/projects/protected_branches/_update_protected_branch.html.haml'
- - 'ee/app/views/projects/protected_branches/ee/_create_protected_branch.html.haml'
- - 'ee/app/views/projects/protected_branches/ee/_dropdown.html.haml'
- - 'ee/app/views/projects/protected_tags/_protected_tag_extra_create_access_levels.haml'
- - 'ee/app/views/projects/protected_tags/ee/_create_protected_tag.html.haml'
- - 'ee/app/views/projects/push_rules/_index.html.haml'
- - 'ee/app/views/projects/services/gitlab_slack_application/_help.html.haml'
- - 'ee/app/views/projects/services/gitlab_slack_application/_slack_integration_form.html.haml'
- - 'ee/app/views/projects/settings/slacks/edit.html.haml'
- - 'ee/app/views/shared/_additional_email_text.html.haml'
- - 'ee/app/views/shared/_mirror_update_button.html.haml'
- - 'ee/app/views/shared/boards/components/_list_weight.html.haml'
- - 'ee/app/views/shared/epic/_search_bar.html.haml'
- - 'ee/app/views/shared/issuable/_approvals.html.haml'
- - 'ee/app/views/shared/issuable/_board_create_list_dropdown.html.haml'
- - 'ee/app/views/shared/issuable/_filter_weight.html.haml'
- - 'ee/app/views/shared/members/ee/_ldap_tag.html.haml'
- - 'ee/app/views/shared/members/ee/_override_member_buttons.html.haml'
- - 'ee/app/views/shared/members/ee/_sso_badge.html.haml'
- - 'ee/app/views/shared/milestones/_burndown.html.haml'
- - 'ee/app/views/shared/milestones/_weight.html.haml'
- - 'ee/app/views/shared/promotions/_promote_burndown_charts.html.haml'
- - 'ee/app/views/shared/promotions/_promote_csv_export.html.haml'
- - 'ee/app/views/shared/promotions/_promote_issue_weights.html.haml'
- - 'ee/app/views/shared/promotions/_promote_repository_features.html.haml'
- - 'ee/app/views/shared/promotions/_promote_servicedesk.html.haml'
- - 'ee/app/views/shared/push_rules/_form.html.haml'
- - 'ee/app/views/unsubscribes/show.html.haml'
+ - "app/views/admin/abuse_reports/_abuse_report.html.haml"
+ - "app/views/admin/abuse_reports/index.html.haml"
+ - "app/views/admin/appearances/_form.html.haml"
+ - "app/views/admin/application_settings/_abuse.html.haml"
+ - "app/views/admin/application_settings/_diff_limits.html.haml"
+ - "app/views/admin/application_settings/_gitaly.html.haml"
+ - "app/views/admin/application_settings/_ip_limits.html.haml"
+ - "app/views/admin/application_settings/_performance.html.haml"
+ - "app/views/admin/application_settings/_plantuml.html.haml"
+ - "app/views/admin/application_settings/_prometheus.html.haml"
+ - "app/views/admin/application_settings/_realtime.html.haml"
+ - "app/views/admin/application_settings/_repository_check.html.haml"
+ - "app/views/admin/application_settings/_signin.html.haml"
+ - "app/views/admin/application_settings/_signup.html.haml"
+ - "app/views/admin/application_settings/_spam.html.haml"
+ - "app/views/admin/application_settings/_terminal.html.haml"
+ - "app/views/admin/application_settings/_usage.html.haml"
+ - "app/views/admin/application_settings/_visibility_and_access.html.haml"
+ - "app/views/admin/applications/_delete_form.html.haml"
+ - "app/views/admin/applications/_form.html.haml"
+ - "app/views/admin/applications/edit.html.haml"
+ - "app/views/admin/applications/index.html.haml"
+ - "app/views/admin/applications/new.html.haml"
+ - "app/views/admin/applications/show.html.haml"
+ - "app/views/admin/background_jobs/show.html.haml"
+ - "app/views/admin/broadcast_messages/index.html.haml"
+ - "app/views/admin/dashboard/index.html.haml"
+ - "app/views/admin/deploy_keys/new.html.haml"
+ - "app/views/admin/health_check/show.html.haml"
+ - "app/views/admin/hook_logs/_index.html.haml"
+ - "app/views/admin/hook_logs/show.html.haml"
+ - "app/views/admin/hooks/_form.html.haml"
+ - "app/views/admin/hooks/edit.html.haml"
+ - "app/views/admin/logs/show.html.haml"
+ - "app/views/admin/projects/_projects.html.haml"
+ - "app/views/admin/projects/show.html.haml"
+ - "app/views/admin/requests_profiles/index.html.haml"
+ - "app/views/admin/runners/_runner.html.haml"
+ - "app/views/admin/runners/index.html.haml"
+ - "app/views/admin/runners/show.html.haml"
+ - "app/views/admin/services/_form.html.haml"
+ - "app/views/admin/services/index.html.haml"
+ - "app/views/admin/spam_logs/_spam_log.html.haml"
+ - "app/views/admin/spam_logs/index.html.haml"
+ - "app/views/admin/system_info/show.html.haml"
+ - "app/views/admin/users/_access_levels.html.haml"
+ - "app/views/admin/users/_form.html.haml"
+ - "app/views/admin/users/_head.html.haml"
+ - "app/views/admin/users/_profile.html.haml"
+ - "app/views/admin/users/_projects.html.haml"
+ - "app/views/admin/users/new.html.haml"
+ - "app/views/admin/users/projects.html.haml"
+ - "app/views/admin/users/show.html.haml"
+ - "app/views/clusters/clusters/_cluster.html.haml"
+ - "app/views/clusters/clusters/new.html.haml"
+ - "app/views/dashboard/milestones/index.html.haml"
+ - "app/views/dashboard/projects/_blank_state_admin_welcome.html.haml"
+ - "app/views/dashboard/projects/_blank_state_welcome.html.haml"
+ - "app/views/dashboard/todos/_todo.html.haml"
+ - "app/views/dashboard/todos/index.html.haml"
+ - "app/views/devise/confirmations/almost_there.haml"
+ - "app/views/devise/mailer/_confirmation_instructions_account.html.haml"
+ - "app/views/devise/mailer/_confirmation_instructions_secondary.html.haml"
+ - "app/views/devise/mailer/email_changed.html.haml"
+ - "app/views/devise/mailer/password_change.html.haml"
+ - "app/views/devise/mailer/reset_password_instructions.html.haml"
+ - "app/views/devise/mailer/unlock_instructions.html.haml"
+ - "app/views/devise/passwords/edit.html.haml"
+ - "app/views/devise/sessions/_new_base.html.haml"
+ - "app/views/devise/sessions/_new_crowd.html.haml"
+ - "app/views/devise/sessions/_new_ldap.html.haml"
+ - "app/views/devise/sessions/new.html.haml"
+ - "app/views/devise/sessions/two_factor.html.haml"
+ - "app/views/devise/shared/_omniauth_box.html.haml"
+ - "app/views/devise/shared/_sign_in_link.html.haml"
+ - "app/views/devise/shared/_tabs_normal.html.haml"
+ - "app/views/discussions/_discussion.html.haml"
+ - "app/views/discussions/_headline.html.haml"
+ - "app/views/discussions/_notes.html.haml"
+ - "app/views/doorkeeper/applications/_delete_form.html.haml"
+ - "app/views/doorkeeper/authorized_applications/_delete_form.html.haml"
+ - "app/views/errors/encoding.html.haml"
+ - "app/views/errors/git_not_found.html.haml"
+ - "app/views/errors/omniauth_error.html.haml"
+ - "app/views/errors/precondition_failed.html.haml"
+ - "app/views/events/_event_push.atom.haml"
+ - "app/views/events/event/_push.html.haml"
+ - "app/views/groups/_create_chat_team.html.haml"
+ - "app/views/groups/_group_admin_settings.html.haml"
+ - "app/views/groups/labels/edit.html.haml"
+ - "app/views/groups/labels/new.html.haml"
+ - "app/views/groups/milestones/edit.html.haml"
+ - "app/views/groups/milestones/index.html.haml"
+ - "app/views/groups/milestones/new.html.haml"
+ - "app/views/groups/projects.html.haml"
+ - "app/views/groups/runners/edit.html.haml"
+ - "app/views/groups/settings/_advanced.html.haml"
+ - "app/views/groups/settings/_lfs.html.haml"
+ - "app/views/help/_shortcuts.html.haml"
+ - "app/views/help/index.html.haml"
+ - "app/views/help/instance_configuration.html.haml"
+ - "app/views/help/instance_configuration/_gitlab_ci.html.haml"
+ - "app/views/help/instance_configuration/_gitlab_pages.html.haml"
+ - "app/views/help/ui.html.haml"
+ - "app/views/import/bitbucket/status.html.haml"
+ - "app/views/import/bitbucket_server/status.html.haml"
+ - "app/views/invites/show.html.haml"
+ - "app/views/layouts/_mailer.html.haml"
+ - "app/views/layouts/header/_default.html.haml"
+ - "app/views/layouts/header/_new_dropdown.haml"
+ - "app/views/layouts/notify.html.haml"
+ - "app/views/notify/_failed_builds.html.haml"
+ - "app/views/notify/_reassigned_issuable_email.html.haml"
+ - "app/views/notify/_removal_notification.html.haml"
+ - "app/views/notify/_successful_pipeline.html.haml"
+ - "app/views/notify/autodevops_disabled_email.html.haml"
+ - "app/views/notify/changed_milestone_email.html.haml"
+ - "app/views/notify/import_issues_csv_email.html.haml"
+ - "app/views/notify/issue_moved_email.html.haml"
+ - "app/views/notify/member_access_denied_email.html.haml"
+ - "app/views/notify/member_invite_accepted_email.html.haml"
+ - "app/views/notify/member_invited_email.html.haml"
+ - "app/views/notify/new_gpg_key_email.html.haml"
+ - "app/views/notify/new_mention_in_issue_email.html.haml"
+ - "app/views/notify/new_ssh_key_email.html.haml"
+ - "app/views/notify/new_user_email.html.haml"
+ - "app/views/notify/pages_domain_disabled_email.html.haml"
+ - "app/views/notify/pages_domain_enabled_email.html.haml"
+ - "app/views/notify/pages_domain_verification_failed_email.html.haml"
+ - "app/views/notify/pages_domain_verification_succeeded_email.html.haml"
+ - "app/views/notify/pipeline_failed_email.html.haml"
+ - "app/views/notify/project_was_exported_email.html.haml"
+ - "app/views/notify/project_was_moved_email.html.haml"
+ - "app/views/notify/project_was_not_exported_email.html.haml"
+ - "app/views/notify/push_to_merge_request_email.html.haml"
+ - "app/views/notify/remote_mirror_update_failed_email.html.haml"
+ - "app/views/notify/removed_milestone_issue_email.html.haml"
+ - "app/views/notify/removed_milestone_merge_request_email.html.haml"
+ - "app/views/notify/repository_push_email.html.haml"
+ - "app/views/profiles/chat_names/_chat_name.html.haml"
+ - "app/views/profiles/chat_names/index.html.haml"
+ - "app/views/profiles/chat_names/new.html.haml"
+ - "app/views/projects/_bitbucket_import_modal.html.haml"
+ - "app/views/projects/_customize_workflow.html.haml"
+ - "app/views/projects/_deletion_failed.html.haml"
+ - "app/views/projects/_fork_suggestion.html.haml"
+ - "app/views/projects/_gitlab_import_modal.html.haml"
+ - "app/views/projects/_home_panel.html.haml"
+ - "app/views/projects/_import_project_pane.html.haml"
+ - "app/views/projects/_issuable_by_email.html.haml"
+ - "app/views/projects/_md_preview.html.haml"
+ - "app/views/projects/_readme.html.haml"
+ - "app/views/projects/artifacts/_artifact.html.haml"
+ - "app/views/projects/artifacts/_tree_file.html.haml"
+ - "app/views/projects/artifacts/browse.html.haml"
+ - "app/views/projects/blame/_age_map_legend.html.haml"
+ - "app/views/projects/blame/show.html.haml"
+ - "app/views/projects/blob/_editor.html.haml"
+ - "app/views/projects/blob/_header_content.html.haml"
+ - "app/views/projects/blob/_remove.html.haml"
+ - "app/views/projects/blob/_render_error.html.haml"
+ - "app/views/projects/blob/edit.html.haml"
+ - "app/views/projects/blob/new.html.haml"
+ - "app/views/projects/blob/preview.html.haml"
+ - "app/views/projects/blob/viewers/_empty.html.haml"
+ - "app/views/projects/blob/viewers/_stl.html.haml"
+ - "app/views/projects/branches/_branch.html.haml"
+ - "app/views/projects/branches/_delete_protected_modal.html.haml"
+ - "app/views/projects/branches/new.html.haml"
+ - "app/views/projects/ci/builds/_build.html.haml"
+ - "app/views/projects/ci/lints/_create.html.haml"
+ - "app/views/projects/compare/_form.html.haml"
+ - "app/views/projects/compare/index.html.haml"
+ - "app/views/projects/cycle_analytics/_empty_stage.html.haml"
+ - "app/views/projects/cycle_analytics/_no_access.html.haml"
+ - "app/views/projects/cycle_analytics/_overview.html.haml"
+ - "app/views/projects/cycle_analytics/show.html.haml"
+ - "app/views/projects/deploy_keys/_form.html.haml"
+ - "app/views/projects/deploy_keys/_index.html.haml"
+ - "app/views/projects/deploy_keys/edit.html.haml"
+ - "app/views/projects/deployments/_deployment.html.haml"
+ - "app/views/projects/diffs/_file_header.html.haml"
+ - "app/views/projects/diffs/_replaced_image_diff.html.haml"
+ - "app/views/projects/diffs/_stats.html.haml"
+ - "app/views/projects/empty.html.haml"
+ - "app/views/projects/generic_commit_statuses/_generic_commit_status.html.haml"
+ - "app/views/projects/hook_logs/_index.html.haml"
+ - "app/views/projects/hook_logs/show.html.haml"
+ - "app/views/projects/hooks/edit.html.haml"
+ - "app/views/projects/imports/new.html.haml"
+ - "app/views/projects/imports/show.html.haml"
+ - "app/views/projects/issues/_new_branch.html.haml"
+ - "app/views/projects/issues/import_csv/_modal.html.haml"
+ - "app/views/projects/issues/show.html.haml"
+ - "app/views/projects/jobs/_header.html.haml"
+ - "app/views/projects/jobs/_table.html.haml"
+ - "app/views/projects/jobs/index.html.haml"
+ - "app/views/projects/labels/edit.html.haml"
+ - "app/views/projects/labels/new.html.haml"
+ - "app/views/projects/mattermosts/_no_teams.html.haml"
+ - "app/views/projects/mattermosts/_team_selection.html.haml"
+ - "app/views/projects/mattermosts/new.html.haml"
+ - "app/views/projects/merge_requests/_commits.html.haml"
+ - "app/views/projects/merge_requests/_discussion.html.haml"
+ - "app/views/projects/merge_requests/_how_to_merge.html.haml"
+ - "app/views/projects/merge_requests/_mr_title.html.haml"
+ - "app/views/projects/merge_requests/conflicts/_commit_stats.html.haml"
+ - "app/views/projects/merge_requests/conflicts/_file_actions.html.haml"
+ - "app/views/projects/merge_requests/conflicts/_submit_form.html.haml"
+ - "app/views/projects/merge_requests/conflicts/components/_diff_file_editor.html.haml"
+ - "app/views/projects/merge_requests/conflicts/components/_inline_conflict_lines.html.haml"
+ - "app/views/projects/merge_requests/conflicts/show.html.haml"
+ - "app/views/projects/merge_requests/creations/_diffs.html.haml"
+ - "app/views/projects/merge_requests/creations/_new_compare.html.haml"
+ - "app/views/projects/merge_requests/creations/_new_submit.html.haml"
+ - "app/views/projects/merge_requests/diffs/_different_base.html.haml"
+ - "app/views/projects/merge_requests/diffs/_diffs.html.haml"
+ - "app/views/projects/merge_requests/diffs/_version_controls.html.haml"
+ - "app/views/projects/merge_requests/invalid.html.haml"
+ - "app/views/projects/merge_requests/widget/open/_error.html.haml"
+ - "app/views/projects/mirrors/_regenerate_public_ssh_key_confirm_modal.html.haml"
+ - "app/views/projects/mirrors/_ssh_host_keys.html.haml"
+ - "app/views/projects/no_repo.html.haml"
+ - "app/views/projects/pipeline_schedules/_pipeline_schedule.html.haml"
+ - "app/views/projects/pipelines/_info.html.haml"
+ - "app/views/projects/protected_branches/shared/_dropdown.html.haml"
+ - "app/views/projects/protected_branches/shared/_index.html.haml"
+ - "app/views/projects/protected_branches/shared/_matching_branch.html.haml"
+ - "app/views/projects/protected_branches/shared/_protected_branch.html.haml"
+ - "app/views/projects/protected_branches/show.html.haml"
+ - "app/views/projects/protected_tags/shared/_create_protected_tag.html.haml"
+ - "app/views/projects/protected_tags/shared/_dropdown.html.haml"
+ - "app/views/projects/protected_tags/shared/_index.html.haml"
+ - "app/views/projects/protected_tags/shared/_matching_tag.html.haml"
+ - "app/views/projects/protected_tags/shared/_protected_tag.html.haml"
+ - "app/views/projects/protected_tags/shared/_tags_list.html.haml"
+ - "app/views/projects/protected_tags/show.html.haml"
+ - "app/views/projects/registry/repositories/_tag.html.haml"
+ - "app/views/projects/repositories/_feed.html.haml"
+ - "app/views/projects/runners/_shared_runners.html.haml"
+ - "app/views/projects/runners/edit.html.haml"
+ - "app/views/projects/services/mattermost_slash_commands/_detailed_help.html.haml"
+ - "app/views/projects/services/mattermost_slash_commands/_help.html.haml"
+ - "app/views/projects/services/prometheus/_metrics.html.haml"
+ - "app/views/projects/services/slack_slash_commands/_help.html.haml"
+ - "app/views/projects/settings/ci_cd/_badge.html.haml"
+ - "app/views/projects/settings/ci_cd/_form.html.haml"
+ - "app/views/projects/tags/index.html.haml"
+ - "app/views/projects/tags/releases/edit.html.haml"
+ - "app/views/projects/tree/_tree_row.html.haml"
+ - "app/views/projects/tree/_truncated_notice_tree_row.html.haml"
+ - "app/views/projects/triggers/_form.html.haml"
+ - "app/views/projects/triggers/_index.html.haml"
+ - "app/views/projects/triggers/_trigger.html.haml"
+ - "app/views/projects/triggers/edit.html.haml"
+ - "app/views/projects/wikis/_pages_wiki_page.html.haml"
+ - "app/views/search/results/_issue.html.haml"
+ - "app/views/search/results/_note.html.haml"
+ - "app/views/search/results/_snippet_blob.html.haml"
+ - "app/views/search/results/_snippet_title.html.haml"
+ - "app/views/shared/_auto_devops_implicitly_enabled_banner.html.haml"
+ - "app/views/shared/_commit_message_container.html.haml"
+ - "app/views/shared/_delete_label_modal.html.haml"
+ - "app/views/shared/_group_form.html.haml"
+ - "app/views/shared/_group_tips.html.haml"
+ - "app/views/shared/_milestone_expired.html.haml"
+ - "app/views/shared/_no_password.html.haml"
+ - "app/views/shared/_ping_consent.html.haml"
+ - "app/views/shared/_project_limit.html.haml"
+ - "app/views/shared/boards/components/_board.html.haml"
+ - "app/views/shared/boards/components/_sidebar.html.haml"
+ - "app/views/shared/boards/components/sidebar/_due_date.html.haml"
+ - "app/views/shared/boards/components/sidebar/_labels.html.haml"
+ - "app/views/shared/boards/components/sidebar/_milestone.html.haml"
+ - "app/views/shared/hook_logs/_content.html.haml"
+ - "app/views/shared/issuable/_assignees.html.haml"
+ - "app/views/shared/issuable/_board_create_list_dropdown.html.haml"
+ - "app/views/shared/issuable/_close_reopen_report_toggle.html.haml"
+ - "app/views/shared/issuable/_form.html.haml"
+ - "app/views/shared/issuable/_search_bar.html.haml"
+ - "app/views/shared/issuable/_sidebar.html.haml"
+ - "app/views/shared/issuable/form/_default_templates.html.haml"
+ - "app/views/shared/issuable/form/_issue_assignee.html.haml"
+ - "app/views/shared/issuable/form/_template_selector.html.haml"
+ - "app/views/shared/issuable/form/_title.html.haml"
+ - "app/views/shared/labels/_form.html.haml"
+ - "app/views/shared/members/_member.html.haml"
+ - "app/views/shared/milestones/_form_dates.html.haml"
+ - "app/views/shared/milestones/_issuable.html.haml"
+ - "app/views/shared/milestones/_milestone.html.haml"
+ - "app/views/shared/milestones/_sidebar.html.haml"
+ - "app/views/shared/milestones/_top.html.haml"
+ - "app/views/shared/notes/_hints.html.haml"
+ - "app/views/shared/notifications/_button.html.haml"
+ - "app/views/shared/notifications/_new_button.html.haml"
+ - "app/views/shared/runners/_runner_description.html.haml"
+ - "app/views/shared/runners/show.html.haml"
+ - "app/views/shared/snippets/_header.html.haml"
+ - "app/views/shared/snippets/_snippet.html.haml"
+ - "app/views/shared/web_hooks/_form.html.haml"
+ - "app/views/shared/web_hooks/_hook.html.haml"
+ - "app/views/u2f/_authenticate.html.haml"
+ - "app/views/u2f/_register.html.haml"
+ - "app/views/users/_deletion_guidance.html.haml"
+ - "ee/app/views/admin/_namespace_plan_info.html.haml"
+ - "ee/app/views/admin/application_settings/_templates.html.haml"
+ - "ee/app/views/admin/audit_logs/index.html.haml"
+ - "ee/app/views/admin/emails/show.html.haml"
+ - "ee/app/views/admin/geo/projects/_registry_failed.html.haml"
+ - "ee/app/views/admin/geo/projects/_registry_never.html.haml"
+ - "ee/app/views/admin/licenses/_upload_trial_license.html.haml"
+ - "ee/app/views/admin/licenses/new.html.haml"
+ - "ee/app/views/admin/licenses/show.html.haml"
+ - "ee/app/views/admin/monitoring/ee/_nav.html.haml"
+ - "ee/app/views/admin/projects/_shared_runner_status.html.haml"
+ - "ee/app/views/admin/users/_auditor_access_level_radio.html.haml"
+ - "ee/app/views/admin/users/_auditor_user_badge.html.haml"
+ - "ee/app/views/admin/users/_limits.html.haml"
+ - "ee/app/views/admin/users/_user_detail_note.html.haml"
+ - "ee/app/views/dashboard/projects/_blank_state_ee_trial.html.haml"
+ - "ee/app/views/errors/kerberos_denied.html.haml"
+ - "ee/app/views/groups/ee/_settings_nav.html.haml"
+ - "ee/app/views/groups/group_members/_ldap_sync.html.haml"
+ - "ee/app/views/groups/group_members/_sync_button.html.haml"
+ - "ee/app/views/groups/hooks/edit.html.haml"
+ - "ee/app/views/groups/ldap_group_links/index.html.haml"
+ - "ee/app/views/jira_connect/subscriptions/index.html.haml"
+ - "ee/app/views/layouts/jira_connect.html.haml"
+ - "ee/app/views/layouts/nav/ee/admin/_new_monitoring_sidebar.html.haml"
+ - "ee/app/views/layouts/service_desk.html.haml"
+ - "ee/app/views/ldap_group_links/_form.html.haml"
+ - "ee/app/views/ldap_group_links/_ldap_group_link.html.haml"
+ - "ee/app/views/ldap_group_links/_ldap_group_links.html.haml"
+ - "ee/app/views/ldap_group_links/_ldap_group_links_show.html.haml"
+ - "ee/app/views/ldap_group_links/_ldap_group_links_synchronizations.html.haml"
+ - "ee/app/views/namespaces/_shared_runner_status.html.haml"
+ - "ee/app/views/namespaces/_shared_runners_minutes_setting.html.haml"
+ - "ee/app/views/namespaces/pipelines_quota/_extra_shared_runners_minutes_quota.html.haml"
+ - "ee/app/views/namespaces/pipelines_quota/_list.haml"
+ - "ee/app/views/notify/approved_merge_request_email.html.haml"
+ - "ee/app/views/notify/epic_status_changed_email.html.haml"
+ - "ee/app/views/notify/new_review_email.html.haml"
+ - "ee/app/views/notify/send_admin_notification.html.haml"
+ - "ee/app/views/notify/send_unsubscribed_notification.html.haml"
+ - "ee/app/views/notify/unapproved_merge_request_email.html.haml"
+ - "ee/app/views/oauth/geo_auth/error.html.haml"
+ - "ee/app/views/projects/commits/_mirror_status.html.haml"
+ - "ee/app/views/projects/jobs/_shared_runner_limit_warning.html.haml"
+ - "ee/app/views/projects/merge_requests/_approvals_count.html.haml"
+ - "ee/app/views/projects/merge_requests/widget/open/_geo.html.haml"
+ - "ee/app/views/projects/mirrors/_mirrored_repositories_count.html.haml"
+ - "ee/app/views/projects/protected_branches/_update_protected_branch.html.haml"
+ - "ee/app/views/projects/protected_branches/ee/_create_protected_branch.html.haml"
+ - "ee/app/views/projects/protected_branches/ee/_dropdown.html.haml"
+ - "ee/app/views/projects/protected_tags/_protected_tag_extra_create_access_levels.haml"
+ - "ee/app/views/projects/protected_tags/ee/_create_protected_tag.html.haml"
+ - "ee/app/views/projects/push_rules/_index.html.haml"
+ - "ee/app/views/projects/services/gitlab_slack_application/_help.html.haml"
+ - "ee/app/views/projects/services/gitlab_slack_application/_slack_integration_form.html.haml"
+ - "ee/app/views/projects/settings/slacks/edit.html.haml"
+ - "ee/app/views/shared/_mirror_update_button.html.haml"
+ - "ee/app/views/shared/boards/components/_list_weight.html.haml"
+ - "ee/app/views/shared/epic/_search_bar.html.haml"
+ - "ee/app/views/shared/issuable/_approvals.html.haml"
+ - "ee/app/views/shared/issuable/_board_create_list_dropdown.html.haml"
+ - "ee/app/views/shared/issuable/_filter_weight.html.haml"
+ - "ee/app/views/shared/members/ee/_ldap_tag.html.haml"
+ - "ee/app/views/shared/members/ee/_override_member_buttons.html.haml"
+ - "ee/app/views/shared/members/ee/_sso_badge.html.haml"
+ - "ee/app/views/shared/milestones/_burndown.html.haml"
+ - "ee/app/views/shared/milestones/_weight.html.haml"
+ - "ee/app/views/shared/promotions/_promote_issue_weights.html.haml"
+ - "ee/app/views/shared/promotions/_promote_repository_features.html.haml"
+ - "ee/app/views/shared/promotions/_promote_servicedesk.html.haml"
+ - "ee/app/views/shared/push_rules/_form.html.haml"
+ - "ee/app/views/unsubscribes/show.html.haml"
diff --git a/.markdownlint.json b/.markdownlint.json
index ce319756c5f..88e3d5f7759 100644
--- a/.markdownlint.json
+++ b/.markdownlint.json
@@ -45,6 +45,8 @@
"DevOps",
"Elasticsearch",
"Facebook",
+ "GDK",
+ "Geo",
"Git LFS",
"git-annex",
"Git",
@@ -59,9 +61,11 @@
"GitLab Shell",
"GitLab Workhorse",
"GitLab",
+ "Gitleaks",
"Gmail",
"Google",
"Grafana",
+ "Gzip",
"Helm",
"HipChat",
"Ingress",
@@ -99,6 +103,7 @@
"PostgreSQL",
"Prometheus",
"Puma",
+ "puma-worker-killer",
"Python",
"Rake",
"Redis",
@@ -113,8 +118,10 @@
"Shibboleth",
"Slack",
"SMTP",
+ "SpotBugs",
"SSH",
"Tiller",
+ "TOML",
"Trello",
"Trello Power-Ups",
"TypeScript",
@@ -123,7 +130,9 @@
"Ultra Auth",
"Unicorn",
"unicorn-worker-killer",
+ "URL",
"WebdriverIO",
+ "YAML",
"YouTrack"
],
"code_blocks": false
diff --git a/.rubocop.yml b/.rubocop.yml
index c98d027cee3..3d013a650e7 100644
--- a/.rubocop.yml
+++ b/.rubocop.yml
@@ -28,6 +28,12 @@ AllCops:
- 'file_hooks/**/*'
CacheRootDirectory: tmp
+Cop/AvoidKeywordArgumentsInSidekiqWorkers:
+ Enabled: true
+ Include:
+ - 'app/workers/**/*'
+ - 'ee/app/workers/**/*'
+
Cop/StaticTranslationDefinition:
Enabled: true
Exclude:
@@ -200,12 +206,26 @@ Gitlab/ConstGetInheritFalse:
Exclude:
- 'qa/bin/*'
+Gitlab/ChangeTimezone:
+ Enabled: true
+ Exclude:
+ - config/initializers/time_zone.rb
+
Gitlab/HTTParty:
Enabled: true
Exclude:
- 'spec/**/*'
- 'ee/spec/**/*'
+Gitlab/Json:
+ Enabled: true
+ Exclude:
+ - 'db/**/*'
+ - 'qa/**/*'
+ - 'scripts/**/*'
+ - 'lib/rspec_flaky/**/*'
+ - 'lib/quality/**/*'
+
GitlabSecurity/PublicSend:
Enabled: true
Exclude:
@@ -221,10 +241,8 @@ GitlabSecurity/PublicSend:
Gitlab/DuplicateSpecLocation:
Exclude:
- - ee/spec/helpers/auth_helper_spec.rb
- ee/spec/lib/gitlab/gl_repository_spec.rb
- ee/spec/services/merge_requests/refresh_service_spec.rb
- - ee/spec/helpers/ee/auth_helper_spec.rb
- ee/spec/services/ee/merge_requests/refresh_service_spec.rb
Cop/InjectEnterpriseEditionModule:
@@ -325,11 +343,45 @@ RSpec/AnyInstanceOf:
RSpec/ImplicitSubject:
Enabled: false
+# WIP See https://gitlab.com/gitlab-org/gitlab/-/issues/211580
RSpec/LeakyConstantDeclaration:
Enabled: true
Exclude:
- - 'spec/**/*.rb'
- - 'qa/spec/**/*.rb'
+ - 'spec/db/schema_spec.rb'
+ - 'spec/lib/feature_spec.rb'
+ - 'spec/lib/gitlab/ci/build/credentials/factory_spec.rb'
+ - 'spec/lib/gitlab/ci/config/entry/retry_spec.rb'
+ - 'spec/lib/gitlab/cluster/mixins/puma_cluster_spec.rb'
+ - 'spec/lib/gitlab/cluster/mixins/unicorn_http_server_spec.rb'
+ - 'spec/lib/gitlab/config/entry/factory_spec.rb'
+ - 'spec/lib/gitlab/config/entry/simplifiable_spec.rb'
+ - 'spec/lib/gitlab/database/migration_helpers_spec.rb'
+ - 'spec/lib/gitlab/database/obsolete_ignored_columns_spec.rb'
+ - 'spec/lib/gitlab/database/with_lock_retries_spec.rb'
+ - 'spec/lib/gitlab/git/diff_collection_spec.rb'
+ - 'spec/lib/gitlab/import_export/import_test_coverage_spec.rb'
+ - 'spec/lib/gitlab/import_export/project/relation_factory_spec.rb'
+ - 'spec/lib/gitlab/jira_import/issues_importer_spec.rb'
+ - 'spec/lib/gitlab/no_cache_headers_spec.rb'
+ - 'spec/lib/gitlab/path_regex_spec.rb'
+ - 'spec/lib/gitlab/quick_actions/dsl_spec.rb'
+ - 'spec/lib/gitlab/sidekiq_middleware/client_metrics_spec.rb'
+ - 'spec/lib/gitlab/sidekiq_middleware/server_metrics_spec.rb'
+ - 'spec/lib/marginalia_spec.rb'
+ - 'spec/mailers/notify_spec.rb'
+ - 'spec/migrations/20191125114345_add_admin_mode_protected_path_spec.rb'
+ - 'spec/models/concerns/batch_destroy_dependent_associations_spec.rb'
+ - 'spec/models/concerns/bulk_insert_safe_spec.rb'
+ - 'spec/models/concerns/bulk_insertable_associations_spec.rb'
+ - 'spec/models/concerns/triggerable_hooks_spec.rb'
+ - 'spec/models/repository_spec.rb'
+ - 'spec/requests/api/graphql/tasks/task_completion_status_spec.rb'
+ - 'spec/serializers/commit_entity_spec.rb'
+ - 'spec/services/clusters/applications/check_installation_progress_service_spec.rb'
+ - 'spec/services/clusters/applications/check_uninstall_progress_service_spec.rb'
+ - 'spec/support/shared_contexts/spam_constants.rb'
+ - 'spec/support/shared_examples/quick_actions/issuable/issuable_quick_actions_shared_examples.rb'
+ - 'spec/support_specs/matchers/exceed_query_limit_helpers_spec.rb'
RSpec/EmptyLineAfterHook:
Enabled: false
@@ -387,3 +439,17 @@ Performance/ChainArrayAllocation:
- 'lib/gitlab/import_export/**/*'
- 'ee/lib/gitlab/import_export/**/*'
- 'ee/lib/ee/gitlab/import_export/**/*'
+
+Rails/TimeZone:
+ Enabled: true
+ EnforcedStyle: 'flexible'
+ Include:
+ - 'app/controllers/**/*'
+ - 'app/services/**/*'
+ - 'spec/controllers/**/*'
+ - 'spec/services/**/*'
+ - 'ee/app/controllers/**/*'
+ - 'ee/app/services/**/*'
+ - 'ee/spec/controllers/**/*'
+ - 'ee/spec/services/**/*'
+
diff --git a/.rubocop_todo.yml b/.rubocop_todo.yml
index 66a025a8fe7..bd0f9184cd6 100644
--- a/.rubocop_todo.yml
+++ b/.rubocop_todo.yml
@@ -475,7 +475,6 @@ Style/MixinUsage:
Style/MultilineIfModifier:
Exclude:
- 'app/helpers/snippets_helper.rb'
- - 'app/models/project_wiki.rb'
- 'app/services/ci/process_pipeline_service.rb'
- 'lib/api/commit_statuses.rb'
@@ -614,29 +613,6 @@ Style/StringLiteralsInInterpolation:
Style/SymbolProc:
Enabled: false
-# Offense count: 7
-# Cop supports --auto-correct.
-# Configuration parameters: EnforcedStyle, AllowSafeAssignment.
-# SupportedStyles: require_parentheses, require_no_parentheses, require_parentheses_when_complex
-Style/TernaryParentheses:
- Exclude:
- - 'app/finders/projects_finder.rb'
- - 'app/helpers/namespaces_helper.rb'
- - 'lib/gitlab/ci/build/artifacts/metadata/entry.rb'
- - 'spec/requests/api/pipeline_schedules_spec.rb'
- - 'spec/support/capybara.rb'
-
-# Offense count: 8
-# Cop supports --auto-correct.
-Style/UnneededCondition:
- Exclude:
- - 'app/helpers/button_helper.rb'
- - 'app/helpers/environment_helper.rb'
- - 'app/models/project.rb'
- - 'app/services/issuable/clone/base_service.rb'
- - 'lib/gitlab/email/message/repository_push.rb'
- - 'spec/lib/rspec_flaky/flaky_example_spec.rb'
-
# Offense count: 99
# Cop supports --auto-correct.
Style/UnneededInterpolation:
diff --git a/CHANGELOG-EE.md b/CHANGELOG-EE.md
index 8af4a07f0b6..b48dce65463 100644
--- a/CHANGELOG-EE.md
+++ b/CHANGELOG-EE.md
@@ -1,9 +1,5 @@
Please view this file on the master branch, on stable branches it's out of date.
-## 12.10.6 (2020-05-15)
-
-- No changes.
-
## 12.10.5 (2020-05-13)
### Fixed (1 change)
@@ -15,21 +11,6 @@ Please view this file on the master branch, on stable branches it's out of date.
- No changes.
-## 12.10.3 (2020-05-04)
-
-### Fixed (1 change)
-
-- Fixes file row commits not showing for certain projects.
-
-### Changed (1 change)
-
-- Move deploy keys section back to repository settings. !29184
-
-### Added (1 change)
-
-- Enable expiring subscription banner. !30304
-
-
## 12.10.2 (2020-04-30)
### Security (3 changes)
@@ -89,6 +70,19 @@ Please view this file on the master branch, on stable branches it's out of date.
- Add health status counts to usage data. !28964
+## 12.9.6 (2020-05-05)
+
+- No changes.
+
+## 12.9.5 (2020-04-30)
+
+### Security (3 changes)
+
+- Fix rendering failure of Audit Event generated by Releases API.
+- Ensure that NuGet package versions are SemVer compliant.
+- Ensure that NuGet package versions are validated before updating the stored file path.
+
+
## 12.9.4 (2020-04-16)
- No changes.
@@ -261,6 +255,15 @@ Please view this file on the master branch, on stable branches it's out of date.
- Allow users to be marked as service users. !202680
+## 12.8.10 (2020-04-30)
+
+### Security (3 changes)
+
+- Fix rendering failure of Audit Event generated by Releases API.
+- Ensure that NuGet package versions are SemVer compliant.
+- Ensure that NuGet package versions are validated before updating the stored file path.
+
+
## 12.8.9 (2020-04-14)
### Security (1 change)
diff --git a/CHANGELOG.md b/CHANGELOG.md
index d82b267037c..3b0b8ba9c0a 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -2,17 +2,6 @@
documentation](doc/development/changelog.md) for instructions on adding your own
entry.
-## 12.10.6 (2020-05-15)
-
-### Fixed (5 changes)
-
-- Fix duplicate index removal on ci_pipelines.project_id. !31043
-- Fix 500 on creating an invalid domains and verification. !31190
-- Fix incorrect number of errors returned when querying sentry errors. !31252
-- Add instance column to services table if it's missing. !31631
-- Fix incorrect regex used in FileUploader#extract_dynamic_path. !32271
-
-
## 12.10.5 (2020-05-13)
### Added (1 change)
@@ -27,22 +16,6 @@ entry.
- Add a Project's group to list of groups when parsing for codeowner entries. !30934
-## 12.10.3 (2020-05-04)
-
-### Fixed (6 changes)
-
-- Fix errors creating project with active Prometheus service template. !30340
-- Fix incorrect commits number in commits list. !30412
-- Fix second 500 error with NULL restricted visibility levels. !30414
-- Add LFS badge feature flag to RefsController#logs_tree. !30442
-- Disable schema dumping after migrations in production. !30812
-- Fixes branch name not getting escaped correctly on frontend.
-
-### Changed (1 change)
-
-- Handle possible RSA key exceptions when generating CI_JOB_JWT. !30702
-
-
## 12.10.2 (2020-04-30)
### Security (8 changes)
@@ -518,6 +491,28 @@ entry.
- Remove store_mentions! in Snippets::CreateService. !29581 (Sashi Kumar)
+## 12.9.6 (2020-05-05)
+
+### Fixed (1 change)
+
+- Add a Project's group to list of groups when parsing for codeowner entries. !30934
+
+
+## 12.9.5 (2020-04-30)
+
+### Security (9 changes)
+
+- Ensure MR diff exists before codeowner check.
+- Apply CODEOWNERS validations to web requests.
+- Prevent unauthorized access to default branch.
+- Do not return private project ID without permission.
+- Fix doorkeeper CVE-2020-10187.
+- Prevent ES credentials leak.
+- Change GitHub service integration token input to password.
+- Return only safe urls for mirrors.
+- Validate workhorse 'rewritten_fields' and properly use them during multipart uploads.
+
+
## 12.9.4 (2020-04-16)
- No changes.
@@ -1036,6 +1031,19 @@ entry.
- Improvement in token reference.
+## 12.8.10 (2020-04-30)
+
+### Security (7 changes)
+
+- Ensure MR diff exists before codeowner check.
+- Prevent unauthorized access to default branch.
+- Do not return private project ID without permission.
+- Fix doorkeeper CVE-2020-10187.
+- Prevent ES credentials leak.
+- Return only safe urls for mirrors.
+- Validate workhorse 'rewritten_fields' and properly use them during multipart uploads.
+
+
## 12.8.9 (2020-04-14)
### Security (3 changes)
diff --git a/GITALY_SERVER_VERSION b/GITALY_SERVER_VERSION
index a3d2194c119..bf6c36b3b9a 100644
--- a/GITALY_SERVER_VERSION
+++ b/GITALY_SERVER_VERSION
@@ -1 +1 @@
-12.10.6
+4cd8d0c5614cafc3ca41f3473004adaeabc77e24
diff --git a/GITLAB_ELASTICSEARCH_INDEXER_VERSION b/GITLAB_ELASTICSEARCH_INDEXER_VERSION
index ccbccc3dc62..276cbf9e285 100644
--- a/GITLAB_ELASTICSEARCH_INDEXER_VERSION
+++ b/GITLAB_ELASTICSEARCH_INDEXER_VERSION
@@ -1 +1 @@
-2.2.0
+2.3.0
diff --git a/GITLAB_PAGES_VERSION b/GITLAB_PAGES_VERSION
index 092afa15df4..84cc529467b 100644
--- a/GITLAB_PAGES_VERSION
+++ b/GITLAB_PAGES_VERSION
@@ -1 +1 @@
-1.17.0
+1.18.0
diff --git a/GITLAB_SHELL_VERSION b/GITLAB_SHELL_VERSION
index 685332623b2..67aee23940e 100644
--- a/GITLAB_SHELL_VERSION
+++ b/GITLAB_SHELL_VERSION
@@ -1 +1 @@
-12.2.0
+13.2.0
diff --git a/GITLAB_WORKHORSE_VERSION b/GITLAB_WORKHORSE_VERSION
index 57b9fc187c0..2f70731b8aa 100644
--- a/GITLAB_WORKHORSE_VERSION
+++ b/GITLAB_WORKHORSE_VERSION
@@ -1 +1 @@
-8.30.1
+8.31.0
diff --git a/Gemfile b/Gemfile
index d15628eb83c..9c8c5e8b30d 100644
--- a/Gemfile
+++ b/Gemfile
@@ -1,6 +1,6 @@
source 'https://rubygems.org'
-gem 'rails', '6.0.2'
+gem 'rails', '~> 6.0.3'
gem 'bootsnap', '~> 1.4.6'
@@ -26,7 +26,7 @@ gem 'marginalia', '~> 1.8.0'
# Authentication libraries
gem 'devise', '~> 4.6'
-gem 'doorkeeper', '~> 5.0.2'
+gem 'doorkeeper', '~> 5.0.3'
gem 'doorkeeper-openid_connect', '~> 1.6.3'
gem 'omniauth', '~> 1.8'
gem 'omniauth-auth0', '~> 2.0.0'
@@ -138,7 +138,7 @@ gem 'faraday_middleware-aws-signers-v4'
# Markdown and HTML processing
gem 'html-pipeline', '~> 2.12'
gem 'deckar01-task_list', '2.3.1'
-gem 'gitlab-markup', '~> 1.7.0'
+gem 'gitlab-markup', '~> 1.7.1'
gem 'github-markup', '~> 1.7.0', require: 'github/markup'
gem 'commonmarker', '~> 0.20'
gem 'RedCloth', '~> 4.3.2'
@@ -148,11 +148,11 @@ gem 'creole', '~> 0.5.0'
gem 'wikicloth', '0.8.1'
gem 'asciidoctor', '~> 2.0.10'
gem 'asciidoctor-include-ext', '~> 0.3.1', require: false
-gem 'asciidoctor-plantuml', '0.0.10'
-gem 'rouge', '~> 3.18.0'
+gem 'asciidoctor-plantuml', '~> 0.0.12'
+gem 'rouge', '~> 3.19.0'
gem 'truncato', '~> 0.7.11'
gem 'bootstrap_form', '~> 4.2.0'
-gem 'nokogiri', '~> 1.10.5'
+gem 'nokogiri', '~> 1.10.9'
gem 'escape_utils', '~> 1.1'
# Calendar rendering
@@ -166,7 +166,7 @@ gem 'diff_match_patch', '~> 0.1.0'
gem 'rack', '~> 2.0.9'
group :unicorn do
- gem 'unicorn', '~> 5.4.1'
+ gem 'unicorn', '~> 5.5'
gem 'unicorn-worker-killer', '~> 0.4.4'
end
@@ -204,7 +204,7 @@ gem 'ruby-progressbar'
gem 'settingslogic', '~> 2.0.9'
# Linear-time regex library for untrusted regular expressions
-gem 're2', '~> 1.1.1'
+gem 're2', '~> 1.2.0'
# Misc
@@ -230,7 +230,7 @@ gem 'discordrb-webhooks-blackst0ne', '~> 3.3', require: false
gem 'hipchat', '~> 1.5.0'
# Jira integration
-gem 'jira-ruby', '~> 1.7'
+gem 'jira-ruby', '~> 2.0.0'
gem 'atlassian-jwt', '~> 0.2.0'
# Flowdock integration
@@ -287,13 +287,13 @@ gem 'addressable', '~> 2.7'
gem 'font-awesome-rails', '~> 4.7'
gem 'gemojione', '~> 3.3'
gem 'gon', '~> 6.2'
-gem 'request_store', '~> 1.3'
+gem 'request_store', '~> 1.5'
gem 'base32', '~> 0.3.0'
gem "gitlab-license", "~> 1.0"
# Protect against bruteforcing
-gem 'rack-attack', '~> 6.2.0'
+gem 'rack-attack', '~> 6.3.0'
# Sentry integration
gem 'sentry-raven', '~> 2.9'
@@ -318,13 +318,9 @@ gem 'peek', '~> 1.1'
# Snowplow events tracking
gem 'snowplow-tracker', '~> 0.6.1'
-# Memory benchmarks
-gem 'derailed_benchmarks', require: false
-
# Metrics
group :metrics do
gem 'method_source', '~> 0.8', require: false
- gem 'influxdb', '~> 0.2', require: false
# Prometheus
gem 'prometheus-client-mmap', '~> 0.10.0'
@@ -355,7 +351,7 @@ group :development, :test do
gem 'database_cleaner', '~> 1.7.0'
gem 'factory_bot_rails', '~> 5.1.0'
- gem 'rspec-rails', '~> 4.0.0.beta4'
+ gem 'rspec-rails', '~> 4.0.0'
# Prevent occasions where minitest is not bundled in packaged versions of ruby (see #3826)
gem 'minitest', '~> 5.11.0'
@@ -414,12 +410,15 @@ group :test do
gem 'test-prof', '~> 0.10.0'
gem 'rspec_junit_formatter'
gem 'guard-rspec'
+
+ # Moved in `test` because https://gitlab.com/gitlab-org/gitlab/-/issues/217527
+ gem 'derailed_benchmarks', require: false
end
gem 'octokit', '~> 4.15'
# https://gitlab.com/gitlab-org/gitlab/issues/207207
-gem 'gitlab-mail_room', '~> 0.0.3', require: 'mail_room'
+gem 'gitlab-mail_room', '~> 0.0.4', require: 'mail_room'
gem 'email_reply_trimmer', '~> 0.1'
gem 'html2text'
@@ -445,7 +444,7 @@ gem 'sys-filesystem', '~> 1.1.6'
gem 'net-ntp'
# SSH host key support
-gem 'net-ssh', '~> 5.2'
+gem 'net-ssh', '~> 6.0'
gem 'sshkey', '~> 2.0'
# Required for ED25519 SSH host key support
@@ -455,7 +454,7 @@ group :ed25519 do
end
# Gitaly GRPC protocol definitions
-gem 'gitaly', '~> 12.9.0.pre.rc4'
+gem 'gitaly', '~> 13.0.0.pre.rc1'
gem 'grpc', '~> 1.24.0'
@@ -481,8 +480,6 @@ gem 'countries', '~> 3.0'
gem 'retriable', '~> 3.1.2'
-gem 'liquid', '~> 4.0'
-
# LRU cache
gem 'lru_redux'
@@ -495,3 +492,9 @@ gem 'mail', '= 2.7.1'
# File encryption
gem 'lockbox', '~> 0.3.3'
+
+# Email validation
+gem 'valid_email', '~> 0.1'
+
+# JSON
+gem 'json', '~> 2.3.0'
diff --git a/Gemfile.lock b/Gemfile.lock
index 64bf4ec44d7..ffff576e8b0 100644
--- a/Gemfile.lock
+++ b/Gemfile.lock
@@ -6,64 +6,64 @@ GEM
ace-rails-ap (4.1.2)
acme-client (2.0.5)
faraday (~> 0.9, >= 0.9.1)
- actioncable (6.0.2)
- actionpack (= 6.0.2)
+ actioncable (6.0.3)
+ actionpack (= 6.0.3)
nio4r (~> 2.0)
websocket-driver (>= 0.6.1)
- actionmailbox (6.0.2)
- actionpack (= 6.0.2)
- activejob (= 6.0.2)
- activerecord (= 6.0.2)
- activestorage (= 6.0.2)
- activesupport (= 6.0.2)
+ actionmailbox (6.0.3)
+ actionpack (= 6.0.3)
+ activejob (= 6.0.3)
+ activerecord (= 6.0.3)
+ activestorage (= 6.0.3)
+ activesupport (= 6.0.3)
mail (>= 2.7.1)
- actionmailer (6.0.2)
- actionpack (= 6.0.2)
- actionview (= 6.0.2)
- activejob (= 6.0.2)
+ actionmailer (6.0.3)
+ actionpack (= 6.0.3)
+ actionview (= 6.0.3)
+ activejob (= 6.0.3)
mail (~> 2.5, >= 2.5.4)
rails-dom-testing (~> 2.0)
- actionpack (6.0.2)
- actionview (= 6.0.2)
- activesupport (= 6.0.2)
- rack (~> 2.0)
+ actionpack (6.0.3)
+ actionview (= 6.0.3)
+ activesupport (= 6.0.3)
+ rack (~> 2.0, >= 2.0.8)
rack-test (>= 0.6.3)
rails-dom-testing (~> 2.0)
rails-html-sanitizer (~> 1.0, >= 1.2.0)
- actiontext (6.0.2)
- actionpack (= 6.0.2)
- activerecord (= 6.0.2)
- activestorage (= 6.0.2)
- activesupport (= 6.0.2)
+ actiontext (6.0.3)
+ actionpack (= 6.0.3)
+ activerecord (= 6.0.3)
+ activestorage (= 6.0.3)
+ activesupport (= 6.0.3)
nokogiri (>= 1.8.5)
- actionview (6.0.2)
- activesupport (= 6.0.2)
+ actionview (6.0.3)
+ activesupport (= 6.0.3)
builder (~> 3.1)
erubi (~> 1.4)
rails-dom-testing (~> 2.0)
rails-html-sanitizer (~> 1.1, >= 1.2.0)
- activejob (6.0.2)
- activesupport (= 6.0.2)
+ activejob (6.0.3)
+ activesupport (= 6.0.3)
globalid (>= 0.3.6)
- activemodel (6.0.2)
- activesupport (= 6.0.2)
- activerecord (6.0.2)
- activemodel (= 6.0.2)
- activesupport (= 6.0.2)
+ activemodel (6.0.3)
+ activesupport (= 6.0.3)
+ activerecord (6.0.3)
+ activemodel (= 6.0.3)
+ activesupport (= 6.0.3)
activerecord-explain-analyze (0.1.0)
activerecord (>= 4)
pg
- activestorage (6.0.2)
- actionpack (= 6.0.2)
- activejob (= 6.0.2)
- activerecord (= 6.0.2)
+ activestorage (6.0.3)
+ actionpack (= 6.0.3)
+ activejob (= 6.0.3)
+ activerecord (= 6.0.3)
marcel (~> 0.3.1)
- activesupport (6.0.2)
+ activesupport (6.0.3)
concurrent-ruby (~> 1.0, >= 1.0.2)
i18n (>= 0.7, < 2)
minitest (~> 5.1)
tzinfo (~> 1.1)
- zeitwerk (~> 2.2)
+ zeitwerk (~> 2.2, >= 2.2.2)
acts-as-taggable-on (6.5.0)
activerecord (>= 5.0, < 6.1)
adamantium (0.2.0)
@@ -84,7 +84,7 @@ GEM
asciidoctor (2.0.10)
asciidoctor-include-ext (0.3.1)
asciidoctor (>= 1.5.6, < 3.0.0)
- asciidoctor-plantuml (0.0.10)
+ asciidoctor-plantuml (0.0.12)
asciidoctor (>= 1.5.6, < 3.0.0)
ast (2.4.0)
atlassian-jwt (0.2.0)
@@ -153,7 +153,6 @@ GEM
activemodel (>= 4.0.0)
activesupport (>= 4.0.0)
mime-types (>= 1.16)
- cause (0.1)
character_set (1.1.2)
charlock_holmes (0.7.6)
childprocess (3.0.0)
@@ -245,7 +244,7 @@ GEM
docile (1.3.2)
domain_name (0.5.20180417)
unf (>= 0.0.5, < 1.0.0)
- doorkeeper (5.0.2)
+ doorkeeper (5.0.3)
railties (>= 4.2)
doorkeeper-openid_connect (1.6.3)
doorkeeper (>= 5.0, < 5.2)
@@ -378,7 +377,7 @@ GEM
po_to_json (>= 1.0.0)
rails (>= 3.2.0)
git (1.5.0)
- gitaly (12.9.0.pre.rc4)
+ gitaly (13.0.0.pre.rc1)
grpc (~> 1.0)
github-markup (1.7.0)
gitlab-chronic (0.10.5)
@@ -391,8 +390,8 @@ GEM
opentracing (~> 0.4)
redis (> 3.0.0, < 5.0.0)
gitlab-license (1.0.0)
- gitlab-mail_room (0.0.3)
- gitlab-markup (1.7.0)
+ gitlab-mail_room (0.0.4)
+ gitlab-markup (1.7.1)
gitlab-net-dns (0.9.1)
gitlab-puma (4.3.3.gitlab.2)
nio4r (~> 2.0)
@@ -535,9 +534,6 @@ GEM
i18n_data (0.8.0)
icalendar (2.4.1)
ice_nine (0.11.2)
- influxdb (0.2.3)
- cause
- json
invisible_captcha (0.12.1)
rails (>= 3.2.0)
ipaddress (0.8.3)
@@ -545,7 +541,7 @@ GEM
opentracing (~> 0.3)
thrift
jaro_winkler (1.5.4)
- jira-ruby (1.7.1)
+ jira-ruby (2.0.0)
activesupport
atlassian-jwt
multipart-post
@@ -555,7 +551,7 @@ GEM
character_set (~> 1.1)
regexp_parser (~> 1.1)
regexp_property_values (~> 0.3)
- json (1.8.6)
+ json (2.3.0)
json-jwt (1.11.0)
activesupport (>= 4.2)
aes_key_wrap
@@ -575,7 +571,7 @@ GEM
activerecord
kaminari-core (= 1.0.1)
kaminari-core (1.0.1)
- kgio (2.11.2)
+ kgio (2.11.3)
knapsack (1.17.0)
rake
kramdown (2.1.0)
@@ -602,7 +598,6 @@ GEM
xml-simple
licensee (8.9.2)
rugged (~> 0.24)
- liquid (4.0.3)
listen (3.1.5)
rb-fsevent (~> 0.9, >= 0.9.4)
rb-inotify (~> 0.9, >= 0.9.7)
@@ -614,7 +609,7 @@ GEM
activesupport (>= 4)
railties (>= 4)
request_store (~> 1.0)
- loofah (2.4.0)
+ loofah (2.5.0)
crass (~> 1.0.2)
nokogiri (>= 1.5.9)
lru_redux (1.1.0)
@@ -634,7 +629,7 @@ GEM
mime-types (3.2.2)
mime-types-data (~> 3.2015)
mime-types-data (3.2019.0331)
- mimemagic (0.3.3)
+ mimemagic (0.3.5)
mini_histogram (0.1.3)
mini_magick (4.9.5)
mini_mime (1.0.2)
@@ -653,11 +648,11 @@ GEM
nenv (0.3.0)
net-ldap (0.16.2)
net-ntp (2.1.3)
- net-ssh (5.2.0)
+ net-ssh (6.0.0)
netrc (0.11.0)
nio4r (2.5.2)
no_proxy_fix (0.1.2)
- nokogiri (1.10.8)
+ nokogiri (1.10.9)
mini_portile2 (~> 2.4.0)
nokogumbo (1.5.0)
nokogiri
@@ -791,7 +786,7 @@ GEM
rack (2.0.9)
rack-accept (0.4.5)
rack (>= 0.4)
- rack-attack (6.2.0)
+ rack-attack (6.3.0)
rack (>= 1.0, < 3)
rack-cors (1.0.6)
rack (>= 1.6.0)
@@ -808,20 +803,20 @@ GEM
rack-test (1.1.0)
rack (>= 1.0, < 3)
rack-timeout (0.5.1)
- rails (6.0.2)
- actioncable (= 6.0.2)
- actionmailbox (= 6.0.2)
- actionmailer (= 6.0.2)
- actionpack (= 6.0.2)
- actiontext (= 6.0.2)
- actionview (= 6.0.2)
- activejob (= 6.0.2)
- activemodel (= 6.0.2)
- activerecord (= 6.0.2)
- activestorage (= 6.0.2)
- activesupport (= 6.0.2)
+ rails (6.0.3)
+ actioncable (= 6.0.3)
+ actionmailbox (= 6.0.3)
+ actionmailer (= 6.0.3)
+ actionpack (= 6.0.3)
+ actiontext (= 6.0.3)
+ actionview (= 6.0.3)
+ activejob (= 6.0.3)
+ activemodel (= 6.0.3)
+ activerecord (= 6.0.3)
+ activestorage (= 6.0.3)
+ activesupport (= 6.0.3)
bundler (>= 1.3.0)
- railties (= 6.0.2)
+ railties (= 6.0.3)
sprockets-rails (>= 2.0.0)
rails-controller-testing (1.0.4)
actionpack (>= 5.0.1.x)
@@ -835,14 +830,14 @@ GEM
rails-i18n (6.0.0)
i18n (>= 0.7, < 2)
railties (>= 6.0.0, < 7)
- railties (6.0.2)
- actionpack (= 6.0.2)
- activesupport (= 6.0.2)
+ railties (6.0.3)
+ actionpack (= 6.0.3)
+ activesupport (= 6.0.3)
method_source
rake (>= 0.8.7)
thor (>= 0.20.3, < 2.0)
rainbow (3.0.0)
- raindrops (0.19.0)
+ raindrops (0.19.1)
rake (12.3.3)
rb-fsevent (0.10.2)
rb-inotify (0.9.10)
@@ -854,10 +849,10 @@ GEM
msgpack (>= 0.4.3)
optimist (>= 3.0.0)
rdoc (6.1.2)
- re2 (1.1.1)
+ re2 (1.2.0)
recaptcha (4.13.1)
json
- recursive-open-struct (1.1.0)
+ recursive-open-struct (1.1.1)
redis (4.1.3)
redis-actionpack (5.2.0)
actionpack (>= 5, < 7)
@@ -883,7 +878,8 @@ GEM
declarative (< 0.1.0)
declarative-option (< 0.2.0)
uber (< 0.2.0)
- request_store (1.3.1)
+ request_store (1.5.0)
+ rack (>= 1.4)
responders (3.0.0)
actionpack (>= 5.0)
railties (>= 5.0)
@@ -894,7 +890,7 @@ GEM
retriable (3.1.2)
rinku (2.0.0)
rotp (2.1.2)
- rouge (3.18.0)
+ rouge (3.19.0)
rqrcode (0.7.0)
chunky_png
rqrcode-rails3 (0.1.7)
@@ -905,7 +901,7 @@ GEM
rspec-mocks (~> 3.9.0)
rspec-core (3.9.1)
rspec-support (~> 3.9.1)
- rspec-expectations (3.9.0)
+ rspec-expectations (3.9.1)
diff-lcs (>= 1.2.0, < 2.0)
rspec-support (~> 3.9.0)
rspec-mocks (3.9.1)
@@ -917,7 +913,7 @@ GEM
proc_to_ast
rspec (>= 2.13, < 4)
unparser
- rspec-rails (4.0.0.beta4)
+ rspec-rails (4.0.0)
actionpack (>= 4.2)
activesupport (>= 4.2)
railties (>= 4.2)
@@ -1077,7 +1073,7 @@ GEM
truncato (0.7.11)
htmlentities (~> 4.3.1)
nokogiri (>= 1.7.0, <= 2.0)
- tzinfo (1.2.6)
+ tzinfo (1.2.7)
thread_safe (~> 0.1)
u2f (0.2.1)
uber (0.1.0)
@@ -1091,7 +1087,7 @@ GEM
unicode_plot (0.0.4)
enumerable-statistics (>= 2.0.1)
unicode_utils (1.4.0)
- unicorn (5.4.1)
+ unicorn (5.5.5)
kgio (~> 2.6)
raindrops (~> 0.7)
unicorn-worker-killer (0.4.4)
@@ -1108,6 +1104,9 @@ GEM
equalizer (~> 0.0.9)
parser (>= 2.6.5)
procto (~> 0.0.2)
+ valid_email (0.1.3)
+ activemodel
+ mail (>= 2.6.1)
validate_email (0.1.6)
activemodel (>= 3.0)
mail (>= 2.2.5)
@@ -1146,7 +1145,7 @@ GEM
xml-simple (1.1.5)
xpath (3.2.0)
nokogiri (~> 1.8)
- zeitwerk (2.2.2)
+ zeitwerk (2.3.0)
PLATFORMS
ruby
@@ -1163,7 +1162,7 @@ DEPENDENCIES
asana (~> 0.9)
asciidoctor (~> 2.0.10)
asciidoctor-include-ext (~> 0.3.1)
- asciidoctor-plantuml (= 0.0.10)
+ asciidoctor-plantuml (~> 0.0.12)
atlassian-jwt (~> 0.2.0)
attr_encrypted (~> 3.1.0)
awesome_print
@@ -1202,7 +1201,7 @@ DEPENDENCIES
diff_match_patch (~> 0.1.0)
diffy (~> 3.3)
discordrb-webhooks-blackst0ne (~> 3.3)
- doorkeeper (~> 5.0.2)
+ doorkeeper (~> 5.0.3)
doorkeeper-openid_connect (~> 1.6.3)
ed25519 (~> 1.2)
elasticsearch-api (~> 6.8)
@@ -1235,13 +1234,13 @@ DEPENDENCIES
gettext (~> 3.2.2)
gettext_i18n_rails (~> 1.8.0)
gettext_i18n_rails_js (~> 1.3)
- gitaly (~> 12.9.0.pre.rc4)
+ gitaly (~> 13.0.0.pre.rc1)
github-markup (~> 1.7.0)
gitlab-chronic (~> 0.10.5)
gitlab-labkit (= 0.12.0)
gitlab-license (~> 1.0)
- gitlab-mail_room (~> 0.0.3)
- gitlab-markup (~> 1.7.0)
+ gitlab-mail_room (~> 0.0.4)
+ gitlab-markup (~> 1.7.1)
gitlab-net-dns (~> 0.9.1)
gitlab-puma (~> 4.3.3.gitlab.2)
gitlab-puma_worker_killer (~> 0.1.1.gitlab.1)
@@ -1273,10 +1272,10 @@ DEPENDENCIES
html2text
httparty (~> 0.16.4)
icalendar
- influxdb (~> 0.2)
invisible_captcha (~> 0.12.1)
- jira-ruby (~> 1.7)
+ jira-ruby (~> 2.0.0)
js_regex (~> 3.1)
+ json (~> 2.3.0)
json-schema (~> 2.8.0)
jwt (~> 2.1.0)
kaminari (~> 1.0)
@@ -1285,7 +1284,6 @@ DEPENDENCIES
letter_opener_web (~> 1.3.4)
license_finder (~> 5.4)
licensee (~> 8.9)
- liquid (~> 4.0)
lockbox (~> 0.3.3)
lograge (~> 0.5)
loofah (~> 2.2)
@@ -1300,8 +1298,8 @@ DEPENDENCIES
nakayoshi_fork (~> 0.0.4)
net-ldap
net-ntp
- net-ssh (~> 5.2)
- nokogiri (~> 1.10.5)
+ net-ssh (~> 6.0)
+ nokogiri (~> 1.10.9)
oauth2 (~> 1.4)
octokit (~> 4.15)
omniauth (~> 1.8)
@@ -1332,12 +1330,12 @@ DEPENDENCIES
pry-byebug (~> 3.5.1)
pry-rails (~> 0.3.9)
rack (~> 2.0.9)
- rack-attack (~> 6.2.0)
+ rack-attack (~> 6.3.0)
rack-cors (~> 1.0.6)
rack-oauth2 (~> 1.9.3)
rack-proxy (~> 0.6.0)
rack-timeout
- rails (= 6.0.2)
+ rails (~> 6.0.3)
rails-controller-testing
rails-i18n (~> 6.0)
rainbow (~> 3.0)
@@ -1345,18 +1343,18 @@ DEPENDENCIES
rblineprof (~> 0.3.6)
rbtrace (~> 0.4)
rdoc (~> 6.1.2)
- re2 (~> 1.1.1)
+ re2 (~> 1.2.0)
recaptcha (~> 4.11)
redis (~> 4.0)
redis-namespace (~> 1.6.0)
redis-rails (~> 5.0.2)
- request_store (~> 1.3)
+ request_store (~> 1.5)
responders (~> 3.0)
retriable (~> 3.1.2)
- rouge (~> 3.18.0)
+ rouge (~> 3.19.0)
rqrcode-rails3 (~> 0.1.7)
rspec-parameterized
- rspec-rails (~> 4.0.0.beta4)
+ rspec-rails (~> 4.0.0)
rspec-retry (~> 0.6.1)
rspec_junit_formatter
rspec_profiling (~> 0.0.5)
@@ -1398,9 +1396,10 @@ DEPENDENCIES
u2f (~> 0.2.1)
uglifier (~> 2.7.2)
unf (~> 0.1.4)
- unicorn (~> 5.4.1)
+ unicorn (~> 5.5)
unicorn-worker-killer (~> 0.4.4)
unleash (~> 0.1.5)
+ valid_email (~> 0.1)
validates_hostname (~> 1.0.6)
version_sorter (~> 2.2.4)
vmstat (~> 2.3.0)
diff --git a/Guardfile b/Guardfile
index 21ee2a9d610..baaa52bd204 100644
--- a/Guardfile
+++ b/Guardfile
@@ -2,28 +2,51 @@
# More info at https://github.com/guard/guard#readme
+require "guard/rspec/dsl"
+
cmd = ENV['GUARD_CMD'] || (ENV['SPRING'] ? 'spring rspec' : 'bundle exec rspec')
-guard :rspec, cmd: cmd do
- require "guard/rspec/dsl"
- dsl = Guard::RSpec::Dsl.new(self)
+directories %w(app ee lib spec)
+
+rspec_context_for = proc do |context_path|
+ OpenStruct.new(to_s: "spec").tap do |rspec|
+ rspec.spec_dir = "#{context_path}spec"
+ rspec.spec = ->(m) { Guard::RSpec::Dsl.detect_spec_file_for(rspec, m) }
+ rspec.spec_helper = "#{rspec.spec_dir}/spec_helper.rb"
+ rspec.spec_files = %r{^#{rspec.spec_dir}/.+_spec\.rb$}
+ rspec.spec_support = %r{^#{rspec.spec_dir}/support/(.+)\.rb$}
+ end
+end
+
+rails_context_for = proc do |context_path, exts|
+ OpenStruct.new.tap do |rails|
+ rails.app_files = %r{^#{context_path}app/(.+)\.rb$}
+
+ rails.views = %r{^#{context_path}app/(views/.+/[^/]*\.(?:#{exts}))$}
+ rails.view_dirs = %r{^#{context_path}app/views/(.+)/[^/]*\.(?:#{exts})$}
+ rails.layouts = %r{^#{context_path}app/layouts/(.+)/[^/]*\.(?:#{exts})$}
- directories %w(app ee lib spec)
+ rails.controllers = %r{^#{context_path}app/controllers/(.+)_controller\.rb$}
+ rails.routes = "#{context_path}config/routes.rb"
+ rails.app_controller = "#{context_path}app/controllers/application_controller.rb"
+ rails.spec_helper = "#{context_path}spec/rails_helper.rb"
+ end
+end
+guard_setup = proc do |context_path|
# RSpec files
- rspec = dsl.rspec
+ rspec = rspec_context_for.call(context_path)
watch(rspec.spec_helper) { rspec.spec_dir }
watch(rspec.spec_support) { rspec.spec_dir }
watch(rspec.spec_files)
# Ruby files
- ruby = dsl.ruby
- dsl.watch_spec_files_for(ruby.lib_files)
+ watch(%r{^#{context_path}(lib/.+)\.rb$}) { |m| rspec.spec.call(m[1]) }
# Rails files
- rails = dsl.rails(view_extensions: %w(erb haml slim))
- dsl.watch_spec_files_for(rails.app_files)
- dsl.watch_spec_files_for(rails.views)
+ rails = rails_context_for.call(context_path, %w(erb haml slim))
+ watch(rails.app_files) { |m| rspec.spec.call(m[1]) }
+ watch(rails.views) { |m| rspec.spec.call(m[1]) }
watch(rails.controllers) do |m|
[
@@ -41,3 +64,11 @@ guard :rspec, cmd: cmd do
watch(rails.view_dirs) { |m| rspec.spec.call("features/#{m[1]}") }
watch(rails.layouts) { |m| rspec.spec.call("features/#{m[1]}") }
end
+
+context_paths = ['', 'ee/']
+
+context_paths.each do |context_path|
+ guard :rspec, cmd: cmd, spec_paths: ["#{context_path}spec/"] do
+ guard_setup.call(context_path)
+ end
+end
diff --git a/README.md b/README.md
index 1372e47d52f..51a54c3bbff 100644
--- a/README.md
+++ b/README.md
@@ -82,7 +82,7 @@ GitLab is a Ruby on Rails application that runs on the following software:
- Ruby (MRI) 2.6.5
- Git 2.8.4+
- Redis 2.8+
-- PostgreSQL 9.6+
+- PostgreSQL 11+
For more information please see the [architecture](https://docs.gitlab.com/ee/development/architecture.html) and [requirements](https://docs.gitlab.com/ee/install/requirements.html) documentation.
diff --git a/app/assets/images/cluster_app_logos/fluentd.png b/app/assets/images/cluster_app_logos/fluentd.png
new file mode 100644
index 00000000000..6d42578f2ce
--- /dev/null
+++ b/app/assets/images/cluster_app_logos/fluentd.png
Binary files differ
diff --git a/app/assets/javascripts/access_tokens/components/expires_at_field.vue b/app/assets/javascripts/access_tokens/components/expires_at_field.vue
new file mode 100644
index 00000000000..d0932ad80e1
--- /dev/null
+++ b/app/assets/javascripts/access_tokens/components/expires_at_field.vue
@@ -0,0 +1,14 @@
+<script>
+import { GlDatepicker } from '@gitlab/ui';
+
+export default {
+ name: 'ExpiresAtField',
+ components: { GlDatepicker },
+};
+</script>
+
+<template>
+ <gl-datepicker :target="null" :min-date="new Date()">
+ <slot></slot>
+ </gl-datepicker>
+</template>
diff --git a/app/assets/javascripts/access_tokens/index.js b/app/assets/javascripts/access_tokens/index.js
new file mode 100644
index 00000000000..9bdb2940956
--- /dev/null
+++ b/app/assets/javascripts/access_tokens/index.js
@@ -0,0 +1,12 @@
+import Vue from 'vue';
+import ExpiresAtField from './components/expires_at_field.vue';
+
+const initExpiresAtField = () => {
+ // eslint-disable-next-line no-new
+ new Vue({
+ el: document.querySelector('.js-access-tokens-expires-at'),
+ components: { ExpiresAtField },
+ });
+};
+
+export default initExpiresAtField;
diff --git a/app/assets/javascripts/actioncable_consumer.js b/app/assets/javascripts/actioncable_consumer.js
new file mode 100644
index 00000000000..5658ffc1a38
--- /dev/null
+++ b/app/assets/javascripts/actioncable_consumer.js
@@ -0,0 +1,3 @@
+import { createConsumer } from '@rails/actioncable';
+
+export default createConsumer();
diff --git a/app/assets/javascripts/alert_management/components/alert_details.vue b/app/assets/javascripts/alert_management/components/alert_details.vue
new file mode 100644
index 00000000000..89db7db77d5
--- /dev/null
+++ b/app/assets/javascripts/alert_management/components/alert_details.vue
@@ -0,0 +1,236 @@
+<script>
+import * as Sentry from '@sentry/browser';
+import {
+ GlAlert,
+ GlIcon,
+ GlLoadingIcon,
+ GlDropdown,
+ GlDropdownItem,
+ GlSprintf,
+ GlTabs,
+ GlTab,
+ GlButton,
+ GlTable,
+} from '@gitlab/ui';
+import createFlash from '~/flash';
+import { s__ } from '~/locale';
+import query from '../graphql/queries/details.query.graphql';
+import { fetchPolicies } from '~/lib/graphql';
+import TimeAgoTooltip from '~/vue_shared/components/time_ago_tooltip.vue';
+import glFeatureFlagsMixin from '~/vue_shared/mixins/gl_feature_flags_mixin';
+import { ALERTS_SEVERITY_LABELS } from '../constants';
+import updateAlertStatus from '../graphql/mutations/update_alert_status.graphql';
+
+export default {
+ statuses: {
+ TRIGGERED: s__('AlertManagement|Triggered'),
+ ACKNOWLEDGED: s__('AlertManagement|Acknowledged'),
+ RESOLVED: s__('AlertManagement|Resolved'),
+ },
+ i18n: {
+ errorMsg: s__(
+ 'AlertManagement|There was an error displaying the alert. Please refresh the page to try again.',
+ ),
+ fullAlertDetailsTitle: s__('AlertManagement|Alert details'),
+ overviewTitle: s__('AlertManagement|Overview'),
+ reportedAt: s__('AlertManagement|Reported %{when}'),
+ reportedAtWithTool: s__('AlertManagement|Reported %{when} by %{tool}'),
+ },
+ severityLabels: ALERTS_SEVERITY_LABELS,
+ components: {
+ GlAlert,
+ GlIcon,
+ GlLoadingIcon,
+ GlSprintf,
+ GlDropdown,
+ GlDropdownItem,
+ GlTab,
+ GlTabs,
+ GlButton,
+ GlTable,
+ TimeAgoTooltip,
+ },
+ mixins: [glFeatureFlagsMixin()],
+ props: {
+ alertId: {
+ type: String,
+ required: true,
+ },
+ projectPath: {
+ type: String,
+ required: true,
+ },
+ newIssuePath: {
+ type: String,
+ required: true,
+ },
+ },
+ apollo: {
+ alert: {
+ fetchPolicy: fetchPolicies.CACHE_AND_NETWORK,
+ query,
+ variables() {
+ return {
+ fullPath: this.projectPath,
+ alertId: this.alertId,
+ };
+ },
+ update(data) {
+ return data?.project?.alertManagementAlerts?.nodes?.[0] ?? null;
+ },
+ error(error) {
+ this.errored = true;
+ Sentry.captureException(error);
+ },
+ },
+ },
+ data() {
+ return { alert: null, errored: false, isErrorDismissed: false };
+ },
+ computed: {
+ loading() {
+ return this.$apollo.queries.alert.loading;
+ },
+ reportedAtMessage() {
+ return this.alert?.monitoringTool
+ ? this.$options.i18n.reportedAtWithTool
+ : this.$options.i18n.reportedAt;
+ },
+ showErrorMsg() {
+ return this.errored && !this.isErrorDismissed;
+ },
+ },
+ methods: {
+ dismissError() {
+ this.isErrorDismissed = true;
+ },
+ updateAlertStatus(status) {
+ this.$apollo
+ .mutate({
+ mutation: updateAlertStatus,
+ variables: {
+ iid: this.alertId,
+ status: status.toUpperCase(),
+ projectPath: this.projectPath,
+ },
+ })
+ .catch(() => {
+ createFlash(
+ s__(
+ 'AlertManagement|There was an error while updating the status of the alert. Please try again.',
+ ),
+ );
+ });
+ },
+ },
+};
+</script>
+<template>
+ <div>
+ <gl-alert v-if="showErrorMsg" variant="danger" @dismiss="dismissError">
+ {{ $options.i18n.errorMsg }}
+ </gl-alert>
+ <div v-if="loading"><gl-loading-icon size="lg" class="gl-mt-5" /></div>
+ <div v-if="alert" class="alert-management-details gl-relative">
+ <div
+ class="gl-display-flex gl-justify-content-space-between gl-align-items-baseline gl-px-1 py-3 py-md-4 gl-border-b-1 gl-border-b-gray-200 gl-border-b-solid flex-column flex-sm-row"
+ >
+ <div
+ data-testid="alert-header"
+ class="gl-display-flex gl-align-items-center gl-justify-content-center"
+ >
+ <div
+ class="gl-display-inline-flex gl-align-items-center gl-justify-content-space-between"
+ >
+ <gl-icon
+ class="gl-mr-3 align-middle"
+ :size="12"
+ :name="`severity-${alert.severity.toLowerCase()}`"
+ :class="`icon-${alert.severity.toLowerCase()}`"
+ />
+ <strong>{{ $options.severityLabels[alert.severity] }}</strong>
+ </div>
+ <span class="mx-2">&bull;</span>
+ <gl-sprintf :message="reportedAtMessage">
+ <template #when>
+ <time-ago-tooltip :time="alert.createdAt" class="gl-ml-3" />
+ </template>
+ <template #tool>{{ alert.monitoringTool }}</template>
+ </gl-sprintf>
+ </div>
+ <gl-button
+ v-if="glFeatures.createIssueFromAlertEnabled"
+ class="gl-mt-3 mt-sm-0 align-self-center align-self-sm-baseline alert-details-create-issue-button"
+ data-testid="createIssueBtn"
+ :href="newIssuePath"
+ category="primary"
+ variant="success"
+ >
+ {{ s__('AlertManagement|Create issue') }}
+ </gl-button>
+ </div>
+ <div
+ v-if="alert"
+ class="gl-display-flex gl-justify-content-space-between gl-align-items-center"
+ >
+ <h2 data-testid="title">{{ alert.title }}</h2>
+ </div>
+ <gl-dropdown :text="$options.statuses[alert.status]" class="gl-absolute gl-right-0" right>
+ <gl-dropdown-item
+ v-for="(label, field) in $options.statuses"
+ :key="field"
+ data-testid="statusDropdownItem"
+ class="gl-vertical-align-middle"
+ @click="updateAlertStatus(label)"
+ >
+ <span class="d-flex">
+ <gl-icon
+ class="flex-shrink-0 append-right-4"
+ :class="{ invisible: label.toUpperCase() !== alert.status }"
+ name="mobile-issue-close"
+ />
+ {{ label }}
+ </span>
+ </gl-dropdown-item>
+ </gl-dropdown>
+ <gl-tabs v-if="alert" data-testid="alertDetailsTabs">
+ <gl-tab data-testid="overviewTab" :title="$options.i18n.overviewTitle">
+ <ul class="pl-4 mb-n1">
+ <li v-if="alert.startedAt" class="my-2">
+ <strong class="bold">{{ s__('AlertManagement|Start time') }}:</strong>
+ <time-ago-tooltip data-testid="startTimeItem" :time="alert.startedAt" />
+ </li>
+ <li v-if="alert.eventCount" class="my-2">
+ <strong class="bold">{{ s__('AlertManagement|Events') }}:</strong>
+ <span data-testid="eventCount">{{ alert.eventCount }}</span>
+ </li>
+ <li v-if="alert.monitoringTool" class="my-2">
+ <strong class="bold">{{ s__('AlertManagement|Tool') }}:</strong>
+ <span data-testid="monitoringTool">{{ alert.monitoringTool }}</span>
+ </li>
+ <li v-if="alert.service" class="my-2">
+ <strong class="bold">{{ s__('AlertManagement|Service') }}:</strong>
+ <span data-testid="service">{{ alert.service }}</span>
+ </li>
+ </ul>
+ </gl-tab>
+ <gl-tab data-testid="fullDetailsTab" :title="$options.i18n.fullAlertDetailsTitle">
+ <gl-table
+ class="alert-management-details-table"
+ :items="[{ key: 'Value', ...alert }]"
+ :show-empty="true"
+ :busy="loading"
+ stacked
+ >
+ <template #empty>
+ {{ s__('AlertManagement|No alert data to display.') }}
+ </template>
+ <template #table-busy>
+ <gl-loading-icon size="lg" color="dark" class="mt-3" />
+ </template>
+ </gl-table>
+ </gl-tab>
+ </gl-tabs>
+ </div>
+ </div>
+</template>
diff --git a/app/assets/javascripts/alert_management/components/alert_management_list.vue b/app/assets/javascripts/alert_management/components/alert_management_list.vue
new file mode 100644
index 00000000000..74fc19ff3d4
--- /dev/null
+++ b/app/assets/javascripts/alert_management/components/alert_management_list.vue
@@ -0,0 +1,303 @@
+<script>
+import {
+ GlEmptyState,
+ GlDeprecatedButton,
+ GlLoadingIcon,
+ GlTable,
+ GlAlert,
+ GlIcon,
+ GlDropdown,
+ GlDropdownItem,
+ GlTabs,
+ GlTab,
+} from '@gitlab/ui';
+import createFlash from '~/flash';
+import { s__ } from '~/locale';
+import { joinPaths, visitUrl } from '~/lib/utils/url_utility';
+import TimeAgo from '~/vue_shared/components/time_ago_tooltip.vue';
+import getAlerts from '../graphql/queries/getAlerts.query.graphql';
+import { ALERTS_STATUS, ALERTS_STATUS_TABS, ALERTS_SEVERITY_LABELS } from '../constants';
+import glFeatureFlagsMixin from '~/vue_shared/mixins/gl_feature_flags_mixin';
+import updateAlertStatus from '../graphql/mutations/update_alert_status.graphql';
+import { capitalizeFirstCharacter } from '~/lib/utils/text_utility';
+
+const tdClass = 'table-col d-flex d-md-table-cell align-items-center';
+const bodyTrClass =
+ 'gl-border-1 gl-border-t-solid gl-border-gray-100 hover-bg-blue-50 hover-gl-cursor-pointer hover-gl-border-b-solid hover-gl-border-blue-200';
+
+export default {
+ bodyTrClass,
+ i18n: {
+ noAlertsMsg: s__(
+ "AlertManagement|No alerts available to display. If you think you're seeing this message in error, refresh the page.",
+ ),
+ errorMsg: s__(
+ "AlertManagement|There was an error displaying the alerts. Confirm your endpoint's configuration details to ensure alerts appear.",
+ ),
+ },
+ fields: [
+ {
+ key: 'severity',
+ label: s__('AlertManagement|Severity'),
+ tdClass: `${tdClass} rounded-top text-capitalize`,
+ },
+ {
+ key: 'startedAt',
+ label: s__('AlertManagement|Start time'),
+ tdClass,
+ },
+ {
+ key: 'endedAt',
+ label: s__('AlertManagement|End time'),
+ tdClass,
+ },
+ {
+ key: 'title',
+ label: s__('AlertManagement|Alert'),
+ thClass: 'w-30p',
+ tdClass,
+ },
+ {
+ key: 'eventCount',
+ label: s__('AlertManagement|Events'),
+ thClass: 'text-right event-count',
+ tdClass: `${tdClass} text-md-right event-count`,
+ },
+ {
+ key: 'status',
+ thClass: 'w-15p',
+ label: s__('AlertManagement|Status'),
+ tdClass: `${tdClass} rounded-bottom`,
+ },
+ ],
+ statuses: {
+ [ALERTS_STATUS.TRIGGERED]: s__('AlertManagement|Triggered'),
+ [ALERTS_STATUS.ACKNOWLEDGED]: s__('AlertManagement|Acknowledged'),
+ [ALERTS_STATUS.RESOLVED]: s__('AlertManagement|Resolved'),
+ },
+ severityLabels: ALERTS_SEVERITY_LABELS,
+ statusTabs: ALERTS_STATUS_TABS,
+ components: {
+ GlEmptyState,
+ GlLoadingIcon,
+ GlTable,
+ GlAlert,
+ GlDeprecatedButton,
+ TimeAgo,
+ GlDropdown,
+ GlDropdownItem,
+ GlIcon,
+ GlTabs,
+ GlTab,
+ },
+ mixins: [glFeatureFlagsMixin()],
+ props: {
+ projectPath: {
+ type: String,
+ required: true,
+ },
+ alertManagementEnabled: {
+ type: Boolean,
+ required: true,
+ },
+ enableAlertManagementPath: {
+ type: String,
+ required: true,
+ },
+ userCanEnableAlertManagement: {
+ type: Boolean,
+ required: true,
+ },
+ emptyAlertSvgPath: {
+ type: String,
+ required: true,
+ },
+ },
+ apollo: {
+ alerts: {
+ query: getAlerts,
+ variables() {
+ return {
+ projectPath: this.projectPath,
+ statuses: this.statusFilter,
+ };
+ },
+ update(data) {
+ return data.project.alertManagementAlerts.nodes;
+ },
+ error() {
+ this.errored = true;
+ },
+ },
+ },
+ data() {
+ return {
+ alerts: null,
+ errored: false,
+ isAlertDismissed: false,
+ isErrorAlertDismissed: false,
+ statusFilter: this.$options.statusTabs[4].filters,
+ };
+ },
+ computed: {
+ showNoAlertsMsg() {
+ return !this.errored && !this.loading && !this.alerts?.length && !this.isAlertDismissed;
+ },
+ showErrorMsg() {
+ return this.errored && !this.isErrorAlertDismissed;
+ },
+ loading() {
+ return this.$apollo.queries.alerts.loading;
+ },
+ },
+ methods: {
+ filterAlertsByStatus(tabIndex) {
+ this.statusFilter = this.$options.statusTabs[tabIndex].filters;
+ },
+ capitalizeFirstCharacter,
+ updateAlertStatus(status, iid) {
+ this.$apollo
+ .mutate({
+ mutation: updateAlertStatus,
+ variables: {
+ iid,
+ status: status.toUpperCase(),
+ projectPath: this.projectPath,
+ },
+ })
+ .then(() => {
+ this.$apollo.queries.alerts.refetch();
+ })
+ .catch(() => {
+ createFlash(
+ s__(
+ 'AlertManagement|There was an error while updating the status of the alert. Please try again.',
+ ),
+ );
+ });
+ },
+ navigateToAlertDetails({ iid }) {
+ return visitUrl(joinPaths(window.location.pathname, iid, 'details'));
+ },
+ },
+};
+</script>
+<template>
+ <div>
+ <div v-if="alertManagementEnabled" class="alert-management-list">
+ <gl-alert v-if="showNoAlertsMsg" @dismiss="isAlertDismissed = true">
+ {{ $options.i18n.noAlertsMsg }}
+ </gl-alert>
+ <gl-alert v-if="showErrorMsg" variant="danger" @dismiss="isErrorAlertDismissed = true">
+ {{ $options.i18n.errorMsg }}
+ </gl-alert>
+
+ <gl-tabs v-if="glFeatures.alertListStatusFilteringEnabled" @input="filterAlertsByStatus">
+ <gl-tab v-for="tab in $options.statusTabs" :key="tab.status">
+ <template slot="title">
+ <span>{{ tab.title }}</span>
+ </template>
+ </gl-tab>
+ </gl-tabs>
+
+ <h4 class="d-block d-md-none my-3">
+ {{ s__('AlertManagement|Alerts') }}
+ </h4>
+ <gl-table
+ class="alert-management-table mt-3"
+ :items="alerts"
+ :fields="$options.fields"
+ :show-empty="true"
+ :busy="loading"
+ stacked="md"
+ :tbody-tr-class="$options.bodyTrClass"
+ @row-clicked="navigateToAlertDetails"
+ >
+ <template #cell(severity)="{ item }">
+ <div
+ class="d-inline-flex align-items-center justify-content-between"
+ data-testid="severityField"
+ >
+ <gl-icon
+ class="mr-2"
+ :size="12"
+ :name="`severity-${item.severity.toLowerCase()}`"
+ :class="`icon-${item.severity.toLowerCase()}`"
+ />
+ {{ $options.severityLabels[item.severity] }}
+ </div>
+ </template>
+
+ <template #cell(startedAt)="{ item }">
+ <time-ago v-if="item.startedAt" :time="item.startedAt" />
+ </template>
+
+ <template #cell(endedAt)="{ item }">
+ <time-ago v-if="item.endedAt" :time="item.endedAt" />
+ </template>
+
+ <template #cell(title)="{ item }">
+ <div class="gl-max-w-full text-truncate">{{ item.title }}</div>
+ </template>
+
+ <template #cell(status)="{ item }">
+ <gl-dropdown
+ :text="capitalizeFirstCharacter(item.status.toLowerCase())"
+ class="w-100"
+ right
+ >
+ <gl-dropdown-item
+ v-for="(label, field) in $options.statuses"
+ :key="field"
+ @click="updateAlertStatus(label, item.iid)"
+ >
+ <span class="d-flex">
+ <gl-icon
+ class="flex-shrink-0 append-right-4"
+ :class="{ invisible: label.toUpperCase() !== item.status }"
+ name="mobile-issue-close"
+ />
+ {{ label }}
+ </span>
+ </gl-dropdown-item>
+ </gl-dropdown>
+ </template>
+
+ <template #empty>
+ {{ s__('AlertManagement|No alerts to display.') }}
+ </template>
+
+ <template #table-busy>
+ <gl-loading-icon size="lg" color="dark" class="mt-3" />
+ </template>
+ </gl-table>
+ </div>
+ <gl-empty-state
+ v-else
+ :title="s__('AlertManagement|Surface alerts in GitLab')"
+ :svg-path="emptyAlertSvgPath"
+ >
+ <template #description>
+ <div class="d-block">
+ <span>{{
+ s__(
+ 'AlertManagement|Display alerts from all your monitoring tools directly within GitLab. Streamline the investigation of your alerts and the escalation of alerts to incidents.',
+ )
+ }}</span>
+ <a href="/help/user/project/operations/alert_management.html" target="_blank">
+ {{ s__('AlertManagement|More information') }}
+ </a>
+ </div>
+ <div v-if="userCanEnableAlertManagement" class="d-block center pt-4">
+ <gl-deprecated-button
+ category="primary"
+ variant="success"
+ :href="enableAlertManagementPath"
+ >
+ {{ s__('AlertManagement|Authorize external service') }}
+ </gl-deprecated-button>
+ </div>
+ </template>
+ </gl-empty-state>
+ </div>
+</template>
diff --git a/app/assets/javascripts/alert_management/constants.js b/app/assets/javascripts/alert_management/constants.js
new file mode 100644
index 00000000000..9df01d9d0b5
--- /dev/null
+++ b/app/assets/javascripts/alert_management/constants.js
@@ -0,0 +1,46 @@
+import { s__ } from '~/locale';
+
+export const ALERTS_SEVERITY_LABELS = {
+ CRITICAL: s__('AlertManagement|Critical'),
+ HIGH: s__('AlertManagement|High'),
+ MEDIUM: s__('AlertManagement|Medium'),
+ LOW: s__('AlertManagement|Low'),
+ INFO: s__('AlertManagement|Info'),
+ UNKNOWN: s__('AlertManagement|Unknown'),
+};
+
+export const ALERTS_STATUS = {
+ OPEN: 'OPEN',
+ TRIGGERED: 'TRIGGERED',
+ ACKNOWLEDGED: 'ACKNOWLEDGED',
+ RESOLVED: 'RESOLVED',
+ ALL: 'ALL',
+};
+
+export const ALERTS_STATUS_TABS = [
+ {
+ title: s__('AlertManagement|Open'),
+ status: ALERTS_STATUS.OPEN,
+ filters: [ALERTS_STATUS.TRIGGERED, ALERTS_STATUS.ACKNOWLEDGED],
+ },
+ {
+ title: s__('AlertManagement|Triggered'),
+ status: ALERTS_STATUS.TRIGGERED,
+ filters: [ALERTS_STATUS.TRIGGERED],
+ },
+ {
+ title: s__('AlertManagement|Acknowledged'),
+ status: ALERTS_STATUS.ACKNOWLEDGED,
+ filters: [ALERTS_STATUS.ACKNOWLEDGED],
+ },
+ {
+ title: s__('AlertManagement|Resolved'),
+ status: ALERTS_STATUS.RESOLVED,
+ filters: [ALERTS_STATUS.RESOLVED],
+ },
+ {
+ title: s__('AlertManagement|All alerts'),
+ status: ALERTS_STATUS.ALL,
+ filters: [ALERTS_STATUS.TRIGGERED, ALERTS_STATUS.ACKNOWLEDGED, ALERTS_STATUS.RESOLVED],
+ },
+];
diff --git a/app/assets/javascripts/alert_management/details.js b/app/assets/javascripts/alert_management/details.js
new file mode 100644
index 00000000000..d3523e0a29d
--- /dev/null
+++ b/app/assets/javascripts/alert_management/details.js
@@ -0,0 +1,47 @@
+import Vue from 'vue';
+import VueApollo from 'vue-apollo';
+import createDefaultClient from '~/lib/graphql';
+import { defaultDataIdFromObject } from 'apollo-cache-inmemory';
+import AlertDetails from './components/alert_details.vue';
+
+Vue.use(VueApollo);
+
+export default selector => {
+ const domEl = document.querySelector(selector);
+ const { alertId, projectPath, newIssuePath } = domEl.dataset;
+
+ const apolloProvider = new VueApollo({
+ defaultClient: createDefaultClient(
+ {},
+ {
+ cacheConfig: {
+ dataIdFromObject: object => {
+ // eslint-disable-next-line no-underscore-dangle
+ if (object.__typename === 'AlertManagementAlert') {
+ return object.iid;
+ }
+ return defaultDataIdFromObject(object);
+ },
+ },
+ },
+ ),
+ });
+
+ // eslint-disable-next-line no-new
+ new Vue({
+ el: selector,
+ apolloProvider,
+ components: {
+ AlertDetails,
+ },
+ render(createElement) {
+ return createElement('alert-details', {
+ props: {
+ alertId,
+ projectPath,
+ newIssuePath,
+ },
+ });
+ },
+ });
+};
diff --git a/app/assets/javascripts/alert_management/graphql/fragments/detailItem.fragment.graphql b/app/assets/javascripts/alert_management/graphql/fragments/detailItem.fragment.graphql
new file mode 100644
index 00000000000..df802616e97
--- /dev/null
+++ b/app/assets/javascripts/alert_management/graphql/fragments/detailItem.fragment.graphql
@@ -0,0 +1,11 @@
+#import "./listItem.fragment.graphql"
+
+fragment AlertDetailItem on AlertManagementAlert {
+ ...AlertListItem
+ createdAt
+ monitoringTool
+ service
+ description
+ updatedAt
+ details
+}
diff --git a/app/assets/javascripts/alert_management/graphql/fragments/listItem.fragment.graphql b/app/assets/javascripts/alert_management/graphql/fragments/listItem.fragment.graphql
new file mode 100644
index 00000000000..fffe07b0cfd
--- /dev/null
+++ b/app/assets/javascripts/alert_management/graphql/fragments/listItem.fragment.graphql
@@ -0,0 +1,9 @@
+fragment AlertListItem on AlertManagementAlert {
+ iid
+ title
+ severity
+ status
+ startedAt
+ endedAt
+ eventCount
+}
diff --git a/app/assets/javascripts/alert_management/graphql/mutations/update_alert_status.graphql b/app/assets/javascripts/alert_management/graphql/mutations/update_alert_status.graphql
new file mode 100644
index 00000000000..009ae0b2930
--- /dev/null
+++ b/app/assets/javascripts/alert_management/graphql/mutations/update_alert_status.graphql
@@ -0,0 +1,9 @@
+mutation ($projectPath: ID!, $status: AlertManagementStatus!, $iid: String!) {
+ updateAlertStatus(input: { iid: $iid, status: $status, projectPath: $projectPath }) {
+ errors
+ alert {
+ iid,
+ status,
+ }
+ }
+}
diff --git a/app/assets/javascripts/alert_management/graphql/queries/details.query.graphql b/app/assets/javascripts/alert_management/graphql/queries/details.query.graphql
new file mode 100644
index 00000000000..7c77715fad2
--- /dev/null
+++ b/app/assets/javascripts/alert_management/graphql/queries/details.query.graphql
@@ -0,0 +1,11 @@
+#import "../fragments/detailItem.fragment.graphql"
+
+query alertDetails($fullPath: ID!, $alertId: String) {
+ project(fullPath: $fullPath) {
+ alertManagementAlerts(iid: $alertId) {
+ nodes {
+ ...AlertDetailItem
+ }
+ }
+ }
+}
diff --git a/app/assets/javascripts/alert_management/graphql/queries/getAlerts.query.graphql b/app/assets/javascripts/alert_management/graphql/queries/getAlerts.query.graphql
new file mode 100644
index 00000000000..54b66389d5b
--- /dev/null
+++ b/app/assets/javascripts/alert_management/graphql/queries/getAlerts.query.graphql
@@ -0,0 +1,11 @@
+#import "../fragments/listItem.fragment.graphql"
+
+query getAlerts($projectPath: ID!, $statuses: [AlertManagementStatus!]) {
+ project(fullPath: $projectPath) {
+ alertManagementAlerts(statuses: $statuses) {
+ nodes {
+ ...AlertListItem
+ }
+ }
+ }
+}
diff --git a/app/assets/javascripts/alert_management/list.js b/app/assets/javascripts/alert_management/list.js
new file mode 100644
index 00000000000..cae6a536b56
--- /dev/null
+++ b/app/assets/javascripts/alert_management/list.js
@@ -0,0 +1,55 @@
+import Vue from 'vue';
+import VueApollo from 'vue-apollo';
+import createDefaultClient from '~/lib/graphql';
+import { defaultDataIdFromObject } from 'apollo-cache-inmemory';
+import { parseBoolean } from '~/lib/utils/common_utils';
+import AlertManagementList from './components/alert_management_list.vue';
+
+Vue.use(VueApollo);
+
+export default () => {
+ const selector = '#js-alert_management';
+
+ const domEl = document.querySelector(selector);
+ const { projectPath, enableAlertManagementPath, emptyAlertSvgPath } = domEl.dataset;
+ let { alertManagementEnabled, userCanEnableAlertManagement } = domEl.dataset;
+
+ alertManagementEnabled = parseBoolean(alertManagementEnabled);
+ userCanEnableAlertManagement = parseBoolean(userCanEnableAlertManagement);
+
+ const apolloProvider = new VueApollo({
+ defaultClient: createDefaultClient(
+ {},
+ {
+ cacheConfig: {
+ dataIdFromObject: object => {
+ // eslint-disable-next-line no-underscore-dangle
+ if (object.__typename === 'AlertManagementAlert') {
+ return object.iid;
+ }
+ return defaultDataIdFromObject(object);
+ },
+ },
+ },
+ ),
+ });
+
+ return new Vue({
+ el: selector,
+ apolloProvider,
+ components: {
+ AlertManagementList,
+ },
+ render(createElement) {
+ return createElement('alert-management-list', {
+ props: {
+ projectPath,
+ enableAlertManagementPath,
+ emptyAlertSvgPath,
+ alertManagementEnabled,
+ userCanEnableAlertManagement,
+ },
+ });
+ },
+ });
+};
diff --git a/app/assets/javascripts/alert_management/services/index.js b/app/assets/javascripts/alert_management/services/index.js
new file mode 100644
index 00000000000..787603d3e7a
--- /dev/null
+++ b/app/assets/javascripts/alert_management/services/index.js
@@ -0,0 +1,7 @@
+import axios from '~/lib/utils/axios_utils';
+
+export default {
+ getAlertManagementList({ endpoint }) {
+ return axios.get(endpoint);
+ },
+};
diff --git a/app/assets/javascripts/alerts_service_settings/components/alerts_service_form.vue b/app/assets/javascripts/alerts_service_settings/components/alerts_service_form.vue
index 785598142fe..410c5c00e8a 100644
--- a/app/assets/javascripts/alerts_service_settings/components/alerts_service_form.vue
+++ b/app/assets/javascripts/alerts_service_settings/components/alerts_service_form.vue
@@ -6,7 +6,7 @@ import {
GlModal,
GlModalDirective,
} from '@gitlab/ui';
-import { escape as esc } from 'lodash';
+import { escape } from 'lodash';
import ClipboardButton from '~/vue_shared/components/clipboard_button.vue';
import ToggleButton from '~/vue_shared/components/toggle_button.vue';
import axios from '~/lib/utils/axios_utils';
@@ -65,7 +65,7 @@ export default {
'AlertService|%{linkStart}Learn more%{linkEnd} about configuring this endpoint to receive alerts.',
),
{
- linkStart: `<a href="${esc(
+ linkStart: `<a href="${escape(
this.learnMoreUrl,
)}" target="_blank" rel="noopener noreferrer">`,
linkEnd: '</a>',
diff --git a/app/assets/javascripts/api.js b/app/assets/javascripts/api.js
index 6301f6a3910..e527659a939 100644
--- a/app/assets/javascripts/api.js
+++ b/app/assets/javascripts/api.js
@@ -23,6 +23,8 @@ const Api = {
projectMergeRequestVersionsPath: '/api/:version/projects/:id/merge_requests/:mrid/versions',
projectRunnersPath: '/api/:version/projects/:id/runners',
projectProtectedBranchesPath: '/api/:version/projects/:id/protected_branches',
+ projectSearchPath: '/api/:version/projects/:id/search',
+ projectMilestonesPath: '/api/:version/projects/:id/milestones',
mergeRequestsPath: '/api/:version/merge_requests',
groupLabelsPath: '/groups/:namespace_path/-/labels',
issuableTemplatePath: '/:namespace_path/:project_path/templates/:type/:key',
@@ -46,6 +48,7 @@ const Api = {
mergeRequestsPipeline: '/api/:version/projects/:id/merge_requests/:merge_request_iid/pipelines',
adminStatisticsPath: '/api/:version/application/statistics',
pipelineSinglePath: '/api/:version/projects/:id/pipelines/:pipeline_id',
+ pipelinesPath: '/api/:version/projects/:id/pipelines/',
environmentsPath: '/api/:version/projects/:id/environments',
rawFilePath: '/api/:version/projects/:id/repository/files/:path/raw',
@@ -74,13 +77,11 @@ const Api = {
const url = Api.buildUrl(Api.groupsPath);
return axios
.get(url, {
- params: Object.assign(
- {
- search: query,
- per_page: DEFAULT_PER_PAGE,
- },
- options,
- ),
+ params: {
+ search: query,
+ per_page: DEFAULT_PER_PAGE,
+ ...options,
+ },
})
.then(({ data }) => {
callback(data);
@@ -247,6 +248,23 @@ const Api = {
.then(({ data }) => data);
},
+ projectSearch(id, options = {}) {
+ const url = Api.buildUrl(Api.projectSearchPath).replace(':id', encodeURIComponent(id));
+
+ return axios.get(url, {
+ params: {
+ search: options.search,
+ scope: options.scope,
+ },
+ });
+ },
+
+ projectMilestones(id) {
+ const url = Api.buildUrl(Api.projectMilestonesPath).replace(':id', encodeURIComponent(id));
+
+ return axios.get(url);
+ },
+
mergeRequests(params = {}) {
const url = Api.buildUrl(Api.mergeRequestsPath);
@@ -281,7 +299,7 @@ const Api = {
};
return axios
.get(url, {
- params: Object.assign({}, defaults, options),
+ params: { ...defaults, ...options },
})
.then(({ data }) => callback(data))
.catch(() => flash(__('Something went wrong while fetching projects')));
@@ -364,13 +382,11 @@ const Api = {
users(query, options) {
const url = Api.buildUrl(this.usersPath);
return axios.get(url, {
- params: Object.assign(
- {
- search: query,
- per_page: DEFAULT_PER_PAGE,
- },
- options,
- ),
+ params: {
+ search: query,
+ per_page: DEFAULT_PER_PAGE,
+ ...options,
+ },
});
},
@@ -401,7 +417,7 @@ const Api = {
};
return axios
.get(url, {
- params: Object.assign({}, defaults, options),
+ params: { ...defaults, ...options },
})
.then(({ data }) => callback(data))
.catch(() => flash(__('Something went wrong while fetching projects')));
@@ -502,6 +518,15 @@ const Api = {
return axios.get(url);
},
+ // Return all pipelines for a project or filter by query params
+ pipelines(id, options = {}) {
+ const url = Api.buildUrl(this.pipelinesPath).replace(':id', encodeURIComponent(id));
+
+ return axios.get(url, {
+ params: options,
+ });
+ },
+
environments(id) {
const url = Api.buildUrl(this.environmentsPath).replace(':id', encodeURIComponent(id));
return axios.get(url);
diff --git a/app/assets/javascripts/autosave.js b/app/assets/javascripts/autosave.js
index 07d79ea1c70..5f50fcc112e 100644
--- a/app/assets/javascripts/autosave.js
+++ b/app/assets/javascripts/autosave.js
@@ -3,7 +3,7 @@
import AccessorUtilities from './lib/utils/accessor';
export default class Autosave {
- constructor(field, key, fallbackKey) {
+ constructor(field, key, fallbackKey, lockVersion) {
this.field = field;
this.isLocalStorageAvailable = AccessorUtilities.isLocalStorageAccessSafe();
@@ -12,6 +12,8 @@ export default class Autosave {
}
this.key = `autosave/${key}`;
this.fallbackKey = fallbackKey;
+ this.lockVersionKey = `${this.key}/lockVersion`;
+ this.lockVersion = lockVersion;
this.field.data('autosave', this);
this.restore();
this.field.on('input', () => this.save());
@@ -40,6 +42,11 @@ export default class Autosave {
}
}
+ getSavedLockVersion() {
+ if (!this.isLocalStorageAvailable) return;
+ return window.localStorage.getItem(this.lockVersionKey);
+ }
+
save() {
if (!this.field.length) return;
@@ -49,6 +56,9 @@ export default class Autosave {
if (this.fallbackKey) {
window.localStorage.setItem(this.fallbackKey, text);
}
+ if (this.lockVersion !== undefined) {
+ window.localStorage.setItem(this.lockVersionKey, this.lockVersion);
+ }
return window.localStorage.setItem(this.key, text);
}
@@ -58,6 +68,7 @@ export default class Autosave {
reset() {
if (!this.isLocalStorageAvailable) return;
+ window.localStorage.removeItem(this.lockVersionKey);
window.localStorage.removeItem(this.fallbackKey);
return window.localStorage.removeItem(this.key);
}
diff --git a/app/assets/javascripts/awards_handler.js b/app/assets/javascripts/awards_handler.js
index 67164997bd8..8381b050900 100644
--- a/app/assets/javascripts/awards_handler.js
+++ b/app/assets/javascripts/awards_handler.js
@@ -1,7 +1,7 @@
/* eslint-disable class-methods-use-this, @gitlab/require-i18n-strings */
import $ from 'jquery';
-import _ from 'underscore';
+import { uniq } from 'lodash';
import { GlBreakpointInstance as bp } from '@gitlab/ui/dist/utils';
import Cookies from 'js-cookie';
import { __ } from './locale';
@@ -513,7 +513,7 @@ export class AwardsHandler {
addEmojiToFrequentlyUsedList(emoji) {
if (this.emoji.isEmojiNameValid(emoji)) {
- this.frequentlyUsedEmojis = _.uniq(this.getFrequentlyUsedEmojis().concat(emoji));
+ this.frequentlyUsedEmojis = uniq(this.getFrequentlyUsedEmojis().concat(emoji));
Cookies.set('frequently_used_emojis', this.frequentlyUsedEmojis.join(','), { expires: 365 });
}
}
@@ -522,9 +522,7 @@ export class AwardsHandler {
return (
this.frequentlyUsedEmojis ||
(() => {
- const frequentlyUsedEmojis = _.uniq(
- (Cookies.get('frequently_used_emojis') || '').split(','),
- );
+ const frequentlyUsedEmojis = uniq((Cookies.get('frequently_used_emojis') || '').split(','));
this.frequentlyUsedEmojis = frequentlyUsedEmojis.filter(inputName =>
this.emoji.isEmojiNameValid(inputName),
);
diff --git a/app/assets/javascripts/behaviors/copy_to_clipboard.js b/app/assets/javascripts/behaviors/copy_to_clipboard.js
index c3541e62568..48bcba7bcca 100644
--- a/app/assets/javascripts/behaviors/copy_to_clipboard.js
+++ b/app/assets/javascripts/behaviors/copy_to_clipboard.js
@@ -17,10 +17,11 @@ function showTooltip(target, title) {
}
function genericSuccess(e) {
- showTooltip(e.trigger, __('Copied'));
// Clear the selection and blur the trigger so it loses its border
e.clearSelection();
$(e.trigger).blur();
+
+ showTooltip(e.trigger, __('Copied'));
}
/**
diff --git a/app/assets/javascripts/behaviors/markdown/marks/inline_html.js b/app/assets/javascripts/behaviors/markdown/marks/inline_html.js
index 7e020139fe7..f8465111959 100644
--- a/app/assets/javascripts/behaviors/markdown/marks/inline_html.js
+++ b/app/assets/javascripts/behaviors/markdown/marks/inline_html.js
@@ -1,7 +1,7 @@
/* eslint-disable class-methods-use-this */
import { Mark } from 'tiptap';
-import { escape as esc } from 'lodash';
+import { escape } from 'lodash';
// Transforms generated HTML back to GFM for Banzai::Filter::MarkdownFilter
export default class InlineHTML extends Mark {
@@ -35,7 +35,7 @@ export default class InlineHTML extends Mark {
mixable: true,
open(state, mark) {
return `<${mark.attrs.tag}${
- mark.attrs.title ? ` title="${state.esc(esc(mark.attrs.title))}"` : ''
+ mark.attrs.title ? ` title="${state.esc(escape(mark.attrs.title))}"` : ''
}>`;
},
close(state, mark) {
diff --git a/app/assets/javascripts/behaviors/markdown/paste_markdown_table.js b/app/assets/javascripts/behaviors/markdown/paste_markdown_table.js
index 665a7216424..278dd857ab8 100644
--- a/app/assets/javascripts/behaviors/markdown/paste_markdown_table.js
+++ b/app/assets/javascripts/behaviors/markdown/paste_markdown_table.js
@@ -47,7 +47,8 @@ export default class PasteMarkdownTable {
const htmlData = this.data.getData('text/html');
this.doc = new DOMParser().parseFromString(htmlData, 'text/html');
- const tables = this.doc.querySelectorAll('table');
+ // Avoid formatting lines that were copied from a diff
+ const tables = this.doc.querySelectorAll('table:not(.diff-wrap-lines)');
// We're only looking for exactly one table. If there happens to be
// multiple tables, it's possible an application copied data into
diff --git a/app/assets/javascripts/behaviors/markdown/render_gfm.js b/app/assets/javascripts/behaviors/markdown/render_gfm.js
index 137cc7b4669..01627b7206d 100644
--- a/app/assets/javascripts/behaviors/markdown/render_gfm.js
+++ b/app/assets/javascripts/behaviors/markdown/render_gfm.js
@@ -16,7 +16,7 @@ $.fn.renderGFM = function renderGFM() {
renderMath(this.find('.js-render-math'));
renderMermaid(this.find('.js-render-mermaid'));
highlightCurrentUser(this.find('.gfm-project_member').get());
- initUserPopovers(this.find('.gfm-project_member').get());
+ initUserPopovers(this.find('.js-user-link').get());
initMRPopovers(this.find('.gfm-merge_request').get());
renderMetrics(this.find('.js-render-metrics').get());
return this;
diff --git a/app/assets/javascripts/behaviors/markdown/render_mermaid.js b/app/assets/javascripts/behaviors/markdown/render_mermaid.js
index fe63ebd470d..057cdb6cc4c 100644
--- a/app/assets/javascripts/behaviors/markdown/render_mermaid.js
+++ b/app/assets/javascripts/behaviors/markdown/render_mermaid.js
@@ -24,13 +24,23 @@ let mermaidModule = {};
function importMermaidModule() {
return import(/* webpackChunkName: 'mermaid' */ 'mermaid')
.then(mermaid => {
+ let theme = 'neutral';
+
+ if (
+ window.gon?.user_color_scheme === 'dark' &&
+ // if on the Web IDE page
+ document.querySelector('.ide')
+ ) {
+ theme = 'dark';
+ }
+
mermaid.initialize({
// mermaid core options
mermaid: {
startOnLoad: false,
},
// mermaidAPI options
- theme: 'neutral',
+ theme,
flowchart: {
useMaxWidth: true,
htmlLabels: false,
diff --git a/app/assets/javascripts/behaviors/markdown/render_metrics.js b/app/assets/javascripts/behaviors/markdown/render_metrics.js
index 9260a89bd52..37cbce46b6f 100644
--- a/app/assets/javascripts/behaviors/markdown/render_metrics.js
+++ b/app/assets/javascripts/behaviors/markdown/render_metrics.js
@@ -1,15 +1,12 @@
import Vue from 'vue';
-import EmbedGroup from '~/monitoring/components/embeds/embed_group.vue';
import { createStore } from '~/monitoring/stores/embed_group/';
// TODO: Handle copy-pasting - https://gitlab.com/gitlab-org/gitlab-foss/issues/64369.
export default function renderMetrics(elements) {
if (!elements.length) {
- return;
+ return Promise.resolve();
}
- const EmbedGroupComponent = Vue.extend(EmbedGroup);
-
const wrapperList = [];
elements.forEach(element => {
@@ -31,14 +28,20 @@ export default function renderMetrics(elements) {
element.parentNode.removeChild(element);
});
- wrapperList.forEach(wrapper => {
- // eslint-disable-next-line no-new
- new EmbedGroupComponent({
- el: wrapper,
- store: createStore(),
- propsData: {
- urls: wrapper.urls,
- },
+ return import(
+ /* webpackChunkName: 'gfm_metrics' */ '~/monitoring/components/embeds/embed_group.vue'
+ ).then(({ default: EmbedGroup }) => {
+ const EmbedGroupComponent = Vue.extend(EmbedGroup);
+
+ wrapperList.forEach(wrapper => {
+ // eslint-disable-next-line no-new
+ new EmbedGroupComponent({
+ el: wrapper,
+ store: createStore(),
+ propsData: {
+ urls: wrapper.urls,
+ },
+ });
});
});
}
diff --git a/app/assets/javascripts/behaviors/shortcuts/shortcuts_blob.js b/app/assets/javascripts/behaviors/shortcuts/shortcuts_blob.js
index d5d8edd5ac0..c35a073b291 100644
--- a/app/assets/javascripts/behaviors/shortcuts/shortcuts_blob.js
+++ b/app/assets/javascripts/behaviors/shortcuts/shortcuts_blob.js
@@ -22,7 +22,7 @@ function eventHasModifierKeys(event) {
export default class ShortcutsBlob extends Shortcuts {
constructor(opts) {
- const options = Object.assign({}, defaults, opts);
+ const options = { ...defaults, ...opts };
super(options.skipResetBindings);
this.options = options;
diff --git a/app/assets/javascripts/blob/blob_fork_suggestion.js b/app/assets/javascripts/blob/blob_fork_suggestion.js
index 476b9405a9e..44dfbfcfe1c 100644
--- a/app/assets/javascripts/blob/blob_fork_suggestion.js
+++ b/app/assets/javascripts/blob/blob_fork_suggestion.js
@@ -17,7 +17,7 @@ const defaults = {
class BlobForkSuggestion {
constructor(options) {
- this.elementMap = Object.assign({}, defaults, options);
+ this.elementMap = { ...defaults, ...options };
this.onOpenButtonClick = this.onOpenButtonClick.bind(this);
this.onCancelButtonClick = this.onCancelButtonClick.bind(this);
}
diff --git a/app/assets/javascripts/blob/components/blob_content.vue b/app/assets/javascripts/blob/components/blob_content.vue
index 7d5d48cfc31..4f433bd8dfd 100644
--- a/app/assets/javascripts/blob/components/blob_content.vue
+++ b/app/assets/javascripts/blob/components/blob_content.vue
@@ -3,12 +3,19 @@ import { GlLoadingIcon } from '@gitlab/ui';
import { RichViewer, SimpleViewer } from '~/vue_shared/components/blob_viewers';
import BlobContentError from './blob_content_error.vue';
+import { BLOB_RENDER_EVENT_LOAD, BLOB_RENDER_EVENT_SHOW_SOURCE } from './constants';
+
export default {
components: {
GlLoadingIcon,
BlobContentError,
},
props: {
+ blob: {
+ type: Object,
+ required: false,
+ default: () => ({}),
+ },
content: {
type: String,
default: '',
@@ -37,6 +44,8 @@ export default {
return this.activeViewer.renderError;
},
},
+ BLOB_RENDER_EVENT_LOAD,
+ BLOB_RENDER_EVENT_SHOW_SOURCE,
};
</script>
<template>
@@ -44,7 +53,13 @@ export default {
<gl-loading-icon v-if="loading" size="md" color="dark" class="my-4 mx-auto" />
<template v-else>
- <blob-content-error v-if="viewerError" :viewer-error="viewerError" />
+ <blob-content-error
+ v-if="viewerError"
+ :viewer-error="viewerError"
+ :blob="blob"
+ @[$options.BLOB_RENDER_EVENT_LOAD]="$emit($options.BLOB_RENDER_EVENT_LOAD)"
+ @[$options.BLOB_RENDER_EVENT_SHOW_SOURCE]="$emit($options.BLOB_RENDER_EVENT_SHOW_SOURCE)"
+ />
<component
:is="viewer"
v-else
diff --git a/app/assets/javascripts/blob/components/blob_content_error.vue b/app/assets/javascripts/blob/components/blob_content_error.vue
index 0f1af0a962d..44dc4a6c727 100644
--- a/app/assets/javascripts/blob/components/blob_content_error.vue
+++ b/app/assets/javascripts/blob/components/blob_content_error.vue
@@ -1,15 +1,84 @@
<script>
+import { __ } from '~/locale';
+import { GlSprintf, GlLink } from '@gitlab/ui';
+import { BLOB_RENDER_ERRORS } from './constants';
+
export default {
+ components: {
+ GlSprintf,
+ GlLink,
+ },
props: {
viewerError: {
type: String,
required: true,
},
+ blob: {
+ type: Object,
+ required: false,
+ default: () => ({}),
+ },
+ },
+ computed: {
+ notStoredExternally() {
+ return this.viewerError !== BLOB_RENDER_ERRORS.REASONS.EXTERNAL.id;
+ },
+ renderErrorReason() {
+ const defaultReasonPath = Object.keys(BLOB_RENDER_ERRORS.REASONS).find(
+ reason => BLOB_RENDER_ERRORS.REASONS[reason].id === this.viewerError,
+ );
+ const defaultReason = BLOB_RENDER_ERRORS.REASONS[defaultReasonPath].text;
+ return this.notStoredExternally
+ ? defaultReason
+ : defaultReason[this.blob.externalStorage || 'default'];
+ },
+ renderErrorOptions() {
+ const load = {
+ ...BLOB_RENDER_ERRORS.OPTIONS.LOAD,
+ condition: this.shouldShowLoadBtn,
+ };
+ const showSource = {
+ ...BLOB_RENDER_ERRORS.OPTIONS.SHOW_SOURCE,
+ condition: this.shouldShowSourceBtn,
+ };
+ const download = {
+ ...BLOB_RENDER_ERRORS.OPTIONS.DOWNLOAD,
+ href: this.blob.rawPath,
+ };
+ return [load, showSource, download];
+ },
+ shouldShowLoadBtn() {
+ return this.viewerError === BLOB_RENDER_ERRORS.REASONS.COLLAPSED.id;
+ },
+ shouldShowSourceBtn() {
+ return this.blob.richViewer && this.blob.renderedAsText && this.notStoredExternally;
+ },
},
+ errorMessage: __(
+ 'This content could not be displayed because %{reason}. You can %{options} instead.',
+ ),
};
</script>
<template>
<div class="file-content code">
- <div class="text-center py-4" v-html="viewerError"></div>
+ <div class="text-center py-4">
+ <gl-sprintf :message="$options.errorMessage">
+ <template #reason>{{ renderErrorReason }}</template>
+ <template #options>
+ <template v-for="option in renderErrorOptions">
+ <span v-if="option.condition" :key="option.text">
+ <gl-link
+ :href="option.href"
+ :target="option.target"
+ :data-test-id="`option-${option.id}`"
+ @click="option.event && $emit(option.event)"
+ >{{ option.text }}</gl-link
+ >
+ {{ option.conjunction }}
+ </span>
+ </template>
+ </template>
+ </gl-sprintf>
+ </div>
</div>
</template>
diff --git a/app/assets/javascripts/blob/components/blob_edit_header.vue b/app/assets/javascripts/blob/components/blob_edit_header.vue
index e9b5ceda479..e1e1d76f721 100644
--- a/app/assets/javascripts/blob/components/blob_edit_header.vue
+++ b/app/assets/javascripts/blob/components/blob_edit_header.vue
@@ -5,6 +5,7 @@ export default {
components: {
GlFormInput,
},
+ inheritAttrs: false,
props: {
value: {
type: String,
@@ -27,8 +28,9 @@ export default {
s__('Snippets|Give your file a name to add code highlighting, e.g. example.rb for Ruby')
"
name="snippet_file_name"
- class="form-control js-snippet-file-name qa-snippet-file-name"
+ class="form-control js-snippet-file-name"
type="text"
+ v-bind="$attrs"
@change="$emit('input', name)"
/>
</div>
diff --git a/app/assets/javascripts/blob/components/blob_header.vue b/app/assets/javascripts/blob/components/blob_header.vue
index b7d9600ec40..e5e01caa9a5 100644
--- a/app/assets/javascripts/blob/components/blob_header.vue
+++ b/app/assets/javascripts/blob/components/blob_header.vue
@@ -66,7 +66,7 @@ export default {
</template>
</blob-filepath>
- <div class="file-actions d-none d-sm-block">
+ <div class="file-actions d-none d-sm-flex">
<viewer-switcher v-if="showViewerSwitcher" v-model="viewer" />
<slot name="actions"></slot>
diff --git a/app/assets/javascripts/blob/components/blob_header_filepath.vue b/app/assets/javascripts/blob/components/blob_header_filepath.vue
index 6c6a22e2b36..e9be7fbcf9b 100644
--- a/app/assets/javascripts/blob/components/blob_header_filepath.vue
+++ b/app/assets/javascripts/blob/components/blob_header_filepath.vue
@@ -28,12 +28,14 @@ export default {
<div class="file-header-content d-flex align-items-center lh-100">
<slot name="filepathPrepend"></slot>
- <file-icon :file-name="blob.path" :size="18" aria-hidden="true" css-classes="mr-2" />
- <strong
- v-if="blob.name"
- class="file-title-name qa-file-title-name mr-1 js-blob-header-filepath"
- >{{ blob.name }}</strong
- >
+ <template v-if="blob.path">
+ <file-icon :file-name="blob.path" :size="18" aria-hidden="true" css-classes="mr-2" />
+ <strong
+ class="file-title-name mr-1 js-blob-header-filepath"
+ data-qa-selector="file_title_name"
+ >{{ blob.path }}</strong
+ >
+ </template>
<small class="mr-2">{{ blobSize }}</small>
diff --git a/app/assets/javascripts/blob/components/blob_header_viewer_switcher.vue b/app/assets/javascripts/blob/components/blob_header_viewer_switcher.vue
index 7155a1d35b1..ed03213d7cf 100644
--- a/app/assets/javascripts/blob/components/blob_header_viewer_switcher.vue
+++ b/app/assets/javascripts/blob/components/blob_header_viewer_switcher.vue
@@ -45,7 +45,7 @@ export default {
};
</script>
<template>
- <gl-button-group class="js-blob-viewer-switcher ml-2">
+ <gl-button-group class="js-blob-viewer-switcher mx-2">
<gl-deprecated-button
v-gl-tooltip.hover
:aria-label="$options.SIMPLE_BLOB_VIEWER_TITLE"
diff --git a/app/assets/javascripts/blob/components/constants.js b/app/assets/javascripts/blob/components/constants.js
index d3fed9e51e9..93dceacabdd 100644
--- a/app/assets/javascripts/blob/components/constants.js
+++ b/app/assets/javascripts/blob/components/constants.js
@@ -1,4 +1,5 @@
-import { __ } from '~/locale';
+import { __, sprintf } from '~/locale';
+import { numberToHumanSize } from '~/lib/utils/number_utils';
export const BTN_COPY_CONTENTS_TITLE = __('Copy file contents');
export const BTN_RAW_TITLE = __('Open raw');
@@ -9,3 +10,56 @@ export const SIMPLE_BLOB_VIEWER_TITLE = __('Display source');
export const RICH_BLOB_VIEWER = 'rich';
export const RICH_BLOB_VIEWER_TITLE = __('Display rendered file');
+
+export const BLOB_RENDER_EVENT_LOAD = 'force-content-fetch';
+export const BLOB_RENDER_EVENT_SHOW_SOURCE = 'force-switch-viewer';
+
+export const BLOB_RENDER_ERRORS = {
+ REASONS: {
+ COLLAPSED: {
+ id: 'collapsed',
+ text: sprintf(__('it is larger than %{limit}'), {
+ limit: numberToHumanSize(1048576), // 1MB in bytes
+ }),
+ },
+ TOO_LARGE: {
+ id: 'too_large',
+ text: sprintf(__('it is larger than %{limit}'), {
+ limit: numberToHumanSize(104857600), // 100MB in bytes
+ }),
+ },
+ EXTERNAL: {
+ id: 'server_side_but_stored_externally',
+ text: {
+ lfs: __('it is stored in LFS'),
+ build_artifact: __('it is stored as a job artifact'),
+ default: __('it is stored externally'),
+ },
+ },
+ },
+ OPTIONS: {
+ LOAD: {
+ id: 'load',
+ text: __('load it anyway'),
+ conjunction: __('or'),
+ href: '#',
+ target: '',
+ event: BLOB_RENDER_EVENT_LOAD,
+ },
+ SHOW_SOURCE: {
+ id: 'show_source',
+ text: __('view the source'),
+ conjunction: __('or'),
+ href: '#',
+ target: '',
+ event: BLOB_RENDER_EVENT_SHOW_SOURCE,
+ },
+ DOWNLOAD: {
+ id: 'download',
+ text: __('download it'),
+ conjunction: '',
+ target: '_blank',
+ condition: true,
+ },
+ },
+};
diff --git a/app/assets/javascripts/blob/suggest_gitlab_ci_yml/components/popover.vue b/app/assets/javascripts/blob/suggest_gitlab_ci_yml/components/popover.vue
index 5023496e2c3..1e9e36feecc 100644
--- a/app/assets/javascripts/blob/suggest_gitlab_ci_yml/components/popover.vue
+++ b/app/assets/javascripts/blob/suggest_gitlab_ci_yml/components/popover.vue
@@ -72,9 +72,6 @@ export default {
dismissCookieName() {
return `${this.trackLabel}_${this.dismissKey}`;
},
- commitCookieName() {
- return `suggest_gitlab_ci_yml_commit_${this.dismissKey}`;
- },
},
mounted() {
if (
diff --git a/app/assets/javascripts/blob/utils.js b/app/assets/javascripts/blob/utils.js
index dc2ec642e59..840a3dbe450 100644
--- a/app/assets/javascripts/blob/utils.js
+++ b/app/assets/javascripts/blob/utils.js
@@ -1,22 +1,15 @@
-/* global ace */
import Editor from '~/editor/editor_lite';
export function initEditorLite({ el, blobPath, blobContent }) {
if (!el) {
throw new Error(`"el" parameter is required to initialize Editor`);
}
- let editor;
-
- if (window?.gon?.features?.monacoSnippets) {
- editor = new Editor();
- editor.createInstance({
- el,
- blobPath,
- blobContent,
- });
- } else {
- editor = ace.edit(el);
- }
+ const editor = new Editor();
+ editor.createInstance({
+ el,
+ blobPath,
+ blobContent,
+ });
return editor;
}
diff --git a/app/assets/javascripts/boards/components/board.js b/app/assets/javascripts/boards/components/board.js
index ac02c229a07..517a13ceb27 100644
--- a/app/assets/javascripts/boards/components/board.js
+++ b/app/assets/javascripts/boards/components/board.js
@@ -113,9 +113,6 @@ export default Vue.extend({
// eslint-disable-next-line @gitlab/require-i18n-strings
return `boards.${this.boardId}.${this.list.type}.${this.list.id}`;
},
- helpLink() {
- return boardsStore.scopedLabels.helpLink;
- },
},
watch: {
filter: {
diff --git a/app/assets/javascripts/boards/components/board_column.vue b/app/assets/javascripts/boards/components/board_column.vue
index 10c855675db..fb854616a04 100644
--- a/app/assets/javascripts/boards/components/board_column.vue
+++ b/app/assets/javascripts/boards/components/board_column.vue
@@ -113,9 +113,6 @@ export default {
// eslint-disable-next-line @gitlab/require-i18n-strings
return `boards.${this.boardId}.${this.list.type}.${this.list.id}`;
},
- helpLink() {
- return boardsStore.scopedLabels.helpLink;
- },
},
watch: {
filter: {
@@ -286,7 +283,6 @@ export default {
:background-color="list.label.color"
:description="list.label.description"
:scoped="showScopedLabels(list.label)"
- :scoped-labels-documentation-link="helpLink"
:size="!list.isExpanded ? 'sm' : ''"
:title="list.label.title"
tooltip-placement="bottom"
@@ -353,7 +349,7 @@ export default {
v-if="isSettingsShown"
ref="settingsBtn"
:aria-label="__(`List settings`)"
- class="no-drag rounded-right"
+ class="no-drag rounded-right js-board-settings-button"
title="List settings"
type="button"
@click="openSidebarSettings"
diff --git a/app/assets/javascripts/boards/components/board_form.vue b/app/assets/javascripts/boards/components/board_form.vue
index c0df8b72095..fbe221041c1 100644
--- a/app/assets/javascripts/boards/components/board_form.vue
+++ b/app/assets/javascripts/boards/components/board_form.vue
@@ -58,11 +58,6 @@ export default {
required: false,
default: false,
},
- scopedLabelsDocumentationLink: {
- type: String,
- required: false,
- default: '#',
- },
},
data() {
return {
@@ -182,7 +177,7 @@ export default {
@cancel="cancel"
@submit="submit"
>
- <template slot="body">
+ <template #body>
<p v-if="isDeleteForm">{{ __('Are you sure you want to delete this board?') }}</p>
<form v-else class="js-board-config-modal" @submit.prevent>
<div v-if="!readonly" class="append-bottom-20">
@@ -208,7 +203,6 @@ export default {
:can-admin-board="canAdminBoard"
:milestone-path="milestonePath"
:labels-path="labelsPath"
- :scoped-labels-documentation-link="scopedLabelsDocumentationLink"
:enable-scoped-labels="enableScopedLabels"
:project-id="projectId"
:group-id="groupId"
diff --git a/app/assets/javascripts/boards/components/board_sidebar.js b/app/assets/javascripts/boards/components/board_sidebar.js
index 66a5e134205..c8953158811 100644
--- a/app/assets/javascripts/boards/components/board_sidebar.js
+++ b/app/assets/javascripts/boards/components/board_sidebar.js
@@ -51,7 +51,7 @@ export default Vue.extend({
return Object.keys(this.issue).length;
},
milestoneTitle() {
- return this.issue.milestone ? this.issue.milestone.title : __('No Milestone');
+ return this.issue.milestone ? this.issue.milestone.title : __('No milestone');
},
canRemove() {
return !this.list.preset;
@@ -70,9 +70,6 @@ export default Vue.extend({
selectedLabels() {
return this.hasLabels ? this.issue.labels.map(l => l.title).join(',') : '';
},
- helpLink() {
- return boardsStore.scopedLabels.helpLink;
- },
},
watch: {
detail: {
diff --git a/app/assets/javascripts/boards/components/boards_selector.vue b/app/assets/javascripts/boards/components/boards_selector.vue
index f2c976be7ae..80db9930259 100644
--- a/app/assets/javascripts/boards/components/boards_selector.vue
+++ b/app/assets/javascripts/boards/components/boards_selector.vue
@@ -86,11 +86,6 @@ export default {
required: false,
default: false,
},
- scopedLabelsDocumentationLink: {
- type: String,
- required: false,
- default: '#',
- },
},
data() {
return {
@@ -348,7 +343,6 @@ export default {
:scoped-issue-board-feature-enabled="scopedIssueBoardFeatureEnabled"
:weights="weights"
:enable-scoped-labels="enabledScopedLabels"
- :scoped-labels-documentation-link="scopedLabelsDocumentationLink"
/>
</span>
</div>
diff --git a/app/assets/javascripts/boards/components/issue_card_inner.vue b/app/assets/javascripts/boards/components/issue_card_inner.vue
index daaa12c096b..a589fb325b2 100644
--- a/app/assets/javascripts/boards/components/issue_card_inner.vue
+++ b/app/assets/javascripts/boards/components/issue_card_inner.vue
@@ -102,9 +102,6 @@ export default {
orderedLabels() {
return sortBy(this.issue.labels.filter(this.isNonListLabel), 'title');
},
- helpLink() {
- return boardsStore.scopedLabels.helpLink;
- },
},
methods: {
isIndexLessThanlimit(index) {
@@ -181,7 +178,6 @@ export default {
:description="label.description"
size="sm"
:scoped="showScopedLabel(label)"
- :scoped-labels-documentation-link="helpLink"
@click="filterByLabel(label)"
/>
</template>
diff --git a/app/assets/javascripts/boards/constants.js b/app/assets/javascripts/boards/constants.js
index dcecfe5e1bb..f577a168e75 100644
--- a/app/assets/javascripts/boards/constants.js
+++ b/app/assets/javascripts/boards/constants.js
@@ -1,3 +1,8 @@
+export const BoardType = {
+ project: 'project',
+ group: 'group',
+};
+
export const ListType = {
assignee: 'assignee',
milestone: 'milestone',
@@ -8,6 +13,9 @@ export const ListType = {
blank: 'blank',
};
+export const inactiveListId = 0;
+
export default {
+ BoardType,
ListType,
};
diff --git a/app/assets/javascripts/boards/filtered_search_boards.js b/app/assets/javascripts/boards/filtered_search_boards.js
index b1b4b1c5508..ca85e54eb89 100644
--- a/app/assets/javascripts/boards/filtered_search_boards.js
+++ b/app/assets/javascripts/boards/filtered_search_boards.js
@@ -1,6 +1,6 @@
import IssuableFilteredSearchTokenKeys from 'ee_else_ce/filtered_search/issuable_filtered_search_token_keys';
import FilteredSearchContainer from '../filtered_search/container';
-import FilteredSearchManager from '../filtered_search/filtered_search_manager';
+import FilteredSearchManager from 'ee_else_ce/filtered_search/filtered_search_manager';
import boardsStore from './stores/boards_store';
export default class FilteredSearchBoards extends FilteredSearchManager {
diff --git a/app/assets/javascripts/boards/icons/fullscreen_collapse.svg b/app/assets/javascripts/boards/icons/fullscreen_collapse.svg
new file mode 100644
index 00000000000..6bd773dc4c5
--- /dev/null
+++ b/app/assets/javascripts/boards/icons/fullscreen_collapse.svg
@@ -0,0 +1 @@
+<svg width="17" height="17" viewBox="0 0 17 17" xmlns="http://www.w3.org/2000/svg"><path d="M.147 15.496l2.146-2.146-1.286-1.286a.55.55 0 0 1-.125-.616c.101-.238.277-.357.527-.357h4a.55.55 0 0 1 .402.17.55.55 0 0 1 .17.401v4c0 .25-.12.426-.358.527-.232.101-.437.06-.616-.125l-1.286-1.286-2.146 2.146-1.428-1.428zM14.996.646l1.428 1.43-2.146 2.145 1.286 1.286c.185.179.226.384.125.616-.101.238-.277.357-.527.357h-4a.55.55 0 0 1-.402-.17.55.55 0 0 1-.17-.401v-4c0-.25.12-.426.358-.527a.553.553 0 0 1 .616.125l1.286 1.286L14.996.647zm-13.42 0L3.72 2.794l1.286-1.286a.55.55 0 0 1 .616-.125c.238.101.357.277.357.527v4a.55.55 0 0 1-.17.402.55.55 0 0 1-.401.17h-4c-.25 0-.426-.12-.527-.358-.101-.232-.06-.437.125-.616l1.286-1.286L.147 2.075 1.575.647zm14.848 14.85l-1.428 1.428-2.146-2.146-1.286 1.286c-.179.185-.384.226-.616.125-.238-.101-.357-.277-.357-.527v-4a.55.55 0 0 1 .17-.402.55.55 0 0 1 .401-.17h4c.25 0 .426.12.527.358a.553.553 0 0 1-.125.616l-1.286 1.286 2.146 2.146z" fill-rule="evenodd"/></svg>
diff --git a/app/assets/javascripts/boards/icons/fullscreen_expand.svg b/app/assets/javascripts/boards/icons/fullscreen_expand.svg
new file mode 100644
index 00000000000..306073b8af2
--- /dev/null
+++ b/app/assets/javascripts/boards/icons/fullscreen_expand.svg
@@ -0,0 +1 @@
+<svg width="15" height="15" viewBox="0 0 15 15" xmlns="http://www.w3.org/2000/svg"><path d="M8.591 5.056l2.147-2.146-1.286-1.286a.55.55 0 0 1-.125-.616c.101-.238.277-.357.527-.357h4a.55.55 0 0 1 .402.17.55.55 0 0 1 .17.401v4c0 .25-.12.426-.358.527-.232.101-.437.06-.616-.125l-1.286-1.286-2.146 2.147-1.429-1.43zM5.018 8.553l1.429 1.43L4.3 12.127l1.286 1.286c.185.179.226.384.125.616-.101.238-.277.357-.527.357h-4a.55.55 0 0 1-.402-.17.55.55 0 0 1-.17-.401v-4c0-.25.12-.426.358-.527a.553.553 0 0 1 .616.125L2.872 10.7l2.146-2.147zm4.964 0l2.146 2.147 1.286-1.286a.55.55 0 0 1 .616-.125c.238.101.357.277.357.527v4a.55.55 0 0 1-.17.402.55.55 0 0 1-.401.17h-4c-.25 0-.426-.12-.527-.358-.101-.232-.06-.437.125-.616l1.286-1.286-2.147-2.146 1.43-1.429zM6.447 5.018l-1.43 1.429L2.873 4.3 1.586 5.586c-.179.185-.384.226-.616.125-.238-.101-.357-.277-.357-.527v-4a.55.55 0 0 1 .17-.402.55.55 0 0 1 .401-.17h4c.25 0 .426.12.527.358a.553.553 0 0 1-.125.616L4.3 2.872l2.147 2.146z" fill-rule="evenodd"/></svg>
diff --git a/app/assets/javascripts/boards/index.js b/app/assets/javascripts/boards/index.js
index a12db7a5f1a..9ff7575ae09 100644
--- a/app/assets/javascripts/boards/index.js
+++ b/app/assets/javascripts/boards/index.js
@@ -6,7 +6,6 @@ import 'ee_else_ce/boards/models/list';
import BoardSidebar from 'ee_else_ce/boards/components/board_sidebar';
import initNewListDropdown from 'ee_else_ce/boards/components/new_list_dropdown';
import boardConfigToggle from 'ee_else_ce/boards/config_toggle';
-import toggleFocusMode from 'ee_else_ce/boards/toggle_focus';
import toggleLabels from 'ee_else_ce/boards/toggle_labels';
import {
setPromotionState,
@@ -16,11 +15,15 @@ import {
getBoardsModalData,
} from 'ee_else_ce/boards/ee_functions';
+import VueApollo from 'vue-apollo';
+import createDefaultClient from '~/lib/graphql';
import Flash from '~/flash';
import { __ } from '~/locale';
import './models/label';
import './models/assignee';
+import { BoardType } from './constants';
+import toggleFocusMode from '~/boards/toggle_focus';
import FilteredSearchBoards from '~/boards/filtered_search_boards';
import eventHub from '~/boards/eventhub';
import sidebarEventHub from '~/sidebar/event_hub';
@@ -37,7 +40,16 @@ import {
convertObjectPropsToCamelCase,
parseBoolean,
} from '~/lib/utils/common_utils';
+import { getIdFromGraphQLId } from '~/graphql_shared/utils';
import mountMultipleBoardsSwitcher from './mount_multiple_boards_switcher';
+import projectBoardQuery from './queries/project_board.query.graphql';
+import groupQuery from './queries/group_board.query.graphql';
+
+Vue.use(VueApollo);
+
+const apolloProvider = new VueApollo({
+ defaultClient: createDefaultClient(),
+});
let issueBoardsApp;
@@ -79,18 +91,22 @@ export default () => {
import('ee_component/boards/components/board_settings_sidebar.vue'),
},
store,
- data: {
- state: boardsStore.state,
- loading: true,
- boardsEndpoint: $boardApp.dataset.boardsEndpoint,
- recentBoardsEndpoint: $boardApp.dataset.recentBoardsEndpoint,
- listsEndpoint: $boardApp.dataset.listsEndpoint,
- boardId: $boardApp.dataset.boardId,
- disabled: parseBoolean($boardApp.dataset.disabled),
- issueLinkBase: $boardApp.dataset.issueLinkBase,
- rootPath: $boardApp.dataset.rootPath,
- bulkUpdatePath: $boardApp.dataset.bulkUpdatePath,
- detailIssue: boardsStore.detail,
+ apolloProvider,
+ data() {
+ return {
+ state: boardsStore.state,
+ loading: 0,
+ boardsEndpoint: $boardApp.dataset.boardsEndpoint,
+ recentBoardsEndpoint: $boardApp.dataset.recentBoardsEndpoint,
+ listsEndpoint: $boardApp.dataset.listsEndpoint,
+ boardId: $boardApp.dataset.boardId,
+ disabled: parseBoolean($boardApp.dataset.disabled),
+ issueLinkBase: $boardApp.dataset.issueLinkBase,
+ rootPath: $boardApp.dataset.rootPath,
+ bulkUpdatePath: $boardApp.dataset.bulkUpdatePath,
+ detailIssue: boardsStore.detail,
+ parent: $boardApp.dataset.parent,
+ };
},
computed: {
detailIssueVisible() {
@@ -124,31 +140,56 @@ export default () => {
this.filterManager.setup();
boardsStore.disabled = this.disabled;
- boardsStore
- .all()
- .then(res => res.data)
- .then(lists => {
- lists.forEach(listObj => {
- let { position } = listObj;
- if (listObj.list_type === 'closed') {
- position = Infinity;
- } else if (listObj.list_type === 'backlog') {
- position = -1;
+
+ if (gon.features.graphqlBoardLists) {
+ this.$apollo.addSmartQuery('lists', {
+ query() {
+ return this.parent === BoardType.group ? groupQuery : projectBoardQuery;
+ },
+ variables() {
+ return {
+ fullPath: this.state.endpoints.fullPath,
+ boardId: `gid://gitlab/Board/${this.boardId}`,
+ };
+ },
+ update(data) {
+ return this.getNodes(data);
+ },
+ result({ data, error }) {
+ if (error) {
+ throw error;
}
- boardsStore.addList({
- ...listObj,
- position,
- });
- });
+ const lists = this.getNodes(data);
+
+ lists.forEach(list =>
+ boardsStore.addList({
+ ...list,
+ id: getIdFromGraphQLId(list.id),
+ }),
+ );
- boardsStore.addBlankState();
- setPromotionState(boardsStore);
- this.loading = false;
- })
- .catch(() => {
- Flash(__('An error occurred while fetching the board lists. Please try again.'));
+ boardsStore.addBlankState();
+ setPromotionState(boardsStore);
+ },
+ error() {
+ Flash(__('An error occurred while fetching the board lists. Please try again.'));
+ },
});
+ } else {
+ boardsStore
+ .all()
+ .then(res => res.data)
+ .then(lists => {
+ lists.forEach(list => boardsStore.addList(list));
+ boardsStore.addBlankState();
+ setPromotionState(boardsStore);
+ this.loading = false;
+ })
+ .catch(() => {
+ Flash(__('An error occurred while fetching the board lists. Please try again.'));
+ });
+ }
},
methods: {
updateTokens() {
@@ -233,6 +274,9 @@ export default () => {
});
}
},
+ getNodes(data) {
+ return data[this.parent]?.board?.lists.nodes;
+ },
},
});
@@ -261,7 +305,7 @@ export default () => {
return {
modal: ModalStore.store,
store: boardsStore.state,
- ...getBoardsModalData($boardApp),
+ ...getBoardsModalData(),
canAdminList: this.$options.el.hasAttribute('data-can-admin-list'),
};
},
@@ -325,7 +369,7 @@ export default () => {
});
}
- toggleFocusMode(ModalStore, boardsStore, $boardApp);
+ toggleFocusMode(ModalStore, boardsStore);
toggleLabels();
mountMultipleBoardsSwitcher();
};
diff --git a/app/assets/javascripts/boards/mixins/sortable_default_options.js b/app/assets/javascripts/boards/mixins/sortable_default_options.js
index 68ea28e68d9..fceb8c9d48e 100644
--- a/app/assets/javascripts/boards/mixins/sortable_default_options.js
+++ b/app/assets/javascripts/boards/mixins/sortable_default_options.js
@@ -19,14 +19,15 @@ export function getBoardSortableDefaultOptions(obj) {
const touchEnabled =
'ontouchstart' in window || (window.DocumentTouch && document instanceof DocumentTouch);
- const defaultSortOptions = Object.assign({}, sortableConfig, {
+ const defaultSortOptions = {
+ ...sortableConfig,
filter: '.no-drag',
delay: touchEnabled ? 100 : 0,
scrollSensitivity: touchEnabled ? 60 : 100,
scrollSpeed: 20,
onStart: sortableStart,
onEnd: sortableEnd,
- });
+ };
Object.keys(obj).forEach(key => {
defaultSortOptions[key] = obj[key];
diff --git a/app/assets/javascripts/boards/models/assignee.js b/app/assets/javascripts/boards/models/assignee.js
index 5f5758583bb..1e822d06bfd 100644
--- a/app/assets/javascripts/boards/models/assignee.js
+++ b/app/assets/javascripts/boards/models/assignee.js
@@ -3,7 +3,7 @@ export default class ListAssignee {
this.id = obj.id;
this.name = obj.name;
this.username = obj.username;
- this.avatar = obj.avatar_url || obj.avatar || gon.default_avatar_url;
+ this.avatar = obj.avatarUrl || obj.avatar_url || obj.avatar || gon.default_avatar_url;
this.path = obj.path;
this.state = obj.state;
this.webUrl = obj.web_url || obj.webUrl;
diff --git a/app/assets/javascripts/boards/models/issue.js b/app/assets/javascripts/boards/models/issue.js
index d099c4b930c..878f49cc6be 100644
--- a/app/assets/javascripts/boards/models/issue.js
+++ b/app/assets/javascripts/boards/models/issue.js
@@ -15,7 +15,7 @@ class ListIssue {
this.labels = [];
this.assignees = [];
this.selected = false;
- this.position = obj.relative_position || Infinity;
+ this.position = obj.position || obj.relative_position || Infinity;
this.isFetching = {
subscriptions: true,
};
@@ -99,31 +99,7 @@ class ListIssue {
}
update() {
- const data = {
- issue: {
- milestone_id: this.milestone ? this.milestone.id : null,
- due_date: this.dueDate,
- assignee_ids: this.assignees.length > 0 ? this.assignees.map(u => u.id) : [0],
- label_ids: this.labels.map(label => label.id),
- },
- };
-
- if (!data.issue.label_ids.length) {
- data.issue.label_ids = [''];
- }
-
- const projectPath = this.project ? this.project.path : '';
- return axios.patch(`${this.path}.json`, data).then(({ data: body = {} } = {}) => {
- /**
- * Since post implementation of Scoped labels, server can reject
- * same key-ed labels. To keep the UI and server Model consistent,
- * we're just assigning labels that server echo's back to us when we
- * PATCH the said object.
- */
- if (body) {
- this.labels = convertObjectPropsToCamelCase(body.labels, { deep: true });
- }
- });
+ return boardsStore.updateIssue(this);
}
}
diff --git a/app/assets/javascripts/boards/models/list.js b/app/assets/javascripts/boards/models/list.js
index 990b648190a..31c372b7a75 100644
--- a/app/assets/javascripts/boards/models/list.js
+++ b/app/assets/javascripts/boards/models/list.js
@@ -1,10 +1,9 @@
-/* eslint-disable no-underscore-dangle, class-methods-use-this, consistent-return, no-shadow */
+/* eslint-disable no-underscore-dangle, class-methods-use-this, consistent-return */
import ListIssue from 'ee_else_ce/boards/models/issue';
import { __ } from '~/locale';
import ListLabel from './label';
import ListAssignee from './assignee';
-import { urlParamsToObject } from '~/lib/utils/common_utils';
import flash from '~/flash';
import boardsStore from '../stores/boards_store';
import ListMilestone from './milestone';
@@ -40,8 +39,8 @@ class List {
this.id = obj.id;
this._uid = this.guid();
this.position = obj.position;
- this.title = obj.list_type === 'backlog' ? __('Open') : obj.title;
- this.type = obj.list_type;
+ this.title = (obj.list_type || obj.listType) === 'backlog' ? __('Open') : obj.title;
+ this.type = obj.list_type || obj.listType;
const typeInfo = this.getTypeInfo(this.type);
this.preset = Boolean(typeInfo.isPreset);
@@ -52,14 +51,12 @@ class List {
this.loadingMore = false;
this.issues = obj.issues || [];
this.issuesSize = obj.issuesSize ? obj.issuesSize : 0;
- this.maxIssueCount = Object.hasOwnProperty.call(obj, 'max_issue_count')
- ? obj.max_issue_count
- : 0;
+ this.maxIssueCount = obj.maxIssueCount || obj.max_issue_count || 0;
if (obj.label) {
this.label = new ListLabel(obj.label);
- } else if (obj.user) {
- this.assignee = new ListAssignee(obj.user);
+ } else if (obj.user || obj.assignee) {
+ this.assignee = new ListAssignee(obj.user || obj.assignee);
this.title = this.assignee.name;
} else if (IS_EE && obj.milestone) {
this.milestone = new ListMilestone(obj.milestone);
@@ -113,34 +110,7 @@ class List {
}
getIssues(emptyIssues = true) {
- const data = {
- ...urlParamsToObject(boardsStore.filter.path),
- page: this.page,
- };
-
- if (this.label && data.label_name) {
- data.label_name = data.label_name.filter(label => label !== this.label.title);
- }
-
- if (emptyIssues) {
- this.loading = true;
- }
-
- return boardsStore
- .getIssuesForList(this.id, data)
- .then(res => res.data)
- .then(data => {
- this.loading = false;
- this.issuesSize = data.size;
-
- if (emptyIssues) {
- this.issues = [];
- }
-
- this.createIssues(data.issues);
-
- return data;
- });
+ return boardsStore.getListIssues(this, emptyIssues);
}
newIssue(issue) {
@@ -164,48 +134,7 @@ class List {
}
addIssue(issue, listFrom, newIndex) {
- let moveBeforeId = null;
- let moveAfterId = null;
-
- if (!this.findIssue(issue.id)) {
- if (newIndex !== undefined) {
- this.issues.splice(newIndex, 0, issue);
-
- if (this.issues[newIndex - 1]) {
- moveBeforeId = this.issues[newIndex - 1].id;
- }
-
- if (this.issues[newIndex + 1]) {
- moveAfterId = this.issues[newIndex + 1].id;
- }
- } else {
- this.issues.push(issue);
- }
-
- if (this.label) {
- issue.addLabel(this.label);
- }
-
- if (this.assignee) {
- if (listFrom && listFrom.type === 'assignee') {
- issue.removeAssignee(listFrom.assignee);
- }
- issue.addAssignee(this.assignee);
- }
-
- if (IS_EE && this.milestone) {
- if (listFrom && listFrom.type === 'milestone') {
- issue.removeMilestone(listFrom.milestone);
- }
- issue.addMilestone(this.milestone);
- }
-
- if (listFrom) {
- this.issuesSize += 1;
-
- this.updateIssueLabel(issue, listFrom, moveBeforeId, moveAfterId);
- }
- }
+ boardsStore.addListIssue(this, issue, listFrom, newIndex);
}
moveIssue(issue, oldIndex, newIndex, moveBeforeId, moveAfterId) {
diff --git a/app/assets/javascripts/boards/queries/board_list.fragment.graphql b/app/assets/javascripts/boards/queries/board_list.fragment.graphql
new file mode 100644
index 00000000000..bbf3314377e
--- /dev/null
+++ b/app/assets/javascripts/boards/queries/board_list.fragment.graphql
@@ -0,0 +1,5 @@
+#import "./board_list_shared.fragment.graphql"
+
+fragment BoardListFragment on BoardList {
+ ...BoardListShared
+}
diff --git a/app/assets/javascripts/boards/queries/board_list_shared.fragment.graphql b/app/assets/javascripts/boards/queries/board_list_shared.fragment.graphql
new file mode 100644
index 00000000000..6ba6c05d6d9
--- /dev/null
+++ b/app/assets/javascripts/boards/queries/board_list_shared.fragment.graphql
@@ -0,0 +1,15 @@
+fragment BoardListShared on BoardList {
+ id,
+ title,
+ position,
+ listType,
+ collapsed,
+ label {
+ id,
+ title,
+ color,
+ textColor,
+ description,
+ descriptionHtml
+ }
+}
diff --git a/app/assets/javascripts/boards/queries/group_board.query.graphql b/app/assets/javascripts/boards/queries/group_board.query.graphql
new file mode 100644
index 00000000000..cb42cb3f73d
--- /dev/null
+++ b/app/assets/javascripts/boards/queries/group_board.query.graphql
@@ -0,0 +1,13 @@
+#import "ee_else_ce/boards/queries/board_list.fragment.graphql"
+
+query GroupBoard($fullPath: ID!, $boardId: ID!) {
+ group(fullPath: $fullPath) {
+ board(id: $boardId) {
+ lists {
+ nodes {
+ ...BoardListFragment
+ }
+ }
+ }
+ }
+}
diff --git a/app/assets/javascripts/boards/queries/project_board.query.graphql b/app/assets/javascripts/boards/queries/project_board.query.graphql
new file mode 100644
index 00000000000..4620a7e0fd5
--- /dev/null
+++ b/app/assets/javascripts/boards/queries/project_board.query.graphql
@@ -0,0 +1,13 @@
+#import "ee_else_ce/boards/queries/board_list.fragment.graphql"
+
+query ProjectBoard($fullPath: ID!, $boardId: ID!) {
+ project(fullPath: $fullPath) {
+ board(id: $boardId) {
+ lists {
+ nodes {
+ ...BoardListFragment
+ }
+ }
+ }
+ }
+}
diff --git a/app/assets/javascripts/boards/stores/boards_store.js b/app/assets/javascripts/boards/stores/boards_store.js
index e5447080e37..fdbd7e89bfb 100644
--- a/app/assets/javascripts/boards/stores/boards_store.js
+++ b/app/assets/javascripts/boards/stores/boards_store.js
@@ -6,7 +6,12 @@ import { sortBy } from 'lodash';
import Vue from 'vue';
import Cookies from 'js-cookie';
import BoardsStoreEE from 'ee_else_ce/boards/stores/boards_store_ee';
-import { getUrlParamsArray, parseBoolean } from '~/lib/utils/common_utils';
+import {
+ urlParamsToObject,
+ getUrlParamsArray,
+ parseBoolean,
+ convertObjectPropsToCamelCase,
+} from '~/lib/utils/common_utils';
import { __ } from '~/locale';
import axios from '~/lib/utils/axios_utils';
import { mergeUrlParams } from '~/lib/utils/url_utility';
@@ -23,7 +28,6 @@ const boardsStore = {
limitToHours: false,
},
scopedLabels: {
- helpLink: '',
enabled: false,
},
filter: {
@@ -75,7 +79,15 @@ const boardsStore = {
this.state.currentPage = page;
},
addList(listObj) {
- const list = new List(listObj);
+ const listType = listObj.listType || listObj.list_type;
+ let { position } = listObj;
+ if (listType === ListType.closed) {
+ position = Infinity;
+ } else if (listType === ListType.backlog) {
+ position = -1;
+ }
+
+ const list = new List({ ...listObj, position });
this.state.lists = sortBy([...this.state.lists, list], 'position');
return list;
},
@@ -121,6 +133,50 @@ const boardsStore = {
path: '',
});
},
+ addListIssue(list, issue, listFrom, newIndex) {
+ let moveBeforeId = null;
+ let moveAfterId = null;
+
+ if (!list.findIssue(issue.id)) {
+ if (newIndex !== undefined) {
+ list.issues.splice(newIndex, 0, issue);
+
+ if (list.issues[newIndex - 1]) {
+ moveBeforeId = list.issues[newIndex - 1].id;
+ }
+
+ if (list.issues[newIndex + 1]) {
+ moveAfterId = list.issues[newIndex + 1].id;
+ }
+ } else {
+ list.issues.push(issue);
+ }
+
+ if (list.label) {
+ issue.addLabel(list.label);
+ }
+
+ if (list.assignee) {
+ if (listFrom && listFrom.type === 'assignee') {
+ issue.removeAssignee(listFrom.assignee);
+ }
+ issue.addAssignee(list.assignee);
+ }
+
+ if (IS_EE && list.milestone) {
+ if (listFrom && listFrom.type === 'milestone') {
+ issue.removeMilestone(listFrom.milestone);
+ }
+ issue.addMilestone(list.milestone);
+ }
+
+ if (listFrom) {
+ list.issuesSize += 1;
+
+ list.updateIssueLabel(issue, listFrom, moveBeforeId, moveAfterId);
+ }
+ }
+ },
welcomeIsHidden() {
return parseBoolean(Cookies.get('issue_board_welcome_hidden'));
},
@@ -487,6 +543,36 @@ const boardsStore = {
});
},
+ getListIssues(list, emptyIssues = true) {
+ const data = {
+ ...urlParamsToObject(this.filter.path),
+ page: list.page,
+ };
+
+ if (list.label && data.label_name) {
+ data.label_name = data.label_name.filter(label => label !== list.label.title);
+ }
+
+ if (emptyIssues) {
+ list.loading = true;
+ }
+
+ return this.getIssuesForList(list.id, data)
+ .then(res => res.data)
+ .then(data => {
+ list.loading = false;
+ list.issuesSize = data.size;
+
+ if (emptyIssues) {
+ list.issues = [];
+ }
+
+ list.createIssues(data.issues);
+
+ return data;
+ });
+ },
+
getIssuesForList(id, filter = {}) {
const data = { id };
Object.keys(filter).forEach(key => {
@@ -632,6 +718,28 @@ const boardsStore = {
issue.assignees = obj.assignees.map(a => new ListAssignee(a));
}
},
+ updateIssue(issue) {
+ const data = {
+ issue: {
+ milestone_id: issue.milestone ? issue.milestone.id : null,
+ due_date: issue.dueDate,
+ assignee_ids: issue.assignees.length > 0 ? issue.assignees.map(({ id }) => id) : [0],
+ label_ids: issue.labels.length > 0 ? issue.labels.map(({ id }) => id) : [''],
+ },
+ };
+
+ return axios.patch(`${issue.path}.json`, data).then(({ data: body = {} } = {}) => {
+ /**
+ * Since post implementation of Scoped labels, server can reject
+ * same key-ed labels. To keep the UI and server Model consistent,
+ * we're just assigning labels that server echo's back to us when we
+ * PATCH the said object.
+ */
+ if (body) {
+ issue.labels = convertObjectPropsToCamelCase(body.labels, { deep: true });
+ }
+ });
+ },
};
BoardsStoreEE.initEESpecific(boardsStore);
diff --git a/app/assets/javascripts/boards/stores/state.js b/app/assets/javascripts/boards/stores/state.js
index 731aea996fb..10aac2f649e 100644
--- a/app/assets/javascripts/boards/stores/state.js
+++ b/app/assets/javascripts/boards/stores/state.js
@@ -1,4 +1,6 @@
+import { inactiveListId } from '~/boards/constants';
+
export default () => ({
isShowingLabels: true,
- activeListId: 0,
+ activeListId: inactiveListId,
});
diff --git a/app/assets/javascripts/boards/toggle_focus.js b/app/assets/javascripts/boards/toggle_focus.js
index 2d1ec238274..a437a34c948 100644
--- a/app/assets/javascripts/boards/toggle_focus.js
+++ b/app/assets/javascripts/boards/toggle_focus.js
@@ -1 +1,45 @@
-export default () => {};
+import $ from 'jquery';
+import Vue from 'vue';
+import collapseIcon from './icons/fullscreen_collapse.svg';
+import expandIcon from './icons/fullscreen_expand.svg';
+
+export default (ModalStore, boardsStore) => {
+ const issueBoardsContent = document.querySelector('.content-wrapper > .js-focus-mode-board');
+
+ return new Vue({
+ el: document.getElementById('js-toggle-focus-btn'),
+ data: {
+ modal: ModalStore.store,
+ store: boardsStore.state,
+ isFullscreen: false,
+ },
+ methods: {
+ toggleFocusMode() {
+ $(this.$refs.toggleFocusModeButton).tooltip('hide');
+ issueBoardsContent.classList.toggle('is-focused');
+
+ this.isFullscreen = !this.isFullscreen;
+ },
+ },
+ template: `
+ <div class="board-extra-actions">
+ <a
+ href="#"
+ class="btn btn-default has-tooltip prepend-left-10 js-focus-mode-btn"
+ data-qa-selector="focus_mode_button"
+ role="button"
+ aria-label="Toggle focus mode"
+ title="Toggle focus mode"
+ ref="toggleFocusModeButton"
+ @click="toggleFocusMode">
+ <span v-show="isFullscreen">
+ ${collapseIcon}
+ </span>
+ <span v-show="!isFullscreen">
+ ${expandIcon}
+ </span>
+ </a>
+ </div>
+ `,
+ });
+};
diff --git a/app/assets/javascripts/broadcast_notification.js b/app/assets/javascripts/broadcast_notification.js
index dc5401199dc..97da6fa34da 100644
--- a/app/assets/javascripts/broadcast_notification.js
+++ b/app/assets/javascripts/broadcast_notification.js
@@ -3,10 +3,10 @@ import Cookies from 'js-cookie';
const handleOnDismiss = ({ currentTarget }) => {
currentTarget.removeEventListener('click', handleOnDismiss);
const {
- dataset: { id },
+ dataset: { id, expireDate },
} = currentTarget;
- Cookies.set(`hide_broadcast_message_${id}`, true);
+ Cookies.set(`hide_broadcast_message_${id}`, true, { expires: new Date(expireDate) });
const notification = document.querySelector(`.js-broadcast-notification-${id}`);
notification.parentNode.removeChild(notification);
diff --git a/app/assets/javascripts/ci_variable_list/ajax_variable_list.js b/app/assets/javascripts/ci_variable_list/ajax_variable_list.js
index da33e092086..470649e63fb 100644
--- a/app/assets/javascripts/ci_variable_list/ajax_variable_list.js
+++ b/app/assets/javascripts/ci_variable_list/ajax_variable_list.js
@@ -1,4 +1,4 @@
-import { escape as esc } from 'lodash';
+import { escape } from 'lodash';
import axios from '../lib/utils/axios_utils';
import { s__ } from '../locale';
import Flash from '../flash';
@@ -10,7 +10,7 @@ function generateErrorBoxContent(errors) {
const errorList = [].concat(errors).map(
errorString => `
<li>
- ${esc(errorString)}
+ ${escape(errorString)}
</li>
`,
);
diff --git a/app/assets/javascripts/ci_variable_list/components/ci_variable_modal.vue b/app/assets/javascripts/ci_variable_list/components/ci_variable_modal.vue
index 8f5acd4a0a0..f6ade0867cd 100644
--- a/app/assets/javascripts/ci_variable_list/components/ci_variable_modal.vue
+++ b/app/assets/javascripts/ci_variable_list/components/ci_variable_modal.vue
@@ -46,6 +46,7 @@ export default {
'isGroup',
'maskableRegex',
'selectedEnvironment',
+ 'isProtectedByDefault',
]),
canSubmit() {
return (
@@ -123,6 +124,7 @@ export default {
'addWildCardScope',
'resetSelectedEnvironment',
'setSelectedEnvironment',
+ 'setVariableProtected',
]),
deleteVarAndClose() {
this.deleteVariable(this.variableBeingEdited);
@@ -147,6 +149,11 @@ export default {
}
this.hideModal();
},
+ setVariableProtectedByDefault() {
+ if (this.isProtectedByDefault && !this.variableBeingEdited) {
+ this.setVariableProtected();
+ }
+ },
},
};
</script>
@@ -159,6 +166,7 @@ export default {
static
lazy
@hidden="resetModalHandler"
+ @shown="setVariableProtectedByDefault"
>
<form>
<ci-key-field
diff --git a/app/assets/javascripts/ci_variable_list/constants.js b/app/assets/javascripts/ci_variable_list/constants.js
index 5fe1e32e37e..a4db6481720 100644
--- a/app/assets/javascripts/ci_variable_list/constants.js
+++ b/app/assets/javascripts/ci_variable_list/constants.js
@@ -4,7 +4,7 @@ import { __ } from '~/locale';
export const ADD_CI_VARIABLE_MODAL_ID = 'add-ci-variable';
export const displayText = {
- variableText: __('Var'),
+ variableText: __('Variable'),
fileText: __('File'),
allEnvironmentsText: __('All (default)'),
};
diff --git a/app/assets/javascripts/ci_variable_list/index.js b/app/assets/javascripts/ci_variable_list/index.js
index 58501b216c1..2b4a56a4e6d 100644
--- a/app/assets/javascripts/ci_variable_list/index.js
+++ b/app/assets/javascripts/ci_variable_list/index.js
@@ -5,14 +5,16 @@ import { parseBoolean } from '~/lib/utils/common_utils';
export default () => {
const el = document.getElementById('js-ci-project-variables');
- const { endpoint, projectId, group, maskableRegex } = el.dataset;
+ const { endpoint, projectId, group, maskableRegex, protectedByDefault } = el.dataset;
const isGroup = parseBoolean(group);
+ const isProtectedByDefault = parseBoolean(protectedByDefault);
const store = createStore({
endpoint,
projectId,
isGroup,
maskableRegex,
+ isProtectedByDefault,
});
return new Vue({
diff --git a/app/assets/javascripts/ci_variable_list/store/actions.js b/app/assets/javascripts/ci_variable_list/store/actions.js
index a22fa03e16d..d9129c919f8 100644
--- a/app/assets/javascripts/ci_variable_list/store/actions.js
+++ b/app/assets/javascripts/ci_variable_list/store/actions.js
@@ -20,6 +20,10 @@ export const resetEditing = ({ commit, dispatch }) => {
commit(types.RESET_EDITING);
};
+export const setVariableProtected = ({ commit }) => {
+ commit(types.SET_VARIABLE_PROTECTED);
+};
+
export const requestAddVariable = ({ commit }) => {
commit(types.REQUEST_ADD_VARIABLE);
};
diff --git a/app/assets/javascripts/ci_variable_list/store/mutation_types.js b/app/assets/javascripts/ci_variable_list/store/mutation_types.js
index 0b41c20bce7..ccf8fbd3cb5 100644
--- a/app/assets/javascripts/ci_variable_list/store/mutation_types.js
+++ b/app/assets/javascripts/ci_variable_list/store/mutation_types.js
@@ -2,6 +2,7 @@ export const TOGGLE_VALUES = 'TOGGLE_VALUES';
export const VARIABLE_BEING_EDITED = 'VARIABLE_BEING_EDITED';
export const RESET_EDITING = 'RESET_EDITING';
export const CLEAR_MODAL = 'CLEAR_MODAL';
+export const SET_VARIABLE_PROTECTED = 'SET_VARIABLE_PROTECTED';
export const REQUEST_VARIABLES = 'REQUEST_VARIABLES';
export const RECEIVE_VARIABLES_SUCCESS = 'RECEIVE_VARIABLES_SUCCESS';
diff --git a/app/assets/javascripts/ci_variable_list/store/mutations.js b/app/assets/javascripts/ci_variable_list/store/mutations.js
index 7ee7d7bdc26..7d9cd0dd727 100644
--- a/app/assets/javascripts/ci_variable_list/store/mutations.js
+++ b/app/assets/javascripts/ci_variable_list/store/mutations.js
@@ -104,4 +104,8 @@ export default {
[types.SET_SELECTED_ENVIRONMENT](state, environment) {
state.selectedEnvironment = environment;
},
+
+ [types.SET_VARIABLE_PROTECTED](state) {
+ state.variable.protected = true;
+ },
};
diff --git a/app/assets/javascripts/ci_variable_list/store/state.js b/app/assets/javascripts/ci_variable_list/store/state.js
index 8c0b9c6966f..2fffd115589 100644
--- a/app/assets/javascripts/ci_variable_list/store/state.js
+++ b/app/assets/javascripts/ci_variable_list/store/state.js
@@ -5,6 +5,7 @@ export default () => ({
projectId: null,
isGroup: null,
maskableRegex: null,
+ isProtectedByDefault: null,
isLoading: false,
isDeleting: false,
variable: {
diff --git a/app/assets/javascripts/close_reopen_report_toggle.js b/app/assets/javascripts/close_reopen_report_toggle.js
index 882d20671cc..bcddce6e727 100644
--- a/app/assets/javascripts/close_reopen_report_toggle.js
+++ b/app/assets/javascripts/close_reopen_report_toggle.js
@@ -2,7 +2,7 @@ import DropLab from './droplab/drop_lab';
import ISetter from './droplab/plugins/input_setter';
// Todo: Remove this when fixing issue in input_setter plugin
-const InputSetter = Object.assign({}, ISetter);
+const InputSetter = { ...ISetter };
class CloseReopenReportToggle {
constructor(opts = {}) {
diff --git a/app/assets/javascripts/clusters/clusters_bundle.js b/app/assets/javascripts/clusters/clusters_bundle.js
index 1b11ec355bb..3699a3b8b2b 100644
--- a/app/assets/javascripts/clusters/clusters_bundle.js
+++ b/app/assets/javascripts/clusters/clusters_bundle.js
@@ -14,6 +14,7 @@ import {
INGRESS_DOMAIN_SUFFIX,
CROSSPLANE,
KNATIVE,
+ FLUENTD,
} from './constants';
import ClustersService from './services/clusters_service';
import ClustersStore from './stores/clusters_store';
@@ -49,6 +50,7 @@ export default class Clusters {
installElasticStackPath,
installCrossplanePath,
installPrometheusPath,
+ installFluentdPath,
managePrometheusPath,
clusterEnvironmentsPath,
hasRbac,
@@ -102,6 +104,7 @@ export default class Clusters {
updateKnativeEndpoint: updateKnativePath,
installElasticStackEndpoint: installElasticStackPath,
clusterEnvironmentsEndpoint: clusterEnvironmentsPath,
+ installFluentdEndpoint: installFluentdPath,
});
this.installApplication = this.installApplication.bind(this);
@@ -265,6 +268,7 @@ export default class Clusters {
eventHub.$on('setIngressModSecurityEnabled', data => this.setIngressModSecurityEnabled(data));
eventHub.$on('setIngressModSecurityMode', data => this.setIngressModSecurityMode(data));
eventHub.$on('resetIngressModSecurityChanges', id => this.resetIngressModSecurityChanges(id));
+ eventHub.$on('setFluentdSettings', data => this.setFluentdSettings(data));
// Add event listener to all the banner close buttons
this.addBannerCloseHandler(this.unreachableContainer, 'unreachable');
this.addBannerCloseHandler(this.authenticationFailureContainer, 'authentication_failure');
@@ -281,6 +285,7 @@ export default class Clusters {
eventHub.$off('setIngressModSecurityEnabled');
eventHub.$off('setIngressModSecurityMode');
eventHub.$off('resetIngressModSecurityChanges');
+ eventHub.$off('setFluentdSettings');
}
initPolling(method, successCallback, errorCallback) {
@@ -320,7 +325,7 @@ export default class Clusters {
handleClusterStatusSuccess(data) {
const prevStatus = this.store.state.status;
- const prevApplicationMap = Object.assign({}, this.store.state.applications);
+ const prevApplicationMap = { ...this.store.state.applications };
this.store.updateStateFromServer(data.data);
@@ -506,6 +511,12 @@ export default class Clusters {
});
}
+ setFluentdSettings(settings = {}) {
+ Object.entries(settings).forEach(([key, value]) => {
+ this.store.updateAppProperty(FLUENTD, key, value);
+ });
+ }
+
toggleIngressDomainHelpText({ externalIp }, { externalIp: newExternalIp }) {
if (externalIp !== newExternalIp) {
this.ingressDomainHelpText.classList.toggle('hide', !newExternalIp);
diff --git a/app/assets/javascripts/clusters/components/applications.vue b/app/assets/javascripts/clusters/components/applications.vue
index 723030c5b8b..f11502a7dde 100644
--- a/app/assets/javascripts/clusters/components/applications.vue
+++ b/app/assets/javascripts/clusters/components/applications.vue
@@ -1,5 +1,5 @@
<script>
-import { escape as esc } from 'lodash';
+import { escape } from 'lodash';
import helmInstallIllustration from '@gitlab/svgs/dist/illustrations/kubernetes-installation.svg';
import { GlLoadingIcon } from '@gitlab/ui';
import elasticsearchLogo from 'images/cluster_app_logos/elasticsearch.png';
@@ -14,6 +14,7 @@ import knativeLogo from 'images/cluster_app_logos/knative.png';
import meltanoLogo from 'images/cluster_app_logos/meltano.png';
import prometheusLogo from 'images/cluster_app_logos/prometheus.png';
import elasticStackLogo from 'images/cluster_app_logos/elastic_stack.png';
+import fluentdLogo from 'images/cluster_app_logos/fluentd.png';
import { s__, sprintf } from '../../locale';
import applicationRow from './application_row.vue';
import clipboardButton from '../../vue_shared/components/clipboard_button.vue';
@@ -22,6 +23,7 @@ import { CLUSTER_TYPE, PROVIDER_TYPE, APPLICATION_STATUS, INGRESS } from '../con
import eventHub from '~/clusters/event_hub';
import CrossplaneProviderStack from './crossplane_provider_stack.vue';
import IngressModsecuritySettings from './ingress_modsecurity_settings.vue';
+import FluentdOutputSettings from './fluentd_output_settings.vue';
export default {
components: {
@@ -31,6 +33,7 @@ export default {
KnativeDomainEditor,
CrossplaneProviderStack,
IngressModsecuritySettings,
+ FluentdOutputSettings,
},
props: {
type: {
@@ -102,6 +105,7 @@ export default {
meltanoLogo,
prometheusLogo,
elasticStackLogo,
+ fluentdLogo,
}),
computed: {
isProjectCluster() {
@@ -134,7 +138,7 @@ export default {
},
ingressDescription() {
return sprintf(
- esc(
+ escape(
s__(
`ClusterIntegration|Installing Ingress may incur additional costs. Learn more about %{pricingLink}.`,
),
@@ -142,14 +146,14 @@ export default {
{
pricingLink: `<a href="https://cloud.google.com/compute/pricing#lb"
target="_blank" rel="noopener noreferrer">
- ${esc(s__('ClusterIntegration|pricing'))}</a>`,
+ ${escape(s__('ClusterIntegration|pricing'))}</a>`,
},
false,
);
},
certManagerDescription() {
return sprintf(
- esc(
+ escape(
s__(
`ClusterIntegration|Cert-Manager is a native Kubernetes certificate management controller that helps with issuing certificates.
Installing Cert-Manager on your cluster will issue a certificate by %{letsEncrypt} and ensure that certificates
@@ -159,14 +163,14 @@ export default {
{
letsEncrypt: `<a href="https://letsencrypt.org/"
target="_blank" rel="noopener noreferrer">
- ${esc(s__("ClusterIntegration|Let's Encrypt"))}</a>`,
+ ${escape(s__("ClusterIntegration|Let's Encrypt"))}</a>`,
},
false,
);
},
crossplaneDescription() {
return sprintf(
- esc(
+ escape(
s__(
`ClusterIntegration|Crossplane enables declarative provisioning of managed services from your cloud of choice using %{kubectl} or %{gitlabIntegrationLink}.
Crossplane runs inside your Kubernetes cluster and supports secure connectivity and secrets management between app containers and the cloud services they depend on.`,
@@ -175,7 +179,7 @@ Crossplane runs inside your Kubernetes cluster and supports secure connectivity
{
gitlabIntegrationLink: `<a href="https://docs.gitlab.com/ee/user/clusters/applications.html#crossplane"
target="_blank" rel="noopener noreferrer">
- ${esc(s__('ClusterIntegration|Gitlab Integration'))}</a>`,
+ ${escape(s__('ClusterIntegration|Gitlab Integration'))}</a>`,
kubectl: `<code>kubectl</code>`,
},
false,
@@ -184,7 +188,7 @@ Crossplane runs inside your Kubernetes cluster and supports secure connectivity
prometheusDescription() {
return sprintf(
- esc(
+ escape(
s__(
`ClusterIntegration|Prometheus is an open-source monitoring system
with %{gitlabIntegrationLink} to monitor deployed applications.`,
@@ -193,7 +197,7 @@ Crossplane runs inside your Kubernetes cluster and supports secure connectivity
{
gitlabIntegrationLink: `<a href="https://docs.gitlab.com/ce/user/project/integrations/prometheus.html"
target="_blank" rel="noopener noreferrer">
- ${esc(s__('ClusterIntegration|GitLab Integration'))}</a>`,
+ ${escape(s__('ClusterIntegration|GitLab Integration'))}</a>`,
},
false,
);
@@ -219,11 +223,11 @@ Crossplane runs inside your Kubernetes cluster and supports secure connectivity
installedVia() {
if (this.cloudRun) {
return sprintf(
- esc(s__(`ClusterIntegration|installed via %{installed_via}`)),
+ escape(s__(`ClusterIntegration|installed via %{installed_via}`)),
{
installed_via: `<a href="${
this.cloudRunHelpPath
- }" target="_blank" rel="noopener noreferrer">${esc(
+ }" target="_blank" rel="noopener noreferrer">${escape(
s__('ClusterIntegration|Cloud Run'),
)}</a>`,
},
@@ -658,7 +662,7 @@ Crossplane runs inside your Kubernetes cluster and supports secure connectivity
:uninstall-successful="applications.elastic_stack.uninstallSuccessful"
:uninstall-failed="applications.elastic_stack.uninstallFailed"
:disabled="!helmInstalled"
- title-link="https://github.com/helm/charts/tree/master/stable/elastic-stack"
+ title-link="https://gitlab.com/gitlab-org/charts/elastic-stack"
>
<div slot="description">
<p>
@@ -670,6 +674,51 @@ Crossplane runs inside your Kubernetes cluster and supports secure connectivity
</p>
</div>
</application-row>
+
+ <application-row
+ id="fluentd"
+ :logo-url="fluentdLogo"
+ :title="applications.fluentd.title"
+ :status="applications.fluentd.status"
+ :status-reason="applications.fluentd.statusReason"
+ :request-status="applications.fluentd.requestStatus"
+ :request-reason="applications.fluentd.requestReason"
+ :installed="applications.fluentd.installed"
+ :install-failed="applications.fluentd.installFailed"
+ :install-application-request-params="{
+ host: applications.fluentd.host,
+ port: applications.fluentd.port,
+ protocol: applications.fluentd.protocol,
+ waf_log_enabled: applications.fluentd.wafLogEnabled,
+ cilium_log_enabled: applications.fluentd.ciliumLogEnabled,
+ }"
+ :uninstallable="applications.fluentd.uninstallable"
+ :uninstall-successful="applications.fluentd.uninstallSuccessful"
+ :uninstall-failed="applications.fluentd.uninstallFailed"
+ :disabled="!helmInstalled"
+ :updateable="false"
+ title-link="https://github.com/helm/charts/tree/master/stable/fluentd"
+ >
+ <div slot="description">
+ <p>
+ {{
+ s__(
+ `ClusterIntegration|Fluentd is an open source data collector, which lets you unify the data collection and consumption for a better use and understanding of data. It requires at least one of the following logs to be successfully installed.`,
+ )
+ }}
+ </p>
+
+ <fluentd-output-settings
+ :port="applications.fluentd.port"
+ :protocol="applications.fluentd.protocol"
+ :host="applications.fluentd.host"
+ :waf-log-enabled="applications.fluentd.wafLogEnabled"
+ :cilium-log-enabled="applications.fluentd.ciliumLogEnabled"
+ :status="applications.fluentd.status"
+ :update-failed="applications.fluentd.updateFailed"
+ />
+ </div>
+ </application-row>
</div>
</section>
</template>
diff --git a/app/assets/javascripts/clusters/components/fluentd_output_settings.vue b/app/assets/javascripts/clusters/components/fluentd_output_settings.vue
new file mode 100644
index 00000000000..1884b501a20
--- /dev/null
+++ b/app/assets/javascripts/clusters/components/fluentd_output_settings.vue
@@ -0,0 +1,241 @@
+<script>
+import { __ } from '~/locale';
+import { APPLICATION_STATUS, FLUENTD } from '~/clusters/constants';
+import {
+ GlAlert,
+ GlDeprecatedButton,
+ GlDropdown,
+ GlDropdownItem,
+ GlFormCheckbox,
+} from '@gitlab/ui';
+import eventHub from '~/clusters/event_hub';
+import { mapValues } from 'lodash';
+
+const { UPDATING, UNINSTALLING, INSTALLING, INSTALLED, UPDATED } = APPLICATION_STATUS;
+
+export default {
+ components: {
+ GlAlert,
+ GlDeprecatedButton,
+ GlDropdown,
+ GlDropdownItem,
+ GlFormCheckbox,
+ },
+ props: {
+ protocols: {
+ type: Array,
+ required: false,
+ default: () => ['TCP', 'UDP'],
+ },
+ status: {
+ type: String,
+ required: false,
+ default: '',
+ },
+ updateFailed: {
+ type: Boolean,
+ required: false,
+ },
+ protocol: {
+ type: String,
+ required: false,
+ default: () => __('Protocol'),
+ },
+ port: {
+ type: Number,
+ required: false,
+ default: 514,
+ },
+ host: {
+ type: String,
+ required: false,
+ default: '',
+ },
+ wafLogEnabled: {
+ type: Boolean,
+ required: false,
+ },
+ ciliumLogEnabled: {
+ type: Boolean,
+ required: false,
+ },
+ },
+ data: () => ({
+ currentServerSideSettings: {
+ host: null,
+ port: null,
+ protocol: null,
+ wafLogEnabled: null,
+ ciliumLogEnabled: null,
+ },
+ }),
+ computed: {
+ isSaving() {
+ return [UPDATING].includes(this.status);
+ },
+ saveButtonDisabled() {
+ return [UNINSTALLING, UPDATING, INSTALLING].includes(this.status);
+ },
+ saveButtonLabel() {
+ return this.isSaving ? __('Saving') : __('Save changes');
+ },
+ /**
+ * Returns true either when:
+ * - The application is getting updated.
+ * - The user has changed some of the settings for an application which is
+ * neither getting installed nor updated.
+ */
+ showButtons() {
+ return this.isSaving || (this.changedByUser && [INSTALLED, UPDATED].includes(this.status));
+ },
+ protocolName() {
+ if (this.protocol) {
+ return this.protocol.toUpperCase();
+ }
+ return __('Protocol');
+ },
+ changedByUser() {
+ return Object.entries(this.currentServerSideSettings).some(([key, value]) => {
+ return value !== null && value !== this[key];
+ });
+ },
+ },
+ watch: {
+ status() {
+ this.resetCurrentServerSideSettings();
+ },
+ },
+ methods: {
+ updateApplication() {
+ eventHub.$emit('updateApplication', {
+ id: FLUENTD,
+ params: {
+ port: this.port,
+ protocol: this.protocol,
+ host: this.host,
+ waf_log_enabled: this.wafLogEnabled,
+ cilium_log_enabled: this.ciliumLogEnabled,
+ },
+ });
+ },
+ resetCurrentServerSideSettings() {
+ this.currentServerSideSettings = mapValues(this.currentServerSideSettings, () => {
+ return null;
+ });
+ },
+ resetStatus() {
+ const newSettings = mapValues(this.currentServerSideSettings, (value, key) => {
+ return value === null ? this[key] : value;
+ });
+ eventHub.$emit('setFluentdSettings', {
+ ...newSettings,
+ isEditingSettings: false,
+ });
+ },
+ updateCurrentServerSideSettings(settings) {
+ Object.keys(settings).forEach(key => {
+ if (this.currentServerSideSettings[key] === null) {
+ this.currentServerSideSettings[key] = this[key];
+ }
+ });
+ },
+ setFluentdSettings(settings) {
+ this.updateCurrentServerSideSettings(settings);
+ eventHub.$emit('setFluentdSettings', {
+ ...settings,
+ isEditingSettings: true,
+ });
+ },
+ selectProtocol(protocol) {
+ this.setFluentdSettings({ protocol });
+ },
+ hostChanged(host) {
+ this.setFluentdSettings({ host });
+ },
+ portChanged(port) {
+ this.setFluentdSettings({ port: Number(port) });
+ },
+ wafLogChanged(wafLogEnabled) {
+ this.setFluentdSettings({ wafLogEnabled });
+ },
+ ciliumLogChanged(ciliumLogEnabled) {
+ this.setFluentdSettings({ ciliumLogEnabled });
+ },
+ },
+};
+</script>
+
+<template>
+ <div>
+ <gl-alert v-if="updateFailed" class="mb-3" variant="danger" :dismissible="false">
+ {{
+ s__(
+ 'ClusterIntegration|Something went wrong while trying to save your settings. Please try again.',
+ )
+ }}
+ </gl-alert>
+ <div class="form-horizontal">
+ <div class="form-group">
+ <label for="fluentd-host">
+ <strong>{{ s__('ClusterIntegration|SIEM Hostname') }}</strong>
+ </label>
+ <input
+ id="fluentd-host"
+ :value="host"
+ type="text"
+ class="form-control"
+ @input="hostChanged($event.target.value)"
+ />
+ </div>
+ <div class="form-group">
+ <label for="fluentd-port">
+ <strong>{{ s__('ClusterIntegration|SIEM Port') }}</strong>
+ </label>
+ <input
+ id="fluentd-port"
+ :value="port"
+ type="number"
+ class="form-control"
+ @input="portChanged($event.target.value)"
+ />
+ </div>
+ <div class="form-group">
+ <label for="fluentd-protocol">
+ <strong>{{ s__('ClusterIntegration|SIEM Protocol') }}</strong>
+ </label>
+ <gl-dropdown :text="protocolName" class="w-100">
+ <gl-dropdown-item
+ v-for="(value, index) in protocols"
+ :key="index"
+ @click="selectProtocol(value.toLowerCase())"
+ >
+ {{ value }}
+ </gl-dropdown-item>
+ </gl-dropdown>
+ </div>
+ <div class="form-group flex flex-wrap">
+ <gl-form-checkbox :checked="wafLogEnabled" @input="wafLogChanged">
+ <strong>{{ s__('ClusterIntegration|Send ModSecurity Logs') }}</strong>
+ </gl-form-checkbox>
+ <gl-form-checkbox :checked="ciliumLogEnabled" @input="ciliumLogChanged">
+ <strong>{{ s__('ClusterIntegration|Send Cilium Logs') }}</strong>
+ </gl-form-checkbox>
+ </div>
+ <div v-if="showButtons" class="mt-3">
+ <gl-deprecated-button
+ ref="saveBtn"
+ class="mr-1"
+ variant="success"
+ :loading="isSaving"
+ :disabled="saveButtonDisabled"
+ @click="updateApplication"
+ >
+ {{ saveButtonLabel }}
+ </gl-deprecated-button>
+ <gl-deprecated-button ref="cancelBtn" :disabled="saveButtonDisabled" @click="resetStatus">
+ {{ __('Cancel') }}
+ </gl-deprecated-button>
+ </div>
+ </div>
+ </div>
+</template>
diff --git a/app/assets/javascripts/clusters/components/ingress_modsecurity_settings.vue b/app/assets/javascripts/clusters/components/ingress_modsecurity_settings.vue
index 95eb427a49c..c2f963f0b34 100644
--- a/app/assets/javascripts/clusters/components/ingress_modsecurity_settings.vue
+++ b/app/assets/javascripts/clusters/components/ingress_modsecurity_settings.vue
@@ -1,5 +1,5 @@
<script>
-import { escape as esc } from 'lodash';
+import { escape } from 'lodash';
import { s__, __ } from '../../locale';
import { APPLICATION_STATUS, INGRESS, LOGGING_MODE, BLOCKING_MODE } from '~/clusters/constants';
import {
@@ -87,7 +87,7 @@ export default {
);
},
ingressModSecurityDescription() {
- return esc(this.ingressModSecurityHelpPath);
+ return escape(this.ingressModSecurityHelpPath);
},
saving() {
return [UPDATING].includes(this.ingress.status);
diff --git a/app/assets/javascripts/clusters/components/remove_cluster_confirmation.vue b/app/assets/javascripts/clusters/components/remove_cluster_confirmation.vue
index b35adae3352..271f9f74838 100644
--- a/app/assets/javascripts/clusters/components/remove_cluster_confirmation.vue
+++ b/app/assets/javascripts/clusters/components/remove_cluster_confirmation.vue
@@ -1,5 +1,5 @@
<script>
-import { escape as esc } from 'lodash';
+import { escape } from 'lodash';
import SplitButton from '~/vue_shared/components/split_button.vue';
import { GlModal, GlDeprecatedButton, GlFormInput } from '@gitlab/ui';
import { s__, sprintf } from '~/locale';
@@ -82,7 +82,7 @@ export default {
)
: s__('ClusterIntegration|To remove your integration, type %{clusterName} to confirm:'),
{
- clusterName: `<code>${esc(this.clusterName)}</code>`,
+ clusterName: `<code>${escape(this.clusterName)}</code>`,
},
false,
);
diff --git a/app/assets/javascripts/clusters/constants.js b/app/assets/javascripts/clusters/constants.js
index 6c3046fc56b..60e179c54eb 100644
--- a/app/assets/javascripts/clusters/constants.js
+++ b/app/assets/javascripts/clusters/constants.js
@@ -53,6 +53,7 @@ export const CERT_MANAGER = 'cert_manager';
export const CROSSPLANE = 'crossplane';
export const PROMETHEUS = 'prometheus';
export const ELASTIC_STACK = 'elastic_stack';
+export const FLUENTD = 'fluentd';
export const APPLICATIONS = [
HELM,
@@ -63,6 +64,7 @@ export const APPLICATIONS = [
CERT_MANAGER,
PROMETHEUS,
ELASTIC_STACK,
+ FLUENTD,
];
export const INGRESS_DOMAIN_SUFFIX = '.nip.io';
diff --git a/app/assets/javascripts/clusters/event_hub.js b/app/assets/javascripts/clusters/event_hub.js
index 0948c2e5352..e31806ad199 100644
--- a/app/assets/javascripts/clusters/event_hub.js
+++ b/app/assets/javascripts/clusters/event_hub.js
@@ -1,3 +1,3 @@
-import Vue from 'vue';
+import createEventHub from '~/helpers/event_hub_factory';
-export default new Vue();
+export default createEventHub();
diff --git a/app/assets/javascripts/clusters/services/clusters_service.js b/app/assets/javascripts/clusters/services/clusters_service.js
index 333fb293a15..2a6c6965dab 100644
--- a/app/assets/javascripts/clusters/services/clusters_service.js
+++ b/app/assets/javascripts/clusters/services/clusters_service.js
@@ -13,6 +13,7 @@ export default class ClusterService {
jupyter: this.options.installJupyterEndpoint,
knative: this.options.installKnativeEndpoint,
elastic_stack: this.options.installElasticStackEndpoint,
+ fluentd: this.options.installFluentdEndpoint,
};
this.appUpdateEndpointMap = {
knative: this.options.updateKnativeEndpoint,
diff --git a/app/assets/javascripts/clusters/stores/clusters_store.js b/app/assets/javascripts/clusters/stores/clusters_store.js
index b09fd6800b6..9d354e66661 100644
--- a/app/assets/javascripts/clusters/stores/clusters_store.js
+++ b/app/assets/javascripts/clusters/stores/clusters_store.js
@@ -13,6 +13,7 @@ import {
UPDATE_EVENT,
UNINSTALL_EVENT,
ELASTIC_STACK,
+ FLUENTD,
} from '../constants';
import transitionApplicationState from '../services/application_state_machine';
@@ -103,6 +104,16 @@ export default class ClusterStore {
...applicationInitialState,
title: s__('ClusterIntegration|Elastic Stack'),
},
+ fluentd: {
+ ...applicationInitialState,
+ title: s__('ClusterIntegration|Fluentd'),
+ host: null,
+ port: null,
+ protocol: null,
+ wafLogEnabled: null,
+ ciliumLogEnabled: null,
+ isEditingSettings: false,
+ },
},
environments: [],
fetchingEnvironments: false,
@@ -253,6 +264,14 @@ export default class ClusterStore {
} else if (appId === ELASTIC_STACK) {
this.state.applications.elastic_stack.version = version;
this.state.applications.elastic_stack.updateAvailable = updateAvailable;
+ } else if (appId === FLUENTD) {
+ if (!this.state.applications.fluentd.isEditingSettings) {
+ this.state.applications.fluentd.port = serverAppEntry.port;
+ this.state.applications.fluentd.host = serverAppEntry.host;
+ this.state.applications.fluentd.protocol = serverAppEntry.protocol;
+ this.state.applications.fluentd.wafLogEnabled = serverAppEntry.waf_log_enabled;
+ this.state.applications.fluentd.ciliumLogEnabled = serverAppEntry.cilium_log_enabled;
+ }
}
});
}
diff --git a/app/assets/javascripts/clusters_list/components/clusters.vue b/app/assets/javascripts/clusters_list/components/clusters.vue
index 46dacf30f39..af3f1437c64 100644
--- a/app/assets/javascripts/clusters_list/components/clusters.vue
+++ b/app/assets/javascripts/clusters_list/components/clusters.vue
@@ -1,61 +1,78 @@
<script>
import { mapState, mapActions } from 'vuex';
-import { GlTable, GlLoadingIcon, GlBadge } from '@gitlab/ui';
+import { GlBadge, GlLink, GlLoadingIcon, GlPagination, GlTable } from '@gitlab/ui';
import tooltip from '~/vue_shared/directives/tooltip';
import { CLUSTER_TYPES, STATUSES } from '../constants';
import { __, sprintf } from '~/locale';
export default {
components: {
- GlTable,
- GlLoadingIcon,
GlBadge,
+ GlLink,
+ GlLoadingIcon,
+ GlPagination,
+ GlTable,
},
directives: {
tooltip,
},
- fields: [
- {
- key: 'name',
- label: __('Kubernetes cluster'),
- },
- {
- key: 'environmentScope',
- label: __('Environment scope'),
- },
- {
- key: 'size',
- label: __('Size'),
- },
- {
- key: 'cpu',
- label: __('Total cores (vCPUs)'),
+ computed: {
+ ...mapState(['clusters', 'clustersPerPage', 'loading', 'page', 'totalCulsters']),
+ currentPage: {
+ get() {
+ return this.page;
+ },
+ set(newVal) {
+ this.setPage(newVal);
+ this.fetchClusters();
+ },
},
- {
- key: 'memory',
- label: __('Total memory (GB)'),
+ fields() {
+ return [
+ {
+ key: 'name',
+ label: __('Kubernetes cluster'),
+ },
+ {
+ key: 'environment_scope',
+ label: __('Environment scope'),
+ },
+ // Wait for backend to send these fields
+ // {
+ // key: 'size',
+ // label: __('Size'),
+ // },
+ // {
+ // key: 'cpu',
+ // label: __('Total cores (vCPUs)'),
+ // },
+ // {
+ // key: 'memory',
+ // label: __('Total memory (GB)'),
+ // },
+ {
+ key: 'cluster_type',
+ label: __('Cluster level'),
+ formatter: value => CLUSTER_TYPES[value],
+ },
+ ];
},
- {
- key: 'clusterType',
- label: __('Cluster level'),
- formatter: value => CLUSTER_TYPES[value],
+ hasClusters() {
+ return this.clustersPerPage > 0;
},
- ],
- computed: {
- ...mapState(['clusters', 'loading']),
},
mounted() {
- // TODO - uncomment this once integrated with BE
- // this.fetchClusters();
+ this.fetchClusters();
},
methods: {
- ...mapActions(['fetchClusters']),
+ ...mapActions(['fetchClusters', 'setPage']),
statusClass(status) {
- return STATUSES[status].className;
+ const iconClass = STATUSES[status] || STATUSES.default;
+ return iconClass.className;
},
statusTitle(status) {
- const { title } = STATUSES[status];
- return sprintf(__('Status: %{title}'), { title }, false);
+ const iconTitle = STATUSES[status] || STATUSES.default;
+ return sprintf(__('Status: %{title}'), { title: iconTitle.title }, false);
},
},
};
@@ -63,37 +80,46 @@ export default {
<template>
<gl-loading-icon v-if="loading" size="md" class="mt-3" />
- <gl-table
- v-else
- :items="clusters"
- :fields="$options.fields"
- stacked="md"
- variant="light"
- class="qa-clusters-table"
- >
- <template #cell(name)="{ item }">
- <div class="d-flex flex-row-reverse flex-md-row js-status">
- {{ item.name }}
- <gl-loading-icon
- v-if="item.status === 'deleting'"
- v-tooltip
- :title="statusTitle(item.status)"
- size="sm"
- class="mr-2 ml-md-2"
- />
- <div
- v-else
- v-tooltip
- class="cluster-status-indicator rounded-circle align-self-center gl-w-8 gl-h-8 mr-2 ml-md-2"
- :class="statusClass(item.status)"
- :title="statusTitle(item.status)"
- ></div>
- </div>
- </template>
- <template #cell(clusterType)="{value}">
- <gl-badge variant="light">
- {{ value }}
- </gl-badge>
- </template>
- </gl-table>
+
+ <section v-else>
+ <gl-table :items="clusters" :fields="fields" stacked="md" class="qa-clusters-table">
+ <template #cell(name)="{ item }">
+ <div class="d-flex flex-row-reverse flex-md-row js-status">
+ <gl-link data-qa-selector="cluster" :data-qa-cluster-name="item.name" :href="item.path">
+ {{ item.name }}
+ </gl-link>
+
+ <gl-loading-icon
+ v-if="item.status === 'deleting'"
+ v-tooltip
+ :title="statusTitle(item.status)"
+ size="sm"
+ class="mr-2 ml-md-2"
+ />
+ <div
+ v-else
+ v-tooltip
+ class="cluster-status-indicator rounded-circle align-self-center gl-w-4 gl-h-4 mr-2 ml-md-2"
+ :class="statusClass(item.status)"
+ :title="statusTitle(item.status)"
+ ></div>
+ </div>
+ </template>
+ <template #cell(cluster_type)="{value}">
+ <gl-badge variant="light">
+ {{ value }}
+ </gl-badge>
+ </template>
+ </gl-table>
+
+ <gl-pagination
+ v-if="hasClusters"
+ v-model="currentPage"
+ :per-page="clustersPerPage"
+ :total-items="totalCulsters"
+ :prev-text="__('Prev')"
+ :next-text="__('Next')"
+ align="center"
+ />
+ </section>
</template>
diff --git a/app/assets/javascripts/clusters_list/constants.js b/app/assets/javascripts/clusters_list/constants.js
index 9428f08176c..eebcaa086f9 100644
--- a/app/assets/javascripts/clusters_list/constants.js
+++ b/app/assets/javascripts/clusters_list/constants.js
@@ -7,8 +7,9 @@ export const CLUSTER_TYPES = {
};
export const STATUSES = {
+ default: { className: 'bg-white', title: __('Unknown') },
disabled: { className: 'disabled', title: __('Disabled') },
- connected: { className: 'bg-success', title: __('Connected') },
+ created: { className: 'bg-success', title: __('Connected') },
unreachable: { className: 'bg-danger', title: __('Unreachable') },
authentication_failure: { className: 'bg-warning', title: __('Authentication Failure') },
deleting: { title: __('Deleting') },
diff --git a/app/assets/javascripts/clusters_list/store/actions.js b/app/assets/javascripts/clusters_list/store/actions.js
index 79bc9932438..919625f69b4 100644
--- a/app/assets/javascripts/clusters_list/store/actions.js
+++ b/app/assets/javascripts/clusters_list/store/actions.js
@@ -1,36 +1,35 @@
-import { convertObjectPropsToCamelCase } from '~/lib/utils/common_utils';
import Poll from '~/lib/utils/poll';
import axios from '~/lib/utils/axios_utils';
-import Visibility from 'visibilityjs';
import flash from '~/flash';
import { __ } from '~/locale';
+import { parseIntPagination, normalizeHeaders } from '~/lib/utils/common_utils';
import * as types from './mutation_types';
export const fetchClusters = ({ state, commit }) => {
const poll = new Poll({
resource: {
- fetchClusters: endpoint => axios.get(endpoint),
+ fetchClusters: paginatedEndPoint => axios.get(paginatedEndPoint),
},
- data: state.endpoint,
+ data: `${state.endpoint}?page=${state.page}`,
method: 'fetchClusters',
- successCallback: ({ data }) => {
- commit(types.SET_CLUSTERS_DATA, convertObjectPropsToCamelCase(data, { deep: true }));
- commit(types.SET_LOADING_STATE, false);
+ successCallback: ({ data, headers }) => {
+ if (data.clusters) {
+ const normalizedHeaders = normalizeHeaders(headers);
+ const paginationInformation = parseIntPagination(normalizedHeaders);
+
+ commit(types.SET_CLUSTERS_DATA, { data, paginationInformation });
+ commit(types.SET_LOADING_STATE, false);
+ poll.stop();
+ }
},
errorCallback: () => flash(__('An error occurred while loading clusters')),
});
- if (!Visibility.hidden()) {
- poll.makeRequest();
- }
+ poll.makeRequest();
+};
- Visibility.change(() => {
- if (!Visibility.hidden()) {
- poll.restart();
- } else {
- poll.stop();
- }
- });
+export const setPage = ({ commit }, page) => {
+ commit(types.SET_PAGE, page);
};
// prevent babel-plugin-rewire from generating an invalid default during karma tests
diff --git a/app/assets/javascripts/clusters_list/store/mutation_types.js b/app/assets/javascripts/clusters_list/store/mutation_types.js
index f056f3ab7d9..a5275f28c13 100644
--- a/app/assets/javascripts/clusters_list/store/mutation_types.js
+++ b/app/assets/javascripts/clusters_list/store/mutation_types.js
@@ -1,2 +1,3 @@
export const SET_CLUSTERS_DATA = 'SET_CLUSTERS_DATA';
export const SET_LOADING_STATE = 'SET_LOADING_STATE';
+export const SET_PAGE = 'SET_PAGE';
diff --git a/app/assets/javascripts/clusters_list/store/mutations.js b/app/assets/javascripts/clusters_list/store/mutations.js
index ffd3c4601bf..2a9df9f38f0 100644
--- a/app/assets/javascripts/clusters_list/store/mutations.js
+++ b/app/assets/javascripts/clusters_list/store/mutations.js
@@ -4,9 +4,15 @@ export default {
[types.SET_LOADING_STATE](state, value) {
state.loading = value;
},
- [types.SET_CLUSTERS_DATA](state, clusters) {
+ [types.SET_CLUSTERS_DATA](state, { data, paginationInformation }) {
Object.assign(state, {
- clusters,
+ clusters: data.clusters,
+ clustersPerPage: paginationInformation.perPage,
+ hasAncestorClusters: data.has_ancestor_clusters,
+ totalCulsters: paginationInformation.total,
});
},
+ [types.SET_PAGE](state, value) {
+ state.page = Number(value) || 1;
+ },
};
diff --git a/app/assets/javascripts/clusters_list/store/state.js b/app/assets/javascripts/clusters_list/store/state.js
index ed032ed8435..d590ea09e66 100644
--- a/app/assets/javascripts/clusters_list/store/state.js
+++ b/app/assets/javascripts/clusters_list/store/state.js
@@ -1,5 +1,9 @@
export default (initialState = {}) => ({
endpoint: initialState.endpoint,
- loading: false, // TODO - set this to true once integrated with BE
+ hasAncestorClusters: false,
+ loading: true,
clusters: [],
+ clustersPerPage: 0,
+ page: 1,
+ totalCulsters: 0,
});
diff --git a/app/assets/javascripts/code_navigation/components/app.vue b/app/assets/javascripts/code_navigation/components/app.vue
index d738c914125..85ec0a60ec5 100644
--- a/app/assets/javascripts/code_navigation/components/app.vue
+++ b/app/assets/javascripts/code_navigation/components/app.vue
@@ -8,7 +8,12 @@ export default {
Popover,
},
computed: {
- ...mapState(['currentDefinition', 'currentDefinitionPosition', 'definitionPathPrefix']),
+ ...mapState([
+ 'currentDefinition',
+ 'currentDefinitionPosition',
+ 'currentBlobPath',
+ 'definitionPathPrefix',
+ ]),
},
mounted() {
this.body = document.body;
@@ -44,5 +49,6 @@ export default {
:position="currentDefinitionPosition"
:data="currentDefinition"
:definition-path-prefix="definitionPathPrefix"
+ :blob-path="currentBlobPath"
/>
</template>
diff --git a/app/assets/javascripts/code_navigation/components/popover.vue b/app/assets/javascripts/code_navigation/components/popover.vue
index b4d9bc7b181..7147ce227e8 100644
--- a/app/assets/javascripts/code_navigation/components/popover.vue
+++ b/app/assets/javascripts/code_navigation/components/popover.vue
@@ -1,9 +1,9 @@
<script>
-import { GlDeprecatedButton } from '@gitlab/ui';
+import { GlButton } from '@gitlab/ui';
export default {
components: {
- GlDeprecatedButton,
+ GlButton,
},
props: {
position: {
@@ -18,6 +18,10 @@ export default {
type: String,
required: true,
},
+ blobPath: {
+ type: String,
+ required: true,
+ },
},
data() {
return {
@@ -32,9 +36,18 @@ export default {
};
},
definitionPath() {
- return (
- this.data.definition_path && `${this.definitionPathPrefix}/${this.data.definition_path}`
- );
+ if (!this.data.definition_path) {
+ return null;
+ }
+
+ if (this.isDefinitionCurrentBlob) {
+ return `#${this.data.definition_path.split('#').pop()}`;
+ }
+
+ return `${this.definitionPathPrefix}/${this.data.definition_path}`;
+ },
+ isDefinitionCurrentBlob() {
+ return this.data.definition_path.indexOf(this.blobPath) === 0;
},
},
watch: {
@@ -77,9 +90,15 @@ export default {
</p>
</div>
<div v-if="definitionPath" class="popover-body">
- <gl-deprecated-button :href="definitionPath" target="_blank" class="w-100" variant="default">
+ <gl-button
+ :href="definitionPath"
+ :target="isDefinitionCurrentBlob ? null : '_blank'"
+ class="w-100"
+ variant="default"
+ data-testid="go-to-definition-btn"
+ >
{{ __('Go to definition') }}
- </gl-deprecated-button>
+ </gl-button>
</div>
</div>
</template>
diff --git a/app/assets/javascripts/code_navigation/store/actions.js b/app/assets/javascripts/code_navigation/store/actions.js
index 6ecede32944..7b2669691bd 100644
--- a/app/assets/javascripts/code_navigation/store/actions.js
+++ b/app/assets/javascripts/code_navigation/store/actions.js
@@ -30,7 +30,9 @@ export default {
});
},
showBlobInteractionZones({ state }, path) {
- Object.values(state.data[path]).forEach(d => addInteractionClass(path, d));
+ if (state.data && state.data[path]) {
+ Object.values(state.data[path]).forEach(d => addInteractionClass(path, d));
+ }
},
showDefinition({ commit, state }, { target: el }) {
let definition;
@@ -52,7 +54,8 @@ export default {
return;
}
- const data = state.data[blobEl.dataset.path];
+ const blobPath = blobEl.dataset.path;
+ const data = state.data[blobPath];
if (!data) return;
@@ -72,6 +75,6 @@ export default {
setCurrentHoverElement(el);
}
- commit(types.SET_CURRENT_DEFINITION, { definition, position });
+ commit(types.SET_CURRENT_DEFINITION, { definition, position, blobPath });
},
};
diff --git a/app/assets/javascripts/code_navigation/store/mutations.js b/app/assets/javascripts/code_navigation/store/mutations.js
index 84b1c264418..07b190c7476 100644
--- a/app/assets/javascripts/code_navigation/store/mutations.js
+++ b/app/assets/javascripts/code_navigation/store/mutations.js
@@ -15,8 +15,9 @@ export default {
[types.REQUEST_DATA_ERROR](state) {
state.loading = false;
},
- [types.SET_CURRENT_DEFINITION](state, { definition, position }) {
+ [types.SET_CURRENT_DEFINITION](state, { definition, position, blobPath }) {
state.currentDefinition = definition;
state.currentDefinitionPosition = position;
+ state.currentBlobPath = blobPath;
},
};
diff --git a/app/assets/javascripts/code_navigation/store/state.js b/app/assets/javascripts/code_navigation/store/state.js
index ffe44ec5381..569d2f7b319 100644
--- a/app/assets/javascripts/code_navigation/store/state.js
+++ b/app/assets/javascripts/code_navigation/store/state.js
@@ -4,4 +4,5 @@ export default () => ({
data: null,
currentDefinition: null,
currentDefinitionPosition: null,
+ currentBlobPath: null,
});
diff --git a/app/assets/javascripts/comment_type_toggle.js b/app/assets/javascripts/comment_type_toggle.js
index a259667bb75..2fcd40a901d 100644
--- a/app/assets/javascripts/comment_type_toggle.js
+++ b/app/assets/javascripts/comment_type_toggle.js
@@ -2,7 +2,7 @@ import DropLab from './droplab/drop_lab';
import ISetter from './droplab/plugins/input_setter';
// Todo: Remove this when fixing issue in input_setter plugin
-const InputSetter = Object.assign({}, ISetter);
+const InputSetter = { ...ISetter };
class CommentTypeToggle {
constructor(opts = {}) {
diff --git a/app/assets/javascripts/commit/image_file.js b/app/assets/javascripts/commit/image_file.js
index fb8b1c17407..ddb129f36f4 100644
--- a/app/assets/javascripts/commit/image_file.js
+++ b/app/assets/javascripts/commit/image_file.js
@@ -1,4 +1,4 @@
-/* eslint-disable func-names, no-else-return, consistent-return, one-var, no-return-assign */
+/* eslint-disable func-names, consistent-return, one-var, no-return-assign */
import $ from 'jquery';
@@ -201,9 +201,8 @@ export default class ImageFile {
if (domImg) {
if (domImg.complete) {
return callback.call(this, domImg.naturalWidth, domImg.naturalHeight);
- } else {
- return img.on('load', () => callback.call(this, domImg.naturalWidth, domImg.naturalHeight));
}
+ return img.on('load', () => callback.call(this, domImg.naturalWidth, domImg.naturalHeight));
}
}
}
diff --git a/app/assets/javascripts/commons/index.js b/app/assets/javascripts/commons/index.js
index ad0f6cc1496..e0d012cef23 100644
--- a/app/assets/javascripts/commons/index.js
+++ b/app/assets/javascripts/commons/index.js
@@ -1,4 +1,3 @@
-import 'underscore';
import './polyfills';
import './jquery';
import './bootstrap';
diff --git a/app/assets/javascripts/compare_autocomplete.js b/app/assets/javascripts/compare_autocomplete.js
index a23707209dc..4539b9a39ef 100644
--- a/app/assets/javascripts/compare_autocomplete.js
+++ b/app/assets/javascripts/compare_autocomplete.js
@@ -1,4 +1,4 @@
-/* eslint-disable func-names, no-else-return */
+/* eslint-disable func-names */
import $ from 'jquery';
import { __ } from './locale';
@@ -52,9 +52,8 @@ export default function initCompareAutocomplete(limitTo = null, clickHandler = (
return $('<li />')
.addClass('dropdown-header')
.text(ref.header);
- } else {
- return $('<li />').append(link);
}
+ return $('<li />').append(link);
},
id(obj, $el) {
return $el.attr('data-ref');
diff --git a/app/assets/javascripts/confidential_merge_request/components/dropdown.vue b/app/assets/javascripts/confidential_merge_request/components/dropdown.vue
index 444640980af..92a5423d5ea 100644
--- a/app/assets/javascripts/confidential_merge_request/components/dropdown.vue
+++ b/app/assets/javascripts/confidential_merge_request/components/dropdown.vue
@@ -39,7 +39,7 @@ export default {
<template>
<gl-dropdown toggle-class="d-flex align-items-center w-100" class="w-100">
- <template slot="button-content">
+ <template #button-content>
<span class="str-truncated-100 mr-2">
<icon name="lock" />
{{ dropdownText }}
diff --git a/app/assets/javascripts/create_cluster/eks_cluster/components/eks_cluster_configuration_form.vue b/app/assets/javascripts/create_cluster/eks_cluster/components/eks_cluster_configuration_form.vue
index 74b5a62f754..d6c402fcb5d 100644
--- a/app/assets/javascripts/create_cluster/eks_cluster/components/eks_cluster_configuration_form.vue
+++ b/app/assets/javascripts/create_cluster/eks_cluster/components/eks_cluster_configuration_form.vue
@@ -1,6 +1,6 @@
<script>
import { createNamespacedHelpers, mapState, mapActions, mapGetters } from 'vuex';
-import { escape as esc } from 'lodash';
+import { escape } from 'lodash';
import { GlFormInput, GlFormCheckbox } from '@gitlab/ui';
import { sprintf, s__ } from '~/locale';
import ClusterFormDropdown from '~/create_cluster/components/cluster_form_dropdown.vue';
@@ -137,7 +137,7 @@ export default {
: s__('ClusterIntegration|Create Kubernetes cluster');
},
kubernetesIntegrationHelpText() {
- const escapedUrl = esc(this.kubernetesIntegrationHelpPath);
+ const escapedUrl = escape(this.kubernetesIntegrationHelpPath);
return sprintf(
s__(
@@ -256,7 +256,7 @@ export default {
);
},
gitlabManagedHelpText() {
- const escapedUrl = esc(this.gitlabManagedClusterHelpPath);
+ const escapedUrl = escape(this.gitlabManagedClusterHelpPath);
return sprintf(
s__(
diff --git a/app/assets/javascripts/create_cluster/eks_cluster/components/service_credentials_form.vue b/app/assets/javascripts/create_cluster/eks_cluster/components/service_credentials_form.vue
index 47cc4e4ce67..e063f9edfd9 100644
--- a/app/assets/javascripts/create_cluster/eks_cluster/components/service_credentials_form.vue
+++ b/app/assets/javascripts/create_cluster/eks_cluster/components/service_credentials_form.vue
@@ -1,6 +1,6 @@
<script>
import { GlFormInput } from '@gitlab/ui';
-import { escape as esc } from 'lodash';
+import { escape } from 'lodash';
import { mapState, mapActions } from 'vuex';
import { sprintf, s__, __ } from '~/locale';
import ClipboardButton from '~/vue_shared/components/clipboard_button.vue';
@@ -42,7 +42,7 @@ export default {
: s__('ClusterIntegration|Authenticate with AWS');
},
accountAndExternalIdsHelpText() {
- const escapedUrl = esc(this.accountAndExternalIdsHelpPath);
+ const escapedUrl = escape(this.accountAndExternalIdsHelpPath);
return sprintf(
s__(
@@ -59,7 +59,7 @@ export default {
);
},
provisionRoleArnHelpText() {
- const escapedUrl = esc(this.createRoleArnHelpPath);
+ const escapedUrl = escape(this.createRoleArnHelpPath);
return sprintf(
s__(
diff --git a/app/assets/javascripts/create_cluster/gke_cluster/components/gke_project_id_dropdown.vue b/app/assets/javascripts/create_cluster/gke_cluster/components/gke_project_id_dropdown.vue
index 6d8e6bbac11..b0bec10f64d 100644
--- a/app/assets/javascripts/create_cluster/gke_cluster/components/gke_project_id_dropdown.vue
+++ b/app/assets/javascripts/create_cluster/gke_cluster/components/gke_project_id_dropdown.vue
@@ -1,5 +1,5 @@
<script>
-import { escape as esc } from 'lodash';
+import { escape } from 'lodash';
import { mapState, mapGetters, mapActions } from 'vuex';
import { s__, sprintf } from '~/locale';
@@ -65,7 +65,7 @@ export default {
s__(message),
{
docsLinkEnd: '&nbsp;<i class="fa fa-external-link" aria-hidden="true"></i></a>',
- docsLinkStart: `<a href="${esc(
+ docsLinkStart: `<a href="${escape(
this.docsUrl,
)}" target="_blank" rel="noopener noreferrer">`,
},
diff --git a/app/assets/javascripts/create_item_dropdown.js b/app/assets/javascripts/create_item_dropdown.js
index 523e5592fd0..ec09dafebcb 100644
--- a/app/assets/javascripts/create_item_dropdown.js
+++ b/app/assets/javascripts/create_item_dropdown.js
@@ -1,4 +1,4 @@
-import { escape as esc } from 'lodash';
+import { escape } from 'lodash';
import '~/gl_dropdown';
export default class CreateItemDropdown {
@@ -37,14 +37,14 @@ export default class CreateItemDropdown {
},
selectable: true,
toggleLabel(selected) {
- return selected && 'id' in selected ? esc(selected.title) : this.defaultToggleLabel;
+ return selected && 'id' in selected ? escape(selected.title) : this.defaultToggleLabel;
},
fieldName: this.fieldName,
text(item) {
- return esc(item.text);
+ return escape(item.text);
},
id(item) {
- return esc(item.id);
+ return escape(item.id);
},
onFilter: this.toggleCreateNewButton.bind(this),
clicked: options => {
diff --git a/app/assets/javascripts/create_merge_request_dropdown.js b/app/assets/javascripts/create_merge_request_dropdown.js
index ba585444ba5..801566d2f2f 100644
--- a/app/assets/javascripts/create_merge_request_dropdown.js
+++ b/app/assets/javascripts/create_merge_request_dropdown.js
@@ -13,7 +13,7 @@ import {
import confidentialMergeRequestState from './confidential_merge_request/state';
// Todo: Remove this when fixing issue in input_setter plugin
-const InputSetter = Object.assign({}, ISetter);
+const InputSetter = { ...ISetter };
const CREATE_MERGE_REQUEST = 'create-mr';
const CREATE_BRANCH = 'create-branch';
diff --git a/app/assets/javascripts/cycle_analytics/cycle_analytics_store.js b/app/assets/javascripts/cycle_analytics/cycle_analytics_store.js
index 304a0726597..4f9069f61a5 100644
--- a/app/assets/javascripts/cycle_analytics/cycle_analytics_store.js
+++ b/app/assets/javascripts/cycle_analytics/cycle_analytics_store.js
@@ -84,7 +84,7 @@ export default {
events.forEach(item => {
if (!item) return;
- const eventItem = Object.assign({}, DEFAULT_EVENT_OBJECTS[stage.slug], item);
+ const eventItem = { ...DEFAULT_EVENT_OBJECTS[stage.slug], ...item };
eventItem.totalTime = eventItem.total_time;
diff --git a/app/assets/javascripts/deploy_keys/eventhub.js b/app/assets/javascripts/deploy_keys/eventhub.js
index 0948c2e5352..e31806ad199 100644
--- a/app/assets/javascripts/deploy_keys/eventhub.js
+++ b/app/assets/javascripts/deploy_keys/eventhub.js
@@ -1,3 +1,3 @@
-import Vue from 'vue';
+import createEventHub from '~/helpers/event_hub_factory';
-export default new Vue();
+export default createEventHub();
diff --git a/app/assets/javascripts/design_management/components/app.vue b/app/assets/javascripts/design_management/components/app.vue
new file mode 100644
index 00000000000..98240aef810
--- /dev/null
+++ b/app/assets/javascripts/design_management/components/app.vue
@@ -0,0 +1,3 @@
+<template>
+ <router-view />
+</template>
diff --git a/app/assets/javascripts/design_management/components/delete_button.vue b/app/assets/javascripts/design_management/components/delete_button.vue
new file mode 100644
index 00000000000..1fd902c9ed7
--- /dev/null
+++ b/app/assets/javascripts/design_management/components/delete_button.vue
@@ -0,0 +1,64 @@
+<script>
+import { GlDeprecatedButton, GlModal, GlModalDirective } from '@gitlab/ui';
+import { uniqueId } from 'lodash';
+
+export default {
+ name: 'DeleteButton',
+ components: {
+ GlDeprecatedButton,
+ GlModal,
+ },
+ directives: {
+ GlModalDirective,
+ },
+ props: {
+ isDeleting: {
+ type: Boolean,
+ required: false,
+ default: false,
+ },
+ buttonClass: {
+ type: String,
+ required: false,
+ default: '',
+ },
+ buttonVariant: {
+ type: String,
+ required: false,
+ default: '',
+ },
+ hasSelectedDesigns: {
+ type: Boolean,
+ required: false,
+ default: true,
+ },
+ },
+ data() {
+ return {
+ modalId: uniqueId('design-deletion-confirmation-'),
+ };
+ },
+};
+</script>
+
+<template>
+ <div>
+ <gl-modal
+ :modal-id="modalId"
+ :title="s__('DesignManagement|Delete designs confirmation')"
+ :ok-title="s__('DesignManagement|Delete')"
+ ok-variant="danger"
+ @ok="$emit('deleteSelectedDesigns')"
+ >
+ <p>{{ s__('DesignManagement|Are you sure you want to delete the selected designs?') }}</p>
+ </gl-modal>
+ <gl-deprecated-button
+ v-gl-modal-directive="modalId"
+ :variant="buttonVariant"
+ :disabled="isDeleting || !hasSelectedDesigns"
+ :class="buttonClass"
+ >
+ <slot></slot>
+ </gl-deprecated-button>
+ </div>
+</template>
diff --git a/app/assets/javascripts/design_management/components/design_destroyer.vue b/app/assets/javascripts/design_management/components/design_destroyer.vue
new file mode 100644
index 00000000000..ad3f2736c4a
--- /dev/null
+++ b/app/assets/javascripts/design_management/components/design_destroyer.vue
@@ -0,0 +1,66 @@
+<script>
+import { ApolloMutation } from 'vue-apollo';
+import getDesignListQuery from '../graphql/queries/get_design_list.query.graphql';
+import destroyDesignMutation from '../graphql/mutations/destroyDesign.mutation.graphql';
+import { updateStoreAfterDesignsDelete } from '../utils/cache_update';
+
+export default {
+ components: {
+ ApolloMutation,
+ },
+ props: {
+ filenames: {
+ type: Array,
+ required: true,
+ },
+ projectPath: {
+ type: String,
+ required: true,
+ },
+ iid: {
+ type: String,
+ required: true,
+ },
+ },
+ computed: {
+ projectQueryBody() {
+ return {
+ query: getDesignListQuery,
+ variables: { fullPath: this.projectPath, iid: this.iid, atVersion: null },
+ };
+ },
+ },
+ methods: {
+ updateStoreAfterDelete(
+ store,
+ {
+ data: { designManagementDelete },
+ },
+ ) {
+ updateStoreAfterDesignsDelete(
+ store,
+ designManagementDelete,
+ this.projectQueryBody,
+ this.filenames,
+ );
+ },
+ },
+ destroyDesignMutation,
+};
+</script>
+
+<template>
+ <apollo-mutation
+ #default="{ mutate, loading, error }"
+ :mutation="$options.destroyDesignMutation"
+ :variables="{
+ filenames,
+ projectPath,
+ iid,
+ }"
+ :update="updateStoreAfterDelete"
+ v-on="$listeners"
+ >
+ <slot v-bind="{ mutate, loading, error }"></slot>
+ </apollo-mutation>
+</template>
diff --git a/app/assets/javascripts/design_management/components/design_note_pin.vue b/app/assets/javascripts/design_management/components/design_note_pin.vue
new file mode 100644
index 00000000000..50ea69d52ce
--- /dev/null
+++ b/app/assets/javascripts/design_management/components/design_note_pin.vue
@@ -0,0 +1,61 @@
+<script>
+import { __, sprintf } from '~/locale';
+import Icon from '~/vue_shared/components/icon.vue';
+
+export default {
+ name: 'DesignNotePin',
+ components: {
+ Icon,
+ },
+ props: {
+ position: {
+ type: Object,
+ required: true,
+ },
+ label: {
+ type: String,
+ required: false,
+ default: null,
+ },
+ repositioning: {
+ type: Boolean,
+ required: false,
+ default: false,
+ },
+ },
+ computed: {
+ isNewNote() {
+ return this.label === null;
+ },
+ pinStyle() {
+ return this.repositioning ? { ...this.position, cursor: 'move' } : this.position;
+ },
+ pinLabel() {
+ return this.isNewNote
+ ? __('Comment form position')
+ : sprintf(__("Comment '%{label}' position"), { label: this.label });
+ },
+ },
+};
+</script>
+
+<template>
+ <button
+ :style="pinStyle"
+ :aria-label="pinLabel"
+ :class="{
+ 'btn-transparent comment-indicator': isNewNote,
+ 'js-image-badge badge badge-pill': !isNewNote,
+ }"
+ class="position-absolute"
+ type="button"
+ @mousedown="$emit('mousedown', $event)"
+ @mouseup="$emit('mouseup', $event)"
+ @click="$emit('click', $event)"
+ >
+ <icon v-if="isNewNote" name="image-comment-dark" />
+ <template v-else>
+ {{ label }}
+ </template>
+ </button>
+</template>
diff --git a/app/assets/javascripts/design_management/components/design_notes/design_discussion.vue b/app/assets/javascripts/design_management/components/design_notes/design_discussion.vue
new file mode 100644
index 00000000000..c6c5ee88a93
--- /dev/null
+++ b/app/assets/javascripts/design_management/components/design_notes/design_discussion.vue
@@ -0,0 +1,169 @@
+<script>
+import { ApolloMutation } from 'vue-apollo';
+import ReplyPlaceholder from '~/notes/components/discussion_reply_placeholder.vue';
+import allVersionsMixin from '../../mixins/all_versions';
+import createNoteMutation from '../../graphql/mutations/createNote.mutation.graphql';
+import getDesignQuery from '../../graphql/queries/getDesign.query.graphql';
+import activeDiscussionQuery from '../../graphql/queries/active_discussion.query.graphql';
+import DesignNote from './design_note.vue';
+import DesignReplyForm from './design_reply_form.vue';
+import { updateStoreAfterAddDiscussionComment } from '../../utils/cache_update';
+import { ACTIVE_DISCUSSION_SOURCE_TYPES } from '../../constants';
+
+export default {
+ components: {
+ ApolloMutation,
+ DesignNote,
+ ReplyPlaceholder,
+ DesignReplyForm,
+ },
+ mixins: [allVersionsMixin],
+ props: {
+ discussion: {
+ type: Object,
+ required: true,
+ },
+ noteableId: {
+ type: String,
+ required: true,
+ },
+ designId: {
+ type: String,
+ required: true,
+ },
+ discussionIndex: {
+ type: Number,
+ required: true,
+ },
+ markdownPreviewPath: {
+ type: String,
+ required: false,
+ default: '',
+ },
+ },
+ apollo: {
+ activeDiscussion: {
+ query: activeDiscussionQuery,
+ result({ data }) {
+ const discussionId = data.activeDiscussion.id;
+ // We watch any changes to the active discussion from the design pins and scroll to this discussion if it exists
+ // We don't want scrollIntoView to be triggered from the discussion click itself
+ if (
+ discussionId &&
+ data.activeDiscussion.source === ACTIVE_DISCUSSION_SOURCE_TYPES.pin &&
+ discussionId === this.discussion.notes[0].id
+ ) {
+ this.$el.scrollIntoView({
+ behavior: 'smooth',
+ inline: 'start',
+ });
+ }
+ },
+ },
+ },
+ data() {
+ return {
+ discussionComment: '',
+ isFormRendered: false,
+ activeDiscussion: {},
+ };
+ },
+ computed: {
+ mutationPayload() {
+ return {
+ noteableId: this.noteableId,
+ body: this.discussionComment,
+ discussionId: this.discussion.id,
+ };
+ },
+ designVariables() {
+ return {
+ fullPath: this.projectPath,
+ iid: this.issueIid,
+ filenames: [this.$route.params.id],
+ atVersion: this.designsVersion,
+ };
+ },
+ isDiscussionHighlighted() {
+ return this.discussion.notes[0].id === this.activeDiscussion.id;
+ },
+ },
+ methods: {
+ addDiscussionComment(
+ store,
+ {
+ data: { createNote },
+ },
+ ) {
+ updateStoreAfterAddDiscussionComment(
+ store,
+ createNote,
+ getDesignQuery,
+ this.designVariables,
+ this.discussion.id,
+ );
+ },
+ onDone() {
+ this.discussionComment = '';
+ this.hideForm();
+ },
+ onError(err) {
+ this.$emit('error', err);
+ },
+ hideForm() {
+ this.isFormRendered = false;
+ this.discussionComment = '';
+ },
+ showForm() {
+ this.isFormRendered = true;
+ },
+ },
+ createNoteMutation,
+};
+</script>
+
+<template>
+ <div class="design-discussion-wrapper">
+ <div class="badge badge-pill" type="button">{{ discussionIndex }}</div>
+ <div
+ class="design-discussion bordered-box position-relative"
+ data-qa-selector="design_discussion_content"
+ >
+ <design-note
+ v-for="note in discussion.notes"
+ :key="note.id"
+ :note="note"
+ :markdown-preview-path="markdownPreviewPath"
+ :class="{ 'gl-bg-blue-50': isDiscussionHighlighted }"
+ @error="$emit('updateNoteError', $event)"
+ />
+ <div class="reply-wrapper">
+ <reply-placeholder
+ v-if="!isFormRendered"
+ class="qa-discussion-reply"
+ :button-text="__('Reply...')"
+ @onClick="showForm"
+ />
+ <apollo-mutation
+ v-else
+ #default="{ mutate, loading }"
+ :mutation="$options.createNoteMutation"
+ :variables="{
+ input: mutationPayload,
+ }"
+ :update="addDiscussionComment"
+ @done="onDone"
+ @error="onError"
+ >
+ <design-reply-form
+ v-model="discussionComment"
+ :is-saving="loading"
+ :markdown-preview-path="markdownPreviewPath"
+ @submitForm="mutate"
+ @cancelForm="hideForm"
+ />
+ </apollo-mutation>
+ </div>
+ </div>
+ </div>
+</template>
diff --git a/app/assets/javascripts/design_management/components/design_notes/design_note.vue b/app/assets/javascripts/design_management/components/design_notes/design_note.vue
new file mode 100644
index 00000000000..c1c19c0a597
--- /dev/null
+++ b/app/assets/javascripts/design_management/components/design_notes/design_note.vue
@@ -0,0 +1,148 @@
+<script>
+import { ApolloMutation } from 'vue-apollo';
+import { GlTooltipDirective, GlIcon } from '@gitlab/ui';
+import updateNoteMutation from '../../graphql/mutations/update_note.mutation.graphql';
+import UserAvatarLink from '~/vue_shared/components/user_avatar/user_avatar_link.vue';
+import TimelineEntryItem from '~/vue_shared/components/notes/timeline_entry_item.vue';
+import TimeAgoTooltip from '~/vue_shared/components/time_ago_tooltip.vue';
+import DesignReplyForm from './design_reply_form.vue';
+import { findNoteId } from '../../utils/design_management_utils';
+import { hasErrors } from '../../utils/cache_update';
+
+export default {
+ components: {
+ UserAvatarLink,
+ TimelineEntryItem,
+ TimeAgoTooltip,
+ DesignReplyForm,
+ ApolloMutation,
+ GlIcon,
+ },
+ directives: {
+ GlTooltip: GlTooltipDirective,
+ },
+ props: {
+ note: {
+ type: Object,
+ required: true,
+ },
+ markdownPreviewPath: {
+ type: String,
+ required: false,
+ default: '',
+ },
+ },
+ data() {
+ return {
+ noteText: this.note.body,
+ isEditing: false,
+ };
+ },
+ computed: {
+ author() {
+ return this.note.author;
+ },
+ noteAnchorId() {
+ return findNoteId(this.note.id);
+ },
+ isNoteLinked() {
+ return this.$route.hash === `#note_${this.noteAnchorId}`;
+ },
+ mutationPayload() {
+ return {
+ id: this.note.id,
+ body: this.noteText,
+ };
+ },
+ },
+ mounted() {
+ if (this.isNoteLinked) {
+ this.$refs.anchor.$el.scrollIntoView({ behavior: 'smooth', inline: 'start' });
+ }
+ },
+ methods: {
+ hideForm() {
+ this.isEditing = false;
+ this.noteText = this.note.body;
+ },
+ onDone({ data }) {
+ this.hideForm();
+ if (hasErrors(data.updateNote)) {
+ this.$emit('error', data.errors[0]);
+ }
+ },
+ },
+ updateNoteMutation,
+};
+</script>
+
+<template>
+ <timeline-entry-item :id="`note_${noteAnchorId}`" ref="anchor" class="design-note note-form">
+ <user-avatar-link
+ :link-href="author.webUrl"
+ :img-src="author.avatarUrl"
+ :img-alt="author.username"
+ :img-size="40"
+ />
+ <div class="d-flex justify-content-between">
+ <div>
+ <a
+ v-once
+ :href="author.webUrl"
+ class="js-user-link"
+ :data-user-id="author.id"
+ :data-username="author.username"
+ >
+ <span class="note-header-author-name bold">{{ author.name }}</span>
+ <span v-if="author.status_tooltip_html" v-html="author.status_tooltip_html"></span>
+ <span class="note-headline-light">@{{ author.username }}</span>
+ </a>
+ <span class="note-headline-light note-headline-meta">
+ <span class="system-note-message"> <slot></slot> </span>
+ <template v-if="note.createdAt">
+ <span class="system-note-separator"></span>
+ <a class="note-timestamp system-note-separator" :href="`#note_${noteAnchorId}`">
+ <time-ago-tooltip :time="note.createdAt" tooltip-placement="bottom" />
+ </a>
+ </template>
+ </span>
+ </div>
+ <button
+ v-if="!isEditing && note.userPermissions.adminNote"
+ v-gl-tooltip
+ type="button"
+ title="Edit comment"
+ class="note-action-button js-note-edit btn btn-transparent qa-note-edit-button"
+ @click="isEditing = true"
+ >
+ <gl-icon name="pencil" class="link-highlight" />
+ </button>
+ </div>
+ <div
+ v-if="!isEditing"
+ class="note-text js-note-text md"
+ data-qa-selector="note_content"
+ v-html="note.bodyHtml"
+ ></div>
+ <apollo-mutation
+ v-else
+ #default="{ mutate, loading }"
+ :mutation="$options.updateNoteMutation"
+ :variables="{
+ input: mutationPayload,
+ }"
+ @error="$emit('error', $event)"
+ @done="onDone"
+ >
+ <design-reply-form
+ v-model="noteText"
+ :is-saving="loading"
+ :markdown-preview-path="markdownPreviewPath"
+ :is-new-comment="false"
+ class="mt-5"
+ @submitForm="mutate"
+ @cancelForm="hideForm"
+ />
+ </apollo-mutation>
+ </timeline-entry-item>
+</template>
diff --git a/app/assets/javascripts/design_management/components/design_notes/design_reply_form.vue b/app/assets/javascripts/design_management/components/design_notes/design_reply_form.vue
new file mode 100644
index 00000000000..40be9867fee
--- /dev/null
+++ b/app/assets/javascripts/design_management/components/design_notes/design_reply_form.vue
@@ -0,0 +1,137 @@
+<script>
+import { GlDeprecatedButton, GlModal } from '@gitlab/ui';
+import MarkdownField from '~/vue_shared/components/markdown/field.vue';
+import { s__ } from '~/locale';
+
+export default {
+ name: 'DesignReplyForm',
+ components: {
+ MarkdownField,
+ GlDeprecatedButton,
+ GlModal,
+ },
+ props: {
+ markdownPreviewPath: {
+ type: String,
+ required: false,
+ default: '',
+ },
+ value: {
+ type: String,
+ required: true,
+ },
+ isSaving: {
+ type: Boolean,
+ required: true,
+ },
+ isNewComment: {
+ type: Boolean,
+ required: false,
+ default: true,
+ },
+ },
+ data() {
+ return {
+ formText: this.value,
+ };
+ },
+ computed: {
+ hasValue() {
+ return this.value.trim().length > 0;
+ },
+ modalSettings() {
+ if (this.isNewComment) {
+ return {
+ title: s__('DesignManagement|Cancel comment confirmation'),
+ okTitle: s__('DesignManagement|Discard comment'),
+ cancelTitle: s__('DesignManagement|Keep comment'),
+ content: s__('DesignManagement|Are you sure you want to cancel creating this comment?'),
+ };
+ }
+ return {
+ title: s__('DesignManagement|Cancel comment update confirmation'),
+ okTitle: s__('DesignManagement|Cancel changes'),
+ cancelTitle: s__('DesignManagement|Keep changes'),
+ content: s__('DesignManagement|Are you sure you want to cancel changes to this comment?'),
+ };
+ },
+ buttonText() {
+ return this.isNewComment
+ ? s__('DesignManagement|Comment')
+ : s__('DesignManagement|Save comment');
+ },
+ },
+ mounted() {
+ this.$refs.textarea.focus();
+ },
+ methods: {
+ submitForm() {
+ if (this.hasValue) this.$emit('submitForm');
+ },
+ cancelComment() {
+ if (this.hasValue && this.formText !== this.value) {
+ this.$refs.cancelCommentModal.show();
+ } else {
+ this.$emit('cancelForm');
+ }
+ },
+ },
+};
+</script>
+
+<template>
+ <form class="new-note common-note-form" @submit.prevent>
+ <markdown-field
+ :markdown-preview-path="markdownPreviewPath"
+ :can-attach-file="false"
+ :enable-autocomplete="true"
+ :textarea-value="value"
+ markdown-docs-path="/help/user/markdown"
+ class="bordered-box"
+ >
+ <template #textarea>
+ <textarea
+ ref="textarea"
+ :value="value"
+ class="note-textarea js-gfm-input js-autosize markdown-area"
+ dir="auto"
+ data-supports-quick-actions="false"
+ data-qa-selector="note_textarea"
+ :aria-label="__('Description')"
+ :placeholder="__('Write a comment…')"
+ @input="$emit('input', $event.target.value)"
+ @keydown.meta.enter="submitForm"
+ @keydown.ctrl.enter="submitForm"
+ @keyup.esc.stop="cancelComment"
+ >
+ </textarea>
+ </template>
+ </markdown-field>
+ <div class="note-form-actions d-flex justify-content-between">
+ <gl-deprecated-button
+ ref="submitButton"
+ :disabled="!hasValue || isSaving"
+ variant="success"
+ type="submit"
+ data-track-event="click_button"
+ data-qa-selector="save_comment_button"
+ @click="$emit('submitForm')"
+ >
+ {{ buttonText }}
+ </gl-deprecated-button>
+ <gl-deprecated-button ref="cancelButton" @click="cancelComment">{{
+ __('Cancel')
+ }}</gl-deprecated-button>
+ </div>
+ <gl-modal
+ ref="cancelCommentModal"
+ ok-variant="danger"
+ :title="modalSettings.title"
+ :ok-title="modalSettings.okTitle"
+ :cancel-title="modalSettings.cancelTitle"
+ modal-id="cancel-comment-modal"
+ @ok="$emit('cancelForm')"
+ >{{ modalSettings.content }}
+ </gl-modal>
+ </form>
+</template>
diff --git a/app/assets/javascripts/design_management/components/design_overlay.vue b/app/assets/javascripts/design_management/components/design_overlay.vue
new file mode 100644
index 00000000000..beb51647821
--- /dev/null
+++ b/app/assets/javascripts/design_management/components/design_overlay.vue
@@ -0,0 +1,279 @@
+<script>
+import activeDiscussionQuery from '../graphql/queries/active_discussion.query.graphql';
+import updateActiveDiscussionMutation from '../graphql/mutations/update_active_discussion.mutation.graphql';
+import DesignNotePin from './design_note_pin.vue';
+import { ACTIVE_DISCUSSION_SOURCE_TYPES } from '../constants';
+
+export default {
+ name: 'DesignOverlay',
+ components: {
+ DesignNotePin,
+ },
+ props: {
+ dimensions: {
+ type: Object,
+ required: true,
+ },
+ position: {
+ type: Object,
+ required: true,
+ },
+ notes: {
+ type: Array,
+ required: false,
+ default: () => [],
+ },
+ currentCommentForm: {
+ type: Object,
+ required: false,
+ default: null,
+ },
+ disableCommenting: {
+ type: Boolean,
+ required: false,
+ default: false,
+ },
+ },
+ apollo: {
+ activeDiscussion: {
+ query: activeDiscussionQuery,
+ },
+ },
+ data() {
+ return {
+ movingNoteNewPosition: null,
+ movingNoteStartPosition: null,
+ activeDiscussion: {},
+ };
+ },
+ computed: {
+ overlayStyle() {
+ const cursor = this.disableCommenting ? 'unset' : undefined;
+
+ return {
+ cursor,
+ width: `${this.dimensions.width}px`,
+ height: `${this.dimensions.height}px`,
+ ...this.position,
+ };
+ },
+ isMovingCurrentComment() {
+ return Boolean(this.movingNoteStartPosition && !this.movingNoteStartPosition.noteId);
+ },
+ currentCommentPositionStyle() {
+ return this.isMovingCurrentComment && this.movingNoteNewPosition
+ ? this.getNotePositionStyle(this.movingNoteNewPosition)
+ : this.getNotePositionStyle(this.currentCommentForm);
+ },
+ },
+ methods: {
+ setNewNoteCoordinates({ x, y }) {
+ this.$emit('openCommentForm', { x, y });
+ },
+ getNoteRelativePosition(position) {
+ const { x, y, width, height } = position;
+ const widthRatio = this.dimensions.width / width;
+ const heightRatio = this.dimensions.height / height;
+ return {
+ left: Math.round(x * widthRatio),
+ top: Math.round(y * heightRatio),
+ };
+ },
+ getNotePositionStyle(position) {
+ const { left, top } = this.getNoteRelativePosition(position);
+ return {
+ left: `${left}px`,
+ top: `${top}px`,
+ };
+ },
+ getMovingNotePositionDelta(e) {
+ let deltaX = 0;
+ let deltaY = 0;
+
+ if (this.movingNoteStartPosition) {
+ const { clientX, clientY } = this.movingNoteStartPosition;
+ deltaX = e.clientX - clientX;
+ deltaY = e.clientY - clientY;
+ }
+
+ return {
+ deltaX,
+ deltaY,
+ };
+ },
+ isMovingNote(noteId) {
+ const movingNoteId = this.movingNoteStartPosition?.noteId;
+ return Boolean(movingNoteId && movingNoteId === noteId);
+ },
+ canMoveNote(note) {
+ const { userPermissions } = note;
+ const { adminNote } = userPermissions || {};
+
+ return Boolean(adminNote);
+ },
+ isPositionInOverlay(position) {
+ const { top, left } = this.getNoteRelativePosition(position);
+ const { height, width } = this.dimensions;
+
+ return top >= 0 && top <= height && left >= 0 && left <= width;
+ },
+ onNewNoteMove(e) {
+ if (!this.isMovingCurrentComment) return;
+
+ const { deltaX, deltaY } = this.getMovingNotePositionDelta(e);
+ const x = this.currentCommentForm.x + deltaX;
+ const y = this.currentCommentForm.y + deltaY;
+
+ const movingNoteNewPosition = {
+ x,
+ y,
+ width: this.dimensions.width,
+ height: this.dimensions.height,
+ };
+
+ if (!this.isPositionInOverlay(movingNoteNewPosition)) {
+ this.onNewNoteMouseup();
+ return;
+ }
+
+ this.movingNoteNewPosition = movingNoteNewPosition;
+ },
+ onExistingNoteMove(e) {
+ const note = this.notes.find(({ id }) => id === this.movingNoteStartPosition.noteId);
+ if (!note) return;
+
+ const { position } = note;
+ const { width, height } = position;
+ const widthRatio = this.dimensions.width / width;
+ const heightRatio = this.dimensions.height / height;
+
+ const { deltaX, deltaY } = this.getMovingNotePositionDelta(e);
+ const x = position.x * widthRatio + deltaX;
+ const y = position.y * heightRatio + deltaY;
+
+ const movingNoteNewPosition = {
+ x,
+ y,
+ width: this.dimensions.width,
+ height: this.dimensions.height,
+ };
+
+ if (!this.isPositionInOverlay(movingNoteNewPosition)) {
+ this.onExistingNoteMouseup();
+ return;
+ }
+
+ this.movingNoteNewPosition = movingNoteNewPosition;
+ },
+ onNewNoteMouseup() {
+ if (!this.movingNoteNewPosition) return;
+
+ const { x, y } = this.movingNoteNewPosition;
+ this.setNewNoteCoordinates({ x, y });
+ },
+ onExistingNoteMouseup(note) {
+ if (!this.movingNoteStartPosition || !this.movingNoteNewPosition) {
+ this.updateActiveDiscussion(note.id);
+ this.$emit('closeCommentForm');
+ return;
+ }
+
+ const { x, y } = this.movingNoteNewPosition;
+ this.$emit('moveNote', {
+ noteId: this.movingNoteStartPosition.noteId,
+ discussionId: this.movingNoteStartPosition.discussionId,
+ coordinates: { x, y },
+ });
+ },
+ onNoteMousedown({ clientX, clientY }, note) {
+ if (note && !this.canMoveNote(note)) return;
+
+ this.movingNoteStartPosition = {
+ noteId: note?.id,
+ discussionId: note?.discussion.id,
+ clientX,
+ clientY,
+ };
+ },
+ onOverlayMousemove(e) {
+ if (!this.movingNoteStartPosition) return;
+
+ if (this.isMovingCurrentComment) {
+ this.onNewNoteMove(e);
+ } else {
+ this.onExistingNoteMove(e);
+ }
+ },
+ onNoteMouseup(note) {
+ if (!this.movingNoteStartPosition) return;
+
+ if (this.isMovingCurrentComment) {
+ this.onNewNoteMouseup();
+ } else {
+ this.onExistingNoteMouseup(note);
+ }
+
+ this.movingNoteStartPosition = null;
+ this.movingNoteNewPosition = null;
+ },
+ onAddCommentMouseup({ offsetX, offsetY }) {
+ if (this.disableCommenting) return;
+ if (this.activeDiscussion.id) {
+ this.updateActiveDiscussion();
+ }
+
+ this.setNewNoteCoordinates({ x: offsetX, y: offsetY });
+ },
+ updateActiveDiscussion(id) {
+ this.$apollo.mutate({
+ mutation: updateActiveDiscussionMutation,
+ variables: {
+ id,
+ source: ACTIVE_DISCUSSION_SOURCE_TYPES.pin,
+ },
+ });
+ },
+ isNoteInactive(note) {
+ return this.activeDiscussion.id && this.activeDiscussion.id !== note.id;
+ },
+ },
+};
+</script>
+
+<template>
+ <div
+ class="position-absolute image-diff-overlay frame"
+ :style="overlayStyle"
+ @mousemove="onOverlayMousemove"
+ @mouseleave="onNoteMouseup"
+ >
+ <button
+ v-show="!disableCommenting"
+ type="button"
+ class="btn-transparent position-absolute image-diff-overlay-add-comment w-100 h-100 js-add-image-diff-note-button"
+ data-qa-selector="design_image_button"
+ @mouseup="onAddCommentMouseup"
+ ></button>
+ <design-note-pin
+ v-for="(note, index) in notes"
+ :key="note.id"
+ :label="`${index + 1}`"
+ :repositioning="isMovingNote(note.id)"
+ :position="
+ isMovingNote(note.id) && movingNoteNewPosition
+ ? getNotePositionStyle(movingNoteNewPosition)
+ : getNotePositionStyle(note.position)
+ "
+ :class="{ inactive: isNoteInactive(note) }"
+ @mousedown.stop="onNoteMousedown($event, note)"
+ @mouseup.stop="onNoteMouseup(note)"
+ />
+ <design-note-pin
+ v-if="currentCommentForm"
+ :position="currentCommentPositionStyle"
+ :repositioning="isMovingCurrentComment"
+ @mousedown.stop="onNoteMousedown"
+ @mouseup.stop="onNoteMouseup"
+ />
+ </div>
+</template>
diff --git a/app/assets/javascripts/design_management/components/design_presentation.vue b/app/assets/javascripts/design_management/components/design_presentation.vue
new file mode 100644
index 00000000000..5c113b3dbed
--- /dev/null
+++ b/app/assets/javascripts/design_management/components/design_presentation.vue
@@ -0,0 +1,314 @@
+<script>
+import { throttle } from 'lodash';
+import DesignImage from './image.vue';
+import DesignOverlay from './design_overlay.vue';
+
+const CLICK_DRAG_BUFFER_PX = 2;
+
+export default {
+ components: {
+ DesignImage,
+ DesignOverlay,
+ },
+ props: {
+ image: {
+ type: String,
+ required: false,
+ default: '',
+ },
+ imageName: {
+ type: String,
+ required: false,
+ default: '',
+ },
+ discussions: {
+ type: Array,
+ required: true,
+ },
+ isAnnotating: {
+ type: Boolean,
+ required: false,
+ default: false,
+ },
+ scale: {
+ type: Number,
+ required: false,
+ default: 1,
+ },
+ },
+ data() {
+ return {
+ overlayDimensions: null,
+ overlayPosition: null,
+ currentAnnotationPosition: null,
+ zoomFocalPoint: {
+ x: 0,
+ y: 0,
+ width: 0,
+ height: 0,
+ },
+ initialLoad: true,
+ lastDragPosition: null,
+ isDraggingDesign: false,
+ };
+ },
+ computed: {
+ discussionStartingNotes() {
+ return this.discussions.map(discussion => discussion.notes[0]);
+ },
+ currentCommentForm() {
+ return (this.isAnnotating && this.currentAnnotationPosition) || null;
+ },
+ presentationStyle() {
+ return {
+ cursor: this.isDraggingDesign ? 'grabbing' : undefined,
+ };
+ },
+ },
+ beforeDestroy() {
+ const { presentationViewport } = this.$refs;
+ if (!presentationViewport) return;
+
+ presentationViewport.removeEventListener('scroll', this.scrollThrottled, false);
+ },
+ mounted() {
+ const { presentationViewport } = this.$refs;
+ if (!presentationViewport) return;
+
+ this.scrollThrottled = throttle(() => {
+ this.shiftZoomFocalPoint();
+ }, 400);
+
+ presentationViewport.addEventListener('scroll', this.scrollThrottled, false);
+ },
+ methods: {
+ syncCurrentAnnotationPosition() {
+ if (!this.currentAnnotationPosition) return;
+
+ const widthRatio = this.overlayDimensions.width / this.currentAnnotationPosition.width;
+ const heightRatio = this.overlayDimensions.height / this.currentAnnotationPosition.height;
+ const x = this.currentAnnotationPosition.x * widthRatio;
+ const y = this.currentAnnotationPosition.y * heightRatio;
+
+ this.currentAnnotationPosition = this.getAnnotationPositon({ x, y });
+ },
+ setOverlayDimensions(overlayDimensions) {
+ this.overlayDimensions = overlayDimensions;
+
+ // every time we set overlay dimensions, we need to
+ // update the current annotation as well
+ this.syncCurrentAnnotationPosition();
+ },
+ setOverlayPosition() {
+ if (!this.overlayDimensions) {
+ this.overlayPosition = {};
+ }
+
+ const { presentationViewport } = this.$refs;
+ if (!presentationViewport) return;
+
+ // default to center
+ this.overlayPosition = {
+ left: `calc(50% - ${this.overlayDimensions.width / 2}px)`,
+ top: `calc(50% - ${this.overlayDimensions.height / 2}px)`,
+ };
+
+ // if the overlay overflows, then don't center
+ if (this.overlayDimensions.width > presentationViewport.offsetWidth) {
+ this.overlayPosition.left = '0';
+ }
+ if (this.overlayDimensions.height > presentationViewport.offsetHeight) {
+ this.overlayPosition.top = '0';
+ }
+ },
+ /**
+ * Return a point that represents the center of an
+ * overflowing child element w.r.t it's parent
+ */
+ getViewportCenter() {
+ const { presentationViewport } = this.$refs;
+ if (!presentationViewport) return {};
+
+ // get height of scroll bars (i.e. the max values for scrollTop, scrollLeft)
+ const scrollBarWidth = presentationViewport.scrollWidth - presentationViewport.offsetWidth;
+ const scrollBarHeight = presentationViewport.scrollHeight - presentationViewport.offsetHeight;
+
+ // determine how many child pixels have been scrolled
+ const xScrollRatio =
+ presentationViewport.scrollLeft > 0 ? presentationViewport.scrollLeft / scrollBarWidth : 0;
+ const yScrollRatio =
+ presentationViewport.scrollTop > 0 ? presentationViewport.scrollTop / scrollBarHeight : 0;
+ const xScrollOffset =
+ (presentationViewport.scrollWidth - presentationViewport.offsetWidth - 0) * xScrollRatio;
+ const yScrollOffset =
+ (presentationViewport.scrollHeight - presentationViewport.offsetHeight - 0) * yScrollRatio;
+
+ const viewportCenterX = presentationViewport.offsetWidth / 2;
+ const viewportCenterY = presentationViewport.offsetHeight / 2;
+ const focalPointX = viewportCenterX + xScrollOffset;
+ const focalPointY = viewportCenterY + yScrollOffset;
+
+ return {
+ x: focalPointX,
+ y: focalPointY,
+ };
+ },
+ /**
+ * Scroll the viewport such that the focal point is positioned centrally
+ */
+ scrollToFocalPoint() {
+ const { presentationViewport } = this.$refs;
+ if (!presentationViewport) return;
+
+ const scrollX = this.zoomFocalPoint.x - presentationViewport.offsetWidth / 2;
+ const scrollY = this.zoomFocalPoint.y - presentationViewport.offsetHeight / 2;
+
+ presentationViewport.scrollTo(scrollX, scrollY);
+ },
+ scaleZoomFocalPoint() {
+ const { x, y, width, height } = this.zoomFocalPoint;
+ const widthRatio = this.overlayDimensions.width / width;
+ const heightRatio = this.overlayDimensions.height / height;
+
+ this.zoomFocalPoint = {
+ x: Math.round(x * widthRatio * 100) / 100,
+ y: Math.round(y * heightRatio * 100) / 100,
+ ...this.overlayDimensions,
+ };
+ },
+ shiftZoomFocalPoint() {
+ this.zoomFocalPoint = {
+ ...this.getViewportCenter(),
+ ...this.overlayDimensions,
+ };
+ },
+ onImageResize(imageDimensions) {
+ this.setOverlayDimensions(imageDimensions);
+ this.setOverlayPosition();
+
+ this.$nextTick(() => {
+ if (this.initialLoad) {
+ // set focal point on initial load
+ this.shiftZoomFocalPoint();
+ this.initialLoad = false;
+ } else {
+ this.scaleZoomFocalPoint();
+ this.scrollToFocalPoint();
+ }
+ });
+ },
+ getAnnotationPositon(coordinates) {
+ const { x, y } = coordinates;
+ const { width, height } = this.overlayDimensions;
+ return {
+ x: Math.round(x),
+ y: Math.round(y),
+ width: Math.round(width),
+ height: Math.round(height),
+ };
+ },
+ openCommentForm(coordinates) {
+ this.currentAnnotationPosition = this.getAnnotationPositon(coordinates);
+ this.$emit('openCommentForm', this.currentAnnotationPosition);
+ },
+ closeCommentForm() {
+ this.currentAnnotationPosition = null;
+ this.$emit('closeCommentForm');
+ },
+ moveNote({ noteId, discussionId, coordinates }) {
+ const position = this.getAnnotationPositon(coordinates);
+ this.$emit('moveNote', { noteId, discussionId, position });
+ },
+ onPresentationMousedown({ clientX, clientY }) {
+ if (!this.isDesignOverflowing()) return;
+
+ this.lastDragPosition = {
+ x: clientX,
+ y: clientY,
+ };
+ },
+ getDragDelta(clientX, clientY) {
+ return {
+ deltaX: this.lastDragPosition.x - clientX,
+ deltaY: this.lastDragPosition.y - clientY,
+ };
+ },
+ exceedsDragThreshold(clientX, clientY) {
+ const { deltaX, deltaY } = this.getDragDelta(clientX, clientY);
+
+ return Math.abs(deltaX) > CLICK_DRAG_BUFFER_PX || Math.abs(deltaY) > CLICK_DRAG_BUFFER_PX;
+ },
+ shouldDragDesign(clientX, clientY) {
+ return (
+ this.lastDragPosition &&
+ (this.isDraggingDesign || this.exceedsDragThreshold(clientX, clientY))
+ );
+ },
+ onPresentationMousemove({ clientX, clientY }) {
+ const { presentationViewport } = this.$refs;
+ if (!presentationViewport || !this.shouldDragDesign(clientX, clientY)) return;
+
+ this.isDraggingDesign = true;
+
+ const { scrollLeft, scrollTop } = presentationViewport;
+ const { deltaX, deltaY } = this.getDragDelta(clientX, clientY);
+ presentationViewport.scrollTo(scrollLeft + deltaX, scrollTop + deltaY);
+
+ this.lastDragPosition = {
+ x: clientX,
+ y: clientY,
+ };
+ },
+ onPresentationMouseup() {
+ this.lastDragPosition = null;
+ this.isDraggingDesign = false;
+ },
+ isDesignOverflowing() {
+ const { presentationViewport } = this.$refs;
+ if (!presentationViewport) return false;
+
+ return (
+ presentationViewport.scrollWidth > presentationViewport.offsetWidth ||
+ presentationViewport.scrollHeight > presentationViewport.offsetHeight
+ );
+ },
+ },
+};
+</script>
+
+<template>
+ <div
+ ref="presentationViewport"
+ class="h-100 w-100 p-3 overflow-auto position-relative"
+ :style="presentationStyle"
+ @mousedown="onPresentationMousedown"
+ @mousemove="onPresentationMousemove"
+ @mouseup="onPresentationMouseup"
+ @mouseleave="onPresentationMouseup"
+ @touchstart="onPresentationMousedown"
+ @touchmove="onPresentationMousemove"
+ @touchend="onPresentationMouseup"
+ @touchcancel="onPresentationMouseup"
+ >
+ <div class="h-100 w-100 d-flex align-items-center position-relative">
+ <design-image
+ v-if="image"
+ :image="image"
+ :name="imageName"
+ :scale="scale"
+ @resize="onImageResize"
+ />
+ <design-overlay
+ v-if="overlayDimensions && overlayPosition"
+ :dimensions="overlayDimensions"
+ :position="overlayPosition"
+ :notes="discussionStartingNotes"
+ :current-comment-form="currentCommentForm"
+ :disable-commenting="isDraggingDesign"
+ @openCommentForm="openCommentForm"
+ @closeCommentForm="closeCommentForm"
+ @moveNote="moveNote"
+ />
+ </div>
+ </div>
+</template>
diff --git a/app/assets/javascripts/design_management/components/design_scaler.vue b/app/assets/javascripts/design_management/components/design_scaler.vue
new file mode 100644
index 00000000000..55dee74bef5
--- /dev/null
+++ b/app/assets/javascripts/design_management/components/design_scaler.vue
@@ -0,0 +1,65 @@
+<script>
+import { GlIcon } from '@gitlab/ui';
+
+const SCALE_STEP_SIZE = 0.2;
+const DEFAULT_SCALE = 1;
+const MIN_SCALE = 1;
+const MAX_SCALE = 2;
+
+export default {
+ components: {
+ GlIcon,
+ },
+ data() {
+ return {
+ scale: DEFAULT_SCALE,
+ };
+ },
+ computed: {
+ disableReset() {
+ return this.scale <= MIN_SCALE;
+ },
+ disableDecrease() {
+ return this.scale === DEFAULT_SCALE;
+ },
+ disableIncrease() {
+ return this.scale >= MAX_SCALE;
+ },
+ },
+ methods: {
+ setScale(scale) {
+ if (scale < MIN_SCALE) {
+ return;
+ }
+
+ this.scale = Math.round(scale * 100) / 100;
+ this.$emit('scale', this.scale);
+ },
+ incrementScale() {
+ this.setScale(this.scale + SCALE_STEP_SIZE);
+ },
+ decrementScale() {
+ this.setScale(this.scale - SCALE_STEP_SIZE);
+ },
+ resetScale() {
+ this.setScale(DEFAULT_SCALE);
+ },
+ },
+};
+</script>
+
+<template>
+ <div class="design-scaler btn-group" role="group">
+ <button class="btn" :disabled="disableDecrease" @click="decrementScale">
+ <span class="d-flex-center gl-icon s16">
+ –
+ </span>
+ </button>
+ <button class="btn" :disabled="disableReset" @click="resetScale">
+ <gl-icon name="redo" />
+ </button>
+ <button class="btn" :disabled="disableIncrease" @click="incrementScale">
+ <gl-icon name="plus" />
+ </button>
+ </div>
+</template>
diff --git a/app/assets/javascripts/design_management/components/image.vue b/app/assets/javascripts/design_management/components/image.vue
new file mode 100644
index 00000000000..91b7b576e0c
--- /dev/null
+++ b/app/assets/javascripts/design_management/components/image.vue
@@ -0,0 +1,110 @@
+<script>
+import { throttle } from 'lodash';
+import { GlIcon } from '@gitlab/ui';
+
+export default {
+ components: {
+ GlIcon,
+ },
+ props: {
+ image: {
+ type: String,
+ required: false,
+ default: '',
+ },
+ name: {
+ type: String,
+ required: false,
+ default: '',
+ },
+ scale: {
+ type: Number,
+ required: false,
+ default: 1,
+ },
+ },
+ data() {
+ return {
+ baseImageSize: null,
+ imageStyle: null,
+ imageError: false,
+ };
+ },
+ watch: {
+ scale(val) {
+ this.zoom(val);
+ },
+ },
+ beforeDestroy() {
+ window.removeEventListener('resize', this.resizeThrottled, false);
+ },
+ mounted() {
+ this.onImgLoad();
+
+ this.resizeThrottled = throttle(() => {
+ // NOTE: if imageStyle is set, then baseImageSize
+ // won't change due to resize. We must still emit a
+ // `resize` event so that the parent can handle
+ // resizes appropriately (e.g. for design_overlay)
+ this.setBaseImageSize();
+ }, 400);
+ window.addEventListener('resize', this.resizeThrottled, false);
+ },
+ methods: {
+ onImgLoad() {
+ requestIdleCallback(this.setBaseImageSize, { timeout: 1000 });
+ },
+ onImgError() {
+ this.imageError = true;
+ },
+ setBaseImageSize() {
+ const { contentImg } = this.$refs;
+ if (!contentImg || contentImg.offsetHeight === 0 || contentImg.offsetWidth === 0) return;
+
+ this.baseImageSize = {
+ height: contentImg.offsetHeight,
+ width: contentImg.offsetWidth,
+ };
+ this.onResize({ width: this.baseImageSize.width, height: this.baseImageSize.height });
+ },
+ onResize({ width, height }) {
+ this.$emit('resize', { width, height });
+ },
+ zoom(amount) {
+ if (amount === 1) {
+ this.imageStyle = null;
+ this.$nextTick(() => {
+ this.setBaseImageSize();
+ });
+ return;
+ }
+ const width = this.baseImageSize.width * amount;
+ const height = this.baseImageSize.height * amount;
+
+ this.imageStyle = {
+ width: `${width}px`,
+ height: `${height}px`,
+ };
+
+ this.onResize({ width, height });
+ },
+ },
+};
+</script>
+
+<template>
+ <div class="m-auto js-design-image">
+ <gl-icon v-if="imageError" class="text-secondary-100" name="media-broken" :size="48" />
+ <img
+ v-show="!imageError"
+ ref="contentImg"
+ class="mh-100"
+ :src="image"
+ :alt="name"
+ :style="imageStyle"
+ :class="{ 'img-fluid': !imageStyle }"
+ @error="onImgError"
+ @load="onImgLoad"
+ />
+ </div>
+</template>
diff --git a/app/assets/javascripts/design_management/components/list/item.vue b/app/assets/javascripts/design_management/components/list/item.vue
new file mode 100644
index 00000000000..eaa641d85d6
--- /dev/null
+++ b/app/assets/javascripts/design_management/components/list/item.vue
@@ -0,0 +1,174 @@
+<script>
+import { GlLoadingIcon, GlIcon, GlIntersectionObserver } from '@gitlab/ui';
+import Icon from '~/vue_shared/components/icon.vue';
+import Timeago from '~/vue_shared/components/time_ago_tooltip.vue';
+import { n__, __ } from '~/locale';
+import { DESIGN_ROUTE_NAME } from '../../router/constants';
+
+export default {
+ components: {
+ GlLoadingIcon,
+ GlIntersectionObserver,
+ GlIcon,
+ Icon,
+ Timeago,
+ },
+ props: {
+ id: {
+ type: [Number, String],
+ required: true,
+ },
+ event: {
+ type: String,
+ required: true,
+ },
+ notesCount: {
+ type: Number,
+ required: true,
+ },
+ image: {
+ type: String,
+ required: true,
+ },
+ filename: {
+ type: String,
+ required: true,
+ },
+ updatedAt: {
+ type: String,
+ required: false,
+ default: null,
+ },
+ isUploading: {
+ type: Boolean,
+ required: false,
+ default: true,
+ },
+ imageV432x230: {
+ type: String,
+ required: false,
+ default: null,
+ },
+ },
+ data() {
+ return {
+ imageLoading: true,
+ imageError: false,
+ wasInView: false,
+ };
+ },
+ computed: {
+ icon() {
+ const normalizedEvent = this.event.toLowerCase();
+ const icons = {
+ creation: {
+ name: 'file-addition-solid',
+ classes: 'text-success-500',
+ tooltip: __('Added in this version'),
+ },
+ modification: {
+ name: 'file-modified-solid',
+ classes: 'text-primary-500',
+ tooltip: __('Modified in this version'),
+ },
+ deletion: {
+ name: 'file-deletion-solid',
+ classes: 'text-danger-500',
+ tooltip: __('Deleted in this version'),
+ },
+ };
+
+ return icons[normalizedEvent] ? icons[normalizedEvent] : {};
+ },
+ notesLabel() {
+ return n__('%d comment', '%d comments', this.notesCount);
+ },
+ imageLink() {
+ return this.wasInView ? this.imageV432x230 || this.image : '';
+ },
+ showLoadingSpinner() {
+ return this.imageLoading || this.isUploading;
+ },
+ showImageErrorIcon() {
+ return this.wasInView && this.imageError;
+ },
+ showImage() {
+ return !this.showLoadingSpinner && !this.showImageErrorIcon;
+ },
+ },
+ methods: {
+ onImageLoad() {
+ this.imageLoading = false;
+ this.imageError = false;
+ },
+ onImageError() {
+ this.imageLoading = false;
+ this.imageError = true;
+ },
+ onAppear() {
+ // do nothing if image has previously
+ // been in view
+ if (this.wasInView) {
+ return;
+ }
+
+ this.wasInView = true;
+ this.imageLoading = true;
+ },
+ },
+ DESIGN_ROUTE_NAME,
+};
+</script>
+
+<template>
+ <router-link
+ :to="{
+ name: $options.DESIGN_ROUTE_NAME,
+ params: { id: filename },
+ query: $route.query,
+ }"
+ class="card cursor-pointer text-plain js-design-list-item design-list-item"
+ >
+ <div class="card-body p-0 d-flex-center overflow-hidden position-relative">
+ <div v-if="icon.name" class="design-event position-absolute">
+ <span :title="icon.tooltip" :aria-label="icon.tooltip">
+ <icon :name="icon.name" :size="18" :class="icon.classes" />
+ </span>
+ </div>
+ <gl-intersection-observer @appear="onAppear">
+ <gl-loading-icon v-if="showLoadingSpinner" size="md" />
+ <gl-icon
+ v-else-if="showImageErrorIcon"
+ name="media-broken"
+ class="text-secondary"
+ :size="32"
+ />
+ <img
+ v-show="showImage"
+ :src="imageLink"
+ :alt="filename"
+ class="block mx-auto mw-100 mh-100 design-img"
+ data-qa-selector="design_image"
+ @load="onImageLoad"
+ @error="onImageError"
+ />
+ </gl-intersection-observer>
+ </div>
+ <div class="card-footer d-flex w-100">
+ <div class="d-flex flex-column str-truncated-100">
+ <span class="bold str-truncated-100" data-qa-selector="design_file_name">{{
+ filename
+ }}</span>
+ <span v-if="updatedAt" class="str-truncated-100">
+ {{ __('Updated') }} <timeago :time="updatedAt" tooltip-placement="bottom" />
+ </span>
+ </div>
+ <div v-if="notesCount" class="ml-auto d-flex align-items-center text-secondary">
+ <icon name="comments" class="ml-1" />
+ <span :aria-label="notesLabel" class="ml-1">
+ {{ notesCount }}
+ </span>
+ </div>
+ </div>
+ </router-link>
+</template>
diff --git a/app/assets/javascripts/design_management/components/toolbar/index.vue b/app/assets/javascripts/design_management/components/toolbar/index.vue
new file mode 100644
index 00000000000..ea9f7300981
--- /dev/null
+++ b/app/assets/javascripts/design_management/components/toolbar/index.vue
@@ -0,0 +1,126 @@
+<script>
+import { GlDeprecatedButton } from '@gitlab/ui';
+import { __, sprintf } from '~/locale';
+import Icon from '~/vue_shared/components/icon.vue';
+import timeagoMixin from '~/vue_shared/mixins/timeago';
+import Pagination from './pagination.vue';
+import DeleteButton from '../delete_button.vue';
+import permissionsQuery from '../../graphql/queries/design_permissions.query.graphql';
+import appDataQuery from '../../graphql/queries/appData.query.graphql';
+import { DESIGNS_ROUTE_NAME } from '../../router/constants';
+
+export default {
+ components: {
+ Icon,
+ Pagination,
+ DeleteButton,
+ GlDeprecatedButton,
+ },
+ mixins: [timeagoMixin],
+ props: {
+ id: {
+ type: String,
+ required: true,
+ },
+ isDeleting: {
+ type: Boolean,
+ required: true,
+ },
+ filename: {
+ type: String,
+ required: false,
+ default: '',
+ },
+ updatedAt: {
+ type: String,
+ required: false,
+ default: null,
+ },
+ updatedBy: {
+ type: Object,
+ required: false,
+ default: () => ({}),
+ },
+ isLatestVersion: {
+ type: Boolean,
+ required: true,
+ },
+ image: {
+ type: String,
+ required: true,
+ },
+ },
+ data() {
+ return {
+ permissions: {
+ createDesign: false,
+ },
+ projectPath: '',
+ issueIid: null,
+ };
+ },
+ apollo: {
+ appData: {
+ query: appDataQuery,
+ manual: true,
+ result({ data: { projectPath, issueIid } }) {
+ this.projectPath = projectPath;
+ this.issueIid = issueIid;
+ },
+ },
+ permissions: {
+ query: permissionsQuery,
+ variables() {
+ return {
+ fullPath: this.projectPath,
+ iid: this.issueIid,
+ };
+ },
+ update: data => data.project.issue.userPermissions,
+ },
+ },
+ computed: {
+ updatedText() {
+ return sprintf(__('Updated %{updated_at} by %{updated_by}'), {
+ updated_at: this.timeFormatted(this.updatedAt),
+ updated_by: this.updatedBy.name,
+ });
+ },
+ canDeleteDesign() {
+ return this.permissions.createDesign;
+ },
+ },
+ DESIGNS_ROUTE_NAME,
+};
+</script>
+
+<template>
+ <header class="d-flex p-2 bg-white align-items-center js-design-header">
+ <router-link
+ :to="{
+ name: $options.DESIGNS_ROUTE_NAME,
+ query: $route.query,
+ }"
+ :aria-label="s__('DesignManagement|Go back to designs')"
+ class="mr-3 text-plain d-flex justify-content-center align-items-center"
+ >
+ <icon :size="18" name="close" />
+ </router-link>
+ <div class="overflow-hidden d-flex align-items-center">
+ <h2 class="m-0 str-truncated-100 gl-font-base">{{ filename }}</h2>
+ <small v-if="updatedAt" class="text-secondary">{{ updatedText }}</small>
+ </div>
+ <pagination :id="id" class="ml-auto flex-shrink-0" />
+ <gl-deprecated-button :href="image" class="mr-2">
+ <icon :size="18" name="download" />
+ </gl-deprecated-button>
+ <delete-button
+ v-if="isLatestVersion && canDeleteDesign"
+ :is-deleting="isDeleting"
+ button-variant="danger"
+ @deleteSelectedDesigns="$emit('delete')"
+ >
+ <icon :size="18" name="remove" />
+ </delete-button>
+ </header>
+</template>
diff --git a/app/assets/javascripts/design_management/components/toolbar/pagination.vue b/app/assets/javascripts/design_management/components/toolbar/pagination.vue
new file mode 100644
index 00000000000..bf62a8f66a6
--- /dev/null
+++ b/app/assets/javascripts/design_management/components/toolbar/pagination.vue
@@ -0,0 +1,83 @@
+<script>
+/* global Mousetrap */
+import 'mousetrap';
+import { s__, sprintf } from '~/locale';
+import PaginationButton from './pagination_button.vue';
+import allDesignsMixin from '../../mixins/all_designs';
+import { DESIGN_ROUTE_NAME } from '../../router/constants';
+
+export default {
+ components: {
+ PaginationButton,
+ },
+ mixins: [allDesignsMixin],
+ props: {
+ id: {
+ type: String,
+ required: true,
+ },
+ },
+ computed: {
+ designsCount() {
+ return this.designs.length;
+ },
+ currentIndex() {
+ return this.designs.findIndex(design => design.filename === this.id);
+ },
+ paginationText() {
+ return sprintf(s__('DesignManagement|%{current_design} of %{designs_count}'), {
+ current_design: this.currentIndex + 1,
+ designs_count: this.designsCount,
+ });
+ },
+ previousDesign() {
+ if (!this.designsCount) return null;
+
+ return this.designs[this.currentIndex - 1];
+ },
+ nextDesign() {
+ if (!this.designsCount) return null;
+
+ return this.designs[this.currentIndex + 1];
+ },
+ },
+ mounted() {
+ Mousetrap.bind('left', () => this.navigateToDesign(this.previousDesign));
+ Mousetrap.bind('right', () => this.navigateToDesign(this.nextDesign));
+ },
+ beforeDestroy() {
+ Mousetrap.unbind(['left', 'right'], this.navigateToDesign);
+ },
+ methods: {
+ navigateToDesign(design) {
+ if (design) {
+ this.$router.push({
+ name: DESIGN_ROUTE_NAME,
+ params: { id: design.filename },
+ query: this.$route.query,
+ });
+ }
+ },
+ },
+};
+</script>
+
+<template>
+ <div v-if="designsCount" class="d-flex align-items-center">
+ {{ paginationText }}
+ <div class="btn-group ml-3 mr-3">
+ <pagination-button
+ :design="previousDesign"
+ :title="s__('DesignManagement|Go to previous design')"
+ icon-name="angle-left"
+ class="js-previous-design"
+ />
+ <pagination-button
+ :design="nextDesign"
+ :title="s__('DesignManagement|Go to next design')"
+ icon-name="angle-right"
+ class="js-next-design"
+ />
+ </div>
+ </div>
+</template>
diff --git a/app/assets/javascripts/design_management/components/toolbar/pagination_button.vue b/app/assets/javascripts/design_management/components/toolbar/pagination_button.vue
new file mode 100644
index 00000000000..f00ecefca01
--- /dev/null
+++ b/app/assets/javascripts/design_management/components/toolbar/pagination_button.vue
@@ -0,0 +1,48 @@
+<script>
+import Icon from '~/vue_shared/components/icon.vue';
+import { DESIGN_ROUTE_NAME } from '../../router/constants';
+
+export default {
+ components: {
+ Icon,
+ },
+ props: {
+ design: {
+ type: Object,
+ required: false,
+ default: null,
+ },
+ title: {
+ type: String,
+ required: true,
+ },
+ iconName: {
+ type: String,
+ required: true,
+ },
+ },
+ computed: {
+ designLink() {
+ if (!this.design) return {};
+
+ return {
+ name: DESIGN_ROUTE_NAME,
+ params: { id: this.design.filename },
+ query: this.$route.query,
+ };
+ },
+ },
+};
+</script>
+
+<template>
+ <router-link
+ :to="designLink"
+ :disabled="!design"
+ :class="{ disabled: !design }"
+ :aria-label="title"
+ class="btn btn-default"
+ >
+ <icon :name="iconName" />
+ </router-link>
+</template>
diff --git a/app/assets/javascripts/design_management/components/upload/button.vue b/app/assets/javascripts/design_management/components/upload/button.vue
new file mode 100644
index 00000000000..e3c5e369170
--- /dev/null
+++ b/app/assets/javascripts/design_management/components/upload/button.vue
@@ -0,0 +1,58 @@
+<script>
+import { GlDeprecatedButton, GlLoadingIcon, GlTooltipDirective } from '@gitlab/ui';
+import { VALID_DESIGN_FILE_MIMETYPE } from '../../constants';
+
+export default {
+ components: {
+ GlDeprecatedButton,
+ GlLoadingIcon,
+ },
+ directives: {
+ GlTooltip: GlTooltipDirective,
+ },
+ props: {
+ isSaving: {
+ type: Boolean,
+ required: true,
+ },
+ },
+ methods: {
+ openFileUpload() {
+ this.$refs.fileUpload.click();
+ },
+ onFileUploadChange(e) {
+ this.$emit('upload', e.target.files);
+ },
+ },
+ VALID_DESIGN_FILE_MIMETYPE,
+};
+</script>
+
+<template>
+ <div>
+ <gl-deprecated-button
+ v-gl-tooltip.hover
+ :title="
+ s__(
+ 'DesignManagement|Adding a design with the same filename replaces the file in a new version.',
+ )
+ "
+ :disabled="isSaving"
+ variant="success"
+ @click="openFileUpload"
+ >
+ {{ s__('DesignManagement|Add designs') }}
+ <gl-loading-icon v-if="isSaving" inline class="ml-1" />
+ </gl-deprecated-button>
+
+ <input
+ ref="fileUpload"
+ type="file"
+ name="design_file"
+ :accept="$options.VALID_DESIGN_FILE_MIMETYPE.mimetype"
+ class="hide"
+ multiple
+ @change="onFileUploadChange"
+ />
+ </div>
+</template>
diff --git a/app/assets/javascripts/design_management/components/upload/design_dropzone.vue b/app/assets/javascripts/design_management/components/upload/design_dropzone.vue
new file mode 100644
index 00000000000..e2e1fc8bfad
--- /dev/null
+++ b/app/assets/javascripts/design_management/components/upload/design_dropzone.vue
@@ -0,0 +1,134 @@
+<script>
+import { GlIcon, GlLink, GlSprintf } from '@gitlab/ui';
+import createFlash from '~/flash';
+import uploadDesignMutation from '../../graphql/mutations/uploadDesign.mutation.graphql';
+import { UPLOAD_DESIGN_INVALID_FILETYPE_ERROR } from '../../utils/error_messages';
+import { isValidDesignFile } from '../../utils/design_management_utils';
+import { VALID_DATA_TRANSFER_TYPE, VALID_DESIGN_FILE_MIMETYPE } from '../../constants';
+
+export default {
+ components: {
+ GlIcon,
+ GlLink,
+ GlSprintf,
+ },
+ data() {
+ return {
+ dragCounter: 0,
+ isDragDataValid: false,
+ };
+ },
+ computed: {
+ dragging() {
+ return this.dragCounter !== 0;
+ },
+ },
+ methods: {
+ isValidUpload(files) {
+ return files.every(isValidDesignFile);
+ },
+ isValidDragDataType({ dataTransfer }) {
+ return Boolean(dataTransfer && dataTransfer.types.some(t => t === VALID_DATA_TRANSFER_TYPE));
+ },
+ ondrop({ dataTransfer = {} }) {
+ this.dragCounter = 0;
+ // User already had feedback when dropzone was active, so bail here
+ if (!this.isDragDataValid) {
+ return;
+ }
+
+ const { files } = dataTransfer;
+ if (!this.isValidUpload(Array.from(files))) {
+ createFlash(UPLOAD_DESIGN_INVALID_FILETYPE_ERROR);
+ return;
+ }
+
+ this.$emit('change', files);
+ },
+ ondragenter(e) {
+ this.dragCounter += 1;
+ this.isDragDataValid = this.isValidDragDataType(e);
+ },
+ ondragleave() {
+ this.dragCounter -= 1;
+ },
+ openFileUpload() {
+ this.$refs.fileUpload.click();
+ },
+ onDesignInputChange(e) {
+ this.$emit('change', e.target.files);
+ },
+ },
+ uploadDesignMutation,
+ VALID_DESIGN_FILE_MIMETYPE,
+};
+</script>
+
+<template>
+ <div
+ class="w-100 position-relative"
+ @dragstart.prevent.stop
+ @dragend.prevent.stop
+ @dragover.prevent.stop
+ @dragenter.prevent.stop="ondragenter"
+ @dragleave.prevent.stop="ondragleave"
+ @drop.prevent.stop="ondrop"
+ >
+ <slot>
+ <button
+ class="card design-dropzone-card design-dropzone-border w-100 h-100 d-flex-center p-3"
+ @click="openFileUpload"
+ >
+ <div class="d-flex-center flex-column text-center">
+ <gl-icon name="doc-new" :size="48" class="mb-4" />
+ <p>
+ <gl-sprintf
+ :message="
+ __(
+ '%{lineOneStart}Drag and drop to upload your designs%{lineOneEnd} or %{linkStart}click to upload%{linkEnd}.',
+ )
+ "
+ >
+ <template #lineOne="{ content }"
+ ><span class="d-block">{{ content }}</span>
+ </template>
+
+ <template #link="{ content }">
+ <gl-link class="h-100 w-100" @click.stop="openFileUpload">{{ content }}</gl-link>
+ </template>
+ </gl-sprintf>
+ </p>
+ </div>
+ </button>
+
+ <input
+ ref="fileUpload"
+ type="file"
+ name="design_file"
+ :accept="$options.VALID_DESIGN_FILE_MIMETYPE.mimetype"
+ class="hide"
+ multiple
+ @change="onDesignInputChange"
+ />
+ </slot>
+ <transition name="design-dropzone-fade">
+ <div
+ v-show="dragging"
+ class="card design-dropzone-border design-dropzone-overlay w-100 h-100 position-absolute d-flex-center p-3 bg-white"
+ >
+ <div v-show="!isDragDataValid" class="mw-50 text-center">
+ <h3>{{ __('Oh no!') }}</h3>
+ <span>{{
+ __(
+ 'You are trying to upload something other than an image. Please upload a .png, .jpg, .jpeg, .gif, .bmp, .tiff or .ico.',
+ )
+ }}</span>
+ </div>
+ <div v-show="isDragDataValid" class="mw-50 text-center">
+ <h3>{{ __('Incoming!') }}</h3>
+ <span>{{ __('Drop your designs to start your upload.') }}</span>
+ </div>
+ </div>
+ </transition>
+ </div>
+</template>
diff --git a/app/assets/javascripts/design_management/components/upload/design_version_dropdown.vue b/app/assets/javascripts/design_management/components/upload/design_version_dropdown.vue
new file mode 100644
index 00000000000..993eac6f37f
--- /dev/null
+++ b/app/assets/javascripts/design_management/components/upload/design_version_dropdown.vue
@@ -0,0 +1,76 @@
+<script>
+import { GlDropdown, GlDropdownItem } from '@gitlab/ui';
+import { __, sprintf } from '~/locale';
+import allVersionsMixin from '../../mixins/all_versions';
+import { findVersionId } from '../../utils/design_management_utils';
+
+export default {
+ components: {
+ GlDropdown,
+ GlDropdownItem,
+ },
+ mixins: [allVersionsMixin],
+ computed: {
+ queryVersion() {
+ return this.$route.query.version;
+ },
+ currentVersionIdx() {
+ if (!this.queryVersion) return 0;
+
+ const idx = this.allVersions.findIndex(
+ version => this.findVersionId(version.node.id) === this.queryVersion,
+ );
+
+ // if the currentVersionId isn't a valid version (i.e. not in allVersions)
+ // then return the latest version (index 0)
+ return idx !== -1 ? idx : 0;
+ },
+ currentVersionId() {
+ if (this.queryVersion) return this.queryVersion;
+
+ const currentVersion = this.allVersions[this.currentVersionIdx];
+ return this.findVersionId(currentVersion.node.id);
+ },
+ dropdownText() {
+ if (this.isLatestVersion) {
+ return __('Showing Latest Version');
+ }
+ // allVersions is sorted in reverse chronological order (latest first)
+ const currentVersionNumber = this.allVersions.length - this.currentVersionIdx;
+
+ return sprintf(__('Showing Version #%{versionNumber}'), {
+ versionNumber: currentVersionNumber,
+ });
+ },
+ },
+ methods: {
+ findVersionId,
+ },
+};
+</script>
+
+<template>
+ <gl-dropdown :text="dropdownText" variant="link" class="design-version-dropdown">
+ <gl-dropdown-item v-for="(version, index) in allVersions" :key="version.node.id">
+ <router-link
+ class="d-flex js-version-link"
+ :to="{ path: $route.path, query: { version: findVersionId(version.node.id) } }"
+ >
+ <div class="flex-grow-1 ml-2">
+ <div>
+ <strong
+ >{{ __('Version') }} {{ allVersions.length - index }}
+ <span v-if="findVersionId(version.node.id) === latestVersionId"
+ >({{ __('latest') }})</span
+ >
+ </strong>
+ </div>
+ </div>
+ <i
+ v-if="findVersionId(version.node.id) === currentVersionId"
+ class="fa fa-check pull-right"
+ ></i>
+ </router-link>
+ </gl-dropdown-item>
+ </gl-dropdown>
+</template>
diff --git a/app/assets/javascripts/design_management/constants.js b/app/assets/javascripts/design_management/constants.js
new file mode 100644
index 00000000000..59d34669ad7
--- /dev/null
+++ b/app/assets/javascripts/design_management/constants.js
@@ -0,0 +1,14 @@
+// WARNING: replace this with something
+// more sensical as per https://gitlab.com/gitlab-org/gitlab/issues/118611
+export const VALID_DESIGN_FILE_MIMETYPE = {
+ mimetype: 'image/*',
+ regex: /image\/.+/,
+};
+
+// https://developer.mozilla.org/en-US/docs/Web/API/DataTransfer/types
+export const VALID_DATA_TRANSFER_TYPE = 'Files';
+
+export const ACTIVE_DISCUSSION_SOURCE_TYPES = {
+ pin: 'pin',
+ discussion: 'discussion',
+};
diff --git a/app/assets/javascripts/design_management/graphql.js b/app/assets/javascripts/design_management/graphql.js
new file mode 100644
index 00000000000..fae337aa75b
--- /dev/null
+++ b/app/assets/javascripts/design_management/graphql.js
@@ -0,0 +1,45 @@
+import Vue from 'vue';
+import VueApollo from 'vue-apollo';
+import { uniqueId } from 'lodash';
+import { defaultDataIdFromObject } from 'apollo-cache-inmemory';
+import createDefaultClient from '~/lib/graphql';
+import activeDiscussionQuery from './graphql/queries/active_discussion.query.graphql';
+import typeDefs from './graphql/typedefs.graphql';
+
+Vue.use(VueApollo);
+
+const resolvers = {
+ Mutation: {
+ updateActiveDiscussion: (_, { id = null, source }, { cache }) => {
+ const data = cache.readQuery({ query: activeDiscussionQuery });
+ data.activeDiscussion = {
+ __typename: 'ActiveDiscussion',
+ id,
+ source,
+ };
+ cache.writeQuery({ query: activeDiscussionQuery, data });
+ },
+ },
+};
+
+const defaultClient = createDefaultClient(
+ resolvers,
+ // This config is added temporarily to resolve an issue with duplicate design IDs.
+ // Should be removed as soon as https://gitlab.com/gitlab-org/gitlab/issues/13495 is resolved
+ {
+ cacheConfig: {
+ dataIdFromObject: object => {
+ // eslint-disable-next-line no-underscore-dangle, @gitlab/require-i18n-strings
+ if (object.__typename === 'Design') {
+ return object.id && object.image ? `${object.id}-${object.image}` : uniqueId();
+ }
+ return defaultDataIdFromObject(object);
+ },
+ },
+ typeDefs,
+ },
+);
+
+export default new VueApollo({
+ defaultClient,
+});
diff --git a/app/assets/javascripts/design_management/graphql/fragments/design.fragment.graphql b/app/assets/javascripts/design_management/graphql/fragments/design.fragment.graphql
new file mode 100644
index 00000000000..ca5b5a52c71
--- /dev/null
+++ b/app/assets/javascripts/design_management/graphql/fragments/design.fragment.graphql
@@ -0,0 +1,22 @@
+#import "./designNote.fragment.graphql"
+#import "./designList.fragment.graphql"
+#import "./diffRefs.fragment.graphql"
+
+fragment DesignItem on Design {
+ ...DesignListItem
+ fullPath
+ diffRefs {
+ ...DesignDiffRefs
+ }
+ discussions {
+ nodes {
+ id
+ replyId
+ notes {
+ nodes {
+ ...DesignNote
+ }
+ }
+ }
+ }
+}
diff --git a/app/assets/javascripts/design_management/graphql/fragments/designList.fragment.graphql b/app/assets/javascripts/design_management/graphql/fragments/designList.fragment.graphql
new file mode 100644
index 00000000000..bc3132f9b42
--- /dev/null
+++ b/app/assets/javascripts/design_management/graphql/fragments/designList.fragment.graphql
@@ -0,0 +1,8 @@
+fragment DesignListItem on Design {
+ id
+ event
+ filename
+ notesCount
+ image
+ imageV432x230
+}
diff --git a/app/assets/javascripts/design_management/graphql/fragments/designNote.fragment.graphql b/app/assets/javascripts/design_management/graphql/fragments/designNote.fragment.graphql
new file mode 100644
index 00000000000..2ad84f9cb17
--- /dev/null
+++ b/app/assets/javascripts/design_management/graphql/fragments/designNote.fragment.graphql
@@ -0,0 +1,28 @@
+#import "./diffRefs.fragment.graphql"
+#import "~/graphql_shared/fragments/author.fragment.graphql"
+#import "./note_permissions.fragment.graphql"
+
+fragment DesignNote on Note {
+ id
+ author {
+ ...Author
+ }
+ body
+ bodyHtml
+ createdAt
+ position {
+ diffRefs {
+ ...DesignDiffRefs
+ }
+ x
+ y
+ height
+ width
+ }
+ userPermissions {
+ ...DesignNotePermissions
+ }
+ discussion {
+ id
+ }
+}
diff --git a/app/assets/javascripts/design_management/graphql/fragments/diffRefs.fragment.graphql b/app/assets/javascripts/design_management/graphql/fragments/diffRefs.fragment.graphql
new file mode 100644
index 00000000000..984a55814b0
--- /dev/null
+++ b/app/assets/javascripts/design_management/graphql/fragments/diffRefs.fragment.graphql
@@ -0,0 +1,5 @@
+fragment DesignDiffRefs on DiffRefs {
+ baseSha
+ startSha
+ headSha
+}
diff --git a/app/assets/javascripts/design_management/graphql/fragments/note_permissions.fragment.graphql b/app/assets/javascripts/design_management/graphql/fragments/note_permissions.fragment.graphql
new file mode 100644
index 00000000000..c243e39f3d3
--- /dev/null
+++ b/app/assets/javascripts/design_management/graphql/fragments/note_permissions.fragment.graphql
@@ -0,0 +1,3 @@
+fragment DesignNotePermissions on NotePermissions {
+ adminNote
+}
diff --git a/app/assets/javascripts/design_management/graphql/fragments/version.fragment.graphql b/app/assets/javascripts/design_management/graphql/fragments/version.fragment.graphql
new file mode 100644
index 00000000000..7eb40b12f51
--- /dev/null
+++ b/app/assets/javascripts/design_management/graphql/fragments/version.fragment.graphql
@@ -0,0 +1,4 @@
+fragment VersionListItem on DesignVersion {
+ id
+ sha
+}
diff --git a/app/assets/javascripts/design_management/graphql/mutations/createImageDiffNote.mutation.graphql b/app/assets/javascripts/design_management/graphql/mutations/createImageDiffNote.mutation.graphql
new file mode 100644
index 00000000000..9e2931b23f2
--- /dev/null
+++ b/app/assets/javascripts/design_management/graphql/mutations/createImageDiffNote.mutation.graphql
@@ -0,0 +1,21 @@
+#import "../fragments/designNote.fragment.graphql"
+
+mutation createImageDiffNote($input: CreateImageDiffNoteInput!) {
+ createImageDiffNote(input: $input) {
+ note {
+ ...DesignNote
+ discussion {
+ id
+ replyId
+ notes {
+ edges {
+ node {
+ ...DesignNote
+ }
+ }
+ }
+ }
+ }
+ errors
+ }
+}
diff --git a/app/assets/javascripts/design_management/graphql/mutations/createNote.mutation.graphql b/app/assets/javascripts/design_management/graphql/mutations/createNote.mutation.graphql
new file mode 100644
index 00000000000..3ae478d658e
--- /dev/null
+++ b/app/assets/javascripts/design_management/graphql/mutations/createNote.mutation.graphql
@@ -0,0 +1,10 @@
+#import "../fragments/designNote.fragment.graphql"
+
+mutation createNote($input: CreateNoteInput!) {
+ createNote(input: $input) {
+ note {
+ ...DesignNote
+ }
+ errors
+ }
+}
diff --git a/app/assets/javascripts/design_management/graphql/mutations/destroyDesign.mutation.graphql b/app/assets/javascripts/design_management/graphql/mutations/destroyDesign.mutation.graphql
new file mode 100644
index 00000000000..0b3cf636cdb
--- /dev/null
+++ b/app/assets/javascripts/design_management/graphql/mutations/destroyDesign.mutation.graphql
@@ -0,0 +1,10 @@
+#import "../fragments/version.fragment.graphql"
+
+mutation destroyDesign($filenames: [String!]!, $projectPath: ID!, $iid: ID!) {
+ designManagementDelete(input: { projectPath: $projectPath, iid: $iid, filenames: $filenames }) {
+ version {
+ ...VersionListItem
+ }
+ errors
+ }
+}
diff --git a/app/assets/javascripts/design_management/graphql/mutations/updateImageDiffNote.mutation.graphql b/app/assets/javascripts/design_management/graphql/mutations/updateImageDiffNote.mutation.graphql
new file mode 100644
index 00000000000..cdb2264d233
--- /dev/null
+++ b/app/assets/javascripts/design_management/graphql/mutations/updateImageDiffNote.mutation.graphql
@@ -0,0 +1,10 @@
+#import "../fragments/designNote.fragment.graphql"
+
+mutation updateImageDiffNote($input: UpdateImageDiffNoteInput!) {
+ updateImageDiffNote(input: $input) {
+ errors
+ note {
+ ...DesignNote
+ }
+ }
+}
diff --git a/app/assets/javascripts/design_management/graphql/mutations/update_active_discussion.mutation.graphql b/app/assets/javascripts/design_management/graphql/mutations/update_active_discussion.mutation.graphql
new file mode 100644
index 00000000000..343de4e3025
--- /dev/null
+++ b/app/assets/javascripts/design_management/graphql/mutations/update_active_discussion.mutation.graphql
@@ -0,0 +1,3 @@
+mutation updateActiveDiscussion($id: String, $source: String) {
+ updateActiveDiscussion (id: $id, source: $source ) @client
+}
diff --git a/app/assets/javascripts/design_management/graphql/mutations/update_note.mutation.graphql b/app/assets/javascripts/design_management/graphql/mutations/update_note.mutation.graphql
new file mode 100644
index 00000000000..d96b2f3934a
--- /dev/null
+++ b/app/assets/javascripts/design_management/graphql/mutations/update_note.mutation.graphql
@@ -0,0 +1,10 @@
+#import "../fragments/designNote.fragment.graphql"
+
+mutation updateNote($input: UpdateNoteInput!) {
+ updateNote(input: $input) {
+ note {
+ ...DesignNote
+ }
+ errors
+ }
+}
diff --git a/app/assets/javascripts/design_management/graphql/mutations/uploadDesign.mutation.graphql b/app/assets/javascripts/design_management/graphql/mutations/uploadDesign.mutation.graphql
new file mode 100644
index 00000000000..904acef599b
--- /dev/null
+++ b/app/assets/javascripts/design_management/graphql/mutations/uploadDesign.mutation.graphql
@@ -0,0 +1,21 @@
+#import "../fragments/design.fragment.graphql"
+
+mutation uploadDesign($files: [Upload!]!, $projectPath: ID!, $iid: ID!) {
+ designManagementUpload(input: { projectPath: $projectPath, iid: $iid, files: $files }) {
+ designs {
+ ...DesignItem
+ versions {
+ edges {
+ node {
+ id
+ sha
+ }
+ }
+ },
+ }
+ skippedDesigns {
+ filename
+ }
+ errors
+ }
+}
diff --git a/app/assets/javascripts/design_management/graphql/queries/active_discussion.query.graphql b/app/assets/javascripts/design_management/graphql/queries/active_discussion.query.graphql
new file mode 100644
index 00000000000..111023cea68
--- /dev/null
+++ b/app/assets/javascripts/design_management/graphql/queries/active_discussion.query.graphql
@@ -0,0 +1,6 @@
+query activeDiscussion {
+ activeDiscussion @client {
+ id
+ source
+ }
+}
diff --git a/app/assets/javascripts/design_management/graphql/queries/appData.query.graphql b/app/assets/javascripts/design_management/graphql/queries/appData.query.graphql
new file mode 100644
index 00000000000..e1269761206
--- /dev/null
+++ b/app/assets/javascripts/design_management/graphql/queries/appData.query.graphql
@@ -0,0 +1,4 @@
+query projectFullPath {
+ projectPath @client
+ issueIid @client
+}
diff --git a/app/assets/javascripts/design_management/graphql/queries/design_permissions.query.graphql b/app/assets/javascripts/design_management/graphql/queries/design_permissions.query.graphql
new file mode 100644
index 00000000000..a87b256dc95
--- /dev/null
+++ b/app/assets/javascripts/design_management/graphql/queries/design_permissions.query.graphql
@@ -0,0 +1,10 @@
+query permissions($fullPath: ID!, $iid: String!) {
+ project(fullPath: $fullPath) {
+ id
+ issue(iid: $iid) {
+ userPermissions {
+ createDesign
+ }
+ }
+ }
+}
diff --git a/app/assets/javascripts/design_management/graphql/queries/getDesign.query.graphql b/app/assets/javascripts/design_management/graphql/queries/getDesign.query.graphql
new file mode 100644
index 00000000000..07a9af55787
--- /dev/null
+++ b/app/assets/javascripts/design_management/graphql/queries/getDesign.query.graphql
@@ -0,0 +1,31 @@
+#import "../fragments/design.fragment.graphql"
+#import "~/graphql_shared/fragments/author.fragment.graphql"
+
+query getDesign($fullPath: ID!, $iid: String!, $atVersion: ID, $filenames: [String!]) {
+ project(fullPath: $fullPath) {
+ id
+ issue(iid: $iid) {
+ designCollection {
+ designs(atVersion: $atVersion, filenames: $filenames) {
+ edges {
+ node {
+ ...DesignItem
+ issue {
+ title
+ webPath
+ webUrl
+ participants {
+ edges {
+ node {
+ ...Author
+ }
+ }
+ }
+ }
+ }
+ }
+ }
+ }
+ }
+ }
+}
diff --git a/app/assets/javascripts/design_management/graphql/queries/get_design_list.query.graphql b/app/assets/javascripts/design_management/graphql/queries/get_design_list.query.graphql
new file mode 100644
index 00000000000..857f205ab07
--- /dev/null
+++ b/app/assets/javascripts/design_management/graphql/queries/get_design_list.query.graphql
@@ -0,0 +1,26 @@
+#import "../fragments/designList.fragment.graphql"
+#import "../fragments/version.fragment.graphql"
+
+query getDesignList($fullPath: ID!, $iid: String!, $atVersion: ID) {
+ project(fullPath: $fullPath) {
+ id
+ issue(iid: $iid) {
+ designCollection {
+ designs(atVersion: $atVersion) {
+ edges {
+ node {
+ ...DesignListItem
+ }
+ }
+ }
+ versions {
+ edges {
+ node {
+ ...VersionListItem
+ }
+ }
+ }
+ }
+ }
+ }
+}
diff --git a/app/assets/javascripts/design_management/graphql/typedefs.graphql b/app/assets/javascripts/design_management/graphql/typedefs.graphql
new file mode 100644
index 00000000000..fdbad4a90e0
--- /dev/null
+++ b/app/assets/javascripts/design_management/graphql/typedefs.graphql
@@ -0,0 +1,12 @@
+type ActiveDiscussion {
+ id: ID
+ source: String
+}
+
+extend type Query {
+ activeDiscussion: ActiveDiscussion
+}
+
+extend type Mutation {
+ updateActiveDiscussion(id: ID!, source: String!): Boolean
+}
diff --git a/app/assets/javascripts/design_management/index.js b/app/assets/javascripts/design_management/index.js
new file mode 100644
index 00000000000..eb00e1742ea
--- /dev/null
+++ b/app/assets/javascripts/design_management/index.js
@@ -0,0 +1,58 @@
+import $ from 'jquery';
+import Vue from 'vue';
+import createRouter from './router';
+import App from './components/app.vue';
+import apolloProvider from './graphql';
+import getDesignListQuery from './graphql/queries/get_design_list.query.graphql';
+import { DESIGNS_ROUTE_NAME, ROOT_ROUTE_NAME } from './router/constants';
+
+export default () => {
+ const el = document.querySelector('.js-design-management');
+ const badge = document.querySelector('.js-designs-count');
+ const { issueIid, projectPath, issuePath } = el.dataset;
+ const router = createRouter(issuePath);
+
+ $('.js-issue-tabs').on('shown.bs.tab', ({ target: { id } }) => {
+ if (id === 'designs' && router.currentRoute.name === ROOT_ROUTE_NAME) {
+ router.push({ name: DESIGNS_ROUTE_NAME });
+ } else if (id === 'discussion') {
+ router.push({ name: ROOT_ROUTE_NAME });
+ }
+ });
+
+ apolloProvider.clients.defaultClient.cache.writeData({
+ data: {
+ projectPath,
+ issueIid,
+ activeDiscussion: {
+ __typename: 'ActiveDiscussion',
+ id: null,
+ source: null,
+ },
+ },
+ });
+
+ apolloProvider.clients.defaultClient
+ .watchQuery({
+ query: getDesignListQuery,
+ variables: {
+ fullPath: projectPath,
+ iid: issueIid,
+ atVersion: null,
+ },
+ })
+ .subscribe(({ data }) => {
+ if (badge) {
+ badge.textContent = data.project.issue.designCollection.designs.edges.length;
+ }
+ });
+
+ return new Vue({
+ el,
+ router,
+ apolloProvider,
+ render(createElement) {
+ return createElement(App);
+ },
+ });
+};
diff --git a/app/assets/javascripts/design_management/mixins/all_designs.js b/app/assets/javascripts/design_management/mixins/all_designs.js
new file mode 100644
index 00000000000..f7d6551c46c
--- /dev/null
+++ b/app/assets/javascripts/design_management/mixins/all_designs.js
@@ -0,0 +1,49 @@
+import { propertyOf } from 'lodash';
+import createFlash from '~/flash';
+import { s__ } from '~/locale';
+import getDesignListQuery from '../graphql/queries/get_design_list.query.graphql';
+import { extractNodes } from '../utils/design_management_utils';
+import allVersionsMixin from './all_versions';
+import { DESIGNS_ROUTE_NAME } from '../router/constants';
+
+export default {
+ mixins: [allVersionsMixin],
+ apollo: {
+ designs: {
+ query: getDesignListQuery,
+ variables() {
+ return {
+ fullPath: this.projectPath,
+ iid: this.issueIid,
+ atVersion: this.designsVersion,
+ };
+ },
+ update: data => {
+ const designEdges = propertyOf(data)(['project', 'issue', 'designCollection', 'designs']);
+ if (designEdges) {
+ return extractNodes(designEdges);
+ }
+ return [];
+ },
+ error() {
+ this.error = true;
+ },
+ result() {
+ if (this.$route.query.version && !this.hasValidVersion) {
+ createFlash(
+ s__(
+ 'DesignManagement|Requested design version does not exist. Showing latest version instead',
+ ),
+ );
+ this.$router.replace({ name: DESIGNS_ROUTE_NAME, query: { version: undefined } });
+ }
+ },
+ },
+ },
+ data() {
+ return {
+ designs: [],
+ error: false,
+ };
+ },
+};
diff --git a/app/assets/javascripts/design_management/mixins/all_versions.js b/app/assets/javascripts/design_management/mixins/all_versions.js
new file mode 100644
index 00000000000..41c93064c26
--- /dev/null
+++ b/app/assets/javascripts/design_management/mixins/all_versions.js
@@ -0,0 +1,62 @@
+import getDesignListQuery from '../graphql/queries/get_design_list.query.graphql';
+import appDataQuery from '../graphql/queries/appData.query.graphql';
+import { findVersionId } from '../utils/design_management_utils';
+
+export default {
+ apollo: {
+ appData: {
+ query: appDataQuery,
+ manual: true,
+ result({ data: { projectPath, issueIid } }) {
+ this.projectPath = projectPath;
+ this.issueIid = issueIid;
+ },
+ },
+ allVersions: {
+ query: getDesignListQuery,
+ variables() {
+ return {
+ fullPath: this.projectPath,
+ iid: this.issueIid,
+ atVersion: null,
+ };
+ },
+ update: data => data.project.issue.designCollection.versions.edges,
+ },
+ },
+ computed: {
+ hasValidVersion() {
+ return (
+ this.$route.query.version &&
+ this.allVersions &&
+ this.allVersions.some(version => version.node.id.endsWith(this.$route.query.version))
+ );
+ },
+ designsVersion() {
+ return this.hasValidVersion
+ ? `gid://gitlab/DesignManagement::Version/${this.$route.query.version}`
+ : null;
+ },
+ latestVersionId() {
+ const latestVersion = this.allVersions[0];
+ return latestVersion && findVersionId(latestVersion.node.id);
+ },
+ isLatestVersion() {
+ if (this.allVersions.length > 0) {
+ return (
+ !this.$route.query.version ||
+ !this.latestVersionId ||
+ this.$route.query.version === this.latestVersionId
+ );
+ }
+ return true;
+ },
+ },
+ data() {
+ return {
+ allVersions: [],
+ projectPath: '',
+ issueIid: null,
+ };
+ },
+};
diff --git a/app/assets/javascripts/design_management/pages/design/index.vue b/app/assets/javascripts/design_management/pages/design/index.vue
new file mode 100644
index 00000000000..7ff3271394d
--- /dev/null
+++ b/app/assets/javascripts/design_management/pages/design/index.vue
@@ -0,0 +1,400 @@
+<script>
+import { ApolloMutation } from 'vue-apollo';
+import Mousetrap from 'mousetrap';
+import { GlLoadingIcon, GlAlert } from '@gitlab/ui';
+import createFlash from '~/flash';
+import { fetchPolicies } from '~/lib/graphql';
+import allVersionsMixin from '../../mixins/all_versions';
+import Toolbar from '../../components/toolbar/index.vue';
+import DesignDiscussion from '../../components/design_notes/design_discussion.vue';
+import DesignReplyForm from '../../components/design_notes/design_reply_form.vue';
+import DesignDestroyer from '../../components/design_destroyer.vue';
+import DesignScaler from '../../components/design_scaler.vue';
+import Participants from '~/sidebar/components/participants/participants.vue';
+import DesignPresentation from '../../components/design_presentation.vue';
+import getDesignQuery from '../../graphql/queries/getDesign.query.graphql';
+import appDataQuery from '../../graphql/queries/appData.query.graphql';
+import createImageDiffNoteMutation from '../../graphql/mutations/createImageDiffNote.mutation.graphql';
+import updateImageDiffNoteMutation from '../../graphql/mutations/updateImageDiffNote.mutation.graphql';
+import updateActiveDiscussionMutation from '../../graphql/mutations/update_active_discussion.mutation.graphql';
+import {
+ extractDiscussions,
+ extractDesign,
+ extractParticipants,
+ updateImageDiffNoteOptimisticResponse,
+} from '../../utils/design_management_utils';
+import {
+ updateStoreAfterAddImageDiffNote,
+ updateStoreAfterUpdateImageDiffNote,
+} from '../../utils/cache_update';
+import {
+ ADD_DISCUSSION_COMMENT_ERROR,
+ ADD_IMAGE_DIFF_NOTE_ERROR,
+ UPDATE_IMAGE_DIFF_NOTE_ERROR,
+ DESIGN_NOT_FOUND_ERROR,
+ DESIGN_VERSION_NOT_EXIST_ERROR,
+ UPDATE_NOTE_ERROR,
+ designDeletionError,
+} from '../../utils/error_messages';
+import { trackDesignDetailView } from '../../utils/tracking';
+import { DESIGNS_ROUTE_NAME } from '../../router/constants';
+import { ACTIVE_DISCUSSION_SOURCE_TYPES } from '../../constants';
+
+export default {
+ components: {
+ ApolloMutation,
+ DesignPresentation,
+ DesignDiscussion,
+ DesignScaler,
+ DesignDestroyer,
+ Toolbar,
+ DesignReplyForm,
+ GlLoadingIcon,
+ GlAlert,
+ Participants,
+ },
+ mixins: [allVersionsMixin],
+ props: {
+ id: {
+ type: String,
+ required: true,
+ },
+ },
+ data() {
+ return {
+ design: {},
+ comment: '',
+ annotationCoordinates: null,
+ projectPath: '',
+ errorMessage: '',
+ issueIid: '',
+ scale: 1,
+ };
+ },
+ apollo: {
+ appData: {
+ query: appDataQuery,
+ manual: true,
+ result({ data: { projectPath, issueIid } }) {
+ this.projectPath = projectPath;
+ this.issueIid = issueIid;
+ },
+ },
+ design: {
+ query: getDesignQuery,
+ // We want to see cached design version if we have one, and fetch newer version on the background to update discussions
+ fetchPolicy: fetchPolicies.CACHE_AND_NETWORK,
+ variables() {
+ return this.designVariables;
+ },
+ update: data => extractDesign(data),
+ result(res) {
+ this.onDesignQueryResult(res);
+ },
+ error() {
+ this.onQueryError(DESIGN_NOT_FOUND_ERROR);
+ },
+ },
+ },
+ computed: {
+ isFirstLoading() {
+ // We only want to show spinner on initial design load (when opened from a deep link to design)
+ // If we already have cached a design, loading shouldn't be indicated to user
+ return this.$apollo.queries.design.loading && !this.design.filename;
+ },
+ discussions() {
+ return extractDiscussions(this.design.discussions);
+ },
+ discussionParticipants() {
+ return extractParticipants(this.design.issue.participants);
+ },
+ markdownPreviewPath() {
+ return `/${this.projectPath}/preview_markdown?target_type=Issue`;
+ },
+ isSubmitButtonDisabled() {
+ return this.comment.trim().length === 0;
+ },
+ renderDiscussions() {
+ return this.discussions.length || this.annotationCoordinates;
+ },
+ designVariables() {
+ return {
+ fullPath: this.projectPath,
+ iid: this.issueIid,
+ filenames: [this.$route.params.id],
+ atVersion: this.designsVersion,
+ };
+ },
+ mutationPayload() {
+ const { x, y, width, height } = this.annotationCoordinates;
+ return {
+ noteableId: this.design.id,
+ body: this.comment,
+ position: {
+ headSha: this.design.diffRefs.headSha,
+ baseSha: this.design.diffRefs.baseSha,
+ startSha: this.design.diffRefs.startSha,
+ x,
+ y,
+ width,
+ height,
+ paths: {
+ newPath: this.design.fullPath,
+ },
+ },
+ };
+ },
+ issue() {
+ return {
+ ...this.design.issue,
+ webPath: this.design.issue.webPath.substr(1),
+ };
+ },
+ isAnnotating() {
+ return Boolean(this.annotationCoordinates);
+ },
+ },
+ mounted() {
+ Mousetrap.bind('esc', this.closeDesign);
+ },
+ beforeDestroy() {
+ Mousetrap.unbind('esc', this.closeDesign);
+ },
+ methods: {
+ addImageDiffNoteToStore(
+ store,
+ {
+ data: { createImageDiffNote },
+ },
+ ) {
+ updateStoreAfterAddImageDiffNote(
+ store,
+ createImageDiffNote,
+ getDesignQuery,
+ this.designVariables,
+ );
+ },
+ updateImageDiffNoteInStore(
+ store,
+ {
+ data: { updateImageDiffNote },
+ },
+ ) {
+ return updateStoreAfterUpdateImageDiffNote(
+ store,
+ updateImageDiffNote,
+ getDesignQuery,
+ this.designVariables,
+ );
+ },
+ onMoveNote({ noteId, discussionId, position }) {
+ const discussion = this.discussions.find(({ id }) => id === discussionId);
+ const note = discussion.notes.find(
+ ({ discussion: noteDiscussion }) => noteDiscussion.id === discussionId,
+ );
+
+ const mutationPayload = {
+ optimisticResponse: updateImageDiffNoteOptimisticResponse(note, {
+ position,
+ }),
+ variables: {
+ input: {
+ id: noteId,
+ position,
+ },
+ },
+ mutation: updateImageDiffNoteMutation,
+ update: this.updateImageDiffNoteInStore,
+ };
+
+ return this.$apollo.mutate(mutationPayload).catch(e => this.onUpdateImageDiffNoteError(e));
+ },
+ onDesignQueryResult({ data, loading }) {
+ // On the initial load with cache-and-network policy data is undefined while loading is true
+ // To prevent throwing an error, we don't perform any logic until loading is false
+ if (loading) {
+ return;
+ }
+
+ if (!data || !extractDesign(data)) {
+ this.onQueryError(DESIGN_NOT_FOUND_ERROR);
+ } else if (this.$route.query.version && !this.hasValidVersion) {
+ this.onQueryError(DESIGN_VERSION_NOT_EXIST_ERROR);
+ }
+ },
+ onQueryError(message) {
+ // because we redirect user to /designs (the issue page),
+ // we want to create these flashes on the issue page
+ createFlash(message);
+ this.$router.push({ name: this.$options.DESIGNS_ROUTE_NAME });
+ },
+ onError(message, e) {
+ this.errorMessage = message;
+ throw e;
+ },
+ onCreateImageDiffNoteError(e) {
+ this.onError(ADD_IMAGE_DIFF_NOTE_ERROR, e);
+ },
+ onUpdateNoteError(e) {
+ this.onError(UPDATE_NOTE_ERROR, e);
+ },
+ onDesignDiscussionError(e) {
+ this.onError(ADD_DISCUSSION_COMMENT_ERROR, e);
+ },
+ onUpdateImageDiffNoteError(e) {
+ this.onError(UPDATE_IMAGE_DIFF_NOTE_ERROR, e);
+ },
+ onDesignDeleteError(e) {
+ this.onError(designDeletionError({ singular: true }), e);
+ },
+ openCommentForm(annotationCoordinates) {
+ this.annotationCoordinates = annotationCoordinates;
+ },
+ closeCommentForm() {
+ this.comment = '';
+ this.annotationCoordinates = null;
+ },
+ closeDesign() {
+ this.$router.push({
+ name: this.$options.DESIGNS_ROUTE_NAME,
+ query: this.$route.query,
+ });
+ },
+ trackEvent() {
+ // TODO: This needs to be made aware of referers, or if it's rendered in a different context than a Issue
+ trackDesignDetailView(
+ 'issue-design-collection',
+ 'issue',
+ this.$route.query.version || this.latestVersionId,
+ this.isLatestVersion,
+ );
+ },
+ updateActiveDiscussion(id) {
+ this.$apollo.mutate({
+ mutation: updateActiveDiscussionMutation,
+ variables: {
+ id,
+ source: ACTIVE_DISCUSSION_SOURCE_TYPES.discussion,
+ },
+ });
+ },
+ },
+ beforeRouteEnter(to, from, next) {
+ next(vm => {
+ vm.trackEvent();
+ });
+ },
+ beforeRouteUpdate(to, from, next) {
+ this.trackEvent();
+ this.closeCommentForm();
+ // We need to reset the active discussion when opening a new design
+ this.updateActiveDiscussion();
+ next();
+ },
+ beforeRouteLeave(to, from, next) {
+ // We need to reset the active discussion when moving to design list view
+ this.updateActiveDiscussion();
+ next();
+ },
+ createImageDiffNoteMutation,
+ DESIGNS_ROUTE_NAME,
+};
+</script>
+
+<template>
+ <div
+ class="design-detail js-design-detail fixed-top w-100 position-bottom-0 d-flex justify-content-center flex-column flex-lg-row"
+ >
+ <gl-loading-icon v-if="isFirstLoading" size="xl" class="align-self-center" />
+ <template v-else>
+ <div class="d-flex overflow-hidden flex-grow-1 flex-column position-relative">
+ <design-destroyer
+ :filenames="[design.filename]"
+ :project-path="projectPath"
+ :iid="issueIid"
+ @done="$router.push({ name: $options.DESIGNS_ROUTE_NAME })"
+ @error="onDesignDeleteError"
+ >
+ <template #default="{ mutate, loading }">
+ <toolbar
+ :id="id"
+ :is-deleting="loading"
+ :is-latest-version="isLatestVersion"
+ v-bind="design"
+ @delete="mutate"
+ />
+ </template>
+ </design-destroyer>
+
+ <div v-if="errorMessage" class="p-3">
+ <gl-alert variant="danger" @dismiss="errorMessage = null">
+ {{ errorMessage }}
+ </gl-alert>
+ </div>
+ <design-presentation
+ :image="design.image"
+ :image-name="design.filename"
+ :discussions="discussions"
+ :is-annotating="isAnnotating"
+ :scale="scale"
+ @openCommentForm="openCommentForm"
+ @closeCommentForm="closeCommentForm"
+ @moveNote="onMoveNote"
+ />
+
+ <div class="design-scaler-wrapper position-absolute mb-4 d-flex-center">
+ <design-scaler @scale="scale = $event" />
+ </div>
+ </div>
+ <div class="image-notes" @click="updateActiveDiscussion()">
+ <h2 class="gl-font-size-20-deprecated-no-really-do-not-use-me font-weight-bold mt-0">
+ {{ issue.title }}
+ </h2>
+ <a class="text-tertiary text-decoration-none mb-3 d-block" :href="issue.webUrl">{{
+ issue.webPath
+ }}</a>
+ <participants
+ :participants="discussionParticipants"
+ :show-participant-label="false"
+ class="mb-4"
+ />
+ <template v-if="renderDiscussions">
+ <design-discussion
+ v-for="(discussion, index) in discussions"
+ :key="discussion.id"
+ :discussion="discussion"
+ :design-id="id"
+ :noteable-id="design.id"
+ :discussion-index="index + 1"
+ :markdown-preview-path="markdownPreviewPath"
+ @error="onDesignDiscussionError"
+ @updateNoteError="onUpdateNoteError"
+ @click.native.stop="updateActiveDiscussion(discussion.notes[0].id)"
+ />
+ <apollo-mutation
+ v-if="annotationCoordinates"
+ #default="{ mutate, loading }"
+ :mutation="$options.createImageDiffNoteMutation"
+ :variables="{
+ input: mutationPayload,
+ }"
+ :update="addImageDiffNoteToStore"
+ @done="closeCommentForm"
+ @error="onCreateImageDiffNoteError"
+ >
+ <design-reply-form
+ v-model="comment"
+ :is-saving="loading"
+ :markdown-preview-path="markdownPreviewPath"
+ @submitForm="mutate"
+ @cancelForm="closeCommentForm"
+ />
+ </apollo-mutation>
+ </template>
+ <h2 v-else class="new-discussion-disclaimer gl-font-base m-0">
+ {{ __("Click the image where you'd like to start a new discussion") }}
+ </h2>
+ </div>
+ </template>
+ </div>
+</template>
diff --git a/app/assets/javascripts/design_management/pages/index.vue b/app/assets/javascripts/design_management/pages/index.vue
new file mode 100644
index 00000000000..7d419bc3ded
--- /dev/null
+++ b/app/assets/javascripts/design_management/pages/index.vue
@@ -0,0 +1,323 @@
+<script>
+import { GlLoadingIcon, GlDeprecatedButton, GlAlert } from '@gitlab/ui';
+import createFlash from '~/flash';
+import { s__, sprintf } from '~/locale';
+import UploadButton from '../components/upload/button.vue';
+import DeleteButton from '../components/delete_button.vue';
+import Design from '../components/list/item.vue';
+import DesignDestroyer from '../components/design_destroyer.vue';
+import DesignVersionDropdown from '../components/upload/design_version_dropdown.vue';
+import DesignDropzone from '../components/upload/design_dropzone.vue';
+import uploadDesignMutation from '../graphql/mutations/uploadDesign.mutation.graphql';
+import permissionsQuery from '../graphql/queries/design_permissions.query.graphql';
+import getDesignListQuery from '../graphql/queries/get_design_list.query.graphql';
+import allDesignsMixin from '../mixins/all_designs';
+import {
+ UPLOAD_DESIGN_ERROR,
+ EXISTING_DESIGN_DROP_MANY_FILES_MESSAGE,
+ EXISTING_DESIGN_DROP_INVALID_FILENAME_MESSAGE,
+ designUploadSkippedWarning,
+ designDeletionError,
+} from '../utils/error_messages';
+import { updateStoreAfterUploadDesign } from '../utils/cache_update';
+import {
+ designUploadOptimisticResponse,
+ isValidDesignFile,
+} from '../utils/design_management_utils';
+import { getFilename } from '~/lib/utils/file_upload';
+import { DESIGNS_ROUTE_NAME } from '../router/constants';
+
+const MAXIMUM_FILE_UPLOAD_LIMIT = 10;
+
+export default {
+ components: {
+ GlLoadingIcon,
+ GlAlert,
+ GlDeprecatedButton,
+ UploadButton,
+ Design,
+ DesignDestroyer,
+ DesignVersionDropdown,
+ DeleteButton,
+ DesignDropzone,
+ },
+ mixins: [allDesignsMixin],
+ apollo: {
+ permissions: {
+ query: permissionsQuery,
+ variables() {
+ return {
+ fullPath: this.projectPath,
+ iid: this.issueIid,
+ };
+ },
+ update: data => data.project.issue.userPermissions,
+ },
+ },
+ data() {
+ return {
+ permissions: {
+ createDesign: false,
+ },
+ filesToBeSaved: [],
+ selectedDesigns: [],
+ };
+ },
+ computed: {
+ isLoading() {
+ return this.$apollo.queries.designs.loading || this.$apollo.queries.permissions.loading;
+ },
+ isSaving() {
+ return this.filesToBeSaved.length > 0;
+ },
+ canCreateDesign() {
+ return this.permissions.createDesign;
+ },
+ showToolbar() {
+ return this.canCreateDesign && this.allVersions.length > 0;
+ },
+ hasDesigns() {
+ return this.designs.length > 0;
+ },
+ hasSelectedDesigns() {
+ return this.selectedDesigns.length > 0;
+ },
+ canDeleteDesigns() {
+ return this.isLatestVersion && this.hasSelectedDesigns;
+ },
+ projectQueryBody() {
+ return {
+ query: getDesignListQuery,
+ variables: { fullPath: this.projectPath, iid: this.issueIid, atVersion: null },
+ };
+ },
+ selectAllButtonText() {
+ return this.hasSelectedDesigns
+ ? s__('DesignManagement|Deselect all')
+ : s__('DesignManagement|Select all');
+ },
+ },
+ mounted() {
+ this.toggleOnPasteListener(this.$route.name);
+ },
+ methods: {
+ resetFilesToBeSaved() {
+ this.filesToBeSaved = [];
+ },
+ /**
+ * Determine if a design upload is valid, given [files]
+ * @param {Array<File>} files
+ */
+ isValidDesignUpload(files) {
+ if (!this.canCreateDesign) return false;
+
+ if (files.length > MAXIMUM_FILE_UPLOAD_LIMIT) {
+ createFlash(
+ sprintf(
+ s__(
+ 'DesignManagement|The maximum number of designs allowed to be uploaded is %{upload_limit}. Please try again.',
+ ),
+ {
+ upload_limit: MAXIMUM_FILE_UPLOAD_LIMIT,
+ },
+ ),
+ );
+
+ return false;
+ }
+ return true;
+ },
+ onUploadDesign(files) {
+ // convert to Array so that we have Array methods (.map, .some, etc.)
+ this.filesToBeSaved = Array.from(files);
+ if (!this.isValidDesignUpload(this.filesToBeSaved)) return null;
+
+ const mutationPayload = {
+ optimisticResponse: designUploadOptimisticResponse(this.filesToBeSaved),
+ variables: {
+ files: this.filesToBeSaved,
+ projectPath: this.projectPath,
+ iid: this.issueIid,
+ },
+ context: {
+ hasUpload: true,
+ },
+ mutation: uploadDesignMutation,
+ update: this.afterUploadDesign,
+ };
+
+ return this.$apollo
+ .mutate(mutationPayload)
+ .then(res => this.onUploadDesignDone(res))
+ .catch(() => this.onUploadDesignError());
+ },
+ afterUploadDesign(
+ store,
+ {
+ data: { designManagementUpload },
+ },
+ ) {
+ updateStoreAfterUploadDesign(store, designManagementUpload, this.projectQueryBody);
+ },
+ onUploadDesignDone(res) {
+ const skippedFiles = res?.data?.designManagementUpload?.skippedDesigns || [];
+ const skippedWarningMessage = designUploadSkippedWarning(this.filesToBeSaved, skippedFiles);
+ if (skippedWarningMessage) {
+ createFlash(skippedWarningMessage, 'warning');
+ }
+
+ // if this upload resulted in a new version being created, redirect user to the latest version
+ if (!this.isLatestVersion) {
+ this.$router.push({ name: DESIGNS_ROUTE_NAME });
+ }
+ this.resetFilesToBeSaved();
+ },
+ onUploadDesignError() {
+ this.resetFilesToBeSaved();
+ createFlash(UPLOAD_DESIGN_ERROR);
+ },
+ changeSelectedDesigns(filename) {
+ if (this.isDesignSelected(filename)) {
+ this.selectedDesigns = this.selectedDesigns.filter(design => design !== filename);
+ } else {
+ this.selectedDesigns.push(filename);
+ }
+ },
+ toggleDesignsSelection() {
+ if (this.hasSelectedDesigns) {
+ this.selectedDesigns = [];
+ } else {
+ this.selectedDesigns = this.designs.map(design => design.filename);
+ }
+ },
+ isDesignSelected(filename) {
+ return this.selectedDesigns.includes(filename);
+ },
+ isDesignToBeSaved(filename) {
+ return this.filesToBeSaved.some(file => file.name === filename);
+ },
+ canSelectDesign(filename) {
+ return this.isLatestVersion && this.canCreateDesign && !this.isDesignToBeSaved(filename);
+ },
+ onDesignDelete() {
+ this.selectedDesigns = [];
+ if (this.$route.query.version) this.$router.push({ name: DESIGNS_ROUTE_NAME });
+ },
+ onDesignDeleteError() {
+ const errorMessage = designDeletionError({ singular: this.selectedDesigns.length === 1 });
+ createFlash(errorMessage);
+ },
+ onExistingDesignDropzoneChange(files, existingDesignFilename) {
+ const filesArr = Array.from(files);
+
+ if (filesArr.length > 1) {
+ createFlash(EXISTING_DESIGN_DROP_MANY_FILES_MESSAGE);
+ return;
+ }
+
+ if (!filesArr.some(({ name }) => existingDesignFilename === name)) {
+ createFlash(EXISTING_DESIGN_DROP_INVALID_FILENAME_MESSAGE);
+ return;
+ }
+
+ this.onUploadDesign(files);
+ },
+ onDesignPaste(event) {
+ const { clipboardData } = event;
+ const files = Array.from(clipboardData.files);
+ if (clipboardData && files.length > 0) {
+ if (!files.some(isValidDesignFile)) {
+ return;
+ }
+ event.preventDefault();
+ let filename = getFilename(event);
+ if (!filename || filename === 'image.png') {
+ filename = `design_${Date.now()}.png`;
+ }
+ const newFile = new File([files[0]], filename);
+ this.onUploadDesign([newFile]);
+ }
+ },
+ toggleOnPasteListener(route) {
+ if (route === DESIGNS_ROUTE_NAME) {
+ document.addEventListener('paste', this.onDesignPaste);
+ } else {
+ document.removeEventListener('paste', this.onDesignPaste);
+ }
+ },
+ },
+ beforeRouteUpdate(to, from, next) {
+ this.toggleOnPasteListener(to.name);
+ this.selectedDesigns = [];
+ next();
+ },
+ beforeRouteLeave(to, from, next) {
+ this.toggleOnPasteListener(to.name);
+ next();
+ },
+};
+</script>
+
+<template>
+ <div>
+ <header v-if="showToolbar" class="row-content-block border-top-0 p-2 d-flex">
+ <div class="d-flex justify-content-between align-items-center w-100">
+ <design-version-dropdown />
+ <div :class="['qa-selector-toolbar', { 'd-flex': hasDesigns, 'd-none': !hasDesigns }]">
+ <gl-deprecated-button
+ v-if="isLatestVersion"
+ variant="link"
+ class="mr-2 js-select-all"
+ @click="toggleDesignsSelection"
+ >{{ selectAllButtonText }}</gl-deprecated-button
+ >
+ <design-destroyer
+ #default="{ mutate, loading }"
+ :filenames="selectedDesigns"
+ :project-path="projectPath"
+ :iid="issueIid"
+ @done="onDesignDelete"
+ @error="onDesignDeleteError"
+ >
+ <delete-button
+ v-if="isLatestVersion"
+ :is-deleting="loading"
+ button-class="btn-danger btn-inverted mr-2"
+ :has-selected-designs="hasSelectedDesigns"
+ @deleteSelectedDesigns="mutate()"
+ >
+ {{ s__('DesignManagement|Delete selected') }}
+ <gl-loading-icon v-if="loading" inline class="ml-1" />
+ </delete-button>
+ </design-destroyer>
+ <upload-button v-if="canCreateDesign" :is-saving="isSaving" @upload="onUploadDesign" />
+ </div>
+ </div>
+ </header>
+ <div class="mt-4">
+ <gl-loading-icon v-if="isLoading" size="md" />
+ <gl-alert v-else-if="error" variant="danger" :dismissible="false">
+ {{ __('An error occurred while loading designs. Please try again.') }}
+ </gl-alert>
+ <ol v-else class="list-unstyled row">
+ <li class="col-md-6 col-lg-4 mb-3">
+ <design-dropzone class="design-list-item" @change="onUploadDesign" />
+ </li>
+ <li v-for="design in designs" :key="design.id" class="col-md-6 col-lg-4 mb-3">
+ <design-dropzone @change="onExistingDesignDropzoneChange($event, design.filename)"
+ ><design v-bind="design" :is-uploading="isDesignToBeSaved(design.filename)"
+ /></design-dropzone>
+
+ <input
+ v-if="canSelectDesign(design.filename)"
+ :checked="isDesignSelected(design.filename)"
+ type="checkbox"
+ class="design-checkbox"
+ @change="changeSelectedDesigns(design.filename)"
+ />
+ </li>
+ </ol>
+ </div>
+ <router-view />
+ </div>
+</template>
diff --git a/app/assets/javascripts/design_management/router/constants.js b/app/assets/javascripts/design_management/router/constants.js
new file mode 100644
index 00000000000..abeef520e33
--- /dev/null
+++ b/app/assets/javascripts/design_management/router/constants.js
@@ -0,0 +1,3 @@
+export const ROOT_ROUTE_NAME = 'root';
+export const DESIGNS_ROUTE_NAME = 'designs';
+export const DESIGN_ROUTE_NAME = 'design';
diff --git a/app/assets/javascripts/design_management/router/index.js b/app/assets/javascripts/design_management/router/index.js
new file mode 100644
index 00000000000..7dc92f55d47
--- /dev/null
+++ b/app/assets/javascripts/design_management/router/index.js
@@ -0,0 +1,22 @@
+import $ from 'jquery';
+import Vue from 'vue';
+import VueRouter from 'vue-router';
+import routes from './routes';
+
+Vue.use(VueRouter);
+
+export default function createRouter(base) {
+ const router = new VueRouter({
+ base,
+ mode: 'history',
+ routes,
+ });
+
+ router.beforeEach(({ meta: { el } }, from, next) => {
+ $(`#${el}`).tab('show');
+
+ next();
+ });
+
+ return router;
+}
diff --git a/app/assets/javascripts/design_management/router/routes.js b/app/assets/javascripts/design_management/router/routes.js
new file mode 100644
index 00000000000..788910e5514
--- /dev/null
+++ b/app/assets/javascripts/design_management/router/routes.js
@@ -0,0 +1,44 @@
+import Home from '../pages/index.vue';
+import DesignDetail from '../pages/design/index.vue';
+import { ROOT_ROUTE_NAME, DESIGNS_ROUTE_NAME, DESIGN_ROUTE_NAME } from './constants';
+
+export default [
+ {
+ name: ROOT_ROUTE_NAME,
+ path: '/',
+ component: Home,
+ meta: {
+ el: 'discussion',
+ },
+ },
+ {
+ name: DESIGNS_ROUTE_NAME,
+ path: '/designs',
+ component: Home,
+ meta: {
+ el: 'designs',
+ },
+ children: [
+ {
+ name: DESIGN_ROUTE_NAME,
+ path: ':id',
+ component: DesignDetail,
+ meta: {
+ el: 'designs',
+ },
+ beforeEnter(
+ {
+ params: { id },
+ },
+ from,
+ next,
+ ) {
+ if (typeof id === 'string') {
+ next();
+ }
+ },
+ props: ({ params: { id } }) => ({ id }),
+ },
+ ],
+ },
+];
diff --git a/app/assets/javascripts/design_management/utils/cache_update.js b/app/assets/javascripts/design_management/utils/cache_update.js
new file mode 100644
index 00000000000..01c073bddc2
--- /dev/null
+++ b/app/assets/javascripts/design_management/utils/cache_update.js
@@ -0,0 +1,272 @@
+/* eslint-disable @gitlab/require-i18n-strings */
+
+import createFlash from '~/flash';
+import { extractCurrentDiscussion, extractDesign } from './design_management_utils';
+import {
+ ADD_IMAGE_DIFF_NOTE_ERROR,
+ UPDATE_IMAGE_DIFF_NOTE_ERROR,
+ ADD_DISCUSSION_COMMENT_ERROR,
+ designDeletionError,
+} from './error_messages';
+
+const deleteDesignsFromStore = (store, query, selectedDesigns) => {
+ const data = store.readQuery(query);
+
+ const changedDesigns = data.project.issue.designCollection.designs.edges.filter(
+ ({ node }) => !selectedDesigns.includes(node.filename),
+ );
+ data.project.issue.designCollection.designs.edges = [...changedDesigns];
+
+ store.writeQuery({
+ ...query,
+ data,
+ });
+};
+
+/**
+ * Adds a new version of designs to store
+ *
+ * @param {Object} store
+ * @param {Object} query
+ * @param {Object} version
+ */
+const addNewVersionToStore = (store, query, version) => {
+ if (!version) return;
+
+ const data = store.readQuery(query);
+ const newEdge = { node: version, __typename: 'DesignVersionEdge' };
+
+ data.project.issue.designCollection.versions.edges = [
+ newEdge,
+ ...data.project.issue.designCollection.versions.edges,
+ ];
+
+ store.writeQuery({
+ ...query,
+ data,
+ });
+};
+
+const addDiscussionCommentToStore = (store, createNote, query, queryVariables, discussionId) => {
+ const data = store.readQuery({
+ query,
+ variables: queryVariables,
+ });
+
+ const design = extractDesign(data);
+ const currentDiscussion = extractCurrentDiscussion(design.discussions, discussionId);
+ currentDiscussion.notes.nodes = [...currentDiscussion.notes.nodes, createNote.note];
+
+ design.notesCount += 1;
+ if (
+ !design.issue.participants.edges.some(
+ participant => participant.node.username === createNote.note.author.username,
+ )
+ ) {
+ design.issue.participants.edges = [
+ ...design.issue.participants.edges,
+ {
+ __typename: 'UserEdge',
+ node: {
+ __typename: 'User',
+ ...createNote.note.author,
+ },
+ },
+ ];
+ }
+ store.writeQuery({
+ query,
+ variables: queryVariables,
+ data: {
+ ...data,
+ design: {
+ ...design,
+ },
+ },
+ });
+};
+
+const addImageDiffNoteToStore = (store, createImageDiffNote, query, variables) => {
+ const data = store.readQuery({
+ query,
+ variables,
+ });
+ const newDiscussion = {
+ __typename: 'Discussion',
+ id: createImageDiffNote.note.discussion.id,
+ replyId: createImageDiffNote.note.discussion.replyId,
+ notes: {
+ __typename: 'NoteConnection',
+ nodes: [createImageDiffNote.note],
+ },
+ };
+ const design = extractDesign(data);
+ const notesCount = design.notesCount + 1;
+ design.discussions.nodes = [...design.discussions.nodes, newDiscussion];
+ if (
+ !design.issue.participants.edges.some(
+ participant => participant.node.username === createImageDiffNote.note.author.username,
+ )
+ ) {
+ design.issue.participants.edges = [
+ ...design.issue.participants.edges,
+ {
+ __typename: 'UserEdge',
+ node: {
+ __typename: 'User',
+ ...createImageDiffNote.note.author,
+ },
+ },
+ ];
+ }
+ store.writeQuery({
+ query,
+ variables,
+ data: {
+ ...data,
+ design: {
+ ...design,
+ notesCount,
+ },
+ },
+ });
+};
+
+const updateImageDiffNoteInStore = (store, updateImageDiffNote, query, variables) => {
+ const data = store.readQuery({
+ query,
+ variables,
+ });
+
+ const design = extractDesign(data);
+ const discussion = extractCurrentDiscussion(
+ design.discussions,
+ updateImageDiffNote.note.discussion.id,
+ );
+
+ discussion.notes = {
+ ...discussion.notes,
+ nodes: [updateImageDiffNote.note, ...discussion.notes.nodes.slice(1)],
+ };
+
+ store.writeQuery({
+ query,
+ variables,
+ data: {
+ ...data,
+ design,
+ },
+ });
+};
+
+const addNewDesignToStore = (store, designManagementUpload, query) => {
+ const data = store.readQuery(query);
+
+ const newDesigns = data.project.issue.designCollection.designs.edges.reduce((acc, design) => {
+ if (!acc.find(d => d.filename === design.node.filename)) {
+ acc.push(design.node);
+ }
+
+ return acc;
+ }, designManagementUpload.designs);
+
+ let newVersionNode;
+ const findNewVersions = designManagementUpload.designs.find(design => design.versions);
+
+ if (findNewVersions) {
+ const findNewVersionsEdges = findNewVersions.versions.edges;
+
+ if (findNewVersionsEdges && findNewVersionsEdges.length) {
+ newVersionNode = [findNewVersionsEdges[0]];
+ }
+ }
+
+ const newVersions = [
+ ...(newVersionNode || []),
+ ...data.project.issue.designCollection.versions.edges,
+ ];
+
+ const updatedDesigns = {
+ __typename: 'DesignCollection',
+ designs: {
+ __typename: 'DesignConnection',
+ edges: newDesigns.map(design => ({
+ __typename: 'DesignEdge',
+ node: design,
+ })),
+ },
+ versions: {
+ __typename: 'DesignVersionConnection',
+ edges: newVersions,
+ },
+ };
+
+ data.project.issue.designCollection = updatedDesigns;
+
+ store.writeQuery({
+ ...query,
+ data,
+ });
+};
+
+const onError = (data, message) => {
+ createFlash(message);
+ throw new Error(data.errors);
+};
+
+export const hasErrors = ({ errors = [] }) => errors?.length;
+
+/**
+ * Updates a store after design deletion
+ *
+ * @param {Object} store
+ * @param {Object} data
+ * @param {Object} query
+ * @param {Array} designs
+ */
+export const updateStoreAfterDesignsDelete = (store, data, query, designs) => {
+ if (hasErrors(data)) {
+ onError(data, designDeletionError({ singular: designs.length === 1 }));
+ } else {
+ deleteDesignsFromStore(store, query, designs);
+ addNewVersionToStore(store, query, data.version);
+ }
+};
+
+export const updateStoreAfterAddDiscussionComment = (
+ store,
+ data,
+ query,
+ queryVariables,
+ discussionId,
+) => {
+ if (hasErrors(data)) {
+ onError(data, ADD_DISCUSSION_COMMENT_ERROR);
+ } else {
+ addDiscussionCommentToStore(store, data, query, queryVariables, discussionId);
+ }
+};
+
+export const updateStoreAfterAddImageDiffNote = (store, data, query, queryVariables) => {
+ if (hasErrors(data)) {
+ onError(data, ADD_IMAGE_DIFF_NOTE_ERROR);
+ } else {
+ addImageDiffNoteToStore(store, data, query, queryVariables);
+ }
+};
+
+export const updateStoreAfterUpdateImageDiffNote = (store, data, query, queryVariables) => {
+ if (hasErrors(data)) {
+ onError(data, UPDATE_IMAGE_DIFF_NOTE_ERROR);
+ } else {
+ updateImageDiffNoteInStore(store, data, query, queryVariables);
+ }
+};
+
+export const updateStoreAfterUploadDesign = (store, data, query) => {
+ if (hasErrors(data)) {
+ onError(data, data.errors[0]);
+ } else {
+ addNewDesignToStore(store, data, query);
+ }
+};
diff --git a/app/assets/javascripts/design_management/utils/design_management_utils.js b/app/assets/javascripts/design_management/utils/design_management_utils.js
new file mode 100644
index 00000000000..e6d8796ffa4
--- /dev/null
+++ b/app/assets/javascripts/design_management/utils/design_management_utils.js
@@ -0,0 +1,125 @@
+import { uniqueId } from 'lodash';
+import { VALID_DESIGN_FILE_MIMETYPE } from '../constants';
+
+export const isValidDesignFile = ({ type }) =>
+ (type.match(VALID_DESIGN_FILE_MIMETYPE.regex) || []).length > 0;
+
+/**
+ * Returns formatted array that doesn't contain
+ * `edges`->`node` nesting
+ *
+ * @param {Array} elements
+ */
+
+export const extractNodes = elements => elements.edges.map(({ node }) => node);
+
+/**
+ * Returns formatted array of discussions that doesn't contain
+ * `edges`->`node` nesting for child notes
+ *
+ * @param {Array} discussions
+ */
+
+export const extractDiscussions = discussions =>
+ discussions.nodes.map(discussion => ({
+ ...discussion,
+ notes: discussion.notes.nodes,
+ }));
+
+/**
+ * Returns a discussion with the given id from discussions array
+ *
+ * @param {Array} discussions
+ */
+
+export const extractCurrentDiscussion = (discussions, id) =>
+ discussions.nodes.find(discussion => discussion.id === id);
+
+export const findVersionId = id => (id.match('::Version/(.+$)') || [])[1];
+
+export const findNoteId = id => (id.match('DiffNote/(.+$)') || [])[1];
+
+export const extractDesigns = data => data.project.issue.designCollection.designs.edges;
+
+export const extractDesign = data => (extractDesigns(data) || [])[0]?.node;
+
+/**
+ * Generates optimistic response for a design upload mutation
+ * @param {Array<File>} files
+ */
+export const designUploadOptimisticResponse = files => {
+ const designs = files.map(file => ({
+ // False positive i18n lint: https://gitlab.com/gitlab-org/frontend/eslint-plugin-i18n/issues/26
+ // eslint-disable-next-line @gitlab/require-i18n-strings
+ __typename: 'Design',
+ id: -uniqueId(),
+ image: '',
+ imageV432x230: '',
+ filename: file.name,
+ fullPath: '',
+ notesCount: 0,
+ event: 'NONE',
+ diffRefs: {
+ __typename: 'DiffRefs',
+ baseSha: '',
+ startSha: '',
+ headSha: '',
+ },
+ discussions: {
+ __typename: 'DesignDiscussion',
+ nodes: [],
+ },
+ versions: {
+ __typename: 'DesignVersionConnection',
+ edges: {
+ __typename: 'DesignVersionEdge',
+ node: {
+ __typename: 'DesignVersion',
+ id: -uniqueId(),
+ sha: -uniqueId(),
+ },
+ },
+ },
+ }));
+
+ return {
+ // False positive i18n lint: https://gitlab.com/gitlab-org/frontend/eslint-plugin-i18n/issues/26
+ // eslint-disable-next-line @gitlab/require-i18n-strings
+ __typename: 'Mutation',
+ designManagementUpload: {
+ __typename: 'DesignManagementUploadPayload',
+ designs,
+ skippedDesigns: [],
+ errors: [],
+ },
+ };
+};
+
+/**
+ * Generates optimistic response for a design upload mutation
+ * @param {Array<File>} files
+ */
+export const updateImageDiffNoteOptimisticResponse = (note, { position }) => ({
+ // False positive i18n lint: https://gitlab.com/gitlab-org/frontend/eslint-plugin-i18n/issues/26
+ // eslint-disable-next-line @gitlab/require-i18n-strings
+ __typename: 'Mutation',
+ updateImageDiffNote: {
+ __typename: 'UpdateImageDiffNotePayload',
+ note: {
+ ...note,
+ position: {
+ ...note.position,
+ ...position,
+ },
+ },
+ errors: [],
+ },
+});
+
+const normalizeAuthor = author => ({
+ ...author,
+ web_url: author.webUrl,
+ avatar_url: author.avatarUrl,
+});
+
+export const extractParticipants = users => users.edges.map(({ node }) => normalizeAuthor(node));
diff --git a/app/assets/javascripts/design_management/utils/error_messages.js b/app/assets/javascripts/design_management/utils/error_messages.js
new file mode 100644
index 00000000000..7666c726c2f
--- /dev/null
+++ b/app/assets/javascripts/design_management/utils/error_messages.js
@@ -0,0 +1,95 @@
+import { __, s__, n__, sprintf } from '~/locale';
+
+export const ADD_DISCUSSION_COMMENT_ERROR = s__(
+ 'DesignManagement|Could not add a new comment. Please try again.',
+);
+
+export const ADD_IMAGE_DIFF_NOTE_ERROR = s__(
+ 'DesignManagement|Could not create new discussion. Please try again.',
+);
+
+export const UPDATE_IMAGE_DIFF_NOTE_ERROR = s__(
+ 'DesignManagement|Could not update discussion. Please try again.',
+);
+
+export const UPDATE_NOTE_ERROR = s__('DesignManagement|Could not update note. Please try again.');
+
+export const UPLOAD_DESIGN_ERROR = s__(
+ 'DesignManagement|Error uploading a new design. Please try again.',
+);
+
+export const UPLOAD_DESIGN_INVALID_FILETYPE_ERROR = __(
+ 'Could not upload your designs as one or more files uploaded are not supported.',
+);
+
+export const DESIGN_NOT_FOUND_ERROR = __('Could not find design.');
+
+export const DESIGN_VERSION_NOT_EXIST_ERROR = __('Requested design version does not exist.');
+
+const DESIGN_UPLOAD_SKIPPED_MESSAGE = s__('DesignManagement|Upload skipped.');
+
+const ALL_DESIGNS_SKIPPED_MESSAGE = `${DESIGN_UPLOAD_SKIPPED_MESSAGE} ${s__(
+ 'The designs you tried uploading did not change.',
+)}`;
+
+export const EXISTING_DESIGN_DROP_MANY_FILES_MESSAGE = __(
+ 'You can only upload one design when dropping onto an existing design.',
+);
+
+export const EXISTING_DESIGN_DROP_INVALID_FILENAME_MESSAGE = __(
+ 'You must upload a file with the same file name when dropping onto an existing design.',
+);
+
+const MAX_SKIPPED_FILES_LISTINGS = 5;
+
+const oneDesignSkippedMessage = filename =>
+ `${DESIGN_UPLOAD_SKIPPED_MESSAGE} ${sprintf(s__('DesignManagement|%{filename} did not change.'), {
+ filename,
+ })}`;
+
+/**
+ * Return warning message indicating that some (but not all) uploaded
+ * files were skipped.
+ * @param {Array<{ filename }>} skippedFiles
+ */
+const someDesignsSkippedMessage = skippedFiles => {
+ const designsSkippedMessage = `${DESIGN_UPLOAD_SKIPPED_MESSAGE} ${s__(
+ 'Some of the designs you tried uploading did not change:',
+ )}`;
+
+ const moreText = sprintf(s__(`DesignManagement|and %{moreCount} more.`), {
+ moreCount: skippedFiles.length - MAX_SKIPPED_FILES_LISTINGS,
+ });
+
+ return `${designsSkippedMessage} ${skippedFiles
+ .slice(0, MAX_SKIPPED_FILES_LISTINGS)
+ .map(({ filename }) => filename)
+ .join(', ')}${skippedFiles.length > MAX_SKIPPED_FILES_LISTINGS ? `, ${moreText}` : '.'}`;
+};
+
+export const designDeletionError = ({ singular = true } = {}) => {
+ const design = singular ? __('a design') : __('designs');
+ return sprintf(s__('Could not delete %{design}. Please try again.'), {
+ design,
+ });
+};
+
+/**
+ * Return warning message, if applicable, that one, some or all uploaded
+ * files were skipped.
+ * @param {Array<{ filename }>} uploadedDesigns
+ * @param {Array<{ filename }>} skippedFiles
+ */
+export const designUploadSkippedWarning = (uploadedDesigns, skippedFiles) => {
+ if (skippedFiles.length === 0) {
+ return null;
+ }
+
+ if (skippedFiles.length === uploadedDesigns.length) {
+ const { filename } = skippedFiles[0];
+
+ return n__(oneDesignSkippedMessage(filename), ALL_DESIGNS_SKIPPED_MESSAGE, skippedFiles.length);
+ }
+
+ return someDesignsSkippedMessage(skippedFiles);
+};
diff --git a/app/assets/javascripts/design_management/utils/tracking.js b/app/assets/javascripts/design_management/utils/tracking.js
new file mode 100644
index 00000000000..39c20376271
--- /dev/null
+++ b/app/assets/javascripts/design_management/utils/tracking.js
@@ -0,0 +1,28 @@
+import Tracking from '~/tracking';
+
+function assembleDesignPayload(payloadArr) {
+ return {
+ value: {
+ 'internal-object-refrerer': payloadArr[0],
+ 'design-collection-owner': payloadArr[1],
+ 'design-version-number': payloadArr[2],
+ 'design-is-current-version': payloadArr[3],
+ },
+ };
+}
+
+// Tracking Constants
+const DESIGN_TRACKING_PAGE_NAME = 'projects:issues:design';
+
+// eslint-disable-next-line import/prefer-default-export
+export function trackDesignDetailView(
+ referer = '',
+ owner = '',
+ designVersion = 1,
+ latestVersion = false,
+) {
+ Tracking.event(DESIGN_TRACKING_PAGE_NAME, 'design_viewed', {
+ label: 'design_viewed',
+ ...assembleDesignPayload([referer, owner, designVersion, latestVersion]),
+ });
+}
diff --git a/app/assets/javascripts/diff_notes/components/comment_resolve_btn.js b/app/assets/javascripts/diff_notes/components/comment_resolve_btn.js
index 84e07598fed..dd60e2c7684 100644
--- a/app/assets/javascripts/diff_notes/components/comment_resolve_btn.js
+++ b/app/assets/javascripts/diff_notes/components/comment_resolve_btn.js
@@ -1,4 +1,3 @@
-/* eslint-disable no-else-return, no-lonely-if */
/* global CommentsStore */
import $ from 'jquery';
@@ -22,27 +21,19 @@ const CommentAndResolveBtn = Vue.extend({
showButton() {
if (this.discussion) {
return this.discussion.isResolvable();
- } else {
- return false;
}
+ return false;
},
isDiscussionResolved() {
return this.discussion.isResolved();
},
buttonText() {
- if (this.isDiscussionResolved) {
- if (this.textareaIsEmpty) {
- return __('Unresolve thread');
- } else {
- return __('Comment & unresolve thread');
- }
- } else {
- if (this.textareaIsEmpty) {
- return __('Resolve thread');
- } else {
- return __('Comment & resolve thread');
- }
+ if (this.textareaIsEmpty) {
+ return this.isDiscussionResolved ? __('Unresolve thread') : __('Resolve thread');
}
+ return this.isDiscussionResolved
+ ? __('Comment & unresolve thread')
+ : __('Comment & resolve thread');
},
},
created() {
diff --git a/app/assets/javascripts/diff_notes/components/jump_to_discussion.js b/app/assets/javascripts/diff_notes/components/jump_to_discussion.js
index fa5f8ea4005..0c521fa29bd 100644
--- a/app/assets/javascripts/diff_notes/components/jump_to_discussion.js
+++ b/app/assets/javascripts/diff_notes/components/jump_to_discussion.js
@@ -1,4 +1,4 @@
-/* eslint-disable func-names, no-else-return, guard-for-in, no-restricted-syntax, no-lonely-if, no-continue */
+/* eslint-disable func-names, guard-for-in, no-restricted-syntax, no-lonely-if, no-continue */
/* global CommentsStore */
import $ from 'jquery';
@@ -25,9 +25,8 @@ const JumpToDiscussion = Vue.extend({
buttonText() {
if (this.discussionId) {
return __('Jump to next unresolved thread');
- } else {
- return __('Jump to first unresolved thread');
}
+ return __('Jump to first unresolved thread');
},
allResolved() {
return this.unresolvedDiscussionCount === 0;
@@ -36,12 +35,10 @@ const JumpToDiscussion = Vue.extend({
if (this.discussionId) {
if (this.unresolvedDiscussionCount > 1) {
return true;
- } else {
- return this.discussionId !== this.lastResolvedId;
}
- } else {
- return this.unresolvedDiscussionCount >= 1;
+ return this.discussionId !== this.lastResolvedId;
}
+ return this.unresolvedDiscussionCount >= 1;
},
lastResolvedId() {
let lastId;
diff --git a/app/assets/javascripts/diffs/components/app.vue b/app/assets/javascripts/diffs/components/app.vue
index 072bcaaad97..941365d9d1d 100644
--- a/app/assets/javascripts/diffs/components/app.vue
+++ b/app/assets/javascripts/diffs/components/app.vue
@@ -7,6 +7,7 @@ import createFlash from '~/flash';
import PanelResizer from '~/vue_shared/components/panel_resizer.vue';
import glFeatureFlagsMixin from '~/vue_shared/mixins/gl_feature_flags_mixin';
import { isSingleViewStyle } from '~/helpers/diffs_helper';
+import { updateHistory } from '~/lib/utils/url_utility';
import eventHub from '../../notes/event_hub';
import CompareVersions from './compare_versions.vue';
import DiffFile from './diff_file.vue';
@@ -140,6 +141,20 @@ export default {
},
},
watch: {
+ commit(newCommit, oldCommit) {
+ const commitChangedAfterRender = newCommit && !this.isLoading;
+ const commitIsDifferent = oldCommit && newCommit.id !== oldCommit.id;
+ const url = window?.location ? String(window.location) : '';
+
+ if (commitChangedAfterRender && commitIsDifferent) {
+ updateHistory({
+ title: document.title,
+ url: url.replace(oldCommit.id, newCommit.id),
+ });
+ this.refetchDiffData();
+ this.adjustView();
+ }
+ },
diffViewType() {
if (this.needsReload() || this.needsFirstLoad()) {
this.refetchDiffData();
@@ -209,6 +224,7 @@ export default {
methods: {
...mapActions(['startTaskList']),
...mapActions('diffs', [
+ 'moveToNeighboringCommit',
'setBaseConfig',
'fetchDiffFiles',
'fetchDiffFilesMeta',
@@ -329,9 +345,16 @@ export default {
break;
}
});
+
+ if (this.commit && this.glFeatures.mrCommitNeighborNav) {
+ Mousetrap.bind('c', () => this.moveToNeighboringCommit({ direction: 'next' }));
+ Mousetrap.bind('x', () => this.moveToNeighboringCommit({ direction: 'previous' }));
+ }
},
removeEventListeners() {
Mousetrap.unbind(['[', 'k', ']', 'j']);
+ Mousetrap.unbind('c');
+ Mousetrap.unbind('x');
},
jumpToFile(step) {
const targetIndex = this.currentDiffIndex + step;
diff --git a/app/assets/javascripts/diffs/components/commit_item.vue b/app/assets/javascripts/diffs/components/commit_item.vue
index 9d4edd84f25..ee93ca020e8 100644
--- a/app/assets/javascripts/diffs/components/commit_item.vue
+++ b/app/assets/javascripts/diffs/components/commit_item.vue
@@ -1,10 +1,18 @@
<script>
+import { mapActions } from 'vuex';
+import { GlButtonGroup, GlButton, GlIcon, GlTooltipDirective } from '@gitlab/ui';
+
+import glFeatureFlagsMixin from '~/vue_shared/mixins/gl_feature_flags_mixin';
+
import UserAvatarLink from '~/vue_shared/components/user_avatar/user_avatar_link.vue';
import Icon from '~/vue_shared/components/icon.vue';
import ClipboardButton from '~/vue_shared/components/clipboard_button.vue';
import TimeAgoTooltip from '~/vue_shared/components/time_ago_tooltip.vue';
+
import CommitPipelineStatus from '~/projects/tree/components/commit_pipeline_status_component.vue';
+
import initUserPopovers from '../../user_popovers';
+import { setUrlParams } from '../../lib/utils/url_utility';
/**
* CommitItem
@@ -18,7 +26,16 @@ import initUserPopovers from '../../user_popovers';
* coexist, but there is an issue to remove the duplication.
* https://gitlab.com/gitlab-org/gitlab-foss/issues/51613
*
+ * EXCEPTION WARNING
+ * 1. The commit navigation buttons (next neighbor, previous neighbor)
+ * are not duplicated because:
+ * - We don't have the same data available on the Rails side (yet,
+ * without backend work)
+ * - This Vue component should always be what's used when in the
+ * context of an MR diff, so the HAML should never have any idea
+ * about navigating among commits.
*/
+
export default {
components: {
UserAvatarLink,
@@ -26,7 +43,14 @@ export default {
ClipboardButton,
TimeAgoTooltip,
CommitPipelineStatus,
+ GlButtonGroup,
+ GlButton,
+ GlIcon,
+ },
+ directives: {
+ GlTooltip: GlTooltipDirective,
},
+ mixins: [glFeatureFlagsMixin()],
props: {
commit: {
type: Object,
@@ -54,12 +78,28 @@ export default {
authorAvatar() {
return this.author.avatar_url || this.commit.author_gravatar_url;
},
+ nextCommitUrl() {
+ return this.commit.next_commit_id
+ ? setUrlParams({ commit_id: this.commit.next_commit_id })
+ : '';
+ },
+ previousCommitUrl() {
+ return this.commit.prev_commit_id
+ ? setUrlParams({ commit_id: this.commit.prev_commit_id })
+ : '';
+ },
+ hasNeighborCommits() {
+ return this.commit.next_commit_id || this.commit.prev_commit_id;
+ },
},
created() {
this.$nextTick(() => {
initUserPopovers(this.$el.querySelectorAll('.js-user-link'));
});
},
+ methods: {
+ ...mapActions('diffs', ['moveToNeighboringCommit']),
+ },
};
</script>
@@ -123,6 +163,41 @@ export default {
class="btn btn-default"
/>
</div>
+ <div
+ v-if="hasNeighborCommits && glFeatures.mrCommitNeighborNav"
+ class="commit-nav-buttons ml-3"
+ >
+ <gl-button-group>
+ <gl-button
+ :href="previousCommitUrl"
+ :disabled="!commit.prev_commit_id"
+ @click.prevent="moveToNeighboringCommit({ direction: 'previous' })"
+ >
+ <span
+ v-if="!commit.prev_commit_id"
+ v-gl-tooltip
+ class="h-100 w-100 position-absolute"
+ :title="__('You\'re at the first commit')"
+ ></span>
+ <gl-icon name="chevron-left" />
+ {{ __('Prev') }}
+ </gl-button>
+ <gl-button
+ :href="nextCommitUrl"
+ :disabled="!commit.next_commit_id"
+ @click.prevent="moveToNeighboringCommit({ direction: 'next' })"
+ >
+ <span
+ v-if="!commit.next_commit_id"
+ v-gl-tooltip
+ class="h-100 w-100 position-absolute"
+ :title="__('You\'re at the last commit')"
+ ></span>
+ {{ __('Next') }}
+ <gl-icon name="chevron-right" />
+ </gl-button>
+ </gl-button-group>
+ </div>
</div>
</div>
</li>
diff --git a/app/assets/javascripts/diffs/components/diff_discussions.vue b/app/assets/javascripts/diffs/components/diff_discussions.vue
index b0460bacff2..b6a0724c201 100644
--- a/app/assets/javascripts/diffs/components/diff_discussions.vue
+++ b/app/assets/javascripts/diffs/components/diff_discussions.vue
@@ -85,9 +85,11 @@ export default {
:help-page-path="helpPagePath"
@noteDeleted="deleteNoteHandler"
>
- <span v-if="renderAvatarBadge" slot="avatar-badge" class="badge badge-pill">
- {{ index + 1 }}
- </span>
+ <template v-if="renderAvatarBadge" #avatar-badge>
+ <span class="badge badge-pill">
+ {{ index + 1 }}
+ </span>
+ </template>
</noteable-discussion>
</ul>
</div>
diff --git a/app/assets/javascripts/diffs/components/diff_file.vue b/app/assets/javascripts/diffs/components/diff_file.vue
index 82ca3749ac1..54852b113ae 100644
--- a/app/assets/javascripts/diffs/components/diff_file.vue
+++ b/app/assets/javascripts/diffs/components/diff_file.vue
@@ -1,6 +1,6 @@
<script>
import { mapActions, mapGetters, mapState } from 'vuex';
-import { escape as esc } from 'lodash';
+import { escape } from 'lodash';
import { GlLoadingIcon } from '@gitlab/ui';
import { __, sprintf } from '~/locale';
import createFlash from '~/flash';
@@ -46,7 +46,7 @@ export default {
return sprintf(
__('You can %{linkStart}view the blob%{linkEnd} instead.'),
{
- linkStart: `<a href="${esc(this.file.view_path)}">`,
+ linkStart: `<a href="${escape(this.file.view_path)}">`,
linkEnd: '</a>',
},
false,
diff --git a/app/assets/javascripts/diffs/components/diff_file_header.vue b/app/assets/javascripts/diffs/components/diff_file_header.vue
index d601c3769a3..61bbf13aa53 100644
--- a/app/assets/javascripts/diffs/components/diff_file_header.vue
+++ b/app/assets/javascripts/diffs/components/diff_file_header.vue
@@ -1,5 +1,5 @@
<script>
-import { escape as esc } from 'lodash';
+import { escape } from 'lodash';
import { mapActions, mapGetters } from 'vuex';
import { GlDeprecatedButton, GlTooltipDirective, GlLoadingIcon } from '@gitlab/ui';
import { polyfillSticky } from '~/lib/utils/sticky';
@@ -91,7 +91,7 @@ export default {
return this.expanded ? 'chevron-down' : 'chevron-right';
},
viewFileButtonText() {
- const truncatedContentSha = esc(truncateSha(this.diffFile.content_sha));
+ const truncatedContentSha = escape(truncateSha(this.diffFile.content_sha));
return sprintf(
s__('MergeRequests|View file @ %{commitId}'),
{ commitId: truncatedContentSha },
@@ -99,7 +99,7 @@ export default {
);
},
viewReplacedFileButtonText() {
- const truncatedBaseSha = esc(truncateSha(this.diffFile.diff_refs.base_sha));
+ const truncatedBaseSha = escape(truncateSha(this.diffFile.diff_refs.base_sha));
return sprintf(
s__('MergeRequests|View replaced file @ %{commitId}'),
{
diff --git a/app/assets/javascripts/diffs/components/edit_button.vue b/app/assets/javascripts/diffs/components/edit_button.vue
index 91e296f8572..21fdb19287d 100644
--- a/app/assets/javascripts/diffs/components/edit_button.vue
+++ b/app/assets/javascripts/diffs/components/edit_button.vue
@@ -1,5 +1,6 @@
<script>
import { GlTooltipDirective, GlDeprecatedButton } from '@gitlab/ui';
+import { __ } from '~/locale';
import Icon from '~/vue_shared/components/icon.vue';
export default {
@@ -13,7 +14,8 @@ export default {
props: {
editPath: {
type: String,
- required: true,
+ required: false,
+ default: '',
},
canCurrentUserFork: {
type: Boolean,
@@ -25,6 +27,18 @@ export default {
default: false,
},
},
+ computed: {
+ tooltipTitle() {
+ if (this.isDisabled) {
+ return __("Can't edit as source branch was deleted");
+ }
+
+ return __('Edit file');
+ },
+ isDisabled() {
+ return !this.editPath;
+ },
+ },
methods: {
handleEditClick(evt) {
if (this.canCurrentUserFork && !this.canModifyBlob) {
@@ -37,13 +51,15 @@ export default {
</script>
<template>
- <gl-deprecated-button
- v-gl-tooltip.top
- :href="editPath"
- :title="__('Edit file')"
- class="js-edit-blob"
- @click.native="handleEditClick"
- >
- <icon name="pencil" />
- </gl-deprecated-button>
+ <span v-gl-tooltip.top :title="tooltipTitle">
+ <gl-deprecated-button
+ :href="editPath"
+ :disabled="isDisabled"
+ :class="{ 'cursor-not-allowed': isDisabled }"
+ class="rounded-0 js-edit-blob"
+ @click.native="handleEditClick"
+ >
+ <icon name="pencil" />
+ </gl-deprecated-button>
+ </span>
</template>
diff --git a/app/assets/javascripts/diffs/components/no_changes.vue b/app/assets/javascripts/diffs/components/no_changes.vue
index 5fd68471094..94c2695a945 100644
--- a/app/assets/javascripts/diffs/components/no_changes.vue
+++ b/app/assets/javascripts/diffs/components/no_changes.vue
@@ -1,6 +1,6 @@
<script>
import { mapGetters } from 'vuex';
-import { escape as esc } from 'lodash';
+import { escape } from 'lodash';
import { GlDeprecatedButton } from '@gitlab/ui';
import { __, sprintf } from '~/locale';
@@ -24,8 +24,8 @@ export default {
{
ref_start: '<span class="ref-name">',
ref_end: '</span>',
- source_branch: esc(this.getNoteableData.source_branch),
- target_branch: esc(this.getNoteableData.target_branch),
+ source_branch: escape(this.getNoteableData.source_branch),
+ target_branch: escape(this.getNoteableData.target_branch),
},
false,
);
diff --git a/app/assets/javascripts/diffs/store/actions.js b/app/assets/javascripts/diffs/store/actions.js
index 93c242e32ac..1975d6996a5 100644
--- a/app/assets/javascripts/diffs/store/actions.js
+++ b/app/assets/javascripts/diffs/store/actions.js
@@ -16,6 +16,7 @@ import {
idleCallback,
allDiscussionWrappersExpanded,
prepareDiffData,
+ prepareLineForRenamedFile,
} from './utils';
import * as types from './mutation_types';
import {
@@ -627,6 +628,42 @@ export const toggleFullDiff = ({ dispatch, getters, state }, filePath) => {
}
};
+export function switchToFullDiffFromRenamedFile({ commit, dispatch, state }, { diffFile }) {
+ return axios
+ .get(diffFile.context_lines_path, {
+ params: {
+ full: true,
+ from_merge_request: true,
+ },
+ })
+ .then(({ data }) => {
+ const lines = data.map((line, index) =>
+ prepareLineForRenamedFile({
+ diffViewType: state.diffViewType,
+ line,
+ diffFile,
+ index,
+ }),
+ );
+
+ commit(types.SET_DIFF_FILE_VIEWER, {
+ filePath: diffFile.file_path,
+ viewer: {
+ ...diffFile.alternate_viewer,
+ collapsed: false,
+ },
+ });
+ commit(types.SET_CURRENT_VIEW_DIFF_FILE_LINES, { filePath: diffFile.file_path, lines });
+
+ dispatch('startRenderDiffsQueue');
+ })
+ .catch(error => {
+ dispatch('receiveFullDiffError', diffFile.file_path);
+
+ throw error;
+ });
+}
+
export const setFileCollapsed = ({ commit }, { filePath, collapsed }) =>
commit(types.SET_FILE_COLLAPSED, { filePath, collapsed });
@@ -642,5 +679,48 @@ export const setSuggestPopoverDismissed = ({ commit, state }) =>
createFlash(s__('MergeRequest|Error dismissing suggestion popover. Please try again.'));
});
+export function changeCurrentCommit({ dispatch, commit, state }, { commitId }) {
+ /* eslint-disable @gitlab/require-i18n-strings */
+ if (!commitId) {
+ return Promise.reject(new Error('`commitId` is a required argument'));
+ } else if (!state.commit) {
+ return Promise.reject(new Error('`state` must already contain a valid `commit`'));
+ }
+ /* eslint-enable @gitlab/require-i18n-strings */
+
+ // this is less than ideal, see: https://gitlab.com/gitlab-org/gitlab/-/issues/215421
+ const commitRE = new RegExp(state.commit.id, 'g');
+
+ commit(types.SET_DIFF_FILES, []);
+ commit(types.SET_BASE_CONFIG, {
+ ...state,
+ endpoint: state.endpoint.replace(commitRE, commitId),
+ endpointBatch: state.endpointBatch.replace(commitRE, commitId),
+ endpointMetadata: state.endpointMetadata.replace(commitRE, commitId),
+ });
+
+ return dispatch('fetchDiffFilesMeta');
+}
+
+export function moveToNeighboringCommit({ dispatch, state }, { direction }) {
+ const previousCommitId = state.commit?.prev_commit_id;
+ const nextCommitId = state.commit?.next_commit_id;
+ const canMove = {
+ next: !state.isLoading && nextCommitId,
+ previous: !state.isLoading && previousCommitId,
+ };
+ let commitId;
+
+ if (direction === 'next' && canMove.next) {
+ commitId = nextCommitId;
+ } else if (direction === 'previous' && canMove.previous) {
+ commitId = previousCommitId;
+ }
+
+ if (commitId) {
+ dispatch('changeCurrentCommit', { commitId });
+ }
+}
+
// prevent babel-plugin-rewire from generating an invalid default during karma tests
export default () => {};
diff --git a/app/assets/javascripts/diffs/store/getters_versions_dropdowns.js b/app/assets/javascripts/diffs/store/getters_versions_dropdowns.js
index acc8874dad8..1e8e736c028 100644
--- a/app/assets/javascripts/diffs/store/getters_versions_dropdowns.js
+++ b/app/assets/javascripts/diffs/store/getters_versions_dropdowns.js
@@ -40,10 +40,7 @@ export const diffCompareDropdownTargetVersions = (state, getters) => {
};
};
- if (gon.features?.diffCompareWithHead) {
- return [...state.mergeRequestDiffs.slice(1).map(formatVersion), baseVersion, headVersion];
- }
- return [...state.mergeRequestDiffs.slice(1).map(formatVersion), baseVersion];
+ return [...state.mergeRequestDiffs.slice(1).map(formatVersion), baseVersion, headVersion];
};
export const diffCompareDropdownSourceVersions = (state, getters) => {
diff --git a/app/assets/javascripts/diffs/store/mutation_types.js b/app/assets/javascripts/diffs/store/mutation_types.js
index 699c61b3ddd..4b1dbc34902 100644
--- a/app/assets/javascripts/diffs/store/mutation_types.js
+++ b/app/assets/javascripts/diffs/store/mutation_types.js
@@ -41,6 +41,8 @@ export const SET_CURRENT_VIEW_DIFF_FILE_LINES = 'SET_CURRENT_VIEW_DIFF_FILE_LINE
export const ADD_CURRENT_VIEW_DIFF_FILE_LINES = 'ADD_CURRENT_VIEW_DIFF_FILE_LINES';
export const TOGGLE_DIFF_FILE_RENDERING_MORE = 'TOGGLE_DIFF_FILE_RENDERING_MORE';
+export const SET_DIFF_FILE_VIEWER = 'SET_DIFF_FILE_VIEWER';
+
export const SET_SHOW_SUGGEST_POPOVER = 'SET_SHOW_SUGGEST_POPOVER';
export const TOGGLE_LINE_DISCUSSIONS = 'TOGGLE_LINE_DISCUSSIONS';
diff --git a/app/assets/javascripts/diffs/store/mutations.js b/app/assets/javascripts/diffs/store/mutations.js
index 104686993a8..7e89d041c21 100644
--- a/app/assets/javascripts/diffs/store/mutations.js
+++ b/app/assets/javascripts/diffs/store/mutations.js
@@ -383,6 +383,11 @@ export default {
file.renderingLines = !file.renderingLines;
},
+ [types.SET_DIFF_FILE_VIEWER](state, { filePath, viewer }) {
+ const file = findDiffFile(state.diffFiles, filePath, 'file_path');
+
+ file.viewer = viewer;
+ },
[types.SET_SHOW_SUGGEST_POPOVER](state) {
state.showSuggestPopover = false;
},
diff --git a/app/assets/javascripts/diffs/store/utils.js b/app/assets/javascripts/diffs/store/utils.js
index dd8dec49a37..2be71c77087 100644
--- a/app/assets/javascripts/diffs/store/utils.js
+++ b/app/assets/javascripts/diffs/store/utils.js
@@ -233,7 +233,7 @@ export function trimFirstCharOfLineContent(line = {}) {
// eslint-disable-next-line no-param-reassign
delete line.text;
- const parsedLine = Object.assign({}, line);
+ const parsedLine = { ...line };
if (line.rich_text) {
const firstChar = parsedLine.rich_text.charAt(0);
@@ -303,6 +303,42 @@ function prepareLine(line) {
}
}
+export function prepareLineForRenamedFile({ line, diffViewType, diffFile, index = 0 }) {
+ /*
+ Renamed files are a little different than other diffs, which
+ is why this is distinct from `prepareDiffFileLines` below.
+
+ We don't get any of the diff file context when we get the diff
+ (so no "inline" vs. "parallel", no "line_code", etc.).
+
+ We can also assume that both the left and the right of each line
+ (for parallel diff view type) are identical, because the file
+ is renamed, not modified.
+
+ This should be cleaned up as part of the effort around flattening our data
+ ==> https://gitlab.com/groups/gitlab-org/-/epics/2852#note_304803402
+ */
+ const lineNumber = index + 1;
+ const cleanLine = {
+ ...line,
+ line_code: `${diffFile.file_hash}_${lineNumber}_${lineNumber}`,
+ new_line: lineNumber,
+ old_line: lineNumber,
+ };
+
+ prepareLine(cleanLine); // WARNING: In-Place Mutations!
+
+ if (diffViewType === PARALLEL_DIFF_VIEW_TYPE) {
+ return {
+ left: { ...cleanLine },
+ right: { ...cleanLine },
+ line_code: cleanLine.line_code,
+ };
+ }
+
+ return cleanLine;
+}
+
function prepareDiffFileLines(file) {
const inlineLines = file.highlighted_diff_lines;
const parallelLines = file.parallel_diff_lines;
@@ -437,7 +473,11 @@ export function getDiffPositionByLineCode(diffFiles, useSingleDiffStyle) {
// This method will check whether the discussion is still applicable
// to the diff line in question regarding different versions of the MR
export function isDiscussionApplicableToLine({ discussion, diffPosition, latestDiff }) {
- const { line_code, ...diffPositionCopy } = diffPosition;
+ const { line_code, ...dp } = diffPosition;
+ // Removing `line_range` from diffPosition because the backend does not
+ // yet consistently return this property. This check can be removed,
+ // once this is addressed. see https://gitlab.com/gitlab-org/gitlab/-/issues/213010
+ const { line_range: dpNotUsed, ...diffPositionCopy } = dp;
if (discussion.original_position && discussion.position) {
const discussionPositions = [
@@ -446,7 +486,14 @@ export function isDiscussionApplicableToLine({ discussion, diffPosition, latestD
...(discussion.positions || []),
];
- return discussionPositions.some(position => isEqual(position, diffPositionCopy));
+ const removeLineRange = position => {
+ const { line_range: pNotUsed, ...positionNoLineRange } = position;
+ return positionNoLineRange;
+ };
+
+ return discussionPositions
+ .map(removeLineRange)
+ .some(position => isEqual(position, diffPositionCopy));
}
// eslint-disable-next-line
diff --git a/app/assets/javascripts/dropzone_input.js b/app/assets/javascripts/dropzone_input.js
index f839e9acf04..9a0b85bd610 100644
--- a/app/assets/javascripts/dropzone_input.js
+++ b/app/assets/javascripts/dropzone_input.js
@@ -1,6 +1,6 @@
import $ from 'jquery';
import Dropzone from 'dropzone';
-import _ from 'underscore';
+import { escape } from 'lodash';
import './behaviors/preview_markdown';
import PasteMarkdownTable from './behaviors/markdown/paste_markdown_table';
import csrf from './lib/utils/csrf';
@@ -16,7 +16,7 @@ Dropzone.autoDiscover = false;
* @param {String|Object} res
*/
function getErrorMessage(res) {
- if (!res || _.isString(res)) {
+ if (!res || typeof res === 'string') {
return res;
}
@@ -233,7 +233,7 @@ export default function dropzoneInput(form, config = { parallelUploads: 2 }) {
};
addFileToForm = path => {
- $(form).append(`<input type="hidden" name="files[]" value="${_.escape(path)}">`);
+ $(form).append(`<input type="hidden" name="files[]" value="${escape(path)}">`);
};
const showSpinner = () => $uploadingProgressContainer.removeClass('hide');
diff --git a/app/assets/javascripts/editor/editor_lite.js b/app/assets/javascripts/editor/editor_lite.js
index 663d14bcfcb..020ed6dc867 100644
--- a/app/assets/javascripts/editor/editor_lite.js
+++ b/app/assets/javascripts/editor/editor_lite.js
@@ -1,6 +1,8 @@
import { editor as monacoEditor, languages as monacoLanguages, Uri } from 'monaco-editor';
import { DEFAULT_THEME, themes } from '~/ide/lib/themes';
+import languages from '~/ide/lib/languages';
import { defaultEditorOptions } from '~/ide/lib/editor_options';
+import { registerLanguages } from '~/ide/utils';
import { clearDomElement } from './utils';
export default class Editor {
@@ -17,6 +19,8 @@ export default class Editor {
};
Editor.setupMonacoTheme();
+
+ registerLanguages(...languages);
}
static setupMonacoTheme() {
diff --git a/app/assets/javascripts/environments/components/confirm_rollback_modal.vue b/app/assets/javascripts/environments/components/confirm_rollback_modal.vue
index 5c03c008faf..f0723e96ddf 100644
--- a/app/assets/javascripts/environments/components/confirm_rollback_modal.vue
+++ b/app/assets/javascripts/environments/components/confirm_rollback_modal.vue
@@ -3,7 +3,7 @@
* Render modal to confirm rollback/redeploy.
*/
-import { escape as esc } from 'lodash';
+import { escape } from 'lodash';
import { GlModal } from '@gitlab/ui';
import { s__, sprintf } from '~/locale';
@@ -30,7 +30,7 @@ export default {
: s__('Environments|Rollback environment %{name}?');
return sprintf(title, {
- name: esc(this.environment.name),
+ name: escape(this.environment.name),
});
},
@@ -50,10 +50,10 @@ export default {
},
modalText() {
- const linkStart = `<a class="commit-sha mr-0" href="${esc(this.commitUrl)}">`;
- const commitId = esc(this.commitShortSha);
+ const linkStart = `<a class="commit-sha mr-0" href="${escape(this.commitUrl)}">`;
+ const commitId = escape(this.commitShortSha);
const linkEnd = '</a>';
- const name = esc(this.name);
+ const name = escape(this.name);
const body = this.environment.isLastDeployment
? s__(
'Environments|This action will relaunch the job for commit %{linkStart}%{commitId}%{linkEnd}, putting the environment in a previous version. Are you sure you want to continue?',
diff --git a/app/assets/javascripts/environments/components/container.vue b/app/assets/javascripts/environments/components/container.vue
index 0a978ab5869..899d7ec8521 100644
--- a/app/assets/javascripts/environments/components/container.vue
+++ b/app/assets/javascripts/environments/components/container.vue
@@ -1,8 +1,7 @@
<script>
import { GlLoadingIcon } from '@gitlab/ui';
-import containerMixin from 'ee_else_ce/environments/mixins/container_mixin';
import TablePagination from '~/vue_shared/components/pagination/table_pagination.vue';
-import EnvironmentTable from '../components/environments_table.vue';
+import EnvironmentTable from './environments_table.vue';
export default {
components: {
@@ -10,8 +9,12 @@ export default {
TablePagination,
GlLoadingIcon,
},
- mixins: [containerMixin],
props: {
+ canaryDeploymentFeatureId: {
+ type: String,
+ required: false,
+ default: null,
+ },
isLoading: {
type: Boolean,
required: true,
@@ -28,6 +31,31 @@ export default {
type: Boolean,
required: true,
},
+ deployBoardsHelpPath: {
+ type: String,
+ required: false,
+ default: '',
+ },
+ helpCanaryDeploymentsPath: {
+ type: String,
+ required: false,
+ default: '',
+ },
+ lockPromotionSvgPath: {
+ type: String,
+ required: false,
+ default: '',
+ },
+ showCanaryDeploymentCallout: {
+ type: Boolean,
+ required: false,
+ default: false,
+ },
+ userCalloutsPath: {
+ type: String,
+ required: false,
+ default: '',
+ },
},
methods: {
onChangePage(page) {
diff --git a/app/assets/javascripts/environments/components/delete_environment_modal.vue b/app/assets/javascripts/environments/components/delete_environment_modal.vue
index f731dc49a5b..29aab268fd3 100644
--- a/app/assets/javascripts/environments/components/delete_environment_modal.vue
+++ b/app/assets/javascripts/environments/components/delete_environment_modal.vue
@@ -52,7 +52,7 @@ export default {
footer-primary-button-variant="danger"
@submit="onSubmit"
>
- <template slot="header">
+ <template #header>
<h4 class="modal-title d-flex mw-100">
{{ __('Delete') }}
<span v-gl-tooltip :title="environment.name" class="text-truncate mx-1 flex-fill">
diff --git a/app/assets/javascripts/environments/components/environment_item.vue b/app/assets/javascripts/environments/components/environment_item.vue
index 335c668474e..fa3d217f148 100644
--- a/app/assets/javascripts/environments/components/environment_item.vue
+++ b/app/assets/javascripts/environments/components/environment_item.vue
@@ -9,7 +9,6 @@ import UserAvatarLink from '~/vue_shared/components/user_avatar/user_avatar_link
import CommitComponent from '~/vue_shared/components/commit.vue';
import Icon from '~/vue_shared/components/icon.vue';
import TooltipOnTruncate from '~/vue_shared/components/tooltip_on_truncate.vue';
-import environmentItemMixin from 'ee_else_ce/environments/mixins/environment_item_mixin';
import eventHub from '../event_hub';
import ActionsComponent from './environment_actions.vue';
import ExternalUrlComponent from './environment_external_url.vue';
@@ -44,7 +43,7 @@ export default {
directives: {
GlTooltip: GlTooltipDirective,
},
- mixins: [environmentItemMixin, timeagoMixin],
+ mixins: [timeagoMixin],
props: {
canReadEnvironment: {
@@ -65,6 +64,9 @@ export default {
},
computed: {
+ deployIconName() {
+ return this.model.isDeployBoardVisible ? 'chevron-down' : 'chevron-right';
+ },
/**
* Verifies if `last_deployment` key exists in the current Environment.
* This key is required to render most of the html - this method works has
@@ -210,6 +212,10 @@ export default {
}));
},
+ shouldRenderDeployBoard() {
+ return this.model.hasDeployBoard;
+ },
+
/**
* Builds the string used in the user image alt attribute.
*
@@ -501,6 +507,9 @@ export default {
},
methods: {
+ toggleDeployBoard() {
+ eventHub.$emit('toggleDeployBoard', this.model);
+ },
onClickFolder() {
eventHub.$emit('toggleFolder', this.model);
},
diff --git a/app/assets/javascripts/environments/components/environments_app.vue b/app/assets/javascripts/environments/components/environments_app.vue
index 0cc6f3df2d7..0a5538237f9 100644
--- a/app/assets/javascripts/environments/components/environments_app.vue
+++ b/app/assets/javascripts/environments/components/environments_app.vue
@@ -1,6 +1,5 @@
<script>
import { GlDeprecatedButton } from '@gitlab/ui';
-import envrionmentsAppMixin from 'ee_else_ce/environments/mixins/environments_app_mixin';
import Flash from '~/flash';
import { s__ } from '~/locale';
import emptyState from './empty_state.vue';
@@ -22,13 +21,18 @@ export default {
DeleteEnvironmentModal,
},
- mixins: [CIPaginationMixin, environmentsMixin, envrionmentsAppMixin],
+ mixins: [CIPaginationMixin, environmentsMixin],
props: {
endpoint: {
type: String,
required: true,
},
+ canaryDeploymentFeatureId: {
+ type: String,
+ required: false,
+ default: '',
+ },
canCreateEnvironment: {
type: Boolean,
required: true,
@@ -41,6 +45,11 @@ export default {
type: String,
required: true,
},
+ helpCanaryDeploymentsPath: {
+ type: String,
+ required: false,
+ default: '',
+ },
helpPagePath: {
type: String,
required: true,
@@ -50,17 +59,37 @@ export default {
required: false,
default: '',
},
+ lockPromotionSvgPath: {
+ type: String,
+ required: false,
+ default: '',
+ },
+ showCanaryDeploymentCallout: {
+ type: Boolean,
+ required: false,
+ default: false,
+ },
+ userCalloutsPath: {
+ type: String,
+ required: false,
+ default: '',
+ },
},
created() {
eventHub.$on('toggleFolder', this.toggleFolder);
+ eventHub.$on('toggleDeployBoard', this.toggleDeployBoard);
},
beforeDestroy() {
eventHub.$off('toggleFolder');
+ eventHub.$off('toggleDeployBoard');
},
methods: {
+ toggleDeployBoard(model) {
+ this.store.toggleDeployBoard(model.id);
+ },
toggleFolder(folder) {
this.store.toggleFolder(folder);
diff --git a/app/assets/javascripts/environments/components/environments_table.vue b/app/assets/javascripts/environments/components/environments_table.vue
index 89e40faa23e..380e16c7b71 100644
--- a/app/assets/javascripts/environments/components/environments_table.vue
+++ b/app/assets/javascripts/environments/components/environments_table.vue
@@ -4,7 +4,6 @@
*/
import { GlLoadingIcon } from '@gitlab/ui';
import { flow, reverse, sortBy } from 'lodash/fp';
-import environmentTableMixin from 'ee_else_ce/environments/mixins/environments_table_mixin';
import { s__ } from '~/locale';
import EnvironmentItem from './environment_item.vue';
@@ -16,7 +15,6 @@ export default {
CanaryDeploymentCallout: () =>
import('ee_component/environments/components/canary_deployment_callout.vue'),
},
- mixins: [environmentTableMixin],
props: {
environments: {
type: Array,
@@ -33,6 +31,31 @@ export default {
required: false,
default: false,
},
+ canaryDeploymentFeatureId: {
+ type: String,
+ required: false,
+ default: '',
+ },
+ helpCanaryDeploymentsPath: {
+ type: String,
+ required: false,
+ default: '',
+ },
+ lockPromotionSvgPath: {
+ type: String,
+ required: false,
+ default: '',
+ },
+ showCanaryDeploymentCallout: {
+ type: Boolean,
+ required: false,
+ default: false,
+ },
+ userCalloutsPath: {
+ type: String,
+ required: false,
+ default: '',
+ },
},
computed: {
sortedEnvironments() {
@@ -79,9 +102,15 @@ export default {
folderUrl(model) {
return `${window.location.pathname}/folders/${model.folderName}`;
},
+ shouldRenderDeployBoard(model) {
+ return model.hasDeployBoard && model.isDeployBoardVisible;
+ },
shouldRenderFolderContent(env) {
return env.isFolder && env.isOpen && env.children && env.children.length > 0;
},
+ shouldShowCanaryCallout(env) {
+ return env.showCanaryCallout && this.showCanaryDeploymentCallout;
+ },
sortEnvironments(environments) {
/*
* The sorting algorithm should sort in the following priorities:
diff --git a/app/assets/javascripts/environments/components/stop_environment_modal.vue b/app/assets/javascripts/environments/components/stop_environment_modal.vue
index d3e8fb7ff08..7448fd584c6 100644
--- a/app/assets/javascripts/environments/components/stop_environment_modal.vue
+++ b/app/assets/javascripts/environments/components/stop_environment_modal.vue
@@ -60,7 +60,7 @@ export default {
footer-primary-button-variant="danger"
@submit="onSubmit"
>
- <template slot="header">
+ <template #header>
<h4 class="modal-title d-flex mw-100">
Stopping
<span v-gl-tooltip :title="environment.name" class="text-truncate ml-1 mr-1 flex-fill">
diff --git a/app/assets/javascripts/environments/event_hub.js b/app/assets/javascripts/environments/event_hub.js
index 0948c2e5352..e31806ad199 100644
--- a/app/assets/javascripts/environments/event_hub.js
+++ b/app/assets/javascripts/environments/event_hub.js
@@ -1,3 +1,3 @@
-import Vue from 'vue';
+import createEventHub from '~/helpers/event_hub_factory';
-export default new Vue();
+export default createEventHub();
diff --git a/app/assets/javascripts/environments/folder/environments_folder_bundle.js b/app/assets/javascripts/environments/folder/environments_folder_bundle.js
index c1bfe8d05fe..56896ac4d43 100644
--- a/app/assets/javascripts/environments/folder/environments_folder_bundle.js
+++ b/app/assets/javascripts/environments/folder/environments_folder_bundle.js
@@ -1,5 +1,5 @@
import Vue from 'vue';
-import canaryCalloutMixin from 'ee_else_ce/environments/mixins/canary_callout_mixin';
+import canaryCalloutMixin from '../mixins/canary_callout_mixin';
import environmentsFolderApp from './environments_folder_view.vue';
import { parseBoolean } from '../../lib/utils/common_utils';
import Translate from '../../vue_shared/translate';
diff --git a/app/assets/javascripts/environments/folder/environments_folder_view.vue b/app/assets/javascripts/environments/folder/environments_folder_view.vue
index 30b02585692..e1e356a977f 100644
--- a/app/assets/javascripts/environments/folder/environments_folder_view.vue
+++ b/app/assets/javascripts/environments/folder/environments_folder_view.vue
@@ -1,5 +1,4 @@
<script>
-import folderMixin from 'ee_else_ce/environments/mixins/environments_folder_view_mixin';
import environmentsMixin from '../mixins/environments_mixin';
import CIPaginationMixin from '../../vue_shared/mixins/ci_pagination_api_mixin';
import StopEnvironmentModal from '../components/stop_environment_modal.vue';
@@ -11,7 +10,7 @@ export default {
DeleteEnvironmentModal,
},
- mixins: [environmentsMixin, CIPaginationMixin, folderMixin],
+ mixins: [environmentsMixin, CIPaginationMixin],
props: {
endpoint: {
@@ -30,6 +29,31 @@ export default {
type: Boolean,
required: true,
},
+ canaryDeploymentFeatureId: {
+ type: String,
+ required: false,
+ default: '',
+ },
+ showCanaryDeploymentCallout: {
+ type: Boolean,
+ required: false,
+ default: false,
+ },
+ userCalloutsPath: {
+ type: String,
+ required: false,
+ default: '',
+ },
+ lockPromotionSvgPath: {
+ type: String,
+ required: false,
+ default: '',
+ },
+ helpCanaryDeploymentsPath: {
+ type: String,
+ required: false,
+ default: '',
+ },
},
methods: {
successCallback(resp) {
diff --git a/app/assets/javascripts/environments/index.js b/app/assets/javascripts/environments/index.js
index 9a68619d4f7..4848cb0f13d 100644
--- a/app/assets/javascripts/environments/index.js
+++ b/app/assets/javascripts/environments/index.js
@@ -1,5 +1,5 @@
import Vue from 'vue';
-import canaryCalloutMixin from 'ee_else_ce/environments/mixins/canary_callout_mixin';
+import canaryCalloutMixin from './mixins/canary_callout_mixin';
import environmentsComponent from './components/environments_app.vue';
import { parseBoolean } from '../lib/utils/common_utils';
import Translate from '../vue_shared/translate';
diff --git a/app/assets/javascripts/environments/mixins/canary_callout_mixin.js b/app/assets/javascripts/environments/mixins/canary_callout_mixin.js
index f6d3d67b777..398576a31cb 100644
--- a/app/assets/javascripts/environments/mixins/canary_callout_mixin.js
+++ b/app/assets/javascripts/environments/mixins/canary_callout_mixin.js
@@ -1,5 +1,26 @@
+import { parseBoolean } from '~/lib/utils/common_utils';
+
export default {
+ data() {
+ const data = document.querySelector(this.$options.el).dataset;
+
+ return {
+ canaryDeploymentFeatureId: data.environmentsDataCanaryDeploymentFeatureId,
+ showCanaryDeploymentCallout: parseBoolean(data.environmentsDataShowCanaryDeploymentCallout),
+ userCalloutsPath: data.environmentsDataUserCalloutsPath,
+ lockPromotionSvgPath: data.environmentsDataLockPromotionSvgPath,
+ helpCanaryDeploymentsPath: data.environmentsDataHelpCanaryDeploymentsPath,
+ };
+ },
computed: {
- canaryCalloutProps() {},
+ canaryCalloutProps() {
+ return {
+ canaryDeploymentFeatureId: this.canaryDeploymentFeatureId,
+ showCanaryDeploymentCallout: this.showCanaryDeploymentCallout,
+ userCalloutsPath: this.userCalloutsPath,
+ lockPromotionSvgPath: this.lockPromotionSvgPath,
+ helpCanaryDeploymentsPath: this.helpCanaryDeploymentsPath,
+ };
+ },
},
};
diff --git a/app/assets/javascripts/environments/mixins/container_mixin.js b/app/assets/javascripts/environments/mixins/container_mixin.js
deleted file mode 100644
index abf7d33be91..00000000000
--- a/app/assets/javascripts/environments/mixins/container_mixin.js
+++ /dev/null
@@ -1,34 +0,0 @@
-export default {
- props: {
- canaryDeploymentFeatureId: {
- type: String,
- required: false,
- default: null,
- },
- showCanaryDeploymentCallout: {
- type: Boolean,
- required: false,
- default: false,
- },
- userCalloutsPath: {
- type: String,
- required: false,
- default: null,
- },
- lockPromotionSvgPath: {
- type: String,
- required: false,
- default: null,
- },
- helpCanaryDeploymentsPath: {
- type: String,
- required: false,
- default: null,
- },
- deployBoardsHelpPath: {
- type: String,
- required: false,
- default: '',
- },
- },
-};
diff --git a/app/assets/javascripts/environments/mixins/environment_item_mixin.js b/app/assets/javascripts/environments/mixins/environment_item_mixin.js
deleted file mode 100644
index 2dfed36ec99..00000000000
--- a/app/assets/javascripts/environments/mixins/environment_item_mixin.js
+++ /dev/null
@@ -1,13 +0,0 @@
-export default {
- computed: {
- deployIconName() {
- return '';
- },
- shouldRenderDeployBoard() {
- return false;
- },
- },
- methods: {
- toggleDeployBoard() {},
- },
-};
diff --git a/app/assets/javascripts/environments/mixins/environments_app_mixin.js b/app/assets/javascripts/environments/mixins/environments_app_mixin.js
deleted file mode 100644
index fc805b9235a..00000000000
--- a/app/assets/javascripts/environments/mixins/environments_app_mixin.js
+++ /dev/null
@@ -1,32 +0,0 @@
-export default {
- props: {
- canaryDeploymentFeatureId: {
- type: String,
- required: false,
- default: '',
- },
- showCanaryDeploymentCallout: {
- type: Boolean,
- required: false,
- default: false,
- },
- userCalloutsPath: {
- type: String,
- required: false,
- default: '',
- },
- lockPromotionSvgPath: {
- type: String,
- required: false,
- default: '',
- },
- helpCanaryDeploymentsPath: {
- type: String,
- required: false,
- default: '',
- },
- },
- metods: {
- toggleDeployBoard() {},
- },
-};
diff --git a/app/assets/javascripts/environments/mixins/environments_folder_view_mixin.js b/app/assets/javascripts/environments/mixins/environments_folder_view_mixin.js
deleted file mode 100644
index e793a7cadf2..00000000000
--- a/app/assets/javascripts/environments/mixins/environments_folder_view_mixin.js
+++ /dev/null
@@ -1,29 +0,0 @@
-export default {
- props: {
- canaryDeploymentFeatureId: {
- type: String,
- required: false,
- default: '',
- },
- showCanaryDeploymentCallout: {
- type: Boolean,
- required: false,
- default: false,
- },
- userCalloutsPath: {
- type: String,
- required: false,
- default: '',
- },
- lockPromotionSvgPath: {
- type: String,
- required: false,
- default: '',
- },
- helpCanaryDeploymentsPath: {
- type: String,
- required: false,
- default: '',
- },
- },
-};
diff --git a/app/assets/javascripts/environments/mixins/environments_table_mixin.js b/app/assets/javascripts/environments/mixins/environments_table_mixin.js
deleted file mode 100644
index 208f1a7373d..00000000000
--- a/app/assets/javascripts/environments/mixins/environments_table_mixin.js
+++ /dev/null
@@ -1,10 +0,0 @@
-export default {
- methods: {
- shouldShowCanaryCallout() {
- return false;
- },
- shouldRenderDeployBoard() {
- return false;
- },
- },
-};
diff --git a/app/assets/javascripts/environments/stores/environments_store.js b/app/assets/javascripts/environments/stores/environments_store.js
index 6b7c1ff627d..1992e753255 100644
--- a/app/assets/javascripts/environments/stores/environments_store.js
+++ b/app/assets/javascripts/environments/stores/environments_store.js
@@ -58,13 +58,14 @@ export default class EnvironmentsStore {
let filtered = {};
if (env.size > 1) {
- filtered = Object.assign({}, env, {
+ filtered = {
+ ...env,
isFolder: true,
isLoadingFolderContent: oldEnvironmentState.isLoading || false,
folderName: env.name,
isOpen: oldEnvironmentState.isOpen || false,
children: oldEnvironmentState.children || [],
- });
+ };
}
if (env.latest) {
@@ -133,6 +134,17 @@ export default class EnvironmentsStore {
}
/**
+ * Toggles deploy board visibility for the provided environment ID.
+ * Currently only works on EE.
+ *
+ * @param {Object} environment
+ * @return {Array}
+ */
+ toggleDeployBoard() {
+ return this.state.environments;
+ }
+
+ /**
* Toggles folder open property for the given folder.
*
* @param {Object} folder
@@ -155,7 +167,7 @@ export default class EnvironmentsStore {
let updated = env;
if (env.latest) {
- updated = Object.assign({}, env, env.latest);
+ updated = { ...env, ...env.latest };
delete updated.latest;
} else {
updated = env;
@@ -181,7 +193,7 @@ export default class EnvironmentsStore {
const { environments } = this.state;
const updatedEnvironments = environments.map(env => {
- const updateEnv = Object.assign({}, env);
+ const updateEnv = { ...env };
if (env.id === environment.id) {
updateEnv[prop] = newValue;
}
diff --git a/app/assets/javascripts/error_tracking/components/error_tracking_list.vue b/app/assets/javascripts/error_tracking/components/error_tracking_list.vue
index 3d700f4d216..45432e8ebd8 100644
--- a/app/assets/javascripts/error_tracking/components/error_tracking_list.vue
+++ b/app/assets/javascripts/error_tracking/components/error_tracking_list.vue
@@ -393,9 +393,9 @@ export default {
<template #description>
<div>
<span>{{ __('Monitor your errors by integrating with Sentry.') }}</span>
- <a href="/help/user/project/operations/error_tracking.html">
- {{ __('More information') }}
- </a>
+ <gl-link target="_blank" href="/help/user/project/operations/error_tracking.html">{{
+ __('More information')
+ }}</gl-link>
</div>
</template>
</gl-empty-state>
diff --git a/app/assets/javascripts/error_tracking/components/stacktrace_entry.vue b/app/assets/javascripts/error_tracking/components/stacktrace_entry.vue
index 8db0b1c5da0..f7f2c450be1 100644
--- a/app/assets/javascripts/error_tracking/components/stacktrace_entry.vue
+++ b/app/assets/javascripts/error_tracking/components/stacktrace_entry.vue
@@ -1,5 +1,5 @@
<script>
-import { escape as esc } from 'lodash';
+import { escape } from 'lodash';
import { GlTooltip } from '@gitlab/ui';
import { __, sprintf } from '~/locale';
import ClipboardButton from '~/vue_shared/components/clipboard_button.vue';
@@ -62,7 +62,7 @@ export default {
? sprintf(
__(`%{spanStart}in%{spanEnd} %{errorFn}`),
{
- errorFn: `<strong>${esc(this.errorFn)}</strong>`,
+ errorFn: `<strong>${escape(this.errorFn)}</strong>`,
spanStart: `<span class="text-tertiary">`,
spanEnd: `</span>`,
},
diff --git a/app/assets/javascripts/error_tracking/store/actions.js b/app/assets/javascripts/error_tracking/store/actions.js
index 8f6f404ef8a..05554b2b566 100644
--- a/app/assets/javascripts/error_tracking/store/actions.js
+++ b/app/assets/javascripts/error_tracking/store/actions.js
@@ -1,4 +1,4 @@
-import service from './../services';
+import service from '../services';
import * as types from './mutation_types';
import createFlash from '~/flash';
import { visitUrl } from '~/lib/utils/url_utility';
diff --git a/app/assets/javascripts/filtered_search/constants.js b/app/assets/javascripts/filtered_search/constants.js
index d7264e96b13..7e7a2588951 100644
--- a/app/assets/javascripts/filtered_search/constants.js
+++ b/app/assets/javascripts/filtered_search/constants.js
@@ -4,3 +4,8 @@ export const DROPDOWN_TYPE = {
hint: 'hint',
operator: 'operator',
};
+
+export const FILTER_TYPE = {
+ none: 'none',
+ any: 'any',
+};
diff --git a/app/assets/javascripts/filtered_search/dropdown_operator.js b/app/assets/javascripts/filtered_search/dropdown_operator.js
index 0c8c8140ee9..1bbd33b6258 100644
--- a/app/assets/javascripts/filtered_search/dropdown_operator.js
+++ b/app/assets/javascripts/filtered_search/dropdown_operator.js
@@ -47,13 +47,17 @@ export default class DropdownOperator extends FilteredSearchDropdown {
title: '=',
help: __('is'),
},
- {
+ ];
+
+ if (gon.features?.notIssuableQueries) {
+ dropdownData.push({
tag: 'not-equal',
type: 'string',
title: '!=',
help: __('is not'),
- },
- ];
+ });
+ }
+
this.droplab.changeHookList(this.hookId, this.dropdown, [Filter], this.config);
this.droplab.setData(this.hookId, dropdownData);
super.renderContent(forceShowList);
diff --git a/app/assets/javascripts/filtered_search/event_hub.js b/app/assets/javascripts/filtered_search/event_hub.js
index 0948c2e5352..e31806ad199 100644
--- a/app/assets/javascripts/filtered_search/event_hub.js
+++ b/app/assets/javascripts/filtered_search/event_hub.js
@@ -1,3 +1,3 @@
-import Vue from 'vue';
+import createEventHub from '~/helpers/event_hub_factory';
-export default new Vue();
+export default createEventHub();
diff --git a/app/assets/javascripts/filtered_search/filtered_search_dropdown.js b/app/assets/javascripts/filtered_search/filtered_search_dropdown.js
index 2b6e1f25dc6..f7ce2ea01e0 100644
--- a/app/assets/javascripts/filtered_search/filtered_search_dropdown.js
+++ b/app/assets/javascripts/filtered_search/filtered_search_dropdown.js
@@ -1,6 +1,7 @@
import DropdownUtils from './dropdown_utils';
import FilteredSearchDropdownManager from './filtered_search_dropdown_manager';
import FilteredSearchVisualTokens from './filtered_search_visual_tokens';
+import { FILTER_TYPE } from './constants';
const DATA_DROPDOWN_TRIGGER = 'data-dropdown-trigger';
@@ -74,6 +75,9 @@ export default class FilteredSearchDropdown {
renderContent(forceShowList = false) {
const currentHook = this.getCurrentHook();
+
+ FilteredSearchDropdown.hideDropdownItemsforNotOperator(currentHook);
+
if (forceShowList && currentHook && currentHook.list.hidden) {
currentHook.list.show();
}
@@ -138,4 +142,41 @@ export default class FilteredSearchDropdown {
hook.list.render(results);
}
}
+
+ /**
+ * Hide None & Any options from the current dropdown.
+ * Hiding happens only for NOT operator.
+ */
+ static hideDropdownItemsforNotOperator(currentHook) {
+ const lastOperator = FilteredSearchVisualTokens.getLastTokenOperator();
+
+ if (lastOperator === '!=') {
+ const { list: dropdownEl } = currentHook.list;
+
+ let shouldHideDivider = true;
+
+ // Iterate over all the static dropdown values,
+ // then hide `None` and `Any` items.
+ Array.from(dropdownEl.querySelectorAll('li[data-value]')).forEach(itemEl => {
+ const {
+ dataset: { value },
+ } = itemEl;
+
+ if (value.toLowerCase() === FILTER_TYPE.none || value.toLowerCase() === FILTER_TYPE.any) {
+ itemEl.classList.add('hidden');
+ } else {
+ // If we encountered any element other than None/Any, then
+ // we shouldn't hide the divider
+ shouldHideDivider = false;
+ }
+ });
+
+ if (shouldHideDivider) {
+ const divider = dropdownEl.querySelector('li.divider');
+ if (divider) {
+ divider.classList.add('hidden');
+ }
+ }
+ }
+ }
}
diff --git a/app/assets/javascripts/filtered_search/filtered_search_dropdown_manager.js b/app/assets/javascripts/filtered_search/filtered_search_dropdown_manager.js
index d051b60814e..161a65c511d 100644
--- a/app/assets/javascripts/filtered_search/filtered_search_dropdown_manager.js
+++ b/app/assets/javascripts/filtered_search/filtered_search_dropdown_manager.js
@@ -120,7 +120,7 @@ export default class FilteredSearchDropdownManager {
filter: key,
};
const extraArguments = mappingKey.extraArguments || {};
- const glArguments = Object.assign({}, defaultArguments, extraArguments);
+ const glArguments = { ...defaultArguments, ...extraArguments };
// Passing glArguments to `new glClass(<arguments>)`
mappingKey.reference = new (Function.prototype.bind.apply(glClass, [null, glArguments]))();
diff --git a/app/assets/javascripts/filtered_search/filtered_search_manager.js b/app/assets/javascripts/filtered_search/filtered_search_manager.js
index 724f80f8866..55a0e91b0f3 100644
--- a/app/assets/javascripts/filtered_search/filtered_search_manager.js
+++ b/app/assets/javascripts/filtered_search/filtered_search_manager.js
@@ -31,6 +31,7 @@ export default class FilteredSearchManager {
isGroupDecendent = false,
filteredSearchTokenKeys = IssuableFilteredSearchTokenKeys,
stateFiltersSelector = '.issues-state-filters',
+ placeholder = __('Search or filter results...'),
}) {
this.isGroup = isGroup;
this.isGroupAncestor = isGroupAncestor;
@@ -45,6 +46,7 @@ export default class FilteredSearchManager {
this.tokensContainer = this.container.querySelector('.tokens-container');
this.filteredSearchTokenKeys = filteredSearchTokenKeys;
this.stateFiltersSelector = stateFiltersSelector;
+ this.placeholder = placeholder;
const { multipleAssignees } = this.filteredSearchInput.dataset;
if (multipleAssignees && this.filteredSearchTokenKeys.enableMultipleAssignees) {
@@ -395,11 +397,10 @@ export default class FilteredSearchManager {
handleInputPlaceholder() {
const query = DropdownUtils.getSearchQuery();
- const placeholder = __('Search or filter results...');
const currentPlaceholder = this.filteredSearchInput.placeholder;
- if (query.length === 0 && currentPlaceholder !== placeholder) {
- this.filteredSearchInput.placeholder = placeholder;
+ if (query.length === 0 && currentPlaceholder !== this.placeholder) {
+ this.filteredSearchInput.placeholder = this.placeholder;
} else if (query.length > 0 && currentPlaceholder !== '') {
this.filteredSearchInput.placeholder = '';
}
@@ -710,13 +711,17 @@ export default class FilteredSearchManager {
}
}
- search(state = null) {
- const paths = [];
+ getSearchTokens() {
const searchQuery = DropdownUtils.getSearchQuery();
this.saveCurrentSearchQuery();
const tokenKeys = this.filteredSearchTokenKeys.getKeys();
- const { tokens, searchToken } = this.tokenizer.processTokens(searchQuery, tokenKeys);
+ return this.tokenizer.processTokens(searchQuery, tokenKeys);
+ }
+
+ search(state = null) {
+ const paths = [];
+ const { tokens, searchToken } = this.getSearchTokens();
const currentState = state || getParameterByName('state') || 'opened';
paths.push(`state=${currentState}`);
diff --git a/app/assets/javascripts/filtered_search/stores/recent_searches_store.js b/app/assets/javascripts/filtered_search/stores/recent_searches_store.js
index b3eb0475d6f..cdbc9ec84bd 100644
--- a/app/assets/javascripts/filtered_search/stores/recent_searches_store.js
+++ b/app/assets/javascripts/filtered_search/stores/recent_searches_store.js
@@ -2,14 +2,12 @@ import { uniq } from 'lodash';
class RecentSearchesStore {
constructor(initialState = {}, allowedKeys) {
- this.state = Object.assign(
- {
- isLocalStorageAvailable: true,
- recentSearches: [],
- allowedKeys,
- },
- initialState,
- );
+ this.state = {
+ isLocalStorageAvailable: true,
+ recentSearches: [],
+ allowedKeys,
+ ...initialState,
+ };
}
addRecentSearch(newSearch) {
diff --git a/app/assets/javascripts/filtered_search/visual_token_value.js b/app/assets/javascripts/filtered_search/visual_token_value.js
index b8f4cd8a1e1..02caf0851af 100644
--- a/app/assets/javascripts/filtered_search/visual_token_value.js
+++ b/app/assets/javascripts/filtered_search/visual_token_value.js
@@ -1,4 +1,4 @@
-import { escape as esc } from 'lodash';
+import { escape } from 'lodash';
import { USER_TOKEN_TYPES } from 'ee_else_ce/filtered_search/constants';
import FilteredSearchContainer from '~/filtered_search/container';
import FilteredSearchVisualTokens from '~/filtered_search/filtered_search_visual_tokens';
@@ -48,7 +48,7 @@ export default class VisualTokenValue {
tokenValueContainer.dataset.originalValue = tokenValue;
tokenValueElement.innerHTML = `
<img class="avatar s20" src="${user.avatar_url}" alt="">
- ${esc(user.name)}
+ ${escape(user.name)}
`;
/* eslint-enable no-param-reassign */
})
diff --git a/app/assets/javascripts/flash.js b/app/assets/javascripts/flash.js
index 4d62ec6e385..74c00d21535 100644
--- a/app/assets/javascripts/flash.js
+++ b/app/assets/javascripts/flash.js
@@ -1,4 +1,4 @@
-import _ from 'underscore';
+import { escape } from 'lodash';
import { spriteIcon } from './lib/utils/common_utils';
const FLASH_TYPES = {
@@ -39,14 +39,14 @@ const createAction = config => `
class="flash-action"
${config.href ? '' : 'role="button"'}
>
- ${_.escape(config.title)}
+ ${escape(config.title)}
</a>
`;
const createFlashEl = (message, type) => `
<div class="flash-${type}">
<div class="flash-text">
- ${_.escape(message)}
+ ${escape(message)}
<div class="close-icon-wrapper js-close-icon">
${spriteIcon('close', 'close-icon')}
</div>
diff --git a/app/assets/javascripts/frequent_items/components/app.vue b/app/assets/javascripts/frequent_items/components/app.vue
index 1f1776a5487..61080fb5487 100644
--- a/app/assets/javascripts/frequent_items/components/app.vue
+++ b/app/assets/javascripts/frequent_items/components/app.vue
@@ -3,7 +3,7 @@ import { mapState, mapActions, mapGetters } from 'vuex';
import { GlLoadingIcon } from '@gitlab/ui';
import AccessorUtilities from '~/lib/utils/accessor';
import eventHub from '../event_hub';
-import store from '../store/';
+import store from '../store';
import { FREQUENT_ITEMS, STORAGE_KEY } from '../constants';
import { isMobile, updateExistingFrequentItem, sanitizeItem } from '../utils';
import FrequentItemsSearchInput from './frequent_items_search_input.vue';
diff --git a/app/assets/javascripts/frequent_items/event_hub.js b/app/assets/javascripts/frequent_items/event_hub.js
index 0948c2e5352..e31806ad199 100644
--- a/app/assets/javascripts/frequent_items/event_hub.js
+++ b/app/assets/javascripts/frequent_items/event_hub.js
@@ -1,3 +1,3 @@
-import Vue from 'vue';
+import createEventHub from '~/helpers/event_hub_factory';
-export default new Vue();
+export default createEventHub();
diff --git a/app/assets/javascripts/gfm_auto_complete.js b/app/assets/javascripts/gfm_auto_complete.js
index b6deedfa5e4..f3ce30c942f 100644
--- a/app/assets/javascripts/gfm_auto_complete.js
+++ b/app/assets/javascripts/gfm_auto_complete.js
@@ -1,6 +1,6 @@
import $ from 'jquery';
import '@gitlab/at.js';
-import _ from 'underscore';
+import { escape, template } from 'lodash';
import SidebarMediator from '~/sidebar/sidebar_mediator';
import glRegexp from './lib/utils/regexp';
import AjaxCache from './lib/utils/ajax_cache';
@@ -11,7 +11,7 @@ function sanitize(str) {
}
export function membersBeforeSave(members) {
- return _.map(members, member => {
+ return members.map(member => {
const GROUP_TYPE = 'Group';
let title = '';
@@ -122,7 +122,7 @@ class GfmAutoComplete {
cssClasses.push('has-warning');
}
- return _.template(tpl)({
+ return template(tpl)({
...value,
className: cssClasses.join(' '),
});
@@ -137,7 +137,7 @@ class GfmAutoComplete {
tpl += '<%- referencePrefix %>';
}
}
- return _.template(tpl)({ referencePrefix });
+ return template(tpl, { interpolate: /<%=([\s\S]+?)%>/g })({ referencePrefix });
},
suffix: '',
callbacks: {
@@ -692,14 +692,14 @@ GfmAutoComplete.Emoji = {
// Team Members
GfmAutoComplete.Members = {
templateFunction({ avatarTag, username, title, icon }) {
- return `<li>${avatarTag} ${username} <small>${_.escape(title)}</small> ${icon}</li>`;
+ return `<li>${avatarTag} ${username} <small>${escape(title)}</small> ${icon}</li>`;
},
};
GfmAutoComplete.Labels = {
templateFunction(color, title) {
- return `<li><span class="dropdown-label-box" style="background: ${_.escape(
+ return `<li><span class="dropdown-label-box" style="background: ${escape(
color,
- )}"></span> ${_.escape(title)}</li>`;
+ )}"></span> ${escape(title)}</li>`;
},
};
// Issues, MergeRequests and Snippets
@@ -709,13 +709,13 @@ GfmAutoComplete.Issues = {
return value.reference || '${atwho-at}${id}';
},
templateFunction({ id, title, reference }) {
- return `<li><small>${reference || id}</small> ${_.escape(title)}</li>`;
+ return `<li><small>${reference || id}</small> ${escape(title)}</li>`;
},
};
// Milestones
GfmAutoComplete.Milestones = {
templateFunction(title) {
- return `<li>${_.escape(title)}</li>`;
+ return `<li>${escape(title)}</li>`;
},
};
GfmAutoComplete.Loading = {
diff --git a/app/assets/javascripts/gl_dropdown.js b/app/assets/javascripts/gl_dropdown.js
index 918276ce329..be4b4b5f87d 100644
--- a/app/assets/javascripts/gl_dropdown.js
+++ b/app/assets/javascripts/gl_dropdown.js
@@ -1,7 +1,7 @@
/* eslint-disable max-classes-per-file, one-var, consistent-return */
import $ from 'jquery';
-import _ from 'underscore';
+import { escape } from 'lodash';
import fuzzaldrinPlus from 'fuzzaldrin-plus';
import axios from './lib/utils/axios_utils';
import { visitUrl } from './lib/utils/url_utility';
@@ -145,7 +145,7 @@ class GitLabDropdownFilter {
// { prop: 'foo' },
// { prop: 'baz' }
// ]
- if (_.isArray(data)) {
+ if (Array.isArray(data)) {
results = fuzzaldrinPlus.filter(data, searchText, {
key: this.options.keys,
});
@@ -261,14 +261,14 @@ class GitLabDropdown {
// If no input is passed create a default one
self = this;
// If selector was passed
- if (_.isString(this.filterInput)) {
+ if (typeof this.filterInput === 'string') {
this.filterInput = this.getElement(this.filterInput);
}
const searchFields = this.options.search ? this.options.search.fields : [];
if (this.options.data) {
// If we provided data
// data could be an array of objects or a group of arrays
- if (_.isObject(this.options.data) && !_.isFunction(this.options.data)) {
+ if (typeof this.options.data === 'object' && !(this.options.data instanceof Function)) {
this.fullData = this.options.data;
currentIndex = -1;
this.parseData(this.options.data);
@@ -595,13 +595,14 @@ class GitLabDropdown {
return renderItem({
instance: this,
- options: Object.assign({}, this.options, {
+ options: {
+ ...this.options,
icon: this.icon,
highlight: this.highlight,
highlightText: text => this.highlightTextMatches(text, this.filterInput.val()),
highlightTemplate: this.highlightTemplate.bind(this),
parent,
- }),
+ },
data,
group,
index,
@@ -610,7 +611,7 @@ class GitLabDropdown {
// eslint-disable-next-line class-methods-use-this
highlightTemplate(text, template) {
- return `"<b>${_.escape(text)}</b>" ${template}`;
+ return `"<b>${escape(text)}</b>" ${template}`;
}
// eslint-disable-next-line class-methods-use-this
diff --git a/app/assets/javascripts/gl_form.js b/app/assets/javascripts/gl_form.js
index 1811a942beb..0b7735a7db9 100644
--- a/app/assets/javascripts/gl_form.js
+++ b/app/assets/javascripts/gl_form.js
@@ -8,7 +8,7 @@ export default class GLForm {
constructor(form, enableGFM = {}) {
this.form = form;
this.textarea = this.form.find('textarea.js-gfm-input');
- this.enableGFM = Object.assign({}, defaultAutocompleteConfig, enableGFM);
+ this.enableGFM = { ...defaultAutocompleteConfig, ...enableGFM };
// Disable autocomplete for keywords which do not have dataSources available
const dataSources = (gl.GfmAutoComplete && gl.GfmAutoComplete.dataSources) || {};
Object.keys(this.enableGFM).forEach(item => {
@@ -29,6 +29,10 @@ export default class GLForm {
if (this.autoComplete) {
this.autoComplete.destroy();
}
+ if (this.formDropzone) {
+ this.formDropzone.destroy();
+ }
+
this.form.data('glForm', null);
}
@@ -45,7 +49,7 @@ export default class GLForm {
);
this.autoComplete = new GfmAutoComplete(gl.GfmAutoComplete && gl.GfmAutoComplete.dataSources);
this.autoComplete.setup(this.form.find('.js-gfm-input'), this.enableGFM);
- dropzoneInput(this.form, { parallelUploads: 1 });
+ this.formDropzone = dropzoneInput(this.form, { parallelUploads: 1 });
autosize(this.textarea);
}
// form and textarea event listeners
diff --git a/app/assets/javascripts/groups/components/app.vue b/app/assets/javascripts/groups/components/app.vue
index ce6591e85cf..0b401f4d732 100644
--- a/app/assets/javascripts/groups/components/app.vue
+++ b/app/assets/javascripts/groups/components/app.vue
@@ -111,8 +111,8 @@ export default {
const filterGroupsBy = getParameterByName('filter') || null;
this.isLoading = true;
- // eslint-disable-next-line promise/catch-or-return
- this.fetchGroups({
+
+ return this.fetchGroups({
page,
filterGroupsBy,
sortBy,
@@ -126,8 +126,7 @@ export default {
fetchPage(page, filterGroupsBy, sortBy, archived) {
this.isLoading = true;
- // eslint-disable-next-line promise/catch-or-return
- this.fetchGroups({
+ return this.fetchGroups({
page,
filterGroupsBy,
sortBy,
diff --git a/app/assets/javascripts/groups/event_hub.js b/app/assets/javascripts/groups/event_hub.js
index 0948c2e5352..e31806ad199 100644
--- a/app/assets/javascripts/groups/event_hub.js
+++ b/app/assets/javascripts/groups/event_hub.js
@@ -1,3 +1,3 @@
-import Vue from 'vue';
+import createEventHub from '~/helpers/event_hub_factory';
-export default new Vue();
+export default createEventHub();
diff --git a/app/assets/javascripts/groups/new_group_child.js b/app/assets/javascripts/groups/new_group_child.js
index 012177479c6..bb2aea3ea76 100644
--- a/app/assets/javascripts/groups/new_group_child.js
+++ b/app/assets/javascripts/groups/new_group_child.js
@@ -2,7 +2,7 @@ import { visitUrl } from '../lib/utils/url_utility';
import DropLab from '../droplab/drop_lab';
import ISetter from '../droplab/plugins/input_setter';
-const InputSetter = Object.assign({}, ISetter);
+const InputSetter = { ...ISetter };
const NEW_PROJECT = 'new-project';
const NEW_SUBGROUP = 'new-subgroup';
diff --git a/app/assets/javascripts/header.js b/app/assets/javascripts/header.js
index 1678991b1ea..67b068f1c6b 100644
--- a/app/assets/javascripts/header.js
+++ b/app/assets/javascripts/header.js
@@ -74,20 +74,27 @@ function initStatusTriggers() {
}
}
+function trackShowUserDropdownLink(trackEvent, elToTrack, el) {
+ const { trackLabel, trackProperty } = elToTrack.dataset;
+
+ $(el).on('shown.bs.dropdown', () => {
+ Tracking.event(document.body.dataset.page, trackEvent, {
+ label: trackLabel,
+ property: trackProperty,
+ });
+ });
+}
export function initNavUserDropdownTracking() {
const el = document.querySelector('.js-nav-user-dropdown');
const buyEl = document.querySelector('.js-buy-ci-minutes-link');
+ const upgradeEl = document.querySelector('.js-upgrade-plan-link');
if (el && buyEl) {
- const { trackLabel, trackProperty } = buyEl.dataset;
- const trackEvent = 'show_buy_ci_minutes';
+ trackShowUserDropdownLink('show_buy_ci_minutes', buyEl, el);
+ }
- $(el).on('shown.bs.dropdown', () => {
- Tracking.event(undefined, trackEvent, {
- label: trackLabel,
- property: trackProperty,
- });
- });
+ if (el && upgradeEl) {
+ trackShowUserDropdownLink('show_upgrade_link', upgradeEl, el);
}
}
diff --git a/app/assets/javascripts/helpers/avatar_helper.js b/app/assets/javascripts/helpers/avatar_helper.js
index 7891b44dd27..4f04a1b8c16 100644
--- a/app/assets/javascripts/helpers/avatar_helper.js
+++ b/app/assets/javascripts/helpers/avatar_helper.js
@@ -1,11 +1,14 @@
import { escape } from 'lodash';
import { getFirstCharacterCapitalized } from '~/lib/utils/text_utility';
+import { getIdFromGraphQLId } from '~/graphql_shared/utils';
export const DEFAULT_SIZE_CLASS = 's40';
export const IDENTICON_BG_COUNT = 7;
export function getIdenticonBackgroundClass(entityId) {
- const type = (entityId % IDENTICON_BG_COUNT) + 1;
+ // If a GraphQL string id is passed in, convert it to the entity number
+ const id = typeof entityId === 'string' ? getIdFromGraphQLId(entityId) : entityId;
+ const type = (id % IDENTICON_BG_COUNT) + 1;
return `bg${type}`;
}
diff --git a/app/assets/javascripts/helpers/event_hub_factory.js b/app/assets/javascripts/helpers/event_hub_factory.js
new file mode 100644
index 00000000000..4d7f7550a94
--- /dev/null
+++ b/app/assets/javascripts/helpers/event_hub_factory.js
@@ -0,0 +1,20 @@
+import mitt from 'mitt';
+
+export default () => {
+ const emitter = mitt();
+
+ emitter.once = (event, handler) => {
+ const wrappedHandler = evt => {
+ handler(evt);
+ emitter.off(event, wrappedHandler);
+ };
+ emitter.on(event, wrappedHandler);
+ };
+
+ emitter.$on = emitter.on;
+ emitter.$once = emitter.once;
+ emitter.$off = emitter.off;
+ emitter.$emit = emitter.emit;
+
+ return emitter;
+};
diff --git a/app/assets/javascripts/ide/components/commit_sidebar/actions.vue b/app/assets/javascripts/ide/components/commit_sidebar/actions.vue
index 6a8ea506d1b..6c563776533 100644
--- a/app/assets/javascripts/ide/components/commit_sidebar/actions.vue
+++ b/app/assets/javascripts/ide/components/commit_sidebar/actions.vue
@@ -1,5 +1,5 @@
<script>
-import { escape as esc } from 'lodash';
+import { escape } from 'lodash';
import { mapState, mapGetters, createNamespacedHelpers } from 'vuex';
import { sprintf, s__ } from '~/locale';
import consts from '../../stores/modules/commit/constants';
@@ -22,7 +22,7 @@ export default {
commitToCurrentBranchText() {
return sprintf(
s__('IDE|Commit to %{branchName} branch'),
- { branchName: `<strong class="monospace">${esc(this.currentBranchId)}</strong>` },
+ { branchName: `<strong class="monospace">${escape(this.currentBranchId)}</strong>` },
false,
);
},
diff --git a/app/assets/javascripts/ide/components/commit_sidebar/editor_header.vue b/app/assets/javascripts/ide/components/commit_sidebar/editor_header.vue
index e618fb3daae..24499fb9f6d 100644
--- a/app/assets/javascripts/ide/components/commit_sidebar/editor_header.vue
+++ b/app/assets/javascripts/ide/components/commit_sidebar/editor_header.vue
@@ -24,8 +24,8 @@ export default {
discardModalTitle() {
return sprintf(__('Discard changes to %{path}?'), { path: this.activeFile.path });
},
- isStaged() {
- return !this.activeFile.changed && this.activeFile.staged;
+ canDiscard() {
+ return this.activeFile.changed || this.activeFile.staged;
},
},
methods: {
@@ -53,7 +53,7 @@ export default {
<changed-file-icon :file="activeFile" :is-centered="false" />
<div class="ml-auto">
<button
- v-if="!isStaged"
+ v-if="canDiscard"
ref="discardButton"
type="button"
class="btn btn-remove btn-inverted append-right-8"
diff --git a/app/assets/javascripts/ide/components/commit_sidebar/form.vue b/app/assets/javascripts/ide/components/commit_sidebar/form.vue
index f6ca728defc..4cbd33e6ed6 100644
--- a/app/assets/javascripts/ide/components/commit_sidebar/form.vue
+++ b/app/assets/javascripts/ide/components/commit_sidebar/form.vue
@@ -1,11 +1,13 @@
<script>
import { mapState, mapActions, mapGetters } from 'vuex';
import { n__, __ } from '~/locale';
+import { GlModal } from '@gitlab/ui';
import LoadingButton from '~/vue_shared/components/loading_button.vue';
import CommitMessageField from './message_field.vue';
import Actions from './actions.vue';
import SuccessMessage from './success_message.vue';
import { leftSidebarViews, MAX_WINDOW_HEIGHT_COMPACT } from '../../constants';
+import consts from '../../stores/modules/commit/constants';
export default {
components: {
@@ -13,6 +15,7 @@ export default {
LoadingButton,
CommitMessageField,
SuccessMessage,
+ GlModal,
},
data() {
return {
@@ -54,7 +57,20 @@ export default {
},
methods: {
...mapActions(['updateActivityBarView']),
- ...mapActions('commit', ['updateCommitMessage', 'discardDraft', 'commitChanges']),
+ ...mapActions('commit', [
+ 'updateCommitMessage',
+ 'discardDraft',
+ 'commitChanges',
+ 'updateCommitAction',
+ ]),
+ commit() {
+ return this.commitChanges().catch(() => {
+ this.$refs.createBranchModal.show();
+ });
+ },
+ forceCreateNewBranch() {
+ return this.updateCommitAction(consts.COMMIT_TO_NEW_BRANCH).then(() => this.commit());
+ },
toggleIsCompact() {
if (this.currentViewIsCommitView) {
this.isCompact = !this.isCompact;
@@ -119,13 +135,13 @@ export default {
</button>
<p class="text-center bold">{{ overviewText }}</p>
</div>
- <form v-if="!isCompact" ref="formEl" @submit.prevent.stop="commitChanges">
+ <form v-if="!isCompact" ref="formEl" @submit.prevent.stop="commit">
<transition name="fade"> <success-message v-show="lastCommitMsg" /> </transition>
<commit-message-field
:text="commitMessage"
:placeholder="preBuiltCommitMessage"
@input="updateCommitMessage"
- @submit="commitChanges"
+ @submit="commit"
/>
<div class="clearfix prepend-top-15">
<actions />
@@ -133,7 +149,7 @@ export default {
:loading="submitCommitLoading"
:label="commitButtonText"
container-class="btn btn-success btn-sm float-left qa-commit-button"
- @click="commitChanges"
+ @click="commit"
/>
<button
v-if="!discardDraftButtonDisabled"
@@ -152,6 +168,19 @@ export default {
{{ __('Collapse') }}
</button>
</div>
+ <gl-modal
+ ref="createBranchModal"
+ modal-id="ide-create-branch-modal"
+ :ok-title="__('Create new branch')"
+ :title="__('Branch has changed')"
+ ok-variant="success"
+ @ok="forceCreateNewBranch"
+ >
+ {{
+ __(`This branch has changed since you started editing.
+ Would you like to create a new branch?`)
+ }}
+ </gl-modal>
</form>
</transition>
</div>
diff --git a/app/assets/javascripts/ide/components/commit_sidebar/list.vue b/app/assets/javascripts/ide/components/commit_sidebar/list.vue
index a15e22d4742..e6a1a1ba73c 100644
--- a/app/assets/javascripts/ide/components/commit_sidebar/list.vue
+++ b/app/assets/javascripts/ide/components/commit_sidebar/list.vue
@@ -1,9 +1,8 @@
<script>
-import $ from 'jquery';
import { mapActions } from 'vuex';
import { __, sprintf } from '~/locale';
+import { GlModal } from '@gitlab/ui';
import Icon from '~/vue_shared/components/icon.vue';
-import DeprecatedModal2 from '~/vue_shared/components/deprecated_modal_2.vue';
import tooltip from '~/vue_shared/directives/tooltip';
import ListItem from './list_item.vue';
@@ -11,7 +10,7 @@ export default {
components: {
Icon,
ListItem,
- GlModal: DeprecatedModal2,
+ GlModal,
},
directives: {
tooltip,
@@ -58,7 +57,7 @@ export default {
methods: {
...mapActions(['stageAllChanges', 'unstageAllChanges', 'discardAllChanges']),
openDiscardModal() {
- $('#discard-all-changes').modal('show');
+ this.$refs.discardAllModal.show();
},
unstageAndDiscardAllChanges() {
this.unstageAllChanges();
@@ -114,11 +113,12 @@ export default {
</p>
<gl-modal
v-if="!stagedList"
- id="discard-all-changes"
- :footer-primary-button-text="__('Discard all changes')"
- :header-title-text="__('Discard all changes?')"
- footer-primary-button-variant="danger"
- @submit="unstageAndDiscardAllChanges"
+ ref="discardAllModal"
+ ok-variant="danger"
+ modal-id="discard-all-changes"
+ :ok-title="__('Discard all changes')"
+ :title="__('Discard all changes?')"
+ @ok="unstageAndDiscardAllChanges"
>
{{ $options.discardModalText }}
</gl-modal>
diff --git a/app/assets/javascripts/ide/components/ide.vue b/app/assets/javascripts/ide/components/ide.vue
index 7ebcacc530f..36c8b18e205 100644
--- a/app/assets/javascripts/ide/components/ide.vue
+++ b/app/assets/javascripts/ide/components/ide.vue
@@ -3,6 +3,7 @@ import Vue from 'vue';
import { mapActions, mapGetters, mapState } from 'vuex';
import { GlDeprecatedButton, GlLoadingIcon } from '@gitlab/ui';
import { __ } from '~/locale';
+import { modalTypes } from '../constants';
import FindFile from '~/vue_shared/components/file_finder/index.vue';
import NewModal from './new_dropdown/modal.vue';
import IdeSidebar from './ide_side_bar.vue';
@@ -12,6 +13,7 @@ import RepoEditor from './repo_editor.vue';
import RightPane from './panes/right.vue';
import ErrorMessage from './error_message.vue';
import CommitEditorHeader from './commit_sidebar/editor_header.vue';
+import glFeatureFlagsMixin from '~/vue_shared/mixins/gl_feature_flags_mixin';
export default {
components: {
@@ -26,6 +28,7 @@ export default {
GlDeprecatedButton,
GlLoadingIcon,
},
+ mixins: [glFeatureFlagsMixin()],
props: {
rightPaneComponent: {
type: Vue.Component,
@@ -52,13 +55,20 @@ export default {
'allBlobs',
'emptyRepo',
'currentTree',
+ 'editorTheme',
]),
+ themeName() {
+ return window.gon?.user_color_scheme;
+ },
},
mounted() {
window.onbeforeunload = e => this.onBeforeUnload(e);
+
+ if (this.themeName)
+ document.querySelector('.navbar-gitlab').classList.add(`theme-${this.themeName}`);
},
methods: {
- ...mapActions(['toggleFileFinder', 'openNewEntryModal']),
+ ...mapActions(['toggleFileFinder']),
onBeforeUnload(e = {}) {
const returnValue = __('Are you sure you want to lose unsaved changes?');
@@ -72,12 +82,18 @@ export default {
openFile(file) {
this.$router.push(`/project${file.url}`);
},
+ createNewFile() {
+ this.$refs.newModal.open(modalTypes.blob);
+ },
},
};
</script>
<template>
- <article class="ide position-relative d-flex flex-column align-items-stretch">
+ <article
+ class="ide position-relative d-flex flex-column align-items-stretch"
+ :class="{ [`theme-${themeName}`]: themeName }"
+ >
<error-message v-if="errorMessage" :message="errorMessage" />
<div class="ide-view flex-grow d-flex">
<find-file
@@ -125,7 +141,7 @@ export default {
variant="success"
:title="__('New file')"
:aria-label="__('New file')"
- @click="openNewEntryModal({ type: 'blob' })"
+ @click="createNewFile()"
>
{{ __('New file') }}
</gl-deprecated-button>
@@ -147,6 +163,6 @@ export default {
<component :is="rightPaneComponent" v-if="currentProjectId" />
</div>
<ide-status-bar />
- <new-modal />
+ <new-modal ref="newModal" />
</article>
</template>
diff --git a/app/assets/javascripts/ide/components/ide_review.vue b/app/assets/javascripts/ide/components/ide_review.vue
index 901b8892e80..62dbfea2088 100644
--- a/app/assets/javascripts/ide/components/ide_review.vue
+++ b/app/assets/javascripts/ide/components/ide_review.vue
@@ -43,7 +43,7 @@ export default {
<template>
<ide-tree-list :viewer-type="viewer" header-class="ide-review-header">
- <template slot="header">
+ <template #header>
<div class="ide-review-button-holder">
{{ __('Review') }}
<editor-mode-dropdown
diff --git a/app/assets/javascripts/ide/components/ide_side_bar.vue b/app/assets/javascripts/ide/components/ide_side_bar.vue
index 40cd2178e09..7cb31df85ce 100644
--- a/app/assets/javascripts/ide/components/ide_side_bar.vue
+++ b/app/assets/javascripts/ide/components/ide_side_bar.vue
@@ -37,7 +37,7 @@ export default {
</script>
<template>
- <resizable-panel :collapsible="false" :initial-width="340" side="left" class="flex-column">
+ <resizable-panel :initial-width="340" side="left" class="flex-column">
<template v-if="loading">
<div class="multi-file-commit-panel-inner">
<div v-for="n in 3" :key="n" class="multi-file-loading-container">
diff --git a/app/assets/javascripts/ide/components/ide_tree.vue b/app/assets/javascripts/ide/components/ide_tree.vue
index 598f3a1dac6..647f4d4be85 100644
--- a/app/assets/javascripts/ide/components/ide_tree.vue
+++ b/app/assets/javascripts/ide/components/ide_tree.vue
@@ -1,14 +1,17 @@
<script>
import { mapState, mapGetters, mapActions } from 'vuex';
+import { modalTypes } from '../constants';
import IdeTreeList from './ide_tree_list.vue';
import Upload from './new_dropdown/upload.vue';
import NewEntryButton from './new_dropdown/button.vue';
+import NewModal from './new_dropdown/modal.vue';
export default {
components: {
Upload,
IdeTreeList,
NewEntryButton,
+ NewModal,
},
computed: {
...mapState(['currentBranchId']),
@@ -26,14 +29,20 @@ export default {
}
},
methods: {
- ...mapActions(['updateViewer', 'openNewEntryModal', 'createTempEntry', 'resetOpenFiles']),
+ ...mapActions(['updateViewer', 'createTempEntry', 'resetOpenFiles']),
+ createNewFile() {
+ this.$refs.newModal.open(modalTypes.blob);
+ },
+ createNewFolder() {
+ this.$refs.newModal.open(modalTypes.tree);
+ },
},
};
</script>
<template>
<ide-tree-list viewer-type="editor">
- <template slot="header">
+ <template #header>
{{ __('Edit') }}
<div class="ide-tree-actions ml-auto d-flex">
<new-entry-button
@@ -41,7 +50,7 @@ export default {
:show-label="false"
class="d-flex border-0 p-0 mr-3 qa-new-file"
icon="doc-new"
- @click="openNewEntryModal({ type: 'blob' })"
+ @click="createNewFile()"
/>
<upload
:show-label="false"
@@ -54,9 +63,10 @@ export default {
:show-label="false"
class="d-flex border-0 p-0"
icon="folder-new"
- @click="openNewEntryModal({ type: 'tree' })"
+ @click="createNewFolder()"
/>
</div>
+ <new-modal ref="newModal" />
</template>
</ide-tree-list>
</template>
diff --git a/app/assets/javascripts/ide/components/jobs/detail.vue b/app/assets/javascripts/ide/components/jobs/detail.vue
index 504391ffdc7..975d54c7a4e 100644
--- a/app/assets/javascripts/ide/components/jobs/detail.vue
+++ b/app/assets/javascripts/ide/components/jobs/detail.vue
@@ -79,7 +79,7 @@ export default {
<icon name="chevron-left" /> {{ __('View jobs') }}
</button>
</header>
- <div class="top-bar d-flex border-left-0">
+ <div class="top-bar d-flex border-left-0 mr-3">
<job-description :job="detailJob" />
<div class="controllers ml-auto">
<a
@@ -97,7 +97,7 @@ export default {
<scroll-button :disabled="isScrolledToBottom" direction="down" @click="scrollDown" />
</div>
</div>
- <pre ref="buildTrace" class="build-trace mb-0 h-100" @scroll="scrollBuildLog">
+ <pre ref="buildTrace" class="build-trace mb-0 h-100 mr-3" @scroll="scrollBuildLog">
<code
v-show="!detailJob.isLoading"
class="bash"
diff --git a/app/assets/javascripts/ide/components/nav_form.vue b/app/assets/javascripts/ide/components/nav_form.vue
index 195504a6861..70a92b8d3ab 100644
--- a/app/assets/javascripts/ide/components/nav_form.vue
+++ b/app/assets/javascripts/ide/components/nav_form.vue
@@ -25,13 +25,13 @@ export default {
<div class="ide-nav-form p-0">
<tabs v-if="showMergeRequests" stop-propagation>
<tab active>
- <template slot="title">
+ <template #title>
{{ __('Branches') }}
</template>
<branches-search-list />
</tab>
<tab>
- <template slot="title">
+ <template #title>
{{ __('Merge Requests') }}
</template>
<merge-request-search-list />
diff --git a/app/assets/javascripts/ide/components/new_dropdown/index.vue b/app/assets/javascripts/ide/components/new_dropdown/index.vue
index 9961c0df52e..2798ede5341 100644
--- a/app/assets/javascripts/ide/components/new_dropdown/index.vue
+++ b/app/assets/javascripts/ide/components/new_dropdown/index.vue
@@ -4,12 +4,14 @@ import icon from '~/vue_shared/components/icon.vue';
import upload from './upload.vue';
import ItemButton from './button.vue';
import { modalTypes } from '../../constants';
+import NewModal from './modal.vue';
export default {
components: {
icon,
upload,
ItemButton,
+ NewModal,
},
props: {
type: {
@@ -37,9 +39,9 @@ export default {
},
},
methods: {
- ...mapActions(['createTempEntry', 'openNewEntryModal', 'deleteEntry']),
+ ...mapActions(['createTempEntry', 'deleteEntry']),
createNewItem(type) {
- this.openNewEntryModal({ type, path: this.path });
+ this.$refs.newModal.open(type, this.path);
this.$emit('toggle', false);
},
openDropdown() {
@@ -109,5 +111,6 @@ export default {
</li>
</ul>
</div>
+ <new-modal ref="newModal" />
</div>
</template>
diff --git a/app/assets/javascripts/ide/components/new_dropdown/modal.vue b/app/assets/javascripts/ide/components/new_dropdown/modal.vue
index bf3d736ddf3..4766a2fe6ae 100644
--- a/app/assets/javascripts/ide/components/new_dropdown/modal.vue
+++ b/app/assets/javascripts/ide/components/new_dropdown/modal.vue
@@ -1,61 +1,49 @@
<script>
-import $ from 'jquery';
import { mapActions, mapState, mapGetters } from 'vuex';
import flash from '~/flash';
import { __, sprintf, s__ } from '~/locale';
-import DeprecatedModal2 from '~/vue_shared/components/deprecated_modal_2.vue';
+import { GlModal } from '@gitlab/ui';
import { modalTypes } from '../../constants';
+import { trimPathComponents } from '../../utils';
export default {
components: {
- GlModal: DeprecatedModal2,
+ GlModal,
},
data() {
return {
- name: '',
+ entryName: '',
+ modalType: modalTypes.blob,
+ path: '',
};
},
computed: {
- ...mapState(['entries', 'entryModal']),
+ ...mapState(['entries']),
...mapGetters('fileTemplates', ['templateTypes']),
- entryName: {
- get() {
- const entryPath = this.entryModal.entry.path;
-
- if (this.entryModal.type === modalTypes.rename) {
- return this.name || entryPath;
- }
-
- return this.name || (entryPath ? `${entryPath}/` : '');
- },
- set(val) {
- this.name = val.trim();
- },
- },
modalTitle() {
- if (this.entryModal.type === modalTypes.tree) {
+ const entry = this.entries[this.path];
+
+ if (this.modalType === modalTypes.tree) {
return __('Create new directory');
- } else if (this.entryModal.type === modalTypes.rename) {
- return this.entryModal.entry.type === modalTypes.tree
- ? __('Rename folder')
- : __('Rename file');
+ } else if (this.modalType === modalTypes.rename) {
+ return entry.type === modalTypes.tree ? __('Rename folder') : __('Rename file');
}
return __('Create new file');
},
buttonLabel() {
- if (this.entryModal.type === modalTypes.tree) {
+ const entry = this.entries[this.path];
+
+ if (this.modalType === modalTypes.tree) {
return __('Create directory');
- } else if (this.entryModal.type === modalTypes.rename) {
- return this.entryModal.entry.type === modalTypes.tree
- ? __('Rename folder')
- : __('Rename file');
+ } else if (this.modalType === modalTypes.rename) {
+ return entry.type === modalTypes.tree ? __('Rename folder') : __('Rename file');
}
return __('Create file');
},
isCreatingNewFile() {
- return this.entryModal.type === 'blob';
+ return this.modalType === modalTypes.blob;
},
placeholder() {
return this.isCreatingNewFile ? 'dir/file_name' : 'dir/';
@@ -64,7 +52,9 @@ export default {
methods: {
...mapActions(['createTempEntry', 'renameEntry']),
submitForm() {
- if (this.entryModal.type === modalTypes.rename) {
+ this.entryName = trimPathComponents(this.entryName);
+
+ if (this.modalType === modalTypes.rename) {
if (this.entries[this.entryName] && !this.entries[this.entryName].deleted) {
flash(
sprintf(s__('The name "%{name}" is already taken in this directory.'), {
@@ -78,32 +68,32 @@ export default {
);
} else {
let parentPath = this.entryName.split('/');
- const entryName = parentPath.pop();
+ const name = parentPath.pop();
parentPath = parentPath.join('/');
this.renameEntry({
- path: this.entryModal.entry.path,
- name: entryName,
+ path: this.path,
+ name,
parentPath,
});
}
} else {
this.createTempEntry({
- name: this.name,
- type: this.entryModal.type,
+ name: this.entryName,
+ type: this.modalType,
});
}
},
createFromTemplate(template) {
this.createTempEntry({
name: template.name,
- type: this.entryModal.type,
+ type: this.modalType,
});
- $('#ide-new-entry').modal('toggle');
+ this.$refs.modal.toggle();
},
focusInput() {
- const name = this.entries[this.entryName] ? this.entries[this.entryName].name : null;
+ const name = this.entries[this.entryName]?.name;
const inputValue = this.$refs.fieldName.value;
this.$refs.fieldName.focus();
@@ -112,8 +102,28 @@ export default {
this.$refs.fieldName.setSelectionRange(inputValue.indexOf(name), inputValue.length);
}
},
- closedModal() {
- this.name = '';
+ resetData() {
+ this.entryName = '';
+ this.path = '';
+ this.modalType = modalTypes.blob;
+ },
+ open(type = modalTypes.blob, path = '') {
+ this.modalType = type;
+ this.path = path;
+
+ if (this.modalType === modalTypes.rename) {
+ this.entryName = path;
+ } else {
+ this.entryName = path ? `${path}/` : '';
+ }
+
+ this.$refs.modal.show();
+
+ // wait for modal to show first
+ this.$nextTick(() => this.focusInput());
+ },
+ close() {
+ this.$refs.modal.hide();
},
},
};
@@ -121,22 +131,22 @@ export default {
<template>
<gl-modal
- id="ide-new-entry"
- class="qa-new-file-modal"
- :header-title-text="modalTitle"
- :footer-primary-button-text="buttonLabel"
- footer-primary-button-variant="success"
- modal-size="lg"
- @submit="submitForm"
- @open="focusInput"
- @closed="closedModal"
+ ref="modal"
+ modal-id="ide-new-entry"
+ modal-class="qa-new-file-modal"
+ :title="modalTitle"
+ :ok-title="buttonLabel"
+ ok-variant="success"
+ size="lg"
+ @ok="submitForm"
+ @hide="resetData"
>
<div class="form-group row">
<label class="label-bold col-form-label col-sm-2"> {{ __('Name') }} </label>
<div class="col-sm-10">
<input
ref="fieldName"
- v-model="entryName"
+ v-model.trim="entryName"
type="text"
class="form-control qa-full-file-path"
:placeholder="placeholder"
diff --git a/app/assets/javascripts/ide/components/panes/collapsible_sidebar.vue b/app/assets/javascripts/ide/components/panes/collapsible_sidebar.vue
index 8adf0122fb4..91e80be7d18 100644
--- a/app/assets/javascripts/ide/components/panes/collapsible_sidebar.vue
+++ b/app/assets/javascripts/ide/components/panes/collapsible_sidebar.vue
@@ -103,7 +103,6 @@ export default {
>
<resizable-panel
v-show="isOpen"
- :collapsible="false"
:initial-width="width"
:min-size="width"
:class="`ide-${side}-sidebar-${currentView}`"
@@ -116,7 +115,7 @@ export default {
v-for="tabView in aliveTabViews"
v-show="isActiveView(tabView.name)"
:key="tabView.name"
- class="flex-fill js-tab-view"
+ class="flex-fill gl-overflow-hidden js-tab-view"
>
<component :is="tabView.component" />
</div>
diff --git a/app/assets/javascripts/ide/components/pipelines/list.vue b/app/assets/javascripts/ide/components/pipelines/list.vue
index d3e5add2e83..cf6d01b6351 100644
--- a/app/assets/javascripts/ide/components/pipelines/list.vue
+++ b/app/assets/javascripts/ide/components/pipelines/list.vue
@@ -1,6 +1,6 @@
<script>
import { mapActions, mapGetters, mapState } from 'vuex';
-import { escape as esc } from 'lodash';
+import { escape } from 'lodash';
import { GlLoadingIcon } from '@gitlab/ui';
import { sprintf, __ } from '../../../locale';
import Icon from '../../../vue_shared/components/icon.vue';
@@ -10,6 +10,8 @@ import Tab from '../../../vue_shared/components/tabs/tab.vue';
import EmptyState from '../../../pipelines/components/empty_state.vue';
import JobsList from '../jobs/list.vue';
+import IDEServices from '~/ide/services';
+
export default {
components: {
Icon,
@@ -35,7 +37,7 @@ export default {
return sprintf(
__('You can test your .gitlab-ci.yml in %{linkStart}CI Lint%{linkEnd}.'),
{
- linkStart: `<a href="${esc(this.currentProject.web_url)}/-/ci/lint">`,
+ linkStart: `<a href="${escape(this.currentProject.web_url)}/-/ci/lint">`,
linkEnd: '</a>',
},
false,
@@ -47,6 +49,7 @@ export default {
},
created() {
this.fetchLatestPipeline();
+ IDEServices.pingUsage(this.currentProject.path_with_namespace);
},
methods: {
...mapActions('pipelines', ['fetchLatestPipeline']),
@@ -85,14 +88,14 @@ export default {
</div>
<tabs v-else class="ide-pipeline-list">
<tab :active="!pipelineFailed">
- <template slot="title">
+ <template #title>
{{ __('Jobs') }}
<span v-if="jobsCount" class="badge badge-pill"> {{ jobsCount }} </span>
</template>
<jobs-list :loading="isLoadingJobs" :stages="stages" />
</tab>
<tab :active="pipelineFailed">
- <template slot="title">
+ <template #title>
{{ __('Failed Jobs') }}
<span v-if="failedJobsCount" class="badge badge-pill"> {{ failedJobsCount }} </span>
</template>
diff --git a/app/assets/javascripts/ide/components/repo_commit_section.vue b/app/assets/javascripts/ide/components/repo_commit_section.vue
index 2e7e55a61c5..530fba49df2 100644
--- a/app/assets/javascripts/ide/components/repo_commit_section.vue
+++ b/app/assets/javascripts/ide/components/repo_commit_section.vue
@@ -1,15 +1,12 @@
<script>
import { mapState, mapActions, mapGetters } from 'vuex';
import tooltip from '~/vue_shared/directives/tooltip';
-import DeprecatedModal from '~/vue_shared/components/deprecated_modal.vue';
import CommitFilesList from './commit_sidebar/list.vue';
import EmptyState from './commit_sidebar/empty_state.vue';
-import consts from '../stores/modules/commit/constants';
import { leftSidebarViews, stageKeys } from '../constants';
export default {
components: {
- DeprecatedModal,
CommitFilesList,
EmptyState,
},
@@ -17,13 +14,7 @@ export default {
tooltip,
},
computed: {
- ...mapState([
- 'changedFiles',
- 'stagedFiles',
- 'rightPanelCollapsed',
- 'lastCommitMsg',
- 'unusedSeal',
- ]),
+ ...mapState(['changedFiles', 'stagedFiles', 'lastCommitMsg', 'unusedSeal']),
...mapState('commit', ['commitMessage', 'submitCommitLoading']),
...mapGetters(['lastOpenedFile', 'hasChanges', 'someUncommittedChanges', 'activeFile']),
...mapGetters('commit', ['discardDraftButtonDisabled']),
@@ -59,10 +50,6 @@ export default {
},
methods: {
...mapActions(['openPendingTab', 'updateViewer', 'updateActivityBarView']),
- ...mapActions('commit', ['commitChanges', 'updateCommitAction']),
- forceCreateNewBranch() {
- return this.updateCommitAction(consts.COMMIT_TO_NEW_BRANCH).then(() => this.commitChanges());
- },
},
stageKeys,
};
@@ -70,20 +57,6 @@ export default {
<template>
<div class="multi-file-commit-panel-section">
- <deprecated-modal
- id="ide-create-branch-modal"
- :primary-button-label="__('Create new branch')"
- :title="__('Branch has changed')"
- kind="success"
- @submit="forceCreateNewBranch"
- >
- <template slot="body">
- {{
- __(`This branch has changed since you started editing.
- Would you like to create a new branch?`)
- }}
- </template>
- </deprecated-modal>
<template v-if="showStageUnstageArea">
<commit-files-list
:key-prefix="$options.stageKeys.staged"
diff --git a/app/assets/javascripts/ide/components/repo_editor.vue b/app/assets/javascripts/ide/components/repo_editor.vue
index 08850679152..c72a8b2b0d0 100644
--- a/app/assets/javascripts/ide/components/repo_editor.vue
+++ b/app/assets/javascripts/ide/components/repo_editor.vue
@@ -13,6 +13,7 @@ import {
import Editor from '../lib/editor';
import FileTemplatesBar from './file_templates/bar.vue';
import { __ } from '~/locale';
+import { extractMarkdownImagesFromEntries } from '../stores/utils';
export default {
components: {
@@ -26,17 +27,23 @@ export default {
required: true,
},
},
+ data() {
+ return {
+ content: '',
+ images: {},
+ };
+ },
computed: {
...mapState('rightPane', {
rightPaneIsOpen: 'isOpen',
}),
...mapState([
- 'rightPanelCollapsed',
'viewer',
'panelResizing',
'currentActivityView',
'renderWhitespaceInCode',
'editorTheme',
+ 'entries',
]),
...mapGetters([
'currentMergeRequest',
@@ -44,6 +51,7 @@ export default {
'isEditModeActive',
'isCommitModeActive',
'isReviewModeActive',
+ 'currentBranch',
]),
...mapGetters('fileTemplates', ['showFileTemplatesBar']),
shouldHideEditor() {
@@ -87,6 +95,9 @@ export default {
theme: this.editorTheme,
};
},
+ currentBranchCommit() {
+ return this.currentBranch?.commit.id;
+ },
},
watch: {
file(newVal, oldVal) {
@@ -114,9 +125,6 @@ export default {
});
}
},
- rightPanelCollapsed() {
- this.refreshEditorDimensions();
- },
viewer() {
if (!this.file.pending) {
this.createEditorInstance();
@@ -136,6 +144,18 @@ export default {
this.$nextTick(() => this.refreshEditorDimensions());
}
},
+ showContentViewer(val) {
+ if (!val) return;
+
+ if (this.fileType === 'markdown') {
+ const { content, images } = extractMarkdownImagesFromEntries(this.file, this.entries);
+ this.content = content;
+ this.images = images;
+ } else {
+ this.content = this.file.content || this.file.raw;
+ this.images = {};
+ }
+ },
},
beforeDestroy() {
this.editor.dispose();
@@ -310,11 +330,13 @@ export default {
></div>
<content-viewer
v-if="showContentViewer"
- :content="file.content || file.raw"
+ :content="content"
+ :images="images"
:path="file.rawPath || file.path"
:file-path="file.path"
:file-size="file.size"
:project-path="file.projectId"
+ :commit-sha="currentBranchCommit"
:type="fileType"
/>
<diff-viewer
diff --git a/app/assets/javascripts/ide/components/resizable_panel.vue b/app/assets/javascripts/ide/components/resizable_panel.vue
index 7277fcb7617..86a4622401c 100644
--- a/app/assets/javascripts/ide/components/resizable_panel.vue
+++ b/app/assets/javascripts/ide/components/resizable_panel.vue
@@ -1,5 +1,5 @@
<script>
-import { mapActions, mapState } from 'vuex';
+import { mapActions } from 'vuex';
import PanelResizer from '~/vue_shared/components/panel_resizer.vue';
export default {
@@ -7,10 +7,6 @@ export default {
PanelResizer,
},
props: {
- collapsible: {
- type: Boolean,
- required: true,
- },
initialWidth: {
type: Number,
required: true,
@@ -31,11 +27,6 @@ export default {
};
},
computed: {
- ...mapState({
- collapsed(state) {
- return state[`${this.side}PanelCollapsed`];
- },
- }),
panelStyle() {
if (!this.collapsed) {
return {
@@ -47,33 +38,17 @@ export default {
},
},
methods: {
- ...mapActions(['setPanelCollapsedStatus', 'setResizingStatus']),
- toggleFullbarCollapsed() {
- if (this.collapsed && this.collapsible) {
- this.setPanelCollapsedStatus({
- side: this.side,
- collapsed: !this.collapsed,
- });
- }
- },
+ ...mapActions(['setResizingStatus']),
},
maxSize: window.innerWidth / 2,
};
</script>
<template>
- <div
- :class="{
- 'is-collapsed': collapsed && collapsible,
- }"
- :style="panelStyle"
- class="multi-file-commit-panel"
- @click="toggleFullbarCollapsed"
- >
+ <div :style="panelStyle" class="multi-file-commit-panel">
<slot></slot>
<panel-resizer
:size.sync="width"
- :enabled="!collapsed"
:start-size="initialWidth"
:min-size="minSize"
:max-size="$options.maxSize"
diff --git a/app/assets/javascripts/ide/constants.js b/app/assets/javascripts/ide/constants.js
index fa2672aaece..ae8550cba76 100644
--- a/app/assets/javascripts/ide/constants.js
+++ b/app/assets/javascripts/ide/constants.js
@@ -78,6 +78,7 @@ export const commitItemIconMap = {
export const modalTypes = {
rename: 'rename',
tree: 'tree',
+ blob: 'blob',
};
export const commitActionTypes = {
diff --git a/app/assets/javascripts/ide/eventhub.js b/app/assets/javascripts/ide/eventhub.js
index 0948c2e5352..e31806ad199 100644
--- a/app/assets/javascripts/ide/eventhub.js
+++ b/app/assets/javascripts/ide/eventhub.js
@@ -1,3 +1,3 @@
-import Vue from 'vue';
+import createEventHub from '~/helpers/event_hub_factory';
-export default new Vue();
+export default createEventHub();
diff --git a/app/assets/javascripts/ide/lib/diff/diff.js b/app/assets/javascripts/ide/lib/diff/diff.js
index 9b7ed68b893..29e29d7fcd3 100644
--- a/app/assets/javascripts/ide/lib/diff/diff.js
+++ b/app/assets/javascripts/ide/lib/diff/diff.js
@@ -14,13 +14,12 @@ export const computeDiff = (originalContent, newContent) => {
endLineNumber: lineNumber + change.count - 1,
});
} else if ('added' in change || 'removed' in change) {
- acc.push(
- Object.assign({}, change, {
- lineNumber,
- modified: undefined,
- endLineNumber: lineNumber + change.count - 1,
- }),
- );
+ acc.push({
+ ...change,
+ lineNumber,
+ modified: undefined,
+ endLineNumber: lineNumber + change.count - 1,
+ });
}
if (!change.removed) {
diff --git a/app/assets/javascripts/ide/lib/editor.js b/app/assets/javascripts/ide/lib/editor.js
index 3aff4d30d81..25224abd77c 100644
--- a/app/assets/javascripts/ide/lib/editor.js
+++ b/app/assets/javascripts/ide/lib/editor.js
@@ -7,8 +7,10 @@ import Disposable from './common/disposable';
import ModelManager from './common/model_manager';
import editorOptions, { defaultEditorOptions } from './editor_options';
import { themes } from './themes';
+import languages from './languages';
import keymap from './keymap.json';
import { clearDomElement } from '~/editor/utils';
+import { registerLanguages } from '../utils';
function setupThemes() {
themes.forEach(theme => {
@@ -37,6 +39,7 @@ export default class Editor {
};
setupThemes();
+ registerLanguages(...languages);
this.debouncedUpdate = debounce(() => {
this.updateDimensions();
diff --git a/app/assets/javascripts/ide/lib/languages/index.js b/app/assets/javascripts/ide/lib/languages/index.js
new file mode 100644
index 00000000000..0c85a1104fc
--- /dev/null
+++ b/app/assets/javascripts/ide/lib/languages/index.js
@@ -0,0 +1,5 @@
+import vue from './vue';
+
+const languages = [vue];
+
+export default languages;
diff --git a/app/assets/javascripts/ide/lib/languages/vue.js b/app/assets/javascripts/ide/lib/languages/vue.js
new file mode 100644
index 00000000000..b9ff5c5d776
--- /dev/null
+++ b/app/assets/javascripts/ide/lib/languages/vue.js
@@ -0,0 +1,306 @@
+/*---------------------------------------------------------------------------------------------
+ * Copyright (c) Microsoft Corporation. All rights reserved.
+ * Licensed under the MIT License. See https://github.com/microsoft/monaco-languages/blob/master/LICENSE.md
+ *--------------------------------------------------------------------------------------------*/
+
+// Based on handlebars template in https://github.com/microsoft/monaco-languages/blob/master/src/handlebars/handlebars.ts
+// Look for "vuejs template attributes" in this file for Vue specific syntax.
+
+import { languages } from 'monaco-editor';
+
+/* eslint-disable no-useless-escape */
+/* eslint-disable @gitlab/require-i18n-strings */
+
+const EMPTY_ELEMENTS = [
+ 'area',
+ 'base',
+ 'br',
+ 'col',
+ 'embed',
+ 'hr',
+ 'img',
+ 'input',
+ 'keygen',
+ 'link',
+ 'menuitem',
+ 'meta',
+ 'param',
+ 'source',
+ 'track',
+ 'wbr',
+];
+
+const conf = {
+ wordPattern: /(-?\d*\.\d\w*)|([^\`\~\!\@\$\^\&\*\(\)\=\+\[\{\]\}\\\|\;\:\'\"\,\.\<\>\/\s]+)/g,
+
+ comments: {
+ blockComment: ['{{!--', '--}}'],
+ },
+
+ brackets: [['<!--', '-->'], ['<', '>'], ['{{', '}}'], ['{', '}'], ['(', ')']],
+
+ autoClosingPairs: [
+ { open: '{', close: '}' },
+ { open: '[', close: ']' },
+ { open: '(', close: ')' },
+ { open: '"', close: '"' },
+ { open: "'", close: "'" },
+ ],
+
+ surroundingPairs: [
+ { open: '<', close: '>' },
+ { open: '"', close: '"' },
+ { open: "'", close: "'" },
+ ],
+
+ onEnterRules: [
+ {
+ beforeText: new RegExp(
+ `<(?!(?:${EMPTY_ELEMENTS.join('|')}))(\\w[\\w\\d]*)([^/>]*(?!/)>)[^<]*$`,
+ 'i',
+ ),
+ afterText: /^<\/(\w[\w\d]*)\s*>$/i,
+ action: { indentAction: languages.IndentAction.IndentOutdent },
+ },
+ {
+ beforeText: new RegExp(
+ `<(?!(?:${EMPTY_ELEMENTS.join('|')}))(\\w[\\w\\d]*)([^/>]*(?!/)>)[^<]*$`,
+ 'i',
+ ),
+ action: { indentAction: languages.IndentAction.Indent },
+ },
+ ],
+};
+
+const language = {
+ defaultToken: '',
+ tokenPostfix: '',
+ // ignoreCase: true,
+
+ // The main tokenizer for our languages
+ tokenizer: {
+ root: [
+ [/\{\{/, { token: '@rematch', switchTo: '@handlebarsInSimpleState.root' }],
+ [/<!DOCTYPE/, 'metatag.html', '@doctype'],
+ [/<!--/, 'comment.html', '@comment'],
+ [/(<)([\w]+)(\/>)/, ['delimiter.html', 'tag.html', 'delimiter.html']],
+ [/(<)(script)/, ['delimiter.html', { token: 'tag.html', next: '@script' }]],
+ [/(<)(style)/, ['delimiter.html', { token: 'tag.html', next: '@style' }]],
+ [/(<)([:\w]+)/, ['delimiter.html', { token: 'tag.html', next: '@otherTag' }]],
+ [/(<\/)([\w]+)/, ['delimiter.html', { token: 'tag.html', next: '@otherTag' }]],
+ [/</, 'delimiter.html'],
+ [/\{/, 'delimiter.html'],
+ [/[^<{]+/], // text
+ ],
+
+ doctype: [
+ [/\{\{/, { token: '@rematch', switchTo: '@handlebarsInSimpleState.comment' }],
+ [/[^>]+/, 'metatag.content.html'],
+ [/>/, 'metatag.html', '@pop'],
+ ],
+
+ comment: [
+ [/\{\{/, { token: '@rematch', switchTo: '@handlebarsInSimpleState.comment' }],
+ [/-->/, 'comment.html', '@pop'],
+ [/[^-]+/, 'comment.content.html'],
+ [/./, 'comment.content.html'],
+ ],
+
+ otherTag: [
+ [/\{\{/, { token: '@rematch', switchTo: '@handlebarsInSimpleState.otherTag' }],
+ [/\/?>/, 'delimiter.html', '@pop'],
+
+ // -- BEGIN vuejs template attributes
+ [/(v-|@|:)[\w\-\.\:\[\]]+="([^"]*)"/, 'variable'],
+ [/(v-|@|:)[\w\-\.\:\[\]]+='([^']*)'/, 'variable'],
+
+ [/"([^"]*)"/, 'attribute.value'],
+ [/'([^']*)'/, 'attribute.value'],
+
+ [/[\w\-\.\:\[\]]+/, 'attribute.name'],
+ // -- END vuejs template attributes
+
+ [/=/, 'delimiter'],
+ [/[ \t\r\n]+/], // whitespace
+ ],
+
+ // -- BEGIN <script> tags handling
+
+ // After <script
+ script: [
+ [/\{\{/, { token: '@rematch', switchTo: '@handlebarsInSimpleState.script' }],
+ [/type/, 'attribute.name', '@scriptAfterType'],
+ [/"([^"]*)"/, 'attribute.value'],
+ [/'([^']*)'/, 'attribute.value'],
+ [/[\w\-]+/, 'attribute.name'],
+ [/=/, 'delimiter'],
+ [
+ />/,
+ {
+ token: 'delimiter.html',
+ next: '@scriptEmbedded.text/javascript',
+ nextEmbedded: 'text/javascript',
+ },
+ ],
+ [/[ \t\r\n]+/], // whitespace
+ [
+ /(<\/)(script\s*)(>)/,
+ ['delimiter.html', 'tag.html', { token: 'delimiter.html', next: '@pop' }],
+ ],
+ ],
+
+ // After <script ... type
+ scriptAfterType: [
+ [/\{\{/, { token: '@rematch', switchTo: '@handlebarsInSimpleState.scriptAfterType' }],
+ [/=/, 'delimiter', '@scriptAfterTypeEquals'],
+ [
+ />/,
+ {
+ token: 'delimiter.html',
+ next: '@scriptEmbedded.text/javascript',
+ nextEmbedded: 'text/javascript',
+ },
+ ], // cover invalid e.g. <script type>
+ [/[ \t\r\n]+/], // whitespace
+ [/<\/script\s*>/, { token: '@rematch', next: '@pop' }],
+ ],
+
+ // After <script ... type =
+ scriptAfterTypeEquals: [
+ [/\{\{/, { token: '@rematch', switchTo: '@handlebarsInSimpleState.scriptAfterTypeEquals' }],
+ [/"([^"]*)"/, { token: 'attribute.value', switchTo: '@scriptWithCustomType.$1' }],
+ [/'([^']*)'/, { token: 'attribute.value', switchTo: '@scriptWithCustomType.$1' }],
+ [
+ />/,
+ {
+ token: 'delimiter.html',
+ next: '@scriptEmbedded.text/javascript',
+ nextEmbedded: 'text/javascript',
+ },
+ ], // cover invalid e.g. <script type=>
+ [/[ \t\r\n]+/], // whitespace
+ [/<\/script\s*>/, { token: '@rematch', next: '@pop' }],
+ ],
+
+ // After <script ... type = $S2
+ scriptWithCustomType: [
+ [
+ /\{\{/,
+ { token: '@rematch', switchTo: '@handlebarsInSimpleState.scriptWithCustomType.$S2' },
+ ],
+ [/>/, { token: 'delimiter.html', next: '@scriptEmbedded.$S2', nextEmbedded: '$S2' }],
+ [/"([^"]*)"/, 'attribute.value'],
+ [/'([^']*)'/, 'attribute.value'],
+ [/[\w\-]+/, 'attribute.name'],
+ [/=/, 'delimiter'],
+ [/[ \t\r\n]+/], // whitespace
+ [/<\/script\s*>/, { token: '@rematch', next: '@pop' }],
+ ],
+
+ scriptEmbedded: [
+ [
+ /\{\{/,
+ {
+ token: '@rematch',
+ switchTo: '@handlebarsInEmbeddedState.scriptEmbedded.$S2',
+ nextEmbedded: '@pop',
+ },
+ ],
+ [/<\/script/, { token: '@rematch', next: '@pop', nextEmbedded: '@pop' }],
+ ],
+
+ // -- END <script> tags handling
+
+ // -- BEGIN <style> tags handling
+
+ // After <style
+ style: [
+ [/\{\{/, { token: '@rematch', switchTo: '@handlebarsInSimpleState.style' }],
+ [/type/, 'attribute.name', '@styleAfterType'],
+ [/"([^"]*)"/, 'attribute.value'],
+ [/'([^']*)'/, 'attribute.value'],
+ [/[\w\-]+/, 'attribute.name'],
+ [/=/, 'delimiter'],
+ [/>/, { token: 'delimiter.html', next: '@styleEmbedded.text/css', nextEmbedded: 'text/css' }],
+ [/[ \t\r\n]+/], // whitespace
+ [
+ /(<\/)(style\s*)(>)/,
+ ['delimiter.html', 'tag.html', { token: 'delimiter.html', next: '@pop' }],
+ ],
+ ],
+
+ // After <style ... type
+ styleAfterType: [
+ [/\{\{/, { token: '@rematch', switchTo: '@handlebarsInSimpleState.styleAfterType' }],
+ [/=/, 'delimiter', '@styleAfterTypeEquals'],
+ [/>/, { token: 'delimiter.html', next: '@styleEmbedded.text/css', nextEmbedded: 'text/css' }], // cover invalid e.g. <style type>
+ [/[ \t\r\n]+/], // whitespace
+ [/<\/style\s*>/, { token: '@rematch', next: '@pop' }],
+ ],
+
+ // After <style ... type =
+ styleAfterTypeEquals: [
+ [/\{\{/, { token: '@rematch', switchTo: '@handlebarsInSimpleState.styleAfterTypeEquals' }],
+ [/"([^"]*)"/, { token: 'attribute.value', switchTo: '@styleWithCustomType.$1' }],
+ [/'([^']*)'/, { token: 'attribute.value', switchTo: '@styleWithCustomType.$1' }],
+ [/>/, { token: 'delimiter.html', next: '@styleEmbedded.text/css', nextEmbedded: 'text/css' }], // cover invalid e.g. <style type=>
+ [/[ \t\r\n]+/], // whitespace
+ [/<\/style\s*>/, { token: '@rematch', next: '@pop' }],
+ ],
+
+ // After <style ... type = $S2
+ styleWithCustomType: [
+ [/\{\{/, { token: '@rematch', switchTo: '@handlebarsInSimpleState.styleWithCustomType.$S2' }],
+ [/>/, { token: 'delimiter.html', next: '@styleEmbedded.$S2', nextEmbedded: '$S2' }],
+ [/"([^"]*)"/, 'attribute.value'],
+ [/'([^']*)'/, 'attribute.value'],
+ [/[\w\-]+/, 'attribute.name'],
+ [/=/, 'delimiter'],
+ [/[ \t\r\n]+/], // whitespace
+ [/<\/style\s*>/, { token: '@rematch', next: '@pop' }],
+ ],
+
+ styleEmbedded: [
+ [
+ /\{\{/,
+ {
+ token: '@rematch',
+ switchTo: '@handlebarsInEmbeddedState.styleEmbedded.$S2',
+ nextEmbedded: '@pop',
+ },
+ ],
+ [/<\/style/, { token: '@rematch', next: '@pop', nextEmbedded: '@pop' }],
+ ],
+
+ // -- END <style> tags handling
+
+ handlebarsInSimpleState: [
+ [/\{\{\{?/, 'delimiter.handlebars'],
+ [/\}\}\}?/, { token: 'delimiter.handlebars', switchTo: '@$S2.$S3' }],
+ { include: 'handlebarsRoot' },
+ ],
+
+ handlebarsInEmbeddedState: [
+ [/\{\{\{?/, 'delimiter.handlebars'],
+ [/\}\}\}?/, { token: 'delimiter.handlebars', switchTo: '@$S2.$S3', nextEmbedded: '$S3' }],
+ { include: 'handlebarsRoot' },
+ ],
+
+ handlebarsRoot: [
+ [/"[^"]*"/, 'string.handlebars'],
+ [/[#/][^\s}]+/, 'keyword.helper.handlebars'],
+ [/else\b/, 'keyword.helper.handlebars'],
+ [/[\s]+/],
+ [/[^}]/, 'variable.parameter.handlebars'],
+ ],
+ },
+};
+
+export default {
+ id: 'vue',
+ extensions: ['.vue'],
+ aliases: ['Vue', 'vue'],
+ mimetypes: ['text/x-vue-template'],
+ conf,
+ language,
+};
diff --git a/app/assets/javascripts/ide/lib/themes/index.js b/app/assets/javascripts/ide/lib/themes/index.js
index 6ed9f6679a4..bb5be50576c 100644
--- a/app/assets/javascripts/ide/lib/themes/index.js
+++ b/app/assets/javascripts/ide/lib/themes/index.js
@@ -1,5 +1,9 @@
import white from './white';
import dark from './dark';
+import monokai from './monokai';
+import solarizedLight from './solarized_light';
+import solarizedDark from './solarized_dark';
+import none from './none';
export const themes = [
{
@@ -10,6 +14,22 @@ export const themes = [
name: 'dark',
data: dark,
},
+ {
+ name: 'solarized-light',
+ data: solarizedLight,
+ },
+ {
+ name: 'solarized-dark',
+ data: solarizedDark,
+ },
+ {
+ name: 'monokai',
+ data: monokai,
+ },
+ {
+ name: 'none',
+ data: none,
+ },
];
export const DEFAULT_THEME = 'white';
diff --git a/app/assets/javascripts/ide/lib/themes/monokai.js b/app/assets/javascripts/ide/lib/themes/monokai.js
new file mode 100644
index 00000000000..d7636574754
--- /dev/null
+++ b/app/assets/javascripts/ide/lib/themes/monokai.js
@@ -0,0 +1,169 @@
+/*
+
+https://github.com/brijeshb42/monaco-themes/blob/master/themes/Tomorrow-Night.json
+
+The MIT License (MIT)
+
+Copyright (c) Brijesh Bittu
+
+Permission is hereby granted, free of charge, to any person obtaining a copy
+of this software and associated documentation files (the "Software"), to deal
+in the Software without restriction, including without limitation the rights
+to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
+copies of the Software, and to permit persons to whom the Software is
+furnished to do so, subject to the following conditions:
+
+The above copyright notice and this permission notice shall be included in
+all copies or substantial portions of the Software.
+
+THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
+OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
+THE SOFTWARE.
+
+*/
+
+export default {
+ base: 'vs-dark',
+ inherit: true,
+ rules: [
+ {
+ foreground: '75715e',
+ token: 'comment',
+ },
+ {
+ foreground: 'e6db74',
+ token: 'string',
+ },
+ {
+ foreground: 'ae81ff',
+ token: 'constant.numeric',
+ },
+ {
+ foreground: 'ae81ff',
+ token: 'constant.language',
+ },
+ {
+ foreground: 'ae81ff',
+ token: 'constant.character',
+ },
+ {
+ foreground: 'ae81ff',
+ token: 'constant.other',
+ },
+ {
+ foreground: 'f92672',
+ token: 'keyword',
+ },
+ {
+ foreground: 'f92672',
+ token: 'storage',
+ },
+ {
+ foreground: '66d9ef',
+ fontStyle: 'italic',
+ token: 'storage.type',
+ },
+ {
+ foreground: 'a6e22e',
+ fontStyle: 'underline',
+ token: 'entity.name.class',
+ },
+ {
+ foreground: 'a6e22e',
+ // eslint-disable-next-line @gitlab/require-i18n-strings
+ fontStyle: 'italic underline',
+ token: 'entity.other.inherited-class',
+ },
+ {
+ foreground: 'a6e22e',
+ token: 'entity.name.function',
+ },
+ {
+ foreground: 'fd971f',
+ fontStyle: 'italic',
+ token: 'variable.parameter',
+ },
+ {
+ foreground: 'f92672',
+ token: 'entity.name.tag',
+ },
+ {
+ foreground: 'a6e22e',
+ token: 'entity.other.attribute-name',
+ },
+ {
+ foreground: '66d9ef',
+ token: 'support.function',
+ },
+ {
+ foreground: '66d9ef',
+ token: 'support.constant',
+ },
+ {
+ foreground: '66d9ef',
+ fontStyle: 'italic',
+ token: 'support.type',
+ },
+ {
+ foreground: '66d9ef',
+ fontStyle: 'italic',
+ token: 'support.class',
+ },
+ {
+ foreground: 'f8f8f0',
+ background: 'f92672',
+ token: 'invalid',
+ },
+ {
+ foreground: 'f8f8f0',
+ background: 'ae81ff',
+ token: 'invalid.deprecated',
+ },
+ {
+ foreground: 'cfcfc2',
+ token: 'meta.structure.dictionary.json string.quoted.double.json',
+ },
+ {
+ foreground: '75715e',
+ token: 'meta.diff',
+ },
+ {
+ foreground: '75715e',
+ token: 'meta.diff.header',
+ },
+ {
+ foreground: 'f92672',
+ token: 'markup.deleted',
+ },
+ {
+ foreground: 'a6e22e',
+ token: 'markup.inserted',
+ },
+ {
+ foreground: 'e6db74',
+ token: 'markup.changed',
+ },
+ {
+ foreground: 'ae81ffa0',
+ token: 'constant.numeric.line-number.find-in-files - match',
+ },
+ {
+ foreground: 'e6db74',
+ token: 'entity.name.filename.find-in-files',
+ },
+ ],
+ colors: {
+ 'editor.foreground': '#F8F8F2',
+ 'editor.background': '#272822',
+ 'editor.selectionBackground': '#49483E',
+ 'editor.lineHighlightBackground': '#3E3D32',
+ 'editorCursor.foreground': '#F8F8F0',
+ 'editorWhitespace.foreground': '#3B3A32',
+ 'editorIndentGuide.activeBackground': '#9D550FB0',
+ 'editor.selectionHighlightBorder': '#222218',
+ },
+};
diff --git a/app/assets/javascripts/ide/lib/themes/none.js b/app/assets/javascripts/ide/lib/themes/none.js
new file mode 100644
index 00000000000..8e722c4ff88
--- /dev/null
+++ b/app/assets/javascripts/ide/lib/themes/none.js
@@ -0,0 +1,17 @@
+export default {
+ base: 'vs',
+ inherit: false,
+ rules: [],
+ colors: {
+ 'editor.foreground': '#2e2e2e',
+ 'editor.selectionBackground': '#aad6f8',
+ 'editor.lineHighlightBackground': '#fffeeb',
+ 'editorCursor.foreground': '#666666',
+ 'editorWhitespace.foreground': '#bbbbbb',
+
+ 'editorLineNumber.foreground': '#cccccc',
+ 'diffEditor.insertedTextBackground': '#a0f5b420',
+ 'diffEditor.removedTextBackground': '#f9d7dc20',
+ 'editorIndentGuide.activeBackground': '#cccccc',
+ },
+};
diff --git a/app/assets/javascripts/ide/lib/themes/solarized_dark.js b/app/assets/javascripts/ide/lib/themes/solarized_dark.js
new file mode 100644
index 00000000000..3c9414b9dc9
--- /dev/null
+++ b/app/assets/javascripts/ide/lib/themes/solarized_dark.js
@@ -0,0 +1,1110 @@
+/*
+
+https://github.com/brijeshb42/monaco-themes/blob/master/themes/Solarized-dark.json
+
+The MIT License (MIT)
+
+Copyright (c) Brijesh Bittu
+
+Permission is hereby granted, free of charge, to any person obtaining a copy
+of this software and associated documentation files (the "Software"), to deal
+in the Software without restriction, including without limitation the rights
+to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
+copies of the Software, and to permit persons to whom the Software is
+furnished to do so, subject to the following conditions:
+
+The above copyright notice and this permission notice shall be included in
+all copies or substantial portions of the Software.
+
+THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
+OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
+THE SOFTWARE.
+
+*/
+export default {
+ base: 'vs-dark',
+ inherit: true,
+ rules: [
+ {
+ foreground: '586e75',
+ token: 'comment',
+ },
+ {
+ foreground: '2aa198',
+ token: 'string',
+ },
+ {
+ foreground: '586e75',
+ token: 'string',
+ },
+ {
+ foreground: 'dc322f',
+ token: 'string.regexp',
+ },
+ {
+ foreground: 'd33682',
+ token: 'constant.numeric',
+ },
+ {
+ foreground: '268bd2',
+ token: 'variable.language',
+ },
+ {
+ foreground: '268bd2',
+ token: 'variable.other',
+ },
+ {
+ foreground: '859900',
+ token: 'keyword',
+ },
+ {
+ foreground: '859900',
+ token: 'storage',
+ },
+ {
+ foreground: '268bd2',
+ token: 'entity.name.class',
+ },
+ {
+ foreground: '268bd2',
+ token: 'entity.name.type.class',
+ },
+ {
+ foreground: '268bd2',
+ token: 'entity.name.function',
+ },
+ {
+ foreground: '859900',
+ token: 'punctuation.definition.variable',
+ },
+ {
+ foreground: 'dc322f',
+ token: 'punctuation.section.embedded.begin',
+ },
+ {
+ foreground: 'dc322f',
+ token: 'punctuation.section.embedded.end',
+ },
+ {
+ foreground: 'b58900',
+ token: 'constant.language',
+ },
+ {
+ foreground: 'b58900',
+ token: 'meta.preprocessor',
+ },
+ {
+ foreground: 'dc322f',
+ token: 'support.function.construct',
+ },
+ {
+ foreground: 'dc322f',
+ token: 'keyword.other.new',
+ },
+ {
+ foreground: 'cb4b16',
+ token: 'constant.character',
+ },
+ {
+ foreground: 'cb4b16',
+ token: 'constant.other',
+ },
+ {
+ foreground: '268bd2',
+ fontStyle: 'bold',
+ token: 'entity.name.tag',
+ },
+ {
+ foreground: '586e75',
+ token: 'punctuation.definition.tag.html',
+ },
+ {
+ foreground: '586e75',
+ token: 'punctuation.definition.tag.begin',
+ },
+ {
+ foreground: '586e75',
+ token: 'punctuation.definition.tag.end',
+ },
+ {
+ foreground: '93a1a1',
+ token: 'entity.other.attribute-name',
+ },
+ {
+ foreground: '268bd2',
+ token: 'support.function',
+ },
+ {
+ foreground: 'dc322f',
+ token: 'punctuation.separator.continuation',
+ },
+ {
+ foreground: '859900',
+ token: 'support.type',
+ },
+ {
+ foreground: '859900',
+ token: 'support.class',
+ },
+ {
+ foreground: 'cb4b16',
+ token: 'support.type.exception',
+ },
+ {
+ foreground: 'cb4b16',
+ token: 'keyword.other.special-method',
+ },
+ {
+ foreground: '2aa198',
+ token: 'string.quoted.double',
+ },
+ {
+ foreground: '2aa198',
+ token: 'string.quoted.single',
+ },
+ {
+ foreground: 'dc322f',
+ token: 'punctuation.definition.string.begin',
+ },
+ {
+ foreground: 'dc322f',
+ token: 'punctuation.definition.string.end',
+ },
+ {
+ foreground: 'b58900',
+ token: 'entity.name.tag.css',
+ },
+ {
+ foreground: 'b58900',
+ token: 'support.type.property-name.css',
+ },
+ {
+ foreground: 'b58900',
+ token: 'meta.property-name.css',
+ },
+ {
+ foreground: 'dc322f',
+ token: 'source.css',
+ },
+ {
+ foreground: '586e75',
+ token: 'meta.selector.css',
+ },
+ {
+ foreground: '6c71c4',
+ token: 'punctuation.section.property-list.css',
+ },
+ {
+ foreground: '2aa198',
+ token: 'meta.property-value.css constant.numeric.css',
+ },
+ {
+ foreground: '2aa198',
+ token: 'keyword.other.unit.css',
+ },
+ {
+ foreground: '2aa198',
+ token: 'constant.other.color.rgb-value.css',
+ },
+ {
+ foreground: '2aa198',
+ token: 'meta.property-value.css',
+ },
+ {
+ foreground: 'dc322f',
+ token: 'keyword.other.important.css',
+ },
+ {
+ foreground: '2aa198',
+ token: 'support.constant.color',
+ },
+ {
+ foreground: '859900',
+ token: 'entity.name.tag.css',
+ },
+ {
+ foreground: '586e75',
+ token: 'punctuation.separator.key-value.css',
+ },
+ {
+ foreground: '586e75',
+ token: 'punctuation.terminator.rule.css',
+ },
+ {
+ foreground: '268bd2',
+ token: 'entity.other.attribute-name.class.css',
+ },
+ {
+ foreground: 'cb4b16',
+ token: 'entity.other.attribute-name.pseudo-element.css',
+ },
+ {
+ foreground: 'cb4b16',
+ token: 'entity.other.attribute-name.pseudo-class.css',
+ },
+ {
+ foreground: '268bd2',
+ token: 'entity.other.attribute-name.id.css',
+ },
+ {
+ foreground: 'b58900',
+ token: 'meta.function.js',
+ },
+ {
+ foreground: 'b58900',
+ token: 'entity.name.function.js',
+ },
+ {
+ foreground: 'b58900',
+ token: 'support.function.dom.js',
+ },
+ {
+ foreground: 'b58900',
+ token: 'text.html.basic source.js.embedded.html',
+ },
+ {
+ foreground: '268bd2',
+ token: 'storage.type.function.js',
+ },
+ {
+ foreground: '2aa198',
+ token: 'constant.numeric.js',
+ },
+ {
+ foreground: '268bd2',
+ token: 'meta.brace.square.js',
+ },
+ {
+ foreground: '268bd2',
+ token: 'storage.type.js',
+ },
+ {
+ foreground: '93a1a1',
+ token: 'meta.brace.round',
+ },
+ {
+ foreground: '93a1a1',
+ token: 'punctuation.definition.parameters.begin.js',
+ },
+ {
+ foreground: '93a1a1',
+ token: 'punctuation.definition.parameters.end.js',
+ },
+ {
+ foreground: '268bd2',
+ token: 'meta.brace.curly.js',
+ },
+ {
+ foreground: '93a1a1',
+ fontStyle: 'italic',
+ token: 'entity.name.tag.doctype.html',
+ },
+ {
+ foreground: '93a1a1',
+ fontStyle: 'italic',
+ token: 'meta.tag.sgml.html',
+ },
+ {
+ foreground: '93a1a1',
+ fontStyle: 'italic',
+ token: 'string.quoted.double.doctype.identifiers-and-DTDs.html',
+ },
+ {
+ foreground: '839496',
+ fontStyle: 'italic',
+ token: 'comment.block.html',
+ },
+ {
+ fontStyle: 'italic',
+ token: 'entity.name.tag.script.html',
+ },
+ {
+ foreground: '2aa198',
+ token: 'source.css.embedded.html string.quoted.double.html',
+ },
+ {
+ foreground: 'cb4b16',
+ fontStyle: 'bold',
+ token: 'text.html.ruby',
+ },
+ {
+ foreground: '657b83',
+ token: 'text.html.basic meta.tag.other.html',
+ },
+ {
+ foreground: '657b83',
+ token: 'text.html.basic meta.tag.any.html',
+ },
+ {
+ foreground: '657b83',
+ token: 'text.html.basic meta.tag.block.any',
+ },
+ {
+ foreground: '657b83',
+ token: 'text.html.basic meta.tag.inline.any',
+ },
+ {
+ foreground: '657b83',
+ token: 'text.html.basic meta.tag.structure.any.html',
+ },
+ {
+ foreground: '657b83',
+ token: 'text.html.basic source.js.embedded.html',
+ },
+ {
+ foreground: '657b83',
+ token: 'punctuation.separator.key-value.html',
+ },
+ {
+ foreground: '657b83',
+ token: 'text.html.basic entity.other.attribute-name.html',
+ },
+ {
+ foreground: '2aa198',
+ token: 'text.html.basic meta.tag.structure.any.html punctuation.definition.string.begin.html',
+ },
+ {
+ foreground: '2aa198',
+ token: 'punctuation.definition.string.begin.html',
+ },
+ {
+ foreground: '2aa198',
+ token: 'punctuation.definition.string.end.html',
+ },
+ {
+ foreground: '268bd2',
+ fontStyle: 'bold',
+ token: 'entity.name.tag.block.any.html',
+ },
+ {
+ fontStyle: 'italic',
+ token: 'source.css.embedded.html entity.name.tag.style.html',
+ },
+ {
+ foreground: '839496',
+ fontStyle: 'italic',
+ token: 'source.css.embedded.html',
+ },
+ {
+ foreground: '839496',
+ fontStyle: 'italic',
+ token: 'comment.block.html',
+ },
+ {
+ foreground: '268bd2',
+ token: 'punctuation.definition.variable.ruby',
+ },
+ {
+ foreground: '657b83',
+ token: 'meta.function.method.with-arguments.ruby',
+ },
+ {
+ foreground: '2aa198',
+ token: 'variable.language.ruby',
+ },
+ {
+ foreground: '268bd2',
+ token: 'entity.name.function.ruby',
+ },
+ {
+ foreground: '859900',
+ fontStyle: 'bold',
+ token: 'keyword.control.ruby',
+ },
+ {
+ foreground: '859900',
+ fontStyle: 'bold',
+ token: 'keyword.control.def.ruby',
+ },
+ {
+ foreground: '859900',
+ token: 'keyword.control.class.ruby',
+ },
+ {
+ foreground: '859900',
+ token: 'meta.class.ruby',
+ },
+ {
+ foreground: 'b58900',
+ token: 'entity.name.type.class.ruby',
+ },
+ {
+ foreground: '859900',
+ token: 'keyword.control.ruby',
+ },
+ {
+ foreground: 'b58900',
+ token: 'support.class.ruby',
+ },
+ {
+ foreground: '859900',
+ token: 'keyword.other.special-method.ruby',
+ },
+ {
+ foreground: '2aa198',
+ token: 'constant.language.ruby',
+ },
+ {
+ foreground: '2aa198',
+ token: 'constant.numeric.ruby',
+ },
+ {
+ foreground: 'b58900',
+ token: 'variable.other.constant.ruby',
+ },
+ {
+ foreground: '2aa198',
+ token: 'constant.other.symbol.ruby',
+ },
+ {
+ foreground: 'dc322f',
+ token: 'punctuation.section.embedded.ruby',
+ },
+ {
+ foreground: 'dc322f',
+ token: 'punctuation.definition.string.begin.ruby',
+ },
+ {
+ foreground: 'dc322f',
+ token: 'punctuation.definition.string.end.ruby',
+ },
+ {
+ foreground: 'cb4b16',
+ token: 'keyword.other.special-method.ruby',
+ },
+ {
+ foreground: 'cb4b16',
+ token: 'keyword.control.import.include.php',
+ },
+ {
+ foreground: '839496',
+ token: 'text.html.ruby meta.tag.inline.any.html',
+ },
+ {
+ foreground: '2aa198',
+ token: 'text.html.ruby punctuation.definition.string.begin',
+ },
+ {
+ foreground: '2aa198',
+ token: 'text.html.ruby punctuation.definition.string.end',
+ },
+ {
+ foreground: '839496',
+ token: 'punctuation.definition.string.begin',
+ },
+ {
+ foreground: '839496',
+ token: 'punctuation.definition.string.end',
+ },
+ {
+ foreground: '839496',
+ token: 'support.class.php',
+ },
+ {
+ foreground: 'dc322f',
+ token: 'keyword.operator.index-start.php',
+ },
+ {
+ foreground: 'dc322f',
+ token: 'keyword.operator.index-end.php',
+ },
+ {
+ foreground: '586e75',
+ token: 'meta.array.php',
+ },
+ {
+ foreground: 'b58900',
+ token: 'meta.array.php support.function.construct.php',
+ },
+ {
+ foreground: 'b58900',
+ token: 'meta.array.empty.php support.function.construct.php',
+ },
+ {
+ foreground: 'b58900',
+ token: 'support.function.construct.php',
+ },
+ {
+ foreground: 'dc322f',
+ token: 'punctuation.definition.array.begin',
+ },
+ {
+ foreground: 'dc322f',
+ token: 'punctuation.definition.array.end',
+ },
+ {
+ foreground: '2aa198',
+ token: 'constant.numeric.php',
+ },
+ {
+ foreground: 'cb4b16',
+ token: 'keyword.other.new.php',
+ },
+ {
+ foreground: '839496',
+ token: 'keyword.operator.class',
+ },
+ {
+ foreground: '93a1a1',
+ token: 'variable.other.property.php',
+ },
+ {
+ foreground: 'b58900',
+ token: 'storage.modifier.extends.php',
+ },
+ {
+ foreground: 'b58900',
+ token: 'storage.type.class.php',
+ },
+ {
+ foreground: 'b58900',
+ token: 'keyword.operator.class.php',
+ },
+ {
+ foreground: '839496',
+ token: 'punctuation.terminator.expression.php',
+ },
+ {
+ foreground: '586e75',
+ token: 'meta.other.inherited-class.php',
+ },
+ {
+ foreground: '859900',
+ token: 'storage.type.php',
+ },
+ {
+ foreground: '93a1a1',
+ token: 'entity.name.function.php',
+ },
+ {
+ foreground: '859900',
+ token: 'support.function.construct.php',
+ },
+ {
+ foreground: '839496',
+ token: 'entity.name.type.class.php',
+ },
+ {
+ foreground: '839496',
+ token: 'meta.function-call.php',
+ },
+ {
+ foreground: '839496',
+ token: 'meta.function-call.static.php',
+ },
+ {
+ foreground: '839496',
+ token: 'meta.function-call.object.php',
+ },
+ {
+ foreground: '93a1a1',
+ token: 'keyword.other.phpdoc',
+ },
+ {
+ foreground: 'cb4b16',
+ token: 'source.php.embedded.block.html',
+ },
+ {
+ foreground: 'cb4b16',
+ token: 'storage.type.function.php',
+ },
+ {
+ foreground: '2aa198',
+ token: 'constant.numeric.c',
+ },
+ {
+ foreground: 'cb4b16',
+ token: 'meta.preprocessor.c.include',
+ },
+ {
+ foreground: 'cb4b16',
+ token: 'meta.preprocessor.macro.c',
+ },
+ {
+ foreground: 'cb4b16',
+ token: 'keyword.control.import.define.c',
+ },
+ {
+ foreground: 'cb4b16',
+ token: 'keyword.control.import.include.c',
+ },
+ {
+ foreground: 'cb4b16',
+ token: 'entity.name.function.preprocessor.c',
+ },
+ {
+ foreground: '2aa198',
+ token: 'meta.preprocessor.c.include string.quoted.other.lt-gt.include.c',
+ },
+ {
+ foreground: '2aa198',
+ token: 'meta.preprocessor.c.include punctuation.definition.string.begin.c',
+ },
+ {
+ foreground: '2aa198',
+ token: 'meta.preprocessor.c.include punctuation.definition.string.end.c',
+ },
+ {
+ foreground: '586e75',
+ token: 'support.function.C99.c',
+ },
+ {
+ foreground: '586e75',
+ token: 'support.function.any-method.c',
+ },
+ {
+ foreground: '586e75',
+ token: 'entity.name.function.c',
+ },
+ {
+ foreground: '2aa198',
+ token: 'punctuation.definition.string.begin.c',
+ },
+ {
+ foreground: '2aa198',
+ token: 'punctuation.definition.string.end.c',
+ },
+ {
+ foreground: 'b58900',
+ token: 'storage.type.c',
+ },
+ {
+ foreground: 'e0eddd',
+ background: 'b58900',
+ fontStyle: 'italic',
+ token: 'meta.diff',
+ },
+ {
+ foreground: 'e0eddd',
+ background: 'b58900',
+ fontStyle: 'italic',
+ token: 'meta.diff.header',
+ },
+ {
+ foreground: 'dc322f',
+ background: 'eee8d5',
+ token: 'markup.deleted',
+ },
+ {
+ foreground: 'cb4b16',
+ background: 'eee8d5',
+ token: 'markup.changed',
+ },
+ {
+ foreground: '219186',
+ background: 'eee8d5',
+ token: 'markup.inserted',
+ },
+ {
+ foreground: 'e0eddd',
+ background: 'b58900',
+ token: 'text.html.markdown meta.dummy.line-break',
+ },
+ {
+ foreground: '2aa198',
+ token: 'text.html.markdown markup.raw.inline',
+ },
+ {
+ foreground: '2aa198',
+ token: 'text.restructuredtext markup.raw',
+ },
+ {
+ foreground: 'dc322f',
+ token: 'other.package.exclude',
+ },
+ {
+ foreground: 'dc322f',
+ token: 'other.remove',
+ },
+ {
+ foreground: '2aa198',
+ token: 'other.add',
+ },
+ {
+ foreground: 'dc322f',
+ token: 'punctuation.section.group.tex',
+ },
+ {
+ foreground: 'dc322f',
+ token: 'punctuation.definition.arguments.begin.latex',
+ },
+ {
+ foreground: 'dc322f',
+ token: 'punctuation.definition.arguments.end.latex',
+ },
+ {
+ foreground: 'dc322f',
+ token: 'punctuation.definition.arguments.latex',
+ },
+ {
+ foreground: 'b58900',
+ token: 'meta.group.braces.tex',
+ },
+ {
+ foreground: 'b58900',
+ token: 'string.other.math.tex',
+ },
+ {
+ foreground: 'cb4b16',
+ token: 'variable.parameter.function.latex',
+ },
+ {
+ foreground: 'dc322f',
+ token: 'punctuation.definition.constant.math.tex',
+ },
+ {
+ foreground: '2aa198',
+ token: 'text.tex.latex constant.other.math.tex',
+ },
+ {
+ foreground: '2aa198',
+ token: 'constant.other.general.math.tex',
+ },
+ {
+ foreground: '2aa198',
+ token: 'constant.other.general.math.tex',
+ },
+ {
+ foreground: '2aa198',
+ token: 'constant.character.math.tex',
+ },
+ {
+ foreground: 'b58900',
+ token: 'string.other.math.tex',
+ },
+ {
+ foreground: 'dc322f',
+ token: 'punctuation.definition.string.begin.tex',
+ },
+ {
+ foreground: 'dc322f',
+ token: 'punctuation.definition.string.end.tex',
+ },
+ {
+ foreground: '2aa198',
+ token: 'keyword.control.label.latex',
+ },
+ {
+ foreground: '2aa198',
+ token: 'text.tex.latex constant.other.general.math.tex',
+ },
+ {
+ foreground: 'dc322f',
+ token: 'variable.parameter.definition.label.latex',
+ },
+ {
+ foreground: '859900',
+ token: 'support.function.be.latex',
+ },
+ {
+ foreground: 'cb4b16',
+ token: 'support.function.section.latex',
+ },
+ {
+ foreground: '2aa198',
+ token: 'support.function.general.tex',
+ },
+ {
+ fontStyle: 'italic',
+ token: 'punctuation.definition.comment.tex',
+ },
+ {
+ fontStyle: 'italic',
+ token: 'comment.line.percentage.tex',
+ },
+ {
+ foreground: '2aa198',
+ token: 'keyword.control.ref.latex',
+ },
+ {
+ foreground: '586e75',
+ token: 'string.quoted.double.block.python',
+ },
+ {
+ foreground: '859900',
+ token: 'storage.type.class.python',
+ },
+ {
+ foreground: '859900',
+ token: 'storage.type.function.python',
+ },
+ {
+ foreground: '859900',
+ token: 'storage.modifier.global.python',
+ },
+ {
+ foreground: 'cb4b16',
+ token: 'keyword.control.import.python',
+ },
+ {
+ foreground: 'cb4b16',
+ token: 'keyword.control.import.from.python',
+ },
+ {
+ foreground: 'b58900',
+ token: 'support.type.exception.python',
+ },
+ {
+ foreground: '859900',
+ token: 'support.function.builtin.shell',
+ },
+ {
+ foreground: 'cb4b16',
+ token: 'variable.other.normal.shell',
+ },
+ {
+ foreground: '268bd2',
+ token: 'source.shell',
+ },
+ {
+ foreground: '586e75',
+ token: 'meta.scope.for-in-loop.shell',
+ },
+ {
+ foreground: '586e75',
+ token: 'variable.other.loop.shell',
+ },
+ {
+ foreground: '859900',
+ token: 'punctuation.definition.string.end.shell',
+ },
+ {
+ foreground: '859900',
+ token: 'punctuation.definition.string.begin.shell',
+ },
+ {
+ foreground: '586e75',
+ token: 'meta.scope.case-block.shell',
+ },
+ {
+ foreground: '586e75',
+ token: 'meta.scope.case-body.shell',
+ },
+ {
+ foreground: 'dc322f',
+ token: 'punctuation.definition.logical-expression.shell',
+ },
+ {
+ fontStyle: 'italic',
+ token: 'comment.line.number-sign.shell',
+ },
+ {
+ foreground: 'cb4b16',
+ token: 'keyword.other.import.java',
+ },
+ {
+ foreground: '586e75',
+ token: 'storage.modifier.import.java',
+ },
+ {
+ foreground: 'b58900',
+ token: 'meta.class.java storage.modifier.java',
+ },
+ {
+ foreground: '586e75',
+ token: 'source.java comment.block',
+ },
+ {
+ foreground: '586e75',
+ token:
+ 'comment.block meta.documentation.tag.param.javadoc keyword.other.documentation.param.javadoc',
+ },
+ {
+ foreground: 'b58900',
+ token: 'punctuation.definition.variable.perl',
+ },
+ {
+ foreground: 'b58900',
+ token: 'variable.other.readwrite.global.perl',
+ },
+ {
+ foreground: 'b58900',
+ token: 'variable.other.predefined.perl',
+ },
+ {
+ foreground: 'b58900',
+ token: 'keyword.operator.comparison.perl',
+ },
+ {
+ foreground: '859900',
+ token: 'support.function.perl',
+ },
+ {
+ foreground: '586e75',
+ fontStyle: 'italic',
+ token: 'comment.line.number-sign.perl',
+ },
+ {
+ foreground: '2aa198',
+ token: 'punctuation.definition.string.begin.perl',
+ },
+ {
+ foreground: '2aa198',
+ token: 'punctuation.definition.string.end.perl',
+ },
+ {
+ foreground: 'dc322f',
+ token: 'constant.character.escape.perl',
+ },
+ {
+ foreground: '268bd2',
+ token: 'markup.heading.markdown',
+ },
+ {
+ foreground: '268bd2',
+ token: 'markup.heading.1.markdown',
+ },
+ {
+ foreground: '268bd2',
+ token: 'markup.heading.2.markdown',
+ },
+ {
+ foreground: '268bd2',
+ token: 'markup.heading.3.markdown',
+ },
+ {
+ foreground: '268bd2',
+ token: 'markup.heading.4.markdown',
+ },
+ {
+ foreground: '268bd2',
+ token: 'markup.heading.5.markdown',
+ },
+ {
+ foreground: '268bd2',
+ token: 'markup.heading.6.markdown',
+ },
+ {
+ foreground: '839496',
+ fontStyle: 'bold',
+ token: 'markup.bold.markdown',
+ },
+ {
+ foreground: '839496',
+ fontStyle: 'italic',
+ token: 'markup.italic.markdown',
+ },
+ {
+ foreground: 'dc322f',
+ token: 'punctuation.definition.bold.markdown',
+ },
+ {
+ foreground: 'dc322f',
+ token: 'punctuation.definition.italic.markdown',
+ },
+ {
+ foreground: 'dc322f',
+ token: 'punctuation.definition.raw.markdown',
+ },
+ {
+ foreground: 'b58900',
+ token: 'markup.list.unnumbered.markdown',
+ },
+ {
+ foreground: '859900',
+ token: 'markup.list.numbered.markdown',
+ },
+ {
+ foreground: '2aa198',
+ token: 'markup.raw.block.markdown',
+ },
+ {
+ foreground: '2aa198',
+ token: 'markup.raw.inline.markdown',
+ },
+ {
+ foreground: '6c71c4',
+ token: 'markup.quote.markdown',
+ },
+ {
+ foreground: '6c71c4',
+ token: 'punctuation.definition.blockquote.markdown',
+ },
+ {
+ foreground: 'd33682',
+ token: 'meta.separator.markdown',
+ },
+ {
+ foreground: '586e75',
+ fontStyle: 'italic',
+ token: 'meta.image.inline.markdown',
+ },
+ {
+ foreground: '586e75',
+ fontStyle: 'italic',
+ token: 'markup.underline.link.markdown',
+ },
+ {
+ foreground: '93a1a1',
+ token: 'string.other.link.title.markdown',
+ },
+ {
+ foreground: '93a1a1',
+ token: 'string.other.link.description.markdown',
+ },
+ {
+ foreground: '586e75',
+ token: 'punctuation.definition.link.markdown',
+ },
+ {
+ foreground: '586e75',
+ token: 'punctuation.definition.metadata.markdown',
+ },
+ {
+ foreground: '586e75',
+ token: 'punctuation.definition.string.begin.markdown',
+ },
+ {
+ foreground: '586e75',
+ token: 'punctuation.definition.string.end.markdown',
+ },
+ {
+ foreground: '586e75',
+ token: 'punctuation.definition.constant.markdown',
+ },
+ {
+ foreground: 'eee8d5',
+ background: 'eee8d5',
+ token: 'sublimelinter.notes',
+ },
+ {
+ foreground: '93a1a1',
+ background: '93a1a1',
+ token: 'sublimelinter.outline.illegal',
+ },
+ {
+ background: 'dc322f',
+ token: 'sublimelinter.underline.illegal',
+ },
+ {
+ foreground: '839496',
+ background: '839496',
+ token: 'sublimelinter.outline.warning',
+ },
+ {
+ background: 'b58900',
+ token: 'sublimelinter.underline.warning',
+ },
+ {
+ foreground: '657b83',
+ background: '657b83',
+ token: 'sublimelinter.outline.violation',
+ },
+ {
+ background: 'cb4b16',
+ token: 'sublimelinter.underline.violation',
+ },
+ ],
+ colors: {
+ 'editor.foreground': '#839496',
+ 'editor.background': '#002B36',
+ 'editor.selectionBackground': '#073642',
+ 'editor.lineHighlightBackground': '#073642',
+ 'editorCursor.foreground': '#819090',
+ 'editorWhitespace.foreground': '#073642',
+ },
+};
diff --git a/app/assets/javascripts/ide/lib/themes/solarized_light.js b/app/assets/javascripts/ide/lib/themes/solarized_light.js
new file mode 100644
index 00000000000..b7bfcf33b0f
--- /dev/null
+++ b/app/assets/javascripts/ide/lib/themes/solarized_light.js
@@ -0,0 +1,1101 @@
+/*
+
+https://github.com/brijeshb42/monaco-themes/blob/master/themes/Solarized-dark.json
+
+The MIT License (MIT)
+
+Copyright (c) Brijesh Bittu
+
+Permission is hereby granted, free of charge, to any person obtaining a copy
+of this software and associated documentation files (the "Software"), to deal
+in the Software without restriction, including without limitation the rights
+to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
+copies of the Software, and to permit persons to whom the Software is
+furnished to do so, subject to the following conditions:
+
+The above copyright notice and this permission notice shall be included in
+all copies or substantial portions of the Software.
+
+THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
+OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
+THE SOFTWARE.
+
+*/
+export default {
+ base: 'vs',
+ inherit: true,
+ rules: [
+ {
+ foreground: '93a1a1',
+ token: 'comment',
+ },
+ {
+ foreground: '2aa198',
+ token: 'string',
+ },
+ {
+ foreground: '586e75',
+ token: 'string',
+ },
+ {
+ foreground: 'dc322f',
+ token: 'string.regexp',
+ },
+ {
+ foreground: 'd33682',
+ token: 'constant.numeric',
+ },
+ {
+ foreground: '268bd2',
+ token: 'variable.language',
+ },
+ {
+ foreground: '268bd2',
+ token: 'variable.other',
+ },
+ {
+ foreground: '859900',
+ token: 'keyword',
+ },
+ {
+ foreground: '073642',
+ fontStyle: 'bold',
+ token: 'storage',
+ },
+ {
+ foreground: '268bd2',
+ token: 'entity.name.class',
+ },
+ {
+ foreground: '268bd2',
+ token: 'entity.name.type.class',
+ },
+ {
+ foreground: '268bd2',
+ token: 'entity.name.function',
+ },
+ {
+ foreground: '859900',
+ token: 'punctuation.definition.variable',
+ },
+ {
+ foreground: 'dc322f',
+ token: 'punctuation.section.embedded.begin',
+ },
+ {
+ foreground: 'dc322f',
+ token: 'punctuation.section.embedded.end',
+ },
+ {
+ foreground: 'b58900',
+ token: 'constant.language',
+ },
+ {
+ foreground: 'b58900',
+ token: 'meta.preprocessor',
+ },
+ {
+ foreground: 'dc322f',
+ token: 'support.function.construct',
+ },
+ {
+ foreground: 'dc322f',
+ token: 'keyword.other.new',
+ },
+ {
+ foreground: 'cb4b16',
+ token: 'constant.character',
+ },
+ {
+ foreground: 'cb4b16',
+ token: 'constant.other',
+ },
+ {
+ foreground: '268bd2',
+ fontStyle: 'bold',
+ token: 'entity.name.tag',
+ },
+ {
+ foreground: '93a1a1',
+ token: 'punctuation.definition.tag.html',
+ },
+ {
+ foreground: '93a1a1',
+ token: 'punctuation.definition.tag.begin',
+ },
+ {
+ foreground: '93a1a1',
+ token: 'punctuation.definition.tag.end',
+ },
+ {
+ foreground: '93a1a1',
+ token: 'entity.other.attribute-name',
+ },
+ {
+ foreground: '268bd2',
+ token: 'support.function',
+ },
+ {
+ foreground: 'dc322f',
+ token: 'punctuation.separator.continuation',
+ },
+ {
+ foreground: '859900',
+ token: 'support.type',
+ },
+ {
+ foreground: '859900',
+ token: 'support.class',
+ },
+ {
+ foreground: 'cb4b16',
+ token: 'support.type.exception',
+ },
+ {
+ foreground: 'cb4b16',
+ token: 'keyword.other.special-method',
+ },
+ {
+ foreground: '2aa198',
+ token: 'string.quoted.double',
+ },
+ {
+ foreground: '2aa198',
+ token: 'string.quoted.single',
+ },
+ {
+ foreground: 'dc322f',
+ token: 'punctuation.definition.string.begin',
+ },
+ {
+ foreground: 'dc322f',
+ token: 'punctuation.definition.string.end',
+ },
+ {
+ foreground: 'b58900',
+ token: 'entity.name.tag.css',
+ },
+ {
+ foreground: 'b58900',
+ token: 'support.type.property-name.css',
+ },
+ {
+ foreground: 'b58900',
+ token: 'meta.property-name.css',
+ },
+ {
+ foreground: 'dc322f',
+ token: 'source.css',
+ },
+ {
+ foreground: '586e75',
+ token: 'meta.selector.css',
+ },
+ {
+ foreground: '6c71c4',
+ token: 'punctuation.section.property-list.css',
+ },
+ {
+ foreground: '2aa198',
+ token: 'meta.property-value.css constant.numeric.css',
+ },
+ {
+ foreground: '2aa198',
+ token: 'keyword.other.unit.css',
+ },
+ {
+ foreground: '2aa198',
+ token: 'constant.other.color.rgb-value.css',
+ },
+ {
+ foreground: '2aa198',
+ token: 'meta.property-value.css',
+ },
+ {
+ foreground: 'dc322f',
+ token: 'keyword.other.important.css',
+ },
+ {
+ foreground: '2aa198',
+ token: 'support.constant.color',
+ },
+ {
+ foreground: '859900',
+ token: 'entity.name.tag.css',
+ },
+ {
+ foreground: '586e75',
+ token: 'punctuation.separator.key-value.css',
+ },
+ {
+ foreground: '586e75',
+ token: 'punctuation.terminator.rule.css',
+ },
+ {
+ foreground: '268bd2',
+ token: 'entity.other.attribute-name.class.css',
+ },
+ {
+ foreground: 'cb4b16',
+ token: 'entity.other.attribute-name.pseudo-element.css',
+ },
+ {
+ foreground: 'cb4b16',
+ token: 'entity.other.attribute-name.pseudo-class.css',
+ },
+ {
+ foreground: '268bd2',
+ token: 'entity.other.attribute-name.id.css',
+ },
+ {
+ foreground: 'b58900',
+ token: 'meta.function.js',
+ },
+ {
+ foreground: 'b58900',
+ token: 'entity.name.function.js',
+ },
+ {
+ foreground: 'b58900',
+ token: 'support.function.dom.js',
+ },
+ {
+ foreground: 'b58900',
+ token: 'text.html.basic source.js.embedded.html',
+ },
+ {
+ foreground: '268bd2',
+ token: 'storage.type.function.js',
+ },
+ {
+ foreground: '2aa198',
+ token: 'constant.numeric.js',
+ },
+ {
+ foreground: '268bd2',
+ token: 'meta.brace.square.js',
+ },
+ {
+ foreground: '268bd2',
+ token: 'storage.type.js',
+ },
+ {
+ foreground: '93a1a1',
+ token: 'meta.brace.round',
+ },
+ {
+ foreground: '93a1a1',
+ token: 'punctuation.definition.parameters.begin.js',
+ },
+ {
+ foreground: '93a1a1',
+ token: 'punctuation.definition.parameters.end.js',
+ },
+ {
+ foreground: '268bd2',
+ token: 'meta.brace.curly.js',
+ },
+ {
+ foreground: '93a1a1',
+ fontStyle: 'italic',
+ token: 'entity.name.tag.doctype.html',
+ },
+ {
+ foreground: '93a1a1',
+ fontStyle: 'italic',
+ token: 'meta.tag.sgml.html',
+ },
+ {
+ foreground: '93a1a1',
+ fontStyle: 'italic',
+ token: 'string.quoted.double.doctype.identifiers-and-DTDs.html',
+ },
+ {
+ foreground: '839496',
+ fontStyle: 'italic',
+ token: 'comment.block.html',
+ },
+ {
+ fontStyle: 'italic',
+ token: 'entity.name.tag.script.html',
+ },
+ {
+ foreground: '2aa198',
+ token: 'source.css.embedded.html string.quoted.double.html',
+ },
+ {
+ foreground: 'cb4b16',
+ fontStyle: 'bold',
+ token: 'text.html.ruby',
+ },
+ {
+ foreground: '657b83',
+ token: 'text.html.basic meta.tag.other.html',
+ },
+ {
+ foreground: '657b83',
+ token: 'text.html.basic meta.tag.any.html',
+ },
+ {
+ foreground: '657b83',
+ token: 'text.html.basic meta.tag.block.any',
+ },
+ {
+ foreground: '657b83',
+ token: 'text.html.basic meta.tag.inline.any',
+ },
+ {
+ foreground: '657b83',
+ token: 'text.html.basic meta.tag.structure.any.html',
+ },
+ {
+ foreground: '657b83',
+ token: 'text.html.basic source.js.embedded.html',
+ },
+ {
+ foreground: '657b83',
+ token: 'punctuation.separator.key-value.html',
+ },
+ {
+ foreground: '657b83',
+ token: 'text.html.basic entity.other.attribute-name.html',
+ },
+ {
+ foreground: '2aa198',
+ token: 'text.html.basic meta.tag.structure.any.html punctuation.definition.string.begin.html',
+ },
+ {
+ foreground: '2aa198',
+ token: 'punctuation.definition.string.begin.html',
+ },
+ {
+ foreground: '2aa198',
+ token: 'punctuation.definition.string.end.html',
+ },
+ {
+ foreground: '268bd2',
+ fontStyle: 'bold',
+ token: 'entity.name.tag.block.any.html',
+ },
+ {
+ fontStyle: 'italic',
+ token: 'source.css.embedded.html entity.name.tag.style.html',
+ },
+ {
+ foreground: '839496',
+ fontStyle: 'italic',
+ token: 'source.css.embedded.html',
+ },
+ {
+ foreground: '839496',
+ fontStyle: 'italic',
+ token: 'comment.block.html',
+ },
+ {
+ foreground: '268bd2',
+ token: 'punctuation.definition.variable.ruby',
+ },
+ {
+ foreground: '657b83',
+ token: 'meta.function.method.with-arguments.ruby',
+ },
+ {
+ foreground: '2aa198',
+ token: 'variable.language.ruby',
+ },
+ {
+ foreground: '268bd2',
+ token: 'entity.name.function.ruby',
+ },
+ {
+ foreground: '859900',
+ fontStyle: 'bold',
+ token: 'keyword.control.ruby',
+ },
+ {
+ foreground: '859900',
+ fontStyle: 'bold',
+ token: 'keyword.control.def.ruby',
+ },
+ {
+ foreground: '859900',
+ token: 'keyword.control.class.ruby',
+ },
+ {
+ foreground: '859900',
+ token: 'meta.class.ruby',
+ },
+ {
+ foreground: 'b58900',
+ token: 'entity.name.type.class.ruby',
+ },
+ {
+ foreground: '859900',
+ token: 'keyword.control.ruby',
+ },
+ {
+ foreground: 'b58900',
+ token: 'support.class.ruby',
+ },
+ {
+ foreground: '859900',
+ token: 'keyword.other.special-method.ruby',
+ },
+ {
+ foreground: '2aa198',
+ token: 'constant.language.ruby',
+ },
+ {
+ foreground: '2aa198',
+ token: 'constant.numeric.ruby',
+ },
+ {
+ foreground: 'b58900',
+ token: 'variable.other.constant.ruby',
+ },
+ {
+ foreground: '2aa198',
+ token: 'constant.other.symbol.ruby',
+ },
+ {
+ foreground: 'dc322f',
+ token: 'punctuation.section.embedded.ruby',
+ },
+ {
+ foreground: 'dc322f',
+ token: 'punctuation.definition.string.begin.ruby',
+ },
+ {
+ foreground: 'dc322f',
+ token: 'punctuation.definition.string.end.ruby',
+ },
+ {
+ foreground: 'cb4b16',
+ token: 'keyword.other.special-method.ruby',
+ },
+ {
+ foreground: 'cb4b16',
+ token: 'keyword.control.import.include.php',
+ },
+ {
+ foreground: '839496',
+ token: 'text.html.ruby meta.tag.inline.any.html',
+ },
+ {
+ foreground: '2aa198',
+ token: 'text.html.ruby punctuation.definition.string.begin',
+ },
+ {
+ foreground: '2aa198',
+ token: 'text.html.ruby punctuation.definition.string.end',
+ },
+ {
+ foreground: '839496',
+ token: 'punctuation.definition.string.begin',
+ },
+ {
+ foreground: '839496',
+ token: 'punctuation.definition.string.end',
+ },
+ {
+ foreground: 'dc322f',
+ token: 'keyword.operator.index-start.php',
+ },
+ {
+ foreground: 'dc322f',
+ token: 'keyword.operator.index-end.php',
+ },
+ {
+ foreground: '586e75',
+ token: 'meta.array.php',
+ },
+ {
+ foreground: 'b58900',
+ token: 'meta.array.php support.function.construct.php',
+ },
+ {
+ foreground: 'b58900',
+ token: 'meta.array.empty.php support.function.construct.php',
+ },
+ {
+ foreground: 'b58900',
+ token: 'support.function.construct.php',
+ },
+ {
+ foreground: 'dc322f',
+ token: 'punctuation.definition.array.begin',
+ },
+ {
+ foreground: 'dc322f',
+ token: 'punctuation.definition.array.end',
+ },
+ {
+ foreground: '2aa198',
+ token: 'constant.numeric.php',
+ },
+ {
+ foreground: 'cb4b16',
+ token: 'keyword.other.new.php',
+ },
+ {
+ foreground: '586e75',
+ token: 'support.class.php',
+ },
+ {
+ foreground: '586e75',
+ token: 'keyword.operator.class',
+ },
+ {
+ foreground: '93a1a1',
+ token: 'variable.other.property.php',
+ },
+ {
+ foreground: 'b58900',
+ token: 'storage.modifier.extends.php',
+ },
+ {
+ foreground: 'b58900',
+ token: 'storage.type.class.php',
+ },
+ {
+ foreground: 'b58900',
+ token: 'keyword.operator.class.php',
+ },
+ {
+ foreground: '586e75',
+ token: 'meta.other.inherited-class.php',
+ },
+ {
+ foreground: '859900',
+ token: 'storage.type.php',
+ },
+ {
+ foreground: '93a1a1',
+ token: 'entity.name.function.php',
+ },
+ {
+ foreground: '859900',
+ token: 'support.function.construct.php',
+ },
+ {
+ foreground: '839496',
+ token: 'entity.name.type.class.php',
+ },
+ {
+ foreground: '839496',
+ token: 'meta.function-call.php',
+ },
+ {
+ foreground: '839496',
+ token: 'meta.function-call.static.php',
+ },
+ {
+ foreground: '839496',
+ token: 'meta.function-call.object.php',
+ },
+ {
+ foreground: '93a1a1',
+ token: 'keyword.other.phpdoc',
+ },
+ {
+ foreground: 'cb4b16',
+ token: 'source.php.embedded.block.html',
+ },
+ {
+ foreground: 'cb4b16',
+ token: 'storage.type.function.php',
+ },
+ {
+ foreground: '2aa198',
+ token: 'constant.numeric.c',
+ },
+ {
+ foreground: 'cb4b16',
+ token: 'meta.preprocessor.c.include',
+ },
+ {
+ foreground: 'cb4b16',
+ token: 'meta.preprocessor.macro.c',
+ },
+ {
+ foreground: 'cb4b16',
+ token: 'keyword.control.import.define.c',
+ },
+ {
+ foreground: 'cb4b16',
+ token: 'keyword.control.import.include.c',
+ },
+ {
+ foreground: 'cb4b16',
+ token: 'entity.name.function.preprocessor.c',
+ },
+ {
+ foreground: '2aa198',
+ token: 'meta.preprocessor.c.include string.quoted.other.lt-gt.include.c',
+ },
+ {
+ foreground: '2aa198',
+ token: 'meta.preprocessor.c.include punctuation.definition.string.begin.c',
+ },
+ {
+ foreground: '2aa198',
+ token: 'meta.preprocessor.c.include punctuation.definition.string.end.c',
+ },
+ {
+ foreground: '586e75',
+ token: 'support.function.C99.c',
+ },
+ {
+ foreground: '586e75',
+ token: 'support.function.any-method.c',
+ },
+ {
+ foreground: '586e75',
+ token: 'entity.name.function.c',
+ },
+ {
+ foreground: '2aa198',
+ token: 'punctuation.definition.string.begin.c',
+ },
+ {
+ foreground: '2aa198',
+ token: 'punctuation.definition.string.end.c',
+ },
+ {
+ foreground: 'b58900',
+ token: 'storage.type.c',
+ },
+ {
+ foreground: 'e0eddd',
+ background: 'b58900',
+ fontStyle: 'italic',
+ token: 'meta.diff',
+ },
+ {
+ foreground: 'e0eddd',
+ background: 'b58900',
+ fontStyle: 'italic',
+ token: 'meta.diff.header',
+ },
+ {
+ foreground: 'dc322f',
+ background: 'eee8d5',
+ token: 'markup.deleted',
+ },
+ {
+ foreground: 'cb4b16',
+ background: 'eee8d5',
+ token: 'markup.changed',
+ },
+ {
+ foreground: '219186',
+ background: 'eee8d5',
+ token: 'markup.inserted',
+ },
+ {
+ foreground: 'e0eddd',
+ background: 'a57706',
+ token: 'text.html.markdown meta.dummy.line-break',
+ },
+ {
+ foreground: '2aa198',
+ token: 'text.html.markdown markup.raw.inline',
+ },
+ {
+ foreground: '2aa198',
+ token: 'text.restructuredtext markup.raw',
+ },
+ {
+ foreground: 'dc322f',
+ token: 'other.package.exclude',
+ },
+ {
+ foreground: 'dc322f',
+ token: 'other.remove',
+ },
+ {
+ foreground: '2aa198',
+ token: 'other.add',
+ },
+ {
+ foreground: 'dc322f',
+ token: 'punctuation.section.group.tex',
+ },
+ {
+ foreground: 'dc322f',
+ token: 'punctuation.definition.arguments.begin.latex',
+ },
+ {
+ foreground: 'dc322f',
+ token: 'punctuation.definition.arguments.end.latex',
+ },
+ {
+ foreground: 'dc322f',
+ token: 'punctuation.definition.arguments.latex',
+ },
+ {
+ foreground: 'b58900',
+ token: 'meta.group.braces.tex',
+ },
+ {
+ foreground: 'b58900',
+ token: 'string.other.math.tex',
+ },
+ {
+ foreground: 'cb4b16',
+ token: 'variable.parameter.function.latex',
+ },
+ {
+ foreground: 'dc322f',
+ token: 'punctuation.definition.constant.math.tex',
+ },
+ {
+ foreground: '2aa198',
+ token: 'text.tex.latex constant.other.math.tex',
+ },
+ {
+ foreground: '2aa198',
+ token: 'constant.other.general.math.tex',
+ },
+ {
+ foreground: '2aa198',
+ token: 'constant.other.general.math.tex',
+ },
+ {
+ foreground: '2aa198',
+ token: 'constant.character.math.tex',
+ },
+ {
+ foreground: 'b58900',
+ token: 'string.other.math.tex',
+ },
+ {
+ foreground: 'dc322f',
+ token: 'punctuation.definition.string.begin.tex',
+ },
+ {
+ foreground: 'dc322f',
+ token: 'punctuation.definition.string.end.tex',
+ },
+ {
+ foreground: '2aa198',
+ token: 'keyword.control.label.latex',
+ },
+ {
+ foreground: '2aa198',
+ token: 'text.tex.latex constant.other.general.math.tex',
+ },
+ {
+ foreground: 'dc322f',
+ token: 'variable.parameter.definition.label.latex',
+ },
+ {
+ foreground: '859900',
+ token: 'support.function.be.latex',
+ },
+ {
+ foreground: 'cb4b16',
+ token: 'support.function.section.latex',
+ },
+ {
+ foreground: '2aa198',
+ token: 'support.function.general.tex',
+ },
+ {
+ fontStyle: 'italic',
+ token: 'punctuation.definition.comment.tex',
+ },
+ {
+ fontStyle: 'italic',
+ token: 'comment.line.percentage.tex',
+ },
+ {
+ foreground: '2aa198',
+ token: 'keyword.control.ref.latex',
+ },
+ {
+ foreground: '586e75',
+ token: 'string.quoted.double.block.python',
+ },
+ {
+ foreground: '859900',
+ token: 'storage.type.class.python',
+ },
+ {
+ foreground: '859900',
+ token: 'storage.type.function.python',
+ },
+ {
+ foreground: '859900',
+ token: 'storage.modifier.global.python',
+ },
+ {
+ foreground: 'cb4b16',
+ token: 'keyword.control.import.python',
+ },
+ {
+ foreground: 'cb4b16',
+ token: 'keyword.control.import.from.python',
+ },
+ {
+ foreground: 'b58900',
+ token: 'support.type.exception.python',
+ },
+ {
+ foreground: '859900',
+ token: 'support.function.builtin.shell',
+ },
+ {
+ foreground: 'cb4b16',
+ token: 'variable.other.normal.shell',
+ },
+ {
+ foreground: '268bd2',
+ token: 'source.shell',
+ },
+ {
+ foreground: '586e75',
+ token: 'meta.scope.for-in-loop.shell',
+ },
+ {
+ foreground: '586e75',
+ token: 'variable.other.loop.shell',
+ },
+ {
+ foreground: '859900',
+ token: 'punctuation.definition.string.end.shell',
+ },
+ {
+ foreground: '859900',
+ token: 'punctuation.definition.string.begin.shell',
+ },
+ {
+ foreground: '586e75',
+ token: 'meta.scope.case-block.shell',
+ },
+ {
+ foreground: '586e75',
+ token: 'meta.scope.case-body.shell',
+ },
+ {
+ foreground: 'dc322f',
+ token: 'punctuation.definition.logical-expression.shell',
+ },
+ {
+ fontStyle: 'italic',
+ token: 'comment.line.number-sign.shell',
+ },
+ {
+ foreground: 'cb4b16',
+ token: 'keyword.other.import.java',
+ },
+ {
+ foreground: '586e75',
+ token: 'storage.modifier.import.java',
+ },
+ {
+ foreground: 'b58900',
+ token: 'meta.class.java storage.modifier.java',
+ },
+ {
+ foreground: '586e75',
+ token: 'source.java comment.block',
+ },
+ {
+ foreground: '586e75',
+ token:
+ 'comment.block meta.documentation.tag.param.javadoc keyword.other.documentation.param.javadoc',
+ },
+ {
+ foreground: 'b58900',
+ token: 'punctuation.definition.variable.perl',
+ },
+ {
+ foreground: 'b58900',
+ token: 'variable.other.readwrite.global.perl',
+ },
+ {
+ foreground: 'b58900',
+ token: 'variable.other.predefined.perl',
+ },
+ {
+ foreground: 'b58900',
+ token: 'keyword.operator.comparison.perl',
+ },
+ {
+ foreground: '859900',
+ token: 'support.function.perl',
+ },
+ {
+ foreground: '586e75',
+ fontStyle: 'italic',
+ token: 'comment.line.number-sign.perl',
+ },
+ {
+ foreground: '2aa198',
+ token: 'punctuation.definition.string.begin.perl',
+ },
+ {
+ foreground: '2aa198',
+ token: 'punctuation.definition.string.end.perl',
+ },
+ {
+ foreground: 'dc322f',
+ token: 'constant.character.escape.perl',
+ },
+ {
+ foreground: '268bd2',
+ token: 'markup.heading.markdown',
+ },
+ {
+ foreground: '268bd2',
+ token: 'markup.heading.1.markdown',
+ },
+ {
+ foreground: '268bd2',
+ token: 'markup.heading.2.markdown',
+ },
+ {
+ foreground: '268bd2',
+ token: 'markup.heading.3.markdown',
+ },
+ {
+ foreground: '268bd2',
+ token: 'markup.heading.4.markdown',
+ },
+ {
+ foreground: '268bd2',
+ token: 'markup.heading.5.markdown',
+ },
+ {
+ foreground: '268bd2',
+ token: 'markup.heading.6.markdown',
+ },
+ {
+ foreground: '586e75',
+ fontStyle: 'bold',
+ token: 'markup.bold.markdown',
+ },
+ {
+ foreground: '586e75',
+ fontStyle: 'italic',
+ token: 'markup.italic.markdown',
+ },
+ {
+ foreground: 'dc322f',
+ token: 'punctuation.definition.bold.markdown',
+ },
+ {
+ foreground: 'dc322f',
+ token: 'punctuation.definition.italic.markdown',
+ },
+ {
+ foreground: 'dc322f',
+ token: 'punctuation.definition.raw.markdown',
+ },
+ {
+ foreground: 'b58900',
+ token: 'markup.list.unnumbered.markdown',
+ },
+ {
+ foreground: '859900',
+ token: 'markup.list.numbered.markdown',
+ },
+ {
+ foreground: '2aa198',
+ token: 'markup.raw.block.markdown',
+ },
+ {
+ foreground: '2aa198',
+ token: 'markup.raw.inline.markdown',
+ },
+ {
+ foreground: '6c71c4',
+ token: 'markup.quote.markdown',
+ },
+ {
+ foreground: '6c71c4',
+ token: 'punctuation.definition.blockquote.markdown',
+ },
+ {
+ foreground: 'd33682',
+ token: 'meta.separator.markdown',
+ },
+ {
+ foreground: '839496',
+ token: 'markup.underline.link.markdown',
+ },
+ {
+ foreground: '839496',
+ token: 'markup.underline.link.markdown',
+ },
+ {
+ foreground: 'dc322f',
+ token: 'meta.link.inet.markdown',
+ },
+ {
+ foreground: 'dc322f',
+ token: 'meta.link.email.lt-gt.markdown',
+ },
+ {
+ foreground: 'dc322f',
+ token: 'punctuation.definition.string.begin.markdown',
+ },
+ {
+ foreground: 'dc322f',
+ token: 'punctuation.definition.string.end.markdown',
+ },
+ {
+ foreground: 'dc322f',
+ token: 'punctuation.definition.link.markdown',
+ },
+ {
+ foreground: '6a8187',
+ token: 'text.plain',
+ },
+ {
+ foreground: 'eee8d5',
+ background: 'eee8d5',
+ token: 'sublimelinter.notes',
+ },
+ {
+ foreground: '93a1a1',
+ background: '93a1a1',
+ token: 'sublimelinter.outline.illegal',
+ },
+ {
+ background: 'dc322f',
+ token: 'sublimelinter.underline.illegal',
+ },
+ {
+ foreground: '839496',
+ background: '839496',
+ token: 'sublimelinter.outline.warning',
+ },
+ {
+ background: 'b58900',
+ token: 'sublimelinter.underline.warning',
+ },
+ {
+ foreground: '657b83',
+ background: '657b83',
+ token: 'sublimelinter.outline.violation',
+ },
+ {
+ background: 'cb4b16',
+ token: 'sublimelinter.underline.violation',
+ },
+ ],
+ colors: {
+ 'editor.foreground': '#586E75',
+ 'editor.background': '#FDF6E3',
+ 'editor.selectionBackground': '#EEE8D5',
+ 'editor.lineHighlightBackground': '#EEE8D5',
+ 'editorCursor.foreground': '#000000',
+ 'editorWhitespace.foreground': '#EAE3C9',
+ },
+};
diff --git a/app/assets/javascripts/ide/services/index.js b/app/assets/javascripts/ide/services/index.js
index 3adf0cf073f..1767d961259 100644
--- a/app/assets/javascripts/ide/services/index.js
+++ b/app/assets/javascripts/ide/services/index.js
@@ -88,12 +88,16 @@ export default {
commit(projectId, payload) {
return Api.commitMultiple(projectId, payload);
},
- getFiles(projectUrl, ref) {
- const url = `${projectUrl}/-/files/${ref}`;
+ getFiles(projectPath, ref) {
+ const url = `${gon.relative_url_root}/${projectPath}/-/files/${ref}`;
return axios.get(url, { params: { format: 'json' } });
},
lastCommitPipelines({ getters }) {
const commitSha = getters.lastCommit.id;
return Api.commitPipelines(getters.currentProject.path_with_namespace, commitSha);
},
+ pingUsage(projectPath) {
+ const url = `${gon.relative_url_root}/${projectPath}/usage_ping/web_ide_pipelines_count`;
+ return axios.post(url);
+ },
};
diff --git a/app/assets/javascripts/ide/stores/actions.js b/app/assets/javascripts/ide/stores/actions.js
index 04cf0ad53d5..e32b5ac7bdc 100644
--- a/app/assets/javascripts/ide/stores/actions.js
+++ b/app/assets/javascripts/ide/stores/actions.js
@@ -1,6 +1,5 @@
-import $ from 'jquery';
import Vue from 'vue';
-import { escape as esc } from 'lodash';
+import { escape } from 'lodash';
import { __, sprintf } from '~/locale';
import { visitUrl } from '~/lib/utils/url_utility';
import flash from '~/flash';
@@ -25,14 +24,6 @@ export const closeAllFiles = ({ state, dispatch }) => {
state.openFiles.forEach(file => dispatch('closeFile', file));
};
-export const setPanelCollapsedStatus = ({ commit }, { side, collapsed }) => {
- if (side === 'left') {
- commit(types.SET_LEFT_PANEL_COLLAPSED, collapsed);
- } else {
- commit(types.SET_RIGHT_PANEL_COLLAPSED, collapsed);
- }
-};
-
export const setResizingStatus = ({ commit }, resizing) => {
commit(types.SET_RESIZING_STATUS, resizing);
};
@@ -176,13 +167,6 @@ export const setLinks = ({ commit }, links) => commit(types.SET_LINKS, links);
export const setErrorMessage = ({ commit }, errorMessage) =>
commit(types.SET_ERROR_MESSAGE, errorMessage);
-export const openNewEntryModal = ({ commit }, { type, path = '' }) => {
- commit(types.OPEN_NEW_ENTRY_MODAL, { type, path });
-
- // open the modal manually so we don't mess around with dropdown/rows
- $('#ide-new-entry').modal('show');
-};
-
export const deleteEntry = ({ commit, dispatch, state }, path) => {
const entry = state.entries[path];
const { prevPath, prevName, prevParentPath } = entry;
@@ -296,7 +280,7 @@ export const getBranchData = ({ commit, state }, { projectId, branchId, force =
sprintf(
__('Branch not loaded - %{branchId}'),
{
- branchId: `<strong>${esc(projectId)}/${esc(branchId)}</strong>`,
+ branchId: `<strong>${escape(projectId)}/${escape(branchId)}</strong>`,
},
false,
),
diff --git a/app/assets/javascripts/ide/stores/actions/project.js b/app/assets/javascripts/ide/stores/actions/project.js
index ae3829dc35e..6c8fb9f90aa 100644
--- a/app/assets/javascripts/ide/stores/actions/project.js
+++ b/app/assets/javascripts/ide/stores/actions/project.js
@@ -1,4 +1,4 @@
-import { escape as esc } from 'lodash';
+import { escape } from 'lodash';
import flash from '~/flash';
import { __, sprintf } from '~/locale';
import service from '../../services';
@@ -73,7 +73,7 @@ export const showBranchNotFoundError = ({ dispatch }, branchId) => {
text: sprintf(
__("Branch %{branchName} was not found in this project's repository."),
{
- branchName: `<strong>${esc(branchId)}</strong>`,
+ branchName: `<strong>${escape(branchId)}</strong>`,
},
false,
),
@@ -162,7 +162,7 @@ export const openBranch = ({ dispatch }, { projectId, branchId, basePath }) => {
sprintf(
__('An error occurred while getting files for - %{branchId}'),
{
- branchId: `<strong>${esc(projectId)}/${esc(branchId)}</strong>`,
+ branchId: `<strong>${escape(projectId)}/${escape(branchId)}</strong>`,
},
false,
),
diff --git a/app/assets/javascripts/ide/stores/actions/tree.js b/app/assets/javascripts/ide/stores/actions/tree.js
index 7d48f0adc4c..1ca608f1287 100644
--- a/app/assets/javascripts/ide/stores/actions/tree.js
+++ b/app/assets/javascripts/ide/stores/actions/tree.js
@@ -59,7 +59,7 @@ export const getFiles = ({ state, commit, dispatch }, payload = {}) =>
commit(types.CREATE_TREE, { treePath: `${projectId}/${branchId}` });
service
- .getFiles(selectedProject.web_url, ref)
+ .getFiles(selectedProject.path_with_namespace, ref)
.then(({ data }) => {
const { entries, treeList } = decorateFiles({
data,
diff --git a/app/assets/javascripts/ide/stores/modules/commit/actions.js b/app/assets/javascripts/ide/stores/modules/commit/actions.js
index 505daa8834d..592c7e15918 100644
--- a/app/assets/javascripts/ide/stores/modules/commit/actions.js
+++ b/app/assets/javascripts/ide/stores/modules/commit/actions.js
@@ -1,6 +1,6 @@
-import $ from 'jquery';
import { sprintf, __ } from '~/locale';
import flash from '~/flash';
+import httpStatusCodes from '~/lib/utils/http_status';
import * as rootTypes from '../../mutation_types';
import { createCommitPayload, createNewMergeRequestUrl } from '../../utils';
import router from '../../../ide_router';
@@ -215,25 +215,23 @@ export const commitChanges = ({ commit, state, getters, dispatch, rootState, roo
);
})
.catch(err => {
- if (err.response.status === 400) {
- $('#ide-create-branch-modal').modal('show');
- } else {
- dispatch(
- 'setErrorMessage',
- {
- text: __('An error occurred while committing your changes.'),
- action: () =>
- dispatch('commitChanges').then(() =>
- dispatch('setErrorMessage', null, { root: true }),
- ),
- actionText: __('Please try again'),
- },
- { root: true },
- );
- window.dispatchEvent(new Event('resize'));
- }
-
commit(types.UPDATE_LOADING, false);
+
+ // don't catch bad request errors, let the view handle them
+ if (err.response.status === httpStatusCodes.BAD_REQUEST) throw err;
+
+ dispatch(
+ 'setErrorMessage',
+ {
+ text: __('An error occurred while committing your changes.'),
+ action: () =>
+ dispatch('commitChanges').then(() => dispatch('setErrorMessage', null, { root: true })),
+ actionText: __('Please try again'),
+ },
+ { root: true },
+ );
+
+ window.dispatchEvent(new Event('resize'));
});
};
diff --git a/app/assets/javascripts/ide/stores/mutation_types.js b/app/assets/javascripts/ide/stores/mutation_types.js
index 78831bdf022..5c78bfefa04 100644
--- a/app/assets/javascripts/ide/stores/mutation_types.js
+++ b/app/assets/javascripts/ide/stores/mutation_types.js
@@ -2,8 +2,6 @@ export const SET_INITIAL_DATA = 'SET_INITIAL_DATA';
export const TOGGLE_LOADING = 'TOGGLE_LOADING';
export const SET_LAST_COMMIT_DATA = 'SET_LAST_COMMIT_DATA';
export const SET_LAST_COMMIT_MSG = 'SET_LAST_COMMIT_MSG';
-export const SET_LEFT_PANEL_COLLAPSED = 'SET_LEFT_PANEL_COLLAPSED';
-export const SET_RIGHT_PANEL_COLLAPSED = 'SET_RIGHT_PANEL_COLLAPSED';
export const SET_RESIZING_STATUS = 'SET_RESIZING_STATUS';
export const SET_EMPTY_STATE_SVGS = 'SET_EMPTY_STATE_SVGS';
export const SET_LINKS = 'SET_LINKS';
@@ -73,7 +71,6 @@ export const RESET_OPEN_FILES = 'RESET_OPEN_FILES';
export const SET_ERROR_MESSAGE = 'SET_ERROR_MESSAGE';
-export const OPEN_NEW_ENTRY_MODAL = 'OPEN_NEW_ENTRY_MODAL';
export const DELETE_ENTRY = 'DELETE_ENTRY';
export const RENAME_ENTRY = 'RENAME_ENTRY';
export const REVERT_RENAME_ENTRY = 'REVERT_RENAME_ENTRY';
diff --git a/app/assets/javascripts/ide/stores/mutations.js b/app/assets/javascripts/ide/stores/mutations.js
index 5d567d9b169..12ac10df206 100644
--- a/app/assets/javascripts/ide/stores/mutations.js
+++ b/app/assets/javascripts/ide/stores/mutations.js
@@ -29,16 +29,6 @@ export default {
});
}
},
- [types.SET_LEFT_PANEL_COLLAPSED](state, collapsed) {
- Object.assign(state, {
- leftPanelCollapsed: collapsed,
- });
- },
- [types.SET_RIGHT_PANEL_COLLAPSED](state, collapsed) {
- Object.assign(state, {
- rightPanelCollapsed: collapsed,
- });
- },
[types.SET_RESIZING_STATUS](state, resizing) {
Object.assign(state, {
panelResizing: resizing,
@@ -192,15 +182,6 @@ export default {
[types.SET_ERROR_MESSAGE](state, errorMessage) {
Object.assign(state, { errorMessage });
},
- [types.OPEN_NEW_ENTRY_MODAL](state, { type, path }) {
- Object.assign(state, {
- entryModal: {
- type,
- path,
- entry: { ...state.entries[path] },
- },
- });
- },
[types.DELETE_ENTRY](state, path) {
const entry = state.entries[path];
const { tempFile = false } = entry;
diff --git a/app/assets/javascripts/ide/stores/mutations/project.js b/app/assets/javascripts/ide/stores/mutations/project.js
index 9230f3839c1..034fdad4305 100644
--- a/app/assets/javascripts/ide/stores/mutations/project.js
+++ b/app/assets/javascripts/ide/stores/mutations/project.js
@@ -16,9 +16,7 @@ export default {
});
Object.assign(state, {
- projects: Object.assign({}, state.projects, {
- [projectPath]: project,
- }),
+ projects: { ...state.projects, [projectPath]: project },
});
},
[types.TOGGLE_EMPTY_STATE](state, { projectPath, value }) {
diff --git a/app/assets/javascripts/ide/stores/mutations/tree.js b/app/assets/javascripts/ide/stores/mutations/tree.js
index 359943b4ab7..c8f14a680c2 100644
--- a/app/assets/javascripts/ide/stores/mutations/tree.js
+++ b/app/assets/javascripts/ide/stores/mutations/tree.js
@@ -14,12 +14,13 @@ export default {
},
[types.CREATE_TREE](state, { treePath }) {
Object.assign(state, {
- trees: Object.assign({}, state.trees, {
+ trees: {
+ ...state.trees,
[treePath]: {
tree: [],
loading: true,
},
- }),
+ },
});
},
[types.SET_DIRECTORY_DATA](state, { data, treePath }) {
diff --git a/app/assets/javascripts/ide/stores/state.js b/app/assets/javascripts/ide/stores/state.js
index 0fd6a448283..0c95c22e8f8 100644
--- a/app/assets/javascripts/ide/stores/state.js
+++ b/app/assets/javascripts/ide/stores/state.js
@@ -15,8 +15,6 @@ export default () => ({
parentTreeUrl: '',
trees: {},
projects: {},
- leftPanelCollapsed: false,
- rightPanelCollapsed: false,
panelResizing: false,
entries: {},
viewer: viewerTypes.edit,
diff --git a/app/assets/javascripts/ide/stores/utils.js b/app/assets/javascripts/ide/stores/utils.js
index 4e5b01596d8..56671142bd4 100644
--- a/app/assets/javascripts/ide/stores/utils.js
+++ b/app/assets/javascripts/ide/stores/utils.js
@@ -1,4 +1,5 @@
import { commitActionTypes, FILE_VIEW_MODE_EDITOR } from '../constants';
+import { relativePathToAbsolute, isAbsolute, isRootRelative } from '~/lib/utils/url_utility';
export const dataStructure = () => ({
id: '',
@@ -274,3 +275,45 @@ export const pathsAreEqual = (a, b) => {
// if the contents of a file dont end with a newline, this function adds a newline
export const addFinalNewlineIfNeeded = content =>
content.charAt(content.length - 1) !== '\n' ? `${content}\n` : content;
+
+export function extractMarkdownImagesFromEntries(mdFile, entries) {
+ /**
+ * Regex to identify an image tag in markdown, like:
+ *
+ * ![img alt goes here](/img.png)
+ * ![img alt](../img 1/img.png "my image title")
+ * ![img alt](https://gitlab.com/assets/logo.svg "title here")
+ *
+ */
+ const reMdImage = /!\[([^\]]*)\]\((.*?)(?:(?="|\))"([^"]*)")?\)/gi;
+ const prefix = 'gl_md_img_';
+ const images = {};
+
+ let content = mdFile.content || mdFile.raw;
+ let i = 0;
+
+ content = content.replace(reMdImage, (_, alt, path, title) => {
+ const imagePath = (isRootRelative(path) ? path : relativePathToAbsolute(path, mdFile.path))
+ .substr(1)
+ .trim();
+
+ const imageContent = entries[imagePath]?.content || entries[imagePath]?.raw;
+
+ if (!isAbsolute(path) && imageContent) {
+ const ext = path.includes('.')
+ ? path
+ .split('.')
+ .pop()
+ .trim()
+ : 'jpeg';
+ const src = `data:image/${ext};base64,${imageContent}`;
+ i += 1;
+ const key = `{{${prefix}${i}}}`;
+ images[key] = { alt, src, title };
+ return key;
+ }
+ return title ? `![${alt}](${path}"${title}")` : `![${alt}](${path})`;
+ });
+
+ return { content, images };
+}
diff --git a/app/assets/javascripts/ide/utils.js b/app/assets/javascripts/ide/utils.js
index 64ac539a4ff..1ea2b199237 100644
--- a/app/assets/javascripts/ide/utils.js
+++ b/app/assets/javascripts/ide/utils.js
@@ -68,3 +68,19 @@ export const createPathWithExt = p => {
return `${p.substring(1, p.lastIndexOf('.') + 1 || p.length)}${ext || '.js'}`;
};
+
+export const trimPathComponents = path =>
+ path
+ .split('/')
+ .map(s => s.trim())
+ .join('/');
+
+export function registerLanguages(def, ...defs) {
+ if (defs.length) defs.forEach(lang => registerLanguages(lang));
+
+ const languageId = def.id;
+
+ languages.register(def);
+ languages.setMonarchTokensProvider(languageId, def.language);
+ languages.setLanguageConfiguration(languageId, def.conf);
+}
diff --git a/app/assets/javascripts/image_diff/helpers/badge_helper.js b/app/assets/javascripts/image_diff/helpers/badge_helper.js
index 7921650e8a0..229e0a62c51 100644
--- a/app/assets/javascripts/image_diff/helpers/badge_helper.js
+++ b/app/assets/javascripts/image_diff/helpers/badge_helper.js
@@ -15,7 +15,7 @@ export function createImageBadge(noteId, { x, y }, classNames = []) {
export function addImageBadge(containerEl, { coordinate, badgeText, noteId }) {
const buttonEl = createImageBadge(noteId, coordinate, ['badge', 'badge-pill']);
- buttonEl.innerText = badgeText;
+ buttonEl.textContent = badgeText;
containerEl.appendChild(buttonEl);
}
@@ -32,6 +32,6 @@ export function addAvatarBadge(el, event) {
// Add badge to new comment
const avatarBadgeEl = el.querySelector(`#${noteId} .badge`);
- avatarBadgeEl.innerText = badgeNumber;
+ avatarBadgeEl.textContent = badgeNumber;
avatarBadgeEl.classList.remove('hidden');
}
diff --git a/app/assets/javascripts/image_diff/helpers/comment_indicator_helper.js b/app/assets/javascripts/image_diff/helpers/comment_indicator_helper.js
index df3d90cff68..deaef686f59 100644
--- a/app/assets/javascripts/image_diff/helpers/comment_indicator_helper.js
+++ b/app/assets/javascripts/image_diff/helpers/comment_indicator_helper.js
@@ -32,9 +32,7 @@ export function removeCommentIndicator(imageFrameEl) {
commentIndicatorEl.remove();
}
- return Object.assign({}, meta, {
- removed: willRemove,
- });
+ return { ...meta, removed: willRemove };
}
export function showCommentIndicator(imageFrameEl, coordinate) {
diff --git a/app/assets/javascripts/image_diff/helpers/dom_helper.js b/app/assets/javascripts/image_diff/helpers/dom_helper.js
index a319bcccb8f..a61e5f01f9b 100644
--- a/app/assets/javascripts/image_diff/helpers/dom_helper.js
+++ b/app/assets/javascripts/image_diff/helpers/dom_helper.js
@@ -4,24 +4,19 @@ export function setPositionDataAttribute(el, options) {
const { x, y, width, height } = options;
const { position } = el.dataset;
- const positionObject = Object.assign({}, JSON.parse(position), {
- x,
- y,
- width,
- height,
- });
+ const positionObject = { ...JSON.parse(position), x, y, width, height };
el.setAttribute('data-position', JSON.stringify(positionObject));
}
export function updateDiscussionAvatarBadgeNumber(discussionEl, newBadgeNumber) {
const avatarBadgeEl = discussionEl.querySelector('.image-diff-avatar-link .badge');
- avatarBadgeEl.innerText = newBadgeNumber;
+ avatarBadgeEl.textContent = newBadgeNumber;
}
export function updateDiscussionBadgeNumber(discussionEl, newBadgeNumber) {
const discussionBadgeEl = discussionEl.querySelector('.badge');
- discussionBadgeEl.innerText = newBadgeNumber;
+ discussionBadgeEl.textContent = newBadgeNumber;
}
export function toggleCollapsed(event) {
diff --git a/app/assets/javascripts/image_diff/image_diff.js b/app/assets/javascripts/image_diff/image_diff.js
index 26c1b0ec7be..079f4a63f6e 100644
--- a/app/assets/javascripts/image_diff/image_diff.js
+++ b/app/assets/javascripts/image_diff/image_diff.js
@@ -75,9 +75,7 @@ export default class ImageDiff {
if (this.renderCommentBadge) {
imageDiffHelper.addImageCommentBadge(this.imageFrameEl, options);
} else {
- const numberBadgeOptions = Object.assign({}, options, {
- badgeText: index + 1,
- });
+ const numberBadgeOptions = { ...options, badgeText: index + 1 };
imageDiffHelper.addImageBadge(this.imageFrameEl, numberBadgeOptions);
}
@@ -130,7 +128,7 @@ export default class ImageDiff {
const updatedBadgeNumber = index;
const discussionEl = this.el.querySelector(`#discussion_${discussionId}`);
- imageBadgeEls[index].innerText = updatedBadgeNumber;
+ imageBadgeEls[index].textContent = updatedBadgeNumber;
imageDiffHelper.updateDiscussionBadgeNumber(discussionEl, updatedBadgeNumber);
imageDiffHelper.updateDiscussionAvatarBadgeNumber(discussionEl, updatedBadgeNumber);
diff --git a/app/assets/javascripts/import_projects/event_hub.js b/app/assets/javascripts/import_projects/event_hub.js
index 0948c2e5352..e31806ad199 100644
--- a/app/assets/javascripts/import_projects/event_hub.js
+++ b/app/assets/javascripts/import_projects/event_hub.js
@@ -1,3 +1,3 @@
-import Vue from 'vue';
+import createEventHub from '~/helpers/event_hub_factory';
-export default new Vue();
+export default createEventHub();
diff --git a/app/assets/javascripts/importer_status.js b/app/assets/javascripts/importer_status.js
index 1ffd5c61282..d6b519f7eac 100644
--- a/app/assets/javascripts/importer_status.js
+++ b/app/assets/javascripts/importer_status.js
@@ -1,5 +1,5 @@
import $ from 'jquery';
-import _ from 'underscore';
+import { escape } from 'lodash';
import { __, sprintf } from './locale';
import axios from './lib/utils/axios_utils';
import flash from './flash';
@@ -73,9 +73,9 @@ class ImporterStatus {
const connectingVerb = this.ciCdOnly ? __('connecting') : __('importing');
job.find('.import-actions').html(
sprintf(
- _.escape(__('%{loadingIcon} Started')),
+ escape(__('%{loadingIcon} Started')),
{
- loadingIcon: `<i class="fa fa-spinner fa-spin" aria-label="${_.escape(
+ loadingIcon: `<i class="fa fa-spinner fa-spin" aria-label="${escape(
connectingVerb,
)}"></i>`,
},
diff --git a/app/assets/javascripts/integrations/edit/components/active_toggle.vue b/app/assets/javascripts/integrations/edit/components/active_toggle.vue
index 2b0aa2586e4..8b95b04d93c 100644
--- a/app/assets/javascripts/integrations/edit/components/active_toggle.vue
+++ b/app/assets/javascripts/integrations/edit/components/active_toggle.vue
@@ -12,10 +12,6 @@ export default {
type: Boolean,
required: true,
},
- disabled: {
- type: Boolean,
- required: true,
- },
},
data() {
return {
@@ -41,12 +37,7 @@ export default {
<div class="form-group row" role="group">
<label for="service[active]" class="col-form-label col-sm-2">{{ __('Active') }}</label>
<div class="col-sm-10 pt-1">
- <gl-toggle
- v-model="activated"
- :disabled="disabled"
- name="service[active]"
- @change="onToggle"
- />
+ <gl-toggle v-model="activated" name="service[active]" @change="onToggle" />
</div>
</div>
</div>
diff --git a/app/assets/javascripts/integrations/edit/components/integration_form.vue b/app/assets/javascripts/integrations/edit/components/integration_form.vue
new file mode 100644
index 00000000000..fbe58c30b13
--- /dev/null
+++ b/app/assets/javascripts/integrations/edit/components/integration_form.vue
@@ -0,0 +1,50 @@
+<script>
+import ActiveToggle from './active_toggle.vue';
+import JiraTriggerFields from './jira_trigger_fields.vue';
+import TriggerFields from './trigger_fields.vue';
+
+export default {
+ name: 'IntegrationForm',
+ components: {
+ ActiveToggle,
+ JiraTriggerFields,
+ TriggerFields,
+ },
+ props: {
+ activeToggleProps: {
+ type: Object,
+ required: true,
+ },
+ showActive: {
+ type: Boolean,
+ required: true,
+ },
+ triggerFieldsProps: {
+ type: Object,
+ required: true,
+ },
+ triggerEvents: {
+ type: Array,
+ required: false,
+ default: () => [],
+ },
+ type: {
+ type: String,
+ required: true,
+ },
+ },
+ computed: {
+ isJira() {
+ return this.type === 'jira';
+ },
+ },
+};
+</script>
+
+<template>
+ <div>
+ <active-toggle v-if="showActive" v-bind="activeToggleProps" />
+ <jira-trigger-fields v-if="isJira" v-bind="triggerFieldsProps" />
+ <trigger-fields v-else-if="triggerEvents.length" :events="triggerEvents" :type="type" />
+ </div>
+</template>
diff --git a/app/assets/javascripts/integrations/edit/components/jira_trigger_fields.vue b/app/assets/javascripts/integrations/edit/components/jira_trigger_fields.vue
new file mode 100644
index 00000000000..70278e401ce
--- /dev/null
+++ b/app/assets/javascripts/integrations/edit/components/jira_trigger_fields.vue
@@ -0,0 +1,99 @@
+<script>
+import { GlFormCheckbox, GlFormRadio } from '@gitlab/ui';
+
+export default {
+ name: 'JiraTriggerFields',
+ components: {
+ GlFormCheckbox,
+ GlFormRadio,
+ },
+ props: {
+ initialTriggerCommit: {
+ type: Boolean,
+ required: true,
+ },
+ initialTriggerMergeRequest: {
+ type: Boolean,
+ required: true,
+ },
+ initialEnableComments: {
+ type: Boolean,
+ required: true,
+ },
+ initialCommentDetail: {
+ type: String,
+ required: false,
+ default: 'standard',
+ },
+ },
+ data() {
+ return {
+ triggerCommit: this.initialTriggerCommit,
+ triggerMergeRequest: this.initialTriggerMergeRequest,
+ enableComments: this.initialEnableComments,
+ commentDetail: this.initialCommentDetail,
+ };
+ },
+};
+</script>
+
+<template>
+ <div class="form-group row pt-2" role="group">
+ <label for="service[trigger]" class="col-form-label col-sm-2 pt-0">{{ __('Trigger') }}</label>
+ <div class="col-sm-10">
+ <label class="weight-normal mb-2">
+ {{
+ s__(
+ 'Integrations|When a Jira issue is mentioned in a commit or merge request a remote link and comment (if enabled) will be created.',
+ )
+ }}
+ </label>
+
+ <input name="service[commit_events]" type="hidden" value="false" />
+ <gl-form-checkbox v-model="triggerCommit" name="service[commit_events]">
+ {{ __('Commit') }}
+ </gl-form-checkbox>
+
+ <input name="service[merge_requests_events]" type="hidden" value="false" />
+ <gl-form-checkbox v-model="triggerMergeRequest" name="service[merge_requests_events]">
+ {{ __('Merge request') }}
+ </gl-form-checkbox>
+
+ <div
+ v-show="triggerCommit || triggerMergeRequest"
+ class="mt-4"
+ data-testid="comment-settings"
+ >
+ <label>
+ {{ s__('Integrations|Comment settings:') }}
+ </label>
+ <input name="service[comment_on_event_enabled]" type="hidden" value="false" />
+ <gl-form-checkbox v-model="enableComments" name="service[comment_on_event_enabled]">
+ {{ s__('Integrations|Enable comments') }}
+ </gl-form-checkbox>
+
+ <div v-show="enableComments" class="mt-4" data-testid="comment-detail">
+ <label>
+ {{ s__('Integrations|Comment detail:') }}
+ </label>
+ <gl-form-radio v-model="commentDetail" value="standard" name="service[comment_detail]">
+ {{ s__('Integrations|Standard') }}
+ <template #help>
+ {{ s__('Integrations|Includes commit title and branch') }}
+ </template>
+ </gl-form-radio>
+ <gl-form-radio v-model="commentDetail" value="all_details" name="service[comment_detail]">
+ {{ s__('Integrations|All details') }}
+ <template #help>
+ {{
+ s__(
+ 'Integrations|Includes Standard plus entire commit message, commit hash, and issue IDs',
+ )
+ }}
+ </template>
+ </gl-form-radio>
+ </div>
+ </div>
+ </div>
+ </div>
+</template>
diff --git a/app/assets/javascripts/integrations/edit/components/trigger_fields.vue b/app/assets/javascripts/integrations/edit/components/trigger_fields.vue
new file mode 100644
index 00000000000..531490ae40c
--- /dev/null
+++ b/app/assets/javascripts/integrations/edit/components/trigger_fields.vue
@@ -0,0 +1,73 @@
+<script>
+import { startCase } from 'lodash';
+import { __ } from '~/locale';
+import { GlFormGroup, GlFormCheckbox, GlFormInput } from '@gitlab/ui';
+
+const typeWithPlaceholder = {
+ SLACK: 'slack',
+ MATTERMOST: 'mattermost',
+};
+
+const placeholderForType = {
+ [typeWithPlaceholder.SLACK]: __('Slack channels (e.g. general, development)'),
+ [typeWithPlaceholder.MATTERMOST]: __('Channel handle (e.g. town-square)'),
+};
+
+export default {
+ name: 'TriggerFields',
+ components: {
+ GlFormGroup,
+ GlFormCheckbox,
+ GlFormInput,
+ },
+ props: {
+ events: {
+ type: Array,
+ required: false,
+ default: null,
+ },
+ type: {
+ type: String,
+ required: true,
+ },
+ },
+ computed: {
+ placeholder() {
+ return placeholderForType[this.type];
+ },
+ },
+ methods: {
+ checkboxName(name) {
+ return `service[${name}]`;
+ },
+ fieldName(name) {
+ return `service[${name}]`;
+ },
+ startCase,
+ },
+};
+</script>
+
+<template>
+ <gl-form-group
+ class="gl-pt-3"
+ :label="__('Trigger')"
+ label-for="trigger-fields"
+ data-testid="trigger-fields-group"
+ >
+ <div id="trigger-fields" class="gl-pt-3">
+ <gl-form-group v-for="event in events" :key="event.title" :description="event.description">
+ <input :name="checkboxName(event.name)" type="hidden" value="false" />
+ <gl-form-checkbox v-model="event.value" :name="checkboxName(event.name)">
+ {{ startCase(event.title) }}
+ </gl-form-checkbox>
+ <gl-form-input
+ v-if="event.field"
+ v-model="event.field.value"
+ :name="fieldName(event.field.name)"
+ :placeholder="placeholder"
+ />
+ </gl-form-group>
+ </div>
+ </gl-form-group>
+</template>
diff --git a/app/assets/javascripts/integrations/edit/event_hub.js b/app/assets/javascripts/integrations/edit/event_hub.js
index 0948c2e5352..e31806ad199 100644
--- a/app/assets/javascripts/integrations/edit/event_hub.js
+++ b/app/assets/javascripts/integrations/edit/event_hub.js
@@ -1,3 +1,3 @@
-import Vue from 'vue';
+import createEventHub from '~/helpers/event_hub_factory';
-export default new Vue();
+export default createEventHub();
diff --git a/app/assets/javascripts/integrations/edit/index.js b/app/assets/javascripts/integrations/edit/index.js
index a2ba581d429..2ae1342a558 100644
--- a/app/assets/javascripts/integrations/edit/index.js
+++ b/app/assets/javascripts/integrations/edit/index.js
@@ -1,28 +1,46 @@
import Vue from 'vue';
import { parseBoolean } from '~/lib/utils/common_utils';
-import ActiveToggle from './components/active_toggle.vue';
+import IntegrationForm from './components/integration_form.vue';
export default el => {
if (!el) {
return null;
}
- const { showActive: showActiveStr, activated: activatedStr, disabled: disabledStr } = el.dataset;
- const showActive = parseBoolean(showActiveStr);
- const activated = parseBoolean(activatedStr);
- const disabled = parseBoolean(disabledStr);
-
- if (!showActive) {
- return null;
+ function parseBooleanInData(data) {
+ const result = {};
+ Object.entries(data).forEach(([key, value]) => {
+ result[key] = parseBoolean(value);
+ });
+ return result;
}
+ const { type, commentDetail, triggerEvents, ...booleanAttributes } = el.dataset;
+ const {
+ showActive,
+ activated,
+ commitEvents,
+ mergeRequestEvents,
+ enableComments,
+ } = parseBooleanInData(booleanAttributes);
+
return new Vue({
el,
render(createElement) {
- return createElement(ActiveToggle, {
+ return createElement(IntegrationForm, {
props: {
- initialActivated: activated,
- disabled,
+ activeToggleProps: {
+ initialActivated: activated,
+ },
+ showActive,
+ type,
+ triggerFieldsProps: {
+ initialTriggerCommit: commitEvents,
+ initialTriggerMergeRequest: mergeRequestEvents,
+ initialEnableComments: enableComments,
+ initialCommentDetail: commentDetail,
+ },
+ triggerEvents: JSON.parse(triggerEvents),
},
});
},
diff --git a/app/assets/javascripts/issuable_bulk_update_actions.js b/app/assets/javascripts/issuable_bulk_update_actions.js
index 45de287d44d..95e10cc75cc 100644
--- a/app/assets/javascripts/issuable_bulk_update_actions.js
+++ b/app/assets/javascripts/issuable_bulk_update_actions.js
@@ -1,7 +1,7 @@
/* eslint-disable consistent-return, func-names, array-callback-return */
import $ from 'jquery';
-import _ from 'underscore';
+import { intersection } from 'lodash';
import axios from './lib/utils/axios_utils';
import Flash from './flash';
import { __ } from './locale';
@@ -111,7 +111,7 @@ export default {
this.getElement('.selected-issuable:checked').each((i, el) => {
labelIds.push(this.getElement(`#${this.prefixId}${el.dataset.id}`).data('labels'));
});
- return _.intersection.apply(this, labelIds);
+ return intersection.apply(this, labelIds);
},
// From issuable's initial bulk selection
@@ -120,7 +120,7 @@ export default {
this.getElement('.selected-issuable:checked').each((i, el) => {
labelIds.push(this.getElement(`#${this.prefixId}${el.dataset.id}`).data('labels'));
});
- return _.intersection.apply(this, labelIds);
+ return intersection.apply(this, labelIds);
},
// From issuable's initial bulk selection
@@ -144,7 +144,7 @@ export default {
// Add uniqueIds to add it as argument for _.intersection
labelIds.unshift(uniqueIds);
// Return IDs that are present but not in all selected issueables
- return _.difference(uniqueIds, _.intersection.apply(this, labelIds));
+ return uniqueIds.filter(x => !intersection.apply(this, labelIds).includes(x));
},
getElement(selector) {
diff --git a/app/assets/javascripts/issuable_bulk_update_sidebar.js b/app/assets/javascripts/issuable_bulk_update_sidebar.js
index bd6e8433544..50562688c53 100644
--- a/app/assets/javascripts/issuable_bulk_update_sidebar.js
+++ b/app/assets/javascripts/issuable_bulk_update_sidebar.js
@@ -1,7 +1,7 @@
/* eslint-disable class-methods-use-this, no-new */
import $ from 'jquery';
-import { property } from 'underscore';
+import { property } from 'lodash';
import IssuableBulkUpdateActions from './issuable_bulk_update_actions';
import MilestoneSelect from './milestone_select';
import issueStatusSelect from './issue_status_select';
diff --git a/app/assets/javascripts/issuable_sidebar/queries/issue_sidebar.query.graphql b/app/assets/javascripts/issuable_sidebar/queries/issue_sidebar.query.graphql
new file mode 100644
index 00000000000..fe01d2c2e78
--- /dev/null
+++ b/app/assets/javascripts/issuable_sidebar/queries/issue_sidebar.query.graphql
@@ -0,0 +1,15 @@
+#import "~/graphql_shared/fragments/author.fragment.graphql"
+
+query getProjectIssue($iid: String!, $fullPath: ID!) {
+ project(fullPath: $fullPath) {
+ issue(iid: $iid) {
+ assignees {
+ nodes {
+ ...Author
+ id
+ state
+ }
+ }
+ }
+ }
+}
diff --git a/app/assets/javascripts/issuables_list/components/issuable_list_root_app.vue b/app/assets/javascripts/issuables_list/components/issuable_list_root_app.vue
new file mode 100644
index 00000000000..27a04da9541
--- /dev/null
+++ b/app/assets/javascripts/issuables_list/components/issuable_list_root_app.vue
@@ -0,0 +1,96 @@
+<script>
+import { GlAlert, GlLabel } from '@gitlab/ui';
+import getIssuesListDetailsQuery from '../queries/get_issues_list_details.query.graphql';
+import { calculateJiraImportLabel, isFinished, isInProgress } from '~/jira_import/utils';
+
+export default {
+ name: 'IssuableListRoot',
+ components: {
+ GlAlert,
+ GlLabel,
+ },
+ props: {
+ canEdit: {
+ type: Boolean,
+ required: true,
+ },
+ isJiraConfigured: {
+ type: Boolean,
+ required: true,
+ },
+ issuesPath: {
+ type: String,
+ required: true,
+ },
+ projectPath: {
+ type: String,
+ required: true,
+ },
+ },
+ data() {
+ return {
+ isFinishedAlertShowing: true,
+ isInProgressAlertShowing: true,
+ jiraImport: {},
+ };
+ },
+ apollo: {
+ jiraImport: {
+ query: getIssuesListDetailsQuery,
+ variables() {
+ return {
+ fullPath: this.projectPath,
+ };
+ },
+ update: ({ project }) => ({
+ isInProgress: isInProgress(project.jiraImportStatus),
+ isFinished: isFinished(project.jiraImportStatus),
+ label: calculateJiraImportLabel(
+ project.jiraImports.nodes,
+ project.issues.nodes.flatMap(({ labels }) => labels.nodes),
+ ),
+ }),
+ skip() {
+ return !this.isJiraConfigured || !this.canEdit;
+ },
+ },
+ },
+ computed: {
+ labelTarget() {
+ return `${this.issuesPath}?label_name[]=${encodeURIComponent(this.jiraImport.label.title)}`;
+ },
+ shouldShowFinishedAlert() {
+ return this.isFinishedAlertShowing && this.jiraImport.isFinished;
+ },
+ shouldShowInProgressAlert() {
+ return this.isInProgressAlertShowing && this.jiraImport.isInProgress;
+ },
+ },
+ methods: {
+ hideFinishedAlert() {
+ this.isFinishedAlertShowing = false;
+ },
+ hideInProgressAlert() {
+ this.isInProgressAlertShowing = false;
+ },
+ },
+};
+</script>
+
+<template>
+ <div class="issuable-list-root">
+ <gl-alert v-if="shouldShowInProgressAlert" @dismiss="hideInProgressAlert">
+ {{ __('Import in progress. Refresh page to see newly added issues.') }}
+ </gl-alert>
+ <gl-alert v-if="shouldShowFinishedAlert" variant="success" @dismiss="hideFinishedAlert">
+ {{ __('Issues successfully imported with the label') }}
+ <gl-label
+ :background-color="jiraImport.label.color"
+ scoped
+ size="sm"
+ :target="labelTarget"
+ :title="jiraImport.label.title"
+ />
+ </gl-alert>
+ </div>
+</template>
diff --git a/app/assets/javascripts/issuables_list/eventhub.js b/app/assets/javascripts/issuables_list/eventhub.js
index d1601a7d8f3..e31806ad199 100644
--- a/app/assets/javascripts/issuables_list/eventhub.js
+++ b/app/assets/javascripts/issuables_list/eventhub.js
@@ -1,5 +1,3 @@
-import Vue from 'vue';
+import createEventHub from '~/helpers/event_hub_factory';
-const issueablesEventBus = new Vue();
-
-export default issueablesEventBus;
+export default createEventHub();
diff --git a/app/assets/javascripts/issuables_list/index.js b/app/assets/javascripts/issuables_list/index.js
index 9fc7fa837ff..6bfb885a8af 100644
--- a/app/assets/javascripts/issuables_list/index.js
+++ b/app/assets/javascripts/issuables_list/index.js
@@ -1,24 +1,63 @@
import Vue from 'vue';
+import VueApollo from 'vue-apollo';
+import createDefaultClient from '~/lib/graphql';
+import { parseBoolean } from '~/lib/utils/common_utils';
+import IssuableListRootApp from './components/issuable_list_root_app.vue';
import IssuablesListApp from './components/issuables_list_app.vue';
-export default function initIssuablesList() {
- if (!gon.features || !gon.features.vueIssuablesList) {
+function mountIssuableListRootApp() {
+ const el = document.querySelector('.js-projects-issues-root');
+
+ if (!el) {
+ return false;
+ }
+
+ Vue.use(VueApollo);
+
+ const defaultClient = createDefaultClient();
+ const apolloProvider = new VueApollo({
+ defaultClient,
+ });
+
+ return new Vue({
+ el,
+ apolloProvider,
+ render(createComponent) {
+ return createComponent(IssuableListRootApp, {
+ props: {
+ canEdit: parseBoolean(el.dataset.canEdit),
+ isJiraConfigured: parseBoolean(el.dataset.isJiraConfigured),
+ issuesPath: el.dataset.issuesPath,
+ projectPath: el.dataset.projectPath,
+ },
+ });
+ },
+ });
+}
+
+function mountIssuablesListApp() {
+ if (!gon.features?.vueIssuablesList) {
return;
}
document.querySelectorAll('.js-issuables-list').forEach(el => {
const { canBulkEdit, ...data } = el.dataset;
- const props = {
- ...data,
- canBulkEdit: Boolean(canBulkEdit),
- };
-
return new Vue({
el,
render(createElement) {
- return createElement(IssuablesListApp, { props });
+ return createElement(IssuablesListApp, {
+ props: {
+ ...data,
+ canBulkEdit: Boolean(canBulkEdit),
+ },
+ });
},
});
});
}
+
+export default function initIssuablesList() {
+ mountIssuableListRootApp();
+ mountIssuablesListApp();
+}
diff --git a/app/assets/javascripts/issuables_list/queries/get_issues_list_details.query.graphql b/app/assets/javascripts/issuables_list/queries/get_issues_list_details.query.graphql
new file mode 100644
index 00000000000..b62b9b2af60
--- /dev/null
+++ b/app/assets/javascripts/issuables_list/queries/get_issues_list_details.query.graphql
@@ -0,0 +1,22 @@
+#import "~/jira_import/queries/jira_import.fragment.graphql"
+
+query($fullPath: ID!) {
+ project(fullPath: $fullPath) {
+ issues {
+ nodes {
+ labels {
+ nodes {
+ title
+ color
+ }
+ }
+ }
+ }
+ jiraImportStatus
+ jiraImports {
+ nodes {
+ ...JiraImport
+ }
+ }
+ }
+}
diff --git a/app/assets/javascripts/issue.js b/app/assets/javascripts/issue.js
index 9136a47d542..f0967e77faf 100644
--- a/app/assets/javascripts/issue.js
+++ b/app/assets/javascripts/issue.js
@@ -12,6 +12,8 @@ export default class Issue {
constructor() {
if ($('a.btn-close').length) this.initIssueBtnEventListeners();
+ if ($('.js-close-blocked-issue-warning').length) this.initIssueWarningBtnEventListener();
+
Issue.$btnNewBranch = $('#new-branch');
Issue.createMrDropdownWrap = document.querySelector('.create-mr-dropdown-wrap');
@@ -89,7 +91,7 @@ export default class Issue {
return $(document).on(
'click',
- '.js-issuable-actions a.btn-close, .js-issuable-actions a.btn-reopen',
+ '.js-issuable-actions a.btn-close, .js-issuable-actions a.btn-reopen, a.btn-close-anyway',
e => {
e.preventDefault();
e.stopImmediatePropagation();
@@ -99,19 +101,30 @@ export default class Issue {
Issue.submitNoteForm($button.closest('form'));
}
- this.disableCloseReopenButton($button);
-
- const url = $button.attr('href');
- return axios
- .put(url)
- .then(({ data }) => {
- const isClosed = $button.hasClass('btn-close');
- this.updateTopState(isClosed, data);
- })
- .catch(() => flash(issueFailMessage))
- .then(() => {
- this.disableCloseReopenButton($button, false);
- });
+ const shouldDisplayBlockedWarning = $button.hasClass('btn-issue-blocked');
+ const warningBanner = $('.js-close-blocked-issue-warning');
+ if (shouldDisplayBlockedWarning) {
+ this.toggleWarningAndCloseButton();
+ } else {
+ this.disableCloseReopenButton($button);
+
+ const url = $button.attr('href');
+ return axios
+ .put(url)
+ .then(({ data }) => {
+ const isClosed = $button.is('.btn-close, .btn-close-anyway');
+ this.updateTopState(isClosed, data);
+ if ($button.hasClass('btn-close-anyway')) {
+ warningBanner.addClass('hidden');
+ if (this.closeReopenReportToggle)
+ $('.js-issuable-close-dropdown').removeClass('hidden');
+ }
+ })
+ .catch(() => flash(issueFailMessage))
+ .then(() => {
+ this.disableCloseReopenButton($button, false);
+ });
+ }
},
);
}
@@ -137,6 +150,23 @@ export default class Issue {
this.reopenButtons.toggleClass('hidden', !isClosed);
}
+ toggleWarningAndCloseButton() {
+ const warningBanner = $('.js-close-blocked-issue-warning');
+ warningBanner.toggleClass('hidden');
+ $('.btn-close').toggleClass('hidden');
+ if (this.closeReopenReportToggle) {
+ $('.js-issuable-close-dropdown').toggleClass('hidden');
+ }
+ }
+
+ initIssueWarningBtnEventListener() {
+ return $(document).on('click', '.js-close-blocked-issue-warning button.btn-secondary', e => {
+ e.preventDefault();
+ e.stopImmediatePropagation();
+ this.toggleWarningAndCloseButton();
+ });
+ }
+
static submitNoteForm(form) {
const noteText = form.find('textarea.js-note-text').val();
if (noteText && noteText.trim().length > 0) {
diff --git a/app/assets/javascripts/issue_show/components/app.vue b/app/assets/javascripts/issue_show/components/app.vue
index b8b3a4f44fd..8cf2cda64a4 100644
--- a/app/assets/javascripts/issue_show/components/app.vue
+++ b/app/assets/javascripts/issue_show/components/app.vue
@@ -295,7 +295,7 @@ export default {
.then(res => res.data)
.then(data => this.checkForSpam(data))
.then(data => {
- if (window.location.pathname !== data.web_url) {
+ if (!window.location.pathname.includes(data.web_url)) {
visitUrl(data.web_url);
}
})
@@ -329,7 +329,7 @@ export default {
},
deleteIssuable(payload) {
- this.service
+ return this.service
.deleteIssuable(payload)
.then(res => res.data)
.then(data => {
@@ -340,7 +340,7 @@ export default {
})
.catch(() => {
createFlash(
- sprintf(s__('Error deleting %{issuableType}'), { issuableType: this.issuableType }),
+ sprintf(s__('Error deleting %{issuableType}'), { issuableType: this.issuableType }),
);
});
},
@@ -365,7 +365,12 @@ export default {
:issuable-type="issuableType"
/>
- <recaptcha-modal v-show="showRecaptcha" :html="recaptchaHTML" @close="closeRecaptchaModal" />
+ <recaptcha-modal
+ v-show="showRecaptcha"
+ ref="recaptchaModal"
+ :html="recaptchaHTML"
+ @close="closeRecaptchaModal"
+ />
</div>
<div v-else>
<title-component
diff --git a/app/assets/javascripts/issue_show/components/fields/description.vue b/app/assets/javascripts/issue_show/components/fields/description.vue
index 447d7bf21a5..35165c9b481 100644
--- a/app/assets/javascripts/issue_show/components/fields/description.vue
+++ b/app/assets/javascripts/issue_show/components/fields/description.vue
@@ -45,22 +45,24 @@ export default {
:markdown-docs-path="markdownDocsPath"
:can-attach-file="canAttachFile"
:enable-autocomplete="enableAutocomplete"
+ :textarea-value="formState.description"
>
- <textarea
- id="issue-description"
- ref="textarea"
- slot="textarea"
- v-model="formState.description"
- class="note-textarea js-gfm-input js-autosize markdown-area
- qa-description-textarea"
- dir="auto"
- data-supports-quick-actions="false"
- :aria-label="__('Description')"
- :placeholder="__('Write a comment or drag your files here…')"
- @keydown.meta.enter="updateIssuable"
- @keydown.ctrl.enter="updateIssuable"
- >
- </textarea>
+ <template #textarea>
+ <textarea
+ id="issue-description"
+ ref="textarea"
+ v-model="formState.description"
+ class="note-textarea js-gfm-input js-autosize markdown-area
+ qa-description-textarea"
+ dir="auto"
+ data-supports-quick-actions="false"
+ :aria-label="__('Description')"
+ :placeholder="__('Write a comment or drag your files here…')"
+ @keydown.meta.enter="updateIssuable"
+ @keydown.ctrl.enter="updateIssuable"
+ >
+ </textarea>
+ </template>
</markdown-field>
</div>
</template>
diff --git a/app/assets/javascripts/issue_show/event_hub.js b/app/assets/javascripts/issue_show/event_hub.js
index 0948c2e5352..e31806ad199 100644
--- a/app/assets/javascripts/issue_show/event_hub.js
+++ b/app/assets/javascripts/issue_show/event_hub.js
@@ -1,3 +1,3 @@
-import Vue from 'vue';
+import createEventHub from '~/helpers/event_hub_factory';
-export default new Vue();
+export default createEventHub();
diff --git a/app/assets/javascripts/jira_import/components/jira_import_app.vue b/app/assets/javascripts/jira_import/components/jira_import_app.vue
index b71c06e4217..d1570f52c8c 100644
--- a/app/assets/javascripts/jira_import/components/jira_import_app.vue
+++ b/app/assets/javascripts/jira_import/components/jira_import_app.vue
@@ -1,5 +1,6 @@
<script>
-import { GlAlert, GlLoadingIcon } from '@gitlab/ui';
+import { GlAlert, GlLoadingIcon, GlSprintf } from '@gitlab/ui';
+import { last } from 'lodash';
import { __ } from '~/locale';
import getJiraImportDetailsQuery from '../queries/get_jira_import_details.query.graphql';
import initiateJiraImportMutation from '../queries/initiate_jira_import.mutation.graphql';
@@ -13,6 +14,7 @@ export default {
components: {
GlAlert,
GlLoadingIcon,
+ GlSprintf,
JiraImportForm,
JiraImportProgress,
JiraImportSetup,
@@ -30,6 +32,10 @@ export default {
type: String,
required: true,
},
+ jiraIntegrationPath: {
+ type: String,
+ required: true,
+ },
jiraProjects: {
type: Array,
required: true,
@@ -47,6 +53,7 @@ export default {
return {
errorMessage: '',
showAlert: false,
+ selectedProject: undefined,
};
},
apollo: {
@@ -59,7 +66,7 @@ export default {
},
update: ({ project }) => ({
status: project.jiraImportStatus,
- import: project.jiraImports.nodes[0],
+ imports: project.jiraImports.nodes,
}),
skip() {
return !this.isJiraConfigured;
@@ -73,6 +80,24 @@ export default {
jiraProjectsOptions() {
return this.jiraProjects.map(([text, value]) => ({ text, value }));
},
+ mostRecentImport() {
+ // The backend returns JiraImports ordered by created_at asc in app/models/project.rb
+ return last(this.jiraImportDetails?.imports);
+ },
+ numberOfPreviousImportsForProject() {
+ return this.jiraImportDetails?.imports?.reduce?.(
+ (acc, jiraProject) => (jiraProject.jiraProjectKey === this.selectedProject ? acc + 1 : acc),
+ 0,
+ );
+ },
+ importLabel() {
+ return this.selectedProject
+ ? `jira-import::${this.selectedProject}-${this.numberOfPreviousImportsForProject + 1}`
+ : 'jira-import::KEY-1';
+ },
+ hasPreviousImports() {
+ return this.numberOfPreviousImportsForProject > 0;
+ },
},
methods: {
dismissAlert() {
@@ -93,6 +118,13 @@ export default {
return;
}
+ const cacheData = store.readQuery({
+ query: getJiraImportDetailsQuery,
+ variables: {
+ fullPath: this.projectPath,
+ },
+ });
+
store.writeQuery({
query: getJiraImportDetailsQuery,
variables: {
@@ -102,7 +134,10 @@ export default {
project: {
jiraImportStatus: IMPORT_STATE.SCHEDULED,
jiraImports: {
- nodes: [data.jiraImportStart.jiraImport],
+ nodes: [
+ ...cacheData.project.jiraImports.nodes,
+ data.jiraImportStart.jiraImport,
+ ],
__typename: 'JiraImportConnection',
},
// eslint-disable-next-line @gitlab/require-i18n-strings
@@ -115,6 +150,8 @@ export default {
.then(({ data }) => {
if (data.jiraImportStart.errors.length) {
this.setAlertMessage(data.jiraImportStart.errors.join('. '));
+ } else {
+ this.selectedProject = undefined;
}
})
.catch(() => this.setAlertMessage(__('There was an error importing the Jira project.')));
@@ -132,19 +169,38 @@ export default {
<gl-alert v-if="showAlert" variant="danger" @dismiss="dismissAlert">
{{ errorMessage }}
</gl-alert>
+ <gl-alert v-if="hasPreviousImports" variant="warning" :dismissible="false">
+ <gl-sprintf
+ :message="
+ __(
+ 'You have imported from this project %{numberOfPreviousImportsForProject} times before. Each new import will create duplicate issues.',
+ )
+ "
+ >
+ <template #numberOfPreviousImportsForProject>{{
+ numberOfPreviousImportsForProject
+ }}</template>
+ </gl-sprintf>
+ </gl-alert>
- <jira-import-setup v-if="!isJiraConfigured" :illustration="setupIllustration" />
+ <jira-import-setup
+ v-if="!isJiraConfigured"
+ :illustration="setupIllustration"
+ :jira-integration-path="jiraIntegrationPath"
+ />
<gl-loading-icon v-else-if="$apollo.loading" size="md" class="mt-3" />
<jira-import-progress
v-else-if="isImportInProgress"
:illustration="inProgressIllustration"
- :import-initiator="jiraImportDetails.import.scheduledBy.name"
- :import-project="jiraImportDetails.import.jiraProjectKey"
- :import-time="jiraImportDetails.import.scheduledAt"
+ :import-initiator="mostRecentImport.scheduledBy.name"
+ :import-project="mostRecentImport.jiraProjectKey"
+ :import-time="mostRecentImport.scheduledAt"
:issues-path="issuesPath"
/>
<jira-import-form
v-else
+ v-model="selectedProject"
+ :import-label="importLabel"
:issues-path="issuesPath"
:jira-projects="jiraProjectsOptions"
@initiateJiraImport="initiateJiraImport"
diff --git a/app/assets/javascripts/jira_import/components/jira_import_form.vue b/app/assets/javascripts/jira_import/components/jira_import_form.vue
index 0146f564260..c2fe7b29c28 100644
--- a/app/assets/javascripts/jira_import/components/jira_import_form.vue
+++ b/app/assets/javascripts/jira_import/components/jira_import_form.vue
@@ -13,6 +13,10 @@ export default {
currentUserAvatarUrl: gon.current_user_avatar_url,
currentUsername: gon.current_username,
props: {
+ importLabel: {
+ type: String,
+ required: true,
+ },
issuesPath: {
type: String,
required: true,
@@ -21,21 +25,25 @@ export default {
type: Array,
required: true,
},
+ value: {
+ type: String,
+ required: false,
+ default: undefined,
+ },
},
data() {
return {
- selectedOption: null,
selectState: null,
};
},
methods: {
initiateJiraImport(event) {
event.preventDefault();
- if (!this.selectedOption) {
- this.showValidationError();
- } else {
+ if (this.value) {
this.hideValidationError();
- this.$emit('initiateJiraImport', this.selectedOption);
+ this.$emit('initiateJiraImport', this.value);
+ } else {
+ this.showValidationError();
}
},
hideValidationError() {
@@ -62,10 +70,11 @@ export default {
>
<gl-form-select
id="jira-project-select"
- v-model="selectedOption"
class="mb-2"
:options="jiraProjects"
:state="selectState"
+ :value="value"
+ @change="$emit('input', $event)"
/>
</gl-form-group>
@@ -79,7 +88,7 @@ export default {
id="jira-project-label"
class="mb-2"
background-color="#428BCA"
- title="jira-import::KEY-1"
+ :title="importLabel"
scoped
/>
</gl-form-group>
diff --git a/app/assets/javascripts/jira_import/components/jira_import_progress.vue b/app/assets/javascripts/jira_import/components/jira_import_progress.vue
index 2d610224658..78f10decd31 100644
--- a/app/assets/javascripts/jira_import/components/jira_import_progress.vue
+++ b/app/assets/javascripts/jira_import/components/jira_import_progress.vue
@@ -46,6 +46,9 @@ export default {
importTime: formatDate(this.importTime),
});
},
+ issuesLink() {
+ return `${this.issuesPath}?search=${this.importProject}`;
+ },
},
};
</script>
@@ -55,7 +58,7 @@ export default {
:svg-path="illustration"
:title="__('Import in progress')"
:primary-button-text="__('View issues')"
- :primary-button-link="issuesPath"
+ :primary-button-link="issuesLink"
>
<template #description>
<p class="mb-0">{{ importInitiatorText }}</p>
diff --git a/app/assets/javascripts/jira_import/components/jira_import_setup.vue b/app/assets/javascripts/jira_import/components/jira_import_setup.vue
index 44773a773d5..285c5c815ac 100644
--- a/app/assets/javascripts/jira_import/components/jira_import_setup.vue
+++ b/app/assets/javascripts/jira_import/components/jira_import_setup.vue
@@ -11,6 +11,10 @@ export default {
type: String,
required: true,
},
+ jiraIntegrationPath: {
+ type: String,
+ required: true,
+ },
},
};
</script>
@@ -21,6 +25,6 @@ export default {
title=""
:description="__('You will first need to set up Jira Integration to use this feature.')"
:primary-button-text="__('Set up Jira Integration')"
- primary-button-link="../services/jira/edit"
+ :primary-button-link="jiraIntegrationPath"
/>
</template>
diff --git a/app/assets/javascripts/jira_import/index.js b/app/assets/javascripts/jira_import/index.js
index 8bd70e4e277..b576668fe7c 100644
--- a/app/assets/javascripts/jira_import/index.js
+++ b/app/assets/javascripts/jira_import/index.js
@@ -27,6 +27,7 @@ export default function mountJiraImportApp() {
inProgressIllustration: el.dataset.inProgressIllustration,
isJiraConfigured: parseBoolean(el.dataset.isJiraConfigured),
issuesPath: el.dataset.issuesPath,
+ jiraIntegrationPath: el.dataset.jiraIntegrationPath,
jiraProjects: el.dataset.jiraProjects ? JSON.parse(el.dataset.jiraProjects) : [],
projectPath: el.dataset.projectPath,
setupIllustration: el.dataset.setupIllustration,
diff --git a/app/assets/javascripts/jira_import/queries/get_jira_import_details.query.graphql b/app/assets/javascripts/jira_import/queries/get_jira_import_details.query.graphql
index 0eaaad580fc..aa8d03c7f17 100644
--- a/app/assets/javascripts/jira_import/queries/get_jira_import_details.query.graphql
+++ b/app/assets/javascripts/jira_import/queries/get_jira_import_details.query.graphql
@@ -3,7 +3,7 @@
query($fullPath: ID!) {
project(fullPath: $fullPath) {
jiraImportStatus
- jiraImports(last: 1) {
+ jiraImports {
nodes {
...JiraImport
}
diff --git a/app/assets/javascripts/jira_import/utils.js b/app/assets/javascripts/jira_import/utils.js
index 504cf19e44e..aa10dfc8099 100644
--- a/app/assets/javascripts/jira_import/utils.js
+++ b/app/assets/javascripts/jira_import/utils.js
@@ -1,3 +1,5 @@
+import { last } from 'lodash';
+
export const IMPORT_STATE = {
FAILED: 'failed',
FINISHED: 'finished',
@@ -8,3 +10,50 @@ export const IMPORT_STATE = {
export const isInProgress = state =>
state === IMPORT_STATE.SCHEDULED || state === IMPORT_STATE.STARTED;
+
+export const isFinished = state => state === IMPORT_STATE.FINISHED;
+
+/**
+ * Calculates the label title for the most recent Jira import.
+ *
+ * @param {Object[]} jiraImports - List of Jira imports
+ * @param {string} jiraImports[].jiraProjectKey - Jira project key
+ * @returns {string} - A label title
+ */
+const calculateJiraImportLabelTitle = jiraImports => {
+ const mostRecentJiraProjectKey = last(jiraImports)?.jiraProjectKey;
+ const jiraProjectImportCount = jiraImports.filter(
+ jiraImport => jiraImport.jiraProjectKey === mostRecentJiraProjectKey,
+ ).length;
+ return `jira-import::${mostRecentJiraProjectKey}-${jiraProjectImportCount}`;
+};
+
+/**
+ * Finds the label color from a list of labels.
+ *
+ * @param {string} labelTitle - Label title
+ * @param {Object[]} labels - List of labels
+ * @param {string} labels[].title - Label title
+ * @param {string} labels[].color - Label color
+ * @returns {string} - The label color associated with the given labelTitle
+ */
+const calculateJiraImportLabelColor = (labelTitle, labels) =>
+ labels.find(label => label.title === labelTitle)?.color;
+
+/**
+ * Calculates the label for the most recent Jira import.
+ *
+ * @param {Object[]} jiraImports - List of Jira imports
+ * @param {string} jiraImports[].jiraProjectKey - Jira project key
+ * @param {Object[]} labels - List of labels
+ * @param {string} labels[].title - Label title
+ * @param {string} labels[].color - Label color
+ * @returns {{color: string, title: string}} - A label object containing a label color and title
+ */
+export const calculateJiraImportLabel = (jiraImports, labels) => {
+ const title = calculateJiraImportLabelTitle(jiraImports);
+ return {
+ color: calculateJiraImportLabelColor(title, labels),
+ title,
+ };
+};
diff --git a/app/assets/javascripts/jobs/components/environments_block.vue b/app/assets/javascripts/jobs/components/environments_block.vue
index d9168f57cc7..28cc03c88cb 100644
--- a/app/assets/javascripts/jobs/components/environments_block.vue
+++ b/app/assets/javascripts/jobs/components/environments_block.vue
@@ -1,5 +1,5 @@
<script>
-import { escape as esc, isEmpty } from 'lodash';
+import { escape, isEmpty } from 'lodash';
import CiIcon from '~/vue_shared/components/ci_icon.vue';
import { sprintf, __ } from '../../locale';
@@ -43,7 +43,7 @@ export default {
'%{startLink}%{name}%{endLink}',
{
startLink: `<a href="${this.deploymentStatus.environment.environment_path}" class="js-environment-link">`,
- name: esc(this.deploymentStatus.environment.name),
+ name: escape(this.deploymentStatus.environment.name),
endLink: '</a>',
},
false,
@@ -74,8 +74,8 @@ export default {
}
const { name, path } = this.deploymentCluster;
- const escapedName = esc(name);
- const escapedPath = esc(path);
+ const escapedName = escape(name);
+ const escapedPath = escape(path);
if (!escapedPath) {
return escapedName;
diff --git a/app/assets/javascripts/jobs/store/actions.js b/app/assets/javascripts/jobs/store/actions.js
index f4030939f2c..0ce8dfe4442 100644
--- a/app/assets/javascripts/jobs/store/actions.js
+++ b/app/assets/javascripts/jobs/store/actions.js
@@ -220,7 +220,7 @@ export const fetchJobsForStage = ({ dispatch }, stage = {}) => {
},
})
.then(({ data }) => {
- const retriedJobs = data.retried.map(job => Object.assign({}, job, { retried: true }));
+ const retriedJobs = data.retried.map(job => ({ ...job, retried: true }));
const jobs = data.latest_statuses.concat(retriedJobs);
dispatch('receiveJobsForStageSuccess', jobs);
@@ -236,7 +236,7 @@ export const receiveJobsForStageError = ({ commit }) => {
export const triggerManualJob = ({ state }, variables) => {
const parsedVariables = variables.map(variable => {
- const copyVar = Object.assign({}, variable);
+ const copyVar = { ...variable };
delete copyVar.id;
return copyVar;
});
diff --git a/app/assets/javascripts/jobs/store/state.js b/app/assets/javascripts/jobs/store/state.js
index 5a61828ec6d..d76828ad19b 100644
--- a/app/assets/javascripts/jobs/store/state.js
+++ b/app/assets/javascripts/jobs/store/state.js
@@ -1,4 +1,4 @@
-import { isNewJobLogActive } from '../store/utils';
+import { isNewJobLogActive } from './utils';
export default () => ({
jobEndpoint: null,
diff --git a/app/assets/javascripts/labels_select.js b/app/assets/javascripts/labels_select.js
index 7107c970457..65d8866fcc3 100644
--- a/app/assets/javascripts/labels_select.js
+++ b/app/assets/javascripts/labels_select.js
@@ -1,9 +1,9 @@
-/* eslint-disable no-useless-return, func-names, no-underscore-dangle, no-new, consistent-return, no-shadow, no-param-reassign, no-lonely-if, no-else-return, dot-notation, no-empty */
+/* eslint-disable no-useless-return, func-names, no-underscore-dangle, no-new, consistent-return, no-shadow, no-param-reassign, no-lonely-if, dot-notation, no-empty */
/* global Issuable */
/* global ListLabel */
import $ from 'jquery';
-import _ from 'underscore';
+import { isEqual, escape, sortBy, template } from 'lodash';
import { sprintf, s__, __ } from './locale';
import axios from './lib/utils/axios_utils';
import IssuableBulkUpdateActions from './issuable_bulk_update_actions';
@@ -55,7 +55,6 @@ export default class LabelsSelect {
})
.get();
const scopedLabels = $dropdown.data('scopedLabels');
- const scopedLabelsDocumentationLink = $dropdown.data('scopedLabelsDocumentationLink');
const { handleClick } = options;
$sidebarLabelTooltip.tooltip();
@@ -76,7 +75,7 @@ export default class LabelsSelect {
})
.get();
- if (_.isEqual(initialSelected, selected)) return;
+ if (isEqual(initialSelected, selected)) return;
initialSelected = selected;
const data = {};
@@ -101,10 +100,9 @@ export default class LabelsSelect {
let labelCount = 0;
if (data.labels.length && issueUpdateURL) {
template = LabelsSelect.getLabelTemplate({
- labels: _.sortBy(data.labels, 'title'),
+ labels: sortBy(data.labels, 'title'),
issueUpdateURL,
enableScopedLabels: scopedLabels,
- scopedLabelsDocumentationLink,
});
labelCount = data.labels.length;
@@ -188,13 +186,13 @@ export default class LabelsSelect {
if (showNo) {
extraData.unshift({
id: 0,
- title: __('No Label'),
+ title: __('No label'),
});
}
if (showAny) {
extraData.unshift({
isAny: true,
- title: __('Any Label'),
+ title: __('Any label'),
});
}
if (extraData.length) {
@@ -269,7 +267,7 @@ export default class LabelsSelect {
}
linkEl.className = selectedClass.join(' ');
- linkEl.innerHTML = `${colorEl} ${_.escape(label.title)}`;
+ linkEl.innerHTML = `${colorEl} ${escape(label.title)}`;
const listItemEl = document.createElement('li');
listItemEl.appendChild(linkEl);
@@ -296,7 +294,7 @@ export default class LabelsSelect {
if (selected && selected.id === 0) {
this.selected = [];
- return __('No Label');
+ return __('No label');
} else if (isSelected) {
this.selected.push(title);
} else if (!isSelected && title) {
@@ -311,9 +309,8 @@ export default class LabelsSelect {
firstLabel: selectedLabels[0],
labelCount: selectedLabels.length - 1,
});
- } else {
- return defaultLabel;
}
+ return defaultLabel;
},
fieldName: $dropdown.data('fieldName'),
id(label) {
@@ -325,9 +322,8 @@ export default class LabelsSelect {
if ($dropdown.hasClass('js-filter-submit') && label.isAny == null) {
return label.title;
- } else {
- return label.id;
}
+ return label.id;
},
hidden() {
const page = $('body').attr('data-page');
@@ -436,7 +432,7 @@ export default class LabelsSelect {
if (isScopedLabel(label)) {
const prevIds = oldLabels.map(label => label.id);
const newIds = boardsStore.detail.issue.labels.map(label => label.id);
- const differentIds = _.difference(prevIds, newIds);
+ const differentIds = prevIds.filter(x => !newIds.includes(x));
$dropdown.data('marked', newIds);
$dropdownMenu
.find(differentIds.map(id => `[data-label-id="${id}"]`).join(','))
@@ -483,7 +479,7 @@ export default class LabelsSelect {
'<a href="<%- issueUpdateURL.slice(0, issueUpdateURL.lastIndexOf("/")) %>?label_name[]=<%- encodeURIComponent(label.title) %>" class="gl-link gl-label-link has-tooltip" <%= linkAttrs %> title="<%= tooltipTitleTemplate({ label, isScopedLabel, enableScopedLabels, escapeStr }) %>">';
const spanOpenTag =
'<span class="gl-label-text" style="background-color: <%= escapeStr(label.color) %>; color: <%= escapeStr(label.text_color) %>;">';
- const labelTemplate = _.template(
+ const labelTemplate = template(
[
'<span class="gl-label">',
linkOpenTag,
@@ -499,15 +495,7 @@ export default class LabelsSelect {
return escapeStr(label.text_color === '#FFFFFF' ? label.color : label.text_color);
};
- const infoIconTemplate = _.template(
- [
- '<a href="<%= scopedLabelsDocumentationLink %>" class="gl-link gl-label-icon" target="_blank" rel="noopener">',
- '<i class="fa fa-question-circle"></i>',
- '</a>',
- ].join(''),
- );
-
- const scopedLabelTemplate = _.template(
+ const scopedLabelTemplate = template(
[
'<span class="gl-label gl-label-scoped" style="color: <%= escapeStr(label.color) %>;">',
linkOpenTag,
@@ -518,12 +506,11 @@ export default class LabelsSelect {
'<%- label.title.slice(label.title.lastIndexOf("::") + 2) %>',
'</span>',
'</a>',
- '<%= infoIconTemplate({ label, scopedLabelsDocumentationLink, escapeStr }) %>',
'</span>',
].join(''),
);
- const tooltipTitleTemplate = _.template(
+ const tooltipTitleTemplate = template(
[
'<% if (isScopedLabel(label) && enableScopedLabels) { %>',
"<span class='font-weight-bold scoped-label-tooltip-title'>Scoped label</span>",
@@ -535,12 +522,12 @@ export default class LabelsSelect {
].join(''),
);
- const tpl = _.template(
+ const tpl = template(
[
- '<% _.each(labels, function(label){ %>',
+ '<% labels.forEach(function(label){ %>',
'<% if (isScopedLabel(label) && enableScopedLabels) { %>',
'<span class="d-inline-block position-relative scoped-label-wrapper">',
- '<%= scopedLabelTemplate({ label, issueUpdateURL, isScopedLabel, enableScopedLabels, rightLabelTextColor, infoIconTemplate, scopedLabelsDocumentationLink, tooltipTitleTemplate, escapeStr, linkAttrs: \'data-html="true"\' }) %>',
+ '<%= scopedLabelTemplate({ label, issueUpdateURL, isScopedLabel, enableScopedLabels, rightLabelTextColor, tooltipTitleTemplate, escapeStr, linkAttrs: \'data-html="true"\' }) %>',
'</span>',
'<% } else { %>',
'<%= labelTemplate({ label, issueUpdateURL, isScopedLabel, enableScopedLabels, tooltipTitleTemplate, escapeStr, linkAttrs: "" }) %>',
@@ -553,11 +540,10 @@ export default class LabelsSelect {
...tplData,
labelTemplate,
rightLabelTextColor,
- infoIconTemplate,
scopedLabelTemplate,
tooltipTitleTemplate,
isScopedLabel,
- escapeStr: _.escape,
+ escapeStr: escape,
});
}
diff --git a/app/assets/javascripts/lib/graphql.js b/app/assets/javascripts/lib/graphql.js
index 8d3b87d5cc0..b6c41ffa7ab 100644
--- a/app/assets/javascripts/lib/graphql.js
+++ b/app/assets/javascripts/lib/graphql.js
@@ -33,6 +33,7 @@ export default (resolvers = {}, config = {}) => {
};
return new ApolloClient({
+ typeDefs: config.typeDefs,
link: ApolloLink.split(
operation => operation.getContext().hasUpload || operation.getContext().isSingleRequest,
createUploadLink(httpOptions),
diff --git a/app/assets/javascripts/lib/utils/datetime_utility.js b/app/assets/javascripts/lib/utils/datetime_utility.js
index f6077673ad5..6b69d2febe0 100644
--- a/app/assets/javascripts/lib/utils/datetime_utility.js
+++ b/app/assets/javascripts/lib/utils/datetime_utility.js
@@ -57,6 +57,19 @@ export const getMonthNames = abbreviated => {
export const pad = (val, len = 2) => `0${val}`.slice(-len);
/**
+ * Returns i18n weekday names array.
+ */
+export const getWeekdayNames = () => [
+ __('Sunday'),
+ __('Monday'),
+ __('Tuesday'),
+ __('Wednesday'),
+ __('Thursday'),
+ __('Friday'),
+ __('Saturday'),
+];
+
+/**
* Given a date object returns the day of the week in English
* @param {date} date
* @returns {String}
diff --git a/app/assets/javascripts/lib/utils/downloader.js b/app/assets/javascripts/lib/utils/downloader.js
new file mode 100644
index 00000000000..2297f5f90ce
--- /dev/null
+++ b/app/assets/javascripts/lib/utils/downloader.js
@@ -0,0 +1,20 @@
+/**
+ * Helper function to trigger a download.
+ *
+ * - If the `fileName` is `_blank` it will open the file in a new tab.
+ * - If `fileData` is provided, it will inline the content and use data URLs to
+ * download the file. In this case the `url` property will be ignored. Please
+ * note that `fileData` needs to be Base64 encoded.
+ */
+export default ({ fileName, url, fileData }) => {
+ let href = url;
+
+ if (fileData) {
+ href = `data:text/plain;base64,${fileData}`;
+ }
+
+ const anchor = document.createElement('a');
+ anchor.download = fileName;
+ anchor.href = href;
+ anchor.click();
+};
diff --git a/app/assets/javascripts/lib/utils/keycodes.js b/app/assets/javascripts/lib/utils/keycodes.js
index 16bffc5c2cf..618266f7a09 100644
--- a/app/assets/javascripts/lib/utils/keycodes.js
+++ b/app/assets/javascripts/lib/utils/keycodes.js
@@ -1,3 +1,6 @@
+// `e.keyCode` is deprecated, these values should be migrated
+// See: https://gitlab.com/gitlab-org/gitlab/-/issues/216102
+
export const BACKSPACE_KEY_CODE = 8;
export const ENTER_KEY_CODE = 13;
export const ESC_KEY_CODE = 27;
diff --git a/app/assets/javascripts/lib/utils/keys.js b/app/assets/javascripts/lib/utils/keys.js
new file mode 100644
index 00000000000..8e5420e87ea
--- /dev/null
+++ b/app/assets/javascripts/lib/utils/keys.js
@@ -0,0 +1,4 @@
+/* eslint-disable @gitlab/require-i18n-strings */
+
+export const ESC_KEY = 'Escape';
+export const ESC_KEY_IE11 = 'Esc'; // https://developer.mozilla.org/en-US/docs/Web/API/KeyboardEvent/key
diff --git a/app/assets/javascripts/lib/utils/text_markdown.js b/app/assets/javascripts/lib/utils/text_markdown.js
index cccf9ad311c..0dfc144c363 100644
--- a/app/assets/javascripts/lib/utils/text_markdown.js
+++ b/app/assets/javascripts/lib/utils/text_markdown.js
@@ -1,4 +1,4 @@
-/* eslint-disable func-names, no-param-reassign, operator-assignment, no-else-return, consistent-return */
+/* eslint-disable func-names, no-param-reassign, operator-assignment, consistent-return */
import $ from 'jquery';
import { insertText } from '~/lib/utils/common_utils';
@@ -217,16 +217,15 @@ export function insertMarkdownText({
}
if (val.indexOf(tag) === 0) {
return String(val.replace(tag, ''));
- } else {
- return String(tag) + val;
}
+ return String(tag) + val;
})
.join('\n');
}
} else if (tag.indexOf(textPlaceholder) > -1) {
textToInsert = tag.replace(textPlaceholder, selected);
} else {
- textToInsert = String(startChar) + tag + selected + (wrap ? tag : ' ');
+ textToInsert = String(startChar) + tag + selected + (wrap ? tag : '');
}
if (removedFirstNewLine) {
diff --git a/app/assets/javascripts/lib/utils/url_utility.js b/app/assets/javascripts/lib/utils/url_utility.js
index a495d2040d3..966e6d42b80 100644
--- a/app/assets/javascripts/lib/utils/url_utility.js
+++ b/app/assets/javascripts/lib/utils/url_utility.js
@@ -63,15 +63,22 @@ export function getParameterValues(sParam, url = window.location) {
}, []);
}
-// @param {Object} params - url keys and value to merge
-// @param {String} url
+/**
+ * Merges a URL to a set of params replacing value for
+ * those already present.
+ *
+ * Also removes `null` param values from the resulting URL.
+ *
+ * @param {Object} params - url keys and value to merge
+ * @param {String} url
+ */
export function mergeUrlParams(params, url) {
const re = /^([^?#]*)(\?[^#]*)?(.*)/;
const merged = {};
- const urlparts = url.match(re);
+ const [, fullpath, query, fragment] = url.match(re);
- if (urlparts[2]) {
- urlparts[2]
+ if (query) {
+ query
.substr(1)
.split('&')
.forEach(part => {
@@ -84,11 +91,15 @@ export function mergeUrlParams(params, url) {
Object.assign(merged, params);
- const query = Object.keys(merged)
+ const newQuery = Object.keys(merged)
+ .filter(key => merged[key] !== null)
.map(key => `${encodeURIComponent(key)}=${encodeURIComponent(merged[key])}`)
.join('&');
- return `${urlparts[1]}?${query}${urlparts[3]}`;
+ if (newQuery) {
+ return `${fullpath}?${newQuery}${fragment}`;
+ }
+ return `${fullpath}${fragment}`;
}
/**
@@ -213,12 +224,45 @@ export function getBaseURL() {
}
/**
+ * Returns true if url is an absolute URL
+ *
+ * @param {String} url
+ */
+export function isAbsolute(url) {
+ return /^https?:\/\//.test(url);
+}
+
+/**
+ * Returns true if url is a root-relative URL
+ *
+ * @param {String} url
+ */
+export function isRootRelative(url) {
+ return /^\//.test(url);
+}
+
+/**
* Returns true if url is an absolute or root-relative URL
*
* @param {String} url
*/
export function isAbsoluteOrRootRelative(url) {
- return /^(https?:)?\//.test(url);
+ return isAbsolute(url) || isRootRelative(url);
+}
+
+/**
+ * Converts a relative path to an absolute or a root relative path depending
+ * on what is passed as a basePath.
+ *
+ * @param {String} path Relative path, eg. ../img/img.png
+ * @param {String} basePath Absolute or root relative path, eg. /user/project or
+ * https://gitlab.com/user/project
+ */
+export function relativePathToAbsolute(path, basePath) {
+ const absolute = isAbsolute(basePath);
+ const base = absolute ? basePath : `file:///${basePath}`;
+ const url = new URL(path, base);
+ return absolute ? url.href : decodeURIComponent(url.pathname);
}
/**
@@ -259,8 +303,10 @@ export function getWebSocketUrl(path) {
export function queryToObject(query) {
const removeQuestionMarkFromQuery = String(query).startsWith('?') ? query.slice(1) : query;
return removeQuestionMarkFromQuery.split('&').reduce((accumulator, curr) => {
- const p = curr.split('=');
- accumulator[decodeURIComponent(p[0])] = decodeURIComponent(p[1]);
+ const [key, value] = curr.split('=');
+ if (value !== undefined) {
+ accumulator[decodeURIComponent(key)] = decodeURIComponent(value);
+ }
return accumulator;
}, {});
}
diff --git a/app/assets/javascripts/line_highlighter.js b/app/assets/javascripts/line_highlighter.js
index dd868bb9f4c..ed10c7646a8 100644
--- a/app/assets/javascripts/line_highlighter.js
+++ b/app/assets/javascripts/line_highlighter.js
@@ -1,4 +1,4 @@
-/* eslint-disable func-names, no-underscore-dangle, no-param-reassign, consistent-return, no-else-return */
+/* eslint-disable func-names, no-underscore-dangle, no-param-reassign, consistent-return */
import $ from 'jquery';
@@ -54,6 +54,7 @@ LineHighlighter.prototype.bindEvents = function() {
$fileHolder.on('click', 'a[data-line-number]', this.clickHandler);
$fileHolder.on('highlight:line', this.highlightHash);
+ window.addEventListener('hashchange', e => this.highlightHash(e.target.location.hash));
};
LineHighlighter.prototype.highlightHash = function(newHash) {
@@ -127,9 +128,8 @@ LineHighlighter.prototype.hashToRange = function(hash) {
const first = parseInt(matches[1], 10);
const last = matches[2] ? parseInt(matches[2], 10) : null;
return [first, last];
- } else {
- return [null, null];
}
+ return [null, null];
};
// Highlight a single line
@@ -152,9 +152,8 @@ LineHighlighter.prototype.highlightRange = function(range) {
}
return results;
- } else {
- return this.highlightLine(range[0]);
}
+ return this.highlightLine(range[0]);
};
// Set the URL hash string
diff --git a/app/assets/javascripts/locale/sprintf.js b/app/assets/javascripts/locale/sprintf.js
index 7ab4e725d99..b4658a159d7 100644
--- a/app/assets/javascripts/locale/sprintf.js
+++ b/app/assets/javascripts/locale/sprintf.js
@@ -5,7 +5,7 @@ import { escape } from 'lodash';
@param input (translated) text with parameters (e.g. '%{num_users} users use us')
@param {Object} parameters object mapping parameter names to values (e.g. { num_users: 5 })
- @param {Boolean} escapeParameters whether parameter values should be escaped (see http://underscorejs.org/#escape)
+ @param {Boolean} escapeParameters whether parameter values should be escaped (see https://lodash.com/docs/4.17.15#escape)
@returns {String} the text with parameters replaces (e.g. '5 users use us')
@see https://ruby-doc.org/core-2.3.3/Kernel.html#method-i-sprintf
diff --git a/app/assets/javascripts/main.js b/app/assets/javascripts/main.js
index 6c8f6372795..713f57a2b27 100644
--- a/app/assets/javascripts/main.js
+++ b/app/assets/javascripts/main.js
@@ -106,12 +106,14 @@ function deferredInitialisation() {
initLogoAnimation();
initUsagePingConsent();
initUserPopovers();
- initUserTracking();
initBroadcastNotifications();
const recoverySettingsCallout = document.querySelector('.js-recovery-settings-callout');
PersistentUserCallout.factory(recoverySettingsCallout);
+ const usersOverLicenseCallout = document.querySelector('.js-users-over-license-callout');
+ PersistentUserCallout.factory(usersOverLicenseCallout);
+
if (document.querySelector('.search')) initSearchAutocomplete();
addSelectOnFocusBehaviour('.js-select-on-focus');
@@ -187,6 +189,7 @@ document.addEventListener('DOMContentLoaded', () => {
if (document.querySelector('#js-peek')) initPerformanceBar({ container: '#js-peek' });
+ initUserTracking();
initLayoutNav();
// Set the default path for all cookies to GitLab's root directory
diff --git a/app/assets/javascripts/member_expiration_date.js b/app/assets/javascripts/member_expiration_date.js
index 0dabb28ea66..ef7d8cc9efe 100644
--- a/app/assets/javascripts/member_expiration_date.js
+++ b/app/assets/javascripts/member_expiration_date.js
@@ -29,8 +29,6 @@ export default function memberExpirationDate(selector = '.js-access-expiration-d
onSelect(dateText) {
$input.val(calendar.toString(dateText));
- $input.trigger('change');
-
toggleClearInput.call($input);
},
firstDay: gon.first_day_of_week,
@@ -49,7 +47,6 @@ export default function memberExpirationDate(selector = '.js-access-expiration-d
const calendar = input.data('pikaday');
calendar.setDate(null);
- input.trigger('change');
toggleClearInput.call(input);
});
diff --git a/app/assets/javascripts/merge_request_tabs.js b/app/assets/javascripts/merge_request_tabs.js
index 87de58443e0..1795a0dbdf8 100644
--- a/app/assets/javascripts/merge_request_tabs.js
+++ b/app/assets/javascripts/merge_request_tabs.js
@@ -1,9 +1,9 @@
/* eslint-disable no-new, class-methods-use-this */
import $ from 'jquery';
-import Vue from 'vue';
import { GlBreakpointInstance as bp } from '@gitlab/ui/dist/utils';
import Cookies from 'js-cookie';
+import createEventHub from '~/helpers/event_hub_factory';
import axios from './lib/utils/axios_utils';
import flash from './flash';
import BlobForkSuggestion from './blob/blob_fork_suggestion';
@@ -93,7 +93,7 @@ export default class MergeRequestTabs {
this.pipelinesLoaded = false;
this.commitsLoaded = false;
this.fixedLayoutPref = null;
- this.eventHub = new Vue();
+ this.eventHub = createEventHub();
this.setUrl = setUrl !== undefined ? setUrl : true;
this.setCurrentAction = this.setCurrentAction.bind(this);
diff --git a/app/assets/javascripts/milestone_select.js b/app/assets/javascripts/milestone_select.js
index d15e4ecb537..e14212254a8 100644
--- a/app/assets/javascripts/milestone_select.js
+++ b/app/assets/javascripts/milestone_select.js
@@ -1,9 +1,9 @@
-/* eslint-disable one-var, no-else-return, no-self-compare, consistent-return, no-param-reassign, no-shadow */
+/* eslint-disable one-var, no-self-compare, consistent-return, no-param-reassign, no-shadow */
/* global Issuable */
/* global ListMilestone */
import $ from 'jquery';
-import _ from 'underscore';
+import { template, escape } from 'lodash';
import { __ } from '~/locale';
import '~/gl_dropdown';
import axios from './lib/utils/axios_utils';
@@ -56,11 +56,11 @@ export default class MilestoneSelect {
const $loading = $block.find('.block-loading').fadeOut();
selectedMilestoneDefault = showAny ? '' : null;
selectedMilestoneDefault =
- showNo && defaultNo ? __('No Milestone') : selectedMilestoneDefault;
+ showNo && defaultNo ? __('No milestone') : selectedMilestoneDefault;
selectedMilestone = $dropdown.data('selected') || selectedMilestoneDefault;
if (issueUpdateURL) {
- milestoneLinkTemplate = _.template(
+ milestoneLinkTemplate = template(
'<a href="<%- web_url %>" class="bold has-tooltip" data-container="body" title="<%- remaining %>"><%- title %></a>',
);
milestoneLinkNoneTemplate = `<span class="no-value">${__('None')}</span>`;
@@ -74,14 +74,14 @@ export default class MilestoneSelect {
extraOptions.push({
id: null,
name: null,
- title: __('Any Milestone'),
+ title: __('Any milestone'),
});
}
if (showNo) {
extraOptions.push({
id: -1,
- name: __('No Milestone'),
- title: __('No Milestone'),
+ name: __('No milestone'),
+ title: __('No milestone'),
});
}
if (showUpcoming) {
@@ -106,12 +106,12 @@ export default class MilestoneSelect {
if (showMenuAbove) {
$dropdown.data('glDropdown').positionMenuAbove();
}
- $(`[data-milestone-id="${_.escape(selectedMilestone)}"] > a`).addClass('is-active');
+ $(`[data-milestone-id="${escape(selectedMilestone)}"] > a`).addClass('is-active');
}),
renderRow: milestone => `
- <li data-milestone-id="${_.escape(milestone.name)}">
+ <li data-milestone-id="${escape(milestone.name)}">
<a href='#' class='dropdown-menu-milestone-link'>
- ${_.escape(milestone.title)}
+ ${escape(milestone.title)}
</a>
</li>
`,
@@ -123,19 +123,17 @@ export default class MilestoneSelect {
toggleLabel: (selected, el) => {
if (selected && 'id' in selected && $(el).hasClass('is-active')) {
return selected.title;
- } else {
- return defaultLabel;
}
+ return defaultLabel;
},
defaultLabel,
fieldName: $dropdown.data('fieldName'),
- text: milestone => _.escape(milestone.title),
+ text: milestone => escape(milestone.title),
id: milestone => {
if (!useId && !$dropdown.is('.js-issuable-form-dropdown')) {
return milestone.name;
- } else {
- return milestone.id;
}
+ return milestone.id;
},
hidden: () => {
$selectBox.hide();
@@ -148,7 +146,7 @@ export default class MilestoneSelect {
selectedMilestone = $dropdown[0].dataset.selected || selectedMilestoneDefault;
}
$('a.is-active', $el).removeClass('is-active');
- $(`[data-milestone-id="${_.escape(selectedMilestone)}"] > a`, $el).addClass('is-active');
+ $(`[data-milestone-id="${escape(selectedMilestone)}"] > a`, $el).addClass('is-active');
},
vue: $dropdown.hasClass('js-issue-board-sidebar'),
clicked: clickEvent => {
@@ -244,13 +242,12 @@ export default class MilestoneSelect {
)
.find('span')
.text(data.milestone.title);
- } else {
- $value.html(milestoneLinkNoneTemplate);
- return $sidebarCollapsedValue
- .attr('data-original-title', __('Milestone'))
- .find('span')
- .text(__('None'));
}
+ $value.html(milestoneLinkNoneTemplate);
+ return $sidebarCollapsedValue
+ .attr('data-original-title', __('Milestone'))
+ .find('span')
+ .text(__('None'));
})
.catch(() => {
// eslint-disable-next-line no-jquery/no-fade
diff --git a/app/assets/javascripts/milestones/project_milestone_combobox.vue b/app/assets/javascripts/milestones/project_milestone_combobox.vue
new file mode 100644
index 00000000000..19148d6184f
--- /dev/null
+++ b/app/assets/javascripts/milestones/project_milestone_combobox.vue
@@ -0,0 +1,228 @@
+<script>
+import {
+ GlNewDropdown,
+ GlNewDropdownDivider,
+ GlNewDropdownHeader,
+ GlNewDropdownItem,
+ GlLoadingIcon,
+ GlSearchBoxByType,
+ GlIcon,
+} from '@gitlab/ui';
+import { __, sprintf } from '~/locale';
+import Api from '~/api';
+import createFlash from '~/flash';
+import { intersection, debounce } from 'lodash';
+
+export default {
+ components: {
+ GlNewDropdown,
+ GlNewDropdownDivider,
+ GlNewDropdownHeader,
+ GlNewDropdownItem,
+ GlLoadingIcon,
+ GlSearchBoxByType,
+ GlIcon,
+ },
+ model: {
+ prop: 'preselectedMilestones',
+ event: 'change',
+ },
+ props: {
+ projectId: {
+ type: String,
+ required: true,
+ },
+ preselectedMilestones: {
+ type: Array,
+ default: () => [],
+ required: false,
+ },
+ extraLinks: {
+ type: Array,
+ default: () => [],
+ required: false,
+ },
+ },
+ data() {
+ return {
+ searchQuery: '',
+ projectMilestones: [],
+ searchResults: [],
+ selectedMilestones: [],
+ requestCount: 0,
+ };
+ },
+ translations: {
+ milestone: __('Milestone'),
+ selectMilestone: __('Select milestone'),
+ noMilestone: __('No milestone'),
+ noResultsLabel: __('No matching results'),
+ searchMilestones: __('Search Milestones'),
+ },
+ computed: {
+ selectedMilestonesLabel() {
+ if (this.milestoneTitles.length === 1) {
+ return this.milestoneTitles[0];
+ }
+
+ if (this.milestoneTitles.length > 1) {
+ const firstMilestoneName = this.milestoneTitles[0];
+ const numberOfOtherMilestones = this.milestoneTitles.length - 1;
+ return sprintf(__('%{firstMilestoneName} + %{numberOfOtherMilestones} more'), {
+ firstMilestoneName,
+ numberOfOtherMilestones,
+ });
+ }
+
+ return this.$options.translations.noMilestone;
+ },
+ milestoneTitles() {
+ return this.preselectedMilestones.map(milestone => milestone.title);
+ },
+ dropdownItems() {
+ return this.searchResults.length ? this.searchResults : this.projectMilestones;
+ },
+ noResults() {
+ return this.searchQuery.length > 2 && this.searchResults.length === 0;
+ },
+ isLoading() {
+ return this.requestCount !== 0;
+ },
+ },
+ mounted() {
+ this.fetchMilestones();
+ },
+ methods: {
+ fetchMilestones() {
+ this.requestCount += 1;
+
+ Api.projectMilestones(this.projectId)
+ .then(({ data }) => {
+ this.projectMilestones = this.getTitles(data);
+ this.selectedMilestones = intersection(this.projectMilestones, this.milestoneTitles);
+ })
+ .catch(() => {
+ createFlash(__('An error occurred while loading milestones'));
+ })
+ .finally(() => {
+ this.requestCount -= 1;
+ });
+ },
+ searchMilestones: debounce(function searchMilestones() {
+ this.requestCount += 1;
+ const options = {
+ search: this.searchQuery,
+ scope: 'milestones',
+ };
+
+ if (this.searchQuery.length < 3) {
+ this.requestCount -= 1;
+ this.searchResults = [];
+ return;
+ }
+
+ Api.projectSearch(this.projectId, options)
+ .then(({ data }) => {
+ const searchResults = this.getTitles(data);
+
+ this.searchResults = searchResults.length ? searchResults : [];
+ })
+ .catch(() => {
+ createFlash(__('An error occurred while searching for milestones'));
+ })
+ .finally(() => {
+ this.requestCount -= 1;
+ });
+ }, 100),
+ toggleMilestoneSelection(clickedMilestone) {
+ if (!clickedMilestone) return [];
+
+ let milestones = [...this.preselectedMilestones];
+ const hasMilestone = this.milestoneTitles.includes(clickedMilestone);
+
+ if (hasMilestone) {
+ milestones = milestones.filter(({ title }) => title !== clickedMilestone);
+ } else {
+ milestones.push({ title: clickedMilestone });
+ }
+
+ return milestones;
+ },
+ onMilestoneClicked(clickedMilestone) {
+ const milestones = this.toggleMilestoneSelection(clickedMilestone);
+ this.$emit('change', milestones);
+
+ this.selectedMilestones = intersection(
+ this.projectMilestones,
+ milestones.map(milestone => milestone.title),
+ );
+ },
+ isSelectedMilestone(milestoneTitle) {
+ return this.selectedMilestones.includes(milestoneTitle);
+ },
+ getTitles(milestones) {
+ return milestones.filter(({ state }) => state === 'active').map(({ title }) => title);
+ },
+ },
+};
+</script>
+
+<template>
+ <gl-new-dropdown>
+ <template slot="button-content">
+ <span ref="buttonText" class="flex-grow-1 ml-1 text-muted">{{
+ selectedMilestonesLabel
+ }}</span>
+ <gl-icon name="chevron-down" />
+ </template>
+
+ <gl-new-dropdown-header>
+ <span class="text-center d-block">{{ $options.translations.selectMilestone }}</span>
+ </gl-new-dropdown-header>
+
+ <gl-new-dropdown-divider />
+
+ <gl-search-box-by-type
+ v-model.trim="searchQuery"
+ class="m-2"
+ :placeholder="this.$options.translations.searchMilestones"
+ @input="searchMilestones"
+ />
+
+ <gl-new-dropdown-item @click="onMilestoneClicked(null)">
+ <span :class="{ 'pl-4': true, 'selected-item': selectedMilestones.length === 0 }">
+ {{ $options.translations.noMilestone }}
+ </span>
+ </gl-new-dropdown-item>
+
+ <gl-new-dropdown-divider />
+
+ <template v-if="isLoading">
+ <gl-loading-icon />
+ <gl-new-dropdown-divider />
+ </template>
+ <template v-else-if="noResults">
+ <div class="dropdown-item-space">
+ <span ref="noResults" class="pl-4">{{ $options.translations.noResultsLabel }}</span>
+ </div>
+ <gl-new-dropdown-divider />
+ </template>
+ <template v-else-if="dropdownItems.length">
+ <gl-new-dropdown-item
+ v-for="item in dropdownItems"
+ :key="item"
+ role="milestone option"
+ @click="onMilestoneClicked(item)"
+ >
+ <span :class="{ 'pl-4': true, 'selected-item': isSelectedMilestone(item) }">
+ {{ item }}
+ </span>
+ </gl-new-dropdown-item>
+ <gl-new-dropdown-divider />
+ </template>
+
+ <gl-new-dropdown-item v-for="(item, idx) in extraLinks" :key="idx" :href="item.url">
+ <span class="pl-4">{{ item.text }}</span>
+ </gl-new-dropdown-item>
+ </gl-new-dropdown>
+</template>
diff --git a/app/assets/javascripts/mirrors/ssh_mirror.js b/app/assets/javascripts/mirrors/ssh_mirror.js
index 2276a723326..986785fdfbe 100644
--- a/app/assets/javascripts/mirrors/ssh_mirror.js
+++ b/app/assets/javascripts/mirrors/ssh_mirror.js
@@ -1,5 +1,5 @@
import $ from 'jquery';
-import { escape as esc } from 'lodash';
+import { escape } from 'lodash';
import { __ } from '~/locale';
import axios from '~/lib/utils/axios_utils';
import Flash from '~/flash';
@@ -161,7 +161,7 @@ export default class SSHMirror {
const $fingerprintsList = this.$hostKeysInformation.find('.js-fingerprints-list');
let fingerprints = '';
sshHostKeys.fingerprints.forEach(fingerprint => {
- const escFingerprints = esc(fingerprint.fingerprint);
+ const escFingerprints = escape(fingerprint.fingerprint);
fingerprints += `<code>${escFingerprints}</code>`;
});
diff --git a/app/assets/javascripts/monitoring/components/alert_widget.vue b/app/assets/javascripts/monitoring/components/alert_widget.vue
new file mode 100644
index 00000000000..86a793c854e
--- /dev/null
+++ b/app/assets/javascripts/monitoring/components/alert_widget.vue
@@ -0,0 +1,286 @@
+<script>
+import { GlBadge, GlLoadingIcon, GlModalDirective, GlIcon, GlTooltip, GlSprintf } from '@gitlab/ui';
+import { s__ } from '~/locale';
+import createFlash from '~/flash';
+import AlertWidgetForm from './alert_widget_form.vue';
+import AlertsService from '../services/alerts_service';
+import { alertsValidator, queriesValidator } from '../validators';
+import { OPERATORS } from '../constants';
+import { values, get } from 'lodash';
+
+export default {
+ components: {
+ AlertWidgetForm,
+ GlBadge,
+ GlLoadingIcon,
+ GlIcon,
+ GlTooltip,
+ GlSprintf,
+ },
+ directives: {
+ GlModal: GlModalDirective,
+ },
+ props: {
+ alertsEndpoint: {
+ type: String,
+ required: true,
+ },
+ showLoadingState: {
+ type: Boolean,
+ required: false,
+ default: true,
+ },
+ // { [alertPath]: { alert_attributes } }. Populated from subsequent API calls.
+ // Includes only the metrics/alerts to be managed by this widget.
+ alertsToManage: {
+ type: Object,
+ required: false,
+ default: () => ({}),
+ validator: alertsValidator,
+ },
+ // [{ metric+query_attributes }]. Represents queries (and alerts) we know about
+ // on intial fetch. Essentially used for reference.
+ relevantQueries: {
+ type: Array,
+ required: true,
+ validator: queriesValidator,
+ },
+ modalId: {
+ type: String,
+ required: true,
+ },
+ },
+ data() {
+ return {
+ service: null,
+ errorMessage: null,
+ isLoading: false,
+ apiAction: 'create',
+ };
+ },
+ i18n: {
+ alertsCountMsg: s__('PrometheusAlerts|%{count} alerts applied'),
+ singleFiringMsg: s__('PrometheusAlerts|Firing: %{alert}'),
+ multipleFiringMsg: s__('PrometheusAlerts|%{firingCount} firing'),
+ firingAlertsTooltip: s__('PrometheusAlerts|Firing: %{alerts}'),
+ },
+ computed: {
+ singleAlertSummary() {
+ return {
+ message: this.isFiring ? this.$options.i18n.singleFiringMsg : this.thresholds[0],
+ alert: this.thresholds[0],
+ };
+ },
+ multipleAlertsSummary() {
+ return {
+ message: this.isFiring
+ ? `${this.$options.i18n.alertsCountMsg}, ${this.$options.i18n.multipleFiringMsg}`
+ : this.$options.i18n.alertsCountMsg,
+ count: this.thresholds.length,
+ firingCount: this.firingAlerts.length,
+ };
+ },
+ shouldShowLoadingIcon() {
+ return this.showLoadingState && this.isLoading;
+ },
+ thresholds() {
+ const alertsToManage = Object.keys(this.alertsToManage);
+ return alertsToManage.map(this.formatAlertSummary);
+ },
+ hasAlerts() {
+ return Boolean(Object.keys(this.alertsToManage).length);
+ },
+ hasMultipleAlerts() {
+ return this.thresholds.length > 1;
+ },
+ isFiring() {
+ return Boolean(this.firingAlerts.length);
+ },
+ firingAlerts() {
+ return values(this.alertsToManage).filter(alert =>
+ this.passedAlertThreshold(this.getQueryData(alert), alert),
+ );
+ },
+ formattedFiringAlerts() {
+ return this.firingAlerts.map(alert => this.formatAlertSummary(alert.alert_path));
+ },
+ configuredAlert() {
+ return this.hasAlerts ? values(this.alertsToManage)[0].metricId : '';
+ },
+ },
+ created() {
+ this.service = new AlertsService({ alertsEndpoint: this.alertsEndpoint });
+ this.fetchAlertData();
+ },
+ methods: {
+ fetchAlertData() {
+ this.isLoading = true;
+
+ const queriesWithAlerts = this.relevantQueries.filter(query => query.alert_path);
+
+ return Promise.all(
+ queriesWithAlerts.map(query =>
+ this.service
+ .readAlert(query.alert_path)
+ .then(alertAttributes => this.setAlert(alertAttributes, query.metricId)),
+ ),
+ )
+ .then(() => {
+ this.isLoading = false;
+ })
+ .catch(() => {
+ createFlash(s__('PrometheusAlerts|Error fetching alert'));
+ this.isLoading = false;
+ });
+ },
+ setAlert(alertAttributes, metricId) {
+ this.$emit('setAlerts', alertAttributes.alert_path, { ...alertAttributes, metricId });
+ },
+ removeAlert(alertPath) {
+ this.$emit('setAlerts', alertPath, null);
+ },
+ formatAlertSummary(alertPath) {
+ const alert = this.alertsToManage[alertPath];
+ const alertQuery = this.relevantQueries.find(query => query.metricId === alert.metricId);
+
+ return `${alertQuery.label} ${alert.operator} ${alert.threshold}`;
+ },
+ passedAlertThreshold(data, alert) {
+ const { threshold, operator } = alert;
+
+ switch (operator) {
+ case OPERATORS.greaterThan:
+ return data.some(value => value > threshold);
+ case OPERATORS.lessThan:
+ return data.some(value => value < threshold);
+ case OPERATORS.equalTo:
+ return data.some(value => value === threshold);
+ default:
+ return false;
+ }
+ },
+ getQueryData(alert) {
+ const alertQuery = this.relevantQueries.find(query => query.metricId === alert.metricId);
+
+ return get(alertQuery, 'result[0].values', []).map(value => get(value, '[1]', null));
+ },
+ showModal() {
+ this.$root.$emit('bv::show::modal', this.modalId);
+ },
+ hideModal() {
+ this.errorMessage = null;
+ this.$root.$emit('bv::hide::modal', this.modalId);
+ },
+ handleSetApiAction(apiAction) {
+ this.apiAction = apiAction;
+ },
+ handleCreate({ operator, threshold, prometheus_metric_id }) {
+ const newAlert = { operator, threshold, prometheus_metric_id };
+ this.isLoading = true;
+ this.service
+ .createAlert(newAlert)
+ .then(alertAttributes => {
+ this.setAlert(alertAttributes, prometheus_metric_id);
+ this.isLoading = false;
+ this.hideModal();
+ })
+ .catch(() => {
+ this.errorMessage = s__('PrometheusAlerts|Error creating alert');
+ this.isLoading = false;
+ });
+ },
+ handleUpdate({ alert, operator, threshold }) {
+ const updatedAlert = { operator, threshold };
+ this.isLoading = true;
+ this.service
+ .updateAlert(alert, updatedAlert)
+ .then(alertAttributes => {
+ this.setAlert(alertAttributes, this.alertsToManage[alert].metricId);
+ this.isLoading = false;
+ this.hideModal();
+ })
+ .catch(() => {
+ this.errorMessage = s__('PrometheusAlerts|Error saving alert');
+ this.isLoading = false;
+ });
+ },
+ handleDelete({ alert }) {
+ this.isLoading = true;
+ this.service
+ .deleteAlert(alert)
+ .then(() => {
+ this.removeAlert(alert);
+ this.isLoading = false;
+ this.hideModal();
+ })
+ .catch(() => {
+ this.errorMessage = s__('PrometheusAlerts|Error deleting alert');
+ this.isLoading = false;
+ });
+ },
+ },
+};
+</script>
+
+<template>
+ <div class="prometheus-alert-widget dropdown flex-grow-2 overflow-hidden">
+ <gl-loading-icon v-if="shouldShowLoadingIcon" :inline="true" />
+ <span v-else-if="errorMessage" ref="alertErrorMessage" class="alert-error-message">{{
+ errorMessage
+ }}</span>
+ <span
+ v-else-if="hasAlerts"
+ ref="alertCurrentSetting"
+ class="alert-current-setting cursor-pointer d-flex"
+ @click="showModal"
+ >
+ <gl-badge
+ :variant="isFiring ? 'danger' : 'secondary'"
+ pill
+ class="d-flex-center text-truncate"
+ >
+ <gl-icon name="warning" :size="16" class="flex-shrink-0" />
+ <span class="text-truncate gl-pl-1-deprecated-no-really-do-not-use-me">
+ <gl-sprintf
+ :message="
+ hasMultipleAlerts ? multipleAlertsSummary.message : singleAlertSummary.message
+ "
+ >
+ <template #alert>
+ {{ singleAlertSummary.alert }}
+ </template>
+ <template #count>
+ {{ multipleAlertsSummary.count }}
+ </template>
+ <template #firingCount>
+ {{ multipleAlertsSummary.firingCount }}
+ </template>
+ </gl-sprintf>
+ </span>
+ </gl-badge>
+ <gl-tooltip v-if="hasMultipleAlerts && isFiring" :target="() => $refs.alertCurrentSetting">
+ <gl-sprintf :message="$options.i18n.firingAlertsTooltip">
+ <template #alerts>
+ <div v-for="alert in formattedFiringAlerts" :key="alert.alert_path">
+ {{ alert }}
+ </div>
+ </template>
+ </gl-sprintf>
+ </gl-tooltip>
+ </span>
+ <alert-widget-form
+ ref="widgetForm"
+ :disabled="isLoading"
+ :alerts-to-manage="alertsToManage"
+ :relevant-queries="relevantQueries"
+ :error-message="errorMessage"
+ :configured-alert="configuredAlert"
+ :modal-id="modalId"
+ @create="handleCreate"
+ @update="handleUpdate"
+ @delete="handleDelete"
+ @cancel="hideModal"
+ @setAction="handleSetApiAction"
+ />
+ </div>
+</template>
diff --git a/app/assets/javascripts/monitoring/components/alert_widget_form.vue b/app/assets/javascripts/monitoring/components/alert_widget_form.vue
new file mode 100644
index 00000000000..74324daa1e3
--- /dev/null
+++ b/app/assets/javascripts/monitoring/components/alert_widget_form.vue
@@ -0,0 +1,307 @@
+<script>
+import { isEmpty, findKey } from 'lodash';
+import Vue from 'vue';
+import {
+ GlLink,
+ GlDeprecatedButton,
+ GlButtonGroup,
+ GlFormGroup,
+ GlFormInput,
+ GlDropdown,
+ GlDropdownItem,
+ GlModal,
+ GlTooltipDirective,
+} from '@gitlab/ui';
+import { __, s__ } from '~/locale';
+import Translate from '~/vue_shared/translate';
+import TrackEventDirective from '~/vue_shared/directives/track_event';
+import glFeatureFlagsMixin from '~/vue_shared/mixins/gl_feature_flags_mixin';
+import Icon from '~/vue_shared/components/icon.vue';
+import { alertsValidator, queriesValidator } from '../validators';
+import { OPERATORS } from '../constants';
+
+Vue.use(Translate);
+
+const SUBMIT_ACTION_TEXT = {
+ create: __('Add'),
+ update: __('Save'),
+ delete: __('Delete'),
+};
+
+const SUBMIT_BUTTON_CLASS = {
+ create: 'btn-success',
+ update: 'btn-success',
+ delete: 'btn-remove',
+};
+
+export default {
+ components: {
+ GlDeprecatedButton,
+ GlButtonGroup,
+ GlFormGroup,
+ GlFormInput,
+ GlDropdown,
+ GlDropdownItem,
+ GlModal,
+ GlLink,
+ Icon,
+ },
+ directives: {
+ GlTooltip: GlTooltipDirective,
+ TrackEvent: TrackEventDirective,
+ },
+ mixins: [glFeatureFlagsMixin()],
+ props: {
+ disabled: {
+ type: Boolean,
+ required: true,
+ },
+ errorMessage: {
+ type: String,
+ required: false,
+ default: '',
+ },
+ configuredAlert: {
+ type: String,
+ required: false,
+ default: '',
+ },
+ alertsToManage: {
+ type: Object,
+ required: false,
+ default: () => ({}),
+ validator: alertsValidator,
+ },
+ relevantQueries: {
+ type: Array,
+ required: true,
+ validator: queriesValidator,
+ },
+ modalId: {
+ type: String,
+ required: true,
+ },
+ },
+ data() {
+ return {
+ operators: OPERATORS,
+ operator: null,
+ threshold: null,
+ prometheusMetricId: null,
+ selectedAlert: {},
+ alertQuery: '',
+ };
+ },
+ computed: {
+ isValidQuery() {
+ // TODO: Add query validation check (most likely via http request)
+ return this.alertQuery.length ? true : null;
+ },
+ currentQuery() {
+ return this.relevantQueries.find(query => query.metricId === this.prometheusMetricId) || {};
+ },
+ formDisabled() {
+ // We need a prometheusMetricId to determine whether we're
+ // creating/updating/deleting
+ return this.disabled || !(this.prometheusMetricId || this.isValidQuery);
+ },
+ supportsComputedAlerts() {
+ return this.glFeatures.prometheusComputedAlerts;
+ },
+ queryDropdownLabel() {
+ return this.currentQuery.label || s__('PrometheusAlerts|Select query');
+ },
+ haveValuesChanged() {
+ return (
+ this.operator &&
+ this.threshold === Number(this.threshold) &&
+ (this.operator !== this.selectedAlert.operator ||
+ this.threshold !== this.selectedAlert.threshold)
+ );
+ },
+ submitAction() {
+ if (isEmpty(this.selectedAlert)) return 'create';
+ if (this.haveValuesChanged) return 'update';
+ return 'delete';
+ },
+ submitActionText() {
+ return SUBMIT_ACTION_TEXT[this.submitAction];
+ },
+ submitButtonClass() {
+ return SUBMIT_BUTTON_CLASS[this.submitAction];
+ },
+ isSubmitDisabled() {
+ return this.disabled || (this.submitAction === 'create' && !this.haveValuesChanged);
+ },
+ dropdownTitle() {
+ return this.submitAction === 'create'
+ ? s__('PrometheusAlerts|Add alert')
+ : s__('PrometheusAlerts|Edit alert');
+ },
+ },
+ watch: {
+ alertsToManage() {
+ this.resetAlertData();
+ },
+ submitAction() {
+ this.$emit('setAction', this.submitAction);
+ },
+ },
+ methods: {
+ selectQuery(queryId) {
+ const existingAlertPath = findKey(this.alertsToManage, alert => alert.metricId === queryId);
+ const existingAlert = this.alertsToManage[existingAlertPath];
+
+ if (existingAlert) {
+ this.selectedAlert = existingAlert;
+ this.operator = existingAlert.operator;
+ this.threshold = existingAlert.threshold;
+ } else {
+ this.selectedAlert = {};
+ this.operator = this.operators.greaterThan;
+ this.threshold = null;
+ }
+
+ this.prometheusMetricId = queryId;
+ },
+ handleHidden() {
+ this.resetAlertData();
+ this.$emit('cancel');
+ },
+ handleSubmit(e) {
+ e.preventDefault();
+ this.$emit(this.submitAction, {
+ alert: this.selectedAlert.alert_path,
+ operator: this.operator,
+ threshold: this.threshold,
+ prometheus_metric_id: this.prometheusMetricId,
+ });
+ },
+ handleShown() {
+ if (this.configuredAlert) {
+ this.selectQuery(this.configuredAlert);
+ } else if (this.relevantQueries.length === 1) {
+ this.selectQuery(this.relevantQueries[0].metricId);
+ }
+ },
+ resetAlertData() {
+ this.operator = null;
+ this.threshold = null;
+ this.prometheusMetricId = null;
+ this.selectedAlert = {};
+ },
+ getAlertFormActionTrackingOption() {
+ const label = `${this.submitAction}_alert`;
+ return {
+ category: document.body.dataset.page,
+ action: 'click_button',
+ label,
+ };
+ },
+ },
+ alertQueryText: {
+ label: __('Query'),
+ validFeedback: __('Query is valid'),
+ invalidFeedback: __('Invalid query'),
+ descriptionTooltip: __(
+ 'Example: Usage = single query. (Requested) / (Capacity) = multiple queries combined into a formula.',
+ ),
+ },
+};
+</script>
+
+<template>
+ <gl-modal
+ ref="alertModal"
+ :title="dropdownTitle"
+ :modal-id="modalId"
+ :ok-variant="submitAction === 'delete' ? 'danger' : 'success'"
+ :ok-disabled="formDisabled"
+ @ok="handleSubmit"
+ @hidden="handleHidden"
+ @shown="handleShown"
+ >
+ <div v-if="errorMessage" class="alert-modal-message danger_message">{{ errorMessage }}</div>
+ <div class="alert-form">
+ <gl-form-group
+ v-if="supportsComputedAlerts"
+ :label="$options.alertQueryText.label"
+ label-for="alert-query-input"
+ :valid-feedback="$options.alertQueryText.validFeedback"
+ :invalid-feedback="$options.alertQueryText.invalidFeedback"
+ :state="isValidQuery"
+ >
+ <gl-form-input id="alert-query-input" v-model.trim="alertQuery" :state="isValidQuery" />
+ <template #description>
+ <div class="d-flex align-items-center">
+ {{ __('Single or combined queries') }}
+ <icon
+ v-gl-tooltip="$options.alertQueryText.descriptionTooltip"
+ name="question"
+ class="prepend-left-4"
+ />
+ </div>
+ </template>
+ </gl-form-group>
+ <gl-form-group v-else label-for="alert-query-dropdown" :label="$options.alertQueryText.label">
+ <gl-dropdown
+ id="alert-query-dropdown"
+ :text="queryDropdownLabel"
+ toggle-class="dropdown-menu-toggle qa-alert-query-dropdown"
+ >
+ <gl-dropdown-item
+ v-for="query in relevantQueries"
+ :key="query.metricId"
+ data-qa-selector="alert_query_option"
+ @click="selectQuery(query.metricId)"
+ >
+ {{ query.label }}
+ </gl-dropdown-item>
+ </gl-dropdown>
+ </gl-form-group>
+ <gl-button-group class="mb-2" :label="s__('PrometheusAlerts|Operator')">
+ <gl-deprecated-button
+ :class="{ active: operator === operators.greaterThan }"
+ :disabled="formDisabled"
+ type="button"
+ @click="operator = operators.greaterThan"
+ >
+ {{ operators.greaterThan }}
+ </gl-deprecated-button>
+ <gl-deprecated-button
+ :class="{ active: operator === operators.equalTo }"
+ :disabled="formDisabled"
+ type="button"
+ @click="operator = operators.equalTo"
+ >
+ {{ operators.equalTo }}
+ </gl-deprecated-button>
+ <gl-deprecated-button
+ :class="{ active: operator === operators.lessThan }"
+ :disabled="formDisabled"
+ type="button"
+ @click="operator = operators.lessThan"
+ >
+ {{ operators.lessThan }}
+ </gl-deprecated-button>
+ </gl-button-group>
+ <gl-form-group :label="s__('PrometheusAlerts|Threshold')" label-for="alerts-threshold">
+ <gl-form-input
+ id="alerts-threshold"
+ v-model.number="threshold"
+ :disabled="formDisabled"
+ type="number"
+ data-qa-selector="alert_threshold_field"
+ />
+ </gl-form-group>
+ </div>
+ <template #modal-ok>
+ <gl-link
+ v-track-event="getAlertFormActionTrackingOption()"
+ class="text-reset text-decoration-none"
+ >
+ {{ submitActionText }}
+ </gl-link>
+ </template>
+ </gl-modal>
+</template>
diff --git a/app/assets/javascripts/monitoring/components/charts/anomaly.vue b/app/assets/javascripts/monitoring/components/charts/anomaly.vue
index 447f8845506..34da5885c97 100644
--- a/app/assets/javascripts/monitoring/components/charts/anomaly.vue
+++ b/app/assets/javascripts/monitoring/components/charts/anomaly.vue
@@ -3,7 +3,7 @@ import { flattenDeep, isNumber } from 'lodash';
import { GlChartSeriesLabel } from '@gitlab/ui/dist/charts';
import { roundOffFloat } from '~/lib/utils/common_utils';
import { hexToRgb } from '~/lib/utils/color_utils';
-import { areaOpacityValues, symbolSizes, colorValues } from '../../constants';
+import { areaOpacityValues, symbolSizes, colorValues, panelTypes } from '../../constants';
import { graphDataValidatorForAnomalyValues } from '../../utils';
import MonitorTimeSeriesChart from './time_series.vue';
@@ -91,7 +91,7 @@ export default {
]);
return {
...this.graphData,
- type: 'line-chart',
+ type: panelTypes.LINE_CHART,
metrics: [metricQuery],
};
},
@@ -209,7 +209,7 @@ export default {
:series-config="metricSeriesConfig"
>
<slot></slot>
- <template v-slot:tooltipContent="slotProps">
+ <template #tooltip-content="slotProps">
<div
v-for="(content, seriesIndex) in slotProps.tooltip.content"
:key="seriesIndex"
diff --git a/app/assets/javascripts/monitoring/components/charts/heatmap.vue b/app/assets/javascripts/monitoring/components/charts/heatmap.vue
index 0a0165a113e..55a25ee09fd 100644
--- a/app/assets/javascripts/monitoring/components/charts/heatmap.vue
+++ b/app/assets/javascripts/monitoring/components/charts/heatmap.vue
@@ -63,7 +63,7 @@ export default {
};
</script>
<template>
- <div v-gl-resize-observer-directive="onResize" class="col-12 col-lg-6">
+ <div v-gl-resize-observer-directive="onResize">
<gl-heatmap
ref="heatmapChart"
v-bind="$attrs"
diff --git a/app/assets/javascripts/monitoring/components/charts/time_series.vue b/app/assets/javascripts/monitoring/components/charts/time_series.vue
index bf40e8f448e..8f37a12af75 100644
--- a/app/assets/javascripts/monitoring/components/charts/time_series.vue
+++ b/app/assets/javascripts/monitoring/components/charts/time_series.vue
@@ -6,7 +6,7 @@ import dateFormat from 'dateformat';
import { s__, __ } from '~/locale';
import { getSvgIconPathContent } from '~/lib/utils/icon_utils';
import Icon from '~/vue_shared/components/icon.vue';
-import { chartHeight, lineTypes, lineWidths, dateFormats } from '../../constants';
+import { panelTypes, chartHeight, lineTypes, lineWidths, dateFormats } from '../../constants';
import { getYAxisOptions, getChartGrid, getTooltipFormatter } from './options';
import { annotationsYAxis, generateAnnotationsSeries } from './annotations';
import { makeDataSeries } from '~/helpers/monitor_helper';
@@ -64,10 +64,10 @@ export default {
required: false,
default: '',
},
- singleEmbed: {
- type: Boolean,
+ height: {
+ type: Number,
required: false,
- default: false,
+ default: chartHeight,
},
thresholds: {
type: Array,
@@ -100,7 +100,6 @@ export default {
sha: '',
},
width: 0,
- height: chartHeight,
svgs: {},
primaryColor: null,
throttledDatazoom: null,
@@ -211,8 +210,8 @@ export default {
},
glChartComponent() {
const chartTypes = {
- 'area-chart': GlAreaChart,
- 'line-chart': GlLineChart,
+ [panelTypes.AREA_CHART]: GlAreaChart,
+ [panelTypes.LINE_CHART]: GlLineChart,
};
return chartTypes[this.graphData.type] || GlAreaChart;
},
diff --git a/app/assets/javascripts/monitoring/components/dashboard.vue b/app/assets/javascripts/monitoring/components/dashboard.vue
index 4d60b02d0df..2018c706b11 100644
--- a/app/assets/javascripts/monitoring/components/dashboard.vue
+++ b/app/assets/javascripts/monitoring/components/dashboard.vue
@@ -1,8 +1,10 @@
<script>
-import { debounce, pickBy } from 'lodash';
+import { debounce } from 'lodash';
import { mapActions, mapState, mapGetters } from 'vuex';
import VueDraggable from 'vuedraggable';
import {
+ GlIcon,
+ GlButton,
GlDeprecatedButton,
GlDropdown,
GlDropdownItem,
@@ -14,10 +16,10 @@ import {
GlModalDirective,
GlTooltipDirective,
} from '@gitlab/ui';
-import PanelType from 'ee_else_ce/monitoring/components/panel_type.vue';
+import DashboardPanel from './dashboard_panel.vue';
import { s__ } from '~/locale';
import createFlash from '~/flash';
-import glFeatureFlagsMixin from '~/vue_shared/mixins/gl_feature_flags_mixin';
+import { ESC_KEY, ESC_KEY_IE11 } from '~/lib/utils/keys';
import CustomMetricsFormFields from '~/custom_metrics/components/custom_metrics_form_fields.vue';
import { mergeUrlParams, redirectTo, updateHistory } from '~/lib/utils/url_utility';
import invalidUrl from '~/lib/utils/invalid_url';
@@ -28,17 +30,27 @@ import GraphGroup from './graph_group.vue';
import EmptyState from './empty_state.vue';
import GroupEmptyState from './group_empty_state.vue';
import DashboardsDropdown from './dashboards_dropdown.vue';
+import VariablesSection from './variables_section.vue';
import TrackEventDirective from '~/vue_shared/directives/track_event';
-import { getAddMetricTrackingOptions, timeRangeToUrl, timeRangeFromUrl } from '../utils';
+import {
+ getAddMetricTrackingOptions,
+ timeRangeToUrl,
+ timeRangeFromUrl,
+ panelToUrl,
+ expandedPanelPayloadFromUrl,
+ convertVariablesForURL,
+} from '../utils';
import { metricStates } from '../constants';
import { defaultTimeRange, timeRanges } from '~/vue_shared/constants';
export default {
components: {
VueDraggable,
- PanelType,
+ DashboardPanel,
Icon,
+ GlIcon,
+ GlButton,
GlDeprecatedButton,
GlDropdown,
GlLoadingIcon,
@@ -54,13 +66,14 @@ export default {
EmptyState,
GroupEmptyState,
DashboardsDropdown,
+
+ VariablesSection,
},
directives: {
GlModal: GlModalDirective,
GlTooltip: GlTooltipDirective,
TrackEvent: TrackEventDirective,
},
- mixins: [glFeatureFlagsMixin()],
props: {
externalDashboardUrl: {
type: String,
@@ -197,7 +210,6 @@ export default {
},
data() {
return {
- state: 'gettingStarted',
formIsValid: null,
selectedTimeRange: timeRangeFromUrl() || defaultTimeRange,
hasValidDates: true,
@@ -212,16 +224,16 @@ export default {
'showEmptyState',
'useDashboardEndpoint',
'allDashboards',
- 'additionalPanelTypesEnabled',
'environmentsLoading',
+ 'expandedPanel',
+ 'promVariables',
+ 'isUpdatingStarredValue',
+ ]),
+ ...mapGetters('monitoringDashboard', [
+ 'selectedDashboard',
+ 'getMetricStates',
+ 'filteredEnvironments',
]),
- ...mapGetters('monitoringDashboard', ['getMetricStates', 'filteredEnvironments']),
- firstDashboard() {
- return this.allDashboards.length > 0 ? this.allDashboards[0] : {};
- },
- selectedDashboard() {
- return this.allDashboards.find(d => d.path === this.currentDashboard) || this.firstDashboard;
- },
showRearrangePanelsBtn() {
return !this.showEmptyState && this.rearrangePanelsAvailable;
},
@@ -229,20 +241,44 @@ export default {
return (
this.customMetricsAvailable &&
!this.showEmptyState &&
- this.firstDashboard === this.selectedDashboard
- );
- },
- hasHeaderButtons() {
- return (
- this.addingMetricsAvailable ||
- this.showRearrangePanelsBtn ||
- this.selectedDashboard.can_edit ||
- this.externalDashboardUrl.length
+ // Custom metrics only avaialble on system dashboards because
+ // they are stored in the database. This can be improved. See:
+ // https://gitlab.com/gitlab-org/gitlab/-/issues/28241
+ this.selectedDashboard?.system_dashboard
);
},
shouldShowEnvironmentsDropdownNoMatchedMsg() {
return !this.environmentsLoading && this.filteredEnvironments.length === 0;
},
+ shouldShowVariablesSection() {
+ return Object.keys(this.promVariables).length > 0;
+ },
+ },
+ watch: {
+ dashboard(newDashboard) {
+ try {
+ const expandedPanel = expandedPanelPayloadFromUrl(newDashboard);
+ if (expandedPanel) {
+ this.setExpandedPanel(expandedPanel);
+ }
+ } catch {
+ createFlash(
+ s__(
+ 'Metrics|Link contains invalid chart information, please verify the link to see the expanded panel.',
+ ),
+ );
+ }
+ },
+ expandedPanel: {
+ handler({ group, panel }) {
+ const dashboardPath = this.currentDashboard || this.selectedDashboard?.path;
+ updateHistory({
+ url: panelToUrl(dashboardPath, convertVariablesForURL(this.promVariables), group, panel),
+ title: document.title,
+ });
+ },
+ deep: true,
+ },
},
created() {
this.setInitialState({
@@ -255,6 +291,10 @@ export default {
logsPath: this.logsPath,
currentEnvironmentName: this.currentEnvironmentName,
});
+ window.addEventListener('keyup', this.onKeyup);
+ },
+ destroyed() {
+ window.removeEventListener('keyup', this.onKeyup);
},
mounted() {
if (!this.hasMetrics) {
@@ -273,6 +313,9 @@ export default {
'setInitialState',
'setPanelGroupMetrics',
'filterEnvironments',
+ 'setExpandedPanel',
+ 'clearExpandedPanel',
+ 'toggleStarredValue',
]),
updatePanels(key, panels) {
this.setPanelGroupMetrics({
@@ -299,11 +342,9 @@ export default {
// As a fallback, switch to default time range instead
this.selectedTimeRange = defaultTimeRange;
},
-
- generateLink(group, title, yLabel) {
- const dashboard = this.currentDashboard || this.firstDashboard.path;
- const params = pickBy({ dashboard, group, title, y_label: yLabel }, value => value != null);
- return mergeUrlParams(params, window.location.href);
+ generatePanelUrl(groupKey, panel) {
+ const dashboardPath = this.currentDashboard || this.selectedDashboard?.path;
+ return panelToUrl(dashboardPath, convertVariablesForURL(this.promVariables), groupKey, panel);
},
hideAddMetricModal() {
this.$refs.addMetricModal.hide();
@@ -366,11 +407,28 @@ export default {
});
this.selectedTimeRange = { start, end };
},
+ onExpandPanel(group, panel) {
+ this.setExpandedPanel({ group, panel });
+ },
+ onGoBack() {
+ this.clearExpandedPanel();
+ },
+ onKeyup(event) {
+ const { key } = event;
+ if (key === ESC_KEY || key === ESC_KEY_IE11) {
+ this.clearExpandedPanel();
+ }
+ },
},
addMetric: {
title: s__('Metrics|Add metric'),
modalId: 'add-metric',
},
+ i18n: {
+ goBackLabel: s__('Metrics|Go back (Esc)'),
+ starDashboard: s__('Metrics|Star dashboard'),
+ unstarDashboard: s__('Metrics|Unstar dashboard'),
+ },
};
</script>
@@ -388,7 +446,6 @@ export default {
class="flex-grow-1"
toggle-class="dropdown-menu-toggle"
:default-branch="defaultBranch"
- :selected-dashboard="selectedDashboard"
@selectDashboard="selectDashboard($event)"
/>
</div>
@@ -443,7 +500,7 @@ export default {
<date-time-picker
ref="dateTimePicker"
class="flex-grow-1 show-last-dropdown"
- data-qa-selector="show_last_dropdown"
+ data-qa-selector="range_picker_dropdown"
:value="selectedTimeRange"
:options="timeRanges"
@input="onDateTimePickerInput"
@@ -467,6 +524,32 @@ export default {
<div class="flex-grow-1"></div>
<div class="d-sm-flex">
+ <div v-if="selectedDashboard" class="mb-2 mr-2 d-flex">
+ <!--
+ wrapper for tooltip as button can be `disabled`
+ https://bootstrap-vue.org/docs/components/tooltip#disabled-elements
+ -->
+ <div
+ v-gl-tooltip
+ class="flex-grow-1"
+ :title="
+ selectedDashboard.starred
+ ? $options.i18n.unstarDashboard
+ : $options.i18n.starDashboard
+ "
+ >
+ <gl-deprecated-button
+ ref="toggleStarBtn"
+ class="w-100"
+ :disabled="isUpdatingStarredValue"
+ variant="default"
+ @click="toggleStarredValue()"
+ >
+ <gl-icon :name="selectedDashboard.starred ? 'star' : 'star-o'" />
+ </gl-deprecated-button>
+ </div>
+ </div>
+
<div v-if="showRearrangePanelsBtn" class="mb-2 mr-2 d-flex">
<gl-deprecated-button
:pressed="isRearrangingPanels"
@@ -516,7 +599,10 @@ export default {
</gl-modal>
</div>
- <div v-if="selectedDashboard.can_edit" class="mb-2 mr-2 d-flex d-sm-block">
+ <div
+ v-if="selectedDashboard && selectedDashboard.can_edit"
+ class="mb-2 mr-2 d-flex d-sm-block"
+ >
<gl-deprecated-button
class="flex-grow-1 js-edit-link"
:href="selectedDashboard.project_blob_path"
@@ -539,61 +625,92 @@ export default {
</div>
</div>
</div>
-
+ <variables-section v-if="shouldShowVariablesSection && !showEmptyState" />
<div v-if="!showEmptyState">
- <graph-group
- v-for="(groupData, index) in dashboard.panelGroups"
- :key="`${groupData.group}.${groupData.priority}`"
- :name="groupData.group"
- :show-panels="showPanels"
- :collapse-group="collapseGroup(groupData.key)"
+ <dashboard-panel
+ v-show="expandedPanel.panel"
+ ref="expandedPanel"
+ :settings-path="settingsPath"
+ :clipboard-text="generatePanelUrl(expandedPanel.group, expandedPanel.panel)"
+ :graph-data="expandedPanel.panel"
+ :alerts-endpoint="alertsEndpoint"
+ :height="600"
+ :prometheus-alerts-available="prometheusAlertsAvailable"
+ @timerangezoom="onTimeRangeZoom"
>
- <vue-draggable
- v-if="!groupSingleEmptyState(groupData.key)"
- :value="groupData.panels"
- group="metrics-dashboard"
- :component-data="{ attrs: { class: 'row mx-0 w-100' } }"
- :disabled="!isRearrangingPanels"
- @input="updatePanels(groupData.key, $event)"
+ <template #topLeft>
+ <gl-button
+ ref="goBackBtn"
+ v-gl-tooltip
+ class="mr-3 my-3"
+ :title="$options.i18n.goBackLabel"
+ @click="onGoBack"
+ >
+ <gl-icon
+ name="arrow-left"
+ :aria-label="$options.i18n.goBackLabel"
+ class="text-secondary"
+ />
+ </gl-button>
+ </template>
+ </dashboard-panel>
+
+ <div v-show="!expandedPanel.panel">
+ <graph-group
+ v-for="groupData in dashboard.panelGroups"
+ :key="`${groupData.group}.${groupData.priority}`"
+ :name="groupData.group"
+ :show-panels="showPanels"
+ :collapse-group="collapseGroup(groupData.key)"
>
- <div
- v-for="(graphData, graphIndex) in groupData.panels"
- :key="`panel-type-${graphIndex}`"
- class="col-12 col-lg-6 px-2 mb-2 draggable"
- :class="{ 'draggable-enabled': isRearrangingPanels }"
+ <vue-draggable
+ v-if="!groupSingleEmptyState(groupData.key)"
+ :value="groupData.panels"
+ group="metrics-dashboard"
+ :component-data="{ attrs: { class: 'row mx-0 w-100' } }"
+ :disabled="!isRearrangingPanels"
+ @input="updatePanels(groupData.key, $event)"
>
- <div class="position-relative draggable-panel js-draggable-panel">
- <div
- v-if="isRearrangingPanels"
- class="draggable-remove js-draggable-remove p-2 w-100 position-absolute d-flex justify-content-end"
- @click="removePanel(groupData.key, groupData.panels, graphIndex)"
- >
- <a class="mx-2 p-2 draggable-remove-link" :aria-label="__('Remove')">
- <icon name="close" />
- </a>
- </div>
+ <div
+ v-for="(graphData, graphIndex) in groupData.panels"
+ :key="`dashboard-panel-${graphIndex}`"
+ class="col-12 col-lg-6 px-2 mb-2 draggable"
+ :class="{ 'draggable-enabled': isRearrangingPanels }"
+ >
+ <div class="position-relative draggable-panel js-draggable-panel">
+ <div
+ v-if="isRearrangingPanels"
+ class="draggable-remove js-draggable-remove p-2 w-100 position-absolute d-flex justify-content-end"
+ @click="removePanel(groupData.key, groupData.panels, graphIndex)"
+ >
+ <a class="mx-2 p-2 draggable-remove-link" :aria-label="__('Remove')">
+ <icon name="close" />
+ </a>
+ </div>
- <panel-type
- :clipboard-text="generateLink(groupData.group, graphData.title, graphData.y_label)"
- :graph-data="graphData"
- :alerts-endpoint="alertsEndpoint"
- :prometheus-alerts-available="prometheusAlertsAvailable"
- :index="`${index}-${graphIndex}`"
- @timerangezoom="onTimeRangeZoom"
- />
+ <dashboard-panel
+ :settings-path="settingsPath"
+ :clipboard-text="generatePanelUrl(groupData.group, graphData)"
+ :graph-data="graphData"
+ :alerts-endpoint="alertsEndpoint"
+ :prometheus-alerts-available="prometheusAlertsAvailable"
+ @timerangezoom="onTimeRangeZoom"
+ @expand="onExpandPanel(groupData.group, graphData)"
+ />
+ </div>
</div>
+ </vue-draggable>
+ <div v-else class="py-5 col col-sm-10 col-md-8 col-lg-7 col-xl-6">
+ <group-empty-state
+ ref="empty-group"
+ :documentation-path="documentationPath"
+ :settings-path="settingsPath"
+ :selected-state="groupSingleEmptyState(groupData.key)"
+ :svg-path="emptyNoDataSmallSvgPath"
+ />
</div>
- </vue-draggable>
- <div v-else class="py-5 col col-sm-10 col-md-8 col-lg-7 col-xl-6">
- <group-empty-state
- ref="empty-group"
- :documentation-path="documentationPath"
- :settings-path="settingsPath"
- :selected-state="groupSingleEmptyState(groupData.key)"
- :svg-path="emptyNoDataSmallSvgPath"
- />
- </div>
- </graph-group>
+ </graph-group>
+ </div>
</div>
<empty-state
v-else
diff --git a/app/assets/javascripts/monitoring/components/dashboard_panel.vue b/app/assets/javascripts/monitoring/components/dashboard_panel.vue
new file mode 100644
index 00000000000..48825fda5c8
--- /dev/null
+++ b/app/assets/javascripts/monitoring/components/dashboard_panel.vue
@@ -0,0 +1,393 @@
+<script>
+import { mapState } from 'vuex';
+import { pickBy } from 'lodash';
+import invalidUrl from '~/lib/utils/invalid_url';
+import {
+ GlResizeObserverDirective,
+ GlIcon,
+ GlLoadingIcon,
+ GlDropdown,
+ GlDropdownItem,
+ GlModal,
+ GlModalDirective,
+ GlTooltip,
+ GlTooltipDirective,
+} from '@gitlab/ui';
+import { __, n__ } from '~/locale';
+import { panelTypes } from '../constants';
+
+import MonitorEmptyChart from './charts/empty_chart.vue';
+import MonitorTimeSeriesChart from './charts/time_series.vue';
+import MonitorAnomalyChart from './charts/anomaly.vue';
+import MonitorSingleStatChart from './charts/single_stat.vue';
+import MonitorHeatmapChart from './charts/heatmap.vue';
+import MonitorColumnChart from './charts/column.vue';
+import MonitorBarChart from './charts/bar.vue';
+import MonitorStackedColumnChart from './charts/stacked_column.vue';
+
+import TrackEventDirective from '~/vue_shared/directives/track_event';
+import AlertWidget from './alert_widget.vue';
+import { timeRangeToUrl, downloadCSVOptions, generateLinkToChartOptions } from '../utils';
+
+const events = {
+ timeRangeZoom: 'timerangezoom',
+ expand: 'expand',
+};
+
+export default {
+ components: {
+ MonitorEmptyChart,
+ AlertWidget,
+ GlIcon,
+ GlLoadingIcon,
+ GlTooltip,
+ GlDropdown,
+ GlDropdownItem,
+ GlModal,
+ },
+ directives: {
+ GlResizeObserver: GlResizeObserverDirective,
+ GlModal: GlModalDirective,
+ GlTooltip: GlTooltipDirective,
+ TrackEvent: TrackEventDirective,
+ },
+ props: {
+ clipboardText: {
+ type: String,
+ required: false,
+ default: '',
+ },
+ graphData: {
+ type: Object,
+ required: false,
+ default: null,
+ },
+ groupId: {
+ type: String,
+ required: false,
+ default: 'dashboard-panel',
+ },
+ namespace: {
+ type: String,
+ required: false,
+ default: 'monitoringDashboard',
+ },
+ alertsEndpoint: {
+ type: String,
+ required: false,
+ default: null,
+ },
+ prometheusAlertsAvailable: {
+ type: Boolean,
+ required: false,
+ default: false,
+ },
+ settingsPath: {
+ type: String,
+ required: false,
+ default: null,
+ },
+ },
+ data() {
+ return {
+ showTitleTooltip: false,
+ zoomedTimeRange: null,
+ allAlerts: {},
+ expandBtnAvailable: Boolean(this.$listeners[events.expand]),
+ };
+ },
+ computed: {
+ // Use functions to support dynamic namespaces in mapXXX helpers. Pattern described
+ // in https://github.com/vuejs/vuex/issues/863#issuecomment-329510765
+ ...mapState({
+ deploymentData(state) {
+ return state[this.namespace].deploymentData;
+ },
+ annotations(state) {
+ return state[this.namespace].annotations;
+ },
+ projectPath(state) {
+ return state[this.namespace].projectPath;
+ },
+ logsPath(state) {
+ return state[this.namespace].logsPath;
+ },
+ timeRange(state) {
+ return state[this.namespace].timeRange;
+ },
+ metricsSavedToDb(state, getters) {
+ return getters[`${this.namespace}/metricsSavedToDb`];
+ },
+ }),
+ title() {
+ return this.graphData?.title || '';
+ },
+ graphDataHasResult() {
+ return this.graphData?.metrics?.[0]?.result?.length > 0;
+ },
+ graphDataIsLoading() {
+ const metrics = this.graphData?.metrics || [];
+ return metrics.some(({ loading }) => loading);
+ },
+ logsPathWithTimeRange() {
+ const timeRange = this.zoomedTimeRange || this.timeRange;
+
+ if (this.logsPath && this.logsPath !== invalidUrl && timeRange) {
+ return timeRangeToUrl(timeRange, this.logsPath);
+ }
+ return null;
+ },
+ csvText() {
+ const chartData = this.graphData?.metrics[0].result[0].values || [];
+ const yLabel = this.graphData.y_label;
+ const header = `timestamp,${yLabel}\r\n`; // eslint-disable-line @gitlab/require-i18n-strings
+ return chartData.reduce((csv, data) => {
+ const row = data.join(',');
+ return `${csv}${row}\r\n`;
+ }, header);
+ },
+ downloadCsv() {
+ const data = new Blob([this.csvText], { type: 'text/plain' });
+ return window.URL.createObjectURL(data);
+ },
+
+ /**
+ * A chart is "basic" if it doesn't support
+ * the same features as the TimeSeries based components
+ * such as "annotations".
+ *
+ * @returns Vue Component wrapping a basic visualization
+ */
+ basicChartComponent() {
+ if (this.isPanelType(panelTypes.SINGLE_STAT)) {
+ return MonitorSingleStatChart;
+ }
+ if (this.isPanelType(panelTypes.HEATMAP)) {
+ return MonitorHeatmapChart;
+ }
+ if (this.isPanelType(panelTypes.BAR)) {
+ return MonitorBarChart;
+ }
+ if (this.isPanelType(panelTypes.COLUMN)) {
+ return MonitorColumnChart;
+ }
+ if (this.isPanelType(panelTypes.STACKED_COLUMN)) {
+ return MonitorStackedColumnChart;
+ }
+ if (this.isPanelType(panelTypes.ANOMALY_CHART)) {
+ return MonitorAnomalyChart;
+ }
+ return null;
+ },
+
+ /**
+ * In monitoring, Time Series charts typically support
+ * a larger feature set like "annotations", "deployment
+ * data", alert "thresholds" and "datazoom".
+ *
+ * This is intentional as Time Series are more frequently
+ * used.
+ *
+ * @returns Vue Component wrapping a time series visualization,
+ * Area Charts are rendered by default.
+ */
+ timeSeriesChartComponent() {
+ if (this.isPanelType(panelTypes.ANOMALY_CHART)) {
+ return MonitorAnomalyChart;
+ }
+ return MonitorTimeSeriesChart;
+ },
+ isContextualMenuShown() {
+ return Boolean(this.graphDataHasResult && !this.basicChartComponent);
+ },
+ editCustomMetricLink() {
+ if (this.graphData.metrics.length > 1) {
+ return this.settingsPath;
+ }
+ return this.graphData?.metrics[0].edit_path;
+ },
+ editCustomMetricLinkText() {
+ return n__('Metrics|Edit metric', 'Metrics|Edit metrics', this.graphData.metrics.length);
+ },
+ hasMetricsInDb() {
+ const { metrics = [] } = this.graphData;
+ return metrics.some(({ metricId }) => this.metricsSavedToDb.includes(metricId));
+ },
+ alertWidgetAvailable() {
+ return (
+ this.prometheusAlertsAvailable &&
+ this.alertsEndpoint &&
+ this.graphData &&
+ this.hasMetricsInDb
+ );
+ },
+ },
+ mounted() {
+ this.refreshTitleTooltip();
+ },
+ methods: {
+ getGraphAlerts(queries) {
+ if (!this.allAlerts) return {};
+ const metricIdsForChart = queries.map(q => q.metricId);
+ return pickBy(this.allAlerts, alert => metricIdsForChart.includes(alert.metricId));
+ },
+ getGraphAlertValues(queries) {
+ return Object.values(this.getGraphAlerts(queries));
+ },
+ isPanelType(type) {
+ return this.graphData?.type === type;
+ },
+ showToast() {
+ this.$toast.show(__('Link copied'));
+ },
+ refreshTitleTooltip() {
+ const { graphTitle } = this.$refs;
+ this.showTitleTooltip =
+ Boolean(graphTitle) && graphTitle.scrollWidth > graphTitle.offsetWidth;
+ },
+
+ downloadCSVOptions,
+ generateLinkToChartOptions,
+
+ onResize() {
+ this.refreshTitleTooltip();
+ },
+ onDatazoom({ start, end }) {
+ this.zoomedTimeRange = { start, end };
+ this.$emit(events.timeRangeZoom, { start, end });
+ },
+ onExpand() {
+ this.$emit(events.expand);
+ },
+ setAlerts(alertPath, alertAttributes) {
+ if (alertAttributes) {
+ this.$set(this.allAlerts, alertPath, alertAttributes);
+ } else {
+ this.$delete(this.allAlerts, alertPath);
+ }
+ },
+ },
+ panelTypes,
+};
+</script>
+<template>
+ <div v-gl-resize-observer="onResize" class="prometheus-graph">
+ <div class="d-flex align-items-center mr-3">
+ <slot name="topLeft"></slot>
+ <h5
+ ref="graphTitle"
+ class="prometheus-graph-title gl-font-lg font-weight-bold text-truncate append-right-8"
+ >
+ {{ title }}
+ </h5>
+ <gl-tooltip :target="() => $refs.graphTitle" :disabled="!showTitleTooltip">
+ {{ title }}
+ </gl-tooltip>
+ <alert-widget
+ v-if="isContextualMenuShown && alertWidgetAvailable"
+ class="mx-1"
+ :modal-id="`alert-modal-${graphData.id}`"
+ :alerts-endpoint="alertsEndpoint"
+ :relevant-queries="graphData.metrics"
+ :alerts-to-manage="getGraphAlerts(graphData.metrics)"
+ @setAlerts="setAlerts"
+ />
+ <div class="flex-grow-1"></div>
+ <div v-if="graphDataIsLoading" class="mx-1 mt-1">
+ <gl-loading-icon />
+ </div>
+ <div
+ v-if="isContextualMenuShown"
+ ref="contextualMenu"
+ data-qa-selector="prometheus_graph_widgets"
+ >
+ <div class="d-flex align-items-center">
+ <gl-dropdown
+ v-gl-tooltip
+ toggle-class="btn btn-transparent border-0"
+ data-qa-selector="prometheus_widgets_dropdown"
+ right
+ no-caret
+ :title="__('More actions')"
+ >
+ <template slot="button-content">
+ <gl-icon name="ellipsis_v" class="text-secondary" />
+ </template>
+ <gl-dropdown-item
+ v-if="expandBtnAvailable"
+ ref="expandBtn"
+ :href="clipboardText"
+ @click.prevent="onExpand"
+ >
+ {{ s__('Metrics|Expand panel') }}
+ </gl-dropdown-item>
+ <gl-dropdown-item
+ v-if="editCustomMetricLink"
+ ref="editMetricLink"
+ :href="editCustomMetricLink"
+ >
+ {{ editCustomMetricLinkText }}
+ </gl-dropdown-item>
+ <gl-dropdown-item
+ v-if="logsPathWithTimeRange"
+ ref="viewLogsLink"
+ :href="logsPathWithTimeRange"
+ >
+ {{ s__('Metrics|View logs') }}
+ </gl-dropdown-item>
+
+ <gl-dropdown-item
+ v-if="csvText"
+ ref="downloadCsvLink"
+ v-track-event="downloadCSVOptions(title)"
+ :href="downloadCsv"
+ download="chart_metrics.csv"
+ >
+ {{ __('Download CSV') }}
+ </gl-dropdown-item>
+ <gl-dropdown-item
+ v-if="clipboardText"
+ ref="copyChartLink"
+ v-track-event="generateLinkToChartOptions(clipboardText)"
+ :data-clipboard-text="clipboardText"
+ data-qa-selector="generate_chart_link_menu_item"
+ @click="showToast(clipboardText)"
+ >
+ {{ __('Copy link to chart') }}
+ </gl-dropdown-item>
+ <gl-dropdown-item
+ v-if="alertWidgetAvailable"
+ v-gl-modal="`alert-modal-${graphData.id}`"
+ data-qa-selector="alert_widget_menu_item"
+ >
+ {{ __('Alerts') }}
+ </gl-dropdown-item>
+ </gl-dropdown>
+ </div>
+ </div>
+ </div>
+
+ <monitor-empty-chart v-if="!graphDataHasResult" />
+ <component
+ :is="basicChartComponent"
+ v-else-if="basicChartComponent"
+ :graph-data="graphData"
+ v-bind="$attrs"
+ v-on="$listeners"
+ />
+ <component
+ :is="timeSeriesChartComponent"
+ v-else
+ ref="timeSeriesChart"
+ :graph-data="graphData"
+ :deployment-data="deploymentData"
+ :annotations="annotations"
+ :project-path="projectPath"
+ :thresholds="getGraphAlertValues(graphData.metrics)"
+ :group-id="groupId"
+ v-bind="$attrs"
+ v-on="$listeners"
+ @datazoom="onDatazoom"
+ />
+ </div>
+</template>
diff --git a/app/assets/javascripts/monitoring/components/dashboards_dropdown.vue b/app/assets/javascripts/monitoring/components/dashboards_dropdown.vue
index 8f3e0a6ec75..8b86890715f 100644
--- a/app/assets/javascripts/monitoring/components/dashboards_dropdown.vue
+++ b/app/assets/javascripts/monitoring/components/dashboards_dropdown.vue
@@ -1,7 +1,8 @@
<script>
-import { mapState, mapActions } from 'vuex';
+import { mapState, mapActions, mapGetters } from 'vuex';
import {
GlAlert,
+ GlIcon,
GlDropdown,
GlDropdownItem,
GlDropdownHeader,
@@ -21,6 +22,7 @@ const events = {
export default {
components: {
GlAlert,
+ GlIcon,
GlDropdown,
GlDropdownItem,
GlDropdownHeader,
@@ -34,11 +36,6 @@ export default {
GlModal: GlModalDirective,
},
props: {
- selectedDashboard: {
- type: Object,
- required: false,
- default: () => ({}),
- },
defaultBranch: {
type: String,
required: true,
@@ -54,26 +51,41 @@ export default {
},
computed: {
...mapState('monitoringDashboard', ['allDashboards']),
+ ...mapGetters('monitoringDashboard', ['selectedDashboard']),
isSystemDashboard() {
- return this.selectedDashboard.system_dashboard;
+ return this.selectedDashboard?.system_dashboard;
},
selectedDashboardText() {
- return this.selectedDashboard.display_name;
+ return this.selectedDashboard?.display_name;
+ },
+ selectedDashboardPath() {
+ return this.selectedDashboard?.path;
},
+
filteredDashboards() {
- return this.allDashboards.filter(({ display_name }) =>
+ return this.allDashboards.filter(({ display_name = '' }) =>
display_name.toLowerCase().includes(this.searchTerm.toLowerCase()),
);
},
shouldShowNoMsgContainer() {
return this.filteredDashboards.length === 0;
},
+ starredDashboards() {
+ return this.filteredDashboards.filter(({ starred }) => starred);
+ },
+ nonStarredDashboards() {
+ return this.filteredDashboards.filter(({ starred }) => !starred);
+ },
+
okButtonText() {
return this.loading ? s__('Metrics|Duplicating...') : s__('Metrics|Duplicate');
},
},
methods: {
...mapActions('monitoringDashboard', ['duplicateSystemDashboard']),
+ dashboardDisplayName(dashboard) {
+ return dashboard.display_name || dashboard.path || '';
+ },
selectDashboard(dashboard) {
this.$emit(events.selectDashboard, dashboard);
},
@@ -127,15 +139,34 @@ export default {
v-model="searchTerm"
class="m-2"
/>
+
<div class="flex-fill overflow-auto">
<gl-dropdown-item
- v-for="dashboard in filteredDashboards"
+ v-for="dashboard in starredDashboards"
+ :key="dashboard.path"
+ :active="dashboard.path === selectedDashboardPath"
+ active-class="is-active"
+ @click="selectDashboard(dashboard)"
+ >
+ <div class="d-flex">
+ {{ dashboardDisplayName(dashboard) }}
+ <gl-icon class="text-muted ml-auto" name="star" />
+ </div>
+ </gl-dropdown-item>
+
+ <gl-dropdown-divider
+ v-if="starredDashboards.length && nonStarredDashboards.length"
+ ref="starredListDivider"
+ />
+
+ <gl-dropdown-item
+ v-for="dashboard in nonStarredDashboards"
:key="dashboard.path"
- :active="dashboard.path === selectedDashboard.path"
+ :active="dashboard.path === selectedDashboardPath"
active-class="is-active"
@click="selectDashboard(dashboard)"
>
- {{ dashboard.display_name || dashboard.path }}
+ {{ dashboardDisplayName(dashboard) }}
</gl-dropdown-item>
</div>
diff --git a/app/assets/javascripts/monitoring/components/embeds/metric_embed.vue b/app/assets/javascripts/monitoring/components/embeds/metric_embed.vue
index 3f8b0f76997..1557a49137e 100644
--- a/app/assets/javascripts/monitoring/components/embeds/metric_embed.vue
+++ b/app/assets/javascripts/monitoring/components/embeds/metric_embed.vue
@@ -1,6 +1,6 @@
<script>
import { mapState, mapActions } from 'vuex';
-import PanelType from 'ee_else_ce/monitoring/components/panel_type.vue';
+import DashboardPanel from '~/monitoring/components/dashboard_panel.vue';
import { convertToFixedRange } from '~/lib/utils/datetime_range';
import { defaultTimeRange } from '~/vue_shared/constants';
import { timeRangeFromUrl, removeTimeRangeParams } from '../../utils';
@@ -10,7 +10,7 @@ let sidebarMutationObserver;
export default {
components: {
- PanelType,
+ DashboardPanel,
},
props: {
containerClass: {
@@ -113,9 +113,9 @@ export default {
</script>
<template>
<div class="metrics-embed p-0 d-flex flex-wrap" :class="embedClass">
- <panel-type
+ <dashboard-panel
v-for="(graphData, graphIndex) in charts"
- :key="`panel-type-${graphIndex}`"
+ :key="`dashboard-panel-${graphIndex}`"
:class="panelClass"
:graph-data="graphData"
:group-id="dashboardUrl"
diff --git a/app/assets/javascripts/monitoring/components/panel_type.vue b/app/assets/javascripts/monitoring/components/panel_type.vue
deleted file mode 100644
index 2beae0d9540..00000000000
--- a/app/assets/javascripts/monitoring/components/panel_type.vue
+++ /dev/null
@@ -1,324 +0,0 @@
-<script>
-import { mapState } from 'vuex';
-import { pickBy } from 'lodash';
-import invalidUrl from '~/lib/utils/invalid_url';
-import {
- GlResizeObserverDirective,
- GlIcon,
- GlLoadingIcon,
- GlDropdown,
- GlDropdownItem,
- GlModal,
- GlModalDirective,
- GlTooltip,
- GlTooltipDirective,
-} from '@gitlab/ui';
-import { __, n__ } from '~/locale';
-import MonitorTimeSeriesChart from './charts/time_series.vue';
-import MonitorAnomalyChart from './charts/anomaly.vue';
-import MonitorSingleStatChart from './charts/single_stat.vue';
-import MonitorHeatmapChart from './charts/heatmap.vue';
-import MonitorColumnChart from './charts/column.vue';
-import MonitorBarChart from './charts/bar.vue';
-import MonitorStackedColumnChart from './charts/stacked_column.vue';
-import MonitorEmptyChart from './charts/empty_chart.vue';
-import TrackEventDirective from '~/vue_shared/directives/track_event';
-import { timeRangeToUrl, downloadCSVOptions, generateLinkToChartOptions } from '../utils';
-
-const events = {
- timeRangeZoom: 'timerangezoom',
-};
-
-export default {
- components: {
- MonitorSingleStatChart,
- MonitorColumnChart,
- MonitorBarChart,
- MonitorHeatmapChart,
- MonitorStackedColumnChart,
- MonitorEmptyChart,
- GlIcon,
- GlLoadingIcon,
- GlTooltip,
- GlDropdown,
- GlDropdownItem,
- GlModal,
- },
- directives: {
- GlResizeObserver: GlResizeObserverDirective,
- GlModal: GlModalDirective,
- GlTooltip: GlTooltipDirective,
- TrackEvent: TrackEventDirective,
- },
- props: {
- clipboardText: {
- type: String,
- required: false,
- default: '',
- },
- graphData: {
- type: Object,
- required: true,
- },
- index: {
- type: String,
- required: false,
- default: '',
- },
- groupId: {
- type: String,
- required: false,
- default: 'panel-type-chart',
- },
- namespace: {
- type: String,
- required: false,
- default: 'monitoringDashboard',
- },
- },
- data() {
- return {
- showTitleTooltip: false,
- zoomedTimeRange: null,
- };
- },
- computed: {
- // Use functions to support dynamic namespaces in mapXXX helpers. Pattern described
- // in https://github.com/vuejs/vuex/issues/863#issuecomment-329510765
- ...mapState({
- deploymentData(state) {
- return state[this.namespace].deploymentData;
- },
- annotations(state) {
- return state[this.namespace].annotations;
- },
- projectPath(state) {
- return state[this.namespace].projectPath;
- },
- logsPath(state) {
- return state[this.namespace].logsPath;
- },
- timeRange(state) {
- return state[this.namespace].timeRange;
- },
- }),
- title() {
- return this.graphData.title || '';
- },
- alertWidgetAvailable() {
- // This method is extended by ee functionality
- return false;
- },
- graphDataHasResult() {
- return (
- this.graphData.metrics &&
- this.graphData.metrics[0].result &&
- this.graphData.metrics[0].result.length > 0
- );
- },
- graphDataIsLoading() {
- const { metrics = [] } = this.graphData;
- return metrics.some(({ loading }) => loading);
- },
- logsPathWithTimeRange() {
- const timeRange = this.zoomedTimeRange || this.timeRange;
-
- if (this.logsPath && this.logsPath !== invalidUrl && timeRange) {
- return timeRangeToUrl(timeRange, this.logsPath);
- }
- return null;
- },
- csvText() {
- const chartData = this.graphData.metrics[0].result[0].values;
- const yLabel = this.graphData.y_label;
- const header = `timestamp,${yLabel}\r\n`; // eslint-disable-line @gitlab/require-i18n-strings
- return chartData.reduce((csv, data) => {
- const row = data.join(',');
- return `${csv}${row}\r\n`;
- }, header);
- },
- downloadCsv() {
- const data = new Blob([this.csvText], { type: 'text/plain' });
- return window.URL.createObjectURL(data);
- },
- timeChartComponent() {
- if (this.isPanelType('anomaly-chart')) {
- return MonitorAnomalyChart;
- }
- return MonitorTimeSeriesChart;
- },
- isContextualMenuShown() {
- return (
- this.graphDataHasResult &&
- !this.isPanelType('single-stat') &&
- !this.isPanelType('heatmap') &&
- !this.isPanelType('column') &&
- !this.isPanelType('stacked-column')
- );
- },
- editCustomMetricLink() {
- return this.graphData?.metrics[0].edit_path;
- },
- editCustomMetricLinkText() {
- return n__('Metrics|Edit metric', 'Metrics|Edit metrics', this.graphData.metrics.length);
- },
- },
- mounted() {
- this.refreshTitleTooltip();
- },
- methods: {
- getGraphAlerts(queries) {
- if (!this.allAlerts) return {};
- const metricIdsForChart = queries.map(q => q.metricId);
- return pickBy(this.allAlerts, alert => metricIdsForChart.includes(alert.metricId));
- },
- getGraphAlertValues(queries) {
- return Object.values(this.getGraphAlerts(queries));
- },
- isPanelType(type) {
- return this.graphData.type && this.graphData.type === type;
- },
- showToast() {
- this.$toast.show(__('Link copied'));
- },
- refreshTitleTooltip() {
- const { graphTitle } = this.$refs;
- this.showTitleTooltip =
- Boolean(graphTitle) && graphTitle.scrollWidth > graphTitle.offsetWidth;
- },
-
- downloadCSVOptions,
- generateLinkToChartOptions,
-
- onResize() {
- this.refreshTitleTooltip();
- },
- onDatazoom({ start, end }) {
- this.zoomedTimeRange = { start, end };
- this.$emit(events.timeRangeZoom, { start, end });
- },
- },
-};
-</script>
-<template>
- <div v-gl-resize-observer="onResize" class="prometheus-graph">
- <div class="d-flex align-items-center mr-3">
- <h5
- ref="graphTitle"
- class="prometheus-graph-title gl-font-size-large font-weight-bold text-truncate append-right-8"
- >
- {{ title }}
- </h5>
- <gl-tooltip :target="() => $refs.graphTitle" :disabled="!showTitleTooltip">
- {{ title }}
- </gl-tooltip>
- <alert-widget
- v-if="isContextualMenuShown && alertWidgetAvailable"
- class="mx-1"
- :modal-id="`alert-modal-${index}`"
- :alerts-endpoint="alertsEndpoint"
- :relevant-queries="graphData.metrics"
- :alerts-to-manage="getGraphAlerts(graphData.metrics)"
- @setAlerts="setAlerts"
- />
- <div class="flex-grow-1"></div>
- <div v-if="graphDataIsLoading" class="mx-1 mt-1">
- <gl-loading-icon />
- </div>
- <div
- v-if="isContextualMenuShown"
- ref="contextualMenu"
- data-qa-selector="prometheus_graph_widgets"
- >
- <div class="d-flex align-items-center">
- <gl-dropdown
- v-gl-tooltip
- toggle-class="btn btn-transparent border-0"
- data-qa-selector="prometheus_widgets_dropdown"
- right
- no-caret
- :title="__('More actions')"
- >
- <template slot="button-content">
- <gl-icon name="ellipsis_v" class="text-secondary" />
- </template>
- <gl-dropdown-item
- v-if="editCustomMetricLink"
- ref="editMetricLink"
- :href="editCustomMetricLink"
- >
- {{ editCustomMetricLinkText }}
- </gl-dropdown-item>
- <gl-dropdown-item
- v-if="logsPathWithTimeRange"
- ref="viewLogsLink"
- :href="logsPathWithTimeRange"
- >
- {{ s__('Metrics|View logs') }}
- </gl-dropdown-item>
-
- <gl-dropdown-item
- v-if="csvText"
- ref="downloadCsvLink"
- v-track-event="downloadCSVOptions(title)"
- :href="downloadCsv"
- download="chart_metrics.csv"
- >
- {{ __('Download CSV') }}
- </gl-dropdown-item>
- <gl-dropdown-item
- v-if="clipboardText"
- ref="copyChartLink"
- v-track-event="generateLinkToChartOptions(clipboardText)"
- :data-clipboard-text="clipboardText"
- @click="showToast(clipboardText)"
- >
- {{ __('Copy link to chart') }}
- </gl-dropdown-item>
- <gl-dropdown-item
- v-if="alertWidgetAvailable"
- v-gl-modal="`alert-modal-${index}`"
- data-qa-selector="alert_widget_menu_item"
- >
- {{ __('Alerts') }}
- </gl-dropdown-item>
- </gl-dropdown>
- </div>
- </div>
- </div>
-
- <monitor-single-stat-chart
- v-if="isPanelType('single-stat') && graphDataHasResult"
- :graph-data="graphData"
- />
- <monitor-heatmap-chart
- v-else-if="isPanelType('heatmap') && graphDataHasResult"
- :graph-data="graphData"
- />
- <monitor-bar-chart
- v-else-if="isPanelType('bar') && graphDataHasResult"
- :graph-data="graphData"
- />
- <monitor-column-chart
- v-else-if="isPanelType('column') && graphDataHasResult"
- :graph-data="graphData"
- />
- <monitor-stacked-column-chart
- v-else-if="isPanelType('stacked-column') && graphDataHasResult"
- :graph-data="graphData"
- />
- <component
- :is="timeChartComponent"
- v-else-if="graphDataHasResult"
- ref="timeChart"
- :graph-data="graphData"
- :deployment-data="deploymentData"
- :annotations="annotations"
- :project-path="projectPath"
- :thresholds="getGraphAlertValues(graphData.metrics)"
- :group-id="groupId"
- @datazoom="onDatazoom"
- />
- <monitor-empty-chart v-else v-bind="$attrs" v-on="$listeners" />
- </div>
-</template>
diff --git a/app/assets/javascripts/monitoring/components/variables/custom_variable.vue b/app/assets/javascripts/monitoring/components/variables/custom_variable.vue
new file mode 100644
index 00000000000..0ac7c0b80df
--- /dev/null
+++ b/app/assets/javascripts/monitoring/components/variables/custom_variable.vue
@@ -0,0 +1,50 @@
+<script>
+import { GlFormGroup, GlDropdown, GlDropdownItem } from '@gitlab/ui';
+
+export default {
+ components: {
+ GlFormGroup,
+ GlDropdown,
+ GlDropdownItem,
+ },
+ props: {
+ name: {
+ type: String,
+ required: true,
+ },
+ label: {
+ type: String,
+ required: true,
+ },
+ value: {
+ type: String,
+ required: false,
+ default: '',
+ },
+ options: {
+ type: Array,
+ required: true,
+ },
+ },
+ computed: {
+ defaultText() {
+ const selectedOpt = this.options.find(opt => opt.value === this.value);
+ return selectedOpt?.text || this.value;
+ },
+ },
+ methods: {
+ onUpdate(value) {
+ this.$emit('onUpdate', this.name, value);
+ },
+ },
+};
+</script>
+<template>
+ <gl-form-group :label="label">
+ <gl-dropdown toggle-class="dropdown-menu-toggle" :text="defaultText">
+ <gl-dropdown-item v-for="(opt, key) in options" :key="key" @click="onUpdate(opt.value)">{{
+ opt.text
+ }}</gl-dropdown-item>
+ </gl-dropdown>
+ </gl-form-group>
+</template>
diff --git a/app/assets/javascripts/monitoring/components/variables/text_variable.vue b/app/assets/javascripts/monitoring/components/variables/text_variable.vue
new file mode 100644
index 00000000000..ce0d19760e2
--- /dev/null
+++ b/app/assets/javascripts/monitoring/components/variables/text_variable.vue
@@ -0,0 +1,39 @@
+<script>
+import { GlFormGroup, GlFormInput } from '@gitlab/ui';
+
+export default {
+ components: {
+ GlFormGroup,
+ GlFormInput,
+ },
+ props: {
+ name: {
+ type: String,
+ required: true,
+ },
+ label: {
+ type: String,
+ required: true,
+ },
+ value: {
+ type: String,
+ required: true,
+ },
+ },
+ methods: {
+ onUpdate(event) {
+ this.$emit('onUpdate', this.name, event.target.value);
+ },
+ },
+};
+</script>
+<template>
+ <gl-form-group :label="label">
+ <gl-form-input
+ :value="value"
+ :name="name"
+ @keyup.native.enter="onUpdate"
+ @blur.native="onUpdate"
+ />
+ </gl-form-group>
+</template>
diff --git a/app/assets/javascripts/monitoring/components/variables_section.vue b/app/assets/javascripts/monitoring/components/variables_section.vue
new file mode 100644
index 00000000000..e054c9d8e26
--- /dev/null
+++ b/app/assets/javascripts/monitoring/components/variables_section.vue
@@ -0,0 +1,56 @@
+<script>
+import { mapState, mapActions } from 'vuex';
+import CustomVariable from './variables/custom_variable.vue';
+import TextVariable from './variables/text_variable.vue';
+import { setPromCustomVariablesFromUrl } from '../utils';
+
+export default {
+ components: {
+ CustomVariable,
+ TextVariable,
+ },
+ computed: {
+ ...mapState('monitoringDashboard', ['promVariables']),
+ },
+ methods: {
+ ...mapActions('monitoringDashboard', ['fetchDashboardData', 'updateVariableValues']),
+ refreshDashboard(variable, value) {
+ if (this.promVariables[variable].value !== value) {
+ const changedVariable = { key: variable, value };
+ // update the Vuex store
+ this.updateVariableValues(changedVariable);
+ // the below calls can ideally be moved out of the
+ // component and into the actions and let the
+ // mutation respond directly.
+ // This can be further investigate in
+ // https://gitlab.com/gitlab-org/gitlab/-/issues/217713
+ setPromCustomVariablesFromUrl(this.promVariables);
+ // fetch data
+ this.fetchDashboardData();
+ }
+ },
+ variableComponent(type) {
+ const types = {
+ text: TextVariable,
+ custom: CustomVariable,
+ };
+ return types[type] || TextVariable;
+ },
+ },
+};
+</script>
+<template>
+ <div ref="variablesSection" class="d-sm-flex flex-sm-wrap pt-2 pr-1 pb-0 pl-2 variables-section">
+ <div v-for="(variable, key) in promVariables" :key="key" class="mb-1 pr-2 d-flex d-sm-block">
+ <component
+ :is="variableComponent(variable.type)"
+ class="mb-0 flex-grow-1"
+ :label="variable.label"
+ :value="variable.value"
+ :name="key"
+ :options="variable.options"
+ @onUpdate="refreshDashboard"
+ />
+ </div>
+ </div>
+</template>
diff --git a/app/assets/javascripts/monitoring/constants.js b/app/assets/javascripts/monitoring/constants.js
index 0b393f19789..0c2eafeed54 100644
--- a/app/assets/javascripts/monitoring/constants.js
+++ b/app/assets/javascripts/monitoring/constants.js
@@ -48,6 +48,55 @@ export const metricStates = {
UNKNOWN_ERROR: 'UNKNOWN_ERROR',
};
+/**
+ * Supported panel types in dashboards, values of `panel.type`.
+ *
+ * Values should not be changed as they correspond to
+ * values in users the `.yml` dashboard definition.
+ */
+export const panelTypes = {
+ /**
+ * Area Chart
+ *
+ * Time Series chart with an area
+ */
+ AREA_CHART: 'area-chart',
+ /**
+ * Line Chart
+ *
+ * Time Series chart with a line
+ */
+ LINE_CHART: 'line-chart',
+ /**
+ * Anomaly Chart
+ *
+ * Time Series chart with 3 metrics
+ */
+ ANOMALY_CHART: 'anomaly-chart',
+ /**
+ * Single Stat
+ *
+ * Single data point visualization
+ */
+ SINGLE_STAT: 'single-stat',
+ /**
+ * Heatmap
+ */
+ HEATMAP: 'heatmap',
+ /**
+ * Bar chart
+ */
+ BAR: 'bar',
+ /**
+ * Column chart
+ */
+ COLUMN: 'column',
+ /**
+ * Stacked column chart
+ */
+ STACKED_COLUMN: 'stacked-column',
+};
+
export const sidebarAnimationDuration = 300; // milliseconds.
export const chartHeight = 300;
@@ -143,3 +192,38 @@ export const annotationsSymbolIcon = 'path://m5 229 5 8h-10z';
* https://gitlab.com/gitlab-org/gitlab/-/issues/214671
*/
export const DEFAULT_DASHBOARD_PATH = 'config/prometheus/common_metrics.yml';
+
+export const OPERATORS = {
+ greaterThan: '>',
+ equalTo: '==',
+ lessThan: '<',
+};
+
+/**
+ * Dashboard yml files support custom user-defined variables that
+ * are rendered as input elements in the monitoring dashboard.
+ * These values can be edited by the user and are passed on to the
+ * the backend and eventually to Prometheus API proxy.
+ *
+ * As of 13.0, the supported types are:
+ * simple custom -> dropdown elements
+ * advanced custom -> dropdown elements
+ * text -> text input elements
+ *
+ * Custom variables have a simple and a advanced variant.
+ */
+export const VARIABLE_TYPES = {
+ custom: 'custom',
+ text: 'text',
+};
+
+/**
+ * The names of templating variables defined in the dashboard yml
+ * file are prefixed with a constant so that it doesn't collide with
+ * other URL params that the monitoring dashboard relies on for
+ * features like panel fullscreen etc.
+ *
+ * The prefix is added before it is appended to the URL and removed
+ * before passing the data to the backend.
+ */
+export const VARIABLE_PREFIX = 'var-';
diff --git a/app/assets/javascripts/monitoring/monitoring_bundle.js b/app/assets/javascripts/monitoring/monitoring_bundle.js
index d296f5b7a66..2bbf9ef9d78 100644
--- a/app/assets/javascripts/monitoring/monitoring_bundle.js
+++ b/app/assets/javascripts/monitoring/monitoring_bundle.js
@@ -1,6 +1,6 @@
import Vue from 'vue';
import { GlToast } from '@gitlab/ui';
-import Dashboard from 'ee_else_ce/monitoring/components/dashboard.vue';
+import Dashboard from '~/monitoring/components/dashboard.vue';
import { parseBoolean } from '~/lib/utils/common_utils';
import { getParameterValues } from '~/lib/utils/url_utility';
import store from './stores';
diff --git a/app/assets/javascripts/monitoring/monitoring_bundle_with_alerts.js b/app/assets/javascripts/monitoring/monitoring_bundle_with_alerts.js
new file mode 100644
index 00000000000..afe5ee0938d
--- /dev/null
+++ b/app/assets/javascripts/monitoring/monitoring_bundle_with_alerts.js
@@ -0,0 +1,13 @@
+import { parseBoolean } from '~/lib/utils/common_utils';
+import initCeBundle from '~/monitoring/monitoring_bundle';
+
+export default () => {
+ const el = document.getElementById('prometheus-graphs');
+
+ if (el && el.dataset) {
+ initCeBundle({
+ customMetricsAvailable: parseBoolean(el.dataset.customMetricsAvailable),
+ prometheusAlertsAvailable: parseBoolean(el.dataset.prometheusAlertsAvailable),
+ });
+ }
+};
diff --git a/app/assets/javascripts/monitoring/services/alerts_service.js b/app/assets/javascripts/monitoring/services/alerts_service.js
new file mode 100644
index 00000000000..4b7337972fe
--- /dev/null
+++ b/app/assets/javascripts/monitoring/services/alerts_service.js
@@ -0,0 +1,32 @@
+import axios from '~/lib/utils/axios_utils';
+
+export default class AlertsService {
+ constructor({ alertsEndpoint }) {
+ this.alertsEndpoint = alertsEndpoint;
+ }
+
+ getAlerts() {
+ return axios.get(this.alertsEndpoint).then(resp => resp.data);
+ }
+
+ createAlert({ prometheus_metric_id, operator, threshold }) {
+ return axios
+ .post(this.alertsEndpoint, { prometheus_metric_id, operator, threshold })
+ .then(resp => resp.data);
+ }
+
+ // eslint-disable-next-line class-methods-use-this
+ readAlert(alertPath) {
+ return axios.get(alertPath).then(resp => resp.data);
+ }
+
+ // eslint-disable-next-line class-methods-use-this
+ updateAlert(alertPath, { operator, threshold }) {
+ return axios.put(alertPath, { operator, threshold }).then(resp => resp.data);
+ }
+
+ // eslint-disable-next-line class-methods-use-this
+ deleteAlert(alertPath) {
+ return axios.delete(alertPath).then(resp => resp.data);
+ }
+}
diff --git a/app/assets/javascripts/monitoring/stores/actions.js b/app/assets/javascripts/monitoring/stores/actions.js
index f04f775761c..b057afa2264 100644
--- a/app/assets/javascripts/monitoring/stores/actions.js
+++ b/app/assets/javascripts/monitoring/stores/actions.js
@@ -3,6 +3,8 @@ import * as types from './mutation_types';
import axios from '~/lib/utils/axios_utils';
import createFlash from '~/flash';
import { convertToFixedRange } from '~/lib/utils/datetime_range';
+import { parseTemplatingVariables } from './variable_mapping';
+import { mergeURLVariables } from '../utils';
import {
gqClient,
parseEnvironmentsResponse,
@@ -13,11 +15,7 @@ import trackDashboardLoad from '../monitoring_tracking_helper';
import getEnvironments from '../queries/getEnvironments.query.graphql';
import getAnnotations from '../queries/getAnnotations.query.graphql';
import statusCodes from '../../lib/utils/http_status';
-import {
- backOff,
- convertObjectPropsToCamelCase,
- isFeatureFlagEnabled,
-} from '../../lib/utils/common_utils';
+import { backOff, convertObjectPropsToCamelCase } from '../../lib/utils/common_utils';
import { s__, sprintf } from '../../locale';
import {
@@ -80,6 +78,10 @@ export const setTimeRange = ({ commit }, timeRange) => {
commit(types.SET_TIME_RANGE, timeRange);
};
+export const setVariables = ({ commit }, variables) => {
+ commit(types.SET_VARIABLES, variables);
+};
+
export const filterEnvironments = ({ commit, dispatch }, searchTerm) => {
commit(types.SET_ENVIRONMENTS_FILTER, searchTerm);
dispatch('fetchEnvironmentsData');
@@ -89,19 +91,30 @@ export const setShowErrorBanner = ({ commit }, enabled) => {
commit(types.SET_SHOW_ERROR_BANNER, enabled);
};
+export const setExpandedPanel = ({ commit }, { group, panel }) => {
+ commit(types.SET_EXPANDED_PANEL, { group, panel });
+};
+
+export const clearExpandedPanel = ({ commit }) => {
+ commit(types.SET_EXPANDED_PANEL, {
+ group: null,
+ panel: null,
+ });
+};
+
// All Data
+/**
+ * Fetch all dashboard data.
+ *
+ * @param {Object} store
+ * @returns A promise that resolves when the dashboard
+ * skeleton has been loaded.
+ */
export const fetchData = ({ dispatch }) => {
dispatch('fetchEnvironmentsData');
dispatch('fetchDashboard');
- /**
- * Annotations data is not yet fetched. This will be
- * ready after the BE piece is implemented.
- * https://gitlab.com/gitlab-org/gitlab/-/issues/211330
- */
- if (isFeatureFlagEnabled('metricsDashboardAnnotations')) {
- dispatch('fetchAnnotations');
- }
+ dispatch('fetchAnnotations');
};
// Metrics dashboard
@@ -148,6 +161,7 @@ export const receiveMetricsDashboardSuccess = ({ commit, dispatch }, { response
commit(types.SET_ALL_DASHBOARDS, all_dashboards);
commit(types.RECEIVE_METRICS_DASHBOARD_SUCCESS, dashboard);
+ commit(types.SET_VARIABLES, mergeURLVariables(parseTemplatingVariables(dashboard.templating)));
commit(types.SET_ENDPOINTS, convertObjectPropsToCamelCase(metrics_data));
return dispatch('fetchDashboardData');
@@ -200,12 +214,19 @@ export const fetchDashboardData = ({ state, dispatch, getters }) => {
*
* @param {metric} metric
*/
-export const fetchPrometheusMetric = ({ commit }, { metric, defaultQueryParams }) => {
+export const fetchPrometheusMetric = (
+ { commit, state, getters },
+ { metric, defaultQueryParams },
+) => {
const queryParams = { ...defaultQueryParams };
if (metric.step) {
queryParams.step = metric.step;
}
+ if (Object.keys(state.promVariables).length > 0) {
+ queryParams.variables = getters.getCustomVariablesArray;
+ }
+
commit(types.REQUEST_METRIC_RESULT, { metricId: metric.metricId });
return getPrometheusMetricResult(metric.prometheusEndpointPath, queryParams)
@@ -327,6 +348,35 @@ export const receiveAnnotationsFailure = ({ commit }) => commit(types.RECEIVE_AN
// Dashboard manipulation
+export const toggleStarredValue = ({ commit, state, getters }) => {
+ const { selectedDashboard } = getters;
+
+ if (state.isUpdatingStarredValue) {
+ // Prevent repeating requests for the same change
+ return;
+ }
+ if (!selectedDashboard) {
+ return;
+ }
+
+ const method = selectedDashboard.starred ? 'DELETE' : 'POST';
+ const url = selectedDashboard.user_starred_path;
+ const newStarredValue = !selectedDashboard.starred;
+
+ commit(types.REQUEST_DASHBOARD_STARRING);
+
+ axios({
+ url,
+ method,
+ })
+ .then(() => {
+ commit(types.RECEIVE_DASHBOARD_STARRING_SUCCESS, newStarredValue);
+ })
+ .catch(() => {
+ commit(types.RECEIVE_DASHBOARD_STARRING_FAILURE);
+ });
+};
+
/**
* Set a new array of metrics to a panel group
* @param {*} data An object containing
@@ -364,5 +414,11 @@ export const duplicateSystemDashboard = ({ state }, payload) => {
});
};
+// Variables manipulation
+
+export const updateVariableValues = ({ commit }, updatedVariable) => {
+ commit(types.UPDATE_VARIABLE_VALUES, updatedVariable);
+};
+
// prevent babel-plugin-rewire from generating an invalid default during karma tests
export default () => {};
diff --git a/app/assets/javascripts/monitoring/stores/getters.js b/app/assets/javascripts/monitoring/stores/getters.js
index a6d80c5063e..ae3ff5596e1 100644
--- a/app/assets/javascripts/monitoring/stores/getters.js
+++ b/app/assets/javascripts/monitoring/stores/getters.js
@@ -1,9 +1,25 @@
+import { flatMap } from 'lodash';
import { NOT_IN_DB_PREFIX } from '../constants';
const metricsIdsInPanel = panel =>
panel.metrics.filter(metric => metric.metricId && metric.result).map(metric => metric.metricId);
/**
+ * Returns a reference to the currently selected dashboard
+ * from the list of dashboards.
+ *
+ * @param {Object} state
+ */
+export const selectedDashboard = state => {
+ const { allDashboards } = state;
+ return (
+ allDashboards.find(d => d.path === state.currentDashboard) ||
+ allDashboards.find(d => d.default) ||
+ null
+ );
+};
+
+/**
* Get all state for metric in the dashboard or a group. The
* states are not repeated so the dashboard or group can show
* a global state.
@@ -96,5 +112,17 @@ export const filteredEnvironments = state =>
env.name.toLowerCase().includes((state.environmentsSearchTerm || '').trim().toLowerCase()),
);
+/**
+ * Maps an variables object to an array along with stripping
+ * the variable prefix.
+ *
+ * @param {Object} variables - Custom variables provided by the user
+ * @returns {Array} The custom variables array to be send to the API
+ * in the format of [variable1, variable1_value]
+ */
+
+export const getCustomVariablesArray = state =>
+ flatMap(state.promVariables, (variable, key) => [key, variable.value]);
+
// prevent babel-plugin-rewire from generating an invalid default during karma tests
export default () => {};
diff --git a/app/assets/javascripts/monitoring/stores/mutation_types.js b/app/assets/javascripts/monitoring/stores/mutation_types.js
index 27a9a67edaa..d60334609fd 100644
--- a/app/assets/javascripts/monitoring/stores/mutation_types.js
+++ b/app/assets/javascripts/monitoring/stores/mutation_types.js
@@ -1,7 +1,13 @@
-// Dashboard "skeleton", groups, panels and metrics
+// Dashboard "skeleton", groups, panels, metrics, query variables
export const REQUEST_METRICS_DASHBOARD = 'REQUEST_METRICS_DASHBOARD';
export const RECEIVE_METRICS_DASHBOARD_SUCCESS = 'RECEIVE_METRICS_DASHBOARD_SUCCESS';
export const RECEIVE_METRICS_DASHBOARD_FAILURE = 'RECEIVE_METRICS_DASHBOARD_FAILURE';
+export const SET_VARIABLES = 'SET_VARIABLES';
+export const UPDATE_VARIABLE_VALUES = 'UPDATE_VARIABLE_VALUES';
+
+export const REQUEST_DASHBOARD_STARRING = 'REQUEST_DASHBOARD_STARRING';
+export const RECEIVE_DASHBOARD_STARRING_SUCCESS = 'RECEIVE_DASHBOARD_STARRING_SUCCESS';
+export const RECEIVE_DASHBOARD_STARRING_FAILURE = 'RECEIVE_DASHBOARD_STARRING_FAILURE';
// Annotations
export const RECEIVE_ANNOTATIONS_SUCCESS = 'RECEIVE_ANNOTATIONS_SUCCESS';
@@ -31,5 +37,5 @@ export const SET_GETTING_STARTED_EMPTY_STATE = 'SET_GETTING_STARTED_EMPTY_STATE'
export const SET_NO_DATA_EMPTY_STATE = 'SET_NO_DATA_EMPTY_STATE';
export const SET_SHOW_ERROR_BANNER = 'SET_SHOW_ERROR_BANNER';
export const SET_PANEL_GROUP_METRICS = 'SET_PANEL_GROUP_METRICS';
-
export const SET_ENVIRONMENTS_FILTER = 'SET_ENVIRONMENTS_FILTER';
+export const SET_EXPANDED_PANEL = 'SET_EXPANDED_PANEL';
diff --git a/app/assets/javascripts/monitoring/stores/mutations.js b/app/assets/javascripts/monitoring/stores/mutations.js
index aa31b6642d7..f41cf3fc477 100644
--- a/app/assets/javascripts/monitoring/stores/mutations.js
+++ b/app/assets/javascripts/monitoring/stores/mutations.js
@@ -1,5 +1,7 @@
-import pick from 'lodash/pick';
+import Vue from 'vue';
+import { pick } from 'lodash';
import * as types from './mutation_types';
+import { selectedDashboard } from './getters';
import { mapToDashboardViewModel, normalizeQueryResult } from './utils';
import { BACKOFF_TIMEOUT } from '../../lib/utils/common_utils';
import { endpointKeys, initialStateKeys, metricStates } from '../constants';
@@ -71,6 +73,23 @@ export default {
state.showEmptyState = true;
},
+ [types.REQUEST_DASHBOARD_STARRING](state) {
+ state.isUpdatingStarredValue = true;
+ },
+ [types.RECEIVE_DASHBOARD_STARRING_SUCCESS](state, newStarredValue) {
+ const dashboard = selectedDashboard(state);
+ const index = state.allDashboards.findIndex(d => d === dashboard);
+
+ state.isUpdatingStarredValue = false;
+
+ // Trigger state updates in the reactivity system for this change
+ // https://vuejs.org/v2/guide/reactivity.html#For-Arrays
+ Vue.set(state.allDashboards, index, { ...dashboard, starred: newStarredValue });
+ },
+ [types.RECEIVE_DASHBOARD_STARRING_FAILURE](state) {
+ state.isUpdatingStarredValue = false;
+ },
+
/**
* Deployments and environments
*/
@@ -134,6 +153,8 @@ export default {
metric.loading = false;
metric.result = null;
},
+
+ // Parameters and other information
[types.SET_INITIAL_STATE](state, initialState = {}) {
Object.assign(state, pick(initialState, initialStateKeys));
},
@@ -163,4 +184,17 @@ export default {
[types.SET_ENVIRONMENTS_FILTER](state, searchTerm) {
state.environmentsSearchTerm = searchTerm;
},
+ [types.SET_EXPANDED_PANEL](state, { group, panel }) {
+ state.expandedPanel.group = group;
+ state.expandedPanel.panel = panel;
+ },
+ [types.SET_VARIABLES](state, variables) {
+ state.promVariables = variables;
+ },
+ [types.UPDATE_VARIABLE_VALUES](state, updatedVariable) {
+ Object.assign(state.promVariables[updatedVariable.key], {
+ ...state.promVariables[updatedVariable.key],
+ value: updatedVariable.value,
+ });
+ },
};
diff --git a/app/assets/javascripts/monitoring/stores/state.js b/app/assets/javascripts/monitoring/stores/state.js
index e60510e747b..9ae1da93e5f 100644
--- a/app/assets/javascripts/monitoring/stores/state.js
+++ b/app/assets/javascripts/monitoring/stores/state.js
@@ -14,10 +14,27 @@ export default () => ({
emptyState: 'gettingStarted',
showEmptyState: true,
showErrorBanner: true,
+ isUpdatingStarredValue: false,
dashboard: {
panelGroups: [],
},
+ /**
+ * Panel that is currently "zoomed" in as
+ * a single panel in view.
+ */
+ expandedPanel: {
+ /**
+ * {?String} Panel's group name.
+ */
+ group: null,
+ /**
+ * {?Object} Panel content from `dashboard`
+ * null when no panel is expanded.
+ */
+ panel: null,
+ },
allDashboards: [],
+ promVariables: {},
// Other project data
annotations: [],
diff --git a/app/assets/javascripts/monitoring/stores/utils.js b/app/assets/javascripts/monitoring/stores/utils.js
index 9f06d18c46f..a47e5f598f5 100644
--- a/app/assets/javascripts/monitoring/stores/utils.js
+++ b/app/assets/javascripts/monitoring/stores/utils.js
@@ -144,6 +144,7 @@ const mapYAxisToViewModel = ({
* @returns {Object}
*/
const mapPanelToViewModel = ({
+ id = null,
title = '',
type,
x_axis = {},
@@ -162,6 +163,7 @@ const mapPanelToViewModel = ({
const yAxis = mapYAxisToViewModel({ name: y_label, ...y_axis }); // eslint-disable-line babel/camelcase
return {
+ id,
title,
type,
xLabel: xAxis.name,
diff --git a/app/assets/javascripts/monitoring/stores/variable_mapping.js b/app/assets/javascripts/monitoring/stores/variable_mapping.js
new file mode 100644
index 00000000000..bfb469da19e
--- /dev/null
+++ b/app/assets/javascripts/monitoring/stores/variable_mapping.js
@@ -0,0 +1,167 @@
+import { isString } from 'lodash';
+import { VARIABLE_TYPES } from '../constants';
+
+/**
+ * This file exclusively deals with parsing user-defined variables
+ * in dashboard yml file.
+ *
+ * As of 13.0, simple text, advanced text, simple custom and
+ * advanced custom variables are supported.
+ *
+ * In the future iterations, text and query variables will be
+ * supported
+ *
+ */
+
+/**
+ * Simple text variable is a string value only.
+ * This method parses such variables to a standard format.
+ *
+ * @param {String|Object} simpleTextVar
+ * @returns {Object}
+ */
+const textSimpleVariableParser = simpleTextVar => ({
+ type: VARIABLE_TYPES.text,
+ label: null,
+ value: simpleTextVar,
+});
+
+/**
+ * Advanced text variable is an object.
+ * This method parses such variables to a standard format.
+ *
+ * @param {Object} advTextVar
+ * @returns {Object}
+ */
+const textAdvancedVariableParser = advTextVar => ({
+ type: VARIABLE_TYPES.text,
+ label: advTextVar.label,
+ value: advTextVar.options.default_value,
+});
+
+/**
+ * Normalize simple and advanced custom variable options to a standard
+ * format
+ * @param {Object} custom variable option
+ * @returns {Object} normalized custom variable options
+ */
+const normalizeCustomVariableOptions = ({ default: defaultOpt = false, text, value }) => ({
+ default: defaultOpt,
+ text,
+ value,
+});
+
+/**
+ * Custom advanced variables are rendered as dropdown elements in the dashboard
+ * header. This method parses advanced custom variables.
+ *
+ * The default value is the option with default set to true or the first option
+ * if none of the options have default prop true.
+ *
+ * @param {Object} advVariable advance custom variable
+ * @returns {Object}
+ */
+const customAdvancedVariableParser = advVariable => {
+ const options = (advVariable?.options?.values ?? []).map(normalizeCustomVariableOptions);
+ const defaultOpt = options.find(opt => opt.default === true) || options[0];
+ return {
+ type: VARIABLE_TYPES.custom,
+ label: advVariable.label,
+ value: defaultOpt?.value,
+ options,
+ };
+};
+
+/**
+ * Simple custom variables have an array of values.
+ * This method parses such variables options to a standard format.
+ *
+ * @param {String} opt option from simple custom variable
+ * @returns {Object}
+ */
+const parseSimpleCustomOptions = opt => ({ text: opt, value: opt });
+
+/**
+ * Custom simple variables are rendered as dropdown elements in the dashboard
+ * header. This method parses simple custom variables.
+ *
+ * Simple custom variables do not have labels so its set to null here.
+ *
+ * The default value is set to the first option as the user cannot
+ * set a default value for this format
+ *
+ * @param {Array} customVariable array of options
+ * @returns {Object}
+ */
+const customSimpleVariableParser = simpleVar => {
+ const options = (simpleVar || []).map(parseSimpleCustomOptions);
+ return {
+ type: VARIABLE_TYPES.custom,
+ value: options[0].value,
+ label: null,
+ options: options.map(normalizeCustomVariableOptions),
+ };
+};
+
+/**
+ * Utility method to determine if a custom variable is
+ * simple or not. If its not simple, it is advanced.
+ *
+ * @param {Array|Object} customVar Array if simple, object if advanced
+ * @returns {Boolean} true if simple, false if advanced
+ */
+const isSimpleCustomVariable = customVar => Array.isArray(customVar);
+
+/**
+ * This method returns a parser based on the type of the variable.
+ * Currently, the supported variables are simple custom and
+ * advanced custom only. In the future, this method will support
+ * text and query variables.
+ *
+ * @param {Array|Object} variable
+ * @return {Function} parser method
+ */
+const getVariableParser = variable => {
+ if (isSimpleCustomVariable(variable)) {
+ return customSimpleVariableParser;
+ } else if (variable.type === VARIABLE_TYPES.custom) {
+ return customAdvancedVariableParser;
+ } else if (variable.type === VARIABLE_TYPES.text) {
+ return textAdvancedVariableParser;
+ } else if (isString(variable)) {
+ return textSimpleVariableParser;
+ }
+ return () => null;
+};
+
+/**
+ * This method parses the templating property in the dashboard yml file.
+ * The templating property has variables that are rendered as input elements
+ * for the user to edit. The values from input elements are relayed to
+ * backend and eventually Prometheus API.
+ *
+ * This method currently is not used anywhere. Once the issue
+ * https://gitlab.com/gitlab-org/gitlab/-/issues/214536 is completed,
+ * this method will have been used by the monitoring dashboard.
+ *
+ * @param {Object} templating templating variables from the dashboard yml file
+ * @returns {Object} a map of processed templating variables
+ */
+export const parseTemplatingVariables = ({ variables = {} } = {}) =>
+ Object.entries(variables).reduce((acc, [key, variable]) => {
+ // get the parser
+ const parser = getVariableParser(variable);
+ // parse the variable
+ const parsedVar = parser(variable);
+ // for simple custom variable label is null and it should be
+ // replace with key instead
+ if (parsedVar) {
+ acc[key] = {
+ ...parsedVar,
+ label: parsedVar.label || key,
+ };
+ }
+ return acc;
+ }, {});
+
+export default {};
diff --git a/app/assets/javascripts/monitoring/utils.js b/app/assets/javascripts/monitoring/utils.js
index 7c6cd19eb7b..1f028ffbcad 100644
--- a/app/assets/javascripts/monitoring/utils.js
+++ b/app/assets/javascripts/monitoring/utils.js
@@ -1,9 +1,23 @@
-import { queryToObject, mergeUrlParams, removeParams } from '~/lib/utils/url_utility';
+import { pickBy, mapKeys } from 'lodash';
+import {
+ queryToObject,
+ mergeUrlParams,
+ removeParams,
+ updateHistory,
+} from '~/lib/utils/url_utility';
import {
timeRangeParamNames,
timeRangeFromParams,
timeRangeToParams,
} from '~/lib/utils/datetime_range';
+import { VARIABLE_PREFIX } from './constants';
+
+/**
+ * List of non time range url parameters
+ * This will be removed once we add support for free text variables
+ * via the dashboard yaml files in https://gitlab.com/gitlab-org/gitlab/-/issues/215689
+ */
+export const dashboardParams = ['dashboard', 'group', 'title', 'y_label', 'embedded'];
/**
* This method is used to validate if the graph data format for a chart component
@@ -28,7 +42,6 @@ export const graphDataValidatorForValues = (isValues, graphData) => {
);
};
-/* eslint-disable @gitlab/require-i18n-strings */
/**
* Checks that element that triggered event is located on cluster health check dashboard
* @param {HTMLElement} element to check against
@@ -36,6 +49,7 @@ export const graphDataValidatorForValues = (isValues, graphData) => {
*/
const isClusterHealthBoard = () => (document.body.dataset.page || '').includes(':clusters:show');
+/* eslint-disable @gitlab/require-i18n-strings */
/**
* Tracks snowplow event when user generates link to metric chart
* @param {String} chart link that will be sent as a property for the event
@@ -71,6 +85,7 @@ export const downloadCSVOptions = title => {
return { category, action, label: 'Chart title', property: title };
};
+/* eslint-enable @gitlab/require-i18n-strings */
/**
* Generate options for snowplow to track adding a new metric via the dashboard
@@ -113,6 +128,78 @@ export const timeRangeFromUrl = (search = window.location.search) => {
};
/**
+ * Variable labels are used as names for the dropdowns and also
+ * as URL params. Prefixing the name reduces the risk of
+ * collision with other URL params
+ *
+ * @param {String} label label for the template variable
+ * @returns {String}
+ */
+export const addPrefixToLabel = label => `${VARIABLE_PREFIX}${label}`;
+
+/**
+ * Before the templating variables are passed to the backend the
+ * prefix needs to be removed.
+ *
+ * This method removes the prefix at the beginning of the string.
+ *
+ * @param {String} label label to remove prefix from
+ * @returns {String}
+ */
+export const removePrefixFromLabel = label =>
+ (label || '').replace(new RegExp(`^${VARIABLE_PREFIX}`), '');
+
+/**
+ * Convert parsed template variables to an object
+ * with just keys and values. Prepare the promVariables
+ * to be added to the URL. Keys of the object will
+ * have a prefix so that these params can be
+ * differentiated from other URL params.
+ *
+ * @param {Object} variables
+ * @returns {Object}
+ */
+export const convertVariablesForURL = variables =>
+ Object.keys(variables || {}).reduce((acc, key) => {
+ acc[addPrefixToLabel(key)] = variables[key]?.value;
+ return acc;
+ }, {});
+
+/**
+ * User-defined variables from the URL are extracted. The variables
+ * begin with a constant prefix so that it doesn't collide with
+ * other URL params.
+ *
+ * @param {String} New URL
+ * @returns {Object} The custom variables defined by the user in the URL
+ */
+
+export const getPromCustomVariablesFromUrl = (search = window.location.search) => {
+ const params = queryToObject(search);
+ // pick the params with variable prefix
+ const paramsWithVars = pickBy(params, (val, key) => key.startsWith(VARIABLE_PREFIX));
+ // remove the prefix before storing in the Vuex store
+ return mapKeys(paramsWithVars, (val, key) => removePrefixFromLabel(key));
+};
+
+/**
+ * Update the URL with promVariables. This usually get triggered when
+ * the user interacts with the dynamic input elements in the monitoring
+ * dashboard header.
+ *
+ * @param {Object} promVariables user defined variables
+ */
+export const setPromCustomVariablesFromUrl = promVariables => {
+ // prep the variables to append to URL
+ const parsedVariables = convertVariablesForURL(promVariables);
+ // update the URL
+ updateHistory({
+ url: mergeUrlParams(parsedVariables, window.location.href),
+ title: document.title,
+ });
+};
+
+/**
* Returns a URL with no time range based on the current URL.
*
* @param {String} New URL
@@ -133,6 +220,81 @@ export const timeRangeToUrl = (timeRange, url = window.location.href) => {
};
/**
+ * Locates a panel (and its corresponding group) given a (URL) search query. Returns
+ * it as payload for the store to set the right expandaded panel.
+ *
+ * Params used to locate a panel are:
+ * - group: Group identifier
+ * - title: Panel title
+ * - y_label: Panel y_label
+ *
+ * @param {Object} dashboard - Dashboard reference from the Vuex store
+ * @param {String} search - URL location search query
+ * @returns {Object} payload - Payload for expanded panel to be displayed
+ * @returns {String} payload.group - Group where panel is located
+ * @returns {Object} payload.panel - Dashboard panel (graphData) reference
+ * @throws Will throw an error if Panel cannot be located.
+ */
+export const expandedPanelPayloadFromUrl = (dashboard, search = window.location.search) => {
+ const params = queryToObject(search);
+
+ // Search for the panel if any of the search params is identified
+ if (params.group || params.title || params.y_label) {
+ const panelGroup = dashboard.panelGroups.find(({ group }) => params.group === group);
+ const panel = panelGroup.panels.find(
+ // eslint-disable-next-line babel/camelcase
+ ({ y_label, title }) => y_label === params.y_label && title === params.title,
+ );
+
+ if (!panel) {
+ // eslint-disable-next-line @gitlab/require-i18n-strings
+ throw new Error('Panel could no found by URL parameters.');
+ }
+ return { group: panelGroup.group, panel };
+ }
+ return null;
+};
+
+/**
+ * Convert panel information to a URL for the user to
+ * bookmark or share highlighting a specific panel.
+ *
+ * If no group/panel is set, the dashboard URL is returned.
+ *
+ * @param {?String} dashboard - Dashboard path, used as identifier for a dashboard
+ * @param {?Object} promVariables - Custom variables that came from the URL
+ * @param {?String} group - Group Identifier
+ * @param {?Object} panel - Panel object from the dashboard
+ * @param {?String} url - Base URL including current search params
+ * @returns Dashboard URL which expands a panel (chart)
+ */
+export const panelToUrl = (
+ dashboard = null,
+ promVariables,
+ group,
+ panel,
+ url = window.location.href,
+) => {
+ const params = {
+ dashboard,
+ ...promVariables,
+ };
+
+ if (group && panel) {
+ params.group = group;
+ params.title = panel.title;
+ params.y_label = panel.y_label;
+ } else {
+ // Remove existing parameters if any
+ params.group = null;
+ params.title = null;
+ params.y_label = null;
+ }
+
+ return mergeUrlParams(params, url);
+};
+
+/**
* Get the metric value from first data point.
* Currently only used for bar charts
*
@@ -191,4 +353,39 @@ export const barChartsDataParser = (data = []) =>
{},
);
+/**
+ * Custom variables are defined in the dashboard yml file
+ * and their values can be passed through the URL.
+ *
+ * On component load, this method merges variables data
+ * from the yml file with URL data to store in the Vuex store.
+ * Not all params coming from the URL need to be stored. Only
+ * the ones that have a corresponding variable defined in the
+ * yml file.
+ *
+ * This ensures that there is always a single source of truth
+ * for variables
+ *
+ * This method can be improved further. See the below issue
+ * https://gitlab.com/gitlab-org/gitlab/-/issues/217713
+ *
+ * @param {Object} varsFromYML template variables from yml file
+ * @returns {Object}
+ */
+export const mergeURLVariables = (varsFromYML = {}) => {
+ const varsFromURL = getPromCustomVariablesFromUrl();
+ const variables = {};
+ Object.keys(varsFromYML).forEach(key => {
+ if (Object.prototype.hasOwnProperty.call(varsFromURL, key)) {
+ variables[key] = {
+ ...varsFromYML[key],
+ value: varsFromURL[key],
+ };
+ } else {
+ variables[key] = varsFromYML[key];
+ }
+ });
+ return variables;
+};
+
export default {};
diff --git a/app/assets/javascripts/monitoring/validators.js b/app/assets/javascripts/monitoring/validators.js
new file mode 100644
index 00000000000..cd426f1a221
--- /dev/null
+++ b/app/assets/javascripts/monitoring/validators.js
@@ -0,0 +1,44 @@
+// Prop validator for alert information, expecting an object like the example below.
+//
+// {
+// '/root/autodevops-deploy/prometheus/alerts/16.json?environment_id=37': {
+// alert_path: "/root/autodevops-deploy/prometheus/alerts/16.json?environment_id=37",
+// metricId: '1',
+// operator: ">",
+// query: "rate(http_requests_total[5m])[30m:1m]",
+// threshold: 0.002,
+// title: "Core Usage (Total)",
+// }
+// }
+export function alertsValidator(value) {
+ return Object.keys(value).every(key => {
+ const alert = value[key];
+ return (
+ alert.alert_path &&
+ key === alert.alert_path &&
+ alert.metricId &&
+ typeof alert.metricId === 'string' &&
+ alert.operator &&
+ typeof alert.threshold === 'number'
+ );
+ });
+}
+
+// Prop validator for query information, expecting an array like the example below.
+//
+// [
+// {
+// metricId: '16',
+// label: 'Total Cores'
+// },
+// {
+// metricId: '17',
+// label: 'Sub-total Cores'
+// }
+// ]
+export function queriesValidator(value) {
+ return value.every(
+ query =>
+ query.metricId && typeof query.metricId === 'string' && typeof query.label === 'string',
+ );
+}
diff --git a/app/assets/javascripts/namespace_select.js b/app/assets/javascripts/namespace_select.js
index 8671f0fd783..b96a111cf13 100644
--- a/app/assets/javascripts/namespace_select.js
+++ b/app/assets/javascripts/namespace_select.js
@@ -1,5 +1,3 @@
-/* eslint-disable no-else-return */
-
import $ from 'jquery';
import '~/gl_dropdown';
import Api from './api';
@@ -23,9 +21,8 @@ export default class NamespaceSelect {
toggleLabel(selected) {
if (selected.id == null) {
return selected.text;
- } else {
- return `${selected.kind}: ${selected.full_path}`;
}
+ return `${selected.kind}: ${selected.full_path}`;
},
data(term, dataCallback) {
return Api.namespaces(term, namespaces => {
@@ -43,9 +40,8 @@ export default class NamespaceSelect {
text(namespace) {
if (namespace.id == null) {
return namespace.text;
- } else {
- return `${namespace.kind}: ${namespace.full_path}`;
}
+ return `${namespace.kind}: ${namespace.full_path}`;
},
renderRow: this.renderRow,
clicked(options) {
diff --git a/app/assets/javascripts/new_branch_form.js b/app/assets/javascripts/new_branch_form.js
index be3ea4e680c..9d064894433 100644
--- a/app/assets/javascripts/new_branch_form.js
+++ b/app/assets/javascripts/new_branch_form.js
@@ -1,4 +1,4 @@
-/* eslint-disable func-names, consistent-return, no-return-assign, no-else-return, @gitlab/require-i18n-strings */
+/* eslint-disable func-names, consistent-return, no-return-assign, @gitlab/require-i18n-strings */
import $ from 'jquery';
import RefSelectDropdown from './ref_select_dropdown';
@@ -76,9 +76,8 @@ export default class NewBranchForm {
const matched = this.name.val().match(restriction.pattern);
if (matched) {
return errors.concat(formatter(matched.reduce(unique, []), restriction));
- } else {
- return errors;
}
+ return errors;
};
const errors = this.restrictions.reduce(validator, []);
if (errors.length > 0) {
diff --git a/app/assets/javascripts/notebook/cells/markdown.vue b/app/assets/javascripts/notebook/cells/markdown.vue
index dab27cf8269..fcb09ea90db 100644
--- a/app/assets/javascripts/notebook/cells/markdown.vue
+++ b/app/assets/javascripts/notebook/cells/markdown.vue
@@ -36,9 +36,9 @@ const katexRegexString = `(
.replace(/\s/g, '')
.trim();
-renderer.paragraph = t => {
+function renderKatex(t) {
let text = t;
- let inline = false;
+ let numInline = 0; // number of successfull converted math formulas
if (typeof katex !== 'undefined') {
const katexString = text
@@ -50,24 +50,40 @@ renderer.paragraph = t => {
const numberOfMatches = katexString.match(regex);
if (numberOfMatches && numberOfMatches.length !== 0) {
+ let matches = regex.exec(katexString);
if (matchLocation > 0) {
- let matches = regex.exec(katexString);
- inline = true;
+ numInline += 1;
while (matches !== null) {
- const renderedKatex = katex.renderToString(matches[0].replace(/\$/g, ''));
- text = `${text.replace(matches[0], ` ${renderedKatex}`)}`;
+ try {
+ const renderedKatex = katex.renderToString(
+ matches[0].replace(/\$/g, '').replace(/&#39;/g, "'"),
+ ); // get the tick ' back again from HTMLified string
+ text = `${text.replace(matches[0], ` ${renderedKatex}`)}`;
+ } catch {
+ numInline -= 1;
+ }
matches = regex.exec(katexString);
}
} else {
- const matches = regex.exec(katexString);
- text = katex.renderToString(matches[2]);
+ try {
+ text = katex.renderToString(matches[2].replace(/&#39;/g, "'"));
+ } catch (error) {
+ numInline -= 1;
+ }
}
}
}
-
+ return [text, numInline > 0];
+}
+renderer.paragraph = t => {
+ const [text, inline] = renderKatex(t);
return `<p class="${inline ? 'inline-katex' : ''}">${text}</p>`;
};
+renderer.listitem = t => {
+ const [text, inline] = renderKatex(t);
+ return `<li class="${inline ? 'inline-katex' : ''}">${text}</li>`;
+};
marked.setOptions({
renderer,
diff --git a/app/assets/javascripts/notebook/cells/output/index.vue b/app/assets/javascripts/notebook/cells/output/index.vue
index 61626f7aaf5..f2d3796cccf 100644
--- a/app/assets/javascripts/notebook/cells/output/index.vue
+++ b/app/assets/javascripts/notebook/cells/output/index.vue
@@ -63,6 +63,9 @@ export default {
},
rawCode(output) {
if (output.text) {
+ if (typeof output.text === 'string') {
+ return output.text;
+ }
return output.text.join('');
}
diff --git a/app/assets/javascripts/notes.js b/app/assets/javascripts/notes.js
index 9e2231922b7..6e695de447d 100644
--- a/app/assets/javascripts/notes.js
+++ b/app/assets/javascripts/notes.js
@@ -1,6 +1,6 @@
/* eslint-disable no-restricted-properties, babel/camelcase,
no-unused-expressions, default-case,
-consistent-return, no-alert, no-param-reassign, no-else-return,
+consistent-return, no-alert, no-param-reassign,
no-shadow, no-useless-escape,
class-methods-use-this */
@@ -256,7 +256,7 @@ export default class Notes {
discussionNoteForm = $textarea.closest('.js-discussion-note-form');
if (discussionNoteForm.length) {
if ($textarea.val() !== '') {
- if (!window.confirm(__('Are you sure you want to cancel creating this comment?'))) {
+ if (!window.confirm(__('Your comment will be discarded.'))) {
return;
}
}
@@ -268,7 +268,7 @@ export default class Notes {
originalText = $textarea.closest('form').data('originalNote');
newText = $textarea.val();
if (originalText !== newText) {
- if (!window.confirm(__('Are you sure you want to cancel editing this comment?'))) {
+ if (!window.confirm(__('Are you sure you want to discard this comment?'))) {
return;
}
}
@@ -964,11 +964,11 @@ export default class Notes {
form
.prepend(
- `<div class="avatar-note-form-holder"><div class="content"><a href="${escape(
+ `<a href="${escape(
gon.current_username,
)}" class="user-avatar-link d-none d-sm-block"><img class="avatar s40" src="${encodeURI(
- gon.current_user_avatar_url,
- )}" alt="${escape(gon.current_user_fullname)}" /></a></div></div>`,
+ gon.current_user_avatar_url || gon.default_avatar_url,
+ )}" alt="${escape(gon.current_user_fullname)}" /></a>`,
)
.append('</div>')
.find('.js-close-discussion-note-form')
@@ -1123,10 +1123,9 @@ export default class Notes {
if (row.is('.js-temp-notes-holder')) {
// remove temporary row for diff lines
return row.remove();
- } else {
- // only remove the form
- return form.remove();
}
+ // only remove the form
+ return form.remove();
}
cancelDiscussionForm(e) {
@@ -1397,7 +1396,7 @@ export default class Notes {
}
/**
- * Check if note does not exists on page
+ * Check if note does not exist on page
*/
static isNewNote(noteEntity, noteIds) {
return $.inArray(noteEntity.id, noteIds) === -1;
diff --git a/app/assets/javascripts/notes/components/comment_form.vue b/app/assets/javascripts/notes/components/comment_form.vue
index 9a809b71a58..a070cf8866a 100644
--- a/app/assets/javascripts/notes/components/comment_form.vue
+++ b/app/assets/javascripts/notes/components/comment_form.vue
@@ -3,6 +3,7 @@ import $ from 'jquery';
import { mapActions, mapGetters, mapState } from 'vuex';
import { isEmpty } from 'lodash';
import Autosize from 'autosize';
+import { GlAlert, GlIntersperse, GlLink, GlSprintf } from '@gitlab/ui';
import { __, sprintf } from '~/locale';
import TimelineEntryItem from '~/vue_shared/components/notes/timeline_entry_item.vue';
import Flash from '../../flash';
@@ -34,6 +35,10 @@ export default {
userAvatarLink,
loadingButton,
TimelineEntryItem,
+ GlAlert,
+ GlIntersperse,
+ GlLink,
+ GlSprintf,
},
mixins: [issuableStateMixin],
props: {
@@ -57,8 +62,9 @@ export default {
'getNoteableData',
'getNotesData',
'openState',
+ 'getBlockedByIssues',
]),
- ...mapState(['isToggleStateButtonLoading']),
+ ...mapState(['isToggleStateButtonLoading', 'isToggleBlockedIssueWarning']),
noteableDisplayName() {
return splitCamelCase(this.noteableType).toLowerCase();
},
@@ -159,6 +165,7 @@ export default {
'reopenIssue',
'toggleIssueLocalState',
'toggleStateButtonLoading',
+ 'toggleBlockedIssueWarning',
]),
setIsSubmitButtonDisabled(note, isSubmitting) {
if (!isEmpty(note) && !isSubmitting) {
@@ -220,22 +227,17 @@ export default {
this.isSubmitting = false;
},
toggleIssueState() {
+ if (
+ this.noteableType.toLowerCase() === constants.ISSUE_NOTEABLE_TYPE &&
+ this.isOpen &&
+ this.getBlockedByIssues &&
+ this.getBlockedByIssues.length > 0
+ ) {
+ this.toggleBlockedIssueWarning(true);
+ return;
+ }
if (this.isOpen) {
- this.closeIssue()
- .then(() => {
- this.enableButton();
- refreshUserMergeRequestCounts();
- })
- .catch(() => {
- this.enableButton();
- this.toggleStateButtonLoading(false);
- Flash(
- sprintf(
- __('Something went wrong while closing the %{issuable}. Please try again later'),
- { issuable: this.noteableDisplayName },
- ),
- );
- });
+ this.forceCloseIssue();
} else {
this.reopenIssue()
.then(() => {
@@ -258,6 +260,23 @@ export default {
});
}
},
+ forceCloseIssue() {
+ this.closeIssue()
+ .then(() => {
+ this.enableButton();
+ refreshUserMergeRequestCounts();
+ })
+ .catch(() => {
+ this.enableButton();
+ this.toggleStateButtonLoading(false);
+ Flash(
+ sprintf(
+ __('Something went wrong while closing the %{issuable}. Please try again later'),
+ { issuable: this.noteableDisplayName },
+ ),
+ );
+ });
+ },
discard(shouldClear = true) {
// `blur` is needed to clear slash commands autocomplete cache if event fired.
// `focus` is needed to remain cursor in the textarea.
@@ -361,6 +380,36 @@ js-gfm-input js-autosize markdown-area js-vue-textarea qa-comment-input"
>
</textarea>
</markdown-field>
+ <gl-alert
+ v-if="isToggleBlockedIssueWarning"
+ class="prepend-top-16"
+ :title="__('Are you sure you want to close this blocked issue?')"
+ :primary-button-text="__('Yes, close issue')"
+ :secondary-button-text="__('Cancel')"
+ variant="warning"
+ :dismissible="false"
+ @primaryAction="forceCloseIssue"
+ @secondaryAction="toggleBlockedIssueWarning(false) && enableButton()"
+ >
+ <p>
+ <gl-sprintf
+ :message="
+ __('This issue is currently blocked by the following issues: %{issues}.')
+ "
+ >
+ <template #issues>
+ <gl-intersperse>
+ <gl-link
+ v-for="blockingIssue in getBlockedByIssues"
+ :key="blockingIssue.web_url"
+ :href="blockingIssue.web_url"
+ >#{{ blockingIssue.iid }}</gl-link
+ >
+ </gl-intersperse>
+ </template>
+ </gl-sprintf>
+ </p>
+ </gl-alert>
<div class="note-form-actions">
<div
class="float-left btn-group
@@ -427,7 +476,7 @@ append-right-10 comment-type-dropdown js-comment-type-dropdown droplab-dropdown"
</div>
<loading-button
- v-if="canToggleIssueState"
+ v-if="canToggleIssueState && !isToggleBlockedIssueWarning"
:loading="isToggleStateButtonLoading"
:container-class="[
actionButtonClassNames,
diff --git a/app/assets/javascripts/notes/components/discussion_counter.vue b/app/assets/javascripts/notes/components/discussion_counter.vue
index 07952f9edd9..4a1a1086329 100644
--- a/app/assets/javascripts/notes/components/discussion_counter.vue
+++ b/app/assets/javascripts/notes/components/discussion_counter.vue
@@ -29,9 +29,6 @@ export default {
resolveAllDiscussionsIssuePath() {
return this.getNoteableData.create_issue_to_resolve_discussions_path;
},
- resolvedDiscussionsCount() {
- return this.resolvableDiscussionsCount - this.unresolvedDiscussionsCount;
- },
toggeableDiscussions() {
return this.discussions.filter(discussion => !discussion.individual_note);
},
@@ -60,15 +57,15 @@ export default {
<div class="full-width-mobile d-flex d-sm-flex">
<div class="line-resolve-all">
<span
- :class="{ 'is-active': allResolved }"
- class="line-resolve-btn is-disabled"
- type="button"
+ :class="{ 'line-resolve-btn is-active': allResolved, 'line-resolve-text': !allResolved }"
>
- <icon :name="allResolved ? 'check-circle-filled' : 'check-circle'" />
- </span>
- <span class="line-resolve-text">
- {{ resolvedDiscussionsCount }}/{{ resolvableDiscussionsCount }}
- {{ n__('thread resolved', 'threads resolved', resolvableDiscussionsCount) }}
+ <template v-if="allResolved">
+ <icon name="check-circle-filled" />
+ {{ __('All threads resolved') }}
+ </template>
+ <template v-else>
+ {{ n__('%d unresolved thread', '%d unresolved threads', unresolvedDiscussionsCount) }}
+ </template>
</span>
</div>
<div
diff --git a/app/assets/javascripts/notes/components/note_form.vue b/app/assets/javascripts/notes/components/note_form.vue
index b024884bea0..21d0bffdf1c 100644
--- a/app/assets/javascripts/notes/components/note_form.vue
+++ b/app/assets/javascripts/notes/components/note_form.vue
@@ -328,7 +328,8 @@ export default {
<button
class="btn note-edit-cancel js-close-discussion-note-form"
type="button"
- @click="cancelHandler()"
+ data-testid="cancelBatchCommentsEnabled"
+ @click="cancelHandler(true)"
>
{{ __('Cancel') }}
</button>
@@ -353,7 +354,8 @@ export default {
<button
class="btn btn-cancel note-edit-cancel js-close-discussion-note-form"
type="button"
- @click="cancelHandler()"
+ data-testid="cancel"
+ @click="cancelHandler(true)"
>
{{ __('Cancel') }}
</button>
diff --git a/app/assets/javascripts/notes/components/note_header.vue b/app/assets/javascripts/notes/components/note_header.vue
index f82b3554cac..81812ee2279 100644
--- a/app/assets/javascripts/notes/components/note_header.vue
+++ b/app/assets/javascripts/notes/components/note_header.vue
@@ -1,12 +1,17 @@
<script>
import { mapActions } from 'vuex';
+import { GlIcon, GlTooltipDirective } from '@gitlab/ui';
import timeAgoTooltip from '~/vue_shared/components/time_ago_tooltip.vue';
-import GitlabTeamMemberBadge from '~/vue_shared/components/user_avatar/badges/gitlab_team_member_badge.vue';
export default {
components: {
timeAgoTooltip,
- GitlabTeamMemberBadge,
+ GitlabTeamMemberBadge: () =>
+ import('ee_component/vue_shared/components/user_avatar/badges/gitlab_team_member_badge.vue'),
+ GlIcon,
+ },
+ directives: {
+ GlTooltip: GlTooltipDirective,
},
props: {
author: {
@@ -44,6 +49,18 @@ export default {
required: false,
default: true,
},
+ isConfidential: {
+ type: Boolean,
+ required: false,
+ default: false,
+ },
+ },
+ data() {
+ return {
+ isUsernameLinkHovered: false,
+ emojiTitle: '',
+ authorStatusHasTooltip: false,
+ };
},
computed: {
toggleChevronClass() {
@@ -55,10 +72,29 @@ export default {
hasAuthor() {
return this.author && Object.keys(this.author).length;
},
- showGitlabTeamMemberBadge() {
- return this.author?.is_gitlab_employee;
+ authorLinkClasses() {
+ return {
+ hover: this.isUsernameLinkHovered,
+ 'text-underline': this.isUsernameLinkHovered,
+ 'author-name-link': true,
+ 'js-user-link': true,
+ };
+ },
+ authorStatus() {
+ return this.author.status_tooltip_html;
+ },
+ emojiElement() {
+ return this.$refs?.authorStatus?.querySelector('gl-emoji');
},
},
+ mounted() {
+ this.emojiTitle = this.emojiElement ? this.emojiElement.getAttribute('title') : '';
+
+ const authorStatusTitle = this.$refs?.authorStatus
+ ?.querySelector('.user-status-emoji')
+ ?.getAttribute('title');
+ this.authorStatusHasTooltip = authorStatusTitle && authorStatusTitle !== '';
+ },
methods: {
...mapActions(['setTargetNoteHash']),
handleToggle() {
@@ -69,6 +105,20 @@ export default {
this.setTargetNoteHash(this.noteTimestampLink);
}
},
+ removeEmojiTitle() {
+ this.emojiElement.removeAttribute('title');
+ },
+ addEmojiTitle() {
+ this.emojiElement.setAttribute('title', this.emojiTitle);
+ },
+ handleUsernameMouseEnter() {
+ this.$refs.authorNameLink.dispatchEvent(new Event('mouseenter'));
+ this.isUsernameLinkHovered = true;
+ },
+ handleUsernameMouseLeave() {
+ this.$refs.authorNameLink.dispatchEvent(new Event('mouseleave'));
+ this.isUsernameLinkHovered = false;
+ },
},
};
</script>
@@ -87,18 +137,34 @@ export default {
</div>
<template v-if="hasAuthor">
<a
- v-once
+ ref="authorNameLink"
:href="author.path"
- class="js-user-link"
+ :class="authorLinkClasses"
:data-user-id="author.id"
:data-username="author.username"
>
<slot name="note-header-info"></slot>
<span class="note-header-author-name bold">{{ author.name }}</span>
- <span v-if="author.status_tooltip_html" v-html="author.status_tooltip_html"></span>
- <span class="note-headline-light">@{{ author.username }}</span>
</a>
- <gitlab-team-member-badge v-if="showGitlabTeamMemberBadge" />
+ <span
+ v-if="authorStatus"
+ ref="authorStatus"
+ v-on="
+ authorStatusHasTooltip ? { mouseenter: removeEmojiTitle, mouseleave: addEmojiTitle } : {}
+ "
+ v-html="authorStatus"
+ ></span>
+ <span class="text-nowrap author-username">
+ <a
+ ref="authorUsernameLink"
+ class="author-username-link"
+ :href="author.path"
+ @mouseenter="handleUsernameMouseEnter"
+ @mouseleave="handleUsernameMouseLeave"
+ ><span class="note-headline-light">@{{ author.username }}</span>
+ </a>
+ <gitlab-team-member-badge v-if="author && author.is_gitlab_employee" />
+ </span>
</template>
<span v-else>{{ __('A deleted user') }}</span>
<span class="note-headline-light note-headline-meta">
@@ -118,6 +184,15 @@ export default {
</a>
<time-ago-tooltip v-else ref="noteTimestamp" :time="createdAt" tooltip-placement="bottom" />
</template>
+ <gl-icon
+ v-if="isConfidential"
+ v-gl-tooltip:tooltipcontainer.bottom
+ data-testid="confidentialIndicator"
+ name="eye-slash"
+ :size="14"
+ :title="s__('Notes|Private comments are accessible by internal staff only')"
+ class="gl-ml-1 gl-text-gray-800 align-middle"
+ />
<slot name="extra-controls"></slot>
<i
v-if="showSpinner"
diff --git a/app/assets/javascripts/notes/components/noteable_note.vue b/app/assets/javascripts/notes/components/noteable_note.vue
index dea782683f2..37675e20b3d 100644
--- a/app/assets/javascripts/notes/components/noteable_note.vue
+++ b/app/assets/javascripts/notes/components/noteable_note.vue
@@ -255,10 +255,16 @@ export default {
</div>
<div class="timeline-content">
<div class="note-header">
- <note-header v-once :author="author" :created-at="note.created_at" :note-id="note.id">
+ <note-header
+ v-once
+ :author="author"
+ :created-at="note.created_at"
+ :note-id="note.id"
+ :is-confidential="note.confidential"
+ >
<slot slot="note-header-info" name="note-header-info"></slot>
<span v-if="commit" v-html="actionText"></span>
- <span v-else class="d-none d-sm-inline">&middot;</span>
+ <span v-else-if="note.created_at" class="d-none d-sm-inline">&middot;</span>
</note-header>
<note-actions
:author-id="author.id"
diff --git a/app/assets/javascripts/notes/components/notes_app.vue b/app/assets/javascripts/notes/components/notes_app.vue
index c1dd56aedf2..faa6006945d 100644
--- a/app/assets/javascripts/notes/components/notes_app.vue
+++ b/app/assets/javascripts/notes/components/notes_app.vue
@@ -230,10 +230,11 @@ export default {
const defaultConfig = { path: this.getNotesDataByProp('discussionsPath') };
if (doesHashExistInUrl(constants.NOTE_UNDERSCORE)) {
- return Object.assign({}, defaultConfig, {
+ return {
+ ...defaultConfig,
filter: constants.DISCUSSION_FILTERS_DEFAULT_VALUE,
persistFilter: false,
- });
+ };
}
return defaultConfig;
},
diff --git a/app/assets/javascripts/notes/index.js b/app/assets/javascripts/notes/index.js
index 8f9e2359e0d..ba814649078 100644
--- a/app/assets/javascripts/notes/index.js
+++ b/app/assets/javascripts/notes/index.js
@@ -2,11 +2,9 @@ import Vue from 'vue';
import notesApp from './components/notes_app.vue';
import initDiscussionFilters from './discussion_filters';
import initSortDiscussions from './sort_discussions';
-import createStore from './stores';
+import { store } from './stores';
document.addEventListener('DOMContentLoaded', () => {
- const store = createStore();
-
// eslint-disable-next-line no-new
new Vue({
el: '#js-vue-notes',
diff --git a/app/assets/javascripts/notes/mixins/discussion_navigation.js b/app/assets/javascripts/notes/mixins/discussion_navigation.js
index 08c7efd69a6..c9026352d18 100644
--- a/app/assets/javascripts/notes/mixins/discussion_navigation.js
+++ b/app/assets/javascripts/notes/mixins/discussion_navigation.js
@@ -1,6 +1,6 @@
import { mapGetters, mapActions, mapState } from 'vuex';
import { scrollToElement } from '~/lib/utils/common_utils';
-import eventHub from '../../notes/event_hub';
+import eventHub from '../event_hub';
/**
* @param {string} selector
diff --git a/app/assets/javascripts/notes/stores/actions.js b/app/assets/javascripts/notes/stores/actions.js
index 1b80b59621a..0999d0aa7ac 100644
--- a/app/assets/javascripts/notes/stores/actions.js
+++ b/app/assets/javascripts/notes/stores/actions.js
@@ -185,12 +185,27 @@ export const toggleResolveNote = ({ commit, dispatch }, { endpoint, isResolved,
});
};
+export const toggleBlockedIssueWarning = ({ commit }, value) => {
+ commit(types.TOGGLE_BLOCKED_ISSUE_WARNING, value);
+ // Hides Close issue button at the top of issue page
+ const closeDropdown = document.querySelector('.js-issuable-close-dropdown');
+ if (closeDropdown) {
+ closeDropdown.classList.toggle('d-none');
+ } else {
+ const closeButton = document.querySelector(
+ '.detail-page-header-actions .btn-close.btn-grouped',
+ );
+ closeButton.classList.toggle('d-md-block');
+ }
+};
+
export const closeIssue = ({ commit, dispatch, state }) => {
dispatch('toggleStateButtonLoading', true);
return axios.put(state.notesData.closePath).then(({ data }) => {
commit(types.CLOSE_ISSUE);
dispatch('emitStateChangedEvent', data);
dispatch('toggleStateButtonLoading', false);
+ dispatch('toggleBlockedIssueWarning', false);
});
};
@@ -233,7 +248,7 @@ export const saveNote = ({ commit, dispatch }, noteData) => {
const hasQuickActions = utils.hasQuickActions(placeholderText);
const replyId = noteData.data.in_reply_to_discussion_id;
let methodToDispatch;
- const postData = Object.assign({}, noteData);
+ const postData = { ...noteData };
if (postData.isDraft === true) {
methodToDispatch = replyId
? 'batchComments/addDraftToDiscussion'
diff --git a/app/assets/javascripts/notes/stores/getters.js b/app/assets/javascripts/notes/stores/getters.js
index eb877083bca..85997b44bcc 100644
--- a/app/assets/javascripts/notes/stores/getters.js
+++ b/app/assets/javascripts/notes/stores/getters.js
@@ -35,6 +35,8 @@ export const getNoteableData = state => state.noteableData;
export const getNoteableDataByProp = state => prop => state.noteableData[prop];
+export const getBlockedByIssues = state => state.noteableData.blocked_by_issues;
+
export const userCanReply = state => Boolean(state.noteableData.current_user.can_create_note);
export const openState = state => state.noteableData.state;
diff --git a/app/assets/javascripts/notes/stores/index.js b/app/assets/javascripts/notes/stores/index.js
index d41b02b4a4b..c4895f58656 100644
--- a/app/assets/javascripts/notes/stores/index.js
+++ b/app/assets/javascripts/notes/stores/index.js
@@ -4,4 +4,8 @@ import notesModule from './modules';
Vue.use(Vuex);
-export default () => new Vuex.Store(notesModule());
+// NOTE: Giving the option to either use a singleton or new instance of notes.
+const notesStore = () => new Vuex.Store(notesModule());
+
+export default notesStore;
+export const store = notesStore();
diff --git a/app/assets/javascripts/notes/stores/modules/index.js b/app/assets/javascripts/notes/stores/modules/index.js
index 81844ad6e98..25f0f546103 100644
--- a/app/assets/javascripts/notes/stores/modules/index.js
+++ b/app/assets/javascripts/notes/stores/modules/index.js
@@ -14,6 +14,7 @@ export default () => ({
// View layer
isToggleStateButtonLoading: false,
+ isToggleBlockedIssueWarning: false,
isNotesFetched: false,
isLoading: true,
isLoadingDescriptionVersion: false,
@@ -24,6 +25,7 @@ export default () => ({
},
userData: {},
noteableData: {
+ confidential: false, // TODO: Move data like this to Issue Store, should not be apart of notes.
current_user: {},
preview_note_path: 'path/to/preview',
},
diff --git a/app/assets/javascripts/notes/stores/mutation_types.js b/app/assets/javascripts/notes/stores/mutation_types.js
index 5b7225bb3d2..2f7b2788d8a 100644
--- a/app/assets/javascripts/notes/stores/mutation_types.js
+++ b/app/assets/javascripts/notes/stores/mutation_types.js
@@ -33,6 +33,7 @@ export const SET_DISCUSSIONS_SORT = 'SET_DISCUSSIONS_SORT';
export const CLOSE_ISSUE = 'CLOSE_ISSUE';
export const REOPEN_ISSUE = 'REOPEN_ISSUE';
export const TOGGLE_STATE_BUTTON_LOADING = 'TOGGLE_STATE_BUTTON_LOADING';
+export const TOGGLE_BLOCKED_ISSUE_WARNING = 'TOGGLE_BLOCKED_ISSUE_WARNING';
// Description version
export const REQUEST_DESCRIPTION_VERSION = 'REQUEST_DESCRIPTION_VERSION';
diff --git a/app/assets/javascripts/notes/stores/mutations.js b/app/assets/javascripts/notes/stores/mutations.js
index dab09d1d05c..f06874991f0 100644
--- a/app/assets/javascripts/notes/stores/mutations.js
+++ b/app/assets/javascripts/notes/stores/mutations.js
@@ -249,6 +249,10 @@ export default {
Object.assign(state, { isToggleStateButtonLoading: value });
},
+ [types.TOGGLE_BLOCKED_ISSUE_WARNING](state, value) {
+ Object.assign(state, { isToggleBlockedIssueWarning: value });
+ },
+
[types.SET_NOTES_FETCHED_STATE](state, value) {
Object.assign(state, { isNotesFetched: value });
},
diff --git a/app/assets/javascripts/pages/admin/application_settings/general/index.js b/app/assets/javascripts/pages/admin/application_settings/general/index.js
index 5ec9688a6e4..8183e81fb02 100644
--- a/app/assets/javascripts/pages/admin/application_settings/general/index.js
+++ b/app/assets/javascripts/pages/admin/application_settings/general/index.js
@@ -1,3 +1,3 @@
-import initUserInternalRegexPlaceholder from '../../application_settings/account_and_limits';
+import initUserInternalRegexPlaceholder from '../account_and_limits';
document.addEventListener('DOMContentLoaded', initUserInternalRegexPlaceholder());
diff --git a/app/assets/javascripts/pages/admin/impersonation_tokens/index.js b/app/assets/javascripts/pages/admin/impersonation_tokens/index.js
index 78a5c4c27be..ae2209b0292 100644
--- a/app/assets/javascripts/pages/admin/impersonation_tokens/index.js
+++ b/app/assets/javascripts/pages/admin/impersonation_tokens/index.js
@@ -1,3 +1,3 @@
-import DueDateSelectors from '~/due_date_select';
+import initExpiresAtField from '~/access_tokens';
-document.addEventListener('DOMContentLoaded', () => new DueDateSelectors());
+document.addEventListener('DOMContentLoaded', initExpiresAtField);
diff --git a/app/assets/javascripts/pages/admin/projects/index/components/delete_project_modal.vue b/app/assets/javascripts/pages/admin/projects/index/components/delete_project_modal.vue
index a99fde54981..b22fbf6b833 100644
--- a/app/assets/javascripts/pages/admin/projects/index/components/delete_project_modal.vue
+++ b/app/assets/javascripts/pages/admin/projects/index/components/delete_project_modal.vue
@@ -1,5 +1,5 @@
<script>
-import { escape as esc } from 'lodash';
+import { escape } from 'lodash';
import DeprecatedModal from '~/vue_shared/components/deprecated_modal.vue';
import { s__, sprintf } from '~/locale';
@@ -34,7 +34,7 @@ export default {
return sprintf(
s__('AdminProjects|Delete Project %{projectName}?'),
{
- projectName: `'${esc(this.projectName)}'`,
+ projectName: `'${escape(this.projectName)}'`,
},
false,
);
@@ -46,7 +46,7 @@ export default {
and all related resources including issues, merge requests, etc.. Once you confirm and press
%{strong_start}Delete project%{strong_end}, it cannot be undone or recovered.`),
{
- projectName: `<strong>${esc(this.projectName)}</strong>`,
+ projectName: `<strong>${escape(this.projectName)}</strong>`,
strong_start: '<strong>',
strong_end: '</strong>',
},
@@ -57,7 +57,7 @@ export default {
return sprintf(
s__('AdminUsers|To confirm, type %{projectName}'),
{
- projectName: `<code>${esc(this.projectName)}</code>`,
+ projectName: `<code>${escape(this.projectName)}</code>`,
},
false,
);
diff --git a/app/assets/javascripts/pages/admin/users/components/delete_user_modal.vue b/app/assets/javascripts/pages/admin/users/components/delete_user_modal.vue
index 5b7c8141084..71df677c7fd 100644
--- a/app/assets/javascripts/pages/admin/users/components/delete_user_modal.vue
+++ b/app/assets/javascripts/pages/admin/users/components/delete_user_modal.vue
@@ -1,5 +1,5 @@
<script>
-import { escape as esc } from 'lodash';
+import { escape } from 'lodash';
import { GlModal, GlDeprecatedButton, GlFormInput } from '@gitlab/ui';
import { s__, sprintf } from '~/locale';
@@ -56,7 +56,7 @@ export default {
return sprintf(
this.content,
{
- username: `<strong>${esc(this.username)}</strong>`,
+ username: `<strong>${escape(this.username)}</strong>`,
strong_start: '<strong>',
strong_end: '</strong>',
},
@@ -67,7 +67,7 @@ export default {
return sprintf(
s__('AdminUsers|To confirm, type %{username}'),
{
- username: `<code>${esc(this.username)}</code>`,
+ username: `<code>${escape(this.username)}</code>`,
},
false,
);
@@ -121,7 +121,7 @@ export default {
/>
</form>
</template>
- <template slot="modal-footer">
+ <template #modal-footer>
<gl-deprecated-button variant="secondary" @click="onCancel">{{
s__('Cancel')
}}</gl-deprecated-button>
diff --git a/app/assets/javascripts/pages/milestones/shared/components/delete_milestone_modal.vue b/app/assets/javascripts/pages/milestones/shared/components/delete_milestone_modal.vue
index 061044eba84..58dba41277d 100644
--- a/app/assets/javascripts/pages/milestones/shared/components/delete_milestone_modal.vue
+++ b/app/assets/javascripts/pages/milestones/shared/components/delete_milestone_modal.vue
@@ -123,7 +123,7 @@ Once deleted, it cannot be undone or recovered.`),
kind="danger"
@submit="onSubmit"
>
- <template slot="body" slot-scope="props">
+ <template #body="props">
<p v-html="props.text"></p>
</template>
</deprecated-modal>
diff --git a/app/assets/javascripts/pages/milestones/shared/components/promote_milestone_modal.vue b/app/assets/javascripts/pages/milestones/shared/components/promote_milestone_modal.vue
index 26adf4cbbe0..e18732d0fd5 100644
--- a/app/assets/javascripts/pages/milestones/shared/components/promote_milestone_modal.vue
+++ b/app/assets/javascripts/pages/milestones/shared/components/promote_milestone_modal.vue
@@ -68,7 +68,7 @@ export default {
footer-primary-button-variant="warning"
@submit="onSubmit"
>
- <template slot="title">
+ <template #title>
{{ title }}
</template>
<div>
diff --git a/app/assets/javascripts/pages/milestones/shared/event_hub.js b/app/assets/javascripts/pages/milestones/shared/event_hub.js
index 0948c2e5352..e31806ad199 100644
--- a/app/assets/javascripts/pages/milestones/shared/event_hub.js
+++ b/app/assets/javascripts/pages/milestones/shared/event_hub.js
@@ -1,3 +1,3 @@
-import Vue from 'vue';
+import createEventHub from '~/helpers/event_hub_factory';
-export default new Vue();
+export default createEventHub();
diff --git a/app/assets/javascripts/pages/profiles/personal_access_tokens/index.js b/app/assets/javascripts/pages/profiles/personal_access_tokens/index.js
index 78a5c4c27be..ae2209b0292 100644
--- a/app/assets/javascripts/pages/profiles/personal_access_tokens/index.js
+++ b/app/assets/javascripts/pages/profiles/personal_access_tokens/index.js
@@ -1,3 +1,3 @@
-import DueDateSelectors from '~/due_date_select';
+import initExpiresAtField from '~/access_tokens';
-document.addEventListener('DOMContentLoaded', () => new DueDateSelectors());
+document.addEventListener('DOMContentLoaded', initExpiresAtField);
diff --git a/app/assets/javascripts/pages/projects/alert_management/details/index.js b/app/assets/javascripts/pages/projects/alert_management/details/index.js
new file mode 100644
index 00000000000..0124795e1af
--- /dev/null
+++ b/app/assets/javascripts/pages/projects/alert_management/details/index.js
@@ -0,0 +1,5 @@
+import AlertDetails from '~/alert_management/details';
+
+document.addEventListener('DOMContentLoaded', () => {
+ AlertDetails('#js-alert_details');
+});
diff --git a/app/assets/javascripts/pages/projects/alert_management/index/index.js b/app/assets/javascripts/pages/projects/alert_management/index/index.js
new file mode 100644
index 00000000000..1e98bcfd2eb
--- /dev/null
+++ b/app/assets/javascripts/pages/projects/alert_management/index/index.js
@@ -0,0 +1,5 @@
+import AlertManagementList from '~/alert_management/list';
+
+document.addEventListener('DOMContentLoaded', () => {
+ AlertManagementList();
+});
diff --git a/app/assets/javascripts/pages/projects/blob/new/index.js b/app/assets/javascripts/pages/projects/blob/new/index.js
index 720cb249052..189053f3ed7 100644
--- a/app/assets/javascripts/pages/projects/blob/new/index.js
+++ b/app/assets/javascripts/pages/projects/blob/new/index.js
@@ -1,12 +1,3 @@
import initBlobBundle from '~/blob_edit/blob_bundle';
-import initPopover from '~/blob/suggest_gitlab_ci_yml';
-document.addEventListener('DOMContentLoaded', () => {
- initBlobBundle();
-
- const suggestEl = document.querySelector('.js-suggest-gitlab-ci-yml');
-
- if (suggestEl) {
- initPopover(suggestEl);
- }
-});
+document.addEventListener('DOMContentLoaded', initBlobBundle);
diff --git a/app/assets/javascripts/pages/projects/blob/show/index.js b/app/assets/javascripts/pages/projects/blob/show/index.js
index 557aea0c5de..e5e4670a5d7 100644
--- a/app/assets/javascripts/pages/projects/blob/show/index.js
+++ b/app/assets/javascripts/pages/projects/blob/show/index.js
@@ -32,9 +32,10 @@ document.addEventListener('DOMContentLoaded', () => {
GpgBadges.fetch();
- if (gon.features?.codeNavigation) {
- const el = document.getElementById('js-code-navigation');
- const { codeNavigationPath, blobPath, definitionPathPrefix } = el.dataset;
+ const codeNavEl = document.getElementById('js-code-navigation');
+
+ if (gon.features?.codeNavigation && codeNavEl) {
+ const { codeNavigationPath, blobPath, definitionPathPrefix } = codeNavEl.dataset;
// eslint-disable-next-line promise/catch-or-return
import('~/code_navigation').then(m =>
diff --git a/app/assets/javascripts/pages/projects/environments/metrics/index.js b/app/assets/javascripts/pages/projects/environments/metrics/index.js
index 0d69a689316..31ec4e29ad2 100644
--- a/app/assets/javascripts/pages/projects/environments/metrics/index.js
+++ b/app/assets/javascripts/pages/projects/environments/metrics/index.js
@@ -1,3 +1,3 @@
-import monitoringBundle from 'ee_else_ce/monitoring/monitoring_bundle';
+import monitoringBundle from '~/monitoring/monitoring_bundle_with_alerts';
document.addEventListener('DOMContentLoaded', monitoringBundle);
diff --git a/app/assets/javascripts/pages/projects/issues/index/index.js b/app/assets/javascripts/pages/projects/issues/index/index.js
index bf54ca972b2..e8e0cda2139 100644
--- a/app/assets/javascripts/pages/projects/issues/index/index.js
+++ b/app/assets/javascripts/pages/projects/issues/index/index.js
@@ -7,6 +7,7 @@ import UsersSelect from '~/users_select';
import initFilteredSearch from '~/pages/search/init_filtered_search';
import { FILTERED_SEARCH } from '~/pages/constants';
import { ISSUABLE_INDEX } from '~/pages/projects/constants';
+import initIssuablesList from '~/issuables_list';
import initManualOrdering from '~/manual_ordering';
document.addEventListener('DOMContentLoaded', () => {
@@ -16,9 +17,11 @@ document.addEventListener('DOMContentLoaded', () => {
page: FILTERED_SEARCH.ISSUES,
filteredSearchTokenKeys: IssuableFilteredSearchTokenKeys,
});
- new IssuableIndex(ISSUABLE_INDEX.ISSUE);
+ new IssuableIndex(ISSUABLE_INDEX.ISSUE);
new ShortcutsNavigation();
new UsersSelect();
+
initManualOrdering();
+ initIssuablesList();
});
diff --git a/app/assets/javascripts/pages/projects/issues/show.js b/app/assets/javascripts/pages/projects/issues/show.js
index 75df80a0f6c..46c9b2fe0af 100644
--- a/app/assets/javascripts/pages/projects/issues/show.js
+++ b/app/assets/javascripts/pages/projects/issues/show.js
@@ -12,6 +12,16 @@ export default function() {
initIssueableApp();
initSentryErrorStackTraceApp();
initRelatedMergeRequestsApp();
+
+ // .js-design-management is currently EE-only.
+ // This will be moved to CE as part of https://gitlab.com/gitlab-org/gitlab/-/issues/212566#frontend
+ // at which point this conditional can be removed.
+ if (document.querySelector('.js-design-management')) {
+ import(/* webpackChunkName: 'design_management' */ '~/design_management')
+ .then(module => module.default())
+ .catch(() => {});
+ }
+
new Issue(); // eslint-disable-line no-new
new ShortcutsIssuable(); // eslint-disable-line no-new
new ZenMode(); // eslint-disable-line no-new
diff --git a/app/assets/javascripts/pages/projects/labels/components/promote_label_modal.vue b/app/assets/javascripts/pages/projects/labels/components/promote_label_modal.vue
index 12e16b79d37..3b26047455d 100644
--- a/app/assets/javascripts/pages/projects/labels/components/promote_label_modal.vue
+++ b/app/assets/javascripts/pages/projects/labels/components/promote_label_modal.vue
@@ -1,5 +1,5 @@
<script>
-import { escape as esc } from 'lodash';
+import { escape } from 'lodash';
import axios from '~/lib/utils/axios_utils';
import createFlash from '~/flash';
import DeprecatedModal2 from '~/vue_shared/components/deprecated_modal_2.vue';
@@ -49,7 +49,7 @@ export default {
const label = `<span
class="label color-label"
style="background-color: ${this.labelColor}; color: ${this.labelTextColor};"
- >${esc(this.labelTitle)}</span>`;
+ >${escape(this.labelTitle)}</span>`;
return sprintf(
s__('Labels|<span>Promote label</span> %{labelTitle} <span>to Group Label?</span>'),
diff --git a/app/assets/javascripts/pages/projects/labels/event_hub.js b/app/assets/javascripts/pages/projects/labels/event_hub.js
index 0948c2e5352..e31806ad199 100644
--- a/app/assets/javascripts/pages/projects/labels/event_hub.js
+++ b/app/assets/javascripts/pages/projects/labels/event_hub.js
@@ -1,3 +1,3 @@
-import Vue from 'vue';
+import createEventHub from '~/helpers/event_hub_factory';
-export default new Vue();
+export default createEventHub();
diff --git a/app/assets/javascripts/pages/projects/pipeline_schedules/shared/components/interval_pattern_input.vue b/app/assets/javascripts/pages/projects/pipeline_schedules/shared/components/interval_pattern_input.vue
index 3a0d9c17228..4efabcb7df3 100644
--- a/app/assets/javascripts/pages/projects/pipeline_schedules/shared/components/interval_pattern_input.vue
+++ b/app/assets/javascripts/pages/projects/pipeline_schedules/shared/components/interval_pattern_input.vue
@@ -1,5 +1,13 @@
<script>
+import { GlSprintf, GlLink } from '@gitlab/ui';
+import { s__, sprintf } from '~/locale';
+import { getWeekdayNames } from '~/lib/utils/datetime_utility';
+
export default {
+ components: {
+ GlSprintf,
+ GlLink,
+ },
props: {
initialCronInterval: {
type: String,
@@ -9,25 +17,51 @@ export default {
},
data() {
return {
+ isEditingCustom: false,
+ randomHour: this.generateRandomHour(),
+ randomWeekDayIndex: this.generateRandomWeekDayIndex(),
+ randomDay: this.generateRandomDay(),
inputNameAttribute: 'schedule[cron]',
cronInterval: this.initialCronInterval,
- cronIntervalPresets: {
- everyDay: '0 4 * * *',
- everyWeek: '0 4 * * 0',
- everyMonth: '0 4 1 * *',
- },
cronSyntaxUrl: 'https://en.wikipedia.org/wiki/Cron',
- customInputEnabled: false,
};
},
computed: {
+ cronIntervalPresets() {
+ return {
+ everyDay: `0 ${this.randomHour} * * *`,
+ everyWeek: `0 ${this.randomHour} * * ${this.randomWeekDayIndex}`,
+ everyMonth: `0 ${this.randomHour} ${this.randomDay} * *`,
+ };
+ },
intervalIsPreset() {
return Object.values(this.cronIntervalPresets).includes(this.cronInterval);
},
- // The text input is editable when there's a custom interval, or when it's
- // a preset interval and the user clicks the 'custom' radio button
- isEditable() {
- return Boolean(this.customInputEnabled || !this.intervalIsPreset);
+ formattedTime() {
+ if (this.randomHour > 12) {
+ return `${this.randomHour - 12}:00pm`;
+ } else if (this.randomHour === 12) {
+ return `12:00pm`;
+ }
+ return `${this.randomHour}:00am`;
+ },
+ weekday() {
+ return getWeekdayNames()[this.randomWeekDayIndex];
+ },
+ everyDayText() {
+ return sprintf(s__(`Every day (at %{time})`), { time: this.formattedTime });
+ },
+ everyWeekText() {
+ return sprintf(s__('Every week (%{weekday} at %{time})'), {
+ weekday: this.weekday,
+ time: this.formattedTime,
+ });
+ },
+ everyMonthText() {
+ return sprintf(s__('Every month (Day %{day} at %{time})'), {
+ day: this.randomDay,
+ time: this.formattedTime,
+ });
},
},
watch: {
@@ -39,14 +73,31 @@ export default {
});
},
},
- created() {
- if (this.intervalIsPreset) {
- this.enableCustomInput = false;
+ // If at the mounting stage the default is still an empty string, we
+ // know we are not editing an existing field so we update it so
+ // that the default is the first radio option
+ mounted() {
+ if (this.cronInterval === '') {
+ this.cronInterval = this.cronIntervalPresets.everyDay;
}
},
methods: {
+ setCustomInput(e) {
+ if (!this.isEditingCustom) {
+ this.isEditingCustom = true;
+ this.$refs.customInput.click();
+ // Because we need to manually trigger the click on the radio btn,
+ // it will add a space to update the v-model. If the user is typing
+ // and the space is added, it will feel very unituitive so we reset
+ // the value to the original
+ this.cronInterval = e.target.value;
+ }
+ if (this.intervalIsPreset) {
+ this.isEditingCustom = false;
+ }
+ },
toggleCustomInput(shouldEnable) {
- this.customInputEnabled = shouldEnable;
+ this.isEditingCustom = shouldEnable;
if (shouldEnable) {
// We need to change the value so other radios don't remain selected
@@ -54,6 +105,15 @@ export default {
this.cronInterval = `${this.cronInterval} `;
}
},
+ generateRandomHour() {
+ return Math.floor(Math.random() * 23);
+ },
+ generateRandomWeekDayIndex() {
+ return Math.floor(Math.random() * 6);
+ },
+ generateRandomDay() {
+ return Math.floor(Math.random() * 28);
+ },
},
};
</script>
@@ -62,24 +122,6 @@ export default {
<div class="interval-pattern-form-group">
<div class="cron-preset-radio-input">
<input
- id="custom"
- :name="inputNameAttribute"
- :value="cronInterval"
- :checked="isEditable"
- class="label-bold"
- type="radio"
- @click="toggleCustomInput(true)"
- />
-
- <label for="custom"> {{ s__('PipelineSheduleIntervalPattern|Custom') }} </label>
-
- <span class="cron-syntax-link-wrap">
- (<a :href="cronSyntaxUrl" target="_blank"> {{ __('Cron syntax') }} </a>)
- </span>
- </div>
-
- <div class="cron-preset-radio-input">
- <input
id="every-day"
v-model="cronInterval"
:name="inputNameAttribute"
@@ -89,7 +131,9 @@ export default {
@click="toggleCustomInput(false)"
/>
- <label class="label-bold" for="every-day"> {{ __('Every day (at 4:00am)') }} </label>
+ <label class="label-bold" for="every-day">
+ {{ everyDayText }}
+ </label>
</div>
<div class="cron-preset-radio-input">
@@ -104,7 +148,7 @@ export default {
/>
<label class="label-bold" for="every-week">
- {{ __('Every week (Sundays at 4:00am)') }}
+ {{ everyWeekText }}
</label>
</div>
@@ -120,20 +164,43 @@ export default {
/>
<label class="label-bold" for="every-month">
- {{ __('Every month (on the 1st at 4:00am)') }}
+ {{ everyMonthText }}
</label>
</div>
+ <div class="cron-preset-radio-input">
+ <input
+ id="custom"
+ ref="customInput"
+ v-model="cronInterval"
+ :name="inputNameAttribute"
+ :value="cronInterval"
+ class="label-bold"
+ type="radio"
+ @click="toggleCustomInput(true)"
+ />
+
+ <label for="custom"> {{ s__('PipelineSheduleIntervalPattern|Custom') }} </label>
+
+ <gl-sprintf :message="__('(%{linkStart}Cron syntax%{linkEnd})')">
+ <template #link="{content}">
+ <gl-link :href="cronSyntaxUrl" target="_blank" class="gl-font-sm">
+ {{ content }}
+ </gl-link>
+ </template>
+ </gl-sprintf>
+ </div>
+
<div class="cron-interval-input-wrapper">
<input
id="schedule_cron"
v-model="cronInterval"
:placeholder="__('Define a custom pattern with cron syntax')"
:name="inputNameAttribute"
- :disabled="!isEditable"
class="form-control inline cron-interval-input"
type="text"
required="true"
+ @input="setCustomInput"
/>
</div>
</div>
diff --git a/app/assets/javascripts/pages/projects/pipeline_schedules/shared/components/pipeline_schedules_callout.vue b/app/assets/javascripts/pages/projects/pipeline_schedules/shared/components/pipeline_schedules_callout.vue
index 22512a6f12a..da96e6f36b4 100644
--- a/app/assets/javascripts/pages/projects/pipeline_schedules/shared/components/pipeline_schedules_callout.vue
+++ b/app/assets/javascripts/pages/projects/pipeline_schedules/shared/components/pipeline_schedules_callout.vue
@@ -2,7 +2,8 @@
import Vue from 'vue';
import Cookies from 'js-cookie';
import Translate from '../../../../../vue_shared/translate';
-import illustrationSvg from '../icons/intro_illustration.svg';
+// Full path is needed for Jest to be able to correctly mock this file
+import illustrationSvg from '~/pages/projects/pipeline_schedules/shared/icons/intro_illustration.svg';
import { parseBoolean } from '~/lib/utils/common_utils';
Vue.use(Translate);
diff --git a/app/assets/javascripts/pages/projects/pipeline_schedules/shared/init_form.js b/app/assets/javascripts/pages/projects/pipeline_schedules/shared/init_form.js
index dc6df27f1c7..497e2c9c0ae 100644
--- a/app/assets/javascripts/pages/projects/pipeline_schedules/shared/init_form.js
+++ b/app/assets/javascripts/pages/projects/pipeline_schedules/shared/init_form.js
@@ -11,9 +11,7 @@ Vue.use(Translate);
function initIntervalPatternInput() {
const intervalPatternMount = document.getElementById('interval-pattern-input');
- const initialCronInterval = intervalPatternMount
- ? intervalPatternMount.dataset.initialInterval
- : '';
+ const initialCronInterval = intervalPatternMount?.dataset?.initialInterval;
return new Vue({
el: intervalPatternMount,
diff --git a/app/assets/javascripts/pages/projects/pipelines/dag/index.js b/app/assets/javascripts/pages/projects/pipelines/dag/index.js
new file mode 100644
index 00000000000..d19c22ba556
--- /dev/null
+++ b/app/assets/javascripts/pages/projects/pipelines/dag/index.js
@@ -0,0 +1,2 @@
+// /dag is an alias for show
+import '../show/index';
diff --git a/app/assets/javascripts/pages/projects/pipelines/index/index.js b/app/assets/javascripts/pages/projects/pipelines/index/index.js
index 4b4a274794d..bbad3238ec4 100644
--- a/app/assets/javascripts/pages/projects/pipelines/index/index.js
+++ b/app/assets/javascripts/pages/projects/pipelines/index/index.js
@@ -50,6 +50,7 @@ document.addEventListener(
hasGitlabCi: parseBoolean(this.dataset.hasGitlabCi),
ciLintPath: this.dataset.ciLintPath,
resetCachePath: this.dataset.resetCachePath,
+ projectId: this.dataset.projectId,
},
});
},
diff --git a/app/assets/javascripts/pages/projects/settings/access_tokens/index.js b/app/assets/javascripts/pages/projects/settings/access_tokens/index.js
new file mode 100644
index 00000000000..ae2209b0292
--- /dev/null
+++ b/app/assets/javascripts/pages/projects/settings/access_tokens/index.js
@@ -0,0 +1,3 @@
+import initExpiresAtField from '~/access_tokens';
+
+document.addEventListener('DOMContentLoaded', initExpiresAtField);
diff --git a/app/assets/javascripts/pages/projects/shared/permissions/components/settings_panel.vue b/app/assets/javascripts/pages/projects/shared/permissions/components/settings_panel.vue
index 6efddec1172..ab32fe18972 100644
--- a/app/assets/javascripts/pages/projects/shared/permissions/components/settings_panel.vue
+++ b/app/assets/javascripts/pages/projects/shared/permissions/components/settings_panel.vue
@@ -1,9 +1,8 @@
<script>
-import { GlSprintf, GlLink } from '@gitlab/ui';
+import { GlSprintf, GlLink, GlFormCheckbox } from '@gitlab/ui';
import settingsMixin from 'ee_else_ce/pages/projects/shared/permissions/mixins/settings_pannel_mixin';
import { s__ } from '~/locale';
-import glFeatureFlagsMixin from '~/vue_shared/mixins/gl_feature_flags_mixin';
import projectFeatureSetting from './project_feature_setting.vue';
import projectFeatureToggle from '~/vue_shared/components/toggle_button.vue';
import projectSettingRow from './project_setting_row.vue';
@@ -12,6 +11,7 @@ import {
visibilityLevelDescriptions,
featureAccessLevelMembers,
featureAccessLevelEveryone,
+ featureAccessLevel,
} from '../constants';
import { toggleHiddenClassBySelector } from '../external';
@@ -24,8 +24,9 @@ export default {
projectSettingRow,
GlSprintf,
GlLink,
+ GlFormCheckbox,
},
- mixins: [settingsMixin, glFeatureFlagsMixin()],
+ mixins: [settingsMixin],
props: {
currentSettings: {
@@ -127,7 +128,7 @@ export default {
wikiAccessLevel: 20,
snippetsAccessLevel: 20,
pagesAccessLevel: 20,
- metricsAccessLevel: visibilityOptions.PRIVATE,
+ metricsDashboardAccessLevel: featureAccessLevel.PROJECT_MEMBERS,
containerRegistryEnabled: true,
lfsEnabled: true,
requestAccessEnabled: true,
@@ -174,6 +175,10 @@ export default {
return options;
},
+ metricsOptionsDropdownEnabled() {
+ return this.featureAccessLevelOptions.length < 2;
+ },
+
repositoryEnabled() {
return this.repositoryAccessLevel > 0;
},
@@ -195,10 +200,6 @@ export default {
'ProjectSettings|View and edit files in this project. Non-project members will only have read access',
);
},
-
- metricsDashboardVisibilitySwitchingAvailable() {
- return this.glFeatures.metricsDashboardVisibilitySwitchingAvailable;
- },
},
watch: {
@@ -211,6 +212,7 @@ export default {
this.buildsAccessLevel = Math.min(10, this.buildsAccessLevel);
this.wikiAccessLevel = Math.min(10, this.wikiAccessLevel);
this.snippetsAccessLevel = Math.min(10, this.snippetsAccessLevel);
+ this.metricsDashboardAccessLevel = Math.min(10, this.metricsDashboardAccessLevel);
if (this.pagesAccessLevel === 20) {
// When from Internal->Private narrow access for only members
this.pagesAccessLevel = 10;
@@ -225,6 +227,7 @@ export default {
if (this.wikiAccessLevel > 0) this.wikiAccessLevel = 20;
if (this.snippetsAccessLevel > 0) this.snippetsAccessLevel = 20;
if (this.pagesAccessLevel === 10) this.pagesAccessLevel = 20;
+ if (this.metricsDashboardAccessLevel === 10) this.metricsDashboardAccessLevel = 20;
this.highlightChanges();
}
},
@@ -473,7 +476,6 @@ export default {
/>
</project-setting-row>
<project-setting-row
- v-if="metricsDashboardVisibilitySwitchingAvailable"
ref="metrics-visibility-settings"
:label="__('Metrics Dashboard')"
:help-text="
@@ -485,17 +487,18 @@ export default {
<div class="project-feature-controls">
<div class="select-wrapper">
<select
- v-model="metricsAccessLevel"
+ v-model="metricsDashboardAccessLevel"
+ :disabled="metricsOptionsDropdownEnabled"
name="project[project_feature_attributes][metrics_dashboard_access_level]"
- class="form-control select-control"
+ class="form-control project-repo-select select-control"
>
<option
- :value="visibilityOptions.PRIVATE"
- :disabled="!visibilityAllowed(visibilityOptions.PRIVATE)"
+ :value="featureAccessLevelMembers[0]"
+ :disabled="!visibilityAllowed(visibilityOptions.INTERNAL)"
>{{ featureAccessLevelMembers[1] }}</option
>
<option
- :value="visibilityOptions.PUBLIC"
+ :value="featureAccessLevelEveryone[0]"
:disabled="!visibilityAllowed(visibilityOptions.PUBLIC)"
>{{ featureAccessLevelEveryone[1] }}</option
>
@@ -517,5 +520,23 @@ export default {
)
}}</span>
</project-setting-row>
+ <project-setting-row class="mb-3">
+ <input
+ :value="showDefaultAwardEmojis"
+ type="hidden"
+ name="project[project_setting_attributes][show_default_award_emojis]"
+ />
+ <gl-form-checkbox
+ v-model="showDefaultAwardEmojis"
+ name="project[project_setting_attributes][show_default_award_emojis]"
+ >
+ {{ s__('ProjectSettings|Show default award emojis') }}
+ <template #help>{{
+ s__(
+ 'ProjectSettings|When enabled, issues, merge requests, and snippets will always show thumbs-up and thumbs-down award emoji buttons.',
+ )
+ }}</template>
+ </gl-form-checkbox>
+ </project-setting-row>
</div>
</template>
diff --git a/app/assets/javascripts/pages/projects/wikis/components/delete_wiki_modal.vue b/app/assets/javascripts/pages/projects/wikis/components/delete_wiki_modal.vue
index 6af346ace67..580cca49b5e 100644
--- a/app/assets/javascripts/pages/projects/wikis/components/delete_wiki_modal.vue
+++ b/app/assets/javascripts/pages/projects/wikis/components/delete_wiki_modal.vue
@@ -1,5 +1,5 @@
<script>
-import { escape as esc } from 'lodash';
+import { escape } from 'lodash';
import { GlModal, GlModalDirective } from '@gitlab/ui';
import { s__, sprintf } from '~/locale';
@@ -38,7 +38,7 @@ export default {
return sprintf(
s__('WikiPageConfirmDelete|Delete page %{pageTitle}?'),
{
- pageTitle: esc(this.pageTitle),
+ pageTitle: escape(this.pageTitle),
},
false,
);
@@ -46,6 +46,7 @@ export default {
},
methods: {
onSubmit() {
+ window.onbeforeunload = null;
this.$refs.form.submit();
},
},
diff --git a/app/assets/javascripts/pages/projects/wikis/wikis.js b/app/assets/javascripts/pages/projects/wikis/wikis.js
index 93afdc54ce1..ed67219383b 100644
--- a/app/assets/javascripts/pages/projects/wikis/wikis.js
+++ b/app/assets/javascripts/pages/projects/wikis/wikis.js
@@ -44,6 +44,19 @@ export default class Wikis {
linkExample.innerHTML = MARKDOWN_LINK_TEXT[e.target.value];
});
}
+
+ const wikiTextarea = document.querySelector('form.wiki-form #wiki_content');
+ const wikiForm = document.querySelector('form.wiki-form');
+
+ if (wikiTextarea) {
+ wikiTextarea.addEventListener('input', () => {
+ window.onbeforeunload = () => '';
+ });
+
+ wikiForm.addEventListener('submit', () => {
+ window.onbeforeunload = null;
+ });
+ }
}
handleWikiTitleChange(e) {
diff --git a/app/assets/javascripts/pages/search/init_filtered_search.js b/app/assets/javascripts/pages/search/init_filtered_search.js
index 7fdf4ee0bf3..e54e32199f0 100644
--- a/app/assets/javascripts/pages/search/init_filtered_search.js
+++ b/app/assets/javascripts/pages/search/init_filtered_search.js
@@ -1,4 +1,4 @@
-import FilteredSearchManager from '~/filtered_search/filtered_search_manager';
+import FilteredSearchManager from 'ee_else_ce/filtered_search/filtered_search_manager';
export default ({
page,
diff --git a/app/assets/javascripts/performance_bar/components/detailed_metric.vue b/app/assets/javascripts/performance_bar/components/detailed_metric.vue
index 24ae900b445..e1a0e2df0e0 100644
--- a/app/assets/javascripts/performance_bar/components/detailed_metric.vue
+++ b/app/assets/javascripts/performance_bar/components/detailed_metric.vue
@@ -116,7 +116,9 @@ export default {
</template>
</table>
- <div slot="footer"></div>
+ <template #footer>
+ <div></div>
+ </template>
</gl-modal>
{{ title }}
<request-warning :html-id="htmlId" :warnings="warnings" />
diff --git a/app/assets/javascripts/persistent_user_callout.js b/app/assets/javascripts/persistent_user_callout.js
index 4598626718c..b3068c46bcb 100644
--- a/app/assets/javascripts/persistent_user_callout.js
+++ b/app/assets/javascripts/persistent_user_callout.js
@@ -18,6 +18,11 @@ export default class PersistentUserCallout {
init() {
const closeButton = this.container.querySelector('.js-close');
+
+ if (!closeButton) {
+ return;
+ }
+
closeButton.addEventListener('click', event => this.dismiss(event));
if (this.deferLinks) {
diff --git a/app/assets/javascripts/pipelines/components/graph/job_group_dropdown.vue b/app/assets/javascripts/pipelines/components/graph/job_group_dropdown.vue
index ebd7a17040a..15c220a554d 100644
--- a/app/assets/javascripts/pipelines/components/graph/job_group_dropdown.vue
+++ b/app/assets/javascripts/pipelines/components/graph/job_group_dropdown.vue
@@ -69,7 +69,9 @@ export default {
>
<ci-icon :status="group.status" />
- <span class="ci-status-text text-truncate mw-70p gl-pl-1 d-inline-block align-bottom">
+ <span
+ class="ci-status-text text-truncate mw-70p gl-pl-1-deprecated-no-really-do-not-use-me d-inline-block align-bottom"
+ >
{{ group.name }}
</span>
diff --git a/app/assets/javascripts/pipelines/components/graph/job_name_component.vue b/app/assets/javascripts/pipelines/components/graph/job_name_component.vue
index 7125790ac3d..74a261f35d7 100644
--- a/app/assets/javascripts/pipelines/components/graph/job_name_component.vue
+++ b/app/assets/javascripts/pipelines/components/graph/job_name_component.vue
@@ -27,7 +27,9 @@ export default {
<template>
<span class="ci-job-name-component mw-100">
<ci-icon :status="status" />
- <span class="ci-status-text text-truncate mw-70p gl-pl-1 d-inline-block align-bottom">
+ <span
+ class="ci-status-text text-truncate mw-70p gl-pl-1-deprecated-no-really-do-not-use-me d-inline-block align-bottom"
+ >
{{ name }}
</span>
</span>
diff --git a/app/assets/javascripts/pipelines/components/graph/stage_column_component.vue b/app/assets/javascripts/pipelines/components/graph/stage_column_component.vue
index 3d3dabbdf22..bed0ed51d5f 100644
--- a/app/assets/javascripts/pipelines/components/graph/stage_column_component.vue
+++ b/app/assets/javascripts/pipelines/components/graph/stage_column_component.vue
@@ -1,5 +1,5 @@
<script>
-import { isEmpty, escape as esc } from 'lodash';
+import { isEmpty, escape } from 'lodash';
import stageColumnMixin from '../../mixins/stage_column_mixin';
import JobItem from './job_item.vue';
import JobGroupDropdown from './job_group_dropdown.vue';
@@ -44,7 +44,7 @@ export default {
},
methods: {
groupId(group) {
- return `ci-badge-${esc(group.name)}`;
+ return `ci-badge-${escape(group.name)}`;
},
pipelineActionRequestComplete() {
this.$emit('refreshPipelineGraph');
diff --git a/app/assets/javascripts/pipelines/components/pipelines.vue b/app/assets/javascripts/pipelines/components/pipelines.vue
index d4f23697e09..fc93635bdb5 100644
--- a/app/assets/javascripts/pipelines/components/pipelines.vue
+++ b/app/assets/javascripts/pipelines/components/pipelines.vue
@@ -9,14 +9,18 @@ import NavigationTabs from '../../vue_shared/components/navigation_tabs.vue';
import NavigationControls from './nav_controls.vue';
import { getParameterByName } from '../../lib/utils/common_utils';
import CIPaginationMixin from '../../vue_shared/mixins/ci_pagination_api_mixin';
+import PipelinesFilteredSearch from './pipelines_filtered_search.vue';
+import { ANY_TRIGGER_AUTHOR, RAW_TEXT_WARNING } from '../constants';
+import glFeatureFlagsMixin from '~/vue_shared/mixins/gl_feature_flags_mixin';
export default {
components: {
TablePagination,
NavigationTabs,
NavigationControls,
+ PipelinesFilteredSearch,
},
- mixins: [pipelinesMixin, CIPaginationMixin],
+ mixins: [pipelinesMixin, CIPaginationMixin, glFeatureFlagsMixin()],
props: {
store: {
type: Object,
@@ -78,6 +82,10 @@ export default {
required: false,
default: null,
},
+ projectId: {
+ type: String,
+ required: true,
+ },
},
data() {
return {
@@ -209,6 +217,9 @@ export default {
},
];
},
+ canFilterPipelines() {
+ return this.glFeatures.filterPipelinesSearch;
+ },
},
created() {
this.service = new PipelinesService(this.endpoint);
@@ -238,6 +249,30 @@ export default {
createFlash(s__('Pipelines|Something went wrong while cleaning runners cache.'));
});
},
+ resetRequestData() {
+ this.requestData = { page: this.page, scope: this.scope };
+ },
+ filterPipelines(filters) {
+ this.resetRequestData();
+
+ filters.forEach(filter => {
+ // do not add Any for username query param, so we
+ // can fetch all trigger authors
+ if (filter.type && filter.value.data !== ANY_TRIGGER_AUTHOR) {
+ this.requestData[filter.type] = filter.value.data;
+ }
+
+ if (!filter.type) {
+ createFlash(RAW_TEXT_WARNING, 'warning');
+ }
+ });
+
+ if (filters.length === 0) {
+ this.resetRequestData();
+ }
+
+ this.updateContent(this.requestData);
+ },
},
};
</script>
@@ -267,6 +302,13 @@ export default {
/>
</div>
+ <pipelines-filtered-search
+ v-if="canFilterPipelines"
+ :pipelines="state.pipelines"
+ :project-id="projectId"
+ @filterPipelines="filterPipelines"
+ />
+
<div class="content-list pipelines">
<gl-loading-icon
v-if="stateToRender === $options.stateMap.loading"
diff --git a/app/assets/javascripts/pipelines/components/pipelines_filtered_search.vue b/app/assets/javascripts/pipelines/components/pipelines_filtered_search.vue
new file mode 100644
index 00000000000..8f9c3eb70a2
--- /dev/null
+++ b/app/assets/javascripts/pipelines/components/pipelines_filtered_search.vue
@@ -0,0 +1,91 @@
+<script>
+import { GlFilteredSearch } from '@gitlab/ui';
+import { __, s__ } from '~/locale';
+import PipelineTriggerAuthorToken from './tokens/pipeline_trigger_author_token.vue';
+import PipelineBranchNameToken from './tokens/pipeline_branch_name_token.vue';
+import Api from '~/api';
+import createFlash from '~/flash';
+import { FETCH_AUTHOR_ERROR_MESSAGE, FETCH_BRANCH_ERROR_MESSAGE } from '../constants';
+
+export default {
+ components: {
+ GlFilteredSearch,
+ },
+ props: {
+ pipelines: {
+ type: Array,
+ required: true,
+ },
+ projectId: {
+ type: String,
+ required: true,
+ },
+ },
+ data() {
+ return {
+ projectUsers: null,
+ projectBranches: null,
+ };
+ },
+ computed: {
+ tokens() {
+ return [
+ {
+ type: 'username',
+ icon: 'user',
+ title: s__('Pipeline|Trigger author'),
+ unique: true,
+ token: PipelineTriggerAuthorToken,
+ operators: [{ value: '=', description: __('is'), default: 'true' }],
+ triggerAuthors: this.projectUsers,
+ projectId: this.projectId,
+ },
+ {
+ type: 'ref',
+ icon: 'branch',
+ title: s__('Pipeline|Branch name'),
+ unique: true,
+ token: PipelineBranchNameToken,
+ operators: [{ value: '=', description: __('is'), default: 'true' }],
+ branches: this.projectBranches,
+ projectId: this.projectId,
+ },
+ ];
+ },
+ },
+ created() {
+ Api.projectUsers(this.projectId)
+ .then(users => {
+ this.projectUsers = users;
+ })
+ .catch(err => {
+ createFlash(FETCH_AUTHOR_ERROR_MESSAGE);
+ throw err;
+ });
+
+ Api.branches(this.projectId)
+ .then(({ data }) => {
+ this.projectBranches = data.map(branch => branch.name);
+ })
+ .catch(err => {
+ createFlash(FETCH_BRANCH_ERROR_MESSAGE);
+ throw err;
+ });
+ },
+ methods: {
+ onSubmit(filters) {
+ this.$emit('filterPipelines', filters);
+ },
+ },
+};
+</script>
+
+<template>
+ <div class="row-content-block">
+ <gl-filtered-search
+ :placeholder="__('Filter pipelines')"
+ :available-tokens="tokens"
+ @submit="onSubmit"
+ />
+ </div>
+</template>
diff --git a/app/assets/javascripts/pipelines/components/pipelines_table_row.vue b/app/assets/javascripts/pipelines/components/pipelines_table_row.vue
index e25f8ab4790..981914dd046 100644
--- a/app/assets/javascripts/pipelines/components/pipelines_table_row.vue
+++ b/app/assets/javascripts/pipelines/components/pipelines_table_row.vue
@@ -99,9 +99,10 @@ export default {
// 3. If GitLab user does not have avatar, they might have a Gravatar
} else if (this.pipeline.commit.author_gravatar_url) {
- commitAuthorInformation = Object.assign({}, this.pipeline.commit.author, {
+ commitAuthorInformation = {
+ ...this.pipeline.commit.author,
avatar_url: this.pipeline.commit.author_gravatar_url,
- });
+ };
}
// 4. If committer is not a GitLab User, they can have a Gravatar
} else {
diff --git a/app/assets/javascripts/pipelines/components/stage.vue b/app/assets/javascripts/pipelines/components/stage.vue
index 7426936515a..569920a4f31 100644
--- a/app/assets/javascripts/pipelines/components/stage.vue
+++ b/app/assets/javascripts/pipelines/components/stage.vue
@@ -137,7 +137,7 @@ export default {
},
isDropdownOpen() {
- return this.$el.classList.contains('open');
+ return this.$el.classList.contains('show');
},
pipelineActionRequestComplete() {
diff --git a/app/assets/javascripts/pipelines/components/test_reports/test_reports.vue b/app/assets/javascripts/pipelines/components/test_reports/test_reports.vue
index 388b300b39d..06ab45adf80 100644
--- a/app/assets/javascripts/pipelines/components/test_reports/test_reports.vue
+++ b/app/assets/javascripts/pipelines/components/test_reports/test_reports.vue
@@ -21,7 +21,8 @@ export default {
return this.selectedSuite.total_count > 0;
},
showTests() {
- return this.testReports.total_count > 0;
+ const { test_suites: testSuites = [] } = this.testReports;
+ return testSuites.length > 0;
},
},
methods: {
diff --git a/app/assets/javascripts/pipelines/components/test_reports/test_summary.vue b/app/assets/javascripts/pipelines/components/test_reports/test_summary.vue
index 9739ef76867..80a1c83f171 100644
--- a/app/assets/javascripts/pipelines/components/test_reports/test_summary.vue
+++ b/app/assets/javascripts/pipelines/components/test_reports/test_summary.vue
@@ -29,7 +29,14 @@ export default {
successPercentage() {
// Returns a full number when the decimals equal .00.
// Otherwise returns a float to two decimal points
- return Number(((this.report.success_count / this.report.total_count) * 100 || 0).toFixed(2));
+ // Do not include skipped tests as part of the total when doing success calculations.
+
+ const totalCompletedCount = this.report.total_count - this.report.skipped_count;
+
+ if (totalCompletedCount > 0) {
+ return Number(((this.report.success_count / totalCompletedCount) * 100 || 0).toFixed(2));
+ }
+ return 0;
},
formattedDuration() {
return formatTime(secondsToMilliseconds(this.report.total_time));
diff --git a/app/assets/javascripts/pipelines/components/test_reports/test_summary_table.vue b/app/assets/javascripts/pipelines/components/test_reports/test_summary_table.vue
index 6effd6e949d..4dfb67dd8e8 100644
--- a/app/assets/javascripts/pipelines/components/test_reports/test_summary_table.vue
+++ b/app/assets/javascripts/pipelines/components/test_reports/test_summary_table.vue
@@ -1,14 +1,19 @@
<script>
import { mapGetters } from 'vuex';
import { s__ } from '~/locale';
+import { GlIcon, GlTooltipDirective } from '@gitlab/ui';
import store from '~/pipelines/stores/test_reports';
import SmartVirtualList from '~/vue_shared/components/smart_virtual_list.vue';
export default {
name: 'TestsSummaryTable',
components: {
+ GlIcon,
SmartVirtualList,
},
+ directives: {
+ GlTooltip: GlTooltipDirective,
+ },
store,
props: {
heading: {
@@ -75,7 +80,10 @@ export default {
v-for="(testSuite, index) in getTestSuites"
:key="index"
role="row"
- class="gl-responsive-table-row gl-responsive-table-row-clickable test-reports-summary-row rounded cursor-pointer js-suite-row"
+ class="gl-responsive-table-row test-reports-summary-row rounded js-suite-row"
+ :class="{
+ 'gl-responsive-table-row-clickable cursor-pointer': !testSuite.suite_error,
+ }"
@click="tableRowClick(testSuite)"
>
<div class="table-section section-25">
@@ -84,6 +92,14 @@ export default {
</div>
<div class="table-mobile-content underline cgray pl-3">
{{ testSuite.name }}
+ <gl-icon
+ v-if="testSuite.suite_error"
+ ref="suiteErrorIcon"
+ v-gl-tooltip
+ name="error"
+ :title="testSuite.suite_error"
+ class="vertical-align-middle"
+ />
</div>
</div>
diff --git a/app/assets/javascripts/pipelines/components/tokens/pipeline_branch_name_token.vue b/app/assets/javascripts/pipelines/components/tokens/pipeline_branch_name_token.vue
new file mode 100644
index 00000000000..a7a3f986255
--- /dev/null
+++ b/app/assets/javascripts/pipelines/components/tokens/pipeline_branch_name_token.vue
@@ -0,0 +1,70 @@
+<script>
+import { GlFilteredSearchToken, GlFilteredSearchSuggestion, GlLoadingIcon } from '@gitlab/ui';
+import Api from '~/api';
+import { FETCH_BRANCH_ERROR_MESSAGE, FILTER_PIPELINES_SEARCH_DELAY } from '../../constants';
+import createFlash from '~/flash';
+import { debounce } from 'lodash';
+
+export default {
+ components: {
+ GlFilteredSearchToken,
+ GlFilteredSearchSuggestion,
+ GlLoadingIcon,
+ },
+ props: {
+ config: {
+ type: Object,
+ required: true,
+ },
+ value: {
+ type: Object,
+ required: true,
+ },
+ },
+ data() {
+ return {
+ branches: this.config.branches,
+ loading: true,
+ };
+ },
+ methods: {
+ fetchBranchBySearchTerm(searchTerm) {
+ Api.branches(this.config.projectId, searchTerm)
+ .then(res => {
+ this.branches = res.data.map(branch => branch.name);
+ this.loading = false;
+ })
+ .catch(err => {
+ createFlash(FETCH_BRANCH_ERROR_MESSAGE);
+ this.loading = false;
+ throw err;
+ });
+ },
+ searchBranches: debounce(function debounceSearch({ data }) {
+ this.fetchBranchBySearchTerm(data);
+ }, FILTER_PIPELINES_SEARCH_DELAY),
+ },
+};
+</script>
+
+<template>
+ <gl-filtered-search-token
+ :config="config"
+ v-bind="{ ...$props, ...$attrs }"
+ v-on="$listeners"
+ @input="searchBranches"
+ >
+ <template #suggestions>
+ <gl-loading-icon v-if="loading" />
+ <template v-else>
+ <gl-filtered-search-suggestion
+ v-for="(branch, index) in branches"
+ :key="index"
+ :value="branch"
+ >
+ {{ branch }}
+ </gl-filtered-search-suggestion>
+ </template>
+ </template>
+ </gl-filtered-search-token>
+</template>
diff --git a/app/assets/javascripts/pipelines/components/tokens/pipeline_trigger_author_token.vue b/app/assets/javascripts/pipelines/components/tokens/pipeline_trigger_author_token.vue
new file mode 100644
index 00000000000..83e3558e1a1
--- /dev/null
+++ b/app/assets/javascripts/pipelines/components/tokens/pipeline_trigger_author_token.vue
@@ -0,0 +1,114 @@
+<script>
+import {
+ GlFilteredSearchToken,
+ GlAvatar,
+ GlFilteredSearchSuggestion,
+ GlDropdownDivider,
+ GlLoadingIcon,
+} from '@gitlab/ui';
+import Api from '~/api';
+import createFlash from '~/flash';
+import { debounce } from 'lodash';
+import {
+ ANY_TRIGGER_AUTHOR,
+ FETCH_AUTHOR_ERROR_MESSAGE,
+ FILTER_PIPELINES_SEARCH_DELAY,
+} from '../../constants';
+
+export default {
+ anyTriggerAuthor: ANY_TRIGGER_AUTHOR,
+ components: {
+ GlFilteredSearchToken,
+ GlAvatar,
+ GlFilteredSearchSuggestion,
+ GlDropdownDivider,
+ GlLoadingIcon,
+ },
+ props: {
+ config: {
+ type: Object,
+ required: true,
+ },
+ value: {
+ type: Object,
+ required: true,
+ },
+ },
+ data() {
+ return {
+ users: this.config.triggerAuthors,
+ loading: true,
+ };
+ },
+ computed: {
+ currentValue() {
+ return this.value.data.toLowerCase();
+ },
+ activeUser() {
+ return this.users.find(user => {
+ return user.username.toLowerCase() === this.currentValue;
+ });
+ },
+ },
+ methods: {
+ fetchAuthorBySearchTerm(searchTerm) {
+ Api.projectUsers(this.config.projectId, searchTerm)
+ .then(res => {
+ this.users = res;
+ this.loading = false;
+ })
+ .catch(err => {
+ createFlash(FETCH_AUTHOR_ERROR_MESSAGE);
+ this.loading = false;
+ throw err;
+ });
+ },
+ searchAuthors: debounce(function debounceSearch({ data }) {
+ this.fetchAuthorBySearchTerm(data);
+ }, FILTER_PIPELINES_SEARCH_DELAY),
+ },
+};
+</script>
+
+<template>
+ <gl-filtered-search-token
+ :config="config"
+ v-bind="{ ...$props, ...$attrs }"
+ v-on="$listeners"
+ @input="searchAuthors"
+ >
+ <template #view="{inputValue}">
+ <gl-avatar
+ v-if="activeUser"
+ :size="16"
+ :src="activeUser.avatar_url"
+ shape="circle"
+ class="gl-mr-2"
+ />
+ <span>{{ activeUser ? activeUser.name : inputValue }}</span>
+ </template>
+ <template #suggestions>
+ <gl-filtered-search-suggestion :value="$options.anyTriggerAuthor">{{
+ $options.anyTriggerAuthor
+ }}</gl-filtered-search-suggestion>
+ <gl-dropdown-divider />
+
+ <gl-loading-icon v-if="loading" />
+ <template v-else>
+ <gl-filtered-search-suggestion
+ v-for="user in users"
+ :key="user.username"
+ :value="user.username"
+ >
+ <div class="d-flex">
+ <gl-avatar :size="32" :src="user.avatar_url" />
+ <div>
+ <div>{{ user.name }}</div>
+ <div>@{{ user.username }}</div>
+ </div>
+ </div>
+ </gl-filtered-search-suggestion>
+ </template>
+ </template>
+ </gl-filtered-search-token>
+</template>
diff --git a/app/assets/javascripts/pipelines/constants.js b/app/assets/javascripts/pipelines/constants.js
index c9655d18a04..d694430830b 100644
--- a/app/assets/javascripts/pipelines/constants.js
+++ b/app/assets/javascripts/pipelines/constants.js
@@ -1,9 +1,19 @@
+import { s__, __ } from '~/locale';
+
export const CANCEL_REQUEST = 'CANCEL_REQUEST';
export const PIPELINES_TABLE = 'PIPELINES_TABLE';
export const LAYOUT_CHANGE_DELAY = 300;
+export const FILTER_PIPELINES_SEARCH_DELAY = 200;
+export const ANY_TRIGGER_AUTHOR = 'Any';
export const TestStatus = {
FAILED: 'failed',
SKIPPED: 'skipped',
SUCCESS: 'success',
};
+
+export const FETCH_AUTHOR_ERROR_MESSAGE = __('There was a problem fetching project users.');
+export const FETCH_BRANCH_ERROR_MESSAGE = __('There was a problem fetching project branches.');
+export const RAW_TEXT_WARNING = s__(
+ 'Pipeline|Raw text search is not currently supported. Please use the available search tokens.',
+);
diff --git a/app/assets/javascripts/pipelines/pipeline_details_bundle.js b/app/assets/javascripts/pipelines/pipeline_details_bundle.js
index d76425c96b7..01295874e56 100644
--- a/app/assets/javascripts/pipelines/pipeline_details_bundle.js
+++ b/app/assets/javascripts/pipelines/pipeline_details_bundle.js
@@ -14,13 +14,7 @@ import axios from '~/lib/utils/axios_utils';
Vue.use(Translate);
-export default () => {
- const { dataset } = document.querySelector('.js-pipeline-details-vue');
-
- const mediator = new PipelinesMediator({ endpoint: dataset.endpoint });
-
- mediator.fetchPipeline();
-
+const createPipelinesDetailApp = mediator => {
// eslint-disable-next-line no-new
new Vue({
el: '#js-pipeline-graph-vue',
@@ -50,7 +44,9 @@ export default () => {
});
},
});
+};
+const createPipelineHeaderApp = mediator => {
// eslint-disable-next-line no-new
new Vue({
el: '#js-pipeline-header-vue',
@@ -94,7 +90,9 @@ export default () => {
});
},
});
+};
+const createPipelinesTabs = dataset => {
const tabsElement = document.querySelector('.pipelines-tabs');
const testReportsEnabled =
window.gon && window.gon.features && window.gon.features.junitPipelineView;
@@ -119,27 +117,40 @@ export default () => {
tabsElement.addEventListener('click', tabClickHandler);
}
+ }
+};
- // eslint-disable-next-line no-new
- new Vue({
- el: '#js-pipeline-tests-detail',
- components: {
- TestReports,
- },
- render(createElement) {
- return createElement('test-reports');
- },
- });
+const createTestDetails = detailsEndpoint => {
+ // eslint-disable-next-line no-new
+ new Vue({
+ el: '#js-pipeline-tests-detail',
+ components: {
+ TestReports,
+ },
+ render(createElement) {
+ return createElement('test-reports');
+ },
+ });
- axios
- .get(dataset.testReportsCountEndpoint)
- .then(({ data }) => {
- if (!data.total_count) {
- return;
- }
+ axios
+ .get(detailsEndpoint)
+ .then(({ data }) => {
+ if (!data.total_count) {
+ return;
+ }
- document.querySelector('.js-test-report-badge-counter').innerHTML = data.total_count;
- })
- .catch(() => {});
- }
+ document.querySelector('.js-test-report-badge-counter').innerHTML = data.total_count;
+ })
+ .catch(() => {});
+};
+
+export default () => {
+ const { dataset } = document.querySelector('.js-pipeline-details-vue');
+ const mediator = new PipelinesMediator({ endpoint: dataset.endpoint });
+ mediator.fetchPipeline();
+
+ createPipelinesDetailApp(mediator);
+ createPipelineHeaderApp(mediator);
+ createPipelinesTabs(dataset);
+ createTestDetails(dataset.testReportsCountEndpoint);
};
diff --git a/app/assets/javascripts/pipelines/services/pipelines_service.js b/app/assets/javascripts/pipelines/services/pipelines_service.js
index 3c755db23dc..ae94d7a7ca0 100644
--- a/app/assets/javascripts/pipelines/services/pipelines_service.js
+++ b/app/assets/javascripts/pipelines/services/pipelines_service.js
@@ -19,13 +19,23 @@ export default class PipelinesService {
}
getPipelines(data = {}) {
- const { scope, page } = data;
+ const { scope, page, username, ref } = data;
const { CancelToken } = axios;
+ const queryParams = { scope, page };
+
+ if (username) {
+ queryParams.username = username;
+ }
+
+ if (ref) {
+ queryParams.ref = ref;
+ }
+
this.cancelationSource = CancelToken.source();
return axios.get(this.endpoint, {
- params: { scope, page },
+ params: queryParams,
cancelToken: this.cancelationSource.token,
});
}
diff --git a/app/assets/javascripts/pipelines/stores/pipeline_store.js b/app/assets/javascripts/pipelines/stores/pipeline_store.js
index 1ef73760e02..c6f65277c8d 100644
--- a/app/assets/javascripts/pipelines/stores/pipeline_store.js
+++ b/app/assets/javascripts/pipelines/stores/pipeline_store.js
@@ -15,7 +15,7 @@ export default class PipelineStore {
* @param {Object} pipeline
*/
storePipeline(pipeline = {}) {
- const pipelineCopy = Object.assign({}, pipeline);
+ const pipelineCopy = { ...pipeline };
if (pipelineCopy.triggered_by) {
pipelineCopy.triggered_by = [pipelineCopy.triggered_by];
diff --git a/app/assets/javascripts/profile/account/components/delete_account_modal.vue b/app/assets/javascripts/profile/account/components/delete_account_modal.vue
index 85c5c073a74..aeb69fb1c05 100644
--- a/app/assets/javascripts/profile/account/components/delete_account_modal.vue
+++ b/app/assets/javascripts/profile/account/components/delete_account_modal.vue
@@ -85,7 +85,7 @@ Once you confirm %{deleteAccount}, it cannot be undone or recovered.`),
kind="danger"
@submit="onSubmit"
>
- <template slot="body" slot-scope="props">
+ <template #body="props">
<p v-html="props.text"></p>
<form ref="form" :action="actionUrl" method="post">
diff --git a/app/assets/javascripts/profile/account/components/update_username.vue b/app/assets/javascripts/profile/account/components/update_username.vue
index fa09e063552..feb83e07607 100644
--- a/app/assets/javascripts/profile/account/components/update_username.vue
+++ b/app/assets/javascripts/profile/account/components/update_username.vue
@@ -1,5 +1,5 @@
<script>
-import { escape as esc } from 'lodash';
+import { escape } from 'lodash';
import axios from '~/lib/utils/axios_utils';
import DeprecatedModal2 from '~/vue_shared/components/deprecated_modal_2.vue';
import { s__, sprintf } from '~/locale';
@@ -43,10 +43,10 @@ You are going to change the username %{currentUsernameBold} to %{newUsernameBold
Profile and projects will be redirected to the %{newUsername} namespace but this redirect will expire once the %{currentUsername} namespace is registered by another user or group.
Please update your Git repository remotes as soon as possible.`),
{
- currentUsernameBold: `<strong>${esc(this.username)}</strong>`,
- newUsernameBold: `<strong>${esc(this.newUsername)}</strong>`,
- currentUsername: esc(this.username),
- newUsername: esc(this.newUsername),
+ currentUsernameBold: `<strong>${escape(this.username)}</strong>`,
+ newUsernameBold: `<strong>${escape(this.newUsername)}</strong>`,
+ currentUsername: escape(this.username),
+ newUsername: escape(this.newUsername),
},
false,
);
diff --git a/app/assets/javascripts/project_select.js b/app/assets/javascripts/project_select.js
index 15c7c09366c..2b2c365dd54 100644
--- a/app/assets/javascripts/project_select.js
+++ b/app/assets/javascripts/project_select.js
@@ -1,4 +1,4 @@
-/* eslint-disable func-names, no-else-return */
+/* eslint-disable func-names */
import $ from 'jquery';
import Api from './api';
@@ -74,18 +74,17 @@ const projectSelect = () => {
},
projectsCallback,
);
- } else {
- return Api.projects(
- query.term,
- {
- order_by: this.orderBy,
- with_issues_enabled: this.withIssuesEnabled,
- with_merge_requests_enabled: this.withMergeRequestsEnabled,
- membership: !this.allProjects,
- },
- projectsCallback,
- );
}
+ return Api.projects(
+ query.term,
+ {
+ order_by: this.orderBy,
+ with_issues_enabled: this.withIssuesEnabled,
+ with_merge_requests_enabled: this.withMergeRequestsEnabled,
+ membership: !this.allProjects,
+ },
+ projectsCallback,
+ );
},
id(project) {
if (simpleFilter) return project.id;
diff --git a/app/assets/javascripts/projects/commits/components/author_select.vue b/app/assets/javascripts/projects/commits/components/author_select.vue
index 78f9389b80c..eb514b5c070 100644
--- a/app/assets/javascripts/projects/commits/components/author_select.vue
+++ b/app/assets/javascripts/projects/commits/components/author_select.vue
@@ -110,6 +110,7 @@ export default {
<gl-new-dropdown
:text="dropdownText"
:disabled="hasSearchParam"
+ toggle-class="gl-py-3"
class="gl-dropdown w-100 mt-2 mt-sm-0"
>
<gl-new-dropdown-header>
diff --git a/app/assets/javascripts/projects/commits/store/actions.js b/app/assets/javascripts/projects/commits/store/actions.js
index daeae071d6a..a3a53c2f975 100644
--- a/app/assets/javascripts/projects/commits/store/actions.js
+++ b/app/assets/javascripts/projects/commits/store/actions.js
@@ -1,3 +1,4 @@
+import * as Sentry from '@sentry/browser';
import * as types from './mutation_types';
import axios from '~/lib/utils/axios_utils';
import createFlash from '~/flash';
@@ -26,6 +27,9 @@ export default {
},
})
.then(({ data }) => dispatch('receiveAuthorsSuccess', data))
- .catch(() => dispatch('receiveAuthorsError'));
+ .catch(error => {
+ Sentry.captureException(error);
+ dispatch('receiveAuthorsError');
+ });
},
};
diff --git a/app/assets/javascripts/projects/pipelines/charts/components/app.vue b/app/assets/javascripts/projects/pipelines/charts/components/app.vue
index 4dc1c512689..cdf03a5013f 100644
--- a/app/assets/javascripts/projects/pipelines/charts/components/app.vue
+++ b/app/assets/javascripts/projects/pipelines/charts/components/app.vue
@@ -113,6 +113,9 @@ export default {
</script>
<template>
<div>
+ <div class="mb-3">
+ <h3>{{ s__('PipelineCharts|CI / CD Analytics') }}</h3>
+ </div>
<h4 class="my-4">{{ s__('PipelineCharts|Overall statistics') }}</h4>
<div class="row">
<div class="col-md-6">
diff --git a/app/assets/javascripts/registry/explorer/components/image_list.vue b/app/assets/javascripts/registry/explorer/components/image_list.vue
new file mode 100644
index 00000000000..bc209b12738
--- /dev/null
+++ b/app/assets/javascripts/registry/explorer/components/image_list.vue
@@ -0,0 +1,124 @@
+<script>
+import { GlPagination, GlTooltipDirective, GlDeprecatedButton, GlIcon } from '@gitlab/ui';
+import ClipboardButton from '~/vue_shared/components/clipboard_button.vue';
+
+import {
+ ASYNC_DELETE_IMAGE_ERROR_MESSAGE,
+ LIST_DELETE_BUTTON_DISABLED,
+ REMOVE_REPOSITORY_LABEL,
+ ROW_SCHEDULED_FOR_DELETION,
+} from '../constants';
+
+export default {
+ name: 'ImageList',
+ components: {
+ GlPagination,
+ ClipboardButton,
+ GlDeprecatedButton,
+ GlIcon,
+ },
+ directives: {
+ GlTooltip: GlTooltipDirective,
+ },
+ props: {
+ images: {
+ type: Array,
+ required: true,
+ },
+ pagination: {
+ type: Object,
+ required: true,
+ },
+ },
+ i18n: {
+ LIST_DELETE_BUTTON_DISABLED,
+ REMOVE_REPOSITORY_LABEL,
+ ROW_SCHEDULED_FOR_DELETION,
+ ASYNC_DELETE_IMAGE_ERROR_MESSAGE,
+ },
+ computed: {
+ currentPage: {
+ get() {
+ return this.pagination.page;
+ },
+ set(page) {
+ this.$emit('pageChange', page);
+ },
+ },
+ },
+ methods: {
+ encodeListItem(item) {
+ const params = JSON.stringify({ name: item.path, tags_path: item.tags_path, id: item.id });
+ return window.btoa(params);
+ },
+ },
+};
+</script>
+
+<template>
+ <div class="gl-display-flex gl-flex-direction-column">
+ <div
+ v-for="(listItem, index) in images"
+ :key="index"
+ v-gl-tooltip="{
+ placement: 'left',
+ disabled: !listItem.deleting,
+ title: $options.i18n.ROW_SCHEDULED_FOR_DELETION,
+ }"
+ data-testid="rowItem"
+ >
+ <div
+ class="gl-display-flex gl-justify-content-space-between gl-align-items-center gl-py-2 gl-px-1 border-bottom"
+ :class="{ 'border-top': index === 0, 'disabled-content': listItem.deleting }"
+ >
+ <div class="gl-display-flex gl-align-items-center">
+ <router-link
+ data-testid="detailsLink"
+ :to="{ name: 'details', params: { id: encodeListItem(listItem) } }"
+ >
+ {{ listItem.path }}
+ </router-link>
+ <clipboard-button
+ v-if="listItem.location"
+ :disabled="listItem.deleting"
+ :text="listItem.location"
+ :title="listItem.location"
+ css-class="btn-default btn-transparent btn-clipboard"
+ />
+ <gl-icon
+ v-if="listItem.failedDelete"
+ v-gl-tooltip
+ :title="$options.i18n.ASYNC_DELETE_IMAGE_ERROR_MESSAGE"
+ name="warning"
+ class="text-warning align-middle"
+ />
+ </div>
+ <div
+ v-gl-tooltip="{ disabled: listItem.destroy_path }"
+ class="d-none d-sm-block"
+ :title="$options.i18n.LIST_DELETE_BUTTON_DISABLED"
+ >
+ <gl-deprecated-button
+ v-gl-tooltip
+ data-testid="deleteImageButton"
+ :disabled="!listItem.destroy_path || listItem.deleting"
+ :title="$options.i18n.REMOVE_REPOSITORY_LABEL"
+ :aria-label="$options.i18n.REMOVE_REPOSITORY_LABEL"
+ class="btn-inverted"
+ variant="danger"
+ @click="$emit('delete', listItem)"
+ >
+ <gl-icon name="remove" />
+ </gl-deprecated-button>
+ </div>
+ </div>
+ </div>
+ <gl-pagination
+ v-model="currentPage"
+ :per-page="pagination.perPage"
+ :total-items="pagination.total"
+ align="center"
+ class="w-100 gl-mt-2"
+ />
+ </div>
+</template>
diff --git a/app/assets/javascripts/registry/explorer/constants.js b/app/assets/javascripts/registry/explorer/constants.js
index d4b9d25b212..7cbe657bfc0 100644
--- a/app/assets/javascripts/registry/explorer/constants.js
+++ b/app/assets/javascripts/registry/explorer/constants.js
@@ -37,16 +37,31 @@ export const DELETE_IMAGE_SUCCESS_MESSAGE = s__(
'ContainerRegistry|%{title} was successfully scheduled for deletion',
);
+export const IMAGE_REPOSITORY_LIST_LABEL = s__('ContainerRegistry|Image Repositories');
+
+export const SEARCH_PLACEHOLDER_TEXT = s__('ContainerRegistry|Filter by name');
+
+export const EMPTY_RESULT_TITLE = s__('ContainerRegistry|Sorry, your filter produced no results.');
+export const EMPTY_RESULT_MESSAGE = s__(
+ 'ContainerRegistry|To widen your search, change or remove the filters above.',
+);
+
// Image details page
+export const DETAILS_PAGE_TITLE = s__('ContainerRegistry|%{imageName} tags');
+
export const DELETE_TAG_ERROR_MESSAGE = s__(
- 'ContainerRegistry|Something went wrong while deleting the tag.',
+ 'ContainerRegistry|Something went wrong while marking the tag for deletion.',
+);
+export const DELETE_TAG_SUCCESS_MESSAGE = s__(
+ 'ContainerRegistry|Tag successfully marked for deletion.',
);
-export const DELETE_TAG_SUCCESS_MESSAGE = s__('ContainerRegistry|Tag deleted successfully');
export const DELETE_TAGS_ERROR_MESSAGE = s__(
- 'ContainerRegistry|Something went wrong while deleting the tags.',
+ 'ContainerRegistry|Something went wrong while marking the tags for deletion.',
+);
+export const DELETE_TAGS_SUCCESS_MESSAGE = s__(
+ 'ContainerRegistry|Tags successfully marked for deletion.',
);
-export const DELETE_TAGS_SUCCESS_MESSAGE = s__('ContainerRegistry|Tags deleted successfully');
export const DEFAULT_PAGE = 1;
export const DEFAULT_PAGE_SIZE = 10;
@@ -65,6 +80,27 @@ export const LIST_LABEL_IMAGE_ID = s__('ContainerRegistry|Image ID');
export const LIST_LABEL_SIZE = s__('ContainerRegistry|Compressed Size');
export const LIST_LABEL_LAST_UPDATED = s__('ContainerRegistry|Last Updated');
+export const REMOVE_TAG_BUTTON_TITLE = s__('ContainerRegistry|Remove tag');
+export const REMOVE_TAGS_BUTTON_TITLE = s__('ContainerRegistry|Remove selected tags');
+
+export const REMOVE_TAG_CONFIRMATION_TEXT = s__(
+ `ContainerRegistry|You are about to remove %{item}. Are you sure?`,
+);
+export const REMOVE_TAGS_CONFIRMATION_TEXT = s__(
+ `ContainerRegistry|You are about to remove %{item} tags. Are you sure?`,
+);
+
+export const EMPTY_IMAGE_REPOSITORY_TITLE = s__('ContainerRegistry|This image has no active tags');
+export const EMPTY_IMAGE_REPOSITORY_MESSAGE = s__(
+ `ContainerRegistry|The last tag related to this image was recently removed.
+This empty image and any associated data will be automatically removed as part of the regular Garbage Collection process.
+If you have any questions, contact your administrator.`,
+);
+
+export const ADMIN_GARBAGE_COLLECTION_TIP = s__(
+ 'ContainerRegistry|Remember to run %{docLinkStart}garbage collection%{docLinkEnd} to remove the stale data from storage.',
+);
+
// Expiration policies
export const EXPIRATION_POLICY_ALERT_TITLE = s__(
diff --git a/app/assets/javascripts/registry/explorer/index.js b/app/assets/javascripts/registry/explorer/index.js
index 9269aa074f8..2bba3ee4ff9 100644
--- a/app/assets/javascripts/registry/explorer/index.js
+++ b/app/assets/javascripts/registry/explorer/index.js
@@ -19,7 +19,7 @@ export default () => {
const { endpoint } = el.dataset;
const store = createStore();
- const router = createRouter(endpoint, store);
+ const router = createRouter(endpoint);
store.dispatch('setInitialState', el.dataset);
const attachMainComponent = () =>
diff --git a/app/assets/javascripts/registry/explorer/pages/details.vue b/app/assets/javascripts/registry/explorer/pages/details.vue
index 6afd4d1107a..cc2dc531dc8 100644
--- a/app/assets/javascripts/registry/explorer/pages/details.vue
+++ b/app/assets/javascripts/registry/explorer/pages/details.vue
@@ -9,12 +9,14 @@ import {
GlPagination,
GlModal,
GlSprintf,
+ GlAlert,
+ GlLink,
GlEmptyState,
GlResizeObserverDirective,
GlSkeletonLoader,
} from '@gitlab/ui';
import { GlBreakpointInstance } from '@gitlab/ui/dist/utils';
-import { n__, s__ } from '~/locale';
+import { n__ } from '~/locale';
import ClipboardButton from '~/vue_shared/components/clipboard_button.vue';
import { numberToHumanSize } from '~/lib/utils/number_utils';
import timeagoMixin from '~/vue_shared/mixins/timeago';
@@ -35,6 +37,14 @@ import {
DELETE_TAG_ERROR_MESSAGE,
DELETE_TAGS_SUCCESS_MESSAGE,
DELETE_TAGS_ERROR_MESSAGE,
+ REMOVE_TAG_CONFIRMATION_TEXT,
+ REMOVE_TAGS_CONFIRMATION_TEXT,
+ DETAILS_PAGE_TITLE,
+ REMOVE_TAGS_BUTTON_TITLE,
+ REMOVE_TAG_BUTTON_TITLE,
+ EMPTY_IMAGE_REPOSITORY_TITLE,
+ EMPTY_IMAGE_REPOSITORY_MESSAGE,
+ ADMIN_GARBAGE_COLLECTION_TIP,
} from '../constants';
export default {
@@ -49,6 +59,8 @@ export default {
GlSkeletonLoader,
GlSprintf,
GlEmptyState,
+ GlAlert,
+ GlLink,
},
directives: {
GlTooltip: GlTooltipDirective,
@@ -60,6 +72,19 @@ export default {
width: 1000,
height: 40,
},
+ i18n: {
+ DETAILS_PAGE_TITLE,
+ REMOVE_TAGS_BUTTON_TITLE,
+ REMOVE_TAG_BUTTON_TITLE,
+ EMPTY_IMAGE_REPOSITORY_TITLE,
+ EMPTY_IMAGE_REPOSITORY_MESSAGE,
+ },
+ alertMessages: {
+ success_tag: DELETE_TAG_SUCCESS_MESSAGE,
+ danger_tag: DELETE_TAG_ERROR_MESSAGE,
+ success_tags: DELETE_TAGS_SUCCESS_MESSAGE,
+ danger_tags: DELETE_TAGS_ERROR_MESSAGE,
+ },
data() {
return {
selectedItems: [],
@@ -67,6 +92,7 @@ export default {
selectAllChecked: false,
modalDescription: null,
isDesktop: true,
+ deleteAlertType: false,
};
},
computed: {
@@ -78,9 +104,15 @@ export default {
},
fields() {
const tagClass = this.isDesktop ? 'w-25' : '';
+ const tagInnerClass = this.isDesktop ? 'mw-m' : 'gl-justify-content-end';
return [
{ key: LIST_KEY_CHECKBOX, label: '', class: 'gl-w-16' },
- { key: LIST_KEY_TAG, label: LIST_LABEL_TAG, class: `${tagClass} js-tag-column` },
+ {
+ key: LIST_KEY_TAG,
+ label: LIST_LABEL_TAG,
+ class: `${tagClass} js-tag-column`,
+ innerClass: tagInnerClass,
+ },
{ key: LIST_KEY_IMAGE_ID, label: LIST_LABEL_IMAGE_ID },
{ key: LIST_KEY_SIZE, label: LIST_LABEL_SIZE },
{ key: LIST_KEY_LAST_UPDATED, label: LIST_LABEL_LAST_UPDATED },
@@ -110,20 +142,43 @@ export default {
this.requestTagsList({ pagination: { page }, params: this.$route.params.id });
},
},
+ deleteAlertConfig() {
+ const config = {
+ title: '',
+ message: '',
+ type: 'success',
+ };
+ if (this.deleteAlertType) {
+ [config.type] = this.deleteAlertType.split('_');
+
+ const defaultMessage = this.$options.alertMessages[this.deleteAlertType];
+
+ if (this.config.isAdmin && config.type === 'success') {
+ config.title = defaultMessage;
+ config.message = ADMIN_GARBAGE_COLLECTION_TIP;
+ } else {
+ config.message = defaultMessage;
+ }
+ }
+ return config;
+ },
+ },
+ mounted() {
+ this.requestTagsList({ params: this.$route.params.id });
},
methods: {
...mapActions(['requestTagsList', 'requestDeleteTag', 'requestDeleteTags']),
setModalDescription(itemIndex = -1) {
if (itemIndex === -1) {
this.modalDescription = {
- message: s__(`ContainerRegistry|You are about to remove %{item} tags. Are you sure?`),
+ message: REMOVE_TAGS_CONFIRMATION_TEXT,
item: this.itemsToBeDeleted.length,
};
} else {
const { path } = this.tags[itemIndex];
this.modalDescription = {
- message: s__(`ContainerRegistry|You are about to remove %{item}. Are you sure?`),
+ message: REMOVE_TAG_CONFIRMATION_TEXT,
item: path,
};
}
@@ -179,19 +234,17 @@ export default {
this.track('click_button');
this.$refs.deleteModal.show();
},
- handleSingleDelete(itemToDelete) {
+ handleSingleDelete(index) {
+ const itemToDelete = this.tags[index];
this.itemsToBeDeleted = [];
+ this.selectedItems = this.selectedItems.filter(i => i !== index);
return this.requestDeleteTag({ tag: itemToDelete, params: this.$route.params.id })
- .then(() =>
- this.$toast.show(DELETE_TAG_SUCCESS_MESSAGE, {
- type: 'success',
- }),
- )
- .catch(() =>
- this.$toast.show(DELETE_TAG_ERROR_MESSAGE, {
- type: 'error',
- }),
- );
+ .then(() => {
+ this.deleteAlertType = 'success_tag';
+ })
+ .catch(() => {
+ this.deleteAlertType = 'danger_tag';
+ });
},
handleMultipleDelete() {
const { itemsToBeDeleted } = this;
@@ -202,24 +255,19 @@ export default {
ids: itemsToBeDeleted.map(x => this.tags[x].name),
params: this.$route.params.id,
})
- .then(() =>
- this.$toast.show(DELETE_TAGS_SUCCESS_MESSAGE, {
- type: 'success',
- }),
- )
- .catch(() =>
- this.$toast.show(DELETE_TAGS_ERROR_MESSAGE, {
- type: 'error',
- }),
- );
+ .then(() => {
+ this.deleteAlertType = 'success_tags';
+ })
+ .catch(() => {
+ this.deleteAlertType = 'danger_tags';
+ });
},
onDeletionConfirmed() {
this.track('confirm_delete');
if (this.isMultiDelete) {
this.handleMultipleDelete();
} else {
- const index = this.itemsToBeDeleted[0];
- this.handleSingleDelete(this.tags[index]);
+ this.handleSingleDelete(this.itemsToBeDeleted[0]);
}
},
handleResize() {
@@ -231,9 +279,24 @@ export default {
<template>
<div v-gl-resize-observer="handleResize" class="my-3 w-100 slide-enter-to-element">
+ <gl-alert
+ v-if="deleteAlertType"
+ :variant="deleteAlertConfig.type"
+ :title="deleteAlertConfig.title"
+ class="my-2"
+ @dismiss="deleteAlertType = null"
+ >
+ <gl-sprintf :message="deleteAlertConfig.message">
+ <template #docLink="{content}">
+ <gl-link :href="config.garbageCollectionHelpPagePath" target="_blank">
+ {{ content }}
+ </gl-link>
+ </template>
+ </gl-sprintf>
+ </gl-alert>
<div class="d-flex my-3 align-items-center">
<h4>
- <gl-sprintf :message="s__('ContainerRegistry|%{imageName} tags')">
+ <gl-sprintf :message="$options.i18n.DETAILS_PAGE_TITLE">
<template #imageName>
{{ imageName }}
</template>
@@ -256,8 +319,8 @@ export default {
:disabled="!selectedItems || selectedItems.length === 0"
class="float-right"
variant="danger"
- :title="s__('ContainerRegistry|Remove selected tags')"
- :aria-label="s__('ContainerRegistry|Remove selected tags')"
+ :title="$options.i18n.REMOVE_TAGS_BUTTON_TITLE"
+ :aria-label="$options.i18n.REMOVE_TAGS_BUTTON_TITLE"
@click="deleteMultipleItems()"
>
<gl-icon name="remove" />
@@ -272,17 +335,24 @@ export default {
@change="updateSelectedItems(index)"
/>
</template>
- <template #cell(name)="{item}">
- <span ref="rowName">
- {{ item.name }}
- </span>
- <clipboard-button
- v-if="item.location"
- ref="rowClipboardButton"
- :title="item.location"
- :text="item.location"
- css-class="btn-default btn-transparent btn-clipboard"
- />
+ <template #cell(name)="{item, field}">
+ <div ref="rowName" :class="[field.innerClass, 'gl-display-flex']">
+ <span
+ v-gl-tooltip
+ data-testid="rowNameText"
+ :title="item.name"
+ class="gl-text-overflow-ellipsis gl-overflow-hidden gl-white-space-nowrap"
+ >
+ {{ item.name }}
+ </span>
+ <clipboard-button
+ v-if="item.location"
+ ref="rowClipboardButton"
+ :title="item.location"
+ :text="item.location"
+ css-class="btn-default btn-transparent btn-clipboard"
+ />
+ </div>
</template>
<template #cell(short_revision)="{value}">
<span ref="rowShortRevision">
@@ -299,15 +369,15 @@ export default {
</span>
</template>
<template #cell(created_at)="{value}">
- <span ref="rowTime">
+ <span ref="rowTime" v-gl-tooltip :title="tooltipTitle(value)">
{{ timeFormatted(value) }}
</span>
</template>
<template #cell(actions)="{index, item}">
<gl-deprecated-button
ref="singleDeleteButton"
- :title="s__('ContainerRegistry|Remove tag')"
- :aria-label="s__('ContainerRegistry|Remove tag')"
+ :title="$options.i18n.REMOVE_TAG_BUTTON_TITLE"
+ :aria-label="$options.i18n.REMOVE_TAG_BUTTON_TITLE"
:disabled="!item.destroy_path"
variant="danger"
class="js-delete-registry float-right btn-inverted btn-border-color btn-icon"
@@ -337,15 +407,9 @@ export default {
</template>
<gl-empty-state
v-else
- :title="s__('ContainerRegistry|This image has no active tags')"
+ :title="$options.i18n.EMPTY_IMAGE_REPOSITORY_TITLE"
:svg-path="config.noContainersImage"
- :description="
- s__(
- `ContainerRegistry|The last tag related to this image was recently removed.
- This empty image and any associated data will be automatically removed as part of the regular Garbage Collection process.
- If you have any questions, contact your administrator.`,
- )
- "
+ :description="$options.i18n.EMPTY_IMAGE_REPOSITORY_MESSAGE"
class="mx-auto my-0"
/>
</template>
diff --git a/app/assets/javascripts/registry/explorer/pages/index.vue b/app/assets/javascripts/registry/explorer/pages/index.vue
index 95d83c82987..709a163d56d 100644
--- a/app/assets/javascripts/registry/explorer/pages/index.vue
+++ b/app/assets/javascripts/registry/explorer/pages/index.vue
@@ -1,46 +1,9 @@
<script>
-import { GlAlert, GlSprintf, GlLink } from '@gitlab/ui';
-import { mapState, mapActions, mapGetters } from 'vuex';
-import { s__ } from '~/locale';
-
-export default {
- components: {
- GlAlert,
- GlSprintf,
- GlLink,
- },
- i18n: {
- garbageCollectionTipText: s__(
- 'ContainerRegistry|This Registry contains deleted image tag data. Remember to run %{docLinkStart}garbage collection%{docLinkEnd} to remove the stale data from storage.',
- ),
- },
- computed: {
- ...mapState(['config']),
- ...mapGetters(['showGarbageCollection']),
- },
- methods: {
- ...mapActions(['setShowGarbageCollectionTip']),
- },
-};
+export default {};
</script>
<template>
<div>
- <gl-alert
- v-if="showGarbageCollection"
- variant="tip"
- class="my-2"
- @dismiss="setShowGarbageCollectionTip(false)"
- >
- <gl-sprintf :message="$options.i18n.garbageCollectionTipText">
- <template #docLink="{content}">
- <gl-link :href="config.garbageCollectionHelpPagePath" target="_blank">
- {{ content }}
- </gl-link>
- </template>
- </gl-sprintf>
- </gl-alert>
-
<transition name="slide">
<router-view ref="router-view" />
</transition>
diff --git a/app/assets/javascripts/registry/explorer/pages/list.vue b/app/assets/javascripts/registry/explorer/pages/list.vue
index 8923c305b2d..4efa6f08d84 100644
--- a/app/assets/javascripts/registry/explorer/pages/list.vue
+++ b/app/assets/javascripts/registry/explorer/pages/list.vue
@@ -2,53 +2,52 @@
import { mapState, mapActions } from 'vuex';
import {
GlEmptyState,
- GlPagination,
GlTooltipDirective,
- GlDeprecatedButton,
- GlIcon,
GlModal,
GlSprintf,
GlLink,
GlAlert,
GlSkeletonLoader,
+ GlSearchBoxByClick,
} from '@gitlab/ui';
import Tracking from '~/tracking';
-import ClipboardButton from '~/vue_shared/components/clipboard_button.vue';
+
import ProjectEmptyState from '../components/project_empty_state.vue';
import GroupEmptyState from '../components/group_empty_state.vue';
import ProjectPolicyAlert from '../components/project_policy_alert.vue';
import QuickstartDropdown from '../components/quickstart_dropdown.vue';
+import ImageList from '../components/image_list.vue';
+
import {
DELETE_IMAGE_SUCCESS_MESSAGE,
DELETE_IMAGE_ERROR_MESSAGE,
- ASYNC_DELETE_IMAGE_ERROR_MESSAGE,
CONTAINER_REGISTRY_TITLE,
CONNECTION_ERROR_TITLE,
CONNECTION_ERROR_MESSAGE,
LIST_INTRO_TEXT,
- LIST_DELETE_BUTTON_DISABLED,
- REMOVE_REPOSITORY_LABEL,
REMOVE_REPOSITORY_MODAL_TEXT,
- ROW_SCHEDULED_FOR_DELETION,
+ REMOVE_REPOSITORY_LABEL,
+ SEARCH_PLACEHOLDER_TEXT,
+ IMAGE_REPOSITORY_LIST_LABEL,
+ EMPTY_RESULT_TITLE,
+ EMPTY_RESULT_MESSAGE,
} from '../constants';
export default {
name: 'RegistryListApp',
components: {
GlEmptyState,
- GlPagination,
ProjectEmptyState,
GroupEmptyState,
ProjectPolicyAlert,
- ClipboardButton,
QuickstartDropdown,
- GlDeprecatedButton,
- GlIcon,
+ ImageList,
GlModal,
GlSprintf,
GlLink,
GlAlert,
GlSkeletonLoader,
+ GlSearchBoxByClick,
},
directives: {
GlTooltip: GlTooltipDirective,
@@ -60,20 +59,23 @@ export default {
height: 40,
},
i18n: {
- containerRegistryTitle: CONTAINER_REGISTRY_TITLE,
- connectionErrorTitle: CONNECTION_ERROR_TITLE,
- connectionErrorMessage: CONNECTION_ERROR_MESSAGE,
- introText: LIST_INTRO_TEXT,
- deleteButtonDisabled: LIST_DELETE_BUTTON_DISABLED,
- removeRepositoryLabel: REMOVE_REPOSITORY_LABEL,
- removeRepositoryModalText: REMOVE_REPOSITORY_MODAL_TEXT,
- rowScheduledForDeletion: ROW_SCHEDULED_FOR_DELETION,
- asyncDeleteErrorMessage: ASYNC_DELETE_IMAGE_ERROR_MESSAGE,
+ CONTAINER_REGISTRY_TITLE,
+ CONNECTION_ERROR_TITLE,
+ CONNECTION_ERROR_MESSAGE,
+ LIST_INTRO_TEXT,
+ REMOVE_REPOSITORY_MODAL_TEXT,
+ REMOVE_REPOSITORY_LABEL,
+ SEARCH_PLACEHOLDER_TEXT,
+ IMAGE_REPOSITORY_LIST_LABEL,
+ EMPTY_RESULT_TITLE,
+ EMPTY_RESULT_MESSAGE,
},
data() {
return {
itemToDelete: {},
deleteAlertType: null,
+ search: null,
+ isEmpty: false,
};
},
computed: {
@@ -83,14 +85,6 @@ export default {
label: 'registry_repository_delete',
};
},
- currentPage: {
- get() {
- return this.pagination.page;
- },
- set(page) {
- this.requestImagesList({ page });
- },
- },
showQuickStartDropdown() {
return Boolean(!this.isLoading && !this.config?.isGroupPage && this.images?.length);
},
@@ -103,8 +97,19 @@ export default {
: DELETE_IMAGE_ERROR_MESSAGE;
},
},
+ mounted() {
+ this.loadImageList(this.$route.name);
+ },
methods: {
...mapActions(['requestImagesList', 'requestDeleteImage']),
+ loadImageList(fromName) {
+ if (!fromName || !this.images?.length) {
+ return this.requestImagesList().then(() => {
+ this.isEmpty = this.images.length === 0;
+ });
+ }
+ return Promise.resolve();
+ },
deleteImage(item) {
this.track('click_button');
this.itemToDelete = item;
@@ -120,10 +125,6 @@ export default {
this.deleteAlertType = 'danger';
});
},
- encodeListItem(item) {
- const params = JSON.stringify({ name: item.path, tags_path: item.tags_path, id: item.id });
- return window.btoa(params);
- },
dismissDeleteAlert() {
this.deleteAlertType = null;
this.itemToDelete = {};
@@ -152,12 +153,12 @@ export default {
<gl-empty-state
v-if="config.characterError"
- :title="$options.i18n.connectionErrorTitle"
+ :title="$options.i18n.CONNECTION_ERROR_TITLE"
:svg-path="config.containersErrorImage"
>
<template #description>
<p>
- <gl-sprintf :message="$options.i18n.connectionErrorMessage">
+ <gl-sprintf :message="$options.i18n.CONNECTION_ERROR_MESSAGE">
<template #docLink="{content}">
<gl-link :href="`${config.helpPagePath}#docker-connection-error`" target="_blank">
{{ content }}
@@ -171,11 +172,11 @@ export default {
<template v-else>
<div>
<div class="d-flex justify-content-between align-items-center">
- <h4>{{ $options.i18n.containerRegistryTitle }}</h4>
+ <h4>{{ $options.i18n.CONTAINER_REGISTRY_TITLE }}</h4>
<quickstart-dropdown v-if="showQuickStartDropdown" class="d-none d-sm-block" />
</div>
<p>
- <gl-sprintf :message="$options.i18n.introText">
+ <gl-sprintf :message="$options.i18n.LIST_INTRO_TEXT">
<template #docLink="{content}">
<gl-link :href="config.helpPagePath" target="_blank">
{{ content }}
@@ -199,73 +200,40 @@ export default {
</gl-skeleton-loader>
</div>
<template v-else>
- <div v-if="images.length" ref="imagesList" class="d-flex flex-column">
- <div
- v-for="(listItem, index) in images"
- :key="index"
- ref="rowItem"
- v-gl-tooltip="{
- placement: 'left',
- disabled: !listItem.deleting,
- title: $options.i18n.rowScheduledForDeletion,
- }"
- >
- <div
- class="d-flex justify-content-between align-items-center py-2 px-1 border-bottom"
- :class="{ 'border-top': index === 0, 'disabled-content': listItem.deleting }"
- >
- <div class="d-felx align-items-center">
- <router-link
- ref="detailsLink"
- :to="{ name: 'details', params: { id: encodeListItem(listItem) } }"
- >
- {{ listItem.path }}
- </router-link>
- <clipboard-button
- v-if="listItem.location"
- ref="clipboardButton"
- :disabled="listItem.deleting"
- :text="listItem.location"
- :title="listItem.location"
- css-class="btn-default btn-transparent btn-clipboard"
- />
- <gl-icon
- v-if="listItem.failedDelete"
- v-gl-tooltip
- :title="$options.i18n.asyncDeleteErrorMessage"
- name="warning"
- class="text-warning align-middle"
- />
- </div>
- <div
- v-gl-tooltip="{ disabled: listItem.destroy_path }"
- class="d-none d-sm-block"
- :title="$options.i18n.deleteButtonDisabled"
- >
- <gl-deprecated-button
- ref="deleteImageButton"
- v-gl-tooltip
- :disabled="!listItem.destroy_path || listItem.deleting"
- :title="$options.i18n.removeRepositoryLabel"
- :aria-label="$options.i18n.removeRepositoryLabel"
- class="btn-inverted"
- variant="danger"
- @click="deleteImage(listItem)"
- >
- <gl-icon name="remove" />
- </gl-deprecated-button>
- </div>
+ <template v-if="!isEmpty">
+ <div class="gl-display-flex gl-p-1" data-testid="listHeader">
+ <div class="gl-flex-fill-1">
+ <h5>{{ $options.i18n.IMAGE_REPOSITORY_LIST_LABEL }}</h5>
+ </div>
+ <div>
+ <gl-search-box-by-click
+ v-model="search"
+ :placeholder="$options.i18n.SEARCH_PLACEHOLDER_TEXT"
+ @submit="requestImagesList({ name: $event })"
+ />
</div>
</div>
- <gl-pagination
- v-model="currentPage"
- :per-page="pagination.perPage"
- :total-items="pagination.total"
- align="center"
- class="w-100 mt-2"
+
+ <image-list
+ v-if="images.length"
+ :images="images"
+ :pagination="pagination"
+ @pageChange="requestImagesList({ pagination: { page: $event }, name: search })"
+ @delete="deleteImage"
/>
- </div>
+ <gl-empty-state
+ v-else
+ :svg-path="config.noContainersImage"
+ data-testid="emptySearch"
+ :title="$options.i18n.EMPTY_RESULT_TITLE"
+ class="container-message"
+ >
+ <template #description>
+ {{ $options.i18n.EMPTY_RESULT_MESSAGE }}
+ </template>
+ </gl-empty-state>
+ </template>
<template v-else>
<project-empty-state v-if="!config.isGroupPage" />
<group-empty-state v-else />
@@ -279,9 +247,9 @@ export default {
@ok="handleDeleteImage"
@cancel="track('cancel_delete')"
>
- <template #modal-title>{{ $options.i18n.removeRepositoryLabel }}</template>
+ <template #modal-title>{{ $options.i18n.REMOVE_REPOSITORY_LABEL }}</template>
<p>
- <gl-sprintf :message="$options.i18n.removeRepositoryModalText">
+ <gl-sprintf :message="$options.i18n.REMOVE_REPOSITORY_MODAL_TEXT">
<template #title>
<b>{{ itemToDelete.path }}</b>
</template>
diff --git a/app/assets/javascripts/registry/explorer/router.js b/app/assets/javascripts/registry/explorer/router.js
index 28df3177df4..478eaca1a68 100644
--- a/app/assets/javascripts/registry/explorer/router.js
+++ b/app/assets/javascripts/registry/explorer/router.js
@@ -7,7 +7,7 @@ import { decodeAndParse } from './utils';
Vue.use(VueRouter);
-export default function createRouter(base, store) {
+export default function createRouter(base) {
const router = new VueRouter({
base,
mode: 'history',
@@ -20,12 +20,6 @@ export default function createRouter(base, store) {
nameGenerator: () => s__('ContainerRegistry|Container Registry'),
root: true,
},
- beforeEnter: (to, from, next) => {
- if (!from.name || !store.state.images?.length) {
- store.dispatch('requestImagesList');
- }
- next();
- },
},
{
name: 'details',
@@ -34,10 +28,6 @@ export default function createRouter(base, store) {
meta: {
nameGenerator: route => decodeAndParse(route.params.id).name,
},
- beforeEnter: (to, from, next) => {
- store.dispatch('requestTagsList', { params: to.params.id });
- next();
- },
},
],
});
diff --git a/app/assets/javascripts/registry/explorer/stores/actions.js b/app/assets/javascripts/registry/explorer/stores/actions.js
index b4f66dbbcd6..7f80bc21d6e 100644
--- a/app/assets/javascripts/registry/explorer/stores/actions.js
+++ b/app/assets/javascripts/registry/explorer/stores/actions.js
@@ -23,12 +23,15 @@ export const receiveTagsListSuccess = ({ commit }, { data, headers }) => {
commit(types.SET_TAGS_PAGINATION, headers);
};
-export const requestImagesList = ({ commit, dispatch, state }, pagination = {}) => {
+export const requestImagesList = (
+ { commit, dispatch, state },
+ { pagination = {}, name = null } = {},
+) => {
commit(types.SET_MAIN_LOADING, true);
const { page = DEFAULT_PAGE, perPage = DEFAULT_PAGE_SIZE } = pagination;
return axios
- .get(state.config.endpoint, { params: { page, per_page: perPage } })
+ .get(state.config.endpoint, { params: { page, per_page: perPage, name } })
.then(({ data, headers }) => {
dispatch('receiveImagesListSuccess', { data, headers });
})
@@ -66,7 +69,7 @@ export const requestDeleteTag = ({ commit, dispatch, state }, { tag, params }) =
dispatch('setShowGarbageCollectionTip', true);
return dispatch('requestTagsList', { pagination: state.tagsPagination, params });
})
- .catch(() => {
+ .finally(() => {
commit(types.SET_MAIN_LOADING, false);
});
};
@@ -83,7 +86,7 @@ export const requestDeleteTags = ({ commit, dispatch, state }, { ids, params })
dispatch('setShowGarbageCollectionTip', true);
return dispatch('requestTagsList', { pagination: state.tagsPagination, params });
})
- .catch(() => {
+ .finally(() => {
commit(types.SET_MAIN_LOADING, false);
});
};
diff --git a/app/assets/javascripts/registry/explorer/stores/index.js b/app/assets/javascripts/registry/explorer/stores/index.js
index b3ff2e6e002..153032e37d3 100644
--- a/app/assets/javascripts/registry/explorer/stores/index.js
+++ b/app/assets/javascripts/registry/explorer/stores/index.js
@@ -15,4 +15,5 @@ export const createStore = () =>
mutations,
});
+// Deprecated and to be removed
export default createStore();
diff --git a/app/assets/javascripts/registry/settings/registry_settings_bundle.js b/app/assets/javascripts/registry/settings/registry_settings_bundle.js
index 6ae1dbb72c4..a318aa2a694 100644
--- a/app/assets/javascripts/registry/settings/registry_settings_bundle.js
+++ b/app/assets/javascripts/registry/settings/registry_settings_bundle.js
@@ -1,7 +1,7 @@
import Vue from 'vue';
import { GlToast } from '@gitlab/ui';
import Translate from '~/vue_shared/translate';
-import store from './store/';
+import store from './store';
import RegistrySettingsApp from './components/registry_settings_app.vue';
Vue.use(GlToast);
diff --git a/app/assets/javascripts/registry/settings/store/getters.js b/app/assets/javascripts/registry/settings/store/getters.js
index ef4b4f0ba02..ac1a931d8e0 100644
--- a/app/assets/javascripts/registry/settings/store/getters.js
+++ b/app/assets/javascripts/registry/settings/store/getters.js
@@ -16,6 +16,7 @@ export const getSettings = (state, getters) => ({
older_than: getters.getOlderThan,
keep_n: getters.getKeepN,
name_regex: state.settings.name_regex,
+ name_regex_keep: state.settings.name_regex_keep,
});
export const getIsEdited = state => !isEqual(state.original, state.settings);
diff --git a/app/assets/javascripts/registry/settings/store/mutations.js b/app/assets/javascripts/registry/settings/store/mutations.js
index bb7071b020b..3ba13419b98 100644
--- a/app/assets/javascripts/registry/settings/store/mutations.js
+++ b/app/assets/javascripts/registry/settings/store/mutations.js
@@ -21,7 +21,7 @@ export default {
state.original = Object.freeze(settings);
},
[types.RESET_SETTINGS](state) {
- state.settings = Object.assign({}, state.original);
+ state.settings = { ...state.original };
},
[types.TOGGLE_LOADING](state) {
state.isLoading = !state.isLoading;
diff --git a/app/assets/javascripts/registry/shared/components/expiration_policy_fields.vue b/app/assets/javascripts/registry/shared/components/expiration_policy_fields.vue
index 3e212f09e35..04a547db07e 100644
--- a/app/assets/javascripts/registry/shared/components/expiration_policy_fields.vue
+++ b/app/assets/javascripts/registry/shared/components/expiration_policy_fields.vue
@@ -1,8 +1,23 @@
<script>
import { uniqueId } from 'lodash';
import { GlFormGroup, GlToggle, GlFormSelect, GlFormTextarea, GlSprintf } from '@gitlab/ui';
-import { s__, __ } from '~/locale';
-import { NAME_REGEX_LENGTH } from '../constants';
+import {
+ NAME_REGEX_LENGTH,
+ ENABLED_TEXT,
+ DISABLED_TEXT,
+ TEXT_AREA_INVALID_FEEDBACK,
+ EXPIRATION_INTERVAL_LABEL,
+ EXPIRATION_SCHEDULE_LABEL,
+ KEEP_N_LABEL,
+ NAME_REGEX_LABEL,
+ NAME_REGEX_PLACEHOLDER,
+ NAME_REGEX_DESCRIPTION,
+ NAME_REGEX_KEEP_LABEL,
+ NAME_REGEX_KEEP_PLACEHOLDER,
+ NAME_REGEX_KEEP_DESCRIPTION,
+ ENABLE_TOGGLE_LABEL,
+ ENABLE_TOGGLE_DESCRIPTION,
+} from '../constants';
import { mapComputedToEvent } from '../utils';
export default {
@@ -40,42 +55,73 @@ export default {
default: 'right',
},
},
- nameRegexPlaceholder: '.*',
+ i18n: {
+ textAreaInvalidFeedback: TEXT_AREA_INVALID_FEEDBACK,
+ enableToggleLabel: ENABLE_TOGGLE_LABEL,
+ enableToggleDescription: ENABLE_TOGGLE_DESCRIPTION,
+ },
selectList: [
{
name: 'expiration-policy-interval',
- label: s__('ContainerRegistry|Expiration interval:'),
+ label: EXPIRATION_INTERVAL_LABEL,
model: 'older_than',
optionKey: 'olderThan',
},
{
name: 'expiration-policy-schedule',
- label: s__('ContainerRegistry|Expiration schedule:'),
+ label: EXPIRATION_SCHEDULE_LABEL,
model: 'cadence',
optionKey: 'cadence',
},
{
name: 'expiration-policy-latest',
- label: s__('ContainerRegistry|Number of tags to retain:'),
+ label: KEEP_N_LABEL,
model: 'keep_n',
optionKey: 'keepN',
},
],
+ textAreaList: [
+ {
+ name: 'expiration-policy-name-matching',
+ label: NAME_REGEX_LABEL,
+ model: 'name_regex',
+ placeholder: NAME_REGEX_PLACEHOLDER,
+ stateVariable: 'nameRegexState',
+ description: NAME_REGEX_DESCRIPTION,
+ },
+ {
+ name: 'expiration-policy-keep-name',
+ label: NAME_REGEX_KEEP_LABEL,
+ model: 'name_regex_keep',
+ placeholder: NAME_REGEX_KEEP_PLACEHOLDER,
+ stateVariable: 'nameKeepRegexState',
+ description: NAME_REGEX_KEEP_DESCRIPTION,
+ },
+ ],
data() {
return {
uniqueId: uniqueId(),
};
},
computed: {
- ...mapComputedToEvent(['enabled', 'cadence', 'older_than', 'keep_n', 'name_regex'], 'value'),
+ ...mapComputedToEvent(
+ ['enabled', 'cadence', 'older_than', 'keep_n', 'name_regex', 'name_regex_keep'],
+ 'value',
+ ),
policyEnabledText() {
- return this.enabled ? __('enabled') : __('disabled');
+ return this.enabled ? ENABLED_TEXT : DISABLED_TEXT;
},
- nameRegexState() {
- return this.name_regex ? this.name_regex.length <= NAME_REGEX_LENGTH : null;
+ textAreaState() {
+ return {
+ nameRegexState: this.validateNameRegex(this.name_regex),
+ nameKeepRegexState: this.validateNameRegex(this.name_regex_keep),
+ };
},
fieldsValidity() {
- return this.nameRegexState !== false;
+ return (
+ this.textAreaState.nameRegexState !== false &&
+ this.textAreaState.nameKeepRegexState !== false
+ );
},
isFormElementDisabled() {
return !this.enabled || this.isLoading;
@@ -94,6 +140,9 @@ export default {
},
},
methods: {
+ validateNameRegex(value) {
+ return value ? value.length <= NAME_REGEX_LENGTH : null;
+ },
idGenerator(id) {
return `${id}_${this.uniqueId}`;
},
@@ -111,7 +160,7 @@ export default {
:label-cols="labelCols"
:label-align="labelAlign"
:label-for="idGenerator('expiration-policy-toggle')"
- :label="s__('ContainerRegistry|Expiration policy:')"
+ :label="$options.i18n.enableToggleLabel"
>
<div class="d-flex align-items-start">
<gl-toggle
@@ -120,9 +169,7 @@ export default {
:disabled="isLoading"
/>
<span class="mb-2 ml-1 lh-2">
- <gl-sprintf
- :message="s__('ContainerRegistry|Docker tag expiration policy is %{toggleStatus}')"
- >
+ <gl-sprintf :message="$options.i18n.enableToggleDescription">
<template #toggleStatus>
<strong>{{ policyEnabledText }}</strong>
</template>
@@ -157,35 +204,34 @@ export default {
</gl-form-group>
<gl-form-group
- :id="idGenerator('expiration-policy-name-matching-group')"
+ v-for="textarea in $options.textAreaList"
+ :id="idGenerator(`${textarea.name}-group`)"
+ :key="textarea.name"
:label-cols="labelCols"
:label-align="labelAlign"
- :label-for="idGenerator('expiration-policy-name-matching')"
- :label="
- s__('ContainerRegistry|Docker tags with names matching this regex pattern will expire:')
- "
- :state="nameRegexState"
- :invalid-feedback="
- s__('ContainerRegistry|The value of this input should be less than 255 characters')
- "
+ :label-for="idGenerator(textarea.name)"
+ :state="textAreaState[textarea.stateVariable]"
+ :invalid-feedback="$options.i18n.textAreaInvalidFeedback"
>
+ <template #label>
+ <gl-sprintf :message="textarea.label">
+ <template #italic="{content}">
+ <i>{{ content }}</i>
+ </template>
+ </gl-sprintf>
+ </template>
<gl-form-textarea
- :id="idGenerator('expiration-policy-name-matching')"
- v-model="name_regex"
- :placeholder="$options.nameRegexPlaceholder"
- :state="nameRegexState"
+ :id="idGenerator(textarea.name)"
+ :value="value[textarea.model]"
+ :placeholder="textarea.placeholder"
+ :state="textAreaState[textarea.stateVariable]"
:disabled="isFormElementDisabled"
trim
+ @input="updateModel($event, textarea.model)"
/>
<template #description>
<span ref="regex-description">
- <gl-sprintf
- :message="
- s__(
- 'ContainerRegistry|Wildcards such as %{codeStart}.*-stable%{codeEnd} or %{codeStart}production/.*%{codeEnd} are supported. To select all tags, use %{codeStart}.*%{codeEnd}',
- )
- "
- >
+ <gl-sprintf :message="textarea.description">
<template #code="{content}">
<code>{{ content }}</code>
</template>
diff --git a/app/assets/javascripts/registry/shared/constants.js b/app/assets/javascripts/registry/shared/constants.js
index c0dac466b29..4689d01b1c8 100644
--- a/app/assets/javascripts/registry/shared/constants.js
+++ b/app/assets/javascripts/registry/shared/constants.js
@@ -1,4 +1,4 @@
-import { s__ } from '~/locale';
+import { s__, __ } from '~/locale';
export const FETCH_SETTINGS_ERROR_MESSAGE = s__(
'ContainerRegistry|Something went wrong while fetching the expiration policy.',
@@ -13,3 +13,33 @@ export const UPDATE_SETTINGS_SUCCESS_MESSAGE = s__(
);
export const NAME_REGEX_LENGTH = 255;
+
+export const ENABLED_TEXT = __('enabled');
+export const DISABLED_TEXT = __('disabled');
+
+export const ENABLE_TOGGLE_LABEL = s__('ContainerRegistry|Expiration policy:');
+export const ENABLE_TOGGLE_DESCRIPTION = s__(
+ 'ContainerRegistry|Docker tag expiration policy is %{toggleStatus}',
+);
+
+export const TEXT_AREA_INVALID_FEEDBACK = s__(
+ 'ContainerRegistry|The value of this input should be less than 255 characters',
+);
+
+export const EXPIRATION_INTERVAL_LABEL = s__('ContainerRegistry|Expiration interval:');
+export const EXPIRATION_SCHEDULE_LABEL = s__('ContainerRegistry|Expiration schedule:');
+export const KEEP_N_LABEL = s__('ContainerRegistry|Number of tags to retain:');
+export const NAME_REGEX_LABEL = s__(
+ 'ContainerRegistry|Tags with names matching this regex pattern will %{italicStart}expire:%{italicEnd}',
+);
+export const NAME_REGEX_PLACEHOLDER = '.*';
+export const NAME_REGEX_DESCRIPTION = s__(
+ 'ContainerRegistry|Regular expressions such as %{codeStart}.*-test%{codeEnd} or %{codeStart}dev-.*%{codeEnd} are supported. To select all tags, use %{codeStart}.*%{codeEnd}',
+);
+export const NAME_REGEX_KEEP_LABEL = s__(
+ 'ContainerRegistry|Tags with names matching this regex pattern will %{italicStart}be preserved:%{italicEnd}',
+);
+export const NAME_REGEX_KEEP_PLACEHOLDER = '';
+export const NAME_REGEX_KEEP_DESCRIPTION = s__(
+ 'ContainerRegistry|Regular expressions such as %{codeStart}.*-master%{codeEnd} or %{codeStart}release-.*%{codeEnd} are supported',
+);
diff --git a/app/assets/javascripts/releases/components/app_edit.vue b/app/assets/javascripts/releases/components/app_edit.vue
index 8d68ff02116..01dd0638023 100644
--- a/app/assets/javascripts/releases/components/app_edit.vue
+++ b/app/assets/javascripts/releases/components/app_edit.vue
@@ -1,7 +1,7 @@
<script>
import { mapState, mapActions, mapGetters } from 'vuex';
import { GlButton, GlFormInput, GlFormGroup } from '@gitlab/ui';
-import { escape as esc } from 'lodash';
+import { escape } from 'lodash';
import { __, sprintf } from '~/locale';
import MarkdownField from '~/vue_shared/components/markdown/field.vue';
import autofocusonshow from '~/vue_shared/directives/autofocusonshow';
@@ -9,6 +9,7 @@ import { BACK_URL_PARAM } from '~/releases/constants';
import { getParameterByName } from '~/lib/utils/common_utils';
import AssetLinksForm from './asset_links_form.vue';
import glFeatureFlagsMixin from '~/vue_shared/mixins/gl_feature_flags_mixin';
+import MilestoneCombobox from '~/milestones/project_milestone_combobox.vue';
export default {
name: 'ReleaseEditApp',
@@ -18,6 +19,7 @@ export default {
GlButton,
MarkdownField,
AssetLinksForm,
+ MilestoneCombobox,
},
directives: {
autofocusonshow,
@@ -32,6 +34,10 @@ export default {
'markdownPreviewPath',
'releasesPagePath',
'updateReleaseApiDocsPath',
+ 'release',
+ 'newMilestonePath',
+ 'manageMilestonesPath',
+ 'projectId',
]),
...mapGetters('detail', ['isValid']),
showForm() {
@@ -58,7 +64,7 @@ export default {
'Changing a Release tag is only supported via Releases API. %{linkStart}More information%{linkEnd}',
),
{
- linkStart: `<a href="${esc(
+ linkStart: `<a href="${escape(
this.updateReleaseApiDocsPath,
)}" target="_blank" rel="noopener noreferrer">`,
linkEnd: '</a>',
@@ -82,6 +88,14 @@ export default {
this.updateReleaseNotes(notes);
},
},
+ releaseMilestones: {
+ get() {
+ return this.$store.state.detail.release.milestones;
+ },
+ set(milestones) {
+ this.updateReleaseMilestones(milestones);
+ },
+ },
cancelPath() {
return getParameterByName(BACK_URL_PARAM) || this.releasesPagePath;
},
@@ -91,6 +105,18 @@ export default {
isSaveChangesDisabled() {
return this.isUpdatingRelease || !this.isValid;
},
+ milestoneComboboxExtraLinks() {
+ return [
+ {
+ text: __('Create new'),
+ url: this.newMilestonePath,
+ },
+ {
+ text: __('Manage milestones'),
+ url: this.manageMilestonesPath,
+ },
+ ];
+ },
},
created() {
this.fetchRelease();
@@ -101,6 +127,7 @@ export default {
'updateRelease',
'updateReleaseTitle',
'updateReleaseNotes',
+ 'updateReleaseMilestones',
]),
},
};
@@ -137,6 +164,16 @@ export default {
class="form-control"
/>
</gl-form-group>
+ <gl-form-group class="w-50">
+ <label>{{ __('Milestones') }}</label>
+ <div class="d-flex flex-column col-md-6 col-sm-10 pl-0">
+ <milestone-combobox
+ v-model="releaseMilestones"
+ :project-id="projectId"
+ :extra-links="milestoneComboboxExtraLinks"
+ />
+ </div>
+ </gl-form-group>
<gl-form-group>
<label for="release-notes">{{ __('Release notes') }}</label>
<div class="bordered-box pr-3 pl-3">
@@ -147,19 +184,19 @@ export default {
:add-spacing-classes="false"
class="prepend-top-10 append-bottom-10"
>
- <textarea
- id="release-notes"
- slot="textarea"
- v-model="releaseNotes"
- class="note-textarea js-gfm-input js-autosize markdown-area"
- dir="auto"
- data-supports-quick-actions="false"
- :aria-label="__('Release notes')"
- :placeholder="__('Write your release notes or drag your files here…')"
- @keydown.meta.enter="updateRelease()"
- @keydown.ctrl.enter="updateRelease()"
- >
- </textarea>
+ <template #textarea>
+ <textarea
+ id="release-notes"
+ v-model="releaseNotes"
+ class="note-textarea js-gfm-input js-autosize markdown-area"
+ dir="auto"
+ data-supports-quick-actions="false"
+ :aria-label="__('Release notes')"
+ :placeholder="__('Write your release notes or drag your files here…')"
+ @keydown.meta.enter="updateRelease()"
+ @keydown.ctrl.enter="updateRelease()"
+ ></textarea>
+ </template>
</markdown-field>
</div>
</gl-form-group>
@@ -174,12 +211,9 @@ export default {
type="submit"
:aria-label="__('Save changes')"
:disabled="isSaveChangesDisabled"
+ >{{ __('Save changes') }}</gl-button
>
- {{ __('Save changes') }}
- </gl-button>
- <gl-button :href="cancelPath" class="js-cancel-button">
- {{ __('Cancel') }}
- </gl-button>
+ <gl-button :href="cancelPath" class="js-cancel-button">{{ __('Cancel') }}</gl-button>
</div>
</form>
</div>
diff --git a/app/assets/javascripts/releases/components/app_index.vue b/app/assets/javascripts/releases/components/app_index.vue
index 215a376fc76..67085ecca2b 100644
--- a/app/assets/javascripts/releases/components/app_index.vue
+++ b/app/assets/javascripts/releases/components/app_index.vue
@@ -1,6 +1,6 @@
<script>
import { mapState, mapActions } from 'vuex';
-import { GlSkeletonLoading, GlEmptyState, GlLink } from '@gitlab/ui';
+import { GlSkeletonLoading, GlEmptyState, GlLink, GlButton } from '@gitlab/ui';
import {
getParameterByName,
historyPushState,
@@ -18,6 +18,7 @@ export default {
ReleaseBlock,
TablePagination,
GlLink,
+ GlButton,
},
props: {
projectId: {
@@ -69,14 +70,16 @@ export default {
</script>
<template>
<div class="flex flex-column mt-2">
- <gl-link
+ <gl-button
v-if="newReleasePath"
:href="newReleasePath"
:aria-describedby="shouldRenderEmptyState && 'releases-description'"
- class="btn btn-success align-self-end mb-2 js-new-release-btn"
+ category="primary"
+ variant="success"
+ class="align-self-end mb-2 js-new-release-btn"
>
{{ __('New release') }}
- </gl-link>
+ </gl-button>
<gl-skeleton-loading v-if="isLoading" class="js-loading" />
diff --git a/app/assets/javascripts/releases/components/asset_links_form.vue b/app/assets/javascripts/releases/components/asset_links_form.vue
index 4bdc88f01dd..0698ca5e31f 100644
--- a/app/assets/javascripts/releases/components/asset_links_form.vue
+++ b/app/assets/javascripts/releases/components/asset_links_form.vue
@@ -162,7 +162,7 @@ export default {
:state="isNameValid(link)"
@change="onLinkTitleInput(link.id, $event)"
/>
- <template v-slot:invalid-feedback>
+ <template #invalid-feedback>
<span v-if="hasEmptyName(link)" class="invalid-feedback d-inline">
{{ __('Link title is required') }}
</span>
diff --git a/app/assets/javascripts/releases/components/evidence_block.vue b/app/assets/javascripts/releases/components/evidence_block.vue
index 59c1b3eb48e..acae6fda533 100644
--- a/app/assets/javascripts/releases/components/evidence_block.vue
+++ b/app/assets/javascripts/releases/components/evidence_block.vue
@@ -76,11 +76,13 @@ export default {
</gl-link>
<expand-button>
- <template slot="short">
+ <template #short>
<span class="js-short monospace">{{ shortSha(index) }}</span>
</template>
- <template slot="expanded">
- <span class="js-expanded monospace gl-pl-1">{{ sha(index) }}</span>
+ <template #expanded>
+ <span class="js-expanded monospace gl-pl-1-deprecated-no-really-do-not-use-me">{{
+ sha(index)
+ }}</span>
</template>
</expand-button>
<clipboard-button
diff --git a/app/assets/javascripts/releases/components/release_block_footer.vue b/app/assets/javascripts/releases/components/release_block_footer.vue
index a95fbc0b373..26154272d39 100644
--- a/app/assets/javascripts/releases/components/release_block_footer.vue
+++ b/app/assets/javascripts/releases/components/release_block_footer.vue
@@ -57,6 +57,11 @@ export default {
? sprintf(__("%{username}'s avatar"), { username: this.author.username })
: null;
},
+ createdTime() {
+ const now = new Date();
+ const isFuture = now < new Date(this.releasedAt);
+ return isFuture ? __('Will be created') : __('Created');
+ },
},
};
</script>
@@ -86,7 +91,7 @@ export default {
v-if="releasedAt || author"
class="float-left d-flex align-items-center js-author-date-info"
>
- <span class="text-secondary">{{ __('Created') }}&nbsp;</span>
+ <span class="text-secondary">{{ createdTime }}&nbsp;</span>
<template v-if="releasedAt">
<span
v-gl-tooltip.bottom
diff --git a/app/assets/javascripts/releases/components/release_block_header.vue b/app/assets/javascripts/releases/components/release_block_header.vue
index 6f7e1dcfe2f..ed49841757a 100644
--- a/app/assets/javascripts/releases/components/release_block_header.vue
+++ b/app/assets/javascripts/releases/components/release_block_header.vue
@@ -1,5 +1,5 @@
<script>
-import { GlTooltipDirective, GlLink, GlBadge } from '@gitlab/ui';
+import { GlTooltipDirective, GlLink, GlBadge, GlButton } from '@gitlab/ui';
import Icon from '~/vue_shared/components/icon.vue';
import { BACK_URL_PARAM } from '~/releases/constants';
import { setUrlParams } from '~/lib/utils/url_utility';
@@ -10,6 +10,7 @@ export default {
GlLink,
GlBadge,
Icon,
+ GlButton,
},
directives: {
GlTooltip: GlTooltipDirective,
@@ -41,7 +42,7 @@ export default {
<template>
<div class="card-header d-flex align-items-center bg-white pr-0">
- <h2 class="card-title my-2 mr-auto gl-font-size-20">
+ <h2 class="card-title my-2 mr-auto gl-font-size-20-deprecated-no-really-do-not-use-me">
<gl-link v-if="selfLink" :href="selfLink" class="font-size-inherit">
{{ release.name }}
</gl-link>
@@ -50,14 +51,16 @@ export default {
__('Upcoming Release')
}}</gl-badge>
</h2>
- <gl-link
+ <gl-button
v-if="editLink"
v-gl-tooltip
- class="btn btn-default append-right-10 js-edit-button ml-2"
+ category="primary"
+ variant="default"
+ class="append-right-10 js-edit-button ml-2 pb-2"
:title="__('Edit this release')"
:href="editLink"
>
<icon name="pencil" />
- </gl-link>
+ </gl-button>
</div>
</template>
diff --git a/app/assets/javascripts/releases/components/release_block_metadata.vue b/app/assets/javascripts/releases/components/release_block_metadata.vue
index 052e4088a5f..40133941011 100644
--- a/app/assets/javascripts/releases/components/release_block_metadata.vue
+++ b/app/assets/javascripts/releases/components/release_block_metadata.vue
@@ -38,9 +38,12 @@ export default {
return Boolean(this.author);
},
releasedTimeAgo() {
- return sprintf(__('released %{time}'), {
- time: this.timeFormatted(this.release.releasedAt),
- });
+ const now = new Date();
+ const isFuture = now < new Date(this.release.releasedAt);
+ const time = this.timeFormatted(this.release.releasedAt);
+ return isFuture
+ ? sprintf(__('will be released %{time}'), { time })
+ : sprintf(__('released %{time}'), { time });
},
shouldRenderMilestones() {
return Boolean(this.release.milestones?.length);
@@ -74,7 +77,11 @@ export default {
<div class="append-right-4">
&bull;
- <span v-gl-tooltip.bottom :title="tooltipTitle(release.releasedAt)">
+ <span
+ v-gl-tooltip.bottom
+ class="js-release-date-info"
+ :title="tooltipTitle(release.releasedAt)"
+ >
{{ releasedTimeAgo }}
</span>
</div>
diff --git a/app/assets/javascripts/releases/components/release_block_milestone_info.vue b/app/assets/javascripts/releases/components/release_block_milestone_info.vue
index 01ad0cbf732..d9fbd2884b7 100644
--- a/app/assets/javascripts/releases/components/release_block_milestone_info.vue
+++ b/app/assets/javascripts/releases/components/release_block_milestone_info.vue
@@ -3,7 +3,7 @@ import {
GlProgressBar,
GlLink,
GlBadge,
- GlDeprecatedButton,
+ GlButton,
GlTooltipDirective,
GlSprintf,
} from '@gitlab/ui';
@@ -17,7 +17,7 @@ export default {
GlProgressBar,
GlLink,
GlBadge,
- GlDeprecatedButton,
+ GlButton,
GlSprintf,
},
directives: {
@@ -134,13 +134,9 @@ export default {
<span :key="'bullet-' + milestone.id" class="append-right-4">&bull;</span>
</template>
<template v-if="shouldRenderShowMoreLink(index)">
- <gl-deprecated-button
- :key="'more-button-' + milestone.id"
- variant="link"
- @click="toggleShowAll"
- >
+ <gl-button :key="'more-button-' + milestone.id" variant="link" @click="toggleShowAll">
{{ moreText }}
- </gl-deprecated-button>
+ </gl-button>
</template>
</template>
</div>
diff --git a/app/assets/javascripts/releases/stores/modules/detail/actions.js b/app/assets/javascripts/releases/stores/modules/detail/actions.js
index 7b84c18242c..3bc427dfa16 100644
--- a/app/assets/javascripts/releases/stores/modules/detail/actions.js
+++ b/app/assets/javascripts/releases/stores/modules/detail/actions.js
@@ -18,7 +18,12 @@ export const fetchRelease = ({ dispatch, state }) => {
return api
.release(state.projectId, state.tagName)
- .then(({ data: release }) => {
+ .then(({ data }) => {
+ const release = {
+ ...data,
+ milestones: data.milestones || [],
+ };
+
dispatch('receiveReleaseSuccess', convertObjectPropsToCamelCase(release, { deep: true }));
})
.catch(error => {
@@ -28,6 +33,8 @@ export const fetchRelease = ({ dispatch, state }) => {
export const updateReleaseTitle = ({ commit }, title) => commit(types.UPDATE_RELEASE_TITLE, title);
export const updateReleaseNotes = ({ commit }, notes) => commit(types.UPDATE_RELEASE_NOTES, notes);
+export const updateReleaseMilestones = ({ commit }, milestones) =>
+ commit(types.UPDATE_RELEASE_MILESTONES, milestones);
export const requestUpdateRelease = ({ commit }) => commit(types.REQUEST_UPDATE_RELEASE);
export const receiveUpdateReleaseSuccess = ({ commit, state, rootState }) => {
@@ -45,12 +52,14 @@ export const updateRelease = ({ dispatch, state, getters }) => {
dispatch('requestUpdateRelease');
const { release } = state;
+ const milestones = release.milestones ? release.milestones.map(milestone => milestone.title) : [];
return (
api
.updateRelease(state.projectId, state.tagName, {
name: release.name,
description: release.description,
+ milestones,
})
/**
diff --git a/app/assets/javascripts/releases/stores/modules/detail/mutation_types.js b/app/assets/javascripts/releases/stores/modules/detail/mutation_types.js
index 04944b76e42..1d6356990ce 100644
--- a/app/assets/javascripts/releases/stores/modules/detail/mutation_types.js
+++ b/app/assets/javascripts/releases/stores/modules/detail/mutation_types.js
@@ -4,6 +4,7 @@ export const RECEIVE_RELEASE_ERROR = 'RECEIVE_RELEASE_ERROR';
export const UPDATE_RELEASE_TITLE = 'UPDATE_RELEASE_TITLE';
export const UPDATE_RELEASE_NOTES = 'UPDATE_RELEASE_NOTES';
+export const UPDATE_RELEASE_MILESTONES = 'UPDATE_RELEASE_MILESTONES';
export const REQUEST_UPDATE_RELEASE = 'REQUEST_UPDATE_RELEASE';
export const RECEIVE_UPDATE_RELEASE_SUCCESS = 'RECEIVE_UPDATE_RELEASE_SUCCESS';
diff --git a/app/assets/javascripts/releases/stores/modules/detail/mutations.js b/app/assets/javascripts/releases/stores/modules/detail/mutations.js
index 3d97e3a75c2..5c29b402cba 100644
--- a/app/assets/javascripts/releases/stores/modules/detail/mutations.js
+++ b/app/assets/javascripts/releases/stores/modules/detail/mutations.js
@@ -28,6 +28,10 @@ export default {
state.release.description = notes;
},
+ [types.UPDATE_RELEASE_MILESTONES](state, milestones) {
+ state.release.milestones = milestones;
+ },
+
[types.REQUEST_UPDATE_RELEASE](state) {
state.isUpdatingRelease = true;
},
diff --git a/app/assets/javascripts/releases/stores/modules/detail/state.js b/app/assets/javascripts/releases/stores/modules/detail/state.js
index b513e1bed79..6d0d102c719 100644
--- a/app/assets/javascripts/releases/stores/modules/detail/state.js
+++ b/app/assets/javascripts/releases/stores/modules/detail/state.js
@@ -6,6 +6,8 @@ export default ({
markdownPreviewPath,
updateReleaseApiDocsPath,
releaseAssetsDocsPath,
+ manageMilestonesPath,
+ newMilestonePath,
}) => ({
projectId,
tagName,
@@ -14,6 +16,8 @@ export default ({
markdownPreviewPath,
updateReleaseApiDocsPath,
releaseAssetsDocsPath,
+ manageMilestonesPath,
+ newMilestonePath,
/** The Release object */
release: null,
diff --git a/app/assets/javascripts/reports/accessibility_report/components/accessibility_issue_body.vue b/app/assets/javascripts/reports/accessibility_report/components/accessibility_issue_body.vue
index 6aae9195be1..653dcced98b 100644
--- a/app/assets/javascripts/reports/accessibility_report/components/accessibility_issue_body.vue
+++ b/app/assets/javascripts/reports/accessibility_report/components/accessibility_issue_body.vue
@@ -26,18 +26,11 @@ export default {
* The TECHS code is the "G18", "G168", "H91", etc. from the code which is used for the documentation.
* Here we simply split the string on `.` and get the code in the 5th position
*/
- if (this.issue.code === undefined) {
- return null;
- }
-
- return this.issue.code.split('.')[4] || null;
+ return this.issue.code?.split('.')[4];
},
learnMoreUrl() {
- if (this.parsedTECHSCode === null) {
- return 'https://www.w3.org/TR/WCAG20-TECHS/Overview.html';
- }
-
- return `https://www.w3.org/TR/WCAG20-TECHS/${this.parsedTECHSCode}.html`;
+ // eslint-disable-next-line @gitlab/require-i18n-strings
+ return `https://www.w3.org/TR/WCAG20-TECHS/${this.parsedTECHSCode || 'Overview'}.html`;
},
},
};
@@ -52,10 +45,19 @@ export default {
>
{{ s__('AccessibilityReport|New') }}
</div>
- {{ issue.name }}
- <gl-link ref="accessibility-issue-learn-more" :href="learnMoreUrl" target="_blank">{{
- s__('AccessibilityReport|Learn More')
- }}</gl-link>
+ <div>
+ {{
+ sprintf(
+ s__(
+ 'AccessibilityReport|The accessibility scanning found an error of the following type: %{code}',
+ ),
+ { code: issue.code },
+ )
+ }}
+ <gl-link ref="accessibility-issue-learn-more" :href="learnMoreUrl" target="_blank">{{
+ s__('AccessibilityReport|Learn More')
+ }}</gl-link>
+ </div>
{{ sprintf(s__('AccessibilityReport|Message: %{message}'), { message: issue.message }) }}
</div>
</div>
diff --git a/app/assets/javascripts/reports/accessibility_report/grouped_accessibility_reports_app.vue b/app/assets/javascripts/reports/accessibility_report/grouped_accessibility_reports_app.vue
new file mode 100644
index 00000000000..6f8ddd01df8
--- /dev/null
+++ b/app/assets/javascripts/reports/accessibility_report/grouped_accessibility_reports_app.vue
@@ -0,0 +1,64 @@
+<script>
+import { mapActions, mapGetters } from 'vuex';
+import { componentNames } from '~/reports/components/issue_body';
+import ReportSection from '~/reports/components/report_section.vue';
+import IssuesList from '~/reports/components/issues_list.vue';
+import createStore from './store';
+
+export default {
+ name: 'GroupedAccessibilityReportsApp',
+ store: createStore(),
+ components: {
+ ReportSection,
+ IssuesList,
+ },
+ props: {
+ endpoint: {
+ type: String,
+ required: true,
+ },
+ },
+ componentNames,
+ computed: {
+ ...mapGetters([
+ 'summaryStatus',
+ 'groupedSummaryText',
+ 'shouldRenderIssuesList',
+ 'unresolvedIssues',
+ 'resolvedIssues',
+ 'newIssues',
+ ]),
+ },
+ created() {
+ this.setEndpoint(this.endpoint);
+
+ this.fetchReport();
+ },
+ methods: {
+ ...mapActions(['fetchReport', 'setEndpoint']),
+ },
+};
+</script>
+<template>
+ <report-section
+ :status="summaryStatus"
+ :success-text="groupedSummaryText"
+ :loading-text="groupedSummaryText"
+ :error-text="groupedSummaryText"
+ :has-issues="shouldRenderIssuesList"
+ class="mr-widget-section grouped-security-reports mr-report"
+ >
+ <template #body>
+ <div class="mr-widget-grouped-section report-block">
+ <issues-list
+ v-if="shouldRenderIssuesList"
+ :unresolved-issues="unresolvedIssues"
+ :new-issues="newIssues"
+ :resolved-issues="resolvedIssues"
+ :component="$options.componentNames.AccessibilityIssueBody"
+ class="report-block-group-list"
+ />
+ </div>
+ </template>
+ </report-section>
+</template>
diff --git a/app/assets/javascripts/reports/accessibility_report/store/actions.js b/app/assets/javascripts/reports/accessibility_report/store/actions.js
new file mode 100644
index 00000000000..446cfd79984
--- /dev/null
+++ b/app/assets/javascripts/reports/accessibility_report/store/actions.js
@@ -0,0 +1,79 @@
+import Visibility from 'visibilityjs';
+import Poll from '~/lib/utils/poll';
+import httpStatusCodes from '~/lib/utils/http_status';
+import axios from '~/lib/utils/axios_utils';
+import * as types from './mutation_types';
+
+let eTagPoll;
+
+export const clearEtagPoll = () => {
+ eTagPoll = null;
+};
+
+export const stopPolling = () => {
+ if (eTagPoll) eTagPoll.stop();
+};
+
+export const restartPolling = () => {
+ if (eTagPoll) eTagPoll.restart();
+};
+
+export const setEndpoint = ({ commit }, endpoint) => commit(types.SET_ENDPOINT, endpoint);
+
+/**
+ * We need to poll the report endpoint while they are being parsed in the Backend.
+ * This can take up to one minute.
+ *
+ * Poll.js will handle etag response.
+ * While http status code is 204, it means it's parsing, and we'll keep polling
+ * When http status code is 200, it means parsing is done, we can show the results & stop polling
+ * When http status code is 500, it means parsing went wrong and we stop polling
+ */
+export const fetchReport = ({ state, dispatch, commit }) => {
+ commit(types.REQUEST_REPORT);
+
+ eTagPoll = new Poll({
+ resource: {
+ getReport(endpoint) {
+ return axios.get(endpoint);
+ },
+ },
+ data: state.endpoint,
+ method: 'getReport',
+ successCallback: ({ status, data }) => dispatch('receiveReportSuccess', { status, data }),
+ errorCallback: () => dispatch('receiveReportError'),
+ });
+
+ if (!Visibility.hidden()) {
+ eTagPoll.makeRequest();
+ } else {
+ axios
+ .get(state.endpoint)
+ .then(({ status, data }) => dispatch('receiveReportSuccess', { status, data }))
+ .catch(() => dispatch('receiveReportError'));
+ }
+
+ Visibility.change(() => {
+ if (!Visibility.hidden() && state.isLoading) {
+ dispatch('restartPolling');
+ } else {
+ dispatch('stopPolling');
+ }
+ });
+};
+
+export const receiveReportSuccess = ({ commit, dispatch }, { status, data }) => {
+ if (status === httpStatusCodes.OK) {
+ commit(types.RECEIVE_REPORT_SUCCESS, data);
+ // Stop polling since we have the information already parsed and it won't be changing
+ dispatch('stopPolling');
+ }
+};
+
+export const receiveReportError = ({ commit, dispatch }) => {
+ commit(types.RECEIVE_REPORT_ERROR);
+ dispatch('stopPolling');
+};
+
+// prevent babel-plugin-rewire from generating an invalid default during karma tests
+export default () => {};
diff --git a/app/assets/javascripts/reports/accessibility_report/store/getters.js b/app/assets/javascripts/reports/accessibility_report/store/getters.js
new file mode 100644
index 00000000000..9aff427e644
--- /dev/null
+++ b/app/assets/javascripts/reports/accessibility_report/store/getters.js
@@ -0,0 +1,48 @@
+import { LOADING, ERROR, SUCCESS, STATUS_FAILED } from '../../constants';
+import { s__, n__ } from '~/locale';
+
+export const groupedSummaryText = state => {
+ if (state.isLoading) {
+ return s__('Reports|Accessibility scanning results are being parsed');
+ }
+
+ if (state.hasError) {
+ return s__('Reports|Accessibility scanning failed loading results');
+ }
+
+ const numberOfResults = state.report?.summary?.errored || 0;
+ if (numberOfResults === 0) {
+ return s__('Reports|Accessibility scanning detected no issues for the source branch only');
+ }
+
+ return n__(
+ 'Reports|Accessibility scanning detected %d issue for the source branch only',
+ 'Reports|Accessibility scanning detected %d issues for the source branch only',
+ numberOfResults,
+ );
+};
+
+export const summaryStatus = state => {
+ if (state.isLoading) {
+ return LOADING;
+ }
+
+ if (state.hasError || state.status === STATUS_FAILED) {
+ return ERROR;
+ }
+
+ return SUCCESS;
+};
+
+export const shouldRenderIssuesList = state =>
+ Object.values(state.report).some(x => Array.isArray(x) && x.length > 0);
+
+// We could just map state, but we're going to iterate in the future
+// to add notes and warnings to these issue lists, so I'm going to
+// keep these as getters
+export const unresolvedIssues = state => state.report.existing_errors;
+export const resolvedIssues = state => state.report.resolved_errors;
+export const newIssues = state => state.report.new_errors;
+
+// prevent babel-plugin-rewire from generating an invalid default during karma tests
+export default () => {};
diff --git a/app/assets/javascripts/reports/accessibility_report/store/index.js b/app/assets/javascripts/reports/accessibility_report/store/index.js
new file mode 100644
index 00000000000..047964260ad
--- /dev/null
+++ b/app/assets/javascripts/reports/accessibility_report/store/index.js
@@ -0,0 +1,16 @@
+import Vue from 'vue';
+import Vuex from 'vuex';
+import * as actions from './actions';
+import * as getters from './getters';
+import mutations from './mutations';
+import state from './state';
+
+Vue.use(Vuex);
+
+export default initialState =>
+ new Vuex.Store({
+ actions,
+ getters,
+ mutations,
+ state: state(initialState),
+ });
diff --git a/app/assets/javascripts/reports/accessibility_report/store/mutation_types.js b/app/assets/javascripts/reports/accessibility_report/store/mutation_types.js
new file mode 100644
index 00000000000..22e2330e1ea
--- /dev/null
+++ b/app/assets/javascripts/reports/accessibility_report/store/mutation_types.js
@@ -0,0 +1,5 @@
+export const SET_ENDPOINT = 'SET_ENDPOINT';
+
+export const REQUEST_REPORT = 'REQUEST_REPORT';
+export const RECEIVE_REPORT_SUCCESS = 'RECEIVE_REPORT_SUCCESS';
+export const RECEIVE_REPORT_ERROR = 'RECEIVE_REPORT_ERROR';
diff --git a/app/assets/javascripts/reports/accessibility_report/store/mutations.js b/app/assets/javascripts/reports/accessibility_report/store/mutations.js
new file mode 100644
index 00000000000..20d3e5be9a3
--- /dev/null
+++ b/app/assets/javascripts/reports/accessibility_report/store/mutations.js
@@ -0,0 +1,20 @@
+import * as types from './mutation_types';
+
+export default {
+ [types.SET_ENDPOINT](state, endpoint) {
+ state.endpoint = endpoint;
+ },
+ [types.REQUEST_REPORT](state) {
+ state.isLoading = true;
+ },
+ [types.RECEIVE_REPORT_SUCCESS](state, report) {
+ state.hasError = false;
+ state.isLoading = false;
+ state.report = report;
+ },
+ [types.RECEIVE_REPORT_ERROR](state) {
+ state.isLoading = false;
+ state.hasError = true;
+ state.report = {};
+ },
+};
diff --git a/app/assets/javascripts/reports/accessibility_report/store/state.js b/app/assets/javascripts/reports/accessibility_report/store/state.js
new file mode 100644
index 00000000000..2a4cefea5e6
--- /dev/null
+++ b/app/assets/javascripts/reports/accessibility_report/store/state.js
@@ -0,0 +1,28 @@
+export default (initialState = {}) => ({
+ endpoint: initialState.endpoint || '',
+
+ isLoading: initialState.isLoading || false,
+ hasError: initialState.hasError || false,
+
+ /**
+ * Report will have the following format:
+ * {
+ * status: {String},
+ * summary: {
+ * total: {Number},
+ * resolved: {Number},
+ * errored: {Number},
+ * },
+ * existing_errors: {Array.<Object>},
+ * existing_notes: {Array.<Object>},
+ * existing_warnings: {Array.<Object>},
+ * new_errors: {Array.<Object>},
+ * new_notes: {Array.<Object>},
+ * new_warnings: {Array.<Object>},
+ * resolved_errors: {Array.<Object>},
+ * resolved_notes: {Array.<Object>},
+ * resolved_warnings: {Array.<Object>},
+ * }
+ */
+ report: initialState.report || {},
+});
diff --git a/app/assets/javascripts/reports/components/grouped_issues_list.vue b/app/assets/javascripts/reports/components/grouped_issues_list.vue
new file mode 100644
index 00000000000..97587636644
--- /dev/null
+++ b/app/assets/javascripts/reports/components/grouped_issues_list.vue
@@ -0,0 +1,93 @@
+<script>
+import { s__ } from '~/locale';
+import SmartVirtualList from '~/vue_shared/components/smart_virtual_list.vue';
+import ReportItem from '~/reports/components/report_item.vue';
+
+export default {
+ components: {
+ ReportItem,
+ SmartVirtualList,
+ },
+ props: {
+ component: {
+ type: String,
+ required: false,
+ default: '',
+ },
+ resolvedIssues: {
+ type: Array,
+ required: false,
+ default: () => [],
+ },
+ unresolvedIssues: {
+ type: Array,
+ required: false,
+ default: () => [],
+ },
+ resolvedHeading: {
+ type: String,
+ required: false,
+ default: s__('ciReport|Fixed'),
+ },
+ unresolvedHeading: {
+ type: String,
+ required: false,
+ default: s__('ciReport|New'),
+ },
+ },
+ groups: ['unresolved', 'resolved'],
+ typicalReportItemHeight: 32,
+ maxShownReportItems: 20,
+ computed: {
+ groups() {
+ return this.$options.groups
+ .map(group => ({
+ name: group,
+ issues: this[`${group}Issues`],
+ heading: this[`${group}Heading`],
+ }))
+ .filter(({ issues }) => issues.length > 0);
+ },
+ listLength() {
+ // every group has a header which is rendered as a list item
+ const groupsCount = this.groups.length;
+ const issuesCount = this.groups.reduce(
+ (totalIssues, { issues }) => totalIssues + issues.length,
+ 0,
+ );
+
+ return groupsCount + issuesCount;
+ },
+ },
+};
+</script>
+
+<template>
+ <smart-virtual-list
+ :length="listLength"
+ :remain="$options.maxShownReportItems"
+ :size="$options.typicalReportItemHeight"
+ class="report-block-container"
+ wtag="ul"
+ wclass="report-block-list"
+ >
+ <template v-for="(group, groupIndex) in groups">
+ <h2
+ :key="group.name"
+ :data-testid="`${group.name}Heading`"
+ :class="[groupIndex > 0 ? 'mt-2' : 'mt-0']"
+ class="h5 mb-1"
+ >
+ {{ group.heading }}
+ </h2>
+ <report-item
+ v-for="(issue, issueIndex) in group.issues"
+ :key="`${group.name}-${issue.name}-${group.name}-${issueIndex}`"
+ :issue="issue"
+ :show-report-section-status-icon="false"
+ :component="component"
+ status="none"
+ />
+ </template>
+ </smart-virtual-list>
+</template>
diff --git a/app/assets/javascripts/reports/components/grouped_test_reports_app.vue b/app/assets/javascripts/reports/components/grouped_test_reports_app.vue
index 88d174f96ed..0f7a0e60dc0 100644
--- a/app/assets/javascripts/reports/components/grouped_test_reports_app.vue
+++ b/app/assets/javascripts/reports/components/grouped_test_reports_app.vue
@@ -1,6 +1,6 @@
<script>
import { mapActions, mapGetters, mapState } from 'vuex';
-import { s__ } from '~/locale';
+import { sprintf, s__ } from '~/locale';
import { componentNames } from './issue_body';
import ReportSection from './report_section.vue';
import SummaryRow from './summary_row.vue';
@@ -52,8 +52,17 @@ export default {
methods: {
...mapActions(['setEndpoint', 'fetchReports']),
reportText(report) {
- const summary = report.summary || {};
- return reportTextBuilder(report.name, summary);
+ const { name, summary } = report || {};
+
+ if (report.status === 'error') {
+ return sprintf(s__('Reports|An error occurred while loading %{name} results'), { name });
+ }
+
+ if (!report.name) {
+ return s__('Reports|An error occured while loading report');
+ }
+
+ return reportTextBuilder(name, summary);
},
getReportIcon(report) {
return statusIcon(report.status);
diff --git a/app/assets/javascripts/reports/components/issue_status_icon.vue b/app/assets/javascripts/reports/components/issue_status_icon.vue
index 62a9338b864..d79e3ddd798 100644
--- a/app/assets/javascripts/reports/components/issue_status_icon.vue
+++ b/app/assets/javascripts/reports/components/issue_status_icon.vue
@@ -8,7 +8,6 @@ export default {
Icon,
},
props: {
- // failed || success
status: {
type: String,
required: true,
@@ -27,7 +26,7 @@ export default {
return 'status_success_borderless';
}
- return 'status_created_borderless';
+ return 'dash';
},
isStatusFailed() {
return this.status === STATUS_FAILED;
diff --git a/app/assets/javascripts/reports/components/report_section.vue b/app/assets/javascripts/reports/components/report_section.vue
index 20b0c52dbda..68956fc6d2b 100644
--- a/app/assets/javascripts/reports/components/report_section.vue
+++ b/app/assets/javascripts/reports/components/report_section.vue
@@ -167,7 +167,7 @@ export default {
<div class="media">
<status-icon :status="statusIconName" :size="24" class="align-self-center" />
<div class="media-body d-flex flex-align-self-center align-items-center">
- <div class="js-code-text code-text">
+ <div data-testid="report-section-code-text" class="js-code-text code-text">
<div>
{{ headerText }}
<slot :name="slotName"></slot>
diff --git a/app/assets/javascripts/reports/constants.js b/app/assets/javascripts/reports/constants.js
index 1845b51e6b2..b3905cbfcfb 100644
--- a/app/assets/javascripts/reports/constants.js
+++ b/app/assets/javascripts/reports/constants.js
@@ -22,3 +22,6 @@ export const status = {
ERROR: 'ERROR',
SUCCESS: 'SUCCESS',
};
+
+export const ACCESSIBILITY_ISSUE_ERROR = 'error';
+export const ACCESSIBILITY_ISSUE_WARNING = 'warning';
diff --git a/app/assets/javascripts/reports/store/mutations.js b/app/assets/javascripts/reports/store/mutations.js
index 68f6de3a7ee..35ab72bf694 100644
--- a/app/assets/javascripts/reports/store/mutations.js
+++ b/app/assets/javascripts/reports/store/mutations.js
@@ -8,8 +8,7 @@ export default {
state.isLoading = true;
},
[types.RECEIVE_REPORTS_SUCCESS](state, response) {
- // Make sure to clean previous state in case it was an error
- state.hasError = false;
+ state.hasError = response.suites.some(suite => suite.status === 'error');
state.isLoading = false;
diff --git a/app/assets/javascripts/repository/components/breadcrumbs.vue b/app/assets/javascripts/repository/components/breadcrumbs.vue
index 886e9d76cca..45c343c3f7f 100644
--- a/app/assets/javascripts/repository/components/breadcrumbs.vue
+++ b/app/assets/javascripts/repository/components/breadcrumbs.vue
@@ -242,7 +242,7 @@ export default {
</li>
<li v-if="renderAddToTreeDropdown" class="breadcrumb-item">
<gl-dropdown toggle-class="add-to-tree qa-add-to-tree ml-1">
- <template slot="button-content">
+ <template #button-content>
<span class="sr-only">{{ __('Add to tree') }}</span>
<icon name="plus" :size="16" class="float-left" />
<icon name="chevron-down" :size="16" class="float-left" />
diff --git a/app/assets/javascripts/repository/components/last_commit.vue b/app/assets/javascripts/repository/components/last_commit.vue
index a13f8ac65cf..010fc9a5d1a 100644
--- a/app/assets/javascripts/repository/components/last_commit.vue
+++ b/app/assets/javascripts/repository/components/last_commit.vue
@@ -121,9 +121,8 @@ export default {
:href="commit.webUrl"
:class="{ 'font-italic': !commit.message }"
class="commit-row-message item-title"
- >
- {{ commit.title }}
- </gl-link>
+ v-html="commit.titleHtml"
+ />
<gl-deprecated-button
v-if="commit.description"
:class="{ open: showDescription }"
diff --git a/app/assets/javascripts/repository/components/table/row.vue b/app/assets/javascripts/repository/components/table/row.vue
index f741a6df5d9..34424121390 100644
--- a/app/assets/javascripts/repository/components/table/row.vue
+++ b/app/assets/javascripts/repository/components/table/row.vue
@@ -167,9 +167,8 @@ export default {
:href="commit.commitPath"
:title="commit.message"
class="str-truncated-100 tree-commit-link"
- >
- {{ commit.message }}
- </gl-link>
+ v-html="commit.titleHtml"
+ />
<gl-skeleton-loading v-else :lines="1" class="h-auto" />
</td>
<td class="tree-time-ago text-right cursor-default">
diff --git a/app/assets/javascripts/repository/queries/commit.fragment.graphql b/app/assets/javascripts/repository/queries/commit.fragment.graphql
index 9bb13c475c7..be6897b9a16 100644
--- a/app/assets/javascripts/repository/queries/commit.fragment.graphql
+++ b/app/assets/javascripts/repository/queries/commit.fragment.graphql
@@ -1,6 +1,7 @@
fragment TreeEntryCommit on LogTreeCommit {
sha
message
+ titleHtml
committedDate
commitPath
fileName
diff --git a/app/assets/javascripts/repository/queries/pathLastCommit.query.graphql b/app/assets/javascripts/repository/queries/pathLastCommit.query.graphql
index a22cadf0e8d..f54f09fd647 100644
--- a/app/assets/javascripts/repository/queries/pathLastCommit.query.graphql
+++ b/app/assets/javascripts/repository/queries/pathLastCommit.query.graphql
@@ -5,6 +5,7 @@ query pathLastCommit($projectPath: ID!, $path: String, $ref: String!) {
lastCommit {
sha
title
+ titleHtml
description
message
webUrl
diff --git a/app/assets/javascripts/repository/router.js b/app/assets/javascripts/repository/router.js
index 49e024ca4ff..c5646c32850 100644
--- a/app/assets/javascripts/repository/router.js
+++ b/app/assets/javascripts/repository/router.js
@@ -7,17 +7,28 @@ import TreePage from './pages/tree.vue';
Vue.use(VueRouter);
export default function createRouter(base, baseRef) {
+ const treePathRoute = {
+ component: TreePage,
+ props: route => ({
+ path: route.params.path?.replace(/^\//, '') || '/',
+ }),
+ };
+
return new VueRouter({
mode: 'history',
base: joinPaths(gon.relative_url_root || '', base),
routes: [
{
- path: `(/-)?/tree/${baseRef}/:path*`,
+ name: 'treePathDecoded',
+ // Sometimes the ref needs decoding depending on how the backend sends it to us
+ path: `(/-)?/tree/${decodeURI(baseRef)}/:path*`,
+ ...treePathRoute,
+ },
+ {
name: 'treePath',
- component: TreePage,
- props: route => ({
- path: route.params.path?.replace(/^\//, '') || '/',
- }),
+ // Support without decoding as well just in case the ref doesn't need to be decoded
+ path: `(/-)?/tree/${baseRef}/:path*`,
+ ...treePathRoute,
},
{
path: '/',
diff --git a/app/assets/javascripts/repository/utils/commit.js b/app/assets/javascripts/repository/utils/commit.js
index 3973798605d..90ac01c5874 100644
--- a/app/assets/javascripts/repository/utils/commit.js
+++ b/app/assets/javascripts/repository/utils/commit.js
@@ -3,6 +3,7 @@ export function normalizeData(data, path, extra = () => {}) {
return data.map(d => ({
sha: d.commit.id,
message: d.commit.message,
+ titleHtml: d.commit_title_html,
committedDate: d.commit.committed_date,
commitPath: d.commit_path,
fileName: d.file_name,
diff --git a/app/assets/javascripts/right_sidebar.js b/app/assets/javascripts/right_sidebar.js
index 550ec3cb0d1..0bb33de0234 100644
--- a/app/assets/javascripts/right_sidebar.js
+++ b/app/assets/javascripts/right_sidebar.js
@@ -1,7 +1,6 @@
/* eslint-disable func-names, consistent-return, no-param-reassign */
import $ from 'jquery';
-import _ from 'underscore';
import Cookies from 'js-cookie';
import flash from './flash';
import axios from './lib/utils/axios_utils';
@@ -142,7 +141,7 @@ Sidebar.prototype.sidebarCollapseClicked = function(e) {
};
Sidebar.prototype.openDropdown = function(blockOrName) {
- const $block = _.isString(blockOrName) ? this.getBlock(blockOrName) : blockOrName;
+ const $block = typeof blockOrName === 'string' ? this.getBlock(blockOrName) : blockOrName;
if (!this.isOpen()) {
this.setCollapseAfterUpdate($block);
this.toggleSidebar('open');
diff --git a/app/assets/javascripts/search_autocomplete.js b/app/assets/javascripts/search_autocomplete.js
index 3eaa34c8a93..d8eb981c106 100644
--- a/app/assets/javascripts/search_autocomplete.js
+++ b/app/assets/javascripts/search_autocomplete.js
@@ -1,7 +1,7 @@
/* eslint-disable no-return-assign, consistent-return, class-methods-use-this */
import $ from 'jquery';
-import { escape, throttle } from 'underscore';
+import { escape, throttle } from 'lodash';
import { s__, __ } from '~/locale';
import { getIdenticonBackgroundClass, getIdenticonTitle } from '~/helpers/avatar_helper';
import axios from './lib/utils/axios_utils';
@@ -407,7 +407,7 @@ export class SearchAutocomplete {
disableAutocomplete() {
if (!this.searchInput.hasClass('js-autocomplete-disabled') && this.dropdown.hasClass('show')) {
this.searchInput.addClass('js-autocomplete-disabled');
- this.dropdown.dropdown('toggle');
+ this.dropdownToggle.dropdown('toggle');
this.restoreMenu();
}
}
diff --git a/app/assets/javascripts/serverless/components/area.vue b/app/assets/javascripts/serverless/components/area.vue
index 272c0bd5614..29a61cfbbfe 100644
--- a/app/assets/javascripts/serverless/components/area.vue
+++ b/app/assets/javascripts/serverless/components/area.vue
@@ -138,8 +138,8 @@ export default {
:width="width"
:include-legend-avg-max="false"
>
- <template slot="tooltipTitle">{{ tooltipPopoverTitle }}</template>
- <template slot="tooltipContent">{{ tooltipPopoverContent }}</template>
+ <template #tooltipTitle>{{ tooltipPopoverTitle }}</template>
+ <template #tooltipContent>{{ tooltipPopoverContent }}</template>
</gl-area-chart>
</div>
</template>
diff --git a/app/assets/javascripts/serverless/event_hub.js b/app/assets/javascripts/serverless/event_hub.js
index 0948c2e5352..e31806ad199 100644
--- a/app/assets/javascripts/serverless/event_hub.js
+++ b/app/assets/javascripts/serverless/event_hub.js
@@ -1,3 +1,3 @@
-import Vue from 'vue';
+import createEventHub from '~/helpers/event_hub_factory';
-export default new Vue();
+export default createEventHub();
diff --git a/app/assets/javascripts/set_status_modal/event_hub.js b/app/assets/javascripts/set_status_modal/event_hub.js
index 0948c2e5352..e31806ad199 100644
--- a/app/assets/javascripts/set_status_modal/event_hub.js
+++ b/app/assets/javascripts/set_status_modal/event_hub.js
@@ -1,3 +1,3 @@
-import Vue from 'vue';
+import createEventHub from '~/helpers/event_hub_factory';
-export default new Vue();
+export default createEventHub();
diff --git a/app/assets/javascripts/sidebar/components/assignees/assignees.vue b/app/assets/javascripts/sidebar/components/assignees/assignees.vue
index f16b16a6837..3baf4bf0742 100644
--- a/app/assets/javascripts/sidebar/components/assignees/assignees.vue
+++ b/app/assets/javascripts/sidebar/components/assignees/assignees.vue
@@ -1,6 +1,6 @@
<script>
-import CollapsedAssigneeList from '../assignees/collapsed_assignee_list.vue';
-import UncollapsedAssigneeList from '../assignees/uncollapsed_assignee_list.vue';
+import CollapsedAssigneeList from './collapsed_assignee_list.vue';
+import UncollapsedAssigneeList from './uncollapsed_assignee_list.vue';
export default {
// name: 'Assignees' is a false positive: https://gitlab.com/gitlab-org/frontend/eslint-plugin-i18n/issues/26#possible-false-positives
diff --git a/app/assets/javascripts/sidebar/components/assignees/assignees_realtime.vue b/app/assets/javascripts/sidebar/components/assignees/assignees_realtime.vue
new file mode 100644
index 00000000000..bf0c52b2341
--- /dev/null
+++ b/app/assets/javascripts/sidebar/components/assignees/assignees_realtime.vue
@@ -0,0 +1,75 @@
+<script>
+import { getIdFromGraphQLId } from '~/graphql_shared/utils';
+import query from '~/issuable_sidebar/queries/issue_sidebar.query.graphql';
+import actionCable from '~/actioncable_consumer';
+
+export default {
+ subscription: null,
+ name: 'AssigneesRealtime',
+ props: {
+ mediator: {
+ type: Object,
+ required: true,
+ },
+ issuableIid: {
+ type: String,
+ required: true,
+ },
+ projectPath: {
+ type: String,
+ required: true,
+ },
+ },
+ apollo: {
+ project: {
+ query,
+ variables() {
+ return {
+ iid: this.issuableIid,
+ fullPath: this.projectPath,
+ };
+ },
+ result(data) {
+ this.handleFetchResult(data);
+ },
+ },
+ },
+ mounted() {
+ this.initActionCablePolling();
+ },
+ beforeDestroy() {
+ this.$options.subscription.unsubscribe();
+ },
+ methods: {
+ received(data) {
+ if (data.event === 'updated') {
+ this.$apollo.queries.project.refetch();
+ }
+ },
+ initActionCablePolling() {
+ this.$options.subscription = actionCable.subscriptions.create(
+ {
+ channel: 'IssuesChannel',
+ project_path: this.projectPath,
+ iid: this.issuableIid,
+ },
+ { received: this.received },
+ );
+ },
+ handleFetchResult({ data }) {
+ const { nodes } = data.project.issue.assignees;
+
+ const assignees = nodes.map(n => ({
+ ...n,
+ avatar_url: n.avatarUrl,
+ id: getIdFromGraphQLId(n.id),
+ }));
+
+ this.mediator.store.setAssigneesFromRealtime(assignees);
+ },
+ },
+ render() {
+ return this.$slots.default;
+ },
+};
+</script>
diff --git a/app/assets/javascripts/sidebar/components/assignees/sidebar_assignees.vue b/app/assets/javascripts/sidebar/components/assignees/sidebar_assignees.vue
index ce592720531..0906d5abec3 100644
--- a/app/assets/javascripts/sidebar/components/assignees/sidebar_assignees.vue
+++ b/app/assets/javascripts/sidebar/components/assignees/sidebar_assignees.vue
@@ -3,8 +3,10 @@ import Flash from '~/flash';
import eventHub from '~/sidebar/event_hub';
import Store from '~/sidebar/stores/sidebar_store';
import { refreshUserMergeRequestCounts } from '~/commons/nav/user_merge_requests';
+import glFeatureFlagsMixin from '~/vue_shared/mixins/gl_feature_flags_mixin';
import AssigneeTitle from './assignee_title.vue';
import Assignees from './assignees.vue';
+import AssigneesRealtime from './assignees_realtime.vue';
import { __ } from '~/locale';
export default {
@@ -12,7 +14,9 @@ export default {
components: {
AssigneeTitle,
Assignees,
+ AssigneesRealtime,
},
+ mixins: [glFeatureFlagsMixin()],
props: {
mediator: {
type: Object,
@@ -32,6 +36,14 @@ export default {
required: false,
default: 'issue',
},
+ issuableIid: {
+ type: String,
+ required: true,
+ },
+ projectPath: {
+ type: String,
+ required: true,
+ },
},
data() {
return {
@@ -39,6 +51,12 @@ export default {
loading: false,
};
},
+ computed: {
+ shouldEnableRealtime() {
+ // Note: Realtime is only available on issues right now, future support for MR wil be built later.
+ return this.glFeatures.realTimeIssueSidebar && this.issuableType === 'issue';
+ },
+ },
created() {
this.removeAssignee = this.store.removeAssignee.bind(this.store);
this.addAssignee = this.store.addAssignee.bind(this.store);
@@ -84,6 +102,12 @@ export default {
<template>
<div>
+ <assignees-realtime
+ v-if="shouldEnableRealtime"
+ :issuable-iid="issuableIid"
+ :project-path="projectPath"
+ :mediator="mediator"
+ />
<assignee-title
:number-of-assignees="store.assignees.length"
:loading="loading || store.isFetching.assignees"
diff --git a/app/assets/javascripts/sidebar/components/assignees/uncollapsed_assignee_list.vue b/app/assets/javascripts/sidebar/components/assignees/uncollapsed_assignee_list.vue
index 3d112bba668..fed9e5886c0 100644
--- a/app/assets/javascripts/sidebar/components/assignees/uncollapsed_assignee_list.vue
+++ b/app/assets/javascripts/sidebar/components/assignees/uncollapsed_assignee_list.vue
@@ -66,7 +66,7 @@ export default {
<template>
<assignee-avatar-link
v-if="hasOneUser"
- v-slot="{ user }"
+ #default="{ user }"
tooltip-placement="left"
:tooltip-has-name="false"
:user="firstUser"
diff --git a/app/assets/javascripts/sidebar/components/confidential/confidential_issue_sidebar.vue b/app/assets/javascripts/sidebar/components/confidential/confidential_issue_sidebar.vue
index 5b3c3642290..550a1be1e64 100644
--- a/app/assets/javascripts/sidebar/components/confidential/confidential_issue_sidebar.vue
+++ b/app/assets/javascripts/sidebar/components/confidential/confidential_issue_sidebar.vue
@@ -1,15 +1,16 @@
<script>
+import { mapState } from 'vuex';
import { __ } from '~/locale';
import Flash from '~/flash';
import tooltip from '~/vue_shared/directives/tooltip';
import Icon from '~/vue_shared/components/icon.vue';
import eventHub from '~/sidebar/event_hub';
-import editForm from './edit_form.vue';
+import EditForm from './edit_form.vue';
import recaptchaModalImplementor from '~/vue_shared/mixins/recaptcha_modal_implementor';
export default {
components: {
- editForm,
+ EditForm,
Icon,
},
directives: {
@@ -17,10 +18,6 @@ export default {
},
mixins: [recaptchaModalImplementor],
props: {
- isConfidential: {
- required: true,
- type: Boolean,
- },
isEditable: {
required: true,
type: Boolean,
@@ -36,11 +33,12 @@ export default {
};
},
computed: {
+ ...mapState({ confidential: ({ noteableData }) => noteableData.confidential }),
confidentialityIcon() {
- return this.isConfidential ? 'eye-slash' : 'eye';
+ return this.confidential ? 'eye-slash' : 'eye';
},
tooltipLabel() {
- return this.isConfidential ? __('Confidential') : __('Not confidential');
+ return this.confidential ? __('Confidential') : __('Not confidential');
},
},
created() {
@@ -95,17 +93,16 @@ export default {
data-track-label="right_sidebar"
data-track-property="confidentiality"
@click.prevent="toggleForm"
+ >{{ __('Edit') }}</a
>
- {{ __('Edit') }}
- </a>
</div>
<div class="value sidebar-item-value hide-collapsed">
- <editForm
+ <edit-form
v-if="edit"
- :is-confidential="isConfidential"
+ :is-confidential="confidential"
:update-confidential-attribute="updateConfidentialAttribute"
/>
- <div v-if="!isConfidential" class="no-value sidebar-item-value">
+ <div v-if="!confidential" class="no-value sidebar-item-value">
<icon :size="16" name="eye" aria-hidden="true" class="sidebar-item-icon inline" />
{{ __('Not confidential') }}
</div>
diff --git a/app/assets/javascripts/sidebar/components/confidential/edit_form_buttons.vue b/app/assets/javascripts/sidebar/components/confidential/edit_form_buttons.vue
index 5d0e39e8195..e106afea9f5 100644
--- a/app/assets/javascripts/sidebar/components/confidential/edit_form_buttons.vue
+++ b/app/assets/javascripts/sidebar/components/confidential/edit_form_buttons.vue
@@ -40,7 +40,12 @@ export default {
<button type="button" class="btn btn-default append-right-10" @click="closeForm">
{{ __('Cancel') }}
</button>
- <button type="button" class="btn btn-close" @click.prevent="submitForm">
+ <button
+ type="button"
+ class="btn btn-close"
+ data-testid="confidential-toggle"
+ @click.prevent="submitForm"
+ >
{{ toggleButtonText }}
</button>
</div>
diff --git a/app/assets/javascripts/sidebar/components/participants/participants.vue b/app/assets/javascripts/sidebar/components/participants/participants.vue
index db2e51c3aca..d2904f4157c 100644
--- a/app/assets/javascripts/sidebar/components/participants/participants.vue
+++ b/app/assets/javascripts/sidebar/components/participants/participants.vue
@@ -95,22 +95,18 @@ export default {
@click="onClickCollapsedIcon"
>
<i class="fa fa-users" aria-hidden="true"> </i>
- <gl-loading-icon v-if="loading" class="js-participants-collapsed-loading-icon" />
- <span v-else class="js-participants-collapsed-count"> {{ participantCount }} </span>
+ <gl-loading-icon v-if="loading" />
+ <span v-else data-testid="collapsed-count"> {{ participantCount }} </span>
</div>
<div v-if="showParticipantLabel" class="title hide-collapsed">
- <gl-loading-icon
- v-if="loading"
- :inline="true"
- class="js-participants-expanded-loading-icon"
- />
+ <gl-loading-icon v-if="loading" :inline="true" />
{{ participantLabel }}
</div>
<div class="participants-list hide-collapsed">
<div
v-for="participant in visibleParticipants"
:key="participant.id"
- class="participants-author js-participants-author"
+ class="participants-author"
>
<a :href="participant.web_url" class="author-link">
<user-avatar-image
@@ -125,11 +121,7 @@ export default {
</div>
</div>
<div v-if="hasMoreParticipants" class="participants-more hide-collapsed">
- <button
- type="button"
- class="btn-transparent btn-link js-toggle-participants-button"
- @click="toggleMoreParticipants"
- >
+ <button type="button" class="btn-transparent btn-link" @click="toggleMoreParticipants">
{{ toggleLabel }}
</button>
</div>
diff --git a/app/assets/javascripts/sidebar/lib/sidebar_move_issue.js b/app/assets/javascripts/sidebar/lib/sidebar_move_issue.js
index 2a61f7b5c05..0fb9cf22653 100644
--- a/app/assets/javascripts/sidebar/lib/sidebar_move_issue.js
+++ b/app/assets/javascripts/sidebar/lib/sidebar_move_issue.js
@@ -1,6 +1,6 @@
import $ from 'jquery';
import '~/gl_dropdown';
-import { escape as esc } from 'lodash';
+import { escape } from 'lodash';
import { __ } from '~/locale';
function isValidProjectId(id) {
@@ -49,7 +49,7 @@ class SidebarMoveIssue {
renderRow: project => `
<li>
<a href="#" class="js-move-issue-dropdown-item">
- ${esc(project.name_with_namespace)}
+ ${escape(project.name_with_namespace)}
</a>
</li>
`,
diff --git a/app/assets/javascripts/sidebar/mount_sidebar.js b/app/assets/javascripts/sidebar/mount_sidebar.js
index 6f8214b18ee..e371091fc53 100644
--- a/app/assets/javascripts/sidebar/mount_sidebar.js
+++ b/app/assets/javascripts/sidebar/mount_sidebar.js
@@ -1,5 +1,6 @@
import $ from 'jquery';
import Vue from 'vue';
+import VueApollo from 'vue-apollo';
import SidebarTimeTracking from './components/time_tracking/sidebar_time_tracking.vue';
import SidebarAssignees from './components/assignees/sidebar_assignees.vue';
import ConfidentialIssueSidebar from './components/confidential/confidential_issue_sidebar.vue';
@@ -8,17 +9,29 @@ import LockIssueSidebar from './components/lock/lock_issue_sidebar.vue';
import sidebarParticipants from './components/participants/sidebar_participants.vue';
import sidebarSubscriptions from './components/subscriptions/sidebar_subscriptions.vue';
import Translate from '../vue_shared/translate';
+import createDefaultClient from '~/lib/graphql';
+import { store } from '~/notes/stores';
Vue.use(Translate);
+Vue.use(VueApollo);
+
+function getSidebarOptions() {
+ return JSON.parse(document.querySelector('.js-sidebar-options').innerHTML);
+}
function mountAssigneesComponent(mediator) {
const el = document.getElementById('js-vue-sidebar-assignees');
+ const apolloProvider = new VueApollo({
+ defaultClient: createDefaultClient(),
+ });
if (!el) return;
+ const { iid, fullPath } = getSidebarOptions();
// eslint-disable-next-line no-new
new Vue({
el,
+ apolloProvider,
components: {
SidebarAssignees,
},
@@ -26,6 +39,8 @@ function mountAssigneesComponent(mediator) {
createElement('sidebar-assignees', {
props: {
mediator,
+ issuableIid: String(iid),
+ projectPath: fullPath,
field: el.dataset.field,
signedIn: el.hasAttribute('data-signed-in'),
issuableType: gl.utils.isInIssuePage() ? 'issue' : 'merge_request',
@@ -45,8 +60,8 @@ function mountConfidentialComponent(mediator) {
const ConfidentialComp = Vue.extend(ConfidentialIssueSidebar);
new ConfidentialComp({
+ store,
propsData: {
- isConfidential: initialData.is_confidential,
isEditable: initialData.is_editable,
service: mediator.service,
},
@@ -144,6 +159,4 @@ export function mountSidebar(mediator) {
mountTimeTrackingComponent();
}
-export function getSidebarOptions() {
- return JSON.parse(document.querySelector('.js-sidebar-options').innerHTML);
-}
+export { getSidebarOptions };
diff --git a/app/assets/javascripts/sidebar/stores/sidebar_store.js b/app/assets/javascripts/sidebar/stores/sidebar_store.js
index 66f7f9e3c66..095f93b72a9 100644
--- a/app/assets/javascripts/sidebar/stores/sidebar_store.js
+++ b/app/assets/javascripts/sidebar/stores/sidebar_store.js
@@ -89,6 +89,10 @@ export default class SidebarStore {
this.assignees = [];
}
+ setAssigneesFromRealtime(data) {
+ this.assignees = data;
+ }
+
setAutocompleteProjects(projects) {
this.autocompleteProjects = projects;
}
diff --git a/app/assets/javascripts/snippet/snippet_bundle.js b/app/assets/javascripts/snippet/snippet_bundle.js
index a3ed8d9c632..76a1f6d1458 100644
--- a/app/assets/javascripts/snippet/snippet_bundle.js
+++ b/app/assets/javascripts/snippet/snippet_bundle.js
@@ -3,18 +3,6 @@ import setupCollapsibleInputs from './collapsible_input';
let editor;
-const initAce = () => {
- const editorEl = document.getElementById('editor');
- const form = document.querySelector('.snippet-form-holder form');
- const content = document.querySelector('.snippet-file-content');
-
- editor = initEditorLite({ el: editorEl });
-
- form.addEventListener('submit', () => {
- content.value = editor.getValue();
- });
-};
-
const initMonaco = () => {
const editorEl = document.getElementById('editor');
const contentEl = document.querySelector('.snippet-file-content');
@@ -36,15 +24,7 @@ const initMonaco = () => {
});
};
-export const initEditor = () => {
- if (window?.gon?.features?.monacoSnippets) {
- initMonaco();
- } else {
- initAce();
- }
- setupCollapsibleInputs();
-};
-
export default () => {
- initEditor();
+ initMonaco();
+ setupCollapsibleInputs();
};
diff --git a/app/assets/javascripts/snippets/components/edit.vue b/app/assets/javascripts/snippets/components/edit.vue
index 2185b1d67e4..e8d6c005435 100644
--- a/app/assets/javascripts/snippets/components/edit.vue
+++ b/app/assets/javascripts/snippets/components/edit.vue
@@ -81,7 +81,10 @@ export default {
return this.isUpdating ? __('Saving') : __('Save changes');
},
cancelButtonHref() {
- return this.projectPath ? `/${this.projectPath}/snippets` : `/snippets`;
+ if (this.newSnippet) {
+ return this.projectPath ? `/${this.projectPath}/snippets` : `/snippets`;
+ }
+ return this.snippet.webUrl;
},
titleFieldId() {
return `${this.isProjectSnippet ? 'project' : 'personal'}_snippet_title`;
@@ -173,7 +176,13 @@ export default {
class="loading-animation prepend-top-20 append-bottom-20"
/>
<template v-else>
- <title-field :id="titleFieldId" v-model="snippet.title" required :autofocus="true" />
+ <title-field
+ :id="titleFieldId"
+ v-model="snippet.title"
+ data-qa-selector="snippet_title"
+ required
+ :autofocus="true"
+ />
<snippet-description-edit
:id="descriptionFieldId"
v-model="snippet.description"
@@ -198,12 +207,15 @@ export default {
category="primary"
variant="success"
:disabled="updatePrevented"
+ data-qa-selector="submit_button"
@click="handleFormSubmit"
>{{ saveButtonLabel }}</gl-button
>
</template>
<template #append>
- <gl-button :href="cancelButtonHref">{{ __('Cancel') }}</gl-button>
+ <gl-button data-testid="snippet-cancel-btn" :href="cancelButtonHref">{{
+ __('Cancel')
+ }}</gl-button>
</template>
</form-footer-actions>
</template>
diff --git a/app/assets/javascripts/snippets/components/snippet_blob_edit.vue b/app/assets/javascripts/snippets/components/snippet_blob_edit.vue
index 44b4607e5a9..dd03902417d 100644
--- a/app/assets/javascripts/snippets/components/snippet_blob_edit.vue
+++ b/app/assets/javascripts/snippets/components/snippet_blob_edit.vue
@@ -33,7 +33,11 @@ export default {
<div class="form-group file-editor">
<label>{{ s__('Snippets|File') }}</label>
<div class="file-holder snippet">
- <blob-header-edit :value="fileName" @input="$emit('name-change', $event)" />
+ <blob-header-edit
+ :value="fileName"
+ data-qa-selector="snippet_file_name"
+ @input="$emit('name-change', $event)"
+ />
<gl-loading-icon
v-if="isLoading"
:label="__('Loading snippet')"
diff --git a/app/assets/javascripts/snippets/components/snippet_blob_view.vue b/app/assets/javascripts/snippets/components/snippet_blob_view.vue
index 02a0fc7686d..6b218b21e56 100644
--- a/app/assets/javascripts/snippets/components/snippet_blob_view.vue
+++ b/app/assets/javascripts/snippets/components/snippet_blob_view.vue
@@ -7,7 +7,12 @@ import CloneDropdownButton from '~/vue_shared/components/clone_dropdown.vue';
import GetBlobContent from '../queries/snippet.blob.content.query.graphql';
-import { SIMPLE_BLOB_VIEWER, RICH_BLOB_VIEWER } from '~/blob/components/constants';
+import {
+ SIMPLE_BLOB_VIEWER,
+ RICH_BLOB_VIEWER,
+ BLOB_RENDER_EVENT_LOAD,
+ BLOB_RENDER_EVENT_SHOW_SOURCE,
+} from '~/blob/components/constants';
export default {
components: {
@@ -27,6 +32,16 @@ export default {
},
update: data =>
data.snippets.edges[0].node.blob.richData || data.snippets.edges[0].node.blob.plainData,
+ result() {
+ if (this.activeViewerType === RICH_BLOB_VIEWER) {
+ this.blob.richViewer.renderError = null;
+ } else {
+ this.blob.simpleViewer.renderError = null;
+ }
+ },
+ skip() {
+ return this.viewer.renderError;
+ },
},
},
props: {
@@ -62,9 +77,15 @@ export default {
},
methods: {
switchViewer(newViewer) {
- this.activeViewerType = newViewer;
+ this.activeViewerType = newViewer || SIMPLE_BLOB_VIEWER;
+ },
+ forceQuery() {
+ this.$apollo.queries.blobContent.skip = false;
+ this.$apollo.queries.blobContent.refetch();
},
},
+ BLOB_RENDER_EVENT_LOAD,
+ BLOB_RENDER_EVENT_SHOW_SOURCE,
};
</script>
<template>
@@ -75,12 +96,21 @@ export default {
<template #actions>
<clone-dropdown-button
v-if="canBeCloned"
+ class="mr-2"
:ssh-link="snippet.sshUrlToRepo"
:http-link="snippet.httpUrlToRepo"
+ data-qa-selector="clone_button"
/>
</template>
</blob-header>
- <blob-content :loading="isContentLoading" :content="blobContent" :active-viewer="viewer" />
+ <blob-content
+ :loading="isContentLoading"
+ :content="blobContent"
+ :active-viewer="viewer"
+ :blob="blob"
+ @[$options.BLOB_RENDER_EVENT_LOAD]="forceQuery"
+ @[$options.BLOB_RENDER_EVENT_SHOW_SOURCE]="switchViewer"
+ />
</article>
</div>
</template>
diff --git a/app/assets/javascripts/snippets/components/snippet_description_edit.vue b/app/assets/javascripts/snippets/components/snippet_description_edit.vue
index 6f3a86be8d7..0fe539a5de7 100644
--- a/app/assets/javascripts/snippets/components/snippet_description_edit.vue
+++ b/app/assets/javascripts/snippets/components/snippet_description_edit.vue
@@ -51,9 +51,9 @@ export default {
>
<textarea
slot="textarea"
- class="note-textarea js-gfm-input js-autosize markdown-area
- qa-description-textarea"
+ class="note-textarea js-gfm-input js-autosize markdown-area"
dir="auto"
+ data-qa-selector="snippet_description_field"
data-supports-quick-actions="false"
:value="value"
:aria-label="__('Description')"
diff --git a/app/assets/javascripts/snippets/components/snippet_description_view.vue b/app/assets/javascripts/snippets/components/snippet_description_view.vue
new file mode 100644
index 00000000000..72afcc30be6
--- /dev/null
+++ b/app/assets/javascripts/snippets/components/snippet_description_view.vue
@@ -0,0 +1,21 @@
+<script>
+import MarkdownFieldView from '~/vue_shared/components/markdown/field_view.vue';
+
+export default {
+ components: {
+ MarkdownFieldView,
+ },
+ props: {
+ description: {
+ type: String,
+ required: false,
+ default: '',
+ },
+ },
+};
+</script>
+<template>
+ <markdown-field-view class="snippet-description" data-qa-selector="snippet_description_field">
+ <div class="md js-snippet-description" v-html="description"></div>
+ </markdown-field-view>
+</template>
diff --git a/app/assets/javascripts/snippets/components/snippet_header.vue b/app/assets/javascripts/snippets/components/snippet_header.vue
index 30a23b51bc4..c0967e9093c 100644
--- a/app/assets/javascripts/snippets/components/snippet_header.vue
+++ b/app/assets/javascripts/snippets/components/snippet_header.vue
@@ -10,6 +10,7 @@ import {
GlDropdown,
GlDropdownItem,
GlButton,
+ GlTooltipDirective,
} from '@gitlab/ui';
import TimeAgoTooltip from '~/vue_shared/components/time_ago_tooltip.vue';
@@ -30,6 +31,9 @@ export default {
TimeAgoTooltip,
GlButton,
},
+ directives: {
+ GlTooltip: GlTooltipDirective,
+ },
apollo: {
canCreateSnippet: {
query() {
@@ -43,7 +47,7 @@ export default {
update(data) {
return this.snippet.project
? data.project.userPermissions.createSnippet
- : data.currentUser.userPermissions.createSnippet;
+ : data.currentUser?.userPermissions.createSnippet;
},
},
},
@@ -67,6 +71,10 @@ export default {
condition: this.snippet.userPermissions.updateSnippet,
text: __('Edit'),
href: this.editLink,
+ disabled: this.snippet.blob.binary,
+ title: this.snippet.blob.binary
+ ? __('Snippets with non-text files can only be edited via Git.')
+ : undefined,
},
{
condition: this.snippet.userPermissions.adminSnippet,
@@ -119,7 +127,7 @@ export default {
},
methods: {
redirectToSnippets() {
- window.location.pathname = 'dashboard/snippets';
+ window.location.pathname = `${this.snippet.project?.fullPath || 'dashboard'}/snippets`;
},
closeDeleteModal() {
this.$refs.deleteModal.hide();
@@ -186,18 +194,26 @@ export default {
<div class="detail-page-header-actions">
<div class="d-none d-sm-flex">
<template v-for="(action, index) in personalSnippetActions">
- <gl-button
+ <div
v-if="action.condition"
:key="index"
- :disabled="action.disabled"
- :variant="action.variant"
- :category="action.category"
- :class="action.cssClass"
- :href="action.href"
- @click="action.click ? action.click() : undefined"
+ v-gl-tooltip
+ :title="action.title"
+ class="d-inline-block"
>
- {{ action.text }}
- </gl-button>
+ <gl-button
+ :disabled="action.disabled"
+ :variant="action.variant"
+ :category="action.category"
+ :class="action.cssClass"
+ :href="action.href"
+ data-qa-selector="snippet_action_button"
+ :data-qa-action="action.text"
+ @click="action.click ? action.click() : undefined"
+ >
+ {{ action.text }}
+ </gl-button>
+ </div>
</template>
</div>
<div class="d-block d-sm-none dropdown">
@@ -205,6 +221,8 @@ export default {
<gl-dropdown-item
v-for="(action, index) in personalSnippetActions"
:key="index"
+ :disabled="action.disabled"
+ :title="action.title"
:href="action.href"
@click="action.click ? action.click() : undefined"
>{{ action.text }}</gl-dropdown-item
diff --git a/app/assets/javascripts/snippets/components/snippet_title.vue b/app/assets/javascripts/snippets/components/snippet_title.vue
index 1fc0423a06c..5267c3748ca 100644
--- a/app/assets/javascripts/snippets/components/snippet_title.vue
+++ b/app/assets/javascripts/snippets/components/snippet_title.vue
@@ -1,11 +1,14 @@
<script>
-import TimeAgoTooltip from '~/vue_shared/components/time_ago_tooltip.vue';
import { GlSprintf } from '@gitlab/ui';
+import TimeAgoTooltip from '~/vue_shared/components/time_ago_tooltip.vue';
+import SnippetDescription from './snippet_description_view.vue';
+
export default {
components: {
TimeAgoTooltip,
GlSprintf,
+ SnippetDescription,
},
props: {
snippet: {
@@ -20,9 +23,8 @@ export default {
<h2 class="snippet-title prepend-top-0 mb-3" data-qa-selector="snippet_title">
{{ snippet.title }}
</h2>
- <div v-if="snippet.description" class="description" data-qa-selector="snippet_description">
- <div class="md js-snippet-description" v-html="snippet.descriptionHtml"></div>
- </div>
+
+ <snippet-description v-if="snippet.description" :description="snippet.descriptionHtml" />
<small v-if="snippet.updatedAt !== snippet.createdAt" class="edited-text">
<gl-sprintf :message="__('Edited %{timeago}')">
diff --git a/app/assets/javascripts/snippets/fragments/snippetBase.fragment.graphql b/app/assets/javascripts/snippets/fragments/snippetBase.fragment.graphql
index d793d0b6bb4..e7765dfd8ba 100644
--- a/app/assets/javascripts/snippets/fragments/snippetBase.fragment.graphql
+++ b/app/assets/javascripts/snippets/fragments/snippetBase.fragment.graphql
@@ -17,6 +17,8 @@ fragment SnippetBase on Snippet {
path
rawPath
size
+ externalStorage
+ renderedAsText
simpleViewer {
...BlobViewer
}
diff --git a/app/assets/javascripts/static_site_editor/components/app.vue b/app/assets/javascripts/static_site_editor/components/app.vue
new file mode 100644
index 00000000000..98240aef810
--- /dev/null
+++ b/app/assets/javascripts/static_site_editor/components/app.vue
@@ -0,0 +1,3 @@
+<template>
+ <router-view />
+</template>
diff --git a/app/assets/javascripts/static_site_editor/components/edit_area.vue b/app/assets/javascripts/static_site_editor/components/edit_area.vue
index 921d93669c5..dff21d919a9 100644
--- a/app/assets/javascripts/static_site_editor/components/edit_area.vue
+++ b/app/assets/javascripts/static_site_editor/components/edit_area.vue
@@ -1,18 +1,61 @@
<script>
-import { GlFormTextarea } from '@gitlab/ui';
+import RichContentEditor from '~/vue_shared/components/rich_content_editor/rich_content_editor.vue';
+import PublishToolbar from './publish_toolbar.vue';
+import EditHeader from './edit_header.vue';
export default {
components: {
- GlFormTextarea,
+ RichContentEditor,
+ PublishToolbar,
+ EditHeader,
},
props: {
- value: {
+ title: {
type: String,
required: true,
},
+ content: {
+ type: String,
+ required: true,
+ },
+ savingChanges: {
+ type: Boolean,
+ required: true,
+ },
+ returnUrl: {
+ type: String,
+ required: false,
+ default: '',
+ },
+ },
+ data() {
+ return {
+ editableContent: this.content,
+ saveable: false,
+ };
+ },
+ computed: {
+ modified() {
+ return this.content !== this.editableContent;
+ },
+ },
+ methods: {
+ onSubmit() {
+ this.$emit('submit', { content: this.editableContent });
+ },
},
};
</script>
<template>
- <gl-form-textarea :value="value" v-on="$listeners" />
+ <div class="d-flex flex-grow-1 flex-column">
+ <edit-header class="py-2" :title="title" />
+ <rich-content-editor v-model="editableContent" class="mb-9" />
+ <publish-toolbar
+ class="gl-fixed gl-left-0 gl-bottom-0 gl-w-full"
+ :return-url="returnUrl"
+ :saveable="modified"
+ :saving-changes="savingChanges"
+ @submit="onSubmit"
+ />
+ </div>
</template>
diff --git a/app/assets/javascripts/static_site_editor/components/publish_toolbar.vue b/app/assets/javascripts/static_site_editor/components/publish_toolbar.vue
index 274d2f71749..6cd2a4dd700 100644
--- a/app/assets/javascripts/static_site_editor/components/publish_toolbar.vue
+++ b/app/assets/javascripts/static_site_editor/components/publish_toolbar.vue
@@ -1,10 +1,9 @@
<script>
-import { GlButton, GlLoadingIcon } from '@gitlab/ui';
+import { GlButton } from '@gitlab/ui';
export default {
components: {
GlButton,
- GlLoadingIcon,
},
props: {
returnUrl: {
@@ -26,14 +25,18 @@ export default {
};
</script>
<template>
- <div class="d-flex bg-light border-top justify-content-between align-items-center py-3 px-4">
- <gl-loading-icon :class="{ invisible: !savingChanges }" size="md" />
+ <div class="d-flex bg-light border-top justify-content-end align-items-center py-3 px-4">
<div>
<gl-button v-if="returnUrl" ref="returnUrlLink" :href="returnUrl">{{
s__('StaticSiteEditor|Return to site')
}}</gl-button>
- <gl-button variant="success" :disabled="!saveable || savingChanges" @click="$emit('submit')">
- {{ __('Submit Changes') }}
+ <gl-button
+ variant="success"
+ :disabled="!saveable"
+ :loading="savingChanges"
+ @click="$emit('submit')"
+ >
+ <span>{{ __('Submit Changes') }}</span>
</gl-button>
</div>
</div>
diff --git a/app/assets/javascripts/static_site_editor/components/saved_changes_message.vue b/app/assets/javascripts/static_site_editor/components/saved_changes_message.vue
index 41cb901720c..dd907570114 100644
--- a/app/assets/javascripts/static_site_editor/components/saved_changes_message.vue
+++ b/app/assets/javascripts/static_site_editor/components/saved_changes_message.vue
@@ -28,7 +28,8 @@ export default {
},
returnUrl: {
type: String,
- required: true,
+ required: false,
+ default: '',
},
},
};
@@ -46,7 +47,7 @@ export default {
}}
</p>
<div class="d-flex justify-content-end">
- <gl-button ref="returnToSiteButton" :href="returnUrl">{{
+ <gl-button v-if="returnUrl" ref="returnToSiteButton" :href="returnUrl">{{
s__('StaticSiteEditor|Return to site')
}}</gl-button>
<gl-button ref="mergeRequestButton" class="ml-2" :href="mergeRequest.url" variant="success">
@@ -60,7 +61,7 @@ export default {
<ul>
<li>
{{ s__('StaticSiteEditor|You created a new branch:') }}
- <span ref="branchLink">{{ branch.label }}</span>
+ <gl-link ref="branchLink" :href="branch.url">{{ branch.label }}</gl-link>
</li>
<li>
{{ s__('StaticSiteEditor|You created a merge request:') }}
diff --git a/app/assets/javascripts/static_site_editor/components/skeleton_loader.vue b/app/assets/javascripts/static_site_editor/components/skeleton_loader.vue
new file mode 100644
index 00000000000..1b6179883aa
--- /dev/null
+++ b/app/assets/javascripts/static_site_editor/components/skeleton_loader.vue
@@ -0,0 +1,19 @@
+<script>
+import { GlSkeletonLoader } from '@gitlab/ui';
+
+export default {
+ components: {
+ GlSkeletonLoader,
+ },
+};
+</script>
+<template>
+ <gl-skeleton-loader :width="500" :height="102">
+ <rect width="500" height="16" rx="4" />
+ <rect y="20" width="375" height="16" rx="4" />
+ <rect x="380" y="20" width="120" height="16" rx="4" />
+ <rect y="40" width="250" height="16" rx="4" />
+ <rect x="255" y="40" width="150" height="16" rx="4" />
+ <rect x="410" y="40" width="90" height="16" rx="4" />
+ </gl-skeleton-loader>
+</template>
diff --git a/app/assets/javascripts/static_site_editor/components/static_site_editor.vue b/app/assets/javascripts/static_site_editor/components/static_site_editor.vue
deleted file mode 100644
index 82917319fc3..00000000000
--- a/app/assets/javascripts/static_site_editor/components/static_site_editor.vue
+++ /dev/null
@@ -1,95 +0,0 @@
-<script>
-import { mapState, mapGetters, mapActions } from 'vuex';
-import { GlSkeletonLoader } from '@gitlab/ui';
-
-import EditArea from './edit_area.vue';
-import EditHeader from './edit_header.vue';
-import SavedChangesMessage from './saved_changes_message.vue';
-import Toolbar from './publish_toolbar.vue';
-import InvalidContentMessage from './invalid_content_message.vue';
-import SubmitChangesError from './submit_changes_error.vue';
-
-export default {
- components: {
- EditArea,
- EditHeader,
- InvalidContentMessage,
- GlSkeletonLoader,
- SavedChangesMessage,
- Toolbar,
- SubmitChangesError,
- },
- computed: {
- ...mapState([
- 'content',
- 'isLoadingContent',
- 'isSavingChanges',
- 'isContentLoaded',
- 'isSupportedContent',
- 'returnUrl',
- 'title',
- 'submitChangesError',
- 'savedContentMeta',
- ]),
- ...mapGetters(['contentChanged']),
- },
- mounted() {
- if (this.isSupportedContent) {
- this.loadContent();
- }
- },
- methods: {
- ...mapActions(['loadContent', 'setContent', 'submitChanges', 'dismissSubmitChangesError']),
- },
-};
-</script>
-<template>
- <div class="d-flex justify-content-center h-100 pt-2">
- <!-- Success view -->
- <saved-changes-message
- v-if="savedContentMeta"
- :branch="savedContentMeta.branch"
- :commit="savedContentMeta.commit"
- :merge-request="savedContentMeta.mergeRequest"
- :return-url="returnUrl"
- />
-
- <!-- Main view -->
- <template v-else-if="isSupportedContent">
- <div v-if="isLoadingContent" class="w-50 h-50">
- <gl-skeleton-loader :width="500" :height="102">
- <rect width="500" height="16" rx="4" />
- <rect y="20" width="375" height="16" rx="4" />
- <rect x="380" y="20" width="120" height="16" rx="4" />
- <rect y="40" width="250" height="16" rx="4" />
- <rect x="255" y="40" width="150" height="16" rx="4" />
- <rect x="410" y="40" width="90" height="16" rx="4" />
- </gl-skeleton-loader>
- </div>
- <div v-if="isContentLoaded" class="d-flex flex-grow-1 flex-column">
- <submit-changes-error
- v-if="submitChangesError"
- class="w-75 align-self-center"
- :error="submitChangesError"
- @retry="submitChanges"
- @dismiss="dismissSubmitChangesError"
- />
- <edit-header class="w-75 align-self-center py-2" :title="title" />
- <edit-area
- class="w-75 h-100 shadow-none align-self-center"
- :value="content"
- @input="setContent"
- />
- <toolbar
- :return-url="returnUrl"
- :saveable="contentChanged"
- :saving-changes="isSavingChanges"
- @submit="submitChanges"
- />
- </div>
- </template>
-
- <!-- Error view -->
- <invalid-content-message v-else class="w-75" />
- </div>
-</template>
diff --git a/app/assets/javascripts/static_site_editor/constants.js b/app/assets/javascripts/static_site_editor/constants.js
index d7ce2a93a56..4794cf5eead 100644
--- a/app/assets/javascripts/static_site_editor/constants.js
+++ b/app/assets/javascripts/static_site_editor/constants.js
@@ -1,4 +1,4 @@
-import { s__ } from '~/locale';
+import { s__, __ } from '~/locale';
export const BRANCH_SUFFIX_COUNT = 8;
export const DEFAULT_TARGET_BRANCH = 'master';
@@ -10,5 +10,10 @@ export const SUBMIT_CHANGES_COMMIT_ERROR = s__(
export const SUBMIT_CHANGES_MERGE_REQUEST_ERROR = s__(
'StaticSiteEditor|Could not create merge request.',
);
+export const LOAD_CONTENT_ERROR = __(
+ 'An error ocurred while loading your content. Please try again.',
+);
export const DEFAULT_HEADING = s__('StaticSiteEditor|Static site editor');
+
+export const TRACKING_ACTION_CREATE_COMMIT = 'create_commit';
diff --git a/app/assets/javascripts/static_site_editor/graphql/index.js b/app/assets/javascripts/static_site_editor/graphql/index.js
new file mode 100644
index 00000000000..0a5d8c07ad9
--- /dev/null
+++ b/app/assets/javascripts/static_site_editor/graphql/index.js
@@ -0,0 +1,39 @@
+import Vue from 'vue';
+import VueApollo from 'vue-apollo';
+import createDefaultClient from '~/lib/graphql';
+import typeDefs from './typedefs.graphql';
+import fileResolver from './resolvers/file';
+import submitContentChangesResolver from './resolvers/submit_content_changes';
+
+Vue.use(VueApollo);
+
+const createApolloProvider = appData => {
+ const defaultClient = createDefaultClient(
+ {
+ Project: {
+ file: fileResolver,
+ },
+ Mutation: {
+ submitContentChanges: submitContentChangesResolver,
+ },
+ },
+ {
+ typeDefs,
+ },
+ );
+
+ defaultClient.cache.writeData({
+ data: {
+ appData: {
+ __typename: 'AppData',
+ ...appData,
+ },
+ },
+ });
+
+ return new VueApollo({
+ defaultClient,
+ });
+};
+
+export default createApolloProvider;
diff --git a/app/assets/javascripts/static_site_editor/graphql/mutations/submit_content_changes.mutation.graphql b/app/assets/javascripts/static_site_editor/graphql/mutations/submit_content_changes.mutation.graphql
new file mode 100644
index 00000000000..2840d419966
--- /dev/null
+++ b/app/assets/javascripts/static_site_editor/graphql/mutations/submit_content_changes.mutation.graphql
@@ -0,0 +1,7 @@
+mutation submitContentChanges($input: SubmitContentChangesInput) {
+ submitContentChanges(input: $input) @client {
+ branch
+ commit
+ mergeRequest
+ }
+}
diff --git a/app/assets/javascripts/static_site_editor/graphql/queries/app_data.query.graphql b/app/assets/javascripts/static_site_editor/graphql/queries/app_data.query.graphql
new file mode 100644
index 00000000000..fdbf4459aee
--- /dev/null
+++ b/app/assets/javascripts/static_site_editor/graphql/queries/app_data.query.graphql
@@ -0,0 +1,9 @@
+query appData {
+ appData @client {
+ isSupportedContent
+ project
+ sourcePath
+ username,
+ returnUrl
+ }
+}
diff --git a/app/assets/javascripts/static_site_editor/graphql/queries/saved_content_meta.query.graphql b/app/assets/javascripts/static_site_editor/graphql/queries/saved_content_meta.query.graphql
new file mode 100644
index 00000000000..c29b6f93b81
--- /dev/null
+++ b/app/assets/javascripts/static_site_editor/graphql/queries/saved_content_meta.query.graphql
@@ -0,0 +1,3 @@
+query savedContentMeta {
+ savedContentMeta @client
+}
diff --git a/app/assets/javascripts/static_site_editor/graphql/queries/source_content.query.graphql b/app/assets/javascripts/static_site_editor/graphql/queries/source_content.query.graphql
new file mode 100644
index 00000000000..e36d244ae57
--- /dev/null
+++ b/app/assets/javascripts/static_site_editor/graphql/queries/source_content.query.graphql
@@ -0,0 +1,9 @@
+query sourceContent($project: ID!, $sourcePath: String!) {
+ project(fullPath: $project) {
+ fullPath,
+ file(path: $sourcePath) @client {
+ title
+ content
+ }
+ }
+}
diff --git a/app/assets/javascripts/static_site_editor/graphql/resolvers/file.js b/app/assets/javascripts/static_site_editor/graphql/resolvers/file.js
new file mode 100644
index 00000000000..16f176581cb
--- /dev/null
+++ b/app/assets/javascripts/static_site_editor/graphql/resolvers/file.js
@@ -0,0 +1,11 @@
+import loadSourceContent from '../../services/load_source_content';
+
+const fileResolver = ({ fullPath: projectId }, { path: sourcePath }) => {
+ return loadSourceContent({ projectId, sourcePath }).then(sourceContent => ({
+ // eslint-disable-next-line @gitlab/require-i18n-strings
+ __typename: 'File',
+ ...sourceContent,
+ }));
+};
+
+export default fileResolver;
diff --git a/app/assets/javascripts/static_site_editor/graphql/resolvers/submit_content_changes.js b/app/assets/javascripts/static_site_editor/graphql/resolvers/submit_content_changes.js
new file mode 100644
index 00000000000..6c4e3a4d973
--- /dev/null
+++ b/app/assets/javascripts/static_site_editor/graphql/resolvers/submit_content_changes.js
@@ -0,0 +1,24 @@
+import submitContentChanges from '../../services/submit_content_changes';
+import savedContentMetaQuery from '../queries/saved_content_meta.query.graphql';
+
+const submitContentChangesResolver = (
+ _,
+ { input: { project: projectId, username, sourcePath, content } },
+ { cache },
+) => {
+ return submitContentChanges({ projectId, username, sourcePath, content }).then(
+ savedContentMeta => {
+ cache.writeQuery({
+ query: savedContentMetaQuery,
+ data: {
+ savedContentMeta: {
+ __typename: 'SavedContentMeta',
+ ...savedContentMeta,
+ },
+ },
+ });
+ },
+ );
+};
+
+export default submitContentChangesResolver;
diff --git a/app/assets/javascripts/static_site_editor/graphql/typedefs.graphql b/app/assets/javascripts/static_site_editor/graphql/typedefs.graphql
new file mode 100644
index 00000000000..59da2e27144
--- /dev/null
+++ b/app/assets/javascripts/static_site_editor/graphql/typedefs.graphql
@@ -0,0 +1,43 @@
+type File {
+ title: String
+ content: String!
+}
+
+type SavedContentField {
+ label: String!
+ url: String!
+}
+
+type SavedContentMeta {
+ mergeRequest: SavedContentField!
+ commit: SavedContentField!
+ branch: SavedContentField!
+}
+
+type AppData {
+ isSupportedContent: Boolean!
+ project: String!
+ returnUrl: String
+ sourcePath: String!
+ username: String!
+}
+
+type SubmitContentChangesInput {
+ project: String!
+ sourcePath: String!
+ content: String!
+ username: String!
+}
+
+extend type Project {
+ file(path: ID!): File
+}
+
+extend type Query {
+ appData: AppData!
+ savedContentMeta: SavedContentMeta
+}
+
+extend type Mutation {
+ submitContentChanges(input: SubmitContentChangesInput!): SavedContentMeta
+}
diff --git a/app/assets/javascripts/static_site_editor/index.js b/app/assets/javascripts/static_site_editor/index.js
index 15d668fd431..12aa301e02f 100644
--- a/app/assets/javascripts/static_site_editor/index.js
+++ b/app/assets/javascripts/static_site_editor/index.js
@@ -1,29 +1,32 @@
import Vue from 'vue';
-import StaticSiteEditor from './components/static_site_editor.vue';
-import createStore from './store';
+import { parseBoolean } from '~/lib/utils/common_utils';
+import App from './components/app.vue';
+import createRouter from './router';
+import createApolloProvider from './graphql';
const initStaticSiteEditor = el => {
- const { projectId, path: sourcePath, returnUrl } = el.dataset;
- const isSupportedContent = 'isSupportedContent' in el.dataset;
+ const { isSupportedContent, path: sourcePath, baseUrl, namespace, project } = el.dataset;
+ const { current_username: username } = window.gon;
+ const returnUrl = el.dataset.returnUrl || null;
- const store = createStore({
- initialState: {
- isSupportedContent,
- projectId,
- returnUrl,
- sourcePath,
- username: window.gon.current_username,
- },
+ const router = createRouter(baseUrl);
+ const apolloProvider = createApolloProvider({
+ isSupportedContent: parseBoolean(isSupportedContent),
+ project: `${namespace}/${project}`,
+ returnUrl,
+ sourcePath,
+ username,
});
return new Vue({
el,
- store,
+ router,
+ apolloProvider,
components: {
- StaticSiteEditor,
+ App,
},
render(createElement) {
- return createElement('static-site-editor', StaticSiteEditor);
+ return createElement('app');
},
});
};
diff --git a/app/assets/javascripts/static_site_editor/pages/home.vue b/app/assets/javascripts/static_site_editor/pages/home.vue
new file mode 100644
index 00000000000..f65b648acd6
--- /dev/null
+++ b/app/assets/javascripts/static_site_editor/pages/home.vue
@@ -0,0 +1,120 @@
+<script>
+import SkeletonLoader from '../components/skeleton_loader.vue';
+import EditArea from '../components/edit_area.vue';
+import InvalidContentMessage from '../components/invalid_content_message.vue';
+import SubmitChangesError from '../components/submit_changes_error.vue';
+import appDataQuery from '../graphql/queries/app_data.query.graphql';
+import sourceContentQuery from '../graphql/queries/source_content.query.graphql';
+import submitContentChangesMutation from '../graphql/mutations/submit_content_changes.mutation.graphql';
+import createFlash from '~/flash';
+import { LOAD_CONTENT_ERROR } from '../constants';
+import { SUCCESS_ROUTE } from '../router/constants';
+
+export default {
+ components: {
+ SkeletonLoader,
+ EditArea,
+ InvalidContentMessage,
+ SubmitChangesError,
+ },
+ apollo: {
+ appData: {
+ query: appDataQuery,
+ },
+ sourceContent: {
+ query: sourceContentQuery,
+ update: ({
+ project: {
+ file: { title, content },
+ },
+ }) => {
+ return { title, content };
+ },
+ variables() {
+ return {
+ project: this.appData.project,
+ sourcePath: this.appData.sourcePath,
+ };
+ },
+ skip() {
+ return !this.appData.isSupportedContent;
+ },
+ error() {
+ createFlash(LOAD_CONTENT_ERROR);
+ },
+ },
+ },
+ data() {
+ return {
+ content: null,
+ submitChangesError: null,
+ isSavingChanges: false,
+ };
+ },
+ computed: {
+ isLoadingContent() {
+ return this.$apollo.queries.sourceContent.loading;
+ },
+ isContentLoaded() {
+ return Boolean(this.sourceContent);
+ },
+ },
+ methods: {
+ onDismissError() {
+ this.submitChangesError = null;
+ },
+ onSubmit({ content }) {
+ this.content = content;
+ this.submitChanges();
+ },
+ submitChanges() {
+ this.isSavingChanges = true;
+
+ this.$apollo
+ .mutate({
+ mutation: submitContentChangesMutation,
+ variables: {
+ input: {
+ project: this.appData.project,
+ username: this.appData.username,
+ sourcePath: this.appData.sourcePath,
+ content: this.content,
+ },
+ },
+ })
+ .then(() => {
+ this.$router.push(SUCCESS_ROUTE);
+ })
+ .catch(e => {
+ this.submitChangesError = e.message;
+ })
+ .finally(() => {
+ this.isSavingChanges = false;
+ });
+ },
+ },
+};
+</script>
+<template>
+ <div class="container d-flex gl-flex-direction-column pt-2 h-100">
+ <template v-if="appData.isSupportedContent">
+ <skeleton-loader v-if="isLoadingContent" class="w-75 gl-align-self-center gl-mt-5" />
+ <submit-changes-error
+ v-if="submitChangesError"
+ :error="submitChangesError"
+ @retry="submitChanges"
+ @dismiss="onDismissError"
+ />
+ <edit-area
+ v-if="isContentLoaded"
+ :title="sourceContent.title"
+ :content="sourceContent.content"
+ :saving-changes="isSavingChanges"
+ :return-url="appData.returnUrl"
+ @submit="onSubmit"
+ />
+ </template>
+
+ <invalid-content-message v-else class="w-75" />
+ </div>
+</template>
diff --git a/app/assets/javascripts/static_site_editor/pages/success.vue b/app/assets/javascripts/static_site_editor/pages/success.vue
new file mode 100644
index 00000000000..123683b2833
--- /dev/null
+++ b/app/assets/javascripts/static_site_editor/pages/success.vue
@@ -0,0 +1,35 @@
+<script>
+import savedContentMetaQuery from '../graphql/queries/saved_content_meta.query.graphql';
+import appDataQuery from '../graphql/queries/app_data.query.graphql';
+import SavedChangesMessage from '../components/saved_changes_message.vue';
+import { HOME_ROUTE } from '../router/constants';
+
+export default {
+ components: {
+ SavedChangesMessage,
+ },
+ apollo: {
+ savedContentMeta: {
+ query: savedContentMetaQuery,
+ },
+ appData: {
+ query: appDataQuery,
+ },
+ },
+ created() {
+ if (!this.savedContentMeta) {
+ this.$router.push(HOME_ROUTE);
+ }
+ },
+};
+</script>
+<template>
+ <div v-if="savedContentMeta" class="container">
+ <saved-changes-message
+ :branch="savedContentMeta.branch"
+ :commit="savedContentMeta.commit"
+ :merge-request="savedContentMeta.mergeRequest"
+ :return-url="appData.returnUrl"
+ />
+ </div>
+</template>
diff --git a/app/assets/javascripts/static_site_editor/router/constants.js b/app/assets/javascripts/static_site_editor/router/constants.js
new file mode 100644
index 00000000000..fd715f918ce
--- /dev/null
+++ b/app/assets/javascripts/static_site_editor/router/constants.js
@@ -0,0 +1,2 @@
+export const HOME_ROUTE = { name: 'home' };
+export const SUCCESS_ROUTE = { name: 'success' };
diff --git a/app/assets/javascripts/static_site_editor/router/index.js b/app/assets/javascripts/static_site_editor/router/index.js
new file mode 100644
index 00000000000..12692612bbc
--- /dev/null
+++ b/app/assets/javascripts/static_site_editor/router/index.js
@@ -0,0 +1,15 @@
+import Vue from 'vue';
+import VueRouter from 'vue-router';
+import routes from './routes';
+
+Vue.use(VueRouter);
+
+export default function createRouter(base) {
+ const router = new VueRouter({
+ base,
+ mode: 'history',
+ routes,
+ });
+
+ return router;
+}
diff --git a/app/assets/javascripts/static_site_editor/router/routes.js b/app/assets/javascripts/static_site_editor/router/routes.js
new file mode 100644
index 00000000000..6fb9dbe0182
--- /dev/null
+++ b/app/assets/javascripts/static_site_editor/router/routes.js
@@ -0,0 +1,21 @@
+import Home from '../pages/home.vue';
+import Success from '../pages/success.vue';
+
+import { HOME_ROUTE, SUCCESS_ROUTE } from './constants';
+
+export default [
+ {
+ ...HOME_ROUTE,
+ path: '/',
+ component: Home,
+ },
+ {
+ ...SUCCESS_ROUTE,
+ path: '/success',
+ component: Success,
+ },
+ {
+ path: '*',
+ redirect: HOME_ROUTE,
+ },
+];
diff --git a/app/assets/javascripts/static_site_editor/services/submit_content_changes.js b/app/assets/javascripts/static_site_editor/services/submit_content_changes.js
index ff591e4b245..49135d2141b 100644
--- a/app/assets/javascripts/static_site_editor/services/submit_content_changes.js
+++ b/app/assets/javascripts/static_site_editor/services/submit_content_changes.js
@@ -1,4 +1,5 @@
import Api from '~/api';
+import Tracking from '~/tracking';
import { s__, sprintf } from '~/locale';
import { convertObjectPropsToSnakeCase } from '~/lib/utils/common_utils';
import generateBranchName from '~/static_site_editor/services/generate_branch_name';
@@ -8,6 +9,7 @@ import {
SUBMIT_CHANGES_BRANCH_ERROR,
SUBMIT_CHANGES_COMMIT_ERROR,
SUBMIT_CHANGES_MERGE_REQUEST_ERROR,
+ TRACKING_ACTION_CREATE_COMMIT,
} from '../constants';
const createBranch = (projectId, branch) =>
@@ -18,8 +20,10 @@ const createBranch = (projectId, branch) =>
throw new Error(SUBMIT_CHANGES_BRANCH_ERROR);
});
-const commitContent = (projectId, message, branch, sourcePath, content) =>
- Api.commitMultiple(
+const commitContent = (projectId, message, branch, sourcePath, content) => {
+ Tracking.event(document.body.dataset.page, TRACKING_ACTION_CREATE_COMMIT);
+
+ return Api.commitMultiple(
projectId,
convertObjectPropsToSnakeCase({
branch,
@@ -35,6 +39,7 @@ const commitContent = (projectId, message, branch, sourcePath, content) =>
).catch(() => {
throw new Error(SUBMIT_CHANGES_COMMIT_ERROR);
});
+};
const createMergeRequest = (projectId, title, sourceBranch, targetBranch = DEFAULT_TARGET_BRANCH) =>
Api.createProjectMergeRequest(
@@ -56,8 +61,8 @@ const submitContentChanges = ({ username, projectId, sourcePath, content }) => {
const meta = {};
return createBranch(projectId, branch)
- .then(() => {
- Object.assign(meta, { branch: { label: branch } });
+ .then(({ data: { web_url: url } }) => {
+ Object.assign(meta, { branch: { label: branch, url } });
return commitContent(projectId, mergeRequestTitle, branch, sourcePath, content);
})
@@ -67,7 +72,7 @@ const submitContentChanges = ({ username, projectId, sourcePath, content }) => {
return createMergeRequest(projectId, mergeRequestTitle, branch);
})
.then(({ data: { iid: label, web_url: url } }) => {
- Object.assign(meta, { mergeRequest: { label, url } });
+ Object.assign(meta, { mergeRequest: { label: label.toString(), url } });
return meta;
});
diff --git a/app/assets/javascripts/static_site_editor/store/actions.js b/app/assets/javascripts/static_site_editor/store/actions.js
deleted file mode 100644
index 9f5e9e8c589..00000000000
--- a/app/assets/javascripts/static_site_editor/store/actions.js
+++ /dev/null
@@ -1,37 +0,0 @@
-import createFlash from '~/flash';
-import { __ } from '~/locale';
-
-import * as mutationTypes from './mutation_types';
-import loadSourceContent from '~/static_site_editor/services/load_source_content';
-import submitContentChanges from '~/static_site_editor/services/submit_content_changes';
-
-export const loadContent = ({ commit, state: { sourcePath, projectId } }) => {
- commit(mutationTypes.LOAD_CONTENT);
-
- return loadSourceContent({ sourcePath, projectId })
- .then(data => commit(mutationTypes.RECEIVE_CONTENT_SUCCESS, data))
- .catch(() => {
- commit(mutationTypes.RECEIVE_CONTENT_ERROR);
- createFlash(__('An error ocurred while loading your content. Please try again.'));
- });
-};
-
-export const setContent = ({ commit }, content) => {
- commit(mutationTypes.SET_CONTENT, content);
-};
-
-export const submitChanges = ({ state: { projectId, content, sourcePath, username }, commit }) => {
- commit(mutationTypes.SUBMIT_CHANGES);
-
- return submitContentChanges({ content, projectId, sourcePath, username })
- .then(data => commit(mutationTypes.SUBMIT_CHANGES_SUCCESS, data))
- .catch(error => {
- commit(mutationTypes.SUBMIT_CHANGES_ERROR, error.message);
- });
-};
-
-export const dismissSubmitChangesError = ({ commit }) => {
- commit(mutationTypes.DISMISS_SUBMIT_CHANGES_ERROR);
-};
-
-export default () => {};
diff --git a/app/assets/javascripts/static_site_editor/store/getters.js b/app/assets/javascripts/static_site_editor/store/getters.js
deleted file mode 100644
index ebc68f8e9e6..00000000000
--- a/app/assets/javascripts/static_site_editor/store/getters.js
+++ /dev/null
@@ -1,2 +0,0 @@
-// eslint-disable-next-line import/prefer-default-export
-export const contentChanged = ({ originalContent, content }) => originalContent !== content;
diff --git a/app/assets/javascripts/static_site_editor/store/index.js b/app/assets/javascripts/static_site_editor/store/index.js
deleted file mode 100644
index 43256979ddd..00000000000
--- a/app/assets/javascripts/static_site_editor/store/index.js
+++ /dev/null
@@ -1,19 +0,0 @@
-import Vuex from 'vuex';
-import Vue from 'vue';
-import createState from './state';
-import * as getters from './getters';
-import * as actions from './actions';
-import mutations from './mutations';
-
-Vue.use(Vuex);
-
-const createStore = ({ initialState } = {}) => {
- return new Vuex.Store({
- state: createState(initialState),
- getters,
- actions,
- mutations,
- });
-};
-
-export default createStore;
diff --git a/app/assets/javascripts/static_site_editor/store/mutation_types.js b/app/assets/javascripts/static_site_editor/store/mutation_types.js
deleted file mode 100644
index 9cf356aecc5..00000000000
--- a/app/assets/javascripts/static_site_editor/store/mutation_types.js
+++ /dev/null
@@ -1,8 +0,0 @@
-export const LOAD_CONTENT = 'loadContent';
-export const RECEIVE_CONTENT_SUCCESS = 'receiveContentSuccess';
-export const RECEIVE_CONTENT_ERROR = 'receiveContentError';
-export const SET_CONTENT = 'setContent';
-export const SUBMIT_CHANGES = 'submitChanges';
-export const SUBMIT_CHANGES_SUCCESS = 'submitChangesSuccess';
-export const SUBMIT_CHANGES_ERROR = 'submitChangesError';
-export const DISMISS_SUBMIT_CHANGES_ERROR = 'dismissSubmitChangesError';
diff --git a/app/assets/javascripts/static_site_editor/store/mutations.js b/app/assets/javascripts/static_site_editor/store/mutations.js
deleted file mode 100644
index 72fe71f1c9b..00000000000
--- a/app/assets/javascripts/static_site_editor/store/mutations.js
+++ /dev/null
@@ -1,36 +0,0 @@
-import * as types from './mutation_types';
-
-export default {
- [types.LOAD_CONTENT](state) {
- state.isLoadingContent = true;
- },
- [types.RECEIVE_CONTENT_SUCCESS](state, { title, content }) {
- state.isLoadingContent = false;
- state.isContentLoaded = true;
- state.title = title;
- state.content = content;
- state.originalContent = content;
- },
- [types.RECEIVE_CONTENT_ERROR](state) {
- state.isLoadingContent = false;
- },
- [types.SET_CONTENT](state, content) {
- state.content = content;
- },
- [types.SUBMIT_CHANGES](state) {
- state.isSavingChanges = true;
- state.submitChangesError = '';
- },
- [types.SUBMIT_CHANGES_SUCCESS](state, meta) {
- state.savedContentMeta = meta;
- state.isSavingChanges = false;
- state.originalContent = state.content;
- },
- [types.SUBMIT_CHANGES_ERROR](state, error) {
- state.submitChangesError = error;
- state.isSavingChanges = false;
- },
- [types.DISMISS_SUBMIT_CHANGES_ERROR](state) {
- state.submitChangesError = '';
- },
-};
diff --git a/app/assets/javascripts/static_site_editor/store/state.js b/app/assets/javascripts/static_site_editor/store/state.js
deleted file mode 100644
index 8c524b4ffe9..00000000000
--- a/app/assets/javascripts/static_site_editor/store/state.js
+++ /dev/null
@@ -1,23 +0,0 @@
-const createState = (initialState = {}) => ({
- username: null,
- projectId: null,
- returnUrl: null,
- sourcePath: null,
-
- isLoadingContent: false,
- isSavingChanges: false,
- isSupportedContent: false,
-
- isContentLoaded: false,
-
- originalContent: '',
- content: '',
- title: '',
-
- submitChangesError: '',
- savedContentMeta: null,
-
- ...initialState,
-});
-
-export default createState;
diff --git a/app/assets/javascripts/syntax_highlight.js b/app/assets/javascripts/syntax_highlight.js
index 37f3dd4b496..474b5132bc6 100644
--- a/app/assets/javascripts/syntax_highlight.js
+++ b/app/assets/javascripts/syntax_highlight.js
@@ -1,4 +1,4 @@
-/* eslint-disable consistent-return, no-else-return */
+/* eslint-disable consistent-return */
import $ from 'jquery';
@@ -16,11 +16,10 @@ export default function syntaxHighlight(el) {
if ($(el).hasClass('js-syntax-highlight')) {
// Given the element itself, apply highlighting
return $(el).addClass(gon.user_color_scheme);
- } else {
- // Given a parent element, recurse to any of its applicable children
- const $children = $(el).find('.js-syntax-highlight');
- if ($children.length) {
- return syntaxHighlight($children);
- }
+ }
+ // Given a parent element, recurse to any of its applicable children
+ const $children = $(el).find('.js-syntax-highlight');
+ if ($children.length) {
+ return syntaxHighlight($children);
}
}
diff --git a/app/assets/javascripts/terminal/terminal.js b/app/assets/javascripts/terminal/terminal.js
index f4e546e4d4e..cf9064aba57 100644
--- a/app/assets/javascripts/terminal/terminal.js
+++ b/app/assets/javascripts/terminal/terminal.js
@@ -13,14 +13,11 @@ Terminal.applyAddon(webLinks);
export default class GLTerminal {
constructor(element, options = {}) {
- this.options = Object.assign(
- {},
- {
- cursorBlink: true,
- screenKeys: true,
- },
- options,
- );
+ this.options = {
+ cursorBlink: true,
+ screenKeys: true,
+ ...options,
+ };
this.container = element;
this.onDispose = [];
diff --git a/app/assets/javascripts/tracking.js b/app/assets/javascripts/tracking.js
index 09fe952e5f0..10510595570 100644
--- a/app/assets/javascripts/tracking.js
+++ b/app/assets/javascripts/tracking.js
@@ -1,4 +1,4 @@
-import _ from 'underscore';
+import { omitBy, isUndefined } from 'lodash';
const DEFAULT_SNOWPLOW_OPTIONS = {
namespace: 'gl',
@@ -14,11 +14,8 @@ const DEFAULT_SNOWPLOW_OPTIONS = {
linkClickTracking: false,
};
-const eventHandler = (e, func, opts = {}) => {
- const el = e.target.closest('[data-track-event]');
- const action = el && el.dataset.trackEvent;
- if (!action) return;
-
+const createEventPayload = (el, { suffix = '' } = {}) => {
+ const action = el.dataset.trackEvent + (suffix || '');
let value = el.dataset.trackValue || el.value || undefined;
if (el.type === 'checkbox' && !el.checked) value = false;
@@ -29,7 +26,19 @@ const eventHandler = (e, func, opts = {}) => {
context: el.dataset.trackContext,
};
- func(opts.category, action + (opts.suffix || ''), _.omit(data, _.isUndefined));
+ return {
+ action,
+ data: omitBy(data, isUndefined),
+ };
+};
+
+const eventHandler = (e, func, opts = {}) => {
+ const el = e.target.closest('[data-track-event]');
+
+ if (!el) return;
+
+ const { action, data } = createEventPayload(el, opts);
+ func(opts.category, action, data);
};
const eventHandlers = (category, func) => {
@@ -62,17 +71,30 @@ export default class Tracking {
return window.snowplow('trackStructEvent', category, action, label, property, value, contexts);
}
- static bindDocument(category = document.body.dataset.page, documentOverride = null) {
- const el = documentOverride || document;
- if (!this.enabled() || el.trackingBound) return [];
+ static bindDocument(category = document.body.dataset.page, parent = document) {
+ if (!this.enabled() || parent.trackingBound) return [];
- el.trackingBound = true;
+ // eslint-disable-next-line no-param-reassign
+ parent.trackingBound = true;
const handlers = eventHandlers(category, (...args) => this.event(...args));
- handlers.forEach(event => el.addEventListener(event.name, event.func));
+ handlers.forEach(event => parent.addEventListener(event.name, event.func));
return handlers;
}
+ static trackLoadEvents(category = document.body.dataset.page, parent = document) {
+ if (!this.enabled()) return [];
+
+ const loadEvents = parent.querySelectorAll('[data-track-event="render"]');
+
+ loadEvents.forEach(element => {
+ const { action, data } = createEventPayload(element);
+ this.event(category, action, data);
+ });
+
+ return loadEvents;
+ }
+
static mixin(opts = {}) {
return {
computed: {
@@ -111,6 +133,7 @@ export function initUserTracking() {
if (opts.linkClickTracking) window.snowplow('enableLinkClickTracking');
Tracking.bindDocument();
+ Tracking.trackLoadEvents();
document.dispatchEvent(new Event('SnowplowInitialized'));
}
diff --git a/app/assets/javascripts/tree.js b/app/assets/javascripts/tree.js
index 59276ee79d8..947246b2fbb 100644
--- a/app/assets/javascripts/tree.js
+++ b/app/assets/javascripts/tree.js
@@ -1,4 +1,4 @@
-/* eslint-disable func-names, consistent-return, one-var, no-else-return, class-methods-use-this */
+/* eslint-disable func-names, consistent-return, one-var, class-methods-use-this */
import $ from 'jquery';
import { visitUrl } from './lib/utils/url_utility';
@@ -15,9 +15,8 @@ export default class TreeView {
if (e.metaKey || e.which === 2) {
e.preventDefault();
return window.open(path, '_blank');
- } else {
- return visitUrl(path);
}
+ return visitUrl(path);
}
});
// Show the "Loading commit data" for only the first element
diff --git a/app/assets/javascripts/users_select.js b/app/assets/javascripts/users_select.js
deleted file mode 100644
index 6821df57b5a..00000000000
--- a/app/assets/javascripts/users_select.js
+++ /dev/null
@@ -1,774 +0,0 @@
-/* eslint-disable func-names, prefer-rest-params, consistent-return, no-shadow, no-else-return, no-self-compare, no-unused-expressions, yoda, prefer-spread, babel/camelcase, no-param-reassign */
-/* global Issuable */
-/* global emitSidebarEvent */
-
-import $ from 'jquery';
-import _ from 'underscore';
-import axios from './lib/utils/axios_utils';
-import { s__, __, sprintf } from './locale';
-import ModalStore from './boards/stores/modal_store';
-import { parseBoolean } from './lib/utils/common_utils';
-
-// TODO: remove eventHub hack after code splitting refactor
-window.emitSidebarEvent = window.emitSidebarEvent || $.noop;
-
-function UsersSelect(currentUser, els, options = {}) {
- const $els = $(els || '.js-user-search');
- this.users = this.users.bind(this);
- this.user = this.user.bind(this);
- this.usersPath = '/autocomplete/users.json';
- this.userPath = '/autocomplete/users/:id.json';
- if (currentUser != null) {
- if (typeof currentUser === 'object') {
- this.currentUser = currentUser;
- } else {
- this.currentUser = JSON.parse(currentUser);
- }
- }
-
- const { handleClick } = options;
- const userSelect = this;
-
- $els.each((i, dropdown) => {
- const userSelect = this;
- const options = {};
- const $dropdown = $(dropdown);
- options.projectId = $dropdown.data('projectId');
- options.groupId = $dropdown.data('groupId');
- options.showCurrentUser = $dropdown.data('currentUser');
- options.todoFilter = $dropdown.data('todoFilter');
- options.todoStateFilter = $dropdown.data('todoStateFilter');
- options.iid = $dropdown.data('iid');
- options.issuableType = $dropdown.data('issuableType');
- const showNullUser = $dropdown.data('nullUser');
- const defaultNullUser = $dropdown.data('nullUserDefault');
- const showMenuAbove = $dropdown.data('showMenuAbove');
- const showAnyUser = $dropdown.data('anyUser');
- const firstUser = $dropdown.data('firstUser');
- options.authorId = $dropdown.data('authorId');
- const defaultLabel = $dropdown.data('defaultLabel');
- const issueURL = $dropdown.data('issueUpdate');
- const $selectbox = $dropdown.closest('.selectbox');
- let $block = $selectbox.closest('.block');
- const abilityName = $dropdown.data('abilityName');
- let $value = $block.find('.value');
- const $collapsedSidebar = $block.find('.sidebar-collapsed-user');
- // eslint-disable-next-line no-jquery/no-fade
- const $loading = $block.find('.block-loading').fadeOut();
- const selectedIdDefault = defaultNullUser && showNullUser ? 0 : null;
- let selectedId = $dropdown.data('selected');
- let assignTo;
- let assigneeTemplate;
- let collapsedAssigneeTemplate;
-
- if (selectedId === undefined) {
- selectedId = selectedIdDefault;
- }
-
- const assignYourself = function() {
- const unassignedSelected = $dropdown
- .closest('.selectbox')
- .find(`input[name='${$dropdown.data('fieldName')}'][value=0]`);
-
- if (unassignedSelected) {
- unassignedSelected.remove();
- }
-
- // Save current selected user to the DOM
- const currentUserInfo = $dropdown.data('currentUserInfo') || {};
- const currentUser = userSelect.currentUser || {};
- const fieldName = $dropdown.data('fieldName');
- const userName = currentUserInfo.name;
- const userId = currentUserInfo.id || currentUser.id;
-
- const inputHtmlString = _.template(`
- <input type="hidden" name="<%- fieldName %>"
- data-meta="<%- userName %>"
- value="<%- userId %>" />
- `)({ fieldName, userName, userId });
-
- if ($selectbox) {
- $dropdown.parent().before(inputHtmlString);
- } else {
- $dropdown.after(inputHtmlString);
- }
- };
-
- if ($block[0]) {
- $block[0].addEventListener('assignYourself', assignYourself);
- }
-
- const getSelectedUserInputs = function() {
- return $selectbox.find(`input[name="${$dropdown.data('fieldName')}"]`);
- };
-
- const getSelected = function() {
- return getSelectedUserInputs()
- .map((index, input) => parseInt(input.value, 10))
- .get();
- };
-
- const checkMaxSelect = function() {
- const maxSelect = $dropdown.data('maxSelect');
- if (maxSelect) {
- const selected = getSelected();
-
- if (selected.length > maxSelect) {
- const firstSelectedId = selected[0];
- const firstSelected = $dropdown
- .closest('.selectbox')
- .find(`input[name='${$dropdown.data('fieldName')}'][value=${firstSelectedId}]`);
-
- firstSelected.remove();
- emitSidebarEvent('sidebar.removeAssignee', {
- id: firstSelectedId,
- });
- }
- }
- };
-
- const getMultiSelectDropdownTitle = function(selectedUser, isSelected) {
- const selectedUsers = getSelected().filter(u => u !== 0);
-
- const firstUser = getSelectedUserInputs()
- .map((index, input) => ({
- name: input.dataset.meta,
- value: parseInt(input.value, 10),
- }))
- .filter(u => u.id !== 0)
- .get(0);
-
- if (selectedUsers.length === 0) {
- return s__('UsersSelect|Unassigned');
- } else if (selectedUsers.length === 1) {
- return firstUser.name;
- } else if (isSelected) {
- const otherSelected = selectedUsers.filter(s => s !== selectedUser.id);
- return sprintf(s__('UsersSelect|%{name} + %{length} more'), {
- name: selectedUser.name,
- length: otherSelected.length,
- });
- } else {
- return sprintf(s__('UsersSelect|%{name} + %{length} more'), {
- name: firstUser.name,
- length: selectedUsers.length - 1,
- });
- }
- };
-
- $('.assign-to-me-link').on('click', e => {
- e.preventDefault();
- $(e.currentTarget).hide();
-
- if ($dropdown.data('multiSelect')) {
- assignYourself();
- checkMaxSelect();
-
- const currentUserInfo = $dropdown.data('currentUserInfo');
- $dropdown
- .find('.dropdown-toggle-text')
- .text(getMultiSelectDropdownTitle(currentUserInfo))
- .removeClass('is-default');
- } else {
- const $input = $(`input[name="${$dropdown.data('fieldName')}"]`);
- $input.val(gon.current_user_id);
- selectedId = $input.val();
- $dropdown
- .find('.dropdown-toggle-text')
- .text(gon.current_user_fullname)
- .removeClass('is-default');
- }
- });
-
- $block.on('click', '.js-assign-yourself', e => {
- e.preventDefault();
- return assignTo(userSelect.currentUser.id);
- });
-
- assignTo = function(selected) {
- const data = {};
- data[abilityName] = {};
- data[abilityName].assignee_id = selected != null ? selected : null;
- // eslint-disable-next-line no-jquery/no-fade
- $loading.removeClass('hidden').fadeIn();
- $dropdown.trigger('loading.gl.dropdown');
-
- return axios.put(issueURL, data).then(({ data }) => {
- let user = {};
- let tooltipTitle = user.name;
- $dropdown.trigger('loaded.gl.dropdown');
- // eslint-disable-next-line no-jquery/no-fade
- $loading.fadeOut();
- if (data.assignee) {
- user = {
- name: data.assignee.name,
- username: data.assignee.username,
- avatar: data.assignee.avatar_url,
- };
- tooltipTitle = _.escape(user.name);
- } else {
- user = {
- name: s__('UsersSelect|Unassigned'),
- username: '',
- avatar: '',
- };
- tooltipTitle = s__('UsersSelect|Assignee');
- }
- $value.html(assigneeTemplate(user));
- $collapsedSidebar.attr('title', tooltipTitle).tooltip('_fixTitle');
- return $collapsedSidebar.html(collapsedAssigneeTemplate(user));
- });
- };
- collapsedAssigneeTemplate = _.template(
- '<% if( avatar ) { %> <a class="author-link" href="/<%- username %>"> <img width="24" class="avatar avatar-inline s24" alt="" src="<%- avatar %>"> </a> <% } else { %> <i class="fa fa-user"></i> <% } %>',
- );
- assigneeTemplate = _.template(
- `<% if (username) { %> <a class="author-link bold" href="/<%- username %>"> <% if( avatar ) { %> <img width="32" class="avatar avatar-inline s32" alt="" src="<%- avatar %>"> <% } %> <span class="author"><%- name %></span> <span class="username"> @<%- username %> </span> </a> <% } else { %> <span class="no-value assign-yourself">
- ${sprintf(s__('UsersSelect|No assignee - %{openingTag} assign yourself %{closingTag}'), {
- openingTag: '<a href="#" class="js-assign-yourself">',
- closingTag: '</a>',
- })}</span> <% } %>`,
- );
- return $dropdown.glDropdown({
- showMenuAbove,
- data(term, callback) {
- return userSelect.users(term, options, users => {
- // GitLabDropdownFilter returns this.instance
- // GitLabDropdownRemote returns this.options.instance
- const glDropdown = this.instance || this.options.instance;
- glDropdown.options.processData(term, users, callback);
- });
- },
- processData(term, data, callback) {
- let users = data;
-
- // Only show assigned user list when there is no search term
- if ($dropdown.hasClass('js-multiselect') && term.length === 0) {
- const selectedInputs = getSelectedUserInputs();
-
- // Potential duplicate entries when dealing with issue board
- // because issue board is also managed by vue
- const selectedUsers = _.uniq(selectedInputs, false, a => a.value)
- .filter(input => {
- const userId = parseInt(input.value, 10);
- const inUsersArray = users.find(u => u.id === userId);
-
- return !inUsersArray && userId !== 0;
- })
- .map(input => {
- const userId = parseInt(input.value, 10);
- const { avatarUrl, avatar_url, name, username, canMerge } = input.dataset;
- return {
- avatar_url: avatarUrl || avatar_url,
- id: userId,
- name,
- username,
- can_merge: parseBoolean(canMerge),
- };
- });
-
- users = data.concat(selectedUsers);
- }
-
- let anyUser;
- let index;
- let len;
- let name;
- let obj;
- let showDivider;
- if (term.length === 0) {
- showDivider = 0;
- if (firstUser) {
- // Move current user to the front of the list
- for (index = 0, len = users.length; index < len; index += 1) {
- obj = users[index];
- if (obj.username === firstUser) {
- users.splice(index, 1);
- users.unshift(obj);
- break;
- }
- }
- }
- if (showNullUser) {
- showDivider += 1;
- users.unshift({
- beforeDivider: true,
- name: s__('UsersSelect|Unassigned'),
- id: 0,
- });
- }
- if (showAnyUser) {
- showDivider += 1;
- name = showAnyUser;
- if (name === true) {
- name = s__('UsersSelect|Any User');
- }
- anyUser = {
- beforeDivider: true,
- name,
- id: null,
- };
- users.unshift(anyUser);
- }
-
- if (showDivider) {
- users.splice(showDivider, 0, { type: 'divider' });
- }
-
- if ($dropdown.hasClass('js-multiselect')) {
- const selected = getSelected().filter(i => i !== 0);
-
- if (selected.length > 0) {
- if ($dropdown.data('dropdownHeader')) {
- showDivider += 1;
- users.splice(showDivider, 0, {
- type: 'header',
- content: $dropdown.data('dropdownHeader'),
- });
- }
-
- const selectedUsers = users
- .filter(u => selected.indexOf(u.id) !== -1)
- .sort((a, b) => a.name > b.name);
-
- users = users.filter(u => selected.indexOf(u.id) === -1);
-
- selectedUsers.forEach(selectedUser => {
- showDivider += 1;
- users.splice(showDivider, 0, selectedUser);
- });
-
- users.splice(showDivider + 1, 0, { type: 'divider' });
- }
- }
- }
-
- callback(users);
- if (showMenuAbove) {
- $dropdown.data('glDropdown').positionMenuAbove();
- }
- },
- filterable: true,
- filterRemote: true,
- search: {
- fields: ['name', 'username'],
- },
- selectable: true,
- fieldName: $dropdown.data('fieldName'),
- toggleLabel(selected, el, glDropdown) {
- const inputValue = glDropdown.filterInput.val();
-
- if (this.multiSelect && inputValue === '') {
- // Remove non-users from the fullData array
- const users = glDropdown.filteredFullData();
- const callback = glDropdown.parseData.bind(glDropdown);
-
- // Update the data model
- this.processData(inputValue, users, callback);
- }
-
- if (this.multiSelect) {
- return getMultiSelectDropdownTitle(selected, $(el).hasClass('is-active'));
- }
-
- if (selected && 'id' in selected && $(el).hasClass('is-active')) {
- $dropdown.find('.dropdown-toggle-text').removeClass('is-default');
- if (selected.text) {
- return selected.text;
- } else {
- return selected.name;
- }
- } else {
- $dropdown.find('.dropdown-toggle-text').addClass('is-default');
- return defaultLabel;
- }
- },
- defaultLabel,
- hidden() {
- if ($dropdown.hasClass('js-multiselect')) {
- emitSidebarEvent('sidebar.saveAssignees');
- }
-
- if (!$dropdown.data('alwaysShowSelectbox')) {
- $selectbox.hide();
-
- // Recalculate where .value is because vue might have changed it
- $block = $selectbox.closest('.block');
- $value = $block.find('.value');
- // display:block overrides the hide-collapse rule
- $value.css('display', '');
- }
- },
- multiSelect: $dropdown.hasClass('js-multiselect'),
- inputMeta: $dropdown.data('inputMeta'),
- clicked(options) {
- const { $el, e, isMarking } = options;
- const user = options.selectedObj;
-
- $el.tooltip('dispose');
-
- if ($dropdown.hasClass('js-multiselect')) {
- const isActive = $el.hasClass('is-active');
- const previouslySelected = $dropdown
- .closest('.selectbox')
- .find(`input[name='${$dropdown.data('fieldName')}'][value!=0]`);
-
- // Enables support for limiting the number of users selected
- // Automatically removes the first on the list if more users are selected
- checkMaxSelect();
-
- if (user.beforeDivider && user.name.toLowerCase() === 'unassigned') {
- // Unassigned selected
- previouslySelected.each((index, element) => {
- element.remove();
- });
- emitSidebarEvent('sidebar.removeAllAssignees');
- } else if (isActive) {
- // user selected
- emitSidebarEvent('sidebar.addAssignee', user);
-
- // Remove unassigned selection (if it was previously selected)
- const unassignedSelected = $dropdown
- .closest('.selectbox')
- .find(`input[name='${$dropdown.data('fieldName')}'][value=0]`);
-
- if (unassignedSelected) {
- unassignedSelected.remove();
- }
- } else {
- if (previouslySelected.length === 0) {
- // Select unassigned because there is no more selected users
- this.addInput($dropdown.data('fieldName'), 0, {});
- }
-
- // User unselected
- emitSidebarEvent('sidebar.removeAssignee', user);
- }
-
- if (getSelected().find(u => u === gon.current_user_id)) {
- $('.assign-to-me-link').hide();
- } else {
- $('.assign-to-me-link').show();
- }
- }
-
- const page = $('body').attr('data-page');
- const isIssueIndex = page === 'projects:issues:index';
- const isMRIndex = page === page && page === 'projects:merge_requests:index';
- if (
- $dropdown.hasClass('js-filter-bulk-update') ||
- $dropdown.hasClass('js-issuable-form-dropdown')
- ) {
- e.preventDefault();
-
- const isSelecting = user.id !== selectedId;
- selectedId = isSelecting ? user.id : selectedIdDefault;
-
- if (selectedId === gon.current_user_id) {
- $('.assign-to-me-link').hide();
- } else {
- $('.assign-to-me-link').show();
- }
- return;
- }
- if ($el.closest('.add-issues-modal').length) {
- ModalStore.store.filter[$dropdown.data('fieldName')] = user.id;
- } else if (handleClick) {
- e.preventDefault();
- handleClick(user, isMarking);
- } else if ($dropdown.hasClass('js-filter-submit') && (isIssueIndex || isMRIndex)) {
- return Issuable.filterResults($dropdown.closest('form'));
- } else if ($dropdown.hasClass('js-filter-submit')) {
- return $dropdown.closest('form').submit();
- } else if (!$dropdown.hasClass('js-multiselect')) {
- const selected = $dropdown
- .closest('.selectbox')
- .find(`input[name='${$dropdown.data('fieldName')}']`)
- .val();
- return assignTo(selected);
- }
-
- // Automatically close dropdown after assignee is selected
- // since CE has no multiple assignees
- // EE does not have a max-select
- if ($dropdown.data('maxSelect') && getSelected().length === $dropdown.data('maxSelect')) {
- // Close the dropdown
- $dropdown.dropdown('toggle');
- }
- },
- id(user) {
- return user.id;
- },
- opened(e) {
- const $el = $(e.currentTarget);
- const selected = getSelected();
- if ($dropdown.hasClass('js-issue-board-sidebar') && selected.length === 0) {
- this.addInput($dropdown.data('fieldName'), 0, {});
- }
- $el.find('.is-active').removeClass('is-active');
-
- function highlightSelected(id) {
- $el.find(`li[data-user-id="${id}"] .dropdown-menu-user-link`).addClass('is-active');
- }
-
- if (selected.length > 0) {
- getSelected().forEach(selectedId => highlightSelected(selectedId));
- } else if ($dropdown.hasClass('js-issue-board-sidebar')) {
- highlightSelected(0);
- } else {
- highlightSelected(selectedId);
- }
- },
- updateLabel: $dropdown.data('dropdownTitle'),
- renderRow(user) {
- const username = user.username ? `@${user.username}` : '';
- const avatar = user.avatar_url ? user.avatar_url : gon.default_avatar_url;
-
- let selected = false;
-
- if (this.multiSelect) {
- selected = getSelected().find(u => user.id === u);
-
- const { fieldName } = this;
- const field = $dropdown
- .closest('.selectbox')
- .find(`input[name='${fieldName}'][value='${user.id}']`);
-
- if (field.length) {
- selected = true;
- }
- } else {
- selected = user.id === selectedId;
- }
-
- let img = '';
- if (user.beforeDivider != null) {
- `<li><a href='#' class='${selected === true ? 'is-active' : ''}'>${_.escape(
- user.name,
- )}</a></li>`;
- } else {
- // 0 margin, because it's now handled by a wrapper
- img = `<img src='${avatar}' class='avatar avatar-inline m-0' width='32' />`;
- }
-
- return userSelect.renderRow(options.issuableType, user, selected, username, img);
- },
- });
- });
- import(/* webpackChunkName: 'select2' */ 'select2/select2')
- .then(() => {
- $('.ajax-users-select').each((i, select) => {
- const options = {};
- options.skipLdap = $(select).hasClass('skip_ldap');
- options.projectId = $(select).data('projectId');
- options.groupId = $(select).data('groupId');
- options.showCurrentUser = $(select).data('currentUser');
- options.authorId = $(select).data('authorId');
- options.skipUsers = $(select).data('skipUsers');
- const showNullUser = $(select).data('nullUser');
- const showAnyUser = $(select).data('anyUser');
- const showEmailUser = $(select).data('emailUser');
- const firstUser = $(select).data('firstUser');
- return $(select).select2({
- placeholder: __('Search for a user'),
- multiple: $(select).hasClass('multiselect'),
- minimumInputLength: 0,
- query(query) {
- return userSelect.users(query.term, options, users => {
- let name;
- const data = {
- results: users,
- };
- if (query.term.length === 0) {
- if (firstUser) {
- // Move current user to the front of the list
- const ref = data.results;
-
- for (let index = 0, len = ref.length; index < len; index += 1) {
- const obj = ref[index];
- if (obj.username === firstUser) {
- data.results.splice(index, 1);
- data.results.unshift(obj);
- break;
- }
- }
- }
- if (showNullUser) {
- const nullUser = {
- name: s__('UsersSelect|Unassigned'),
- id: 0,
- };
- data.results.unshift(nullUser);
- }
- if (showAnyUser) {
- name = showAnyUser;
- if (name === true) {
- name = s__('UsersSelect|Any User');
- }
- const anyUser = {
- name,
- id: null,
- };
- data.results.unshift(anyUser);
- }
- }
- if (showEmailUser && data.results.length === 0 && query.term.match(/^[^@]+@[^@]+$/)) {
- const trimmed = query.term.trim();
- const emailUser = {
- name: sprintf(__('Invite "%{trimmed}" by email'), { trimmed }),
- username: trimmed,
- id: trimmed,
- invite: true,
- };
- data.results.unshift(emailUser);
- }
- return query.callback(data);
- });
- },
- initSelection() {
- const args = 1 <= arguments.length ? [].slice.call(arguments, 0) : [];
- return userSelect.initSelection.apply(userSelect, args);
- },
- formatResult() {
- const args = 1 <= arguments.length ? [].slice.call(arguments, 0) : [];
- return userSelect.formatResult.apply(userSelect, args);
- },
- formatSelection() {
- const args = 1 <= arguments.length ? [].slice.call(arguments, 0) : [];
- return userSelect.formatSelection.apply(userSelect, args);
- },
- dropdownCssClass: 'ajax-users-dropdown',
- // we do not want to escape markup since we are displaying html in results
- escapeMarkup(m) {
- return m;
- },
- });
- });
- })
- .catch(() => {});
-}
-
-UsersSelect.prototype.initSelection = function(element, callback) {
- const id = $(element).val();
- if (id === '0') {
- const nullUser = {
- name: s__('UsersSelect|Unassigned'),
- };
- return callback(nullUser);
- } else if (id !== '') {
- return this.user(id, callback);
- }
-};
-
-UsersSelect.prototype.formatResult = function(user) {
- let avatar = gon.default_avatar_url;
- if (user.avatar_url) {
- avatar = user.avatar_url;
- }
- return `
- <div class='user-result'>
- <div class='user-image'>
- <img class='avatar avatar-inline s32' src='${avatar}'>
- </div>
- <div class='user-info'>
- <div class='user-name dropdown-menu-user-full-name'>
- ${_.escape(user.name)}
- </div>
- <div class='user-username dropdown-menu-user-username text-secondary'>
- ${!user.invite ? `@${_.escape(user.username)}` : ''}
- </div>
- </div>
- </div>
- `;
-};
-
-UsersSelect.prototype.formatSelection = function(user) {
- return _.escape(user.name);
-};
-
-UsersSelect.prototype.user = function(user_id, callback) {
- if (!/^\d+$/.test(user_id)) {
- return false;
- }
-
- let url = this.buildUrl(this.userPath);
- url = url.replace(':id', user_id);
- return axios.get(url).then(({ data }) => {
- callback(data);
- });
-};
-
-// Return users list. Filtered by query
-// Only active users retrieved
-UsersSelect.prototype.users = function(query, options, callback) {
- const url = this.buildUrl(this.usersPath);
- const params = {
- search: query,
- active: true,
- project_id: options.projectId || null,
- group_id: options.groupId || null,
- skip_ldap: options.skipLdap || null,
- todo_filter: options.todoFilter || null,
- todo_state_filter: options.todoStateFilter || null,
- current_user: options.showCurrentUser || null,
- author_id: options.authorId || null,
- skip_users: options.skipUsers || null,
- };
-
- if (options.issuableType === 'merge_request') {
- params.merge_request_iid = options.iid || null;
- }
-
- return axios.get(url, { params }).then(({ data }) => {
- callback(data);
- });
-};
-
-UsersSelect.prototype.buildUrl = function(url) {
- if (gon.relative_url_root != null) {
- url = gon.relative_url_root.replace(/\/$/, '') + url;
- }
- return url;
-};
-
-UsersSelect.prototype.renderRow = function(issuableType, user, selected, username, img) {
- const tooltip = issuableType === 'merge_request' && !user.can_merge ? __('Cannot merge') : '';
- const tooltipClass = tooltip ? `has-tooltip` : '';
- const selectedClass = selected === true ? 'is-active' : '';
- const linkClasses = `${selectedClass} ${tooltipClass}`;
- const tooltipAttributes = tooltip
- ? `data-container="body" data-placement="left" data-title="${tooltip}"`
- : '';
-
- return `
- <li data-user-id=${user.id}>
- <a href="#" class="dropdown-menu-user-link d-flex align-items-center ${linkClasses}" ${tooltipAttributes}>
- ${this.renderRowAvatar(issuableType, user, img)}
- <span class="d-flex flex-column overflow-hidden">
- <strong class="dropdown-menu-user-full-name">
- ${_.escape(user.name)}
- </strong>
- ${username ? `<span class="dropdown-menu-user-username">${username}</span>` : ''}
- </span>
- </a>
- </li>
- `;
-};
-
-UsersSelect.prototype.renderRowAvatar = function(issuableType, user, img) {
- if (user.beforeDivider) {
- return img;
- }
-
- const mergeIcon =
- issuableType === 'merge_request' && !user.can_merge
- ? '<i class="fa fa-exclamation-triangle merge-icon"></i>'
- : '';
-
- return `<span class="position-relative mr-2">
- ${img}
- ${mergeIcon}
- </span>`;
-};
-
-export default UsersSelect;
diff --git a/app/assets/javascripts/users_select/constants.js b/app/assets/javascripts/users_select/constants.js
new file mode 100644
index 00000000000..64df1e1748c
--- /dev/null
+++ b/app/assets/javascripts/users_select/constants.js
@@ -0,0 +1,18 @@
+export const AJAX_USERS_SELECT_OPTIONS_MAP = {
+ projectId: 'projectId',
+ groupId: 'groupId',
+ showCurrentUser: 'currentUser',
+ authorId: 'authorId',
+ skipUsers: 'skipUsers',
+};
+
+export const AJAX_USERS_SELECT_PARAMS_MAP = {
+ project_id: 'projectId',
+ group_id: 'groupId',
+ skip_ldap: 'skipLdap',
+ todo_filter: 'todoFilter',
+ todo_state_filter: 'todoStateFilter',
+ current_user: 'showCurrentUser',
+ author_id: 'authorId',
+ skip_users: 'skipUsers',
+};
diff --git a/app/assets/javascripts/users_select/index.js b/app/assets/javascripts/users_select/index.js
new file mode 100644
index 00000000000..2dbe5a8171e
--- /dev/null
+++ b/app/assets/javascripts/users_select/index.js
@@ -0,0 +1,764 @@
+/* eslint-disable func-names, prefer-rest-params, consistent-return, no-shadow, no-self-compare, no-unused-expressions, yoda, prefer-spread, babel/camelcase, no-param-reassign */
+/* global Issuable */
+/* global emitSidebarEvent */
+
+import $ from 'jquery';
+import { escape, template, uniqBy } from 'lodash';
+import axios from '../lib/utils/axios_utils';
+import { s__, __, sprintf } from '../locale';
+import ModalStore from '../boards/stores/modal_store';
+import { parseBoolean } from '../lib/utils/common_utils';
+import {
+ AJAX_USERS_SELECT_OPTIONS_MAP,
+ AJAX_USERS_SELECT_PARAMS_MAP,
+} from 'ee_else_ce/users_select/constants';
+import { getAjaxUsersSelectOptions, getAjaxUsersSelectParams } from './utils';
+
+// TODO: remove eventHub hack after code splitting refactor
+window.emitSidebarEvent = window.emitSidebarEvent || $.noop;
+
+function UsersSelect(currentUser, els, options = {}) {
+ const $els = $(els || '.js-user-search');
+ this.users = this.users.bind(this);
+ this.user = this.user.bind(this);
+ this.usersPath = '/autocomplete/users.json';
+ this.userPath = '/autocomplete/users/:id.json';
+ if (currentUser != null) {
+ if (typeof currentUser === 'object') {
+ this.currentUser = currentUser;
+ } else {
+ this.currentUser = JSON.parse(currentUser);
+ }
+ }
+
+ const { handleClick } = options;
+ const userSelect = this;
+
+ $els.each((i, dropdown) => {
+ const userSelect = this;
+ const options = {};
+ const $dropdown = $(dropdown);
+ options.projectId = $dropdown.data('projectId');
+ options.groupId = $dropdown.data('groupId');
+ options.showCurrentUser = $dropdown.data('currentUser');
+ options.todoFilter = $dropdown.data('todoFilter');
+ options.todoStateFilter = $dropdown.data('todoStateFilter');
+ options.iid = $dropdown.data('iid');
+ options.issuableType = $dropdown.data('issuableType');
+ const showNullUser = $dropdown.data('nullUser');
+ const defaultNullUser = $dropdown.data('nullUserDefault');
+ const showMenuAbove = $dropdown.data('showMenuAbove');
+ const showAnyUser = $dropdown.data('anyUser');
+ const firstUser = $dropdown.data('firstUser');
+ options.authorId = $dropdown.data('authorId');
+ const defaultLabel = $dropdown.data('defaultLabel');
+ const issueURL = $dropdown.data('issueUpdate');
+ const $selectbox = $dropdown.closest('.selectbox');
+ let $block = $selectbox.closest('.block');
+ const abilityName = $dropdown.data('abilityName');
+ let $value = $block.find('.value');
+ const $collapsedSidebar = $block.find('.sidebar-collapsed-user');
+ // eslint-disable-next-line no-jquery/no-fade
+ const $loading = $block.find('.block-loading').fadeOut();
+ const selectedIdDefault = defaultNullUser && showNullUser ? 0 : null;
+ let selectedId = $dropdown.data('selected');
+ let assignTo;
+ let assigneeTemplate;
+ let collapsedAssigneeTemplate;
+
+ if (selectedId === undefined) {
+ selectedId = selectedIdDefault;
+ }
+
+ const assignYourself = function() {
+ const unassignedSelected = $dropdown
+ .closest('.selectbox')
+ .find(`input[name='${$dropdown.data('fieldName')}'][value=0]`);
+
+ if (unassignedSelected) {
+ unassignedSelected.remove();
+ }
+
+ // Save current selected user to the DOM
+ const currentUserInfo = $dropdown.data('currentUserInfo') || {};
+ const currentUser = userSelect.currentUser || {};
+ const fieldName = $dropdown.data('fieldName');
+ const userName = currentUserInfo.name;
+ const userId = currentUserInfo.id || currentUser.id;
+
+ const inputHtmlString = template(`
+ <input type="hidden" name="<%- fieldName %>"
+ data-meta="<%- userName %>"
+ value="<%- userId %>" />
+ `)({ fieldName, userName, userId });
+
+ if ($selectbox) {
+ $dropdown.parent().before(inputHtmlString);
+ } else {
+ $dropdown.after(inputHtmlString);
+ }
+ };
+
+ if ($block[0]) {
+ $block[0].addEventListener('assignYourself', assignYourself);
+ }
+
+ const getSelectedUserInputs = function() {
+ return $selectbox.find(`input[name="${$dropdown.data('fieldName')}"]`);
+ };
+
+ const getSelected = function() {
+ return getSelectedUserInputs()
+ .map((index, input) => parseInt(input.value, 10))
+ .get();
+ };
+
+ const checkMaxSelect = function() {
+ const maxSelect = $dropdown.data('maxSelect');
+ if (maxSelect) {
+ const selected = getSelected();
+
+ if (selected.length > maxSelect) {
+ const firstSelectedId = selected[0];
+ const firstSelected = $dropdown
+ .closest('.selectbox')
+ .find(`input[name='${$dropdown.data('fieldName')}'][value=${firstSelectedId}]`);
+
+ firstSelected.remove();
+ emitSidebarEvent('sidebar.removeAssignee', {
+ id: firstSelectedId,
+ });
+ }
+ }
+ };
+
+ const getMultiSelectDropdownTitle = function(selectedUser, isSelected) {
+ const selectedUsers = getSelected().filter(u => u !== 0);
+
+ const firstUser = getSelectedUserInputs()
+ .map((index, input) => ({
+ name: input.dataset.meta,
+ value: parseInt(input.value, 10),
+ }))
+ .filter(u => u.id !== 0)
+ .get(0);
+
+ if (selectedUsers.length === 0) {
+ return s__('UsersSelect|Unassigned');
+ } else if (selectedUsers.length === 1) {
+ return firstUser.name;
+ } else if (isSelected) {
+ const otherSelected = selectedUsers.filter(s => s !== selectedUser.id);
+ return sprintf(s__('UsersSelect|%{name} + %{length} more'), {
+ name: selectedUser.name,
+ length: otherSelected.length,
+ });
+ }
+ return sprintf(s__('UsersSelect|%{name} + %{length} more'), {
+ name: firstUser.name,
+ length: selectedUsers.length - 1,
+ });
+ };
+
+ $('.assign-to-me-link').on('click', e => {
+ e.preventDefault();
+ $(e.currentTarget).hide();
+
+ if ($dropdown.data('multiSelect')) {
+ assignYourself();
+ checkMaxSelect();
+
+ const currentUserInfo = $dropdown.data('currentUserInfo');
+ $dropdown
+ .find('.dropdown-toggle-text')
+ .text(getMultiSelectDropdownTitle(currentUserInfo))
+ .removeClass('is-default');
+ } else {
+ const $input = $(`input[name="${$dropdown.data('fieldName')}"]`);
+ $input.val(gon.current_user_id);
+ selectedId = $input.val();
+ $dropdown
+ .find('.dropdown-toggle-text')
+ .text(gon.current_user_fullname)
+ .removeClass('is-default');
+ }
+ });
+
+ $block.on('click', '.js-assign-yourself', e => {
+ e.preventDefault();
+ return assignTo(userSelect.currentUser.id);
+ });
+
+ assignTo = function(selected) {
+ const data = {};
+ data[abilityName] = {};
+ data[abilityName].assignee_id = selected != null ? selected : null;
+ // eslint-disable-next-line no-jquery/no-fade
+ $loading.removeClass('hidden').fadeIn();
+ $dropdown.trigger('loading.gl.dropdown');
+
+ return axios.put(issueURL, data).then(({ data }) => {
+ let user = {};
+ let tooltipTitle = user.name;
+ $dropdown.trigger('loaded.gl.dropdown');
+ // eslint-disable-next-line no-jquery/no-fade
+ $loading.fadeOut();
+ if (data.assignee) {
+ user = {
+ name: data.assignee.name,
+ username: data.assignee.username,
+ avatar: data.assignee.avatar_url,
+ };
+ tooltipTitle = escape(user.name);
+ } else {
+ user = {
+ name: s__('UsersSelect|Unassigned'),
+ username: '',
+ avatar: '',
+ };
+ tooltipTitle = s__('UsersSelect|Assignee');
+ }
+ $value.html(assigneeTemplate(user));
+ $collapsedSidebar.attr('title', tooltipTitle).tooltip('_fixTitle');
+ return $collapsedSidebar.html(collapsedAssigneeTemplate(user));
+ });
+ };
+ collapsedAssigneeTemplate = template(
+ '<% if( avatar ) { %> <a class="author-link" href="/<%- username %>"> <img width="24" class="avatar avatar-inline s24" alt="" src="<%- avatar %>"> </a> <% } else { %> <i class="fa fa-user"></i> <% } %>',
+ );
+ assigneeTemplate = template(
+ `<% if (username) { %> <a class="author-link bold" href="/<%- username %>"> <% if( avatar ) { %> <img width="32" class="avatar avatar-inline s32" alt="" src="<%- avatar %>"> <% } %> <span class="author"><%- name %></span> <span class="username"> @<%- username %> </span> </a> <% } else { %> <span class="no-value assign-yourself">
+ ${sprintf(s__('UsersSelect|No assignee - %{openingTag} assign yourself %{closingTag}'), {
+ openingTag: '<a href="#" class="js-assign-yourself">',
+ closingTag: '</a>',
+ })}</span> <% } %>`,
+ );
+ return $dropdown.glDropdown({
+ showMenuAbove,
+ data(term, callback) {
+ return userSelect.users(term, options, users => {
+ // GitLabDropdownFilter returns this.instance
+ // GitLabDropdownRemote returns this.options.instance
+ const glDropdown = this.instance || this.options.instance;
+ glDropdown.options.processData(term, users, callback);
+ });
+ },
+ processData(term, data, callback) {
+ let users = data;
+
+ // Only show assigned user list when there is no search term
+ if ($dropdown.hasClass('js-multiselect') && term.length === 0) {
+ const selectedInputs = getSelectedUserInputs();
+
+ // Potential duplicate entries when dealing with issue board
+ // because issue board is also managed by vue
+ const selectedUsers = uniqBy(selectedInputs, a => a.value)
+ .filter(input => {
+ const userId = parseInt(input.value, 10);
+ const inUsersArray = users.find(u => u.id === userId);
+
+ return !inUsersArray && userId !== 0;
+ })
+ .map(input => {
+ const userId = parseInt(input.value, 10);
+ const { avatarUrl, avatar_url, name, username, canMerge } = input.dataset;
+ return {
+ avatar_url: avatarUrl || avatar_url,
+ id: userId,
+ name,
+ username,
+ can_merge: parseBoolean(canMerge),
+ };
+ });
+
+ users = data.concat(selectedUsers);
+ }
+
+ let anyUser;
+ let index;
+ let len;
+ let name;
+ let obj;
+ let showDivider;
+ if (term.length === 0) {
+ showDivider = 0;
+ if (firstUser) {
+ // Move current user to the front of the list
+ for (index = 0, len = users.length; index < len; index += 1) {
+ obj = users[index];
+ if (obj.username === firstUser) {
+ users.splice(index, 1);
+ users.unshift(obj);
+ break;
+ }
+ }
+ }
+ if (showNullUser) {
+ showDivider += 1;
+ users.unshift({
+ beforeDivider: true,
+ name: s__('UsersSelect|Unassigned'),
+ id: 0,
+ });
+ }
+ if (showAnyUser) {
+ showDivider += 1;
+ name = showAnyUser;
+ if (name === true) {
+ name = s__('UsersSelect|Any User');
+ }
+ anyUser = {
+ beforeDivider: true,
+ name,
+ id: null,
+ };
+ users.unshift(anyUser);
+ }
+
+ if (showDivider) {
+ users.splice(showDivider, 0, { type: 'divider' });
+ }
+
+ if ($dropdown.hasClass('js-multiselect')) {
+ const selected = getSelected().filter(i => i !== 0);
+
+ if (selected.length > 0) {
+ if ($dropdown.data('dropdownHeader')) {
+ showDivider += 1;
+ users.splice(showDivider, 0, {
+ type: 'header',
+ content: $dropdown.data('dropdownHeader'),
+ });
+ }
+
+ const selectedUsers = users
+ .filter(u => selected.indexOf(u.id) !== -1)
+ .sort((a, b) => a.name > b.name);
+
+ users = users.filter(u => selected.indexOf(u.id) === -1);
+
+ selectedUsers.forEach(selectedUser => {
+ showDivider += 1;
+ users.splice(showDivider, 0, selectedUser);
+ });
+
+ users.splice(showDivider + 1, 0, { type: 'divider' });
+ }
+ }
+ }
+
+ callback(users);
+ if (showMenuAbove) {
+ $dropdown.data('glDropdown').positionMenuAbove();
+ }
+ },
+ filterable: true,
+ filterRemote: true,
+ search: {
+ fields: ['name', 'username'],
+ },
+ selectable: true,
+ fieldName: $dropdown.data('fieldName'),
+ toggleLabel(selected, el, glDropdown) {
+ const inputValue = glDropdown.filterInput.val();
+
+ if (this.multiSelect && inputValue === '') {
+ // Remove non-users from the fullData array
+ const users = glDropdown.filteredFullData();
+ const callback = glDropdown.parseData.bind(glDropdown);
+
+ // Update the data model
+ this.processData(inputValue, users, callback);
+ }
+
+ if (this.multiSelect) {
+ return getMultiSelectDropdownTitle(selected, $(el).hasClass('is-active'));
+ }
+
+ if (selected && 'id' in selected && $(el).hasClass('is-active')) {
+ $dropdown.find('.dropdown-toggle-text').removeClass('is-default');
+ if (selected.text) {
+ return selected.text;
+ }
+ return selected.name;
+ }
+ $dropdown.find('.dropdown-toggle-text').addClass('is-default');
+ return defaultLabel;
+ },
+ defaultLabel,
+ hidden() {
+ if ($dropdown.hasClass('js-multiselect')) {
+ emitSidebarEvent('sidebar.saveAssignees');
+ }
+
+ if (!$dropdown.data('alwaysShowSelectbox')) {
+ $selectbox.hide();
+
+ // Recalculate where .value is because vue might have changed it
+ $block = $selectbox.closest('.block');
+ $value = $block.find('.value');
+ // display:block overrides the hide-collapse rule
+ $value.css('display', '');
+ }
+ },
+ multiSelect: $dropdown.hasClass('js-multiselect'),
+ inputMeta: $dropdown.data('inputMeta'),
+ clicked(options) {
+ const { $el, e, isMarking } = options;
+ const user = options.selectedObj;
+
+ $el.tooltip('dispose');
+
+ if ($dropdown.hasClass('js-multiselect')) {
+ const isActive = $el.hasClass('is-active');
+ const previouslySelected = $dropdown
+ .closest('.selectbox')
+ .find(`input[name='${$dropdown.data('fieldName')}'][value!=0]`);
+
+ // Enables support for limiting the number of users selected
+ // Automatically removes the first on the list if more users are selected
+ checkMaxSelect();
+
+ if (user.beforeDivider && user.name.toLowerCase() === 'unassigned') {
+ // Unassigned selected
+ previouslySelected.each((index, element) => {
+ element.remove();
+ });
+ emitSidebarEvent('sidebar.removeAllAssignees');
+ } else if (isActive) {
+ // user selected
+ emitSidebarEvent('sidebar.addAssignee', user);
+
+ // Remove unassigned selection (if it was previously selected)
+ const unassignedSelected = $dropdown
+ .closest('.selectbox')
+ .find(`input[name='${$dropdown.data('fieldName')}'][value=0]`);
+
+ if (unassignedSelected) {
+ unassignedSelected.remove();
+ }
+ } else {
+ if (previouslySelected.length === 0) {
+ // Select unassigned because there is no more selected users
+ this.addInput($dropdown.data('fieldName'), 0, {});
+ }
+
+ // User unselected
+ emitSidebarEvent('sidebar.removeAssignee', user);
+ }
+
+ if (getSelected().find(u => u === gon.current_user_id)) {
+ $('.assign-to-me-link').hide();
+ } else {
+ $('.assign-to-me-link').show();
+ }
+ }
+
+ const page = $('body').attr('data-page');
+ const isIssueIndex = page === 'projects:issues:index';
+ const isMRIndex = page === page && page === 'projects:merge_requests:index';
+ if (
+ $dropdown.hasClass('js-filter-bulk-update') ||
+ $dropdown.hasClass('js-issuable-form-dropdown')
+ ) {
+ e.preventDefault();
+
+ const isSelecting = user.id !== selectedId;
+ selectedId = isSelecting ? user.id : selectedIdDefault;
+
+ if (selectedId === gon.current_user_id) {
+ $('.assign-to-me-link').hide();
+ } else {
+ $('.assign-to-me-link').show();
+ }
+ return;
+ }
+ if ($el.closest('.add-issues-modal').length) {
+ ModalStore.store.filter[$dropdown.data('fieldName')] = user.id;
+ } else if (handleClick) {
+ e.preventDefault();
+ handleClick(user, isMarking);
+ } else if ($dropdown.hasClass('js-filter-submit') && (isIssueIndex || isMRIndex)) {
+ return Issuable.filterResults($dropdown.closest('form'));
+ } else if ($dropdown.hasClass('js-filter-submit')) {
+ return $dropdown.closest('form').submit();
+ } else if (!$dropdown.hasClass('js-multiselect')) {
+ const selected = $dropdown
+ .closest('.selectbox')
+ .find(`input[name='${$dropdown.data('fieldName')}']`)
+ .val();
+ return assignTo(selected);
+ }
+
+ // Automatically close dropdown after assignee is selected
+ // since CE has no multiple assignees
+ // EE does not have a max-select
+ if ($dropdown.data('maxSelect') && getSelected().length === $dropdown.data('maxSelect')) {
+ // Close the dropdown
+ $dropdown.dropdown('toggle');
+ }
+ },
+ id(user) {
+ return user.id;
+ },
+ opened(e) {
+ const $el = $(e.currentTarget);
+ const selected = getSelected();
+ if ($dropdown.hasClass('js-issue-board-sidebar') && selected.length === 0) {
+ this.addInput($dropdown.data('fieldName'), 0, {});
+ }
+ $el.find('.is-active').removeClass('is-active');
+
+ function highlightSelected(id) {
+ $el.find(`li[data-user-id="${id}"] .dropdown-menu-user-link`).addClass('is-active');
+ }
+
+ if (selected.length > 0) {
+ getSelected().forEach(selectedId => highlightSelected(selectedId));
+ } else if ($dropdown.hasClass('js-issue-board-sidebar')) {
+ highlightSelected(0);
+ } else {
+ highlightSelected(selectedId);
+ }
+ },
+ updateLabel: $dropdown.data('dropdownTitle'),
+ renderRow(user) {
+ const username = user.username ? `@${user.username}` : '';
+ const avatar = user.avatar_url ? user.avatar_url : gon.default_avatar_url;
+
+ let selected = false;
+
+ if (this.multiSelect) {
+ selected = getSelected().find(u => user.id === u);
+
+ const { fieldName } = this;
+ const field = $dropdown
+ .closest('.selectbox')
+ .find(`input[name='${fieldName}'][value='${user.id}']`);
+
+ if (field.length) {
+ selected = true;
+ }
+ } else {
+ selected = user.id === selectedId;
+ }
+
+ let img = '';
+ if (user.beforeDivider != null) {
+ `<li><a href='#' class='${selected === true ? 'is-active' : ''}'>${escape(
+ user.name,
+ )}</a></li>`;
+ } else {
+ // 0 margin, because it's now handled by a wrapper
+ img = `<img src='${avatar}' class='avatar avatar-inline m-0' width='32' />`;
+ }
+
+ return userSelect.renderRow(options.issuableType, user, selected, username, img);
+ },
+ });
+ });
+ import(/* webpackChunkName: 'select2' */ 'select2/select2')
+ .then(() => {
+ $('.ajax-users-select').each((i, select) => {
+ const options = getAjaxUsersSelectOptions($(select), AJAX_USERS_SELECT_OPTIONS_MAP);
+ options.skipLdap = $(select).hasClass('skip_ldap');
+ const showNullUser = $(select).data('nullUser');
+ const showAnyUser = $(select).data('anyUser');
+ const showEmailUser = $(select).data('emailUser');
+ const firstUser = $(select).data('firstUser');
+ return $(select).select2({
+ placeholder: __('Search for a user'),
+ multiple: $(select).hasClass('multiselect'),
+ minimumInputLength: 0,
+ query(query) {
+ return userSelect.users(query.term, options, users => {
+ let name;
+ const data = {
+ results: users,
+ };
+ if (query.term.length === 0) {
+ if (firstUser) {
+ // Move current user to the front of the list
+ const ref = data.results;
+
+ for (let index = 0, len = ref.length; index < len; index += 1) {
+ const obj = ref[index];
+ if (obj.username === firstUser) {
+ data.results.splice(index, 1);
+ data.results.unshift(obj);
+ break;
+ }
+ }
+ }
+ if (showNullUser) {
+ const nullUser = {
+ name: s__('UsersSelect|Unassigned'),
+ id: 0,
+ };
+ data.results.unshift(nullUser);
+ }
+ if (showAnyUser) {
+ name = showAnyUser;
+ if (name === true) {
+ name = s__('UsersSelect|Any User');
+ }
+ const anyUser = {
+ name,
+ id: null,
+ };
+ data.results.unshift(anyUser);
+ }
+ }
+ if (showEmailUser && data.results.length === 0 && query.term.match(/^[^@]+@[^@]+$/)) {
+ const trimmed = query.term.trim();
+ const emailUser = {
+ name: sprintf(__('Invite "%{trimmed}" by email'), { trimmed }),
+ username: trimmed,
+ id: trimmed,
+ invite: true,
+ };
+ data.results.unshift(emailUser);
+ }
+ return query.callback(data);
+ });
+ },
+ initSelection() {
+ const args = 1 <= arguments.length ? [].slice.call(arguments, 0) : [];
+ return userSelect.initSelection.apply(userSelect, args);
+ },
+ formatResult() {
+ const args = 1 <= arguments.length ? [].slice.call(arguments, 0) : [];
+ return userSelect.formatResult.apply(userSelect, args);
+ },
+ formatSelection() {
+ const args = 1 <= arguments.length ? [].slice.call(arguments, 0) : [];
+ return userSelect.formatSelection.apply(userSelect, args);
+ },
+ dropdownCssClass: 'ajax-users-dropdown',
+ // we do not want to escape markup since we are displaying html in results
+ escapeMarkup(m) {
+ return m;
+ },
+ });
+ });
+ })
+ .catch(() => {});
+}
+
+UsersSelect.prototype.initSelection = function(element, callback) {
+ const id = $(element).val();
+ if (id === '0') {
+ const nullUser = {
+ name: s__('UsersSelect|Unassigned'),
+ };
+ return callback(nullUser);
+ } else if (id !== '') {
+ return this.user(id, callback);
+ }
+};
+
+UsersSelect.prototype.formatResult = function(user) {
+ let avatar = gon.default_avatar_url;
+ if (user.avatar_url) {
+ avatar = user.avatar_url;
+ }
+ return `
+ <div class='user-result'>
+ <div class='user-image'>
+ <img class='avatar avatar-inline s32' src='${avatar}'>
+ </div>
+ <div class='user-info'>
+ <div class='user-name dropdown-menu-user-full-name'>
+ ${escape(user.name)}
+ </div>
+ <div class='user-username dropdown-menu-user-username text-secondary'>
+ ${!user.invite ? `@${escape(user.username)}` : ''}
+ </div>
+ </div>
+ </div>
+ `;
+};
+
+UsersSelect.prototype.formatSelection = function(user) {
+ return escape(user.name);
+};
+
+UsersSelect.prototype.user = function(user_id, callback) {
+ if (!/^\d+$/.test(user_id)) {
+ return false;
+ }
+
+ let url = this.buildUrl(this.userPath);
+ url = url.replace(':id', user_id);
+ return axios.get(url).then(({ data }) => {
+ callback(data);
+ });
+};
+
+// Return users list. Filtered by query
+// Only active users retrieved
+UsersSelect.prototype.users = function(query, options, callback) {
+ const url = this.buildUrl(this.usersPath);
+ const params = {
+ search: query,
+ active: true,
+ ...getAjaxUsersSelectParams(options, AJAX_USERS_SELECT_PARAMS_MAP),
+ };
+
+ if (options.issuableType === 'merge_request') {
+ params.merge_request_iid = options.iid || null;
+ }
+
+ return axios.get(url, { params }).then(({ data }) => {
+ callback(data);
+ });
+};
+
+UsersSelect.prototype.buildUrl = function(url) {
+ if (gon.relative_url_root != null) {
+ url = gon.relative_url_root.replace(/\/$/, '') + url;
+ }
+ return url;
+};
+
+UsersSelect.prototype.renderRow = function(issuableType, user, selected, username, img) {
+ const tooltip = issuableType === 'merge_request' && !user.can_merge ? __('Cannot merge') : '';
+ const tooltipClass = tooltip ? `has-tooltip` : '';
+ const selectedClass = selected === true ? 'is-active' : '';
+ const linkClasses = `${selectedClass} ${tooltipClass}`;
+ const tooltipAttributes = tooltip
+ ? `data-container="body" data-placement="left" data-title="${tooltip}"`
+ : '';
+
+ return `
+ <li data-user-id=${user.id}>
+ <a href="#" class="dropdown-menu-user-link d-flex align-items-center ${linkClasses}" ${tooltipAttributes}>
+ ${this.renderRowAvatar(issuableType, user, img)}
+ <span class="d-flex flex-column overflow-hidden">
+ <strong class="dropdown-menu-user-full-name">
+ ${escape(user.name)}
+ </strong>
+ ${username ? `<span class="dropdown-menu-user-username">${username}</span>` : ''}
+ </span>
+ </a>
+ </li>
+ `;
+};
+
+UsersSelect.prototype.renderRowAvatar = function(issuableType, user, img) {
+ if (user.beforeDivider) {
+ return img;
+ }
+
+ const mergeIcon =
+ issuableType === 'merge_request' && !user.can_merge
+ ? '<i class="fa fa-exclamation-triangle merge-icon"></i>'
+ : '';
+
+ return `<span class="position-relative mr-2">
+ ${img}
+ ${mergeIcon}
+ </span>`;
+};
+
+export default UsersSelect;
diff --git a/app/assets/javascripts/users_select/utils.js b/app/assets/javascripts/users_select/utils.js
new file mode 100644
index 00000000000..b46fd15fb77
--- /dev/null
+++ b/app/assets/javascripts/users_select/utils.js
@@ -0,0 +1,27 @@
+/**
+ * Get options from data attributes on passed `$select`.
+ * @param {jQuery} $select
+ * @param {Object} optionsMap e.g. { optionKeyName: 'dataAttributeName' }
+ */
+export const getAjaxUsersSelectOptions = ($select, optionsMap) => {
+ return Object.keys(optionsMap).reduce((accumulator, optionKey) => {
+ const dataKey = optionsMap[optionKey];
+ accumulator[optionKey] = $select.data(dataKey);
+
+ return accumulator;
+ }, {});
+};
+
+/**
+ * Get query parameters used for users request from passed `options` parameter
+ * @param {Object} options e.g. { currentUserId: 1, fooBar: 'baz' }
+ * @param {Object} paramsMap e.g. { user_id: 'currentUserId', foo_bar: 'fooBar' }
+ */
+export const getAjaxUsersSelectParams = (options, paramsMap) => {
+ return Object.keys(paramsMap).reduce((accumulator, paramKey) => {
+ const optionKey = paramsMap[paramKey];
+ accumulator[paramKey] = options[optionKey] || null;
+
+ return accumulator;
+ }, {});
+};
diff --git a/app/assets/javascripts/vue_merge_request_widget/components/deployment/deployment_info.vue b/app/assets/javascripts/vue_merge_request_widget/components/deployment/deployment_info.vue
index 33db9b87b17..2f922b990d9 100644
--- a/app/assets/javascripts/vue_merge_request_widget/components/deployment/deployment_info.vue
+++ b/app/assets/javascripts/vue_merge_request_widget/components/deployment/deployment_info.vue
@@ -75,7 +75,7 @@ export default {
:href="deployment.url"
target="_blank"
rel="noopener noreferrer nofollow"
- class="js-deploy-meta gl-font-size-12"
+ class="js-deploy-meta gl-font-sm"
>
{{ deployment.name }}
</gl-link>
diff --git a/app/assets/javascripts/vue_merge_request_widget/components/mr_collapsible_extension.vue b/app/assets/javascripts/vue_merge_request_widget/components/mr_collapsible_extension.vue
index ba8da46d207..294871ca5c2 100644
--- a/app/assets/javascripts/vue_merge_request_widget/components/mr_collapsible_extension.vue
+++ b/app/assets/javascripts/vue_merge_request_widget/components/mr_collapsible_extension.vue
@@ -51,7 +51,7 @@ export default {
<div class="mr-widget-extension d-flex align-items-center pl-3">
<div v-if="hasError" class="ci-widget media">
<div class="media-body">
- <span class="gl-font-size-small mr-widget-margin-left gl-line-height-24 js-error-state">{{
+ <span class="gl-font-sm mr-widget-margin-left gl-line-height-24 js-error-state">{{
title
}}</span>
</div>
diff --git a/app/assets/javascripts/vue_merge_request_widget/components/mr_widget_header.vue b/app/assets/javascripts/vue_merge_request_widget/components/mr_widget_header.vue
index c38272ab239..2433ba879aa 100644
--- a/app/assets/javascripts/vue_merge_request_widget/components/mr_widget_header.vue
+++ b/app/assets/javascripts/vue_merge_request_widget/components/mr_widget_header.vue
@@ -1,5 +1,5 @@
<script>
-import { escape as esc } from 'lodash';
+import { escape } from 'lodash';
import { n__, s__, sprintf } from '~/locale';
import { mergeUrlParams, webIDEUrl } from '~/lib/utils/url_utility';
import Icon from '~/vue_shared/components/icon.vue';
@@ -35,7 +35,7 @@ export default {
'mrWidget|The source branch is %{commitsBehindLinkStart}%{commitsBehind}%{commitsBehindLinkEnd} the target branch',
),
{
- commitsBehindLinkStart: `<a href="${esc(this.mr.targetBranchPath)}">`,
+ commitsBehindLinkStart: `<a href="${escape(this.mr.targetBranchPath)}">`,
commitsBehind: n__('%d commit behind', '%d commits behind', this.mr.divergedCommitsCount),
commitsBehindLinkEnd: '</a>',
},
diff --git a/app/assets/javascripts/vue_merge_request_widget/components/mr_widget_pipeline.vue b/app/assets/javascripts/vue_merge_request_widget/components/mr_widget_pipeline.vue
index 42db1935123..6df53311ef0 100644
--- a/app/assets/javascripts/vue_merge_request_widget/components/mr_widget_pipeline.vue
+++ b/app/assets/javascripts/vue_merge_request_widget/components/mr_widget_pipeline.vue
@@ -42,6 +42,10 @@ export default {
type: String,
required: false,
},
+ pipelineMustSucceed: {
+ type: Boolean,
+ required: false,
+ },
sourceBranchLink: {
type: String,
required: false,
@@ -60,7 +64,10 @@ export default {
return this.pipeline && Object.keys(this.pipeline).length > 0;
},
hasCIError() {
- return this.hasCi && !this.ciStatus;
+ return (this.hasCi && !this.ciStatus) || this.hasPipelineMustSucceedConflict;
+ },
+ hasPipelineMustSucceedConflict() {
+ return !this.hasCi && this.pipelineMustSucceed;
},
status() {
return this.pipeline.details && this.pipeline.details.status
@@ -76,9 +83,13 @@ export default {
return this.pipeline.commit && Object.keys(this.pipeline.commit).length > 0;
},
errorText() {
+ if (this.hasPipelineMustSucceedConflict) {
+ return s__('Pipeline|No pipeline has been run for this commit.');
+ }
+
return sprintf(
s__(
- 'Pipeline|Could not retrieve the pipeline status. For troubleshooting steps, read the %{linkStart}documentation.%{linkEnd}',
+ 'Pipeline|Could not retrieve the pipeline status. For troubleshooting steps, read the %{linkStart}documentation%{linkEnd}.',
),
{
linkStart: `<a href="${this.troubleshootingDocsPath}">`,
diff --git a/app/assets/javascripts/vue_merge_request_widget/components/mr_widget_pipeline_container.vue b/app/assets/javascripts/vue_merge_request_widget/components/mr_widget_pipeline_container.vue
index d81e99d3c09..8fba0e2981f 100644
--- a/app/assets/javascripts/vue_merge_request_widget/components/mr_widget_pipeline_container.vue
+++ b/app/assets/javascripts/vue_merge_request_widget/components/mr_widget_pipeline_container.vue
@@ -79,11 +79,12 @@ export default {
:pipeline-coverage-delta="mr.pipelineCoverageDelta"
:ci-status="mr.ciStatus"
:has-ci="mr.hasCI"
+ :pipeline-must-succeed="mr.onlyAllowMergeIfPipelineSucceeds"
:source-branch="branch"
:source-branch-link="branchLink"
:troubleshooting-docs-path="mr.troubleshootingDocsPath"
/>
- <template v-slot:footer>
+ <template #footer>
<div v-if="mr.exposedArtifactsPath" class="js-exposed-artifacts">
<artifacts-app :endpoint="mr.exposedArtifactsPath" />
</div>
diff --git a/app/assets/javascripts/vue_merge_request_widget/components/mr_widget_terraform_plan.vue b/app/assets/javascripts/vue_merge_request_widget/components/mr_widget_terraform_plan.vue
index edf90085a5b..8313b8afb1b 100644
--- a/app/assets/javascripts/vue_merge_request_widget/components/mr_widget_terraform_plan.vue
+++ b/app/assets/javascripts/vue_merge_request_widget/components/mr_widget_terraform_plan.vue
@@ -5,7 +5,6 @@ import axios from '~/lib/utils/axios_utils';
import CiIcon from '../../vue_shared/components/ci_icon.vue';
import flash from '~/flash';
import Poll from '~/lib/utils/poll';
-import Visibility from 'visibilityjs';
export default {
name: 'MRWidgetTerraformPlan',
@@ -68,7 +67,11 @@ export default {
method: 'fetchPlans',
successCallback: ({ data }) => {
this.plans = data;
- this.loading = false;
+
+ if (Object.keys(this.plan).length) {
+ this.loading = false;
+ poll.stop();
+ }
},
errorCallback: () => {
this.plans = {};
@@ -77,17 +80,7 @@ export default {
},
});
- if (!Visibility.hidden()) {
- poll.makeRequest();
- }
-
- Visibility.change(() => {
- if (!Visibility.hidden()) {
- poll.restart();
- } else {
- poll.stop();
- }
- });
+ poll.makeRequest();
},
},
};
diff --git a/app/assets/javascripts/vue_merge_request_widget/components/states/commits_header.vue b/app/assets/javascripts/vue_merge_request_widget/components/states/commits_header.vue
index dcf02a29f52..e4f4032776b 100644
--- a/app/assets/javascripts/vue_merge_request_widget/components/states/commits_header.vue
+++ b/app/assets/javascripts/vue_merge_request_widget/components/states/commits_header.vue
@@ -1,6 +1,6 @@
<script>
import { GlDeprecatedButton } from '@gitlab/ui';
-import { escape as esc } from 'lodash';
+import { escape } from 'lodash';
import { __, n__, sprintf, s__ } from '~/locale';
import Icon from '~/vue_shared/components/icon.vue';
@@ -60,7 +60,7 @@ export default {
{
commitCount: `<strong class="commits-count-message">${this.commitsCountMessage}</strong>`,
mergeCommitCount: `<strong>${s__('mrWidgetCommitsAdded|1 merge commit')}</strong>`,
- targetBranch: `<span class="label-branch">${esc(this.targetBranch)}</span>`,
+ targetBranch: `<span class="label-branch">${escape(this.targetBranch)}</span>`,
},
false,
);
diff --git a/app/assets/javascripts/vue_merge_request_widget/components/states/mr_widget_auto_merge_enabled.vue b/app/assets/javascripts/vue_merge_request_widget/components/states/mr_widget_auto_merge_enabled.vue
index a368e29d086..92848e86e76 100644
--- a/app/assets/javascripts/vue_merge_request_widget/components/states/mr_widget_auto_merge_enabled.vue
+++ b/app/assets/javascripts/vue_merge_request_widget/components/states/mr_widget_auto_merge_enabled.vue
@@ -2,7 +2,7 @@
import autoMergeMixin from 'ee_else_ce/vue_merge_request_widget/mixins/auto_merge';
import Flash from '../../../flash';
import statusIcon from '../mr_widget_status_icon.vue';
-import MrWidgetAuthor from '../../components/mr_widget_author.vue';
+import MrWidgetAuthor from '../mr_widget_author.vue';
import eventHub from '../../event_hub';
import { AUTO_MERGE_STRATEGIES } from '../../constants';
import { __ } from '~/locale';
@@ -52,7 +52,6 @@ export default {
.then(res => res.data)
.then(data => {
eventHub.$emit('UpdateWidgetData', data);
- eventHub.$emit('MRWidgetUpdateRequested');
})
.catch(() => {
this.isCancellingAutoMerge = false;
diff --git a/app/assets/javascripts/vue_merge_request_widget/components/states/mr_widget_closed.vue b/app/assets/javascripts/vue_merge_request_widget/components/states/mr_widget_closed.vue
index a5c75369fa1..302a30dab54 100644
--- a/app/assets/javascripts/vue_merge_request_widget/components/states/mr_widget_closed.vue
+++ b/app/assets/javascripts/vue_merge_request_widget/components/states/mr_widget_closed.vue
@@ -1,5 +1,5 @@
<script>
-import MrWidgetAuthorTime from '../../components/mr_widget_author_time.vue';
+import MrWidgetAuthorTime from '../mr_widget_author_time.vue';
import statusIcon from '../mr_widget_status_icon.vue';
export default {
diff --git a/app/assets/javascripts/vue_merge_request_widget/components/states/mr_widget_conflicts.vue b/app/assets/javascripts/vue_merge_request_widget/components/states/mr_widget_conflicts.vue
index 139cbe17e35..d421b744fa1 100644
--- a/app/assets/javascripts/vue_merge_request_widget/components/states/mr_widget_conflicts.vue
+++ b/app/assets/javascripts/vue_merge_request_widget/components/states/mr_widget_conflicts.vue
@@ -1,6 +1,6 @@
<script>
import $ from 'jquery';
-import { escape as esc } from 'lodash';
+import { escape } from 'lodash';
import { s__, sprintf } from '~/locale';
import { mouseenter, debouncedMouseleave, togglePopover } from '~/shared/popover';
import StatusIcon from '../mr_widget_status_icon.vue';
@@ -50,7 +50,7 @@ export default {
content: sprintf(
s__('mrWidget|%{link_start}Learn more about resolving conflicts%{link_end}'),
{
- link_start: `<a href="${esc(
+ link_start: `<a href="${escape(
this.mr.conflictsDocsPath,
)}" target="_blank" rel="noopener noreferrer">`,
link_end: '</a>',
diff --git a/app/assets/javascripts/vue_merge_request_widget/components/states/mr_widget_merged.vue b/app/assets/javascripts/vue_merge_request_widget/components/states/mr_widget_merged.vue
index 7279aaf0809..1a6e186a371 100644
--- a/app/assets/javascripts/vue_merge_request_widget/components/states/mr_widget_merged.vue
+++ b/app/assets/javascripts/vue_merge_request_widget/components/states/mr_widget_merged.vue
@@ -5,7 +5,7 @@ import Flash from '~/flash';
import tooltip from '~/vue_shared/directives/tooltip';
import { s__, __ } from '~/locale';
import ClipboardButton from '~/vue_shared/components/clipboard_button.vue';
-import MrWidgetAuthorTime from '../../components/mr_widget_author_time.vue';
+import MrWidgetAuthorTime from '../mr_widget_author_time.vue';
import statusIcon from '../mr_widget_status_icon.vue';
import eventHub from '../../event_hub';
diff --git a/app/assets/javascripts/vue_merge_request_widget/components/states/mr_widget_pipeline_tour.vue b/app/assets/javascripts/vue_merge_request_widget/components/states/mr_widget_pipeline_tour.vue
index 01a195049ba..f6bfb178437 100644
--- a/app/assets/javascripts/vue_merge_request_widget/components/states/mr_widget_pipeline_tour.vue
+++ b/app/assets/javascripts/vue_merge_request_widget/components/states/mr_widget_pipeline_tour.vue
@@ -1,5 +1,4 @@
<script>
-import { s__, sprintf } from '~/locale';
import { GlPopover, GlDeprecatedButton } from '@gitlab/ui';
import Icon from '~/vue_shared/components/icon.vue';
import Cookies from 'js-cookie';
@@ -15,18 +14,6 @@ export default {
dismissTrackValue: 20,
showTrackValue: 10,
trackEvent: 'click_button',
- popoverContent: sprintf(
- '%{messageText1}%{lineBreak}%{messageText2}%{lineBreak}%{messageText3}%{lineBreak}%{messageText4}%{lineBreak}%{messageText5}',
- {
- messageText1: s__('mrWidget|Detect issues before deployment with a CI pipeline'),
- messageText2: s__('mrWidget|that continuously tests your code. We created'),
- messageText3: s__("mrWidget|a quick guide that'll show you how to create"),
- messageText4: s__('mrWidget|one. Make your code more secure and more'),
- messageText5: s__('mrWidget|robust in just a minute.'),
- lineBreak: '<br/>',
- },
- false,
- ),
components: {
GlPopover,
GlDeprecatedButton,
@@ -110,7 +97,13 @@ export default {
<div class="svg-content svg-150 pt-1">
<img :src="pipelineSvgPath" />
</div>
- <p v-html="$options.popoverContent"></p>
+ <p>
+ {{
+ s__(
+ 'mrWidget|Detect issues before deployment with a CI pipeline that continuously tests your code. We created a quick guide that will show you how to create one. Make your code more secure and more robust in just a minute.',
+ )
+ }}
+ </p>
<gl-deprecated-button
ref="ok"
category="primary"
diff --git a/app/assets/javascripts/vue_merge_request_widget/components/states/ready_to_merge.vue b/app/assets/javascripts/vue_merge_request_widget/components/states/ready_to_merge.vue
index 360a75c3946..82be5eeb5ff 100644
--- a/app/assets/javascripts/vue_merge_request_widget/components/states/ready_to_merge.vue
+++ b/app/assets/javascripts/vue_merge_request_widget/components/states/ready_to_merge.vue
@@ -1,6 +1,6 @@
<script>
import { isEmpty } from 'lodash';
-import { GlIcon, GlDeprecatedButton } from '@gitlab/ui';
+import { GlIcon, GlDeprecatedButton, GlSprintf, GlLink } from '@gitlab/ui';
import successSvg from 'icons/_icon_status_success.svg';
import warningSvg from 'icons/_icon_status_warning.svg';
import readyToMergeMixin from 'ee_else_ce/vue_merge_request_widget/mixins/ready_to_merge';
@@ -26,6 +26,8 @@ export default {
CommitEdit,
CommitMessageDropdown,
GlIcon,
+ GlSprintf,
+ GlLink,
GlDeprecatedButton,
MergeImmediatelyConfirmationDialog: () =>
import(
@@ -56,7 +58,7 @@ export default {
status() {
const { pipeline, isPipelineFailed, hasCI, ciStatus } = this.mr;
- if (hasCI && !ciStatus) {
+ if ((hasCI && !ciStatus) || this.hasPipelineMustSucceedConflict) {
return 'failed';
} else if (this.isAutoMergeAvailable) {
return 'pending';
@@ -97,6 +99,9 @@ export default {
return __('Merge');
},
+ hasPipelineMustSucceedConflict() {
+ return !this.mr.hasCI && this.mr.onlyAllowMergeIfPipelineSucceeds;
+ },
isRemoveSourceBranchButtonDisabled() {
return this.isMergeButtonDisabled;
},
@@ -343,9 +348,19 @@ export default {
/>
</template>
<template v-else>
- <span class="bold js-resolve-mr-widget-items-message">
- {{ mergeDisabledText }}
- </span>
+ <div class="bold js-resolve-mr-widget-items-message">
+ <gl-sprintf
+ v-if="hasPipelineMustSucceedConflict"
+ :message="pipelineMustSucceedConflictText"
+ >
+ <template #link="{ content }">
+ <gl-link :href="mr.pipelineMustSucceedDocsPath" target="_blank">
+ {{ content }}
+ </gl-link>
+ </template>
+ </gl-sprintf>
+ <gl-sprintf v-else :message="mergeDisabledText" />
+ </div>
</template>
</div>
</div>
diff --git a/app/assets/javascripts/vue_merge_request_widget/components/states/squash_before_merge.vue b/app/assets/javascripts/vue_merge_request_widget/components/states/squash_before_merge.vue
index 98f682c2e8a..5305894873f 100644
--- a/app/assets/javascripts/vue_merge_request_widget/components/states/squash_before_merge.vue
+++ b/app/assets/javascripts/vue_merge_request_widget/components/states/squash_before_merge.vue
@@ -51,7 +51,7 @@ export default {
rel="noopener noreferrer nofollow"
data-container="body"
>
- <icon name="question-o" />
+ <icon name="question" />
</a>
</div>
</template>
diff --git a/app/assets/javascripts/vue_merge_request_widget/event_hub.js b/app/assets/javascripts/vue_merge_request_widget/event_hub.js
index 0948c2e5352..e31806ad199 100644
--- a/app/assets/javascripts/vue_merge_request_widget/event_hub.js
+++ b/app/assets/javascripts/vue_merge_request_widget/event_hub.js
@@ -1,3 +1,3 @@
-import Vue from 'vue';
+import createEventHub from '~/helpers/event_hub_factory';
-export default new Vue();
+export default createEventHub();
diff --git a/app/assets/javascripts/vue_merge_request_widget/mixins/ready_to_merge.js b/app/assets/javascripts/vue_merge_request_widget/mixins/ready_to_merge.js
index 32a2b7b83f4..39fa5e465b8 100644
--- a/app/assets/javascripts/vue_merge_request_widget/mixins/ready_to_merge.js
+++ b/app/assets/javascripts/vue_merge_request_widget/mixins/ready_to_merge.js
@@ -1,6 +1,9 @@
import { __ } from '~/locale';
export const MERGE_DISABLED_TEXT = __('You can only merge once the items above are resolved.');
+export const PIPELINE_MUST_SUCCEED_CONFLICT_TEXT = __(
+ 'Pipelines must succeed for merge requests to be eligible to merge. Please enable pipelines for this project to continue. For more information, see the %{linkStart}documentation.%{linkEnd}',
+);
export default {
computed: {
@@ -16,6 +19,9 @@ export default {
mergeDisabledText() {
return MERGE_DISABLED_TEXT;
},
+ pipelineMustSucceedConflictText() {
+ return PIPELINE_MUST_SUCCEED_CONFLICT_TEXT;
+ },
autoMergeText() {
// MWPS is currently the only auto merge strategy available in CE
return __('Merge when pipeline succeeds');
diff --git a/app/assets/javascripts/vue_merge_request_widget/mr_widget_options.vue b/app/assets/javascripts/vue_merge_request_widget/mr_widget_options.vue
index 05f73c4cdaf..265ff81f39f 100644
--- a/app/assets/javascripts/vue_merge_request_widget/mr_widget_options.vue
+++ b/app/assets/javascripts/vue_merge_request_widget/mr_widget_options.vue
@@ -39,6 +39,7 @@ import SourceBranchRemovalStatus from './components/source_branch_removal_status
import TerraformPlan from './components/mr_widget_terraform_plan.vue';
import GroupedTestReportsApp from '../reports/components/grouped_test_reports_app.vue';
import { setFaviconOverlay } from '../lib/utils/common_utils';
+import GroupedAccessibilityReportsApp from '../reports/accessibility_report/grouped_accessibility_reports_app.vue';
export default {
el: '#js-vue-mr-widget',
@@ -76,6 +77,7 @@ export default {
SourceBranchRemovalStatus,
GroupedTestReportsApp,
TerraformPlan,
+ GroupedAccessibilityReportsApp,
},
props: {
mrData: {
@@ -100,8 +102,11 @@ export default {
shouldRenderMergeHelp() {
return stateMaps.statesToShowHelpWidget.indexOf(this.mr.state) > -1;
},
+ hasPipelineMustSucceedConflict() {
+ return !this.mr.hasCI && this.mr.onlyAllowMergeIfPipelineSucceeds;
+ },
shouldRenderPipelines() {
- return this.mr.hasCI;
+ return this.mr.hasCI || this.hasPipelineMustSucceedConflict;
},
shouldSuggestPipelines() {
return gon.features?.suggestPipeline && !this.mr.hasCI && this.mr.mergeRequestAddCiConfigPath;
@@ -138,6 +143,9 @@ export default {
mergeError,
});
},
+ shouldShowAccessibilityReport() {
+ return this.mr.accessibilityReportPath;
+ },
},
watch: {
state(newVal, oldVal) {
@@ -380,6 +388,11 @@ export default {
<terraform-plan v-if="mr.terraformReportsPath" :endpoint="mr.terraformReportsPath" />
+ <grouped-accessibility-reports-app
+ v-if="shouldShowAccessibilityReport"
+ :endpoint="mr.accessibilityReportPath"
+ />
+
<div class="mr-widget-section">
<component :is="componentName" :mr="mr" :service="service" />
@@ -415,7 +428,9 @@ export default {
<source-branch-removal-status v-if="shouldRenderSourceBranchRemovalStatus" />
</div>
</div>
- <div v-if="shouldRenderMergeHelp" class="mr-widget-footer"><mr-widget-merge-help /></div>
+ <div v-if="shouldRenderMergeHelp" class="mr-widget-footer">
+ <mr-widget-merge-help />
+ </div>
</div>
<mr-widget-pipeline-container
v-if="shouldRenderMergedPipeline"
diff --git a/app/assets/javascripts/vue_merge_request_widget/stores/get_state_key.js b/app/assets/javascripts/vue_merge_request_widget/stores/get_state_key.js
index a298331c1fc..a2ee0bc3ca1 100644
--- a/app/assets/javascripts/vue_merge_request_widget/stores/get_state_key.js
+++ b/app/assets/javascripts/vue_merge_request_widget/stores/get_state_key.js
@@ -21,7 +21,7 @@ export default function deviseState(data) {
return stateKey.unresolvedDiscussions;
} else if (this.isPipelineBlocked) {
return stateKey.pipelineBlocked;
- } else if (this.isSHAMismatch) {
+ } else if (this.canMerge && this.isSHAMismatch) {
return stateKey.shaMismatch;
} else if (this.autoMergeEnabled) {
return this.mergeError ? stateKey.autoMergeFailed : stateKey.autoMergeEnabled;
diff --git a/app/assets/javascripts/vue_merge_request_widget/stores/mr_widget_store.js b/app/assets/javascripts/vue_merge_request_widget/stores/mr_widget_store.js
index 9f001dda540..d61e122d612 100644
--- a/app/assets/javascripts/vue_merge_request_widget/stores/mr_widget_store.js
+++ b/app/assets/javascripts/vue_merge_request_widget/stores/mr_widget_store.js
@@ -103,6 +103,7 @@ export default class MergeRequestStore {
this.ciStatusFaviconPath = pipelineStatus ? pipelineStatus.favicon : null;
this.terraformReportsPath = data.terraform_reports_path;
this.testResultsPath = data.test_reports_path;
+ this.accessibilityReportPath = data.accessibility_report_path;
this.exposedArtifactsPath = data.exposed_artifacts_path;
this.cancelAutoMergePath = data.cancel_auto_merge_path;
this.canCancelAutomaticMerge = Boolean(data.cancel_auto_merge_path);
@@ -123,15 +124,13 @@ export default class MergeRequestStore {
const currentUser = data.current_user;
- if (currentUser) {
- this.cherryPickInForkPath = currentUser.cherry_pick_in_fork_path;
- this.revertInForkPath = currentUser.revert_in_fork_path;
+ this.cherryPickInForkPath = currentUser.cherry_pick_in_fork_path;
+ this.revertInForkPath = currentUser.revert_in_fork_path;
- this.canRemoveSourceBranch = currentUser.can_remove_source_branch || false;
- this.canCreateIssue = currentUser.can_create_issue || false;
- this.canCherryPickInCurrentMR = currentUser.can_cherry_pick_on_current_merge_request || false;
- this.canRevertInCurrentMR = currentUser.can_revert_on_current_merge_request || false;
- }
+ this.canRemoveSourceBranch = currentUser.can_remove_source_branch || false;
+ this.canCreateIssue = currentUser.can_create_issue || false;
+ this.canCherryPickInCurrentMR = currentUser.can_cherry_pick_on_current_merge_request || false;
+ this.canRevertInCurrentMR = currentUser.can_revert_on_current_merge_request || false;
this.setState(data);
}
@@ -162,6 +161,7 @@ export default class MergeRequestStore {
// Paths are set on the first load of the page and not auto-refreshed
this.squashBeforeMergeHelpPath = data.squash_before_merge_help_path;
this.troubleshootingDocsPath = data.troubleshooting_docs_path;
+ this.pipelineMustSucceedDocsPath = data.pipeline_must_succeed_docs_path;
this.mergeRequestBasicPath = data.merge_request_basic_path;
this.mergeRequestWidgetPath = data.merge_request_widget_path;
this.mergeRequestCachedWidgetPath = data.merge_request_cached_widget_path;
diff --git a/app/assets/javascripts/vue_shared/components/awards_list.vue b/app/assets/javascripts/vue_shared/components/awards_list.vue
index 848295cc984..c0a42e08dee 100644
--- a/app/assets/javascripts/vue_shared/components/awards_list.vue
+++ b/app/assets/javascripts/vue_shared/components/awards_list.vue
@@ -34,10 +34,21 @@ export default {
required: false,
default: '',
},
+ defaultAwards: {
+ type: Array,
+ required: false,
+ default: () => [],
+ },
},
computed: {
+ groupedDefaultAwards() {
+ return this.defaultAwards.reduce((obj, key) => Object.assign(obj, { [key]: [] }), {});
+ },
groupedAwards() {
- const { thumbsup, thumbsdown, ...rest } = groupBy(this.awards, x => x.name);
+ const { thumbsup, thumbsdown, ...rest } = {
+ ...this.groupedDefaultAwards,
+ ...groupBy(this.awards, x => x.name),
+ };
return [
...(thumbsup ? [this.createAwardList('thumbsup', thumbsup)] : []),
@@ -73,6 +84,10 @@ export default {
};
},
getAwardListTitle(awardsList) {
+ if (!awardsList.length) {
+ return '';
+ }
+
const hasReactionByCurrentUser = this.hasReactionByCurrentUser(awardsList);
const TOOLTIP_NAME_COUNT = hasReactionByCurrentUser ? 9 : 10;
let awardList = awardsList;
diff --git a/app/assets/javascripts/vue_shared/components/blob_viewers/rich_viewer.vue b/app/assets/javascripts/vue_shared/components/blob_viewers/rich_viewer.vue
index afbfb1e0ee2..52ce05f0d99 100644
--- a/app/assets/javascripts/vue_shared/components/blob_viewers/rich_viewer.vue
+++ b/app/assets/javascripts/vue_shared/components/blob_viewers/rich_viewer.vue
@@ -1,8 +1,12 @@
<script>
+import MarkdownFieldView from '~/vue_shared/components/markdown/field_view.vue';
import ViewerMixin from './mixins';
import { handleBlobRichViewer } from '~/blob/viewer';
export default {
+ components: {
+ MarkdownFieldView,
+ },
mixins: [ViewerMixin],
mounted() {
handleBlobRichViewer(this.$refs.content, this.type);
@@ -10,5 +14,5 @@ export default {
};
</script>
<template>
- <div ref="content" v-html="content"></div>
+ <markdown-field-view ref="content" v-html="content" />
</template>
diff --git a/app/assets/javascripts/vue_shared/components/blob_viewers/simple_viewer.vue b/app/assets/javascripts/vue_shared/components/blob_viewers/simple_viewer.vue
index e64c7132117..1eb05780206 100644
--- a/app/assets/javascripts/vue_shared/components/blob_viewers/simple_viewer.vue
+++ b/app/assets/javascripts/vue_shared/components/blob_viewers/simple_viewer.vue
@@ -44,7 +44,8 @@ export default {
</script>
<template>
<div
- class="file-content code js-syntax-highlight qa-file-content"
+ class="file-content code js-syntax-highlight"
+ data-qa-selector="file_content"
:class="$options.userColorScheme"
>
<div class="line-numbers">
diff --git a/app/assets/javascripts/vue_shared/components/ci_icon.vue b/app/assets/javascripts/vue_shared/components/ci_icon.vue
index 162cfc02959..890dbe86c0d 100644
--- a/app/assets/javascripts/vue_shared/components/ci_icon.vue
+++ b/app/assets/javascripts/vue_shared/components/ci_icon.vue
@@ -1,5 +1,5 @@
<script>
-import Icon from '../../vue_shared/components/icon.vue';
+import Icon from './icon.vue';
/**
* Renders CI icon based on API response shared between all places where it is used.
diff --git a/app/assets/javascripts/vue_shared/components/clipboard_button.vue b/app/assets/javascripts/vue_shared/components/clipboard_button.vue
index d38dd258ce6..0234b6bf848 100644
--- a/app/assets/javascripts/vue_shared/components/clipboard_button.vue
+++ b/app/assets/javascripts/vue_shared/components/clipboard_button.vue
@@ -67,6 +67,7 @@ export default {
<template>
<gl-deprecated-button
v-gl-tooltip="{ placement: tooltipPlacement, container: tooltipContainer }"
+ v-gl-tooltip.hover.blur
:class="cssClass"
:title="title"
:data-clipboard-text="clipboardText"
diff --git a/app/assets/javascripts/vue_shared/components/clone_dropdown.vue b/app/assets/javascripts/vue_shared/components/clone_dropdown.vue
index 7826c179889..ac95c88225e 100644
--- a/app/assets/javascripts/vue_shared/components/clone_dropdown.vue
+++ b/app/assets/javascripts/vue_shared/components/clone_dropdown.vue
@@ -4,7 +4,6 @@ import {
GlNewDropdownHeader,
GlFormInputGroup,
GlButton,
- GlIcon,
GlTooltipDirective,
} from '@gitlab/ui';
import { __, sprintf } from '~/locale';
@@ -16,7 +15,6 @@ export default {
GlNewDropdownHeader,
GlFormInputGroup,
GlButton,
- GlIcon,
},
directives: {
GlTooltip: GlTooltipDirective,
@@ -59,9 +57,10 @@ export default {
v-gl-tooltip.hover
:title="$options.copyURLTooltip"
:data-clipboard-text="sshLink"
- >
- <gl-icon name="copy-to-clipboard" :title="$options.copyURLTooltip" />
- </gl-button>
+ data-qa-selector="copy_ssh_url_button"
+ icon="copy-to-clipboard"
+ class="d-inline-flex"
+ />
</template>
</gl-form-input-group>
</div>
@@ -77,9 +76,10 @@ export default {
v-gl-tooltip.hover
:title="$options.copyURLTooltip"
:data-clipboard-text="httpLink"
- >
- <gl-icon name="copy-to-clipboard" :title="$options.copyURLTooltip" />
- </gl-button>
+ data-qa-selector="copy_http_url_button"
+ icon="copy-to-clipboard"
+ class="d-inline-flex"
+ />
</template>
</gl-form-input-group>
</div>
diff --git a/app/assets/javascripts/vue_shared/components/code_block.vue b/app/assets/javascripts/vue_shared/components/code_block.vue
index 3cca7a86bef..1928bf6dac5 100644
--- a/app/assets/javascripts/vue_shared/components/code_block.vue
+++ b/app/assets/javascripts/vue_shared/components/code_block.vue
@@ -6,11 +6,26 @@ export default {
type: String,
required: true,
},
+ maxHeight: {
+ type: String,
+ required: false,
+ default: 'initial',
+ },
+ },
+ computed: {
+ styleObject() {
+ const { maxHeight } = this;
+ const isScrollable = maxHeight !== 'initial';
+ const scrollableStyles = {
+ maxHeight,
+ overflowY: 'auto',
+ };
+
+ return isScrollable ? scrollableStyles : null;
+ },
},
};
</script>
<template>
- <pre class="code-block rounded">
- <code class="d-block">{{ code }}</code>
- </pre>
+ <pre class="code-block rounded" :style="styleObject"><code class="d-block">{{ code }}</code></pre>
</template>
diff --git a/app/assets/javascripts/vue_shared/components/commit.vue b/app/assets/javascripts/vue_shared/components/commit.vue
index 356f733fb8c..23bea6c28b4 100644
--- a/app/assets/javascripts/vue_shared/components/commit.vue
+++ b/app/assets/javascripts/vue_shared/components/commit.vue
@@ -4,7 +4,7 @@ import { GlTooltipDirective, GlLink } from '@gitlab/ui';
import { __, sprintf } from '~/locale';
import TooltipOnTruncate from '~/vue_shared/components/tooltip_on_truncate.vue';
import UserAvatarLink from './user_avatar/user_avatar_link.vue';
-import Icon from '../../vue_shared/components/icon.vue';
+import Icon from './icon.vue';
export default {
directives: {
diff --git a/app/assets/javascripts/vue_shared/components/content_viewer/content_viewer.vue b/app/assets/javascripts/vue_shared/components/content_viewer/content_viewer.vue
index 2f5e5f35064..fe488ab6cfa 100644
--- a/app/assets/javascripts/vue_shared/components/content_viewer/content_viewer.vue
+++ b/app/assets/javascripts/vue_shared/components/content_viewer/content_viewer.vue
@@ -24,6 +24,11 @@ export default {
required: false,
default: '',
},
+ commitSha: {
+ type: String,
+ required: false,
+ default: '',
+ },
projectPath: {
type: String,
required: false,
@@ -34,6 +39,11 @@ export default {
required: false,
default: '',
},
+ images: {
+ type: Object,
+ required: false,
+ default: () => ({}),
+ },
},
computed: {
viewer() {
@@ -62,6 +72,8 @@ export default {
:file-size="fileSize"
:project-path="projectPath"
:content="content"
+ :images="images"
+ :commit-sha="commitSha"
/>
</div>
</template>
diff --git a/app/assets/javascripts/vue_shared/components/content_viewer/lib/viewer_utils.js b/app/assets/javascripts/vue_shared/components/content_viewer/lib/viewer_utils.js
index da0b45110e2..b7fa73bc197 100644
--- a/app/assets/javascripts/vue_shared/components/content_viewer/lib/viewer_utils.js
+++ b/app/assets/javascripts/vue_shared/components/content_viewer/lib/viewer_utils.js
@@ -26,8 +26,8 @@ const fileExtensionViewers = {
export function viewerInformationForPath(path) {
if (!path) return null;
const name = path.split('/').pop();
- const viewerName =
- fileNameViewers[name] || fileExtensionViewers[name ? name.split('.').pop() : ''] || '';
+ const extension = name.includes('.') && name.split('.').pop();
+ const viewerName = fileNameViewers[name] || fileExtensionViewers[extension];
return viewers[viewerName];
}
diff --git a/app/assets/javascripts/vue_shared/components/content_viewer/viewers/markdown_viewer.vue b/app/assets/javascripts/vue_shared/components/content_viewer/viewers/markdown_viewer.vue
index eb3e489fb8c..1344c766e0e 100644
--- a/app/assets/javascripts/vue_shared/components/content_viewer/viewers/markdown_viewer.vue
+++ b/app/assets/javascripts/vue_shared/components/content_viewer/viewers/markdown_viewer.vue
@@ -1,8 +1,11 @@
<script>
import $ from 'jquery';
+import '~/behaviors/markdown/render_gfm';
+
import { GlSkeletonLoading } from '@gitlab/ui';
import axios from '~/lib/utils/axios_utils';
import { __ } from '~/locale';
+import { forEach, escape } from 'lodash';
const { CancelToken } = axios;
let axiosSource;
@@ -16,6 +19,11 @@ export default {
type: String,
required: true,
},
+ commitSha: {
+ type: String,
+ required: false,
+ default: '',
+ },
filePath: {
type: String,
required: false,
@@ -25,6 +33,11 @@ export default {
type: String,
required: true,
},
+ images: {
+ type: Object,
+ required: false,
+ default: () => ({}),
+ },
},
data() {
return {
@@ -55,6 +68,9 @@ export default {
text: this.content,
path: this.filePath,
};
+ if (this.commitSha) {
+ postBody.ref = this.commitSha;
+ }
const postOptions = {
cancelToken: axiosSource.token,
};
@@ -66,11 +82,19 @@ export default {
postOptions,
)
.then(({ data }) => {
- this.previewContent = data.body;
+ let previewContent = data.body;
+ forEach(this.images, ({ src, title = '', alt }, key) => {
+ previewContent = previewContent.replace(
+ key,
+ `<img src="${escape(src)}" title="${escape(title)}" alt="${escape(alt)}">`,
+ );
+ });
+
+ this.previewContent = previewContent;
this.isLoading = false;
this.$nextTick(() => {
- $(this.$refs['markdown-preview']).renderGFM();
+ $(this.$refs.markdownPreview).renderGFM();
});
})
.catch(() => {
@@ -84,7 +108,7 @@ export default {
</script>
<template>
- <div ref="markdown-preview" class="md-previewer">
+ <div ref="markdownPreview" class="md-previewer">
<gl-skeleton-loading v-if="isLoading" />
<div v-else class="md" v-html="previewContent"></div>
</div>
diff --git a/app/assets/javascripts/vue_shared/components/date_time_picker/date_time_picker.vue b/app/assets/javascripts/vue_shared/components/date_time_picker/date_time_picker.vue
index ffc616d7309..07748482204 100644
--- a/app/assets/javascripts/vue_shared/components/date_time_picker/date_time_picker.vue
+++ b/app/assets/javascripts/vue_shared/components/date_time_picker/date_time_picker.vue
@@ -169,15 +169,15 @@ export default {
menu-class="date-time-picker-menu"
toggle-class="date-time-picker-toggle text-truncate"
>
- <div class="d-flex justify-content-between gl-p-2">
+ <div class="d-flex justify-content-between gl-p-2-deprecated-no-really-do-not-use-me">
<gl-form-group
v-if="customEnabled"
:label="__('Custom range')"
label-for="custom-from-time"
- label-class="gl-pb-1"
- class="custom-time-range-form-group col-md-7 gl-pl-1 gl-pr-0 m-0"
+ label-class="gl-pb-1-deprecated-no-really-do-not-use-me"
+ class="custom-time-range-form-group col-md-7 gl-pl-1-deprecated-no-really-do-not-use-me gl-pr-0 m-0"
>
- <div class="gl-pt-2">
+ <div class="gl-pt-2-deprecated-no-really-do-not-use-me">
<date-time-picker-input
id="custom-time-from"
v-model="startInput"
@@ -198,14 +198,18 @@ export default {
</gl-deprecated-button>
</gl-form-group>
</gl-form-group>
- <gl-form-group label-for="group-id-dropdown" class="col-md-5 gl-pl-1 gl-pr-1 m-0">
+ <gl-form-group
+ label-for="group-id-dropdown"
+ class="col-md-5 gl-pl-1-deprecated-no-really-do-not-use-me gl-pr-1-deprecated-no-really-do-not-use-me m-0"
+ >
<template #label>
- <span class="gl-pl-5">{{ __('Quick range') }}</span>
+ <span class="gl-pl-5-deprecated-no-really-do-not-use-me">{{ __('Quick range') }}</span>
</template>
<gl-dropdown-item
v-for="(option, index) in options"
:key="index"
+ data-qa-selector="quick_range_item"
:active="isOptionActive(option)"
active-class="active"
@click="setQuickRange(option)"
diff --git a/app/assets/javascripts/vue_shared/components/file_finder/item.vue b/app/assets/javascripts/vue_shared/components/file_finder/item.vue
index 73511879ff2..018e3a84c39 100644
--- a/app/assets/javascripts/vue_shared/components/file_finder/item.vue
+++ b/app/assets/javascripts/vue_shared/components/file_finder/item.vue
@@ -1,8 +1,8 @@
<script>
import fuzzaldrinPlus from 'fuzzaldrin-plus';
import Icon from '~/vue_shared/components/icon.vue';
-import FileIcon from '../../../vue_shared/components/file_icon.vue';
-import ChangedFileIcon from '../../../vue_shared/components/changed_file_icon.vue';
+import FileIcon from '../file_icon.vue';
+import ChangedFileIcon from '../changed_file_icon.vue';
const MAX_PATH_LENGTH = 60;
diff --git a/app/assets/javascripts/vue_shared/components/file_icon/file_icon_map.js b/app/assets/javascripts/vue_shared/components/file_icon/file_icon_map.js
index 2f6640232dd..9ecae87c1a9 100644
--- a/app/assets/javascripts/vue_shared/components/file_icon/file_icon_map.js
+++ b/app/assets/javascripts/vue_shared/components/file_icon/file_icon_map.js
@@ -493,6 +493,7 @@ const fileNameIcons = {
'.npmignore': 'npm',
'.npmrc': 'npm',
'.yarnrc': 'yarn',
+ '.yarnrc.yml': 'yarn',
'yarn.lock': 'yarn',
'.yarnclean': 'yarn',
'.yarn-integrity': 'yarn',
@@ -575,6 +576,7 @@ const fileNameIcons = {
'.prettierrc.json': 'prettier',
'.prettierrc.yaml': 'prettier',
'.prettierrc.yml': 'prettier',
+ '.prettierignore': 'prettier',
'nodemon.json': 'nodemon',
'.sonarrc': 'sonar',
browserslist: 'browserlist',
diff --git a/app/assets/javascripts/vue_shared/components/file_row.vue b/app/assets/javascripts/vue_shared/components/file_row.vue
index 0a5cc7b693c..0cc96309a92 100644
--- a/app/assets/javascripts/vue_shared/components/file_row.vue
+++ b/app/assets/javascripts/vue_shared/components/file_row.vue
@@ -148,19 +148,6 @@ export default {
cursor: pointer;
}
-.file-row:hover,
-.file-row:focus {
- background: #f2f2f2;
-}
-
-.file-row:active {
- background: #dfdfdf;
-}
-
-.file-row.is-active {
- background: #f2f2f2;
-}
-
.file-row-name-container {
display: flex;
width: 100%;
diff --git a/app/assets/javascripts/vue_shared/components/form/title.vue b/app/assets/javascripts/vue_shared/components/form/title.vue
index fad69dc1e24..5d6633fa6d7 100644
--- a/app/assets/javascripts/vue_shared/components/form/title.vue
+++ b/app/assets/javascripts/vue_shared/components/form/title.vue
@@ -6,6 +6,7 @@ export default {
GlFormInput,
GlFormGroup,
},
+ inheritAttrs: false,
};
</script>
<template>
diff --git a/app/assets/javascripts/vue_shared/components/gl_mentions.vue b/app/assets/javascripts/vue_shared/components/gl_mentions.vue
index bbf293664a6..508f43afe61 100644
--- a/app/assets/javascripts/vue_shared/components/gl_mentions.vue
+++ b/app/assets/javascripts/vue_shared/components/gl_mentions.vue
@@ -34,7 +34,7 @@ function createMenuItemTemplate({ original }) {
return `${avatarTag}
${original.username}
- <small class="small font-weight-normal gl-color-inherit">${name}${count}</small>
+ <small class="small font-weight-normal gl-reset-color">${name}${count}</small>
${icon}`;
}
diff --git a/app/assets/javascripts/vue_shared/components/identicon.vue b/app/assets/javascripts/vue_shared/components/identicon.vue
index 9dd61c8eada..87a995464fa 100644
--- a/app/assets/javascripts/vue_shared/components/identicon.vue
+++ b/app/assets/javascripts/vue_shared/components/identicon.vue
@@ -4,7 +4,7 @@ import { getIdenticonBackgroundClass, getIdenticonTitle } from '~/helpers/avatar
export default {
props: {
entityId: {
- type: Number,
+ type: [Number, String],
required: true,
},
entityName: {
diff --git a/app/assets/javascripts/vue_shared/components/issue/issue_warning.vue b/app/assets/javascripts/vue_shared/components/issue/issue_warning.vue
index 89a8595fc79..cb3cd18e5a7 100644
--- a/app/assets/javascripts/vue_shared/components/issue/issue_warning.vue
+++ b/app/assets/javascripts/vue_shared/components/issue/issue_warning.vue
@@ -1,11 +1,11 @@
<script>
import { GlLink } from '@gitlab/ui';
-import { escape as esc } from 'lodash';
+import { escape } from 'lodash';
import { __, sprintf } from '~/locale';
-import icon from '../../../vue_shared/components/icon.vue';
+import icon from '../icon.vue';
function buildDocsLinkStart(path) {
- return `<a href="${esc(path)}" target="_blank" rel="noopener noreferrer">`;
+ return `<a href="${escape(path)}" target="_blank" rel="noopener noreferrer">`;
}
export default {
diff --git a/app/assets/javascripts/vue_shared/components/issue/related_issuable_item.vue b/app/assets/javascripts/vue_shared/components/issue/related_issuable_item.vue
index 5d7e9557aff..4f1b1c758b2 100644
--- a/app/assets/javascripts/vue_shared/components/issue/related_issuable_item.vue
+++ b/app/assets/javascripts/vue_shared/components/issue/related_issuable_item.vue
@@ -1,9 +1,9 @@
<script>
import '~/commons/bootstrap';
-import { GlTooltip, GlTooltipDirective } from '@gitlab/ui';
+import { GlIcon, GlTooltip, GlTooltipDirective } from '@gitlab/ui';
import { sprintf } from '~/locale';
-import IssueMilestone from '../../components/issue/issue_milestone.vue';
-import IssueAssignees from '../../components/issue/issue_assignees.vue';
+import IssueMilestone from './issue_milestone.vue';
+import IssueAssignees from './issue_assignees.vue';
import relatedIssuableMixin from '../../mixins/related_issuable_mixin';
import CiIcon from '../ci_icon.vue';
@@ -13,6 +13,7 @@ export default {
IssueMilestone,
IssueAssignees,
CiIcon,
+ GlIcon,
GlTooltip,
},
directives: {
@@ -44,6 +45,9 @@ export default {
visibility: 'hidden',
};
},
+ iconClasses() {
+ return `${this.iconClass} ic-${this.iconName}`;
+ },
},
};
</script>
@@ -54,30 +58,29 @@ export default {
'issuable-info-container': !canReorder,
'card-body': canReorder,
}"
- class="item-body d-flex align-items-center p-2 p-lg-3 py-xl-2 px-xl-3"
+ class="item-body d-flex align-items-center py-2 px-3"
>
<div class="item-contents d-flex align-items-center flex-wrap flex-grow-1 flex-xl-nowrap">
<!-- Title area: Status icon (XL) and title -->
- <div class="item-title d-flex align-items-center mb-xl-0">
- <span ref="iconElementXL">
- <icon
+ <div class="item-title d-flex align-items-xl-center mb-xl-0">
+ <div ref="iconElementXL">
+ <gl-icon
v-if="hasState"
ref="iconElementXL"
- :class="iconClass"
+ class="mr-2 d-block"
+ :class="iconClasses"
:name="iconName"
- :size="16"
:title="stateTitle"
:aria-label="state"
/>
- </span>
+ </div>
<gl-tooltip :target="() => $refs.iconElementXL">
<span v-html="stateTitle"></span>
</gl-tooltip>
- <icon
+ <gl-icon
v-if="confidential"
v-gl-tooltip
name="eye-slash"
- :size="16"
:title="__('Confidential')"
class="confidential-icon append-right-4 align-self-baseline align-self-md-auto mt-xl-0"
:aria-label="__('Confidential')"
@@ -97,17 +100,6 @@ export default {
<div
class="item-path-area item-path-id d-flex align-items-center mr-2 mt-2 mt-xl-0 ml-xl-2"
>
- <span ref="iconElement">
- <icon
- v-if="hasState"
- :class="iconClass"
- :name="iconName"
- :title="stateTitle"
- :aria-label="state"
- data-html="true"
- class="d-xl-none"
- />
- </span>
<gl-tooltip :target="() => this.$refs.iconElement">
<span v-html="stateTitle"></span>
</gl-tooltip>
@@ -159,7 +151,7 @@ export default {
v-gl-tooltip
:disabled="removeDisabled"
type="button"
- class="btn btn-default btn-svg btn-item-remove js-issue-item-remove-button mr-xl-0 align-self-xl-center"
+ class="btn btn-default btn-svg btn-item-remove js-issue-item-remove-button"
data-qa-selector="remove_related_issue_button"
:title="__('Remove')"
:aria-label="__('Remove')"
diff --git a/app/assets/javascripts/vue_shared/components/markdown/field.vue b/app/assets/javascripts/vue_shared/components/markdown/field.vue
index 26e878d56a0..8007ccb91d5 100644
--- a/app/assets/javascripts/vue_shared/components/markdown/field.vue
+++ b/app/assets/javascripts/vue_shared/components/markdown/field.vue
@@ -1,7 +1,7 @@
<script>
import $ from 'jquery';
import '~/behaviors/markdown/render_gfm';
-import { unescape as unesc } from 'lodash';
+import { unescape } from 'lodash';
import { __, sprintf } from '~/locale';
import { stripHtml } from '~/lib/utils/text_utility';
import Flash from '../../../flash';
@@ -115,7 +115,7 @@ export default {
return text;
}
- return unesc(stripHtml(richText).replace(/\n/g, ''));
+ return unescape(stripHtml(richText).replace(/\n/g, ''));
}
return '';
diff --git a/app/assets/javascripts/vue_shared/components/markdown/field_view.vue b/app/assets/javascripts/vue_shared/components/markdown/field_view.vue
new file mode 100644
index 00000000000..d77123371f2
--- /dev/null
+++ b/app/assets/javascripts/vue_shared/components/markdown/field_view.vue
@@ -0,0 +1,19 @@
+<script>
+import $ from 'jquery';
+import '~/behaviors/markdown/render_gfm';
+
+export default {
+ mounted() {
+ this.renderGFM();
+ },
+ methods: {
+ renderGFM() {
+ $(this.$el).renderGFM();
+ },
+ },
+};
+</script>
+
+<template>
+ <div><slot></slot></div>
+</template>
diff --git a/app/assets/javascripts/vue_shared/components/recaptcha_eventhub.js b/app/assets/javascripts/vue_shared/components/recaptcha_eventhub.js
index a4e004c3341..e193883b6e9 100644
--- a/app/assets/javascripts/vue_shared/components/recaptcha_eventhub.js
+++ b/app/assets/javascripts/vue_shared/components/recaptcha_eventhub.js
@@ -1,9 +1,9 @@
-import Vue from 'vue';
+import createEventHub from '~/helpers/event_hub_factory';
// see recaptcha_tags in app/views/shared/_recaptcha_form.html.haml
export const callbackName = 'recaptchaDialogCallback';
-export const eventHub = new Vue();
+export const eventHub = createEventHub();
const throwDuplicateCallbackError = () => {
throw new Error(`${callbackName} is already defined!`);
diff --git a/app/assets/javascripts/vue_shared/components/rich_content_editor/constants.js b/app/assets/javascripts/vue_shared/components/rich_content_editor/constants.js
new file mode 100644
index 00000000000..457f1806452
--- /dev/null
+++ b/app/assets/javascripts/vue_shared/components/rich_content_editor/constants.js
@@ -0,0 +1,37 @@
+import { __ } from '~/locale';
+import { generateToolbarItem } from './toolbar_service';
+
+/* eslint-disable @gitlab/require-i18n-strings */
+const TOOLBAR_ITEM_CONFIGS = [
+ { icon: 'heading', event: 'openHeadingSelect', classes: 'tui-heading', tooltip: __('Headings') },
+ { icon: 'bold', command: 'Bold', tooltip: __('Add bold text') },
+ { icon: 'italic', command: 'Italic', tooltip: __('Add italic text') },
+ { icon: 'strikethrough', command: 'Strike', tooltip: __('Add strikethrough text') },
+ { isDivider: true },
+ { icon: 'quote', command: 'Blockquote', tooltip: __('Insert a quote') },
+ { icon: 'link', event: 'openPopupAddLink', tooltip: __('Add a link') },
+ { icon: 'doc-code', command: 'CodeBlock', tooltip: __('Insert a code block') },
+ { isDivider: true },
+ { icon: 'list-bulleted', command: 'UL', tooltip: __('Add a bullet list') },
+ { icon: 'list-numbered', command: 'OL', tooltip: __('Add a numbered list') },
+ { icon: 'list-task', command: 'Task', tooltip: __('Add a task list') },
+ { icon: 'list-indent', command: 'Indent', tooltip: __('Indent') },
+ { icon: 'list-outdent', command: 'Outdent', tooltip: __('Outdent') },
+ { isDivider: true },
+ { icon: 'dash', command: 'HR', tooltip: __('Add a line') },
+ { icon: 'table', event: 'openPopupAddTable', classes: 'tui-table', tooltip: __('Add a table') },
+ { isDivider: true },
+ { icon: 'code', command: 'Code', tooltip: __('Insert inline code') },
+];
+
+export const EDITOR_OPTIONS = {
+ toolbarItems: TOOLBAR_ITEM_CONFIGS.map(config => generateToolbarItem(config)),
+};
+
+export const EDITOR_TYPES = {
+ wysiwyg: 'wysiwyg',
+};
+
+export const EDITOR_HEIGHT = '100%';
+
+export const EDITOR_PREVIEW_STYLE = 'horizontal';
diff --git a/app/assets/javascripts/vue_shared/components/rich_content_editor/rich_content_editor.vue b/app/assets/javascripts/vue_shared/components/rich_content_editor/rich_content_editor.vue
new file mode 100644
index 00000000000..ba3696c8ad1
--- /dev/null
+++ b/app/assets/javascripts/vue_shared/components/rich_content_editor/rich_content_editor.vue
@@ -0,0 +1,65 @@
+<script>
+import 'codemirror/lib/codemirror.css';
+import '@toast-ui/editor/dist/toastui-editor.css';
+
+import { EDITOR_OPTIONS, EDITOR_TYPES, EDITOR_HEIGHT, EDITOR_PREVIEW_STYLE } from './constants';
+
+export default {
+ components: {
+ ToastEditor: () =>
+ import(/* webpackChunkName: 'toast_editor' */ '@toast-ui/vue-editor').then(
+ toast => toast.Editor,
+ ),
+ },
+ props: {
+ value: {
+ type: String,
+ required: true,
+ },
+ options: {
+ type: Object,
+ required: false,
+ default: () => EDITOR_OPTIONS,
+ },
+ initialEditType: {
+ type: String,
+ required: false,
+ default: EDITOR_TYPES.wysiwyg,
+ },
+ height: {
+ type: String,
+ required: false,
+ default: EDITOR_HEIGHT,
+ },
+ previewStyle: {
+ type: String,
+ required: false,
+ default: EDITOR_PREVIEW_STYLE,
+ },
+ },
+ computed: {
+ editorOptions() {
+ return { ...EDITOR_OPTIONS, ...this.options };
+ },
+ },
+ methods: {
+ onContentChanged() {
+ this.$emit('input', this.getMarkdown());
+ },
+ getMarkdown() {
+ return this.$refs.editor.invoke('getMarkdown');
+ },
+ },
+};
+</script>
+<template>
+ <toast-editor
+ ref="editor"
+ :initial-value="value"
+ :options="editorOptions"
+ :preview-style="previewStyle"
+ :initial-edit-type="initialEditType"
+ :height="height"
+ @change="onContentChanged"
+ />
+</template>
diff --git a/app/assets/javascripts/vue_shared/components/rich_content_editor/toolbar_item.vue b/app/assets/javascripts/vue_shared/components/rich_content_editor/toolbar_item.vue
new file mode 100644
index 00000000000..58aaeef45f2
--- /dev/null
+++ b/app/assets/javascripts/vue_shared/components/rich_content_editor/toolbar_item.vue
@@ -0,0 +1,20 @@
+<script>
+import { GlIcon } from '@gitlab/ui';
+
+export default {
+ components: {
+ GlIcon,
+ },
+ props: {
+ icon: {
+ type: String,
+ required: true,
+ },
+ },
+};
+</script>
+<template>
+ <button class="p-0 gl-display-flex toolbar-button">
+ <gl-icon class="gl-mx-auto" :name="icon" />
+ </button>
+</template>
diff --git a/app/assets/javascripts/vue_shared/components/rich_content_editor/toolbar_service.js b/app/assets/javascripts/vue_shared/components/rich_content_editor/toolbar_service.js
new file mode 100644
index 00000000000..fff90f3e3fb
--- /dev/null
+++ b/app/assets/javascripts/vue_shared/components/rich_content_editor/toolbar_service.js
@@ -0,0 +1,32 @@
+import Vue from 'vue';
+import ToolbarItem from './toolbar_item.vue';
+
+const buildWrapper = propsData => {
+ const instance = new Vue({
+ render(createElement) {
+ return createElement(ToolbarItem, propsData);
+ },
+ });
+
+ instance.$mount();
+ return instance.$el;
+};
+
+// eslint-disable-next-line import/prefer-default-export
+export const generateToolbarItem = config => {
+ const { icon, classes, event, command, tooltip, isDivider } = config;
+
+ if (isDivider) {
+ return 'divider';
+ }
+
+ return {
+ type: 'button',
+ options: {
+ el: buildWrapper({ props: { icon }, class: classes }),
+ event,
+ command,
+ tooltip,
+ },
+ };
+};
diff --git a/app/assets/javascripts/vue_shared/components/sidebar/labels_select/base.vue b/app/assets/javascripts/vue_shared/components/sidebar/labels_select/base.vue
index 44cc11a6aaa..5eef439aa90 100644
--- a/app/assets/javascripts/vue_shared/components/sidebar/labels_select/base.vue
+++ b/app/assets/javascripts/vue_shared/components/sidebar/labels_select/base.vue
@@ -80,11 +80,6 @@ export default {
required: false,
default: false,
},
- scopedLabelsDocumentationLink: {
- type: String,
- required: false,
- default: '#',
- },
},
computed: {
hiddenInputName() {
@@ -136,7 +131,6 @@ export default {
<dropdown-value
:labels="context.labels"
:label-filter-base-path="labelFilterBasePath"
- :scoped-labels-documentation-link="scopedLabelsDocumentationLink"
:enable-scoped-labels="enableScopedLabels"
>
<slot></slot>
@@ -157,7 +151,6 @@ export default {
:namespace="namespace"
:labels="context.labels"
:show-extra-options="!showCreate"
- :scoped-labels-documentation-link="scopedLabelsDocumentationLink"
:enable-scoped-labels="enableScopedLabels"
/>
<div
diff --git a/app/assets/javascripts/vue_shared/components/sidebar/labels_select/dropdown_button.vue b/app/assets/javascripts/vue_shared/components/sidebar/labels_select/dropdown_button.vue
index c3bc61d0053..30f7e6a5980 100644
--- a/app/assets/javascripts/vue_shared/components/sidebar/labels_select/dropdown_button.vue
+++ b/app/assets/javascripts/vue_shared/components/sidebar/labels_select/dropdown_button.vue
@@ -36,11 +36,6 @@ export default {
required: false,
default: false,
},
- scopedLabelsDocumentationLink: {
- type: String,
- required: false,
- default: '#',
- },
},
computed: {
dropdownToggleText() {
@@ -72,7 +67,6 @@ export default {
:data-namespace-path="namespace"
:data-show-any="showExtraOptions"
:data-scoped-labels="enableScopedLabels"
- :data-scoped-labels-documentation-link="scopedLabelsDocumentationLink"
type="button"
class="dropdown-menu-toggle wide js-label-select js-multiselect js-context-config-modal"
data-toggle="dropdown"
diff --git a/app/assets/javascripts/vue_shared/components/sidebar/labels_select/dropdown_value.vue b/app/assets/javascripts/vue_shared/components/sidebar/labels_select/dropdown_value.vue
index fe43f77b1ee..71d7069dd57 100644
--- a/app/assets/javascripts/vue_shared/components/sidebar/labels_select/dropdown_value.vue
+++ b/app/assets/javascripts/vue_shared/components/sidebar/labels_select/dropdown_value.vue
@@ -20,11 +20,6 @@ export default {
required: false,
default: false,
},
- scopedLabelsDocumentationLink: {
- type: String,
- required: false,
- default: '#',
- },
},
computed: {
isEmpty() {
@@ -64,7 +59,6 @@ export default {
:title="label.title"
:description="label.description"
:scoped="showScopedLabels(label)"
- :scoped-labels-documentation-link="scopedLabelsDocumentationLink"
/>
</template>
</div>
diff --git a/app/assets/javascripts/vue_shared/components/sidebar/labels_select_vue/constants.js b/app/assets/javascripts/vue_shared/components/sidebar/labels_select_vue/constants.js
new file mode 100644
index 00000000000..ab652c9356a
--- /dev/null
+++ b/app/assets/javascripts/vue_shared/components/sidebar/labels_select_vue/constants.js
@@ -0,0 +1,5 @@
+// eslint-disable-next-line import/prefer-default-export
+export const DropdownVariant = {
+ Sidebar: 'sidebar',
+ Standalone: 'standalone',
+};
diff --git a/app/assets/javascripts/vue_shared/components/sidebar/labels_select_vue/dropdown_button.vue b/app/assets/javascripts/vue_shared/components/sidebar/labels_select_vue/dropdown_button.vue
index 55fa1e4ef9c..f45c14f8344 100644
--- a/app/assets/javascripts/vue_shared/components/sidebar/labels_select_vue/dropdown_button.vue
+++ b/app/assets/javascripts/vue_shared/components/sidebar/labels_select_vue/dropdown_button.vue
@@ -1,21 +1,35 @@
<script>
-import { mapGetters } from 'vuex';
-import { GlDeprecatedButton, GlIcon } from '@gitlab/ui';
+import { mapActions, mapGetters } from 'vuex';
+import { GlButton, GlIcon } from '@gitlab/ui';
export default {
components: {
- GlDeprecatedButton,
+ GlButton,
GlIcon,
},
computed: {
- ...mapGetters(['dropdownButtonText']),
+ ...mapGetters(['dropdownButtonText', 'isDropdownVariantStandalone']),
+ },
+ methods: {
+ ...mapActions(['toggleDropdownContents']),
+ handleButtonClick(e) {
+ if (this.isDropdownVariantStandalone) {
+ this.toggleDropdownContents();
+ e.stopPropagation();
+ }
+ },
},
};
</script>
<template>
- <gl-deprecated-button class="labels-select-dropdown-button w-100 text-left">
- <span class="dropdown-toggle-text">{{ dropdownButtonText }}</span>
+ <gl-button
+ class="labels-select-dropdown-button js-dropdown-button w-100 text-left"
+ @click="handleButtonClick"
+ >
+ <span class="dropdown-toggle-text flex-fill">
+ {{ dropdownButtonText }}
+ </span>
<gl-icon name="chevron-down" class="pull-right" />
- </gl-deprecated-button>
+ </gl-button>
</template>
diff --git a/app/assets/javascripts/vue_shared/components/sidebar/labels_select_vue/dropdown_contents_create_view.vue b/app/assets/javascripts/vue_shared/components/sidebar/labels_select_vue/dropdown_contents_create_view.vue
index 6bb77f6b6f3..ba8d8391952 100644
--- a/app/assets/javascripts/vue_shared/components/sidebar/labels_select_vue/dropdown_contents_create_view.vue
+++ b/app/assets/javascripts/vue_shared/components/sidebar/labels_select_vue/dropdown_contents_create_view.vue
@@ -1,18 +1,10 @@
<script>
import { mapState, mapActions } from 'vuex';
-import {
- GlTooltipDirective,
- GlDeprecatedButton,
- GlIcon,
- GlFormInput,
- GlLink,
- GlLoadingIcon,
-} from '@gitlab/ui';
+import { GlTooltipDirective, GlButton, GlFormInput, GlLink, GlLoadingIcon } from '@gitlab/ui';
export default {
components: {
- GlDeprecatedButton,
- GlIcon,
+ GlButton,
GlFormInput,
GlLink,
GlLoadingIcon,
@@ -60,25 +52,23 @@ export default {
<template>
<div class="labels-select-contents-create js-labels-create">
<div class="dropdown-title d-flex align-items-center pt-0 pb-2">
- <gl-deprecated-button
+ <gl-button
:aria-label="__('Go back')"
variant="link"
- size="sm"
+ size="small"
class="js-btn-back dropdown-header-button p-0"
+ icon="arrow-left"
@click="toggleDropdownContentsCreateView"
- >
- <gl-icon name="arrow-left" />
- </gl-deprecated-button>
+ />
<span class="flex-grow-1">{{ labelsCreateTitle }}</span>
- <gl-deprecated-button
+ <gl-button
:aria-label="__('Close')"
variant="link"
- size="sm"
+ size="small"
class="dropdown-header-button p-0"
+ icon="close"
@click="toggleDropdownContents"
- >
- <gl-icon name="close" />
- </gl-deprecated-button>
+ />
</div>
<div class="dropdown-input">
<gl-form-input
@@ -107,21 +97,19 @@ export default {
</div>
</div>
<div class="dropdown-actions clearfix pt-2 px-2">
- <gl-deprecated-button
+ <gl-button
:disabled="disableCreate"
- variant="primary"
+ category="primary"
+ variant="success"
class="pull-left d-flex align-items-center"
@click="handleCreateClick"
>
<gl-loading-icon v-show="labelCreateInProgress" :inline="true" class="mr-1" />
{{ __('Create') }}
- </gl-deprecated-button>
- <gl-deprecated-button
- class="pull-right js-btn-cancel-create"
- @click="toggleDropdownContentsCreateView"
- >
+ </gl-button>
+ <gl-button class="pull-right js-btn-cancel-create" @click="toggleDropdownContentsCreateView">
{{ __('Cancel') }}
- </gl-deprecated-button>
+ </gl-button>
</div>
</div>
</template>
diff --git a/app/assets/javascripts/vue_shared/components/sidebar/labels_select_vue/dropdown_contents_labels_view.vue b/app/assets/javascripts/vue_shared/components/sidebar/labels_select_vue/dropdown_contents_labels_view.vue
index a8e48bfe1a1..1ef2e8b3bed 100644
--- a/app/assets/javascripts/vue_shared/components/sidebar/labels_select_vue/dropdown_contents_labels_view.vue
+++ b/app/assets/javascripts/vue_shared/components/sidebar/labels_select_vue/dropdown_contents_labels_view.vue
@@ -1,16 +1,18 @@
<script>
import { mapState, mapGetters, mapActions } from 'vuex';
-import { GlLoadingIcon, GlDeprecatedButton, GlIcon, GlSearchBoxByType, GlLink } from '@gitlab/ui';
+import { GlLoadingIcon, GlButton, GlSearchBoxByType, GlLink } from '@gitlab/ui';
import { UP_KEY_CODE, DOWN_KEY_CODE, ENTER_KEY_CODE, ESC_KEY_CODE } from '~/lib/utils/keycodes';
+import LabelItem from './label_item.vue';
+
export default {
components: {
GlLoadingIcon,
- GlDeprecatedButton,
- GlIcon,
+ GlButton,
GlSearchBoxByType,
GlLink,
+ LabelItem,
},
data() {
return {
@@ -20,6 +22,8 @@ export default {
},
computed: {
...mapState([
+ 'allowLabelCreate',
+ 'allowMultiselect',
'labelsManagePath',
'labels',
'labelsFetchInProgress',
@@ -27,7 +31,7 @@ export default {
'footerCreateLabelTitle',
'footerManageLabelTitle',
]),
- ...mapGetters(['selectedLabelsList']),
+ ...mapGetters(['selectedLabelsList', 'isDropdownVariantSidebar']),
visibleLabels() {
if (this.searchKey) {
return this.labels.filter(label =>
@@ -56,12 +60,8 @@ export default {
'toggleDropdownContentsCreateView',
'fetchLabels',
'updateSelectedLabels',
+ 'toggleDropdownContents',
]),
- getDropdownLabelBoxStyle(label) {
- return {
- backgroundColor: label.color,
- };
- },
isLabelSelected(label) {
return this.selectedLabelsList.includes(label.id);
},
@@ -111,6 +111,7 @@ export default {
},
handleLabelClick(label) {
this.updateSelectedLabels([label]);
+ if (!this.allowMultiselect) this.toggleDropdownContents();
},
},
};
@@ -123,54 +124,47 @@ export default {
class="labels-fetch-loading position-absolute d-flex align-items-center w-100 h-100"
size="md"
/>
- <div class="dropdown-title d-flex align-items-center pt-0 pb-2">
+ <div v-if="isDropdownVariantSidebar" class="dropdown-title d-flex align-items-center pt-0 pb-2">
<span class="flex-grow-1">{{ labelsListTitle }}</span>
- <gl-deprecated-button
+ <gl-button
:aria-label="__('Close')"
variant="link"
- size="sm"
+ size="small"
class="dropdown-header-button p-0"
+ icon="close"
@click="toggleDropdownContents"
- >
- <gl-icon name="close" />
- </gl-deprecated-button>
+ />
</div>
- <div class="dropdown-input">
+ <div class="dropdown-input" @click.stop="() => {}">
<gl-search-box-by-type v-model="searchKey" :autofocus="true" />
</div>
- <div v-if="!labelsFetchInProgress" ref="labelsListContainer" class="dropdown-content">
+ <div v-show="!labelsFetchInProgress" ref="labelsListContainer" class="dropdown-content">
<ul class="list-unstyled mb-0">
<li v-for="(label, index) in visibleLabels" :key="label.id" class="d-block text-left">
- <gl-link
- class="d-flex align-items-baseline text-break-word label-item"
- :class="{ 'is-focused': index === currentHighlightItem }"
- @click="handleLabelClick(label)"
- >
- <gl-icon v-show="label.set" name="mobile-issue-close" class="mr-2 align-self-center" />
- <span v-show="!label.set" class="mr-3 pr-2"></span>
- <span class="dropdown-label-box" :style="getDropdownLabelBoxStyle(label)"></span>
- <span>{{ label.title }}</span>
- </gl-link>
+ <label-item
+ :label="label"
+ :highlight="index === currentHighlightItem"
+ @clickLabel="handleLabelClick(label)"
+ />
</li>
- <li v-if="!visibleLabels.length" class="p-2 text-center">
+ <li v-show="!visibleLabels.length" class="p-2 text-center">
{{ __('No matching results') }}
</li>
</ul>
</div>
- <div class="dropdown-footer">
+ <div v-if="isDropdownVariantSidebar" class="dropdown-footer">
<ul class="list-unstyled">
- <li>
- <gl-deprecated-button
- variant="link"
+ <li v-if="allowLabelCreate">
+ <gl-link
class="d-flex w-100 flex-row text-break-word label-item"
@click="toggleDropdownContentsCreateView"
- >{{ footerCreateLabelTitle }}</gl-deprecated-button
+ >{{ footerCreateLabelTitle }}</gl-link
>
</li>
<li>
- <gl-link :href="labelsManagePath" class="d-flex flex-row text-break-word label-item">
- {{ footerManageLabelTitle }}
- </gl-link>
+ <gl-link :href="labelsManagePath" class="d-flex flex-row text-break-word label-item">{{
+ footerManageLabelTitle
+ }}</gl-link>
</li>
</ul>
</div>
diff --git a/app/assets/javascripts/vue_shared/components/sidebar/labels_select_vue/dropdown_value.vue b/app/assets/javascripts/vue_shared/components/sidebar/labels_select_vue/dropdown_value.vue
index 695af775750..12ad2acf308 100644
--- a/app/assets/javascripts/vue_shared/components/sidebar/labels_select_vue/dropdown_value.vue
+++ b/app/assets/javascripts/vue_shared/components/sidebar/labels_select_vue/dropdown_value.vue
@@ -9,12 +9,7 @@ export default {
GlLabel,
},
computed: {
- ...mapState([
- 'selectedLabels',
- 'allowScopedLabels',
- 'labelsFilterBasePath',
- 'scopedLabelsDocumentationPath',
- ]),
+ ...mapState(['selectedLabels', 'allowScopedLabels', 'labelsFilterBasePath']),
},
methods: {
labelFilterUrl(label) {
@@ -45,7 +40,6 @@ export default {
:background-color="label.color"
:target="labelFilterUrl(label)"
:scoped="scopedLabel(label)"
- :scoped-labels-documentation-link="scopedLabelsDocumentationPath"
tooltip-placement="top"
/>
</template>
diff --git a/app/assets/javascripts/vue_shared/components/sidebar/labels_select_vue/label_item.vue b/app/assets/javascripts/vue_shared/components/sidebar/labels_select_vue/label_item.vue
new file mode 100644
index 00000000000..c95221d71b5
--- /dev/null
+++ b/app/assets/javascripts/vue_shared/components/sidebar/labels_select_vue/label_item.vue
@@ -0,0 +1,52 @@
+<script>
+import { GlIcon, GlLink } from '@gitlab/ui';
+
+export default {
+ components: {
+ GlIcon,
+ GlLink,
+ },
+ props: {
+ label: {
+ type: Object,
+ required: true,
+ },
+ highlight: {
+ type: Boolean,
+ required: false,
+ default: false,
+ },
+ },
+ data() {
+ return {
+ isSet: this.label.set,
+ };
+ },
+ computed: {
+ labelBoxStyle() {
+ return {
+ backgroundColor: this.label.color,
+ };
+ },
+ },
+ methods: {
+ handleClick() {
+ this.isSet = !this.isSet;
+ this.$emit('clickLabel', this.label);
+ },
+ },
+};
+</script>
+
+<template>
+ <gl-link
+ class="d-flex align-items-baseline text-break-word label-item"
+ :class="{ 'is-focused': highlight }"
+ @click="handleClick"
+ >
+ <gl-icon v-show="isSet" name="mobile-issue-close" class="mr-2 align-self-center" />
+ <span v-show="!isSet" data-testid="no-icon" class="mr-3 pr-2"></span>
+ <span class="dropdown-label-box" data-testid="label-color-box" :style="labelBoxStyle"></span>
+ <span>{{ label.title }}</span>
+ </gl-link>
+</template>
diff --git a/app/assets/javascripts/vue_shared/components/sidebar/labels_select_vue/labels_select_root.vue b/app/assets/javascripts/vue_shared/components/sidebar/labels_select_vue/labels_select_root.vue
index 78102caacf5..f38b66fdfdf 100644
--- a/app/assets/javascripts/vue_shared/components/sidebar/labels_select_vue/labels_select_root.vue
+++ b/app/assets/javascripts/vue_shared/components/sidebar/labels_select_vue/labels_select_root.vue
@@ -1,7 +1,7 @@
<script>
import $ from 'jquery';
import Vue from 'vue';
-import Vuex, { mapState, mapActions } from 'vuex';
+import Vuex, { mapState, mapActions, mapGetters } from 'vuex';
import { __ } from '~/locale';
import DropdownValueCollapsed from '~/vue_shared/components/sidebar/labels_select/dropdown_value_collapsed.vue';
@@ -13,6 +13,8 @@ import DropdownValue from './dropdown_value.vue';
import DropdownButton from './dropdown_button.vue';
import DropdownContents from './dropdown_contents.vue';
+import { DropdownVariant } from './constants';
+
Vue.use(Vuex);
export default {
@@ -33,14 +35,19 @@ export default {
type: Boolean,
required: true,
},
+ allowMultiselect: {
+ type: Boolean,
+ required: false,
+ default: false,
+ },
allowScopedLabels: {
type: Boolean,
required: true,
},
- dropdownOnly: {
- type: Boolean,
+ variant: {
+ type: String,
required: false,
- default: false,
+ default: DropdownVariant.Sidebar,
},
selectedLabels: {
type: Array,
@@ -67,11 +74,6 @@ export default {
required: false,
default: '',
},
- scopedLabelsDocumentationPath: {
- type: String,
- required: false,
- default: '',
- },
labelsListTitle: {
type: String,
required: false,
@@ -95,6 +97,10 @@ export default {
},
computed: {
...mapState(['showDropdownButton', 'showDropdownContents']),
+ ...mapGetters(['isDropdownVariantSidebar', 'isDropdownVariantStandalone']),
+ dropdownButtonVisible() {
+ return this.isDropdownVariantSidebar ? this.showDropdownButton : true;
+ },
},
watch: {
selectedLabels(selectedLabels) {
@@ -105,15 +111,15 @@ export default {
},
mounted() {
this.setInitialState({
- dropdownOnly: this.dropdownOnly,
+ variant: this.variant,
allowLabelEdit: this.allowLabelEdit,
allowLabelCreate: this.allowLabelCreate,
+ allowMultiselect: this.allowMultiselect,
allowScopedLabels: this.allowScopedLabels,
selectedLabels: this.selectedLabels,
labelsFetchPath: this.labelsFetchPath,
labelsManagePath: this.labelsManagePath,
labelsFilterBasePath: this.labelsFilterBasePath,
- scopedLabelsDocumentationPath: this.scopedLabelsDocumentationPath,
labelsListTitle: this.labelsListTitle,
labelsCreateTitle: this.labelsCreateTitle,
footerCreateLabelTitle: this.footerCreateLabelTitle,
@@ -154,13 +160,24 @@ export default {
// as the dropdown wrapper is not using `GlDropdown` as
// it will also require us to use `BDropdownForm`
// which is yet to be implemented in GitLab UI.
+ const hasExceptionClass = [
+ 'js-dropdown-button',
+ 'js-btn-cancel-create',
+ 'js-sidebar-dropdown-toggle',
+ ].some(
+ className =>
+ target?.classList.contains(className) ||
+ target?.parentElement.classList.contains(className),
+ );
+
+ const hadExceptionParent = ['.js-btn-back', '.js-labels-list'].some(
+ className => $(target).parents(className).length,
+ );
+
if (
- this.showDropdownButton &&
this.showDropdownContents &&
- !$(target).parents('.js-btn-back').length &&
- !$(target).parents('.js-labels-list').length &&
- !target?.classList.contains('js-btn-cancel-create') &&
- !target?.classList.contains('js-sidebar-dropdown-toggle') &&
+ !hadExceptionParent &&
+ !hasExceptionClass &&
!this.$refs.dropdownButtonCollapsed?.$el.contains(target) &&
!this.$refs.dropdownContents?.$el.contains(target)
) {
@@ -181,10 +198,12 @@ export default {
</script>
<template>
- <div class="labels-select-wrapper position-relative">
- <div v-if="!dropdownOnly">
+ <div
+ class="labels-select-wrapper position-relative"
+ :class="{ 'is-standalone': isDropdownVariantStandalone }"
+ >
+ <template v-if="isDropdownVariantSidebar">
<dropdown-value-collapsed
- v-if="allowLabelCreate"
ref="dropdownButtonCollapsed"
:labels="selectedLabels"
@onValueClick="handleCollapsedValueClick"
@@ -196,8 +215,18 @@ export default {
<dropdown-value v-show="!showDropdownButton">
<slot></slot>
</dropdown-value>
- <dropdown-button v-show="showDropdownButton" />
- <dropdown-contents v-if="showDropdownButton && showDropdownContents" ref="dropdownContents" />
- </div>
+ <dropdown-button v-show="dropdownButtonVisible" />
+ <dropdown-contents
+ v-if="dropdownButtonVisible && showDropdownContents"
+ ref="dropdownContents"
+ />
+ </template>
+ <template v-if="isDropdownVariantStandalone">
+ <dropdown-button v-show="dropdownButtonVisible" />
+ <dropdown-contents
+ v-if="dropdownButtonVisible && showDropdownContents"
+ ref="dropdownContents"
+ />
+ </template>
</div>
</template>
diff --git a/app/assets/javascripts/vue_shared/components/sidebar/labels_select_vue/store/getters.js b/app/assets/javascripts/vue_shared/components/sidebar/labels_select_vue/store/getters.js
index c08a8a8ea58..c39222959a9 100644
--- a/app/assets/javascripts/vue_shared/components/sidebar/labels_select_vue/store/getters.js
+++ b/app/assets/javascripts/vue_shared/components/sidebar/labels_select_vue/store/getters.js
@@ -1,4 +1,5 @@
import { __, s__, sprintf } from '~/locale';
+import { DropdownVariant } from '../constants';
/**
* Returns string representing current labels
@@ -6,8 +7,11 @@ import { __, s__, sprintf } from '~/locale';
*
* @param {object} state
*/
-export const dropdownButtonText = state => {
- const selectedLabels = state.labels.filter(label => label.set);
+export const dropdownButtonText = (state, getters) => {
+ const selectedLabels = getters.isDropdownVariantSidebar
+ ? state.labels.filter(label => label.set)
+ : state.selectedLabels;
+
if (!selectedLabels.length) {
return __('Label');
} else if (selectedLabels.length > 1) {
@@ -26,5 +30,19 @@ export const dropdownButtonText = state => {
*/
export const selectedLabelsList = state => state.selectedLabels.map(label => label.id);
+/**
+ * Returns boolean representing whether dropdown variant
+ * is `sidebar`
+ * @param {object} state
+ */
+export const isDropdownVariantSidebar = state => state.variant === DropdownVariant.Sidebar;
+
+/**
+ * Returns boolean representing whether dropdown variant
+ * is `standalone`
+ * @param {object} state
+ */
+export const isDropdownVariantStandalone = state => state.variant === DropdownVariant.Standalone;
+
// prevent babel-plugin-rewire from generating an invalid default during karma tests
export default () => {};
diff --git a/app/assets/javascripts/vue_shared/components/sidebar/labels_select_vue/store/mutations.js b/app/assets/javascripts/vue_shared/components/sidebar/labels_select_vue/store/mutations.js
index 32a78507e88..54f8c78b4e1 100644
--- a/app/assets/javascripts/vue_shared/components/sidebar/labels_select_vue/store/mutations.js
+++ b/app/assets/javascripts/vue_shared/components/sidebar/labels_select_vue/store/mutations.js
@@ -1,4 +1,5 @@
import * as types from './mutation_types';
+import { DropdownVariant } from '../constants';
export default {
[types.SET_INITIAL_STATE](state, props) {
@@ -10,7 +11,7 @@ export default {
},
[types.TOGGLE_DROPDOWN_CONTENTS](state) {
- if (!state.dropdownOnly) {
+ if (state.variant === DropdownVariant.Sidebar) {
state.showDropdownButton = !state.showDropdownButton;
}
state.showDropdownContents = !state.showDropdownContents;
@@ -57,20 +58,13 @@ export default {
},
[types.UPDATE_SELECTED_LABELS](state, { labels }) {
- // Iterate over all the labels and update
- // `set` prop value to represent their current state.
- const labelIds = labels.map(label => label.id);
- state.labels = state.labels.reduce((allLabels, label) => {
- if (labelIds.includes(label.id)) {
- allLabels.push({
- ...label,
- touched: true,
- set: !label.set,
- });
- } else {
- allLabels.push(label);
- }
- return allLabels;
- }, []);
+ // Find the label to update from all the labels
+ // and change `set` prop value to represent their current state.
+ const labelId = labels.pop()?.id;
+ const candidateLabel = state.labels.find(label => labelId === label.id);
+ if (candidateLabel) {
+ candidateLabel.touched = true;
+ candidateLabel.set = !candidateLabel.set;
+ }
},
};
diff --git a/app/assets/javascripts/vue_shared/components/sidebar/labels_select_vue/store/state.js b/app/assets/javascripts/vue_shared/components/sidebar/labels_select_vue/store/state.js
index ceabc696693..6a6c0b4c0ee 100644
--- a/app/assets/javascripts/vue_shared/components/sidebar/labels_select_vue/store/state.js
+++ b/app/assets/javascripts/vue_shared/components/sidebar/labels_select_vue/store/state.js
@@ -11,13 +11,13 @@ export default () => ({
namespace: '',
labelsFetchPath: '',
labelsFilterBasePath: '',
- scopedLabelsDocumentationPath: '#',
// UI Flags
+ variant: '',
allowLabelCreate: false,
allowLabelEdit: false,
allowScopedLabels: false,
- dropdownOnly: false,
+ allowMultiselect: false,
showDropdownButton: false,
showDropdownContents: false,
showDropdownContentsCreateView: false,
diff --git a/app/assets/javascripts/vue_shared/components/user_avatar/badges/gitlab_team_member_badge.vue b/app/assets/javascripts/vue_shared/components/user_avatar/badges/gitlab_team_member_badge.vue
deleted file mode 100644
index 527cbd458e2..00000000000
--- a/app/assets/javascripts/vue_shared/components/user_avatar/badges/gitlab_team_member_badge.vue
+++ /dev/null
@@ -1,27 +0,0 @@
-<script>
-import { GlTooltipDirective, GlIcon } from '@gitlab/ui';
-import { __ } from '~/locale';
-
-const GITLAB_TEAM_MEMBER_LABEL = __('GitLab Team Member');
-
-export default {
- name: 'GitlabTeamMemberBadge',
- directives: {
- GlTooltip: GlTooltipDirective,
- },
- components: { GlIcon },
- gitlabTeamMemberLabel: GITLAB_TEAM_MEMBER_LABEL,
-};
-</script>
-
-<template>
- <span
- v-gl-tooltip.hover
- :title="$options.gitlabTeamMemberLabel"
- role="img"
- :aria-label="$options.gitlabTeamMemberLabel"
- class="d-inline-block align-middle"
- >
- <gl-icon name="tanuki-verified" class="gl-text-purple d-block" />
- </span>
-</template>
diff --git a/app/assets/javascripts/vue_shared/components/user_avatar/user_avatar_svg.vue b/app/assets/javascripts/vue_shared/components/user_avatar/user_avatar_svg.vue
deleted file mode 100644
index 7ed4da84120..00000000000
--- a/app/assets/javascripts/vue_shared/components/user_avatar/user_avatar_svg.vue
+++ /dev/null
@@ -1,38 +0,0 @@
-<script>
-/* This is a re-usable vue component for rendering a user avatar svg (typically
- for a blank state). It will receive styles comparable to the user avatar,
- but no image is loaded, it isn't wrapped in a link, and tooltips aren't supported.
- The svg and avatar size can be configured by props passed to this component.
-
- Sample configuration:
-
- <user-avatar-svg
- :svg="potentialApproverSvg"
- :size="20"
- />
-
-*/
-
-export default {
- props: {
- svg: {
- type: String,
- required: true,
- },
- size: {
- type: Number,
- required: false,
- default: 20,
- },
- },
- computed: {
- avatarSizeClass() {
- return `s${this.size}`;
- },
- },
-};
-</script>
-
-<template>
- <svg :class="avatarSizeClass" :height="size" :width="size" v-html="svg" />
-</template>
diff --git a/app/assets/javascripts/vue_shared/mixins/ci_pagination_api_mixin.js b/app/assets/javascripts/vue_shared/mixins/ci_pagination_api_mixin.js
index f9e3f3df0cc..c93b3d37a63 100644
--- a/app/assets/javascripts/vue_shared/mixins/ci_pagination_api_mixin.js
+++ b/app/assets/javascripts/vue_shared/mixins/ci_pagination_api_mixin.js
@@ -9,21 +9,46 @@ import { historyPushState, buildUrlWithCurrentLocation } from '../../lib/utils/c
export default {
methods: {
onChangeTab(scope) {
- this.updateContent({ scope, page: '1' });
+ let params = {
+ scope,
+ page: '1',
+ };
+
+ params = this.onChangeWithFilter(params);
+
+ this.updateContent(params);
},
onChangePage(page) {
/* URLS parameters are strings, we need to parse to match types */
- const params = {
+ let params = {
page: Number(page).toString(),
};
if (this.scope) {
params.scope = this.scope;
}
+
+ params = this.onChangeWithFilter(params);
+
this.updateContent(params);
},
+ onChangeWithFilter(params) {
+ const { username, ref } = this.requestData;
+ const paramsData = params;
+
+ if (username) {
+ paramsData.username = username;
+ }
+
+ if (ref) {
+ paramsData.ref = ref;
+ }
+
+ return paramsData;
+ },
+
updateInternalState(parameters) {
// stop polling
this.poll.stop();
diff --git a/app/assets/stylesheets/application.scss b/app/assets/stylesheets/application.scss
index 657e52674db..cc4d13db150 100644
--- a/app/assets/stylesheets/application.scss
+++ b/app/assets/stylesheets/application.scss
@@ -37,4 +37,12 @@
@import "application_ee";
// CSS util classes
+/**
+ These are deprecated in favor of the Gitlab UI utilities imported below.
+ Please check https://unpkg.com/browse/@gitlab/ui/src/scss/utilities.scss
+ to see the available utility classes.
+**/
@import "utilities";
+
+// Gitlab UI util classes
+@import "@gitlab/ui/src/scss/utilities";
diff --git a/app/assets/stylesheets/bootstrap_migration.scss b/app/assets/stylesheets/bootstrap_migration.scss
index ed5c133950d..1c15400542a 100644
--- a/app/assets/stylesheets/bootstrap_migration.scss
+++ b/app/assets/stylesheets/bootstrap_migration.scss
@@ -211,7 +211,7 @@ h3.popover-header {
}
.info-well {
- background: $gray-50;
+ background: $gray-10;
color: $gl-text-color;
border: 1px solid $border-color;
border-radius: 4px;
diff --git a/app/assets/stylesheets/components/dashboard_skeleton.scss b/app/assets/stylesheets/components/dashboard_skeleton.scss
index 2e2c1fefc79..ce33aa94df3 100644
--- a/app/assets/stylesheets/components/dashboard_skeleton.scss
+++ b/app/assets/stylesheets/components/dashboard_skeleton.scss
@@ -68,7 +68,7 @@
background-size: cover;
background-image: linear-gradient(to right,
$gray-100 0%,
- $gray-50 20%,
+ $gray-10 20%,
$gray-100 40%,
$gray-100 100%);
border-radius: $gl-padding;
diff --git a/app/assets/stylesheets/components/design_management/design.scss b/app/assets/stylesheets/components/design_management/design.scss
new file mode 100644
index 00000000000..1061aae2bbb
--- /dev/null
+++ b/app/assets/stylesheets/components/design_management/design.scss
@@ -0,0 +1,140 @@
+.design-detail {
+ background-color: rgba($black, 0.9);
+
+ .with-performance-bar & {
+ top: 35px;
+ }
+
+ .inactive {
+ opacity: 0.5;
+ }
+}
+
+.design-presentation-wrapper {
+ top: 0;
+ left: 0;
+}
+
+.design-scaler {
+ z-index: 1;
+}
+
+.design-scaler-wrapper {
+ bottom: 0;
+ left: 50%;
+ transform: translateX(-50%);
+}
+
+.design-checkbox {
+ position: absolute;
+ top: $gl-padding;
+ left: 30px;
+}
+
+.image-notes {
+ overflow-y: scroll;
+ padding: $gl-padding;
+ padding-top: 50px;
+ background-color: $white;
+ flex-shrink: 0;
+ min-width: 400px;
+ flex-basis: 28%;
+
+ .badge.badge-pill {
+ margin-left: $gl-padding;
+ background-color: $blue-400;
+ color: $white;
+ border: $white 1px solid;
+ min-height: 28px;
+ padding: 7px 10px;
+ border-radius: $gl-padding;
+ }
+
+ .design-discussion {
+ margin: $gl-padding 0;
+
+ &::before {
+ content: '';
+ border-left: 1px solid $gray-200;
+ position: absolute;
+ left: 28px;
+ top: -18px;
+ height: 18px;
+ }
+
+ .design-note {
+ padding: $gl-padding;
+ list-style: none;
+
+ a {
+ color: inherit;
+ }
+
+ .note-text a {
+ color: $blue-600;
+ }
+ }
+
+ .reply-wrapper {
+ padding: $gl-padding;
+ }
+ }
+
+ .reply-wrapper {
+ border-top: 1px solid $border-color;
+ }
+
+ .new-discussion-disclaimer {
+ line-height: 20px;
+ }
+}
+
+@media (max-width: map-get($grid-breakpoints, lg)) {
+ .design-detail {
+ overflow-y: scroll;
+ }
+
+ .image-notes {
+ overflow-y: auto;
+ min-width: 100%;
+ flex-grow: 1;
+ flex-basis: auto;
+ }
+}
+
+.design-dropzone-border {
+ border: 2px dashed $gray-200;
+}
+
+.design-dropzone-card {
+ transition: border $general-hover-transition-duration $general-hover-transition-curve;
+
+ &:focus,
+ &:active {
+ outline: none;
+ border: 2px dashed $purple;
+ color: $gl-text-color;
+ }
+
+ &:hover {
+ border-color: $gray-500;
+ }
+}
+
+.design-dropzone-overlay {
+ border: 2px dashed $purple;
+ top: 0;
+ left: 0;
+ pointer-events: none;
+ opacity: 1;
+}
+
+.design-dropzone-fade-enter-active,
+.design-dropzone-fade-leave-active {
+ transition: opacity $general-hover-transition-duration $general-hover-transition-curve;
+}
+
+.design-dropzone-fade-enter,
+.design-dropzone-fade-leave-to {
+ opacity: 0;
+}
diff --git a/app/assets/stylesheets/components/design_management/design_list_item.scss b/app/assets/stylesheets/components/design_management/design_list_item.scss
new file mode 100644
index 00000000000..aacb1f91e59
--- /dev/null
+++ b/app/assets/stylesheets/components/design_management/design_list_item.scss
@@ -0,0 +1,19 @@
+.design-list-item {
+ height: 280px;
+ text-decoration: none;
+
+ .icon-version-status {
+ position: absolute;
+ right: 10px;
+ top: 10px;
+ }
+
+ .design-event {
+ top: $gl-padding;
+ right: $gl-padding;
+ }
+
+ .card-body {
+ height: 230px;
+ }
+}
diff --git a/app/assets/stylesheets/components/design_management/design_version_dropdown.scss b/app/assets/stylesheets/components/design_management/design_version_dropdown.scss
new file mode 100644
index 00000000000..f79d672e238
--- /dev/null
+++ b/app/assets/stylesheets/components/design_management/design_version_dropdown.scss
@@ -0,0 +1,3 @@
+.design-version-dropdown > button {
+ background: inherit;
+}
diff --git a/app/assets/stylesheets/components/milestone_combobox.scss b/app/assets/stylesheets/components/milestone_combobox.scss
new file mode 100644
index 00000000000..e0637088bbb
--- /dev/null
+++ b/app/assets/stylesheets/components/milestone_combobox.scss
@@ -0,0 +1,13 @@
+.selected-item::before {
+ content: '\f00c';
+ color: $green-500;
+ position: absolute;
+ left: 16px;
+ top: 16px;
+ transform: translateY(-50%);
+ font: 14px FontAwesome;
+}
+
+.dropdown-item-space {
+ padding: 8px 12px;
+}
diff --git a/app/assets/stylesheets/components/related_items_list.scss b/app/assets/stylesheets/components/related_items_list.scss
index ce1039832d3..61f971a3185 100644
--- a/app/assets/stylesheets/components/related_items_list.scss
+++ b/app/assets/stylesheets/components/related_items_list.scss
@@ -1,9 +1,11 @@
$item-path-max-width: 160px;
$item-milestone-max-width: 120px;
$item-weight-max-width: 48px;
+$item-remove-button-space: 42px;
.related-items-list {
padding: $gl-padding-4;
+ padding-right: $gl-padding-6;
&,
.list-item:last-child {
@@ -11,16 +13,16 @@ $item-weight-max-width: 48px;
}
}
-.sortable-link {
- max-width: 85%;
-}
-
.related-items-tree {
.card-header {
.gl-label {
line-height: $gl-line-height;
}
}
+
+ .sortable-link {
+ white-space: normal;
+ }
}
.item-body {
@@ -48,17 +50,12 @@ $item-weight-max-width: 48px;
cursor: help;
}
- .issue-token-state-icon-open,
- .issue-token-state-icon-closed {
- margin-right: $gl-padding-4;
- }
-
.confidential-icon {
color: $orange-600;
}
.item-title-wrapper {
- max-width: 100%;
+ max-width: calc(100% - #{$item-remove-button-space});
}
.item-title {
@@ -69,11 +66,6 @@ $item-weight-max-width: 48px;
font-weight: $gl-font-weight-bold;
}
- .issue-token-state-icon-open,
- .issue-token-state-icon-closed {
- display: none;
- }
-
.sortable-link {
color: $gray-900;
font-weight: normal;
@@ -90,17 +82,14 @@ $item-weight-max-width: 48px;
white-space: nowrap;
}
- @include media-breakpoint-down(lg) {
- .issue-count-badge {
- padding-left: 0;
- }
+ .health-label-short {
+ display: none;
}
}
.item-body,
.card-header {
.health-label-short {
- display: initial;
max-width: 0;
}
@@ -135,6 +124,12 @@ $item-weight-max-width: 48px;
}
}
+.card-header {
+ .health-label-short {
+ display: initial;
+ }
+}
+
.item-meta {
flex-basis: 100%;
font-size: $gl-font-size;
@@ -227,25 +222,28 @@ $item-weight-max-width: 48px;
font-weight: $gl-font-weight-bold;
max-width: $item-path-max-width;
}
-
- .issue-token-state-icon-open,
- .issue-token-state-icon-closed {
- display: block;
- }
}
.btn-item-remove {
position: absolute;
- right: 0;
top: $gl-padding-4 / 2;
+ right: 0;
padding: $gl-padding-4;
margin-right: $gl-padding-4 / 2;
line-height: 0;
border-color: transparent;
color: $gl-text-color-secondary;
+ .related-items-tree & {
+ position: relative;
+ top: initial;
+ padding: $btn-sm-side-margin;
+ margin-right: initial;
+ }
+
&:hover {
color: $gl-text-color;
+ border-color: $border-color;
}
}
@@ -269,7 +267,6 @@ $item-weight-max-width: 48px;
max-width: 90%;
}
- .item-body,
.card-header {
.health-label-short {
max-width: 30px;
@@ -279,6 +276,15 @@ $item-weight-max-width: 48px;
/* Small devices (landscape phones, 768px and up) */
@include media-breakpoint-up(md) {
+ .item-body .item-contents {
+ max-width: 95%;
+ }
+
+ .related-items-tree .item-contents,
+ .item-body .item-title {
+ max-width: 100%;
+ }
+
.sortable-link {
text-overflow: ellipsis;
overflow: hidden;
@@ -290,27 +296,8 @@ $item-weight-max-width: 48px;
.item-contents {
min-width: 0;
}
-
- .item-title {
- flex-basis: unset;
- // 95% because we compensate
- // for remove button which is
- // positioned absolutely
- width: 95%;
- }
-
- .btn-item-remove {
- order: 1;
- }
- }
-
- .item-meta {
- .item-meta-child {
- flex-basis: unset;
- }
}
- .item-body,
.card-header {
.health-label-short {
max-width: 60px;
@@ -330,7 +317,6 @@ $item-weight-max-width: 48px;
}
}
- .item-body,
.card-header {
.health-label-short {
max-width: 100px;
@@ -346,32 +332,13 @@ $item-weight-max-width: 48px;
@include media-breakpoint-up(xl) {
.item-body {
.item-title {
- min-width: 0;
width: auto;
flex-basis: auto;
flex-shrink: 1;
font-weight: $gl-font-weight-normal;
-
- .issue-token-state-icon-open,
- .issue-token-state-icon-closed {
- display: block;
- margin-right: $gl-padding-8;
- }
- }
-
- .item-title-wrapper {
- max-width: calc(100% - 500px);
- }
-
- .item-info-area {
- flex-basis: auto;
}
}
- .health-label-short {
- display: initial;
- }
-
.health-label-long {
display: none;
}
@@ -380,16 +347,7 @@ $item-weight-max-width: 48px;
overflow: hidden;
}
- .item-meta {
- flex: 1;
- }
-
.item-assignees {
- .avatar {
- height: $gl-padding-24;
- width: $gl-padding-24;
- }
-
.avatar-counter {
height: $gl-padding-24;
min-width: $gl-padding-24;
@@ -401,12 +359,8 @@ $item-weight-max-width: 48px;
.btn-item-remove {
position: relative;
top: initial;
- right: 0;
padding: $btn-sm-side-margin;
-
- &:hover {
- border-color: $border-color;
- }
+ margin-right: $gl-padding-4 / 2;
}
.sortable-link {
@@ -415,8 +369,7 @@ $item-weight-max-width: 48px;
}
@media only screen and (min-width: 1500px) {
- .card-header,
- .item-body {
+ .card-header {
.health-label-short {
display: none;
}
@@ -425,10 +378,4 @@ $item-weight-max-width: 48px;
display: initial;
}
}
-
- .item-body {
- .item-title-wrapper {
- max-width: calc(100% - 640px);
- }
- }
}
diff --git a/app/assets/stylesheets/components/rich_content_editor.scss b/app/assets/stylesheets/components/rich_content_editor.scss
new file mode 100644
index 00000000000..eca0f1114af
--- /dev/null
+++ b/app/assets/stylesheets/components/rich_content_editor.scss
@@ -0,0 +1,11 @@
+// Overrides styles from ToastUI editor
+.tui-editor-defaultUI-toolbar .toolbar-button {
+ color: $gl-gray-600;
+ border: 0;
+
+ &:hover,
+ &:active {
+ color: $blue-500;
+ border: 0;
+ }
+}
diff --git a/app/assets/stylesheets/framework/animations.scss b/app/assets/stylesheets/framework/animations.scss
index d222fc4aefe..13174687e5d 100644
--- a/app/assets/stylesheets/framework/animations.scss
+++ b/app/assets/stylesheets/framework/animations.scss
@@ -193,7 +193,7 @@ a {
background-size: cover;
background-image: linear-gradient(to right,
$gray-100 0%,
- $gray-50 20%,
+ $gray-10 20%,
$gray-100 40%,
$gray-100 100%);
height: 10px;
diff --git a/app/assets/stylesheets/framework/buttons.scss b/app/assets/stylesheets/framework/buttons.scss
index ecf2097dc87..f47d0cab31f 100644
--- a/app/assets/stylesheets/framework/buttons.scss
+++ b/app/assets/stylesheets/framework/buttons.scss
@@ -507,6 +507,10 @@
opacity: 1 !important;
cursor: default !important;
+ &.cursor-not-allowed {
+ cursor: not-allowed !important;
+ }
+
i {
color: $gl-text-color-disabled !important;
}
diff --git a/app/assets/stylesheets/framework/common.scss b/app/assets/stylesheets/framework/common.scss
index 320bd4adaaa..93361c21642 100644
--- a/app/assets/stylesheets/framework/common.scss
+++ b/app/assets/stylesheets/framework/common.scss
@@ -392,6 +392,10 @@ img.emoji {
}
/** COMMON CLASSES **/
+/**
+ 🚨 Do not use these classes — they are deprecated and being removed. 🚨
+ See https://gitlab.com/gitlab-org/gitlab/-/issues/217418 for more details.
+**/
.prepend-top-0 { margin-top: 0; }
.prepend-top-2 { margin-top: 2px; }
.prepend-top-4 { margin-top: $gl-padding-4; }
@@ -434,6 +438,7 @@ img.emoji {
.append-bottom-20 { margin-bottom: 20px; }
.append-bottom-default { margin-bottom: $gl-padding; }
.prepend-bottom-32 { margin-bottom: 32px; }
+.ml-10 { margin-left: 4.5rem; }
.inline { display: inline-block; }
.center { text-align: center; }
.block { display: block; }
@@ -490,7 +495,8 @@ img.emoji {
🚨 Do not use these classes — they are deprecated and being removed. 🚨
See https://gitlab.com/gitlab-org/gitlab/issues/36857 for more details.
- Instead, if you need a spacing class, add it below using the following values.
+ Instead, if you need a spacing class, please use one from Gitlab UI —
+ https://unpkg.com/browse/@gitlab/ui/src/scss/utilities.scss — which uses the following scale.
$gl-spacing-scale-0: 0;
$gl-spacing-scale-1: 2px;
$gl-spacing-scale-2: 4px;
@@ -505,21 +511,38 @@ img.emoji {
$gl-spacing-scale-11: 64px;
$gl-spacing-scale-12: 80px;
$gl-spacing-scale-13: 96px;
-
- E.g., a padding top of 96px can be added using:
- .gl-shim-pt-13 {
- padding-top: 96px;
- }
-
- Please use -shim- so it can be differentiated from the old scale classes.
- These will be replaced when the Gitlab UI utilities are included.
**/
@each $index, $padding in $spacing-scale {
- #{'.gl-p-#{$index}'} { padding: $padding; }
- #{'.gl-pl-#{$index}'} { padding-left: $padding; }
- #{'.gl-pr-#{$index}'} { padding-right: $padding; }
- #{'.gl-pt-#{$index}'} { padding-top: $padding; }
- #{'.gl-pb-#{$index}'} { padding-bottom: $padding; }
+ #{'.gl-p-#{$index}-deprecated-no-really-do-not-use-me'} { padding: $padding; }
+ #{'.gl-pl-#{$index}-deprecated-no-really-do-not-use-me'} { padding-left: $padding; }
+ #{'.gl-pr-#{$index}-deprecated-no-really-do-not-use-me'} { padding-right: $padding; }
+ #{'.gl-pt-#{$index}-deprecated-no-really-do-not-use-me'} { padding-top: $padding; }
+ #{'.gl-pb-#{$index}-deprecated-no-really-do-not-use-me'} { padding-bottom: $padding; }
+}
+
+/**
+ The zero-indexed classes will not change and do not need to be updated.
+ These can be removed when the Gitlab UI class include is merged.
+**/
+
+.gl-p-0 {
+ padding: 0;
+}
+
+.gl-pl-0 {
+ padding-left: 0;
+}
+
+.gl-pr-0 {
+ padding-right: 0;
+}
+
+.gl-pt-0 {
+ padding-top: 0;
+}
+
+.gl-pb-0 {
+ padding-bottom: 0;
}
/**
@@ -610,15 +633,13 @@ img.emoji {
}
}
-.gl-font-size-small { font-size: $gl-font-size-small; }
-.gl-font-size-large { font-size: $gl-font-size-large; }
+.gl-font-sm { font-size: $gl-font-size-small; }
+.gl-font-lg { font-size: $gl-font-size-large; }
+.gl-font-base { font-size: $gl-font-size-14; }
.gl-line-height-24 { line-height: $gl-line-height-24; }
.gl-font-size-0 { font-size: 0; }
-.gl-font-size-12 { font-size: $gl-font-size-12; }
-.gl-font-size-14 { font-size: $gl-font-size-14; }
-.gl-font-size-16 { font-size: $gl-font-size-16; }
.gl-font-size-28 { font-size: $gl-font-size-28; }
.gl-font-size-42 { font-size: $gl-font-size-42; }
diff --git a/app/assets/stylesheets/framework/dropdowns.scss b/app/assets/stylesheets/framework/dropdowns.scss
index f746d7e6f69..1df9818a877 100644
--- a/app/assets/stylesheets/framework/dropdowns.scss
+++ b/app/assets/stylesheets/framework/dropdowns.scss
@@ -317,13 +317,6 @@
}
}
- // Temporary fix to ensure tick is aligned
- // Follow up Issue to remove after the GlNewDropdownItem component is fixed
- // > https://gitlab.com/gitlab-org/gitlab/-/issues/213948
- li:not(.gl-new-dropdown-item) .dropdown-item {
- @include dropdown-link;
- }
-
.divider {
height: 1px;
margin: #{$grid-size / 2} 0;
@@ -384,6 +377,10 @@
}
}
+.dropdown-item {
+ @include dropdown-link;
+}
+
.droplab-dropdown {
.dropdown-toggle > i {
pointer-events: none;
@@ -1032,6 +1029,16 @@ header.header-content .dropdown-menu.frequent-items-dropdown-menu {
}
.labels-select-wrapper {
+ &.is-standalone {
+ .labels-select-dropdown-contents {
+ max-height: 350px;
+
+ .dropdown-content {
+ height: 250px;
+ }
+ }
+ }
+
.labels-select-dropdown-contents {
min-height: $dropdown-min-height;
max-height: 330px;
diff --git a/app/assets/stylesheets/framework/header.scss b/app/assets/stylesheets/framework/header.scss
index a0a020ec548..2c7e9428ef1 100644
--- a/app/assets/stylesheets/framework/header.scss
+++ b/app/assets/stylesheets/framework/header.scss
@@ -553,6 +553,7 @@
vertical-align: text-top;
}
+ a.upgrade-plan-link gl-emoji,
a.ci-minutes-emoji gl-emoji,
a.trial-link gl-emoji {
font-size: $gl-font-size;
diff --git a/app/assets/stylesheets/framework/secondary_navigation_elements.scss b/app/assets/stylesheets/framework/secondary_navigation_elements.scss
index 79f203091f2..bd262b65dc3 100644
--- a/app/assets/stylesheets/framework/secondary_navigation_elements.scss
+++ b/app/assets/stylesheets/framework/secondary_navigation_elements.scss
@@ -146,11 +146,13 @@
display: inline-block;
position: relative;
- /* Medium devices (desktops, 992px and up) */
- @include media-breakpoint-up(md) { width: 200px; }
+ &:not[type='checkbox'] {
+ /* Medium devices (desktops, 992px and up) */
+ @include media-breakpoint-up(md) { width: 200px; }
- /* Large devices (large desktops, 1200px and up) */
- @include media-breakpoint-up(lg) { width: 250px; }
+ /* Large devices (large desktops, 1200px and up) */
+ @include media-breakpoint-up(lg) { width: 250px; }
+ }
}
@include media-breakpoint-down(sm) {
diff --git a/app/assets/stylesheets/framework/tables.scss b/app/assets/stylesheets/framework/tables.scss
index 514bd090e28..5739f048e86 100644
--- a/app/assets/stylesheets/framework/tables.scss
+++ b/app/assets/stylesheets/framework/tables.scss
@@ -4,6 +4,21 @@
}
table {
+ /*
+ * TODO
+ * This is a temporary workaround until we fix the neutral
+ * color palette in https://gitlab.com/gitlab-org/gitlab/-/issues/213570
+ *
+ * The overwrites here affected the security dashboard tables, when removing
+ * this code, table-th-transparent and original-text-color classes should
+ * be removed there.
+ *
+ * Remove this code as soon as this happens
+ */
+ &.gl-table {
+ @include gl-text-gray-700;
+ }
+
&.table {
margin-bottom: $gl-padding;
@@ -32,8 +47,7 @@ table {
}
th {
- background-color: $gray-light;
- font-weight: $gl-font-weight-normal;
+ @include gl-bg-gray-100;
border-bottom: 0;
&.wide {
@@ -44,6 +58,11 @@ table {
background: none;
color: $gl-text-color-secondary;
}
+
+ &.original-gl-th {
+ @include gl-text-gray-700;
+ border-bottom: 1px solid $cycle-analytics-light-gray;
+ }
}
td {
diff --git a/app/assets/stylesheets/framework/typography.scss b/app/assets/stylesheets/framework/typography.scss
index 816dbc6931c..1afcbc6d514 100644
--- a/app/assets/stylesheets/framework/typography.scss
+++ b/app/assets/stylesheets/framework/typography.scss
@@ -86,7 +86,7 @@
line-height: 10px;
color: $gl-gray-700;
vertical-align: middle;
- background-color: $gray-50;
+ background-color: $gray-10;
border-width: 1px;
border-style: solid;
border-color: $gray-200 $gray-200 $gray-400;
@@ -533,6 +533,17 @@
margin: 0;
font-size: $gl-font-size-small;
}
+
+ ul.dropdown-menu {
+ margin-top: 4px;
+ margin-bottom: 24px;
+ padding: 8px 0;
+
+ li {
+ margin: 0;
+ padding: 0 1px;
+ }
+ }
}
}
diff --git a/app/assets/stylesheets/framework/variables.scss b/app/assets/stylesheets/framework/variables.scss
index c23623005b0..ac4d431ea57 100644
--- a/app/assets/stylesheets/framework/variables.scss
+++ b/app/assets/stylesheets/framework/variables.scss
@@ -163,7 +163,8 @@ $red-800: #8b2615;
$red-900: #711e11;
$red-950: #4b140b;
-$gray-50: #fafafa;
+$gray-10: #fafafa;
+$gray-50: #f0f0f0;
$gray-100: #f2f2f2;
$gray-200: #dfdfdf;
$gray-300: #ccc;
@@ -232,6 +233,7 @@ $reds: (
);
$grays: (
+ '10': $gray-10,
'50': $gray-50,
'100': $gray-100,
'200': $gray-200,
@@ -398,6 +400,7 @@ $tooltip-font-size: 12px;
* Padding
*/
$gl-padding-4: 4px;
+$gl-padding-6: 6px;
$gl-padding-8: 8px;
$gl-padding-12: 12px;
$gl-padding: 16px;
@@ -447,6 +450,7 @@ $breadcrumb-min-height: 48px;
$home-panel-title-row-height: 64px;
$home-panel-avatar-mobile-size: 24px;
$gl-line-height: 16px;
+$gl-line-height-18: 18px;
$gl-line-height-20: 20px;
$gl-line-height-24: 24px;
$gl-line-height-14: 14px;
@@ -697,7 +701,7 @@ $logs-p-color: #333;
*/
$input-height: 34px;
$input-danger-bg: #f2dede;
-$input-group-addon-bg: $gray-50;
+$input-group-addon-bg: $gray-10;
$gl-field-focus-shadow: rgba(0, 0, 0, 0.075);
$gl-field-focus-shadow-error: rgba($red-500, 0.6);
$input-short-width: 200px;
diff --git a/app/assets/stylesheets/page_bundles/_ide_mixins.scss b/app/assets/stylesheets/page_bundles/_ide_mixins.scss
index 9465dd5bed6..48b8a7230b1 100644
--- a/app/assets/stylesheets/page_bundles/_ide_mixins.scss
+++ b/app/assets/stylesheets/page_bundles/_ide_mixins.scss
@@ -9,7 +9,6 @@
top: 0;
font-size: 12px;
border-top-right-radius: $border-radius-default;
- margin-left: -$gl-padding;
.controllers {
@include build-controllers(15px, center, false, 0, inline, 0);
diff --git a/app/assets/stylesheets/page_bundles/_ide_monaco_overrides.scss b/app/assets/stylesheets/page_bundles/_ide_monaco_overrides.scss
index 1aa112e0957..5675835a622 100644
--- a/app/assets/stylesheets/page_bundles/_ide_monaco_overrides.scss
+++ b/app/assets/stylesheets/page_bundles/_ide_monaco_overrides.scss
@@ -69,8 +69,17 @@
display: none !important;
}
}
+}
+
+.multi-file-editor-holder {
+ height: 100%;
+ min-height: 0; // firefox fix
+}
- .monaco-diff-editor.vs {
+// Apply theme related overrides only to the white theme and none theme
+.theme-white .blob-editor-container,
+.theme-none .blob-editor-container {
+ .monaco-diff-editor {
.editor.modified {
box-shadow: none;
}
@@ -131,16 +140,14 @@
}
}
-.multi-file-editor-holder {
- height: 100%;
- min-height: 0; // firefox fix
-
- &.is-readonly .vs,
- .vs .editor.original {
+.theme-white .multi-file-editor-holder,
+.theme-none .multi-file-editor-holder {
+ &.is-readonly,
+ .editor.original {
.monaco-editor,
.monaco-editor-background,
.monaco-editor .inputarea.ime-input {
- background-color: $gray-50;
+ background-color: $gray-10;
}
}
}
diff --git a/app/assets/stylesheets/page_bundles/_ide_theme_overrides.scss b/app/assets/stylesheets/page_bundles/_ide_theme_overrides.scss
new file mode 100644
index 00000000000..e4c01c2bd6c
--- /dev/null
+++ b/app/assets/stylesheets/page_bundles/_ide_theme_overrides.scss
@@ -0,0 +1,308 @@
+// -------
+// Please see `app/assets/stylesheets/page_bundles/ide_themes/README.md` for a guide on contributing new themes
+// -------
+.ide.theme-dark {
+ a:not(.btn) {
+ color: var(--ide-link-color);
+ }
+
+ h1,
+ h2,
+ h3,
+ h4,
+ h5,
+ h6,
+ code,
+ .md table:not(.code),
+ .md,
+ .md p,
+ .context-header > a,
+ input,
+ textarea,
+ .md-area.is-focused,
+ .dropdown-menu li button,
+ .dropdown-menu-selectable li a.is-active,
+ .dropdown-menu-inner-title,
+ .dropdown-menu-inner-content,
+ .nav-links:not(.quick-links) li:not(.md-header-toolbar) a,
+ .nav-links:not(.quick-links) li:not(.md-header-toolbar) a:hover,
+ .nav-links:not(.quick-links) li:not(.md-header-toolbar) a.active .badge.badge-pill,
+ .nav-links:not(.quick-links) li:not(.md-header-toolbar) a:hover .badge.badge-pill,
+ .badge.badge-pill,
+ .bs-callout,
+ .ide-pipeline .top-bar,
+ .ide-pipeline .top-bar .controllers .controllers-buttons {
+ color: var(--ide-text-color);
+ }
+
+ .drag-handle:hover,
+ .card-header .badge.badge-pill {
+ background-color: var(--ide-dropdown-hover-background);
+ }
+
+ .file-row .file-row-icon svg,
+ .file-row:hover .file-row-icon svg,
+ .controllers-buttons svg {
+ color: var(--ide-text-color-secondary);
+ }
+
+ .text-secondary {
+ color: var(--ide-text-color-secondary) !important;
+ }
+
+ input[type='search']::placeholder,
+ input[type='text']::placeholder,
+ textarea::placeholder,
+ .dropdown-input .fa {
+ color: var(--ide-input-border);
+ }
+
+ .ide-nav-form .input-icon {
+ color: var(--ide-input-border);
+ }
+
+ code,
+ .badge.badge-pill,
+ .card-header,
+ .bs-callout,
+ .ide-pipeline .top-bar,
+ .ide-terminal .top-bar {
+ background-color: var(--ide-background);
+ }
+
+ .bs-callout {
+ border-color: var(--ide-dropdown-background);
+
+ code {
+ background-color: var(--ide-dropdown-background);
+ }
+ }
+
+ .nav-links:not(.quick-links) li:not(.md-header-toolbar) a:hover {
+ border-color: var(--ide-dropdown-hover-background);
+ }
+
+ .common-note-form .md-area {
+ border-color: var(--ide-input-border);
+ }
+
+ &,
+ .md table:not(.code) tr th,
+ .common-note-form .md-area,
+ .card {
+ background-color: var(--ide-highlight-background);
+ }
+
+ .card,
+ .card-header,
+ .ide-terminal .top-bar,
+ .ide-pipeline .top-bar {
+ border-color: var(--ide-border-color);
+ }
+
+ hr,
+ .md h1,
+ .md h2,
+ .md blockquote,
+ pre,
+ .md table:not(.code) tbody td,
+ .md table:not(.code) tr th,
+ .nav-links:not(.quick-links) {
+ border-color: var(--ide-border-color-alt);
+ }
+
+ .ide-sidebar-link.active {
+ color: var(--ide-highlight-accent);
+ box-shadow: inset 3px 0 var(--ide-highlight-accent);
+
+ &.is-right {
+ box-shadow: inset -3px 0 var(--ide-highlight-accent);
+ }
+ }
+
+ .nav-links li.active a,
+ .nav-links li a.active {
+ border-color: var(--ide-highlight-accent);
+ color: var(--ide-text-color);
+ }
+
+ .avatar-container {
+ &,
+ .avatar {
+ color: var(--ide-text-color);
+ background-color: var(--ide-highlight-background);
+ border-color: var(--ide-highlight-background);
+ }
+ }
+
+ input[type='text'],
+ input[type='search'],
+ .filtered-search-box {
+ border-color: var(--ide-input-border);
+ background: var(--ide-input-background) !important;
+ }
+
+ input[type='text'],
+ input[type='search'],
+ .filtered-search-box,
+ textarea {
+ color: var(--ide-input-color) !important;
+ }
+
+ .filtered-search-box input[type='search'] {
+ border-color: transparent;
+ }
+
+ .filtered-search-token .value-container,
+ .filtered-search-term .value-container {
+ background-color: var(--ide-dropdown-hover-background);
+ color: var(--ide-text-color);
+
+ &:hover {
+ background-color: var(--ide-input-border);
+ }
+ }
+
+ @function calc-btn-hover-padding($original-padding, $original-border: 1px) {
+ @return calc(#{$original-padding + $original-border} - var(--ide-btn-hover-border-width));
+ }
+
+ .btn:not(.btn-link):not([disabled]):hover {
+ border-width: var(--ide-btn-hover-border-width);
+ padding: calc-btn-hover-padding(6px) calc-btn-hover-padding(10px);
+ }
+
+ .btn:not([disabled]).btn-sm:hover {
+ padding: calc-btn-hover-padding(4px) calc-btn-hover-padding(10px);
+ }
+
+ .btn:not([disabled]).btn-block:hover {
+ padding: calc-btn-hover-padding(6px) 0;
+ }
+
+ .btn-inverted,
+ .btn-default,
+ .dropdown,
+ .dropdown-menu-toggle {
+ background-color: var(--ide-input-background) !important;
+ color: var(--ide-input-color) !important;
+ border-color: var(--ide-btn-default-border);
+ }
+
+ .btn-inverted,
+ .btn-default {
+ &:hover,
+ &:focus {
+ border-color: var(--ide-btn-default-hover-border) !important;
+ }
+ }
+
+ .dropdown,
+ .dropdown-menu-toggle {
+ &:hover,
+ &:focus {
+ background-color: var(--ide-dropdown-btn-hover-background) !important;
+ border-color: var(--ide-dropdown-btn-hover-border) !important;
+ }
+ }
+
+ .dropdown-menu {
+ color: var(--ide-text-color);
+ border-color: var(--ide-background);
+ background-color: var(--ide-dropdown-background);
+
+ .divider,
+ .nav-links:not(.quick-links) {
+ background-color: var(--ide-dropdown-hover-background);
+ border-color: var(--ide-dropdown-hover-background);
+ }
+
+ .nav-links li a.active {
+ border-color: var(--ide-highlight-accent);
+ }
+
+ .nav-links:not(.quick-links) li:not(.md-header-toolbar) a {
+ color: var(--ide-text-color);
+
+ &.active {
+ color: var(--ide-text-color);
+ }
+ }
+
+ li > a:not(.disable-hover):hover,
+ li > a:not(.disable-hover):focus,
+ li button:not(.disable-hover):hover,
+ li button:not(.disable-hover):focus,
+ li button.is-focused {
+ background-color: var(--ide-dropdown-hover-background);
+ color: var(--ide-text-color);
+ }
+ }
+
+ .dropdown-title,
+ .dropdown-input {
+ border-color: var(--ide-dropdown-hover-background) !important;
+ }
+
+ .btn-primary,
+ .btn-info {
+ background-color: var(--ide-btn-primary-background);
+ border-color: var(--ide-btn-primary-border) !important;
+
+ &:hover,
+ &:focus {
+ border-color: var(--ide-btn-primary-hover-border) !important;
+ }
+ }
+
+ .btn-success {
+ background-color: var(--ide-btn-success-background);
+ border-color: var(--ide-btn-success-border) !important;
+
+ &:hover,
+ &:focus {
+ border-color: var(--ide-btn-success-hover-border) !important;
+ }
+ }
+
+ .btn[disabled] {
+ background: var(--ide-btn-default-background) !important;
+ border: 1px solid var(--ide-btn-disabled-border) !important;
+ color: var(--ide-btn-disabled-color) !important;
+ }
+
+ pre code,
+ .md table:not(.code) tbody {
+ background-color: var(--ide-border-color);
+ }
+
+ .animation-container {
+ [class^='skeleton-line-'] {
+ background-color: var(--ide-animation-gradient-1);
+
+ &::after {
+ background-image: linear-gradient(to right,
+ var(--ide-animation-gradient-1) 0%,
+ var(--ide-animation-gradient-2) 20%,
+ var(--ide-animation-gradient-1) 40%,
+ var(--ide-animation-gradient-1) 100%);
+ }
+ }
+ }
+
+ .idiff.addition {
+ background-color: var(--ide-diff-insert);
+ }
+
+ .idiff.deletion {
+ background-color: var(--ide-diff-remove);
+ }
+}
+
+.navbar.theme-dark {
+ border-bottom-color: transparent;
+}
+
+.theme-dark ~ .popover {
+ box-shadow: none;
+}
diff --git a/app/assets/stylesheets/page_bundles/ide.scss b/app/assets/stylesheets/page_bundles/ide.scss
index 024c1781bf8..61914740ac0 100644
--- a/app/assets/stylesheets/page_bundles/ide.scss
+++ b/app/assets/stylesheets/page_bundles/ide.scss
@@ -2,6 +2,9 @@
@import 'framework/mixins';
@import './ide_mixins';
@import './ide_monaco_overrides';
+@import './ide_theme_overrides';
+
+@import './ide_themes/dark';
$search-list-icon-width: 18px;
$ide-activity-bar-width: 60px;
@@ -25,7 +28,7 @@ $ide-commit-header-height: 48px;
position: relative;
margin-top: 0;
padding-bottom: $ide-statusbar-height;
- color: $gl-text-color;
+ color: var(--ide-text-color, $gl-text-color);
min-height: 0; // firefox fix
&.is-collapsed {
@@ -61,14 +64,14 @@ $ide-commit-header-height: 48px;
display: flex;
flex-direction: column;
flex: 1;
- border-left: 1px solid $white-dark;
+ border-left: 1px solid var(--ide-border-color, $white-dark);
overflow: hidden;
}
.multi-file-tabs {
display: flex;
- background-color: $gray-light;
- box-shadow: inset 0 -1px $white-dark;
+ background-color: var(--ide-background, $gray-light);
+ box-shadow: inset 0 -1px var(--ide-border-color, $white-dark);
> ul {
display: flex;
@@ -79,13 +82,13 @@ $ide-commit-header-height: 48px;
display: flex;
align-items: center;
padding: $grid-size $gl-padding;
- background-color: $gray-normal;
- border-right: 1px solid $white-dark;
- border-bottom: 1px solid $white-dark;
+ background-color: var(--ide-background-hover, $gray-normal);
+ border-right: 1px solid var(--ide-border-color, $white-dark);
+ border-bottom: 1px solid var(--ide-border-color, $white-dark);
&.active {
- background-color: $white;
- border-bottom-color: $white;
+ background-color: var(--ide-highlight-background, $white);
+ border-bottom-color: var(--ide-border-color, $white);
}
&:not(.disabled) {
@@ -118,7 +121,7 @@ $ide-commit-header-height: 48px;
background: none;
border: 0;
border-radius: $border-radius-default;
- color: $gray-900;
+ color: var(--ide-text-color, $gray-900);
svg {
position: relative;
@@ -133,11 +136,11 @@ $ide-commit-header-height: 48px;
}
&:not([disabled]):hover {
- background-color: $gray-200;
+ background-color: var(--ide-input-border, $gray-200);
}
&:not([disabled]):focus {
- background-color: $blue-500;
+ background-color: var(--ide-link-color, $blue-500);
color: $white;
outline: 0;
@@ -164,10 +167,11 @@ $ide-commit-header-height: 48px;
height: 100%;
overflow: auto;
padding: $gl-padding;
+ background-color: var(--ide-border-color, transparent);
}
.file-container {
- background-color: $gray-darker;
+ background-color: var(--ide-border-color, $gray-darker);
display: flex;
height: 100%;
align-items: center;
@@ -183,13 +187,13 @@ $ide-commit-header-height: 48px;
.file-info {
font-size: $label-font-size;
- color: $diff-image-info-color;
+ color: var(--ide-text-color, $diff-image-info-color);
}
}
}
.ide-mode-tabs {
- border-bottom: 1px solid $white-dark;
+ border-bottom: 1px solid var(--ide-border-color, $white-dark);
li a {
padding: $gl-padding-8 $gl-padding;
@@ -203,9 +207,10 @@ $ide-commit-header-height: 48px;
}
.ide-status-bar {
- border-top: 1px solid $white-dark;
+ color: var(--ide-text-color, $gl-text-color);
+ border-top: 1px solid var(--ide-border-color, $white-dark);
padding: 2px $gl-padding-8 0;
- background: $white;
+ background-color: var(--ide-footer-background, $white);
display: flex;
justify-content: space-between;
height: $ide-statusbar-height;
@@ -278,8 +283,7 @@ $ide-commit-header-height: 48px;
position: relative;
width: 340px;
padding: 0;
- background-color: $gray-light;
- padding-right: 1px;
+ background-color: var(--ide-background, $gray-light);
.context-header {
width: auto;
@@ -306,9 +310,9 @@ $ide-commit-header-height: 48px;
display: flex;
flex: 1;
flex-direction: column;
- background-color: $white;
- border-left: 1px solid $white-dark;
- border-top: 1px solid $white-dark;
+ background-color: var(--ide-highlight-background, $white);
+ border-left: 1px solid var(--ide-border-color, $white-dark);
+ border-top: 1px solid var(--ide-border-color, $white-dark);
border-top-left-radius: $border-radius-small;
min-height: 0; // firefox fix
}
@@ -333,15 +337,10 @@ $ide-commit-header-height: 48px;
.multi-file-commit-panel-header {
height: $ide-commit-header-height;
- border-bottom: 1px solid $white-dark;
+ border-bottom: 1px solid var(--ide-border-color-alt, $white-dark);
padding: 12px 0;
}
-.multi-file-commit-panel-collapse-btn {
- border-left: 1px solid $white-dark;
- margin-left: auto;
-}
-
.multi-file-commit-list {
flex: 1;
overflow: auto;
@@ -363,7 +362,7 @@ $ide-commit-header-height: 48px;
display: block;
margin-left: auto;
margin-right: auto;
- color: $gray-700;
+ color: var(--ide-text-color-secondary, $gray-700);
}
.file-status-icon {
@@ -387,17 +386,17 @@ $ide-commit-header-height: 48px;
&:hover,
&:focus {
- background: $gray-100;
+ background: var(--ide-background, $gray-100);
outline: 0;
}
&:active {
- background: $gray-200;
+ background: var(--ide-background, $gray-200);
}
&.is-active {
- background-color: $white-normal;
+ background-color: var(--ide-background, $white-normal);
}
svg {
@@ -418,8 +417,8 @@ $ide-commit-header-height: 48px;
.multi-file-commit-form {
position: relative;
- background-color: $white;
- border-left: 1px solid $white-dark;
+ background-color: var(--ide-highlight-background, $white);
+ border-left: 1px solid var(--ide-border-color, $white-dark);
transition: all 0.3s ease;
> form,
@@ -427,7 +426,7 @@ $ide-commit-header-height: 48px;
padding: $gl-padding 0;
margin-left: $gl-padding;
margin-right: $gl-padding;
- border-top: 1px solid $white-dark;
+ border-top: 1px solid var(--ide-border-color-alt, $white-dark);
}
.btn {
@@ -488,6 +487,7 @@ $ide-commit-header-height: 48px;
height: 100vh;
align-items: center;
justify-content: center;
+ background-color: var(--ide-border-color, transparent);
}
.ide {
@@ -504,7 +504,7 @@ $ide-commit-header-height: 48px;
margin-right: $gl-padding;
&.is-first {
- border-bottom: 1px solid $white-dark;
+ border-bottom: 1px solid var(--ide-border-color-alt, $white-dark);
}
}
@@ -512,12 +512,7 @@ $ide-commit-header-height: 48px;
width: $ide-commit-row-height;
height: $ide-commit-row-height;
color: inherit;
-}
-
-.ide-commit-file-count {
- min-width: 22px;
- background-color: $gray-light;
- border: 1px solid $white-dark;
+ background-color: var(--ide-background, $white-normal);
}
.ide-commit-options {
@@ -549,7 +544,7 @@ $ide-commit-header-height: 48px;
height: 60px;
width: 100%;
padding: 0 $gl-padding;
- color: $gl-text-color-secondary;
+ color: var(--ide-text-color-secondary, $gl-text-color-secondary);
background-color: transparent;
border: 0;
border-top: 1px solid transparent;
@@ -562,22 +557,22 @@ $ide-commit-header-height: 48px;
}
&:hover {
- color: $gl-text-color;
- background-color: $gray-100;
+ color: var(--ide-text-color, $gl-text-color);
+ background-color: var(--ide-background-hover, $gray-100);
}
&:focus {
- color: $gl-text-color;
- background-color: $gray-200;
+ color: var(--ide-text-color, $gl-text-color);
+ background-color: var(--ide-background-hover, $gray-200);
}
&.active {
// extend width over border of sidebar section
width: calc(100% + 1px);
padding-right: $gl-padding + 1px;
- background-color: $white;
- border-top-color: $white-dark;
- border-bottom-color: $white-dark;
+ background-color: var(--ide-highlight-background, $white);
+ border-top-color: var(--ide-border-color, $white-dark);
+ border-bottom-color: var(--ide-border-color, $white-dark);
&::after {
content: '';
@@ -586,7 +581,7 @@ $ide-commit-header-height: 48px;
top: 0;
bottom: 0;
width: 1px;
- background: $white;
+ background: var(--ide-highlight-background, $white);
}
&.is-right {
@@ -609,7 +604,7 @@ $ide-commit-header-height: 48px;
.ide-commit-message-field {
height: 200px;
- background-color: $white;
+ background-color: var(--ide-highlight-background, $white);
.md-area {
display: flex;
@@ -623,7 +618,7 @@ $ide-commit-header-height: 48px;
.form-text.text-muted {
margin-top: 2px;
- color: $blue-500;
+ color: var(--ide-link-color, $blue-500);
cursor: pointer;
}
}
@@ -686,14 +681,14 @@ $ide-commit-header-height: 48px;
padding: 12px 0;
margin-left: $ide-tree-padding;
margin-right: $ide-tree-padding;
- border-bottom: 1px solid $white-dark;
+ border-bottom: 1px solid var(--ide-border-color-alt, $white-dark);
svg {
- color: $gray-700;
+ color: var(--ide-text-color-secondary, $gray-700);
&:focus,
&:hover {
- color: $blue-600;
+ color: var(--ide-link-color, $blue-600);
}
}
@@ -702,7 +697,7 @@ $ide-commit-header-height: 48px;
}
button {
- color: $gl-text-color;
+ color: var(--ide-text-color, $gl-text-color);
}
}
@@ -718,21 +713,21 @@ $ide-commit-header-height: 48px;
.dropdown-menu-toggle {
svg {
vertical-align: middle;
- color: $gray-700;
+ &,
&:hover {
- color: $gray-700;
+ color: var(--ide-text-color-secondary, $gray-700);
}
}
&:hover {
- background-color: $white-normal;
+ background-color: var(--ide-dropdown-btn-hover-background, $white-normal);
}
}
&.show {
.dropdown-menu-toggle {
- background-color: $white-dark;
+ background-color: var(--ide-input-background, $white-dark);
}
}
}
@@ -798,12 +793,12 @@ $ide-commit-header-height: 48px;
}
a {
- color: $blue-600;
+ color: var(--ide-link-color, $blue-600);
}
}
.ide-review-sub-header {
- color: $gl-text-color-secondary;
+ color: var(--ide-text-color-secondary, $gl-text-color-secondary);
}
.ide-tree-changes {
@@ -819,7 +814,7 @@ $ide-commit-header-height: 48px;
bottom: 0;
right: 0;
z-index: 10;
- background: $white;
+ background-color: var(--ide-highlight-background, $white);
overflow: auto;
display: flex;
flex-direction: column;
@@ -883,14 +878,18 @@ $ide-commit-header-height: 48px;
.ide-right-sidebar {
.ide-activity-bar {
- border-left: 1px solid $white-dark;
+ border-left: 1px solid var(--ide-border-color, $white-dark);
}
.multi-file-commit-panel-inner {
width: 350px;
- padding: $grid-size $gl-padding;
- background-color: $white;
- border-left: 1px solid $white-dark;
+ padding: $grid-size 0;
+ background-color: var(--ide-highlight-background, $white);
+ border-left: 1px solid var(--ide-border-color, $white-dark);
+ }
+
+ .ide-right-sidebar-jobs-detail {
+ padding-bottom: 0;
}
.ide-right-sidebar-clientside {
@@ -901,6 +900,10 @@ $ide-commit-header-height: 48px;
.ide-pipeline {
@include ide-trace-view();
+ svg {
+ --svg-status-bg: var(--ide-background, $white);
+ }
+
.empty-state {
p {
margin: $grid-size 0;
@@ -913,15 +916,12 @@ $ide-commit-header-height: 48px;
margin: 0;
}
}
-
- .build-trace {
- margin-left: -$gl-padding;
- }
}
.ide-pipeline-list {
flex: 1;
overflow: auto;
+ padding: 0 $gl-padding;
}
.ide-pipeline-header {
@@ -935,7 +935,7 @@ $ide-commit-header-height: 48px;
padding: 16px;
&:not(:last-child) {
- border-bottom: 1px solid $border-color;
+ border-bottom: 1px solid var(--ide-border-color, $border-color);
}
.ci-status-icon {
@@ -964,6 +964,7 @@ $ide-commit-header-height: 48px;
.ide-job-header {
min-height: 60px;
+ padding: 0 $gl-padding;
}
.ide-nav-form {
@@ -976,7 +977,7 @@ $ide-commit-header-height: 48px;
text-align: center;
&:not(.active) {
- background-color: $gray-light;
+ background-color: var(--ide-dropdown-background, $gray-light);
}
}
}
@@ -1025,7 +1026,7 @@ $ide-commit-header-height: 48px;
.ide-merge-request-project-path {
font-size: 12px;
line-height: 16px;
- color: $gl-text-color-secondary;
+ color: var(--ide-text-color-secondary, $gl-text-color-secondary);
}
.ide-merge-request-info {
@@ -1041,17 +1042,17 @@ $ide-commit-header-height: 48px;
.ide-entry-dropdown-toggle {
padding: $gl-padding-4;
- color: $gl-text-color;
- background-color: $gray-100;
+ color: var(--ide-text-color, $gl-text-color);
+ background-color: var(--ide-background, $gray-100);
&:hover {
- background-color: $gray-200;
+ background-color: var(--ide-file-row-btn-hover-background, $gray-200);
}
&:active,
&:focus {
color: $white-normal;
- background-color: $blue-500;
+ background-color: var(--ide-link-color, $blue-500);
outline: 0;
}
}
@@ -1065,14 +1066,14 @@ $ide-commit-header-height: 48px;
.dropdown.show .ide-entry-dropdown-toggle {
color: $white-normal;
- background-color: $blue-500;
+ background-color: var(--ide-link-color, $blue-500);
}
}
.ide-preview-header {
padding: 0 $grid-size;
- border-bottom: 1px solid $white-dark;
- background-color: $gray-light;
+ border-bottom: 1px solid var(--ide-border-color-alt, $white-dark);
+ background-color: var(--ide-highlight-background, $gray-light);
min-height: 44px;
}
@@ -1082,7 +1083,7 @@ $ide-commit-header-height: 48px;
max-width: 24px;
padding: 0;
margin: 0 ($grid-size / 2);
- color: $gl-gray-light;
+ color: var(--ide-text-color-secondary, $gl-gray-light);
&:first-child {
margin-left: 0;
@@ -1096,7 +1097,7 @@ $ide-commit-header-height: 48px;
&:focus {
outline: 0;
box-shadow: none;
- border-color: $gray-200;
+ border-color: var(--ide-border-color, $gray-200);
}
}
@@ -1108,8 +1109,8 @@ $ide-commit-header-height: 48px;
.ide-file-templates {
padding: $grid-size $gl-padding;
- background-color: $gray-light;
- border-bottom: 1px solid $white-dark;
+ background-color: var(--ide-background, $gray-light);
+ border-bottom: 1px solid var(--ide-border-color, $white-dark);
.dropdown {
min-width: 180px;
@@ -1123,8 +1124,8 @@ $ide-commit-header-height: 48px;
.ide-commit-editor-header {
height: 65px;
padding: 8px 16px;
- background-color: $gray-50;
- box-shadow: inset 0 -1px $white-dark;
+ background-color: var(--ide-background, $gray-10);
+ box-shadow: inset 0 -1px var(--ide-border-color, $white-dark);
}
.ide-commit-list-changed-icon {
@@ -1135,16 +1136,26 @@ $ide-commit-header-height: 48px;
.ide-file-icon-holder {
display: flex;
align-items: center;
- color: $gray-700;
+ color: var(--ide-text-color-secondary, $gray-700);
+}
+
+.file-row:active {
+ background: var(--ide-background, $gray-200);
+}
+
+.file-row.is-active {
+ background: var(--ide-background, $gray-100);
}
.file-row:hover,
.file-row:focus {
+ background: var(--ide-background, $gray-100);
+
.ide-new-btn {
display: block;
}
.folder-icon {
- fill: $gl-text-color-secondary;
+ fill: var(--ide-text-color-secondary, $gl-text-color-secondary);
}
}
diff --git a/app/assets/stylesheets/page_bundles/ide_themes/README.md b/app/assets/stylesheets/page_bundles/ide_themes/README.md
new file mode 100644
index 00000000000..535179cc4c2
--- /dev/null
+++ b/app/assets/stylesheets/page_bundles/ide_themes/README.md
@@ -0,0 +1,53 @@
+# Web IDE Themes
+
+Web IDE currently supports 5 syntax highlighting themes based on themes from the user's profile preferences:
+
+* White
+* Dark
+* Monokai
+* Solarized Dark
+* Solarized Light
+
+Currently, the Web IDE supports the white theme by default, and the dark theme by the introduction of CSS
+variables.
+
+The Web IDE automatically adds an appropriate theme class to the `ide.vue` component based on the current syntax
+highlighting theme. Below are those theme classes, which come from the `gon.user_color_scheme` global setting:
+
+| # | Color Scheme | `gon.user_color_scheme` | Theme class |
+|---|-----------------|-------------------------|-------------------------|
+| 1 | White | `"white"` | `.theme-white` |
+| 2 | Dark | `"dark"` | `.theme-dark` |
+| 3 | Monokai | `"monokai"` | `.theme-monokai` |
+| 4 | Solarized Dark | `"solarized-dark"` | `.theme-solarized-dark` |
+| 5 | Solarized Light | `"solarized-light"` | `.theme-solarized-light` |
+| 6 | None | `"none"` | `.theme-none` |
+
+## Adding New Themes (SCSS)
+
+To add a new theme, follow the following steps:
+
+1. Pick a theme from the table above, lets say **Solarized Dark**.
+2. Create a new file in this folder called `_solarized_dark.scss`.
+3. Copy over all the CSS variables from `_dark.scss` to `_solarized_dark.scss` and assign them your own values.
+ Put them under the selector `.ide.theme-solarized-dark`.
+4. Import this newly created SCSS file in `ide.scss` file in the parent directory.
+5. To make sure the variables apply to to your theme, add the selector `.ide.theme-solarized-dark` to the top
+ of `_ide_theme_overrides.scss` file. The file should now look like this:
+
+ ```scss
+ .ide.theme-dark,
+ .ide.theme-solarized-dark {
+ /* file contents */
+ }
+ ```
+
+ This step is temporary until all CSS variables in that file have their
+ default values assigned.
+6. That's it! Raise a merge request with your newly added theme.
+
+## Modifying Monaco Themes
+
+Monaco themes are defined in Javascript and are stored in the `app/assets/javascripts/ide/lib/themes/` directory.
+To modify any syntax highlighting colors or to synchronize the theme colors with syntax highlighting colors, you
+can modify the files in that directory directly.
diff --git a/app/assets/stylesheets/page_bundles/ide_themes/_dark.scss b/app/assets/stylesheets/page_bundles/ide_themes/_dark.scss
new file mode 100644
index 00000000000..37e6be9849b
--- /dev/null
+++ b/app/assets/stylesheets/page_bundles/ide_themes/_dark.scss
@@ -0,0 +1,50 @@
+// -------
+// Please see `app/assets/stylesheets/page_bundles/ide_themes/README.md` for a guide on contributing new themes
+// -------
+.ide.theme-dark {
+ --ide-border-color: #1d1f21;
+ --ide-border-color-alt: #333;
+ --ide-highlight-accent: #fff;
+ --ide-text-color: #ccc;
+ --ide-text-color-secondary: #ccc;
+ --ide-background: #333;
+ --ide-background-hover: #2d2d2d;
+ --ide-highlight-background: #252526;
+ --ide-link-color: #428fdc;
+ --ide-footer-background: #060606;
+
+ --ide-input-border: #868686;
+ --ide-input-background: transparent;
+ --ide-input-color: #fff;
+
+ --ide-btn-default-background: transparent;
+ --ide-btn-default-border: #bfbfbf;
+ --ide-btn-default-hover-border: #d8d8d8;
+
+ --ide-btn-primary-background: #1068bf;
+ --ide-btn-primary-border: #428fdc;
+ --ide-btn-primary-hover-border: #63a6e9;
+
+ --ide-btn-success-background: #217645;
+ --ide-btn-success-border: #108548;
+ --ide-btn-success-hover-border: #2da160;
+
+ --ide-btn-disabled-border: rgba(223, 223, 223, 0.24);
+ --ide-btn-disabled-color: rgba(145, 145, 145, 0.48);
+
+ --ide-btn-hover-border-width: 2px;
+
+ --ide-dropdown-background: #404040;
+ --ide-dropdown-hover-background: #525252;
+
+ --ide-dropdown-btn-hover-border: #{$gray-200};
+ --ide-dropdown-btn-hover-background: #{$gray-900};
+
+ --ide-file-row-btn-hover-background: #{$gray-800};
+
+ --ide-diff-insert: rgba(155, 185, 85, 0.2);
+ --ide-diff-remove: rgba(255, 0, 0, 0.2);
+
+ --ide-animation-gradient-1: #{$gray-800};
+ --ide-animation-gradient-2: #{$gray-700};
+}
diff --git a/app/assets/stylesheets/pages/alert_management/details.scss b/app/assets/stylesheets/pages/alert_management/details.scss
new file mode 100644
index 00000000000..89219e41644
--- /dev/null
+++ b/app/assets/stylesheets/pages/alert_management/details.scss
@@ -0,0 +1,42 @@
+.alert-management-details {
+ // these styles need to be deleted once GlTable component looks in GitLab same as in @gitlab/ui
+ table {
+ tr {
+ td {
+ @include gl-border-0;
+ @include gl-p-5;
+ border-color: transparent;
+ border-bottom: 1px solid $table-border-color;
+
+ &:first-child {
+ div {
+ font-weight: bold;
+ }
+ }
+
+ &:not(:first-child) {
+ &::before {
+ color: $gray-700;
+ font-weight: normal !important;
+ }
+
+ div {
+ color: $gray-700;
+ }
+ }
+
+ @include media-breakpoint-up(sm) {
+ div {
+ text-align: left !important;
+ }
+ }
+ }
+ }
+ }
+
+ @include media-breakpoint-down(xs) {
+ .alert-details-create-issue-button {
+ width: 100%;
+ }
+ }
+}
diff --git a/app/assets/stylesheets/pages/alert_management/list.scss b/app/assets/stylesheets/pages/alert_management/list.scss
new file mode 100644
index 00000000000..dc181342def
--- /dev/null
+++ b/app/assets/stylesheets/pages/alert_management/list.scss
@@ -0,0 +1,83 @@
+.alert-management-list {
+ // consider adding these stateful variants to @gitlab-ui
+ // https://gitlab.com/gitlab-org/gitlab-ui/-/merge_requests/1178
+ .hover-bg-blue-50:hover {
+ background-color: $blue-50;
+ }
+
+ .hover-gl-cursor-pointer:hover {
+ cursor: pointer;
+ }
+
+ .hover-gl-border-b-solid:hover {
+ @include gl-border-b-solid;
+ }
+
+ .hover-gl-border-blue-200:hover {
+ border-color: $blue-200;
+ }
+
+ // these styles need to be deleted once GlTable component looks in GitLab same as in @gitlab/ui
+ table {
+ color: $gray-700;
+
+ tr {
+ &:focus {
+ outline: none;
+ }
+
+ td,
+ th {
+ @include gl-p-5;
+ border: 0; // Remove cell border styling so that we can set border styling per row
+
+ &.event-count {
+ @include gl-pr-9;
+ }
+ }
+
+ th {
+ background-color: transparent;
+ font-weight: $gl-font-weight-bold;
+ color: $gl-gray-600;
+ }
+
+ &:last-child {
+ td {
+ @include gl-border-0;
+ }
+ }
+ }
+ }
+
+ @include media-breakpoint-down(sm) {
+ .alert-management-table {
+ .table-col {
+ min-height: 68px;
+
+ &:last-child {
+ background-color: $gray-10;
+
+ &::before {
+ content: none !important;
+ }
+
+ div {
+ width: 100% !important;
+ padding: 0 !important;
+ }
+ }
+ }
+ }
+ }
+
+ .gl-tab-nav-item {
+ color: $gl-gray-600;
+
+ > .gl-tab-counter-badge {
+ color: inherit;
+ @include gl-font-sm;
+ background-color: $white-normal;
+ }
+ }
+}
diff --git a/app/assets/stylesheets/pages/alert_management/severity-icons.scss b/app/assets/stylesheets/pages/alert_management/severity-icons.scss
new file mode 100644
index 00000000000..b400e80d5c5
--- /dev/null
+++ b/app/assets/stylesheets/pages/alert_management/severity-icons.scss
@@ -0,0 +1,26 @@
+.alert-management-list,
+.alert-management-details {
+ .icon-critical {
+ color: $red-800;
+ }
+
+ .icon-high {
+ color: $red-600;
+ }
+
+ .icon-medium {
+ color: $orange-400;
+ }
+
+ .icon-low {
+ color: $orange-300;
+ }
+
+ .icon-info {
+ color: $blue-400;
+ }
+
+ .icon-unknown {
+ color: $gray-400;
+ }
+}
diff --git a/app/assets/stylesheets/pages/boards.scss b/app/assets/stylesheets/pages/boards.scss
index 11291dad28b..d755170fe1f 100644
--- a/app/assets/stylesheets/pages/boards.scss
+++ b/app/assets/stylesheets/pages/boards.scss
@@ -548,3 +548,27 @@
*/
height: $input-height;
}
+
+.issue-boards-content.is-focused {
+ position: fixed;
+ width: 100%;
+ height: 100%;
+ top: 0;
+ left: 0;
+ background: $white;
+ z-index: 9000;
+
+ @include media-breakpoint-down(sm) {
+ padding-top: 10px;
+ }
+
+ .boards-list {
+ height: calc(100vh - #{$issue-boards-filter-height});
+ overflow-x: scroll;
+ }
+
+ .issue-boards-sidebar {
+ height: 100%;
+ top: 0;
+ }
+}
diff --git a/app/assets/stylesheets/pages/commits.scss b/app/assets/stylesheets/pages/commits.scss
index 230f390aa90..9a69afc6044 100644
--- a/app/assets/stylesheets/pages/commits.scss
+++ b/app/assets/stylesheets/pages/commits.scss
@@ -208,6 +208,18 @@
}
}
+.commit-nav-buttons {
+ a.btn,
+ button {
+ // See: https://gitlab.com/gitlab-org/gitlab-ui/-/issues/730
+ &:last-child > svg {
+ margin-left: 0.25rem;
+ margin-right: 0;
+ }
+ }
+}
+
+
.clipboard-group,
.commit-sha-group {
display: inline-flex;
diff --git a/app/assets/stylesheets/pages/cycle_analytics.scss b/app/assets/stylesheets/pages/cycle_analytics.scss
index 0292919ea50..b97709e140f 100644
--- a/app/assets/stylesheets/pages/cycle_analytics.scss
+++ b/app/assets/stylesheets/pages/cycle_analytics.scss
@@ -187,7 +187,6 @@
.stage-events {
width: 60%;
- overflow: scroll;
min-height: 467px;
}
diff --git a/app/assets/stylesheets/pages/error_list.scss b/app/assets/stylesheets/pages/error_list.scss
index a61a85649b8..3ec3e4f6b43 100644
--- a/app/assets/stylesheets/pages/error_list.scss
+++ b/app/assets/stylesheets/pages/error_list.scss
@@ -17,7 +17,7 @@
min-height: 68px;
&:last-child {
- background-color: $gray-normal;
+ background-color: $gray-10;
&::before {
content: none !important;
diff --git a/app/assets/stylesheets/pages/issuable.scss b/app/assets/stylesheets/pages/issuable.scss
index 85fdcb753b4..b241d0a2bdc 100644
--- a/app/assets/stylesheets/pages/issuable.scss
+++ b/app/assets/stylesheets/pages/issuable.scss
@@ -167,10 +167,6 @@
}
}
- a.gl-label-icon {
- color: $gray-500;
- }
-
.gl-label .gl-label-link:hover {
text-decoration: none;
color: inherit;
@@ -180,11 +176,6 @@
}
}
- .gl-label .gl-label-icon:hover {
- text-decoration: none;
- color: $gray-500;
- }
-
.btn-link {
color: inherit;
}
@@ -826,10 +817,6 @@
}
}
}
-
- .gl-label-icon {
- color: $gray-500;
- }
}
@media(max-width: map-get($grid-breakpoints, lg)-1) {
diff --git a/app/assets/stylesheets/pages/issues.scss b/app/assets/stylesheets/pages/issues.scss
index e74b420f9e9..0dd25ec5360 100644
--- a/app/assets/stylesheets/pages/issues.scss
+++ b/app/assets/stylesheets/pages/issues.scss
@@ -303,3 +303,13 @@ ul.related-merge-requests > li {
}
}
}
+
+.issuable-list-root {
+ .gl-label-link {
+ text-decoration: none;
+
+ &:hover {
+ color: inherit;
+ }
+ }
+}
diff --git a/app/assets/stylesheets/pages/labels.scss b/app/assets/stylesheets/pages/labels.scss
index 7f6542261b8..22c1cb127cd 100644
--- a/app/assets/stylesheets/pages/labels.scss
+++ b/app/assets/stylesheets/pages/labels.scss
@@ -307,7 +307,7 @@
}
.label-name {
- width: 150px;
+ width: 200px;
flex-shrink: 0;
.scoped-label-wrapper,
@@ -460,8 +460,7 @@
// Label inside title of Delete Label Modal
.modal-header .page-title {
.scoped-label-wrapper {
- .scoped-label,
- .gl-label-icon {
+ .scoped-label {
line-height: 20px;
}
diff --git a/app/assets/stylesheets/pages/milestone.scss b/app/assets/stylesheets/pages/milestone.scss
index fa10ab022dc..c473cc44637 100644
--- a/app/assets/stylesheets/pages/milestone.scss
+++ b/app/assets/stylesheets/pages/milestone.scss
@@ -68,10 +68,6 @@ $status-box-line-height: 26px;
.gl-label-link {
color: inherit;
}
-
- .gl-label-icon {
- color: $gray-500;
- }
}
}
diff --git a/app/assets/stylesheets/pages/notes.scss b/app/assets/stylesheets/pages/notes.scss
index 8b51ba7ae62..bed147aa3a7 100644
--- a/app/assets/stylesheets/pages/notes.scss
+++ b/app/assets/stylesheets/pages/notes.scss
@@ -588,7 +588,8 @@ $note-form-margin-left: 72px;
a {
color: inherit;
- &:hover {
+ &:hover,
+ &.hover {
color: $blue-600;
}
@@ -605,6 +606,21 @@ $note-form-margin-left: 72px;
.author-link {
color: $gl-text-color;
}
+
+ // Prevent flickering of link when hovering between `author-name-link` and `.author-username-link`
+ .author-name-link + .author-username .author-username-link {
+ position: relative;
+
+ &::before {
+ content: '';
+ position: absolute;
+ right: 100%;
+ width: 0.25rem;
+ height: 100%;
+ top: 0;
+ bottom: 0;
+ }
+ }
}
.discussion-header {
@@ -672,8 +688,7 @@ $note-form-margin-left: 72px;
text-decoration: underline;
}
- .gl-label-link:hover,
- .gl-label-icon:hover {
+ .gl-label-link:hover {
text-decoration: none;
color: inherit;
@@ -892,11 +907,10 @@ $note-form-margin-left: 72px;
border-right: 0;
.line-resolve-btn {
- margin-right: 5px;
color: $gray-700;
svg {
- vertical-align: middle;
+ vertical-align: text-top;
}
}
diff --git a/app/assets/stylesheets/pages/pipeline_schedules.scss b/app/assets/stylesheets/pages/pipeline_schedules.scss
index 85c4902eee2..81716991a36 100644
--- a/app/assets/stylesheets/pages/pipeline_schedules.scss
+++ b/app/assets/stylesheets/pages/pipeline_schedules.scss
@@ -21,11 +21,6 @@
.cron-interval-input {
margin: 10px 10px 0 0;
}
-
- .cron-syntax-link-wrap {
- margin-right: 10px;
- font-size: 12px;
- }
}
.pipeline-schedule-table-row {
diff --git a/app/assets/stylesheets/pages/prometheus.scss b/app/assets/stylesheets/pages/prometheus.scss
index f61245bed24..0f56b98a78d 100644
--- a/app/assets/stylesheets/pages/prometheus.scss
+++ b/app/assets/stylesheets/pages/prometheus.scss
@@ -13,6 +13,14 @@
.form-group {
margin-bottom: map-get($spacing-scale, 3);
}
+
+ .variables-section {
+ input {
+ @include media-breakpoint-up(sm) {
+ width: 160px;
+ }
+ }
+ }
}
.draggable {
@@ -143,7 +151,7 @@
> .arrow::after {
border-top: 6px solid transparent;
border-bottom: 6px solid transparent;
- border-left: 4px solid $gray-50;
+ border-left: 4px solid $gray-10;
}
.arrow-shadow {
@@ -165,7 +173,7 @@
> .arrow::after {
border-top: 6px solid transparent;
border-bottom: 6px solid transparent;
- border-right: 4px solid $gray-50;
+ border-right: 4px solid $gray-10;
}
.arrow-shadow {
@@ -199,7 +207,7 @@
}
> .popover-title {
- background-color: $gray-50;
+ background-color: $gray-10;
border-radius: $border-radius-default $border-radius-default 0 0;
}
}
diff --git a/app/assets/stylesheets/pages/settings.scss b/app/assets/stylesheets/pages/settings.scss
index 2c0ca792ec3..d26c07ce51b 100644
--- a/app/assets/stylesheets/pages/settings.scss
+++ b/app/assets/stylesheets/pages/settings.scss
@@ -335,15 +335,6 @@
}
}
-.deprecated-service {
- cursor: default;
-
- a {
- font-weight: $gl-font-weight-bold;
- color: $white;
- }
-}
-
.personal-access-tokens-never-expires-label {
color: $note-disabled-comment-color;
}
@@ -401,4 +392,3 @@
}
}
}
-
diff --git a/app/assets/stylesheets/snippets.scss b/app/assets/stylesheets/snippets.scss
index 93a12cf28a2..d410a16a1d9 100644
--- a/app/assets/stylesheets/snippets.scss
+++ b/app/assets/stylesheets/snippets.scss
@@ -40,10 +40,9 @@
margin: 0;
padding: 0;
table-layout: fixed;
+ overflow-x: auto;
.blob-content {
- overflow-x: auto;
-
pre {
height: 100%;
padding: 10px;
@@ -61,6 +60,7 @@
font-family: $monospace-font;
font-size: $code-font-size;
line-height: $code-line-height;
+ display: inline-block;
}
}
@@ -73,7 +73,7 @@
font-family: $monospace-font;
display: block;
font-size: $code-font-size;
- min-height: $code-line-height;
+ line-height: $code-line-height;
white-space: nowrap;
color: $black-transparent;
min-width: 30px;
diff --git a/app/assets/stylesheets/utilities.scss b/app/assets/stylesheets/utilities.scss
index b829a7b518e..8cf5c533f1f 100644
--- a/app/assets/stylesheets/utilities.scss
+++ b/app/assets/stylesheets/utilities.scss
@@ -1,3 +1,10 @@
+/**
+ Please note: These are deprecated in favor of the Gitlab UI utility classes.
+ Check https://unpkg.com/browse/@gitlab/ui/src/scss/utilities.scss
+ to see the available utility classes. If you cannot find the class you need,
+ consider adding it to Gitlab UI before adding it here.
+**/
+
@each $variant, $range in $color-ranges {
@each $suffix, $color in $range {
#{'.bg-#{$variant}-#{$suffix}'} {
@@ -7,6 +14,12 @@
#{'.text-#{$variant}-#{$suffix}'} {
color: $color;
}
+
+ #{'.hover-text-#{$variant}-#{$suffix}'} {
+ &:hover {
+ color: $color;
+ }
+ }
}
}
@@ -44,6 +57,7 @@
.border-color-default { border-color: $border-color; }
.border-bottom-color-default { border-bottom-color: $border-color; }
.border-radius-default { border-radius: $border-radius-default; }
+.border-radius-small { border-radius: $border-radius-small; }
.box-shadow-default { box-shadow: 0 2px 4px 0 $black-transparent; }
.gl-children-ml-sm-3 > * {
@@ -75,6 +89,19 @@
width: px-to-rem(16px);
}
+.gl-shim-pb-3 {
+ padding-bottom: 8px;
+}
+
+.gl-shim-pt-5 {
+ padding-top: 16px;
+}
+
+.gl-shim-mx-2 {
+ margin-left: 4px;
+ margin-right: 4px;
+}
+
.gl-text-purple { color: $purple; }
.gl-text-gray-800 { color: $gray-800; }
.gl-bg-purple-light { background-color: $purple-light; }
@@ -85,6 +112,7 @@
.gl-bg-blue-50 { @include gl-bg-blue-50; }
.gl-bg-red-100 { @include gl-bg-red-100; }
.gl-bg-orange-100 { @include gl-bg-orange-100; }
+.gl-bg-gray-50 { @include gl-bg-gray-50; }
.gl-bg-gray-100 { @include gl-bg-gray-100; }
.gl-bg-green-100 { @include gl-bg-green-100;}
.gl-bg-blue-500 { @include gl-bg-blue-500; }
@@ -107,8 +135,14 @@
.gl-text-green-700 { @include gl-text-green-700; }
.gl-align-items-center { @include gl-align-items-center; }
+
.d-sm-table-column {
@include media-breakpoint-up(sm) {
display: table-column !important;
}
}
+
+.gl-white-space-normal { @include gl-white-space-normal; }
+.gl-word-break-all { @include gl-word-break-all; }
+.gl-reset-line-height { @include gl-reset-line-height; }
+.gl-reset-text-align { @include gl-reset-text-align; }
diff --git a/app/channels/application_cable/channel.rb b/app/channels/application_cable/channel.rb
new file mode 100644
index 00000000000..9aec2305390
--- /dev/null
+++ b/app/channels/application_cable/channel.rb
@@ -0,0 +1,6 @@
+# frozen_string_literal: true
+
+module ApplicationCable
+ class Channel < ActionCable::Channel::Base
+ end
+end
diff --git a/app/channels/application_cable/connection.rb b/app/channels/application_cable/connection.rb
new file mode 100644
index 00000000000..87c833f3593
--- /dev/null
+++ b/app/channels/application_cable/connection.rb
@@ -0,0 +1,22 @@
+# frozen_string_literal: true
+
+module ApplicationCable
+ class Connection < ActionCable::Connection::Base
+ identified_by :current_user
+
+ def connect
+ self.current_user = find_user_from_session_store
+ end
+
+ private
+
+ def find_user_from_session_store
+ session = ActiveSession.sessions_from_ids([session_id]).first
+ Warden::SessionSerializer.new('rack.session' => session).fetch(:user)
+ end
+
+ def session_id
+ Rack::Session::SessionId.new(cookies[Gitlab::Application.config.session_options[:key]])
+ end
+ end
+end
diff --git a/app/channels/issues_channel.rb b/app/channels/issues_channel.rb
new file mode 100644
index 00000000000..5f3909b7716
--- /dev/null
+++ b/app/channels/issues_channel.rb
@@ -0,0 +1,13 @@
+# frozen_string_literal: true
+
+class IssuesChannel < ApplicationCable::Channel
+ def subscribed
+ project = Project.find_by_full_path(params[:project_path])
+ return reject unless project
+
+ issue = project.issues.find_by_iid(params[:iid])
+ return reject unless issue && Ability.allowed?(current_user, :read_issue, issue)
+
+ stream_for issue
+ end
+end
diff --git a/app/controllers/admin/appearances_controller.rb b/app/controllers/admin/appearances_controller.rb
index 383ec2a7d16..8405f2a5cf8 100644
--- a/app/controllers/admin/appearances_controller.rb
+++ b/app/controllers/admin/appearances_controller.rb
@@ -73,6 +73,7 @@ class Admin::AppearancesController < Admin::ApplicationController
favicon
favicon_cache
new_project_guidelines
+ profile_image_guidelines
updated_by
header_message
footer_message
diff --git a/app/controllers/admin/application_settings_controller.rb b/app/controllers/admin/application_settings_controller.rb
index 16254c74ba4..355662bbb38 100644
--- a/app/controllers/admin/application_settings_controller.rb
+++ b/app/controllers/admin/application_settings_controller.rb
@@ -5,7 +5,7 @@ class Admin::ApplicationSettingsController < Admin::ApplicationController
# NOTE: Use @application_setting in this controller when you need to access
# application_settings after it has been modified. This is because the
- # ApplicationSetting model uses Gitlab::ThreadMemoryCache for caching and the
+ # ApplicationSetting model uses Gitlab::ProcessMemoryCache for caching and the
# cache might be stale immediately after an update.
# https://gitlab.com/gitlab-org/gitlab-foss/-/merge_requests/30233
before_action :set_application_setting, except: :integrations
@@ -43,7 +43,7 @@ class Admin::ApplicationSettingsController < Admin::ApplicationController
def usage_data
respond_to do |format|
format.html do
- usage_data_json = JSON.pretty_generate(Gitlab::UsageData.data)
+ usage_data_json = Gitlab::Json.pretty_generate(Gitlab::UsageData.data)
render html: Gitlab::Highlight.highlight('payload.json', usage_data_json, language: 'json')
end
diff --git a/app/controllers/admin/ci/variables_controller.rb b/app/controllers/admin/ci/variables_controller.rb
new file mode 100644
index 00000000000..ca9b393550d
--- /dev/null
+++ b/app/controllers/admin/ci/variables_controller.rb
@@ -0,0 +1,48 @@
+# frozen_string_literal: true
+
+class Admin::Ci::VariablesController < Admin::ApplicationController
+ def show
+ respond_to do |format|
+ format.json { render_instance_variables }
+ end
+ end
+
+ def update
+ service = Ci::UpdateInstanceVariablesService.new(variables_params)
+
+ if service.execute
+ respond_to do |format|
+ format.json { render_instance_variables }
+ end
+ else
+ respond_to do |format|
+ format.json { render_error(service.errors) }
+ end
+ end
+ end
+
+ private
+
+ def variables
+ @variables ||= Ci::InstanceVariable.all
+ end
+
+ def render_instance_variables
+ render status: :ok,
+ json: {
+ variables: Ci::InstanceVariableSerializer.new.represent(variables)
+ }
+ end
+
+ def render_error(errors)
+ render status: :bad_request, json: errors
+ end
+
+ def variables_params
+ params.permit(variables_attributes: [*variable_params_attributes])
+ end
+
+ def variable_params_attributes
+ %i[id variable_type key secret_value protected masked _destroy]
+ end
+end
diff --git a/app/controllers/admin/dashboard_controller.rb b/app/controllers/admin/dashboard_controller.rb
index cd95105a893..b7b535e70df 100644
--- a/app/controllers/admin/dashboard_controller.rb
+++ b/app/controllers/admin/dashboard_controller.rb
@@ -13,6 +13,7 @@ class Admin::DashboardController < Admin::ApplicationController
@users = User.order_id_desc.limit(10)
@groups = Group.order_id_desc.with_route.limit(10)
@notices = Gitlab::ConfigChecker::PumaRuggedChecker.check
+ @notices += Gitlab::ConfigChecker::ExternalDatabaseChecker.check
end
# rubocop: enable CodeReuse/ActiveRecord
diff --git a/app/controllers/admin/logs_controller.rb b/app/controllers/admin/logs_controller.rb
deleted file mode 100644
index 3ae0aef0fa4..00000000000
--- a/app/controllers/admin/logs_controller.rb
+++ /dev/null
@@ -1,24 +0,0 @@
-# frozen_string_literal: true
-
-class Admin::LogsController < Admin::ApplicationController
- before_action :loggers
-
- def show
- end
-
- private
-
- def loggers
- @loggers ||= [
- Gitlab::AppJsonLogger,
- Gitlab::GitLogger,
- Gitlab::EnvironmentLogger,
- Gitlab::SidekiqLogger,
- Gitlab::RepositoryCheckLogger,
- Gitlab::ProjectServiceLogger,
- Gitlab::Kubernetes::Logger
- ]
- end
-end
-
-Admin::LogsController.prepend_if_ee('EE::Admin::LogsController')
diff --git a/app/controllers/admin/sessions_controller.rb b/app/controllers/admin/sessions_controller.rb
index 1dc1cd5fb82..0c0bbaf4d93 100644
--- a/app/controllers/admin/sessions_controller.rb
+++ b/app/controllers/admin/sessions_controller.rb
@@ -3,6 +3,7 @@
class Admin::SessionsController < ApplicationController
include Authenticates2FAForAdminMode
include InternalRedirect
+ include RendersLdapServers
before_action :user_is_admin!
diff --git a/app/controllers/admin/users_controller.rb b/app/controllers/admin/users_controller.rb
index 8414095d454..ee42baa8326 100644
--- a/app/controllers/admin/users_controller.rb
+++ b/app/controllers/admin/users_controller.rb
@@ -145,7 +145,7 @@ class Admin::UsersController < Admin::ApplicationController
password_confirmation: params[:user][:password_confirmation]
}
- password_params[:password_expires_at] = Time.now unless changing_own_password?
+ password_params[:password_expires_at] = Time.current unless changing_own_password?
user_params_with_pass.merge!(password_params)
end
diff --git a/app/controllers/application_controller.rb b/app/controllers/application_controller.rb
index b5695322eb6..54e3275662b 100644
--- a/app/controllers/application_controller.rb
+++ b/app/controllers/application_controller.rb
@@ -18,6 +18,9 @@ class ApplicationController < ActionController::Base
include Gitlab::Tracking::ControllerConcern
include Gitlab::Experimentation::ControllerConcern
include InitializesCurrentUserMode
+ include Impersonation
+ include Gitlab::Logging::CloudflareHelper
+ include Gitlab::Utils::StrongMemoize
before_action :authenticate_user!, except: [:route_not_found]
before_action :enforce_terms!, if: :should_enforce_terms?
@@ -35,6 +38,10 @@ class ApplicationController < ActionController::Base
before_action :check_impersonation_availability
before_action :required_signup_info
+ # Make sure the `auth_user` is memoized so it can be logged, we do this after
+ # all other before filters that could have set the user.
+ before_action :auth_user
+
prepend_around_action :set_current_context
around_action :sessionless_bypass_admin_mode!, if: :sessionless_user?
@@ -141,16 +148,19 @@ class ApplicationController < ActionController::Base
payload[:ua] = request.env["HTTP_USER_AGENT"]
payload[:remote_ip] = request.remote_ip
+
payload[Labkit::Correlation::CorrelationId::LOG_KEY] = Labkit::Correlation::CorrelationId.current_id
+ payload[:metadata] = @current_context
logged_user = auth_user
-
if logged_user.present?
payload[:user_id] = logged_user.try(:id)
payload[:username] = logged_user.try(:username)
end
payload[:queue_duration_s] = request.env[::Gitlab::Middleware::RailsQueueDuration::GITLAB_RAILS_QUEUE_DURATION_KEY]
+
+ store_cloudflare_headers!(payload, request)
end
##
@@ -158,10 +168,12 @@ class ApplicationController < ActionController::Base
# (e.g. tokens) to authenticate the user, whereas Devise sets current_user.
#
def auth_user
- if user_signed_in?
- current_user
- else
- try(:authenticated_user)
+ strong_memoize(:auth_user) do
+ if user_signed_in?
+ current_user
+ else
+ try(:authenticated_user)
+ end
end
end
@@ -453,11 +465,16 @@ class ApplicationController < ActionController::Base
def set_current_context(&block)
Gitlab::ApplicationContext.with_context(
- user: -> { auth_user },
- project: -> { @project },
- namespace: -> { @group },
- caller_id: full_action_name,
- &block)
+ # Avoid loading the auth_user again after the request. Otherwise calling
+ # `auth_user` again would also trigger the Warden callbacks again
+ user: -> { auth_user if strong_memoized?(:auth_user) },
+ project: -> { @project if @project&.persisted? },
+ namespace: -> { @group if @group&.persisted? },
+ caller_id: full_action_name) do
+ yield
+ ensure
+ @current_context = Labkit::Context.current.to_h
+ end
end
def set_locale(&block)
@@ -525,36 +542,6 @@ class ApplicationController < ActionController::Base
.execute
end
- def check_impersonation_availability
- return unless session[:impersonator_id]
-
- unless Gitlab.config.gitlab.impersonation_enabled
- stop_impersonation
- access_denied! _('Impersonation has been disabled')
- end
- end
-
- def stop_impersonation
- log_impersonation_event
-
- warden.set_user(impersonator, scope: :user)
- session[:impersonator_id] = nil
-
- impersonated_user
- end
-
- def impersonated_user
- current_user
- end
-
- def log_impersonation_event
- Gitlab::AppLogger.info("User #{impersonator.username} has stopped impersonating #{impersonated_user.username}")
- end
-
- def impersonator
- @impersonator ||= User.find(session[:impersonator_id]) if session[:impersonator_id]
- end
-
def sentry_context(&block)
Gitlab::ErrorTracking.with_context(current_user, &block)
end
diff --git a/app/controllers/boards/issues_controller.rb b/app/controllers/boards/issues_controller.rb
index 1bfff210ecf..a18c80b996e 100644
--- a/app/controllers/boards/issues_controller.rb
+++ b/app/controllers/boards/issues_controller.rb
@@ -20,9 +20,6 @@ module Boards
skip_before_action :authenticate_user!, only: [:index]
before_action :validate_id_list, only: [:bulk_move]
before_action :can_move_issues?, only: [:bulk_move]
- before_action do
- push_frontend_feature_flag(:board_search_optimization, board.group, default_enabled: true)
- end
def index
list_service = Boards::Issues::ListService.new(board_parent, current_user, filter_params)
diff --git a/app/controllers/clusters/applications_controller.rb b/app/controllers/clusters/applications_controller.rb
index de14bd319e0..c533fe007d7 100644
--- a/app/controllers/clusters/applications_controller.rb
+++ b/app/controllers/clusters/applications_controller.rb
@@ -47,7 +47,7 @@ class Clusters::ApplicationsController < Clusters::BaseController
end
def cluster_application_params
- params.permit(:application, :hostname, :pages_domain_id, :email, :stack, :modsecurity_enabled, :modsecurity_mode, :host, :port, :protocol)
+ params.permit(:application, :hostname, :pages_domain_id, :email, :stack, :modsecurity_enabled, :modsecurity_mode, :host, :port, :protocol, :waf_log_enabled, :cilium_log_enabled)
end
def cluster_application_destroy_params
diff --git a/app/controllers/clusters/clusters_controller.rb b/app/controllers/clusters/clusters_controller.rb
index 2c9ee69c8c4..aa39d430b24 100644
--- a/app/controllers/clusters/clusters_controller.rb
+++ b/app/controllers/clusters/clusters_controller.rb
@@ -18,20 +18,19 @@ class Clusters::ClustersController < Clusters::BaseController
STATUS_POLLING_INTERVAL = 10_000
def index
- finder = ClusterAncestorsFinder.new(clusterable.subject, current_user)
- clusters = finder.execute
+ @clusters = cluster_list
- # Note: We are paginating through an array here but this should OK as:
- #
- # In CE, we can have a maximum group nesting depth of 21, so including
- # project cluster, we can have max 22 clusters for a group hierarchy.
- # In EE (Premium) we can have any number, as multiple clusters are
- # supported, but the number of clusters are fairly low currently.
- #
- # See https://gitlab.com/gitlab-org/gitlab-foss/issues/55260 also.
- @clusters = Kaminari.paginate_array(clusters).page(params[:page]).per(20)
+ respond_to do |format|
+ format.html
+ format.json do
+ serializer = ClusterSerializer.new(current_user: current_user)
- @has_ancestor_clusters = finder.has_ancestor_clusters?
+ render json: {
+ clusters: serializer.with_pagination(request, response).represent_list(@clusters),
+ has_ancestor_clusters: @has_ancestor_clusters
+ }
+ end
+ end
end
def new
@@ -158,6 +157,23 @@ class Clusters::ClustersController < Clusters::BaseController
private
+ def cluster_list
+ finder = ClusterAncestorsFinder.new(clusterable.subject, current_user)
+ clusters = finder.execute
+
+ @has_ancestor_clusters = finder.has_ancestor_clusters?
+
+ # Note: We are paginating through an array here but this should OK as:
+ #
+ # In CE, we can have a maximum group nesting depth of 21, so including
+ # project cluster, we can have max 22 clusters for a group hierarchy.
+ # In EE (Premium) we can have any number, as multiple clusters are
+ # supported, but the number of clusters are fairly low currently.
+ #
+ # See https://gitlab.com/gitlab-org/gitlab-foss/issues/55260 also.
+ Kaminari.paginate_array(clusters).page(params[:page]).per(20)
+ end
+
def destroy_params
params.permit(:cleanup)
end
diff --git a/app/controllers/concerns/boards_actions.rb b/app/controllers/concerns/boards_actions.rb
index eb1080cb3d2..9d40b9e8c88 100644
--- a/app/controllers/concerns/boards_actions.rb
+++ b/app/controllers/concerns/boards_actions.rb
@@ -10,6 +10,9 @@ module BoardsActions
before_action :boards, only: :index
before_action :board, only: :show
before_action :push_wip_limits, only: [:index, :show]
+ before_action do
+ push_frontend_feature_flag(:not_issuable_queries, parent, default_enabled: true)
+ end
end
def index
diff --git a/app/controllers/concerns/impersonation.rb b/app/controllers/concerns/impersonation.rb
new file mode 100644
index 00000000000..a4f2c263eb4
--- /dev/null
+++ b/app/controllers/concerns/impersonation.rb
@@ -0,0 +1,43 @@
+# frozen_string_literal: true
+
+module Impersonation
+ include Gitlab::Utils::StrongMemoize
+
+ def current_user
+ user = super
+
+ user.impersonator = impersonator if impersonator
+
+ user
+ end
+
+ protected
+
+ def check_impersonation_availability
+ return unless session[:impersonator_id]
+
+ unless Gitlab.config.gitlab.impersonation_enabled
+ stop_impersonation
+ access_denied! _('Impersonation has been disabled')
+ end
+ end
+
+ def stop_impersonation
+ log_impersonation_event
+
+ warden.set_user(impersonator, scope: :user)
+ session[:impersonator_id] = nil
+
+ current_user
+ end
+
+ def log_impersonation_event
+ Gitlab::AppLogger.info("User #{impersonator.username} has stopped impersonating #{current_user.username}")
+ end
+
+ def impersonator
+ strong_memoize(:impersonator) do
+ User.find(session[:impersonator_id]) if session[:impersonator_id]
+ end
+ end
+end
diff --git a/app/controllers/concerns/issuable_actions.rb b/app/controllers/concerns/issuable_actions.rb
index ca43bf42580..0b1b3f2bcba 100644
--- a/app/controllers/concerns/issuable_actions.rb
+++ b/app/controllers/concerns/issuable_actions.rb
@@ -11,6 +11,9 @@ module IssuableActions
before_action only: :show do
push_frontend_feature_flag(:scoped_labels, default_enabled: true)
end
+ before_action do
+ push_frontend_feature_flag(:not_issuable_queries, @project, default_enabled: true)
+ end
end
def permitted_keys
diff --git a/app/controllers/concerns/issuable_collections_action.rb b/app/controllers/concerns/issuable_collections_action.rb
index 0a6f684a9fc..78b3c6771b3 100644
--- a/app/controllers/concerns/issuable_collections_action.rb
+++ b/app/controllers/concerns/issuable_collections_action.rb
@@ -32,6 +32,10 @@ module IssuableCollectionsAction
private
+ def set_not_query_feature_flag(object = nil)
+ push_frontend_feature_flag(:not_issuable_queries, object, default_enabled: true)
+ end
+
def sorting_field
case action_name
when 'issues'
diff --git a/app/controllers/concerns/known_sign_in.rb b/app/controllers/concerns/known_sign_in.rb
new file mode 100644
index 00000000000..97883d8d08c
--- /dev/null
+++ b/app/controllers/concerns/known_sign_in.rb
@@ -0,0 +1,31 @@
+# frozen_string_literal: true
+
+module KnownSignIn
+ include Gitlab::Utils::StrongMemoize
+
+ private
+
+ def verify_known_sign_in
+ return unless current_user
+
+ notify_user unless known_remote_ip?
+ end
+
+ def known_remote_ip?
+ known_ip_addresses.include?(request.remote_ip)
+ end
+
+ def sessions
+ strong_memoize(:session) do
+ ActiveSession.list(current_user).reject(&:is_impersonated)
+ end
+ end
+
+ def known_ip_addresses
+ [current_user.last_sign_in_ip, sessions.map(&:ip_address)].flatten
+ end
+
+ def notify_user
+ current_user.notification_service.unknown_sign_in(current_user, request.remote_ip)
+ end
+end
diff --git a/app/controllers/concerns/members_presentation.rb b/app/controllers/concerns/members_presentation.rb
index 0a9d3d86245..ceccef8113f 100644
--- a/app/controllers/concerns/members_presentation.rb
+++ b/app/controllers/concerns/members_presentation.rb
@@ -5,6 +5,7 @@ module MembersPresentation
def present_members(members)
preload_associations(members)
+
Gitlab::View::Presenter::Factory.new(
members,
current_user: current_user,
diff --git a/app/controllers/concerns/metrics_dashboard.rb b/app/controllers/concerns/metrics_dashboard.rb
index fa79f3bc4e6..1aea0e294a5 100644
--- a/app/controllers/concerns/metrics_dashboard.rb
+++ b/app/controllers/concerns/metrics_dashboard.rb
@@ -18,7 +18,7 @@ module MetricsDashboard
if result
result[:all_dashboards] = all_dashboards if include_all_dashboards?
- result[:metrics_data] = metrics_data(project_for_dashboard, environment_for_dashboard) if project_for_dashboard && environment_for_dashboard
+ result[:metrics_data] = metrics_data(project_for_dashboard, environment_for_dashboard)
end
respond_to do |format|
@@ -35,10 +35,9 @@ module MetricsDashboard
private
def all_dashboards
- dashboards = dashboard_finder.find_all_paths(project_for_dashboard)
- dashboards.map do |dashboard|
- amend_dashboard(dashboard)
- end
+ dashboard_finder
+ .find_all_paths(project_for_dashboard)
+ .map(&method(:amend_dashboard))
end
def amend_dashboard(dashboard)
@@ -46,10 +45,16 @@ module MetricsDashboard
dashboard[:can_edit] = project_dashboard ? can_edit?(dashboard) : false
dashboard[:project_blob_path] = project_dashboard ? dashboard_project_blob_path(dashboard) : nil
+ dashboard[:starred] = starred_dashboards.include?(dashboard[:path])
+ dashboard[:user_starred_path] = project_for_dashboard ? user_starred_path(project_for_dashboard, dashboard[:path]) : nil
dashboard
end
+ def user_starred_path(project, path)
+ expose_path(api_v4_projects_metrics_user_starred_dashboards_path(id: project.id, params: { dashboard_path: path }))
+ end
+
def dashboard_project_blob_path(dashboard)
project_blob_path(project_for_dashboard, File.join(project_for_dashboard.default_branch, dashboard.fetch(:path, "")))
end
@@ -73,6 +78,20 @@ module MetricsDashboard
::Gitlab::Metrics::Dashboard::Finder
end
+ def starred_dashboards
+ @starred_dashboards ||= begin
+ if project_for_dashboard.present?
+ ::Metrics::UsersStarredDashboardsFinder
+ .new(user: current_user, project: project_for_dashboard)
+ .execute
+ .map(&:dashboard_path)
+ .to_set
+ else
+ Set.new
+ end
+ end
+ end
+
# Project is not defined for group and admin level clusters.
def project_for_dashboard
defined?(project) ? project : nil
diff --git a/app/controllers/concerns/notes_actions.rb b/app/controllers/concerns/notes_actions.rb
index 7dd2f6e5706..d4b0d3b2674 100644
--- a/app/controllers/concerns/notes_actions.rb
+++ b/app/controllers/concerns/notes_actions.rb
@@ -13,7 +13,7 @@ module NotesActions
end
def index
- current_fetched_at = Time.now.to_i
+ current_fetched_at = Time.current.to_i
notes_json = { notes: [], last_fetched_at: current_fetched_at }
diff --git a/app/controllers/concerns/preview_markdown.rb b/app/controllers/concerns/preview_markdown.rb
index c7c9f2e9b70..ba15d611c0d 100644
--- a/app/controllers/concerns/preview_markdown.rb
+++ b/app/controllers/concerns/preview_markdown.rb
@@ -37,7 +37,7 @@ module PreviewMarkdown
when 'groups' then { group: group }
when 'projects' then projects_filter_params
else {}
- end.merge(requested_path: params[:path])
+ end.merge(requested_path: params[:path], ref: params[:ref])
end
# rubocop:enable Gitlab/ModuleWithInstanceVariables
diff --git a/app/controllers/concerns/record_user_last_activity.rb b/app/controllers/concerns/record_user_last_activity.rb
index 4013596ba12..29164df4516 100644
--- a/app/controllers/concerns/record_user_last_activity.rb
+++ b/app/controllers/concerns/record_user_last_activity.rb
@@ -17,7 +17,6 @@ module RecordUserLastActivity
def set_user_last_activity
return unless request.get?
- return unless Feature.enabled?(:set_user_last_activity, default_enabled: true)
return if Gitlab::Database.read_only?
if current_user && current_user.last_activity_on != Date.today
diff --git a/app/controllers/concerns/renders_ldap_servers.rb b/app/controllers/concerns/renders_ldap_servers.rb
new file mode 100644
index 00000000000..cc83ff47048
--- /dev/null
+++ b/app/controllers/concerns/renders_ldap_servers.rb
@@ -0,0 +1,19 @@
+# frozen_string_literal: true
+
+module RendersLdapServers
+ extend ActiveSupport::Concern
+
+ included do
+ helper_method :ldap_servers
+ end
+
+ def ldap_servers
+ @ldap_servers ||= begin
+ if Gitlab::Auth::Ldap::Config.sign_in_enabled?
+ Gitlab::Auth::Ldap::Config.available_servers
+ else
+ []
+ end
+ end
+ end
+end
diff --git a/app/controllers/concerns/service_params.rb b/app/controllers/concerns/service_params.rb
index 3ccf227c431..e2c83f9a069 100644
--- a/app/controllers/concerns/service_params.rb
+++ b/app/controllers/concerns/service_params.rb
@@ -19,6 +19,7 @@ module ServiceParams
:color,
:colorize_messages,
:comment_on_event_enabled,
+ :comment_detail,
:confidential_issues_events,
:default_irc_uri,
:description,
diff --git a/app/controllers/concerns/snippets_actions.rb b/app/controllers/concerns/snippets_actions.rb
index 096c6efc0fc..e78723bdda2 100644
--- a/app/controllers/concerns/snippets_actions.rb
+++ b/app/controllers/concerns/snippets_actions.rb
@@ -53,10 +53,10 @@ module SnippetsActions
def blob
return unless snippet
- @blob ||= if Feature.enabled?(:version_snippets, current_user) && !snippet.repository.empty?
- snippet.blobs.first
- else
+ @blob ||= if snippet.empty_repo?
snippet.blob
+ else
+ snippet.blobs.first
end
end
# rubocop:enable Gitlab/ModuleWithInstanceVariables
@@ -65,11 +65,12 @@ module SnippetsActions
params[:line_ending] == 'raw' ? content : content.gsub(/\r\n/, "\n")
end
- def check_repository_error
- repository_errors = Array(snippet.errors.delete(:repository))
+ def handle_repository_error(action)
+ errors = Array(snippet.errors.delete(:repository))
+
+ flash.now[:alert] = errors.first if errors.present?
- flash.now[:alert] = repository_errors.first if repository_errors.present?
- recaptcha_check_with_fallback(repository_errors.empty?) { render :edit }
+ recaptcha_check_with_fallback(errors.empty?) { render action }
end
def redirect_if_binary
diff --git a/app/controllers/concerns/spammable_actions.rb b/app/controllers/concerns/spammable_actions.rb
index 46ba270f328..50c93441dd4 100644
--- a/app/controllers/concerns/spammable_actions.rb
+++ b/app/controllers/concerns/spammable_actions.rb
@@ -82,6 +82,6 @@ module SpammableActions
return false if spammable.errors.count > 1 # re-render "new" template in case there are other errors
return false unless Gitlab::Recaptcha.enabled?
- spammable.spam
+ spammable.needs_recaptcha?
end
end
diff --git a/app/controllers/dashboard/projects_controller.rb b/app/controllers/dashboard/projects_controller.rb
index c173d7d2310..25c48fadf49 100644
--- a/app/controllers/dashboard/projects_controller.rb
+++ b/app/controllers/dashboard/projects_controller.rb
@@ -83,11 +83,13 @@ class Dashboard::ProjectsController < Dashboard::ApplicationController
def use_cte_for_finder?
# The starred action loads public projects, which causes the CTE to be less efficient
- action_name == 'index' && Feature.enabled?(:use_cte_for_projects_finder, default_enabled: true)
+ action_name == 'index'
end
def load_events
- projects = load_projects(params.merge(non_public: true))
+ projects = ProjectsFinder
+ .new(params: params.merge(non_public: true), current_user: current_user)
+ .execute
@events = EventCollection
.new(projects, offset: params[:offset].to_i, filter: event_filter)
diff --git a/app/controllers/dashboard_controller.rb b/app/controllers/dashboard_controller.rb
index 1668cf004f8..dd9e6488bc5 100644
--- a/app/controllers/dashboard_controller.rb
+++ b/app/controllers/dashboard_controller.rb
@@ -10,6 +10,7 @@ class DashboardController < Dashboard::ApplicationController
before_action :projects, only: [:issues, :merge_requests]
before_action :set_show_full_reference, only: [:issues, :merge_requests]
before_action :check_filters_presence!, only: [:issues, :merge_requests]
+ before_action :set_not_query_feature_flag
respond_to :html
diff --git a/app/controllers/google_api/authorizations_controller.rb b/app/controllers/google_api/authorizations_controller.rb
index ed0995e7ffd..5723ccc14a7 100644
--- a/app/controllers/google_api/authorizations_controller.rb
+++ b/app/controllers/google_api/authorizations_controller.rb
@@ -15,6 +15,9 @@ module GoogleApi
session[GoogleApi::CloudPlatform::Client.session_key_for_expires_at] =
expires_at.to_s
+ rescue ::Faraday::TimeoutError, ::Faraday::ConnectionFailed
+ flash[:alert] = _('Timeout connecting to the Google API. Please try again.')
+ ensure
redirect_to redirect_uri_from_session
end
diff --git a/app/controllers/graphql_controller.rb b/app/controllers/graphql_controller.rb
index 522d171b5bf..a1348e4d858 100644
--- a/app/controllers/graphql_controller.rb
+++ b/app/controllers/graphql_controller.rb
@@ -3,7 +3,12 @@
class GraphqlController < ApplicationController
# Unauthenticated users have access to the API for public data
skip_before_action :authenticate_user!
- skip_around_action :set_session_storage
+
+ # If a user is using their session to access GraphQL, we need to have session
+ # storage, since the admin-mode check is session wide.
+ # We can't enable this for anonymous users because that would cause users using
+ # enforced SSO from using an auth token to access the API.
+ skip_around_action :set_session_storage, unless: :current_user
# Allow missing CSRF tokens, this would mean that if a CSRF is invalid or missing,
# the user won't be authenticated but can proceed as an anonymous user.
@@ -14,6 +19,7 @@ class GraphqlController < ApplicationController
before_action :authorize_access_api!
before_action(only: [:execute]) { authenticate_sessionless_user!(:api) }
+ before_action :set_user_last_activity
# Since we deactivate authentication from the main ApplicationController and
# defer it to :authorize_access_api!, we need to override the bypass session
@@ -42,6 +48,12 @@ class GraphqlController < ApplicationController
private
+ def set_user_last_activity
+ return unless current_user
+
+ Users::ActivityService.new(current_user).execute
+ end
+
def execute_multiplex
GitlabSchema.multiplex(multiplex_queries, context: context)
end
diff --git a/app/controllers/groups/group_links_controller.rb b/app/controllers/groups/group_links_controller.rb
index 23daa29ac43..52ee69edaa5 100644
--- a/app/controllers/groups/group_links_controller.rb
+++ b/app/controllers/groups/group_links_controller.rb
@@ -1,7 +1,6 @@
# frozen_string_literal: true
class Groups::GroupLinksController < Groups::ApplicationController
- before_action :check_feature_flag!
before_action :authorize_admin_group!
before_action :group_link, only: [:update, :destroy]
@@ -51,8 +50,4 @@ class Groups::GroupLinksController < Groups::ApplicationController
def group_link_params
params.require(:group_link).permit(:group_access, :expires_at)
end
-
- def check_feature_flag!
- render_404 unless Feature.enabled?(:share_group_with_group, default_enabled: true)
- end
end
diff --git a/app/controllers/groups/group_members_controller.rb b/app/controllers/groups/group_members_controller.rb
index 664c58e8b7a..63311ab983b 100644
--- a/app/controllers/groups/group_members_controller.rb
+++ b/app/controllers/groups/group_members_controller.rb
@@ -21,19 +21,26 @@ class Groups::GroupMembersController < Groups::ApplicationController
def index
@sort = params[:sort].presence || sort_value_name
+
@project = @group.projects.find(params[:project_id]) if params[:project_id]
- @members = find_members
+
+ @members = GroupMembersFinder
+ .new(@group, current_user, params: filter_params)
+ .execute(include_relations: requested_relations)
if can_manage_members
@skip_groups = @group.related_group_ids
- @invited_members = present_invited_members(@members)
+
+ @invited_members = @members.invite
+ @invited_members = @invited_members.search_invite_email(params[:search_invited]) if params[:search_invited].present?
+ @invited_members = present_invited_members(@invited_members)
end
- @members = @members.non_invite
- @members = present_group_members(@members)
+ @members = present_group_members(@members.non_invite)
@requesters = present_members(
- AccessRequestsFinder.new(@group).execute(current_user))
+ AccessRequestsFinder.new(@group).execute(current_user)
+ )
@group_member = @group.group_members.new
end
@@ -43,30 +50,24 @@ class Groups::GroupMembersController < Groups::ApplicationController
private
- def present_invited_members(members)
- invited_members = members.invite
-
- if params[:search_invited].present?
- invited_members = invited_members.search_invite_email(params[:search_invited])
- end
-
- present_members(invited_members
- .page(params[:invited_members_page])
- .per(MEMBER_PER_PAGE_LIMIT))
+ def can_manage_members
+ can?(current_user, :admin_group_member, @group)
end
- def find_members
- filter_params = params.slice(:two_factor, :search).merge(sort: @sort)
- GroupMembersFinder.new(@group, current_user, params: filter_params).execute(include_relations: requested_relations)
+ def present_invited_members(invited_members)
+ present_members(invited_members
+ .page(params[:invited_members_page])
+ .per(MEMBER_PER_PAGE_LIMIT))
end
- def can_manage_members
- can?(current_user, :admin_group_member, @group)
+ def present_group_members(members)
+ present_members(members
+ .page(params[:page])
+ .per(MEMBER_PER_PAGE_LIMIT))
end
- def present_group_members(original_members)
- members = original_members.page(params[:page]).per(MEMBER_PER_PAGE_LIMIT)
- present_members(members)
+ def filter_params
+ params.permit(:two_factor, :search).merge(sort: @sort)
end
end
diff --git a/app/controllers/groups/registry/repositories_controller.rb b/app/controllers/groups/registry/repositories_controller.rb
index 16aa6e50320..14651e0794a 100644
--- a/app/controllers/groups/registry/repositories_controller.rb
+++ b/app/controllers/groups/registry/repositories_controller.rb
@@ -9,7 +9,9 @@ module Groups
respond_to do |format|
format.html
format.json do
- @images = ContainerRepositoriesFinder.new(user: current_user, subject: group).execute.with_api_entity_associations
+ @images = ContainerRepositoriesFinder.new(user: current_user, subject: group, params: params.slice(:name))
+ .execute
+ .with_api_entity_associations
track_event(:list_repositories)
diff --git a/app/controllers/groups/settings/repository_controller.rb b/app/controllers/groups/settings/repository_controller.rb
index 6e8c5628d24..4af5e613296 100644
--- a/app/controllers/groups/settings/repository_controller.rb
+++ b/app/controllers/groups/settings/repository_controller.rb
@@ -46,7 +46,7 @@ module Groups
end
def deploy_token_params
- params.require(:deploy_token).permit(:name, :expires_at, :read_repository, :read_registry, :write_registry, :username)
+ params.require(:deploy_token).permit(:name, :expires_at, :read_repository, :read_registry, :write_registry, :read_package_registry, :write_package_registry, :username)
end
end
end
diff --git a/app/controllers/groups_controller.rb b/app/controllers/groups_controller.rb
index 44120fda17c..d5f2239b16a 100644
--- a/app/controllers/groups_controller.rb
+++ b/app/controllers/groups_controller.rb
@@ -31,6 +31,10 @@ class GroupsController < Groups::ApplicationController
push_frontend_feature_flag(:vue_issuables_list, @group)
end
+ before_action do
+ set_not_query_feature_flag(@group)
+ end
+
before_action :export_rate_limit, only: [:export, :download_export]
skip_cross_project_access_check :index, :new, :create, :edit, :update,
@@ -142,7 +146,7 @@ class GroupsController < Groups::ApplicationController
export_service = Groups::ImportExport::ExportService.new(group: @group, user: current_user)
if export_service.async_execute
- redirect_to edit_group_path(@group), notice: _('Group export started.')
+ redirect_to edit_group_path(@group), notice: _('Group export started. A download link will be sent by email and made available on this page.')
else
redirect_to edit_group_path(@group), alert: _('Group export could not be started.')
end
diff --git a/app/controllers/help_controller.rb b/app/controllers/help_controller.rb
index 91bba1eb617..a1bbcf34f69 100644
--- a/app/controllers/help_controller.rb
+++ b/app/controllers/help_controller.rb
@@ -26,7 +26,7 @@ class HelpController < ApplicationController
respond_to do |format|
format.any(:markdown, :md, :html) do
- # Note: We are purposefully NOT using `Rails.root.join`
+ # Note: We are purposefully NOT using `Rails.root.join` because of https://gitlab.com/gitlab-org/gitlab/-/issues/216028.
path = File.join(Rails.root, 'doc', "#{@path}.md")
if File.exist?(path)
@@ -42,7 +42,7 @@ class HelpController < ApplicationController
# Allow access to specific media files in the doc folder
format.any(:png, :gif, :jpeg, :mp4, :mp3) do
- # Note: We are purposefully NOT using `Rails.root.join`
+ # Note: We are purposefully NOT using `Rails.root.join` because of https://gitlab.com/gitlab-org/gitlab/-/issues/216028.
path = File.join(Rails.root, 'doc', "#{@path}.#{params[:format]}")
if File.exist?(path)
diff --git a/app/controllers/ide_controller.rb b/app/controllers/ide_controller.rb
index bffbdf01f8f..8a838db04f9 100644
--- a/app/controllers/ide_controller.rb
+++ b/app/controllers/ide_controller.rb
@@ -6,10 +6,6 @@ class IdeController < ApplicationController
include ClientsidePreviewCSP
include StaticObjectExternalStorageCSP
- before_action do
- push_frontend_feature_flag(:webide_dark_theme)
- end
-
def index
Gitlab::UsageDataCounters::WebIdeCounter.increment_views_count
end
diff --git a/app/controllers/import/github_controller.rb b/app/controllers/import/github_controller.rb
index 34af1ecd6a5..4e8ceae75bd 100644
--- a/app/controllers/import/github_controller.rb
+++ b/app/controllers/import/github_controller.rb
@@ -144,7 +144,7 @@ class Import::GithubController < Import::BaseController
end
def provider_rate_limit(exception)
- reset_time = Time.at(exception.response_headers['x-ratelimit-reset'].to_i)
+ reset_time = Time.zone.at(exception.response_headers['x-ratelimit-reset'].to_i)
session[access_token_key] = nil
redirect_to new_import_url,
alert: _("GitHub API rate limit exceeded. Try again after %{reset_time}") % { reset_time: reset_time }
diff --git a/app/controllers/import/google_code_controller.rb b/app/controllers/import/google_code_controller.rb
index 4dddfbcd20d..03bde0345e3 100644
--- a/app/controllers/import/google_code_controller.rb
+++ b/app/controllers/import/google_code_controller.rb
@@ -15,7 +15,7 @@ class Import::GoogleCodeController < Import::BaseController
end
begin
- dump = JSON.parse(dump_file.read)
+ dump = Gitlab::Json.parse(dump_file.read)
rescue
return redirect_back_or_default(options: { alert: _("The uploaded file is not a valid Google Takeout archive.") })
end
@@ -42,7 +42,7 @@ class Import::GoogleCodeController < Import::BaseController
user_map_json = "{}" if user_map_json.blank?
begin
- user_map = JSON.parse(user_map_json)
+ user_map = Gitlab::Json.parse(user_map_json)
rescue
flash.now[:alert] = _("The entered user map is not a valid JSON user map.")
diff --git a/app/controllers/jwt_controller.rb b/app/controllers/jwt_controller.rb
index da39d64c93d..3e7755046cd 100644
--- a/app/controllers/jwt_controller.rb
+++ b/app/controllers/jwt_controller.rb
@@ -4,7 +4,9 @@ class JwtController < ApplicationController
skip_around_action :set_session_storage
skip_before_action :authenticate_user!
skip_before_action :verify_authenticity_token
- before_action :authenticate_project_or_user
+
+ # Add this before other actions, since we want to have the user or project
+ prepend_before_action :auth_user, :authenticate_project_or_user
SERVICES = {
Auth::ContainerRegistryAuthenticationService::AUDIENCE => Auth::ContainerRegistryAuthenticationService
@@ -75,4 +77,11 @@ class JwtController < ApplicationController
Array(Rack::Utils.parse_query(request.query_string)['scope'])
end
+
+ def auth_user
+ strong_memoize(:auth_user) do
+ actor = @authentication_result&.actor
+ actor.is_a?(User) ? actor : nil
+ end
+ end
end
diff --git a/app/controllers/ldap/omniauth_callbacks_controller.rb b/app/controllers/ldap/omniauth_callbacks_controller.rb
index 8e4d8f3d21b..4b6339c21cd 100644
--- a/app/controllers/ldap/omniauth_callbacks_controller.rb
+++ b/app/controllers/ldap/omniauth_callbacks_controller.rb
@@ -16,6 +16,10 @@ class Ldap::OmniauthCallbacksController < OmniauthCallbacksController
def ldap
return unless Gitlab::Auth::Ldap::Config.sign_in_enabled?
+ if Feature.enabled?(:user_mode_in_session)
+ return admin_mode_flow(Gitlab::Auth::Ldap::User) if current_user_mode.admin_mode_requested?
+ end
+
sign_in_user_flow(Gitlab::Auth::Ldap::User)
end
diff --git a/app/controllers/omniauth_callbacks_controller.rb b/app/controllers/omniauth_callbacks_controller.rb
index d82a46e57ea..4c595313cb6 100644
--- a/app/controllers/omniauth_callbacks_controller.rb
+++ b/app/controllers/omniauth_callbacks_controller.rb
@@ -6,6 +6,9 @@ class OmniauthCallbacksController < Devise::OmniauthCallbacksController
include Devise::Controllers::Rememberable
include AuthHelper
include InitializesCurrentUserMode
+ include KnownSignIn
+
+ after_action :verify_known_sign_in
protect_from_forgery except: [:kerberos, :saml, :cas3, :failure], with: :exception, prepend: true
@@ -87,6 +90,14 @@ class OmniauthCallbacksController < Devise::OmniauthCallbacksController
private
+ def after_omniauth_failure_path_for(scope)
+ if Feature.enabled?(:user_mode_in_session)
+ return new_admin_session_path if current_user_mode.admin_mode_requested?
+ end
+
+ super
+ end
+
def omniauth_flow(auth_module, identity_linker: nil)
if fragment = request.env.dig('omniauth.params', 'redirect_fragment').presence
store_redirect_fragment(fragment)
diff --git a/app/controllers/projects/alert_management_controller.rb b/app/controllers/projects/alert_management_controller.rb
new file mode 100644
index 00000000000..0c0a91e136f
--- /dev/null
+++ b/app/controllers/projects/alert_management_controller.rb
@@ -0,0 +1,16 @@
+# frozen_string_literal: true
+
+class Projects::AlertManagementController < Projects::ApplicationController
+ before_action :authorize_read_alert_management_alert!
+ before_action do
+ push_frontend_feature_flag(:alert_list_status_filtering_enabled)
+ push_frontend_feature_flag(:create_issue_from_alert_enabled)
+ end
+
+ def index
+ end
+
+ def details
+ @alert_id = params[:id]
+ end
+end
diff --git a/app/controllers/projects/artifacts_controller.rb b/app/controllers/projects/artifacts_controller.rb
index 50399a8cfbb..b8663bc59f2 100644
--- a/app/controllers/projects/artifacts_controller.rb
+++ b/app/controllers/projects/artifacts_controller.rb
@@ -10,7 +10,7 @@ class Projects::ArtifactsController < Projects::ApplicationController
before_action :authorize_update_build!, only: [:keep]
before_action :authorize_destroy_artifacts!, only: [:destroy]
before_action :extract_ref_name_and_path
- before_action :validate_artifacts!, except: [:index, :download, :destroy]
+ before_action :validate_artifacts!, except: [:index, :download, :raw, :destroy]
before_action :entry, only: [:file]
MAX_PER_PAGE = 20
@@ -22,7 +22,7 @@ class Projects::ArtifactsController < Projects::ApplicationController
# issues: https://gitlab.com/gitlab-org/gitlab/issues/32281
return head :no_content unless Feature.enabled?(:artifacts_management_page, @project)
- finder = ArtifactsFinder.new(@project, artifacts_params)
+ finder = Ci::JobArtifactsFinder.new(@project, artifacts_params)
all_artifacts = finder.execute
@artifacts = all_artifacts.page(params[:page]).per(MAX_PER_PAGE)
@@ -73,9 +73,11 @@ class Projects::ArtifactsController < Projects::ApplicationController
end
def raw
+ return render_404 unless zip_artifact?
+
path = Gitlab::Ci::Build::Artifacts::Path.new(params[:path])
- send_artifacts_entry(build, path)
+ send_artifacts_entry(artifacts_file, path)
end
def keep
@@ -138,6 +140,13 @@ class Projects::ArtifactsController < Projects::ApplicationController
@artifacts_file ||= build&.artifacts_file_for_type(params[:file_type] || :archive)
end
+ def zip_artifact?
+ types = HashWithIndifferentAccess.new(Ci::JobArtifact::TYPE_AND_FORMAT_PAIRS)
+ file_type = params[:file_type] || :archive
+
+ types[file_type] == :zip
+ end
+
def entry
@entry = build.artifacts_metadata_entry(params[:path])
diff --git a/app/controllers/projects/branches_controller.rb b/app/controllers/projects/branches_controller.rb
index 09754409104..cc595740696 100644
--- a/app/controllers/projects/branches_controller.rb
+++ b/app/controllers/projects/branches_controller.rb
@@ -186,7 +186,6 @@ class Projects::BranchesController < Projects::ApplicationController
end
def confidential_issue_project
- return unless helpers.create_confidential_merge_request_enabled?
return if params[:confidential_issue_project_id].blank?
confidential_issue_project = Project.find(params[:confidential_issue_project_id])
diff --git a/app/controllers/projects/ci/daily_build_group_report_results_controller.rb b/app/controllers/projects/ci/daily_build_group_report_results_controller.rb
new file mode 100644
index 00000000000..dfda5fca310
--- /dev/null
+++ b/app/controllers/projects/ci/daily_build_group_report_results_controller.rb
@@ -0,0 +1,77 @@
+# frozen_string_literal: true
+
+class Projects::Ci::DailyBuildGroupReportResultsController < Projects::ApplicationController
+ include Gitlab::Utils::StrongMemoize
+
+ MAX_ITEMS = 1000
+ REPORT_WINDOW = 90.days
+
+ before_action :validate_feature_flag!
+ before_action :authorize_download_code! # Share the same authorization rules as the graphs controller
+ before_action :validate_param_type!
+
+ def index
+ respond_to do |format|
+ format.csv { send_data(render_csv(results), type: 'text/csv; charset=utf-8') }
+ end
+ end
+
+ private
+
+ def validate_feature_flag!
+ render_404 unless Feature.enabled?(:ci_download_daily_code_coverage, project, default_enabled: true)
+ end
+
+ def validate_param_type!
+ respond_422 unless allowed_param_types.include?(param_type)
+ end
+
+ def render_csv(collection)
+ CsvBuilders::SingleBatch.new(
+ collection,
+ {
+ date: 'date',
+ group_name: 'group_name',
+ param_type => -> (record) { record.data[param_type] }
+ }
+ ).render
+ end
+
+ def results
+ Ci::DailyBuildGroupReportResultsFinder.new(finder_params).execute
+ end
+
+ def finder_params
+ {
+ current_user: current_user,
+ project: project,
+ ref_path: params.require(:ref_path),
+ start_date: start_date,
+ end_date: end_date,
+ limit: MAX_ITEMS
+ }
+ end
+
+ def start_date
+ strong_memoize(:start_date) do
+ start_date = Date.parse(params.require(:start_date))
+
+ # The start_date cannot be older than `end_date - 90 days`
+ [start_date, end_date - REPORT_WINDOW].max
+ end
+ end
+
+ def end_date
+ strong_memoize(:end_date) do
+ Date.parse(params.require(:end_date))
+ end
+ end
+
+ def allowed_param_types
+ Ci::DailyBuildGroupReportResult::PARAM_TYPES
+ end
+
+ def param_type
+ params.require(:param_type)
+ end
+end
diff --git a/app/controllers/projects/design_management/designs/raw_images_controller.rb b/app/controllers/projects/design_management/designs/raw_images_controller.rb
new file mode 100644
index 00000000000..beb7e9d294b
--- /dev/null
+++ b/app/controllers/projects/design_management/designs/raw_images_controller.rb
@@ -0,0 +1,30 @@
+# frozen_string_literal: true
+
+# Returns full-size design images
+module Projects
+ module DesignManagement
+ module Designs
+ class RawImagesController < Projects::DesignManagement::DesignsController
+ include SendsBlob
+
+ skip_before_action :default_cache_headers, only: :show
+
+ def show
+ blob = design_repository.blob_at(ref, design.full_path)
+
+ send_blob(design_repository, blob, inline: false, allow_caching: project.public?)
+ end
+
+ private
+
+ def design_repository
+ @design_repository ||= project.design_repository
+ end
+
+ def ref
+ sha || design_repository.root_ref
+ end
+ end
+ end
+ end
+end
diff --git a/app/controllers/projects/design_management/designs/resized_image_controller.rb b/app/controllers/projects/design_management/designs/resized_image_controller.rb
new file mode 100644
index 00000000000..50a997f32db
--- /dev/null
+++ b/app/controllers/projects/design_management/designs/resized_image_controller.rb
@@ -0,0 +1,46 @@
+# frozen_string_literal: true
+
+# Returns smaller sized design images
+module Projects
+ module DesignManagement
+ module Designs
+ class ResizedImageController < Projects::DesignManagement::DesignsController
+ include SendFileUpload
+
+ before_action :validate_size!
+
+ skip_before_action :default_cache_headers, only: :show
+
+ def show
+ relation = design.actions
+ relation = relation.up_to_version(sha) if sha
+ action = relation.most_recent.first
+
+ return render_404 unless action
+
+ # This controller returns a 404 if the the `size` param
+ # is not one of our specific sizes, so using `send` here is safe.
+ uploader = action.public_send(:"image_#{size}") # rubocop:disable GitlabSecurity/PublicSend
+
+ return render_404 unless uploader.file # The image has not been processed
+
+ if stale?(etag: action.cache_key)
+ workhorse_set_content_type!
+
+ send_upload(uploader, attachment: design.filename)
+ end
+ end
+
+ private
+
+ def validate_size!
+ render_404 unless ::DesignManagement::DESIGN_IMAGE_SIZES.include?(size)
+ end
+
+ def size
+ params[:id]
+ end
+ end
+ end
+ end
+end
diff --git a/app/controllers/projects/design_management/designs_controller.rb b/app/controllers/projects/design_management/designs_controller.rb
new file mode 100644
index 00000000000..fec09fa9515
--- /dev/null
+++ b/app/controllers/projects/design_management/designs_controller.rb
@@ -0,0 +1,21 @@
+# frozen_string_literal: true
+
+class Projects::DesignManagement::DesignsController < Projects::ApplicationController
+ before_action :authorize_read_design!
+
+ private
+
+ def authorize_read_design!
+ unless can?(current_user, :read_design, design)
+ access_denied!
+ end
+ end
+
+ def design
+ @design ||= project.designs.find(params[:design_id])
+ end
+
+ def sha
+ params[:sha].presence
+ end
+end
diff --git a/app/controllers/projects/environments_controller.rb b/app/controllers/projects/environments_controller.rb
index 09dc4d118a1..5f4d88c57e9 100644
--- a/app/controllers/projects/environments_controller.rb
+++ b/app/controllers/projects/environments_controller.rb
@@ -4,6 +4,12 @@ class Projects::EnvironmentsController < Projects::ApplicationController
include MetricsDashboard
layout 'project'
+
+ before_action only: [:metrics, :additional_metrics, :metrics_dashboard] do
+ authorize_metrics_dashboard!
+
+ push_frontend_feature_flag(:prometheus_computed_alerts)
+ end
before_action :authorize_read_environment!
before_action :authorize_create_environment!, only: [:new, :create]
before_action :authorize_stop_environment!, only: [:stop]
@@ -12,10 +18,6 @@ class Projects::EnvironmentsController < Projects::ApplicationController
before_action :environment, only: [:show, :edit, :update, :stop, :terminal, :terminal_websocket_authorize, :metrics, :cancel_auto_stop]
before_action :verify_api_request!, only: :terminal_websocket_authorize
before_action :expire_etag_cache, only: [:index], unless: -> { request.format.json? }
- before_action only: [:metrics, :additional_metrics, :metrics_dashboard] do
- push_frontend_feature_flag(:prometheus_computed_alerts)
- push_frontend_feature_flag(:metrics_dashboard_annotations)
- end
after_action :expire_etag_cache, only: [:cancel_auto_stop]
def index
@@ -27,12 +29,13 @@ class Projects::EnvironmentsController < Projects::ApplicationController
format.html
format.json do
Gitlab::PollingInterval.set_header(response, interval: 3_000)
+ environments_count_by_state = project.environments.count_by_state
render json: {
environments: serialize_environments(request, response, params[:nested]),
review_app: serialize_review_app,
- available_count: project.environments.available.count,
- stopped_count: project.environments.stopped.count
+ available_count: environments_count_by_state[:available],
+ stopped_count: environments_count_by_state[:stopped]
}
end
end
diff --git a/app/controllers/projects/graphs_controller.rb b/app/controllers/projects/graphs_controller.rb
index 889dcefb65a..34246f27241 100644
--- a/app/controllers/projects/graphs_controller.rb
+++ b/app/controllers/projects/graphs_controller.rb
@@ -28,6 +28,7 @@ class Projects::GraphsController < Projects::ApplicationController
def charts
get_commits
get_languages
+ get_daily_coverage_options
end
def ci
@@ -52,6 +53,27 @@ class Projects::GraphsController < Projects::ApplicationController
end
end
+ def get_daily_coverage_options
+ return unless Feature.enabled?(:ci_download_daily_code_coverage, default_enabled: true)
+
+ date_today = Date.current
+ report_window = Projects::Ci::DailyBuildGroupReportResultsController::REPORT_WINDOW
+
+ @daily_coverage_options = {
+ base_params: {
+ start_date: date_today - report_window,
+ end_date: date_today,
+ ref_path: @project.repository.expand_ref(@ref),
+ param_type: 'coverage'
+ },
+ download_path: namespace_project_ci_daily_build_group_report_results_path(
+ namespace_id: @project.namespace,
+ project_id: @project,
+ format: :csv
+ )
+ }
+ end
+
def fetch_graph
@commits = @project.repository.commits(@ref, limit: 6000, skip_merges: true)
@log = []
diff --git a/app/controllers/projects/import/jira_controller.rb b/app/controllers/projects/import/jira_controller.rb
index 4a70ed45404..711e23dc3ce 100644
--- a/app/controllers/projects/import/jira_controller.rb
+++ b/app/controllers/projects/import/jira_controller.rb
@@ -36,7 +36,7 @@ module Projects
response = ::JiraImport::StartImportService.new(current_user, @project, jira_project_key).execute
flash[:notice] = response.message if response.message.present?
else
- flash[:alert] = 'No jira project key has been provided.'
+ flash[:alert] = 'No Jira project key has been provided.'
end
redirect_to project_import_jira_path(@project)
diff --git a/app/controllers/projects/issues_controller.rb b/app/controllers/projects/issues_controller.rb
index 3aae8990f07..3e9d956f7b1 100644
--- a/app/controllers/projects/issues_controller.rb
+++ b/app/controllers/projects/issues_controller.rb
@@ -21,7 +21,6 @@ class Projects::IssuesController < Projects::ApplicationController
prepend_before_action(only: [:index]) { authenticate_sessionless_user!(:rss) }
prepend_before_action(only: [:calendar]) { authenticate_sessionless_user!(:ics) }
prepend_before_action :authenticate_user!, only: [:new, :export_csv]
- # designs is only applicable to EE, but defining a prepend_before_action in EE code would overwrite this
prepend_before_action :store_uri, only: [:new, :show, :designs]
before_action :whitelist_query_limiting, only: [:create, :create_merge_request, :move, :bulk_update]
@@ -50,6 +49,10 @@ class Projects::IssuesController < Projects::ApplicationController
push_frontend_feature_flag(:save_issuable_health_status, project.group, default_enabled: true)
end
+ before_action only: :show do
+ push_frontend_feature_flag(:real_time_issue_sidebar, @project)
+ end
+
around_action :allow_gitaly_ref_name_caching, only: [:discussions]
respond_to :html
@@ -81,11 +84,13 @@ class Projects::IssuesController < Projects::ApplicationController
)
build_params = issue_params.merge(
merge_request_to_resolve_discussions_of: params[:merge_request_to_resolve_discussions_of],
- discussion_to_resolve: params[:discussion_to_resolve]
+ discussion_to_resolve: params[:discussion_to_resolve],
+ confidential: !!Gitlab::Utils.to_boolean(params[:issue][:confidential])
)
service = Issues::BuildService.new(project, current_user, build_params)
@issue = @noteable = service.execute
+
@merge_request_to_resolve_discussions_of = service.merge_request_to_resolve_discussions_of
@discussion_to_resolve = service.discussions_to_resolve.first if params[:discussion_to_resolve]
@@ -154,7 +159,10 @@ class Projects::IssuesController < Projects::ApplicationController
end
def related_branches
- @related_branches = Issues::RelatedBranchesService.new(project, current_user).execute(issue)
+ @related_branches = Issues::RelatedBranchesService
+ .new(project, current_user)
+ .execute(issue)
+ .map { |branch| branch.merge(link: branch_link(branch)) }
respond_to do |format|
format.json do
@@ -179,7 +187,7 @@ class Projects::IssuesController < Projects::ApplicationController
def create_merge_request
create_params = params.slice(:branch_name, :ref).merge(issue_iid: issue.iid)
- create_params[:target_project_id] = params[:target_project_id] if helpers.create_confidential_merge_request_enabled?
+ create_params[:target_project_id] = params[:target_project_id]
result = ::MergeRequests::CreateFromIssueService.new(project, current_user, create_params).execute
if result[:status] == :success
@@ -193,7 +201,8 @@ class Projects::IssuesController < Projects::ApplicationController
ExportCsvWorker.perform_async(current_user.id, project.id, finder_options.to_h) # rubocop:disable CodeReuse/Worker
index_path = project_issues_path(project)
- redirect_to(index_path, notice: "Your CSV export has started. It will be emailed to #{current_user.notification_email} when complete.")
+ message = _('Your CSV export has started. It will be emailed to %{email} when complete.') % { email: current_user.notification_email }
+ redirect_to(index_path, notice: message)
end
def import_csv
@@ -305,6 +314,10 @@ class Projects::IssuesController < Projects::ApplicationController
private
+ def branch_link(branch)
+ project_compare_path(project, from: project.default_branch, to: branch[:name])
+ end
+
def create_rate_limit
key = :issues_create
diff --git a/app/controllers/projects/mattermosts_controller.rb b/app/controllers/projects/mattermosts_controller.rb
index 085b1bc1498..cfaeddf711a 100644
--- a/app/controllers/projects/mattermosts_controller.rb
+++ b/app/controllers/projects/mattermosts_controller.rb
@@ -30,7 +30,7 @@ class Projects::MattermostsController < Projects::ApplicationController
def configure_params
params.require(:mattermost).permit(:trigger, :team_id).merge(
url: service_trigger_url(@service),
- icon_url: asset_url('slash-command-logo.png'))
+ icon_url: asset_url('slash-command-logo.png', skip_pipeline: true))
end
def teams
diff --git a/app/controllers/projects/merge_requests/creations_controller.rb b/app/controllers/projects/merge_requests/creations_controller.rb
index 23222cbd37c..28aa1b300aa 100644
--- a/app/controllers/projects/merge_requests/creations_controller.rb
+++ b/app/controllers/projects/merge_requests/creations_controller.rb
@@ -16,7 +16,6 @@ class Projects::MergeRequests::CreationsController < Projects::MergeRequests::Ap
end
def create
- @target_branches ||= []
@merge_request = ::MergeRequests::CreateService.new(project, current_user, merge_request_params).execute
if @merge_request.valid?
@@ -97,13 +96,6 @@ class Projects::MergeRequests::CreationsController < Projects::MergeRequests::Ap
def define_new_vars
@noteable = @merge_request
-
- @target_branches = if @merge_request.target_project
- @merge_request.target_project.repository.branch_names
- else
- []
- end
-
@target_project = @merge_request.target_project
@source_project = @merge_request.source_project
diff --git a/app/controllers/projects/merge_requests/diffs_controller.rb b/app/controllers/projects/merge_requests/diffs_controller.rb
index 79598c0aaff..2331674f42c 100644
--- a/app/controllers/projects/merge_requests/diffs_controller.rb
+++ b/app/controllers/projects/merge_requests/diffs_controller.rb
@@ -9,6 +9,8 @@ class Projects::MergeRequests::DiffsController < Projects::MergeRequests::Applic
before_action :define_diff_vars
before_action :define_diff_comment_vars, except: [:diffs_batch, :diffs_metadata]
+ around_action :allow_gitaly_ref_name_caching
+
def show
render_diffs
end
diff --git a/app/controllers/projects/merge_requests_controller.rb b/app/controllers/projects/merge_requests_controller.rb
index 8c37d70d4c9..5613b5b9589 100644
--- a/app/controllers/projects/merge_requests_controller.rb
+++ b/app/controllers/projects/merge_requests_controller.rb
@@ -14,7 +14,13 @@ class Projects::MergeRequestsController < Projects::MergeRequests::ApplicationCo
skip_before_action :merge_request, only: [:index, :bulk_update]
before_action :whitelist_query_limiting, only: [:assign_related_issues, :update]
before_action :authorize_update_issuable!, only: [:close, :edit, :update, :remove_wip, :sort]
- before_action :authorize_read_actual_head_pipeline!, only: [:test_reports, :exposed_artifacts, :coverage_reports]
+ before_action :authorize_read_actual_head_pipeline!, only: [
+ :test_reports,
+ :exposed_artifacts,
+ :coverage_reports,
+ :terraform_reports,
+ :accessibility_reports
+ ]
before_action :set_issuables_index, only: [:index]
before_action :authenticate_user!, only: [:assign_related_issues]
before_action :check_user_can_push_to_source_branch!, only: [:rebase]
@@ -26,7 +32,7 @@ class Projects::MergeRequestsController < Projects::MergeRequests::ApplicationCo
push_frontend_feature_flag(:code_navigation, @project)
push_frontend_feature_flag(:widget_visibility_polling, @project, default_enabled: true)
push_frontend_feature_flag(:merge_ref_head_comments, @project)
- push_frontend_feature_flag(:diff_compare_with_head, @project)
+ push_frontend_feature_flag(:mr_commit_neighbor_nav, @project, default_enabled: true)
end
before_action do
@@ -136,6 +142,14 @@ class Projects::MergeRequestsController < Projects::MergeRequests::ApplicationCo
reports_response(@merge_request.compare_test_reports)
end
+ def accessibility_reports
+ if @merge_request.has_accessibility_reports?
+ reports_response(@merge_request.compare_accessibility_reports)
+ else
+ head :no_content
+ end
+ end
+
def coverage_reports
if @merge_request.has_coverage_reports?
reports_response(@merge_request.find_coverage_reports)
@@ -144,6 +158,10 @@ class Projects::MergeRequestsController < Projects::MergeRequests::ApplicationCo
end
end
+ def terraform_reports
+ reports_response(@merge_request.find_terraform_reports)
+ end
+
def exposed_artifacts
if @merge_request.has_exposed_artifacts?
reports_response(@merge_request.find_exposed_artifacts)
@@ -353,7 +371,6 @@ class Projects::MergeRequestsController < Projects::MergeRequests::ApplicationCo
def define_edit_vars
@source_project = @merge_request.source_project
@target_project = @merge_request.target_project
- @target_branches = @merge_request.target_project.repository.branch_names
@noteable = @merge_request
# FIXME: We have to assign a presenter to another instance variable
diff --git a/app/controllers/projects/pipelines_controller.rb b/app/controllers/projects/pipelines_controller.rb
index 726ce8974c7..678d0862f48 100644
--- a/app/controllers/projects/pipelines_controller.rb
+++ b/app/controllers/projects/pipelines_controller.rb
@@ -11,7 +11,9 @@ class Projects::PipelinesController < Projects::ApplicationController
before_action :authorize_create_pipeline!, only: [:new, :create]
before_action :authorize_update_pipeline!, only: [:retry, :cancel]
before_action do
- push_frontend_feature_flag(:junit_pipeline_view)
+ push_frontend_feature_flag(:junit_pipeline_view, project)
+ push_frontend_feature_flag(:filter_pipelines_search, default_enabled: true)
+ push_frontend_feature_flag(:dag_pipeline_tab)
end
before_action :ensure_pipeline, only: [:show]
@@ -22,9 +24,8 @@ class Projects::PipelinesController < Projects::ApplicationController
POLLING_INTERVAL = 10_000
def index
- @scope = params[:scope]
@pipelines = Ci::PipelinesFinder
- .new(project, current_user, scope: @scope)
+ .new(project, current_user, index_params)
.execute
.page(params[:page])
.per(30)
@@ -69,6 +70,8 @@ class Projects::PipelinesController < Projects::ApplicationController
end
def show
+ Gitlab::QueryLimiting.whitelist('https://gitlab.com/gitlab-org/gitlab/-/issues/26657')
+
respond_to do |format|
format.html
format.json do
@@ -91,6 +94,10 @@ class Projects::PipelinesController < Projects::ApplicationController
render_show
end
+ def dag
+ render_show
+ end
+
def failures
if @pipeline.failed_builds.present?
render_show
@@ -169,19 +176,9 @@ class Projects::PipelinesController < Projects::ApplicationController
end
format.json do
- if pipeline_test_report == :error
- render json: { status: :error_parsing_report }
- else
- test_reports = if params[:scope] == "with_attachment"
- pipeline_test_report.with_attachment!
- else
- pipeline_test_report
- end
-
- render json: TestReportSerializer
- .new(current_user: @current_user)
- .represent(test_reports, project: project)
- end
+ render json: TestReportSerializer
+ .new(current_user: @current_user)
+ .represent(pipeline_test_report, project: project)
end
end
end
@@ -189,11 +186,7 @@ class Projects::PipelinesController < Projects::ApplicationController
def test_reports_count
return unless Feature.enabled?(:junit_pipeline_view, project)
- begin
- render json: { total_count: pipeline.test_reports_count }.to_json
- rescue Gitlab::Ci::Parsers::ParserError
- render json: { total_count: 0 }.to_json
- end
+ render json: { total_count: pipeline.test_reports_count }.to_json
end
private
@@ -262,18 +255,22 @@ class Projects::PipelinesController < Projects::ApplicationController
end
def limited_pipelines_count(project, scope = nil)
- finder = Ci::PipelinesFinder.new(project, current_user, scope: scope)
+ finder = Ci::PipelinesFinder.new(project, current_user, index_params.merge(scope: scope))
view_context.limited_counter_with_delimiter(finder.execute)
end
def pipeline_test_report
strong_memoize(:pipeline_test_report) do
- @pipeline.test_reports
- rescue Gitlab::Ci::Parsers::ParserError
- :error
+ @pipeline.test_reports.tap do |reports|
+ reports.with_attachment! if params[:scope] == 'with_attachment'
+ end
end
end
+
+ def index_params
+ params.permit(:scope, :username, :ref)
+ end
end
Projects::PipelinesController.prepend_if_ee('EE::Projects::PipelinesController')
diff --git a/app/controllers/projects/project_members_controller.rb b/app/controllers/projects/project_members_controller.rb
index 109c8b7005f..3e52248f292 100644
--- a/app/controllers/projects/project_members_controller.rb
+++ b/app/controllers/projects/project_members_controller.rb
@@ -17,8 +17,9 @@ class Projects::ProjectMembersController < Projects::ApplicationController
@group_links = @project.project_group_links
@group_links = @group_links.search(params[:search]) if params[:search].present?
- @project_members = MembersFinder.new(@project, current_user)
- .execute(include_relations: requested_relations, params: params.merge(sort: @sort))
+ @project_members = MembersFinder
+ .new(@project, current_user, params: filter_params)
+ .execute(include_relations: requested_relations)
@project_members = present_members(@project_members.page(params[:page]))
@@ -43,12 +44,17 @@ class Projects::ProjectMembersController < Projects::ApplicationController
return render_404
end
- redirect_to(project_project_members_path(project),
- notice: notice)
+ redirect_to(project_project_members_path(project), notice: notice)
end
# MembershipActions concern
alias_method :membershipable, :project
+
+ private
+
+ def filter_params
+ params.permit(:search).merge(sort: @sort)
+ end
end
Projects::ProjectMembersController.prepend_if_ee('EE::Projects::ProjectMembersController')
diff --git a/app/controllers/projects/refs_controller.rb b/app/controllers/projects/refs_controller.rb
index 7c606bd8c45..fcbeb5c840c 100644
--- a/app/controllers/projects/refs_controller.rb
+++ b/app/controllers/projects/refs_controller.rb
@@ -12,7 +12,7 @@ class Projects::RefsController < Projects::ApplicationController
before_action :authorize_download_code!
before_action only: [:logs_tree] do
- push_frontend_feature_flag(:vue_file_list_lfs_badge)
+ push_frontend_feature_flag(:vue_file_list_lfs_badge, default_enabled: true)
end
def switch
@@ -44,30 +44,25 @@ class Projects::RefsController < Projects::ApplicationController
end
def logs_tree
- summary = ::Gitlab::TreeSummary.new(
- @commit,
- @project,
- path: @path,
- offset: params[:offset],
- limit: 25
- )
-
- @logs, commits = summary.summarize
- @more_log_url = more_url(summary.next_offset) if summary.more?
+ tree_summary = ::Gitlab::TreeSummary.new(
+ @commit, @project, current_user,
+ path: @path, offset: params[:offset], limit: 25)
respond_to do |format|
format.html { render_404 }
format.json do
- response.headers["More-Logs-Url"] = @more_log_url if summary.more?
- response.headers["More-Logs-Offset"] = summary.next_offset if summary.more?
- render json: @logs
+ logs, next_offset = tree_summary.fetch_logs
+
+ response.headers["More-Logs-Offset"] = next_offset if next_offset
+
+ render json: logs
end
- # The commit titles must be rendered and redacted before being shown.
- # Doing it here allows us to apply performance optimizations that avoid
- # N+1 problems
+ # Deprecated due to https://gitlab.com/gitlab-org/gitlab/-/issues/36863
+ # Will be removed soon https://gitlab.com/gitlab-org/gitlab/-/merge_requests/29895
format.js do
- prerender_commit_full_titles!(commits)
+ @logs, _ = tree_summary.summarize
+ @more_log_url = more_url(tree_summary.next_offset) if tree_summary.more?
end
end
end
@@ -78,14 +73,6 @@ class Projects::RefsController < Projects::ApplicationController
logs_file_project_ref_path(@project, @ref, @path, offset: offset)
end
- def prerender_commit_full_titles!(commits)
- # Preload commit authors as they are used in rendering
- commits.each(&:lazy_author)
-
- renderer = Banzai::ObjectRenderer.new(user: current_user, default_project: @project)
- renderer.render(commits, :full_title)
- end
-
def validate_ref_id
return not_found! if params[:id].present? && params[:id] !~ Gitlab::PathRegex.git_reference_regex
end
diff --git a/app/controllers/projects/registry/repositories_controller.rb b/app/controllers/projects/registry/repositories_controller.rb
index 2418ea97409..19d0cb9acdc 100644
--- a/app/controllers/projects/registry/repositories_controller.rb
+++ b/app/controllers/projects/registry/repositories_controller.rb
@@ -10,7 +10,8 @@ module Projects
respond_to do |format|
format.html
format.json do
- @images = project.container_repositories
+ @images = ContainerRepositoriesFinder.new(user: current_user, subject: project, params: params.slice(:name))
+ .execute
track_event(:list_repositories)
diff --git a/app/controllers/projects/settings/access_tokens_controller.rb b/app/controllers/projects/settings/access_tokens_controller.rb
new file mode 100644
index 00000000000..d6b4c4dd5dc
--- /dev/null
+++ b/app/controllers/projects/settings/access_tokens_controller.rb
@@ -0,0 +1,71 @@
+# frozen_string_literal: true
+
+module Projects
+ module Settings
+ class AccessTokensController < Projects::ApplicationController
+ include ProjectsHelper
+
+ before_action :check_feature_availability
+
+ def index
+ @project_access_token = PersonalAccessToken.new
+ set_index_vars
+ end
+
+ def create
+ token_response = ResourceAccessTokens::CreateService.new(current_user, @project, create_params).execute
+
+ if token_response.success?
+ @project_access_token = token_response.payload[:access_token]
+ PersonalAccessToken.redis_store!(key_identity, @project_access_token.token)
+
+ redirect_to namespace_project_settings_access_tokens_path, notice: _("Your new project access token has been created.")
+ else
+ render :index
+ end
+ end
+
+ def revoke
+ @project_access_token = finder.find(params[:id])
+ revoked_response = ResourceAccessTokens::RevokeService.new(current_user, @project, @project_access_token).execute
+
+ if revoked_response.success?
+ flash[:notice] = _("Revoked project access token %{project_access_token_name}!") % { project_access_token_name: @project_access_token.name }
+ else
+ flash[:alert] = _("Could not revoke project access token %{project_access_token_name}.") % { project_access_token_name: @project_access_token.name }
+ end
+
+ redirect_to namespace_project_settings_access_tokens_path
+ end
+
+ private
+
+ def check_feature_availability
+ render_404 unless project_access_token_available?(@project)
+ end
+
+ def create_params
+ params.require(:project_access_token).permit(:name, :expires_at, scopes: [])
+ end
+
+ def set_index_vars
+ @scopes = Gitlab::Auth.resource_bot_scopes
+ @active_project_access_tokens = finder(state: 'active').execute
+ @inactive_project_access_tokens = finder(state: 'inactive', sort: 'expires_at_asc').execute
+ @new_project_access_token = PersonalAccessToken.redis_getdel(key_identity)
+ end
+
+ def finder(options = {})
+ PersonalAccessTokensFinder.new({ user: bot_users, impersonation: false }.merge(options))
+ end
+
+ def bot_users
+ @project.bots
+ end
+
+ def key_identity
+ "#{current_user.id}:#{@project.id}"
+ end
+ end
+ end
+end
diff --git a/app/controllers/projects/settings/repository_controller.rb b/app/controllers/projects/settings/repository_controller.rb
index 0aa55dcc5b9..35ca9336613 100644
--- a/app/controllers/projects/settings/repository_controller.rb
+++ b/app/controllers/projects/settings/repository_controller.rb
@@ -89,7 +89,7 @@ module Projects
end
def deploy_token_params
- params.require(:deploy_token).permit(:name, :expires_at, :read_repository, :read_registry, :write_registry, :username)
+ params.require(:deploy_token).permit(:name, :expires_at, :read_repository, :read_registry, :write_registry, :read_package_registry, :write_package_registry, :username)
end
def access_levels_options
diff --git a/app/controllers/projects/snippets_controller.rb b/app/controllers/projects/snippets_controller.rb
index da0e3a44f05..9233f063f55 100644
--- a/app/controllers/projects/snippets_controller.rb
+++ b/app/controllers/projects/snippets_controller.rb
@@ -52,15 +52,8 @@ class Projects::SnippetsController < Projects::ApplicationController
create_params = snippet_params.merge(spammable_params)
service_response = Snippets::CreateService.new(project, current_user, create_params).execute
@snippet = service_response.payload[:snippet]
- repository_operation_error = service_response.error? && !@snippet.persisted? && @snippet.valid?
- if repository_operation_error
- flash.now[:alert] = service_response.message
-
- render :new
- else
- recaptcha_check_with_fallback { render :new }
- end
+ handle_repository_error(:new)
end
def update
@@ -69,7 +62,7 @@ class Projects::SnippetsController < Projects::ApplicationController
service_response = Snippets::UpdateService.new(project, current_user, update_params).execute(@snippet)
@snippet = service_response.payload[:snippet]
- check_repository_error
+ handle_repository_error(:edit)
end
def show
diff --git a/app/controllers/projects/tree_controller.rb b/app/controllers/projects/tree_controller.rb
index b8fe2a47b30..9cb345724cc 100644
--- a/app/controllers/projects/tree_controller.rb
+++ b/app/controllers/projects/tree_controller.rb
@@ -16,7 +16,7 @@ class Projects::TreeController < Projects::ApplicationController
before_action :authorize_edit_tree!, only: [:create_dir]
before_action only: [:show] do
- push_frontend_feature_flag(:vue_file_list_lfs_badge)
+ push_frontend_feature_flag(:vue_file_list_lfs_badge, default_enabled: true)
end
def show
diff --git a/app/controllers/projects/usage_ping_controller.rb b/app/controllers/projects/usage_ping_controller.rb
index ebdf28bd59c..0e2c8f879b1 100644
--- a/app/controllers/projects/usage_ping_controller.rb
+++ b/app/controllers/projects/usage_ping_controller.rb
@@ -10,4 +10,10 @@ class Projects::UsagePingController < Projects::ApplicationController
head(200)
end
+
+ def web_ide_pipelines_count
+ Gitlab::UsageDataCounters::WebIdeCounter.increment_pipelines_count
+
+ head(200)
+ end
end
diff --git a/app/controllers/projects/wikis_controller.rb b/app/controllers/projects/wikis_controller.rb
index 90ff798077a..508b1f5bd0a 100644
--- a/app/controllers/projects/wikis_controller.rb
+++ b/app/controllers/projects/wikis_controller.rb
@@ -10,8 +10,9 @@ class Projects::WikisController < Projects::ApplicationController
before_action :authorize_admin_wiki!, only: :destroy
before_action :load_project_wiki
before_action :load_page, only: [:show, :edit, :update, :history, :destroy]
- before_action :valid_encoding?,
- if: -> { %w[show edit update].include?(action_name) && load_page }
+ before_action only: [:show, :edit, :update] do
+ @valid_encoding = valid_encoding?
+ end
before_action only: [:edit, :update], unless: :valid_encoding? do
redirect_to(project_wiki_path(@project, @page))
end
@@ -64,7 +65,7 @@ class Projects::WikisController < Projects::ApplicationController
def update
return render('empty') unless can?(current_user, :create_wiki, @project)
- @page = WikiPages::UpdateService.new(@project, current_user, wiki_params).execute(@page)
+ @page = WikiPages::UpdateService.new(container: @project, current_user: current_user, params: wiki_params).execute(@page)
if @page.valid?
redirect_to(
@@ -80,7 +81,7 @@ class Projects::WikisController < Projects::ApplicationController
end
def create
- @page = WikiPages::CreateService.new(@project, current_user, wiki_params).execute
+ @page = WikiPages::CreateService.new(container: @project, current_user: current_user, params: wiki_params).execute
if @page.persisted?
redirect_to(
@@ -111,7 +112,7 @@ class Projects::WikisController < Projects::ApplicationController
end
def destroy
- WikiPages::DestroyService.new(@project, current_user).execute(@page)
+ WikiPages::DestroyService.new(container: @project, current_user: current_user).execute(@page)
redirect_to project_wiki_path(@project, :home),
status: :found,
@@ -144,7 +145,7 @@ class Projects::WikisController < Projects::ApplicationController
@sidebar_page = @project_wiki.find_sidebar(params[:version_id])
unless @sidebar_page # Fallback to default sidebar
- @sidebar_wiki_entries = WikiPage.group_by_directory(@project_wiki.list_pages(limit: 15))
+ @sidebar_wiki_entries, @sidebar_limited = @project_wiki.sidebar_entries
end
rescue ProjectWiki::CouldNotCreateWikiError
flash[:notice] = _("Could not create Wiki Repository at this time. Please try again later.")
@@ -167,7 +168,11 @@ class Projects::WikisController < Projects::ApplicationController
end
def load_page
- @page ||= @project_wiki.find_page(*page_params)
+ @page ||= find_page
+ end
+
+ def find_page
+ @project_wiki.find_page(*page_params)
end
def page_params
@@ -178,9 +183,11 @@ class Projects::WikisController < Projects::ApplicationController
end
def valid_encoding?
- strong_memoize(:valid_encoding) do
- @page.content.encoding == Encoding::UTF_8
- end
+ page_encoding == Encoding::UTF_8
+ end
+
+ def page_encoding
+ strong_memoize(:page_encoding) { @page&.content&.encoding }
end
def set_encoding_error
diff --git a/app/controllers/projects_controller.rb b/app/controllers/projects_controller.rb
index bb20ea1de49..2f86b945b06 100644
--- a/app/controllers/projects_controller.rb
+++ b/app/controllers/projects_controller.rb
@@ -36,10 +36,6 @@ class ProjectsController < Projects::ApplicationController
layout :determine_layout
- before_action do
- push_frontend_feature_flag(:metrics_dashboard_visibility_switching_available)
- end
-
def index
redirect_to(current_user ? root_path : explore_root_path)
end
@@ -62,7 +58,7 @@ class ProjectsController < Projects::ApplicationController
@project = ::Projects::CreateService.new(current_user, project_params(attributes: project_params_create_attributes)).execute
if @project.saved?
- cookies[:issue_board_welcome_hidden] = { path: project_path(@project), value: nil, expires: Time.at(0) }
+ cookies[:issue_board_welcome_hidden] = { path: project_path(@project), value: nil, expires: Time.zone.at(0) }
redirect_to(
project_path(@project, custom_import_params),
@@ -205,7 +201,7 @@ class ProjectsController < Projects::ApplicationController
redirect_to(
edit_project_path(@project, anchor: 'js-export-project'),
- notice: _("Project export started. A download link will be sent by email.")
+ notice: _("Project export started. A download link will be sent by email and made available on this page.")
)
end
@@ -403,6 +399,10 @@ class ProjectsController < Projects::ApplicationController
snippets_access_level
wiki_access_level
pages_access_level
+ metrics_dashboard_access_level
+ ],
+ project_setting_attributes: %i[
+ show_default_award_emojis
]
]
end
diff --git a/app/controllers/registrations_controller.rb b/app/controllers/registrations_controller.rb
index a6c5a6d8526..ffbccbb01f2 100644
--- a/app/controllers/registrations_controller.rb
+++ b/app/controllers/registrations_controller.rb
@@ -8,7 +8,7 @@ class RegistrationsController < Devise::RegistrationsController
layout :choose_layout
- skip_before_action :required_signup_info, only: [:welcome, :update_registration]
+ skip_before_action :required_signup_info, :check_two_factor_requirement, only: [:welcome, :update_registration]
prepend_before_action :check_captcha, only: :create
before_action :whitelist_query_limiting, only: [:destroy]
before_action :ensure_terms_accepted,
@@ -137,7 +137,6 @@ class RegistrationsController < Devise::RegistrationsController
def check_captcha
ensure_correct_params!
- return unless Feature.enabled?(:registrations_recaptcha, default_enabled: true) # reCAPTCHA on the UI will still display however
return unless show_recaptcha_sign_up?
return unless Gitlab::Recaptcha.load_configurations!
diff --git a/app/controllers/repositories/git_http_controller.rb b/app/controllers/repositories/git_http_controller.rb
index 118036de230..e3dbe6fcbdf 100644
--- a/app/controllers/repositories/git_http_controller.rb
+++ b/app/controllers/repositories/git_http_controller.rb
@@ -4,7 +4,6 @@ module Repositories
class GitHttpController < Repositories::GitHttpClientController
include WorkhorseRequest
- before_action :snippet_request_allowed?
before_action :access_check
prepend_before_action :deny_head_requests, only: [:info_refs]
@@ -121,13 +120,6 @@ module Repositories
def log_user_activity
Users::ActivityService.new(user).execute
end
-
- def snippet_request_allowed?
- if repo_type.snippet? && Feature.disabled?(:version_snippets, user)
- Gitlab::AppLogger.info('Snippet access attempt with feature disabled')
- render plain: 'Snippet git access is disabled.', status: :forbidden
- end
- end
end
end
diff --git a/app/controllers/search_controller.rb b/app/controllers/search_controller.rb
index d1e15a72350..04d2b3068da 100644
--- a/app/controllers/search_controller.rb
+++ b/app/controllers/search_controller.rb
@@ -5,7 +5,6 @@ class SearchController < ApplicationController
include SearchHelper
include RendersCommits
- before_action :override_snippet_scope, only: :show
around_action :allow_gitaly_ref_name_caching
skip_before_action :authenticate_user!
@@ -104,14 +103,4 @@ class SearchController < ApplicationController
Gitlab::UsageDataCounters::SearchCounter.increment_navbar_searches_count
end
-
- # Disallow web snippet_blobs search as we migrate snippet
- # from database-backed storage to git repository-based,
- # and searching across multiple git repositories is not feasible.
- #
- # TODO: after 13.0 refactor this into Search::SnippetService
- # See https://gitlab.com/gitlab-org/gitlab/issues/208882
- def override_snippet_scope
- params[:scope] = 'snippet_titles' if params[:snippets] == 'true'
- end
end
diff --git a/app/controllers/sessions_controller.rb b/app/controllers/sessions_controller.rb
index 2c87c3c890f..9e8075d4bcc 100644
--- a/app/controllers/sessions_controller.rb
+++ b/app/controllers/sessions_controller.rb
@@ -6,6 +6,8 @@ class SessionsController < Devise::SessionsController
include Devise::Controllers::Rememberable
include Recaptcha::ClientHelper
include Recaptcha::Verify
+ include RendersLdapServers
+ include KnownSignIn
skip_before_action :check_two_factor_requirement, only: [:destroy]
# replaced with :require_no_authentication_without_flash
@@ -16,7 +18,6 @@ class SessionsController < Devise::SessionsController
if: -> { action_name == 'create' && two_factor_enabled? }
prepend_before_action :check_captcha, only: [:create]
prepend_before_action :store_redirect_uri, only: [:new]
- prepend_before_action :ldap_servers, only: [:new, :create]
prepend_before_action :require_no_authentication_without_flash, only: [:new, :create]
prepend_before_action :ensure_password_authentication_enabled!, if: -> { action_name == 'create' && password_based_login? }
@@ -27,6 +28,7 @@ class SessionsController < Devise::SessionsController
before_action :frontend_tracking_data, only: [:new]
after_action :log_failed_login, if: :action_new_and_failed_login?
+ after_action :verify_known_sign_in, only: [:create]
helper_method :captcha_enabled?, :captcha_on_login_required?
@@ -269,16 +271,6 @@ class SessionsController < Devise::SessionsController
Gitlab::Recaptcha.load_configurations!
end
- def ldap_servers
- @ldap_servers ||= begin
- if Gitlab::Auth::Ldap::Config.sign_in_enabled?
- Gitlab::Auth::Ldap::Config.available_servers
- else
- []
- end
- end
- end
-
def unverified_anonymous_user?
exceeded_failed_login_attempts? || exceeded_anonymous_sessions?
end
diff --git a/app/controllers/snippets_controller.rb b/app/controllers/snippets_controller.rb
index a07baa1a045..425e0458b41 100644
--- a/app/controllers/snippets_controller.rb
+++ b/app/controllers/snippets_controller.rb
@@ -49,29 +49,22 @@ class SnippetsController < ApplicationController
end
def create
- create_params = snippet_params.merge(spammable_params)
+ create_params = snippet_params.merge(files: params.delete(:files))
service_response = Snippets::CreateService.new(nil, current_user, create_params).execute
@snippet = service_response.payload[:snippet]
- repository_operation_error = service_response.error? && !@snippet.persisted? && @snippet.valid?
- if repository_operation_error
- flash.now[:alert] = service_response.message
-
- render :new
+ if service_response.error? && @snippet.errors[:repository].present?
+ handle_repository_error(:new)
else
- move_temporary_files if @snippet.valid? && params[:files]
-
recaptcha_check_with_fallback { render :new }
end
end
def update
- update_params = snippet_params.merge(spammable_params)
-
- service_response = Snippets::UpdateService.new(nil, current_user, update_params).execute(@snippet)
+ service_response = Snippets::UpdateService.new(nil, current_user, snippet_params).execute(@snippet)
@snippet = service_response.payload[:snippet]
- check_repository_error
+ handle_repository_error(:edit)
end
def show
@@ -153,12 +146,6 @@ class SnippetsController < ApplicationController
end
def snippet_params
- params.require(:personal_snippet).permit(:title, :content, :file_name, :private, :visibility_level, :description)
- end
-
- def move_temporary_files
- params[:files].each do |file|
- FileMover.new(file, from_model: current_user, to_model: @snippet).execute
- end
+ params.require(:personal_snippet).permit(:title, :content, :file_name, :private, :visibility_level, :description).merge(spammable_params)
end
end
diff --git a/app/controllers/user_callouts_controller.rb b/app/controllers/user_callouts_controller.rb
index 4ee75218db1..06f422b9d90 100644
--- a/app/controllers/user_callouts_controller.rb
+++ b/app/controllers/user_callouts_controller.rb
@@ -5,7 +5,7 @@ class UserCalloutsController < ApplicationController
callout = ensure_callout
if callout.persisted?
- callout.update(dismissed_at: Time.now)
+ callout.update(dismissed_at: Time.current)
respond_to do |format|
format.json { head :ok }
end
diff --git a/app/finders/alert_management/alerts_finder.rb b/app/finders/alert_management/alerts_finder.rb
new file mode 100644
index 00000000000..cb35be43c15
--- /dev/null
+++ b/app/finders/alert_management/alerts_finder.rb
@@ -0,0 +1,55 @@
+# frozen_string_literal: true
+
+module AlertManagement
+ class AlertsFinder
+ # @return [Hash<Integer,Integer>] Mapping of status id to count
+ # ex) { 0: 6, ...etc }
+ def self.counts_by_status(current_user, project, params = {})
+ new(current_user, project, params).execute.counts_by_status
+ end
+
+ def initialize(current_user, project, params)
+ @current_user = current_user
+ @project = project
+ @params = params
+ end
+
+ def execute
+ return AlertManagement::Alert.none unless authorized?
+
+ collection = project.alert_management_alerts
+ collection = by_status(collection)
+ collection = by_search(collection)
+ collection = by_iid(collection)
+ sort(collection)
+ end
+
+ private
+
+ attr_reader :current_user, :project, :params
+
+ def by_iid(collection)
+ return collection unless params[:iid]
+
+ collection.for_iid(params[:iid])
+ end
+
+ def by_status(collection)
+ values = AlertManagement::Alert::STATUSES.values & Array(params[:status])
+
+ values.present? ? collection.for_status(values) : collection
+ end
+
+ def by_search(collection)
+ params[:search].present? ? collection.search(params[:search]) : collection
+ end
+
+ def sort(collection)
+ params[:sort] ? collection.sort_by_attribute(params[:sort]) : collection
+ end
+
+ def authorized?
+ Ability.allowed?(current_user, :read_alert_management_alert, project)
+ end
+ end
+end
diff --git a/app/finders/artifacts_finder.rb b/app/finders/artifacts_finder.rb
deleted file mode 100644
index 81c5168d782..00000000000
--- a/app/finders/artifacts_finder.rb
+++ /dev/null
@@ -1,24 +0,0 @@
-# frozen_string_literal: true
-
-class ArtifactsFinder
- def initialize(project, params = {})
- @project = project
- @params = params
- end
-
- def execute
- artifacts = @project.job_artifacts
-
- sort(artifacts)
- end
-
- private
-
- def sort_key
- @params[:sort] || 'created_desc'
- end
-
- def sort(artifacts)
- artifacts.order_by(sort_key)
- end
-end
diff --git a/app/finders/ci/daily_build_group_report_results_finder.rb b/app/finders/ci/daily_build_group_report_results_finder.rb
new file mode 100644
index 00000000000..3c3c24c1479
--- /dev/null
+++ b/app/finders/ci/daily_build_group_report_results_finder.rb
@@ -0,0 +1,37 @@
+# frozen_string_literal: true
+
+module Ci
+ class DailyBuildGroupReportResultsFinder
+ include Gitlab::Allowable
+
+ def initialize(current_user:, project:, ref_path:, start_date:, end_date:, limit: nil)
+ @current_user = current_user
+ @project = project
+ @ref_path = ref_path
+ @start_date = start_date
+ @end_date = end_date
+ @limit = limit
+ end
+
+ def execute
+ return none unless can?(current_user, :download_code, project)
+
+ Ci::DailyBuildGroupReportResult.recent_results(
+ {
+ project_id: project,
+ ref_path: ref_path,
+ date: start_date..end_date
+ },
+ limit: @limit
+ )
+ end
+
+ private
+
+ attr_reader :current_user, :project, :ref_path, :start_date, :end_date
+
+ def none
+ Ci::DailyBuildGroupReportResult.none
+ end
+ end
+end
diff --git a/app/finders/ci/job_artifacts_finder.rb b/app/finders/ci/job_artifacts_finder.rb
new file mode 100644
index 00000000000..808c159ced1
--- /dev/null
+++ b/app/finders/ci/job_artifacts_finder.rb
@@ -0,0 +1,26 @@
+# frozen_string_literal: true
+
+module Ci
+ class JobArtifactsFinder
+ def initialize(project, params = {})
+ @project = project
+ @params = params
+ end
+
+ def execute
+ artifacts = @project.job_artifacts
+
+ sort(artifacts)
+ end
+
+ private
+
+ def sort_key
+ @params[:sort] || 'created_desc'
+ end
+
+ def sort(artifacts)
+ artifacts.order_by(sort_key)
+ end
+ end
+end
diff --git a/app/finders/clusters/knative_services_finder.rb b/app/finders/clusters/knative_services_finder.rb
index 71cebe4495e..af8c42f672f 100644
--- a/app/finders/clusters/knative_services_finder.rb
+++ b/app/finders/clusters/knative_services_finder.rb
@@ -11,6 +11,7 @@ module Clusters
}.freeze
self.reactive_cache_key = ->(finder) { finder.model_name }
+ self.reactive_cache_work_type = :external_dependency
self.reactive_cache_worker_finder = ->(_id, *cache_args) { from_cache(*cache_args) }
attr_reader :cluster, :environment
diff --git a/app/finders/container_repositories_finder.rb b/app/finders/container_repositories_finder.rb
index 34921df840b..5109efb361b 100644
--- a/app/finders/container_repositories_finder.rb
+++ b/app/finders/container_repositories_finder.rb
@@ -3,17 +3,18 @@
class ContainerRepositoriesFinder
VALID_SUBJECTS = [Group, Project].freeze
- def initialize(user:, subject:)
+ def initialize(user:, subject:, params: {})
@user = user
@subject = subject
+ @params = params
end
def execute
raise ArgumentError, "invalid subject_type" unless valid_subject_type?
return unless authorized?
- return project_repositories if @subject.is_a?(Project)
- return group_repositories if @subject.is_a?(Group)
+ repositories = @subject.is_a?(Project) ? project_repositories : group_repositories
+ filter_by_image_name(repositories)
end
private
@@ -32,6 +33,12 @@ class ContainerRepositoriesFinder
ContainerRepository.for_group_and_its_subgroups(@subject)
end
+ def filter_by_image_name(repositories)
+ return repositories unless @params[:name]
+
+ repositories.search_by_name(@params[:name])
+ end
+
def authorized?
Ability.allowed?(@user, :read_container_image, @subject)
end
diff --git a/app/finders/design_management/designs_finder.rb b/app/finders/design_management/designs_finder.rb
new file mode 100644
index 00000000000..10f95520d1e
--- /dev/null
+++ b/app/finders/design_management/designs_finder.rb
@@ -0,0 +1,57 @@
+# frozen_string_literal: true
+
+module DesignManagement
+ class DesignsFinder
+ include Gitlab::Allowable
+
+ # Params:
+ # ids: integer[]
+ # filenames: string[]
+ # visible_at_version: ?version
+ # filenames: String[]
+ def initialize(issue, current_user, params = {})
+ @issue = issue
+ @current_user = current_user
+ @params = params
+ end
+
+ def execute
+ items = init_collection
+
+ items = by_visible_at_version(items)
+ items = by_filename(items)
+ items = by_id(items)
+
+ items
+ end
+
+ private
+
+ attr_reader :issue, :current_user, :params
+
+ def init_collection
+ return ::DesignManagement::Design.none unless can?(current_user, :read_design, issue)
+
+ issue.designs
+ end
+
+ # Returns all designs that existed at a particular design version
+ def by_visible_at_version(items)
+ items.visible_at_version(params[:visible_at_version])
+ end
+
+ def by_filename(items)
+ return items if params[:filenames].nil?
+ return ::DesignManagement::Design.none if params[:filenames].empty?
+
+ items.with_filename(params[:filenames])
+ end
+
+ def by_id(items)
+ return items if params[:ids].nil?
+ return ::DesignManagement::Design.none if params[:ids].empty?
+
+ items.id_in(params[:ids])
+ end
+ end
+end
diff --git a/app/finders/design_management/versions_finder.rb b/app/finders/design_management/versions_finder.rb
new file mode 100644
index 00000000000..c4aefd3078e
--- /dev/null
+++ b/app/finders/design_management/versions_finder.rb
@@ -0,0 +1,58 @@
+# frozen_string_literal: true
+
+module DesignManagement
+ class VersionsFinder
+ attr_reader :design_or_collection, :current_user, :params
+
+ # The `design_or_collection` argument should be either a:
+ #
+ # - DesignManagement::Design, or
+ # - DesignManagement::DesignCollection
+ #
+ # The object will have `#versions` called on it to set up the
+ # initial scope of the versions.
+ #
+ # valid params:
+ # - earlier_or_equal_to: Version
+ # - sha: String
+ # - version_id: Integer
+ #
+ def initialize(design_or_collection, current_user, params = {})
+ @design_or_collection = design_or_collection
+ @current_user = current_user
+ @params = params
+ end
+
+ def execute
+ unless Ability.allowed?(current_user, :read_design, design_or_collection)
+ return ::DesignManagement::Version.none
+ end
+
+ items = design_or_collection.versions
+ items = by_earlier_or_equal_to(items)
+ items = by_sha(items)
+ items = by_version_id(items)
+ items.ordered
+ end
+
+ private
+
+ def by_earlier_or_equal_to(items)
+ return items unless params[:earlier_or_equal_to]
+
+ items.earlier_or_equal_to(params[:earlier_or_equal_to])
+ end
+
+ def by_version_id(items)
+ return items unless params[:version_id]
+
+ items.id_in(params[:version_id])
+ end
+
+ def by_sha(items)
+ return items unless params[:sha]
+
+ items.by_sha(params[:sha])
+ end
+ end
+end
diff --git a/app/finders/freeze_periods_finder.rb b/app/finders/freeze_periods_finder.rb
new file mode 100644
index 00000000000..2a9bfbe12ba
--- /dev/null
+++ b/app/finders/freeze_periods_finder.rb
@@ -0,0 +1,14 @@
+# frozen_string_literal: true
+
+class FreezePeriodsFinder
+ def initialize(project, current_user = nil)
+ @project = project
+ @current_user = current_user
+ end
+
+ def execute
+ return Ci::FreezePeriod.none unless Ability.allowed?(@current_user, :read_freeze_period, @project)
+
+ @project.freeze_periods
+ end
+end
diff --git a/app/finders/group_members_finder.rb b/app/finders/group_members_finder.rb
index a56d4ebb368..949af103eb3 100644
--- a/app/finders/group_members_finder.rb
+++ b/app/finders/group_members_finder.rb
@@ -9,7 +9,6 @@ class GroupMembersFinder < UnionFinder
# search: string
# created_after: datetime
# created_before: datetime
-
attr_reader :params
def initialize(group, user = nil, params: {})
@@ -22,7 +21,6 @@ class GroupMembersFinder < UnionFinder
def execute(include_relations: [:inherited, :direct])
group_members = group.members
relations = []
- @params = params
return group_members if include_relations == [:direct]
diff --git a/app/finders/issuable_finder.rb b/app/finders/issuable_finder.rb
index 5687b375cf0..7014f2ec205 100644
--- a/app/finders/issuable_finder.rb
+++ b/app/finders/issuable_finder.rb
@@ -40,7 +40,7 @@ class IssuableFinder
requires_cross_project_access unless: -> { params.project? }
- NEGATABLE_PARAMS_HELPER_KEYS = %i[include_subgroups in].freeze
+ NEGATABLE_PARAMS_HELPER_KEYS = %i[project_id scope status include_subgroups].freeze
attr_accessor :current_user, :params
@@ -68,7 +68,7 @@ class IssuableFinder
# This should not be used in controller strong params!
def negatable_scalar_params
- @negatable_scalar_params ||= scalar_params + %i[project_id group_id]
+ @negatable_scalar_params ||= scalar_params - %i[search in]
end
# This should not be used in controller strong params!
@@ -100,7 +100,7 @@ class IssuableFinder
items = filter_items(items)
# Let's see if we have to negate anything
- items = by_negation(items)
+ items = filter_negated_items(items)
# This has to be last as we use a CTE as an optimization fence
# for counts by passing the force_cte param and enabling the
@@ -132,6 +132,22 @@ class IssuableFinder
by_my_reaction_emoji(items)
end
+ # Negates all params found in `negatable_params`
+ def filter_negated_items(items)
+ return items unless Feature.enabled?(:not_issuable_queries, params.group || params.project, default_enabled: true)
+
+ # API endpoints send in `nil` values so we test if there are any non-nil
+ return items unless not_params.present? && not_params.values.any?
+
+ items = by_negated_author(items)
+ items = by_negated_assignee(items)
+ items = by_negated_label(items)
+ items = by_negated_milestone(items)
+ items = by_negated_release(items)
+ items = by_negated_my_reaction_emoji(items)
+ by_negated_iids(items)
+ end
+
def row_count
Gitlab::IssuablesCountForState.new(self).for_state_or_opened(params[:state])
end
@@ -189,6 +205,21 @@ class IssuableFinder
private
+ def not_params
+ strong_memoize(:not_params) do
+ params_class.new(params[:not].dup, current_user, klass).tap do |not_params|
+ next unless not_params.present?
+
+ # These are "helper" params that modify the results, like :in and :search. They usually come in at the top-level
+ # params, but if they do come in inside the `:not` params, the inner ones should take precedence.
+ not_helpers = params.slice(*NEGATABLE_PARAMS_HELPER_KEYS).merge(params[:not].slice(*NEGATABLE_PARAMS_HELPER_KEYS))
+ not_helpers.each do |key, value|
+ not_params[key] = value unless not_params[key].present?
+ end
+ end
+ end
+ end
+
def force_cte?
!!params[:force_cte]
end
@@ -215,33 +246,6 @@ class IssuableFinder
klass.available_states.key(value)
end
- # Negates all params found in `negatable_params`
- # rubocop: disable CodeReuse/ActiveRecord
- def by_negation(items)
- not_params = params[:not].dup
- # API endpoints send in `nil` values so we test if there are any non-nil
- return items unless not_params.present? && not_params.values.any?
-
- not_params.keep_if { |_k, v| v.present? }.each do |(key, value)|
- # These aren't negatable params themselves, but rather help other searches, so we skip them.
- # They will be added into all the NOT searches.
- next if NEGATABLE_PARAMS_HELPER_KEYS.include?(key.to_sym)
- next unless self.class.negatable_params.include?(key.to_sym)
-
- # These are "helper" params that are required inside the NOT to get the right results. They usually come in
- # at the top-level params, but if they do come in inside the `:not` params, they should take precedence.
- not_helpers = params.slice(*NEGATABLE_PARAMS_HELPER_KEYS).merge(params[:not].slice(*NEGATABLE_PARAMS_HELPER_KEYS))
- not_param = { key => value }.with_indifferent_access.merge(not_helpers).merge(not_query: true)
-
- items_to_negate = self.class.new(current_user, not_param).execute
-
- items = items.where.not(id: items_to_negate)
- end
-
- items
- end
- # rubocop: enable CodeReuse/ActiveRecord
-
# rubocop: disable CodeReuse/ActiveRecord
def by_scope(items)
return items.none if params.current_user_related? && !current_user
@@ -326,6 +330,12 @@ class IssuableFinder
# rubocop: enable CodeReuse/ActiveRecord
# rubocop: disable CodeReuse/ActiveRecord
+ def by_negated_iids(items)
+ not_params[:iids].present? ? items.where.not(iid: not_params[:iids]) : items
+ end
+ # rubocop: enable CodeReuse/ActiveRecord
+
+ # rubocop: disable CodeReuse/ActiveRecord
def sort(items)
# Ensure we always have an explicit sort order (instead of inheriting
# multiple orders when combining ActiveRecord::Relation objects).
@@ -347,9 +357,19 @@ class IssuableFinder
end
# rubocop: enable CodeReuse/ActiveRecord
- def by_assignee(items)
- return items.assigned_to(params.assignees) if not_query? && params.assignees.any?
+ # rubocop: disable CodeReuse/ActiveRecord
+ def by_negated_author(items)
+ if not_params.author
+ items.where.not(author_id: not_params.author.id)
+ elsif not_params.author_id? || not_params.author_username? # author not found
+ items.none
+ else
+ items
+ end
+ end
+ # rubocop: enable CodeReuse/ActiveRecord
+ def by_assignee(items)
if params.filter_by_no_assignee?
items.unassigned
elsif params.filter_by_any_assignee?
@@ -363,6 +383,17 @@ class IssuableFinder
end
end
+ def by_negated_assignee(items)
+ # We want CE users to be able to say "Issues not assigned to either PersonA nor PersonB"
+ if not_params.assignees.present?
+ items.not_assigned_to(not_params.assignees)
+ elsif not_params.assignee_id? || not_params.assignee_username? # assignee not found
+ items.none
+ else
+ items
+ end
+ end
+
# rubocop: disable CodeReuse/ActiveRecord
def by_milestone(items)
return items unless params.milestones?
@@ -382,6 +413,20 @@ class IssuableFinder
end
# rubocop: enable CodeReuse/ActiveRecord
+ # rubocop: disable CodeReuse/ActiveRecord
+ def by_negated_milestone(items)
+ return items unless not_params.milestones?
+
+ if not_params.filter_by_upcoming_milestone?
+ items.joins(:milestone).merge(Milestone.not_upcoming)
+ elsif not_params.filter_by_started_milestone?
+ items.joins(:milestone).merge(Milestone.not_started)
+ else
+ items.without_particular_milestone(not_params[:milestone_title])
+ end
+ end
+ # rubocop: enable CodeReuse/ActiveRecord
+
def by_release(items)
return items unless params.releases?
@@ -394,6 +439,12 @@ class IssuableFinder
end
end
+ def by_negated_release(items)
+ return items unless not_params.releases?
+
+ items.without_particular_release(not_params[:release_tag], not_params[:project_id])
+ end
+
def by_label(items)
return items unless params.labels?
@@ -402,10 +453,16 @@ class IssuableFinder
elsif params.filter_by_any_label?
items.any_label
else
- items.with_label(params.label_names, params[:sort], not_query: not_query?)
+ items.with_label(params.label_names, params[:sort])
end
end
+ def by_negated_label(items)
+ return items unless not_params.labels?
+
+ items.without_particular_labels(not_params.label_names)
+ end
+
def by_my_reaction_emoji(items)
return items unless params[:my_reaction_emoji] && current_user
@@ -418,11 +475,13 @@ class IssuableFinder
end
end
- def by_non_archived(items)
- params[:non_archived].present? ? items.non_archived : items
+ def by_negated_my_reaction_emoji(items)
+ return items unless not_params[:my_reaction_emoji] && current_user
+
+ items.not_awarded(current_user, not_params[:my_reaction_emoji])
end
- def not_query?
- !!params[:not_query]
+ def by_non_archived(items)
+ params[:non_archived].present? ? items.non_archived : items
end
end
diff --git a/app/finders/issuable_finder/params.rb b/app/finders/issuable_finder/params.rb
index 120ef364368..adf9f1ca9d8 100644
--- a/app/finders/issuable_finder/params.rb
+++ b/app/finders/issuable_finder/params.rb
@@ -132,6 +132,8 @@ class IssuableFinder
def project
strong_memoize(:project) do
+ next nil unless params[:project_id].present?
+
project = Project.find(params[:project_id])
project = nil unless Ability.allowed?(current_user, :"read_#{klass.to_ability_name}", project)
diff --git a/app/finders/issues_finder/params.rb b/app/finders/issues_finder/params.rb
index aaeead7c709..cd92b79265d 100644
--- a/app/finders/issues_finder/params.rb
+++ b/app/finders/issues_finder/params.rb
@@ -50,4 +50,4 @@ class IssuesFinder
end
end
-IssuableFinder::Params.prepend_if_ee('EE::IssuesFinder::Params')
+IssuesFinder::Params.prepend_if_ee('EE::IssuesFinder::Params')
diff --git a/app/finders/members_finder.rb b/app/finders/members_finder.rb
index 0617f34dc8c..e08ed737ca6 100644
--- a/app/finders/members_finder.rb
+++ b/app/finders/members_finder.rb
@@ -4,17 +4,19 @@ class MembersFinder
# Params can be any of the following:
# sort: string
# search: string
+ attr_reader :params
- def initialize(project, current_user)
+ def initialize(project, current_user, params: {})
@project = project
- @current_user = current_user
@group = project.group
+ @current_user = current_user
+ @params = params
end
- def execute(include_relations: [:inherited, :direct], params: {})
- members = find_members(include_relations, params)
+ def execute(include_relations: [:inherited, :direct])
+ members = find_members(include_relations)
- filter_members(members, params)
+ filter_members(members)
end
def can?(*args)
@@ -25,7 +27,7 @@ class MembersFinder
attr_reader :project, :current_user, :group
- def find_members(include_relations, params)
+ def find_members(include_relations)
project_members = project.project_members
project_members = project_members.non_invite unless can?(current_user, :admin_project, project)
@@ -39,7 +41,7 @@ class MembersFinder
distinct_union_of_members(union_members)
end
- def filter_members(members, params)
+ def filter_members(members)
members = members.search(params[:search]) if params[:search].present?
members = members.sort_by_attribute(params[:sort]) if params[:sort].present?
members
diff --git a/app/finders/metrics/users_starred_dashboards_finder.rb b/app/finders/metrics/users_starred_dashboards_finder.rb
new file mode 100644
index 00000000000..7244c51f9a7
--- /dev/null
+++ b/app/finders/metrics/users_starred_dashboards_finder.rb
@@ -0,0 +1,35 @@
+# frozen_string_literal: true
+
+module Metrics
+ class UsersStarredDashboardsFinder
+ def initialize(user:, project:, params: {})
+ @user, @project, @params = user, project, params
+ end
+
+ def execute
+ return ::Metrics::UsersStarredDashboard.none unless Ability.allowed?(user, :read_metrics_user_starred_dashboard, project)
+
+ items = starred_dashboards
+ items = by_project(items)
+ by_dashboard(items)
+ end
+
+ private
+
+ attr_reader :user, :project, :params
+
+ def by_project(items)
+ items.for_project(project)
+ end
+
+ def by_dashboard(items)
+ return items unless params[:dashboard_path]
+
+ items.merge(starred_dashboards.for_project_dashboard(project, params[:dashboard_path]))
+ end
+
+ def starred_dashboards
+ @starred_dashboards ||= user.metrics_users_starred_dashboards
+ end
+ end
+end
diff --git a/app/finders/projects/serverless/functions_finder.rb b/app/finders/projects/serverless/functions_finder.rb
index 3b4ecbb5387..13f84e0e3a5 100644
--- a/app/finders/projects/serverless/functions_finder.rb
+++ b/app/finders/projects/serverless/functions_finder.rb
@@ -9,6 +9,7 @@ module Projects
attr_reader :project
self.reactive_cache_key = ->(finder) { finder.cache_key }
+ self.reactive_cache_work_type = :external_dependency
self.reactive_cache_worker_finder = ->(_id, *args) { from_cache(*args) }
MAX_CLUSTERS = 10
diff --git a/app/finders/projects_finder.rb b/app/finders/projects_finder.rb
index 3a84600b09f..8846ff54eb2 100644
--- a/app/finders/projects_finder.rb
+++ b/app/finders/projects_finder.rb
@@ -151,11 +151,11 @@ class ProjectsFinder < UnionFinder
end
def by_personal(items)
- (params[:personal].present? && current_user) ? items.personal(current_user) : items
+ params[:personal].present? && current_user ? items.personal(current_user) : items
end
def by_starred(items)
- (params[:starred].present? && current_user) ? items.starred_by(current_user) : items
+ params[:starred].present? && current_user ? items.starred_by(current_user) : items
end
def by_trending(items)
diff --git a/app/finders/releases_finder.rb b/app/finders/releases_finder.rb
index e58a90922a5..6a754fdb5a1 100644
--- a/app/finders/releases_finder.rb
+++ b/app/finders/releases_finder.rb
@@ -1,17 +1,31 @@
# frozen_string_literal: true
class ReleasesFinder
- def initialize(project, current_user = nil)
+ attr_reader :project, :current_user, :params
+
+ def initialize(project, current_user = nil, params = {})
@project = project
@current_user = current_user
+ @params = params
end
def execute(preload: true)
- return Release.none unless Ability.allowed?(@current_user, :read_release, @project)
+ return Release.none unless Ability.allowed?(current_user, :read_release, project)
# See https://gitlab.com/gitlab-org/gitlab/-/issues/211988
- releases = @project.releases.where.not(tag: nil) # rubocop:disable CodeReuse/ActiveRecord
+ releases = project.releases.where.not(tag: nil) # rubocop:disable CodeReuse/ActiveRecord
+ releases = by_tag(releases)
releases = releases.preloaded if preload
releases.sorted
end
+
+ private
+
+ # rubocop: disable CodeReuse/ActiveRecord
+ def by_tag(releases)
+ return releases unless params[:tag].present?
+
+ releases.where(tag: params[:tag])
+ end
+ # rubocop: enable CodeReuse/ActiveRecord
end
diff --git a/app/finders/todos_finder.rb b/app/finders/todos_finder.rb
index e56009be33d..672bbd52b07 100644
--- a/app/finders/todos_finder.rb
+++ b/app/finders/todos_finder.rb
@@ -23,7 +23,7 @@ class TodosFinder
NONE = '0'
- TODO_TYPES = Set.new(%w(Issue MergeRequest)).freeze
+ TODO_TYPES = Set.new(%w(Issue MergeRequest DesignManagement::Design)).freeze
attr_accessor :current_user, :params
diff --git a/app/graphql/mutations/alert_management/base.rb b/app/graphql/mutations/alert_management/base.rb
new file mode 100644
index 00000000000..ca2057d4845
--- /dev/null
+++ b/app/graphql/mutations/alert_management/base.rb
@@ -0,0 +1,40 @@
+# frozen_string_literal: true
+
+module Mutations
+ module AlertManagement
+ class Base < BaseMutation
+ include Mutations::ResolvesProject
+
+ argument :project_path, GraphQL::ID_TYPE,
+ required: true,
+ description: "The project the alert to mutate is in"
+
+ argument :iid, GraphQL::STRING_TYPE,
+ required: true,
+ description: "The iid of the alert to mutate"
+
+ field :alert,
+ Types::AlertManagement::AlertType,
+ null: true,
+ description: "The alert after mutation"
+
+ field :issue,
+ Types::IssueType,
+ null: true,
+ description: "The issue created after mutation"
+
+ authorize :update_alert_management_alert
+
+ private
+
+ def find_object(project_path:, iid:)
+ project = resolve_project(full_path: project_path)
+
+ return unless project
+
+ resolver = Resolvers::AlertManagementAlertResolver.single.new(object: project, context: context, field: nil)
+ resolver.resolve(iid: iid)
+ end
+ end
+ end
+end
diff --git a/app/graphql/mutations/alert_management/create_alert_issue.rb b/app/graphql/mutations/alert_management/create_alert_issue.rb
new file mode 100644
index 00000000000..adb048a4479
--- /dev/null
+++ b/app/graphql/mutations/alert_management/create_alert_issue.rb
@@ -0,0 +1,30 @@
+# frozen_string_literal: true
+
+module Mutations
+ module AlertManagement
+ class CreateAlertIssue < Base
+ graphql_name 'CreateAlertIssue'
+
+ def resolve(args)
+ alert = authorized_find!(project_path: args[:project_path], iid: args[:iid])
+ result = create_alert_issue(alert, current_user)
+
+ prepare_response(alert, result)
+ end
+
+ private
+
+ def create_alert_issue(alert, user)
+ ::AlertManagement::CreateAlertIssueService.new(alert, user).execute
+ end
+
+ def prepare_response(alert, result)
+ {
+ alert: alert,
+ issue: result.payload[:issue],
+ errors: Array(result.message)
+ }
+ end
+ end
+ end
+end
diff --git a/app/graphql/mutations/alert_management/update_alert_status.rb b/app/graphql/mutations/alert_management/update_alert_status.rb
new file mode 100644
index 00000000000..e73a591378a
--- /dev/null
+++ b/app/graphql/mutations/alert_management/update_alert_status.rb
@@ -0,0 +1,35 @@
+# frozen_string_literal: true
+
+module Mutations
+ module AlertManagement
+ class UpdateAlertStatus < Base
+ graphql_name 'UpdateAlertStatus'
+
+ argument :status, Types::AlertManagement::StatusEnum,
+ required: true,
+ description: 'The status to set the alert'
+
+ def resolve(args)
+ alert = authorized_find!(project_path: args[:project_path], iid: args[:iid])
+ result = update_status(alert, args[:status])
+
+ prepare_response(result)
+ end
+
+ private
+
+ def update_status(alert, status)
+ ::AlertManagement::UpdateAlertStatusService
+ .new(alert, current_user, status)
+ .execute
+ end
+
+ def prepare_response(result)
+ {
+ alert: result.payload[:alert],
+ errors: result.error? ? [result.message] : []
+ }
+ end
+ end
+ end
+end
diff --git a/app/graphql/mutations/base_mutation.rb b/app/graphql/mutations/base_mutation.rb
index 623f7c27584..30510cfab50 100644
--- a/app/graphql/mutations/base_mutation.rb
+++ b/app/graphql/mutations/base_mutation.rb
@@ -9,7 +9,7 @@ module Mutations
field :errors, [GraphQL::STRING_TYPE],
null: false,
- description: "Reasons why the mutation failed."
+ description: "Errors encountered during execution of the mutation."
def current_user
context[:current_user]
diff --git a/app/graphql/mutations/branches/create.rb b/app/graphql/mutations/branches/create.rb
new file mode 100644
index 00000000000..127d5447d0a
--- /dev/null
+++ b/app/graphql/mutations/branches/create.rb
@@ -0,0 +1,51 @@
+# frozen_string_literal: true
+
+module Mutations
+ module Branches
+ class Create < BaseMutation
+ include Mutations::ResolvesProject
+
+ graphql_name 'CreateBranch'
+
+ argument :project_path, GraphQL::ID_TYPE,
+ required: true,
+ description: 'Project full path the branch is associated with'
+
+ argument :name, GraphQL::STRING_TYPE,
+ required: true,
+ description: 'Name of the branch'
+
+ argument :ref,
+ GraphQL::STRING_TYPE,
+ required: true,
+ description: 'Branch name or commit SHA to create branch from'
+
+ field :branch,
+ Types::BranchType,
+ null: true,
+ description: 'Branch after mutation'
+
+ authorize :push_code
+
+ def resolve(project_path:, name:, ref:)
+ project = authorized_find!(full_path: project_path)
+
+ context.scoped_set!(:branch_project, project)
+
+ result = ::Branches::CreateService.new(project, current_user)
+ .execute(name, ref)
+
+ {
+ branch: (result[:branch] if result[:status] == :success),
+ errors: Array.wrap(result[:message])
+ }
+ end
+
+ private
+
+ def find_object(full_path:)
+ resolve_project(full_path: full_path)
+ end
+ end
+ end
+end
diff --git a/app/graphql/mutations/design_management/base.rb b/app/graphql/mutations/design_management/base.rb
new file mode 100644
index 00000000000..918e5709b94
--- /dev/null
+++ b/app/graphql/mutations/design_management/base.rb
@@ -0,0 +1,23 @@
+# frozen_string_literal: true
+
+module Mutations
+ module DesignManagement
+ class Base < ::Mutations::BaseMutation
+ include Mutations::ResolvesIssuable
+
+ argument :project_path, GraphQL::ID_TYPE,
+ required: true,
+ description: "The project where the issue is to upload designs for"
+
+ argument :iid, GraphQL::ID_TYPE,
+ required: true,
+ description: "The iid of the issue to modify designs for"
+
+ private
+
+ def find_object(project_path:, iid:)
+ resolve_issuable(type: :issue, parent_path: project_path, iid: iid)
+ end
+ end
+ end
+end
diff --git a/app/graphql/mutations/design_management/delete.rb b/app/graphql/mutations/design_management/delete.rb
new file mode 100644
index 00000000000..d2ef2c9bcca
--- /dev/null
+++ b/app/graphql/mutations/design_management/delete.rb
@@ -0,0 +1,66 @@
+# frozen_string_literal: true
+
+module Mutations
+ module DesignManagement
+ class Delete < Base
+ Errors = ::Gitlab::Graphql::Errors
+
+ graphql_name "DesignManagementDelete"
+
+ argument :filenames, [GraphQL::STRING_TYPE],
+ required: true,
+ description: "The filenames of the designs to delete",
+ prepare: ->(names, _ctx) do
+ names.presence || (raise Errors::ArgumentError, 'no filenames')
+ end
+
+ field :version, Types::DesignManagement::VersionType,
+ null: true, # null on error
+ description: 'The new version in which the designs are deleted'
+
+ authorize :destroy_design
+
+ def resolve(project_path:, iid:, filenames:)
+ issue = authorized_find!(project_path: project_path, iid: iid)
+ project = issue.project
+ designs = resolve_designs(issue, filenames)
+
+ result = ::DesignManagement::DeleteDesignsService
+ .new(project, current_user, issue: issue, designs: designs)
+ .execute
+
+ {
+ version: result[:version],
+ errors: Array.wrap(result[:message])
+ }
+ end
+
+ private
+
+ # Here we check that:
+ # * we find exactly as many designs as filenames
+ def resolve_designs(issue, filenames)
+ designs = issue.design_collection.designs_by_filename(filenames)
+
+ validate_all_were_found!(designs, filenames)
+
+ designs
+ end
+
+ def validate_all_were_found!(designs, filenames)
+ found_filenames = designs.map(&:filename)
+ missing = filenames.difference(found_filenames)
+
+ if missing.present?
+ raise Errors::ArgumentError, <<~MSG
+ Not all the designs you named currently exist.
+ The following filenames were not found:
+ #{missing.join(', ')}
+
+ They may have already been deleted.
+ MSG
+ end
+ end
+ end
+ end
+end
diff --git a/app/graphql/mutations/design_management/upload.rb b/app/graphql/mutations/design_management/upload.rb
new file mode 100644
index 00000000000..1ed7f8e49e6
--- /dev/null
+++ b/app/graphql/mutations/design_management/upload.rb
@@ -0,0 +1,38 @@
+# frozen_string_literal: true
+
+module Mutations
+ module DesignManagement
+ class Upload < Base
+ graphql_name "DesignManagementUpload"
+
+ argument :files, [ApolloUploadServer::Upload],
+ required: true,
+ description: "The files to upload"
+
+ authorize :create_design
+
+ field :designs, [Types::DesignManagement::DesignType],
+ null: false,
+ description: "The designs that were uploaded by the mutation"
+
+ field :skipped_designs, [Types::DesignManagement::DesignType],
+ null: false,
+ description: "Any designs that were skipped from the upload due to there " \
+ "being no change to their content since their last version"
+
+ def resolve(project_path:, iid:, files:)
+ issue = authorized_find!(project_path: project_path, iid: iid)
+ project = issue.project
+
+ result = ::DesignManagement::SaveDesignsService.new(project, current_user, issue: issue, files: files)
+ .execute
+
+ {
+ designs: Array.wrap(result[:designs]),
+ skipped_designs: Array.wrap(result[:skipped_designs]),
+ errors: Array.wrap(result[:message])
+ }
+ end
+ end
+ end
+end
diff --git a/app/graphql/mutations/metrics/dashboard/annotations/create.rb b/app/graphql/mutations/metrics/dashboard/annotations/create.rb
new file mode 100644
index 00000000000..f99688aeac6
--- /dev/null
+++ b/app/graphql/mutations/metrics/dashboard/annotations/create.rb
@@ -0,0 +1,106 @@
+# frozen_string_literal: true
+
+module Mutations
+ module Metrics
+ module Dashboard
+ module Annotations
+ class Create < BaseMutation
+ graphql_name 'CreateAnnotation'
+
+ ANNOTATION_SOURCE_ARGUMENT_ERROR = 'Either a cluster or environment global id is required'
+ INVALID_ANNOTATION_SOURCE_ERROR = 'Invalid cluster or environment id'
+
+ authorize :create_metrics_dashboard_annotation
+
+ field :annotation,
+ Types::Metrics::Dashboards::AnnotationType,
+ null: true,
+ description: 'The created annotation'
+
+ argument :environment_id,
+ GraphQL::ID_TYPE,
+ required: false,
+ description: 'The global id of the environment to add an annotation to'
+
+ argument :cluster_id,
+ GraphQL::ID_TYPE,
+ required: false,
+ description: 'The global id of the cluster to add an annotation to'
+
+ argument :starting_at, Types::TimeType,
+ required: true,
+ description: 'Timestamp indicating starting moment to which the annotation relates'
+
+ argument :ending_at, Types::TimeType,
+ required: false,
+ description: 'Timestamp indicating ending moment to which the annotation relates'
+
+ argument :dashboard_path,
+ GraphQL::STRING_TYPE,
+ required: true,
+ description: 'The path to a file defining the dashboard on which the annotation should be added'
+
+ argument :description,
+ GraphQL::STRING_TYPE,
+ required: true,
+ description: 'The description of the annotation'
+
+ AnnotationSource = Struct.new(:object, keyword_init: true) do
+ def type_keys
+ { 'Clusters::Cluster' => :cluster, 'Environment' => :environment }
+ end
+
+ def klass
+ object.class.name
+ end
+
+ def type
+ raise Gitlab::Graphql::Errors::ArgumentError, INVALID_ANNOTATION_SOURCE_ERROR unless type_keys[klass]
+
+ type_keys[klass]
+ end
+ end
+
+ def resolve(args)
+ annotation_response = ::Metrics::Dashboard::Annotations::CreateService.new(context[:current_user], annotation_create_params(args)).execute
+
+ annotation = annotation_response[:annotation]
+
+ {
+ annotation: annotation.valid? ? annotation : nil,
+ errors: errors_on_object(annotation)
+ }
+ end
+
+ private
+
+ def ready?(**args)
+ # Raise error if both cluster_id and environment_id are present or neither is present
+ unless args[:cluster_id].present? ^ args[:environment_id].present?
+ raise Gitlab::Graphql::Errors::ArgumentError, ANNOTATION_SOURCE_ARGUMENT_ERROR
+ end
+
+ super(args)
+ end
+
+ def find_object(id:)
+ GitlabSchema.object_from_id(id)
+ end
+
+ def annotation_create_params(args)
+ annotation_source = AnnotationSource.new(object: annotation_source(args))
+
+ args[annotation_source.type] = annotation_source.object
+
+ args
+ end
+
+ def annotation_source(args)
+ annotation_source_id = args[:cluster_id] || args[:environment_id]
+ authorized_find!(id: annotation_source_id)
+ end
+ end
+ end
+ end
+ end
+end
diff --git a/app/graphql/mutations/snippets/base.rb b/app/graphql/mutations/snippets/base.rb
index 9dc6d49774e..c8cc721b2e0 100644
--- a/app/graphql/mutations/snippets/base.rb
+++ b/app/graphql/mutations/snippets/base.rb
@@ -15,6 +15,8 @@ module Mutations
end
def authorized_resource?(snippet)
+ return false if snippet.nil?
+
Ability.allowed?(context[:current_user], ability_for(snippet), snippet)
end
diff --git a/app/graphql/mutations/snippets/create.rb b/app/graphql/mutations/snippets/create.rb
index 266a123de82..6fc223fbee7 100644
--- a/app/graphql/mutations/snippets/create.rb
+++ b/app/graphql/mutations/snippets/create.rb
@@ -36,6 +36,10 @@ module Mutations
required: false,
description: 'The project full path the snippet is associated with'
+ argument :uploaded_files, [GraphQL::STRING_TYPE],
+ required: false,
+ description: 'The paths to files uploaded in the snippet description'
+
def resolve(args)
project_path = args.delete(:project_path)
@@ -45,9 +49,14 @@ module Mutations
raise_resource_not_available_error!
end
+ # We need to rename `uploaded_files` into `files` because
+ # it's the expected key param
+ args[:files] = args.delete(:uploaded_files)
+
service_response = ::Snippets::CreateService.new(project,
context[:current_user],
args).execute
+
snippet = service_response.payload[:snippet]
{
diff --git a/app/graphql/resolvers/alert_management/alert_status_counts_resolver.rb b/app/graphql/resolvers/alert_management/alert_status_counts_resolver.rb
new file mode 100644
index 00000000000..7f4346632ca
--- /dev/null
+++ b/app/graphql/resolvers/alert_management/alert_status_counts_resolver.rb
@@ -0,0 +1,13 @@
+# frozen_string_literal: true
+
+module Resolvers
+ module AlertManagement
+ class AlertStatusCountsResolver < BaseResolver
+ type Types::AlertManagement::AlertStatusCountsType, null: true
+
+ def resolve(**args)
+ ::Gitlab::AlertManagement::AlertStatusCounts.new(context[:current_user], object, args)
+ end
+ end
+ end
+end
diff --git a/app/graphql/resolvers/alert_management_alert_resolver.rb b/app/graphql/resolvers/alert_management_alert_resolver.rb
new file mode 100644
index 00000000000..51ebbb96476
--- /dev/null
+++ b/app/graphql/resolvers/alert_management_alert_resolver.rb
@@ -0,0 +1,31 @@
+# frozen_string_literal: true
+
+module Resolvers
+ class AlertManagementAlertResolver < BaseResolver
+ argument :iid, GraphQL::STRING_TYPE,
+ required: false,
+ description: 'IID of the alert. For example, "1"'
+
+ argument :statuses, [Types::AlertManagement::StatusEnum],
+ as: :status,
+ required: false,
+ description: 'Alerts with the specified statues. For example, [TRIGGERED]'
+
+ argument :sort, Types::AlertManagement::AlertSortEnum,
+ description: 'Sort alerts by this criteria',
+ required: false
+
+ argument :search, GraphQL::STRING_TYPE,
+ description: 'Search criteria for filtering alerts. This will search on title, description, service, monitoring_tool.',
+ required: false
+
+ type Types::AlertManagement::AlertType, null: true
+
+ def resolve(**args)
+ parent = object.respond_to?(:sync) ? object.sync : object
+ return ::AlertManagement::Alert.none if parent.nil?
+
+ ::AlertManagement::AlertsFinder.new(context[:current_user], parent, args).execute
+ end
+ end
+end
diff --git a/app/graphql/resolvers/board_lists_resolver.rb b/app/graphql/resolvers/board_lists_resolver.rb
new file mode 100644
index 00000000000..f8d62ba86af
--- /dev/null
+++ b/app/graphql/resolvers/board_lists_resolver.rb
@@ -0,0 +1,38 @@
+# frozen_string_literal: true
+
+module Resolvers
+ class BoardListsResolver < BaseResolver
+ include Gitlab::Graphql::Authorize::AuthorizeResource
+
+ type Types::BoardListType, null: true
+
+ alias_method :board, :object
+
+ def resolve(lookahead: nil)
+ authorize!(board)
+
+ lists = board_lists
+
+ if load_preferences?(lookahead)
+ List.preload_preferences_for_user(lists, context[:current_user])
+ end
+
+ Gitlab::Graphql::Pagination::OffsetActiveRecordRelationConnection.new(lists)
+ end
+
+ private
+
+ def board_lists
+ service = Boards::Lists::ListService.new(board.resource_parent, context[:current_user])
+ service.execute(board, create_default_lists: false)
+ end
+
+ def authorized_resource?(board)
+ Ability.allowed?(context[:current_user], :read_list, board)
+ end
+
+ def load_preferences?(lookahead)
+ lookahead&.selection(:edges)&.selection(:node)&.selects?(:collapsed)
+ end
+ end
+end
diff --git a/app/graphql/resolvers/branch_commit_resolver.rb b/app/graphql/resolvers/branch_commit_resolver.rb
new file mode 100644
index 00000000000..11c49e17bc5
--- /dev/null
+++ b/app/graphql/resolvers/branch_commit_resolver.rb
@@ -0,0 +1,17 @@
+# frozen_string_literal: true
+
+module Resolvers
+ class BranchCommitResolver < BaseResolver
+ type Types::CommitType, null: true
+
+ alias_method :branch, :object
+
+ def resolve(**args)
+ return unless branch
+
+ commit = branch.dereferenced_target
+
+ ::Commit.new(commit, context[:branch_project]) if commit
+ end
+ end
+end
diff --git a/app/graphql/resolvers/design_management/design_at_version_resolver.rb b/app/graphql/resolvers/design_management/design_at_version_resolver.rb
new file mode 100644
index 00000000000..fd9b349f974
--- /dev/null
+++ b/app/graphql/resolvers/design_management/design_at_version_resolver.rb
@@ -0,0 +1,46 @@
+# frozen_string_literal: true
+
+module Resolvers
+ module DesignManagement
+ class DesignAtVersionResolver < BaseResolver
+ include Gitlab::Graphql::Authorize::AuthorizeResource
+
+ type Types::DesignManagement::DesignAtVersionType, null: false
+
+ authorize :read_design
+
+ argument :id, GraphQL::ID_TYPE,
+ required: true,
+ description: 'The Global ID of the design at this version'
+
+ def resolve(id:)
+ authorized_find!(id: id)
+ end
+
+ def find_object(id:)
+ dav = GitlabSchema.object_from_id(id, expected_type: ::DesignManagement::DesignAtVersion)
+ return unless consistent?(dav)
+
+ dav
+ end
+
+ def self.single
+ self
+ end
+
+ private
+
+ # If this resolver is mounted on something that has an issue
+ # (such as design collection for instance), then we should check
+ # that the DesignAtVersion as found by its ID does in fact belong
+ # to this issue.
+ def consistent?(dav)
+ issue.nil? || (dav&.design&.issue_id == issue.id)
+ end
+
+ def issue
+ object&.issue
+ end
+ end
+ end
+end
diff --git a/app/graphql/resolvers/design_management/design_resolver.rb b/app/graphql/resolvers/design_management/design_resolver.rb
new file mode 100644
index 00000000000..05bdbbbe407
--- /dev/null
+++ b/app/graphql/resolvers/design_management/design_resolver.rb
@@ -0,0 +1,57 @@
+# frozen_string_literal: true
+
+module Resolvers
+ module DesignManagement
+ class DesignResolver < BaseResolver
+ argument :id, GraphQL::ID_TYPE,
+ required: false,
+ description: 'Find a design by its ID'
+
+ argument :filename, GraphQL::STRING_TYPE,
+ required: false,
+ description: 'Find a design by its filename'
+
+ def resolve(filename: nil, id: nil)
+ params = parse_args(filename, id)
+
+ build_finder(params).execute.first
+ end
+
+ def self.single
+ self
+ end
+
+ private
+
+ def issue
+ object.issue
+ end
+
+ def build_finder(params)
+ ::DesignManagement::DesignsFinder.new(issue, current_user, params)
+ end
+
+ def error(msg)
+ raise ::Gitlab::Graphql::Errors::ArgumentError, msg
+ end
+
+ def parse_args(filename, id)
+ provided = [filename, id].map(&:present?)
+
+ if provided.none?
+ error('one of id or filename must be passed')
+ elsif provided.all?
+ error('only one of id or filename may be passed')
+ elsif filename.present?
+ { filenames: [filename] }
+ else
+ { ids: [parse_gid(id)] }
+ end
+ end
+
+ def parse_gid(gid)
+ GitlabSchema.parse_gid(gid, expected_type: ::DesignManagement::Design).model_id
+ end
+ end
+ end
+end
diff --git a/app/graphql/resolvers/design_management/designs_resolver.rb b/app/graphql/resolvers/design_management/designs_resolver.rb
new file mode 100644
index 00000000000..81f94d5cb30
--- /dev/null
+++ b/app/graphql/resolvers/design_management/designs_resolver.rb
@@ -0,0 +1,50 @@
+# frozen_string_literal: true
+
+module Resolvers
+ module DesignManagement
+ class DesignsResolver < BaseResolver
+ argument :ids,
+ [GraphQL::ID_TYPE],
+ required: false,
+ description: 'Filters designs by their ID'
+ argument :filenames,
+ [GraphQL::STRING_TYPE],
+ required: false,
+ description: 'Filters designs by their filename'
+ argument :at_version,
+ GraphQL::ID_TYPE,
+ required: false,
+ description: 'Filters designs to only those that existed at the version. ' \
+ 'If argument is omitted or nil then all designs will reflect the latest version'
+
+ def self.single
+ ::Resolvers::DesignManagement::DesignResolver
+ end
+
+ def resolve(ids: nil, filenames: nil, at_version: nil)
+ ::DesignManagement::DesignsFinder.new(
+ issue,
+ current_user,
+ ids: design_ids(ids),
+ filenames: filenames,
+ visible_at_version: version(at_version),
+ order: :id
+ ).execute
+ end
+
+ private
+
+ def version(at_version)
+ GitlabSchema.object_from_id(at_version)&.sync if at_version
+ end
+
+ def design_ids(ids)
+ ids&.map { |id| GlobalID.parse(id).model_id }
+ end
+
+ def issue
+ object.issue
+ end
+ end
+ end
+end
diff --git a/app/graphql/resolvers/design_management/version/design_at_version_resolver.rb b/app/graphql/resolvers/design_management/version/design_at_version_resolver.rb
new file mode 100644
index 00000000000..03f7908780c
--- /dev/null
+++ b/app/graphql/resolvers/design_management/version/design_at_version_resolver.rb
@@ -0,0 +1,95 @@
+# frozen_string_literal: true
+
+module Resolvers
+ module DesignManagement
+ module Version
+ # Resolver for a DesignAtVersion object given an implicit version context
+ class DesignAtVersionResolver < BaseResolver
+ include Gitlab::Graphql::Authorize::AuthorizeResource
+
+ type Types::DesignManagement::DesignAtVersionType, null: true
+
+ authorize :read_design
+
+ argument :id, GraphQL::ID_TYPE,
+ required: false,
+ as: :design_at_version_id,
+ description: 'The ID of the DesignAtVersion'
+ argument :design_id, GraphQL::ID_TYPE,
+ required: false,
+ description: 'The ID of a specific design'
+ argument :filename, GraphQL::STRING_TYPE,
+ required: false,
+ description: 'The filename of a specific design'
+
+ def self.single
+ self
+ end
+
+ def resolve(design_id: nil, filename: nil, design_at_version_id: nil)
+ validate_arguments(design_id, filename, design_at_version_id)
+
+ return unless Ability.allowed?(current_user, :read_design, issue)
+ return specific_design_at_version(design_at_version_id) if design_at_version_id
+
+ find(design_id, filename).map { |d| make(d) }.first
+ end
+
+ private
+
+ def validate_arguments(design_id, filename, design_at_version_id)
+ args = { filename: filename, id: design_at_version_id, design_id: design_id }
+ passed = args.compact.keys
+
+ return if passed.size == 1
+
+ msg = "Exactly one of #{args.keys.join(', ')} expected, got #{passed}"
+
+ raise Gitlab::Graphql::Errors::ArgumentError, msg
+ end
+
+ def specific_design_at_version(id)
+ dav = GitlabSchema.object_from_id(id, expected_type: ::DesignManagement::DesignAtVersion)
+ return unless consistent?(dav)
+
+ dav
+ end
+
+ # Test that the DAV found by ID actually belongs on this version, and
+ # that it is visible at this version.
+ def consistent?(dav)
+ return false unless dav.present?
+
+ dav.design.issue_id == issue.id &&
+ dav.version.id == version.id &&
+ dav.design.visible_in?(version)
+ end
+
+ def find(id, filename)
+ ids = [parse_design_id(id).model_id] if id
+ filenames = [filename] if filename
+
+ ::DesignManagement::DesignsFinder
+ .new(issue, current_user, ids: ids, filenames: filenames, visible_at_version: version)
+ .execute
+ end
+
+ def parse_design_id(id)
+ GitlabSchema.parse_gid(id, expected_type: ::DesignManagement::Design)
+ end
+
+ def issue
+ version.issue
+ end
+
+ def version
+ object
+ end
+
+ def make(design)
+ ::DesignManagement::DesignAtVersion.new(design: design, version: version)
+ end
+ end
+ end
+ end
+end
diff --git a/app/graphql/resolvers/design_management/version/designs_at_version_resolver.rb b/app/graphql/resolvers/design_management/version/designs_at_version_resolver.rb
new file mode 100644
index 00000000000..5ccb2f3e311
--- /dev/null
+++ b/app/graphql/resolvers/design_management/version/designs_at_version_resolver.rb
@@ -0,0 +1,60 @@
+# frozen_string_literal: true
+
+module Resolvers
+ module DesignManagement
+ module Version
+ # Resolver for DesignAtVersion objects given an implicit version context
+ class DesignsAtVersionResolver < BaseResolver
+ include Gitlab::Graphql::Authorize::AuthorizeResource
+
+ type Types::DesignManagement::DesignAtVersionType, null: true
+
+ authorize :read_design
+
+ argument :ids,
+ [GraphQL::ID_TYPE],
+ required: false,
+ description: 'Filters designs by their ID'
+ argument :filenames,
+ [GraphQL::STRING_TYPE],
+ required: false,
+ description: 'Filters designs by their filename'
+
+ def self.single
+ ::Resolvers::DesignManagement::Version::DesignAtVersionResolver
+ end
+
+ def resolve(ids: nil, filenames: nil)
+ find(ids, filenames).execute.map { |d| make(d) }
+ end
+
+ private
+
+ def find(ids, filenames)
+ ids = ids&.map { |id| parse_design_id(id).model_id }
+
+ ::DesignManagement::DesignsFinder.new(issue, current_user,
+ ids: ids,
+ filenames: filenames,
+ visible_at_version: version)
+ end
+
+ def parse_design_id(id)
+ GitlabSchema.parse_gid(id, expected_type: ::DesignManagement::Design)
+ end
+
+ def issue
+ version.issue
+ end
+
+ def version
+ object
+ end
+
+ def make(design)
+ ::DesignManagement::DesignAtVersion.new(design: design, version: version)
+ end
+ end
+ end
+ end
+end
diff --git a/app/graphql/resolvers/design_management/version_in_collection_resolver.rb b/app/graphql/resolvers/design_management/version_in_collection_resolver.rb
new file mode 100644
index 00000000000..9e729172881
--- /dev/null
+++ b/app/graphql/resolvers/design_management/version_in_collection_resolver.rb
@@ -0,0 +1,45 @@
+# frozen_string_literal: true
+
+module Resolvers
+ module DesignManagement
+ class VersionInCollectionResolver < BaseResolver
+ include Gitlab::Graphql::Authorize::AuthorizeResource
+
+ type Types::DesignManagement::VersionType, null: true
+
+ authorize :read_design
+
+ alias_method :collection, :object
+
+ argument :sha, GraphQL::STRING_TYPE,
+ required: false,
+ description: "The SHA256 of a specific version"
+ argument :id, GraphQL::ID_TYPE,
+ required: false,
+ description: 'The Global ID of the version'
+
+ def resolve(id: nil, sha: nil)
+ check_args(id, sha)
+
+ gid = GitlabSchema.parse_gid(id, expected_type: ::DesignManagement::Version) if id
+
+ ::DesignManagement::VersionsFinder
+ .new(collection, current_user, sha: sha, version_id: gid&.model_id)
+ .execute
+ .first
+ end
+
+ def self.single
+ self
+ end
+
+ private
+
+ def check_args(id, sha)
+ return if id.present? || sha.present?
+
+ raise ::Gitlab::Graphql::Errors::ArgumentError, 'one of id or sha is required'
+ end
+ end
+ end
+end
diff --git a/app/graphql/resolvers/design_management/version_resolver.rb b/app/graphql/resolvers/design_management/version_resolver.rb
new file mode 100644
index 00000000000..b0e0843e6c8
--- /dev/null
+++ b/app/graphql/resolvers/design_management/version_resolver.rb
@@ -0,0 +1,25 @@
+# frozen_string_literal: true
+
+module Resolvers
+ module DesignManagement
+ class VersionResolver < BaseResolver
+ include Gitlab::Graphql::Authorize::AuthorizeResource
+
+ type Types::DesignManagement::VersionType, null: true
+
+ authorize :read_design
+
+ argument :id, GraphQL::ID_TYPE,
+ required: true,
+ description: 'The Global ID of the version'
+
+ def resolve(id:)
+ authorized_find!(id: id)
+ end
+
+ def find_object(id:)
+ GitlabSchema.object_from_id(id, expected_type: ::DesignManagement::Version)
+ end
+ end
+ end
+end
diff --git a/app/graphql/resolvers/design_management/versions_resolver.rb b/app/graphql/resolvers/design_management/versions_resolver.rb
new file mode 100644
index 00000000000..a62258dad5c
--- /dev/null
+++ b/app/graphql/resolvers/design_management/versions_resolver.rb
@@ -0,0 +1,76 @@
+# frozen_string_literal: true
+
+module Resolvers
+ module DesignManagement
+ class VersionsResolver < BaseResolver
+ type Types::DesignManagement::VersionType.connection_type, null: false
+
+ alias_method :design_or_collection, :object
+
+ argument :earlier_or_equal_to_sha, GraphQL::STRING_TYPE,
+ as: :sha,
+ required: false,
+ description: 'The SHA256 of the most recent acceptable version'
+
+ argument :earlier_or_equal_to_id, GraphQL::ID_TYPE,
+ as: :id,
+ required: false,
+ description: 'The Global ID of the most recent acceptable version'
+
+ # This resolver has a custom singular resolver
+ def self.single
+ ::Resolvers::DesignManagement::VersionInCollectionResolver
+ end
+
+ def resolve(parent: nil, id: nil, sha: nil)
+ version = cutoff(parent, id, sha)
+
+ raise ::Gitlab::Graphql::Errors::ResourceNotAvailable, 'cutoff not found' unless version.present?
+
+ if version == :unconstrained
+ find
+ else
+ find(earlier_or_equal_to: version)
+ end
+ end
+
+ private
+
+ # Find the most recent version that the client will accept
+ def cutoff(parent, id, sha)
+ if sha.present? || id.present?
+ specific_version(id, sha)
+ elsif at_version = at_version_arg(parent)
+ by_id(at_version)
+ else
+ :unconstrained
+ end
+ end
+
+ def specific_version(id, sha)
+ gid = GitlabSchema.parse_gid(id, expected_type: ::DesignManagement::Version) if id
+ find(sha: sha, version_id: gid&.model_id).first
+ end
+
+ def find(**params)
+ ::DesignManagement::VersionsFinder
+ .new(design_or_collection, current_user, params)
+ .execute
+ end
+
+ def by_id(id)
+ GitlabSchema.object_from_id(id, expected_type: ::DesignManagement::Version).sync
+ end
+
+ # Find an `at_version` argument passed to a parent node.
+ #
+ # If one is found, then a design collection further up the AST
+ # has been filtered to reflect designs at that version, and so
+ # for consistency we should only present versions up to the given
+ # version here.
+ def at_version_arg(parent)
+ ::Gitlab::Graphql::FindArgumentInParent.find(parent, :at_version, limit_depth: 4)
+ end
+ end
+ end
+end
diff --git a/app/graphql/resolvers/issues_resolver.rb b/app/graphql/resolvers/issues_resolver.rb
index 04da54a6bb6..f103da07666 100644
--- a/app/graphql/resolvers/issues_resolver.rb
+++ b/app/graphql/resolvers/issues_resolver.rb
@@ -52,6 +52,10 @@ module Resolvers
type Types::IssueType, null: true
+ NON_STABLE_CURSOR_SORTS = %i[priority_asc priority_desc
+ label_priority_asc label_priority_desc
+ milestone_due_asc milestone_due_desc].freeze
+
def resolve(**args)
# The project could have been loaded in batch by `BatchLoader`.
# At this point we need the `id` of the project to query for issues, so
@@ -70,7 +74,15 @@ module Resolvers
args[:iids] ||= [args[:iid]].compact
args[:attempt_project_search_optimizations] = args[:search].present?
- IssuesFinder.new(context[:current_user], args).execute
+ issues = IssuesFinder.new(context[:current_user], args).execute
+
+ if non_stable_cursor_sort?(args[:sort])
+ # Certain complex sorts are not supported by the stable cursor pagination yet.
+ # In these cases, we use offset pagination, so we return the correct connection.
+ Gitlab::Graphql::Pagination::OffsetActiveRecordRelationConnection.new(issues)
+ else
+ issues
+ end
end
def self.resolver_complexity(args, child_complexity:)
@@ -79,5 +91,9 @@ module Resolvers
complexity
end
+
+ def non_stable_cursor_sort?(sort)
+ NON_STABLE_CURSOR_SORTS.include?(sort)
+ end
end
end
diff --git a/app/graphql/resolvers/metrics/dashboards/annotation_resolver.rb b/app/graphql/resolvers/metrics/dashboards/annotation_resolver.rb
index 068323a3073..2dd224bb17b 100644
--- a/app/graphql/resolvers/metrics/dashboards/annotation_resolver.rb
+++ b/app/graphql/resolvers/metrics/dashboards/annotation_resolver.rb
@@ -18,7 +18,6 @@ module Resolvers
def resolve(**args)
return [] unless dashboard
- return [] unless Feature.enabled?(:metrics_dashboard_annotations, dashboard.environment&.project)
::Metrics::Dashboards::AnnotationsFinder.new(dashboard: dashboard, params: args).execute
end
diff --git a/app/graphql/resolvers/milestone_resolver.rb b/app/graphql/resolvers/milestone_resolver.rb
index 2e7b6fdfd5f..6c6513e0ee4 100644
--- a/app/graphql/resolvers/milestone_resolver.rb
+++ b/app/graphql/resolvers/milestone_resolver.rb
@@ -9,6 +9,10 @@ module Resolvers
required: false,
description: 'Filter milestones by state'
+ argument :include_descendants, GraphQL::BOOLEAN_TYPE,
+ required: false,
+ description: 'Return also milestones in all subgroups and subprojects'
+
type Types::MilestoneType, null: true
def resolve(**args)
@@ -26,16 +30,16 @@ module Resolvers
state: args[:state] || 'all',
start_date: args[:start_date],
end_date: args[:end_date]
- }.merge(parent_id_parameter)
+ }.merge(parent_id_parameter(args))
end
def parent
@parent ||= object.respond_to?(:sync) ? object.sync : object
end
- def parent_id_parameter
+ def parent_id_parameter(args)
if parent.is_a?(Group)
- { group_ids: parent.id }
+ group_parameters(args)
elsif parent.is_a?(Project)
{ project_ids: parent.id }
end
@@ -46,5 +50,26 @@ module Resolvers
def authorize!
Ability.allowed?(context[:current_user], :read_milestone, parent) || raise_resource_not_available_error!
end
+
+ def group_parameters(args)
+ return { group_ids: parent.id } unless include_descendants?(args)
+
+ {
+ group_ids: parent.self_and_descendants.public_or_visible_to_user(current_user).select(:id),
+ project_ids: group_projects.with_issues_or_mrs_available_for_user(current_user)
+ }
+ end
+
+ def include_descendants?(args)
+ args[:include_descendants].present? && Feature.enabled?(:group_milestone_descendants, parent)
+ end
+
+ def group_projects
+ GroupProjectsFinder.new(
+ group: parent,
+ current_user: current_user,
+ options: { include_subgroups: true }
+ ).execute
+ end
end
end
diff --git a/app/graphql/resolvers/namespace_projects_resolver.rb b/app/graphql/resolvers/namespace_projects_resolver.rb
index f5b60f91be6..e841132eea7 100644
--- a/app/graphql/resolvers/namespace_projects_resolver.rb
+++ b/app/graphql/resolvers/namespace_projects_resolver.rb
@@ -29,3 +29,5 @@ module Resolvers
end
end
end
+
+Resolvers::NamespaceProjectsResolver.prepend_if_ee('::EE::Resolvers::NamespaceProjectsResolver')
diff --git a/app/graphql/resolvers/projects_resolver.rb b/app/graphql/resolvers/projects_resolver.rb
new file mode 100644
index 00000000000..068546cd39f
--- /dev/null
+++ b/app/graphql/resolvers/projects_resolver.rb
@@ -0,0 +1,31 @@
+# frozen_string_literal: true
+
+module Resolvers
+ class ProjectsResolver < BaseResolver
+ type Types::ProjectType, null: true
+
+ argument :membership, GraphQL::BOOLEAN_TYPE,
+ required: false,
+ description: 'Limit projects that the current user is a member of'
+
+ argument :search, GraphQL::STRING_TYPE,
+ required: false,
+ description: 'Search criteria'
+
+ def resolve(**args)
+ ProjectsFinder
+ .new(current_user: current_user, params: project_finder_params(args))
+ .execute
+ end
+
+ private
+
+ def project_finder_params(params)
+ {
+ without_deleted: true,
+ non_public: params[:membership],
+ search: params[:search]
+ }.compact
+ end
+ end
+end
diff --git a/app/graphql/resolvers/release_resolver.rb b/app/graphql/resolvers/release_resolver.rb
new file mode 100644
index 00000000000..9bae8b8cd13
--- /dev/null
+++ b/app/graphql/resolvers/release_resolver.rb
@@ -0,0 +1,25 @@
+# frozen_string_literal: true
+
+module Resolvers
+ class ReleaseResolver < BaseResolver
+ type Types::ReleaseType, null: true
+
+ argument :tag_name, GraphQL::STRING_TYPE,
+ required: true,
+ description: 'The name of the tag associated to the release'
+
+ alias_method :project, :object
+
+ def self.single
+ self
+ end
+
+ def resolve(tag_name:)
+ ReleasesFinder.new(
+ project,
+ current_user,
+ { tag: tag_name }
+ ).execute.first
+ end
+ end
+end
diff --git a/app/graphql/resolvers/releases_resolver.rb b/app/graphql/resolvers/releases_resolver.rb
new file mode 100644
index 00000000000..b2afbb92684
--- /dev/null
+++ b/app/graphql/resolvers/releases_resolver.rb
@@ -0,0 +1,21 @@
+# frozen_string_literal: true
+
+module Resolvers
+ class ReleasesResolver < BaseResolver
+ type Types::ReleaseType.connection_type, null: true
+
+ alias_method :project, :object
+
+ # This resolver has a custom singular resolver
+ def self.single
+ Resolvers::ReleaseResolver
+ end
+
+ def resolve(**args)
+ ReleasesFinder.new(
+ project,
+ current_user
+ ).execute
+ end
+ end
+end
diff --git a/app/graphql/types/alert_management/alert_sort_enum.rb b/app/graphql/types/alert_management/alert_sort_enum.rb
new file mode 100644
index 00000000000..e6d38af8170
--- /dev/null
+++ b/app/graphql/types/alert_management/alert_sort_enum.rb
@@ -0,0 +1,25 @@
+# frozen_string_literal: true
+
+module Types
+ module AlertManagement
+ class AlertSortEnum < SortEnum
+ graphql_name 'AlertManagementAlertSort'
+ description 'Values for sorting alerts'
+
+ value 'START_TIME_ASC', 'Start time by ascending order', value: :start_time_asc
+ value 'START_TIME_DESC', 'Start time by descending order', value: :start_time_desc
+ value 'END_TIME_ASC', 'End time by ascending order', value: :end_time_asc
+ value 'END_TIME_DESC', 'End time by descending order', value: :end_time_desc
+ value 'CREATED_TIME_ASC', 'Created time by ascending order', value: :created_at_asc
+ value 'CREATED_TIME_DESC', 'Created time by descending order', value: :created_at_desc
+ value 'UPDATED_TIME_ASC', 'Created time by ascending order', value: :updated_at_asc
+ value 'UPDATED_TIME_DESC', 'Created time by descending order', value: :updated_at_desc
+ value 'EVENTS_COUNT_ASC', 'Events count by ascending order', value: :events_count_asc
+ value 'EVENTS_COUNT_DESC', 'Events count by descending order', value: :events_count_desc
+ value 'SEVERITY_ASC', 'Severity by ascending order', value: :severity_asc
+ value 'SEVERITY_DESC', 'Severity by descending order', value: :severity_desc
+ value 'STATUS_ASC', 'Status by ascending order', value: :status_asc
+ value 'STATUS_DESC', 'Status by descending order', value: :status_desc
+ end
+ end
+end
diff --git a/app/graphql/types/alert_management/alert_status_counts_type.rb b/app/graphql/types/alert_management/alert_status_counts_type.rb
new file mode 100644
index 00000000000..f80b289eabc
--- /dev/null
+++ b/app/graphql/types/alert_management/alert_status_counts_type.rb
@@ -0,0 +1,30 @@
+# frozen_string_literal: true
+
+# Service for managing alert counts and cache updates.
+module Types
+ module AlertManagement
+ class AlertStatusCountsType < BaseObject
+ graphql_name 'AlertManagementAlertStatusCountsType'
+ description "Represents total number of alerts for the represented categories"
+
+ authorize :read_alert_management_alert
+
+ ::Gitlab::AlertManagement::AlertStatusCounts::STATUSES.each_key do |status|
+ field status,
+ GraphQL::INT_TYPE,
+ null: true,
+ description: "Number of alerts with status #{status.upcase} for the project"
+ end
+
+ field :open,
+ GraphQL::INT_TYPE,
+ null: true,
+ description: 'Number of alerts with status TRIGGERED or ACKNOWLEDGED for the project'
+
+ field :all,
+ GraphQL::INT_TYPE,
+ null: true,
+ description: 'Total number of alerts for the project'
+ end
+ end
+end
diff --git a/app/graphql/types/alert_management/alert_type.rb b/app/graphql/types/alert_management/alert_type.rb
new file mode 100644
index 00000000000..a766fb3236d
--- /dev/null
+++ b/app/graphql/types/alert_management/alert_type.rb
@@ -0,0 +1,88 @@
+# frozen_string_literal: true
+
+module Types
+ module AlertManagement
+ class AlertType < BaseObject
+ graphql_name 'AlertManagementAlert'
+ description "Describes an alert from the project's Alert Management"
+
+ authorize :read_alert_management_alert
+
+ field :iid,
+ GraphQL::ID_TYPE,
+ null: false,
+ description: 'Internal ID of the alert'
+
+ field :issue_iid,
+ GraphQL::ID_TYPE,
+ null: true,
+ description: 'Internal ID of the GitLab issue attached to the alert'
+
+ field :title,
+ GraphQL::STRING_TYPE,
+ null: true,
+ description: 'Title of the alert'
+
+ field :description,
+ GraphQL::STRING_TYPE,
+ null: true,
+ description: 'Description of the alert'
+
+ field :severity,
+ AlertManagement::SeverityEnum,
+ null: true,
+ description: 'Severity of the alert'
+
+ field :status,
+ AlertManagement::StatusEnum,
+ null: true,
+ description: 'Status of the alert'
+
+ field :service,
+ GraphQL::STRING_TYPE,
+ null: true,
+ description: 'Service the alert came from'
+
+ field :monitoring_tool,
+ GraphQL::STRING_TYPE,
+ null: true,
+ description: 'Monitoring tool the alert came from'
+
+ field :hosts,
+ [GraphQL::STRING_TYPE],
+ null: true,
+ description: 'List of hosts the alert came from'
+
+ field :started_at,
+ Types::TimeType,
+ null: true,
+ description: 'Timestamp the alert was raised'
+
+ field :ended_at,
+ Types::TimeType,
+ null: true,
+ description: 'Timestamp the alert ended'
+
+ field :event_count,
+ GraphQL::INT_TYPE,
+ null: true,
+ description: 'Number of events of this alert',
+ method: :events
+
+ field :details,
+ GraphQL::Types::JSON,
+ null: true,
+ description: 'Alert details'
+
+ field :created_at,
+ Types::TimeType,
+ null: true,
+ description: 'Timestamp the alert was created'
+
+ field :updated_at,
+ Types::TimeType,
+ null: true,
+ description: 'Timestamp the alert was last updated'
+ end
+ end
+end
diff --git a/app/graphql/types/alert_management/severity_enum.rb b/app/graphql/types/alert_management/severity_enum.rb
new file mode 100644
index 00000000000..99ea56da02c
--- /dev/null
+++ b/app/graphql/types/alert_management/severity_enum.rb
@@ -0,0 +1,14 @@
+# frozen_string_literal: true
+
+module Types
+ module AlertManagement
+ class SeverityEnum < BaseEnum
+ graphql_name 'AlertManagementSeverity'
+ description 'Alert severity values'
+
+ ::AlertManagement::Alert.severities.keys.each do |severity|
+ value severity.upcase, value: severity, description: "#{severity.titleize} severity"
+ end
+ end
+ end
+end
diff --git a/app/graphql/types/alert_management/status_enum.rb b/app/graphql/types/alert_management/status_enum.rb
new file mode 100644
index 00000000000..4ff6c4a9505
--- /dev/null
+++ b/app/graphql/types/alert_management/status_enum.rb
@@ -0,0 +1,14 @@
+# frozen_string_literal: true
+
+module Types
+ module AlertManagement
+ class StatusEnum < BaseEnum
+ graphql_name 'AlertManagementStatus'
+ description 'Alert status values'
+
+ ::AlertManagement::Alert::STATUSES.each do |name, value|
+ value name.upcase, value: value, description: "#{name.to_s.titleize} status"
+ end
+ end
+ end
+end
diff --git a/app/graphql/types/board_list_type.rb b/app/graphql/types/board_list_type.rb
new file mode 100644
index 00000000000..e94ff898807
--- /dev/null
+++ b/app/graphql/types/board_list_type.rb
@@ -0,0 +1,26 @@
+# frozen_string_literal: true
+
+module Types
+ # rubocop: disable Graphql/AuthorizeTypes
+ class BoardListType < BaseObject
+ graphql_name 'BoardList'
+ description 'Represents a list for an issue board'
+
+ field :id, GraphQL::ID_TYPE, null: false,
+ description: 'ID (global ID) of the list'
+ field :title, GraphQL::STRING_TYPE, null: false,
+ description: 'Title of the list'
+ field :list_type, GraphQL::STRING_TYPE, null: false,
+ description: 'Type of the list'
+ field :position, GraphQL::INT_TYPE, null: true,
+ description: 'Position of list within the board'
+ field :label, Types::LabelType, null: true,
+ description: 'Label of the list'
+ field :collapsed, GraphQL::BOOLEAN_TYPE, null: true,
+ description: 'Indicates if list is collapsed for this user',
+ resolve: -> (list, _args, ctx) { list.collapsed?(ctx[:current_user]) }
+ end
+ # rubocop: enable Graphql/AuthorizeTypes
+end
+
+Types::BoardListType.prepend_if_ee('::EE::Types::BoardListType')
diff --git a/app/graphql/types/board_type.rb b/app/graphql/types/board_type.rb
index 9c95a987fe4..c0be782ed1e 100644
--- a/app/graphql/types/board_type.rb
+++ b/app/graphql/types/board_type.rb
@@ -11,6 +11,13 @@ module Types
description: 'ID (global ID) of the board'
field :name, type: GraphQL::STRING_TYPE, null: true,
description: 'Name of the board'
+
+ field :lists,
+ Types::BoardListType.connection_type,
+ null: true,
+ description: 'Lists of the project board',
+ resolver: Resolvers::BoardListsResolver,
+ extras: [:lookahead]
end
end
diff --git a/app/graphql/types/branch_type.rb b/app/graphql/types/branch_type.rb
new file mode 100644
index 00000000000..b15038a46de
--- /dev/null
+++ b/app/graphql/types/branch_type.rb
@@ -0,0 +1,18 @@
+# frozen_string_literal: true
+
+module Types
+ # rubocop: disable Graphql/AuthorizeTypes
+ class BranchType < BaseObject
+ graphql_name 'Branch'
+
+ field :name,
+ GraphQL::STRING_TYPE,
+ null: false,
+ description: 'Name of the branch'
+
+ field :commit, Types::CommitType,
+ null: true, resolver: Resolvers::BranchCommitResolver,
+ description: 'Commit for the branch'
+ end
+ # rubocop: enable Graphql/AuthorizeTypes
+end
diff --git a/app/graphql/types/commit_type.rb b/app/graphql/types/commit_type.rb
index aaf2dfd8488..be5165da545 100644
--- a/app/graphql/types/commit_type.rb
+++ b/app/graphql/types/commit_type.rb
@@ -14,6 +14,7 @@ module Types
description: 'SHA1 ID of the commit'
field :title, type: GraphQL::STRING_TYPE, null: true, calls_gitaly: true,
description: 'Title of the commit message'
+ markdown_field :title_html, null: true
field :description, type: GraphQL::STRING_TYPE, null: true,
description: 'Description of the commit message'
field :message, type: GraphQL::STRING_TYPE, null: true,
diff --git a/app/graphql/types/design_management/design_at_version_type.rb b/app/graphql/types/design_management/design_at_version_type.rb
new file mode 100644
index 00000000000..343d4cf4ff4
--- /dev/null
+++ b/app/graphql/types/design_management/design_at_version_type.rb
@@ -0,0 +1,37 @@
+# frozen_string_literal: true
+
+module Types
+ module DesignManagement
+ class DesignAtVersionType < BaseObject
+ graphql_name 'DesignAtVersion'
+
+ description 'A design pinned to a specific version. ' \
+ 'The image field reflects the design as of the associated version.'
+
+ authorize :read_design
+
+ delegate :design, :version, to: :object
+ delegate :issue, :filename, :full_path, :diff_refs, to: :design
+
+ implements ::Types::DesignManagement::DesignFields
+
+ field :version,
+ Types::DesignManagement::VersionType,
+ null: false,
+ description: 'The version this design-at-versions is pinned to'
+
+ field :design,
+ Types::DesignManagement::DesignType,
+ null: false,
+ description: 'The underlying design.'
+
+ def cached_stateful_version(_parent)
+ version
+ end
+
+ def notes_count
+ design.user_notes_count
+ end
+ end
+ end
+end
diff --git a/app/graphql/types/design_management/design_collection_type.rb b/app/graphql/types/design_management/design_collection_type.rb
new file mode 100644
index 00000000000..194910831c6
--- /dev/null
+++ b/app/graphql/types/design_management/design_collection_type.rb
@@ -0,0 +1,44 @@
+# frozen_string_literal: true
+
+module Types
+ module DesignManagement
+ class DesignCollectionType < BaseObject
+ graphql_name 'DesignCollection'
+ description 'A collection of designs.'
+
+ authorize :read_design
+
+ field :project, Types::ProjectType, null: false,
+ description: 'Project associated with the design collection'
+ field :issue, Types::IssueType, null: false,
+ description: 'Issue associated with the design collection'
+
+ field :designs,
+ Types::DesignManagement::DesignType.connection_type,
+ null: false,
+ resolver: Resolvers::DesignManagement::DesignsResolver,
+ description: 'All designs for the design collection',
+ complexity: 5
+
+ field :versions,
+ Types::DesignManagement::VersionType.connection_type,
+ resolver: Resolvers::DesignManagement::VersionsResolver,
+ description: 'All versions related to all designs, ordered newest first'
+
+ field :version,
+ Types::DesignManagement::VersionType,
+ resolver: Resolvers::DesignManagement::VersionsResolver.single,
+ description: 'A specific version'
+
+ field :design_at_version, ::Types::DesignManagement::DesignAtVersionType,
+ null: true,
+ resolver: ::Resolvers::DesignManagement::DesignAtVersionResolver,
+ description: 'Find a design as of a version'
+
+ field :design, ::Types::DesignManagement::DesignType,
+ null: true,
+ resolver: ::Resolvers::DesignManagement::DesignResolver,
+ description: 'Find a specific design'
+ end
+ end
+end
diff --git a/app/graphql/types/design_management/design_fields.rb b/app/graphql/types/design_management/design_fields.rb
new file mode 100644
index 00000000000..b03b3927392
--- /dev/null
+++ b/app/graphql/types/design_management/design_fields.rb
@@ -0,0 +1,78 @@
+# frozen_string_literal: true
+
+module Types
+ module DesignManagement
+ module DesignFields
+ include BaseInterface
+
+ field_class Types::BaseField
+
+ field :id, GraphQL::ID_TYPE, description: 'The ID of this design', null: false
+ field :project, Types::ProjectType, null: false, description: 'The project the design belongs to'
+ field :issue, Types::IssueType, null: false, description: 'The issue the design belongs to'
+ field :filename, GraphQL::STRING_TYPE, null: false, description: 'The filename of the design'
+ field :full_path, GraphQL::STRING_TYPE, null: false, description: 'The full path to the design file'
+ field :image, GraphQL::STRING_TYPE, null: false, extras: [:parent], description: 'The URL of the full-sized image'
+ field :image_v432x230, GraphQL::STRING_TYPE, null: true, extras: [:parent],
+ description: 'The URL of the design resized to fit within the bounds of 432x230. ' \
+ 'This will be `null` if the image has not been generated'
+ field :diff_refs, Types::DiffRefsType,
+ null: false,
+ calls_gitaly: true,
+ extras: [:parent],
+ description: 'The diff refs for this design'
+ field :event, Types::DesignManagement::DesignVersionEventEnum,
+ null: false,
+ extras: [:parent],
+ description: 'How this design was changed in the current version'
+ field :notes_count,
+ GraphQL::INT_TYPE,
+ null: false,
+ method: :user_notes_count,
+ description: 'The total count of user-created notes for this design'
+
+ def diff_refs(parent:)
+ version = cached_stateful_version(parent)
+ version.diff_refs
+ end
+
+ def image(parent:)
+ sha = cached_stateful_version(parent).sha
+
+ Gitlab::UrlBuilder.build(design, ref: sha)
+ end
+
+ def image_v432x230(parent:)
+ version = cached_stateful_version(parent)
+ action = design.actions.up_to_version(version).most_recent.first
+
+ # A `nil` return value indicates that the image has not been processed
+ return unless action.image_v432x230.file
+
+ Gitlab::UrlBuilder.build(design, ref: version.sha, size: :v432x230)
+ end
+
+ def event(parent:)
+ version = cached_stateful_version(parent)
+
+ action = cached_actions_for_version(version)[design.id]
+
+ action&.event || ::Types::DesignManagement::DesignVersionEventEnum::NONE
+ end
+
+ def cached_actions_for_version(version)
+ Gitlab::SafeRequestStore.fetch(['DesignFields', 'actions_for_version', version.id]) do
+ version.actions.to_h { |dv| [dv.design_id, dv] }
+ end
+ end
+
+ def project
+ ::Gitlab::Graphql::Loaders::BatchModelLoader.new(::Project, design.project_id).find
+ end
+
+ def issue
+ ::Gitlab::Graphql::Loaders::BatchModelLoader.new(::Issue, design.issue_id).find
+ end
+ end
+ end
+end
diff --git a/app/graphql/types/design_management/design_type.rb b/app/graphql/types/design_management/design_type.rb
new file mode 100644
index 00000000000..3c84dc151bd
--- /dev/null
+++ b/app/graphql/types/design_management/design_type.rb
@@ -0,0 +1,44 @@
+# frozen_string_literal: true
+
+module Types
+ module DesignManagement
+ class DesignType < BaseObject
+ graphql_name 'Design'
+ description 'A single design'
+
+ authorize :read_design
+
+ alias_method :design, :object
+
+ implements(Types::Notes::NoteableType)
+ implements(Types::DesignManagement::DesignFields)
+
+ field :versions,
+ Types::DesignManagement::VersionType.connection_type,
+ resolver: Resolvers::DesignManagement::VersionsResolver,
+ description: "All versions related to this design ordered newest first",
+ extras: [:parent]
+
+ # Returns a `DesignManagement::Version` for this query based on the
+ # `atVersion` argument passed to a parent node if present, or otherwise
+ # the most recent `Version` for the issue.
+ def cached_stateful_version(parent_node)
+ version_gid = Gitlab::Graphql::FindArgumentInParent.find(parent_node, :at_version)
+
+ # Caching is scoped to an `issue_id` to allow us to cache the
+ # most recent `Version` for an issue
+ Gitlab::SafeRequestStore.fetch([request_cache_base_key, 'stateful_version', object.issue_id, version_gid]) do
+ if version_gid
+ GitlabSchema.object_from_id(version_gid)&.sync
+ else
+ object.issue.design_versions.most_recent
+ end
+ end
+ end
+
+ def request_cache_base_key
+ self.class.name
+ end
+ end
+ end
+end
diff --git a/app/graphql/types/design_management/design_version_event_enum.rb b/app/graphql/types/design_management/design_version_event_enum.rb
new file mode 100644
index 00000000000..ea4bc1ffbfa
--- /dev/null
+++ b/app/graphql/types/design_management/design_version_event_enum.rb
@@ -0,0 +1,18 @@
+# frozen_string_literal: true
+
+module Types
+ module DesignManagement
+ class DesignVersionEventEnum < BaseEnum
+ graphql_name 'DesignVersionEvent'
+ description 'Mutation event of a design within a version'
+
+ NONE = 'NONE'
+
+ value NONE, 'No change'
+
+ ::DesignManagement::Action.events.keys.each do |event_name|
+ value event_name.upcase, value: event_name, description: "A #{event_name} event"
+ end
+ end
+ end
+end
diff --git a/app/graphql/types/design_management/version_type.rb b/app/graphql/types/design_management/version_type.rb
new file mode 100644
index 00000000000..c774f5d1bdf
--- /dev/null
+++ b/app/graphql/types/design_management/version_type.rb
@@ -0,0 +1,37 @@
+# frozen_string_literal: true
+
+module Types
+ module DesignManagement
+ class VersionType < ::Types::BaseObject
+ # Just `Version` might be a bit to general to expose globally so adding
+ # a `Design` prefix to specify the class exposed in GraphQL
+ graphql_name 'DesignVersion'
+
+ description 'A specific version in which designs were added, modified or deleted'
+
+ authorize :read_design
+
+ field :id, GraphQL::ID_TYPE, null: false,
+ description: 'ID of the design version'
+ field :sha, GraphQL::ID_TYPE, null: false,
+ description: 'SHA of the design version'
+
+ field :designs,
+ ::Types::DesignManagement::DesignType.connection_type,
+ null: false,
+ description: 'All designs that were changed in the version'
+
+ field :designs_at_version,
+ ::Types::DesignManagement::DesignAtVersionType.connection_type,
+ null: false,
+ description: 'All designs that are visible at this version, as of this version',
+ resolver: ::Resolvers::DesignManagement::Version::DesignsAtVersionResolver
+
+ field :design_at_version,
+ ::Types::DesignManagement::DesignAtVersionType,
+ null: false,
+ description: 'A particular design as of this version, provided it is visible at this version',
+ resolver: ::Resolvers::DesignManagement::Version::DesignsAtVersionResolver.single
+ end
+ end
+end
diff --git a/app/graphql/types/design_management_type.rb b/app/graphql/types/design_management_type.rb
new file mode 100644
index 00000000000..ec85b8a0c1f
--- /dev/null
+++ b/app/graphql/types/design_management_type.rb
@@ -0,0 +1,18 @@
+# frozen_string_literal: true
+
+# rubocop: disable Graphql/AuthorizeTypes
+module Types
+ class DesignManagementType < BaseObject
+ graphql_name 'DesignManagement'
+
+ field :version, ::Types::DesignManagement::VersionType,
+ null: true,
+ resolver: ::Resolvers::DesignManagement::VersionResolver,
+ description: 'Find a version'
+
+ field :design_at_version, ::Types::DesignManagement::DesignAtVersionType,
+ null: true,
+ resolver: ::Resolvers::DesignManagement::DesignAtVersionResolver,
+ description: 'Find a design as of a version'
+ end
+end
diff --git a/app/graphql/types/grafana_integration_type.rb b/app/graphql/types/grafana_integration_type.rb
index c0582b266ab..7db733fc62a 100644
--- a/app/graphql/types/grafana_integration_type.rb
+++ b/app/graphql/types/grafana_integration_type.rb
@@ -9,7 +9,7 @@ module Types
field :id, GraphQL::ID_TYPE, null: false,
description: 'Internal ID of the Grafana integration'
field :grafana_url, GraphQL::STRING_TYPE, null: false,
- description: 'Url for the Grafana host for the Grafana integration'
+ description: 'URL for the Grafana host for the Grafana integration'
field :enabled, GraphQL::BOOLEAN_TYPE, null: false,
description: 'Indicates whether Grafana integration is enabled'
field :created_at, Types::TimeType, null: false,
diff --git a/app/graphql/types/issuable_sort_enum.rb b/app/graphql/types/issuable_sort_enum.rb
index 9fb1249d582..a6d52124d99 100644
--- a/app/graphql/types/issuable_sort_enum.rb
+++ b/app/graphql/types/issuable_sort_enum.rb
@@ -4,5 +4,12 @@ module Types
class IssuableSortEnum < SortEnum
graphql_name 'IssuableSort'
description 'Values for sorting issuables'
+
+ value 'PRIORITY_ASC', 'Priority by ascending order', value: :priority_asc
+ value 'PRIORITY_DESC', 'Priority by descending order', value: :priority_desc
+ value 'LABEL_PRIORITY_ASC', 'Label priority by ascending order', value: :label_priority_asc
+ value 'LABEL_PRIORITY_DESC', 'Label priority by descending order', value: :label_priority_desc
+ value 'MILESTONE_DUE_ASC', 'Milestone due date by ascending order', value: :milestone_due_asc
+ value 'MILESTONE_DUE_DESC', 'Milestone due date by descending order', value: :milestone_due_desc
end
end
diff --git a/app/graphql/types/issue_sort_enum.rb b/app/graphql/types/issue_sort_enum.rb
index c8d8f3ef079..e458d6e02c5 100644
--- a/app/graphql/types/issue_sort_enum.rb
+++ b/app/graphql/types/issue_sort_enum.rb
@@ -5,9 +5,9 @@ module Types
graphql_name 'IssueSort'
description 'Values for sorting issues'
- value 'DUE_DATE_ASC', 'Due date by ascending order', value: 'due_date_asc'
- value 'DUE_DATE_DESC', 'Due date by descending order', value: 'due_date_desc'
- value 'RELATIVE_POSITION_ASC', 'Relative position by ascending order', value: 'relative_position_asc'
+ value 'DUE_DATE_ASC', 'Due date by ascending order', value: :due_date_asc
+ value 'DUE_DATE_DESC', 'Due date by descending order', value: :due_date_desc
+ value 'RELATIVE_POSITION_ASC', 'Relative position by ascending order', value: :relative_position_asc
end
end
diff --git a/app/graphql/types/issue_type.rb b/app/graphql/types/issue_type.rb
index 11850e5865f..73219ca9e1e 100644
--- a/app/graphql/types/issue_type.rb
+++ b/app/graphql/types/issue_type.rb
@@ -85,6 +85,14 @@ module Types
field :task_completion_status, Types::TaskCompletionStatus, null: false,
description: 'Task completion status of the issue'
+
+ field :designs, Types::DesignManagement::DesignCollectionType, null: true,
+ method: :design_collection,
+ deprecated: { reason: 'Use `designCollection`', milestone: '12.2' },
+ description: 'The designs associated with this issue'
+
+ field :design_collection, Types::DesignManagement::DesignCollectionType, null: true,
+ description: 'Collection of design images associated with this issue'
end
end
diff --git a/app/graphql/types/jira_import_type.rb b/app/graphql/types/jira_import_type.rb
index ccd463370b6..4a124566ffb 100644
--- a/app/graphql/types/jira_import_type.rb
+++ b/app/graphql/types/jira_import_type.rb
@@ -7,9 +7,10 @@ module Types
class JiraImportType < BaseObject
graphql_name 'JiraImport'
- field :scheduled_at, Types::TimeType, null: true,
- method: :created_at,
+ field :created_at, Types::TimeType, null: true,
description: 'Timestamp of when the Jira import was created'
+ field :scheduled_at, Types::TimeType, null: true,
+ description: 'Timestamp of when the Jira import was scheduled'
field :scheduled_by, Types::UserType, null: true,
description: 'User that started the Jira import'
field :jira_project_key, GraphQL::STRING_TYPE, null: false,
diff --git a/app/graphql/types/metrics/dashboard_type.rb b/app/graphql/types/metrics/dashboard_type.rb
index e7d09866bb5..d684533ff94 100644
--- a/app/graphql/types/metrics/dashboard_type.rb
+++ b/app/graphql/types/metrics/dashboard_type.rb
@@ -11,8 +11,7 @@ module Types
description: 'Path to a file with the dashboard definition'
field :annotations, Types::Metrics::Dashboards::AnnotationType.connection_type, null: true,
- description: 'Annotations added to the dashboard. Will always return `null` ' \
- 'if `metrics_dashboard_annotations` feature flag is disabled',
+ description: 'Annotations added to the dashboard',
resolver: Resolvers::Metrics::Dashboards::AnnotationResolver
end
# rubocop: enable Graphql/AuthorizeTypes
diff --git a/app/graphql/types/metrics/dashboards/annotation_type.rb b/app/graphql/types/metrics/dashboards/annotation_type.rb
index 055d2544eff..0f8f95c187b 100644
--- a/app/graphql/types/metrics/dashboards/annotation_type.rb
+++ b/app/graphql/types/metrics/dashboards/annotation_type.rb
@@ -16,10 +16,10 @@ module Types
field :panel_id, GraphQL::STRING_TYPE, null: true,
description: 'ID of a dashboard panel to which the annotation should be scoped'
- field :starting_at, GraphQL::STRING_TYPE, null: true,
+ field :starting_at, Types::TimeType, null: true,
description: 'Timestamp marking start of annotated time span'
- field :ending_at, GraphQL::STRING_TYPE, null: true,
+ field :ending_at, Types::TimeType, null: true,
description: 'Timestamp marking end of annotated time span'
def panel_id
diff --git a/app/graphql/types/mutation_type.rb b/app/graphql/types/mutation_type.rb
index ab25d5baf71..aeff84b83b8 100644
--- a/app/graphql/types/mutation_type.rb
+++ b/app/graphql/types/mutation_type.rb
@@ -7,9 +7,12 @@ module Types
graphql_name 'Mutation'
mount_mutation Mutations::Admin::SidekiqQueues::DeleteJobs
+ mount_mutation Mutations::AlertManagement::CreateAlertIssue
+ mount_mutation Mutations::AlertManagement::UpdateAlertStatus
mount_mutation Mutations::AwardEmojis::Add
mount_mutation Mutations::AwardEmojis::Remove
mount_mutation Mutations::AwardEmojis::Toggle
+ mount_mutation Mutations::Branches::Create, calls_gitaly: true
mount_mutation Mutations::Issues::SetConfidential
mount_mutation Mutations::Issues::SetDueDate
mount_mutation Mutations::Issues::Update
@@ -19,6 +22,7 @@ module Types
mount_mutation Mutations::MergeRequests::SetSubscription
mount_mutation Mutations::MergeRequests::SetWip, calls_gitaly: true
mount_mutation Mutations::MergeRequests::SetAssignees
+ mount_mutation Mutations::Metrics::Dashboard::Annotations::Create
mount_mutation Mutations::Notes::Create::Note, calls_gitaly: true
mount_mutation Mutations::Notes::Create::DiffNote, calls_gitaly: true
mount_mutation Mutations::Notes::Create::ImageDiffNote, calls_gitaly: true
@@ -40,6 +44,8 @@ module Types
mount_mutation Mutations::Snippets::Create
mount_mutation Mutations::Snippets::MarkAsSpam
mount_mutation Mutations::JiraImport::Start
+ mount_mutation Mutations::DesignManagement::Upload, calls_gitaly: true
+ mount_mutation Mutations::DesignManagement::Delete, calls_gitaly: true
end
end
diff --git a/app/graphql/types/notes/noteable_type.rb b/app/graphql/types/notes/noteable_type.rb
index 2ac66452841..187c9109f8c 100644
--- a/app/graphql/types/notes/noteable_type.rb
+++ b/app/graphql/types/notes/noteable_type.rb
@@ -17,6 +17,8 @@ module Types
Types::MergeRequestType
when Snippet
Types::SnippetType
+ when ::DesignManagement::Design
+ Types::DesignManagement::DesignType
else
raise "Unknown GraphQL type for #{object}"
end
@@ -25,5 +27,3 @@ module Types
end
end
end
-
-Types::Notes::NoteableType.extend_if_ee('::EE::Types::Notes::NoteableType')
diff --git a/app/graphql/types/permission_types/issue.rb b/app/graphql/types/permission_types/issue.rb
index e26c5950e73..94e1bffd685 100644
--- a/app/graphql/types/permission_types/issue.rb
+++ b/app/graphql/types/permission_types/issue.rb
@@ -6,11 +6,9 @@ module Types
description 'Check permissions for the current user on a issue'
graphql_name 'IssuePermissions'
- abilities :read_issue, :admin_issue,
- :update_issue, :create_note,
- :reopen_issue
+ abilities :read_issue, :admin_issue, :update_issue, :reopen_issue,
+ :read_design, :create_design, :destroy_design,
+ :create_note
end
end
end
-
-Types::PermissionTypes::Issue.prepend_if_ee('::EE::Types::PermissionTypes::Issue')
diff --git a/app/graphql/types/permission_types/project.rb b/app/graphql/types/permission_types/project.rb
index f773fce0c63..5747e63d195 100644
--- a/app/graphql/types/permission_types/project.rb
+++ b/app/graphql/types/permission_types/project.rb
@@ -17,7 +17,7 @@ module Types
:admin_wiki, :admin_project, :update_pages,
:admin_remote_mirror, :create_label, :update_wiki, :destroy_wiki,
:create_pages, :destroy_pages, :read_pages_content, :admin_operations,
- :read_merge_request
+ :read_merge_request, :read_design, :create_design, :destroy_design
permission_field :create_snippet
@@ -27,5 +27,3 @@ module Types
end
end
end
-
-Types::PermissionTypes::Project.prepend_if_ee('EE::Types::PermissionTypes::Project')
diff --git a/app/graphql/types/project_type.rb b/app/graphql/types/project_type.rb
index 8356e763be9..4e438ed2576 100644
--- a/app/graphql/types/project_type.rb
+++ b/app/graphql/types/project_type.rb
@@ -205,6 +205,38 @@ module Types
null: true,
description: 'Project services',
resolver: Resolvers::Projects::ServicesResolver
+
+ field :alert_management_alerts,
+ Types::AlertManagement::AlertType.connection_type,
+ null: true,
+ description: 'Alert Management alerts of the project',
+ resolver: Resolvers::AlertManagementAlertResolver
+
+ field :alert_management_alert,
+ Types::AlertManagement::AlertType,
+ null: true,
+ description: 'A single Alert Management alert of the project',
+ resolver: Resolvers::AlertManagementAlertResolver.single
+
+ field :alert_management_alert_status_counts,
+ Types::AlertManagement::AlertStatusCountsType,
+ null: true,
+ description: 'Counts of alerts by status for the project',
+ resolver: Resolvers::AlertManagement::AlertStatusCountsResolver
+
+ field :releases,
+ Types::ReleaseType.connection_type,
+ null: true,
+ description: 'Releases of the project',
+ resolver: Resolvers::ReleasesResolver,
+ feature_flag: :graphql_release_data
+
+ field :release,
+ Types::ReleaseType,
+ null: true,
+ description: 'A single release of the project',
+ resolver: Resolvers::ReleasesResolver.single,
+ feature_flag: :graphql_release_data
end
end
diff --git a/app/graphql/types/query_type.rb b/app/graphql/types/query_type.rb
index e8f6eeff3e9..70cdcb62bc6 100644
--- a/app/graphql/types/query_type.rb
+++ b/app/graphql/types/query_type.rb
@@ -4,11 +4,19 @@ module Types
class QueryType < ::Types::BaseObject
graphql_name 'Query'
+ # The design management context object needs to implement #issue
+ DesignManagementObject = Struct.new(:issue)
+
field :project, Types::ProjectType,
null: true,
resolver: Resolvers::ProjectResolver,
description: "Find a project"
+ field :projects, Types::ProjectType.connection_type,
+ null: true,
+ resolver: Resolvers::ProjectsResolver,
+ description: "Find projects visible to the current user"
+
field :group, Types::GroupType,
null: true,
resolver: Resolvers::GroupResolver,
@@ -35,9 +43,17 @@ module Types
resolver: Resolvers::SnippetsResolver,
description: 'Find Snippets visible to the current user'
+ field :design_management, Types::DesignManagementType,
+ null: false,
+ description: 'Fields related to design management'
+
field :echo, GraphQL::STRING_TYPE, null: false,
description: 'Text to echo back',
resolver: Resolvers::EchoResolver
+
+ def design_management
+ DesignManagementObject.new(nil)
+ end
end
end
diff --git a/app/graphql/types/release_type.rb b/app/graphql/types/release_type.rb
new file mode 100644
index 00000000000..632351be5d3
--- /dev/null
+++ b/app/graphql/types/release_type.rb
@@ -0,0 +1,47 @@
+# frozen_string_literal: true
+
+module Types
+ class ReleaseType < BaseObject
+ graphql_name 'Release'
+
+ authorize :read_release
+
+ alias_method :release, :object
+
+ present_using ReleasePresenter
+
+ field :tag_name, GraphQL::STRING_TYPE, null: false, method: :tag,
+ description: 'Name of the tag associated with the release'
+ field :tag_path, GraphQL::STRING_TYPE, null: true,
+ description: 'Relative web path to the tag associated with the release'
+ field :description, GraphQL::STRING_TYPE, null: true,
+ description: 'Description (also known as "release notes") of the release'
+ markdown_field :description_html, null: true
+ field :name, GraphQL::STRING_TYPE, null: true,
+ description: 'Name of the release'
+ field :created_at, Types::TimeType, null: true,
+ description: 'Timestamp of when the release was created'
+ field :released_at, Types::TimeType, null: true,
+ description: 'Timestamp of when the release was released'
+ field :milestones, Types::MilestoneType.connection_type, null: true,
+ description: 'Milestones associated to the release'
+
+ field :author, Types::UserType, null: true,
+ description: 'User that created the release'
+
+ def author
+ Gitlab::Graphql::Loaders::BatchModelLoader.new(User, release.author_id).find
+ end
+
+ field :commit, Types::CommitType, null: true,
+ complexity: 10, calls_gitaly: true,
+ description: 'The commit associated with the release',
+ authorize: :reporter_access
+
+ def commit
+ return if release.sha.nil?
+
+ release.project.commit_by(oid: release.sha)
+ end
+ end
+end
diff --git a/app/graphql/types/snippet_type.rb b/app/graphql/types/snippet_type.rb
index 4ebdbd5766c..b23c4f71ffa 100644
--- a/app/graphql/types/snippet_type.rb
+++ b/app/graphql/types/snippet_type.rb
@@ -14,7 +14,7 @@ module Types
expose_permissions Types::PermissionTypes::Snippet
field :id, GraphQL::ID_TYPE,
- description: 'Id of the snippet',
+ description: 'ID of the snippet',
null: false
field :title, GraphQL::STRING_TYPE,
diff --git a/app/graphql/types/snippets/blob_type.rb b/app/graphql/types/snippets/blob_type.rb
index feff5d20874..dcde1e5a73b 100644
--- a/app/graphql/types/snippets/blob_type.rb
+++ b/app/graphql/types/snippets/blob_type.rb
@@ -14,6 +14,7 @@ module Types
field :plain_data, GraphQL::STRING_TYPE,
description: 'Blob plain highlighted data',
+ calls_gitaly: true,
null: true
field :raw_path, GraphQL::STRING_TYPE,
@@ -48,6 +49,15 @@ module Types
field :mode, type: GraphQL::STRING_TYPE,
description: 'Blob mode',
null: true
+
+ field :external_storage, type: GraphQL::STRING_TYPE,
+ description: 'Blob external storage',
+ null: true
+
+ field :rendered_as_text, type: GraphQL::BOOLEAN_TYPE,
+ description: 'Shows whether the blob is rendered as text',
+ method: :rendered_as_text?,
+ null: false
end
# rubocop: enable Graphql/AuthorizeTypes
end
diff --git a/app/graphql/types/snippets/blob_viewer_type.rb b/app/graphql/types/snippets/blob_viewer_type.rb
index 3e653576d07..50d0b0522d6 100644
--- a/app/graphql/types/snippets/blob_viewer_type.rb
+++ b/app/graphql/types/snippets/blob_viewer_type.rb
@@ -17,12 +17,14 @@ module Types
field :collapsed, GraphQL::BOOLEAN_TYPE,
description: 'Shows whether the blob should be displayed collapsed',
method: :collapsed?,
- null: false
+ null: false,
+ resolve: -> (viewer, _args, _ctx) { !!viewer&.collapsed? }
field :too_large, GraphQL::BOOLEAN_TYPE,
description: 'Shows whether the blob too large to be displayed',
method: :too_large?,
- null: false
+ null: false,
+ resolve: -> (viewer, _args, _ctx) { !!viewer&.too_large? }
field :render_error, GraphQL::STRING_TYPE,
description: 'Error rendering the blob content',
diff --git a/app/graphql/types/todo_target_enum.rb b/app/graphql/types/todo_target_enum.rb
index 8358a86b35c..a377c3aafdc 100644
--- a/app/graphql/types/todo_target_enum.rb
+++ b/app/graphql/types/todo_target_enum.rb
@@ -5,6 +5,7 @@ module Types
value 'COMMIT', value: 'Commit', description: 'A Commit'
value 'ISSUE', value: 'Issue', description: 'An Issue'
value 'MERGEREQUEST', value: 'MergeRequest', description: 'A MergeRequest'
+ value 'DESIGN', value: 'DesignManagement::Design', description: 'A Design'
end
end
diff --git a/app/graphql/types/todo_type.rb b/app/graphql/types/todo_type.rb
index 5ce5093c55e..08e7fabeb74 100644
--- a/app/graphql/types/todo_type.rb
+++ b/app/graphql/types/todo_type.rb
@@ -10,7 +10,7 @@ module Types
authorize :read_todo
field :id, GraphQL::ID_TYPE,
- description: 'Id of the todo',
+ description: 'ID of the todo',
null: false
field :project, Types::ProjectType,
diff --git a/app/graphql/types/user_type.rb b/app/graphql/types/user_type.rb
index e530641d6ae..29a3f5d452f 100644
--- a/app/graphql/types/user_type.rb
+++ b/app/graphql/types/user_type.rb
@@ -10,8 +10,12 @@ module Types
expose_permissions Types::PermissionTypes::User
+ field :id, GraphQL::ID_TYPE, null: false,
+ description: 'ID of the user'
field :name, GraphQL::STRING_TYPE, null: false,
description: 'Human-readable name of the user'
+ field :state, GraphQL::STRING_TYPE, null: false,
+ description: 'State of the issue'
field :username, GraphQL::STRING_TYPE, null: false,
description: 'Username of the user. Unique within this instance of GitLab'
field :avatar_url, GraphQL::STRING_TYPE, null: true,
diff --git a/app/helpers/access_tokens_helper.rb b/app/helpers/access_tokens_helper.rb
new file mode 100644
index 00000000000..877ad6db576
--- /dev/null
+++ b/app/helpers/access_tokens_helper.rb
@@ -0,0 +1,7 @@
+# frozen_string_literal: true
+
+module AccessTokensHelper
+ def scope_description(prefix)
+ prefix == :project_access_token ? [:doorkeeper, :project_access_token_scope_desc] : [:doorkeeper, :scope_desc]
+ end
+end
diff --git a/app/helpers/appearances_helper.rb b/app/helpers/appearances_helper.rb
index 0c1b2c7d093..3ae9f93a27a 100644
--- a/app/helpers/appearances_helper.rb
+++ b/app/helpers/appearances_helper.rb
@@ -25,6 +25,10 @@ module AppearancesHelper
markdown_field(current_appearance, :new_project_guidelines)
end
+ def brand_profile_image_guidelines
+ markdown_field(current_appearance, :profile_image_guidelines)
+ end
+
def current_appearance
strong_memoize(:current_appearance) do
Appearance.current
diff --git a/app/helpers/application_helper.rb b/app/helpers/application_helper.rb
index a815b378f8b..2df33073a89 100644
--- a/app/helpers/application_helper.rb
+++ b/app/helpers/application_helper.rb
@@ -54,6 +54,10 @@ module ApplicationHelper
args.any? { |v| v.to_s.downcase == action_name }
end
+ def admin_section?
+ controller.class.ancestors.include?(Admin::ApplicationController)
+ end
+
def last_commit(project)
if project.repo_exists?
time_ago_with_tooltip(project.repository.commit.committed_date)
diff --git a/app/helpers/application_settings_helper.rb b/app/helpers/application_settings_helper.rb
index 443451cd394..b9f0e3582df 100644
--- a/app/helpers/application_settings_helper.rb
+++ b/app/helpers/application_settings_helper.rb
@@ -229,14 +229,7 @@ module ApplicationSettingsHelper
:max_artifacts_size,
:max_attachment_size,
:max_pages_size,
- :metrics_enabled,
- :metrics_host,
:metrics_method_call_threshold,
- :metrics_packet_size,
- :metrics_pool_size,
- :metrics_port,
- :metrics_sample_interval,
- :metrics_timeout,
:minimum_password_length,
:mirror_available,
:pages_domain_verification_enabled,
@@ -310,7 +303,9 @@ module ApplicationSettingsHelper
:custom_http_clone_url_root,
:snippet_size_limit,
:email_restrictions_enabled,
- :email_restrictions
+ :email_restrictions,
+ :issues_create_limit,
+ :raw_blob_request_limit
]
end
@@ -365,7 +360,7 @@ module ApplicationSettingsHelper
end
end
-ApplicationSettingsHelper.prepend_if_ee('EE::ApplicationSettingsHelper') # rubocop: disable Cop/InjectEnterpriseEditionModule
+ApplicationSettingsHelper.prepend_if_ee('EE::ApplicationSettingsHelper')
# The methods in `EE::ApplicationSettingsHelper` should be available as both
# instance and class methods.
diff --git a/app/helpers/auth_helper.rb b/app/helpers/auth_helper.rb
index 1f1ff75359d..a57e27d23c8 100644
--- a/app/helpers/auth_helper.rb
+++ b/app/helpers/auth_helper.rb
@@ -145,10 +145,14 @@ module AuthHelper
IdentityProviderPolicy.new(current_user, provider).can?(:link)
end
+ def allow_admin_mode_password_authentication_for_web?
+ current_user.allow_password_authentication_for_web? && !current_user.password_automatically_set?
+ end
+
extend self
end
-AuthHelper.prepend_if_ee('EE::AuthHelper') # rubocop: disable Cop/InjectEnterpriseEditionModule
+AuthHelper.prepend_if_ee('EE::AuthHelper')
# The methods added in EE should be available as both class and instance
# methods, just like the methods provided by `AuthHelper` itself.
diff --git a/app/helpers/blob_helper.rb b/app/helpers/blob_helper.rb
index 4debf66db64..69fe3303840 100644
--- a/app/helpers/blob_helper.rb
+++ b/app/helpers/blob_helper.rb
@@ -17,7 +17,7 @@ module BlobHelper
options[:link_opts])
end
- def ide_edit_path(project = @project, ref = @ref, path = @path, options = {})
+ def ide_edit_path(project = @project, ref = @ref, path = @path)
project_path =
if !current_user || can?(current_user, :push_code, project)
project.full_path
@@ -52,28 +52,25 @@ module BlobHelper
edit_button_tag(blob,
common_classes,
_('Edit'),
- Feature.enabled?(:web_ide_default) ? ide_edit_path(project, ref, path, options) : edit_blob_path(project, ref, path, options),
+ Feature.enabled?(:web_ide_default) ? ide_edit_path(project, ref, path) : edit_blob_path(project, ref, path, options),
project,
ref)
end
- def ide_edit_button(project = @project, ref = @ref, path = @path, options = {})
+ def ide_edit_button(project = @project, ref = @ref, path = @path, blob:)
return if Feature.enabled?(:web_ide_default)
- return unless blob = readable_blob(options, path, project, ref)
+ return unless blob
edit_button_tag(blob,
'btn btn-inverted btn-primary ide-edit-button ml-2',
_('Web IDE'),
- ide_edit_path(project, ref, path, options),
+ ide_edit_path(project, ref, path),
project,
ref)
end
- def modify_file_button(project = @project, ref = @ref, path = @path, label:, action:, btn_class:, modal_type:)
+ def modify_file_button(project = @project, ref = @ref, path = @path, blob:, label:, action:, btn_class:, modal_type:)
return unless current_user
-
- blob = project.repository.blob_at(ref, path) rescue nil
-
return unless blob
common_classes = "btn btn-#{btn_class}"
@@ -89,11 +86,12 @@ module BlobHelper
end
end
- def replace_blob_link(project = @project, ref = @ref, path = @path)
+ def replace_blob_link(project = @project, ref = @ref, path = @path, blob:)
modify_file_button(
project,
ref,
path,
+ blob: blob,
label: _("Replace"),
action: "replace",
btn_class: "default",
@@ -101,11 +99,12 @@ module BlobHelper
)
end
- def delete_blob_link(project = @project, ref = @ref, path = @path)
+ def delete_blob_link(project = @project, ref = @ref, path = @path, blob:)
modify_file_button(
project,
ref,
path,
+ blob: blob,
label: _("Delete"),
action: "delete",
btn_class: "default",
diff --git a/app/helpers/boards_helper.rb b/app/helpers/boards_helper.rb
index c14bc454bb9..f8c00f3a4cd 100644
--- a/app/helpers/boards_helper.rb
+++ b/app/helpers/boards_helper.rb
@@ -16,7 +16,8 @@ module BoardsHelper
full_path: full_path,
bulk_update_path: @bulk_issues_path,
time_tracking_limit_to_hours: Gitlab::CurrentSettings.time_tracking_limit_to_hours.to_s,
- recent_boards_endpoint: recent_boards_path
+ recent_boards_endpoint: recent_boards_path,
+ parent: current_board_parent.model_name.param_key
}
end
diff --git a/app/helpers/button_helper.rb b/app/helpers/button_helper.rb
index e1aed5393ea..c999d1f94ad 100644
--- a/app/helpers/button_helper.rb
+++ b/app/helpers/button_helper.rb
@@ -93,8 +93,8 @@ module ButtonHelper
content_tag (href ? :a : :span),
(href ? button_content : title),
class: "#{title.downcase}-selector #{active_class}",
- href: (href if href),
- data: (data if data)
+ href: href,
+ data: data
end
end
diff --git a/app/helpers/clusters_helper.rb b/app/helpers/clusters_helper.rb
index a97216f8a22..39aaf242231 100644
--- a/app/helpers/clusters_helper.rb
+++ b/app/helpers/clusters_helper.rb
@@ -17,6 +17,17 @@ module ClustersHelper
end
end
+ def provider_icon(provider = nil)
+ case provider
+ when 'aws'
+ image_tag 'illustrations/logos/amazon_eks.svg', alt: s_('ClusterIntegration|Amazon EKS'), class: 'gl-h-full'
+ when 'gcp'
+ image_tag 'illustrations/logos/google_gke.svg', alt: s_('ClusterIntegration|Google GKE'), class: 'gl-h-full'
+ else
+ image_tag 'illustrations/logos/kubernetes.svg', alt: _('Kubernetes Cluster'), class: 'gl-h-full'
+ end
+ end
+
def render_gcp_signup_offer
return if Gitlab::CurrentSettings.current_application_settings.hide_third_party_offers?
return unless show_gcp_signup_offer?
diff --git a/app/helpers/commits_helper.rb b/app/helpers/commits_helper.rb
index ace8bae03ac..2a0c2e73dd6 100644
--- a/app/helpers/commits_helper.rb
+++ b/app/helpers/commits_helper.rb
@@ -215,6 +215,8 @@ module CommitsHelper
def commit_path(project, commit, merge_request: nil)
if merge_request&.persisted?
diffs_project_merge_request_path(project, merge_request, commit_id: commit.id)
+ elsif merge_request
+ project_commit_path(merge_request&.source_project, commit)
else
project_commit_path(project, commit)
end
diff --git a/app/helpers/environment_helper.rb b/app/helpers/environment_helper.rb
index 52f189b122f..bd400009c96 100644
--- a/app/helpers/environment_helper.rb
+++ b/app/helpers/environment_helper.rb
@@ -25,7 +25,7 @@ module EnvironmentHelper
def deployment_link(deployment, text: nil)
return unless deployment
- link_label = text ? text : "##{deployment.iid}"
+ link_label = text || "##{deployment.iid}"
link_to link_label, deployment_path(deployment)
end
diff --git a/app/helpers/environments_helper.rb b/app/helpers/environments_helper.rb
index 5b640ea6538..e7b561af3da 100644
--- a/app/helpers/environments_helper.rb
+++ b/app/helpers/environments_helper.rb
@@ -2,7 +2,6 @@
module EnvironmentsHelper
include ActionView::Helpers::AssetUrlHelper
- prepend_if_ee('::EE::EnvironmentsHelper') # rubocop: disable Cop/InjectEnterpriseEditionModule
def environments_list_data
{
@@ -23,31 +22,13 @@ module EnvironmentsHelper
end
def metrics_data(project, environment)
- {
- "settings-path" => edit_project_service_path(project, 'prometheus'),
- "clusters-path" => project_clusters_path(project),
- "current-environment-name" => environment.name,
- "documentation-path" => help_page_path('administration/monitoring/prometheus/index.md'),
- "empty-getting-started-svg-path" => image_path('illustrations/monitoring/getting_started.svg'),
- "empty-loading-svg-path" => image_path('illustrations/monitoring/loading.svg'),
- "empty-no-data-svg-path" => image_path('illustrations/monitoring/no_data.svg'),
- "empty-no-data-small-svg-path" => image_path('illustrations/chart-empty-state-small.svg'),
- "empty-unable-to-connect-svg-path" => image_path('illustrations/monitoring/unable_to_connect.svg'),
- "metrics-endpoint" => additional_metrics_project_environment_path(project, environment, format: :json),
- "dashboards-endpoint" => project_performance_monitoring_dashboards_path(project, format: :json),
- "dashboard-endpoint" => metrics_dashboard_project_environment_path(project, environment, format: :json),
- "deployments-endpoint" => project_environment_deployments_path(project, environment, format: :json),
- "default-branch" => project.default_branch,
- "project-path" => project_path(project),
- "tags-path" => project_tags_path(project),
- "has-metrics" => "#{environment.has_metrics?}",
- "prometheus-status" => "#{environment.prometheus_status}",
- "external-dashboard-url" => project.metrics_setting_external_dashboard_url,
- "environment-state" => "#{environment.state}",
- "custom-metrics-path" => project_prometheus_metrics_path(project),
- "validate-query-path" => validate_query_project_prometheus_metrics_path(project),
- "custom-metrics-available" => "#{custom_metrics_available?(project)}"
- }
+ metrics_data = {}
+ metrics_data.merge!(project_metrics_data(project)) if project
+ metrics_data.merge!(environment_metrics_data(environment)) if environment
+ metrics_data.merge!(project_and_environment_metrics_data(project, environment)) if project && environment
+ metrics_data.merge!(static_metrics_data)
+
+ metrics_data
end
def environment_logs_data(project, environment)
@@ -62,4 +43,60 @@ module EnvironmentsHelper
def can_destroy_environment?(environment)
can?(current_user, :destroy_environment, environment)
end
+
+ private
+
+ def project_metrics_data(project)
+ return {} unless project
+
+ {
+ 'settings-path' => edit_project_service_path(project, 'prometheus'),
+ 'clusters-path' => project_clusters_path(project),
+ 'dashboards-endpoint' => project_performance_monitoring_dashboards_path(project, format: :json),
+ 'default-branch' => project.default_branch,
+ 'project-path' => project_path(project),
+ 'tags-path' => project_tags_path(project),
+ 'external-dashboard-url' => project.metrics_setting_external_dashboard_url,
+ 'custom-metrics-path' => project_prometheus_metrics_path(project),
+ 'validate-query-path' => validate_query_project_prometheus_metrics_path(project),
+ 'custom-metrics-available' => "#{custom_metrics_available?(project)}",
+ 'prometheus-alerts-available' => "#{can?(current_user, :read_prometheus_alerts, project)}"
+ }
+ end
+
+ def environment_metrics_data(environment)
+ return {} unless environment
+
+ {
+ 'current-environment-name' => environment.name,
+ 'has-metrics' => "#{environment.has_metrics?}",
+ 'prometheus-status' => "#{environment.prometheus_status}",
+ 'environment-state' => "#{environment.state}"
+ }
+ end
+
+ def project_and_environment_metrics_data(project, environment)
+ return {} unless project && environment
+
+ {
+ 'metrics-endpoint' => additional_metrics_project_environment_path(project, environment, format: :json),
+ 'dashboard-endpoint' => metrics_dashboard_project_environment_path(project, environment, format: :json),
+ 'deployments-endpoint' => project_environment_deployments_path(project, environment, format: :json),
+ 'alerts-endpoint' => project_prometheus_alerts_path(project, environment_id: environment.id, format: :json)
+
+ }
+ end
+
+ def static_metrics_data
+ {
+ 'documentation-path' => help_page_path('administration/monitoring/prometheus/index.md'),
+ 'empty-getting-started-svg-path' => image_path('illustrations/monitoring/getting_started.svg'),
+ 'empty-loading-svg-path' => image_path('illustrations/monitoring/loading.svg'),
+ 'empty-no-data-svg-path' => image_path('illustrations/monitoring/no_data.svg'),
+ 'empty-no-data-small-svg-path' => image_path('illustrations/chart-empty-state-small.svg'),
+ 'empty-unable-to-connect-svg-path' => image_path('illustrations/monitoring/unable_to_connect.svg')
+ }
+ end
end
+
+EnvironmentsHelper.prepend_if_ee('::EE::EnvironmentsHelper')
diff --git a/app/helpers/events_helper.rb b/app/helpers/events_helper.rb
index ba8e046f504..e93aeba6dfd 100644
--- a/app/helpers/events_helper.rb
+++ b/app/helpers/events_helper.rb
@@ -4,12 +4,14 @@ module EventsHelper
ICON_NAMES_BY_EVENT_TYPE = {
'pushed to' => 'commit',
'pushed new' => 'commit',
+ 'updated' => 'commit',
'created' => 'status_open',
'opened' => 'status_open',
'closed' => 'status_closed',
'accepted' => 'fork',
'commented on' => 'comment',
'deleted' => 'remove',
+ 'destroyed' => 'remove',
'imported' => 'import',
'joined' => 'users'
}.freeze
@@ -167,6 +169,8 @@ module EventsHelper
project_issue_url(event.project, id: event.note_target, anchor: dom_id(event.target))
elsif event.merge_request_note?
project_merge_request_url(event.project, id: event.note_target, anchor: dom_id(event.target))
+ elsif event.design_note?
+ design_url(event.note_target, anchor: dom_id(event.note))
else
polymorphic_url([event.project.namespace.becomes(Namespace),
event.project, event.note_target],
@@ -237,6 +241,16 @@ module EventsHelper
concat content_tag(:span, event.author.to_reference, class: "username")
end
end
+
+ private
+
+ def design_url(design, opts)
+ designs_project_issue_url(
+ design.project,
+ design.issue,
+ opts.merge(vueroute: design.filename)
+ )
+ end
end
EventsHelper.prepend_if_ee('EE::EventsHelper')
diff --git a/app/helpers/export_helper.rb b/app/helpers/export_helper.rb
index d03fa6eadb2..483b350b99b 100644
--- a/app/helpers/export_helper.rb
+++ b/app/helpers/export_helper.rb
@@ -9,7 +9,18 @@ module ExportHelper
_('Project configuration, including services'),
_('Issues with comments, merge requests with diffs and comments, labels, milestones, snippets, and other project entities'),
_('LFS objects'),
- _('Issue Boards')
+ _('Issue Boards'),
+ _('Design Management files and data')
+ ]
+ end
+
+ def group_export_descriptions
+ [
+ _('Milestones'),
+ _('Labels'),
+ _('Boards and Board Lists'),
+ _('Badges'),
+ _('Subgroups')
]
end
end
diff --git a/app/helpers/form_helper.rb b/app/helpers/form_helper.rb
index b611f700d21..ecacde65c10 100644
--- a/app/helpers/form_helper.rb
+++ b/app/helpers/form_helper.rb
@@ -1,8 +1,6 @@
# frozen_string_literal: true
module FormHelper
- prepend_if_ee('::EE::FormHelper') # rubocop: disable Cop/InjectEnterpriseEditionModule
-
def form_errors(model, type: 'form', truncate: [])
return unless model.errors.any?
@@ -79,3 +77,5 @@ module FormHelper
new_options
end
end
+
+FormHelper.prepend_if_ee('::EE::FormHelper')
diff --git a/app/helpers/groups_helper.rb b/app/helpers/groups_helper.rb
index 91f8bc33e3e..a6c3c97a873 100644
--- a/app/helpers/groups_helper.rb
+++ b/app/helpers/groups_helper.rb
@@ -49,6 +49,10 @@ module GroupsHelper
can?(current_user, :change_visibility_level, group)
end
+ def can_update_default_branch_protection?(group)
+ can?(current_user, :update_default_branch_protection, group)
+ end
+
def can_change_share_with_group_lock?(group)
can?(current_user, :change_share_with_group_lock, group)
end
diff --git a/app/helpers/icons_helper.rb b/app/helpers/icons_helper.rb
index 876789e0d4a..8a32d3c8a3f 100644
--- a/app/helpers/icons_helper.rb
+++ b/app/helpers/icons_helper.rb
@@ -158,6 +158,6 @@ module IconsHelper
def known_sprites
return if Rails.env.production?
- @known_sprites ||= JSON.parse(File.read(Rails.root.join('node_modules/@gitlab/svgs/dist/icons.json')))['icons']
+ @known_sprites ||= Gitlab::Json.parse(File.read(Rails.root.join('node_modules/@gitlab/svgs/dist/icons.json')))['icons']
end
end
diff --git a/app/helpers/issuables_helper.rb b/app/helpers/issuables_helper.rb
index 7e0cc591308..1ce99652463 100644
--- a/app/helpers/issuables_helper.rb
+++ b/app/helpers/issuables_helper.rb
@@ -196,7 +196,7 @@ module IssuablesHelper
author_output = link_to_member(project, issuable.author, size: 24, mobile_classes: "d-none d-sm-inline")
author_output << link_to_member(project, issuable.author, size: 24, by_username: true, avatar: false, mobile_classes: "d-inline d-sm-none")
- author_output << gitlab_team_member_badge(issuable.author, css_class: 'ml-1')
+ author_output << issuable_meta_author_slot(issuable.author, css_class: 'ml-1')
if status = user_status(issuable.author)
author_output << "#{status}".html_safe
@@ -213,6 +213,11 @@ module IssuablesHelper
output.join.html_safe
end
+ # This is a dummy method, and has an override defined in ee
+ def issuable_meta_author_slot(author, css_class: nil)
+ nil
+ end
+
def issuable_labels_tooltip(labels, limit: 5)
first, last = labels.partition.with_index { |_, i| i < limit }
@@ -242,27 +247,6 @@ module IssuablesHelper
html.html_safe
end
- def gitlab_team_member_badge(author, css_class: nil)
- return unless author.gitlab_employee?
-
- default_css_class = 'd-inline-block align-middle'
- gitlab_team_member = _('GitLab Team Member')
-
- content_tag(
- :span,
- class: css_class ? "#{default_css_class} #{css_class}" : default_css_class,
- data: { toggle: 'tooltip', title: gitlab_team_member, container: 'body' },
- role: 'img',
- aria: { label: gitlab_team_member }
- ) do
- sprite_icon(
- 'tanuki-verified',
- size: 16,
- css_class: 'gl-text-purple d-block'
- )
- end
- end
-
def issuable_first_contribution_icon
content_tag(:span, class: 'fa-stack') do
concat(icon('certificate', class: "fa-stack-2x"))
diff --git a/app/helpers/issues_helper.rb b/app/helpers/issues_helper.rb
index 34b6ba05a62..39edfeea81e 100644
--- a/app/helpers/issues_helper.rb
+++ b/app/helpers/issues_helper.rb
@@ -9,13 +9,6 @@ module IssuesHelper
classes.join(' ')
end
- # Returns an OpenStruct object suitable for use by <tt>options_from_collection_for_select</tt>
- # to allow filtering issues by an unassigned User or Milestone
- def unassigned_filter
- # Milestone uses :title, Issue uses :name
- OpenStruct.new(id: 0, title: 'None (backlog)', name: 'Unassigned')
- end
-
def url_for_issue(issue_iid, project = @project, options = {})
return '' if project.nil?
@@ -145,17 +138,12 @@ module IssuesHelper
can?(current_user, :create_issue, project)
end
- def create_confidential_merge_request_enabled?
- Feature.enabled?(:create_confidential_merge_request, @project, default_enabled: true)
- end
-
def show_new_branch_button?
can_create_confidential_merge_request? || !@issue.confidential?
end
def can_create_confidential_merge_request?
@issue.confidential? && !@project.private? &&
- create_confidential_merge_request_enabled? &&
can?(current_user, :create_merge_request_in, @project)
end
@@ -177,6 +165,10 @@ module IssuesHelper
end
end
+ def show_moved_service_desk_issue_warning?(issue)
+ false
+ end
+
# Required for Banzai::Filter::IssueReferenceFilter
module_function :url_for_issue
module_function :url_for_internal_issue
diff --git a/app/helpers/members_helper.rb b/app/helpers/members_helper.rb
index 11d5591d509..31995c27fac 100644
--- a/app/helpers/members_helper.rb
+++ b/app/helpers/members_helper.rb
@@ -15,7 +15,18 @@ module MembersHelper
elsif member.invite?
"revoke the invitation for #{member.invite_email} to join"
else
- "remove #{member.user.name} from"
+ if member.user
+ "remove #{member.user.name} from"
+ else
+ e = RuntimeError.new("Data integrity error: no associated user for member ID #{member.id}")
+ Gitlab::ErrorTracking.track_exception(e,
+ member_id: member.id,
+ invite_email: member.invite_email,
+ invite_accepted_at: member.invite_accepted_at,
+ source_id: member.source_id,
+ source_type: member.source_type)
+ "remove this orphaned member from"
+ end
end
"#{text} #{action} the #{member.source.human_name} #{source_text(member)}?"
diff --git a/app/helpers/milestones_helper.rb b/app/helpers/milestones_helper.rb
index 2f5aac892ab..df1ee54c5ac 100644
--- a/app/helpers/milestones_helper.rb
+++ b/app/helpers/milestones_helper.rb
@@ -249,7 +249,7 @@ module MilestonesHelper
if milestone.legacy_group_milestone?
group_milestone_path(@group, milestone.safe_title, title: milestone.title, milestone: params)
else
- group_milestone_path(@group, milestone.iid, milestone: params)
+ group_milestone_path(milestone.group, milestone.iid, milestone: params)
end
end
diff --git a/app/helpers/namespaces_helper.rb b/app/helpers/namespaces_helper.rb
index 9de28fb3ed9..228dc2cc27f 100644
--- a/app/helpers/namespaces_helper.rb
+++ b/app/helpers/namespaces_helper.rb
@@ -80,8 +80,8 @@ module NamespacesHelper
visibility_level: n.visibility_level_value,
visibility: n.visibility,
name: n.name,
- show_path: (type == 'group') ? group_path(n) : user_path(n),
- edit_path: (type == 'group') ? edit_group_path(n) : nil
+ show_path: type == 'group' ? group_path(n) : user_path(n),
+ edit_path: type == 'group' ? edit_group_path(n) : nil
}]
end
diff --git a/app/helpers/nav_helper.rb b/app/helpers/nav_helper.rb
index 6013475acb1..9ea0b9cb584 100644
--- a/app/helpers/nav_helper.rb
+++ b/app/helpers/nav_helper.rb
@@ -54,11 +54,12 @@ module NavHelper
current_path?('merge_requests#show') ||
current_path?('projects/merge_requests/conflicts#show') ||
current_path?('issues#show') ||
- current_path?('milestones#show')
+ current_path?('milestones#show') ||
+ current_path?('issues#designs')
end
def admin_monitoring_nav_links
- %w(system_info background_jobs logs health_check requests_profiles)
+ %w(system_info background_jobs health_check requests_profiles)
end
def group_issues_sub_menu_items
diff --git a/app/helpers/preferences_helper.rb b/app/helpers/preferences_helper.rb
index 070089d6ef8..7a0462e1b2c 100644
--- a/app/helpers/preferences_helper.rb
+++ b/app/helpers/preferences_helper.rb
@@ -70,7 +70,7 @@ module PreferencesHelper
end
def language_choices
- Gitlab::I18n::AVAILABLE_LANGUAGES.map { |value, label| [label, value] }
+ Gitlab::I18n::AVAILABLE_LANGUAGES.map(&:reverse).sort
end
private
diff --git a/app/helpers/projects/alert_management_helper.rb b/app/helpers/projects/alert_management_helper.rb
new file mode 100644
index 00000000000..af86ef715c2
--- /dev/null
+++ b/app/helpers/projects/alert_management_helper.rb
@@ -0,0 +1,21 @@
+# frozen_string_literal: true
+
+module Projects::AlertManagementHelper
+ def alert_management_data(current_user, project)
+ {
+ 'project-path' => project.full_path,
+ 'enable-alert-management-path' => edit_project_service_path(project, AlertsService),
+ 'empty-alert-svg-path' => image_path('illustrations/alert-management-empty-state.svg'),
+ 'user-can-enable-alert-management' => can?(current_user, :admin_project, project).to_s,
+ 'alert-management-enabled' => (!!project.alerts_service_activated?).to_s
+ }
+ end
+
+ def alert_management_detail_data(project, alert_id)
+ {
+ 'alert-id' => alert_id,
+ 'project-path' => project.full_path,
+ 'new-issue-path' => new_project_issue_path(project)
+ }
+ end
+end
diff --git a/app/helpers/projects_helper.rb b/app/helpers/projects_helper.rb
index 8bec7599158..d743ea6aeea 100644
--- a/app/helpers/projects_helper.rb
+++ b/app/helpers/projects_helper.rb
@@ -1,8 +1,6 @@
# frozen_string_literal: true
module ProjectsHelper
- prepend_if_ee('::EE::ProjectsHelper') # rubocop: disable Cop/InjectEnterpriseEditionModule
-
def project_incident_management_setting
@project_incident_management_setting ||= @project.incident_management_setting ||
@project.build_incident_management_setting
@@ -297,11 +295,11 @@ module ProjectsHelper
end
def show_merge_request_count?(disabled: false, compact_mode: false)
- !disabled && !compact_mode && Feature.enabled?(:project_list_show_mr_count, default_enabled: true)
+ !disabled && !compact_mode
end
def show_issue_count?(disabled: false, compact_mode: false)
- !disabled && !compact_mode && Feature.enabled?(:project_list_show_issue_count, default_enabled: true)
+ !disabled && !compact_mode
end
# overridden in EE
@@ -448,6 +446,7 @@ module ProjectsHelper
clusters: :read_cluster,
serverless: :read_cluster,
error_tracking: :read_sentry_issue,
+ alert_management: :read_alert_management_alert,
labels: :read_label,
issues: :read_issue,
project_members: :read_project_member,
@@ -588,7 +587,9 @@ module ProjectsHelper
pagesAccessLevel: feature.pages_access_level,
containerRegistryEnabled: !!project.container_registry_enabled,
lfsEnabled: !!project.lfs_enabled,
- emailsDisabled: project.emails_disabled?
+ emailsDisabled: project.emails_disabled?,
+ metricsDashboardAccessLevel: feature.metrics_dashboard_access_level,
+ showDefaultAwardEmojis: project.show_default_award_emojis?
}
end
@@ -674,6 +675,7 @@ module ProjectsHelper
services#edit
hooks#index
hooks#edit
+ access_tokens#index
hook_logs#show
repository#show
ci_cd#show
@@ -708,6 +710,7 @@ module ProjectsHelper
clusters
functions
error_tracking
+ alert_management
user
gcp
logs
@@ -737,6 +740,12 @@ module ProjectsHelper
Gitlab.config.registry.enabled &&
can?(current_user, :destroy_container_image, project)
end
+
+ def project_access_token_available?(project)
+ return false if ::Gitlab.com?
+
+ ::Feature.enabled?(:resource_access_token, project)
+ end
end
ProjectsHelper.prepend_if_ee('EE::ProjectsHelper')
diff --git a/app/helpers/releases_helper.rb b/app/helpers/releases_helper.rb
index af51427dc91..1238567a4ed 100644
--- a/app/helpers/releases_helper.rb
+++ b/app/helpers/releases_helper.rb
@@ -30,7 +30,9 @@ module ReleasesHelper
markdown_docs_path: help_page_path('user/markdown'),
releases_page_path: project_releases_path(@project, anchor: @release.tag),
update_release_api_docs_path: help_page_path('api/releases/index.md', anchor: 'update-a-release'),
- release_assets_docs_path: help_page(anchor: 'release-assets')
+ release_assets_docs_path: help_page(anchor: 'release-assets'),
+ manage_milestones_path: project_milestones_path(@project),
+ new_milestone_path: new_project_milestone_url(@project)
}
end
end
diff --git a/app/helpers/search_helper.rb b/app/helpers/search_helper.rb
index e478f76818f..5ad65c59a2e 100644
--- a/app/helpers/search_helper.rb
+++ b/app/helpers/search_helper.rb
@@ -58,8 +58,6 @@ module SearchHelper
ns_('SearchResults|comment', 'SearchResults|comments', count)
when 'projects'
ns_('SearchResults|project', 'SearchResults|projects', count)
- when 'snippet_blobs'
- ns_('SearchResults|snippet result', 'SearchResults|snippet results', count)
when 'snippet_titles'
ns_('SearchResults|snippet', 'SearchResults|snippets', count)
when 'users'
@@ -209,11 +207,11 @@ module SearchHelper
end
end
- def search_filter_input_options(type)
+ def search_filter_input_options(type, placeholder = _('Search or filter results...'))
opts =
{
id: "filtered-search-#{type}",
- placeholder: _('Search or filter results...'),
+ placeholder: placeholder,
data: {
'username-params' => UserSerializer.new.represent(@users)
},
diff --git a/app/helpers/services_helper.rb b/app/helpers/services_helper.rb
index f3f4cdc857f..b13cc93436f 100644
--- a/app/helpers/services_helper.rb
+++ b/app/helpers/services_helper.rb
@@ -51,17 +51,13 @@ module ServicesHelper
end
end
- def service_save_button(service)
- button_tag(class: 'btn btn-success', type: 'submit', disabled: service.deprecated?, data: { qa_selector: 'save_changes_button' }) do
+ def service_save_button
+ button_tag(class: 'btn btn-success', type: 'submit', data: { qa_selector: 'save_changes_button' }) do
icon('spinner spin', class: 'hidden js-btn-spinner') +
content_tag(:span, 'Save changes', class: 'js-btn-label')
end
end
- def disable_fields_service?(service)
- !current_controller?("admin/services") && service.deprecated?
- end
-
def scoped_integrations_path
if @project.present?
project_settings_integrations_path(@project)
@@ -84,7 +80,7 @@ module ServicesHelper
def scoped_edit_integration_path(integration)
if @project.present?
- edit_project_settings_integration_path(@project, integration)
+ edit_project_service_path(@project, integration)
elsif @group.present?
edit_group_settings_integration_path(@group, integration)
else
@@ -105,7 +101,7 @@ module ServicesHelper
extend self
end
-ServicesHelper.prepend_if_ee('EE::ServicesHelper') # rubocop: disable Cop/InjectEnterpriseEditionModule
+ServicesHelper.prepend_if_ee('EE::ServicesHelper')
# The methods in `EE::ServicesHelper` should be available as both instance and
# class methods.
diff --git a/app/helpers/snippets_helper.rb b/app/helpers/snippets_helper.rb
index a9f90a8f5e4..d6a9e447fbc 100644
--- a/app/helpers/snippets_helper.rb
+++ b/app/helpers/snippets_helper.rb
@@ -42,72 +42,6 @@ module SnippetsHelper
(lower..upper).to_a
end
- # Returns a sorted set of lines to be included in a snippet preview.
- # This ensures matching adjacent lines do not display duplicated
- # surrounding code.
- #
- # @returns Array, unique and sorted.
- def matching_lines(lined_content, surrounding_lines, query)
- used_lines = []
- lined_content.each_with_index do |line, line_number|
- used_lines.concat bounded_line_numbers(
- line_number,
- 0,
- lined_content.size,
- surrounding_lines
- ) if line.downcase.include?(query.downcase)
- end
-
- used_lines.uniq.sort
- end
-
- # 'Chunkify' entire snippet. Splits the snippet data into matching lines +
- # surrounding_lines() worth of unmatching lines.
- #
- # @returns a hash with {snippet_object, snippet_chunks:{data,start_line}}
- def chunk_snippet(snippet, query, surrounding_lines = 3)
- lined_content = snippet.content.split("\n")
- used_lines = matching_lines(lined_content, surrounding_lines, query)
-
- snippet_chunk = []
- snippet_chunks = []
- snippet_start_line = 0
- last_line = -1
-
- # Go through each used line, and add consecutive lines as a single chunk
- # to the snippet chunk array.
- used_lines.each do |line_number|
- if last_line < 0
- # Start a new chunk.
- snippet_start_line = line_number
- snippet_chunk << lined_content[line_number]
- elsif last_line == line_number - 1
- # Consecutive line, continue chunk.
- snippet_chunk << lined_content[line_number]
- else
- # Non-consecutive line, add chunk to chunk array.
- snippet_chunks << {
- data: snippet_chunk.join("\n"),
- start_line: snippet_start_line + 1
- }
-
- # Start a new chunk.
- snippet_chunk = [lined_content[line_number]]
- snippet_start_line = line_number
- end
-
- last_line = line_number
- end
- # Add final chunk to chunk array
- snippet_chunks << {
- data: snippet_chunk.join("\n"),
- start_line: snippet_start_line + 1
- }
-
- # Return snippet with chunk array
- { snippet_object: snippet, snippet_chunks: snippet_chunks }
- end
-
def snippet_embed_tag(snippet)
content_tag(:script, nil, src: gitlab_snippet_url(snippet, format: :js))
end
@@ -160,14 +94,4 @@ module SnippetsHelper
title: 'Download',
rel: 'noopener noreferrer')
end
-
- def snippet_file_name(snippet)
- blob = if Feature.enabled?(:version_snippets, current_user) && !snippet.repository.empty?
- snippet.blobs.first
- else
- snippet.blob
- end
-
- blob.name
- end
end
diff --git a/app/helpers/sorting_helper.rb b/app/helpers/sorting_helper.rb
index 3e448087db0..ed1b35338ae 100644
--- a/app/helpers/sorting_helper.rb
+++ b/app/helpers/sorting_helper.rb
@@ -1,8 +1,6 @@
# frozen_string_literal: true
module SortingHelper
- prepend_if_ee('::EE::SortingHelper') # rubocop: disable Cop/InjectEnterpriseEditionModule
-
def sort_options_hash
{
sort_value_created_date => sort_title_created_date,
@@ -584,3 +582,5 @@ module SortingHelper
'expired_asc'
end
end
+
+SortingHelper.prepend_if_ee('::EE::SortingHelper')
diff --git a/app/helpers/system_note_helper.rb b/app/helpers/system_note_helper.rb
index d3b6ecf2bd7..7baa615d36f 100644
--- a/app/helpers/system_note_helper.rb
+++ b/app/helpers/system_note_helper.rb
@@ -27,7 +27,11 @@ module SystemNoteHelper
'locked' => 'lock',
'unlocked' => 'lock-open',
'due_date' => 'calendar',
- 'health_status' => 'status-health'
+ 'health_status' => 'status-health',
+ 'designs_added' => 'doc-image',
+ 'designs_modified' => 'doc-image',
+ 'designs_removed' => 'doc-image',
+ 'designs_discussion_added' => 'doc-image'
}.freeze
def system_note_icon_name(note)
@@ -42,7 +46,7 @@ module SystemNoteHelper
extend self
end
-SystemNoteHelper.prepend_if_ee('EE::SystemNoteHelper') # rubocop: disable Cop/InjectEnterpriseEditionModule
+SystemNoteHelper.prepend_if_ee('EE::SystemNoteHelper')
# The methods in `EE::SystemNoteHelper` should be available as both instance and
# class methods.
diff --git a/app/helpers/todos_helper.rb b/app/helpers/todos_helper.rb
index 0211a22a8c4..41f39c7e798 100644
--- a/app/helpers/todos_helper.rb
+++ b/app/helpers/todos_helper.rb
@@ -53,6 +53,8 @@ module TodosHelper
end
def todo_target_type_name(todo)
+ return _('design') if todo.for_design?
+
todo.target_type.titleize.downcase
end
@@ -63,6 +65,8 @@ module TodosHelper
if todo.for_commit?
project_commit_path(todo.project, todo.target, path_options)
+ elsif todo.for_design?
+ todos_design_path(todo, path_options)
else
path = [todo.resource_parent, todo.target]
@@ -151,7 +155,8 @@ module TodosHelper
[
{ id: '', text: 'Any Type' },
{ id: 'Issue', text: 'Issue' },
- { id: 'MergeRequest', text: 'Merge Request' }
+ { id: 'MergeRequest', text: 'Merge Request' },
+ { id: 'DesignManagement::Design', text: 'Design' }
]
end
@@ -188,6 +193,18 @@ module TodosHelper
private
+ def todos_design_path(todo, path_options)
+ design = todo.target
+
+ designs_project_issue_path(
+ todo.resource_parent,
+ design.issue,
+ path_options.merge(
+ vueroute: design.filename
+ )
+ )
+ end
+
def todo_action_subject(todo)
todo.self_added? ? 'yourself' : 'you'
end
diff --git a/app/helpers/workhorse_helper.rb b/app/helpers/workhorse_helper.rb
index bb5b1555dc4..f74b53d68a1 100644
--- a/app/helpers/workhorse_helper.rb
+++ b/app/helpers/workhorse_helper.rb
@@ -36,8 +36,8 @@ module WorkhorseHelper
end
# Send an entry from artifacts through Workhorse
- def send_artifacts_entry(build, entry)
- headers.store(*Gitlab::Workhorse.send_artifacts_entry(build, entry))
+ def send_artifacts_entry(file, entry)
+ headers.store(*Gitlab::Workhorse.send_artifacts_entry(file, entry))
head :ok
end
diff --git a/app/helpers/x509_helper.rb b/app/helpers/x509_helper.rb
index c330b599d74..009635fb629 100644
--- a/app/helpers/x509_helper.rb
+++ b/app/helpers/x509_helper.rb
@@ -16,4 +16,8 @@ module X509Helper
rescue
{}
end
+
+ def x509_signature?(sig)
+ sig.is_a?(X509CommitSignature) || sig.is_a?(Gitlab::X509::Signature)
+ end
end
diff --git a/app/mailers/emails/groups.rb b/app/mailers/emails/groups.rb
new file mode 100644
index 00000000000..07812a01202
--- /dev/null
+++ b/app/mailers/emails/groups.rb
@@ -0,0 +1,19 @@
+# frozen_string_literal: true
+
+module Emails
+ module Groups
+ def group_was_exported_email(current_user, group)
+ group_email(current_user, group, _('Group was exported'))
+ end
+
+ def group_was_not_exported_email(current_user, group, errors)
+ group_email(current_user, group, _('Group export error'), errors: errors)
+ end
+
+ def group_email(current_user, group, subj, errors: nil)
+ @group = group
+ @errors = errors
+ mail(to: current_user.notification_email_for(@group), subject: subject(subj))
+ end
+ end
+end
diff --git a/app/mailers/emails/notes.rb b/app/mailers/emails/notes.rb
index 6dd4ccb510a..4b56ff60f09 100644
--- a/app/mailers/emails/notes.rb
+++ b/app/mailers/emails/notes.rb
@@ -40,6 +40,18 @@ module Emails
mail_answer_note_thread(@snippet, @note, note_thread_options(recipient_id, reason))
end
+ def note_design_email(recipient_id, note_id, reason = nil)
+ setup_note_mail(note_id, recipient_id)
+
+ design = @note.noteable
+ @target_url = ::Gitlab::Routing.url_helpers.designs_project_issue_url(
+ @note.resource_parent,
+ design.issue,
+ note_target_url_query_params.merge(vueroute: design.filename)
+ )
+ mail_answer_note_thread(design, @note, note_thread_options(recipient_id, reason))
+ end
+
private
def note_target_url_options
diff --git a/app/mailers/emails/profile.rb b/app/mailers/emails/profile.rb
index 441439444d5..4b19149a833 100644
--- a/app/mailers/emails/profile.rb
+++ b/app/mailers/emails/profile.rb
@@ -44,6 +44,16 @@ module Emails
mail(to: @user.notification_email, subject: subject(_("Your Personal Access Tokens will expire in %{days_to_expire} days or less") % { days_to_expire: @days_to_expire }))
end
end
+
+ def unknown_sign_in_email(user, ip)
+ @user = user
+ @ip = ip
+ @target_url = edit_profile_password_url
+
+ Gitlab::I18n.with_locale(@user.preferred_language) do
+ mail(to: @user.notification_email, subject: subject(_("Unknown sign-in from new location")))
+ end
+ end
end
end
diff --git a/app/mailers/notify.rb b/app/mailers/notify.rb
index 49eacc44519..d9483bab543 100644
--- a/app/mailers/notify.rb
+++ b/app/mailers/notify.rb
@@ -17,6 +17,7 @@ class Notify < ApplicationMailer
include Emails::AutoDevops
include Emails::RemoteMirrors
include Emails::Releases
+ include Emails::Groups
helper MilestonesHelper
helper MergeRequestsHelper
diff --git a/app/mailers/previews/notify_preview.rb b/app/mailers/previews/notify_preview.rb
index 38e1d9532a6..c931b5a848f 100644
--- a/app/mailers/previews/notify_preview.rb
+++ b/app/mailers/previews/notify_preview.rb
@@ -161,6 +161,10 @@ class NotifyPreview < ActionMailer::Preview
Notify.remote_mirror_update_failed_email(remote_mirror.id, user.id).message
end
+ def unknown_sign_in_email
+ Notify.unknown_sign_in_email(user, '127.0.0.1').message
+ end
+
private
def project
diff --git a/app/models/active_session.rb b/app/models/active_session.rb
index 050155398ab..065bd5507be 100644
--- a/app/models/active_session.rb
+++ b/app/models/active_session.rb
@@ -124,7 +124,7 @@ class ActiveSession
end
end
- # Lists the ActiveSession objects for the given session IDs.
+ # Lists the session Hash objects for the given session IDs.
#
# session_ids - An array of Rack::Session::SessionId objects
#
@@ -143,7 +143,7 @@ class ActiveSession
end
end
- # Deserializes an ActiveSession object from Redis.
+ # Deserializes a session Hash object from Redis.
#
# raw_session - Raw bytes from Redis
#
diff --git a/app/models/alert_management/alert.rb b/app/models/alert_management/alert.rb
new file mode 100644
index 00000000000..acaf474ecc2
--- /dev/null
+++ b/app/models/alert_management/alert.rb
@@ -0,0 +1,146 @@
+# frozen_string_literal: true
+
+module AlertManagement
+ class Alert < ApplicationRecord
+ include AtomicInternalId
+ include ShaAttribute
+ include Sortable
+ include Gitlab::SQL::Pattern
+
+ STATUSES = {
+ triggered: 0,
+ acknowledged: 1,
+ resolved: 2,
+ ignored: 3
+ }.freeze
+
+ STATUS_EVENTS = {
+ triggered: :trigger,
+ acknowledged: :acknowledge,
+ resolved: :resolve,
+ ignored: :ignore
+ }.freeze
+
+ belongs_to :project
+ belongs_to :issue, optional: true
+ has_internal_id :iid, scope: :project, init: ->(s) { s.project.alert_management_alerts.maximum(:iid) }
+
+ self.table_name = 'alert_management_alerts'
+
+ sha_attribute :fingerprint
+
+ HOSTS_MAX_LENGTH = 255
+
+ validates :title, length: { maximum: 200 }, presence: true
+ validates :description, length: { maximum: 1_000 }
+ validates :service, length: { maximum: 100 }
+ validates :monitoring_tool, length: { maximum: 100 }
+ validates :project, presence: true
+ validates :events, presence: true
+ validates :severity, presence: true
+ validates :status, presence: true
+ validates :started_at, presence: true
+ validates :fingerprint, uniqueness: { scope: :project }, allow_blank: true
+ validate :hosts_length
+
+ enum severity: {
+ critical: 0,
+ high: 1,
+ medium: 2,
+ low: 3,
+ info: 4,
+ unknown: 5
+ }
+
+ state_machine :status, initial: :triggered do
+ state :triggered, value: STATUSES[:triggered]
+
+ state :acknowledged, value: STATUSES[:acknowledged]
+
+ state :resolved, value: STATUSES[:resolved] do
+ validates :ended_at, presence: true
+ end
+
+ state :ignored, value: STATUSES[:ignored]
+
+ state :triggered, :acknowledged, :ignored do
+ validates :ended_at, absence: true
+ end
+
+ event :trigger do
+ transition any => :triggered
+ end
+
+ event :acknowledge do
+ transition any => :acknowledged
+ end
+
+ event :resolve do
+ transition any => :resolved
+ end
+
+ event :ignore do
+ transition any => :ignored
+ end
+
+ before_transition to: [:triggered, :acknowledged, :ignored] do |alert, _transition|
+ alert.ended_at = nil
+ end
+
+ before_transition to: :resolved do |alert, transition|
+ ended_at = transition.args.first
+ alert.ended_at = ended_at || Time.current
+ end
+ end
+
+ delegate :iid, to: :issue, prefix: true, allow_nil: true
+
+ scope :for_iid, -> (iid) { where(iid: iid) }
+ scope :for_status, -> (status) { where(status: status) }
+ scope :for_fingerprint, -> (project, fingerprint) { where(project: project, fingerprint: fingerprint) }
+ scope :search, -> (query) { fuzzy_search(query, [:title, :description, :monitoring_tool, :service]) }
+
+ scope :order_start_time, -> (sort_order) { order(started_at: sort_order) }
+ scope :order_end_time, -> (sort_order) { order(ended_at: sort_order) }
+ scope :order_events_count, -> (sort_order) { order(events: sort_order) }
+ scope :order_severity, -> (sort_order) { order(severity: sort_order) }
+ scope :order_status, -> (sort_order) { order(status: sort_order) }
+
+ scope :counts_by_status, -> { group(:status).count }
+
+ def self.sort_by_attribute(method)
+ case method.to_s
+ when 'start_time_asc' then order_start_time(:asc)
+ when 'start_time_desc' then order_start_time(:desc)
+ when 'end_time_asc' then order_end_time(:asc)
+ when 'end_time_desc' then order_end_time(:desc)
+ when 'events_count_asc' then order_events_count(:asc)
+ when 'events_count_desc' then order_events_count(:desc)
+ when 'severity_asc' then order_severity(:asc)
+ when 'severity_desc' then order_severity(:desc)
+ when 'status_asc' then order_status(:asc)
+ when 'status_desc' then order_status(:desc)
+ else
+ order_by(method)
+ end
+ end
+
+ def details
+ details_payload = payload.except(*attributes.keys)
+
+ Gitlab::Utils::InlineHash.merge_keys(details_payload)
+ end
+
+ def prometheus?
+ monitoring_tool == Gitlab::AlertManagement::AlertParams::MONITORING_TOOLS[:prometheus]
+ end
+
+ private
+
+ def hosts_length
+ return unless hosts
+
+ errors.add(:hosts, "hosts array is over #{HOSTS_MAX_LENGTH} chars") if hosts.join.length > HOSTS_MAX_LENGTH
+ end
+ end
+end
diff --git a/app/models/appearance.rb b/app/models/appearance.rb
index 9da4dfd43b5..00a95070691 100644
--- a/app/models/appearance.rb
+++ b/app/models/appearance.rb
@@ -8,6 +8,7 @@ class Appearance < ApplicationRecord
cache_markdown_field :description
cache_markdown_field :new_project_guidelines
+ cache_markdown_field :profile_image_guidelines
cache_markdown_field :header_message, pipeline: :broadcast_message
cache_markdown_field :footer_message, pipeline: :broadcast_message
@@ -15,12 +16,14 @@ class Appearance < ApplicationRecord
validates :header_logo, file_size: { maximum: 1.megabyte }
validates :message_background_color, allow_blank: true, color: true
validates :message_font_color, allow_blank: true, color: true
+ validates :profile_image_guidelines, length: { maximum: 4096 }
validate :single_appearance_row, on: :create
default_value_for :title, ''
default_value_for :description, ''
default_value_for :new_project_guidelines, ''
+ default_value_for :profile_image_guidelines, ''
default_value_for :header_message, ''
default_value_for :footer_message, ''
default_value_for :message_background_color, '#E75E40'
diff --git a/app/models/application_setting.rb b/app/models/application_setting.rb
index 0aa0216558f..b29d6731b08 100644
--- a/app/models/application_setting.rb
+++ b/app/models/application_setting.rb
@@ -144,7 +144,7 @@ class ApplicationSetting < ApplicationRecord
validates :default_artifacts_expire_in, presence: true, duration: true
validates :container_expiration_policies_enable_historic_entries,
- inclusion: { in: [true, false], message: 'must be a boolean value' }
+ inclusion: { in: [true, false], message: 'must be a boolean value' }
validates :container_registry_token_expire_delay,
presence: true,
@@ -263,6 +263,8 @@ class ApplicationSetting < ApplicationRecord
validates :email_restrictions, untrusted_regexp: true
+ validates :hashed_storage_enabled, inclusion: { in: [true], message: _("Hashed storage can't be disabled anymore for new projects") }
+
SUPPORTED_KEY_TYPES.each do |type|
validates :"#{type}_key_restriction", presence: true, key_restriction: { type: type }
end
@@ -345,6 +347,12 @@ class ApplicationSetting < ApplicationRecord
presence: true,
numericality: { only_integer: true, greater_than_or_equal_to: 0 }
+ validates :issues_create_limit,
+ numericality: { only_integer: true, greater_than_or_equal_to: 0 }
+
+ validates :raw_blob_request_limit,
+ numericality: { only_integer: true, greater_than_or_equal_to: 0 }
+
attr_encrypted :asset_proxy_secret_key,
mode: :per_attribute_iv,
key: Settings.attr_encrypted_db_key_base_truncated,
@@ -412,7 +420,7 @@ class ApplicationSetting < ApplicationRecord
# can cause a significant amount of load on Redis, let's cache it in
# memory.
def self.cache_backend
- Gitlab::ThreadMemoryCache.cache_backend
+ Gitlab::ProcessMemoryCache.cache_backend
end
def recaptcha_or_login_protection_enabled
diff --git a/app/models/application_setting_implementation.rb b/app/models/application_setting_implementation.rb
index c96f086684f..221e4d5e0c6 100644
--- a/app/models/application_setting_implementation.rb
+++ b/app/models/application_setting_implementation.rb
@@ -43,7 +43,10 @@ module ApplicationSettingImplementation
authorized_keys_enabled: true, # TODO default to false if the instance is configured to use AuthorizedKeysCommand
commit_email_hostname: default_commit_email_hostname,
container_expiration_policies_enable_historic_entries: false,
+ container_registry_features: [],
container_registry_token_expire_delay: 5,
+ container_registry_vendor: '',
+ container_registry_version: '',
default_artifacts_expire_in: '30 days',
default_branch_protection: Settings.gitlab['default_branch_protection'],
default_ci_config_path: nil,
@@ -93,7 +96,7 @@ module ApplicationSettingImplementation
plantuml_url: nil,
polling_interval_multiplier: 1,
project_export_enabled: true,
- protected_ci_variables: false,
+ protected_ci_variables: true,
push_event_hooks_limit: 3,
push_event_activities_limit: 3,
raw_blob_request_limit: 300,
diff --git a/app/models/blob.rb b/app/models/blob.rb
index cdc5838797b..c8df6c7732a 100644
--- a/app/models/blob.rb
+++ b/app/models/blob.rb
@@ -86,8 +86,8 @@ class Blob < SimpleDelegator
new(blob, container)
end
- def self.lazy(container, commit_id, path, blob_size_limit: Gitlab::Git::Blob::MAX_DATA_DISPLAY_SIZE)
- BatchLoader.for([commit_id, path]).batch(key: container.repository) do |items, loader, args|
+ def self.lazy(repository, commit_id, path, blob_size_limit: Gitlab::Git::Blob::MAX_DATA_DISPLAY_SIZE)
+ BatchLoader.for([commit_id, path]).batch(key: repository) do |items, loader, args|
args[:key].blobs_at(items, blob_size_limit: blob_size_limit).each do |blob|
loader.call([blob.commit_id, blob.path], blob) if blob
end
@@ -129,7 +129,7 @@ class Blob < SimpleDelegator
def external_storage_error?
if external_storage == :lfs
- !project&.lfs_enabled?
+ !repository.lfs_enabled?
else
false
end
diff --git a/app/models/blob_viewer/dependency_manager.rb b/app/models/blob_viewer/dependency_manager.rb
index 711465c7c79..a851f22bfcd 100644
--- a/app/models/blob_viewer/dependency_manager.rb
+++ b/app/models/blob_viewer/dependency_manager.rb
@@ -32,7 +32,7 @@ module BlobViewer
def json_data
@json_data ||= begin
prepare!
- JSON.parse(blob.data)
+ Gitlab::Json.parse(blob.data)
rescue
{}
end
diff --git a/app/models/broadcast_message.rb b/app/models/broadcast_message.rb
index 0a536a01f72..856f86201ec 100644
--- a/app/models/broadcast_message.rb
+++ b/app/models/broadcast_message.rb
@@ -105,7 +105,10 @@ class BroadcastMessage < ApplicationRecord
def matches_current_path(current_path)
return true if current_path.blank? || target_path.blank?
- current_path.match(Regexp.escape(target_path).gsub('\\*', '.*'))
+ escaped = Regexp.escape(target_path).gsub('\\*', '.*')
+ regexp = Regexp.new "^#{escaped}$", Regexp::IGNORECASE
+
+ regexp.match(current_path)
end
def flush_redis_cache
diff --git a/app/models/ci/bridge.rb b/app/models/ci/bridge.rb
index 76882dfcb0d..1e92a47ab49 100644
--- a/app/models/ci/bridge.rb
+++ b/app/models/ci/bridge.rb
@@ -166,6 +166,10 @@ module Ci
end
end
+ def dependency_variables
+ []
+ end
+
private
def cross_project_params
diff --git a/app/models/ci/build.rb b/app/models/ci/build.rb
index e515447e394..7f64ea7dd97 100644
--- a/app/models/ci/build.rb
+++ b/app/models/ci/build.rb
@@ -25,13 +25,16 @@ module Ci
RUNNER_FEATURES = {
upload_multiple_artifacts: -> (build) { build.publishes_artifacts_reports? },
- refspecs: -> (build) { build.merge_request_ref? }
+ refspecs: -> (build) { build.merge_request_ref? },
+ artifacts_exclude: -> (build) { build.supports_artifacts_exclude? }
}.freeze
DEFAULT_RETRIES = {
scheduler_failure: 2
}.freeze
+ DEGRADATION_THRESHOLD_VARIABLE_NAME = 'DEGRADATION_THRESHOLD'
+
has_one :deployment, as: :deployable, class_name: 'Deployment'
has_one :resource, class_name: 'Ci::Resource', inverse_of: :build
has_many :trace_sections, class_name: 'Ci::BuildTraceSection'
@@ -87,8 +90,12 @@ module Ci
scope :unstarted, ->() { where(runner_id: nil) }
scope :ignore_failures, ->() { where(allow_failure: false) }
- scope :with_artifacts_archive, ->() do
- where('EXISTS (?)', Ci::JobArtifact.select(1).where('ci_builds.id = ci_job_artifacts.job_id').archive)
+ scope :with_downloadable_artifacts, ->() do
+ where('EXISTS (?)',
+ Ci::JobArtifact.select(1)
+ .where('ci_builds.id = ci_job_artifacts.job_id')
+ .where(file_type: Ci::JobArtifact::DOWNLOADABLE_TYPES)
+ )
end
scope :with_existing_job_artifacts, ->(query) do
@@ -130,8 +137,8 @@ module Ci
.includes(:metadata, :job_artifacts_metadata)
end
- scope :with_artifacts_not_expired, ->() { with_artifacts_archive.where('artifacts_expire_at IS NULL OR artifacts_expire_at > ?', Time.now) }
- scope :with_expired_artifacts, ->() { with_artifacts_archive.where('artifacts_expire_at < ?', Time.now) }
+ scope :with_artifacts_not_expired, ->() { with_downloadable_artifacts.where('artifacts_expire_at IS NULL OR artifacts_expire_at > ?', Time.now) }
+ scope :with_expired_artifacts, ->() { with_downloadable_artifacts.where('artifacts_expire_at < ?', Time.now) }
scope :last_month, ->() { where('created_at > ?', Date.today - 1.month) }
scope :manual_actions, ->() { where(when: :manual, status: COMPLETED_STATUSES + %i[manual]) }
scope :scheduled_actions, ->() { where(when: :delayed, status: COMPLETED_STATUSES + %i[scheduled]) }
@@ -486,8 +493,7 @@ module Ci
end
def requires_resource?
- Feature.enabled?(:ci_resource_group, project, default_enabled: true) &&
- self.resource_group_id.present?
+ self.resource_group_id.present?
end
def has_environment?
@@ -530,6 +536,7 @@ module Ci
.concat(job_variables)
.concat(environment_changed_page_variables)
.concat(persisted_environment_variables)
+ .concat(deploy_freeze_variables)
.to_runner_variables
end
end
@@ -585,6 +592,26 @@ module Ci
end
end
+ def deploy_freeze_variables
+ Gitlab::Ci::Variables::Collection.new.tap do |variables|
+ break variables unless freeze_period?
+
+ variables.append(key: 'CI_DEPLOY_FREEZE', value: 'true')
+ end
+ end
+
+ def freeze_period?
+ Ci::FreezePeriodStatus.new(project: project).execute
+ end
+
+ def dependency_variables
+ return [] if all_dependencies.empty?
+
+ Gitlab::Ci::Variables::Collection.new.concat(
+ Ci::JobVariable.where(job: all_dependencies).dotenv_source
+ )
+ end
+
def features
{ trace_sections: true }
end
@@ -870,6 +897,14 @@ module Ci
end
end
+ def collect_accessibility_reports!(accessibility_report)
+ each_report(Ci::JobArtifact::ACCESSIBILITY_REPORT_FILE_TYPES) do |file_type, blob|
+ Gitlab::Ci::Parsers.fabricate!(file_type).parse!(blob, accessibility_report)
+ end
+
+ accessibility_report
+ end
+
def collect_coverage_reports!(coverage_report)
each_report(Ci::JobArtifact::COVERAGE_REPORT_FILE_TYPES) do |file_type, blob|
Gitlab::Ci::Parsers.fabricate!(file_type).parse!(blob, coverage_report)
@@ -878,6 +913,14 @@ module Ci
coverage_report
end
+ def collect_terraform_reports!(terraform_reports)
+ each_report(::Ci::JobArtifact::TERRAFORM_REPORT_FILE_TYPES) do |file_type, blob, report_artifact|
+ ::Gitlab::Ci::Parsers.fabricate!(file_type).parse!(blob, terraform_reports, artifact: report_artifact)
+ end
+
+ terraform_reports
+ end
+
def report_artifacts
job_artifacts.with_reports
end
@@ -902,6 +945,16 @@ module Ci
failure_reason: :data_integrity_failure)
end
+ def supports_artifacts_exclude?
+ options&.dig(:artifacts, :exclude)&.any? &&
+ Gitlab::Ci::Features.artifacts_exclude_enabled?
+ end
+
+ def degradation_threshold
+ var = yaml_variables.find { |v| v[:key] == DEGRADATION_THRESHOLD_VARIABLE_NAME }
+ var[:value]&.to_i if var
+ end
+
private
def dependencies
diff --git a/app/models/ci/daily_build_group_report_result.rb b/app/models/ci/daily_build_group_report_result.rb
new file mode 100644
index 00000000000..3506b27e974
--- /dev/null
+++ b/app/models/ci/daily_build_group_report_result.rb
@@ -0,0 +1,20 @@
+# frozen_string_literal: true
+
+module Ci
+ class DailyBuildGroupReportResult < ApplicationRecord
+ extend Gitlab::Ci::Model
+
+ PARAM_TYPES = %w[coverage].freeze
+
+ belongs_to :last_pipeline, class_name: 'Ci::Pipeline', foreign_key: :last_pipeline_id
+ belongs_to :project
+
+ def self.upsert_reports(data)
+ upsert_all(data, unique_by: :index_daily_build_group_report_results_unique_columns) if data.any?
+ end
+
+ def self.recent_results(attrs, limit: nil)
+ where(attrs).order(date: :desc, group_name: :asc).limit(limit)
+ end
+ end
+end
diff --git a/app/models/ci/daily_report_result.rb b/app/models/ci/daily_report_result.rb
deleted file mode 100644
index 3c1c5f11ed4..00000000000
--- a/app/models/ci/daily_report_result.rb
+++ /dev/null
@@ -1,22 +0,0 @@
-# frozen_string_literal: true
-
-module Ci
- class DailyReportResult < ApplicationRecord
- extend Gitlab::Ci::Model
-
- belongs_to :last_pipeline, class_name: 'Ci::Pipeline', foreign_key: :last_pipeline_id
- belongs_to :project
-
- # TODO: Refactor this out when BuildReportResult is implemented.
- # They both need to share the same enum values for param.
- REPORT_PARAMS = {
- coverage: 0
- }.freeze
-
- enum param_type: REPORT_PARAMS
-
- def self.upsert_reports(data)
- upsert_all(data, unique_by: :index_daily_report_results_unique_columns) if data.any?
- end
- end
-end
diff --git a/app/models/ci/freeze_period.rb b/app/models/ci/freeze_period.rb
new file mode 100644
index 00000000000..bf03b92259a
--- /dev/null
+++ b/app/models/ci/freeze_period.rb
@@ -0,0 +1,18 @@
+# frozen_string_literal: true
+
+module Ci
+ class FreezePeriod < ApplicationRecord
+ include StripAttribute
+ self.table_name = 'ci_freeze_periods'
+
+ default_scope { order(created_at: :asc) }
+
+ belongs_to :project, inverse_of: :freeze_periods
+
+ strip_attributes :freeze_start, :freeze_end
+
+ validates :freeze_start, cron: true, presence: true
+ validates :freeze_end, cron: true, presence: true
+ validates :cron_timezone, cron_freeze_period_timezone: true, presence: true
+ end
+end
diff --git a/app/models/ci/freeze_period_status.rb b/app/models/ci/freeze_period_status.rb
new file mode 100644
index 00000000000..befa935e750
--- /dev/null
+++ b/app/models/ci/freeze_period_status.rb
@@ -0,0 +1,47 @@
+# frozen_string_literal: true
+
+module Ci
+ class FreezePeriodStatus
+ attr_reader :project
+
+ def initialize(project:)
+ @project = project
+ end
+
+ def execute
+ project.freeze_periods.any? { |period| within_freeze_period?(period) }
+ end
+
+ def within_freeze_period?(period)
+ # previous_freeze_end, ..., previous_freeze_start, ..., NOW, ..., next_freeze_end, ..., next_freeze_start
+ # Current time is within a freeze period if
+ # it falls between a previous freeze start and next freeze end
+ start_freeze = Gitlab::Ci::CronParser.new(period.freeze_start, period.cron_timezone)
+ end_freeze = Gitlab::Ci::CronParser.new(period.freeze_end, period.cron_timezone)
+
+ previous_freeze_start = previous_time(start_freeze)
+ previous_freeze_end = previous_time(end_freeze)
+ next_freeze_start = next_time(start_freeze)
+ next_freeze_end = next_time(end_freeze)
+
+ previous_freeze_end < previous_freeze_start &&
+ previous_freeze_start <= time_zone_now &&
+ time_zone_now <= next_freeze_end &&
+ next_freeze_end < next_freeze_start
+ end
+
+ private
+
+ def previous_time(cron_parser)
+ cron_parser.previous_time_from(time_zone_now)
+ end
+
+ def next_time(cron_parser)
+ cron_parser.next_time_from(time_zone_now)
+ end
+
+ def time_zone_now
+ @time_zone_now ||= Time.zone.now
+ end
+ end
+end
diff --git a/app/models/ci/group.rb b/app/models/ci/group.rb
index 15dc1ca8954..4b2081f2977 100644
--- a/app/models/ci/group.rb
+++ b/app/models/ci/group.rb
@@ -46,7 +46,7 @@ module Ci
end
def self.fabricate(project, stage)
- stage.statuses.ordered.latest
+ stage.latest_statuses
.sort_by(&:sortable_name).group_by(&:group_name)
.map do |group_name, grouped_statuses|
self.new(project, stage, name: group_name, jobs: grouped_statuses)
diff --git a/app/models/ci/instance_variable.rb b/app/models/ci/instance_variable.rb
new file mode 100644
index 00000000000..c674f76d229
--- /dev/null
+++ b/app/models/ci/instance_variable.rb
@@ -0,0 +1,76 @@
+# frozen_string_literal: true
+
+module Ci
+ class InstanceVariable < ApplicationRecord
+ extend Gitlab::Ci::Model
+ include Ci::NewHasVariable
+ include Ci::Maskable
+
+ alias_attribute :secret_value, :value
+
+ validates :key, uniqueness: {
+ message: "(%{value}) has already been taken"
+ }
+
+ scope :unprotected, -> { where(protected: false) }
+ after_commit { self.class.touch_redis_cache_timestamp }
+
+ class << self
+ def all_cached
+ cached_data[:all]
+ end
+
+ def unprotected_cached
+ cached_data[:unprotected]
+ end
+
+ def touch_redis_cache_timestamp(time = Time.current.to_f)
+ shared_backend.write(:ci_instance_variable_changed_at, time)
+ end
+
+ private
+
+ def cached_data
+ fetch_memory_cache(:ci_instance_variable_data) do
+ all_records = unscoped.all.to_a
+
+ { all: all_records, unprotected: all_records.reject(&:protected?) }
+ end
+ end
+
+ def fetch_memory_cache(key, &payload)
+ cache = process_backend.read(key)
+
+ if cache && !stale_cache?(cache)
+ cache[:data]
+ else
+ store_cache(key, &payload)
+ end
+ end
+
+ def stale_cache?(cache_info)
+ shared_timestamp = shared_backend.read(:ci_instance_variable_changed_at)
+ return true unless shared_timestamp
+
+ shared_timestamp.to_f > cache_info[:cached_at].to_f
+ end
+
+ def store_cache(key)
+ data = yield
+ time = Time.current.to_f
+
+ process_backend.write(key, data: data, cached_at: time)
+ touch_redis_cache_timestamp(time)
+ data
+ end
+
+ def shared_backend
+ Rails.cache
+ end
+
+ def process_backend
+ Gitlab::ProcessMemoryCache.cache_backend
+ end
+ end
+ end
+end
diff --git a/app/models/ci/job_artifact.rb b/app/models/ci/job_artifact.rb
index ef0701b3874..d931428dccd 100644
--- a/app/models/ci/job_artifact.rb
+++ b/app/models/ci/job_artifact.rb
@@ -12,7 +12,10 @@ module Ci
TEST_REPORT_FILE_TYPES = %w[junit].freeze
COVERAGE_REPORT_FILE_TYPES = %w[cobertura].freeze
+ ACCESSIBILITY_REPORT_FILE_TYPES = %w[accessibility].freeze
NON_ERASABLE_FILE_TYPES = %w[trace].freeze
+ TERRAFORM_REPORT_FILE_TYPES = %w[terraform].freeze
+ UNSUPPORTED_FILE_TYPES = %i[license_management].freeze
DEFAULT_FILE_NAMES = {
archive: nil,
metadata: nil,
@@ -20,6 +23,7 @@ module Ci
metrics_referee: nil,
network_referee: nil,
junit: 'junit.xml',
+ accessibility: 'gl-accessibility.json',
codequality: 'gl-code-quality-report.json',
sast: 'gl-sast-report.json',
dependency_scanning: 'gl-dependency-scanning-report.json',
@@ -32,7 +36,8 @@ module Ci
lsif: 'lsif.json',
dotenv: '.env',
cobertura: 'cobertura-coverage.xml',
- terraform: 'tfplan.json'
+ terraform: 'tfplan.json',
+ cluster_applications: 'gl-cluster-applications.json'
}.freeze
INTERNAL_TYPES = {
@@ -46,13 +51,15 @@ module Ci
metrics: :gzip,
metrics_referee: :gzip,
network_referee: :gzip,
- lsif: :gzip,
dotenv: :gzip,
cobertura: :gzip,
+ cluster_applications: :gzip,
+ lsif: :zip,
# All these file formats use `raw` as we need to store them uncompressed
# for Frontend to fetch the files and do analysis
# When they will be only used by backend, they can be `gzipped`.
+ accessibility: :raw,
codequality: :raw,
sast: :raw,
dependency_scanning: :raw,
@@ -64,15 +71,38 @@ module Ci
terraform: :raw
}.freeze
+ DOWNLOADABLE_TYPES = %w[
+ accessibility
+ archive
+ cobertura
+ codequality
+ container_scanning
+ dast
+ dependency_scanning
+ dotenv
+ junit
+ license_management
+ license_scanning
+ lsif
+ metrics
+ performance
+ sast
+ ].freeze
+
TYPE_AND_FORMAT_PAIRS = INTERNAL_TYPES.merge(REPORT_TYPES).freeze
+ # This is required since we cannot add a default to the database
+ # https://gitlab.com/gitlab-org/gitlab/-/issues/215418
+ attribute :locked, :boolean, default: false
+
belongs_to :project
belongs_to :job, class_name: "Ci::Build", foreign_key: :job_id
mount_uploader :file, JobArtifactUploader
validates :file_format, presence: true, unless: :trace?, on: :create
- validate :valid_file_format?, unless: :trace?, on: :create
+ validate :validate_supported_file_format!, on: :create
+ validate :validate_file_format!, unless: :trace?, on: :create
before_save :set_size, if: :file_changed?
update_project_statistics project_statistics_name: :build_artifacts_size
@@ -82,6 +112,7 @@ module Ci
scope :with_files_stored_locally, -> { where(file_store: [nil, ::JobArtifactUploader::Store::LOCAL]) }
scope :with_files_stored_remotely, -> { where(file_store: ::JobArtifactUploader::Store::REMOTE) }
scope :for_sha, ->(sha, project_id) { joins(job: :pipeline).where(ci_pipelines: { sha: sha, project_id: project_id }) }
+ scope :for_ref, ->(ref, project_id) { joins(job: :pipeline).where(ci_pipelines: { ref: ref, project_id: project_id }) }
scope :for_job_name, ->(name) { joins(:job).where(ci_builds: { name: name }) }
scope :with_file_types, -> (file_types) do
@@ -98,10 +129,18 @@ module Ci
with_file_types(TEST_REPORT_FILE_TYPES)
end
+ scope :accessibility_reports, -> do
+ with_file_types(ACCESSIBILITY_REPORT_FILE_TYPES)
+ end
+
scope :coverage_reports, -> do
with_file_types(COVERAGE_REPORT_FILE_TYPES)
end
+ scope :terraform_reports, -> do
+ with_file_types(TERRAFORM_REPORT_FILE_TYPES)
+ end
+
scope :erasable, -> do
types = self.file_types.reject { |file_type| NON_ERASABLE_FILE_TYPES.include?(file_type) }.values
@@ -109,6 +148,8 @@ module Ci
end
scope :expired, -> (limit) { where('expire_at < ?', Time.now).limit(limit) }
+ scope :locked, -> { where(locked: true) }
+ scope :unlocked, -> { where(locked: [false, nil]) }
scope :scoped_project, -> { where('ci_job_artifacts.project_id = projects.id') }
@@ -133,7 +174,9 @@ module Ci
lsif: 15, # LSIF data for code navigation
dotenv: 16,
cobertura: 17,
- terraform: 18 # Transformed json
+ terraform: 18, # Transformed json
+ accessibility: 19,
+ cluster_applications: 20
}
enum file_format: {
@@ -161,7 +204,15 @@ module Ci
raw: Gitlab::Ci::Build::Artifacts::Adapters::RawStream
}.freeze
- def valid_file_format?
+ def validate_supported_file_format!
+ return if Feature.disabled?(:drop_license_management_artifact, project, default_enabled: true)
+
+ if UNSUPPORTED_FILE_TYPES.include?(self.file_type&.to_sym)
+ errors.add(:base, _("File format is no longer supported"))
+ end
+ end
+
+ def validate_file_format!
unless TYPE_AND_FORMAT_PAIRS[self.file_type&.to_sym] == self.file_format&.to_sym
errors.add(:base, _('Invalid file format with specified file type'))
end
diff --git a/app/models/ci/legacy_stage.rb b/app/models/ci/legacy_stage.rb
index f156219ea81..250306e2be4 100644
--- a/app/models/ci/legacy_stage.rb
+++ b/app/models/ci/legacy_stage.rb
@@ -41,6 +41,10 @@ module Ci
.fabricate!
end
+ def latest_statuses
+ statuses.ordered.latest
+ end
+
def statuses
@statuses ||= pipeline.statuses.where(stage: name)
end
diff --git a/app/models/ci/persistent_ref.rb b/app/models/ci/persistent_ref.rb
index 76139f5d676..91163c85a9e 100644
--- a/app/models/ci/persistent_ref.rb
+++ b/app/models/ci/persistent_ref.rb
@@ -14,16 +14,12 @@ module Ci
delegate :ref_exists?, :create_ref, :delete_refs, to: :repository
def exist?
- return unless enabled?
-
ref_exists?(path)
rescue
false
end
def create
- return unless enabled?
-
create_ref(sha, path)
rescue => e
Gitlab::ErrorTracking
@@ -31,8 +27,6 @@ module Ci
end
def delete
- return unless enabled?
-
delete_refs(path)
rescue Gitlab::Git::Repository::NoRepository
# no-op
@@ -44,11 +38,5 @@ module Ci
def path
"refs/#{Repository::REF_PIPELINES}/#{pipeline.id}"
end
-
- private
-
- def enabled?
- Feature.enabled?(:depend_on_persistent_pipeline_ref, project, default_enabled: true)
- end
end
end
diff --git a/app/models/ci/pipeline.rb b/app/models/ci/pipeline.rb
index 8a3ca2e758c..5db1635f64d 100644
--- a/app/models/ci/pipeline.rb
+++ b/app/models/ci/pipeline.rb
@@ -82,7 +82,7 @@ module Ci
has_one :pipeline_config, class_name: 'Ci::PipelineConfig', inverse_of: :pipeline
- has_many :daily_report_results, class_name: 'Ci::DailyReportResult', foreign_key: :last_pipeline_id
+ has_many :daily_build_group_report_results, class_name: 'Ci::DailyBuildGroupReportResult', foreign_key: :last_pipeline_id
accepts_nested_attributes_for :variables, reject_if: :persisted?
@@ -115,8 +115,11 @@ module Ci
state_machine :status, initial: :created do
event :enqueue do
- transition [:created, :waiting_for_resource, :preparing, :skipped, :scheduled] => :pending
+ transition [:created, :manual, :waiting_for_resource, :preparing, :skipped, :scheduled] => :pending
transition [:success, :failed, :canceled] => :running
+
+ # this is needed to ensure tests to be covered
+ transition [:running] => :running
end
event :request_resource do
@@ -194,7 +197,7 @@ module Ci
# We wait a little bit to ensure that all BuildFinishedWorkers finish first
# because this is where some metrics like code coverage is parsed and stored
# in CI build records which the daily build metrics worker relies on.
- pipeline.run_after_commit { Ci::DailyReportResultsWorker.perform_in(10.minutes, pipeline.id) }
+ pipeline.run_after_commit { Ci::DailyBuildGroupReportResultsWorker.perform_in(10.minutes, pipeline.id) }
end
after_transition do |pipeline, transition|
@@ -393,16 +396,18 @@ module Ci
false
end
- ##
- # TODO We do not completely switch to persisted stages because of
- # race conditions with setting statuses gitlab-foss#23257.
- #
def ordered_stages
- return legacy_stages unless complete?
-
- if Feature.enabled?('ci_pipeline_persisted_stages', default_enabled: true)
+ if Feature.enabled?(:ci_atomic_processing, project, default_enabled: false)
+ # The `Ci::Stage` contains all up-to date data
+ # as atomic processing updates all data in-bulk
+ stages
+ elsif Feature.enabled?(:ci_pipeline_persisted_stages, default_enabled: true) && complete?
+ # The `Ci::Stage` contains up-to date data only for `completed` pipelines
+ # this is due to asynchronous processing of pipeline, and stages possibly
+ # not updated inline with processing of pipeline
stages
else
+ # In other cases, we need to calculate stages dynamically
legacy_stages
end
end
@@ -440,7 +445,7 @@ module Ci
end
def legacy_stages
- if Feature.enabled?(:ci_composite_status, default_enabled: false)
+ if Feature.enabled?(:ci_composite_status, project, default_enabled: false)
legacy_stages_using_composite_status
else
legacy_stages_using_sql
@@ -681,6 +686,8 @@ module Ci
variables.concat(merge_request.predefined_variables)
end
+ variables.append(key: 'CI_KUBERNETES_ACTIVE', value: 'true') if has_kubernetes_active?
+
if external_pull_request_event? && external_pull_request
variables.concat(external_pull_request.predefined_variables)
end
@@ -781,7 +788,7 @@ module Ci
end
def find_job_with_archive_artifacts(name)
- builds.latest.with_artifacts_archive.find_by_name(name)
+ builds.latest.with_downloadable_artifacts.find_by_name(name)
end
def latest_builds_with_artifacts
@@ -809,6 +816,14 @@ module Ci
end
end
+ def accessibility_reports
+ Gitlab::Ci::Reports::AccessibilityReports.new.tap do |accessibility_reports|
+ builds.latest.with_reports(Ci::JobArtifact.accessibility_reports).each do |build|
+ build.collect_accessibility_reports!(accessibility_reports)
+ end
+ end
+ end
+
def coverage_reports
Gitlab::Ci::Reports::CoverageReports.new.tap do |coverage_reports|
builds.latest.with_reports(Ci::JobArtifact.coverage_reports).each do |build|
@@ -817,6 +832,14 @@ module Ci
end
end
+ def terraform_reports
+ ::Gitlab::Ci::Reports::TerraformReports.new.tap do |terraform_reports|
+ builds.latest.with_reports(::Ci::JobArtifact.terraform_reports).each do |build|
+ build.collect_terraform_reports!(terraform_reports)
+ end
+ end
+ end
+
def has_exposed_artifacts?
complete? && builds.latest.with_exposed_artifacts.exists?
end
@@ -938,6 +961,14 @@ module Ci
end
end
+ # Set scheduling type of processables if they were created before scheduling_type
+ # data was deployed (https://gitlab.com/gitlab-org/gitlab/-/merge_requests/22246).
+ def ensure_scheduling_type!
+ return unless ::Gitlab::Ci::Features.ensure_scheduling_type_enabled?
+
+ processables.populate_scheduling_type!
+ end
+
private
def pipeline_data
diff --git a/app/models/ci/pipeline_schedule.rb b/app/models/ci/pipeline_schedule.rb
index f5785000062..8c9ad343f32 100644
--- a/app/models/ci/pipeline_schedule.rb
+++ b/app/models/ci/pipeline_schedule.rb
@@ -6,6 +6,10 @@ module Ci
include Importable
include StripAttribute
include Schedulable
+ include Limitable
+
+ self.limit_name = 'ci_pipeline_schedules'
+ self.limit_scope = :project
belongs_to :project
belongs_to :owner, class_name: 'User'
diff --git a/app/models/ci/processable.rb b/app/models/ci/processable.rb
index c123bd7c33b..cc00500662d 100644
--- a/app/models/ci/processable.rb
+++ b/app/models/ci/processable.rb
@@ -49,7 +49,7 @@ module Ci
end
validates :type, presence: true
- validates :scheduling_type, presence: true, on: :create, if: :validate_scheduling_type?
+ validates :scheduling_type, presence: true, on: :create, unless: :importing?
delegate :merge_request?,
:merge_request_ref?,
@@ -83,7 +83,7 @@ module Ci
# Overriding scheduling_type enum's method for nil `scheduling_type`s
def scheduling_type_dag?
- super || find_legacy_scheduling_type == :dag
+ scheduling_type.nil? ? find_legacy_scheduling_type == :dag : super
end
# scheduling_type column of previous builds/bridges have not been populated,
@@ -100,10 +100,12 @@ module Ci
end
end
- private
+ def ensure_scheduling_type!
+ # If this has a scheduling_type, it means all processables in the pipeline already have.
+ return if scheduling_type
- def validate_scheduling_type?
- !importing? && Feature.enabled?(:validate_scheduling_type_of_processables, project)
+ pipeline.ensure_scheduling_type!
+ reset
end
end
end
diff --git a/app/models/ci/stage.rb b/app/models/ci/stage.rb
index 93bd42f8734..a316b4718e0 100644
--- a/app/models/ci/stage.rb
+++ b/app/models/ci/stage.rb
@@ -13,6 +13,7 @@ module Ci
belongs_to :pipeline
has_many :statuses, class_name: 'CommitStatus', foreign_key: :stage_id
+ has_many :latest_statuses, -> { ordered.latest }, class_name: 'CommitStatus', foreign_key: :stage_id
has_many :processables, class_name: 'Ci::Processable', foreign_key: :stage_id
has_many :builds, foreign_key: :stage_id
has_many :bridges, foreign_key: :stage_id
@@ -42,8 +43,7 @@ module Ci
state_machine :status, initial: :created do
event :enqueue do
- transition [:created, :waiting_for_resource, :preparing] => :pending
- transition [:success, :failed, :canceled, :skipped] => :running
+ transition any - [:pending] => :pending
end
event :request_resource do
diff --git a/app/models/clusters/applications/elastic_stack.rb b/app/models/clusters/applications/elastic_stack.rb
index afdc1c91c69..0d029aabc3b 100644
--- a/app/models/clusters/applications/elastic_stack.rb
+++ b/app/models/clusters/applications/elastic_stack.rb
@@ -3,7 +3,7 @@
module Clusters
module Applications
class ElasticStack < ApplicationRecord
- VERSION = '1.9.0'
+ VERSION = '3.0.0'
ELASTICSEARCH_PORT = 9200
@@ -18,7 +18,11 @@ module Clusters
default_value_for :version, VERSION
def chart
- 'stable/elastic-stack'
+ 'elastic-stack/elastic-stack'
+ end
+
+ def repository
+ 'https://charts.gitlab.io'
end
def install_command
@@ -27,7 +31,9 @@ module Clusters
version: VERSION,
rbac: cluster.platform_kubernetes_rbac?,
chart: chart,
+ repository: repository,
files: files,
+ preinstall: migrate_to_3_script,
postinstall: post_install_script
)
end
@@ -49,7 +55,7 @@ module Clusters
strong_memoize(:elasticsearch_client) do
next unless kube_client
- proxy_url = kube_client.proxy_url('service', 'elastic-stack-elasticsearch-client', ::Clusters::Applications::ElasticStack::ELASTICSEARCH_PORT, Gitlab::Kubernetes::Helm::NAMESPACE)
+ proxy_url = kube_client.proxy_url('service', service_name, ::Clusters::Applications::ElasticStack::ELASTICSEARCH_PORT, Gitlab::Kubernetes::Helm::NAMESPACE)
Elasticsearch::Client.new(url: proxy_url) do |faraday|
# ensures headers containing auth data are appended to original client options
@@ -69,23 +75,54 @@ module Clusters
end
end
+ def chart_above_v2?
+ Gem::Version.new(version) >= Gem::Version.new('2.0.0')
+ end
+
+ def chart_above_v3?
+ Gem::Version.new(version) >= Gem::Version.new('3.0.0')
+ end
+
private
+ def service_name
+ chart_above_v3? ? 'elastic-stack-elasticsearch-master' : 'elastic-stack-elasticsearch-client'
+ end
+
+ def pvc_selector
+ chart_above_v3? ? "app=elastic-stack-elasticsearch-master" : "release=elastic-stack"
+ end
+
def post_install_script
[
- "timeout -t60 sh /data/helm/elastic-stack/config/wait-for-elasticsearch.sh http://elastic-stack-elasticsearch-client:9200"
+ "timeout -t60 sh /data/helm/elastic-stack/config/wait-for-elasticsearch.sh http://elastic-stack-elasticsearch-master:9200"
]
end
def post_delete_script
[
- Gitlab::Kubernetes::KubectlCmd.delete("pvc", "--selector", "release=elastic-stack")
+ Gitlab::Kubernetes::KubectlCmd.delete("pvc", "--selector", pvc_selector, "--namespace", Gitlab::Kubernetes::Helm::NAMESPACE)
]
end
def kube_client
cluster&.kubeclient&.core_client
end
+
+ def migrate_to_3_script
+ return [] if !updating? || chart_above_v3?
+
+ # Chart version 3.0.0 moves to our own chart at https://gitlab.com/gitlab-org/charts/elastic-stack
+ # and is not compatible with pre-existing resources. We first remove them.
+ [
+ Gitlab::Kubernetes::Helm::DeleteCommand.new(
+ name: 'elastic-stack',
+ rbac: cluster.platform_kubernetes_rbac?,
+ files: files
+ ).delete_command,
+ Gitlab::Kubernetes::KubectlCmd.delete("pvc", "--selector", "release=elastic-stack", "--namespace", Gitlab::Kubernetes::Helm::NAMESPACE)
+ ]
+ end
end
end
end
diff --git a/app/models/clusters/applications/fluentd.rb b/app/models/clusters/applications/fluentd.rb
index a33b1e39ace..3fd6e870edc 100644
--- a/app/models/clusters/applications/fluentd.rb
+++ b/app/models/clusters/applications/fluentd.rb
@@ -4,6 +4,7 @@ module Clusters
module Applications
class Fluentd < ApplicationRecord
VERSION = '2.4.0'
+ CILIUM_CONTAINER_NAME = 'cilium-monitor'
self.table_name = 'clusters_applications_fluentd'
@@ -18,6 +19,8 @@ module Clusters
enum protocol: { tcp: 0, udp: 1 }
+ validate :has_at_least_one_log_enabled?
+
def chart
'stable/fluentd'
end
@@ -39,6 +42,12 @@ module Clusters
private
+ def has_at_least_one_log_enabled?
+ if !waf_log_enabled && !cilium_log_enabled
+ errors.add(:base, _("At least one logging option is required to be enabled"))
+ end
+ end
+
def content_values
YAML.load_file(chart_values_file).deep_merge!(specification)
end
@@ -62,7 +71,7 @@ module Clusters
program fluentd
hostname ${kubernetes_host}
protocol #{protocol}
- packet_size 65535
+ packet_size 131072
<buffer kubernetes_host>
</buffer>
<format>
@@ -85,7 +94,7 @@ module Clusters
<source>
@type tail
@id in_tail_container_logs
- path /var/log/containers/*#{Ingress::MODSECURITY_LOG_CONTAINER_NAME}*.log
+ path #{path_to_logs}
pos_file /var/log/fluentd-containers.log.pos
tag kubernetes.*
read_from_head true
@@ -96,6 +105,13 @@ module Clusters
</source>
EOF
end
+
+ def path_to_logs
+ path = []
+ path << "/var/log/containers/*#{Ingress::MODSECURITY_LOG_CONTAINER_NAME}*.log" if waf_log_enabled
+ path << "/var/log/containers/*#{CILIUM_CONTAINER_NAME}*.log" if cilium_log_enabled
+ path.join(',')
+ end
end
end
end
diff --git a/app/models/clusters/applications/ingress.rb b/app/models/clusters/applications/ingress.rb
index 5985e08d73e..dd354198910 100644
--- a/app/models/clusters/applications/ingress.rb
+++ b/app/models/clusters/applications/ingress.rb
@@ -17,6 +17,7 @@ module Clusters
include ::Clusters::Concerns::ApplicationVersion
include ::Clusters::Concerns::ApplicationData
include AfterCommitQueue
+ include UsageStatistics
default_value_for :ingress_type, :nginx
default_value_for :modsecurity_enabled, true
@@ -29,6 +30,10 @@ module Clusters
enum modsecurity_mode: { logging: 0, blocking: 1 }
+ scope :modsecurity_not_installed, -> { where(modsecurity_enabled: nil) }
+ scope :modsecurity_enabled, -> { where(modsecurity_enabled: true) }
+ scope :modsecurity_disabled, -> { where(modsecurity_enabled: false) }
+
FETCH_IP_ADDRESS_DELAY = 30.seconds
state_machine :status do
@@ -98,7 +103,7 @@ module Clusters
"args" => [
"/bin/sh",
"-c",
- "tail -f /var/log/modsec/audit.log"
+ "tail -F /var/log/modsec/audit.log"
],
"volumeMounts" => [
{
diff --git a/app/models/clusters/applications/jupyter.rb b/app/models/clusters/applications/jupyter.rb
index 42fa4a6f179..056ea355de6 100644
--- a/app/models/clusters/applications/jupyter.rb
+++ b/app/models/clusters/applications/jupyter.rb
@@ -5,7 +5,7 @@ require 'securerandom'
module Clusters
module Applications
class Jupyter < ApplicationRecord
- VERSION = '0.9.0-beta.2'
+ VERSION = '0.9.0'
self.table_name = 'clusters_applications_jupyter'
diff --git a/app/models/clusters/applications/knative.rb b/app/models/clusters/applications/knative.rb
index 1f90318f845..3047da12dd9 100644
--- a/app/models/clusters/applications/knative.rb
+++ b/app/models/clusters/applications/knative.rb
@@ -4,8 +4,8 @@ module Clusters
module Applications
class Knative < ApplicationRecord
VERSION = '0.9.0'
- REPOSITORY = 'https://storage.googleapis.com/triggermesh-charts'
- METRICS_CONFIG = 'https://storage.googleapis.com/triggermesh-charts/istio-metrics.yaml'
+ REPOSITORY = 'https://charts.gitlab.io'
+ METRICS_CONFIG = 'https://gitlab.com/gitlab-org/charts/knative/-/raw/v0.9.0/vendor/istio-metrics.yml'
FETCH_IP_ADDRESS_DELAY = 30.seconds
API_GROUPS_PATH = 'config/knative/api_groups.yml'
diff --git a/app/models/clusters/applications/runner.rb b/app/models/clusters/applications/runner.rb
index 7d67e258991..a861126908f 100644
--- a/app/models/clusters/applications/runner.rb
+++ b/app/models/clusters/applications/runner.rb
@@ -3,7 +3,7 @@
module Clusters
module Applications
class Runner < ApplicationRecord
- VERSION = '0.15.0'
+ VERSION = '0.16.1'
self.table_name = 'clusters_applications_runners'
diff --git a/app/models/clusters/cluster.rb b/app/models/clusters/cluster.rb
index 430a9b3c43e..83f558af1a1 100644
--- a/app/models/clusters/cluster.rb
+++ b/app/models/clusters/cluster.rb
@@ -26,6 +26,8 @@ module Clusters
KUBE_INGRESS_BASE_DOMAIN = 'KUBE_INGRESS_BASE_DOMAIN'
APPLICATIONS_ASSOCIATIONS = APPLICATIONS.values.map(&:association_name).freeze
+ self.reactive_cache_work_type = :external_dependency
+
belongs_to :user
belongs_to :management_project, class_name: '::Project', optional: true
@@ -33,6 +35,7 @@ module Clusters
has_many :projects, through: :cluster_projects, class_name: '::Project'
has_one :cluster_project, -> { order(id: :desc) }, class_name: 'Clusters::Project'
has_many :deployment_clusters
+ has_many :deployments, inverse_of: :cluster
has_many :cluster_groups, class_name: 'Clusters::Group'
has_many :groups, through: :cluster_groups, class_name: '::Group'
@@ -203,10 +206,16 @@ module Clusters
end
end
+ def nodes
+ with_reactive_cache do |data|
+ data[:nodes]
+ end
+ end
+
def calculate_reactive_cache
return unless enabled?
- { connection_status: retrieve_connection_status }
+ { connection_status: retrieve_connection_status, nodes: retrieve_nodes }
end
def persisted_applications
@@ -214,11 +223,19 @@ module Clusters
end
def applications
- APPLICATIONS_ASSOCIATIONS.map do |association_name|
- public_send(association_name) || public_send("build_#{association_name}") # rubocop:disable GitlabSecurity/PublicSend
+ APPLICATIONS.each_value.map do |application_class|
+ find_or_build_application(application_class)
end
end
+ def find_or_build_application(application_class)
+ raise ArgumentError, "#{application_class} is not in APPLICATIONS" unless APPLICATIONS.value?(application_class)
+
+ association_name = application_class.association_name
+
+ public_send(association_name) || public_send("build_#{association_name}") # rubocop:disable GitlabSecurity/PublicSend
+ end
+
def provider
if gcp?
provider_gcp
@@ -345,32 +362,55 @@ module Clusters
end
def retrieve_connection_status
- kubeclient.core_client.discover
- rescue *Gitlab::Kubernetes::Errors::CONNECTION
- :unreachable
- rescue *Gitlab::Kubernetes::Errors::AUTHENTICATION
- :authentication_failure
- rescue Kubeclient::HttpError => e
- kubeclient_error_status(e.message)
- rescue => e
- Gitlab::ErrorTracking.track_exception(e, cluster_id: id)
-
- :unknown_failure
- else
- :connected
- end
-
- # KubeClient uses the same error class
- # For connection errors (eg. timeout) and
- # for Kubernetes errors.
- def kubeclient_error_status(message)
- if message&.match?(/timed out|timeout/i)
- :unreachable
- else
- :authentication_failure
+ result = ::Gitlab::Kubernetes::KubeClient.graceful_request(id) { kubeclient.core_client.discover }
+ result[:status]
+ end
+
+ def retrieve_nodes
+ result = ::Gitlab::Kubernetes::KubeClient.graceful_request(id) { kubeclient.get_nodes }
+ cluster_nodes = result[:response].to_a
+
+ result = ::Gitlab::Kubernetes::KubeClient.graceful_request(id) { kubeclient.metrics_client.get_nodes }
+ nodes_metrics = result[:response].to_a
+
+ cluster_nodes.inject([]) do |memo, node|
+ sliced_node = filter_relevant_node_attributes(node)
+
+ matched_node_metric = nodes_metrics.find { |node_metric| node_metric.metadata.name == node.metadata.name }
+
+ sliced_node_metrics = matched_node_metric ? filter_relevant_node_metrics_attributes(matched_node_metric) : {}
+
+ memo << sliced_node.merge(sliced_node_metrics)
end
end
+ def filter_relevant_node_attributes(node)
+ {
+ 'metadata' => {
+ 'name' => node.metadata.name
+ },
+ 'status' => {
+ 'capacity' => {
+ 'cpu' => node.status.capacity.cpu,
+ 'memory' => node.status.capacity.memory
+ },
+ 'allocatable' => {
+ 'cpu' => node.status.allocatable.cpu,
+ 'memory' => node.status.allocatable.memory
+ }
+ }
+ }
+ end
+
+ def filter_relevant_node_metrics_attributes(node_metrics)
+ {
+ 'usage' => {
+ 'cpu' => node_metrics.usage.cpu,
+ 'memory' => node_metrics.usage.memory
+ }
+ }
+ end
+
# To keep backward compatibility with AUTO_DEVOPS_DOMAIN
# environment variable, we need to ensure KUBE_INGRESS_BASE_DOMAIN
# is set if AUTO_DEVOPS_DOMAIN is set on any of the following options:
diff --git a/app/models/clusters/concerns/application_status.rb b/app/models/clusters/concerns/application_status.rb
index 14237439a8d..0b915126f8a 100644
--- a/app/models/clusters/concerns/application_status.rb
+++ b/app/models/clusters/concerns/application_status.rb
@@ -27,6 +27,7 @@ module Clusters
state :update_errored, value: 6
state :uninstalling, value: 7
state :uninstall_errored, value: 8
+ state :uninstalled, value: 10
# Used for applications that are pre-installed by the cluster,
# e.g. Knative in GCP Cloud Run enabled clusters
@@ -35,6 +36,14 @@ module Clusters
# and no exit transitions.
state :pre_installed, value: 9
+ event :make_externally_installed do
+ transition any => :installed
+ end
+
+ event :make_externally_uninstalled do
+ transition any => :uninstalled
+ end
+
event :make_scheduled do
transition [:installable, :errored, :installed, :updated, :update_errored, :uninstall_errored] => :scheduled
end
diff --git a/app/models/commit_status.rb b/app/models/commit_status.rb
index 046f131b041..7e99f128dad 100644
--- a/app/models/commit_status.rb
+++ b/app/models/commit_status.rb
@@ -7,8 +7,6 @@ class CommitStatus < ApplicationRecord
include Presentable
include EnumWithNil
- prepend_if_ee('::EE::CommitStatus') # rubocop: disable Cop/InjectEnterpriseEditionModule
-
self.table_name = 'ci_builds'
belongs_to :user
@@ -267,8 +265,16 @@ class CommitStatus < ApplicationRecord
end
end
+ def recoverable?
+ failed? && !unrecoverable_failure?
+ end
+
private
+ def unrecoverable_failure?
+ script_failure? || missing_dependency_failure? || archived_failure? || scheduler_failure? || data_integrity_failure?
+ end
+
def schedule_stage_and_pipeline_update
if Feature.enabled?(:ci_atomic_processing, project)
# Atomic Processing requires only single Worker
@@ -284,3 +290,5 @@ class CommitStatus < ApplicationRecord
end
end
end
+
+CommitStatus.prepend_if_ee('::EE::CommitStatus')
diff --git a/app/models/concerns/async_devise_email.rb b/app/models/concerns/async_devise_email.rb
new file mode 100644
index 00000000000..38c99dc7e71
--- /dev/null
+++ b/app/models/concerns/async_devise_email.rb
@@ -0,0 +1,14 @@
+# frozen_string_literal: true
+
+module AsyncDeviseEmail
+ extend ActiveSupport::Concern
+
+ private
+
+ # Added according to https://github.com/plataformatec/devise/blob/7df57d5081f9884849ca15e4fde179ef164a575f/README.md#activejob-integration
+ def send_devise_notification(notification, *args)
+ return true unless can?(:receive_notifications)
+
+ devise_mailer.__send__(notification, self, *args).deliver_later # rubocop:disable GitlabSecurity/PublicSend
+ end
+end
diff --git a/app/models/concerns/awardable.rb b/app/models/concerns/awardable.rb
index 0f2a389f0a3..896f0916d8c 100644
--- a/app/models/concerns/awardable.rb
+++ b/app/models/concerns/awardable.rb
@@ -14,32 +14,29 @@ module Awardable
class_methods do
def awarded(user, name = nil)
- sql = <<~EOL
- EXISTS (
- SELECT TRUE
- FROM award_emoji
- WHERE user_id = :user_id AND
- #{"name = :name AND" if name.present?}
- awardable_type = :awardable_type AND
- awardable_id = #{self.arel_table.name}.id
- )
- EOL
+ award_emoji_table = Arel::Table.new('award_emoji')
+ inner_query = award_emoji_table
+ .project('true')
+ .where(award_emoji_table[:user_id].eq(user.id))
+ .where(award_emoji_table[:awardable_type].eq(self.name))
+ .where(award_emoji_table[:awardable_id].eq(self.arel_table[:id]))
+
+ inner_query = inner_query.where(award_emoji_table[:name].eq(name)) if name.present?
- where(sql, user_id: user.id, name: name, awardable_type: self.name)
+ where(inner_query.exists)
end
- def not_awarded(user)
- sql = <<~EOL
- NOT EXISTS (
- SELECT TRUE
- FROM award_emoji
- WHERE user_id = :user_id AND
- awardable_type = :awardable_type AND
- awardable_id = #{self.arel_table.name}.id
- )
- EOL
+ def not_awarded(user, name = nil)
+ award_emoji_table = Arel::Table.new('award_emoji')
+ inner_query = award_emoji_table
+ .project('true')
+ .where(award_emoji_table[:user_id].eq(user.id))
+ .where(award_emoji_table[:awardable_type].eq(self.name))
+ .where(award_emoji_table[:awardable_id].eq(self.arel_table[:id]))
+
+ inner_query = inner_query.where(award_emoji_table[:name].eq(name)) if name.present?
- where(sql, user_id: user.id, awardable_type: self.name)
+ where(inner_query.exists.not)
end
def order_upvotes_desc
@@ -77,7 +74,7 @@ module Awardable
# By default we always load award_emoji user association
awards = award_emoji.group_by(&:name)
- if with_thumbs
+ if with_thumbs && (!project || project.show_default_award_emojis?)
awards[AwardEmoji::UPVOTE_NAME] ||= []
awards[AwardEmoji::DOWNVOTE_NAME] ||= []
end
diff --git a/app/models/concerns/cache_markdown_field.rb b/app/models/concerns/cache_markdown_field.rb
index cc13f279c4d..e4e0f55d5f4 100644
--- a/app/models/concerns/cache_markdown_field.rb
+++ b/app/models/concerns/cache_markdown_field.rb
@@ -161,7 +161,6 @@ module CacheMarkdownField
define_method(invalidation_method) do
changed_fields = changed_attributes.keys
invalidations = changed_fields & [markdown_field.to_s, *INVALIDATED_BY]
- invalidations.delete(markdown_field.to_s) if changed_fields.include?("#{markdown_field}_html")
!invalidations.empty? || !cached_html_up_to_date?(markdown_field)
end
end
diff --git a/app/models/concerns/ci/contextable.rb b/app/models/concerns/ci/contextable.rb
index 5ff537a7837..ccd90ea5900 100644
--- a/app/models/concerns/ci/contextable.rb
+++ b/app/models/concerns/ci/contextable.rb
@@ -18,6 +18,8 @@ module Ci
variables.concat(deployment_variables(environment: environment))
variables.concat(yaml_variables)
variables.concat(user_variables)
+ variables.concat(dependency_variables) if Feature.enabled?(:ci_dependency_variables, project)
+ variables.concat(secret_instance_variables)
variables.concat(secret_group_variables)
variables.concat(secret_project_variables(environment: environment))
variables.concat(trigger_request.user_variables) if trigger_request
@@ -81,6 +83,12 @@ module Ci
)
end
+ def secret_instance_variables
+ return [] unless ::Feature.enabled?(:ci_instance_level_variables, project, default_enabled: true)
+
+ project.ci_instance_variables_for(ref: git_ref)
+ end
+
def secret_group_variables
return [] unless project.group
diff --git a/app/models/concerns/diff_positionable_note.rb b/app/models/concerns/diff_positionable_note.rb
index 6484a3157b1..cea3c7d119c 100644
--- a/app/models/concerns/diff_positionable_note.rb
+++ b/app/models/concerns/diff_positionable_note.rb
@@ -17,12 +17,14 @@ module DiffPositionableNote
%i(original_position position change_position).each do |meth|
define_method "#{meth}=" do |new_position|
if new_position.is_a?(String)
- new_position = JSON.parse(new_position) rescue nil
+ new_position = Gitlab::Json.parse(new_position) rescue nil
end
if new_position.is_a?(Hash)
new_position = new_position.with_indifferent_access
new_position = Gitlab::Diff::Position.new(new_position)
+ elsif !new_position.is_a?(Gitlab::Diff::Position)
+ new_position = nil
end
return if new_position == read_attribute(meth)
diff --git a/app/models/concerns/has_repository.rb b/app/models/concerns/has_repository.rb
index af7afd6604a..29d31b8bb4f 100644
--- a/app/models/concerns/has_repository.rb
+++ b/app/models/concerns/has_repository.rb
@@ -9,7 +9,6 @@
# needs any special behavior.
module HasRepository
extend ActiveSupport::Concern
- include AfterCommitQueue
include Referable
include Gitlab::ShellAdapter
include Gitlab::Utils::StrongMemoize
diff --git a/app/models/concerns/has_user_type.rb b/app/models/concerns/has_user_type.rb
new file mode 100644
index 00000000000..8a238dc736c
--- /dev/null
+++ b/app/models/concerns/has_user_type.rb
@@ -0,0 +1,45 @@
+# frozen_string_literal: true
+
+module HasUserType
+ extend ActiveSupport::Concern
+
+ USER_TYPES = {
+ human: nil,
+ support_bot: 1,
+ alert_bot: 2,
+ visual_review_bot: 3,
+ service_user: 4,
+ ghost: 5,
+ project_bot: 6,
+ migration_bot: 7
+ }.with_indifferent_access.freeze
+
+ BOT_USER_TYPES = %w[alert_bot project_bot support_bot visual_review_bot migration_bot].freeze
+ NON_INTERNAL_USER_TYPES = %w[human project_bot service_user].freeze
+ INTERNAL_USER_TYPES = (USER_TYPES.keys - NON_INTERNAL_USER_TYPES).freeze
+
+ included do
+ scope :humans, -> { where(user_type: :human) }
+ scope :bots, -> { where(user_type: BOT_USER_TYPES) }
+ scope :bots_without_project_bot, -> { where(user_type: BOT_USER_TYPES - ['project_bot']) }
+ scope :non_internal, -> { humans.or(where(user_type: NON_INTERNAL_USER_TYPES)) }
+ scope :without_ghosts, -> { humans.or(where.not(user_type: :ghost)) }
+ scope :without_project_bot, -> { humans.or(where.not(user_type: :project_bot)) }
+
+ enum user_type: USER_TYPES
+
+ def human?
+ super || user_type.nil?
+ end
+ end
+
+ def bot?
+ BOT_USER_TYPES.include?(user_type)
+ end
+
+ # The explicit check for project_bot will be removed with Bot Categorization
+ # Ref: https://gitlab.com/gitlab-org/gitlab/-/issues/213945
+ def internal?
+ ghost? || (bot? && !project_bot?)
+ end
+end
diff --git a/app/models/concerns/has_wiki.rb b/app/models/concerns/has_wiki.rb
new file mode 100644
index 00000000000..4dd72216e77
--- /dev/null
+++ b/app/models/concerns/has_wiki.rb
@@ -0,0 +1,44 @@
+# frozen_string_literal: true
+
+module HasWiki
+ extend ActiveSupport::Concern
+
+ included do
+ validate :check_wiki_path_conflict
+ end
+
+ def create_wiki
+ wiki.wiki
+ true
+ rescue Wiki::CouldNotCreateWikiError
+ errors.add(:base, _('Failed to create wiki'))
+ false
+ end
+
+ def wiki
+ strong_memoize(:wiki) do
+ Wiki.for_container(self, self.owner)
+ end
+ end
+
+ def wiki_repository_exists?
+ wiki.repository_exists?
+ end
+
+ def after_wiki_activity
+ true
+ end
+
+ private
+
+ def check_wiki_path_conflict
+ return if path.blank?
+
+ path_to_check = path.ends_with?('.wiki') ? path.chomp('.wiki') : "#{path}.wiki"
+
+ if Project.in_namespace(parent_id).where(path: path_to_check).exists? ||
+ GroupsFinder.new(nil, parent: parent_id).execute.where(path: path_to_check).exists?
+ errors.add(:name, _('has already been taken'))
+ end
+ end
+end
diff --git a/app/models/concerns/issuable.rb b/app/models/concerns/issuable.rb
index 37f2209b9d2..a1b14dca4ac 100644
--- a/app/models/concerns/issuable.rb
+++ b/app/models/concerns/issuable.rb
@@ -115,9 +115,31 @@ module Issuable
end
# rubocop:enable GitlabSecurity/SqlInjection
+ scope :not_assigned_to, ->(users) do
+ assignees_table = Arel::Table.new("#{to_ability_name}_assignees")
+ sql = assignees_table.project('true')
+ .where(assignees_table[:user_id].in(users))
+ .where(Arel::Nodes::SqlLiteral.new("#{to_ability_name}_id = #{to_ability_name}s.id"))
+ where(sql.exists.not)
+ end
+
+ scope :without_particular_labels, ->(label_names) do
+ labels_table = Label.arel_table
+ label_links_table = LabelLink.arel_table
+ issuables_table = klass.arel_table
+ inner_query = label_links_table.project('true')
+ .join(labels_table, Arel::Nodes::InnerJoin).on(labels_table[:id].eq(label_links_table[:label_id]))
+ .where(label_links_table[:target_type].eq(name)
+ .and(label_links_table[:target_id].eq(issuables_table[:id]))
+ .and(labels_table[:title].in(label_names)))
+ .exists.not
+
+ where(inner_query)
+ end
+
scope :without_label, -> { joins("LEFT OUTER JOIN label_links ON label_links.target_type = '#{name}' AND label_links.target_id = #{table_name}.id").where(label_links: { id: nil }) }
scope :with_label_ids, ->(label_ids) { joins(:label_links).where(label_links: { label_id: label_ids }) }
- scope :any_label, -> { joins(:label_links).group(:id) }
+ scope :any_label, -> { joins(:label_links).distinct }
scope :join_project, -> { joins(:project) }
scope :inc_notes_with_associations, -> { includes(notes: [:project, :author, :award_emoji]) }
scope :references_project, -> { references(:project) }
@@ -286,9 +308,8 @@ module Issuable
.reorder(Gitlab::Database.nulls_last_order('highest_priority', direction))
end
- def with_label(title, sort = nil, not_query: false)
- multiple_labels = title.is_a?(Array) && title.size > 1
- if multiple_labels && !not_query
+ def with_label(title, sort = nil)
+ if title.is_a?(Array) && title.size > 1
joins(:labels).where(labels: { title: title }).group(*grouping_columns(sort)).having("COUNT(DISTINCT labels.title) = #{title.size}")
else
joins(:labels).where(labels: { title: title })
diff --git a/app/models/concerns/issue_resource_event.rb b/app/models/concerns/issue_resource_event.rb
new file mode 100644
index 00000000000..1c24032dbbb
--- /dev/null
+++ b/app/models/concerns/issue_resource_event.rb
@@ -0,0 +1,13 @@
+# frozen_string_literal: true
+
+module IssueResourceEvent
+ extend ActiveSupport::Concern
+
+ included do
+ belongs_to :issue
+
+ scope :by_issue, ->(issue) { where(issue_id: issue.id) }
+
+ scope :by_issue_ids_and_created_at_earlier_or_equal_to, ->(issue_ids, time) { where(issue_id: issue_ids).where('created_at <= ?', time) }
+ end
+end
diff --git a/app/models/concerns/limitable.rb b/app/models/concerns/limitable.rb
new file mode 100644
index 00000000000..f320f54bb82
--- /dev/null
+++ b/app/models/concerns/limitable.rb
@@ -0,0 +1,27 @@
+# frozen_string_literal: true
+
+module Limitable
+ extend ActiveSupport::Concern
+
+ included do
+ class_attribute :limit_scope
+ class_attribute :limit_name
+ self.limit_name = self.name.demodulize.tableize
+
+ validate :validate_plan_limit_not_exceeded, on: :create
+ end
+
+ private
+
+ def validate_plan_limit_not_exceeded
+ scope_relation = self.public_send(limit_scope) # rubocop:disable GitlabSecurity/PublicSend
+ return unless scope_relation
+
+ relation = self.class.where(limit_scope => scope_relation)
+
+ if scope_relation.actual_limits.exceeded?(limit_name, relation)
+ errors.add(:base, _("Maximum number of %{name} (%{count}) exceeded") %
+ { name: limit_name.humanize(capitalize: false), count: scope_relation.actual_limits.public_send(limit_name) }) # rubocop:disable GitlabSecurity/PublicSend
+ end
+ end
+end
diff --git a/app/models/concerns/merge_request_resource_event.rb b/app/models/concerns/merge_request_resource_event.rb
new file mode 100644
index 00000000000..7fb7fb4ec62
--- /dev/null
+++ b/app/models/concerns/merge_request_resource_event.rb
@@ -0,0 +1,11 @@
+# frozen_string_literal: true
+
+module MergeRequestResourceEvent
+ extend ActiveSupport::Concern
+
+ included do
+ belongs_to :merge_request
+
+ scope :by_merge_request, ->(merge_request) { where(merge_request_id: merge_request.id) }
+ end
+end
diff --git a/app/models/concerns/milestoneable.rb b/app/models/concerns/milestoneable.rb
index 3ffb32f94fc..8f8494a9678 100644
--- a/app/models/concerns/milestoneable.rb
+++ b/app/models/concerns/milestoneable.rb
@@ -17,8 +17,10 @@ module Milestoneable
scope :of_milestones, ->(ids) { where(milestone_id: ids) }
scope :any_milestone, -> { where('milestone_id IS NOT NULL') }
scope :with_milestone, ->(title) { left_joins_milestones.where(milestones: { title: title }) }
+ scope :without_particular_milestone, ->(title) { left_outer_joins(:milestone).where("milestones.title != ? OR milestone_id IS NULL", title) }
scope :any_release, -> { joins_milestone_releases }
scope :with_release, -> (tag, project_id) { joins_milestone_releases.where( milestones: { releases: { tag: tag, project_id: project_id } } ) }
+ scope :without_particular_release, -> (tag, project_id) { joins_milestone_releases.where.not( milestones: { releases: { tag: tag, project_id: project_id } } ) }
scope :left_joins_milestones, -> { joins("LEFT OUTER JOIN milestones ON #{table_name}.milestone_id = milestones.id") }
scope :order_milestone_due_desc, -> { left_joins_milestones.reorder(Arel.sql('milestones.due_date IS NULL, milestones.id IS NULL, milestones.due_date DESC')) }
diff --git a/app/models/concerns/noteable.rb b/app/models/concerns/noteable.rb
index a7f1fb66a88..933a0b167e2 100644
--- a/app/models/concerns/noteable.rb
+++ b/app/models/concerns/noteable.rb
@@ -17,7 +17,7 @@ module Noteable
# `Noteable` class names that support resolvable notes.
def resolvable_types
- %w(MergeRequest)
+ %w(MergeRequest DesignManagement::Design)
end
end
@@ -138,15 +138,25 @@ module Noteable
end
def note_etag_key
+ return Gitlab::Routing.url_helpers.designs_project_issue_path(project, issue, { vueroute: filename }) if self.is_a?(DesignManagement::Design)
+
Gitlab::Routing.url_helpers.project_noteable_notes_path(
project,
target_type: self.class.name.underscore,
target_id: id
)
end
+
+ def after_note_created(_note)
+ # no-op
+ end
+
+ def after_note_destroyed(_note)
+ # no-op
+ end
end
Noteable.extend(Noteable::ClassMethods)
-Noteable::ClassMethods.prepend_if_ee('EE::Noteable::ClassMethods') # rubocop: disable Cop/InjectEnterpriseEditionModule
+Noteable::ClassMethods.prepend_if_ee('EE::Noteable::ClassMethods')
Noteable.prepend_if_ee('EE::Noteable')
diff --git a/app/models/concerns/prometheus_adapter.rb b/app/models/concerns/prometheus_adapter.rb
index abc41a1c476..761a151a474 100644
--- a/app/models/concerns/prometheus_adapter.rb
+++ b/app/models/concerns/prometheus_adapter.rb
@@ -9,6 +9,7 @@ module PrometheusAdapter
self.reactive_cache_lease_timeout = 30.seconds
self.reactive_cache_refresh_interval = 30.seconds
self.reactive_cache_lifetime = 1.minute
+ self.reactive_cache_work_type = :external_dependency
def prometheus_client
raise NotImplementedError
diff --git a/app/models/concerns/protected_ref_access.rb b/app/models/concerns/protected_ref_access.rb
index 7373f006d64..d1e3d9b2aff 100644
--- a/app/models/concerns/protected_ref_access.rb
+++ b/app/models/concerns/protected_ref_access.rb
@@ -50,8 +50,8 @@ module ProtectedRefAccess
end
end
-ProtectedRefAccess.include_if_ee('EE::ProtectedRefAccess::Scopes') # rubocop: disable Cop/InjectEnterpriseEditionModule
-ProtectedRefAccess.prepend_if_ee('EE::ProtectedRefAccess') # rubocop: disable Cop/InjectEnterpriseEditionModule
+ProtectedRefAccess.include_if_ee('EE::ProtectedRefAccess::Scopes')
+ProtectedRefAccess.prepend_if_ee('EE::ProtectedRefAccess')
# When using `prepend` (or `include` for that matter), the `ClassMethods`
# constants are not merged. This means that `class_methods` in
diff --git a/app/models/concerns/reactive_caching.rb b/app/models/concerns/reactive_caching.rb
index 4b472cfdf45..d294563139c 100644
--- a/app/models/concerns/reactive_caching.rb
+++ b/app/models/concerns/reactive_caching.rb
@@ -8,6 +8,11 @@ module ReactiveCaching
InvalidateReactiveCache = Class.new(StandardError)
ExceededReactiveCacheLimit = Class.new(StandardError)
+ WORK_TYPE = {
+ default: ReactiveCachingWorker,
+ external_dependency: ExternalServiceReactiveCachingWorker
+ }.freeze
+
included do
extend ActiveModel::Naming
@@ -16,6 +21,7 @@ module ReactiveCaching
class_attribute :reactive_cache_refresh_interval
class_attribute :reactive_cache_lifetime
class_attribute :reactive_cache_hard_limit
+ class_attribute :reactive_cache_work_type
class_attribute :reactive_cache_worker_finder
# defaults
@@ -24,6 +30,7 @@ module ReactiveCaching
self.reactive_cache_refresh_interval = 1.minute
self.reactive_cache_lifetime = 10.minutes
self.reactive_cache_hard_limit = 1.megabyte
+ self.reactive_cache_work_type = :default
self.reactive_cache_worker_finder = ->(id, *_args) do
find_by(primary_key => id)
end
@@ -112,7 +119,7 @@ module ReactiveCaching
def refresh_reactive_cache!(*args)
clear_reactive_cache!(*args)
keep_alive_reactive_cache!(*args)
- ReactiveCachingWorker.perform_async(self.class, id, *args)
+ worker_class.perform_async(self.class, id, *args)
end
def keep_alive_reactive_cache!(*args)
@@ -145,7 +152,11 @@ module ReactiveCaching
def enqueuing_update(*args)
yield
- ReactiveCachingWorker.perform_in(self.class.reactive_cache_refresh_interval, self.class, id, *args)
+ worker_class.perform_in(self.class.reactive_cache_refresh_interval, self.class, id, *args)
+ end
+
+ def worker_class
+ WORK_TYPE.fetch(self.class.reactive_cache_work_type.to_sym)
end
def check_exceeded_reactive_cache_limit!(data)
diff --git a/app/models/concerns/redis_cacheable.rb b/app/models/concerns/redis_cacheable.rb
index 4bb4ffe2a8e..2d4ed51ce3b 100644
--- a/app/models/concerns/redis_cacheable.rb
+++ b/app/models/concerns/redis_cacheable.rb
@@ -26,7 +26,7 @@ module RedisCacheable
end
def cache_attributes(values)
- Gitlab::Redis::SharedState.with do |redis|
+ Gitlab::Redis::Cache.with do |redis|
redis.set(cache_attribute_key, values.to_json, ex: CACHED_ATTRIBUTES_EXPIRY_TIME)
end
@@ -41,9 +41,9 @@ module RedisCacheable
def cached_attributes
strong_memoize(:cached_attributes) do
- Gitlab::Redis::SharedState.with do |redis|
+ Gitlab::Redis::Cache.with do |redis|
data = redis.get(cache_attribute_key)
- JSON.parse(data, symbolize_names: true) if data
+ Gitlab::Json.parse(data, symbolize_names: true) if data
end
end
end
diff --git a/app/models/concerns/spammable.rb b/app/models/concerns/spammable.rb
index 4fbb5dcb649..9cd1a22b203 100644
--- a/app/models/concerns/spammable.rb
+++ b/app/models/concerns/spammable.rb
@@ -13,9 +13,13 @@ module Spammable
has_one :user_agent_detail, as: :subject, dependent: :destroy # rubocop:disable Cop/ActiveRecordDependent
attr_accessor :spam
+ attr_accessor :needs_recaptcha
attr_accessor :spam_log
+
alias_method :spam?, :spam
+ alias_method :needs_recaptcha?, :needs_recaptcha
+ # if spam errors are added before validation, they will be wiped
after_validation :invalidate_if_spam, on: [:create, :update]
cattr_accessor :spammable_attrs, instance_accessor: false do
@@ -38,24 +42,35 @@ module Spammable
end
def needs_recaptcha!
- self.errors.add(:base, "Your #{spammable_entity_type} has been recognized as spam. "\
- "Please, change the content or solve the reCAPTCHA to proceed.")
+ self.needs_recaptcha = true
end
- def unrecoverable_spam_error!
- self.errors.add(:base, "Your #{spammable_entity_type} has been recognized as spam and has been discarded.")
+ def spam!
+ self.spam = true
end
- def invalidate_if_spam
- return unless spam?
+ def clear_spam_flags!
+ self.spam = false
+ self.needs_recaptcha = false
+ end
- if Gitlab::Recaptcha.enabled?
- needs_recaptcha!
- else
+ def invalidate_if_spam
+ if needs_recaptcha? && Gitlab::Recaptcha.enabled?
+ recaptcha_error!
+ elsif needs_recaptcha? || spam?
unrecoverable_spam_error!
end
end
+ def recaptcha_error!
+ self.errors.add(:base, "Your #{spammable_entity_type} has been recognized as spam. "\
+ "Please, change the content or solve the reCAPTCHA to proceed.")
+ end
+
+ def unrecoverable_spam_error!
+ self.errors.add(:base, "Your #{spammable_entity_type} has been recognized as spam and has been discarded.")
+ end
+
def spammable_entity_type
self.class.name.underscore
end
diff --git a/app/models/concerns/state_eventable.rb b/app/models/concerns/state_eventable.rb
new file mode 100644
index 00000000000..68129798543
--- /dev/null
+++ b/app/models/concerns/state_eventable.rb
@@ -0,0 +1,9 @@
+# frozen_string_literal: true
+
+module StateEventable
+ extend ActiveSupport::Concern
+
+ included do
+ has_many :resource_state_events
+ end
+end
diff --git a/app/models/concerns/storage/legacy_project_wiki.rb b/app/models/concerns/storage/legacy_project_wiki.rb
deleted file mode 100644
index a377fa1e5de..00000000000
--- a/app/models/concerns/storage/legacy_project_wiki.rb
+++ /dev/null
@@ -1,11 +0,0 @@
-# frozen_string_literal: true
-
-module Storage
- module LegacyProjectWiki
- extend ActiveSupport::Concern
-
- def disk_path
- project.disk_path + '.wiki'
- end
- end
-end
diff --git a/app/models/concerns/timebox.rb b/app/models/concerns/timebox.rb
new file mode 100644
index 00000000000..d29e6a01c56
--- /dev/null
+++ b/app/models/concerns/timebox.rb
@@ -0,0 +1,204 @@
+# frozen_string_literal: true
+
+module Timebox
+ extend ActiveSupport::Concern
+
+ include AtomicInternalId
+ include CacheMarkdownField
+ include Gitlab::SQL::Pattern
+ include IidRoutes
+ include StripAttribute
+
+ TimeboxStruct = Struct.new(:title, :name, :id) do
+ # Ensure these models match the interface required for exporting
+ def serializable_hash(_opts = {})
+ { title: title, name: name, id: id }
+ end
+ end
+
+ # Represents a "No Timebox" state used for filtering Issues and Merge
+ # Requests that have no timeboxes assigned.
+ None = TimeboxStruct.new('No Timebox', 'No Timebox', 0)
+ Any = TimeboxStruct.new('Any Timebox', '', -1)
+ Upcoming = TimeboxStruct.new('Upcoming', '#upcoming', -2)
+ Started = TimeboxStruct.new('Started', '#started', -3)
+
+ included do
+ # Defines the same constants above, but inside the including class.
+ const_set :None, TimeboxStruct.new("No #{self.name}", "No #{self.name}", 0)
+ const_set :Any, TimeboxStruct.new("Any #{self.name}", '', -1)
+ const_set :Upcoming, TimeboxStruct.new('Upcoming', '#upcoming', -2)
+ const_set :Started, TimeboxStruct.new('Started', '#started', -3)
+
+ alias_method :timebox_id, :id
+
+ validates :group, presence: true, unless: :project
+ validates :project, presence: true, unless: :group
+ validates :title, presence: true
+
+ validate :uniqueness_of_title, if: :title_changed?
+ validate :timebox_type_check
+ validate :start_date_should_be_less_than_due_date, if: proc { |m| m.start_date.present? && m.due_date.present? }
+ validate :dates_within_4_digits
+
+ cache_markdown_field :title, pipeline: :single_line
+ cache_markdown_field :description
+
+ belongs_to :project
+ belongs_to :group
+
+ has_many :issues
+ has_many :labels, -> { distinct.reorder('labels.title') }, through: :issues
+ has_many :merge_requests
+
+ scope :of_projects, ->(ids) { where(project_id: ids) }
+ scope :of_groups, ->(ids) { where(group_id: ids) }
+ scope :closed, -> { with_state(:closed) }
+ scope :for_projects, -> { where(group: nil).includes(:project) }
+ scope :with_title, -> (title) { where(title: title) }
+
+ scope :for_projects_and_groups, -> (projects, groups) do
+ projects = projects.compact if projects.is_a? Array
+ projects = [] if projects.nil?
+
+ groups = groups.compact if groups.is_a? Array
+ groups = [] if groups.nil?
+
+ where(project_id: projects).or(where(group_id: groups))
+ end
+
+ scope :within_timeframe, -> (start_date, end_date) do
+ where('start_date is not NULL or due_date is not NULL')
+ .where('start_date is NULL or start_date <= ?', end_date)
+ .where('due_date is NULL or due_date >= ?', start_date)
+ end
+
+ strip_attributes :title
+
+ alias_attribute :name, :title
+ end
+
+ class_methods do
+ # Searches for timeboxes with a matching title or description.
+ #
+ # This method uses ILIKE on PostgreSQL
+ #
+ # query - The search query as a String
+ #
+ # Returns an ActiveRecord::Relation.
+ def search(query)
+ fuzzy_search(query, [:title, :description])
+ end
+
+ # Searches for timeboxes with a matching title.
+ #
+ # This method uses ILIKE on PostgreSQL
+ #
+ # query - The search query as a String
+ #
+ # Returns an ActiveRecord::Relation.
+ def search_title(query)
+ fuzzy_search(query, [:title])
+ end
+
+ def filter_by_state(timeboxes, state)
+ case state
+ when 'closed' then timeboxes.closed
+ when 'all' then timeboxes
+ else timeboxes.active
+ end
+ end
+
+ def count_by_state
+ reorder(nil).group(:state).count
+ end
+
+ def predefined_id?(id)
+ [Any.id, None.id, Upcoming.id, Started.id].include?(id)
+ end
+
+ def predefined?(timebox)
+ predefined_id?(timebox&.id)
+ end
+ end
+
+ def title=(value)
+ write_attribute(:title, sanitize_title(value)) if value.present?
+ end
+
+ def timebox_name
+ model_name.singular
+ end
+
+ def group_timebox?
+ group_id.present?
+ end
+
+ def project_timebox?
+ project_id.present?
+ end
+
+ def safe_title
+ title.to_slug.normalize.to_s
+ end
+
+ def resource_parent
+ group || project
+ end
+
+ def to_ability_name
+ model_name.singular
+ end
+
+ def merge_requests_enabled?
+ if group_timebox?
+ # Assume that groups have at least one project with merge requests enabled.
+ # Otherwise, we would need to load all of the projects from the database.
+ true
+ elsif project_timebox?
+ project&.merge_requests_enabled?
+ end
+ end
+
+ private
+
+ # Timebox titles must be unique across project and group timeboxes
+ def uniqueness_of_title
+ if project
+ relation = self.class.for_projects_and_groups([project_id], [project.group&.id])
+ elsif group
+ relation = self.class.for_projects_and_groups(group.projects.select(:id), [group.id])
+ end
+
+ title_exists = relation.find_by_title(title)
+ errors.add(:title, _("already being used for another group or project %{timebox_name}.") % { timebox_name: timebox_name }) if title_exists
+ end
+
+ # Timebox should be either a project timebox or a group timebox
+ def timebox_type_check
+ if group_id && project_id
+ field = project_id_changed? ? :project_id : :group_id
+ errors.add(field, _("%{timebox_name} should belong either to a project or a group.") % { timebox_name: timebox_name })
+ end
+ end
+
+ def start_date_should_be_less_than_due_date
+ if due_date <= start_date
+ errors.add(:due_date, _("must be greater than start date"))
+ end
+ end
+
+ def dates_within_4_digits
+ if start_date && start_date > Date.new(9999, 12, 31)
+ errors.add(:start_date, _("date must not be after 9999-12-31"))
+ end
+
+ if due_date && due_date > Date.new(9999, 12, 31)
+ errors.add(:due_date, _("date must not be after 9999-12-31"))
+ end
+ end
+
+ def sanitize_title(value)
+ CGI.unescape_html(Sanitize.clean(value.to_s))
+ end
+end
diff --git a/app/models/concerns/update_project_statistics.rb b/app/models/concerns/update_project_statistics.rb
index a84fb1cf56d..6cf012680d8 100644
--- a/app/models/concerns/update_project_statistics.rb
+++ b/app/models/concerns/update_project_statistics.rb
@@ -68,21 +68,11 @@ module UpdateProjectStatistics
def schedule_update_project_statistic(delta)
return if delta.zero?
+ return if project.nil?
- if Feature.enabled?(:update_project_statistics_after_commit, default_enabled: true)
- # Update ProjectStatistics after the transaction
- run_after_commit do
- ProjectStatistics.increment_statistic(
- project_id, self.class.project_statistics_name, delta)
- end
- else
- # Use legacy-way to update within transaction
+ run_after_commit do
ProjectStatistics.increment_statistic(
project_id, self.class.project_statistics_name, delta)
- end
-
- run_after_commit do
- next if project.nil?
Namespaces::ScheduleAggregationWorker.perform_async(
project.namespace_id)
diff --git a/app/models/container_repository.rb b/app/models/container_repository.rb
index 3bff7cb06c1..455c672cea3 100644
--- a/app/models/container_repository.rb
+++ b/app/models/container_repository.rb
@@ -2,6 +2,7 @@
class ContainerRepository < ApplicationRecord
include Gitlab::Utils::StrongMemoize
+ include Gitlab::SQL::Pattern
belongs_to :project
@@ -17,6 +18,7 @@ class ContainerRepository < ApplicationRecord
scope :for_group_and_its_subgroups, ->(group) do
where(project_id: Project.for_group_and_its_subgroups(group).with_container_registry.select(:id))
end
+ scope :search_by_name, ->(query) { fuzzy_search(query, [:name], use_minimum_char_limit: false) }
def self.exists_by_path?(path)
where(
diff --git a/app/models/cycle_analytics/group_level.rb b/app/models/cycle_analytics/group_level.rb
deleted file mode 100644
index a41e1375484..00000000000
--- a/app/models/cycle_analytics/group_level.rb
+++ /dev/null
@@ -1,29 +0,0 @@
-# frozen_string_literal: true
-
-module CycleAnalytics
- class GroupLevel
- include LevelBase
- attr_reader :options, :group
-
- def initialize(group:, options:)
- @group = group
- @options = options.merge(group: group)
- end
-
- def summary
- @summary ||= ::Gitlab::CycleAnalytics::GroupStageSummary.new(group, options: options).data
- end
-
- def permissions(*)
- STAGES.each_with_object({}) do |stage, obj|
- obj[stage] = true
- end
- end
-
- def stats
- @stats ||= STAGES.map do |stage_name|
- self[stage_name].as_json(serializer: GroupAnalyticsStageSerializer)
- end
- end
- end
-end
diff --git a/app/models/deploy_token.rb b/app/models/deploy_token.rb
index 69245710f01..395260b5201 100644
--- a/app/models/deploy_token.rb
+++ b/app/models/deploy_token.rb
@@ -7,7 +7,8 @@ class DeployToken < ApplicationRecord
include Gitlab::Utils::StrongMemoize
add_authentication_token_field :token, encrypted: :optional
- AVAILABLE_SCOPES = %i(read_repository read_registry write_registry).freeze
+ AVAILABLE_SCOPES = %i(read_repository read_registry write_registry
+ read_package_registry write_package_registry).freeze
GITLAB_DEPLOY_TOKEN_NAME = 'gitlab-deploy-token'
default_value_for(:expires_at) { Forever.date }
@@ -105,7 +106,7 @@ class DeployToken < ApplicationRecord
end
def ensure_at_least_one_scope
- errors.add(:base, _("Scopes can't be blank")) unless read_repository || read_registry || write_registry
+ errors.add(:base, _("Scopes can't be blank")) unless scopes.any?
end
def default_username
diff --git a/app/models/design_management.rb b/app/models/design_management.rb
new file mode 100644
index 00000000000..81e170f7e59
--- /dev/null
+++ b/app/models/design_management.rb
@@ -0,0 +1,13 @@
+# frozen_string_literal: true
+
+module DesignManagement
+ DESIGN_IMAGE_SIZES = %w(v432x230).freeze
+
+ def self.designs_directory
+ 'designs'
+ end
+
+ def self.table_name_prefix
+ 'design_management_'
+ end
+end
diff --git a/app/models/design_management/action.rb b/app/models/design_management/action.rb
new file mode 100644
index 00000000000..ecd7973a523
--- /dev/null
+++ b/app/models/design_management/action.rb
@@ -0,0 +1,44 @@
+# frozen_string_literal: true
+
+require_dependency 'design_management'
+
+module DesignManagement
+ class Action < ApplicationRecord
+ include WithUploads
+
+ self.table_name = "#{DesignManagement.table_name_prefix}designs_versions"
+
+ mount_uploader :image_v432x230, DesignManagement::DesignV432x230Uploader
+
+ belongs_to :design, class_name: "DesignManagement::Design", inverse_of: :actions
+ belongs_to :version, class_name: "DesignManagement::Version", inverse_of: :actions
+
+ enum event: { creation: 0, modification: 1, deletion: 2 }
+
+ # we assume sequential ordering.
+ scope :ordered, -> { order(version_id: :asc) }
+
+ # For each design, only select the most recent action
+ scope :most_recent, -> do
+ selection = Arel.sql("DISTINCT ON (#{table_name}.design_id) #{table_name}.*")
+
+ order(arel_table[:design_id].asc, arel_table[:version_id].desc).select(selection)
+ end
+
+ # Find all records created before or at the given version, or all if nil
+ scope :up_to_version, ->(version = nil) do
+ case version
+ when nil
+ all
+ when DesignManagement::Version
+ where(arel_table[:version_id].lteq(version.id))
+ when ::Gitlab::Git::COMMIT_ID
+ versions = DesignManagement::Version.arel_table
+ subquery = versions.project(versions[:id]).where(versions[:sha].eq(version))
+ where(arel_table[:version_id].lteq(subquery))
+ else
+ raise ArgumentError, "Expected a DesignManagement::Version, got #{version}"
+ end
+ end
+ end
+end
diff --git a/app/models/design_management/design.rb b/app/models/design_management/design.rb
new file mode 100644
index 00000000000..e9b69eab7a7
--- /dev/null
+++ b/app/models/design_management/design.rb
@@ -0,0 +1,266 @@
+# frozen_string_literal: true
+
+module DesignManagement
+ class Design < ApplicationRecord
+ include Importable
+ include Noteable
+ include Gitlab::FileTypeDetection
+ include Gitlab::Utils::StrongMemoize
+ include Referable
+ include Mentionable
+ include WhereComposite
+
+ belongs_to :project, inverse_of: :designs
+ belongs_to :issue
+
+ has_many :actions
+ has_many :versions, through: :actions, class_name: 'DesignManagement::Version', inverse_of: :designs
+ # This is a polymorphic association, so we can't count on FK's to delete the
+ # data
+ has_many :notes, as: :noteable, dependent: :delete_all # rubocop:disable Cop/ActiveRecordDependent
+ has_many :user_mentions, class_name: 'DesignUserMention', dependent: :delete_all # rubocop:disable Cop/ActiveRecordDependent
+
+ validates :project, :filename, presence: true
+ validates :issue, presence: true, unless: :importing?
+ validates :filename, uniqueness: { scope: :issue_id }
+ validate :validate_file_is_image
+
+ alias_attribute :title, :filename
+
+ # Pre-fetching scope to include the data necessary to construct a
+ # reference using `to_reference`.
+ scope :for_reference, -> { includes(issue: [{ project: [:route, :namespace] }]) }
+
+ # A design can be uniquely identified by issue_id and filename
+ # Takes one or more sets of composite IDs of the form:
+ # `{issue_id: Integer, filename: String}`.
+ #
+ # @see WhereComposite::where_composite
+ #
+ # e.g:
+ #
+ # by_issue_id_and_filename(issue_id: 1, filename: 'homescreen.jpg')
+ # by_issue_id_and_filename([]) # returns ActiveRecord::NullRelation
+ # by_issue_id_and_filename([
+ # { issue_id: 1, filename: 'homescreen.jpg' },
+ # { issue_id: 2, filename: 'homescreen.jpg' },
+ # { issue_id: 1, filename: 'menu.png' }
+ # ])
+ #
+ scope :by_issue_id_and_filename, ->(composites) do
+ where_composite(%i[issue_id filename], composites)
+ end
+
+ # Find designs visible at the given version
+ #
+ # @param version [nil, DesignManagement::Version]:
+ # the version at which the designs must be visible
+ # Passing `nil` is the same as passing the most current version
+ #
+ # Restricts to designs
+ # - created at least *before* the given version
+ # - not deleted as of the given version.
+ #
+ # As a query, we ascertain this by finding the last event prior to
+ # (or equal to) the cut-off, and seeing whether that version was a deletion.
+ scope :visible_at_version, -> (version) do
+ deletion = ::DesignManagement::Action.events[:deletion]
+ designs = arel_table
+ actions = ::DesignManagement::Action
+ .most_recent.up_to_version(version)
+ .arel.as('most_recent_actions')
+
+ join = designs.join(actions)
+ .on(actions[:design_id].eq(designs[:id]))
+
+ joins(join.join_sources).where(actions[:event].not_eq(deletion)).order(:id)
+ end
+
+ scope :with_filename, -> (filenames) { where(filename: filenames) }
+ scope :on_issue, ->(issue) { where(issue_id: issue) }
+
+ # Scope called by our REST API to avoid N+1 problems
+ scope :with_api_entity_associations, -> { preload(:issue) }
+
+ # A design is current if the most recent event is not a deletion
+ scope :current, -> { visible_at_version(nil) }
+
+ def status
+ if new_design?
+ :new
+ elsif deleted?
+ :deleted
+ else
+ :current
+ end
+ end
+
+ def deleted?
+ most_recent_action&.deletion?
+ end
+
+ # A design is visible_in? a version if:
+ # * it was created before that version
+ # * the most recent action before the version was not a deletion
+ def visible_in?(version)
+ map = strong_memoize(:visible_in) do
+ Hash.new do |h, k|
+ h[k] = self.class.visible_at_version(k).where(id: id).exists?
+ end
+ end
+
+ map[version]
+ end
+
+ def most_recent_action
+ strong_memoize(:most_recent_action) { actions.ordered.last }
+ end
+
+ # A reference for a design is the issue reference, indexed by the filename
+ # with an optional infix when full.
+ #
+ # e.g.
+ # #123[homescreen.png]
+ # other-project#72[sidebar.jpg]
+ # #38/designs[transition.gif]
+ # #12["filename with [] in it.jpg"]
+ def to_reference(from = nil, full: false)
+ infix = full ? '/designs' : ''
+ totally_simple = %r{ \A #{self.class.simple_file_name} \z }x
+ safe_name = if totally_simple.match?(filename)
+ filename
+ elsif filename =~ /[<>]/
+ %Q{base64:#{Base64.strict_encode64(filename)}}
+ else
+ escaped = filename.gsub(%r{[\\"]}) { |x| "\\#{x}" }
+ %Q{"#{escaped}"}
+ end
+
+ "#{issue.to_reference(from, full: full)}#{infix}[#{safe_name}]"
+ end
+
+ def self.reference_pattern
+ @reference_pattern ||= begin
+ # Filenames can be escaped with double quotes to name filenames
+ # that include square brackets, or other special characters
+ %r{
+ #{Issue.reference_pattern}
+ (\/designs)?
+ \[
+ (?<design> #{simple_file_name} | #{quoted_file_name} | #{base_64_encoded_name})
+ \]
+ }x
+ end
+ end
+
+ def self.simple_file_name
+ %r{
+ (?<simple_file_name>
+ ( \w | [_:,'-] | \. | \s )+
+ \.
+ \w+
+ )
+ }x
+ end
+
+ def self.base_64_encoded_name
+ %r{
+ base64:
+ (?<base_64_encoded_name>
+ [A-Za-z0-9+\n]+
+ =?
+ )
+ }x
+ end
+
+ def self.quoted_file_name
+ %r{
+ "
+ (?<escaped_filename>
+ (\\ \\ | \\ " | [^"\\])+
+ )
+ "
+ }x
+ end
+
+ def self.link_reference_pattern
+ @link_reference_pattern ||= begin
+ exts = SAFE_IMAGE_EXT + DANGEROUS_IMAGE_EXT
+ path_segment = %r{issues/#{Gitlab::Regex.issue}/designs}
+ filename_pattern = %r{(?<simple_file_name>[a-z0-9_=-]+\.(#{exts.join('|')}))}i
+
+ super(path_segment, filename_pattern)
+ end
+ end
+
+ def to_ability_name
+ 'design'
+ end
+
+ def description
+ ''
+ end
+
+ def new_design?
+ strong_memoize(:new_design) { actions.none? }
+ end
+
+ def full_path
+ @full_path ||= File.join(DesignManagement.designs_directory, "issue-#{issue.iid}", filename)
+ end
+
+ def diff_refs
+ strong_memoize(:diff_refs) { head_version&.diff_refs }
+ end
+
+ def clear_version_cache
+ [versions, actions].each(&:reset)
+ %i[new_design diff_refs head_sha visible_in most_recent_action].each do |key|
+ clear_memoization(key)
+ end
+ end
+
+ def repository
+ project.design_repository
+ end
+
+ def user_notes_count
+ user_notes_count_service.count
+ end
+
+ def after_note_changed(note)
+ user_notes_count_service.delete_cache unless note.system?
+ end
+ alias_method :after_note_created, :after_note_changed
+ alias_method :after_note_destroyed, :after_note_changed
+
+ private
+
+ def head_version
+ strong_memoize(:head_sha) { versions.ordered.first }
+ end
+
+ def allow_dangerous_images?
+ Feature.enabled?(:design_management_allow_dangerous_images, project)
+ end
+
+ def valid_file_extensions
+ allow_dangerous_images? ? (SAFE_IMAGE_EXT + DANGEROUS_IMAGE_EXT) : SAFE_IMAGE_EXT
+ end
+
+ def validate_file_is_image
+ unless image? || (dangerous_image? && allow_dangerous_images?)
+ message = _('does not have a supported extension. Only %{extension_list} are supported') % {
+ extension_list: valid_file_extensions.to_sentence
+ }
+ errors.add(:filename, message)
+ end
+ end
+
+ def user_notes_count_service
+ strong_memoize(:user_notes_count_service) do
+ ::DesignManagement::DesignUserNotesCountService.new(self) # rubocop: disable CodeReuse/ServiceClass
+ end
+ end
+ end
+end
diff --git a/app/models/design_management/design_action.rb b/app/models/design_management/design_action.rb
new file mode 100644
index 00000000000..22baa916296
--- /dev/null
+++ b/app/models/design_management/design_action.rb
@@ -0,0 +1,64 @@
+# frozen_string_literal: true
+
+module DesignManagement
+ # Parameter object which is a tuple of the database record and the
+ # last gitaly call made to change it. This serves to perform the
+ # logical mapping from git action to database representation.
+ class DesignAction
+ include ActiveModel::Validations
+
+ EVENT_FOR_GITALY_ACTION = {
+ create: DesignManagement::Action.events[:creation],
+ update: DesignManagement::Action.events[:modification],
+ delete: DesignManagement::Action.events[:deletion]
+ }.freeze
+
+ attr_reader :design, :action, :content
+
+ delegate :issue_id, to: :design
+
+ validates :design, presence: true
+ validates :action, presence: true, inclusion: { in: EVENT_FOR_GITALY_ACTION.keys }
+ validates :content,
+ absence: { if: :forbids_content?,
+ message: 'this action forbids content' },
+ presence: { if: :needs_content?,
+ message: 'this action needs content' }
+
+ # Parameters:
+ # - design [DesignManagement::Design]: the design that was changed
+ # - action [Symbol]: the action that gitaly performed
+ def initialize(design, action, content = nil)
+ @design, @action, @content = design, action, content
+ validate!
+ end
+
+ def row_attrs(version)
+ { design_id: design.id, version_id: version.id, event: event }
+ end
+
+ def gitaly_action
+ { action: action, file_path: design.full_path, content: content }.compact
+ end
+
+ # This action has been performed - do any post-creation actions
+ # such as clearing method caches.
+ def performed
+ design.clear_version_cache
+ end
+
+ private
+
+ def needs_content?
+ action != :delete
+ end
+
+ def forbids_content?
+ action == :delete
+ end
+
+ def event
+ EVENT_FOR_GITALY_ACTION[action]
+ end
+ end
+end
diff --git a/app/models/design_management/design_at_version.rb b/app/models/design_management/design_at_version.rb
new file mode 100644
index 00000000000..b4cafb93c2c
--- /dev/null
+++ b/app/models/design_management/design_at_version.rb
@@ -0,0 +1,119 @@
+# frozen_string_literal: true
+
+# Tuple of design and version
+# * has a composite ID, with lazy_find
+module DesignManagement
+ class DesignAtVersion
+ include ActiveModel::Validations
+ include GlobalID::Identification
+ include Gitlab::Utils::StrongMemoize
+
+ attr_reader :version
+ attr_reader :design
+
+ validates :version, presence: true
+ validates :design, presence: true
+
+ validate :design_and_version_belong_to_the_same_issue
+ validate :design_and_version_have_issue_id
+
+ def initialize(design: nil, version: nil)
+ @design, @version = design, version
+ end
+
+ def self.instantiate(attrs)
+ new(attrs).tap { |obj| obj.validate! }
+ end
+
+ # The ID, needed by GraphQL types and as part of the Lazy-fetch
+ # protocol, includes information about both the design and the version.
+ #
+ # The particular format is not interesting, and should be treated as opaque
+ # by all callers.
+ def id
+ "#{design.id}.#{version.id}"
+ end
+
+ def ==(other)
+ return false unless other && self.class == other.class
+
+ other.id == id
+ end
+
+ alias_method :eql?, :==
+
+ def self.lazy_find(id)
+ BatchLoader.for(id).batch do |ids, callback|
+ find(ids).each do |record|
+ callback.call(record.id, record)
+ end
+ end
+ end
+
+ def self.find(ids)
+ pairs = ids.map { |id| id.split('.').map(&:to_i) }
+
+ design_ids = pairs.map(&:first).uniq
+ version_ids = pairs.map(&:second).uniq
+
+ designs = ::DesignManagement::Design
+ .where(id: design_ids)
+ .index_by(&:id)
+
+ versions = ::DesignManagement::Version
+ .where(id: version_ids)
+ .index_by(&:id)
+
+ pairs.map do |(design_id, version_id)|
+ design = designs[design_id]
+ version = versions[version_id]
+
+ obj = new(design: design, version: version)
+
+ obj if obj.valid?
+ end.compact
+ end
+
+ def status
+ if not_created_yet?
+ :not_created_yet
+ elsif deleted?
+ :deleted
+ else
+ :current
+ end
+ end
+
+ def deleted?
+ action&.deletion?
+ end
+
+ def not_created_yet?
+ action.nil?
+ end
+
+ private
+
+ def action
+ strong_memoize(:most_recent_action) do
+ ::DesignManagement::Action
+ .most_recent.up_to_version(version)
+ .find_by(design: design)
+ end
+ end
+
+ def design_and_version_belong_to_the_same_issue
+ id_a, id_b = [design, version].map { |obj| obj&.issue_id }
+
+ return if id_a == id_b
+
+ errors.add(:issue, 'must be the same on design and version')
+ end
+
+ def design_and_version_have_issue_id
+ return if [design, version].all? { |obj| obj.try(:issue_id).present? }
+
+ errors.add(:issue, 'must be present on both design and version')
+ end
+ end
+end
diff --git a/app/models/design_management/design_collection.rb b/app/models/design_management/design_collection.rb
new file mode 100644
index 00000000000..18d1541e9c7
--- /dev/null
+++ b/app/models/design_management/design_collection.rb
@@ -0,0 +1,30 @@
+# frozen_string_literal: true
+
+module DesignManagement
+ class DesignCollection
+ attr_reader :issue
+
+ delegate :designs, :project, to: :issue
+
+ def initialize(issue)
+ @issue = issue
+ end
+
+ def find_or_create_design!(filename:)
+ designs.find { |design| design.filename == filename } ||
+ designs.safe_find_or_create_by!(project: project, filename: filename)
+ end
+
+ def versions
+ @versions ||= DesignManagement::Version.for_designs(designs)
+ end
+
+ def repository
+ project.design_repository
+ end
+
+ def designs_by_filename(filenames)
+ designs.current.where(filename: filenames)
+ end
+ end
+end
diff --git a/app/models/design_management/repository.rb b/app/models/design_management/repository.rb
new file mode 100644
index 00000000000..985d6317d5d
--- /dev/null
+++ b/app/models/design_management/repository.rb
@@ -0,0 +1,51 @@
+# frozen_string_literal: true
+
+module DesignManagement
+ class Repository < ::Repository
+ extend ::Gitlab::Utils::Override
+
+ # We define static git attributes for the design repository as this
+ # repository is entirely GitLab-managed rather than user-facing.
+ #
+ # Enable all uploaded files to be stored in LFS.
+ MANAGED_GIT_ATTRIBUTES = <<~GA.freeze
+ /#{DesignManagement.designs_directory}/* filter=lfs diff=lfs merge=lfs -text
+ GA
+
+ def initialize(project)
+ full_path = project.full_path + Gitlab::GlRepository::DESIGN.path_suffix
+ disk_path = project.disk_path + Gitlab::GlRepository::DESIGN.path_suffix
+
+ super(full_path, project, shard: project.repository_storage, disk_path: disk_path, repo_type: Gitlab::GlRepository::DESIGN)
+ end
+
+ # Override of a method called on Repository instances but sent via
+ # method_missing to Gitlab::Git::Repository where it is defined
+ def info_attributes
+ @info_attributes ||= Gitlab::Git::AttributesParser.new(MANAGED_GIT_ATTRIBUTES)
+ end
+
+ # Override of a method called on Repository instances but sent via
+ # method_missing to Gitlab::Git::Repository where it is defined
+ def attributes(path)
+ info_attributes.attributes(path)
+ end
+
+ # Override of a method called on Repository instances but sent via
+ # method_missing to Gitlab::Git::Repository where it is defined
+ def gitattribute(path, name)
+ attributes(path)[name]
+ end
+
+ # Override of a method called on Repository instances but sent via
+ # method_missing to Gitlab::Git::Repository where it is defined
+ def attributes_at(_ref = nil)
+ info_attributes
+ end
+
+ override :copy_gitattributes
+ def copy_gitattributes(_ref = nil)
+ true
+ end
+ end
+end
diff --git a/app/models/design_management/version.rb b/app/models/design_management/version.rb
new file mode 100644
index 00000000000..6be98fe3d44
--- /dev/null
+++ b/app/models/design_management/version.rb
@@ -0,0 +1,144 @@
+# frozen_string_literal: true
+
+module DesignManagement
+ class Version < ApplicationRecord
+ include Importable
+ include ShaAttribute
+ include AfterCommitQueue
+ include Gitlab::Utils::StrongMemoize
+ extend Gitlab::ExclusiveLeaseHelpers
+
+ NotSameIssue = Class.new(StandardError)
+
+ class CouldNotCreateVersion < StandardError
+ attr_reader :sha, :issue_id, :actions
+
+ def initialize(sha, issue_id, actions)
+ @sha, @issue_id, @actions = sha, issue_id, actions
+ end
+
+ def message
+ "could not create version from commit: #{sha}"
+ end
+
+ def sentry_extra_data
+ {
+ sha: sha,
+ issue_id: issue_id,
+ design_ids: actions.map { |a| a.design.id }
+ }
+ end
+ end
+
+ belongs_to :issue
+ belongs_to :author, class_name: 'User'
+ has_many :actions
+ has_many :designs,
+ through: :actions,
+ class_name: "DesignManagement::Design",
+ source: :design,
+ inverse_of: :versions
+
+ validates :designs, presence: true, unless: :importing?
+ validates :sha, presence: true
+ validates :sha, uniqueness: { case_sensitive: false, scope: :issue_id }
+ validates :author, presence: true
+ # We are not validating the issue object as it incurs an extra query to fetch
+ # the record from the DB. Instead, we rely on the foreign key constraint to
+ # ensure referential integrity.
+ validates :issue_id, presence: true, unless: :importing?
+
+ sha_attribute :sha
+
+ delegate :project, to: :issue
+
+ scope :for_designs, -> (designs) do
+ where(id: ::DesignManagement::Action.where(design_id: designs).select(:version_id)).distinct
+ end
+ scope :earlier_or_equal_to, -> (version) { where("(#{table_name}.id) <= ?", version) } # rubocop:disable GitlabSecurity/SqlInjection
+ scope :ordered, -> { order(id: :desc) }
+ scope :for_issue, -> (issue) { where(issue: issue) }
+ scope :by_sha, -> (sha) { where(sha: sha) }
+
+ # This is the one true way to create a Version.
+ #
+ # This method means you can avoid the paradox of versions being invalid without
+ # designs, and not being able to add designs without a saved version. Also this
+ # method inserts designs in bulk, rather than one by one.
+ #
+ # Before calling this method, callers must guard against concurrent
+ # modification by obtaining the lock on the design repository. See:
+ # `DesignManagement::Version.with_lock`.
+ #
+ # Parameters:
+ # - design_actions [DesignManagement::DesignAction]:
+ # the actions that have been performed in the repository.
+ # - sha [String]:
+ # the SHA of the commit that performed them
+ # - author [User]:
+ # the user who performed the commit
+ # returns [DesignManagement::Version]
+ def self.create_for_designs(design_actions, sha, author)
+ issue_id, not_uniq = design_actions.map(&:issue_id).compact.uniq
+ raise NotSameIssue, 'All designs must belong to the same issue!' if not_uniq
+
+ transaction do
+ version = new(sha: sha, issue_id: issue_id, author: author)
+ version.save(validate: false) # We need it to have an ID. Validate later when designs are present
+
+ rows = design_actions.map { |action| action.row_attrs(version) }
+
+ Gitlab::Database.bulk_insert(::DesignManagement::Action.table_name, rows)
+ version.designs.reset
+ version.validate!
+ design_actions.each(&:performed)
+
+ version
+ end
+ rescue
+ raise CouldNotCreateVersion.new(sha, issue_id, design_actions)
+ end
+
+ CREATION_TTL = 5.seconds
+ RETRY_DELAY = ->(num) { 0.2.seconds * num**2 }
+
+ def self.with_lock(project_id, repository, &block)
+ key = "with_lock:#{name}:{#{project_id}}"
+
+ in_lock(key, ttl: CREATION_TTL, retries: 5, sleep_sec: RETRY_DELAY) do |_retried|
+ repository.create_if_not_exists
+ yield
+ end
+ end
+
+ def designs_by_event
+ actions
+ .includes(:design)
+ .group_by(&:event)
+ .transform_values { |group| group.map(&:design) }
+ end
+
+ def author
+ super || (commit_author if persisted?)
+ end
+
+ def diff_refs
+ strong_memoize(:diff_refs) { commit&.diff_refs }
+ end
+
+ def reset
+ %i[diff_refs commit].each { |k| clear_memoization(k) }
+ super
+ end
+
+ private
+
+ def commit_author
+ commit&.author
+ end
+
+ def commit
+ strong_memoize(:commit) { issue.project.design_repository.commit(sha) }
+ end
+ end
+end
diff --git a/app/models/design_user_mention.rb b/app/models/design_user_mention.rb
new file mode 100644
index 00000000000..baf4db29a0f
--- /dev/null
+++ b/app/models/design_user_mention.rb
@@ -0,0 +1,6 @@
+# frozen_string_literal: true
+
+class DesignUserMention < UserMention
+ belongs_to :design, class_name: 'DesignManagement::Design'
+ belongs_to :note
+end
diff --git a/app/models/diff_note.rb b/app/models/diff_note.rb
index e3df61dadae..ff39dbb59f3 100644
--- a/app/models/diff_note.rb
+++ b/app/models/diff_note.rb
@@ -9,7 +9,7 @@ class DiffNote < Note
include Gitlab::Utils::StrongMemoize
def self.noteable_types
- %w(MergeRequest Commit)
+ %w(MergeRequest Commit DesignManagement::Design)
end
validates :original_position, presence: true
@@ -60,6 +60,8 @@ class DiffNote < Note
# Returns the diff file from `position`
def latest_diff_file
strong_memoize(:latest_diff_file) do
+ next if for_design?
+
position.diff_file(repository)
end
end
@@ -67,6 +69,8 @@ class DiffNote < Note
# Returns the diff file from `original_position`
def diff_file
strong_memoize(:diff_file) do
+ next if for_design?
+
enqueue_diff_file_creation_job if should_create_diff_file?
fetch_diff_file
@@ -145,7 +149,7 @@ class DiffNote < Note
end
def supported?
- for_commit? || self.noteable.has_complete_diff_refs?
+ for_commit? || for_design? || self.noteable.has_complete_diff_refs?
end
def set_line_code
@@ -184,5 +188,3 @@ class DiffNote < Note
noteable.respond_to?(:repository) ? noteable.repository : project.repository
end
end
-
-DiffNote.prepend_if_ee('::EE::DiffNote')
diff --git a/app/models/email.rb b/app/models/email.rb
index 580633d3232..c5154267ff0 100644
--- a/app/models/email.rb
+++ b/app/models/email.rb
@@ -6,7 +6,8 @@ class Email < ApplicationRecord
belongs_to :user, optional: false
- validates :email, presence: true, uniqueness: true, devise_email: true
+ validates :email, presence: true, uniqueness: true
+ validate :validate_email_format
validate :unique_email, if: ->(email) { email.email_changed? }
scope :confirmed, -> { where.not(confirmed_at: nil) }
@@ -14,9 +15,14 @@ class Email < ApplicationRecord
after_commit :update_invalid_gpg_signatures, if: -> { previous_changes.key?('confirmed_at') }
devise :confirmable
+
+ # This module adds async behaviour to Devise emails
+ # and should be added after Devise modules are initialized.
+ include AsyncDeviseEmail
+
self.reconfirmable = false # currently email can't be changed, no need to reconfirm
- delegate :username, to: :user
+ delegate :username, :can?, to: :user
def email=(value)
write_attribute(:email, value.downcase.strip)
@@ -30,6 +36,10 @@ class Email < ApplicationRecord
user.accept_pending_invitations!
end
+ def validate_email_format
+ self.errors.add(:email, I18n.t(:invalid, scope: 'valid_email.validations.email')) unless ValidateEmail.valid?(self.email)
+ end
+
# once email is confirmed, update the gpg signatures
def update_invalid_gpg_signatures
user.update_invalid_gpg_signatures if confirmed?
diff --git a/app/models/environment.rb b/app/models/environment.rb
index b2391f33aca..21044771bbb 100644
--- a/app/models/environment.rb
+++ b/app/models/environment.rb
@@ -8,6 +8,7 @@ class Environment < ApplicationRecord
self.reactive_cache_refresh_interval = 1.minute
self.reactive_cache_lifetime = 55.seconds
self.reactive_cache_hard_limit = 10.megabytes
+ self.reactive_cache_work_type = :external_dependency
belongs_to :project, required: true
@@ -151,6 +152,14 @@ class Environment < ApplicationRecord
.preload(:user, :metadata, :deployment)
end
+ def count_by_state
+ environments_count_by_state = group(:state).count
+
+ valid_states.each_with_object({}) do |state, count_hash|
+ count_hash[state] = environments_count_by_state[state.to_s] || 0
+ end
+ end
+
private
def cte_for_deployments_with_stop_action
diff --git a/app/models/epic.rb b/app/models/epic.rb
index 04e19c17e18..e09dc1080e6 100644
--- a/app/models/epic.rb
+++ b/app/models/epic.rb
@@ -1,7 +1,7 @@
# frozen_string_literal: true
# Placeholder class for model that is implemented in EE
-# It reserves '&' as a reference prefix, but the table does not exists in CE
+# It reserves '&' as a reference prefix, but the table does not exist in FOSS
class Epic < ApplicationRecord
include IgnorableColumns
diff --git a/app/models/error_tracking/project_error_tracking_setting.rb b/app/models/error_tracking/project_error_tracking_setting.rb
index 133850b6ab6..fa32c8a5450 100644
--- a/app/models/error_tracking/project_error_tracking_setting.rb
+++ b/app/models/error_tracking/project_error_tracking_setting.rb
@@ -22,6 +22,7 @@ module ErrorTracking
}x.freeze
self.reactive_cache_key = ->(setting) { [setting.class.model_name.singular, setting.project_id] }
+ self.reactive_cache_work_type = :external_dependency
belongs_to :project
diff --git a/app/models/event.rb b/app/models/event.rb
index 447ab753421..12b85697690 100644
--- a/app/models/event.rb
+++ b/app/models/event.rb
@@ -96,6 +96,8 @@ class Event < ApplicationRecord
end
scope :for_milestone_id, ->(milestone_id) { where(target_type: "Milestone", target_id: milestone_id) }
+ scope :for_wiki_meta, ->(meta) { where(target_type: 'WikiPage::Meta', target_id: meta.id) }
+ scope :created_at, ->(time) { where(created_at: time) }
# Authors are required as they're used to display who pushed data.
#
@@ -313,6 +315,10 @@ class Event < ApplicationRecord
note? && target && target.for_personal_snippet?
end
+ def design_note?
+ note? && note.for_design?
+ end
+
def note_target
target.noteable
end
@@ -380,6 +386,11 @@ class Event < ApplicationRecord
protected
+ # rubocop:disable Metrics/CyclomaticComplexity
+ # rubocop:disable Metrics/PerceivedComplexity
+ #
+ # TODO Refactor this method so we no longer need to disable the above cops
+ # https://gitlab.com/gitlab-org/gitlab/-/issues/216879.
def capability
@capability ||= begin
if push_action? || commit_note?
@@ -396,9 +407,13 @@ class Event < ApplicationRecord
:read_milestone
elsif wiki_page?
:read_wiki
+ elsif design_note?
+ :read_design
end
end
end
+ # rubocop:enable Metrics/CyclomaticComplexity
+ # rubocop:enable Metrics/PerceivedComplexity
private
diff --git a/app/models/global_milestone.rb b/app/models/global_milestone.rb
index d0cec0e9fc6..43de7454cb7 100644
--- a/app/models/global_milestone.rb
+++ b/app/models/global_milestone.rb
@@ -2,7 +2,6 @@
# Global Milestones are milestones that can be shared across multiple projects
class GlobalMilestone
include Milestoneish
- include_if_ee('::EE::GlobalMilestone') # rubocop: disable Cop/InjectEnterpriseEditionModule
STATE_COUNT_HASH = { opened: 0, closed: 0, all: 0 }.freeze
@@ -11,7 +10,7 @@ class GlobalMilestone
delegate :title, :state, :due_date, :start_date, :participants, :project,
:group, :expires_at, :closed?, :iid, :group_milestone?, :safe_title,
- :milestoneish_id, :resource_parent, :releases, to: :milestone
+ :timebox_id, :milestoneish_id, :resource_parent, :releases, to: :milestone
def to_hash
{
@@ -105,3 +104,5 @@ class GlobalMilestone
true
end
end
+
+GlobalMilestone.include_if_ee('::EE::GlobalMilestone')
diff --git a/app/models/group.rb b/app/models/group.rb
index 55a2c4ba9a9..04cb6b8b4da 100644
--- a/app/models/group.rb
+++ b/app/models/group.rb
@@ -30,6 +30,7 @@ class Group < Namespace
has_many :members_and_requesters, as: :source, class_name: 'GroupMember'
has_many :milestones
+ has_many :iterations
has_many :shared_group_links, foreign_key: :shared_with_group_id, class_name: 'GroupGroupLink'
has_many :shared_with_group_links, foreign_key: :shared_group_id, class_name: 'GroupGroupLink'
has_many :shared_groups, through: :shared_group_links, source: :shared_group
@@ -59,6 +60,8 @@ class Group < Namespace
has_many :import_failures, inverse_of: :group
+ has_one :import_state, class_name: 'GroupImportState', inverse_of: :group
+
has_many :group_deploy_tokens
has_many :deploy_tokens, through: :group_deploy_tokens
@@ -168,7 +171,7 @@ class Group < Namespace
notification_settings.find { |n| n.notification_email.present? }&.notification_email
end
- def to_reference(_from = nil, full: nil)
+ def to_reference(_from = nil, target_project: nil, full: nil)
"#{self.class.reference_prefix}#{full_path}"
end
@@ -302,9 +305,10 @@ class Group < Namespace
# rubocop: enable CodeReuse/ServiceClass
# rubocop: disable CodeReuse/ServiceClass
- def refresh_members_authorized_projects(blocking: true)
- UserProjectAccessChangedService.new(user_ids_for_project_authorizations)
- .execute(blocking: blocking)
+ def refresh_members_authorized_projects(blocking: true, priority: UserProjectAccessChangedService::HIGH_PRIORITY)
+ UserProjectAccessChangedService
+ .new(user_ids_for_project_authorizations)
+ .execute(blocking: blocking, priority: priority)
end
# rubocop: enable CodeReuse/ServiceClass
@@ -332,6 +336,11 @@ class Group < Namespace
.where(source_id: source_ids)
end
+ def members_from_self_and_ancestors_with_effective_access_level
+ members_with_parents.select([:user_id, 'MAX(access_level) AS access_level'])
+ .group(:user_id)
+ end
+
def members_with_descendants
GroupMember
.active_without_invites_and_requests
@@ -475,14 +484,14 @@ class Group < Namespace
false
end
- def wiki_access_level
- # TODO: Remove this method once we implement group-level features.
- # https://gitlab.com/gitlab-org/gitlab/-/issues/208412
- if Feature.enabled?(:group_wiki, self)
- ProjectFeature::ENABLED
- else
- ProjectFeature::DISABLED
- end
+ def execute_hooks(data, hooks_scope)
+ # NOOP
+ # TODO: group hooks https://gitlab.com/gitlab-org/gitlab/-/issues/216904
+ end
+
+ def execute_services(data, hooks_scope)
+ # NOOP
+ # TODO: group hooks https://gitlab.com/gitlab-org/gitlab/-/issues/216904
end
private
@@ -516,8 +525,6 @@ class Group < Namespace
end
def max_member_access_for_user_from_shared_groups(user)
- return unless Feature.enabled?(:share_group_with_group, default_enabled: true)
-
group_group_link_table = GroupGroupLink.arel_table
group_member_table = GroupMember.arel_table
diff --git a/app/models/group_import_state.rb b/app/models/group_import_state.rb
new file mode 100644
index 00000000000..7773b887249
--- /dev/null
+++ b/app/models/group_import_state.rb
@@ -0,0 +1,34 @@
+# frozen_string_literal: true
+
+class GroupImportState < ApplicationRecord
+ self.primary_key = :group_id
+
+ belongs_to :group, inverse_of: :import_state
+
+ validates :group, :status, :jid, presence: true
+
+ state_machine :status, initial: :created do
+ state :created, value: 0
+ state :started, value: 1
+ state :finished, value: 2
+ state :failed, value: -1
+
+ event :start do
+ transition created: :started
+ end
+
+ event :finish do
+ transition started: :finished
+ end
+
+ event :fail_op do
+ transition any => :failed
+ end
+
+ after_transition any => :failed do |state, transition|
+ last_error = transition.args.first
+
+ state.update_column(:last_error, last_error) if last_error
+ end
+ end
+end
diff --git a/app/models/group_milestone.rb b/app/models/group_milestone.rb
index 87338512d99..60e97174e50 100644
--- a/app/models/group_milestone.rb
+++ b/app/models/group_milestone.rb
@@ -1,7 +1,6 @@
# frozen_string_literal: true
# Group Milestones are milestones that can be shared among many projects within the same group
class GroupMilestone < GlobalMilestone
- include_if_ee('::EE::GroupMilestone') # rubocop: disable Cop/InjectEnterpriseEditionModule
attr_reader :group, :milestones
def self.build_collection(group, projects, params)
@@ -46,3 +45,5 @@ class GroupMilestone < GlobalMilestone
true
end
end
+
+GroupMilestone.include_if_ee('::EE::GroupMilestone')
diff --git a/app/models/hooks/project_hook.rb b/app/models/hooks/project_hook.rb
index bc480b14e67..71494b6de4d 100644
--- a/app/models/hooks/project_hook.rb
+++ b/app/models/hooks/project_hook.rb
@@ -3,6 +3,9 @@
class ProjectHook < WebHook
include TriggerableHooks
include Presentable
+ include Limitable
+
+ self.limit_scope = :project
triggerable_hooks [
:push_hooks,
diff --git a/app/models/internal_id_enums.rb b/app/models/internal_id_enums.rb
index 2f7d7aeff2f..125ae7573b6 100644
--- a/app/models/internal_id_enums.rb
+++ b/app/models/internal_id_enums.rb
@@ -3,7 +3,18 @@
module InternalIdEnums
def self.usage_resources
# when adding new resource, make sure it doesn't conflict with EE usage_resources
- { issues: 0, merge_requests: 1, deployments: 2, milestones: 3, epics: 4, ci_pipelines: 5, operations_feature_flags: 6 }
+ {
+ issues: 0,
+ merge_requests: 1,
+ deployments: 2,
+ milestones: 3,
+ epics: 4,
+ ci_pipelines: 5,
+ operations_feature_flags: 6,
+ operations_user_lists: 7,
+ alert_management_alerts: 8,
+ sprints: 9 # iterations
+ }
end
end
diff --git a/app/models/issue.rb b/app/models/issue.rb
index cdd7429bc58..a04ac412940 100644
--- a/app/models/issue.rb
+++ b/app/models/issue.rb
@@ -17,6 +17,7 @@ class Issue < ApplicationRecord
include IgnorableColumns
include MilestoneEventable
include WhereComposite
+ include StateEventable
DueDateStruct = Struct.new(:title, :name).freeze
NoDueDate = DueDateStruct.new('No Due Date', '0').freeze
@@ -29,9 +30,12 @@ class Issue < ApplicationRecord
SORTING_PREFERENCE_FIELD = :issues_sort
belongs_to :project
- belongs_to :moved_to, class_name: 'Issue'
belongs_to :duplicated_to, class_name: 'Issue'
belongs_to :closed_by, class_name: 'User'
+ belongs_to :iteration, foreign_key: 'sprint_id'
+
+ belongs_to :moved_to, class_name: 'Issue'
+ has_one :moved_from, class_name: 'Issue', foreign_key: :moved_to_id
has_internal_id :iid, scope: :project, track_if: -> { !importing? }, init: ->(s) { s&.project&.issues&.maximum(:iid) }
@@ -46,8 +50,15 @@ class Issue < ApplicationRecord
has_many :zoom_meetings
has_many :user_mentions, class_name: "IssueUserMention", dependent: :delete_all # rubocop:disable Cop/ActiveRecordDependent
has_many :sent_notifications, as: :noteable
+ has_many :designs, class_name: 'DesignManagement::Design', inverse_of: :issue
+ has_many :design_versions, class_name: 'DesignManagement::Version', inverse_of: :issue do
+ def most_recent
+ ordered.first
+ end
+ end
has_one :sentry_issue
+ has_one :alert_management_alert, class_name: 'AlertManagement::Alert'
accepts_nested_attributes_for :sentry_issue
@@ -63,6 +74,7 @@ class Issue < ApplicationRecord
scope :due_before, ->(date) { where('issues.due_date < ?', date) }
scope :due_between, ->(from_date, to_date) { where('issues.due_date >= ?', from_date).where('issues.due_date <= ?', to_date) }
scope :due_tomorrow, -> { where(due_date: Date.tomorrow) }
+ scope :not_authored_by, ->(user) { where.not(author_id: user) }
scope :order_due_date_asc, -> { reorder(::Gitlab::Database.nulls_last_order('due_date', 'ASC')) }
scope :order_due_date_desc, -> { reorder(::Gitlab::Database.nulls_last_order('due_date', 'DESC')) }
@@ -73,11 +85,13 @@ class Issue < ApplicationRecord
scope :preload_associated_models, -> { preload(:assignees, :labels, project: :namespace) }
scope :with_api_entity_associations, -> { preload(:timelogs, :assignees, :author, :notes, :labels, project: [:route, { namespace: :route }] ) }
+ scope :with_label_attributes, ->(label_attributes) { joins(:labels).where(labels: label_attributes) }
scope :public_only, -> { where(confidential: false) }
scope :confidential_only, -> { where(confidential: true) }
scope :counts_by_state, -> { reorder(nil).group(:state_id).count }
+ scope :with_alert_management_alerts, -> { joins(:alert_management_alert) }
# An issue can be uniquely identified by project_id and iid
# Takes one or more sets of composite IDs, expressed as hash-like records of
@@ -330,6 +344,10 @@ class Issue < ApplicationRecord
previous_changes['updated_at']&.first || updated_at
end
+ def design_collection
+ @design_collection ||= ::DesignManagement::DesignCollection.new(self)
+ end
+
private
def ensure_metrics
@@ -343,7 +361,7 @@ class Issue < ApplicationRecord
# for performance reasons, check commit: 002ad215818450d2cbbc5fa065850a953dc7ada8
# Make sure to sync this method with issue_policy.rb
def readable_by?(user)
- if user.admin?
+ if user.can_read_all_resources?
true
elsif project.owner == user
true
diff --git a/app/models/iteration.rb b/app/models/iteration.rb
new file mode 100644
index 00000000000..1acd08f2063
--- /dev/null
+++ b/app/models/iteration.rb
@@ -0,0 +1,100 @@
+# frozen_string_literal: true
+
+class Iteration < ApplicationRecord
+ include Timebox
+
+ self.table_name = 'sprints'
+
+ attr_accessor :skip_future_date_validation
+
+ STATE_ENUM_MAP = {
+ upcoming: 1,
+ started: 2,
+ closed: 3
+ }.with_indifferent_access.freeze
+
+ include AtomicInternalId
+
+ has_many :issues, foreign_key: 'sprint_id'
+ has_many :merge_requests, foreign_key: 'sprint_id'
+
+ belongs_to :project
+ belongs_to :group
+
+ has_internal_id :iid, scope: :project, init: ->(s) { s&.project&.iterations&.maximum(:iid) }
+ has_internal_id :iid, scope: :group, init: ->(s) { s&.group&.iterations&.maximum(:iid) }
+
+ validates :start_date, presence: true
+ validates :due_date, presence: true
+
+ validate :dates_do_not_overlap, if: :start_or_due_dates_changed?
+ validate :future_date, if: :start_or_due_dates_changed?, unless: :skip_future_date_validation
+
+ scope :upcoming, -> { with_state(:upcoming) }
+ scope :started, -> { with_state(:started) }
+
+ state_machine :state_enum, initial: :upcoming do
+ event :start do
+ transition upcoming: :started
+ end
+
+ event :close do
+ transition [:upcoming, :started] => :closed
+ end
+
+ state :upcoming, value: Iteration::STATE_ENUM_MAP[:upcoming]
+ state :started, value: Iteration::STATE_ENUM_MAP[:started]
+ state :closed, value: Iteration::STATE_ENUM_MAP[:closed]
+ end
+
+ # Alias to state machine .with_state_enum method
+ # This needs to be defined after the state machine block to avoid errors
+ class << self
+ alias_method :with_state, :with_state_enum
+ alias_method :with_states, :with_state_enums
+
+ def filter_by_state(iterations, state)
+ case state
+ when 'closed' then iterations.closed
+ when 'started' then iterations.started
+ when 'opened' then iterations.started.or(iterations.upcoming)
+ when 'all' then iterations
+ else iterations.upcoming
+ end
+ end
+ end
+
+ def state
+ STATE_ENUM_MAP.key(state_enum)
+ end
+
+ def state=(value)
+ self.state_enum = STATE_ENUM_MAP[value]
+ end
+
+ private
+
+ def start_or_due_dates_changed?
+ start_date_changed? || due_date_changed?
+ end
+
+ # ensure dates do not overlap with other Iterations in the same group/project
+ def dates_do_not_overlap
+ return unless resource_parent.iterations.within_timeframe(start_date, due_date).exists?
+
+ errors.add(:base, s_("Iteration|Dates cannot overlap with other existing Iterations"))
+ end
+
+ # ensure dates are in the future
+ def future_date
+ if start_date_changed?
+ errors.add(:start_date, s_("Iteration|cannot be in the past")) if start_date < Date.current
+ errors.add(:start_date, s_("Iteration|cannot be more than 500 years in the future")) if start_date > 500.years.from_now
+ end
+
+ if due_date_changed?
+ errors.add(:due_date, s_("Iteration|cannot be in the past")) if due_date < Date.current
+ errors.add(:due_date, s_("Iteration|cannot be more than 500 years in the future")) if due_date > 500.years.from_now
+ end
+ end
+end
diff --git a/app/models/jira_import_state.rb b/app/models/jira_import_state.rb
index bde2795e7b8..92147794e88 100644
--- a/app/models/jira_import_state.rb
+++ b/app/models/jira_import_state.rb
@@ -3,6 +3,7 @@
class JiraImportState < ApplicationRecord
include AfterCommitQueue
include ImportState::SidekiqJobTracker
+ include UsageStatistics
self.table_name = 'jira_imports'
@@ -46,7 +47,7 @@ class JiraImportState < ApplicationRecord
after_transition initial: :scheduled do |state, _|
state.run_after_commit do
job_id = Gitlab::JiraImport::Stage::StartImportWorker.perform_async(project.id)
- state.update(jid: job_id) if job_id
+ state.update(jid: job_id, scheduled_at: Time.now) if job_id
end
end
@@ -97,4 +98,8 @@ class JiraImportState < ApplicationRecord
}
)
end
+
+ def self.finished_imports_count
+ finished.sum(:imported_issues_count)
+ end
end
diff --git a/app/models/list.rb b/app/models/list.rb
index 64247fdb983..ec211dfd497 100644
--- a/app/models/list.rb
+++ b/app/models/list.rb
@@ -3,8 +3,6 @@
class List < ApplicationRecord
include Importable
- prepend_if_ee('::EE::List') # rubocop: disable Cop/InjectEnterpriseEditionModule
-
belongs_to :board
belongs_to :label
has_many :list_user_preferences
@@ -74,14 +72,18 @@ class List < ApplicationRecord
label? ? label.name : list_type.humanize
end
+ def collapsed?(user)
+ preferences = preferences_for(user)
+
+ preferences.collapsed?
+ end
+
def as_json(options = {})
super(options).tap do |json|
json[:collapsed] = false
if options.key?(:collapsed)
- preferences = preferences_for(options[:current_user])
-
- json[:collapsed] = preferences.collapsed?
+ json[:collapsed] = collapsed?(options[:current_user])
end
if options.key?(:label)
@@ -100,3 +102,5 @@ class List < ApplicationRecord
throw(:abort) unless destroyable? # rubocop:disable Cop/BanCatchThrow
end
end
+
+List.prepend_if_ee('::EE::List')
diff --git a/app/models/member.rb b/app/models/member.rb
index 5b33333aa23..791073da095 100644
--- a/app/models/member.rb
+++ b/app/models/member.rb
@@ -1,6 +1,7 @@
# frozen_string_literal: true
class Member < ApplicationRecord
+ include EachBatch
include AfterCommitQueue
include Sortable
include Importable
diff --git a/app/models/members/project_member.rb b/app/models/members/project_member.rb
index 68c51860c47..fa2e0cb8198 100644
--- a/app/models/members/project_member.rb
+++ b/app/models/members/project_member.rb
@@ -17,6 +17,11 @@ class ProjectMember < Member
.where('projects.namespace_id in (?)', groups.select(:id))
end
+ scope :without_project_bots, -> do
+ left_join_users
+ .merge(User.without_project_bot)
+ end
+
class << self
# Add users to projects with passed access option
#
diff --git a/app/models/members_preloader.rb b/app/models/members_preloader.rb
index 1ed0434eacf..6da8d5f3161 100644
--- a/app/models/members_preloader.rb
+++ b/app/models/members_preloader.rb
@@ -1,8 +1,6 @@
# frozen_string_literal: true
class MembersPreloader
- prepend_if_ee('EE::MembersPreloader') # rubocop: disable Cop/InjectEnterpriseEditionModule
-
attr_reader :members
def initialize(members)
@@ -16,3 +14,5 @@ class MembersPreloader
ActiveRecord::Associations::Preloader.new.preload(members.map(&:user), :u2f_registrations)
end
end
+
+MembersPreloader.prepend_if_ee('EE::MembersPreloader')
diff --git a/app/models/merge_request.rb b/app/models/merge_request.rb
index a28e054e13c..b4d0b729454 100644
--- a/app/models/merge_request.rb
+++ b/app/models/merge_request.rb
@@ -19,6 +19,7 @@ class MergeRequest < ApplicationRecord
include ShaAttribute
include IgnorableColumns
include MilestoneEventable
+ include StateEventable
sha_attribute :squash_commit_sha
@@ -32,6 +33,7 @@ class MergeRequest < ApplicationRecord
belongs_to :target_project, class_name: "Project"
belongs_to :source_project, class_name: "Project"
belongs_to :merge_user, class_name: "User"
+ belongs_to :iteration, foreign_key: 'sprint_id'
has_internal_id :iid, scope: :target_project, track_if: -> { !importing? }, init: ->(s) { s&.target_project&.merge_requests&.maximum(:iid) }
@@ -864,7 +866,7 @@ class MergeRequest < ApplicationRecord
check_service = MergeRequests::MergeabilityCheckService.new(self)
- if async && Feature.enabled?(:async_merge_request_check_mergeability, project)
+ if async && Feature.enabled?(:async_merge_request_check_mergeability, project, default_enabled: true)
check_service.async_execute
else
check_service.execute(retry_lease: false)
@@ -873,7 +875,7 @@ class MergeRequest < ApplicationRecord
# rubocop: enable CodeReuse/ServiceClass
def diffable_merge_ref?
- Feature.enabled?(:diff_compare_with_head, target_project) && can_be_merged? && merge_ref_head.present?
+ can_be_merged? && merge_ref_head.present?
end
# Returns boolean indicating the merge_status should be rechecked in order to
@@ -1129,26 +1131,6 @@ class MergeRequest < ApplicationRecord
end
end
- # Return array of possible target branches
- # depends on target project of MR
- def target_branches
- if target_project.nil?
- []
- else
- target_project.repository.branch_names
- end
- end
-
- # Return array of possible source branches
- # depends on source project of MR
- def source_branches
- if source_project.nil?
- []
- else
- source_project.repository.branch_names
- end
- end
-
def has_ci?
return false if has_no_commits?
@@ -1319,12 +1301,30 @@ class MergeRequest < ApplicationRecord
compare_reports(Ci::CompareTestReportsService)
end
+ def has_accessibility_reports?
+ return false unless Feature.enabled?(:accessibility_report_view, project)
+
+ actual_head_pipeline.present? && actual_head_pipeline.has_reports?(Ci::JobArtifact.accessibility_reports)
+ end
+
def has_coverage_reports?
return false unless Feature.enabled?(:coverage_report_view, project)
actual_head_pipeline&.has_reports?(Ci::JobArtifact.coverage_reports)
end
+ def has_terraform_reports?
+ actual_head_pipeline&.has_reports?(Ci::JobArtifact.terraform_reports)
+ end
+
+ def compare_accessibility_reports
+ unless has_accessibility_reports?
+ return { status: :error, status_reason: _('This merge request does not have accessibility reports') }
+ end
+
+ compare_reports(Ci::CompareAccessibilityReportsService)
+ end
+
# TODO: this method and compare_test_reports use the same
# result type, which is handled by the controller's #reports_response.
# we should minimize mistakes by isolating the common parts.
@@ -1337,9 +1337,15 @@ class MergeRequest < ApplicationRecord
compare_reports(Ci::GenerateCoverageReportsService)
end
- def has_exposed_artifacts?
- return false unless Feature.enabled?(:ci_expose_arbitrary_artifacts_in_mr, default_enabled: true)
+ def find_terraform_reports
+ unless has_terraform_reports?
+ return { status: :error, status_reason: 'This merge request does not have terraform reports' }
+ end
+ compare_reports(Ci::GenerateTerraformReportsService)
+ end
+
+ def has_exposed_artifacts?
actual_head_pipeline&.has_exposed_artifacts?
end
diff --git a/app/models/merge_request_diff.rb b/app/models/merge_request_diff.rb
index 7b15d21c095..f793bd3d76f 100644
--- a/app/models/merge_request_diff.rb
+++ b/app/models/merge_request_diff.rb
@@ -141,7 +141,7 @@ class MergeRequestDiff < ApplicationRecord
after_create :save_git_content, unless: :importing?
after_create_commit :set_as_latest_diff, unless: :importing?
- after_save :update_external_diff_store, if: -> { !importing? && saved_change_to_external_diff? }
+ after_save :update_external_diff_store
def self.find_by_diff_refs(diff_refs)
find_by(start_commit_sha: diff_refs.start_sha, head_commit_sha: diff_refs.head_sha, base_commit_sha: diff_refs.base_sha)
@@ -385,34 +385,11 @@ class MergeRequestDiff < ApplicationRecord
end
end
- # Carrierwave defines `write_uploader` dynamically on this class, so `super`
- # does not work. Alias the carrierwave method so we can call it when needed
- alias_method :carrierwave_write_uploader, :write_uploader
-
- # The `external_diff`, `external_diff_store`, and `stored_externally`
- # columns were introduced in GitLab 11.8, but some background migration specs
- # use factories that rely on current code with an old schema. Without these
- # `has_attribute?` guards, they fail with a `MissingAttributeError`.
- #
- # For more details, see: https://gitlab.com/gitlab-org/gitlab-foss/issues/44990
-
- def write_uploader(column, identifier)
- carrierwave_write_uploader(column, identifier) if has_attribute?(column)
- end
-
def update_external_diff_store
- update_column(:external_diff_store, external_diff.object_store) if
- has_attribute?(:external_diff_store)
- end
-
- def saved_change_to_external_diff?
- super if has_attribute?(:external_diff)
- end
+ return unless saved_change_to_external_diff? || saved_change_to_stored_externally?
- def stored_externally
- super if has_attribute?(:stored_externally)
+ update_column(:external_diff_store, external_diff.object_store)
end
- alias_method :stored_externally?, :stored_externally
# If enabled, yields the external file containing the diff. Otherwise, yields
# nil. This method is not thread-safe, but it *is* re-entrant, which allows
@@ -575,7 +552,6 @@ class MergeRequestDiff < ApplicationRecord
end
def use_external_diff?
- return false unless has_attribute?(:external_diff)
return false unless Gitlab.config.external_diffs.enabled
case Gitlab.config.external_diffs.when
diff --git a/app/models/metrics/users_starred_dashboard.rb b/app/models/metrics/users_starred_dashboard.rb
new file mode 100644
index 00000000000..07748eb1431
--- /dev/null
+++ b/app/models/metrics/users_starred_dashboard.rb
@@ -0,0 +1,18 @@
+# frozen_string_literal: true
+
+module Metrics
+ class UsersStarredDashboard < ApplicationRecord
+ self.table_name = 'metrics_users_starred_dashboards'
+
+ belongs_to :user, inverse_of: :metrics_users_starred_dashboards
+ belongs_to :project, inverse_of: :metrics_users_starred_dashboards
+
+ validates :user_id, presence: true
+ validates :project_id, presence: true
+ validates :dashboard_path, presence: true, length: { maximum: 255 }
+ validates :dashboard_path, uniqueness: { scope: %i[user_id project_id] }
+
+ scope :for_project, ->(project) { where(project: project) }
+ scope :for_project_dashboard, ->(project, path) { for_project(project).where(dashboard_path: path) }
+ end
+end
diff --git a/app/models/milestone.rb b/app/models/milestone.rb
index 4ccfe314526..b5e4f62792e 100644
--- a/app/models/milestone.rb
+++ b/app/models/milestone.rb
@@ -1,88 +1,37 @@
# frozen_string_literal: true
class Milestone < ApplicationRecord
- # Represents a "No Milestone" state used for filtering Issues and Merge
- # Requests that have no milestone assigned.
- MilestoneStruct = Struct.new(:title, :name, :id) do
- # Ensure these models match the interface required for exporting
- def serializable_hash(_opts = {})
- { title: title, name: name, id: id }
- end
- end
-
- None = MilestoneStruct.new('No Milestone', 'No Milestone', 0)
- Any = MilestoneStruct.new('Any Milestone', '', -1)
- Upcoming = MilestoneStruct.new('Upcoming', '#upcoming', -2)
- Started = MilestoneStruct.new('Started', '#started', -3)
-
- include CacheMarkdownField
- include AtomicInternalId
- include IidRoutes
include Sortable
include Referable
- include StripAttribute
+ include Timebox
include Milestoneish
include FromUnion
include Importable
- include Gitlab::SQL::Pattern
prepend_if_ee('::EE::Milestone') # rubocop: disable Cop/InjectEnterpriseEditionModule
- cache_markdown_field :title, pipeline: :single_line
- cache_markdown_field :description
-
- belongs_to :project
- belongs_to :group
-
has_many :milestone_releases
has_many :releases, through: :milestone_releases
has_internal_id :iid, scope: :project, track_if: -> { !importing? }, init: ->(s) { s&.project&.milestones&.maximum(:iid) }
has_internal_id :iid, scope: :group, track_if: -> { !importing? }, init: ->(s) { s&.group&.milestones&.maximum(:iid) }
- has_many :issues
- has_many :labels, -> { distinct.reorder('labels.title') }, through: :issues
- has_many :merge_requests
has_many :events, as: :target, dependent: :delete_all # rubocop:disable Cop/ActiveRecordDependent
- scope :of_projects, ->(ids) { where(project_id: ids) }
- scope :of_groups, ->(ids) { where(group_id: ids) }
scope :active, -> { with_state(:active) }
- scope :closed, -> { with_state(:closed) }
- scope :for_projects, -> { where(group: nil).includes(:project) }
scope :started, -> { active.where('milestones.start_date <= CURRENT_DATE') }
-
- scope :for_projects_and_groups, -> (projects, groups) do
- projects = projects.compact if projects.is_a? Array
- projects = [] if projects.nil?
-
- groups = groups.compact if groups.is_a? Array
- groups = [] if groups.nil?
-
- where(project_id: projects).or(where(group_id: groups))
- end
-
- scope :within_timeframe, -> (start_date, end_date) do
- where('start_date is not NULL or due_date is not NULL')
- .where('start_date is NULL or start_date <= ?', end_date)
- .where('due_date is NULL or due_date >= ?', start_date)
+ scope :not_started, -> { active.where('milestones.start_date > CURRENT_DATE') }
+ scope :not_upcoming, -> do
+ active
+ .where('milestones.due_date <= CURRENT_DATE')
+ .order(:project_id, :group_id, :due_date)
end
scope :order_by_name_asc, -> { order(Arel::Nodes::Ascending.new(arel_table[:title].lower)) }
scope :reorder_by_due_date_asc, -> { reorder(Gitlab::Database.nulls_last_order('due_date', 'ASC')) }
- validates :group, presence: true, unless: :project
- validates :project, presence: true, unless: :group
- validates :title, presence: true
-
- validate :uniqueness_of_title, if: :title_changed?
- validate :milestone_type_check
- validate :start_date_should_be_less_than_due_date, if: proc { |m| m.start_date.present? && m.due_date.present? }
- validate :dates_within_4_digits
validates_associated :milestone_releases, message: -> (_, obj) { obj[:value].map(&:errors).map(&:full_messages).join(",") }
- strip_attributes :title
-
state_machine :state, initial: :active do
event :close do
transition active: :closed
@@ -97,52 +46,6 @@ class Milestone < ApplicationRecord
state :active
end
- alias_attribute :name, :title
-
- class << self
- # Searches for milestones with a matching title or description.
- #
- # This method uses ILIKE on PostgreSQL and LIKE on MySQL.
- #
- # query - The search query as a String
- #
- # Returns an ActiveRecord::Relation.
- def search(query)
- fuzzy_search(query, [:title, :description])
- end
-
- # Searches for milestones with a matching title.
- #
- # This method uses ILIKE on PostgreSQL and LIKE on MySQL.
- #
- # query - The search query as a String
- #
- # Returns an ActiveRecord::Relation.
- def search_title(query)
- fuzzy_search(query, [:title])
- end
-
- def filter_by_state(milestones, state)
- case state
- when 'closed' then milestones.closed
- when 'all' then milestones
- else milestones.active
- end
- end
-
- def count_by_state
- reorder(nil).group(:state).count
- end
-
- def predefined_id?(id)
- [Any.id, None.id, Upcoming.id, Started.id].include?(id)
- end
-
- def predefined?(milestone)
- predefined_id?(milestone&.id)
- end
- end
-
def self.reference_prefix
'%'
end
@@ -220,7 +123,7 @@ class Milestone < ApplicationRecord
end
##
- # Returns the String necessary to reference this Milestone in Markdown. Group
+ # Returns the String necessary to reference a Milestone in Markdown. Group
# milestones only support name references, and do not support cross-project
# references.
#
@@ -248,10 +151,6 @@ class Milestone < ApplicationRecord
self.class.reference_prefix + self.title
end
- def milestoneish_id
- id
- end
-
def for_display
self
end
@@ -264,62 +163,24 @@ class Milestone < ApplicationRecord
nil
end
- def title=(value)
- write_attribute(:title, sanitize_title(value)) if value.present?
- end
+ # TODO: remove after all code paths use `timebox_id`
+ # https://gitlab.com/gitlab-org/gitlab/-/issues/215688
+ alias_method :milestoneish_id, :timebox_id
+ # TODO: remove after all code paths use (group|project)_timebox?
+ # https://gitlab.com/gitlab-org/gitlab/-/issues/215690
+ alias_method :group_milestone?, :group_timebox?
+ alias_method :project_milestone?, :project_timebox?
- def safe_title
- title.to_slug.normalize.to_s
- end
-
- def resource_parent
- group || project
- end
-
- def to_ability_name
- model_name.singular
- end
-
- def group_milestone?
- group_id.present?
- end
-
- def project_milestone?
- project_id.present?
- end
-
- def merge_requests_enabled?
+ def parent
if group_milestone?
- # Assume that groups have at least one project with merge requests enabled.
- # Otherwise, we would need to load all of the projects from the database.
- true
- elsif project_milestone?
- project&.merge_requests_enabled?
+ group
+ else
+ project
end
end
private
- # Milestone titles must be unique across project milestones and group milestones
- def uniqueness_of_title
- if project
- relation = Milestone.for_projects_and_groups([project_id], [project.group&.id])
- elsif group
- relation = Milestone.for_projects_and_groups(group.projects.select(:id), [group.id])
- end
-
- title_exists = relation.find_by_title(title)
- errors.add(:title, _("already being used for another group or project milestone.")) if title_exists
- end
-
- # Milestone should be either a project milestone or a group milestone
- def milestone_type_check
- if group_id && project_id
- field = project_id_changed? ? :project_id : :group_id
- errors.add(field, _("milestone should belong either to a project or a group."))
- end
- end
-
def milestone_format_reference(format = :iid)
raise ArgumentError, _('Unknown format') unless [:iid, :name].include?(format)
@@ -334,26 +195,6 @@ class Milestone < ApplicationRecord
end
end
- def sanitize_title(value)
- CGI.unescape_html(Sanitize.clean(value.to_s))
- end
-
- def start_date_should_be_less_than_due_date
- if due_date <= start_date
- errors.add(:due_date, _("must be greater than start date"))
- end
- end
-
- def dates_within_4_digits
- if start_date && start_date > Date.new(9999, 12, 31)
- errors.add(:start_date, _("date must not be after 9999-12-31"))
- end
-
- if due_date && due_date > Date.new(9999, 12, 31)
- errors.add(:due_date, _("date must not be after 9999-12-31"))
- end
- end
-
def issues_finder_params
{ project_id: project_id, group_id: group_id, include_subgroups: group_id.present? }.compact
end
diff --git a/app/models/milestone_note.rb b/app/models/milestone_note.rb
index 2ff9791feb0..19171e682b7 100644
--- a/app/models/milestone_note.rb
+++ b/app/models/milestone_note.rb
@@ -17,6 +17,6 @@ class MilestoneNote < SyntheticNote
def note_text(html: false)
format = milestone&.group_milestone? ? :name : :iid
- milestone.nil? ? 'removed milestone' : "changed milestone to #{milestone.to_reference(project, format: format)}"
+ event.remove? ? 'removed milestone' : "changed milestone to #{milestone.to_reference(project, format: format)}"
end
end
diff --git a/app/models/namespace.rb b/app/models/namespace.rb
index 9e7589a1f18..8116f7a256f 100644
--- a/app/models/namespace.rb
+++ b/app/models/namespace.rb
@@ -14,6 +14,7 @@ class Namespace < ApplicationRecord
include IgnorableColumns
ignore_column :plan_id, remove_with: '13.1', remove_after: '2020-06-22'
+ ignore_column :trial_ends_on, remove_with: '13.2', remove_after: '2020-07-22'
# Prevent users from creating unreasonably deep level of nesting.
# The number 20 was taken based on maximum nesting level of
@@ -135,11 +136,6 @@ class Namespace < ApplicationRecord
name = host.delete_suffix(gitlab_host)
Namespace.where(parent_id: nil).by_path(name)
end
-
- # overridden in ee
- def reset_ci_minutes!(namespace_id)
- false
- end
end
def default_branch_protection
@@ -180,6 +176,10 @@ class Namespace < ApplicationRecord
kind == 'user'
end
+ def group?
+ type == 'Group'
+ end
+
def find_fork_of(project)
return unless project.fork_network
@@ -346,6 +346,21 @@ class Namespace < ApplicationRecord
.try(name)
end
+ def actual_plan
+ Plan.default
+ end
+
+ def actual_limits
+ # We default to PlanLimits.new otherwise a lot of specs would fail
+ # On production each plan should already have associated limits record
+ # https://gitlab.com/gitlab-org/gitlab/issues/36037
+ actual_plan.actual_limits
+ end
+
+ def actual_plan_name
+ actual_plan.name
+ end
+
private
def all_projects_with_pages
diff --git a/app/models/namespace/root_storage_size.rb b/app/models/namespace/root_storage_size.rb
new file mode 100644
index 00000000000..d61917e468e
--- /dev/null
+++ b/app/models/namespace/root_storage_size.rb
@@ -0,0 +1,31 @@
+# frozen_string_literal: true
+
+class Namespace::RootStorageSize
+ def initialize(root_namespace)
+ @root_namespace = root_namespace
+ end
+
+ def above_size_limit?
+ return false if limit == 0
+
+ usage_ratio > 1
+ end
+
+ def usage_ratio
+ return 0 if limit == 0
+
+ current_size.to_f / limit.to_f
+ end
+
+ def current_size
+ @current_size ||= root_namespace.root_storage_statistics&.storage_size
+ end
+
+ def limit
+ @limit ||= Gitlab::CurrentSettings.namespace_storage_size_limit.megabytes
+ end
+
+ private
+
+ attr_reader :root_namespace
+end
diff --git a/app/models/note.rb b/app/models/note.rb
index a2a711c987f..d174ba8fe83 100644
--- a/app/models/note.rb
+++ b/app/models/note.rb
@@ -159,6 +159,8 @@ class Note < ApplicationRecord
after_save :touch_noteable, unless: :importing?
after_destroy :expire_etag_cache
after_save :store_mentions!, if: :any_mentionable_attributes_changed?
+ after_commit :notify_after_create, on: :create
+ after_commit :notify_after_destroy, on: :destroy
class << self
def model_name
@@ -279,6 +281,10 @@ class Note < ApplicationRecord
!for_personal_snippet?
end
+ def for_design?
+ noteable_type == DesignManagement::Design.name
+ end
+
def for_issuable?
for_issue? || for_merge_request?
end
@@ -505,6 +511,14 @@ class Note < ApplicationRecord
noteable_object
end
+ def notify_after_create
+ noteable&.after_note_created(self)
+ end
+
+ def notify_after_destroy
+ noteable&.after_note_destroyed(self)
+ end
+
def banzai_render_context(field)
super.merge(noteable: noteable, system_note: system?)
end
diff --git a/app/models/pages_domain.rb b/app/models/pages_domain.rb
index 486da2c6b45..da5e4012f05 100644
--- a/app/models/pages_domain.rb
+++ b/app/models/pages_domain.rb
@@ -2,6 +2,7 @@
class PagesDomain < ApplicationRecord
include Presentable
+ include FromUnion
VERIFICATION_KEY = 'gitlab-pages-verification-code'
VERIFICATION_THRESHOLD = 3.days.freeze
@@ -58,12 +59,14 @@ class PagesDomain < ApplicationRecord
end
scope :need_auto_ssl_renewal, -> do
- expiring = where(certificate_valid_not_after: nil).or(
- where(arel_table[:certificate_valid_not_after].lt(SSL_RENEWAL_THRESHOLD.from_now)))
+ enabled_and_not_failed = where(auto_ssl_enabled: true, auto_ssl_failed: false)
- user_provided_or_expiring = certificate_user_provided.or(expiring)
+ user_provided = enabled_and_not_failed.certificate_user_provided
+ certificate_not_valid = enabled_and_not_failed.where(certificate_valid_not_after: nil)
+ certificate_expiring = enabled_and_not_failed
+ .where(arel_table[:certificate_valid_not_after].lt(SSL_RENEWAL_THRESHOLD.from_now))
- where(auto_ssl_enabled: true).merge(user_provided_or_expiring)
+ from_union([user_provided, certificate_not_valid, certificate_expiring])
end
scope :for_removal, -> { where("remove_at < ?", Time.now) }
diff --git a/app/models/performance_monitoring/prometheus_dashboard.rb b/app/models/performance_monitoring/prometheus_dashboard.rb
index 30fb1935a27..57222c61b36 100644
--- a/app/models/performance_monitoring/prometheus_dashboard.rb
+++ b/app/models/performance_monitoring/prometheus_dashboard.rb
@@ -4,7 +4,7 @@ module PerformanceMonitoring
class PrometheusDashboard
include ActiveModel::Model
- attr_accessor :dashboard, :panel_groups, :path, :environment, :priority
+ attr_accessor :dashboard, :panel_groups, :path, :environment, :priority, :templating
validates :dashboard, presence: true
validates :panel_groups, presence: true
diff --git a/app/models/personal_access_token.rb b/app/models/personal_access_token.rb
index af079f7ebc4..7afee2a35cb 100644
--- a/app/models/personal_access_token.rb
+++ b/app/models/personal_access_token.rb
@@ -4,6 +4,7 @@ class PersonalAccessToken < ApplicationRecord
include Expirable
include TokenAuthenticatable
include Sortable
+ extend ::Gitlab::Utils::Override
add_authentication_token_field :token, digest: true
@@ -23,6 +24,8 @@ class PersonalAccessToken < ApplicationRecord
scope :without_impersonation, -> { where(impersonation: false) }
scope :for_user, -> (user) { where(user: user) }
scope :preload_users, -> { preload(:user) }
+ scope :order_expires_at_asc, -> { reorder(expires_at: :asc) }
+ scope :order_expires_at_desc, -> { reorder(expires_at: :desc) }
validates :scopes, presence: true
validate :validate_scopes
@@ -39,12 +42,14 @@ class PersonalAccessToken < ApplicationRecord
def self.redis_getdel(user_id)
Gitlab::Redis::SharedState.with do |redis|
- encrypted_token = redis.get(redis_shared_state_key(user_id))
- redis.del(redis_shared_state_key(user_id))
+ redis_key = redis_shared_state_key(user_id)
+ encrypted_token = redis.get(redis_key)
+ redis.del(redis_key)
+
begin
Gitlab::CryptoHelper.aes256_gcm_decrypt(encrypted_token)
rescue => ex
- logger.warn "Failed to decrypt PersonalAccessToken value stored in Redis for User ##{user_id}: #{ex.class}"
+ logger.warn "Failed to decrypt #{self.name} value stored in Redis for key ##{redis_key}: #{ex.class}"
encrypted_token
end
end
@@ -58,6 +63,16 @@ class PersonalAccessToken < ApplicationRecord
end
end
+ override :simple_sorts
+ def self.simple_sorts
+ super.merge(
+ {
+ 'expires_at_asc' => -> { order_expires_at_asc },
+ 'expires_at_desc' => -> { order_expires_at_desc }
+ }
+ )
+ end
+
protected
def validate_scopes
diff --git a/app/models/personal_snippet.rb b/app/models/personal_snippet.rb
index 1b5be8698b1..197795dccfe 100644
--- a/app/models/personal_snippet.rb
+++ b/app/models/personal_snippet.rb
@@ -2,4 +2,8 @@
class PersonalSnippet < Snippet
include WithUploads
+
+ def skip_project_check?
+ true
+ end
end
diff --git a/app/models/plan.rb b/app/models/plan.rb
new file mode 100644
index 00000000000..acac5f9aeae
--- /dev/null
+++ b/app/models/plan.rb
@@ -0,0 +1,42 @@
+# frozen_string_literal: true
+
+class Plan < ApplicationRecord
+ DEFAULT = 'default'.freeze
+
+ has_one :limits, class_name: 'PlanLimits'
+
+ ALL_PLANS = [DEFAULT].freeze
+ DEFAULT_PLANS = [DEFAULT].freeze
+ private_constant :ALL_PLANS, :DEFAULT_PLANS
+
+ # This always returns an object
+ def self.default
+ Gitlab::SafeRequestStore.fetch(:plan_default) do
+ # find_by allows us to find object (cheaply) against replica DB
+ # safe_find_or_create_by does stick to primary DB
+ find_by(name: DEFAULT) || safe_find_or_create_by(name: DEFAULT)
+ end
+ end
+
+ def self.all_plans
+ ALL_PLANS
+ end
+
+ def self.default_plans
+ DEFAULT_PLANS
+ end
+
+ def actual_limits
+ self.limits || PlanLimits.new
+ end
+
+ def default?
+ self.class.default_plans.include?(name)
+ end
+
+ def paid?
+ false
+ end
+end
+
+Plan.prepend_if_ee('EE::Plan')
diff --git a/app/models/plan_limits.rb b/app/models/plan_limits.rb
new file mode 100644
index 00000000000..575105cfd79
--- /dev/null
+++ b/app/models/plan_limits.rb
@@ -0,0 +1,23 @@
+# frozen_string_literal: true
+
+class PlanLimits < ApplicationRecord
+ belongs_to :plan
+
+ def exceeded?(limit_name, object)
+ return false unless enabled?(limit_name)
+
+ if object.is_a?(Integer)
+ object >= read_attribute(limit_name)
+ else
+ # object.count >= limit value is slower than checking
+ # if a record exists at the limit value - 1 position.
+ object.offset(read_attribute(limit_name) - 1).exists?
+ end
+ end
+
+ private
+
+ def enabled?(limit_name)
+ read_attribute(limit_name) > 0
+ end
+end
diff --git a/app/models/project.rb b/app/models/project.rb
index 5db349463d8..c0dd2eb8584 100644
--- a/app/models/project.rb
+++ b/app/models/project.rb
@@ -3,6 +3,7 @@
require 'carrierwave/orm/activerecord'
class Project < ApplicationRecord
+ extend ::Gitlab::Utils::Override
include Gitlab::ConfigHelper
include Gitlab::VisibilityLevel
include AccessRequestable
@@ -18,6 +19,7 @@ class Project < ApplicationRecord
include SelectForProjectAuthorization
include Presentable
include HasRepository
+ include HasWiki
include Routable
include GroupDescendant
include Gitlab::SQL::Pattern
@@ -175,6 +177,7 @@ class Project < ApplicationRecord
has_one :packagist_service
has_one :hangouts_chat_service
has_one :unify_circuit_service
+ has_one :webex_teams_service
has_one :root_of_fork_network,
foreign_key: 'root_project_id',
@@ -206,12 +209,14 @@ class Project < ApplicationRecord
has_many :services
has_many :events
has_many :milestones
+ has_many :iterations
has_many :notes
has_many :snippets, class_name: 'ProjectSnippet'
has_many :hooks, class_name: 'ProjectHook'
has_many :protected_branches
has_many :protected_tags
has_many :repository_languages, -> { order "share DESC" }
+ has_many :designs, inverse_of: :project, class_name: 'DesignManagement::Design'
has_many :project_authorizations
has_many :authorized_users, through: :project_authorizations, source: :user, class_name: 'User'
@@ -254,6 +259,9 @@ class Project < ApplicationRecord
has_many :prometheus_alerts, inverse_of: :project
has_many :prometheus_alert_events, inverse_of: :project
has_many :self_managed_prometheus_alert_events, inverse_of: :project
+ has_many :metrics_users_starred_dashboards, class_name: 'Metrics::UsersStarredDashboard', inverse_of: :project
+
+ has_many :alert_management_alerts, class_name: 'AlertManagement::Alert', inverse_of: :project
# Container repositories need to remove data from the container registry,
# which is not managed by the DB. Hence we're still using dependent: :destroy
@@ -295,6 +303,7 @@ class Project < ApplicationRecord
has_many :project_deploy_tokens
has_many :deploy_tokens, through: :project_deploy_tokens
has_many :resource_groups, class_name: 'Ci::ResourceGroup', inverse_of: :project
+ has_many :freeze_periods, class_name: 'Ci::FreezePeriod', inverse_of: :project
has_one :auto_devops, class_name: 'ProjectAutoDevops', inverse_of: :project, autosave: true
has_many :custom_attributes, class_name: 'ProjectCustomAttribute'
@@ -315,10 +324,13 @@ class Project < ApplicationRecord
has_many :import_failures, inverse_of: :project
has_many :jira_imports, -> { order 'jira_imports.created_at' }, class_name: 'JiraImportState', inverse_of: :project
- has_many :daily_report_results, class_name: 'Ci::DailyReportResult'
+ has_many :daily_build_group_report_results, class_name: 'Ci::DailyBuildGroupReportResult'
+
+ has_many :repository_storage_moves, class_name: 'ProjectRepositoryStorageMove'
accepts_nested_attributes_for :variables, allow_destroy: true
accepts_nested_attributes_for :project_feature, update_only: true
+ accepts_nested_attributes_for :project_setting, update_only: true
accepts_nested_attributes_for :import_data
accepts_nested_attributes_for :auto_devops, update_only: true
accepts_nested_attributes_for :ci_cd_settings, update_only: true
@@ -342,6 +354,9 @@ class Project < ApplicationRecord
:wiki_access_level, :snippets_access_level, :builds_access_level,
:repository_access_level, :pages_access_level, :metrics_dashboard_access_level,
to: :project_feature, allow_nil: true
+ delegate :show_default_award_emojis, :show_default_award_emojis=,
+ :show_default_award_emojis?,
+ to: :project_setting, allow_nil: true
delegate :scheduled?, :started?, :in_progress?, :failed?, :finished?,
prefix: :import, to: :import_state, allow_nil: true
delegate :no_import?, to: :import_state, allow_nil: true
@@ -355,6 +370,7 @@ class Project < ApplicationRecord
delegate :external_dashboard_url, to: :metrics_setting, allow_nil: true, prefix: true
delegate :default_git_depth, :default_git_depth=, to: :ci_cd_settings, prefix: :ci
delegate :forward_deployment_enabled, :forward_deployment_enabled=, :forward_deployment_enabled?, to: :ci_cd_settings
+ delegate :actual_limits, :actual_plan_name, to: :namespace, allow_nil: true
# Validations
validates :creator, presence: true, on: :create
@@ -386,7 +402,6 @@ class Project < ApplicationRecord
validate :check_repository_path_availability, on: :update, if: ->(project) { project.renamed? }
validate :visibility_level_allowed_by_group, if: :should_validate_visibility_level?
validate :visibility_level_allowed_as_fork, if: :should_validate_visibility_level?
- validate :check_wiki_path_conflict
validate :validate_pages_https_only, if: -> { changes.has_key?(:pages_https_only) }
validates :repository_storage,
presence: true,
@@ -515,12 +530,14 @@ class Project < ApplicationRecord
def self.public_or_visible_to_user(user = nil, min_access_level = nil)
min_access_level = nil if user&.admin?
- if user
+ return public_to_user unless user
+
+ if user.is_a?(DeployToken)
+ user.projects
+ else
where('EXISTS (?) OR projects.visibility_level IN (?)',
user.authorizations_for_projects(min_access_level: min_access_level),
Gitlab::VisibilityLevel.levels_for_user(user))
- else
- public_to_user
end
end
@@ -785,6 +802,11 @@ class Project < ApplicationRecord
Feature.enabled?(:jira_issue_import, self, default_enabled: true)
end
+ # LFS and hashed repository storage are required for using Design Management.
+ def design_management_enabled?
+ lfs_enabled? && hashed_storage?(:repository)
+ end
+
def team
@team ||= ProjectTeam.new(self)
end
@@ -793,6 +815,12 @@ class Project < ApplicationRecord
@repository ||= Repository.new(full_path, self, shard: repository_storage, disk_path: disk_path)
end
+ def design_repository
+ strong_memoize(:design_repository) do
+ DesignManagement::Repository.new(self)
+ end
+ end
+
def cleanup
@repository = nil
end
@@ -819,7 +847,7 @@ class Project < ApplicationRecord
latest_pipeline = ci_pipelines.latest_successful_for_ref(ref)
return unless latest_pipeline
- latest_pipeline.builds.latest.with_artifacts_archive.find_by(name: job_name)
+ latest_pipeline.builds.latest.with_downloadable_artifacts.find_by(name: job_name)
end
def latest_successful_build_for_sha(job_name, sha)
@@ -828,7 +856,7 @@ class Project < ApplicationRecord
latest_pipeline = ci_pipelines.latest_successful_for_sha(sha)
return unless latest_pipeline
- latest_pipeline.builds.latest.with_artifacts_archive.find_by(name: job_name)
+ latest_pipeline.builds.latest.with_downloadable_artifacts.find_by(name: job_name)
end
def latest_successful_build_for_ref!(job_name, ref = default_branch)
@@ -865,10 +893,12 @@ class Project < ApplicationRecord
raise Projects::ImportService::Error, _('Jira import feature is disabled.') unless jira_issues_import_feature_flag_enabled?
raise Projects::ImportService::Error, _('Jira integration not configured.') unless jira_service&.active?
- return unless user
+ if user
+ raise Projects::ImportService::Error, _('Cannot import because issues are not available in this project.') unless feature_available?(:issues, user)
+ raise Projects::ImportService::Error, _('You do not have permissions to run the import.') unless user.can?(:admin_project, self)
+ end
- raise Projects::ImportService::Error, _('Cannot import because issues are not available in this project.') unless feature_available?(:issues, user)
- raise Projects::ImportService::Error, _('You do not have permissions to run the import.') unless user.can?(:admin_project, self)
+ raise Projects::ImportService::Error, _('Unable to connect to the Jira instance. Please check your Jira integration configuration.') unless jira_service.test(nil)[:success]
end
def human_import_status_name
@@ -1056,16 +1086,6 @@ class Project < ApplicationRecord
self.errors.add(:visibility_level, _("%{level_name} is not allowed since the fork source project has lower visibility.") % { level_name: level_name })
end
- def check_wiki_path_conflict
- return if path.blank?
-
- path_to_check = path.ends_with?('.wiki') ? path.chomp('.wiki') : "#{path}.wiki"
-
- if Project.where(namespace_id: namespace_id, path: path_to_check).exists?
- errors.add(:name, _('has already been taken'))
- end
- end
-
def pages_https_only
return false unless Gitlab.config.pages.external_https
@@ -1179,11 +1199,7 @@ class Project < ApplicationRecord
end
def issues_tracker
- if external_issue_tracker
- external_issue_tracker
- else
- default_issue_tracker
- end
+ external_issue_tracker || default_issue_tracker
end
def external_issue_reference_pattern
@@ -1328,11 +1344,7 @@ class Project < ApplicationRecord
# rubocop: enable CodeReuse/ServiceClass
def owner
- if group
- group
- else
- namespace.try(:owner)
- end
+ group || namespace.try(:owner)
end
def to_ability_name
@@ -1432,15 +1444,12 @@ class Project < ApplicationRecord
# Expires various caches before a project is renamed.
def expire_caches_before_rename(old_path)
- repo = Repository.new(old_path, self, shard: repository_storage)
- wiki = Repository.new("#{old_path}.wiki", self, shard: repository_storage, repo_type: Gitlab::GlRepository::WIKI)
+ project_repo = Repository.new(old_path, self, shard: repository_storage)
+ wiki_repo = Repository.new("#{old_path}#{Gitlab::GlRepository::WIKI.path_suffix}", self, shard: repository_storage, repo_type: Gitlab::GlRepository::WIKI)
+ design_repo = Repository.new("#{old_path}#{Gitlab::GlRepository::DESIGN.path_suffix}", self, shard: repository_storage, repo_type: Gitlab::GlRepository::DESIGN)
- if repo.exists?
- repo.before_delete
- end
-
- if wiki.exists?
- wiki.before_delete
+ [project_repo, wiki_repo, design_repo].each do |repo|
+ repo.before_delete if repo.exists?
end
end
@@ -1517,6 +1526,10 @@ class Project < ApplicationRecord
end
end
+ def bots
+ users.project_bot
+ end
+
# Filters `users` to return only authorized users of the project
def members_among(users)
if users.is_a?(ActiveRecord::Relation) && !users.loaded?
@@ -1565,10 +1578,6 @@ class Project < ApplicationRecord
create_repository(force: true) unless repository_exists?
end
- def wiki_repository_exists?
- wiki.repository_exists?
- end
-
# update visibility_level of forks
def update_forks_visibility_level
return if unlink_forks_upon_visibility_decrease_enabled?
@@ -1582,20 +1591,6 @@ class Project < ApplicationRecord
end
end
- def create_wiki
- ProjectWiki.new(self, self.owner).wiki
- true
- rescue ProjectWiki::CouldNotCreateWikiError
- errors.add(:base, _('Failed create wiki'))
- false
- end
-
- def wiki
- strong_memoize(:wiki) do
- ProjectWiki.new(self, self.owner)
- end
- end
-
def allowed_to_share_with_group?
!namespace.share_with_group_lock
end
@@ -2024,6 +2019,14 @@ class Project < ApplicationRecord
end
end
+ def ci_instance_variables_for(ref:)
+ if protected_for?(ref)
+ Ci::InstanceVariable.all_cached
+ else
+ Ci::InstanceVariable.unprotected_cached
+ end
+ end
+
def protected_for?(ref)
raise Repository::AmbiguousRefError if repository.ambiguous_ref?(ref)
@@ -2085,7 +2088,12 @@ class Project < ApplicationRecord
raise ArgumentError unless ::Gitlab.config.repositories.storages.key?(new_repository_storage_key)
- run_after_commit { ProjectUpdateRepositoryStorageWorker.perform_async(id, new_repository_storage_key) }
+ storage_move = repository_storage_moves.create!(
+ source_storage_name: repository_storage,
+ destination_storage_name: new_repository_storage_key
+ )
+ storage_move.schedule!
+
self.repository_read_only = true
end
@@ -2425,6 +2433,11 @@ class Project < ApplicationRecord
jira_imports.last
end
+ override :after_wiki_activity
+ def after_wiki_activity
+ touch(:last_activity_at, :last_repository_updated_at)
+ end
+
private
def find_service(services, name)
diff --git a/app/models/project_authorization.rb b/app/models/project_authorization.rb
index e81d9d0f5fe..366852d93bf 100644
--- a/app/models/project_authorization.rb
+++ b/app/models/project_authorization.rb
@@ -2,7 +2,6 @@
class ProjectAuthorization < ApplicationRecord
include FromUnion
- prepend_if_ee('::EE::ProjectAuthorization') # rubocop: disable Cop/InjectEnterpriseEditionModule
belongs_to :user
belongs_to :project
@@ -30,3 +29,5 @@ class ProjectAuthorization < ApplicationRecord
end
end
end
+
+ProjectAuthorization.prepend_if_ee('::EE::ProjectAuthorization')
diff --git a/app/models/project_ci_cd_setting.rb b/app/models/project_ci_cd_setting.rb
index 39e177e8bd8..c295837002a 100644
--- a/app/models/project_ci_cd_setting.rb
+++ b/app/models/project_ci_cd_setting.rb
@@ -37,8 +37,6 @@ class ProjectCiCdSetting < ApplicationRecord
private
def set_default_git_depth
- return unless Feature.enabled?(:ci_set_project_default_git_depth, default_enabled: true)
-
self.default_git_depth ||= DEFAULT_GIT_DEPTH
end
end
diff --git a/app/models/project_feature.rb b/app/models/project_feature.rb
index 31a3fa12c00..9201cd24d66 100644
--- a/app/models/project_feature.rb
+++ b/app/models/project_feature.rb
@@ -23,7 +23,7 @@ class ProjectFeature < ApplicationRecord
PUBLIC = 30
FEATURES = %i(issues forking merge_requests wiki snippets builds repository pages metrics_dashboard).freeze
- PRIVATE_FEATURES_MIN_ACCESS_LEVEL = { merge_requests: Gitlab::Access::REPORTER }.freeze
+ PRIVATE_FEATURES_MIN_ACCESS_LEVEL = { merge_requests: Gitlab::Access::REPORTER, metrics_dashboard: Gitlab::Access::REPORTER }.freeze
PRIVATE_FEATURES_MIN_ACCESS_LEVEL_FOR_PRIVATE_PROJECT = { repository: Gitlab::Access::REPORTER }.freeze
STRING_OPTIONS = HashWithIndifferentAccess.new({
'disabled' => DISABLED,
diff --git a/app/models/project_repository_storage_move.rb b/app/models/project_repository_storage_move.rb
new file mode 100644
index 00000000000..e88cc5cfca6
--- /dev/null
+++ b/app/models/project_repository_storage_move.rb
@@ -0,0 +1,58 @@
+# frozen_string_literal: true
+
+# ProjectRepositoryStorageMove are details of repository storage moves for a
+# project. For example, moving a project to another gitaly node to help
+# balance storage capacity.
+class ProjectRepositoryStorageMove < ApplicationRecord
+ include AfterCommitQueue
+
+ belongs_to :project, inverse_of: :repository_storage_moves
+
+ validates :project, presence: true
+ validates :state, presence: true
+ validates :source_storage_name,
+ on: :create,
+ presence: true,
+ inclusion: { in: ->(_) { Gitlab.config.repositories.storages.keys } }
+ validates :destination_storage_name,
+ on: :create,
+ presence: true,
+ inclusion: { in: ->(_) { Gitlab.config.repositories.storages.keys } }
+
+ state_machine initial: :initial do
+ event :schedule do
+ transition initial: :scheduled
+ end
+
+ event :start do
+ transition scheduled: :started
+ end
+
+ event :finish do
+ transition started: :finished
+ end
+
+ event :do_fail do
+ transition [:initial, :scheduled, :started] => :failed
+ end
+
+ after_transition initial: :scheduled do |storage_move, _|
+ storage_move.run_after_commit do
+ ProjectUpdateRepositoryStorageWorker.perform_async(
+ storage_move.project_id,
+ storage_move.destination_storage_name,
+ storage_move.id
+ )
+ end
+ end
+
+ state :initial, value: 1
+ state :scheduled, value: 2
+ state :started, value: 3
+ state :finished, value: 4
+ state :failed, value: 5
+ end
+
+ scope :order_created_at_desc, -> { order(created_at: :desc) }
+ scope :with_projects, -> { includes(project: :route) }
+end
diff --git a/app/models/project_services/chat_message/merge_message.rb b/app/models/project_services/chat_message/merge_message.rb
index dc62a4c8908..0a2d9120adc 100644
--- a/app/models/project_services/chat_message/merge_message.rb
+++ b/app/models/project_services/chat_message/merge_message.rb
@@ -2,8 +2,6 @@
module ChatMessage
class MergeMessage < BaseMessage
- prepend_if_ee('::EE::ChatMessage::MergeMessage') # rubocop: disable Cop/InjectEnterpriseEditionModule
-
attr_reader :merge_request_iid
attr_reader :source_branch
attr_reader :target_branch
@@ -71,3 +69,5 @@ module ChatMessage
end
end
end
+
+ChatMessage::MergeMessage.prepend_if_ee('::EE::ChatMessage::MergeMessage')
diff --git a/app/models/project_services/chat_message/pipeline_message.rb b/app/models/project_services/chat_message/pipeline_message.rb
index 50b982a803f..1cd3837433f 100644
--- a/app/models/project_services/chat_message/pipeline_message.rb
+++ b/app/models/project_services/chat_message/pipeline_message.rb
@@ -52,8 +52,6 @@ module ChatMessage
def attachments
return message if markdown
- return [{ text: format(message), color: attachment_color }] unless fancy_notifications?
-
[{
fallback: format(message),
color: attachment_color,
@@ -103,10 +101,6 @@ module ChatMessage
failed_jobs.uniq { |job| job[:name] }.reverse
end
- def fancy_notifications?
- Feature.enabled?(:fancy_pipeline_slack_notifications, default_enabled: true)
- end
-
def failed_stages_field
{
title: s_("ChatMessage|Failed stage").pluralize(failed_stages.length),
@@ -166,42 +160,22 @@ module ChatMessage
end
def humanized_status
- if fancy_notifications?
- case status
- when 'success'
- detailed_status == "passed with warnings" ? s_("ChatMessage|has passed with warnings") : s_("ChatMessage|has passed")
- when 'failed'
- s_("ChatMessage|has failed")
- else
- status
- end
+ case status
+ when 'success'
+ detailed_status == "passed with warnings" ? s_("ChatMessage|has passed with warnings") : s_("ChatMessage|has passed")
+ when 'failed'
+ s_("ChatMessage|has failed")
else
- case status
- when 'success'
- s_("ChatMessage|passed")
- when 'failed'
- s_("ChatMessage|failed")
- else
- status
- end
+ status
end
end
def attachment_color
- if fancy_notifications?
- case status
- when 'success'
- detailed_status == 'passed with warnings' ? 'warning' : 'good'
- else
- 'danger'
- end
+ case status
+ when 'success'
+ detailed_status == 'passed with warnings' ? 'warning' : 'good'
else
- case status
- when 'success'
- 'good'
- else
- 'danger'
- end
+ 'danger'
end
end
@@ -230,7 +204,7 @@ module ChatMessage
end
def pipeline_url
- if fancy_notifications? && failed_jobs.any?
+ if failed_jobs.any?
pipeline_failed_jobs_url
else
"#{project_url}/pipelines/#{pipeline_id}"
diff --git a/app/models/project_services/jira_service.rb b/app/models/project_services/jira_service.rb
index eaddac9cce3..53da874ede8 100644
--- a/app/models/project_services/jira_service.rb
+++ b/app/models/project_services/jira_service.rb
@@ -25,6 +25,11 @@ class JiraService < IssueTrackerService
before_update :reset_password
+ enum comment_detail: {
+ standard: 1,
+ all_details: 2
+ }
+
alias_method :project_url, :url
# When these are false GitLab does not create cross reference
@@ -172,6 +177,7 @@ class JiraService < IssueTrackerService
noteable_id = noteable.respond_to?(:iid) ? noteable.iid : noteable.id
noteable_type = noteable_name(noteable)
entity_url = build_entity_url(noteable_type, noteable_id)
+ entity_meta = build_entity_meta(noteable)
data = {
user: {
@@ -180,12 +186,15 @@ class JiraService < IssueTrackerService
},
project: {
name: project.full_path,
- url: resource_url(namespace_project_path(project.namespace, project)) # rubocop:disable Cop/ProjectPathHelper
+ url: resource_url(project_path(project))
},
entity: {
+ id: entity_meta[:id],
name: noteable_type.humanize.downcase,
url: entity_url,
- title: noteable.title
+ title: noteable.title,
+ description: entity_meta[:description],
+ branch: entity_meta[:branch]
}
}
@@ -259,14 +268,11 @@ class JiraService < IssueTrackerService
end
def add_comment(data, issue)
- user_name = data[:user][:name]
- user_url = data[:user][:url]
entity_name = data[:entity][:name]
entity_url = data[:entity][:url]
entity_title = data[:entity][:title]
- project_name = data[:project][:name]
- message = "[#{user_name}|#{user_url}] mentioned this issue in [a #{entity_name} of #{project_name}|#{entity_url}]:\n'#{entity_title.chomp}'"
+ message = comment_message(data)
link_title = "#{entity_name.capitalize} - #{entity_title}"
link_props = build_remote_link_props(url: entity_url, title: link_title)
@@ -275,6 +281,37 @@ class JiraService < IssueTrackerService
end
end
+ def comment_message(data)
+ user_link = build_jira_link(data[:user][:name], data[:user][:url])
+
+ entity = data[:entity]
+ entity_ref = all_details? ? "#{entity[:name]} #{entity[:id]}" : "a #{entity[:name]}"
+ entity_link = build_jira_link(entity_ref, entity[:url])
+
+ project_link = build_jira_link(project.full_name, Gitlab::Routing.url_helpers.project_url(project))
+ branch =
+ if entity[:branch].present?
+ s_('JiraService| on branch %{branch_link}') % {
+ branch_link: build_jira_link(entity[:branch], project_tree_url(project, entity[:branch]))
+ }
+ end
+
+ entity_message = entity[:description].presence if all_details?
+ entity_message ||= entity[:title].chomp
+
+ s_('JiraService|%{user_link} mentioned this issue in %{entity_link} of %{project_link}%{branch}:{quote}%{entity_message}{quote}') % {
+ user_link: user_link,
+ entity_link: entity_link,
+ project_link: project_link,
+ branch: branch,
+ entity_message: entity_message
+ }
+ end
+
+ def build_jira_link(title, url)
+ "[#{title}|#{url}]"
+ end
+
def has_resolution?(issue)
issue.respond_to?(:resolution) && issue.resolution.present?
end
@@ -348,6 +385,23 @@ class JiraService < IssueTrackerService
)
end
+ def build_entity_meta(noteable)
+ if noteable.is_a?(Commit)
+ {
+ id: noteable.short_id,
+ description: noteable.safe_message,
+ branch: noteable.ref_names(project.repository).first
+ }
+ elsif noteable.is_a?(MergeRequest)
+ {
+ id: noteable.to_reference,
+ branch: noteable.source_branch
+ }
+ else
+ {}
+ end
+ end
+
def noteable_name(noteable)
name = noteable.model_name.singular
diff --git a/app/models/project_services/mattermost_slash_commands_service.rb b/app/models/project_services/mattermost_slash_commands_service.rb
index ca324f68d2d..0fd85e3a5a9 100644
--- a/app/models/project_services/mattermost_slash_commands_service.rb
+++ b/app/models/project_services/mattermost_slash_commands_service.rb
@@ -36,6 +36,10 @@ class MattermostSlashCommandsService < SlashCommandsService
[[], e.message]
end
+ def chat_responder
+ ::Gitlab::Chat::Responder::Mattermost
+ end
+
private
def command(params)
diff --git a/app/models/project_services/mock_monitoring_service.rb b/app/models/project_services/mock_monitoring_service.rb
index bcf8f1df5da..25ae0f6b60d 100644
--- a/app/models/project_services/mock_monitoring_service.rb
+++ b/app/models/project_services/mock_monitoring_service.rb
@@ -14,7 +14,7 @@ class MockMonitoringService < MonitoringService
end
def metrics(environment)
- JSON.parse(File.read(Rails.root + 'spec/fixtures/metrics.json'))
+ Gitlab::Json.parse(File.read(Rails.root + 'spec/fixtures/metrics.json'))
end
def can_test?
diff --git a/app/models/project_services/webex_teams_service.rb b/app/models/project_services/webex_teams_service.rb
new file mode 100644
index 00000000000..1d791b19486
--- /dev/null
+++ b/app/models/project_services/webex_teams_service.rb
@@ -0,0 +1,57 @@
+# frozen_string_literal: true
+
+class WebexTeamsService < ChatNotificationService
+ def title
+ 'Webex Teams'
+ end
+
+ def description
+ 'Receive event notifications in Webex Teams'
+ end
+
+ def self.to_param
+ 'webex_teams'
+ end
+
+ def help
+ 'This service sends notifications about projects events to a Webex Teams conversation.<br />
+ To set up this service:
+ <ol>
+ <li><a href="https://apphub.webex.com/teams/applications/incoming-webhooks-cisco-systems">Set up an incoming webhook for your conversation</a>. All notifications will come to this conversation.</li>
+ <li>Paste the <strong>Webhook URL</strong> into the field below.</li>
+ <li>Select events below to enable notifications.</li>
+ </ol>'
+ end
+
+ def event_field(event)
+ end
+
+ def default_channel_placeholder
+ end
+
+ def self.supported_events
+ %w[push issue confidential_issue merge_request note confidential_note tag_push
+ pipeline wiki_page]
+ end
+
+ def default_fields
+ [
+ { type: 'text', name: 'webhook', placeholder: "e.g. https://api.ciscospark.com/v1/webhooks/incoming/…", required: true },
+ { type: 'checkbox', name: 'notify_only_broken_pipelines' },
+ { type: 'select', name: 'branches_to_be_notified', choices: branch_choices }
+ ]
+ end
+
+ private
+
+ def notify(message, opts)
+ header = { 'Content-Type' => 'application/json' }
+ response = Gitlab::HTTP.post(webhook, headers: header, body: { markdown: message.pretext }.to_json)
+
+ response if response.success?
+ end
+
+ def custom_data(data)
+ super(data).merge(markdown: true)
+ end
+end
diff --git a/app/models/project_services/youtrack_service.rb b/app/models/project_services/youtrack_service.rb
index 0815e27850d..40203ad692d 100644
--- a/app/models/project_services/youtrack_service.rb
+++ b/app/models/project_services/youtrack_service.rb
@@ -27,8 +27,8 @@ class YoutrackService < IssueTrackerService
def fields
[
{ type: 'text', name: 'description', placeholder: description },
- { type: 'text', name: 'project_url', placeholder: 'Project url', required: true },
- { type: 'text', name: 'issues_url', placeholder: 'Issue url', required: true }
+ { type: 'text', name: 'project_url', title: 'Project URL', placeholder: 'Project URL', required: true },
+ { type: 'text', name: 'issues_url', title: 'Issue URL', placeholder: 'Issue URL', required: true }
]
end
end
diff --git a/app/models/project_statistics.rb b/app/models/project_statistics.rb
index b71ed75dde6..6f04a36392d 100644
--- a/app/models/project_statistics.rb
+++ b/app/models/project_statistics.rb
@@ -21,6 +21,9 @@ class ProjectStatistics < ApplicationRecord
scope :for_project_ids, ->(project_ids) { where(project_id: project_ids) }
+ scope :for_namespaces, -> (namespaces) { where(namespace: namespaces) }
+ scope :with_any_ci_minutes_used, -> { where.not(shared_runners_seconds: 0) }
+
def total_repository_size
repository_size + lfs_objects_size
end
diff --git a/app/models/project_wiki.rb b/app/models/project_wiki.rb
index 708b45cf5f0..5df0a33dc9a 100644
--- a/app/models/project_wiki.rb
+++ b/app/models/project_wiki.rb
@@ -1,219 +1,17 @@
# frozen_string_literal: true
-class ProjectWiki
- include Storage::LegacyProjectWiki
- include Gitlab::Utils::StrongMemoize
+class ProjectWiki < Wiki
+ alias_method :project, :container
- MARKUPS = {
- 'Markdown' => :markdown,
- 'RDoc' => :rdoc,
- 'AsciiDoc' => :asciidoc,
- 'Org' => :org
- }.freeze unless defined?(MARKUPS)
+ # Project wikis are tied to the main project storage
+ delegate :storage, :repository_storage, :hashed_storage?, to: :container
- CouldNotCreateWikiError = Class.new(StandardError)
- SIDEBAR = '_sidebar'
-
- TITLE_ORDER = 'title'
- CREATED_AT_ORDER = 'created_at'
- DIRECTION_DESC = 'desc'
- DIRECTION_ASC = 'asc'
-
- attr_reader :project, :user
-
- # Returns a string describing what went wrong after
- # an operation fails.
- attr_reader :error_message
-
- def initialize(project, user = nil)
- @project = project
- @user = user
- end
-
- delegate :repository_storage, :hashed_storage?, to: :project
-
- def path
- @project.path + '.wiki'
- end
-
- def full_path
- @project.full_path + '.wiki'
- end
- alias_method :id, :full_path
-
- # @deprecated use full_path when you need it for an URL route or disk_path when you want to point to the filesystem
- alias_method :path_with_namespace, :full_path
-
- def web_url(only_path: nil)
- Gitlab::UrlBuilder.build(self, only_path: only_path)
- end
-
- def url_to_repo
- ssh_url_to_repo
- end
-
- def ssh_url_to_repo
- Gitlab::RepositoryUrlBuilder.build(repository.full_path, protocol: :ssh)
- end
-
- def http_url_to_repo
- Gitlab::RepositoryUrlBuilder.build(repository.full_path, protocol: :http)
- end
-
- def wiki_base_path
- [Gitlab.config.gitlab.relative_url_root, '/', @project.full_path, '/-', '/wikis'].join('')
- end
-
- # Returns the Gitlab::Git::Wiki object.
- def wiki
- strong_memoize(:wiki) do
- repository.create_if_not_exists
- raise CouldNotCreateWikiError unless repository_exists?
-
- Gitlab::Git::Wiki.new(repository.raw)
- end
- rescue => err
- Gitlab::ErrorTracking.track_exception(err, project_wiki: { project_id: project.id, full_path: full_path, disk_path: disk_path })
- raise CouldNotCreateWikiError
- end
-
- def repository_exists?
- !!repository.exists?
- end
-
- def has_home_page?
- !!find_page('home')
- end
-
- def empty?
- list_pages(limit: 1).empty?
- end
-
- def exists?
- !empty?
- end
-
- # Lists wiki pages of the repository.
- #
- # limit - max number of pages returned by the method.
- # sort - criterion by which the pages are sorted.
- # direction - order of the sorted pages.
- # load_content - option, which specifies whether the content inside the page
- # will be loaded.
- #
- # Returns an Array of GitLab WikiPage instances or an
- # empty Array if this Wiki has no pages.
- def list_pages(limit: 0, sort: nil, direction: DIRECTION_ASC, load_content: false)
- wiki.list_pages(
- limit: limit,
- sort: sort,
- direction_desc: direction == DIRECTION_DESC,
- load_content: load_content
- ).map do |page|
- WikiPage.new(self, page)
- end
- end
-
- # Finds a page within the repository based on a tile
- # or slug.
- #
- # title - The human readable or parameterized title of
- # the page.
- #
- # Returns an initialized WikiPage instance or nil
- def find_page(title, version = nil)
- page_title, page_dir = page_title_and_dir(title)
-
- if page = wiki.page(title: page_title, version: version, dir: page_dir)
- WikiPage.new(self, page)
- end
- end
-
- def find_sidebar(version = nil)
- find_page(SIDEBAR, version)
- end
-
- def find_file(name, version = nil)
- wiki.file(name, version)
- end
-
- def create_page(title, content, format = :markdown, message = nil)
- commit = commit_details(:created, message, title)
-
- wiki.write_page(title, format.to_sym, content, commit)
-
- update_project_activity
- rescue Gitlab::Git::Wiki::DuplicatePageError => e
- @error_message = "Duplicate page: #{e.message}"
- false
- end
-
- def update_page(page, content:, title: nil, format: :markdown, message: nil)
- commit = commit_details(:updated, message, page.title)
-
- wiki.update_page(page.path, title || page.name, format.to_sym, content, commit)
-
- update_project_activity
- end
-
- def delete_page(page, message = nil)
- return unless page
-
- wiki.delete_page(page.path, commit_details(:deleted, message, page.title))
-
- update_project_activity
- end
-
- def page_title_and_dir(title)
- return unless title
-
- title_array = title.split("/")
- title = title_array.pop
- [title, title_array.join("/")]
- end
-
- def repository
- @repository ||= Repository.new(full_path, @project, shard: repository_storage, disk_path: disk_path, repo_type: Gitlab::GlRepository::WIKI)
- end
-
- def default_branch
- wiki.class.default_ref
- end
-
- def ensure_repository
- raise CouldNotCreateWikiError unless wiki.repository_exists?
- end
-
- def hook_attrs
- {
- web_url: web_url,
- git_ssh_url: ssh_url_to_repo,
- git_http_url: http_url_to_repo,
- path_with_namespace: full_path,
- default_branch: default_branch
- }
- end
-
- private
-
- def commit_details(action, message = nil, title = nil)
- commit_message = message.presence || default_message(action, title)
- git_user = Gitlab::Git::User.from_gitlab(user)
-
- Gitlab::Git::Wiki::CommitDetails.new(user.id,
- git_user.username,
- git_user.name,
- git_user.email,
- commit_message)
- end
-
- def default_message(action, title)
- "#{user.username} #{action} page: #{title}"
- end
-
- def update_project_activity
- @project.touch(:last_activity_at, :last_repository_updated_at)
+ override :disk_path
+ def disk_path(*args, &block)
+ container.disk_path + '.wiki'
end
end
+# TODO: Remove this once we implement ES support for group wikis.
+# https://gitlab.com/gitlab-org/gitlab/-/issues/207889
ProjectWiki.prepend_if_ee('EE::ProjectWiki')
diff --git a/app/models/release.rb b/app/models/release.rb
index 403087a2cad..a0245105cd9 100644
--- a/app/models/release.rb
+++ b/app/models/release.rb
@@ -34,8 +34,6 @@ class Release < ApplicationRecord
delegate :repository, to: :project
- after_commit :notify_new_release, on: :create, unless: :importing?
-
MAX_NUMBER_TO_DISPLAY = 3
def to_param
@@ -81,14 +79,6 @@ class Release < ApplicationRecord
self.milestones.map {|m| m.title }.sort.join(", ")
end
- def evidence_sha
- evidences.first&.summary_sha
- end
-
- def evidence_summary
- evidences.first&.summary || {}
- end
-
private
def actual_sha
@@ -100,10 +90,6 @@ class Release < ApplicationRecord
repository.find_tag(tag)
end
end
-
- def notify_new_release
- NewReleaseWorker.perform_async(id)
- end
end
Release.prepend_if_ee('EE::Release')
diff --git a/app/models/remote_mirror.rb b/app/models/remote_mirror.rb
index 0334d63dd36..8e7612e63c8 100644
--- a/app/models/remote_mirror.rb
+++ b/app/models/remote_mirror.rb
@@ -106,7 +106,23 @@ class RemoteMirror < ApplicationRecord
update_status == 'started'
end
- def update_repository(options)
+ def update_repository
+ Gitlab::Git::RemoteMirror.new(
+ project.repository.raw,
+ remote_name,
+ **options_for_update
+ ).update
+ end
+
+ def options_for_update
+ options = {
+ keep_divergent_refs: keep_divergent_refs?
+ }
+
+ if only_protected_branches?
+ options[:only_branches_matching] = project.protected_branches.pluck(:name)
+ end
+
if ssh_mirror_url?
if ssh_key_auth? && ssh_private_key.present?
options[:ssh_key] = ssh_private_key
@@ -117,13 +133,7 @@ class RemoteMirror < ApplicationRecord
end
end
- options[:keep_divergent_refs] = keep_divergent_refs?
-
- Gitlab::Git::RemoteMirror.new(
- project.repository.raw,
- remote_name,
- **options
- ).update
+ options
end
def sync?
diff --git a/app/models/repository.rb b/app/models/repository.rb
index a9ef0504a3d..2673033ff1f 100644
--- a/app/models/repository.rb
+++ b/app/models/repository.rb
@@ -1120,6 +1120,17 @@ class Repository
end
end
+ # TODO: pass this in directly to `Blob` rather than delegating it to here
+ #
+ # https://gitlab.com/gitlab-org/gitlab/-/issues/201886
+ def lfs_enabled?
+ if container.is_a?(Project)
+ container.lfs_enabled?
+ else
+ false # LFS is not supported for snippet or group repositories
+ end
+ end
+
private
# TODO Genericize finder, later split this on finders by Ref or Oid
diff --git a/app/models/resource_label_event.rb b/app/models/resource_label_event.rb
index cd47c154eef..845be408d5e 100644
--- a/app/models/resource_label_event.rb
+++ b/app/models/resource_label_event.rb
@@ -2,16 +2,14 @@
class ResourceLabelEvent < ResourceEvent
include CacheMarkdownField
+ include IssueResourceEvent
+ include MergeRequestResourceEvent
cache_markdown_field :reference
- belongs_to :issue
- belongs_to :merge_request
belongs_to :label
scope :inc_relations, -> { includes(:label, :user) }
- scope :by_issue, ->(issue) { where(issue_id: issue.id) }
- scope :by_merge_request, ->(merge_request) { where(merge_request_id: merge_request.id) }
validates :label, presence: { unless: :importing? }, on: :create
validate :exactly_one_issuable
diff --git a/app/models/resource_milestone_event.rb b/app/models/resource_milestone_event.rb
index a40af22061e..039f26d8e3f 100644
--- a/app/models/resource_milestone_event.rb
+++ b/app/models/resource_milestone_event.rb
@@ -2,14 +2,11 @@
class ResourceMilestoneEvent < ResourceEvent
include IgnorableColumns
+ include IssueResourceEvent
+ include MergeRequestResourceEvent
- belongs_to :issue
- belongs_to :merge_request
belongs_to :milestone
- scope :by_issue, ->(issue) { where(issue_id: issue.id) }
- scope :by_merge_request, ->(merge_request) { where(merge_request_id: merge_request.id) }
-
validate :exactly_one_issuable
enum action: {
@@ -25,4 +22,8 @@ class ResourceMilestoneEvent < ResourceEvent
def self.issuable_attrs
%i(issue merge_request).freeze
end
+
+ def milestone_title
+ milestone&.title
+ end
end
diff --git a/app/models/resource_state_event.rb b/app/models/resource_state_event.rb
new file mode 100644
index 00000000000..1d6573b180f
--- /dev/null
+++ b/app/models/resource_state_event.rb
@@ -0,0 +1,15 @@
+# frozen_string_literal: true
+
+class ResourceStateEvent < ResourceEvent
+ include IssueResourceEvent
+ include MergeRequestResourceEvent
+
+ validate :exactly_one_issuable
+
+ # state is used for issue and merge request states.
+ enum state: Issue.available_states.merge(MergeRequest.available_states).merge(reopened: 5)
+
+ def self.issuable_attrs
+ %i(issue merge_request).freeze
+ end
+end
diff --git a/app/models/resource_weight_event.rb b/app/models/resource_weight_event.rb
index e0cc0c87a83..bbabd54325e 100644
--- a/app/models/resource_weight_event.rb
+++ b/app/models/resource_weight_event.rb
@@ -3,7 +3,5 @@
class ResourceWeightEvent < ResourceEvent
validates :issue, presence: true
- belongs_to :issue
-
- scope :by_issue, ->(issue) { where(issue_id: issue.id) }
+ include IssueResourceEvent
end
diff --git a/app/models/sent_notification.rb b/app/models/sent_notification.rb
index f3a9293376f..4165d3b753f 100644
--- a/app/models/sent_notification.rb
+++ b/app/models/sent_notification.rb
@@ -76,12 +76,14 @@ class SentNotification < ApplicationRecord
def position=(new_position)
if new_position.is_a?(String)
- new_position = JSON.parse(new_position) rescue nil
+ new_position = Gitlab::Json.parse(new_position) rescue nil
end
if new_position.is_a?(Hash)
new_position = new_position.with_indifferent_access
new_position = Gitlab::Diff::Position.new(new_position)
+ else
+ new_position = nil
end
super(new_position)
diff --git a/app/models/service.rb b/app/models/service.rb
index 543869c71d6..fb4d9a77077 100644
--- a/app/models/service.rb
+++ b/app/models/service.rb
@@ -12,7 +12,7 @@ class Service < ApplicationRecord
alerts asana assembla bamboo bugzilla buildkite campfire custom_issue_tracker discord
drone_ci emails_on_push external_wiki flowdock hangouts_chat hipchat irker jira
mattermost mattermost_slash_commands microsoft_teams packagist pipelines_email
- pivotaltracker prometheus pushover redmine slack slack_slash_commands teamcity unify_circuit youtrack
+ pivotaltracker prometheus pushover redmine slack slack_slash_commands teamcity unify_circuit webex_teams youtrack
].freeze
DEV_SERVICE_NAMES = %w[
@@ -81,6 +81,10 @@ class Service < ApplicationRecord
active
end
+ def operating?
+ active && persisted?
+ end
+
def show_active_box?
true
end
@@ -345,14 +349,6 @@ class Service < ApplicationRecord
service
end
- def deprecated?
- false
- end
-
- def deprecation_message
- nil
- end
-
# override if needed
def supports_data_fields?
false
diff --git a/app/models/snippet.rb b/app/models/snippet.rb
index dbf600cf0df..72ebdf61787 100644
--- a/app/models/snippet.rb
+++ b/app/models/snippet.rb
@@ -15,9 +15,11 @@ class Snippet < ApplicationRecord
include FromUnion
include IgnorableColumns
include HasRepository
+ include AfterCommitQueue
extend ::Gitlab::Utils::Override
- MAX_FILE_COUNT = 1
+ MAX_FILE_COUNT = 10
+ MAX_SINGLE_FILE_COUNT = 1
cache_markdown_field :title, pipeline: :single_line
cache_markdown_field :description
@@ -101,6 +103,10 @@ class Snippet < ApplicationRecord
where(project_id: nil)
end
+ def self.only_project_snippets
+ where.not(project_id: nil)
+ end
+
def self.only_include_projects_visible_to(current_user = nil)
levels = Gitlab::VisibilityLevel.levels_for_user(current_user)
@@ -164,6 +170,10 @@ class Snippet < ApplicationRecord
Snippet.find_by(id: id, project: project)
end
+ def self.max_file_limit(user)
+ Feature.enabled?(:snippet_multiple_files, user) ? MAX_FILE_COUNT : MAX_SINGLE_FILE_COUNT
+ end
+
def initialize(attributes = {})
# We can't use default_value_for because the database has a default
# value of 0 for visibility_level. If someone attempts to create a
@@ -199,7 +209,7 @@ class Snippet < ApplicationRecord
def blobs
return [] unless repository_exists?
- repository.ls_files(repository.root_ref).map { |file| Blob.lazy(self, repository.root_ref, file) }
+ repository.ls_files(repository.root_ref).map { |file| Blob.lazy(repository, repository.root_ref, file) }
end
def hook_attrs
@@ -318,8 +328,10 @@ class Snippet < ApplicationRecord
Digest::SHA256.hexdigest("#{title}#{description}#{created_at}#{updated_at}")
end
- def versioned_enabled_for?(user)
- ::Feature.enabled?(:version_snippets, user) && repository_exists?
+ def file_name_on_repo
+ return if repository.empty?
+
+ repository.ls_files(repository.root_ref).first
end
class << self
@@ -334,17 +346,6 @@ class Snippet < ApplicationRecord
fuzzy_search(query, [:title, :description, :file_name])
end
- # Searches for snippets with matching content.
- #
- # This method uses ILIKE on PostgreSQL and LIKE on MySQL.
- #
- # query - The search query as a String.
- #
- # Returns an ActiveRecord::Relation.
- def search_code(query)
- fuzzy_search(query, [:content])
- end
-
def parent_class
::Project
end
diff --git a/app/models/snippet_repository.rb b/app/models/snippet_repository.rb
index e60dbb4d141..2276851b7a1 100644
--- a/app/models/snippet_repository.rb
+++ b/app/models/snippet_repository.rb
@@ -7,6 +7,8 @@ class SnippetRepository < ApplicationRecord
EMPTY_FILE_PATTERN = /^#{DEFAULT_EMPTY_FILE_NAME}(\d+)\.txt$/.freeze
CommitError = Class.new(StandardError)
+ InvalidPathError = Class.new(CommitError)
+ InvalidSignatureError = Class.new(CommitError)
belongs_to :snippet, inverse_of: :snippet_repository
@@ -40,8 +42,12 @@ class SnippetRepository < ApplicationRecord
rescue Gitlab::Git::Index::IndexError,
Gitlab::Git::CommitError,
Gitlab::Git::PreReceiveError,
- Gitlab::Git::CommandError => e
- raise CommitError, e.message
+ Gitlab::Git::CommandError,
+ ArgumentError => error
+
+ logger.error(message: "Snippet git error. Reason: #{error.message}", snippet: snippet.id)
+
+ raise commit_error_exception(error)
end
def transform_file_entries(files)
@@ -85,4 +91,24 @@ class SnippetRepository < ApplicationRecord
def build_empty_file_name(index)
"#{DEFAULT_EMPTY_FILE_NAME}#{index}.txt"
end
+
+ def commit_error_exception(err)
+ if invalid_path_error?(err)
+ InvalidPathError.new('Invalid file name') # To avoid returning the message with the path included
+ elsif invalid_signature_error?(err)
+ InvalidSignatureError.new(err.message)
+ else
+ CommitError.new(err.message)
+ end
+ end
+
+ def invalid_path_error?(err)
+ err.is_a?(Gitlab::Git::Index::IndexError) &&
+ err.message.downcase.start_with?('invalid path', 'path cannot include directory traversal')
+ end
+
+ def invalid_signature_error?(err)
+ err.is_a?(ArgumentError) &&
+ err.message.downcase.match?(/failed to parse signature/)
+ end
end
diff --git a/app/models/ssh_host_key.rb b/app/models/ssh_host_key.rb
index 9bd35d30845..72690ad7d04 100644
--- a/app/models/ssh_host_key.rb
+++ b/app/models/ssh_host_key.rb
@@ -24,6 +24,7 @@ class SshHostKey
# This is achieved by making the lifetime shorter than the refresh interval.
self.reactive_cache_refresh_interval = 15.minutes
self.reactive_cache_lifetime = 10.minutes
+ self.reactive_cache_work_type = :external_dependency
def self.find_by(opts = {})
opts = HashWithIndifferentAccess.new(opts)
diff --git a/app/models/state_note.rb b/app/models/state_note.rb
new file mode 100644
index 00000000000..cbcb1c2b49d
--- /dev/null
+++ b/app/models/state_note.rb
@@ -0,0 +1,19 @@
+# frozen_string_literal: true
+
+class StateNote < SyntheticNote
+ def self.from_event(event, resource: nil, resource_parent: nil)
+ attrs = note_attributes(event.state, event, resource, resource_parent)
+
+ StateNote.new(attrs)
+ end
+
+ def note_html
+ @note_html ||= "<p dir=\"auto\">#{note_text(html: true)}</p>"
+ end
+
+ private
+
+ def note_text(html: false)
+ event.state
+ end
+end
diff --git a/app/models/storage/hashed.rb b/app/models/storage/hashed.rb
index 3dea50ab98b..c61cd3b6b30 100644
--- a/app/models/storage/hashed.rb
+++ b/app/models/storage/hashed.rb
@@ -6,6 +6,7 @@ module Storage
delegate :gitlab_shell, :repository_storage, to: :container
REPOSITORY_PATH_PREFIX = '@hashed'
+ GROUP_REPOSITORY_PATH_PREFIX = '@groups'
SNIPPET_REPOSITORY_PATH_PREFIX = '@snippets'
POOL_PATH_PREFIX = '@pools'
diff --git a/app/models/system_note_metadata.rb b/app/models/system_note_metadata.rb
index b881a43ad4d..4e14bb4e92c 100644
--- a/app/models/system_note_metadata.rb
+++ b/app/models/system_note_metadata.rb
@@ -15,6 +15,7 @@ class SystemNoteMetadata < ApplicationRecord
ICON_TYPES = %w[
commit description merge confidential visible label assignee cross_reference
+ designs_added designs_modified designs_removed designs_discussion_added
title time_tracking branch milestone discussion task moved
opened closed merged duplicate locked unlocked outdated
tag due_date pinned_embed cherry_pick health_status
diff --git a/app/models/timelog.rb b/app/models/timelog.rb
index f52dd74d4c9..c0aac6f27aa 100644
--- a/app/models/timelog.rb
+++ b/app/models/timelog.rb
@@ -16,8 +16,8 @@ class Timelog < ApplicationRecord
)
end
- scope :between_dates, -> (start_date, end_date) do
- where('spent_at BETWEEN ? AND ?', start_date, end_date)
+ scope :between_times, -> (start_time, end_time) do
+ where('spent_at BETWEEN ? AND ?', start_time, end_time)
end
def issuable
diff --git a/app/models/todo.rb b/app/models/todo.rb
index d337ef33051..dc42551f0ab 100644
--- a/app/models/todo.rb
+++ b/app/models/todo.rb
@@ -110,7 +110,7 @@ class Todo < ApplicationRecord
base = where.not(state: new_state).except(:order)
ids = base.pluck(:id)
- base.update_all(state: new_state)
+ base.update_all(state: new_state, updated_at: Time.now)
ids
end
@@ -183,6 +183,10 @@ class Todo < ApplicationRecord
target_type == "Commit"
end
+ def for_design?
+ target_type == DesignManagement::Design.name
+ end
+
# override to return commits, which are not active record
def target
if for_commit?
diff --git a/app/models/user.rb b/app/models/user.rb
index 1b087da3a2f..b2d3978551e 100644
--- a/app/models/user.rb
+++ b/app/models/user.rb
@@ -24,6 +24,7 @@ class User < ApplicationRecord
include HasUniqueInternalUsers
include IgnorableColumns
include UpdateHighestRole
+ include HasUserType
DEFAULT_NOTIFICATION_LEVEL = :participating
@@ -57,6 +58,10 @@ class User < ApplicationRecord
devise :lockable, :recoverable, :rememberable, :trackable,
:validatable, :omniauthable, :confirmable, :registerable
+ # This module adds async behaviour to Devise emails
+ # and should be added after Devise modules are initialized.
+ include AsyncDeviseEmail
+
BLOCKED_MESSAGE = "Your account has been blocked. Please contact your GitLab " \
"administrator if you think this is an error."
LOGIN_FORBIDDEN = "Your account does not have the required permission to login. Please contact your GitLab " \
@@ -64,9 +69,8 @@ class User < ApplicationRecord
MINIMUM_INACTIVE_DAYS = 180
- enum user_type: ::UserTypeEnums.types
-
- ignore_column :bot_type, remove_with: '12.11', remove_after: '2020-04-22'
+ ignore_column :bot_type, remove_with: '13.1', remove_after: '2020-05-22'
+ ignore_column :ghost, remove_with: '13.2', remove_after: '2020-06-22'
# Override Devise::Models::Trackable#update_tracked_fields!
# to limit database writes to at most once every hour
@@ -88,6 +92,9 @@ class User < ApplicationRecord
# Virtual attribute for authenticating by either username or email
attr_accessor :login
+ # Virtual attribute for impersonator
+ attr_accessor :impersonator
+
#
# Relations
#
@@ -166,6 +173,8 @@ class User < ApplicationRecord
has_many :term_agreements
belongs_to :accepted_term, class_name: 'ApplicationSetting::Term'
+ has_many :metrics_users_starred_dashboards, class_name: 'Metrics::UsersStarredDashboard', inverse_of: :user
+
has_one :status, class_name: 'UserStatus'
has_one :user_preference
has_one :user_detail
@@ -246,15 +255,12 @@ class User < ApplicationRecord
enum layout: { fixed: 0, fluid: 1 }
# User's Dashboard preference
- # Note: When adding an option, it MUST go on the end of the array.
enum dashboard: { projects: 0, stars: 1, project_activity: 2, starred_project_activity: 3, groups: 4, todos: 5, issues: 6, merge_requests: 7, operations: 8 }
# User's Project preference
- # Note: When adding an option, it MUST go on the end of the array.
enum project_view: { readme: 0, activity: 1, files: 2 }
# User's role
- # Note: When adding an option, it MUST go on the end of the array.
enum role: { software_developer: 0, development_team_lead: 1, devops_engineer: 2, systems_administrator: 3, security_analyst: 4, data_analyst: 5, product_manager: 6, product_designer: 7, other: 8 }, _suffix: true
delegate :path, to: :namespace, allow_nil: true, prefix: true
@@ -321,32 +327,26 @@ class User < ApplicationRecord
scope :admins, -> { where(admin: true) }
scope :blocked, -> { with_states(:blocked, :ldap_blocked) }
scope :external, -> { where(external: true) }
+ scope :confirmed, -> { where.not(confirmed_at: nil) }
scope :active, -> { with_state(:active).non_internal }
scope :active_without_ghosts, -> { with_state(:active).without_ghosts }
- scope :without_ghosts, -> { where('ghost IS NOT TRUE') }
scope :deactivated, -> { with_state(:deactivated).non_internal }
scope :without_projects, -> { joins('LEFT JOIN project_authorizations ON users.id = project_authorizations.user_id').where(project_authorizations: { user_id: nil }) }
- scope :order_recent_sign_in, -> { reorder(Gitlab::Database.nulls_last_order('current_sign_in_at', 'DESC')) }
- scope :order_oldest_sign_in, -> { reorder(Gitlab::Database.nulls_last_order('current_sign_in_at', 'ASC')) }
- scope :order_recent_last_activity, -> { reorder(Gitlab::Database.nulls_last_order('last_activity_on', 'DESC')) }
- scope :order_oldest_last_activity, -> { reorder(Gitlab::Database.nulls_first_order('last_activity_on', 'ASC')) }
- scope :confirmed, -> { where.not(confirmed_at: nil) }
scope :by_username, -> (usernames) { iwhere(username: Array(usernames).map(&:to_s)) }
scope :for_todos, -> (todos) { where(id: todos.select(:user_id)) }
scope :with_emails, -> { preload(:emails) }
scope :with_dashboard, -> (dashboard) { where(dashboard: dashboard) }
scope :with_public_profile, -> { where(private_profile: false) }
- scope :bots, -> { where(user_type: UserTypeEnums.bots.values) }
- scope :bots_without_project_bot, -> { bots.where.not(user_type: UserTypeEnums.bots[:project_bot]) }
- scope :with_project_bots, -> { humans.or(where.not(user_type: UserTypeEnums.bots.except(:project_bot).values)) }
- scope :humans, -> { where(user_type: nil) }
-
scope :with_expiring_and_not_notified_personal_access_tokens, ->(at) do
where('EXISTS (?)',
::PersonalAccessToken
.where('personal_access_tokens.user_id = users.id')
.expiring_and_not_notified(at).select(1))
end
+ scope :order_recent_sign_in, -> { reorder(Gitlab::Database.nulls_last_order('current_sign_in_at', 'DESC')) }
+ scope :order_oldest_sign_in, -> { reorder(Gitlab::Database.nulls_last_order('current_sign_in_at', 'ASC')) }
+ scope :order_recent_last_activity, -> { reorder(Gitlab::Database.nulls_last_order('last_activity_on', 'DESC')) }
+ scope :order_oldest_last_activity, -> { reorder(Gitlab::Database.nulls_first_order('last_activity_on', 'ASC')) }
def active_for_authentication?
super && can?(:log_in)
@@ -624,7 +624,7 @@ class User < ApplicationRecord
# owns records previously belonging to deleted users.
def ghost
email = 'ghost%s@example.com'
- unique_internal(where(ghost: true, user_type: :ghost), 'ghost', email) do |u|
+ unique_internal(where(user_type: :ghost), 'ghost', email) do |u|
u.bio = _('This is a "Ghost User", created to hold all issues authored by users that have since been deleted. This user cannot be removed.')
u.name = 'Ghost User'
end
@@ -639,6 +639,16 @@ class User < ApplicationRecord
end
end
+ def migration_bot
+ email_pattern = "noreply+gitlab-migration-bot%s@#{Settings.gitlab.host}"
+
+ unique_internal(where(user_type: :migration_bot), 'migration-bot', email_pattern) do |u|
+ u.bio = 'The GitLab migration bot'
+ u.name = 'GitLab Migration Bot'
+ u.confirmed_at = Time.zone.now
+ end
+ end
+
# Return true if there is only single non-internal user in the deployment,
# ghost user is ignored.
def single_user?
@@ -650,43 +660,14 @@ class User < ApplicationRecord
end
end
- def full_path
- username
- end
-
- def bot?
- UserTypeEnums.bots.has_key?(user_type)
- end
-
- # The explicit check for project_bot will be removed with Bot Categorization
- # Ref: https://gitlab.com/gitlab-org/gitlab/-/issues/213945
- def internal?
- ghost? || (bot? && !project_bot?)
- end
-
- # We are transitioning from ghost boolean column to user_type
- # so we need to read from old column for now
- # @see https://gitlab.com/gitlab-org/gitlab/-/issues/210025
- def ghost?
- ghost
- end
-
- # The explicit check for project_bot will be removed with Bot Categorization
- # Ref: https://gitlab.com/gitlab-org/gitlab/-/issues/213945
- def self.internal
- where(ghost: true).or(bots_without_project_bot)
- end
-
- # The explicit check for project_bot will be removed with Bot Categorization
- # Ref: https://gitlab.com/gitlab-org/gitlab/-/issues/213945
- def self.non_internal
- without_ghosts.with_project_bots
- end
-
#
# Instance methods
#
+ def full_path
+ username
+ end
+
def to_param
username
end
@@ -1700,16 +1681,6 @@ class User < ApplicationRecord
callouts.any?
end
- def gitlab_employee?
- strong_memoize(:gitlab_employee) do
- if Feature.enabled?(:gitlab_employee_badge) && Gitlab.com?
- Mail::Address.new(email).domain == "gitlab.com" && confirmed?
- else
- false
- end
- end
- end
-
# Load the current highest access by looking directly at the user's memberships
def current_highest_access_level
members.non_request.maximum(:access_level)
@@ -1719,8 +1690,8 @@ class User < ApplicationRecord
!confirmed? && !confirmation_period_valid?
end
- def organization
- gitlab_employee? ? 'GitLab' : super
+ def impersonated?
+ impersonator.present?
end
protected
@@ -1779,13 +1750,6 @@ class User < ApplicationRecord
ApplicationSetting.current_without_cache&.usage_stats_set_by_user_id == self.id
end
- # Added according to https://github.com/plataformatec/devise/blob/7df57d5081f9884849ca15e4fde179ef164a575f/README.md#activejob-integration
- def send_devise_notification(notification, *args)
- return true unless can?(:receive_notifications)
-
- devise_mailer.__send__(notification, self, *args).deliver_later # rubocop:disable GitlabSecurity/PublicSend
- end
-
def ensure_user_rights_and_limits
if external?
self.can_create_group = false
@@ -1834,7 +1798,6 @@ class User < ApplicationRecord
end
def check_email_restrictions
- return unless Feature.enabled?(:email_restrictions)
return unless Gitlab::CurrentSettings.email_restrictions_enabled?
restrictions = Gitlab::CurrentSettings.email_restrictions
diff --git a/app/models/user_type_enums.rb b/app/models/user_type_enums.rb
deleted file mode 100644
index cb5aac89ed3..00000000000
--- a/app/models/user_type_enums.rb
+++ /dev/null
@@ -1,13 +0,0 @@
-# frozen_string_literal: true
-
-module UserTypeEnums
- def self.types
- @types ||= bots.merge(human: nil, ghost: 5)
- end
-
- def self.bots
- @bots ||= { alert_bot: 2, project_bot: 6 }.with_indifferent_access
- end
-end
-
-UserTypeEnums.prepend_if_ee('EE::UserTypeEnums')
diff --git a/app/models/wiki.rb b/app/models/wiki.rb
new file mode 100644
index 00000000000..54bcec32095
--- /dev/null
+++ b/app/models/wiki.rb
@@ -0,0 +1,233 @@
+# frozen_string_literal: true
+
+class Wiki
+ extend ::Gitlab::Utils::Override
+ include HasRepository
+ include Gitlab::Utils::StrongMemoize
+
+ MARKUPS = { # rubocop:disable Style/MultilineIfModifier
+ 'Markdown' => :markdown,
+ 'RDoc' => :rdoc,
+ 'AsciiDoc' => :asciidoc,
+ 'Org' => :org
+ }.freeze unless defined?(MARKUPS)
+
+ CouldNotCreateWikiError = Class.new(StandardError)
+
+ HOMEPAGE = 'home'
+ SIDEBAR = '_sidebar'
+
+ TITLE_ORDER = 'title'
+ CREATED_AT_ORDER = 'created_at'
+ DIRECTION_DESC = 'desc'
+ DIRECTION_ASC = 'asc'
+
+ attr_reader :container, :user
+
+ # Returns a string describing what went wrong after
+ # an operation fails.
+ attr_reader :error_message
+
+ def self.for_container(container, user = nil)
+ "#{container.class.name}Wiki".constantize.new(container, user)
+ end
+
+ def initialize(container, user = nil)
+ @container = container
+ @user = user
+ end
+
+ def path
+ container.path + '.wiki'
+ end
+
+ # Returns the Gitlab::Git::Wiki object.
+ def wiki
+ strong_memoize(:wiki) do
+ create_wiki_repository
+ Gitlab::Git::Wiki.new(repository.raw)
+ end
+ end
+
+ def create_wiki_repository
+ repository.create_if_not_exists
+
+ raise CouldNotCreateWikiError unless repository_exists?
+ rescue => err
+ Gitlab::ErrorTracking.track_exception(err, wiki: {
+ container_type: container.class.name,
+ container_id: container.id,
+ full_path: full_path,
+ disk_path: disk_path
+ })
+
+ raise CouldNotCreateWikiError
+ end
+
+ def has_home_page?
+ !!find_page(HOMEPAGE)
+ end
+
+ def empty?
+ list_pages(limit: 1).empty?
+ end
+
+ def exists?
+ !empty?
+ end
+
+ # Lists wiki pages of the repository.
+ #
+ # limit - max number of pages returned by the method.
+ # sort - criterion by which the pages are sorted.
+ # direction - order of the sorted pages.
+ # load_content - option, which specifies whether the content inside the page
+ # will be loaded.
+ #
+ # Returns an Array of GitLab WikiPage instances or an
+ # empty Array if this Wiki has no pages.
+ def list_pages(limit: 0, sort: nil, direction: DIRECTION_ASC, load_content: false)
+ wiki.list_pages(
+ limit: limit,
+ sort: sort,
+ direction_desc: direction == DIRECTION_DESC,
+ load_content: load_content
+ ).map do |page|
+ WikiPage.new(self, page)
+ end
+ end
+
+ def sidebar_entries(limit: Gitlab::WikiPages::MAX_SIDEBAR_PAGES, **options)
+ pages = list_pages(**options.merge(limit: limit + 1))
+ limited = pages.size > limit
+ pages = pages.first(limit) if limited
+
+ [WikiPage.group_by_directory(pages), limited]
+ end
+
+ # Finds a page within the repository based on a tile
+ # or slug.
+ #
+ # title - The human readable or parameterized title of
+ # the page.
+ #
+ # Returns an initialized WikiPage instance or nil
+ def find_page(title, version = nil)
+ page_title, page_dir = page_title_and_dir(title)
+
+ if page = wiki.page(title: page_title, version: version, dir: page_dir)
+ WikiPage.new(self, page)
+ end
+ end
+
+ def find_sidebar(version = nil)
+ find_page(SIDEBAR, version)
+ end
+
+ def find_file(name, version = nil)
+ wiki.file(name, version)
+ end
+
+ def create_page(title, content, format = :markdown, message = nil)
+ commit = commit_details(:created, message, title)
+
+ wiki.write_page(title, format.to_sym, content, commit)
+
+ update_container_activity
+ rescue Gitlab::Git::Wiki::DuplicatePageError => e
+ @error_message = "Duplicate page: #{e.message}"
+ false
+ end
+
+ def update_page(page, content:, title: nil, format: :markdown, message: nil)
+ commit = commit_details(:updated, message, page.title)
+
+ wiki.update_page(page.path, title || page.name, format.to_sym, content, commit)
+
+ update_container_activity
+ end
+
+ def delete_page(page, message = nil)
+ return unless page
+
+ wiki.delete_page(page.path, commit_details(:deleted, message, page.title))
+
+ update_container_activity
+ end
+
+ def page_title_and_dir(title)
+ return unless title
+
+ title_array = title.split("/")
+ title = title_array.pop
+ [title, title_array.join("/")]
+ end
+
+ def ensure_repository
+ raise CouldNotCreateWikiError unless wiki.repository_exists?
+ end
+
+ def hook_attrs
+ {
+ web_url: web_url,
+ git_ssh_url: ssh_url_to_repo,
+ git_http_url: http_url_to_repo,
+ path_with_namespace: full_path,
+ default_branch: default_branch
+ }
+ end
+
+ override :repository
+ def repository
+ @repository ||= Repository.new(full_path, container, shard: repository_storage, disk_path: disk_path, repo_type: Gitlab::GlRepository::WIKI)
+ end
+
+ def repository_storage
+ raise NotImplementedError
+ end
+
+ def hashed_storage?
+ raise NotImplementedError
+ end
+
+ override :full_path
+ def full_path
+ container.full_path + '.wiki'
+ end
+ alias_method :id, :full_path
+
+ # @deprecated use full_path when you need it for an URL route or disk_path when you want to point to the filesystem
+ alias_method :path_with_namespace, :full_path
+
+ override :default_branch
+ def default_branch
+ wiki.class.default_ref
+ end
+
+ def wiki_base_path
+ Gitlab.config.gitlab.relative_url_root + web_url(only_path: true).sub(%r{/#{Wiki::HOMEPAGE}\z}, '')
+ end
+
+ private
+
+ def commit_details(action, message = nil, title = nil)
+ commit_message = message.presence || default_message(action, title)
+ git_user = Gitlab::Git::User.from_gitlab(user)
+
+ Gitlab::Git::Wiki::CommitDetails.new(user.id,
+ git_user.username,
+ git_user.name,
+ git_user.email,
+ commit_message)
+ end
+
+ def default_message(action, title)
+ "#{user.username} #{action} page: #{title}"
+ end
+
+ def update_container_activity
+ container.after_wiki_activity
+ end
+end
+
+Wiki.prepend_if_ee('EE::Wiki')
diff --git a/app/models/wiki_page.rb b/app/models/wiki_page.rb
index 9c887fc87f3..319cdd38d93 100644
--- a/app/models/wiki_page.rb
+++ b/app/models/wiki_page.rb
@@ -26,7 +26,7 @@ class WikiPage
def eql?(other)
return false unless other.present? && other.is_a?(self.class)
- slug == other.slug && wiki.project == other.wiki.project
+ slug == other.slug && wiki.container == other.wiki.container
end
alias_method :==, :eql?
@@ -66,9 +66,9 @@ class WikiPage
validates :content, presence: true
validate :validate_path_limits, if: :title_changed?
- # The GitLab ProjectWiki instance.
+ # The GitLab Wiki instance.
attr_reader :wiki
- delegate :project, to: :wiki
+ delegate :container, to: :wiki
# The raw Gitlab::Git::WikiPage instance.
attr_reader :page
@@ -83,7 +83,7 @@ class WikiPage
# Construct a new WikiPage
#
- # @param [ProjectWiki] wiki
+ # @param [Wiki] wiki
# @param [Gitlab::Git::WikiPage] page
def initialize(wiki, page = nil)
@wiki = wiki
@@ -95,29 +95,29 @@ class WikiPage
# The escaped URL path of this page.
def slug
- @attributes[:slug].presence || wiki.wiki.preview_slug(title, format)
+ attributes[:slug].presence || wiki.wiki.preview_slug(title, format)
end
alias_method :to_param, :slug
def human_title
- return 'Home' if title == 'home'
+ return 'Home' if title == Wiki::HOMEPAGE
title
end
# The formatted title of this page.
def title
- @attributes[:title] || ''
+ attributes[:title] || ''
end
# Sets the title of this page.
def title=(new_title)
- @attributes[:title] = new_title
+ attributes[:title] = new_title
end
def raw_content
- @attributes[:content] ||= @page&.text_data
+ attributes[:content] ||= page&.text_data
end
# The hierarchy of the directory this page is contained in.
@@ -127,7 +127,7 @@ class WikiPage
# The markup format for the page.
def format
- @attributes[:format] || :markdown
+ attributes[:format] || :markdown
end
# The commit message for this page version.
@@ -151,13 +151,13 @@ class WikiPage
def versions(options = {})
return [] unless persisted?
- wiki.wiki.page_versions(@page.path, options)
+ wiki.wiki.page_versions(page.path, options)
end
def count_versions
return [] unless persisted?
- wiki.wiki.count_page_versions(@page.path)
+ wiki.wiki.count_page_versions(page.path)
end
def last_version
@@ -173,7 +173,7 @@ class WikiPage
def historical?
return false unless last_commit_sha && version
- @page.historical? && last_commit_sha != version.sha
+ page.historical? && last_commit_sha != version.sha
end
# Returns boolean True or False if this instance
@@ -185,7 +185,7 @@ class WikiPage
# Returns boolean True or False if this instance
# has been fully created on disk or not.
def persisted?
- @page.present?
+ page.present?
end
# Creates a new Wiki Page.
@@ -195,7 +195,7 @@ class WikiPage
# :content - The raw markup content.
# :format - Optional symbol representing the
# content format. Can be any type
- # listed in the ProjectWiki::MARKUPS
+ # listed in the Wiki::MARKUPS
# Hash.
# :message - Optional commit message to set on
# the new page.
@@ -215,7 +215,7 @@ class WikiPage
# attrs - Hash of attributes to be updated on the page.
# :content - The raw markup content to replace the existing.
# :format - Optional symbol representing the content format.
- # See ProjectWiki::MARKUPS Hash for available formats.
+ # See Wiki::MARKUPS Hash for available formats.
# :message - Optional commit message to set on the new version.
# :last_commit_sha - Optional last commit sha to validate the page unchanged.
# :title - The Title (optionally including dir) to replace existing title
@@ -232,13 +232,13 @@ class WikiPage
update_attributes(attrs)
if title.present? && title_changed? && wiki.find_page(title).present?
- @attributes[:title] = @page.title
+ attributes[:title] = page.title
raise PageRenameError
end
save do
wiki.update_page(
- @page,
+ page,
content: raw_content,
format: format,
message: attrs[:message],
@@ -251,7 +251,7 @@ class WikiPage
#
# Returns boolean True or False.
def delete
- if wiki.delete_page(@page)
+ if wiki.delete_page(page)
true
else
false
@@ -261,6 +261,7 @@ class WikiPage
# Relative path to the partial to be used when rendering collections
# of this object.
def to_partial_path
+ # TODO: Move into shared/ with https://gitlab.com/gitlab-org/gitlab/-/issues/196054
'projects/wikis/wiki_page'
end
@@ -270,7 +271,7 @@ class WikiPage
def title_changed?
if persisted?
- old_title, old_dir = wiki.page_title_and_dir(self.class.unhyphenize(@page.url_path))
+ old_title, old_dir = wiki.page_title_and_dir(self.class.unhyphenize(page.url_path))
new_title, new_dir = wiki.page_title_and_dir(self.class.unhyphenize(title))
new_title != old_title || (title.include?('/') && new_dir != old_dir)
@@ -287,13 +288,17 @@ class WikiPage
attrs.slice!(:content, :format, :message, :title)
clear_memoization(:parsed_content) if attrs.has_key?(:content)
- @attributes.merge!(attrs)
+ attributes.merge!(attrs)
end
def to_ability_name
'wiki_page'
end
+ def version_commit_timestamp
+ version&.commit&.committed_date
+ end
+
private
def serialize_front_matter(hash)
@@ -303,7 +308,7 @@ class WikiPage
end
def update_front_matter(attrs)
- return unless Gitlab::WikiPages::FrontMatterParser.enabled?(project)
+ return unless Gitlab::WikiPages::FrontMatterParser.enabled?(container)
return unless attrs.has_key?(:front_matter)
fm_yaml = serialize_front_matter(attrs[:front_matter])
@@ -314,7 +319,7 @@ class WikiPage
def parsed_content
strong_memoize(:parsed_content) do
- Gitlab::WikiPages::FrontMatterParser.new(raw_content, project).parse
+ Gitlab::WikiPages::FrontMatterParser.new(raw_content, container).parse
end
end
@@ -325,7 +330,7 @@ class WikiPage
title = deep_title_squish(title)
current_dirname = File.dirname(title)
- if @page.present?
+ if persisted?
return title[1..-1] if current_dirname == '/'
return File.join([directory.presence, title].compact) if current_dirname == '.'
end
@@ -362,9 +367,11 @@ class WikiPage
end
def validate_path_limits
- *dirnames, title = @attributes[:title].split('/')
+ return unless title.present?
+
+ *dirnames, filename = title.split('/')
- if title && title.bytesize > Gitlab::WikiPages::MAX_TITLE_BYTES
+ if filename && filename.bytesize > Gitlab::WikiPages::MAX_TITLE_BYTES
errors.add(:title, _("exceeds the limit of %{bytes} bytes") % {
bytes: Gitlab::WikiPages::MAX_TITLE_BYTES
})
diff --git a/app/models/wiki_page/meta.rb b/app/models/wiki_page/meta.rb
index 2af7d86ebcc..474968122b1 100644
--- a/app/models/wiki_page/meta.rb
+++ b/app/models/wiki_page/meta.rb
@@ -5,6 +5,7 @@ class WikiPage
include Gitlab::Utils::StrongMemoize
CanonicalSlugConflictError = Class.new(ActiveRecord::RecordInvalid)
+ WikiPageInvalid = Class.new(ArgumentError)
self.table_name = 'wiki_page_meta'
@@ -23,46 +24,62 @@ class WikiPage
alias_method :resource_parent, :project
- # Return the (updated) WikiPage::Meta record for a given wiki page
- #
- # If none is found, then a new record is created, and its fields are set
- # to reflect the wiki_page passed.
- #
- # @param [String] last_known_slug
- # @param [WikiPage] wiki_page
- #
- # As with all `find_or_create` methods, this one raises errors on
- # validation issues.
- def self.find_or_create(last_known_slug, wiki_page)
- project = wiki_page.wiki.project
- known_slugs = [last_known_slug, wiki_page.slug].compact.uniq
- raise 'no slugs!' if known_slugs.empty?
-
- transaction do
- found = find_by_canonical_slug(known_slugs, project)
- meta = found || create(title: wiki_page.title, project_id: project.id)
-
- meta.update_state(found.nil?, known_slugs, wiki_page)
-
- # We don't need to run validations here, since find_by_canonical_slug
- # guarantees that there is no conflict in canonical_slug, and DB
- # constraints on title and project_id enforce our other invariants
- # This saves us a query.
- meta
+ class << self
+ # Return the (updated) WikiPage::Meta record for a given wiki page
+ #
+ # If none is found, then a new record is created, and its fields are set
+ # to reflect the wiki_page passed.
+ #
+ # @param [String] last_known_slug
+ # @param [WikiPage] wiki_page
+ #
+ # This method raises errors on validation issues.
+ def find_or_create(last_known_slug, wiki_page)
+ raise WikiPageInvalid unless wiki_page.valid?
+
+ project = wiki_page.wiki.project
+ known_slugs = [last_known_slug, wiki_page.slug].compact.uniq
+ raise 'No slugs found! This should not be possible.' if known_slugs.empty?
+
+ transaction do
+ updates = wiki_page_updates(wiki_page)
+ found = find_by_canonical_slug(known_slugs, project)
+ meta = found || create!(updates.merge(project_id: project.id))
+
+ meta.update_state(found.nil?, known_slugs, wiki_page, updates)
+
+ # We don't need to run validations here, since find_by_canonical_slug
+ # guarantees that there is no conflict in canonical_slug, and DB
+ # constraints on title and project_id enforce our other invariants
+ # This saves us a query.
+ meta
+ end
end
- end
- def self.find_by_canonical_slug(canonical_slug, project)
- meta, conflict = with_canonical_slug(canonical_slug)
- .where(project_id: project.id)
- .limit(2)
+ def find_by_canonical_slug(canonical_slug, project)
+ meta, conflict = with_canonical_slug(canonical_slug)
+ .where(project_id: project.id)
+ .limit(2)
- if conflict.present?
- meta.errors.add(:canonical_slug, 'Duplicate value found')
- raise CanonicalSlugConflictError.new(meta)
+ if conflict.present?
+ meta.errors.add(:canonical_slug, 'Duplicate value found')
+ raise CanonicalSlugConflictError.new(meta)
+ end
+
+ meta
end
- meta
+ private
+
+ def wiki_page_updates(wiki_page)
+ last_commit_date = wiki_page.version_commit_timestamp || Time.now.utc
+
+ {
+ title: wiki_page.title,
+ created_at: last_commit_date,
+ updated_at: last_commit_date
+ }
+ end
end
def canonical_slug
@@ -85,24 +102,21 @@ class WikiPage
@canonical_slug = slug
end
- def update_state(created, known_slugs, wiki_page)
- update_wiki_page_attributes(wiki_page)
+ def update_state(created, known_slugs, wiki_page, updates)
+ update_wiki_page_attributes(updates)
insert_slugs(known_slugs, created, wiki_page.slug)
self.canonical_slug = wiki_page.slug
end
- def update_columns(attrs = {})
- super(attrs.reverse_merge(updated_at: Time.now.utc))
- end
-
- def self.update_all(attrs = {})
- super(attrs.reverse_merge(updated_at: Time.now.utc))
- end
-
private
- def update_wiki_page_attributes(page)
- update_columns(title: page.title) unless page.title == title
+ def update_wiki_page_attributes(updates)
+ # Remove all unnecessary updates:
+ updates.delete(:updated_at) if updated_at == updates[:updated_at]
+ updates.delete(:created_at) if created_at <= updates[:created_at]
+ updates.delete(:title) if title == updates[:title]
+
+ update_columns(updates) unless updates.empty?
end
def insert_slugs(strings, is_new, canonical_slug)
diff --git a/app/models/x509_certificate.rb b/app/models/x509_certificate.rb
index 75b711eab5b..428fd336a32 100644
--- a/app/models/x509_certificate.rb
+++ b/app/models/x509_certificate.rb
@@ -26,6 +26,8 @@ class X509Certificate < ApplicationRecord
validates :x509_issuer_id, presence: true
+ scope :by_x509_issuer, ->(issuer) { where(x509_issuer_id: issuer.id) }
+
after_commit :mark_commit_signatures_unverified
def self.safe_create!(attributes)
@@ -33,6 +35,10 @@ class X509Certificate < ApplicationRecord
.safe_find_or_create_by!(subject_key_identifier: attributes[:subject_key_identifier])
end
+ def self.serial_numbers(issuer)
+ by_x509_issuer(issuer).pluck(:serial_number)
+ end
+
def mark_commit_signatures_unverified
X509CertificateRevokeWorker.perform_async(self.id) if revoked?
end
diff --git a/app/models/x509_commit_signature.rb b/app/models/x509_commit_signature.rb
index ed7c638cecc..57d809f7cfb 100644
--- a/app/models/x509_commit_signature.rb
+++ b/app/models/x509_commit_signature.rb
@@ -41,4 +41,8 @@ class X509CommitSignature < ApplicationRecord
Gitlab::X509::Commit.new(commit)
end
+
+ def user
+ commit.committer
+ end
end
diff --git a/app/policies/alert_management/alert_policy.rb b/app/policies/alert_management/alert_policy.rb
new file mode 100644
index 00000000000..85fafcde2cc
--- /dev/null
+++ b/app/policies/alert_management/alert_policy.rb
@@ -0,0 +1,7 @@
+# frozen_string_literal: true
+
+module AlertManagement
+ class AlertPolicy < ::BasePolicy
+ delegate { @subject.project }
+ end
+end
diff --git a/app/policies/ci/build_policy.rb b/app/policies/ci/build_policy.rb
index ebb99270b9a..12892a69257 100644
--- a/app/policies/ci/build_policy.rb
+++ b/app/policies/ci/build_policy.rb
@@ -12,6 +12,14 @@ module Ci
end
end
+ condition(:unprotected_ref) do
+ if @subject.tag?
+ !ProtectedTag.protected?(@subject.project, @subject.ref)
+ else
+ !ProtectedBranch.protected?(@subject.project, @subject.ref)
+ end
+ end
+
condition(:owner_of_job) do
@subject.triggered_by?(@user)
end
@@ -34,7 +42,7 @@ module Ci
prevent :erase_build
end
- rule { can?(:admin_build) | (can?(:update_build) & owner_of_job) }.enable :erase_build
+ rule { can?(:admin_build) | (can?(:update_build) & owner_of_job & unprotected_ref) }.enable :erase_build
rule { can?(:public_access) & branch_allows_collaboration }.policy do
enable :update_build
diff --git a/app/policies/ci/freeze_period_policy.rb b/app/policies/ci/freeze_period_policy.rb
new file mode 100644
index 00000000000..60e53a7b2f9
--- /dev/null
+++ b/app/policies/ci/freeze_period_policy.rb
@@ -0,0 +1,7 @@
+# frozen_string_literal: true
+
+module Ci
+ class FreezePeriodPolicy < BasePolicy
+ delegate { @subject.resource_parent }
+ end
+end
diff --git a/app/policies/concerns/policy_actor.rb b/app/policies/concerns/policy_actor.rb
index 406677d7b56..f910e04d015 100644
--- a/app/policies/concerns/policy_actor.rb
+++ b/app/policies/concerns/policy_actor.rb
@@ -1,8 +1,15 @@
# frozen_string_literal: true
-# Include this module if we want to pass something else than the user to
-# check policies. This defines several methods which the policy checker
-# would call and check.
+# Include this module to have an object respond to user messages without being
+# a user.
+#
+# Use Case 1:
+# Pass something else than the user to check policies. This defines several
+# methods which the policy checker would call and check.
+#
+# Use Case 2:
+# Access the API with non-user object such as deploy tokens. This defines
+# several methods which the API auth flow would call.
module PolicyActor
extend ActiveSupport::Concern
@@ -37,6 +44,30 @@ module PolicyActor
def alert_bot?
false
end
+
+ def deactivated?
+ false
+ end
+
+ def confirmation_required_on_sign_in?
+ false
+ end
+
+ def can?(action, subject = :global)
+ Ability.allowed?(self, action, subject)
+ end
+
+ def preferred_language
+ nil
+ end
+
+ def requires_ldap_check?
+ false
+ end
+
+ def try_obtain_ldap_lease
+ nil
+ end
end
PolicyActor.prepend_if_ee('EE::PolicyActor')
diff --git a/app/policies/design_management/design_at_version_policy.rb b/app/policies/design_management/design_at_version_policy.rb
new file mode 100644
index 00000000000..9decbc0c4b2
--- /dev/null
+++ b/app/policies/design_management/design_at_version_policy.rb
@@ -0,0 +1,8 @@
+# frozen_string_literal: true
+
+module DesignManagement
+ class DesignAtVersionPolicy < ::BasePolicy
+ delegate { @subject.version }
+ delegate { @subject.design }
+ end
+end
diff --git a/app/policies/design_management/design_collection_policy.rb b/app/policies/design_management/design_collection_policy.rb
new file mode 100644
index 00000000000..6a833da27cc
--- /dev/null
+++ b/app/policies/design_management/design_collection_policy.rb
@@ -0,0 +1,7 @@
+# frozen_string_literal: true
+
+module DesignManagement
+ class DesignCollectionPolicy < DesignPolicy
+ # Delegates everything to the `issue` just like the `DesignPolicy`
+ end
+end
diff --git a/app/policies/design_management/design_policy.rb b/app/policies/design_management/design_policy.rb
new file mode 100644
index 00000000000..57846095f80
--- /dev/null
+++ b/app/policies/design_management/design_policy.rb
@@ -0,0 +1,8 @@
+# frozen_string_literal: true
+
+module DesignManagement
+ class DesignPolicy < ::BasePolicy
+ # The IssuePolicy will delegate to the ProjectPolicy
+ delegate { @subject.issue }
+ end
+end
diff --git a/app/policies/design_management/version_policy.rb b/app/policies/design_management/version_policy.rb
new file mode 100644
index 00000000000..1c59ceaea98
--- /dev/null
+++ b/app/policies/design_management/version_policy.rb
@@ -0,0 +1,8 @@
+# frozen_string_literal: true
+
+module DesignManagement
+ class VersionPolicy < ::BasePolicy
+ # The IssuePolicy will delegate to the ProjectPolicy
+ delegate { @subject.issue }
+ end
+end
diff --git a/app/policies/global_policy.rb b/app/policies/global_policy.rb
index 9353b361c2a..03f5a863421 100644
--- a/app/policies/global_policy.rb
+++ b/app/policies/global_policy.rb
@@ -18,6 +18,7 @@ class GlobalPolicy < BasePolicy
condition(:private_instance_statistics, score: 0) { Gitlab::CurrentSettings.instance_statistics_visibility_private? }
condition(:project_bot, scope: :user) { @user&.project_bot? }
+ condition(:migration_bot, scope: :user) { @user&.migration_bot? }
rule { admin | (~private_instance_statistics & ~anonymous) }
.enable :read_instance_statistics
@@ -48,11 +49,14 @@ class GlobalPolicy < BasePolicy
rule { blocked | internal }.policy do
prevent :log_in
prevent :access_api
- prevent :access_git
prevent :receive_notifications
prevent :use_slash_commands
end
+ rule { blocked | (internal & ~migration_bot) }.policy do
+ prevent :access_git
+ end
+
rule { project_bot }.policy do
prevent :log_in
prevent :receive_notifications
@@ -74,6 +78,10 @@ class GlobalPolicy < BasePolicy
enable :create_group
end
+ rule { can?(:create_group) }.policy do
+ enable :create_group_with_default_branch_protection
+ end
+
rule { can_create_fork }.policy do
enable :create_fork
end
diff --git a/app/policies/group_policy.rb b/app/policies/group_policy.rb
index 728c4b76498..136ac4cce63 100644
--- a/app/policies/group_policy.rb
+++ b/app/policies/group_policy.rb
@@ -1,7 +1,6 @@
# frozen_string_literal: true
class GroupPolicy < BasePolicy
- include CrudPolicyHelpers
include FindGroupProjects
desc "Group is public"
@@ -43,23 +42,15 @@ class GroupPolicy < BasePolicy
@subject.subgroup_creation_level == ::Gitlab::Access::MAINTAINER_SUBGROUP_ACCESS
end
- desc "Group has wiki disabled"
- condition(:wiki_disabled, score: 32) { !feature_available?(:wiki) }
-
rule { public_group }.policy do
enable :read_group
enable :read_package
- enable :read_wiki
end
- rule { logged_in_viewable }.policy do
- enable :read_group
- enable :read_wiki
- end
+ rule { logged_in_viewable }.enable :read_group
rule { guest }.policy do
enable :read_group
- enable :read_wiki
enable :upload_file
end
@@ -87,13 +78,11 @@ class GroupPolicy < BasePolicy
enable :create_metrics_dashboard_annotation
enable :delete_metrics_dashboard_annotation
enable :update_metrics_dashboard_annotation
- enable :create_wiki
end
rule { reporter }.policy do
enable :reporter_access
enable :read_container_image
- enable :download_wiki_code
enable :admin_label
enable :admin_list
enable :admin_issue
@@ -112,7 +101,6 @@ class GroupPolicy < BasePolicy
enable :destroy_deploy_token
enable :read_deploy_token
enable :create_deploy_token
- enable :admin_wiki
end
rule { owner }.policy do
@@ -123,6 +111,7 @@ class GroupPolicy < BasePolicy
enable :set_note_created_at
enable :set_emails_disabled
+ enable :update_default_branch_protection
end
rule { can?(:read_nested_project_resources) }.policy do
@@ -158,11 +147,6 @@ class GroupPolicy < BasePolicy
rule { maintainer & can?(:create_projects) }.enable :transfer_projects
- rule { wiki_disabled }.policy do
- prevent(*create_read_update_admin_destroy(:wiki))
- prevent(:download_wiki_code)
- end
-
def access_level
return GroupMember::NO_ACCESS if @user.nil?
@@ -172,21 +156,6 @@ class GroupPolicy < BasePolicy
def lookup_access_level!
@subject.max_member_access_for_user(@user)
end
-
- # TODO: Extract this into a helper shared with ProjectPolicy, once we implement group-level features.
- # https://gitlab.com/gitlab-org/gitlab/-/issues/208412
- def feature_available?(feature)
- return false unless feature == :wiki
-
- case @subject.wiki_access_level
- when ProjectFeature::DISABLED
- false
- when ProjectFeature::PRIVATE
- admin? || access_level >= ProjectFeature.required_minimum_access_level(feature)
- else
- true
- end
- end
end
GroupPolicy.prepend_if_ee('EE::GroupPolicy')
diff --git a/app/policies/issue_policy.rb b/app/policies/issue_policy.rb
index 20df823c737..28baa0d8338 100644
--- a/app/policies/issue_policy.rb
+++ b/app/policies/issue_policy.rb
@@ -15,6 +15,9 @@ class IssuePolicy < IssuablePolicy
desc "Issue is confidential"
condition(:confidential, scope: :subject) { @subject.confidential? }
+ desc "Issue has moved"
+ condition(:moved) { @subject.moved? }
+
rule { confidential & ~can_read_confidential }.policy do
prevent(*create_read_update_admin_destroy(:issue))
prevent :read_issue_iid
@@ -25,6 +28,15 @@ class IssuePolicy < IssuablePolicy
rule { locked }.policy do
prevent :reopen_issue
end
-end
-IssuePolicy.prepend_if_ee('::EE::IssuePolicy')
+ rule { ~can?(:read_issue) }.policy do
+ prevent :read_design
+ prevent :create_design
+ prevent :destroy_design
+ end
+
+ rule { locked | moved }.policy do
+ prevent :create_design
+ prevent :destroy_design
+ end
+end
diff --git a/app/policies/project_policy.rb b/app/policies/project_policy.rb
index 7454343a357..a24c0471d6c 100644
--- a/app/policies/project_policy.rb
+++ b/app/policies/project_policy.rb
@@ -11,6 +11,7 @@ class ProjectPolicy < BasePolicy
milestone
snippet
wiki
+ design
note
pipeline
pipeline_schedule
@@ -83,11 +84,26 @@ class ProjectPolicy < BasePolicy
project.merge_requests_allowing_push_to_user(user).any?
end
+ desc "Deploy token with read_package_registry scope"
+ condition(:read_package_registry_deploy_token) do
+ user.is_a?(DeployToken) && user.has_access_to?(project) && user.read_package_registry
+ end
+
+ desc "Deploy token with write_package_registry scope"
+ condition(:write_package_registry_deploy_token) do
+ user.is_a?(DeployToken) && user.has_access_to?(project) && user.write_package_registry
+ end
+
with_scope :subject
condition(:forking_allowed) do
@subject.feature_available?(:forking, @user)
end
+ with_scope :subject
+ condition(:metrics_dashboard_allowed) do
+ feature_available?(:metrics_dashboard)
+ end
+
with_scope :global
condition(:mirror_available, score: 0) do
::Gitlab::CurrentSettings.current_application_settings.mirror_available
@@ -102,6 +118,11 @@ class ProjectPolicy < BasePolicy
)
end
+ with_scope :subject
+ condition(:design_management_disabled) do
+ !@subject.design_management_enabled?
+ end
+
# We aren't checking `:read_issue` or `:read_merge_request` in this case
# because it could be possible for a user to see an issuable-iid
# (`:read_issue_iid` or `:read_merge_request_iid`) but then wouldn't be
@@ -134,6 +155,7 @@ class ProjectPolicy < BasePolicy
wiki
builds
pages
+ metrics_dashboard
]
features.each do |f|
@@ -174,6 +196,7 @@ class ProjectPolicy < BasePolicy
enable :set_issue_updated_at
enable :set_note_created_at
enable :set_emails_disabled
+ enable :set_show_default_award_emojis
end
rule { can?(:guest_access) }.policy do
@@ -218,6 +241,7 @@ class ProjectPolicy < BasePolicy
enable :read_build
enable :read_container_image
enable :read_pipeline
+ enable :read_pipeline_schedule
enable :read_environment
enable :read_deployment
enable :read_merge_request
@@ -225,6 +249,7 @@ class ProjectPolicy < BasePolicy
enable :update_sentry_issue
enable :read_prometheus
enable :read_metrics_dashboard_annotation
+ enable :metrics_dashboard
end
# We define `:public_user_access` separately because there are cases in gitlab-ee
@@ -247,6 +272,21 @@ class ProjectPolicy < BasePolicy
enable :fork_project
end
+ rule { metrics_dashboard_disabled }.policy do
+ prevent(:metrics_dashboard)
+ end
+
+ rule { can?(:metrics_dashboard) }.policy do
+ enable :read_prometheus
+ enable :read_environment
+ enable :read_deployment
+ end
+
+ rule { ~anonymous & can?(:metrics_dashboard) }.policy do
+ enable :create_metrics_user_starred_dashboard
+ enable :read_metrics_user_starred_dashboard
+ end
+
rule { owner | admin | guest | group_member }.prevent :request_access
rule { ~request_access_enabled }.prevent :request_access
@@ -262,7 +302,6 @@ class ProjectPolicy < BasePolicy
enable :update_commit_status
enable :create_build
enable :update_build
- enable :read_pipeline_schedule
enable :create_merge_request_from
enable :create_wiki
enable :push_code
@@ -277,9 +316,14 @@ class ProjectPolicy < BasePolicy
enable :update_deployment
enable :create_release
enable :update_release
+ enable :daily_statistics
enable :create_metrics_dashboard_annotation
enable :delete_metrics_dashboard_annotation
enable :update_metrics_dashboard_annotation
+ enable :read_alert_management_alert
+ enable :update_alert_management_alert
+ enable :create_design
+ enable :destroy_design
end
rule { can?(:developer_access) & user_confirmed? }.policy do
@@ -315,7 +359,6 @@ class ProjectPolicy < BasePolicy
enable :create_environment_terminal
enable :destroy_release
enable :destroy_artifacts
- enable :daily_statistics
enable :admin_operations
enable :read_deploy_token
enable :create_deploy_token
@@ -323,6 +366,18 @@ class ProjectPolicy < BasePolicy
enable :destroy_deploy_token
enable :read_prometheus_alerts
enable :admin_terraform_state
+ enable :create_freeze_period
+ enable :read_freeze_period
+ enable :update_freeze_period
+ enable :destroy_freeze_period
+ end
+
+ rule { public_project & metrics_dashboard_allowed }.policy do
+ enable :metrics_dashboard
+ end
+
+ rule { internal_access & metrics_dashboard_allowed }.policy do
+ enable :metrics_dashboard
end
rule { (mirror_available & can?(:admin_project)) | admin }.enable :admin_remote_mirror
@@ -374,11 +429,27 @@ class ProjectPolicy < BasePolicy
rule { builds_disabled | repository_disabled }.policy do
prevent(*create_read_update_admin_destroy(:build))
prevent(*create_read_update_admin_destroy(:pipeline_schedule))
- prevent(*create_read_update_admin_destroy(:environment))
prevent(*create_read_update_admin_destroy(:cluster))
prevent(*create_read_update_admin_destroy(:deployment))
end
+ # Enabling `read_environment` specifically for the condition of `metrics_dashboard_allowed` is
+ # necessary due to the route for metrics dashboard requiring an environment id.
+ # This will be addressed in https://gitlab.com/gitlab-org/gitlab/-/issues/213833 when
+ # environments and metrics are decoupled and these rules will be removed.
+
+ rule { (builds_disabled | repository_disabled) & ~metrics_dashboard_allowed}.policy do
+ prevent(*create_read_update_admin_destroy(:environment))
+ end
+
+ rule { (builds_disabled | repository_disabled) & metrics_dashboard_allowed}.policy do
+ prevent :create_environment
+ prevent :update_environment
+ prevent :admin_environment
+ prevent :destroy_environment
+ enable :read_environment
+ end
+
# There's two separate cases when builds_disabled is true:
# 1. When internal CI is disabled - builds_disabled && internal_builds_disabled
# - We do not prevent the user from accessing Pipelines to allow them to access external CI
@@ -395,6 +466,7 @@ class ProjectPolicy < BasePolicy
prevent :fork_project
prevent :read_commit_status
prevent :read_pipeline
+ prevent :read_pipeline_schedule
prevent(*create_read_update_admin_destroy(:release))
end
@@ -421,6 +493,7 @@ class ProjectPolicy < BasePolicy
enable :read_merge_request
enable :read_note
enable :read_pipeline
+ enable :read_pipeline_schedule
enable :read_commit_status
enable :read_container_image
enable :download_code
@@ -439,6 +512,7 @@ class ProjectPolicy < BasePolicy
rule { public_builds & can?(:guest_access) }.policy do
enable :read_pipeline
+ enable :read_pipeline_schedule
end
# These rules are included to allow maintainers of projects to push to certain
@@ -481,6 +555,27 @@ class ProjectPolicy < BasePolicy
rule { admin }.enable :change_repository_storage
+ rule { can?(:read_issue) }.policy do
+ enable :read_design
+ end
+
+ # Design abilities could also be prevented in the issue policy.
+ rule { design_management_disabled }.policy do
+ prevent :read_design
+ prevent :create_design
+ prevent :destroy_design
+ end
+
+ rule { read_package_registry_deploy_token }.policy do
+ enable :read_package
+ enable :read_project
+ end
+
+ rule { write_package_registry_deploy_token }.policy do
+ enable :create_package
+ enable :read_project
+ end
+
private
def team_member?
diff --git a/app/policies/wiki_page_policy.rb b/app/policies/wiki_page_policy.rb
index 468632c9085..f284fd9f5df 100644
--- a/app/policies/wiki_page_policy.rb
+++ b/app/policies/wiki_page_policy.rb
@@ -1,7 +1,7 @@
# frozen_string_literal: true
class WikiPagePolicy < BasePolicy
- delegate { @subject.wiki.project }
+ delegate { @subject.wiki.container }
rule { can?(:read_wiki) }.enable :read_wiki_page
end
diff --git a/app/presenters/README.md b/app/presenters/README.md
index dc4173a880e..62aec4fc8a2 100644
--- a/app/presenters/README.md
+++ b/app/presenters/README.md
@@ -8,7 +8,7 @@ methods from models to presenters.
### When your view is full of logic
-When your view is full of logic (`if`, `else`, `select` on arrays etc.), it's
+When your view is full of logic (`if`, `else`, `select` on arrays, etc.), it's
time to create a presenter!
### When your model has a lot of view-related logic/data methods
@@ -27,11 +27,11 @@ Presenters should be used for:
https://gitlab.com/gitlab-org/gitlab-foss/merge_requests/7073/diffs.
- Data and logic methods that can be pulled from models.
- Simple text output methods: it's ok if the method returns a string, but not a
- whole DOM element for which we'd need HAML, a view context, helpers etc.
+ whole DOM element for which we'd need HAML, a view context, helpers, etc.
## Why use presenters instead of model concerns?
-We should strive to follow the single-responsibility principle, and view-related
+We should strive to follow the single-responsibility principle and view-related
logic/data methods are definitely not the responsibility of models!
Another reason is as follows:
@@ -52,22 +52,22 @@ we gain the following benefits:
- rules are more explicit and centralized in the presenter => improves security
- testing is easier and faster as presenters are Plain Old Ruby Object (PORO)
- views are more readable and maintainable
-- decreases number of CE -> EE merge conflicts since code is in separate files
+- decreases the number of CE -> EE merge conflicts since code is in separate files
- moves the conflicts from views (not always obvious) to presenters (a lot easier to resolve)
## What not to do with presenters?
- Don't use helpers in presenters. Presenters are not aware of the view context.
-- Don't generate complex DOM elements, forms etc. with presenters. Presenters
- can return simple data as texts, and URLs using URL helpers from
- `Gitlab::Routing` but nothing much more fancy.
+- Don't generate complex DOM elements, forms, etc. with presenters. Presenters
+ can return simple data like texts, and URLs using URL helpers from
+ `Gitlab::Routing` but nothing much fancier.
## Implementation
### Presenter definition
Every presenter should inherit from `Gitlab::View::Presenter::Simple`, which
-provides a `.presents` method which allows you to define an accessor for the
+provides a `.presents` the method which allows you to define an accessor for the
presented object. It also includes common helpers like `Gitlab::Routing` and
`Gitlab::Allowable`.
diff --git a/app/presenters/ci/build_runner_presenter.rb b/app/presenters/ci/build_runner_presenter.rb
index 33b7899f912..5e35bfc79ef 100644
--- a/app/presenters/ci/build_runner_presenter.rb
+++ b/app/presenters/ci/build_runner_presenter.rb
@@ -27,14 +27,13 @@ module Ci
def git_depth
if git_depth_variable
git_depth_variable[:value]
- elsif Feature.enabled?(:ci_project_git_depth, default_enabled: true)
+ else
project.ci_default_git_depth
end.to_i
end
def refspecs
specs = []
- specs << refspec_for_pipeline_ref if should_expose_merge_request_ref?
specs << refspec_for_persistent_ref if persistent_ref_exist?
if git_depth > 0
@@ -50,23 +49,10 @@ module Ci
private
- # We will stop exposing merge request refs when we fully depend on persistent refs
- # (i.e. remove `refspec_for_pipeline_ref` when we remove `depend_on_persistent_pipeline_ref` feature flag.)
- # `ci_force_exposing_merge_request_refs` is an extra feature flag that allows us to
- # forcibly expose MR refs even if the `depend_on_persistent_pipeline_ref` feature flag enabled.
- # This is useful when we see an unexpected behaviors/reports from users.
- # See https://gitlab.com/gitlab-org/gitlab/issues/35140.
- def should_expose_merge_request_ref?
- return false unless merge_request_ref?
- return true if Feature.enabled?(:ci_force_exposing_merge_request_refs, project)
-
- Feature.disabled?(:depend_on_persistent_pipeline_ref, project, default_enabled: true)
- end
-
def create_archive(artifacts)
return unless artifacts[:untracked] || artifacts[:paths]
- {
+ archive = {
artifact_type: :archive,
artifact_format: :zip,
name: artifacts[:name],
@@ -75,6 +61,12 @@ module Ci
when: artifacts[:when],
expire_in: artifacts[:expire_in]
}
+
+ if artifacts.dig(:exclude).present? && ::Gitlab::Ci::Features.artifacts_exclude_enabled?
+ archive.merge(exclude: artifacts[:exclude])
+ else
+ archive
+ end
end
def create_reports(reports, expire_in:)
@@ -100,15 +92,18 @@ module Ci
"+#{Gitlab::Git::TAG_REF_PREFIX}#{ref}:#{RUNNER_REMOTE_TAG_PREFIX}#{ref}"
end
- def refspec_for_pipeline_ref
- "+#{ref}:#{ref}"
- end
-
def refspec_for_persistent_ref
"+#{persistent_ref_path}:#{persistent_ref_path}"
end
def persistent_ref_exist?
+ ##
+ # Persistent refs for pipelines definitely exist from GitLab 12.4,
+ # hence, we don't need to check the ref existence before passing it to runners.
+ # Checking refs pressurizes gitaly node and should be avoided.
+ # Issue: https://gitlab.com/gitlab-com/gl-infra/production/-/issues/2143
+ return true if Feature.enabled?(:ci_skip_persistent_ref_existence_check)
+
pipeline.persistent_ref.exist?
end
diff --git a/app/presenters/clusterable_presenter.rb b/app/presenters/clusterable_presenter.rb
index 6b1d82e7557..5e669ff2e50 100644
--- a/app/presenters/clusterable_presenter.rb
+++ b/app/presenters/clusterable_presenter.rb
@@ -21,8 +21,8 @@ class ClusterablePresenter < Gitlab::View::Presenter::Delegated
can?(current_user, :create_cluster, clusterable)
end
- def index_path
- polymorphic_path([clusterable, :clusters])
+ def index_path(options = {})
+ polymorphic_path([clusterable, :clusters], options)
end
def new_path(options = {})
diff --git a/app/presenters/commit_status_presenter.rb b/app/presenters/commit_status_presenter.rb
index 23e688e562e..52811e152a6 100644
--- a/app/presenters/commit_status_presenter.rb
+++ b/app/presenters/commit_status_presenter.rb
@@ -33,14 +33,6 @@ class CommitStatusPresenter < Gitlab::View::Presenter::Delegated
def callout_failure_message
self.class.callout_failure_messages.fetch(failure_reason.to_sym)
end
-
- def recoverable?
- failed? && !unrecoverable?
- end
-
- def unrecoverable?
- script_failure? || missing_dependency_failure? || archived_failure? || scheduler_failure? || data_integrity_failure?
- end
end
CommitStatusPresenter.prepend_if_ee('::EE::CommitStatusPresenter')
diff --git a/app/presenters/instance_clusterable_presenter.rb b/app/presenters/instance_clusterable_presenter.rb
index 0c267fd5735..41071bc7bc7 100644
--- a/app/presenters/instance_clusterable_presenter.rb
+++ b/app/presenters/instance_clusterable_presenter.rb
@@ -13,8 +13,8 @@ class InstanceClusterablePresenter < ClusterablePresenter
end
override :index_path
- def index_path
- admin_clusters_path
+ def index_path(options = {})
+ admin_clusters_path(options)
end
override :new_path
diff --git a/app/presenters/pages_domain_presenter.rb b/app/presenters/pages_domain_presenter.rb
index 6b74983d932..6ef89760bec 100644
--- a/app/presenters/pages_domain_presenter.rb
+++ b/app/presenters/pages_domain_presenter.rb
@@ -8,8 +8,6 @@ class PagesDomainPresenter < Gitlab::View::Presenter::Delegated
end
def show_auto_ssl_failed_warning?
- return false unless Feature.enabled?(:pages_letsencrypt_errors, pages_domain.project)
-
# validations prevents auto ssl from working, so there is no need to show that warning until
return false if needs_verification?
diff --git a/app/presenters/projects/prometheus/alert_presenter.rb b/app/presenters/projects/prometheus/alert_presenter.rb
index c03925c0871..2114e06a8c5 100644
--- a/app/presenters/projects/prometheus/alert_presenter.rb
+++ b/app/presenters/projects/prometheus/alert_presenter.rb
@@ -7,6 +7,7 @@ module Projects
GENERIC_ALERT_SUMMARY_ANNOTATIONS = %w(monitoring_tool service hosts).freeze
MARKDOWN_LINE_BREAK = " \n".freeze
INCIDENT_LABEL_NAME = IncidentManagement::CreateIssueService::INCIDENT_LABEL[:title].freeze
+ METRIC_TIME_WINDOW = 30.minutes
def full_title
[environment_name, alert_title].compact.join(': ')
@@ -119,9 +120,63 @@ module Projects
Array(hosts.value).join(' ')
end
- def metric_embed_for_alert; end
+ def metric_embed_for_alert
+ url = embed_url_for_gitlab_alert || embed_url_for_self_managed_alert
+
+ "\n[](#{url})" if url
+ end
+
+ def embed_url_for_gitlab_alert
+ return unless gitlab_alert
+
+ metrics_dashboard_project_prometheus_alert_url(
+ project,
+ gitlab_alert.prometheus_metric_id,
+ environment_id: environment.id,
+ **alert_embed_window_params(embed_time)
+ )
+ end
+
+ def embed_url_for_self_managed_alert
+ return unless environment && full_query && title
+
+ metrics_dashboard_project_environment_url(
+ project,
+ environment,
+ embed_json: dashboard_for_self_managed_alert.to_json,
+ **alert_embed_window_params(embed_time)
+ )
+ end
+
+ def embed_time
+ starts_at ? Time.rfc3339(starts_at) : Time.current
+ end
+
+ def alert_embed_window_params(time)
+ {
+ start: format_embed_timestamp(time - METRIC_TIME_WINDOW),
+ end: format_embed_timestamp(time + METRIC_TIME_WINDOW)
+ }
+ end
+
+ def format_embed_timestamp(time)
+ time.utc.strftime('%FT%TZ')
+ end
+
+ def dashboard_for_self_managed_alert
+ {
+ panel_groups: [{
+ panels: [{
+ type: 'line-graph',
+ title: title,
+ y_label: y_label,
+ metrics: [{
+ query_range: full_query
+ }]
+ }]
+ }]
+ }
+ end
end
end
end
-
-Projects::Prometheus::AlertPresenter.prepend_if_ee('EE::Projects::Prometheus::AlertPresenter')
diff --git a/app/presenters/projects/settings/deploy_keys_presenter.rb b/app/presenters/projects/settings/deploy_keys_presenter.rb
index 66211d02696..103c26289bf 100644
--- a/app/presenters/projects/settings/deploy_keys_presenter.rb
+++ b/app/presenters/projects/settings/deploy_keys_presenter.rb
@@ -60,11 +60,11 @@ module Projects
end
def to_partial_path
- 'projects/deploy_keys/index'
+ '../../shared/deploy_keys/index'
end
def form_partial_path
- 'projects/deploy_keys/form'
+ 'shared/deploy_keys/project_group_form'
end
private
diff --git a/app/presenters/release_presenter.rb b/app/presenters/release_presenter.rb
index 3db89df1cc8..ea46f0a234b 100644
--- a/app/presenters/release_presenter.rb
+++ b/app/presenters/release_presenter.rb
@@ -43,13 +43,6 @@ class ReleasePresenter < Gitlab::View::Presenter::Delegated
edit_project_release_url(project, release)
end
- def evidence_file_path
- evidence = release.evidences.first
- return unless evidence
-
- project_evidence_url(project, release, evidence, format: :json)
- end
-
private
def can_download_code?
diff --git a/app/presenters/snippet_presenter.rb b/app/presenters/snippet_presenter.rb
index ba0b2b42383..faaf7568c72 100644
--- a/app/presenters/snippet_presenter.rb
+++ b/app/presenters/snippet_presenter.rb
@@ -12,11 +12,11 @@ class SnippetPresenter < Gitlab::View::Presenter::Delegated
end
def ssh_url_to_repo
- snippet.ssh_url_to_repo if snippet.versioned_enabled_for?(current_user)
+ snippet.ssh_url_to_repo if snippet.repository_exists?
end
def http_url_to_repo
- snippet.http_url_to_repo if snippet.versioned_enabled_for?(current_user)
+ snippet.http_url_to_repo if snippet.repository_exists?
end
def can_read_snippet?
@@ -36,10 +36,10 @@ class SnippetPresenter < Gitlab::View::Presenter::Delegated
end
def blob
- if Feature.enabled?(:version_snippets, current_user) && !snippet.repository.empty?
- snippet.blobs.first
- else
+ if snippet.empty_repo?
snippet.blob
+ else
+ snippet.blobs.first
end
end
diff --git a/app/serializers/accessibility_error_entity.rb b/app/serializers/accessibility_error_entity.rb
new file mode 100644
index 00000000000..540f5384d66
--- /dev/null
+++ b/app/serializers/accessibility_error_entity.rb
@@ -0,0 +1,12 @@
+# frozen_string_literal: true
+
+class AccessibilityErrorEntity < Grape::Entity
+ expose :code
+ expose :type
+ expose :typeCode, as: :type_code
+ expose :message
+ expose :context
+ expose :selector
+ expose :runner
+ expose :runnerExtras, as: :runner_extras
+end
diff --git a/app/serializers/accessibility_reports_comparer_entity.rb b/app/serializers/accessibility_reports_comparer_entity.rb
new file mode 100644
index 00000000000..3768607a3fc
--- /dev/null
+++ b/app/serializers/accessibility_reports_comparer_entity.rb
@@ -0,0 +1,15 @@
+# frozen_string_literal: true
+
+class AccessibilityReportsComparerEntity < Grape::Entity
+ expose :status
+
+ expose :new_errors, using: AccessibilityErrorEntity
+ expose :resolved_errors, using: AccessibilityErrorEntity
+ expose :existing_errors, using: AccessibilityErrorEntity
+
+ expose :summary do
+ expose :total_count, as: :total
+ expose :resolved_count, as: :resolved
+ expose :errors_count, as: :errored
+ end
+end
diff --git a/app/serializers/accessibility_reports_comparer_serializer.rb b/app/serializers/accessibility_reports_comparer_serializer.rb
new file mode 100644
index 00000000000..a6b8162e4ea
--- /dev/null
+++ b/app/serializers/accessibility_reports_comparer_serializer.rb
@@ -0,0 +1,5 @@
+# frozen_string_literal: true
+
+class AccessibilityReportsComparerSerializer < BaseSerializer
+ entity AccessibilityReportsComparerEntity
+end
diff --git a/app/serializers/analytics_summary_entity.rb b/app/serializers/analytics_summary_entity.rb
index 57e9225e2da..62828fc1428 100644
--- a/app/serializers/analytics_summary_entity.rb
+++ b/app/serializers/analytics_summary_entity.rb
@@ -8,8 +8,6 @@ class AnalyticsSummaryEntity < Grape::Entity
private
def value
- return object.value if object.value.is_a? String
-
- object.value&.nonzero? ? object.value.to_s : '-'
+ object.value.to_s
end
end
diff --git a/app/serializers/ci/basic_variable_entity.rb b/app/serializers/ci/basic_variable_entity.rb
new file mode 100644
index 00000000000..dad59e8735b
--- /dev/null
+++ b/app/serializers/ci/basic_variable_entity.rb
@@ -0,0 +1,13 @@
+# frozen_string_literal: true
+
+module Ci
+ class BasicVariableEntity < Grape::Entity
+ expose :id
+ expose :key
+ expose :value
+ expose :variable_type
+
+ expose :protected?, as: :protected
+ expose :masked?, as: :masked
+ end
+end
diff --git a/app/serializers/ci/dag_job_entity.rb b/app/serializers/ci/dag_job_entity.rb
new file mode 100644
index 00000000000..b4947319ed1
--- /dev/null
+++ b/app/serializers/ci/dag_job_entity.rb
@@ -0,0 +1,11 @@
+# frozen_string_literal: true
+
+module Ci
+ class DagJobEntity < Grape::Entity
+ expose :name
+
+ expose :needs, if: -> (job, _) { job.scheduling_type_dag? } do |job|
+ job.needs.pluck(:name) # rubocop: disable CodeReuse/ActiveRecord
+ end
+ end
+end
diff --git a/app/serializers/ci/dag_job_group_entity.rb b/app/serializers/ci/dag_job_group_entity.rb
new file mode 100644
index 00000000000..ac1ed89281c
--- /dev/null
+++ b/app/serializers/ci/dag_job_group_entity.rb
@@ -0,0 +1,9 @@
+# frozen_string_literal: true
+
+module Ci
+ class DagJobGroupEntity < Grape::Entity
+ expose :name
+ expose :size
+ expose :jobs, with: Ci::DagJobEntity
+ end
+end
diff --git a/app/serializers/ci/dag_pipeline_entity.rb b/app/serializers/ci/dag_pipeline_entity.rb
new file mode 100644
index 00000000000..b615dd2b194
--- /dev/null
+++ b/app/serializers/ci/dag_pipeline_entity.rb
@@ -0,0 +1,20 @@
+# frozen_string_literal: true
+
+module Ci
+ class DagPipelineEntity < Grape::Entity
+ expose :ordered_stages_with_preloads, as: :stages, using: Ci::DagStageEntity
+
+ private
+
+ def ordered_stages_with_preloads
+ object.ordered_stages.preload(preloaded_relations) # rubocop: disable CodeReuse/ActiveRecord
+ end
+
+ def preloaded_relations
+ [
+ :project,
+ { latest_statuses: :needs }
+ ]
+ end
+ end
+end
diff --git a/app/serializers/ci/dag_pipeline_serializer.rb b/app/serializers/ci/dag_pipeline_serializer.rb
new file mode 100644
index 00000000000..0c9e9a9db69
--- /dev/null
+++ b/app/serializers/ci/dag_pipeline_serializer.rb
@@ -0,0 +1,7 @@
+# frozen_string_literal: true
+
+module Ci
+ class DagPipelineSerializer < BaseSerializer
+ entity Ci::DagPipelineEntity
+ end
+end
diff --git a/app/serializers/ci/dag_stage_entity.rb b/app/serializers/ci/dag_stage_entity.rb
new file mode 100644
index 00000000000..c7969da6c3c
--- /dev/null
+++ b/app/serializers/ci/dag_stage_entity.rb
@@ -0,0 +1,9 @@
+# frozen_string_literal: true
+
+module Ci
+ class DagStageEntity < Grape::Entity
+ expose :name
+
+ expose :groups, with: Ci::DagJobGroupEntity
+ end
+end
diff --git a/app/serializers/ci/instance_variable_serializer.rb b/app/serializers/ci/instance_variable_serializer.rb
new file mode 100644
index 00000000000..b0b49aecdbd
--- /dev/null
+++ b/app/serializers/ci/instance_variable_serializer.rb
@@ -0,0 +1,7 @@
+# frozen_string_literal: true
+
+module Ci
+ class InstanceVariableSerializer < BaseSerializer
+ entity BasicVariableEntity
+ end
+end
diff --git a/app/serializers/cluster_application_entity.rb b/app/serializers/cluster_application_entity.rb
index 85a40f1f5cb..32b759b9628 100644
--- a/app/serializers/cluster_application_entity.rb
+++ b/app/serializers/cluster_application_entity.rb
@@ -19,4 +19,6 @@ class ClusterApplicationEntity < Grape::Entity
expose :host, if: -> (e, _) { e.respond_to?(:host) }
expose :port, if: -> (e, _) { e.respond_to?(:port) }
expose :protocol, if: -> (e, _) { e.respond_to?(:protocol) }
+ expose :waf_log_enabled, if: -> (e, _) { e.respond_to?(:waf_log_enabled) }
+ expose :cilium_log_enabled, if: -> (e, _) { e.respond_to?(:cilium_log_enabled) }
end
diff --git a/app/serializers/cluster_entity.rb b/app/serializers/cluster_entity.rb
index c59f68bbc49..4f53ea30544 100644
--- a/app/serializers/cluster_entity.rb
+++ b/app/serializers/cluster_entity.rb
@@ -3,7 +3,16 @@
class ClusterEntity < Grape::Entity
include RequestAwareEntity
+ expose :cluster_type
+ expose :enabled
+ expose :environment_scope
+ expose :name
+ expose :nodes
expose :status_name, as: :status
expose :status_reason
expose :applications, using: ClusterApplicationEntity
+
+ expose :path do |cluster|
+ Clusters::ClusterPresenter.new(cluster).show_path # rubocop: disable CodeReuse/Presenter
+ end
end
diff --git a/app/serializers/cluster_serializer.rb b/app/serializers/cluster_serializer.rb
index 4bb4d4880d4..f59b6a35a29 100644
--- a/app/serializers/cluster_serializer.rb
+++ b/app/serializers/cluster_serializer.rb
@@ -1,8 +1,23 @@
# frozen_string_literal: true
class ClusterSerializer < BaseSerializer
+ include WithPagination
entity ClusterEntity
+ def represent_list(resource)
+ represent(resource, {
+ only: [
+ :cluster_type,
+ :enabled,
+ :environment_scope,
+ :name,
+ :nodes,
+ :path,
+ :status
+ ]
+ })
+ end
+
def represent_status(resource)
represent(resource, { only: [:status, :status_reason, :applications] })
end
diff --git a/app/serializers/diff_file_base_entity.rb b/app/serializers/diff_file_base_entity.rb
index 302fe3d7c67..8c2b3a65d57 100644
--- a/app/serializers/diff_file_base_entity.rb
+++ b/app/serializers/diff_file_base_entity.rb
@@ -22,16 +22,16 @@ class DiffFileBaseEntity < Grape::Entity
expose :edit_path, if: -> (_, options) { options[:merge_request] } do |diff_file|
merge_request = options[:merge_request]
- options = merge_request.persisted? ? { from_merge_request_iid: merge_request.iid } : {}
+ next unless merge_request.merged? || merge_request.source_branch_exists?
- next unless merge_request.source_project
+ target_project, target_branch = edit_project_branch_options(merge_request)
if Feature.enabled?(:web_ide_default)
- ide_edit_path(merge_request.source_project, merge_request.source_branch, diff_file.new_path)
+ ide_edit_path(target_project, target_branch, diff_file.new_path)
else
- project_edit_blob_path(merge_request.source_project,
- tree_join(merge_request.source_branch, diff_file.new_path),
- options)
+ options = merge_request.persisted? && merge_request.source_branch_exists? && !merge_request.merged? ? { from_merge_request_iid: merge_request.iid } : {}
+
+ project_edit_blob_path(target_project, tree_join(target_branch, diff_file.new_path), options)
end
end
@@ -61,7 +61,7 @@ class DiffFileBaseEntity < Grape::Entity
next unless diff_file.blob
if merge_request&.source_project && current_user
- can_modify_blob?(diff_file.blob, merge_request.source_project, merge_request.source_branch)
+ can_modify_blob?(diff_file.blob, merge_request.source_project, merge_request.source_branch_exists? ? merge_request.source_branch : merge_request.target_branch)
else
false
end
@@ -88,6 +88,7 @@ class DiffFileBaseEntity < Grape::Entity
expose :b_mode
expose :viewer, using: DiffViewerEntity
+ expose :alternate_viewer, using: DiffViewerEntity
expose :old_size do |diff_file|
diff_file.old_blob&.raw_size
@@ -112,4 +113,12 @@ class DiffFileBaseEntity < Grape::Entity
def current_user
request.current_user
end
+
+ def edit_project_branch_options(merge_request)
+ if merge_request.source_branch_exists? && !merge_request.merged?
+ [merge_request.source_project, merge_request.source_branch]
+ else
+ [merge_request.target_project, merge_request.target_branch]
+ end
+ end
end
diff --git a/app/serializers/diffs_entity.rb b/app/serializers/diffs_entity.rb
index 568d0f6aa8f..fb4fbe57130 100644
--- a/app/serializers/diffs_entity.rb
+++ b/app/serializers/diffs_entity.rb
@@ -11,6 +11,10 @@ class DiffsEntity < Grape::Entity
merge_request&.source_branch
end
+ expose :source_branch_exists do |diffs|
+ merge_request&.source_branch_exists?
+ end
+
expose :target_branch_name do |diffs|
merge_request&.target_branch
end
diff --git a/app/serializers/group_variable_entity.rb b/app/serializers/group_variable_entity.rb
index 622106458c3..4f44723fefe 100644
--- a/app/serializers/group_variable_entity.rb
+++ b/app/serializers/group_variable_entity.rb
@@ -1,11 +1,4 @@
# frozen_string_literal: true
-class GroupVariableEntity < Grape::Entity
- expose :id
- expose :key
- expose :value
- expose :variable_type
-
- expose :protected?, as: :protected
- expose :masked?, as: :masked
+class GroupVariableEntity < Ci::BasicVariableEntity
end
diff --git a/app/serializers/issuable_sidebar_basic_entity.rb b/app/serializers/issuable_sidebar_basic_entity.rb
index 498cfe5930d..bbec107544e 100644
--- a/app/serializers/issuable_sidebar_basic_entity.rb
+++ b/app/serializers/issuable_sidebar_basic_entity.rb
@@ -21,7 +21,7 @@ class IssuableSidebarBasicEntity < Grape::Entity
expose :labels, using: LabelEntity
expose :current_user, if: lambda { |_issuable| current_user } do
- expose :current_user, merge: true, using: API::Entities::UserBasic
+ expose :current_user, merge: true, using: ::API::Entities::UserBasic
expose :todo, using: IssuableSidebarTodoEntity do |issuable|
current_user.pending_todo_for(issuable)
diff --git a/app/serializers/issuable_sidebar_extras_entity.rb b/app/serializers/issuable_sidebar_extras_entity.rb
index 0e1fcc58d7a..77f2e34fa5d 100644
--- a/app/serializers/issuable_sidebar_extras_entity.rb
+++ b/app/serializers/issuable_sidebar_extras_entity.rb
@@ -21,5 +21,5 @@ class IssuableSidebarExtrasEntity < Grape::Entity
issuable.subscribed?(request.current_user, issuable.project)
end
- expose :assignees, using: API::Entities::UserBasic
+ expose :assignees, using: ::API::Entities::UserBasic
end
diff --git a/app/serializers/merge_request_assignee_entity.rb b/app/serializers/merge_request_assignee_entity.rb
index 6849c62e759..b7ef7449270 100644
--- a/app/serializers/merge_request_assignee_entity.rb
+++ b/app/serializers/merge_request_assignee_entity.rb
@@ -5,3 +5,5 @@ class MergeRequestAssigneeEntity < ::API::Entities::UserBasic
options[:merge_request]&.can_be_merged_by?(assignee)
end
end
+
+MergeRequestAssigneeEntity.prepend_if_ee('EE::MergeRequestAssigneeEntity')
diff --git a/app/serializers/merge_request_poll_widget_entity.rb b/app/serializers/merge_request_poll_widget_entity.rb
index 18e8ec0e7d1..aad607f358a 100644
--- a/app/serializers/merge_request_poll_widget_entity.rb
+++ b/app/serializers/merge_request_poll_widget_entity.rb
@@ -71,6 +71,18 @@ class MergeRequestPollWidgetEntity < Grape::Entity
end
end
+ expose :accessibility_report_path do |merge_request|
+ if merge_request.has_accessibility_reports?
+ accessibility_reports_project_merge_request_path(merge_request.project, merge_request, format: :json)
+ end
+ end
+
+ expose :terraform_reports_path do |merge_request|
+ if merge_request.has_terraform_reports?
+ terraform_reports_project_merge_request_path(merge_request.project, merge_request, format: :json)
+ end
+ end
+
expose :exposed_artifacts_path do |merge_request|
if merge_request.has_exposed_artifacts?
exposed_artifacts_project_merge_request_path(merge_request.project, merge_request, format: :json)
diff --git a/app/serializers/merge_request_serializer.rb b/app/serializers/merge_request_serializer.rb
index 9fd50c8c51d..508a2510dbd 100644
--- a/app/serializers/merge_request_serializer.rb
+++ b/app/serializers/merge_request_serializer.rb
@@ -27,3 +27,5 @@ class MergeRequestSerializer < BaseSerializer
super(merge_request, opts, entity)
end
end
+
+MergeRequestSerializer.prepend_if_ee('EE::MergeRequestSerializer')
diff --git a/app/serializers/note_user_entity.rb b/app/serializers/note_user_entity.rb
index 8d30bbff5e4..38e71528f18 100644
--- a/app/serializers/note_user_entity.rb
+++ b/app/serializers/note_user_entity.rb
@@ -1,7 +1,7 @@
# frozen_string_literal: true
class NoteUserEntity < UserEntity
- expose :gitlab_employee?, as: :is_gitlab_employee, if: ->(user, options) { user.gitlab_employee? }
-
unexpose :web_url
end
+
+NoteUserEntity.prepend_if_ee('EE::NoteUserEntity')
diff --git a/app/serializers/service_event_entity.rb b/app/serializers/service_event_entity.rb
new file mode 100644
index 00000000000..fd655dd1ed3
--- /dev/null
+++ b/app/serializers/service_event_entity.rb
@@ -0,0 +1,44 @@
+# frozen_string_literal: true
+
+class ServiceEventEntity < Grape::Entity
+ include RequestAwareEntity
+
+ expose :title do |event|
+ event
+ end
+
+ expose :event_field_name, as: :name
+
+ expose :value do |event|
+ service[event_field_name]
+ end
+
+ expose :description do |event|
+ service.class.event_description(event)
+ end
+
+ expose :field, if: -> (_, _) { event_field } do
+ expose :name do |event|
+ event_field[:name]
+ end
+ expose :value do |event|
+ service.public_send(event_field[:name]) # rubocop:disable GitlabSecurity/PublicSend
+ end
+ end
+
+ private
+
+ alias_method :event, :object
+
+ def event_field_name
+ ServicesHelper.service_event_field_name(event)
+ end
+
+ def event_field
+ @event_field ||= service.event_field(event)
+ end
+
+ def service
+ request.service
+ end
+end
diff --git a/app/serializers/service_event_serializer.rb b/app/serializers/service_event_serializer.rb
new file mode 100644
index 00000000000..7f5fe36e571
--- /dev/null
+++ b/app/serializers/service_event_serializer.rb
@@ -0,0 +1,5 @@
+# frozen_string_literal: true
+
+class ServiceEventSerializer < BaseSerializer
+ entity ServiceEventEntity
+end
diff --git a/app/serializers/test_suite_comparer_entity.rb b/app/serializers/test_suite_comparer_entity.rb
index 78c243f75b8..a9f19564b60 100644
--- a/app/serializers/test_suite_comparer_entity.rb
+++ b/app/serializers/test_suite_comparer_entity.rb
@@ -46,8 +46,6 @@ class TestSuiteComparerEntity < Grape::Entity
private
def max_tests(*used)
- return Integer::MAX unless Feature.enabled?(:ci_limit_test_reports_size, default_enabled: true)
-
[DEFAULT_MAX_TESTS - used.map(&:count).sum, DEFAULT_MIN_TESTS].max
end
# rubocop: enable CodeReuse/ActiveRecord
diff --git a/app/serializers/test_suite_entity.rb b/app/serializers/test_suite_entity.rb
index 0f88a496c77..53fa830718a 100644
--- a/app/serializers/test_suite_entity.rb
+++ b/app/serializers/test_suite_entity.rb
@@ -9,8 +9,9 @@ class TestSuiteEntity < Grape::Entity
expose :failed_count
expose :skipped_count
expose :error_count
+ expose :suite_error
expose :test_cases, using: TestCaseEntity do |test_suite|
- test_suite.test_cases.values.flat_map(&:values)
+ test_suite.suite_error ? [] : test_suite.test_cases.values.flat_map(&:values)
end
end
diff --git a/app/serializers/variable_entity.rb b/app/serializers/variable_entity.rb
index 017035fa117..9b0db371acb 100644
--- a/app/serializers/variable_entity.rb
+++ b/app/serializers/variable_entity.rb
@@ -1,12 +1,5 @@
# frozen_string_literal: true
-class VariableEntity < Grape::Entity
- expose :id
- expose :key
- expose :value
- expose :variable_type
-
- expose :protected?, as: :protected
- expose :masked?, as: :masked
+class VariableEntity < Ci::BasicVariableEntity
expose :environment_scope
end
diff --git a/app/services/alert_management/create_alert_issue_service.rb b/app/services/alert_management/create_alert_issue_service.rb
new file mode 100644
index 00000000000..0197f29145d
--- /dev/null
+++ b/app/services/alert_management/create_alert_issue_service.rb
@@ -0,0 +1,70 @@
+# frozen_string_literal: true
+
+module AlertManagement
+ class CreateAlertIssueService
+ # @param alert [AlertManagement::Alert]
+ # @param user [User]
+ def initialize(alert, user)
+ @alert = alert
+ @user = user
+ end
+
+ def execute
+ return error_no_permissions unless allowed?
+ return error_issue_already_exists if alert.issue
+
+ result = create_issue(alert, user, alert_payload)
+ @issue = result[:issue]
+
+ return error(result[:message]) if result[:status] == :error
+ return error(alert.errors.full_messages.to_sentence) unless update_alert_issue_id
+
+ success
+ end
+
+ private
+
+ attr_reader :alert, :user, :issue
+
+ delegate :project, to: :alert
+
+ def allowed?
+ Feature.enabled?(:alert_management_create_alert_issue, project) &&
+ user.can?(:create_issue, project)
+ end
+
+ def create_issue(alert, user, alert_payload)
+ ::IncidentManagement::CreateIssueService
+ .new(project, alert_payload, user)
+ .execute(skip_settings_check: true)
+ end
+
+ def alert_payload
+ if alert.prometheus?
+ alert.payload
+ else
+ Gitlab::Alerting::NotificationPayloadParser.call(alert.payload.to_h)
+ end
+ end
+
+ def update_alert_issue_id
+ alert.update(issue_id: issue.id)
+ end
+
+ def success
+ ServiceResponse.success(payload: { issue: issue })
+ end
+
+ def error(message)
+ ServiceResponse.error(payload: { issue: issue }, message: message)
+ end
+
+ def error_issue_already_exists
+ error(_('An issue already exists'))
+ end
+
+ def error_no_permissions
+ error(_('You have no permissions'))
+ end
+ end
+end
diff --git a/app/services/alert_management/process_prometheus_alert_service.rb b/app/services/alert_management/process_prometheus_alert_service.rb
new file mode 100644
index 00000000000..af28f1354b3
--- /dev/null
+++ b/app/services/alert_management/process_prometheus_alert_service.rb
@@ -0,0 +1,86 @@
+# frozen_string_literal: true
+
+module AlertManagement
+ class ProcessPrometheusAlertService < BaseService
+ include Gitlab::Utils::StrongMemoize
+
+ def execute
+ return bad_request unless parsed_alert.valid?
+
+ process_alert_management_alert
+
+ ServiceResponse.success
+ end
+
+ private
+
+ delegate :firing?, :resolved?, :gitlab_fingerprint, :ends_at, to: :parsed_alert
+
+ def parsed_alert
+ strong_memoize(:parsed_alert) do
+ Gitlab::Alerting::Alert.new(project: project, payload: params)
+ end
+ end
+
+ def process_alert_management_alert
+ process_firing_alert_management_alert if firing?
+ process_resolved_alert_management_alert if resolved?
+ end
+
+ def process_firing_alert_management_alert
+ if am_alert.present?
+ reset_alert_management_alert_status
+ else
+ create_alert_management_alert
+ end
+ end
+
+ def reset_alert_management_alert_status
+ return if am_alert.trigger
+
+ logger.warn(
+ message: 'Unable to update AlertManagement::Alert status to triggered',
+ project_id: project.id,
+ alert_id: am_alert.id
+ )
+ end
+
+ def create_alert_management_alert
+ am_alert = AlertManagement::Alert.new(am_alert_params.merge(ended_at: nil))
+ return if am_alert.save
+
+ logger.warn(
+ message: 'Unable to create AlertManagement::Alert',
+ project_id: project.id,
+ alert_errors: am_alert.errors.messages
+ )
+ end
+
+ def am_alert_params
+ Gitlab::AlertManagement::AlertParams.from_prometheus_alert(project: project, parsed_alert: parsed_alert)
+ end
+
+ def process_resolved_alert_management_alert
+ return if am_alert.blank?
+ return if am_alert.resolve(ends_at)
+
+ logger.warn(
+ message: 'Unable to update AlertManagement::Alert status to resolved',
+ project_id: project.id,
+ alert_id: am_alert.id
+ )
+ end
+
+ def logger
+ @logger ||= Gitlab::AppLogger
+ end
+
+ def am_alert
+ @am_alert ||= AlertManagement::Alert.for_fingerprint(project, gitlab_fingerprint).first
+ end
+
+ def bad_request
+ ServiceResponse.error(message: 'Bad Request', http_status: :bad_request)
+ end
+ end
+end
diff --git a/app/services/alert_management/update_alert_status_service.rb b/app/services/alert_management/update_alert_status_service.rb
new file mode 100644
index 00000000000..a7ebddb82e0
--- /dev/null
+++ b/app/services/alert_management/update_alert_status_service.rb
@@ -0,0 +1,63 @@
+# frozen_string_literal: true
+
+module AlertManagement
+ class UpdateAlertStatusService
+ include Gitlab::Utils::StrongMemoize
+
+ # @param alert [AlertManagement::Alert]
+ # @param user [User]
+ # @param status [Integer] Must match a value from AlertManagement::Alert::STATUSES
+ def initialize(alert, user, status)
+ @alert = alert
+ @user = user
+ @status = status
+ end
+
+ def execute
+ return error_no_permissions unless allowed?
+ return error_invalid_status unless status_key
+
+ if alert.update(status_event: status_event)
+ success
+ else
+ error(alert.errors.full_messages.to_sentence)
+ end
+ end
+
+ private
+
+ attr_reader :alert, :user, :status
+
+ delegate :project, to: :alert
+
+ def allowed?
+ user.can?(:update_alert_management_alert, project)
+ end
+
+ def status_key
+ strong_memoize(:status_key) do
+ AlertManagement::Alert::STATUSES.key(status)
+ end
+ end
+
+ def status_event
+ AlertManagement::Alert::STATUS_EVENTS[status_key]
+ end
+
+ def success
+ ServiceResponse.success(payload: { alert: alert })
+ end
+
+ def error_no_permissions
+ error(_('You have no permissions'))
+ end
+
+ def error_invalid_status
+ error(_('Invalid status'))
+ end
+
+ def error(message)
+ ServiceResponse.error(payload: { alert: alert }, message: message)
+ end
+ end
+end
diff --git a/app/services/audit_event_service.rb b/app/services/audit_event_service.rb
index d9e40c456aa..fb309aed649 100644
--- a/app/services/audit_event_service.rb
+++ b/app/services/audit_event_service.rb
@@ -50,8 +50,9 @@ class AuditEventService
private
def build_author(author)
- if author.is_a?(User)
- author
+ case author
+ when User
+ author.impersonated? ? Gitlab::Audit::ImpersonatedAuthor.new(author) : author
else
Gitlab::Audit::UnauthenticatedAuthor.new(name: author)
end
diff --git a/app/services/auth/container_registry_authentication_service.rb b/app/services/auth/container_registry_authentication_service.rb
index 4a699fe3213..44a434f4402 100644
--- a/app/services/auth/container_registry_authentication_service.rb
+++ b/app/services/auth/container_registry_authentication_service.rb
@@ -52,7 +52,7 @@ module Auth
end
def self.token_expire_at
- Time.now + Gitlab::CurrentSettings.container_registry_token_expire_delay.minutes
+ Time.current + Gitlab::CurrentSettings.container_registry_token_expire_delay.minutes
end
private
@@ -103,17 +103,19 @@ module Auth
return unless requested_project
- actions = actions.select do |action|
+ authorized_actions = actions.select do |action|
can_access?(requested_project, action)
end
- return unless actions.present?
+ log_if_actions_denied(type, requested_project, actions, authorized_actions)
+
+ return unless authorized_actions.present?
# At this point user/build is already authenticated.
#
- ensure_container_repository!(path, actions)
+ ensure_container_repository!(path, authorized_actions)
- { type: type, name: path.to_s, actions: actions }
+ { type: type, name: path.to_s, actions: authorized_actions }
end
##
@@ -222,5 +224,22 @@ module Auth
REGISTRY_LOGIN_ABILITIES.include?(ability)
end
end
+
+ def log_if_actions_denied(type, requested_project, requested_actions, authorized_actions)
+ return if requested_actions == authorized_actions
+
+ log_info = {
+ message: "Denied container registry permissions",
+ scope_type: type,
+ requested_project_path: requested_project.full_path,
+ requested_actions: requested_actions,
+ authorized_actions: authorized_actions,
+ username: current_user&.username,
+ user_id: current_user&.id,
+ project_path: project&.full_path
+ }.compact
+
+ Gitlab::AuthLogger.warn(log_info)
+ end
end
end
diff --git a/app/services/authorized_project_update/project_create_service.rb b/app/services/authorized_project_update/project_create_service.rb
new file mode 100644
index 00000000000..c17c0a033fe
--- /dev/null
+++ b/app/services/authorized_project_update/project_create_service.rb
@@ -0,0 +1,34 @@
+# frozen_string_literal: true
+
+module AuthorizedProjectUpdate
+ class ProjectCreateService < BaseService
+ BATCH_SIZE = 1000
+
+ def initialize(project)
+ @project = project
+ end
+
+ def execute
+ group = project.group
+
+ unless group
+ return ServiceResponse.error(message: 'Project does not have a group')
+ end
+
+ group.members_from_self_and_ancestors_with_effective_access_level
+ .each_batch(of: BATCH_SIZE, column: :user_id) do |members|
+ attributes = members.map do |member|
+ { user_id: member.user_id, project_id: project.id, access_level: member.access_level }
+ end
+
+ ProjectAuthorization.insert_all(attributes)
+ end
+
+ ServiceResponse.success
+ end
+
+ private
+
+ attr_reader :project
+ end
+end
diff --git a/app/services/base_container_service.rb b/app/services/base_container_service.rb
new file mode 100644
index 00000000000..56e4b8c908c
--- /dev/null
+++ b/app/services/base_container_service.rb
@@ -0,0 +1,12 @@
+# frozen_string_literal: true
+
+# Base class, scoped by container (project or group)
+class BaseContainerService
+ include BaseServiceUtility
+
+ attr_reader :container, :current_user, :params
+
+ def initialize(container:, current_user: nil, params: {})
+ @container, @current_user, @params = container, current_user, params.dup
+ end
+end
diff --git a/app/services/base_service.rb b/app/services/base_service.rb
index bc0b968f516..b4c4b6980a8 100644
--- a/app/services/base_service.rb
+++ b/app/services/base_service.rb
@@ -1,7 +1,16 @@
# frozen_string_literal: true
+# This is the original root class for service related classes,
+# and due to historical reason takes a project as scope.
+# Later separate base classes for different scopes will be created,
+# and existing service will use these one by one.
+# After all are migrated, we can remove this class.
+#
+# TODO: New services should consider inheriting from
+# BaseContainerService, or create new base class:
+# https://gitlab.com/gitlab-org/gitlab/-/issues/216672
class BaseService
- include Gitlab::Allowable
+ include BaseServiceUtility
attr_accessor :project, :current_user, :params
@@ -9,67 +18,5 @@ class BaseService
@project, @current_user, @params = project, user, params.dup
end
- def notification_service
- NotificationService.new
- end
-
- def event_service
- EventCreateService.new
- end
-
- def todo_service
- TodoService.new
- end
-
- def log_info(message)
- Gitlab::AppLogger.info message
- end
-
- def log_error(message)
- Gitlab::AppLogger.error message
- end
-
- def system_hook_service
- SystemHooksService.new
- end
-
delegate :repository, to: :project
-
- # Add an error to the specified model for restricted visibility levels
- def deny_visibility_level(model, denied_visibility_level = nil)
- denied_visibility_level ||= model.visibility_level
-
- level_name = Gitlab::VisibilityLevel.level_name(denied_visibility_level).downcase
-
- model.errors.add(:visibility_level, "#{level_name} has been restricted by your GitLab administrator")
- end
-
- def visibility_level
- params[:visibility].is_a?(String) ? Gitlab::VisibilityLevel.level_value(params[:visibility]) : params[:visibility_level]
- end
-
- private
-
- # Return a Hash with an `error` status
- #
- # message - Error message to include in the Hash
- # http_status - Optional HTTP status code override (default: nil)
- # pass_back - Additional attributes to be included in the resulting Hash
- def error(message, http_status = nil, pass_back: {})
- result = {
- message: message,
- status: :error
- }.reverse_merge(pass_back)
-
- result[:http_status] = http_status if http_status
- result
- end
-
- # Return a Hash with a `success` status
- #
- # pass_back - Additional attributes to be included in the resulting Hash
- def success(pass_back = {})
- pass_back[:status] = :success
- pass_back
- end
end
diff --git a/app/services/boards/issues/list_service.rb b/app/services/boards/issues/list_service.rb
index 9637eb1b918..e08509b84db 100644
--- a/app/services/boards/issues/list_service.rb
+++ b/app/services/boards/issues/list_service.rb
@@ -12,7 +12,7 @@ module Boards
def execute
return fetch_issues.order_closed_date_desc if list&.closed?
- fetch_issues.order_by_position_and_priority(with_cte: can_attempt_search_optimization?)
+ fetch_issues.order_by_position_and_priority(with_cte: params[:search].present?)
end
# rubocop: disable CodeReuse/ActiveRecord
@@ -91,7 +91,7 @@ module Boards
end
def set_attempt_search_optimizations
- return unless can_attempt_search_optimization?
+ return unless params[:search].present?
if board.group_board?
params[:attempt_group_search_optimizations] = true
@@ -130,11 +130,6 @@ module Boards
def board_group
board.group_board? ? parent : parent.group
end
-
- def can_attempt_search_optimization?
- params[:search].present? &&
- Feature.enabled?(:board_search_optimization, board_group, default_enabled: true)
- end
end
end
end
diff --git a/app/services/boards/lists/list_service.rb b/app/services/boards/lists/list_service.rb
index c96ea970943..07ce58b6851 100644
--- a/app/services/boards/lists/list_service.rb
+++ b/app/services/boards/lists/list_service.rb
@@ -3,8 +3,10 @@
module Boards
module Lists
class ListService < Boards::BaseService
- def execute(board)
- board.lists.create(list_type: :backlog) unless board.lists.backlog.exists?
+ def execute(board, create_default_lists: true)
+ if create_default_lists && !board.lists.backlog.exists?
+ board.lists.create(list_type: :backlog)
+ end
board.lists.preload_associated_models
end
diff --git a/app/services/branches/create_service.rb b/app/services/branches/create_service.rb
index c8afd97e6bf..958dd5c9965 100644
--- a/app/services/branches/create_service.rb
+++ b/app/services/branches/create_service.rb
@@ -14,7 +14,7 @@ module Branches
if new_branch
success(new_branch)
else
- error("Invalid reference name: #{branch_name}")
+ error("Invalid reference name: #{ref}")
end
rescue Gitlab::Git::PreReceiveError => ex
error(ex.message)
diff --git a/app/services/ci/compare_accessibility_reports_service.rb b/app/services/ci/compare_accessibility_reports_service.rb
new file mode 100644
index 00000000000..efb38d39d98
--- /dev/null
+++ b/app/services/ci/compare_accessibility_reports_service.rb
@@ -0,0 +1,17 @@
+# frozen_string_literal: true
+
+module Ci
+ class CompareAccessibilityReportsService < CompareReportsBaseService
+ def comparer_class
+ Gitlab::Ci::Reports::AccessibilityReportsComparer
+ end
+
+ def serializer_class
+ AccessibilityReportsComparerSerializer
+ end
+
+ def get_report(pipeline)
+ pipeline&.accessibility_reports
+ end
+ end
+end
diff --git a/app/services/ci/create_job_artifacts_service.rb b/app/services/ci/create_job_artifacts_service.rb
index 5d7d552dc5a..f0ffe67510b 100644
--- a/app/services/ci/create_job_artifacts_service.rb
+++ b/app/services/ci/create_job_artifacts_service.rb
@@ -46,6 +46,11 @@ module Ci
expire_in: expire_in)
end
+ if Feature.enabled?(:keep_latest_artifact_for_ref, job.project)
+ artifact.locked = true
+ artifact_metadata&.locked = true
+ end
+
[artifact, artifact_metadata]
end
@@ -56,6 +61,7 @@ module Ci
case artifact.file_type
when 'dotenv' then parse_dotenv_artifact(job, artifact)
+ when 'cluster_applications' then parse_cluster_applications_artifact(job, artifact)
else success
end
end
@@ -64,6 +70,7 @@ module Ci
Ci::JobArtifact.transaction do
artifact.save!
artifact_metadata&.save!
+ unlock_previous_artifacts!(artifact)
# NOTE: The `artifacts_expire_at` column is already deprecated and to be removed in the near future.
job.update_column(:artifacts_expire_at, artifact.expire_at)
@@ -81,6 +88,12 @@ module Ci
error(error.message, :bad_request)
end
+ def unlock_previous_artifacts!(artifact)
+ return unless Feature.enabled?(:keep_latest_artifact_for_ref, artifact.job.project)
+
+ Ci::JobArtifact.for_ref(artifact.job.ref, artifact.project_id).locked.update_all(locked: false)
+ end
+
def sha256_matches_existing_artifact?(job, artifact_type, artifacts_file)
existing_artifact = job.job_artifacts.find_by_file_type(artifact_type)
return false unless existing_artifact
@@ -99,5 +112,9 @@ module Ci
def parse_dotenv_artifact(job, artifact)
Ci::ParseDotenvArtifactService.new(job.project, current_user).execute(artifact)
end
+
+ def parse_cluster_applications_artifact(job, artifact)
+ Clusters::ParseClusterApplicationsArtifactService.new(job, job.user).execute(artifact)
+ end
end
end
diff --git a/app/services/ci/create_pipeline_service.rb b/app/services/ci/create_pipeline_service.rb
index 347630f865f..922c3556362 100644
--- a/app/services/ci/create_pipeline_service.rb
+++ b/app/services/ci/create_pipeline_service.rb
@@ -102,21 +102,12 @@ module Ci
# rubocop: disable CodeReuse/ActiveRecord
def auto_cancelable_pipelines
- # TODO: Introduced by https://gitlab.com/gitlab-org/gitlab-foss/merge_requests/23464
- if Feature.enabled?(:ci_support_interruptible_pipelines, project, default_enabled: true)
- project.ci_pipelines
- .where(ref: pipeline.ref)
- .where.not(id: pipeline.same_family_pipeline_ids)
- .where.not(sha: project.commit(pipeline.ref).try(:id))
- .alive_or_scheduled
- .with_only_interruptible_builds
- else
- project.ci_pipelines
- .where(ref: pipeline.ref)
- .where.not(id: pipeline.same_family_pipeline_ids)
- .where.not(sha: project.commit(pipeline.ref).try(:id))
- .created_or_pending
- end
+ project.ci_pipelines
+ .where(ref: pipeline.ref)
+ .where.not(id: pipeline.same_family_pipeline_ids)
+ .where.not(sha: project.commit(pipeline.ref).try(:id))
+ .alive_or_scheduled
+ .with_only_interruptible_builds
end
# rubocop: enable CodeReuse/ActiveRecord
diff --git a/app/services/ci/daily_build_group_report_result_service.rb b/app/services/ci/daily_build_group_report_result_service.rb
new file mode 100644
index 00000000000..6cdf3c88f8c
--- /dev/null
+++ b/app/services/ci/daily_build_group_report_result_service.rb
@@ -0,0 +1,40 @@
+# frozen_string_literal: true
+
+module Ci
+ class DailyBuildGroupReportResultService
+ def execute(pipeline)
+ return unless Feature.enabled?(:ci_daily_code_coverage, pipeline.project, default_enabled: true)
+
+ DailyBuildGroupReportResult.upsert_reports(coverage_reports(pipeline))
+ end
+
+ private
+
+ def coverage_reports(pipeline)
+ base_attrs = {
+ project_id: pipeline.project_id,
+ ref_path: pipeline.source_ref_path,
+ date: pipeline.created_at.to_date,
+ last_pipeline_id: pipeline.id
+ }
+
+ aggregate(pipeline.builds.with_coverage).map do |group_name, group|
+ base_attrs.merge(
+ group_name: group_name,
+ data: {
+ 'coverage' => average_coverage(group)
+ }
+ )
+ end
+ end
+
+ def aggregate(builds)
+ builds.group_by(&:group_name)
+ end
+
+ def average_coverage(group)
+ total_coverage = group.reduce(0.0) { |sum, build| sum + build.coverage }
+ (total_coverage / group.size).round(2)
+ end
+ end
+end
diff --git a/app/services/ci/daily_report_result_service.rb b/app/services/ci/daily_report_result_service.rb
deleted file mode 100644
index b774a806203..00000000000
--- a/app/services/ci/daily_report_result_service.rb
+++ /dev/null
@@ -1,39 +0,0 @@
-# frozen_string_literal: true
-
-module Ci
- class DailyReportResultService
- def execute(pipeline)
- return unless Feature.enabled?(:ci_daily_code_coverage, pipeline.project, default_enabled: true)
-
- DailyReportResult.upsert_reports(coverage_reports(pipeline))
- end
-
- private
-
- def coverage_reports(pipeline)
- base_attrs = {
- project_id: pipeline.project_id,
- ref_path: pipeline.source_ref_path,
- param_type: DailyReportResult.param_types[:coverage],
- date: pipeline.created_at.to_date,
- last_pipeline_id: pipeline.id
- }
-
- aggregate(pipeline.builds.with_coverage).map do |group_name, group|
- base_attrs.merge(
- title: group_name,
- value: average_coverage(group)
- )
- end
- end
-
- def aggregate(builds)
- builds.group_by(&:group_name)
- end
-
- def average_coverage(group)
- total_coverage = group.reduce(0.0) { |sum, build| sum + build.coverage }
- (total_coverage / group.size).round(2)
- end
- end
-end
diff --git a/app/services/ci/destroy_expired_job_artifacts_service.rb b/app/services/ci/destroy_expired_job_artifacts_service.rb
index 7d2f5d33fed..5deb84812ac 100644
--- a/app/services/ci/destroy_expired_job_artifacts_service.rb
+++ b/app/services/ci/destroy_expired_job_artifacts_service.rb
@@ -28,7 +28,13 @@ module Ci
private
def destroy_batch
- artifacts = Ci::JobArtifact.expired(BATCH_SIZE).to_a
+ artifact_batch = if Feature.enabled?(:keep_latest_artifact_for_ref)
+ Ci::JobArtifact.expired(BATCH_SIZE).unlocked
+ else
+ Ci::JobArtifact.expired(BATCH_SIZE)
+ end
+
+ artifacts = artifact_batch.to_a
return false if artifacts.empty?
diff --git a/app/services/ci/generate_terraform_reports_service.rb b/app/services/ci/generate_terraform_reports_service.rb
new file mode 100644
index 00000000000..d768ce777d4
--- /dev/null
+++ b/app/services/ci/generate_terraform_reports_service.rb
@@ -0,0 +1,29 @@
+# frozen_string_literal: true
+
+module Ci
+ # TODO: a couple of points with this approach:
+ # + reuses existing architecture and reactive caching
+ # - it's not a report comparison and some comparing features must be turned off.
+ # see CompareReportsBaseService for more notes.
+ # issue: https://gitlab.com/gitlab-org/gitlab/issues/34224
+ class GenerateTerraformReportsService < CompareReportsBaseService
+ def execute(base_pipeline, head_pipeline)
+ {
+ status: :parsed,
+ key: key(base_pipeline, head_pipeline),
+ data: head_pipeline.terraform_reports.plans
+ }
+ rescue => e
+ Gitlab::ErrorTracking.track_exception(e, project_id: project.id)
+ {
+ status: :error,
+ key: key(base_pipeline, head_pipeline),
+ status_reason: _('An error occurred while fetching terraform reports.')
+ }
+ end
+
+ def latest?(base_pipeline, head_pipeline, data)
+ data&.fetch(:key, nil) == key(base_pipeline, head_pipeline)
+ end
+ end
+end
diff --git a/app/services/ci/pipeline_processing/atomic_processing_service.rb b/app/services/ci/pipeline_processing/atomic_processing_service.rb
index 2a1bf15b9a3..b01a9d2e3b8 100644
--- a/app/services/ci/pipeline_processing/atomic_processing_service.rb
+++ b/app/services/ci/pipeline_processing/atomic_processing_service.rb
@@ -95,7 +95,7 @@ module Ci
def processable_status(processable)
if processable.scheduling_type_dag?
# Processable uses DAG, get status of all dependent needs
- @collection.status_for_names(processable.aggregated_needs_names.to_a)
+ @collection.status_for_names(processable.aggregated_needs_names.to_a, dag: true)
else
# Processable uses Stages, get status of prior stage
@collection.status_for_prior_stage_position(processable.stage_idx.to_i)
diff --git a/app/services/ci/pipeline_processing/atomic_processing_service/status_collection.rb b/app/services/ci/pipeline_processing/atomic_processing_service/status_collection.rb
index 42e38a5c80f..2228328882d 100644
--- a/app/services/ci/pipeline_processing/atomic_processing_service/status_collection.rb
+++ b/app/services/ci/pipeline_processing/atomic_processing_service/status_collection.rb
@@ -32,14 +32,14 @@ module Ci
# This methods gets composite status of all processables
def status_of_all
- status_for_array(all_statuses)
+ status_for_array(all_statuses, dag: false)
end
# This methods gets composite status for processables with given names
- def status_for_names(names)
+ def status_for_names(names, dag:)
name_statuses = all_statuses_by_name.slice(*names)
- status_for_array(name_statuses.values)
+ status_for_array(name_statuses.values, dag: dag)
end
# This methods gets composite status for processables before given stage
@@ -48,7 +48,7 @@ module Ci
stage_statuses = all_statuses_grouped_by_stage_position
.select { |stage_position, _| stage_position < position }
- status_for_array(stage_statuses.values.flatten)
+ status_for_array(stage_statuses.values.flatten, dag: false)
end
end
@@ -65,7 +65,7 @@ module Ci
strong_memoize("status_for_stage_position_#{current_position}") do
stage_statuses = all_statuses_grouped_by_stage_position[current_position].to_a
- status_for_array(stage_statuses.flatten)
+ status_for_array(stage_statuses.flatten, dag: false)
end
end
@@ -76,7 +76,14 @@ module Ci
private
- def status_for_array(statuses)
+ def status_for_array(statuses, dag:)
+ # TODO: This is hack to support
+ # the same exact behaviour for Atomic and Legacy processing
+ # that DAG is blocked from executing if dependent is not "complete"
+ if dag && statuses.any? { |status| HasStatus::COMPLETED_STATUSES.exclude?(status[:status]) }
+ return 'pending'
+ end
+
result = Gitlab::Ci::Status::Composite
.new(statuses)
.status
diff --git a/app/services/ci/pipeline_schedule_service.rb b/app/services/ci/pipeline_schedule_service.rb
index 6028643489d..596c3b80bda 100644
--- a/app/services/ci/pipeline_schedule_service.rb
+++ b/app/services/ci/pipeline_schedule_service.rb
@@ -6,19 +6,7 @@ module Ci
# Ensure `next_run_at` is set properly before creating a pipeline.
# Otherwise, multiple pipelines could be created in a short interval.
schedule.schedule_next_run!
-
- if Feature.enabled?(:ci_pipeline_schedule_async)
- RunPipelineScheduleWorker.perform_async(schedule.id, schedule.owner&.id)
- else
- begin
- RunPipelineScheduleWorker.new.perform(schedule.id, schedule.owner&.id)
- ensure
- ##
- # This is the temporary solution for avoiding the memory bloat.
- # See more https://gitlab.com/gitlab-org/gitlab-foss/issues/61955
- GC.start if Feature.enabled?(:ci_pipeline_schedule_force_gc, default_enabled: true)
- end
- end
+ RunPipelineScheduleWorker.perform_async(schedule.id, schedule.owner&.id)
end
end
end
diff --git a/app/services/ci/process_pipeline_service.rb b/app/services/ci/process_pipeline_service.rb
index d1efa19eb0d..3f23e81dcdd 100644
--- a/app/services/ci/process_pipeline_service.rb
+++ b/app/services/ci/process_pipeline_service.rb
@@ -10,7 +10,6 @@ module Ci
def execute(trigger_build_ids = nil, initial_process: false)
update_retried
- ensure_scheduling_type_for_processables
if Feature.enabled?(:ci_atomic_processing, pipeline.project)
Ci::PipelineProcessing::AtomicProcessingService
@@ -44,17 +43,5 @@ module Ci
.update_all(retried: true) if latest_statuses.any?
end
# rubocop: enable CodeReuse/ActiveRecord
-
- # Set scheduling type of processables if they were created before scheduling_type
- # data was deployed (https://gitlab.com/gitlab-org/gitlab/-/merge_requests/22246).
- # Given that this service runs multiple times during the pipeline
- # life cycle we need to ensure we populate the data once.
- # See more: https://gitlab.com/gitlab-org/gitlab/issues/205426
- def ensure_scheduling_type_for_processables
- lease = Gitlab::ExclusiveLease.new("set-scheduling-types:#{pipeline.id}", timeout: 1.hour.to_i)
- return unless lease.try_obtain
-
- pipeline.processables.populate_scheduling_type!
- end
end
end
diff --git a/app/services/ci/register_job_service.rb b/app/services/ci/register_job_service.rb
index fb59797a8df..17b9e56636b 100644
--- a/app/services/ci/register_job_service.rb
+++ b/app/services/ci/register_job_service.rb
@@ -85,8 +85,6 @@ module Ci
# to make sure that this is properly handled by runner.
Result.new(nil, false)
rescue => ex
- raise ex unless Feature.enabled?(:ci_doom_build, default_enabled: true)
-
scheduler_failure!(build)
track_exception_for_build(ex, build)
@@ -203,7 +201,7 @@ module Ci
labels[:shard] = shard.gsub(METRICS_SHARD_TAG_PREFIX, '') if shard
end
- job_queue_duration_seconds.observe(labels, Time.now - job.queued_at) unless job.queued_at.nil?
+ job_queue_duration_seconds.observe(labels, Time.current - job.queued_at) unless job.queued_at.nil?
attempt_counter.increment
end
diff --git a/app/services/ci/retry_build_service.rb b/app/services/ci/retry_build_service.rb
index a65fe2ecb3a..23507a31c72 100644
--- a/app/services/ci/retry_build_service.rb
+++ b/app/services/ci/retry_build_service.rb
@@ -9,6 +9,8 @@ module Ci
resource_group scheduling_type].freeze
def execute(build)
+ build.ensure_scheduling_type!
+
reprocess!(build).tap do |new_build|
build.pipeline.mark_as_processable_after_stage(build.stage_idx)
@@ -31,6 +33,9 @@ module Ci
end.to_h
attributes[:user] = current_user
+
+ # TODO: we can probably remove this logic
+ # see: https://gitlab.com/gitlab-org/gitlab/-/issues/217930
attributes[:scheduling_type] ||= build.find_legacy_scheduling_type
Ci::Build.transaction do
diff --git a/app/services/ci/retry_pipeline_service.rb b/app/services/ci/retry_pipeline_service.rb
index 9bb236ac44c..4229be6c7d7 100644
--- a/app/services/ci/retry_pipeline_service.rb
+++ b/app/services/ci/retry_pipeline_service.rb
@@ -11,6 +11,8 @@ module Ci
needs = Set.new
+ pipeline.ensure_scheduling_type!
+
pipeline.retryable_builds.preload_needs.find_each do |build|
next unless can?(current_user, :update_build, build)
diff --git a/app/services/ci/update_instance_variables_service.rb b/app/services/ci/update_instance_variables_service.rb
new file mode 100644
index 00000000000..ee513647d08
--- /dev/null
+++ b/app/services/ci/update_instance_variables_service.rb
@@ -0,0 +1,72 @@
+# frozen_string_literal: true
+
+# This class is a simplified version of assign_nested_attributes_for_collection_association from ActiveRecord
+# https://github.com/rails/rails/blob/v6.0.2.1/activerecord/lib/active_record/nested_attributes.rb#L466
+
+module Ci
+ class UpdateInstanceVariablesService
+ UNASSIGNABLE_KEYS = %w(id _destroy).freeze
+
+ def initialize(params)
+ @params = params[:variables_attributes]
+ end
+
+ def execute
+ instantiate_records
+ persist_records
+ end
+
+ def errors
+ @records.to_a.flat_map { |r| r.errors.full_messages }
+ end
+
+ private
+
+ attr_reader :params
+
+ def existing_records_by_id
+ @existing_records_by_id ||= Ci::InstanceVariable
+ .all
+ .index_by { |var| var.id.to_s }
+ end
+
+ def instantiate_records
+ @records = params.map do |attributes|
+ find_or_initialize_record(attributes).tap do |record|
+ record.assign_attributes(attributes.except(*UNASSIGNABLE_KEYS))
+ record.mark_for_destruction if has_destroy_flag?(attributes)
+ end
+ end
+ end
+
+ def find_or_initialize_record(attributes)
+ id = attributes[:id].to_s
+
+ if id.blank?
+ Ci::InstanceVariable.new
+ else
+ existing_records_by_id.fetch(id) { raise ActiveRecord::RecordNotFound }
+ end
+ end
+
+ def persist_records
+ Ci::InstanceVariable.transaction do
+ success = @records.map do |record|
+ if record.marked_for_destruction?
+ record.destroy
+ else
+ record.save
+ end
+ end.all?
+
+ raise ActiveRecord::Rollback unless success
+
+ success
+ end
+ end
+
+ def has_destroy_flag?(hash)
+ Gitlab::Utils.to_boolean(hash['_destroy'])
+ end
+ end
+end
diff --git a/app/services/clusters/applications/base_service.rb b/app/services/clusters/applications/base_service.rb
index 86b48b5228d..39a2d6bf758 100644
--- a/app/services/clusters/applications/base_service.rb
+++ b/app/services/clusters/applications/base_service.rb
@@ -5,6 +5,8 @@ module Clusters
class BaseService
InvalidApplicationError = Class.new(StandardError)
+ FLUENTD_KNOWN_ATTRS = %i[host protocol port waf_log_enabled cilium_log_enabled].freeze
+
attr_reader :cluster, :current_user, :params
def initialize(cluster, user, params = {})
@@ -35,17 +37,7 @@ module Clusters
application.modsecurity_mode = params[:modsecurity_mode] || 0
end
- if application.has_attribute?(:host)
- application.host = params[:host]
- end
-
- if application.has_attribute?(:protocol)
- application.protocol = params[:protocol]
- end
-
- if application.has_attribute?(:port)
- application.port = params[:port]
- end
+ apply_fluentd_related_attributes(application)
if application.respond_to?(:oauth_application)
application.oauth_application = create_oauth_application(application, request)
@@ -111,6 +103,12 @@ module Clusters
::Applications::CreateService.new(current_user, oauth_application_params).execute(request)
end
+
+ def apply_fluentd_related_attributes(application)
+ FLUENTD_KNOWN_ATTRS.each do |attr|
+ application[attr] = params[attr] if application.has_attribute?(attr)
+ end
+ end
end
end
end
diff --git a/app/services/clusters/applications/check_installation_progress_service.rb b/app/services/clusters/applications/check_installation_progress_service.rb
index 7d064abfaa3..249abd3ff9d 100644
--- a/app/services/clusters/applications/check_installation_progress_service.rb
+++ b/app/services/clusters/applications/check_installation_progress_service.rb
@@ -33,7 +33,7 @@ module Clusters
end
def timed_out?
- Time.now.utc - app.updated_at.utc > ClusterWaitForAppInstallationWorker::TIMEOUT
+ Time.current.utc - app.updated_at.utc > ClusterWaitForAppInstallationWorker::TIMEOUT
end
def remove_installation_pod
diff --git a/app/services/clusters/applications/check_uninstall_progress_service.rb b/app/services/clusters/applications/check_uninstall_progress_service.rb
index fe9c488bdfd..cd213c3ebbf 100644
--- a/app/services/clusters/applications/check_uninstall_progress_service.rb
+++ b/app/services/clusters/applications/check_uninstall_progress_service.rb
@@ -31,7 +31,7 @@ module Clusters
end
def timed_out?
- Time.now.utc - app.updated_at.utc > WaitForUninstallAppWorker::TIMEOUT
+ Time.current.utc - app.updated_at.utc > WaitForUninstallAppWorker::TIMEOUT
end
def remove_uninstallation_pod
diff --git a/app/services/clusters/applications/check_upgrade_progress_service.rb b/app/services/clusters/applications/check_upgrade_progress_service.rb
index 8502ea69f27..bc161218618 100644
--- a/app/services/clusters/applications/check_upgrade_progress_service.rb
+++ b/app/services/clusters/applications/check_upgrade_progress_service.rb
@@ -46,7 +46,7 @@ module Clusters
end
def timed_out?
- Time.now.utc - app.updated_at.to_time.utc > ::ClusterWaitForAppUpdateWorker::TIMEOUT
+ Time.current.utc - app.updated_at.to_time.utc > ::ClusterWaitForAppUpdateWorker::TIMEOUT
end
def remove_pod
diff --git a/app/services/clusters/applications/ingress_modsecurity_usage_service.rb b/app/services/clusters/applications/ingress_modsecurity_usage_service.rb
deleted file mode 100644
index 4aac8bb3cbd..00000000000
--- a/app/services/clusters/applications/ingress_modsecurity_usage_service.rb
+++ /dev/null
@@ -1,69 +0,0 @@
-# frozen_string_literal: true
-
-# rubocop: disable CodeReuse/ActiveRecord
-module Clusters
- module Applications
- ##
- # This service measures usage of the Modsecurity Web Application Firewall across the entire
- # instance's deployed environments.
- #
- # The default configuration is`AUTO_DEVOPS_MODSECURITY_SEC_RULE_ENGINE=DetectionOnly` so we
- # measure non-default values via definition of either ci_variables or ci_pipeline_variables.
- # Since both these values are encrypted, we must decrypt and count them in memory.
- #
- # NOTE: this service is an approximation as it does not yet take into account `environment_scope` or `ci_group_variables`.
- ##
- class IngressModsecurityUsageService
- ADO_MODSEC_KEY = "AUTO_DEVOPS_MODSECURITY_SEC_RULE_ENGINE"
-
- def initialize(blocking_count: 0, disabled_count: 0)
- @blocking_count = blocking_count
- @disabled_count = disabled_count
- end
-
- def execute
- conditions = -> { merge(::Environment.available).merge(::Deployment.success).where(key: ADO_MODSEC_KEY) }
-
- ci_pipeline_var_enabled =
- ::Ci::PipelineVariable
- .joins(pipeline: { environments: :last_visible_deployment })
- .merge(conditions)
- .order('deployments.environment_id, deployments.id DESC')
-
- ci_var_enabled =
- ::Ci::Variable
- .joins(project: { environments: :last_visible_deployment })
- .merge(conditions)
- .merge(
- # Give priority to pipeline variables by excluding from dataset
- ::Ci::Variable.joins(project: :environments).where.not(
- environments: { id: ci_pipeline_var_enabled.select('DISTINCT ON (deployments.environment_id) deployments.environment_id') }
- )
- ).select('DISTINCT ON (deployments.environment_id) ci_variables.*')
-
- sum_modsec_config_counts(
- ci_pipeline_var_enabled.select('DISTINCT ON (deployments.environment_id) ci_pipeline_variables.*')
- )
- sum_modsec_config_counts(ci_var_enabled)
-
- {
- ingress_modsecurity_blocking: @blocking_count,
- ingress_modsecurity_disabled: @disabled_count
- }
- end
-
- private
-
- # These are encrypted so we must decrypt and count in memory
- def sum_modsec_config_counts(dataset)
- dataset.each do |var|
- case var.value
- when "On" then @blocking_count += 1
- when "Off" then @disabled_count += 1
- # `else` could be default or any unsupported user input
- end
- end
- end
- end
- end
-end
diff --git a/app/services/clusters/applications/schedule_update_service.rb b/app/services/clusters/applications/schedule_update_service.rb
index b7639c771a8..41718df9a98 100644
--- a/app/services/clusters/applications/schedule_update_service.rb
+++ b/app/services/clusters/applications/schedule_update_service.rb
@@ -16,9 +16,9 @@ module Clusters
return unless application
if recently_scheduled?
- worker_class.perform_in(BACKOFF_DELAY, application.name, application.id, project.id, Time.now)
+ worker_class.perform_in(BACKOFF_DELAY, application.name, application.id, project.id, Time.current)
else
- worker_class.perform_async(application.name, application.id, project.id, Time.now)
+ worker_class.perform_async(application.name, application.id, project.id, Time.current)
end
end
@@ -31,7 +31,7 @@ module Clusters
def recently_scheduled?
return false unless application.last_update_started_at
- application.last_update_started_at.utc >= Time.now.utc - BACKOFF_DELAY
+ application.last_update_started_at.utc >= Time.current.utc - BACKOFF_DELAY
end
end
end
diff --git a/app/services/clusters/gcp/verify_provision_status_service.rb b/app/services/clusters/gcp/verify_provision_status_service.rb
index b24246f5c4b..ddb2832aae6 100644
--- a/app/services/clusters/gcp/verify_provision_status_service.rb
+++ b/app/services/clusters/gcp/verify_provision_status_service.rb
@@ -35,7 +35,7 @@ module Clusters
end
def elapsed_time_from_creation(operation)
- Time.now.utc - operation.start_time.to_time.utc
+ Time.current.utc - operation.start_time.to_time.utc
end
def finalize_creation
diff --git a/app/services/clusters/kubernetes/configure_istio_ingress_service.rb b/app/services/clusters/kubernetes/configure_istio_ingress_service.rb
index a81014d99ff..53c3c686f07 100644
--- a/app/services/clusters/kubernetes/configure_istio_ingress_service.rb
+++ b/app/services/clusters/kubernetes/configure_istio_ingress_service.rb
@@ -54,8 +54,8 @@ module Clusters
cert = OpenSSL::X509::Certificate.new
cert.version = 2
cert.serial = 0
- cert.not_before = Time.now
- cert.not_after = Time.now + 1000.years
+ cert.not_before = Time.current
+ cert.not_after = Time.current + 1000.years
cert.public_key = key.public_key
cert.subject = name
diff --git a/app/services/clusters/management/create_project_service.rb b/app/services/clusters/management/create_project_service.rb
index 0a33582be98..5a0176edd12 100644
--- a/app/services/clusters/management/create_project_service.rb
+++ b/app/services/clusters/management/create_project_service.rb
@@ -15,11 +15,8 @@ module Clusters
def execute
return unless management_project_required?
- ActiveRecord::Base.transaction do
- project = create_management_project!
-
- update_cluster!(project)
- end
+ project = create_management_project!
+ update_cluster!(project)
end
private
diff --git a/app/services/clusters/parse_cluster_applications_artifact_service.rb b/app/services/clusters/parse_cluster_applications_artifact_service.rb
new file mode 100644
index 00000000000..b8e1c80cfe7
--- /dev/null
+++ b/app/services/clusters/parse_cluster_applications_artifact_service.rb
@@ -0,0 +1,95 @@
+# frozen_string_literal: true
+
+module Clusters
+ class ParseClusterApplicationsArtifactService < ::BaseService
+ include Gitlab::Utils::StrongMemoize
+
+ MAX_ACCEPTABLE_ARTIFACT_SIZE = 5.kilobytes
+ RELEASE_NAMES = %w[prometheus].freeze
+
+ def initialize(job, current_user)
+ @job = job
+
+ super(job.project, current_user)
+ end
+
+ def execute(artifact)
+ return success unless Feature.enabled?(:cluster_applications_artifact, project)
+
+ raise ArgumentError, 'Artifact is not cluster_applications file type' unless artifact&.cluster_applications?
+
+ unless artifact.file.size < MAX_ACCEPTABLE_ARTIFACT_SIZE
+ return error(too_big_error_message, :bad_request)
+ end
+
+ unless cluster
+ return error(s_('ClusterIntegration|No deployment cluster found for this job'))
+ end
+
+ parse!(artifact)
+
+ success
+ rescue Gitlab::Kubernetes::Helm::Parsers::ListV2::ParserError, ActiveRecord::RecordInvalid => error
+ Gitlab::ErrorTracking.track_exception(error, job_id: artifact.job_id)
+ error(error.message, :bad_request)
+ end
+
+ private
+
+ attr_reader :job
+
+ def cluster
+ strong_memoize(:cluster) do
+ deployment_cluster = job.deployment&.cluster
+
+ deployment_cluster if Ability.allowed?(current_user, :admin_cluster, deployment_cluster)
+ end
+ end
+
+ def parse!(artifact)
+ releases = []
+
+ artifact.each_blob do |blob|
+ releases.concat(Gitlab::Kubernetes::Helm::Parsers::ListV2.new(blob).releases)
+ end
+
+ update_cluster_application_statuses!(releases)
+ end
+
+ def update_cluster_application_statuses!(releases)
+ release_by_name = releases.index_by { |release| release['Name'] }
+
+ Clusters::Cluster.transaction do
+ RELEASE_NAMES.each do |release_name|
+ application = find_or_build_application(release_name)
+
+ release = release_by_name[release_name]
+
+ if release
+ case release['Status']
+ when 'DEPLOYED'
+ application.make_externally_installed!
+ when 'FAILED'
+ application.make_errored!(s_('ClusterIntegration|Helm release failed to install'))
+ end
+ else
+ # missing, so by definition, we consider this uninstalled
+ application.make_externally_uninstalled! if application.persisted?
+ end
+ end
+ end
+ end
+
+ def find_or_build_application(application_name)
+ application_class = Clusters::Cluster::APPLICATIONS[application_name]
+
+ cluster.find_or_build_application(application_class)
+ end
+
+ def too_big_error_message
+ human_size = ActiveSupport::NumberHelper.number_to_human_size(MAX_ACCEPTABLE_ARTIFACT_SIZE)
+
+ s_('ClusterIntegration|Cluster_applications artifact too big. Maximum allowable size: %{human_size}') % { human_size: human_size }
+ end
+ end
+end
diff --git a/app/services/concerns/base_service_utility.rb b/app/services/concerns/base_service_utility.rb
new file mode 100644
index 00000000000..70b223a0289
--- /dev/null
+++ b/app/services/concerns/base_service_utility.rb
@@ -0,0 +1,72 @@
+# frozen_string_literal: true
+
+module BaseServiceUtility
+ extend ActiveSupport::Concern
+ include Gitlab::Allowable
+
+ ### Convenience service methods
+
+ def notification_service
+ NotificationService.new
+ end
+
+ def event_service
+ EventCreateService.new
+ end
+
+ def todo_service
+ TodoService.new
+ end
+
+ def system_hook_service
+ SystemHooksService.new
+ end
+
+ # Logging
+
+ def log_info(message)
+ Gitlab::AppLogger.info message
+ end
+
+ def log_error(message)
+ Gitlab::AppLogger.error message
+ end
+
+ # Add an error to the specified model for restricted visibility levels
+ def deny_visibility_level(model, denied_visibility_level = nil)
+ denied_visibility_level ||= model.visibility_level
+
+ level_name = Gitlab::VisibilityLevel.level_name(denied_visibility_level).downcase
+
+ model.errors.add(:visibility_level, "#{level_name} has been restricted by your GitLab administrator")
+ end
+
+ def visibility_level
+ params[:visibility].is_a?(String) ? Gitlab::VisibilityLevel.level_value(params[:visibility]) : params[:visibility_level]
+ end
+
+ private
+
+ # Return a Hash with an `error` status
+ #
+ # message - Error message to include in the Hash
+ # http_status - Optional HTTP status code override (default: nil)
+ # pass_back - Additional attributes to be included in the resulting Hash
+ def error(message, http_status = nil, pass_back: {})
+ result = {
+ message: message,
+ status: :error
+ }.reverse_merge(pass_back)
+
+ result[:http_status] = http_status if http_status
+ result
+ end
+
+ # Return a Hash with a `success` status
+ #
+ # pass_back - Additional attributes to be included in the resulting Hash
+ def success(pass_back = {})
+ pass_back[:status] = :success
+ pass_back
+ end
+end
diff --git a/app/services/concerns/git/logger.rb b/app/services/concerns/git/logger.rb
deleted file mode 100644
index 7c036212e66..00000000000
--- a/app/services/concerns/git/logger.rb
+++ /dev/null
@@ -1,10 +0,0 @@
-# frozen_string_literal: true
-
-module Git
- module Logger
- def log_error(message, save_message_on_model: false)
- Gitlab::GitLogger.error("#{self.class.name} error (#{merge_request.to_reference(full: true)}): #{message}")
- merge_request.update(merge_error: message) if save_message_on_model
- end
- end
-end
diff --git a/app/services/concerns/measurable.rb b/app/services/concerns/measurable.rb
new file mode 100644
index 00000000000..5a74f15506e
--- /dev/null
+++ b/app/services/concerns/measurable.rb
@@ -0,0 +1,61 @@
+# frozen_string_literal: true
+
+# In order to measure and log execution of our service, we just need to 'prepend Measurable' module
+# Example:
+# ```
+# class DummyService
+# prepend Measurable
+#
+# def execute
+# # ...
+# end
+# end
+
+# DummyService.prepend(Measurable)
+# ```
+#
+# In case when we are prepending a module from the `EE` namespace with EE features
+# we need to prepend Measurable after prepending `EE` module.
+# This way Measurable will be at the bottom of the ancestor chain,
+# in order to measure execution of `EE` features as well
+# ```
+# class DummyService
+# def execute
+# # ...
+# end
+# end
+#
+# DummyService.prepend_if_ee('EE::DummyService')
+# DummyService.prepend(Measurable)
+# ```
+#
+module Measurable
+ extend ::Gitlab::Utils::Override
+
+ override :execute
+ def execute(*args)
+ measuring? ? ::Gitlab::Utils::Measuring.new(base_log_data).with_measuring { super(*args) } : super(*args)
+ end
+
+ protected
+
+ # You can set extra attributes for performance measurement log.
+ def extra_attributes_for_measurement
+ defined?(super) ? super : {}
+ end
+
+ private
+
+ def measuring?
+ Feature.enabled?("gitlab_service_measuring_#{service_class}")
+ end
+
+ # These attributes are always present in log.
+ def base_log_data
+ extra_attributes_for_measurement.merge({ class: self.class.name })
+ end
+
+ def service_class
+ self.class.name.underscore.tr('/', '_')
+ end
+end
diff --git a/app/services/concerns/spam_check_methods.rb b/app/services/concerns/spam_check_methods.rb
index 695bdf92b49..53e9e001463 100644
--- a/app/services/concerns/spam_check_methods.rb
+++ b/app/services/concerns/spam_check_methods.rb
@@ -23,14 +23,14 @@ module SpamCheckMethods
# attribute values.
# rubocop:disable Gitlab/ModuleWithInstanceVariables
def spam_check(spammable, user)
- Spam::SpamCheckService.new(
+ Spam::SpamActionService.new(
spammable: spammable,
request: @request
).execute(
api: @api,
recaptcha_verified: @recaptcha_verified,
spam_log_id: @spam_log_id,
- user_id: user.id)
+ user: user)
end
# rubocop:enable Gitlab/ModuleWithInstanceVariables
end
diff --git a/app/services/deployments/older_deployments_drop_service.rb b/app/services/deployments/older_deployments_drop_service.rb
index 122f8ac89ed..e765d2484ea 100644
--- a/app/services/deployments/older_deployments_drop_service.rb
+++ b/app/services/deployments/older_deployments_drop_service.rb
@@ -12,7 +12,9 @@ module Deployments
return unless @deployment&.running?
older_deployments.find_each do |older_deployment|
- older_deployment.deployable&.drop!(:forward_deployment_failure)
+ Gitlab::OptimisticLocking.retry_lock(older_deployment.deployable) do |deployable|
+ deployable.drop(:forward_deployment_failure)
+ end
rescue => e
Gitlab::ErrorTracking.track_exception(e, subject_id: @deployment.id, deployment_id: older_deployment.id)
end
diff --git a/app/services/design_management/delete_designs_service.rb b/app/services/design_management/delete_designs_service.rb
new file mode 100644
index 00000000000..e69f07db5bf
--- /dev/null
+++ b/app/services/design_management/delete_designs_service.rb
@@ -0,0 +1,66 @@
+# frozen_string_literal: true
+
+module DesignManagement
+ class DeleteDesignsService < DesignService
+ include RunsDesignActions
+ include OnSuccessCallbacks
+
+ def initialize(project, user, params = {})
+ super
+
+ @designs = params.fetch(:designs)
+ end
+
+ def execute
+ return error('Forbidden!') unless can_delete_designs?
+
+ version = delete_designs!
+
+ success(version: version)
+ end
+
+ def commit_message
+ n = designs.size
+
+ <<~MSG
+ Removed #{n} #{'designs'.pluralize(n)}
+
+ #{formatted_file_list}
+ MSG
+ end
+
+ private
+
+ attr_reader :designs
+
+ def delete_designs!
+ DesignManagement::Version.with_lock(project.id, repository) do
+ run_actions(build_actions)
+ end
+ end
+
+ def can_delete_designs?
+ Ability.allowed?(current_user, :destroy_design, issue)
+ end
+
+ def build_actions
+ designs.map { |d| design_action(d) }
+ end
+
+ def design_action(design)
+ on_success { counter.count(:delete) }
+
+ DesignManagement::DesignAction.new(design, :delete)
+ end
+
+ def counter
+ ::Gitlab::UsageDataCounters::DesignsCounter
+ end
+
+ def formatted_file_list
+ designs.map { |design| "- #{design.full_path}" }.join("\n")
+ end
+ end
+end
+
+DesignManagement::DeleteDesignsService.prepend_if_ee('EE::DesignManagement::DeleteDesignsService')
diff --git a/app/services/design_management/design_service.rb b/app/services/design_management/design_service.rb
new file mode 100644
index 00000000000..54e53609646
--- /dev/null
+++ b/app/services/design_management/design_service.rb
@@ -0,0 +1,31 @@
+# frozen_string_literal: true
+
+module DesignManagement
+ class DesignService < ::BaseService
+ def initialize(project, user, params = {})
+ super
+
+ @issue = params.fetch(:issue)
+ end
+
+ # Accessors common to all subclasses:
+
+ attr_reader :issue
+
+ def target_branch
+ repository.root_ref || "master"
+ end
+
+ def collection
+ issue.design_collection
+ end
+
+ def repository
+ collection.repository
+ end
+
+ def project
+ issue.project
+ end
+ end
+end
diff --git a/app/services/design_management/design_user_notes_count_service.rb b/app/services/design_management/design_user_notes_count_service.rb
new file mode 100644
index 00000000000..e49914ea6d3
--- /dev/null
+++ b/app/services/design_management/design_user_notes_count_service.rb
@@ -0,0 +1,34 @@
+# frozen_string_literal: true
+
+module DesignManagement
+ # Service class for counting and caching the number of unresolved
+ # notes of a Design
+ class DesignUserNotesCountService < ::BaseCountService
+ # The version of the cache format. This should be bumped whenever the
+ # underlying logic changes. This removes the need for explicitly flushing
+ # all caches.
+ VERSION = 1
+
+ def initialize(design)
+ @design = design
+ end
+
+ def relation_for_count
+ design.notes.user
+ end
+
+ def raw?
+ # Since we're storing simple integers we don't need all of the
+ # additional Marshal data Rails includes by default.
+ true
+ end
+
+ def cache_key
+ ['designs', 'notes_count', VERSION, design.id]
+ end
+
+ private
+
+ attr_reader :design
+ end
+end
diff --git a/app/services/design_management/generate_image_versions_service.rb b/app/services/design_management/generate_image_versions_service.rb
new file mode 100644
index 00000000000..213aac164ff
--- /dev/null
+++ b/app/services/design_management/generate_image_versions_service.rb
@@ -0,0 +1,99 @@
+# frozen_string_literal: true
+
+module DesignManagement
+ # This service generates smaller image versions for `DesignManagement::Design`
+ # records within a given `DesignManagement::Version`.
+ class GenerateImageVersionsService < DesignService
+ # We limit processing to only designs with file sizes that don't
+ # exceed `MAX_DESIGN_SIZE`.
+ #
+ # Note, we may be able to remove checking this limit, if when we come to
+ # implement a file size limit for designs, there are no designs that
+ # exceed 40MB on GitLab.com
+ #
+ # See https://gitlab.com/gitlab-org/gitlab/-/merge_requests/22860#note_281780387
+ MAX_DESIGN_SIZE = 40.megabytes.freeze
+
+ def initialize(version)
+ super(version.project, version.author, issue: version.issue)
+
+ @version = version
+ end
+
+ def execute
+ # rubocop: disable CodeReuse/ActiveRecord
+ version.actions.includes(:design).each do |action|
+ generate_image(action)
+ end
+ # rubocop: enable CodeReuse/ActiveRecord
+
+ success(version: version)
+ end
+
+ private
+
+ attr_reader :version
+
+ def generate_image(action)
+ raw_file = get_raw_file(action)
+
+ unless raw_file
+ log_error("No design file found for Action: #{action.id}")
+ return
+ end
+
+ # Skip attempting to process images that would be rejected by CarrierWave.
+ return unless DesignManagement::DesignV432x230Uploader::MIME_TYPE_WHITELIST.include?(raw_file.content_type)
+
+ # Store and process the file
+ action.image_v432x230.store!(raw_file)
+ action.save!
+ rescue CarrierWave::UploadError => e
+ Gitlab::ErrorTracking.track_exception(e, project_id: project.id, design_id: action.design_id, version_id: action.version_id)
+ log_error(e.message)
+ end
+
+ # Returns the `CarrierWave::SanitizedFile` of the original design file
+ def get_raw_file(action)
+ raw_files_by_path[action.design.full_path]
+ end
+
+ # Returns the `Carrierwave:SanitizedFile` instances for all of the original
+ # design files, mapping to { design.filename => `Carrierwave::SanitizedFile` }.
+ #
+ # As design files are stored in Git LFS, the only way to retrieve their original
+ # files is to first fetch the LFS pointer file data from the Git design repository.
+ # The LFS pointer file data contains an "OID" that lets us retrieve `LfsObject`
+ # records, which have an Uploader (`LfsObjectUploader`) for the original design file.
+ def raw_files_by_path
+ @raw_files_by_path ||= begin
+ LfsObject.for_oids(blobs_by_oid.keys).each_with_object({}) do |lfs_object, h|
+ blob = blobs_by_oid[lfs_object.oid]
+ file = lfs_object.file.file
+ # The `CarrierWave::SanitizedFile` is loaded without knowing the `content_type`
+ # of the file, due to the file not having an extension.
+ #
+ # Set the content_type from the `Blob`.
+ file.content_type = blob.content_type
+ h[blob.path] = file
+ end
+ end
+ end
+
+ # Returns the `Blob`s that correspond to the design files in the repository.
+ #
+ # All design `Blob`s are LFS Pointer files, and are therefore small amounts
+ # of data to load.
+ #
+ # `Blob`s whose size are above a certain threshold: `MAX_DESIGN_SIZE`
+ # are filtered out.
+ def blobs_by_oid
+ @blobs ||= begin
+ items = version.designs.map { |design| [version.sha, design.full_path] }
+ blobs = repository.blobs_at(items)
+ blobs.reject! { |blob| blob.lfs_size > MAX_DESIGN_SIZE }
+ blobs.index_by(&:lfs_oid)
+ end
+ end
+ end
+end
diff --git a/app/services/design_management/on_success_callbacks.rb b/app/services/design_management/on_success_callbacks.rb
new file mode 100644
index 00000000000..be55890a02d
--- /dev/null
+++ b/app/services/design_management/on_success_callbacks.rb
@@ -0,0 +1,23 @@
+# frozen_string_literal: true
+
+module DesignManagement
+ module OnSuccessCallbacks
+ def on_success(&block)
+ success_callbacks.push(block)
+ end
+
+ def success(*_)
+ while cb = success_callbacks.pop
+ cb.call
+ end
+
+ super
+ end
+
+ private
+
+ def success_callbacks
+ @success_callbacks ||= []
+ end
+ end
+end
diff --git a/app/services/design_management/runs_design_actions.rb b/app/services/design_management/runs_design_actions.rb
new file mode 100644
index 00000000000..4bd6bb45658
--- /dev/null
+++ b/app/services/design_management/runs_design_actions.rb
@@ -0,0 +1,35 @@
+# frozen_string_literal: true
+
+module DesignManagement
+ module RunsDesignActions
+ NoActions = Class.new(StandardError)
+
+ # this concern requires the following methods to be implemented:
+ # current_user, target_branch, repository, commit_message
+ #
+ # Before calling `run_actions`, you should ensure the repository exists, by
+ # calling `repository.create_if_not_exists`.
+ #
+ # @raise [NoActions] if actions are empty
+ def run_actions(actions)
+ raise NoActions if actions.empty?
+
+ sha = repository.multi_action(current_user,
+ branch_name: target_branch,
+ message: commit_message,
+ actions: actions.map(&:gitaly_action))
+
+ ::DesignManagement::Version
+ .create_for_designs(actions, sha, current_user)
+ .tap { |version| post_process(version) }
+ end
+
+ private
+
+ def post_process(version)
+ version.run_after_commit_or_now do
+ ::DesignManagement::NewVersionWorker.perform_async(id)
+ end
+ end
+ end
+end
diff --git a/app/services/design_management/save_designs_service.rb b/app/services/design_management/save_designs_service.rb
new file mode 100644
index 00000000000..a09c19bc885
--- /dev/null
+++ b/app/services/design_management/save_designs_service.rb
@@ -0,0 +1,114 @@
+# frozen_string_literal: true
+
+module DesignManagement
+ class SaveDesignsService < DesignService
+ include RunsDesignActions
+ include OnSuccessCallbacks
+
+ MAX_FILES = 10
+
+ def initialize(project, user, params = {})
+ super
+
+ @files = params.fetch(:files)
+ end
+
+ def execute
+ return error("Not allowed!") unless can_create_designs?
+ return error("Only #{MAX_FILES} files are allowed simultaneously") if files.size > MAX_FILES
+
+ uploaded_designs, version = upload_designs!
+ skipped_designs = designs - uploaded_designs
+
+ success({ designs: uploaded_designs, version: version, skipped_designs: skipped_designs })
+ rescue ::ActiveRecord::RecordInvalid => e
+ error(e.message)
+ end
+
+ private
+
+ attr_reader :files
+
+ def upload_designs!
+ ::DesignManagement::Version.with_lock(project.id, repository) do
+ actions = build_actions
+
+ [actions.map(&:design), actions.presence && run_actions(actions)]
+ end
+ end
+
+ # Returns `Design` instances that correspond with `files`.
+ # New `Design`s will be created where a file name does not match
+ # an existing `Design`
+ def designs
+ @designs ||= files.map do |file|
+ collection.find_or_create_design!(filename: file.original_filename)
+ end
+ end
+
+ def build_actions
+ files.zip(designs).flat_map do |(file, design)|
+ Array.wrap(build_design_action(file, design))
+ end
+ end
+
+ def build_design_action(file, design)
+ content = file_content(file, design.full_path)
+ return if design_unchanged?(design, content)
+
+ action = new_file?(design) ? :create : :update
+ on_success { ::Gitlab::UsageDataCounters::DesignsCounter.count(action) }
+
+ DesignManagement::DesignAction.new(design, action, content)
+ end
+
+ # Returns true if the design file is the same as its latest version
+ def design_unchanged?(design, content)
+ content == existing_blobs[design]&.data
+ end
+
+ def commit_message
+ <<~MSG
+ Updated #{files.size} #{'designs'.pluralize(files.size)}
+
+ #{formatted_file_list}
+ MSG
+ end
+
+ def formatted_file_list
+ filenames.map { |name| "- #{name}" }.join("\n")
+ end
+
+ def filenames
+ @filenames ||= files.map(&:original_filename)
+ end
+
+ def can_create_designs?
+ Ability.allowed?(current_user, :create_design, issue)
+ end
+
+ def new_file?(design)
+ !existing_blobs[design]
+ end
+
+ def file_content(file, full_path)
+ transformer = ::Lfs::FileTransformer.new(project, repository, target_branch)
+ transformer.new_file(full_path, file.to_io).content
+ end
+
+ # Returns the latest blobs for the designs as a Hash of `{ Design => Blob }`
+ def existing_blobs
+ @existing_blobs ||= begin
+ items = designs.map { |d| ['HEAD', d.full_path] }
+
+ repository.blobs_at(items).each_with_object({}) do |blob, h|
+ design = designs.find { |d| d.full_path == blob.path }
+
+ h[design] = blob
+ end
+ end
+ end
+ end
+end
+
+DesignManagement::SaveDesignsService.prepend_if_ee('EE::DesignManagement::SaveDesignsService')
diff --git a/app/services/emails/base_service.rb b/app/services/emails/base_service.rb
index 99324638300..c94505b2068 100644
--- a/app/services/emails/base_service.rb
+++ b/app/services/emails/base_service.rb
@@ -11,3 +11,5 @@ module Emails
end
end
end
+
+Emails::BaseService.prepend_if_ee('::EE::Emails::BaseService')
diff --git a/app/services/event_create_service.rb b/app/services/event_create_service.rb
index 0b044e1679a..522f36cda46 100644
--- a/app/services/event_create_service.rb
+++ b/app/services/event_create_service.rb
@@ -85,18 +85,40 @@ class EventCreateService
# Create a new wiki page event
#
# @param [WikiPage::Meta] wiki_page_meta The event target
- # @param [User] current_user The event author
+ # @param [User] author The event author
# @param [Integer] action One of the Event::WIKI_ACTIONS
- def wiki_event(wiki_page_meta, current_user, action)
+ #
+ # @return a tuple of event and either :found or :created
+ def wiki_event(wiki_page_meta, author, action)
return unless Feature.enabled?(:wiki_events)
raise IllegalActionError, action unless Event::WIKI_ACTIONS.include?(action)
- create_record_event(wiki_page_meta, current_user, action)
+ if duplicate = existing_wiki_event(wiki_page_meta, action)
+ return duplicate
+ end
+
+ event = create_record_event(wiki_page_meta, author, action)
+ # Ensure that the event is linked in time to the metadata, for non-deletes
+ unless action == Event::DESTROYED
+ time_stamp = wiki_page_meta.updated_at
+ event.update_columns(updated_at: time_stamp, created_at: time_stamp)
+ end
+
+ event
end
private
+ def existing_wiki_event(wiki_page_meta, action)
+ if action == Event::DESTROYED
+ most_recent = Event.for_wiki_meta(wiki_page_meta).recent.first
+ return most_recent if most_recent.present? && most_recent.action == action
+ else
+ Event.for_wiki_meta(wiki_page_meta).created_at(wiki_page_meta.updated_at).first
+ end
+ end
+
def create_record_event(record, current_user, status)
create_event(record.resource_parent, current_user, status, target_id: record.id, target_type: record.class.name)
end
diff --git a/app/services/git/branch_hooks_service.rb b/app/services/git/branch_hooks_service.rb
index e1cc1f8c834..92e7702727c 100644
--- a/app/services/git/branch_hooks_service.rb
+++ b/app/services/git/branch_hooks_service.rb
@@ -112,7 +112,7 @@ module Git
end
def enqueue_update_signatures
- unsigned = unsigned_x509_shas(commits) & unsigned_gpg_shas(commits)
+ unsigned = unsigned_x509_shas(limited_commits) & unsigned_gpg_shas(limited_commits)
return if unsigned.empty?
signable = Gitlab::Git::Commit.shas_with_signatures(project.repository, unsigned)
diff --git a/app/services/git/wiki_push_service.rb b/app/services/git/wiki_push_service.rb
index d4267d4a3c5..8bdbc28f3e8 100644
--- a/app/services/git/wiki_push_service.rb
+++ b/app/services/git/wiki_push_service.rb
@@ -2,8 +2,63 @@
module Git
class WikiPushService < ::BaseService
+ # Maximum number of change events we will process on any single push
+ MAX_CHANGES = 100
+
def execute
- # This is used in EE
+ process_changes
+ end
+
+ private
+
+ def process_changes
+ return unless can_process_wiki_events?
+
+ push_changes.take(MAX_CHANGES).each do |change| # rubocop:disable CodeReuse/ActiveRecord
+ next unless change.page.present?
+
+ response = create_event_for(change)
+ log_error(response.message) if response.error?
+ end
+ end
+
+ def can_process_wiki_events?
+ Feature.enabled?(:wiki_events) && Feature.enabled?(:wiki_events_on_git_push, project)
+ end
+
+ def push_changes
+ default_branch_changes.flat_map do |change|
+ raw_changes(change).map { |raw| Git::WikiPushService::Change.new(wiki, change, raw) }
+ end
+ end
+
+ def raw_changes(change)
+ wiki.repository.raw.raw_changes_between(change[:oldrev], change[:newrev])
+ end
+
+ def wiki
+ project.wiki
+ end
+
+ def create_event_for(change)
+ event_service.execute(change.last_known_slug, change.page, change.event_action)
+ end
+
+ def event_service
+ @event_service ||= WikiPages::EventCreateService.new(current_user)
+ end
+
+ def on_default_branch?(change)
+ project.wiki.default_branch == ::Gitlab::Git.branch_name(change[:ref])
+ end
+
+ # See: [Gitlab::GitPostReceive#changes]
+ def changes
+ params[:changes] || []
+ end
+
+ def default_branch_changes
+ @default_branch_changes ||= changes.select { |change| on_default_branch?(change) }
end
end
end
diff --git a/app/services/git/wiki_push_service/change.rb b/app/services/git/wiki_push_service/change.rb
new file mode 100644
index 00000000000..8685850165a
--- /dev/null
+++ b/app/services/git/wiki_push_service/change.rb
@@ -0,0 +1,67 @@
+# frozen_string_literal: true
+
+module Git
+ class WikiPushService
+ class Change
+ include Gitlab::Utils::StrongMemoize
+
+ # @param [ProjectWiki] wiki
+ # @param [Hash] change - must have keys `:oldrev` and `:newrev`
+ # @param [Gitlab::Git::RawDiffChange] raw_change
+ def initialize(project_wiki, change, raw_change)
+ @wiki, @raw_change, @change = project_wiki, raw_change, change
+ end
+
+ def page
+ strong_memoize(:page) { wiki.find_page(slug, revision) }
+ end
+
+ # See [Gitlab::Git::RawDiffChange#extract_operation] for the
+ # definition of the full range of operation values.
+ def event_action
+ case raw_change.operation
+ when :added
+ Event::CREATED
+ when :deleted
+ Event::DESTROYED
+ else
+ Event::UPDATED
+ end
+ end
+
+ def last_known_slug
+ strip_extension(raw_change.old_path || raw_change.new_path)
+ end
+
+ private
+
+ attr_reader :raw_change, :change, :wiki
+
+ def filename
+ return raw_change.old_path if deleted?
+
+ raw_change.new_path
+ end
+
+ def slug
+ strip_extension(filename)
+ end
+
+ def revision
+ return change[:oldrev] if deleted?
+
+ change[:newrev]
+ end
+
+ def deleted?
+ raw_change.operation == :deleted
+ end
+
+ def strip_extension(filename)
+ return unless filename
+
+ File.basename(filename, File.extname(filename))
+ end
+ end
+ end
+end
diff --git a/app/services/grafana/proxy_service.rb b/app/services/grafana/proxy_service.rb
index 74fcdc750b0..ac4c3cc091c 100644
--- a/app/services/grafana/proxy_service.rb
+++ b/app/services/grafana/proxy_service.rb
@@ -12,6 +12,7 @@ module Grafana
self.reactive_cache_key = ->(service) { service.cache_key }
self.reactive_cache_lease_timeout = 30.seconds
self.reactive_cache_refresh_interval = 30.seconds
+ self.reactive_cache_work_type = :external_dependency
self.reactive_cache_worker_finder = ->(_id, *args) { from_cache(*args) }
attr_accessor :project, :datasource_id, :proxy_path, :query_params
diff --git a/app/services/groups/create_service.rb b/app/services/groups/create_service.rb
index 8cc31200689..eb1b8d4fcc0 100644
--- a/app/services/groups/create_service.rb
+++ b/app/services/groups/create_service.rb
@@ -38,6 +38,10 @@ module Groups
# overridden in EE
end
+ def remove_unallowed_params
+ params.delete(:default_branch_protection) unless can?(current_user, :create_group_with_default_branch_protection)
+ end
+
def create_chat_team?
Gitlab.config.mattermost.enabled && @chat_team && group.chat_team.nil?
end
diff --git a/app/services/groups/import_export/export_service.rb b/app/services/groups/import_export/export_service.rb
index f8715b57d6e..0f2e3bb65f9 100644
--- a/app/services/groups/import_export/export_service.rb
+++ b/app/services/groups/import_export/export_service.rb
@@ -52,11 +52,11 @@ module Groups
end
def savers
- [tree_exporter, file_saver]
+ [version_saver, tree_exporter, file_saver]
end
def tree_exporter
- Gitlab::ImportExport::Group::LegacyTreeSaver.new(
+ tree_exporter_class.new(
group: @group,
current_user: @current_user,
shared: @shared,
@@ -64,6 +64,18 @@ module Groups
)
end
+ def tree_exporter_class
+ if ::Feature.enabled?(:group_export_ndjson, @group&.parent, default_enabled: true)
+ Gitlab::ImportExport::Group::TreeSaver
+ else
+ Gitlab::ImportExport::Group::LegacyTreeSaver
+ end
+ end
+
+ def version_saver
+ Gitlab::ImportExport::VersionSaver.new(shared: shared)
+ end
+
def file_saver
Gitlab::ImportExport::Saver.new(exportable: @group, shared: @shared)
end
@@ -84,6 +96,8 @@ module Groups
group_name: @group.name,
message: 'Group Import/Export: Export succeeded'
)
+
+ notification_service.group_was_exported(@group, @current_user)
end
def notify_error
@@ -93,6 +107,12 @@ module Groups
error: @shared.errors.join(', '),
message: 'Group Import/Export: Export failed'
)
+
+ notification_service.group_was_not_exported(@group, @current_user, @shared.errors)
+ end
+
+ def notification_service
+ @notification_service ||= NotificationService.new
end
end
end
diff --git a/app/services/groups/import_export/import_service.rb b/app/services/groups/import_export/import_service.rb
index f62b9d3c8a6..6f692c98c38 100644
--- a/app/services/groups/import_export/import_service.rb
+++ b/app/services/groups/import_export/import_service.rb
@@ -27,18 +27,34 @@ module Groups
private
def import_file
- @import_file ||= Gitlab::ImportExport::FileImporter.import(importable: @group,
- archive_file: nil,
- shared: @shared)
+ @import_file ||= Gitlab::ImportExport::FileImporter.import(
+ importable: @group,
+ archive_file: nil,
+ shared: @shared
+ )
end
def restorer
- @restorer ||= Gitlab::ImportExport::Group::LegacyTreeRestorer.new(
- user: @current_user,
- shared: @shared,
- group: @group,
- group_hash: nil
- )
+ @restorer ||=
+ if ndjson?
+ Gitlab::ImportExport::Group::TreeRestorer.new(
+ user: @current_user,
+ shared: @shared,
+ group: @group
+ )
+ else
+ Gitlab::ImportExport::Group::LegacyTreeRestorer.new(
+ user: @current_user,
+ shared: @shared,
+ group: @group,
+ group_hash: nil
+ )
+ end
+ end
+
+ def ndjson?
+ ::Feature.enabled?(:group_import_ndjson, @group&.parent, default_enabled: true) &&
+ File.exist?(File.join(@shared.export_path, 'tree/groups/_all.ndjson'))
end
def remove_import_file
diff --git a/app/services/groups/update_service.rb b/app/services/groups/update_service.rb
index 8635b82461b..948540619ae 100644
--- a/app/services/groups/update_service.rb
+++ b/app/services/groups/update_service.rb
@@ -66,6 +66,7 @@ module Groups
# overridden in EE
def remove_unallowed_params
params.delete(:emails_disabled) unless can?(current_user, :set_emails_disabled, group)
+ params.delete(:default_branch_protection) unless can?(current_user, :update_default_branch_protection, group)
end
def valid_share_with_group_lock_change?
diff --git a/app/services/incident_management/create_issue_service.rb b/app/services/incident_management/create_issue_service.rb
index 43077e03e6d..4b59dc64cec 100644
--- a/app/services/incident_management/create_issue_service.rb
+++ b/app/services/incident_management/create_issue_service.rb
@@ -13,12 +13,12 @@ module IncidentManagement
DESCRIPTION
}.freeze
- def initialize(project, params)
- super(project, User.alert_bot, params)
+ def initialize(project, params, user = User.alert_bot)
+ super(project, user, params)
end
- def execute
- return error_with('setting disabled') unless incident_management_setting.create_issue?
+ def execute(skip_settings_check: false)
+ return error_with('setting disabled') unless skip_settings_check || incident_management_setting.create_issue?
return error_with('invalid alert') unless alert.valid?
issue = create_issue
diff --git a/app/services/issuable/clone/attributes_rewriter.rb b/app/services/issuable/clone/attributes_rewriter.rb
index 55f5629baac..a78e191c85f 100644
--- a/app/services/issuable/clone/attributes_rewriter.rb
+++ b/app/services/issuable/clone/attributes_rewriter.rb
@@ -20,6 +20,7 @@ module Issuable
copy_resource_label_events
copy_resource_weight_events
copy_resource_milestone_events
+ copy_resource_state_events
end
private
@@ -47,8 +48,6 @@ module Issuable
end
def copy_resource_label_events
- entity_key = new_entity.class.name.underscore.foreign_key
-
copy_events(ResourceLabelEvent.table_name, original_entity.resource_label_events) do |event|
event.attributes
.except('id', 'reference', 'reference_html')
@@ -67,22 +66,39 @@ module Issuable
end
def copy_resource_milestone_events
- entity_key = new_entity.class.name.underscore.foreign_key
+ return unless milestone_events_supported?
copy_events(ResourceMilestoneEvent.table_name, original_entity.resource_milestone_events) do |event|
- matching_destination_milestone = matching_milestone(event.milestone.title)
-
- if matching_destination_milestone.present?
- event.attributes
- .except('id')
- .merge(entity_key => new_entity.id,
- 'milestone_id' => matching_destination_milestone.id,
- 'action' => ResourceMilestoneEvent.actions[event.action],
- 'state' => ResourceMilestoneEvent.states[event.state])
+ if event.remove?
+ event_attributes_with_milestone(event, nil)
+ else
+ matching_destination_milestone = matching_milestone(event.milestone_title)
+
+ event_attributes_with_milestone(event, matching_destination_milestone) if matching_destination_milestone.present?
end
end
end
+ def copy_resource_state_events
+ return unless state_events_supported?
+
+ copy_events(ResourceStateEvent.table_name, original_entity.resource_state_events) do |event|
+ event.attributes
+ .except('id')
+ .merge(entity_key => new_entity.id,
+ 'state' => ResourceStateEvent.states[event.state])
+ end
+ end
+
+ def event_attributes_with_milestone(event, milestone)
+ event.attributes
+ .except('id')
+ .merge(entity_key => new_entity.id,
+ 'milestone_id' => milestone&.id,
+ 'action' => ResourceMilestoneEvent.actions[event.action],
+ 'state' => ResourceMilestoneEvent.states[event.state])
+ end
+
def copy_events(table_name, events_to_copy)
events_to_copy.find_in_batches do |batch|
events = batch.map do |event|
@@ -94,7 +110,20 @@ module Issuable
end
def entity_key
- new_entity.class.name.parameterize('_').foreign_key
+ new_entity.class.name.underscore.foreign_key
+ end
+
+ def milestone_events_supported?
+ both_respond_to?(:resource_milestone_events)
+ end
+
+ def state_events_supported?
+ both_respond_to?(:resource_state_events)
+ end
+
+ def both_respond_to?(method)
+ original_entity.respond_to?(method) &&
+ new_entity.respond_to?(method)
end
end
end
diff --git a/app/services/issuable/clone/base_service.rb b/app/services/issuable/clone/base_service.rb
index 54576e82030..0d1640924e5 100644
--- a/app/services/issuable/clone/base_service.rb
+++ b/app/services/issuable/clone/base_service.rb
@@ -47,7 +47,7 @@ module Issuable
end
def new_parent
- new_entity.project ? new_entity.project : new_entity.group
+ new_entity.project || new_entity.group
end
def group
diff --git a/app/services/issuable/common_system_notes_service.rb b/app/services/issuable/common_system_notes_service.rb
index 67cf212691f..195616857dc 100644
--- a/app/services/issuable/common_system_notes_service.rb
+++ b/app/services/issuable/common_system_notes_service.rb
@@ -4,7 +4,7 @@ module Issuable
class CommonSystemNotesService < ::BaseService
attr_reader :issuable
- def execute(issuable, old_labels: [], is_update: true)
+ def execute(issuable, old_labels: [], old_milestone: nil, is_update: true)
@issuable = issuable
# We disable touch so that created system notes do not update
@@ -22,17 +22,13 @@ module Issuable
end
create_due_date_note if issuable.previous_changes.include?('due_date')
- create_milestone_note if has_milestone_changes?
+ create_milestone_note(old_milestone) if issuable.previous_changes.include?('milestone_id')
create_labels_note(old_labels) if old_labels && issuable.labels != old_labels
end
end
private
- def has_milestone_changes?
- issuable.previous_changes.include?('milestone_id')
- end
-
def handle_time_tracking_note
if issuable.previous_changes.include?('time_estimate')
create_time_estimate_note
@@ -98,15 +94,19 @@ module Issuable
SystemNoteService.change_time_spent(issuable, issuable.project, issuable.time_spent_user)
end
- def create_milestone_note
+ def create_milestone_note(old_milestone)
if milestone_changes_tracking_enabled?
- # Creates a synthetic note
- ResourceEvents::ChangeMilestoneService.new(issuable, current_user).execute
+ create_milestone_change_event(old_milestone)
else
SystemNoteService.change_milestone(issuable, issuable.project, current_user, issuable.milestone)
end
end
+ def create_milestone_change_event(old_milestone)
+ ResourceEvents::ChangeMilestoneService.new(issuable, current_user, old_milestone: old_milestone)
+ .execute
+ end
+
def milestone_changes_tracking_enabled?
::Feature.enabled?(:track_resource_milestone_change_events, issuable.project)
end
diff --git a/app/services/issuable_base_service.rb b/app/services/issuable_base_service.rb
index 506f4309a1e..18062bd60da 100644
--- a/app/services/issuable_base_service.rb
+++ b/app/services/issuable_base_service.rb
@@ -22,7 +22,9 @@ class IssuableBaseService < BaseService
params.delete(:milestone_id)
params.delete(:labels)
params.delete(:add_label_ids)
+ params.delete(:add_labels)
params.delete(:remove_label_ids)
+ params.delete(:remove_labels)
params.delete(:label_ids)
params.delete(:assignee_ids)
params.delete(:assignee_id)
@@ -91,6 +93,8 @@ class IssuableBaseService < BaseService
elsif params[label_key]
params[label_id_key] = labels_service.find_or_create_by_titles(label_key, find_only: find_only).map(&:id)
end
+
+ params.delete(label_key) if params[label_key].nil?
end
def filter_labels_in_param(key)
@@ -217,7 +221,7 @@ class IssuableBaseService < BaseService
issuable.assign_attributes(params)
if has_title_or_description_changed?(issuable)
- issuable.assign_attributes(last_edited_at: Time.now, last_edited_by: current_user)
+ issuable.assign_attributes(last_edited_at: Time.current, last_edited_by: current_user)
end
before_update(issuable)
@@ -237,7 +241,8 @@ class IssuableBaseService < BaseService
end
if issuable_saved
- Issuable::CommonSystemNotesService.new(project, current_user).execute(issuable, old_labels: old_associations[:labels])
+ Issuable::CommonSystemNotesService.new(project, current_user).execute(
+ issuable, old_labels: old_associations[:labels], old_milestone: old_associations[:milestone])
handle_changes(issuable, old_associations: old_associations)
@@ -265,7 +270,7 @@ class IssuableBaseService < BaseService
if issuable.changed? || params.present?
issuable.assign_attributes(params.merge(updated_by: current_user,
- last_edited_at: Time.now,
+ last_edited_at: Time.current,
last_edited_by: current_user))
before_update(issuable, skip_spam_check: true)
@@ -360,7 +365,8 @@ class IssuableBaseService < BaseService
{
labels: issuable.labels.to_a,
mentioned_users: issuable.mentioned_users(current_user).to_a,
- assignees: issuable.assignees.to_a
+ assignees: issuable.assignees.to_a,
+ milestone: issuable.try(:milestone)
}
associations[:total_time_spent] = issuable.total_time_spent if issuable.respond_to?(:total_time_spent)
associations[:description] = issuable.description
diff --git a/app/services/issues/build_service.rb b/app/services/issues/build_service.rb
index daef468987e..e62315de5f9 100644
--- a/app/services/issues/build_service.rb
+++ b/app/services/issues/build_service.rb
@@ -65,15 +65,19 @@ module Issues
private
def whitelisted_issue_params
+ base_params = [:title, :description, :confidential]
+ admin_params = [:milestone_id]
+
if can?(current_user, :admin_issue, project)
- params.slice(:title, :description, :milestone_id)
+ params.slice(*(base_params + admin_params))
else
- params.slice(:title, :description)
+ params.slice(*base_params)
end
end
def build_issue_params
- issue_params_with_info_from_discussions.merge(whitelisted_issue_params)
+ { author: current_user }.merge(issue_params_with_info_from_discussions)
+ .merge(whitelisted_issue_params)
end
end
end
diff --git a/app/services/issues/related_branches_service.rb b/app/services/issues/related_branches_service.rb
index 76af482b7ac..46076218857 100644
--- a/app/services/issues/related_branches_service.rb
+++ b/app/services/issues/related_branches_service.rb
@@ -5,11 +5,29 @@
module Issues
class RelatedBranchesService < Issues::BaseService
def execute(issue)
- branches_with_iid_of(issue) - branches_with_merge_request_for(issue)
+ branch_names = branches_with_iid_of(issue) - branches_with_merge_request_for(issue)
+ branch_names.map { |branch_name| branch_data(branch_name) }
end
private
+ def branch_data(branch_name)
+ {
+ name: branch_name,
+ pipeline_status: pipeline_status(branch_name)
+ }
+ end
+
+ def pipeline_status(branch_name)
+ branch = project.repository.find_branch(branch_name)
+ target = branch&.dereferenced_target
+
+ return unless target
+
+ pipeline = project.pipeline_for(branch_name, target.sha)
+ pipeline.detailed_status(current_user) if can?(current_user, :read_pipeline, pipeline)
+ end
+
def branches_with_merge_request_for(issue)
Issues::ReferencedMergeRequestsService
.new(project, current_user)
diff --git a/app/services/issues/update_service.rb b/app/services/issues/update_service.rb
index 78ebbd7bff2..ee1a22634af 100644
--- a/app/services/issues/update_service.rb
+++ b/app/services/issues/update_service.rb
@@ -21,6 +21,10 @@ module Issues
spam_check(issue, current_user) unless skip_spam_check
end
+ def after_update(issue)
+ IssuesChannel.broadcast_to(issue, event: 'updated') if Feature.enabled?(:broadcast_issue_updates, issue.project)
+ end
+
def handle_changes(issue, options)
old_associations = options.fetch(:old_associations, {})
old_labels = old_associations.fetch(:labels, [])
diff --git a/app/services/jira_import/start_import_service.rb b/app/services/jira_import/start_import_service.rb
index de4e490281f..59fd463022f 100644
--- a/app/services/jira_import/start_import_service.rb
+++ b/app/services/jira_import/start_import_service.rb
@@ -56,7 +56,7 @@ module JiraImport
import_start_time = Time.zone.now
jira_imports_for_project = project.jira_imports.by_jira_project_key(jira_project_key).size + 1
title = "jira-import::#{jira_project_key}-#{jira_imports_for_project}"
- description = "Label for issues that were imported from jira on #{import_start_time.strftime('%Y-%m-%d %H:%M:%S')}"
+ description = "Label for issues that were imported from Jira on #{import_start_time.strftime('%Y-%m-%d %H:%M:%S')}"
color = "#{Label.color_for(title)}"
{ title: title, description: description, color: color }
end
diff --git a/app/services/lfs/file_transformer.rb b/app/services/lfs/file_transformer.rb
index 88f59b820a4..69d33e1c873 100644
--- a/app/services/lfs/file_transformer.rb
+++ b/app/services/lfs/file_transformer.rb
@@ -5,8 +5,7 @@ module Lfs
# return a transformed result with `content` and `encoding` to commit.
#
# The `repository` passed to the initializer can be a Repository or
- # a DesignManagement::Repository (an EE-specific class that inherits
- # from Repository).
+ # class that inherits from Repository.
#
# The `repository_type` property will be one of the types named in
# `Gitlab::GlRepository.types`, and is recorded on the `LfsObjectsProject`
diff --git a/app/services/members/request_access_service.rb b/app/services/members/request_access_service.rb
index b9b0550e290..4dfedc6cd4e 100644
--- a/app/services/members/request_access_service.rb
+++ b/app/services/members/request_access_service.rb
@@ -8,7 +8,7 @@ module Members
source.members.create(
access_level: Gitlab::Access::DEVELOPER,
user: current_user,
- requested_at: Time.now.utc)
+ requested_at: Time.current.utc)
end
private
diff --git a/app/services/merge_requests/base_service.rb b/app/services/merge_requests/base_service.rb
index 00bf69739ad..7f7bfa29af7 100644
--- a/app/services/merge_requests/base_service.rb
+++ b/app/services/merge_requests/base_service.rb
@@ -39,6 +39,8 @@ module MergeRequests
# Don't try to print expensive instance variables.
def inspect
+ return "#<#{self.class}>" unless respond_to?(:merge_request)
+
"#<#{self.class} #{merge_request.to_reference(full: true)}>"
end
@@ -89,8 +91,7 @@ module MergeRequests
end
def can_use_merge_request_ref?(merge_request)
- Feature.enabled?(:ci_use_merge_request_ref, project, default_enabled: true) &&
- !merge_request.for_fork?
+ !merge_request.for_fork?
end
def abort_auto_merge(merge_request, reason)
@@ -115,6 +116,32 @@ module MergeRequests
yield merge_request
end
end
+
+ def log_error(exception:, message:, save_message_on_model: false)
+ reference = merge_request.to_reference(full: true)
+ data = {
+ class: self.class.name,
+ message: message,
+ merge_request_id: merge_request.id,
+ merge_request: reference,
+ save_message_on_model: save_message_on_model
+ }
+
+ if exception
+ Gitlab::ErrorTracking.with_context(current_user) do
+ Gitlab::ErrorTracking.track_exception(exception, data)
+ end
+
+ data[:"exception.message"] = exception.message
+ end
+
+ # TODO: Deprecate Gitlab::GitLogger since ErrorTracking should suffice:
+ # https://gitlab.com/gitlab-org/gitlab/-/issues/216379
+ data[:message] = "#{self.class.name} error (#{reference}): #{message}"
+ Gitlab::GitLogger.error(data)
+
+ merge_request.update(merge_error: message) if save_message_on_model
+ end
end
end
diff --git a/app/services/merge_requests/rebase_service.rb b/app/services/merge_requests/rebase_service.rb
index bc1e97088af..87808a21a15 100644
--- a/app/services/merge_requests/rebase_service.rb
+++ b/app/services/merge_requests/rebase_service.rb
@@ -2,8 +2,6 @@
module MergeRequests
class RebaseService < MergeRequests::BaseService
- include Git::Logger
-
REBASE_ERROR = 'Rebase failed. Please rebase locally'
attr_reader :merge_request
@@ -22,7 +20,7 @@ module MergeRequests
def rebase
# Ensure Gitaly isn't already running a rebase
if source_project.repository.rebase_in_progress?(merge_request.id)
- log_error('Rebase task canceled: Another rebase is already in progress', save_message_on_model: true)
+ log_error(exception: nil, message: 'Rebase task canceled: Another rebase is already in progress', save_message_on_model: true)
return false
end
@@ -30,8 +28,8 @@ module MergeRequests
true
rescue => e
- log_error(REBASE_ERROR, save_message_on_model: true)
- log_error(e.message)
+ log_error(exception: e, message: REBASE_ERROR, save_message_on_model: true)
+
false
ensure
merge_request.update_column(:rebase_jid, nil)
diff --git a/app/services/merge_requests/refresh_service.rb b/app/services/merge_requests/refresh_service.rb
index c6e1651fa26..56a91fa0305 100644
--- a/app/services/merge_requests/refresh_service.rb
+++ b/app/services/merge_requests/refresh_service.rb
@@ -115,6 +115,10 @@ module MergeRequests
filter_merge_requests(merge_requests).each do |merge_request|
if branch_and_project_match?(merge_request) || @push.force_push?
merge_request.reload_diff(current_user)
+ # Clear existing merge error if the push were directed at the
+ # source branch. Clearing the error when the target branch
+ # changes will hide the error from the user.
+ merge_request.merge_error = nil
elsif merge_request.merge_request_diff.includes_any_commits?(push_commit_ids)
merge_request.reload_diff(current_user)
end
diff --git a/app/services/merge_requests/squash_service.rb b/app/services/merge_requests/squash_service.rb
index d25997c925e..4b04d42b48e 100644
--- a/app/services/merge_requests/squash_service.rb
+++ b/app/services/merge_requests/squash_service.rb
@@ -2,7 +2,7 @@
module MergeRequests
class SquashService < MergeRequests::BaseService
- include Git::Logger
+ SquashInProgressError = Class.new(RuntimeError)
def execute
# If performing a squash would result in no change, then
@@ -11,11 +11,13 @@ module MergeRequests
return success(squash_sha: merge_request.diff_head_sha)
end
- if merge_request.squash_in_progress?
+ if squash_in_progress?
return error(s_('MergeRequests|Squash task canceled: another squash is already in progress.'))
end
squash! || error(s_('MergeRequests|Failed to squash. Should be done manually.'))
+ rescue SquashInProgressError
+ error(s_('MergeRequests|An error occurred while checking whether another squash is in progress.'))
end
private
@@ -25,11 +27,19 @@ module MergeRequests
success(squash_sha: squash_sha)
rescue => e
- log_error("Failed to squash merge request #{merge_request.to_reference(full: true)}:")
- log_error(e.message)
+ log_error(exception: e, message: 'Failed to squash merge request')
+
false
end
+ def squash_in_progress?
+ merge_request.squash_in_progress?
+ rescue => e
+ log_error(exception: e, message: 'Failed to check squash in progress')
+
+ raise SquashInProgressError, e.message
+ end
+
def repository
target_project.repository
end
diff --git a/app/services/metrics/dashboard/base_service.rb b/app/services/metrics/dashboard/base_service.rb
index c112d75a9b5..514793694ba 100644
--- a/app/services/metrics/dashboard/base_service.rb
+++ b/app/services/metrics/dashboard/base_service.rb
@@ -42,7 +42,7 @@ module Metrics
def allowed?
return false unless params[:environment]
- Ability.allowed?(current_user, :read_environment, project)
+ project&.feature_available?(:metrics_dashboard, current_user)
end
# Returns a new dashboard Hash, supplemented with DB info
diff --git a/app/services/metrics/dashboard/grafana_metric_embed_service.rb b/app/services/metrics/dashboard/grafana_metric_embed_service.rb
index d58b80162f5..d9ce2c5e905 100644
--- a/app/services/metrics/dashboard/grafana_metric_embed_service.rb
+++ b/app/services/metrics/dashboard/grafana_metric_embed_service.rb
@@ -18,6 +18,7 @@ module Metrics
self.reactive_cache_lease_timeout = 30.seconds
self.reactive_cache_refresh_interval = 30.minutes
self.reactive_cache_lifetime = 30.minutes
+ self.reactive_cache_work_type = :external_dependency
self.reactive_cache_worker_finder = ->(_id, *args) { from_cache(*args) }
class << self
@@ -112,7 +113,7 @@ module Metrics
end
def parse_json(json)
- JSON.parse(json, symbolize_names: true)
+ Gitlab::Json.parse(json, symbolize_names: true)
rescue JSON::ParserError
raise DashboardProcessingError.new('Grafana response contains invalid json')
end
diff --git a/app/services/metrics/dashboard/transient_embed_service.rb b/app/services/metrics/dashboard/transient_embed_service.rb
index ce81f337e47..cb6ca215447 100644
--- a/app/services/metrics/dashboard/transient_embed_service.rb
+++ b/app/services/metrics/dashboard/transient_embed_service.rb
@@ -23,7 +23,9 @@ module Metrics
override :get_raw_dashboard
def get_raw_dashboard
- JSON.parse(params[:embed_json])
+ Gitlab::Json.parse(params[:embed_json])
+ rescue JSON::ParserError => e
+ invalid_embed_json!(e.message)
end
override :sequence
@@ -35,6 +37,10 @@ module Metrics
def identifiers
Digest::SHA256.hexdigest(params[:embed_json])
end
+
+ def invalid_embed_json!(message)
+ raise DashboardProcessingError.new("Parsing error for param :embed_json. #{message}")
+ end
end
end
end
diff --git a/app/services/metrics/users_starred_dashboards/create_service.rb b/app/services/metrics/users_starred_dashboards/create_service.rb
new file mode 100644
index 00000000000..7784ed4eb4e
--- /dev/null
+++ b/app/services/metrics/users_starred_dashboards/create_service.rb
@@ -0,0 +1,74 @@
+# frozen_string_literal: true
+
+# Create Metrics::UsersStarredDashboard entry for given user based on matched dashboard_path, project
+module Metrics
+ module UsersStarredDashboards
+ class CreateService < ::BaseService
+ include Stepable
+
+ steps :authorize_create_action,
+ :parse_dashboard_path,
+ :create
+
+ def initialize(user, project, dashboard_path)
+ @user, @project, @dashboard_path = user, project, dashboard_path
+ end
+
+ def execute
+ keys = %i[status message starred_dashboard]
+ status, message, dashboards = execute_steps.values_at(*keys)
+
+ if status != :success
+ ServiceResponse.error(message: message)
+ else
+ ServiceResponse.success(payload: dashboards)
+ end
+ end
+
+ private
+
+ attr_reader :user, :project, :dashboard_path
+
+ def authorize_create_action(_options)
+ if Ability.allowed?(user, :create_metrics_user_starred_dashboard, project)
+ success(user: user, project: project)
+ else
+ error(s_('Metrics::UsersStarredDashboards|You are not authorized to add star to this dashboard'))
+ end
+ end
+
+ def parse_dashboard_path(options)
+ if dashboard_path_exists?
+ options[:dashboard_path] = dashboard_path
+ success(options)
+ else
+ error(s_('Metrics::UsersStarredDashboards|Dashboard with requested path can not be found'))
+ end
+ end
+
+ def create(options)
+ starred_dashboard = build_starred_dashboard_from(options)
+
+ if starred_dashboard.save
+ success(starred_dashboard: starred_dashboard)
+ else
+ error(starred_dashboard.errors.messages)
+ end
+ end
+
+ def build_starred_dashboard_from(options)
+ Metrics::UsersStarredDashboard.new(
+ user: options.fetch(:user),
+ project: options.fetch(:project),
+ dashboard_path: options.fetch(:dashboard_path)
+ )
+ end
+
+ def dashboard_path_exists?
+ Gitlab::Metrics::Dashboard::Finder
+ .find_all_paths(project)
+ .any? { |dashboard| dashboard[:path] == dashboard_path }
+ end
+ end
+ end
+end
diff --git a/app/services/metrics/users_starred_dashboards/delete_service.rb b/app/services/metrics/users_starred_dashboards/delete_service.rb
new file mode 100644
index 00000000000..579715bd49f
--- /dev/null
+++ b/app/services/metrics/users_starred_dashboards/delete_service.rb
@@ -0,0 +1,33 @@
+# frozen_string_literal: true
+
+# Delete all matching Metrics::UsersStarredDashboard entries for given user based on matched dashboard_path, project
+module Metrics
+ module UsersStarredDashboards
+ class DeleteService < ::BaseService
+ def initialize(user, project, dashboard_path = nil)
+ @user, @project, @dashboard_path = user, project, dashboard_path
+ end
+
+ def execute
+ ServiceResponse.success(payload: { deleted_rows: starred_dashboards.delete_all })
+ end
+
+ private
+
+ attr_reader :user, :project, :dashboard_path
+
+ def starred_dashboards
+ # since deleted records are scoped to their owner there is no need to
+ # check if that user can delete them, also if user lost access to
+ # project it shouldn't block that user from removing them
+ dashboards = user.metrics_users_starred_dashboards
+
+ if dashboard_path.present?
+ dashboards.for_project_dashboard(project, dashboard_path)
+ else
+ dashboards.for_project(project)
+ end
+ end
+ end
+ end
+end
diff --git a/app/services/namespaces/check_storage_size_service.rb b/app/services/namespaces/check_storage_size_service.rb
new file mode 100644
index 00000000000..b3cf17681ee
--- /dev/null
+++ b/app/services/namespaces/check_storage_size_service.rb
@@ -0,0 +1,94 @@
+# frozen_string_literal: true
+
+module Namespaces
+ class CheckStorageSizeService
+ include ActiveSupport::NumberHelper
+ include Gitlab::Allowable
+ include Gitlab::Utils::StrongMemoize
+
+ def initialize(namespace, user)
+ @root_namespace = namespace.root_ancestor
+ @root_storage_size = Namespace::RootStorageSize.new(root_namespace)
+ @user = user
+ end
+
+ def execute
+ return ServiceResponse.success unless Feature.enabled?(:namespace_storage_limit, root_namespace)
+ return ServiceResponse.success if alert_level == :none
+
+ if root_storage_size.above_size_limit?
+ ServiceResponse.error(message: above_size_limit_message, payload: payload)
+ else
+ ServiceResponse.success(payload: payload)
+ end
+ end
+
+ private
+
+ attr_reader :root_namespace, :root_storage_size, :user
+
+ USAGE_THRESHOLDS = {
+ none: 0.0,
+ info: 0.5,
+ warning: 0.75,
+ alert: 0.95,
+ error: 1.0
+ }.freeze
+
+ def payload
+ return {} unless can?(user, :admin_namespace, root_namespace)
+
+ {
+ explanation_message: explanation_message,
+ usage_message: usage_message,
+ alert_level: alert_level
+ }
+ end
+
+ def explanation_message
+ root_storage_size.above_size_limit? ? above_size_limit_message : below_size_limit_message
+ end
+
+ def usage_message
+ s_("You reached %{usage_in_percent} of %{namespace_name}'s capacity (%{used_storage} of %{storage_limit})" % current_usage_params)
+ end
+
+ def alert_level
+ strong_memoize(:alert_level) do
+ usage_ratio = root_storage_size.usage_ratio
+ current_level = USAGE_THRESHOLDS.each_key.first
+
+ USAGE_THRESHOLDS.each do |level, threshold|
+ current_level = level if usage_ratio >= threshold
+ end
+
+ current_level
+ end
+ end
+
+ def below_size_limit_message
+ s_("If you reach 100%% storage capacity, you will not be able to: %{base_message}" % { base_message: base_message } )
+ end
+
+ def above_size_limit_message
+ s_("%{namespace_name} is now read-only. You cannot: %{base_message}" % { namespace_name: root_namespace.name, base_message: base_message })
+ end
+
+ def base_message
+ s_("push to your repository, create pipelines, create issues or add comments. To reduce storage capacity, delete unused repositories, artifacts, wikis, issues, and pipelines.")
+ end
+
+ def current_usage_params
+ {
+ usage_in_percent: number_to_percentage(root_storage_size.usage_ratio * 100, precision: 0),
+ namespace_name: root_namespace.name,
+ used_storage: formatted(root_storage_size.current_size),
+ storage_limit: formatted(root_storage_size.limit)
+ }
+ end
+
+ def formatted(number)
+ number_to_human_size(number, delimiter: ',', precision: 2)
+ end
+ end
+end
diff --git a/app/services/notes/post_process_service.rb b/app/services/notes/post_process_service.rb
index 53b3b57f4af..bc86118a150 100644
--- a/app/services/notes/post_process_service.rb
+++ b/app/services/notes/post_process_service.rb
@@ -16,10 +16,18 @@ module Notes
return if @note.for_personal_snippet?
@note.create_cross_references!
+ ::SystemNoteService.design_discussion_added(@note) if create_design_discussion_system_note?
+
execute_note_hooks
end
end
+ private
+
+ def create_design_discussion_system_note?
+ @note && @note.for_design? && @note.start_of_discussion?
+ end
+
def hook_data
Gitlab::DataBuilder::Note.build(@note, @note.author)
end
diff --git a/app/services/notification_service.rb b/app/services/notification_service.rb
index 91e19d190bd..4c1db03fab8 100644
--- a/app/services/notification_service.rb
+++ b/app/services/notification_service.rb
@@ -66,6 +66,14 @@ class NotificationService
mailer.access_token_about_to_expire_email(user).deliver_later
end
+ # Notify a user when a previously unknown IP or device is used to
+ # sign in to their account
+ def unknown_sign_in(user, ip)
+ return unless user.can?(:receive_notifications)
+
+ mailer.unknown_sign_in_email(user, ip).deliver_later
+ end
+
# When create an issue we should send an email to:
#
# * issue assignee if their notification level is not Disabled
@@ -537,6 +545,18 @@ class NotificationService
end
end
+ def group_was_exported(group, current_user)
+ return true unless notifiable?(current_user, :mention, group: group)
+
+ mailer.group_was_exported_email(current_user, group).deliver_later
+ end
+
+ def group_was_not_exported(group, current_user, errors)
+ return true unless notifiable?(current_user, :mention, group: group)
+
+ mailer.group_was_not_exported_email(current_user, group, errors).deliver_later
+ end
+
protected
def new_resource_email(target, method)
diff --git a/app/services/pages_domains/obtain_lets_encrypt_certificate_service.rb b/app/services/pages_domains/obtain_lets_encrypt_certificate_service.rb
index 1c03641469e..e14241158a6 100644
--- a/app/services/pages_domains/obtain_lets_encrypt_certificate_service.rb
+++ b/app/services/pages_domains/obtain_lets_encrypt_certificate_service.rb
@@ -51,8 +51,6 @@ module PagesDomains
def save_order_error(acme_order, api_order)
log_error(api_order)
- return unless Feature.enabled?(:pages_letsencrypt_errors, pages_domain.project)
-
pages_domain.assign_attributes(auto_ssl_failed: true)
pages_domain.save!(validate: false)
diff --git a/app/services/pod_logs/base_service.rb b/app/services/pod_logs/base_service.rb
index 2451ab8e0ce..8936f9b67a5 100644
--- a/app/services/pod_logs/base_service.rb
+++ b/app/services/pod_logs/base_service.rb
@@ -58,6 +58,9 @@ module PodLogs
result[:pod_name] = params['pod_name'].presence
result[:container_name] = params['container_name'].presence
+ return error(_('Invalid pod_name')) if result[:pod_name] && !result[:pod_name].is_a?(String)
+ return error(_('Invalid container_name')) if result[:container_name] && !result[:container_name].is_a?(String)
+
success(result)
end
diff --git a/app/services/pod_logs/elasticsearch_service.rb b/app/services/pod_logs/elasticsearch_service.rb
index aac0fa424ca..f79562c8ab3 100644
--- a/app/services/pod_logs/elasticsearch_service.rb
+++ b/app/services/pod_logs/elasticsearch_service.rb
@@ -11,6 +11,7 @@ module PodLogs
:pod_logs,
:filter_return_keys
+ self.reactive_cache_work_type = :external_dependency
self.reactive_cache_worker_finder = ->(id, _cache_key, namespace, params) { new(::Clusters::Cluster.find(id), namespace, params: params) }
private
@@ -52,12 +53,16 @@ module PodLogs
def check_search(result)
result[:search] = params['search'] if params.key?('search')
+ return error(_('Invalid search parameter')) if result[:search] && !result[:search].is_a?(String)
+
success(result)
end
def check_cursor(result)
result[:cursor] = params['cursor'] if params.key?('cursor')
+ return error(_('Invalid cursor parameter')) if result[:cursor] && !result[:cursor].is_a?(String)
+
success(result)
end
@@ -65,6 +70,8 @@ module PodLogs
client = cluster&.application_elastic_stack&.elasticsearch_client
return error(_('Unable to connect to Elasticsearch')) unless client
+ chart_above_v2 = cluster.application_elastic_stack.chart_above_v2?
+
response = ::Gitlab::Elasticsearch::Logs::Lines.new(client).pod_logs(
namespace,
pod_name: result[:pod_name],
@@ -72,7 +79,8 @@ module PodLogs
search: result[:search],
start_time: result[:start_time],
end_time: result[:end_time],
- cursor: result[:cursor]
+ cursor: result[:cursor],
+ chart_above_v2: chart_above_v2
)
result.merge!(response)
diff --git a/app/services/pod_logs/kubernetes_service.rb b/app/services/pod_logs/kubernetes_service.rb
index 0a8072a9037..b573ceae1aa 100644
--- a/app/services/pod_logs/kubernetes_service.rb
+++ b/app/services/pod_logs/kubernetes_service.rb
@@ -17,6 +17,7 @@ module PodLogs
:split_logs,
:filter_return_keys
+ self.reactive_cache_work_type = :external_dependency
self.reactive_cache_worker_finder = ->(id, _cache_key, namespace, params) { new(::Clusters::Cluster.find(id), namespace, params: params) }
private
@@ -46,6 +47,10 @@ module PodLogs
' chars' % { max_length: K8S_NAME_MAX_LENGTH }))
end
+ unless result[:pod_name] =~ Gitlab::Regex.kubernetes_dns_subdomain_regex
+ return error(_('pod_name can contain only lowercase letters, digits, \'-\', and \'.\' and must start and end with an alphanumeric character'))
+ end
+
unless result[:pods].include?(result[:pod_name])
return error(_('Pod does not exist'))
end
@@ -69,6 +74,10 @@ module PodLogs
' %{max_length} chars' % { max_length: K8S_NAME_MAX_LENGTH }))
end
+ unless result[:container_name] =~ Gitlab::Regex.kubernetes_dns_subdomain_regex
+ return error(_('container_name can contain only lowercase letters, digits, \'-\', and \'.\' and must start and end with an alphanumeric character'))
+ end
+
unless container_names.include?(result[:container_name])
return error(_('Container does not exist'))
end
diff --git a/app/services/post_receive_service.rb b/app/services/post_receive_service.rb
index f12e45d701a..65e6ebc17d2 100644
--- a/app/services/post_receive_service.rb
+++ b/app/services/post_receive_service.rb
@@ -29,6 +29,8 @@ class PostReceiveService
response.add_alert_message(message)
end
+ response.add_alert_message(storage_size_limit_alert)
+
broadcast_message = BroadcastMessage.current_banner_messages&.last&.message
response.add_alert_message(broadcast_message)
@@ -74,4 +76,19 @@ class PostReceiveService
::MergeRequests::GetUrlsService.new(project).execute(params[:changes])
end
+
+ private
+
+ def storage_size_limit_alert
+ return unless repository&.repo_type&.project?
+
+ payload = Namespaces::CheckStorageSizeService.new(project.namespace, user).execute.payload
+ return unless payload.present?
+
+ alert_level = "##### #{payload[:alert_level].to_s.upcase} #####"
+
+ [alert_level, payload[:usage_message], payload[:explanation_message]].join("\n")
+ end
end
+
+PostReceiveService.prepend_if_ee('EE::PostReceiveService')
diff --git a/app/services/projects/alerting/notify_service.rb b/app/services/projects/alerting/notify_service.rb
index 1ce1ef7a1cd..76c89e85f17 100644
--- a/app/services/projects/alerting/notify_service.rb
+++ b/app/services/projects/alerting/notify_service.rb
@@ -10,7 +10,10 @@ module Projects
return forbidden unless alerts_service_activated?
return unauthorized unless valid_token?(token)
- process_incident_issues if process_issues?
+ alert = create_alert
+ return bad_request unless alert.persisted?
+
+ process_incident_issues(alert) if process_issues?
send_alert_email if send_email?
ServiceResponse.success
@@ -22,13 +25,21 @@ module Projects
delegate :alerts_service, :alerts_service_activated?, to: :project
+ def am_alert_params
+ Gitlab::AlertManagement::AlertParams.from_generic_alert(project: project, payload: params.to_h)
+ end
+
+ def create_alert
+ AlertManagement::Alert.create(am_alert_params)
+ end
+
def send_email?
incident_management_setting.send_email?
end
- def process_incident_issues
+ def process_incident_issues(alert)
IncidentManagement::ProcessAlertWorker
- .perform_async(project.id, parsed_payload)
+ .perform_async(project.id, parsed_payload, alert.id)
end
def send_alert_email
diff --git a/app/services/projects/container_repository/cleanup_tags_service.rb b/app/services/projects/container_repository/cleanup_tags_service.rb
index fc09d14ba4d..b53a9c1561e 100644
--- a/app/services/projects/container_repository/cleanup_tags_service.rb
+++ b/app/services/projects/container_repository/cleanup_tags_service.rb
@@ -33,7 +33,7 @@ module Projects
end
def order_by_date(tags)
- now = DateTime.now
+ now = DateTime.current
tags.sort_by { |tag| tag.created_at || now }.reverse
end
diff --git a/app/services/projects/create_service.rb b/app/services/projects/create_service.rb
index 429ae905e3d..3233d1799b8 100644
--- a/app/services/projects/create_service.rb
+++ b/app/services/projects/create_service.rb
@@ -108,8 +108,22 @@ module Projects
# users in the background
def setup_authorizations
if @project.group
- @project.group.refresh_members_authorized_projects(blocking: false)
current_user.refresh_authorized_projects
+
+ if Feature.enabled?(:specialized_project_authorization_workers)
+ AuthorizedProjectUpdate::ProjectCreateWorker.perform_async(@project.id)
+ # AuthorizedProjectsWorker uses an exclusive lease per user but
+ # specialized workers might have synchronization issues. Until we
+ # compare the inconsistency rates of both approaches, we still run
+ # AuthorizedProjectsWorker but with some delay and lower urgency as a
+ # safety net.
+ @project.group.refresh_members_authorized_projects(
+ blocking: false,
+ priority: UserProjectAccessChangedService::LOW_PRIORITY
+ )
+ else
+ @project.group.refresh_members_authorized_projects(blocking: false)
+ end
else
@project.add_maintainer(@project.namespace.owner, current_user: current_user)
end
@@ -202,8 +216,19 @@ module Projects
end
end
+ def extra_attributes_for_measurement
+ {
+ current_user: current_user&.name,
+ project_full_path: "#{project_namespace&.full_path}/#{@params[:path]}"
+ }
+ end
+
private
+ def project_namespace
+ @project_namespace ||= Namespace.find_by_id(@params[:namespace_id]) || current_user.namespace
+ end
+
def create_from_template?
@params[:template_name].present? || @params[:template_project_id].present?
end
@@ -224,4 +249,9 @@ module Projects
end
end
+# rubocop: disable Cop/InjectEnterpriseEditionModule
Projects::CreateService.prepend_if_ee('EE::Projects::CreateService')
+# rubocop: enable Cop/InjectEnterpriseEditionModule
+
+# Measurable should be at the bottom of the ancestor chain, so it will measure execution of EE::Projects::CreateService as well
+Projects::CreateService.prepend(Measurable)
diff --git a/app/services/projects/gitlab_projects_import_service.rb b/app/services/projects/gitlab_projects_import_service.rb
index 234ebbc6651..2e192942b9c 100644
--- a/app/services/projects/gitlab_projects_import_service.rb
+++ b/app/services/projects/gitlab_projects_import_service.rb
@@ -29,17 +29,21 @@ module Projects
end
def project_with_same_full_path?
- Project.find_by_full_path("#{current_namespace.full_path}/#{params[:path]}").present?
+ Project.find_by_full_path(project_path).present?
end
# rubocop: disable CodeReuse/ActiveRecord
def current_namespace
strong_memoize(:current_namespace) do
- Namespace.find_by(id: params[:namespace_id])
+ Namespace.find_by(id: params[:namespace_id]) || current_user.namespace
end
end
# rubocop: enable CodeReuse/ActiveRecord
+ def project_path
+ "#{current_namespace.full_path}/#{params[:path]}"
+ end
+
def overwrite?
strong_memoize(:overwrite) do
params.delete(:overwrite)
diff --git a/app/services/projects/hashed_storage/base_attachment_service.rb b/app/services/projects/hashed_storage/base_attachment_service.rb
index f8852c206e3..a2a7895ba17 100644
--- a/app/services/projects/hashed_storage/base_attachment_service.rb
+++ b/app/services/projects/hashed_storage/base_attachment_service.rb
@@ -70,7 +70,7 @@ module Projects
#
# @param [String] new_path
def discard_path!(new_path)
- discarded_path = "#{new_path}-#{Time.now.utc.to_i}"
+ discarded_path = "#{new_path}-#{Time.current.utc.to_i}"
logger.info("Moving existing empty attachments folder from '#{new_path}' to '#{discarded_path}', (PROJECT_ID=#{project.id})")
FileUtils.mv(new_path, discarded_path)
diff --git a/app/services/projects/hashed_storage/base_repository_service.rb b/app/services/projects/hashed_storage/base_repository_service.rb
index d81aa4de9f1..065bf8725be 100644
--- a/app/services/projects/hashed_storage/base_repository_service.rb
+++ b/app/services/projects/hashed_storage/base_repository_service.rb
@@ -8,13 +8,15 @@ module Projects
class BaseRepositoryService < BaseService
include Gitlab::ShellAdapter
- attr_reader :old_disk_path, :new_disk_path, :old_storage_version, :logger, :move_wiki
+ attr_reader :old_disk_path, :new_disk_path, :old_storage_version,
+ :logger, :move_wiki, :move_design
def initialize(project:, old_disk_path:, logger: nil)
@project = project
@logger = logger || Gitlab::AppLogger
@old_disk_path = old_disk_path
@move_wiki = has_wiki?
+ @move_design = has_design?
end
protected
@@ -23,6 +25,10 @@ module Projects
gitlab_shell.repository_exists?(project.repository_storage, "#{old_wiki_disk_path}.git")
end
+ def has_design?
+ gitlab_shell.repository_exists?(project.repository_storage, "#{old_design_disk_path}.git")
+ end
+
def move_repository(from_name, to_name)
from_exists = gitlab_shell.repository_exists?(project.repository_storage, "#{from_name}.git")
to_exists = gitlab_shell.repository_exists?(project.repository_storage, "#{to_name}.git")
@@ -58,12 +64,18 @@ module Projects
project.clear_memoization(:wiki)
end
+ if move_design
+ result &&= move_repository(old_design_disk_path, new_design_disk_path)
+ project.clear_memoization(:design_repository)
+ end
+
result
end
def rollback_folder_move
move_repository(new_disk_path, old_disk_path)
move_repository(new_wiki_disk_path, old_wiki_disk_path)
+ move_repository(new_design_disk_path, old_design_disk_path) if move_design
end
def try_to_set_repository_read_only!
@@ -87,8 +99,18 @@ module Projects
def new_wiki_disk_path
@new_wiki_disk_path ||= "#{new_disk_path}#{wiki_path_suffix}"
end
+
+ def design_path_suffix
+ @design_path_suffix ||= ::Gitlab::GlRepository::DESIGN.path_suffix
+ end
+
+ def old_design_disk_path
+ @old_design_disk_path ||= "#{old_disk_path}#{design_path_suffix}"
+ end
+
+ def new_design_disk_path
+ @new_design_disk_path ||= "#{new_disk_path}#{design_path_suffix}"
+ end
end
end
end
-
-Projects::HashedStorage::BaseRepositoryService.prepend_if_ee('EE::Projects::HashedStorage::BaseRepositoryService')
diff --git a/app/services/projects/import_export/export_service.rb b/app/services/projects/import_export/export_service.rb
index 8893bf18e1f..86cb4f35206 100644
--- a/app/services/projects/import_export/export_service.rb
+++ b/app/services/projects/import_export/export_service.rb
@@ -3,19 +3,35 @@
module Projects
module ImportExport
class ExportService < BaseService
- def execute(after_export_strategy = nil, options = {})
+ prepend Measurable
+
+ def initialize(*args)
+ super
+
+ @shared = project.import_export_shared
+ end
+
+ def execute(after_export_strategy = nil)
unless project.template_source? || can?(current_user, :admin_project, project)
raise ::Gitlab::ImportExport::Error.permission_error(current_user, project)
end
- @shared = project.import_export_shared
-
save_all!
execute_after_export_action(after_export_strategy)
ensure
cleanup
end
+ protected
+
+ def extra_attributes_for_measurement
+ {
+ current_user: current_user&.name,
+ project_full_path: project&.full_path,
+ file_path: shared.export_path
+ }
+ end
+
private
attr_accessor :shared
@@ -42,7 +58,10 @@ module Projects
end
def exporters
- [version_saver, avatar_saver, project_tree_saver, uploads_saver, repo_saver, wiki_repo_saver, lfs_saver, snippets_repo_saver]
+ [
+ version_saver, avatar_saver, project_tree_saver, uploads_saver,
+ repo_saver, wiki_repo_saver, lfs_saver, snippets_repo_saver, design_repo_saver
+ ]
end
def version_saver
@@ -81,6 +100,10 @@ module Projects
Gitlab::ImportExport::SnippetsRepoSaver.new(current_user: current_user, project: project, shared: shared)
end
+ def design_repo_saver
+ Gitlab::ImportExport::DesignRepoSaver.new(project: project, shared: shared)
+ end
+
def cleanup
FileUtils.rm_rf(shared.archive_path) if shared&.archive_path
end
@@ -103,5 +126,3 @@ module Projects
end
end
end
-
-Projects::ImportExport::ExportService.prepend_if_ee('EE::Projects::ImportExport::ExportService')
diff --git a/app/services/projects/import_service.rb b/app/services/projects/import_service.rb
index 4b294a97516..449c4c3de6b 100644
--- a/app/services/projects/import_service.rb
+++ b/app/services/projects/import_service.rb
@@ -3,6 +3,7 @@
module Projects
class ImportService < BaseService
Error = Class.new(StandardError)
+ PermissionError = Class.new(StandardError)
# Returns true if this importer is supposed to perform its work in the
# background.
@@ -21,6 +22,8 @@ module Projects
import_data
+ after_execute_hook
+
success
rescue Gitlab::UrlBlocker::BlockedUrlError => e
Gitlab::ErrorTracking.track_exception(e, project_path: project.full_path, importer: project.import_type)
@@ -34,8 +37,23 @@ module Projects
error(s_("ImportProjects|Error importing repository %{project_safe_import_url} into %{project_full_path} - %{message}") % { project_safe_import_url: project.safe_import_url, project_full_path: project.full_path, message: message })
end
+ protected
+
+ def extra_attributes_for_measurement
+ {
+ current_user: current_user&.name,
+ project_full_path: project&.full_path,
+ import_type: project&.import_type,
+ file_path: project&.import_source
+ }
+ end
+
private
+ def after_execute_hook
+ # Defined in EE::Projects::ImportService
+ end
+
def add_repository_to_project
if project.external_import? && !unknown_url?
begin
@@ -130,3 +148,10 @@ module Projects
end
end
end
+
+# rubocop: disable Cop/InjectEnterpriseEditionModule
+Projects::ImportService.prepend_if_ee('EE::Projects::ImportService')
+# rubocop: enable Cop/InjectEnterpriseEditionModule
+
+# Measurable should be at the bottom of the ancestor chain, so it will measure execution of EE::Projects::ImportService as well
+Projects::ImportService.prepend(Measurable)
diff --git a/app/services/projects/lfs_pointers/lfs_download_link_list_service.rb b/app/services/projects/lfs_pointers/lfs_download_link_list_service.rb
index 48a21bf94ba..efd410088ab 100644
--- a/app/services/projects/lfs_pointers/lfs_download_link_list_service.rb
+++ b/app/services/projects/lfs_pointers/lfs_download_link_list_service.rb
@@ -69,7 +69,7 @@ module Projects
# application/vnd.git-lfs+json
# (https://github.com/git-lfs/git-lfs/blob/master/docs/api/batch.md#requests),
# HTTParty does not know this is actually JSON.
- data = JSON.parse(response.body)
+ data = Gitlab::Json.parse(response.body)
raise DownloadLinksError, "LFS Batch API did return any objects" unless data.is_a?(Hash) && data.key?('objects')
diff --git a/app/services/projects/lsif_data_service.rb b/app/services/projects/lsif_data_service.rb
index 142a5a910d4..5e7055b3309 100644
--- a/app/services/projects/lsif_data_service.rb
+++ b/app/services/projects/lsif_data_service.rb
@@ -42,7 +42,7 @@ module Projects
file.open do |stream|
Zlib::GzipReader.wrap(stream) do |gz_stream|
- data = JSON.parse(gz_stream.read)
+ data = Gitlab::Json.parse(gz_stream.read)
end
end
diff --git a/app/services/projects/prometheus/alerts/notify_service.rb b/app/services/projects/prometheus/alerts/notify_service.rb
index 6ebc061c2e3..2583a6cae9f 100644
--- a/app/services/projects/prometheus/alerts/notify_service.rb
+++ b/app/services/projects/prometheus/alerts/notify_service.rb
@@ -12,6 +12,7 @@ module Projects
return unprocessable_entity unless valid_version?
return unauthorized unless valid_alert_manager_token?(token)
+ process_prometheus_alerts
persist_events
send_alert_email if send_email?
process_incident_issues if process_issues?
@@ -115,6 +116,14 @@ module Projects
end
end
+ def process_prometheus_alerts
+ alerts.each do |alert|
+ AlertManagement::ProcessPrometheusAlertService
+ .new(project, nil, alert.to_h)
+ .execute
+ end
+ end
+
def persist_events
CreateEventsService.new(project, nil, params).execute
end
diff --git a/app/services/projects/propagate_service_template.rb b/app/services/projects/propagate_service_template.rb
index 6013b00b8c6..0483c951f1e 100644
--- a/app/services/projects/propagate_service_template.rb
+++ b/app/services/projects/propagate_service_template.rb
@@ -4,8 +4,10 @@ module Projects
class PropagateServiceTemplate
BATCH_SIZE = 100
- def self.propagate(*args)
- new(*args).propagate
+ delegate :data_fields_present?, to: :template
+
+ def self.propagate(template)
+ new(template).propagate
end
def initialize(template)
@@ -13,15 +15,15 @@ module Projects
end
def propagate
- return unless @template.active?
-
- Rails.logger.info("Propagating services for template #{@template.id}") # rubocop:disable Gitlab/RailsLogger
+ return unless template.active?
propagate_projects_with_template
end
private
+ attr_reader :template
+
def propagate_projects_with_template
loop do
batch = Project.uncached { project_ids_batch }
@@ -38,7 +40,14 @@ module Projects
end
Project.transaction do
- bulk_insert_services(service_hash.keys << 'project_id', service_list)
+ results = bulk_insert(Service, service_hash.keys << 'project_id', service_list)
+
+ if data_fields_present?
+ data_list = results.map { |row| data_hash.values << row['id'] }
+
+ bulk_insert(template.data_fields.class, data_hash.keys << 'service_id', data_list)
+ end
+
run_callbacks(batch)
end
end
@@ -52,36 +61,27 @@ module Projects
SELECT true
FROM services
WHERE services.project_id = projects.id
- AND services.type = '#{@template.type}'
+ AND services.type = #{ActiveRecord::Base.connection.quote(template.type)}
)
AND projects.pending_delete = false
AND projects.archived = false
LIMIT #{BATCH_SIZE}
- SQL
+ SQL
)
end
- def bulk_insert_services(columns, values_array)
- ActiveRecord::Base.connection.execute(
- <<-SQL.strip_heredoc
- INSERT INTO services (#{columns.join(', ')})
- VALUES #{values_array.map { |tuple| "(#{tuple.join(', ')})" }.join(', ')}
- SQL
- )
+ def bulk_insert(klass, columns, values_array)
+ items_to_insert = values_array.map { |array| Hash[columns.zip(array)] }
+
+ klass.insert_all(items_to_insert, returning: [:id])
end
def service_hash
- @service_hash ||=
- begin
- template_hash = @template.as_json(methods: :type).except('id', 'template', 'project_id')
-
- template_hash.each_with_object({}) do |(key, value), service_hash|
- value = value.is_a?(Hash) ? value.to_json : value
+ @service_hash ||= template.as_json(methods: :type, except: %w[id template project_id])
+ end
- service_hash[ActiveRecord::Base.connection.quote_column_name(key)] =
- ActiveRecord::Base.connection.quote(value)
- end
- end
+ def data_hash
+ @data_hash ||= template.data_fields.as_json(only: template.data_fields.class.column_names).except('id', 'service_id')
end
# rubocop: disable CodeReuse/ActiveRecord
@@ -97,11 +97,11 @@ module Projects
# rubocop: enable CodeReuse/ActiveRecord
def active_external_issue_tracker?
- @template.issue_tracker? && !@template.default
+ template.issue_tracker? && !template.default
end
def active_external_wiki?
- @template.type == 'ExternalWikiService'
+ template.type == 'ExternalWikiService'
end
end
end
diff --git a/app/services/projects/transfer_service.rb b/app/services/projects/transfer_service.rb
index 309eab59463..60e5b7e2639 100644
--- a/app/services/projects/transfer_service.rb
+++ b/app/services/projects/transfer_service.rb
@@ -135,7 +135,8 @@ module Projects
return if project.hashed_storage?(:repository)
move_repo_folder(@new_path, @old_path)
- move_repo_folder("#{@new_path}.wiki", "#{@old_path}.wiki")
+ move_repo_folder(new_wiki_repo_path, old_wiki_repo_path)
+ move_repo_folder(new_design_repo_path, old_design_repo_path)
end
def move_repo_folder(from_name, to_name)
@@ -157,8 +158,9 @@ module Projects
# Disk path is changed; we need to ensure we reload it
project.reload_repository!
- # Move wiki repo also if present
- move_repo_folder("#{@old_path}.wiki", "#{@new_path}.wiki")
+ # Move wiki and design repos also if present
+ move_repo_folder(old_wiki_repo_path, new_wiki_repo_path)
+ move_repo_folder(old_design_repo_path, new_design_repo_path)
end
def move_project_uploads(project)
@@ -170,6 +172,22 @@ module Projects
@new_namespace.full_path
)
end
+
+ def old_wiki_repo_path
+ "#{old_path}#{::Gitlab::GlRepository::WIKI.path_suffix}"
+ end
+
+ def new_wiki_repo_path
+ "#{new_path}#{::Gitlab::GlRepository::WIKI.path_suffix}"
+ end
+
+ def old_design_repo_path
+ "#{old_path}#{::Gitlab::GlRepository::DESIGN.path_suffix}"
+ end
+
+ def new_design_repo_path
+ "#{new_path}#{::Gitlab::GlRepository::DESIGN.path_suffix}"
+ end
end
end
diff --git a/app/services/projects/update_remote_mirror_service.rb b/app/services/projects/update_remote_mirror_service.rb
index 13a467a3ef9..e554bed6819 100644
--- a/app/services/projects/update_remote_mirror_service.rb
+++ b/app/services/projects/update_remote_mirror_service.rb
@@ -29,14 +29,16 @@ module Projects
remote_mirror.ensure_remote!
repository.fetch_remote(remote_mirror.remote_name, ssh_auth: remote_mirror, no_tags: true)
- opts = {}
- if remote_mirror.only_protected_branches?
- opts[:only_branches_matching] = project.protected_branches.select(:name).map(&:name)
- end
+ response = remote_mirror.update_repository
- remote_mirror.update_repository(opts)
+ if response.divergent_refs.any?
+ message = "Some refs have diverged and have not been updated on the remote:"
+ message += "\n\n#{response.divergent_refs.join("\n")}"
- remote_mirror.update_finish!
+ remote_mirror.mark_as_failed!(message)
+ else
+ remote_mirror.update_finish!
+ end
end
def retry_or_fail(mirror, message, tries)
diff --git a/app/services/projects/update_repository_storage_service.rb b/app/services/projects/update_repository_storage_service.rb
index 2e5de9411d1..0632df6f6d7 100644
--- a/app/services/projects/update_repository_storage_service.rb
+++ b/app/services/projects/update_repository_storage_service.rb
@@ -1,37 +1,49 @@
# frozen_string_literal: true
module Projects
- class UpdateRepositoryStorageService < BaseService
- include Gitlab::ShellAdapter
-
+ class UpdateRepositoryStorageService
Error = Class.new(StandardError)
SameFilesystemError = Class.new(Error)
- def initialize(project)
- @project = project
+ attr_reader :repository_storage_move
+ delegate :project, :destination_storage_name, to: :repository_storage_move
+ delegate :repository, to: :project
+
+ def initialize(repository_storage_move)
+ @repository_storage_move = repository_storage_move
end
- def execute(new_repository_storage_key)
- raise SameFilesystemError if same_filesystem?(project.repository.storage, new_repository_storage_key)
+ def execute
+ repository_storage_move.start!
- mirror_repositories(new_repository_storage_key)
+ raise SameFilesystemError if same_filesystem?(repository.storage, destination_storage_name)
- mark_old_paths_for_archive
+ mirror_repositories
- project.update(repository_storage: new_repository_storage_key, repository_read_only: false)
- project.leave_pool_repository
- project.track_project_repository
+ project.transaction do
+ mark_old_paths_for_archive
+
+ repository_storage_move.finish!
+ project.update!(repository_storage: destination_storage_name, repository_read_only: false)
+ project.leave_pool_repository
+ project.track_project_repository
+ end
enqueue_housekeeping
- success
+ ServiceResponse.success
- rescue Error, ArgumentError, Gitlab::Git::BaseError => e
- project.update(repository_read_only: false)
+ rescue StandardError => e
+ project.transaction do
+ repository_storage_move.do_fail!
+ project.update!(repository_read_only: false)
+ end
Gitlab::ErrorTracking.track_exception(e, project_path: project.full_path)
- error(s_("UpdateRepositoryStorage|Error moving repository storage for %{project_full_path} - %{message}") % { project_full_path: project.full_path, message: e.message })
+ ServiceResponse.error(
+ message: s_("UpdateRepositoryStorage|Error moving repository storage for %{project_full_path} - %{message}") % { project_full_path: project.full_path, message: e.message }
+ )
end
private
@@ -40,15 +52,19 @@ module Projects
Gitlab::GitalyClient.filesystem_id(old_storage) == Gitlab::GitalyClient.filesystem_id(new_storage)
end
- def mirror_repositories(new_repository_storage_key)
- mirror_repository(new_repository_storage_key)
+ def mirror_repositories
+ mirror_repository
if project.wiki.repository_exists?
- mirror_repository(new_repository_storage_key, type: Gitlab::GlRepository::WIKI)
+ mirror_repository(type: Gitlab::GlRepository::WIKI)
+ end
+
+ if project.design_repository.exists?
+ mirror_repository(type: ::Gitlab::GlRepository::DESIGN)
end
end
- def mirror_repository(new_storage_key, type: Gitlab::GlRepository::PROJECT)
+ def mirror_repository(type: Gitlab::GlRepository::PROJECT)
unless wait_for_pushes(type)
raise Error, s_('UpdateRepositoryStorage|Timeout waiting for %{type} repository pushes') % { type: type.name }
end
@@ -60,7 +76,7 @@ module Projects
# Initialize a git repository on the target path
new_repository = Gitlab::Git::Repository.new(
- new_storage_key,
+ destination_storage_name,
raw_repository.relative_path,
raw_repository.gl_repository,
full_path
@@ -94,11 +110,18 @@ module Projects
wiki.disk_path,
"#{new_project_path}.wiki")
end
+
+ if design_repository.exists?
+ GitlabShellWorker.perform_async(:mv_repository,
+ old_repository_storage,
+ design_repository.disk_path,
+ "#{new_project_path}.design")
+ end
end
end
def moved_path(path)
- "#{path}+#{project.id}+moved+#{Time.now.to_i}"
+ "#{path}+#{project.id}+moved+#{Time.current.to_i}"
end
# The underlying FetchInternalRemote call uses a `git fetch` to move data
@@ -128,5 +151,3 @@ module Projects
end
end
end
-
-Projects::UpdateRepositoryStorageService.prepend_if_ee('EE::Projects::UpdateRepositoryStorageService')
diff --git a/app/services/prometheus/proxy_service.rb b/app/services/prometheus/proxy_service.rb
index 99c739a630b..085cfc76196 100644
--- a/app/services/prometheus/proxy_service.rb
+++ b/app/services/prometheus/proxy_service.rb
@@ -17,6 +17,7 @@ module Prometheus
# is expected to change *and* be fetched again by the frontend
self.reactive_cache_refresh_interval = 90.seconds
self.reactive_cache_lifetime = 1.minute
+ self.reactive_cache_work_type = :external_dependency
self.reactive_cache_worker_finder = ->(_id, *args) { from_cache(*args) }
attr_accessor :proxyable, :method, :path, :params
diff --git a/app/services/prometheus/proxy_variable_substitution_service.rb b/app/services/prometheus/proxy_variable_substitution_service.rb
index 240586c8419..aa3a09ba05c 100644
--- a/app/services/prometheus/proxy_variable_substitution_service.rb
+++ b/app/services/prometheus/proxy_variable_substitution_service.rb
@@ -4,11 +4,20 @@ module Prometheus
class ProxyVariableSubstitutionService < BaseService
include Stepable
+ VARIABLE_INTERPOLATION_REGEX = /
+ {{ # Variable needs to be wrapped in these chars.
+ \s* # Allow whitespace before and after the variable name.
+ (?<variable> # Named capture.
+ \w+ # Match one or more word characters.
+ )
+ \s*
+ }}
+ /x.freeze
+
steps :validate_variables,
:add_params_to_result,
:substitute_params,
- :substitute_ruby_variables,
- :substitute_liquid_variables
+ :substitute_variables
def initialize(environment, params = {})
@environment, @params = environment, params.deep_dup
@@ -46,37 +55,28 @@ module Prometheus
success(result)
end
- def substitute_liquid_variables(result)
+ def substitute_variables(result)
return success(result) unless query(result)
- result[:params][:query] =
- TemplateEngines::LiquidService.new(query(result)).render(full_context)
+ result[:params][:query] = gsub(query(result), full_context)
success(result)
- rescue TemplateEngines::LiquidService::RenderError => e
- error(e.message)
end
- def substitute_ruby_variables(result)
- return success(result) unless query(result)
-
- # The % operator doesn't replace variables if the hash contains string
- # keys.
- result[:params][:query] = query(result) % predefined_context.symbolize_keys
-
- success(result)
- rescue TypeError, ArgumentError => exception
- log_error(exception.message)
- Gitlab::ErrorTracking.track_exception(exception, {
- template_string: query(result),
- variables: predefined_context
- })
-
- error(_('Malformed string'))
+ def gsub(string, context)
+ # Search for variables of the form `{{variable}}` in the string and replace
+ # them with their value.
+ string.gsub(VARIABLE_INTERPOLATION_REGEX) do |match|
+ # Replace with the value of the variable, or if there is no such variable,
+ # replace the invalid variable with itself. So,
+ # `up{instance="{{invalid_variable}}"}` will remain
+ # `up{instance="{{invalid_variable}}"}` after substitution.
+ context.fetch($~[:variable], match)
+ end
end
def predefined_context
- @predefined_context ||= Gitlab::Prometheus::QueryVariables.call(@environment)
+ Gitlab::Prometheus::QueryVariables.call(@environment).stringify_keys
end
def full_context
diff --git a/app/services/releases/create_service.rb b/app/services/releases/create_service.rb
index 9a0a876454f..81ca9d6d123 100644
--- a/app/services/releases/create_service.rb
+++ b/app/services/releases/create_service.rb
@@ -47,11 +47,17 @@ module Releases
release.save!
+ notify_create_release(release)
+
success(tag: tag, release: release)
rescue => e
error(e.message, 400)
end
+ def notify_create_release(release)
+ NotificationService.new.async.send_new_release_notifications(release)
+ end
+
def build_release(tag)
project.releases.build(
name: name,
diff --git a/app/services/resource_access_tokens/create_service.rb b/app/services/resource_access_tokens/create_service.rb
new file mode 100644
index 00000000000..c8e86e68383
--- /dev/null
+++ b/app/services/resource_access_tokens/create_service.rb
@@ -0,0 +1,111 @@
+# frozen_string_literal: true
+
+module ResourceAccessTokens
+ class CreateService < BaseService
+ def initialize(current_user, resource, params = {})
+ @resource_type = resource.class.name.downcase
+ @resource = resource
+ @current_user = current_user
+ @params = params.dup
+ end
+
+ def execute
+ return unless feature_enabled?
+ return error("User does not have permission to create #{resource_type} Access Token") unless has_permission_to_create?
+
+ # We skip authorization by default, since the user creating the bot is not an admin
+ # and project/group bot users are not created via sign-up
+ user = create_user
+
+ return error(user.errors.full_messages.to_sentence) unless user.persisted?
+ return error("Failed to provide maintainer access") unless provision_access(resource, user)
+
+ token_response = create_personal_access_token(user)
+
+ if token_response.success?
+ success(token_response.payload[:personal_access_token])
+ else
+ error(token_response.message)
+ end
+ end
+
+ private
+
+ attr_reader :resource_type, :resource
+
+ def feature_enabled?
+ ::Feature.enabled?(:resource_access_token, resource)
+ end
+
+ def has_permission_to_create?
+ case resource_type
+ when 'project'
+ can?(current_user, :admin_project, resource)
+ when 'group'
+ can?(current_user, :admin_group, resource)
+ else
+ false
+ end
+ end
+
+ def create_user
+ Users::CreateService.new(current_user, default_user_params).execute(skip_authorization: true)
+ end
+
+ def default_user_params
+ {
+ name: params[:name] || "#{resource.name.to_s.humanize} bot",
+ email: generate_email,
+ username: generate_username,
+ user_type: "#{resource_type}_bot".to_sym
+ }
+ end
+
+ def generate_username
+ base_username = "#{resource_type}_#{resource.id}_bot"
+
+ uniquify.string(base_username) { |s| User.find_by_username(s) }
+ end
+
+ def generate_email
+ email_pattern = "#{resource_type}#{resource.id}_bot%s@example.com"
+
+ uniquify.string(-> (n) { Kernel.sprintf(email_pattern, n) }) do |s|
+ User.find_by_email(s)
+ end
+ end
+
+ def uniquify
+ Uniquify.new
+ end
+
+ def create_personal_access_token(user)
+ PersonalAccessTokens::CreateService.new(user, personal_access_token_params).execute
+ end
+
+ def personal_access_token_params
+ {
+ name: params[:name] || "#{resource_type}_bot",
+ impersonation: false,
+ scopes: params[:scopes] || default_scopes,
+ expires_at: params[:expires_at] || nil
+ }
+ end
+
+ def default_scopes
+ Gitlab::Auth.resource_bot_scopes
+ end
+
+ def provision_access(resource, user)
+ resource.add_maintainer(user)
+ end
+
+ def error(message)
+ ServiceResponse.error(message: message)
+ end
+
+ def success(access_token)
+ ServiceResponse.success(payload: { access_token: access_token })
+ end
+ end
+end
diff --git a/app/services/resource_access_tokens/revoke_service.rb b/app/services/resource_access_tokens/revoke_service.rb
new file mode 100644
index 00000000000..eea6bff572b
--- /dev/null
+++ b/app/services/resource_access_tokens/revoke_service.rb
@@ -0,0 +1,65 @@
+# frozen_string_literal: true
+
+module ResourceAccessTokens
+ class RevokeService < BaseService
+ include Gitlab::Utils::StrongMemoize
+
+ RevokeAccessTokenError = Class.new(RuntimeError)
+
+ def initialize(current_user, resource, access_token)
+ @current_user = current_user
+ @access_token = access_token
+ @bot_user = access_token.user
+ @resource = resource
+ end
+
+ def execute
+ return error("Failed to find bot user") unless find_member
+
+ PersonalAccessToken.transaction do
+ access_token.revoke!
+
+ raise RevokeAccessTokenError, "Failed to remove #{bot_user.name} member from: #{resource.name}" unless remove_member
+
+ raise RevokeAccessTokenError, "Migration to ghost user failed" unless migrate_to_ghost_user
+ end
+
+ success("Revoked access token: #{access_token.name}")
+ rescue ActiveRecord::ActiveRecordError, RevokeAccessTokenError => error
+ log_error("Failed to revoke access token for #{bot_user.name}: #{error.message}")
+ error(error.message)
+ end
+
+ private
+
+ attr_reader :current_user, :access_token, :bot_user, :resource
+
+ def remove_member
+ ::Members::DestroyService.new(current_user).execute(find_member)
+ end
+
+ def migrate_to_ghost_user
+ ::Users::MigrateToGhostUserService.new(bot_user).execute
+ end
+
+ def find_member
+ strong_memoize(:member) do
+ if resource.is_a?(Project)
+ resource.project_member(bot_user)
+ elsif resource.is_a?(Group)
+ resource.group_member(bot_user)
+ else
+ false
+ end
+ end
+ end
+
+ def error(message)
+ ServiceResponse.error(message: message)
+ end
+
+ def success(message)
+ ServiceResponse.success(message: message)
+ end
+ end
+end
diff --git a/app/services/resource_events/base_synthetic_notes_builder_service.rb b/app/services/resource_events/base_synthetic_notes_builder_service.rb
index 1b85ca811a1..db8bf6e4b74 100644
--- a/app/services/resource_events/base_synthetic_notes_builder_service.rb
+++ b/app/services/resource_events/base_synthetic_notes_builder_service.rb
@@ -26,7 +26,7 @@ module ResourceEvents
def since_fetch_at(events)
return events unless params[:last_fetched_at].present?
- last_fetched_at = Time.at(params.fetch(:last_fetched_at).to_i)
+ last_fetched_at = Time.zone.at(params.fetch(:last_fetched_at).to_i)
events.created_after(last_fetched_at - NotesFinder::FETCH_OVERLAP)
end
diff --git a/app/services/resource_events/change_milestone_service.rb b/app/services/resource_events/change_milestone_service.rb
index ea196822f74..82c3e2acad5 100644
--- a/app/services/resource_events/change_milestone_service.rb
+++ b/app/services/resource_events/change_milestone_service.rb
@@ -2,13 +2,14 @@
module ResourceEvents
class ChangeMilestoneService
- attr_reader :resource, :user, :event_created_at, :milestone
+ attr_reader :resource, :user, :event_created_at, :milestone, :old_milestone
- def initialize(resource, user, created_at: Time.now)
+ def initialize(resource, user, created_at: Time.current, old_milestone:)
@resource = resource
@user = user
@event_created_at = created_at
@milestone = resource&.milestone
+ @old_milestone = old_milestone
end
def execute
@@ -26,7 +27,7 @@ module ResourceEvents
{
user_id: user.id,
created_at: event_created_at,
- milestone_id: milestone&.id,
+ milestone_id: action == :add ? milestone&.id : old_milestone&.id,
state: ResourceMilestoneEvent.states[resource.state],
action: ResourceMilestoneEvent.actions[action],
key => resource.id
diff --git a/app/services/resources/create_access_token_service.rb b/app/services/resources/create_access_token_service.rb
deleted file mode 100644
index fd3c8d78e58..00000000000
--- a/app/services/resources/create_access_token_service.rb
+++ /dev/null
@@ -1,111 +0,0 @@
-# frozen_string_literal: true
-
-module Resources
- class CreateAccessTokenService < BaseService
- attr_accessor :resource_type, :resource
-
- def initialize(resource_type, resource, user, params = {})
- @resource_type = resource_type
- @resource = resource
- @current_user = user
- @params = params.dup
- end
-
- def execute
- return unless feature_enabled?
- return error("User does not have permission to create #{resource_type} Access Token") unless has_permission_to_create?
-
- # We skip authorization by default, since the user creating the bot is not an admin
- # and project/group bot users are not created via sign-up
- user = create_user
-
- return error(user.errors.full_messages.to_sentence) unless user.persisted?
- return error("Failed to provide maintainer access") unless provision_access(resource, user)
-
- token_response = create_personal_access_token(user)
-
- if token_response.success?
- success(token_response.payload[:personal_access_token])
- else
- error(token_response.message)
- end
- end
-
- private
-
- def feature_enabled?
- ::Feature.enabled?(:resource_access_token, resource)
- end
-
- def has_permission_to_create?
- case resource_type
- when 'project'
- can?(current_user, :admin_project, resource)
- when 'group'
- can?(current_user, :admin_group, resource)
- else
- false
- end
- end
-
- def create_user
- Users::CreateService.new(current_user, default_user_params).execute(skip_authorization: true)
- end
-
- def default_user_params
- {
- name: params[:name] || "#{resource.name.to_s.humanize} bot",
- email: generate_email,
- username: generate_username,
- user_type: "#{resource_type}_bot".to_sym
- }
- end
-
- def generate_username
- base_username = "#{resource_type}_#{resource.id}_bot"
-
- uniquify.string(base_username) { |s| User.find_by_username(s) }
- end
-
- def generate_email
- email_pattern = "#{resource_type}#{resource.id}_bot%s@example.com"
-
- uniquify.string(-> (n) { Kernel.sprintf(email_pattern, n) }) do |s|
- User.find_by_email(s)
- end
- end
-
- def uniquify
- Uniquify.new
- end
-
- def create_personal_access_token(user)
- PersonalAccessTokens::CreateService.new(user, personal_access_token_params).execute
- end
-
- def personal_access_token_params
- {
- name: "#{resource_type}_bot",
- impersonation: false,
- scopes: params[:scopes] || default_scopes,
- expires_at: params[:expires_at] || nil
- }
- end
-
- def default_scopes
- Gitlab::Auth::API_SCOPES + Gitlab::Auth::REPOSITORY_SCOPES + Gitlab::Auth.registry_scopes - [:read_user]
- end
-
- def provision_access(resource, user)
- resource.add_maintainer(user)
- end
-
- def error(message)
- ServiceResponse.error(message: message)
- end
-
- def success(access_token)
- ServiceResponse.success(payload: { access_token: access_token })
- end
- end
-end
diff --git a/app/services/search/snippet_service.rb b/app/services/search/snippet_service.rb
index e686d3bf7c2..30401b28571 100644
--- a/app/services/search/snippet_service.rb
+++ b/app/services/search/snippet_service.rb
@@ -7,7 +7,7 @@ module Search
end
def scope
- @scope ||= %w[snippet_titles].delete(params[:scope]) { 'snippet_blobs' }
+ @scope ||= 'snippet_titles'
end
end
end
diff --git a/app/services/search_service.rb b/app/services/search_service.rb
index c96599f9958..bf21eba28f7 100644
--- a/app/services/search_service.rb
+++ b/app/services/search_service.rb
@@ -6,6 +6,9 @@ class SearchService
SEARCH_TERM_LIMIT = 64
SEARCH_CHAR_LIMIT = 4096
+ DEFAULT_PER_PAGE = Gitlab::SearchResults::DEFAULT_PER_PAGE
+ MAX_PER_PAGE = 200
+
def initialize(current_user, params = {})
@current_user = current_user
@params = params.dup
@@ -60,11 +63,19 @@ class SearchService
end
def search_objects
- @search_objects ||= redact_unauthorized_results(search_results.objects(scope, params[:page]))
+ @search_objects ||= redact_unauthorized_results(search_results.objects(scope, page: params[:page], per_page: per_page))
end
private
+ def per_page
+ per_page_param = params[:per_page].to_i
+
+ return DEFAULT_PER_PAGE unless per_page_param.positive?
+
+ [MAX_PER_PAGE, per_page_param].min
+ end
+
def visible_result?(object)
return true unless object.respond_to?(:to_ability_name) && DeclarativePolicy.has_policy?(object)
@@ -75,13 +86,13 @@ class SearchService
results = results_collection.to_a
permitted_results = results.select { |object| visible_result?(object) }
- filtered_results = (results - permitted_results).each_with_object({}) do |object, memo|
+ redacted_results = (results - permitted_results).each_with_object({}) do |object, memo|
memo[object.id] = { ability: :"read_#{object.to_ability_name}", id: object.id, class_name: object.class.name }
end
- log_redacted_search_results(filtered_results.values) if filtered_results.any?
+ log_redacted_search_results(redacted_results.values) if redacted_results.any?
- return results_collection.id_not_in(filtered_results.keys) if results_collection.is_a?(ActiveRecord::Relation)
+ return results_collection.id_not_in(redacted_results.keys) if results_collection.is_a?(ActiveRecord::Relation)
Kaminari.paginate_array(
permitted_results,
diff --git a/app/services/snippets/base_service.rb b/app/services/snippets/base_service.rb
index 2b450db0b83..81d12997335 100644
--- a/app/services/snippets/base_service.rb
+++ b/app/services/snippets/base_service.rb
@@ -2,8 +2,32 @@
module Snippets
class BaseService < ::BaseService
+ include SpamCheckMethods
+
+ CreateRepositoryError = Class.new(StandardError)
+
+ attr_reader :uploaded_files
+
+ def initialize(project, user = nil, params = {})
+ super
+
+ @uploaded_files = Array(@params.delete(:files).presence)
+
+ filter_spam_check_params
+ end
+
private
+ def visibility_allowed?(snippet, visibility_level)
+ Gitlab::VisibilityLevel.allowed_for?(current_user, visibility_level)
+ end
+
+ def error_forbidden_visibility(snippet)
+ deny_visibility_level(snippet)
+
+ snippet_error_response(snippet, 403)
+ end
+
def snippet_error_response(snippet, http_status)
ServiceResponse.error(
message: snippet.errors.full_messages.to_sentence,
@@ -11,5 +35,22 @@ module Snippets
payload: { snippet: snippet }
)
end
+
+ def add_snippet_repository_error(snippet:, error:)
+ message = repository_error_message(error)
+
+ snippet.errors.add(:repository, message)
+ end
+
+ def repository_error_message(error)
+ message = self.is_a?(Snippets::CreateService) ? _("Error creating the snippet") : _("Error updating the snippet")
+
+ # We only want to include additional error detail in the message
+ # if the error is not a CommitError because we cannot guarantee the message
+ # will be user-friendly
+ message += " - #{error.message}" unless error.instance_of?(SnippetRepository::CommitError)
+
+ message
+ end
end
end
diff --git a/app/services/snippets/create_service.rb b/app/services/snippets/create_service.rb
index 155013db344..ed6da3a0ad0 100644
--- a/app/services/snippets/create_service.rb
+++ b/app/services/snippets/create_service.rb
@@ -2,23 +2,11 @@
module Snippets
class CreateService < Snippets::BaseService
- include SpamCheckMethods
-
- CreateRepositoryError = Class.new(StandardError)
-
def execute
- filter_spam_check_params
-
- @snippet = if project
- project.snippets.build(params)
- else
- PersonalSnippet.new(params)
- end
-
- unless Gitlab::VisibilityLevel.allowed_for?(current_user, @snippet.visibility_level)
- deny_visibility_level(@snippet)
+ @snippet = build_from_params
- return snippet_error_response(@snippet, 403)
+ unless visibility_allowed?(@snippet, @snippet.visibility_level)
+ return error_forbidden_visibility(@snippet)
end
@snippet.author = current_user
@@ -29,6 +17,8 @@ module Snippets
UserAgentDetailService.new(@snippet, @request).create
Gitlab::UsageDataCounters::SnippetCounter.count(:create)
+ move_temporary_files
+
ServiceResponse.success(payload: { snippet: @snippet } )
else
snippet_error_response(@snippet, 400)
@@ -37,10 +27,18 @@ module Snippets
private
+ def build_from_params
+ if project
+ project.snippets.build(params)
+ else
+ PersonalSnippet.new(params)
+ end
+ end
+
def save_and_commit
snippet_saved = @snippet.save
- if snippet_saved && Feature.enabled?(:version_snippets, current_user)
+ if snippet_saved
create_repository
create_commit
end
@@ -60,7 +58,7 @@ module Snippets
@snippet = @snippet.dup
end
- @snippet.errors.add(:base, e.message)
+ add_snippet_repository_error(snippet: @snippet, error: e)
false
end
@@ -83,5 +81,13 @@ module Snippets
def snippet_files
[{ file_path: params[:file_name], content: params[:content] }]
end
+
+ def move_temporary_files
+ return unless @snippet.is_a?(PersonalSnippet)
+
+ uploaded_files.each do |file|
+ FileMover.new(file, from_model: current_user, to_model: @snippet).execute
+ end
+ end
end
end
diff --git a/app/services/snippets/update_service.rb b/app/services/snippets/update_service.rb
index e56b20c6057..2dc9266dbd0 100644
--- a/app/services/snippets/update_service.rb
+++ b/app/services/snippets/update_service.rb
@@ -2,24 +2,15 @@
module Snippets
class UpdateService < Snippets::BaseService
- include SpamCheckMethods
+ COMMITTABLE_ATTRIBUTES = %w(file_name content).freeze
UpdateError = Class.new(StandardError)
- CreateRepositoryError = Class.new(StandardError)
def execute(snippet)
- # check that user is allowed to set specified visibility_level
- new_visibility = visibility_level
-
- if new_visibility && new_visibility.to_i != snippet.visibility_level
- unless Gitlab::VisibilityLevel.allowed_for?(current_user, new_visibility)
- deny_visibility_level(snippet, new_visibility)
-
- return snippet_error_response(snippet, 403)
- end
+ if visibility_changed?(snippet) && !visibility_allowed?(snippet, visibility_level)
+ return error_forbidden_visibility(snippet)
end
- filter_spam_check_params
snippet.assign_attributes(params)
spam_check(snippet, current_user)
@@ -34,30 +25,32 @@ module Snippets
private
+ def visibility_changed?(snippet)
+ visibility_level && visibility_level.to_i != snippet.visibility_level
+ end
+
def save_and_commit(snippet)
return false unless snippet.save
- # In order to avoid non migrated snippets scenarios,
- # if the snippet does not have a repository we created it
- # We don't need to check if the repository exists
- # because `create_repository` already handles it
- if Feature.enabled?(:version_snippets, current_user)
- create_repository_for(snippet)
- end
+ # If the updated attributes does not need to update
+ # the repository we can just return
+ return true unless committable_attributes?
- # If the snippet repository exists we commit always
- # the changes
- create_commit(snippet) if snippet.repository_exists?
+ create_repository_for(snippet)
+ create_commit(snippet)
true
rescue => e
- # Restore old attributes
+ # Restore old attributes but re-assign changes so they're not lost
unless snippet.previous_changes.empty?
snippet.previous_changes.each { |attr, value| snippet[attr] = value[0] }
snippet.save
+
+ snippet.assign_attributes(params)
end
- snippet.errors.add(:repository, 'Error updating the snippet')
+ add_snippet_repository_error(snippet: snippet, error: e)
+
log_error(e.message)
# If the commit action failed we remove it because
@@ -92,7 +85,7 @@ module Snippets
end
def snippet_files(snippet)
- [{ previous_path: snippet.blobs.first&.path,
+ [{ previous_path: snippet.file_name_on_repo,
file_path: params[:file_name],
content: params[:content] }]
end
@@ -104,5 +97,9 @@ module Snippets
def repository_empty?(snippet)
snippet.repository._uncached_exists? && !snippet.repository._uncached_has_visible_content?
end
+
+ def committable_attributes?
+ (params.stringify_keys.keys & COMMITTABLE_ATTRIBUTES).present?
+ end
end
end
diff --git a/app/services/spam/akismet_service.rb b/app/services/spam/akismet_service.rb
index 7d16743b3ed..ab35fb8700f 100644
--- a/app/services/spam/akismet_service.rb
+++ b/app/services/spam/akismet_service.rb
@@ -17,7 +17,7 @@ module Spam
params = {
type: 'comment',
text: text,
- created_at: DateTime.now,
+ created_at: DateTime.current,
author: owner_name,
author_email: owner_email,
referrer: options[:referrer]
diff --git a/app/services/spam/spam_action_service.rb b/app/services/spam/spam_action_service.rb
new file mode 100644
index 00000000000..f0a4aff4443
--- /dev/null
+++ b/app/services/spam/spam_action_service.rb
@@ -0,0 +1,91 @@
+# frozen_string_literal: true
+
+module Spam
+ class SpamActionService
+ include SpamConstants
+
+ attr_accessor :target, :request, :options
+ attr_reader :spam_log
+
+ def initialize(spammable:, request:)
+ @target = spammable
+ @request = request
+ @options = {}
+
+ if @request
+ @options[:ip_address] = @request.env['action_dispatch.remote_ip'].to_s
+ @options[:user_agent] = @request.env['HTTP_USER_AGENT']
+ @options[:referrer] = @request.env['HTTP_REFERRER']
+ else
+ @options[:ip_address] = @target.ip_address
+ @options[:user_agent] = @target.user_agent
+ end
+ end
+
+ def execute(api: false, recaptcha_verified:, spam_log_id:, user:)
+ if recaptcha_verified
+ # If it's a request which is already verified through reCAPTCHA,
+ # update the spam log accordingly.
+ SpamLog.verify_recaptcha!(user_id: user.id, id: spam_log_id)
+ else
+ return if allowlisted?(user)
+ return unless request
+ return unless check_for_spam?
+
+ perform_spam_service_check(api)
+ end
+ end
+
+ delegate :check_for_spam?, to: :target
+
+ private
+
+ def allowlisted?(user)
+ user.respond_to?(:gitlab_employee) && user.gitlab_employee?
+ end
+
+ def perform_spam_service_check(api)
+ # since we can check for spam, and recaptcha is not verified,
+ # ask the SpamVerdictService what to do with the target.
+ spam_verdict_service.execute.tap do |result|
+ case result
+ when REQUIRE_RECAPTCHA
+ create_spam_log(api)
+
+ break if target.allow_possible_spam?
+
+ target.needs_recaptcha!
+ when DISALLOW
+ # TODO: remove `unless target.allow_possible_spam?` once this flag has been passed to `SpamVerdictService`
+ # https://gitlab.com/gitlab-org/gitlab/-/issues/214739
+ target.spam! unless target.allow_possible_spam?
+ create_spam_log(api)
+ when ALLOW
+ target.clear_spam_flags!
+ end
+ end
+ end
+
+ def create_spam_log(api)
+ @spam_log = SpamLog.create!(
+ {
+ user_id: target.author_id,
+ title: target.spam_title,
+ description: target.spam_description,
+ source_ip: options[:ip_address],
+ user_agent: options[:user_agent],
+ noteable_type: target.class.to_s,
+ via_api: api
+ }
+ )
+
+ target.spam_log = spam_log
+ end
+
+ def spam_verdict_service
+ SpamVerdictService.new(target: target,
+ request: @request,
+ options: options)
+ end
+ end
+end
diff --git a/app/services/spam/spam_check_service.rb b/app/services/spam/spam_check_service.rb
deleted file mode 100644
index 3269f9d687a..00000000000
--- a/app/services/spam/spam_check_service.rb
+++ /dev/null
@@ -1,68 +0,0 @@
-# frozen_string_literal: true
-
-module Spam
- class SpamCheckService
- include AkismetMethods
-
- attr_accessor :target, :request, :options
- attr_reader :spam_log
-
- def initialize(spammable:, request:)
- @target = spammable
- @request = request
- @options = {}
-
- if @request
- @options[:ip_address] = @request.env['action_dispatch.remote_ip'].to_s
- @options[:user_agent] = @request.env['HTTP_USER_AGENT']
- @options[:referrer] = @request.env['HTTP_REFERRER']
- else
- @options[:ip_address] = @target.ip_address
- @options[:user_agent] = @target.user_agent
- end
- end
-
- def execute(api: false, recaptcha_verified:, spam_log_id:, user_id:)
- if recaptcha_verified
- # If it's a request which is already verified through recaptcha,
- # update the spam log accordingly.
- SpamLog.verify_recaptcha!(user_id: user_id, id: spam_log_id)
- else
- # Otherwise, it goes to Akismet for spam check.
- # If so, it assigns spammable object as "spam" and creates a SpamLog record.
- possible_spam = check(api)
- target.spam = possible_spam unless target.allow_possible_spam?
- target.spam_log = spam_log
- end
- end
-
- private
-
- def check(api)
- return unless request
- return unless check_for_spam?
- return unless akismet.spam?
-
- create_spam_log(api)
- true
- end
-
- def check_for_spam?
- target.check_for_spam?
- end
-
- def create_spam_log(api)
- @spam_log = SpamLog.create!(
- {
- user_id: target.author_id,
- title: target.spam_title,
- description: target.spam_description,
- source_ip: options[:ip_address],
- user_agent: options[:user_agent],
- noteable_type: target.class.to_s,
- via_api: api
- }
- )
- end
- end
-end
diff --git a/app/services/spam/spam_constants.rb b/app/services/spam/spam_constants.rb
new file mode 100644
index 00000000000..085bac684c4
--- /dev/null
+++ b/app/services/spam/spam_constants.rb
@@ -0,0 +1,9 @@
+# frozen_string_literal: true
+
+module Spam
+ module SpamConstants
+ REQUIRE_RECAPTCHA = :recaptcha
+ DISALLOW = :disallow
+ ALLOW = :allow
+ end
+end
diff --git a/app/services/spam/spam_verdict_service.rb b/app/services/spam/spam_verdict_service.rb
new file mode 100644
index 00000000000..2b4d5f4a984
--- /dev/null
+++ b/app/services/spam/spam_verdict_service.rb
@@ -0,0 +1,26 @@
+# frozen_string_literal: true
+
+module Spam
+ class SpamVerdictService
+ include AkismetMethods
+ include SpamConstants
+
+ def initialize(target:, request:, options:)
+ @target = target
+ @request = request
+ @options = options
+ end
+
+ def execute
+ if akismet.spam?
+ Gitlab::Recaptcha.enabled? ? REQUIRE_RECAPTCHA : DISALLOW
+ else
+ ALLOW
+ end
+ end
+
+ private
+
+ attr_reader :target, :request, :options
+ end
+end
diff --git a/app/services/system_note_service.rb b/app/services/system_note_service.rb
index 1b9f5971f73..6bf04c55415 100644
--- a/app/services/system_note_service.rb
+++ b/app/services/system_note_service.rb
@@ -245,6 +245,34 @@ module SystemNoteService
def auto_resolve_prometheus_alert(noteable, project, author)
::SystemNotes::IssuablesService.new(noteable: noteable, project: project, author: author).auto_resolve_prometheus_alert
end
+
+ # Parameters:
+ # - version [DesignManagement::Version]
+ #
+ # Example Note text:
+ #
+ # "added [1 designs](link-to-version)"
+ # "changed [2 designs](link-to-version)"
+ #
+ # Returns [Array<Note>]: the created Note objects
+ def design_version_added(version)
+ ::SystemNotes::DesignManagementService.new(noteable: version.issue, project: version.issue.project, author: version.author).design_version_added(version)
+ end
+
+ # Called when a new discussion is created on a design
+ #
+ # discussion_note - DiscussionNote
+ #
+ # Example Note text:
+ #
+ # "started a discussion on screen.png"
+ #
+ # Returns the created Note object
+ def design_discussion_added(discussion_note)
+ design = discussion_note.noteable
+
+ ::SystemNotes::DesignManagementService.new(noteable: design.issue, project: design.project, author: discussion_note.author).design_discussion_added(discussion_note)
+ end
end
SystemNoteService.prepend_if_ee('EE::SystemNoteService')
diff --git a/app/services/system_notes/design_management_service.rb b/app/services/system_notes/design_management_service.rb
new file mode 100644
index 00000000000..a773877e25b
--- /dev/null
+++ b/app/services/system_notes/design_management_service.rb
@@ -0,0 +1,83 @@
+# frozen_string_literal: true
+
+module SystemNotes
+ class DesignManagementService < ::SystemNotes::BaseService
+ include ActionView::RecordIdentifier
+
+ # Parameters:
+ # - version [DesignManagement::Version]
+ #
+ # Example Note text:
+ #
+ # "added [1 designs](link-to-version)"
+ # "changed [2 designs](link-to-version)"
+ #
+ # Returns [Array<Note>]: the created Note objects
+ def design_version_added(version)
+ events = DesignManagement::Action.events
+ link_href = designs_path(version: version.id)
+
+ version.designs_by_event.map do |(event_name, designs)|
+ note_data = self.class.design_event_note_data(events[event_name])
+ icon_name = note_data[:icon]
+ n = designs.size
+
+ body = "%s [%d %s](%s)" % [note_data[:past_tense], n, 'design'.pluralize(n), link_href]
+
+ create_note(NoteSummary.new(noteable, project, author, body, action: icon_name))
+ end
+ end
+
+ # Called when a new discussion is created on a design
+ #
+ # discussion_note - DiscussionNote
+ #
+ # Example Note text:
+ #
+ # "started a discussion on screen.png"
+ #
+ # Returns the created Note object
+ def design_discussion_added(discussion_note)
+ design = discussion_note.noteable
+
+ body = _('started a discussion on %{design_link}') % {
+ design_link: '[%s](%s)' % [
+ design.filename,
+ designs_path(vueroute: design.filename, anchor: dom_id(discussion_note))
+ ]
+ }
+
+ action = :designs_discussion_added
+
+ create_note(NoteSummary.new(noteable, project, author, body, action: action))
+ end
+
+ # Take one of the `DesignManagement::Action.events` and
+ # return:
+ # * an English past-tense verb.
+ # * the name of an icon used in renderin a system note
+ #
+ # We do not currently internationalize our system notes,
+ # instead we just produce English-language descriptions.
+ # See: https://gitlab.com/gitlab-org/gitlab/issues/30408
+ # See: https://gitlab.com/gitlab-org/gitlab/issues/14056
+ def self.design_event_note_data(event)
+ case event
+ when DesignManagement::Action.events[:creation]
+ { icon: 'designs_added', past_tense: 'added' }
+ when DesignManagement::Action.events[:modification]
+ { icon: 'designs_modified', past_tense: 'updated' }
+ when DesignManagement::Action.events[:deletion]
+ { icon: 'designs_removed', past_tense: 'removed' }
+ else
+ raise "Unknown event: #{event}"
+ end
+ end
+
+ private
+
+ def designs_path(params = {})
+ url_helpers.designs_project_issue_path(project, noteable, params)
+ end
+ end
+end
diff --git a/app/services/tags/destroy_service.rb b/app/services/tags/destroy_service.rb
index 4f6ae07be7d..3a01192487d 100644
--- a/app/services/tags/destroy_service.rb
+++ b/app/services/tags/destroy_service.rb
@@ -18,11 +18,6 @@ module Tags
.new(project, current_user, tag: tag_name)
.execute
- push_data = build_push_data(tag)
- EventCreateService.new.push(project, current_user, push_data)
- project.execute_hooks(push_data.dup, :tag_push_hooks)
- project.execute_services(push_data.dup, :tag_push_hooks)
-
success('Tag was removed')
else
error('Failed to remove tag')
@@ -38,14 +33,5 @@ module Tags
def success(message)
super().merge(message: message)
end
-
- def build_push_data(tag)
- Gitlab::DataBuilder::Push.build(
- project: project,
- user: current_user,
- oldrev: tag.dereferenced_target.sha,
- newrev: Gitlab::Git::BLANK_SHA,
- ref: "#{Gitlab::Git::TAG_REF_PREFIX}#{tag.name}")
- end
end
end
diff --git a/app/services/template_engines/liquid_service.rb b/app/services/template_engines/liquid_service.rb
deleted file mode 100644
index 809ebd0316b..00000000000
--- a/app/services/template_engines/liquid_service.rb
+++ /dev/null
@@ -1,48 +0,0 @@
-# frozen_string_literal: true
-
-module TemplateEngines
- class LiquidService < BaseService
- RenderError = Class.new(StandardError)
-
- DEFAULT_RENDER_SCORE_LIMIT = 1_000
-
- def initialize(string)
- @template = Liquid::Template.parse(string)
- end
-
- def render(context, render_score_limit: DEFAULT_RENDER_SCORE_LIMIT)
- set_limits(render_score_limit)
-
- @template.render!(context.stringify_keys)
- rescue Liquid::MemoryError => e
- handle_exception(e, string: @string, context: context)
-
- raise RenderError, _('Memory limit exceeded while rendering template')
- rescue Liquid::Error => e
- handle_exception(e, string: @string, context: context)
-
- raise RenderError, _('Error rendering query')
- end
-
- private
-
- def set_limits(render_score_limit)
- @template.resource_limits.render_score_limit = render_score_limit
-
- # We can also set assign_score_limit and render_length_limit if required.
-
- # render_score_limit limits the number of nodes (string, variable, block, tags)
- # that are allowed in the template.
- # render_length_limit seems to limit the sum of the bytesize of all node blocks.
- # assign_score_limit seems to limit the sum of the bytesize of all capture blocks.
- end
-
- def handle_exception(exception, extra = {})
- log_error(exception.message)
- Gitlab::ErrorTracking.track_exception(exception, {
- template_string: extra[:string],
- variables: extra[:context]
- })
- end
- end
-end
diff --git a/app/services/terraform/remote_state_handler.rb b/app/services/terraform/remote_state_handler.rb
index 5bb6f6a1dee..d180a3a2432 100644
--- a/app/services/terraform/remote_state_handler.rb
+++ b/app/services/terraform/remote_state_handler.rb
@@ -42,7 +42,7 @@ module Terraform
state.lock_xid = params[:lock_id]
state.locked_by_user = current_user
- state.locked_at = Time.now
+ state.locked_at = Time.current
state.save!
end
diff --git a/app/services/user_project_access_changed_service.rb b/app/services/user_project_access_changed_service.rb
index 21d0861ac3f..66f1ccfab70 100644
--- a/app/services/user_project_access_changed_service.rb
+++ b/app/services/user_project_access_changed_service.rb
@@ -1,17 +1,26 @@
# frozen_string_literal: true
class UserProjectAccessChangedService
+ DELAY = 1.hour
+
+ HIGH_PRIORITY = :high
+ LOW_PRIORITY = :low
+
def initialize(user_ids)
@user_ids = Array.wrap(user_ids)
end
- def execute(blocking: true)
+ def execute(blocking: true, priority: HIGH_PRIORITY)
bulk_args = @user_ids.map { |id| [id] }
if blocking
AuthorizedProjectsWorker.bulk_perform_and_wait(bulk_args)
else
- AuthorizedProjectsWorker.bulk_perform_async(bulk_args) # rubocop:disable Scalability/BulkPerformWithContext
+ if priority == HIGH_PRIORITY
+ AuthorizedProjectsWorker.bulk_perform_async(bulk_args) # rubocop:disable Scalability/BulkPerformWithContext
+ else
+ AuthorizedProjectUpdate::UserRefreshWithLowUrgencyWorker.bulk_perform_in(DELAY, bulk_args) # rubocop:disable Scalability/BulkPerformWithContext
+ end
end
end
end
diff --git a/app/services/users/migrate_to_ghost_user_service.rb b/app/services/users/migrate_to_ghost_user_service.rb
index e7186fdfb63..5ca9ed67e56 100644
--- a/app/services/users/migrate_to_ghost_user_service.rb
+++ b/app/services/users/migrate_to_ghost_user_service.rb
@@ -52,6 +52,7 @@ module Users
migrate_notes
migrate_abuse_reports
migrate_award_emoji
+ migrate_snippets
end
# rubocop: disable CodeReuse/ActiveRecord
@@ -79,6 +80,11 @@ module Users
def migrate_award_emoji
user.award_emoji.update_all(user_id: ghost_user.id)
end
+
+ def migrate_snippets
+ snippets = user.snippets.only_project_snippets
+ snippets.update_all(author_id: ghost_user.id)
+ end
end
end
diff --git a/app/services/verify_pages_domain_service.rb b/app/services/verify_pages_domain_service.rb
index b53c3145caf..a9e219547d7 100644
--- a/app/services/verify_pages_domain_service.rb
+++ b/app/services/verify_pages_domain_service.rb
@@ -37,7 +37,7 @@ class VerifyPagesDomainService < BaseService
# Prevent any pre-existing grace period from being truncated
reverify = [domain.enabled_until, VERIFICATION_PERIOD.from_now].compact.max
- domain.assign_attributes(verified_at: Time.now, enabled_until: reverify, remove_at: nil)
+ domain.assign_attributes(verified_at: Time.current, enabled_until: reverify, remove_at: nil)
domain.save!(validate: false)
if was_disabled
@@ -73,7 +73,7 @@ class VerifyPagesDomainService < BaseService
# A domain is only expired until `disable!` has been called
def expired?
- domain.enabled_until && domain.enabled_until < Time.now
+ domain.enabled_until && domain.enabled_until < Time.current
end
def dns_record_present?
diff --git a/app/services/wiki_pages/base_service.rb b/app/services/wiki_pages/base_service.rb
index 2e774973ca5..a0256ea5e69 100644
--- a/app/services/wiki_pages/base_service.rb
+++ b/app/services/wiki_pages/base_service.rb
@@ -6,13 +6,13 @@ module WikiPages
# - external_action: the action we report to external clients with webhooks
# - usage_counter_action: the action that we count in out internal counters
# - event_action: what we record as the value of `Event#action`
- class BaseService < ::BaseService
+ class BaseService < ::BaseContainerService
private
def execute_hooks(page)
page_data = payload(page)
- @project.execute_hooks(page_data, :wiki_page_hooks)
- @project.execute_services(page_data, :wiki_page_hooks)
+ container.execute_hooks(page_data, :wiki_page_hooks)
+ container.execute_services(page_data, :wiki_page_hooks)
increment_usage
create_wiki_event(page)
end
@@ -46,12 +46,9 @@ module WikiPages
def create_wiki_event(page)
return unless ::Feature.enabled?(:wiki_events)
- slug = slug_for_page(page)
+ response = WikiPages::EventCreateService.new(current_user).execute(slug_for_page(page), page, event_action)
- Event.transaction do
- wiki_page_meta = WikiPage::Meta.find_or_create(slug, page)
- EventCreateService.new.wiki_event(wiki_page_meta, current_user, event_action)
- end
+ log_error(response.message) if response.error?
end
def slug_for_page(page)
diff --git a/app/services/wiki_pages/create_service.rb b/app/services/wiki_pages/create_service.rb
index 811f460e042..4ef19676d82 100644
--- a/app/services/wiki_pages/create_service.rb
+++ b/app/services/wiki_pages/create_service.rb
@@ -3,8 +3,8 @@
module WikiPages
class CreateService < WikiPages::BaseService
def execute
- project_wiki = ProjectWiki.new(@project, current_user)
- page = WikiPage.new(project_wiki)
+ wiki = Wiki.for_container(container, current_user)
+ page = WikiPage.new(wiki)
if page.create(@params)
execute_hooks(page)
diff --git a/app/services/wiki_pages/event_create_service.rb b/app/services/wiki_pages/event_create_service.rb
new file mode 100644
index 00000000000..18a45d057a9
--- /dev/null
+++ b/app/services/wiki_pages/event_create_service.rb
@@ -0,0 +1,30 @@
+# frozen_string_literal: true
+
+module WikiPages
+ class EventCreateService
+ # @param [User] author The event author
+ def initialize(author)
+ raise ArgumentError, 'author must not be nil' unless author
+
+ @author = author
+ end
+
+ def execute(slug, page, action)
+ return ServiceResponse.success(message: 'No event created as `wiki_events` feature is disabled') unless ::Feature.enabled?(:wiki_events)
+
+ event = Event.transaction do
+ wiki_page_meta = WikiPage::Meta.find_or_create(slug, page)
+
+ ::EventCreateService.new.wiki_event(wiki_page_meta, author, action)
+ end
+
+ ServiceResponse.success(payload: { event: event })
+ rescue ::EventCreateService::IllegalActionError, ::ActiveRecord::ActiveRecordError => e
+ ServiceResponse.error(message: e.message, payload: { error: e })
+ end
+
+ private
+
+ attr_reader :author
+ end
+end
diff --git a/app/services/wikis/create_attachment_service.rb b/app/services/wikis/create_attachment_service.rb
index 6ef6cbc3c12..82179459345 100644
--- a/app/services/wikis/create_attachment_service.rb
+++ b/app/services/wikis/create_attachment_service.rb
@@ -5,12 +5,15 @@ module Wikis
ATTACHMENT_PATH = 'uploads'
MAX_FILENAME_LENGTH = 255
- delegate :wiki, to: :project
+ attr_reader :container
+
+ delegate :wiki, to: :container
delegate :repository, to: :wiki
- def initialize(*args)
- super
+ def initialize(container:, current_user: nil, params: {})
+ super(nil, current_user, params)
+ @container = container
@file_name = clean_file_name(params[:file_name])
@file_path = File.join(ATTACHMENT_PATH, SecureRandom.hex, @file_name) if @file_name
@commit_message ||= "Upload attachment #{@file_name}"
@@ -51,7 +54,7 @@ module Wikis
end
def validate_permissions!
- unless can?(current_user, :create_wiki, project)
+ unless can?(current_user, :create_wiki, container)
raise_error('You are not allowed to push to the wiki')
end
end
diff --git a/app/uploaders/design_management/design_v432x230_uploader.rb b/app/uploaders/design_management/design_v432x230_uploader.rb
new file mode 100644
index 00000000000..ba48f381bbd
--- /dev/null
+++ b/app/uploaders/design_management/design_v432x230_uploader.rb
@@ -0,0 +1,45 @@
+# frozen_string_literal: true
+
+module DesignManagement
+ # This Uploader is used to generate and serve the smaller versions of
+ # the design files.
+ #
+ # The original (full-sized) design files are stored in Git LFS, and so
+ # have a different uploader, `LfsObjectUploader`.
+ class DesignV432x230Uploader < GitlabUploader
+ include CarrierWave::MiniMagick
+ include RecordsUploads::Concern
+ include ObjectStorage::Concern
+ prepend ObjectStorage::Extension::RecordsUploads
+
+ # We choose not to resize `image/ico` as we assume there will be no
+ # benefit in generating an 432x230 sized icon.
+ #
+ # We currently cannot resize `image/tiff`.
+ # See https://gitlab.com/gitlab-org/gitlab/issues/207740
+ #
+ # We currently choose not to resize `image/svg+xml` for security reasons.
+ # See https://gitlab.com/gitlab-org/gitlab/issues/207740#note_302766171
+ MIME_TYPE_WHITELIST = %w(image/png image/jpeg image/bmp image/gif).freeze
+
+ process resize_to_fit: [432, 230]
+
+ # Allow CarrierWave to reject files without correct mimetypes.
+ def content_type_whitelist
+ MIME_TYPE_WHITELIST
+ end
+
+ # Override `GitlabUploader` and always return false, otherwise local
+ # `LfsObject` files would be deleted.
+ # https://github.com/carrierwaveuploader/carrierwave/blob/f84672a/lib/carrierwave/uploader/cache.rb#L131-L135
+ def move_to_cache
+ false
+ end
+
+ private
+
+ def dynamic_segment
+ File.join(model.class.underscore, mounted_as.to_s, model.id.to_s)
+ end
+ end
+end
diff --git a/app/validators/cron_freeze_period_timezone_validator.rb b/app/validators/cron_freeze_period_timezone_validator.rb
new file mode 100644
index 00000000000..143a0262136
--- /dev/null
+++ b/app/validators/cron_freeze_period_timezone_validator.rb
@@ -0,0 +1,13 @@
+# frozen_string_literal: true
+
+# CronTimezoneValidator
+#
+# Custom validator for CronTimezone.
+class CronFreezePeriodTimezoneValidator < ActiveModel::EachValidator
+ def validate_each(record, attribute, value)
+ freeze_start_parser = Gitlab::Ci::CronParser.new(record.freeze_start, record.cron_timezone)
+ freeze_end_parser = Gitlab::Ci::CronParser.new(record.freeze_end, record.cron_timezone)
+
+ record.errors.add(attribute, " is invalid syntax") unless freeze_start_parser.cron_timezone_valid? && freeze_end_parser.cron_timezone_valid?
+ end
+end
diff --git a/app/validators/cron_validator.rb b/app/validators/cron_validator.rb
index bd48a7a6efb..6f42bdb5f9b 100644
--- a/app/validators/cron_validator.rb
+++ b/app/validators/cron_validator.rb
@@ -1,11 +1,16 @@
# frozen_string_literal: true
-# CronValidator
-#
-# Custom validator for Cron.
class CronValidator < ActiveModel::EachValidator
+ ATTRIBUTE_WHITELIST = %i[cron freeze_start freeze_end].freeze
+
+ NonWhitelistedAttributeError = Class.new(StandardError)
+
def validate_each(record, attribute, value)
- cron_parser = Gitlab::Ci::CronParser.new(record.cron, record.cron_timezone)
- record.errors.add(attribute, " is invalid syntax") unless cron_parser.cron_valid?
+ if ATTRIBUTE_WHITELIST.include?(attribute)
+ cron_parser = Gitlab::Ci::CronParser.new(record.public_send(attribute), record.cron_timezone) # rubocop:disable GitlabSecurity/PublicSend
+ record.errors.add(attribute, " is invalid syntax") unless cron_parser.cron_valid?
+ else
+ raise NonWhitelistedAttributeError.new "Non-whitelisted attribute"
+ end
end
end
diff --git a/app/views/admin/appearances/_form.html.haml b/app/views/admin/appearances/_form.html.haml
index a5f34d0dab2..5bb05bcba26 100644
--- a/app/views/admin/appearances/_form.html.haml
+++ b/app/views/admin/appearances/_form.html.haml
@@ -1,3 +1,5 @@
+- parsed_with_gfm = "Content parsed with #{link_to('GitLab Flavored Markdown', help_page_path('user/markdown'), target: '_blank')}.".html_safe
+
= form_for @appearance, url: admin_appearances_path, html: { class: 'prepend-top-default' } do |f|
= form_errors(@appearance)
@@ -57,7 +59,7 @@
= f.label :description, class: 'col-form-label label-bold'
= f.text_area :description, class: "form-control", rows: 10
.hint
- Description parsed with #{link_to "GitLab Flavored Markdown", help_page_path('user/markdown'), target: '_blank'}.
+ = parsed_with_gfm
.form-group
= f.label :logo, class: 'col-form-label label-bold pt-0'
%p
@@ -83,15 +85,30 @@
%p
= f.text_area :new_project_guidelines, class: "form-control", rows: 10
.hint
- Guidelines parsed with #{link_to "GitLab Flavored Markdown", help_page_path('user/markdown'), target: '_blank'}.
+ = parsed_with_gfm
+
+ %hr
+ .row
+ .col-lg-4.profile-settings-sidebar
+ %h4.prepend-top-0 Profile image guideline
+
+ .col-lg-8
+ .form-group
+ = f.label :profile_image_guidelines, class: 'col-form-label label-bold'
+ %p
+ = f.text_area :profile_image_guidelines, class: "form-control", rows: 10
+ .hint
+ = parsed_with_gfm
.prepend-top-default.append-bottom-default
= f.submit 'Update appearance settings', class: 'btn btn-success'
- - if @appearance.persisted?
- Preview last save:
- = link_to 'Sign-in page', preview_sign_in_admin_appearances_path, class: 'btn', target: '_blank', rel: 'noopener noreferrer'
- = link_to 'New project page', new_project_path, class: 'btn', target: '_blank', rel: 'noopener noreferrer'
+ - if @appearance.persisted? || @appearance.updated_at
+ .mt-4
+ - if @appearance.persisted?
+ Preview last save:
+ = link_to 'Sign-in page', preview_sign_in_admin_appearances_path, class: 'btn', target: '_blank', rel: 'noopener noreferrer'
+ = link_to 'New project page', new_project_path, class: 'btn', target: '_blank', rel: 'noopener noreferrer'
- - if @appearance.updated_at
- %span.float-right
- Last edit #{time_ago_with_tooltip(@appearance.updated_at)}
+ - if @appearance.updated_at
+ %span.float-right
+ Last edit #{time_ago_with_tooltip(@appearance.updated_at)}
diff --git a/app/views/admin/application_settings/_influx.html.haml b/app/views/admin/application_settings/_influx.html.haml
deleted file mode 100644
index 300b01c6777..00000000000
--- a/app/views/admin/application_settings/_influx.html.haml
+++ /dev/null
@@ -1,60 +0,0 @@
-= form_for @application_setting, url: metrics_and_profiling_admin_application_settings_path(anchor: 'js-influx-settings'), html: { class: 'fieldset-form' } do |f|
- = form_errors(@application_setting)
-
- %fieldset
- %p
- Set up InfluxDB to measure a wide variety of statistics like the time spent
- in running SQL queries. These settings require a
- = link_to 'restart', help_page_path('administration/restart_gitlab')
- to take effect.
- = link_to icon('question-circle'), help_page_path('administration/monitoring/performance/index')
- .form-group
- .form-check
- = f.check_box :metrics_enabled, class: 'form-check-input'
- = f.label :metrics_enabled, class: 'form-check-label' do
- Enable InfluxDB Metrics
- .form-group
- = f.label :metrics_host, 'InfluxDB host', class: 'label-bold'
- = f.text_field :metrics_host, class: 'form-control', placeholder: 'influxdb.example.com'
- .form-group
- = f.label :metrics_port, 'InfluxDB port', class: 'label-bold'
- = f.text_field :metrics_port, class: 'form-control', placeholder: '8089'
- .form-text.text-muted
- The UDP port to use for connecting to InfluxDB. InfluxDB requires that
- your server configuration specifies a database to store data in when
- sending messages to this port, without it metrics data will not be
- saved.
- .form-group
- = f.label :metrics_pool_size, 'Connection pool size', class: 'label-bold'
- = f.number_field :metrics_pool_size, class: 'form-control'
- .form-text.text-muted
- The amount of InfluxDB connections to open. Connections are opened
- lazily. Users using multi-threaded application servers should ensure
- enough connections are available (at minimum the amount of application
- server threads).
- .form-group
- = f.label :metrics_timeout, 'Connection timeout', class: 'label-bold'
- = f.number_field :metrics_timeout, class: 'form-control'
- .form-text.text-muted
- The amount of seconds after which an InfluxDB connection will time
- out.
- .form-group
- = f.label :metrics_method_call_threshold, 'Method Call Threshold (ms)', class: 'label-bold'
- = f.number_field :metrics_method_call_threshold, class: 'form-control'
- .form-text.text-muted
- A method call is only tracked when it takes longer to complete than
- the given amount of milliseconds.
- .form-group
- = f.label :metrics_sample_interval, 'Sampler Interval (sec)', class: 'label-bold'
- = f.number_field :metrics_sample_interval, class: 'form-control'
- .form-text.text-muted
- The sampling interval in seconds. Sampled data includes memory usage,
- retained Ruby objects, file descriptors and so on.
- .form-group
- = f.label :metrics_packet_size, 'Metrics per packet', class: 'label-bold'
- = f.number_field :metrics_packet_size, class: 'form-control'
- .form-text.text-muted
- The amount of points to store in a single UDP packet. More points
- results in fewer but larger UDP packets being sent.
-
- = f.submit 'Save changes', class: "btn btn-success"
diff --git a/app/views/admin/application_settings/_prometheus.html.haml b/app/views/admin/application_settings/_prometheus.html.haml
index 4c0ff3a18e8..b2ec25cdf8d 100644
--- a/app/views/admin/application_settings/_prometheus.html.haml
+++ b/app/views/admin/application_settings/_prometheus.html.haml
@@ -23,5 +23,11 @@
%code prometheus_multiproc_dir
does not exist or is not pointing to a valid directory.
= link_to icon('question-circle'), help_page_path('administration/monitoring/prometheus/gitlab_metrics', anchor: 'metrics-shared-directory')
+ .form-group
+ = f.label :metrics_method_call_threshold, 'Method Call Threshold (ms)', class: 'label-bold'
+ = f.number_field :metrics_method_call_threshold, class: 'form-control'
+ .form-text.text-muted
+ A method call is only tracked when it takes longer to complete than
+ the given amount of milliseconds.
= f.submit 'Save changes', class: "btn btn-success"
diff --git a/app/views/admin/application_settings/_repository_mirrors_form.html.haml b/app/views/admin/application_settings/_repository_mirrors_form.html.haml
index 6e5fa6eb62c..8ec9b3c528a 100644
--- a/app/views/admin/application_settings/_repository_mirrors_form.html.haml
+++ b/app/views/admin/application_settings/_repository_mirrors_form.html.haml
@@ -10,7 +10,7 @@
= _('Allow repository mirroring to be configured by project maintainers')
%span.form-text.text-muted
= _('If disabled, only admins will be able to configure repository mirroring.')
- = link_to icon('question-circle'), help_page_path('workflow/repository_mirroring')
+ = link_to icon('question-circle'), help_page_path('user/project/repository/repository_mirroring.md')
= render_if_exists 'admin/application_settings/mirror_settings', form: f
diff --git a/app/views/admin/application_settings/_repository_storage.html.haml b/app/views/admin/application_settings/_repository_storage.html.haml
index c3ae39ddd48..6fabafe3fc1 100644
--- a/app/views/admin/application_settings/_repository_storage.html.haml
+++ b/app/views/admin/application_settings/_repository_storage.html.haml
@@ -6,10 +6,10 @@
%h4= _("Hashed repository storage paths")
.form-group
.form-check
- = f.check_box :hashed_storage_enabled, class: 'form-check-input qa-hashed-storage-checkbox'
+ = f.check_box :hashed_storage_enabled, class: 'form-check-input qa-hashed-storage-checkbox', disabled: @application_setting.hashed_storage_enabled?
= f.label :hashed_storage_enabled, _("Use hashed storage"), class: 'label-bold form-check-label'
.form-text.text-muted
- = _("Use hashed storage paths for newly created and renamed repositories. Enable immutable, hash-based paths and repository names to store repositories on disk. This prevents repositories from having to be moved or renamed when the Repository URL changes and may improve disk I/O performance.")
+ = _("Use hashed storage paths for newly created and renamed repositories. Enable immutable, hash-based paths and repository names to store repositories on disk. This prevents repositories from having to be moved or renamed when the Repository URL changes and may improve disk I/O performance. (Always enabled since 13.0)")
.sub-section
%h4= _("Storage nodes for new repositories")
.form-group
diff --git a/app/views/admin/application_settings/_signup.html.haml b/app/views/admin/application_settings/_signup.html.haml
index dc6d68e54ec..d8495c82af1 100644
--- a/app/views/admin/application_settings/_signup.html.haml
+++ b/app/views/admin/application_settings/_signup.html.haml
@@ -49,20 +49,19 @@
= f.label :domain_blacklist, 'Blacklisted domains for sign-ups', class: 'label-bold'
= f.text_area :domain_blacklist_raw, placeholder: 'domain.com', class: 'form-control', rows: 8
.form-text.text-muted Users with e-mail addresses that match these domain(s) will NOT be able to sign-up. Wildcards allowed. Use separate lines for multiple entries. Ex: domain.com, *.domain.com
- - if Feature.enabled?(:email_restrictions)
- .form-group
- = f.label :email_restrictions_enabled, _('Email restrictions'), class: 'label-bold'
- .form-check
- = f.check_box :email_restrictions_enabled, class: 'form-check-input'
- = f.label :email_restrictions_enabled, class: 'form-check-label' do
- = _('Enable email restrictions for sign ups')
- .form-group
- = f.label :email_restrictions, _('Email restrictions for sign-ups'), class: 'label-bold'
- = f.text_area :email_restrictions, class: 'form-control', rows: 4
- .form-text.text-muted
- - supported_syntax_link_url = 'https://github.com/google/re2/wiki/Syntax'
- - supported_syntax_link_start = '<a href="%{url}" target="_blank" rel="noopener noreferrer">'.html_safe % { url: supported_syntax_link_url }
- = _('Restricts sign-ups for email addresses that match the given regex. See the %{supported_syntax_link_start}supported syntax%{supported_syntax_link_end} for more information.').html_safe % { supported_syntax_link_start: supported_syntax_link_start, supported_syntax_link_end: '</a>'.html_safe }
+ .form-group
+ = f.label :email_restrictions_enabled, _('Email restrictions'), class: 'label-bold'
+ .form-check
+ = f.check_box :email_restrictions_enabled, class: 'form-check-input'
+ = f.label :email_restrictions_enabled, class: 'form-check-label' do
+ = _('Enable email restrictions for sign ups')
+ .form-group
+ = f.label :email_restrictions, _('Email restrictions for sign-ups'), class: 'label-bold'
+ = f.text_area :email_restrictions, class: 'form-control', rows: 4
+ .form-text.text-muted
+ - supported_syntax_link_url = 'https://github.com/google/re2/wiki/Syntax'
+ - supported_syntax_link_start = '<a href="%{url}" target="_blank" rel="noopener noreferrer">'.html_safe % { url: supported_syntax_link_url }
+ = _('Restricts sign-ups for email addresses that match the given regex. See the %{supported_syntax_link_start}supported syntax%{supported_syntax_link_end} for more information.').html_safe % { supported_syntax_link_start: supported_syntax_link_start, supported_syntax_link_end: '</a>'.html_safe }
.form-group
= f.label :after_sign_up_text, class: 'label-bold'
diff --git a/app/views/admin/application_settings/_visibility_and_access.html.haml b/app/views/admin/application_settings/_visibility_and_access.html.haml
index a4acbe6c885..3c4fc75dbee 100644
--- a/app/views/admin/application_settings/_visibility_and_access.html.haml
+++ b/app/views/admin/application_settings/_visibility_and_access.html.haml
@@ -3,6 +3,7 @@
%fieldset
= render 'shared/default_branch_protection', f: f, selected_level: @application_setting.default_branch_protection
+ = render_if_exists 'admin/application_settings/group_owners_can_manage_default_branch_protection_setting', form: f
.form-group
= f.label s_('ProjectCreationLevel|Default project creation protection'), class: 'label-bold'
diff --git a/app/views/admin/application_settings/general.html.haml b/app/views/admin/application_settings/general.html.haml
index bebda385886..fd3f04fefd1 100644
--- a/app/views/admin/application_settings/general.html.haml
+++ b/app/views/admin/application_settings/general.html.haml
@@ -98,9 +98,9 @@
.form-check
= f.check_box :web_ide_clientside_preview_enabled, class: 'form-check-input'
= f.label :web_ide_clientside_preview_enabled, class: 'form-check-label' do
- = s_('IDE|Client side evaluation')
+ = s_('IDE|Live Preview')
%span.form-text.text-muted
- = s_('IDE|Allow live previews of JavaScript projects in the Web IDE using CodeSandbox client side evaluation.')
+ = s_('IDE|Allow live previews of JavaScript projects in the Web IDE using CodeSandbox Live Preview.')
= f.submit _('Save changes'), class: "btn btn-success"
diff --git a/app/views/admin/application_settings/integrations.html.haml b/app/views/admin/application_settings/integrations.html.haml
index 2b01160a230..a8eff26b94c 100644
--- a/app/views/admin/application_settings/integrations.html.haml
+++ b/app/views/admin/application_settings/integrations.html.haml
@@ -18,7 +18,7 @@
%p
= s_('AdminSettings|Integrations configured here will automatically apply to all projects on this instance.')
= link_to _('Learn more'), '#'
- = render 'shared/integrations/integrations', integrations: @integrations
+ = render 'shared/integrations/index', integrations: @integrations
- else
= render_if_exists 'admin/application_settings/elasticsearch_form'
diff --git a/app/views/admin/application_settings/metrics_and_profiling.html.haml b/app/views/admin/application_settings/metrics_and_profiling.html.haml
index 6a703d0b70c..befe10ea510 100644
--- a/app/views/admin/application_settings/metrics_and_profiling.html.haml
+++ b/app/views/admin/application_settings/metrics_and_profiling.html.haml
@@ -2,17 +2,6 @@
- page_title _("Metrics and profiling")
- @content_class = "limit-container-width" unless fluid_layout
-%section.settings.as-influx.no-animate#js-influx-settings{ class: ('expanded' if expanded_by_default?) }
- .settings-header
- %h4
- = _('Metrics - Influx')
- %button.btn.btn-default.js-settings-toggle{ type: 'button' }
- = expanded_by_default? ? _('Collapse') : _('Expand')
- %p
- = _('Enable and configure InfluxDB metrics.')
- .settings-content
- = render 'influx'
-
%section.settings.as-prometheus.no-animate#js-prometheus-settings{ class: ('expanded' if expanded_by_default?) }
.settings-header
%h4
diff --git a/app/views/admin/impersonation_tokens/index.html.haml b/app/views/admin/impersonation_tokens/index.html.haml
index a7da14d16ff..8342507d8a6 100644
--- a/app/views/admin/impersonation_tokens/index.html.haml
+++ b/app/views/admin/impersonation_tokens/index.html.haml
@@ -1,15 +1,29 @@
-- add_to_breadcrumbs "Users", admin_users_path
+- add_to_breadcrumbs 'Users', admin_users_path
- breadcrumb_title @user.name
-- page_title "Impersonation Tokens", @user.name, "Users"
+- page_title _('Impersonation Tokens'), @user.name, _('Users')
+- type = _('impersonation token')
+- type_plural = _('impersonation tokens')
+
= render 'admin/users/head'
.row.prepend-top-default
.col-lg-12
- if @new_impersonation_token
- = render "shared/personal_access_tokens_created_container", new_token_value: @new_impersonation_token,
- container_title: 'Your New Impersonation Token',
- clipboard_button_title: _('Copy impersonation token')
+ = render 'shared/access_tokens/created_container',
+ type: type,
+ new_token_value: @new_impersonation_token
- = render "shared/personal_access_tokens_form", path: admin_user_impersonation_tokens_path, impersonation: true, token: @impersonation_token, scopes: @scopes
+ = render 'shared/access_tokens/form',
+ type: type,
+ title: _('Add an impersonation token'),
+ path: admin_user_impersonation_tokens_path,
+ impersonation: true,
+ token: @impersonation_token,
+ scopes: @scopes
- = render "shared/personal_access_tokens_table", impersonation: true, active_tokens: @active_impersonation_tokens, inactive_tokens: @inactive_impersonation_tokens
+ = render 'shared/access_tokens/table',
+ type: type,
+ type_plural: type_plural,
+ impersonation: true,
+ active_tokens: @active_impersonation_tokens,
+ revoke_route_helper: ->(token) { revoke_admin_user_impersonation_token_path(token.user, token) }
diff --git a/app/views/admin/logs/show.html.haml b/app/views/admin/logs/show.html.haml
deleted file mode 100644
index eb93f645ea6..00000000000
--- a/app/views/admin/logs/show.html.haml
+++ /dev/null
@@ -1,24 +0,0 @@
-- page_title "Logs"
-
-%ul.nav-links.log-tabs.nav.nav-tabs
- - @loggers.each do |klass|
- %li.nav-item
- = link_to klass.file_name, "##{klass.file_name_noext}", data: { toggle: 'tab' }, class: "#{active_when(klass == @loggers.first)} nav-link"
-.row-content-block
- To prevent performance issues admin logs output the last 2000 lines
-.tab-content
- - @loggers.each do |klass|
- .tab-pane{ class: active_when(klass == @loggers.first), id: klass.file_name_noext }
- .file-holder#README
- .js-file-title.file-title
- %i.fa.fa-file
- = klass.file_name
- .float-right
- = link_to '#', class: 'log-bottom' do
- %i.fa.fa-arrow-down
- Scroll down
- .file-content.logs
- %ol
- - klass.read_latest.each do |line|
- %li
- %p= line
diff --git a/app/views/admin/projects/show.html.haml b/app/views/admin/projects/show.html.haml
index 7274099806d..8abc4c37e70 100644
--- a/app/views/admin/projects/show.html.haml
+++ b/app/views/admin/projects/show.html.haml
@@ -14,11 +14,9 @@
.col-md-12
.card
.card-header.alert.alert-danger
- Last repository check
- = "(#{time_ago_with_tooltip(@project.last_repository_check_at)})"
- failed. See
- = link_to 'repocheck.log', admin_logs_path
- for error messages.
+ - last_check_message = _("Last repository check (%{last_check_timestamp}) failed. See the 'repocheck.log' file for error messages.")
+ - last_check_message = last_check_message % { last_check_timestamp: time_ago_with_tooltip(@project.last_repository_check_at) }
+ = last_check_message.html_safe
.row
.col-md-6
.card
@@ -135,24 +133,18 @@
.card.repository-check
.card-header
- Repository check
+ = _("Repository check")
.card-body
= form_for @project, url: repository_check_admin_project_path(@project), method: :post do |f|
.form-group
- if @project.last_repository_check_at.nil?
- This repository has never been checked.
+ = _("This repository has never been checked.")
+ - elsif @project.last_repository_check_failed?
+ - failed_message = _("This repository was last checked %{last_check_timestamp}. The check %{strong_start}failed.%{strong_end} See the 'repocheck.log' file for error messages.")
+ - failed_message = failed_message % { last_check_timestamp: @project.last_repository_check_at.to_s(:medium), strong_start: "<strong class='cred'>", strong_end: "</strong>" }
+ = failed_message.html_safe
- else
- This repository was last checked
- = @project.last_repository_check_at.to_s(:medium) + '.'
- The check
- - if @project.last_repository_check_failed?
- = succeed '.' do
- %strong.cred failed
- See
- = link_to 'repocheck.log', admin_logs_path
- for error messages.
- - else
- passed.
+ = _("This repository was last checked %{last_check_timestamp}. The check passed.") % { last_check_timestamp: @project.last_repository_check_at.to_s(:medium) }
= link_to icon('question-circle'), help_page_path('administration/repository_checks')
diff --git a/app/views/admin/services/_deprecated_message.html.haml b/app/views/admin/services/_deprecated_message.html.haml
deleted file mode 100644
index fea9506a4bb..00000000000
--- a/app/views/admin/services/_deprecated_message.html.haml
+++ /dev/null
@@ -1,3 +0,0 @@
-.flash-container.flash-container-page
- .flash-alert.deprecated-service
- %span= @service.deprecation_message
diff --git a/app/views/admin/services/edit.html.haml b/app/views/admin/services/edit.html.haml
index 79f5ab0d77d..00ed5464a44 100644
--- a/app/views/admin/services/edit.html.haml
+++ b/app/views/admin/services/edit.html.haml
@@ -2,6 +2,4 @@
- breadcrumb_title @service.title
- page_title @service.title, "Service Templates"
-= render 'deprecated_message' if @service.deprecation_message
-
= render 'form'
diff --git a/app/views/admin/sessions/_new_base.html.haml b/app/views/admin/sessions/_new_base.html.haml
index a8d678d2b61..5be1c90d6aa 100644
--- a/app/views/admin/sessions/_new_base.html.haml
+++ b/app/views/admin/sessions/_new_base.html.haml
@@ -1,4 +1,4 @@
-= form_tag(admin_session_path, method: :post, html: { class: 'new_user gl-show-field-errors', 'aria-live': 'assertive'}) do
+= form_tag(admin_session_path, method: :post, class: 'new_user gl-show-field-errors', 'aria-live': 'assertive') do
.form-group
= label_tag :user_password, _('Password'), class: 'label-bold'
= password_field_tag 'user[password]', nil, class: 'form-control', required: true, title: _('This field is required.'), data: { qa_selector: 'password_field' }
diff --git a/app/views/admin/sessions/_signin_box.html.haml b/app/views/admin/sessions/_signin_box.html.haml
new file mode 100644
index 00000000000..cb6c0a76e56
--- /dev/null
+++ b/app/views/admin/sessions/_signin_box.html.haml
@@ -0,0 +1,19 @@
+- if any_form_based_providers_enabled?
+ - if crowd_enabled?
+ .login-box.tab-pane{ id: "crowd", role: 'tabpanel', class: active_when(form_based_auth_provider_has_active_class?(:crowd)) }
+ .login-body
+ = render 'devise/sessions/new_crowd'
+
+ = render_if_exists 'devise/sessions/new_kerberos_tab'
+
+ - ldap_servers.each_with_index do |server, i|
+ .login-box.tab-pane{ id: "#{server['provider_name']}", role: 'tabpanel', class: active_when(i.zero? && form_based_auth_provider_has_active_class?(:ldapmain)) }
+ .login-body
+ = render 'devise/sessions/new_ldap', server: server, hide_remember_me: true, submit_message: _('Enter Admin Mode')
+
+ = render_if_exists 'devise/sessions/new_smartcard'
+
+- if allow_admin_mode_password_authentication_for_web?
+ .login-box.tab-pane{ id: 'login-pane', role: 'tabpanel', class: active_when(!any_form_based_providers_enabled?) }
+ .login-body
+ = render 'admin/sessions/new_base'
diff --git a/app/views/admin/sessions/_tabs_normal.html.haml b/app/views/admin/sessions/_tabs_normal.html.haml
deleted file mode 100644
index 2e279013720..00000000000
--- a/app/views/admin/sessions/_tabs_normal.html.haml
+++ /dev/null
@@ -1,3 +0,0 @@
-%ul.nav-links.new-session-tabs.nav-tabs.nav{ role: 'tablist' }
- %li.nav-item{ role: 'presentation' }
- %a.nav-link.active{ href: '#login-pane', data: { toggle: 'tab', qa_selector: 'sign_in_tab' }, role: 'tab' }= tab_title
diff --git a/app/views/admin/sessions/new.html.haml b/app/views/admin/sessions/new.html.haml
index 0a7f20b861e..4ce1629bb53 100644
--- a/app/views/admin/sessions/new.html.haml
+++ b/app/views/admin/sessions/new.html.haml
@@ -5,18 +5,19 @@
.col-md-5.new-session-forms-container
.login-page
#signin-container
- = render 'admin/sessions/tabs_normal', tab_title: _('Enter Admin Mode')
+ - if any_form_based_providers_enabled?
+ = render 'devise/shared/tabs_ldap', show_password_form: allow_admin_mode_password_authentication_for_web?, render_signup_link: false
+ - else
+ = render 'devise/shared/tabs_normal', tab_title: _('Enter Admin Mode'), render_signup_link: false
.tab-content
- - if !current_user.require_password_creation_for_web?
- .login-box.tab-pane.active{ id: 'login-pane', role: 'tabpanel' }
- .login-body
- = render 'admin/sessions/new_base'
+ - if allow_admin_mode_password_authentication_for_web? || ldap_sign_in_enabled? || crowd_enabled?
+ = render 'admin/sessions/signin_box'
- - if omniauth_enabled? && button_based_providers_enabled?
- .clearfix
- = render 'devise/shared/omniauth_box', hide_remember_me: true
+ -# Show a message if none of the mechanisms above are enabled
+ - if !allow_admin_mode_password_authentication_for_web? && !ldap_sign_in_enabled? && !omniauth_enabled?
+ .prepend-top-default.center
+ = _('No authentication methods configured.')
- -# Show a message if none of the mechanisms above are enabled
- - if current_user.require_password_creation_for_web? && !omniauth_enabled?
- .prepend-top-default.center
- = _('No authentication methods configured.')
+ - if omniauth_enabled? && button_based_providers_enabled?
+ .clearfix
+ = render 'devise/shared/omniauth_box', hide_remember_me: true
diff --git a/app/views/admin/sessions/two_factor.html.haml b/app/views/admin/sessions/two_factor.html.haml
index 3a0cbe3facb..57a3452cf35 100644
--- a/app/views/admin/sessions/two_factor.html.haml
+++ b/app/views/admin/sessions/two_factor.html.haml
@@ -5,7 +5,7 @@
.col-md-5.new-session-forms-container
.login-page
#signin-container
- = render 'admin/sessions/tabs_normal', tab_title: _('Enter Admin Mode')
+ = render 'devise/shared/tabs_normal', tab_title: _('Enter Admin Mode'), render_signup_link: false
.tab-content
.login-box.tab-pane.active{ id: 'login-pane', role: 'tabpanel' }
.login-body
diff --git a/app/views/ci/status/_dropdown_graph_badge.html.haml b/app/views/ci/status/_dropdown_graph_badge.html.haml
index 369b0f7e62c..d9d646c77d9 100644
--- a/app/views/ci/status/_dropdown_graph_badge.html.haml
+++ b/app/views/ci/status/_dropdown_graph_badge.html.haml
@@ -8,12 +8,12 @@
- if status.has_details?
= link_to status.details_path, class: 'mini-pipeline-graph-dropdown-item d-flex', data: { toggle: 'tooltip', title: tooltip, container: 'body' } do
%span{ class: klass }= sprite_icon(status.icon)
- %span.ci-build-text.text-truncate.mw-70p.gl-pl-1= subject.name
+ %span.ci-build-text.text-truncate.mw-70p.gl-pl-1-deprecated-no-really-do-not-use-me= subject.name
- else
.menu-item.mini-pipeline-graph-dropdown-item.d-flex{ data: { toggle: 'tooltip', title: tooltip, container: 'body' } }
%span{ class: klass }= sprite_icon(status.icon)
- %span.ci-build-text.text-truncate.mw-70p.gl-pl-1= subject.name
+ %span.ci-build-text.text-truncate.mw-70p.gl-pl-1-deprecated-no-really-do-not-use-me= subject.name
- if status.has_action?
= link_to status.action_path, class: "ci-action-icon-container ci-action-icon-wrapper js-ci-action-icon", method: status.action_method, data: { toggle: 'tooltip', title: status.action_title, container: 'body' } do
diff --git a/app/views/ci/variables/_index.html.haml b/app/views/ci/variables/_index.html.haml
index 4d8df4cc12a..26051261715 100644
--- a/app/views/ci/variables/_index.html.haml
+++ b/app/views/ci/variables/_index.html.haml
@@ -8,7 +8,7 @@
- if Feature.enabled?(:new_variables_ui, @project || @group, default_enabled: true)
- is_group = !@group.nil?
- #js-ci-project-variables{ data: { endpoint: save_endpoint, project_id: @project&.id || '', group: is_group.to_s, maskable_regex: ci_variable_maskable_regex} }
+ #js-ci-project-variables{ data: { endpoint: save_endpoint, project_id: @project&.id || '', group: is_group.to_s, maskable_regex: ci_variable_maskable_regex, protected_by_default: ci_variable_protected_by_default?.to_s} }
- else
.row
diff --git a/app/views/clusters/clusters/_cluster.html.haml b/app/views/clusters/clusters/_cluster.html.haml
index 9b6c0c20080..f11117ea5c4 100644
--- a/app/views/clusters/clusters/_cluster.html.haml
+++ b/app/views/clusters/clusters/_cluster.html.haml
@@ -2,7 +2,8 @@
.card-body.gl-responsive-table-row
.table-section.section-60
.table-mobile-header{ role: "rowheader" }= s_("ClusterIntegration|Kubernetes cluster")
- .table-mobile-content
+ .table-mobile-content.gl-display-flex.gl-align-items-center.gl-justify-content-end.gl-justify-content-md-start
+ .gl-w-6.gl-h-6.gl-mr-3.gl-display-flex.gl-align-items-center= provider_icon(cluster.provider_type)
= cluster.item_link(clusterable, html_options: { data: { qa_selector: 'cluster', qa_cluster_name: cluster.name } })
- if cluster.status_name == :creating
.spinner.ml-2.align-middle.has-tooltip{ title: s_("ClusterIntegration|Cluster being created") }
diff --git a/app/views/clusters/clusters/index.html.haml b/app/views/clusters/clusters/index.html.haml
index 28002dbff92..86194842664 100644
--- a/app/views/clusters/clusters/index.html.haml
+++ b/app/views/clusters/clusters/index.html.haml
@@ -19,7 +19,7 @@
= link_to _('More information'), help_page_path('user/group/clusters/index', anchor: 'cluster-precedence')
- if Feature.enabled?(:clusters_list_redesign)
- #js-clusters-list-app{ data: { endpoint: 'todo/add/endpoint' } }
+ #js-clusters-list-app{ data: { endpoint: clusterable.index_path(format: :json) } }
- else
.clusters-table.js-clusters-list
.gl-responsive-table-row.table-row-header{ role: "row" }
diff --git a/app/views/clusters/clusters/show.html.haml b/app/views/clusters/clusters/show.html.haml
index 7fc76880480..1cc68d927bd 100644
--- a/app/views/clusters/clusters/show.html.haml
+++ b/app/views/clusters/clusters/show.html.haml
@@ -17,6 +17,7 @@
install_knative_path: clusterable.install_applications_cluster_path(@cluster, :knative),
update_knative_path: clusterable.update_applications_cluster_path(@cluster, :knative),
install_elastic_stack_path: clusterable.install_applications_cluster_path(@cluster, :elastic_stack),
+ install_fluentd_path: clusterable.install_applications_cluster_path(@cluster, :fluentd),
cluster_environments_path: cluster_environments_path,
toggle_status: @cluster.enabled? ? 'true': 'false',
has_rbac: has_rbac_enabled?(@cluster) ? 'true': 'false',
diff --git a/app/views/dashboard/snippets/index.html.haml b/app/views/dashboard/snippets/index.html.haml
index 05214346496..2f0cc76f2e0 100644
--- a/app/views/dashboard/snippets/index.html.haml
+++ b/app/views/dashboard/snippets/index.html.haml
@@ -6,8 +6,6 @@
= render 'dashboard/snippets_head'
- if current_user.snippets.exists?
= render partial: 'snippets/snippets_scope_menu', locals: { include_private: true, counts: @snippet_counts }
-
-- if current_user.snippets.exists?
= render partial: 'shared/snippets/list', locals: { link_project: true }
- else
= render 'shared/empty_states/snippets', button_path: button_path
diff --git a/app/views/devise/registrations/new.html.haml b/app/views/devise/registrations/new.html.haml
index 232dffa28b4..9fb5e27b692 100644
--- a/app/views/devise/registrations/new.html.haml
+++ b/app/views/devise/registrations/new.html.haml
@@ -8,7 +8,7 @@
= _("GitLab is a single application for the entire software development lifecycle. From project planning and source code management to CI/CD, monitoring, and security.")
.col-lg-5.order-12
.text-center.mb-3
- %h2.font-weight-bold.gl-font-size-20= _('Register for GitLab')
+ %h2.font-weight-bold.gl-font-size-20-deprecated-no-really-do-not-use-me= _('Register for GitLab')
= render 'devise/shared/experimental_separate_sign_up_flow_box'
= render 'devise/shared/sign_in_link'
- else
diff --git a/app/views/devise/sessions/_new_ldap.html.haml b/app/views/devise/sessions/_new_ldap.html.haml
index 31c4bb0e33e..3fc99b6a47d 100644
--- a/app/views/devise/sessions/_new_ldap.html.haml
+++ b/app/views/devise/sessions/_new_ldap.html.haml
@@ -1,4 +1,6 @@
- server = local_assigns.fetch(:server)
+- hide_remember_me = local_assigns.fetch(:hide_remember_me, false)
+- submit_message = local_assigns.fetch(:submit_message, _('Sign in'))
= form_tag(omniauth_callback_path(:user, server['provider_name']), id: 'new_ldap_user', class: "gl-show-field-errors") do
.form-group
@@ -7,9 +9,11 @@
.form-group
= label_tag :password
= password_field_tag :password, nil, { class: "form-control bottom", title: "This field is required.", data: { qa_selector: 'password_field' }, required: true }
- - if devise_mapping.rememberable?
+ - if !hide_remember_me && devise_mapping.rememberable?
.remember-me
%label{ for: "remember_me" }
= check_box_tag :remember_me, '1', false, id: 'remember_me'
%span Remember me
- = submit_tag "Sign in", class: "btn-success btn", data: { qa_selector: 'sign_in_button' }
+
+ .submit-container.move-submit-down
+ = submit_tag submit_message, class: "btn-success btn", data: { qa_selector: 'sign_in_button' }
diff --git a/app/views/devise/shared/_omniauth_box.html.haml b/app/views/devise/shared/_omniauth_box.html.haml
index cca0f756e76..5c3e4ccbfe5 100644
--- a/app/views/devise/shared/_omniauth_box.html.haml
+++ b/app/views/devise/shared/_omniauth_box.html.haml
@@ -1,3 +1,5 @@
+- hide_remember_me = local_assigns.fetch(:hide_remember_me, false)
+
.omniauth-container.prepend-top-15
%label.label-bold.d-block
Sign in with
@@ -10,7 +12,7 @@
= provider_image_tag(provider)
%span
= label_for_provider(provider)
- - unless defined?(hide_remember_me) && hide_remember_me
+ - unless hide_remember_me
%fieldset.remember-me
%label
= check_box_tag :remember_me, nil, false, class: 'remember-me-checkbox'
diff --git a/app/views/devise/shared/_signin_box.html.haml b/app/views/devise/shared/_signin_box.html.haml
index 6ddb7e1ac48..c0b005bac77 100644
--- a/app/views/devise/shared/_signin_box.html.haml
+++ b/app/views/devise/shared/_signin_box.html.haml
@@ -6,7 +6,7 @@
= render_if_exists 'devise/sessions/new_kerberos_tab'
- - @ldap_servers.each_with_index do |server, i|
+ - ldap_servers.each_with_index do |server, i|
.login-box.tab-pane{ id: "#{server['provider_name']}", role: 'tabpanel', class: active_when(i.zero? && form_based_auth_provider_has_active_class?(:ldapmain)) }
.login-body
= render 'devise/sessions/new_ldap', server: server
diff --git a/app/views/devise/shared/_tabs_ldap.html.haml b/app/views/devise/shared/_tabs_ldap.html.haml
index b8f0cd2a91a..eb14ad6006f 100644
--- a/app/views/devise/shared/_tabs_ldap.html.haml
+++ b/app/views/devise/shared/_tabs_ldap.html.haml
@@ -1,17 +1,20 @@
+- show_password_form = local_assigns.fetch(:show_password_form, password_authentication_enabled_for_web?)
+- render_signup_link = local_assigns.fetch(:render_signup_link, true)
+
%ul.nav-links.new-session-tabs.nav-tabs.nav{ class: ('custom-provider-tabs' if any_form_based_providers_enabled?) }
- if crowd_enabled?
%li.nav-item
= link_to "Crowd", "#crowd", class: "nav-link #{active_when(form_based_auth_provider_has_active_class?(:crowd))}", 'data-toggle' => 'tab'
= render_if_exists "devise/shared/kerberos_tab"
- - @ldap_servers.each_with_index do |server, i|
+ - ldap_servers.each_with_index do |server, i|
%li.nav-item
= link_to server['label'], "##{server['provider_name']}", class: "nav-link #{active_when(i.zero? && form_based_auth_provider_has_active_class?(:ldapmain))}", data: { toggle: 'tab', qa_selector: 'ldap_tab' }
= render_if_exists 'devise/shared/tab_smartcard'
- - if password_authentication_enabled_for_web?
+ - if show_password_form
%li.nav-item
- = link_to 'Standard', '#login-pane', class: 'nav-link', data: { toggle: 'tab', qa_selector: 'standard_tab' }
- - if allow_signup?
+ = link_to _('Standard'), '#login-pane', class: 'nav-link', data: { toggle: 'tab', qa_selector: 'standard_tab' }
+ - if render_signup_link && allow_signup?
%li.nav-item
= link_to 'Register', '#register-pane', class: 'nav-link', data: { toggle: 'tab', qa_selector: 'register_tab' }
diff --git a/app/views/devise/shared/_tabs_normal.html.haml b/app/views/devise/shared/_tabs_normal.html.haml
index b6a1b8805ee..a2d5a8be625 100644
--- a/app/views/devise/shared/_tabs_normal.html.haml
+++ b/app/views/devise/shared/_tabs_normal.html.haml
@@ -1,6 +1,9 @@
+- tab_title = local_assigns.fetch(:tab_title, _('Sign in'))
+- render_signup_link = local_assigns.fetch(:render_signup_link, true)
+
%ul.nav-links.new-session-tabs.nav-tabs.nav{ role: 'tablist' }
%li.nav-item{ role: 'presentation' }
- %a.nav-link.active{ href: '#login-pane', data: { toggle: 'tab', qa_selector: 'sign_in_tab' }, role: 'tab' } Sign in
- - if allow_signup?
+ %a.nav-link.active{ href: '#login-pane', data: { toggle: 'tab', qa_selector: 'sign_in_tab' }, role: 'tab' }= tab_title
+ - if render_signup_link && allow_signup?
%li.nav-item{ role: 'presentation' }
%a.nav-link{ href: '#register-pane', data: { track_label: 'sign_in_register', track_property: '', track_event: 'click_button', track_value: '', toggle: 'tab', qa_selector: 'register_tab' }, role: 'tab' } Register
diff --git a/app/views/groups/_flash_messages.html.haml b/app/views/groups/_flash_messages.html.haml
new file mode 100644
index 00000000000..fa1a9d2cca4
--- /dev/null
+++ b/app/views/groups/_flash_messages.html.haml
@@ -0,0 +1,2 @@
+= content_for :flash_message do
+ = render_if_exists 'shared/shared_runners_minutes_limit', namespace: @group, classes: [container_class, ("limit-container-width" unless fluid_layout)]
diff --git a/app/views/groups/_home_panel.html.haml b/app/views/groups/_home_panel.html.haml
index 6772ee94d46..d083288edc8 100644
--- a/app/views/groups/_home_panel.html.haml
+++ b/app/views/groups/_home_panel.html.haml
@@ -1,4 +1,5 @@
- can_create_subgroups = can?(current_user, :create_subgroup, @group)
+- can_create_projects = can?(current_user, :create_projects, @group)
- emails_disabled = @group.emails_disabled?
.group-home-panel
@@ -23,32 +24,33 @@
- if current_user
.group-buttons
= render 'shared/notifications/new_button', notification_setting: @notification_setting, btn_class: 'btn', emails_disabled: emails_disabled
- - if can? current_user, :create_projects, @group
- - new_project_label = _("New project")
- - new_subgroup_label = _("New subgroup")
- - if can_create_subgroups
- .btn-group.new-project-subgroup.droplab-dropdown.home-panel-action-button.prepend-top-default.js-new-project-subgroup.qa-new-project-or-subgroup-dropdown{ data: { project_path: new_project_path(namespace_id: @group.id), subgroup_path: new_group_path(parent_id: @group.id) } }
- %input.btn.btn-success.dropdown-primary.js-new-group-child.qa-new-in-group-button{ type: "button", value: new_project_label, data: { action: "new-project" } }
- %button.btn.btn-success.dropdown-toggle.js-dropdown-toggle.qa-new-project-or-subgroup-dropdown-toggle{ type: "button", data: { "dropdown-trigger" => "#new-project-or-subgroup-dropdown", 'display' => 'static' } }
- = sprite_icon("chevron-down", css_class: "icon dropdown-btn-icon")
- %ul#new-project-or-subgroup-dropdown.dropdown-menu.dropdown-menu-right{ data: { dropdown: true } }
- %li.droplab-item-selected.qa-new-project-option{ role: "button", data: { value: "new-project", text: new_project_label } }
+ - new_project_label = _("New project")
+ - new_subgroup_label = _("New subgroup")
+ - if can_create_projects and can_create_subgroups
+ .btn-group.new-project-subgroup.droplab-dropdown.home-panel-action-button.prepend-top-default.js-new-project-subgroup.qa-new-project-or-subgroup-dropdown{ data: { project_path: new_project_path(namespace_id: @group.id), subgroup_path: new_group_path(parent_id: @group.id) } }
+ %input.btn.btn-success.dropdown-primary.js-new-group-child.qa-new-in-group-button{ type: "button", value: new_project_label, data: { action: "new-project" } }
+ %button.btn.btn-success.dropdown-toggle.js-dropdown-toggle.qa-new-project-or-subgroup-dropdown-toggle{ type: "button", data: { "dropdown-trigger" => "#new-project-or-subgroup-dropdown", 'display' => 'static' } }
+ = sprite_icon("chevron-down", css_class: "icon dropdown-btn-icon")
+ %ul#new-project-or-subgroup-dropdown.dropdown-menu.dropdown-menu-right{ data: { dropdown: true } }
+ %li.droplab-item-selected.qa-new-project-option{ role: "button", data: { value: "new-project", text: new_project_label } }
+ .menu-item
+ .icon-container
+ = icon("check", class: "list-item-checkmark")
+ .description
+ %strong= new_project_label
+ %span= s_("GroupsTree|Create a project in this group.")
+ %li.divider.droplap-item-ignore
+ %li.qa-new-subgroup-option{ role: "button", data: { value: "new-subgroup", text: new_subgroup_label } }
.menu-item
.icon-container
= icon("check", class: "list-item-checkmark")
.description
- %strong= new_project_label
- %span= s_("GroupsTree|Create a project in this group.")
- %li.divider.droplap-item-ignore
- %li.qa-new-subgroup-option{ role: "button", data: { value: "new-subgroup", text: new_subgroup_label } }
- .menu-item
- .icon-container
- = icon("check", class: "list-item-checkmark")
- .description
- %strong= new_subgroup_label
- %span= s_("GroupsTree|Create a subgroup in this group.")
- - else
- = link_to new_project_label, new_project_path(namespace_id: @group.id), class: "btn btn-success prepend-top-default"
+ %strong= new_subgroup_label
+ %span= s_("GroupsTree|Create a subgroup in this group.")
+ - elsif can_create_projects
+ = link_to new_project_label, new_project_path(namespace_id: @group.id), class: "btn btn-success prepend-top-default"
+ - elsif can_create_subgroups
+ = link_to new_subgroup_label, new_group_path(parent_id: @group.id), class: "btn btn-success prepend-top-default"
- if @group.description.present?
.group-home-desc.mt-1
diff --git a/app/views/groups/edit.html.haml b/app/views/groups/edit.html.haml
index fe5a00e3be9..2e58517fdc7 100644
--- a/app/views/groups/edit.html.haml
+++ b/app/views/groups/edit.html.haml
@@ -45,11 +45,11 @@
%section.settings.gs-advanced.no-animate#js-advanced-settings{ class: ('expanded' if expanded) }
.settings-header
%h4.settings-title.js-settings-toggle.js-settings-toggle-trigger-only{ role: 'button' }
- = _('Path, transfer, remove')
+ = _('Advanced')
%button.btn.js-settings-toggle{ type: 'button' }
= expanded ? _('Collapse') : _('Expand')
%p
- = _('Perform advanced options such as changing path, transferring, or removing the group.')
+ = _('Perform advanced options such as changing path, transferring, exporting, or removing the group.')
.settings-content
= render 'groups/settings/advanced'
diff --git a/app/views/groups/group_members/index.html.haml b/app/views/groups/group_members/index.html.haml
index 048edb80d99..1f2fb747c7d 100644
--- a/app/views/groups/group_members/index.html.haml
+++ b/app/views/groups/group_members/index.html.haml
@@ -9,20 +9,16 @@
= _("Group members")
%hr
- if can_manage_members
- - if Feature.enabled?(:share_group_with_group, default_enabled: true)
- %ul.nav-links.nav.nav-tabs.gitlab-tabs{ role: 'tablist' }
+ %ul.nav-links.nav.nav-tabs.gitlab-tabs{ role: 'tablist' }
+ %li.nav-tab{ role: 'presentation' }
+ %a.nav-link.active{ href: '#invite-member-pane', id: 'invite-member-tab', data: { toggle: 'tab' }, role: 'tab' }= _("Invite member")
%li.nav-tab{ role: 'presentation' }
- %a.nav-link.active{ href: '#invite-member-pane', id: 'invite-member-tab', data: { toggle: 'tab' }, role: 'tab' }= _("Invite member")
- %li.nav-tab{ role: 'presentation' }
- %a.nav-link{ href: '#invite-group-pane', id: 'invite-group-tab', data: { toggle: 'tab', qa_selector: 'invite_group_tab' }, role: 'tab' }= _("Invite group")
- .tab-content.gitlab-tab-content
- .tab-pane.active{ id: 'invite-member-pane', role: 'tabpanel' }
- = render_invite_member_for_group(@group, @group_member.access_level)
- - if Feature.enabled?(:share_group_with_group, default_enabled: true)
- .tab-pane{ id: 'invite-group-pane', role: 'tabpanel' }
- = render 'shared/members/invite_group', submit_url: group_group_links_path(@group), access_levels: GroupMember.access_level_roles, default_access_level: @group_member.access_level, group_link_field: 'shared_with_group_id', group_access_field: 'shared_group_access'
- - else
- = render_invite_member_for_group(@group, @group_member.access_level)
+ %a.nav-link{ href: '#invite-group-pane', id: 'invite-group-tab', data: { toggle: 'tab', qa_selector: 'invite_group_tab' }, role: 'tab' }= _("Invite group")
+ .tab-content.gitlab-tab-content
+ .tab-pane.active{ id: 'invite-member-pane', role: 'tabpanel' }
+ = render_invite_member_for_group(@group, @group_member.access_level)
+ .tab-pane{ id: 'invite-group-pane', role: 'tabpanel' }
+ = render 'shared/members/invite_group', submit_url: group_group_links_path(@group), access_levels: GroupMember.access_level_roles, default_access_level: @group_member.access_level, group_link_field: 'shared_with_group_id', group_access_field: 'shared_group_access'
= render 'shared/members/requests', membership_source: @group, requesters: @requesters
diff --git a/app/views/groups/settings/_advanced.html.haml b/app/views/groups/settings/_advanced.html.haml
index 2734ab538a0..0df82898644 100644
--- a/app/views/groups/settings/_advanced.html.haml
+++ b/app/views/groups/settings/_advanced.html.haml
@@ -1,3 +1,5 @@
+= render 'groups/settings/export', group: @group
+
.sub-section
%h4.warning-title= s_('GroupSettings|Change group path')
= form_for @group, html: { multipart: true, class: 'gl-show-field-errors' }, authenticity_token: true do |f|
diff --git a/app/views/groups/settings/_default_branch_protection.html.haml b/app/views/groups/settings/_default_branch_protection.html.haml
new file mode 100644
index 00000000000..e0e901cbc4a
--- /dev/null
+++ b/app/views/groups/settings/_default_branch_protection.html.haml
@@ -0,0 +1,3 @@
+- return unless can_update_default_branch_protection?(group)
+
+= render 'shared/default_branch_protection', f: f, selected_level: group.default_branch_protection
diff --git a/app/views/groups/settings/_export.html.haml b/app/views/groups/settings/_export.html.haml
new file mode 100644
index 00000000000..ef7bf562c69
--- /dev/null
+++ b/app/views/groups/settings/_export.html.haml
@@ -0,0 +1,28 @@
+- return unless Feature.enabled?(:group_import_export, @group, default_enabled: true)
+
+- group = local_assigns.fetch(:group)
+
+.sub-section
+ %h4= s_('GroupSettings|Export group')
+ %p= _('Export this group with all related data to a new GitLab instance. Once complete, you can import the data file from the "New Group" page.')
+
+ .bs-callout.bs-callout-info
+ %p.append-bottom-0
+ %p= _('The following items will be exported:')
+ %ul
+ - group_export_descriptions.each do |description|
+ %li= description
+ %p= _('The following items will NOT be exported:')
+ %ul
+ %li= _('Projects')
+ %li= _('Runner tokens')
+ %li= _('SAML discovery tokens')
+ %p= _('Once the exported file is ready, you will receive a notification email with a download link, or you can download it from this page.')
+ - if group.export_file_exists?
+ = link_to _('Regenerate export'), export_group_path(group),
+ method: :post, class: 'btn btn-default', data: { qa_selector: 'regenerate_export_group_link' }
+ = link_to _('Download export'), download_export_group_path(group),
+ rel: 'nofollow', method: :get, class: 'btn btn-default', data: { qa_selector: 'download_export_link' }
+ - else
+ = link_to _('Export group'), export_group_path(group),
+ method: :post, class: 'btn btn-default', data: { qa_selector: 'export_group_link' }
diff --git a/app/views/groups/settings/_permissions.html.haml b/app/views/groups/settings/_permissions.html.haml
index 1ddaa855e62..e886c99a656 100644
--- a/app/views/groups/settings/_permissions.html.haml
+++ b/app/views/groups/settings/_permissions.html.haml
@@ -33,7 +33,7 @@
= render_if_exists 'groups/settings/ip_restriction', f: f, group: @group
= render_if_exists 'groups/settings/allowed_email_domain', f: f, group: @group
= render 'groups/settings/lfs', f: f
- = render 'shared/default_branch_protection', f: f, selected_level: @group.default_branch_protection
+ = render 'groups/settings/default_branch_protection', f: f, group: @group
= render 'groups/settings/project_creation_level', f: f, group: @group
= render 'groups/settings/subgroup_creation_level', f: f, group: @group
= render 'groups/settings/two_factor_auth', f: f
diff --git a/app/views/groups/settings/integrations/index.html.haml b/app/views/groups/settings/integrations/index.html.haml
index 78825cc72b0..96bd6d69a96 100644
--- a/app/views/groups/settings/integrations/index.html.haml
+++ b/app/views/groups/settings/integrations/index.html.haml
@@ -6,4 +6,4 @@
%p
= s_('GroupSettings|Integrations configured here will automatically apply to all projects in this group.')
= link_to _('Learn more'), '#'
-= render 'shared/integrations/integrations', integrations: @integrations
+= render 'shared/integrations/index', integrations: @integrations
diff --git a/app/views/groups/settings/repository/show.html.haml b/app/views/groups/settings/repository/show.html.haml
index 1f1d7779267..ff0c9de4fef 100644
--- a/app/views/groups/settings/repository/show.html.haml
+++ b/app/views/groups/settings/repository/show.html.haml
@@ -1,6 +1,6 @@
- breadcrumb_title _('Repository Settings')
- page_title _('Repository')
-- deploy_token_description = s_('DeployTokens|Group deploy tokens allow read-only access to the repositories and registry images within the group.')
+- deploy_token_description = s_('DeployTokens|Group deploy tokens allow access to the packages, repositories, and registry images within the group.')
= render "shared/deploy_tokens/index", group_or_project: @group, description: deploy_token_description
diff --git a/app/views/groups/show.html.haml b/app/views/groups/show.html.haml
index a9c19502a7c..032766327ca 100644
--- a/app/views/groups/show.html.haml
+++ b/app/views/groups/show.html.haml
@@ -1,9 +1,15 @@
- breadcrumb_title _("Details")
- @content_class = "limit-container-width" unless fluid_layout
+= content_for :flash_message do
+ - if Feature.enabled?(:subscribable_banner_subscription)
+ = render_if_exists "layouts/header/ee_subscribable_banner", subscription: true
+
= content_for :meta_tags do
= auto_discovery_link_tag(:atom, group_url(@group, rss_url_options), title: "#{@group.name} activity")
+= render partial: 'flash_messages'
+
%div{ class: [("limit-container-width" unless fluid_layout)] }
= render_if_exists 'trials/banner', namespace: @group
diff --git a/app/views/groups/sidebar/_packages.html.haml b/app/views/groups/sidebar/_packages.html.haml
index 16b902a18b9..67e759a4d63 100644
--- a/app/views/groups/sidebar/_packages.html.haml
+++ b/app/views/groups/sidebar/_packages.html.haml
@@ -4,12 +4,12 @@
.nav-icon-container
= sprite_icon('package')
%span.nav-item-name
- = _('Packages')
+ = _('Packages & Registries')
%ul.sidebar-sub-level-items
= nav_link(controller: [:packages, :repositories], html_options: { class: "fly-out-top-item" } ) do
= link_to group_container_registries_path(@group), title: _('Container Registry') do
%strong.fly-out-top-item-name
- = _('Packages')
+ = _('Packages & Registries')
%li.divider.fly-out-top-item
= nav_link(controller: 'groups/container_registries') do
= link_to group_container_registries_path(@group), title: _('Container Registry') do
diff --git a/app/views/help/_shortcuts.html.haml b/app/views/help/_shortcuts.html.haml
index 4b9304cfdb9..bd5424c30c6 100644
--- a/app/views/help/_shortcuts.html.haml
+++ b/app/views/help/_shortcuts.html.haml
@@ -65,25 +65,23 @@
%tbody
%tr
%th
- %th= _('Web IDE')
+ %th= _('Editing')
%tr
%td.shortcut
- if browser.platform.mac?
- %kbd &#8984; p
+ %kbd &#8984; shift p
- else
- %kbd ctrl p
- %td= _('Go to file')
+ %kbd ctrl shift p
+ %td= _('Toggle Markdown preview')
%tr
%td.shortcut
- - if browser.platform.mac?
- %kbd &#8984; enter
- - else
- %kbd ctrl enter
- %td= _('Commit (when editing commit message)')
+ %kbd
+ %i.fa.fa-arrow-up
+ %td= _('Edit your most recent comment in a thread (from an empty textarea)')
%tbody
%tr
%th
- %th= _('Wiki pages')
+ %th= _('Wiki')
%tr
%td.shortcut
%kbd e
@@ -91,19 +89,49 @@
%tbody
%tr
%th
- %th= _('Editing')
+ %th= _('Repository Graph')
%tr
%td.shortcut
- - if browser.platform.mac?
- %kbd &#8984; shift p
- - else
- %kbd ctrl shift p
- %td= _('Toggle Markdown preview')
+ %kbd
+ %i.fa.fa-arrow-left
+ \/
+ %kbd h
+ %td= _('Scroll left')
+ %tr
+ %td.shortcut
+ %kbd
+ %i.fa.fa-arrow-right
+ \/
+ %kbd l
+ %td= _('Scroll right')
%tr
%td.shortcut
%kbd
%i.fa.fa-arrow-up
- %td= _('Edit your most recent comment in a thread (from an empty textarea)')
+ \/
+ %kbd k
+ %td= _('Scroll up')
+ %tr
+ %td.shortcut
+ %kbd
+ %i.fa.fa-arrow-down
+ \/
+ %kbd j
+ %td= _('Scroll down')
+ %tr
+ %td.shortcut
+ %kbd
+ shift
+ %i.fa.fa-arrow-up
+ \/ k
+ %td= _('Scroll to top')
+ %tr
+ %td.shortcut
+ %kbd
+ shift
+ %i.fa.fa-arrow-down
+ \/ j
+ %td= _('Scroll to bottom')
.col-lg-4
%table.shortcut-mappings.text-2
%tbody
@@ -229,15 +257,7 @@
%tbody
%tr
%th
- %th= _('Issues / Merge Requests')
- %tr
- %td.shortcut
- %kbd a
- %td= _('Change assignee')
- %tr
- %td.shortcut
- %kbd m
- %td= _('Change milestone')
+ %th= _('Epics, Issues, and Merge Requests')
%tr
%td.shortcut
%kbd r
@@ -250,92 +270,76 @@
%td.shortcut
%kbd l
%td= _('Change label')
+ %tbody
+ %tr
+ %th
+ %th= _('Issues and Merge Requests')
+ %tr
+ %td.shortcut
+ %kbd a
+ %td= _('Change assignee')
+ %tr
+ %td.shortcut
+ %kbd m
+ %td= _('Change milestone')
+ %tbody
+ %tr
+ %th
+ %th= _('Merge Requests')
%tr
%td.shortcut
%kbd ]
\/
%kbd j
- %td= _('Next file in diff (MRs only)')
+ %td= _('Next file in diff')
%tr
%td.shortcut
%kbd [
\/
%kbd k
- %td= _('Previous file in diff (MRs only)')
+ %td= _('Previous file in diff')
%tr
%td.shortcut
- if browser.platform.mac?
%kbd &#8984; p
- else
%kbd ctrl p
- %td= _('Go to file (MRs only)')
+ %td= _('Go to file')
%tr
%td.shortcut
%kbd n
- %td= _('Next unresolved discussion (MRs only)')
+ %td= _('Next unresolved discussion')
%tr
%td.shortcut
%kbd p
- %td= _('Previous unresolved discussion (MRs only)')
+ %td= _('Previous unresolved discussion')
%tbody
%tr
%th
- %th= _('Epics (Ultimate / Gold license only)')
+ %th= _('Merge Request Commits')
%tr
%td.shortcut
- %kbd r
- %td= _('Comment/Reply (quoting selected text)')
- %tr
- %td.shortcut
- %kbd e
- %td= _('Edit epic description')
+ %kbd c
+ %td= _('Next commit')
%tr
%td.shortcut
- %kbd l
- %td= _('Change label')
+ %kbd x
+ %td= _('Previous commit')
%tbody
%tr
%th
- %th= _('Repository Graph')
- %tr
- %td.shortcut
- %kbd
- %i.fa.fa-arrow-left
- \/
- %kbd h
- %td= _('Scroll left')
- %tr
- %td.shortcut
- %kbd
- %i.fa.fa-arrow-right
- \/
- %kbd l
- %td= _('Scroll right')
- %tr
- %td.shortcut
- %kbd
- %i.fa.fa-arrow-up
- \/
- %kbd k
- %td= _('Scroll up')
- %tr
- %td.shortcut
- %kbd
- %i.fa.fa-arrow-down
- \/
- %kbd j
- %td= _('Scroll down')
+ %th= _('Web IDE')
%tr
%td.shortcut
- %kbd
- shift
- %i.fa.fa-arrow-up
- \/ k
- %td= _('Scroll to top')
+ - if browser.platform.mac?
+ %kbd &#8984; p
+ - else
+ %kbd ctrl p
+ %td= _('Go to file')
%tr
%td.shortcut
- %kbd
- shift
- %i.fa.fa-arrow-down
- \/ j
- %td= _('Scroll to bottom')
+ - if browser.platform.mac?
+ %kbd &#8984; enter
+ - else
+ %kbd ctrl enter
+ %td= _('Commit (when editing commit message)')
diff --git a/app/views/import/google_code/new_user_map.html.haml b/app/views/import/google_code/new_user_map.html.haml
index f523b993aa7..732ba95a63f 100644
--- a/app/views/import/google_code/new_user_map.html.haml
+++ b/app/views/import/google_code/new_user_map.html.haml
@@ -30,7 +30,7 @@
.form-group.row
.col-sm-12
- = text_area_tag :user_map, JSON.pretty_generate(@user_map), class: 'form-control', rows: 15
+ = text_area_tag :user_map, Gitlab::Json.pretty_generate(@user_map), class: 'form-control', rows: 15
.form-actions
= submit_tag _('Continue to the next step'), class: "btn btn-success"
diff --git a/app/views/kaminari/gitlab/_page.html.haml b/app/views/kaminari/gitlab/_page.html.haml
index 33e00256100..b000a490e3e 100644
--- a/app/views/kaminari/gitlab/_page.html.haml
+++ b/app/views/kaminari/gitlab/_page.html.haml
@@ -11,4 +11,4 @@
('js-first-button' if page.first?),
('js-last-button' if page.last?),
('d-none d-md-block' if !page.current?) ] }
- = link_to page, url, { remote: remote, rel: page.next? ? 'next' : page.prev? ? 'prev' : nil, class: 'page-link' }
+ = link_to page, url, { remote: remote, rel: page.next? ? 'next' : page.prev? ? 'prev' : nil, class: ['page-link', active_when(page.current?)] }
diff --git a/app/views/layouts/_head.html.haml b/app/views/layouts/_head.html.haml
index 8c272a73d40..99c4fc0d1b6 100644
--- a/app/views/layouts/_head.html.haml
+++ b/app/views/layouts/_head.html.haml
@@ -68,6 +68,7 @@
= csrf_meta_tags
= csp_meta_tag
+ = action_cable_meta_tag
- unless browser.safari?
%meta{ name: 'referrer', content: 'origin-when-cross-origin' }
diff --git a/app/views/layouts/_page.html.haml b/app/views/layouts/_page.html.haml
index 49345b7b215..3885fa311ba 100644
--- a/app/views/layouts/_page.html.haml
+++ b/app/views/layouts/_page.html.haml
@@ -5,6 +5,7 @@
.mobile-overlay
.alert-wrapper
= render 'shared/outdated_browser'
+ = render_if_exists 'layouts/header/users_over_license_banner'
- if Feature.enabled?(:subscribable_banner_license, default_enabled: true)
= render_if_exists "layouts/header/ee_subscribable_banner"
= render "layouts/broadcast"
diff --git a/app/views/layouts/devise.html.haml b/app/views/layouts/devise.html.haml
index 6a261bbbc46..bbcb525ea4f 100644
--- a/app/views/layouts/devise.html.haml
+++ b/app/views/layouts/devise.html.haml
@@ -22,10 +22,10 @@
= brand_text
- else
%h3.mt-sm-0
- = _('Open source software to collaborate on code')
+ = _('A complete DevOps platform')
%p
- = _('Manage Git repositories with fine-grained access controls that keep your code secure. Perform code reviews and enhance collaboration with merge requests. Each project can also have an issue tracker and a wiki.')
+ = _('GitLab is a single application for the entire software development lifecycle. From project planning and source code management to CI/CD, monitoring, and security.')
- if Gitlab::CurrentSettings.sign_in_text.present?
= markdown_field(Gitlab::CurrentSettings.current_application_settings, :sign_in_text)
diff --git a/app/views/layouts/header/_current_user_dropdown.html.haml b/app/views/layouts/header/_current_user_dropdown.html.haml
index 410b120396d..7d9924719a2 100644
--- a/app/views/layouts/header/_current_user_dropdown.html.haml
+++ b/app/views/layouts/header/_current_user_dropdown.html.haml
@@ -27,6 +27,7 @@
%li
= link_to s_("CurrentUser|Settings"), profile_path, data: { qa_selector: 'settings_link' }
= render_if_exists 'layouts/header/buy_ci_minutes', project: @project, namespace: @group
+ = render_if_exists 'layouts/header/upgrade'
- if current_user_menu?(:help)
%li.divider.d-md-none
diff --git a/app/views/layouts/header/_help_dropdown.html.haml b/app/views/layouts/header/_help_dropdown.html.haml
index a003d6f8903..2b3f5d266b0 100644
--- a/app/views/layouts/header/_help_dropdown.html.haml
+++ b/app/views/layouts/header/_help_dropdown.html.haml
@@ -1,5 +1,6 @@
%ul
- if current_user_menu?(:help)
+ = render_if_exists 'layouts/header/whats_new_dropdown_item'
%li
= link_to _("Help"), help_path
%li
diff --git a/app/views/layouts/nav/sidebar/_admin.html.haml b/app/views/layouts/nav/sidebar/_admin.html.haml
index 52964dd6739..28e52dc85db 100644
--- a/app/views/layouts/nav/sidebar/_admin.html.haml
+++ b/app/views/layouts/nav/sidebar/_admin.html.haml
@@ -56,7 +56,7 @@
= _('Monitoring')
%ul.sidebar-sub-level-items{ data: { qa_selector: 'admin_sidebar_monitoring_submenu_content' } }
- = nav_link(controller: %w(system_info background_jobs logs health_check requests_profiles), html_options: { class: "fly-out-top-item" } ) do
+ = nav_link(controller: %w(system_info background_jobs health_check requests_profiles), html_options: { class: "fly-out-top-item" } ) do
= link_to admin_system_info_path do
%strong.fly-out-top-item-name
= _('Monitoring')
@@ -69,10 +69,6 @@
= link_to admin_background_jobs_path, title: _('Background Jobs') do
%span
= _('Background Jobs')
- = nav_link(controller: :logs) do
- = link_to admin_logs_path, title: _('Logs') do
- %span
- = _('Logs')
= nav_link(controller: :health_check) do
= link_to admin_health_check_path, title: _('Health Check') do
%span
@@ -271,11 +267,6 @@
= link_to network_admin_application_settings_path, title: _('Network'), data: { qa_selector: 'admin_settings_network_item' } do
%span
= _('Network')
- - if template_exists?('admin/geo/settings/show')
- = nav_link do
- = link_to geo_admin_application_settings_path, title: _('Geo') do
- %span
- = _('Geo')
= nav_link(path: 'application_settings#preferences') do
= link_to preferences_admin_application_settings_path, title: _('Preferences'), data: { qa_selector: 'admin_settings_preferences_link' } do
%span
diff --git a/app/views/layouts/nav/sidebar/_group.html.haml b/app/views/layouts/nav/sidebar/_group.html.haml
index f63a7b3a664..92b6174795b 100644
--- a/app/views/layouts/nav/sidebar/_group.html.haml
+++ b/app/views/layouts/nav/sidebar/_group.html.haml
@@ -102,6 +102,8 @@
= render_if_exists "layouts/nav/ee/security_link" # EE-specific
+ = render_if_exists "layouts/nav/ee/push_rules_link" # EE-specific
+
- if group_sidebar_link?(:kubernetes)
= nav_link(controller: [:clusters]) do
= link_to group_clusters_path(@group) do
diff --git a/app/views/layouts/nav/sidebar/_profile.html.haml b/app/views/layouts/nav/sidebar/_profile.html.haml
index 15f1067f0d9..95d66786984 100644
--- a/app/views/layouts/nav/sidebar/_profile.html.haml
+++ b/app/views/layouts/nav/sidebar/_profile.html.haml
@@ -152,10 +152,6 @@
= link_to audit_log_profile_path do
%strong.fly-out-top-item-name
= _('Authentication Log')
-
- - if Feature.enabled?(:user_usage_quota)
= render_if_exists 'layouts/nav/sidebar/profile_usage_quotas_link'
- - else
- = render_if_exists 'layouts/nav/sidebar/profile_pipeline_quota_link'
= render 'shared/sidebar_toggle_button'
diff --git a/app/views/layouts/nav/sidebar/_project.html.haml b/app/views/layouts/nav/sidebar/_project.html.haml
index c11d1256d21..a67860e8e2e 100644
--- a/app/views/layouts/nav/sidebar/_project.html.haml
+++ b/app/views/layouts/nav/sidebar/_project.html.haml
@@ -203,7 +203,7 @@
- if project_nav_tab? :operations
= nav_link(controller: sidebar_operations_paths) do
- = link_to sidebar_operations_link_path, class: 'shortcuts-operations qa-link-operations' do
+ = link_to sidebar_operations_link_path, class: 'shortcuts-operations', data: { qa_selector: 'operations_link' } do
.nav-icon-container
= sprite_icon('cloud-gear')
%span.nav-item-name
@@ -222,6 +222,13 @@
%span
= _('Metrics')
+ - if project_nav_tab?(:alert_management)
+ = nav_link(controller: :alert_management) do
+ = link_to project_alert_management_index_path(@project), title: _('Alerts'), class: 'shortcuts-tracking qa-operations-tracking-link' do
+ %span
+ = _('Alerts')
+
+ - if project_nav_tab? :environments
= render_if_exists "layouts/nav/sidebar/tracing_link"
= nav_link(controller: :environments, action: [:index, :folder, :show, :new, :edit, :create, :update, :stop, :terminal]) do
@@ -356,6 +363,11 @@
= link_to project_hooks_path(@project), title: _('Webhooks'), data: { qa_selector: 'webhooks_settings_link' } do
%span
= _('Webhooks')
+ - if project_access_token_available?(@project)
+ = nav_link(controller: [:access_tokens]) do
+ = link_to project_settings_access_tokens_path(@project), title: _('Access Tokens'), data: { qa_selector: 'access_tokens_settings_link' } do
+ %span
+ = _('Access Tokens')
= nav_link(controller: :repository) do
= link_to project_settings_repository_path(@project), title: _('Repository') do
%span
@@ -367,7 +379,7 @@
= _('CI / CD')
- if !@project.archived? && settings_operations_available?
= nav_link(controller: [:operations]) do
- = link_to project_settings_operations_path(@project), title: _('Operations') do
+ = link_to project_settings_operations_path(@project), title: _('Operations'), data: { qa_selector: 'operations_settings_link' } do
= _('Operations')
- if @project.pages_available?
= nav_link(controller: :pages) do
diff --git a/app/views/layouts/nav/sidebar/_project_packages_link.html.haml b/app/views/layouts/nav/sidebar/_project_packages_link.html.haml
index 0fdfc6cd2ab..0931ccdf637 100644
--- a/app/views/layouts/nav/sidebar/_project_packages_link.html.haml
+++ b/app/views/layouts/nav/sidebar/_project_packages_link.html.haml
@@ -4,12 +4,12 @@
.nav-icon-container
= sprite_icon('package')
%span.nav-item-name
- = _('Packages')
+ = _('Packages & Registries')
%ul.sidebar-sub-level-items
= nav_link(controller: :repositories, html_options: { class: "fly-out-top-item" } ) do
= link_to project_container_registry_index_path(@project) do
%strong.fly-out-top-item-name
- = _('Packages')
+ = _('Packages & Registries')
%li.divider.fly-out-top-item
= nav_link controller: :repositories do
= link_to project_container_registry_index_path(@project), class: 'shortcuts-container-registry', title: _('Container Registry') do
diff --git a/app/views/notify/group_was_exported_email.html.haml b/app/views/notify/group_was_exported_email.html.haml
new file mode 100644
index 00000000000..a2f34537662
--- /dev/null
+++ b/app/views/notify/group_was_exported_email.html.haml
@@ -0,0 +1,9 @@
+%p
+ = _('Group %{group_name} was exported successfully.') % { group_name: @group.name }
+
+%p
+ = _('The group export can be downloaded from:')
+ = link_to download_export_group_url(@group), rel: 'nofollow', download: '' do
+ #{@group.full_name} export
+%p
+ = _('The download link will expire in 24 hours.')
diff --git a/app/views/notify/group_was_exported_email.text.erb b/app/views/notify/group_was_exported_email.text.erb
new file mode 100644
index 00000000000..02571459af0
--- /dev/null
+++ b/app/views/notify/group_was_exported_email.text.erb
@@ -0,0 +1,6 @@
+<%= _('Group %{group_name} was exported successfully.') % { group_name: @group.name } %>
+
+<%= _('The group export can be downloaded from:') %>
+<%= download_export_group_url(@group) %>
+
+<%= _('The download link will expire in 24 hours.') %>
diff --git a/app/views/notify/group_was_not_exported_email.html.haml b/app/views/notify/group_was_not_exported_email.html.haml
new file mode 100644
index 00000000000..58fc34d41a3
--- /dev/null
+++ b/app/views/notify/group_was_not_exported_email.html.haml
@@ -0,0 +1,10 @@
+%p
+ = _("Group %{group_name} couldn't be exported.") % { group_name: @group.name }
+
+%p
+ = _('The errors we encountered were:')
+
+ %ul
+ - @errors.each do |error|
+ %li
+ #{error}
diff --git a/app/views/notify/group_was_not_exported_email.text.erb b/app/views/notify/group_was_not_exported_email.text.erb
new file mode 100644
index 00000000000..92bd79b7b85
--- /dev/null
+++ b/app/views/notify/group_was_not_exported_email.text.erb
@@ -0,0 +1,7 @@
+<%= _("Group %{group_name} couldn't be exported.") % { group_name: @group.name } %>
+
+<%= _('The errors we encountered were:') %>
+
+<% @errors.each do |error| -%>
+ - <%= error %>
+<% end -%>
diff --git a/app/views/notify/issues_csv_email.html.haml b/app/views/notify/issues_csv_email.html.haml
index b777ca1e57d..77502a45f02 100644
--- a/app/views/notify/issues_csv_email.html.haml
+++ b/app/views/notify/issues_csv_email.html.haml
@@ -1,9 +1,6 @@
--# haml-lint:disable NoPlainNodes
%p{ style: 'font-size:18px; text-align:center; line-height:30px;' }
- Your CSV export of #{ pluralize(@written_count, 'issue') } from project
- %a{ href: project_url(@project), style: "color:#3777b0; text-decoration:none; display:block;" }
- = @project.full_name
- has been added to this email as an attachment.
+ - project_link = link_to(@project.full_name, project_url(@project), style: "color:#3777b0; text-decoration:none; display:block;")
+ = _('Your CSV export of %{issues_count} from project %{project_link} has been added to this email as an attachment.').html_safe % { issues_count: pluralize(@written_count, 'issue'), project_link: project_link }
- if @truncated
%p
- This attachment has been truncated to avoid exceeding a maximum allowed attachment size of 15MB. #{ @written_count } of #{ @issues_count } issues have been included. Consider re-exporting with a narrower selection of issues.
+ = _('This attachment has been truncated to avoid exceeding the maximum allowed attachment size of 15MB. %{written_count} of %{issues_count} issues have been included. Consider re-exporting with a narrower selection of issues.') % { written_count: @written_count, issues_count: @issues_count }
diff --git a/app/views/notify/issues_csv_email.text.erb b/app/views/notify/issues_csv_email.text.erb
index 5d4128e3ae9..a1d2a4691bc 100644
--- a/app/views/notify/issues_csv_email.text.erb
+++ b/app/views/notify/issues_csv_email.text.erb
@@ -1,5 +1,5 @@
-Your CSV export of <%= pluralize(@written_count, 'issue') %> from project <%= @project.full_name %> (<%= project_url(@project) %>) has been added to this email as an attachment.
+<%= _('Your CSV export of %{written_count} from project %{project_name} (%{project_url}) has been added to this email as an attachment.') % { written_count: pluralize(@written_count, 'issue'), project_name: @project.full_name, project_url: project_url(@project) } %>
<% if @truncated %>
-This attachment has been truncated to avoid exceeding a maximum allowed attachment size of 15MB. <%= @written_count %> of <%= @issues_count %> issues have been included. Consider re-exporting with a narrower selection of issues.
+ <%= _('This attachment has been truncated to avoid exceeding the maximum allowed attachment size of 15MB. %{written_count} of %{issues_count} issues have been included. Consider re-exporting with a narrower selection of issues.') % { written_count: @written_count, issues_count: @issues_count} %>
<% end %>
diff --git a/app/views/notify/note_design_email.html.haml b/app/views/notify/note_design_email.html.haml
new file mode 100644
index 00000000000..5e69f01a486
--- /dev/null
+++ b/app/views/notify/note_design_email.html.haml
@@ -0,0 +1 @@
+= render 'note_email'
diff --git a/app/views/notify/note_design_email.text.erb b/app/views/notify/note_design_email.text.erb
new file mode 100644
index 00000000000..413d9e6e9ac
--- /dev/null
+++ b/app/views/notify/note_design_email.text.erb
@@ -0,0 +1 @@
+<%= render 'note_email' %>
diff --git a/app/views/notify/unknown_sign_in_email.html.haml b/app/views/notify/unknown_sign_in_email.html.haml
new file mode 100644
index 00000000000..a4123fada1b
--- /dev/null
+++ b/app/views/notify/unknown_sign_in_email.html.haml
@@ -0,0 +1,14 @@
+%p
+ = _('Hi %{username}!') % { username: sanitize_name(@user.name) }
+%p
+ = _('A sign-in to your account has been made from the following IP address: %{ip}.') % { ip: @ip }
+%p
+ - password_link_start = '<a href="%{url}" target="_blank" rel="noopener noreferrer">'.html_safe % { url: 'https://docs.gitlab.com/ee/user/profile/#changing-your-password' }
+ = _('If you recently signed in and recognize the IP address, you may disregard this email.')
+ = _('If you did not recently sign in, you should immediately %{password_link_start}change your password%{password_link_end}.').html_safe % { password_link_start: password_link_start, password_link_end: '</a>'.html_safe }
+ = _('Passwords should be unique and not used for any other sites or services.')
+
+- unless @user.two_factor_enabled?
+ %p
+ - mfa_link_start = '<a href="https://docs.gitlab.com/ee/user/profile/account/two_factor_authentication.html" target="_blank">'.html_safe
+ = _('To further protect your account, consider configuring a %{mfa_link_start}two-factor authentication%{mfa_link_end} method.').html_safe % { mfa_link_start: mfa_link_start, mfa_link_end: '</a>'.html_safe }
diff --git a/app/views/notify/unknown_sign_in_email.text.haml b/app/views/notify/unknown_sign_in_email.text.haml
new file mode 100644
index 00000000000..f3efc4c4fcd
--- /dev/null
+++ b/app/views/notify/unknown_sign_in_email.text.haml
@@ -0,0 +1,10 @@
+= _('Hi %{username}!') % { username: sanitize_name(@user.name) }
+
+= _('A sign-in to your account has been made from the following IP address: %{ip}') % { ip: @ip }
+
+= _('If you recently signed in and recognize the IP address, you may disregard this email.')
+= _('If you did not recently sign in, you should immediately change your password: %{password_link}.') % { password_link: 'https://docs.gitlab.com/ee/user/profile/#changing-your-password' }
+= _('Passwords should be unique and not used for any other sites or services.')
+
+- unless @user.two_factor_enabled?
+ = _('To further protect your account, consider configuring a two-factor authentication method: %{mfa_link}.') % { mfa_link: 'https://docs.gitlab.com/ee/user/profile/account/two_factor_authentication.html' }
diff --git a/app/views/profiles/keys/_form.html.haml b/app/views/profiles/keys/_form.html.haml
index 34e81285328..7709aa8f4b9 100644
--- a/app/views/profiles/keys/_form.html.haml
+++ b/app/views/profiles/keys/_form.html.haml
@@ -14,7 +14,7 @@
.col.form-group
= f.label :expires_at, s_('Profiles|Expires at'), class: 'label-bold'
- = f.date_field :expires_at, class: "form-control input-lg qa-key-expiry-field", min: Date.tomorrow
+ = f.date_field :expires_at, class: "form-control input-lg", min: Date.tomorrow, data: { qa_selector: 'key_expiry_date_field' }
.js-add-ssh-key-validation-warning.hide
.bs-callout.bs-callout-warning{ role: 'alert', aria_live: 'assertive' }
diff --git a/app/views/profiles/personal_access_tokens/index.html.haml b/app/views/profiles/personal_access_tokens/index.html.haml
index d9e94908b80..81b22d964a5 100644
--- a/app/views/profiles/personal_access_tokens/index.html.haml
+++ b/app/views/profiles/personal_access_tokens/index.html.haml
@@ -1,6 +1,8 @@
- breadcrumb_title s_('AccessTokens|Access Tokens')
- page_title s_('AccessTokens|Personal Access Tokens')
-- @content_class = "limit-container-width" unless fluid_layout
+- type = _('personal access token')
+- type_plural = _('personal access tokens')
+- @content_class = 'limit-container-width' unless fluid_layout
.row.prepend-top-default
.col-lg-4.profile-settings-sidebar
@@ -14,11 +16,21 @@
.col-lg-8
- if @new_personal_access_token
- = render "shared/personal_access_tokens_created_container", new_token_value: @new_personal_access_token
+ = render 'shared/access_tokens/created_container',
+ type: type,
+ new_token_value: @new_personal_access_token
- = render "shared/personal_access_tokens_form", path: profile_personal_access_tokens_path, impersonation: false, token: @personal_access_token, scopes: @scopes
+ = render 'shared/access_tokens/form',
+ type: type,
+ path: profile_personal_access_tokens_path,
+ token: @personal_access_token,
+ scopes: @scopes
- = render "shared/personal_access_tokens_table", impersonation: false, active_tokens: @active_personal_access_tokens, inactive_tokens: @inactive_personal_access_tokens
+ = render 'shared/access_tokens/table',
+ type: type,
+ type_plural: type_plural,
+ active_tokens: @active_personal_access_tokens,
+ revoke_route_helper: ->(token) { revoke_profile_personal_access_token_path(token) }
%hr
.row.prepend-top-default
@@ -30,7 +42,7 @@
%p
= s_('AccessTokens|It cannot be used to access any other data.')
.col-lg-8.feed-token-reset
- = label_tag :feed_token, s_('AccessTokens|Feed token'), class: "label-bold"
+ = label_tag :feed_token, s_('AccessTokens|Feed token'), class: 'label-bold'
= text_field_tag :feed_token, current_user.feed_token, class: 'form-control js-select-on-focus', readonly: true
%p.form-text.text-muted
- reset_link = link_to s_('AccessTokens|reset it'), [:reset, :feed_token, :profile], method: :put, data: { confirm: s_('AccessTokens|Are you sure? Any RSS or calendar URLs currently in use will stop working.') }
@@ -48,7 +60,7 @@
%p
= s_('AccessTokens|It cannot be used to access any other data.')
.col-lg-8.incoming-email-token-reset
- = label_tag :incoming_email_token, s_('AccessTokens|Incoming email token'), class: "label-bold"
+ = label_tag :incoming_email_token, s_('AccessTokens|Incoming email token'), class: 'label-bold'
= text_field_tag :incoming_email_token, current_user.incoming_email_token, class: 'form-control js-select-on-focus', readonly: true
%p.form-text.text-muted
- reset_link = link_to s_('AccessTokens|reset it'), [:reset, :incoming_email_token, :profile], method: :put, data: { confirm: s_('AccessTokens|Are you sure? Any issue email addresses currently in use will stop working.') }
diff --git a/app/views/profiles/show.html.haml b/app/views/profiles/show.html.haml
index da2b8c40191..43fc9150e99 100644
--- a/app/views/profiles/show.html.haml
+++ b/app/views/profiles/show.html.haml
@@ -20,6 +20,9 @@
= s_("Profiles|You can upload your avatar here or change it at %{gravatar_link}").html_safe % { gravatar_link: gravatar_link }
- else
= s_("Profiles|You can upload your avatar here")
+ - if current_appearance&.profile_image_guidelines?
+ .md
+ = brand_profile_image_guidelines
.col-lg-8
.clearfix.avatar-image.append-bottom-default
= link_to avatar_icon_for_user(@user, 400), target: '_blank', rel: 'noopener noreferrer' do
@@ -101,7 +104,7 @@
- else
= f.text_field :location, label: s_('Profiles|Location'), class: 'input-lg', placeholder: s_("Profiles|City, country")
= f.text_field :job_title, class: 'input-md'
- = f.text_field :organization, readonly: @user.gitlab_employee?, label: s_('Profiles|Organization'), class: 'input-md', help: s_("Profiles|Who you represent or work for")
+ = f.text_field :organization, label: s_('Profiles|Organization'), class: 'input-md', help: s_("Profiles|Who you represent or work for")
= f.text_area :bio, label: s_('Profiles|Bio'), rows: 4, maxlength: 250, help: s_("Profiles|Tell us about yourself in fewer than 250 characters")
%hr
%h5= s_("Private profile")
diff --git a/app/views/projects/alert_management/details.html.haml b/app/views/projects/alert_management/details.html.haml
new file mode 100644
index 00000000000..5230d5e3476
--- /dev/null
+++ b/app/views/projects/alert_management/details.html.haml
@@ -0,0 +1,4 @@
+- add_to_breadcrumbs s_('AlertManagement|Alerts'), project_alert_management_index_path(@project)
+- page_title s_('AlertManagement|Alert detail')
+
+#js-alert_details{ data: alert_management_detail_data(@project, @alert_id) }
diff --git a/app/views/projects/alert_management/index.html.haml b/app/views/projects/alert_management/index.html.haml
new file mode 100644
index 00000000000..415820ac3ad
--- /dev/null
+++ b/app/views/projects/alert_management/index.html.haml
@@ -0,0 +1,3 @@
+- page_title _('Alerts')
+
+#js-alert_management{ data: alert_management_data(@current_user, @project) }
diff --git a/app/views/projects/blob/_header.html.haml b/app/views/projects/blob/_header.html.haml
index 76a9d3df5d7..2a1545e7db7 100644
--- a/app/views/projects/blob/_header.html.haml
+++ b/app/views/projects/blob/_header.html.haml
@@ -4,13 +4,13 @@
.file-actions<
= render 'projects/blob/viewer_switcher', blob: blob unless blame
- = edit_blob_button
- = ide_edit_button
+ = edit_blob_button(@project, @ref, @path, blob: blob)
+ = ide_edit_button(@project, @ref, @path, blob: blob)
.btn-group.ml-2{ role: "group" }>
= render_if_exists 'projects/blob/header_file_locks_link'
- if current_user
- = replace_blob_link
- = delete_blob_link
+ = replace_blob_link(@project, @ref, @path, blob: blob)
+ = delete_blob_link(@project, @ref, @path, blob: blob)
.btn-group.ml-2{ role: "group" }
= copy_blob_source_button(blob) unless blame
= open_raw_blob_button(blob)
diff --git a/app/views/projects/blob/_template_selectors.html.haml b/app/views/projects/blob/_template_selectors.html.haml
index 2be95bc5541..ba8029ac32a 100644
--- a/app/views/projects/blob/_template_selectors.html.haml
+++ b/app/views/projects/blob/_template_selectors.html.haml
@@ -1,4 +1,4 @@
-.template-selectors-menu.gl-pl-2
+.template-selectors-menu.gl-pl-2-deprecated-no-really-do-not-use-me
.template-selector-dropdowns-wrap
.template-type-selector.js-template-type-selector-wrap.hidden
- toggle_text = should_suggest_gitlab_ci_yml? ? '.gitlab-ci.yml' : 'Select a template type'
diff --git a/app/views/projects/buttons/_download.html.haml b/app/views/projects/buttons/_download.html.haml
index cae8bbf8c01..445752d0a15 100644
--- a/app/views/projects/buttons/_download.html.haml
+++ b/app/views/projects/buttons/_download.html.haml
@@ -12,14 +12,13 @@
%h5.m-0.dropdown-bold-header= _('Download source code')
.dropdown-menu-content
= render 'projects/buttons/download_links', project: project, ref: ref, archive_prefix: archive_prefix, path: nil
- - if Feature.enabled?(:git_archive_path, default_enabled: true)
- - if vue_file_list_enabled?
- #js-directory-downloads{ data: { links: directory_download_links(project, ref, archive_prefix).to_json } }
- - elsif directory?
- %section.border-top.pt-1.mt-1
- %h5.m-0.dropdown-bold-header= _('Download this directory')
- .dropdown-menu-content
- = render 'projects/buttons/download_links', project: project, ref: ref, archive_prefix: archive_prefix, path: @path
+ - if vue_file_list_enabled?
+ #js-directory-downloads{ data: { links: directory_download_links(project, ref, archive_prefix).to_json } }
+ - elsif directory?
+ %section.border-top.pt-1.mt-1
+ %h5.m-0.dropdown-bold-header= _('Download this directory')
+ .dropdown-menu-content
+ = render 'projects/buttons/download_links', project: project, ref: ref, archive_prefix: archive_prefix, path: @path
- if pipeline && pipeline.latest_builds_with_artifacts.any?
%section.border-top.pt-1.mt-1
%h5.m-0.dropdown-bold-header= _('Download artifacts')
diff --git a/app/views/projects/commit/_signature.html.haml b/app/views/projects/commit/_signature.html.haml
index aa7c90bad66..fb31ac44118 100644
--- a/app/views/projects/commit/_signature.html.haml
+++ b/app/views/projects/commit/_signature.html.haml
@@ -1,3 +1,3 @@
- if signature
- - uri = "projects/commit/#{"x509/" if signature.instance_of?(X509CommitSignature)}"
+ - uri = "projects/commit/#{"x509/" if x509_signature?(signature)}"
= render partial: "#{uri}#{signature.verification_status}_signature_badge", locals: { signature: signature }
diff --git a/app/views/projects/commit/_signature_badge.html.haml b/app/views/projects/commit/_signature_badge.html.haml
index 8ecaa1329fd..8004a5facd7 100644
--- a/app/views/projects/commit/_signature_badge.html.haml
+++ b/app/views/projects/commit/_signature_badge.html.haml
@@ -17,13 +17,13 @@
- content = capture do
- if show_user
.clearfix
- - uri_signature_badge_user = "projects/commit/#{"x509/" if signature.instance_of?(X509CommitSignature)}signature_badge_user"
+ - uri_signature_badge_user = "projects/commit/#{"x509/" if x509_signature?(signature)}signature_badge_user"
= render partial: "#{uri_signature_badge_user}", locals: { signature: signature }
- - if signature.instance_of?(X509CommitSignature)
+ - if x509_signature?(signature)
= render partial: "projects/commit/x509/certificate_details", locals: { signature: signature }
- = link_to(_('Learn more about x509 signed commits'), help_page_path('user/project/repository/x509_signed_commits/index.md'), class: 'gpg-popover-help-link')
+ = link_to(_('Learn more about X.509 signed commits'), help_page_path('user/project/repository/x509_signed_commits/index.md'), class: 'gpg-popover-help-link')
- else
= _('GPG Key ID:')
%span.monospace= signature.gpg_key_primary_keyid
diff --git a/app/views/projects/commit/x509/_signature_badge_user.html.haml b/app/views/projects/commit/x509/_signature_badge_user.html.haml
index b64ccba2a18..f3d39b21ec2 100644
--- a/app/views/projects/commit/x509/_signature_badge_user.html.haml
+++ b/app/views/projects/commit/x509/_signature_badge_user.html.haml
@@ -1,5 +1,5 @@
-- user = signature.commit.committer
- user_email = signature.x509_certificate.email
+- user = signature.user
- if user
= link_to user_path(user), class: 'gpg-popover-user-link' do
diff --git a/app/views/projects/commits/_commit.html.haml b/app/views/projects/commits/_commit.html.haml
index 8b659034fe6..b42eef32a76 100644
--- a/app/views/projects/commits/_commit.html.haml
+++ b/app/views/projects/commits/_commit.html.haml
@@ -1,6 +1,8 @@
-#-----------------------------------------------------------------
WARNING: Please keep changes up-to-date with the following files:
- `assets/javascripts/diffs/components/commit_item.vue`
+
+ EXCEPTION WARNING - see above `.vue` file for de-sync drift
-#-----------------------------------------------------------------
- view_details = local_assigns.fetch(:view_details, false)
- merge_request = local_assigns.fetch(:merge_request, nil)
diff --git a/app/views/projects/cycle_analytics/show.html.haml b/app/views/projects/cycle_analytics/show.html.haml
index da20fee227a..b6c30c680e4 100644
--- a/app/views/projects/cycle_analytics/show.html.haml
+++ b/app/views/projects/cycle_analytics/show.html.haml
@@ -5,6 +5,9 @@
%banner{ "v-if" => "!isOverviewDialogDismissed",
"documentation-link": help_page_path('user/analytics/value_stream_analytics.md'),
"v-on:dismiss-overview-dialog" => "dismissOverviewDialog()" }
+ .mb-3
+ %h3
+ = _("Value Stream Analytics")
%gl-loading-icon{ "v-show" => "isLoading", "size" => "lg" }
.wrapper{ "v-show" => "!isLoading && !hasError" }
.card
@@ -54,7 +57,7 @@
%nav.stage-nav
%ul
%stage-nav-item{ "v-for" => "stage in state.stages", ":key" => '`ca-stage-title-${stage.title}`', '@select' => 'selectStage(stage)', ":title" => "stage.title", ":is-user-allowed" => "stage.isUserAllowed", ":value" => "stage.value", ":is-active" => "stage.active" }
- .section.stage-events
+ .section.stage-events.overflow-auto
%gl-loading-icon{ "v-show" => "isLoadingStage", "size" => "lg" }
%template{ "v-if" => "currentStage && !currentStage.isUserAllowed" }
= render partial: "no_access"
diff --git a/app/views/projects/deploy_keys/_form.html.haml b/app/views/projects/deploy_keys/_form.html.haml
deleted file mode 100644
index 568930595a2..00000000000
--- a/app/views/projects/deploy_keys/_form.html.haml
+++ /dev/null
@@ -1,24 +0,0 @@
-= form_for [@project.namespace.becomes(Namespace), @project, @deploy_keys.new_key], url: namespace_project_deploy_keys_path, html: { class: "js-requires-input container" } do |f|
- = form_errors(@deploy_keys.new_key)
- .form-group.row
- = f.label :title, class: "label-bold"
- = f.text_field :title, class: 'form-control', required: true
- .form-group.row
- = f.label :key, class: "label-bold"
- = f.text_area :key, class: "form-control", rows: 5, required: true
- .form-group.row
- %p.light.append-bottom-0
- Paste a machine public key here. Read more about how to generate it
- = link_to "here", help_page_path("ssh/README")
-
- = f.fields_for :deploy_keys_projects do |deploy_keys_project_form|
- .form-group.row
- = deploy_keys_project_form.label :can_push do
- = deploy_keys_project_form.check_box :can_push
- %strong Write access allowed
- .form-group.row
- %p.light.append-bottom-0
- Allow this key to push to repository as well? (Default only allows pull access.)
-
- .form-group.row
- = f.submit "Add key", class: "btn-success btn"
diff --git a/app/views/projects/deploy_keys/_index.html.haml b/app/views/projects/deploy_keys/_index.html.haml
deleted file mode 100644
index 6b3b824f72f..00000000000
--- a/app/views/projects/deploy_keys/_index.html.haml
+++ /dev/null
@@ -1,15 +0,0 @@
-- expanded = expanded_by_default?
-%section.qa-deploy-keys-settings.settings.no-animate#js-deploy-keys-settings{ class: ('expanded' if expanded), data: { qa_selector: 'deploy_keys_settings' } }
- .settings-header
- %h4
- Deploy Keys
- %button.btn.js-settings-toggle{ type: 'button' }
- = expanded ? 'Collapse' : 'Expand'
- %p
- Deploy keys allow read-only or read-write (if enabled) access to your repository. Deploy keys can be used for CI, staging or production servers. You can create a deploy key or add an existing one.
- .settings-content
- %h5.prepend-top-0
- Create a new deploy key for this project
- = render @deploy_keys.form_partial_path
- %hr
- #js-deploy-keys{ data: { endpoint: project_deploy_keys_path(@project), project_id: @project.id } }
diff --git a/app/views/projects/graphs/charts.html.haml b/app/views/projects/graphs/charts.html.haml
index 19fe7ba4360..7257dacf680 100644
--- a/app/views/projects/graphs/charts.html.haml
+++ b/app/views/projects/graphs/charts.html.haml
@@ -1,5 +1,9 @@
- page_title _("Repository Analytics")
+.mb-3
+ %h3
+ = _("Repository Analytics")
+
.repo-charts
%h4.sub-header
= _("Programming languages used in this repository")
@@ -9,6 +13,23 @@
#js-languages-chart{ data: { chart_data: @languages.to_json.html_safe } }
+- if defined?(@daily_coverage_options)
+ .repo-charts.my-5
+ .sub-header-block.border-top
+ .d-flex.justify-content-between.align-items-center
+ %h4.sub-header.m-0
+ - start_date = capture do
+ #{@daily_coverage_options[:base_params][:start_date].strftime('%b %d')}
+ - end_date = capture do
+ #{@daily_coverage_options[:base_params][:end_date].strftime('%b %d')}
+ = (_("Code coverage statistics for master %{start_date} - %{end_date}") % {start_date: start_date, end_date: end_date})
+ - download_path = capture do
+ #{@daily_coverage_options[:download_path]}
+ %a.btn.btn-sm{ href: "#{download_path}?#{@daily_coverage_options[:base_params].to_query}" }
+ %small
+ = _("Download raw data (.csv)")
+ #js-code-coverage-chart{ data: { daily_coverage_options: @daily_coverage_options.to_json.html_safe } }
+
.repo-charts
.sub-header-block.border-top
diff --git a/app/views/projects/graphs/show.html.haml b/app/views/projects/graphs/show.html.haml
index a952db0eea3..495a4ac50bf 100644
--- a/app/views/projects/graphs/show.html.haml
+++ b/app/views/projects/graphs/show.html.haml
@@ -1,6 +1,6 @@
- page_title _('Contributors')
-.sub-header-block.bg-gray-light.gl-p-3
+.sub-header-block.bg-gray-light.gl-p-3-deprecated-no-really-do-not-use-me
.tree-ref-holder.inline.vertical-align-middle
= render 'shared/ref_switcher', destination: 'graphs'
= link_to s_('Commits|History'), project_commits_path(@project, current_ref), class: 'btn'
diff --git a/app/views/projects/import/jira/show.html.haml b/app/views/projects/import/jira/show.html.haml
index 4106bcc2e5a..cddd97cbc84 100644
--- a/app/views/projects/import/jira/show.html.haml
+++ b/app/views/projects/import/jira/show.html.haml
@@ -1,6 +1,7 @@
- if Feature.enabled?(:jira_issue_import_vue, @project, default_enabled: true)
.js-jira-import-root{ data: { project_path: @project.full_path,
issues_path: project_issues_path(@project),
+ jira_integration_path: edit_project_service_path(@project, :jira),
is_jira_configured: @project.jira_service.present?.to_s,
jira_projects: @jira_projects.to_json,
in_progress_illustration: image_path('illustrations/export-import.svg'),
diff --git a/app/views/projects/issues/_design_management.html.haml b/app/views/projects/issues/_design_management.html.haml
new file mode 100644
index 00000000000..96f1dc0155c
--- /dev/null
+++ b/app/views/projects/issues/_design_management.html.haml
@@ -0,0 +1,15 @@
+- if @project.design_management_enabled?
+ .js-design-management{ data: { project_path: @project.full_path, issue_iid: @issue.iid, issue_path: project_issue_path(@project, @issue) } }
+- else
+ .mt-4
+ .row.empty-state
+ .col-12
+ .text-content
+ %h4.center
+ = _('The one place for your designs')
+ %p.center
+ - requirements_link_url = help_page_path('user/project/issues/design_management', anchor: 'requirements')
+ - requirements_link_start = '<a href="%{url}" target="_blank" rel="noopener noreferrer">'.html_safe % { url: requirements_link_url }
+ - support_link_start = '<a href="%{url}" target="_blank" rel="noopener noreferrer">'.html_safe % { url: support_url }
+ - link_end = '</a>'.html_safe
+ = s_("DesignManagement|To enable design management, you'll need to %{requirements_link_start}meet the requirements%{requirements_link_end}. If you need help, reach out to our %{support_link_start}support team%{support_link_end} for assistance.").html_safe % { requirements_link_start: requirements_link_start, requirements_link_end: link_end, support_link_start: support_link_start, support_link_end: link_end }
diff --git a/app/views/projects/issues/_issue.html.haml b/app/views/projects/issues/_issue.html.haml
index 54002b9ca2e..1bf0c8eb031 100644
--- a/app/views/projects/issues/_issue.html.haml
+++ b/app/views/projects/issues/_issue.html.haml
@@ -6,12 +6,13 @@
= check_box_tag dom_id(issue, "selected"), nil, false, 'data-id' => issue.id, class: "selected-issuable"
.issuable-info-container
.issuable-main-info
- .issue-title.title
+ .issue-title.title.d-flex.align-items-center
%span.issue-title-text.js-onboarding-issue-item{ dir: "auto" }
- if issue.confidential?
%span.has-tooltip{ title: _('Confidential') }
= confidential_icon(issue)
= link_to issue.title, issue_path(issue)
+ = render_if_exists 'projects/issues/subepic_flag', issue: issue
- if issue.tasks?
%span.task-status.d-none.d-sm-inline-block
&nbsp;
@@ -24,7 +25,7 @@
&middot;
opened #{time_ago_with_tooltip(issue.created_at, placement: 'bottom')}
by #{link_to_member(@project, issue.author, avatar: false)}
- = gitlab_team_member_badge(issue.author)
+ = render_if_exists 'shared/issuable/gitlab_team_member_badge', {author: issue.author}
- if issue.milestone
%span.issuable-milestone.d-none.d-sm-inline-block
&nbsp;
diff --git a/app/views/projects/issues/_related_branches.html.haml b/app/views/projects/issues/_related_branches.html.haml
index 69b030ed76a..0604e89be6e 100644
--- a/app/views/projects/issues/_related_branches.html.haml
+++ b/app/views/projects/issues/_related_branches.html.haml
@@ -4,11 +4,9 @@
%ul.unstyled-list.related-merge-requests
- @related_branches.each do |branch|
%li
- - target = @project.repository.find_branch(branch).dereferenced_target
- - pipeline = @project.pipeline_for(branch, target.sha) if target
- - if can?(current_user, :read_pipeline, pipeline)
+ - if branch[:pipeline_status].present?
%span.related-branch-ci-status
- = render 'ci/status/icon', status: pipeline.detailed_status(current_user)
+ = render 'ci/status/icon', status: branch[:pipeline_status]
%span.related-branch-info
%strong
- = link_to branch, project_compare_path(@project, from: @project.default_branch, to: branch), class: "ref-name"
+ = link_to branch[:name], branch[:link], class: "ref-name"
diff --git a/app/views/projects/issues/_tabs.html.haml b/app/views/projects/issues/_tabs.html.haml
new file mode 100644
index 00000000000..d998a01623f
--- /dev/null
+++ b/app/views/projects/issues/_tabs.html.haml
@@ -0,0 +1,14 @@
+%ul.nav-tabs.nav.nav-links{ role: 'tablist' }
+ %li
+ = link_to '#discussion-tab', class: 'active js-issue-tabs', id: 'discussion', role: 'tab', 'aria-controls': 'js-discussion', 'aria-selected': 'true', data: { toggle: 'tab', target: '#discussion-tab', qa_selector: 'discussion_tab_link' } do
+ = _('Discussion')
+ %span.badge.badge-pill.js-discussions-count
+ %li
+ = link_to '#designs-tab', class: 'js-issue-tabs', id: 'designs', role: 'tab', 'aria-controls': 'js-designs', 'aria-selected': 'false', data: { toggle: 'tab', target: '#designs-tab', qa_selector: 'designs_tab_link' } do
+ = _('Designs')
+ %span.badge.badge-pill.js-designs-count
+.tab-content
+ #discussion-tab.tab-pane.show.active{ role: 'tabpanel', 'aria-labelledby': 'discussion', data: { qa_selector: 'discussion_tab_content' } }
+ = render 'projects/issues/discussion'
+ #designs-tab.tab-pane{ role: 'tabpanel', 'aria-labelledby': 'designs', data: { qa_selector: 'designs_tab_content' } }
+ = render 'projects/issues/design_management'
diff --git a/app/views/projects/issues/export_csv/_modal.html.haml b/app/views/projects/issues/export_csv/_modal.html.haml
index af3a087ca59..9fdeb901b56 100644
--- a/app/views/projects/issues/export_csv/_modal.html.haml
+++ b/app/views/projects/issues/export_csv/_modal.html.haml
@@ -1,4 +1,3 @@
--# haml-lint:disable NoPlainNodes
- if current_user
.issues-export-modal.modal
.modal-dialog
diff --git a/app/views/projects/issues/index.html.haml b/app/views/projects/issues/index.html.haml
index 2633a3899f7..0aef4e39466 100644
--- a/app/views/projects/issues/index.html.haml
+++ b/app/views/projects/issues/index.html.haml
@@ -6,6 +6,12 @@
= content_for :meta_tags do
= auto_discovery_link_tag(:atom, safe_params.merge(rss_url_options).to_h, title: "#{@project.name} issues")
+- if @project.jira_issues_import_feature_flag_enabled?
+ .js-projects-issues-root{ data: { can_edit: can?(current_user, :admin_project, @project).to_s,
+ is_jira_configured: @project.jira_service.present?.to_s,
+ issues_path: project_issues_path(@project),
+ project_path: @project.full_path } }
+
- if project_issues(@project).exists?
.top-area
= render 'shared/issuable/nav', type: :issues
diff --git a/app/views/projects/issues/show.html.haml b/app/views/projects/issues/show.html.haml
index 4fc67884584..c8ffa2e3720 100644
--- a/app/views/projects/issues/show.html.haml
+++ b/app/views/projects/issues/show.html.haml
@@ -10,6 +10,8 @@
- can_report_spam = @issue.submittable_as_spam_by?(current_user)
- can_create_issue = show_new_issue_link?(@project)
+= render_if_exists "projects/issues/alert_blocked", issue: @issue, current_user: current_user
+
.detail-page-header
.detail-page-header-body
.issuable-status-box.status-box.status-box-issue-closed{ class: issue_status_visibility(@issue, status_box: :closed) }
@@ -50,7 +52,7 @@
%li.divider
%li= link_to 'New issue', new_project_issue_path(@project), id: 'new_issue_link'
- = render 'shared/issuable/close_reopen_button', issuable: @issue, can_update: can_update_issue, can_reopen: can_reopen_issue
+ = render 'shared/issuable/close_reopen_button', issuable: @issue, can_update: can_update_issue, can_reopen: can_reopen_issue, warn_before_close: defined?(@issue.blocked?) && @issue.blocked?
- if can_report_spam
= link_to 'Submit as spam', mark_as_spam_project_issue_path(@project, @issue), method: :post, class: 'd-none d-sm-none d-md-block btn btn-grouped btn-spam', title: 'Submit as spam'
@@ -84,13 +86,13 @@
.content-block.emoji-block.emoji-block-sticky
.row
- .col-md-12.col-lg-6.js-noteable-awards
+ .col-md-12.col-lg-4.js-noteable-awards
= render 'award_emoji/awards_block', awardable: @issue, inline: true
- .col-md-12.col-lg-6.new-branch-col
+ .col-md-12.col-lg-8.new-branch-col
#js-vue-sort-issue-discussions
#js-vue-discussion-filter{ data: { default_filter: current_user&.notes_filter_for(@issue), notes_filters: UserPreference.notes_filters.to_json } }
= render 'new_branch' if show_new_branch_button?
- = render_if_exists 'projects/issues/discussion'
+ = render 'projects/issues/tabs'
= render 'shared/issuable/sidebar', issuable_sidebar: @issuable_sidebar, assignees: @issue.assignees
diff --git a/app/views/projects/labels/index.html.haml b/app/views/projects/labels/index.html.haml
index 0373e37818d..760d81136c6 100644
--- a/app/views/projects/labels/index.html.haml
+++ b/app/views/projects/labels/index.html.haml
@@ -22,13 +22,13 @@
.content-list.manage-labels-list.js-prioritized-labels{ data: { url: set_priorities_project_labels_path(@project), sortable: can_admin_label } }
#js-priority-labels-empty-state.priority-labels-empty-state{ class: "#{'hidden' unless @prioritized_labels.empty? && search.blank?}" }
= render 'shared/empty_states/priority_labels'
- - if @prioritized_labels.present?
+ - if @prioritized_labels.any?
= render partial: 'shared/label', collection: @prioritized_labels, as: :label, locals: { force_priority: true, subject: @project }
- elsif search.present?
.nothing-here-block
= _('No prioritized labels with such name or description')
- - if @labels.present?
+ - if @labels.any?
.other-labels
%h5{ class: ('hide' if hide) }= _('Other Labels')
.content-list.manage-labels-list.js-other-labels
diff --git a/app/views/projects/merge_requests/_how_to_merge.html.haml b/app/views/projects/merge_requests/_how_to_merge.html.haml
index 9cdbbe7204b..a2da0e707d3 100644
--- a/app/views/projects/merge_requests/_how_to_merge.html.haml
+++ b/app/views/projects/merge_requests/_how_to_merge.html.haml
@@ -53,4 +53,4 @@
%strong Tip:
= succeed '.' do
You can also checkout merge requests locally by
- = link_to 'following these guidelines', help_page_path('user/project/merge_requests/index.md', anchor: "checkout-merge-requests-locally"), target: '_blank', rel: 'noopener noreferrer'
+ = link_to 'following these guidelines', help_page_path('user/project/merge_requests/reviewing_and_managing_merge_requests.md', anchor: "checkout-merge-requests-locally"), target: '_blank', rel: 'noopener noreferrer'
diff --git a/app/views/projects/merge_requests/_merge_request.html.haml b/app/views/projects/merge_requests/_merge_request.html.haml
index f7f5388a54a..a753ee50c43 100644
--- a/app/views/projects/merge_requests/_merge_request.html.haml
+++ b/app/views/projects/merge_requests/_merge_request.html.haml
@@ -20,7 +20,7 @@
&middot;
opened #{time_ago_with_tooltip(merge_request.created_at, placement: 'bottom')}
by #{link_to_member(@project, merge_request.author, avatar: false)}
- = gitlab_team_member_badge(merge_request.author)
+ = render_if_exists 'shared/issuable/gitlab_team_member_badge', {author: merge_request.author}
- if merge_request.milestone
%span.issuable-milestone.d-none.d-sm-inline-block
&nbsp;
diff --git a/app/views/projects/merge_requests/_widget.html.haml b/app/views/projects/merge_requests/_widget.html.haml
index 1853d40c2e4..6aba5c98d52 100644
--- a/app/views/projects/merge_requests/_widget.html.haml
+++ b/app/views/projects/merge_requests/_widget.html.haml
@@ -8,6 +8,7 @@
window.gl.mrWidgetData.squash_before_merge_help_path = '#{help_page_path("user/project/merge_requests/squash_and_merge")}';
window.gl.mrWidgetData.troubleshooting_docs_path = '#{help_page_path('user/project/merge_requests/reviewing_and_managing_merge_requests.md', anchor: 'troubleshooting')}';
+ window.gl.mrWidgetData.pipeline_must_succeed_docs_path = '#{help_page_path('user/project/merge_requests/merge_when_pipeline_succeeds.md', anchor: 'only-allow-merge-requests-to-be-merged-if-the-pipeline-succeeds')}';
window.gl.mrWidgetData.security_approvals_help_page_path = '#{help_page_path('user/application_security/index.html', anchor: 'security-approvals-in-merge-requests-ultimate')}';
window.gl.mrWidgetData.eligible_approvers_docs_path = '#{help_page_path('user/project/merge_requests/merge_request_approvals', anchor: 'eligible-approvers')}';
window.gl.mrWidgetData.pipelines_empty_svg_path = '#{image_path('illustrations/pipelines_empty.svg')}';
diff --git a/app/views/projects/merge_requests/creations/update_branches.html.haml b/app/views/projects/merge_requests/creations/update_branches.html.haml
deleted file mode 100644
index 64482973a89..00000000000
--- a/app/views/projects/merge_requests/creations/update_branches.html.haml
+++ /dev/null
@@ -1,3 +0,0 @@
-= render 'projects/merge_requests/dropdowns/branch',
-branches: @target_branches,
-selected: nil
diff --git a/app/views/projects/mirrors/_mirror_repos.html.haml b/app/views/projects/mirrors/_mirror_repos.html.haml
index 4004c4f4b07..38e4fbf73e0 100644
--- a/app/views/projects/mirrors/_mirror_repos.html.haml
+++ b/app/views/projects/mirrors/_mirror_repos.html.haml
@@ -10,7 +10,7 @@
= expanded ? _('Collapse') : _('Expand')
%p
= _('Set up your project to automatically push and/or pull changes to/from another repository. Branches, tags, and commits will be synced automatically.')
- = link_to _('Read more'), help_page_path('workflow/repository_mirroring'), target: '_blank'
+ = link_to _('Read more'), help_page_path('user/project/repository/repository_mirroring.md'), target: '_blank'
.settings-content
- if mirror_settings_enabled
diff --git a/app/views/projects/mirrors/_mirror_repos_push.html.haml b/app/views/projects/mirrors/_mirror_repos_push.html.haml
index 8482424a184..9b5b31bfc15 100644
--- a/app/views/projects/mirrors/_mirror_repos_push.html.haml
+++ b/app/views/projects/mirrors/_mirror_repos_push.html.haml
@@ -1,15 +1,13 @@
- protocols = Gitlab::UrlSanitizer::ALLOWED_SCHEMES.join('|')
-- keep_divergent_refs = Feature.enabled?(:keep_divergent_refs, @project)
= f.fields_for :remote_mirrors, @project.remote_mirrors.build do |rm_f|
= rm_f.hidden_field :enabled, value: '1'
= rm_f.hidden_field :url, class: 'js-mirror-url-hidden', required: true, pattern: "(#{protocols}):\/\/.+"
= rm_f.hidden_field :only_protected_branches, class: 'js-mirror-protected-hidden'
- - if keep_divergent_refs
- = rm_f.hidden_field :keep_divergent_refs, class: 'js-mirror-keep-divergent-refs-hidden'
+ = rm_f.hidden_field :keep_divergent_refs, class: 'js-mirror-keep-divergent-refs-hidden'
= render partial: 'projects/mirrors/ssh_host_keys', locals: { f: rm_f }
= render partial: 'projects/mirrors/authentication_method', locals: { f: rm_f }
- - if keep_divergent_refs
- .form-check.append-bottom-10
- = check_box_tag :keep_divergent_refs, '1', false, class: 'js-mirror-keep-divergent-refs form-check-input'
- = label_tag :keep_divergent_refs, 'Keep divergent refs', class: 'form-check-label'
+ .form-check.append-bottom-10
+ = check_box_tag :keep_divergent_refs, '1', false, class: 'js-mirror-keep-divergent-refs form-check-input'
+ = label_tag :keep_divergent_refs, _('Keep divergent refs'), class: 'form-check-label'
+ = link_to icon('question-circle'), help_page_path('user/project/repository/repository_mirroring', anchor: 'keep-divergent-refs-core'), target: '_blank'
diff --git a/app/views/projects/new.html.haml b/app/views/projects/new.html.haml
index 3ff4ab354b9..c18af6a267b 100644
--- a/app/views/projects/new.html.haml
+++ b/app/views/projects/new.html.haml
@@ -18,7 +18,7 @@
= _('All features are enabled for blank projects, from templates, or when importing, but you can disable them afterward in the project settings.')
= render_if_exists 'projects/new_ci_cd_banner_external_repo'
%p
- - pages_getting_started_guide = link_to _('Pages getting started guide'), help_page_path("user/project/pages/getting_started_part_two", anchor: "fork-a-project-to-get-started-from"), target: '_blank'
+ - pages_getting_started_guide = link_to _('Pages getting started guide'), help_page_path("user/project/pages/index", anchor: "getting-started"), target: '_blank'
= _('Information about additional Pages templates and how to install them can be found in our %{pages_getting_started_guide}.').html_safe % { pages_getting_started_guide: pages_getting_started_guide }
.md
= brand_new_project_guidelines
diff --git a/app/views/projects/pipelines/_with_tabs.html.haml b/app/views/projects/pipelines/_with_tabs.html.haml
index 37ca020cfb6..e39f543d42e 100644
--- a/app/views/projects/pipelines/_with_tabs.html.haml
+++ b/app/views/projects/pipelines/_with_tabs.html.haml
@@ -1,4 +1,5 @@
-- test_reports_enabled = Feature.enabled?(:junit_pipeline_view)
+- test_reports_enabled = Feature.enabled?(:junit_pipeline_view, @project)
+- dag_pipeline_tab_enabled = Feature.enabled?(:dag_pipeline_tab)
.tabs-holder
%ul.pipelines-tabs.nav-links.no-top.no-bottom.mobile-separator.nav.nav-tabs
@@ -9,6 +10,10 @@
= link_to builds_project_pipeline_path(@project, @pipeline), data: { target: '#js-tab-builds', action: 'builds', toggle: 'tab' }, class: 'builds-tab' do
= _('Jobs')
%span.badge.badge-pill.js-builds-counter= pipeline.total_size
+ - if dag_pipeline_tab_enabled
+ %li.js-dag-tab-link
+ = link_to dag_project_pipeline_path(@project, @pipeline), data: { target: '#js-tab-dag', action: 'dag', toggle: 'tab' }, class: 'dag-tab' do
+ = _('DAG')
- if @pipeline.failed_builds.present?
%li.js-failures-tab-link
= link_to failures_project_pipeline_path(@project, @pipeline), data: { target: '#js-tab-failures', action: 'failures', toggle: 'tab' }, class: 'failures-tab' do
@@ -75,6 +80,9 @@
%code.bash.js-build-output
= build_summary(build)
+ - if dag_pipeline_tab_enabled
+ #js-tab-dag.tab-pane
+
#js-tab-tests.tab-pane
#js-pipeline-tests-detail
= render_if_exists "projects/pipelines/tabs_content", pipeline: @pipeline, project: @project
diff --git a/app/views/projects/pipelines/index.html.haml b/app/views/projects/pipelines/index.html.haml
index f64f07487fd..64789c7c263 100644
--- a/app/views/projects/pipelines/index.html.haml
+++ b/app/views/projects/pipelines/index.html.haml
@@ -3,6 +3,7 @@
= render_if_exists "shared/shared_runners_minutes_limit_flash_message"
#pipelines-list-vue{ data: { endpoint: project_pipelines_path(@project, format: :json),
+ project_id: @project.id,
"help-page-path" => help_page_path('ci/quick_start/README'),
"help-auto-devops-path" => help_page_path('topics/autodevops/index.md'),
"empty-state-svg-path" => image_path('illustrations/pipelines_empty.svg'),
diff --git a/app/views/projects/services/_deprecated_message.html.haml b/app/views/projects/services/_deprecated_message.html.haml
deleted file mode 100644
index fea9506a4bb..00000000000
--- a/app/views/projects/services/_deprecated_message.html.haml
+++ /dev/null
@@ -1,3 +0,0 @@
-.flash-container.flash-container-page
- .flash-alert.deprecated-service
- %span= @service.deprecation_message
diff --git a/app/views/projects/services/_form.html.haml b/app/views/projects/services/_form.html.haml
index 0dbd6a48ec5..3f91bdc4266 100644
--- a/app/views/projects/services/_form.html.haml
+++ b/app/views/projects/services/_form.html.haml
@@ -1,19 +1,19 @@
.row.prepend-top-default.append-bottom-default
- .col-lg-3
+ .col-lg-4
%h4.prepend-top-0
= @service.title
- [true, false].each do |value|
- - hide_class = 'd-none' if @service.activated? != value
+ - hide_class = 'd-none' if @service.operating? != value
%span.js-service-active-status{ class: hide_class, data: { value: value.to_s } }
= boolean_to_icon value
- if @service.respond_to?(:detailed_description)
%p= @service.detailed_description
- .col-lg-9
+ .col-lg-8
= form_for(@service, as: :service, url: scoped_integration_path(@service), method: :put, html: { class: 'gl-show-field-errors integration-settings-form js-integration-settings-form', data: { 'can-test' => @service.can_test?, 'test-url' => test_project_service_path(@project, @service) } }) do |form|
= render 'shared/service_settings', form: form, service: @service
.footer-block.row-content-block
- = service_save_button(@service)
+ = service_save_button
&nbsp;
= link_to _('Cancel'), project_settings_integrations_path(@project), class: 'btn btn-cancel'
diff --git a/app/views/projects/services/_index.html.haml b/app/views/projects/services/_index.html.haml
deleted file mode 100644
index dca324ac846..00000000000
--- a/app/views/projects/services/_index.html.haml
+++ /dev/null
@@ -1,30 +0,0 @@
-.row.prepend-top-default
- .col-lg-4
- %h4.prepend-top-0
- = _('Integrations')
- %p= _('Integrations allow you to integrate GitLab with other applications')
- .col-lg-8
- %table.table
- %colgroup
- %col
- %col
- %col
- %col{ width: "120" }
- %thead
- %tr
- %th
- %th= _('Integration')
- %th.d-none.d-sm-block= _("Description")
- %th= s_("ProjectService|Last edit")
- - @services.sort_by(&:title).each do |service|
- %tr
- %td{ "aria-label" => (service.activated? ? s_("ProjectService|%{service_title}: status on") : s_("ProjectService|%{service_title}: status off")) % { service_title: service.title } }
- = boolean_to_icon service.activated?
- %td
- = link_to edit_project_service_path(@project, service.to_param), { data: { qa_selector: "#{service.title.downcase.gsub(/[\s\(\)]/,'_')}_link" } } do
- %strong= service.title
- %td.d-none.d-sm-block
- = service.description
- %td.light
- - if service.updated_at.present?
- = time_ago_with_tooltip service.updated_at
diff --git a/app/views/projects/services/edit.html.haml b/app/views/projects/services/edit.html.haml
index 4195dce7780..1aaea50c8d5 100644
--- a/app/views/projects/services/edit.html.haml
+++ b/app/views/projects/services/edit.html.haml
@@ -1,8 +1,7 @@
- breadcrumb_title @service.title
- add_to_breadcrumbs _('Integration Settings'), project_settings_integrations_path(@project)
- page_title @service.title, _('Integrations')
-
-= render 'deprecated_message' if @service.deprecation_message
+- @content_class = 'limit-container-width' unless fluid_layout
= render 'form'
- if @web_hook_logs
diff --git a/app/views/projects/services/prometheus/_custom_metrics.html.haml b/app/views/projects/services/prometheus/_custom_metrics.html.haml
index 21f9d1125e0..210d0f37d65 100644
--- a/app/views/projects/services/prometheus/_custom_metrics.html.haml
+++ b/app/views/projects/services/prometheus/_custom_metrics.html.haml
@@ -6,14 +6,14 @@
= link_to s_('PrometheusService|More information'), help_page_path('user/project/integrations/prometheus', anchor: 'adding-custom-metrics'), target: '_blank', rel: "noopener noreferrer"
.col-lg-9
- .card.custom-monitored-metrics.js-panel-custom-monitored-metrics{ data: { active_custom_metrics: project_prometheus_metrics_path(project), environments_data: environments_list_data, service_active: "#{@service.active}" } }
+ .card.custom-monitored-metrics.js-panel-custom-monitored-metrics{ data: { qa_selector: 'custom_metrics_container', active_custom_metrics: project_prometheus_metrics_path(project), environments_data: environments_list_data, service_active: "#{@service.active}" } }
.card-header
%strong
= s_('PrometheusService|Custom metrics')
-# haml-lint:disable NoPlainNodes
%span.badge.badge-pill.js-custom-monitored-count 0
-# haml-lint:enable NoPlainNodes
- = link_to s_('PrometheusService|New metric'), new_project_prometheus_metric_path(project), class: 'btn btn-success js-new-metric-button hidden'
+ = link_to s_('PrometheusService|New metric'), new_project_prometheus_metric_path(project), class: 'btn btn-success js-new-metric-button hidden', data: { qa_selector: 'new_metric_button' }
.card-body
.flash-container.hidden
.flash-warning
diff --git a/app/views/projects/services/slack_slash_commands/_help.html.haml b/app/views/projects/services/slack_slash_commands/_help.html.haml
index 93ea17a3a3d..0cf78d4f681 100644
--- a/app/views/projects/services/slack_slash_commands/_help.html.haml
+++ b/app/views/projects/services/slack_slash_commands/_help.html.haml
@@ -57,7 +57,7 @@
.form-group
= label_tag nil, _('Customize icon'), class: 'col-12 col-form-label label-bold'
.col-12
- = image_tag(asset_url('slash-command-logo.png'), width: 36, height: 36, class: 'mr-3')
+ = image_tag(asset_url('slash-command-logo.png', skip_pipeline: true), width: 36, height: 36, class: 'mr-3')
= link_to(_('Download image'), asset_url('gitlab_logo.png'), class: 'btn btn-sm', target: '_blank', rel: 'noopener noreferrer')
.form-group
diff --git a/app/views/projects/settings/_general.html.haml b/app/views/projects/settings/_general.html.haml
index 0f60fc18026..5eeebe4160f 100644
--- a/app/views/projects/settings/_general.html.haml
+++ b/app/views/projects/settings/_general.html.haml
@@ -16,7 +16,7 @@
.row
.form-group.col-md-9
- = f.label :tag_list, _('Topics'), class: 'label-bold'
+ = f.label :tag_list, _('Topics (optional)'), class: 'label-bold'
= f.text_field :tag_list, value: @project.tag_list.join(', '), maxlength: 2000, class: "form-control"
%p.form-text.text-muted= _('Separate topics with commas.')
diff --git a/app/views/projects/settings/access_tokens/index.html.haml b/app/views/projects/settings/access_tokens/index.html.haml
new file mode 100644
index 00000000000..07784dce677
--- /dev/null
+++ b/app/views/projects/settings/access_tokens/index.html.haml
@@ -0,0 +1,34 @@
+- breadcrumb_title s_('AccessTokens|Access Tokens')
+- page_title _('Project Access Tokens')
+- type = _('project access token')
+- type_plural = _('project access tokens')
+- @content_class = 'limit-container-width' unless fluid_layout
+
+.row.prepend-top-default
+ .col-lg-4.profile-settings-sidebar
+ %h4.prepend-top-0
+ = page_title
+ %p
+ = _('You can generate an access token scoped to this project for each application to use the GitLab API.')
+ %p
+ = _('You can also use project access tokens to authenticate against Git over HTTP.')
+
+ .col-lg-8
+ - if @new_project_access_token
+ = render 'shared/access_tokens/created_container',
+ type: type,
+ new_token_value: @new_project_access_token
+
+ = render 'shared/access_tokens/form',
+ type: type,
+ path: project_settings_access_tokens_path(@project),
+ token: @project_access_token,
+ scopes: @scopes,
+ prefix: :project_access_token
+
+ = render 'shared/access_tokens/table',
+ active_tokens: @active_project_access_tokens,
+ type: type,
+ type_plural: type_plural,
+ revoke_route_helper: ->(token) { revoke_namespace_project_settings_access_token_path(id: token) },
+ no_active_tokens_message: _('This project has no active access tokens.')
diff --git a/app/views/projects/settings/ci_cd/_autodevops_form.html.haml b/app/views/projects/settings/ci_cd/_autodevops_form.html.haml
index 6702786fdb3..8b84acb67c1 100644
--- a/app/views/projects/settings/ci_cd/_autodevops_form.html.haml
+++ b/app/views/projects/settings/ci_cd/_autodevops_form.html.haml
@@ -30,7 +30,7 @@
.card-footer.js-extra-settings{ class: auto_devops_enabled || 'hidden' }
- if @project.all_clusters.empty?
%p.settings-message.text-center
- = s_('CICD|You must add a %{kubernetes_cluster_link_start}Kubernetes cluster integration%{link_end} to this project with a domain in order for your deployment strategy to work correctly.').html_safe % { kubernetes_cluster_link_start: kubernetes_cluster_link_start, link_end: link_end }
+ = s_('CICD|Add a %{kubernetes_cluster_link_start}Kubernetes cluster integration%{link_end} with a domain or create an AUTO_DEVOPS_PLATFORM_TARGET CI variable.').html_safe % { kubernetes_cluster_link_start: kubernetes_cluster_link_start, link_end: link_end }
- elsif !has_base_domain
%p.settings-message.text-center
= s_('CICD|You must add a %{base_domain_link_start}base domain%{link_end} to your %{kubernetes_cluster_link_start}Kubernetes cluster%{link_end} in order for your deployment strategy to work.').html_safe % { base_domain_link_start: base_domain_link_start, kubernetes_cluster_link_start: kubernetes_cluster_link_start, link_end: link_end }
diff --git a/app/views/projects/settings/ci_cd/_form.html.haml b/app/views/projects/settings/ci_cd/_form.html.haml
index 4040b1094aa..b50f712922f 100644
--- a/app/views/projects/settings/ci_cd/_form.html.haml
+++ b/app/views/projects/settings/ci_cd/_form.html.haml
@@ -103,7 +103,7 @@
.input-group
%span.input-group-prepend
.input-group-text /
- = f.text_field :build_coverage_regex, class: 'form-control', placeholder: 'Regular expression'
+ = f.text_field :build_coverage_regex, class: 'form-control', placeholder: 'Regular expression', data: { qa_selector: 'build_coverage_regex_field' }
%span.input-group-append
.input-group-text /
%p.form-text.text-muted
@@ -143,7 +143,7 @@
go test -cover (Go)
%code coverage: \d+.\d+% of statements
- = f.submit _('Save changes'), class: "btn btn-success"
+ = f.submit _('Save changes'), class: "btn btn-success", data: { qa_selector: 'save_general_pipelines_changes_button' }
%hr
diff --git a/app/views/projects/settings/ci_cd/show.html.haml b/app/views/projects/settings/ci_cd/show.html.haml
index 1358077f2b2..4e14426a069 100644
--- a/app/views/projects/settings/ci_cd/show.html.haml
+++ b/app/views/projects/settings/ci_cd/show.html.haml
@@ -5,7 +5,7 @@
- expanded = expanded_by_default?
- general_expanded = @project.errors.empty? ? expanded : true
-%section.settings#js-general-pipeline-settings.no-animate{ class: ('expanded' if general_expanded) }
+%section.settings#js-general-pipeline-settings.no-animate{ class: ('expanded' if general_expanded), data: { qa_selector: 'general_pipelines_settings_content' } }
.settings-header
%h4
= _("General pipelines")
diff --git a/app/views/projects/settings/integrations/show.html.haml b/app/views/projects/settings/integrations/show.html.haml
index f603f23a2c7..4372763fcf7 100644
--- a/app/views/projects/settings/integrations/show.html.haml
+++ b/app/views/projects/settings/integrations/show.html.haml
@@ -12,4 +12,6 @@
.gl-alert-actions
= link_to _('Go to Webhooks'), project_hooks_path(@project), class: 'btn gl-alert-action btn-info new-gl-button'
-= render 'projects/services/index'
+%h4= s_('Integrations')
+%p= s_('Integrations allow you to integrate GitLab with other applications')
+= render 'shared/integrations/index', integrations: @services
diff --git a/app/views/projects/settings/operations/_incidents.html.haml b/app/views/projects/settings/operations/_incidents.html.haml
index a96a41b78c2..92fffa42b73 100644
--- a/app/views/projects/settings/operations/_incidents.html.haml
+++ b/app/views/projects/settings/operations/_incidents.html.haml
@@ -2,7 +2,7 @@
- setting = project_incident_management_setting
- templates = setting.available_issue_templates.map { |t| [t.name, t.key] }
-%section.settings.no-animate.qa-incident-management-settings
+%section.settings.no-animate.qa-incident-management-settings{ data: { qa_selector: 'incidents_settings_content' } }
.settings-header
%h3{ :class => "h4" }= _('Incidents')
%button.btn.js-settings-toggle{ type: 'button' }
@@ -17,16 +17,16 @@
.form-group
= f.fields_for :incident_management_setting_attributes, setting do |form|
.form-group
- = form.check_box :create_issue
+ = form.check_box :create_issue, data: { qa_selector: 'create_issue_checkbox' }
= form.label :create_issue, _('Create an issue. Issues are created for each alert triggered.'), class: 'form-check-label'
.form-group.col-sm-8
= form.label :issue_template_key, class: 'label-bold' do
= _('Issue template (optional)')
= link_to icon('question-circle'), help_page_path('user/project/description_templates', anchor: 'creating-issue-templates'), target: '_blank', rel: 'noopener noreferrer'
.select-wrapper
- = form.select :issue_template_key, templates, {include_blank: 'No template selected'}, class: "form-control select-control"
+ = form.select :issue_template_key, templates, {include_blank: 'No template selected'}, class: "form-control select-control", data: { qa_selector: 'incident_templates_dropdown' }
= icon('chevron-down')
.form-group
= form.check_box :send_email
= form.label :send_email, _('Send a separate email notification to Developers.'), class: 'form-check-label'
- = f.submit _('Save changes'), class: 'btn btn-success'
+ = f.submit _('Save changes'), class: 'btn btn-success', data: { qa_selector: 'save_changes_button' }
diff --git a/app/views/projects/settings/repository/show.html.haml b/app/views/projects/settings/repository/show.html.haml
index 193053c8c97..24fc137fd29 100644
--- a/app/views/projects/settings/repository/show.html.haml
+++ b/app/views/projects/settings/repository/show.html.haml
@@ -1,7 +1,7 @@
- breadcrumb_title _("Repository Settings")
- page_title _("Repository")
- @content_class = "limit-container-width" unless fluid_layout
-- deploy_token_description = s_('DeployTokens|Deploy tokens allow access to your repository and registry images.')
+- deploy_token_description = s_('DeployTokens|Deploy tokens allow access to packages, your repository, and registry images.')
= render "projects/default_branch/show"
= render_if_exists "projects/push_rules/index"
diff --git a/app/views/projects/snippets/show.html.haml b/app/views/projects/snippets/show.html.haml
index ccf109968fc..7cf5de8947c 100644
--- a/app/views/projects/snippets/show.html.haml
+++ b/app/views/projects/snippets/show.html.haml
@@ -3,7 +3,7 @@
- breadcrumb_title @snippet.to_reference
- page_title "#{@snippet.title} (#{@snippet.to_reference})", _("Snippets")
-- if Feature.enabled?(:snippets_vue)
+- if Feature.enabled?(:snippets_vue, default_enabled: true)
#js-snippet-view{ data: {'qa-selector': 'snippet_view', 'snippet-gid': @snippet.to_global_id} }
- else
= render 'shared/snippets/header'
diff --git a/app/views/projects/tags/_tag.html.haml b/app/views/projects/tags/_tag.html.haml
index 75805192a61..da693a15ec2 100644
--- a/app/views/projects/tags/_tag.html.haml
+++ b/app/views/projects/tags/_tag.html.haml
@@ -30,6 +30,9 @@
= markdown_field(release, :description)
.row-fixed-content.controls.flex-row
+ - if tag.has_signature?
+ = render partial: 'projects/commit/signature', object: tag.signature
+
= render 'projects/buttons/download', project: @project, ref: tag.name, pipeline: @tags_pipelines[tag.name]
- if can?(current_user, :admin_tag, @project)
diff --git a/app/views/projects/tags/show.html.haml b/app/views/projects/tags/show.html.haml
index 8086d47479d..6f53a687fb9 100644
--- a/app/views/projects/tags/show.html.haml
+++ b/app/views/projects/tags/show.html.haml
@@ -39,6 +39,8 @@
= s_("TagsPage|Can't find HEAD commit for this tag")
.nav-controls
+ - if @tag.has_signature?
+ = render partial: 'projects/commit/signature', object: @tag.signature
- if can?(current_user, :admin_tag, @project)
= link_to edit_project_tag_release_path(@project, @tag.name), class: 'btn btn-edit controls-item has-tooltip', title: s_('TagsPage|Edit release notes') do
= icon("pencil")
diff --git a/app/views/projects/wikis/_sidebar.html.haml b/app/views/projects/wikis/_sidebar.html.haml
index 0f2938686cc..2b8da83b126 100644
--- a/app/views/projects/wikis/_sidebar.html.haml
+++ b/app/views/projects/wikis/_sidebar.html.haml
@@ -17,6 +17,6 @@
%ul.wiki-pages
= render @sidebar_wiki_entries, context: 'sidebar'
.block.w-100
- - if @sidebar_wiki_entries&.length.to_i >= 15
+ - if @sidebar_limited
= link_to project_wikis_pages_path(@project), class: 'btn btn-block' do
= s_("Wiki|View All Pages")
diff --git a/app/views/projects/wikis/git_access.html.haml b/app/views/projects/wikis/git_access.html.haml
index 6972eda9bb7..72c9f45779a 100644
--- a/app/views/projects/wikis/git_access.html.haml
+++ b/app/views/projects/wikis/git_access.html.haml
@@ -18,11 +18,6 @@
%pre.dark
:preserve
gem install gollum
- %p
- = (s_("WikiClone|It is recommended to install %{markdown} so that GFM features render locally:") % { markdown: "<code>github-markdown</code>" }).html_safe
- %pre.dark
- :preserve
- gem install github-markdown
%h3= s_("WikiClone|Clone your wiki")
%pre.dark
diff --git a/app/views/search/_category.html.haml b/app/views/search/_category.html.haml
index 6ad155eb715..db7769fa743 100644
--- a/app/views/search/_category.html.haml
+++ b/app/views/search/_category.html.haml
@@ -24,7 +24,7 @@
= users
- elsif @show_snippets
- = search_filter_link 'snippet_titles', _("Titles and Filenames"), search: { snippets: true, group_id: nil, project_id: nil }
+ = search_filter_link 'snippet_titles', _("Titles and Descriptions"), search: { snippets: true, group_id: nil, project_id: nil }
- else
= search_filter_link 'projects', _("Projects"), data: { qa_selector: 'projects_tab' }
= search_filter_link 'issues', _("Issues")
diff --git a/app/views/search/results/_blob_data.html.haml b/app/views/search/results/_blob_data.html.haml
index 01e42224428..218de30d707 100644
--- a/app/views/search/results/_blob_data.html.haml
+++ b/app/views/search/results/_blob_data.html.haml
@@ -7,4 +7,4 @@
= search_blob_title(project, path)
- if blob.data
.file-content.code.term{ data: { qa_selector: 'file_text_content' } }
- = render 'shared/file_highlight', blob: blob, first_line_number: blob.startline
+ = render 'shared/file_highlight', blob: blob, first_line_number: blob.startline, blob_link: blob_link
diff --git a/app/views/search/results/_snippet_blob.html.haml b/app/views/search/results/_snippet_blob.html.haml
deleted file mode 100644
index fa77566dddb..00000000000
--- a/app/views/search/results/_snippet_blob.html.haml
+++ /dev/null
@@ -1,50 +0,0 @@
-- snippet_blob = chunk_snippet(snippet_blob, @search_term)
-- snippet = snippet_blob[:snippet_object]
-- snippet_chunks = snippet_blob[:snippet_chunks]
-- snippet_path = gitlab_snippet_path(snippet)
-
-.search-result-row.snippet-row
- = image_tag avatar_icon_for_user(snippet.author), class: "avatar s40 d-none d-sm-block", alt: ''
- .title
- = link_to gitlab_snippet_path(snippet) do
- = snippet.title
- .snippet-info
- = snippet.to_reference
- &middot;
- authored
- = time_ago_with_tooltip(snippet.created_at)
- by
- = link_to user_snippets_path(snippet.author) do
- = snippet.author_name
-
- .file-holder.my-2
- .js-file-title.file-title-flex-parent
- = link_to snippet_path do
- %i.fa.fa-file
- %strong= snippet.file_name
- - if markup?(snippet.file_name)
- .file-content.md
- - snippet_chunks.each do |chunk|
- - unless chunk[:data].empty?
- = markup(snippet.file_name, chunk[:data])
- - else
- .file-content.code
- .nothing-here-block= _("Empty file")
- - else
- .file-content.code.js-syntax-highlight
- .line-numbers
- - snippet_chunks.each do |chunk|
- - unless chunk[:data].empty?
- - Gitlab::Git::Util.count_lines(chunk[:data]).times do |index|
- - offset = defined?(chunk[:start_line]) ? chunk[:start_line] : 1
- - i = index + offset
- = link_to snippet_path+"#L#{i}", id: "L#{i}", rel: "#L#{i}", class: "diff-line-num" do
- %i.fa.fa-link
- = i
- .blob-content
- - snippet_chunks.each do |chunk|
- - unless chunk[:data].empty?
- = highlight(snippet.file_name, chunk[:data])
- - else
- .file-content.code
- .nothing-here-block= _("Empty file")
diff --git a/app/views/search/results/_snippet_title.html.haml b/app/views/search/results/_snippet_title.html.haml
index 81e746c55a3..a28d9effbdd 100644
--- a/app/views/search/results/_snippet_title.html.haml
+++ b/app/views/search/results/_snippet_title.html.haml
@@ -1,5 +1,5 @@
.search-result-row
- %h4.snippet-title.term
+ %h4
= link_to gitlab_snippet_path(snippet_title) do
= truncate(snippet_title.title, length: 60)
= snippet_badge(snippet_title)
diff --git a/app/views/shared/_auto_devops_callout.html.haml b/app/views/shared/_auto_devops_callout.html.haml
index 128508e954e..bf1683be32d 100644
--- a/app/views/shared/_auto_devops_callout.html.haml
+++ b/app/views/shared/_auto_devops_callout.html.haml
@@ -11,5 +11,5 @@
= link_to s_('AutoDevOps|Enable in settings'), project_settings_ci_cd_path(@project, anchor: 'autodevops-settings'), class: 'btn btn-md new-gl-button js-close-callout'
%button.gl-banner-close.close.js-close-callout{ type: 'button',
- 'aria-label' => 'Dismiss Auto DevOps box' }
+ 'aria-label' => s_('AutoDevOps|Dismiss Auto DevOps box') }
= icon('times', class: 'dismiss-icon', 'aria-hidden' => 'true')
diff --git a/app/views/shared/_broadcast_message.html.haml b/app/views/shared/_broadcast_message.html.haml
index bc4db672938..b809696cccb 100644
--- a/app/views/shared/_broadcast_message.html.haml
+++ b/app/views/shared/_broadcast_message.html.haml
@@ -6,5 +6,5 @@
= render_broadcast_message(message)
.flex-grow-1.text-right{ style: 'flex-basis: 0' }
- if (message.notification? || message.dismissable?) && opts[:preview].blank?
- %button.broadcast-message-dismiss.js-dismiss-current-broadcast-notification.btn.btn-link.pl-2.pr-2{ 'aria-label' => _('Close'), :type => 'button', data: { id: message.id } }
+ %button.broadcast-message-dismiss.js-dismiss-current-broadcast-notification.btn.btn-link.pl-2.pr-2{ 'aria-label' => _('Close'), :type => 'button', data: { id: message.id, expire_date: message.ends_at.iso8601 } }
%i.fa.fa-times
diff --git a/app/views/shared/_clone_panel.html.haml b/app/views/shared/_clone_panel.html.haml
index 3e805189055..9ec8d3c18cd 100644
--- a/app/views/shared/_clone_panel.html.haml
+++ b/app/views/shared/_clone_panel.html.haml
@@ -18,7 +18,7 @@
= http_clone_button(project)
= render_if_exists 'shared/kerberos_clone_button', project: project
- = text_field_tag :project_clone, default_url_to_repo(project), class: "js-select-on-focus form-control", readonly: true, aria: { label: 'Project clone URL' }
+ = text_field_tag :project_clone, default_url_to_repo(project), class: "js-select-on-focus form-control", readonly: true, aria: { label: _('Project clone URL') }
.input-group-append
= clipboard_button(target: '#project_clone', title: _("Copy URL"), class: "input-group-text btn-default btn-clipboard")
diff --git a/app/views/shared/_commit_message_container.html.haml b/app/views/shared/_commit_message_container.html.haml
index 68c14c307ac..d65b7492690 100644
--- a/app/views/shared/_commit_message_container.html.haml
+++ b/app/views/shared/_commit_message_container.html.haml
@@ -14,12 +14,11 @@
id: "commit_message-#{nonce}"
- if local_assigns[:hint]
%p.hint
- Try to keep the first line under 52 characters
- and the others under 72.
+ = _('Try to keep the first line under 52 characters and the others under 72.')
- if descriptions.present?
.hint.js-with-description-hint
= link_to "#", class: "js-with-description-link" do
- Include description in commit message
+ = _('Include description in commit message')
.hint.js-without-description-hint.hide
= link_to "#", class: "js-without-description-link" do
- Don't include description in commit message
+ = _("Don't include description in commit message")
diff --git a/app/views/shared/_delete_label_modal.html.haml b/app/views/shared/_delete_label_modal.html.haml
index c6629cd33a5..25c841d2344 100644
--- a/app/views/shared/_delete_label_modal.html.haml
+++ b/app/views/shared/_delete_label_modal.html.haml
@@ -2,20 +2,19 @@
.modal-dialog
.modal-content
.modal-header
- %h3.page-title Delete label: #{label.name} ?
+ %h3.page-title= _('Delete label: %{label_name} ?') % { label_name: label.name }
%button.close{ type: "button", "data-dismiss": "modal", "aria-label" => _('Close') }
%span{ "aria-hidden": true } &times;
.modal-body
%p
- %strong= label.name
- %span will be permanently deleted from #{label.subject_name}. This cannot be undone.
+ = _('<strong>%{label_name}</strong> <span>will be permanently deleted from %{subject_name}. This cannot be undone.</span>').html_safe % { label_name: label.name, subject_name: label.subject_name }
.modal-footer
- %a{ href: '#', data: { dismiss: 'modal' }, class: 'btn btn-default' } Cancel
+ %a{ href: '#', data: { dismiss: 'modal' }, class: 'btn btn-default' }= _('Cancel')
- = link_to 'Delete label',
+ = link_to _('Delete label'),
label.destroy_path,
- title: 'Delete',
+ title: _('Delete'),
method: :delete,
class: 'btn btn-remove'
diff --git a/app/views/shared/_field.html.haml b/app/views/shared/_field.html.haml
index a7ad6d6f2c4..4f416c483f2 100644
--- a/app/views/shared/_field.html.haml
+++ b/app/views/shared/_field.html.haml
@@ -7,23 +7,22 @@
- choices = field[:choices]
- default_choice = field[:default_choice]
- help = field[:help]
-- disabled = disable_fields_service?(@service)
.form-group.row
- if type == "password" && value.present?
- = form.label name, "Enter new #{title.downcase}", class: "col-form-label col-sm-2"
+ = form.label name, _("Enter new %{field_title}") % { field_title: title.downcase }, class: "col-form-label col-sm-2"
- else
= form.label name, title, class: "col-form-label col-sm-2"
.col-sm-10
- if type == 'text'
- = form.text_field name, class: "form-control", placeholder: placeholder, required: required, disabled: disabled, data: { qa_selector: "#{name.downcase.gsub('\s', '')}_field" }
+ = form.text_field name, class: "form-control", placeholder: placeholder, required: required, data: { qa_selector: "#{name.downcase.gsub('\s', '')}_field" }
- elsif type == 'textarea'
- = form.text_area name, rows: 5, class: "form-control", placeholder: placeholder, required: required, disabled: disabled
+ = form.text_area name, rows: 5, class: "form-control", placeholder: placeholder, required: required
- elsif type == 'checkbox'
- = form.check_box name, disabled: disabled
+ = form.check_box name
- elsif type == 'select'
- = form.select name, options_for_select(choices, value ? value : default_choice), {}, { class: "form-control", disabled: disabled}
+ = form.select name, options_for_select(choices, value ? value : default_choice), {}, { class: "form-control"}
- elsif type == 'password'
- = form.password_field name, autocomplete: "new-password", placeholder: placeholder, class: "form-control", required: value.blank? && required, disabled: disabled, data: { qa_selector: "#{name.downcase.gsub('\s', '')}_field" }
+ = form.password_field name, autocomplete: "new-password", placeholder: placeholder, class: "form-control", required: value.blank? && required, data: { qa_selector: "#{name.downcase.gsub('\s', '')}_field" }
- if help
%span.form-text.text-muted= help
diff --git a/app/views/shared/_group_form.html.haml b/app/views/shared/_group_form.html.haml
index 9a65981ed58..019b2ef89a4 100644
--- a/app/views/shared/_group_form.html.haml
+++ b/app/views/shared/_group_form.html.haml
@@ -6,7 +6,7 @@
.form-group.group-name-holder.col-sm-12
= f.label :name, class: 'label-bold' do
= _("Group name")
- = f.text_field :name, placeholder: 'My Awesome Group', class: 'form-control input-lg',
+ = f.text_field :name, placeholder: _('My Awesome Group'), class: 'form-control input-lg',
required: true,
title: _('Please fill in a descriptive name for your group.'),
autofocus: true
@@ -22,7 +22,7 @@
- if parent
%strong= parent.full_path + '/'
= f.hidden_field :parent_id
- = f.text_field :path, placeholder: 'my-awesome-group', class: 'form-control js-validate-group-path',
+ = f.text_field :path, placeholder: _('my-awesome-group'), class: 'form-control js-validate-group-path',
autofocus: local_assigns[:autofocus] || false, required: true,
pattern: Gitlab::PathRegex::NAMESPACE_FORMAT_REGEX_JS,
title: _('Please choose a group URL with no special characters.'),
diff --git a/app/views/shared/_group_tips.html.haml b/app/views/shared/_group_tips.html.haml
index 46e4340511a..2d7f8e36139 100644
--- a/app/views/shared/_group_tips.html.haml
+++ b/app/views/shared/_group_tips.html.haml
@@ -1,5 +1,5 @@
%ul
- %li A group is a collection of several projects
- %li Members of a group may only view projects they have permission to access
- %li Group project URLs are prefixed with the group namespace
- %li Existing projects may be moved into a group
+ %li= _('A group is a collection of several projects')
+ %li= _('Members of a group may only view projects they have permission to access')
+ %li= _('Group project URLs are prefixed with the group namespace')
+ %li= _('Existing projects may be moved into a group')
diff --git a/app/views/shared/_label_row.html.haml b/app/views/shared/_label_row.html.haml
index b05d903fabe..cd303dd7a3d 100644
--- a/app/views/shared/_label_row.html.haml
+++ b/app/views/shared/_label_row.html.haml
@@ -13,7 +13,7 @@
%ul.label-links
- if show_label_issues_link
%li.label-link-item.inline
- = link_to_label(label) { 'Issues' }
+ = link_to_label(label) { _('Issues') }
- if show_label_merge_requests_link
&middot;
%li.label-link-item.inline
diff --git a/app/views/shared/_milestone_expired.html.haml b/app/views/shared/_milestone_expired.html.haml
index 099e3ac8462..48a97a18ca9 100644
--- a/app/views/shared/_milestone_expired.html.haml
+++ b/app/views/shared/_milestone_expired.html.haml
@@ -1,6 +1,6 @@
- if milestone.expired? and not milestone.closed?
- .status-box.status-box-expired.append-bottom-5 Expired
+ .status-box.status-box-expired.append-bottom-5= _('Expired')
- if milestone.upcoming?
- .status-box.status-box-mr-merged.append-bottom-5 Upcoming
+ .status-box.status-box-mr-merged.append-bottom-5= _('Upcoming')
- if milestone.closed?
- .status-box.status-box-closed.append-bottom-5 Closed
+ .status-box.status-box-closed.append-bottom-5= _('Closed')
diff --git a/app/views/shared/_milestones_filter.html.haml b/app/views/shared/_milestones_filter.html.haml
index 6c1ac20d544..eb50960202a 100644
--- a/app/views/shared/_milestones_filter.html.haml
+++ b/app/views/shared/_milestones_filter.html.haml
@@ -1,13 +1,13 @@
%ul.nav-links.mobile-separator.nav.nav-tabs
%li{ class: milestone_class_for_state(params[:state], 'opened', true) }>
= link_to milestones_filter_path(state: 'opened') do
- Open
+ = _('Open')
%span.badge.badge-pill= counts[:opened]
%li{ class: milestone_class_for_state(params[:state], 'closed') }>
= link_to milestones_filter_path(state: 'closed', sort: 'due_date_desc') do
- Closed
+ = _('Closed')
%span.badge.badge-pill= counts[:closed]
%li{ class: milestone_class_for_state(params[:state], 'all') }>
= link_to milestones_filter_path(state: 'all', sort: 'due_date_desc') do
- All
+ = _('All')
%span.badge.badge-pill= counts[:all]
diff --git a/app/views/shared/_mini_pipeline_graph.html.haml b/app/views/shared/_mini_pipeline_graph.html.haml
index a1f21c2a83e..172f3d85472 100644
--- a/app/views/shared/_mini_pipeline_graph.html.haml
+++ b/app/views/shared/_mini_pipeline_graph.html.haml
@@ -14,4 +14,4 @@
%li.js-builds-dropdown-loading.hidden
.loading-container.text-center
- %span.spinner{ 'aria-label': 'Loading' }
+ %span.spinner{ 'aria-label': _('Loading') }
diff --git a/app/views/shared/_no_ssh.html.haml b/app/views/shared/_no_ssh.html.haml
index fbfd4d0e9a9..2b04e3e1c98 100644
--- a/app/views/shared/_no_ssh.html.haml
+++ b/app/views/shared/_no_ssh.html.haml
@@ -1,7 +1,7 @@
- if show_no_ssh_key_message?
%div{ class: 'no-ssh-key-message gl-alert gl-alert-warning', role: 'alert' }
= sprite_icon('warning', size: 16, css_class: 'gl-icon s16 gl-alert-icon gl-alert-icon-no-title')
- %button{ class: 'gl-alert-dismiss hide-no-ssh-message', type: 'button', 'aria-label': 'Dismiss' }
+ %button{ class: 'gl-alert-dismiss hide-no-ssh-message', type: 'button', 'aria-label': _('Dismiss') }
= sprite_icon('close', size: 16, css_class: 'gl-icon s16')
.gl-alert-body
= s_("MissingSSHKeyWarningLink|You won't be able to pull or push project code via SSH until you add an SSH key to your profile").html_safe
diff --git a/app/views/shared/_personal_access_tokens_created_container.html.haml b/app/views/shared/_personal_access_tokens_created_container.html.haml
deleted file mode 100644
index df4577e2862..00000000000
--- a/app/views/shared/_personal_access_tokens_created_container.html.haml
+++ /dev/null
@@ -1,15 +0,0 @@
-- container_title = local_assigns.fetch(:container_title, _('Your New Personal Access Token'))
-- clipboard_button_title = local_assigns.fetch(:clipboard_button_title, _('Copy personal access token'))
-
-.created-personal-access-token-container
- %h5.prepend-top-0
- = container_title
- .form-group
- .input-group
- = text_field_tag 'created-personal-access-token', new_token_value, readonly: true, class: "qa-created-personal-access-token form-control js-select-on-focus", 'aria-describedby' => "created-token-help-block"
- %span.input-group-append
- = clipboard_button(text: new_token_value, title: clipboard_button_title, placement: "left", class: "input-group-text btn-default btn-clipboard")
- %span#created-token-help-block.form-text.text-muted.text-danger
- = _("Make sure you save it - you won't be able to access it again.")
-
-%hr
diff --git a/app/views/shared/_personal_access_tokens_form.html.haml b/app/views/shared/_personal_access_tokens_form.html.haml
deleted file mode 100644
index 71f3447ebc7..00000000000
--- a/app/views/shared/_personal_access_tokens_form.html.haml
+++ /dev/null
@@ -1,31 +0,0 @@
-- type = impersonation ? s_('Profiles|impersonation') : s_('Profiles|personal access')
-
-%h5.prepend-top-0
- = _('Add a %{type} token') % { type: type }
-%p.profile-settings-content
- = _("Pick a name for the application, and we'll give you a unique %{type} token.") % { type: type }
-
-= form_for token, url: path, method: :post, html: { class: 'js-requires-input' } do |f|
-
- = form_errors(token)
-
- .row
- .form-group.col-md-6
- = f.label :name, _('Name'), class: 'label-bold'
- = f.text_field :name, class: "form-control", required: true, data: { qa_selector: 'personal_access_token_name_field' }
-
- .row
- .form-group.col-md-6
- = f.label :expires_at, _('Expires at'), class: 'label-bold'
- .input-icon-wrapper
-
- = render_if_exists 'personal_access_tokens/callout_max_personal_access_token_lifetime'
-
- = f.text_field :expires_at, class: "datepicker form-control", placeholder: 'YYYY-MM-DD', data: { qa_selector: 'expiry_date_field' }
-
- .form-group
- = f.label :scopes, _('Scopes'), class: 'label-bold'
- = render 'shared/tokens/scopes_form', prefix: 'personal_access_token', token: token, scopes: scopes
-
- .prepend-top-default
- = f.submit _('Create %{type} token') % { type: type }, class: "btn btn-success", data: { qa_selector: 'create_token_button' }
diff --git a/app/views/shared/_personal_access_tokens_table.html.haml b/app/views/shared/_personal_access_tokens_table.html.haml
deleted file mode 100644
index 823117f37ca..00000000000
--- a/app/views/shared/_personal_access_tokens_table.html.haml
+++ /dev/null
@@ -1,36 +0,0 @@
-- type = impersonation ? s_('Profiles|Impersonation') : s_('Profiles|Personal Access')
-%hr
-
-%h5
- = _('Active %{type} Tokens (%{token_length})') % { type: type, token_length: active_tokens.length }
-- if impersonation
- %p.profile-settings-content
- = _("To see all the user's personal access tokens you must impersonate them first.")
-
-- if active_tokens.present?
- .table-responsive
- %table.table.active-tokens
- %thead
- %tr
- %th= _('Name')
- %th= s_('AccessTokens|Created')
- %th= _('Expires')
- %th= _('Scopes')
- %th
- %tbody
- - active_tokens.each do |token|
- %tr
- %td= token.name
- %td= token.created_at.to_date.to_s(:medium)
- %td
- - if token.expires?
- %span{ class: ('text-warning' if token.expires_soon?) }
- In #{distance_of_time_in_words_to_now(token.expires_at)}
- - else
- %span.token-never-expires-label= _('Never')
- %td= token.scopes.present? ? token.scopes.join(", ") : _('<no scopes selected>')
- - path = impersonation ? revoke_admin_user_impersonation_token_path(token.user, token) : revoke_profile_personal_access_token_path(token)
- %td= link_to _('Revoke'), path, method: :put, class: "btn btn-danger float-right qa-revoke-button", data: { confirm: _('Are you sure you want to revoke this %{type} Token? This action cannot be undone.') % { type: type } }
-- else
- .settings-message.text-center
- = _('This user has no active %{type} Tokens.') % { type: type }
diff --git a/app/views/shared/_project_limit.html.haml b/app/views/shared/_project_limit.html.haml
index 2c52eccccb6..88f213612fc 100644
--- a/app/views/shared/_project_limit.html.haml
+++ b/app/views/shared/_project_limit.html.haml
@@ -1,8 +1,8 @@
- if cookies[:hide_project_limit_message].blank? && !current_user.hide_project_limit && !current_user.can_create_project? && current_user.projects_limit > 0
.project-limit-message.alert.alert-warning.d-none.d-sm-block
- You won't be able to create new projects because you have reached your project limit.
+ = _("You won't be able to create new projects because you have reached your project limit.")
.float-right
- = link_to "Don't show again", profile_path(user: {hide_project_limit: true}), method: :put, class: 'alert-link'
+ = link_to _("Don't show again"), profile_path(user: {hide_project_limit: true}), method: :put, class: 'alert-link'
|
- = link_to 'Remind later', '#', class: 'hide-project-limit-message alert-link'
+ = link_to _('Remind later'), '#', class: 'hide-project-limit-message alert-link'
diff --git a/app/views/shared/_recaptcha_form.html.haml b/app/views/shared/_recaptcha_form.html.haml
index 10f358402c1..245a86721eb 100644
--- a/app/views/shared/_recaptcha_form.html.haml
+++ b/app/views/shared/_recaptcha_form.html.haml
@@ -17,4 +17,4 @@
- if has_submit
.row-content-block.footer-block
- = f.submit "Submit #{humanized_resource_name}", class: 'btn btn-success'
+ = f.submit _("Submit %{humanized_resource_name}") % { humanized_resource_name: humanized_resource_name }, class: 'btn btn-success'
diff --git a/app/views/shared/_ref_dropdown.html.haml b/app/views/shared/_ref_dropdown.html.haml
index 8b2a3bee407..ee2b2a17e21 100644
--- a/app/views/shared/_ref_dropdown.html.haml
+++ b/app/views/shared/_ref_dropdown.html.haml
@@ -1,7 +1,7 @@
- dropdown_class = local_assigns.fetch(:dropdown_class, '')
.dropdown-menu.dropdown-menu-selectable.git-revision-dropdown{ class: dropdown_class }
- = dropdown_title "Select Git revision"
- = dropdown_filter "Filter by Git revision"
+ = dropdown_title _('Select Git revision')
+ = dropdown_filter _('Filter by Git revision')
= dropdown_content
= dropdown_loading
diff --git a/app/views/shared/_service_settings.html.haml b/app/views/shared/_service_settings.html.haml
index 3da4b77b5eb..a9203459914 100644
--- a/app/views/shared/_service_settings.html.haml
+++ b/app/views/shared/_service_settings.html.haml
@@ -1,4 +1,5 @@
= form_errors(@service)
+- trigger_events = Feature.enabled?(:integration_form_refactor) ? ServiceEventSerializer.new(service: @service).represent(@service.configurable_events).to_json : []
- if lookup_context.template_exists?('help', "projects/services/#{@service.to_param}", true)
= render "projects/services/#{@service.to_param}/help", subject: @service
@@ -8,9 +9,10 @@
= markdown @service.help
.service-settings
- .js-vue-integration-settings{ data: { show_active: @service.show_active_box?.to_s, activated: (@service.active || @service.new_record?).to_s, disabled: disable_fields_service?(@service).to_s } }
+ .js-vue-integration-settings{ data: { show_active: @service.show_active_box?.to_s, activated: (@service.active || @service.new_record?).to_s, type: @service.to_param, merge_request_events: @service.merge_requests_events.to_s,
+commit_events: @service.commit_events.to_s, enable_comments: @service.comment_on_event_enabled.to_s, comment_detail: @service.comment_detail, trigger_events: trigger_events } }
- - if @service.configurable_events.present?
+ - if @service.configurable_events.present? && !@service.is_a?(JiraService) && Feature.disabled?(:integration_form_refactor)
.form-group.row
%label.col-form-label.col-sm-2= _('Trigger')
@@ -31,32 +33,5 @@
%p.text-muted
= @service.class.event_description(event)
- - if @service.configurable_event_actions.present?
- .form-group.row
- %label.col-form-label.col-sm-2= _('Event Actions')
-
- .col-sm-10
- - @service.configurable_event_actions.each do |action|
- .form-group
- .form-check
- = form.check_box service_event_action_field_name(action), class: 'form-check-input'
- = form.label service_event_action_field_name(action), class: 'form-check-label' do
- %strong
- = event_action_description(action)
-
- %p.text-muted
- = event_action_description(action)
-
- @service.global_fields.each do |field|
- - type = field[:type]
-
- - if type == 'fieldset'
- - fields = field[:fields]
- - legend = field[:legend]
-
- %fieldset
- %legend= legend
- - fields.each do |subfield|
- = render 'shared/field', form: form, field: subfield
- - else
- = render 'shared/field', form: form, field: field
+ = render 'shared/field', form: form, field: field
diff --git a/app/views/shared/access_tokens/_created_container.html.haml b/app/views/shared/access_tokens/_created_container.html.haml
new file mode 100644
index 00000000000..f11ef1e01de
--- /dev/null
+++ b/app/views/shared/access_tokens/_created_container.html.haml
@@ -0,0 +1,12 @@
+.created-personal-access-token-container
+ %h5.prepend-top-0
+ = _('Your new %{type}') % { type: type }
+ .form-group
+ .input-group
+ = text_field_tag 'created-personal-access-token', new_token_value, readonly: true, class: 'qa-created-access-token form-control js-select-on-focus', 'aria-describedby' => 'created-token-help-block'
+ %span.input-group-append
+ = clipboard_button(text: new_token_value, title: _('Copy %{type}') % { type: type }, placement: 'left', class: 'input-group-text btn-default btn-clipboard')
+ %span#created-token-help-block.form-text.text-muted.text-danger
+ = _("Make sure you save it - you won't be able to access it again.")
+
+%hr
diff --git a/app/views/shared/access_tokens/_form.html.haml b/app/views/shared/access_tokens/_form.html.haml
new file mode 100644
index 00000000000..cb7f907308f
--- /dev/null
+++ b/app/views/shared/access_tokens/_form.html.haml
@@ -0,0 +1,34 @@
+- title = local_assigns.fetch(:title, _('Add a %{type}') % { type: type })
+- prefix = local_assigns.fetch(:prefix, :personal_access_token)
+
+%h5.prepend-top-0
+ = title
+%p.profile-settings-content
+ = _("Enter the name of your application, and we'll return a unique %{type}.") % { type: type }
+
+= form_for token, as: prefix, url: path, method: :post, html: { class: 'js-requires-input' } do |f|
+
+ = form_errors(token)
+
+ .row
+ .form-group.col-md-6
+ = f.label :name, _('Name'), class: 'label-bold'
+ = f.text_field :name, class: 'form-control', required: true, data: { qa_selector: 'access_token_name_field' }
+
+ .row
+ .form-group.col-md-6
+ = f.label :expires_at, _('Expires at'), class: 'label-bold'
+ .input-icon-wrapper
+
+ = render_if_exists 'personal_access_tokens/callout_max_personal_access_token_lifetime'
+
+ .js-access-tokens-expires-at
+ %expires-at-field
+ = f.text_field :expires_at, class: 'datepicker form-control gl-datepicker-input', placeholder: 'YYYY-MM-DD', autocomplete: 'off', inputmode: 'none', data: { qa_selector: 'expiry_date_field' }
+
+ .form-group
+ = f.label :scopes, _('Scopes'), class: 'label-bold'
+ = render 'shared/tokens/scopes_form', prefix: prefix, token: token, scopes: scopes
+
+ .prepend-top-default
+ = f.submit _('Create %{type}') % { type: type }, class: 'btn btn-success', data: { qa_selector: 'create_token_button' }
diff --git a/app/views/shared/access_tokens/_table.html.haml b/app/views/shared/access_tokens/_table.html.haml
new file mode 100644
index 00000000000..5518c31cb06
--- /dev/null
+++ b/app/views/shared/access_tokens/_table.html.haml
@@ -0,0 +1,37 @@
+- no_active_tokens_message = local_assigns.fetch(:no_active_tokens_message, _('This user has no active %{type}.') % { type: type_plural })
+- impersonation = local_assigns.fetch(:impersonation, false)
+
+%hr
+
+%h5
+ = _('Active %{type} (%{token_length})') % { type: type_plural, token_length: active_tokens.length }
+- if impersonation
+ %p.profile-settings-content
+ = _("To see all the user's personal access tokens you must impersonate them first.")
+
+- if active_tokens.present?
+ .table-responsive
+ %table.table.active-tokens
+ %thead
+ %tr
+ %th= _('Name')
+ %th= s_('AccessTokens|Created')
+ %th= _('Expires')
+ %th= _('Scopes')
+ %th
+ %tbody
+ - active_tokens.each do |token|
+ %tr
+ %td= token.name
+ %td= token.created_at.to_date.to_s(:medium)
+ %td
+ - if token.expires?
+ %span{ class: ('text-warning' if token.expires_soon?) }
+ = _('In %{time_to_now}') % { time_to_now: distance_of_time_in_words_to_now(token.expires_at) }
+ - else
+ %span.token-never-expires-label= _('Never')
+ %td= token.scopes.present? ? token.scopes.join(', ') : _('<no scopes selected>')
+ %td= link_to _('Revoke'), revoke_route_helper.call(token), method: :put, class: 'btn btn-danger float-right qa-revoke-button', data: { confirm: _('Are you sure you want to revoke this %{type}? This action cannot be undone.') % { type: type } }
+- else
+ .settings-message.text-center
+ = no_active_tokens_message
diff --git a/app/views/shared/boards/components/_board.html.haml b/app/views/shared/boards/components/_board.html.haml
index ffb406ac35b..2a5b72d478a 100644
--- a/app/views/shared/boards/components/_board.html.haml
+++ b/app/views/shared/boards/components/_board.html.haml
@@ -38,8 +38,7 @@
":description" => "list.label.description",
"tooltipPlacement" => "bottom",
":size" => '(!list.isExpanded ? "sm" : "")',
- ":scoped" => "showScopedLabels(list.label)",
- ":scoped-labels-documentation-link" => "helpLink" }
+ ":scoped" => "showScopedLabels(list.label)" }
- if can?(current_user, :admin_list, current_board_parent)
%board-delete{ "inline-template" => true,
diff --git a/app/views/shared/boards/components/sidebar/_labels.html.haml b/app/views/shared/boards/components/sidebar/_labels.html.haml
index a1088dc5222..58ffa3942ef 100644
--- a/app/views/shared/boards/components/sidebar/_labels.html.haml
+++ b/app/views/shared/boards/components/sidebar/_labels.html.haml
@@ -12,8 +12,7 @@
":background-color" => "label.color",
":title" => "label.title",
":description" => "label.description",
- ":scoped" => "showScopedLabels(label)",
- ":scoped-labels-documentation-link" => "helpLink" }
+ ":scoped" => "showScopedLabels(label)" }
- if can_admin_issue?
.selectbox
diff --git a/app/views/shared/deploy_keys/_index.html.haml b/app/views/shared/deploy_keys/_index.html.haml
new file mode 100644
index 00000000000..f28e745f4c5
--- /dev/null
+++ b/app/views/shared/deploy_keys/_index.html.haml
@@ -0,0 +1,14 @@
+- expanded = expanded_by_default?
+%section.qa-deploy-keys-settings.settings.no-animate#js-deploy-keys-settings{ class: ('expanded' if expanded), data: { qa_selector: 'deploy_keys_settings' } }
+ .settings-header
+ %h4= _('Deploy Keys')
+ %button.btn.js-settings-toggle{ type: 'button' }
+ = expanded ? 'Collapse' : 'Expand'
+ %p
+ = _('Deploy keys allow read-only or read-write (if enabled) access to your repository. Deploy keys can be used for CI, staging or production servers. You can create a deploy key or add an existing one.')
+ .settings-content
+ %h5.prepend-top-0
+ = _('Create a new deploy key for this project')
+ = render @deploy_keys.form_partial_path
+ %hr
+ #js-deploy-keys{ data: { endpoint: project_deploy_keys_path(@project), project_id: @project.id } }
diff --git a/app/views/shared/deploy_keys/_project_group_form.html.haml b/app/views/shared/deploy_keys/_project_group_form.html.haml
new file mode 100644
index 00000000000..8edd1d9deb8
--- /dev/null
+++ b/app/views/shared/deploy_keys/_project_group_form.html.haml
@@ -0,0 +1,24 @@
+= form_for [@project.namespace.becomes(Namespace), @project, @deploy_keys.new_key], url: namespace_project_deploy_keys_path, html: { class: "js-requires-input container" } do |f|
+ = form_errors(@deploy_keys.new_key)
+ .form-group.row
+ = f.label :title, class: "label-bold"
+ = f.text_field :title, class: 'form-control', required: true
+ .form-group.row
+ = f.label :key, class: "label-bold"
+ = f.text_area :key, class: "form-control", rows: 5, required: true
+ .form-group.row
+ %p.light.append-bottom-0
+ = _('Paste a machine public key here. Read more about how to generate it')
+ = link_to "here", help_page_path("ssh/README")
+
+ = f.fields_for :deploy_keys_projects do |deploy_keys_project_form|
+ .form-group.row
+ = deploy_keys_project_form.label :can_push do
+ = deploy_keys_project_form.check_box :can_push
+ %strong= _('Write access allowed')
+ .form-group.row
+ %p.light.append-bottom-0
+ = _('Allow this key to push to repository as well? (Default only allows pull access.)')
+
+ .form-group.row
+ = f.submit "Add key", class: "btn-success btn"
diff --git a/app/views/shared/deploy_tokens/_form.html.haml b/app/views/shared/deploy_tokens/_form.html.haml
index 5751ed9cb7a..512644518fa 100644
--- a/app/views/shared/deploy_tokens/_form.html.haml
+++ b/app/views/shared/deploy_tokens/_form.html.haml
@@ -35,5 +35,15 @@
= label_tag ("deploy_token_write_registry"), 'write_registry', class: 'label-bold form-check-label'
.text-secondary= s_('DeployTokens|Allows write access to the registry images')
+ %fieldset.form-group.form-check
+ = f.check_box :read_package_registry, class: 'form-check-input'
+ = label_tag ("deploy_token_read_package_registry"), 'read_package_registry', class: 'label-bold form-check-label'
+ .text-secondary= s_('DeployTokens|Allows read access to the package registry')
+
+ %fieldset.form-group.form-check
+ = f.check_box :write_package_registry, class: 'form-check-input'
+ = label_tag ("deploy_token_write_package_registry"), 'write_package_registry', class: 'label-bold form-check-label'
+ .text-secondary= s_('DeployTokens|Allows write access to the package registry')
+
.prepend-top-default
= f.submit s_('DeployTokens|Create deploy token'), class: 'btn btn-success qa-create-deploy-token'
diff --git a/app/views/shared/file_hooks/_index.html.haml b/app/views/shared/file_hooks/_index.html.haml
index 74eb6c94116..0e1f41bbbf6 100644
--- a/app/views/shared/file_hooks/_index.html.haml
+++ b/app/views/shared/file_hooks/_index.html.haml
@@ -19,6 +19,10 @@
%li
.monospace
= File.basename(file)
+ - if File.dirname(file).ends_with?('plugins')
+ .text-warning
+ = _('Plugins directory is deprecated and will be removed in 14.0. Please move this file into /file_hooks directory.')
+
- else
.card.bg-light.text-center
.nothing-here-block= _('No file hooks found.')
diff --git a/app/views/shared/hook_logs/_content.html.haml b/app/views/shared/hook_logs/_content.html.haml
index f3b56df0c96..6b056e93460 100644
--- a/app/views/shared/hook_logs/_content.html.haml
+++ b/app/views/shared/hook_logs/_content.html.haml
@@ -31,7 +31,7 @@
%h5 Request body:
%pre
:escaped
- #{JSON.pretty_generate(hook_log.request_data)}
+ #{Gitlab::Json.pretty_generate(hook_log.request_data)}
%h5 Response headers:
%pre
- hook_log.response_headers.each do |k,v|
diff --git a/app/views/shared/integrations/_form.html.haml b/app/views/shared/integrations/_form.html.haml
index 0ddab1368c2..4ec7f286c7a 100644
--- a/app/views/shared/integrations/_form.html.haml
+++ b/app/views/shared/integrations/_form.html.haml
@@ -10,5 +10,5 @@
- if integration.editable?
.footer-block.row-content-block
- = service_save_button(integration)
+ = service_save_button
= link_to _('Cancel'), scoped_integration_path(integration), class: 'btn btn-cancel'
diff --git a/app/views/shared/integrations/_index.html.haml b/app/views/shared/integrations/_index.html.haml
new file mode 100644
index 00000000000..2dbd612ea38
--- /dev/null
+++ b/app/views/shared/integrations/_index.html.haml
@@ -0,0 +1,27 @@
+%table.table.b-table.gl-table.mt-3{ role: 'table', 'aria-busy': false, 'aria-colcount': 4 }
+ %colgroup
+ %col
+ %col
+ %col.d-none.d-sm-table-column
+ %col{ width: 130 }
+ %thead{ role: 'rowgroup' }
+ %tr{ role: 'row' }
+ %th{ role: 'columnheader', scope: 'col', 'aria-colindex': 1 }
+ %th{ role: 'columnheader', scope: 'col', 'aria-colindex': 2 }= _('Integration')
+ %th.d-none.d-sm-block{ role: 'columnheader', scope: 'col', 'aria-colindex': 3 }= _('Description')
+ %th{ role: 'columnheader', scope: 'col', 'aria-colindex': 4 }= _('Last updated')
+
+ %tbody{ role: 'rowgroup' }
+ - integrations.each do |integration|
+ - activated_label = (integration.activated? ? s_("ProjectService|%{service_title}: status on") : s_("ProjectService|%{service_title}: status off")) % { service_title: integration.title }
+ %tr{ role: 'row' }
+ %td{ role: 'cell', 'aria-colindex': 1, 'aria-label': activated_label }
+ = boolean_to_icon integration.operating?
+ %td{ role: 'cell', 'aria-colindex': 2 }
+ = link_to scoped_edit_integration_path(integration), { data: { qa_selector: "#{integration.to_param}_link" } } do
+ %strong= integration.title
+ %td.d-none.d-sm-table-cell{ role: 'cell', 'aria-colindex': 3 }
+ = integration.description
+ %td{ role: 'cell', 'aria-colindex': 4 }
+ - if integration.updated_at.present?
+ = time_ago_with_tooltip integration.updated_at
diff --git a/app/views/shared/integrations/_integrations.html.haml b/app/views/shared/integrations/_integrations.html.haml
deleted file mode 100644
index b2359aca016..00000000000
--- a/app/views/shared/integrations/_integrations.html.haml
+++ /dev/null
@@ -1,26 +0,0 @@
-%table.table.b-table.gl-table.mt-3{ role: 'table', 'aria-busy': false, 'aria-colcount': 4 }
- %colgroup
- %col
- %col
- %col.d-none.d-sm-table-column
- %col{ width: 120 }
- %thead{ role: 'rowgroup' }
- %tr{ role: 'row' }
- %th{ role: 'columnheader', scope: 'col', 'aria-colindex': 1 }
- %th{ role: 'columnheader', scope: 'col', 'aria-colindex': 2 }= _('Integration')
- %th.d-none.d-sm-block{ role: 'columnheader', scope: 'col', 'aria-colindex': 3 }= _('Description')
- %th{ role: 'columnheader', scope: 'col', 'aria-colindex': 4 }= _('Last updated')
-
- %tbody{ role: 'rowgroup' }
- - integrations.each do |integration|
- %tr{ role: 'row' }
- %td{ role: 'cell', 'aria-colindex': 1 }
- = boolean_to_icon integration.activated?
- %td{ role: 'cell', 'aria-colindex': 2 }
- = link_to scoped_edit_integration_path(integration) do
- %strong= integration.title
- %td.d-none.d-sm-block{ role: 'cell', 'aria-colindex': 3 }
- = integration.description
- %td{ role: 'cell', 'aria-colindex': 4 }
- - if integration.updated_at.present?
- = time_ago_with_tooltip integration.updated_at
diff --git a/app/views/shared/issuable/_bulk_update_sidebar.html.haml b/app/views/shared/issuable/_bulk_update_sidebar.html.haml
index a05a13814ac..4bc6c1dee37 100644
--- a/app/views/shared/issuable/_bulk_update_sidebar.html.haml
+++ b/app/views/shared/issuable/_bulk_update_sidebar.html.haml
@@ -5,45 +5,49 @@
= form_tag [:bulk_update, @project.namespace.becomes(Namespace), @project, type], method: :post, class: "bulk-update" do
.block.issuable-sidebar-header
.filter-item.inline.update-issues-btn.float-left
- = button_tag "Update all", class: "btn update-selected-issues btn-info", disabled: true
- = button_tag "Cancel", class: "btn btn-default js-bulk-update-menu-hide float-right"
+ = button_tag _('Update all'), class: "btn update-selected-issues btn-info", disabled: true
+ = button_tag _('Cancel'), class: "btn btn-default js-bulk-update-menu-hide float-right"
.block
.title
- Status
+ = _('Status')
.filter-item
- = dropdown_tag("Select status", options: { toggle_class: "js-issue-status", title: "Change status", dropdown_class: "dropdown-menu-status dropdown-menu-selectable", data: { field_name: "update[state_event]", default_label: "Status" } } ) do
+ = dropdown_tag(_("Select status"), options: { toggle_class: "js-issue-status", title: _("Change status"), dropdown_class: "dropdown-menu-status dropdown-menu-selectable", data: { field_name: "update[state_event]", default_label: _("Status") } } ) do
%ul
%li
- %a{ href: "#", data: { id: "reopen" } } Open
+ %a{ href: "#", data: { id: "reopen" } }
+ = _('Open')
%li
- %a{ href: "#", data: { id: "close" } } Closed
+ %a{ href: "#", data: { id: "close" } }
+ = _('Closed')
.block
.title
- Assignee
+ = _('Assignee')
.filter-item
- field_name = "update[assignee_ids][]"
- = dropdown_tag("Select assignee", options: { toggle_class: "js-user-search js-update-assignee js-filter-submit js-filter-bulk-update", title: "Assign to", filter: true, dropdown_class: "dropdown-menu-user dropdown-menu-selectable",
- placeholder: "Search authors", data: { first_user: (current_user.username if current_user), null_user: true, current_user: true, project_id: @project.id, field_name: field_name } })
+ = dropdown_tag(_("Select assignee"), options: { toggle_class: "js-user-search js-update-assignee js-filter-submit js-filter-bulk-update", title: _("Assign to"), filter: true, dropdown_class: "dropdown-menu-user dropdown-menu-selectable",
+ placeholder: _("Search authors"), data: { first_user: (current_user.username if current_user), null_user: true, current_user: true, project_id: @project.id, field_name: field_name } })
.block
.title
- Milestone
+ = _('Milestone')
.filter-item
- = dropdown_tag("Select milestone", options: { title: "Assign milestone", toggle_class: "js-milestone-select js-extra-options js-filter-submit js-filter-bulk-update", filter: true, dropdown_class: "dropdown-menu-selectable dropdown-menu-milestone", placeholder: "Search milestones", data: { show_no: true, field_name: "update[milestone_id]", project_id: @project.id, milestones: project_milestones_path(@project, :json), use_id: true, default_label: "Milestone" } })
+ = dropdown_tag(_("Select milestone"), options: { title: _("Assign milestone"), toggle_class: "js-milestone-select js-extra-options js-filter-submit js-filter-bulk-update", filter: true, dropdown_class: "dropdown-menu-selectable dropdown-menu-milestone", placeholder: _("Search milestones"), data: { show_no: true, field_name: "update[milestone_id]", project_id: @project.id, milestones: project_milestones_path(@project, :json), use_id: true, default_label: _("Milestone") } })
.block
.title
- Labels
+ = _('Labels')
.filter-item.labels-filter
- = render "shared/issuable/label_dropdown", classes: ["js-filter-bulk-update", "js-multiselect"], dropdown_title: "Apply a label", show_create: false, show_footer: false, extra_options: false, filter_submit: false, data_options: { persist_when_hide: "true", field_name: "update[label_ids][]", show_no: false, show_any: false, use_id: true, default_label: "Labels" }, label_name: "Select labels", no_default_styles: true
+ = render "shared/issuable/label_dropdown", classes: ["js-filter-bulk-update", "js-multiselect"], dropdown_title: _("Apply a label"), show_create: false, show_footer: false, extra_options: false, filter_submit: false, data_options: { persist_when_hide: "true", field_name: "update[label_ids][]", show_no: false, show_any: false, use_id: true, default_label: _("Labels") }, label_name: _("Select labels"), no_default_styles: true
.block
.title
- Subscriptions
+ = _('Subscriptions')
.filter-item
- = dropdown_tag("Select subscription", options: { toggle_class: "js-subscription-event", title: "Change subscription", dropdown_class: "dropdown-menu-selectable", data: { field_name: "update[subscription_event]", default_label: "Subscription" } } ) do
+ = dropdown_tag(_("Select subscription"), options: { toggle_class: "js-subscription-event", title: _("Change subscription"), dropdown_class: "dropdown-menu-selectable", data: { field_name: "update[subscription_event]", default_label: _("Subscription") } } ) do
%ul
%li
- %a{ href: "#", data: { id: "subscribe" } } Subscribe
+ %a{ href: "#", data: { id: "subscribe" } }
+ = _('Subscribe')
%li
- %a{ href: "#", data: { id: "unsubscribe" } } Unsubscribe
+ %a{ href: "#", data: { id: "unsubscribe" } }
+ = _('Unsubscribe')
= hidden_field_tag "update[issuable_ids]", []
= hidden_field_tag :state_event, params[:state_event]
diff --git a/app/views/shared/issuable/_close_reopen_button.html.haml b/app/views/shared/issuable/_close_reopen_button.html.haml
index 2eb96a7bc9b..5f7cfdc9d03 100644
--- a/app/views/shared/issuable/_close_reopen_button.html.haml
+++ b/app/views/shared/issuable/_close_reopen_button.html.haml
@@ -2,17 +2,20 @@
- display_issuable_type = issuable_display_type(issuable)
- button_method = issuable_close_reopen_button_method(issuable)
- are_close_and_open_buttons_hidden = issuable_button_hidden?(issuable, true) && issuable_button_hidden?(issuable, false)
+- add_blocked_class = false
+- if defined? warn_before_close
+ - add_blocked_class = warn_before_close
- if is_current_user
- if can_update
- = link_to "Close #{display_issuable_type}", close_issuable_path(issuable), method: button_method,
- class: "d-none d-sm-none d-md-block btn btn-grouped btn-close js-btn-issue-action #{issuable_button_visibility(issuable, true)}", title: "Close #{display_issuable_type}", data: { qa_selector: 'close_issue_button' }
+ = link_to _("Close %{display_issuable_type}") % { display_issuable_type: display_issuable_type }, close_issuable_path(issuable), method: button_method,
+ class: "d-none d-sm-none d-md-block btn btn-grouped btn-close js-btn-issue-action #{issuable_button_visibility(issuable, true)} #{(add_blocked_class ? 'btn-issue-blocked' : '')}", title: _("Close %{display_issuable_type}") % { display_issuable_type: display_issuable_type }, data: { qa_selector: 'close_issue_button' }
- if can_reopen
- = link_to "Reopen #{display_issuable_type}", reopen_issuable_path(issuable), method: button_method,
- class: "d-none d-sm-none d-md-block btn btn-grouped btn-reopen js-btn-issue-action #{issuable_button_visibility(issuable, false)}", title: "Reopen #{display_issuable_type}", data: { qa_selector: 'reopen_issue_button' }
+ = link_to _("Reopen %{display_issuable_type}") % { display_issuable_type: display_issuable_type }, reopen_issuable_path(issuable), method: button_method,
+ class: "d-none d-sm-none d-md-block btn btn-grouped btn-reopen js-btn-issue-action #{issuable_button_visibility(issuable, false)}", title: _("Reopen %{display_issuable_type}") % { display_issuable_type: display_issuable_type }, data: { qa_selector: 'reopen_issue_button' }
- else
- if can_update && !are_close_and_open_buttons_hidden
- = render 'shared/issuable/close_reopen_report_toggle', issuable: issuable
+ = render 'shared/issuable/close_reopen_report_toggle', issuable: issuable, warn_before_close: add_blocked_class
- else
- = link_to 'Report abuse', new_abuse_report_path(user_id: issuable.author.id, ref_url: issuable_url(issuable)),
- class: 'd-none d-sm-none d-md-block btn btn-grouped btn-close-color', title: 'Report abuse'
+ = link_to _('Report abuse'), new_abuse_report_path(user_id: issuable.author.id, ref_url: issuable_url(issuable)),
+ class: 'd-none d-sm-none d-md-block btn btn-grouped btn-close-color', title: _('Report abuse')
diff --git a/app/views/shared/issuable/_close_reopen_report_toggle.html.haml b/app/views/shared/issuable/_close_reopen_report_toggle.html.haml
index 0d59c9304b4..9d718083d2d 100644
--- a/app/views/shared/issuable/_close_reopen_report_toggle.html.haml
+++ b/app/views/shared/issuable/_close_reopen_report_toggle.html.haml
@@ -5,45 +5,46 @@
- button_class = "#{button_responsive_class} btn btn-grouped js-issuable-close-button js-btn-issue-action issuable-close-button"
- toggle_class = "#{button_responsive_class} btn btn-nr dropdown-toggle js-issuable-close-toggle"
- button_method = issuable_close_reopen_button_method(issuable)
+- add_blocked_class = false
+- if defined? warn_before_close
+ - add_blocked_class = !issuable.closed? && warn_before_close
.float-left.btn-group.prepend-left-10.issuable-close-dropdown.droplab-dropdown.js-issuable-close-dropdown
= link_to "#{display_button_action} #{display_issuable_type}", close_reopen_issuable_path(issuable),
- method: button_method, class: "#{button_class} btn-#{button_action}", title: "#{display_button_action} #{display_issuable_type}"
+ method: button_method, class: "#{button_class} btn-#{button_action} #{(add_blocked_class ? 'btn-issue-blocked' : '')}", title: "#{display_button_action} #{display_issuable_type}", data: { qa_selector: 'close_issue_button' }
= button_tag type: 'button', class: "#{toggle_class} btn-#{button_action}-color",
- data: { 'dropdown-trigger' => '#issuable-close-menu' }, 'aria-label' => 'Toggle dropdown' do
+ data: { 'dropdown-trigger' => '#issuable-close-menu' }, 'aria-label' => _('Toggle dropdown') do
= icon('caret-down', class: 'toggle-icon icon')
%ul#issuable-close-menu.js-issuable-close-menu.dropdown-menu{ data: { dropdown: true } }
%li.close-item{ class: "#{issuable_button_visibility(issuable, true) || 'droplab-item-selected'}",
- data: { text: "Close #{display_issuable_type}", url: close_issuable_path(issuable),
+ data: { text: _("Close %{display_issuable_type}") % { display_issuable_type: display_issuable_type }, url: close_issuable_path(issuable),
button_class: "#{button_class} btn-close", toggle_class: "#{toggle_class} btn-close-color", method: button_method } }
%button.btn.btn-transparent
= icon('check', class: 'icon')
.description
%strong.title
- Close
+ = _('Close')
= display_issuable_type
%li.reopen-item{ class: "#{issuable_button_visibility(issuable, false) || 'droplab-item-selected'}",
- data: { text: "Reopen #{display_issuable_type}", url: reopen_issuable_path(issuable),
+ data: { text: _("Reopen %{display_issuable_type}") % { display_issuable_type: display_issuable_type }, url: reopen_issuable_path(issuable),
button_class: "#{button_class} btn-reopen", toggle_class: "#{toggle_class} btn-reopen-color", method: button_method } }
%button.btn.btn-transparent
= icon('check', class: 'icon')
.description
%strong.title
- Reopen
+ = _('Reopen')
= display_issuable_type
%li.divider.droplab-item-ignore
- %li.report-item{ data: { text: 'Report abuse', url: new_abuse_report_path(user_id: issuable.author.id, ref_url: issuable_url(issuable)),
+ %li.report-item{ data: { text: _('Report abuse'), url: new_abuse_report_path(user_id: issuable.author.id, ref_url: issuable_url(issuable)),
button_class: "#{button_class} btn-close-color", toggle_class: "#{toggle_class} btn-close-color", method: '' } }
%button.btn.btn-transparent
= icon('check', class: 'icon')
.description
- %strong.title Report abuse
+ %strong.title= _('Report abuse')
%p.text
- Report
- = display_issuable_type.pluralize
- that are abusive, inappropriate or spam.
+ = _('Report %{display_issuable_type} that are abusive, inappropriate or spam.') % { display_issuable_type: display_issuable_type.pluralize }
diff --git a/app/views/shared/issuable/_label_dropdown.html.haml b/app/views/shared/issuable/_label_dropdown.html.haml
index bca5db16bd3..535af522c1a 100644
--- a/app/views/shared/issuable/_label_dropdown.html.haml
+++ b/app/views/shared/issuable/_label_dropdown.html.haml
@@ -8,10 +8,11 @@
- data_options = local_assigns.fetch(:data_options, {})
- classes = local_assigns.fetch(:classes, [])
- selected = local_assigns.fetch(:selected, nil)
-- dropdown_title = local_assigns.fetch(:dropdown_title, "Filter by label")
-- dropdown_data = label_dropdown_data(edit_context, labels: labels_filter_path_with_defaults(only_group_labels: edit_context.is_a?(Group)), default_label: "Labels")
+- dropdown_title = local_assigns.fetch(:dropdown_title, _('Filter by label'))
+- dropdown_data = label_dropdown_data(edit_context, labels: labels_filter_path_with_defaults(only_group_labels: edit_context.is_a?(Group)), default_label: _('Labels'))
+
- dropdown_data.merge!(data_options)
-- label_name = local_assigns.fetch(:label_name, "Labels")
+- label_name = local_assigns.fetch(:label_name, _('Labels'))
- no_default_styles = local_assigns.fetch(:no_default_styles, false)
- classes << 'js-extra-options' if extra_options
- classes << 'js-filter-submit' if filter_submit
diff --git a/app/views/shared/issuable/_label_page_default.html.haml b/app/views/shared/issuable/_label_page_default.html.haml
index a0fb5229fc3..43e80c9db27 100644
--- a/app/views/shared/issuable/_label_page_default.html.haml
+++ b/app/views/shared/issuable/_label_page_default.html.haml
@@ -3,7 +3,7 @@
- show_title = local_assigns.fetch(:show_title, true)
- show_create = local_assigns.fetch(:show_create, true)
- show_footer = local_assigns.fetch(:show_footer, true)
-- filter_placeholder = local_assigns.fetch(:filter_placeholder, 'Search')
+- filter_placeholder = local_assigns.fetch(:filter_placeholder, _('Search'))
- show_boards_content = local_assigns.fetch(:show_boards_content, false)
- subject = @project || @group
.dropdown-page-one
diff --git a/app/views/shared/issuable/_nav.html.haml b/app/views/shared/issuable/_nav.html.haml
index 93408e0bfc0..c715cd8f736 100644
--- a/app/views/shared/issuable/_nav.html.haml
+++ b/app/views/shared/issuable/_nav.html.haml
@@ -4,20 +4,20 @@
%ul.nav-links.issues-state-filters.mobile-separator.nav.nav-tabs
%li{ class: active_when(params[:state] == 'opened') }>
- = link_to page_filter_path(state: 'opened'), id: 'state-opened', title: "Filter by #{page_context_word} that are currently opened.", data: { state: 'opened' } do
+ = link_to page_filter_path(state: 'opened'), id: 'state-opened', title: _("Filter by %{page_context_word} that are currently opened.") % { page_context_word: page_context_word }, data: { state: 'opened' } do
#{issuables_state_counter_text(type, :opened, display_count)}
- if type == :merge_requests
%li{ class: active_when(params[:state] == 'merged') }>
- = link_to page_filter_path(state: 'merged'), id: 'state-merged', title: 'Filter by merge requests that are currently merged.', data: { state: 'merged' } do
+ = link_to page_filter_path(state: 'merged'), id: 'state-merged', title: _('Filter by merge requests that are currently merged.'), data: { state: 'merged' } do
#{issuables_state_counter_text(type, :merged, display_count)}
%li{ class: active_when(params[:state] == 'closed') }>
- = link_to page_filter_path(state: 'closed'), id: 'state-closed', title: 'Filter by merge requests that are currently closed and unmerged.', data: { state: 'closed' } do
+ = link_to page_filter_path(state: 'closed'), id: 'state-closed', title: _('Filter by merge requests that are currently closed and unmerged.'), data: { state: 'closed' } do
#{issuables_state_counter_text(type, :closed, display_count)}
- else
%li{ class: active_when(params[:state] == 'closed') }>
- = link_to page_filter_path(state: 'closed'), id: 'state-closed', title: 'Filter by issues that are currently closed.', data: { state: 'closed', qa_selector: 'closed_issues_link' } do
+ = link_to page_filter_path(state: 'closed'), id: 'state-closed', title: _('Filter by issues that are currently closed.'), data: { state: 'closed', qa_selector: 'closed_issues_link' } do
#{issuables_state_counter_text(type, :closed, display_count)}
= render 'shared/issuable/nav_links/all', page_context_word: page_context_word, counter: issuables_state_counter_text(type, :all, display_count)
diff --git a/app/views/shared/issuable/_search_bar.html.haml b/app/views/shared/issuable/_search_bar.html.haml
index d9ca0b8869f..34be9291f1f 100644
--- a/app/views/shared/issuable/_search_bar.html.haml
+++ b/app/views/shared/issuable/_search_bar.html.haml
@@ -1,6 +1,7 @@
- type = local_assigns.fetch(:type)
- board = local_assigns.fetch(:board, nil)
- show_sorting_dropdown = local_assigns.fetch(:show_sorting_dropdown, true)
+- placeholder = local_assigns[:placeholder] || _('Search or filter results...')
- is_not_boards_modal_or_productivity_analytics = type != :boards_modal && type != :productivity_analytics
- block_css_class = is_not_boards_modal_or_productivity_analytics ? 'row-content-block second-block' : ''
- user_can_admin_list = board && can?(current_user, :admin_list, board.resource_parent)
@@ -29,7 +30,7 @@
.scroll-container
%ul.tokens-container.list-unstyled
%li.input-token
- %input.form-control.filtered-search{ search_filter_input_options(type) }
+ %input.form-control.filtered-search{ search_filter_input_options(type, placeholder) }
#js-dropdown-hint.filtered-search-input-dropdown-menu.dropdown-menu.hint-dropdown
%ul.filter-dropdown{ data: { dynamic: true, dropdown: true } }
%li.filter-dropdown-item{ data: {hint: "#{'{{hint}}'}", tag: "#{'{{tag}}'}", action: "#{'{{hint === \'search\' ? \'submit\' : \'\' }}'}" } }
@@ -73,6 +74,7 @@
user: User.new(username: '{{username}}', name: '{{name}}'),
avatar: { lazy: true, url: '{{avatar_url}}' }
= render_if_exists 'shared/issuable/approver_dropdown'
+ = render_if_exists 'shared/issuable/approved_by_dropdown'
#js-dropdown-milestone.filtered-search-input-dropdown-menu.dropdown-menu
%ul{ data: { dropdown: true } }
%li.filter-dropdown-item{ data: { value: 'None' } }
diff --git a/app/views/shared/issuable/_sidebar.html.haml b/app/views/shared/issuable/_sidebar.html.haml
index e20573ed3a7..a1c56cdb64f 100644
--- a/app/views/shared/issuable/_sidebar.html.haml
+++ b/app/views/shared/issuable/_sidebar.html.haml
@@ -15,7 +15,7 @@
- if signed_in
%span.issuable-header-text.hide-collapsed.float-left
= _('To Do')
- %a.gutter-toggle.float-right.js-sidebar-toggle.has-tooltip{ role: "button", href: "#", "aria-label" => "Toggle sidebar", title: sidebar_gutter_tooltip_text, data: { container: 'body', placement: 'left', boundary: 'viewport' } }
+ %a.gutter-toggle.float-right.js-sidebar-toggle.has-tooltip{ role: "button", href: "#", "aria-label" => _('Toggle sidebar'), title: sidebar_gutter_tooltip_text, data: { container: 'body', placement: 'left', boundary: 'viewport' } }
= sidebar_gutter_toggle_icon
- if signed_in
= render "shared/issuable/sidebar_todo", issuable_sidebar: issuable_sidebar
@@ -65,7 +65,7 @@
.sidebar-collapsed-icon.has-tooltip{ data: { placement: 'left', container: 'body', html: 'true', boundary: 'viewport' }, title: sidebar_due_date_tooltip_label(issuable_sidebar[:due_date]) }
= icon('calendar', 'aria-hidden': 'true')
%span.js-due-date-sidebar-value
- = issuable_sidebar[:due_date].try(:to_s, :medium) || 'None'
+ = issuable_sidebar[:due_date].try(:to_s, :medium) || _('None')
.title.hide-collapsed
= _('Due date')
= icon('spinner spin', class: 'hidden block-loading', 'aria-hidden': 'true')
diff --git a/app/views/shared/issuable/_sidebar_assignees.html.haml b/app/views/shared/issuable/_sidebar_assignees.html.haml
index b5a27f2f17d..4192ecd2238 100644
--- a/app/views/shared/issuable/_sidebar_assignees.html.haml
+++ b/app/views/shared/issuable/_sidebar_assignees.html.haml
@@ -1,7 +1,7 @@
- issuable_type = issuable_sidebar[:type]
- signed_in = !!issuable_sidebar.dig(:current_user, :id)
-#js-vue-sidebar-assignees{ data: { field: "#{issuable_type}", signed_in: signed_in } }
+#js-vue-sidebar-assignees{ data: { field: issuable_type, signed_in: signed_in } }
.title.hide-collapsed
= _('Assignee')
.spinner.spinner-sm.align-bottom
diff --git a/app/views/shared/issuable/form/_branch_chooser.html.haml b/app/views/shared/issuable/form/_branch_chooser.html.haml
index d8253924e0a..3794a3b3845 100644
--- a/app/views/shared/issuable/form/_branch_chooser.html.haml
+++ b/app/views/shared/issuable/form/_branch_chooser.html.haml
@@ -6,7 +6,7 @@
- source_title, target_title = format_mr_branch_names(@merge_request)
-.form-group.row.d-flex.gl-pl-3.gl-pr-3.branch-selector
+.form-group.row.d-flex.gl-pl-3-deprecated-no-really-do-not-use-me.gl-pr-3-deprecated-no-really-do-not-use-me.branch-selector
.align-self-center
%span
= _('From <code>%{source_title}</code> into').html_safe % { source_title: source_title }
diff --git a/app/views/shared/members/_badge.html.haml b/app/views/shared/members/_badge.html.haml
new file mode 100644
index 00000000000..e304207f3e9
--- /dev/null
+++ b/app/views/shared/members/_badge.html.haml
@@ -0,0 +1,4 @@
+- type ||= 'info'
+
+%span.px-1.py-1
+ %span{ class: "badge badge-#{type}" }= yield
diff --git a/app/views/shared/members/_blocked_badge.html.haml b/app/views/shared/members/_blocked_badge.html.haml
new file mode 100644
index 00000000000..95335ebe74d
--- /dev/null
+++ b/app/views/shared/members/_blocked_badge.html.haml
@@ -0,0 +1,3 @@
+- if user.blocked?
+ = render 'shared/members/badge', type: 'danger' do
+ = _("Blocked")
diff --git a/app/views/shared/members/_its_you_badge.html.haml b/app/views/shared/members/_its_you_badge.html.haml
new file mode 100644
index 00000000000..b53ffd8032d
--- /dev/null
+++ b/app/views/shared/members/_its_you_badge.html.haml
@@ -0,0 +1,3 @@
+- if user == current_user
+ = render 'shared/members/badge', type: 'success' do
+ = _("It's you")
diff --git a/app/views/shared/members/_member.html.haml b/app/views/shared/members/_member.html.haml
index d74030c566f..f7d90a588c7 100644
--- a/app/views/shared/members/_member.html.haml
+++ b/app/views/shared/members/_member.html.haml
@@ -13,24 +13,23 @@
- if user
= image_tag avatar_icon_for_user(user, 40), class: "avatar s40 flex-shrink-0 flex-grow-0", alt: ''
.user-info
- = link_to user.name, user_path(user), class: 'member js-user-link', data: { user_id: user.id }
- = user_status(user)
- %span.cgray= user.to_reference
+ %span.mr-1
+ = link_to user.name, user_path(user), class: 'member js-user-link', data: { user_id: user.id }
+ = user_status(user)
+ %span.cgray= user.to_reference
- = render_if_exists 'shared/members/ee/sso_badge', member: member
+ .mx-n1.d-inline-flex.flex-wrap
+ = render_if_exists 'shared/members/ee/sso_badge', member: member
- - if user == current_user
- %span.badge.badge-success.prepend-left-5= _("It's you")
+ = render_if_exists 'shared/members/ee/gma_badge', member: member
- = render_if_exists 'shared/members/ee/license_badge', user: user, group: @group
+ = render 'shared/members/its_you_badge', user: user, current_user: current_user
- - if user.blocked?
- %label.badge.badge-danger
- %strong= _("Blocked")
+ = render_if_exists 'shared/members/ee/license_badge', user: user, group: @group
- - if user.two_factor_enabled?
- %label.badge.badge-info
- = _("2FA")
+ = render 'shared/members/blocked_badge', user: user
+
+ = render 'shared/members/two_factor_auth_badge', user: user
- if source.instance_of?(Group) && source != @group
&middot;
@@ -68,7 +67,7 @@
class: 'btn btn-default align-self-center mr-sm-2',
title: _('Resend invite')
- - if user != current_user && member.can_update?
+ - if user != current_user && member.can_update? && !user&.project_bot?
= form_for member, remote: true, html: { class: "js-edit-member-form form-group #{'d-sm-flex' unless force_mobile_view}" } do |f|
= f.hidden_field :access_level
.member-form-control.dropdown{ class: [("mr-sm-2 d-sm-inline-block" unless force_mobile_view)] }
@@ -118,7 +117,7 @@
method: :delete,
data: { confirm: leave_confirmation_message(member.source) },
class: "btn btn-remove align-self-center m-0 #{'ml-sm-2' unless force_mobile_view}"
- - else
+ - elsif !user&.project_bot?
= link_to member,
method: :delete,
data: { confirm: remove_member_message(member), qa_selector: 'delete_member_button' },
diff --git a/app/views/shared/members/_two_factor_auth_badge.html.haml b/app/views/shared/members/_two_factor_auth_badge.html.haml
new file mode 100644
index 00000000000..34850c135d6
--- /dev/null
+++ b/app/views/shared/members/_two_factor_auth_badge.html.haml
@@ -0,0 +1,3 @@
+- if user.two_factor_enabled?
+ = render 'shared/members/badge', type: 'info' do
+ = _("2FA")
diff --git a/app/views/shared/milestones/_issuable.html.haml b/app/views/shared/milestones/_issuable.html.haml
index 0adfe2f0c04..f8bf3e7ad6a 100644
--- a/app/views/shared/milestones/_issuable.html.haml
+++ b/app/views/shared/milestones/_issuable.html.haml
@@ -25,5 +25,5 @@
%span.assignee-icon
- assignees.each do |assignee|
= link_to polymorphic_path(issuable_type_args, { milestone_title: @milestone.title, assignee_id: assignee.id, state: 'all' }),
- class: 'has-tooltip', title: "Assigned to #{assignee.name}", data: { container: 'body' } do
+ class: 'has-tooltip', title: _("Assigned to %{assignee_name}") % { assignee_name: assignee.name }, data: { container: 'body' } do
- image_tag(avatar_icon_for_user(assignee, 16), class: "avatar s16", alt: '')
diff --git a/app/views/shared/milestones/_issues_tab.html.haml b/app/views/shared/milestones/_issues_tab.html.haml
index d7e4f2ed5a0..6684f6d752a 100644
--- a/app/views/shared/milestones/_issues_tab.html.haml
+++ b/app/views/shared/milestones/_issues_tab.html.haml
@@ -8,8 +8,8 @@
.row.prepend-top-default
.col-md-4
- = render 'shared/milestones/issuables', args.merge(title: 'Unstarted Issues (open and unassigned)', issuables: issues.opened.unassigned, id: 'unassigned', show_counter: true)
+ = render 'shared/milestones/issuables', args.merge(title: s_('Milestones|Unstarted Issues (open and unassigned)'), issuables: issues.opened.unassigned, id: 'unassigned', show_counter: true)
.col-md-4
- = render 'shared/milestones/issuables', args.merge(title: 'Ongoing Issues (open and assigned)', issuables: issues.opened.assigned, id: 'ongoing', show_counter: true)
+ = render 'shared/milestones/issuables', args.merge(title: s_('Milestones|Ongoing Issues (open and assigned)'), issuables: issues.opened.assigned, id: 'ongoing', show_counter: true)
.col-md-4
- = render 'shared/milestones/issuables', args.merge(title: 'Completed Issues (closed)', issuables: issues.closed, id: 'closed', show_counter: true)
+ = render 'shared/milestones/issuables', args.merge(title: s_('Milestones|Completed Issues (closed)'), issuables: issues.closed, id: 'closed', show_counter: true)
diff --git a/app/views/shared/milestones/_labels_tab.html.haml b/app/views/shared/milestones/_labels_tab.html.haml
index 6d79b0d31b2..3b4d29ca7b0 100644
--- a/app/views/shared/milestones/_labels_tab.html.haml
+++ b/app/views/shared/milestones/_labels_tab.html.haml
@@ -9,6 +9,6 @@
.float-right.d-none.d-lg-block.d-xl-block
= link_to milestones_issues_path(options.merge(state: 'opened')), class: 'btn btn-transparent btn-action' do
- - pluralize milestone_issues_by_label_count(@milestone, label, state: :opened), 'open issue'
+ - pluralize milestone_issues_by_label_count(@milestone, label, state: :opened), _('open issue')
= link_to milestones_issues_path(options.merge(state: 'closed')), class: 'btn btn-transparent btn-action' do
- - pluralize milestone_issues_by_label_count(@milestone, label, state: :closed), 'closed issue'
+ - pluralize milestone_issues_by_label_count(@milestone, label, state: :closed), _('closed issue')
diff --git a/app/views/shared/milestones/_merge_requests_tab.haml b/app/views/shared/milestones/_merge_requests_tab.haml
index 9c193f901e2..4dba2473efc 100644
--- a/app/views/shared/milestones/_merge_requests_tab.haml
+++ b/app/views/shared/milestones/_merge_requests_tab.haml
@@ -3,10 +3,10 @@
.row.prepend-top-default
.col-md-3
- = render 'shared/milestones/issuables', args.merge(title: 'Work in progress (open and unassigned)', issuables: merge_requests.opened.unassigned, id: 'unassigned', show_counter: true)
+ = render 'shared/milestones/issuables', args.merge(title: _('Work in progress (open and unassigned)'), issuables: merge_requests.opened.unassigned, id: 'unassigned', show_counter: true)
.col-md-3
- = render 'shared/milestones/issuables', args.merge(title: 'Waiting for merge (open and assigned)', issuables: merge_requests.opened.assigned, id: 'ongoing', show_counter: true)
+ = render 'shared/milestones/issuables', args.merge(title: _('Waiting for merge (open and assigned)'), issuables: merge_requests.opened.assigned, id: 'ongoing', show_counter: true)
.col-md-3
- = render 'shared/milestones/issuables', args.merge(title: 'Rejected (closed)', issuables: merge_requests.closed, id: 'closed', show_counter: true)
+ = render 'shared/milestones/issuables', args.merge(title: _('Rejected (closed)'), issuables: merge_requests.closed, id: 'closed', show_counter: true)
.col-md-3
- = render 'shared/milestones/issuables', args.merge(title: 'Merged', issuables: merge_requests.merged, id: 'merged', primary: true, show_counter: true)
+ = render 'shared/milestones/issuables', args.merge(title: _('Merged'), issuables: merge_requests.merged, id: 'merged', primary: true, show_counter: true)
diff --git a/app/views/shared/milestones/_milestone.html.haml b/app/views/shared/milestones/_milestone.html.haml
index 451c2c2ba10..9f61082d605 100644
--- a/app/views/shared/milestones/_milestone.html.haml
+++ b/app/views/shared/milestones/_milestone.html.haml
@@ -1,6 +1,6 @@
- dashboard = local_assigns[:dashboard]
- custom_dom_id = dom_id(milestone.try(:milestone) ? milestone.milestone : milestone)
-- milestone_type = milestone.group_milestone? ? 'Group Milestone' : 'Project Milestone'
+- milestone_type = milestone.group_milestone? ? s_('Milestones|Group Milestone') : s_('Milestones|Project Milestone')
%li{ class: "milestone milestone-#{milestone.closed? ? 'closed' : 'open'}", id: custom_dom_id }
.row
@@ -42,17 +42,17 @@
.col-sm-4.milestone-progress
= milestone_progress_bar(milestone)
- = link_to pluralize(milestone.total_issues_count, 'Issue'), issues_path
+ = link_to pluralize(milestone.total_issues_count, _('Issue')), issues_path
- if milestone.merge_requests_enabled?
&middot;
- = link_to pluralize(milestone.merge_requests_visible_to_user(current_user).size, 'Merge Request'), merge_requests_path
+ = link_to pluralize(milestone.merge_requests_visible_to_user(current_user).size, _('Merge Request')), merge_requests_path
.float-lg-right.light #{milestone.percent_complete}% complete
.col-sm-2
.milestone-actions.d-flex.justify-content-sm-start.justify-content-md-end
- if @project
- if can_admin_project_milestones? and milestone.active?
- if can_admin_group_milestones?
- %button.js-promote-project-milestone-button.btn.btn-blank.btn-sm.btn-grouped.has-tooltip{ title: _('Promote to Group Milestone'),
+ %button.js-promote-project-milestone-button.btn.btn-blank.btn-sm.btn-grouped.has-tooltip{ title: s_('Milestones|Promote to Group Milestone'),
disabled: true,
type: 'button',
data: { url: promote_project_milestone_path(milestone.project, milestone),
@@ -63,15 +63,15 @@
toggle: 'modal' } }
= sprite_icon('level-up', size: 14)
- = link_to 'Close Milestone', project_milestone_path(@project, milestone, milestone: {state_event: :close }), method: :put, remote: true, class: "btn btn-sm btn-close btn-grouped"
+ = link_to s_('Milestones|Close Milestone'), project_milestone_path(@project, milestone, milestone: {state_event: :close }), method: :put, remote: true, class: "btn btn-sm btn-close btn-grouped"
- unless milestone.active?
- = link_to 'Reopen Milestone', project_milestone_path(@project, milestone, milestone: {state_event: :activate }), method: :put, class: "btn btn-grouped btn-reopen"
+ = link_to s_('Milestones|Reopen Milestone'), project_milestone_path(@project, milestone, milestone: {state_event: :activate }), method: :put, class: "btn btn-grouped btn-reopen"
- if @group
- if can?(current_user, :admin_milestone, @group)
- if milestone.closed?
- = link_to 'Reopen Milestone', group_milestone_route(milestone, {state_event: :activate }), method: :put, class: "btn btn-sm btn-grouped btn-reopen"
+ = link_to s_('Milestones|Reopen Milestone'), group_milestone_route(milestone, {state_event: :activate }), method: :put, class: "btn btn-sm btn-grouped btn-reopen"
- else
- = link_to 'Close Milestone', group_milestone_route(milestone, {state_event: :close }), method: :put, class: "btn btn-sm btn-grouped btn-close"
+ = link_to s_('Milestones|Close Milestone'), group_milestone_route(milestone, {state_event: :close }), method: :put, class: "btn btn-sm btn-grouped btn-close"
- if dashboard
.label-badge.label-badge-gray
= milestone_type
diff --git a/app/views/shared/milestones/_top.html.haml b/app/views/shared/milestones/_top.html.haml
index 8d911d4247e..5f53e6316af 100644
--- a/app/views/shared/milestones/_top.html.haml
+++ b/app/views/shared/milestones/_top.html.haml
@@ -21,10 +21,10 @@
%table.table
%thead
%tr
- %th Project
- %th Open issues
- %th State
- %th Due date
+ %th= _('Project')
+ %th= _('Open issues')
+ %th= _('State')
+ %th= _('Due date')
%tr
%td
- project_name = group ? milestone.project.name : milestone.project.full_name
@@ -33,8 +33,8 @@
= milestone.milestone.issues_visible_to_user(current_user).opened.count
%td
- if milestone.closed?
- Closed
+ = _('Closed')
- else
- Open
+ = _('Open')
%td
= milestone.expires_at
diff --git a/app/views/shared/notes/_form.html.haml b/app/views/shared/notes/_form.html.haml
index d91bc6e57c9..327745e4f4d 100644
--- a/app/views/shared/notes/_form.html.haml
+++ b/app/views/shared/notes/_form.html.haml
@@ -5,7 +5,7 @@
- else
- preview_url = preview_markdown_path(@project)
-= form_for form_resources, url: new_form_url, remote: true, html: { :'data-type' => 'json', multipart: true, id: nil, class: "new-note js-new-note-form js-quick-submit common-note-form", "data-noteable-iid" => @note.noteable.try(:iid), }, authenticity_token: true do |f|
+= form_for form_resources, url: new_form_url, remote: true, html: { :'data-type' => 'json', multipart: true, id: nil, class: "new-note js-new-note-form js-quick-submit common-note-form discussion-reply-holder", "data-noteable-iid" => @note.noteable.try(:iid), }, authenticity_token: true do |f|
= hidden_field_tag :view, diff_view
= hidden_field_tag :line_type
= hidden_field_tag :merge_request_diff_head_sha, @note.noteable.try(:diff_head_sha)
@@ -24,7 +24,7 @@
-# DiffNote
= f.hidden_field :position
- .discussion-form-container
+ .discussion-form-container.discussion-with-resolve-btn.flex-column.p-0
= render layout: 'projects/md_preview', locals: { url: preview_url, referenced_users: true } do
= render 'projects/zen', f: f,
attr: :note,
diff --git a/app/views/shared/notes/_note.html.haml b/app/views/shared/notes/_note.html.haml
index 50bc4fb35df..df09c4338a1 100644
--- a/app/views/shared/notes/_note.html.haml
+++ b/app/views/shared/notes/_note.html.haml
@@ -40,9 +40,10 @@
- if note.system
%span.system-note-message
= markdown_field(note, :note)
- %span.system-note-separator
- &middot;
- %a.system-note-separator{ href: "##{dom_id(note)}" }= time_ago_with_tooltip(note.created_at, placement: 'bottom', html_class: 'note-created-ago')
+ - if note.created_at
+ %span.system-note-separator
+ &middot;
+ %a.system-note-separator{ href: "##{dom_id(note)}" }= time_ago_with_tooltip(note.created_at, placement: 'bottom', html_class: 'note-created-ago')
- unless note.system?
.note-actions
- if note.for_personal_snippet?
diff --git a/app/views/shared/snippets/_form.html.haml b/app/views/shared/snippets/_form.html.haml
index 396b6e56ea9..4695692fb53 100644
--- a/app/views/shared/snippets/_form.html.haml
+++ b/app/views/shared/snippets/_form.html.haml
@@ -1,7 +1,3 @@
-- if Feature.disabled?(:monaco_snippets)
- - content_for :page_specific_javascripts do
- = page_specific_javascript_tag('lib/ace.js')
-
- if Feature.enabled?(:snippets_edit_vue)
#js-snippet-edit.snippet-form{ data: {'project_path': @snippet.project&.full_path, 'snippet-gid': @snippet.new_record? ? '' : @snippet.to_global_id, 'markdown-preview-path': preview_markdown_path(parent), 'markdown-docs-path': help_page_path('user/markdown'), 'visibility-help-link': help_page_path("public_access/public_access") } }
- else
@@ -24,7 +20,7 @@
= text_field_tag nil, nil, class: 'form-control', placeholder: description_placeholder, data: { qa_selector: 'description_placeholder' }
.js-expanded{ class: ('d-none' if !is_expanded) }
= render layout: 'projects/md_preview', locals: { url: preview_markdown_path(@project), referenced_users: true } do
- = render 'projects/zen', f: f, attr: :description, classes: 'note-textarea', placeholder: description_placeholder, qa_selector: 'description_field'
+ = render 'projects/zen', f: f, attr: :description, classes: 'note-textarea', placeholder: description_placeholder, qa_selector: 'snippet_description_field'
= render 'shared/notes/hints'
.form-group.file-editor
@@ -48,9 +44,9 @@
.form-actions
- if @snippet.new_record?
- = f.submit 'Create snippet', class: "btn-success btn qa-create-snippet-button"
+ = f.submit 'Create snippet', class: "btn-success btn", data: { qa_selector: 'submit_button' }
- else
- = f.submit 'Save changes', class: "btn-success btn"
+ = f.submit 'Save changes', class: "btn-success btn", data: { qa_selector: 'submit_button' }
- if @snippet.project_id
= link_to "Cancel", project_snippets_path(@project), class: "btn btn-cancel"
diff --git a/app/views/shared/snippets/_header.html.haml b/app/views/shared/snippets/_header.html.haml
index 1243bdab6dd..e663d57ae6a 100644
--- a/app/views/shared/snippets/_header.html.haml
+++ b/app/views/shared/snippets/_header.html.haml
@@ -21,7 +21,7 @@
= markdown_field(@snippet, :title)
- if @snippet.description.present?
- .description{ data: { qa_selector: 'snippet_description' } }
+ .description{ data: { qa_selector: 'snippet_description_field' } }
.md
= markdown_field(@snippet, :description)
%textarea.hidden.js-task-list-field
@@ -34,7 +34,7 @@
.embed-snippet
.input-group
.input-group-prepend
- %button.btn.btn-svg.embed-toggle.input-group-text.qa-embed-type{ 'data-toggle': 'dropdown', type: 'button' }
+ %button.btn.btn-svg.embed-toggle.input-group-text{ 'data-toggle': 'dropdown', type: 'button' }
%span.js-embed-action= _("Embed")
= sprite_icon('angle-down', size: 12, css_class: 'caret-down')
%ul.dropdown-menu.dropdown-menu-selectable.embed-toggle-list
diff --git a/app/views/shared/snippets/_snippet.html.haml b/app/views/shared/snippets/_snippet.html.haml
index 3fea2c1e3fc..128ddbb8e8b 100644
--- a/app/views/shared/snippets/_snippet.html.haml
+++ b/app/views/shared/snippets/_snippet.html.haml
@@ -1,6 +1,5 @@
- link_project = local_assigns.fetch(:link_project, false)
- notes_count = @noteable_meta_data[snippet.id].user_notes_count
-- file_name = snippet_file_name(snippet)
%li.snippet-row.py-3
= image_tag avatar_icon_for_user(snippet.author), class: "avatar s40 d-none d-sm-block", alt: ''
@@ -8,10 +7,6 @@
.title
= link_to gitlab_snippet_path(snippet) do
= snippet.title
- - if file_name.present?
- %span.snippet-filename.d-none.d-sm-inline-block.ml-2
- = sprite_icon('doc-code', size: 16, css_class: 'file-icon align-text-bottom')
- = file_name
%ul.controls
%li
diff --git a/app/views/shared/tokens/_scopes_form.html.haml b/app/views/shared/tokens/_scopes_form.html.haml
index a5d3e1c8de0..82e32597c94 100644
--- a/app/views/shared/tokens/_scopes_form.html.haml
+++ b/app/views/shared/tokens/_scopes_form.html.haml
@@ -6,4 +6,4 @@
%fieldset.form-group.form-check
= check_box_tag "#{prefix}[scopes][]", scope, token.scopes.include?(scope), id: "#{prefix}_scopes_#{scope}", class: "form-check-input qa-#{scope}-radio"
= label_tag ("#{prefix}_scopes_#{scope}"), scope, class: 'label-bold form-check-label'
- .text-secondary= t scope, scope: [:doorkeeper, :scope_desc]
+ .text-secondary= t scope, scope: scope_description(prefix)
diff --git a/app/views/snippets/show.html.haml b/app/views/snippets/show.html.haml
index 741e38e3d84..819f02b78fe 100644
--- a/app/views/snippets/show.html.haml
+++ b/app/views/snippets/show.html.haml
@@ -4,7 +4,7 @@
- breadcrumb_title @snippet.to_reference
- page_title "#{@snippet.title} (#{@snippet.to_reference})", _("Snippets")
-- if Feature.enabled?(:snippets_vue)
+- if Feature.enabled?(:snippets_vue, default_enabled: true)
#js-snippet-view{ data: {'qa-selector': 'snippet_view', 'snippet-gid': @snippet.to_global_id} }
- else
= render 'shared/snippets/header'
diff --git a/app/views/users/_deletion_guidance.html.haml b/app/views/users/_deletion_guidance.html.haml
index 0545cab538c..7169aebea74 100644
--- a/app/views/users/_deletion_guidance.html.haml
+++ b/app/views/users/_deletion_guidance.html.haml
@@ -3,8 +3,9 @@
%ul
%li
%p
- Certain user content will be moved to a system-wide "Ghost User" in order to maintain content for posterity. For further information, please refer to the
- = link_to 'user account deletion documentation.', help_page_path("user/profile/account/delete_account", anchor: "associated-records")
+ - link_start = '<a href="%{url}">'.html_safe % { url: help_page_path("user/profile/account/delete_account", anchor: "associated-records") }
+ = _('Certain user content will be moved to a system-wide "Ghost User" in order to maintain content for posterity. For further information, please refer to the %{link_start}user account deletion documentation.%{link_end}').html_safe % { link_start: link_start, link_end: '</a>'.html_safe }
- personal_projects_count = user.personal_projects.count
- unless personal_projects_count.zero?
- %li #{pluralize(personal_projects_count, 'personal project')} will be removed and cannot be restored
+ %li
+ = n_('personal project will be removed and cannot be restored', '%d personal projects will be removed and cannot be restored', personal_projects_count)
diff --git a/app/views/users/calendar_activities.html.haml b/app/views/users/calendar_activities.html.haml
index 7516dfe1602..a5197a9950b 100644
--- a/app/views/users/calendar_activities.html.haml
+++ b/app/views/users/calendar_activities.html.haml
@@ -22,14 +22,14 @@
- elsif event.target
= link_to event.target.to_reference, [event.project.namespace.becomes(Namespace), event.project, event.target], class: 'has-tooltip', title: event.target_title
- at
+ = s_('UserProfile|at')
%strong
- if event.project
= link_to_project(event.project)
- else
= event.resource_parent_name
- else
- made a private contribution
+ = s_('UserProfile|made a private contribution')
- else
%p
= _('No contributions were found')
diff --git a/app/workers/all_queues.yml b/app/workers/all_queues.yml
index 57d41bfaec2..1f9a53d64d9 100644
--- a/app/workers/all_queues.yml
+++ b/app/workers/all_queues.yml
@@ -3,6 +3,20 @@
#
# Do not edit it manually!
---
+- :name: authorized_project_update:authorized_project_update_project_create
+ :feature_category: :authentication_and_authorization
+ :has_external_dependencies:
+ :urgency: :low
+ :resource_boundary: :unknown
+ :weight: 1
+ :idempotent: true
+- :name: authorized_project_update:authorized_project_update_user_refresh_with_low_urgency
+ :feature_category: :authentication_and_authorization
+ :has_external_dependencies:
+ :urgency: :low
+ :resource_boundary: :unknown
+ :weight: 1
+ :idempotent: true
- :name: auto_devops:auto_devops_disable
:feature_category: :auto_devops
:has_external_dependencies:
@@ -18,35 +32,35 @@
:weight: 3
:idempotent:
- :name: chaos:chaos_cpu_spin
- :feature_category: :chaos_engineering
+ :feature_category: :not_owned
:has_external_dependencies:
:urgency: :low
:resource_boundary: :unknown
:weight: 2
:idempotent:
- :name: chaos:chaos_db_spin
- :feature_category: :chaos_engineering
+ :feature_category: :not_owned
:has_external_dependencies:
:urgency: :low
:resource_boundary: :unknown
:weight: 2
:idempotent:
- :name: chaos:chaos_kill
- :feature_category: :chaos_engineering
+ :feature_category: :not_owned
:has_external_dependencies:
:urgency: :low
:resource_boundary: :unknown
:weight: 2
:idempotent:
- :name: chaos:chaos_leak_mem
- :feature_category: :chaos_engineering
+ :feature_category: :not_owned
:has_external_dependencies:
:urgency: :low
:resource_boundary: :unknown
:weight: 2
:idempotent:
- :name: chaos:chaos_sleep
- :feature_category: :chaos_engineering
+ :feature_category: :not_owned
:has_external_dependencies:
:urgency: :low
:resource_boundary: :unknown
@@ -269,6 +283,13 @@
:resource_boundary: :unknown
:weight: 1
:idempotent:
+- :name: cronjob:x509_issuer_crl_check
+ :feature_category: :source_code_management
+ :has_external_dependencies: true
+ :urgency: :low
+ :resource_boundary: :unknown
+ :weight: 1
+ :idempotent: true
- :name: deployment:deployments_finished
:feature_category: :continuous_delivery
:has_external_dependencies:
@@ -290,13 +311,6 @@
:resource_boundary: :cpu
:weight: 3
:idempotent:
-- :name: gcp_cluster:cluster_configure
- :feature_category: :kubernetes_management
- :has_external_dependencies:
- :urgency: :low
- :resource_boundary: :unknown
- :weight: 1
- :idempotent:
- :name: gcp_cluster:cluster_configure_istio
:feature_category: :kubernetes_management
:has_external_dependencies: true
@@ -318,13 +332,6 @@
:resource_boundary: :unknown
:weight: 1
:idempotent:
-- :name: gcp_cluster:cluster_project_configure
- :feature_category: :kubernetes_management
- :has_external_dependencies: true
- :urgency: :low
- :resource_boundary: :unknown
- :weight: 1
- :idempotent:
- :name: gcp_cluster:cluster_provision
:feature_category: :kubernetes_management
:has_external_dependencies: true
@@ -689,7 +696,7 @@
:resource_boundary: :unknown
:weight: 1
:idempotent:
-- :name: pipeline_background:ci_daily_report_results
+- :name: pipeline_background:ci_daily_build_group_report_results
:feature_category: :continuous_integration
:has_external_dependencies:
:urgency: :low
@@ -849,14 +856,14 @@
:urgency: :high
:resource_boundary: :unknown
:weight: 5
- :idempotent:
+ :idempotent: true
- :name: pipeline_processing:update_head_pipeline_for_merge_request
:feature_category: :continuous_integration
:has_external_dependencies:
:urgency: :high
:resource_boundary: :cpu
:weight: 5
- :idempotent:
+ :idempotent: true
- :name: repository_check:repository_check_batch
:feature_category: :source_code_management
:has_external_dependencies:
@@ -961,7 +968,7 @@
:urgency: :low
:resource_boundary: :unknown
:weight: 2
- :idempotent:
+ :idempotent: true
- :name: create_evidence
:feature_category: :release_evidence
:has_external_dependencies:
@@ -1011,6 +1018,13 @@
:resource_boundary: :unknown
:weight: 1
:idempotent:
+- :name: design_management_new_version
+ :feature_category: :design_management
+ :has_external_dependencies:
+ :urgency: :low
+ :resource_boundary: :memory
+ :weight: 1
+ :idempotent:
- :name: detect_repository_languages
:feature_category: :source_code_management
:has_external_dependencies:
@@ -1053,6 +1067,13 @@
:resource_boundary: :cpu
:weight: 1
:idempotent:
+- :name: external_service_reactive_caching
+ :feature_category: :not_owned
+ :has_external_dependencies: true
+ :urgency: :low
+ :resource_boundary: :unknown
+ :weight: 1
+ :idempotent:
- :name: file_hook
:feature_category: :integrations
:has_external_dependencies:
@@ -1143,7 +1164,7 @@
:urgency: :low
:resource_boundary: :unknown
:weight: 1
- :idempotent:
+ :idempotent: true
- :name: migrate_external_diffs
:feature_category: :source_code_management
:has_external_dependencies:
@@ -1220,7 +1241,7 @@
:urgency: :high
:resource_boundary: :unknown
:weight: 3
- :idempotent:
+ :idempotent: true
- :name: project_cache
:feature_category: :source_code_management
:has_external_dependencies:
@@ -1280,7 +1301,7 @@
- :name: reactive_caching
:feature_category: :not_owned
:has_external_dependencies:
- :urgency: :high
+ :urgency: :low
:resource_boundary: :cpu
:weight: 1
:idempotent:
diff --git a/app/workers/authorized_project_update/project_create_worker.rb b/app/workers/authorized_project_update/project_create_worker.rb
new file mode 100644
index 00000000000..651849b57ec
--- /dev/null
+++ b/app/workers/authorized_project_update/project_create_worker.rb
@@ -0,0 +1,19 @@
+# frozen_string_literal: true
+
+module AuthorizedProjectUpdate
+ class ProjectCreateWorker
+ include ApplicationWorker
+
+ feature_category :authentication_and_authorization
+ urgency :low
+ queue_namespace :authorized_project_update
+
+ idempotent!
+
+ def perform(project_id)
+ project = Project.find(project_id)
+
+ AuthorizedProjectUpdate::ProjectCreateService.new(project).execute
+ end
+ end
+end
diff --git a/app/workers/authorized_project_update/user_refresh_with_low_urgency_worker.rb b/app/workers/authorized_project_update/user_refresh_with_low_urgency_worker.rb
new file mode 100644
index 00000000000..19038cb8900
--- /dev/null
+++ b/app/workers/authorized_project_update/user_refresh_with_low_urgency_worker.rb
@@ -0,0 +1,11 @@
+# frozen_string_literal: true
+
+module AuthorizedProjectUpdate
+ class UserRefreshWithLowUrgencyWorker < ::AuthorizedProjectsWorker
+ feature_category :authentication_and_authorization
+ urgency :low
+ queue_namespace :authorized_project_update
+
+ idempotent!
+ end
+end
diff --git a/app/workers/ci/daily_build_group_report_results_worker.rb b/app/workers/ci/daily_build_group_report_results_worker.rb
new file mode 100644
index 00000000000..a6d3c485e24
--- /dev/null
+++ b/app/workers/ci/daily_build_group_report_results_worker.rb
@@ -0,0 +1,16 @@
+# frozen_string_literal: true
+
+module Ci
+ class DailyBuildGroupReportResultsWorker
+ include ApplicationWorker
+ include PipelineBackgroundQueue
+
+ idempotent!
+
+ def perform(pipeline_id)
+ Ci::Pipeline.find_by_id(pipeline_id).try do |pipeline|
+ Ci::DailyBuildGroupReportResultService.new.execute(pipeline)
+ end
+ end
+ end
+end
diff --git a/app/workers/ci/daily_report_results_worker.rb b/app/workers/ci/daily_report_results_worker.rb
deleted file mode 100644
index 314fd44f86c..00000000000
--- a/app/workers/ci/daily_report_results_worker.rb
+++ /dev/null
@@ -1,16 +0,0 @@
-# frozen_string_literal: true
-
-module Ci
- class DailyReportResultsWorker
- include ApplicationWorker
- include PipelineBackgroundQueue
-
- idempotent!
-
- def perform(pipeline_id)
- Ci::Pipeline.find_by_id(pipeline_id).try do |pipeline|
- Ci::DailyReportResultService.new.execute(pipeline)
- end
- end
- end
-end
diff --git a/app/workers/cluster_configure_worker.rb b/app/workers/cluster_configure_worker.rb
deleted file mode 100644
index f9364ab7144..00000000000
--- a/app/workers/cluster_configure_worker.rb
+++ /dev/null
@@ -1,10 +0,0 @@
-# frozen_string_literal: true
-
-class ClusterConfigureWorker # rubocop:disable Scalability/IdempotentWorker
- include ApplicationWorker
- include ClusterQueue
-
- def perform(cluster_id)
- # Scheduled for removal in https://gitlab.com/gitlab-org/gitlab-foss/issues/59319
- end
-end
diff --git a/app/workers/cluster_project_configure_worker.rb b/app/workers/cluster_project_configure_worker.rb
deleted file mode 100644
index b68df01dc7a..00000000000
--- a/app/workers/cluster_project_configure_worker.rb
+++ /dev/null
@@ -1,12 +0,0 @@
-# frozen_string_literal: true
-
-class ClusterProjectConfigureWorker # rubocop:disable Scalability/IdempotentWorker
- include ApplicationWorker
- include ClusterQueue
-
- worker_has_external_dependencies!
-
- def perform(project_id)
- # Scheduled for removal in https://gitlab.com/gitlab-org/gitlab-foss/issues/59319
- end
-end
diff --git a/app/workers/concerns/application_worker.rb b/app/workers/concerns/application_worker.rb
index c0062780688..7ab9a0c2a02 100644
--- a/app/workers/concerns/application_worker.rb
+++ b/app/workers/concerns/application_worker.rb
@@ -11,6 +11,8 @@ module ApplicationWorker
include WorkerAttributes
include WorkerContext
+ LOGGING_EXTRA_KEY = 'extra'
+
included do
set_queue
@@ -24,6 +26,21 @@ module ApplicationWorker
payload.stringify_keys.merge(context)
end
+
+ def log_extra_metadata_on_done(key, value)
+ @done_log_extra_metadata ||= {}
+ @done_log_extra_metadata[key] = value
+ end
+
+ def logging_extras
+ return {} unless @done_log_extra_metadata
+
+ # Prefix keys with class name to avoid conflicts in Elasticsearch types.
+ # Also prefix with "extra." so that we know to log these new fields.
+ @done_log_extra_metadata.transform_keys do |k|
+ "#{LOGGING_EXTRA_KEY}.#{self.class.name.gsub("::", "_").underscore}.#{k}"
+ end
+ end
end
class_methods do
diff --git a/app/workers/concerns/chaos_queue.rb b/app/workers/concerns/chaos_queue.rb
index c5db10491f2..a9c557f0175 100644
--- a/app/workers/concerns/chaos_queue.rb
+++ b/app/workers/concerns/chaos_queue.rb
@@ -5,6 +5,6 @@ module ChaosQueue
included do
queue_namespace :chaos
- feature_category :chaos_engineering
+ feature_category_not_owned!
end
end
diff --git a/app/workers/concerns/reactive_cacheable_worker.rb b/app/workers/concerns/reactive_cacheable_worker.rb
new file mode 100644
index 00000000000..e73707c2b43
--- /dev/null
+++ b/app/workers/concerns/reactive_cacheable_worker.rb
@@ -0,0 +1,33 @@
+# frozen_string_literal: true
+
+module ReactiveCacheableWorker
+ extend ActiveSupport::Concern
+
+ included do
+ include ApplicationWorker
+
+ feature_category_not_owned!
+
+ def self.context_for_arguments(arguments)
+ class_name, *_other_args = arguments
+ Gitlab::ApplicationContext.new(related_class: class_name.to_s)
+ end
+ end
+
+ def perform(class_name, id, *args)
+ klass = begin
+ class_name.constantize
+ rescue NameError
+ nil
+ end
+
+ return unless klass
+
+ klass
+ .reactive_cache_worker_finder
+ .call(id, *args)
+ .try(:exclusively_update_reactive_cache!, *args)
+ rescue ReactiveCaching::ExceededReactiveCacheLimit => e
+ Gitlab::ErrorTracking.track_exception(e)
+ end
+end
diff --git a/app/workers/create_commit_signature_worker.rb b/app/workers/create_commit_signature_worker.rb
index 9cbc75f8944..a88d2bf7d15 100644
--- a/app/workers/create_commit_signature_worker.rb
+++ b/app/workers/create_commit_signature_worker.rb
@@ -1,11 +1,13 @@
# frozen_string_literal: true
-class CreateCommitSignatureWorker # rubocop:disable Scalability/IdempotentWorker
+class CreateCommitSignatureWorker
include ApplicationWorker
feature_category :source_code_management
weight 2
+ idempotent!
+
# rubocop: disable CodeReuse/ActiveRecord
def perform(commit_shas, project_id)
# Older versions of Git::BranchPushService may push a single commit ID on
diff --git a/app/workers/design_management/new_version_worker.rb b/app/workers/design_management/new_version_worker.rb
new file mode 100644
index 00000000000..3634dcbcebd
--- /dev/null
+++ b/app/workers/design_management/new_version_worker.rb
@@ -0,0 +1,31 @@
+# frozen_string_literal: true
+
+module DesignManagement
+ class NewVersionWorker # rubocop:disable Scalability/IdempotentWorker
+ include ApplicationWorker
+
+ feature_category :design_management
+ # Declare this worker as memory bound due to
+ # `GenerateImageVersionsService` resizing designs
+ worker_resource_boundary :memory
+
+ def perform(version_id)
+ version = DesignManagement::Version.find(version_id)
+
+ add_system_note(version)
+ generate_image_versions(version)
+ rescue ActiveRecord::RecordNotFound => e
+ Sidekiq.logger.warn(e)
+ end
+
+ private
+
+ def add_system_note(version)
+ SystemNoteService.design_version_added(version)
+ end
+
+ def generate_image_versions(version)
+ DesignManagement::GenerateImageVersionsService.new(version).execute
+ end
+ end
+end
diff --git a/app/workers/external_service_reactive_caching_worker.rb b/app/workers/external_service_reactive_caching_worker.rb
new file mode 100644
index 00000000000..e3104b44a7f
--- /dev/null
+++ b/app/workers/external_service_reactive_caching_worker.rb
@@ -0,0 +1,7 @@
+# frozen_string_literal: true
+
+class ExternalServiceReactiveCachingWorker # rubocop:disable Scalability/IdempotentWorker
+ include ReactiveCacheableWorker
+
+ worker_has_external_dependencies!
+end
diff --git a/app/workers/gitlab/jira_import/import_issue_worker.rb b/app/workers/gitlab/jira_import/import_issue_worker.rb
index 7ace0a35fd9..78de5cf1307 100644
--- a/app/workers/gitlab/jira_import/import_issue_worker.rb
+++ b/app/workers/gitlab/jira_import/import_issue_worker.rb
@@ -28,19 +28,35 @@ module Gitlab
private
def create_issue(issue_attributes, project_id)
+ label_ids = issue_attributes.delete('label_ids')
+ assignee_ids = issue_attributes.delete('assignee_ids')
issue_id = insert_and_return_id(issue_attributes, Issue)
- label_issue(project_id, issue_id)
+ label_issue(project_id, issue_id, label_ids)
+ assign_issue(project_id, issue_id, assignee_ids)
issue_id
end
- def label_issue(project_id, issue_id)
- label_id = JiraImport.get_import_label_id(project_id)
- return unless label_id
+ def label_issue(project_id, issue_id, label_ids)
+ label_link_attrs = label_ids.to_a.map do |label_id|
+ build_label_attrs(issue_id, label_id.to_i)
+ end
- label_link_attrs = build_label_attrs(issue_id, label_id.to_i)
- insert_and_return_id(label_link_attrs, LabelLink)
+ import_label_id = JiraImport.get_import_label_id(project_id)
+ return unless import_label_id
+
+ label_link_attrs << build_label_attrs(issue_id, import_label_id.to_i)
+
+ Gitlab::Database.bulk_insert(LabelLink.table_name, label_link_attrs)
+ end
+
+ def assign_issue(project_id, issue_id, assignee_ids)
+ return if assignee_ids.blank?
+
+ assignee_attrs = assignee_ids.map { |user_id| { issue_id: issue_id, user_id: user_id } }
+
+ Gitlab::Database.bulk_insert(IssueAssignee.table_name, assignee_attrs)
end
def build_label_attrs(issue_id, label_id)
diff --git a/app/workers/group_import_worker.rb b/app/workers/group_import_worker.rb
index b6fc5afc28c..d8f236013bf 100644
--- a/app/workers/group_import_worker.rb
+++ b/app/workers/group_import_worker.rb
@@ -2,14 +2,23 @@
class GroupImportWorker # rubocop:disable Scalability/IdempotentWorker
include ApplicationWorker
- include ExceptionBacktrace
+ sidekiq_options retry: false
feature_category :importers
def perform(user_id, group_id)
current_user = User.find(user_id)
group = Group.find(group_id)
+ group_import = group.build_import_state(jid: self.jid)
+
+ group_import.start!
::Groups::ImportExport::ImportService.new(group: group, user: current_user).execute
+
+ group_import.finish!
+ rescue StandardError => e
+ group_import&.fail_op(e.message)
+
+ raise e
end
end
diff --git a/app/workers/incident_management/process_alert_worker.rb b/app/workers/incident_management/process_alert_worker.rb
index 8d4294cc231..2ce9fe359b5 100644
--- a/app/workers/incident_management/process_alert_worker.rb
+++ b/app/workers/incident_management/process_alert_worker.rb
@@ -7,11 +7,14 @@ module IncidentManagement
queue_namespace :incident_management
feature_category :incident_management
- def perform(project_id, alert)
+ def perform(project_id, alert_payload, am_alert_id = nil)
project = find_project(project_id)
return unless project
- create_issue(project, alert)
+ new_issue = create_issue(project, alert_payload)
+ return unless am_alert_id && new_issue.persisted?
+
+ link_issue_with_alert(am_alert_id, new_issue.id)
end
private
@@ -20,10 +23,24 @@ module IncidentManagement
Project.find_by_id(project_id)
end
- def create_issue(project, alert)
+ def create_issue(project, alert_payload)
IncidentManagement::CreateIssueService
- .new(project, alert)
+ .new(project, alert_payload)
.execute
end
+
+ def link_issue_with_alert(alert_id, issue_id)
+ alert = AlertManagement::Alert.find_by_id(alert_id)
+ return unless alert
+
+ return if alert.update(issue_id: issue_id)
+
+ Gitlab::AppLogger.warn(
+ message: 'Cannot link an Issue with Alert',
+ issue_id: issue_id,
+ alert_id: alert_id,
+ alert_errors: alert.errors.messages
+ )
+ end
end
end
diff --git a/app/workers/irker_worker.rb b/app/workers/irker_worker.rb
index 73bc050d7be..7622f40a949 100644
--- a/app/workers/irker_worker.rb
+++ b/app/workers/irker_worker.rb
@@ -53,7 +53,7 @@ class IrkerWorker # rubocop:disable Scalability/IdempotentWorker
def sendtoirker(privmsg)
to_send = { to: @channels, privmsg: privmsg }
- @socket.puts JSON.dump(to_send)
+ @socket.puts Gitlab::Json.dump(to_send)
end
def close_connection
diff --git a/app/workers/merge_request_mergeability_check_worker.rb b/app/workers/merge_request_mergeability_check_worker.rb
index a26c1a886f6..1a84efb4e52 100644
--- a/app/workers/merge_request_mergeability_check_worker.rb
+++ b/app/workers/merge_request_mergeability_check_worker.rb
@@ -1,9 +1,10 @@
# frozen_string_literal: true
-class MergeRequestMergeabilityCheckWorker # rubocop:disable Scalability/IdempotentWorker
+class MergeRequestMergeabilityCheckWorker
include ApplicationWorker
feature_category :source_code_management
+ idempotent!
def perform(merge_request_id)
merge_request = MergeRequest.find_by_id(merge_request_id)
diff --git a/app/workers/new_release_worker.rb b/app/workers/new_release_worker.rb
index 3c19e5f3d2b..fa4703d10f2 100644
--- a/app/workers/new_release_worker.rb
+++ b/app/workers/new_release_worker.rb
@@ -1,5 +1,7 @@
# frozen_string_literal: true
+# TODO: Worker can be removed in 13.2:
+# https://gitlab.com/gitlab-org/gitlab/-/issues/218231
class NewReleaseWorker # rubocop:disable Scalability/IdempotentWorker
include ApplicationWorker
diff --git a/app/workers/pages_domain_ssl_renewal_cron_worker.rb b/app/workers/pages_domain_ssl_renewal_cron_worker.rb
index 43fb35c5298..fe6d516d3cf 100644
--- a/app/workers/pages_domain_ssl_renewal_cron_worker.rb
+++ b/app/workers/pages_domain_ssl_renewal_cron_worker.rb
@@ -10,11 +10,6 @@ class PagesDomainSslRenewalCronWorker # rubocop:disable Scalability/IdempotentWo
return unless ::Gitlab::LetsEncrypt.enabled?
PagesDomain.need_auto_ssl_renewal.with_logging_info.find_each do |domain|
- # Ideally that should be handled in PagesDomain.need_auto_ssl_renewal scope
- # but it's hard to make scope work with feature flags
- # once we remove feature flag we can modify scope to implement this behaviour
- next if Feature.enabled?(:pages_letsencrypt_errors, domain.project) && domain.auto_ssl_failed
-
with_context(project: domain.project) do
PagesDomainSslRenewalWorker.perform_async(domain.id)
end
diff --git a/app/workers/process_commit_worker.rb b/app/workers/process_commit_worker.rb
index 9960e812a2f..bdfabea8938 100644
--- a/app/workers/process_commit_worker.rb
+++ b/app/workers/process_commit_worker.rb
@@ -7,13 +7,15 @@
# result of this the workload of this worker should be kept to a bare minimum.
# Consider using an extra worker if you need to add any extra (and potentially
# slow) processing of commits.
-class ProcessCommitWorker # rubocop:disable Scalability/IdempotentWorker
+class ProcessCommitWorker
include ApplicationWorker
feature_category :source_code_management
urgency :high
weight 3
+ idempotent!
+
# project_id - The ID of the project this commit belongs to.
# user_id - The ID of the user that pushed the commit.
# commit_hash - Hash containing commit details to use for constructing a
diff --git a/app/workers/project_update_repository_storage_worker.rb b/app/workers/project_update_repository_storage_worker.rb
index ecee33e6421..5c1a8062f12 100644
--- a/app/workers/project_update_repository_storage_worker.rb
+++ b/app/workers/project_update_repository_storage_worker.rb
@@ -5,9 +5,19 @@ class ProjectUpdateRepositoryStorageWorker # rubocop:disable Scalability/Idempot
feature_category :gitaly
- def perform(project_id, new_repository_storage_key)
- project = Project.find(project_id)
+ def perform(project_id, new_repository_storage_key, repository_storage_move_id = nil)
+ repository_storage_move =
+ if repository_storage_move_id
+ ProjectRepositoryStorageMove.find(repository_storage_move_id)
+ else
+ # maintain compatibility with workers queued before release
+ project = Project.find(project_id)
+ project.repository_storage_moves.create!(
+ source_storage_name: project.repository_storage,
+ destination_storage_name: new_repository_storage_key
+ )
+ end
- ::Projects::UpdateRepositoryStorageService.new(project).execute(new_repository_storage_key)
+ ::Projects::UpdateRepositoryStorageService.new(repository_storage_move).execute
end
end
diff --git a/app/workers/reactive_caching_worker.rb b/app/workers/reactive_caching_worker.rb
index 513033281e5..a0829c31280 100644
--- a/app/workers/reactive_caching_worker.rb
+++ b/app/workers/reactive_caching_worker.rb
@@ -1,36 +1,8 @@
# frozen_string_literal: true
class ReactiveCachingWorker # rubocop:disable Scalability/IdempotentWorker
- include ApplicationWorker
+ include ReactiveCacheableWorker
- feature_category_not_owned!
-
- # TODO: The reactive caching worker should be split into
- # two different workers, one for high urgency jobs without external dependencies
- # and another worker without high urgency, but with external dependencies
- # https://gitlab.com/gitlab-com/gl-infra/scalability/issues/34
- # This worker should also have `worker_has_external_dependencies!` enabled
- urgency :high
+ urgency :low
worker_resource_boundary :cpu
-
- def self.context_for_arguments(arguments)
- class_name, *_other_args = arguments
- Gitlab::ApplicationContext.new(related_class: class_name.to_s)
- end
-
- def perform(class_name, id, *args)
- klass = begin
- class_name.constantize
- rescue NameError
- nil
- end
- return unless klass
-
- klass
- .reactive_cache_worker_finder
- .call(id, *args)
- .try(:exclusively_update_reactive_cache!, *args)
- rescue ReactiveCaching::ExceededReactiveCacheLimit => e
- Gitlab::ErrorTracking.track_exception(e)
- end
end
diff --git a/app/workers/stage_update_worker.rb b/app/workers/stage_update_worker.rb
index aface8288e3..20db19536c3 100644
--- a/app/workers/stage_update_worker.rb
+++ b/app/workers/stage_update_worker.rb
@@ -1,12 +1,14 @@
# frozen_string_literal: true
-class StageUpdateWorker # rubocop:disable Scalability/IdempotentWorker
+class StageUpdateWorker
include ApplicationWorker
include PipelineQueue
queue_namespace :pipeline_processing
urgency :high
+ idempotent!
+
def perform(stage_id)
Ci::Stage.find_by_id(stage_id)&.update_legacy_status
end
diff --git a/app/workers/update_head_pipeline_for_merge_request_worker.rb b/app/workers/update_head_pipeline_for_merge_request_worker.rb
index 69698ba81bd..63d11d33283 100644
--- a/app/workers/update_head_pipeline_for_merge_request_worker.rb
+++ b/app/workers/update_head_pipeline_for_merge_request_worker.rb
@@ -1,6 +1,6 @@
# frozen_string_literal: true
-class UpdateHeadPipelineForMergeRequestWorker # rubocop:disable Scalability/IdempotentWorker
+class UpdateHeadPipelineForMergeRequestWorker
include ApplicationWorker
include PipelineQueue
@@ -9,6 +9,8 @@ class UpdateHeadPipelineForMergeRequestWorker # rubocop:disable Scalability/Idem
urgency :high
worker_resource_boundary :cpu
+ idempotent!
+
def perform(merge_request_id)
MergeRequest.find_by_id(merge_request_id).try do |merge_request|
merge_request.update_head_pipeline
diff --git a/app/workers/x509_issuer_crl_check_worker.rb b/app/workers/x509_issuer_crl_check_worker.rb
new file mode 100644
index 00000000000..5fc92da803c
--- /dev/null
+++ b/app/workers/x509_issuer_crl_check_worker.rb
@@ -0,0 +1,76 @@
+# frozen_string_literal: true
+
+class X509IssuerCrlCheckWorker
+ include ApplicationWorker
+ include CronjobQueue
+
+ feature_category :source_code_management
+ urgency :low
+
+ idempotent!
+ worker_has_external_dependencies!
+
+ attr_accessor :logger
+
+ def perform
+ @logger = Gitlab::GitLogger.build
+
+ X509Issuer.all.find_each do |issuer|
+ with_context(related_class: X509IssuerCrlCheckWorker) do
+ update_certificates(issuer)
+ end
+ end
+ end
+
+ private
+
+ def update_certificates(issuer)
+ crl = download_crl(issuer)
+ return unless crl
+
+ serials = X509Certificate.serial_numbers(issuer)
+ return if serials.empty?
+
+ revoked_serials = serials & crl.revoked.map(&:serial).map(&:to_i)
+
+ revoked_serials.each_slice(1000) do |batch|
+ certs = issuer.x509_certificates.where(serial_number: batch, certificate_status: :good) # rubocop: disable CodeReuse/ActiveRecord
+
+ certs.find_each do |cert|
+ logger.info(message: "Certificate revoked",
+ id: cert.id,
+ email: cert.email,
+ subject: cert.subject,
+ serial_number: cert.serial_number,
+ issuer: cert.x509_issuer.id,
+ issuer_subject: cert.x509_issuer.subject,
+ issuer_crl_url: cert.x509_issuer.crl_url)
+ end
+
+ certs.update_all(certificate_status: :revoked)
+ end
+ end
+
+ def download_crl(issuer)
+ response = Gitlab::HTTP.try_get(issuer.crl_url)
+
+ if response&.code == 200
+ OpenSSL::X509::CRL.new(response.body)
+ else
+ logger.warn(message: "Failed to download certificate revocation list",
+ issuer: issuer.id,
+ issuer_subject: issuer.subject,
+ issuer_crl_url: issuer.crl_url)
+
+ nil
+ end
+
+ rescue OpenSSL::X509::CRLError
+ logger.warn(message: "Failed to parse certificate revocation list",
+ issuer: issuer.id,
+ issuer_subject: issuer.subject,
+ issuer_crl_url: issuer.crl_url)
+
+ nil
+ end
+end
diff --git a/bin/background_jobs b/bin/background_jobs
index 598d36abde6..866f5c39cd6 100755
--- a/bin/background_jobs
+++ b/bin/background_jobs
@@ -1,4 +1,4 @@
-#!/bin/bash
+#!/usr/bin/env bash
cd $(dirname $0)/..
diff --git a/bin/background_jobs_sk b/bin/background_jobs_sk
index 131cfe116ff..fb7de0a6180 100755
--- a/bin/background_jobs_sk
+++ b/bin/background_jobs_sk
@@ -1,4 +1,4 @@
-#!/bin/bash
+#!/usr/bin/env bash
cd $(dirname $0)/..
app_root=$(pwd)
diff --git a/bin/background_jobs_sk_cluster b/bin/background_jobs_sk_cluster
index 1982fd0810d..b1d5fce204e 100755
--- a/bin/background_jobs_sk_cluster
+++ b/bin/background_jobs_sk_cluster
@@ -1,4 +1,4 @@
-#!/bin/bash
+#!/usr/bin/env bash
cd $(dirname $0)/..
app_root=$(pwd)
diff --git a/cable/config.ru b/cable/config.ru
index 3b93c483ded..a528672ce25 100644
--- a/cable/config.ru
+++ b/cable/config.ru
@@ -3,4 +3,6 @@
require ::File.expand_path('../../config/environment', __FILE__)
Rails.application.eager_load!
+ACTION_CABLE_SERVER = true
+
run ActionCable.server
diff --git a/changelogs/add-name-parameter-to-project-environments-api.yml b/changelogs/add-name-parameter-to-project-environments-api.yml
deleted file mode 100644
index 01d456eb75c..00000000000
--- a/changelogs/add-name-parameter-to-project-environments-api.yml
+++ /dev/null
@@ -1,5 +0,0 @@
----
-title: Add `name` and `search` parameters to project environments API
-merge_request: 29385
-author: Lee Tickett
-type: added
diff --git a/changelogs/unreleased/10343-remove-a-lonely-dot-in-batch-comments.yml b/changelogs/unreleased/10343-remove-a-lonely-dot-in-batch-comments.yml
new file mode 100644
index 00000000000..f741179ab92
--- /dev/null
+++ b/changelogs/unreleased/10343-remove-a-lonely-dot-in-batch-comments.yml
@@ -0,0 +1,5 @@
+---
+title: Remove a lonely dot in Batch Comments.
+merge_request: 31783
+author: Gilang Gumilar
+type: changed
diff --git a/changelogs/unreleased/118609-design-comment-edit-comment-text.yml b/changelogs/unreleased/118609-design-comment-edit-comment-text.yml
new file mode 100644
index 00000000000..d5fdc2ca274
--- /dev/null
+++ b/changelogs/unreleased/118609-design-comment-edit-comment-text.yml
@@ -0,0 +1,5 @@
+---
+title: 'Resolve Design Comment: Edit Comment text'
+merge_request: 30479
+author:
+type: added
diff --git a/changelogs/unreleased/14108-instance-level-ci-variables-api.yml b/changelogs/unreleased/14108-instance-level-ci-variables-api.yml
new file mode 100644
index 00000000000..3c4288d2a5d
--- /dev/null
+++ b/changelogs/unreleased/14108-instance-level-ci-variables-api.yml
@@ -0,0 +1,5 @@
+---
+title: Add API CRUD actions for instance-level CI/CD variables
+merge_request: 31342
+author:
+type: added
diff --git a/changelogs/unreleased/14108-instance-level-ci-variables-controller.yml b/changelogs/unreleased/14108-instance-level-ci-variables-controller.yml
new file mode 100644
index 00000000000..a95a6d3b9c8
--- /dev/null
+++ b/changelogs/unreleased/14108-instance-level-ci-variables-controller.yml
@@ -0,0 +1,5 @@
+---
+title: Add admin controller actions for interacting with instance variables
+merge_request: 30385
+author:
+type: added
diff --git a/changelogs/unreleased/14108-instance-level-ci-variables-logic.yml b/changelogs/unreleased/14108-instance-level-ci-variables-logic.yml
new file mode 100644
index 00000000000..e8efce3ef64
--- /dev/null
+++ b/changelogs/unreleased/14108-instance-level-ci-variables-logic.yml
@@ -0,0 +1,5 @@
+---
+title: Integrate CI instance variables in the build process
+merge_request: 30186
+author:
+type: added
diff --git a/changelogs/unreleased/14108-instance-level-ci-variables.yml b/changelogs/unreleased/14108-instance-level-ci-variables.yml
new file mode 100644
index 00000000000..16de371282c
--- /dev/null
+++ b/changelogs/unreleased/14108-instance-level-ci-variables.yml
@@ -0,0 +1,5 @@
+---
+title: Add migrations for global CI variables
+merge_request: 30156
+author:
+type: added
diff --git a/changelogs/unreleased/1600-remove-jenkinsdeprecatedservice.yml b/changelogs/unreleased/1600-remove-jenkinsdeprecatedservice.yml
new file mode 100644
index 00000000000..60086adf60a
--- /dev/null
+++ b/changelogs/unreleased/1600-remove-jenkinsdeprecatedservice.yml
@@ -0,0 +1,5 @@
+---
+title: Remove JenkinsDeprecatedService
+merge_request: 31607
+author: tnwx
+type: removed
diff --git a/changelogs/unreleased/195887-jira-comment-details-column-migration.yml b/changelogs/unreleased/195887-jira-comment-details-column-migration.yml
new file mode 100644
index 00000000000..0135e0c0c4d
--- /dev/null
+++ b/changelogs/unreleased/195887-jira-comment-details-column-migration.yml
@@ -0,0 +1,5 @@
+---
+title: Add comment_detail column to services
+merge_request: 29891
+author:
+type: added
diff --git a/changelogs/unreleased/195887-jira-comment-details-include-branch-name.yml b/changelogs/unreleased/195887-jira-comment-details-include-branch-name.yml
new file mode 100644
index 00000000000..8a5721cc596
--- /dev/null
+++ b/changelogs/unreleased/195887-jira-comment-details-include-branch-name.yml
@@ -0,0 +1,5 @@
+---
+title: Update Jira comment to include more information
+merge_request: 30258
+author:
+type: added
diff --git a/changelogs/unreleased/195889-integrations-list-views-should-use-recommended-page-layout.yml b/changelogs/unreleased/195889-integrations-list-views-should-use-recommended-page-layout.yml
new file mode 100644
index 00000000000..580c970ad14
--- /dev/null
+++ b/changelogs/unreleased/195889-integrations-list-views-should-use-recommended-page-layout.yml
@@ -0,0 +1,5 @@
+---
+title: Apply shared integrations view to project level
+merge_request: 30971
+author:
+type: changed
diff --git a/changelogs/unreleased/196862-drop-plan-id.yml b/changelogs/unreleased/196862-drop-plan-id.yml
new file mode 100644
index 00000000000..e56ab11b117
--- /dev/null
+++ b/changelogs/unreleased/196862-drop-plan-id.yml
@@ -0,0 +1,5 @@
+---
+title: Remove namespaces.plan_id column
+merge_request: 30351
+author:
+type: other
diff --git a/changelogs/unreleased/197170-issues-api-endpoint-missmatch-web-ui.yml b/changelogs/unreleased/197170-issues-api-endpoint-missmatch-web-ui.yml
new file mode 100644
index 00000000000..3b0fcb5529d
--- /dev/null
+++ b/changelogs/unreleased/197170-issues-api-endpoint-missmatch-web-ui.yml
@@ -0,0 +1,5 @@
+---
+title: Add non_archived argument to issues API endpoint
+merge_request: 30381
+author:
+type: added
diff --git a/changelogs/unreleased/197344-use-cookies-with-metadata.yml b/changelogs/unreleased/197344-use-cookies-with-metadata.yml
new file mode 100644
index 00000000000..c07927ee6ad
--- /dev/null
+++ b/changelogs/unreleased/197344-use-cookies-with-metadata.yml
@@ -0,0 +1,5 @@
+---
+title: Use cookies with metadata to prevent reuse as another cookie
+merge_request: 31311
+author:
+type: other
diff --git a/changelogs/unreleased/198324-avoid_subqueries.yml b/changelogs/unreleased/198324-avoid_subqueries.yml
new file mode 100644
index 00000000000..4aa37fd4c0d
--- /dev/null
+++ b/changelogs/unreleased/198324-avoid_subqueries.yml
@@ -0,0 +1,5 @@
+---
+title: Speed up NOT Issue filters
+merge_request: 27639
+author:
+type: performance
diff --git a/changelogs/unreleased/198603-add-foreign-key-on-projects-namespace-id-and-clean-up-ghost-projec.yml b/changelogs/unreleased/198603-add-foreign-key-on-projects-namespace-id-and-clean-up-ghost-projec.yml
new file mode 100644
index 00000000000..87481ee524b
--- /dev/null
+++ b/changelogs/unreleased/198603-add-foreign-key-on-projects-namespace-id-and-clean-up-ghost-projec.yml
@@ -0,0 +1,5 @@
+---
+title: Add Foreign Key on projects.namespaces_id
+merge_request: 31675
+author:
+type: other
diff --git a/changelogs/unreleased/199046-text-for-future-release-date-grammatically-incorrect.yml b/changelogs/unreleased/199046-text-for-future-release-date-grammatically-incorrect.yml
new file mode 100644
index 00000000000..7cc1f6daa28
--- /dev/null
+++ b/changelogs/unreleased/199046-text-for-future-release-date-grammatically-incorrect.yml
@@ -0,0 +1,5 @@
+---
+title: Resolve Text for future Release date grammatically incorrect
+merge_request: 28075
+author:
+type: fixed
diff --git a/changelogs/unreleased/199053-inconsistent-help-icon-styling.yml b/changelogs/unreleased/199053-inconsistent-help-icon-styling.yml
new file mode 100644
index 00000000000..3d11351ca8b
--- /dev/null
+++ b/changelogs/unreleased/199053-inconsistent-help-icon-styling.yml
@@ -0,0 +1,5 @@
+---
+title: Update merge request widget question mark icons
+merge_request: 30759
+author:
+type: other
diff --git a/changelogs/unreleased/199428-update-the-main-left-side-navigation-for-the-package-area-to-bette.yml b/changelogs/unreleased/199428-update-the-main-left-side-navigation-for-the-package-area-to-bette.yml
new file mode 100644
index 00000000000..5352edfa4b0
--- /dev/null
+++ b/changelogs/unreleased/199428-update-the-main-left-side-navigation-for-the-package-area-to-bette.yml
@@ -0,0 +1,5 @@
+---
+title: Update sidebar packages name
+merge_request: 30712
+author:
+type: changed
diff --git a/changelogs/unreleased/199843-fix-viewing-nil-blobs.yml b/changelogs/unreleased/199843-fix-viewing-nil-blobs.yml
new file mode 100644
index 00000000000..61cedbf5ab2
--- /dev/null
+++ b/changelogs/unreleased/199843-fix-viewing-nil-blobs.yml
@@ -0,0 +1,5 @@
+---
+title: Fix display of some overflowing merge request diffs
+merge_request: 29267
+author:
+type: fixed
diff --git a/changelogs/unreleased/199912-ff-enable-by-default.yml b/changelogs/unreleased/199912-ff-enable-by-default.yml
new file mode 100644
index 00000000000..829c36b8aeb
--- /dev/null
+++ b/changelogs/unreleased/199912-ff-enable-by-default.yml
@@ -0,0 +1,5 @@
+---
+title: Enable async_merge_request_check_mergeability by default
+merge_request: 31196
+author:
+type: other
diff --git a/changelogs/unreleased/201872-partitioning-implement-cascading-deletes-without-foreign-keys.yml b/changelogs/unreleased/201872-partitioning-implement-cascading-deletes-without-foreign-keys.yml
new file mode 100644
index 00000000000..0d242f5a0eb
--- /dev/null
+++ b/changelogs/unreleased/201872-partitioning-implement-cascading-deletes-without-foreign-keys.yml
@@ -0,0 +1,6 @@
+---
+title: Migration to add partitioned_foreign_keys table that tracks foreign keys for
+ partitioned tables
+merge_request: 29064
+author:
+type: added
diff --git a/changelogs/unreleased/201927-solarized-dark.yml b/changelogs/unreleased/201927-solarized-dark.yml
new file mode 100644
index 00000000000..54939ccadd4
--- /dev/null
+++ b/changelogs/unreleased/201927-solarized-dark.yml
@@ -0,0 +1,5 @@
+---
+title: Monokai and Solarized Dark syntax highlighting theme for Web IDE
+merge_request: 30931
+author:
+type: added
diff --git a/changelogs/unreleased/201930-webide-solarized.yml b/changelogs/unreleased/201930-webide-solarized.yml
new file mode 100644
index 00000000000..330deaf1a91
--- /dev/null
+++ b/changelogs/unreleased/201930-webide-solarized.yml
@@ -0,0 +1,5 @@
+---
+title: Solarized light syntax highlighting theme for the Web IDE
+merge_request: 30989
+author:
+type: added
diff --git a/changelogs/unreleased/202143-fix-not-enough-data-in-vsa.yml b/changelogs/unreleased/202143-fix-not-enough-data-in-vsa.yml
new file mode 100644
index 00000000000..6045e244d1a
--- /dev/null
+++ b/changelogs/unreleased/202143-fix-not-enough-data-in-vsa.yml
@@ -0,0 +1,6 @@
+---
+title: Fix 'not enough data' in Value Stream Analytics when low median values are
+ returned
+merge_request: 31315
+author:
+type: fixed
diff --git a/changelogs/unreleased/202525-add-test-report-api-route.yml b/changelogs/unreleased/202525-add-test-report-api-route.yml
new file mode 100644
index 00000000000..9e673f9b8e7
--- /dev/null
+++ b/changelogs/unreleased/202525-add-test-report-api-route.yml
@@ -0,0 +1,5 @@
+---
+title: Add test report API route
+merge_request: 24648
+author:
+type: added
diff --git a/changelogs/unreleased/204839-registry-application-settings.yml b/changelogs/unreleased/204839-registry-application-settings.yml
new file mode 100644
index 00000000000..a04fd2e756d
--- /dev/null
+++ b/changelogs/unreleased/204839-registry-application-settings.yml
@@ -0,0 +1,5 @@
+---
+title: Add container registry settings to application_settings table
+merge_request: 31125
+author:
+type: added
diff --git a/changelogs/unreleased/205513-create-subgroup-without-project-permission.yml b/changelogs/unreleased/205513-create-subgroup-without-project-permission.yml
new file mode 100644
index 00000000000..6b4cdc1cc4d
--- /dev/null
+++ b/changelogs/unreleased/205513-create-subgroup-without-project-permission.yml
@@ -0,0 +1,5 @@
+---
+title: Always display new subgroup button when permission is granted
+merge_request: 28309
+author: Mattias Michaux
+type: fixed
diff --git a/changelogs/unreleased/205570-sprint_initial_migrations.yml b/changelogs/unreleased/205570-sprint_initial_migrations.yml
new file mode 100644
index 00000000000..c368cde2673
--- /dev/null
+++ b/changelogs/unreleased/205570-sprint_initial_migrations.yml
@@ -0,0 +1,5 @@
+---
+title: Create Sprints table and barebones model
+merge_request: 30125
+author:
+type: added
diff --git a/changelogs/unreleased/205570-sprint_relationships.yml b/changelogs/unreleased/205570-sprint_relationships.yml
new file mode 100644
index 00000000000..c654cb36552
--- /dev/null
+++ b/changelogs/unreleased/205570-sprint_relationships.yml
@@ -0,0 +1,5 @@
+---
+title: Flesh out Sprints relationships and constraints
+merge_request: 30127
+author:
+type: added
diff --git a/changelogs/unreleased/207235-snippets-vue-ff-on-by-default.yml b/changelogs/unreleased/207235-snippets-vue-ff-on-by-default.yml
new file mode 100644
index 00000000000..352d80eaa93
--- /dev/null
+++ b/changelogs/unreleased/207235-snippets-vue-ff-on-by-default.yml
@@ -0,0 +1,5 @@
+---
+title: Refactored Snippet view to Vue
+merge_request: 31450
+author:
+type: added
diff --git a/changelogs/unreleased/207267-expiration-policy-copy.yml b/changelogs/unreleased/207267-expiration-policy-copy.yml
new file mode 100644
index 00000000000..9ddff8a76fb
--- /dev/null
+++ b/changelogs/unreleased/207267-expiration-policy-copy.yml
@@ -0,0 +1,5 @@
+---
+title: Update the example regex in the image expiration policy UI
+merge_request: 31104
+author:
+type: changed
diff --git a/changelogs/unreleased/207267-regex-keep-frontend.yml b/changelogs/unreleased/207267-regex-keep-frontend.yml
new file mode 100644
index 00000000000..8ff20c390eb
--- /dev/null
+++ b/changelogs/unreleased/207267-regex-keep-frontend.yml
@@ -0,0 +1,5 @@
+---
+title: Add new keep regex to expiration policy settings ui
+merge_request: 29940
+author:
+type: changed
diff --git a/changelogs/unreleased/207324-search-api-scoped-to-blobs-does-not-honor-per_page.yml b/changelogs/unreleased/207324-search-api-scoped-to-blobs-does-not-honor-per_page.yml
new file mode 100644
index 00000000000..e1e24bfdd7d
--- /dev/null
+++ b/changelogs/unreleased/207324-search-api-scoped-to-blobs-does-not-honor-per_page.yml
@@ -0,0 +1,5 @@
+---
+title: Honor per_page in Search API
+merge_request: 29197
+author:
+type: fixed
diff --git a/changelogs/unreleased/207471-expose-note-confidential-attribute-in-apis-and-display-on-frontend.yml b/changelogs/unreleased/207471-expose-note-confidential-attribute-in-apis-and-display-on-frontend.yml
new file mode 100644
index 00000000000..b4241587a4d
--- /dev/null
+++ b/changelogs/unreleased/207471-expose-note-confidential-attribute-in-apis-and-display-on-frontend.yml
@@ -0,0 +1,5 @@
+---
+title: Add confidential status support for comment and replies
+merge_request: 31622
+author:
+type: added
diff --git a/changelogs/unreleased/207934-snippet-embed-scrolling.yml b/changelogs/unreleased/207934-snippet-embed-scrolling.yml
new file mode 100644
index 00000000000..583b6067651
--- /dev/null
+++ b/changelogs/unreleased/207934-snippet-embed-scrolling.yml
@@ -0,0 +1,5 @@
+---
+title: Fix display of embedded snippets
+merge_request: 32411
+author: Jan Beckmann
+type: fixed
diff --git a/changelogs/unreleased/208132-add-ecs-to-autodeploy.yml b/changelogs/unreleased/208132-add-ecs-to-autodeploy.yml
new file mode 100644
index 00000000000..92f77c1fe86
--- /dev/null
+++ b/changelogs/unreleased/208132-add-ecs-to-autodeploy.yml
@@ -0,0 +1,5 @@
+---
+title: Add Deployment to ECS process to AutoDevOps
+merge_request: 29971
+author:
+type: added
diff --git a/changelogs/unreleased/208171-clicking-on-search-results-does-not-follow-link.yml b/changelogs/unreleased/208171-clicking-on-search-results-does-not-follow-link.yml
new file mode 100644
index 00000000000..0ef8666e10e
--- /dev/null
+++ b/changelogs/unreleased/208171-clicking-on-search-results-does-not-follow-link.yml
@@ -0,0 +1,5 @@
+---
+title: Fix an issue where the Search dropdown results would not be clickable.
+merge_request: 30087
+author: mbergeron
+type: fixed
diff --git a/changelogs/unreleased/208223-provider-icon-haml.yml b/changelogs/unreleased/208223-provider-icon-haml.yml
new file mode 100644
index 00000000000..54fb49ab90b
--- /dev/null
+++ b/changelogs/unreleased/208223-provider-icon-haml.yml
@@ -0,0 +1,5 @@
+---
+title: Added provider icon to cluster index display
+merge_request: 31134
+author:
+type: added
diff --git a/changelogs/unreleased/208250-collect-object-store-config-in-usage-data.yml b/changelogs/unreleased/208250-collect-object-store-config-in-usage-data.yml
new file mode 100644
index 00000000000..7ebf5f61f4b
--- /dev/null
+++ b/changelogs/unreleased/208250-collect-object-store-config-in-usage-data.yml
@@ -0,0 +1,5 @@
+---
+title: Collect object store config in usage data
+merge_request: 29149
+author:
+type: added
diff --git a/changelogs/unreleased/208255-editing-markdown-fields-don-t-work.yml b/changelogs/unreleased/208255-editing-markdown-fields-don-t-work.yml
new file mode 100644
index 00000000000..79425012b3f
--- /dev/null
+++ b/changelogs/unreleased/208255-editing-markdown-fields-don-t-work.yml
@@ -0,0 +1,5 @@
+---
+title: Fix updating of Markdown fields when Markdown cache version is incremented
+merge_request: 32219
+author:
+type: fixed
diff --git a/changelogs/unreleased/208715-bump-tslint-search-depth.yml b/changelogs/unreleased/208715-bump-tslint-search-depth.yml
new file mode 100644
index 00000000000..2fdb511ed5d
--- /dev/null
+++ b/changelogs/unreleased/208715-bump-tslint-search-depth.yml
@@ -0,0 +1,5 @@
+---
+title: Bump max search depth from 2 to 4 when looking for files SAST analyzers can handle
+merge_request: 29732
+author:
+type: fixed
diff --git a/changelogs/unreleased/208897-remove-bot-type-column.yml b/changelogs/unreleased/208897-remove-bot-type-column.yml
new file mode 100644
index 00000000000..109fedca0e0
--- /dev/null
+++ b/changelogs/unreleased/208897-remove-bot-type-column.yml
@@ -0,0 +1,5 @@
+---
+title: Remove obsolete bot_type column
+merge_request: 27076
+author:
+type: other
diff --git a/changelogs/unreleased/208920-jira-import-usage-data.yml b/changelogs/unreleased/208920-jira-import-usage-data.yml
new file mode 100644
index 00000000000..b52c91da212
--- /dev/null
+++ b/changelogs/unreleased/208920-jira-import-usage-data.yml
@@ -0,0 +1,5 @@
+---
+title: Add jira imports to usage data
+merge_request: 29925
+author:
+type: added
diff --git a/changelogs/unreleased/209808-webide-dark-theme.yml b/changelogs/unreleased/209808-webide-dark-theme.yml
new file mode 100644
index 00000000000..29b196d02c7
--- /dev/null
+++ b/changelogs/unreleased/209808-webide-dark-theme.yml
@@ -0,0 +1,5 @@
+---
+title: Add WebIDE Dark Theme Support
+merge_request: 28407
+author:
+type: added
diff --git a/changelogs/unreleased/210018-remove-admin-ability-not-to-use-hashed-storage.yml b/changelogs/unreleased/210018-remove-admin-ability-not-to-use-hashed-storage.yml
new file mode 100644
index 00000000000..19575ebb614
--- /dev/null
+++ b/changelogs/unreleased/210018-remove-admin-ability-not-to-use-hashed-storage.yml
@@ -0,0 +1,5 @@
+---
+title: Force hashed storage to always be enabled
+merge_request: 31801
+author:
+type: changed
diff --git a/changelogs/unreleased/210045-make-ci-ci-variables-protected-by-default.yml b/changelogs/unreleased/210045-make-ci-ci-variables-protected-by-default.yml
new file mode 100644
index 00000000000..f1ed15d1268
--- /dev/null
+++ b/changelogs/unreleased/210045-make-ci-ci-variables-protected-by-default.yml
@@ -0,0 +1,5 @@
+---
+title: Make protected_ci_variables setting enabled by default
+merge_request: 31715
+author:
+type: changed
diff --git a/changelogs/unreleased/210232-select-the-first-option-if-there-is-only-one-metric-option-on-a.yml b/changelogs/unreleased/210232-select-the-first-option-if-there-is-only-one-metric-option-on-a.yml
new file mode 100644
index 00000000000..5db852e1ded
--- /dev/null
+++ b/changelogs/unreleased/210232-select-the-first-option-if-there-is-only-one-metric-option-on-a.yml
@@ -0,0 +1,5 @@
+---
+title: Select the first option if there is only one metric option on alerts dropdown.
+merge_request: 29857
+author: Gilang Gumilar
+type: added
diff --git a/changelogs/unreleased/211460-annotations-clusters-endpoint.yml b/changelogs/unreleased/211460-annotations-clusters-endpoint.yml
new file mode 100644
index 00000000000..56d5bae3f26
--- /dev/null
+++ b/changelogs/unreleased/211460-annotations-clusters-endpoint.yml
@@ -0,0 +1,5 @@
+---
+title: Create cluster annotations API endpoint
+merge_request: 29502
+author:
+type: added
diff --git a/changelogs/unreleased/211461-create-annotations-graphql-endpoint.yml b/changelogs/unreleased/211461-create-annotations-graphql-endpoint.yml
new file mode 100644
index 00000000000..5a9bfb7a0fc
--- /dev/null
+++ b/changelogs/unreleased/211461-create-annotations-graphql-endpoint.yml
@@ -0,0 +1,5 @@
+---
+title: Create dashboard annotations via Graphql
+merge_request: 31249
+author:
+type: added
diff --git a/changelogs/unreleased/211519-hide-default-award-emojis.yml b/changelogs/unreleased/211519-hide-default-award-emojis.yml
new file mode 100644
index 00000000000..790b8344c45
--- /dev/null
+++ b/changelogs/unreleased/211519-hide-default-award-emojis.yml
@@ -0,0 +1,6 @@
+---
+title: Add option to hide the default "thumbs up" and "thumbs down" buttons on
+ issues, merge requests, and snippets.
+merge_request: 27734
+author: Steve Mokris
+type: added
diff --git a/changelogs/unreleased/211637-blob-path-name.yml b/changelogs/unreleased/211637-blob-path-name.yml
new file mode 100644
index 00000000000..be1579cfff0
--- /dev/null
+++ b/changelogs/unreleased/211637-blob-path-name.yml
@@ -0,0 +1,5 @@
+---
+title: Updated snippet view to show path instead of name for a blob
+merge_request: 30550
+author:
+type: changed
diff --git a/changelogs/unreleased/211944-provide-instance-level-setting-to-enable-or-disable-default-branch-add-policies.yml b/changelogs/unreleased/211944-provide-instance-level-setting-to-enable-or-disable-default-branch-add-policies.yml
new file mode 100644
index 00000000000..e8762cf6dcd
--- /dev/null
+++ b/changelogs/unreleased/211944-provide-instance-level-setting-to-enable-or-disable-default-branch-add-policies.yml
@@ -0,0 +1,5 @@
+---
+title: Add policies for managing 'default_branch_protection' setting in groups
+merge_request: 29879
+author:
+type: added
diff --git a/changelogs/unreleased/211984-jira-import-connection-test.yml b/changelogs/unreleased/211984-jira-import-connection-test.yml
new file mode 100644
index 00000000000..f2a9c362456
--- /dev/null
+++ b/changelogs/unreleased/211984-jira-import-connection-test.yml
@@ -0,0 +1,5 @@
+---
+title: Test Jira connection before running import
+merge_request: 29926
+author:
+type: changed
diff --git a/changelogs/unreleased/212213-cablett-allowlist-gitlab-team-member.yml b/changelogs/unreleased/212213-cablett-allowlist-gitlab-team-member.yml
new file mode 100644
index 00000000000..5f812369a74
--- /dev/null
+++ b/changelogs/unreleased/212213-cablett-allowlist-gitlab-team-member.yml
@@ -0,0 +1,5 @@
+---
+title: Skip spam check for GitLab team members on gitlab.com
+merge_request: 31052
+author:
+type: added
diff --git a/changelogs/unreleased/212279-webide-vue-files.yml b/changelogs/unreleased/212279-webide-vue-files.yml
new file mode 100644
index 00000000000..957c1642c0c
--- /dev/null
+++ b/changelogs/unreleased/212279-webide-vue-files.yml
@@ -0,0 +1,5 @@
+---
+title: 'Web IDE: Introduce syntax highlighting for .vue files.'
+merge_request: 30986
+author:
+type: added
diff --git a/changelogs/unreleased/212331-move-features-to-core-issue-board-focus-mode.yml b/changelogs/unreleased/212331-move-features-to-core-issue-board-focus-mode.yml
new file mode 100644
index 00000000000..436eef02d45
--- /dev/null
+++ b/changelogs/unreleased/212331-move-features-to-core-issue-board-focus-mode.yml
@@ -0,0 +1,5 @@
+---
+title: Moved issue board focus mode to Core and available for for everyone
+merge_request: 29200
+author:
+type: added
diff --git a/changelogs/unreleased/212470-change-text-on-self-managed-sign-on-page.yml b/changelogs/unreleased/212470-change-text-on-self-managed-sign-on-page.yml
new file mode 100644
index 00000000000..ec1742da241
--- /dev/null
+++ b/changelogs/unreleased/212470-change-text-on-self-managed-sign-on-page.yml
@@ -0,0 +1,5 @@
+---
+title: Update text on self-managed sign in page
+merge_request: 30135
+author:
+type: other
diff --git a/changelogs/unreleased/212549-padding-in-snippet-blob-header.yml b/changelogs/unreleased/212549-padding-in-snippet-blob-header.yml
new file mode 100644
index 00000000000..faf1bf279bd
--- /dev/null
+++ b/changelogs/unreleased/212549-padding-in-snippet-blob-header.yml
@@ -0,0 +1,5 @@
+---
+title: Fix minor spacing issue at Snippet blob viewer
+merge_request: 29625
+author: Karthick Venkatesan
+type: fixed
diff --git a/changelogs/unreleased/212592-disable-binary-edit.yml b/changelogs/unreleased/212592-disable-binary-edit.yml
new file mode 100644
index 00000000000..e115d75108e
--- /dev/null
+++ b/changelogs/unreleased/212592-disable-binary-edit.yml
@@ -0,0 +1,5 @@
+---
+title: Disabled Edit button for binary snippets
+merge_request: 30904
+author:
+type: added
diff --git a/changelogs/unreleased/212816-container-registry-missing-elipsis-on-tag-name.yml b/changelogs/unreleased/212816-container-registry-missing-elipsis-on-tag-name.yml
new file mode 100644
index 00000000000..1278b36115a
--- /dev/null
+++ b/changelogs/unreleased/212816-container-registry-missing-elipsis-on-tag-name.yml
@@ -0,0 +1,5 @@
+---
+title: Add elipsis to container registry tag name
+merge_request: 31584
+author:
+type: fixed
diff --git a/changelogs/unreleased/212948-jira-importer-labels.yml b/changelogs/unreleased/212948-jira-importer-labels.yml
new file mode 100644
index 00000000000..14270acaa23
--- /dev/null
+++ b/changelogs/unreleased/212948-jira-importer-labels.yml
@@ -0,0 +1,5 @@
+---
+title: Map labels from Jira to labels in GitLab
+merge_request: 29970
+author:
+type: added
diff --git a/changelogs/unreleased/213238-fix-ide-discard-empty-file.yml b/changelogs/unreleased/213238-fix-ide-discard-empty-file.yml
new file mode 100644
index 00000000000..1a8ac3400c5
--- /dev/null
+++ b/changelogs/unreleased/213238-fix-ide-discard-empty-file.yml
@@ -0,0 +1,5 @@
+---
+title: Fix discard button not showing for new empty files in Web IDE
+merge_request: 30767
+author:
+type: fixed
diff --git a/changelogs/unreleased/213282-add-support-for-vue-routes.yml b/changelogs/unreleased/213282-add-support-for-vue-routes.yml
new file mode 100644
index 00000000000..302f3a52100
--- /dev/null
+++ b/changelogs/unreleased/213282-add-support-for-vue-routes.yml
@@ -0,0 +1,5 @@
+---
+title: Add vue routes support to Static Site Editor
+merge_request: 30163
+author:
+type: added
diff --git a/changelogs/unreleased/213282-update-template-for-sse.yml b/changelogs/unreleased/213282-update-template-for-sse.yml
new file mode 100644
index 00000000000..ea17b46d75e
--- /dev/null
+++ b/changelogs/unreleased/213282-update-template-for-sse.yml
@@ -0,0 +1,5 @@
+---
+title: Update the template for Static Site Editor / Middleman
+merge_request: 30642
+author:
+type: other
diff --git a/changelogs/unreleased/213324-fix-table-colors.yml b/changelogs/unreleased/213324-fix-table-colors.yml
new file mode 100644
index 00000000000..55410d9d303
--- /dev/null
+++ b/changelogs/unreleased/213324-fix-table-colors.yml
@@ -0,0 +1,5 @@
+---
+title: Increase constrast ratio of text in some tables
+merge_request: 30903
+author:
+type: fixed
diff --git a/changelogs/unreleased/213341-project-snippet-delete-redirect.yml b/changelogs/unreleased/213341-project-snippet-delete-redirect.yml
new file mode 100644
index 00000000000..0587892a3de
--- /dev/null
+++ b/changelogs/unreleased/213341-project-snippet-delete-redirect.yml
@@ -0,0 +1,5 @@
+---
+title: Fixed redirection when deleting a project snippet
+merge_request: 31709
+author:
+type: fixed
diff --git a/changelogs/unreleased/213382-use-not-valid-to-immediately-enforce-a-not-null-constraint.yml b/changelogs/unreleased/213382-use-not-valid-to-immediately-enforce-a-not-null-constraint.yml
new file mode 100644
index 00000000000..f3091832880
--- /dev/null
+++ b/changelogs/unreleased/213382-use-not-valid-to-immediately-enforce-a-not-null-constraint.yml
@@ -0,0 +1,5 @@
+---
+title: Use NOT VALID to enforce a not null constraint on file store columns
+merge_request: 31261
+author:
+type: performance
diff --git a/changelogs/unreleased/213392-pagination-blue-background.yml b/changelogs/unreleased/213392-pagination-blue-background.yml
new file mode 100644
index 00000000000..14cb71f4716
--- /dev/null
+++ b/changelogs/unreleased/213392-pagination-blue-background.yml
@@ -0,0 +1,5 @@
+---
+title: Apply active class on active link element in HAML pagination
+merge_request: 31396
+author:
+type: other
diff --git a/changelogs/unreleased/213424-update-info-text-when-export-is-started.yml b/changelogs/unreleased/213424-update-info-text-when-export-is-started.yml
new file mode 100644
index 00000000000..42580a8b5e4
--- /dev/null
+++ b/changelogs/unreleased/213424-update-info-text-when-export-is-started.yml
@@ -0,0 +1,5 @@
+---
+title: Update group and project export info messages
+merge_request: 31981
+author: briankabiro
+type: other
diff --git a/changelogs/unreleased/213473-move-custom-metric-alerts.yml b/changelogs/unreleased/213473-move-custom-metric-alerts.yml
new file mode 100644
index 00000000000..fbdf20a7e75
--- /dev/null
+++ b/changelogs/unreleased/213473-move-custom-metric-alerts.yml
@@ -0,0 +1,5 @@
+---
+title: Make setting alerts on the monitoring dashboard available to GitLab Core users
+merge_request: 29789
+author:
+type: changed
diff --git a/changelogs/unreleased/213531-fix-init-d-web-server-pid.yml b/changelogs/unreleased/213531-fix-init-d-web-server-pid.yml
new file mode 100644
index 00000000000..2e4167cf495
--- /dev/null
+++ b/changelogs/unreleased/213531-fix-init-d-web-server-pid.yml
@@ -0,0 +1,5 @@
+---
+title: Fix init.d script to correctly set web server PID
+merge_request: 29164
+author:
+type: fixed
diff --git a/changelogs/unreleased/213556-move-the-hashed-storage-checks-to-gitlab-app-check-task-instead-of.yml b/changelogs/unreleased/213556-move-the-hashed-storage-checks-to-gitlab-app-check-task-instead-of.yml
new file mode 100644
index 00000000000..2975df9e275
--- /dev/null
+++ b/changelogs/unreleased/213556-move-the-hashed-storage-checks-to-gitlab-app-check-task-instead-of.yml
@@ -0,0 +1,5 @@
+---
+title: app:gitlab:check rake task now warns when projects are not in hashed storage
+merge_request: 31172
+author:
+type: changed
diff --git a/changelogs/unreleased/213566-deploy-token-basic-auth.yml b/changelogs/unreleased/213566-deploy-token-basic-auth.yml
new file mode 100644
index 00000000000..d8f87924ea8
--- /dev/null
+++ b/changelogs/unreleased/213566-deploy-token-basic-auth.yml
@@ -0,0 +1,6 @@
+---
+title: Deploy tokens can be used in the API with Basic Auth Headers enabling NuGet
+ and PyPI to be used with deploy tokens
+merge_request: 31035
+author:
+type: added
diff --git a/changelogs/unreleased/213566-deploy-token-conan.yml b/changelogs/unreleased/213566-deploy-token-conan.yml
new file mode 100644
index 00000000000..7f06fa3d7f7
--- /dev/null
+++ b/changelogs/unreleased/213566-deploy-token-conan.yml
@@ -0,0 +1,5 @@
+---
+title: Conan registry is accessible using deploy tokens
+merge_request: 31114
+author:
+type: added
diff --git a/changelogs/unreleased/213566-deploy-token-npm.yml b/changelogs/unreleased/213566-deploy-token-npm.yml
new file mode 100644
index 00000000000..14af30a049a
--- /dev/null
+++ b/changelogs/unreleased/213566-deploy-token-npm.yml
@@ -0,0 +1,5 @@
+---
+title: Enable deploy token authentication for the NPM registry
+merge_request: 31264
+author:
+type: added
diff --git a/changelogs/unreleased/213566-package-deploy-token-auth.yml b/changelogs/unreleased/213566-package-deploy-token-auth.yml
new file mode 100644
index 00000000000..01cfdc7aaa0
--- /dev/null
+++ b/changelogs/unreleased/213566-package-deploy-token-auth.yml
@@ -0,0 +1,5 @@
+---
+title: Deploy token authentication for API with Maven endpoints
+merge_request: 30332
+author:
+type: added
diff --git a/changelogs/unreleased/213566-package-deploy-tokens.yml b/changelogs/unreleased/213566-package-deploy-tokens.yml
new file mode 100644
index 00000000000..1301935eba5
--- /dev/null
+++ b/changelogs/unreleased/213566-package-deploy-tokens.yml
@@ -0,0 +1,5 @@
+---
+title: Add read/write_package_registry to deploy_tokens
+merge_request: 29383
+author:
+type: added
diff --git a/changelogs/unreleased/213571-drop-lm-artifact-from-ci.yml b/changelogs/unreleased/213571-drop-lm-artifact-from-ci.yml
new file mode 100644
index 00000000000..5dc7def6dc0
--- /dev/null
+++ b/changelogs/unreleased/213571-drop-lm-artifact-from-ci.yml
@@ -0,0 +1,5 @@
+---
+title: Drop support for `license_management` artifact
+merge_request: 31247
+author:
+type: removed
diff --git a/changelogs/unreleased/213572-drop-lm-template-support.yml b/changelogs/unreleased/213572-drop-lm-template-support.yml
new file mode 100644
index 00000000000..4d6163bf1a3
--- /dev/null
+++ b/changelogs/unreleased/213572-drop-lm-template-support.yml
@@ -0,0 +1,5 @@
+---
+title: Drop support for License-Management CI template
+merge_request: 30645
+author:
+type: removed
diff --git a/changelogs/unreleased/213652-don-t-lose-user-content-in-wiki.yml b/changelogs/unreleased/213652-don-t-lose-user-content-in-wiki.yml
new file mode 100644
index 00000000000..519bc8a2e58
--- /dev/null
+++ b/changelogs/unreleased/213652-don-t-lose-user-content-in-wiki.yml
@@ -0,0 +1,5 @@
+---
+title: Warn user before losing wiki content
+merge_request: 30037
+author:
+type: fixed
diff --git a/changelogs/unreleased/213678-API-documentation.yml b/changelogs/unreleased/213678-API-documentation.yml
new file mode 100644
index 00000000000..2fb155b70db
--- /dev/null
+++ b/changelogs/unreleased/213678-API-documentation.yml
@@ -0,0 +1,5 @@
+---
+title: Remove deprecated Release Evidence endpoints documentation
+merge_request: 30978
+author:
+type: removed
diff --git a/changelogs/unreleased/213678-remove-deprecated-release-evidence-endpoints.yml b/changelogs/unreleased/213678-remove-deprecated-release-evidence-endpoints.yml
new file mode 100644
index 00000000000..80bb68c4e85
--- /dev/null
+++ b/changelogs/unreleased/213678-remove-deprecated-release-evidence-endpoints.yml
@@ -0,0 +1,5 @@
+---
+title: Remove deprecated Release Evidence endpoints
+merge_request: 30975
+author:
+type: removed
diff --git a/changelogs/unreleased/213808-add-scheduled-at-field-to-jira-imports.yml b/changelogs/unreleased/213808-add-scheduled-at-field-to-jira-imports.yml
new file mode 100644
index 00000000000..29de6311997
--- /dev/null
+++ b/changelogs/unreleased/213808-add-scheduled-at-field-to-jira-imports.yml
@@ -0,0 +1,5 @@
+---
+title: Add scheduled_at field to jira_imports table
+merge_request: 30284
+author:
+type: added
diff --git a/changelogs/unreleased/213853-client-side-evaluation-to-live-preview-app-updates.yml b/changelogs/unreleased/213853-client-side-evaluation-to-live-preview-app-updates.yml
new file mode 100644
index 00000000000..8361d6d0951
--- /dev/null
+++ b/changelogs/unreleased/213853-client-side-evaluation-to-live-preview-app-updates.yml
@@ -0,0 +1,5 @@
+---
+title: "Rename Client Side Evaluation to Live Preview"
+merge_request: 30309
+author:
+type: changed
diff --git a/changelogs/unreleased/213876.yml b/changelogs/unreleased/213876.yml
new file mode 100644
index 00000000000..eb3fa811e6d
--- /dev/null
+++ b/changelogs/unreleased/213876.yml
@@ -0,0 +1,5 @@
+---
+title: Surface alerts add sidebar link
+merge_request: 29775
+author:
+type: changed
diff --git a/changelogs/unreleased/213877.yml b/changelogs/unreleased/213877.yml
new file mode 100644
index 00000000000..eac1a38cce2
--- /dev/null
+++ b/changelogs/unreleased/213877.yml
@@ -0,0 +1,5 @@
+---
+title: Surface alerts add empty state
+merge_request: 29775
+author:
+type: added
diff --git a/changelogs/unreleased/213880-alert-management-list-graphql.yml b/changelogs/unreleased/213880-alert-management-list-graphql.yml
new file mode 100644
index 00000000000..db8247fab8a
--- /dev/null
+++ b/changelogs/unreleased/213880-alert-management-list-graphql.yml
@@ -0,0 +1,5 @@
+---
+title: Add GraphQL type for reading Alert Management Alerts
+merge_request: 30140
+author:
+type: added
diff --git a/changelogs/unreleased/213884-alert-management-plain-text-search.yml b/changelogs/unreleased/213884-alert-management-plain-text-search.yml
new file mode 100644
index 00000000000..3bef818d616
--- /dev/null
+++ b/changelogs/unreleased/213884-alert-management-plain-text-search.yml
@@ -0,0 +1,5 @@
+---
+title: Add search to Alert Management Alerts GraphQL query
+merge_request: 32047
+author:
+type: added
diff --git a/changelogs/unreleased/213890-alerts-loading-state.yml b/changelogs/unreleased/213890-alerts-loading-state.yml
new file mode 100644
index 00000000000..a8a1d686e6e
--- /dev/null
+++ b/changelogs/unreleased/213890-alerts-loading-state.yml
@@ -0,0 +1,5 @@
+---
+title: Alerts list loading & error state
+merge_request: 30315
+author:
+type: added
diff --git a/changelogs/unreleased/213892-Alerts-empty-state.yml b/changelogs/unreleased/213892-Alerts-empty-state.yml
new file mode 100644
index 00000000000..5421297c324
--- /dev/null
+++ b/changelogs/unreleased/213892-Alerts-empty-state.yml
@@ -0,0 +1,5 @@
+---
+title: Empty state for alerts list
+merge_request: 30215
+author:
+type: added
diff --git a/changelogs/unreleased/213894-northstar-metric-monitor-health.yml b/changelogs/unreleased/213894-northstar-metric-monitor-health.yml
new file mode 100644
index 00000000000..ae9072bc959
--- /dev/null
+++ b/changelogs/unreleased/213894-northstar-metric-monitor-health.yml
@@ -0,0 +1,5 @@
+---
+title: Add incident_labeled_issues to usage ping
+merge_request: 31406
+author:
+type: added
diff --git a/changelogs/unreleased/214007-example-expiration-regex.yml b/changelogs/unreleased/214007-example-expiration-regex.yml
new file mode 100644
index 00000000000..43db738ed0f
--- /dev/null
+++ b/changelogs/unreleased/214007-example-expiration-regex.yml
@@ -0,0 +1,5 @@
+---
+title: Update the example regex in the image expiration policy UI
+merge_request: 29348
+author:
+type: changed
diff --git a/changelogs/unreleased/214065-increase-width-of-label-column-on-labels-page.yml b/changelogs/unreleased/214065-increase-width-of-label-column-on-labels-page.yml
new file mode 100644
index 00000000000..0e4299a6749
--- /dev/null
+++ b/changelogs/unreleased/214065-increase-width-of-label-column-on-labels-page.yml
@@ -0,0 +1,5 @@
+---
+title: Increase label list label column width
+merge_request: 29963
+author:
+type: other
diff --git a/changelogs/unreleased/214242-remove-deprecated-buttons-in-release-page.yml b/changelogs/unreleased/214242-remove-deprecated-buttons-in-release-page.yml
new file mode 100644
index 00000000000..760b7fff331
--- /dev/null
+++ b/changelogs/unreleased/214242-remove-deprecated-buttons-in-release-page.yml
@@ -0,0 +1,5 @@
+---
+title: Updated deprecated buttons in release page
+merge_request: 30941
+author: Özgür Adem Işıklı @iozguradem
+type: added
diff --git a/changelogs/unreleased/214243.yml b/changelogs/unreleased/214243.yml
new file mode 100644
index 00000000000..5b26cac461a
--- /dev/null
+++ b/changelogs/unreleased/214243.yml
@@ -0,0 +1,5 @@
+---
+title: Format the alert payload into a table view
+merge_request: 32423
+author:
+type: changed
diff --git a/changelogs/unreleased/214283-add-updated-at-to-todos-api.yml b/changelogs/unreleased/214283-add-updated-at-to-todos-api.yml
new file mode 100644
index 00000000000..42f2d3a6615
--- /dev/null
+++ b/changelogs/unreleased/214283-add-updated-at-to-todos-api.yml
@@ -0,0 +1,5 @@
+---
+title: Expose the updated_at attribute in the todos API
+merge_request: 30035
+author:
+type: changed
diff --git a/changelogs/unreleased/214301-change-placeholder-in-search-input-for-analytics-features.yml b/changelogs/unreleased/214301-change-placeholder-in-search-input-for-analytics-features.yml
new file mode 100644
index 00000000000..c8221c710b2
--- /dev/null
+++ b/changelogs/unreleased/214301-change-placeholder-in-search-input-for-analytics-features.yml
@@ -0,0 +1,5 @@
+---
+title: Change placeholder in search input for Analytics features.
+merge_request: 29858
+author: Gilang Gumilar
+type: changed
diff --git a/changelogs/unreleased/214322-remove-token-from-runners-api.yml b/changelogs/unreleased/214322-remove-token-from-runners-api.yml
new file mode 100644
index 00000000000..8ee10b43e74
--- /dev/null
+++ b/changelogs/unreleased/214322-remove-token-from-runners-api.yml
@@ -0,0 +1,5 @@
+---
+title: Remove token attribute from Runners API
+merge_request: 31448
+author:
+type: removed
diff --git a/changelogs/unreleased/214382-remove-project_list_show_issue_count-feature-flag.yml b/changelogs/unreleased/214382-remove-project_list_show_issue_count-feature-flag.yml
new file mode 100644
index 00000000000..463e697d344
--- /dev/null
+++ b/changelogs/unreleased/214382-remove-project_list_show_issue_count-feature-flag.yml
@@ -0,0 +1,5 @@
+---
+title: Remove project_list_show_issue_count feature flag.
+merge_request: 31793
+author: Gilang Gumilar
+type: removed
diff --git a/changelogs/unreleased/214382-remove-project_list_show_mr_count-feature-flag.yml b/changelogs/unreleased/214382-remove-project_list_show_mr_count-feature-flag.yml
new file mode 100644
index 00000000000..459d6342680
--- /dev/null
+++ b/changelogs/unreleased/214382-remove-project_list_show_mr_count-feature-flag.yml
@@ -0,0 +1,5 @@
+---
+title: Remove project_list_show_mr_count feature flag.
+merge_request: 31789
+author: Gilang Gumilar
+type: removed
diff --git a/changelogs/unreleased/214382-remove-registrations_recaptcha-feature-flag.yml b/changelogs/unreleased/214382-remove-registrations_recaptcha-feature-flag.yml
new file mode 100644
index 00000000000..a5b0fe5dc0c
--- /dev/null
+++ b/changelogs/unreleased/214382-remove-registrations_recaptcha-feature-flag.yml
@@ -0,0 +1,5 @@
+---
+title: Remove registrations_recaptcha feature flag.
+merge_request: 31797
+author: Gilang Gumilar
+type: removed
diff --git a/changelogs/unreleased/214382-remove-set_user_last_activity-feature-flag.yml b/changelogs/unreleased/214382-remove-set_user_last_activity-feature-flag.yml
new file mode 100644
index 00000000000..1d3b40fe227
--- /dev/null
+++ b/changelogs/unreleased/214382-remove-set_user_last_activity-feature-flag.yml
@@ -0,0 +1,5 @@
+---
+title: Remove set_user_last_activity feature flag.
+merge_request: 31795
+author: Gilang Gumilar
+type: removed
diff --git a/changelogs/unreleased/214477-reactor-toast-to-alert-in-image-repository-details-page.yml b/changelogs/unreleased/214477-reactor-toast-to-alert-in-image-repository-details-page.yml
new file mode 100644
index 00000000000..a87cb780b8c
--- /dev/null
+++ b/changelogs/unreleased/214477-reactor-toast-to-alert-in-image-repository-details-page.yml
@@ -0,0 +1,5 @@
+---
+title: Use alerts instead of toasts in Image Repository details
+merge_request: 29685
+author:
+type: changed
diff --git a/changelogs/unreleased/214478-add-image-repository-search.yml b/changelogs/unreleased/214478-add-image-repository-search.yml
new file mode 100644
index 00000000000..6352c931393
--- /dev/null
+++ b/changelogs/unreleased/214478-add-image-repository-search.yml
@@ -0,0 +1,5 @@
+---
+title: Add search by name to registry image repositories
+merge_request: 29763
+author:
+type: added
diff --git a/changelogs/unreleased/214518-database-table-for-alert-management-alerts.yml b/changelogs/unreleased/214518-database-table-for-alert-management-alerts.yml
new file mode 100644
index 00000000000..f368c5d858f
--- /dev/null
+++ b/changelogs/unreleased/214518-database-table-for-alert-management-alerts.yml
@@ -0,0 +1,5 @@
+---
+title: Add table for Alert Management alerts
+merge_request: 29864
+author:
+type: added
diff --git a/changelogs/unreleased/214522-list.yml b/changelogs/unreleased/214522-list.yml
new file mode 100644
index 00000000000..f390c037c66
--- /dev/null
+++ b/changelogs/unreleased/214522-list.yml
@@ -0,0 +1,5 @@
+---
+title: Alert Management mobile styling
+merge_request: 31082
+author:
+type: other
diff --git a/changelogs/unreleased/214528.yml b/changelogs/unreleased/214528.yml
new file mode 100644
index 00000000000..3b85bf73552
--- /dev/null
+++ b/changelogs/unreleased/214528.yml
@@ -0,0 +1,5 @@
+---
+title: Add severity icons for alert management
+merge_request: 30472
+author:
+type: changed
diff --git a/changelogs/unreleased/214542-graphql-status-mutation.yml b/changelogs/unreleased/214542-graphql-status-mutation.yml
new file mode 100644
index 00000000000..4a8b4cca37a
--- /dev/null
+++ b/changelogs/unreleased/214542-graphql-status-mutation.yml
@@ -0,0 +1,5 @@
+---
+title: Add mutation for AlertManagement's Alert status
+merge_request: 30576
+author:
+type: added
diff --git a/changelogs/unreleased/214547_expose_web_url.yml b/changelogs/unreleased/214547_expose_web_url.yml
new file mode 100644
index 00000000000..20bc0aea35e
--- /dev/null
+++ b/changelogs/unreleased/214547_expose_web_url.yml
@@ -0,0 +1,5 @@
+---
+title: Add `web_url` to branch API response
+merge_request: 30147
+author:
+type: added
diff --git a/changelogs/unreleased/214581-star-dashboard-btn.yml b/changelogs/unreleased/214581-star-dashboard-btn.yml
new file mode 100644
index 00000000000..d2659d143f2
--- /dev/null
+++ b/changelogs/unreleased/214581-star-dashboard-btn.yml
@@ -0,0 +1,6 @@
+---
+title: Allow users to star/unstar dashboards which will appear at the top of their
+ dashboards options
+merge_request: 31597
+author:
+type: added
diff --git a/changelogs/unreleased/214582-revert-sort-order-change-BE.yml b/changelogs/unreleased/214582-revert-sort-order-change-BE.yml
new file mode 100644
index 00000000000..38757fbde1b
--- /dev/null
+++ b/changelogs/unreleased/214582-revert-sort-order-change-BE.yml
@@ -0,0 +1,5 @@
+---
+title: Restore original sort order of the metrics dashboard select list
+merge_request: 31859
+author:
+type: fixed
diff --git a/changelogs/unreleased/214582-show-starred-dashboards-in-metrics-dashboard-dropdown.yml b/changelogs/unreleased/214582-show-starred-dashboards-in-metrics-dashboard-dropdown.yml
new file mode 100644
index 00000000000..62338f3cb33
--- /dev/null
+++ b/changelogs/unreleased/214582-show-starred-dashboards-in-metrics-dashboard-dropdown.yml
@@ -0,0 +1,6 @@
+---
+title: Add database relation to preserve users starred
+ metrics dashboard information.
+merge_request: 29912
+author:
+type: added
diff --git a/changelogs/unreleased/214583-add-ability-to-query-projects.yml b/changelogs/unreleased/214583-add-ability-to-query-projects.yml
new file mode 100644
index 00000000000..57187afa250
--- /dev/null
+++ b/changelogs/unreleased/214583-add-ability-to-query-projects.yml
@@ -0,0 +1,5 @@
+---
+title: Add ability to query Projects using GraphQL API
+merge_request: 30146
+author:
+type: added
diff --git a/changelogs/unreleased/214678-add-an-api-for-flipper-percentage-of-actors-rollout.yml b/changelogs/unreleased/214678-add-an-api-for-flipper-percentage-of-actors-rollout.yml
new file mode 100644
index 00000000000..e6c47a67beb
--- /dev/null
+++ b/changelogs/unreleased/214678-add-an-api-for-flipper-percentage-of-actors-rollout.yml
@@ -0,0 +1,5 @@
+---
+title: Add percentage of actors feature flag rollout
+merge_request: 29698
+author:
+type: added
diff --git a/changelogs/unreleased/214710-rename-configure-to-enable.yml b/changelogs/unreleased/214710-rename-configure-to-enable.yml
new file mode 100644
index 00000000000..b68bd130323
--- /dev/null
+++ b/changelogs/unreleased/214710-rename-configure-to-enable.yml
@@ -0,0 +1,5 @@
+---
+title: Changed terminology of security scanner status from configure to enable
+merge_request: 31503
+author:
+type: changed
diff --git a/changelogs/unreleased/214773-metrics-dashboadr-heatmap-styles.yml b/changelogs/unreleased/214773-metrics-dashboadr-heatmap-styles.yml
new file mode 100644
index 00000000000..6f2a910e4e8
--- /dev/null
+++ b/changelogs/unreleased/214773-metrics-dashboadr-heatmap-styles.yml
@@ -0,0 +1,5 @@
+---
+title: Stretch heatmap metrics full column size
+merge_request: 30524
+author:
+type: fixed
diff --git a/changelogs/unreleased/214777-match-jira-keys-with-trailing-characters.yml b/changelogs/unreleased/214777-match-jira-keys-with-trailing-characters.yml
new file mode 100644
index 00000000000..7daa0ac0cf8
--- /dev/null
+++ b/changelogs/unreleased/214777-match-jira-keys-with-trailing-characters.yml
@@ -0,0 +1,5 @@
+---
+title: Match Jira keys with trailing characters
+merge_request: 29953
+author:
+type: fixed
diff --git a/changelogs/unreleased/214834-propagate-service-tempate.yml b/changelogs/unreleased/214834-propagate-service-tempate.yml
new file mode 100644
index 00000000000..58b00cf6b72
--- /dev/null
+++ b/changelogs/unreleased/214834-propagate-service-tempate.yml
@@ -0,0 +1,5 @@
+---
+title: Propagation of service templates also covers services with separate data tables.
+merge_request: 29805
+author:
+type: fixed
diff --git a/changelogs/unreleased/214882-esc-key-handler.yml b/changelogs/unreleased/214882-esc-key-handler.yml
new file mode 100644
index 00000000000..de593921806
--- /dev/null
+++ b/changelogs/unreleased/214882-esc-key-handler.yml
@@ -0,0 +1,6 @@
+---
+title: When viewing a single panel, return to a full dashboard by pressing the Escape
+ key
+merge_request: 30126
+author:
+type: added
diff --git a/changelogs/unreleased/214882-render-single-panel.yml b/changelogs/unreleased/214882-render-single-panel.yml
new file mode 100644
index 00000000000..841b3ec92a9
--- /dev/null
+++ b/changelogs/unreleased/214882-render-single-panel.yml
@@ -0,0 +1,5 @@
+---
+title: View a details of a panel in 'full screen mode'
+merge_request: 29902
+author:
+type: added
diff --git a/changelogs/unreleased/214983-remove_additional_rows_in_application_settings.yml b/changelogs/unreleased/214983-remove_additional_rows_in_application_settings.yml
new file mode 100644
index 00000000000..bea090b5039
--- /dev/null
+++ b/changelogs/unreleased/214983-remove_additional_rows_in_application_settings.yml
@@ -0,0 +1,5 @@
+---
+title: Delete orphaned rows in application_settings table
+merge_request: 29981
+author:
+type: performance
diff --git a/changelogs/unreleased/215121-add-wysiwyg-documentation.yml b/changelogs/unreleased/215121-add-wysiwyg-documentation.yml
new file mode 100644
index 00000000000..56470537230
--- /dev/null
+++ b/changelogs/unreleased/215121-add-wysiwyg-documentation.yml
@@ -0,0 +1,5 @@
+---
+title: Add WYSIWYG editor to the Static Site Editor
+merge_request: 31099
+author:
+type: added
diff --git a/changelogs/unreleased/215147-drop-feature-flag.yml b/changelogs/unreleased/215147-drop-feature-flag.yml
new file mode 100644
index 00000000000..1cdd219add0
--- /dev/null
+++ b/changelogs/unreleased/215147-drop-feature-flag.yml
@@ -0,0 +1,5 @@
+---
+title: 'Project import queries are now partially batched'
+merge_request: 30057
+author:
+type: performance
diff --git a/changelogs/unreleased/215164-show-correct-import-label-and-import-count-on-jira-import-form.yml b/changelogs/unreleased/215164-show-correct-import-label-and-import-count-on-jira-import-form.yml
new file mode 100644
index 00000000000..e5735296a52
--- /dev/null
+++ b/changelogs/unreleased/215164-show-correct-import-label-and-import-count-on-jira-import-form.yml
@@ -0,0 +1,5 @@
+---
+title: Show correct label and count on Jira import form
+merge_request: 30072
+author:
+type: changed
diff --git a/changelogs/unreleased/215308-follow-up-from-add-issues_create_limit-to-settings-api.yml b/changelogs/unreleased/215308-follow-up-from-add-issues_create_limit-to-settings-api.yml
new file mode 100644
index 00000000000..7173673b6b1
--- /dev/null
+++ b/changelogs/unreleased/215308-follow-up-from-add-issues_create_limit-to-settings-api.yml
@@ -0,0 +1,5 @@
+---
+title: Add raw_blob_request_limit to Application Settings API
+merge_request: 30211
+author:
+type: added
diff --git a/changelogs/unreleased/215326-moving-an-issue-referencing-a-group-in-a-note-can-fail.yml b/changelogs/unreleased/215326-moving-an-issue-referencing-a-group-in-a-note-can-fail.yml
new file mode 100644
index 00000000000..85faa2610c9
--- /dev/null
+++ b/changelogs/unreleased/215326-moving-an-issue-referencing-a-group-in-a-note-can-fail.yml
@@ -0,0 +1,5 @@
+---
+title: Fix moving an issue when there is a group reference
+merge_request: 30185
+author:
+type: fixed
diff --git a/changelogs/unreleased/215352-update-cancel-comment-note-text.yml b/changelogs/unreleased/215352-update-cancel-comment-note-text.yml
new file mode 100644
index 00000000000..7a9417e4295
--- /dev/null
+++ b/changelogs/unreleased/215352-update-cancel-comment-note-text.yml
@@ -0,0 +1,5 @@
+---
+title: Update the cancel comment note text to a less ambiguous statement.
+merge_request: 30189
+author:
+type: changed
diff --git a/changelogs/unreleased/215472-single-chart-from-url.yml b/changelogs/unreleased/215472-single-chart-from-url.yml
new file mode 100644
index 00000000000..f5fef9e76c3
--- /dev/null
+++ b/changelogs/unreleased/215472-single-chart-from-url.yml
@@ -0,0 +1,5 @@
+---
+title: Display expanded dashboard from a panel's "Link to chart" URL
+merge_request: 30476
+author:
+type: added
diff --git a/changelogs/unreleased/215473-url-update-single-panel.yml b/changelogs/unreleased/215473-url-update-single-panel.yml
new file mode 100644
index 00000000000..730c35ddce0
--- /dev/null
+++ b/changelogs/unreleased/215473-url-update-single-panel.yml
@@ -0,0 +1,5 @@
+---
+title: Update metrics dashboard url when a panel is expanded or contracted
+merge_request: 30704
+author:
+type: added
diff --git a/changelogs/unreleased/215542-add-jira-import-finished-alert.yml b/changelogs/unreleased/215542-add-jira-import-finished-alert.yml
new file mode 100644
index 00000000000..84f34c24682
--- /dev/null
+++ b/changelogs/unreleased/215542-add-jira-import-finished-alert.yml
@@ -0,0 +1,5 @@
+---
+title: Add alert on project issues page to show Jira import has finished
+merge_request: 31375
+author:
+type: added
diff --git a/changelogs/unreleased/215542-show-flash-alert-on-issues-page-which-displays-the-jira-import-sta.yml b/changelogs/unreleased/215542-show-flash-alert-on-issues-page-which-displays-the-jira-import-sta.yml
new file mode 100644
index 00000000000..9e19485bfb5
--- /dev/null
+++ b/changelogs/unreleased/215542-show-flash-alert-on-issues-page-which-displays-the-jira-import-sta.yml
@@ -0,0 +1,5 @@
+---
+title: Add alert on project issues page to show Jira import is in progress
+merge_request: 31329
+author:
+type: added
diff --git a/changelogs/unreleased/215563-migration-to-import-common-metrics.yml b/changelogs/unreleased/215563-migration-to-import-common-metrics.yml
new file mode 100644
index 00000000000..eef492ea1ac
--- /dev/null
+++ b/changelogs/unreleased/215563-migration-to-import-common-metrics.yml
@@ -0,0 +1,6 @@
+---
+title: Add migration to import changes to the system dashboard Prometheus queries
+ into DB
+merge_request: 31618
+author:
+type: changed
diff --git a/changelogs/unreleased/215563-remove-ruby-syntax.yml b/changelogs/unreleased/215563-remove-ruby-syntax.yml
new file mode 100644
index 00000000000..0a57c24d02c
--- /dev/null
+++ b/changelogs/unreleased/215563-remove-ruby-syntax.yml
@@ -0,0 +1,6 @@
+---
+title: Remove support for Ruby format variable interpolation (`%{variable}`) in custom
+ dashboards
+merge_request: 31581
+author:
+type: removed
diff --git a/changelogs/unreleased/215569-jira-import-fix-default-author.yml b/changelogs/unreleased/215569-jira-import-fix-default-author.yml
new file mode 100644
index 00000000000..4c7583c0958
--- /dev/null
+++ b/changelogs/unreleased/215569-jira-import-fix-default-author.yml
@@ -0,0 +1,5 @@
+---
+title: Use Jira import owner as the issue author when importing issues from Jira
+merge_request: 30504
+author:
+type: fixed
diff --git a/changelogs/unreleased/215598-alert-management-graphql-sort-backend.yml b/changelogs/unreleased/215598-alert-management-graphql-sort-backend.yml
new file mode 100644
index 00000000000..5ab1107ef32
--- /dev/null
+++ b/changelogs/unreleased/215598-alert-management-graphql-sort-backend.yml
@@ -0,0 +1,5 @@
+---
+title: Add sorting to AlertManagement Alert Graphql
+merge_request: 30964
+author:
+type: added
diff --git a/changelogs/unreleased/215617_mutation_to_add_a_new_branch.yml b/changelogs/unreleased/215617_mutation_to_add_a_new_branch.yml
new file mode 100644
index 00000000000..f452b1a8deb
--- /dev/null
+++ b/changelogs/unreleased/215617_mutation_to_add_a_new_branch.yml
@@ -0,0 +1,5 @@
+---
+title: Add mutation to create a new branch in GraphQL
+merge_request: 30388
+author:
+type: added
diff --git a/changelogs/unreleased/215700-fix-incorrect-commits-number-in-commits-list.yml b/changelogs/unreleased/215700-fix-incorrect-commits-number-in-commits-list.yml
new file mode 100644
index 00000000000..4da7a43050c
--- /dev/null
+++ b/changelogs/unreleased/215700-fix-incorrect-commits-number-in-commits-list.yml
@@ -0,0 +1,5 @@
+---
+title: Fix incorrect commits number in commits list
+merge_request: 30412
+author:
+type: fixed
diff --git a/changelogs/unreleased/215902-ci-job-jwt-handle-signing-key-issues.yml b/changelogs/unreleased/215902-ci-job-jwt-handle-signing-key-issues.yml
new file mode 100644
index 00000000000..8d4f48bffa7
--- /dev/null
+++ b/changelogs/unreleased/215902-ci-job-jwt-handle-signing-key-issues.yml
@@ -0,0 +1,5 @@
+---
+title: Handle possible RSA key exceptions when generating CI_JOB_JWT
+merge_request: 30702
+author:
+type: changed
diff --git a/changelogs/unreleased/215919-email-validation.yml b/changelogs/unreleased/215919-email-validation.yml
new file mode 100644
index 00000000000..19783ff8720
--- /dev/null
+++ b/changelogs/unreleased/215919-email-validation.yml
@@ -0,0 +1,5 @@
+---
+title: Change validation rules for profile email addresses
+merge_request: 30633
+author:
+type: fixed
diff --git a/changelogs/unreleased/215955-redirect-loop-when-logging-in-for-the-experimental-sign_up-flow.yml b/changelogs/unreleased/215955-redirect-loop-when-logging-in-for-the-experimental-sign_up-flow.yml
new file mode 100644
index 00000000000..c0a1a586d44
--- /dev/null
+++ b/changelogs/unreleased/215955-redirect-loop-when-logging-in-for-the-experimental-sign_up-flow.yml
@@ -0,0 +1,5 @@
+---
+title: Fix redirect loop on .com when 2FA is required
+merge_request: 31229
+author:
+type: fixed
diff --git a/changelogs/unreleased/215975-remove-monaco-snippets-flag.yml b/changelogs/unreleased/215975-remove-monaco-snippets-flag.yml
new file mode 100644
index 00000000000..a122f79362d
--- /dev/null
+++ b/changelogs/unreleased/215975-remove-monaco-snippets-flag.yml
@@ -0,0 +1,5 @@
+---
+title: Enable Monaco for editing Snippets by default
+merge_request: 30892
+author:
+type: added
diff --git a/changelogs/unreleased/216001-fix-copy-button-hover.yml b/changelogs/unreleased/216001-fix-copy-button-hover.yml
new file mode 100644
index 00000000000..e6a00d831ae
--- /dev/null
+++ b/changelogs/unreleased/216001-fix-copy-button-hover.yml
@@ -0,0 +1,5 @@
+---
+title: Fixes overlapping tooltips when clicking copy buttons
+merge_request: 30622
+author:
+type: fixed
diff --git a/changelogs/unreleased/216031-show-accurate-error-message-when-pipelines-disabled-mr-must-succee.yml b/changelogs/unreleased/216031-show-accurate-error-message-when-pipelines-disabled-mr-must-succee.yml
new file mode 100644
index 00000000000..c7868e90106
--- /dev/null
+++ b/changelogs/unreleased/216031-show-accurate-error-message-when-pipelines-disabled-mr-must-succee.yml
@@ -0,0 +1,7 @@
+---
+title:
+ Add clear explanation to the MR widget when no CI is available and Pipeline
+ must succeed option is activated
+merge_request: 31112
+author:
+type: changed
diff --git a/changelogs/unreleased/216035-clone-dropdown-icons-alignment.yml b/changelogs/unreleased/216035-clone-dropdown-icons-alignment.yml
new file mode 100644
index 00000000000..ab7f1ab2df2
--- /dev/null
+++ b/changelogs/unreleased/216035-clone-dropdown-icons-alignment.yml
@@ -0,0 +1,5 @@
+---
+title: Fixed alignment of Snippet Clone copy buttons
+merge_request: 30897
+author:
+type: fixed
diff --git a/changelogs/unreleased/216046-snippet-search-results-page-styling-issue.yml b/changelogs/unreleased/216046-snippet-search-results-page-styling-issue.yml
new file mode 100644
index 00000000000..ad70c61d621
--- /dev/null
+++ b/changelogs/unreleased/216046-snippet-search-results-page-styling-issue.yml
@@ -0,0 +1,5 @@
+---
+title: Align styling of snippet search results
+merge_request: 30837
+author:
+type: fixed
diff --git a/changelogs/unreleased/216091-align-author-dropdown-height.yml b/changelogs/unreleased/216091-align-author-dropdown-height.yml
new file mode 100644
index 00000000000..bc1f2f058f3
--- /dev/null
+++ b/changelogs/unreleased/216091-align-author-dropdown-height.yml
@@ -0,0 +1,5 @@
+---
+title: Fix misalignment of author dropdown on the commits search page
+merge_request: 31686
+author:
+type: other
diff --git a/changelogs/unreleased/216122-use-search-to-quickly-find-and-discover-images-hosted-in-the-gitla.yml b/changelogs/unreleased/216122-use-search-to-quickly-find-and-discover-images-hosted-in-the-gitla.yml
new file mode 100644
index 00000000000..cca9be40473
--- /dev/null
+++ b/changelogs/unreleased/216122-use-search-to-quickly-find-and-discover-images-hosted-in-the-gitla.yml
@@ -0,0 +1,5 @@
+---
+title: Add search bar to container registry image list
+merge_request: 31322
+author:
+type: added
diff --git a/changelogs/unreleased/216157-add-usage-ping-recording_finished_at-field.yml b/changelogs/unreleased/216157-add-usage-ping-recording_finished_at-field.yml
new file mode 100644
index 00000000000..5f0e8cbb94b
--- /dev/null
+++ b/changelogs/unreleased/216157-add-usage-ping-recording_finished_at-field.yml
@@ -0,0 +1,5 @@
+---
+title: Record usage ping finish time
+merge_request: 31222
+author:
+type: performance
diff --git a/changelogs/unreleased/216216-fix-mr-diffs-external-diffs-store-bug.yml b/changelogs/unreleased/216216-fix-mr-diffs-external-diffs-store-bug.yml
new file mode 100644
index 00000000000..6051b3f70c6
--- /dev/null
+++ b/changelogs/unreleased/216216-fix-mr-diffs-external-diffs-store-bug.yml
@@ -0,0 +1,5 @@
+---
+title: Correctly track the store that external MR diffs are placed on
+merge_request: 31005
+author:
+type: fixed
diff --git a/changelogs/unreleased/216243.yml b/changelogs/unreleased/216243.yml
new file mode 100644
index 00000000000..dfdfac5832a
--- /dev/null
+++ b/changelogs/unreleased/216243.yml
@@ -0,0 +1,5 @@
+---
+title: Add docs for alert management list
+merge_request: 31225
+author:
+type: other
diff --git a/changelogs/unreleased/216300-allow-for-mr-creation.yml b/changelogs/unreleased/216300-allow-for-mr-creation.yml
new file mode 100644
index 00000000000..9852b7b687a
--- /dev/null
+++ b/changelogs/unreleased/216300-allow-for-mr-creation.yml
@@ -0,0 +1,5 @@
+---
+title: Add ability to create merge request from vulnerability page
+merge_request: 31620
+author:
+type: added
diff --git a/changelogs/unreleased/216453-large-files-snippet.yml b/changelogs/unreleased/216453-large-files-snippet.yml
new file mode 100644
index 00000000000..5e0f5dd6009
--- /dev/null
+++ b/changelogs/unreleased/216453-large-files-snippet.yml
@@ -0,0 +1,5 @@
+---
+title: Refactored render errors for blob to Vue
+merge_request: 32345
+author:
+type: changed
diff --git a/changelogs/unreleased/216472-move-embeding-metrics-to-core.yml b/changelogs/unreleased/216472-move-embeding-metrics-to-core.yml
new file mode 100644
index 00000000000..bd3dcda4c89
--- /dev/null
+++ b/changelogs/unreleased/216472-move-embeding-metrics-to-core.yml
@@ -0,0 +1,5 @@
+---
+title: Moves embedded metrics for Prometheus alerts to Core
+merge_request: 31203
+author:
+type: changed
diff --git a/changelogs/unreleased/216477-gllink-updates.yml b/changelogs/unreleased/216477-gllink-updates.yml
new file mode 100644
index 00000000000..3a786fa479e
--- /dev/null
+++ b/changelogs/unreleased/216477-gllink-updates.yml
@@ -0,0 +1,5 @@
+---
+title: Update style of buttons on the Releases page
+merge_request: 31129
+author: Özgür Adem Işıklı @iozguradem
+type: changed
diff --git a/changelogs/unreleased/216505-public-dashboard-visibility.yml b/changelogs/unreleased/216505-public-dashboard-visibility.yml
new file mode 100644
index 00000000000..ffbc0e7ad80
--- /dev/null
+++ b/changelogs/unreleased/216505-public-dashboard-visibility.yml
@@ -0,0 +1,5 @@
+---
+title: Fix public metrics dashboard visibility bug
+merge_request: 31925
+author:
+type: fixed
diff --git a/changelogs/unreleased/216509-ide-new-file-trim.yml b/changelogs/unreleased/216509-ide-new-file-trim.yml
new file mode 100644
index 00000000000..ca7e6cc2eb3
--- /dev/null
+++ b/changelogs/unreleased/216509-ide-new-file-trim.yml
@@ -0,0 +1,5 @@
+---
+title: Trim whitespace in directory names in the Web IDE
+merge_request: 31305
+author:
+type: fixed
diff --git a/changelogs/unreleased/216543-none-syntax-highlighting-theme-for-web-ide.yml b/changelogs/unreleased/216543-none-syntax-highlighting-theme-for-web-ide.yml
new file mode 100644
index 00000000000..c669952d4d6
--- /dev/null
+++ b/changelogs/unreleased/216543-none-syntax-highlighting-theme-for-web-ide.yml
@@ -0,0 +1,5 @@
+---
+title: None syntax highlighting theme for Web IDE
+merge_request: 31056
+author:
+type: added
diff --git a/changelogs/unreleased/216584-drop-ruby-memory-bytes.yml b/changelogs/unreleased/216584-drop-ruby-memory-bytes.yml
new file mode 100644
index 00000000000..6b01e48e997
--- /dev/null
+++ b/changelogs/unreleased/216584-drop-ruby-memory-bytes.yml
@@ -0,0 +1,5 @@
+---
+title: Remove ruby_memory_bytes metric, duplicate of ruby_process_resident_memory_bytes
+merge_request: 31705
+author:
+type: removed
diff --git a/changelogs/unreleased/216597-drop-uss-pss-env-var.yml b/changelogs/unreleased/216597-drop-uss-pss-env-var.yml
new file mode 100644
index 00000000000..04be98dfc4d
--- /dev/null
+++ b/changelogs/unreleased/216597-drop-uss-pss-env-var.yml
@@ -0,0 +1,5 @@
+---
+title: Ruby metrics now include USS and PSS memory readings
+merge_request: 31707
+author:
+type: added
diff --git a/changelogs/unreleased/216618-remove-liquid.yml b/changelogs/unreleased/216618-remove-liquid.yml
new file mode 100644
index 00000000000..24a767c9580
--- /dev/null
+++ b/changelogs/unreleased/216618-remove-liquid.yml
@@ -0,0 +1,6 @@
+---
+title: Use gsub instead of the Liquid gem for variable substitution in the Prometheus
+ proxy API
+merge_request: 31482
+author:
+type: changed
diff --git a/changelogs/unreleased/216728-actionview-template-error-undefined-method-pages_domain-for-pagesd.yml b/changelogs/unreleased/216728-actionview-template-error-undefined-method-pages_domain-for-pagesd.yml
new file mode 100644
index 00000000000..d34e092c9ee
--- /dev/null
+++ b/changelogs/unreleased/216728-actionview-template-error-undefined-method-pages_domain-for-pagesd.yml
@@ -0,0 +1,5 @@
+---
+title: Fix 500 on creating an invalid domains and verification
+merge_request: 31190
+author:
+type: fixed
diff --git a/changelogs/unreleased/216750-open-single-panel-new-tab.yml b/changelogs/unreleased/216750-open-single-panel-new-tab.yml
new file mode 100644
index 00000000000..7a252c8b1db
--- /dev/null
+++ b/changelogs/unreleased/216750-open-single-panel-new-tab.yml
@@ -0,0 +1,5 @@
+---
+title: Allow monitoring dashboard users to open single panels in a new tab
+merge_request: 31206
+author:
+type: added
diff --git a/changelogs/unreleased/216851-graphql-externallypaginatedarrayconnection-can-return-incorrect-nu.yml b/changelogs/unreleased/216851-graphql-externallypaginatedarrayconnection-can-return-incorrect-nu.yml
new file mode 100644
index 00000000000..dd36d52f1c4
--- /dev/null
+++ b/changelogs/unreleased/216851-graphql-externallypaginatedarrayconnection-can-return-incorrect-nu.yml
@@ -0,0 +1,5 @@
+---
+title: Fix incorrect number of errors returned when querying sentry errors
+merge_request: 31252
+author:
+type: fixed
diff --git a/changelogs/unreleased/216920-bug-restore-exact-time-tooltip-on-last-updated-tag-column.yml b/changelogs/unreleased/216920-bug-restore-exact-time-tooltip-on-last-updated-tag-column.yml
new file mode 100644
index 00000000000..02daaa4aba4
--- /dev/null
+++ b/changelogs/unreleased/216920-bug-restore-exact-time-tooltip-on-last-updated-tag-column.yml
@@ -0,0 +1,5 @@
+---
+title: Add tooltip to container registry tags last update column
+merge_request: 31317
+author:
+type: fixed
diff --git a/changelogs/unreleased/216970-add-instance-to-service-if-missing.yml b/changelogs/unreleased/216970-add-instance-to-service-if-missing.yml
new file mode 100644
index 00000000000..d8fcbb8f587
--- /dev/null
+++ b/changelogs/unreleased/216970-add-instance-to-service-if-missing.yml
@@ -0,0 +1,5 @@
+---
+title: Add instance column to services table if it's missing
+merge_request: 31631
+author:
+type: fixed
diff --git a/changelogs/unreleased/217455-skip-mergeability-check-on-list-endpoint-by-default.yml b/changelogs/unreleased/217455-skip-mergeability-check-on-list-endpoint-by-default.yml
new file mode 100644
index 00000000000..af0cfa4b80b
--- /dev/null
+++ b/changelogs/unreleased/217455-skip-mergeability-check-on-list-endpoint-by-default.yml
@@ -0,0 +1,5 @@
+---
+title: Skip mergeability check when listing MRs in the API
+merge_request: 31890
+author:
+type: performance
diff --git a/changelogs/unreleased/217602-file-uploads-on-local-storage-with-nil-secret-in-the-db-are-broken.yml b/changelogs/unreleased/217602-file-uploads-on-local-storage-with-nil-secret-in-the-db-are-broken.yml
new file mode 100644
index 00000000000..48e585b59e8
--- /dev/null
+++ b/changelogs/unreleased/217602-file-uploads-on-local-storage-with-nil-secret-in-the-db-are-broken.yml
@@ -0,0 +1,5 @@
+---
+title: Fix incorrect regex used in FileUploader#extract_dynamic_path
+merge_request: 32271
+author:
+type: fixed
diff --git a/changelogs/unreleased/217708-nomethoderror-undefined-method-admin-for-nil-nilclass.yml b/changelogs/unreleased/217708-nomethoderror-undefined-method-admin-for-nil-nilclass.yml
new file mode 100644
index 00000000000..550d679a99e
--- /dev/null
+++ b/changelogs/unreleased/217708-nomethoderror-undefined-method-admin-for-nil-nilclass.yml
@@ -0,0 +1,6 @@
+---
+title: Fix bug in Groups API when statistics are requested in an unauthenticated
+ API call
+merge_request: 32057
+author:
+type: fixed
diff --git a/changelogs/unreleased/217992.yml b/changelogs/unreleased/217992.yml
new file mode 100644
index 00000000000..fd0c5a4af9d
--- /dev/null
+++ b/changelogs/unreleased/217992.yml
@@ -0,0 +1,5 @@
+---
+title: Use visitUrl in Alert management
+merge_request: 32414
+author:
+type: other
diff --git a/changelogs/unreleased/22691-externalize-i18n-strings-from---app-views-shared-issuable-_bulk_upd.yml b/changelogs/unreleased/22691-externalize-i18n-strings-from---app-views-shared-issuable-_bulk_upd.yml
new file mode 100644
index 00000000000..bb4a2ad2e62
--- /dev/null
+++ b/changelogs/unreleased/22691-externalize-i18n-strings-from---app-views-shared-issuable-_bulk_upd.yml
@@ -0,0 +1,5 @@
+---
+title: Externalize i18n strings from ./app/views/shared/issuable/_bulk_update_sidebar.html.haml
+merge_request: 32173
+author: Gilang Gumilar
+type: changed
diff --git a/changelogs/unreleased/22691-externalize-i18n-strings-from---app-views-shared-issuable-_close_re.yml b/changelogs/unreleased/22691-externalize-i18n-strings-from---app-views-shared-issuable-_close_re.yml
new file mode 100644
index 00000000000..c2c9dddd7fb
--- /dev/null
+++ b/changelogs/unreleased/22691-externalize-i18n-strings-from---app-views-shared-issuable-_close_re.yml
@@ -0,0 +1,5 @@
+---
+title: Externalize i18n strings from ./app/views/shared/issuable/_close_reopen_report_toggle.html.haml
+merge_request: 32168
+author: Gilang Gumilar
+type: changed
diff --git a/changelogs/unreleased/22691-externalize-i18n-strings-from---app-views-shared-issuable-_label_.yml b/changelogs/unreleased/22691-externalize-i18n-strings-from---app-views-shared-issuable-_label_.yml
new file mode 100644
index 00000000000..db5e87bc74c
--- /dev/null
+++ b/changelogs/unreleased/22691-externalize-i18n-strings-from---app-views-shared-issuable-_label_.yml
@@ -0,0 +1,5 @@
+---
+title: Externalize i18n strings from ./app/views/shared/issuable/_label_*
+merge_request: 32167
+author: Gilang Gumilar
+type: changed
diff --git a/changelogs/unreleased/22691-externalize-i18n-strings-from---app-views-shared-issuable-_nav-html.yml b/changelogs/unreleased/22691-externalize-i18n-strings-from---app-views-shared-issuable-_nav-html.yml
new file mode 100644
index 00000000000..0635a47d0b0
--- /dev/null
+++ b/changelogs/unreleased/22691-externalize-i18n-strings-from---app-views-shared-issuable-_nav-html.yml
@@ -0,0 +1,5 @@
+---
+title: Externalize i18n strings from ./app/views/shared/issuable/_nav.html.haml
+merge_request: 32165
+author: Gilang Gumilar
+type: changed
diff --git a/changelogs/unreleased/22691-externalize-i18n-strings-from---app-views-shared-issuable-_sidebar-.yml b/changelogs/unreleased/22691-externalize-i18n-strings-from---app-views-shared-issuable-_sidebar-.yml
new file mode 100644
index 00000000000..4ce29c54199
--- /dev/null
+++ b/changelogs/unreleased/22691-externalize-i18n-strings-from---app-views-shared-issuable-_sidebar-.yml
@@ -0,0 +1,5 @@
+---
+title: Externalize i18n strings from ./app/views/shared/issuable/_sidebar.html.haml
+merge_request: 32164
+author: Gilang Gumilar
+type: changed
diff --git a/changelogs/unreleased/22691-externalize-i18n-strings-from---app-views-shared-milestones-_issuab.yml b/changelogs/unreleased/22691-externalize-i18n-strings-from---app-views-shared-milestones-_issuab.yml
new file mode 100644
index 00000000000..d4392edbcb5
--- /dev/null
+++ b/changelogs/unreleased/22691-externalize-i18n-strings-from---app-views-shared-milestones-_issuab.yml
@@ -0,0 +1,5 @@
+---
+title: Externalize i18n strings from ./app/views/shared/milestones/_issuable.html.haml
+merge_request: 32161
+author: Gilang Gumilar
+type: changed
diff --git a/changelogs/unreleased/22691-externalize-i18n-strings-from---app-views-shared-milestones-_issues.yml b/changelogs/unreleased/22691-externalize-i18n-strings-from---app-views-shared-milestones-_issues.yml
new file mode 100644
index 00000000000..8c6231007e1
--- /dev/null
+++ b/changelogs/unreleased/22691-externalize-i18n-strings-from---app-views-shared-milestones-_issues.yml
@@ -0,0 +1,5 @@
+---
+title: Externalize i18n strings from ./app/views/shared/milestones/_issues_tab.html.haml
+merge_request: 32160
+author: Gilang Gumilar
+type: changed
diff --git a/changelogs/unreleased/22691-externalize-i18n-strings-from---app-views-shared-milestones-_labels.yml b/changelogs/unreleased/22691-externalize-i18n-strings-from---app-views-shared-milestones-_labels.yml
new file mode 100644
index 00000000000..6b10c7cc2cb
--- /dev/null
+++ b/changelogs/unreleased/22691-externalize-i18n-strings-from---app-views-shared-milestones-_labels.yml
@@ -0,0 +1,5 @@
+---
+title: Externalize i18n strings from ./app/views/shared/milestones/_labels_tab.html.haml
+merge_request: 32159
+author: Gilang Gumilar
+type: changed
diff --git a/changelogs/unreleased/22691-externalize-i18n-strings-from---app-views-shared-milestones-_merge_.yml b/changelogs/unreleased/22691-externalize-i18n-strings-from---app-views-shared-milestones-_merge_.yml
new file mode 100644
index 00000000000..d85793902fa
--- /dev/null
+++ b/changelogs/unreleased/22691-externalize-i18n-strings-from---app-views-shared-milestones-_merge_.yml
@@ -0,0 +1,5 @@
+---
+title: Externalize i18n strings from ./app/views/shared/milestones/_merge_requests_tab.haml
+merge_request: 32158
+author: Gilang Gumilar
+type: changed
diff --git a/changelogs/unreleased/22691-externalize-i18n-strings-from---app-views-shared-milestones-_milest.yml b/changelogs/unreleased/22691-externalize-i18n-strings-from---app-views-shared-milestones-_milest.yml
new file mode 100644
index 00000000000..0b071794ad9
--- /dev/null
+++ b/changelogs/unreleased/22691-externalize-i18n-strings-from---app-views-shared-milestones-_milest.yml
@@ -0,0 +1,5 @@
+---
+title: Externalize i18n strings from ./app/views/shared/milestones/_milestone.html.haml
+merge_request: 32154
+author: Gilang Gumilar
+type: changed
diff --git a/changelogs/unreleased/22691-externalize-i18n-strings-from---views-shared-issuable-_close_re-btn.yml b/changelogs/unreleased/22691-externalize-i18n-strings-from---views-shared-issuable-_close_re-btn.yml
new file mode 100644
index 00000000000..b5e4ec2a408
--- /dev/null
+++ b/changelogs/unreleased/22691-externalize-i18n-strings-from---views-shared-issuable-_close_re-btn.yml
@@ -0,0 +1,5 @@
+---
+title: Externalize i18n strings from ./app/views/shared/issuable/_close_reopen_button.html.haml
+merge_request: 32172
+author: Gilang Gumilar
+type: changed
diff --git a/changelogs/unreleased/22691-externelize-i18n-strings-from---app-views-shared--aria-label.yml b/changelogs/unreleased/22691-externelize-i18n-strings-from---app-views-shared--aria-label.yml
new file mode 100644
index 00000000000..be6007eab6d
--- /dev/null
+++ b/changelogs/unreleased/22691-externelize-i18n-strings-from---app-views-shared--aria-label.yml
@@ -0,0 +1,5 @@
+---
+title: Externalize i18n aria-label strings from ./app/views/shared/*
+merge_request: 32142
+author: Gilang Gumilar
+type: changed
diff --git a/changelogs/unreleased/22691-externelize-i18n-strings-from---app-views-shared-_commit_message_co.yml b/changelogs/unreleased/22691-externelize-i18n-strings-from---app-views-shared-_commit_message_co.yml
new file mode 100644
index 00000000000..9eb62695966
--- /dev/null
+++ b/changelogs/unreleased/22691-externelize-i18n-strings-from---app-views-shared-_commit_message_co.yml
@@ -0,0 +1,5 @@
+---
+title: Externalize i18n strings from ./app/views/shared/_commit_message_container.html.haml
+merge_request: 32139
+author: Gilang Gumilar
+type: changed
diff --git a/changelogs/unreleased/22691-externelize-i18n-strings-from---app-views-shared-_delete_label_moda.yml b/changelogs/unreleased/22691-externelize-i18n-strings-from---app-views-shared-_delete_label_moda.yml
new file mode 100644
index 00000000000..22a328cb51a
--- /dev/null
+++ b/changelogs/unreleased/22691-externelize-i18n-strings-from---app-views-shared-_delete_label_moda.yml
@@ -0,0 +1,5 @@
+---
+title: Externalize i18n strings from ./app/views/shared/_delete_label_modal.html.haml
+merge_request: 32138
+author: Gilang Gumilar
+type: changed
diff --git a/changelogs/unreleased/22691-externelize-i18n-strings-from---app-views-shared-_field-html-haml.yml b/changelogs/unreleased/22691-externelize-i18n-strings-from---app-views-shared-_field-html-haml.yml
new file mode 100644
index 00000000000..cb9b46fd15e
--- /dev/null
+++ b/changelogs/unreleased/22691-externelize-i18n-strings-from---app-views-shared-_field-html-haml.yml
@@ -0,0 +1,5 @@
+---
+title: Externalize i18n strings from ./app/views/shared/_field.html.haml
+merge_request: 32136
+author: Gilang Gumilar
+type: changed
diff --git a/changelogs/unreleased/22691-externelize-i18n-strings-from---app-views-shared-_group_form-html-h.yml b/changelogs/unreleased/22691-externelize-i18n-strings-from---app-views-shared-_group_form-html-h.yml
new file mode 100644
index 00000000000..84051751bf0
--- /dev/null
+++ b/changelogs/unreleased/22691-externelize-i18n-strings-from---app-views-shared-_group_form-html-h.yml
@@ -0,0 +1,5 @@
+---
+title: Externalize i18n strings from ./app/views/shared/_group_form.html.haml
+merge_request: 32132
+author: Gilang Gumilar
+type: changed
diff --git a/changelogs/unreleased/22691-externelize-i18n-strings-from---app-views-shared-_group_tips-html-h.yml b/changelogs/unreleased/22691-externelize-i18n-strings-from---app-views-shared-_group_tips-html-h.yml
new file mode 100644
index 00000000000..8308e3dcb12
--- /dev/null
+++ b/changelogs/unreleased/22691-externelize-i18n-strings-from---app-views-shared-_group_tips-html-h.yml
@@ -0,0 +1,5 @@
+---
+title: Externalize i18n strings from ./app/views/shared/_group_tips.html.haml
+merge_request: 32127
+author: Gilang Gumilar
+type: changed
diff --git a/changelogs/unreleased/22691-externelize-i18n-strings-from---app-views-shared-_label_row-html-ha.yml b/changelogs/unreleased/22691-externelize-i18n-strings-from---app-views-shared-_label_row-html-ha.yml
new file mode 100644
index 00000000000..7f360c31198
--- /dev/null
+++ b/changelogs/unreleased/22691-externelize-i18n-strings-from---app-views-shared-_label_row-html-ha.yml
@@ -0,0 +1,5 @@
+---
+title: Externalize i18n strings from ./app/views/shared/_label_row.html.haml
+merge_request: 32124
+author: Gilang Gumilar
+type: changed
diff --git a/changelogs/unreleased/22691-externelize-i18n-strings-from---app-views-shared-_milestone_expired.yml b/changelogs/unreleased/22691-externelize-i18n-strings-from---app-views-shared-_milestone_expired.yml
new file mode 100644
index 00000000000..6fb82f413ad
--- /dev/null
+++ b/changelogs/unreleased/22691-externelize-i18n-strings-from---app-views-shared-_milestone_expired.yml
@@ -0,0 +1,5 @@
+---
+title: Externalize i18n strings from ./app/views/shared/_milestone_expired.html.haml
+merge_request: 32121
+author: Gilang Gumilar
+type: changed
diff --git a/changelogs/unreleased/22691-externelize-i18n-strings-from---app-views-shared-_milestones_filter.yml b/changelogs/unreleased/22691-externelize-i18n-strings-from---app-views-shared-_milestones_filter.yml
new file mode 100644
index 00000000000..f486a150931
--- /dev/null
+++ b/changelogs/unreleased/22691-externelize-i18n-strings-from---app-views-shared-_milestones_filter.yml
@@ -0,0 +1,5 @@
+---
+title: Externalize i18n strings from ./app/views/shared/_milestones_filter.html.haml
+merge_request: 32120
+author: Gilang Gumilar
+type: changed
diff --git a/changelogs/unreleased/22691-externelize-i18n-strings-from---app-views-shared-_personal_access_t.yml b/changelogs/unreleased/22691-externelize-i18n-strings-from---app-views-shared-_personal_access_t.yml
new file mode 100644
index 00000000000..8015a67062f
--- /dev/null
+++ b/changelogs/unreleased/22691-externelize-i18n-strings-from---app-views-shared-_personal_access_t.yml
@@ -0,0 +1,5 @@
+---
+title: Externalize i18n strings from ./app/views/shared/_personal_access_tokens_table.html.haml
+merge_request: 32116
+author: Gilang Gumilar
+type: changed
diff --git a/changelogs/unreleased/22691-externelize-i18n-strings-from---app-views-shared-_project_limit-htm.yml b/changelogs/unreleased/22691-externelize-i18n-strings-from---app-views-shared-_project_limit-htm.yml
new file mode 100644
index 00000000000..23875a15396
--- /dev/null
+++ b/changelogs/unreleased/22691-externelize-i18n-strings-from---app-views-shared-_project_limit-htm.yml
@@ -0,0 +1,5 @@
+---
+title: Externalize i18n strings from ./app/views/shared/_project_limit.html.haml
+merge_request: 32110
+author: Gilang Gumilar
+type: changed
diff --git a/changelogs/unreleased/22691-externelize-i18n-strings-from---app-views-shared-_recaptcha_form-ht.yml b/changelogs/unreleased/22691-externelize-i18n-strings-from---app-views-shared-_recaptcha_form-ht.yml
new file mode 100644
index 00000000000..faa328617dc
--- /dev/null
+++ b/changelogs/unreleased/22691-externelize-i18n-strings-from---app-views-shared-_recaptcha_form-ht.yml
@@ -0,0 +1,5 @@
+---
+title: Externalize i18n strings from ./app/views/shared/_recaptcha_form.html.haml
+merge_request: 32106
+author: Gilang Gumilar
+type: changed
diff --git a/changelogs/unreleased/22691-externelize-i18n-strings-from---app-views-shared-_ref_dropdown-html.yml b/changelogs/unreleased/22691-externelize-i18n-strings-from---app-views-shared-_ref_dropdown-html.yml
new file mode 100644
index 00000000000..6c5da93f1e9
--- /dev/null
+++ b/changelogs/unreleased/22691-externelize-i18n-strings-from---app-views-shared-_ref_dropdown-html.yml
@@ -0,0 +1,5 @@
+---
+title: Externalize i18n strings from ./app/views/shared/_ref_dropdown.html.haml
+merge_request: 32102
+author: Gilang Gumilar
+type: changed
diff --git a/changelogs/unreleased/22691-externelize-i18n-strings-from---app-views-shared-milestones-_top-ht.yml b/changelogs/unreleased/22691-externelize-i18n-strings-from---app-views-shared-milestones-_top-ht.yml
new file mode 100644
index 00000000000..c373a4605c8
--- /dev/null
+++ b/changelogs/unreleased/22691-externelize-i18n-strings-from---app-views-shared-milestones-_top-ht.yml
@@ -0,0 +1,5 @@
+---
+title: Externalize i18n strings from ./app/views/shared/milestones/_top.html.haml
+merge_request: 32148
+author: Gilang Gumilar
+type: changed
diff --git a/changelogs/unreleased/22691-externelize-i18n-strings-from---app-views-users-_deletion_guidance-.yml b/changelogs/unreleased/22691-externelize-i18n-strings-from---app-views-users-_deletion_guidance-.yml
new file mode 100644
index 00000000000..6f4a1864a18
--- /dev/null
+++ b/changelogs/unreleased/22691-externelize-i18n-strings-from---app-views-users-_deletion_guidance-.yml
@@ -0,0 +1,5 @@
+---
+title: Externalize i18n strings from ./app/views/users/_deletion_guidance.html.haml
+merge_request: 32097
+author: Gilang Gumilar
+type: changed
diff --git a/changelogs/unreleased/22691-i18n-externelize-strings-from---app-views-users-calendar_activities.yml b/changelogs/unreleased/22691-i18n-externelize-strings-from---app-views-users-calendar_activities.yml
new file mode 100644
index 00000000000..c812f0f76e0
--- /dev/null
+++ b/changelogs/unreleased/22691-i18n-externelize-strings-from---app-views-users-calendar_activities.yml
@@ -0,0 +1,5 @@
+---
+title: Externalize i18n strings from ./app/views/users/calendar_activities.html.haml
+merge_request: 32094
+author: Gilang Gumilar
+type: changed
diff --git a/changelogs/unreleased/23847-download-reports.yml b/changelogs/unreleased/23847-download-reports.yml
new file mode 100644
index 00000000000..b29a690ca5f
--- /dev/null
+++ b/changelogs/unreleased/23847-download-reports.yml
@@ -0,0 +1,5 @@
+---
+title: Make report-type artifacts available for download
+merge_request: 31513
+author:
+type: added
diff --git a/changelogs/unreleased/24295-freeze-period-API-changes.yml b/changelogs/unreleased/24295-freeze-period-API-changes.yml
new file mode 100644
index 00000000000..31455bcc818
--- /dev/null
+++ b/changelogs/unreleased/24295-freeze-period-API-changes.yml
@@ -0,0 +1,5 @@
+---
+title: Expose Freeze Periods in REST API
+merge_request: 29382
+author:
+type: added
diff --git a/changelogs/unreleased/24295-freeze-period-db-changes.yml b/changelogs/unreleased/24295-freeze-period-db-changes.yml
new file mode 100644
index 00000000000..eace0795545
--- /dev/null
+++ b/changelogs/unreleased/24295-freeze-period-db-changes.yml
@@ -0,0 +1,5 @@
+---
+title: Add freeze period model
+merge_request: 29162
+author:
+type: added
diff --git a/changelogs/unreleased/24295-freeze-period-service.yml b/changelogs/unreleased/24295-freeze-period-service.yml
new file mode 100644
index 00000000000..54d5f3b2acd
--- /dev/null
+++ b/changelogs/unreleased/24295-freeze-period-service.yml
@@ -0,0 +1,5 @@
+---
+title: Add freeze periods via CI_DEPLOY_FREEZE variable
+merge_request: 29244
+author:
+type: added
diff --git a/changelogs/unreleased/24525-ide-renaming-issue.yml b/changelogs/unreleased/24525-ide-renaming-issue.yml
new file mode 100644
index 00000000000..d4af52bbe25
--- /dev/null
+++ b/changelogs/unreleased/24525-ide-renaming-issue.yml
@@ -0,0 +1,5 @@
+---
+title: Fix error renaming files using web IDE
+merge_request: 30969
+author:
+type: fixed
diff --git a/changelogs/unreleased/25375-webide-markdown-broken-images.yml b/changelogs/unreleased/25375-webide-markdown-broken-images.yml
new file mode 100644
index 00000000000..6ec333116e8
--- /dev/null
+++ b/changelogs/unreleased/25375-webide-markdown-broken-images.yml
@@ -0,0 +1,5 @@
+---
+title: Fix issue with broken images in Web IDE markdown
+merge_request: 31638
+author:
+type: fixed
diff --git a/changelogs/unreleased/25807-status-tooltip-overlaps-extended-tooltip.yml b/changelogs/unreleased/25807-status-tooltip-overlaps-extended-tooltip.yml
new file mode 100644
index 00000000000..099eb6445ed
--- /dev/null
+++ b/changelogs/unreleased/25807-status-tooltip-overlaps-extended-tooltip.yml
@@ -0,0 +1,5 @@
+---
+title: Prevent duplicate tooltips when hovering over status emoji in comments
+merge_request: 29356
+author:
+type: fixed
diff --git a/changelogs/unreleased/25875-Add-Webex-Teams-project-integration-service.yml b/changelogs/unreleased/25875-Add-Webex-Teams-project-integration-service.yml
new file mode 100644
index 00000000000..ee79fd33c13
--- /dev/null
+++ b/changelogs/unreleased/25875-Add-Webex-Teams-project-integration-service.yml
@@ -0,0 +1,5 @@
+---
+title: Add Webex Teams project integration service
+merge_request: 31543
+author: Sebastian Leuser
+type: added
diff --git a/changelogs/unreleased/27481-remove-unused-cluster-workers.yml b/changelogs/unreleased/27481-remove-unused-cluster-workers.yml
new file mode 100644
index 00000000000..f05ce3a9a60
--- /dev/null
+++ b/changelogs/unreleased/27481-remove-unused-cluster-workers.yml
@@ -0,0 +1,5 @@
+---
+title: Remove unused cluster configuration workers
+merge_request: 30695
+author:
+type: other
diff --git a/changelogs/unreleased/28560_cleanup_optimistic_locking_db_pt2-second-try.yml b/changelogs/unreleased/28560_cleanup_optimistic_locking_db_pt2-second-try.yml
new file mode 100644
index 00000000000..4f8fc440283
--- /dev/null
+++ b/changelogs/unreleased/28560_cleanup_optimistic_locking_db_pt2-second-try.yml
@@ -0,0 +1,5 @@
+---
+title: Set NULL `lock_version` values to 0 for CI objects
+merge_request: 30305
+author:
+type: fixed
diff --git a/changelogs/unreleased/28566-add-sec-binaries-template.yml b/changelogs/unreleased/28566-add-sec-binaries-template.yml
new file mode 100644
index 00000000000..e9e00911f20
--- /dev/null
+++ b/changelogs/unreleased/28566-add-sec-binaries-template.yml
@@ -0,0 +1,5 @@
+---
+title: Add secure binaries template
+merge_request: 28566
+author:
+type: added
diff --git a/changelogs/unreleased/28617-add-global-sec-prefix.yml b/changelogs/unreleased/28617-add-global-sec-prefix.yml
new file mode 100644
index 00000000000..a348e94e638
--- /dev/null
+++ b/changelogs/unreleased/28617-add-global-sec-prefix.yml
@@ -0,0 +1,5 @@
+---
+title: Add the global var SECURE_ANALYZERS_PREFIX
+merge_request: 28617
+author:
+type: added
diff --git a/changelogs/unreleased/29713-graphql-add-issue-priority-sort.yml b/changelogs/unreleased/29713-graphql-add-issue-priority-sort.yml
new file mode 100644
index 00000000000..12661ae69ad
--- /dev/null
+++ b/changelogs/unreleased/29713-graphql-add-issue-priority-sort.yml
@@ -0,0 +1,5 @@
+---
+title: Graphql query for issues can now be sorted by priority
+merge_request: 18901
+author:
+type: added
diff --git a/changelogs/unreleased/30146-let-s-encrypt-errors.yml b/changelogs/unreleased/30146-let-s-encrypt-errors.yml
new file mode 100644
index 00000000000..8853c93e408
--- /dev/null
+++ b/changelogs/unreleased/30146-let-s-encrypt-errors.yml
@@ -0,0 +1,5 @@
+---
+title: Allow users to retry obtaining Let's Encrypt certificates for GitLab Pages
+merge_request: 28784
+author:
+type: added
diff --git a/changelogs/unreleased/30526-c-be-wiki-activity-Pushes.yml b/changelogs/unreleased/30526-c-be-wiki-activity-Pushes.yml
new file mode 100644
index 00000000000..95b5abbb55b
--- /dev/null
+++ b/changelogs/unreleased/30526-c-be-wiki-activity-Pushes.yml
@@ -0,0 +1,5 @@
+---
+title: Create Wiki activity events on pushes to Wiki git repository
+merge_request: 26624
+author:
+type: added
diff --git a/changelogs/unreleased/31810-markdown-images.yml b/changelogs/unreleased/31810-markdown-images.yml
new file mode 100644
index 00000000000..fdf35adb281
--- /dev/null
+++ b/changelogs/unreleased/31810-markdown-images.yml
@@ -0,0 +1,5 @@
+---
+title: Allow Web IDE markdown to preview uncommitted images
+merge_request: 31540
+author:
+type: added
diff --git a/changelogs/unreleased/32358-add-initial-stats-for-modsecurity-installations.yml b/changelogs/unreleased/32358-add-initial-stats-for-modsecurity-installations.yml
new file mode 100644
index 00000000000..cbb0098c718
--- /dev/null
+++ b/changelogs/unreleased/32358-add-initial-stats-for-modsecurity-installations.yml
@@ -0,0 +1,5 @@
+---
+title: Add indexes on ingress, enabled clusters and successful deployments
+merge_request: 28331
+author:
+type: performance
diff --git a/changelogs/unreleased/33098-distribute-daily-cron-schedules-out-over-the-hour.yml b/changelogs/unreleased/33098-distribute-daily-cron-schedules-out-over-the-hour.yml
new file mode 100644
index 00000000000..d9012cfa48e
--- /dev/null
+++ b/changelogs/unreleased/33098-distribute-daily-cron-schedules-out-over-the-hour.yml
@@ -0,0 +1,5 @@
+---
+title: Update cron job schedule to have a random time generated on page load
+merge_request: 30729
+author:
+type: changed
diff --git a/changelogs/unreleased/33161-highlight-focused-design-discussion-in-image-markers.yml b/changelogs/unreleased/33161-highlight-focused-design-discussion-in-image-markers.yml
new file mode 100644
index 00000000000..1f0ddb0215c
--- /dev/null
+++ b/changelogs/unreleased/33161-highlight-focused-design-discussion-in-image-markers.yml
@@ -0,0 +1,5 @@
+---
+title: Highlight focused Design discussion in image markers
+merge_request: 31323
+author:
+type: added
diff --git a/changelogs/unreleased/33200-add-empty-needs-to-auto-devops-test.yml b/changelogs/unreleased/33200-add-empty-needs-to-auto-devops-test.yml
new file mode 100644
index 00000000000..81f02558134
--- /dev/null
+++ b/changelogs/unreleased/33200-add-empty-needs-to-auto-devops-test.yml
@@ -0,0 +1,5 @@
+---
+title: Allow Auto DevOps Test stage to start immediately
+merge_request: 31185
+author:
+type: other
diff --git a/changelogs/unreleased/35069-protect-builds.yml b/changelogs/unreleased/35069-protect-builds.yml
new file mode 100644
index 00000000000..7efe4ab529c
--- /dev/null
+++ b/changelogs/unreleased/35069-protect-builds.yml
@@ -0,0 +1,5 @@
+---
+title: Disallow developers to delete builds of protected branches
+merge_request: 28881
+author: Alexander Kutelev
+type: changed
diff --git a/changelogs/unreleased/36758-graphql-query-one-or-all-lists-in-an-issue-board.yml b/changelogs/unreleased/36758-graphql-query-one-or-all-lists-in-an-issue-board.yml
new file mode 100644
index 00000000000..bb6a1e28c98
--- /dev/null
+++ b/changelogs/unreleased/36758-graphql-query-one-or-all-lists-in-an-issue-board.yml
@@ -0,0 +1,5 @@
+---
+title: "Add GraphQL support for querying a board's lists"
+merge_request: 24812
+author:
+type: added
diff --git a/changelogs/unreleased/36810-webide-branch-with-path.yml b/changelogs/unreleased/36810-webide-branch-with-path.yml
new file mode 100644
index 00000000000..2238101799c
--- /dev/null
+++ b/changelogs/unreleased/36810-webide-branch-with-path.yml
@@ -0,0 +1,5 @@
+---
+title: In WebIDE get files with relative path instead of web_url
+merge_request: 31478
+author:
+type: fixed
diff --git a/changelogs/unreleased/37278-DS_DISABLE_DIND-true.yml b/changelogs/unreleased/37278-DS_DISABLE_DIND-true.yml
new file mode 100644
index 00000000000..36c068c5c8d
--- /dev/null
+++ b/changelogs/unreleased/37278-DS_DISABLE_DIND-true.yml
@@ -0,0 +1,5 @@
+---
+title: 'Disable Docker-in-Docker for Dependency Scanning by default'
+merge_request: 31588
+author:
+type: changed
diff --git a/changelogs/unreleased/37278-SAST_DISABLE_DIND-true.yml b/changelogs/unreleased/37278-SAST_DISABLE_DIND-true.yml
new file mode 100644
index 00000000000..973648e5167
--- /dev/null
+++ b/changelogs/unreleased/37278-SAST_DISABLE_DIND-true.yml
@@ -0,0 +1,5 @@
+---
+title: 'Disable Docker-in-Docker for SAST by default'
+merge_request: 31589
+author:
+type: changed
diff --git a/changelogs/unreleased/38022-add-remove_label-quick-action-more-intuitive-than-unlabel.yml b/changelogs/unreleased/38022-add-remove_label-quick-action-more-intuitive-than-unlabel.yml
new file mode 100644
index 00000000000..ac670b1157c
--- /dev/null
+++ b/changelogs/unreleased/38022-add-remove_label-quick-action-more-intuitive-than-unlabel.yml
@@ -0,0 +1,5 @@
+---
+title: Define remove_label quick action as alias of unlabel
+merge_request: 24962
+author: Jacopo Beschi @jacopo-beschi
+type: added
diff --git a/changelogs/unreleased/38080-scala-gitlab-ci-template-does-not-work.yml b/changelogs/unreleased/38080-scala-gitlab-ci-template-does-not-work.yml
new file mode 100644
index 00000000000..843088f88a1
--- /dev/null
+++ b/changelogs/unreleased/38080-scala-gitlab-ci-template-does-not-work.yml
@@ -0,0 +1,5 @@
+---
+title: Fix GitLab CI/CD Scala template
+merge_request: 30667
+author:
+type: fixed
diff --git a/changelogs/unreleased/38096-add-index-on-resource-weight-events-created-at-pd.yml b/changelogs/unreleased/38096-add-index-on-resource-weight-events-created-at-pd.yml
new file mode 100644
index 00000000000..ecaa3da4bc5
--- /dev/null
+++ b/changelogs/unreleased/38096-add-index-on-resource-weight-events-created-at-pd.yml
@@ -0,0 +1,5 @@
+---
+title: Add index to issue_id and created_at of resource_weight_events
+merge_request: 28930
+author:
+type: other
diff --git a/changelogs/unreleased/38096-resource-state-events-pd.yml b/changelogs/unreleased/38096-resource-state-events-pd.yml
new file mode 100644
index 00000000000..a761945d994
--- /dev/null
+++ b/changelogs/unreleased/38096-resource-state-events-pd.yml
@@ -0,0 +1,5 @@
+---
+title: Add resource_state_events table
+merge_request: 28926
+author:
+type: added
diff --git a/changelogs/unreleased/38296-graphql-add-milestone_due_asc-sort-for-issuables.yml b/changelogs/unreleased/38296-graphql-add-milestone_due_asc-sort-for-issuables.yml
new file mode 100644
index 00000000000..4cc12c7710a
--- /dev/null
+++ b/changelogs/unreleased/38296-graphql-add-milestone_due_asc-sort-for-issuables.yml
@@ -0,0 +1,5 @@
+---
+title: GraphQL issue queries can now be sorted by milestone due date
+merge_request: 29992
+author:
+type: added
diff --git a/changelogs/unreleased/38355-user-popovers-don-t-work-in-system-notes.yml b/changelogs/unreleased/38355-user-popovers-don-t-work-in-system-notes.yml
new file mode 100644
index 00000000000..48f6a1a17e6
--- /dev/null
+++ b/changelogs/unreleased/38355-user-popovers-don-t-work-in-system-notes.yml
@@ -0,0 +1,5 @@
+---
+title: Add user popovers to system notes
+merge_request: 24241
+author:
+type: fixed
diff --git a/changelogs/unreleased/38358-update-migration-helpers-to-use-check-constraints-instead-of-change.yml b/changelogs/unreleased/38358-update-migration-helpers-to-use-check-constraints-instead-of-change.yml
new file mode 100644
index 00000000000..0a0163300e3
--- /dev/null
+++ b/changelogs/unreleased/38358-update-migration-helpers-to-use-check-constraints-instead-of-change.yml
@@ -0,0 +1,5 @@
+---
+title: Support limits for offset based pagination
+merge_request: 28460
+author:
+type: changed
diff --git a/changelogs/unreleased/39467-allow-a-release-s-associated-milestones-to-be-edited-through-the-ed.yml b/changelogs/unreleased/39467-allow-a-release-s-associated-milestones-to-be-edited-through-the-ed.yml
new file mode 100644
index 00000000000..f182418b0e1
--- /dev/null
+++ b/changelogs/unreleased/39467-allow-a-release-s-associated-milestones-to-be-edited-through-the-ed.yml
@@ -0,0 +1,5 @@
+---
+title: 'Allow to assign milestones to a release on the "Edit Release page"'
+merge_request: 28583
+author:
+type: added
diff --git a/changelogs/unreleased/55241-add-issues-create-limit-to-settings-api.yml b/changelogs/unreleased/55241-add-issues-create-limit-to-settings-api.yml
new file mode 100644
index 00000000000..ee693083376
--- /dev/null
+++ b/changelogs/unreleased/55241-add-issues-create-limit-to-settings-api.yml
@@ -0,0 +1,5 @@
+---
+title: Add issues_create_limit to settings api
+merge_request: 29960
+author:
+type: added
diff --git a/changelogs/unreleased/Remove-addIssue-function-logic-from-list-js.yml b/changelogs/unreleased/Remove-addIssue-function-logic-from-list-js.yml
new file mode 100644
index 00000000000..8ccd85d35ff
--- /dev/null
+++ b/changelogs/unreleased/Remove-addIssue-function-logic-from-list-js.yml
@@ -0,0 +1,5 @@
+---
+title: removes store logic from issue board models
+merge_request: 21408
+author: nuwe1
+type: other
diff --git a/changelogs/unreleased/Remove-getIssues-function-logic-from-list-js.yml b/changelogs/unreleased/Remove-getIssues-function-logic-from-list-js.yml
new file mode 100644
index 00000000000..3608f9a8dac
--- /dev/null
+++ b/changelogs/unreleased/Remove-getIssues-function-logic-from-list-js.yml
@@ -0,0 +1,5 @@
+---
+title: removes store logic from issue board models
+merge_request: 21400
+author: nuwe1
+type: other
diff --git a/changelogs/unreleased/Remove-update-function-logic-from-issue-js.yml b/changelogs/unreleased/Remove-update-function-logic-from-issue-js.yml
new file mode 100644
index 00000000000..a9e80606601
--- /dev/null
+++ b/changelogs/unreleased/Remove-update-function-logic-from-issue-js.yml
@@ -0,0 +1,5 @@
+---
+title: Moves updateIssue from issue model to board store
+merge_request: 21414
+author: nuwe1
+type: other
diff --git a/changelogs/unreleased/aalakkad-fix-mirro-repo-docs-link.yml b/changelogs/unreleased/aalakkad-fix-mirro-repo-docs-link.yml
new file mode 100644
index 00000000000..3f5d1c59752
--- /dev/null
+++ b/changelogs/unreleased/aalakkad-fix-mirro-repo-docs-link.yml
@@ -0,0 +1,5 @@
+---
+title: Fix mirror repos docs link
+merge_request: 30443
+author:
+type: fixed
diff --git a/changelogs/unreleased/ab-fix-not-null-inconsistency.yml b/changelogs/unreleased/ab-fix-not-null-inconsistency.yml
new file mode 100644
index 00000000000..6a874b8405d
--- /dev/null
+++ b/changelogs/unreleased/ab-fix-not-null-inconsistency.yml
@@ -0,0 +1,5 @@
+---
+title: Fix database schema inconsistency with not-null checks
+merge_request: 31930
+author:
+type: other
diff --git a/changelogs/unreleased/ab-north-star-usage-ping-2.yml b/changelogs/unreleased/ab-north-star-usage-ping-2.yml
new file mode 100644
index 00000000000..66724cc7152
--- /dev/null
+++ b/changelogs/unreleased/ab-north-star-usage-ping-2.yml
@@ -0,0 +1,5 @@
+---
+title: Add issues_created_gitlab_alerts to usage ping
+merge_request: 31802
+author:
+type: added
diff --git a/changelogs/unreleased/add-ci-kubernetes-active-pipeline-variable.yml b/changelogs/unreleased/add-ci-kubernetes-active-pipeline-variable.yml
new file mode 100644
index 00000000000..d8aa1b0e0e9
--- /dev/null
+++ b/changelogs/unreleased/add-ci-kubernetes-active-pipeline-variable.yml
@@ -0,0 +1,6 @@
+---
+title: Add a CI variable CI_KUBERNETES_ACTIVE as an alternative to only:kubernetes/except:kubernetes
+ that works with the rules syntax
+merge_request: 31146
+author:
+type: added
diff --git a/changelogs/unreleased/add-epic-id-to-resource-state-event-pd.yml b/changelogs/unreleased/add-epic-id-to-resource-state-event-pd.yml
new file mode 100644
index 00000000000..56aee1e41e8
--- /dev/null
+++ b/changelogs/unreleased/add-epic-id-to-resource-state-event-pd.yml
@@ -0,0 +1,5 @@
+---
+title: Add epic_id to resource_state_events
+merge_request: 31587
+author:
+type: other
diff --git a/changelogs/unreleased/add-inherit-from-to-services.yml b/changelogs/unreleased/add-inherit-from-to-services.yml
new file mode 100644
index 00000000000..8956cca44e2
--- /dev/null
+++ b/changelogs/unreleased/add-inherit-from-to-services.yml
@@ -0,0 +1,5 @@
+---
+title: Add inherit_from_id column to services table
+merge_request: 31320
+author:
+type: other
diff --git a/changelogs/unreleased/add-missing-cluster-usage-data-index.yml b/changelogs/unreleased/add-missing-cluster-usage-data-index.yml
new file mode 100644
index 00000000000..10b06ef68c9
--- /dev/null
+++ b/changelogs/unreleased/add-missing-cluster-usage-data-index.yml
@@ -0,0 +1,5 @@
+---
+title: Add clusters index to improve usage data queries
+merge_request: 28626
+author:
+type: performance
diff --git a/changelogs/unreleased/add-ops-strategies-user-lists-table.yml b/changelogs/unreleased/add-ops-strategies-user-lists-table.yml
new file mode 100644
index 00000000000..245e4499206
--- /dev/null
+++ b/changelogs/unreleased/add-ops-strategies-user-lists-table.yml
@@ -0,0 +1,5 @@
+---
+title: Create operations_strategies_user_lists table
+merge_request: 30243
+author:
+type: added
diff --git a/changelogs/unreleased/add_confidential_issue_url_param.yml b/changelogs/unreleased/add_confidential_issue_url_param.yml
new file mode 100644
index 00000000000..bb3ce4166e6
--- /dev/null
+++ b/changelogs/unreleased/add_confidential_issue_url_param.yml
@@ -0,0 +1,5 @@
+---
+title: Adds URL parameter for confidential new issue creation
+merge_request: 30250
+author:
+type: added
diff --git a/changelogs/unreleased/add_fluentd_into_cluster_app_page.yml b/changelogs/unreleased/add_fluentd_into_cluster_app_page.yml
new file mode 100644
index 00000000000..a01786dcbdc
--- /dev/null
+++ b/changelogs/unreleased/add_fluentd_into_cluster_app_page.yml
@@ -0,0 +1,5 @@
+---
+title: Add Fluentd into cluster apps page
+merge_request: 28847
+author:
+type: changed
diff --git a/changelogs/unreleased/add_waf_and_cilium_flags_into_fluentd_table.yml b/changelogs/unreleased/add_waf_and_cilium_flags_into_fluentd_table.yml
new file mode 100644
index 00000000000..8f652cc336a
--- /dev/null
+++ b/changelogs/unreleased/add_waf_and_cilium_flags_into_fluentd_table.yml
@@ -0,0 +1,5 @@
+---
+title: Add WAF and Cilium Log column for Fluentd table
+merge_request: 29457
+author:
+type: changed
diff --git a/changelogs/unreleased/ajk-212987-nil-wiki-page.yml b/changelogs/unreleased/ajk-212987-nil-wiki-page.yml
new file mode 100644
index 00000000000..f8a89d3945f
--- /dev/null
+++ b/changelogs/unreleased/ajk-212987-nil-wiki-page.yml
@@ -0,0 +1,5 @@
+---
+title: Eliminate errors in wiki controller during edit
+merge_request: 29645
+author:
+type: fixed
diff --git a/changelogs/unreleased/ajk-fix-extra-sleep-exclusive-lease.yml b/changelogs/unreleased/ajk-fix-extra-sleep-exclusive-lease.yml
new file mode 100644
index 00000000000..c57a1e5fd87
--- /dev/null
+++ b/changelogs/unreleased/ajk-fix-extra-sleep-exclusive-lease.yml
@@ -0,0 +1,5 @@
+---
+title: Remove extra sleep when obtaining exclusive lease
+merge_request: 30654
+author:
+type: fixed
diff --git a/changelogs/unreleased/ak-official-helm-chart.yml b/changelogs/unreleased/ak-official-helm-chart.yml
new file mode 100644
index 00000000000..f981f6fbe18
--- /dev/null
+++ b/changelogs/unreleased/ak-official-helm-chart.yml
@@ -0,0 +1,5 @@
+---
+title: Move to supported Elastic helm charts
+merge_request: 30528
+author:
+type: changed
diff --git a/changelogs/unreleased/ak-update-es.yml b/changelogs/unreleased/ak-update-es.yml
new file mode 100644
index 00000000000..a4cb1a91adb
--- /dev/null
+++ b/changelogs/unreleased/ak-update-es.yml
@@ -0,0 +1,5 @@
+---
+title: Update Elastic Stack chart to 2.0.0 to support kubernetes 1.16
+merge_request: 29601
+author:
+type: fixed
diff --git a/changelogs/unreleased/al-214347-web-ide-pipelines-usage.yml b/changelogs/unreleased/al-214347-web-ide-pipelines-usage.yml
new file mode 100644
index 00000000000..f60fe0e0748
--- /dev/null
+++ b/changelogs/unreleased/al-214347-web-ide-pipelines-usage.yml
@@ -0,0 +1,5 @@
+---
+title: Add Web IDE pipelines usage counter
+merge_request: 31658
+author:
+type: added
diff --git a/changelogs/unreleased/al-214695-improve-response-in-snippets-api.yml b/changelogs/unreleased/al-214695-improve-response-in-snippets-api.yml
new file mode 100644
index 00000000000..915322c3734
--- /dev/null
+++ b/changelogs/unreleased/al-214695-improve-response-in-snippets-api.yml
@@ -0,0 +1,5 @@
+---
+title: Improve responses in the snippet create/update API endpoints
+merge_request: 32282
+author:
+type: fixed
diff --git a/changelogs/unreleased/alert-management-colour.yml b/changelogs/unreleased/alert-management-colour.yml
new file mode 100644
index 00000000000..c8541b5bcd4
--- /dev/null
+++ b/changelogs/unreleased/alert-management-colour.yml
@@ -0,0 +1,5 @@
+---
+title: Update alert management table background colour to correct gray
+merge_request: 32068
+author:
+type: other
diff --git a/changelogs/unreleased/allow_public_view_of_pipeline_schedules.yml b/changelogs/unreleased/allow_public_view_of_pipeline_schedules.yml
new file mode 100644
index 00000000000..e931e935501
--- /dev/null
+++ b/changelogs/unreleased/allow_public_view_of_pipeline_schedules.yml
@@ -0,0 +1,5 @@
+---
+title: Allow public access to pipeline schedules
+merge_request: 20806
+author: Lee Tickett
+type: fixed
diff --git a/changelogs/unreleased/app-helpers.yml b/changelogs/unreleased/app-helpers.yml
new file mode 100644
index 00000000000..1c74a4e5c64
--- /dev/null
+++ b/changelogs/unreleased/app-helpers.yml
@@ -0,0 +1,5 @@
+---
+title: Move prepend to last line in helper files
+merge_request: 29327
+author: Rajendra Kadam
+type: fixed
diff --git a/changelogs/unreleased/app-models-1.yml b/changelogs/unreleased/app-models-1.yml
new file mode 100644
index 00000000000..82cb959c023
--- /dev/null
+++ b/changelogs/unreleased/app-models-1.yml
@@ -0,0 +1,5 @@
+---
+title: Move prepend to last line in app models
+merge_request: 31826
+author: Rajendra Kadam
+type: fixed
diff --git a/changelogs/unreleased/app-models-2.yml b/changelogs/unreleased/app-models-2.yml
new file mode 100644
index 00000000000..1c3d9d95da8
--- /dev/null
+++ b/changelogs/unreleased/app-models-2.yml
@@ -0,0 +1,5 @@
+---
+title: Move prepend to last line in app models 2
+merge_request: 31827
+author: Rajendra Kadam
+type: fixed
diff --git a/changelogs/unreleased/app-models-3.yml b/changelogs/unreleased/app-models-3.yml
new file mode 100644
index 00000000000..8e06346701b
--- /dev/null
+++ b/changelogs/unreleased/app-models-3.yml
@@ -0,0 +1,5 @@
+---
+title: Move prepend to last line in app models 3
+merge_request: 31829
+author: Rajendra Kadam
+type: fixed
diff --git a/changelogs/unreleased/async-secondary-email-devise-mailers.yml b/changelogs/unreleased/async-secondary-email-devise-mailers.yml
new file mode 100644
index 00000000000..facfe0ba4bd
--- /dev/null
+++ b/changelogs/unreleased/async-secondary-email-devise-mailers.yml
@@ -0,0 +1,5 @@
+---
+title: Send Devise emails triggered from the 'Email' model asynchronously
+merge_request: 32286
+author:
+type: fixed
diff --git a/changelogs/unreleased/bvl-remove-admin-logs.yml b/changelogs/unreleased/bvl-remove-admin-logs.yml
new file mode 100644
index 00000000000..daf1ea8beda
--- /dev/null
+++ b/changelogs/unreleased/bvl-remove-admin-logs.yml
@@ -0,0 +1,5 @@
+---
+title: Remove logs from the admin pages
+merge_request: 30485
+author:
+type: removed
diff --git a/changelogs/unreleased/bvl-remove-sidekiq-request-store-env.yml b/changelogs/unreleased/bvl-remove-sidekiq-request-store-env.yml
new file mode 100644
index 00000000000..a0f6cffe63d
--- /dev/null
+++ b/changelogs/unreleased/bvl-remove-sidekiq-request-store-env.yml
@@ -0,0 +1,5 @@
+---
+title: Remove the SIDEKIQ_REQUEST_STORE configuration
+merge_request: 29955
+author:
+type: other
diff --git a/changelogs/unreleased/cat-duplicate-ci-pipelines-index-215790.yml b/changelogs/unreleased/cat-duplicate-ci-pipelines-index-215790.yml
new file mode 100644
index 00000000000..a21cafe5e14
--- /dev/null
+++ b/changelogs/unreleased/cat-duplicate-ci-pipelines-index-215790.yml
@@ -0,0 +1,5 @@
+---
+title: Fix duplicate index removal on ci_pipelines.project_id
+merge_request: 31043
+author:
+type: fixed
diff --git a/changelogs/unreleased/change-var-to-variable.yml b/changelogs/unreleased/change-var-to-variable.yml
new file mode 100644
index 00000000000..ec2983696d4
--- /dev/null
+++ b/changelogs/unreleased/change-var-to-variable.yml
@@ -0,0 +1,5 @@
+---
+title: Change Var to Variable text
+merge_request: 30878
+author:
+type: changed
diff --git a/changelogs/unreleased/chore-migrate-models-policies-specs-admin-mode.yml b/changelogs/unreleased/chore-migrate-models-policies-specs-admin-mode.yml
new file mode 100644
index 00000000000..5b66606c4cf
--- /dev/null
+++ b/changelogs/unreleased/chore-migrate-models-policies-specs-admin-mode.yml
@@ -0,0 +1,5 @@
+---
+title: Migrate models and policies specs to consider admin mode
+merge_request: 30430
+author: Diego Louzán
+type: other
diff --git a/changelogs/unreleased/chore-mitt-migration-issuables-list.yml b/changelogs/unreleased/chore-mitt-migration-issuables-list.yml
new file mode 100644
index 00000000000..b60f9f7557c
--- /dev/null
+++ b/changelogs/unreleased/chore-mitt-migration-issuables-list.yml
@@ -0,0 +1,5 @@
+---
+title: Migrate from Vue event hub to Mitt in issuables list
+merge_request: 31652
+author: Arun Kumar Mohan
+type: changed
diff --git a/changelogs/unreleased/chore-vue-event-hub-to-mitt-migration.yml b/changelogs/unreleased/chore-vue-event-hub-to-mitt-migration.yml
new file mode 100644
index 00000000000..ea69304982e
--- /dev/null
+++ b/changelogs/unreleased/chore-vue-event-hub-to-mitt-migration.yml
@@ -0,0 +1,5 @@
+---
+title: Migrate from Vue event hub to Mitt
+merge_request: 31666
+author: Arun Kumar Mohan
+type: changed
diff --git a/changelogs/unreleased/cl-update-indexer-230.yml b/changelogs/unreleased/cl-update-indexer-230.yml
new file mode 100644
index 00000000000..da06321c464
--- /dev/null
+++ b/changelogs/unreleased/cl-update-indexer-230.yml
@@ -0,0 +1,5 @@
+---
+title: Update GitLab Elasticsearch Indexer to v2.3.0
+merge_request: 32199
+author:
+type: other
diff --git a/changelogs/unreleased/cl-wiki-result.yml b/changelogs/unreleased/cl-wiki-result.yml
new file mode 100644
index 00000000000..515b43d72a3
--- /dev/null
+++ b/changelogs/unreleased/cl-wiki-result.yml
@@ -0,0 +1,5 @@
+---
+title: Wrap wiki blob search result in its own object
+merge_request: 31155
+author:
+type: fixed
diff --git a/changelogs/unreleased/cleanup_user_highest_roles_background_migration.yml b/changelogs/unreleased/cleanup_user_highest_roles_background_migration.yml
new file mode 100644
index 00000000000..fa7af7c7f34
--- /dev/null
+++ b/changelogs/unreleased/cleanup_user_highest_roles_background_migration.yml
@@ -0,0 +1,5 @@
+---
+title: Cleanup background migration for populating user_highest_roles table
+merge_request: 31218
+author:
+type: other
diff --git a/changelogs/unreleased/cluster-applications-artifact.yml b/changelogs/unreleased/cluster-applications-artifact.yml
new file mode 100644
index 00000000000..d027fcc11bc
--- /dev/null
+++ b/changelogs/unreleased/cluster-applications-artifact.yml
@@ -0,0 +1,5 @@
+---
+title: Add support for cluster applications CI artifact report
+merge_request: 28866
+author:
+type: added
diff --git a/changelogs/unreleased/cngo-fix-jira-importer-urls.yml b/changelogs/unreleased/cngo-fix-jira-importer-urls.yml
new file mode 100644
index 00000000000..e4b4f17ee99
--- /dev/null
+++ b/changelogs/unreleased/cngo-fix-jira-importer-urls.yml
@@ -0,0 +1,5 @@
+---
+title: Fix Jira importer URLs
+merge_request: 30155
+author:
+type: added
diff --git a/changelogs/unreleased/cngo-make-edit-board-text-sentence-case.yml b/changelogs/unreleased/cngo-make-edit-board-text-sentence-case.yml
new file mode 100644
index 00000000000..399fd474129
--- /dev/null
+++ b/changelogs/unreleased/cngo-make-edit-board-text-sentence-case.yml
@@ -0,0 +1,5 @@
+---
+title: Make edit board text sentence case
+merge_request: 31418
+author:
+type: fixed
diff --git a/changelogs/unreleased/correctly-count-wiki-pages-in-sidebar.yml b/changelogs/unreleased/correctly-count-wiki-pages-in-sidebar.yml
new file mode 100644
index 00000000000..f98a9e9cecf
--- /dev/null
+++ b/changelogs/unreleased/correctly-count-wiki-pages-in-sidebar.yml
@@ -0,0 +1,5 @@
+---
+title: Correctly count wiki pages in sidebar
+merge_request: 30508
+author:
+type: fixed
diff --git a/changelogs/unreleased/dblessing-email-on-new-signin.yml b/changelogs/unreleased/dblessing-email-on-new-signin.yml
new file mode 100644
index 00000000000..6bf0922d842
--- /dev/null
+++ b/changelogs/unreleased/dblessing-email-on-new-signin.yml
@@ -0,0 +1,5 @@
+---
+title: Send email notification for unknown sign-ins
+merge_request: 29741
+author:
+type: added
diff --git a/changelogs/unreleased/dblessing-google-oauth2-timeout.yml b/changelogs/unreleased/dblessing-google-oauth2-timeout.yml
new file mode 100644
index 00000000000..a4cdc3817c2
--- /dev/null
+++ b/changelogs/unreleased/dblessing-google-oauth2-timeout.yml
@@ -0,0 +1,5 @@
+---
+title: Set timeout for Google OAuth to prevent 503 error
+merge_request: 30653
+author:
+type: fixed
diff --git a/changelogs/unreleased/dblessing-remove-app-settings-redirect.yml b/changelogs/unreleased/dblessing-remove-app-settings-redirect.yml
new file mode 100644
index 00000000000..1c232e60eee
--- /dev/null
+++ b/changelogs/unreleased/dblessing-remove-app-settings-redirect.yml
@@ -0,0 +1,5 @@
+---
+title: Remove deprecated /admin/application_settings redirect
+merge_request: 30532
+author:
+type: removed
diff --git a/changelogs/unreleased/dblessing-scim-identitites-new-user-flow.yml b/changelogs/unreleased/dblessing-scim-identitites-new-user-flow.yml
new file mode 100644
index 00000000000..49e92dd85d0
--- /dev/null
+++ b/changelogs/unreleased/dblessing-scim-identitites-new-user-flow.yml
@@ -0,0 +1,5 @@
+---
+title: Fix regression and allow SCIM to create SAML identity
+merge_request: 31238
+author:
+type: fixed
diff --git a/changelogs/unreleased/dbodicherla-add-dropdown-input-based-on-dashboard-file.yml b/changelogs/unreleased/dbodicherla-add-dropdown-input-based-on-dashboard-file.yml
new file mode 100644
index 00000000000..befae5dcb5b
--- /dev/null
+++ b/changelogs/unreleased/dbodicherla-add-dropdown-input-based-on-dashboard-file.yml
@@ -0,0 +1,6 @@
+---
+title: Render dropdown and text elements based on variables defined in monitoring
+ dashboard yml file
+merge_request: 31524
+author:
+type: added
diff --git a/changelogs/unreleased/dbodicherla-add-prefix-for-templating-variables.yml b/changelogs/unreleased/dbodicherla-add-prefix-for-templating-variables.yml
new file mode 100644
index 00000000000..027868220ee
--- /dev/null
+++ b/changelogs/unreleased/dbodicherla-add-prefix-for-templating-variables.yml
@@ -0,0 +1,5 @@
+---
+title: Add prefix to template variables in URL in the monitoring dashboard
+merge_request: 31690
+author:
+type: changed
diff --git a/changelogs/unreleased/dbodicherla-update-urls-params-only-update-existing-variables.yml b/changelogs/unreleased/dbodicherla-update-urls-params-only-update-existing-variables.yml
new file mode 100644
index 00000000000..78f6e6df8e6
--- /dev/null
+++ b/changelogs/unreleased/dbodicherla-update-urls-params-only-update-existing-variables.yml
@@ -0,0 +1,6 @@
+---
+title: URL params in the monitoring dashboard update variable values defined in yml
+ file
+merge_request: 31662
+author:
+type: changed
diff --git a/changelogs/unreleased/deprecate-x-y-stable-secure-images.yml b/changelogs/unreleased/deprecate-x-y-stable-secure-images.yml
new file mode 100644
index 00000000000..c3a54d883b7
--- /dev/null
+++ b/changelogs/unreleased/deprecate-x-y-stable-secure-images.yml
@@ -0,0 +1,5 @@
+---
+title: Drop x-y-stable version pinning for Secure templates
+merge_request: 29603
+author:
+type: removed
diff --git a/changelogs/unreleased/design-management-ee-to-ce-fe-designs-tab.yml b/changelogs/unreleased/design-management-ee-to-ce-fe-designs-tab.yml
new file mode 100644
index 00000000000..e624746ec33
--- /dev/null
+++ b/changelogs/unreleased/design-management-ee-to-ce-fe-designs-tab.yml
@@ -0,0 +1,5 @@
+---
+title: Add Design Management (via Designs tab on Issues) to GitLab FOSS
+merge_request: 31309
+author:
+type: added
diff --git a/changelogs/unreleased/display-db-deprecation-notice.yml b/changelogs/unreleased/display-db-deprecation-notice.yml
new file mode 100644
index 00000000000..b402beba0d2
--- /dev/null
+++ b/changelogs/unreleased/display-db-deprecation-notice.yml
@@ -0,0 +1,5 @@
+---
+title: Implement external database checker in dashboard controller
+merge_request: 30389
+author:
+type: deprecated
diff --git a/changelogs/unreleased/dmishunov-clone-btn-margin.yml b/changelogs/unreleased/dmishunov-clone-btn-margin.yml
new file mode 100644
index 00000000000..197d5974ac0
--- /dev/null
+++ b/changelogs/unreleased/dmishunov-clone-btn-margin.yml
@@ -0,0 +1,5 @@
+---
+title: Added right margin to Clone Snippet button
+merge_request: 30471
+author:
+type: fixed
diff --git a/changelogs/unreleased/dmishunov-fix-project-snippet-redirect.yml b/changelogs/unreleased/dmishunov-fix-project-snippet-redirect.yml
new file mode 100644
index 00000000000..58c089f2eb0
--- /dev/null
+++ b/changelogs/unreleased/dmishunov-fix-project-snippet-redirect.yml
@@ -0,0 +1,5 @@
+---
+title: Fixed redirection to project snippets
+merge_request: 32530
+author:
+type: fixed
diff --git a/changelogs/unreleased/dmishunov-js-error-anon-view-snippet.yml b/changelogs/unreleased/dmishunov-js-error-anon-view-snippet.yml
new file mode 100644
index 00000000000..81197e76aaa
--- /dev/null
+++ b/changelogs/unreleased/dmishunov-js-error-anon-view-snippet.yml
@@ -0,0 +1,5 @@
+---
+title: Fixed JS error for anonymous views of a snippet
+merge_request: 29854
+author:
+type: fixed
diff --git a/changelogs/unreleased/dmishunov-snippet-edit-cancellation.yml b/changelogs/unreleased/dmishunov-snippet-edit-cancellation.yml
new file mode 100644
index 00000000000..56af03aed41
--- /dev/null
+++ b/changelogs/unreleased/dmishunov-snippet-edit-cancellation.yml
@@ -0,0 +1,5 @@
+---
+title: Fixed Cancel action on Snippet edit for existing snippets
+merge_request: 29993
+author:
+type: fixed
diff --git a/changelogs/unreleased/dont-support-raw-text-on-filter.yml b/changelogs/unreleased/dont-support-raw-text-on-filter.yml
new file mode 100644
index 00000000000..97d47e02b7d
--- /dev/null
+++ b/changelogs/unreleased/dont-support-raw-text-on-filter.yml
@@ -0,0 +1,5 @@
+---
+title: Show warning message to user if raw text search is used when filtering pipelines
+merge_request: 31942
+author:
+type: changed
diff --git a/changelogs/unreleased/dz-deprecate-plugins-dir.yml b/changelogs/unreleased/dz-deprecate-plugins-dir.yml
new file mode 100644
index 00000000000..e47fdcf7b54
--- /dev/null
+++ b/changelogs/unreleased/dz-deprecate-plugins-dir.yml
@@ -0,0 +1,5 @@
+---
+title: Deprecate /plugins directory
+merge_request: 29678
+author:
+type: deprecated
diff --git a/changelogs/unreleased/dz-redirect-unscoped-issue-routes.yml b/changelogs/unreleased/dz-redirect-unscoped-issue-routes.yml
new file mode 100644
index 00000000000..41dc8259756
--- /dev/null
+++ b/changelogs/unreleased/dz-redirect-unscoped-issue-routes.yml
@@ -0,0 +1,5 @@
+---
+title: Redirect issues routes under /-/ scope
+merge_request: 28655
+author:
+type: changed
diff --git a/changelogs/unreleased/dz-remove-legacy-routes.yml b/changelogs/unreleased/dz-remove-legacy-routes.yml
new file mode 100644
index 00000000000..9233df74853
--- /dev/null
+++ b/changelogs/unreleased/dz-remove-legacy-routes.yml
@@ -0,0 +1,5 @@
+---
+title: Remove project routes that were deprecated before 12.1
+merge_request: 26808
+author:
+type: removed
diff --git a/changelogs/unreleased/dz-scope-autocomplete-routing.yml b/changelogs/unreleased/dz-scope-autocomplete-routing.yml
new file mode 100644
index 00000000000..73f9ab74292
--- /dev/null
+++ b/changelogs/unreleased/dz-scope-autocomplete-routing.yml
@@ -0,0 +1,5 @@
+---
+title: Move global autocomplete routes to /-/ scope
+merge_request: 30173
+author:
+type: changed
diff --git a/changelogs/unreleased/dz-scope-pipeline-routes.yml b/changelogs/unreleased/dz-scope-pipeline-routes.yml
new file mode 100644
index 00000000000..ed735200c43
--- /dev/null
+++ b/changelogs/unreleased/dz-scope-pipeline-routes.yml
@@ -0,0 +1,5 @@
+---
+title: Copy pipelines routing under - scope
+merge_request: 30159
+author:
+type: changed
diff --git a/changelogs/unreleased/dz-update-android-template.yml b/changelogs/unreleased/dz-update-android-template.yml
new file mode 100644
index 00000000000..06f027616d6
--- /dev/null
+++ b/changelogs/unreleased/dz-update-android-template.yml
@@ -0,0 +1,5 @@
+---
+title: Update android template
+merge_request: 32096
+author:
+type: fixed
diff --git a/changelogs/unreleased/e2300-cs-template.yml b/changelogs/unreleased/e2300-cs-template.yml
new file mode 100644
index 00000000000..39f792370d6
--- /dev/null
+++ b/changelogs/unreleased/e2300-cs-template.yml
@@ -0,0 +1,5 @@
+---
+title: Migrate Container-Scanning template to rules syntax
+merge_request: 30775
+author:
+type: changed
diff --git a/changelogs/unreleased/e2300-dast-template.yml b/changelogs/unreleased/e2300-dast-template.yml
new file mode 100644
index 00000000000..2d06deed7bd
--- /dev/null
+++ b/changelogs/unreleased/e2300-dast-template.yml
@@ -0,0 +1,5 @@
+---
+title: Migrate DAST CI template to rules syntax
+merge_request: 30776
+author:
+type: changed
diff --git a/changelogs/unreleased/e2300-ds-template.yml b/changelogs/unreleased/e2300-ds-template.yml
new file mode 100644
index 00000000000..cfa6a3cbb7c
--- /dev/null
+++ b/changelogs/unreleased/e2300-ds-template.yml
@@ -0,0 +1,5 @@
+---
+title: Migrate Dependency-Scanning CI template to rules syntax
+merge_request: 30907
+author:
+type: changed
diff --git a/changelogs/unreleased/e2300-lm-template.yml b/changelogs/unreleased/e2300-lm-template.yml
new file mode 100644
index 00000000000..26f133f24f4
--- /dev/null
+++ b/changelogs/unreleased/e2300-lm-template.yml
@@ -0,0 +1,5 @@
+---
+title: Migrate License-Scanning CI template to rules syntax
+merge_request: 30784
+author:
+type: changed
diff --git a/changelogs/unreleased/e2300-sast-template.yml b/changelogs/unreleased/e2300-sast-template.yml
new file mode 100644
index 00000000000..fe8ed585d4c
--- /dev/null
+++ b/changelogs/unreleased/e2300-sast-template.yml
@@ -0,0 +1,5 @@
+---
+title: Migrate SAST CI template to rules syntax
+merge_request: 31127
+author:
+type: changed
diff --git a/changelogs/unreleased/eb-download-daily-coverage-csv.yml b/changelogs/unreleased/eb-download-daily-coverage-csv.yml
new file mode 100644
index 00000000000..17080ba1abf
--- /dev/null
+++ b/changelogs/unreleased/eb-download-daily-coverage-csv.yml
@@ -0,0 +1,6 @@
+---
+title: Allow users to download a CSV of the recent daily code coverage values per
+ job
+merge_request: 27094
+author:
+type: added
diff --git a/changelogs/unreleased/ee-app-1.yml b/changelogs/unreleased/ee-app-1.yml
new file mode 100644
index 00000000000..11e2dad942b
--- /dev/null
+++ b/changelogs/unreleased/ee-app-1.yml
@@ -0,0 +1,5 @@
+---
+title: Move include_if_ee to last line in ee/app 1
+merge_request: 31832
+author: Rajendra Kadam
+type: fixed
diff --git a/changelogs/unreleased/emilyring-cluster-list-refactor-pagination.yml b/changelogs/unreleased/emilyring-cluster-list-refactor-pagination.yml
new file mode 100644
index 00000000000..042b744e912
--- /dev/null
+++ b/changelogs/unreleased/emilyring-cluster-list-refactor-pagination.yml
@@ -0,0 +1,5 @@
+---
+title: 'Cluster index refactor: Add missing pagination'
+merge_request: 32338
+author:
+type: changed
diff --git a/changelogs/unreleased/emilyring-terraform-mr-fix.yml b/changelogs/unreleased/emilyring-terraform-mr-fix.yml
new file mode 100644
index 00000000000..3b38388b110
--- /dev/null
+++ b/changelogs/unreleased/emilyring-terraform-mr-fix.yml
@@ -0,0 +1,5 @@
+---
+title: Remove Visibility from terraform widget
+merge_request: 30737
+author:
+type: fixed
diff --git a/changelogs/unreleased/enable-group-export-ndjson-by-default.yml b/changelogs/unreleased/enable-group-export-ndjson-by-default.yml
new file mode 100644
index 00000000000..74229070c33
--- /dev/null
+++ b/changelogs/unreleased/enable-group-export-ndjson-by-default.yml
@@ -0,0 +1,5 @@
+---
+title: Prepare group export feature to use ndjson
+merge_request: 31742
+author:
+type: changed
diff --git a/changelogs/unreleased/enable-group-import-ndjson-by-default.yml b/changelogs/unreleased/enable-group-import-ndjson-by-default.yml
new file mode 100644
index 00000000000..97a43b8e7ff
--- /dev/null
+++ b/changelogs/unreleased/enable-group-import-ndjson-by-default.yml
@@ -0,0 +1,5 @@
+---
+title: Prepare group import feature to use ndjson
+merge_request: 31741
+author:
+type: changed
diff --git a/changelogs/unreleased/enable-uploadpack-filters.yml b/changelogs/unreleased/enable-uploadpack-filters.yml
new file mode 100644
index 00000000000..d3db29ac2b8
--- /dev/null
+++ b/changelogs/unreleased/enable-uploadpack-filters.yml
@@ -0,0 +1,5 @@
+---
+title: Enable uploadpack filters by default
+merge_request: 29787
+author:
+type: added
diff --git a/changelogs/unreleased/error-list-colour.yml b/changelogs/unreleased/error-list-colour.yml
new file mode 100644
index 00000000000..8c4ffb8dc10
--- /dev/null
+++ b/changelogs/unreleased/error-list-colour.yml
@@ -0,0 +1,5 @@
+---
+title: Update error tracking table background colour to correct gray
+merge_request: 32133
+author:
+type: other
diff --git a/changelogs/unreleased/error-tracking-link-target.yml b/changelogs/unreleased/error-tracking-link-target.yml
new file mode 100644
index 00000000000..2bb1fbb368c
--- /dev/null
+++ b/changelogs/unreleased/error-tracking-link-target.yml
@@ -0,0 +1,5 @@
+---
+title: Error tracking target blank empty state
+merge_request: 30525
+author:
+type: other
diff --git a/changelogs/unreleased/exclude_html_entities_from_haml_lint.yml b/changelogs/unreleased/exclude_html_entities_from_haml_lint.yml
new file mode 100644
index 00000000000..77a445f6e4a
--- /dev/null
+++ b/changelogs/unreleased/exclude_html_entities_from_haml_lint.yml
@@ -0,0 +1,5 @@
+---
+title: Exclude html entities from haml lint
+merge_request: 29847
+author: Lee Tickett
+type: fixed
diff --git a/changelogs/unreleased/expose-issue-iid-in-alert-management-alert-graphql.yml b/changelogs/unreleased/expose-issue-iid-in-alert-management-alert-graphql.yml
new file mode 100644
index 00000000000..8c72c58f51a
--- /dev/null
+++ b/changelogs/unreleased/expose-issue-iid-in-alert-management-alert-graphql.yml
@@ -0,0 +1,5 @@
+---
+title: Exposes issue IID in Alert Management Alert's GraphQL endpoint
+merge_request: 31313
+author:
+type: added
diff --git a/changelogs/unreleased/expose-more-fields-in-alert-management-alert-graphql.yml b/changelogs/unreleased/expose-more-fields-in-alert-management-alert-graphql.yml
new file mode 100644
index 00000000000..7003180bb32
--- /dev/null
+++ b/changelogs/unreleased/expose-more-fields-in-alert-management-alert-graphql.yml
@@ -0,0 +1,5 @@
+---
+title: Exposes description, hosts, details, and timestamps for Alert Management Alert GraphQL
+merge_request: 31091
+author:
+type: changed
diff --git a/changelogs/unreleased/fe-ide-fix-scroll-in-right-pane.yml b/changelogs/unreleased/fe-ide-fix-scroll-in-right-pane.yml
new file mode 100644
index 00000000000..0a0dc3ead62
--- /dev/null
+++ b/changelogs/unreleased/fe-ide-fix-scroll-in-right-pane.yml
@@ -0,0 +1,5 @@
+---
+title: Fix no scroll when overflow in IDE right pane
+merge_request: 31961
+author:
+type: fixed
diff --git a/changelogs/unreleased/feat-add-expunge-option-mailroom.yml b/changelogs/unreleased/feat-add-expunge-option-mailroom.yml
new file mode 100644
index 00000000000..efdc0eefb13
--- /dev/null
+++ b/changelogs/unreleased/feat-add-expunge-option-mailroom.yml
@@ -0,0 +1,5 @@
+---
+title: Add expunge deleted messages option to mailroom
+merge_request: 31531
+author: Diego Louzán
+type: added
diff --git a/changelogs/unreleased/feat-add-intermediate-cas-to-smime.yml b/changelogs/unreleased/feat-add-intermediate-cas-to-smime.yml
new file mode 100644
index 00000000000..3a771ba8af5
--- /dev/null
+++ b/changelogs/unreleased/feat-add-intermediate-cas-to-smime.yml
@@ -0,0 +1,5 @@
+---
+title: Add intermediate CAs capability to S/MIME email signature
+merge_request: 29352
+author: Diego Louzán
+type: added
diff --git a/changelogs/unreleased/feat-ldap-auth-admin-mode.yml b/changelogs/unreleased/feat-ldap-auth-admin-mode.yml
new file mode 100644
index 00000000000..aa06f99acc5
--- /dev/null
+++ b/changelogs/unreleased/feat-ldap-auth-admin-mode.yml
@@ -0,0 +1,5 @@
+---
+title: LDAP authentication support for admin mode
+merge_request: 28572
+author: Diego Louzán
+type: added
diff --git a/changelogs/unreleased/feat-profile-image-guidelines.yml b/changelogs/unreleased/feat-profile-image-guidelines.yml
new file mode 100644
index 00000000000..aefec7b533e
--- /dev/null
+++ b/changelogs/unreleased/feat-profile-image-guidelines.yml
@@ -0,0 +1,5 @@
+---
+title: Add option to add custom profile image guidelines
+merge_request: 29894
+author: Roger Meier
+type: added
diff --git a/changelogs/unreleased/feat-x509-tag.yml b/changelogs/unreleased/feat-x509-tag.yml
new file mode 100644
index 00000000000..b57b87a889f
--- /dev/null
+++ b/changelogs/unreleased/feat-x509-tag.yml
@@ -0,0 +1,5 @@
+---
+title: Display x509 signed tags
+merge_request: 27211
+author: Roger Meier
+type: added
diff --git a/changelogs/unreleased/feat-x509_issuer_crl_check.yml b/changelogs/unreleased/feat-x509_issuer_crl_check.yml
new file mode 100644
index 00000000000..b3b35cea9ff
--- /dev/null
+++ b/changelogs/unreleased/feat-x509_issuer_crl_check.yml
@@ -0,0 +1,5 @@
+---
+title: Add certification revocation list download and certificate revoke
+merge_request: 28336
+author: Roger Meier
+type: added
diff --git a/changelogs/unreleased/feature-next-prev-commit-buttons.yml b/changelogs/unreleased/feature-next-prev-commit-buttons.yml
new file mode 100644
index 00000000000..3a53f803349
--- /dev/null
+++ b/changelogs/unreleased/feature-next-prev-commit-buttons.yml
@@ -0,0 +1,5 @@
+---
+title: Add Previous and Next buttons for commit-by-commit navigation
+merge_request: 28596
+author:
+type: added
diff --git a/changelogs/unreleased/feature-refactor-keyboard-shortcuts-modal.yml b/changelogs/unreleased/feature-refactor-keyboard-shortcuts-modal.yml
new file mode 100644
index 00000000000..5dea0db0fba
--- /dev/null
+++ b/changelogs/unreleased/feature-refactor-keyboard-shortcuts-modal.yml
@@ -0,0 +1,5 @@
+---
+title: Clean up & Re-arrange the keyboard shortcuts modal
+merge_request: 28992
+author:
+type: other
diff --git a/changelogs/unreleased/ff-iid-routing.yml b/changelogs/unreleased/ff-iid-routing.yml
new file mode 100644
index 00000000000..73117dc2127
--- /dev/null
+++ b/changelogs/unreleased/ff-iid-routing.yml
@@ -0,0 +1,5 @@
+---
+title: Route to feature flags based on internal id
+merge_request: 29740
+author:
+type: added
diff --git a/changelogs/unreleased/ff-lists-api.yml b/changelogs/unreleased/ff-lists-api.yml
new file mode 100644
index 00000000000..42ee4910fbc
--- /dev/null
+++ b/changelogs/unreleased/ff-lists-api.yml
@@ -0,0 +1,5 @@
+---
+title: Add public API for feature flag user lists
+merge_request: 29415
+author:
+type: added
diff --git a/changelogs/unreleased/filter-pipeline-by-branch.yml b/changelogs/unreleased/filter-pipeline-by-branch.yml
new file mode 100644
index 00000000000..ae96b92151e
--- /dev/null
+++ b/changelogs/unreleased/filter-pipeline-by-branch.yml
@@ -0,0 +1,5 @@
+---
+title: Filter pipelines by trigger author and branch name
+merge_request: 31386
+author:
+type: added
diff --git a/changelogs/unreleased/fix-36628-create-a-rake-task-to-cleanup-unused-lfs-files.yml b/changelogs/unreleased/fix-36628-create-a-rake-task-to-cleanup-unused-lfs-files.yml
new file mode 100644
index 00000000000..ab70fc10f90
--- /dev/null
+++ b/changelogs/unreleased/fix-36628-create-a-rake-task-to-cleanup-unused-lfs-files.yml
@@ -0,0 +1,5 @@
+---
+title: Rake task gitlab:cleanup:orphan_lfs_files should clear the cached value or repository size
+merge_request: 32541
+author:
+type: fixed
diff --git a/changelogs/unreleased/fix-admin-mode-graphiql-explorer.yml b/changelogs/unreleased/fix-admin-mode-graphiql-explorer.yml
new file mode 100644
index 00000000000..e4c5091b59d
--- /dev/null
+++ b/changelogs/unreleased/fix-admin-mode-graphiql-explorer.yml
@@ -0,0 +1,5 @@
+---
+title: Fix admin mode access on GraphiQL controller
+merge_request: 29845
+author: Diego Louzán
+type: fixed
diff --git a/changelogs/unreleased/fix-approvals-given-count.yml b/changelogs/unreleased/fix-approvals-given-count.yml
new file mode 100644
index 00000000000..c05f0a9bbd2
--- /dev/null
+++ b/changelogs/unreleased/fix-approvals-given-count.yml
@@ -0,0 +1,5 @@
+---
+title: Fix number of approvals given calculation
+merge_request: 28293
+author: Steffen Köhler
+type: fixed
diff --git a/changelogs/unreleased/fix-branch-dot-txt.yml b/changelogs/unreleased/fix-branch-dot-txt.yml
new file mode 100644
index 00000000000..c6860cb0ecb
--- /dev/null
+++ b/changelogs/unreleased/fix-branch-dot-txt.yml
@@ -0,0 +1,5 @@
+---
+title: Fix API requests for branch names ending in .txt
+merge_request: 31446
+author: Daniel Stone
+type: fixed
diff --git a/changelogs/unreleased/fix-broken-heading-of-vue-3-migration-guide-doc.yml b/changelogs/unreleased/fix-broken-heading-of-vue-3-migration-guide-doc.yml
new file mode 100644
index 00000000000..4e8fc37bea0
--- /dev/null
+++ b/changelogs/unreleased/fix-broken-heading-of-vue-3-migration-guide-doc.yml
@@ -0,0 +1,5 @@
+---
+title: Fix broken heading of Vue 3 migration guide doc
+merge_request: 31951
+author: Gilang Gumilar
+type: fixed
diff --git a/changelogs/unreleased/fix-cop-inject-multiple.yml b/changelogs/unreleased/fix-cop-inject-multiple.yml
new file mode 100644
index 00000000000..8fe1556abac
--- /dev/null
+++ b/changelogs/unreleased/fix-cop-inject-multiple.yml
@@ -0,0 +1,5 @@
+---
+title: Allow multiple usage of EE extension/inclusion on last lines
+merge_request: 31183
+author: Rajendra Kadam
+type: fixed
diff --git a/changelogs/unreleased/fix-how-to-checkout-mr-link.yml b/changelogs/unreleased/fix-how-to-checkout-mr-link.yml
new file mode 100644
index 00000000000..783ee22cccd
--- /dev/null
+++ b/changelogs/unreleased/fix-how-to-checkout-mr-link.yml
@@ -0,0 +1,5 @@
+---
+title: Fix "how to checkout MR" help link
+merge_request: 31688
+author:
+type: fixed
diff --git a/changelogs/unreleased/fix-issue-details-row-emoji-block.yml b/changelogs/unreleased/fix-issue-details-row-emoji-block.yml
new file mode 100644
index 00000000000..15d9025e3b4
--- /dev/null
+++ b/changelogs/unreleased/fix-issue-details-row-emoji-block.yml
@@ -0,0 +1,5 @@
+---
+title: Fix layout in issue view, on large screen some buttons were misaligned
+merge_request: 30947
+author: Michele (macno) Azzolari
+type: fixed
diff --git a/changelogs/unreleased/fix-istio-url-should-be-raw.yml b/changelogs/unreleased/fix-istio-url-should-be-raw.yml
new file mode 100644
index 00000000000..9956260a347
--- /dev/null
+++ b/changelogs/unreleased/fix-istio-url-should-be-raw.yml
@@ -0,0 +1,5 @@
+---
+title: Fix Istio broken Istio metrics installation
+merge_request: 31382
+author:
+type: fixed
diff --git a/changelogs/unreleased/fix-mentions-personal-snippet.yml b/changelogs/unreleased/fix-mentions-personal-snippet.yml
new file mode 100644
index 00000000000..f6f17038b0f
--- /dev/null
+++ b/changelogs/unreleased/fix-mentions-personal-snippet.yml
@@ -0,0 +1,5 @@
+---
+title: Fix bug in personal snippets when somebody is mentioned
+merge_request: 29835
+author: Sashi Kumar
+type: fixed
diff --git a/changelogs/unreleased/fix-missing-rss-feed-events.yml b/changelogs/unreleased/fix-missing-rss-feed-events.yml
new file mode 100644
index 00000000000..74af7425772
--- /dev/null
+++ b/changelogs/unreleased/fix-missing-rss-feed-events.yml
@@ -0,0 +1,5 @@
+---
+title: Fix missing RSS feed events
+merge_request: 28054
+author:
+type: fixed
diff --git a/changelogs/unreleased/fix-nested-search-for-matching-ds-files.yml b/changelogs/unreleased/fix-nested-search-for-matching-ds-files.yml
new file mode 100644
index 00000000000..4d50cd8e3a2
--- /dev/null
+++ b/changelogs/unreleased/fix-nested-search-for-matching-ds-files.yml
@@ -0,0 +1,5 @@
+---
+title: Add nested file detection for Dependency Scanning
+merge_request: 31932
+author:
+type: fixed
diff --git a/changelogs/unreleased/fix-new-mr-project-commit-link.yml b/changelogs/unreleased/fix-new-mr-project-commit-link.yml
new file mode 100644
index 00000000000..97c07f0e3d4
--- /dev/null
+++ b/changelogs/unreleased/fix-new-mr-project-commit-link.yml
@@ -0,0 +1,5 @@
+---
+title: 'fix: use the source project to generate commit links for un-persisted merge requests'
+merge_request: 29243
+author: Chieh-Min Wang
+type: fixed
diff --git a/changelogs/unreleased/fix-protected-vars-by-default.yml b/changelogs/unreleased/fix-protected-vars-by-default.yml
new file mode 100644
index 00000000000..97e7ae61a24
--- /dev/null
+++ b/changelogs/unreleased/fix-protected-vars-by-default.yml
@@ -0,0 +1,6 @@
+---
+title: Fixes bug where variables were not protected by default when using the correct
+ CI/CD admin setting
+merge_request: 31655
+author:
+type: fixed
diff --git a/changelogs/unreleased/fix-started-guide-link-201724.yml b/changelogs/unreleased/fix-started-guide-link-201724.yml
new file mode 100644
index 00000000000..93c56a78ff9
--- /dev/null
+++ b/changelogs/unreleased/fix-started-guide-link-201724.yml
@@ -0,0 +1,5 @@
+---
+title: Replace the outdated link
+merge_request: 31874
+author: Renamoo
+type: fixed
diff --git a/changelogs/unreleased/fix-subgroup-milestone-links.yml b/changelogs/unreleased/fix-subgroup-milestone-links.yml
new file mode 100644
index 00000000000..dab14b33f9e
--- /dev/null
+++ b/changelogs/unreleased/fix-subgroup-milestone-links.yml
@@ -0,0 +1,5 @@
+---
+title: Link to subgroup milestones correctly from group milestones page
+merge_request: 31383
+author:
+type: fixed
diff --git a/changelogs/unreleased/fix-wiki-activity-icons.yml b/changelogs/unreleased/fix-wiki-activity-icons.yml
new file mode 100644
index 00000000000..9d281c902ab
--- /dev/null
+++ b/changelogs/unreleased/fix-wiki-activity-icons.yml
@@ -0,0 +1,5 @@
+---
+title: Add Activity icons for Wiki updated and destroyed events
+merge_request: 30349
+author:
+type: fixed
diff --git a/changelogs/unreleased/fj-208904-add-snippet-backfilling-repository-migration.yml b/changelogs/unreleased/fj-208904-add-snippet-backfilling-repository-migration.yml
new file mode 100644
index 00000000000..7f00f98497f
--- /dev/null
+++ b/changelogs/unreleased/fj-208904-add-snippet-backfilling-repository-migration.yml
@@ -0,0 +1,5 @@
+---
+title: Add snippet repository backfilling migration
+merge_request: 29927
+author:
+type: other
diff --git a/changelogs/unreleased/fj-214692-avoid-commit-when-content-file-name-not-updated.yml b/changelogs/unreleased/fj-214692-avoid-commit-when-content-file-name-not-updated.yml
new file mode 100644
index 00000000000..3da4417fc3a
--- /dev/null
+++ b/changelogs/unreleased/fj-214692-avoid-commit-when-content-file-name-not-updated.yml
@@ -0,0 +1,5 @@
+---
+title: Avoid commit when snippet file_name and content are not present
+merge_request: 29761
+author:
+type: changed
diff --git a/changelogs/unreleased/fj-215861-add-rake-tasks-to-migrate-snippets.yml b/changelogs/unreleased/fj-215861-add-rake-tasks-to-migrate-snippets.yml
new file mode 100644
index 00000000000..5b487c9caf7
--- /dev/null
+++ b/changelogs/unreleased/fj-215861-add-rake-tasks-to-migrate-snippets.yml
@@ -0,0 +1,5 @@
+---
+title: Add snippet migration rake tasks
+merge_request: 30489
+author:
+type: other
diff --git a/changelogs/unreleased/fj-add-files-param-snippet-create-mutation.yml b/changelogs/unreleased/fj-add-files-param-snippet-create-mutation.yml
new file mode 100644
index 00000000000..16dbe415f3f
--- /dev/null
+++ b/changelogs/unreleased/fj-add-files-param-snippet-create-mutation.yml
@@ -0,0 +1,5 @@
+---
+title: Add files param to snippet create mutation
+merge_request: 32309
+author:
+type: changed
diff --git a/changelogs/unreleased/fj-add-migration-user.yml b/changelogs/unreleased/fj-add-migration-user.yml
new file mode 100644
index 00000000000..ac851add77d
--- /dev/null
+++ b/changelogs/unreleased/fj-add-migration-user.yml
@@ -0,0 +1,5 @@
+---
+title: Add migration bot user
+merge_request: 30738
+author:
+type: added
diff --git a/changelogs/unreleased/fj-add-presence-validation-rest-snippet-endpoints.yml b/changelogs/unreleased/fj-add-presence-validation-rest-snippet-endpoints.yml
new file mode 100644
index 00000000000..2abea83c135
--- /dev/null
+++ b/changelogs/unreleased/fj-add-presence-validation-rest-snippet-endpoints.yml
@@ -0,0 +1,5 @@
+---
+title: Add presence validation to content and title in snippet rest endpoints
+merge_request: 32522
+author:
+type: changed
diff --git a/changelogs/unreleased/fj-avoid-repository-size-checks-if-migration-bot.yml b/changelogs/unreleased/fj-avoid-repository-size-checks-if-migration-bot.yml
new file mode 100644
index 00000000000..5a1d3636832
--- /dev/null
+++ b/changelogs/unreleased/fj-avoid-repository-size-checks-if-migration-bot.yml
@@ -0,0 +1,5 @@
+---
+title: Avoid repository size checkings in snippet migrations for migration bot
+merge_request: 31473
+author:
+type: fixed
diff --git a/changelogs/unreleased/fj-expand-graphql-snippet-blob-type.yml b/changelogs/unreleased/fj-expand-graphql-snippet-blob-type.yml
new file mode 100644
index 00000000000..48e9393f9e8
--- /dev/null
+++ b/changelogs/unreleased/fj-expand-graphql-snippet-blob-type.yml
@@ -0,0 +1,5 @@
+---
+title: Add fields to GraphQL snippet blob type
+merge_request: 31710
+author:
+type: changed
diff --git a/changelogs/unreleased/fj-fix-bug-snippet-blob-viewer-graphql.yml b/changelogs/unreleased/fj-fix-bug-snippet-blob-viewer-graphql.yml
new file mode 100644
index 00000000000..ab6285d3cf6
--- /dev/null
+++ b/changelogs/unreleased/fj-fix-bug-snippet-blob-viewer-graphql.yml
@@ -0,0 +1,5 @@
+---
+title: Fix bug in Snippet BlobViewer GraphQL definition
+merge_request: 30927
+author:
+type: fixed
diff --git a/changelogs/unreleased/fj-fix-migration-when-user-invalid-commit-name.yml b/changelogs/unreleased/fj-fix-migration-when-user-invalid-commit-name.yml
new file mode 100644
index 00000000000..9245c099208
--- /dev/null
+++ b/changelogs/unreleased/fj-fix-migration-when-user-invalid-commit-name.yml
@@ -0,0 +1,5 @@
+---
+title: Fix snippet migration when user has invalid info
+merge_request: 31488
+author:
+type: fixed
diff --git a/changelogs/unreleased/fj-fix-raw-snippet-endpoint.yml b/changelogs/unreleased/fj-fix-raw-snippet-endpoint.yml
new file mode 100644
index 00000000000..2261367b5aa
--- /dev/null
+++ b/changelogs/unreleased/fj-fix-raw-snippet-endpoint.yml
@@ -0,0 +1,5 @@
+---
+title: Return content from repo in snippet raw endpoint
+merge_request: 29781
+author:
+type: fixed
diff --git a/changelogs/unreleased/fj-fix-templates-endpoint-when-project-has-dots.yml b/changelogs/unreleased/fj-fix-templates-endpoint-when-project-has-dots.yml
new file mode 100644
index 00000000000..3de4c3eb6de
--- /dev/null
+++ b/changelogs/unreleased/fj-fix-templates-endpoint-when-project-has-dots.yml
@@ -0,0 +1,5 @@
+---
+title: Fix templates API endpoint when project name has dots
+merge_request: 31758
+author:
+type: fixed
diff --git a/changelogs/unreleased/fj-move-raw-url-to-snippet-entity.yml b/changelogs/unreleased/fj-move-raw-url-to-snippet-entity.yml
new file mode 100644
index 00000000000..1ac98fec468
--- /dev/null
+++ b/changelogs/unreleased/fj-move-raw-url-to-snippet-entity.yml
@@ -0,0 +1,5 @@
+---
+title: Move snippet raw_url attribute to base entity
+merge_request: 29776
+author:
+type: fixed
diff --git a/changelogs/unreleased/fj-remove-file-name-in-snippet-lists.yml b/changelogs/unreleased/fj-remove-file-name-in-snippet-lists.yml
new file mode 100644
index 00000000000..941903b52a2
--- /dev/null
+++ b/changelogs/unreleased/fj-remove-file-name-in-snippet-lists.yml
@@ -0,0 +1,5 @@
+---
+title: Remove snippet file_name from snippet lists
+merge_request: 29937
+author:
+type: changed
diff --git a/changelogs/unreleased/fj-remove-flag-version-snippets.yml b/changelogs/unreleased/fj-remove-flag-version-snippets.yml
new file mode 100644
index 00000000000..a6c9193ffdd
--- /dev/null
+++ b/changelogs/unreleased/fj-remove-flag-version-snippets.yml
@@ -0,0 +1,5 @@
+---
+title: Enabling git versioned snippets
+merge_request: 27705
+author:
+type: added
diff --git a/changelogs/unreleased/fj-return-proper-file-name-snippet-entity.yml b/changelogs/unreleased/fj-return-proper-file-name-snippet-entity.yml
new file mode 100644
index 00000000000..fc44ce269d0
--- /dev/null
+++ b/changelogs/unreleased/fj-return-proper-file-name-snippet-entity.yml
@@ -0,0 +1,5 @@
+---
+title: Return file name from repo in snippet endpoints
+merge_request: 29785
+author:
+type: fixed
diff --git a/changelogs/unreleased/fj-update-snippet-backfilling-with-migrate-bot.yml b/changelogs/unreleased/fj-update-snippet-backfilling-with-migrate-bot.yml
new file mode 100644
index 00000000000..07030a44875
--- /dev/null
+++ b/changelogs/unreleased/fj-update-snippet-backfilling-with-migrate-bot.yml
@@ -0,0 +1,5 @@
+---
+title: Use migration bot user in snippet migration
+merge_request: 30762
+author:
+type: fixed
diff --git a/changelogs/unreleased/georgekoltsov-add-group-export-email-nofitication.yml b/changelogs/unreleased/georgekoltsov-add-group-export-email-nofitication.yml
new file mode 100644
index 00000000000..ea968288f28
--- /dev/null
+++ b/changelogs/unreleased/georgekoltsov-add-group-export-email-nofitication.yml
@@ -0,0 +1,5 @@
+---
+title: Add email notification on group export complete
+merge_request: 30522
+author:
+type: added
diff --git a/changelogs/unreleased/graphql_log_user_last_activity_on.yml b/changelogs/unreleased/graphql_log_user_last_activity_on.yml
new file mode 100644
index 00000000000..19c1115a179
--- /dev/null
+++ b/changelogs/unreleased/graphql_log_user_last_activity_on.yml
@@ -0,0 +1,5 @@
+---
+title: Add user last_activity logging in GraphQL
+merge_request: 23063
+author:
+type: fixed
diff --git a/changelogs/unreleased/group-shared-projects-api.yml b/changelogs/unreleased/group-shared-projects-api.yml
new file mode 100644
index 00000000000..8b42273c921
--- /dev/null
+++ b/changelogs/unreleased/group-shared-projects-api.yml
@@ -0,0 +1,5 @@
+---
+title: Introduce API for fetching shared projects in a group
+merge_request: 30461
+author:
+type: added
diff --git a/changelogs/unreleased/id-cache-logs-tree.yml b/changelogs/unreleased/id-cache-logs-tree.yml
new file mode 100644
index 00000000000..6722d4dcc9d
--- /dev/null
+++ b/changelogs/unreleased/id-cache-logs-tree.yml
@@ -0,0 +1,5 @@
+---
+title: Cache TreeSummary response for logs_tree
+merge_request: 29828
+author:
+type: performance
diff --git a/changelogs/unreleased/id-fetch-raw-artifact-of-any-type.yml b/changelogs/unreleased/id-fetch-raw-artifact-of-any-type.yml
new file mode 100644
index 00000000000..710855593d5
--- /dev/null
+++ b/changelogs/unreleased/id-fetch-raw-artifact-of-any-type.yml
@@ -0,0 +1,5 @@
+---
+title: View raw file of any zip artifacts
+merge_request: 31912
+author:
+type: added
diff --git a/changelogs/unreleased/id-fix-diff-link-on-search.yml b/changelogs/unreleased/id-fix-diff-link-on-search.yml
new file mode 100644
index 00000000000..defc9c4e27a
--- /dev/null
+++ b/changelogs/unreleased/id-fix-diff-link-on-search.yml
@@ -0,0 +1,5 @@
+---
+title: Fix blob link for the code search
+merge_request: 30473
+author:
+type: fixed
diff --git a/changelogs/unreleased/id-remove-diff-compare-with-head-feature-flag.yml b/changelogs/unreleased/id-remove-diff-compare-with-head-feature-flag.yml
new file mode 100644
index 00000000000..c7222c6583d
--- /dev/null
+++ b/changelogs/unreleased/id-remove-diff-compare-with-head-feature-flag.yml
@@ -0,0 +1,5 @@
+---
+title: Allow showing merge request diffs compared to current version of target branch
+merge_request: 31325
+author:
+type: added
diff --git a/changelogs/unreleased/ifrenkel-remove-pipfilelock-from-ds-tpl.yml b/changelogs/unreleased/ifrenkel-remove-pipfilelock-from-ds-tpl.yml
new file mode 100644
index 00000000000..f4833b79fc5
--- /dev/null
+++ b/changelogs/unreleased/ifrenkel-remove-pipfilelock-from-ds-tpl.yml
@@ -0,0 +1,5 @@
+---
+title: Remove detection of file in Dependency Scanning template
+merge_request: 31819
+author:
+type: fixed
diff --git a/changelogs/unreleased/improve_add_remove_labels_api.yml b/changelogs/unreleased/improve_add_remove_labels_api.yml
new file mode 100644
index 00000000000..13b502edef7
--- /dev/null
+++ b/changelogs/unreleased/improve_add_remove_labels_api.yml
@@ -0,0 +1,5 @@
+---
+title: Add ability to add or remove MR labels via API
+merge_request: 31522
+author: Lee Tickett
+type: changed
diff --git a/changelogs/unreleased/initial_repository_storage_move.yml b/changelogs/unreleased/initial_repository_storage_move.yml
new file mode 100644
index 00000000000..376f3361c49
--- /dev/null
+++ b/changelogs/unreleased/initial_repository_storage_move.yml
@@ -0,0 +1,5 @@
+---
+title: Store status of repository storage moves
+merge_request: 29095
+author:
+type: changed
diff --git a/changelogs/unreleased/ipython_katex_fixes.yml b/changelogs/unreleased/ipython_katex_fixes.yml
new file mode 100644
index 00000000000..af1c96bd64f
--- /dev/null
+++ b/changelogs/unreleased/ipython_katex_fixes.yml
@@ -0,0 +1,5 @@
+---
+title: Katex render and vscode output improvements for markdown
+merge_request: 31433
+author: Reinhold Gschweicher <pyro4hell@gmail.com>
+type: fixed
diff --git a/changelogs/unreleased/jh-group_export_ui_frontend.yml b/changelogs/unreleased/jh-group_export_ui_frontend.yml
new file mode 100644
index 00000000000..22d4819085f
--- /dev/null
+++ b/changelogs/unreleased/jh-group_export_ui_frontend.yml
@@ -0,0 +1,5 @@
+---
+title: Add UI for exporting group data to the group settings
+merge_request: 28573
+author:
+type: added
diff --git a/changelogs/unreleased/jira-user-importer-prep.yml b/changelogs/unreleased/jira-user-importer-prep.yml
new file mode 100644
index 00000000000..539b8e26a8b
--- /dev/null
+++ b/changelogs/unreleased/jira-user-importer-prep.yml
@@ -0,0 +1,5 @@
+---
+title: Fix mapping group membets as Jira issues authors/assignees
+merge_request: 30820
+author:
+type: fixed
diff --git a/changelogs/unreleased/jira-user-importer.yml b/changelogs/unreleased/jira-user-importer.yml
new file mode 100644
index 00000000000..b4a9f7af594
--- /dev/null
+++ b/changelogs/unreleased/jira-user-importer.yml
@@ -0,0 +1,5 @@
+---
+title: Map Jira issue assignee and author
+merge_request: 30498
+author:
+type: added
diff --git a/changelogs/unreleased/jivanvl-ensure-links-generated-manage-variables.yml b/changelogs/unreleased/jivanvl-ensure-links-generated-manage-variables.yml
new file mode 100644
index 00000000000..217b7ada151
--- /dev/null
+++ b/changelogs/unreleased/jivanvl-ensure-links-generated-manage-variables.yml
@@ -0,0 +1,5 @@
+---
+title: Ensure links generated by the copy link feature contain variables
+merge_request: 31636
+author:
+type: changed
diff --git a/changelogs/unreleased/jivanvl-remove-feature-flag-visibility-toggle-monitoring.yml b/changelogs/unreleased/jivanvl-remove-feature-flag-visibility-toggle-monitoring.yml
new file mode 100644
index 00000000000..dca7f86418f
--- /dev/null
+++ b/changelogs/unreleased/jivanvl-remove-feature-flag-visibility-toggle-monitoring.yml
@@ -0,0 +1,5 @@
+---
+title: Add metric dashboard public visibility toggle
+merge_request: 29718
+author:
+type: added
diff --git a/changelogs/unreleased/jivanvl-use-metrics-url-query-param.yml b/changelogs/unreleased/jivanvl-use-metrics-url-query-param.yml
new file mode 100644
index 00000000000..0d97385664f
--- /dev/null
+++ b/changelogs/unreleased/jivanvl-use-metrics-url-query-param.yml
@@ -0,0 +1,5 @@
+---
+title: In metrics dashboard use custom variables from URL in queries
+merge_request: 30560
+author:
+type: added
diff --git a/changelogs/unreleased/json-gem-upgrade.yml b/changelogs/unreleased/json-gem-upgrade.yml
new file mode 100644
index 00000000000..4e5792ba815
--- /dev/null
+++ b/changelogs/unreleased/json-gem-upgrade.yml
@@ -0,0 +1,5 @@
+---
+title: Upgrade json gem to 2.3.0
+merge_request: 30852
+author:
+type: performance
diff --git a/changelogs/unreleased/kc-escape-dashboard-path-annotations.yml b/changelogs/unreleased/kc-escape-dashboard-path-annotations.yml
new file mode 100644
index 00000000000..9b21cb7de03
--- /dev/null
+++ b/changelogs/unreleased/kc-escape-dashboard-path-annotations.yml
@@ -0,0 +1,5 @@
+---
+title: Decode dashboard_path when creating annotations
+merge_request: 31665
+author:
+type: fixed
diff --git a/changelogs/unreleased/kubeclient-create-or-update.yml b/changelogs/unreleased/kubeclient-create-or-update.yml
new file mode 100644
index 00000000000..143738c5195
--- /dev/null
+++ b/changelogs/unreleased/kubeclient-create-or-update.yml
@@ -0,0 +1,5 @@
+---
+title: Uses Kubernetes API conventions to create or update a resource leandrogs
+merge_request: 29010
+author: Leandro Silva
+type: performance
diff --git a/changelogs/unreleased/leaky-constant-fix-1.yml b/changelogs/unreleased/leaky-constant-fix-1.yml
new file mode 100644
index 00000000000..f0a9df59e4a
--- /dev/null
+++ b/changelogs/unreleased/leaky-constant-fix-1.yml
@@ -0,0 +1,5 @@
+---
+title: Add class stubs and fix leaky constant cop alert
+merge_request: 31938
+author: Rajendra Kadam
+type: fixed
diff --git a/changelogs/unreleased/leaky-constant-fix-10.yml b/changelogs/unreleased/leaky-constant-fix-10.yml
new file mode 100644
index 00000000000..696bb5aeaa4
--- /dev/null
+++ b/changelogs/unreleased/leaky-constant-fix-10.yml
@@ -0,0 +1,5 @@
+---
+title: Fix leaky constant issue in retry build service check
+merge_request: 32038
+author: Rajendra Kadam
+type: fixed
diff --git a/changelogs/unreleased/leaky-constant-fix-12.yml b/changelogs/unreleased/leaky-constant-fix-12.yml
new file mode 100644
index 00000000000..77caebc778b
--- /dev/null
+++ b/changelogs/unreleased/leaky-constant-fix-12.yml
@@ -0,0 +1,5 @@
+---
+title: Fix leaky constant issue in env assignment spec
+merge_request: 32040
+author: Rajendra Kadam
+type: fixed
diff --git a/changelogs/unreleased/leaky-constant-fix-13.yml b/changelogs/unreleased/leaky-constant-fix-13.yml
new file mode 100644
index 00000000000..b2333e1f03c
--- /dev/null
+++ b/changelogs/unreleased/leaky-constant-fix-13.yml
@@ -0,0 +1,5 @@
+---
+title: Fix leaky constant issue in statistics api spec
+merge_request: 32042
+author: Rajendra Kadam
+type: fixed
diff --git a/changelogs/unreleased/leaky-constant-fix-15.yml b/changelogs/unreleased/leaky-constant-fix-15.yml
new file mode 100644
index 00000000000..91d93a4e7e1
--- /dev/null
+++ b/changelogs/unreleased/leaky-constant-fix-15.yml
@@ -0,0 +1,5 @@
+---
+title: Fix leaky constant issue in merge request policy spec
+merge_request: 32044
+author: Rajendra Kadam
+type: fixed
diff --git a/changelogs/unreleased/leaky-constant-fix-16.yml b/changelogs/unreleased/leaky-constant-fix-16.yml
new file mode 100644
index 00000000000..ef3d3784635
--- /dev/null
+++ b/changelogs/unreleased/leaky-constant-fix-16.yml
@@ -0,0 +1,5 @@
+---
+title: Fix leaky constant issue in tree spec
+merge_request: 32045
+author: Rajendra Kadam
+type: fixed
diff --git a/changelogs/unreleased/leaky-constant-fix-17.yml b/changelogs/unreleased/leaky-constant-fix-17.yml
new file mode 100644
index 00000000000..9f7d2fbf710
--- /dev/null
+++ b/changelogs/unreleased/leaky-constant-fix-17.yml
@@ -0,0 +1,5 @@
+---
+title: Fix leaky constant issue in mentionable spec
+merge_request: 32049
+author: Rajendra Kadam
+type: fixed
diff --git a/changelogs/unreleased/leaky-constant-fix-18.yml b/changelogs/unreleased/leaky-constant-fix-18.yml
new file mode 100644
index 00000000000..8609dc0b7b5
--- /dev/null
+++ b/changelogs/unreleased/leaky-constant-fix-18.yml
@@ -0,0 +1,5 @@
+---
+title: Fix leaky constant issue in json serialization spec
+merge_request: 32051
+author: Rajendra Kadam
+type: fixed
diff --git a/changelogs/unreleased/leaky-constant-fix-19.yml b/changelogs/unreleased/leaky-constant-fix-19.yml
new file mode 100644
index 00000000000..9a3bea50b49
--- /dev/null
+++ b/changelogs/unreleased/leaky-constant-fix-19.yml
@@ -0,0 +1,5 @@
+---
+title: Fix leaky constant issue in cluster spec
+merge_request: 32053
+author: Rajendra Kadam
+type: fixed
diff --git a/changelogs/unreleased/leaky-constant-fix-2.yml b/changelogs/unreleased/leaky-constant-fix-2.yml
new file mode 100644
index 00000000000..194450da556
--- /dev/null
+++ b/changelogs/unreleased/leaky-constant-fix-2.yml
@@ -0,0 +1,5 @@
+---
+title: Add class stubs and fix leaky constant alert in content whitelist spec
+merge_request: 31946
+author: Rajendra Kadam
+type: fixed
diff --git a/changelogs/unreleased/leaky-constant-fix-20.yml b/changelogs/unreleased/leaky-constant-fix-20.yml
new file mode 100644
index 00000000000..6f434e003b4
--- /dev/null
+++ b/changelogs/unreleased/leaky-constant-fix-20.yml
@@ -0,0 +1,5 @@
+---
+title: Fix leaky constant issue in nulls pt2 spec
+merge_request: 32058
+author: Rajendra Kadam
+type: fixed
diff --git a/changelogs/unreleased/leaky-constant-fix-21.yml b/changelogs/unreleased/leaky-constant-fix-21.yml
new file mode 100644
index 00000000000..49b5e604494
--- /dev/null
+++ b/changelogs/unreleased/leaky-constant-fix-21.yml
@@ -0,0 +1,5 @@
+---
+title: Fix leaky constant issue in application settings encrypt spec
+merge_request: 32066
+author: Rajendra Kadam
+type: fixed
diff --git a/changelogs/unreleased/leaky-constant-fix-23.yml b/changelogs/unreleased/leaky-constant-fix-23.yml
new file mode 100644
index 00000000000..71b3d4d3cf9
--- /dev/null
+++ b/changelogs/unreleased/leaky-constant-fix-23.yml
@@ -0,0 +1,5 @@
+---
+title: Fix leaky constant issue in system check spec
+merge_request: 32080
+author: Rajendra Kadam
+type: fixed
diff --git a/changelogs/unreleased/leaky-constant-fix-24.yml b/changelogs/unreleased/leaky-constant-fix-24.yml
new file mode 100644
index 00000000000..9eb6a99190c
--- /dev/null
+++ b/changelogs/unreleased/leaky-constant-fix-24.yml
@@ -0,0 +1,5 @@
+---
+title: Fix leaky constant issue in simple executor spec
+merge_request: 32082
+author: Rajendra Kadam
+type: fixed
diff --git a/changelogs/unreleased/leaky-constant-fix-25.yml b/changelogs/unreleased/leaky-constant-fix-25.yml
new file mode 100644
index 00000000000..4ef55381de7
--- /dev/null
+++ b/changelogs/unreleased/leaky-constant-fix-25.yml
@@ -0,0 +1,5 @@
+---
+title: Fix leaky constant issue in jwt spec
+merge_request: 32093
+author: Rajendra Kadam
+type: fixed
diff --git a/changelogs/unreleased/leaky-constant-fix-26.yml b/changelogs/unreleased/leaky-constant-fix-26.yml
new file mode 100644
index 00000000000..c567b5dc36a
--- /dev/null
+++ b/changelogs/unreleased/leaky-constant-fix-26.yml
@@ -0,0 +1,5 @@
+---
+title: Fix leaky constant issue in factory spec
+merge_request: 32099
+author: Rajendra Kadam
+type: fixed
diff --git a/changelogs/unreleased/leaky-constant-fix-27.yml b/changelogs/unreleased/leaky-constant-fix-27.yml
new file mode 100644
index 00000000000..75baec2fb78
--- /dev/null
+++ b/changelogs/unreleased/leaky-constant-fix-27.yml
@@ -0,0 +1,5 @@
+---
+title: Fix leaky constant issue in sidekiq middleware spec
+merge_request: 32101
+author: Rajendra Kadam
+type: fixed
diff --git a/changelogs/unreleased/leaky-constant-fix-35.yml b/changelogs/unreleased/leaky-constant-fix-35.yml
new file mode 100644
index 00000000000..8a58ea944b9
--- /dev/null
+++ b/changelogs/unreleased/leaky-constant-fix-35.yml
@@ -0,0 +1,5 @@
+---
+title: Fix leaky constant issue connection, master check and attr config spec
+merge_request: 32144
+author: Rajendra Kadam
+type: fixed
diff --git a/changelogs/unreleased/leaky-constant-fix-4.yml b/changelogs/unreleased/leaky-constant-fix-4.yml
new file mode 100644
index 00000000000..fa1a25c7bba
--- /dev/null
+++ b/changelogs/unreleased/leaky-constant-fix-4.yml
@@ -0,0 +1,5 @@
+---
+title: Add class stubs and fix leaky constant alert in query recorder spec
+merge_request: 31954
+author: Rajendra Kadam
+type: fixed
diff --git a/changelogs/unreleased/leaky-constant-fix-6.yml b/changelogs/unreleased/leaky-constant-fix-6.yml
new file mode 100644
index 00000000000..d4b83631f0b
--- /dev/null
+++ b/changelogs/unreleased/leaky-constant-fix-6.yml
@@ -0,0 +1,5 @@
+---
+title: Fix leaky constant cop issue in clone dashboard service spec
+merge_request: 31962
+author: Rajendra Kadam
+type: fixed
diff --git a/changelogs/unreleased/leaky-constant-fix-7.yml b/changelogs/unreleased/leaky-constant-fix-7.yml
new file mode 100644
index 00000000000..407ea5ee839
--- /dev/null
+++ b/changelogs/unreleased/leaky-constant-fix-7.yml
@@ -0,0 +1,5 @@
+---
+title: Stub class constant in resolve discussion spec
+merge_request: 31965
+author: Rajendra Kadam
+type: fixed
diff --git a/changelogs/unreleased/leaky-constant-fix-8.yml b/changelogs/unreleased/leaky-constant-fix-8.yml
new file mode 100644
index 00000000000..8b55a9a1e41
--- /dev/null
+++ b/changelogs/unreleased/leaky-constant-fix-8.yml
@@ -0,0 +1,5 @@
+---
+title: Fix leaky constant issue in upgrade progress service check
+merge_request: 31969
+author: Rajendra Kadam
+type: fixed
diff --git a/changelogs/unreleased/lib-gitlab-refactoring-2.yml b/changelogs/unreleased/lib-gitlab-refactoring-2.yml
new file mode 100644
index 00000000000..7b6e2fefee8
--- /dev/null
+++ b/changelogs/unreleased/lib-gitlab-refactoring-2.yml
@@ -0,0 +1,5 @@
+---
+title: Move prepend to last line in lib/gitlab files
+merge_request: 30070
+author: Rajendra Kadam
+type: fixed
diff --git a/changelogs/unreleased/lib-gitlab-refactoring-3.yml b/changelogs/unreleased/lib-gitlab-refactoring-3.yml
new file mode 100644
index 00000000000..ef46e71a67d
--- /dev/null
+++ b/changelogs/unreleased/lib-gitlab-refactoring-3.yml
@@ -0,0 +1,5 @@
+---
+title: Move prepend to last line in lib/gitlab files
+merge_request: 30194
+author: Rajendra Kadam
+type: fixed
diff --git a/changelogs/unreleased/lib-gitlab-refactoring-4.yml b/changelogs/unreleased/lib-gitlab-refactoring-4.yml
new file mode 100644
index 00000000000..1ee06d4c496
--- /dev/null
+++ b/changelogs/unreleased/lib-gitlab-refactoring-4.yml
@@ -0,0 +1,5 @@
+---
+title: Move prepend to last line in lib/gitlab files
+merge_request: 30289
+author: Rajendra Kadam
+type: fixed
diff --git a/changelogs/unreleased/lib-gitlab-refactoring-5.yml b/changelogs/unreleased/lib-gitlab-refactoring-5.yml
new file mode 100644
index 00000000000..0f3cd603f07
--- /dev/null
+++ b/changelogs/unreleased/lib-gitlab-refactoring-5.yml
@@ -0,0 +1,5 @@
+---
+title: Move prepend to last line in lib/gitlab files
+merge_request: 30291
+author: Rajendra Kadam
+type: fixed
diff --git a/changelogs/unreleased/lib-gitlab-refactoring.yml b/changelogs/unreleased/lib-gitlab-refactoring.yml
new file mode 100644
index 00000000000..4caa45b6fc9
--- /dev/null
+++ b/changelogs/unreleased/lib-gitlab-refactoring.yml
@@ -0,0 +1,5 @@
+---
+title: Move prepend to last line in lib/gitlab files
+merge_request: 29938
+author: Rajendra
+type: fixed
diff --git a/changelogs/unreleased/markdown-toolbar-spaces.yml b/changelogs/unreleased/markdown-toolbar-spaces.yml
new file mode 100644
index 00000000000..07200f021a3
--- /dev/null
+++ b/changelogs/unreleased/markdown-toolbar-spaces.yml
@@ -0,0 +1,5 @@
+---
+title: Remove extra spaces from markdown toolbar items
+merge_request: 31288
+author:
+type: other
diff --git a/changelogs/unreleased/mattermost-chat-responder.yml b/changelogs/unreleased/mattermost-chat-responder.yml
new file mode 100644
index 00000000000..16cc3503922
--- /dev/null
+++ b/changelogs/unreleased/mattermost-chat-responder.yml
@@ -0,0 +1,5 @@
+---
+title: Add responding to ChatOps jobs triggered in Mattermost
+merge_request: 29366
+author: Brian Kintz
+type: added
diff --git a/changelogs/unreleased/mc-feature-keep-latest-artifact-for-ref.yml b/changelogs/unreleased/mc-feature-keep-latest-artifact-for-ref.yml
new file mode 100644
index 00000000000..4e4bf350298
--- /dev/null
+++ b/changelogs/unreleased/mc-feature-keep-latest-artifact-for-ref.yml
@@ -0,0 +1,5 @@
+---
+title: Keep latest artifact for each ref.
+merge_request: 29802
+author:
+type: changed
diff --git a/changelogs/unreleased/merge-requests-spec.yml b/changelogs/unreleased/merge-requests-spec.yml
new file mode 100644
index 00000000000..7f2588cd910
--- /dev/null
+++ b/changelogs/unreleased/merge-requests-spec.yml
@@ -0,0 +1,5 @@
+---
+title: Replace let! with let_it_be in merge request spec
+merge_request: 31909
+author: Rajendra Kadam
+type: fixed
diff --git a/changelogs/unreleased/mg-dedupe-monaco-chunks.yml b/changelogs/unreleased/mg-dedupe-monaco-chunks.yml
new file mode 100644
index 00000000000..fb249df3f8c
--- /dev/null
+++ b/changelogs/unreleased/mg-dedupe-monaco-chunks.yml
@@ -0,0 +1,5 @@
+---
+title: Improve cacheability of monaco-editor code
+merge_request: 30032
+author:
+type: performance
diff --git a/changelogs/unreleased/move-browser-performance-testing-to-rules-syntax.yml b/changelogs/unreleased/move-browser-performance-testing-to-rules-syntax.yml
new file mode 100644
index 00000000000..4809d9f0de7
--- /dev/null
+++ b/changelogs/unreleased/move-browser-performance-testing-to-rules-syntax.yml
@@ -0,0 +1,5 @@
+---
+title: Move Browser-Perfomance-Testing.gitlab-ci.yml to `rules` syntax
+merge_request: 31413
+author:
+type: changed
diff --git a/changelogs/unreleased/move-build-template-to-rules-syntax.yml b/changelogs/unreleased/move-build-template-to-rules-syntax.yml
new file mode 100644
index 00000000000..a6681e70e23
--- /dev/null
+++ b/changelogs/unreleased/move-build-template-to-rules-syntax.yml
@@ -0,0 +1,5 @@
+---
+title: Move Build.gitlab-ci.yml to `rules` syntax
+merge_request: 30895
+author:
+type: changed
diff --git a/changelogs/unreleased/move-code-quality-template-to-rules-syntax.yml b/changelogs/unreleased/move-code-quality-template-to-rules-syntax.yml
new file mode 100644
index 00000000000..dbc7e3c7e8f
--- /dev/null
+++ b/changelogs/unreleased/move-code-quality-template-to-rules-syntax.yml
@@ -0,0 +1,5 @@
+---
+title: Move Code-Quality.gitlab-ci.yml to `rules` syntax
+merge_request: 30896
+author:
+type: changed
diff --git a/changelogs/unreleased/move-deploy-template-to-rules-syntax.yml b/changelogs/unreleased/move-deploy-template-to-rules-syntax.yml
new file mode 100644
index 00000000000..46ec2bcbcf5
--- /dev/null
+++ b/changelogs/unreleased/move-deploy-template-to-rules-syntax.yml
@@ -0,0 +1,5 @@
+---
+title: Move Deploy.gitlab-ci.yml to `rules` syntax
+merge_request: 31290
+author:
+type: changed
diff --git a/changelogs/unreleased/move_daily_users_statistics_cronjob_to_ce.yml b/changelogs/unreleased/move_daily_users_statistics_cronjob_to_ce.yml
new file mode 100644
index 00000000000..58c3130429d
--- /dev/null
+++ b/changelogs/unreleased/move_daily_users_statistics_cronjob_to_ce.yml
@@ -0,0 +1,5 @@
+---
+title: Move daily create users statistics cronjob to CE
+merge_request: 30843
+author:
+type: fixed
diff --git a/changelogs/unreleased/mv-to-service.yml b/changelogs/unreleased/mv-to-service.yml
new file mode 100644
index 00000000000..ff279122a17
--- /dev/null
+++ b/changelogs/unreleased/mv-to-service.yml
@@ -0,0 +1,5 @@
+---
+title: Move release notification from model callbacks to service
+merge_request: 29853
+author: Ravishankar
+type: performance
diff --git a/changelogs/unreleased/mw-ca-add-title.yml b/changelogs/unreleased/mw-ca-add-title.yml
new file mode 100644
index 00000000000..5282b17bc04
--- /dev/null
+++ b/changelogs/unreleased/mw-ca-add-title.yml
@@ -0,0 +1,5 @@
+---
+title: 'Contribution Analytics: Add title to page'
+merge_request: 30842
+author:
+type: added
diff --git a/changelogs/unreleased/mw-cicd-analytics-add-title.yml b/changelogs/unreleased/mw-cicd-analytics-add-title.yml
new file mode 100644
index 00000000000..e435452b1e4
--- /dev/null
+++ b/changelogs/unreleased/mw-cicd-analytics-add-title.yml
@@ -0,0 +1,5 @@
+---
+title: 'CI / CD Analytics: Add title to page'
+merge_request: 30891
+author:
+type: added
diff --git a/changelogs/unreleased/mw-cr-title-margin.yml b/changelogs/unreleased/mw-cr-title-margin.yml
new file mode 100644
index 00000000000..6edc6c1bf9b
--- /dev/null
+++ b/changelogs/unreleased/mw-cr-title-margin.yml
@@ -0,0 +1,5 @@
+---
+title: 'Code review analytics: Change margin between title and description'
+merge_request: 30834
+author:
+type: changed
diff --git a/changelogs/unreleased/mw-ia-add-title.yml b/changelogs/unreleased/mw-ia-add-title.yml
new file mode 100644
index 00000000000..cc337ca9b46
--- /dev/null
+++ b/changelogs/unreleased/mw-ia-add-title.yml
@@ -0,0 +1,5 @@
+---
+title: 'Issues Analytics: Add title to page'
+merge_request: 30836
+author:
+type: added
diff --git a/changelogs/unreleased/mw-ia-group-level-add-title.yml b/changelogs/unreleased/mw-ia-group-level-add-title.yml
new file mode 100644
index 00000000000..e24923d0350
--- /dev/null
+++ b/changelogs/unreleased/mw-ia-group-level-add-title.yml
@@ -0,0 +1,5 @@
+---
+title: 'Issues Analytics: Add title to group-level page'
+merge_request: 31057
+author:
+type: added
diff --git a/changelogs/unreleased/mw-insights-add-title.yml b/changelogs/unreleased/mw-insights-add-title.yml
new file mode 100644
index 00000000000..8832c5ff84f
--- /dev/null
+++ b/changelogs/unreleased/mw-insights-add-title.yml
@@ -0,0 +1,5 @@
+---
+title: 'Insights Analytics: Add title to page'
+merge_request: 30853
+author:
+type: added
diff --git a/changelogs/unreleased/mw-pa-title-cleanup.yml b/changelogs/unreleased/mw-pa-title-cleanup.yml
new file mode 100644
index 00000000000..a056b3b14e4
--- /dev/null
+++ b/changelogs/unreleased/mw-pa-title-cleanup.yml
@@ -0,0 +1,5 @@
+---
+title: 'Productivity Analytics: Remove separator and cleanup title margins'
+merge_request: 30839
+author:
+type: changed
diff --git a/changelogs/unreleased/mw-ra-add-title.yml b/changelogs/unreleased/mw-ra-add-title.yml
new file mode 100644
index 00000000000..129ef57a6b1
--- /dev/null
+++ b/changelogs/unreleased/mw-ra-add-title.yml
@@ -0,0 +1,5 @@
+---
+title: 'Repository Analytics: Add title to page'
+merge_request: 30855
+author:
+type: added
diff --git a/changelogs/unreleased/mw-vsa-title-cleanup.yml b/changelogs/unreleased/mw-vsa-title-cleanup.yml
new file mode 100644
index 00000000000..1a1b115f558
--- /dev/null
+++ b/changelogs/unreleased/mw-vsa-title-cleanup.yml
@@ -0,0 +1,5 @@
+---
+title: 'Value Stream Analytics: Add title and remove separator'
+merge_request: 30841
+author:
+type: other
diff --git a/changelogs/unreleased/mwam-214582-add-starred-dashboard-to-metrics-dashboard-endpoint.yml b/changelogs/unreleased/mwam-214582-add-starred-dashboard-to-metrics-dashboard-endpoint.yml
new file mode 100644
index 00000000000..db9635aaac7
--- /dev/null
+++ b/changelogs/unreleased/mwam-214582-add-starred-dashboard-to-metrics-dashboard-endpoint.yml
@@ -0,0 +1,5 @@
+---
+title: Display metrics dashboards starred by user at the top of dashboard select field.
+merge_request: 31059
+author:
+type: added
diff --git a/changelogs/unreleased/mwaw-214581-delete-star-metrics-dashboard-endpoint.yml b/changelogs/unreleased/mwaw-214581-delete-star-metrics-dashboard-endpoint.yml
new file mode 100644
index 00000000000..0f043cd535c
--- /dev/null
+++ b/changelogs/unreleased/mwaw-214581-delete-star-metrics-dashboard-endpoint.yml
@@ -0,0 +1,5 @@
+---
+title: New API endpoint for removing stars from metrics dashboards
+merge_request: 31892
+author:
+type: added
diff --git a/changelogs/unreleased/mwaw-214581-star-metrics-dashboard-endpoint.yml b/changelogs/unreleased/mwaw-214581-star-metrics-dashboard-endpoint.yml
new file mode 100644
index 00000000000..3b539cd3140
--- /dev/null
+++ b/changelogs/unreleased/mwaw-214581-star-metrics-dashboard-endpoint.yml
@@ -0,0 +1,5 @@
+---
+title: New API endpoint for starring metrics dashboards
+merge_request: 31316
+author:
+type: added
diff --git a/changelogs/unreleased/mwaw-215224-make-dashboard-annotations-generally-available.yml b/changelogs/unreleased/mwaw-215224-make-dashboard-annotations-generally-available.yml
new file mode 100644
index 00000000000..84f8dfb0798
--- /dev/null
+++ b/changelogs/unreleased/mwaw-215224-make-dashboard-annotations-generally-available.yml
@@ -0,0 +1,6 @@
+---
+title: Add metrics dashboard annotations feature, which enables marking interesting
+ events over metrics dashboard charts
+merge_request: 30371
+author:
+type: added
diff --git a/changelogs/unreleased/mwaw-216963-annotations-missing-from-metric-charts.yml b/changelogs/unreleased/mwaw-216963-annotations-missing-from-metric-charts.yml
new file mode 100644
index 00000000000..0aaf9fe1d9e
--- /dev/null
+++ b/changelogs/unreleased/mwaw-216963-annotations-missing-from-metric-charts.yml
@@ -0,0 +1,6 @@
+---
+title: Use iso 8601 timestamp format in metrics dashboard annotations graphql resource
+ to assure multi browser compatibility
+merge_request: 31474
+author:
+type: fixed
diff --git a/changelogs/unreleased/nicolasdular-hide-broadcast-notification-until-end-date.yml b/changelogs/unreleased/nicolasdular-hide-broadcast-notification-until-end-date.yml
new file mode 100644
index 00000000000..d4e888c43db
--- /dev/null
+++ b/changelogs/unreleased/nicolasdular-hide-broadcast-notification-until-end-date.yml
@@ -0,0 +1,5 @@
+---
+title: Hide broadcast messages until the end of the period
+merge_request: 30432
+author:
+type: changed
diff --git a/changelogs/unreleased/nicolasdular-improve-target-path-regex.yml b/changelogs/unreleased/nicolasdular-improve-target-path-regex.yml
new file mode 100644
index 00000000000..4b007c572d4
--- /dev/null
+++ b/changelogs/unreleased/nicolasdular-improve-target-path-regex.yml
@@ -0,0 +1,5 @@
+---
+title: Use stricter regex for broadcast target path
+merge_request: 30210
+author:
+type: changed
diff --git a/changelogs/unreleased/nicolasdular-remove-email-restrictions-ff.yml b/changelogs/unreleased/nicolasdular-remove-email-restrictions-ff.yml
new file mode 100644
index 00000000000..b338bf32853
--- /dev/null
+++ b/changelogs/unreleased/nicolasdular-remove-email-restrictions-ff.yml
@@ -0,0 +1,5 @@
+---
+title: Add option to restrict emails that match a configured regular expression
+merge_request: 30548
+author:
+type: added
diff --git a/changelogs/unreleased/ntepluhina-edit-note-fix.yml b/changelogs/unreleased/ntepluhina-edit-note-fix.yml
new file mode 100644
index 00000000000..024a1e191ec
--- /dev/null
+++ b/changelogs/unreleased/ntepluhina-edit-note-fix.yml
@@ -0,0 +1,5 @@
+---
+title: Allow only users with `adminNote` permission to edit the design note
+merge_request: 32035
+author:
+type: fixed
diff --git a/changelogs/unreleased/package-deploy-tokens-fe.yml b/changelogs/unreleased/package-deploy-tokens-fe.yml
new file mode 100644
index 00000000000..9d9557cd06f
--- /dev/null
+++ b/changelogs/unreleased/package-deploy-tokens-fe.yml
@@ -0,0 +1,5 @@
+---
+title: Add read and write package registry scopes to deploy tokens
+merge_request: 31267
+author:
+type: added
diff --git a/changelogs/unreleased/pages-1-18.yml b/changelogs/unreleased/pages-1-18.yml
new file mode 100644
index 00000000000..84b45bc8ad4
--- /dev/null
+++ b/changelogs/unreleased/pages-1-18.yml
@@ -0,0 +1,5 @@
+---
+title: Update GitLab Pages to 1.18.0
+merge_request:
+author:
+type: added
diff --git a/changelogs/unreleased/pedroms-threads-counter-wording.yml b/changelogs/unreleased/pedroms-threads-counter-wording.yml
new file mode 100644
index 00000000000..6098a75609f
--- /dev/null
+++ b/changelogs/unreleased/pedroms-threads-counter-wording.yml
@@ -0,0 +1,5 @@
+---
+title: Change wording of merge request threads counter
+merge_request: 30217
+author:
+type: changed
diff --git a/changelogs/unreleased/ph-207121-shaMismatchCanMerge.yml b/changelogs/unreleased/ph-207121-shaMismatchCanMerge.yml
new file mode 100644
index 00000000000..2d96f14642d
--- /dev/null
+++ b/changelogs/unreleased/ph-207121-shaMismatchCanMerge.yml
@@ -0,0 +1,5 @@
+---
+title: Fixed enabled merge button incorrectly showing to users who can't merge
+merge_request:
+author:
+type: fixed
diff --git a/changelogs/unreleased/ph-212669-commitTypeMarkdownHTML.yml b/changelogs/unreleased/ph-212669-commitTypeMarkdownHTML.yml
new file mode 100644
index 00000000000..e60bbc0a3e6
--- /dev/null
+++ b/changelogs/unreleased/ph-212669-commitTypeMarkdownHTML.yml
@@ -0,0 +1,5 @@
+---
+title: Fixes commit message emojis not rendering in Vue file list
+merge_request:
+author:
+type: fixed
diff --git a/changelogs/unreleased/ph-213189-codeNavSameFile.yml b/changelogs/unreleased/ph-213189-codeNavSameFile.yml
new file mode 100644
index 00000000000..b0f7a7f20a4
--- /dev/null
+++ b/changelogs/unreleased/ph-213189-codeNavSameFile.yml
@@ -0,0 +1,5 @@
+---
+title: Fix jump to definition linking to same file opening a new tab
+merge_request:
+author:
+type: changed
diff --git a/changelogs/unreleased/ph-214480-copyUploadLinks.yml b/changelogs/unreleased/ph-214480-copyUploadLinks.yml
new file mode 100644
index 00000000000..cd8313f58de
--- /dev/null
+++ b/changelogs/unreleased/ph-214480-copyUploadLinks.yml
@@ -0,0 +1,5 @@
+---
+title: Fixed copy as GFM not copying upload links
+merge_request: 29683
+author:
+type: fixed
diff --git a/changelogs/unreleased/ph-215917-escapeRef.yml b/changelogs/unreleased/ph-215917-escapeRef.yml
new file mode 100644
index 00000000000..d7c62250aa2
--- /dev/null
+++ b/changelogs/unreleased/ph-215917-escapeRef.yml
@@ -0,0 +1,5 @@
+---
+title: Fixes branch name not getting escaped correctly on frontend
+merge_request:
+author:
+type: fixed
diff --git a/changelogs/unreleased/ph-33423-fixCommitReplyForm.yml b/changelogs/unreleased/ph-33423-fixCommitReplyForm.yml
new file mode 100644
index 00000000000..d30ecca1159
--- /dev/null
+++ b/changelogs/unreleased/ph-33423-fixCommitReplyForm.yml
@@ -0,0 +1,5 @@
+---
+title: Fixed misaligned avatar in commit discussion form
+merge_request:
+author:
+type: fixed
diff --git a/changelogs/unreleased/ph-33586-confirmCancelReply.yml b/changelogs/unreleased/ph-33586-confirmCancelReply.yml
new file mode 100644
index 00000000000..ce0ffc38148
--- /dev/null
+++ b/changelogs/unreleased/ph-33586-confirmCancelReply.yml
@@ -0,0 +1,5 @@
+---
+title: Fixed cancel reply button not alerting the user
+merge_request:
+author:
+type: fixed
diff --git a/changelogs/unreleased/pipelines-spec.yml b/changelogs/unreleased/pipelines-spec.yml
new file mode 100644
index 00000000000..e663052d8b4
--- /dev/null
+++ b/changelogs/unreleased/pipelines-spec.yml
@@ -0,0 +1,5 @@
+---
+title: angelog Replace let! with let_it_be in pipelines spec
+merge_request: 31916
+author: Rajendra Kadam
+type: fixed
diff --git a/changelogs/unreleased/psi-dropdown-item-check.yml b/changelogs/unreleased/psi-dropdown-item-check.yml
new file mode 100644
index 00000000000..a39217d466d
--- /dev/null
+++ b/changelogs/unreleased/psi-dropdown-item-check.yml
@@ -0,0 +1,5 @@
+---
+title: Fix checkmark position on dropdowns
+merge_request: 30685
+author:
+type: fixed
diff --git a/changelogs/unreleased/rc-add_metrics_dashboard_policy.yml b/changelogs/unreleased/rc-add_metrics_dashboard_policy.yml
new file mode 100644
index 00000000000..85459ef705c
--- /dev/null
+++ b/changelogs/unreleased/rc-add_metrics_dashboard_policy.yml
@@ -0,0 +1,5 @@
+---
+title: Add ability to change metrics dashboard visibility
+merge_request: 29634
+author:
+type: added
diff --git a/changelogs/unreleased/rc-remove_influx_views_docs.yml b/changelogs/unreleased/rc-remove_influx_views_docs.yml
new file mode 100644
index 00000000000..233ea92ad5d
--- /dev/null
+++ b/changelogs/unreleased/rc-remove_influx_views_docs.yml
@@ -0,0 +1,5 @@
+---
+title: Remove deprecated InfluxDB
+merge_request: 30786
+author:
+type: removed
diff --git a/changelogs/unreleased/rc-remove_influxdb_supporting_libs.yml b/changelogs/unreleased/rc-remove_influxdb_supporting_libs.yml
new file mode 100644
index 00000000000..f866e80989d
--- /dev/null
+++ b/changelogs/unreleased/rc-remove_influxdb_supporting_libs.yml
@@ -0,0 +1,5 @@
+---
+title: Remove rake task `gitlab:track_deployment`
+merge_request: 31404
+author:
+type: removed
diff --git a/changelogs/unreleased/re-enable_boards_negative_filters.yml b/changelogs/unreleased/re-enable_boards_negative_filters.yml
new file mode 100644
index 00000000000..3a916ea054f
--- /dev/null
+++ b/changelogs/unreleased/re-enable_boards_negative_filters.yml
@@ -0,0 +1,5 @@
+---
+title: Re-enable negative filters for Boards
+merge_request: 32348
+author:
+type: fixed
diff --git a/changelogs/unreleased/recreate_foreign_key_with_nullify.yml b/changelogs/unreleased/recreate_foreign_key_with_nullify.yml
new file mode 100644
index 00000000000..92acfae21bf
--- /dev/null
+++ b/changelogs/unreleased/recreate_foreign_key_with_nullify.yml
@@ -0,0 +1,5 @@
+---
+title: Recreate foreign key in project settings to use nullify instead of cascade
+merge_request: 29767
+author:
+type: changed
diff --git a/changelogs/unreleased/refactor-ee-app-services.yml b/changelogs/unreleased/refactor-ee-app-services.yml
new file mode 100644
index 00000000000..ea8dc859b21
--- /dev/null
+++ b/changelogs/unreleased/refactor-ee-app-services.yml
@@ -0,0 +1,5 @@
+---
+title: Move prepend to last line in ee/services
+merge_request: 30425
+author: Rajendra Kadam
+type: fixed
diff --git a/changelogs/unreleased/remove-create_confidential_merge_request.yml b/changelogs/unreleased/remove-create_confidential_merge_request.yml
new file mode 100644
index 00000000000..49d6b201362
--- /dev/null
+++ b/changelogs/unreleased/remove-create_confidential_merge_request.yml
@@ -0,0 +1,5 @@
+---
+title: Removes create_confidential_merge_request feature flag leandrogs
+merge_request: 31968
+author: Leandro Silva
+type: other
diff --git a/changelogs/unreleased/remove-deprecated-container-scanning-report-format.yml b/changelogs/unreleased/remove-deprecated-container-scanning-report-format.yml
new file mode 100644
index 00000000000..a4e754fee1d
--- /dev/null
+++ b/changelogs/unreleased/remove-deprecated-container-scanning-report-format.yml
@@ -0,0 +1,5 @@
+---
+title: Remove deprecated container scanning report parser
+merge_request: 31294
+author:
+type: removed
diff --git a/changelogs/unreleased/remove-exceptions-from-junit-parser.yml b/changelogs/unreleased/remove-exceptions-from-junit-parser.yml
new file mode 100644
index 00000000000..d58fa346f37
--- /dev/null
+++ b/changelogs/unreleased/remove-exceptions-from-junit-parser.yml
@@ -0,0 +1,5 @@
+---
+title: Render TestReport parsing errors back to pipeline test summary
+merge_request: 24188
+author:
+type: fixed
diff --git a/changelogs/unreleased/remove-experimental-indexer-column.yml b/changelogs/unreleased/remove-experimental-indexer-column.yml
new file mode 100644
index 00000000000..7e8f389cc3c
--- /dev/null
+++ b/changelogs/unreleased/remove-experimental-indexer-column.yml
@@ -0,0 +1,5 @@
+---
+title: Remove elasticsearch_experimental_indexer column
+merge_request: 30628
+author:
+type: other
diff --git a/changelogs/unreleased/remove-obsolete-resource-milestone-events-columns.yml b/changelogs/unreleased/remove-obsolete-resource-milestone-events-columns.yml
new file mode 100644
index 00000000000..bc1c9e2fb59
--- /dev/null
+++ b/changelogs/unreleased/remove-obsolete-resource-milestone-events-columns.yml
@@ -0,0 +1,5 @@
+---
+title: Remove obsolete columns from resource_milestone_events
+merge_request: 28536
+author:
+type: other
diff --git a/changelogs/unreleased/remove-sidekiq-rake-tasks.yml b/changelogs/unreleased/remove-sidekiq-rake-tasks.yml
new file mode 100644
index 00000000000..f2677b18146
--- /dev/null
+++ b/changelogs/unreleased/remove-sidekiq-rake-tasks.yml
@@ -0,0 +1,5 @@
+---
+title: Remove deprecated Sidekiq rake tasks
+merge_request:
+author:
+type: removed
diff --git a/changelogs/unreleased/remove_admin_settings_geo_navigation.yml b/changelogs/unreleased/remove_admin_settings_geo_navigation.yml
new file mode 100644
index 00000000000..4635c8a7ca0
--- /dev/null
+++ b/changelogs/unreleased/remove_admin_settings_geo_navigation.yml
@@ -0,0 +1,5 @@
+---
+title: Remove Admin -> Settings -> Geo navigation
+merge_request: 21005
+author: Lee Tickett
+type: other
diff --git a/changelogs/unreleased/remove_ci_expose_arbitrary_artifacts_in_mr_feature_flag.yml b/changelogs/unreleased/remove_ci_expose_arbitrary_artifacts_in_mr_feature_flag.yml
new file mode 100644
index 00000000000..f3fe6b82506
--- /dev/null
+++ b/changelogs/unreleased/remove_ci_expose_arbitrary_artifacts_in_mr_feature_flag.yml
@@ -0,0 +1,5 @@
+---
+title: Remove ci_expose_arbitrary_artifacts_in_mr feature flag
+merge_request: 29363
+author: Lee Tickett
+type: other
diff --git a/changelogs/unreleased/remove_git_archive_path_feature_flag.yml b/changelogs/unreleased/remove_git_archive_path_feature_flag.yml
new file mode 100644
index 00000000000..68e7ffd25a1
--- /dev/null
+++ b/changelogs/unreleased/remove_git_archive_path_feature_flag.yml
@@ -0,0 +1,5 @@
+---
+title: Remove git_archive_path feature flag
+merge_request: 29369
+author: Lee Tickett
+type: other
diff --git a/changelogs/unreleased/remove_kwargs_from_storage_move_worker.yml b/changelogs/unreleased/remove_kwargs_from_storage_move_worker.yml
new file mode 100644
index 00000000000..96c98842847
--- /dev/null
+++ b/changelogs/unreleased/remove_kwargs_from_storage_move_worker.yml
@@ -0,0 +1,5 @@
+---
+title: Remove kwargs from storage move worker
+merge_request: 31412
+author:
+type: fixed
diff --git a/changelogs/unreleased/revert-0ddee28a.yml b/changelogs/unreleased/revert-0ddee28a.yml
new file mode 100644
index 00000000000..909a8673a08
--- /dev/null
+++ b/changelogs/unreleased/revert-0ddee28a.yml
@@ -0,0 +1,5 @@
+---
+title: Move Auto DevOps Test.gitlab-ci.yml template to rules syntax instead of only/except
+merge_request: 30876
+author:
+type: changed
diff --git a/changelogs/unreleased/rs-enable-keep-divergent-refs.yml b/changelogs/unreleased/rs-enable-keep-divergent-refs.yml
new file mode 100644
index 00000000000..b7611c9dec9
--- /dev/null
+++ b/changelogs/unreleased/rs-enable-keep-divergent-refs.yml
@@ -0,0 +1,5 @@
+---
+title: Add "Keep divergent refs" option for push mirrors
+merge_request: 32381
+author:
+type: added
diff --git a/changelogs/unreleased/rubocop-flag-kwargs-in-sidekiq-workers-cop.yml b/changelogs/unreleased/rubocop-flag-kwargs-in-sidekiq-workers-cop.yml
new file mode 100644
index 00000000000..88797f4fb73
--- /dev/null
+++ b/changelogs/unreleased/rubocop-flag-kwargs-in-sidekiq-workers-cop.yml
@@ -0,0 +1,5 @@
+---
+title: Add Rubocop cop to flag keyword arguments usage in Sidekiq workers
+merge_request: 31551
+author: Arun Kumar Mohan
+type: added
diff --git a/changelogs/unreleased/rw-exclude-skipped-tests-from-success.yml b/changelogs/unreleased/rw-exclude-skipped-tests-from-success.yml
new file mode 100644
index 00000000000..c5d348f104e
--- /dev/null
+++ b/changelogs/unreleased/rw-exclude-skipped-tests-from-success.yml
@@ -0,0 +1,4 @@
+---
+title: Changed test success calculation to exclude skipped tests
+merge_request: 31154
+type: changed
diff --git a/changelogs/unreleased/rz_fix_api_statistics_permission.yml b/changelogs/unreleased/rz_fix_api_statistics_permission.yml
new file mode 100644
index 00000000000..4bd7a60271c
--- /dev/null
+++ b/changelogs/unreleased/rz_fix_api_statistics_permission.yml
@@ -0,0 +1,5 @@
+---
+title: Correct the permission according to docs
+merge_request: 28657
+author:
+type: fixed
diff --git a/changelogs/unreleased/set-multiple-custom-metrics-edit-path.yml b/changelogs/unreleased/set-multiple-custom-metrics-edit-path.yml
new file mode 100644
index 00000000000..ca911ef6999
--- /dev/null
+++ b/changelogs/unreleased/set-multiple-custom-metrics-edit-path.yml
@@ -0,0 +1,5 @@
+---
+title: Multiple metrics edit navigates to prom edit page
+merge_request: 30666
+author:
+type: added
diff --git a/changelogs/unreleased/sh-clean-up-public-visiblity-level-check.yml b/changelogs/unreleased/sh-clean-up-public-visiblity-level-check.yml
new file mode 100644
index 00000000000..88f737a49a1
--- /dev/null
+++ b/changelogs/unreleased/sh-clean-up-public-visiblity-level-check.yml
@@ -0,0 +1,5 @@
+---
+title: Fix second 500 error with NULL restricted visibility levels
+merge_request: 30414
+author:
+type: fixed
diff --git a/changelogs/unreleased/sh-cleanup-mwps-refresh.yml b/changelogs/unreleased/sh-cleanup-mwps-refresh.yml
new file mode 100644
index 00000000000..8c2489a6d7d
--- /dev/null
+++ b/changelogs/unreleased/sh-cleanup-mwps-refresh.yml
@@ -0,0 +1,5 @@
+---
+title: Clean up refresh fix for cancel automatic merge
+merge_request: 29844
+author:
+type: other
diff --git a/changelogs/unreleased/sh-destroy-dropzone-hidden-input.yml b/changelogs/unreleased/sh-destroy-dropzone-hidden-input.yml
new file mode 100644
index 00000000000..991e29a7f55
--- /dev/null
+++ b/changelogs/unreleased/sh-destroy-dropzone-hidden-input.yml
@@ -0,0 +1,5 @@
+---
+title: Destroy Dropzone hidden input when form is destroyed
+merge_request: 29882
+author:
+type: fixed
diff --git a/changelogs/unreleased/sh-disable-schema-dump-prod.yml b/changelogs/unreleased/sh-disable-schema-dump-prod.yml
new file mode 100644
index 00000000000..145b1c3d4c6
--- /dev/null
+++ b/changelogs/unreleased/sh-disable-schema-dump-prod.yml
@@ -0,0 +1,5 @@
+---
+title: Disable schema dumping after migrations in production
+merge_request: 30812
+author:
+type: fixed
diff --git a/changelogs/unreleased/sh-enable-ref-caching-diffs-controller.yml b/changelogs/unreleased/sh-enable-ref-caching-diffs-controller.yml
new file mode 100644
index 00000000000..e4c654eeaf6
--- /dev/null
+++ b/changelogs/unreleased/sh-enable-ref-caching-diffs-controller.yml
@@ -0,0 +1,5 @@
+---
+title: Enable ref name caching for merge request diffs
+merge_request: 31530
+author:
+type: performance
diff --git a/changelogs/unreleased/sh-fix-create-projects-with-prometheus-service.yml b/changelogs/unreleased/sh-fix-create-projects-with-prometheus-service.yml
new file mode 100644
index 00000000000..2105d10e964
--- /dev/null
+++ b/changelogs/unreleased/sh-fix-create-projects-with-prometheus-service.yml
@@ -0,0 +1,5 @@
+---
+title: Fix errors creating project with active Prometheus service template
+merge_request: 30340
+author:
+type: fixed
diff --git a/changelogs/unreleased/sh-fix-lfs-badge-feature-flag.yml b/changelogs/unreleased/sh-fix-lfs-badge-feature-flag.yml
new file mode 100644
index 00000000000..37bd675434f
--- /dev/null
+++ b/changelogs/unreleased/sh-fix-lfs-badge-feature-flag.yml
@@ -0,0 +1,5 @@
+---
+title: Add LFS badge feature flag to RefsController#logs_tree
+merge_request: 30442
+author:
+type: fixed
diff --git a/changelogs/unreleased/sh-fix-merge-request-stickiness.yml b/changelogs/unreleased/sh-fix-merge-request-stickiness.yml
new file mode 100644
index 00000000000..796648f7ede
--- /dev/null
+++ b/changelogs/unreleased/sh-fix-merge-request-stickiness.yml
@@ -0,0 +1,5 @@
+---
+title: Clear merge request error on push to source branch
+merge_request: 32001
+author:
+type: fixed
diff --git a/changelogs/unreleased/sh-fix-orphaned-invited-members.yml b/changelogs/unreleased/sh-fix-orphaned-invited-members.yml
new file mode 100644
index 00000000000..a997a3bb603
--- /dev/null
+++ b/changelogs/unreleased/sh-fix-orphaned-invited-members.yml
@@ -0,0 +1,5 @@
+---
+title: Gracefully handle orphaned member invites
+merge_request: 30355
+author:
+type: fixed
diff --git a/changelogs/unreleased/sh-fix-overwrite-import-export-check.yml b/changelogs/unreleased/sh-fix-overwrite-import-export-check.yml
new file mode 100644
index 00000000000..6a897c1cca5
--- /dev/null
+++ b/changelogs/unreleased/sh-fix-overwrite-import-export-check.yml
@@ -0,0 +1,5 @@
+---
+title: Fix overwrite check in GitLab import/export
+merge_request: 31439
+author:
+type: fixed
diff --git a/changelogs/unreleased/sh-fix-paste-markdown-from-diff.yml b/changelogs/unreleased/sh-fix-paste-markdown-from-diff.yml
new file mode 100644
index 00000000000..d9bea5c89e5
--- /dev/null
+++ b/changelogs/unreleased/sh-fix-paste-markdown-from-diff.yml
@@ -0,0 +1,5 @@
+---
+title: Avoid copying diffs as Markdown tables
+merge_request: 30572
+author:
+type: fixed
diff --git a/changelogs/unreleased/sh-fix-render-rst.yml b/changelogs/unreleased/sh-fix-render-rst.yml
new file mode 100644
index 00000000000..31b60693d06
--- /dev/null
+++ b/changelogs/unreleased/sh-fix-render-rst.yml
@@ -0,0 +1,5 @@
+---
+title: Fix RST rendering hanging on large files
+merge_request: 31287
+author:
+type: fixed
diff --git a/changelogs/unreleased/sh-fix-squash-error-handling.yml b/changelogs/unreleased/sh-fix-squash-error-handling.yml
new file mode 100644
index 00000000000..d011d424a27
--- /dev/null
+++ b/changelogs/unreleased/sh-fix-squash-error-handling.yml
@@ -0,0 +1,5 @@
+---
+title: Improve error handling of squash and rebase
+merge_request: 23740
+author:
+type: other
diff --git a/changelogs/unreleased/sh-fix-user-logging-for-jwt-controller.yml b/changelogs/unreleased/sh-fix-user-logging-for-jwt-controller.yml
new file mode 100644
index 00000000000..66cdbdc7a36
--- /dev/null
+++ b/changelogs/unreleased/sh-fix-user-logging-for-jwt-controller.yml
@@ -0,0 +1,5 @@
+---
+title: Fix logging of username in /jwt/auth
+merge_request:
+author:
+type: fixed
diff --git a/changelogs/unreleased/sh-handle-deployments-no-pods.yml b/changelogs/unreleased/sh-handle-deployments-no-pods.yml
new file mode 100644
index 00000000000..72ffb22518a
--- /dev/null
+++ b/changelogs/unreleased/sh-handle-deployments-no-pods.yml
@@ -0,0 +1,5 @@
+---
+title: Fix 500 error loading environments index
+merge_request: 31184
+author:
+type: fixed
diff --git a/changelogs/unreleased/sh-handle-invalid-gitattributes.yml b/changelogs/unreleased/sh-handle-invalid-gitattributes.yml
new file mode 100644
index 00000000000..56378d18297
--- /dev/null
+++ b/changelogs/unreleased/sh-handle-invalid-gitattributes.yml
@@ -0,0 +1,5 @@
+---
+title: Ignore .gitattributes if they contain invalid byte sequences
+merge_request: 30922
+author:
+type: fixed
diff --git a/changelogs/unreleased/sh-log-api-errors.yml b/changelogs/unreleased/sh-log-api-errors.yml
new file mode 100644
index 00000000000..eeeca6c4de6
--- /dev/null
+++ b/changelogs/unreleased/sh-log-api-errors.yml
@@ -0,0 +1,5 @@
+---
+title: Log server responses of API bad requests in api_json.log
+merge_request: 29839
+author:
+type: other
diff --git a/changelogs/unreleased/sh-log-cloudflare-headers.yml b/changelogs/unreleased/sh-log-cloudflare-headers.yml
new file mode 100644
index 00000000000..0186e30585f
--- /dev/null
+++ b/changelogs/unreleased/sh-log-cloudflare-headers.yml
@@ -0,0 +1,5 @@
+---
+title: Log Cloudflare request headers
+merge_request: 31532
+author:
+type: added
diff --git a/changelogs/unreleased/sh-log-container-registry-access-warnings.yml b/changelogs/unreleased/sh-log-container-registry-access-warnings.yml
new file mode 100644
index 00000000000..43f0abcb7de
--- /dev/null
+++ b/changelogs/unreleased/sh-log-container-registry-access-warnings.yml
@@ -0,0 +1,5 @@
+---
+title: Log when container registry permissions are denied
+merge_request: 31536
+author:
+type: other
diff --git a/changelogs/unreleased/sh-revert-codeowners-check.yml b/changelogs/unreleased/sh-revert-codeowners-check.yml
new file mode 100644
index 00000000000..04181d0f678
--- /dev/null
+++ b/changelogs/unreleased/sh-revert-codeowners-check.yml
@@ -0,0 +1,5 @@
+---
+title: Revert CODEOWNERS validation of Web requests in diff check
+merge_request: 31087
+author:
+type: fixed
diff --git a/changelogs/unreleased/sh-update-nokogiri-1-10-9.yml b/changelogs/unreleased/sh-update-nokogiri-1-10-9.yml
new file mode 100644
index 00000000000..127742872d2
--- /dev/null
+++ b/changelogs/unreleased/sh-update-nokogiri-1-10-9.yml
@@ -0,0 +1,5 @@
+---
+title: Upgrade Nokogiri to v1.10.9
+merge_request: 30435
+author:
+type: other
diff --git a/changelogs/unreleased/sh-update-plantutml-gem.yml b/changelogs/unreleased/sh-update-plantutml-gem.yml
new file mode 100644
index 00000000000..ee138a974f2
--- /dev/null
+++ b/changelogs/unreleased/sh-update-plantutml-gem.yml
@@ -0,0 +1,5 @@
+---
+title: Update asciidoctor-plantuml gem to v0.0.12
+merge_request: 32376
+author:
+type: other
diff --git a/changelogs/unreleased/sh-update-rails-6-0-2-2.yml b/changelogs/unreleased/sh-update-rails-6-0-2-2.yml
new file mode 100644
index 00000000000..0359c42c678
--- /dev/null
+++ b/changelogs/unreleased/sh-update-rails-6-0-2-2.yml
@@ -0,0 +1,5 @@
+---
+title: Update to Rails 6.0.2.2
+merge_request: 29743
+author:
+type: other
diff --git a/changelogs/unreleased/sh-upgrade-unicorn.yml b/changelogs/unreleased/sh-upgrade-unicorn.yml
new file mode 100644
index 00000000000..5cbcc2f2800
--- /dev/null
+++ b/changelogs/unreleased/sh-upgrade-unicorn.yml
@@ -0,0 +1,5 @@
+---
+title: Upgrade Unicorn to v5.5.1
+merge_request: 30541
+author:
+type: fixed
diff --git a/changelogs/unreleased/sh-use-gitlab-markdown-in-wiki.yml b/changelogs/unreleased/sh-use-gitlab-markdown-in-wiki.yml
new file mode 100644
index 00000000000..1d7a56c2874
--- /dev/null
+++ b/changelogs/unreleased/sh-use-gitlab-markdown-in-wiki.yml
@@ -0,0 +1,5 @@
+---
+title: Remove mention of github-markup in Wiki clone help
+merge_request: 30962
+author:
+type: other
diff --git a/changelogs/unreleased/shard_move_capture_all_errors.yml b/changelogs/unreleased/shard_move_capture_all_errors.yml
new file mode 100644
index 00000000000..5c455672266
--- /dev/null
+++ b/changelogs/unreleased/shard_move_capture_all_errors.yml
@@ -0,0 +1,5 @@
+---
+title: Capture all errors when updating repository storage
+merge_request: 30119
+author:
+type: fixed
diff --git a/changelogs/unreleased/show_unpersisted_services_not_active.yml b/changelogs/unreleased/show_unpersisted_services_not_active.yml
new file mode 100644
index 00000000000..55597110e65
--- /dev/null
+++ b/changelogs/unreleased/show_unpersisted_services_not_active.yml
@@ -0,0 +1,5 @@
+---
+title: Fix bug when services appear active even though they are not
+merge_request: 30160
+author:
+type: fixed
diff --git a/changelogs/unreleased/sk-216910-validate-package-type.yml b/changelogs/unreleased/sk-216910-validate-package-type.yml
new file mode 100644
index 00000000000..19c10ed16f9
--- /dev/null
+++ b/changelogs/unreleased/sk-216910-validate-package-type.yml
@@ -0,0 +1,5 @@
+---
+title: Validate package types in package metadatum models
+merge_request: 32091
+author: Sashi Kumar
+type: other
diff --git a/changelogs/unreleased/sk-217056-relocate-nuget-presenter.yml b/changelogs/unreleased/sk-217056-relocate-nuget-presenter.yml
new file mode 100644
index 00000000000..cb625f8a446
--- /dev/null
+++ b/changelogs/unreleased/sk-217056-relocate-nuget-presenter.yml
@@ -0,0 +1,5 @@
+---
+title: Relocate Nuget presenter helpers to presenters module
+merge_request: 31985
+author: Sashi Kumar
+type: other
diff --git a/changelogs/unreleased/sk-add-validation-on-params-for-podlogs-service.yml b/changelogs/unreleased/sk-add-validation-on-params-for-podlogs-service.yml
new file mode 100644
index 00000000000..daef625d7b9
--- /dev/null
+++ b/changelogs/unreleased/sk-add-validation-on-params-for-podlogs-service.yml
@@ -0,0 +1,5 @@
+---
+title: Return validation errors for invalid pod name or container name when viewing pod logs
+merge_request: 30165
+author: Sashi Kumar
+type: changed
diff --git a/changelogs/unreleased/sk-fix-500-on-snippet-graphql-mutations.yml b/changelogs/unreleased/sk-fix-500-on-snippet-graphql-mutations.yml
new file mode 100644
index 00000000000..f5ac55a72ec
--- /dev/null
+++ b/changelogs/unreleased/sk-fix-500-on-snippet-graphql-mutations.yml
@@ -0,0 +1,5 @@
+---
+title: Fix 500 error for non-existing snippet on graphql mutations
+merge_request: 30632
+author: Sashi Kumar
+type: fixed
diff --git a/changelogs/unreleased/sk-fix-tag-deletion-duplicate-events.yml b/changelogs/unreleased/sk-fix-tag-deletion-duplicate-events.yml
new file mode 100644
index 00000000000..8dc6dcb1317
--- /dev/null
+++ b/changelogs/unreleased/sk-fix-tag-deletion-duplicate-events.yml
@@ -0,0 +1,5 @@
+---
+title: Fix duplicated activity and events on deletion of tag
+merge_request: 28861
+author: Sashi Kumar
+type: fixed
diff --git a/changelogs/unreleased/sk-organize-package-models.yml b/changelogs/unreleased/sk-organize-package-models.yml
new file mode 100644
index 00000000000..d85acc0e9ce
--- /dev/null
+++ b/changelogs/unreleased/sk-organize-package-models.yml
@@ -0,0 +1,5 @@
+---
+title: Organize package models by package type
+merge_request: 31346
+author: Sashi Kumar
+type: other
diff --git a/changelogs/unreleased/sk-refactor-count-queries-in-environments-controller.yml b/changelogs/unreleased/sk-refactor-count-queries-in-environments-controller.yml
new file mode 100644
index 00000000000..7be17cb220a
--- /dev/null
+++ b/changelogs/unreleased/sk-refactor-count-queries-in-environments-controller.yml
@@ -0,0 +1,5 @@
+---
+title: Refactor count queries to single query on Projects::EnvironmentsController
+merge_request: 30073
+author: Sashi Kumar
+type: other
diff --git a/changelogs/unreleased/sluongngoc-decouple-partial-clone.yml b/changelogs/unreleased/sluongngoc-decouple-partial-clone.yml
new file mode 100644
index 00000000000..779d678738a
--- /dev/null
+++ b/changelogs/unreleased/sluongngoc-decouple-partial-clone.yml
@@ -0,0 +1,5 @@
+---
+title: Decouple partial clone config from max input size
+merge_request: 30354
+author: Son Luong Ngoc
+type: changed
diff --git a/changelogs/unreleased/sort-issues-by-label-prio.yml b/changelogs/unreleased/sort-issues-by-label-prio.yml
new file mode 100644
index 00000000000..447a549d805
--- /dev/null
+++ b/changelogs/unreleased/sort-issues-by-label-prio.yml
@@ -0,0 +1,5 @@
+---
+title: Add sorting issues by label priority to graphQL endpoint
+merge_request: 27936
+author:
+type: added
diff --git a/changelogs/unreleased/storage_move_api.yml b/changelogs/unreleased/storage_move_api.yml
new file mode 100644
index 00000000000..6147440bbc3
--- /dev/null
+++ b/changelogs/unreleased/storage_move_api.yml
@@ -0,0 +1,5 @@
+---
+title: Read only storage move API
+merge_request: 31285
+author:
+type: added
diff --git a/changelogs/unreleased/switch-thread-to-process-memory-cache.yml b/changelogs/unreleased/switch-thread-to-process-memory-cache.yml
new file mode 100644
index 00000000000..7cc12849d5f
--- /dev/null
+++ b/changelogs/unreleased/switch-thread-to-process-memory-cache.yml
@@ -0,0 +1,5 @@
+---
+title: Use process-wide cache for application settings and performance bar
+merge_request: 31135
+author:
+type: performance
diff --git a/changelogs/unreleased/sy-fix-transient-embed-again.yml b/changelogs/unreleased/sy-fix-transient-embed-again.yml
new file mode 100644
index 00000000000..70e00a75018
--- /dev/null
+++ b/changelogs/unreleased/sy-fix-transient-embed-again.yml
@@ -0,0 +1,5 @@
+---
+title: Embed metrics charts for both /metrics and /metrics_dashboard routes
+merge_request: 29838
+author:
+type: fixed
diff --git a/changelogs/unreleased/sy-publish-status.yml b/changelogs/unreleased/sy-publish-status.yml
new file mode 100644
index 00000000000..364010b3059
--- /dev/null
+++ b/changelogs/unreleased/sy-publish-status.yml
@@ -0,0 +1,5 @@
+---
+title: Add table for tracking issues published to status page
+merge_request: 29994
+author:
+type: added
diff --git a/changelogs/unreleased/sy-status-counts.yml b/changelogs/unreleased/sy-status-counts.yml
new file mode 100644
index 00000000000..a2a6c54df71
--- /dev/null
+++ b/changelogs/unreleased/sy-status-counts.yml
@@ -0,0 +1,5 @@
+---
+title: Add alert counts by status to GraphQL API
+merge_request: 31818
+author:
+type: changed
diff --git a/changelogs/unreleased/topics-optional.yml b/changelogs/unreleased/topics-optional.yml
new file mode 100644
index 00000000000..b590a59766b
--- /dev/null
+++ b/changelogs/unreleased/topics-optional.yml
@@ -0,0 +1,5 @@
+---
+title: Indicate topics are optional
+merge_request: 30264
+author: Ben Bodenmiller
+type: changed
diff --git a/changelogs/unreleased/tr-alert-detail-remaining-fields.yml b/changelogs/unreleased/tr-alert-detail-remaining-fields.yml
new file mode 100644
index 00000000000..e1b12ecd5eb
--- /dev/null
+++ b/changelogs/unreleased/tr-alert-detail-remaining-fields.yml
@@ -0,0 +1,5 @@
+---
+title: Add fields to Alert Details view
+merge_request: 32392
+author:
+type: added
diff --git a/changelogs/unreleased/tr-alert-management-feature-flag.yml b/changelogs/unreleased/tr-alert-management-feature-flag.yml
new file mode 100644
index 00000000000..6ae9b766d13
--- /dev/null
+++ b/changelogs/unreleased/tr-alert-management-feature-flag.yml
@@ -0,0 +1,5 @@
+---
+title: Move alert management behind a feature flag
+merge_request: 30133
+author:
+type: fixed
diff --git a/changelogs/unreleased/tr-fix-space-char.yml b/changelogs/unreleased/tr-fix-space-char.yml
new file mode 100644
index 00000000000..19f3a47c8a8
--- /dev/null
+++ b/changelogs/unreleased/tr-fix-space-char.yml
@@ -0,0 +1,5 @@
+---
+title: Fix missing space character in alert header
+merge_request: 32395
+author:
+type: fixed
diff --git a/changelogs/unreleased/tr-remove-alert-detail-feature-flag.yml b/changelogs/unreleased/tr-remove-alert-detail-feature-flag.yml
new file mode 100644
index 00000000000..11c221a32c1
--- /dev/null
+++ b/changelogs/unreleased/tr-remove-alert-detail-feature-flag.yml
@@ -0,0 +1,5 @@
+---
+title: Add Alert Detail view
+merge_request: 31877
+author:
+type: added
diff --git a/changelogs/unreleased/tr-remove-alert-mgmt-feature-flag.yml b/changelogs/unreleased/tr-remove-alert-mgmt-feature-flag.yml
new file mode 100644
index 00000000000..ba0d4bcfc34
--- /dev/null
+++ b/changelogs/unreleased/tr-remove-alert-mgmt-feature-flag.yml
@@ -0,0 +1,5 @@
+---
+title: Enable Alert Management functionality
+merge_request: 31171
+author:
+type: added
diff --git a/changelogs/unreleased/update-ado-image-to-0-14-0.yml b/changelogs/unreleased/update-ado-image-to-0-14-0.yml
new file mode 100644
index 00000000000..320625d7f0a
--- /dev/null
+++ b/changelogs/unreleased/update-ado-image-to-0-14-0.yml
@@ -0,0 +1,6 @@
+---
+title: Update auto-deploy-image to v0.14.0 with helm 2.16.6, --atomic deployments
+ and improved kubernetes 1.16 support
+merge_request: 31505
+author:
+type: changed
diff --git a/changelogs/unreleased/update-ado-image-to-0-15-0.yml b/changelogs/unreleased/update-ado-image-to-0-15-0.yml
new file mode 100644
index 00000000000..26223761525
--- /dev/null
+++ b/changelogs/unreleased/update-ado-image-to-0-15-0.yml
@@ -0,0 +1,6 @@
+---
+title: Update auto-deploy-image to v0.15.0, with an upgraded PostgreSQL chart used
+ by default for Auto DevOps deployments
+merge_request: 31799
+author:
+type: changed
diff --git a/changelogs/unreleased/update-css-loader.yml b/changelogs/unreleased/update-css-loader.yml
new file mode 100644
index 00000000000..f164c9f4e30
--- /dev/null
+++ b/changelogs/unreleased/update-css-loader.yml
@@ -0,0 +1,5 @@
+---
+title: Update css-loader ^1.0.0 -> ^2.1.1
+merge_request: 31743
+author: Pirate Praveen
+type: other
diff --git a/changelogs/unreleased/update-deprecated-slot-syntax-in---app-assets-javascripts-boards-componen.yml b/changelogs/unreleased/update-deprecated-slot-syntax-in---app-assets-javascripts-boards-componen.yml
new file mode 100644
index 00000000000..1088f6aafab
--- /dev/null
+++ b/changelogs/unreleased/update-deprecated-slot-syntax-in---app-assets-javascripts-boards-componen.yml
@@ -0,0 +1,5 @@
+---
+title: Update deprecated slot syntax in ./app/assets/javascripts/boards/components/board_form.vue
+merge_request: 32005
+author: Gilang Gumilar
+type: changed
diff --git a/changelogs/unreleased/update-deprecated-slot-syntax-in---app-assets-javascripts-confidential_me.yml b/changelogs/unreleased/update-deprecated-slot-syntax-in---app-assets-javascripts-confidential_me.yml
new file mode 100644
index 00000000000..da481f4e582
--- /dev/null
+++ b/changelogs/unreleased/update-deprecated-slot-syntax-in---app-assets-javascripts-confidential_me.yml
@@ -0,0 +1,5 @@
+---
+title: Update deprecated slot syntax in ./app/assets/javascripts/confidential_merge_request/components/dropdown.vue
+merge_request: 31999
+author: Gilang Gumilar
+type: changed
diff --git a/changelogs/unreleased/update-deprecated-slot-syntax-in---app-assets-javascripts-diffs-component.yml b/changelogs/unreleased/update-deprecated-slot-syntax-in---app-assets-javascripts-diffs-component.yml
new file mode 100644
index 00000000000..9a7e751d604
--- /dev/null
+++ b/changelogs/unreleased/update-deprecated-slot-syntax-in---app-assets-javascripts-diffs-component.yml
@@ -0,0 +1,5 @@
+---
+title: Update deprecated slot syntax in ./app/assets/javascripts/diffs/components/diff_discussions.vue
+merge_request: 32004
+author: Gilang Gumilar
+type: changed
diff --git a/changelogs/unreleased/update-deprecated-slot-syntax-in---app-assets-javascripts-pages-admin-use.yml b/changelogs/unreleased/update-deprecated-slot-syntax-in---app-assets-javascripts-pages-admin-use.yml
new file mode 100644
index 00000000000..ebf77eb5147
--- /dev/null
+++ b/changelogs/unreleased/update-deprecated-slot-syntax-in---app-assets-javascripts-pages-admin-use.yml
@@ -0,0 +1,5 @@
+---
+title: Update deprecated slot syntax in ./app/assets/javascripts/pages/admin/users/components/delete_user_modal.vue
+merge_request: 31992
+author: Gilang Gumilar
+type: other
diff --git a/changelogs/unreleased/update-deprecated-slot-syntax-in---app-assets-javascripts-pages-milestone.yml b/changelogs/unreleased/update-deprecated-slot-syntax-in---app-assets-javascripts-pages-milestone.yml
new file mode 100644
index 00000000000..15ea555a8fe
--- /dev/null
+++ b/changelogs/unreleased/update-deprecated-slot-syntax-in---app-assets-javascripts-pages-milestone.yml
@@ -0,0 +1,5 @@
+---
+title: Update deprecated slot syntax in ./app/assets/javascripts/pages/milestones/shared/components/delete_milestone_modal.vue
+merge_request: 31990
+author: Gilang Gumilar
+type: changed
diff --git a/changelogs/unreleased/update-deprecated-slot-syntax-in---app-assets-javascripts-performance_bar.yml b/changelogs/unreleased/update-deprecated-slot-syntax-in---app-assets-javascripts-performance_bar.yml
new file mode 100644
index 00000000000..e23a49c5dc5
--- /dev/null
+++ b/changelogs/unreleased/update-deprecated-slot-syntax-in---app-assets-javascripts-performance_bar.yml
@@ -0,0 +1,5 @@
+---
+title: Update deprecated slot syntax in ./app/assets/javascripts/performance_bar/components/detailed_metric.vue
+merge_request: 32006
+author: Gilang Gumilar
+type: other
diff --git a/changelogs/unreleased/update-deprecated-slot-syntax-in---app-assets-javascripts-profile-account.yml b/changelogs/unreleased/update-deprecated-slot-syntax-in---app-assets-javascripts-profile-account.yml
new file mode 100644
index 00000000000..d80afbffc79
--- /dev/null
+++ b/changelogs/unreleased/update-deprecated-slot-syntax-in---app-assets-javascripts-profile-account.yml
@@ -0,0 +1,5 @@
+---
+title: Update deprecated slot syntax in ./app/assets/javascripts/profile/account/components/delete_account_modal.vue
+merge_request: 32007
+author: Gilang Gumilar
+type: other
diff --git a/changelogs/unreleased/update-deprecated-slot-syntax-in---app-assets-javascripts-releases-compon.yml b/changelogs/unreleased/update-deprecated-slot-syntax-in---app-assets-javascripts-releases-compon.yml
new file mode 100644
index 00000000000..b30c5c684ea
--- /dev/null
+++ b/changelogs/unreleased/update-deprecated-slot-syntax-in---app-assets-javascripts-releases-compon.yml
@@ -0,0 +1,5 @@
+---
+title: Update deprecated slot syntax in ./app/assets/javascripts/releases/components/app_edit.vue
+merge_request: 32018
+author: Gilang Gumilar
+type: other
diff --git a/changelogs/unreleased/update-deprecated-slot-syntax-in---app-assets-javascripts-repository-comp.yml b/changelogs/unreleased/update-deprecated-slot-syntax-in---app-assets-javascripts-repository-comp.yml
new file mode 100644
index 00000000000..7ca74c01be4
--- /dev/null
+++ b/changelogs/unreleased/update-deprecated-slot-syntax-in---app-assets-javascripts-repository-comp.yml
@@ -0,0 +1,5 @@
+---
+title: Update deprecated slot syntax in ./app/assets/javascripts/repository/components/breadcrumbs.vue
+merge_request: 32017
+author: Gilang Gumilar
+type: changed
diff --git a/changelogs/unreleased/update-deprecated-slot-syntax-in---app-assets-javascripts-serverless-comp.yml b/changelogs/unreleased/update-deprecated-slot-syntax-in---app-assets-javascripts-serverless-comp.yml
new file mode 100644
index 00000000000..3c87741f0de
--- /dev/null
+++ b/changelogs/unreleased/update-deprecated-slot-syntax-in---app-assets-javascripts-serverless-comp.yml
@@ -0,0 +1,5 @@
+---
+title: Update deprecated slot syntax in ./app/assets/javascripts/serverless/components/area.vue
+merge_request: 32015
+author: Gilang Gumilar
+type: other
diff --git a/changelogs/unreleased/update-deprecated-slot-syntax-in---assets-javascripts-pages-milestone-pro.yml b/changelogs/unreleased/update-deprecated-slot-syntax-in---assets-javascripts-pages-milestone-pro.yml
new file mode 100644
index 00000000000..1bd33720c47
--- /dev/null
+++ b/changelogs/unreleased/update-deprecated-slot-syntax-in---assets-javascripts-pages-milestone-pro.yml
@@ -0,0 +1,5 @@
+---
+title: Update deprecated slot syntax in ./app/assets/javascripts/pages/milestones/shared/components/promote_milestone_modal.vue
+merge_request: 31980
+author: Gilang Gumilar
+type: other
diff --git a/changelogs/unreleased/update-deprecated-slot-syntax-in---environments-delete_environment_modal-vue.yml b/changelogs/unreleased/update-deprecated-slot-syntax-in---environments-delete_environment_modal-vue.yml
new file mode 100644
index 00000000000..317da0d88f5
--- /dev/null
+++ b/changelogs/unreleased/update-deprecated-slot-syntax-in---environments-delete_environment_modal-vue.yml
@@ -0,0 +1,5 @@
+---
+title: Update deprecated slot syntax in ./app/assets/javascripts/environments/components/stop_environment_modal.vue
+merge_request: 32012
+author: Gilang Gumilar
+type: other
diff --git a/changelogs/unreleased/update-deprecated-slot-syntax-in---javascripts-ide-ide_tree-vue.yml b/changelogs/unreleased/update-deprecated-slot-syntax-in---javascripts-ide-ide_tree-vue.yml
new file mode 100644
index 00000000000..7bac2a1c6b3
--- /dev/null
+++ b/changelogs/unreleased/update-deprecated-slot-syntax-in---javascripts-ide-ide_tree-vue.yml
@@ -0,0 +1,5 @@
+---
+title: Update deprecated slot syntax in ./app/assets/javascripts/ide/components/ide_review.vue
+merge_request: 32025
+author: Gilang Gumilar
+type: other
diff --git a/changelogs/unreleased/update-deprecated-slot-syntax-in---javascripts-ide-pipelines-list-vue.yml b/changelogs/unreleased/update-deprecated-slot-syntax-in---javascripts-ide-pipelines-list-vue.yml
new file mode 100644
index 00000000000..315f0a0fc50
--- /dev/null
+++ b/changelogs/unreleased/update-deprecated-slot-syntax-in---javascripts-ide-pipelines-list-vue.yml
@@ -0,0 +1,5 @@
+---
+title: Update deprecated slot syntax in ./app/assets/javascripts/ide/components/pipelines/list.vue
+merge_request: 32027
+author: Gilang Gumilar
+type: other
diff --git a/changelogs/unreleased/update-deprecated-slot-syntax-in---releases-evidence_block-vue.yml b/changelogs/unreleased/update-deprecated-slot-syntax-in---releases-evidence_block-vue.yml
new file mode 100644
index 00000000000..b2800dbeb31
--- /dev/null
+++ b/changelogs/unreleased/update-deprecated-slot-syntax-in---releases-evidence_block-vue.yml
@@ -0,0 +1,5 @@
+---
+title: Update deprecated slot syntax in ./app/assets/javascripts/releases/components/evidence_block.vue
+merge_request: 32019
+author: Gilang Gumilar
+type: other
diff --git a/changelogs/unreleased/update-deprecated-slot-syntax-in-app-assets-javascripts-issue_show-compon.yml b/changelogs/unreleased/update-deprecated-slot-syntax-in-app-assets-javascripts-issue_show-compon.yml
new file mode 100644
index 00000000000..7fcf63f7fca
--- /dev/null
+++ b/changelogs/unreleased/update-deprecated-slot-syntax-in-app-assets-javascripts-issue_show-compon.yml
@@ -0,0 +1,5 @@
+---
+title: Update deprecated slot syntax in ./app/assets/javascripts/issue_show/components/fields/description.vue
+merge_request: 31979
+author: Gilang Gumilar
+type: other
diff --git a/changelogs/unreleased/update-doorkeeper-503.yml b/changelogs/unreleased/update-doorkeeper-503.yml
new file mode 100644
index 00000000000..f11dbd74ed8
--- /dev/null
+++ b/changelogs/unreleased/update-doorkeeper-503.yml
@@ -0,0 +1,5 @@
+---
+title: Update doorkeeper to latest version 5.0.3
+merge_request: 31673
+author:
+type: other
diff --git a/changelogs/unreleased/update-gitlab-managed-helm-to-2-16-6.yml b/changelogs/unreleased/update-gitlab-managed-helm-to-2-16-6.yml
new file mode 100644
index 00000000000..09b06d54356
--- /dev/null
+++ b/changelogs/unreleased/update-gitlab-managed-helm-to-2-16-6.yml
@@ -0,0 +1,6 @@
+---
+title: Update GitLab-managed helm from 2.16.3 to 2.16.6, improving the reliability
+ of GitLab's Kubernetes integration
+merge_request: 30067
+author:
+type: changed
diff --git a/changelogs/unreleased/update-gitlab-runner-helm-chart-to-0-16-1.yml b/changelogs/unreleased/update-gitlab-runner-helm-chart-to-0-16-1.yml
new file mode 100644
index 00000000000..ef7394ccddd
--- /dev/null
+++ b/changelogs/unreleased/update-gitlab-runner-helm-chart-to-0-16-1.yml
@@ -0,0 +1,5 @@
+---
+title: Update GitLab Runner Helm Chart to 0.16.1
+merge_request: 31492
+author:
+type: other
diff --git a/changelogs/unreleased/update-jupyter-to-0-9-0.yml b/changelogs/unreleased/update-jupyter-to-0-9-0.yml
new file mode 100644
index 00000000000..2e309513b87
--- /dev/null
+++ b/changelogs/unreleased/update-jupyter-to-0-9-0.yml
@@ -0,0 +1,5 @@
+---
+title: Update managed jupyter chart to 0.9.0 (stable)
+merge_request: 30393
+author:
+type: changed
diff --git a/changelogs/unreleased/update-recursive-open-struct-gem.yml b/changelogs/unreleased/update-recursive-open-struct-gem.yml
new file mode 100644
index 00000000000..0bab3114d85
--- /dev/null
+++ b/changelogs/unreleased/update-recursive-open-struct-gem.yml
@@ -0,0 +1,5 @@
+---
+title: Update recursive-open-struct to 1.1.1 to make it compatible with ruby 2.7
+merge_request: 31047
+author:
+type: fixed
diff --git a/changelogs/unreleased/update_fluentd_model_to_include_log_flags.yml b/changelogs/unreleased/update_fluentd_model_to_include_log_flags.yml
new file mode 100644
index 00000000000..8d38f8f794c
--- /dev/null
+++ b/changelogs/unreleased/update_fluentd_model_to_include_log_flags.yml
@@ -0,0 +1,5 @@
+---
+title: Update Fluentd model to support multiple logs
+merge_request: 29458
+author:
+type: changed
diff --git a/changelogs/unreleased/update_ui_to_support_cilium_log_checkbox.yml b/changelogs/unreleased/update_ui_to_support_cilium_log_checkbox.yml
new file mode 100644
index 00000000000..0022ac84024
--- /dev/null
+++ b/changelogs/unreleased/update_ui_to_support_cilium_log_checkbox.yml
@@ -0,0 +1,5 @@
+---
+title: Add Cilium to Fluentd UI controls on the Cluster Application page
+merge_request: 29511
+author:
+type: changed
diff --git a/changelogs/unreleased/user-api-spec.yml b/changelogs/unreleased/user-api-spec.yml
new file mode 100644
index 00000000000..06f1b5acd24
--- /dev/null
+++ b/changelogs/unreleased/user-api-spec.yml
@@ -0,0 +1,5 @@
+---
+title: Replace let! with let_it_be in user api spec
+merge_request: 31901
+author: Rajendra Kadam
+type: fixed
diff --git a/changelogs/unreleased/user-can-enable-alert-management.yml b/changelogs/unreleased/user-can-enable-alert-management.yml
new file mode 100644
index 00000000000..5012a078b26
--- /dev/null
+++ b/changelogs/unreleased/user-can-enable-alert-management.yml
@@ -0,0 +1,5 @@
+---
+title: Alert management can user enable
+merge_request: 30024
+author:
+type: changed
diff --git a/changelogs/unreleased/vij-fix-update-bug.yml b/changelogs/unreleased/vij-fix-update-bug.yml
new file mode 100644
index 00000000000..ea636f9e4b3
--- /dev/null
+++ b/changelogs/unreleased/vij-fix-update-bug.yml
@@ -0,0 +1,5 @@
+---
+title: Fix Snippet update error bug losing changes
+merge_request: 31873
+author:
+type: fixed
diff --git a/changelogs/unreleased/vij-invalid-path-error.yml b/changelogs/unreleased/vij-invalid-path-error.yml
new file mode 100644
index 00000000000..bf13c09610e
--- /dev/null
+++ b/changelogs/unreleased/vij-invalid-path-error.yml
@@ -0,0 +1,5 @@
+---
+title: Handle Snippet file name errors in backfill
+merge_request: 30981
+author:
+type: fixed
diff --git a/changelogs/unreleased/vij-snippet-blob-search-views.yml b/changelogs/unreleased/vij-snippet-blob-search-views.yml
new file mode 100644
index 00000000000..44cbd5c9346
--- /dev/null
+++ b/changelogs/unreleased/vij-snippet-blob-search-views.yml
@@ -0,0 +1,5 @@
+---
+title: Rename Snippet search results title
+merge_request: 29599
+author:
+type: other
diff --git a/changelogs/unreleased/vij-snippet-code-references.yml b/changelogs/unreleased/vij-snippet-code-references.yml
new file mode 100644
index 00000000000..c01ff35cad7
--- /dev/null
+++ b/changelogs/unreleased/vij-snippet-code-references.yml
@@ -0,0 +1,5 @@
+---
+title: Remove deprecated Snippet `code` attribute from Project Snippets API
+merge_request: 30739
+author:
+type: other
diff --git a/changelogs/unreleased/vij-snippet-create-update-errors.yml b/changelogs/unreleased/vij-snippet-create-update-errors.yml
new file mode 100644
index 00000000000..36714435da3
--- /dev/null
+++ b/changelogs/unreleased/vij-snippet-create-update-errors.yml
@@ -0,0 +1,5 @@
+---
+title: Modify Snippet git path errors to be more helpful
+merge_request: 31333
+author:
+type: changed
diff --git a/config/application.rb b/config/application.rb
index a135bef342a..d8f02277527 100644
--- a/config/application.rb
+++ b/config/application.rb
@@ -131,6 +131,7 @@ module Gitlab
encrypted_key
hook
import_url
+ elasticsearch_url
otp_attempt
sentry_dsn
trace
diff --git a/config/cable.yml.example b/config/cable.yml.example
new file mode 100644
index 00000000000..ee3a8da9be8
--- /dev/null
+++ b/config/cable.yml.example
@@ -0,0 +1,14 @@
+# This file is used for configuring ActionCable in our CI environment
+# When using GDK or Omnibus, cable.yml is generated from a different template
+development:
+ adapter: redis
+ url: redis://localhost:6379
+ channel_prefix: gitlab_development
+test:
+ adapter: redis
+ url: redis://localhost:6379
+ channel_prefix: gitlab_test
+production:
+ adapter: redis
+ url: unix:/var/run/redis/redis.sock
+ channel_prefix: gitlab_production
diff --git a/config/environments/test.rb b/config/environments/test.rb
index 71cd5200415..c130eb84baa 100644
--- a/config/environments/test.rb
+++ b/config/environments/test.rb
@@ -1,12 +1,13 @@
require 'gitlab/testing/request_blocker_middleware'
require 'gitlab/testing/request_inspector_middleware'
-require 'gitlab/testing/clear_thread_memory_cache_middleware'
+require 'gitlab/testing/clear_process_memory_cache_middleware'
+require 'gitlab/utils'
Rails.application.configure do
# Make sure the middleware is inserted first in middleware chain
config.middleware.insert_before(ActionDispatch::Static, Gitlab::Testing::RequestBlockerMiddleware)
config.middleware.insert_before(ActionDispatch::Static, Gitlab::Testing::RequestInspectorMiddleware)
- config.middleware.insert_before(ActionDispatch::Static, Gitlab::Testing::ClearThreadMemoryCacheMiddleware)
+ config.middleware.insert_before(ActionDispatch::Static, Gitlab::Testing::ClearProcessMemoryCacheMiddleware)
# Settings specified here will take precedence over those in config/application.rb
@@ -43,7 +44,7 @@ Rails.application.configure do
# Print deprecation notices to the stderr
config.active_support.deprecation = :stderr
- config.eager_load = true
+ config.eager_load = Gitlab::Utils.to_boolean(ENV['GITLAB_TEST_EAGER_LOAD'], default: true)
config.cache_store = :null_store
diff --git a/config/feature_categories.yml b/config/feature_categories.yml
index f12c0ef466a..010d3d14fcb 100644
--- a/config/feature_categories.yml
+++ b/config/feature_categories.yml
@@ -8,6 +8,7 @@
#
---
- accessibility_testing
+- alert_management
- analysis
- api
- attack_emulation
@@ -17,11 +18,10 @@
- auto_devops
- backup_restore
- behavior_analytics
-- chaos_engineering
+- billing
- chatops
- cloud_native_installation
- cluster_cost_optimization
-- cluster_monitoring
- code_analytics
- code_quality
- code_review
@@ -71,6 +71,7 @@
- kubernetes_management
- language_specific
- license_compliance
+- live_preview
- load_testing
- logging
- malware_scanning
@@ -92,6 +93,7 @@
- runner
- secret_detection
- secrets_management
+- security_benchmarking
- serverless
- service_desk
- snippets
diff --git a/config/gitlab.yml.example b/config/gitlab.yml.example
index 090ec9265df..9a2e470f852 100644
--- a/config/gitlab.yml.example
+++ b/config/gitlab.yml.example
@@ -107,6 +107,9 @@ production: &base
# S/MIME public certificate key in PEM format, will be attached to signed messages
# Default is '.gitlab_smime_cert' relative to Rails.root (i.e. root of the GitLab app).
# cert_file: /home/git/gitlab/.gitlab_smime_cert
+ # S/MIME extra CA public certificates in PEM format, will be attached to signed messages
+ # Optional
+ # ca_certs_file: /home/git/gitlab/.gitlab_smime_ca_certs
# Email server smtp settings are in config/initializers/smtp_settings.rb.sample
@@ -199,6 +202,9 @@ production: &base
#
# log_path: log/mail_room_json.log
+ # Whether to expunge (permanently remove) messages from the mailbox when they are deleted after delivery
+ expunge_deleted: false
+
## Build Artifacts
artifacts:
enabled: true
@@ -767,7 +773,7 @@ production: &base
# Allow smartcard authentication
enabled: false
- # Path to a file containing a CA certificate
+ # Path to a file containing a CA certificate bundle
ca_file: '/etc/ssl/certs/CA.pem'
# Host and port where the client side certificate is requested by the
@@ -1062,6 +1068,11 @@ production: &base
# host: localhost
# port: 3808
+ ## ActionCable settings
+ action_cable:
+ # Number of threads used to process ActionCable connection callbacks and channel actions
+ # worker_pool_size: 4
+
## Monitoring
# Built in monitoring settings
monitoring:
diff --git a/config/helpers/is_eslint.js b/config/helpers/is_eslint.js
new file mode 100644
index 00000000000..5dfb7e533e4
--- /dev/null
+++ b/config/helpers/is_eslint.js
@@ -0,0 +1,18 @@
+/**
+ * Returns true if the given module is required from eslint
+ */
+const isESLint = mod => {
+ let parent = mod.parent;
+
+ while (parent) {
+ if (parent.filename.includes('/eslint')) {
+ return true;
+ }
+
+ parent = parent.parent;
+ }
+
+ return false;
+};
+
+module.exports = isESLint;
diff --git a/config/initializers/0_thread_cache.rb b/config/initializers/0_thread_cache.rb
deleted file mode 100644
index feb8057132e..00000000000
--- a/config/initializers/0_thread_cache.rb
+++ /dev/null
@@ -1,3 +0,0 @@
-# frozen_string_literal: true
-
-Gitlab::ThreadMemoryCache.cache_backend
diff --git a/config/initializers/1_settings.rb b/config/initializers/1_settings.rb
index c3fcd0f8ff0..c0cd491547a 100644
--- a/config/initializers/1_settings.rb
+++ b/config/initializers/1_settings.rb
@@ -487,6 +487,12 @@ Settings.cron_jobs['namespaces_prune_aggregation_schedules_worker']['job_class']
Settings.cron_jobs['container_expiration_policy_worker'] ||= Settingslogic.new({})
Settings.cron_jobs['container_expiration_policy_worker']['cron'] ||= '50 * * * *'
Settings.cron_jobs['container_expiration_policy_worker']['job_class'] = 'ContainerExpirationPolicyWorker'
+Settings.cron_jobs['x509_issuer_crl_check_worker'] ||= Settingslogic.new({})
+Settings.cron_jobs['x509_issuer_crl_check_worker']['cron'] ||= '30 1 * * *'
+Settings.cron_jobs['x509_issuer_crl_check_worker']['job_class'] = 'X509IssuerCrlCheckWorker'
+Settings.cron_jobs['users_create_statistics_worker'] ||= Settingslogic.new({})
+Settings.cron_jobs['users_create_statistics_worker']['cron'] ||= '2 15 * * *'
+Settings.cron_jobs['users_create_statistics_worker']['job_class'] = 'Users::CreateStatisticsWorker'
Gitlab.ee do
Settings.cron_jobs['adjourned_group_deletion_worker'] ||= Settingslogic.new({})
@@ -549,9 +555,6 @@ Gitlab.ee do
Settings.cron_jobs['sync_seat_link_worker'] ||= Settingslogic.new({})
Settings.cron_jobs['sync_seat_link_worker']['cron'] ||= "#{rand(60)} 0 * * *"
Settings.cron_jobs['sync_seat_link_worker']['job_class'] = 'SyncSeatLinkWorker'
- Settings.cron_jobs['users_create_statistics_worker'] ||= Settingslogic.new({})
- Settings.cron_jobs['users_create_statistics_worker']['cron'] ||= '2 15 * * *'
- Settings.cron_jobs['users_create_statistics_worker']['job_class'] = 'Users::CreateStatisticsWorker'
end
#
@@ -698,7 +701,6 @@ Settings.rack_attack.git_basic_auth['ip_whitelist'] ||= %w{127.0.0.1}
Settings.rack_attack.git_basic_auth['maxretry'] ||= 10
Settings.rack_attack.git_basic_auth['findtime'] ||= 1.minute
Settings.rack_attack.git_basic_auth['bantime'] ||= 1.hour
-Settings.rack_attack['admin_area_protected_paths_enabled'] ||= false
#
# Gitaly
@@ -715,6 +717,12 @@ Settings.webpack.dev_server['host'] ||= 'localhost'
Settings.webpack.dev_server['port'] ||= 3808
#
+# ActionCable settings
+#
+Settings['action_cable'] ||= Settingslogic.new({})
+Settings.action_cable['worker_pool_size'] ||= 4
+
+#
# Monitoring settings
#
Settings['monitoring'] ||= Settingslogic.new({})
diff --git a/config/initializers/7_prometheus_metrics.rb b/config/initializers/7_prometheus_metrics.rb
index 3ad90ad7d65..267a1f0b1a5 100644
--- a/config/initializers/7_prometheus_metrics.rb
+++ b/config/initializers/7_prometheus_metrics.rb
@@ -44,6 +44,10 @@ if !Rails.env.test? && Gitlab::Metrics.prometheus_metrics_enabled?
Gitlab::Metrics::Samplers::RubySampler.initialize_instance(Settings.monitoring.ruby_sampler_interval).start
+ if Gitlab::Utils.to_boolean(ENV['ENABLE_DATABASE_CONNECTION_POOL_METRICS'])
+ Gitlab::Metrics::Samplers::DatabaseSampler.initialize_instance(Gitlab::Metrics::Samplers::DatabaseSampler::SAMPLING_INTERVAL_SECONDS).start
+ end
+
if Gitlab.ee? && Gitlab::Runtime.sidekiq?
Gitlab::Metrics::Samplers::GlobalSearchSampler.instance(Settings.monitoring.global_search_sampler_interval).start
end
@@ -61,6 +65,8 @@ if !Rails.env.test? && Gitlab::Metrics.prometheus_metrics_enabled?
Gitlab::Metrics::Samplers::PumaSampler.instance(Settings.monitoring.puma_sampler_interval).start
end
+ Gitlab::Metrics.gauge(:deployments, 'GitLab Version', {}, :max).set({ version: Gitlab::VERSION }, 1)
+
Gitlab::Metrics::RequestsRackMiddleware.initialize_http_request_duration_seconds
rescue IOError => e
Gitlab::ErrorTracking.track_exception(e)
diff --git a/config/initializers/action_cable.rb b/config/initializers/action_cable.rb
new file mode 100644
index 00000000000..eb44ff00d09
--- /dev/null
+++ b/config/initializers/action_cable.rb
@@ -0,0 +1,9 @@
+# frozen_string_literal: true
+
+Rails.application.configure do
+ # We only mount the ActionCable engine in tests where we run it in-app
+ # For other environments, we run it on a standalone Puma server
+ config.action_cable.mount_path = Rails.env.test? ? '/-/cable' : nil
+ config.action_cable.url = Gitlab::Utils.append_path(Gitlab.config.gitlab.relative_url_root, '/-/cable')
+ config.action_cable.worker_pool_size = Gitlab.config.action_cable.worker_pool_size
+end
diff --git a/config/initializers/actioncable.rb b/config/initializers/actioncable.rb
deleted file mode 100644
index ed96f965150..00000000000
--- a/config/initializers/actioncable.rb
+++ /dev/null
@@ -1,8 +0,0 @@
-# frozen_string_literal: true
-
-Rails.application.configure do
- # Prevents the default engine from being mounted because
- # we're running ActionCable as a standalone server
- config.action_cable.mount_path = nil
- config.action_cable.url = Gitlab::Utils.append_path(Gitlab.config.gitlab.relative_url_root, '/-/cable')
-end
diff --git a/config/initializers/active_record_fix_insert_all.rb b/config/initializers/active_record_fix_insert_all.rb
deleted file mode 100644
index 8ae208dd0e5..00000000000
--- a/config/initializers/active_record_fix_insert_all.rb
+++ /dev/null
@@ -1,26 +0,0 @@
-# frozen_string_literal: true
-
-# This fix is needed to properly support
-# columns that perform data mutation to a SQL datatype
-# ex. would be `jsonb` and `enum`
-#
-# This is covered by tests in `BulkInsertSafe`
-# that validates handling of different data types
-
-if Rails.gem_version > Gem::Version.new("6.0.2")
- raise Gem::DependencyError,
- "Remove patch once the https://github.com/rails/rails/pull/38763 is included"
-end
-
-module ActiveRecordInsertAllBuilderMixin
- def extract_types_from_columns_on(table_name, keys:)
- columns = connection.schema_cache.columns_hash(table_name)
-
- unknown_column = (keys - columns.keys).first
- raise UnknownAttributeError.new(model.new, unknown_column) if unknown_column
-
- keys.index_with { |key| model.type_for_attribute(key) }
- end
-end
-
-ActiveRecord::InsertAll::Builder.prepend(ActiveRecordInsertAllBuilderMixin)
diff --git a/config/initializers/cookies_serializer.rb b/config/initializers/cookies_serializer.rb
index a04d5044f4e..fa1736dfea6 100644
--- a/config/initializers/cookies_serializer.rb
+++ b/config/initializers/cookies_serializer.rb
@@ -1,4 +1,4 @@
# Be sure to restart your server when you modify this file.
-Rails.application.config.action_dispatch.use_cookies_with_metadata = false
+Rails.application.config.action_dispatch.use_cookies_with_metadata = true
Rails.application.config.action_dispatch.cookies_serializer = :hybrid
diff --git a/config/initializers/gettext_rails_i18n_patch.rb b/config/initializers/gettext_rails_i18n_patch.rb
index 714dd505824..09c9b325a04 100644
--- a/config/initializers/gettext_rails_i18n_patch.rb
+++ b/config/initializers/gettext_rails_i18n_patch.rb
@@ -45,7 +45,7 @@ module GettextI18nRailsJs
private
def gettext_messages_by_file
- @gettext_messages_by_file ||= JSON.parse(load_messages)
+ @gettext_messages_by_file ||= Gitlab::Json.parse(load_messages)
end
def load_messages
diff --git a/config/initializers/measuring.rb b/config/initializers/measuring.rb
new file mode 100644
index 00000000000..79258cda365
--- /dev/null
+++ b/config/initializers/measuring.rb
@@ -0,0 +1,3 @@
+# frozen_string_literal: true
+
+Gitlab::Utils::Measuring.logger = Gitlab::Services::Logger.build
diff --git a/config/initializers/rack_attack.rb.example b/config/initializers/rack_attack.rb.example
deleted file mode 100644
index 69052c029f2..00000000000
--- a/config/initializers/rack_attack.rb.example
+++ /dev/null
@@ -1,29 +0,0 @@
-# 1. Rename this file to rack_attack.rb
-# 2. Review the paths_to_be_protected and add any other path you need protecting
-#
-# If you change this file in a Merge Request, please also create a Merge Request on https://gitlab.com/gitlab-org/omnibus-gitlab/merge_requests
-
-paths_to_be_protected = [
- "#{Rails.application.config.relative_url_root}/users/password",
- "#{Rails.application.config.relative_url_root}/users/sign_in",
- "#{Rails.application.config.relative_url_root}/api/#{API::API.version}/session.json",
- "#{Rails.application.config.relative_url_root}/api/#{API::API.version}/session",
- "#{Rails.application.config.relative_url_root}/users",
- "#{Rails.application.config.relative_url_root}/users/confirmation",
- "#{Rails.application.config.relative_url_root}/unsubscribes/",
- "#{Rails.application.config.relative_url_root}/import/github/personal_access_token"
-
-]
-
-# Create one big regular expression that matches strings starting with any of
-# the paths_to_be_protected.
-paths_regex = Regexp.union(paths_to_be_protected.map { |path| /\A#{Regexp.escape(path)}/ })
-rack_attack_enabled = Gitlab.config.rack_attack.git_basic_auth['enabled']
-
-unless Rails.env.test? || !rack_attack_enabled
- Rack::Attack.throttle('protected paths', limit: 10, period: 60.seconds) do |req|
- if req.post? && req.path =~ paths_regex
- req.ip
- end
- end
-end
diff --git a/config/initializers/rack_attack_new.rb b/config/initializers/rack_attack_new.rb
index 267d4c1eda9..51b49bec864 100644
--- a/config/initializers/rack_attack_new.rb
+++ b/config/initializers/rack_attack_new.rb
@@ -8,17 +8,9 @@ module Gitlab::Throttle
# Returns true if we should use the Admin Area protected paths throttle
def self.protected_paths_enabled?
- return false if should_use_omnibus_protected_paths?
-
self.settings.throttle_protected_paths_enabled?
end
- # To be removed in 13.0: https://gitlab.com/gitlab-org/gitlab/issues/29952
- def self.should_use_omnibus_protected_paths?
- !Settings.rack_attack.admin_area_protected_paths_enabled &&
- self.omnibus_protected_paths_present?
- end
-
def self.omnibus_protected_paths_present?
Rack::Attack.throttles.key?('protected paths')
end
@@ -168,5 +160,5 @@ class Rack::Attack
end
end
-::Rack::Attack.extend_if_ee('::EE::Gitlab::Rack::Attack') # rubocop: disable Cop/InjectEnterpriseEditionModule
+::Rack::Attack.extend_if_ee('::EE::Gitlab::Rack::Attack')
::Rack::Attack::Request.prepend_if_ee('::EE::Gitlab::Rack::Attack::Request')
diff --git a/config/initializers/sidekiq.rb b/config/initializers/sidekiq.rb
index fa4fc2d2c7b..febcedfee82 100644
--- a/config/initializers/sidekiq.rb
+++ b/config/initializers/sidekiq.rb
@@ -1,7 +1,5 @@
# frozen_string_literal: true
-require 'sidekiq/web'
-
def enable_reliable_fetch?
return true unless Feature::FlipperFeature.table_exists?
@@ -14,26 +12,14 @@ def enable_semi_reliable_fetch_mode?
Feature.enabled?(:gitlab_sidekiq_enable_semi_reliable_fetcher, default_enabled: true)
end
-# Disable the Sidekiq Rack session since GitLab already has its own session store.
-# CSRF protection still works (https://github.com/mperham/sidekiq/commit/315504e766c4fd88a29b7772169060afc4c40329).
-Sidekiq::Web.set :sessions, false
-
# Custom Queues configuration
queues_config_hash = Gitlab::Redis::Queues.params
queues_config_hash[:namespace] = Gitlab::Redis::Queues::SIDEKIQ_NAMESPACE
-# Default is to retry 25 times with exponential backoff. That's too much.
-Sidekiq.default_worker_options = { retry: 3 }
-
-if Rails.env.development?
- Sidekiq.default_worker_options[:backtrace] = true
-end
-
enable_json_logs = Gitlab.config.sidekiq.log_format == 'json'
enable_sidekiq_memory_killer = ENV['SIDEKIQ_MEMORY_KILLER_MAX_RSS'].to_i.nonzero?
use_sidekiq_daemon_memory_killer = ENV["SIDEKIQ_DAEMON_MEMORY_KILLER"].to_i.nonzero?
use_sidekiq_legacy_memory_killer = !use_sidekiq_daemon_memory_killer
-use_request_store = ENV.fetch('SIDEKIQ_REQUEST_STORE', 1).to_i.nonzero?
Sidekiq.configure_server do |config|
if enable_json_logs
@@ -50,8 +36,7 @@ Sidekiq.configure_server do |config|
config.server_middleware(&Gitlab::SidekiqMiddleware.server_configurator({
metrics: Settings.monitoring.sidekiq_exporter,
arguments_logger: ENV['SIDEKIQ_LOG_ARGUMENTS'] && !enable_json_logs,
- memory_killer: enable_sidekiq_memory_killer && use_sidekiq_legacy_memory_killer,
- request_store: use_request_store
+ memory_killer: enable_sidekiq_memory_killer && use_sidekiq_legacy_memory_killer
}))
config.client_middleware(&Gitlab::SidekiqMiddleware.client_configurator)
@@ -77,7 +62,7 @@ Sidekiq.configure_server do |config|
# Sidekiq-cron: load recurring jobs from gitlab.yml
# UGLY Hack to get nested hash from settingslogic
- cron_jobs = JSON.parse(Gitlab.config.cron_jobs.to_json)
+ cron_jobs = Gitlab::Json.parse(Gitlab.config.cron_jobs.to_json)
# UGLY hack: Settingslogic doesn't allow 'class' key
cron_jobs_required_keys = %w(job_class cron)
cron_jobs.each do |k, v|
diff --git a/config/initializers/zz_metrics.rb b/config/initializers/zz_metrics.rb
index 5bbfb97277c..26f6743f480 100644
--- a/config/initializers/zz_metrics.rb
+++ b/config/initializers/zz_metrics.rb
@@ -100,7 +100,7 @@ def instrument_classes(instrumentation)
instrumentation.instrument_instance_methods(Gitlab::Elastic::ProjectSearchResults)
instrumentation.instrument_instance_methods(Gitlab::Elastic::Indexer)
instrumentation.instrument_instance_methods(Gitlab::Elastic::SnippetSearchResults)
- instrumentation.instrument_methods(Gitlab::Elastic::Helper)
+ instrumentation.instrument_instance_methods(Gitlab::Elastic::Helper)
instrumentation.instrument_instance_methods(Elastic::ApplicationVersionedSearch)
instrumentation.instrument_instance_methods(Elastic::ProjectsSearch)
@@ -135,7 +135,6 @@ end
# loading of our custom migration templates.
if Gitlab::Metrics.enabled? && !Rails.env.test? && !(Rails.env.development? && defined?(Rails::Generators))
require 'pathname'
- require 'influxdb'
require 'connection_pool'
require 'method_source'
@@ -193,10 +192,6 @@ if Gitlab::Metrics.enabled? && !Rails.env.test? && !(Rails.env.development? && d
GC::Profiler.enable
- Gitlab::Cluster::LifecycleEvents.on_worker_start do
- Gitlab::Metrics::Samplers::InfluxSampler.initialize_instance.start
- end
-
module TrackNewRedisConnections
def connect(*args)
val = super
diff --git a/config/initializers_before_autoloader/002_sidekiq.rb b/config/initializers_before_autoloader/002_sidekiq.rb
new file mode 100644
index 00000000000..4ce9127a45d
--- /dev/null
+++ b/config/initializers_before_autoloader/002_sidekiq.rb
@@ -0,0 +1,20 @@
+# frozen_string_literal: true
+
+# Preloads Sidekiq configurations that don't require application references.
+#
+# It ensures default settings are loaded before any other file references
+# (directly or indirectly) Sidekiq workers.
+#
+
+require 'sidekiq/web'
+
+# Disable the Sidekiq Rack session since GitLab already has its own session store.
+# CSRF protection still works (https://github.com/mperham/sidekiq/commit/315504e766c4fd88a29b7772169060afc4c40329).
+Sidekiq::Web.set :sessions, false
+
+# Default is to retry 25 times with exponential backoff. That's too much.
+Sidekiq.default_worker_options = { retry: 3 }
+
+if Rails.env.development?
+ Sidekiq.default_worker_options[:backtrace] = true
+end
diff --git a/config/initializers_before_autoloader/100_patch_omniauth_oauth2.rb b/config/initializers_before_autoloader/100_patch_omniauth_oauth2.rb
new file mode 100644
index 00000000000..760fcba5935
--- /dev/null
+++ b/config/initializers_before_autoloader/100_patch_omniauth_oauth2.rb
@@ -0,0 +1,17 @@
+# frozen_string_literal: true
+
+module OmniAuth
+ module Strategies
+ class OAuth2
+ alias_method :original_callback_phase, :callback_phase
+
+ # Monkey patch until PR is merged and released upstream
+ # https://github.com/omniauth/omniauth-oauth2/pull/129
+ def callback_phase
+ original_callback_phase
+ rescue ::Faraday::TimeoutError, ::Faraday::ConnectionFailed => e
+ fail!(:timeout, e)
+ end
+ end
+ end
+end
diff --git a/config/locales/doorkeeper.en.yml b/config/locales/doorkeeper.en.yml
index c9dbde23d35..93293a0236c 100644
--- a/config/locales/doorkeeper.en.yml
+++ b/config/locales/doorkeeper.en.yml
@@ -88,6 +88,19 @@ en:
Grants read-only access to the user's profile data using OpenID Connect.
email:
Grants read-only access to the user's primary email address using OpenID Connect.
+ project_access_token_scope_desc:
+ api:
+ Grants complete read/write access to the scoped project API.
+ read_api:
+ Grants read access to the scoped project API.
+ read_repository:
+ Allows read-only access (pull) to the repository.
+ write_repository:
+ Allows read-write access (pull, push) to the repository.
+ read_registry:
+ Allows read-access (pull) to container registry images if the project is private and authorization is required.
+ write_registry:
+ Allows write-access (push) to container registry.
flash:
applications:
create:
diff --git a/config/mail_room.yml b/config/mail_room.yml
index da37ef60587..23170acbf65 100644
--- a/config/mail_room.yml
+++ b/config/mail_room.yml
@@ -17,6 +17,7 @@
:name: <%= config[:mailbox].to_json %>
:delete_after_delivery: true
+ :expunge_deleted: <%= config[:expunge_deleted].to_json %>
:delivery_method: sidekiq
:delivery_options:
diff --git a/config/prometheus/common_metrics.yml b/config/prometheus/common_metrics.yml
index 4d0ea4a345d..f0491df3db9 100644
--- a/config/prometheus/common_metrics.yml
+++ b/config/prometheus/common_metrics.yml
@@ -10,7 +10,7 @@ panel_groups:
weight: 4
metrics:
- id: system_metrics_kubernetes_container_memory_total
- query_range: 'avg(sum(container_memory_usage_bytes{container_name!="POD",pod_name=~"^%{ci_environment_slug}-(.*)",namespace="%{kube_namespace}"}) by (job)) without (job) /1024/1024/1024'
+ query_range: 'avg(sum(container_memory_usage_bytes{container_name!="POD",pod_name=~"^{{ci_environment_slug}}-(.*)",namespace="{{kube_namespace}}"}) by (job)) without (job) /1024/1024/1024'
label: Total (GB)
unit: GB
- title: "Core Usage (Total)"
@@ -19,7 +19,7 @@ panel_groups:
weight: 3
metrics:
- id: system_metrics_kubernetes_container_cores_total
- query_range: 'avg(sum(rate(container_cpu_usage_seconds_total{container_name!="POD",pod_name=~"^%{ci_environment_slug}-(.*)",namespace="%{kube_namespace}"}[15m])) by (job)) without (job)'
+ query_range: 'avg(sum(rate(container_cpu_usage_seconds_total{container_name!="POD",pod_name=~"^{{ci_environment_slug}}-(.*)",namespace="{{kube_namespace}}"}[15m])) by (job)) without (job)'
label: Total (cores)
unit: "cores"
- title: "Memory Usage (Pod average)"
@@ -28,7 +28,7 @@ panel_groups:
weight: 2
metrics:
- id: system_metrics_kubernetes_container_memory_average
- query_range: 'avg(sum(container_memory_usage_bytes{container_name!="POD",pod_name=~"^%{ci_environment_slug}-([^c].*|c([^a]|a([^n]|n([^a]|a([^r]|r[^y])))).*|)-(.*)",namespace="%{kube_namespace}"}) by (job)) without (job) / count(avg(container_memory_usage_bytes{container_name!="POD",pod_name=~"^%{ci_environment_slug}-([^c].*|c([^a]|a([^n]|n([^a]|a([^r]|r[^y])))).*|)-(.*)",namespace="%{kube_namespace}"}) without (job)) /1024/1024'
+ query_range: 'avg(sum(container_memory_usage_bytes{container_name!="POD",pod_name=~"^{{ci_environment_slug}}-([^c].*|c([^a]|a([^n]|n([^a]|a([^r]|r[^y])))).*|)-(.*)",namespace="{{kube_namespace}}"}) by (job)) without (job) / count(avg(container_memory_usage_bytes{container_name!="POD",pod_name=~"^{{ci_environment_slug}}-([^c].*|c([^a]|a([^n]|n([^a]|a([^r]|r[^y])))).*|)-(.*)",namespace="{{kube_namespace}}"}) without (job)) /1024/1024'
label: Pod average (MB)
unit: MB
- title: "Canary: Memory Usage (Pod Average)"
@@ -37,7 +37,7 @@ panel_groups:
weight: 2
metrics:
- id: system_metrics_kubernetes_container_memory_average_canary
- query_range: 'avg(sum(container_memory_usage_bytes{container_name!="POD",pod_name=~"^%{ci_environment_slug}-canary-(.*)",namespace="%{kube_namespace}"}) by (job)) without (job) / count(avg(container_memory_usage_bytes{container_name!="POD",pod_name=~"^%{ci_environment_slug}-canary-(.*)",namespace="%{kube_namespace}"}) without (job)) /1024/1024'
+ query_range: 'avg(sum(container_memory_usage_bytes{container_name!="POD",pod_name=~"^{{ci_environment_slug}}-canary-(.*)",namespace="{{kube_namespace}}"}) by (job)) without (job) / count(avg(container_memory_usage_bytes{container_name!="POD",pod_name=~"^{{ci_environment_slug}}-canary-(.*)",namespace="{{kube_namespace}}"}) without (job)) /1024/1024'
label: Pod average (MB)
unit: MB
track: canary
@@ -47,7 +47,7 @@ panel_groups:
weight: 1
metrics:
- id: system_metrics_kubernetes_container_core_usage
- query_range: 'avg(sum(rate(container_cpu_usage_seconds_total{container_name!="POD",pod_name=~"^%{ci_environment_slug}-([^c].*|c([^a]|a([^n]|n([^a]|a([^r]|r[^y])))).*|)-(.*)",namespace="%{kube_namespace}"}[15m])) by (job)) without (job) / count(sum(rate(container_cpu_usage_seconds_total{container_name!="POD",pod_name=~"^%{ci_environment_slug}-([^c].*|c([^a]|a([^n]|n([^a]|a([^r]|r[^y])))).*|)-(.*)",namespace="%{kube_namespace}"}[15m])) by (pod_name))'
+ query_range: 'avg(sum(rate(container_cpu_usage_seconds_total{container_name!="POD",pod_name=~"^{{ci_environment_slug}}-([^c].*|c([^a]|a([^n]|n([^a]|a([^r]|r[^y])))).*|)-(.*)",namespace="{{kube_namespace}}"}[15m])) by (job)) without (job) / count(sum(rate(container_cpu_usage_seconds_total{container_name!="POD",pod_name=~"^{{ci_environment_slug}}-([^c].*|c([^a]|a([^n]|n([^a]|a([^r]|r[^y])))).*|)-(.*)",namespace="{{kube_namespace}}"}[15m])) by (pod_name))'
label: Pod average (cores)
unit: "cores"
- title: "Canary: Core Usage (Pod Average)"
@@ -56,7 +56,7 @@ panel_groups:
weight: 1
metrics:
- id: system_metrics_kubernetes_container_core_usage_canary
- query_range: 'avg(sum(rate(container_cpu_usage_seconds_total{container_name!="POD",pod_name=~"^%{ci_environment_slug}-canary-(.*)",namespace="%{kube_namespace}"}[15m])) by (job)) without (job) / count(sum(rate(container_cpu_usage_seconds_total{container_name!="POD",pod_name=~"^%{ci_environment_slug}-canary-(.*)",namespace="%{kube_namespace}"}[15m])) by (pod_name))'
+ query_range: 'avg(sum(rate(container_cpu_usage_seconds_total{container_name!="POD",pod_name=~"^{{ci_environment_slug}}-canary-(.*)",namespace="{{kube_namespace}}"}[15m])) by (job)) without (job) / count(sum(rate(container_cpu_usage_seconds_total{container_name!="POD",pod_name=~"^{{ci_environment_slug}}-canary-(.*)",namespace="{{kube_namespace}}"}[15m])) by (pod_name))'
label: Pod average (cores)
unit: "cores"
track: canary
@@ -66,7 +66,7 @@ panel_groups:
weight: 1
metrics:
- id: system_metrics_knative_function_invocation_count
- query_range: 'sum(ceil(rate(istio_requests_total{destination_service_namespace="%{kube_namespace}", destination_service=~"%{function_name}.*"}[1m])*60))'
+ query_range: 'sum(ceil(rate(istio_requests_total{destination_service_namespace="{{kube_namespace}}", destination_service=~"{{function_name}}.*"}[1m])*60))'
label: invocations / minute
unit: requests
# NGINX Ingress metrics for pre-0.16.0 versions
@@ -79,7 +79,7 @@ panel_groups:
weight: 1
metrics:
- id: response_metrics_nginx_ingress_throughput_status_code
- query_range: 'sum(rate(nginx_upstream_responses_total{upstream=~"%{kube_namespace}-%{ci_environment_slug}-.*"}[2m])) by (status_code)'
+ query_range: 'sum(rate(nginx_upstream_responses_total{upstream=~"{{kube_namespace}}-{{ci_environment_slug}}-.*"}[2m])) by (status_code)'
unit: req / sec
label: Status Code
- title: "Latency"
@@ -90,7 +90,7 @@ panel_groups:
weight: 1
metrics:
- id: response_metrics_nginx_ingress_latency_pod_average
- query_range: 'avg(nginx_upstream_response_msecs_avg{upstream=~"%{kube_namespace}-%{ci_environment_slug}-.*"})'
+ query_range: 'avg(nginx_upstream_response_msecs_avg{upstream=~"{{kube_namespace}}-{{ci_environment_slug}}-.*"})'
label: Pod average (ms)
unit: ms
- title: "HTTP Error Rate"
@@ -101,7 +101,7 @@ panel_groups:
weight: 1
metrics:
- id: response_metrics_nginx_ingress_http_error_rate
- query_range: 'sum(rate(nginx_upstream_responses_total{status_code="5xx", upstream=~"%{kube_namespace}-%{ci_environment_slug}-.*"}[2m])) / sum(rate(nginx_upstream_responses_total{upstream=~"%{kube_namespace}-%{ci_environment_slug}-.*"}[2m])) * 100'
+ query_range: 'sum(rate(nginx_upstream_responses_total{status_code="5xx", upstream=~"{{kube_namespace}}-{{ci_environment_slug}}-.*"}[2m])) / sum(rate(nginx_upstream_responses_total{upstream=~"{{kube_namespace}}-{{ci_environment_slug}}-.*"}[2m])) * 100'
label: 5xx Errors (%)
unit: "%"
# NGINX Ingress metrics for post-0.16.0 versions
@@ -114,7 +114,7 @@ panel_groups:
weight: 1
metrics:
- id: response_metrics_nginx_ingress_16_throughput_status_code
- query_range: 'sum(label_replace(rate(nginx_ingress_controller_requests{namespace="%{kube_namespace}",ingress=~".*%{ci_environment_slug}.*"}[2m]), "status_code", "${1}xx", "status", "(.)..")) by (status_code)'
+ query_range: 'sum(label_replace(rate(nginx_ingress_controller_requests{namespace="{{kube_namespace}}",ingress=~".*{{ci_environment_slug}}.*"}[2m]), "status_code", "${1}xx", "status", "(.)..")) by (status_code)'
unit: req / sec
label: Status Code
- title: "Latency"
@@ -123,7 +123,7 @@ panel_groups:
weight: 1
metrics:
- id: response_metrics_nginx_ingress_16_latency_pod_average
- query_range: 'sum(rate(nginx_ingress_controller_ingress_upstream_latency_seconds_sum{namespace="%{kube_namespace}",ingress=~".*%{ci_environment_slug}.*"}[2m])) / sum(rate(nginx_ingress_controller_ingress_upstream_latency_seconds_count{namespace="%{kube_namespace}",ingress=~".*%{ci_environment_slug}.*"}[2m])) * 1000'
+ query_range: 'sum(rate(nginx_ingress_controller_ingress_upstream_latency_seconds_sum{namespace="{{kube_namespace}}",ingress=~".*{{ci_environment_slug}}.*"}[2m])) / sum(rate(nginx_ingress_controller_ingress_upstream_latency_seconds_count{namespace="{{kube_namespace}}",ingress=~".*{{ci_environment_slug}}.*"}[2m])) * 1000'
label: Pod average (ms)
unit: ms
- title: "HTTP Error Rate"
@@ -132,7 +132,7 @@ panel_groups:
weight: 1
metrics:
- id: response_metrics_nginx_ingress_16_http_error_rate
- query_range: 'sum(rate(nginx_ingress_controller_requests{status=~"5.*",namespace="%{kube_namespace}",ingress=~".*%{ci_environment_slug}.*"}[2m])) / sum(rate(nginx_ingress_controller_requests{namespace="%{kube_namespace}",ingress=~".*%{ci_environment_slug}.*"}[2m])) * 100'
+ query_range: 'sum(rate(nginx_ingress_controller_requests{status=~"5.*",namespace="{{kube_namespace}}",ingress=~".*{{ci_environment_slug}}.*"}[2m])) / sum(rate(nginx_ingress_controller_requests{namespace="{{kube_namespace}}",ingress=~".*{{ci_environment_slug}}.*"}[2m])) * 100'
label: 5xx Errors (%)
unit: "%"
- group: Response metrics (HA Proxy)
@@ -144,7 +144,7 @@ panel_groups:
weight: 1
metrics:
- id: response_metrics_ha_proxy_throughput_status_code
- query_range: 'sum(rate(haproxy_frontend_http_requests_total{%{environment_filter}}[2m])) by (code)'
+ query_range: 'sum(rate(haproxy_frontend_http_requests_total{ {{environment_filter}} }[2m])) by (code)'
unit: req / sec
label: Status Code
- title: "HTTP Error Rate"
@@ -153,7 +153,7 @@ panel_groups:
weight: 1
metrics:
- id: response_metrics_ha_proxy_http_error_rate
- query_range: 'sum(rate(haproxy_frontend_http_responses_total{code="5xx",%{environment_filter}}[2m])) / sum(rate(haproxy_frontend_http_responses_total{%{environment_filter}}[2m]))'
+ query_range: 'sum(rate(haproxy_frontend_http_responses_total{code="5xx",{{environment_filter}} }[2m])) / sum(rate(haproxy_frontend_http_responses_total{ {{environment_filter}} }[2m]))'
label: HTTP Errors (%)
unit: "%"
- group: Response metrics (AWS ELB)
@@ -165,7 +165,7 @@ panel_groups:
weight: 1
metrics:
- id: response_metrics_aws_elb_throughput_requests
- query_range: 'sum(aws_elb_request_count_sum{%{environment_filter}}) / 60'
+ query_range: 'sum(aws_elb_request_count_sum{ {{environment_filter}} }) / 60'
label: Total (req/sec)
unit: req / sec
- title: "Latency"
@@ -174,7 +174,7 @@ panel_groups:
weight: 1
metrics:
- id: response_metrics_aws_elb_latency_average
- query_range: 'avg(aws_elb_latency_average{%{environment_filter}}) * 1000'
+ query_range: 'avg(aws_elb_latency_average{ {{environment_filter}} }) * 1000'
label: Average (ms)
unit: ms
- title: "HTTP Error Rate"
@@ -183,7 +183,7 @@ panel_groups:
weight: 1
metrics:
- id: response_metrics_aws_elb_http_error_rate
- query_range: 'sum(aws_elb_httpcode_backend_5_xx_sum{%{environment_filter}}) / sum(aws_elb_request_count_sum{%{environment_filter}})'
+ query_range: 'sum(aws_elb_httpcode_backend_5_xx_sum{ {{environment_filter}} }) / sum(aws_elb_request_count_sum{ {{environment_filter}} })'
label: HTTP Errors (%)
unit: "%"
- group: Response metrics (NGINX)
@@ -195,7 +195,7 @@ panel_groups:
weight: 1
metrics:
- id: response_metrics_nginx_throughput_status_code
- query_range: 'sum(rate(nginx_server_requests{server_zone!="*", server_zone!="_", %{environment_filter}}[2m])) by (code)'
+ query_range: 'sum(rate(nginx_server_requests{server_zone!="*", server_zone!="_", {{environment_filter}} }[2m])) by (code)'
unit: req / sec
label: Status Code
- title: "Latency"
@@ -204,7 +204,7 @@ panel_groups:
weight: 1
metrics:
- id: response_metrics_nginx_latency
- query_range: 'avg(nginx_server_requestMsec{%{environment_filter}})'
+ query_range: 'avg(nginx_server_requestMsec{ {{environment_filter}} })'
label: Upstream (ms)
unit: ms
- title: "HTTP Error Rate (Errors / Sec)"
@@ -215,7 +215,7 @@ panel_groups:
weight: 1
metrics:
- id: response_metrics_nginx_http_error_rate
- query_range: 'sum(rate(nginx_server_requests{code="5xx", %{environment_filter}}[2m]))'
+ query_range: 'sum(rate(nginx_server_requests{code="5xx", {{environment_filter}} }[2m]))'
label: HTTP Errors
unit: "errors / sec"
- title: "HTTP Error Rate"
@@ -224,7 +224,6 @@ panel_groups:
weight: 1
metrics:
- id: response_metrics_nginx_http_error_percentage
- query_range: 'sum(rate(nginx_server_requests{code=~"5.*", host="*", %{environment_filter}}[2m])) / sum(rate(nginx_server_requests{code="total", host="*", %{environment_filter}}[2m])) * 100'
+ query_range: 'sum(rate(nginx_server_requests{code=~"5.*", host="*", {{environment_filter}} }[2m])) / sum(rate(nginx_server_requests{code="total", host="*", {{environment_filter}} }[2m])) * 100'
label: 5xx Errors (%)
unit: "%"
-
diff --git a/config/pseudonymizer.yml b/config/pseudonymizer.yml
index 195506ac4a1..e9d5fd5623f 100644
--- a/config/pseudonymizer.yml
+++ b/config/pseudonymizer.yml
@@ -469,7 +469,6 @@ tables:
- last_activity_on
- notified_of_own_activity
- user_type
- - bot_type
- preferred_language
- theme_id
diff --git a/config/puma_actioncable.example.development.rb b/config/puma_actioncable.example.development.rb
index aef15da54f9..c975f9e4f9b 100644
--- a/config/puma_actioncable.example.development.rb
+++ b/config/puma_actioncable.example.development.rb
@@ -43,7 +43,7 @@ queue_requests false
# accepted protocols.
bind 'unix:///home/git/gitlab_actioncable.socket'
-workers 2
+workers 1
require_relative "/home/git/gitlab/lib/gitlab/cluster/lifecycle_events"
diff --git a/config/redis.cache.yml.example b/config/redis.cache.yml.example
index 27478f0a93e..b20f1dd2122 100644
--- a/config/redis.cache.yml.example
+++ b/config/redis.cache.yml.example
@@ -10,7 +10,7 @@ development:
# host: localhost
# port: 26380 # point to sentinel, not to redis port
# -
- # host: slave2
+ # host: replica2
# port: 26380 # point to sentinel, not to redis port
test:
url: redis://localhost:6379/10
@@ -31,8 +31,8 @@ production:
# url: redis://master:6380
# sentinels:
# -
- # host: slave1
+ # host: replica1
# port: 26380 # point to sentinel, not to redis port
# -
- # host: slave2
+ # host: replica2
# port: 26380 # point to sentinel, not to redis port
diff --git a/config/redis.queues.yml.example b/config/redis.queues.yml.example
index dab1f26b096..46ab39729c4 100644
--- a/config/redis.queues.yml.example
+++ b/config/redis.queues.yml.example
@@ -10,7 +10,7 @@ development:
# host: localhost
# port: 26381 # point to sentinel, not to redis port
# -
- # host: slave2
+ # host: replica2
# port: 26381 # point to sentinel, not to redis port
test:
url: redis://localhost:6379/11
@@ -31,8 +31,8 @@ production:
# url: redis://master:6381
# sentinels:
# -
- # host: slave1
+ # host: replica1
# port: 26381 # point to sentinel, not to redis port
# -
- # host: slave2
+ # host: replica2
# port: 26381 # point to sentinel, not to redis port
diff --git a/config/redis.shared_state.yml.example b/config/redis.shared_state.yml.example
index 9371e3619b7..05fed947f52 100644
--- a/config/redis.shared_state.yml.example
+++ b/config/redis.shared_state.yml.example
@@ -10,7 +10,7 @@ development:
# host: localhost
# port: 26382 # point to sentinel, not to redis port
# -
- # host: slave2
+ # host: replica2
# port: 26382 # point to sentinel, not to redis port
test:
url: redis://localhost:6379/12
@@ -31,8 +31,8 @@ production:
# url: redis://master:6382
# sentinels:
# -
- # host: slave1
+ # host: replica1
# port: 26382 # point to sentinel, not to redis port
# -
- # host: slave2
+ # host: replica2
# port: 26382 # point to sentinel, not to redis port
diff --git a/config/resque.yml.example b/config/resque.yml.example
index 0c19d8bc1d3..932c1553dfb 100644
--- a/config/resque.yml.example
+++ b/config/resque.yml.example
@@ -8,7 +8,7 @@ development:
# host: localhost
# port: 26380 # point to sentinel, not to redis port
# -
- # host: slave2
+ # host: replica2
# port: 26381 # point to sentinel, not to redis port
test:
url: redis://localhost:6379
@@ -27,8 +27,8 @@ production:
# url: redis://master:6379
# sentinels:
# -
- # host: slave1
+ # host: replica1
# port: 26379 # point to sentinel, not to redis port
# -
- # host: slave2
+ # host: replica2
# port: 26379 # point to sentinel, not to redis port
diff --git a/config/routes.rb b/config/routes.rb
index 097814d90a7..86f42822299 100644
--- a/config/routes.rb
+++ b/config/routes.rb
@@ -44,19 +44,6 @@ Rails.application.routes.draw do
use_doorkeeper_openid_connect
- # Autocomplete
- get '/autocomplete/users' => 'autocomplete#users'
- get '/autocomplete/users/:id' => 'autocomplete#user'
- get '/autocomplete/projects' => 'autocomplete#projects'
- get '/autocomplete/award_emojis' => 'autocomplete#award_emojis'
- get '/autocomplete/merge_request_target_branches' => 'autocomplete#merge_request_target_branches'
-
- Gitlab.ee do
- get '/autocomplete/project_groups' => 'autocomplete#project_groups'
- get '/autocomplete/project_routes' => 'autocomplete#project_routes'
- get '/autocomplete/namespace_routes' => 'autocomplete#namespace_routes'
- end
-
# Sign up
get 'users/sign_up/welcome' => 'registrations#welcome'
patch 'users/sign_up/update_registration' => 'registrations#update_registration'
@@ -75,6 +62,19 @@ Rails.application.routes.draw do
# Begin of the /-/ scope.
# Use this scope for all new global routes.
scope path: '-' do
+ # Autocomplete
+ get '/autocomplete/users' => 'autocomplete#users'
+ get '/autocomplete/users/:id' => 'autocomplete#user'
+ get '/autocomplete/projects' => 'autocomplete#projects'
+ get '/autocomplete/award_emojis' => 'autocomplete#award_emojis'
+ get '/autocomplete/merge_request_target_branches' => 'autocomplete#merge_request_target_branches'
+
+ Gitlab.ee do
+ get '/autocomplete/project_groups' => 'autocomplete#project_groups'
+ get '/autocomplete/project_routes' => 'autocomplete#project_routes'
+ get '/autocomplete/namespace_routes' => 'autocomplete#namespace_routes'
+ end
+
# '/-/health' implemented by BasicHealthCheck middleware
get 'liveness' => 'health#liveness'
get 'readiness' => 'health#readiness'
@@ -129,6 +129,9 @@ Rails.application.routes.draw do
scope '/push_from_secondary/:geo_node_id' do
draw :git_http
end
+
+ # Used for survey responses
+ resources :survey_responses, only: :index
end
if ENV['GITLAB_CHAOS_SECRET'] || Rails.env.development? || Rails.env.test?
@@ -199,6 +202,19 @@ Rails.application.routes.draw do
# Deprecated routes.
# Will be removed as part of https://gitlab.com/gitlab-org/gitlab/-/issues/210024
scope as: :deprecated do
+ # Autocomplete
+ get '/autocomplete/users' => 'autocomplete#users'
+ get '/autocomplete/users/:id' => 'autocomplete#user'
+ get '/autocomplete/projects' => 'autocomplete#projects'
+ get '/autocomplete/award_emojis' => 'autocomplete#award_emojis'
+ get '/autocomplete/merge_request_target_branches' => 'autocomplete#merge_request_target_branches'
+
+ Gitlab.ee do
+ get '/autocomplete/project_groups' => 'autocomplete#project_groups'
+ get '/autocomplete/project_routes' => 'autocomplete#project_routes'
+ get '/autocomplete/namespace_routes' => 'autocomplete#namespace_routes'
+ end
+
resources :invites, only: [:show], constraints: { id: /[A-Za-z0-9_-]+/ } do
member do
post :accept
diff --git a/config/routes/admin.rb b/config/routes/admin.rb
index 96cd6e5f587..f3b7fb5ed45 100644
--- a/config/routes/admin.rb
+++ b/config/routes/admin.rb
@@ -81,7 +81,6 @@ namespace :admin do
post :preview, on: :collection
end
- resource :logs, only: [:show]
resource :health_check, controller: 'health_check', only: [:show]
resource :background_jobs, controller: 'background_jobs', only: [:show]
@@ -116,10 +115,6 @@ namespace :admin do
end
resource :application_settings, only: :update do
- # This redirect should be removed with 13.0 release.
- # https://gitlab.com/gitlab-org/gitlab/issues/199427
- get '/', to: redirect('admin/application_settings/general'), as: nil
-
resources :services, only: [:index, :edit, :update]
resources :integrations, only: [:edit, :update] do
member do
@@ -159,6 +154,10 @@ namespace :admin do
end
end
+ namespace :ci do
+ resource :variables, only: [:show, :update]
+ end
+
concerns :clusterable
get '/dashboard/stats', to: 'dashboard#stats'
diff --git a/config/routes/issues.rb b/config/routes/issues.rb
index 51b4637b89f..04a935c1016 100644
--- a/config/routes/issues.rb
+++ b/config/routes/issues.rb
@@ -13,6 +13,7 @@ resources :issues, concerns: :awardable, constraints: { id: /\d+/ } do
get :realtime_changes
post :create_merge_request
get :discussions, format: :json
+ get '/designs(/*vueroute)', to: 'issues#designs', as: :designs, format: false
end
collection do
diff --git a/config/routes/merge_requests.rb b/config/routes/merge_requests.rb
index fe58649b684..f6c45081ce0 100644
--- a/config/routes/merge_requests.rb
+++ b/config/routes/merge_requests.rb
@@ -14,7 +14,9 @@ resources :merge_requests, concerns: :awardable, except: [:new, :create, :show],
post :rebase
get :test_reports
get :exposed_artifacts
+ get :accessibility_reports
get :coverage_reports
+ get :terraform_reports
scope constraints: ->(req) { req.format == :json }, as: :json do
get :commits
diff --git a/config/routes/pipelines.rb b/config/routes/pipelines.rb
new file mode 100644
index 00000000000..cc3c3400526
--- /dev/null
+++ b/config/routes/pipelines.rb
@@ -0,0 +1,37 @@
+# frozen_string_literal: true
+
+resources :pipelines, only: [:index, :new, :create, :show, :destroy] do
+ collection do
+ resource :pipelines_settings, path: 'settings', only: [:show, :update]
+ get :charts
+ scope '(*ref)', constraints: { ref: Gitlab::PathRegex.git_reference_regex } do
+ get :latest, action: :show, defaults: { latest: true }
+ end
+ end
+
+ member do
+ get :stage
+ get :stage_ajax
+ post :cancel
+ post :retry
+ get :builds
+ get :dag
+ get :failures
+ get :status
+ get :test_report
+ get :test_reports_count
+ end
+
+ member do
+ resources :stages, only: [], param: :name do
+ post :play_manual
+ end
+ end
+end
+
+resources :pipeline_schedules, except: [:show] do
+ member do
+ post :play
+ post :take_ownership
+ end
+end
diff --git a/config/routes/project.rb b/config/routes/project.rb
index 29e83b8dd5d..020bfa7687d 100644
--- a/config/routes/project.rb
+++ b/config/routes/project.rb
@@ -65,6 +65,7 @@ constraints(::Constraints::ProjectUrlConstrainer.new) do
namespace :ci do
resource :lint, only: [:show, :create]
+ resources :daily_build_group_report_results, only: [:index], constraints: { format: 'csv' }
end
namespace :settings do
@@ -90,6 +91,12 @@ constraints(::Constraints::ProjectUrlConstrainer.new) do
post :create_deploy_token, path: 'deploy_token/create'
post :cleanup
end
+
+ resources :access_tokens, only: [:index, :create] do
+ member do
+ put :revoke
+ end
+ end
end
resources :autocomplete_sources, only: [] do
@@ -277,6 +284,10 @@ constraints(::Constraints::ProjectUrlConstrainer.new) do
end
end
+ resources :alert_management, only: [:index] do
+ get 'details', on: :member
+ end
+
namespace :error_tracking do
resources :projects, only: :index
end
@@ -295,6 +306,13 @@ constraints(::Constraints::ProjectUrlConstrainer.new) do
end
end
+ namespace :design_management do
+ namespace :designs, path: 'designs/:design_id(/:sha)', constraints: -> (params) { params[:sha].nil? || Gitlab::Git.commit_id?(params[:sha]) } do
+ resource :raw_image, only: :show
+ resources :resized_image, only: :show, constraints: -> (params) { DesignManagement::DESIGN_IMAGE_SIZES.include?(params[:id]) }
+ end
+ end
+
draw :issues
draw :merge_requests
@@ -314,7 +332,6 @@ constraints(::Constraints::ProjectUrlConstrainer.new) do
# All new routes should go under /-/ scope.
# Look for scope '-' at the top of the file.
- # rubocop: disable Cop/PutProjectRoutesUnderScope
#
# Templates
@@ -330,8 +347,8 @@ constraints(::Constraints::ProjectUrlConstrainer.new) do
defaults: { format: 'json' },
constraints: { template_type: %r{issue|merge_request}, format: 'json' }
- resource :pages, only: [:show, :update, :destroy] do
- resources :domains, except: :index, controller: 'pages_domains', constraints: { id: %r{[^/]+} } do
+ resource :pages, only: [:show, :update, :destroy] do # rubocop: disable Cop/PutProjectRoutesUnderScope
+ resources :domains, except: :index, controller: 'pages_domains', constraints: { id: %r{[^/]+} } do # rubocop: disable Cop/PutProjectRoutesUnderScope
member do
post :verify
post :retry_auto_ssl
@@ -340,7 +357,7 @@ constraints(::Constraints::ProjectUrlConstrainer.new) do
end
end
- resources :snippets, concerns: :awardable, constraints: { id: /\d+/ } do
+ resources :snippets, concerns: :awardable, constraints: { id: /\d+/ } do # rubocop: disable Cop/PutProjectRoutesUnderScope
member do
get :raw
post :mark_as_spam
@@ -348,14 +365,14 @@ constraints(::Constraints::ProjectUrlConstrainer.new) do
end
namespace :prometheus do
- resources :alerts, constraints: { id: /\d+/ }, only: [:index, :create, :show, :update, :destroy] do
+ resources :alerts, constraints: { id: /\d+/ }, only: [:index, :create, :show, :update, :destroy] do # rubocop: disable Cop/PutProjectRoutesUnderScope
post :notify, on: :collection
member do
get :metrics_dashboard
end
end
- resources :metrics, constraints: { id: %r{[^\/]+} }, only: [:index, :new, :create, :edit, :update, :destroy] do
+ resources :metrics, constraints: { id: %r{[^\/]+} }, only: [:index, :new, :create, :edit, :update, :destroy] do # rubocop: disable Cop/PutProjectRoutesUnderScope
get :active_common, on: :collection
post :validate_query, on: :collection
end
@@ -363,65 +380,41 @@ constraints(::Constraints::ProjectUrlConstrainer.new) do
post 'alerts/notify', to: 'alerting/notifications#create'
- resources :pipelines, only: [:index, :new, :create, :show, :destroy] do
- collection do
- resource :pipelines_settings, path: 'settings', only: [:show, :update]
- get :charts
- scope '(*ref)', constraints: { ref: Gitlab::PathRegex.git_reference_regex } do
- get :latest, action: :show, defaults: { latest: true }
- end
- end
-
- member do
- get :stage
- get :stage_ajax
- post :cancel
- post :retry
- get :builds
- get :failures
- get :status
- get :test_report
- get :test_reports_count
- end
-
- member do
- resources :stages, only: [], param: :name do
- post :play_manual
- end
- end
- end
+ # Unscoped route. It will be replaced with redirect to /-/pipelines/
+ # Issue https://gitlab.com/gitlab-org/gitlab/issues/118849
+ draw :pipelines
- resources :pipeline_schedules, except: [:show] do
- member do
- post :play
- post :take_ownership
- end
+ # To ensure an old unscoped routing is used for the UI we need to
+ # add prefix 'as' to the scope routing and place it below original routing.
+ # Issue https://gitlab.com/gitlab-org/gitlab/issues/118849
+ scope '-', as: 'scoped' do
+ draw :pipelines
end
draw :legacy_builds
- resources :hooks, only: [:index, :create, :edit, :update, :destroy], constraints: { id: /\d+/ } do
+ resources :hooks, only: [:index, :create, :edit, :update, :destroy], constraints: { id: /\d+/ } do # rubocop: disable Cop/PutProjectRoutesUnderScope
member do
post :test
end
- resources :hook_logs, only: [:show] do
+ resources :hook_logs, only: [:show] do # rubocop: disable Cop/PutProjectRoutesUnderScope
member do
post :retry
end
end
end
- resources :container_registry, only: [:index, :destroy, :show],
+ resources :container_registry, only: [:index, :destroy, :show], # rubocop: disable Cop/PutProjectRoutesUnderScope
controller: 'registry/repositories'
namespace :registry do
- resources :repository, only: [] do
+ resources :repository, only: [] do # rubocop: disable Cop/PutProjectRoutesUnderScope
# We default to JSON format in the controller to avoid ambiguity.
# `latest.json` could either be a request for a tag named `latest`
# in JSON format, or a request for tag named `latest.json`.
scope format: false do
- resources :tags, only: [:index, :destroy],
+ resources :tags, only: [:index, :destroy], # rubocop: disable Cop/PutProjectRoutesUnderScope
constraints: { id: Gitlab::Regex.container_registry_tag_regex } do
collection do
delete :bulk_destroy
@@ -431,13 +424,7 @@ constraints(::Constraints::ProjectUrlConstrainer.new) do
end
end
- # Unscoped route. It will be replaced with redirect to /-/issues/
- # Issue https://gitlab.com/gitlab-org/gitlab/issues/118849
- scope as: 'deprecated' do
- draw :issues
- end
-
- resources :notes, only: [:create, :destroy, :update], concerns: :awardable, constraints: { id: /\d+/ } do
+ resources :notes, only: [:create, :destroy, :update], concerns: :awardable, constraints: { id: /\d+/ } do # rubocop: disable Cop/PutProjectRoutesUnderScope
member do
delete :delete_attachment
post :resolve
@@ -447,16 +434,16 @@ constraints(::Constraints::ProjectUrlConstrainer.new) do
get 'noteable/:target_type/:target_id/notes' => 'notes#index', as: 'noteable_notes'
- resources :todos, only: [:create]
+ resources :todos, only: [:create] # rubocop: disable Cop/PutProjectRoutesUnderScope
- resources :uploads, only: [:create] do
+ resources :uploads, only: [:create] do # rubocop: disable Cop/PutProjectRoutesUnderScope
collection do
get ":secret/:filename", action: :show, as: :show, constraints: { filename: %r{[^/]+} }, format: false, defaults: { format: nil }
post :authorize
end
end
- resources :runners, only: [:index, :edit, :update, :destroy, :show] do
+ resources :runners, only: [:index, :edit, :update, :destroy, :show] do # rubocop: disable Cop/PutProjectRoutesUnderScope
member do
post :resume
post :pause
@@ -468,8 +455,8 @@ constraints(::Constraints::ProjectUrlConstrainer.new) do
end
end
- resources :runner_projects, only: [:create, :destroy]
- resources :badges, only: [:index] do
+ resources :runner_projects, only: [:create, :destroy] # rubocop: disable Cop/PutProjectRoutesUnderScope
+ resources :badges, only: [:index] do # rubocop: disable Cop/PutProjectRoutesUnderScope
collection do
scope '*ref', constraints: { ref: Gitlab::PathRegex.git_reference_regex } do
constraints format: /svg/ do
@@ -482,6 +469,7 @@ constraints(::Constraints::ProjectUrlConstrainer.new) do
scope :usage_ping, controller: :usage_ping do
post :web_ide_clientside_preview
+ post :web_ide_pipelines_count
end
# Deprecated unscoped routing.
@@ -492,20 +480,15 @@ constraints(::Constraints::ProjectUrlConstrainer.new) do
# All new routes should go under /-/ scope.
# Look for scope '-' at the top of the file.
- # rubocop: enable Cop/PutProjectRoutesUnderScope
# Legacy routes.
# Introduced in 12.0.
# Should be removed with https://gitlab.com/gitlab-org/gitlab/issues/28848.
- Gitlab::Routing.redirect_legacy_paths(self, :settings, :branches, :tags,
- :network, :graphs, :autocomplete_sources,
- :project_members, :deploy_keys, :deploy_tokens,
- :labels, :milestones, :services, :boards, :releases,
- :forks, :group_links, :import, :avatar, :mirror,
+ Gitlab::Routing.redirect_legacy_paths(self, :mirror, :tags,
:cycle_analytics, :mattermost, :variables, :triggers,
- :environments, :protected_environments, :error_tracking,
+ :environments, :protected_environments, :error_tracking, :alert_management,
:serverless, :clusters, :audit_events, :wikis, :merge_requests,
- :vulnerability_feedback, :security, :dependencies)
+ :vulnerability_feedback, :security, :dependencies, :issues)
end
# rubocop: disable Cop/PutProjectRoutesUnderScope
diff --git a/config/routes/repository_scoped.rb b/config/routes/repository_scoped.rb
index 42ec8ca1806..865a5bdb5a9 100644
--- a/config/routes/repository_scoped.rb
+++ b/config/routes/repository_scoped.rb
@@ -31,9 +31,9 @@ scope format: false do
resources :protected_branches, only: [:index, :show, :create, :update, :destroy, :patch], constraints: { id: Gitlab::PathRegex.git_reference_regex }
resources :protected_tags, only: [:index, :show, :create, :update, :destroy]
- scope constraints: { id: /[^\0]+/ } do
+ scope constraints: { id: /[^\0]+?/ } do
scope controller: :static_site_editor do
- get '/sse/*id', action: :show, as: :show_sse
+ get '/sse/:id(/*vueroute)', action: :show, as: :show_sse
end
end
end
diff --git a/config/sidekiq_queues.yml b/config/sidekiq_queues.yml
index 9e446cd1b9a..e6e0b4b4409 100644
--- a/config/sidekiq_queues.yml
+++ b/config/sidekiq_queues.yml
@@ -32,6 +32,8 @@
- 1
- - authorized_keys
- 2
+- - authorized_project_update
+ - 1
- - authorized_projects
- 2
- - auto_devops
@@ -100,6 +102,8 @@
- 1
- - export_csv
- 1
+- - external_service_reactive_caching
+ - 1
- - file_hook
- 1
- - gcp_cluster
diff --git a/config/smime_signature_settings.rb b/config/smime_signature_settings.rb
index 3d19db84c19..4a8cf1a06f7 100644
--- a/config/smime_signature_settings.rb
+++ b/config/smime_signature_settings.rb
@@ -5,6 +5,7 @@ class SmimeSignatureSettings
email_smime['enabled'] = false unless email_smime['enabled']
email_smime['key_file'] ||= Rails.root.join('.gitlab_smime_key')
email_smime['cert_file'] ||= Rails.root.join('.gitlab_smime_cert')
+ email_smime['ca_certs_file'] ||= nil
email_smime
end
diff --git a/config/webpack.config.js b/config/webpack.config.js
index e220482d769..7c130b010b6 100644
--- a/config/webpack.config.js
+++ b/config/webpack.config.js
@@ -141,8 +141,8 @@ module.exports = {
output: {
path: path.join(ROOT_PATH, 'public/assets/webpack'),
publicPath: '/assets/webpack/',
- filename: IS_PRODUCTION ? '[name].[chunkhash:8].bundle.js' : '[name].bundle.js',
- chunkFilename: IS_PRODUCTION ? '[name].[chunkhash:8].chunk.js' : '[name].chunk.js',
+ filename: IS_PRODUCTION ? '[name].[contenthash:8].bundle.js' : '[name].bundle.js',
+ chunkFilename: IS_PRODUCTION ? '[name].[contenthash:8].chunk.js' : '[name].chunk.js',
globalObject: 'this', // allow HMR and web workers to play nice
},
@@ -191,7 +191,7 @@ module.exports = {
test: /icons\.svg$/,
loader: 'file-loader',
options: {
- name: '[name].[hash:8].[ext]',
+ name: '[name].[contenthash:8].[ext]',
},
},
{
@@ -210,7 +210,7 @@ module.exports = {
{
loader: 'worker-loader',
options: {
- name: '[name].[hash:8].worker.js',
+ name: '[name].[contenthash:8].worker.js',
inline: IS_DEV_SERVER,
},
},
@@ -222,7 +222,7 @@ module.exports = {
exclude: /node_modules/,
loader: 'file-loader',
options: {
- name: '[name].[hash:8].[ext]',
+ name: '[name].[contenthash:8].[ext]',
},
},
{
@@ -232,7 +232,8 @@ module.exports = {
{
loader: 'css-loader',
options: {
- name: '[name].[hash:8].[ext]',
+ modules: 'global',
+ localIdentName: '[name].[contenthash:8].[ext]',
},
},
],
@@ -242,13 +243,15 @@ module.exports = {
include: /node_modules\/katex\/dist\/fonts/,
loader: 'file-loader',
options: {
- name: '[name].[hash:8].[ext]',
+ name: '[name].[contenthash:8].[ext]',
},
},
],
},
optimization: {
+ // Replace 'hashed' with 'deterministic' in webpack 5
+ moduleIds: 'hashed',
runtimeChunk: 'single',
splitChunks: {
maxInitialRequests: 4,
@@ -260,6 +263,30 @@ module.exports = {
chunks: 'initial',
minChunks: autoEntriesCount * 0.9,
}),
+ monaco: {
+ priority: 15,
+ name: 'monaco',
+ chunks: 'initial',
+ test: /[\\/]node_modules[\\/]monaco-editor[\\/]/,
+ minChunks: 2,
+ reuseExistingChunk: true,
+ },
+ echarts: {
+ priority: 14,
+ name: 'echarts',
+ chunks: 'all',
+ test: /[\\/]node_modules[\\/](echarts|zrender)[\\/]/,
+ minChunks: 2,
+ reuseExistingChunk: true,
+ },
+ security_reports: {
+ priority: 13,
+ name: 'security_reports',
+ chunks: 'initial',
+ test: /[\\/](vue_shared[\\/](security_reports|license_compliance)|security_dashboard)[\\/]/,
+ minChunks: 2,
+ reuseExistingChunk: true,
+ },
vendors: {
priority: 10,
chunks: 'async',
diff --git a/config/webpack.vendor.config.js b/config/webpack.vendor.config.js
index 7ecb9b06fdd..bebd1d656b5 100644
--- a/config/webpack.vendor.config.js
+++ b/config/webpack.vendor.config.js
@@ -29,7 +29,6 @@ module.exports = {
'core-js',
'echarts',
'lodash',
- 'underscore',
'vuex',
'pikaday',
'vue/dist/vue.esm.js',
diff --git a/danger/changelog/Dangerfile b/danger/changelog/Dangerfile
index 210fe24c1e3..7b8a096639d 100644
--- a/danger/changelog/Dangerfile
+++ b/danger/changelog/Dangerfile
@@ -17,7 +17,7 @@ If you want to create a changelog entry for GitLab EE, run the following instead
bin/changelog --ee -m %<mr_iid>s "%<mr_title>s"
```
-Note: Merge requests with %<labels>s do not trigger this check.
+If this merge request [doesn't need a CHANGELOG entry](https://docs.gitlab.com/ee/development/changelog.html#what-warrants-a-changelog-entry), feel free to ignore this message.
MSG
def check_changelog_yaml(path)
@@ -57,7 +57,7 @@ end
if git.modified_files.include?("CHANGELOG.md")
fail "**CHANGELOG.md was edited.** Please remove the additions and create a CHANGELOG entry.\n\n" +
- format(CREATE_CHANGELOG_MESSAGE, mr_iid: gitlab.mr_json["iid"], mr_title: sanitized_mr_title, labels: changelog.presented_no_changelog_labels)
+ format(CREATE_CHANGELOG_MESSAGE, mr_iid: gitlab.mr_json["iid"], mr_title: sanitized_mr_title)
end
changelog_found = changelog.found
@@ -66,6 +66,6 @@ if changelog_found
check_changelog_yaml(changelog_found)
check_changelog_path(changelog_found)
elsif changelog.needed?
- message "**[CHANGELOG missing](https://docs.gitlab.com/ee/development/changelog.html)**: If this merge request [doesn't need a CHANGELOG entry](https://docs.gitlab.com/ee/development/changelog.html#what-warrants-a-changelog-entry), feel free to ignore this message.\n\n" +
- format(CREATE_CHANGELOG_MESSAGE, mr_iid: gitlab.mr_json["iid"], mr_title: sanitized_mr_title, labels: changelog.presented_no_changelog_labels)
+ message "**[CHANGELOG missing](https://docs.gitlab.com/ee/development/changelog.html)**:\n\n" +
+ format(CREATE_CHANGELOG_MESSAGE, mr_iid: gitlab.mr_json["iid"], mr_title: sanitized_mr_title)
end
diff --git a/danger/gitlab_ui_wg/Dangerfile b/danger/gitlab_ui_wg/Dangerfile
deleted file mode 100644
index 672b1deecb3..00000000000
--- a/danger/gitlab_ui_wg/Dangerfile
+++ /dev/null
@@ -1,62 +0,0 @@
-FRONTEND_MAINTAINERS = %w[filipa iamphill psimyn sarahghp mishunov].freeze
-UX_MAINTAINERS = %w[tauriedavis rverissimo].freeze
-NO_REVIEWER = 'No reviewer available'.freeze
-
-def mention_single_codebase_approvers
- canonical_branch_name =
- roulette.canonical_branch_name(gitlab.mr_json['source_branch'])
-
- random = roulette.new_random(canonical_branch_name)
-
- frontend_maintainers = helper.new_teammates(FRONTEND_MAINTAINERS)
- ux_maintainers = helper.new_teammates(UX_MAINTAINERS)
-
- rows = []
-
- if gitlab.mr_labels.include?('frontend')
- frontend_maintainer =
- roulette.spin_for_person(frontend_maintainers, random: random)
-
- rows << "| ~frontend | #{frontend_maintainer&.markdown_name || NO_REVIEWER}"
- end
-
- if gitlab.mr_labels.include?('UX')
- ux_maintainers =
- roulette.spin_for_person(ux_maintainers, random: random)
-
- rows << "| ~UX | #{ux_maintainers&.markdown_name || NO_REVIEWER}"
- end
-
- if rows.empty?
- backup_maintainer = frontend_maintainers.sample
-
- rows << "| ~frontend / ~UX | #{backup_maintainer.markdown_name}"
- end
-
- markdown(<<~MARKDOWN.strip)
- ## GitLab UI Working Group changes
-
- This merge request contains changes related to the work of [cleaning up CSS and creating
- reusable components](https://gitlab.com/groups/gitlab-org/-/epics/950).
- These changes will need to be reviewed and approved by the following engineers:
-
- | Category | Reviewer
- |----------|---------
- #{rows.join("\n")}
-
- To make sure this happens, please follow these steps:
-
- 1. Add all of the mentioned users to the list of merge request approvals.
- 2. Assign the merge request to the first person in the above list.
-
- If you are a reviewer, please follow these steps:
-
- 1. Review the merge request. If it is good to go, approve it.
- 2. Once approved, assign to the next person in the above list. If you are
- the last person in the list, merge the merge request.
- MARKDOWN
-end
-
-if gitlab.mr_labels.include?('CSS cleanup')
- mention_single_codebase_approvers
-end
diff --git a/danger/roulette/Dangerfile b/danger/roulette/Dangerfile
index 417b5889bcf..d73f6bf4f10 100644
--- a/danger/roulette/Dangerfile
+++ b/danger/roulette/Dangerfile
@@ -13,8 +13,9 @@ MARKDOWN
CATEGORY_TABLE_HEADER = <<MARKDOWN
To spread load more evenly across eligible reviewers, Danger has randomly picked
-a candidate for each review slot. Feel free to override this selection if you
-think someone else would be better-suited, or the chosen person is unavailable.
+a candidate for each review slot. Feel free to
+[override these selections](https://about.gitlab.com/handbook/engineering/projects/#gitlab)
+if you think someone else would be better-suited, or the chosen person is unavailable.
To read more on how to use the reviewer roulette, please take a look at the
[Engineering workflow](https://about.gitlab.com/handbook/engineering/workflow/#basics)
@@ -76,9 +77,7 @@ categories = changes.keys - [:unknown]
# Ensure to spin for database reviewer/maintainer when ~database is applied (e.g. to review SQL queries)
categories << :database if gitlab.mr_labels.include?('database') && !categories.include?(:database)
-# CSS Clean up MRs are reviewed using a slightly different process, so we
-# disable the review roulette for such MRs.
-if changes.any? && !gitlab.mr_labels.include?('CSS cleanup')
+if changes.any?
# Strip leading and trailing CE/EE markers
canonical_branch_name =
roulette.canonical_branch_name(gitlab.mr_json['source_branch'])
diff --git a/danger/specs/Dangerfile b/danger/specs/Dangerfile
index 50887c44b3e..7803fad8472 100644
--- a/danger/specs/Dangerfile
+++ b/danger/specs/Dangerfile
@@ -7,14 +7,10 @@ That's OK as long as you're refactoring existing code,
but please consider adding any of the %<labels>s labels.
MSG
-def presented_no_changelog_labels
- NO_SPECS_LABELS.map { |label| "~#{label}" }.join(', ')
-end
-
has_app_changes = !helper.all_changed_files.grep(%r{\A(ee/)?(app|lib|db/(geo/)?(post_)?migrate)/}).empty?
has_spec_changes = !helper.all_changed_files.grep(%r{\A(ee/)?spec/}).empty?
new_specs_needed = (gitlab.mr_labels & NO_SPECS_LABELS).empty?
if has_app_changes && !has_spec_changes && new_specs_needed
- warn format(NO_NEW_SPEC_MESSAGE, labels: presented_no_changelog_labels), sticky: false
+ warn format(NO_NEW_SPEC_MESSAGE, labels: helper.labels_list(NO_SPECS_LABELS)), sticky: false
end
diff --git a/danger/telemetry/Dangerfile b/danger/telemetry/Dangerfile
index f308fb206bb..c18a15fcb03 100644
--- a/danger/telemetry/Dangerfile
+++ b/danger/telemetry/Dangerfile
@@ -9,24 +9,27 @@ USAGE_DATA_FILES_MESSAGE = <<~MSG
For the following files, a review from the [Data team and Telemetry team](https://gitlab.com/groups/gitlab-org/growth/telemetry/engineers/-/group_members?with_inherited_permissions=exclude) is recommended:
MSG
-usage_data_changed_files = git.modified_files.grep(%r{usage_data})
+tracking_files = [
+ 'lib/gitlab/tracking.rb',
+ 'spec/lib/gitlab/tracking_spec.rb',
+ 'app/helpers/tracking_helper.rb',
+ 'spec/helpers/tracking_helper_spec.rb',
+ 'app/assets/javascripts/tracking.js',
+ 'spec/frontend/tracking_spec.js'
+ ]
-def has_label?(label)
- gitlab.mr_labels.include?(label)
-end
+usage_data_changed_files = git.modified_files.grep(%r{usage_data})
+snowplow_events_changed_files = git.modified_files & tracking_files
-def labels_for_merge_request(labels)
- labels_list = labels.map { |label| %Q{~"#{label}"} }.join(' ')
- "/label #{labels_list}"
-end
+changed_files = (usage_data_changed_files + snowplow_events_changed_files)
-if usage_data_changed_files.any?
+if changed_files.any?
warn format(TELEMETRY_CHANGED_FILES_MESSAGE)
- markdown(USAGE_DATA_FILES_MESSAGE + helper.markdown_list(usage_data_changed_files))
+ markdown(USAGE_DATA_FILES_MESSAGE + helper.markdown_list(changed_files))
telemetry_labels = ['telemetry']
- telemetry_labels << 'telemetry::review pending' unless has_label?('telemetry::reviewed')
+ telemetry_labels << 'telemetry::review pending' unless helper.mr_has_labels?('telemetry::reviewed')
- markdown(labels_for_merge_request(telemetry_labels))
+ markdown(helper.prepare_labels_for_mr(telemetry_labels))
end
diff --git a/danger/utility_css/Dangerfile b/danger/utility_css/Dangerfile
new file mode 100644
index 00000000000..4f56c40379e
--- /dev/null
+++ b/danger/utility_css/Dangerfile
@@ -0,0 +1,51 @@
+# frozen_string_literal: true
+
+common = 'app/assets/stylesheets/framework/common.scss'
+utilities = 'app/assets/stylesheets/utilities.scss'
+
+def get_css_files(files, common_filepath, utilities_filepath)
+ files.select do |file|
+ file.include?(common_filepath) ||
+ file.include?(utilities_filepath)
+ end
+end
+
+changed_util_files = get_css_files(helper.all_changed_files.to_a, common, utilities)
+
+unless changed_util_files.empty?
+
+ markdown(<<~MARKDOWN)
+ ## Changes to utility SCSS files
+ MARKDOWN
+
+ if changed_util_files.include?(common)
+
+ markdown(<<~MARKDOWN)
+ ### Addition to `#{common}`
+
+ You have added a new rule to `#{common}`. Are you sure you need this rule?
+
+ If it is a component class shared across items, could it be added to the component as a utility class or to the component's stylesheet? If not, you can ignore this warning.
+
+ If it is a new utility class, is there another class that shares the same values in either this file or in `#{utilities}`? If not, please add it to `#{utilities}`, following the [Gitlab UI naming style](https://unpkg.com/browse/@gitlab/ui/src/scss/utilities.scss).
+
+ MARKDOWN
+
+ end
+
+ if changed_util_files.include?(utilities)
+ markdown(<<~MARKDOWN)
+ ### Addition to `#{utilities}`
+
+ You have added a new rule to `#{utilities}`. Are you sure you need this rule?
+
+ If it is a component class shared across items, could it be added to the component as a utility class or to the component's stylesheet? If not, consider adding it to `#{common}`
+
+ If it is a new utility class, is there another class that shares the same values in either this file or in `#{utilities}`? If not, please be sure this addition follows the [Gitlab UI naming style](https://unpkg.com/browse/@gitlab/ui/src/scss/utilities.scss) so it may be removed when these rules are included. See [Include gitlab-ui utility-class library](https://gitlab.com/gitlab-org/gitlab/issues/36857) for more about this project.
+
+ MARKDOWN
+ end
+
+ warn "This merge request adds a new rule to #{common} or #{utilities}."
+
+end
diff --git a/db/migrate/20171230123729_init_schema.rb b/db/migrate/20171230123729_init_schema.rb
index 79c9ebbb691..99ff85ff01f 100644
--- a/db/migrate/20171230123729_init_schema.rb
+++ b/db/migrate/20171230123729_init_schema.rb
@@ -5,7 +5,8 @@
# rubocop:disable Metrics/AbcSize
# rubocop:disable Migration/AddConcurrentForeignKey
# rubocop:disable Style/WordArray
-# rubocop:disable Migration/AddLimitToStringColumns
+# rubocop:disable Migration/PreventStrings
+# rubocop:disable Migration/AddLimitToTextColumns
# rubocop:disable Migration/Datetime
class InitSchema < ActiveRecord::Migration[4.2]
@@ -1854,4 +1855,5 @@ class InitSchema < ActiveRecord::Migration[4.2]
raise ActiveRecord::IrreversibleMigration, "The initial migration is not revertable"
end
end
-# rubocop:enable Migration/AddLimitToStringColumns
+# rubocop:enable Migration/AddLimitToTextColumns
+# rubocop:enable Migration/PreventStrings
diff --git a/db/migrate/20180101160629_create_prometheus_metrics.rb b/db/migrate/20180101160629_create_prometheus_metrics.rb
index a098b999a0a..f94abd0e76e 100644
--- a/db/migrate/20180101160629_create_prometheus_metrics.rb
+++ b/db/migrate/20180101160629_create_prometheus_metrics.rb
@@ -3,8 +3,8 @@
class CreatePrometheusMetrics < ActiveRecord::Migration[4.2]
DOWNTIME = false
+ # rubocop:disable Migration/PreventStrings
def change
- # rubocop:disable Migration/AddLimitToStringColumns
create_table :prometheus_metrics do |t|
t.references :project, index: true, foreign_key: { on_delete: :cascade }, null: false
t.string :title, null: false
@@ -15,6 +15,6 @@ class CreatePrometheusMetrics < ActiveRecord::Migration[4.2]
t.integer :group, null: false, index: true
t.timestamps_with_timezone null: false
end
- # rubocop:enable Migration/AddLimitToStringColumns
end
+ # rubocop:enable Migration/PreventStrings
end
diff --git a/db/migrate/20180115094742_add_default_project_creation_setting.rb b/db/migrate/20180115094742_add_default_project_creation_setting.rb
index 465a89c39e8..b19e588ece3 100644
--- a/db/migrate/20180115094742_add_default_project_creation_setting.rb
+++ b/db/migrate/20180115094742_add_default_project_creation_setting.rb
@@ -7,7 +7,7 @@ class AddDefaultProjectCreationSetting < ActiveRecord::Migration[4.2]
def up
unless column_exists?(:application_settings, :default_project_creation)
- add_column_with_default(:application_settings, :default_project_creation, :integer, default: 2)
+ add_column_with_default(:application_settings, :default_project_creation, :integer, default: 2) # rubocop:disable Migration/AddColumnWithDefault
end
end
diff --git a/db/migrate/20180116193854_create_lfs_file_locks.rb b/db/migrate/20180116193854_create_lfs_file_locks.rb
index b94f69ad42e..6d49281dca2 100644
--- a/db/migrate/20180116193854_create_lfs_file_locks.rb
+++ b/db/migrate/20180116193854_create_lfs_file_locks.rb
@@ -10,7 +10,7 @@ class CreateLfsFileLocks < ActiveRecord::Migration[4.2]
t.references :project, null: false, foreign_key: { on_delete: :cascade }
t.references :user, null: false, index: true, foreign_key: { on_delete: :cascade }
t.datetime :created_at, null: false # rubocop:disable Migration/Datetime
- t.string :path, limit: 511
+ t.string :path, limit: 511 # rubocop:disable Migration/PreventStrings
end
add_index :lfs_file_locks, [:project_id, :path], unique: true
diff --git a/db/migrate/20180122162010_add_auto_devops_domain_to_application_settings.rb b/db/migrate/20180122162010_add_auto_devops_domain_to_application_settings.rb
index eb446ad0d72..603f4eb13db 100644
--- a/db/migrate/20180122162010_add_auto_devops_domain_to_application_settings.rb
+++ b/db/migrate/20180122162010_add_auto_devops_domain_to_application_settings.rb
@@ -7,7 +7,9 @@ class AddAutoDevopsDomainToApplicationSettings < ActiveRecord::Migration[4.2]
# Set this constant to true if this migration requires downtime.
DOWNTIME = false
+ # rubocop:disable Migration/PreventStrings
def change
- add_column :application_settings, :auto_devops_domain, :string # rubocop:disable Migration/AddLimitToStringColumns
+ add_column :application_settings, :auto_devops_domain, :string
end
+ # rubocop:enable Migration/PreventStrings
end
diff --git a/db/migrate/20180129193323_add_uploads_builder_context.rb b/db/migrate/20180129193323_add_uploads_builder_context.rb
index 710fa7b3ba8..308b732dca4 100644
--- a/db/migrate/20180129193323_add_uploads_builder_context.rb
+++ b/db/migrate/20180129193323_add_uploads_builder_context.rb
@@ -7,10 +7,10 @@ class AddUploadsBuilderContext < ActiveRecord::Migration[4.2]
# Set this constant to true if this migration requires downtime.
DOWNTIME = false
+ # rubocop:disable Migration/PreventStrings
def change
- # rubocop:disable Migration/AddLimitToStringColumns
add_column :uploads, :mount_point, :string
add_column :uploads, :secret, :string
- # rubocop:enable Migration/AddLimitToStringColumns
end
+ # rubocop:enable Migration/PreventStrings
end
diff --git a/db/migrate/20180209115333_create_chatops_tables.rb b/db/migrate/20180209115333_create_chatops_tables.rb
index 2cfb71e1007..9e11dfde7a3 100644
--- a/db/migrate/20180209115333_create_chatops_tables.rb
+++ b/db/migrate/20180209115333_create_chatops_tables.rb
@@ -9,7 +9,7 @@ class CreateChatopsTables < ActiveRecord::Migration[4.2]
create_table :ci_pipeline_chat_data, id: :bigserial do |t|
t.integer :pipeline_id, null: false
t.references :chat_name, foreign_key: { on_delete: :cascade }, null: false
- t.text :response_url, null: false
+ t.text :response_url, null: false # rubocop:disable Migration/AddLimitToTextColumns
# A pipeline can only contain one row in this table, hence this index is
# unique.
diff --git a/db/migrate/20180212030105_add_external_ip_to_clusters_applications_ingress.rb b/db/migrate/20180212030105_add_external_ip_to_clusters_applications_ingress.rb
index 78aa2014601..78bb49b8b84 100644
--- a/db/migrate/20180212030105_add_external_ip_to_clusters_applications_ingress.rb
+++ b/db/migrate/20180212030105_add_external_ip_to_clusters_applications_ingress.rb
@@ -3,7 +3,9 @@ class AddExternalIpToClustersApplicationsIngress < ActiveRecord::Migration[4.2]
DOWNTIME = false
+ # rubocop:disable Migration/PreventStrings
def change
- add_column :clusters_applications_ingress, :external_ip, :string # rubocop:disable Migration/AddLimitToStringColumns
+ add_column :clusters_applications_ingress, :external_ip, :string
end
+ # rubocop:enable Migration/PreventStrings
end
diff --git a/db/migrate/20180214093516_create_badges.rb b/db/migrate/20180214093516_create_badges.rb
index fe27d465571..7a0d82d2d81 100644
--- a/db/migrate/20180214093516_create_badges.rb
+++ b/db/migrate/20180214093516_create_badges.rb
@@ -1,8 +1,8 @@
class CreateBadges < ActiveRecord::Migration[4.2]
DOWNTIME = false
+ # rubocop:disable Migration/PreventStrings
def change
- # rubocop:disable Migration/AddLimitToStringColumns
create_table :badges do |t|
t.string :link_url, null: false
t.string :image_url, null: false
@@ -12,9 +12,10 @@ class CreateBadges < ActiveRecord::Migration[4.2]
t.timestamps_with_timezone null: false
end
- # rubocop:enable Migration/AddLimitToStringColumns
# rubocop:disable Migration/AddConcurrentForeignKey
add_foreign_key :badges, :namespaces, column: :group_id, on_delete: :cascade
+ # rubocop:enable Migration/AddConcurrentForeignKey
end
+ # rubocop:enable Migration/PreventStrings
end
diff --git a/db/migrate/20180214155405_create_clusters_applications_runners.rb b/db/migrate/20180214155405_create_clusters_applications_runners.rb
index f611fefbb0d..e6025007507 100644
--- a/db/migrate/20180214155405_create_clusters_applications_runners.rb
+++ b/db/migrate/20180214155405_create_clusters_applications_runners.rb
@@ -13,8 +13,8 @@ class CreateClustersApplicationsRunners < ActiveRecord::Migration[4.2]
t.index :cluster_id, unique: true
t.integer :status, null: false
t.timestamps_with_timezone null: false
- t.string :version, null: false # rubocop:disable Migration/AddLimitToStringColumns
- t.text :status_reason
+ t.string :version, null: false # rubocop:disable Migration/PreventStrings
+ t.text :status_reason # rubocop:disable Migration/AddLimitToTextColumns
end
add_concurrent_foreign_key :clusters_applications_runners, :ci_runners,
diff --git a/db/migrate/20180216120000_add_pages_domain_verification.rb b/db/migrate/20180216120000_add_pages_domain_verification.rb
index b2f19f2e1a9..0600fe633a7 100644
--- a/db/migrate/20180216120000_add_pages_domain_verification.rb
+++ b/db/migrate/20180216120000_add_pages_domain_verification.rb
@@ -3,6 +3,6 @@ class AddPagesDomainVerification < ActiveRecord::Migration[4.2]
def change
add_column :pages_domains, :verified_at, :datetime_with_timezone
- add_column :pages_domains, :verification_code, :string # rubocop:disable Migration/AddLimitToStringColumns
+ add_column :pages_domains, :verification_code, :string # rubocop:disable Migration/PreventStrings
end
end
diff --git a/db/migrate/20180222043024_add_ip_address_to_runner.rb b/db/migrate/20180222043024_add_ip_address_to_runner.rb
index 08fb0bd900c..cc183112f6f 100644
--- a/db/migrate/20180222043024_add_ip_address_to_runner.rb
+++ b/db/migrate/20180222043024_add_ip_address_to_runner.rb
@@ -4,6 +4,6 @@ class AddIpAddressToRunner < ActiveRecord::Migration[4.2]
DOWNTIME = false
def change
- add_column :ci_runners, :ip_address, :string # rubocop:disable Migration/AddLimitToStringColumns
+ add_column :ci_runners, :ip_address, :string # rubocop:disable Migration/PreventStrings
end
end
diff --git a/db/migrate/20180223144945_add_allow_local_requests_from_hooks_and_services_to_application_settings.rb b/db/migrate/20180223144945_add_allow_local_requests_from_hooks_and_services_to_application_settings.rb
index 3bd7d6fd827..3171ff56299 100644
--- a/db/migrate/20180223144945_add_allow_local_requests_from_hooks_and_services_to_application_settings.rb
+++ b/db/migrate/20180223144945_add_allow_local_requests_from_hooks_and_services_to_application_settings.rb
@@ -6,7 +6,7 @@ class AddAllowLocalRequestsFromHooksAndServicesToApplicationSettings < ActiveRec
disable_ddl_transaction!
def up
- add_column_with_default(:application_settings, :allow_local_requests_from_hooks_and_services,
+ add_column_with_default(:application_settings, :allow_local_requests_from_hooks_and_services, # rubocop:disable Migration/AddColumnWithDefault
:boolean,
default: false,
allow_null: false)
diff --git a/db/migrate/20180305144721_add_privileged_to_runner.rb b/db/migrate/20180305144721_add_privileged_to_runner.rb
index 359498bf9b0..1ad3c045d60 100644
--- a/db/migrate/20180305144721_add_privileged_to_runner.rb
+++ b/db/migrate/20180305144721_add_privileged_to_runner.rb
@@ -9,7 +9,7 @@ class AddPrivilegedToRunner < ActiveRecord::Migration[4.2]
disable_ddl_transaction!
def up
- add_column_with_default :clusters_applications_runners, :privileged, :boolean, default: true, allow_null: false
+ add_column_with_default :clusters_applications_runners, :privileged, :boolean, default: true, allow_null: false # rubocop:disable Migration/AddColumnWithDefault
end
def down
diff --git a/db/migrate/20180308125206_add_user_internal_regex_to_application_setting.rb b/db/migrate/20180308125206_add_user_internal_regex_to_application_setting.rb
index 9bdd44baf58..d97d2953677 100644
--- a/db/migrate/20180308125206_add_user_internal_regex_to_application_setting.rb
+++ b/db/migrate/20180308125206_add_user_internal_regex_to_application_setting.rb
@@ -3,9 +3,11 @@ class AddUserInternalRegexToApplicationSetting < ActiveRecord::Migration[4.2]
DOWNTIME = false
+ # rubocop:disable Migration/PreventStrings
def up
- add_column :application_settings, :user_default_internal_regex, :string, null: true # rubocop:disable Migration/AddLimitToStringColumns
+ add_column :application_settings, :user_default_internal_regex, :string, null: true
end
+ # rubocop:enable Migration/PreventStrings
def down
remove_column :application_settings, :user_default_internal_regex
diff --git a/db/migrate/20180314145917_add_header_and_footer_banners_to_appearances_table.rb b/db/migrate/20180314145917_add_header_and_footer_banners_to_appearances_table.rb
index 8aba3448035..a6f2605c906 100644
--- a/db/migrate/20180314145917_add_header_and_footer_banners_to_appearances_table.rb
+++ b/db/migrate/20180314145917_add_header_and_footer_banners_to_appearances_table.rb
@@ -6,6 +6,7 @@ class AddHeaderAndFooterBannersToAppearancesTable < ActiveRecord::Migration[4.2]
DOWNTIME = false
def change
+ # rubocop:disable Migration/AddLimitToTextColumns
add_column :appearances, :header_message, :text
add_column :appearances, :header_message_html, :text
@@ -14,5 +15,6 @@ class AddHeaderAndFooterBannersToAppearancesTable < ActiveRecord::Migration[4.2]
add_column :appearances, :message_background_color, :text
add_column :appearances, :message_font_color, :text
+ # rubocop:enable Migration/AddLimitToTextColumns
end
end
diff --git a/db/migrate/20180315160435_add_external_auth_mutual_tls_fields_to_project_settings.rb b/db/migrate/20180315160435_add_external_auth_mutual_tls_fields_to_project_settings.rb
index c379d207ff0..dd40c5fdf4d 100644
--- a/db/migrate/20180315160435_add_external_auth_mutual_tls_fields_to_project_settings.rb
+++ b/db/migrate/20180315160435_add_external_auth_mutual_tls_fields_to_project_settings.rb
@@ -1,8 +1,9 @@
class AddExternalAuthMutualTlsFieldsToProjectSettings < ActiveRecord::Migration[4.2]
DOWNTIME = false
+ # rubocop:disable Migration/PreventStrings
+ # rubocop:disable Migration/AddLimitToTextColumns
def change
- # rubocop:disable Migration/AddLimitToStringColumns
add_column :application_settings,
:external_auth_client_cert, :text
add_column :application_settings,
@@ -13,6 +14,7 @@ class AddExternalAuthMutualTlsFieldsToProjectSettings < ActiveRecord::Migration[
:encrypted_external_auth_client_key_pass, :string
add_column :application_settings,
:encrypted_external_auth_client_key_pass_iv, :string
- # rubocop:enable Migration/AddLimitToStringColumns
end
+ # rubocop:enable Migration/AddLimitToTextColumns
+ # rubocop:enable Migration/PreventStrings
end
diff --git a/db/migrate/20180319190020_create_deploy_tokens.rb b/db/migrate/20180319190020_create_deploy_tokens.rb
index f444521b3ae..019543cb2f1 100644
--- a/db/migrate/20180319190020_create_deploy_tokens.rb
+++ b/db/migrate/20180319190020_create_deploy_tokens.rb
@@ -1,8 +1,8 @@
class CreateDeployTokens < ActiveRecord::Migration[4.2]
DOWNTIME = false
+ # rubocop:disable Migration/PreventStrings
def change
- # rubocop:disable Migration/AddLimitToStringColumns
create_table :deploy_tokens do |t|
t.boolean :revoked, default: false
t.boolean :read_repository, null: false, default: false
@@ -16,6 +16,6 @@ class CreateDeployTokens < ActiveRecord::Migration[4.2]
t.index [:token, :expires_at, :id], where: "(revoked IS FALSE)"
end
- # rubocop:enable Migration/AddLimitToStringColumns
end
+ # rubocop:enable Migration/PreventStrings
end
diff --git a/db/migrate/20180424134533_create_application_setting_terms.rb b/db/migrate/20180424134533_create_application_setting_terms.rb
index 8741f20daeb..41edb8a9559 100644
--- a/db/migrate/20180424134533_create_application_setting_terms.rb
+++ b/db/migrate/20180424134533_create_application_setting_terms.rb
@@ -3,6 +3,7 @@ class CreateApplicationSettingTerms < ActiveRecord::Migration[4.2]
DOWNTIME = false
+ # rubocop:disable Migration/AddLimitToTextColumns
def change
create_table :application_setting_terms do |t|
t.integer :cached_markdown_version
@@ -10,4 +11,5 @@ class CreateApplicationSettingTerms < ActiveRecord::Migration[4.2]
t.text :terms_html
end
end
+ # rubocop:enable Migration/AddLimitToTextColumns
end
diff --git a/db/migrate/20180502122856_create_project_mirror_data.rb b/db/migrate/20180502122856_create_project_mirror_data.rb
index 04367e1c98b..1ecfd8926fb 100644
--- a/db/migrate/20180502122856_create_project_mirror_data.rb
+++ b/db/migrate/20180502122856_create_project_mirror_data.rb
@@ -3,7 +3,8 @@ class CreateProjectMirrorData < ActiveRecord::Migration[4.2]
DOWNTIME = false
- # rubocop:disable Migration/AddLimitToStringColumns
+ # rubocop:disable Migration/PreventStrings
+ # rubocop:disable Migration/AddLimitToTextColumns
def up
if table_exists?(:project_mirror_data)
add_column :project_mirror_data, :status, :string unless column_exists?(:project_mirror_data, :status)
@@ -18,7 +19,8 @@ class CreateProjectMirrorData < ActiveRecord::Migration[4.2]
end
end
end
- # rubocop:enable Migration/AddLimitToStringColumns
+ # rubocop:enable Migration/AddLimitToTextColumns
+ # rubocop:enable Migration/PreventStrings
def down
remove_column :project_mirror_data, :status
diff --git a/db/migrate/20180503131624_create_remote_mirrors.rb b/db/migrate/20180503131624_create_remote_mirrors.rb
index 80090c14314..330a0d8f62d 100644
--- a/db/migrate/20180503131624_create_remote_mirrors.rb
+++ b/db/migrate/20180503131624_create_remote_mirrors.rb
@@ -5,7 +5,7 @@ class CreateRemoteMirrors < ActiveRecord::Migration[4.2]
disable_ddl_transaction!
- # rubocop:disable Migration/AddLimitToStringColumns
+ # rubocop:disable Migration/PreventStrings
def up
return if table_exists?(:remote_mirrors)
@@ -20,7 +20,7 @@ class CreateRemoteMirrors < ActiveRecord::Migration[4.2]
t.string :last_error
t.boolean :only_protected_branches, default: false, null: false
t.string :remote_name
- t.text :encrypted_credentials
+ t.text :encrypted_credentials # rubocop:disable Migration/AddLimitToTextColumns
t.string :encrypted_credentials_iv
t.string :encrypted_credentials_salt
@@ -28,7 +28,7 @@ class CreateRemoteMirrors < ActiveRecord::Migration[4.2]
t.timestamps null: false
end
end
- # rubocop:enable Migration/AddLimitToStringColumns
+ # rubocop:enable Migration/PreventStrings
def down
# ee/db/migrate/20160321161032_create_remote_mirrors_ee.rb will remove the table
diff --git a/db/migrate/20180503175053_ensure_missing_columns_to_project_mirror_data.rb b/db/migrate/20180503175053_ensure_missing_columns_to_project_mirror_data.rb
index f00493ed515..12ebac47850 100644
--- a/db/migrate/20180503175053_ensure_missing_columns_to_project_mirror_data.rb
+++ b/db/migrate/20180503175053_ensure_missing_columns_to_project_mirror_data.rb
@@ -3,11 +3,15 @@ class EnsureMissingColumnsToProjectMirrorData < ActiveRecord::Migration[4.2]
DOWNTIME = false
+ # rubocop:disable Migration/AddLimitToTextColumns
+ # rubocop:disable Migration/PreventStrings
def up
- add_column :project_mirror_data, :status, :string unless column_exists?(:project_mirror_data, :status) # rubocop:disable Migration/AddLimitToStringColumns
- add_column :project_mirror_data, :jid, :string unless column_exists?(:project_mirror_data, :jid) # rubocop:disable Migration/AddLimitToStringColumns
+ add_column :project_mirror_data, :status, :string unless column_exists?(:project_mirror_data, :status)
+ add_column :project_mirror_data, :jid, :string unless column_exists?(:project_mirror_data, :jid)
add_column :project_mirror_data, :last_error, :text unless column_exists?(:project_mirror_data, :last_error)
end
+ # rubocop:enable Migration/PreventStrings
+ # rubocop:enable Migration/AddLimitToTextColumns
def down
# db/migrate/20180502122856_create_project_mirror_data.rb will remove the table
diff --git a/db/migrate/20180503193953_add_mirror_available_to_application_settings.rb b/db/migrate/20180503193953_add_mirror_available_to_application_settings.rb
index d6a04035d48..117887a8bc2 100644
--- a/db/migrate/20180503193953_add_mirror_available_to_application_settings.rb
+++ b/db/migrate/20180503193953_add_mirror_available_to_application_settings.rb
@@ -6,7 +6,9 @@ class AddMirrorAvailableToApplicationSettings < ActiveRecord::Migration[4.2]
disable_ddl_transaction!
def up
+ # rubocop:disable Migration/AddColumnWithDefault
add_column_with_default(:application_settings, :mirror_available, :boolean, default: true, allow_null: false) unless column_exists?(:application_settings, :mirror_available)
+ # rubocop:enable Migration/AddColumnWithDefault
end
def down
diff --git a/db/migrate/20180511131058_create_clusters_applications_jupyter.rb b/db/migrate/20180511131058_create_clusters_applications_jupyter.rb
index 4633d930e2d..908a6113f98 100644
--- a/db/migrate/20180511131058_create_clusters_applications_jupyter.rb
+++ b/db/migrate/20180511131058_create_clusters_applications_jupyter.rb
@@ -6,20 +6,20 @@ class CreateClustersApplicationsJupyter < ActiveRecord::Migration[4.2]
DOWNTIME = false
+ # rubocop:disable Migration/PreventStrings
def change
- # rubocop:disable Migration/AddLimitToStringColumns
create_table :clusters_applications_jupyter do |t|
t.references :cluster, null: false, unique: true, foreign_key: { on_delete: :cascade }
t.references :oauth_application, foreign_key: { on_delete: :nullify }
t.integer :status, null: false
- t.string :version, null: false # rubocop:disable Migration/AddLimitToStringColumns
- t.string :hostname # rubocop:disable Migration/AddLimitToStringColumns
+ t.string :version, null: false
+ t.string :hostname
t.timestamps_with_timezone null: false
- t.text :status_reason
+ t.text :status_reason # rubocop:disable Migration/AddLimitToTextColumns
end
- # rubocop:enable Migration/AddLimitToStringColumns
end
+ # rubocop:enable Migration/PreventStrings
end
diff --git a/db/migrate/20180515121227_create_notes_diff_files.rb b/db/migrate/20180515121227_create_notes_diff_files.rb
index 5f6dba11ff9..1797a86c679 100644
--- a/db/migrate/20180515121227_create_notes_diff_files.rb
+++ b/db/migrate/20180515121227_create_notes_diff_files.rb
@@ -3,8 +3,9 @@ class CreateNotesDiffFiles < ActiveRecord::Migration[4.2]
disable_ddl_transaction!
+ # rubocop:disable Migration/AddLimitToTextColumns
+ # rubocop:disable Migration/PreventStrings
def change
- # rubocop:disable Migration/AddLimitToStringColumns
create_table :note_diff_files do |t|
t.references :diff_note, references: :notes, null: false, index: { unique: true }
t.text :diff, null: false
@@ -19,6 +20,8 @@ class CreateNotesDiffFiles < ActiveRecord::Migration[4.2]
# rubocop:disable Migration/AddConcurrentForeignKey
add_foreign_key :note_diff_files, :notes, column: :diff_note_id, on_delete: :cascade
- # rubocop:enable Migration/AddLimitToStringColumns
+ # rubocop:enable Migration/AddConcurrentForeignKey
end
+ # rubocop:enable Migration/PreventStrings
+ # rubocop:enable Migration/AddLimitToTextColumns
end
diff --git a/db/migrate/20180529093006_ensure_remote_mirror_columns.rb b/db/migrate/20180529093006_ensure_remote_mirror_columns.rb
index a0a1150f022..8cf636e1da6 100644
--- a/db/migrate/20180529093006_ensure_remote_mirror_columns.rb
+++ b/db/migrate/20180529093006_ensure_remote_mirror_columns.rb
@@ -5,19 +5,22 @@ class EnsureRemoteMirrorColumns < ActiveRecord::Migration[4.2]
disable_ddl_transaction!
+ # rubocop:disable Migration/Datetime
+ # rubocop:disable Migration/PreventStrings
def up
- # rubocop:disable Migration/Datetime
add_column :remote_mirrors, :last_update_started_at, :datetime unless column_exists?(:remote_mirrors, :last_update_started_at)
- add_column :remote_mirrors, :remote_name, :string unless column_exists?(:remote_mirrors, :remote_name) # rubocop:disable Migration/AddLimitToStringColumns
+ add_column :remote_mirrors, :remote_name, :string unless column_exists?(:remote_mirrors, :remote_name)
unless column_exists?(:remote_mirrors, :only_protected_branches)
- add_column_with_default(:remote_mirrors,
+ add_column_with_default(:remote_mirrors, # rubocop:disable Migration/AddColumnWithDefault
:only_protected_branches,
:boolean,
default: false,
allow_null: false)
end
end
+ # rubocop:enable Migration/PreventStrings
+ # rubocop:enable Migration/Datetime
def down
# db/migrate/20180503131624_create_remote_mirrors.rb will remove the table
diff --git a/db/migrate/20180531185349_add_repository_languages.rb b/db/migrate/20180531185349_add_repository_languages.rb
index d517c21c26c..08871e8b6f0 100644
--- a/db/migrate/20180531185349_add_repository_languages.rb
+++ b/db/migrate/20180531185349_add_repository_languages.rb
@@ -3,8 +3,8 @@ class AddRepositoryLanguages < ActiveRecord::Migration[4.2]
DOWNTIME = false
+ # rubocop:disable Migration/PreventStrings
def up
- # rubocop:disable Migration/AddLimitToStringColumns
create_table(:programming_languages) do |t|
t.string :name, null: false
t.string :color, null: false
@@ -20,8 +20,8 @@ class AddRepositoryLanguages < ActiveRecord::Migration[4.2]
add_index :programming_languages, :name, unique: true
add_index :repository_languages, [:project_id, :programming_language_id],
unique: true, name: "index_repository_languages_on_project_and_languages_id"
- # rubocop:enable Migration/AddLimitToStringColumns
end
+ # rubocop:enable Migration/PreventStrings
def down
drop_table :repository_languages
diff --git a/db/migrate/20180601213245_add_deploy_strategy_to_project_auto_devops.rb b/db/migrate/20180601213245_add_deploy_strategy_to_project_auto_devops.rb
index 78a3617ec93..67d20b949d9 100644
--- a/db/migrate/20180601213245_add_deploy_strategy_to_project_auto_devops.rb
+++ b/db/migrate/20180601213245_add_deploy_strategy_to_project_auto_devops.rb
@@ -10,7 +10,7 @@ class AddDeployStrategyToProjectAutoDevops < ActiveRecord::Migration[4.2]
disable_ddl_transaction!
def up
- add_column_with_default :project_auto_devops, :deploy_strategy, :integer, default: 0, allow_null: false
+ add_column_with_default :project_auto_devops, :deploy_strategy, :integer, default: 0, allow_null: false # rubocop:disable Migration/AddColumnWithDefault
end
def down
diff --git a/db/migrate/20180607071808_add_push_events_branch_filter_to_web_hooks.rb b/db/migrate/20180607071808_add_push_events_branch_filter_to_web_hooks.rb
index e6a1daaffc2..42bb0a784bb 100644
--- a/db/migrate/20180607071808_add_push_events_branch_filter_to_web_hooks.rb
+++ b/db/migrate/20180607071808_add_push_events_branch_filter_to_web_hooks.rb
@@ -7,6 +7,6 @@ class AddPushEventsBranchFilterToWebHooks < ActiveRecord::Migration[4.2]
DOWNTIME = false
def change
- add_column :web_hooks, :push_events_branch_filter, :text
+ add_column :web_hooks, :push_events_branch_filter, :text # rubocop:disable Migration/AddLimitToTextColumns
end
end
diff --git a/db/migrate/20180612103626_add_columns_for_helm_tiller_certificates.rb b/db/migrate/20180612103626_add_columns_for_helm_tiller_certificates.rb
index d7273dff48e..515aab45145 100644
--- a/db/migrate/20180612103626_add_columns_for_helm_tiller_certificates.rb
+++ b/db/migrate/20180612103626_add_columns_for_helm_tiller_certificates.rb
@@ -4,9 +4,11 @@ class AddColumnsForHelmTillerCertificates < ActiveRecord::Migration[4.2]
DOWNTIME = false
+ # rubocop:disable Migration/AddLimitToTextColumns
def change
add_column :clusters_applications_helm, :encrypted_ca_key, :text
add_column :clusters_applications_helm, :encrypted_ca_key_iv, :text
add_column :clusters_applications_helm, :ca_cert, :text
end
+ # rubocop:enable Migration/AddLimitToTextColumns
end
diff --git a/db/migrate/20180613081317_create_ci_builds_runner_session.rb b/db/migrate/20180613081317_create_ci_builds_runner_session.rb
index 68af38834d2..29e133de1b7 100644
--- a/db/migrate/20180613081317_create_ci_builds_runner_session.rb
+++ b/db/migrate/20180613081317_create_ci_builds_runner_session.rb
@@ -7,8 +7,8 @@ class CreateCiBuildsRunnerSession < ActiveRecord::Migration[4.2]
# Set this constant to true if this migration requires downtime.
DOWNTIME = false
+ # rubocop:disable Migration/PreventStrings
def change
- # rubocop:disable Migration/AddLimitToStringColumns
create_table :ci_builds_runner_session, id: :bigserial do |t|
t.integer :build_id, null: false
t.string :url, null: false
@@ -18,6 +18,6 @@ class CreateCiBuildsRunnerSession < ActiveRecord::Migration[4.2]
t.foreign_key :ci_builds, column: :build_id, on_delete: :cascade
t.index :build_id, unique: true
end
- # rubocop:enable Migration/AddLimitToStringColumns
end
+ # rubocop:enable Migration/PreventStrings
end
diff --git a/db/migrate/20180625113853_create_import_export_uploads.rb b/db/migrate/20180625113853_create_import_export_uploads.rb
index d76b3e8cc15..2fe5a1e7f6b 100644
--- a/db/migrate/20180625113853_create_import_export_uploads.rb
+++ b/db/migrate/20180625113853_create_import_export_uploads.rb
@@ -1,6 +1,7 @@
class CreateImportExportUploads < ActiveRecord::Migration[4.2]
DOWNTIME = false
+ # rubocop:disable Migration/AddLimitToTextColumns
def change
create_table :import_export_uploads do |t|
t.datetime_with_timezone :updated_at, null: false
@@ -13,4 +14,5 @@ class CreateImportExportUploads < ActiveRecord::Migration[4.2]
add_index :import_export_uploads, :updated_at
end
+ # rubocop:enable Migration/AddLimitToTextColumns
end
diff --git a/db/migrate/20180704204006_add_hide_third_party_offers_to_application_settings.rb b/db/migrate/20180704204006_add_hide_third_party_offers_to_application_settings.rb
index 03afbe217b5..b037f72a964 100644
--- a/db/migrate/20180704204006_add_hide_third_party_offers_to_application_settings.rb
+++ b/db/migrate/20180704204006_add_hide_third_party_offers_to_application_settings.rb
@@ -6,7 +6,7 @@ class AddHideThirdPartyOffersToApplicationSettings < ActiveRecord::Migration[4.2
disable_ddl_transaction!
def up
- add_column_with_default(:application_settings, :hide_third_party_offers,
+ add_column_with_default(:application_settings, :hide_third_party_offers, # rubocop:disable Migration/AddColumnWithDefault
:boolean,
default: false,
allow_null: false)
diff --git a/db/migrate/20180713092803_create_user_statuses.rb b/db/migrate/20180713092803_create_user_statuses.rb
index 3abab4e45a9..79a12c9b8ee 100644
--- a/db/migrate/20180713092803_create_user_statuses.rb
+++ b/db/migrate/20180713092803_create_user_statuses.rb
@@ -5,8 +5,8 @@ class CreateUserStatuses < ActiveRecord::Migration[4.2]
DOWNTIME = false
+ # rubocop:disable Migration/PreventStrings
def change
- # rubocop:disable Migration/AddLimitToStringColumns
create_table :user_statuses, id: false, primary_key: :user_id do |t|
t.references :user,
foreign_key: { on_delete: :cascade },
@@ -17,6 +17,6 @@ class CreateUserStatuses < ActiveRecord::Migration[4.2]
t.string :message, limit: 100
t.string :message_html
end
- # rubocop:enable Migration/AddLimitToStringColumns
end
+ # rubocop:enable Migration/PreventStrings
end
diff --git a/db/migrate/20180718005113_add_instance_statistics_visibility_to_application_setting.rb b/db/migrate/20180718005113_add_instance_statistics_visibility_to_application_setting.rb
index ed5fa58b481..9e453be8c57 100644
--- a/db/migrate/20180718005113_add_instance_statistics_visibility_to_application_setting.rb
+++ b/db/migrate/20180718005113_add_instance_statistics_visibility_to_application_setting.rb
@@ -8,7 +8,7 @@ class AddInstanceStatisticsVisibilityToApplicationSetting < ActiveRecord::Migrat
disable_ddl_transaction!
def up
- add_column_with_default(:application_settings, :instance_statistics_visibility_private,
+ add_column_with_default(:application_settings, :instance_statistics_visibility_private, # rubocop:disable Migration/AddColumnWithDefault
:boolean,
default: false,
allow_null: false)
diff --git a/db/migrate/20180723135214_add_web_ide_client_side_preview_enabled_to_application_settings.rb b/db/migrate/20180723135214_add_web_ide_client_side_preview_enabled_to_application_settings.rb
index 23b8e04674a..96b5c43d0a3 100644
--- a/db/migrate/20180723135214_add_web_ide_client_side_preview_enabled_to_application_settings.rb
+++ b/db/migrate/20180723135214_add_web_ide_client_side_preview_enabled_to_application_settings.rb
@@ -8,7 +8,7 @@ class AddWebIdeClientSidePreviewEnabledToApplicationSettings < ActiveRecord::Mig
disable_ddl_transaction!
def up
- add_column_with_default(:application_settings, :web_ide_clientside_preview_enabled,
+ add_column_with_default(:application_settings, :web_ide_clientside_preview_enabled, # rubocop:disable Migration/AddColumnWithDefault
:boolean,
default: false,
allow_null: false)
diff --git a/db/migrate/20180808162000_add_user_show_add_ssh_key_message_to_application_settings.rb b/db/migrate/20180808162000_add_user_show_add_ssh_key_message_to_application_settings.rb
index 946d99a4c5f..6bf54f79644 100644
--- a/db/migrate/20180808162000_add_user_show_add_ssh_key_message_to_application_settings.rb
+++ b/db/migrate/20180808162000_add_user_show_add_ssh_key_message_to_application_settings.rb
@@ -10,7 +10,9 @@ class AddUserShowAddSshKeyMessageToApplicationSettings < ActiveRecord::Migration
disable_ddl_transaction!
def up
+ # rubocop:disable Migration/AddColumnWithDefault
add_column_with_default :application_settings, :user_show_add_ssh_key_message, :boolean, default: true, allow_null: false
+ # rubocop:enable Migration/AddColumnWithDefault
end
def down
diff --git a/db/migrate/20180814153625_add_commit_email_to_users.rb b/db/migrate/20180814153625_add_commit_email_to_users.rb
index 303be9b2312..c94ee9512de 100644
--- a/db/migrate/20180814153625_add_commit_email_to_users.rb
+++ b/db/migrate/20180814153625_add_commit_email_to_users.rb
@@ -27,11 +27,11 @@ class AddCommitEmailToUsers < ActiveRecord::Migration[4.2]
# comments:
# disable_ddl_transaction!
- # rubocop:disable Migration/AddLimitToStringColumns
+ # rubocop:disable Migration/PreventStrings
# rubocop:disable Migration/AddColumnsToWideTables
def change
add_column :users, :commit_email, :string
end
- # rubocop:enable Migration/AddLimitToStringColumns
# rubocop:enable Migration/AddColumnsToWideTables
+ # rubocop:enable Migration/PreventStrings
end
diff --git a/db/migrate/20180824202952_add_outbound_requests_whitelist_to_application_settings.rb b/db/migrate/20180824202952_add_outbound_requests_whitelist_to_application_settings.rb
index 4ee654ce873..7b87b04bc9d 100644
--- a/db/migrate/20180824202952_add_outbound_requests_whitelist_to_application_settings.rb
+++ b/db/migrate/20180824202952_add_outbound_requests_whitelist_to_application_settings.rb
@@ -3,7 +3,9 @@
class AddOutboundRequestsWhitelistToApplicationSettings < ActiveRecord::Migration[5.1]
DOWNTIME = false
+ # rubocop:disable Migration/PreventStrings
def change
add_column :application_settings, :outbound_local_requests_whitelist, :string, array: true, limit: 255
end
+ # rubocop:enable Migration/PreventStrings
end
diff --git a/db/migrate/20180831164905_add_common_to_prometheus_metrics.rb b/db/migrate/20180831164905_add_common_to_prometheus_metrics.rb
index 5eb77d0480d..6654e6d1957 100644
--- a/db/migrate/20180831164905_add_common_to_prometheus_metrics.rb
+++ b/db/migrate/20180831164905_add_common_to_prometheus_metrics.rb
@@ -8,7 +8,7 @@ class AddCommonToPrometheusMetrics < ActiveRecord::Migration[4.2]
disable_ddl_transaction!
def up
- add_column_with_default(:prometheus_metrics, :common, :boolean, default: false)
+ add_column_with_default(:prometheus_metrics, :common, :boolean, default: false) # rubocop:disable Migration/AddColumnWithDefault
end
def down
diff --git a/db/migrate/20180831164908_add_identifier_to_prometheus_metric.rb b/db/migrate/20180831164908_add_identifier_to_prometheus_metric.rb
index 8f30363c310..efdcab53920 100644
--- a/db/migrate/20180831164908_add_identifier_to_prometheus_metric.rb
+++ b/db/migrate/20180831164908_add_identifier_to_prometheus_metric.rb
@@ -6,6 +6,6 @@ class AddIdentifierToPrometheusMetric < ActiveRecord::Migration[4.2]
DOWNTIME = false
def change
- add_column :prometheus_metrics, :identifier, :string # rubocop:disable Migration/AddLimitToStringColumns
+ add_column :prometheus_metrics, :identifier, :string # rubocop:disable Migration/PreventStrings
end
end
diff --git a/db/migrate/20180901200537_add_resource_label_event_reference_fields.rb b/db/migrate/20180901200537_add_resource_label_event_reference_fields.rb
index c8875e16914..2dc86e1ac9a 100644
--- a/db/migrate/20180901200537_add_resource_label_event_reference_fields.rb
+++ b/db/migrate/20180901200537_add_resource_label_event_reference_fields.rb
@@ -3,9 +3,11 @@
class AddResourceLabelEventReferenceFields < ActiveRecord::Migration[4.2]
DOWNTIME = false
+ # rubocop:disable Migration/AddLimitToTextColumns
def change
add_column :resource_label_events, :cached_markdown_version, :integer
add_column :resource_label_events, :reference, :text
add_column :resource_label_events, :reference_html, :text
end
+ # rubocop:enable Migration/AddLimitToTextColumns
end
diff --git a/db/migrate/20180907015926_add_legacy_abac_to_cluster_providers_gcp.rb b/db/migrate/20180907015926_add_legacy_abac_to_cluster_providers_gcp.rb
index c57611a0f7d..8bfb0c5612a 100644
--- a/db/migrate/20180907015926_add_legacy_abac_to_cluster_providers_gcp.rb
+++ b/db/migrate/20180907015926_add_legacy_abac_to_cluster_providers_gcp.rb
@@ -8,7 +8,7 @@ class AddLegacyAbacToClusterProvidersGcp < ActiveRecord::Migration[4.2]
disable_ddl_transaction!
def up
- add_column_with_default(:cluster_providers_gcp, :legacy_abac, :boolean, default: true)
+ add_column_with_default(:cluster_providers_gcp, :legacy_abac, :boolean, default: true) # rubocop:disable Migration/AddColumnWithDefault
end
def down
diff --git a/db/migrate/20180910115836_add_attr_encrypted_columns_to_web_hook.rb b/db/migrate/20180910115836_add_attr_encrypted_columns_to_web_hook.rb
index 9757f7fdc79..3341fd5c593 100644
--- a/db/migrate/20180910115836_add_attr_encrypted_columns_to_web_hook.rb
+++ b/db/migrate/20180910115836_add_attr_encrypted_columns_to_web_hook.rb
@@ -5,13 +5,13 @@ class AddAttrEncryptedColumnsToWebHook < ActiveRecord::Migration[4.2]
DOWNTIME = false
+ # rubocop:disable Migration/PreventStrings
def change
- # rubocop:disable Migration/AddLimitToStringColumns
add_column :web_hooks, :encrypted_token, :string
add_column :web_hooks, :encrypted_token_iv, :string
add_column :web_hooks, :encrypted_url, :string
add_column :web_hooks, :encrypted_url_iv, :string
- # rubocop:enable Migration/AddLimitToStringColumns
end
+ # rubocop:enable Migration/PreventStrings
end
diff --git a/db/migrate/20180910153412_add_token_digest_to_personal_access_tokens.rb b/db/migrate/20180910153412_add_token_digest_to_personal_access_tokens.rb
index 52923f52499..4ed31bf7f39 100644
--- a/db/migrate/20180910153412_add_token_digest_to_personal_access_tokens.rb
+++ b/db/migrate/20180910153412_add_token_digest_to_personal_access_tokens.rb
@@ -8,7 +8,7 @@ class AddTokenDigestToPersonalAccessTokens < ActiveRecord::Migration[4.2]
def up
change_column :personal_access_tokens, :token, :string, null: true
- add_column :personal_access_tokens, :token_digest, :string # rubocop:disable Migration/AddLimitToStringColumns
+ add_column :personal_access_tokens, :token_digest, :string # rubocop:disable Migration/PreventStrings
end
def down
diff --git a/db/migrate/20180912111628_add_knative_application.rb b/db/migrate/20180912111628_add_knative_application.rb
index 7c55de02d1c..cd2512cc17a 100644
--- a/db/migrate/20180912111628_add_knative_application.rb
+++ b/db/migrate/20180912111628_add_knative_application.rb
@@ -5,8 +5,8 @@ class AddKnativeApplication < ActiveRecord::Migration[4.2]
DOWNTIME = false
+ # rubocop:disable Migration/PreventStrings
def change
- # rubocop:disable Migration/AddLimitToStringColumns
create_table "clusters_applications_knative" do |t|
t.references :cluster, null: false, unique: true, foreign_key: { on_delete: :cascade }
@@ -15,8 +15,8 @@ class AddKnativeApplication < ActiveRecord::Migration[4.2]
t.integer "status", null: false
t.string "version", null: false
t.string "hostname"
- t.text "status_reason"
+ t.text "status_reason" # rubocop:disable Migration/AddLimitToTextColumns
end
- # rubocop:enable Migration/AddLimitToStringColumns
end
+ # rubocop:enable Migration/PreventStrings
end
diff --git a/db/migrate/20180924141949_add_diff_max_patch_bytes_to_application_settings.rb b/db/migrate/20180924141949_add_diff_max_patch_bytes_to_application_settings.rb
index 5dac5f0d100..69166f8d1a4 100644
--- a/db/migrate/20180924141949_add_diff_max_patch_bytes_to_application_settings.rb
+++ b/db/migrate/20180924141949_add_diff_max_patch_bytes_to_application_settings.rb
@@ -12,7 +12,7 @@ class AddDiffMaxPatchBytesToApplicationSettings < ActiveRecord::Migration[4.2]
disable_ddl_transaction!
def up
- add_column_with_default(:application_settings,
+ add_column_with_default(:application_settings, # rubocop:disable Migration/AddColumnWithDefault
:diff_max_patch_bytes,
:integer,
default: 100.kilobytes,
diff --git a/db/migrate/20181009190428_create_clusters_kubernetes_namespaces.rb b/db/migrate/20181009190428_create_clusters_kubernetes_namespaces.rb
index b6ffb2866aa..42d0899fe31 100644
--- a/db/migrate/20181009190428_create_clusters_kubernetes_namespaces.rb
+++ b/db/migrate/20181009190428_create_clusters_kubernetes_namespaces.rb
@@ -4,8 +4,8 @@ class CreateClustersKubernetesNamespaces < ActiveRecord::Migration[4.2]
DOWNTIME = false
INDEX_NAME = 'kubernetes_namespaces_cluster_and_namespace'
+ # rubocop:disable Migration/PreventStrings
def change
- # rubocop:disable Migration/AddLimitToStringColumns
create_table :clusters_kubernetes_namespaces, id: :bigserial do |t|
t.references :cluster, null: false, index: true, foreign_key: { on_delete: :cascade }
t.references :project, index: true, foreign_key: { on_delete: :nullify }
@@ -17,10 +17,10 @@ class CreateClustersKubernetesNamespaces < ActiveRecord::Migration[4.2]
t.string :namespace, null: false
t.string :service_account_name
- t.text :encrypted_service_account_token
+ t.text :encrypted_service_account_token # rubocop:disable Migration/AddLimitToTextColumns
t.index [:cluster_id, :namespace], name: INDEX_NAME, unique: true
end
- # rubocop:enable Migration/AddLimitToStringColumns
end
+ # rubocop:enable Migration/PreventStrings
end
diff --git a/db/migrate/20181017001059_add_cluster_type_to_clusters.rb b/db/migrate/20181017001059_add_cluster_type_to_clusters.rb
index d032afe1a43..75abcfedfc9 100644
--- a/db/migrate/20181017001059_add_cluster_type_to_clusters.rb
+++ b/db/migrate/20181017001059_add_cluster_type_to_clusters.rb
@@ -9,7 +9,7 @@ class AddClusterTypeToClusters < ActiveRecord::Migration[4.2]
disable_ddl_transaction!
def up
- add_column_with_default(:clusters, :cluster_type, :smallint, default: PROJECT_CLUSTER_TYPE)
+ add_column_with_default(:clusters, :cluster_type, :smallint, default: PROJECT_CLUSTER_TYPE) # rubocop:disable Migration/AddColumnWithDefault
end
def down
diff --git a/db/migrate/20181019032400_add_shards_table.rb b/db/migrate/20181019032400_add_shards_table.rb
index 82287e5c3b5..713039c1a15 100644
--- a/db/migrate/20181019032400_add_shards_table.rb
+++ b/db/migrate/20181019032400_add_shards_table.rb
@@ -5,7 +5,7 @@ class AddShardsTable < ActiveRecord::Migration[4.2]
def change
create_table :shards do |t|
- t.string :name, null: false, index: { unique: true } # rubocop:disable Migration/AddLimitToStringColumns
+ t.string :name, null: false, index: { unique: true } # rubocop:disable Migration/PreventStrings
end
end
end
diff --git a/db/migrate/20181019032408_add_repositories_table.rb b/db/migrate/20181019032408_add_repositories_table.rb
index 5ee31b37b66..d4d3acf7029 100644
--- a/db/migrate/20181019032408_add_repositories_table.rb
+++ b/db/migrate/20181019032408_add_repositories_table.rb
@@ -6,7 +6,7 @@ class AddRepositoriesTable < ActiveRecord::Migration[4.2]
def change
create_table :repositories, id: :bigserial do |t|
t.references :shard, null: false, index: true, foreign_key: { on_delete: :restrict }
- t.string :disk_path, null: false, index: { unique: true } # rubocop:disable Migration/AddLimitToStringColumns
+ t.string :disk_path, null: false, index: { unique: true } # rubocop:disable Migration/PreventStrings
end
add_column :projects, :pool_repository_id, :bigint # rubocop:disable Migration/AddColumnsToWideTables
diff --git a/db/migrate/20181025115728_add_private_commit_email_hostname_to_application_settings.rb b/db/migrate/20181025115728_add_private_commit_email_hostname_to_application_settings.rb
index c0e4897b8d7..0923975f43a 100644
--- a/db/migrate/20181025115728_add_private_commit_email_hostname_to_application_settings.rb
+++ b/db/migrate/20181025115728_add_private_commit_email_hostname_to_application_settings.rb
@@ -5,7 +5,9 @@ class AddPrivateCommitEmailHostnameToApplicationSettings < ActiveRecord::Migrati
DOWNTIME = false
+ # rubocop:disable Migration/PreventStrings
def change
- add_column(:application_settings, :commit_email_hostname, :string, null: true) # rubocop:disable Migration/AddLimitToStringColumns
+ add_column(:application_settings, :commit_email_hostname, :string, null: true)
end
+ # rubocop:enable Migration/PreventStrings
end
diff --git a/db/migrate/20181028120717_add_first_day_of_week_to_application_settings.rb b/db/migrate/20181028120717_add_first_day_of_week_to_application_settings.rb
index e9a8c1011ad..c4c25d08754 100644
--- a/db/migrate/20181028120717_add_first_day_of_week_to_application_settings.rb
+++ b/db/migrate/20181028120717_add_first_day_of_week_to_application_settings.rb
@@ -7,7 +7,7 @@ class AddFirstDayOfWeekToApplicationSettings < ActiveRecord::Migration[5.0]
DOWNTIME = false
def up
- add_column_with_default(:application_settings, :first_day_of_week, :integer, default: 0)
+ add_column_with_default(:application_settings, :first_day_of_week, :integer, default: 0) # rubocop:disable Migration/AddColumnWithDefault
end
def down
diff --git a/db/migrate/20181031145139_add_protected_ci_variables_to_application_settings.rb b/db/migrate/20181031145139_add_protected_ci_variables_to_application_settings.rb
index 85ee34afe1e..1817677c58d 100644
--- a/db/migrate/20181031145139_add_protected_ci_variables_to_application_settings.rb
+++ b/db/migrate/20181031145139_add_protected_ci_variables_to_application_settings.rb
@@ -8,7 +8,7 @@ class AddProtectedCiVariablesToApplicationSettings < ActiveRecord::Migration[5.0
disable_ddl_transaction!
def up
- add_column_with_default(:application_settings, :protected_ci_variables, :boolean, default: false, allow_null: false)
+ add_column_with_default(:application_settings, :protected_ci_variables, :boolean, default: false, allow_null: false) # rubocop:disable Migration/AddColumnWithDefault
end
def down
diff --git a/db/migrate/20181031190559_drop_gcp_clusters_table.rb b/db/migrate/20181031190559_drop_gcp_clusters_table.rb
index 850fa93c75a..597fe49f4c8 100644
--- a/db/migrate/20181031190559_drop_gcp_clusters_table.rb
+++ b/db/migrate/20181031190559_drop_gcp_clusters_table.rb
@@ -10,7 +10,6 @@ class DropGcpClustersTable < ActiveRecord::Migration[4.2]
end
def down
- # rubocop:disable Migration/AddLimitToStringColumns
create_table :gcp_clusters do |t|
# Order columns by best align scheme
t.references :project, null: false, index: { unique: true }, foreign_key: { on_delete: :cascade }
@@ -50,6 +49,5 @@ class DropGcpClustersTable < ActiveRecord::Migration[4.2]
t.text :encrypted_gcp_token
t.string :encrypted_gcp_token_iv
end
- # rubocop:enable Migration/AddLimitToStringColumns
end
end
diff --git a/db/migrate/20181101191341_create_clusters_applications_cert_manager.rb b/db/migrate/20181101191341_create_clusters_applications_cert_manager.rb
index 3bc20046311..95996531669 100644
--- a/db/migrate/20181101191341_create_clusters_applications_cert_manager.rb
+++ b/db/migrate/20181101191341_create_clusters_applications_cert_manager.rb
@@ -5,17 +5,17 @@ class CreateClustersApplicationsCertManager < ActiveRecord::Migration[4.2]
DOWNTIME = false
+ # rubocop:disable Migration/PreventStrings
def change
- # rubocop:disable Migration/AddLimitToStringColumns
create_table :clusters_applications_cert_managers do |t|
t.references :cluster, null: false, index: false, foreign_key: { on_delete: :cascade }
t.integer :status, null: false
t.string :version, null: false
t.string :email, null: false
t.timestamps_with_timezone null: false
- t.text :status_reason
+ t.text :status_reason # rubocop:disable Migration/AddLimitToTextColumns
t.index :cluster_id, unique: true
end
- # rubocop:enable Migration/AddLimitToStringColumns
end
+ # rubocop:enable Migration/PreventStrings
end
diff --git a/db/migrate/20181115140140_add_encrypted_runners_token_to_settings.rb b/db/migrate/20181115140140_add_encrypted_runners_token_to_settings.rb
index 124eedf7933..abb720dafb2 100644
--- a/db/migrate/20181115140140_add_encrypted_runners_token_to_settings.rb
+++ b/db/migrate/20181115140140_add_encrypted_runners_token_to_settings.rb
@@ -5,7 +5,9 @@ class AddEncryptedRunnersTokenToSettings < ActiveRecord::Migration[4.2]
DOWNTIME = false
+ # rubocop:disable Migration/PreventStrings
def change
- add_column :application_settings, :runners_registration_token_encrypted, :string # rubocop:disable Migration/AddLimitToStringColumns
+ add_column :application_settings, :runners_registration_token_encrypted, :string
end
+ # rubocop:enable Migration/PreventStrings
end
diff --git a/db/migrate/20181116050532_knative_external_ip.rb b/db/migrate/20181116050532_knative_external_ip.rb
index 4634b411108..4179e13c1b8 100644
--- a/db/migrate/20181116050532_knative_external_ip.rb
+++ b/db/migrate/20181116050532_knative_external_ip.rb
@@ -8,7 +8,9 @@ class KnativeExternalIp < ActiveRecord::Migration[4.2]
DOWNTIME = false
+ # rubocop:disable Migration/PreventStrings
def change
- add_column :clusters_applications_knative, :external_ip, :string # rubocop:disable Migration/AddLimitToStringColumns
+ add_column :clusters_applications_knative, :external_ip, :string
end
+ # rubocop:enable Migration/PreventStrings
end
diff --git a/db/migrate/20181116141415_add_encrypted_runners_token_to_namespaces.rb b/db/migrate/20181116141415_add_encrypted_runners_token_to_namespaces.rb
index 0a8ed912891..00c88140b52 100644
--- a/db/migrate/20181116141415_add_encrypted_runners_token_to_namespaces.rb
+++ b/db/migrate/20181116141415_add_encrypted_runners_token_to_namespaces.rb
@@ -5,7 +5,9 @@ class AddEncryptedRunnersTokenToNamespaces < ActiveRecord::Migration[4.2]
DOWNTIME = false
+ # rubocop:disable Migration/PreventStrings
def change
- add_column :namespaces, :runners_token_encrypted, :string # rubocop:disable Migration/AddLimitToStringColumns
+ add_column :namespaces, :runners_token_encrypted, :string
end
+ # rubocop:enable Migration/PreventStrings
end
diff --git a/db/migrate/20181116141504_add_encrypted_runners_token_to_projects.rb b/db/migrate/20181116141504_add_encrypted_runners_token_to_projects.rb
index f6986fcdfeb..f42239475f7 100644
--- a/db/migrate/20181116141504_add_encrypted_runners_token_to_projects.rb
+++ b/db/migrate/20181116141504_add_encrypted_runners_token_to_projects.rb
@@ -6,10 +6,10 @@ class AddEncryptedRunnersTokenToProjects < ActiveRecord::Migration[4.2]
DOWNTIME = false
# rubocop:disable Migration/AddColumnsToWideTables
- # rubocop:disable Migration/AddLimitToStringColumns
+ # rubocop:disable Migration/PreventStrings
def change
add_column :projects, :runners_token_encrypted, :string
end
+ # rubocop:enable Migration/PreventStrings
# rubocop:enable Migration/AddColumnsToWideTables
- # rubocop:enable Migration/AddLimitToStringColumns
end
diff --git a/db/migrate/20181120151656_add_token_encrypted_to_ci_runners.rb b/db/migrate/20181120151656_add_token_encrypted_to_ci_runners.rb
index 2270246dfb4..2b78d390907 100644
--- a/db/migrate/20181120151656_add_token_encrypted_to_ci_runners.rb
+++ b/db/migrate/20181120151656_add_token_encrypted_to_ci_runners.rb
@@ -6,6 +6,6 @@ class AddTokenEncryptedToCiRunners < ActiveRecord::Migration[4.2]
DOWNTIME = false
def change
- add_column :ci_runners, :token_encrypted, :string # rubocop:disable Migration/AddLimitToStringColumns
+ add_column :ci_runners, :token_encrypted, :string # rubocop:disable Migration/PreventStrings
end
end
diff --git a/db/migrate/20181122160027_create_project_repositories.rb b/db/migrate/20181122160027_create_project_repositories.rb
index 3f123daa150..9148cde2fb4 100644
--- a/db/migrate/20181122160027_create_project_repositories.rb
+++ b/db/migrate/20181122160027_create_project_repositories.rb
@@ -11,7 +11,7 @@ class CreateProjectRepositories < ActiveRecord::Migration[5.0]
def change
create_table :project_repositories, id: :bigserial do |t|
t.references :shard, null: false, index: true, foreign_key: { on_delete: :restrict }
- t.string :disk_path, null: false, index: { unique: true } # rubocop:disable Migration/AddLimitToStringColumns
+ t.string :disk_path, null: false, index: { unique: true } # rubocop:disable Migration/PreventStrings
t.references :project, null: false, index: { unique: true }, foreign_key: { on_delete: :cascade }
end
end
diff --git a/db/migrate/20181123144235_create_suggestions.rb b/db/migrate/20181123144235_create_suggestions.rb
index 78888517db5..b92e8ac8027 100644
--- a/db/migrate/20181123144235_create_suggestions.rb
+++ b/db/migrate/20181123144235_create_suggestions.rb
@@ -3,12 +3,14 @@
class CreateSuggestions < ActiveRecord::Migration[5.0]
DOWNTIME = false
+ # rubocop:disable Migration/PreventStrings
+ # rubocop:disable Migration/AddLimitToTextColumns
def change
create_table :suggestions, id: :bigserial do |t|
t.references :note, foreign_key: { on_delete: :cascade }, null: false
t.integer :relative_order, null: false, limit: 2
t.boolean :applied, null: false, default: false
- t.string :commit_id # rubocop:disable Migration/AddLimitToStringColumns
+ t.string :commit_id
t.text :from_content, null: false
t.text :to_content, null: false
@@ -17,4 +19,6 @@ class CreateSuggestions < ActiveRecord::Migration[5.0]
unique: true
end
end
+ # rubocop:enable Migration/AddLimitToTextColumns
+ # rubocop:enable Migration/PreventStrings
end
diff --git a/db/migrate/20181128123704_add_state_to_pool_repository.rb b/db/migrate/20181128123704_add_state_to_pool_repository.rb
index 04bbcf24f62..4d4e56390df 100644
--- a/db/migrate/20181128123704_add_state_to_pool_repository.rb
+++ b/db/migrate/20181128123704_add_state_to_pool_repository.rb
@@ -7,13 +7,17 @@ class AddStateToPoolRepository < ActiveRecord::Migration[5.0]
# Given the table is empty, and the non concurrent methods are chosen so
# the transactions don't have to be disabled
- # rubocop: disable Migration/AddConcurrentForeignKey, Migration/AddIndex
+ # rubocop:disable Migration/AddConcurrentForeignKey
+ # rubocop:disable Migration/AddIndex
+ # rubocop:disable Migration/PreventStrings
def change
- add_column(:pool_repositories, :state, :string, null: true) # rubocop:disable Migration/AddLimitToStringColumns
+ add_column(:pool_repositories, :state, :string, null: true)
add_column :pool_repositories, :source_project_id, :integer
add_index :pool_repositories, :source_project_id, unique: true
add_foreign_key :pool_repositories, :projects, column: :source_project_id, on_delete: :nullify
end
- # rubocop: enable Migration/AddConcurrentForeignKey, Migration/AddIndex
+ # rubocop:enable Migration/PreventStrings
+ # rubocop:enable Migration/AddIndex
+ # rubocop:enable Migration/AddConcurrentForeignKey
end
diff --git a/db/migrate/20181129104854_add_token_encrypted_to_ci_builds.rb b/db/migrate/20181129104854_add_token_encrypted_to_ci_builds.rb
index 097cc59bcdc..b83cabd6b75 100644
--- a/db/migrate/20181129104854_add_token_encrypted_to_ci_builds.rb
+++ b/db/migrate/20181129104854_add_token_encrypted_to_ci_builds.rb
@@ -6,10 +6,10 @@ class AddTokenEncryptedToCiBuilds < ActiveRecord::Migration[5.0]
DOWNTIME = false
# rubocop:disable Migration/AddColumnsToWideTables
- # rubocop:disable Migration/AddLimitToStringColumns
+ # rubocop:disable Migration/PreventStrings
def change
add_column :ci_builds, :token_encrypted, :string
end
+ # rubocop:enable Migration/PreventStrings
# rubocop:enable Migration/AddColumnsToWideTables
- # rubocop:enable Migration/AddLimitToStringColumns
end
diff --git a/db/migrate/20181203002526_add_project_bfg_object_map_column.rb b/db/migrate/20181203002526_add_project_bfg_object_map_column.rb
index d8c4fe1ecf6..7850f314ca8 100644
--- a/db/migrate/20181203002526_add_project_bfg_object_map_column.rb
+++ b/db/migrate/20181203002526_add_project_bfg_object_map_column.rb
@@ -4,10 +4,10 @@ class AddProjectBfgObjectMapColumn < ActiveRecord::Migration[5.0]
DOWNTIME = false
# rubocop:disable Migration/AddColumnsToWideTables
- # rubocop:disable Migration/AddLimitToStringColumns
+ # rubocop:disable Migration/PreventStrings
def change
add_column :projects, :bfg_object_map, :string
end
+ # rubocop:enable Migration/PreventStrings
# rubocop:enable Migration/AddColumnsToWideTables
- # rubocop:enable Migration/AddLimitToStringColumns
end
diff --git a/db/migrate/20181211092510_add_name_author_id_and_sha_to_releases.rb b/db/migrate/20181211092510_add_name_author_id_and_sha_to_releases.rb
index 3ab808ba667..79711d13cfe 100644
--- a/db/migrate/20181211092510_add_name_author_id_and_sha_to_releases.rb
+++ b/db/migrate/20181211092510_add_name_author_id_and_sha_to_releases.rb
@@ -5,9 +5,11 @@ class AddNameAuthorIdAndShaToReleases < ActiveRecord::Migration[5.0]
DOWNTIME = false
+ # rubocop:disable Migration/PreventStrings
def change
add_column :releases, :author_id, :integer
- add_column :releases, :name, :string # rubocop:disable Migration/AddLimitToStringColumns
- add_column :releases, :sha, :string # rubocop:disable Migration/AddLimitToStringColumns
+ add_column :releases, :name, :string
+ add_column :releases, :sha, :string
end
+ # rubocop:enable Migration/PreventStrings
end
diff --git a/db/migrate/20181212171634_create_error_tracking_settings.rb b/db/migrate/20181212171634_create_error_tracking_settings.rb
index 950b9a88005..567abe6f45f 100644
--- a/db/migrate/20181212171634_create_error_tracking_settings.rb
+++ b/db/migrate/20181212171634_create_error_tracking_settings.rb
@@ -5,8 +5,8 @@ class CreateErrorTrackingSettings < ActiveRecord::Migration[5.0]
DOWNTIME = false
+ # rubocop:disable Migration/PreventStrings
def change
- # rubocop:disable Migration/AddLimitToStringColumns
create_table :project_error_tracking_settings, id: :int, primary_key: :project_id, default: nil do |t|
t.boolean :enabled, null: false, default: true
t.string :api_url, null: false
@@ -14,6 +14,6 @@ class CreateErrorTrackingSettings < ActiveRecord::Migration[5.0]
t.string :encrypted_token_iv
t.foreign_key :projects, column: :project_id, on_delete: :cascade
end
- # rubocop:enable Migration/AddLimitToStringColumns
end
+ # rubocop:enable Migration/PreventStrings
end
diff --git a/db/migrate/20181228175414_create_releases_link_table.rb b/db/migrate/20181228175414_create_releases_link_table.rb
index 168c4722cc1..9d770d0dc2f 100644
--- a/db/migrate/20181228175414_create_releases_link_table.rb
+++ b/db/migrate/20181228175414_create_releases_link_table.rb
@@ -5,8 +5,8 @@ class CreateReleasesLinkTable < ActiveRecord::Migration[5.0]
DOWNTIME = false
+ # rubocop:disable Migration/PreventStrings
def change
- # rubocop:disable Migration/AddLimitToStringColumns
create_table :release_links, id: :bigserial do |t|
t.references :release, null: false, index: false, foreign_key: { on_delete: :cascade }
t.string :url, null: false
@@ -16,6 +16,6 @@ class CreateReleasesLinkTable < ActiveRecord::Migration[5.0]
t.index [:release_id, :url], unique: true
t.index [:release_id, :name], unique: true
end
- # rubocop:enable Migration/AddLimitToStringColumns
end
+ # rubocop:enable Migration/PreventStrings
end
diff --git a/db/migrate/20190109153125_add_merge_request_external_diffs.rb b/db/migrate/20190109153125_add_merge_request_external_diffs.rb
index a680856a3d8..d787e78142a 100644
--- a/db/migrate/20190109153125_add_merge_request_external_diffs.rb
+++ b/db/migrate/20190109153125_add_merge_request_external_diffs.rb
@@ -11,7 +11,7 @@ class AddMergeRequestExternalDiffs < ActiveRecord::Migration[5.0]
def change
# Allow the merge request diff to store details about an external file
- add_column :merge_request_diffs, :external_diff, :string # rubocop:disable Migration/AddLimitToStringColumns
+ add_column :merge_request_diffs, :external_diff, :string # rubocop:disable Migration/PreventStrings
add_column :merge_request_diffs, :external_diff_store, :integer
add_column :merge_request_diffs, :stored_externally, :boolean
diff --git a/db/migrate/20190114172110_add_domain_to_cluster.rb b/db/migrate/20190114172110_add_domain_to_cluster.rb
index d8f10af9cad..ebe588d8b77 100644
--- a/db/migrate/20190114172110_add_domain_to_cluster.rb
+++ b/db/migrate/20190114172110_add_domain_to_cluster.rb
@@ -4,6 +4,6 @@ class AddDomainToCluster < ActiveRecord::Migration[5.0]
DOWNTIME = false
def change
- add_column :clusters, :domain, :string # rubocop:disable Migration/AddLimitToStringColumns
+ add_column :clusters, :domain, :string # rubocop:disable Migration/PreventStrings
end
end
diff --git a/db/migrate/20190115092821_add_columns_project_error_tracking_settings.rb b/db/migrate/20190115092821_add_columns_project_error_tracking_settings.rb
index afed929cce4..ee6d63b558f 100644
--- a/db/migrate/20190115092821_add_columns_project_error_tracking_settings.rb
+++ b/db/migrate/20190115092821_add_columns_project_error_tracking_settings.rb
@@ -5,12 +5,14 @@ class AddColumnsProjectErrorTrackingSettings < ActiveRecord::Migration[5.0]
DOWNTIME = false
+ # rubocop:disable Migration/PreventStrings
def change
- add_column :project_error_tracking_settings, :project_name, :string # rubocop:disable Migration/AddLimitToStringColumns
- add_column :project_error_tracking_settings, :organization_name, :string # rubocop:disable Migration/AddLimitToStringColumns
+ add_column :project_error_tracking_settings, :project_name, :string
+ add_column :project_error_tracking_settings, :organization_name, :string
change_column_default :project_error_tracking_settings, :enabled, from: true, to: false
change_column_null :project_error_tracking_settings, :api_url, true
end
+ # rubocop:enable Migration/PreventStrings
end
diff --git a/db/migrate/20190116234221_add_sorting_fields_to_user_preference.rb b/db/migrate/20190116234221_add_sorting_fields_to_user_preference.rb
index 39aab600546..5d33f6f4339 100644
--- a/db/migrate/20190116234221_add_sorting_fields_to_user_preference.rb
+++ b/db/migrate/20190116234221_add_sorting_fields_to_user_preference.rb
@@ -9,10 +9,12 @@ class AddSortingFieldsToUserPreference < ActiveRecord::Migration[5.0]
# Set this constant to true if this migration requires downtime.
DOWNTIME = false
+ # rubocop:disable Migration/PreventStrings
def up
- add_column :user_preferences, :issues_sort, :string # rubocop:disable Migration/AddLimitToStringColumns
- add_column :user_preferences, :merge_requests_sort, :string # rubocop:disable Migration/AddLimitToStringColumns
+ add_column :user_preferences, :issues_sort, :string
+ add_column :user_preferences, :merge_requests_sort, :string
end
+ # rubocop:enable Migration/PreventStrings
def down
remove_column :user_preferences, :issues_sort
diff --git a/db/migrate/20190218134158_add_masked_to_ci_variables.rb b/db/migrate/20190218134158_add_masked_to_ci_variables.rb
index b4999d5b4a9..60dcc0d7af5 100644
--- a/db/migrate/20190218134158_add_masked_to_ci_variables.rb
+++ b/db/migrate/20190218134158_add_masked_to_ci_variables.rb
@@ -12,7 +12,7 @@ class AddMaskedToCiVariables < ActiveRecord::Migration[5.0]
disable_ddl_transaction!
def up
- add_column_with_default :ci_variables, :masked, :boolean, default: false, allow_null: false
+ add_column_with_default :ci_variables, :masked, :boolean, default: false, allow_null: false # rubocop:disable Migration/AddColumnWithDefault
end
def down
diff --git a/db/migrate/20190218134209_add_masked_to_ci_group_variables.rb b/db/migrate/20190218134209_add_masked_to_ci_group_variables.rb
index 8633875b341..c25881410d0 100644
--- a/db/migrate/20190218134209_add_masked_to_ci_group_variables.rb
+++ b/db/migrate/20190218134209_add_masked_to_ci_group_variables.rb
@@ -12,7 +12,7 @@ class AddMaskedToCiGroupVariables < ActiveRecord::Migration[5.0]
disable_ddl_transaction!
def up
- add_column_with_default :ci_group_variables, :masked, :boolean, default: false, allow_null: false
+ add_column_with_default :ci_group_variables, :masked, :boolean, default: false, allow_null: false # rubocop:disable Migration/AddColumnWithDefault
end
def down
diff --git a/db/migrate/20190219201635_add_asset_proxy_settings.rb b/db/migrate/20190219201635_add_asset_proxy_settings.rb
index 9de38cf8a89..173b2916c4d 100644
--- a/db/migrate/20190219201635_add_asset_proxy_settings.rb
+++ b/db/migrate/20190219201635_add_asset_proxy_settings.rb
@@ -6,11 +6,15 @@ class AddAssetProxySettings < ActiveRecord::Migration[5.0]
# Set this constant to true if this migration requires downtime.
DOWNTIME = false
+ # rubocop:disable Migration/PreventStrings
+ # rubocop:disable Migration/AddLimitToTextColumns
def change
add_column :application_settings, :asset_proxy_enabled, :boolean, default: false, null: false
- add_column :application_settings, :asset_proxy_url, :string # rubocop:disable Migration/AddLimitToStringColumns
+ add_column :application_settings, :asset_proxy_url, :string
add_column :application_settings, :asset_proxy_whitelist, :text
add_column :application_settings, :encrypted_asset_proxy_secret_key, :text
- add_column :application_settings, :encrypted_asset_proxy_secret_key_iv, :string # rubocop:disable Migration/AddLimitToStringColumns
+ add_column :application_settings, :encrypted_asset_proxy_secret_key_iv, :string
end
+ # rubocop:enable Migration/AddLimitToTextColumns
+ # rubocop:enable Migration/PreventStrings
end
diff --git a/db/migrate/20190220142344_add_email_header_and_footer_enabled_flag_to_appearances_table.rb b/db/migrate/20190220142344_add_email_header_and_footer_enabled_flag_to_appearances_table.rb
index 85b9e0580f4..33fb6b8ef0d 100644
--- a/db/migrate/20190220142344_add_email_header_and_footer_enabled_flag_to_appearances_table.rb
+++ b/db/migrate/20190220142344_add_email_header_and_footer_enabled_flag_to_appearances_table.rb
@@ -8,7 +8,7 @@ class AddEmailHeaderAndFooterEnabledFlagToAppearancesTable < ActiveRecord::Migra
DOWNTIME = false
def up
- add_column_with_default(:appearances, :email_header_and_footer_enabled, :boolean, default: false)
+ add_column_with_default(:appearances, :email_header_and_footer_enabled, :boolean, default: false) # rubocop:disable Migration/AddColumnWithDefault
end
def down
diff --git a/db/migrate/20190228192410_add_multi_line_attributes_to_suggestion.rb b/db/migrate/20190228192410_add_multi_line_attributes_to_suggestion.rb
index 856dfc89fa3..4daabb6baa8 100644
--- a/db/migrate/20190228192410_add_multi_line_attributes_to_suggestion.rb
+++ b/db/migrate/20190228192410_add_multi_line_attributes_to_suggestion.rb
@@ -8,9 +8,9 @@ class AddMultiLineAttributesToSuggestion < ActiveRecord::Migration[5.0]
disable_ddl_transaction!
def up
- add_column_with_default :suggestions, :lines_above, :integer, default: 0, allow_null: false
- add_column_with_default :suggestions, :lines_below, :integer, default: 0, allow_null: false
- add_column_with_default :suggestions, :outdated, :boolean, default: false, allow_null: false
+ add_column_with_default :suggestions, :lines_above, :integer, default: 0, allow_null: false # rubocop:disable Migration/AddColumnWithDefault
+ add_column_with_default :suggestions, :lines_below, :integer, default: 0, allow_null: false # rubocop:disable Migration/AddColumnWithDefault
+ add_column_with_default :suggestions, :outdated, :boolean, default: false, allow_null: false # rubocop:disable Migration/AddColumnWithDefault
end
def down
diff --git a/db/migrate/20190301182457_add_external_hostname_to_ingress_and_knative.rb b/db/migrate/20190301182457_add_external_hostname_to_ingress_and_knative.rb
index 37ba1090cf0..4cb6053fbe1 100644
--- a/db/migrate/20190301182457_add_external_hostname_to_ingress_and_knative.rb
+++ b/db/migrate/20190301182457_add_external_hostname_to_ingress_and_knative.rb
@@ -3,8 +3,10 @@
class AddExternalHostnameToIngressAndKnative < ActiveRecord::Migration[5.0]
DOWNTIME = false
+ # rubocop:disable Migration/PreventStrings
def change
- add_column :clusters_applications_ingress, :external_hostname, :string # rubocop:disable Migration/AddLimitToStringColumns
- add_column :clusters_applications_knative, :external_hostname, :string # rubocop:disable Migration/AddLimitToStringColumns
+ add_column :clusters_applications_ingress, :external_hostname, :string
+ add_column :clusters_applications_knative, :external_hostname, :string
end
+ # rubocop:enable Migration/PreventStrings
end
diff --git a/db/migrate/20190320174702_add_lets_encrypt_notification_email_to_application_settings.rb b/db/migrate/20190320174702_add_lets_encrypt_notification_email_to_application_settings.rb
index aeabf4e3cb4..e0ec10ca204 100644
--- a/db/migrate/20190320174702_add_lets_encrypt_notification_email_to_application_settings.rb
+++ b/db/migrate/20190320174702_add_lets_encrypt_notification_email_to_application_settings.rb
@@ -9,7 +9,9 @@ class AddLetsEncryptNotificationEmailToApplicationSettings < ActiveRecord::Migra
# Set this constant to true if this migration requires downtime.
DOWNTIME = false
+ # rubocop:disable Migration/PreventStrings
def change
- add_column :application_settings, :lets_encrypt_notification_email, :string # rubocop:disable Migration/AddLimitToStringColumns
+ add_column :application_settings, :lets_encrypt_notification_email, :string
end
+ # rubocop:enable Migration/PreventStrings
end
diff --git a/db/migrate/20190322164830_add_auto_ssl_enabled_to_pages_domain.rb b/db/migrate/20190322164830_add_auto_ssl_enabled_to_pages_domain.rb
index e74a9535ddf..41552b0e2e3 100644
--- a/db/migrate/20190322164830_add_auto_ssl_enabled_to_pages_domain.rb
+++ b/db/migrate/20190322164830_add_auto_ssl_enabled_to_pages_domain.rb
@@ -8,7 +8,7 @@ class AddAutoSslEnabledToPagesDomain < ActiveRecord::Migration[5.0]
disable_ddl_transaction!
def up
- add_column_with_default :pages_domains, :auto_ssl_enabled, :boolean, default: false
+ add_column_with_default :pages_domains, :auto_ssl_enabled, :boolean, default: false # rubocop:disable Migration/AddColumnWithDefault
end
def down
diff --git a/db/migrate/20190325105715_add_fields_to_user_preferences.rb b/db/migrate/20190325105715_add_fields_to_user_preferences.rb
index 4da5c496147..78ccbd65c84 100644
--- a/db/migrate/20190325105715_add_fields_to_user_preferences.rb
+++ b/db/migrate/20190325105715_add_fields_to_user_preferences.rb
@@ -11,7 +11,7 @@ class AddFieldsToUserPreferences < ActiveRecord::Migration[5.0]
DOWNTIME = false
def up
- add_column(:user_preferences, :timezone, :string) # rubocop:disable Migration/AddLimitToStringColumns
+ add_column(:user_preferences, :timezone, :string) # rubocop:disable Migration/PreventStrings
add_column(:user_preferences, :time_display_relative, :boolean)
add_column(:user_preferences, :time_format_in_24h, :boolean)
end
diff --git a/db/migrate/20190325165127_add_managed_to_cluster.rb b/db/migrate/20190325165127_add_managed_to_cluster.rb
index e960df9d502..14ed4db143e 100644
--- a/db/migrate/20190325165127_add_managed_to_cluster.rb
+++ b/db/migrate/20190325165127_add_managed_to_cluster.rb
@@ -8,7 +8,7 @@ class AddManagedToCluster < ActiveRecord::Migration[5.0]
DOWNTIME = false
def up
- add_column_with_default(:clusters, :managed, :boolean, default: true)
+ add_column_with_default(:clusters, :managed, :boolean, default: true) # rubocop:disable Migration/AddColumnWithDefault
end
def down
diff --git a/db/migrate/20190327163904_add_notification_email_to_notification_settings.rb b/db/migrate/20190327163904_add_notification_email_to_notification_settings.rb
index d912f922510..d8a7e96f902 100644
--- a/db/migrate/20190327163904_add_notification_email_to_notification_settings.rb
+++ b/db/migrate/20190327163904_add_notification_email_to_notification_settings.rb
@@ -5,7 +5,9 @@ class AddNotificationEmailToNotificationSettings < ActiveRecord::Migration[5.0]
DOWNTIME = false
+ # rubocop:disable Migration/PreventStrings
def change
- add_column :notification_settings, :notification_email, :string # rubocop:disable Migration/AddLimitToStringColumns
+ add_column :notification_settings, :notification_email, :string
end
+ # rubocop:enable Migration/PreventStrings
end
diff --git a/db/migrate/20190329085614_add_lets_encrypt_terms_of_service_accepted_to_application_settings.rb b/db/migrate/20190329085614_add_lets_encrypt_terms_of_service_accepted_to_application_settings.rb
index 16de63f207f..36641e24e45 100644
--- a/db/migrate/20190329085614_add_lets_encrypt_terms_of_service_accepted_to_application_settings.rb
+++ b/db/migrate/20190329085614_add_lets_encrypt_terms_of_service_accepted_to_application_settings.rb
@@ -12,7 +12,7 @@ class AddLetsEncryptTermsOfServiceAcceptedToApplicationSettings < ActiveRecord::
disable_ddl_transaction!
def up
- add_column_with_default(:application_settings, :lets_encrypt_terms_of_service_accepted, :boolean, default: false)
+ add_column_with_default(:application_settings, :lets_encrypt_terms_of_service_accepted, :boolean, default: false) # rubocop:disable Migration/AddColumnWithDefault
end
def down
diff --git a/db/migrate/20190402150158_backport_enterprise_schema.rb b/db/migrate/20190402150158_backport_enterprise_schema.rb
index d1e911a04e6..694c0feba0a 100644
--- a/db/migrate/20190402150158_backport_enterprise_schema.rb
+++ b/db/migrate/20190402150158_backport_enterprise_schema.rb
@@ -2,7 +2,8 @@
# rubocop: disable Metrics/AbcSize
# rubocop: disable Migration/Datetime
-# rubocop: disable Migration/AddLimitToStringColumns
+# rubocop: disable Migration/PreventStrings
+# rubocop: disable Migration/AddLimitToTextColumns
class BackportEnterpriseSchema < ActiveRecord::Migration[5.0]
include Gitlab::Database::MigrationHelpers
@@ -197,7 +198,7 @@ class BackportEnterpriseSchema < ActiveRecord::Migration[5.0]
def add_column_with_default_if_not_exists(table, name, *args)
unless column_exists?(table, name)
- add_column_with_default(table, name, *args)
+ add_column_with_default(table, name, *args) # rubocop:disable Migration/AddColumnWithDefault
end
end
@@ -2188,4 +2189,5 @@ class BackportEnterpriseSchema < ActiveRecord::Migration[5.0]
end
# rubocop: enable Metrics/AbcSize
# rubocop: enable Migration/Datetime
-# rubocop: enable Migration/AddLimitToStringColumns
+# rubocop: enable Migration/PreventStrings
+# rubocop: enable Migration/AddLimitToTextColumns
diff --git a/db/migrate/20190409224933_add_name_to_geo_nodes.rb b/db/migrate/20190409224933_add_name_to_geo_nodes.rb
index 65c01683995..ac3eda701d1 100644
--- a/db/migrate/20190409224933_add_name_to_geo_nodes.rb
+++ b/db/migrate/20190409224933_add_name_to_geo_nodes.rb
@@ -10,7 +10,7 @@ class AddNameToGeoNodes < ActiveRecord::Migration[5.0]
DOWNTIME = false
def up
- add_column :geo_nodes, :name, :string # rubocop:disable Migration/AddLimitToStringColumns
+ add_column :geo_nodes, :name, :string # rubocop:disable Migration/PreventStrings
# url is also unique, and its type and size is identical to the name column,
# so this is safe.
diff --git a/db/migrate/20190414185432_add_comment_to_vulnerability_feedback.rb b/db/migrate/20190414185432_add_comment_to_vulnerability_feedback.rb
index 63644a2f8fd..f2047e21d1e 100644
--- a/db/migrate/20190414185432_add_comment_to_vulnerability_feedback.rb
+++ b/db/migrate/20190414185432_add_comment_to_vulnerability_feedback.rb
@@ -5,7 +5,7 @@ class AddCommentToVulnerabilityFeedback < ActiveRecord::Migration[5.1]
def up
add_column :vulnerability_feedback, :comment_author_id, :integer
- add_column :vulnerability_feedback, :comment, :text
+ add_column :vulnerability_feedback, :comment, :text # rubocop:disable Migration/AddLimitToTextColumns
add_column :vulnerability_feedback, :comment_timestamp, :datetime_with_timezone
end
diff --git a/db/migrate/20190415030217_add_variable_type_to_ci_variables.rb b/db/migrate/20190415030217_add_variable_type_to_ci_variables.rb
index 433f510299a..ed7af455e12 100644
--- a/db/migrate/20190415030217_add_variable_type_to_ci_variables.rb
+++ b/db/migrate/20190415030217_add_variable_type_to_ci_variables.rb
@@ -8,7 +8,7 @@ class AddVariableTypeToCiVariables < ActiveRecord::Migration[5.0]
ENV_VAR_VARIABLE_TYPE = 1
def up
- add_column_with_default(:ci_variables, :variable_type, :smallint, default: ENV_VAR_VARIABLE_TYPE)
+ add_column_with_default(:ci_variables, :variable_type, :smallint, default: ENV_VAR_VARIABLE_TYPE) # rubocop:disable Migration/AddColumnWithDefault
end
def down
diff --git a/db/migrate/20190416213556_add_variable_type_to_ci_group_variables.rb b/db/migrate/20190416213556_add_variable_type_to_ci_group_variables.rb
index dce73caeb5e..4d329cea1b5 100644
--- a/db/migrate/20190416213556_add_variable_type_to_ci_group_variables.rb
+++ b/db/migrate/20190416213556_add_variable_type_to_ci_group_variables.rb
@@ -8,7 +8,7 @@ class AddVariableTypeToCiGroupVariables < ActiveRecord::Migration[5.0]
ENV_VAR_VARIABLE_TYPE = 1
def up
- add_column_with_default(:ci_group_variables, :variable_type, :smallint, default: ENV_VAR_VARIABLE_TYPE)
+ add_column_with_default(:ci_group_variables, :variable_type, :smallint, default: ENV_VAR_VARIABLE_TYPE) # rubocop:disable Migration/AddColumnWithDefault
end
def down
diff --git a/db/migrate/20190416213631_add_variable_type_to_ci_pipeline_schedule_variables.rb b/db/migrate/20190416213631_add_variable_type_to_ci_pipeline_schedule_variables.rb
index 3079b2afd9c..b7d80cb2d0d 100644
--- a/db/migrate/20190416213631_add_variable_type_to_ci_pipeline_schedule_variables.rb
+++ b/db/migrate/20190416213631_add_variable_type_to_ci_pipeline_schedule_variables.rb
@@ -8,7 +8,7 @@ class AddVariableTypeToCiPipelineScheduleVariables < ActiveRecord::Migration[5.0
ENV_VAR_VARIABLE_TYPE = 1
def up
- add_column_with_default(:ci_pipeline_schedule_variables, :variable_type, :smallint, default: ENV_VAR_VARIABLE_TYPE)
+ add_column_with_default(:ci_pipeline_schedule_variables, :variable_type, :smallint, default: ENV_VAR_VARIABLE_TYPE) # rubocop:disable Migration/AddColumnWithDefault
end
def down
diff --git a/db/migrate/20190422082247_create_project_metrics_settings.rb b/db/migrate/20190422082247_create_project_metrics_settings.rb
index a0a2ed64820..177c4820a0c 100644
--- a/db/migrate/20190422082247_create_project_metrics_settings.rb
+++ b/db/migrate/20190422082247_create_project_metrics_settings.rb
@@ -7,7 +7,7 @@ class CreateProjectMetricsSettings < ActiveRecord::Migration[5.0]
def change
create_table :project_metrics_settings, id: :int, primary_key: :project_id, default: nil do |t|
- t.string :external_dashboard_url, null: false # rubocop:disable Migration/AddLimitToStringColumns
+ t.string :external_dashboard_url, null: false # rubocop:disable Migration/PreventStrings
t.foreign_key :projects, column: :project_id, on_delete: :cascade
end
end
diff --git a/db/migrate/20190429082448_create_pages_domain_acme_orders.rb b/db/migrate/20190429082448_create_pages_domain_acme_orders.rb
index ca1796d054c..75636031193 100644
--- a/db/migrate/20190429082448_create_pages_domain_acme_orders.rb
+++ b/db/migrate/20190429082448_create_pages_domain_acme_orders.rb
@@ -9,8 +9,9 @@ class CreatePagesDomainAcmeOrders < ActiveRecord::Migration[5.1]
# Set this constant to true if this migration requires downtime.
DOWNTIME = false
+ # rubocop:disable Migration/PreventStrings
+ # rubocop:disable Migration/AddLimitToTextColumns
def change
- # rubocop:disable Migration/AddLimitToStringColumns
create_table :pages_domain_acme_orders do |t|
t.references :pages_domain, null: false, index: true, foreign_key: { on_delete: :cascade }, type: :integer
@@ -25,6 +26,7 @@ class CreatePagesDomainAcmeOrders < ActiveRecord::Migration[5.1]
t.text :encrypted_private_key, null: false
t.text :encrypted_private_key_iv, null: false
end
- # rubocop:enable Migration/AddLimitToStringColumns
end
+ # rubocop:enable Migration/AddLimitToTextColumns
+ # rubocop:enable Migration/PreventStrings
end
diff --git a/db/migrate/20190430131225_create_issue_tracker_data.rb b/db/migrate/20190430131225_create_issue_tracker_data.rb
index d2134ad82c7..2ec9802dbcd 100644
--- a/db/migrate/20190430131225_create_issue_tracker_data.rb
+++ b/db/migrate/20190430131225_create_issue_tracker_data.rb
@@ -8,8 +8,8 @@ class CreateIssueTrackerData < ActiveRecord::Migration[5.1]
DOWNTIME = false
+ # rubocop:disable Migration/PreventStrings
def change
- # rubocop:disable Migration/AddLimitToStringColumns
create_table :issue_tracker_data do |t|
t.references :service, foreign_key: { on_delete: :cascade }, type: :integer, index: true, null: false
t.timestamps_with_timezone
@@ -20,6 +20,6 @@ class CreateIssueTrackerData < ActiveRecord::Migration[5.1]
t.string :encrypted_new_issue_url
t.string :encrypted_new_issue_url_iv
end
- # rubocop:enable Migration/AddLimitToStringColumns
end
+ # rubocop:enable Migration/PreventStrings
end
diff --git a/db/migrate/20190430142025_create_jira_tracker_data.rb b/db/migrate/20190430142025_create_jira_tracker_data.rb
index 5e53e5a701a..2144c60a267 100644
--- a/db/migrate/20190430142025_create_jira_tracker_data.rb
+++ b/db/migrate/20190430142025_create_jira_tracker_data.rb
@@ -8,8 +8,8 @@ class CreateJiraTrackerData < ActiveRecord::Migration[5.1]
DOWNTIME = false
+ # rubocop:disable Migration/PreventStrings
def change
- # rubocop:disable Migration/AddLimitToStringColumns
create_table :jira_tracker_data do |t|
t.references :service, foreign_key: { on_delete: :cascade }, type: :integer, index: true, null: false
t.timestamps_with_timezone
@@ -23,6 +23,6 @@ class CreateJiraTrackerData < ActiveRecord::Migration[5.1]
t.string :encrypted_password_iv
t.string :jira_issue_transition_id
end
- # rubocop:enable Migration/AddLimitToStringColumns
end
+ # rubocop:enable Migration/PreventStrings
end
diff --git a/db/migrate/20190514105711_create_ip_restriction.rb b/db/migrate/20190514105711_create_ip_restriction.rb
index 69f8c1b8c4e..51e711ca32b 100644
--- a/db/migrate/20190514105711_create_ip_restriction.rb
+++ b/db/migrate/20190514105711_create_ip_restriction.rb
@@ -12,7 +12,7 @@ class CreateIpRestriction < ActiveRecord::Migration[5.1]
type: :integer,
null: false,
index: true
- t.string :range, null: false # rubocop:disable Migration/AddLimitToStringColumns
+ t.string :range, null: false # rubocop:disable Migration/PreventStrings
end
add_foreign_key(:ip_restrictions, :namespaces, column: :group_id, on_delete: :cascade) # rubocop: disable Migration/AddConcurrentForeignKey
diff --git a/db/migrate/20190516151857_add_lets_encrypt_private_key_to_application_settings.rb b/db/migrate/20190516151857_add_lets_encrypt_private_key_to_application_settings.rb
index e1d3cca48d6..a2692ad32e9 100644
--- a/db/migrate/20190516151857_add_lets_encrypt_private_key_to_application_settings.rb
+++ b/db/migrate/20190516151857_add_lets_encrypt_private_key_to_application_settings.rb
@@ -9,8 +9,10 @@ class AddLetsEncryptPrivateKeyToApplicationSettings < ActiveRecord::Migration[5.
# Set this constant to true if this migration requires downtime.
DOWNTIME = false
+ # rubocop:disable Migration/AddLimitToTextColumns
def change
add_column :application_settings, :encrypted_lets_encrypt_private_key, :text
add_column :application_settings, :encrypted_lets_encrypt_private_key_iv, :text
end
+ # rubocop:enable Migration/AddLimitToTextColumns
end
diff --git a/db/migrate/20190520200123_add_rule_type_to_approval_merge_request_approval_rules.rb b/db/migrate/20190520200123_add_rule_type_to_approval_merge_request_approval_rules.rb
index 7339a4fccba..7bdb48f3eec 100644
--- a/db/migrate/20190520200123_add_rule_type_to_approval_merge_request_approval_rules.rb
+++ b/db/migrate/20190520200123_add_rule_type_to_approval_merge_request_approval_rules.rb
@@ -12,7 +12,7 @@ class AddRuleTypeToApprovalMergeRequestApprovalRules < ActiveRecord::Migration[5
disable_ddl_transaction!
def up
- add_column_with_default(:approval_merge_request_rules, :rule_type, :integer, limit: 2, default: 1)
+ add_column_with_default(:approval_merge_request_rules, :rule_type, :integer, limit: 2, default: 1) # rubocop:disable Migration/AddColumnWithDefault
end
def down
diff --git a/db/migrate/20190527011309_add_required_template_name_to_application_settings.rb b/db/migrate/20190527011309_add_required_template_name_to_application_settings.rb
index 5c47e6f33c2..9e0f5c5b1e7 100644
--- a/db/migrate/20190527011309_add_required_template_name_to_application_settings.rb
+++ b/db/migrate/20190527011309_add_required_template_name_to_application_settings.rb
@@ -9,7 +9,9 @@ class AddRequiredTemplateNameToApplicationSettings < ActiveRecord::Migration[5.1
# Set this constant to true if this migration requires downtime.
DOWNTIME = false
+ # rubocop:disable Migration/PreventStrings
def change
- add_column :application_settings, :required_instance_ci_template, :string, null: true # rubocop:disable Migration/AddLimitToStringColumns
+ add_column :application_settings, :required_instance_ci_template, :string, null: true
end
+ # rubocop:enable Migration/PreventStrings
end
diff --git a/db/migrate/20190529142545_add_dns_rebinding_protection_enabled_to_application_settings.rb b/db/migrate/20190529142545_add_dns_rebinding_protection_enabled_to_application_settings.rb
index 8835dc8b7ba..b50845c85b3 100644
--- a/db/migrate/20190529142545_add_dns_rebinding_protection_enabled_to_application_settings.rb
+++ b/db/migrate/20190529142545_add_dns_rebinding_protection_enabled_to_application_settings.rb
@@ -11,7 +11,7 @@ class AddDnsRebindingProtectionEnabledToApplicationSettings < ActiveRecord::Migr
disable_ddl_transaction!
def up
- add_column_with_default(:application_settings, :dns_rebinding_protection_enabled,
+ add_column_with_default(:application_settings, :dns_rebinding_protection_enabled, # rubocop:disable Migration/AddColumnWithDefault
:boolean,
default: true,
allow_null: false)
diff --git a/db/migrate/20190604091310_add_ldap_membership_lock.rb b/db/migrate/20190604091310_add_ldap_membership_lock.rb
index 1afc6aeefd5..d2e99f33b87 100644
--- a/db/migrate/20190604091310_add_ldap_membership_lock.rb
+++ b/db/migrate/20190604091310_add_ldap_membership_lock.rb
@@ -11,7 +11,7 @@ class AddLdapMembershipLock < ActiveRecord::Migration[5.1]
disable_ddl_transaction!
def up
- add_column_with_default(:application_settings, :lock_memberships_to_ldap, :boolean, default: false)
+ add_column_with_default(:application_settings, :lock_memberships_to_ldap, :boolean, default: false) # rubocop:disable Migration/AddColumnWithDefault
end
def down
diff --git a/db/migrate/20190605104727_add_default_project_deletion_protection_to_application_settings.rb b/db/migrate/20190605104727_add_default_project_deletion_protection_to_application_settings.rb
index ee04b49813b..e660c918fa4 100644
--- a/db/migrate/20190605104727_add_default_project_deletion_protection_to_application_settings.rb
+++ b/db/migrate/20190605104727_add_default_project_deletion_protection_to_application_settings.rb
@@ -11,7 +11,9 @@ class AddDefaultProjectDeletionProtectionToApplicationSettings < ActiveRecord::M
disable_ddl_transaction!
def up
+ # rubocop:disable Migration/AddColumnWithDefault
add_column_with_default :application_settings, :default_project_deletion_protection, :boolean, default: false, allow_null: false
+ # rubocop:enable Migration/AddColumnWithDefault
end
def down
diff --git a/db/migrate/20190606054742_add_token_encrypted_to_operations_feature_flags_clients.rb b/db/migrate/20190606054742_add_token_encrypted_to_operations_feature_flags_clients.rb
index 2db4dc85750..01cd49ac219 100644
--- a/db/migrate/20190606054742_add_token_encrypted_to_operations_feature_flags_clients.rb
+++ b/db/migrate/20190606054742_add_token_encrypted_to_operations_feature_flags_clients.rb
@@ -5,7 +5,9 @@ class AddTokenEncryptedToOperationsFeatureFlagsClients < ActiveRecord::Migration
DOWNTIME = false
+ # rubocop:disable Migration/PreventStrings
def change
- add_column :operations_feature_flags_clients, :token_encrypted, :string # rubocop:disable Migration/AddLimitToStringColumns
+ add_column :operations_feature_flags_clients, :token_encrypted, :string
end
+ # rubocop:enable Migration/PreventStrings
end
diff --git a/db/migrate/20190606202100_add_name_to_badges.rb b/db/migrate/20190606202100_add_name_to_badges.rb
index 472e1202ad8..7b386a6e690 100644
--- a/db/migrate/20190606202100_add_name_to_badges.rb
+++ b/db/migrate/20190606202100_add_name_to_badges.rb
@@ -6,6 +6,6 @@ class AddNameToBadges < ActiveRecord::Migration[5.0]
DOWNTIME = false
def change
- add_column :badges, :name, :string, null: true, limit: 255
+ add_column :badges, :name, :string, null: true, limit: 255 # rubocop:disable Migration/PreventStrings
end
end
diff --git a/db/migrate/20190607085356_add_source_to_pages_domains.rb b/db/migrate/20190607085356_add_source_to_pages_domains.rb
index 0a845d7d11f..d681ab67431 100644
--- a/db/migrate/20190607085356_add_source_to_pages_domains.rb
+++ b/db/migrate/20190607085356_add_source_to_pages_domains.rb
@@ -12,7 +12,7 @@ class AddSourceToPagesDomains < ActiveRecord::Migration[5.1]
disable_ddl_transaction!
def up
- add_column_with_default(:pages_domains, :certificate_source, :smallint, default: 0)
+ add_column_with_default(:pages_domains, :certificate_source, :smallint, default: 0) # rubocop:disable Migration/AddColumnWithDefault
end
def down
diff --git a/db/migrate/20190611090827_add_time_tracking_limit_to_hours_to_application_settings.rb b/db/migrate/20190611090827_add_time_tracking_limit_to_hours_to_application_settings.rb
index a5f8925c1db..f0a3ec1d3bc 100644
--- a/db/migrate/20190611090827_add_time_tracking_limit_to_hours_to_application_settings.rb
+++ b/db/migrate/20190611090827_add_time_tracking_limit_to_hours_to_application_settings.rb
@@ -12,7 +12,9 @@ class AddTimeTrackingLimitToHoursToApplicationSettings < ActiveRecord::Migration
disable_ddl_transaction!
def up
+ # rubocop:disable Migration/AddColumnWithDefault
add_column_with_default :application_settings, :time_tracking_limit_to_hours, :boolean, default: false, allow_null: false
+ # rubocop:enable Migration/AddColumnWithDefault
end
def down
diff --git a/db/migrate/20190611161641_add_target_project_id_to_merge_trains.rb b/db/migrate/20190611161641_add_target_project_id_to_merge_trains.rb
index 553e9371ab3..7b73202ab8d 100644
--- a/db/migrate/20190611161641_add_target_project_id_to_merge_trains.rb
+++ b/db/migrate/20190611161641_add_target_project_id_to_merge_trains.rb
@@ -5,10 +5,14 @@ class AddTargetProjectIdToMergeTrains < ActiveRecord::Migration[5.1]
DOWNTIME = false
+ # rubocop:disable Rails/NotNullColumn
+ # rubocop:disable Migration/AddReference
+ # rubocop:disable Migration/AddLimitToTextColumns
def change
- # rubocop:disable Rails/NotNullColumn, Migration/AddReference
add_reference :merge_trains, :target_project, null: false, index: true, foreign_key: { on_delete: :cascade, to_table: :projects }, type: :integer
add_column :merge_trains, :target_branch, :text, null: false
- # rubocop:enable Rails/NotNullColumn, Migration/AddReference
end
+ # rubocop:enable Migration/AddLimitToTextColumns
+ # rubocop:enable Migration/AddReference
+ # rubocop:enable Rails/NotNullColumn
end
diff --git a/db/migrate/20190613044655_add_username_to_deploy_tokens.rb b/db/migrate/20190613044655_add_username_to_deploy_tokens.rb
index a0acb02013b..ac5a6589f07 100644
--- a/db/migrate/20190613044655_add_username_to_deploy_tokens.rb
+++ b/db/migrate/20190613044655_add_username_to_deploy_tokens.rb
@@ -4,6 +4,6 @@ class AddUsernameToDeployTokens < ActiveRecord::Migration[5.1]
DOWNTIME = false
def change
- add_column :deploy_tokens, :username, :string # rubocop:disable Migration/AddLimitToStringColumns
+ add_column :deploy_tokens, :username, :string # rubocop:disable Migration/PreventStrings
end
end
diff --git a/db/migrate/20190613073003_create_project_aliases.rb b/db/migrate/20190613073003_create_project_aliases.rb
index 896d3ca5813..ee111d437e9 100644
--- a/db/migrate/20190613073003_create_project_aliases.rb
+++ b/db/migrate/20190613073003_create_project_aliases.rb
@@ -8,7 +8,7 @@ class CreateProjectAliases < ActiveRecord::Migration[5.1]
def change
create_table :project_aliases do |t|
t.references :project, null: false, index: true, foreign_key: { on_delete: :cascade }, type: :integer
- t.string :name, null: false, index: { unique: true } # rubocop:disable Migration/AddLimitToStringColumns
+ t.string :name, null: false, index: { unique: true } # rubocop:disable Migration/PreventStrings
t.timestamps_with_timezone null: false
end
diff --git a/db/migrate/20190617123615_add_grafana_to_settings.rb b/db/migrate/20190617123615_add_grafana_to_settings.rb
index f9c6f4d883e..2d2250cef27 100644
--- a/db/migrate/20190617123615_add_grafana_to_settings.rb
+++ b/db/migrate/20190617123615_add_grafana_to_settings.rb
@@ -8,7 +8,7 @@ class AddGrafanaToSettings < ActiveRecord::Migration[5.1]
DOWNTIME = false
def up
- add_column_with_default(:application_settings, :grafana_enabled, :boolean,
+ add_column_with_default(:application_settings, :grafana_enabled, :boolean, # rubocop:disable Migration/AddColumnWithDefault
default: false, allow_null: false)
end
diff --git a/db/migrate/20190621151636_add_merge_request_rebase_jid.rb b/db/migrate/20190621151636_add_merge_request_rebase_jid.rb
index 6c1081732e8..df1b1c79375 100644
--- a/db/migrate/20190621151636_add_merge_request_rebase_jid.rb
+++ b/db/migrate/20190621151636_add_merge_request_rebase_jid.rb
@@ -4,6 +4,6 @@ class AddMergeRequestRebaseJid < ActiveRecord::Migration[5.1]
DOWNTIME = false
def change
- add_column :merge_requests, :rebase_jid, :string # rubocop:disable Migration/AddLimitToStringColumns
+ add_column :merge_requests, :rebase_jid, :string # rubocop:disable Migration/PreventStrings
end
end
diff --git a/db/migrate/20190624123615_add_grafana_url_to_settings.rb b/db/migrate/20190624123615_add_grafana_url_to_settings.rb
index 835ec4e9094..f10014872e9 100644
--- a/db/migrate/20190624123615_add_grafana_url_to_settings.rb
+++ b/db/migrate/20190624123615_add_grafana_url_to_settings.rb
@@ -7,12 +7,12 @@ class AddGrafanaUrlToSettings < ActiveRecord::Migration[5.1]
DOWNTIME = false
+ # rubocop:disable Migration/PreventStrings
def up
- # rubocop:disable Migration/AddLimitToStringColumns
- add_column_with_default(:application_settings, :grafana_url, :string,
+ add_column_with_default(:application_settings, :grafana_url, :string, # rubocop:disable Migration/AddColumnWithDefault
default: '/-/grafana', allow_null: false)
- # rubocop:enable Migration/AddLimitToStringColumns
end
+ # rubocop:enable Migration/PreventStrings
def down
remove_column(:application_settings, :grafana_url)
diff --git a/db/migrate/20190625115224_add_description_to_services.rb b/db/migrate/20190625115224_add_description_to_services.rb
index d5ef3900462..bdd6d70cb0f 100644
--- a/db/migrate/20190625115224_add_description_to_services.rb
+++ b/db/migrate/20190625115224_add_description_to_services.rb
@@ -9,6 +9,6 @@ class AddDescriptionToServices < ActiveRecord::Migration[5.1]
DOWNTIME = false
def change
- add_column :services, :description, :string, limit: 500
+ add_column :services, :description, :string, limit: 500 # rubocop:disable Migration/PreventStrings
end
end
diff --git a/db/migrate/20190628145246_add_strategies_to_operations_feature_flag_scopes.rb b/db/migrate/20190628145246_add_strategies_to_operations_feature_flag_scopes.rb
index ed1f16ee69a..185a2b04531 100644
--- a/db/migrate/20190628145246_add_strategies_to_operations_feature_flag_scopes.rb
+++ b/db/migrate/20190628145246_add_strategies_to_operations_feature_flag_scopes.rb
@@ -8,7 +8,9 @@ class AddStrategiesToOperationsFeatureFlagScopes < ActiveRecord::Migration[5.1]
disable_ddl_transaction!
def up
+ # rubocop:disable Migration/AddColumnWithDefault
add_column_with_default :operations_feature_flag_scopes, :strategies, :jsonb, default: [{ name: "default", parameters: {} }]
+ # rubocop:enable Migration/AddColumnWithDefault
end
def down
diff --git a/db/migrate/20190709204413_add_rule_type_to_approval_project_rules.rb b/db/migrate/20190709204413_add_rule_type_to_approval_project_rules.rb
index 87a228c9bf9..b11154d0f26 100644
--- a/db/migrate/20190709204413_add_rule_type_to_approval_project_rules.rb
+++ b/db/migrate/20190709204413_add_rule_type_to_approval_project_rules.rb
@@ -8,7 +8,7 @@ class AddRuleTypeToApprovalProjectRules < ActiveRecord::Migration[5.1]
disable_ddl_transaction!
def up
- add_column_with_default :approval_project_rules, :rule_type, :integer, limit: 2, default: 0, allow_null: false
+ add_column_with_default :approval_project_rules, :rule_type, :integer, limit: 2, default: 0, allow_null: false # rubocop:disable Migration/AddColumnWithDefault
end
def down
diff --git a/db/migrate/20190711124721_create_job_variables.rb b/db/migrate/20190711124721_create_job_variables.rb
index 4ff4b031d8f..6174afc236c 100644
--- a/db/migrate/20190711124721_create_job_variables.rb
+++ b/db/migrate/20190711124721_create_job_variables.rb
@@ -9,8 +9,9 @@ class CreateJobVariables < ActiveRecord::Migration[5.1]
# Set this constant to true if this migration requires downtime.
DOWNTIME = false
+ # rubocop:disable Migration/PreventStrings
+ # rubocop:disable Migration/AddLimitToTextColumns
def change
- # rubocop:disable Migration/AddLimitToStringColumns
create_table :ci_job_variables do |t|
t.string :key, null: false
t.text :encrypted_value
@@ -18,8 +19,9 @@ class CreateJobVariables < ActiveRecord::Migration[5.1]
t.references :job, null: false, index: true, foreign_key: { to_table: :ci_builds, on_delete: :cascade }
t.integer :variable_type, null: false, limit: 2, default: 1
end
- # rubocop:enable Migration/AddLimitToStringColumns
add_index :ci_job_variables, [:key, :job_id], unique: true
end
+ # rubocop:enable Migration/AddLimitToTextColumns
+ # rubocop:enable Migration/PreventStrings
end
diff --git a/db/migrate/20190711200508_add_token_encrypted_to_deploy_tokens.rb b/db/migrate/20190711200508_add_token_encrypted_to_deploy_tokens.rb
index ea0956fdf7f..dff682e8ce7 100644
--- a/db/migrate/20190711200508_add_token_encrypted_to_deploy_tokens.rb
+++ b/db/migrate/20190711200508_add_token_encrypted_to_deploy_tokens.rb
@@ -5,7 +5,9 @@ class AddTokenEncryptedToDeployTokens < ActiveRecord::Migration[5.1]
DOWNTIME = false
+ # rubocop:disable Migration/PreventStrings
def change
add_column :deploy_tokens, :token_encrypted, :string, limit: 255
end
+ # rubocop:enable Migration/PreventStrings
end
diff --git a/db/migrate/20190712064021_add_namespace_per_environment_flag_to_clusters.rb b/db/migrate/20190712064021_add_namespace_per_environment_flag_to_clusters.rb
index 4c8a0ab3def..771eb21c4b6 100644
--- a/db/migrate/20190712064021_add_namespace_per_environment_flag_to_clusters.rb
+++ b/db/migrate/20190712064021_add_namespace_per_environment_flag_to_clusters.rb
@@ -11,7 +11,7 @@ class AddNamespacePerEnvironmentFlagToClusters < ActiveRecord::Migration[5.1]
disable_ddl_transaction!
def up
- add_column_with_default :clusters, :namespace_per_environment, :boolean, default: false
+ add_column_with_default :clusters, :namespace_per_environment, :boolean, default: false # rubocop:disable Migration/AddColumnWithDefault
end
def down
diff --git a/db/migrate/20190715140740_add_event_type_to_design_management_designs_versions.rb b/db/migrate/20190715140740_add_event_type_to_design_management_designs_versions.rb
index 81a8b0a3271..136a9d27e25 100644
--- a/db/migrate/20190715140740_add_event_type_to_design_management_designs_versions.rb
+++ b/db/migrate/20190715140740_add_event_type_to_design_management_designs_versions.rb
@@ -10,7 +10,6 @@ class AddEventTypeToDesignManagementDesignsVersions < ActiveRecord::Migration[5.
# We disable these cops here because adding this column is safe. The table does not
# have any data in it.
# rubocop: disable Migration/AddIndex
- # rubocop: disable Migration/AddColumn
def up
add_column(:design_management_designs_versions, :event, :integer,
limit: 2,
diff --git a/db/migrate/20190715173819_add_object_storage_flag_to_geo_node.rb b/db/migrate/20190715173819_add_object_storage_flag_to_geo_node.rb
index 2d3243f3357..cbc353b6282 100644
--- a/db/migrate/20190715173819_add_object_storage_flag_to_geo_node.rb
+++ b/db/migrate/20190715173819_add_object_storage_flag_to_geo_node.rb
@@ -12,7 +12,7 @@ class AddObjectStorageFlagToGeoNode < ActiveRecord::Migration[5.2]
disable_ddl_transaction!
def up
- add_column_with_default :geo_nodes, :sync_object_storage, :boolean, default: false
+ add_column_with_default :geo_nodes, :sync_object_storage, :boolean, default: false # rubocop:disable Migration/AddColumnWithDefault
end
def down
diff --git a/db/migrate/20190716144222_create_analytics_cycle_analytics_project_stages.rb b/db/migrate/20190716144222_create_analytics_cycle_analytics_project_stages.rb
index 5c005377b00..c03191013dc 100644
--- a/db/migrate/20190716144222_create_analytics_cycle_analytics_project_stages.rb
+++ b/db/migrate/20190716144222_create_analytics_cycle_analytics_project_stages.rb
@@ -26,7 +26,7 @@ class CreateAnalyticsCycleAnalyticsProjectStages < ActiveRecord::Migration[5.2]
})
t.boolean :hidden, default: false, null: false
t.boolean :custom, default: true, null: false
- t.string :name, null: false, limit: 255
+ t.string :name, null: false, limit: 255 # rubocop:disable Migration/PreventStrings
end
add_index :analytics_cycle_analytics_project_stages, [:project_id, :name], unique: true, name: INDEX_PREFIX + 'on_project_id_and_name'
diff --git a/db/migrate/20190722104947_add_static_object_token_to_users.rb b/db/migrate/20190722104947_add_static_object_token_to_users.rb
index 180e6a30b04..3983f076b3f 100644
--- a/db/migrate/20190722104947_add_static_object_token_to_users.rb
+++ b/db/migrate/20190722104947_add_static_object_token_to_users.rb
@@ -8,9 +8,13 @@ class AddStaticObjectTokenToUsers < ActiveRecord::Migration[5.2]
disable_ddl_transaction!
+ # rubocop:disable Migration/AddColumnsToWideTables
+ # rubocop:disable Migration/PreventStrings
def up
- add_column :users, :static_object_token, :string, limit: 255 # rubocop:disable Migration/AddColumnsToWideTables
+ add_column :users, :static_object_token, :string, limit: 255
end
+ # rubocop:enable Migration/PreventStrings
+ # rubocop:enable Migration/AddColumnsToWideTables
def down
remove_column :users, :static_object_token
diff --git a/db/migrate/20190722132830_add_static_objects_external_storage_columns_to_application_settings.rb b/db/migrate/20190722132830_add_static_objects_external_storage_columns_to_application_settings.rb
index a23e6ed66cd..f493eae26a8 100644
--- a/db/migrate/20190722132830_add_static_objects_external_storage_columns_to_application_settings.rb
+++ b/db/migrate/20190722132830_add_static_objects_external_storage_columns_to_application_settings.rb
@@ -7,8 +7,10 @@ class AddStaticObjectsExternalStorageColumnsToApplicationSettings < ActiveRecord
# Set this constant to true if this migration requires downtime.
DOWNTIME = false
+ # rubocop:disable Migration/PreventStrings
def change
add_column :application_settings, :static_objects_external_storage_url, :string, limit: 255
add_column :application_settings, :static_objects_external_storage_auth_token, :string, limit: 255
end
+ # rubocop:enable Migration/PreventStrings
end
diff --git a/db/migrate/20190723153247_create_allowed_email_domains_for_groups.rb b/db/migrate/20190723153247_create_allowed_email_domains_for_groups.rb
index c6c5b56ed8b..71af7434f9d 100644
--- a/db/migrate/20190723153247_create_allowed_email_domains_for_groups.rb
+++ b/db/migrate/20190723153247_create_allowed_email_domains_for_groups.rb
@@ -16,7 +16,7 @@ class CreateAllowedEmailDomainsForGroups < ActiveRecord::Migration[5.2]
null: false,
index: true
t.foreign_key :namespaces, column: :group_id, on_delete: :cascade
- t.string :domain, null: false, limit: 255
+ t.string :domain, null: false, limit: 255 # rubocop:disable Migration/PreventStrings
end
end
end
diff --git a/db/migrate/20190729062536_create_analytics_cycle_analytics_group_stages.rb b/db/migrate/20190729062536_create_analytics_cycle_analytics_group_stages.rb
index 5b327dc5332..4753a7684e7 100644
--- a/db/migrate/20190729062536_create_analytics_cycle_analytics_group_stages.rb
+++ b/db/migrate/20190729062536_create_analytics_cycle_analytics_group_stages.rb
@@ -26,7 +26,7 @@ class CreateAnalyticsCycleAnalyticsGroupStages < ActiveRecord::Migration[5.2]
})
t.boolean :hidden, default: false, null: false
t.boolean :custom, default: true, null: false
- t.string :name, null: false, limit: 255
+ t.string :name, null: false, limit: 255 # rubocop:disable Migration/PreventStrings
end
add_index :analytics_cycle_analytics_group_stages, [:group_id, :name], unique: true, name: INDEX_PREFIX + 'on_group_id_and_name'
diff --git a/db/migrate/20190729180447_add_merge_requests_require_code_owner_approval_to_protected_branches.rb b/db/migrate/20190729180447_add_merge_requests_require_code_owner_approval_to_protected_branches.rb
index 098fcff9ace..bfac67606d6 100644
--- a/db/migrate/20190729180447_add_merge_requests_require_code_owner_approval_to_protected_branches.rb
+++ b/db/migrate/20190729180447_add_merge_requests_require_code_owner_approval_to_protected_branches.rb
@@ -9,7 +9,7 @@ class AddMergeRequestsRequireCodeOwnerApprovalToProtectedBranches < ActiveRecord
disable_ddl_transaction!
def up
- add_column_with_default(
+ add_column_with_default( # rubocop:disable Migration/AddColumnWithDefault
:protected_branches,
:code_owner_approval_required,
:boolean,
diff --git a/db/migrate/20190731084415_add_build_need.rb b/db/migrate/20190731084415_add_build_need.rb
index 45b8abb480d..ba044260db2 100644
--- a/db/migrate/20190731084415_add_build_need.rb
+++ b/db/migrate/20190731084415_add_build_need.rb
@@ -11,7 +11,7 @@ class AddBuildNeed < ActiveRecord::Migration[5.2]
def change
create_table :ci_build_needs, id: :serial do |t|
t.integer :build_id, null: false
- t.text :name, null: false
+ t.text :name, null: false # rubocop:disable Migration/AddLimitToTextColumns
t.index [:build_id, :name], unique: true
t.foreign_key :ci_builds, column: :build_id, on_delete: :cascade
diff --git a/db/migrate/20190801142441_add_throttle_protected_path_columns.rb b/db/migrate/20190801142441_add_throttle_protected_path_columns.rb
index bb6d54f3b7b..f9e0922ad66 100644
--- a/db/migrate/20190801142441_add_throttle_protected_path_columns.rb
+++ b/db/migrate/20190801142441_add_throttle_protected_path_columns.rb
@@ -16,10 +16,12 @@ class AddThrottleProtectedPathColumns < ActiveRecord::Migration[5.2]
'/import/github/personal_access_token'
]
+ # rubocop:disable Migration/PreventStrings
def change
add_column :application_settings, :throttle_protected_paths_enabled, :boolean, default: true, null: false
add_column :application_settings, :throttle_protected_paths_requests_per_period, :integer, default: 10, null: false
add_column :application_settings, :throttle_protected_paths_period_in_seconds, :integer, default: 60, null: false
add_column :application_settings, :protected_paths, :string, array: true, limit: 255, default: DEFAULT_PROTECTED_PATHS
end
+ # rubocop:enable Migration/PreventStrings
end
diff --git a/db/migrate/20190806071559_remove_epic_issues_default_relative_position.rb b/db/migrate/20190806071559_remove_epic_issues_default_relative_position.rb
index 3037f2ea106..0994bbbfd17 100644
--- a/db/migrate/20190806071559_remove_epic_issues_default_relative_position.rb
+++ b/db/migrate/20190806071559_remove_epic_issues_default_relative_position.rb
@@ -14,7 +14,7 @@ class RemoveEpicIssuesDefaultRelativePosition < ActiveRecord::Migration[5.2]
change_column_null :epic_issues, :relative_position, true
change_column_default :epic_issues, :relative_position, from: 1073741823, to: nil
else
- add_column_with_default(:epic_issues, :relative_position, :integer, default: nil, allow_null: true)
+ add_column_with_default(:epic_issues, :relative_position, :integer, default: nil, allow_null: true) # rubocop:disable Migration/AddColumnWithDefault
end
end
diff --git a/db/migrate/20190808152507_add_projects_sorting_field_to_user_preferences.rb b/db/migrate/20190808152507_add_projects_sorting_field_to_user_preferences.rb
index 941fead655e..5022ae843e7 100644
--- a/db/migrate/20190808152507_add_projects_sorting_field_to_user_preferences.rb
+++ b/db/migrate/20190808152507_add_projects_sorting_field_to_user_preferences.rb
@@ -3,9 +3,11 @@
class AddProjectsSortingFieldToUserPreferences < ActiveRecord::Migration[5.2]
DOWNTIME = false
+ # rubocop:disable Migration/PreventStrings
def up
add_column :user_preferences, :projects_sort, :string, limit: 64
end
+ # rubocop:enable Migration/PreventStrings
def down
remove_column :user_preferences, :projects_sort
diff --git a/db/migrate/20190816151221_add_active_jobs_limit_to_plans.rb b/db/migrate/20190816151221_add_active_jobs_limit_to_plans.rb
index 951ff41f1a8..193e6cb188e 100644
--- a/db/migrate/20190816151221_add_active_jobs_limit_to_plans.rb
+++ b/db/migrate/20190816151221_add_active_jobs_limit_to_plans.rb
@@ -8,7 +8,7 @@ class AddActiveJobsLimitToPlans < ActiveRecord::Migration[5.2]
disable_ddl_transaction!
def up
- add_column_with_default :plans, :active_jobs_limit, :integer, default: 0
+ add_column_with_default :plans, :active_jobs_limit, :integer, default: 0 # rubocop:disable Migration/AddColumnWithDefault
end
def down
diff --git a/db/migrate/20190820163320_add_first_last_name_to_user.rb b/db/migrate/20190820163320_add_first_last_name_to_user.rb
index 62bd1443b9d..908f55fa821 100644
--- a/db/migrate/20190820163320_add_first_last_name_to_user.rb
+++ b/db/migrate/20190820163320_add_first_last_name_to_user.rb
@@ -8,9 +8,11 @@ class AddFirstLastNameToUser < ActiveRecord::Migration[5.2]
DOWNTIME = false
# rubocop:disable Migration/AddColumnsToWideTables
+ # rubocop:disable Migration/PreventStrings
def change
add_column(:users, :first_name, :string, null: true, limit: 255)
add_column(:users, :last_name, :string, null: true, limit: 255)
end
+ # rubocop:enable Migration/PreventStrings
# rubocop:enable Migration/AddColumnsToWideTables
end
diff --git a/db/migrate/20190821040941_create_cluster_providers_aws.rb b/db/migrate/20190821040941_create_cluster_providers_aws.rb
index f80559861c4..666c33759a2 100644
--- a/db/migrate/20190821040941_create_cluster_providers_aws.rb
+++ b/db/migrate/20190821040941_create_cluster_providers_aws.rb
@@ -3,6 +3,8 @@
class CreateClusterProvidersAws < ActiveRecord::Migration[5.2]
DOWNTIME = false
+ # rubocop:disable Migration/PreventStrings
+ # rubocop:disable Migration/AddLimitToTextColumns
def change
create_table :cluster_providers_aws do |t|
t.references :cluster, null: false, type: :bigint, index: { unique: true }, foreign_key: { on_delete: :cascade }
@@ -30,4 +32,6 @@ class CreateClusterProvidersAws < ActiveRecord::Migration[5.2]
t.index [:cluster_id, :status]
end
end
+ # rubocop:enable Migration/AddLimitToTextColumns
+ # rubocop:enable Migration/PreventStrings
end
diff --git a/db/migrate/20190827222124_add_sourcegraph_configuration_to_application_settings.rb b/db/migrate/20190827222124_add_sourcegraph_configuration_to_application_settings.rb
index e624642c2fc..1059f73db8a 100644
--- a/db/migrate/20190827222124_add_sourcegraph_configuration_to_application_settings.rb
+++ b/db/migrate/20190827222124_add_sourcegraph_configuration_to_application_settings.rb
@@ -9,10 +9,12 @@ class AddSourcegraphConfigurationToApplicationSettings < ActiveRecord::Migration
# Set this constant to true if this migration requires downtime.
DOWNTIME = false
+ # rubocop:disable Migration/PreventStrings
def up
add_column(:application_settings, :sourcegraph_enabled, :boolean, default: false, null: false)
add_column(:application_settings, :sourcegraph_url, :string, null: true, limit: 255)
end
+ # rubocop:enable Migration/PreventStrings
def down
remove_column(:application_settings, :sourcegraph_enabled)
diff --git a/db/migrate/20190828172831_create_package_tag.rb b/db/migrate/20190828172831_create_package_tag.rb
index baf0a5cf11b..3d26b7ce602 100644
--- a/db/migrate/20190828172831_create_package_tag.rb
+++ b/db/migrate/20190828172831_create_package_tag.rb
@@ -8,7 +8,7 @@ class CreatePackageTag < ActiveRecord::Migration[5.2]
def change
create_table :packages_package_tags do |t|
t.references :package, index: true, null: false, foreign_key: { to_table: :packages_packages, on_delete: :cascade }, type: :integer
- t.string :name, limit: 255, null: false
+ t.string :name, limit: 255, null: false # rubocop:disable Migration/PreventStrings
end
end
end
diff --git a/db/migrate/20190829131130_create_external_pull_requests.rb b/db/migrate/20190829131130_create_external_pull_requests.rb
index 0c3168807ec..817f84017e2 100644
--- a/db/migrate/20190829131130_create_external_pull_requests.rb
+++ b/db/migrate/20190829131130_create_external_pull_requests.rb
@@ -6,6 +6,7 @@ class CreateExternalPullRequests < ActiveRecord::Migration[5.2]
DOWNTIME = false
INDEX = 'index_external_pull_requests_on_project_and_branches'
+ # rubocop:disable Migration/PreventStrings
def change
create_table :external_pull_requests do |t|
t.timestamps_with_timezone null: false
@@ -22,4 +23,5 @@ class CreateExternalPullRequests < ActiveRecord::Migration[5.2]
t.index [:project_id, :source_branch, :target_branch], unique: true, name: INDEX
end
end
+ # rubocop:enable Migration/PreventStrings
end
diff --git a/db/migrate/20190901174200_add_max_issue_count_to_list.rb b/db/migrate/20190901174200_add_max_issue_count_to_list.rb
index 59359f28d6a..7408d2f1c93 100644
--- a/db/migrate/20190901174200_add_max_issue_count_to_list.rb
+++ b/db/migrate/20190901174200_add_max_issue_count_to_list.rb
@@ -7,7 +7,7 @@ class AddMaxIssueCountToList < ActiveRecord::Migration[4.2]
DOWNTIME = false
def up
- add_column_with_default :lists, :max_issue_count, :integer, default: 0
+ add_column_with_default :lists, :max_issue_count, :integer, default: 0 # rubocop:disable Migration/AddColumnWithDefault
end
def down
diff --git a/db/migrate/20190903150358_create_analytics_repository_files_table.rb b/db/migrate/20190903150358_create_analytics_repository_files_table.rb
index e7c30a149f9..69c9cd504f4 100644
--- a/db/migrate/20190903150358_create_analytics_repository_files_table.rb
+++ b/db/migrate/20190903150358_create_analytics_repository_files_table.rb
@@ -5,6 +5,7 @@ class CreateAnalyticsRepositoryFilesTable < ActiveRecord::Migration[5.2]
DOWNTIME = false
+ # rubocop:disable Migration/PreventStrings
def change
create_table :analytics_repository_files do |t|
t.references :project,
@@ -18,4 +19,5 @@ class CreateAnalyticsRepositoryFilesTable < ActiveRecord::Migration[5.2]
add_index :analytics_repository_files, [:project_id, :file_path], unique: true
end
+ # rubocop:enable Migration/PreventStrings
end
diff --git a/db/migrate/20190905140605_add_cloud_run_to_clusters_providers_gcp.rb b/db/migrate/20190905140605_add_cloud_run_to_clusters_providers_gcp.rb
index e7ffd7cd4d3..cd6b2fb7d4f 100644
--- a/db/migrate/20190905140605_add_cloud_run_to_clusters_providers_gcp.rb
+++ b/db/migrate/20190905140605_add_cloud_run_to_clusters_providers_gcp.rb
@@ -8,7 +8,7 @@ class AddCloudRunToClustersProvidersGcp < ActiveRecord::Migration[5.2]
disable_ddl_transaction!
def up
- add_column_with_default(:cluster_providers_gcp, :cloud_run, :boolean, default: false)
+ add_column_with_default(:cluster_providers_gcp, :cloud_run, :boolean, default: false) # rubocop:disable Migration/AddColumnWithDefault
end
def down
diff --git a/db/migrate/20190906104555_create_alerts_service_data.rb b/db/migrate/20190906104555_create_alerts_service_data.rb
index 8ce447a67d1..9388a205978 100644
--- a/db/migrate/20190906104555_create_alerts_service_data.rb
+++ b/db/migrate/20190906104555_create_alerts_service_data.rb
@@ -3,6 +3,7 @@
class CreateAlertsServiceData < ActiveRecord::Migration[5.2]
DOWNTIME = false
+ # rubocop:disable Migration/PreventStrings
def change
create_table :alerts_service_data do |t|
t.references :service, type: :integer, index: true, null: false,
@@ -12,4 +13,5 @@ class CreateAlertsServiceData < ActiveRecord::Migration[5.2]
t.string :encrypted_token_iv, limit: 255
end
end
+ # rubocop:enable Migration/PreventStrings
end
diff --git a/db/migrate/20190907184714_add_show_whitespace_in_diffs_to_user_preferences.rb b/db/migrate/20190907184714_add_show_whitespace_in_diffs_to_user_preferences.rb
index 50d5d2b0574..41f9b36278a 100644
--- a/db/migrate/20190907184714_add_show_whitespace_in_diffs_to_user_preferences.rb
+++ b/db/migrate/20190907184714_add_show_whitespace_in_diffs_to_user_preferences.rb
@@ -11,7 +11,7 @@ class AddShowWhitespaceInDiffsToUserPreferences < ActiveRecord::Migration[5.2]
disable_ddl_transaction!
def up
- add_column_with_default :user_preferences, :show_whitespace_in_diffs, :boolean, default: true, allow_null: false
+ add_column_with_default :user_preferences, :show_whitespace_in_diffs, :boolean, default: true, allow_null: false # rubocop:disable Migration/AddColumnWithDefault
end
def down
diff --git a/db/migrate/20190910211526_create_packages_conan_file_metadata.rb b/db/migrate/20190910211526_create_packages_conan_file_metadata.rb
index 0f8dacb72de..a6830a93cbc 100644
--- a/db/migrate/20190910211526_create_packages_conan_file_metadata.rb
+++ b/db/migrate/20190910211526_create_packages_conan_file_metadata.rb
@@ -5,6 +5,7 @@ class CreatePackagesConanFileMetadata < ActiveRecord::Migration[5.2]
DOWNTIME = false
+ # rubocop:disable Migration/PreventStrings
def change
create_table :packages_conan_file_metadata do |t|
t.references :package_file, index: { unique: true }, null: false, foreign_key: { to_table: :packages_package_files, on_delete: :cascade }, type: :bigint
@@ -15,4 +16,5 @@ class CreatePackagesConanFileMetadata < ActiveRecord::Migration[5.2]
t.integer "conan_file_type", limit: 2, null: false
end
end
+ # rubocop:enable Migration/PreventStrings
end
diff --git a/db/migrate/20190913174707_add_spdx_id_to_software_licenses.rb b/db/migrate/20190913174707_add_spdx_id_to_software_licenses.rb
index 66cd450895c..46e1400c394 100644
--- a/db/migrate/20190913174707_add_spdx_id_to_software_licenses.rb
+++ b/db/migrate/20190913174707_add_spdx_id_to_software_licenses.rb
@@ -3,9 +3,11 @@
class AddSpdxIdToSoftwareLicenses < ActiveRecord::Migration[5.2]
DOWNTIME = false
+ # rubocop:disable Migration/PreventStrings
def up
add_column :software_licenses, :spdx_identifier, :string, limit: 255
end
+ # rubocop:enable Migration/PreventStrings
def down
remove_column :software_licenses, :spdx_identifier
diff --git a/db/migrate/20190918102042_create_grafana_integrations.rb b/db/migrate/20190918102042_create_grafana_integrations.rb
index aac27d129db..d48aaaf45bc 100644
--- a/db/migrate/20190918102042_create_grafana_integrations.rb
+++ b/db/migrate/20190918102042_create_grafana_integrations.rb
@@ -3,6 +3,7 @@
class CreateGrafanaIntegrations < ActiveRecord::Migration[5.2]
DOWNTIME = false
+ # rubocop:disable Migration/PreventStrings
def change
create_table :grafana_integrations do |t|
t.references :project, index: true, foreign_key: { on_delete: :cascade }, unique: true, null: false
@@ -12,4 +13,5 @@ class CreateGrafanaIntegrations < ActiveRecord::Migration[5.2]
t.string :grafana_url, null: false, limit: 1024
end
end
+ # rubocop:enable Migration/PreventStrings
end
diff --git a/db/migrate/20190918104731_add_cleanup_status_to_cluster.rb b/db/migrate/20190918104731_add_cleanup_status_to_cluster.rb
index 0ba9d8e6c89..62290fb0fa6 100644
--- a/db/migrate/20190918104731_add_cleanup_status_to_cluster.rb
+++ b/db/migrate/20190918104731_add_cleanup_status_to_cluster.rb
@@ -9,7 +9,7 @@ class AddCleanupStatusToCluster < ActiveRecord::Migration[5.2]
disable_ddl_transaction!
def up
- add_column_with_default(:clusters, :cleanup_status,
+ add_column_with_default(:clusters, :cleanup_status, # rubocop:disable Migration/AddColumnWithDefault
:smallint,
default: 1,
allow_null: false)
diff --git a/db/migrate/20190918121135_add_cleanup_status_reason_to_cluster.rb b/db/migrate/20190918121135_add_cleanup_status_reason_to_cluster.rb
index 4e71905e3a3..85293ebbe86 100644
--- a/db/migrate/20190918121135_add_cleanup_status_reason_to_cluster.rb
+++ b/db/migrate/20190918121135_add_cleanup_status_reason_to_cluster.rb
@@ -7,6 +7,6 @@ class AddCleanupStatusReasonToCluster < ActiveRecord::Migration[5.2]
DOWNTIME = false
def change
- add_column :clusters, :cleanup_status_reason, :text
+ add_column :clusters, :cleanup_status_reason, :text # rubocop:disable Migration/AddLimitToTextColumns
end
end
diff --git a/db/migrate/20190920224341_create_merge_request_context_commits_and_diffs.rb b/db/migrate/20190920224341_create_merge_request_context_commits_and_diffs.rb
index dcc9da670f7..bd97d3d3183 100644
--- a/db/migrate/20190920224341_create_merge_request_context_commits_and_diffs.rb
+++ b/db/migrate/20190920224341_create_merge_request_context_commits_and_diffs.rb
@@ -6,6 +6,8 @@
class CreateMergeRequestContextCommitsAndDiffs < ActiveRecord::Migration[5.2]
DOWNTIME = false
+ # rubocop:disable Migration/PreventStrings
+ # rubocop:disable Migration/AddLimitToTextColumns
def change
create_table :merge_request_context_commits do |t|
t.references :merge_request, foreign_key: { on_delete: :cascade }
@@ -38,4 +40,6 @@ class CreateMergeRequestContextCommitsAndDiffs < ActiveRecord::Migration[5.2]
t.index [:merge_request_context_commit_id, :sha], name: 'idx_mr_cc_diff_files_on_mr_cc_id_and_sha'
end
end
+ # rubocop:enable Migration/AddLimitToTextColumns
+ # rubocop:enable Migration/PreventStrings
end
diff --git a/db/migrate/20190924124627_add_pull_mirror_branch_prefix_to_projects.rb b/db/migrate/20190924124627_add_pull_mirror_branch_prefix_to_projects.rb
index f1af925c421..2750468834f 100644
--- a/db/migrate/20190924124627_add_pull_mirror_branch_prefix_to_projects.rb
+++ b/db/migrate/20190924124627_add_pull_mirror_branch_prefix_to_projects.rb
@@ -3,7 +3,11 @@
class AddPullMirrorBranchPrefixToProjects < ActiveRecord::Migration[5.2]
DOWNTIME = false
+ # rubocop:disable Migration/AddColumnsToWideTables
+ # rubocop:disable Migration/PreventStrings
def change
- add_column :projects, :pull_mirror_branch_prefix, :string, limit: 50 # rubocop:disable Migration/AddColumnsToWideTables
+ add_column :projects, :pull_mirror_branch_prefix, :string, limit: 50
end
+ # rubocop:enable Migration/PreventStrings
+ # rubocop:enable Migration/AddColumnsToWideTables
end
diff --git a/db/migrate/20190926225633_create_x509_signatures.rb b/db/migrate/20190926225633_create_x509_signatures.rb
index 88f6b03afba..40d2c351524 100644
--- a/db/migrate/20190926225633_create_x509_signatures.rb
+++ b/db/migrate/20190926225633_create_x509_signatures.rb
@@ -6,6 +6,7 @@
class CreateX509Signatures < ActiveRecord::Migration[5.2]
DOWNTIME = false
+ # rubocop:disable Migration/PreventStrings
def change
create_table :x509_issuers do |t|
t.timestamps_with_timezone null: false
@@ -38,4 +39,5 @@ class CreateX509Signatures < ActiveRecord::Migration[5.2]
t.integer :verification_status, limit: 2, default: 0, null: false
end
end
+ # rubocop:enable Migration/PreventStrings
end
diff --git a/db/migrate/20190927055500_create_description_versions.rb b/db/migrate/20190927055500_create_description_versions.rb
index 6ad34d4a89e..b3082533a6f 100644
--- a/db/migrate/20190927055500_create_description_versions.rb
+++ b/db/migrate/20190927055500_create_description_versions.rb
@@ -11,7 +11,7 @@ class CreateDescriptionVersions < ActiveRecord::Migration[5.2]
t.references :issue, index: false, foreign_key: { on_delete: :cascade }, type: :integer
t.references :merge_request, index: false, foreign_key: { on_delete: :cascade }, type: :integer
t.references :epic, index: false, foreign_key: { on_delete: :cascade }, type: :integer
- t.text :description
+ t.text :description # rubocop:disable Migration/AddLimitToTextColumns
end
add_index :description_versions, :issue_id, where: 'issue_id IS NOT NULL'
diff --git a/db/migrate/20190929180751_create_vulnerabilities.rb b/db/migrate/20190929180751_create_vulnerabilities.rb
index aea018c5979..e4c53b1063a 100644
--- a/db/migrate/20190929180751_create_vulnerabilities.rb
+++ b/db/migrate/20190929180751_create_vulnerabilities.rb
@@ -8,6 +8,7 @@ class CreateVulnerabilities < ActiveRecord::Migration[5.2]
DOWNTIME = false
+ # rubocop:disable Migration/AddLimitToTextColumns
def change
create_table :vulnerabilities do |t|
t.bigint "milestone_id"
@@ -30,10 +31,11 @@ class CreateVulnerabilities < ActiveRecord::Migration[5.2]
t.integer "confidence", limit: 2, null: false # auto-calculated as lowest-confidence finding, but overrideable
t.boolean "severity_overridden", default: false
t.boolean "confidence_overridden", default: false
- t.string "title", limit: 255, null: false
+ t.string "title", limit: 255, null: false # rubocop:disable Migration/PreventStrings
t.text "title_html", null: false
t.text "description"
t.text "description_html"
end
end
+ # rubocop:enable Migration/AddLimitToTextColumns
end
diff --git a/db/migrate/20190930153535_create_zoom_meetings.rb b/db/migrate/20190930153535_create_zoom_meetings.rb
index 6b92c53da79..8bc9f0e89de 100644
--- a/db/migrate/20190930153535_create_zoom_meetings.rb
+++ b/db/migrate/20190930153535_create_zoom_meetings.rb
@@ -15,7 +15,7 @@ class CreateZoomMeetings < ActiveRecord::Migration[5.2]
null: false
t.timestamps_with_timezone null: false
t.integer :issue_status, limit: 2, default: 1, null: false
- t.string :url, limit: 255
+ t.string :url, limit: 255 # rubocop:disable Migration/PreventStrings
t.index [:issue_id, :issue_status], unique: true,
where: "issue_status = #{ZOOM_MEETING_STATUS_ADDED}"
diff --git a/db/migrate/20191001170300_create_ci_ref.rb b/db/migrate/20191001170300_create_ci_ref.rb
index af25e67430b..feeb8517e6e 100644
--- a/db/migrate/20191001170300_create_ci_ref.rb
+++ b/db/migrate/20191001170300_create_ci_ref.rb
@@ -3,6 +3,7 @@
class CreateCiRef < ActiveRecord::Migration[5.2]
DOWNTIME = false
+ # rubocop:disable Migration/PreventStrings
def change
create_table :ci_refs do |t|
t.references :project, null: false, index: false, foreign_key: { on_delete: :cascade }, type: :integer
@@ -16,4 +17,5 @@ class CreateCiRef < ActiveRecord::Migration[5.2]
t.index [:last_updated_by_pipeline_id]
end
end
+ # rubocop:enable Migration/PreventStrings
end
diff --git a/db/migrate/20191002123516_create_clusters_applications_elastic_stack.rb b/db/migrate/20191002123516_create_clusters_applications_elastic_stack.rb
index 8910dc0d9fb..e1236423672 100644
--- a/db/migrate/20191002123516_create_clusters_applications_elastic_stack.rb
+++ b/db/migrate/20191002123516_create_clusters_applications_elastic_stack.rb
@@ -8,6 +8,7 @@ class CreateClustersApplicationsElasticStack < ActiveRecord::Migration[5.2]
DOWNTIME = false
+ # rubocop:disable Migration/PreventStrings
def change
create_table :clusters_applications_elastic_stacks do |t|
t.timestamps_with_timezone null: false
@@ -15,8 +16,9 @@ class CreateClustersApplicationsElasticStack < ActiveRecord::Migration[5.2]
t.integer :status, null: false
t.string :version, null: false, limit: 255
t.string :kibana_hostname, limit: 255
- t.text :status_reason
+ t.text :status_reason # rubocop:disable Migration/AddLimitToTextColumns
t.index :cluster_id, unique: true
end
end
+ # rubocop:enable Migration/PreventStrings
end
diff --git a/db/migrate/20191003015155_add_self_managed_prometheus_alerts.rb b/db/migrate/20191003015155_add_self_managed_prometheus_alerts.rb
index 71d10153422..eec6f518257 100644
--- a/db/migrate/20191003015155_add_self_managed_prometheus_alerts.rb
+++ b/db/migrate/20191003015155_add_self_managed_prometheus_alerts.rb
@@ -3,6 +3,7 @@
class AddSelfManagedPrometheusAlerts < ActiveRecord::Migration[5.2]
DOWNTIME = false
+ # rubocop:disable Migration/PreventStrings
def change
create_table :self_managed_prometheus_alert_events do |t|
t.references :project, index: false, foreign_key: { on_delete: :cascade }, null: false
@@ -17,4 +18,5 @@ class AddSelfManagedPrometheusAlerts < ActiveRecord::Migration[5.2]
t.index [:project_id, :payload_key], unique: true, name: 'idx_project_id_payload_key_self_managed_prometheus_alert_events'
end
end
+ # rubocop:enable Migration/PreventStrings
end
diff --git a/db/migrate/20191003060227_add_push_event_hooks_limit_to_application_settings.rb b/db/migrate/20191003060227_add_push_event_hooks_limit_to_application_settings.rb
index f107181bbde..4b381dca81b 100644
--- a/db/migrate/20191003060227_add_push_event_hooks_limit_to_application_settings.rb
+++ b/db/migrate/20191003060227_add_push_event_hooks_limit_to_application_settings.rb
@@ -8,7 +8,7 @@ class AddPushEventHooksLimitToApplicationSettings < ActiveRecord::Migration[5.2]
disable_ddl_transaction!
def up
- add_column_with_default(:application_settings, :push_event_hooks_limit, :integer, default: 3)
+ add_column_with_default(:application_settings, :push_event_hooks_limit, :integer, default: 3) # rubocop:disable Migration/AddColumnWithDefault
end
def down
diff --git a/db/migrate/20191003064615_create_aws_roles.rb b/db/migrate/20191003064615_create_aws_roles.rb
index ee35953f558..960e9b28053 100644
--- a/db/migrate/20191003064615_create_aws_roles.rb
+++ b/db/migrate/20191003064615_create_aws_roles.rb
@@ -6,6 +6,7 @@
class CreateAwsRoles < ActiveRecord::Migration[5.2]
DOWNTIME = false
+ # rubocop:disable Migration/PreventStrings
def change
create_table :aws_roles, id: false do |t|
t.references :user, primary_key: true, default: nil, type: :integer, index: { unique: true }, foreign_key: { on_delete: :cascade }
@@ -18,4 +19,5 @@ class CreateAwsRoles < ActiveRecord::Migration[5.2]
t.index :role_external_id, unique: true
end
end
+ # rubocop:enable Migration/PreventStrings
end
diff --git a/db/migrate/20191003195218_add_pendo_enabled_to_application_settings.rb b/db/migrate/20191003195218_add_pendo_enabled_to_application_settings.rb
index c5f5a8cd70c..29ae831d4f4 100644
--- a/db/migrate/20191003195218_add_pendo_enabled_to_application_settings.rb
+++ b/db/migrate/20191003195218_add_pendo_enabled_to_application_settings.rb
@@ -6,7 +6,7 @@ class AddPendoEnabledToApplicationSettings < ActiveRecord::Migration[5.2]
disable_ddl_transaction!
def up
- add_column_with_default :application_settings, :pendo_enabled, :boolean, default: false, allow_null: false
+ add_column_with_default :application_settings, :pendo_enabled, :boolean, default: false, allow_null: false # rubocop:disable Migration/AddColumnWithDefault
end
def down
diff --git a/db/migrate/20191003195620_add_pendo_url_to_application_settings.rb b/db/migrate/20191003195620_add_pendo_url_to_application_settings.rb
index cc0895f8bee..6763cb5544c 100644
--- a/db/migrate/20191003195620_add_pendo_url_to_application_settings.rb
+++ b/db/migrate/20191003195620_add_pendo_url_to_application_settings.rb
@@ -3,7 +3,9 @@ class AddPendoUrlToApplicationSettings < ActiveRecord::Migration[5.2]
DOWNTIME = false
+ # rubocop:disable Migration/PreventStrings
def change
add_column :application_settings, :pendo_url, :string, limit: 255
end
+ # rubocop:enable Migration/PreventStrings
end
diff --git a/db/migrate/20191008013056_add_push_event_activities_limit_to_application_settings.rb b/db/migrate/20191008013056_add_push_event_activities_limit_to_application_settings.rb
index 84befc95d00..af206bcb8d0 100644
--- a/db/migrate/20191008013056_add_push_event_activities_limit_to_application_settings.rb
+++ b/db/migrate/20191008013056_add_push_event_activities_limit_to_application_settings.rb
@@ -8,7 +8,7 @@ class AddPushEventActivitiesLimitToApplicationSettings < ActiveRecord::Migration
disable_ddl_transaction!
def up
- add_column_with_default(:application_settings, :push_event_activities_limit, :integer, default: 3)
+ add_column_with_default(:application_settings, :push_event_activities_limit, :integer, default: 3) # rubocop:disable Migration/AddColumnWithDefault
end
def down
diff --git a/db/migrate/20191009222222_add_custom_http_clone_url_root_to_application_settings.rb b/db/migrate/20191009222222_add_custom_http_clone_url_root_to_application_settings.rb
index 0fa8ff449f7..41876ee2cdf 100644
--- a/db/migrate/20191009222222_add_custom_http_clone_url_root_to_application_settings.rb
+++ b/db/migrate/20191009222222_add_custom_http_clone_url_root_to_application_settings.rb
@@ -7,7 +7,9 @@ class AddCustomHttpCloneUrlRootToApplicationSettings < ActiveRecord::Migration[5
# Set this constant to true if this migration requires downtime.
DOWNTIME = false
+ # rubocop:disable Migration/PreventStrings
def change
add_column :application_settings, :custom_http_clone_url_root, :string, limit: 511
end
+ # rubocop:enable Migration/PreventStrings
end
diff --git a/db/migrate/20191010174846_add_snowplow_iglu_registry_url_to_application_settings.rb b/db/migrate/20191010174846_add_snowplow_iglu_registry_url_to_application_settings.rb
index a40ce8dbee5..8f882bcaa50 100644
--- a/db/migrate/20191010174846_add_snowplow_iglu_registry_url_to_application_settings.rb
+++ b/db/migrate/20191010174846_add_snowplow_iglu_registry_url_to_application_settings.rb
@@ -3,7 +3,9 @@
class AddSnowplowIgluRegistryUrlToApplicationSettings < ActiveRecord::Migration[5.2]
DOWNTIME = false
+ # rubocop:disable Migration/PreventStrings
def change
add_column :application_settings, :snowplow_iglu_registry_url, :string, limit: 255
end
+ # rubocop:enable Migration/PreventStrings
end
diff --git a/db/migrate/20191014123159_add_expire_notification_delivered_to_personal_access_tokens.rb b/db/migrate/20191014123159_add_expire_notification_delivered_to_personal_access_tokens.rb
index f172d3bdcbd..41a81e3ac87 100644
--- a/db/migrate/20191014123159_add_expire_notification_delivered_to_personal_access_tokens.rb
+++ b/db/migrate/20191014123159_add_expire_notification_delivered_to_personal_access_tokens.rb
@@ -8,7 +8,7 @@ class AddExpireNotificationDeliveredToPersonalAccessTokens < ActiveRecord::Migra
disable_ddl_transaction!
def up
- add_column_with_default :personal_access_tokens, :expire_notification_delivered, :boolean, default: false
+ add_column_with_default :personal_access_tokens, :expire_notification_delivered, :boolean, default: false # rubocop:disable Migration/AddColumnWithDefault
end
def down
diff --git a/db/migrate/20191017191341_create_clusters_applications_crossplane.rb b/db/migrate/20191017191341_create_clusters_applications_crossplane.rb
index 8dc25c56116..42f980ed99d 100644
--- a/db/migrate/20191017191341_create_clusters_applications_crossplane.rb
+++ b/db/migrate/20191017191341_create_clusters_applications_crossplane.rb
@@ -5,6 +5,7 @@ class CreateClustersApplicationsCrossplane < ActiveRecord::Migration[5.2]
DOWNTIME = false
+ # rubocop:disable Migration/PreventStrings
def change
create_table :clusters_applications_crossplane do |t|
t.timestamps_with_timezone null: false
@@ -12,8 +13,9 @@ class CreateClustersApplicationsCrossplane < ActiveRecord::Migration[5.2]
t.integer :status, null: false
t.string :version, null: false, limit: 255
t.string :stack, null: false, limit: 255
- t.text :status_reason
+ t.text :status_reason # rubocop:disable Migration/AddLimitToTextColumns
t.index :cluster_id, unique: true
end
end
+ # rubocop:enable Migration/PreventStrings
end
diff --git a/db/migrate/20191025092748_add_secret_token_to_snippet.rb b/db/migrate/20191025092748_add_secret_token_to_snippet.rb
index 0649f58d23e..c43c1fbe2b7 100644
--- a/db/migrate/20191025092748_add_secret_token_to_snippet.rb
+++ b/db/migrate/20191025092748_add_secret_token_to_snippet.rb
@@ -3,8 +3,10 @@
class AddSecretTokenToSnippet < ActiveRecord::Migration[5.2]
DOWNTIME = false
+ # rubocop:disable Migration/PreventStrings
def change
add_column :snippets, :encrypted_secret_token, :string, limit: 255
add_column :snippets, :encrypted_secret_token_iv, :string, limit: 255
end
+ # rubocop:enable Migration/PreventStrings
end
diff --git a/db/migrate/20191028130054_add_max_issue_weight_to_list.rb b/db/migrate/20191028130054_add_max_issue_weight_to_list.rb
index eec7c42c907..f15b65067f6 100644
--- a/db/migrate/20191028130054_add_max_issue_weight_to_list.rb
+++ b/db/migrate/20191028130054_add_max_issue_weight_to_list.rb
@@ -8,7 +8,7 @@ class AddMaxIssueWeightToList < ActiveRecord::Migration[5.2]
DOWNTIME = false
def up
- add_column_with_default :lists, :max_issue_weight, :integer, default: 0
+ add_column_with_default :lists, :max_issue_weight, :integer, default: 0 # rubocop:disable Migration/AddColumnWithDefault
end
def down
diff --git a/db/migrate/20191029125305_create_packages_conan_metadata.rb b/db/migrate/20191029125305_create_packages_conan_metadata.rb
index c6abc509e41..fa14b73e19b 100644
--- a/db/migrate/20191029125305_create_packages_conan_metadata.rb
+++ b/db/migrate/20191029125305_create_packages_conan_metadata.rb
@@ -5,6 +5,7 @@ class CreatePackagesConanMetadata < ActiveRecord::Migration[5.2]
DOWNTIME = false
+ # rubocop:disable Migration/PreventStrings
def change
create_table :packages_conan_metadata do |t|
t.references :package, index: { unique: true }, null: false, foreign_key: { to_table: :packages_packages, on_delete: :cascade }, type: :bigint
@@ -13,4 +14,5 @@ class CreatePackagesConanMetadata < ActiveRecord::Migration[5.2]
t.string "package_channel", null: false, limit: 255
end
end
+ # rubocop:enable Migration/PreventStrings
end
diff --git a/db/migrate/20191029191901_add_enabled_to_grafana_integrations.rb b/db/migrate/20191029191901_add_enabled_to_grafana_integrations.rb
index 8db11724874..40e361e2150 100644
--- a/db/migrate/20191029191901_add_enabled_to_grafana_integrations.rb
+++ b/db/migrate/20191029191901_add_enabled_to_grafana_integrations.rb
@@ -8,7 +8,7 @@ class AddEnabledToGrafanaIntegrations < ActiveRecord::Migration[5.2]
disable_ddl_transaction!
def up
- add_column_with_default(
+ add_column_with_default( # rubocop:disable Migration/AddColumnWithDefault
:grafana_integrations,
:enabled,
:boolean,
diff --git a/db/migrate/20191103202505_add_eks_credentials_to_application_settings.rb b/db/migrate/20191103202505_add_eks_credentials_to_application_settings.rb
index 3a167b4c67f..5ed3e7edb3c 100644
--- a/db/migrate/20191103202505_add_eks_credentials_to_application_settings.rb
+++ b/db/migrate/20191103202505_add_eks_credentials_to_application_settings.rb
@@ -4,6 +4,8 @@ class AddEksCredentialsToApplicationSettings < ActiveRecord::Migration[5.2]
# Set this constant to true if this migration requires downtime.
DOWNTIME = false
+ # rubocop:disable Migration/PreventStrings
+ # rubocop:disable Migration/AddLimitToTextColumns
def change
add_column :application_settings, :eks_integration_enabled, :boolean, null: false, default: false
add_column :application_settings, :eks_account_id, :string, limit: 128
@@ -11,4 +13,6 @@ class AddEksCredentialsToApplicationSettings < ActiveRecord::Migration[5.2]
add_column :application_settings, :encrypted_eks_secret_access_key_iv, :string, limit: 255
add_column :application_settings, :encrypted_eks_secret_access_key, :text
end
+ # rubocop:enable Migration/AddLimitToTextColumns
+ # rubocop:enable Migration/PreventStrings
end
diff --git a/db/migrate/20191105134413_create_service_desk_settings.rb b/db/migrate/20191105134413_create_service_desk_settings.rb
index ee026b6f26d..dc8268f1c90 100644
--- a/db/migrate/20191105134413_create_service_desk_settings.rb
+++ b/db/migrate/20191105134413_create_service_desk_settings.rb
@@ -3,6 +3,7 @@
class CreateServiceDeskSettings < ActiveRecord::Migration[5.2]
DOWNTIME = false
+ # rubocop:disable Migration/PreventStrings
def change
create_table :service_desk_settings, id: false do |t|
t.references :project,
@@ -15,4 +16,5 @@ class CreateServiceDeskSettings < ActiveRecord::Migration[5.2]
t.string :issue_template_key, limit: 255
end
end
+ # rubocop:enable Migration/PreventStrings
end
diff --git a/db/migrate/20191105155113_add_secret_to_snippet.rb b/db/migrate/20191105155113_add_secret_to_snippet.rb
index ae514d48494..8f0a330238b 100644
--- a/db/migrate/20191105155113_add_secret_to_snippet.rb
+++ b/db/migrate/20191105155113_add_secret_to_snippet.rb
@@ -9,7 +9,7 @@ class AddSecretToSnippet < ActiveRecord::Migration[5.2]
def up
unless column_exists?(:snippets, :secret)
- add_column_with_default :snippets, :secret, :boolean, default: false
+ add_column_with_default :snippets, :secret, :boolean, default: false # rubocop:disable Migration/AddColumnWithDefault
end
add_concurrent_index :snippets, [:visibility_level, :secret]
diff --git a/db/migrate/20191106144901_add_state_to_merge_trains.rb b/db/migrate/20191106144901_add_state_to_merge_trains.rb
index e2256705f53..64a70575c91 100644
--- a/db/migrate/20191106144901_add_state_to_merge_trains.rb
+++ b/db/migrate/20191106144901_add_state_to_merge_trains.rb
@@ -9,7 +9,7 @@ class AddStateToMergeTrains < ActiveRecord::Migration[5.2]
disable_ddl_transaction!
def up
- add_column_with_default :merge_trains, :status, :integer, limit: 2, default: MERGE_TRAIN_STATUS_CREATED
+ add_column_with_default :merge_trains, :status, :integer, limit: 2, default: MERGE_TRAIN_STATUS_CREATED # rubocop:disable Migration/AddColumnWithDefault
end
def down
diff --git a/db/migrate/20191111121500_default_ci_config_path.rb b/db/migrate/20191111121500_default_ci_config_path.rb
index f391f5ffe99..3914058d35b 100644
--- a/db/migrate/20191111121500_default_ci_config_path.rb
+++ b/db/migrate/20191111121500_default_ci_config_path.rb
@@ -3,9 +3,11 @@
class DefaultCiConfigPath < ActiveRecord::Migration[5.2]
DOWNTIME = false
+ # rubocop:disable Migration/PreventStrings
def up
add_column :application_settings, :default_ci_config_path, :string, limit: 255
end
+ # rubocop:enable Migration/PreventStrings
def down
remove_column :application_settings, :default_ci_config_path
diff --git a/db/migrate/20191112090226_add_artifacts_to_ci_build_need.rb b/db/migrate/20191112090226_add_artifacts_to_ci_build_need.rb
index 2fbd003b2e5..b868e0b44a8 100644
--- a/db/migrate/20191112090226_add_artifacts_to_ci_build_need.rb
+++ b/db/migrate/20191112090226_add_artifacts_to_ci_build_need.rb
@@ -8,7 +8,7 @@ class AddArtifactsToCiBuildNeed < ActiveRecord::Migration[5.2]
disable_ddl_transaction!
def up
- add_column_with_default(:ci_build_needs, :artifacts,
+ add_column_with_default(:ci_build_needs, :artifacts, # rubocop:disable Migration/AddColumnWithDefault
:boolean,
default: true,
allow_null: false)
diff --git a/db/migrate/20191119231621_create_container_expiration_policies.rb b/db/migrate/20191119231621_create_container_expiration_policies.rb
index d7108870cf1..d06ae659f41 100644
--- a/db/migrate/20191119231621_create_container_expiration_policies.rb
+++ b/db/migrate/20191119231621_create_container_expiration_policies.rb
@@ -3,6 +3,7 @@
class CreateContainerExpirationPolicies < ActiveRecord::Migration[5.2]
DOWNTIME = false
+ # rubocop:disable Migration/PreventStrings
def change
create_table :container_expiration_policies, id: false, primary_key: :project_id do |t|
t.timestamps_with_timezone null: false
@@ -18,4 +19,5 @@ class CreateContainerExpirationPolicies < ActiveRecord::Migration[5.2]
add_index :container_expiration_policies, [:next_run_at, :enabled],
name: 'index_container_expiration_policies_on_next_run_at_and_enabled'
end
+ # rubocop:enable Migration/PreventStrings
end
diff --git a/db/migrate/20191120084627_add_encrypted_fields_to_application_settings.rb b/db/migrate/20191120084627_add_encrypted_fields_to_application_settings.rb
index 4e0886a5121..7bb2d9c6301 100644
--- a/db/migrate/20191120084627_add_encrypted_fields_to_application_settings.rb
+++ b/db/migrate/20191120084627_add_encrypted_fields_to_application_settings.rb
@@ -14,12 +14,16 @@ class AddEncryptedFieldsToApplicationSettings < ActiveRecord::Migration[5.2]
slack_app_verification_token
].freeze
+ # rubocop:disable Migration/PreventStrings
+ # rubocop:disable Migration/AddLimitToTextColumns
def up
PLAINTEXT_ATTRIBUTES.each do |plaintext_attribute|
add_column :application_settings, "encrypted_#{plaintext_attribute}", :text
add_column :application_settings, "encrypted_#{plaintext_attribute}_iv", :string, limit: 255
end
end
+ # rubocop:enable Migration/AddLimitToTextColumns
+ # rubocop:enable Migration/PreventStrings
def down
PLAINTEXT_ATTRIBUTES.each do |plaintext_attribute|
diff --git a/db/migrate/20191121111621_create_packages_dependencies.rb b/db/migrate/20191121111621_create_packages_dependencies.rb
index 61e52627b05..29a8434514e 100644
--- a/db/migrate/20191121111621_create_packages_dependencies.rb
+++ b/db/migrate/20191121111621_create_packages_dependencies.rb
@@ -3,6 +3,7 @@
class CreatePackagesDependencies < ActiveRecord::Migration[5.2]
DOWNTIME = false
+ # rubocop:disable Migration/PreventStrings
def change
create_table :packages_dependencies do |t|
t.string :name, null: false, limit: 255
@@ -11,4 +12,5 @@ class CreatePackagesDependencies < ActiveRecord::Migration[5.2]
add_index :packages_dependencies, [:name, :version_pattern], unique: true
end
+ # rubocop:enable Migration/PreventStrings
end
diff --git a/db/migrate/20191121193110_add_issue_links_type.rb b/db/migrate/20191121193110_add_issue_links_type.rb
index 61ef2e7d7e8..86bfd41b916 100644
--- a/db/migrate/20191121193110_add_issue_links_type.rb
+++ b/db/migrate/20191121193110_add_issue_links_type.rb
@@ -8,7 +8,7 @@ class AddIssueLinksType < ActiveRecord::Migration[5.1]
disable_ddl_transaction!
def up
- add_column_with_default :issue_links, :link_type, :integer, default: 0, limit: 2
+ add_column_with_default :issue_links, :link_type, :integer, default: 0, limit: 2 # rubocop:disable Migration/AddColumnWithDefault
end
def down
diff --git a/db/migrate/20191125133353_add_target_path_to_broadcast_message.rb b/db/migrate/20191125133353_add_target_path_to_broadcast_message.rb
index 65aa758e502..8597cce5b19 100644
--- a/db/migrate/20191125133353_add_target_path_to_broadcast_message.rb
+++ b/db/migrate/20191125133353_add_target_path_to_broadcast_message.rb
@@ -3,7 +3,9 @@
class AddTargetPathToBroadcastMessage < ActiveRecord::Migration[5.2]
DOWNTIME = false
+ # rubocop:disable Migration/PreventStrings
def change
add_column :broadcast_messages, :target_path, :string, limit: 255
end
+ # rubocop:enable Migration/PreventStrings
end
diff --git a/db/migrate/20191125140458_create_import_failures.rb b/db/migrate/20191125140458_create_import_failures.rb
index 43e8efe90a4..e5dbcf5d41d 100644
--- a/db/migrate/20191125140458_create_import_failures.rb
+++ b/db/migrate/20191125140458_create_import_failures.rb
@@ -3,6 +3,7 @@
class CreateImportFailures < ActiveRecord::Migration[5.2]
DOWNTIME = false
+ # rubocop:disable Migration/PreventStrings
def change
create_table :import_failures do |t|
t.integer :relation_index
@@ -14,4 +15,5 @@ class CreateImportFailures < ActiveRecord::Migration[5.2]
t.string :exception_message, limit: 255
end
end
+ # rubocop:enable Migration/PreventStrings
end
diff --git a/db/migrate/20191127030005_create_serverless_domain_cluster.rb b/db/migrate/20191127030005_create_serverless_domain_cluster.rb
index 7fb24400b0d..c65301bf133 100644
--- a/db/migrate/20191127030005_create_serverless_domain_cluster.rb
+++ b/db/migrate/20191127030005_create_serverless_domain_cluster.rb
@@ -3,6 +3,7 @@
class CreateServerlessDomainCluster < ActiveRecord::Migration[5.2]
DOWNTIME = false
+ # rubocop:disable Migration/PreventStrings
def change
create_table :serverless_domain_cluster, id: false, primary_key: :uuid do |t|
t.references :pages_domain, null: false, foreign_key: { on_delete: :cascade }
@@ -14,4 +15,5 @@ class CreateServerlessDomainCluster < ActiveRecord::Migration[5.2]
t.string :uuid, null: false, limit: 14, primary_key: true
end
end
+ # rubocop:enable Migration/PreventStrings
end
diff --git a/db/migrate/20191127163053_add_confidential_to_doorkeeper_application.rb b/db/migrate/20191127163053_add_confidential_to_doorkeeper_application.rb
index 1fb02085c37..12e22b4744c 100644
--- a/db/migrate/20191127163053_add_confidential_to_doorkeeper_application.rb
+++ b/db/migrate/20191127163053_add_confidential_to_doorkeeper_application.rb
@@ -8,7 +8,7 @@ class AddConfidentialToDoorkeeperApplication < ActiveRecord::Migration[5.2]
disable_ddl_transaction!
def up
- add_column_with_default(
+ add_column_with_default( # rubocop:disable Migration/AddColumnWithDefault
:oauth_applications,
:confidential,
:boolean,
diff --git a/db/migrate/20191127221608_add_wildcard_and_domain_type_to_pages_domains.rb b/db/migrate/20191127221608_add_wildcard_and_domain_type_to_pages_domains.rb
index 6893a02bcad..96a2e8275c6 100644
--- a/db/migrate/20191127221608_add_wildcard_and_domain_type_to_pages_domains.rb
+++ b/db/migrate/20191127221608_add_wildcard_and_domain_type_to_pages_domains.rb
@@ -9,8 +9,8 @@ class AddWildcardAndDomainTypeToPagesDomains < ActiveRecord::Migration[5.2]
disable_ddl_transaction!
def up
- add_column_with_default :pages_domains, :wildcard, :boolean, default: false
- add_column_with_default :pages_domains, :domain_type, :integer, limit: 2, default: PROJECT_TYPE
+ add_column_with_default :pages_domains, :wildcard, :boolean, default: false # rubocop:disable Migration/AddColumnWithDefault
+ add_column_with_default :pages_domains, :domain_type, :integer, limit: 2, default: PROJECT_TYPE # rubocop:disable Migration/AddColumnWithDefault
end
def down
diff --git a/db/migrate/20191128145231_add_ci_resource_groups.rb b/db/migrate/20191128145231_add_ci_resource_groups.rb
index 8bde0254701..77975120f72 100644
--- a/db/migrate/20191128145231_add_ci_resource_groups.rb
+++ b/db/migrate/20191128145231_add_ci_resource_groups.rb
@@ -7,7 +7,7 @@ class AddCiResourceGroups < ActiveRecord::Migration[5.2]
create_table :ci_resource_groups do |t|
t.timestamps_with_timezone
t.bigint :project_id, null: false
- t.string :key, null: false, limit: 255
+ t.string :key, null: false, limit: 255 # rubocop:disable Migration/PreventStrings
t.index %i[project_id key], unique: true
end
diff --git a/db/migrate/20191129134844_add_broadcast_type_to_broadcast_message.rb b/db/migrate/20191129134844_add_broadcast_type_to_broadcast_message.rb
index 84d17f558d1..884d9ac6d7f 100644
--- a/db/migrate/20191129134844_add_broadcast_type_to_broadcast_message.rb
+++ b/db/migrate/20191129134844_add_broadcast_type_to_broadcast_message.rb
@@ -10,7 +10,7 @@ class AddBroadcastTypeToBroadcastMessage < ActiveRecord::Migration[5.2]
disable_ddl_transaction!
def up
- add_column_with_default(:broadcast_messages, :broadcast_type, :smallint, default: BROADCAST_MESSAGE_BANNER_TYPE)
+ add_column_with_default(:broadcast_messages, :broadcast_type, :smallint, default: BROADCAST_MESSAGE_BANNER_TYPE) # rubocop:disable Migration/AddColumnWithDefault
end
def down
diff --git a/db/migrate/20191202181924_add_environment_auto_stop_in_to_ci_builds_metadata.rb b/db/migrate/20191202181924_add_environment_auto_stop_in_to_ci_builds_metadata.rb
index bce191a7ec0..c09196da91b 100644
--- a/db/migrate/20191202181924_add_environment_auto_stop_in_to_ci_builds_metadata.rb
+++ b/db/migrate/20191202181924_add_environment_auto_stop_in_to_ci_builds_metadata.rb
@@ -3,9 +3,11 @@
class AddEnvironmentAutoStopInToCiBuildsMetadata < ActiveRecord::Migration[5.2]
DOWNTIME = false
+ # rubocop:disable Migration/PreventStrings
def up
add_column :ci_builds_metadata, :environment_auto_stop_in, :string, limit: 255
end
+ # rubocop:enable Migration/PreventStrings
def down
remove_column :ci_builds_metadata, :environment_auto_stop_in
diff --git a/db/migrate/20191204192726_add_design_disk_path_to_geo_hashed_storage_migrated_events.rb b/db/migrate/20191204192726_add_design_disk_path_to_geo_hashed_storage_migrated_events.rb
index 39b5d5c7e97..8631502ded8 100644
--- a/db/migrate/20191204192726_add_design_disk_path_to_geo_hashed_storage_migrated_events.rb
+++ b/db/migrate/20191204192726_add_design_disk_path_to_geo_hashed_storage_migrated_events.rb
@@ -5,8 +5,10 @@ class AddDesignDiskPathToGeoHashedStorageMigratedEvents < ActiveRecord::Migratio
DOWNTIME = false
+ # rubocop:disable Migration/AddLimitToTextColumns
def change
add_column :geo_hashed_storage_migrated_events, :old_design_disk_path, :text
add_column :geo_hashed_storage_migrated_events, :new_design_disk_path, :text
end
+ # rubocop:enable Migration/AddLimitToTextColumns
end
diff --git a/db/migrate/20191206014412_add_image_to_design_management_designs_versions.rb b/db/migrate/20191206014412_add_image_to_design_management_designs_versions.rb
index d8e2269d21a..54ef9b74f29 100644
--- a/db/migrate/20191206014412_add_image_to_design_management_designs_versions.rb
+++ b/db/migrate/20191206014412_add_image_to_design_management_designs_versions.rb
@@ -3,7 +3,9 @@
class AddImageToDesignManagementDesignsVersions < ActiveRecord::Migration[6.0]
DOWNTIME = false
+ # rubocop:disable Migration/PreventStrings
def change
add_column :design_management_designs_versions, :image_v432x230, :string, limit: 255
end
+ # rubocop:enable Migration/PreventStrings
end
diff --git a/db/migrate/20191208110214_add_suggestion_commit_message_to_projects.rb b/db/migrate/20191208110214_add_suggestion_commit_message_to_projects.rb
index cb897edde8e..3c96a1737c2 100644
--- a/db/migrate/20191208110214_add_suggestion_commit_message_to_projects.rb
+++ b/db/migrate/20191208110214_add_suggestion_commit_message_to_projects.rb
@@ -3,7 +3,11 @@
class AddSuggestionCommitMessageToProjects < ActiveRecord::Migration[5.2]
DOWNTIME = false
+ # rubocop:disable Migration/AddColumnsToWideTables
+ # rubocop:disable Migration/PreventStrings
def change
- add_column :projects, :suggestion_commit_message, :string, limit: 255 # rubocop:disable Migration/AddColumnsToWideTables
+ add_column :projects, :suggestion_commit_message, :string, limit: 255
end
+ # rubocop:enable Migration/PreventStrings
+ # rubocop:enable Migration/AddColumnsToWideTables
end
diff --git a/db/migrate/20191212140117_change_commit_user_mentions_commit_id_column_type.rb b/db/migrate/20191212140117_change_commit_user_mentions_commit_id_column_type.rb
index f30cdab3441..f205de3587e 100644
--- a/db/migrate/20191212140117_change_commit_user_mentions_commit_id_column_type.rb
+++ b/db/migrate/20191212140117_change_commit_user_mentions_commit_id_column_type.rb
@@ -12,6 +12,7 @@ class ChangeCommitUserMentionsCommitIdColumnType < ActiveRecord::Migration[5.2]
NEW_TMP_INDEX = 'temp_commit_id_for_type_change_and_note_id_index'
NEW_INDEX = 'commit_id_and_note_id_index'
+ # rubocop:disable Migration/PreventStrings
def up
# the initial index name is too long and fails during migration. Renaming the index first.
add_concurrent_index :commit_user_mentions, [:commit_id, :note_id], name: OLD_TMP_INDEX
@@ -29,6 +30,7 @@ class ChangeCommitUserMentionsCommitIdColumnType < ActiveRecord::Migration[5.2]
add_concurrent_index :commit_user_mentions, [:commit_id_for_type_change, :note_id], name: NEW_INDEX
remove_concurrent_index_by_name :commit_user_mentions, NEW_TMP_INDEX
end
+ # rubocop:enable Migration/PreventStrings
def down
cleanup_concurrent_column_type_change :commit_user_mentions, :commit_id
diff --git a/db/migrate/20191213104838_add_service_desk_username.rb b/db/migrate/20191213104838_add_service_desk_username.rb
index 945bdb67fa1..d50de4c362c 100644
--- a/db/migrate/20191213104838_add_service_desk_username.rb
+++ b/db/migrate/20191213104838_add_service_desk_username.rb
@@ -3,7 +3,9 @@
class AddServiceDeskUsername < ActiveRecord::Migration[5.2]
DOWNTIME = false
+ # rubocop:disable Migration/PreventStrings
def change
add_column :service_desk_settings, :outgoing_name, :string, limit: 255
end
+ # rubocop:enable Migration/PreventStrings
end
diff --git a/db/migrate/20191213143656_create_ci_pipelines_config.rb b/db/migrate/20191213143656_create_ci_pipelines_config.rb
index 92636f35d01..a1821ee9f66 100644
--- a/db/migrate/20191213143656_create_ci_pipelines_config.rb
+++ b/db/migrate/20191213143656_create_ci_pipelines_config.rb
@@ -8,7 +8,7 @@ class CreateCiPipelinesConfig < ActiveRecord::Migration[5.2]
t.references :pipeline,
primary_key: true,
foreign_key: { to_table: :ci_pipelines, on_delete: :cascade }
- t.text :content, null: false
+ t.text :content, null: false # rubocop:disable Migration/AddLimitToTextColumns
end
end
end
diff --git a/db/migrate/20191217165641_add_saml_provider_prohibited_outer_forks.rb b/db/migrate/20191217165641_add_saml_provider_prohibited_outer_forks.rb
index 6cd32cdcfe9..4b528578848 100644
--- a/db/migrate/20191217165641_add_saml_provider_prohibited_outer_forks.rb
+++ b/db/migrate/20191217165641_add_saml_provider_prohibited_outer_forks.rb
@@ -8,7 +8,7 @@ class AddSamlProviderProhibitedOuterForks < ActiveRecord::Migration[5.2]
disable_ddl_transaction!
def up
- add_column_with_default :saml_providers, :prohibited_outer_forks, :boolean, default: false, allow_null: true
+ add_column_with_default :saml_providers, :prohibited_outer_forks, :boolean, default: false, allow_null: true # rubocop:disable Migration/AddColumnWithDefault
end
def down
diff --git a/db/migrate/20191218084115_add_updating_name_disabled_for_users_to_application_settings.rb b/db/migrate/20191218084115_add_updating_name_disabled_for_users_to_application_settings.rb
index eb9d4ace5b4..d0b2e53650b 100644
--- a/db/migrate/20191218084115_add_updating_name_disabled_for_users_to_application_settings.rb
+++ b/db/migrate/20191218084115_add_updating_name_disabled_for_users_to_application_settings.rb
@@ -8,7 +8,7 @@ class AddUpdatingNameDisabledForUsersToApplicationSettings < ActiveRecord::Migra
disable_ddl_transaction!
def up
- add_column_with_default(:application_settings, :updating_name_disabled_for_users,
+ add_column_with_default(:application_settings, :updating_name_disabled_for_users, # rubocop:disable Migration/AddColumnWithDefault
:boolean,
default: false,
allow_null: false)
diff --git a/db/migrate/20191218124915_add_repository_storage_to_snippets.rb b/db/migrate/20191218124915_add_repository_storage_to_snippets.rb
index ff391c04062..e2ba11aef20 100644
--- a/db/migrate/20191218124915_add_repository_storage_to_snippets.rb
+++ b/db/migrate/20191218124915_add_repository_storage_to_snippets.rb
@@ -7,8 +7,9 @@ class AddRepositoryStorageToSnippets < ActiveRecord::Migration[5.2]
disable_ddl_transaction!
+ # rubocop:disable Migration/PreventStrings
def up
- add_column_with_default(
+ add_column_with_default( # rubocop:disable Migration/AddColumnWithDefault
:snippets,
:repository_storage,
:string,
@@ -17,6 +18,7 @@ class AddRepositoryStorageToSnippets < ActiveRecord::Migration[5.2]
allow_null: false
)
end
+ # rubocop:enable Migration/PreventStrings
def down
remove_column(:snippets, :repository_storage)
diff --git a/db/migrate/20191218125015_add_storage_version_to_snippets.rb b/db/migrate/20191218125015_add_storage_version_to_snippets.rb
index 659f36f42b5..b1bd3589692 100644
--- a/db/migrate/20191218125015_add_storage_version_to_snippets.rb
+++ b/db/migrate/20191218125015_add_storage_version_to_snippets.rb
@@ -8,7 +8,7 @@ class AddStorageVersionToSnippets < ActiveRecord::Migration[5.2]
disable_ddl_transaction!
def up
- add_column_with_default(
+ add_column_with_default( # rubocop:disable Migration/AddColumnWithDefault
:snippets,
:storage_version,
:integer,
diff --git a/db/migrate/20200102140148_add_expanded_environment_name_to_ci_build_metadata.rb b/db/migrate/20200102140148_add_expanded_environment_name_to_ci_build_metadata.rb
index e76806c5d3f..21520f86bf8 100644
--- a/db/migrate/20200102140148_add_expanded_environment_name_to_ci_build_metadata.rb
+++ b/db/migrate/20200102140148_add_expanded_environment_name_to_ci_build_metadata.rb
@@ -3,9 +3,11 @@
class AddExpandedEnvironmentNameToCiBuildMetadata < ActiveRecord::Migration[5.2]
DOWNTIME = false
+ # rubocop:disable Migration/PreventStrings
def up
add_column :ci_builds_metadata, :expanded_environment_name, :string, limit: 255
end
+ # rubocop:enable Migration/PreventStrings
def down
remove_column :ci_builds_metadata, :expanded_environment_name
diff --git a/db/migrate/20200121192942_create_geo_events.rb b/db/migrate/20200121192942_create_geo_events.rb
index 6dbe131051f..0c885e8524c 100644
--- a/db/migrate/20200121192942_create_geo_events.rb
+++ b/db/migrate/20200121192942_create_geo_events.rb
@@ -3,6 +3,7 @@
class CreateGeoEvents < ActiveRecord::Migration[5.2]
DOWNTIME = false
+ # rubocop:disable Migration/PreventStrings
def change
create_table :geo_events do |t|
t.string :replicable_name, limit: 255, null: false
@@ -11,4 +12,5 @@ class CreateGeoEvents < ActiveRecord::Migration[5.2]
t.datetime_with_timezone :created_at, null: false
end
end
+ # rubocop:enable Migration/PreventStrings
end
diff --git a/db/migrate/20200122161638_add_deploy_token_type_to_deploy_tokens.rb b/db/migrate/20200122161638_add_deploy_token_type_to_deploy_tokens.rb
index 2fe79250ea2..99459199709 100644
--- a/db/migrate/20200122161638_add_deploy_token_type_to_deploy_tokens.rb
+++ b/db/migrate/20200122161638_add_deploy_token_type_to_deploy_tokens.rb
@@ -8,7 +8,7 @@ class AddDeployTokenTypeToDeployTokens < ActiveRecord::Migration[5.2]
DOWNTIME = false
def up
- add_column_with_default :deploy_tokens, :deploy_token_type, :integer, default: 2, limit: 2, allow_null: false
+ add_column_with_default :deploy_tokens, :deploy_token_type, :integer, default: 2, limit: 2, allow_null: false # rubocop:disable Migration/AddColumnWithDefault
end
def down
diff --git a/db/migrate/20200124053531_add_source_to_import_failures.rb b/db/migrate/20200124053531_add_source_to_import_failures.rb
index 532d5803c74..2ca0bcba09f 100644
--- a/db/migrate/20200124053531_add_source_to_import_failures.rb
+++ b/db/migrate/20200124053531_add_source_to_import_failures.rb
@@ -3,7 +3,9 @@
class AddSourceToImportFailures < ActiveRecord::Migration[5.2]
DOWNTIME = false
+ # rubocop:disable Migration/PreventStrings
def change
add_column :import_failures, :source, :string, limit: 128
end
+ # rubocop:enable Migration/PreventStrings
end
diff --git a/db/migrate/20200128184209_add_usage_to_pages_domains.rb b/db/migrate/20200128184209_add_usage_to_pages_domains.rb
index ac644814076..292490078cd 100644
--- a/db/migrate/20200128184209_add_usage_to_pages_domains.rb
+++ b/db/migrate/20200128184209_add_usage_to_pages_domains.rb
@@ -9,7 +9,7 @@ class AddUsageToPagesDomains < ActiveRecord::Migration[5.2]
disable_ddl_transaction!
def up
- add_column_with_default :pages_domains, :usage, :integer, limit: 2, default: PAGES_USAGE, allow_null: false
+ add_column_with_default :pages_domains, :usage, :integer, limit: 2, default: PAGES_USAGE, allow_null: false # rubocop:disable Migration/AddColumnWithDefault
end
def down
diff --git a/db/migrate/20200129133716_add_resource_milestone_events_table.rb b/db/migrate/20200129133716_add_resource_milestone_events_table.rb
index 0ead21820c2..681190a8744 100644
--- a/db/migrate/20200129133716_add_resource_milestone_events_table.rb
+++ b/db/migrate/20200129133716_add_resource_milestone_events_table.rb
@@ -3,6 +3,7 @@
class AddResourceMilestoneEventsTable < ActiveRecord::Migration[5.2]
DOWNTIME = false
+ # rubocop:disable Migration/AddLimitToTextColumns
def change
create_table :resource_milestone_events, id: :bigserial do |t|
t.references :user, null: false, foreign_key: { on_delete: :nullify },
@@ -22,4 +23,5 @@ class AddResourceMilestoneEventsTable < ActiveRecord::Migration[5.2]
t.datetime_with_timezone :created_at, null: false
end
end
+ # rubocop:enable Migration/AddLimitToTextColumns
end
diff --git a/db/migrate/20200130134335_add_cert_and_key_to_serverless_domain_cluster.rb b/db/migrate/20200130134335_add_cert_and_key_to_serverless_domain_cluster.rb
index 39249053ee0..81764aefe7c 100644
--- a/db/migrate/20200130134335_add_cert_and_key_to_serverless_domain_cluster.rb
+++ b/db/migrate/20200130134335_add_cert_and_key_to_serverless_domain_cluster.rb
@@ -5,9 +5,13 @@ class AddCertAndKeyToServerlessDomainCluster < ActiveRecord::Migration[5.2]
DOWNTIME = false
+ # rubocop:disable Migration/PreventStrings
+ # rubocop:disable Migration/AddLimitToTextColumns
def change
add_column :serverless_domain_cluster, :encrypted_key, :text
add_column :serverless_domain_cluster, :encrypted_key_iv, :string, limit: 255
add_column :serverless_domain_cluster, :certificate, :text
end
+ # rubocop:enable Migration/AddLimitToTextColumns
+ # rubocop:enable Migration/PreventStrings
end
diff --git a/db/migrate/20200202100932_add_service_desk_project_key.rb b/db/migrate/20200202100932_add_service_desk_project_key.rb
index d42ed4159e4..5095e628b42 100644
--- a/db/migrate/20200202100932_add_service_desk_project_key.rb
+++ b/db/migrate/20200202100932_add_service_desk_project_key.rb
@@ -3,7 +3,9 @@
class AddServiceDeskProjectKey < ActiveRecord::Migration[5.2]
DOWNTIME = false
+ # rubocop:disable Migration/PreventStrings
def change
add_column :service_desk_settings, :project_key, :string, limit: 255
end
+ # rubocop:enable Migration/PreventStrings
end
diff --git a/db/migrate/20200204131831_create_daily_report_results.rb b/db/migrate/20200204131831_create_daily_report_results.rb
index e18d4efb7fa..6992b6317e1 100644
--- a/db/migrate/20200204131831_create_daily_report_results.rb
+++ b/db/migrate/20200204131831_create_daily_report_results.rb
@@ -3,6 +3,7 @@
class CreateDailyReportResults < ActiveRecord::Migration[6.0]
DOWNTIME = false
+ # rubocop:disable Migration/PreventStrings
def change
create_table :ci_daily_report_results do |t|
t.date :date, null: false
@@ -10,8 +11,8 @@ class CreateDailyReportResults < ActiveRecord::Migration[6.0]
t.bigint :last_pipeline_id, null: false
t.float :value, null: false
t.integer :param_type, limit: 8, null: false
- t.string :ref_path, null: false # rubocop:disable Migration/AddLimitToStringColumns
- t.string :title, null: false # rubocop:disable Migration/AddLimitToStringColumns
+ t.string :ref_path, null: false
+ t.string :title, null: false
t.index :last_pipeline_id
t.index [:project_id, :ref_path, :param_type, :date, :title], name: 'index_daily_report_results_unique_columns', unique: true
@@ -19,4 +20,5 @@ class CreateDailyReportResults < ActiveRecord::Migration[6.0]
t.foreign_key :ci_pipelines, column: :last_pipeline_id, on_delete: :cascade
end
end
+ # rubocop:enable Migration/PreventStrings
end
diff --git a/db/migrate/20200206112850_create_snippet_repository_table.rb b/db/migrate/20200206112850_create_snippet_repository_table.rb
index 0c14b37855d..b9120264bb7 100644
--- a/db/migrate/20200206112850_create_snippet_repository_table.rb
+++ b/db/migrate/20200206112850_create_snippet_repository_table.rb
@@ -3,6 +3,7 @@
class CreateSnippetRepositoryTable < ActiveRecord::Migration[6.0]
DOWNTIME = false
+ # rubocop:disable Migration/PreventStrings
def change
create_table :snippet_repositories, id: false, primary_key: :snippet_id do |t|
t.references :shard, null: false, index: true, foreign_key: { on_delete: :restrict }
@@ -10,4 +11,5 @@ class CreateSnippetRepositoryTable < ActiveRecord::Migration[6.0]
t.string :disk_path, limit: 80, null: false, index: { unique: true }
end
end
+ # rubocop:enable Migration/PreventStrings
end
diff --git a/db/migrate/20200207151640_create_deployment_clusters.rb b/db/migrate/20200207151640_create_deployment_clusters.rb
index 233e91d31b0..dd05d1730d9 100644
--- a/db/migrate/20200207151640_create_deployment_clusters.rb
+++ b/db/migrate/20200207151640_create_deployment_clusters.rb
@@ -3,6 +3,7 @@
class CreateDeploymentClusters < ActiveRecord::Migration[6.0]
DOWNTIME = false
+ # rubocop:disable Migration/PreventStrings
def change
create_table :deployment_clusters, id: false, force: :cascade do |t|
t.references :deployment, foreign_key: { on_delete: :cascade }, primary_key: true, type: :integer, index: false, default: nil
@@ -13,4 +14,5 @@ class CreateDeploymentClusters < ActiveRecord::Migration[6.0]
t.index [:cluster_id, :deployment_id], unique: true
end
end
+ # rubocop:enable Migration/PreventStrings
end
diff --git a/db/migrate/20200210184410_create_operations_strategies_table.rb b/db/migrate/20200210184410_create_operations_strategies_table.rb
index 1046bd11bc7..4d65267c1a7 100644
--- a/db/migrate/20200210184410_create_operations_strategies_table.rb
+++ b/db/migrate/20200210184410_create_operations_strategies_table.rb
@@ -3,6 +3,7 @@
class CreateOperationsStrategiesTable < ActiveRecord::Migration[6.0]
DOWNTIME = false
+ # rubocop:disable Migration/PreventStrings
def change
create_table :operations_strategies do |t|
t.references :feature_flag, index: true, null: false, foreign_key: { to_table: :operations_feature_flags, on_delete: :cascade }
@@ -10,4 +11,5 @@ class CreateOperationsStrategiesTable < ActiveRecord::Migration[6.0]
t.jsonb :parameters, null: false, default: {}
end
end
+ # rubocop:enable Migration/PreventStrings
end
diff --git a/db/migrate/20200210184420_create_operations_scopes_table.rb b/db/migrate/20200210184420_create_operations_scopes_table.rb
index 0b33882fe3d..7ce21feb865 100644
--- a/db/migrate/20200210184420_create_operations_scopes_table.rb
+++ b/db/migrate/20200210184420_create_operations_scopes_table.rb
@@ -3,6 +3,7 @@
class CreateOperationsScopesTable < ActiveRecord::Migration[6.0]
DOWNTIME = false
+ # rubocop:disable Migration/PreventStrings
def change
create_table :operations_scopes do |t|
t.references :strategy, null: false, index: false, foreign_key: { to_table: :operations_strategies, on_delete: :cascade }
@@ -11,4 +12,5 @@ class CreateOperationsScopesTable < ActiveRecord::Migration[6.0]
add_index :operations_scopes, [:strategy_id, :environment_scope], unique: true
end
+ # rubocop:enable Migration/PreventStrings
end
diff --git a/db/migrate/20200213093702_add_email_restrictions_to_application_settings.rb b/db/migrate/20200213093702_add_email_restrictions_to_application_settings.rb
index 22d7b9f37e0..2b8afacb6b3 100644
--- a/db/migrate/20200213093702_add_email_restrictions_to_application_settings.rb
+++ b/db/migrate/20200213093702_add_email_restrictions_to_application_settings.rb
@@ -3,10 +3,12 @@
class AddEmailRestrictionsToApplicationSettings < ActiveRecord::Migration[6.0]
DOWNTIME = false
+ # rubocop:disable Migration/AddLimitToTextColumns
def up
add_column(:application_settings, :email_restrictions_enabled, :boolean, default: false, null: false)
add_column(:application_settings, :email_restrictions, :text, null: true)
end
+ # rubocop:enable Migration/AddLimitToTextColumns
def down
remove_column(:application_settings, :email_restrictions_enabled)
diff --git a/db/migrate/20200213100530_add_verification_columns_to_packages.rb b/db/migrate/20200213100530_add_verification_columns_to_packages.rb
index 4c4e9a88c60..b715f35fcab 100644
--- a/db/migrate/20200213100530_add_verification_columns_to_packages.rb
+++ b/db/migrate/20200213100530_add_verification_columns_to_packages.rb
@@ -3,6 +3,7 @@
class AddVerificationColumnsToPackages < ActiveRecord::Migration[6.0]
DOWNTIME = false
+ # rubocop:disable Migration/PreventStrings
def change
add_column :packages_package_files, :verification_retry_at, :datetime_with_timezone
add_column :packages_package_files, :verified_at, :datetime_with_timezone
@@ -10,4 +11,5 @@ class AddVerificationColumnsToPackages < ActiveRecord::Migration[6.0]
add_column :packages_package_files, :verification_failure, :string, limit: 255
add_column :packages_package_files, :verification_retry_count, :integer
end
+ # rubocop:enable Migration/PreventStrings
end
diff --git a/db/migrate/20200213155311_add_npm_package_requests_forwarding_to_application_settings.rb b/db/migrate/20200213155311_add_npm_package_requests_forwarding_to_application_settings.rb
index db67437232c..3815117a7e8 100644
--- a/db/migrate/20200213155311_add_npm_package_requests_forwarding_to_application_settings.rb
+++ b/db/migrate/20200213155311_add_npm_package_requests_forwarding_to_application_settings.rb
@@ -8,7 +8,7 @@ class AddNpmPackageRequestsForwardingToApplicationSettings < ActiveRecord::Migra
disable_ddl_transaction!
def up
- add_column_with_default(:application_settings, :npm_package_requests_forwarding,
+ add_column_with_default(:application_settings, :npm_package_requests_forwarding, # rubocop:disable Migration/AddColumnWithDefault
:boolean,
default: false,
allow_null: false)
diff --git a/db/migrate/20200213224220_add_sprints.rb b/db/migrate/20200213224220_add_sprints.rb
new file mode 100644
index 00000000000..8d82d1e261a
--- /dev/null
+++ b/db/migrate/20200213224220_add_sprints.rb
@@ -0,0 +1,35 @@
+# frozen_string_literal: true
+
+class AddSprints < ActiveRecord::Migration[6.0]
+ DOWNTIME = false
+
+ def change
+ create_table :sprints, id: :bigserial do |t|
+ t.timestamps_with_timezone null: false
+ t.date :start_date
+ t.date :due_date
+
+ t.references :project, foreign_key: false, index: false
+ t.references :group, foreign_key: false, index: true
+
+ t.integer :iid, null: false
+ t.integer :cached_markdown_version
+ t.integer :state, limit: 2
+ # rubocop:disable Migration/AddLimitToTextColumns
+ t.text :title, null: false
+ t.text :title_html
+ t.text :description
+ t.text :description_html
+ # rubocop:enable Migration/AddLimitToTextColumns
+
+ t.index :description, name: "index_sprints_on_description_trigram", opclass: :gin_trgm_ops, using: :gin
+ t.index :due_date
+ t.index %w(project_id iid), unique: true
+ t.index :title
+ t.index :title, name: "index_sprints_on_title_trigram", opclass: :gin_trgm_ops, using: :gin
+
+ t.index %w(project_id title), unique: true, where: 'project_id IS NOT NULL'
+ t.index %w(group_id title), unique: true, where: 'group_id IS NOT NULL'
+ end
+ end
+end
diff --git a/db/migrate/20200214025454_add_canonical_emails.rb b/db/migrate/20200214025454_add_canonical_emails.rb
index 80cf535abfa..0732d39169d 100644
--- a/db/migrate/20200214025454_add_canonical_emails.rb
+++ b/db/migrate/20200214025454_add_canonical_emails.rb
@@ -10,7 +10,7 @@ class AddCanonicalEmails < ActiveRecord::Migration[6.0]
create_table :user_canonical_emails do |t|
t.timestamps_with_timezone
t.references :user, index: false, null: false, foreign_key: { on_delete: :cascade }
- t.string :canonical_email, null: false, index: true # rubocop:disable Migration/AddLimitToStringColumns
+ t.string :canonical_email, null: false, index: true # rubocop:disable Migration/PreventStrings
end
end
diff --git a/db/migrate/20200215222507_drop_forked_project_links_fk.rb b/db/migrate/20200215222507_drop_forked_project_links_fk.rb
index 0be7a57ed0e..7fe38a6ccdb 100644
--- a/db/migrate/20200215222507_drop_forked_project_links_fk.rb
+++ b/db/migrate/20200215222507_drop_forked_project_links_fk.rb
@@ -8,21 +8,16 @@ class DropForkedProjectLinksFk < ActiveRecord::Migration[6.0]
disable_ddl_transaction!
def up
- # rubocop: disable Migration/WithLockRetriesWithoutDdlTransaction
with_lock_retries do
remove_foreign_key_if_exists :forked_project_links, column: :forked_to_project_id
end
- # rubocop: enable Migration/WithLockRetriesWithoutDdlTransaction
end
def down
unless foreign_key_exists?(:forked_project_links, :projects, column: :forked_to_project_id)
- # rubocop: disable Migration/WithLockRetriesWithoutDdlTransaction
with_lock_retries do
- # rubocop: disable Migration/AddConcurrentForeignKey
add_foreign_key :forked_project_links, :projects, column: :forked_to_project_id, on_delete: :cascade, validate: false
end
- # rubocop: enable Migration/WithLockRetriesWithoutDdlTransaction
end
fk_name = concurrent_foreign_key_name(:forked_project_links, :forked_to_project_id, prefix: 'fk_rails_')
diff --git a/db/migrate/20200218113721_add_indexes_to_package_file.rb b/db/migrate/20200218113721_add_indexes_to_package_file.rb
new file mode 100644
index 00000000000..7c66da6819e
--- /dev/null
+++ b/db/migrate/20200218113721_add_indexes_to_package_file.rb
@@ -0,0 +1,19 @@
+# frozen_string_literal: true
+
+class AddIndexesToPackageFile < ActiveRecord::Migration[6.0]
+ include Gitlab::Database::MigrationHelpers
+
+ DOWNTIME = false
+
+ disable_ddl_transaction!
+
+ def up
+ add_concurrent_index :packages_package_files, :verification_failure, where: "(verification_failure IS NOT NULL)", name: "packages_packages_verification_failure_partial"
+ add_concurrent_index :packages_package_files, :verification_checksum, where: "(verification_checksum IS NOT NULL)", name: "packages_packages_verification_checksum_partial"
+ end
+
+ def down
+ remove_concurrent_index :packages_package_files, :verification_failure
+ remove_concurrent_index :packages_package_files, :verification_checksum
+ end
+end
diff --git a/db/migrate/20200219105209_add_filepath_to_release_links.rb b/db/migrate/20200219105209_add_filepath_to_release_links.rb
index bcc204c22e8..ba69332fb70 100644
--- a/db/migrate/20200219105209_add_filepath_to_release_links.rb
+++ b/db/migrate/20200219105209_add_filepath_to_release_links.rb
@@ -3,6 +3,6 @@ class AddFilepathToReleaseLinks < ActiveRecord::Migration[6.0]
DOWNTIME = false
def change
- add_column :release_links, :filepath, :string, limit: 128
+ add_column :release_links, :filepath, :string, limit: 128 # rubocop:disable Migration/PreventStrings
end
end
diff --git a/db/migrate/20200219135440_add_limit_metric_type_to_list.rb b/db/migrate/20200219135440_add_limit_metric_type_to_list.rb
index 35e36b4805b..a2b2fb25ac9 100644
--- a/db/migrate/20200219135440_add_limit_metric_type_to_list.rb
+++ b/db/migrate/20200219135440_add_limit_metric_type_to_list.rb
@@ -4,6 +4,6 @@ class AddLimitMetricTypeToList < ActiveRecord::Migration[6.0]
DOWNTIME = false
def change
- add_column :lists, :limit_metric, :string, limit: 20
+ add_column :lists, :limit_metric, :string, limit: 20 # rubocop:disable Migration/PreventStrings
end
end
diff --git a/db/migrate/20200224020219_add_status_page_settings.rb b/db/migrate/20200224020219_add_status_page_settings.rb
index b960b60881e..b45dfc8ec46 100644
--- a/db/migrate/20200224020219_add_status_page_settings.rb
+++ b/db/migrate/20200224020219_add_status_page_settings.rb
@@ -3,6 +3,7 @@
class AddStatusPageSettings < ActiveRecord::Migration[6.0]
DOWNTIME = false
+ # rubocop:disable Migration/PreventStrings
def change
create_table :status_page_settings, id: false do |t|
t.references :project, index: true, primary_key: true, foreign_key: { on_delete: :cascade }, unique: true, null: false
@@ -15,4 +16,5 @@ class AddStatusPageSettings < ActiveRecord::Migration[6.0]
t.string :encrypted_aws_secret_key_iv, limit: 255, null: false
end
end
+ # rubocop:enable Migration/PreventStrings
end
diff --git a/db/migrate/20200224163804_add_version_to_feature_flags_table.rb b/db/migrate/20200224163804_add_version_to_feature_flags_table.rb
index bf3179f070c..d300f7c83e1 100644
--- a/db/migrate/20200224163804_add_version_to_feature_flags_table.rb
+++ b/db/migrate/20200224163804_add_version_to_feature_flags_table.rb
@@ -12,7 +12,9 @@ class AddVersionToFeatureFlagsTable < ActiveRecord::Migration[6.0]
def up
# The operations_feature_flags table is small enough that we can disable this cop.
# See https://gitlab.com/gitlab-org/gitlab/-/merge_requests/25552#note_291202882
+ # rubocop:disable Migration/AddColumnWithDefault
add_column_with_default(:operations_feature_flags, :version, :smallint, default: FEATURE_FLAG_LEGACY_VERSION, allow_null: false)
+ # rubocop:enable Migration/AddColumnWithDefault
end
def down
diff --git a/db/migrate/20200226100614_create_requirements.rb b/db/migrate/20200226100614_create_requirements.rb
index 4ebbf38b8d1..f374e1e9581 100644
--- a/db/migrate/20200226100614_create_requirements.rb
+++ b/db/migrate/20200226100614_create_requirements.rb
@@ -5,6 +5,7 @@ class CreateRequirements < ActiveRecord::Migration[6.0]
DOWNTIME = false
+ # rubocop:disable Migration/PreventStrings
def change
create_table :requirements do |t|
t.timestamps_with_timezone null: false
@@ -14,7 +15,7 @@ class CreateRequirements < ActiveRecord::Migration[6.0]
t.integer :cached_markdown_version
t.integer :state, limit: 2, default: 1, null: false
t.string :title, limit: 255, null: false
- t.text :title_html
+ t.text :title_html # rubocop:disable Migration/AddLimitToTextColumns
t.index :project_id
t.index :author_id
@@ -25,4 +26,5 @@ class CreateRequirements < ActiveRecord::Migration[6.0]
t.index %w(project_id iid), name: 'index_requirements_on_project_id_and_iid', where: 'project_id IS NOT NULL', unique: true, using: :btree
end
end
+ # rubocop:enable Migration/PreventStrings
end
diff --git a/db/migrate/20200227164113_create_scim_identities.rb b/db/migrate/20200227164113_create_scim_identities.rb
index 1942270761b..34e119ccfe3 100644
--- a/db/migrate/20200227164113_create_scim_identities.rb
+++ b/db/migrate/20200227164113_create_scim_identities.rb
@@ -3,6 +3,7 @@
class CreateScimIdentities < ActiveRecord::Migration[6.0]
DOWNTIME = false
+ # rubocop:disable Migration/PreventStrings
def change
create_table :scim_identities do |t|
t.references :group, foreign_key: { to_table: :namespaces, on_delete: :cascade }, null: false
@@ -15,4 +16,5 @@ class CreateScimIdentities < ActiveRecord::Migration[6.0]
t.index [:user_id, :group_id], unique: true
end
end
+ # rubocop:enable Migration/PreventStrings
end
diff --git a/db/migrate/20200227165129_create_user_details.rb b/db/migrate/20200227165129_create_user_details.rb
index fe9f23fcb21..89258eadb9f 100644
--- a/db/migrate/20200227165129_create_user_details.rb
+++ b/db/migrate/20200227165129_create_user_details.rb
@@ -5,6 +5,7 @@ class CreateUserDetails < ActiveRecord::Migration[6.0]
DOWNTIME = false
+ # rubocop:disable Migration/PreventStrings
def up
with_lock_retries do
create_table :user_details, id: false do |t|
@@ -15,6 +16,7 @@ class CreateUserDetails < ActiveRecord::Migration[6.0]
add_index :user_details, :user_id, unique: true
end
+ # rubocop:enable Migration/PreventStrings
def down
with_lock_retries do
diff --git a/db/migrate/20200302152516_add_wiki_slug.rb b/db/migrate/20200302152516_add_wiki_slug.rb
index dabac39aeb8..a7891427c10 100644
--- a/db/migrate/20200302152516_add_wiki_slug.rb
+++ b/db/migrate/20200302152516_add_wiki_slug.rb
@@ -3,6 +3,7 @@
class AddWikiSlug < ActiveRecord::Migration[6.0]
DOWNTIME = false
+ # rubocop:disable Migration/PreventStrings
def change
create_table :wiki_page_meta, id: :serial do |t|
t.references :project, index: true, foreign_key: { on_delete: :cascade }, null: false
@@ -19,4 +20,5 @@ class AddWikiSlug < ActiveRecord::Migration[6.0]
t.index [:wiki_page_meta_id], name: 'one_canonical_wiki_page_slug_per_metadata', unique: true, where: "(canonical = true)"
end
end
+ # rubocop:enable Migration/PreventStrings
end
diff --git a/db/migrate/20200304023245_add_sprint_to_issues.rb b/db/migrate/20200304023245_add_sprint_to_issues.rb
new file mode 100644
index 00000000000..c8bc5258ffd
--- /dev/null
+++ b/db/migrate/20200304023245_add_sprint_to_issues.rb
@@ -0,0 +1,20 @@
+# frozen_string_literal: true
+
+class AddSprintToIssues < ActiveRecord::Migration[6.0]
+ include Gitlab::Database::MigrationHelpers
+
+ DOWNTIME = false
+
+ def up
+ with_lock_retries do
+ # index will be added in another migration with `add_concurrent_index`
+ add_column :issues, :sprint_id, :bigint
+ end
+ end
+
+ def down
+ with_lock_retries do
+ remove_column :issues, :sprint_id
+ end
+ end
+end
diff --git a/db/migrate/20200304023851_add_sprint_to_merge_requests.rb b/db/migrate/20200304023851_add_sprint_to_merge_requests.rb
new file mode 100644
index 00000000000..e294abe66cd
--- /dev/null
+++ b/db/migrate/20200304023851_add_sprint_to_merge_requests.rb
@@ -0,0 +1,20 @@
+# frozen_string_literal: true
+
+class AddSprintToMergeRequests < ActiveRecord::Migration[6.0]
+ include Gitlab::Database::MigrationHelpers
+
+ DOWNTIME = false
+
+ def up
+ with_lock_retries do
+ # index will be added in another migration with `add_concurrent_index`
+ add_column :merge_requests, :sprint_id, :bigint
+ end
+ end
+
+ def down
+ with_lock_retries do
+ remove_column :merge_requests, :sprint_id
+ end
+ end
+end
diff --git a/db/migrate/20200304024025_add_sprint_id_index_to_issues.rb b/db/migrate/20200304024025_add_sprint_id_index_to_issues.rb
new file mode 100644
index 00000000000..aaece3445db
--- /dev/null
+++ b/db/migrate/20200304024025_add_sprint_id_index_to_issues.rb
@@ -0,0 +1,21 @@
+# frozen_string_literal: true
+
+class AddSprintIdIndexToIssues < ActiveRecord::Migration[6.0]
+ include Gitlab::Database::MigrationHelpers
+
+ DOWNTIME = false
+
+ disable_ddl_transaction!
+
+ def up
+ add_concurrent_index :issues, :sprint_id
+ add_concurrent_foreign_key :issues, :sprints, column: :sprint_id
+ end
+
+ def down
+ with_lock_retries do
+ remove_foreign_key :issues, column: :sprint_id
+ end
+ remove_concurrent_index :issues, :sprint_id
+ end
+end
diff --git a/db/migrate/20200304024042_add_sprint_id_index_to_merge_requests.rb b/db/migrate/20200304024042_add_sprint_id_index_to_merge_requests.rb
new file mode 100644
index 00000000000..219d10ff47c
--- /dev/null
+++ b/db/migrate/20200304024042_add_sprint_id_index_to_merge_requests.rb
@@ -0,0 +1,21 @@
+# frozen_string_literal: true
+
+class AddSprintIdIndexToMergeRequests < ActiveRecord::Migration[6.0]
+ include Gitlab::Database::MigrationHelpers
+
+ DOWNTIME = false
+
+ disable_ddl_transaction!
+
+ def up
+ add_concurrent_index :merge_requests, :sprint_id
+ add_concurrent_foreign_key :merge_requests, :sprints, column: :sprint_id
+ end
+
+ def down
+ with_lock_retries do
+ remove_foreign_key :merge_requests, column: :sprint_id
+ end
+ remove_concurrent_index :merge_requests, :sprint_id
+ end
+end
diff --git a/db/migrate/20200305200641_create_terraform_states.rb b/db/migrate/20200305200641_create_terraform_states.rb
index 3e137369e33..8429d5cfeb3 100644
--- a/db/migrate/20200305200641_create_terraform_states.rb
+++ b/db/migrate/20200305200641_create_terraform_states.rb
@@ -3,6 +3,7 @@
class CreateTerraformStates < ActiveRecord::Migration[6.0]
DOWNTIME = false
+ # rubocop:disable Migration/PreventStrings
def change
create_table :terraform_states do |t|
t.references :project, index: true, foreign_key: { on_delete: :cascade }, null: false
@@ -11,4 +12,5 @@ class CreateTerraformStates < ActiveRecord::Migration[6.0]
t.string :file, limit: 255
end
end
+ # rubocop:enable Migration/PreventStrings
end
diff --git a/db/migrate/20200309162244_add_open_project_tracker_data.rb b/db/migrate/20200309162244_add_open_project_tracker_data.rb
index 672dde4d518..5906eb7ebff 100644
--- a/db/migrate/20200309162244_add_open_project_tracker_data.rb
+++ b/db/migrate/20200309162244_add_open_project_tracker_data.rb
@@ -6,6 +6,7 @@
class AddOpenProjectTrackerData < ActiveRecord::Migration[6.0]
DOWNTIME = false
+ # rubocop:disable Migration/PreventStrings
def change
create_table :open_project_tracker_data do |t|
t.references :service, foreign_key: { on_delete: :cascade }, type: :integer, index: true, null: false
@@ -20,4 +21,5 @@ class AddOpenProjectTrackerData < ActiveRecord::Migration[6.0]
t.string :project_identifier_code, limit: 100
end
end
+ # rubocop:enable Migration/PreventStrings
end
diff --git a/db/migrate/20200310145304_add_runtime_created_to_ci_job_variables.rb b/db/migrate/20200310145304_add_runtime_created_to_ci_job_variables.rb
index d5ec8854bfa..e76136a5c1f 100644
--- a/db/migrate/20200310145304_add_runtime_created_to_ci_job_variables.rb
+++ b/db/migrate/20200310145304_add_runtime_created_to_ci_job_variables.rb
@@ -10,7 +10,7 @@ class AddRuntimeCreatedToCiJobVariables < ActiveRecord::Migration[6.0]
DEFAULT_SOURCE = 0 # Equvalent to Ci::JobVariable.internal_source
def up
- add_column_with_default(:ci_job_variables, :source, :integer, limit: 2, default: DEFAULT_SOURCE, allow_null: false)
+ add_column_with_default(:ci_job_variables, :source, :integer, limit: 2, default: DEFAULT_SOURCE, allow_null: false) # rubocop:disable Migration/AddColumnWithDefault
end
def down
diff --git a/db/migrate/20200311141053_add_ci_pipeline_schedules_to_plan_limits.rb b/db/migrate/20200311141053_add_ci_pipeline_schedules_to_plan_limits.rb
index 2fc7785fe9c..e7fe3e63349 100644
--- a/db/migrate/20200311141053_add_ci_pipeline_schedules_to_plan_limits.rb
+++ b/db/migrate/20200311141053_add_ci_pipeline_schedules_to_plan_limits.rb
@@ -8,7 +8,7 @@ class AddCiPipelineSchedulesToPlanLimits < ActiveRecord::Migration[6.0]
DOWNTIME = false
def up
- add_column_with_default(:plan_limits, :ci_pipeline_schedules, :integer, default: 0, allow_null: false)
+ add_column_with_default(:plan_limits, :ci_pipeline_schedules, :integer, default: 0, allow_null: false) # rubocop:disable Migration/AddColumnWithDefault
end
def down
diff --git a/db/migrate/20200311154110_create_vulnerability_exports.rb b/db/migrate/20200311154110_create_vulnerability_exports.rb
index f3162f8808c..5a3355ab5be 100644
--- a/db/migrate/20200311154110_create_vulnerability_exports.rb
+++ b/db/migrate/20200311154110_create_vulnerability_exports.rb
@@ -5,6 +5,7 @@ class CreateVulnerabilityExports < ActiveRecord::Migration[6.0]
DOWNTIME = false
+ # rubocop:disable Migration/PreventStrings
def change
create_table :vulnerability_exports do |t|
t.timestamps_with_timezone null: false
@@ -21,4 +22,5 @@ class CreateVulnerabilityExports < ActiveRecord::Migration[6.0]
t.index %i[author_id]
end
end
+ # rubocop:enable Migration/PreventStrings
end
diff --git a/db/migrate/20200311165635_create_project_export_jobs.rb b/db/migrate/20200311165635_create_project_export_jobs.rb
index 026ad2cd771..21ff5f488f7 100644
--- a/db/migrate/20200311165635_create_project_export_jobs.rb
+++ b/db/migrate/20200311165635_create_project_export_jobs.rb
@@ -3,6 +3,7 @@
class CreateProjectExportJobs < ActiveRecord::Migration[6.0]
DOWNTIME = false
+ # rubocop:disable Migration/PreventStrings
def change
create_table :project_export_jobs do |t|
t.references :project, index: false, null: false, foreign_key: { on_delete: :cascade }
@@ -16,4 +17,5 @@ class CreateProjectExportJobs < ActiveRecord::Migration[6.0]
t.index [:project_id, :status]
end
end
+ # rubocop:enable Migration/PreventStrings
end
diff --git a/db/migrate/20200311214912_add_modsecurity_mode_to_ingress_application.rb b/db/migrate/20200311214912_add_modsecurity_mode_to_ingress_application.rb
index 6ca17dc9b55..7f3366a4900 100644
--- a/db/migrate/20200311214912_add_modsecurity_mode_to_ingress_application.rb
+++ b/db/migrate/20200311214912_add_modsecurity_mode_to_ingress_application.rb
@@ -10,7 +10,7 @@ class AddModsecurityModeToIngressApplication < ActiveRecord::Migration[6.0]
disable_ddl_transaction!
def up
- add_column_with_default(:clusters_applications_ingress, :modsecurity_mode, :smallint, default: 0, allow_null: false)
+ add_column_with_default(:clusters_applications_ingress, :modsecurity_mode, :smallint, default: 0, allow_null: false) # rubocop:disable Migration/AddColumnWithDefault
end
def down
diff --git a/db/migrate/20200318162148_add_external_key_to_issues_table.rb b/db/migrate/20200318162148_add_external_key_to_issues_table.rb
index eeac6ae3155..b98937861be 100644
--- a/db/migrate/20200318162148_add_external_key_to_issues_table.rb
+++ b/db/migrate/20200318162148_add_external_key_to_issues_table.rb
@@ -7,7 +7,7 @@ class AddExternalKeyToIssuesTable < ActiveRecord::Migration[6.0]
def up
with_lock_retries do
- add_column :issues, :external_key, :string, limit: 255
+ add_column :issues, :external_key, :string, limit: 255 # rubocop:disable Migration/PreventStrings
end
end
diff --git a/db/migrate/20200318164448_add_external_key_to_epics_table.rb b/db/migrate/20200318164448_add_external_key_to_epics_table.rb
index c21fb1698bc..dee2ca98f36 100644
--- a/db/migrate/20200318164448_add_external_key_to_epics_table.rb
+++ b/db/migrate/20200318164448_add_external_key_to_epics_table.rb
@@ -7,7 +7,7 @@ class AddExternalKeyToEpicsTable < ActiveRecord::Migration[6.0]
def up
with_lock_retries do
- add_column :epics, :external_key, :string, limit: 255
+ add_column :epics, :external_key, :string, limit: 255 # rubocop:disable Migration/PreventStrings
end
end
diff --git a/db/migrate/20200318183553_create_pypi_package_metadata.rb b/db/migrate/20200318183553_create_pypi_package_metadata.rb
index 3f0204d51ac..6ebe70da643 100644
--- a/db/migrate/20200318183553_create_pypi_package_metadata.rb
+++ b/db/migrate/20200318183553_create_pypi_package_metadata.rb
@@ -8,7 +8,7 @@ class CreatePypiPackageMetadata < ActiveRecord::Migration[6.0]
def change
create_table :packages_pypi_metadata, id: false do |t|
t.references :package, primary_key: true, index: false, default: nil, foreign_key: { to_table: :packages_packages, on_delete: :cascade }, type: :bigint
- t.string "required_python", null: false, limit: 50
+ t.string "required_python", null: false, limit: 50 # rubocop:disable Migration/PreventStrings
end
end
end
diff --git a/db/migrate/20200319124127_create_metrics_dashboard_annotations.rb b/db/migrate/20200319124127_create_metrics_dashboard_annotations.rb
index 4c57e38518a..9e10299b4f8 100644
--- a/db/migrate/20200319124127_create_metrics_dashboard_annotations.rb
+++ b/db/migrate/20200319124127_create_metrics_dashboard_annotations.rb
@@ -7,6 +7,7 @@ class CreateMetricsDashboardAnnotations < ActiveRecord::Migration[6.0]
# Set this constant to true if this migration requires downtime.
DOWNTIME = false
+ # rubocop:disable Migration/PreventStrings
def change
create_table :metrics_dashboard_annotations do |t|
t.datetime_with_timezone :starting_at, null: false
@@ -15,10 +16,11 @@ class CreateMetricsDashboardAnnotations < ActiveRecord::Migration[6.0]
t.references :cluster, index: false, foreign_key: { on_delete: :cascade }, null: true
t.string :dashboard_path, null: false, limit: 255
t.string :panel_xid, limit: 255
- t.text :description, null: false, limit: 255
+ t.text :description, null: false, limit: 255 # rubocop:disable Migration/AddLimitToTextColumns
t.index %i(environment_id dashboard_path starting_at ending_at), where: 'environment_id IS NOT NULL', name: "index_metrics_dashboard_annotations_on_environment_id_and_3_col"
t.index %i(cluster_id dashboard_path starting_at ending_at), where: 'cluster_id IS NOT NULL', name: "index_metrics_dashboard_annotations_on_cluster_id_and_3_columns"
end
end
+ # rubocop:enable Migration/PreventStrings
end
diff --git a/db/migrate/20200320112455_add_cost_factor_fileds_to_ci_runners.rb b/db/migrate/20200320112455_add_cost_factor_fileds_to_ci_runners.rb
index 472baa32340..ca92b395b6f 100644
--- a/db/migrate/20200320112455_add_cost_factor_fileds_to_ci_runners.rb
+++ b/db/migrate/20200320112455_add_cost_factor_fileds_to_ci_runners.rb
@@ -8,8 +8,8 @@ class AddCostFactorFiledsToCiRunners < ActiveRecord::Migration[6.0]
disable_ddl_transaction!
def up
- add_column_with_default(:ci_runners, :public_projects_minutes_cost_factor, :float, allow_null: false, default: 0.0)
- add_column_with_default(:ci_runners, :private_projects_minutes_cost_factor, :float, allow_null: false, default: 1.0)
+ add_column_with_default(:ci_runners, :public_projects_minutes_cost_factor, :float, allow_null: false, default: 0.0) # rubocop:disable Migration/AddColumnWithDefault
+ add_column_with_default(:ci_runners, :private_projects_minutes_cost_factor, :float, allow_null: false, default: 1.0) # rubocop:disable Migration/AddColumnWithDefault
end
def down
diff --git a/db/migrate/20200320123839_add_letsencrypt_errors_to_pages_domains.rb b/db/migrate/20200320123839_add_letsencrypt_errors_to_pages_domains.rb
index ad4debf9a1e..6e7c37918e1 100644
--- a/db/migrate/20200320123839_add_letsencrypt_errors_to_pages_domains.rb
+++ b/db/migrate/20200320123839_add_letsencrypt_errors_to_pages_domains.rb
@@ -11,7 +11,7 @@ class AddLetsencryptErrorsToPagesDomains < ActiveRecord::Migration[6.0]
disable_ddl_transaction!
def up
- add_column_with_default :pages_domains, :auto_ssl_failed, :boolean, default: false
+ add_column_with_default :pages_domains, :auto_ssl_failed, :boolean, default: false # rubocop:disable Migration/AddColumnWithDefault
end
def down
diff --git a/db/migrate/20200320212400_add_project_show_default_award_emojis.rb b/db/migrate/20200320212400_add_project_show_default_award_emojis.rb
new file mode 100644
index 00000000000..c8238f75455
--- /dev/null
+++ b/db/migrate/20200320212400_add_project_show_default_award_emojis.rb
@@ -0,0 +1,11 @@
+# frozen_string_literal: true
+
+class AddProjectShowDefaultAwardEmojis < ActiveRecord::Migration[6.0]
+ include Gitlab::Database::MigrationHelpers
+
+ DOWNTIME = false
+
+ def change
+ add_column :project_settings, :show_default_award_emojis, :boolean, default: true, null: false
+ end
+end
diff --git a/db/migrate/20200323071918_add_bio_to_user_details.rb b/db/migrate/20200323071918_add_bio_to_user_details.rb
index 90e4b964695..9ef9d553215 100644
--- a/db/migrate/20200323071918_add_bio_to_user_details.rb
+++ b/db/migrate/20200323071918_add_bio_to_user_details.rb
@@ -7,9 +7,13 @@ class AddBioToUserDetails < ActiveRecord::Migration[6.0]
disable_ddl_transaction!
+ # rubocop:disable Migration/PreventStrings
def up
- add_column_with_default(:user_details, :bio, :string, default: '', allow_null: false, limit: 255, update_column_in_batches_args: { batch_column_name: :user_id })
+ # rubocop:disable Migration/AddColumnWithDefault
+ add_column_with_default(:user_details, :bio, :string, default: '', allow_null: false, limit: 255)
+ # rubocop:enable Migration/AddColumnWithDefault
end
+ # rubocop:enable Migration/PreventStrings
def down
remove_column(:user_details, :bio)
diff --git a/db/migrate/20200324115359_add_namespace_storage_size_limit_to_application_settings.rb b/db/migrate/20200324115359_add_namespace_storage_size_limit_to_application_settings.rb
index e5fe98a4d19..793dc4678c6 100644
--- a/db/migrate/20200324115359_add_namespace_storage_size_limit_to_application_settings.rb
+++ b/db/migrate/20200324115359_add_namespace_storage_size_limit_to_application_settings.rb
@@ -7,7 +7,7 @@ class AddNamespaceStorageSizeLimitToApplicationSettings < ActiveRecord::Migratio
disable_ddl_transaction!
def up
- add_column_with_default :application_settings, :namespace_storage_size_limit, :bigint, default: 0
+ add_column_with_default :application_settings, :namespace_storage_size_limit, :bigint, default: 0 # rubocop:disable Migration/AddColumnWithDefault
end
def down
diff --git a/db/migrate/20200325152327_add_seat_link_enabled_to_application_settings.rb b/db/migrate/20200325152327_add_seat_link_enabled_to_application_settings.rb
index 52f579c175c..fbea3ad1227 100644
--- a/db/migrate/20200325152327_add_seat_link_enabled_to_application_settings.rb
+++ b/db/migrate/20200325152327_add_seat_link_enabled_to_application_settings.rb
@@ -8,7 +8,7 @@ class AddSeatLinkEnabledToApplicationSettings < ActiveRecord::Migration[6.0]
disable_ddl_transaction!
def up
- add_column_with_default(:application_settings, :seat_link_enabled,
+ add_column_with_default(:application_settings, :seat_link_enabled, # rubocop:disable Migration/AddColumnWithDefault
:boolean,
default: true,
allow_null: false)
diff --git a/db/migrate/20200326114443_create_jira_imports_table.rb b/db/migrate/20200326114443_create_jira_imports_table.rb
index e114bd513f4..d53ccb166b0 100644
--- a/db/migrate/20200326114443_create_jira_imports_table.rb
+++ b/db/migrate/20200326114443_create_jira_imports_table.rb
@@ -7,6 +7,7 @@ class CreateJiraImportsTable < ActiveRecord::Migration[6.0]
disable_ddl_transaction!
+ # rubocop:disable Migration/PreventStrings
def change
create_table :jira_imports do |t|
t.integer :project_id, null: false, limit: 8
@@ -26,4 +27,5 @@ class CreateJiraImportsTable < ActiveRecord::Migration[6.0]
add_index :jira_imports, [:project_id, :jira_project_key], name: 'index_jira_imports_on_project_id_and_jira_project_key'
end
+ # rubocop:enable Migration/PreventStrings
end
diff --git a/db/migrate/20200326122700_create_diff_note_positions.rb b/db/migrate/20200326122700_create_diff_note_positions.rb
index 87159e666b5..d37f7fef078 100644
--- a/db/migrate/20200326122700_create_diff_note_positions.rb
+++ b/db/migrate/20200326122700_create_diff_note_positions.rb
@@ -5,6 +5,8 @@ class CreateDiffNotePositions < ActiveRecord::Migration[6.0]
DOWNTIME = false
+ # rubocop:disable Migration/AddLimitToTextColumns
+ # rubocop:disable Migration/PreventStrings
def up
with_lock_retries do
create_table :diff_note_positions do |t|
@@ -24,6 +26,8 @@ class CreateDiffNotePositions < ActiveRecord::Migration[6.0]
end
end
end
+ # rubocop:enable Migration/PreventStrings
+ # rubocop:enable Migration/AddLimitToTextColumns
def down
drop_table :diff_note_positions
diff --git a/db/migrate/20200330121000_add_confidential_attribute_to_epics.rb b/db/migrate/20200330121000_add_confidential_attribute_to_epics.rb
index 01728436b93..4db227728c8 100644
--- a/db/migrate/20200330121000_add_confidential_attribute_to_epics.rb
+++ b/db/migrate/20200330121000_add_confidential_attribute_to_epics.rb
@@ -8,7 +8,7 @@ class AddConfidentialAttributeToEpics < ActiveRecord::Migration[6.0]
disable_ddl_transaction!
def up
- add_column_with_default(:epics, :confidential, :boolean, default: false)
+ add_column_with_default(:epics, :confidential, :boolean, default: false) # rubocop:disable Migration/AddColumnWithDefault
end
def down
diff --git a/db/migrate/20200331195952_add_container_expiration_policies_enable_historic_entries_to_application_settings.rb b/db/migrate/20200331195952_add_container_expiration_policies_enable_historic_entries_to_application_settings.rb
index 95ce75efccc..b4cbb04b93a 100644
--- a/db/migrate/20200331195952_add_container_expiration_policies_enable_historic_entries_to_application_settings.rb
+++ b/db/migrate/20200331195952_add_container_expiration_policies_enable_historic_entries_to_application_settings.rb
@@ -8,7 +8,7 @@ class AddContainerExpirationPoliciesEnableHistoricEntriesToApplicationSettings <
disable_ddl_transaction!
def up
- add_column_with_default(:application_settings,
+ add_column_with_default(:application_settings, # rubocop:disable Migration/AddColumnWithDefault
:container_expiration_policies_enable_historic_entries,
:boolean,
default: false,
diff --git a/db/migrate/20200401211005_create_operations_user_lists.rb b/db/migrate/20200401211005_create_operations_user_lists.rb
index c5b928b8350..b0b02ca8886 100644
--- a/db/migrate/20200401211005_create_operations_user_lists.rb
+++ b/db/migrate/20200401211005_create_operations_user_lists.rb
@@ -8,8 +8,8 @@ class CreateOperationsUserLists < ActiveRecord::Migration[6.0]
t.references :project, index: false, foreign_key: { on_delete: :cascade }, null: false
t.timestamps_with_timezone
t.integer :iid, null: false
- t.string :name, null: false, limit: 255
- t.text :user_xids, null: false, default: ''
+ t.string :name, null: false, limit: 255 # rubocop:disable Migration/PreventStrings
+ t.text :user_xids, null: false, default: '' # rubocop:disable Migration/AddLimitToTextColumns
t.index [:project_id, :iid], unique: true
t.index [:project_id, :name], unique: true
diff --git a/db/migrate/20200402001106_add_cluster_type_index_to_clusters.rb b/db/migrate/20200402001106_add_cluster_type_index_to_clusters.rb
new file mode 100644
index 00000000000..b328b8681c1
--- /dev/null
+++ b/db/migrate/20200402001106_add_cluster_type_index_to_clusters.rb
@@ -0,0 +1,19 @@
+# frozen_string_literal: true
+
+class AddClusterTypeIndexToClusters < ActiveRecord::Migration[6.0]
+ include Gitlab::Database::MigrationHelpers
+
+ # Set this constant to true if this migration requires downtime.
+ DOWNTIME = false
+ INDEX_NAME = 'index_clusters_on_enabled_cluster_type_id_and_created_at'
+
+ disable_ddl_transaction!
+
+ def up
+ add_concurrent_index :clusters, [:enabled, :cluster_type, :id, :created_at], name: INDEX_NAME
+ end
+
+ def down
+ remove_concurrent_index_by_name :clusters, INDEX_NAME
+ end
+end
diff --git a/db/migrate/20200402115013_add_index_on_modsecurity_to_ingress.rb b/db/migrate/20200402115013_add_index_on_modsecurity_to_ingress.rb
new file mode 100644
index 00000000000..8bd2d957092
--- /dev/null
+++ b/db/migrate/20200402115013_add_index_on_modsecurity_to_ingress.rb
@@ -0,0 +1,18 @@
+# frozen_string_literal: true
+
+class AddIndexOnModsecurityToIngress < ActiveRecord::Migration[6.0]
+ include Gitlab::Database::MigrationHelpers
+
+ DOWNTIME = false
+ INDEX_NAME = 'index_clusters_applications_ingress_on_modsecurity'
+
+ disable_ddl_transaction!
+
+ def up
+ add_concurrent_index :clusters_applications_ingress, [:modsecurity_enabled, :modsecurity_mode, :cluster_id], name: INDEX_NAME
+ end
+
+ def down
+ remove_concurrent_index :clusters_applications_ingress, [:modsecurity_enabled, :modsecurity_mode, :cluster_id], name: INDEX_NAME
+ end
+end
diff --git a/db/migrate/20200402115623_add_index_on_successful_deployment_and_environment_id_to_deployments.rb b/db/migrate/20200402115623_add_index_on_successful_deployment_and_environment_id_to_deployments.rb
new file mode 100644
index 00000000000..c86f7ad63f0
--- /dev/null
+++ b/db/migrate/20200402115623_add_index_on_successful_deployment_and_environment_id_to_deployments.rb
@@ -0,0 +1,18 @@
+# frozen_string_literal: true
+
+class AddIndexOnSuccessfulDeploymentAndEnvironmentIdToDeployments < ActiveRecord::Migration[6.0]
+ include Gitlab::Database::MigrationHelpers
+
+ DOWNTIME = false
+ INDEX_NAME = 'index_successful_deployments_on_cluster_id_and_environment_id'
+
+ disable_ddl_transaction!
+
+ def up
+ add_concurrent_index :deployments, [:cluster_id, :environment_id], where: 'status = 2', name: INDEX_NAME
+ end
+
+ def down
+ remove_concurrent_index :deployments, [:cluster_id, :environment_id], where: 'status = 2', name: INDEX_NAME
+ end
+end
diff --git a/db/migrate/20200402124802_add_correlation_id_to_project_import_state.rb b/db/migrate/20200402124802_add_correlation_id_to_project_import_state.rb
index 974009b101a..97eb01529cc 100644
--- a/db/migrate/20200402124802_add_correlation_id_to_project_import_state.rb
+++ b/db/migrate/20200402124802_add_correlation_id_to_project_import_state.rb
@@ -5,11 +5,13 @@ class AddCorrelationIdToProjectImportState < ActiveRecord::Migration[6.0]
DOWNTIME = false
+ # rubocop:disable Migration/PreventStrings
def up
with_lock_retries do
add_column :project_mirror_data, :correlation_id_value, :string, limit: 128
end
end
+ # rubocop:enable Migration/PreventStrings
def down
with_lock_retries do
diff --git a/db/migrate/20200402185044_create_clusters_applications_fluentd.rb b/db/migrate/20200402185044_create_clusters_applications_fluentd.rb
index 08f3faf80e1..2116b2ffe01 100644
--- a/db/migrate/20200402185044_create_clusters_applications_fluentd.rb
+++ b/db/migrate/20200402185044_create_clusters_applications_fluentd.rb
@@ -3,6 +3,7 @@
class CreateClustersApplicationsFluentd < ActiveRecord::Migration[6.0]
DOWNTIME = false
+ # rubocop:disable Migration/PreventStrings
def change
create_table :clusters_applications_fluentd do |t|
t.integer :protocol, null: false, limit: 2
@@ -12,7 +13,8 @@ class CreateClustersApplicationsFluentd < ActiveRecord::Migration[6.0]
t.timestamps_with_timezone null: false
t.string :version, null: false, limit: 255
t.string :host, null: false, limit: 255
- t.text :status_reason
+ t.text :status_reason # rubocop:disable Migration/AddLimitToTextColumns
end
end
+ # rubocop:enable Migration/PreventStrings
end
diff --git a/db/migrate/20200406095930_add_needs_ssl_renewal_user_provided_pages_domains_index.rb b/db/migrate/20200406095930_add_needs_ssl_renewal_user_provided_pages_domains_index.rb
new file mode 100644
index 00000000000..73db5e71b94
--- /dev/null
+++ b/db/migrate/20200406095930_add_needs_ssl_renewal_user_provided_pages_domains_index.rb
@@ -0,0 +1,20 @@
+# frozen_string_literal: true
+
+class AddNeedsSslRenewalUserProvidedPagesDomainsIndex < ActiveRecord::Migration[6.0]
+ include Gitlab::Database::MigrationHelpers
+
+ DOWNTIME = false
+
+ INDEX_NAME = 'index_pages_domains_need_auto_ssl_renewal_user_provided'
+ INDEX_SCOPE = "auto_ssl_enabled = true AND auto_ssl_failed = false AND certificate_source = 0"
+
+ disable_ddl_transaction!
+
+ def up
+ add_concurrent_index(:pages_domains, :id, where: INDEX_SCOPE, name: INDEX_NAME)
+ end
+
+ def down
+ remove_concurrent_index(:pages_domains, :id, where: INDEX_SCOPE, name: INDEX_NAME)
+ end
+end
diff --git a/db/migrate/20200406100909_add_needs_ssl_renewal_valid_not_after_pages_domains_index.rb b/db/migrate/20200406100909_add_needs_ssl_renewal_valid_not_after_pages_domains_index.rb
new file mode 100644
index 00000000000..3c5db9ec082
--- /dev/null
+++ b/db/migrate/20200406100909_add_needs_ssl_renewal_valid_not_after_pages_domains_index.rb
@@ -0,0 +1,20 @@
+# frozen_string_literal: true
+
+class AddNeedsSslRenewalValidNotAfterPagesDomainsIndex < ActiveRecord::Migration[6.0]
+ include Gitlab::Database::MigrationHelpers
+
+ DOWNTIME = false
+
+ INDEX_NAME = 'index_pages_domains_need_auto_ssl_renewal_valid_not_after'
+ INDEX_SCOPE = "auto_ssl_enabled = true AND auto_ssl_failed = false"
+
+ disable_ddl_transaction!
+
+ def up
+ add_concurrent_index(:pages_domains, :certificate_valid_not_after, where: INDEX_SCOPE, name: INDEX_NAME)
+ end
+
+ def down
+ remove_concurrent_index(:pages_domains, :certificate_valid_not_after, where: INDEX_SCOPE, name: INDEX_NAME)
+ end
+end
diff --git a/db/migrate/20200406132529_add_resource_state_events_table.rb b/db/migrate/20200406132529_add_resource_state_events_table.rb
new file mode 100644
index 00000000000..ce241dff4dd
--- /dev/null
+++ b/db/migrate/20200406132529_add_resource_state_events_table.rb
@@ -0,0 +1,20 @@
+# frozen_string_literal: true
+
+class AddResourceStateEventsTable < ActiveRecord::Migration[6.0]
+ DOWNTIME = false
+
+ def change
+ create_table :resource_state_events, id: :bigserial do |t|
+ t.bigint :user_id, null: false
+ t.bigint :issue_id, null: true
+ t.bigint :merge_request_id, null: true
+
+ t.datetime_with_timezone :created_at, null: false
+ t.integer :state, limit: 2, null: false
+
+ t.index [:issue_id, :created_at], name: 'index_resource_state_events_on_issue_id_and_created_at'
+ t.index [:user_id], name: 'index_resource_state_events_on_user_id'
+ t.index [:merge_request_id], name: 'index_resource_state_events_on_merge_request_id'
+ end
+ end
+end
diff --git a/db/migrate/20200406141452_add_index_to_issue_id_and_created_at_on_resource_weight_events.rb b/db/migrate/20200406141452_add_index_to_issue_id_and_created_at_on_resource_weight_events.rb
new file mode 100644
index 00000000000..94a1b589ff9
--- /dev/null
+++ b/db/migrate/20200406141452_add_index_to_issue_id_and_created_at_on_resource_weight_events.rb
@@ -0,0 +1,19 @@
+# frozen_string_literal: true
+
+class AddIndexToIssueIdAndCreatedAtOnResourceWeightEvents < ActiveRecord::Migration[6.0]
+ include Gitlab::Database::MigrationHelpers
+
+ INDEX_NAME = 'index_resource_weight_events_on_issue_id_and_created_at'
+
+ DOWNTIME = false
+
+ disable_ddl_transaction!
+
+ def up
+ add_concurrent_index :resource_weight_events, [:issue_id, :created_at], name: INDEX_NAME
+ end
+
+ def down
+ remove_concurrent_index_by_name :resource_weight_events, INDEX_NAME
+ end
+end
diff --git a/db/migrate/20200406192059_add_write_registry_to_deploy_tokens.rb b/db/migrate/20200406192059_add_write_registry_to_deploy_tokens.rb
index 22fdb030edc..9d1628bc42a 100644
--- a/db/migrate/20200406192059_add_write_registry_to_deploy_tokens.rb
+++ b/db/migrate/20200406192059_add_write_registry_to_deploy_tokens.rb
@@ -8,7 +8,7 @@ class AddWriteRegistryToDeployTokens < ActiveRecord::Migration[6.0]
disable_ddl_transaction!
def up
- add_column_with_default(:deploy_tokens, :write_registry, :boolean, default: false, allow_null: false)
+ add_column_with_default(:deploy_tokens, :write_registry, :boolean, default: false, allow_null: false) # rubocop:disable Migration/AddColumnWithDefault
end
def down
diff --git a/db/migrate/20200407171133_add_protected_tag_create_access_levels_user_id_foreign_key.rb b/db/migrate/20200407171133_add_protected_tag_create_access_levels_user_id_foreign_key.rb
index 69222710026..79532b8179c 100644
--- a/db/migrate/20200407171133_add_protected_tag_create_access_levels_user_id_foreign_key.rb
+++ b/db/migrate/20200407171133_add_protected_tag_create_access_levels_user_id_foreign_key.rb
@@ -1,7 +1,5 @@
# frozen_string_literal: true
-# rubocop: disable Migration/AddConcurrentForeignKey
-# rubocop: disable Migration/WithLockRetriesWithoutDdlTransaction
class AddProtectedTagCreateAccessLevelsUserIdForeignKey < ActiveRecord::Migration[6.0]
include Gitlab::Database::MigrationHelpers
diff --git a/db/migrate/20200407182205_create_partitioned_foreign_keys.rb b/db/migrate/20200407182205_create_partitioned_foreign_keys.rb
new file mode 100644
index 00000000000..aca8116d2dd
--- /dev/null
+++ b/db/migrate/20200407182205_create_partitioned_foreign_keys.rb
@@ -0,0 +1,31 @@
+# frozen_string_literal: true
+
+class CreatePartitionedForeignKeys < ActiveRecord::Migration[6.0]
+ include Gitlab::Database::MigrationHelpers
+
+ DOWNTIME = false
+
+ disable_ddl_transaction!
+
+ def up
+ create_table :partitioned_foreign_keys do |t|
+ t.boolean :cascade_delete, null: false, default: true
+ t.text :from_table, null: false
+ t.text :from_column, null: false
+ t.text :to_table, null: false
+ t.text :to_column, null: false
+ end
+
+ add_text_limit :partitioned_foreign_keys, :from_table, 63
+ add_text_limit :partitioned_foreign_keys, :from_column, 63
+ add_text_limit :partitioned_foreign_keys, :to_table, 63
+ add_text_limit :partitioned_foreign_keys, :to_column, 63
+
+ add_index :partitioned_foreign_keys, [:to_table, :from_table, :from_column], unique: true,
+ name: "index_partitioned_foreign_keys_unique_index"
+ end
+
+ def down
+ drop_table :partitioned_foreign_keys
+ end
+end
diff --git a/db/migrate/20200407222647_create_project_repository_storage_moves.rb b/db/migrate/20200407222647_create_project_repository_storage_moves.rb
new file mode 100644
index 00000000000..402a1cdd4a6
--- /dev/null
+++ b/db/migrate/20200407222647_create_project_repository_storage_moves.rb
@@ -0,0 +1,31 @@
+# frozen_string_literal: true
+
+class CreateProjectRepositoryStorageMoves < ActiveRecord::Migration[6.0]
+ include Gitlab::Database::MigrationHelpers
+
+ DOWNTIME = false
+
+ disable_ddl_transaction!
+
+ def up
+ create_table :project_repository_storage_moves do |t|
+ t.timestamps_with_timezone
+ t.integer :project_id, limit: 8, null: false
+ t.integer :state, limit: 2, default: 1, null: false
+ t.text :source_storage_name, null: false
+ t.text :destination_storage_name, null: false
+ end
+
+ add_index :project_repository_storage_moves, :project_id
+
+ add_text_limit(:project_repository_storage_moves, :source_storage_name, 255, constraint_name: 'project_repository_storage_moves_source_storage_name')
+ add_text_limit(:project_repository_storage_moves, :destination_storage_name, 255, constraint_name: 'project_repository_storage_moves_destination_storage_name')
+ end
+
+ def down
+ remove_check_constraint(:project_repository_storage_moves, 'project_repository_storage_moves_source_storage_name')
+ remove_check_constraint(:project_repository_storage_moves, 'project_repository_storage_moves_destination_storage_name')
+
+ drop_table :project_repository_storage_moves
+ end
+end
diff --git a/db/migrate/20200408125046_create_ci_freeze_periods.rb b/db/migrate/20200408125046_create_ci_freeze_periods.rb
new file mode 100644
index 00000000000..42a385150b8
--- /dev/null
+++ b/db/migrate/20200408125046_create_ci_freeze_periods.rb
@@ -0,0 +1,30 @@
+# frozen_string_literal: true
+
+class CreateCiFreezePeriods < ActiveRecord::Migration[6.0]
+ include Gitlab::Database::MigrationHelpers
+
+ DOWNTIME = false
+
+ disable_ddl_transaction!
+
+ def up
+ unless table_exists?(:ci_freeze_periods)
+ create_table :ci_freeze_periods do |t|
+ t.references :project, foreign_key: true, null: false
+ t.text :freeze_start, null: false
+ t.text :freeze_end, null: false
+ t.text :cron_timezone, null: false
+
+ t.timestamps_with_timezone null: false
+ end
+ end
+
+ add_text_limit :ci_freeze_periods, :freeze_start, 998
+ add_text_limit :ci_freeze_periods, :freeze_end, 998
+ add_text_limit :ci_freeze_periods, :cron_timezone, 255
+ end
+
+ def down
+ drop_table :ci_freeze_periods
+ end
+end
diff --git a/db/migrate/20200408154331_add_protected_branch_merge_access_levels_user_id_foreign_key.rb b/db/migrate/20200408154331_add_protected_branch_merge_access_levels_user_id_foreign_key.rb
index 1b31da93fe7..0c5118c162b 100644
--- a/db/migrate/20200408154331_add_protected_branch_merge_access_levels_user_id_foreign_key.rb
+++ b/db/migrate/20200408154331_add_protected_branch_merge_access_levels_user_id_foreign_key.rb
@@ -1,7 +1,5 @@
# frozen_string_literal: true
-# rubocop: disable Migration/AddConcurrentForeignKey
-# rubocop: disable Migration/WithLockRetriesWithoutDdlTransaction
class AddProtectedBranchMergeAccessLevelsUserIdForeignKey < ActiveRecord::Migration[6.0]
include Gitlab::Database::MigrationHelpers
diff --git a/db/migrate/20200408154411_add_path_locks_user_id_foreign_key.rb b/db/migrate/20200408154411_add_path_locks_user_id_foreign_key.rb
index 5cab846d281..aae79c175bc 100644
--- a/db/migrate/20200408154411_add_path_locks_user_id_foreign_key.rb
+++ b/db/migrate/20200408154411_add_path_locks_user_id_foreign_key.rb
@@ -1,7 +1,5 @@
# frozen_string_literal: true
-# rubocop: disable Migration/AddConcurrentForeignKey
-# rubocop: disable Migration/WithLockRetriesWithoutDdlTransaction
class AddPathLocksUserIdForeignKey < ActiveRecord::Migration[6.0]
include Gitlab::Database::MigrationHelpers
diff --git a/db/migrate/20200408154455_add_protected_branch_push_access_levels_user_id_foreign_key.rb b/db/migrate/20200408154455_add_protected_branch_push_access_levels_user_id_foreign_key.rb
index c0c844b8853..1cfe240df05 100644
--- a/db/migrate/20200408154455_add_protected_branch_push_access_levels_user_id_foreign_key.rb
+++ b/db/migrate/20200408154455_add_protected_branch_push_access_levels_user_id_foreign_key.rb
@@ -1,7 +1,5 @@
# frozen_string_literal: true
-# rubocop: disable Migration/AddConcurrentForeignKey
-# rubocop: disable Migration/WithLockRetriesWithoutDdlTransaction
class AddProtectedBranchPushAccessLevelsUserIdForeignKey < ActiveRecord::Migration[6.0]
include Gitlab::Database::MigrationHelpers
diff --git a/db/migrate/20200408154604_add_u2f_registrations_user_id_foreign_key.rb b/db/migrate/20200408154604_add_u2f_registrations_user_id_foreign_key.rb
index 0dc8a967955..cdb81d73c99 100644
--- a/db/migrate/20200408154604_add_u2f_registrations_user_id_foreign_key.rb
+++ b/db/migrate/20200408154604_add_u2f_registrations_user_id_foreign_key.rb
@@ -1,7 +1,5 @@
# frozen_string_literal: true
-# rubocop: disable Migration/AddConcurrentForeignKey
-# rubocop: disable Migration/WithLockRetriesWithoutDdlTransaction
class AddU2fRegistrationsUserIdForeignKey < ActiveRecord::Migration[6.0]
include Gitlab::Database::MigrationHelpers
diff --git a/db/migrate/20200409105455_change_verification_checksum_field_type_in_package_file.rb b/db/migrate/20200409105455_change_verification_checksum_field_type_in_package_file.rb
new file mode 100644
index 00000000000..187ab0aa20b
--- /dev/null
+++ b/db/migrate/20200409105455_change_verification_checksum_field_type_in_package_file.rb
@@ -0,0 +1,17 @@
+# frozen_string_literal: true
+
+class ChangeVerificationChecksumFieldTypeInPackageFile < ActiveRecord::Migration[6.0]
+ DOWNTIME = false
+
+ def up
+ # The use of this column is behind a feature flag that never got enabled,
+ # so it's safe to remove it in a normal migration
+ remove_column :packages_package_files, :verification_checksum, :string # rubocop:disable Migration/RemoveColumn
+ add_column :packages_package_files, :verification_checksum, :binary
+ end
+
+ def down
+ remove_column :packages_package_files, :verification_checksum, :binary
+ add_column :packages_package_files, :verification_checksum, :string
+ end
+end
diff --git a/db/migrate/20200409105456_add_checksum_index_to_package_file.rb b/db/migrate/20200409105456_add_checksum_index_to_package_file.rb
new file mode 100644
index 00000000000..07762109895
--- /dev/null
+++ b/db/migrate/20200409105456_add_checksum_index_to_package_file.rb
@@ -0,0 +1,17 @@
+# frozen_string_literal: true
+
+class AddChecksumIndexToPackageFile < ActiveRecord::Migration[6.0]
+ include Gitlab::Database::MigrationHelpers
+
+ DOWNTIME = false
+
+ disable_ddl_transaction!
+
+ def up
+ add_concurrent_index :packages_package_files, :verification_checksum, where: "(verification_checksum IS NOT NULL)", name: "packages_packages_verification_checksum_partial"
+ end
+
+ def down
+ remove_concurrent_index :packages_package_files, :verification_checksum
+ end
+end
diff --git a/db/migrate/20200410104828_add_comment_detail_to_services.rb b/db/migrate/20200410104828_add_comment_detail_to_services.rb
new file mode 100644
index 00000000000..61d993cce32
--- /dev/null
+++ b/db/migrate/20200410104828_add_comment_detail_to_services.rb
@@ -0,0 +1,13 @@
+# frozen_string_literal: true
+
+class AddCommentDetailToServices < ActiveRecord::Migration[6.0]
+ DOWNTIME = false
+
+ def up
+ add_column :services, :comment_detail, :smallint
+ end
+
+ def down
+ remove_column :services, :comment_detail
+ end
+end
diff --git a/db/migrate/20200411125656_add_package_scopes_to_deploy_tokens.rb b/db/migrate/20200411125656_add_package_scopes_to_deploy_tokens.rb
new file mode 100644
index 00000000000..e7e2f91d4d1
--- /dev/null
+++ b/db/migrate/20200411125656_add_package_scopes_to_deploy_tokens.rb
@@ -0,0 +1,19 @@
+# frozen_string_literal: true
+
+class AddPackageScopesToDeployTokens < ActiveRecord::Migration[6.0]
+ include Gitlab::Database::MigrationHelpers
+
+ DOWNTIME = false
+
+ disable_ddl_transaction!
+
+ def up
+ add_column_with_default(:deploy_tokens, :read_package_registry, :boolean, default: false, allow_null: false) # rubocop:disable Migration/AddColumnWithDefault
+ add_column_with_default(:deploy_tokens, :write_package_registry, :boolean, default: false, allow_null: false) # rubocop:disable Migration/AddColumnWithDefault
+ end
+
+ def down
+ remove_column(:deploy_tokens, :read_package_registry)
+ remove_column(:deploy_tokens, :write_package_registry)
+ end
+end
diff --git a/db/migrate/20200413072059_add_group_owners_can_manage_default_branch_protection_to_application_settings.rb b/db/migrate/20200413072059_add_group_owners_can_manage_default_branch_protection_to_application_settings.rb
index eea564d00f4..2918aaea93e 100644
--- a/db/migrate/20200413072059_add_group_owners_can_manage_default_branch_protection_to_application_settings.rb
+++ b/db/migrate/20200413072059_add_group_owners_can_manage_default_branch_protection_to_application_settings.rb
@@ -7,7 +7,7 @@ class AddGroupOwnersCanManageDefaultBranchProtectionToApplicationSettings < Acti
disable_ddl_transaction!
def up
- add_column_with_default(:application_settings,
+ add_column_with_default(:application_settings, # rubocop:disable Migration/AddColumnWithDefault
:group_owners_can_manage_default_branch_protection,
:boolean,
default: true)
diff --git a/db/migrate/20200413230056_add_waf_and_cilium_logs_to_applications_fluentd.rb b/db/migrate/20200413230056_add_waf_and_cilium_logs_to_applications_fluentd.rb
new file mode 100644
index 00000000000..99f6aa0e4a8
--- /dev/null
+++ b/db/migrate/20200413230056_add_waf_and_cilium_logs_to_applications_fluentd.rb
@@ -0,0 +1,29 @@
+# frozen_string_literal: true
+
+class AddWafAndCiliumLogsToApplicationsFluentd < ActiveRecord::Migration[6.0]
+ include Gitlab::Database::MigrationHelpers
+
+ DOWNTIME = false
+
+ disable_ddl_transaction!
+
+ def up
+ add_column_with_default(:clusters_applications_fluentd, # rubocop:disable Migration/AddColumnWithDefault
+ :waf_log_enabled,
+ :boolean,
+ default: true,
+ allow_null: false)
+ add_column_with_default(:clusters_applications_fluentd, # rubocop:disable Migration/AddColumnWithDefault
+ :cilium_log_enabled,
+ :boolean,
+ default: true,
+ allow_null: false)
+ end
+
+ def down
+ remove_column(:clusters_applications_fluentd,
+ :waf_log_enabled)
+ remove_column(:clusters_applications_fluentd,
+ :cilium_log_enabled)
+ end
+end
diff --git a/db/migrate/20200414112444_add_group_id_to_vulnerability_exports.rb b/db/migrate/20200414112444_add_group_id_to_vulnerability_exports.rb
new file mode 100644
index 00000000000..fad63d53a81
--- /dev/null
+++ b/db/migrate/20200414112444_add_group_id_to_vulnerability_exports.rb
@@ -0,0 +1,10 @@
+# frozen_string_literal: true
+
+class AddGroupIdToVulnerabilityExports < ActiveRecord::Migration[6.0]
+ DOWNTIME = false
+
+ def change
+ add_column :vulnerability_exports, :group_id, :integer
+ change_column_null :vulnerability_exports, :project_id, true
+ end
+end
diff --git a/db/migrate/20200414114611_add_group_id_index_and_fk_to_vulnerability_exports.rb b/db/migrate/20200414114611_add_group_id_index_and_fk_to_vulnerability_exports.rb
new file mode 100644
index 00000000000..a3e60b87857
--- /dev/null
+++ b/db/migrate/20200414114611_add_group_id_index_and_fk_to_vulnerability_exports.rb
@@ -0,0 +1,20 @@
+# frozen_string_literal: true
+
+class AddGroupIdIndexAndFkToVulnerabilityExports < ActiveRecord::Migration[6.0]
+ include Gitlab::Database::MigrationHelpers
+
+ DOWNTIME = false
+ GROUP_ID_INDEX_NAME = 'index_vulnerability_exports_on_group_id_not_null'
+
+ disable_ddl_transaction!
+
+ def up
+ add_concurrent_index(:vulnerability_exports, :group_id, where: 'group_id IS NOT NULL', name: GROUP_ID_INDEX_NAME)
+ add_concurrent_foreign_key(:vulnerability_exports, :namespaces, column: :group_id)
+ end
+
+ def down
+ remove_foreign_key(:vulnerability_exports, column: :group_id)
+ remove_concurrent_index_by_name(:vulnerability_exports, GROUP_ID_INDEX_NAME)
+ end
+end
diff --git a/db/migrate/20200414115801_change_project_index_on_vulnerability_exports.rb b/db/migrate/20200414115801_change_project_index_on_vulnerability_exports.rb
new file mode 100644
index 00000000000..e669faa7fca
--- /dev/null
+++ b/db/migrate/20200414115801_change_project_index_on_vulnerability_exports.rb
@@ -0,0 +1,21 @@
+# frozen_string_literal: true
+
+class ChangeProjectIndexOnVulnerabilityExports < ActiveRecord::Migration[6.0]
+ include Gitlab::Database::MigrationHelpers
+
+ DOWNTIME = false
+ OLD_INDEX_NAME = 'index_vulnerability_exports_on_project_id_and_id'
+ NEW_INDEX_NAME = 'index_vulnerability_exports_on_project_id_not_null'
+
+ disable_ddl_transaction!
+
+ def up
+ add_concurrent_index(:vulnerability_exports, :project_id, where: 'project_id IS NOT NULL', name: NEW_INDEX_NAME)
+ remove_concurrent_index_by_name(:vulnerability_exports, OLD_INDEX_NAME)
+ end
+
+ def down
+ add_concurrent_index(:vulnerability_exports, [:project_id, :id], unique: true, name: OLD_INDEX_NAME)
+ remove_concurrent_index_by_name(:vulnerability_exports, NEW_INDEX_NAME)
+ end
+end
diff --git a/db/migrate/20200415153154_add_unique_index_on_plan_name.rb b/db/migrate/20200415153154_add_unique_index_on_plan_name.rb
new file mode 100644
index 00000000000..d959d1315b4
--- /dev/null
+++ b/db/migrate/20200415153154_add_unique_index_on_plan_name.rb
@@ -0,0 +1,19 @@
+# frozen_string_literal: true
+
+class AddUniqueIndexOnPlanName < ActiveRecord::Migration[6.0]
+ include Gitlab::Database::MigrationHelpers
+
+ DOWNTIME = false
+
+ disable_ddl_transaction!
+
+ def up
+ remove_concurrent_index :plans, :name
+ add_concurrent_index :plans, :name, unique: true
+ end
+
+ def down
+ remove_concurrent_index :plans, :name, unique: true
+ add_concurrent_index :plans, :name
+ end
+end
diff --git a/db/migrate/20200415203024_add_offset_pagination_plan_limit.rb b/db/migrate/20200415203024_add_offset_pagination_plan_limit.rb
new file mode 100644
index 00000000000..b4d4be894f6
--- /dev/null
+++ b/db/migrate/20200415203024_add_offset_pagination_plan_limit.rb
@@ -0,0 +1,9 @@
+# frozen_string_literal: true
+
+class AddOffsetPaginationPlanLimit < ActiveRecord::Migration[6.0]
+ DOWNTIME = false
+
+ def change
+ add_column :plan_limits, :offset_pagination_limit, :integer, default: 50000, null: false
+ end
+end
diff --git a/db/migrate/20200416005331_create_status_page_published_incidents.rb b/db/migrate/20200416005331_create_status_page_published_incidents.rb
new file mode 100644
index 00000000000..75889cd5bb6
--- /dev/null
+++ b/db/migrate/20200416005331_create_status_page_published_incidents.rb
@@ -0,0 +1,20 @@
+# frozen_string_literal: true
+
+class CreateStatusPagePublishedIncidents < ActiveRecord::Migration[6.0]
+ include Gitlab::Database::MigrationHelpers
+
+ DOWNTIME = false
+
+ def up
+ with_lock_retries do
+ create_table :status_page_published_incidents do |t|
+ t.timestamps_with_timezone null: false
+ t.references :issue, null: false, index: { unique: true }, foreign_key: { on_delete: :cascade }
+ end
+ end
+ end
+
+ def down
+ drop_table :status_page_published_incidents
+ end
+end
diff --git a/db/migrate/20200416120128_add_columns_to_terraform_state.rb b/db/migrate/20200416120128_add_columns_to_terraform_state.rb
index e657b1fde3f..65d25d842e5 100644
--- a/db/migrate/20200416120128_add_columns_to_terraform_state.rb
+++ b/db/migrate/20200416120128_add_columns_to_terraform_state.rb
@@ -3,6 +3,7 @@
class AddColumnsToTerraformState < ActiveRecord::Migration[6.0]
DOWNTIME = false
+ # rubocop:disable Migration/PreventStrings
def change
add_column :terraform_states, :lock_xid, :string, limit: 255
add_column :terraform_states, :locked_at, :datetime_with_timezone
@@ -14,4 +15,5 @@ class AddColumnsToTerraformState < ActiveRecord::Migration[6.0]
add_index :terraform_states, [:project_id, :name], unique: true # rubocop:disable Migration/AddIndex (table not used yet)
remove_index :terraform_states, :project_id # rubocop:disable Migration/RemoveIndex (table not used yet)
end
+ # rubocop:enable Migration/PreventStrings
end
diff --git a/db/migrate/20200417044453_create_alert_management_alerts.rb b/db/migrate/20200417044453_create_alert_management_alerts.rb
new file mode 100644
index 00000000000..6221eeeb24b
--- /dev/null
+++ b/db/migrate/20200417044453_create_alert_management_alerts.rb
@@ -0,0 +1,44 @@
+# frozen_string_literal: true
+
+class CreateAlertManagementAlerts < ActiveRecord::Migration[6.0]
+ include Gitlab::Database::MigrationHelpers
+
+ DOWNTIME = false
+
+ disable_ddl_transaction!
+
+ def up
+ unless table_exists?(:alert_management_alerts)
+ create_table :alert_management_alerts do |t|
+ t.timestamps_with_timezone
+ t.datetime_with_timezone :started_at, null: false
+ t.datetime_with_timezone :ended_at
+ t.integer :events, default: 1, null: false
+ t.integer :iid, null: false
+ t.integer :severity, default: 0, null: false, limit: 2
+ t.integer :status, default: 0, null: false, limit: 2
+ t.binary :fingerprint
+ t.bigint :issue_id, index: true
+ t.bigint :project_id, null: false
+ t.text :title, null: false
+ t.text :description
+ t.text :service
+ t.text :monitoring_tool
+ t.text :hosts, array: true, null: false, default: [] # rubocop:disable Migration/AddLimitToTextColumns
+ t.jsonb :payload, null: false, default: {}
+
+ t.index %w(project_id iid), name: 'index_alert_management_alerts_on_project_id_and_iid', unique: true, using: :btree
+ t.index %w(project_id fingerprint), name: 'index_alert_management_alerts_on_project_id_and_fingerprint', unique: true, using: :btree
+ end
+ end
+
+ add_text_limit :alert_management_alerts, :title, 200
+ add_text_limit :alert_management_alerts, :description, 1000
+ add_text_limit :alert_management_alerts, :service, 100
+ add_text_limit :alert_management_alerts, :monitoring_tool, 100
+ end
+
+ def down
+ drop_table :alert_management_alerts
+ end
+end
diff --git a/db/migrate/20200417075843_remove_and_add_foreign_key_to_project_settings.rb b/db/migrate/20200417075843_remove_and_add_foreign_key_to_project_settings.rb
new file mode 100644
index 00000000000..695bd3381c8
--- /dev/null
+++ b/db/migrate/20200417075843_remove_and_add_foreign_key_to_project_settings.rb
@@ -0,0 +1,19 @@
+# frozen_string_literal: true
+
+class RemoveAndAddForeignKeyToProjectSettings < ActiveRecord::Migration[6.0]
+ include Gitlab::Database::MigrationHelpers
+
+ DOWNTIME = false
+ CONSTRAINT_NAME = 'fk_project_settings_push_rule_id'
+ disable_ddl_transaction!
+
+ def up
+ add_concurrent_foreign_key :project_settings, :push_rules, column: :push_rule_id, name: CONSTRAINT_NAME, on_delete: :nullify
+ remove_foreign_key_if_exists :project_settings, column: :push_rule_id, on_delete: :cascade
+ end
+
+ def down
+ add_concurrent_foreign_key :project_settings, :push_rules, column: :push_rule_id, on_delete: :cascade
+ remove_foreign_key_if_exists :project_settings, column: :push_rule_id, name: CONSTRAINT_NAME, on_delete: :nullify
+ end
+end
diff --git a/db/migrate/20200417145946_add_locked_to_ci_job_artifact.rb b/db/migrate/20200417145946_add_locked_to_ci_job_artifact.rb
new file mode 100644
index 00000000000..3bcc733f3b0
--- /dev/null
+++ b/db/migrate/20200417145946_add_locked_to_ci_job_artifact.rb
@@ -0,0 +1,19 @@
+# frozen_string_literal: true
+
+class AddLockedToCiJobArtifact < ActiveRecord::Migration[6.0]
+ include Gitlab::Database::MigrationHelpers
+
+ DOWNTIME = false
+
+ def up
+ with_lock_retries do
+ add_column :ci_job_artifacts, :locked, :boolean
+ end
+ end
+
+ def down
+ with_lock_retries do
+ remove_column :ci_job_artifacts, :locked
+ end
+ end
+end
diff --git a/db/migrate/20200420092011_add_profile_image_guidelines_to_appearances.rb b/db/migrate/20200420092011_add_profile_image_guidelines_to_appearances.rb
new file mode 100644
index 00000000000..bab3e21e285
--- /dev/null
+++ b/db/migrate/20200420092011_add_profile_image_guidelines_to_appearances.rb
@@ -0,0 +1,21 @@
+# frozen_string_literal: true
+
+class AddProfileImageGuidelinesToAppearances < ActiveRecord::Migration[6.0]
+ include Gitlab::Database::MigrationHelpers
+
+ DOWNTIME = false
+
+ disable_ddl_transaction!
+
+ def up
+ add_column :appearances, :profile_image_guidelines, :text, null: true
+ add_column :appearances, :profile_image_guidelines_html, :text, null: true # rubocop:disable Migration/AddLimitToTextColumns
+
+ add_text_limit :appearances, :profile_image_guidelines, 4096, constraint_name: 'appearances_profile_image_guidelines'
+ end
+
+ def down
+ remove_column :appearances, :profile_image_guidelines
+ remove_column :appearances, :profile_image_guidelines_html
+ end
+end
diff --git a/db/migrate/20200420104303_add_group_import_states_table.rb b/db/migrate/20200420104303_add_group_import_states_table.rb
new file mode 100644
index 00000000000..a44a2ea75f3
--- /dev/null
+++ b/db/migrate/20200420104303_add_group_import_states_table.rb
@@ -0,0 +1,25 @@
+# frozen_string_literal: true
+
+class AddGroupImportStatesTable < ActiveRecord::Migration[6.0]
+ include Gitlab::Database::MigrationHelpers
+
+ DOWNTIME = false
+
+ # rubocop:disable Migration/AddLimitToTextColumns
+ def up
+ with_lock_retries do
+ create_table :group_import_states, id: false do |t|
+ t.references :group, primary_key: true, foreign_key: { to_table: :namespaces, on_delete: :cascade }
+ t.timestamps_with_timezone null: false
+ t.integer :status, limit: 2, null: false, default: 0
+ t.text :jid, null: false, unique: true
+ t.text :last_error
+ end
+ end
+ end
+ # rubocop:enable Migration/AddLimitToTextColumns
+
+ def down
+ drop_table :group_import_states
+ end
+end
diff --git a/db/migrate/20200420104323_add_text_limit_to_group_import_states.rb b/db/migrate/20200420104323_add_text_limit_to_group_import_states.rb
new file mode 100644
index 00000000000..c21b18d7b0e
--- /dev/null
+++ b/db/migrate/20200420104323_add_text_limit_to_group_import_states.rb
@@ -0,0 +1,19 @@
+# frozen_string_literal: true
+
+class AddTextLimitToGroupImportStates < ActiveRecord::Migration[6.0]
+ include Gitlab::Database::MigrationHelpers
+
+ DOWNTIME = false
+
+ disable_ddl_transaction!
+
+ def up
+ add_text_limit :group_import_states, :jid, 100
+ add_text_limit :group_import_states, :last_error, 255
+ end
+
+ def down
+ remove_text_limit :group_import_states, :jid
+ remove_text_limit :group_import_states, :last_error
+ end
+end
diff --git a/db/migrate/20200420115948_create_metrics_users_starred_dashboard.rb b/db/migrate/20200420115948_create_metrics_users_starred_dashboard.rb
new file mode 100644
index 00000000000..27130136e9d
--- /dev/null
+++ b/db/migrate/20200420115948_create_metrics_users_starred_dashboard.rb
@@ -0,0 +1,25 @@
+# frozen_string_literal: true
+
+class CreateMetricsUsersStarredDashboard < ActiveRecord::Migration[6.0]
+ DOWNTIME = false
+
+ # limit added in following migration db/migrate/20200424101920_add_text_limit_to_metrics_users_starred_dashboards_dashboard_path.rb
+ # to allow this migration to be run inside the transaction
+ # rubocop: disable Migration/AddLimitToTextColumns
+ def up
+ create_table :metrics_users_starred_dashboards do |t|
+ t.timestamps_with_timezone
+ t.bigint :project_id, null: false
+ t.bigint :user_id, null: false
+ t.text :dashboard_path, null: false
+
+ t.index :project_id
+ t.index %i(user_id project_id dashboard_path), name: "idx_metrics_users_starred_dashboard_on_user_project_dashboard", unique: true
+ end
+ end
+ # rubocop: enable Migration/AddLimitToTextColumns
+
+ def down
+ drop_table :metrics_users_starred_dashboards
+ end
+end
diff --git a/db/migrate/20200420141733_add_index_on_enabled_clusters.rb b/db/migrate/20200420141733_add_index_on_enabled_clusters.rb
new file mode 100644
index 00000000000..43f4b072c82
--- /dev/null
+++ b/db/migrate/20200420141733_add_index_on_enabled_clusters.rb
@@ -0,0 +1,18 @@
+# frozen_string_literal: true
+
+class AddIndexOnEnabledClusters < ActiveRecord::Migration[6.0]
+ include Gitlab::Database::MigrationHelpers
+
+ DOWNTIME = false
+ INDEX_NAME = 'index_enabled_clusters_on_id'
+
+ disable_ddl_transaction!
+
+ def up
+ add_concurrent_index :clusters, [:id], where: 'enabled = true', name: INDEX_NAME
+ end
+
+ def down
+ remove_concurrent_index :clusters, [:id], where: 'enabled = true', name: INDEX_NAME
+ end
+end
diff --git a/db/migrate/20200420172113_add_text_limit_to_sprints_title.rb b/db/migrate/20200420172113_add_text_limit_to_sprints_title.rb
new file mode 100644
index 00000000000..17707cc9dd1
--- /dev/null
+++ b/db/migrate/20200420172113_add_text_limit_to_sprints_title.rb
@@ -0,0 +1,19 @@
+# frozen_string_literal: true
+
+class AddTextLimitToSprintsTitle < ActiveRecord::Migration[6.0]
+ include Gitlab::Database::MigrationHelpers
+
+ DOWNTIME = false
+
+ disable_ddl_transaction!
+
+ CONSTRAINT_NAME = 'sprints_title'
+
+ def up
+ add_text_limit :sprints, :title, 255, constraint_name: CONSTRAINT_NAME
+ end
+
+ def down
+ remove_check_constraint :sprints, CONSTRAINT_NAME
+ end
+end
diff --git a/db/migrate/20200420172752_add_sprints_foreign_key_to_projects.rb b/db/migrate/20200420172752_add_sprints_foreign_key_to_projects.rb
new file mode 100644
index 00000000000..46b5c9c2ac2
--- /dev/null
+++ b/db/migrate/20200420172752_add_sprints_foreign_key_to_projects.rb
@@ -0,0 +1,19 @@
+# frozen_string_literal: true
+
+class AddSprintsForeignKeyToProjects < ActiveRecord::Migration[6.0]
+ include Gitlab::Database::MigrationHelpers
+
+ DOWNTIME = false
+
+ disable_ddl_transaction!
+
+ def up
+ add_concurrent_foreign_key :sprints, :projects, column: :project_id, on_delete: :cascade
+ end
+
+ def down
+ with_lock_retries do
+ remove_foreign_key :sprints, column: :project_id
+ end
+ end
+end
diff --git a/db/migrate/20200420172927_add_sprints_foreign_key_to_groups.rb b/db/migrate/20200420172927_add_sprints_foreign_key_to_groups.rb
new file mode 100644
index 00000000000..cd90fe50cde
--- /dev/null
+++ b/db/migrate/20200420172927_add_sprints_foreign_key_to_groups.rb
@@ -0,0 +1,19 @@
+# frozen_string_literal: true
+
+class AddSprintsForeignKeyToGroups < ActiveRecord::Migration[6.0]
+ include Gitlab::Database::MigrationHelpers
+
+ DOWNTIME = false
+
+ disable_ddl_transaction!
+
+ def up
+ add_concurrent_foreign_key :sprints, :namespaces, column: :group_id, on_delete: :cascade
+ end
+
+ def down
+ with_lock_retries do
+ remove_foreign_key :sprints, column: :group_id
+ end
+ end
+end
diff --git a/db/migrate/20200420201933_add_check_constraint_to_sprint_must_belong_to_project_or_group.rb b/db/migrate/20200420201933_add_check_constraint_to_sprint_must_belong_to_project_or_group.rb
new file mode 100644
index 00000000000..9962eab95e3
--- /dev/null
+++ b/db/migrate/20200420201933_add_check_constraint_to_sprint_must_belong_to_project_or_group.rb
@@ -0,0 +1,19 @@
+# frozen_string_literal: true
+
+class AddCheckConstraintToSprintMustBelongToProjectOrGroup < ActiveRecord::Migration[6.0]
+ include Gitlab::Database::MigrationHelpers
+
+ DOWNTIME = false
+
+ disable_ddl_transaction!
+
+ CONSTRAINT_NAME = 'sprints_must_belong_to_project_or_group'
+
+ def up
+ add_check_constraint :sprints, '(project_id != NULL AND group_id IS NULL) OR (group_id != NULL AND project_id IS NULL)', CONSTRAINT_NAME
+ end
+
+ def down
+ remove_check_constraint :sprints, CONSTRAINT_NAME
+ end
+end
diff --git a/db/migrate/20200421092907_add_index_container_repository_on_name_trigram_to_container_repository.rb b/db/migrate/20200421092907_add_index_container_repository_on_name_trigram_to_container_repository.rb
new file mode 100644
index 00000000000..debca8c6008
--- /dev/null
+++ b/db/migrate/20200421092907_add_index_container_repository_on_name_trigram_to_container_repository.rb
@@ -0,0 +1,18 @@
+# frozen_string_literal: true
+
+class AddIndexContainerRepositoryOnNameTrigramToContainerRepository < ActiveRecord::Migration[6.0]
+ include Gitlab::Database::MigrationHelpers
+
+ DOWNTIME = false
+ INDEX_NAME = 'index_container_repository_on_name_trigram'
+
+ disable_ddl_transaction!
+
+ def up
+ add_concurrent_index :container_repositories, :name, name: INDEX_NAME, using: :gin, opclass: { name: :gin_trgm_ops }
+ end
+
+ def down
+ remove_concurrent_index_by_name(:container_repositories, INDEX_NAME)
+ end
+end
diff --git a/db/migrate/20200421111005_create_daily_build_group_report_results.rb b/db/migrate/20200421111005_create_daily_build_group_report_results.rb
new file mode 100644
index 00000000000..12d1c7531d5
--- /dev/null
+++ b/db/migrate/20200421111005_create_daily_build_group_report_results.rb
@@ -0,0 +1,21 @@
+# frozen_string_literal: true
+
+class CreateDailyBuildGroupReportResults < ActiveRecord::Migration[6.0]
+ DOWNTIME = false
+
+ def change
+ create_table :ci_daily_build_group_report_results do |t|
+ t.date :date, null: false
+ t.bigint :project_id, null: false
+ t.bigint :last_pipeline_id, null: false
+ t.text :ref_path, null: false # rubocop:disable Migration/AddLimitToTextColumns
+ t.text :group_name, null: false # rubocop:disable Migration/AddLimitToTextColumns
+ t.jsonb :data, null: false
+
+ t.index :last_pipeline_id
+ t.index [:project_id, :ref_path, :date, :group_name], name: 'index_daily_build_group_report_results_unique_columns', unique: true
+ t.foreign_key :projects, on_delete: :cascade
+ t.foreign_key :ci_pipelines, column: :last_pipeline_id, on_delete: :cascade
+ end
+ end
+end
diff --git a/db/migrate/20200421233150_add_foreign_keys_for_alert_management_alerts.rb b/db/migrate/20200421233150_add_foreign_keys_for_alert_management_alerts.rb
new file mode 100644
index 00000000000..b16bdf9830c
--- /dev/null
+++ b/db/migrate/20200421233150_add_foreign_keys_for_alert_management_alerts.rb
@@ -0,0 +1,19 @@
+# frozen_string_literal: true
+
+class AddForeignKeysForAlertManagementAlerts < ActiveRecord::Migration[6.0]
+ include Gitlab::Database::MigrationHelpers
+
+ DOWNTIME = false
+
+ disable_ddl_transaction!
+
+ def up
+ add_concurrent_foreign_key :alert_management_alerts, :projects, column: :project_id, on_delete: :cascade
+ add_concurrent_foreign_key :alert_management_alerts, :issues, column: :issue_id, on_delete: :nullify
+ end
+
+ def down
+ remove_foreign_key_if_exists :alert_management_alerts, column: :project_id
+ remove_foreign_key_if_exists :alert_management_alerts, column: :issue_id
+ end
+end
diff --git a/db/migrate/20200422091541_create_ci_instance_variables.rb b/db/migrate/20200422091541_create_ci_instance_variables.rb
new file mode 100644
index 00000000000..ab2a4722f89
--- /dev/null
+++ b/db/migrate/20200422091541_create_ci_instance_variables.rb
@@ -0,0 +1,31 @@
+# frozen_string_literal: true
+
+class CreateCiInstanceVariables < ActiveRecord::Migration[6.0]
+ include Gitlab::Database::MigrationHelpers
+ DOWNTIME = false
+
+ disable_ddl_transaction!
+
+ def up
+ unless table_exists?(:ci_instance_variables)
+ create_table :ci_instance_variables do |t|
+ t.integer :variable_type, null: false, limit: 2, default: 1
+ t.boolean :masked, default: false, allow_null: false
+ t.boolean :protected, default: false, allow_null: false
+ t.text :key, null: false
+ t.text :encrypted_value
+ t.text :encrypted_value_iv
+
+ t.index [:key], name: 'index_ci_instance_variables_on_key', unique: true, using: :btree
+ end
+ end
+
+ add_text_limit(:ci_instance_variables, :key, 255)
+ add_text_limit(:ci_instance_variables, :encrypted_value, 1024)
+ add_text_limit(:ci_instance_variables, :encrypted_value_iv, 255)
+ end
+
+ def down
+ drop_table :ci_instance_variables
+ end
+end
diff --git a/db/migrate/20200422213749_create_operations_strategies_user_lists.rb b/db/migrate/20200422213749_create_operations_strategies_user_lists.rb
new file mode 100644
index 00000000000..113f2f2f54a
--- /dev/null
+++ b/db/migrate/20200422213749_create_operations_strategies_user_lists.rb
@@ -0,0 +1,14 @@
+# frozen_string_literal: true
+
+class CreateOperationsStrategiesUserLists < ActiveRecord::Migration[6.0]
+ DOWNTIME = false
+
+ def change
+ create_table :operations_strategies_user_lists do |t|
+ t.references :strategy, index: false, foreign_key: { on_delete: :cascade, to_table: :operations_strategies }, null: false
+ t.references :user_list, index: true, foreign_key: { on_delete: :cascade, to_table: :operations_user_lists }, null: false
+
+ t.index [:strategy_id, :user_list_id], unique: true, name: :index_ops_strategies_user_lists_on_strategy_id_and_user_list_id
+ end
+ end
+end
diff --git a/db/migrate/20200423075720_add_user_id_foreign_key_to_resource_state_events.rb b/db/migrate/20200423075720_add_user_id_foreign_key_to_resource_state_events.rb
new file mode 100644
index 00000000000..702347e5d43
--- /dev/null
+++ b/db/migrate/20200423075720_add_user_id_foreign_key_to_resource_state_events.rb
@@ -0,0 +1,19 @@
+# frozen_string_literal: true
+
+class AddUserIdForeignKeyToResourceStateEvents < ActiveRecord::Migration[6.0]
+ include Gitlab::Database::MigrationHelpers
+
+ DOWNTIME = false
+
+ def up
+ with_lock_retries do
+ add_foreign_key :resource_state_events, :users, column: :user_id, on_delete: :nullify # rubocop:disable Migration/AddConcurrentForeignKey
+ end
+ end
+
+ def down
+ with_lock_retries do
+ remove_foreign_key :resource_state_events, column: :user_id
+ end
+ end
+end
diff --git a/db/migrate/20200423080334_add_issue_id_foreign_key_to_resource_state_events.rb b/db/migrate/20200423080334_add_issue_id_foreign_key_to_resource_state_events.rb
new file mode 100644
index 00000000000..660c51eb3a6
--- /dev/null
+++ b/db/migrate/20200423080334_add_issue_id_foreign_key_to_resource_state_events.rb
@@ -0,0 +1,19 @@
+# frozen_string_literal: true
+
+class AddIssueIdForeignKeyToResourceStateEvents < ActiveRecord::Migration[6.0]
+ include Gitlab::Database::MigrationHelpers
+
+ DOWNTIME = false
+
+ def up
+ with_lock_retries do
+ add_foreign_key :resource_state_events, :issues, column: :issue_id, on_delete: :cascade # rubocop:disable Migration/AddConcurrentForeignKey
+ end
+ end
+
+ def down
+ with_lock_retries do
+ remove_foreign_key :resource_state_events, column: :issue_id
+ end
+ end
+end
diff --git a/db/migrate/20200423080607_add_merge_request_id_foreign_key_to_resource_state_events.rb b/db/migrate/20200423080607_add_merge_request_id_foreign_key_to_resource_state_events.rb
new file mode 100644
index 00000000000..4f0a689a992
--- /dev/null
+++ b/db/migrate/20200423080607_add_merge_request_id_foreign_key_to_resource_state_events.rb
@@ -0,0 +1,19 @@
+# frozen_string_literal: true
+
+class AddMergeRequestIdForeignKeyToResourceStateEvents < ActiveRecord::Migration[6.0]
+ include Gitlab::Database::MigrationHelpers
+
+ DOWNTIME = false
+
+ def up
+ with_lock_retries do
+ add_foreign_key :resource_state_events, :merge_requests, column: :merge_request_id, on_delete: :cascade # rubocop:disable Migration/AddConcurrentForeignKey
+ end
+ end
+
+ def down
+ with_lock_retries do
+ remove_foreign_key :resource_state_events, column: :merge_request_id
+ end
+ end
+end
diff --git a/db/migrate/20200423081409_add_constraint_to_resource_state_events_must_belong_to_issue_or_merge_request.rb b/db/migrate/20200423081409_add_constraint_to_resource_state_events_must_belong_to_issue_or_merge_request.rb
new file mode 100644
index 00000000000..57df1045e2c
--- /dev/null
+++ b/db/migrate/20200423081409_add_constraint_to_resource_state_events_must_belong_to_issue_or_merge_request.rb
@@ -0,0 +1,19 @@
+# frozen_string_literal: true
+
+class AddConstraintToResourceStateEventsMustBelongToIssueOrMergeRequest < ActiveRecord::Migration[6.0]
+ include Gitlab::Database::MigrationHelpers
+
+ DOWNTIME = false
+
+ disable_ddl_transaction!
+
+ CONSTRAINT_NAME = 'resource_state_events_must_belong_to_issue_or_merge_request'
+
+ def up
+ add_check_constraint :resource_state_events, '(issue_id != NULL AND merge_request_id IS NULL) OR (merge_request_id != NULL AND issue_id IS NULL)', CONSTRAINT_NAME
+ end
+
+ def down
+ remove_check_constraint :resource_state_events, CONSTRAINT_NAME
+ end
+end
diff --git a/db/migrate/20200423081441_add_foreign_key_from_users_to_metrics_users_starred_dashboars.rb b/db/migrate/20200423081441_add_foreign_key_from_users_to_metrics_users_starred_dashboars.rb
new file mode 100644
index 00000000000..c3a7635193c
--- /dev/null
+++ b/db/migrate/20200423081441_add_foreign_key_from_users_to_metrics_users_starred_dashboars.rb
@@ -0,0 +1,18 @@
+# frozen_string_literal: true
+
+class AddForeignKeyFromUsersToMetricsUsersStarredDashboars < ActiveRecord::Migration[6.0]
+ include Gitlab::Database::MigrationHelpers
+
+ DOWNTIME = false
+ disable_ddl_transaction!
+
+ def up
+ add_concurrent_foreign_key :metrics_users_starred_dashboards, :users, column: :user_id, on_delete: :cascade
+ end
+
+ def down
+ with_lock_retries do
+ remove_foreign_key_if_exists :metrics_users_starred_dashboards, column: :user_id
+ end
+ end
+end
diff --git a/db/migrate/20200423081519_add_foreign_key_from_projects_to_metrics_users_starred_dashboars.rb b/db/migrate/20200423081519_add_foreign_key_from_projects_to_metrics_users_starred_dashboars.rb
new file mode 100644
index 00000000000..88565c24eca
--- /dev/null
+++ b/db/migrate/20200423081519_add_foreign_key_from_projects_to_metrics_users_starred_dashboars.rb
@@ -0,0 +1,18 @@
+# frozen_string_literal: true
+
+class AddForeignKeyFromProjectsToMetricsUsersStarredDashboars < ActiveRecord::Migration[6.0]
+ include Gitlab::Database::MigrationHelpers
+
+ DOWNTIME = false
+ disable_ddl_transaction!
+
+ def up
+ add_concurrent_foreign_key :metrics_users_starred_dashboards, :projects, column: :project_id, on_delete: :cascade
+ end
+
+ def down
+ with_lock_retries do
+ remove_foreign_key_if_exists :metrics_users_starred_dashboards, column: :project_id
+ end
+ end
+end
diff --git a/db/migrate/20200423101529_add_scheduled_at_to_jira_imports.rb b/db/migrate/20200423101529_add_scheduled_at_to_jira_imports.rb
new file mode 100644
index 00000000000..f0d9393b6f9
--- /dev/null
+++ b/db/migrate/20200423101529_add_scheduled_at_to_jira_imports.rb
@@ -0,0 +1,9 @@
+# frozen_string_literal: true
+
+class AddScheduledAtToJiraImports < ActiveRecord::Migration[6.0]
+ DOWNTIME = false
+
+ def change
+ add_column :jira_imports, :scheduled_at, :datetime_with_timezone
+ end
+end
diff --git a/db/migrate/20200424050250_remove_orphaned_invited_members.rb b/db/migrate/20200424050250_remove_orphaned_invited_members.rb
new file mode 100644
index 00000000000..0fb2f7d9c39
--- /dev/null
+++ b/db/migrate/20200424050250_remove_orphaned_invited_members.rb
@@ -0,0 +1,13 @@
+# frozen_string_literal: true
+
+class RemoveOrphanedInvitedMembers < ActiveRecord::Migration[6.0]
+ DOWNTIME = false
+
+ def up
+ # As of 2020-04-23, there are 19 entries on GitLab.com that match this criteria.
+ execute "DELETE FROM members WHERE user_id IS NULL AND invite_token IS NULL AND invite_accepted_at IS NOT NULL"
+ end
+
+ def down
+ end
+end
diff --git a/db/migrate/20200424101920_add_text_limit_to_metrics_users_starred_dashboards_dashboard_path.rb b/db/migrate/20200424101920_add_text_limit_to_metrics_users_starred_dashboards_dashboard_path.rb
new file mode 100644
index 00000000000..a5620fd8e29
--- /dev/null
+++ b/db/migrate/20200424101920_add_text_limit_to_metrics_users_starred_dashboards_dashboard_path.rb
@@ -0,0 +1,16 @@
+# frozen_string_literal: true
+
+class AddTextLimitToMetricsUsersStarredDashboardsDashboardPath < ActiveRecord::Migration[6.0]
+ include Gitlab::Database::MigrationHelpers
+
+ DOWNTIME = false
+ disable_ddl_transaction!
+
+ def up
+ add_text_limit :metrics_users_starred_dashboards, :dashboard_path, 255
+ end
+
+ def down
+ remove_text_limit :metrics_users_starred_dashboards, :dashboard_path
+ end
+end
diff --git a/db/migrate/20200424135319_create_nuget_dependency_link_metadata.rb b/db/migrate/20200424135319_create_nuget_dependency_link_metadata.rb
new file mode 100644
index 00000000000..8aa3d98aa80
--- /dev/null
+++ b/db/migrate/20200424135319_create_nuget_dependency_link_metadata.rb
@@ -0,0 +1,26 @@
+# frozen_string_literal: true
+
+class CreateNugetDependencyLinkMetadata < ActiveRecord::Migration[6.0]
+ include Gitlab::Database::MigrationHelpers
+
+ DOWNTIME = false
+
+ disable_ddl_transaction!
+
+ CONSTRAINT_NAME = 'packages_nuget_dependency_link_metadata_target_framework_constraint'
+
+ def up
+ unless table_exists?(:packages_nuget_dependency_link_metadata)
+ create_table :packages_nuget_dependency_link_metadata, id: false do |t|
+ t.references :dependency_link, primary_key: true, default: nil, foreign_key: { to_table: :packages_dependency_links, on_delete: :cascade }, index: { name: 'index_packages_nuget_dl_metadata_on_dependency_link_id' }, type: :bigint
+ t.text :target_framework, null: false
+ end
+ end
+
+ add_text_limit :packages_nuget_dependency_link_metadata, :target_framework, 255, constraint_name: CONSTRAINT_NAME
+ end
+
+ def down
+ drop_table :packages_nuget_dependency_link_metadata
+ end
+end
diff --git a/db/migrate/20200429001827_sprint_rename_state_to_state_enum.rb b/db/migrate/20200429001827_sprint_rename_state_to_state_enum.rb
new file mode 100644
index 00000000000..721b9b7c64f
--- /dev/null
+++ b/db/migrate/20200429001827_sprint_rename_state_to_state_enum.rb
@@ -0,0 +1,17 @@
+# frozen_string_literal: true
+
+class SprintRenameStateToStateEnum < ActiveRecord::Migration[6.0]
+ include Gitlab::Database::MigrationHelpers
+
+ DOWNTIME = false
+
+ disable_ddl_transaction!
+
+ def up
+ rename_column_concurrently :sprints, :state, :state_enum
+ end
+
+ def down
+ undo_rename_column_concurrently :sprints, :state, :state_enum
+ end
+end
diff --git a/db/migrate/20200429015603_add_fk_to_project_repository_storage_moves.rb b/db/migrate/20200429015603_add_fk_to_project_repository_storage_moves.rb
new file mode 100644
index 00000000000..a68ce66e4a6
--- /dev/null
+++ b/db/migrate/20200429015603_add_fk_to_project_repository_storage_moves.rb
@@ -0,0 +1,19 @@
+# frozen_string_literal: true
+
+class AddFkToProjectRepositoryStorageMoves < ActiveRecord::Migration[6.0]
+ include Gitlab::Database::MigrationHelpers
+
+ DOWNTIME = false
+
+ def up
+ with_lock_retries do
+ add_foreign_key :project_repository_storage_moves, :projects, on_delete: :cascade # rubocop:disable Migration/AddConcurrentForeignKey
+ end
+ end
+
+ def down
+ with_lock_retries do
+ remove_foreign_key :project_repository_storage_moves, :projects
+ end
+ end
+end
diff --git a/db/migrate/20200429181335_add_default_value_for_file_store_to_lfs_objects.rb b/db/migrate/20200429181335_add_default_value_for_file_store_to_lfs_objects.rb
new file mode 100644
index 00000000000..f316a092bfc
--- /dev/null
+++ b/db/migrate/20200429181335_add_default_value_for_file_store_to_lfs_objects.rb
@@ -0,0 +1,19 @@
+# frozen_string_literal: true
+
+class AddDefaultValueForFileStoreToLfsObjects < ActiveRecord::Migration[6.0]
+ include Gitlab::Database::MigrationHelpers
+
+ DOWNTIME = false
+
+ def up
+ with_lock_retries do
+ change_column_default :lfs_objects, :file_store, 1
+ end
+ end
+
+ def down
+ with_lock_retries do
+ change_column_default :lfs_objects, :file_store, nil
+ end
+ end
+end
diff --git a/db/migrate/20200429181955_add_default_value_for_file_store_to_ci_job_artifacts.rb b/db/migrate/20200429181955_add_default_value_for_file_store_to_ci_job_artifacts.rb
new file mode 100644
index 00000000000..ac3d5e41e3e
--- /dev/null
+++ b/db/migrate/20200429181955_add_default_value_for_file_store_to_ci_job_artifacts.rb
@@ -0,0 +1,19 @@
+# frozen_string_literal: true
+
+class AddDefaultValueForFileStoreToCiJobArtifacts < ActiveRecord::Migration[6.0]
+ include Gitlab::Database::MigrationHelpers
+
+ DOWNTIME = false
+
+ def up
+ with_lock_retries do
+ change_column_default :ci_job_artifacts, :file_store, 1
+ end
+ end
+
+ def down
+ with_lock_retries do
+ change_column_default :ci_job_artifacts, :file_store, nil
+ end
+ end
+end
diff --git a/db/migrate/20200429182245_add_default_value_for_store_to_uploads.rb b/db/migrate/20200429182245_add_default_value_for_store_to_uploads.rb
new file mode 100644
index 00000000000..f28fcce8f2f
--- /dev/null
+++ b/db/migrate/20200429182245_add_default_value_for_store_to_uploads.rb
@@ -0,0 +1,19 @@
+# frozen_string_literal: true
+
+class AddDefaultValueForStoreToUploads < ActiveRecord::Migration[6.0]
+ include Gitlab::Database::MigrationHelpers
+
+ DOWNTIME = false
+
+ def up
+ with_lock_retries do
+ change_column_default :uploads, :store, 1
+ end
+ end
+
+ def down
+ with_lock_retries do
+ change_column_default :uploads, :store, nil
+ end
+ end
+end
diff --git a/db/migrate/20200430103158_create_group_wiki_repositories.rb b/db/migrate/20200430103158_create_group_wiki_repositories.rb
new file mode 100644
index 00000000000..b84a8a55def
--- /dev/null
+++ b/db/migrate/20200430103158_create_group_wiki_repositories.rb
@@ -0,0 +1,15 @@
+# frozen_string_literal: true
+
+class CreateGroupWikiRepositories < ActiveRecord::Migration[6.0]
+ DOWNTIME = false
+
+ def change
+ create_table :group_wiki_repositories, id: false do |t|
+ t.bigint :shard_id, null: false, index: true
+ t.bigint :group_id, null: false, index: false, primary_key: true, default: nil
+
+ # The limit is added in db/migrate/20200511120430_add_group_wiki_repositories_disk_path_limit.rb
+ t.text :disk_path, null: false, index: { unique: true } # rubocop:disable Migration/AddLimitToTextColumns
+ end
+ end
+end
diff --git a/db/migrate/20200430130048_create_packages_nuget_metadata.rb b/db/migrate/20200430130048_create_packages_nuget_metadata.rb
new file mode 100644
index 00000000000..0f0d490c93d
--- /dev/null
+++ b/db/migrate/20200430130048_create_packages_nuget_metadata.rb
@@ -0,0 +1,34 @@
+# frozen_string_literal: true
+
+class CreatePackagesNugetMetadata < ActiveRecord::Migration[6.0]
+ include Gitlab::Database::MigrationHelpers
+
+ DOWNTIME = false
+
+ disable_ddl_transaction!
+
+ LICENSE_URL_CONSTRAINT_NAME = 'packages_nuget_metadata_license_url_constraint'
+ PROJECT_URL_CONSTRAINT_NAME = 'packages_nuget_metadata_project_url_constraint'
+ ICON_URL_CONSTRAINT_NAME = 'packages_nuget_metadata_icon_url_constraint'
+
+ def up
+ unless table_exists?(:packages_nuget_metadata)
+ with_lock_retries do
+ create_table :packages_nuget_metadata, id: false do |t|
+ t.references :package, primary_key: true, default: nil, index: false, foreign_key: { to_table: :packages_packages, on_delete: :cascade }, type: :bigint
+ t.text :license_url
+ t.text :project_url
+ t.text :icon_url
+ end
+ end
+ end
+
+ add_text_limit :packages_nuget_metadata, :license_url, 255, constraint_name: LICENSE_URL_CONSTRAINT_NAME
+ add_text_limit :packages_nuget_metadata, :project_url, 255, constraint_name: PROJECT_URL_CONSTRAINT_NAME
+ add_text_limit :packages_nuget_metadata, :icon_url, 255, constraint_name: ICON_URL_CONSTRAINT_NAME
+ end
+
+ def down
+ drop_table :packages_nuget_metadata
+ end
+end
diff --git a/db/migrate/20200505164958_add_registry_settings_to_application_settings.rb b/db/migrate/20200505164958_add_registry_settings_to_application_settings.rb
new file mode 100644
index 00000000000..4c307dbe845
--- /dev/null
+++ b/db/migrate/20200505164958_add_registry_settings_to_application_settings.rb
@@ -0,0 +1,30 @@
+# frozen_string_literal: true
+
+class AddRegistrySettingsToApplicationSettings < ActiveRecord::Migration[6.0]
+ include Gitlab::Database::MigrationHelpers
+
+ DOWNTIME = false
+
+ disable_ddl_transaction!
+
+ # rubocop:disable Migration/AddLimitToTextColumns
+ def up
+ add_column_with_default(:application_settings, # rubocop:disable Migration/AddColumnWithDefault
+ :container_registry_vendor,
+ :text,
+ default: '',
+ allow_null: false)
+
+ add_column_with_default(:application_settings, # rubocop:disable Migration/AddColumnWithDefault
+ :container_registry_version,
+ :text,
+ default: '',
+ allow_null: false)
+ end
+ # rubocop:enable Migration/AddLimitToTextColumns
+
+ def down
+ remove_column :application_settings, :container_registry_vendor
+ remove_column :application_settings, :container_registry_version
+ end
+end
diff --git a/db/migrate/20200505171834_add_text_limit_to_container_registry_vendor.rb b/db/migrate/20200505171834_add_text_limit_to_container_registry_vendor.rb
new file mode 100644
index 00000000000..242dcc9a837
--- /dev/null
+++ b/db/migrate/20200505171834_add_text_limit_to_container_registry_vendor.rb
@@ -0,0 +1,17 @@
+# frozen_string_literal: true
+
+class AddTextLimitToContainerRegistryVendor < ActiveRecord::Migration[6.0]
+ include Gitlab::Database::MigrationHelpers
+
+ DOWNTIME = false
+
+ disable_ddl_transaction!
+
+ def up
+ add_text_limit :application_settings, :container_registry_vendor, 255
+ end
+
+ def down
+ remove_text_limit :application_settings, :container_registry_vendor
+ end
+end
diff --git a/db/migrate/20200505172405_add_text_limit_to_container_registry_version.rb b/db/migrate/20200505172405_add_text_limit_to_container_registry_version.rb
new file mode 100644
index 00000000000..36589c9cc75
--- /dev/null
+++ b/db/migrate/20200505172405_add_text_limit_to_container_registry_version.rb
@@ -0,0 +1,17 @@
+# frozen_string_literal: true
+
+class AddTextLimitToContainerRegistryVersion < ActiveRecord::Migration[6.0]
+ include Gitlab::Database::MigrationHelpers
+
+ DOWNTIME = false
+
+ disable_ddl_transaction!
+
+ def up
+ add_text_limit :application_settings, :container_registry_version, 255
+ end
+
+ def down
+ remove_text_limit :application_settings, :container_registry_version
+ end
+end
diff --git a/db/migrate/20200507221434_add_container_registry_features_to_application_settings.rb b/db/migrate/20200507221434_add_container_registry_features_to_application_settings.rb
new file mode 100644
index 00000000000..b333db56eee
--- /dev/null
+++ b/db/migrate/20200507221434_add_container_registry_features_to_application_settings.rb
@@ -0,0 +1,13 @@
+# frozen_string_literal: true
+
+class AddContainerRegistryFeaturesToApplicationSettings < ActiveRecord::Migration[6.0]
+ DOWNTIME = false
+
+ def up
+ add_column :application_settings, :container_registry_features, :text, array: true, default: [], null: false # rubocop:disable Migration/AddLimitToTextColumns
+ end
+
+ def down
+ remove_column :application_settings, :container_registry_features
+ end
+end
diff --git a/db/migrate/20200511092246_add_epic_id_to_resource_state_events.rb b/db/migrate/20200511092246_add_epic_id_to_resource_state_events.rb
new file mode 100644
index 00000000000..85d965972aa
--- /dev/null
+++ b/db/migrate/20200511092246_add_epic_id_to_resource_state_events.rb
@@ -0,0 +1,17 @@
+# frozen_string_literal: true
+
+class AddEpicIdToResourceStateEvents < ActiveRecord::Migration[6.0]
+ DOWNTIME = false
+
+ INDEX_NAME = 'index_resource_state_events_on_epic_id'
+
+ def up
+ add_column :resource_state_events, :epic_id, :integer
+ add_index :resource_state_events, :epic_id, name: INDEX_NAME # rubocop:disable Migration/AddIndex
+ end
+
+ def down
+ remove_index :resource_state_events, name: INDEX_NAME # rubocop:disable Migration/RemoveIndex
+ remove_column :resource_state_events, :epic_id, :integer
+ end
+end
diff --git a/db/migrate/20200511092505_add_foreign_key_to_epic_id_on_resource_state_events.rb b/db/migrate/20200511092505_add_foreign_key_to_epic_id_on_resource_state_events.rb
new file mode 100644
index 00000000000..8072fe81c4a
--- /dev/null
+++ b/db/migrate/20200511092505_add_foreign_key_to_epic_id_on_resource_state_events.rb
@@ -0,0 +1,19 @@
+# frozen_string_literal: true
+
+class AddForeignKeyToEpicIdOnResourceStateEvents < ActiveRecord::Migration[6.0]
+ include Gitlab::Database::MigrationHelpers
+
+ DOWNTIME = false
+
+ def up
+ with_lock_retries do
+ add_foreign_key :resource_state_events, :epics, column: :epic_id, on_delete: :cascade # rubocop:disable Migration/AddConcurrentForeignKey
+ end
+ end
+
+ def down
+ with_lock_retries do
+ remove_foreign_key :resource_state_events, column: :epic_id
+ end
+ end
+end
diff --git a/db/migrate/20200511115430_add_inherit_from_to_services.rb b/db/migrate/20200511115430_add_inherit_from_to_services.rb
new file mode 100644
index 00000000000..c409b543dbf
--- /dev/null
+++ b/db/migrate/20200511115430_add_inherit_from_to_services.rb
@@ -0,0 +1,9 @@
+# frozen_string_literal: true
+
+class AddInheritFromToServices < ActiveRecord::Migration[6.0]
+ DOWNTIME = false
+
+ def change
+ add_column :services, :inherit_from_id, :bigint
+ end
+end
diff --git a/db/migrate/20200511115431_add_index_inherit_from_id_to_services.rb b/db/migrate/20200511115431_add_index_inherit_from_id_to_services.rb
new file mode 100644
index 00000000000..a0a5f361edf
--- /dev/null
+++ b/db/migrate/20200511115431_add_index_inherit_from_id_to_services.rb
@@ -0,0 +1,21 @@
+# frozen_string_literal: true
+
+class AddIndexInheritFromIdToServices < ActiveRecord::Migration[6.0]
+ include Gitlab::Database::MigrationHelpers
+
+ DOWNTIME = false
+
+ disable_ddl_transaction!
+
+ def up
+ add_concurrent_index :services, :inherit_from_id
+
+ add_concurrent_foreign_key :services, :services, column: :inherit_from_id, on_delete: :nullify
+ end
+
+ def down
+ remove_foreign_key_if_exists :services, column: :inherit_from_id
+
+ remove_concurrent_index :services, :inherit_from_id
+ end
+end
diff --git a/db/migrate/20200511121549_add_group_wiki_repositories_shard_id_foreign_key.rb b/db/migrate/20200511121549_add_group_wiki_repositories_shard_id_foreign_key.rb
new file mode 100644
index 00000000000..c2606dfb0d5
--- /dev/null
+++ b/db/migrate/20200511121549_add_group_wiki_repositories_shard_id_foreign_key.rb
@@ -0,0 +1,19 @@
+# frozen_string_literal: true
+
+class AddGroupWikiRepositoriesShardIdForeignKey < ActiveRecord::Migration[6.0]
+ include Gitlab::Database::MigrationHelpers
+
+ DOWNTIME = false
+
+ def up
+ with_lock_retries do
+ add_foreign_key :group_wiki_repositories, :shards, on_delete: :restrict # rubocop:disable Migration/AddConcurrentForeignKey
+ end
+ end
+
+ def down
+ with_lock_retries do
+ remove_foreign_key :group_wiki_repositories, :shards
+ end
+ end
+end
diff --git a/db/migrate/20200511121610_add_group_wiki_repositories_group_id_foreign_key.rb b/db/migrate/20200511121610_add_group_wiki_repositories_group_id_foreign_key.rb
new file mode 100644
index 00000000000..3a4c75be3b9
--- /dev/null
+++ b/db/migrate/20200511121610_add_group_wiki_repositories_group_id_foreign_key.rb
@@ -0,0 +1,19 @@
+# frozen_string_literal: true
+
+class AddGroupWikiRepositoriesGroupIdForeignKey < ActiveRecord::Migration[6.0]
+ include Gitlab::Database::MigrationHelpers
+
+ DOWNTIME = false
+
+ def up
+ with_lock_retries do
+ add_foreign_key :group_wiki_repositories, :namespaces, column: :group_id, on_delete: :cascade # rubocop:disable Migration/AddConcurrentForeignKey
+ end
+ end
+
+ def down
+ with_lock_retries do
+ remove_foreign_key :group_wiki_repositories, :namespaces, column: :group_id
+ end
+ end
+end
diff --git a/db/migrate/20200511121620_add_group_wiki_repositories_disk_path_limit.rb b/db/migrate/20200511121620_add_group_wiki_repositories_disk_path_limit.rb
new file mode 100644
index 00000000000..db4b9d8a6e4
--- /dev/null
+++ b/db/migrate/20200511121620_add_group_wiki_repositories_disk_path_limit.rb
@@ -0,0 +1,18 @@
+# frozen_string_literal: true
+
+class AddGroupWikiRepositoriesDiskPathLimit < ActiveRecord::Migration[6.0]
+ include Gitlab::Database::MigrationHelpers
+
+ # Set this constant to true if this migration requires downtime.
+ DOWNTIME = false
+
+ disable_ddl_transaction!
+
+ def up
+ add_text_limit :group_wiki_repositories, :disk_path, 80
+ end
+
+ def down
+ remove_text_limit :group_wiki_repositories, :disk_path
+ end
+end
diff --git a/db/migrate/20200511130129_remove_deprecated_jenkins_service_records.rb b/db/migrate/20200511130129_remove_deprecated_jenkins_service_records.rb
new file mode 100644
index 00000000000..258142f950e
--- /dev/null
+++ b/db/migrate/20200511130129_remove_deprecated_jenkins_service_records.rb
@@ -0,0 +1,17 @@
+# frozen_string_literal: true
+
+class RemoveDeprecatedJenkinsServiceRecords < ActiveRecord::Migration[6.0]
+ DOWNTIME = false
+
+ def up
+ execute <<~SQL.strip
+ DELETE FROM services WHERE type = 'JenkinsDeprecatedService';
+ SQL
+ end
+
+ def down
+ # no-op
+
+ # The records were removed by `up`
+ end
+end
diff --git a/db/migrate/20200512085150_change_default_value_of_protected_ci_variables_of_application_settings_to_true.rb b/db/migrate/20200512085150_change_default_value_of_protected_ci_variables_of_application_settings_to_true.rb
new file mode 100644
index 00000000000..ca623ccf8b4
--- /dev/null
+++ b/db/migrate/20200512085150_change_default_value_of_protected_ci_variables_of_application_settings_to_true.rb
@@ -0,0 +1,9 @@
+# frozen_string_literal: true
+
+class ChangeDefaultValueOfProtectedCiVariablesOfApplicationSettingsToTrue < ActiveRecord::Migration[6.0]
+ DOWNTIME = false
+
+ def change
+ change_column_default :application_settings, :protected_ci_variables, from: false, to: true
+ end
+end
diff --git a/db/migrate/20200512164334_sprint_make_state_enum_not_null_and_default.rb b/db/migrate/20200512164334_sprint_make_state_enum_not_null_and_default.rb
new file mode 100644
index 00000000000..f0db228e249
--- /dev/null
+++ b/db/migrate/20200512164334_sprint_make_state_enum_not_null_and_default.rb
@@ -0,0 +1,15 @@
+# frozen_string_literal: true
+
+class SprintMakeStateEnumNotNullAndDefault < ActiveRecord::Migration[6.0]
+ DOWNTIME = false
+
+ def up
+ change_column_default :sprints, :state_enum, from: 0, to: 1
+ change_column_null :sprints, :state_enum, false, 1
+ end
+
+ def down
+ change_column_null :sprints, :state_enum, true
+ change_column_default :sprints, :state_enum, from: 1, to: nil
+ end
+end
diff --git a/db/migrate/20200513160930_fix_not_null_check_constraint_inconsistency.rb b/db/migrate/20200513160930_fix_not_null_check_constraint_inconsistency.rb
new file mode 100644
index 00000000000..a8bb91cf6cf
--- /dev/null
+++ b/db/migrate/20200513160930_fix_not_null_check_constraint_inconsistency.rb
@@ -0,0 +1,20 @@
+# frozen_string_literal: true
+
+class FixNotNullCheckConstraintInconsistency < ActiveRecord::Migration[6.0]
+ include Gitlab::Database::MigrationHelpers
+
+ DOWNTIME = false
+
+ def up
+ table = :application_settings
+
+ %i(container_registry_vendor container_registry_version).each do |column|
+ change_column_null table, column, false
+ remove_not_null_constraint(table, column) if check_not_null_constraint_exists?(table, column)
+ end
+ end
+
+ def down
+ # No-op: for regular systems without the inconsistency, #up is a no-op, too
+ end
+end
diff --git a/db/migrate/20200513224143_add_section_to_approval_merge_request_rule.rb b/db/migrate/20200513224143_add_section_to_approval_merge_request_rule.rb
new file mode 100644
index 00000000000..98424e9b735
--- /dev/null
+++ b/db/migrate/20200513224143_add_section_to_approval_merge_request_rule.rb
@@ -0,0 +1,21 @@
+# frozen_string_literal: true
+
+class AddSectionToApprovalMergeRequestRule < ActiveRecord::Migration[6.0]
+ include Gitlab::Database::MigrationHelpers
+
+ DOWNTIME = false
+
+ disable_ddl_transaction!
+
+ def up
+ unless column_exists?(:approval_merge_request_rules, :section)
+ add_column :approval_merge_request_rules, :section, :text
+ end
+
+ add_text_limit :approval_merge_request_rules, :section, 255
+ end
+
+ def down
+ remove_column :approval_merge_request_rules, :section
+ end
+end
diff --git a/db/migrate/20200513234502_fill_file_store_lfs_objects.rb b/db/migrate/20200513234502_fill_file_store_lfs_objects.rb
new file mode 100644
index 00000000000..74066afbd83
--- /dev/null
+++ b/db/migrate/20200513234502_fill_file_store_lfs_objects.rb
@@ -0,0 +1,19 @@
+# frozen_string_literal: true
+
+class FillFileStoreLfsObjects < ActiveRecord::Migration[6.0]
+ include Gitlab::Database::MigrationHelpers
+
+ DOWNTIME = false
+
+ disable_ddl_transaction!
+
+ def up
+ update_column_in_batches(:lfs_objects, :file_store, 1) do |table, query|
+ query.where(table[:file_store].eq(nil))
+ end
+ end
+
+ def down
+ # no-op
+ end
+end
diff --git a/db/migrate/20200513235347_fill_store_uploads.rb b/db/migrate/20200513235347_fill_store_uploads.rb
new file mode 100644
index 00000000000..42db5e4cc08
--- /dev/null
+++ b/db/migrate/20200513235347_fill_store_uploads.rb
@@ -0,0 +1,19 @@
+# frozen_string_literal: true
+
+class FillStoreUploads < ActiveRecord::Migration[6.0]
+ include Gitlab::Database::MigrationHelpers
+
+ DOWNTIME = false
+
+ disable_ddl_transaction!
+
+ def up
+ update_column_in_batches(:uploads, :store, 1) do |table, query|
+ query.where(table[:store].eq(nil))
+ end
+ end
+
+ def down
+ # no-op
+ end
+end
diff --git a/db/migrate/20200513235532_fill_file_store_ci_job_artifacts.rb b/db/migrate/20200513235532_fill_file_store_ci_job_artifacts.rb
new file mode 100644
index 00000000000..d4c25167535
--- /dev/null
+++ b/db/migrate/20200513235532_fill_file_store_ci_job_artifacts.rb
@@ -0,0 +1,21 @@
+# frozen_string_literal: true
+
+class FillFileStoreCiJobArtifacts < ActiveRecord::Migration[6.0]
+ include Gitlab::Database::MigrationHelpers
+
+ DOWNTIME = false
+
+ disable_ddl_transaction!
+
+ def up
+ # rubocop:disable Migration/UpdateLargeTable
+ update_column_in_batches(:ci_job_artifacts, :file_store, 1) do |table, query|
+ query.where(table[:file_store].eq(nil))
+ end
+ # rubocop:enable Migration/UpdateLargeTable
+ end
+
+ def down
+ # no-op
+ end
+end
diff --git a/db/migrate/20200515155620_add_index_non_requested_project_members_on_source_id_source_type.rb b/db/migrate/20200515155620_add_index_non_requested_project_members_on_source_id_source_type.rb
new file mode 100644
index 00000000000..333f4e93e95
--- /dev/null
+++ b/db/migrate/20200515155620_add_index_non_requested_project_members_on_source_id_source_type.rb
@@ -0,0 +1,17 @@
+# frozen_string_literal: true
+
+class AddIndexNonRequestedProjectMembersOnSourceIdSourceType < ActiveRecord::Migration[6.0]
+ include Gitlab::Database::MigrationHelpers
+
+ DOWNTIME = false
+
+ disable_ddl_transaction!
+
+ def up
+ add_concurrent_index(:members, [:source_id, :source_type], where: "requested_at IS NULL and type = 'ProjectMember'", name: 'index_non_requested_project_members_on_source_id_and_type')
+ end
+
+ def down
+ remove_concurrent_index_by_name(:members, 'index_non_requested_project_members_on_source_id_and_type')
+ end
+end
diff --git a/db/post_migrate/20181008200441_remove_circuit_breaker.rb b/db/post_migrate/20181008200441_remove_circuit_breaker.rb
index 378692e8886..09491038e50 100644
--- a/db/post_migrate/20181008200441_remove_circuit_breaker.rb
+++ b/db/post_migrate/20181008200441_remove_circuit_breaker.rb
@@ -24,7 +24,9 @@ class RemoveCircuitBreaker < ActiveRecord::Migration[4.2]
def down
CIRCUIT_BREAKER_COLUMS_WITH_DEFAULT.each do |column, default|
+ # rubocop:disable Migration/AddColumnWithDefault
add_column_with_default(:application_settings, column, :integer, default: default) unless column_exists?(:application_settings, column)
+ # rubocop:enable Migration/AddColumnWithDefault
end
end
end
diff --git a/db/post_migrate/20181013005024_remove_koding_from_application_settings.rb b/db/post_migrate/20181013005024_remove_koding_from_application_settings.rb
index b6e5473e896..550ad94f4ab 100644
--- a/db/post_migrate/20181013005024_remove_koding_from_application_settings.rb
+++ b/db/post_migrate/20181013005024_remove_koding_from_application_settings.rb
@@ -12,6 +12,6 @@ class RemoveKodingFromApplicationSettings < ActiveRecord::Migration[4.2]
def down
add_column :application_settings, :koding_enabled, :boolean # rubocop:disable Migration/SaferBooleanColumn
- add_column :application_settings, :koding_url, :string # rubocop:disable Migration/AddLimitToStringColumns
+ add_column :application_settings, :koding_url, :string
end
end
diff --git a/db/post_migrate/20190404231137_remove_alternate_url_from_geo_nodes.rb b/db/post_migrate/20190404231137_remove_alternate_url_from_geo_nodes.rb
index 8e7ef0ec54f..785ceb2fb28 100644
--- a/db/post_migrate/20190404231137_remove_alternate_url_from_geo_nodes.rb
+++ b/db/post_migrate/20190404231137_remove_alternate_url_from_geo_nodes.rb
@@ -16,6 +16,6 @@ class RemoveAlternateUrlFromGeoNodes < ActiveRecord::Migration[5.0]
end
def down
- add_column :geo_nodes, :alternate_url, :string # rubocop:disable Migration/AddLimitToStringColumns
+ add_column :geo_nodes, :alternate_url, :string
end
end
diff --git a/db/post_migrate/20190625184066_remove_sentry_from_application_settings.rb b/db/post_migrate/20190625184066_remove_sentry_from_application_settings.rb
index 9d71bfafffb..7a0923aabd8 100644
--- a/db/post_migrate/20190625184066_remove_sentry_from_application_settings.rb
+++ b/db/post_migrate/20190625184066_remove_sentry_from_application_settings.rb
@@ -28,11 +28,13 @@ class RemoveSentryFromApplicationSettings < ActiveRecord::Migration[5.0]
def down
SENTRY_ENABLED_COLUMNS.each do |column|
+ # rubocop:disable Migration/AddColumnWithDefault
add_column_with_default(:application_settings, column, :boolean, default: false, allow_null: false) unless column_exists?(:application_settings, column)
+ # rubocop:enable Migration/AddColumnWithDefault
end
SENTRY_DSN_COLUMNS.each do |column|
- add_column(:application_settings, column, :string) unless column_exists?(:application_settings, column) # rubocop:disable Migration/AddLimitToStringColumns
+ add_column(:application_settings, column, :string) unless column_exists?(:application_settings, column)
end
end
end
diff --git a/db/post_migrate/20191030193050_remove_pendo_from_application_settings.rb b/db/post_migrate/20191030193050_remove_pendo_from_application_settings.rb
index 33bbe6f8ea7..c1a1cc01aa4 100644
--- a/db/post_migrate/20191030193050_remove_pendo_from_application_settings.rb
+++ b/db/post_migrate/20191030193050_remove_pendo_from_application_settings.rb
@@ -13,7 +13,7 @@ class RemovePendoFromApplicationSettings < ActiveRecord::Migration[5.2]
end
def down
- add_column_with_default :application_settings, :pendo_enabled, :boolean, default: false, allow_null: false
+ add_column_with_default :application_settings, :pendo_enabled, :boolean, default: false, allow_null: false # rubocop:disable Migration/AddColumnWithDefault
add_column :application_settings, :pendo_url, :string, limit: 255
end
end
diff --git a/db/post_migrate/20191128162854_drop_project_ci_cd_settings_merge_trains_enabled.rb b/db/post_migrate/20191128162854_drop_project_ci_cd_settings_merge_trains_enabled.rb
index c2e6792e611..7f91d5112b3 100644
--- a/db/post_migrate/20191128162854_drop_project_ci_cd_settings_merge_trains_enabled.rb
+++ b/db/post_migrate/20191128162854_drop_project_ci_cd_settings_merge_trains_enabled.rb
@@ -12,6 +12,8 @@ class DropProjectCiCdSettingsMergeTrainsEnabled < ActiveRecord::Migration[5.2]
end
def down
+ # rubocop:disable Migration/AddColumnWithDefault
add_column_with_default :project_ci_cd_settings, :merge_trains_enabled, :boolean, default: false, allow_null: true # rubocop:disable Migration/UpdateLargeTable
+ # rubocop:enable Migration/AddColumnWithDefault
end
end
diff --git a/db/post_migrate/20191202031812_drop_operations_feature_flags_clients_token.rb b/db/post_migrate/20191202031812_drop_operations_feature_flags_clients_token.rb
index bda461af7bc..93cef322f02 100644
--- a/db/post_migrate/20191202031812_drop_operations_feature_flags_clients_token.rb
+++ b/db/post_migrate/20191202031812_drop_operations_feature_flags_clients_token.rb
@@ -14,7 +14,7 @@ class DropOperationsFeatureFlagsClientsToken < ActiveRecord::Migration[5.2]
def down
unless column_exists?(:operations_feature_flags_clients, :token)
- add_column :operations_feature_flags_clients, :token, :string # rubocop:disable Migration/AddLimitToStringColumns
+ add_column :operations_feature_flags_clients, :token, :string
end
add_concurrent_index :operations_feature_flags_clients, [:project_id, :token], unique: true,
diff --git a/db/post_migrate/20200120083607_remove_storage_version_column_from_snippets.rb b/db/post_migrate/20200120083607_remove_storage_version_column_from_snippets.rb
index e94dc75e65c..62bb3f46cae 100644
--- a/db/post_migrate/20200120083607_remove_storage_version_column_from_snippets.rb
+++ b/db/post_migrate/20200120083607_remove_storage_version_column_from_snippets.rb
@@ -19,7 +19,7 @@ class RemoveStorageVersionColumnFromSnippets < ActiveRecord::Migration[5.2]
def down
return if column_exists?(:snippets, :storage_version)
- add_column_with_default(
+ add_column_with_default( # rubocop:disable Migration/AddColumnWithDefault
:snippets,
:storage_version,
:integer,
diff --git a/db/post_migrate/20200128210353_cleanup_optimistic_locking_nulls.rb b/db/post_migrate/20200128210353_cleanup_optimistic_locking_nulls.rb
index 8bc037c7333..5c172159561 100644
--- a/db/post_migrate/20200128210353_cleanup_optimistic_locking_nulls.rb
+++ b/db/post_migrate/20200128210353_cleanup_optimistic_locking_nulls.rb
@@ -31,7 +31,7 @@ class CleanupOptimisticLockingNulls < ActiveRecord::Migration[5.2]
'CleanupOptimisticLockingNulls',
2.minutes,
batch_size: BATCH_SIZE,
- other_arguments: [table]
+ other_job_arguments: [table]
)
end
end
diff --git a/db/post_migrate/20200212052620_readd_template_column_to_services.rb b/db/post_migrate/20200212052620_readd_template_column_to_services.rb
index 2b0d26b2ad4..c636cb0a07b 100644
--- a/db/post_migrate/20200212052620_readd_template_column_to_services.rb
+++ b/db/post_migrate/20200212052620_readd_template_column_to_services.rb
@@ -15,7 +15,7 @@ class ReaddTemplateColumnToServices < ActiveRecord::Migration[6.0]
# to production, so we should be okay to re-add it without worrying
# about doing a data migration. If we needed to restore the value
# of `template`, we would look for entries with `project_id IS NULL`.
- add_column_with_default :services, :template, :boolean, default: false, allow_null: true
+ add_column_with_default :services, :template, :boolean, default: false, allow_null: true # rubocop:disable Migration/AddColumnWithDefault
end
# rubocop:enable Migration/UpdateLargeTable
diff --git a/db/post_migrate/20200217210353_cleanup_optimistic_locking_nulls_pt2.rb b/db/post_migrate/20200217210353_cleanup_optimistic_locking_nulls_pt2.rb
new file mode 100644
index 00000000000..1ec3e817e8e
--- /dev/null
+++ b/db/post_migrate/20200217210353_cleanup_optimistic_locking_nulls_pt2.rb
@@ -0,0 +1,7 @@
+# frozen_string_literal: true
+
+class CleanupOptimisticLockingNullsPt2 < ActiveRecord::Migration[5.2]
+ def change
+ # no-op: the MR that contained this migration was reverted
+ end
+end
diff --git a/db/post_migrate/20200219193058_remove_state_from_issues.rb b/db/post_migrate/20200219193058_remove_state_from_issues.rb
index ac27a9a9b69..007ba600ce7 100644
--- a/db/post_migrate/20200219193058_remove_state_from_issues.rb
+++ b/db/post_migrate/20200219193058_remove_state_from_issues.rb
@@ -18,7 +18,7 @@ class RemoveStateFromIssues < ActiveRecord::Migration[6.0]
return if issue_state_column_exists?
with_lock_retries do
- add_column :issues, :state, :string # rubocop:disable Migration/AddLimitToStringColumns
+ add_column :issues, :state, :string
end
end
diff --git a/db/post_migrate/20200219193117_remove_state_from_merge_requests.rb b/db/post_migrate/20200219193117_remove_state_from_merge_requests.rb
index c99a732f37b..384a694b549 100644
--- a/db/post_migrate/20200219193117_remove_state_from_merge_requests.rb
+++ b/db/post_migrate/20200219193117_remove_state_from_merge_requests.rb
@@ -18,7 +18,7 @@ class RemoveStateFromMergeRequests < ActiveRecord::Migration[6.0]
return if merge_requests_state_column_exists?
with_lock_retries do
- add_column :merge_requests, :state, :string # rubocop:disable Migration/AddLimitToStringColumns
+ add_column :merge_requests, :state, :string
end
end
diff --git a/db/post_migrate/20200221142216_remove_repository_storage_from_snippets.rb b/db/post_migrate/20200221142216_remove_repository_storage_from_snippets.rb
index fb8721a45b8..f9ef985218b 100644
--- a/db/post_migrate/20200221142216_remove_repository_storage_from_snippets.rb
+++ b/db/post_migrate/20200221142216_remove_repository_storage_from_snippets.rb
@@ -16,7 +16,7 @@ class RemoveRepositoryStorageFromSnippets < ActiveRecord::Migration[6.0]
def down
return if column_exists?(:snippets, :repository_storage)
- add_column_with_default(
+ add_column_with_default( # rubocop:disable Migration/AddColumnWithDefault
:snippets,
:repository_storage,
:string,
diff --git a/db/post_migrate/20200310215714_migrate_saml_identities_to_scim_identities.rb b/db/post_migrate/20200310215714_migrate_saml_identities_to_scim_identities.rb
new file mode 100644
index 00000000000..e2ec7b62d31
--- /dev/null
+++ b/db/post_migrate/20200310215714_migrate_saml_identities_to_scim_identities.rb
@@ -0,0 +1,29 @@
+# frozen_string_literal: true
+
+class MigrateSamlIdentitiesToScimIdentities < ActiveRecord::Migration[6.0]
+ DOWNTIME = false
+
+ class Identity < ActiveRecord::Base
+ self.table_name = 'identities'
+
+ include ::EachBatch
+ end
+
+ def up
+ Identity
+ .joins('INNER JOIN saml_providers ON saml_providers.id = identities.saml_provider_id')
+ .where('saml_providers.group_id IN (SELECT group_id FROM scim_oauth_access_tokens)')
+ .select('identities.extern_uid, identities.user_id, saml_providers.group_id, TRUE AS active,
+ identities.created_at, CURRENT_TIMESTAMP AS updated_at')
+ .each_batch do |batch|
+ data_to_insert = batch.map do |record|
+ record.attributes.extract!("extern_uid", "user_id", "group_id", "active", "created_at", "updated_at")
+ end
+
+ Gitlab::Database.bulk_insert(:scim_identities, data_to_insert, on_conflict: :do_nothing)
+ end
+ end
+
+ def down
+ end
+end
diff --git a/db/post_migrate/20200312134637_backfill_environment_id_on_deployment_merge_requests.rb b/db/post_migrate/20200312134637_backfill_environment_id_on_deployment_merge_requests.rb
new file mode 100644
index 00000000000..77cb1ae8508
--- /dev/null
+++ b/db/post_migrate/20200312134637_backfill_environment_id_on_deployment_merge_requests.rb
@@ -0,0 +1,27 @@
+# frozen_string_literal: true
+
+class BackfillEnvironmentIdOnDeploymentMergeRequests < ActiveRecord::Migration[6.0]
+ include Gitlab::Database::MigrationHelpers
+
+ DOWNTIME = false
+
+ disable_ddl_transaction!
+
+ def up
+ # no-op
+
+ # this migration is deleted because there is no foreign key for
+ # deployments.environment_id and this caused a failure upgrading
+ # deployments_merge_requests.environment_id
+ #
+ # Details on the following issues:
+ # * https://gitlab.com/gitlab-org/gitlab/-/issues/217191
+ # * https://gitlab.com/gitlab-org/gitlab/-/issues/26229
+ end
+
+ def down
+ # no-op
+
+ # this migration is designed to delete duplicated data
+ end
+end
diff --git a/db/post_migrate/20200401091051_remove_reference_columns_from_resource_milestone_events.rb b/db/post_migrate/20200401091051_remove_reference_columns_from_resource_milestone_events.rb
new file mode 100644
index 00000000000..639ab93cf18
--- /dev/null
+++ b/db/post_migrate/20200401091051_remove_reference_columns_from_resource_milestone_events.rb
@@ -0,0 +1,11 @@
+# frozen_string_literal: true
+
+class RemoveReferenceColumnsFromResourceMilestoneEvents < ActiveRecord::Migration[6.0]
+ DOWNTIME = false
+
+ def change
+ remove_column :resource_milestone_events, :reference, :text
+ remove_column :resource_milestone_events, :reference_html, :text
+ remove_column :resource_milestone_events, :cached_markdown_version, :integer
+ end
+end
diff --git a/db/post_migrate/20200403132349_remove_old_index_pages_domains_need_auto_ssl_renewal.rb b/db/post_migrate/20200403132349_remove_old_index_pages_domains_need_auto_ssl_renewal.rb
new file mode 100644
index 00000000000..b36dce188df
--- /dev/null
+++ b/db/post_migrate/20200403132349_remove_old_index_pages_domains_need_auto_ssl_renewal.rb
@@ -0,0 +1,21 @@
+# frozen_string_literal: true
+
+class RemoveOldIndexPagesDomainsNeedAutoSslRenewal < ActiveRecord::Migration[6.0]
+ include Gitlab::Database::MigrationHelpers
+
+ DOWNTIME = false
+
+ INDEX_NAME = 'index_pages_domains_need_auto_ssl_renewal'
+
+ disable_ddl_transaction!
+
+ def up
+ remove_concurrent_index(:pages_domains, [:certificate_source, :certificate_valid_not_after],
+ where: "auto_ssl_enabled = true", name: INDEX_NAME)
+ end
+
+ def down
+ add_concurrent_index(:pages_domains, [:certificate_source, :certificate_valid_not_after],
+ where: "auto_ssl_enabled = true", name: INDEX_NAME)
+ end
+end
diff --git a/db/post_migrate/20200420094444_backfill_snippet_repositories.rb b/db/post_migrate/20200420094444_backfill_snippet_repositories.rb
new file mode 100644
index 00000000000..452a1a5330f
--- /dev/null
+++ b/db/post_migrate/20200420094444_backfill_snippet_repositories.rb
@@ -0,0 +1,30 @@
+# frozen_string_literal: true
+
+class BackfillSnippetRepositories < ActiveRecord::Migration[6.0]
+ include Gitlab::Database::MigrationHelpers
+
+ DOWNTIME = false
+ INTERVAL = 3.minutes
+ BATCH_SIZE = 100
+ MIGRATION = 'BackfillSnippetRepositories'
+
+ disable_ddl_transaction!
+
+ class Snippet < ActiveRecord::Base
+ include EachBatch
+
+ self.table_name = 'snippets'
+ self.inheritance_column = :_type_disabled
+ end
+
+ def up
+ queue_background_migration_jobs_by_range_at_intervals(Snippet,
+ MIGRATION,
+ INTERVAL,
+ batch_size: BATCH_SIZE)
+ end
+
+ def down
+ # no-op
+ end
+end
diff --git a/db/post_migrate/20200420162730_remove_additional_application_settings_rows.rb b/db/post_migrate/20200420162730_remove_additional_application_settings_rows.rb
new file mode 100644
index 00000000000..e4a0ec1eb4a
--- /dev/null
+++ b/db/post_migrate/20200420162730_remove_additional_application_settings_rows.rb
@@ -0,0 +1,17 @@
+# frozen_string_literal: true
+
+class RemoveAdditionalApplicationSettingsRows < ActiveRecord::Migration[6.0]
+ class ApplicationSetting < ActiveRecord::Base
+ self.table_name = 'application_settings'
+ end
+
+ def up
+ return if ApplicationSetting.count == 1
+
+ execute "DELETE from application_settings WHERE id NOT IN (SELECT MAX(id) FROM application_settings);"
+ end
+
+ def down
+ # no changes
+ end
+end
diff --git a/db/post_migrate/20200424043515_drop_namespaces_plan_id.rb b/db/post_migrate/20200424043515_drop_namespaces_plan_id.rb
new file mode 100644
index 00000000000..16a56b16e5a
--- /dev/null
+++ b/db/post_migrate/20200424043515_drop_namespaces_plan_id.rb
@@ -0,0 +1,26 @@
+# frozen_string_literal: true
+
+class DropNamespacesPlanId < ActiveRecord::Migration[6.0]
+ include Gitlab::Database::MigrationHelpers
+
+ DOWNTIME = false
+
+ disable_ddl_transaction!
+
+ def up
+ with_lock_retries do
+ remove_column :namespaces, :plan_id
+ end
+ end
+
+ def down
+ unless column_exists?(:namespaces, :plan_id)
+ with_lock_retries do
+ add_column :namespaces, :plan_id, :integer
+ end
+ end
+
+ add_concurrent_index :namespaces, :plan_id
+ add_concurrent_foreign_key :namespaces, :plans, column: :plan_id, on_delete: :nullify
+ end
+end
diff --git a/db/post_migrate/20200427064130_cleanup_optimistic_locking_nulls_pt2_fixed.rb b/db/post_migrate/20200427064130_cleanup_optimistic_locking_nulls_pt2_fixed.rb
new file mode 100644
index 00000000000..63f85fc7156
--- /dev/null
+++ b/db/post_migrate/20200427064130_cleanup_optimistic_locking_nulls_pt2_fixed.rb
@@ -0,0 +1,47 @@
+# frozen_string_literal: true
+
+class CleanupOptimisticLockingNullsPt2Fixed < ActiveRecord::Migration[6.0]
+ include Gitlab::Database::MigrationHelpers
+
+ DOWNTIME = false
+
+ disable_ddl_transaction!
+
+ TABLES = %w(ci_stages ci_builds ci_pipelines).freeze
+ BATCH_SIZE = 10_000
+
+ def declare_class(table)
+ Class.new(ActiveRecord::Base) do
+ include EachBatch
+
+ self.table_name = table
+ self.inheritance_column = :_type_disabled # Disable STI
+ end
+ end
+
+ def up
+ last_table_final_delay = 0
+
+ TABLES.each do |table|
+ # cleanup wrong index created in the previous migration, it might be there on staging
+ remove_concurrent_index table.to_sym, :lock_version, where: "lock_version IS NULL"
+
+ add_concurrent_index table.to_sym, :id, where: "lock_version IS NULL", name: "tmp_index_#{table}_lock_version"
+
+ last_table_final_delay = queue_background_migration_jobs_by_range_at_intervals(
+ declare_class(table).where(lock_version: nil),
+ 'CleanupOptimisticLockingNulls',
+ 2.minutes,
+ batch_size: BATCH_SIZE,
+ other_job_arguments: [table],
+ initial_delay: last_table_final_delay
+ )
+ end
+ end
+
+ def down
+ TABLES.each do |table|
+ remove_concurrent_index table.to_sym, :id, where: "lock_version IS NULL", name: "tmp_index_#{table}_lock_version"
+ end
+ end
+end
diff --git a/db/post_migrate/20200428134356_remove_elastic_experimental_indexer_from_application_settings.rb b/db/post_migrate/20200428134356_remove_elastic_experimental_indexer_from_application_settings.rb
new file mode 100644
index 00000000000..a9baf6fd8e3
--- /dev/null
+++ b/db/post_migrate/20200428134356_remove_elastic_experimental_indexer_from_application_settings.rb
@@ -0,0 +1,8 @@
+# frozen_string_literal: true
+class RemoveElasticExperimentalIndexerFromApplicationSettings < ActiveRecord::Migration[6.0]
+ DOWNTIME = false
+
+ def change
+ remove_column :application_settings, :elasticsearch_experimental_indexer, :boolean
+ end
+end
diff --git a/db/post_migrate/20200429002150_cleanup_sprints_state_rename.rb b/db/post_migrate/20200429002150_cleanup_sprints_state_rename.rb
new file mode 100644
index 00000000000..7f67a55a19d
--- /dev/null
+++ b/db/post_migrate/20200429002150_cleanup_sprints_state_rename.rb
@@ -0,0 +1,17 @@
+# frozen_string_literal: true
+
+class CleanupSprintsStateRename < ActiveRecord::Migration[6.0]
+ include Gitlab::Database::MigrationHelpers
+
+ DOWNTIME = false
+
+ disable_ddl_transaction!
+
+ def up
+ cleanup_concurrent_column_rename :sprints, :state, :state_enum
+ end
+
+ def down
+ undo_cleanup_concurrent_column_rename :sprints, :state, :state_enum
+ end
+end
diff --git a/db/post_migrate/20200506085748_update_undefined_confidence_from_occurrences.rb b/db/post_migrate/20200506085748_update_undefined_confidence_from_occurrences.rb
new file mode 100644
index 00000000000..06c82ad404b
--- /dev/null
+++ b/db/post_migrate/20200506085748_update_undefined_confidence_from_occurrences.rb
@@ -0,0 +1,35 @@
+# frozen_string_literal: true
+
+class UpdateUndefinedConfidenceFromOccurrences < ActiveRecord::Migration[6.0]
+ include Gitlab::Database::MigrationHelpers
+
+ INDEX_NAME = 'index_vulnerability_occurrences_on_id_and_confidence_eq_zero'
+ DOWNTIME = false
+
+ disable_ddl_transaction!
+ BATCH_SIZE = 1_000
+ INTERVAL = 2.minutes
+
+ # 286_159 records to be updated on GitLab.com
+ def up
+ # create temporary index for undefined vulnerabilities
+ add_concurrent_index(:vulnerability_occurrences, :id, where: 'confidence = 0', name: INDEX_NAME)
+
+ return unless Gitlab.ee?
+
+ migration = Gitlab::BackgroundMigration::RemoveUndefinedOccurrenceConfidenceLevel
+ migration_name = migration.to_s.demodulize
+ relation = migration::Occurrence.undefined_confidence
+ queue_background_migration_jobs_by_range_at_intervals(relation,
+ migration_name,
+ INTERVAL,
+ batch_size: BATCH_SIZE)
+ end
+
+ def down
+ # no-op
+ # temporary index is to be dropped in a different migration in an upcoming release
+ remove_concurrent_index(:vulnerability_occurrences, :id, where: 'confidence = 0', name: INDEX_NAME)
+ # This migration can not be reversed because we can not know which records had undefined confidence
+ end
+end
diff --git a/db/post_migrate/20200506125731_cleanup_user_highest_roles_population.rb b/db/post_migrate/20200506125731_cleanup_user_highest_roles_population.rb
new file mode 100644
index 00000000000..5e613228c56
--- /dev/null
+++ b/db/post_migrate/20200506125731_cleanup_user_highest_roles_population.rb
@@ -0,0 +1,23 @@
+# frozen_string_literal: true
+
+class CleanupUserHighestRolesPopulation < ActiveRecord::Migration[6.0]
+ include Gitlab::Database::MigrationHelpers
+
+ DOWNTIME = false
+ INDEX_NAME = 'index_for_migrating_user_highest_roles_table'
+
+ disable_ddl_transaction!
+
+ def up
+ Gitlab::BackgroundMigration.steal('PopulateUserHighestRolesTable')
+
+ remove_concurrent_index(:users, :id, name: INDEX_NAME)
+ end
+
+ def down
+ add_concurrent_index(:users,
+ :id,
+ where: "state = 'active' AND user_type IS NULL AND bot_type IS NULL AND ghost IS NOT TRUE",
+ name: INDEX_NAME)
+ end
+end
diff --git a/db/post_migrate/20200506154421_migrate_scim_identities_to_saml_for_new_users.rb b/db/post_migrate/20200506154421_migrate_scim_identities_to_saml_for_new_users.rb
new file mode 100644
index 00000000000..718e788aad7
--- /dev/null
+++ b/db/post_migrate/20200506154421_migrate_scim_identities_to_saml_for_new_users.rb
@@ -0,0 +1,37 @@
+# frozen_string_literal: true
+
+class MigrateScimIdentitiesToSamlForNewUsers < ActiveRecord::Migration[6.0]
+ DOWNTIME = false
+
+ class ScimIdentity < ActiveRecord::Base
+ self.table_name = 'scim_identities'
+
+ belongs_to :user
+
+ include ::EachBatch
+ end
+
+ class Identity < ActiveRecord::Base
+ self.table_name = 'identities'
+
+ belongs_to :saml_provider
+ end
+
+ def up
+ users_with_saml_provider = Identity.select('user_id').joins(:saml_provider)
+
+ ScimIdentity.each_batch do |relation|
+ identity_records = relation
+ .select("scim_identities.extern_uid, 'group_saml', scim_identities.user_id, CURRENT_TIMESTAMP, CURRENT_TIMESTAMP, saml_providers.id")
+ .joins(:user)
+ .joins('inner join saml_providers on saml_providers.group_id=scim_identities.group_id')
+ .where("date_trunc('second',scim_identities.created_at) at time zone 'UTC' = date_trunc('second',users.created_at)")
+ .where.not(user_id: users_with_saml_provider)
+
+ execute "insert into identities (extern_uid, provider, user_id, created_at, updated_at, saml_provider_id) #{identity_records.to_sql} on conflict do nothing"
+ end
+ end
+
+ def down
+ end
+end
diff --git a/db/post_migrate/20200508091106_remove_bot_type.rb b/db/post_migrate/20200508091106_remove_bot_type.rb
new file mode 100644
index 00000000000..2afcf5308e7
--- /dev/null
+++ b/db/post_migrate/20200508091106_remove_bot_type.rb
@@ -0,0 +1,29 @@
+# frozen_string_literal: true
+
+class RemoveBotType < ActiveRecord::Migration[6.0]
+ include Gitlab::Database::MigrationHelpers
+
+ DOWNTIME = false
+
+ disable_ddl_transaction!
+
+ def up
+ remove_concurrent_index_by_name :users, 'index_users_on_bot_type'
+
+ with_lock_retries do
+ remove_column :users, :bot_type
+ end
+ end
+
+ def down
+ unless column_exists?(:users, :bot_type)
+ with_lock_retries do
+ add_column :users, :bot_type, :integer, limit: 2 # rubocop:disable Migration/AddColumnsToWideTables
+ end
+ end
+
+ execute 'UPDATE users set bot_type = user_type WHERE user_type IN(1,2,3,6)'
+
+ add_concurrent_index :users, :bot_type
+ end
+end
diff --git a/db/post_migrate/20200511080113_add_projects_foreign_key_to_namespaces.rb b/db/post_migrate/20200511080113_add_projects_foreign_key_to_namespaces.rb
new file mode 100644
index 00000000000..a7f67a3b5cd
--- /dev/null
+++ b/db/post_migrate/20200511080113_add_projects_foreign_key_to_namespaces.rb
@@ -0,0 +1,27 @@
+# frozen_string_literal: true
+
+class AddProjectsForeignKeyToNamespaces < ActiveRecord::Migration[6.0]
+ include Gitlab::Database::MigrationHelpers
+
+ DOWNTIME = false
+ FK_NAME = 'fk_projects_namespace_id'
+
+ def up
+ with_lock_retries do
+ add_foreign_key(
+ :projects,
+ :namespaces,
+ column: :namespace_id,
+ on_delete: :restrict,
+ validate: false,
+ name: FK_NAME
+ )
+ end
+ end
+
+ def down
+ with_lock_retries do
+ remove_foreign_key_if_exists :projects, column: :namespace_id, name: FK_NAME
+ end
+ end
+end
diff --git a/db/post_migrate/20200511083541_cleanup_projects_with_missing_namespace.rb b/db/post_migrate/20200511083541_cleanup_projects_with_missing_namespace.rb
new file mode 100644
index 00000000000..442acfc6d16
--- /dev/null
+++ b/db/post_migrate/20200511083541_cleanup_projects_with_missing_namespace.rb
@@ -0,0 +1,263 @@
+# frozen_string_literal: true
+
+# rubocop:disable Migration/PreventStrings
+
+# This migration cleans up Projects that were orphaned when their namespace was deleted
+# Instead of deleting them, we:
+# - Find (or create) the Ghost User
+# - Create (if not already exists) a `lost-and-found` group owned by the Ghost User
+# - Find orphaned projects --> namespace_id can not be found in namespaces
+# - Move the orphaned projects to the `lost-and-found` group
+# (while making them private and setting `archived=true`)
+#
+# On GitLab.com (2020-05-11) this migration will update 66 orphaned projects
+class CleanupProjectsWithMissingNamespace < ActiveRecord::Migration[6.0]
+ include Gitlab::Database::MigrationHelpers
+
+ DOWNTIME = false
+ VISIBILITY_PRIVATE = 0
+ ACCESS_LEVEL_OWNER = 50
+
+ # The batch size of projects to check in each iteration
+ # We expect the selectivity for orphaned projects to be very low:
+ # (66 orphaned projects out of a total 13.6M)
+ # so 10K should be a safe choice
+ BATCH_SIZE = 10000
+
+ disable_ddl_transaction!
+
+ class UserDetail < ActiveRecord::Base
+ self.table_name = 'user_details'
+
+ belongs_to :user, class_name: 'CleanupProjectsWithMissingNamespace::User'
+ end
+
+ class User < ActiveRecord::Base
+ self.table_name = 'users'
+
+ LOST_AND_FOUND_GROUP = 'lost-and-found'
+ USER_TYPE_GHOST = 5
+ DEFAULT_PROJECTS_LIMIT = 100000
+
+ default_value_for :admin, false
+ default_value_for :can_create_group, true # we need this to create the group
+ default_value_for :can_create_team, false
+ default_value_for :project_view, :files
+ default_value_for :notified_of_own_activity, false
+ default_value_for :preferred_language, I18n.default_locale
+
+ has_one :user_detail, class_name: 'CleanupProjectsWithMissingNamespace::UserDetail'
+ has_one :namespace, -> { where(type: nil) },
+ foreign_key: :owner_id, inverse_of: :owner, autosave: true,
+ class_name: 'CleanupProjectsWithMissingNamespace::Namespace'
+
+ before_save :ensure_namespace_correct
+ before_save :ensure_bio_is_assigned_to_user_details, if: :bio_changed?
+
+ enum project_view: { readme: 0, activity: 1, files: 2 }
+
+ def ensure_namespace_correct
+ if namespace
+ namespace.path = username if username_changed?
+ namespace.name = name if name_changed?
+ else
+ build_namespace(path: username, name: name)
+ end
+ end
+
+ def ensure_bio_is_assigned_to_user_details
+ return if Feature.disabled?(:migrate_bio_to_user_details, default_enabled: true)
+
+ user_detail.bio = bio.to_s[0...255]
+ end
+
+ def user_detail
+ super.presence || build_user_detail
+ end
+
+ # Return (or create if necessary) the `lost-and-found` group
+ def lost_and_found_group
+ existing_lost_and_found_group || Group.create_unique_group(self, LOST_AND_FOUND_GROUP)
+ end
+
+ def existing_lost_and_found_group
+ # There should only be one Group for User Ghost starting with LOST_AND_FOUND_GROUP
+ Group
+ .joins('INNER JOIN members ON namespaces.id = members.source_id')
+ .where('namespaces.type = ?', 'Group')
+ .where('members.type = ?', 'GroupMember')
+ .where('members.source_type = ?', 'Namespace')
+ .where('members.user_id = ?', self.id)
+ .where('members.requested_at IS NULL')
+ .where('members.access_level = ?', ACCESS_LEVEL_OWNER)
+ .find_by(Group.arel_table[:name].matches("#{LOST_AND_FOUND_GROUP}%"))
+ end
+
+ class << self
+ # Return (or create if necessary) the ghost user
+ def ghost
+ email = 'ghost%s@example.com'
+
+ unique_internal(where(user_type: USER_TYPE_GHOST), 'ghost', email) do |u|
+ u.bio = _('This is a "Ghost User", created to hold all issues authored by users that have since been deleted. This user cannot be removed.')
+ u.name = 'Ghost User'
+ end
+ end
+
+ def unique_internal(scope, username, email_pattern, &block)
+ scope.first || create_unique_internal(scope, username, email_pattern, &block)
+ end
+
+ def create_unique_internal(scope, username, email_pattern, &creation_block)
+ # Since we only want a single one of these in an instance, we use an
+ # exclusive lease to ensure that this block is never run concurrently.
+ lease_key = "user:unique_internal:#{username}"
+ lease = Gitlab::ExclusiveLease.new(lease_key, timeout: 1.minute.to_i)
+
+ until uuid = lease.try_obtain
+ # Keep trying until we obtain the lease. To prevent hammering Redis too
+ # much we'll wait for a bit between retries.
+ sleep(1)
+ end
+
+ # Recheck if the user is already present. One might have been
+ # added between the time we last checked (first line of this method)
+ # and the time we acquired the lock.
+ existing_user = uncached { scope.first }
+ return existing_user if existing_user.present?
+
+ uniquify = Uniquify.new
+
+ username = uniquify.string(username) { |s| User.find_by_username(s) }
+
+ email = uniquify.string(-> (n) { Kernel.sprintf(email_pattern, n) }) do |s|
+ User.find_by_email(s)
+ end
+
+ User.create!(
+ username: username,
+ email: email,
+ user_type: USER_TYPE_GHOST,
+ projects_limit: DEFAULT_PROJECTS_LIMIT,
+ state: :active,
+ &creation_block
+ )
+ ensure
+ Gitlab::ExclusiveLease.cancel(lease_key, uuid)
+ end
+ end
+ end
+
+ class Namespace < ActiveRecord::Base
+ self.table_name = 'namespaces'
+
+ belongs_to :owner, class_name: 'CleanupProjectsWithMissingNamespace::User'
+ end
+
+ class Group < Namespace
+ # Disable STI to allow us to manually set "type = 'Group'"
+ # Otherwise rails forces "type = CleanupProjectsWithMissingNamespace::Group"
+ self.inheritance_column = :_type_disabled
+
+ def self.create_unique_group(user, group_name)
+ # 'lost-and-found' may be already defined, find a unique one
+ group_name = Uniquify.new.string(group_name) do |str|
+ Group.where(parent_id: nil, name: str).exists?
+ end
+
+ group = Group.create!(
+ name: group_name,
+ path: group_name,
+ type: 'Group',
+ description: 'Group to store orphaned projects',
+ visibility_level: VISIBILITY_PRIVATE
+ )
+
+ # No need to create a route for the lost-and-found group
+
+ GroupMember.add_user(group, user, ACCESS_LEVEL_OWNER)
+
+ group
+ end
+ end
+
+ class Member < ActiveRecord::Base
+ self.table_name = 'members'
+ end
+
+ class GroupMember < Member
+ NOTIFICATION_SETTING_GLOBAL = 3
+
+ # Disable STI to allow us to manually set "type = 'GroupMember'"
+ # Otherwise rails forces "type = CleanupProjectsWithMissingNamespace::GroupMember"
+ self.inheritance_column = :_type_disabled
+
+ def self.add_user(source, user, access_level)
+ GroupMember.create!(
+ type: 'GroupMember',
+ source_id: source.id,
+ user_id: user.id,
+ source_type: 'Namespace',
+ access_level: access_level,
+ notification_level: NOTIFICATION_SETTING_GLOBAL
+ )
+ end
+ end
+
+ class Project < ActiveRecord::Base
+ self.table_name = 'projects'
+
+ include ::EachBatch
+
+ def self.without_namespace
+ where(
+ 'NOT EXISTS (
+ SELECT 1
+ FROM namespaces
+ WHERE projects.namespace_id = namespaces.id
+ )'
+ )
+ end
+ end
+
+ def up
+ # Reset the column information of all the models that update the database
+ # to ensure the Active Record's knowledge of the table structure is current
+ User.reset_column_information
+ Namespace.reset_column_information
+ Member.reset_column_information
+ Project.reset_column_information
+
+ # Find or Create the ghost user
+ ghost_user = User.ghost
+
+ # Find or Create the `lost-and-found`
+ lost_and_found = ghost_user.lost_and_found_group
+
+ # With BATCH_SIZE=10K and projects.count=13.6M
+ # ~1360 iterations will be run:
+ # - each requires on average ~160ms for relation.without_namespace
+ # - worst case scenario is that 66 of those batches will trigger an update (~200ms each)
+ # In general, we expect less than 5% (=66/13.6M x 10K) to trigger an update
+ # Expected total run time: ~235 seconds (== 220 seconds + 14 seconds)
+ Project.each_batch(of: BATCH_SIZE) do |relation|
+ relation.without_namespace.update_all <<~SQL
+ namespace_id = #{lost_and_found.id},
+ archived = TRUE,
+ visibility_level = #{VISIBILITY_PRIVATE},
+
+ -- Names are expected to be unique inside their namespace
+ -- (uniqueness validation on namespace_id, name)
+ -- Attach the id to the name and path to make sure that they are unique
+ name = name || '_' || id,
+ path = path || '_' || id
+ SQL
+ end
+ end
+
+ def down
+ # no-op: the original state for those projects was inconsistent
+ # Also, the original namespace_id for each project is lost during the update
+ end
+end
+# rubocop:enable Migration/PreventStrings
diff --git a/db/post_migrate/20200511092714_update_undefined_confidence_from_vulnerabilities.rb b/db/post_migrate/20200511092714_update_undefined_confidence_from_vulnerabilities.rb
new file mode 100644
index 00000000000..d6611ddbd66
--- /dev/null
+++ b/db/post_migrate/20200511092714_update_undefined_confidence_from_vulnerabilities.rb
@@ -0,0 +1,35 @@
+# frozen_string_literal: true
+
+class UpdateUndefinedConfidenceFromVulnerabilities < ActiveRecord::Migration[6.0]
+ include Gitlab::Database::MigrationHelpers
+
+ INDEX_NAME = 'index_vulnerability_on_id_and_confidence_eq_zero'
+ DOWNTIME = false
+
+ disable_ddl_transaction!
+ BATCH_SIZE = 1_000
+ INTERVAL = 2.minutes
+
+ # 87_602 records to be updated on GitLab.com
+ def up
+ # create temporary index for undefined vulnerabilities
+ add_concurrent_index(:vulnerabilities, :id, where: 'confidence = 0', name: INDEX_NAME)
+
+ return unless Gitlab.ee?
+
+ migration = Gitlab::BackgroundMigration::RemoveUndefinedVulnerabilityConfidenceLevel
+ migration_name = migration.to_s.demodulize
+ relation = migration::Vulnerability.undefined_confidence
+ queue_background_migration_jobs_by_range_at_intervals(relation,
+ migration_name,
+ INTERVAL,
+ batch_size: BATCH_SIZE)
+ end
+
+ def down
+ # no-op
+ # temporary index is to be dropped in a different migration in an upcoming release
+ remove_concurrent_index(:vulnerabilities, :id, where: 'confidence = 0', name: INDEX_NAME)
+ # This migration can not be reversed because we can not know which records had undefined confidence
+ end
+end
diff --git a/db/post_migrate/20200511130130_ensure_deprecated_jenkins_service_records_removal.rb b/db/post_migrate/20200511130130_ensure_deprecated_jenkins_service_records_removal.rb
new file mode 100644
index 00000000000..4c1f29f8e47
--- /dev/null
+++ b/db/post_migrate/20200511130130_ensure_deprecated_jenkins_service_records_removal.rb
@@ -0,0 +1,17 @@
+# frozen_string_literal: true
+
+class EnsureDeprecatedJenkinsServiceRecordsRemoval < ActiveRecord::Migration[6.0]
+ DOWNTIME = false
+
+ def up
+ execute <<~SQL.strip
+ DELETE FROM services WHERE type = 'JenkinsDeprecatedService';
+ SQL
+ end
+
+ def down
+ # no-op
+
+ # The records were removed by `up`
+ end
+end
diff --git a/db/post_migrate/20200511145545_change_variable_interpolation_format_in_common_metrics.rb b/db/post_migrate/20200511145545_change_variable_interpolation_format_in_common_metrics.rb
new file mode 100644
index 00000000000..ac3c545350d
--- /dev/null
+++ b/db/post_migrate/20200511145545_change_variable_interpolation_format_in_common_metrics.rb
@@ -0,0 +1,15 @@
+# frozen_string_literal: true
+
+class ChangeVariableInterpolationFormatInCommonMetrics < ActiveRecord::Migration[6.0]
+ DOWNTIME = false
+
+ def up
+ ::Gitlab::DatabaseImporters::CommonMetrics::Importer.new.execute
+ end
+
+ def down
+ # no-op
+ # The import cannot be reversed since we do not know the state that the
+ # common metrics in the PrometheusMetric table were in before the import.
+ end
+end
diff --git a/db/post_migrate/20200511220023_validate_projects_foreign_key_to_namespaces.rb b/db/post_migrate/20200511220023_validate_projects_foreign_key_to_namespaces.rb
new file mode 100644
index 00000000000..37a761507fc
--- /dev/null
+++ b/db/post_migrate/20200511220023_validate_projects_foreign_key_to_namespaces.rb
@@ -0,0 +1,21 @@
+# frozen_string_literal: true
+
+class ValidateProjectsForeignKeyToNamespaces < ActiveRecord::Migration[6.0]
+ include Gitlab::Database::MigrationHelpers
+
+ DOWNTIME = false
+ FK_NAME = 'fk_projects_namespace_id'
+
+ def up
+ # Validate the FK added with 20200511080113_add_projects_foreign_key_to_namespaces.rb
+ validate_foreign_key :projects, :namespace_id, name: FK_NAME
+ end
+
+ def down
+ # no-op: No need to invalidate the foreign key
+ # The inconsistent data are permanently fixed with the data migration
+ # `20200511083541_cleanup_projects_with_missing_namespace.rb`
+ # even if it is rolled back.
+ # If there is an issue with the FK, we'll roll back the migration that adds the FK
+ end
+end
diff --git a/db/post_migrate/20200513171959_enable_hashed_storage.rb b/db/post_migrate/20200513171959_enable_hashed_storage.rb
new file mode 100644
index 00000000000..53e52b1caff
--- /dev/null
+++ b/db/post_migrate/20200513171959_enable_hashed_storage.rb
@@ -0,0 +1,17 @@
+# frozen_string_literal: true
+
+class EnableHashedStorage < ActiveRecord::Migration[6.0]
+ DOWNTIME = false
+
+ class ApplicationSetting < ActiveRecord::Base
+ self.table_name = 'application_settings'
+ end
+
+ def up
+ ApplicationSetting.update_all(hashed_storage_enabled: true)
+ end
+
+ def down
+ # in 13.0 we are forcing hashed storage to always be enabled for new projects
+ end
+end
diff --git a/db/post_migrate/20200514000009_add_not_null_constraint_on_file_store_to_lfs_objects.rb b/db/post_migrate/20200514000009_add_not_null_constraint_on_file_store_to_lfs_objects.rb
new file mode 100644
index 00000000000..6b3b9a3155d
--- /dev/null
+++ b/db/post_migrate/20200514000009_add_not_null_constraint_on_file_store_to_lfs_objects.rb
@@ -0,0 +1,17 @@
+# frozen_string_literal: true
+
+class AddNotNullConstraintOnFileStoreToLfsObjects < ActiveRecord::Migration[6.0]
+ include Gitlab::Database::MigrationHelpers
+
+ DOWNTIME = false
+
+ disable_ddl_transaction!
+
+ def up
+ add_not_null_constraint(:lfs_objects, :file_store, validate: false)
+ end
+
+ def down
+ remove_not_null_constraint(:lfs_objects, :file_store)
+ end
+end
diff --git a/db/post_migrate/20200514000132_add_not_null_constraint_on_store_to_uploads.rb b/db/post_migrate/20200514000132_add_not_null_constraint_on_store_to_uploads.rb
new file mode 100644
index 00000000000..c5f1cfa79b8
--- /dev/null
+++ b/db/post_migrate/20200514000132_add_not_null_constraint_on_store_to_uploads.rb
@@ -0,0 +1,17 @@
+# frozen_string_literal: true
+
+class AddNotNullConstraintOnStoreToUploads < ActiveRecord::Migration[6.0]
+ include Gitlab::Database::MigrationHelpers
+
+ DOWNTIME = false
+
+ disable_ddl_transaction!
+
+ def up
+ add_not_null_constraint(:uploads, :store, validate: false)
+ end
+
+ def down
+ remove_not_null_constraint(:uploads, :store)
+ end
+end
diff --git a/db/post_migrate/20200514000340_add_not_null_constraint_on_file_store_to_ci_jobs_artifacts.rb b/db/post_migrate/20200514000340_add_not_null_constraint_on_file_store_to_ci_jobs_artifacts.rb
new file mode 100644
index 00000000000..5759803e3b7
--- /dev/null
+++ b/db/post_migrate/20200514000340_add_not_null_constraint_on_file_store_to_ci_jobs_artifacts.rb
@@ -0,0 +1,17 @@
+# frozen_string_literal: true
+
+class AddNotNullConstraintOnFileStoreToCiJobsArtifacts < ActiveRecord::Migration[6.0]
+ include Gitlab::Database::MigrationHelpers
+
+ DOWNTIME = false
+
+ disable_ddl_transaction!
+
+ def up
+ add_not_null_constraint(:ci_job_artifacts, :file_store, validate: false)
+ end
+
+ def down
+ remove_not_null_constraint(:ci_job_artifacts, :file_store)
+ end
+end
diff --git a/db/structure.sql b/db/structure.sql
index 1f0f401165e..38a8f98a1f3 100644
--- a/db/structure.sql
+++ b/db/structure.sql
@@ -24,6 +24,40 @@ CREATE SEQUENCE public.abuse_reports_id_seq
ALTER SEQUENCE public.abuse_reports_id_seq OWNED BY public.abuse_reports.id;
+CREATE TABLE public.alert_management_alerts (
+ id bigint NOT NULL,
+ created_at timestamp with time zone NOT NULL,
+ updated_at timestamp with time zone NOT NULL,
+ started_at timestamp with time zone NOT NULL,
+ ended_at timestamp with time zone,
+ events integer DEFAULT 1 NOT NULL,
+ iid integer NOT NULL,
+ severity smallint DEFAULT 0 NOT NULL,
+ status smallint DEFAULT 0 NOT NULL,
+ fingerprint bytea,
+ issue_id bigint,
+ project_id bigint NOT NULL,
+ title text NOT NULL,
+ description text,
+ service text,
+ monitoring_tool text,
+ hosts text[] DEFAULT '{}'::text[] NOT NULL,
+ payload jsonb DEFAULT '{}'::jsonb NOT NULL,
+ CONSTRAINT check_2df3e2fdc1 CHECK ((char_length(monitoring_tool) <= 100)),
+ CONSTRAINT check_5e9e57cadb CHECK ((char_length(description) <= 1000)),
+ CONSTRAINT check_bac14dddde CHECK ((char_length(service) <= 100)),
+ CONSTRAINT check_d1d1c2d14c CHECK ((char_length(title) <= 200))
+);
+
+CREATE SEQUENCE public.alert_management_alerts_id_seq
+ START WITH 1
+ INCREMENT BY 1
+ NO MINVALUE
+ NO MAXVALUE
+ CACHE 1;
+
+ALTER SEQUENCE public.alert_management_alerts_id_seq OWNED BY public.alert_management_alerts.id;
+
CREATE TABLE public.alerts_service_data (
id bigint NOT NULL,
service_id integer NOT NULL,
@@ -137,7 +171,10 @@ CREATE TABLE public.appearances (
message_background_color text,
message_font_color text,
favicon character varying,
- email_header_and_footer_enabled boolean DEFAULT false NOT NULL
+ email_header_and_footer_enabled boolean DEFAULT false NOT NULL,
+ profile_image_guidelines text,
+ profile_image_guidelines_html text,
+ CONSTRAINT appearances_profile_image_guidelines CHECK ((char_length(profile_image_guidelines) <= 4096))
);
CREATE SEQUENCE public.appearances_id_seq
@@ -251,7 +288,6 @@ CREATE TABLE public.application_settings (
geo_status_timeout integer DEFAULT 10,
uuid character varying,
polling_interval_multiplier numeric DEFAULT 1.0 NOT NULL,
- elasticsearch_experimental_indexer boolean,
cached_markdown_version integer,
check_namespace_plan boolean DEFAULT false NOT NULL,
mirror_max_delay integer DEFAULT 300 NOT NULL,
@@ -312,7 +348,7 @@ CREATE TABLE public.application_settings (
diff_max_patch_bytes integer DEFAULT 102400 NOT NULL,
archive_builds_in_seconds integer,
commit_email_hostname character varying,
- protected_ci_variables boolean DEFAULT false NOT NULL,
+ protected_ci_variables boolean DEFAULT true NOT NULL,
runners_registration_token_encrypted character varying,
local_markdown_version integer DEFAULT 0 NOT NULL,
first_day_of_week integer DEFAULT 0 NOT NULL,
@@ -401,7 +437,12 @@ CREATE TABLE public.application_settings (
container_expiration_policies_enable_historic_entries boolean DEFAULT false NOT NULL,
issues_create_limit integer DEFAULT 300 NOT NULL,
push_rule_id bigint,
- group_owners_can_manage_default_branch_protection boolean DEFAULT true NOT NULL
+ group_owners_can_manage_default_branch_protection boolean DEFAULT true NOT NULL,
+ container_registry_vendor text DEFAULT ''::text NOT NULL,
+ container_registry_version text DEFAULT ''::text NOT NULL,
+ container_registry_features text[] DEFAULT '{}'::text[] NOT NULL,
+ CONSTRAINT check_d03919528d CHECK ((char_length(container_registry_vendor) <= 255)),
+ CONSTRAINT check_e5aba18f02 CHECK ((char_length(container_registry_version) <= 255))
);
CREATE SEQUENCE public.application_settings_id_seq
@@ -437,7 +478,9 @@ CREATE TABLE public.approval_merge_request_rules (
code_owner boolean DEFAULT false NOT NULL,
name character varying NOT NULL,
rule_type smallint DEFAULT 1 NOT NULL,
- report_type smallint
+ report_type smallint,
+ section text,
+ CONSTRAINT check_6fca5928b2 CHECK ((char_length(section) <= 255))
);
CREATE TABLE public.approval_merge_request_rules_approved_approvers (
@@ -988,6 +1031,25 @@ CREATE SEQUENCE public.ci_builds_runner_session_id_seq
ALTER SEQUENCE public.ci_builds_runner_session_id_seq OWNED BY public.ci_builds_runner_session.id;
+CREATE TABLE public.ci_daily_build_group_report_results (
+ id bigint NOT NULL,
+ date date NOT NULL,
+ project_id bigint NOT NULL,
+ last_pipeline_id bigint NOT NULL,
+ ref_path text NOT NULL,
+ group_name text NOT NULL,
+ data jsonb NOT NULL
+);
+
+CREATE SEQUENCE public.ci_daily_build_group_report_results_id_seq
+ START WITH 1
+ INCREMENT BY 1
+ NO MINVALUE
+ NO MAXVALUE
+ CACHE 1;
+
+ALTER SEQUENCE public.ci_daily_build_group_report_results_id_seq OWNED BY public.ci_daily_build_group_report_results.id;
+
CREATE TABLE public.ci_daily_report_results (
id bigint NOT NULL,
date date NOT NULL,
@@ -1008,6 +1070,25 @@ CREATE SEQUENCE public.ci_daily_report_results_id_seq
ALTER SEQUENCE public.ci_daily_report_results_id_seq OWNED BY public.ci_daily_report_results.id;
+CREATE TABLE public.ci_freeze_periods (
+ id bigint NOT NULL,
+ project_id bigint NOT NULL,
+ freeze_start character varying(998) NOT NULL,
+ freeze_end character varying(998) NOT NULL,
+ cron_timezone character varying(255) NOT NULL,
+ created_at timestamp with time zone NOT NULL,
+ updated_at timestamp with time zone NOT NULL
+);
+
+CREATE SEQUENCE public.ci_freeze_periods_id_seq
+ START WITH 1
+ INCREMENT BY 1
+ NO MINVALUE
+ NO MAXVALUE
+ CACHE 1;
+
+ALTER SEQUENCE public.ci_freeze_periods_id_seq OWNED BY public.ci_freeze_periods.id;
+
CREATE TABLE public.ci_group_variables (
id integer NOT NULL,
key character varying NOT NULL,
@@ -1032,6 +1113,28 @@ CREATE SEQUENCE public.ci_group_variables_id_seq
ALTER SEQUENCE public.ci_group_variables_id_seq OWNED BY public.ci_group_variables.id;
+CREATE TABLE public.ci_instance_variables (
+ id bigint NOT NULL,
+ variable_type smallint DEFAULT 1 NOT NULL,
+ masked boolean DEFAULT false,
+ protected boolean DEFAULT false,
+ key text NOT NULL,
+ encrypted_value text,
+ encrypted_value_iv text,
+ CONSTRAINT check_07a45a5bcb CHECK ((char_length(encrypted_value_iv) <= 255)),
+ CONSTRAINT check_5aede12208 CHECK ((char_length(key) <= 255)),
+ CONSTRAINT check_5ebd0515a0 CHECK ((char_length(encrypted_value) <= 1024))
+);
+
+CREATE SEQUENCE public.ci_instance_variables_id_seq
+ START WITH 1
+ INCREMENT BY 1
+ NO MINVALUE
+ NO MAXVALUE
+ CACHE 1;
+
+ALTER SEQUENCE public.ci_instance_variables_id_seq OWNED BY public.ci_instance_variables.id;
+
CREATE TABLE public.ci_job_artifacts (
id integer NOT NULL,
project_id integer NOT NULL,
@@ -1042,10 +1145,11 @@ CREATE TABLE public.ci_job_artifacts (
updated_at timestamp with time zone NOT NULL,
expire_at timestamp with time zone,
file character varying,
- file_store integer,
+ file_store integer DEFAULT 1,
file_sha256 bytea,
file_format smallint,
- file_location smallint
+ file_location smallint,
+ locked boolean
);
CREATE SEQUENCE public.ci_job_artifacts_id_seq
@@ -1663,7 +1767,9 @@ CREATE TABLE public.clusters_applications_fluentd (
updated_at timestamp with time zone NOT NULL,
version character varying(255) NOT NULL,
host character varying(255) NOT NULL,
- status_reason text
+ status_reason text,
+ waf_log_enabled boolean DEFAULT true NOT NULL,
+ cilium_log_enabled boolean DEFAULT true NOT NULL
);
CREATE SEQUENCE public.clusters_applications_fluentd_id_seq
@@ -2003,7 +2109,9 @@ CREATE TABLE public.deploy_tokens (
username character varying,
token_encrypted character varying(255),
deploy_token_type smallint DEFAULT 2 NOT NULL,
- write_registry boolean DEFAULT false NOT NULL
+ write_registry boolean DEFAULT false NOT NULL,
+ read_package_registry boolean DEFAULT false NOT NULL,
+ write_package_registry boolean DEFAULT false NOT NULL
);
CREATE SEQUENCE public.deploy_tokens_id_seq
@@ -3040,6 +3148,33 @@ CREATE SEQUENCE public.group_group_links_id_seq
ALTER SEQUENCE public.group_group_links_id_seq OWNED BY public.group_group_links.id;
+CREATE TABLE public.group_import_states (
+ group_id bigint NOT NULL,
+ created_at timestamp with time zone NOT NULL,
+ updated_at timestamp with time zone NOT NULL,
+ status smallint DEFAULT 0 NOT NULL,
+ jid text NOT NULL,
+ last_error text,
+ CONSTRAINT check_87b58f6b30 CHECK ((char_length(last_error) <= 255)),
+ CONSTRAINT check_96558fff96 CHECK ((char_length(jid) <= 100))
+);
+
+CREATE SEQUENCE public.group_import_states_group_id_seq
+ START WITH 1
+ INCREMENT BY 1
+ NO MINVALUE
+ NO MAXVALUE
+ CACHE 1;
+
+ALTER SEQUENCE public.group_import_states_group_id_seq OWNED BY public.group_import_states.group_id;
+
+CREATE TABLE public.group_wiki_repositories (
+ shard_id bigint NOT NULL,
+ group_id bigint NOT NULL,
+ disk_path text NOT NULL,
+ CONSTRAINT check_07f1c81806 CHECK ((char_length(disk_path) <= 80))
+);
+
CREATE TABLE public.historical_data (
id integer NOT NULL,
date date NOT NULL,
@@ -3299,7 +3434,8 @@ CREATE TABLE public.issues (
duplicated_to_id integer,
promoted_to_epic_id integer,
health_status smallint,
- external_key character varying(255)
+ external_key character varying(255),
+ sprint_id bigint
);
CREATE SEQUENCE public.issues_id_seq
@@ -3374,7 +3510,8 @@ CREATE TABLE public.jira_imports (
status smallint DEFAULT 0 NOT NULL,
jid character varying(255),
jira_project_key character varying(255) NOT NULL,
- jira_project_name character varying(255) NOT NULL
+ jira_project_name character varying(255) NOT NULL,
+ scheduled_at timestamp with time zone
);
CREATE SEQUENCE public.jira_imports_id_seq
@@ -3539,7 +3676,7 @@ CREATE TABLE public.lfs_objects (
created_at timestamp without time zone,
updated_at timestamp without time zone,
file character varying,
- file_store integer
+ file_store integer DEFAULT 1
);
CREATE SEQUENCE public.lfs_objects_id_seq
@@ -3874,7 +4011,8 @@ CREATE TABLE public.merge_requests (
allow_maintainer_to_push boolean,
state_id smallint DEFAULT 1 NOT NULL,
rebase_jid character varying,
- squash_commit_sha bytea
+ squash_commit_sha bytea,
+ sprint_id bigint
);
CREATE TABLE public.merge_requests_closing_issues (
@@ -3946,6 +4084,25 @@ CREATE SEQUENCE public.metrics_dashboard_annotations_id_seq
ALTER SEQUENCE public.metrics_dashboard_annotations_id_seq OWNED BY public.metrics_dashboard_annotations.id;
+CREATE TABLE public.metrics_users_starred_dashboards (
+ id bigint NOT NULL,
+ created_at timestamp with time zone NOT NULL,
+ updated_at timestamp with time zone NOT NULL,
+ project_id bigint NOT NULL,
+ user_id bigint NOT NULL,
+ dashboard_path text NOT NULL,
+ CONSTRAINT check_79a84a0f57 CHECK ((char_length(dashboard_path) <= 255))
+);
+
+CREATE SEQUENCE public.metrics_users_starred_dashboards_id_seq
+ START WITH 1
+ INCREMENT BY 1
+ NO MINVALUE
+ NO MAXVALUE
+ CACHE 1;
+
+ALTER SEQUENCE public.metrics_users_starred_dashboards_id_seq OWNED BY public.metrics_users_starred_dashboards.id;
+
CREATE TABLE public.milestone_releases (
milestone_id bigint NOT NULL,
release_id bigint NOT NULL
@@ -4035,7 +4192,6 @@ CREATE TABLE public.namespaces (
require_two_factor_authentication boolean DEFAULT false NOT NULL,
two_factor_grace_period integer DEFAULT 48 NOT NULL,
cached_markdown_version integer,
- plan_id integer,
project_creation_level integer,
runners_token character varying,
trial_ends_on timestamp with time zone,
@@ -4355,6 +4511,21 @@ CREATE SEQUENCE public.operations_strategies_id_seq
ALTER SEQUENCE public.operations_strategies_id_seq OWNED BY public.operations_strategies.id;
+CREATE TABLE public.operations_strategies_user_lists (
+ id bigint NOT NULL,
+ strategy_id bigint NOT NULL,
+ user_list_id bigint NOT NULL
+);
+
+CREATE SEQUENCE public.operations_strategies_user_lists_id_seq
+ START WITH 1
+ INCREMENT BY 1
+ NO MINVALUE
+ NO MAXVALUE
+ CACHE 1;
+
+ALTER SEQUENCE public.operations_strategies_user_lists_id_seq OWNED BY public.operations_strategies_user_lists.id;
+
CREATE TABLE public.operations_user_lists (
id bigint NOT NULL,
project_id bigint NOT NULL,
@@ -4478,6 +4649,22 @@ CREATE SEQUENCE public.packages_maven_metadata_id_seq
ALTER SEQUENCE public.packages_maven_metadata_id_seq OWNED BY public.packages_maven_metadata.id;
+CREATE TABLE public.packages_nuget_dependency_link_metadata (
+ dependency_link_id bigint NOT NULL,
+ target_framework text NOT NULL,
+ CONSTRAINT packages_nuget_dependency_link_metadata_target_framework_constr CHECK ((char_length(target_framework) <= 255))
+);
+
+CREATE TABLE public.packages_nuget_metadata (
+ package_id bigint NOT NULL,
+ license_url text,
+ project_url text,
+ icon_url text,
+ CONSTRAINT packages_nuget_metadata_icon_url_constraint CHECK ((char_length(icon_url) <= 255)),
+ CONSTRAINT packages_nuget_metadata_license_url_constraint CHECK ((char_length(license_url) <= 255)),
+ CONSTRAINT packages_nuget_metadata_project_url_constraint CHECK ((char_length(project_url) <= 255))
+);
+
CREATE TABLE public.packages_package_files (
id bigint NOT NULL,
package_id bigint NOT NULL,
@@ -4492,9 +4679,9 @@ CREATE TABLE public.packages_package_files (
file_sha256 bytea,
verification_retry_at timestamp with time zone,
verified_at timestamp with time zone,
- verification_checksum character varying(255),
verification_failure character varying(255),
- verification_retry_count integer
+ verification_retry_count integer,
+ verification_checksum bytea
);
CREATE SEQUENCE public.packages_package_files_id_seq
@@ -4600,6 +4787,28 @@ CREATE SEQUENCE public.pages_domains_id_seq
ALTER SEQUENCE public.pages_domains_id_seq OWNED BY public.pages_domains.id;
+CREATE TABLE public.partitioned_foreign_keys (
+ id bigint NOT NULL,
+ cascade_delete boolean DEFAULT true NOT NULL,
+ from_table text NOT NULL,
+ from_column text NOT NULL,
+ to_table text NOT NULL,
+ to_column text NOT NULL,
+ CONSTRAINT check_2c2e02a62b CHECK ((char_length(from_column) <= 63)),
+ CONSTRAINT check_40738efb57 CHECK ((char_length(to_table) <= 63)),
+ CONSTRAINT check_741676d405 CHECK ((char_length(from_table) <= 63)),
+ CONSTRAINT check_7e98be694f CHECK ((char_length(to_column) <= 63))
+);
+
+CREATE SEQUENCE public.partitioned_foreign_keys_id_seq
+ START WITH 1
+ INCREMENT BY 1
+ NO MINVALUE
+ NO MAXVALUE
+ CACHE 1;
+
+ALTER SEQUENCE public.partitioned_foreign_keys_id_seq OWNED BY public.partitioned_foreign_keys.id;
+
CREATE TABLE public.path_locks (
id integer NOT NULL,
path character varying NOT NULL,
@@ -4651,7 +4860,8 @@ CREATE TABLE public.plan_limits (
project_hooks integer DEFAULT 100 NOT NULL,
group_hooks integer DEFAULT 50 NOT NULL,
ci_project_subscriptions integer DEFAULT 2 NOT NULL,
- ci_pipeline_schedules integer DEFAULT 10 NOT NULL
+ ci_pipeline_schedules integer DEFAULT 10 NOT NULL,
+ offset_pagination_limit integer DEFAULT 50000 NOT NULL
);
CREATE SEQUENCE public.plan_limits_id_seq
@@ -5028,11 +5238,34 @@ CREATE SEQUENCE public.project_repository_states_id_seq
ALTER SEQUENCE public.project_repository_states_id_seq OWNED BY public.project_repository_states.id;
+CREATE TABLE public.project_repository_storage_moves (
+ id bigint NOT NULL,
+ created_at timestamp with time zone NOT NULL,
+ updated_at timestamp with time zone NOT NULL,
+ project_id bigint NOT NULL,
+ state smallint DEFAULT 1 NOT NULL,
+ source_storage_name text NOT NULL,
+ destination_storage_name text NOT NULL,
+ CONSTRAINT project_repository_storage_moves_destination_storage_name CHECK ((char_length(destination_storage_name) <= 255)),
+ CONSTRAINT project_repository_storage_moves_source_storage_name CHECK ((char_length(source_storage_name) <= 255))
+);
+
+CREATE SEQUENCE public.project_repository_storage_moves_id_seq
+ START WITH 1
+ INCREMENT BY 1
+ NO MINVALUE
+ NO MAXVALUE
+ CACHE 1;
+
+ALTER SEQUENCE public.project_repository_storage_moves_id_seq OWNED BY public.project_repository_storage_moves.id;
+
CREATE TABLE public.project_settings (
project_id integer NOT NULL,
created_at timestamp with time zone NOT NULL,
updated_at timestamp with time zone NOT NULL,
- push_rule_id bigint
+ push_rule_id bigint,
+ show_default_award_emojis boolean DEFAULT true,
+ CONSTRAINT check_bde223416c CHECK ((show_default_award_emojis IS NOT NULL))
);
CREATE TABLE public.project_statistics (
@@ -5571,9 +5804,6 @@ CREATE TABLE public.resource_milestone_events (
milestone_id bigint,
action smallint NOT NULL,
state smallint NOT NULL,
- cached_markdown_version integer,
- reference text,
- reference_html text,
created_at timestamp with time zone NOT NULL
);
@@ -5586,6 +5816,26 @@ CREATE SEQUENCE public.resource_milestone_events_id_seq
ALTER SEQUENCE public.resource_milestone_events_id_seq OWNED BY public.resource_milestone_events.id;
+CREATE TABLE public.resource_state_events (
+ id bigint NOT NULL,
+ user_id bigint NOT NULL,
+ issue_id bigint,
+ merge_request_id bigint,
+ created_at timestamp with time zone NOT NULL,
+ state smallint NOT NULL,
+ epic_id integer,
+ CONSTRAINT resource_state_events_must_belong_to_issue_or_merge_request CHECK ((((issue_id <> NULL::bigint) AND (merge_request_id IS NULL)) OR ((merge_request_id <> NULL::bigint) AND (issue_id IS NULL))))
+);
+
+CREATE SEQUENCE public.resource_state_events_id_seq
+ START WITH 1
+ INCREMENT BY 1
+ NO MINVALUE
+ NO MAXVALUE
+ CACHE 1;
+
+ALTER SEQUENCE public.resource_state_events_id_seq OWNED BY public.resource_state_events.id;
+
CREATE TABLE public.resource_weight_events (
id bigint NOT NULL,
user_id bigint NOT NULL,
@@ -5821,7 +6071,9 @@ CREATE TABLE public.services (
description character varying(500),
comment_on_event_enabled boolean DEFAULT true NOT NULL,
template boolean DEFAULT false,
- instance boolean DEFAULT false NOT NULL
+ instance boolean DEFAULT false NOT NULL,
+ comment_detail smallint,
+ inherit_from_id bigint
);
CREATE SEQUENCE public.services_id_seq
@@ -5994,6 +6246,50 @@ CREATE SEQUENCE public.spam_logs_id_seq
ALTER SEQUENCE public.spam_logs_id_seq OWNED BY public.spam_logs.id;
+CREATE TABLE public.sprints (
+ id bigint NOT NULL,
+ created_at timestamp with time zone NOT NULL,
+ updated_at timestamp with time zone NOT NULL,
+ start_date date,
+ due_date date,
+ project_id bigint,
+ group_id bigint,
+ iid integer NOT NULL,
+ cached_markdown_version integer,
+ title text NOT NULL,
+ title_html text,
+ description text,
+ description_html text,
+ state_enum smallint DEFAULT 1 NOT NULL,
+ CONSTRAINT sprints_must_belong_to_project_or_group CHECK ((((project_id <> NULL::bigint) AND (group_id IS NULL)) OR ((group_id <> NULL::bigint) AND (project_id IS NULL)))),
+ CONSTRAINT sprints_title CHECK ((char_length(title) <= 255))
+);
+
+CREATE SEQUENCE public.sprints_id_seq
+ START WITH 1
+ INCREMENT BY 1
+ NO MINVALUE
+ NO MAXVALUE
+ CACHE 1;
+
+ALTER SEQUENCE public.sprints_id_seq OWNED BY public.sprints.id;
+
+CREATE TABLE public.status_page_published_incidents (
+ id bigint NOT NULL,
+ created_at timestamp with time zone NOT NULL,
+ updated_at timestamp with time zone NOT NULL,
+ issue_id bigint NOT NULL
+);
+
+CREATE SEQUENCE public.status_page_published_incidents_id_seq
+ START WITH 1
+ INCREMENT BY 1
+ NO MINVALUE
+ NO MAXVALUE
+ CACHE 1;
+
+ALTER SEQUENCE public.status_page_published_incidents_id_seq OWNED BY public.status_page_published_incidents.id;
+
CREATE TABLE public.status_page_settings (
project_id bigint NOT NULL,
created_at timestamp with time zone NOT NULL,
@@ -6243,7 +6539,7 @@ CREATE TABLE public.uploads (
model_type character varying,
uploader character varying NOT NULL,
created_at timestamp without time zone NOT NULL,
- store integer,
+ store integer DEFAULT 1,
mount_point character varying,
secret character varying
);
@@ -6504,7 +6800,6 @@ CREATE TABLE public.users (
commit_email character varying,
group_view integer,
managing_group_id integer,
- bot_type smallint,
first_name character varying(255),
last_name character varying(255),
static_object_token character varying(255),
@@ -6634,10 +6929,11 @@ CREATE TABLE public.vulnerability_exports (
finished_at timestamp with time zone,
status character varying(255) NOT NULL,
file character varying(255),
- project_id bigint NOT NULL,
+ project_id bigint,
author_id bigint NOT NULL,
file_store integer,
- format smallint DEFAULT 0 NOT NULL
+ format smallint DEFAULT 0 NOT NULL,
+ group_id integer
);
CREATE SEQUENCE public.vulnerability_exports_id_seq
@@ -6987,6 +7283,8 @@ ALTER SEQUENCE public.zoom_meetings_id_seq OWNED BY public.zoom_meetings.id;
ALTER TABLE ONLY public.abuse_reports ALTER COLUMN id SET DEFAULT nextval('public.abuse_reports_id_seq'::regclass);
+ALTER TABLE ONLY public.alert_management_alerts ALTER COLUMN id SET DEFAULT nextval('public.alert_management_alerts_id_seq'::regclass);
+
ALTER TABLE ONLY public.alerts_service_data ALTER COLUMN id SET DEFAULT nextval('public.alerts_service_data_id_seq'::regclass);
ALTER TABLE ONLY public.allowed_email_domains ALTER COLUMN id SET DEFAULT nextval('public.allowed_email_domains_id_seq'::regclass);
@@ -7057,10 +7355,16 @@ ALTER TABLE ONLY public.ci_builds_metadata ALTER COLUMN id SET DEFAULT nextval('
ALTER TABLE ONLY public.ci_builds_runner_session ALTER COLUMN id SET DEFAULT nextval('public.ci_builds_runner_session_id_seq'::regclass);
+ALTER TABLE ONLY public.ci_daily_build_group_report_results ALTER COLUMN id SET DEFAULT nextval('public.ci_daily_build_group_report_results_id_seq'::regclass);
+
ALTER TABLE ONLY public.ci_daily_report_results ALTER COLUMN id SET DEFAULT nextval('public.ci_daily_report_results_id_seq'::regclass);
+ALTER TABLE ONLY public.ci_freeze_periods ALTER COLUMN id SET DEFAULT nextval('public.ci_freeze_periods_id_seq'::regclass);
+
ALTER TABLE ONLY public.ci_group_variables ALTER COLUMN id SET DEFAULT nextval('public.ci_group_variables_id_seq'::regclass);
+ALTER TABLE ONLY public.ci_instance_variables ALTER COLUMN id SET DEFAULT nextval('public.ci_instance_variables_id_seq'::regclass);
+
ALTER TABLE ONLY public.ci_job_artifacts ALTER COLUMN id SET DEFAULT nextval('public.ci_job_artifacts_id_seq'::regclass);
ALTER TABLE ONLY public.ci_job_variables ALTER COLUMN id SET DEFAULT nextval('public.ci_job_variables_id_seq'::regclass);
@@ -7247,6 +7551,8 @@ ALTER TABLE ONLY public.group_deploy_tokens ALTER COLUMN id SET DEFAULT nextval(
ALTER TABLE ONLY public.group_group_links ALTER COLUMN id SET DEFAULT nextval('public.group_group_links_id_seq'::regclass);
+ALTER TABLE ONLY public.group_import_states ALTER COLUMN group_id SET DEFAULT nextval('public.group_import_states_group_id_seq'::regclass);
+
ALTER TABLE ONLY public.historical_data ALTER COLUMN id SET DEFAULT nextval('public.historical_data_id_seq'::regclass);
ALTER TABLE ONLY public.identities ALTER COLUMN id SET DEFAULT nextval('public.identities_id_seq'::regclass);
@@ -7325,6 +7631,8 @@ ALTER TABLE ONLY public.merge_trains ALTER COLUMN id SET DEFAULT nextval('public
ALTER TABLE ONLY public.metrics_dashboard_annotations ALTER COLUMN id SET DEFAULT nextval('public.metrics_dashboard_annotations_id_seq'::regclass);
+ALTER TABLE ONLY public.metrics_users_starred_dashboards ALTER COLUMN id SET DEFAULT nextval('public.metrics_users_starred_dashboards_id_seq'::regclass);
+
ALTER TABLE ONLY public.milestones ALTER COLUMN id SET DEFAULT nextval('public.milestones_id_seq'::regclass);
ALTER TABLE ONLY public.namespace_statistics ALTER COLUMN id SET DEFAULT nextval('public.namespace_statistics_id_seq'::regclass);
@@ -7357,6 +7665,8 @@ ALTER TABLE ONLY public.operations_scopes ALTER COLUMN id SET DEFAULT nextval('p
ALTER TABLE ONLY public.operations_strategies ALTER COLUMN id SET DEFAULT nextval('public.operations_strategies_id_seq'::regclass);
+ALTER TABLE ONLY public.operations_strategies_user_lists ALTER COLUMN id SET DEFAULT nextval('public.operations_strategies_user_lists_id_seq'::regclass);
+
ALTER TABLE ONLY public.operations_user_lists ALTER COLUMN id SET DEFAULT nextval('public.operations_user_lists_id_seq'::regclass);
ALTER TABLE ONLY public.packages_build_infos ALTER COLUMN id SET DEFAULT nextval('public.packages_build_infos_id_seq'::regclass);
@@ -7381,6 +7691,8 @@ ALTER TABLE ONLY public.pages_domain_acme_orders ALTER COLUMN id SET DEFAULT nex
ALTER TABLE ONLY public.pages_domains ALTER COLUMN id SET DEFAULT nextval('public.pages_domains_id_seq'::regclass);
+ALTER TABLE ONLY public.partitioned_foreign_keys ALTER COLUMN id SET DEFAULT nextval('public.partitioned_foreign_keys_id_seq'::regclass);
+
ALTER TABLE ONLY public.path_locks ALTER COLUMN id SET DEFAULT nextval('public.path_locks_id_seq'::regclass);
ALTER TABLE ONLY public.personal_access_tokens ALTER COLUMN id SET DEFAULT nextval('public.personal_access_tokens_id_seq'::regclass);
@@ -7423,6 +7735,8 @@ ALTER TABLE ONLY public.project_repositories ALTER COLUMN id SET DEFAULT nextval
ALTER TABLE ONLY public.project_repository_states ALTER COLUMN id SET DEFAULT nextval('public.project_repository_states_id_seq'::regclass);
+ALTER TABLE ONLY public.project_repository_storage_moves ALTER COLUMN id SET DEFAULT nextval('public.project_repository_storage_moves_id_seq'::regclass);
+
ALTER TABLE ONLY public.project_statistics ALTER COLUMN id SET DEFAULT nextval('public.project_statistics_id_seq'::regclass);
ALTER TABLE ONLY public.project_tracing_settings ALTER COLUMN id SET DEFAULT nextval('public.project_tracing_settings_id_seq'::regclass);
@@ -7467,6 +7781,8 @@ ALTER TABLE ONLY public.resource_label_events ALTER COLUMN id SET DEFAULT nextva
ALTER TABLE ONLY public.resource_milestone_events ALTER COLUMN id SET DEFAULT nextval('public.resource_milestone_events_id_seq'::regclass);
+ALTER TABLE ONLY public.resource_state_events ALTER COLUMN id SET DEFAULT nextval('public.resource_state_events_id_seq'::regclass);
+
ALTER TABLE ONLY public.resource_weight_events ALTER COLUMN id SET DEFAULT nextval('public.resource_weight_events_id_seq'::regclass);
ALTER TABLE ONLY public.reviews ALTER COLUMN id SET DEFAULT nextval('public.reviews_id_seq'::regclass);
@@ -7505,6 +7821,10 @@ ALTER TABLE ONLY public.software_licenses ALTER COLUMN id SET DEFAULT nextval('p
ALTER TABLE ONLY public.spam_logs ALTER COLUMN id SET DEFAULT nextval('public.spam_logs_id_seq'::regclass);
+ALTER TABLE ONLY public.sprints ALTER COLUMN id SET DEFAULT nextval('public.sprints_id_seq'::regclass);
+
+ALTER TABLE ONLY public.status_page_published_incidents ALTER COLUMN id SET DEFAULT nextval('public.status_page_published_incidents_id_seq'::regclass);
+
ALTER TABLE ONLY public.status_page_settings ALTER COLUMN project_id SET DEFAULT nextval('public.status_page_settings_project_id_seq'::regclass);
ALTER TABLE ONLY public.subscriptions ALTER COLUMN id SET DEFAULT nextval('public.subscriptions_id_seq'::regclass);
@@ -7594,6 +7914,9 @@ ALTER TABLE ONLY public.zoom_meetings ALTER COLUMN id SET DEFAULT nextval('publi
ALTER TABLE ONLY public.abuse_reports
ADD CONSTRAINT abuse_reports_pkey PRIMARY KEY (id);
+ALTER TABLE ONLY public.alert_management_alerts
+ ADD CONSTRAINT alert_management_alerts_pkey PRIMARY KEY (id);
+
ALTER TABLE ONLY public.alerts_service_data
ADD CONSTRAINT alerts_service_data_pkey PRIMARY KEY (id);
@@ -7687,6 +8010,15 @@ ALTER TABLE ONLY public.chat_names
ALTER TABLE ONLY public.chat_teams
ADD CONSTRAINT chat_teams_pkey PRIMARY KEY (id);
+ALTER TABLE public.ci_job_artifacts
+ ADD CONSTRAINT check_27f0f6dbab CHECK ((file_store IS NOT NULL)) NOT VALID;
+
+ALTER TABLE public.uploads
+ ADD CONSTRAINT check_5e9547379c CHECK ((store IS NOT NULL)) NOT VALID;
+
+ALTER TABLE public.lfs_objects
+ ADD CONSTRAINT check_eecfc5717d CHECK ((file_store IS NOT NULL)) NOT VALID;
+
ALTER TABLE ONLY public.ci_build_needs
ADD CONSTRAINT ci_build_needs_pkey PRIMARY KEY (id);
@@ -7705,12 +8037,21 @@ ALTER TABLE ONLY public.ci_builds
ALTER TABLE ONLY public.ci_builds_runner_session
ADD CONSTRAINT ci_builds_runner_session_pkey PRIMARY KEY (id);
+ALTER TABLE ONLY public.ci_daily_build_group_report_results
+ ADD CONSTRAINT ci_daily_build_group_report_results_pkey PRIMARY KEY (id);
+
ALTER TABLE ONLY public.ci_daily_report_results
ADD CONSTRAINT ci_daily_report_results_pkey PRIMARY KEY (id);
+ALTER TABLE ONLY public.ci_freeze_periods
+ ADD CONSTRAINT ci_freeze_periods_pkey PRIMARY KEY (id);
+
ALTER TABLE ONLY public.ci_group_variables
ADD CONSTRAINT ci_group_variables_pkey PRIMARY KEY (id);
+ALTER TABLE ONLY public.ci_instance_variables
+ ADD CONSTRAINT ci_instance_variables_pkey PRIMARY KEY (id);
+
ALTER TABLE ONLY public.ci_job_artifacts
ADD CONSTRAINT ci_job_artifacts_pkey PRIMARY KEY (id);
@@ -7999,6 +8340,12 @@ ALTER TABLE ONLY public.group_deploy_tokens
ALTER TABLE ONLY public.group_group_links
ADD CONSTRAINT group_group_links_pkey PRIMARY KEY (id);
+ALTER TABLE ONLY public.group_import_states
+ ADD CONSTRAINT group_import_states_pkey PRIMARY KEY (group_id);
+
+ALTER TABLE ONLY public.group_wiki_repositories
+ ADD CONSTRAINT group_wiki_repositories_pkey PRIMARY KEY (group_id);
+
ALTER TABLE ONLY public.historical_data
ADD CONSTRAINT historical_data_pkey PRIMARY KEY (id);
@@ -8116,6 +8463,9 @@ ALTER TABLE ONLY public.merge_trains
ALTER TABLE ONLY public.metrics_dashboard_annotations
ADD CONSTRAINT metrics_dashboard_annotations_pkey PRIMARY KEY (id);
+ALTER TABLE ONLY public.metrics_users_starred_dashboards
+ ADD CONSTRAINT metrics_users_starred_dashboards_pkey PRIMARY KEY (id);
+
ALTER TABLE ONLY public.milestones
ADD CONSTRAINT milestones_pkey PRIMARY KEY (id);
@@ -8170,6 +8520,9 @@ ALTER TABLE ONLY public.operations_scopes
ALTER TABLE ONLY public.operations_strategies
ADD CONSTRAINT operations_strategies_pkey PRIMARY KEY (id);
+ALTER TABLE ONLY public.operations_strategies_user_lists
+ ADD CONSTRAINT operations_strategies_user_lists_pkey PRIMARY KEY (id);
+
ALTER TABLE ONLY public.operations_user_lists
ADD CONSTRAINT operations_user_lists_pkey PRIMARY KEY (id);
@@ -8191,6 +8544,12 @@ ALTER TABLE ONLY public.packages_dependency_links
ALTER TABLE ONLY public.packages_maven_metadata
ADD CONSTRAINT packages_maven_metadata_pkey PRIMARY KEY (id);
+ALTER TABLE ONLY public.packages_nuget_dependency_link_metadata
+ ADD CONSTRAINT packages_nuget_dependency_link_metadata_pkey PRIMARY KEY (dependency_link_id);
+
+ALTER TABLE ONLY public.packages_nuget_metadata
+ ADD CONSTRAINT packages_nuget_metadata_pkey PRIMARY KEY (package_id);
+
ALTER TABLE ONLY public.packages_package_files
ADD CONSTRAINT packages_package_files_pkey PRIMARY KEY (id);
@@ -8209,6 +8568,9 @@ ALTER TABLE ONLY public.pages_domain_acme_orders
ALTER TABLE ONLY public.pages_domains
ADD CONSTRAINT pages_domains_pkey PRIMARY KEY (id);
+ALTER TABLE ONLY public.partitioned_foreign_keys
+ ADD CONSTRAINT partitioned_foreign_keys_pkey PRIMARY KEY (id);
+
ALTER TABLE ONLY public.path_locks
ADD CONSTRAINT path_locks_pkey PRIMARY KEY (id);
@@ -8284,6 +8646,9 @@ ALTER TABLE ONLY public.project_repositories
ALTER TABLE ONLY public.project_repository_states
ADD CONSTRAINT project_repository_states_pkey PRIMARY KEY (id);
+ALTER TABLE ONLY public.project_repository_storage_moves
+ ADD CONSTRAINT project_repository_storage_moves_pkey PRIMARY KEY (id);
+
ALTER TABLE ONLY public.project_settings
ADD CONSTRAINT project_settings_pkey PRIMARY KEY (project_id);
@@ -8353,6 +8718,9 @@ ALTER TABLE ONLY public.resource_label_events
ALTER TABLE ONLY public.resource_milestone_events
ADD CONSTRAINT resource_milestone_events_pkey PRIMARY KEY (id);
+ALTER TABLE ONLY public.resource_state_events
+ ADD CONSTRAINT resource_state_events_pkey PRIMARY KEY (id);
+
ALTER TABLE ONLY public.resource_weight_events
ADD CONSTRAINT resource_weight_events_pkey PRIMARY KEY (id);
@@ -8422,6 +8790,12 @@ ALTER TABLE ONLY public.software_licenses
ALTER TABLE ONLY public.spam_logs
ADD CONSTRAINT spam_logs_pkey PRIMARY KEY (id);
+ALTER TABLE ONLY public.sprints
+ ADD CONSTRAINT sprints_pkey PRIMARY KEY (id);
+
+ALTER TABLE ONLY public.status_page_published_incidents
+ ADD CONSTRAINT status_page_published_incidents_pkey PRIMARY KEY (id);
+
ALTER TABLE ONLY public.status_page_settings
ADD CONSTRAINT status_page_settings_pkey PRIMARY KEY (project_id);
@@ -8616,6 +8990,8 @@ CREATE INDEX idx_merge_requests_on_state_id_and_merge_status ON public.merge_req
CREATE INDEX idx_merge_requests_on_target_project_id_and_iid_opened ON public.merge_requests USING btree (target_project_id, iid) WHERE (state_id = 1);
+CREATE UNIQUE INDEX idx_metrics_users_starred_dashboard_on_user_project_dashboard ON public.metrics_users_starred_dashboards USING btree (user_id, project_id, dashboard_path);
+
CREATE INDEX idx_mr_cc_diff_files_on_mr_cc_id_and_sha ON public.merge_request_context_commit_diff_files USING btree (merge_request_context_commit_id, sha);
CREATE INDEX idx_packages_packages_on_project_id_name_version_package_type ON public.packages_packages USING btree (project_id, name, version, package_type);
@@ -8654,6 +9030,12 @@ CREATE UNIQUE INDEX idx_vulnerability_issue_links_on_vulnerability_id_and_link_t
CREATE INDEX index_abuse_reports_on_user_id ON public.abuse_reports USING btree (user_id);
+CREATE INDEX index_alert_management_alerts_on_issue_id ON public.alert_management_alerts USING btree (issue_id);
+
+CREATE UNIQUE INDEX index_alert_management_alerts_on_project_id_and_fingerprint ON public.alert_management_alerts USING btree (project_id, fingerprint);
+
+CREATE UNIQUE INDEX index_alert_management_alerts_on_project_id_and_iid ON public.alert_management_alerts USING btree (project_id, iid);
+
CREATE INDEX index_alerts_service_data_on_service_id ON public.alerts_service_data USING btree (service_id);
CREATE INDEX index_allowed_email_domains_on_group_id ON public.allowed_email_domains USING btree (group_id);
@@ -8864,10 +9246,16 @@ CREATE INDEX index_ci_builds_project_id_and_status_for_live_jobs_partial2 ON pub
CREATE UNIQUE INDEX index_ci_builds_runner_session_on_build_id ON public.ci_builds_runner_session USING btree (build_id);
+CREATE INDEX index_ci_daily_build_group_report_results_on_last_pipeline_id ON public.ci_daily_build_group_report_results USING btree (last_pipeline_id);
+
CREATE INDEX index_ci_daily_report_results_on_last_pipeline_id ON public.ci_daily_report_results USING btree (last_pipeline_id);
+CREATE INDEX index_ci_freeze_periods_on_project_id ON public.ci_freeze_periods USING btree (project_id);
+
CREATE UNIQUE INDEX index_ci_group_variables_on_group_id_and_key ON public.ci_group_variables USING btree (group_id, key);
+CREATE UNIQUE INDEX index_ci_instance_variables_on_key ON public.ci_instance_variables USING btree (key);
+
CREATE INDEX index_ci_job_artifacts_file_store_is_null ON public.ci_job_artifacts USING btree (id) WHERE (file_store IS NULL);
CREATE INDEX index_ci_job_artifacts_on_expire_at_and_job_id ON public.ci_job_artifacts USING btree (expire_at, job_id);
@@ -9030,6 +9418,8 @@ CREATE UNIQUE INDEX index_clusters_applications_helm_on_cluster_id ON public.clu
CREATE UNIQUE INDEX index_clusters_applications_ingress_on_cluster_id ON public.clusters_applications_ingress USING btree (cluster_id);
+CREATE INDEX index_clusters_applications_ingress_on_modsecurity ON public.clusters_applications_ingress USING btree (modsecurity_enabled, modsecurity_mode, cluster_id);
+
CREATE UNIQUE INDEX index_clusters_applications_jupyter_on_cluster_id ON public.clusters_applications_jupyter USING btree (cluster_id);
CREATE INDEX index_clusters_applications_jupyter_on_oauth_application_id ON public.clusters_applications_jupyter USING btree (oauth_application_id);
@@ -9052,6 +9442,8 @@ CREATE INDEX index_clusters_kubernetes_namespaces_on_project_id ON public.cluste
CREATE INDEX index_clusters_on_enabled_and_provider_type_and_id ON public.clusters USING btree (enabled, provider_type, id);
+CREATE INDEX index_clusters_on_enabled_cluster_type_id_and_created_at ON public.clusters USING btree (enabled, cluster_type, id, created_at);
+
CREATE INDEX index_clusters_on_management_project_id ON public.clusters USING btree (management_project_id) WHERE (management_project_id IS NOT NULL);
CREATE INDEX index_clusters_on_user_id ON public.clusters USING btree (user_id);
@@ -9064,6 +9456,10 @@ CREATE INDEX index_container_repositories_on_project_id ON public.container_repo
CREATE UNIQUE INDEX index_container_repositories_on_project_id_and_name ON public.container_repositories USING btree (project_id, name);
+CREATE INDEX index_container_repository_on_name_trigram ON public.container_repositories USING gin (name public.gin_trgm_ops);
+
+CREATE UNIQUE INDEX index_daily_build_group_report_results_unique_columns ON public.ci_daily_build_group_report_results USING btree (project_id, ref_path, date, group_name);
+
CREATE UNIQUE INDEX index_daily_report_results_unique_columns ON public.ci_daily_report_results USING btree (project_id, ref_path, param_type, date, title);
CREATE INDEX index_dependency_proxy_blobs_on_group_id_and_file_name ON public.dependency_proxy_blobs USING btree (group_id, file_name);
@@ -9158,6 +9554,8 @@ CREATE UNIQUE INDEX index_emails_on_email ON public.emails USING btree (email);
CREATE INDEX index_emails_on_user_id ON public.emails USING btree (user_id);
+CREATE INDEX index_enabled_clusters_on_id ON public.clusters USING btree (id) WHERE (enabled = true);
+
CREATE INDEX index_environments_on_auto_stop_at ON public.environments USING btree (auto_stop_at) WHERE (auto_stop_at IS NOT NULL);
CREATE INDEX index_environments_on_name_varchar_pattern_ops ON public.environments USING btree (name varchar_pattern_ops);
@@ -9234,8 +9632,6 @@ CREATE UNIQUE INDEX index_feature_gates_on_feature_key_and_key_and_value ON publ
CREATE UNIQUE INDEX index_features_on_key ON public.features USING btree (key);
-CREATE INDEX index_for_migrating_user_highest_roles_table ON public.users USING btree (id) WHERE (((state)::text = 'active'::text) AND (user_type IS NULL) AND (bot_type IS NULL) AND (ghost IS NOT TRUE));
-
CREATE INDEX index_for_resource_group ON public.ci_builds USING btree (resource_group_id, id) WHERE (resource_group_id IS NOT NULL);
CREATE INDEX index_for_status_per_branch_per_project ON public.merge_trains USING btree (target_project_id, target_branch, status);
@@ -9362,6 +9758,12 @@ CREATE UNIQUE INDEX index_group_group_links_on_shared_group_and_shared_with_grou
CREATE INDEX index_group_group_links_on_shared_with_group_id ON public.group_group_links USING btree (shared_with_group_id);
+CREATE INDEX index_group_import_states_on_group_id ON public.group_import_states USING btree (group_id);
+
+CREATE UNIQUE INDEX index_group_wiki_repositories_on_disk_path ON public.group_wiki_repositories USING btree (disk_path);
+
+CREATE INDEX index_group_wiki_repositories_on_shard_id ON public.group_wiki_repositories USING btree (shard_id);
+
CREATE INDEX index_identities_on_saml_provider_id ON public.identities USING btree (saml_provider_id) WHERE (saml_provider_id IS NOT NULL);
CREATE INDEX index_identities_on_user_id ON public.identities USING btree (user_id);
@@ -9440,6 +9842,8 @@ CREATE INDEX index_issues_on_promoted_to_epic_id ON public.issues USING btree (p
CREATE INDEX index_issues_on_relative_position ON public.issues USING btree (relative_position);
+CREATE INDEX index_issues_on_sprint_id ON public.issues USING btree (sprint_id);
+
CREATE INDEX index_issues_on_title_trigram ON public.issues USING gin (title public.gin_trgm_ops);
CREATE INDEX index_issues_on_updated_at ON public.issues USING btree (updated_at);
@@ -9602,6 +10006,8 @@ CREATE INDEX index_merge_requests_on_source_branch ON public.merge_requests USIN
CREATE INDEX index_merge_requests_on_source_project_id_and_source_branch ON public.merge_requests USING btree (source_project_id, source_branch);
+CREATE INDEX index_merge_requests_on_sprint_id ON public.merge_requests USING btree (sprint_id);
+
CREATE INDEX index_merge_requests_on_target_branch ON public.merge_requests USING btree (target_branch);
CREATE UNIQUE INDEX index_merge_requests_on_target_project_id_and_iid ON public.merge_requests USING btree (target_project_id, iid);
@@ -9628,6 +10034,8 @@ CREATE INDEX index_metrics_dashboard_annotations_on_cluster_id_and_3_columns ON
CREATE INDEX index_metrics_dashboard_annotations_on_environment_id_and_3_col ON public.metrics_dashboard_annotations USING btree (environment_id, dashboard_path, starting_at, ending_at) WHERE (environment_id IS NOT NULL);
+CREATE INDEX index_metrics_users_starred_dashboards_on_project_id ON public.metrics_users_starred_dashboards USING btree (project_id);
+
CREATE INDEX index_milestone_releases_on_release_id ON public.milestone_releases USING btree (release_id);
CREATE INDEX index_milestones_on_description_trigram ON public.milestones USING gin (description public.gin_trgm_ops);
@@ -9678,8 +10086,6 @@ CREATE INDEX index_namespaces_on_path ON public.namespaces USING btree (path);
CREATE INDEX index_namespaces_on_path_trigram ON public.namespaces USING gin (path public.gin_trgm_ops);
-CREATE INDEX index_namespaces_on_plan_id ON public.namespaces USING btree (plan_id);
-
CREATE UNIQUE INDEX index_namespaces_on_push_rule_id ON public.namespaces USING btree (push_rule_id);
CREATE INDEX index_namespaces_on_require_two_factor_authentication ON public.namespaces USING btree (require_two_factor_authentication);
@@ -9694,6 +10100,8 @@ CREATE INDEX index_namespaces_on_trial_ends_on ON public.namespaces USING btree
CREATE INDEX index_namespaces_on_type_partial ON public.namespaces USING btree (type) WHERE (type IS NOT NULL);
+CREATE INDEX index_non_requested_project_members_on_source_id_and_type ON public.members USING btree (source_id, source_type) WHERE ((requested_at IS NULL) AND ((type)::text = 'ProjectMember'::text));
+
CREATE UNIQUE INDEX index_note_diff_files_on_diff_note_id ON public.note_diff_files USING btree (diff_note_id);
CREATE INDEX index_notes_on_author_id_and_created_at_and_id ON public.notes USING btree (author_id, created_at, id);
@@ -9756,10 +10164,14 @@ CREATE UNIQUE INDEX index_operations_scopes_on_strategy_id_and_environment_scope
CREATE INDEX index_operations_strategies_on_feature_flag_id ON public.operations_strategies USING btree (feature_flag_id);
+CREATE INDEX index_operations_strategies_user_lists_on_user_list_id ON public.operations_strategies_user_lists USING btree (user_list_id);
+
CREATE UNIQUE INDEX index_operations_user_lists_on_project_id_and_iid ON public.operations_user_lists USING btree (project_id, iid);
CREATE UNIQUE INDEX index_operations_user_lists_on_project_id_and_name ON public.operations_user_lists USING btree (project_id, name);
+CREATE UNIQUE INDEX index_ops_strategies_user_lists_on_strategy_id_and_user_list_id ON public.operations_strategies_user_lists USING btree (strategy_id, user_list_id);
+
CREATE UNIQUE INDEX index_packages_build_infos_on_package_id ON public.packages_build_infos USING btree (package_id);
CREATE INDEX index_packages_build_infos_on_pipeline_id ON public.packages_build_infos USING btree (pipeline_id);
@@ -9774,6 +10186,8 @@ CREATE INDEX index_packages_dependency_links_on_dependency_id ON public.packages
CREATE INDEX index_packages_maven_metadata_on_package_id_and_path ON public.packages_maven_metadata USING btree (package_id, path);
+CREATE INDEX index_packages_nuget_dl_metadata_on_dependency_link_id ON public.packages_nuget_dependency_link_metadata USING btree (dependency_link_id);
+
CREATE INDEX index_packages_package_files_on_package_id_and_file_name ON public.packages_package_files USING btree (package_id, file_name);
CREATE INDEX index_packages_packages_on_name_trigram ON public.packages_packages USING gin (name public.gin_trgm_ops);
@@ -9794,7 +10208,9 @@ CREATE INDEX index_pages_domain_acme_orders_on_challenge_token ON public.pages_d
CREATE INDEX index_pages_domain_acme_orders_on_pages_domain_id ON public.pages_domain_acme_orders USING btree (pages_domain_id);
-CREATE INDEX index_pages_domains_need_auto_ssl_renewal ON public.pages_domains USING btree (certificate_source, certificate_valid_not_after) WHERE (auto_ssl_enabled = true);
+CREATE INDEX index_pages_domains_need_auto_ssl_renewal_user_provided ON public.pages_domains USING btree (id) WHERE ((auto_ssl_enabled = true) AND (auto_ssl_failed = false) AND (certificate_source = 0));
+
+CREATE INDEX index_pages_domains_need_auto_ssl_renewal_valid_not_after ON public.pages_domains USING btree (certificate_valid_not_after) WHERE ((auto_ssl_enabled = true) AND (auto_ssl_failed = false));
CREATE UNIQUE INDEX index_pages_domains_on_domain_and_wildcard ON public.pages_domains USING btree (domain, wildcard);
@@ -9816,6 +10232,8 @@ CREATE INDEX index_pages_domains_on_verified_at_and_enabled_until ON public.page
CREATE INDEX index_pages_domains_on_wildcard ON public.pages_domains USING btree (wildcard);
+CREATE UNIQUE INDEX index_partitioned_foreign_keys_unique_index ON public.partitioned_foreign_keys USING btree (to_table, from_table, from_column);
+
CREATE INDEX index_pat_on_user_id_and_expires_at ON public.personal_access_tokens USING btree (user_id, expires_at);
CREATE INDEX index_path_locks_on_path ON public.path_locks USING btree (path);
@@ -9830,7 +10248,7 @@ CREATE INDEX index_personal_access_tokens_on_user_id ON public.personal_access_t
CREATE UNIQUE INDEX index_plan_limits_on_plan_id ON public.plan_limits USING btree (plan_id);
-CREATE INDEX index_plans_on_name ON public.plans USING btree (name);
+CREATE UNIQUE INDEX index_plans_on_name ON public.plans USING btree (name);
CREATE UNIQUE INDEX index_pool_repositories_on_disk_path ON public.pool_repositories USING btree (disk_path);
@@ -9906,6 +10324,8 @@ CREATE INDEX index_project_repositories_on_shard_id ON public.project_repositori
CREATE UNIQUE INDEX index_project_repository_states_on_project_id ON public.project_repository_states USING btree (project_id);
+CREATE INDEX index_project_repository_storage_moves_on_project_id ON public.project_repository_storage_moves USING btree (project_id);
+
CREATE UNIQUE INDEX index_project_settings_on_push_rule_id ON public.project_settings USING btree (push_rule_id);
CREATE INDEX index_project_statistics_on_namespace_id ON public.project_statistics USING btree (namespace_id);
@@ -10118,6 +10538,16 @@ CREATE INDEX index_resource_milestone_events_on_milestone_id ON public.resource_
CREATE INDEX index_resource_milestone_events_on_user_id ON public.resource_milestone_events USING btree (user_id);
+CREATE INDEX index_resource_state_events_on_epic_id ON public.resource_state_events USING btree (epic_id);
+
+CREATE INDEX index_resource_state_events_on_issue_id_and_created_at ON public.resource_state_events USING btree (issue_id, created_at);
+
+CREATE INDEX index_resource_state_events_on_merge_request_id ON public.resource_state_events USING btree (merge_request_id);
+
+CREATE INDEX index_resource_state_events_on_user_id ON public.resource_state_events USING btree (user_id);
+
+CREATE INDEX index_resource_weight_events_on_issue_id_and_created_at ON public.resource_weight_events USING btree (issue_id, created_at);
+
CREATE INDEX index_resource_weight_events_on_issue_id_and_weight ON public.resource_weight_events USING btree (issue_id, weight);
CREATE INDEX index_resource_weight_events_on_user_id ON public.resource_weight_events USING btree (user_id);
@@ -10162,6 +10592,8 @@ CREATE INDEX index_serverless_domain_cluster_on_pages_domain_id ON public.server
CREATE INDEX index_service_desk_enabled_projects_on_id_creator_id_created_at ON public.projects USING btree (id, creator_id, created_at) WHERE (service_desk_enabled = true);
+CREATE INDEX index_services_on_inherit_from_id ON public.services USING btree (inherit_from_id);
+
CREATE INDEX index_services_on_project_id_and_type ON public.services USING btree (project_id, type);
CREATE INDEX index_services_on_template ON public.services USING btree (template);
@@ -10216,12 +10648,32 @@ CREATE INDEX index_software_licenses_on_spdx_identifier ON public.software_licen
CREATE UNIQUE INDEX index_software_licenses_on_unique_name ON public.software_licenses USING btree (name);
+CREATE INDEX index_sprints_on_description_trigram ON public.sprints USING gin (description public.gin_trgm_ops);
+
+CREATE INDEX index_sprints_on_due_date ON public.sprints USING btree (due_date);
+
+CREATE INDEX index_sprints_on_group_id ON public.sprints USING btree (group_id);
+
+CREATE UNIQUE INDEX index_sprints_on_group_id_and_title ON public.sprints USING btree (group_id, title) WHERE (group_id IS NOT NULL);
+
+CREATE UNIQUE INDEX index_sprints_on_project_id_and_iid ON public.sprints USING btree (project_id, iid);
+
+CREATE UNIQUE INDEX index_sprints_on_project_id_and_title ON public.sprints USING btree (project_id, title) WHERE (project_id IS NOT NULL);
+
+CREATE INDEX index_sprints_on_title ON public.sprints USING btree (title);
+
+CREATE INDEX index_sprints_on_title_trigram ON public.sprints USING gin (title public.gin_trgm_ops);
+
+CREATE UNIQUE INDEX index_status_page_published_incidents_on_issue_id ON public.status_page_published_incidents USING btree (issue_id);
+
CREATE INDEX index_status_page_settings_on_project_id ON public.status_page_settings USING btree (project_id);
CREATE INDEX index_subscriptions_on_project_id ON public.subscriptions USING btree (project_id);
CREATE UNIQUE INDEX index_subscriptions_on_subscribable_and_user_id_and_project_id ON public.subscriptions USING btree (subscribable_id, subscribable_type, user_id, project_id);
+CREATE INDEX index_successful_deployments_on_cluster_id_and_environment_id ON public.deployments USING btree (cluster_id, environment_id) WHERE (status = 2);
+
CREATE UNIQUE INDEX index_suggestions_on_note_id_and_relative_order ON public.suggestions USING btree (note_id, relative_order);
CREATE UNIQUE INDEX index_system_note_metadata_on_description_version_id ON public.system_note_metadata USING btree (description_version_id) WHERE (description_version_id IS NOT NULL);
@@ -10326,8 +10778,6 @@ CREATE INDEX index_users_on_accepted_term_id ON public.users USING btree (accept
CREATE INDEX index_users_on_admin ON public.users USING btree (admin);
-CREATE INDEX index_users_on_bot_type ON public.users USING btree (bot_type);
-
CREATE UNIQUE INDEX index_users_on_confirmation_token ON public.users USING btree (confirmation_token);
CREATE INDEX index_users_on_created_at ON public.users USING btree (created_at);
@@ -10404,7 +10854,9 @@ CREATE INDEX index_vulnerabilities_on_updated_by_id ON public.vulnerabilities US
CREATE INDEX index_vulnerability_exports_on_author_id ON public.vulnerability_exports USING btree (author_id);
-CREATE UNIQUE INDEX index_vulnerability_exports_on_project_id_and_id ON public.vulnerability_exports USING btree (project_id, id);
+CREATE INDEX index_vulnerability_exports_on_group_id_not_null ON public.vulnerability_exports USING btree (group_id) WHERE (group_id IS NOT NULL);
+
+CREATE INDEX index_vulnerability_exports_on_project_id_not_null ON public.vulnerability_exports USING btree (project_id) WHERE (project_id IS NOT NULL);
CREATE INDEX index_vulnerability_feedback_on_author_id ON public.vulnerability_feedback USING btree (author_id);
@@ -10426,6 +10878,8 @@ CREATE UNIQUE INDEX index_vulnerability_occurrence_identifiers_on_unique_keys ON
CREATE INDEX index_vulnerability_occurrence_pipelines_on_pipeline_id ON public.vulnerability_occurrence_pipelines USING btree (pipeline_id);
+CREATE INDEX index_vulnerability_occurrences_on_id_and_confidence_eq_zero ON public.vulnerability_occurrences USING btree (id) WHERE (confidence = 0);
+
CREATE INDEX index_vulnerability_occurrences_on_primary_identifier_id ON public.vulnerability_occurrences USING btree (primary_identifier_id);
CREATE INDEX index_vulnerability_occurrences_on_scanner_id ON public.vulnerability_occurrences USING btree (scanner_id);
@@ -10436,6 +10890,8 @@ CREATE UNIQUE INDEX index_vulnerability_occurrences_on_uuid ON public.vulnerabil
CREATE INDEX index_vulnerability_occurrences_on_vulnerability_id ON public.vulnerability_occurrences USING btree (vulnerability_id);
+CREATE INDEX index_vulnerability_on_id_and_confidence_eq_zero ON public.vulnerabilities USING btree (id) WHERE (confidence = 0);
+
CREATE UNIQUE INDEX index_vulnerability_scanners_on_project_id_and_external_id ON public.vulnerability_scanners USING btree (project_id, external_id);
CREATE UNIQUE INDEX index_vulnerability_user_mentions_on_note_id ON public.vulnerability_user_mentions USING btree (note_id) WHERE (note_id IS NOT NULL);
@@ -10504,6 +10960,10 @@ CREATE INDEX note_mentions_temp_index ON public.notes USING btree (id, noteable_
CREATE UNIQUE INDEX one_canonical_wiki_page_slug_per_metadata ON public.wiki_page_slugs USING btree (wiki_page_meta_id) WHERE (canonical = true);
+CREATE INDEX packages_packages_verification_checksum_partial ON public.packages_package_files USING btree (verification_checksum) WHERE (verification_checksum IS NOT NULL);
+
+CREATE INDEX packages_packages_verification_failure_partial ON public.packages_package_files USING btree (verification_failure) WHERE (verification_failure IS NOT NULL);
+
CREATE INDEX partial_index_ci_builds_on_scheduled_at_with_scheduled_jobs ON public.ci_builds USING btree (scheduled_at) WHERE ((scheduled_at IS NOT NULL) AND ((type)::text = 'Ci::Build'::text) AND ((status)::text = 'scheduled'::text));
CREATE INDEX partial_index_deployments_for_legacy_successful_deployments ON public.deployments USING btree (id) WHERE ((finished_at IS NULL) AND (status = 2));
@@ -10522,6 +10982,12 @@ CREATE INDEX tmp_build_stage_position_index ON public.ci_builds USING btree (sta
CREATE INDEX tmp_idx_on_user_id_where_bio_is_filled ON public.users USING btree (id) WHERE ((COALESCE(bio, ''::character varying))::text IS DISTINCT FROM ''::text);
+CREATE INDEX tmp_index_ci_builds_lock_version ON public.ci_builds USING btree (id) WHERE (lock_version IS NULL);
+
+CREATE INDEX tmp_index_ci_pipelines_lock_version ON public.ci_pipelines USING btree (id) WHERE (lock_version IS NULL);
+
+CREATE INDEX tmp_index_ci_stages_lock_version ON public.ci_stages USING btree (id) WHERE (lock_version IS NULL);
+
CREATE UNIQUE INDEX users_security_dashboard_projects_unique_index ON public.users_security_dashboard_projects USING btree (project_id, user_id);
CREATE UNIQUE INDEX vulnerability_feedback_unique_idx ON public.vulnerability_feedback USING btree (project_id, category, feedback_type, project_fingerprint);
@@ -10597,6 +11063,9 @@ ALTER TABLE ONLY public.geo_container_repository_updated_events
ALTER TABLE ONLY public.users_star_projects
ADD CONSTRAINT fk_22cd27ddfc FOREIGN KEY (project_id) REFERENCES public.projects(id) ON DELETE CASCADE;
+ALTER TABLE ONLY public.alert_management_alerts
+ ADD CONSTRAINT fk_2358b75436 FOREIGN KEY (issue_id) REFERENCES public.issues(id) ON DELETE SET NULL;
+
ALTER TABLE ONLY public.ci_stages
ADD CONSTRAINT fk_2360681d1d FOREIGN KEY (project_id) REFERENCES public.projects(id) ON DELETE CASCADE;
@@ -10654,15 +11123,15 @@ ALTER TABLE ONLY public.push_event_payloads
ALTER TABLE ONLY public.ci_builds
ADD CONSTRAINT fk_3a9eaa254d FOREIGN KEY (stage_id) REFERENCES public.ci_stages(id) ON DELETE CASCADE;
+ALTER TABLE ONLY public.issues
+ ADD CONSTRAINT fk_3b8c72ea56 FOREIGN KEY (sprint_id) REFERENCES public.sprints(id) ON DELETE CASCADE;
+
ALTER TABLE ONLY public.epics
ADD CONSTRAINT fk_3c1fd1cccc FOREIGN KEY (due_date_sourcing_milestone_id) REFERENCES public.milestones(id) ON DELETE SET NULL;
ALTER TABLE ONLY public.ci_pipelines
ADD CONSTRAINT fk_3d34ab2e06 FOREIGN KEY (pipeline_schedule_id) REFERENCES public.ci_pipeline_schedules(id) ON DELETE SET NULL;
-ALTER TABLE ONLY public.project_settings
- ADD CONSTRAINT fk_413a953e20 FOREIGN KEY (push_rule_id) REFERENCES public.push_rules(id) ON DELETE CASCADE;
-
ALTER TABLE ONLY public.ci_pipeline_schedule_variables
ADD CONSTRAINT fk_41c35fda51 FOREIGN KEY (pipeline_schedule_id) REFERENCES public.ci_pipeline_schedules(id) ON DELETE CASCADE;
@@ -10771,9 +11240,15 @@ ALTER TABLE ONLY public.vulnerabilities
ALTER TABLE ONLY public.labels
ADD CONSTRAINT fk_7de4989a69 FOREIGN KEY (project_id) REFERENCES public.projects(id) ON DELETE CASCADE;
+ALTER TABLE ONLY public.merge_requests
+ ADD CONSTRAINT fk_7e85395a64 FOREIGN KEY (sprint_id) REFERENCES public.sprints(id) ON DELETE CASCADE;
+
ALTER TABLE ONLY public.merge_request_metrics
ADD CONSTRAINT fk_7f28d925f3 FOREIGN KEY (merged_by_id) REFERENCES public.users(id) ON DELETE SET NULL;
+ALTER TABLE ONLY public.sprints
+ ADD CONSTRAINT fk_80aa8a1f95 FOREIGN KEY (group_id) REFERENCES public.namespaces(id) ON DELETE CASCADE;
+
ALTER TABLE ONLY public.import_export_uploads
ADD CONSTRAINT fk_83319d9721 FOREIGN KEY (group_id) REFERENCES public.namespaces(id) ON DELETE CASCADE;
@@ -10786,6 +11261,9 @@ ALTER TABLE ONLY public.merge_request_diffs
ALTER TABLE ONLY public.ci_pipelines
ADD CONSTRAINT fk_86635dbd80 FOREIGN KEY (project_id) REFERENCES public.projects(id) ON DELETE CASCADE;
+ALTER TABLE ONLY public.services
+ ADD CONSTRAINT fk_868a8e7ad6 FOREIGN KEY (inherit_from_id) REFERENCES public.services(id) ON DELETE SET NULL;
+
ALTER TABLE ONLY public.geo_event_log
ADD CONSTRAINT fk_86c84214ec FOREIGN KEY (repository_renamed_event_id) REFERENCES public.geo_repository_renamed_events(id) ON DELETE CASCADE;
@@ -10852,6 +11330,9 @@ ALTER TABLE ONLY public.issues
ALTER TABLE ONLY public.epics
ADD CONSTRAINT fk_9d480c64b2 FOREIGN KEY (start_date_sourcing_epic_id) REFERENCES public.epics(id) ON DELETE SET NULL;
+ALTER TABLE ONLY public.alert_management_alerts
+ ADD CONSTRAINT fk_9e49e5c2b7 FOREIGN KEY (project_id) REFERENCES public.projects(id) ON DELETE CASCADE;
+
ALTER TABLE ONLY public.ci_pipeline_schedules
ADD CONSTRAINT fk_9ea99f58d2 FOREIGN KEY (owner_id) REFERENCES public.users(id) ON DELETE SET NULL;
@@ -10915,6 +11396,9 @@ ALTER TABLE ONLY public.deployments
ALTER TABLE ONLY public.gitlab_subscriptions
ADD CONSTRAINT fk_bd0c4019c3 FOREIGN KEY (hosted_plan_id) REFERENCES public.plans(id) ON DELETE CASCADE;
+ALTER TABLE ONLY public.metrics_users_starred_dashboards
+ ADD CONSTRAINT fk_bd6ae32fac FOREIGN KEY (user_id) REFERENCES public.users(id) ON DELETE CASCADE;
+
ALTER TABLE ONLY public.snippets
ADD CONSTRAINT fk_be41fd4bb7 FOREIGN KEY (project_id) REFERENCES public.projects(id) ON DELETE CASCADE;
@@ -10933,6 +11417,9 @@ ALTER TABLE ONLY public.design_management_versions
ALTER TABLE ONLY public.geo_event_log
ADD CONSTRAINT fk_c1f241c70d FOREIGN KEY (upload_deleted_event_id) REFERENCES public.geo_upload_deleted_events(id) ON DELETE CASCADE;
+ALTER TABLE ONLY public.vulnerability_exports
+ ADD CONSTRAINT fk_c3d3cb5d0f FOREIGN KEY (group_id) REFERENCES public.namespaces(id) ON DELETE CASCADE;
+
ALTER TABLE ONLY public.geo_event_log
ADD CONSTRAINT fk_c4b1c1f66e FOREIGN KEY (repository_deleted_event_id) REFERENCES public.geo_repository_deleted_events(id) ON DELETE CASCADE;
@@ -10966,6 +11453,9 @@ ALTER TABLE ONLY public.geo_event_log
ALTER TABLE ONLY public.lists
ADD CONSTRAINT fk_d6cf4279f7 FOREIGN KEY (user_id) REFERENCES public.users(id) ON DELETE CASCADE;
+ALTER TABLE ONLY public.metrics_users_starred_dashboards
+ ADD CONSTRAINT fk_d76a2b9a8c FOREIGN KEY (project_id) REFERENCES public.projects(id) ON DELETE CASCADE;
+
ALTER TABLE ONLY public.system_note_metadata
ADD CONSTRAINT fk_d83a918cb1 FOREIGN KEY (note_id) REFERENCES public.notes(id) ON DELETE CASCADE;
@@ -11008,6 +11498,9 @@ ALTER TABLE ONLY public.namespaces
ALTER TABLE ONLY public.fork_networks
ADD CONSTRAINT fk_e7b436b2b5 FOREIGN KEY (root_project_id) REFERENCES public.projects(id) ON DELETE SET NULL;
+ALTER TABLE ONLY public.sprints
+ ADD CONSTRAINT fk_e8206c9686 FOREIGN KEY (project_id) REFERENCES public.projects(id) ON DELETE CASCADE;
+
ALTER TABLE ONLY public.application_settings
ADD CONSTRAINT fk_e8a145f3a7 FOREIGN KEY (instance_administrators_group_id) REFERENCES public.namespaces(id) ON DELETE SET NULL;
@@ -11053,9 +11546,6 @@ ALTER TABLE ONLY public.system_note_metadata
ALTER TABLE ONLY public.merge_requests
ADD CONSTRAINT fk_fd82eae0b9 FOREIGN KEY (head_pipeline_id) REFERENCES public.ci_pipelines(id) ON DELETE SET NULL;
-ALTER TABLE ONLY public.namespaces
- ADD CONSTRAINT fk_fdd12e5b80 FOREIGN KEY (plan_id) REFERENCES public.plans(id) ON DELETE SET NULL;
-
ALTER TABLE ONLY public.project_import_data
ADD CONSTRAINT fk_ffb9ee3a10 FOREIGN KEY (project_id) REFERENCES public.projects(id) ON DELETE CASCADE;
@@ -11071,6 +11561,12 @@ ALTER TABLE ONLY public.path_locks
ALTER TABLE ONLY public.personal_access_tokens
ADD CONSTRAINT fk_personal_access_tokens_user_id FOREIGN KEY (user_id) REFERENCES public.users(id) ON DELETE CASCADE;
+ALTER TABLE ONLY public.project_settings
+ ADD CONSTRAINT fk_project_settings_push_rule_id FOREIGN KEY (push_rule_id) REFERENCES public.push_rules(id) ON DELETE SET NULL;
+
+ALTER TABLE ONLY public.projects
+ ADD CONSTRAINT fk_projects_namespace_id FOREIGN KEY (namespace_id) REFERENCES public.namespaces(id) ON DELETE RESTRICT;
+
ALTER TABLE ONLY public.protected_branch_merge_access_levels
ADD CONSTRAINT fk_protected_branch_merge_access_levels_user_id FOREIGN KEY (user_id) REFERENCES public.users(id) ON DELETE CASCADE;
@@ -11095,6 +11591,9 @@ ALTER TABLE ONLY public.events
ALTER TABLE ONLY public.ip_restrictions
ADD CONSTRAINT fk_rails_04a93778d5 FOREIGN KEY (group_id) REFERENCES public.namespaces(id) ON DELETE CASCADE;
+ALTER TABLE ONLY public.ci_daily_build_group_report_results
+ ADD CONSTRAINT fk_rails_0667f7608c FOREIGN KEY (project_id) REFERENCES public.projects(id) ON DELETE CASCADE;
+
ALTER TABLE ONLY public.ci_subscriptions_projects
ADD CONSTRAINT fk_rails_0818751483 FOREIGN KEY (downstream_project_id) REFERENCES public.projects(id) ON DELETE CASCADE;
@@ -11179,6 +11678,9 @@ ALTER TABLE ONLY public.cluster_providers_aws
ALTER TABLE ONLY public.grafana_integrations
ADD CONSTRAINT fk_rails_18d0e2b564 FOREIGN KEY (project_id) REFERENCES public.projects(id) ON DELETE CASCADE;
+ALTER TABLE ONLY public.group_wiki_repositories
+ ADD CONSTRAINT fk_rails_19755e374b FOREIGN KEY (shard_id) REFERENCES public.shards(id) ON DELETE RESTRICT;
+
ALTER TABLE ONLY public.open_project_tracker_data
ADD CONSTRAINT fk_rails_1987546e48 FOREIGN KEY (service_id) REFERENCES public.services(id) ON DELETE CASCADE;
@@ -11227,12 +11729,18 @@ ALTER TABLE ONLY public.service_desk_settings
ALTER TABLE ONLY public.group_custom_attributes
ADD CONSTRAINT fk_rails_246e0db83a FOREIGN KEY (group_id) REFERENCES public.namespaces(id) ON DELETE CASCADE;
+ALTER TABLE ONLY public.group_wiki_repositories
+ ADD CONSTRAINT fk_rails_26f867598c FOREIGN KEY (group_id) REFERENCES public.namespaces(id) ON DELETE CASCADE;
+
ALTER TABLE ONLY public.lfs_file_locks
ADD CONSTRAINT fk_rails_27a1d98fa8 FOREIGN KEY (user_id) REFERENCES public.users(id) ON DELETE CASCADE;
ALTER TABLE ONLY public.project_alerting_settings
ADD CONSTRAINT fk_rails_27a84b407d FOREIGN KEY (project_id) REFERENCES public.projects(id) ON DELETE CASCADE;
+ALTER TABLE ONLY public.resource_state_events
+ ADD CONSTRAINT fk_rails_29af06892a FOREIGN KEY (issue_id) REFERENCES public.issues(id) ON DELETE CASCADE;
+
ALTER TABLE ONLY public.reviews
ADD CONSTRAINT fk_rails_29e6f859c4 FOREIGN KEY (author_id) REFERENCES public.users(id) ON DELETE SET NULL;
@@ -11248,12 +11756,21 @@ ALTER TABLE ONLY public.geo_repository_updated_events
ALTER TABLE ONLY public.protected_branch_unprotect_access_levels
ADD CONSTRAINT fk_rails_2d2aba21ef FOREIGN KEY (user_id) REFERENCES public.users(id) ON DELETE CASCADE;
+ALTER TABLE ONLY public.ci_freeze_periods
+ ADD CONSTRAINT fk_rails_2e02bbd1a6 FOREIGN KEY (project_id) REFERENCES public.projects(id);
+
ALTER TABLE ONLY public.saml_providers
ADD CONSTRAINT fk_rails_306d459be7 FOREIGN KEY (group_id) REFERENCES public.namespaces(id) ON DELETE CASCADE;
+ALTER TABLE ONLY public.resource_state_events
+ ADD CONSTRAINT fk_rails_3112bba7dc FOREIGN KEY (merge_request_id) REFERENCES public.merge_requests(id) ON DELETE CASCADE;
+
ALTER TABLE ONLY public.merge_request_diff_commits
ADD CONSTRAINT fk_rails_316aaceda3 FOREIGN KEY (merge_request_diff_id) REFERENCES public.merge_request_diffs(id) ON DELETE CASCADE;
+ALTER TABLE ONLY public.group_import_states
+ ADD CONSTRAINT fk_rails_31c3e0503a FOREIGN KEY (group_id) REFERENCES public.namespaces(id) ON DELETE CASCADE;
+
ALTER TABLE ONLY public.zoom_meetings
ADD CONSTRAINT fk_rails_3263f29616 FOREIGN KEY (issue_id) REFERENCES public.issues(id) ON DELETE CASCADE;
@@ -11347,6 +11864,9 @@ ALTER TABLE ONLY public.ci_resources
ALTER TABLE ONLY public.clusters_applications_fluentd
ADD CONSTRAINT fk_rails_4319b1dcd2 FOREIGN KEY (cluster_id) REFERENCES public.clusters(id) ON DELETE CASCADE;
+ALTER TABLE ONLY public.operations_strategies_user_lists
+ ADD CONSTRAINT fk_rails_43241e8d29 FOREIGN KEY (strategy_id) REFERENCES public.operations_strategies(id) ON DELETE CASCADE;
+
ALTER TABLE ONLY public.lfs_file_locks
ADD CONSTRAINT fk_rails_43df7a0412 FOREIGN KEY (project_id) REFERENCES public.projects(id) ON DELETE CASCADE;
@@ -11416,6 +11936,9 @@ ALTER TABLE ONLY public.merge_request_diff_files
ALTER TABLE ONLY public.status_page_settings
ADD CONSTRAINT fk_rails_506e5ba391 FOREIGN KEY (project_id) REFERENCES public.projects(id) ON DELETE CASCADE;
+ALTER TABLE ONLY public.project_repository_storage_moves
+ ADD CONSTRAINT fk_rails_5106dbd44a FOREIGN KEY (project_id) REFERENCES public.projects(id) ON DELETE CASCADE;
+
ALTER TABLE ONLY public.x509_commit_signatures
ADD CONSTRAINT fk_rails_53fe41188f FOREIGN KEY (x509_certificate_id) REFERENCES public.x509_certificates(id) ON DELETE CASCADE;
@@ -11482,6 +12005,9 @@ ALTER TABLE ONLY public.dependency_proxy_group_settings
ALTER TABLE ONLY public.group_deploy_tokens
ADD CONSTRAINT fk_rails_61a572b41a FOREIGN KEY (group_id) REFERENCES public.namespaces(id) ON DELETE CASCADE;
+ALTER TABLE ONLY public.status_page_published_incidents
+ ADD CONSTRAINT fk_rails_61e5493940 FOREIGN KEY (issue_id) REFERENCES public.issues(id) ON DELETE CASCADE;
+
ALTER TABLE ONLY public.deployment_clusters
ADD CONSTRAINT fk_rails_6359a164df FOREIGN KEY (deployment_id) REFERENCES public.deployments(id) ON DELETE CASCADE;
@@ -11881,6 +12407,9 @@ ALTER TABLE ONLY public.user_canonical_emails
ALTER TABLE ONLY public.project_repositories
ADD CONSTRAINT fk_rails_c3258dc63b FOREIGN KEY (shard_id) REFERENCES public.shards(id) ON DELETE RESTRICT;
+ALTER TABLE ONLY public.packages_nuget_dependency_link_metadata
+ ADD CONSTRAINT fk_rails_c3313ee2e4 FOREIGN KEY (dependency_link_id) REFERENCES public.packages_dependency_links(id) ON DELETE CASCADE;
+
ALTER TABLE ONLY public.merge_request_user_mentions
ADD CONSTRAINT fk_rails_c440b9ea31 FOREIGN KEY (note_id) REFERENCES public.notes(id) ON DELETE CASCADE;
@@ -11905,6 +12434,9 @@ ALTER TABLE ONLY public.vulnerability_occurrences
ALTER TABLE ONLY public.project_export_jobs
ADD CONSTRAINT fk_rails_c88d8db2e1 FOREIGN KEY (project_id) REFERENCES public.projects(id) ON DELETE CASCADE;
+ALTER TABLE ONLY public.resource_state_events
+ ADD CONSTRAINT fk_rails_c913c64977 FOREIGN KEY (epic_id) REFERENCES public.epics(id) ON DELETE CASCADE;
+
ALTER TABLE ONLY public.resource_milestone_events
ADD CONSTRAINT fk_rails_c940fb9fc5 FOREIGN KEY (milestone_id) REFERENCES public.milestones(id) ON DELETE CASCADE;
@@ -11920,6 +12452,9 @@ ALTER TABLE ONLY public.ci_daily_report_results
ALTER TABLE ONLY public.issues_self_managed_prometheus_alert_events
ADD CONSTRAINT fk_rails_cc5d88bbb0 FOREIGN KEY (issue_id) REFERENCES public.issues(id) ON DELETE CASCADE;
+ALTER TABLE ONLY public.operations_strategies_user_lists
+ ADD CONSTRAINT fk_rails_ccb7e4bc0b FOREIGN KEY (user_list_id) REFERENCES public.operations_user_lists(id) ON DELETE CASCADE;
+
ALTER TABLE ONLY public.issue_tracker_data
ADD CONSTRAINT fk_rails_ccc0840427 FOREIGN KEY (service_id) REFERENCES public.services(id) ON DELETE CASCADE;
@@ -12019,6 +12554,9 @@ ALTER TABLE ONLY public.ci_daily_report_results
ALTER TABLE ONLY public.cluster_providers_aws
ADD CONSTRAINT fk_rails_ed1fdfaeb2 FOREIGN KEY (created_by_user_id) REFERENCES public.users(id) ON DELETE SET NULL;
+ALTER TABLE ONLY public.ci_daily_build_group_report_results
+ ADD CONSTRAINT fk_rails_ee072d13b3 FOREIGN KEY (last_pipeline_id) REFERENCES public.ci_pipelines(id) ON DELETE CASCADE;
+
ALTER TABLE ONLY public.label_priorities
ADD CONSTRAINT fk_rails_ef916d14fa FOREIGN KEY (project_id) REFERENCES public.projects(id) ON DELETE CASCADE;
@@ -12052,6 +12590,9 @@ ALTER TABLE ONLY public.insights
ALTER TABLE ONLY public.board_group_recent_visits
ADD CONSTRAINT fk_rails_f410736518 FOREIGN KEY (group_id) REFERENCES public.namespaces(id) ON DELETE CASCADE;
+ALTER TABLE ONLY public.resource_state_events
+ ADD CONSTRAINT fk_rails_f5827a7ccd FOREIGN KEY (user_id) REFERENCES public.users(id) ON DELETE SET NULL;
+
ALTER TABLE ONLY public.design_user_mentions
ADD CONSTRAINT fk_rails_f7075a53c1 FOREIGN KEY (design_id) REFERENCES public.design_management_designs(id) ON DELETE CASCADE;
@@ -12082,6 +12623,9 @@ ALTER TABLE ONLY public.serverless_domain_cluster
ALTER TABLE ONLY public.ci_job_variables
ADD CONSTRAINT fk_rails_fbf3b34792 FOREIGN KEY (job_id) REFERENCES public.ci_builds(id) ON DELETE CASCADE;
+ALTER TABLE ONLY public.packages_nuget_metadata
+ ADD CONSTRAINT fk_rails_fc0c19f5b4 FOREIGN KEY (package_id) REFERENCES public.packages_packages(id) ON DELETE CASCADE;
+
ALTER TABLE ONLY public.cluster_groups
ADD CONSTRAINT fk_rails_fdb8648a96 FOREIGN KEY (cluster_id) REFERENCES public.clusters(id) ON DELETE CASCADE;
@@ -13016,14 +13560,17 @@ COPY "schema_migrations" (version) FROM STDIN;
20200213204737
20200213220159
20200213220211
+20200213224220
20200214025454
20200214034836
20200214085940
20200214214934
20200215222507
20200215225103
+20200217210353
20200217223651
20200217225719
+20200218113721
20200219105209
20200219133859
20200219135440
@@ -13064,6 +13611,10 @@ COPY "schema_migrations" (version) FROM STDIN;
20200303055348
20200303074328
20200303181648
+20200304023245
+20200304023851
+20200304024025
+20200304024042
20200304085423
20200304090155
20200304121828
@@ -13094,6 +13645,7 @@ COPY "schema_migrations" (version) FROM STDIN;
20200310135818
20200310135823
20200310145304
+20200310215714
20200311074438
20200311082301
20200311084025
@@ -13108,6 +13660,7 @@ COPY "schema_migrations" (version) FROM STDIN;
20200311214912
20200312053852
20200312125121
+20200312134637
20200312160532
20200312163407
20200313101649
@@ -13136,6 +13689,7 @@ COPY "schema_migrations" (version) FROM STDIN;
20200319203901
20200320112455
20200320123839
+20200320212400
20200323011225
20200323011955
20200323071918
@@ -13169,18 +13723,27 @@ COPY "schema_migrations" (version) FROM STDIN;
20200331132103
20200331195952
20200331220930
+20200401091051
20200401095430
20200401211005
+20200402001106
+20200402115013
+20200402115623
20200402123926
20200402124802
20200402135250
20200402185044
+20200403132349
20200403184110
20200403185127
20200403185422
+20200406095930
+20200406100909
20200406102111
20200406102120
+20200406132529
20200406135648
+20200406141452
20200406192059
20200406193427
20200407094005
@@ -13189,7 +13752,10 @@ COPY "schema_migrations" (version) FROM STDIN;
20200407121321
20200407171133
20200407171417
+20200407182205
+20200407222647
20200408110856
+20200408125046
20200408133211
20200408153842
20200408154331
@@ -13203,18 +13769,103 @@ COPY "schema_migrations" (version) FROM STDIN;
20200408175424
20200408212219
20200409085956
+20200409105455
+20200409105456
20200409211607
+20200410104828
20200410232012
+20200411125656
20200413072059
+20200413230056
+20200414112444
+20200414114611
+20200414115801
20200414144547
+20200415153154
20200415160722
20200415161021
20200415161206
20200415192656
+20200415203024
+20200416005331
20200416111111
20200416120128
20200416120354
+20200417044453
+20200417075843
+20200417145946
+20200420092011
+20200420094444
+20200420104303
+20200420104323
+20200420115948
+20200420141733
+20200420162730
+20200420172113
+20200420172752
+20200420172927
+20200420201933
+20200421092907
+20200421111005
+20200421233150
+20200422091541
+20200422213749
+20200423075720
+20200423080334
+20200423080607
+20200423081409
+20200423081441
+20200423081519
+20200423101529
+20200424043515
+20200424050250
+20200424101920
+20200424135319
+20200427064130
+20200428134356
+20200429001827
+20200429002150
+20200429015603
+20200429181335
+20200429181955
+20200429182245
+20200430103158
+20200430130048
+20200505164958
+20200505171834
+20200505172405
+20200506085748
+20200506125731
+20200506154421
+20200507221434
+20200508091106
+20200511080113
+20200511083541
+20200511092246
+20200511092505
+20200511092714
+20200511115430
+20200511115431
+20200511121549
+20200511121610
+20200511121620
+20200511130129
+20200511130130
+20200511145545
20200511162057
20200511162115
+20200511220023
+20200512085150
+20200512164334
+20200513160930
+20200513171959
+20200513224143
+20200513234502
+20200513235347
+20200513235532
+20200514000009
+20200514000132
+20200514000340
+20200515155620
\.
diff --git a/doc/.vale/gitlab/BadgeCapitalization.yml b/doc/.vale/gitlab/BadgeCapitalization.yml
new file mode 100644
index 00000000000..7e68a06b4d5
--- /dev/null
+++ b/doc/.vale/gitlab/BadgeCapitalization.yml
@@ -0,0 +1,42 @@
+---
+# Verifies that badges are not lower case, which won't render properly.
+#
+# For a list of all options, see https://errata-ai.github.io/vale/styles/
+extends: existence
+message: 'Badge "%s" must be capitalized.'
+link: https://docs.gitlab.com/ee/development/documentation/styleguide.html#product-badges
+level: error
+scope: raw
+raw:
+ - '(\*\*\(Core\)\*\*|'
+ - '\*\*\(core\)\*\*|'
+ - '\*\*\(Starter\)\*\*|'
+ - '\*\*\(starter\)\*\*|'
+ - '\*\*\(Premium\)\*\*|'
+ - '\*\*\(premium\)\*\*|'
+ - '\*\*\(Ultimate\)\*\*|'
+ - '\*\*\(ultimate\)\*\*|'
+ - '\*\*\(Core Only\)\*\*|'
+ - '\*\*\(Core only\)\*\*|'
+ - '\*\*\(core only\)\*\*|'
+ - '\*\*\(Starter Only\)\*\*|'
+ - '\*\*\(Starter only\)\*\*|'
+ - '\*\*\(starter only\)\*\*|'
+ - '\*\*\(Premium Only\)\*\*|'
+ - '\*\*\(Premium only\)\*\*|'
+ - '\*\*\(premium only\)\*\*|'
+ - '\*\*\(Ultimate Only\)\*\*|'
+ - '\*\*\(Ultimate only\)\*\*|'
+ - '\*\*\(ultimate only\)\*\*|'
+ - '\*\*\(Free Only\)\*\*|'
+ - '\*\*\(Free only\)\*\*|'
+ - '\*\*\(free only\)\*\*|'
+ - '\*\*\(Bronze Only\)\*\*|'
+ - '\*\*\(Bronze only\)\*\*|'
+ - '\*\*\(bronze only\)\*\*|'
+ - '\*\*\(Silver Only\)\*\*|'
+ - '\*\*\(Silver only\)\*\*|'
+ - '\*\*\(silver only\)\*\*|'
+ - '\*\*\(Gold Only\)\*\*|'
+ - '\*\*\(Gold only\)\*\*|'
+ - '\*\*\(gold only\)\*\*)'
diff --git a/doc/.vale/gitlab/British.yml b/doc/.vale/gitlab/British.yml
new file mode 100644
index 00000000000..943e85beba1
--- /dev/null
+++ b/doc/.vale/gitlab/British.yml
@@ -0,0 +1,106 @@
+---
+# Checks for use of some of the top misused terms at GitLab.
+#
+# For a list of all options, see https://errata-ai.github.io/vale/styles/
+extends: substitution
+message: 'Use the American spelling "%s" instead of the British "%s".'
+link: https://about.gitlab.com/handbook/communication/#top-misused-terms
+level: error
+ignorecase: true
+swap:
+ aeon: eon
+ aeroplane: airplane
+ ageing: aging
+ aluminium: aluminum
+ anaemia: anemia
+ anaesthesia: anesthesia
+ analyse: analyze
+ annexe: annex
+ apologise: apologize
+ behaviour: behavior
+ busses: buses
+ calibre: caliber
+ centre: center
+ cheque: check
+ civilisation: civilization
+ civilise: civilize
+ colour: color
+ cosy: cozy
+ cypher: cipher
+ dependant: dependent
+ defence: defense
+ distil: distill
+ draught: draft
+ encyclopaedia: encyclopedia
+ enquiry: inquiry
+ enrol: enroll
+ enrolment: enrollment
+ enthral: enthrall
+ # equalled: equaled // Under discussion
+ # equalling: equaling // Under discussion
+ favourite: favorite
+ fibre: fiber
+ fillet: filet
+ flavour: flavor
+ furore: furor
+ fulfil: fulfill
+ gaol: jail
+ grey: gray
+ humour: humor
+ honour: honor
+ initialled: initialed
+ initialling: initialing
+ instil: instill
+ jewellery: jewelry
+ labelling: labeling
+ labelled: labeled
+ labour: labor
+ libellous: libelous
+ licence: license
+ likeable: likable
+ liveable: livable
+ lustre: luster
+ manoeuvre: maneuver
+ marvellous: marvelous
+ matt: matte
+ meagre: meager
+ metre: meter
+ mitre: miter
+ modelling: modeling
+ moustache: mustache
+ neighbour: neighbor
+ normalise: normalize
+ offence: offense
+ organise: organize
+ orientated: oriented
+ paralyse: paralyze
+ plough: plow
+ pretence: pretense
+ programme: program
+ pyjamas: pajamas
+ rateable: ratable
+ realise: realize
+ recognise: recognize
+ reconnoitre: reconnoiter
+ rumour: rumor
+ sabre: saber
+ saleable: salable
+ saltpetre: saltpeter
+ sceptic: skeptic
+ sepulchre: sepulcher
+ signalling: signaling
+ sizeable: sizable
+ skilful: skillful
+ sombre: somber
+ smoulder: smolder
+ speciality: specialty
+ spectre: specter
+ splendour: splendor
+ sulphur: sulfur
+ theatre: theater
+ travelled: traveled
+ traveller: traveler
+ travelling: traveling
+ unshakeable: unshakable
+ wilful: willful
+ yoghurt: yogurt
diff --git a/doc/.vale/gitlab/Contractions.yml b/doc/.vale/gitlab/Contractions.yml
index 5f389bd1ea4..f4ec24742da 100644
--- a/doc/.vale/gitlab/Contractions.yml
+++ b/doc/.vale/gitlab/Contractions.yml
@@ -3,7 +3,7 @@
#
# For a list of all options, see https://errata-ai.github.io/vale/styles/
extends: substitution
-message: Use "%s" instead of "%s" in most cases.
+message: Use "%s" instead of "%s", for a friendly, informal tone.
link: https://docs.gitlab.com/ee/development/documentation/styleguide.html#language
level: suggestion
nonword: false
diff --git a/doc/.vale/gitlab/Profanity.yml b/doc/.vale/gitlab/Profanity.yml
new file mode 100644
index 00000000000..c386b23e52c
--- /dev/null
+++ b/doc/.vale/gitlab/Profanity.yml
@@ -0,0 +1,30 @@
+---
+extends: existence
+message: "Remove profanity: '%s'"
+ignorecase: true
+level: error
+tokens:
+ - 'arse(hole)?'
+ - 'ass(hole)?'
+ - 'bastard'
+ - 'bitch'
+ - 'bloody'
+ - 'bollocks'
+ - 'bugger'
+ - 'cocksucker'
+ - 'crap'
+ - 'cunt'
+ - 'damn'
+ - 'eff(ing)?'
+ - 'fart'
+ - 'fuck(er|ing)?'
+ - 'goddamn(it?|ed)'
+ - 'hell'
+ - 'horseshit'
+ - 'motherfuck(ers?|ing)'
+ - 'piss(ing)?'
+ - 'shit'
+ - 'tits'
+ - 'turd'
+ - 'twat'
+ - 'wank(er|ing)?'
diff --git a/doc/.vale/gitlab/ReferenceLinks.yml b/doc/.vale/gitlab/ReferenceLinks.yml
new file mode 100644
index 00000000000..35a657710de
--- /dev/null
+++ b/doc/.vale/gitlab/ReferenceLinks.yml
@@ -0,0 +1,10 @@
+# Checks for the presence of reference-style links that must be inline.
+#
+# For a list of all options, see https://errata-ai.github.io/vale/styles/
+extends: existence
+message: 'Link "%s" must be inline.'
+link: https://docs.gitlab.com/ee/development/documentation/styleguide.html#basic-link-criteria
+level: error
+scope: raw
+raw:
+ - '\n\[.*\]: .*'
diff --git a/doc/.vale/gitlab/SubstitutionWarning.yml b/doc/.vale/gitlab/SubstitutionWarning.yml
new file mode 100644
index 00000000000..fe690d708ed
--- /dev/null
+++ b/doc/.vale/gitlab/SubstitutionWarning.yml
@@ -0,0 +1,16 @@
+---
+# Warns against using common shorthand for terms.
+# For substitutions flagged as errors, see Substitutions.yml
+#
+# For a list of all options, see https://errata-ai.github.io/vale/styles/
+extends: substitution
+message: 'If possible, use "%s" instead of "%s".'
+link: https://about.gitlab.com/handbook/communication/#top-misused-terms
+level: warning
+ignorecase: true
+swap:
+ admin: administrator
+ config: configuration
+ distro: distribution
+ info: information
+ repo: repository
diff --git a/doc/.vale/gitlab/Substitutions.yml b/doc/.vale/gitlab/Substitutions.yml
index 44b96d1a5e3..b9c0dbecf31 100644
--- a/doc/.vale/gitlab/Substitutions.yml
+++ b/doc/.vale/gitlab/Substitutions.yml
@@ -1,5 +1,6 @@
---
# Checks for use of some of the top misused terms at GitLab.
+# For substitutions only flagged as warnings, see SubstitutionWarning.yml
#
# For a list of all options, see https://errata-ai.github.io/vale/styles/
extends: substitution
@@ -9,8 +10,16 @@ level: error
ignorecase: true
swap:
GitLabber: GitLab team member
+ gitlab omnibus: Omnibus GitLab
param: parameter
params: parameters
+ pg: PostgreSQL
postgres: PostgreSQL
+ raketask: Rake task
+ raketasks: Rake tasks
+ rspec: RSpec
self hosted: self-managed
self-hosted: self-managed
+ styleguide: style guide
+ x509: X.509
+ yaml: YAML
diff --git a/doc/.vale/gitlab/spelling-exceptions.txt b/doc/.vale/gitlab/spelling-exceptions.txt
index 3451219c267..c5e89f72043 100644
--- a/doc/.vale/gitlab/spelling-exceptions.txt
+++ b/doc/.vale/gitlab/spelling-exceptions.txt
@@ -55,6 +55,7 @@ CentOS
Chatops
Citrix
Cloudwatch
+Cobertura
Cognito
colocated
colocating
@@ -85,9 +86,11 @@ discoverability
Disqus
Dockerfile
Dockerfiles
+dotenv
downvoted
downvotes
Dpl
+Dreamweaver
Elasticsearch
enablement
enqueued
@@ -98,6 +101,7 @@ failovers
failsafe
favicon
firewalled
+Flawfinder
Flowdock
Fluentd
Forgerock
@@ -109,15 +113,20 @@ Gitea
GitHub
GitLab
gitlabsos
+Gitleaks
Gitter
Gmail
Google
+Gosec
Gradle
Grafana
gravatar
+Gzip
hardcode
hardcoded
hardcodes
+heatmap
+heatmaps
Helm
Heroku
Herokuish
@@ -130,11 +139,13 @@ hotfixes
hotfixing
http
https
+idempotence
Ingress
initializer
initializers
interdependencies
interdependency
+interruptible
Irker
Istio
jasmine-jquery
@@ -152,6 +163,7 @@ Kibana
Knative
Kramdown
Kubernetes
+Kubesec
Laravel
LDAP
Libravatar
@@ -173,6 +185,7 @@ mergeable
Microsoft
middleware
middlewares
+Minikube
MinIO
mitmproxy
misconfigure
@@ -192,6 +205,8 @@ namespaced
namespaces
Nanoc
NGINX
+npm
+Nurtch
OAuth
Okta
offboarded
@@ -204,10 +219,13 @@ Packagist
parallelization
parallelizations
performant
+Pipfile
+Pipfiles
Piwik
PgBouncer
plaintext
PostgreSQL
+precompile
preconfigure
preconfigured
preconfigures
@@ -232,10 +250,12 @@ Qualys
Rackspace
Raketask
Raketasks
+reachability
rebase
rebased
rebases
rebasing
+Redcarpet
Redis
Redmine
reCAPTCHA
@@ -246,6 +266,7 @@ reindexed
reindexes
reindexing
relicensing
+remediations
Repmgr
Repmgrd
requeue
@@ -265,6 +286,7 @@ resync
reverified
reverifies
reverify
+Rubix
runbook
runbooks
runit
@@ -272,26 +294,36 @@ runtime
runtimes
Salesforce
SAML
+sbt
Sendmail
Sentry
serverless
Sidekiq
sharding
+shfmt
Shibboleth
sanitization
serializer
serializers
serializing
+Sitespeed
Slack
Slony
SMTP
-Sourcegraph
+Sobelow
+spidering
Splunk
+SpotBugs
SSH
storable
strace
+strikethrough
+strikethroughs
+subpath
subfolder
subfolders
+subgraph
+subgraphs
sublicense
sublicensed
sublicenses
@@ -305,6 +337,8 @@ subqueried
subqueries
subquery
subquerying
+substring
+substrings
syslog
Tiller
todos
@@ -331,6 +365,7 @@ unchecking
unchecks
uncomment
uncommented
+uncommenting
unencode
unencoded
unencoder
@@ -349,20 +384,27 @@ unoptimize
unoptimized
unoptimizes
unoptimizing
+unprioritized
unprotect
unprotects
unprotected
unpublish
unpublished
unpublishes
+unpublishing
unreferenced
unresolve
unresolved
unresolving
+unschedule
unstage
unstaged
unstages
unstaging
+unstash
+unstashed
+unstashing
+untarred
untracked
untrusted
unverified
diff --git a/doc/README.md b/doc/README.md
index 8437e7f798e..c9511b22f8f 100644
--- a/doc/README.md
+++ b/doc/README.md
@@ -23,10 +23,11 @@ No matter how you use GitLab, we have documentation for you.
| Essential Documentation | Essential Documentation |
|:-------------------------------------------------------------------------------------------------------------------------------------------|:---------------------------------------------------------------------------------------------------------------------------|
| [**User Documentation**](user/index.md)<br/>Discover features and concepts for GitLab users. | [**Administrator documentation**](administration/index.md)<br/>Everything GitLab self-managed administrators need to know. |
-| [**Contributing to GitLab**](#contributing-to-gitlab)<br/>At GitLab, everyone can contribute! | [**New to Git and GitLab?**](#new-to-git-and-gitlab)<br/>We have the resources to get you started. |
+| [**Contributing to GitLab**](#contributing-to-gitlab)<br/>At GitLab, everyone can contribute! | [**New to Git and GitLab?**](#new-to-git-and-gitlab)<br/>We have the resources to get you started. |
| [**Building an integration with GitLab?**](#building-an-integration-with-gitlab)<br/>Consult our automation and integration documentation. | [**Coming to GitLab from another platform?**](#coming-to-gitlab-from-another-platform)<br/>Consult our handy guides. |
| [**Install GitLab**](https://about.gitlab.com/install/)<br/>Installation options for different platforms. | [**Customers**](subscriptions/index.md)<br/>Information for new and existing customers. |
-| [**Update GitLab**](update/README.md)<br/>Update your GitLab self-managed instance to the latest version. | [**GitLab Releases**](https://about.gitlab.com/releases/)<br/>What's new in GitLab. |
+| [**Update GitLab**](update/README.md)<br/>Update your GitLab self-managed instance to the latest version. | [**Reference Architectures**](administration/reference_architectures/index.md)<br/>GitLab's reference architectures |
+| [**GitLab Releases**](https://about.gitlab.com/releases/)<br/>What's new in GitLab. | |
## Popular Documentation
@@ -50,7 +51,7 @@ GitLab is the first single application for software development, security,
and operations that enables [Concurrent DevOps](https://about.gitlab.com/concurrent-devops/),
making the software lifecycle faster and radically improving the speed of business.
-GitLab provides solutions for [all the stages of the DevOps lifecycle](https://about.gitlab.com/stages-devops-lifecycle/):
+GitLab provides solutions for [each of the stages of the DevOps lifecycle](https://about.gitlab.com/stages-devops-lifecycle/):
![DevOps Stages](img/devops-stages.png)
@@ -65,7 +66,7 @@ The following sections provide links to documentation for each DevOps stage:
|:------------------------|:------------------------------------------------------------|
| [Manage](#manage) | Statistics and analytics features. |
| [Plan](#plan) | Project planning and management features. |
-| [Create](#create) | Source code and data creation and management features. |
+| [Create](#create) | Source code, data creation, and management features. |
| [Verify](#verify) | Testing, code quality, and continuous integration features. |
| [Package](#package) | Docker container registry. |
| [Release](#release) | Application release and delivery features. |
@@ -118,6 +119,7 @@ The following documentation relates to the DevOps **Plan** stage:
| [Project Issue Board](user/project/issue_board.md) | Display issues on a Scrum or Kanban board. |
| [Quick Actions](user/project/quick_actions.md) | Shortcuts for common actions on issues or merge requests, replacing the need to click buttons or use dropdowns in GitLab's UI. |
| [Related Issues](user/project/issues/related_issues.md) **(STARTER)** | Create a relationship between issues. |
+| [Requirements Management](user/project/requirements/index.md) **(ULTIMATE)** | Check your products against a set of criteria. |
| [Roadmap](user/group/roadmap/index.md) **(ULTIMATE)** | Visualize epic timelines. |
| [Service Desk](user/project/service_desk.md) **(PREMIUM)** | A simple way to allow people to create issues in your GitLab instance without needing their own user account. |
| [Time Tracking](user/project/time_tracking.md) | Track time spent on issues and merge requests. |
@@ -283,8 +285,8 @@ The following documentation relates to the DevOps **Release** stage:
| [Auto Deploy](topics/autodevops/stages.md#auto-deploy) | Configure GitLab for the deployment of your application. |
| [Canary Deployments](user/project/canary_deployments.md) **(PREMIUM)** | Employ a popular CI strategy where a small portion of the fleet is updated to the new version first. |
| [Deploy Boards](user/project/deploy_boards.md) **(PREMIUM)** | View the current health and status of each CI environment running on Kubernetes, displaying the status of the pods in the deployment. |
-| [Environments and deployments](ci/environments.md) | With environments, you can control the continuous deployment of your software within GitLab. |
-| [Environment-specific variables](ci/variables/README.md#limiting-environment-scopes-of-environment-variables) | Limit scope of variables to specific environments. |
+| [Environments and deployments](ci/environments/index.md) | With environments, you can control the continuous deployment of your software within GitLab. |
+| [Environment-specific variables](ci/variables/README.md#limit-the-environment-scopes-of-environment-variables) | Limit the scope of variables to specific environments. |
| [GitLab CI/CD](ci/README.md) | Explore the features and capabilities of Continuous Deployment and Delivery with GitLab. |
| [GitLab Pages](user/project/pages/index.md) | Build, test, and deploy a static site directly from GitLab. |
| [Protected Runners](ci/runners/README.md#protected-runners) | Select Runners to only pick jobs for protected branches and tags. |
@@ -299,7 +301,7 @@ The following documentation relates to the DevOps **Release** stage:
### Configure
Automate your entire workflow from build to deploy and monitoring with GitLab
-Auto DevOps. Best practice templates get you started with minimal to zero
+Auto DevOps. Best practice templates help get you started with minimal to zero
configuration. Then customize everything from buildpacks to CI/CD.
The following documentation relates to the DevOps **Configure** stage:
@@ -313,7 +315,7 @@ The following documentation relates to the DevOps **Configure** stage:
| [Installing Applications](user/project/clusters/index.md#installing-applications) | Deploy Helm, Ingress, and Prometheus on Kubernetes. |
| [Mattermost slash commands](user/project/integrations/mattermost_slash_commands.md) | Enable and use slash commands from within Mattermost. |
| [Multiple Kubernetes Clusters](user/project/clusters/index.md#multiple-kubernetes-clusters-premium) **(PREMIUM)** | Associate more than one Kubernetes clusters to your project. |
-| [Protected variables](ci/variables/README.md#protected-environment-variables) | Restrict variables to protected branches and tags. |
+| [Protected variables](ci/variables/README.md#protect-a-custom-variable) | Restrict variables to protected branches and tags. |
| [Serverless](user/project/clusters/serverless/index.md) | Run serverless workloads on Kubernetes. |
| [Slack slash commands](user/project/integrations/slack_slash_commands.md) | Enable and use slash commands from within Slack. |
| [Manage your infrastructure with Terraform](user/infrastructure/index.md) | Manage your infrastructure as you run your CI/CD pipeline. |
@@ -335,7 +337,7 @@ The following documentation relates to the DevOps **Monitor** stage:
| Monitor Topics | Description |
|:------------------------------------------------------------------------------------------------|:-----------------------------------------------------------------------------------------------------------------------------------------|
-| [GitLab Performance Monitoring](administration/monitoring/performance/index.md) **(CORE ONLY)** | Use InfluxDB and Grafana to monitor the performance of your GitLab instance (will be eventually replaced by Prometheus). |
+| [GitLab Performance Monitoring](administration/monitoring/performance/index.md) **(CORE ONLY)** | Use Prometheus and Grafana to monitor the performance of your GitLab instance. |
| [GitLab Prometheus](administration/monitoring/prometheus/index.md) **(CORE ONLY)** | Configure the bundled Prometheus to collect various metrics from your GitLab instance. |
| [Health check](user/admin_area/monitoring/health_check.md) | GitLab provides liveness and readiness probes to indicate service health and reachability to required services. |
| [Prometheus project integration](user/project/integrations/prometheus.md) | Configure the Prometheus integration per project and monitor your CI/CD environments. |
diff --git a/doc/administration/audit_events.md b/doc/administration/audit_events.md
index 26b4434de77..e42982e6524 100644
--- a/doc/administration/audit_events.md
+++ b/doc/administration/audit_events.md
@@ -1,6 +1,12 @@
+---
+stage: Monitor
+group: APM
+info: To determine the technical writer assigned to the Stage/Group associated with this page, see https://about.gitlab.com/handbook/engineering/ux/technical-writing/#designated-technical-writers
+---
+
# Audit Events **(STARTER)**
-GitLab offers a way to view the changes made within the GitLab server for owners and administrators on a [paid plan][ee].
+GitLab offers a way to view the changes made within the GitLab server for owners and administrators on a [paid plan](https://about.gitlab.com/pricing/).
GitLab system administrators can also take advantage of the logs located on the
filesystem. See [the logs system documentation](logs.md) for more details.
@@ -28,10 +34,16 @@ There are two kinds of events logged:
- Instance events scoped to the whole GitLab instance, used by your Compliance team to
perform formal audits.
+### Impersonation data **(PREMIUM)**
+
+> [Introduced](https://gitlab.com/gitlab-org/gitlab/issues/536) in [GitLab Premium](https://about.gitlab.com/pricing/) 13.0.
+
+Impersonation is where an administrator uses credentials to perform an action as a different user.
+
### Group events **(STARTER)**
NOTE: **Note:**
-You need Owner [permissions] to view the group Audit Events page.
+You need Owner [permissions](../user/permissions.md) to view the group Audit Events page.
To view a group's audit events, navigate to **Group > Settings > Audit Events**.
From there, you can see the following actions:
@@ -40,14 +52,15 @@ From there, you can see the following actions:
- Group repository size limit changed
- Group created or deleted
- Group changed visibility
-- User was added to group and with which [permissions]
+- User was added to group and with which [permissions](../user/permissions.md)
- User sign-in via [Group SAML](../user/group/saml_sso/index.md)
- Permissions changes of a user assigned to a group
- Removed user from group
+- Project imported in to group
- Project added to group and with which visibility level
- Project removed from group
- [Project shared with group](../user/project/members/share_project_with_groups.md)
- and with which [permissions]
+ and with which [permissions](../user/permissions.md)
- Removal of a previously shared group with a project
- LFS enabled or disabled
- Shared runners minutes limit changed
@@ -61,7 +74,7 @@ Group events can also be accessed via the [Group Audit Events API](../api/audit_
### Project events **(STARTER)**
NOTE: **Note:**
-You need Maintainer [permissions] or higher to view the project Audit Events page.
+You need Maintainer [permissions](../user/permissions.md) or higher to view the project Audit Events page.
To view a project's audit events, navigate to **Project > Settings > Audit Events**.
From there, you can see the following actions:
@@ -69,7 +82,7 @@ From there, you can see the following actions:
- Added or removed deploy keys
- Project created, deleted, renamed, moved(transferred), changed path
- Project changed visibility level
-- User was added to project and with which [permissions]
+- User was added to project and with which [permissions](../user/permissions.md)
- Permission changes of a user assigned to a project
- User was removed from project
- Project export was downloaded
@@ -86,7 +99,7 @@ From there, you can see the following actions:
### Instance events **(PREMIUM ONLY)**
-> [Introduced][ee-2336] in [GitLab Premium][ee] 9.3.
+> [Introduced](https://gitlab.com/gitlab-org/gitlab/issues/2336) in [GitLab Premium](https://about.gitlab.com/pricing/) 9.3.
Server-wide audit logging introduces the ability to observe user actions across
the entire instance of your GitLab server, making it easy to understand who
@@ -157,7 +170,3 @@ the steps bellow.
```ruby
Feature.enable(:repository_push_audit_event)
```
-
-[ee-2336]: https://gitlab.com/gitlab-org/gitlab/issues/2336
-[ee]: https://about.gitlab.com/pricing/
-[permissions]: ../user/permissions.md
diff --git a/doc/administration/auditor_users.md b/doc/administration/auditor_users.md
index 2bb229d4f68..ace210183b2 100644
--- a/doc/administration/auditor_users.md
+++ b/doc/administration/auditor_users.md
@@ -1,6 +1,6 @@
# Auditor users **(PREMIUM ONLY)**
->[Introduced][ee-998] in [GitLab Premium][eep] 8.17.
+>[Introduced](https://gitlab.com/gitlab-org/gitlab/-/merge_requests/998) in [GitLab Premium](https://about.gitlab.com/pricing/) 8.17.
Auditor users are given read-only access to all projects, groups, and other
resources on the GitLab instance.
@@ -12,7 +12,7 @@ snippets, etc.), and read-only access to **all** other resources, except the
Admin Area. To put another way, they are just regular users (who can be added
to projects, create personal snippets, create milestones on their groups, etc.)
who also happen to have read-only access to all projects on the system that
-they haven't been explicitly [given access][permissions] to.
+they haven't been explicitly [given access](../user/permissions.md) to.
The Auditor role is _not_ a read-only version of the Admin role. Auditor users
will not be able to access the project/group settings pages, or the Admin Area.
@@ -25,7 +25,7 @@ To sum up, assuming you have logged-in as an Auditor user:
- For a project the Auditor owns, the Auditor should have full access to
everything.
- For a project the Auditor has been added to as a member, the Auditor should
- have the same access as the [permissions] they were given to. For example, if
+ have the same access as the [permissions](../user/permissions.md) they were given to. For example, if
they were added as a Developer, they could then push commits or comment on
issues.
- The Auditor cannot view the Admin Area, or perform any admin actions.
@@ -82,7 +82,3 @@ instance, with the following permissions/restrictions:
- Cannot create/modify files from the Web UI
- Cannot merge a merge request
- Cannot create project snippets
-
-[ee-998]: https://gitlab.com/gitlab-org/gitlab/-/merge_requests/998
-[eep]: https://about.gitlab.com/pricing/
-[permissions]: ../user/permissions.md
diff --git a/doc/administration/auth/crowd.md b/doc/administration/auth/crowd.md
index 6c2e4edac31..71938d4fd2b 100644
--- a/doc/administration/auth/crowd.md
+++ b/doc/administration/auth/crowd.md
@@ -66,14 +66,11 @@ Authenticate to GitLab using the Atlassian Crowd OmniAuth provider.
1. Change `YOUR_APP_NAME` to the application name from Crowd applications page.
1. Change `YOUR_APP_PASSWORD` to the application password you've set.
1. Save the configuration file.
-1. [Reconfigure][] or [restart][] for the changes to take effect if you
+1. [Reconfigure](../restart_gitlab.md#omnibus-gitlab-reconfigure) or [restart](../restart_gitlab.md#installations-from-source) for the changes to take effect if you
installed GitLab via Omnibus or from source respectively.
On the sign in page there should now be a Crowd tab in the sign in form.
-[reconfigure]: ../restart_gitlab.md#omnibus-gitlab-reconfigure
-[restart]: ../restart_gitlab.md#installations-from-source
-
## Troubleshooting
If you see an error message like the one below when you sign in after Crowd authentication is configured, you may want to consult the Crowd administrator for the Crowd log file to know the exact cause:
diff --git a/doc/administration/auth/google_secure_ldap.md b/doc/administration/auth/google_secure_ldap.md
index cb7901ea5b4..b643dd2f7b9 100644
--- a/doc/administration/auth/google_secure_ldap.md
+++ b/doc/administration/auth/google_secure_ldap.md
@@ -133,7 +133,7 @@ values obtained during the LDAP client configuration earlier:
EOS
```
-1. Save the file and [reconfigure] GitLab for the changes to take effect.
+1. Save the file and [reconfigure](../restart_gitlab.md#omnibus-gitlab-reconfigure) GitLab for the changes to take effect.
---
@@ -204,10 +204,7 @@ values obtained during the LDAP client configuration earlier:
-----END PRIVATE KEY-----
```
-1. Save the file and [restart] GitLab for the changes to take effect.
-
-[reconfigure]: ../restart_gitlab.md#omnibus-gitlab-reconfigure
-[restart]: ../restart_gitlab.md#installations-from-source
+1. Save the file and [restart](../restart_gitlab.md#installations-from-source) GitLab for the changes to take effect.
<!-- ## Troubleshooting
diff --git a/doc/administration/auth/how_to_configure_ldap_gitlab_ce/index.md b/doc/administration/auth/how_to_configure_ldap_gitlab_ce/index.md
index 49bf786061e..01da4528eab 100644
--- a/doc/administration/auth/how_to_configure_ldap_gitlab_ce/index.md
+++ b/doc/administration/auth/how_to_configure_ldap_gitlab_ce/index.md
@@ -45,10 +45,7 @@ Configuring organizational units (**OU**s) is an important part of setting up Ac
| GitLab **OU** Design | GitLab AD Structure |
| :----------------------------: | :------------------------------: |
-| ![GitLab OU Design][gitlab_ou] | ![GitLab AD Structure][ldap_ou] |
-
-[gitlab_ou]: img/gitlab_ou.png
-[ldap_ou]: img/ldap_ou.gif
+| ![GitLab OU Design](img/gitlab_ou.png) | ![GitLab AD Structure](img/ldap_ou.gif) |
Using PowerShell you can output the **OU** structure as a table (_all names are examples only_):
diff --git a/doc/administration/auth/how_to_configure_ldap_gitlab_ee/index.md b/doc/administration/auth/how_to_configure_ldap_gitlab_ee/index.md
index 46bc079971d..d7b32c8a92a 100644
--- a/doc/administration/auth/how_to_configure_ldap_gitlab_ee/index.md
+++ b/doc/administration/auth/how_to_configure_ldap_gitlab_ee/index.md
@@ -8,7 +8,7 @@ This article expands on [How to Configure LDAP with GitLab CE](../how_to_configu
## GitLab Enterprise Edition - LDAP features
-[GitLab Enterprise Edition (EE)](https://about.gitlab.com/pricing/) has a number of advantages when it comes to integrating with Active Directory (LDAP):
+[GitLab Enterprise Edition (EE)](https://about.gitlab.com/pricing/) has several advantages when it comes to integrating with Active Directory (LDAP):
- [Administrator Sync](../ldap-ee.md#administrator-sync): As an extension of group sync, you can automatically manage your global GitLab administrators. Specify a group CN for `admin_group` and all members of the LDAP group will be given administrator privileges.
- [Group Sync](#group-sync): This allows GitLab group membership to be automatically updated based on LDAP group members.
@@ -16,7 +16,7 @@ This article expands on [How to Configure LDAP with GitLab CE](../how_to_configu
- Daily user synchronization: Once a day, GitLab will run a synchronization to check and update GitLab users against LDAP. This process updates all user details automatically.
-On the following section, you'll find a description for each of these features. Read through [LDAP GitLab EE docs](../ldap-ee.md) for complementary information.
+In the following section, you'll find a description of each of these features. Read through [LDAP GitLab EE docs](../ldap-ee.md) for complementary information.
![GitLab OU Structure](img/admin_group.png)
@@ -28,7 +28,7 @@ Group syncing allows AD (LDAP) groups to be mapped to GitLab groups. This provid
#### Creating group links - example
-As an example, let's suppose we have a "UKGov" GitLab group, which deals with confidential government information. Therefore, it is important that users of this group are given the correct permissions to projects contained within the group. Granular group permissions can be applied based on the AD group.
+As an example, let's suppose we have a "UKGov" GitLab group, which deals with confidential government information. Therefore, users of this group must be given the correct permissions to projects contained within the group. Granular group permissions can be applied based on the AD group.
**UK Developers** of our "UKGov" group are given **"developer"** permissions.
diff --git a/doc/administration/auth/jwt.md b/doc/administration/auth/jwt.md
index 15eee50c771..5a773485842 100644
--- a/doc/administration/auth/jwt.md
+++ b/doc/administration/auth/jwt.md
@@ -64,7 +64,7 @@ JWT will provide you with a secret key for you to use.
1. Change `YOUR_APP_SECRET` to the client secret and set `auth_url` to your redirect URL.
1. Save the configuration file.
-1. [Reconfigure][] or [restart GitLab][] for the changes to take effect if you
+1. [Reconfigure](../restart_gitlab.md#omnibus-gitlab-reconfigure) or [restart GitLab](../restart_gitlab.md#installations-from-source) for the changes to take effect if you
installed GitLab via Omnibus or from source respectively.
On the sign in page there should now be a JWT icon below the regular sign in form.
@@ -72,9 +72,6 @@ Click the icon to begin the authentication process. JWT will ask the user to
sign in and authorize the GitLab application. If everything goes well, the user
will be redirected to GitLab and will be signed in.
-[reconfigure]: ../restart_gitlab.md#omnibus-gitlab-reconfigure
-[restart GitLab]: ../restart_gitlab.md#installations-from-source
-
<!-- ## Troubleshooting
Include any troubleshooting steps that you can foresee. If you know beforehand what issues
diff --git a/doc/administration/auth/ldap-ee.md b/doc/administration/auth/ldap-ee.md
index 3bc30c4c01c..9e193dba08c 100644
--- a/doc/administration/auth/ldap-ee.md
+++ b/doc/administration/auth/ldap-ee.md
@@ -119,6 +119,9 @@ following.
To take advantage of group sync, group owners or maintainers will need to [create one
or more LDAP group links](#adding-group-links).
+NOTE: **Note:**
+If an LDAP user is a group member when LDAP Synchronization is added, and they are not part of the LDAP group, they will be removed from the group.
+
### Adding group links
Once [group sync has been configured](#group-sync) on the instance, one or more LDAP
diff --git a/doc/administration/auth/ldap-troubleshooting.md b/doc/administration/auth/ldap-troubleshooting.md
index 612f782e841..77c9c30d140 100644
--- a/doc/administration/auth/ldap-troubleshooting.md
+++ b/doc/administration/auth/ldap-troubleshooting.md
@@ -312,7 +312,7 @@ things to check to debug the situation.
interval](ldap-ee.md#adjusting-ldap-group-sync-schedule) for the group to
sync. To speed up the process, either go to the GitLab group **Settings ->
Members** and press **Sync now** (sync one group) or [run the group sync Rake
- task](../raketasks/ldap.md#run-a-group-sync) (sync all groups).
+ task](../raketasks/ldap.md#run-a-group-sync-starter-only) (sync all groups).
If all of the above looks good, jump in to a little more advanced debugging in
the rails console.
@@ -352,7 +352,7 @@ GitLab syncs the `admin_group`.
NOTE: **NOTE:**
To sync all groups manually when debugging is unnecessary, [use the Rake
-task](../raketasks/ldap.md#run-a-group-sync) instead.
+task](../raketasks/ldap.md#run-a-group-sync-starter-only) instead.
The output from a manual [group sync](ldap-ee.md#group-sync) can show you what happens
when GitLab syncs its LDAP group memberships against LDAP.
diff --git a/doc/administration/auth/oidc.md b/doc/administration/auth/oidc.md
index 76e83c8e0ad..7a808636e94 100644
--- a/doc/administration/auth/oidc.md
+++ b/doc/administration/auth/oidc.md
@@ -94,7 +94,7 @@ The OpenID Connect will provide you with a client details and secret for you to
- `basic` - HTTP Basic Authentication
- `jwt_bearer` - JWT based authentication (private key and client secret signing)
- `mtls` - Mutual TLS or X.509 certificate validation
- - Any other value will POST the client id and secret in the request body
+ - Any other value will POST the client ID and secret in the request body
- If not specified, defaults to `basic`.
- `<uid_field>` (optional) is the field name from the `user_info` details that will be used as `uid` value. For example, `preferred_username`.
If this value is not provided or the field with the configured value is missing from the `user_info` details, the `uid` will use the `sub` field.
diff --git a/doc/administration/auth/smartcard.md b/doc/administration/auth/smartcard.md
index 8986a9866e7..7131fd7571f 100644
--- a/doc/administration/auth/smartcard.md
+++ b/doc/administration/auth/smartcard.md
@@ -37,7 +37,7 @@ To use a smartcard with an X.509 certificate to authenticate against a local
database with GitLab, `CN` and `emailAddress` must be defined in the
certificate. For example:
-```text
+```plaintext
Certificate:
Data:
Version: 1 (0x0)
@@ -73,7 +73,7 @@ database with GitLab, in:
For example:
-```text
+```plaintext
Certificate:
Data:
Version: 1 (0x0)
diff --git a/doc/administration/availability/index.md b/doc/administration/availability/index.md
index a0d4ea7919f..748373c8941 100644
--- a/doc/administration/availability/index.md
+++ b/doc/administration/availability/index.md
@@ -1,139 +1,5 @@
---
-type: reference, concepts
+redirect_to: ../reference_architectures/index.md
---
-# Availability
-
-GitLab offers high availability options for organizations that require
-the fault tolerance and redundancy necessary to maintain high-uptime operations.
-
-Please consult our [scaling documentation](../scaling) if you want to resolve
-performance bottlenecks you encounter in individual GitLab components without
-incurring the additional complexity costs associated with maintaining a
-highly-available architecture.
-
-On this page, we present examples of self-managed instances which demonstrate
-how GitLab can be scaled out and made highly available. These examples progress
-from simple to complex as scaling or highly-available components are added.
-
-For larger setups serving 2,000 or more users, we provide
-[reference architectures](../scaling/index.md#reference-architectures) based on GitLab's
-experience with GitLab.com and internal scale testing that aim to achieve the
-right balance of scalability and availability.
-
-For detailed insight into how GitLab scales and configures GitLab.com, you can
-watch [this 1 hour Q&A](https://www.youtube.com/watch?v=uCU8jdYzpac)
-with [John Northrup](https://gitlab.com/northrup), and live questions coming
-in from some of our customers.
-
-GitLab offers a number of options to manage availability and resiliency. Below are the options to consider with trade-offs.
-
-| Event | GitLab Feature | Recovery Point Objective (RPO) | Recovery Time Objective (RTO) | Cost |
-| ----- | -------------- | --- | --- | ---- |
-| Availability Zone failure | "GitLab HA" | No loss | No loss | 2x Git storage, multiple nodes balanced across AZ's |
-| Region failure | "GitLab Disaster Recovery" | 5-10 minutes | 30 minutes | 2x primary cost |
-| All failures | Backup/Restore | Last backup | Hours to Days | Cost of storing the backups |
-
-## High availability
-
-### Omnibus installation with automatic database failover
-
-By adding automatic failover for database systems, we can enable higher uptime with an additional layer of complexity.
-
-- For PostgreSQL, we provide repmgr for server cluster management and failover
- and a combination of [PgBouncer](../high_availability/pgbouncer.md) and [Consul](../high_availability/consul.md) for
- database client cutover.
-- For Redis, we use [Redis Sentinel](../high_availability/redis.md) for server failover and client cutover.
-
-You can also optionally run [additional Sidekiq processes on dedicated hardware](../high_availability/sidekiq.md)
-and configure individual Sidekiq processes to
-[process specific background job queues](../operations/extra_sidekiq_processes.md)
-if you need to scale out background job processing.
-
-### GitLab Geo
-
-GitLab Geo allows you to replicate your GitLab instance to other geographical locations as a read-only fully operational instance that can also be promoted in case of disaster.
-
-This configuration is supported in [GitLab Premium and Ultimate](https://about.gitlab.com/pricing/).
-
-References:
-
-- [Geo Documentation](../geo/replication/index.md)
-- [GitLab Geo with a highly available configuration](../geo/replication/high_availability.md)
-
-## GitLab components and configuration instructions
-
-The GitLab application depends on the following [components](../../development/architecture.md#component-diagram).
-It can also depend on several third party services depending on
-your environment setup. Here we'll detail both in the order in which
-you would typically configure them along with our recommendations for
-their use and configuration.
-
-### Third party services
-
-Here's some details of several third party services a typical environment
-will depend on. The services can be provided by numerous applications
-or providers and further advice can be given on how best to select.
-These should be configured first, before the [GitLab components](#gitlab-components).
-
-| Component | Description | Configuration instructions |
-|--------------------------------------------------------|---------------------------------------------------------------------------------------------------------------------|---------------------------------------------------------|
-| [Load Balancer(s)](../high_availability/load_balancer.md)[^6] | Handles load balancing for the GitLab nodes where required | [Load balancer HA configuration](../high_availability/load_balancer.md) |
-| [Cloud Object Storage service](../high_availability/object_storage.md)[^4] | Recommended store for shared data objects | [Cloud Object Storage configuration](../high_availability/object_storage.md) |
-| [NFS](../high_availability/nfs.md)[^5] [^7] | Shared disk storage service. Can be used as an alternative for Gitaly or Object Storage. Required for GitLab Pages | [NFS configuration](../high_availability/nfs.md) |
-
-### GitLab components
-
-Next are all of the components provided directly by GitLab. As mentioned
-earlier, they are presented in the typical order you would configure
-them.
-
-| Component | Description | Configuration instructions |
-|---------------------------------------------------------------------------------------------------------------------|---------------------------------------------------------------------|---------------------------------------------------------------|
-| [Consul](../../development/architecture.md#consul)[^3] | Service discovery and health checks/failover | [Consul HA configuration](../high_availability/consul.md) **(PREMIUM ONLY)** |
-| [PostgreSQL](../../development/architecture.md#postgresql) | Database | [Database HA configuration](../high_availability/database.md) |
-| [PgBouncer](../../development/architecture.md#pgbouncer) | Database Pool Manager | [PgBouncer HA configuration](../high_availability/pgbouncer.md) **(PREMIUM ONLY)** |
-| [Redis](../../development/architecture.md#redis)[^3] with Redis Sentinel | Key/Value store for shared data with HA watcher service | [Redis HA configuration](../high_availability/redis.md) |
-| [Gitaly](../../development/architecture.md#gitaly)[^2] [^5] [^7] | Recommended high-level storage for Git repository data | [Gitaly HA configuration](../high_availability/gitaly.md) |
-| [Sidekiq](../../development/architecture.md#sidekiq) | Asynchronous/Background jobs | [Sidekiq configuration](../high_availability/sidekiq.md) |
-| [GitLab application nodes](../../development/architecture.md#unicorn)[^1] | (Unicorn / Puma, Workhorse) - Web-requests (UI, API, Git over HTTP) | [GitLab app HA/scaling configuration](../high_availability/gitlab.md) |
-| [Prometheus](../../development/architecture.md#prometheus) and [Grafana](../../development/architecture.md#grafana) | GitLab environment monitoring | [Monitoring node for scaling/HA](../high_availability/monitoring_node.md) |
-
-In some cases, components can be combined on the same nodes to reduce complexity as well.
-
-[^1]: In our architectures we run each GitLab Rails node using the Puma webserver
- and have its number of workers set to 90% of available CPUs along with 4 threads.
-
-[^2]: Gitaly node requirements are dependent on customer data, specifically the number of
- projects and their sizes. We recommend 2 nodes as an absolute minimum for HA environments
- and at least 4 nodes should be used when supporting 50,000 or more users.
- We also recommend that each Gitaly node should store no more than 5TB of data
- and have the number of [`gitaly-ruby` workers](../gitaly/index.md#gitaly-ruby)
- set to 20% of available CPUs. Additional nodes should be considered in conjunction
- with a review of expected data size and spread based on the recommendations above.
-
-[^3]: Recommended Redis setup differs depending on the size of the architecture.
- For smaller architectures (up to 5,000 users) we suggest one Redis cluster for all
- classes and that Redis Sentinel is hosted alongside Consul.
- For larger architectures (10,000 users or more) we suggest running a separate
- [Redis Cluster](../high_availability/redis.md#running-multiple-redis-clusters) for the Cache class
- and another for the Queues and Shared State classes respectively. We also recommend
- that you run the Redis Sentinel clusters separately as well for each Redis Cluster.
-
-[^4]: For data objects such as LFS, Uploads, Artifacts, etc. We recommend a [Cloud Object Storage service](../object_storage.md)
- over NFS where possible, due to better performance and availability.
-
-[^5]: NFS can be used as an alternative for both repository data (replacing Gitaly) and
- object storage but this isn't typically recommended for performance reasons. Note however it is required for
- [GitLab Pages](https://gitlab.com/gitlab-org/gitlab-pages/issues/196).
-
-[^6]: Our architectures have been tested and validated with [HAProxy](https://www.haproxy.org/)
- as the load balancer. However other reputable load balancers with similar feature sets
- should also work instead but be aware these aren't validated.
-
-[^7]: We strongly recommend that any Gitaly and / or NFS nodes are set up with SSD disks over
- HDD with a throughput of at least 8,000 IOPS for read operations and 2,000 IOPS for write
- as these components have heavy I/O. These IOPS values are recommended only as a starter
- as with time they may be adjusted higher or lower depending on the scale of your
- environment's workload. If you're running the environment on a Cloud provider
- you may need to refer to their documentation on how configure IOPS correctly.
+This document was moved to [another location](../reference_architectures/index.md).
diff --git a/doc/administration/database_load_balancing.md b/doc/administration/database_load_balancing.md
index b6ce6883dd6..0f566fcc114 100644
--- a/doc/administration/database_load_balancing.md
+++ b/doc/administration/database_load_balancing.md
@@ -1,6 +1,6 @@
# Database Load Balancing **(PREMIUM ONLY)**
-> [Introduced][ee-1283] in [GitLab Premium][eep] 9.0.
+> [Introduced](https://gitlab.com/gitlab-org/gitlab/-/merge_requests/1283) in [GitLab Premium](https://about.gitlab.com/pricing/) 9.0.
Distribute read-only queries among multiple database servers.
@@ -11,7 +11,7 @@ multiple computing resources. Load balancing aims to optimize resource use,
maximize throughput, minimize response time, and avoid overload of any single
resource. Using multiple components with load balancing instead of a single
component may increase reliability and availability through redundancy.
-[_Wikipedia article_][wikipedia]
+[_Wikipedia article_](https://en.wikipedia.org/wiki/Load_balancing_(computing))
When database load balancing is enabled in GitLab, the load is balanced using
a simple round-robin algorithm, without any external dependencies such as Redis.
@@ -26,9 +26,9 @@ sent to the primary (unless necessary), the primary (`db3`) hardly has any load.
## Requirements
-For load balancing to work you will need at least PostgreSQL 9.2 or newer,
-[**MySQL is not supported**][db-req]. You also need to make sure that you have
-at least 1 secondary in [hot standby][hot-standby] mode.
+For load balancing to work you will need at least PostgreSQL 11 or newer,
+[**MySQL is not supported**](../install/requirements.md#database). You also need to make sure that you have
+at least 1 secondary in [hot standby](https://www.postgresql.org/docs/11/hot-standby.html) mode.
Load balancing also requires that the configured hosts **always** point to the
primary, even after a database failover. Furthermore, the additional hosts to
@@ -78,7 +78,7 @@ the following. This will balance the load between `host1.example.com` and
gitlab_rails['db_load_balancing'] = { 'hosts' => ['host1.example.com', 'host2.example.com'] }
```
-1. Save the file and [reconfigure GitLab][] for the changes to take effect.
+1. Save the file and [reconfigure GitLab](restart_gitlab.md#omnibus-gitlab-reconfigure) for the changes to take effect.
---
@@ -97,11 +97,11 @@ the following. This will balance the load between `host1.example.com` and
- host2.example.com
```
-1. Save the file and [restart GitLab][] for the changes to take effect.
+1. Save the file and [restart GitLab](restart_gitlab.md#installations-from-source) for the changes to take effect.
## Service Discovery
-> [Introduced][ee-5883] in [GitLab Premium][eep] 11.0.
+> [Introduced](https://gitlab.com/gitlab-org/gitlab/-/merge_requests/5883) in [GitLab Premium](https://about.gitlab.com/pricing/) 11.0.
Service discovery allows GitLab to automatically retrieve a list of secondary
databases to use, instead of having to manually specify these in the
@@ -161,12 +161,16 @@ When the list of hosts is updated, it might take a while for the old connections
to be terminated. The `disconnect_timeout` setting can be used to enforce an
upper limit on the time it will take to terminate all old database connections.
-Some nameservers (like [Consul][consul-udp]) can return a truncated list of hosts when
+Some nameservers (like [Consul](https://www.consul.io/docs/agent/dns.html#udp-based-dns-queries)) can return a truncated list of hosts when
queried over UDP. To overcome this issue, you can use TCP for querying by setting
`use_tcp` to `true`.
### Forking
+NOTE: **Note:**
+Starting with GitLab 13.0, Puma is the default web server used in GitLab
+all-in-one package based installations as well as GitLab Helm chart deployments.
+
If you use an application server that forks, such as Unicorn, you _have to_
update your Unicorn configuration to start service discovery _after_ a fork.
Failure to do so will lead to service discovery only running in the parent
@@ -239,14 +243,14 @@ For example:
## Handling Stale Reads
-> [Introduced][ee-3526] in [GitLab Premium][eep] 10.3.
+> [Introduced](https://gitlab.com/gitlab-org/gitlab/-/merge_requests/3526) in [GitLab Premium](https://about.gitlab.com/pricing/) 10.3.
To prevent reading from an outdated secondary the load balancer will check if it
is in sync with the primary. If the data is determined to be recent enough the
secondary can be used, otherwise it will be ignored. To reduce the overhead of
these checks we only perform these checks at certain intervals.
-There are three configuration options that influence this behaviour:
+There are three configuration options that influence this behavior:
| Option | Description | Default |
|------------------------------|----------------------------------------------------------------------------------------------------------------|------------|
@@ -270,14 +274,3 @@ production:
max_replication_lag_time: 30
replica_check_interval: 30
```
-
-[hot-standby]: https://www.postgresql.org/docs/9.6/hot-standby.html
-[ee-1283]: https://gitlab.com/gitlab-org/gitlab/-/merge_requests/1283
-[eep]: https://about.gitlab.com/pricing/
-[reconfigure gitlab]: restart_gitlab.md#omnibus-gitlab-reconfigure "How to reconfigure Omnibus GitLab"
-[restart gitlab]: restart_gitlab.md#installations-from-source "How to restart GitLab"
-[wikipedia]: https://en.wikipedia.org/wiki/Load_balancing_(computing)
-[db-req]: ../install/requirements.md#database
-[ee-3526]: https://gitlab.com/gitlab-org/gitlab/-/merge_requests/3526
-[ee-5883]: https://gitlab.com/gitlab-org/gitlab/-/merge_requests/5883
-[consul-udp]: https://www.consul.io/docs/agent/dns.html#udp-based-dns-queries
diff --git a/doc/administration/external_database.md b/doc/administration/external_database.md
index ec2d30c82d1..47509828c20 100644
--- a/doc/administration/external_database.md
+++ b/doc/administration/external_database.md
@@ -5,7 +5,7 @@ managed service for PostgreSQL. For example, AWS offers a managed Relational
Database Service (RDS) that runs PostgreSQL.
Alternatively, you may opt to manage your own PostgreSQL instance or cluster
-separate from the GitLab Omnibus package.
+separate from the Omnibus GitLab package.
If you use a cloud-managed service, or provide your own PostgreSQL instance:
@@ -13,5 +13,29 @@ If you use a cloud-managed service, or provide your own PostgreSQL instance:
[database requirements document](../install/requirements.md#database).
1. Set up a `gitlab` username with a password of your choice. The `gitlab` user
needs privileges to create the `gitlabhq_production` database.
-1. Configure the GitLab application servers with the appropriate details.
- This step is covered in [Configuring GitLab for HA](high_availability/gitlab.md).
+1. If you are using a cloud-managed service, you may need to grant additional
+ roles to your `gitlab` user:
+ - Amazon RDS requires the [`rds_superuser`](https://docs.aws.amazon.com/AmazonRDS/latest/UserGuide/Appendix.PostgreSQL.CommonDBATasks.html#Appendix.PostgreSQL.CommonDBATasks.Roles) role.
+ - Azure Database for PostgreSQL requires the [`azure_pg_admin`](https://docs.microsoft.com/en-us/azure/postgresql/howto-create-users#how-to-create-additional-admin-users-in-azure-database-for-postgresql) role.
+
+1. Configure the GitLab application servers with the appropriate connection details
+ for your external PostgreSQL service in your `/etc/gitlab/gitlab.rb` file:
+
+ ```ruby
+ # Disable the bundled Omnibus provided PostgreSQL
+ postgresql['enable'] = false
+
+ # PostgreSQL connection details
+ gitlab_rails['db_adapter'] = 'postgresql'
+ gitlab_rails['db_encoding'] = 'unicode'
+ gitlab_rails['db_host'] = '10.1.0.5' # IP/hostname of database server
+ gitlab_rails['db_password'] = 'DB password'
+ ```
+
+ For more information on GitLab HA setups, refer to [configuring GitLab for HA](high_availability/gitlab.md).
+
+1. Reconfigure for the changes to take effect:
+
+ ```shell
+ sudo gitlab-ctl reconfigure
+ ```
diff --git a/doc/administration/external_pipeline_validation.md b/doc/administration/external_pipeline_validation.md
index 19d4de3b705..489d1924803 100644
--- a/doc/administration/external_pipeline_validation.md
+++ b/doc/administration/external_pipeline_validation.md
@@ -20,7 +20,7 @@ Response Code Legend:
## Configuration
-Set the `EXTERNAL_VALIDATION_SERVICE_URL` to the external service url.
+Set the `EXTERNAL_VALIDATION_SERVICE_URL` to the external service URL.
## Payload Schema
diff --git a/doc/administration/feature_flags.md b/doc/administration/feature_flags.md
new file mode 100644
index 00000000000..59cd5497032
--- /dev/null
+++ b/doc/administration/feature_flags.md
@@ -0,0 +1,99 @@
+---
+type: reference
+description: "GitLab administrator: enable and disable GitLab features deployed behind feature flags"
+---
+
+# Enable and disable GitLab features deployed behind feature flags **(CORE ONLY)**
+
+GitLab adopted [feature flags strategies](../development/feature_flags/index.md)
+to deploy features in an early stage of development so that they can be
+incrementally rolled out.
+
+Before making them permanently available, features can be deployed behind
+flags for a [number of reasons](../development/feature_flags/process.md#when-to-use-feature-flags), such as:
+
+- To test the feature.
+- To get feedback from users and customers while in an early stage of the development of the feature.
+- To evaluate users adoption.
+- To evaluate how it impacts GitLab's performance.
+- To build it in smaller pieces throughout releases.
+
+Features behind flags can be gradually rolled out, typically:
+
+1. The feature starts disabled by default.
+1. The feature becomes enabled by default.
+1. The feature flag is removed.
+
+These features can be enabled and disabled to allow or disallow users to use
+them. It can be done by GitLab administrators with access to GitLab Rails
+console.
+
+If you used a certain feature and identified a bug, a misbehavior, or an
+error, it's very important that you [**provide feedback**](https://gitlab.com/gitlab-org/gitlab/issues/new?issue[title]=Docs%20-%20feature%20flag%20feedback%3A%20Feature%20Name&issue[description]=Describe%20the%20problem%20you%27ve%20encountered.%0A%0A%3C!--%20Don%27t%20edit%20below%20this%20line%20--%3E%0A%0A%2Flabel%20~%22docs%5C-comments%22%20) to GitLab as soon
+as possible so we can improve or fix it while behind a flag. When you upgrade
+GitLab to an earlier version, the feature flag status may change.
+
+NOTE: **Note:**
+Mind that features deployed behind feature flags may not be ready for
+production use. However, disabling features behind flags that were deployed
+enabled by default may also present a risk. If they're enabled, we recommend
+you leave them as-is.
+
+## How to enable and disable features behind flags
+
+Each feature has its own flag that should be used to enable and disable it.
+The documentation of each feature behind a flag includes a section informing
+the status of the flag and the command to enable or disable it.
+
+### Start the GitLab Rails console
+
+The first thing you need to enable or disable a feature behind a flag is to
+start a session on GitLab Rails console.
+
+For Omnibus installations:
+
+```shell
+sudo gitlab-rails console
+```
+
+For installations from the source:
+
+```shell
+sudo -u git -H bundle exec rails console -e production
+```
+
+For details, see [starting a Rails console session](troubleshooting/debug.md#starting-a-rails-console-session).
+
+### Enable or disable the feature
+
+Once the Rails console session has started, run the `Feature.enable` or
+`Feature.disable` commands accordingly. The specific flag can be found
+in the feature's documentation itself.
+
+To enable a feature, run:
+
+```ruby
+Feature.enable(:<feature flag>)
+```
+
+Example, to enable Evidence Collection:
+
+```ruby
+Feature.enable(:release_evidence_collection)
+```
+
+To disable a feature, run:
+
+```ruby
+Feature.disable(:<feature flag>)
+```
+
+Example, to disable Evidence Collection:
+
+```ruby
+Feature.disable(:release_evidence_collection)
+```
+
+When the feature is ready, GitLab will remove the feature flag, the option for
+enabling and disabling it will no longer exist, and the feature will become
+available in all instances.
diff --git a/doc/administration/file_hooks.md b/doc/administration/file_hooks.md
index 21ade36a2a5..7903da675fd 100644
--- a/doc/administration/file_hooks.md
+++ b/doc/administration/file_hooks.md
@@ -14,12 +14,12 @@ ensure functionality is preserved across versions and covered by tests.
NOTE: **Note:**
File hooks must be configured on the filesystem of the GitLab server. Only GitLab
server administrators will be able to complete these tasks. Explore
-[system hooks] or [webhooks] as an option if you do not have filesystem access.
+[system hooks](../system_hooks/system_hooks.md) or [webhooks](../user/project/integrations/webhooks.md) as an option if you do not have filesystem access.
A file hook will run on each event so it's up to you to filter events or projects
within a file hook code. You can have as many file hooks as you want. Each file hook will
be triggered by GitLab asynchronously in case of an event. For a list of events
-see the [system hooks] documentation.
+see the [system hooks](../system_hooks/system_hooks.md) documentation.
## Setup
@@ -35,8 +35,8 @@ Follow the steps below to set up a custom hook:
`/home/git/gitlab/file_hooks/`. For Omnibus installs the path is
usually `/opt/gitlab/embedded/service/gitlab-rails/file_hooks`.
- For [highly available] configurations, your hook file should exist on each
- application server.
+ For [configurations with multiple servers](reference_architectures/index.md),
+ your hook file should exist on each application server.
1. Inside the `file_hooks` directory, create a file with a name of your choice,
without spaces or special characters.
@@ -46,7 +46,7 @@ Follow the steps below to set up a custom hook:
language type. For example, if the script is in Ruby the shebang will
probably be `#!/usr/bin/env ruby`.
1. The data to the file hook will be provided as JSON on STDIN. It will be exactly
- same as for [system hooks]
+ same as for [system hooks](../system_hooks/system_hooks.md).
That's it! Assuming the file hook code is properly implemented, the hook will fire
as appropriate. The file hooks file list is updated for each event, there is no
@@ -110,7 +110,3 @@ Validating file hooks from /file_hooks directory
* /home/git/gitlab/file_hooks/save_to_file.clj succeed (zero exit code)
* /home/git/gitlab/file_hooks/save_to_file.rb failure (non-zero exit code)
```
-
-[system hooks]: ../system_hooks/system_hooks.md
-[webhooks]: ../user/project/integrations/webhooks.md
-[highly available]: ./availability/index.md
diff --git a/doc/administration/geo/disaster_recovery/background_verification.md b/doc/administration/geo/disaster_recovery/background_verification.md
index 6d6aee08c95..5b24c222f06 100644
--- a/doc/administration/geo/disaster_recovery/background_verification.md
+++ b/doc/administration/geo/disaster_recovery/background_verification.md
@@ -54,14 +54,14 @@ Feature.enable('geo_repository_verification')
Navigate to the **{admin}** **Admin Area >** **{location-dot}** **Geo** dashboard on the **primary** node and expand
the **Verification information** tab for that node to view automatic checksumming
status for repositories and wikis. Successes are shown in green, pending work
-in grey, and failures in red.
+in gray, and failures in red.
![Verification status](img/verification-status-primary.png)
Navigate to the **{admin}** **Admin Area >** **{location-dot}** **Geo** dashboard on the **secondary** node and expand
the **Verification information** tab for that node to view automatic verification
status for repositories and wikis. As with checksumming, successes are shown in
-green, pending work in grey, and failures in red.
+green, pending work in gray, and failures in red.
![Verification status](img/verification-status-secondary.png)
diff --git a/doc/administration/geo/disaster_recovery/index.md b/doc/administration/geo/disaster_recovery/index.md
index 6f417f955ac..9f7afad353b 100644
--- a/doc/administration/geo/disaster_recovery/index.md
+++ b/doc/administration/geo/disaster_recovery/index.md
@@ -167,6 +167,45 @@ do this manually.
previously for the **secondary**.
1. Success! The **secondary** has now been promoted to **primary**.
+#### Promoting a **secondary** node with an external PostgreSQL database
+
+The `gitlab-ctl promote-to-primary-node` command cannot be used in conjunction with
+an external PostgreSQL database, as it can only perform changes on a **secondary**
+node with GitLab and the database on the same machine. As a result, a manual process is
+required:
+
+1. Promote the replica database associated with the **secondary** site. This will
+ set the database to read-write:
+ - Amazon RDS - [Promoting a Read Replica to Be a Standalone DB Instance](https://docs.aws.amazon.com/AmazonRDS/latest/UserGuide/USER_ReadRepl.html#USER_ReadRepl.Promote)
+ - Azure Database for PostgreSQL - [Stop replication](https://docs.microsoft.com/en-us/azure/postgresql/howto-read-replicas-portal#stop-replication)
+
+1. Edit `/etc/gitlab/gitlab.rb` on every node in the **secondary** site to
+ reflect its new status as **primary** by removing any lines that enabled the
+ `geo_secondary_role`:
+
+ ```ruby
+ ## In GitLab 11.4 and earlier, remove this line.
+ geo_secondary_role['enable'] = true
+
+ ## In GitLab 11.5 and later, remove this line.
+ roles ['geo_secondary_role']
+ ```
+
+ After making these changes [Reconfigure GitLab](../../restart_gitlab.md#omnibus-gitlab-reconfigure)
+ on each node so the changes take effect.
+
+1. Promote the **secondary** to **primary**. SSH into a single secondary application
+ node and execute:
+
+ ```shell
+ sudo gitlab-rake geo:set_secondary_as_primary
+ ```
+
+1. Verify you can connect to the newly promoted **primary** site using the URL used
+ previously for the **secondary** site.
+
+Success! The **secondary** site has now been promoted to **primary**.
+
### Step 4. (Optional) Updating the primary domain DNS record
Updating the DNS records for the primary domain to point to the **secondary** node
diff --git a/doc/administration/geo/disaster_recovery/planned_failover.md b/doc/administration/geo/disaster_recovery/planned_failover.md
index 4b3b464b710..00ca1e4b8b0 100644
--- a/doc/administration/geo/disaster_recovery/planned_failover.md
+++ b/doc/administration/geo/disaster_recovery/planned_failover.md
@@ -95,7 +95,7 @@ ensure these processes are close to 100% as possible during active use.
Navigate to the **{admin}** **Admin Area >** **{location-dot}** **Geo** dashboard on the **secondary** node to
review status. Replicated objects (shown in green) should be close to 100%,
and there should be no failures (shown in red). If a large proportion of
-objects aren't yet replicated (shown in grey), consider giving the node more
+objects aren't yet replicated (shown in gray), consider giving the node more
time to complete
![Replication status](img/replication-status.png)
diff --git a/doc/administration/geo/replication/configuration.md b/doc/administration/geo/replication/configuration.md
index 0b076e7ff3c..86a8e5b28d1 100644
--- a/doc/administration/geo/replication/configuration.md
+++ b/doc/administration/geo/replication/configuration.md
@@ -262,7 +262,7 @@ You can login to the **secondary** node with the same credentials you used for t
**secondary** Geo node and if Geo is enabled.
The initial replication, or 'backfill', will probably still be in progress. You
-can monitor the synchronization process on each geo node from the **primary**
+can monitor the synchronization process on each Geo node from the **primary**
node's **Geo Nodes** dashboard in your browser.
![Geo dashboard](img/geo_node_dashboard.png)
@@ -314,19 +314,17 @@ It is important to note that selective synchronization:
Selective synchronization restrictions are implemented on the **secondary** nodes,
not the **primary** node.
-### Git operations on unreplicated respositories
+### Git operations on unreplicated repositories
-> [Introduced](https://gitlab.com/groups/gitlab-org/-/epics/2562) in GitLab 12.10.
+> [Introduced](https://gitlab.com/groups/gitlab-org/-/epics/2562) in GitLab 12.10 for HTTP(S) and in GitLab 13.0 for SSH.
-Git clone, pull, and push operations over HTTP(S) are supported for repositories that
+Git clone, pull, and push operations over HTTP(S) and SSH are supported for repositories that
exist on the **primary** node but not on **secondary** nodes. This situation can occur
when:
- Selective synchronization does not include the project attached to the repository.
- The repository is actively being replicated but has not completed yet.
-SSH [support is planned](https://gitlab.com/groups/gitlab-org/-/epics/2562).
-
## Upgrading Geo
See the [updating the Geo nodes document](updating_the_geo_nodes.md).
diff --git a/doc/administration/geo/replication/database.md b/doc/administration/geo/replication/database.md
index ffdec5a83c7..62bd0e6ac19 100644
--- a/doc/administration/geo/replication/database.md
+++ b/doc/administration/geo/replication/database.md
@@ -33,9 +33,9 @@ recover. See below for more details.
The following guide assumes that:
-- You are using Omnibus and therefore you are using PostgreSQL 9.6 or later
- which includes the [`pg_basebackup` tool](https://www.postgresql.org/docs/9.6/app-pgbasebackup.html) and improved
- [Foreign Data Wrapper](https://www.postgresql.org/docs/9.6/postgres-fdw.html) support.
+- You are using Omnibus and therefore you are using PostgreSQL 11 or later
+ which includes the [`pg_basebackup` tool](https://www.postgresql.org/docs/11/app-pgbasebackup.html) and improved
+ [Foreign Data Wrapper](https://www.postgresql.org/docs/11/postgres-fdw.html) support.
- You have a **primary** node already set up (the GitLab server you are
replicating from), running Omnibus' PostgreSQL (or equivalent version), and
you have a new **secondary** server set up with the same versions of the OS,
@@ -91,7 +91,7 @@ There is an [issue where support is being discussed](https://gitlab.com/gitlab-o
# Fill with the hash generated by `gitlab-ctl pg-password-md5 gitlab`
postgresql['sql_user_password'] = '<md5_hash_of_your_password>'
- # Every node that runs Unicorn or Sidekiq needs to have the database
+ # Every node that runs Puma or Sidekiq needs to have the database
# password specified as below. If you have a high-availability setup, this
# must be present in all application nodes.
gitlab_rails['db_password'] = '<your_password_here>'
@@ -160,7 +160,7 @@ There is an [issue where support is being discussed](https://gitlab.com/gitlab-o
`postgresql['md5_auth_cidr_addresses']` and `postgresql['listen_address']`.
The `listen_address` option opens PostgreSQL up to network connections with the interface
- corresponding to the given address. See [the PostgreSQL documentation](https://www.postgresql.org/docs/9.6/runtime-config-connection.html)
+ corresponding to the given address. See [the PostgreSQL documentation](https://www.postgresql.org/docs/11/runtime-config-connection.html)
for more details.
Depending on your network configuration, the suggested addresses may not
@@ -213,7 +213,7 @@ There is an [issue where support is being discussed](https://gitlab.com/gitlab-o
```
You may also want to edit the `wal_keep_segments` and `max_wal_senders` to match your
- database replication requirements. Consult the [PostgreSQL - Replication documentation](https://www.postgresql.org/docs/9.6/runtime-config-replication.html)
+ database replication requirements. Consult the [PostgreSQL - Replication documentation](https://www.postgresql.org/docs/11/runtime-config-replication.html)
for more information.
1. Save the file and reconfigure GitLab for the database listen changes and
@@ -273,7 +273,7 @@ There is an [issue where support is being discussed](https://gitlab.com/gitlab-o
1. Stop application server and Sidekiq
```shell
- gitlab-ctl stop unicorn
+ gitlab-ctl stop puma
gitlab-ctl stop sidekiq
```
@@ -442,7 +442,7 @@ data before running `pg_basebackup`.
(e.g., you know the network path is secure, or you are using a site-to-site
VPN). This is **not** safe over the public Internet!
- You can read more details about each `sslmode` in the
- [PostgreSQL documentation](https://www.postgresql.org/docs/9.6/libpq-ssl.html#LIBPQ-SSL-PROTECTION);
+ [PostgreSQL documentation](https://www.postgresql.org/docs/11/libpq-ssl.html#LIBPQ-SSL-PROTECTION);
the instructions above are carefully written to ensure protection against
both passive eavesdroppers and active "man-in-the-middle" attackers.
- Change the `--slot-name` to the name of the replication slot
@@ -461,10 +461,10 @@ The replication process is now complete.
PostgreSQL connections. We recommend using PgBouncer if you use GitLab in a
high-availability configuration with a cluster of nodes supporting a Geo
**primary** node and another cluster of nodes supporting a Geo **secondary** node. For more
-information, see [High Availability with GitLab Omnibus](../../high_availability/database.md#high-availability-with-gitlab-omnibus-premium-only).
+information, see [High Availability with Omnibus GitLab](../../high_availability/database.md#high-availability-with-omnibus-gitlab-premium-only).
For a Geo **secondary** node to work properly with PgBouncer in front of the database,
-it will need a separate read-only user to make [PostgreSQL FDW queries](https://www.postgresql.org/docs/9.6/postgres-fdw.html)
+it will need a separate read-only user to make [PostgreSQL FDW queries](https://www.postgresql.org/docs/11/postgres-fdw.html)
work:
1. On the **primary** Geo database, enter the PostgreSQL on the console as an
diff --git a/doc/administration/geo/replication/datatypes.md b/doc/administration/geo/replication/datatypes.md
index 3431df3ed1f..17031b11f51 100644
--- a/doc/administration/geo/replication/datatypes.md
+++ b/doc/administration/geo/replication/datatypes.md
@@ -79,7 +79,7 @@ GitLab stores files and blobs such as Issue attachments or LFS objects into eith
- A Storage Appliance that exposes an Object Storage-compatible API.
When using the filesystem store instead of Object Storage, you need to use network mounted filesystems
-to run GitLab when using more than one server (for example with a High Availability setup).
+to run GitLab when using more than one server.
With respect to replication and verification:
@@ -135,6 +135,7 @@ successfully, you must replicate their data using some other means.
| CI job artifacts (other than traces) | **Yes** | [No](https://gitlab.com/gitlab-org/gitlab/issues/8923) | Verified only manually (*1*) |
| Archived traces | **Yes** | [No](https://gitlab.com/gitlab-org/gitlab/issues/8923) | Verified only on transfer, or manually (*1*) |
| Personal snippets | **Yes** | **Yes** | |
+| [Versioned snippets](../../../user/snippets.md#versioned-snippets) | [No](https://gitlab.com/groups/gitlab-org/-/epics/2809) | [No](https://gitlab.com/groups/gitlab-org/-/epics/2810) | |
| Project snippets | **Yes** | **Yes** | |
| Object pools for forked project deduplication | **Yes** | No | |
| [Server-side Git Hooks](../../custom_hooks.md) | No | No | |
@@ -145,7 +146,7 @@ successfully, you must replicate their data using some other means.
| [Maven Repository](../../../user/packages/maven_repository/index.md) | [No](https://gitlab.com/groups/gitlab-org/-/epics/2346) | No | |
| [Conan Repository](../../../user/packages/conan_repository/index.md) | [No](https://gitlab.com/groups/gitlab-org/-/epics/2346) | No | |
| [NuGet Repository](../../../user/packages/nuget_repository/index.md) | [No](https://gitlab.com/groups/gitlab-org/-/epics/2346) | No | |
-| [PyPi Repository](../../../user/packages/pypi_repository/index.md) | [No](https://gitlab.com/groups/gitlab-org/-/epics/2554) | No | |
+| [PyPi Repository](../../../user/packages/pypi_repository/index.md) | [No](https://gitlab.com/groups/gitlab-org/-/epics/2554) | No | |
| [External merge request diffs](../../merge_request_diffs.md) | [No](https://gitlab.com/gitlab-org/gitlab/issues/33817) | No | |
| Content in object storage | **Yes** | No | |
diff --git a/doc/administration/geo/replication/external_database.md b/doc/administration/geo/replication/external_database.md
index e305718580e..ae3069a0e40 100644
--- a/doc/administration/geo/replication/external_database.md
+++ b/doc/administration/geo/replication/external_database.md
@@ -17,6 +17,19 @@ developed and tested. We aim to be compatible with most external
sudo -i
```
+1. Edit `/etc/gitlab/gitlab.rb` and add a **unique** ID for your node (arbitrary value):
+
+ ```ruby
+ # The unique identifier for the Geo node.
+ gitlab_rails['geo_node_name'] = '<node_name_here>'
+ ```
+
+1. Reconfigure the **primary** node for the change to take effect:
+
+ ```shell
+ gitlab-ctl reconfigure
+ ```
+
1. Execute the command below to define the node as **primary** node:
```shell
@@ -38,7 +51,14 @@ Given you have a primary node set up on AWS EC2 that uses RDS.
You can now just create a read-only replica in a different region and the
replication process will be managed by AWS. Make sure you've set Network ACL, Subnet, and
Security Group according to your needs, so the secondary application node can access the database.
-Skip to the [Configure secondary application node](#configure-secondary-application-nodes-to-use-the-external-read-replica) section below.
+
+The following instructions detail how to create a read-only replica for common
+cloud providers:
+
+- Amazon RDS - [Creating a Read Replica](https://docs.aws.amazon.com/AmazonRDS/latest/UserGuide/USER_ReadRepl.html#USER_ReadRepl.Create)
+- Azure Database for PostgreSQL - [Create and manage read replicas in Azure Database for PostgreSQL](https://docs.microsoft.com/en-us/azure/postgresql/howto-read-replicas-portal)
+
+Once your read-only replica is set up, you can skip to [configure you secondary application node](#configure-secondary-application-nodes-to-use-the-external-read-replica).
#### Manually configure the primary database for replication
@@ -133,6 +153,10 @@ To configure the connection to the external read-replica database and enable Log
gitlab_rails['db_username'] = 'gitlab'
gitlab_rails['db_host'] = '<database_read_replica_host>'
+
+ # Disable the bundled Omnibus PostgreSQL, since we are
+ # using an external PostgreSQL
+ postgresql['enable'] = false
```
1. Save the file and [reconfigure GitLab](../../restart_gitlab.md#omnibus-gitlab-reconfigure)
@@ -142,11 +166,17 @@ To configure the connection to the external read-replica database and enable Log
**Secondary** nodes use a separate PostgreSQL installation as a tracking
database to keep track of replication status and automatically recover from
potential replication issues. Omnibus automatically configures a tracking database
-when `roles ['geo_secondary_role']` is set. For high availability,
-refer to [Geo High Availability](../../availability/index.md).
+when `roles ['geo_secondary_role']` is set.
If you want to run this database external to Omnibus, please follow the instructions below.
-The tracking database requires an [FDW](https://www.postgresql.org/docs/9.6/postgres-fdw.html)
+If you are using a cloud-managed service for the tracking database, you may need
+to grant additional roles to your tracking database user (by default, this is
+`gitlab_geo`):
+
+- Amazon RDS requires the [`rds_superuser`](https://docs.aws.amazon.com/AmazonRDS/latest/UserGuide/Appendix.PostgreSQL.CommonDBATasks.html#Appendix.PostgreSQL.CommonDBATasks.Roles) role.
+- Azure Database for PostgreSQL requires the [`azure_pg_admin`](https://docs.microsoft.com/en-us/azure/postgresql/howto-create-users#how-to-create-additional-admin-users-in-azure-database-for-postgresql) role.
+
+The tracking database requires an [FDW](https://www.postgresql.org/docs/11/postgres-fdw.html)
connection with the **secondary** replica database for improved performance.
If you have an external database ready to be used as the tracking database,
@@ -200,7 +230,7 @@ the tracking database on port 5432.
gitlab-rake geo:db:migrate
```
-1. Configure the [PostgreSQL FDW](https://www.postgresql.org/docs/9.6/postgres-fdw.html)
+1. Configure the [PostgreSQL FDW](https://www.postgresql.org/docs/11/postgres-fdw.html)
connection and credentials:
Save the script below in a file, ex. `/tmp/geo_fdw.sh` and modify the connection
diff --git a/doc/administration/geo/replication/geo_validation_tests.md b/doc/administration/geo/replication/geo_validation_tests.md
new file mode 100644
index 00000000000..a8b0bdeb7da
--- /dev/null
+++ b/doc/administration/geo/replication/geo_validation_tests.md
@@ -0,0 +1,100 @@
+# Geo validation tests
+
+The Geo team performs manual testing and validation on common deployment configurations to ensure
+that Geo works when upgrading between minor GitLab versions and major PostgreSQL database versions.
+
+This section contains a journal of recent validation tests and links to the relevant issues.
+
+## GitLab upgrades
+
+The following are GitLab upgrade validation tests we performed.
+
+### February 2020
+
+[Upgrade Geo multi-server installation](https://gitlab.com/gitlab-org/gitlab/-/issues/201837):
+
+- Description: Tested upgrading from GitLab 12.7.5 to the latest GitLab 12.8 package in a multi-server
+ configuration.
+- Outcome: Partial success because we did not run the looping pipeline during the demo to monitor
+ downtime.
+
+### January 2020
+
+[Upgrade Geo multi-server installation](https://gitlab.com/gitlab-org/gitlab/-/issues/200085):
+
+- Description: Tested upgrading from GitLab 12.6.x to the latest GitLab 12.7 package in a multi-server
+ configuration.
+- Outcome: Upgrade test was successful.
+- Follow up issues:
+ - [Investigate Geo end-to-end test failures](https://gitlab.com/gitlab-org/gitlab/issues/201823).
+ - [Add more logging to Geo end-to-end tests](https://gitlab.com/gitlab-org/gitlab/issues/201830).
+ - [Excess service restarts during zero-downtime upgrade](https://gitlab.com/gitlab-org/omnibus-gitlab/-/issues/5047).
+
+[Upgrade Geo multi-server installation](https://gitlab.com/gitlab-org/gitlab/-/issues/199836):
+
+- Description: Tested upgrading from GitLab 12.5.7 to GitLab 12.6.6 in a multi-server configuration.
+- Outcome: Upgrade test was successful.
+- Follow up issue:
+ [Update documentation for zero-downtime upgrades to ensure deploy node it not in use](https://gitlab.com/gitlab-org/omnibus-gitlab/-/issues/5046).
+
+[Upgrade Geo multi-server installation](https://gitlab.com/gitlab-org/gitlab/-/issues/37044):
+
+- Description: Tested upgrading from GitLab 12.4.x to the latest GitLab 12.5 package in a multi-server
+ configuration.
+- Outcome: Upgrade test was successful.
+- Follow up issues:
+ - [Investigate why HTTP push spec failed on primary node](https://gitlab.com/gitlab-org/gitlab/issues/199825).
+ - [Investigate if documentation should be modified to include refresh foreign tables task](https://gitlab.com/gitlab-org/omnibus-gitlab/-/issues/5041).
+
+### October 2019
+
+[Upgrade Geo multi-server installation](https://gitlab.com/gitlab-org/gitlab/-/issues/35262):
+
+- Description: Tested upgrading from GitLab 12.3.5 to GitLab 12.4.1 in a multi-server configuration.
+- Outcome: Upgrade test was successful.
+
+[Upgrade Geo multi-server installation](https://gitlab.com/gitlab-org/gitlab/-/issues/32437):
+
+- Description: Tested upgrading from GitLab 12.2.8 to GitLab 12.3.5.
+- Outcome: Upgrade test was successful.
+
+[Upgrade Geo multi-server installation](https://gitlab.com/gitlab-org/gitlab/-/issues/32435):
+
+- Description: Tested upgrading from GitLab 12.1.9 to GitLab 12.2.8.
+- Outcome: Partial success due to possible misconfiguration issues.
+
+## PostgreSQL upgrades
+
+The following are PostgreSQL upgrade validation tests we performed.
+
+### April 2020
+
+[PostgreSQL 11 upgrade procedure for Geo installations](https://gitlab.com/gitlab-org/omnibus-gitlab/-/issues/4975):
+
+- Description: Prior to making PostgreSQL 11 the default version of PostgreSQL in GitLab 12.10, we
+ tested upgrading to PostgreSQL 11 in Geo deployments on GitLab 12.9.
+- Outcome: Partially successful. Issues were discovered in multi-server configurations with a separate
+ tracking database and concerns were raised about allowing automatic upgrades when Geo enabled.
+- Follow up issues:
+ - [`replicate-geo-database` incorrectly tries to back up repositories](https://gitlab.com/gitlab-org/omnibus-gitlab/-/issues/5241).
+ - [`pg-upgrade` fails to upgrade a standalone Geo tracking database](https://gitlab.com/gitlab-org/omnibus-gitlab/-/issues/5242).
+ - [`revert-pg-upgrade` fails to downgrade the PostgreSQL data of a Geo secondary’s standalone tracking database](https://gitlab.com/gitlab-org/omnibus-gitlab/-/issues/5243).
+ - [Timeout error on Geo secondary read-replica near the end of `gitlab-ctl pg-upgrade`](https://gitlab.com/gitlab-org/omnibus-gitlab/-/issues/5235).
+
+[Verify Geo installation with PostgreSQL 11](https://gitlab.com/gitlab-org/omnibus-gitlab/-/issues/4971):
+
+- Description: Prior to making PostgreSQL 11 the default version of PostgreSQL in GitLab 12.10, we
+ tested fresh installations of GitLab 12.9 with Geo installed with PostgreSQL 11.
+- Outcome: Installation test was successful.
+
+### September 2019
+
+[Test and validate PostgreSQL 10.0 upgrade for Geo](https://gitlab.com/gitlab-org/gitlab/issues/12092):
+
+- Description: With the 12.0 release, GitLab required an upgrade to PostgreSQL 10.0. We tested
+ various upgrade scenarios from GitLab 11.11.5 through to GitLab 12.1.8.
+- Outcome: Multiple issues were found when upgrading and addressed in follow-up issues.
+- Follow up issues:
+ - [`gitlab-ctl` reconfigure fails on Redis node in multi-server Geo setup](https://gitlab.com/gitlab-org/omnibus-gitlab/-/issues/4706).
+ - [Geo multi-server upgrade from 12.0.9 to 12.1.9 does not upgrade PostgreSQL](https://gitlab.com/gitlab-org/omnibus-gitlab/-/issues/4705).
+ - [Refresh foreign tables fails on app server in multi-server setup after upgrade to 12.1.9](https://gitlab.com/gitlab-org/gitlab/-/issues/32119).
diff --git a/doc/administration/geo/replication/high_availability.md b/doc/administration/geo/replication/high_availability.md
index 5099e73d5e8..214f15b7565 100644
--- a/doc/administration/geo/replication/high_availability.md
+++ b/doc/administration/geo/replication/high_availability.md
@@ -1,460 +1,5 @@
-# Geo High Availability **(PREMIUM ONLY)**
+---
+redirect_to: 'multiple_servers.md'
+---
-This document describes a minimal reference architecture for running Geo
-in a high availability configuration. If your HA setup differs from the one
-described, it is possible to adapt these instructions to your needs.
-
-## Architecture overview
-
-![Geo HA Diagram](../../high_availability/img/geo-ha-diagram.png)
-
-_[diagram source - GitLab employees only](https://docs.google.com/drawings/d/1z0VlizKiLNXVVVaERFwgsIOuEgjcUqDTWPdQYsE7Z4c/edit)_
-
-The topology above assumes that the **primary** and **secondary** Geo clusters
-are located in two separate locations, on their own virtual network
-with private IP addresses. The network is configured such that all machines within
-one geographic location can communicate with each other using their private IP addresses.
-The IP addresses given are examples and may be different depending on the
-network topology of your deployment.
-
-The only external way to access the two Geo deployments is by HTTPS at
-`gitlab.us.example.com` and `gitlab.eu.example.com` in the example above.
-
-NOTE: **Note:**
-The **primary** and **secondary** Geo deployments must be able to communicate to each other over HTTPS.
-
-## Redis and PostgreSQL High Availability
-
-Geo supports:
-
-- Redis and PostgreSQL on the **primary** node configured for high availability
-- Redis on **secondary** nodes configured for high availability.
-
-NOTE: **Note:**
-Support for PostgreSQL on **secondary** nodes in high availability configuration
-[is planned](https://gitlab.com/groups/gitlab-org/-/epics/2536).
-
-Because of the additional complexity involved in setting up this configuration
-for PostgreSQL and Redis, it is not covered by this Geo HA documentation.
-
-For more information about setting up a highly available PostgreSQL cluster and Redis cluster using the omnibus package see the high availability documentation for
-[PostgreSQL](../../high_availability/database.md) and
-[Redis](../../high_availability/redis.md), respectively.
-
-NOTE: **Note:**
-It is possible to use cloud hosted services for PostgreSQL and Redis, but this is beyond the scope of this document.
-
-## Prerequisites: Two working GitLab HA clusters
-
-One cluster will serve as the **primary** node. Use the
-[GitLab HA documentation](../../availability/index.md) to set this up. If
-you already have a working GitLab instance that is in-use, it can be used as a
-**primary**.
-
-The second cluster will serve as the **secondary** node. Again, use the
-[GitLab HA documentation](../../availability/index.md) to set this up.
-It's a good idea to log in and test it, however, note that its data will be
-wiped out as part of the process of replicating from the **primary**.
-
-## Configure the GitLab cluster to be the **primary** node
-
-The following steps enable a GitLab cluster to serve as the **primary** node.
-
-### Step 1: Configure the **primary** frontend servers
-
-1. Edit `/etc/gitlab/gitlab.rb` and add the following:
-
- ```ruby
- ##
- ## Enable the Geo primary role
- ##
- roles ['geo_primary_role']
-
- ##
- ## The unique identifier for the Geo node.
- ##
- gitlab_rails['geo_node_name'] = '<node_name_here>'
-
- ##
- ## Disable automatic migrations
- ##
- gitlab_rails['auto_migrate'] = false
- ```
-
-After making these changes, [reconfigure GitLab](../../restart_gitlab.md#omnibus-gitlab-reconfigure) so the changes take effect.
-
-NOTE: **Note:** PostgreSQL and Redis should have already been disabled on the
-application servers, and connections from the application servers to those
-services on the backend servers configured, during normal GitLab HA set up. See
-high availability configuration documentation for
-[PostgreSQL](../../high_availability/database.md#configuring-the-application-nodes)
-and [Redis](../../high_availability/redis.md#example-configuration-for-the-gitlab-application).
-
-### Step 2: Configure the **primary** database
-
-1. Edit `/etc/gitlab/gitlab.rb` and add the following:
-
- ```ruby
- ##
- ## Configure the Geo primary role and the PostgreSQL role
- ##
- roles ['geo_primary_role', 'postgres_role']
- ```
-
-## Configure a **secondary** node
-
-A **secondary** cluster is similar to any other GitLab HA cluster, with two
-major differences:
-
-- The main PostgreSQL database is a read-only replica of the **primary** node's
- PostgreSQL database.
-- There is also a single PostgreSQL database for the **secondary** cluster,
- called the "tracking database", which tracks the synchronization state of
- various resources.
-
-Therefore, we will set up the HA components one-by-one, and include deviations
-from the normal HA setup. However, we highly recommend first configuring a
-brand-new cluster as if it were not part of a Geo setup so that it can be
-tested and verified as a working cluster. And only then should it be modified
-for use as a Geo **secondary**. This helps to separate problems that are related
-and are not related to Geo setup.
-
-### Step 1: Configure the Redis and Gitaly services on the **secondary** node
-
-Configure the following services, again using the non-Geo high availability
-documentation:
-
-- [Configuring Redis for GitLab HA](../../high_availability/redis.md) for high
- availability.
-- [Gitaly](../../high_availability/gitaly.md), which will store data that is
- synchronized from the **primary** node.
-
-NOTE: **Note:**
-[NFS](../../high_availability/nfs.md) can be used in place of Gitaly but is not
-recommended.
-
-### Step 2: Configure the main read-only replica PostgreSQL database on the **secondary** node
-
-NOTE: **Note:** The following documentation assumes the database will be run on
-a single node only. PostgreSQL HA on **secondary** nodes is
-[not currently supported](https://gitlab.com/groups/gitlab-org/-/epics/2536).
-
-Configure the [**secondary** database](database.md) as a read-only replica of
-the **primary** database. Use the following as a guide.
-
-1. Generate an MD5 hash of the desired password for the database user that the
- GitLab application will use to access the read-replica database:
-
- Note that the username (`gitlab` by default) is incorporated into the hash.
-
- ```shell
- gitlab-ctl pg-password-md5 gitlab
- # Enter password: <your_password_here>
- # Confirm password: <your_password_here>
- # fca0b89a972d69f00eb3ec98a5838484
- ```
-
- Use this hash to fill in `<md5_hash_of_your_password>` in the next step.
-
-1. Edit `/etc/gitlab/gitlab.rb` in the replica database machine, and add the
- following:
-
- ```ruby
- ##
- ## Configure the Geo secondary role and the PostgreSQL role
- ##
- roles ['geo_secondary_role', 'postgres_role']
-
- ##
- ## Secondary address
- ## - replace '<secondary_node_ip>' with the public or VPC address of your Geo secondary node
- ## - replace '<tracking_database_ip>' with the public or VPC address of your Geo tracking database node
- ##
- postgresql['listen_address'] = '<secondary_node_ip>'
- postgresql['md5_auth_cidr_addresses'] = ['<secondary_node_ip>/32', '<tracking_database_ip>/32']
-
- ##
- ## Database credentials password (defined previously in primary node)
- ## - replicate same values here as defined in primary node
- ##
- postgresql['sql_user_password'] = '<md5_hash_of_your_password>'
- gitlab_rails['db_password'] = '<your_password_here>'
-
- ##
- ## When running the Geo tracking database on a separate machine, disable it
- ## here and allow connections from the tracking database host. And ensure
- ## the tracking database IP is in postgresql['md5_auth_cidr_addresses'] above.
- ##
- geo_postgresql['enable'] = false
-
- ##
- ## Disable `geo_logcursor` service so Rails doesn't get configured here
- ##
- geo_logcursor['enable'] = false
- ```
-
-After making these changes, [reconfigure GitLab](../../restart_gitlab.md#omnibus-gitlab-reconfigure) so the changes take effect.
-
-If using an external PostgreSQL instance, refer also to
-[Geo with external PostgreSQL instances](external_database.md).
-
-### Step 3: Configure the tracking database on the **secondary** node
-
-NOTE: **Note:** This documentation assumes the tracking database will be run on
-only a single machine, rather than as a PostgreSQL cluster.
-
-Configure the tracking database.
-
-1. Generate an MD5 hash of the desired password for the database user that the
- GitLab application will use to access the tracking database:
-
- Note that the username (`gitlab_geo` by default) is incorporated into the
- hash.
-
- ```shell
- gitlab-ctl pg-password-md5 gitlab_geo
- # Enter password: <your_password_here>
- # Confirm password: <your_password_here>
- # fca0b89a972d69f00eb3ec98a5838484
- ```
-
- Use this hash to fill in `<tracking_database_password_md5_hash>` in the next
- step.
-
-1. Edit `/etc/gitlab/gitlab.rb` in the tracking database machine, and add the
- following:
-
- ```ruby
- ##
- ## Enable the Geo secondary tracking database
- ##
- geo_postgresql['enable'] = true
- geo_postgresql['listen_address'] = '<ip_address_of_this_host>'
- geo_postgresql['sql_user_password'] = '<tracking_database_password_md5_hash>'
-
- ##
- ## Configure FDW connection to the replica database
- ##
- geo_secondary['db_fdw'] = true
- geo_postgresql['fdw_external_password'] = '<replica_database_password_plaintext>'
- geo_postgresql['md5_auth_cidr_addresses'] = ['<replica_database_ip>/32']
- gitlab_rails['db_host'] = '<replica_database_ip>'
-
- # Prevent reconfigure from attempting to run migrations on the replica DB
- gitlab_rails['auto_migrate'] = false
-
- ##
- ## Disable all other services that aren't needed, since we don't have a role
- ## that does this.
- ##
- alertmanager['enable'] = false
- consul['enable'] = false
- gitaly['enable'] = false
- gitlab_exporter['enable'] = false
- gitlab_workhorse['enable'] = false
- nginx['enable'] = false
- node_exporter['enable'] = false
- pgbouncer_exporter['enable'] = false
- postgresql['enable'] = false
- prometheus['enable'] = false
- redis['enable'] = false
- redis_exporter['enable'] = false
- repmgr['enable'] = false
- sidekiq['enable'] = false
- unicorn['enable'] = false
- ```
-
-After making these changes, [reconfigure GitLab](../../restart_gitlab.md#omnibus-gitlab-reconfigure) so the changes take effect.
-
-If using an external PostgreSQL instance, refer also to
-[Geo with external PostgreSQL instances](external_database.md).
-
-### Step 4: Configure the frontend application servers on the **secondary** node
-
-In the architecture overview, there are two machines running the GitLab
-application services. These services are enabled selectively in the
-configuration.
-
-Configure the application servers following
-[Configuring GitLab for HA](../../high_availability/gitlab.md), then make the
-following modifications:
-
-1. Edit `/etc/gitlab/gitlab.rb` on each application server in the **secondary**
- cluster, and add the following:
-
- ```ruby
- ##
- ## Enable the Geo secondary role
- ##
- roles ['geo_secondary_role', 'application_role']
-
- ##
- ## The unique identifier for the Geo node.
- ##
- gitlab_rails['geo_node_name'] = '<node_name_here>'
-
- ##
- ## Disable automatic migrations
- ##
- gitlab_rails['auto_migrate'] = false
-
- ##
- ## Configure the connection to the tracking DB. And disable application
- ## servers from running tracking databases.
- ##
- geo_secondary['db_host'] = '<geo_tracking_db_host>'
- geo_secondary['db_password'] = '<geo_tracking_db_password>'
- geo_postgresql['enable'] = false
-
- ##
- ## Configure connection to the streaming replica database, if you haven't
- ## already
- ##
- gitlab_rails['db_host'] = '<replica_database_host>'
- gitlab_rails['db_password'] = '<replica_database_password>'
-
- ##
- ## Configure connection to Redis, if you haven't already
- ##
- gitlab_rails['redis_host'] = '<redis_host>'
- gitlab_rails['redis_password'] = '<redis_password>'
-
- ##
- ## If you are using custom users not managed by Omnibus, you need to specify
- ## UIDs and GIDs like below, and ensure they match between servers in a
- ## cluster to avoid permissions issues
- ##
- user['uid'] = 9000
- user['gid'] = 9000
- web_server['uid'] = 9001
- web_server['gid'] = 9001
- registry['uid'] = 9002
- registry['gid'] = 9002
- ```
-
-NOTE: **Note:**
-If you had set up PostgreSQL cluster using the omnibus package and you had set
-up `postgresql['sql_user_password'] = 'md5 digest of secret'` setting, keep in
-mind that `gitlab_rails['db_password']` and `geo_secondary['db_password']`
-mentioned above contains the plaintext passwords. This is used to let the Rails
-servers connect to the databases.
-
-NOTE: **Note:**
-Make sure that current node IP is listed in `postgresql['md5_auth_cidr_addresses']` setting of your remote database.
-
-After making these changes [Reconfigure GitLab](../../restart_gitlab.md#omnibus-gitlab-reconfigure) so the changes take effect.
-
-On the secondary the following GitLab frontend services will be enabled:
-
-- `geo-logcursor`
-- `gitlab-pages`
-- `gitlab-workhorse`
-- `logrotate`
-- `nginx`
-- `registry`
-- `remote-syslog`
-- `sidekiq`
-- `unicorn`
-
-Verify these services by running `sudo gitlab-ctl status` on the frontend
-application servers.
-
-### Step 5: Set up the LoadBalancer for the **secondary** node
-
-In this topology, a load balancer is required at each geographic location to
-route traffic to the application servers.
-
-See [Load Balancer for GitLab HA](../../high_availability/load_balancer.md) for
-more information.
-
-### Step 6: Configure the backend application servers on the **secondary** node
-
-The minimal reference architecture diagram above shows all application services
-running together on the same machines. However, for high availability we
-[strongly recommend running all services separately](../../availability/index.md).
-
-For example, a Sidekiq server could be configured similarly to the frontend
-application servers above, with some changes to run only the `sidekiq` service:
-
-1. Edit `/etc/gitlab/gitlab.rb` on each Sidekiq server in the **secondary**
- cluster, and add the following:
-
- ```ruby
- ##
- ## Enable the Geo secondary role
- ##
- roles ['geo_secondary_role']
-
- ##
- ## Enable the Sidekiq service
- ##
- sidekiq['enable'] = true
-
- ##
- ## Ensure unnecessary services are disabled
- ##
- alertmanager['enable'] = false
- consul['enable'] = false
- geo_logcursor['enable'] = false
- gitaly['enable'] = false
- gitlab_exporter['enable'] = false
- gitlab_workhorse['enable'] = false
- nginx['enable'] = false
- node_exporter['enable'] = false
- pgbouncer_exporter['enable'] = false
- postgresql['enable'] = false
- prometheus['enable'] = false
- redis['enable'] = false
- redis_exporter['enable'] = false
- repmgr['enable'] = false
- unicorn['enable'] = false
-
- ##
- ## The unique identifier for the Geo node.
- ##
- gitlab_rails['geo_node_name'] = '<node_name_here>'
-
- ##
- ## Disable automatic migrations
- ##
- gitlab_rails['auto_migrate'] = false
-
- ##
- ## Configure the connection to the tracking DB. And disable application
- ## servers from running tracking databases.
- ##
- geo_secondary['db_host'] = '<geo_tracking_db_host>'
- geo_secondary['db_password'] = '<geo_tracking_db_password>'
- geo_postgresql['enable'] = false
-
- ##
- ## Configure connection to the streaming replica database, if you haven't
- ## already
- ##
- gitlab_rails['db_host'] = '<replica_database_host>'
- gitlab_rails['db_password'] = '<replica_database_password>'
-
- ##
- ## Configure connection to Redis, if you haven't already
- ##
- gitlab_rails['redis_host'] = '<redis_host>'
- gitlab_rails['redis_password'] = '<redis_password>'
-
- ##
- ## If you are using custom users not managed by Omnibus, you need to specify
- ## UIDs and GIDs like below, and ensure they match between servers in a
- ## cluster to avoid permissions issues
- ##
- user['uid'] = 9000
- user['gid'] = 9000
- web_server['uid'] = 9001
- web_server['gid'] = 9001
- registry['uid'] = 9002
- registry['gid'] = 9002
- ```
-
- You can similarly configure a server to run only the `geo-logcursor` service
- with `geo_logcursor['enable'] = true` and disabling Sidekiq with
- `sidekiq['enable'] = false`.
-
- These servers do not need to be attached to the load balancer.
+This document was moved to [another location](multiple_servers.md).
diff --git a/doc/administration/geo/replication/index.md b/doc/administration/geo/replication/index.md
index 7c661abef9a..87bd7b69515 100644
--- a/doc/administration/geo/replication/index.md
+++ b/doc/administration/geo/replication/index.md
@@ -2,7 +2,7 @@
> - Introduced in GitLab Enterprise Edition 8.9.
> - Using Geo in combination with
-> [High Availability](../../availability/index.md)
+> [multi-server architectures](../../reference_architectures/index.md)
> is considered **Generally Available** (GA) in
> [GitLab Premium](https://about.gitlab.com/pricing/) 10.4.
@@ -110,7 +110,7 @@ The following are required to run Geo:
The following operating systems are known to ship with a current version of OpenSSH:
- [CentOS](https://www.centos.org) 7.4+
- [Ubuntu](https://ubuntu.com) 16.04+
-- PostgreSQL 9.6+ with [FDW](https://www.postgresql.org/docs/9.6/postgres-fdw.html) support and [Streaming Replication](https://wiki.postgresql.org/wiki/Streaming_Replication)
+- PostgreSQL 11+ with [FDW](https://www.postgresql.org/docs/11/postgres-fdw.html) support and [Streaming Replication](https://wiki.postgresql.org/wiki/Streaming_Replication)
- Git 2.9+
- All nodes must run the same GitLab version.
@@ -134,7 +134,7 @@ The following table lists basic ports that must be open between the **primary**
See the full list of ports used by GitLab in [Package defaults](https://docs.gitlab.com/omnibus/package-information/defaults.html)
NOTE: **Note:**
-[Web terminal](../../../ci/environments.md#web-terminals) support requires your load balancer to correctly handle WebSocket connections.
+[Web terminal](../../../ci/environments/index.md#web-terminals) support requires your load balancer to correctly handle WebSocket connections.
When using HTTP or HTTPS proxying, your load balancer must be configured to pass through the `Connection` and `Upgrade` hop-by-hop headers. See the [web terminal](../../integration/terminal.md) integration guide for more details.
NOTE: **Note:**
@@ -206,9 +206,9 @@ For information on configuring Geo, see [Geo configuration](configuration.md).
For information on how to update your Geo nodes to the latest GitLab version, see [Updating the Geo nodes](updating_the_geo_nodes.md).
-### Configuring Geo high availability
+### Configuring Geo for multiple servers
-For information on configuring Geo for high availability, see [Geo High Availability](high_availability.md).
+For information on configuring Geo for multiple servers, see [Geo for multiple servers](multiple_servers.md).
### Configuring Geo with Object Storage
@@ -245,7 +245,7 @@ This list of limitations only reflects the latest version of GitLab. If you are
- Pushing directly to a **secondary** node redirects (for HTTP) or proxies (for SSH) the request to the **primary** node instead of [handling it directly](https://gitlab.com/gitlab-org/gitlab/issues/1381), except when using Git over HTTP with credentials embedded within the URI. For example, `https://user:password@secondary.tld`.
- Cloning, pulling, or pushing repositories that exist on the **primary** node but not on the **secondary** nodes where [selective synchronization](configuration.md#selective-synchronization) does not include the project is not supported over SSH [but support is planned](https://gitlab.com/groups/gitlab-org/-/epics/2562). HTTP(S) is supported.
-- The **primary** node has to be online for OAuth login to happen. Existing sessions and Git are not affected.
+- The **primary** node has to be online for OAuth login to happen. Existing sessions and Git are not affected. Support for the **secondary** node to use an OAuth provider independent from the primary is [being planned](https://gitlab.com/gitlab-org/gitlab/issues/208465).
- The installation takes multiple manual steps that together can take about an hour depending on circumstances. We are working on improving this experience. See [Omnibus GitLab issue #2978](https://gitlab.com/gitlab-org/omnibus-gitlab/issues/2978) for details.
- Real-time updates of issues/merge requests (for example, via long polling) doesn't work on the **secondary** node.
- [Selective synchronization](configuration.md#selective-synchronization) applies only to files and repositories. Other datasets are replicated to the **secondary** node in full, making it inappropriate for use as an access control mechanism.
diff --git a/doc/administration/geo/replication/multiple_servers.md b/doc/administration/geo/replication/multiple_servers.md
new file mode 100644
index 00000000000..9322c4cc417
--- /dev/null
+++ b/doc/administration/geo/replication/multiple_servers.md
@@ -0,0 +1,459 @@
+# Geo for multiple servers **(PREMIUM ONLY)**
+
+This document describes a minimal reference architecture for running Geo
+in a multi-server configuration. If your multi-server setup differs from the one
+described, it is possible to adapt these instructions to your needs.
+
+## Architecture overview
+
+![Geo multi-server diagram](../../high_availability/img/geo-ha-diagram.png)
+
+_[diagram source - GitLab employees only](https://docs.google.com/drawings/d/1z0VlizKiLNXVVVaERFwgsIOuEgjcUqDTWPdQYsE7Z4c/edit)_
+
+The topology above assumes that the **primary** and **secondary** Geo clusters
+are located in two separate locations, on their own virtual network
+with private IP addresses. The network is configured such that all machines within
+one geographic location can communicate with each other using their private IP addresses.
+The IP addresses given are examples and may be different depending on the
+network topology of your deployment.
+
+The only external way to access the two Geo deployments is by HTTPS at
+`gitlab.us.example.com` and `gitlab.eu.example.com` in the example above.
+
+NOTE: **Note:**
+The **primary** and **secondary** Geo deployments must be able to communicate to each other over HTTPS.
+
+## Redis and PostgreSQL for multiple servers
+
+Geo supports:
+
+- Redis and PostgreSQL on the **primary** node configured for multiple servers.
+- Redis on **secondary** nodes configured for multiple servers.
+
+NOTE: **Note:**
+Support for PostgreSQL on **secondary** nodes in multi-server configuration
+[is planned](https://gitlab.com/groups/gitlab-org/-/epics/2536).
+
+Because of the additional complexity involved in setting up this configuration
+for PostgreSQL and Redis, it is not covered by this Geo multi-server documentation.
+
+For more information about setting up a multi-server PostgreSQL cluster and Redis cluster using the omnibus package see the multi-server documentation for
+[PostgreSQL](../../high_availability/database.md) and
+[Redis](../../high_availability/redis.md), respectively.
+
+NOTE: **Note:**
+It is possible to use cloud hosted services for PostgreSQL and Redis, but this is beyond the scope of this document.
+
+## Prerequisites: Two working GitLab multi-server clusters
+
+One cluster will serve as the **primary** node. Use the
+[GitLab multi-server documentation](../../reference_architectures/index.md) to set this up. If
+you already have a working GitLab instance that is in-use, it can be used as a
+**primary**.
+
+The second cluster will serve as the **secondary** node. Again, use the
+[GitLab multi-server documentation](../../reference_architectures/index.md) to set this up.
+It's a good idea to log in and test it, however, note that its data will be
+wiped out as part of the process of replicating from the **primary**.
+
+## Configure the GitLab cluster to be the **primary** node
+
+The following steps enable a GitLab cluster to serve as the **primary** node.
+
+### Step 1: Configure the **primary** frontend servers
+
+1. Edit `/etc/gitlab/gitlab.rb` and add the following:
+
+ ```ruby
+ ##
+ ## Enable the Geo primary role
+ ##
+ roles ['geo_primary_role']
+
+ ##
+ ## The unique identifier for the Geo node.
+ ##
+ gitlab_rails['geo_node_name'] = '<node_name_here>'
+
+ ##
+ ## Disable automatic migrations
+ ##
+ gitlab_rails['auto_migrate'] = false
+ ```
+
+After making these changes, [reconfigure GitLab](../../restart_gitlab.md#omnibus-gitlab-reconfigure) so the changes take effect.
+
+NOTE: **Note:** PostgreSQL and Redis should have already been disabled on the
+application servers, and connections from the application servers to those
+services on the backend servers configured, during normal GitLab multi-server set up. See
+multi-server configuration documentation for
+[PostgreSQL](../../high_availability/database.md#configuring-the-application-nodes)
+and [Redis](../../high_availability/redis.md#example-configuration-for-the-gitlab-application).
+
+### Step 2: Configure the **primary** database
+
+1. Edit `/etc/gitlab/gitlab.rb` and add the following:
+
+ ```ruby
+ ##
+ ## Configure the Geo primary role and the PostgreSQL role
+ ##
+ roles ['geo_primary_role', 'postgres_role']
+ ```
+
+## Configure a **secondary** node
+
+A **secondary** cluster is similar to any other GitLab multi-server cluster, with two
+major differences:
+
+- The main PostgreSQL database is a read-only replica of the **primary** node's
+ PostgreSQL database.
+- There is also a single PostgreSQL database for the **secondary** cluster,
+ called the "tracking database", which tracks the synchronization state of
+ various resources.
+
+Therefore, we will set up the multi-server components one-by-one, and include deviations
+from the normal multi-server setup. However, we highly recommend first configuring a
+brand-new cluster as if it were not part of a Geo setup so that it can be
+tested and verified as a working cluster. And only then should it be modified
+for use as a Geo **secondary**. This helps to separate problems that are related
+and are not related to Geo setup.
+
+### Step 1: Configure the Redis and Gitaly services on the **secondary** node
+
+Configure the following services, again using the non-Geo multi-server
+documentation:
+
+- [Configuring Redis for GitLab](../../high_availability/redis.md) for multiple servers.
+- [Gitaly](../../high_availability/gitaly.md), which will store data that is
+ synchronized from the **primary** node.
+
+NOTE: **Note:**
+[NFS](../../high_availability/nfs.md) can be used in place of Gitaly but is not
+recommended.
+
+### Step 2: Configure the main read-only replica PostgreSQL database on the **secondary** node
+
+NOTE: **Note:** The following documentation assumes the database will be run on
+a single node only. Multi-server PostgreSQL on **secondary** nodes is
+[not currently supported](https://gitlab.com/groups/gitlab-org/-/epics/2536).
+
+Configure the [**secondary** database](database.md) as a read-only replica of
+the **primary** database. Use the following as a guide.
+
+1. Generate an MD5 hash of the desired password for the database user that the
+ GitLab application will use to access the read-replica database:
+
+ Note that the username (`gitlab` by default) is incorporated into the hash.
+
+ ```shell
+ gitlab-ctl pg-password-md5 gitlab
+ # Enter password: <your_password_here>
+ # Confirm password: <your_password_here>
+ # fca0b89a972d69f00eb3ec98a5838484
+ ```
+
+ Use this hash to fill in `<md5_hash_of_your_password>` in the next step.
+
+1. Edit `/etc/gitlab/gitlab.rb` in the replica database machine, and add the
+ following:
+
+ ```ruby
+ ##
+ ## Configure the Geo secondary role and the PostgreSQL role
+ ##
+ roles ['geo_secondary_role', 'postgres_role']
+
+ ##
+ ## Secondary address
+ ## - replace '<secondary_node_ip>' with the public or VPC address of your Geo secondary node
+ ## - replace '<tracking_database_ip>' with the public or VPC address of your Geo tracking database node
+ ##
+ postgresql['listen_address'] = '<secondary_node_ip>'
+ postgresql['md5_auth_cidr_addresses'] = ['<secondary_node_ip>/32', '<tracking_database_ip>/32']
+
+ ##
+ ## Database credentials password (defined previously in primary node)
+ ## - replicate same values here as defined in primary node
+ ##
+ postgresql['sql_user_password'] = '<md5_hash_of_your_password>'
+ gitlab_rails['db_password'] = '<your_password_here>'
+
+ ##
+ ## When running the Geo tracking database on a separate machine, disable it
+ ## here and allow connections from the tracking database host. And ensure
+ ## the tracking database IP is in postgresql['md5_auth_cidr_addresses'] above.
+ ##
+ geo_postgresql['enable'] = false
+
+ ##
+ ## Disable `geo_logcursor` service so Rails doesn't get configured here
+ ##
+ geo_logcursor['enable'] = false
+ ```
+
+After making these changes, [reconfigure GitLab](../../restart_gitlab.md#omnibus-gitlab-reconfigure) so the changes take effect.
+
+If using an external PostgreSQL instance, refer also to
+[Geo with external PostgreSQL instances](external_database.md).
+
+### Step 3: Configure the tracking database on the **secondary** node
+
+NOTE: **Note:** This documentation assumes the tracking database will be run on
+only a single machine, rather than as a PostgreSQL cluster.
+
+Configure the tracking database.
+
+1. Generate an MD5 hash of the desired password for the database user that the
+ GitLab application will use to access the tracking database:
+
+ Note that the username (`gitlab_geo` by default) is incorporated into the
+ hash.
+
+ ```shell
+ gitlab-ctl pg-password-md5 gitlab_geo
+ # Enter password: <your_password_here>
+ # Confirm password: <your_password_here>
+ # fca0b89a972d69f00eb3ec98a5838484
+ ```
+
+ Use this hash to fill in `<tracking_database_password_md5_hash>` in the next
+ step.
+
+1. Edit `/etc/gitlab/gitlab.rb` in the tracking database machine, and add the
+ following:
+
+ ```ruby
+ ##
+ ## Enable the Geo secondary tracking database
+ ##
+ geo_postgresql['enable'] = true
+ geo_postgresql['listen_address'] = '<ip_address_of_this_host>'
+ geo_postgresql['sql_user_password'] = '<tracking_database_password_md5_hash>'
+
+ ##
+ ## Configure FDW connection to the replica database
+ ##
+ geo_secondary['db_fdw'] = true
+ geo_postgresql['fdw_external_password'] = '<replica_database_password_plaintext>'
+ geo_postgresql['md5_auth_cidr_addresses'] = ['<replica_database_ip>/32']
+ gitlab_rails['db_host'] = '<replica_database_ip>'
+
+ # Prevent reconfigure from attempting to run migrations on the replica DB
+ gitlab_rails['auto_migrate'] = false
+
+ ##
+ ## Disable all other services that aren't needed, since we don't have a role
+ ## that does this.
+ ##
+ alertmanager['enable'] = false
+ consul['enable'] = false
+ gitaly['enable'] = false
+ gitlab_exporter['enable'] = false
+ gitlab_workhorse['enable'] = false
+ nginx['enable'] = false
+ node_exporter['enable'] = false
+ pgbouncer_exporter['enable'] = false
+ postgresql['enable'] = false
+ prometheus['enable'] = false
+ redis['enable'] = false
+ redis_exporter['enable'] = false
+ repmgr['enable'] = false
+ sidekiq['enable'] = false
+ puma['enable'] = false
+ ```
+
+After making these changes, [reconfigure GitLab](../../restart_gitlab.md#omnibus-gitlab-reconfigure) so the changes take effect.
+
+If using an external PostgreSQL instance, refer also to
+[Geo with external PostgreSQL instances](external_database.md).
+
+### Step 4: Configure the frontend application servers on the **secondary** node
+
+In the architecture overview, there are two machines running the GitLab
+application services. These services are enabled selectively in the
+configuration.
+
+Configure the application servers following
+[Configuring GitLab for multiple servers](../../high_availability/gitlab.md), then make the
+following modifications:
+
+1. Edit `/etc/gitlab/gitlab.rb` on each application server in the **secondary**
+ cluster, and add the following:
+
+ ```ruby
+ ##
+ ## Enable the Geo secondary role
+ ##
+ roles ['geo_secondary_role', 'application_role']
+
+ ##
+ ## The unique identifier for the Geo node.
+ ##
+ gitlab_rails['geo_node_name'] = '<node_name_here>'
+
+ ##
+ ## Disable automatic migrations
+ ##
+ gitlab_rails['auto_migrate'] = false
+
+ ##
+ ## Configure the connection to the tracking DB. And disable application
+ ## servers from running tracking databases.
+ ##
+ geo_secondary['db_host'] = '<geo_tracking_db_host>'
+ geo_secondary['db_password'] = '<geo_tracking_db_password>'
+ geo_postgresql['enable'] = false
+
+ ##
+ ## Configure connection to the streaming replica database, if you haven't
+ ## already
+ ##
+ gitlab_rails['db_host'] = '<replica_database_host>'
+ gitlab_rails['db_password'] = '<replica_database_password>'
+
+ ##
+ ## Configure connection to Redis, if you haven't already
+ ##
+ gitlab_rails['redis_host'] = '<redis_host>'
+ gitlab_rails['redis_password'] = '<redis_password>'
+
+ ##
+ ## If you are using custom users not managed by Omnibus, you need to specify
+ ## UIDs and GIDs like below, and ensure they match between servers in a
+ ## cluster to avoid permissions issues
+ ##
+ user['uid'] = 9000
+ user['gid'] = 9000
+ web_server['uid'] = 9001
+ web_server['gid'] = 9001
+ registry['uid'] = 9002
+ registry['gid'] = 9002
+ ```
+
+NOTE: **Note:**
+If you had set up PostgreSQL cluster using the omnibus package and you had set
+up `postgresql['sql_user_password'] = 'md5 digest of secret'` setting, keep in
+mind that `gitlab_rails['db_password']` and `geo_secondary['db_password']`
+mentioned above contains the plaintext passwords. This is used to let the Rails
+servers connect to the databases.
+
+NOTE: **Note:**
+Make sure that current node IP is listed in `postgresql['md5_auth_cidr_addresses']` setting of your remote database.
+
+After making these changes [Reconfigure GitLab](../../restart_gitlab.md#omnibus-gitlab-reconfigure) so the changes take effect.
+
+On the secondary the following GitLab frontend services will be enabled:
+
+- `geo-logcursor`
+- `gitlab-pages`
+- `gitlab-workhorse`
+- `logrotate`
+- `nginx`
+- `registry`
+- `remote-syslog`
+- `sidekiq`
+- `puma`
+
+Verify these services by running `sudo gitlab-ctl status` on the frontend
+application servers.
+
+### Step 5: Set up the LoadBalancer for the **secondary** node
+
+In this topology, a load balancer is required at each geographic location to
+route traffic to the application servers.
+
+See [Load Balancer for GitLab with multiple servers](../../high_availability/load_balancer.md) for
+more information.
+
+### Step 6: Configure the backend application servers on the **secondary** node
+
+The minimal reference architecture diagram above shows all application services
+running together on the same machines. However, for multiple servers we
+[strongly recommend running all services separately](../../reference_architectures/index.md).
+
+For example, a Sidekiq server could be configured similarly to the frontend
+application servers above, with some changes to run only the `sidekiq` service:
+
+1. Edit `/etc/gitlab/gitlab.rb` on each Sidekiq server in the **secondary**
+ cluster, and add the following:
+
+ ```ruby
+ ##
+ ## Enable the Geo secondary role
+ ##
+ roles ['geo_secondary_role']
+
+ ##
+ ## Enable the Sidekiq service
+ ##
+ sidekiq['enable'] = true
+
+ ##
+ ## Ensure unnecessary services are disabled
+ ##
+ alertmanager['enable'] = false
+ consul['enable'] = false
+ geo_logcursor['enable'] = false
+ gitaly['enable'] = false
+ gitlab_exporter['enable'] = false
+ gitlab_workhorse['enable'] = false
+ nginx['enable'] = false
+ node_exporter['enable'] = false
+ pgbouncer_exporter['enable'] = false
+ postgresql['enable'] = false
+ prometheus['enable'] = false
+ redis['enable'] = false
+ redis_exporter['enable'] = false
+ repmgr['enable'] = false
+ puma['enable'] = false
+
+ ##
+ ## The unique identifier for the Geo node.
+ ##
+ gitlab_rails['geo_node_name'] = '<node_name_here>'
+
+ ##
+ ## Disable automatic migrations
+ ##
+ gitlab_rails['auto_migrate'] = false
+
+ ##
+ ## Configure the connection to the tracking DB. And disable application
+ ## servers from running tracking databases.
+ ##
+ geo_secondary['db_host'] = '<geo_tracking_db_host>'
+ geo_secondary['db_password'] = '<geo_tracking_db_password>'
+ geo_postgresql['enable'] = false
+
+ ##
+ ## Configure connection to the streaming replica database, if you haven't
+ ## already
+ ##
+ gitlab_rails['db_host'] = '<replica_database_host>'
+ gitlab_rails['db_password'] = '<replica_database_password>'
+
+ ##
+ ## Configure connection to Redis, if you haven't already
+ ##
+ gitlab_rails['redis_host'] = '<redis_host>'
+ gitlab_rails['redis_password'] = '<redis_password>'
+
+ ##
+ ## If you are using custom users not managed by Omnibus, you need to specify
+ ## UIDs and GIDs like below, and ensure they match between servers in a
+ ## cluster to avoid permissions issues
+ ##
+ user['uid'] = 9000
+ user['gid'] = 9000
+ web_server['uid'] = 9001
+ web_server['gid'] = 9001
+ registry['uid'] = 9002
+ registry['gid'] = 9002
+ ```
+
+ You can similarly configure a server to run only the `geo-logcursor` service
+ with `geo_logcursor['enable'] = true` and disabling Sidekiq with
+ `sidekiq['enable'] = false`.
+
+ These servers do not need to be attached to the load balancer.
diff --git a/doc/administration/geo/replication/security_review.md b/doc/administration/geo/replication/security_review.md
index 18fe1ad22cd..0ac8157220a 100644
--- a/doc/administration/geo/replication/security_review.md
+++ b/doc/administration/geo/replication/security_review.md
@@ -73,7 +73,7 @@ from [owasp.org](https://owasp.org/).
- Nothing Geo-specific. Any user where `admin: true` is set in the database is
considered an admin with super-user privileges.
- See also: [more granular access control](https://gitlab.com/gitlab-org/gitlab-foss/issues/32730)
- (not geo-specific)
+ (not Geo-specific).
- Much of Geo’s integration (database replication, for instance) must be
configured with the application, typically by system administrators.
@@ -177,7 +177,7 @@ from [owasp.org](https://owasp.org/).
### What databases and application servers support the application?
-- PostgreSQL >= 9.6, Redis, Sidekiq, Unicorn.
+- PostgreSQL >= 11, Redis, Sidekiq, Puma.
### How will database connection strings, encryption keys, and other sensitive components be stored, accessed, and protected from unauthorized detection?
diff --git a/doc/administration/geo/replication/troubleshooting.md b/doc/administration/geo/replication/troubleshooting.md
index fae9705e935..293414a6e5e 100644
--- a/doc/administration/geo/replication/troubleshooting.md
+++ b/doc/administration/geo/replication/troubleshooting.md
@@ -497,6 +497,12 @@ to start again from scratch, there are a few steps that can help you:
gitlab-ctl start
```
+1. Refresh Foreign Data Wrapper tables
+
+ ```shell
+ gitlab-rake geo:db:refresh_foreign_tables
+ ```
+
## Fixing errors during a failover or when promoting a secondary to a primary node
The following are possible errors that might be encountered during failover or
@@ -538,6 +544,27 @@ or `gitlab-ctl promote-to-primary-node`, either:
bug](https://gitlab.com/gitlab-org/gitlab/-/merge_requests/22021) was
fixed.
+### Message: ``NoMethodError: undefined method `secondary?' for nil:NilClass``
+
+When [promoting a **secondary** node](../disaster_recovery/index.md#step-3-promoting-a-secondary-node),
+you might encounter the following error:
+
+```plaintext
+sudo gitlab-rake geo:set_secondary_as_primary
+
+rake aborted!
+NoMethodError: undefined method `secondary?' for nil:NilClass
+/opt/gitlab/embedded/service/gitlab-rails/ee/lib/tasks/geo.rake:232:in `block (3 levels) in <top (required)>'
+/opt/gitlab/embedded/service/gitlab-rails/ee/lib/tasks/geo.rake:221:in `block (2 levels) in <top (required)>'
+/opt/gitlab/embedded/bin/bundle:23:in `load'
+/opt/gitlab/embedded/bin/bundle:23:in `<main>'
+Tasks: TOP => geo:set_secondary_as_primary
+(See full trace by running task with --trace)
+```
+
+This command is intended to be executed on a secondary node only, and this error
+is displayed if you attempt to run this command on a primary node.
+
### Message: `sudo: gitlab-pg-ctl: command not found`
When
@@ -624,9 +651,9 @@ To check the configuration:
```
This password is normally set on the tracking database during
- [Step 3: Configure the tracking database on the secondary node](high_availability.md#step-3-configure-the-tracking-database-on-the-secondary-node),
+ [Step 3: Configure the tracking database on the secondary node](multiple_servers.md#step-3-configure-the-tracking-database-on-the-secondary-node),
and it is set on the app nodes during
- [Step 4: Configure the frontend application servers on the secondary node](high_availability.md#step-4-configure-the-frontend-application-servers-on-the-secondary-node).
+ [Step 4: Configure the frontend application servers on the secondary node](multiple_servers.md#step-4-configure-the-frontend-application-servers-on-the-secondary-node).
1. Check whether any tables are present with the following statement:
@@ -833,6 +860,8 @@ which Geo expects to have access to. It usually means, either:
- An unsupported replication method was used (for example, logical replication).
- The instructions to setup a [Geo database replication](database.md) were not followed correctly.
+- Your database connection details are incorrect, that is you have specified the wrong
+ user in your `/etc/gitlab/gitlab.rb` file.
A common source of confusion with **secondary** nodes is that it requires two separate
PostgreSQL instances:
@@ -854,7 +883,7 @@ Make sure you follow the [Geo database replication](database.md) instructions fo
### Geo database version (...) does not match latest migration (...)
-If you are using GitLab Omnibus installation, something might have failed during upgrade. You can:
+If you are using Omnibus GitLab installation, something might have failed during upgrade. You can:
- Run `sudo gitlab-ctl reconfigure`.
- Manually trigger the database migration by running: `sudo gitlab-rake geo:db:migrate` as root on the **secondary** node.
diff --git a/doc/administration/geo/replication/updating_the_geo_nodes.md b/doc/administration/geo/replication/updating_the_geo_nodes.md
index df66b1b36ec..fa1576e19eb 100644
--- a/doc/administration/geo/replication/updating_the_geo_nodes.md
+++ b/doc/administration/geo/replication/updating_the_geo_nodes.md
@@ -11,6 +11,7 @@ Updating Geo nodes involves performing:
Depending on which version of Geo you are updating to/from, there may be
different steps.
+- [Updating to GitLab 12.9](version_specific_updates.md#updating-to-gitlab-129)
- [Updating to GitLab 12.7](version_specific_updates.md#updating-to-gitlab-127)
- [Updating to GitLab 12.2](version_specific_updates.md#updating-to-gitlab-122)
- [Updating to GitLab 12.1](version_specific_updates.md#updating-to-gitlab-121)
@@ -44,7 +45,7 @@ and all **secondary** nodes:
Now that the update process is complete, you may want to check whether
everything is working correctly:
-1. Run the Geo raketask on all nodes, everything should be green:
+1. Run the Geo Rake task on all nodes, everything should be green:
```shell
sudo gitlab-rake gitlab:geo:check
diff --git a/doc/administration/geo/replication/using_a_geo_server.md b/doc/administration/geo/replication/using_a_geo_server.md
index 0f55272f667..2fec2b2b59c 100644
--- a/doc/administration/geo/replication/using_a_geo_server.md
+++ b/doc/administration/geo/replication/using_a_geo_server.md
@@ -1,4 +1,4 @@
-[//]: # (Please update EE::GitLab::GeoGitAccess::GEO_SERVER_DOCS_URL if this file is moved)
+<!-- Please update EE::GitLab::GeoGitAccess::GEO_SERVER_DOCS_URL if this file is moved) -->
# Using a Geo Server **(PREMIUM ONLY)**
diff --git a/doc/administration/geo/replication/version_specific_updates.md b/doc/administration/geo/replication/version_specific_updates.md
index 81868d19c7f..db8bbddec3b 100644
--- a/doc/administration/geo/replication/version_specific_updates.md
+++ b/doc/administration/geo/replication/version_specific_updates.md
@@ -30,7 +30,7 @@ GitLab 12.2 includes the following minor PostgreSQL updates:
This update will occur even if major PostgreSQL updates are disabled.
-Before [refreshing Foreign Data Wrapper during a Geo HA upgrade](https://docs.gitlab.com/omnibus/update/README.html#run-post-deployment-migrations-and-checks),
+Before [refreshing Foreign Data Wrapper during a Geo upgrade](https://docs.gitlab.com/omnibus/update/README.html#run-post-deployment-migrations-and-checks),
restart the Geo tracking database:
```shell
@@ -100,8 +100,8 @@ authentication method.
postgresql['sql_user_password'] = '<md5_hash_of_your_password>'
# Every node that runs Unicorn or Sidekiq needs to have the database
- # password specified as below. If you have a high-availability setup, this
- # must be present in all application nodes.
+ # password specified as below.
+ # This must be present in all application nodes.
gitlab_rails['db_password'] = '<your_password_here>'
```
@@ -125,8 +125,8 @@ authentication method.
postgresql['sql_user_password'] = '<md5_hash_of_your_password>'
# Every node that runs Unicorn or Sidekiq needs to have the database
- # password specified as below. If you have a high-availability setup, this
- # must be present in all application nodes.
+ # password specified as below.
+ # This must be present in all application nodes.
gitlab_rails['db_password'] = '<your_password_here>'
# Enable Foreign Data Wrapper
diff --git a/doc/administration/git_annex.md b/doc/administration/git_annex.md
index 4ac70e7fac2..0d44ed9312c 100644
--- a/doc/administration/git_annex.md
+++ b/doc/administration/git_annex.md
@@ -5,11 +5,11 @@ disqus_identifier: 'https://docs.gitlab.com/ee/workflow/git_annex.html'
# Git annex
> **Warning:** GitLab has [completely
-removed][deprecate-annex-issue] in GitLab 9.0 (2017/03/22).
-Read through the [migration guide from git-annex to Git LFS][guide].
+removed](https://gitlab.com/gitlab-org/gitlab/issues/1648) in GitLab 9.0 (2017/03/22).
+Read through the [migration guide from git-annex to Git LFS](../topics/git/lfs/migrate_from_git_annex_to_git_lfs.md).
The biggest limitation of Git, compared to some older centralized version
-control systems, has been the maximum size of the repositories.
+control systems has been the maximum size of the repositories.
The general recommendation is to not have Git repositories larger than 1GB to
preserve performance. Although GitLab has no limit (some repositories in GitLab
@@ -21,10 +21,10 @@ larger organizations.
Videos, photos, audio, compiled binaries, and many other types of files are too
large. As a workaround, people keep artwork-in-progress in a Dropbox folder and
only check in the final result. This results in using outdated files, not
-having a complete history and increases the risk of losing work.
+having a complete history, and increases the risk of losing work.
This problem is solved in GitLab Enterprise Edition by integrating the
-[git-annex] application.
+[git-annex](https://git-annex.branchable.com/) application.
`git-annex` allows managing large binaries with Git without checking the
contents into Git.
@@ -39,7 +39,7 @@ configuration options required to enable it.
### Requirements
-`git-annex` needs to be installed both on the server and the client side.
+`git-annex` needs to be installed both on the server and the client-side.
For Debian-like systems (for example, Debian and Ubuntu) this can be achieved by running:
@@ -64,7 +64,7 @@ The Omnibus package will internally set the correct options in all locations.
gitlab_shell['git_annex_enabled'] = true
```
-1. Save the file and [reconfigure GitLab][] for the changes to take effect.
+1. Save the file and [reconfigure GitLab](restart_gitlab.md#omnibus-gitlab-reconfigure) for the changes to take effect.
### Configuration for installations from source
@@ -86,7 +86,7 @@ one is located in `config.yml` of GitLab Shell.
git_annex_enabled: true
```
-1. Save the files and [restart GitLab][] for the changes to take effect.
+1. Save the files and [restart GitLab](restart_gitlab.md#installations-from-source) for the changes to take effect.
## Using GitLab git-annex
@@ -186,17 +186,17 @@ access files of projects you have access to (developer, maintainer, or owner rol
## How it works
-Internally GitLab uses [GitLab Shell] to handle SSH access and this was a great
+Internally GitLab uses [GitLab Shell](https://gitlab.com/gitlab-org/gitlab-shell) to handle SSH access and this was a great
integration point for `git-annex`.
There is a setting in GitLab Shell so you can disable GitLab Annex support
if you want to.
## Troubleshooting tips
-Differences in version of `git-annex` on the GitLab server and on local machines
+Differences in the version of `git-annex` on the GitLab server and on local machines
can cause `git-annex` to raise unpredicted warnings and errors.
-Consult the [Annex upgrade page][annex-upgrade] for more information about
+Consult the [Annex upgrade page](https://git-annex.branchable.com/upgrades/) for more information about
the differences between versions. You can find out which version is installed
on your server by navigating to <https://pkgs.org/download/git-annex> and
searching for your distribution.
@@ -208,7 +208,7 @@ on how to go around the warnings.
This warning can appear on the initial `git annex sync --content` and is caused
by differences in `git-annex-shell`. You can read more about it
-[in this git-annex issue][issue].
+[in this git-annex issue](https://git-annex.branchable.com/forum/Error_from_git-annex-shell_on_creation_of_gcrypt_special_remote/).
One important thing to note is that despite the warning, the `sync` succeeds
and the files are pushed to the GitLab repository.
@@ -231,12 +231,3 @@ pull origin
ok
push origin
```
-
-[annex-upgrade]: https://git-annex.branchable.com/upgrades/
-[deprecate-annex-issue]: https://gitlab.com/gitlab-org/gitlab/issues/1648
-[git-annex]: https://git-annex.branchable.com/ "git-annex website"
-[gitlab shell]: https://gitlab.com/gitlab-org/gitlab-shell "GitLab Shell repository"
-[guide]: ../topics/git/lfs/migrate_from_git_annex_to_git_lfs.md
-[issue]: https://git-annex.branchable.com/forum/Error_from_git-annex-shell_on_creation_of_gcrypt_special_remote/ "git-annex issue"
-[reconfigure GitLab]: restart_gitlab.md#omnibus-gitlab-reconfigure
-[restart GitLab]: restart_gitlab.md#installations-from-source
diff --git a/doc/administration/gitaly/img/praefect_architecture_v12_10.png b/doc/administration/gitaly/img/praefect_architecture_v12_10.png
index 7b8f1138b23..024a12b0a5d 100644
--- a/doc/administration/gitaly/img/praefect_architecture_v12_10.png
+++ b/doc/administration/gitaly/img/praefect_architecture_v12_10.png
Binary files differ
diff --git a/doc/administration/gitaly/index.md b/doc/administration/gitaly/index.md
index 20b9f2104de..14b0a6bd450 100644
--- a/doc/administration/gitaly/index.md
+++ b/doc/administration/gitaly/index.md
@@ -10,6 +10,11 @@ On this page, *Gitaly server* refers to a standalone node that only runs Gitaly
and *Gitaly client* is a GitLab Rails app node that runs all other processes
except Gitaly.
+CAUTION: **Caution:**
+From GitLab 13.0, using NFS for Git repositories is deprecated. In GitLab 14.0,
+support for NFS for Git repositories is scheduled to be removed. Upgrade to
+[Gitaly Cluster](praefect.md) as soon as possible.
+
## Architecture
Here's a high-level architecture overview of how Gitaly is used.
@@ -69,7 +74,7 @@ The following list depicts what the network architecture of Gitaly is:
- A GitLab server can use one or more Gitaly servers.
- Gitaly addresses must be specified in such a way that they resolve
correctly for ALL Gitaly clients.
-- Gitaly clients are: Unicorn, Sidekiq, GitLab Workhorse,
+- Gitaly clients are: Puma/Unicorn, Sidekiq, GitLab Workhorse,
GitLab Shell, Elasticsearch Indexer, and Gitaly itself.
- A Gitaly server must be able to make RPC calls **to itself** via its own
`(Gitaly address, Gitaly token)` pair as specified in `/config/gitlab.yml`.
@@ -101,13 +106,16 @@ Omnibus GitLab or install it from source:
**_do not_** provide the `EXTERNAL_URL=` value.
- From source: [Install Gitaly](../../install/installation.md#install-gitaly).
-### 2. Client side token configuration
+### 2. Authentication
-Configure a token on the instance that runs the GitLab Rails application.
+Gitaly and GitLab use two shared secrets for authentication, one to authenticate gRPC requests
+to Gitaly, and a second for authentication callbacks from GitLab-Shell to the GitLab internal API.
**For Omnibus GitLab**
-1. On the client node(s), edit `/etc/gitlab/gitlab.rb`:
+To configure the Gitaly token:
+
+1. On the client server, edit `/etc/gitlab/gitlab.rb`:
```ruby
gitlab_rails['gitaly_token'] = 'abc123secret'
@@ -115,9 +123,42 @@ Configure a token on the instance that runs the GitLab Rails application.
1. Save the file and [reconfigure GitLab](../restart_gitlab.md#omnibus-gitlab-reconfigure).
+1. On the Gitaly server, edit `/etc/gitlab/gitlab.rb`:
+
+ ```ruby
+ gitaly['auth_token'] = 'abc123secret'
+ ```
+
+1. [Reconfigure GitLab](../restart_gitlab.md#omnibus-gitlab-reconfigure).
+
+There are two ways to configure the GitLab-Shell token:
+
+1. Copy `/etc/gitlab/gitlab-secrets.json` from the client server to same path on the Gitaly server.
+1. [Reconfigure GitLab](../restart_gitlab.md#omnibus-gitlab-reconfigure).
+
+**OR**
+
+1. On the client server, edit `/etc/gitlab/gitlab.rb`:
+
+ ```ruby
+ gitlab_shell['secret_token'] = 'shellsecret'
+ ```
+
+1. Save the file and [reconfigure GitLab](../restart_gitlab.md#omnibus-gitlab-reconfigure).
+
+1. On the Gitaly server, edit `/etc/gitlab/gitlab.rb`:
+
+ ```ruby
+ gitlab_shell['secret_token'] = 'shellsecret'
+ ```
+
+1. [Reconfigure GitLab](../restart_gitlab.md#omnibus-gitlab-reconfigure).
+
**For installations from source**
-1. On the client node(s), edit `/home/git/gitlab/config/gitlab.yml`:
+1. Copy `/home/git/gitlab/.gitlab_shell_secret` from the client server to the same path on the Gitaly
+server.
+1. On the client server, edit `/home/git/gitlab/config/gitlab.yml`:
```yaml
gitlab:
@@ -138,12 +179,6 @@ documentation on configuring Gitaly
authentication](https://gitlab.com/gitlab-org/gitaly/blob/master/doc/configuration/README.md#authentication)
.
-Gitaly must trigger some callbacks to GitLab via GitLab Shell. As a result,
-the GitLab Shell secret must be the same between the other GitLab servers and
-the Gitaly server. The easiest way to accomplish this is to copy `/etc/gitlab/gitlab-secrets.json`
-from an existing GitLab server to the Gitaly server. Without this shared secret,
-Git operations in GitLab will result in an API error.
-
**For Omnibus GitLab**
1. Edit `/etc/gitlab/gitlab.rb`:
@@ -160,17 +195,17 @@ Git operations in GitLab will result in an API error.
postgresql['enable'] = false
redis['enable'] = false
nginx['enable'] = false
- unicorn['enable'] = false
+ puma['enable'] = false
sidekiq['enable'] = false
gitlab_workhorse['enable'] = false
grafana['enable'] = false
- # If you run a seperate monitoring node you can disable these services
+ # If you run a separate monitoring node you can disable these services
alertmanager['enable'] = false
prometheus['enable'] = false
- # If you don't run a seperate monitoring node you can
- # Enable Prometheus access & disable these extra services
+ # If you don't run a separate monitoring node you can
+ # enable Prometheus access & disable these extra services.
# This makes Prometheus listen on all interfaces. You must use firewalls to restrict access to this address/port.
# prometheus['listen_address'] = '0.0.0.0:9090'
# prometheus['monitor_kubernetes'] = false
@@ -189,10 +224,6 @@ Git operations in GitLab will result in an API error.
# Don't forget to copy `/etc/gitlab/gitlab-secrets.json` from web server to Gitaly server.
gitlab_rails['internal_api_url'] = 'https://gitlab.example.com'
- # Authentication token to ensure only authorized servers can communicate with
- # Gitaly server
- gitaly['auth_token'] = 'abc123secret'
-
# Make Gitaly accept connections on all network interfaces. You must use
# firewalls to restrict access to this address/port.
# Comment out following line if you only want to support TLS connections
@@ -230,6 +261,8 @@ Git operations in GitLab will result in an API error.
```
1. Save the file and [reconfigure GitLab](../restart_gitlab.md#omnibus-gitlab-reconfigure).
+1. Run `sudo /opt/gitlab/embedded/service/gitlab-shell/bin/check -config /opt/gitlab/embedded/service/gitlab-shell/config.yml`
+to confirm that Gitaly can perform callbacks to the internal API.
**For installations from source**
@@ -271,7 +304,15 @@ Git operations in GitLab will result in an API error.
path = '/srv/gitlab/git-data/repositories'
```
+1. On each Gitaly server, edit `/home/git/gitlab-shell/config.yml`:
+
+ ```yaml
+ gitlab_url: https://gitlab.example.com
+ ```
+
1. Save the file and [restart GitLab](../restart_gitlab.md#installations-from-source).
+1. Run `sudo -u git /home/git/gitlab-shell/bin/check -config /home/git/gitlab-shell/config.yml`
+to confirm that Gitaly can perform callbacks to the internal API.
### 4. Converting clients to use the Gitaly server
@@ -302,11 +343,10 @@ can read and write to `/mnt/gitlab/storage2`.
'storage1' => { 'gitaly_address' => 'tcp://gitaly1.internal:8075' },
'storage2' => { 'gitaly_address' => 'tcp://gitaly2.internal:8075' },
})
-
- gitlab_rails['gitaly_token'] = 'abc123secret'
```
1. Save the file and [reconfigure GitLab](../restart_gitlab.md#omnibus-gitlab-reconfigure).
+1. Run `sudo gitlab-rake gitlab:gitaly:check` to confirm the client can connect to Gitaly.
1. Tail the logs to see the requests:
```shell
@@ -330,9 +370,6 @@ can read and write to `/mnt/gitlab/storage2`.
storage2:
gitaly_address: tcp://gitaly2.internal:8075
path: /some/dummy/path
-
- gitaly:
- token: 'abc123secret'
```
NOTE: **Note:**
@@ -341,6 +378,8 @@ can read and write to `/mnt/gitlab/storage2`.
[this issue](https://gitlab.com/gitlab-org/gitaly/issues/1282) is resolved.
1. Save the file and [restart GitLab](../restart_gitlab.md#installations-from-source).
+1. Run `sudo -u git -H bundle exec rake gitlab:gitaly:check RAILS_ENV=production` to
+confirm the client can connect to Gitaly.
1. Tail the logs to see the requests:
```shell
@@ -397,9 +436,9 @@ with a Gitaly instance that listens for secure connections you will need to use
scheme in the `gitaly_address` of the corresponding storage entry in the GitLab configuration.
You will need to bring your own certificates as this isn't provided automatically.
-The certificate to be used needs to be installed on all Gitaly nodes, and the
-certificate (or CA of certificate) on all
-client nodes that communicate with it following the procedure described in
+The certificate, or its certificate authority, must be installed on all Gitaly
+nodes (including the Gitaly node using the certificate) and on all client nodes
+that communicate with it following the procedure described in
[GitLab custom certificate configuration](https://docs.gitlab.com/omnibus/settings/ssl.html#install-custom-public-certificates).
NOTE: **Note**
@@ -430,17 +469,32 @@ To configure Gitaly with TLS:
})
gitlab_rails['gitaly_token'] = 'abc123secret'
+ gitlab_shell['secret_token'] = 'shellsecret'
```
1. Save the file and [reconfigure GitLab](../restart_gitlab.md#omnibus-gitlab-reconfigure) on client node(s).
+1. On the client node(s), copy the cert into the `/etc/gitlab/trusted-certs`:
+
+ ```shell
+ sudo cp cert.pem /etc/gitlab/trusted-certs/
+ ```
+
1. On the Gitaly server, create the `/etc/gitlab/ssl` directory and copy your key and certificate there:
```shell
sudo mkdir -p /etc/gitlab/ssl
sudo chmod 755 /etc/gitlab/ssl
sudo cp key.pem cert.pem /etc/gitlab/ssl/
+ sudo chmod 644 key.pem cert.pem
```
+1. Copy the cert to `/etc/gitlab/trusted-certs` so Gitaly will trust the cert when
+calling into itself:
+
+ ```shell
+ sudo cp /etc/gitlab/ssl/cert.pem /etc/gitlab/trusted-certs/
+ ```
+
1. On the Gitaly server node(s), edit `/etc/gitlab/gitlab.rb` and add:
<!--
@@ -463,6 +517,13 @@ To configure Gitaly with TLS:
**For installations from source**
+1. On the client node(s), add the cert to the system trusted certs:
+
+ ```shell
+ sudo cp cert.pem /usr/local/share/ca-certificates/gitaly.crt
+ sudo update-ca-certificates
+ ```
+
1. On the client node(s), edit `/home/git/gitlab/config/gitlab.yml` as follows:
```yaml
@@ -488,13 +549,32 @@ To configure Gitaly with TLS:
data will be stored in this folder. This will no longer be necessary after
[this issue](https://gitlab.com/gitlab-org/gitaly/issues/1282) is resolved.
-1. Save the file and [restart GitLab](../restart_gitlab.md#installations-from-source) on client node(s).
+1. Save the file and[restart GitLab](../restart_gitlab.md#installations-from-source)
+on client node(s).
+1. Copy `/home/git/gitlab/.gitlab_shell_secret` from the client server to the same
+path on the Gitaly server.
+1. On the Gitaly server, create or edit `/etc/default/gitlab` and add:
+
+ ```shell
+ export SSL_CERT_DIR=/etc/gitlab/ssl
+ ```
+
+1. Save the file.
1. Create the `/etc/gitlab/ssl` directory and copy your key and certificate there:
```shell
sudo mkdir -p /etc/gitlab/ssl
- sudo chmod 700 /etc/gitlab/ssl
+ sudo chmod 755 /etc/gitlab/ssl
sudo cp key.pem cert.pem /etc/gitlab/ssl/
+ sudo chmod 644 key.pem cert.pem
+ ```
+
+1. On the Gitaly server, add the cert to the system trusted certs so Gitaly will trust it
+when calling into itself:
+
+ ```shell
+ sudo cp cert.pem /usr/local/share/ca-certificates/gitaly.crt
+ sudo update-ca-certificates
```
1. On the Gitaly server node(s), edit `/home/git/gitaly/config.toml` and add:
@@ -781,7 +861,7 @@ two checks. The result of both of these checks is cached.
see if we can access filesystem underneath the Gitaly server
directly. If so, use the Rugged patch.
-To see if GitLab Rails can access the repo filesystem directly, we use
+To see if GitLab Rails can access the repository filesystem directly, we use
the following heuristic:
- Gitaly ensures that the filesystem has a metadata file in its root
diff --git a/doc/administration/gitaly/praefect.md b/doc/administration/gitaly/praefect.md
index 19a69dc348c..0ea83f0e090 100644
--- a/doc/administration/gitaly/praefect.md
+++ b/doc/administration/gitaly/praefect.md
@@ -1,35 +1,70 @@
-# Praefect: High Availability
+---
+type: reference
+---
-NOTE: **Note:** Praefect is an experimental service, and data loss is likely.
+# Gitaly Cluster
-Praefect is an optional reverse-proxy for [Gitaly](../index.md) to manage a
-cluster of Gitaly nodes for high availability. Initially, high availability
-be implemented through asynchronous replication. If a Gitaly node becomes
-unavailable, it will be possible to fail over to a warm Gitaly replica.
+[Gitaly](index.md), the service that provides storage for Git repositories, can
+be run in a clustered configuration to increase fault tolerance. In this
+configuration, every Git repository is stored on every Gitaly node in the
+cluster. Multiple clusters (or shards), can be configured.
-The first minimal version will support:
+NOTE: **Note:**
+Gitaly Clusters can be created using [GitLab Core](https://about.gitlab.com/pricing/#self-managed)
+and higher tiers. However, technical support is limited to GitLab Premium and Ultimate customers
+only. Not available in GitLab.com.
+
+Praefect is a router and transaction manager for Gitaly, and a required
+component for running a Gitaly Cluster.
+
+![Architecture diagram](img/praefect_architecture_v12_10.png)
+
+Using a Gitaly Cluster increase fault tolerance by:
+
+- Replicating write operations to warm standby Gitaly nodes.
+- Detecting Gitaly node failures.
+- Automatically routing Git requests to an available Gitaly node.
+
+The availability objectives for Gitaly clusters are:
+
+- **Recovery Point Objective (RPO):** Less than 1 minute.
+
+ Writes are replicated asynchronously. Any writes that have not been replicated
+ to the newly promoted primary are lost.
+
+ [Strong Consistency](https://gitlab.com/groups/gitlab-org/-/epics/1189) is
+ planned to improve this to "no loss".
+
+- **Recovery Time Objective (RTO):** Less than 10 seconds.
+
+ Outages are detected by a health checks run by each Praefect node every
+ second. Failover requires ten consecutive failed health checks on each
+ Praefect node.
+
+ [Faster outage detection](https://gitlab.com/gitlab-org/gitaly/-/issues/2608)
+ is planned to improve this to less than 1 second.
+
+The current version supports:
- Eventual consistency of the secondary replicas.
-- Automatic fail over from the primary to the secondary.
+- Automatic failover from the primary to the secondary.
- Reporting of possible data loss if replication queue is non empty.
+- Marking the newly promoted primary read only if possible data loss is
+ detected.
Follow the [HA Gitaly epic](https://gitlab.com/groups/gitlab-org/-/epics/1489)
-for updates and roadmap.
-
-## Requirements for configuring Gitaly for High Availability
+for improvements including
+[horizontally distributing reads](https://gitlab.com/groups/gitlab-org/-/epics/2013).
-NOTE: **Note:** this reference architecture is not highly available because
-Praefect is a single point of failure.
+## Requirements for configuring a Gitaly Cluster
-The minimal [alpha](https://about.gitlab.com/handbook/product/#alpha-beta-ga)
-reference architecture additionally requires:
+The minimum recommended configuration for a Gitaly Cluster requires:
-- 1 Praefect node
-- 1 PostgreSQL server (PostgreSQL 9.6 or newer)
+- 1 load balancer
+- 1 PostgreSQL server (PostgreSQL 11 or newer)
+- 3 Praefect nodes
- 3 Gitaly nodes (1 primary, 2 secondary)
-![Alpha architecture diagram](img/praefect_architecture_v12_10.png)
-
See the [design
document](https://gitlab.com/gitlab-org/gitaly/-/blob/master/doc/design_ha.md)
for implementation details.
@@ -43,6 +78,7 @@ package (highly recommended), follow the steps below:
1. [Configuring the Praefect database](#postgresql)
1. [Configuring the Praefect proxy/router](#praefect)
1. [Configuring each Gitaly node](#gitaly) (once for each Gitaly node)
+1. [Configure the load balancer](#load-balancer)
1. [Updating the GitLab server configuration](#gitlab)
1. [Configure Grafana](#grafana)
@@ -51,8 +87,8 @@ package (highly recommended), follow the steps below:
Before beginning, you should already have a working GitLab instance. [Learn how
to install GitLab](https://about.gitlab.com/install/).
-Provision a PostgreSQL server (PostgreSQL 9.6 or newer). Configuration through
-the GitLab Omnibus distribution is not yet supported. Follow this
+Provision a PostgreSQL server (PostgreSQL 11 or newer). Configuration through
+the Omnibus GitLab distribution is not yet supported. Follow this
[issue](https://gitlab.com/gitlab-org/gitaly/issues/2476) for updates.
Prepare all your new nodes by [installing
@@ -64,6 +100,7 @@ GitLab](https://about.gitlab.com/install/).
You will need the IP/host address for each node.
+1. `LOAD_BALANCER_SERVER_ADDRESS`: the IP/hots address of the load balancer
1. `POSTGRESQL_SERVER_ADDRESS`: the IP/host address of the PostgreSQL server
1. `PRAEFECT_HOST`: the IP/host address of the Praefect server
1. `GITALY_HOST`: the IP/host address of each Gitaly server
@@ -98,17 +135,19 @@ We will note in the instructions below where these secrets are required.
### PostgreSQL
-NOTE: **Note:** don't reuse the GitLab application database for the Praefect
-database.
+NOTE: **Note:** do not store the GitLab application database and the Praefect
+database on the same PostgreSQL server if using
+[Geo](../geo/replication/index.md). The replication state is internal to each instance
+of GitLab and should not be replicated.
To complete this section you will need:
- 1 Praefect node
-- 1 PostgreSQL server (PostgreSQL 9.6 or newer)
+- 1 PostgreSQL server (PostgreSQL 11 or newer)
- An SQL user with permissions to create databases
During this section, we will configure the PostgreSQL server, from the Praefect
-node, using `psql` which is installed by GitLab Omnibus.
+node, using `psql` which is installed by Omnibus GitLab.
1. SSH into the **Praefect** node and login as root:
@@ -173,7 +212,7 @@ application server, or a Gitaly node.
nginx['enable'] = false
prometheus['enable'] = false
grafana['enable'] = false
- unicorn['enable'] = false
+ puma['enable'] = false
sidekiq['enable'] = false
gitlab_workhorse['enable'] = false
gitaly['enable'] = false
@@ -189,16 +228,12 @@ application server, or a Gitaly node.
1. Configure **Praefect** to listen on network interfaces by editing
`/etc/gitlab/gitlab.rb`:
- You will need to replace:
-
- - `PRAEFECT_HOST` with the IP address or hostname of the Praefect node
-
```ruby
- praefect['listen_addr'] = 'PRAEFECT_HOST:2305'
+ praefect['listen_addr'] = '0.0.0.0:2305'
# Enable Prometheus metrics access to Praefect. You must use firewalls
# to restrict access to this address/port.
- praefect['prometheus_listen_addr'] = 'PRAEFECT_HOST:9652'
+ praefect['prometheus_listen_addr'] = '0.0.0.0:9652'
```
1. Configure a strong `auth_token` for **Praefect** by editing
@@ -245,9 +280,9 @@ application server, or a Gitaly node.
1. Configure the **Praefect** cluster to connect to each Gitaly node in the
cluster by editing `/etc/gitlab/gitlab.rb`.
- In the example below we have configured one cluster named `praefect`. This
- cluster has three Gitaly nodes `gitaly-1`, `gitaly-2`, and `gitaly-3`, which
- will be replicas of each other.
+ In the example below we have configured one virtual storage (or shard) named
+ `storage-1`. This cluster has three Gitaly nodes `gitaly-1`, `gitaly-2`, and
+ `gitaly-3`, which will be replicas of each other.
Replace `PRAEFECT_INTERNAL_TOKEN` with a strong secret, which will be used by
Praefect when communicating with Gitaly nodes in the cluster. This token is
@@ -260,13 +295,13 @@ application server, or a Gitaly node.
NOTE: **Note:** The `gitaly-1` node is currently denoted the primary. This
can be used to manually fail from one node to another. This will be removed
- in the future to allow for automatic failover.
+ in the [future](https://gitlab.com/gitlab-org/gitaly/-/issues/2634).
```ruby
# Name of storage hash must match storage name in git_data_dirs on GitLab
# server ('praefect') and in git_data_dirs on Gitaly nodes ('gitaly-1')
praefect['virtual_storages'] = {
- 'praefect' => {
+ 'storage-1' => {
'gitaly-1' => {
'address' => 'tcp://GITALY_HOST:8075',
'token' => 'PRAEFECT_INTERNAL_TOKEN',
@@ -284,75 +319,63 @@ application server, or a Gitaly node.
}
```
-1. Save the changes to `/etc/gitlab/gitlab.rb` and [reconfigure Praefect](../restart_gitlab.md#omnibus-gitlab-reconfigure):
+1. Enable the database replication queue:
- ```shell
- gitlab-ctl reconfigure
+ ```ruby
+ praefect['postgres_queue_enabled'] = true
```
-1. Verify that Praefect can reach PostgreSQL:
+ In the next release, database replication queue will be enabled by default.
+ See [issue #2615](https://gitlab.com/gitlab-org/gitaly/-/issues/2615).
- ```shell
- sudo -u git /opt/gitlab/embedded/bin/praefect -config /var/opt/gitlab/praefect/config.toml sql-ping
+1. Enable automatic failover by editing `/etc/gitlab/gitlab.rb`:
+
+ ```ruby
+ praefect['failover_enabled'] = true
+ praefect['failover_election_strategy'] = 'sql'
```
- If the check fails, make sure you have followed the steps correctly. If you
- edit `/etc/gitlab/gitlab.rb`, remember to run `sudo gitlab-ctl reconfigure`
- again before trying the `sql-ping` command.
+ When automatic failover is enabled, Praefect checks the health of internal
+ Gitaly nodes. If the primary has a certain amount of health checks fail, it
+ will promote one of the secondaries to be primary, and demote the primary to
+ be a secondary.
-#### Automatic failover
+ NOTE: **Note:** Database leader election will be [enabled by default in the
+ future](https://gitlab.com/gitlab-org/gitaly/-/issues/2682).
-When automatic failover is enabled, Praefect will do automatic detection of the health of internal Gitaly nodes. If the
-primary has a certain amount of health checks fail, it will decide to promote one of the secondaries to be primary, and
-demote the primary to be a secondary.
+ Caution, **automatic failover** favors availability over consistency and will
+ cause data loss if changes have not been replicated to the newly elected
+ primary. In the next release, leader election will [prefer to promote up to
+ date replicas](https://gitlab.com/gitlab-org/gitaly/-/issues/2642), and it
+ will be an option to favor consistency by marking [out-of-date repositories
+ read-only](https://gitlab.com/gitlab-org/gitaly/-/issues/2630).
-1. To enable automatic failover, edit `/etc/gitlab/gitlab.rb`:
+1. Save the changes to `/etc/gitlab/gitlab.rb` and [reconfigure
+ Praefect](../restart_gitlab.md#omnibus-gitlab-reconfigure):
- ```ruby
- # failover_enabled turns on automatic failover
- praefect['failover_enabled'] = true
- praefect['virtual_storages'] = {
- 'praefect' => {
- 'gitaly-1' => {
- 'address' => 'tcp://GITALY_HOST:8075',
- 'token' => 'PRAEFECT_INTERNAL_TOKEN',
- 'primary' => true
- },
- 'gitaly-2' => {
- 'address' => 'tcp://GITALY_HOST:8075',
- 'token' => 'PRAEFECT_INTERNAL_TOKEN'
- },
- 'gitaly-3' => {
- 'address' => 'tcp://GITALY_HOST:8075',
- 'token' => 'PRAEFECT_INTERNAL_TOKEN'
- }
- }
- }
+ ```shell
+ gitlab-ctl reconfigure
```
-1. Save the file and [reconfigure GitLab](../restart_gitlab.md#omnibus-gitlab-reconfigure).
+1. To ensure that Praefect [has updated its Prometheus listen
+ address](https://gitlab.com/gitlab-org/gitaly/-/issues/2734), [restart
+ Gitaly](../restart_gitlab.md#omnibus-gitlab-restart):
-Below is the picture when Praefect starts up with the config.toml above:
+ ```shell
+ gitlab-ctl restart praefect
+ ```
-```mermaid
-graph TD
- A[Praefect] -->|Mutator RPC| B(internal_storage_0)
- B --> |Replication|C[internal_storage_1]
-```
+1. Verify that Praefect can reach PostgreSQL:
-Let's say suddenly `internal_storage_0` goes down. Praefect will detect this and
-automatically switch over to `internal_storage_1`, and `internal_storage_0` will serve as a secondary:
+ ```shell
+ sudo -u git /opt/gitlab/embedded/bin/praefect -config /var/opt/gitlab/praefect/config.toml sql-ping
+ ```
-```mermaid
-graph TD
- A[Praefect] -->|Mutator RPC| B(internal_storage_1)
- B --> |Replication|C[internal_storage_0]
-```
+ If the check fails, make sure you have followed the steps correctly. If you
+ edit `/etc/gitlab/gitlab.rb`, remember to run `sudo gitlab-ctl reconfigure`
+ again before trying the `sql-ping` command.
-NOTE: **Note:**: Currently this feature is supported for setups that only have 1 Praefect instance. Praefect instances running,
-for example behind a load balancer, `failover_enabled` should be disabled. The reason is The reason is because there
-is no coordination that currently happens across different Praefect instances, so there could be a situation where
-two Praefect instances think two different Gitaly nodes are the primary.
+**The steps above must be completed for each Praefect node!**
### Gitaly
@@ -365,7 +388,7 @@ To complete this section you will need:
These should be dedicated nodes, do not run other services on these nodes.
Every Gitaly server assigned to the Praefect cluster needs to be configured. The
-configuration is the same as a normal [standalone Gitaly server](../index.md),
+configuration is the same as a normal [standalone Gitaly server](index.md),
except:
- the storage names are exposed to Praefect, not GitLab
@@ -403,7 +426,7 @@ documentation](index.md#3-gitaly-server-configuration).
nginx['enable'] = false
prometheus['enable'] = false
grafana['enable'] = false
- unicorn['enable'] = false
+ puma['enable'] = false
sidekiq['enable'] = false
gitlab_workhorse['enable'] = false
prometheus_monitoring['enable'] = false
@@ -419,18 +442,14 @@ documentation](index.md#3-gitaly-server-configuration).
1. Configure **Gitaly** to listen on network interfaces by editing
`/etc/gitlab/gitlab.rb`:
- You will need to replace:
-
- - `GITALY_HOST` with the IP address or hostname of the Gitaly node
-
```ruby
# Make Gitaly accept connections on all network interfaces.
# Use firewalls to restrict access to this address/port.
- gitaly['listen_addr'] = 'GITALY_HOST:8075'
+ gitaly['listen_addr'] = '0.0.0.0:8075'
# Enable Prometheus metrics access to Gitaly. You must use firewalls
# to restrict access to this address/port.
- gitaly['prometheus_listen_addr'] = 'GITALY_HOST:9236'
+ gitaly['prometheus_listen_addr'] = '0.0.0.0:9236'
```
1. Configure a strong `auth_token` for **Gitaly** by editing
@@ -445,7 +464,7 @@ documentation](index.md#3-gitaly-server-configuration).
1. Configure the GitLab Shell `secret_token`, and `internal_api_url` which are
needed for `git push` operations.
- If you have already configured [Gitaly on its own server](../index.md)
+ If you have already configured [Gitaly on its own server](index.md)
```ruby
gitlab_shell['secret_token'] = 'GITLAB_SHELL_SECRET_TOKEN'
@@ -458,12 +477,12 @@ documentation](index.md#3-gitaly-server-configuration).
1. Configure the storage location for Git data by setting `git_data_dirs` in
`/etc/gitlab/gitlab.rb`. Each Gitaly node should have a unique storage name
- (eg `gitaly-1`).
+ (such as `gitaly-1`).
Instead of configuring `git_data_dirs` uniquely for each Gitaly node, it is
often easier to have include the configuration for all Gitaly nodes on every
Gitaly node. This is supported because the Praefect `virtual_storages`
- configuration maps each storage name (eg `gitaly-1`) to a specific node, and
+ configuration maps each storage name (such as `gitaly-1`) to a specific node, and
requests are routed accordingly. This means every Gitaly node in your fleet
can share the same configuration.
@@ -484,13 +503,16 @@ documentation](index.md#3-gitaly-server-configuration).
})
```
-1. Save the changes to `/etc/gitlab/gitlab.rb` and [reconfigure Gitaly](../restart_gitlab.md#omnibus-gitlab-reconfigure):
+1. Save the changes to `/etc/gitlab/gitlab.rb` and [reconfigure
+ Gitaly](../restart_gitlab.md#omnibus-gitlab-reconfigure):
```shell
gitlab-ctl reconfigure
```
-1. To ensure that Gitaly [has updated its Prometheus listen address](https://gitlab.com/gitlab-org/gitaly/-/issues/2521), [restart Gitaly](../restart_gitlab.md#omnibus-gitlab-restart):
+1. To ensure that Gitaly [has updated its Prometheus listen
+ address](https://gitlab.com/gitlab-org/gitaly/-/issues/2734), [restart
+ Gitaly](../restart_gitlab.md#omnibus-gitlab-restart):
```shell
gitlab-ctl restart gitaly
@@ -508,37 +530,23 @@ config.
sudo /opt/gitlab/embedded/bin/praefect -config /var/opt/gitlab/praefect/config.toml dial-nodes
```
-1. Enable automatic failover by editing `/etc/gitlab/gitlab.rb`:
+### Load Balancer
- ```ruby
- praefect['failover_enabled'] = true
- ```
-
- When automatic failover is enabled, Praefect checks the health of internal
- Gitaly nodes. If the primary has a certain amount of health checks fail, it
- will promote one of the secondaries to be primary, and demote the primary to
- be a secondary.
+In a highly available Gitaly configuration, a load balancer is needed to route
+internal traffic from the GitLab application to the Praefect nodes. The
+specifics on which load balancer to use or the exact configuration is beyond the
+scope of the GitLab documentation.
- Manual failover is possible by updating `praefect['virtual_storages']` and
- nominating a new primary node.
+We hope that if you’re managing HA systems like GitLab, you have a load balancer
+of choice already. Some examples include [HAProxy](https://www.haproxy.org/)
+(open-source), [Google Internal Load Balancer](https://cloud.google.com/load-balancing/docs/internal/),
+[AWS Elastic Load Balancer](https://aws.amazon.com/elasticloadbalancing/), F5
+Big-IP LTM, and Citrix Net Scaler. This documentation will outline what ports
+and protocols you need configure.
-1. By default, Praefect will nominate a primary Gitaly node for each
- shard and store the state of the primary in local memory. This state
- does not persist across restarts and will cause a split brain
- if multiple Praefect nodes are used for redundancy.
-
- To avoid this limitation, enable the SQL election strategy:
-
- ```ruby
- praefect['failover_election_strategy'] = 'sql'
- ```
-
-1. Save the changes to `/etc/gitlab/gitlab.rb` and [reconfigure
- Praefect](../restart_gitlab.md#omnibus-gitlab-reconfigure):
-
- ```shell
- gitlab-ctl reconfigure
- ```
+| LB Port | Backend Port | Protocol |
+|---------|--------------|----------|
+| 2305 | 2305 | TCP |
### GitLab
@@ -555,7 +563,7 @@ Particular attention should be shown to:
- the storage name added to `git_data_dirs` in this section must match the
storage name under `praefect['virtual_storages']` on the Praefect node. This
was set in the [Praefect](#praefect) section of this guide. This document uses
- `praefect` as the Praefect storage name.
+ `storage-1` as the Praefect storage name.
1. SSH into the **GitLab** node and login as root:
@@ -563,12 +571,23 @@ Particular attention should be shown to:
sudo -i
```
+1. Configure the `external_url` so that files could be served by GitLab
+ by proper endpoint access by editing `/etc/gitlab/gitlab.rb`:
+
+ You will need to replace `GITLAB_SERVER_URL` with the real external facing
+ URL on which current GitLab instance is serving:
+
+ ```ruby
+ external_url 'GITLAB_SERVER_URL'
+ ```
+
1. Add the Praefect cluster as a storage location by editing
`/etc/gitlab/gitlab.rb`.
You will need to replace:
- - `PRAEFECT_HOST` with the IP address or hostname of the Praefect node
+ - `LOAD_BALANCER_SERVER_ADDRESS` with the IP address or hostname of the load
+ balancer.
- `GITLAB_HOST` with the IP address or hostname of the GitLab server
- `PRAEFECT_EXTERNAL_TOKEN` with the real secret
@@ -577,18 +596,18 @@ Particular attention should be shown to:
"default" => {
"gitaly_address" => "tcp://GITLAB_HOST:8075"
},
- "praefect" => {
- "gitaly_address" => "tcp://PRAEFECT_HOST:2305",
+ "storage-1" => {
+ "gitaly_address" => "tcp://LOAD_BALANCER_SERVER_ADDRESS:2305",
"gitaly_token" => 'PRAEFECT_EXTERNAL_TOKEN'
}
})
```
-1. Allow Gitaly to listen on a tcp port by editing
+1. Allow Gitaly to listen on a TCP port by editing
`/etc/gitlab/gitlab.rb`
```ruby
- gitaly['listen_addr'] = 'GITLAB_HOST:8075'
+ gitaly['listen_addr'] = '0.0.0.0:8075'
```
1. Configure the `gitlab_shell['secret_token']` so that callbacks from Gitaly
@@ -601,16 +620,6 @@ Particular attention should be shown to:
gitlab_shell['secret_token'] = 'GITLAB_SHELL_SECRET_TOKEN'
```
-1. Configure the `external_url` so that files could be served by GitLab
- by proper endpoint access by editing `/etc/gitlab/gitlab.rb`:
-
- You will need to replace `GITLAB_SERVER_URL` with the real external facing URL on which
- current GitLab instance is serving:
-
- ```ruby
- external_url 'GITLAB_SERVER_URL'
- ```
-
1. Add Prometheus monitoring settings by editing `/etc/gitlab/gitlab.rb`.
You will need to replace:
@@ -624,7 +633,9 @@ Particular attention should be shown to:
'job_name' => 'praefect',
'static_configs' => [
'targets' => [
- 'PRAEFECT_HOST:9652' # praefect
+ 'PRAEFECT_HOST:9652', # praefect-1
+ 'PRAEFECT_HOST:9652', # praefect-2
+ 'PRAEFECT_HOST:9652', # praefect-3
]
]
},
@@ -647,6 +658,14 @@ Particular attention should be shown to:
gitlab-ctl reconfigure
```
+1. To ensure that Gitaly [has updated its Prometheus listen
+ address](https://gitlab.com/gitlab-org/gitaly/-/issues/2734), [restart
+ Gitaly](../restart_gitlab.md#omnibus-gitlab-restart):
+
+ ```shell
+ gitlab-ctl restart gitaly
+ ```
+
1. Verify each `gitlab-shell` on each Gitaly instance can reach GitLab. On each Gitaly instance run:
```shell
@@ -725,6 +744,13 @@ Praefect regularly checks the health of each backend Gitaly node. This
information can be used to automatically failover to a new primary node if the
current primary node is found to be unhealthy.
+- **PostgreSQL (recommended):** Enabled by setting
+ `praefect['failover_election_strategy'] = sql`. This configuration
+ option will allow multiple Praefect nodes to coordinate via the
+ PostgreSQL database to elect a primary Gitaly node. This configuration
+ will cause Praefect nodes to elect a new primary, monitor its health,
+ and elect a new primary if the current one has not been reachable in
+ 10 seconds by a majority of the Praefect nodes.
- **Manual:** Automatic failover is disabled. The primary node can be
reconfigured in `/etc/gitlab/gitlab.rb` on the Praefect node. Modify the
`praefect['virtual_storages']` field by moving the `primary = true` to promote
@@ -735,13 +761,6 @@ current primary node is found to be unhealthy.
checks fail for the current primary backend Gitaly node, and new primary will
be elected. **Do not use with multiple Praefect nodes!** Using with multiple
Praefect nodes is likely to result in a split brain.
-- **PostgreSQL:** Enabled by setting
- `praefect['failover_election_strategy'] = sql`. This configuration
- option will allow multiple Praefect nodes to coordinate via the
- PostgreSQL database to elect a primary Gitaly node. This configuration
- will cause Praefect nodes to elect a new primary, monitor its health,
- and elect a new primary if the current one has not been reachable in
- 10 seconds by a majority of the Praefect nodes.
NOTE: **Note:**: Praefect does not yet account for replication lag on
the secondaries during the election process, so data loss can occur
@@ -753,13 +772,13 @@ strategy in the future.
## Identifying Impact of a Primary Node Failure
-When a primary Gitaly node fails, there is a chance of dataloss. Dataloss can occur if there were outstanding replication jobs the secondaries did not manage to process before the failure. The Praefect `dataloss` subcommand helps identify these cases by counting the number of dead replication jobs for each repository within a given timeframe.
+When a primary Gitaly node fails, there is a chance of data loss. Data loss can occur if there were outstanding replication jobs the secondaries did not manage to process before the failure. The Praefect `dataloss` sub-command helps identify these cases by counting the number of dead replication jobs for each repository within a given time frame.
```shell
sudo /opt/gitlab/embedded/bin/praefect -config /var/opt/gitlab/praefect/config.toml dataloss -from <rfc3339-time> -to <rfc3339-time>
```
-If the timeframe is not specified, dead replication jobs from the last six hours are counted:
+If the time frame is not specified, dead replication jobs from the last six hours are counted:
```shell
sudo /opt/gitlab/embedded/bin/praefect -config /var/opt/gitlab/praefect/config.toml dataloss
@@ -770,19 +789,27 @@ example/repository-2: 4 jobs
example/repository-3: 2 jobs
```
-To specify a timeframe in UTC, run:
+To specify a time frame in UTC, run:
```shell
sudo /opt/gitlab/embedded/bin/praefect -config /var/opt/gitlab/praefect/config.toml dataloss -from 2020-01-02T00:00:00+00:00 -to 2020-01-02T00:02:00+00:00
```
+### Checking repository checksums
+
+To check a project's checksums across all nodes, the Praefect replicas Rake task can be used:
+
+```shell
+sudo gitlab-rake "gitlab:praefect:replicas[project_id]"
+```
+
## Backend Node Recovery
When a Praefect backend node fails and is no longer able to
replicate changes, the backend node will start to drift from the primary. If
that node eventually recovers, it will need to be reconciled with the current
primary. The primary node is considered the single source of truth for the
-state of a shard. The Praefect `reconcile` subcommand allows for the manual
+state of a shard. The Praefect `reconcile` sub-command allows for the manual
reconciliation between a backend node and the current primary.
Run the following command on the Praefect server after all placeholders
diff --git a/doc/administration/gitaly/reference.md b/doc/administration/gitaly/reference.md
index 6b6919247fe..e718d8953ca 100644
--- a/doc/administration/gitaly/reference.md
+++ b/doc/administration/gitaly/reference.md
@@ -149,7 +149,7 @@ before and after. If the hit ratio does not improve, the higher limit is
probably not making a meaningful difference. Here is an example
Prometheus query to see the hit rate:
-```text
+```plaintext
sum(rate(gitaly_catfile_cache_total{type="hit"}[5m])) / sum(rate(gitaly_catfile_cache_total{type=~"(hit)|(miss)"}[5m]))
```
@@ -258,7 +258,7 @@ You can adjust the `concurrency` of each RPC endpoint.
| ---- | ---- | -------- | ----------- |
| `concurrency` | array | yes | An array of RPC endpoints. |
| `rpc` | string | no | The name of the RPC endpoint (`/gitaly.RepositoryService/GarbageCollect`). |
-| `max_per_repo` | integer | no | Concurrency per RPC per repo. |
+| `max_per_repo` | integer | no | Concurrency per RPC per repository. |
Example:
diff --git a/doc/administration/high_availability/README.md b/doc/administration/high_availability/README.md
index 55ec3b8d6c4..d36b029cbb3 100644
--- a/doc/administration/high_availability/README.md
+++ b/doc/administration/high_availability/README.md
@@ -1,7 +1,7 @@
---
-type: reference, concepts
+redirect_to: ../reference_architectures/index.md
---
-# High Availability
+# Reference Architectures
-This content has been moved to the [availability page](../availability/index.md).
+This document was moved to [another location](../reference_architectures/index.md).
diff --git a/doc/administration/high_availability/consul.md b/doc/administration/high_availability/consul.md
index 6762a81f671..1f22c46a0ad 100644
--- a/doc/administration/high_availability/consul.md
+++ b/doc/administration/high_availability/consul.md
@@ -4,14 +4,17 @@ type: reference
# Working with the bundled Consul service **(PREMIUM ONLY)**
-As part of its High Availability stack, GitLab Premium includes a bundled version of [Consul](https://www.consul.io/) that can be managed through `/etc/gitlab/gitlab.rb`.
+As part of its High Availability stack, GitLab Premium includes a bundled version of [Consul](https://www.consul.io/) that can be managed through `/etc/gitlab/gitlab.rb`. Consul is a service networking solution. When it comes to [GitLab Architecture](../../development/architecture.md), Consul utilization is supported for configuring:
+
+1. [Monitoring in Scaled and Highly Available environments](monitoring_node.md)
+1. [PostgreSQL High Availability with Omnibus](database.md#high-availability-with-omnibus-gitlab-premium-only)
A Consul cluster consists of multiple server agents, as well as client agents that run on other nodes which need to talk to the Consul cluster.
## Prerequisites
First, make sure to [download/install](https://about.gitlab.com/install/)
-GitLab Omnibus **on each node**.
+Omnibus GitLab **on each node**.
Choose an installation method, then make sure you complete steps:
@@ -107,7 +110,7 @@ For larger clusters, it is possible to restart multiple agents at a time. See th
Nodes running GitLab-bundled Consul should be:
-- Members of a healthy cluster prior to upgrading the GitLab Omnibus package.
+- Members of a healthy cluster prior to upgrading the Omnibus GitLab package.
- Upgraded one node at a time.
NOTE: **NOTE:**
diff --git a/doc/administration/high_availability/database.md b/doc/administration/high_availability/database.md
index f06870be93c..6f1873af993 100644
--- a/doc/administration/high_availability/database.md
+++ b/doc/administration/high_availability/database.md
@@ -24,16 +24,16 @@ If you use a cloud-managed service, or provide your own PostgreSQL:
## PostgreSQL in a Scaled and Highly Available Environment
-This section is relevant for [Scalable and Highly Available Setups](../scaling/index.md).
+This section is relevant for [Scalable and Highly Available Setups](../reference_architectures/index.md).
### Provide your own PostgreSQL instance **(CORE ONLY)**
If you want to use your own deployed PostgreSQL instance(s),
see [Provide your own PostgreSQL instance](#provide-your-own-postgresql-instance-core-only)
-for more details. However, you can use the GitLab Omnibus package to easily
+for more details. However, you can use the Omnibus GitLab package to easily
deploy the bundled PostgreSQL.
-### Standalone PostgreSQL using GitLab Omnibus **(CORE ONLY)**
+### Standalone PostgreSQL using Omnibus GitLab **(CORE ONLY)**
1. SSH into the PostgreSQL server.
1. [Download/install](https://about.gitlab.com/install/) the Omnibus GitLab
@@ -92,7 +92,7 @@ deploy the bundled PostgreSQL.
Advanced configuration options are supported and can be added if
needed.
-### High Availability with GitLab Omnibus **(PREMIUM ONLY)**
+### High Availability with Omnibus GitLab **(PREMIUM ONLY)**
> Important notes:
>
@@ -125,7 +125,7 @@ otherwise the networks will become a single point of failure.
#### Architecture
-![PG HA Architecture](img/pg_ha_architecture.png)
+![PostgreSQL HA Architecture](img/pg_ha_architecture.png)
Database nodes run two services with PostgreSQL:
@@ -271,7 +271,7 @@ Few notes on the service itself:
#### Installing Omnibus GitLab
First, make sure to [download/install](https://about.gitlab.com/install/)
-GitLab Omnibus **on each node**.
+Omnibus GitLab **on each node**.
Make sure you install the necessary dependencies from step 1,
add GitLab package repository from step 2.
@@ -969,7 +969,7 @@ repmgr['trust_auth_cidr_addresses'] = %w(192.168.1.44/32 db2.example.com)
##### MD5 Authentication
If you are running on an untrusted network, repmgr can use md5 authentication
-with a [`.pgpass` file](https://www.postgresql.org/docs/9.6/libpq-pgpass.html)
+with a [`.pgpass` file](https://www.postgresql.org/docs/11/libpq-pgpass.html)
to authenticate.
You can specify by IP address, FQDN, or by subnet, using the same format as in
@@ -1091,7 +1091,7 @@ If you're running into an issue with a component not outlined here, be sure to c
## Configure using Omnibus
-**Note**: We recommend that you follow the instructions here for a full [PostgreSQL cluster](#high-availability-with-gitlab-omnibus-premium-only).
+**Note**: We recommend that you follow the instructions here for a full [PostgreSQL cluster](#high-availability-with-omnibus-gitlab-premium-only).
If you are reading this section due to an old bookmark, you can find that old documentation [in the repository](https://gitlab.com/gitlab-org/gitlab/blob/v10.1.4/doc/administration/high_availability/database.md#configure-using-omnibus).
Read more on high-availability configuration:
diff --git a/doc/administration/high_availability/gitaly.md b/doc/administration/high_availability/gitaly.md
index 5d66d3c5c94..2e6bcabeb06 100644
--- a/doc/administration/high_availability/gitaly.md
+++ b/doc/administration/high_availability/gitaly.md
@@ -4,14 +4,10 @@ type: reference
# Configuring Gitaly for Scaled and High Availability
-Gitaly does not yet support full high availability. However, Gitaly is quite
-stable and is in use on GitLab.com. Scaled and highly available GitLab environments
-should consider using Gitaly on a separate node.
+A [Gitaly Cluster](../gitaly/praefect.md) can be used to increase the fault
+tolerance of Gitaly in high availability configurations.
-See the [Gitaly HA Epic](https://gitlab.com/groups/gitlab-org/-/epics/289) to
-track plans and progress toward high availability support.
-
-This document is relevant for [Scalable and Highly Available Setups](../scaling/index.md).
+This document is relevant for [scalable and highly available setups](../reference_architectures/index.md).
## Running Gitaly on its own server
@@ -19,7 +15,7 @@ See [Running Gitaly on its own server](../gitaly/index.md#running-gitaly-on-its-
in Gitaly documentation.
Continue configuration of other components by going back to the
-[High Availability](../availability/index.md#gitlab-components-and-configuration-instructions) page.
+[reference architecture](../reference_architectures/index.md#configure-gitlab-to-scale) page.
## Enable Monitoring
diff --git a/doc/administration/high_availability/gitlab.md b/doc/administration/high_availability/gitlab.md
index c9c425d366b..cdafdbc4954 100644
--- a/doc/administration/high_availability/gitlab.md
+++ b/doc/administration/high_availability/gitlab.md
@@ -2,7 +2,9 @@
type: reference
---
-# Configuring GitLab for Scaling and High Availability
+# Configuring GitLab application (Rails)
+
+This section describes how to configure the GitLab application (Rails) component.
NOTE: **Note:** There is some additional configuration near the bottom for
additional GitLab application servers. It's important to read and understand
@@ -34,7 +36,7 @@ is recommended over [NFS](nfs.md) wherever possible for improved performance.
mkdir -p /var/opt/gitlab/.ssh /var/opt/gitlab/gitlab-rails/uploads /var/opt/gitlab/gitlab-rails/shared /var/opt/gitlab/gitlab-ci/builds /var/opt/gitlab/git-data
```
-1. Download/install GitLab Omnibus using **steps 1 and 2** from
+1. Download/install Omnibus GitLab using **steps 1 and 2** from
[GitLab downloads](https://about.gitlab.com/install/). Do not complete other
steps on the download page.
1. Create/edit `/etc/gitlab/gitlab.rb` and use the following configuration.
@@ -79,8 +81,8 @@ is recommended over [NFS](nfs.md) wherever possible for improved performance.
NOTE: **Note:** To maintain uniformity of links across HA clusters, the `external_url`
on the first application server as well as the additional application
- servers should point to the external url that users will use to access GitLab.
- In a typical HA setup, this will be the url of the load balancer which will
+ servers should point to the external URL that users will use to access GitLab.
+ In a typical HA setup, this will be the URL of the load balancer which will
route traffic to all GitLab application servers in the HA cluster.
NOTE: **Note:** When you specify `https` in the `external_url`, as in the example
@@ -131,6 +133,9 @@ need some extra configuration.
1. Run `sudo gitlab-ctl reconfigure` to compile the configuration.
+NOTE: **Note:** You will need to restart the GitLab applications nodes after an update has occurred and database
+migrations performed.
+
## Enable Monitoring
> [Introduced](https://gitlab.com/gitlab-org/omnibus-gitlab/issues/3786) in GitLab 12.0.
@@ -157,7 +162,7 @@ If you enable Monitoring, it must be enabled on **all** GitLab servers.
node_exporter['listen_address'] = '0.0.0.0:9100'
gitlab_workhorse['prometheus_listen_addr'] = '0.0.0.0:9229'
sidekiq['listen_address'] = "0.0.0.0"
- unicorn['listen'] = '0.0.0.0'
+ puma['listen'] = '0.0.0.0'
# Add the monitoring node's IP address to the monitoring whitelist and allow it to
# scrape the NGINX metrics. Replace placeholder `monitoring.gitlab.example.com` with
@@ -169,9 +174,11 @@ If you enable Monitoring, it must be enabled on **all** GitLab servers.
1. Run `sudo gitlab-ctl reconfigure` to compile the configuration.
CAUTION: **Warning:**
- After changing `unicorn['listen']` in `gitlab.rb`, and running `sudo gitlab-ctl reconfigure`,
- it can take an extended period of time for Unicorn to complete reloading after receiving a `HUP`.
- For more information, see the [issue](https://gitlab.com/gitlab-org/omnibus-gitlab/issues/4401).
+ If running Unicorn, after changing `unicorn['listen']` in `gitlab.rb`, and
+ running `sudo gitlab-ctl reconfigure`, it can take an extended period of time
+ for Unicorn to complete reloading after receiving a `HUP`. For more
+ information, see the
+ [issue](https://gitlab.com/gitlab-org/omnibus-gitlab/issues/4401).
## Troubleshooting
diff --git a/doc/administration/high_availability/load_balancer.md b/doc/administration/high_availability/load_balancer.md
index 43a7c442d8c..beeb57a0e32 100644
--- a/doc/administration/high_availability/load_balancer.md
+++ b/doc/administration/high_availability/load_balancer.md
@@ -66,7 +66,7 @@ for details on managing SSL certificates and configuring NGINX.
| 443 | 443 | TCP or HTTPS (*1*) (*2*) |
| 22 | 22 | TCP |
-- (*1*): [Web terminal](../../ci/environments.md#web-terminals) support requires
+- (*1*): [Web terminal](../../ci/environments/index.md#web-terminals) support requires
your load balancer to correctly handle WebSocket connections. When using
HTTP or HTTPS proxying, this means your load balancer must be configured
to pass through the `Connection` and `Upgrade` hop-by-hop headers. See the
diff --git a/doc/administration/high_availability/monitoring_node.md b/doc/administration/high_availability/monitoring_node.md
index 79e583c5fcb..409a4dfecb9 100644
--- a/doc/administration/high_availability/monitoring_node.md
+++ b/doc/administration/high_availability/monitoring_node.md
@@ -8,10 +8,10 @@ type: reference
You can configure a Prometheus node to monitor GitLab.
-## Standalone Monitoring node using GitLab Omnibus
+## Standalone Monitoring node using Omnibus GitLab
-The GitLab Omnibus package can be used to configure a standalone Monitoring node running [Prometheus](../monitoring/prometheus/index.md) and [Grafana](../monitoring/performance/grafana_configuration.md).
-The monitoring node is not highly available. See [Scaling and High Availability](../scaling/index.md)
+The Omnibus GitLab package can be used to configure a standalone Monitoring node running [Prometheus](../monitoring/prometheus/index.md) and [Grafana](../monitoring/performance/grafana_configuration.md).
+The monitoring node is not highly available. See [Scaling and High Availability](../reference_architectures/index.md)
for an overview of GitLab scaling and high availability options.
The steps below are the minimum necessary to configure a Monitoring node running Prometheus and Grafana with
@@ -64,7 +64,7 @@ Omnibus:
redis['enable'] = false
redis_exporter['enable'] = false
sidekiq['enable'] = false
- unicorn['enable'] = false
+ puma['enable'] = false
node_exporter['enable'] = false
gitlab_exporter['enable'] = false
```
diff --git a/doc/administration/high_availability/nfs.md b/doc/administration/high_availability/nfs.md
index 66f2986ab2a..d2b8cf65b35 100644
--- a/doc/administration/high_availability/nfs.md
+++ b/doc/administration/high_availability/nfs.md
@@ -7,14 +7,16 @@ type: reference
You can view information and options set for each of the mounted NFS file
systems by running `nfsstat -m` and `cat /etc/fstab`.
+CAUTION: **Caution:**
+From GitLab 13.0, using NFS for Git repositories is deprecated. In GitLab 14.0,
+support for NFS for Git repositories is scheduled to be removed. Upgrade to
+[Gitaly Cluster](../gitaly/praefect.md) as soon as possible.
+
NOTE: **Note:** Filesystem performance has a big impact on overall GitLab
performance, especially for actions that read or write to Git repositories. See
[Filesystem Performance Benchmarking](../operations/filesystem_benchmarking.md)
for steps to test filesystem performance.
-NOTE: **Note:** [Cloud Object Storage service](object_storage.md) with [Gitaly](gitaly.md)
-is recommended over NFS wherever possible for improved performance.
-
## NFS Server features
### Required features
@@ -73,7 +75,7 @@ To disable NFS server delegation, do the following:
#### Important notes
The kernel bug may be fixed in
-[more recent kernels with this commit](https://github.om/torvalds/linux/commit/95da1b3a5aded124dd1bda1e3cdb876184813140).
+[more recent kernels with this commit](https://github.com/torvalds/linux/commit/95da1b3a5aded124dd1bda1e3cdb876184813140).
Red Hat Enterprise 7 [shipped a kernel update](https://access.redhat.com/errata/RHSA-2019:2029)
on August 6, 2019 that may also have resolved this problem.
@@ -184,7 +186,7 @@ The NFS man page states:
Read the [Linux man page](https://linux.die.net/man/5/nfs) to understand the difference,
and if you do use `soft`, ensure that you've taken steps to mitigate the risks.
-If you experience behaviour that might have been caused by
+If you experience behavior that might have been caused by
writes to disk on the NFS server not occurring, such as commits going missing,
use the `hard` option, because (from the man page):
@@ -271,7 +273,7 @@ following are the 4 locations need to be shared:
Other GitLab directories should not be shared between nodes. They contain
node-specific files and GitLab code that does not need to be shared. To ship
-logs to a central location consider using remote syslog. GitLab Omnibus packages
+logs to a central location consider using remote syslog. Omnibus GitLab packages
provide configuration for [UDP log shipping](https://docs.gitlab.com/omnibus/settings/logs.html#udp-log-shipping-gitlab-enterprise-edition-only).
Having multiple NFS mounts will require manually making sure the data directories
diff --git a/doc/administration/high_availability/nfs_host_client_setup.md b/doc/administration/high_availability/nfs_host_client_setup.md
index 6823c1d9abe..7519ebf028d 100644
--- a/doc/administration/high_availability/nfs_host_client_setup.md
+++ b/doc/administration/high_availability/nfs_host_client_setup.md
@@ -36,7 +36,7 @@ apt-get install nfs-kernel-server
In this setup we will share the home directory on the host with the client. Edit the exports file as below to share the host's home directory with the client. If you have multiple clients running GitLab you must enter the client IP addresses in line in the `/etc/exports` file.
-```text
+```plaintext
#/etc/exports for one client
/home <client-ip-address>(rw,sync,no_root_squash,no_subtree_check)
@@ -96,7 +96,7 @@ NFS version 4. NFSv3 also supports locking as long as Linux Kernel 2.6.5+ is use
We recommend using version 4 and do not specifically test NFSv3.
See [NFS documentation](nfs.md#nfs-client-mount-options) for guidance on mount options.
-```text
+```plaintext
#/etc/fstab
10.0.0.1:/nfs/home /nfs/home nfs4 defaults,hard,vers=4.1,rsize=1048576,wsize=1048576,noatime,nofail,lookupcache=positive 0 2
```
@@ -111,7 +111,7 @@ default file locations in `gitlab.rb` on the client allows you to have one main
point and have all the required locations as subdirectories to use the NFS mount for
`git-data`.
-```text
+```plaintext
git_data_dirs({"default" => {"path" => "/nfs/home/var/opt/gitlab-data/git-data"}})
gitlab_rails['uploads_directory'] = '/nfs/home/var/opt/gitlab-data/uploads'
gitlab_rails['shared_path'] = '/nfs/home/var/opt/gitlab-data/shared'
diff --git a/doc/administration/high_availability/pgbouncer.md b/doc/administration/high_availability/pgbouncer.md
index f0e76172a00..4c672f49e26 100644
--- a/doc/administration/high_availability/pgbouncer.md
+++ b/doc/administration/high_availability/pgbouncer.md
@@ -165,7 +165,7 @@ Refer to your preferred Load Balancer's documentation for further guidance.
1. Run `gitlab-ctl reconfigure`
-1. On the node running Unicorn, make sure the following is set in `/etc/gitlab/gitlab.rb`
+1. On the node running Puma, make sure the following is set in `/etc/gitlab/gitlab.rb`
```ruby
gitlab_rails['db_host'] = 'PGBOUNCER_HOST'
@@ -215,7 +215,7 @@ To start a session, run
```shell
# gitlab-ctl pgb-console
Password for user pgbouncer:
-psql (9.6.8, server 1.7.2/bouncer)
+psql (11.7, server 1.7.2/bouncer)
Type "help" for help.
pgbouncer=#
diff --git a/doc/administration/high_availability/redis.md b/doc/administration/high_availability/redis.md
index 1e19e7e6c01..d52c80aec0d 100644
--- a/doc/administration/high_availability/redis.md
+++ b/doc/administration/high_availability/redis.md
@@ -27,24 +27,24 @@ These will be necessary when configuring the GitLab application servers later.
## Redis in a Scaled and Highly Available Environment
-This section is relevant for [Scalable and Highly Available Setups](../scaling/index.md).
+This section is relevant for [scalable and highly available setups](../reference_architectures/index.md).
### Provide your own Redis instance **(CORE ONLY)**
If you want to use your own deployed Redis instance(s),
see [Provide your own Redis instance](#provide-your-own-redis-instance-core-only)
-for more details. However, you can use the GitLab Omnibus package to easily
+for more details. However, you can use the Omnibus GitLab package to easily
deploy the bundled Redis.
-### Standalone Redis using GitLab Omnibus **(CORE ONLY)**
+### Standalone Redis using Omnibus GitLab **(CORE ONLY)**
-The GitLab Omnibus package can be used to configure a standalone Redis server.
+The Omnibus GitLab package can be used to configure a standalone Redis server.
In this configuration Redis is not highly available, and represents a single
point of failure. However, in a scaled environment the objective is to allow
the environment to handle more users or to increase throughput. Redis itself
is generally stable and can handle many requests so it is an acceptable
-trade off to have only a single instance. See [High Availability](../availability/index.md)
-for an overview of GitLab scaling and high availability options.
+trade off to have only a single instance. See the [reference architectures](../reference_architectures/index.md)
+page for an overview of GitLab scaling and high availability options.
The steps below are the minimum necessary to configure a Redis server with
Omnibus:
@@ -63,7 +63,7 @@ Omnibus:
## Disable all other services
sidekiq['enable'] = false
gitlab_workhorse['enable'] = false
- unicorn['enable'] = false
+ puma['enable'] = false
postgresql['enable'] = false
nginx['enable'] = false
prometheus['enable'] = false
@@ -89,16 +89,16 @@ Advanced configuration options are supported and can be added if
needed.
Continue configuration of other components by going back to the
-[High Availability](../availability/index.md#gitlab-components-and-configuration-instructions) page.
+[reference architectures](../reference_architectures/index.md#configure-gitlab-to-scale) page.
-### High Availability with GitLab Omnibus **(PREMIUM ONLY)**
+### High Availability with Omnibus GitLab **(PREMIUM ONLY)**
> Experimental Redis Sentinel support was [introduced in GitLab 8.11](https://gitlab.com/gitlab-org/gitlab-foss/-/merge_requests/1877).
Starting with 8.14, Redis Sentinel is no longer experimental.
If you've used it with versions `< 8.14` before, please check the updated
documentation here.
-High Availability with [Redis](https://redis.io/) is possible using a **Master** x **Slave**
+High Availability with [Redis](https://redis.io/) is possible using a **Master** x **Replica**
topology with a [Redis Sentinel](https://redis.io/topics/sentinel) service to watch and automatically
start the failover procedure.
@@ -130,12 +130,12 @@ make sure you read this Overview section to better understand how the components
are tied together.
You need at least `3` independent machines: physical, or VMs running into
-distinct physical machines. It is essential that all master and slaves Redis
+distinct physical machines. It is essential that all master and replica Redis
instances run in different machines. If you fail to provision the machines in
that specific way, any issue with the shared environment can bring your entire
setup down.
-It is OK to run a Sentinel alongside of a master or slave Redis instance.
+It is OK to run a Sentinel alongside of a master or replica Redis instance.
There should be no more than one Sentinel on the same machine though.
You also need to take into consideration the underlying network topology,
@@ -156,16 +156,16 @@ components below.
High Availability with Redis requires a few things:
- Multiple Redis instances
-- Run Redis in a **Master** x **Slave** topology
+- Run Redis in a **Master** x **Replica** topology
- Multiple Sentinel instances
- Application support and visibility to all Sentinel and Redis instances
Redis Sentinel can handle the most important tasks in an HA environment and that's
to help keep servers online with minimal to no downtime. Redis Sentinel:
-- Monitors **Master** and **Slaves** instances to see if they are available
-- Promotes a **Slave** to **Master** when the **Master** fails
-- Demotes a **Master** to **Slave** when the failed **Master** comes back online
+- Monitors **Master** and **Replicas** instances to see if they are available
+- Promotes a **Replica** to **Master** when the **Master** fails
+- Demotes a **Master** to **Replica** when the failed **Master** comes back online
(to prevent data-partitioning)
- Can be queried by the application to always connect to the current **Master**
server
@@ -185,8 +185,8 @@ For a minimal setup, you will install the Omnibus GitLab package in `3`
**independent** machines, both with **Redis** and **Sentinel**:
- Redis Master + Sentinel
-- Redis Slave + Sentinel
-- Redis Slave + Sentinel
+- Redis Replica + Sentinel
+- Redis Replica + Sentinel
If you are not sure or don't understand why and where the amount of nodes come
from, read [Redis setup overview](#redis-setup-overview) and
@@ -197,14 +197,14 @@ the Omnibus GitLab package in `5` **independent** machines, both with
**Redis** and **Sentinel**:
- Redis Master + Sentinel
-- Redis Slave + Sentinel
-- Redis Slave + Sentinel
-- Redis Slave + Sentinel
-- Redis Slave + Sentinel
+- Redis Replica + Sentinel
+- Redis Replica + Sentinel
+- Redis Replica + Sentinel
+- Redis Replica + Sentinel
### Redis setup overview
-You must have at least `3` Redis servers: `1` Master, `2` Slaves, and they
+You must have at least `3` Redis servers: `1` Master, `2` Replicas, and they
need to each be on independent machines (see explanation above).
You can have additional Redis nodes, that will help survive a situation
@@ -221,7 +221,7 @@ nodes to be provisioned. See [Sentinel setup overview](#sentinel-setup-overview)
documentation for more information.
All Redis nodes should be configured the same way and with similar server specs, as
-in a failover situation, any **Slave** can be promoted as the new **Master** by
+in a failover situation, any **Replica** can be promoted as the new **Master** by
the Sentinel servers.
The replication requires authentication, so you need to define a password to
@@ -241,9 +241,9 @@ need to be available and reachable, so that they can elect the Sentinel **leader
who will take all the decisions to restore the service availability by:
- Promoting a new **Master**
-- Reconfiguring the other **Slaves** and make them point to the new **Master**
+- Reconfiguring the other **Replicas** and make them point to the new **Master**
- Announce the new **Master** to every other Sentinel peer
-- Reconfigure the old **Master** and demote to **Slave** when it comes back online
+- Reconfigure the old **Master** and demote to **Replica** when it comes back online
You must have at least `3` Redis Sentinel servers, and they need to
be each in an independent machine (that are believed to fail independently),
@@ -280,18 +280,18 @@ the official documentation:
already tried against the same master by a given Sentinel, is two
times the failover timeout.
-- The time needed for a slave replicating to a wrong master according
+- The time needed for a replica replicating to a wrong master according
to a Sentinel current configuration, to be forced to replicate
with the right master, is exactly the failover timeout (counting since
the moment a Sentinel detected the misconfiguration).
- The time needed to cancel a failover that is already in progress but
- did not produced any configuration change (SLAVEOF NO ONE yet not
- acknowledged by the promoted slave).
+ did not produced any configuration change (REPLICAOF NO ONE yet not
+ acknowledged by the promoted replica).
-- The maximum time a failover in progress waits for all the slaves to be
- reconfigured as slaves of the new master. However even after this time
- the slaves will be reconfigured by the Sentinels anyway, but not with
+- The maximum time a failover in progress waits for all the replicas to be
+ reconfigured as replicas of the new master. However even after this time
+ the replicas will be reconfigured by the Sentinels anyway, but not with
the exact parallel-syncs progression as specified.
### Available configuration setups
@@ -306,12 +306,12 @@ Pick the one that suits your needs.
documentation.
- [Omnibus GitLab **Community Edition** (CE) package](https://about.gitlab.com/install/?version=ce): Redis is bundled, so you
can use the package with only the Redis service enabled as described in steps
- 1 and 2 of this document (works for both master and slave setups). To install
+ 1 and 2 of this document (works for both master and replica setups). To install
and configure Sentinel, jump directly to the Sentinel section in the
[Redis HA installation from source](redis_source.md#step-3-configuring-the-redis-sentinel-instances) documentation.
- [Omnibus GitLab **Enterprise Edition** (EE) package](https://about.gitlab.com/install/?version=ee): Both Redis and Sentinel
are bundled in the package, so you can use the EE package to set up the whole
- Redis HA infrastructure (master, slave and Sentinel) which is described in
+ Redis HA infrastructure (master, replica and Sentinel) which is described in
this document.
- If you have installed GitLab using the Omnibus GitLab packages (CE or EE),
but you want to use your own external Redis server, follow steps 1-3 in the
@@ -328,9 +328,9 @@ This is the section where we install and set up the new Redis instances.
> - We assume that you have installed GitLab and all HA components from scratch. If you
> already have it installed and running, read how to
> [switch from a single-machine installation to Redis HA](#switching-from-an-existing-single-machine-installation-to-redis-ha).
-> - Redis nodes (both master and slaves) will need the same password defined in
+> - Redis nodes (both master and replica) will need the same password defined in
> `redis['password']`. At any time during a failover the Sentinels can
-> reconfigure a node and change its status from master to slave and vice versa.
+> reconfigure a node and change its status from master to replica and vice versa.
### Prerequisites
@@ -392,9 +392,9 @@ The prerequisites for a HA Redis setup are the following:
> `roles ['redis_sentinel_role', 'redis_master_role']`. Read more about high
> availability roles at <https://docs.gitlab.com/omnibus/roles/>.
-### Step 2. Configuring the slave Redis instances
+### Step 2. Configuring the replica Redis instances
-1. SSH into the **slave** Redis server.
+1. SSH into the **replica** Redis server.
1. [Download/install](https://about.gitlab.com/install/) the Omnibus GitLab
package you want using **steps 1 and 2** from the GitLab downloads page.
- Make sure you select the correct Omnibus package, with the same version
@@ -404,8 +404,8 @@ The prerequisites for a HA Redis setup are the following:
1. Edit `/etc/gitlab/gitlab.rb` and add the contents:
```ruby
- # Specify server role as 'redis_slave_role'
- roles ['redis_slave_role']
+ # Specify server role as 'redis_replica_role'
+ roles ['redis_replica_role']
# IP address pointing to a local IP that the other machines can reach to.
# You can also set bind to '0.0.0.0' which listen in all interfaces.
@@ -435,10 +435,10 @@ The prerequisites for a HA Redis setup are the following:
```
1. [Reconfigure Omnibus GitLab](../restart_gitlab.md#omnibus-gitlab-reconfigure) for the changes to take effect.
-1. Go through the steps again for all the other slave nodes.
+1. Go through the steps again for all the other replica nodes.
> Note: You can specify multiple roles like sentinel and Redis as:
-> `roles ['redis_sentinel_role', 'redis_slave_role']`. Read more about high
+> `roles ['redis_sentinel_role', 'redis_replica_role']`. Read more about high
> availability roles at <https://docs.gitlab.com/omnibus/roles/>.
---
@@ -542,18 +542,18 @@ multiple machines with the Sentinel daemon.
## already tried against the same master by a given Sentinel, is two
## times the failover timeout.
##
- ## - The time needed for a slave replicating to a wrong master according
+ ## - The time needed for a replica replicating to a wrong master according
## to a Sentinel current configuration, to be forced to replicate
## with the right master, is exactly the failover timeout (counting since
## the moment a Sentinel detected the misconfiguration).
##
## - The time needed to cancel a failover that is already in progress but
- ## did not produced any configuration change (SLAVEOF NO ONE yet not
- ## acknowledged by the promoted slave).
+ ## did not produced any configuration change (REPLICAOF NO ONE yet not
+ ## acknowledged by the promoted replica).
##
- ## - The maximum time a failover in progress waits for all the slaves to be
- ## reconfigured as slaves of the new master. However even after this time
- ## the slaves will be reconfigured by the Sentinels anyway, but not with
+ ## - The maximum time a failover in progress waits for all the replica to be
+ ## reconfigured as replicas of the new master. However even after this time
+ ## the replicas will be reconfigured by the Sentinels anyway, but not with
## the exact parallel-syncs progression as specified.
# sentinel['failover_timeout'] = 60000
```
@@ -612,7 +612,7 @@ replicate from this machine first, before de-activating the Redis instance
inside it.
Your single-machine install will be the initial **Master**, and the `3` others
-should be configured as **Slave** pointing to this machine.
+should be configured as **Replica** pointing to this machine.
After replication catches up, you will need to stop services in the
single-machine install, to rotate the **Master** to one of the new nodes.
@@ -627,7 +627,7 @@ redis['enable'] = false
If you fail to replicate first, you may loose data (unprocessed background jobs).
-## Example of a minimal configuration with 1 master, 2 slaves and 3 Sentinels
+## Example of a minimal configuration with 1 master, 2 replicas and 3 Sentinels
>**Note:**
Redis Sentinel is bundled with Omnibus GitLab Enterprise Edition only. For
@@ -649,8 +649,8 @@ discussed in [Redis setup overview](#redis-setup-overview) and
Here is a list and description of each **machine** and the assigned **IP**:
- `10.0.0.1`: Redis Master + Sentinel 1
-- `10.0.0.2`: Redis Slave 1 + Sentinel 2
-- `10.0.0.3`: Redis Slave 2 + Sentinel 3
+- `10.0.0.2`: Redis Replica 1 + Sentinel 2
+- `10.0.0.3`: Redis Replica 2 + Sentinel 3
- `10.0.0.4`: GitLab application
Please note that after the initial configuration, if a failover is initiated
@@ -684,12 +684,12 @@ sentinel['quorum'] = 2
[Reconfigure Omnibus GitLab](../restart_gitlab.md#omnibus-gitlab-reconfigure) for the changes to take effect.
-### Example configuration for Redis slave 1 and Sentinel 2
+### Example configuration for Redis replica 1 and Sentinel 2
In `/etc/gitlab/gitlab.rb`:
```ruby
-roles ['redis_sentinel_role', 'redis_slave_role']
+roles ['redis_sentinel_role', 'redis_replica_role']
redis['bind'] = '10.0.0.2'
redis['port'] = 6379
redis['password'] = 'redis-password-goes-here'
@@ -706,12 +706,12 @@ sentinel['quorum'] = 2
[Reconfigure Omnibus GitLab](../restart_gitlab.md#omnibus-gitlab-reconfigure) for the changes to take effect.
-### Example configuration for Redis slave 2 and Sentinel 3
+### Example configuration for Redis replica 2 and Sentinel 3
In `/etc/gitlab/gitlab.rb`:
```ruby
-roles ['redis_sentinel_role', 'redis_slave_role']
+roles ['redis_sentinel_role', 'redis_replica_role']
redis['bind'] = '10.0.0.3'
redis['port'] = 6379
redis['password'] = 'redis-password-goes-here'
@@ -847,11 +847,11 @@ mailroom['enable'] = false
-------
-## Redis master/slave Role
+## Redis master/replica Role
redis_master_role['enable'] = true # enable only one of them
-redis_slave_role['enable'] = true # enable only one of them
+redis_replica_role['enable'] = true # enable only one of them
-# When Redis Master or Slave role are enabled, the following services are
+# When Redis Master or Replica role are enabled, the following services are
# enabled/disabled. Note that if Redis and Sentinel roles are combined, both
# services will be enabled.
@@ -863,7 +863,7 @@ postgresql['enable'] = false
gitlab_rails['enable'] = false
mailroom['enable'] = false
-# For Redis Slave role, also change this setting from default 'true' to 'false':
+# For Redis Replica role, also change this setting from default 'true' to 'false':
redis['master'] = false
```
@@ -894,13 +894,13 @@ You can check if everything is correct by connecting to each server using
```
When connected to a `master` Redis, you will see the number of connected
-`slaves`, and a list of each with connection details:
+`replicas`, and a list of each with connection details:
```plaintext
# Replication
role:master
-connected_slaves:1
-slave0:ip=10.133.5.21,port=6379,state=online,offset=208037514,lag=1
+connected_replicas:1
+replica0:ip=10.133.5.21,port=6379,state=online,offset=208037514,lag=1
master_repl_offset:208037658
repl_backlog_active:1
repl_backlog_size:1048576
@@ -908,21 +908,21 @@ repl_backlog_first_byte_offset:206989083
repl_backlog_histlen:1048576
```
-When it's a `slave`, you will see details of the master connection and if
+When it's a `replica`, you will see details of the master connection and if
its `up` or `down`:
```plaintext
# Replication
-role:slave
+role:replica
master_host:10.133.1.58
master_port:6379
master_link_status:up
master_last_io_seconds_ago:1
master_sync_in_progress:0
-slave_repl_offset:208096498
-slave_priority:100
-slave_read_only:1
-connected_slaves:0
+replica_repl_offset:208096498
+replica_priority:100
+replica_read_only:1
+connected_replicas:0
master_repl_offset:0
repl_backlog_active:0
repl_backlog_size:1048576
diff --git a/doc/administration/high_availability/redis_source.md b/doc/administration/high_availability/redis_source.md
index 95b1397bab4..97be480bc3b 100644
--- a/doc/administration/high_availability/redis_source.md
+++ b/doc/administration/high_availability/redis_source.md
@@ -40,7 +40,7 @@ This is the section where we install and set up the new Redis instances.
- Since Redis 3.2, you must define a password to receive external connections
(`requirepass`).
- If you are using Redis with Sentinel, you will also need to define the same
- password for the slave password definition (`masterauth`) in the same instance.
+ password for the replica password definition (`masterauth`) in the same instance.
In addition, read the prerequisites as described in the
[Omnibus Redis HA document](redis.md#prerequisites) since they provide some
@@ -72,9 +72,9 @@ Assuming that the Redis master instance IP is `10.0.0.1`:
1. Restart the Redis service for the changes to take effect.
-### Step 2. Configuring the slave Redis instances
+### Step 2. Configuring the replica Redis instances
-Assuming that the Redis slave instance IP is `10.0.0.2`:
+Assuming that the Redis replica instance IP is `10.0.0.2`:
1. [Install Redis](../../install/installation.md#7-redis).
1. Edit `/etc/redis/redis.conf`:
@@ -95,12 +95,12 @@ Assuming that the Redis slave instance IP is `10.0.0.2`:
requirepass redis-password-goes-here
masterauth redis-password-goes-here
- ## Define `slaveof` pointing to the Redis master instance with IP and port.
- slaveof 10.0.0.1 6379
+ ## Define `replicaof` pointing to the Redis master instance with IP and port.
+ replicaof 10.0.0.1 6379
```
1. Restart the Redis service for the changes to take effect.
-1. Go through the steps again for all the other slave nodes.
+1. Go through the steps again for all the other replica nodes.
### Step 3. Configuring the Redis Sentinel instances
@@ -131,7 +131,7 @@ master with IP `10.0.0.1` (some settings might overlap with the master):
masterauth redis-password-goes-here
## Define with `sentinel auth-pass` the same shared password you have
- ## defined for both Redis master and slaves instances.
+ ## defined for both Redis master and replicas instances.
sentinel auth-pass gitlab-redis redis-password-goes-here
## Define with `sentinel monitor` the IP and port of the Redis
@@ -149,18 +149,18 @@ master with IP `10.0.0.1` (some settings might overlap with the master):
## already tried against the same master by a given Sentinel, is two
## times the failover timeout.
##
- ## * The time needed for a slave replicating to a wrong master according
+ ## * The time needed for a replica replicating to a wrong master according
## to a Sentinel current configuration, to be forced to replicate
## with the right master, is exactly the failover timeout (counting since
## the moment a Sentinel detected the misconfiguration).
##
## * The time needed to cancel a failover that is already in progress but
- ## did not produced any configuration change (SLAVEOF NO ONE yet not
- ## acknowledged by the promoted slave).
+ ## did not produced any configuration change (REPLICAOF NO ONE yet not
+ ## acknowledged by the promoted replica).
##
- ## * The maximum time a failover in progress waits for all the slaves to be
- ## reconfigured as slaves of the new master. However even after this time
- ## the slaves will be reconfigured by the Sentinels anyway, but not with
+ ## * The maximum time a failover in progress waits for all the replicas to be
+ ## reconfigured as replicas of the new master. However even after this time
+ ## the replicas will be reconfigured by the Sentinels anyway, but not with
## the exact parallel-syncs progression as specified.
sentinel failover_timeout 30000
```
@@ -203,7 +203,7 @@ setup:
1. [Restart GitLab](../restart_gitlab.md#installations-from-source) for the changes to take effect.
-## Example of minimal configuration with 1 master, 2 slaves and 3 Sentinels
+## Example of minimal configuration with 1 master, 2 replicas and 3 Sentinels
In this example we consider that all servers have an internal network
interface with IPs in the `10.0.0.x` range, and that they can connect
@@ -215,13 +215,13 @@ outside ([Internet](https://gitlab.com/gitlab-org/gitlab-foss/uploads/c4cc8cd353
For this example, **Sentinel 1** will be configured in the same machine as the
**Redis Master**, **Sentinel 2** and **Sentinel 3** in the same machines as the
-**Slave 1** and **Slave 2** respectively.
+**Replica 1** and **Replica 2** respectively.
Here is a list and description of each **machine** and the assigned **IP**:
- `10.0.0.1`: Redis Master + Sentinel 1
-- `10.0.0.2`: Redis Slave 1 + Sentinel 2
-- `10.0.0.3`: Redis Slave 2 + Sentinel 3
+- `10.0.0.2`: Redis Replica 1 + Sentinel 2
+- `10.0.0.3`: Redis Replica 2 + Sentinel 3
- `10.0.0.4`: GitLab application
Please note that after the initial configuration, if a failover is initiated
@@ -257,7 +257,7 @@ or a failover promotes a different **Master** node.
1. Restart the Redis service for the changes to take effect.
-### Example configuration for Redis slave 1 and Sentinel 2
+### Example configuration for Redis replica 1 and Sentinel 2
1. In `/etc/redis/redis.conf`:
@@ -266,7 +266,7 @@ or a failover promotes a different **Master** node.
port 6379
requirepass redis-password-goes-here
masterauth redis-password-goes-here
- slaveof 10.0.0.1 6379
+ replicaof 10.0.0.1 6379
```
1. In `/etc/redis/sentinel.conf`:
@@ -282,7 +282,7 @@ or a failover promotes a different **Master** node.
1. Restart the Redis service for the changes to take effect.
-### Example configuration for Redis slave 2 and Sentinel 3
+### Example configuration for Redis replica 2 and Sentinel 3
1. In `/etc/redis/redis.conf`:
@@ -291,7 +291,7 @@ or a failover promotes a different **Master** node.
port 6379
requirepass redis-password-goes-here
masterauth redis-password-goes-here
- slaveof 10.0.0.1 6379
+ replicaof 10.0.0.1 6379
```
1. In `/etc/redis/sentinel.conf`:
diff --git a/doc/administration/high_availability/sidekiq.md b/doc/administration/high_availability/sidekiq.md
index ed273c3b113..493929dcf3b 100644
--- a/doc/administration/high_availability/sidekiq.md
+++ b/doc/administration/high_availability/sidekiq.md
@@ -88,12 +88,15 @@ you want using steps 1 and 2 from the GitLab downloads page.
postgresql['enable'] = false
redis['enable'] = false
redis_exporter['enable'] = false
- unicorn['enable'] = false
+ puma['enable'] = false
gitlab_exporter['enable'] = false
```
1. Run `gitlab-ctl reconfigure`.
+NOTE: **Note:** You will need to restart the Sidekiq nodes after an update has occurred and database
+migrations performed.
+
## Example configuration
Here's what the ending `/etc/gitlab/gitlab.rb` would look like:
@@ -116,7 +119,7 @@ postgres_exporter['enable'] = false
postgresql['enable'] = false
redis['enable'] = false
redis_exporter['enable'] = false
-unicorn['enable'] = false
+puma['enable'] = false
gitlab_exporter['enable'] = false
########################################
diff --git a/doc/administration/incoming_email.md b/doc/administration/incoming_email.md
index dcc590bea9c..fcd69464b13 100644
--- a/doc/administration/incoming_email.md
+++ b/doc/administration/incoming_email.md
@@ -182,6 +182,9 @@ gitlab_rails['incoming_email_start_tls'] = false
gitlab_rails['incoming_email_mailbox_name'] = "inbox"
# The IDLE command timeout.
gitlab_rails['incoming_email_idle_timeout'] = 60
+
+# Whether to expunge (permanently remove) messages from the mailbox when they are deleted after delivery
+gitlab_rails['incoming_email_expunge_deleted'] = true
```
Example for source installs:
@@ -214,12 +217,18 @@ incoming_email:
mailbox: "inbox"
# The IDLE command timeout.
idle_timeout: 60
+
+ # Whether to expunge (permanently remove) messages from the mailbox when they are deleted after delivery
+ expunge_deleted: true
```
#### Gmail
Example configuration for Gmail/G Suite. Assumes mailbox `gitlab-incoming@gmail.com`.
+NOTE: **Note:**
+`incoming_email_email` cannot be a Gmail alias account.
+
Example for Omnibus installs:
```ruby
@@ -249,6 +258,9 @@ gitlab_rails['incoming_email_start_tls'] = false
gitlab_rails['incoming_email_mailbox_name'] = "inbox"
# The IDLE command timeout.
gitlab_rails['incoming_email_idle_timeout'] = 60
+
+# Whether to expunge (permanently remove) messages from the mailbox when they are deleted after delivery
+gitlab_rails['incoming_email_expunge_deleted'] = true
```
Example for source installs:
@@ -281,6 +293,9 @@ incoming_email:
mailbox: "inbox"
# The IDLE command timeout.
idle_timeout: 60
+
+ # Whether to expunge (permanently remove) messages from the mailbox when they are deleted after delivery
+ expunge_deleted: true
```
#### Microsoft Exchange Server
diff --git a/doc/administration/index.md b/doc/administration/index.md
index 1f4e23fce8a..fb827ae8573 100644
--- a/doc/administration/index.md
+++ b/doc/administration/index.md
@@ -34,8 +34,8 @@ Learn how to install, configure, update, and maintain your GitLab instance.
- [Install](../install/README.md): Requirements, directory structures, and installation methods.
- [Database load balancing](database_load_balancing.md): Distribute database queries among multiple database servers. **(STARTER ONLY)**
- [Omnibus support for log forwarding](https://docs.gitlab.com/omnibus/settings/logs.html#udp-log-shipping-gitlab-enterprise-edition-only) **(STARTER ONLY)**
-- [High Availability](availability/index.md): Configure multiple servers for scaling or high availability.
- - [Installing GitLab HA on Amazon Web Services (AWS)](../install/aws/index.md): Set up GitLab High Availability on Amazon AWS.
+- [Reference architectures](reference_architectures/index.md): Add additional resources to support more users.
+ - [Installing GitLab on Amazon Web Services (AWS)](../install/aws/index.md): Set up GitLab on Amazon AWS.
- [Geo](geo/replication/index.md): Replicate your GitLab instance to other geographic locations as a read-only fully operational version. **(PREMIUM ONLY)**
- [Disaster Recovery](geo/disaster_recovery/index.md): Quickly fail-over to a different site with minimal effort in a disaster situation. **(PREMIUM ONLY)**
- [Pivotal Tile](../install/pivotal/index.md): Deploy GitLab as a preconfigured appliance using Ops Manager (BOSH) for Pivotal Cloud Foundry. **(PREMIUM ONLY)**
@@ -64,7 +64,8 @@ Learn how to install, configure, update, and maintain your GitLab instance.
- [External Classification Policy Authorization](../user/admin_area/settings/external_authorization.md) **(PREMIUM ONLY)**
- [Upload a license](../user/admin_area/license.md): Upload a license to unlock features that are in paid tiers of GitLab. **(STARTER ONLY)**
- [Admin Area](../user/admin_area/index.md): for self-managed instance-wide configuration and maintenance.
-- [S/MIME Signing](smime_signing_email.md): how to sign all outgoing notification emails with S/MIME
+- [S/MIME Signing](smime_signing_email.md): how to sign all outgoing notification emails with S/MIME.
+- [Enabling and disabling features flags](feature_flags.md): how to enable and disable GitLab features deployed behind feature flags.
#### Customizing GitLab's appearance
@@ -76,9 +77,9 @@ Learn how to install, configure, update, and maintain your GitLab instance.
### Maintaining GitLab
-- [Raketasks](../raketasks/README.md): Perform various tasks for maintenance, backups, automatic webhooks setup, and more.
+- [Rake tasks](../raketasks/README.md): Perform various tasks for maintenance, backups, automatic webhooks setup, and more.
- [Backup and restore](../raketasks/backup_restore.md): Backup and restore your GitLab instance.
-- [Operations](operations/index.md): Keeping GitLab up and running (clean up Redis sessions, moving repositories, Sidekiq MemoryKiller, Unicorn).
+- [Operations](operations/index.md): Keeping GitLab up and running (clean up Redis sessions, moving repositories, Sidekiq MemoryKiller, Puma).
- [Restart GitLab](restart_gitlab.md): Learn how to restart GitLab and its components.
- [Invalidate Markdown cache](invalidate_markdown_cache.md): Invalidate any cached Markdown.
@@ -98,8 +99,8 @@ Learn how to install, configure, update, and maintain your GitLab instance.
- [Mattermost](https://docs.gitlab.com/omnibus/gitlab-mattermost/): Integrate with [Mattermost](https://mattermost.com), an open source, private cloud workplace for web messaging.
- [PlantUML](integration/plantuml.md): Create simple diagrams in AsciiDoc and Markdown documents
- created in snippets, wikis, and repos.
-- [Web terminals](integration/terminal.md): Provide terminal access to your applications deployed to Kubernetes from within GitLab's CI/CD [environments](../ci/environments.md#web-terminals).
+ created in snippets, wikis, and repositories.
+- [Web terminals](integration/terminal.md): Provide terminal access to your applications deployed to Kubernetes from within GitLab's CI/CD [environments](../ci/environments/index.md#web-terminals).
## User settings and permissions
@@ -183,8 +184,6 @@ Learn how to install, configure, update, and maintain your GitLab instance.
- [GitLab Performance Monitoring](monitoring/performance/index.md):
- [Enable Performance Monitoring](monitoring/performance/gitlab_configuration.md): Enable GitLab Performance Monitoring.
- - [GitLab performance monitoring with InfluxDB](monitoring/performance/influxdb_configuration.md): Configure GitLab and InfluxDB for measuring performance metrics.
- - [InfluxDB Schema](monitoring/performance/influxdb_schema.md): Measurements stored in InfluxDB.
- [GitLab performance monitoring with Prometheus](monitoring/prometheus/index.md): Configure GitLab and Prometheus for measuring performance metrics.
- [GitLab performance monitoring with Grafana](monitoring/performance/grafana_configuration.md): Configure GitLab to visualize time series metrics through graphs and dashboards.
- [Request Profiling](monitoring/performance/request_profiling.md): Get a detailed profile on slow requests.
diff --git a/doc/administration/instance_limits.md b/doc/administration/instance_limits.md
index 57acdec4ea2..d0e8f079cb2 100644
--- a/doc/administration/instance_limits.md
+++ b/doc/administration/instance_limits.md
@@ -99,6 +99,29 @@ header. Such emails don't create comments on issues or merge requests.
Sentry payloads sent to GitLab have a 1 MB maximum limit, both for security reasons
and to limit memory consumption.
+## Max offset allowed via REST API for offset-based pagination
+
+> [Introduced](https://gitlab.com/gitlab-org/gitlab/-/issues/34565) in GitLab 13.0.
+
+When using offset-based pagination in the REST API, there is a limit to the maximum
+requested offset into the set of results. This limit is only applied to endpoints that
+support keyset-based pagination. More information about pagination options can be
+found in the [API docs section on pagination](../api/README.md#pagination).
+
+To set this limit on a self-managed installation, run the following in the
+[GitLab Rails console](troubleshooting/debug.md#starting-a-rails-console-session):
+
+```ruby
+# If limits don't exist for the default plan, you can create one with:
+# Plan.default.create_limits!
+
+Plan.default.limits.update!(offset_pagination_limit: 10000)
+```
+
+- **Default offset pagination limit:** 50000
+
+NOTE: **Note:** Set the limit to `0` to disable it.
+
## CI/CD limits
### Number of jobs in active pipelines
@@ -180,7 +203,7 @@ Plan.default.limits.update!(ci_pipeline_schedules: 100)
### Incident Management inbound alert limits
-> [Introduced](https://gitlab.com/gitlab-org/gitlab/-/issues/14932) in GitLab 12.5.
+> [Introduced](https://gitlab.com/gitlab-org/gitlab/-/merge_requests/17859) in GitLab 12.5.
Limiting inbound alerts for an incident reduces the number of alerts (issues)
that can be created within a period of time, which can help prevent overloading
@@ -192,10 +215,16 @@ alerts in the following ways:
### Prometheus Alert JSON payloads
-> [Introduced](https://gitlab.com/gitlab-org/gitlab/-/issues/14929) in GitLab 12.6.
+> [Introduced](https://gitlab.com/gitlab-org/gitlab/-/merge_requests/19940) in GitLab 12.6.
Prometheus alert payloads sent to the `notify.json` endpoint are limited to 1 MB in size.
+### Generic Alert JSON payloads
+
+> [Introduced](https://gitlab.com/gitlab-org/gitlab/-/merge_requests/16441) in GitLab 12.4.
+
+Alert payloads sent to the `notify.json` endpoint are limited to 1 MB in size.
+
## Environment data on Deploy Boards
[Deploy Boards](../user/project/deploy_boards.md) load information from Kubernetes about
@@ -233,6 +262,10 @@ NOTE: **Note:** Set the limit to `0` to disable it.
- [Length restrictions for file and directory names](../user/project/wiki/index.md#length-restrictions-for-file-and-directory-names).
+## Snippets limits
+
+See the [documentation on Snippets settings](snippets/index.md).
+
## Push Event Limits
### Webhooks and Project Services
diff --git a/doc/administration/instance_review.md b/doc/administration/instance_review.md
index bb64c7967b7..8ea4bff252e 100644
--- a/doc/administration/instance_review.md
+++ b/doc/administration/instance_review.md
@@ -1,6 +1,6 @@
# Instance Review **(CORE ONLY)**
-> [Introduced][6995] in [GitLab Core][ee] 11.3.
+> [Introduced](https://gitlab.com/gitlab-org/gitlab-foss/-/merge_requests/6995) in [GitLab Core](https://about.gitlab.com/pricing/) 11.3.
If you are running a medium size instance of GitLab Core edition you are qualified for a free Instance Review. You can find the button in the User menu.
@@ -11,6 +11,3 @@ When you click the button you will be redirected to a form with prefilled data o
Once you submit the data to GitLab Inc. you can see the initial report.
Additionally you will be contacted by our team for further review which should help you to improve your usage of GitLab.
-
-[6995]: https://gitlab.com/gitlab-org/gitlab-foss/-/merge_requests/6995
-[ee]: https://about.gitlab.com/pricing/
diff --git a/doc/administration/integration/plantuml.md b/doc/administration/integration/plantuml.md
index 4af787fd19f..3372d05bd6b 100644
--- a/doc/administration/integration/plantuml.md
+++ b/doc/administration/integration/plantuml.md
@@ -4,7 +4,7 @@
When [PlantUML](https://plantuml.com) integration is enabled and configured in
GitLab we are able to create simple diagrams in AsciiDoc and Markdown documents
-created in snippets, wikis, and repos.
+created in snippets, wikis, and repositories.
## PlantUML Server
@@ -70,7 +70,7 @@ sudo service tomcat8 restart
Once the Tomcat service restarts the PlantUML service will be ready and
listening for requests on port 8080:
-```text
+```plaintext
http://localhost:8080/plantuml
```
@@ -115,10 +115,23 @@ that, login with an Admin account and do following:
- Check **Enable PlantUML** checkbox.
- Set the PlantUML instance as `https://gitlab.example.com/-/plantuml/`.
+NOTE: **Note:** If you are using a PlantUML server running v1.2020.9 and
+above (for example, [plantuml.com](https://plantuml.com)), set the `PLANTUML_ENCODING`
+environment variable to enable the `deflate` compression. On Omnibus,
+this can be done set in `/etc/gitlab.rb`:
+
+```ruby
+gitlab_rails['env'] = { 'PLANTUML_ENCODING' => 'deflate' }
+```
+
+From GitLab 13.1 and later, PlantUML integration now
+[requires a header prefix in the URL](https://github.com/plantuml/plantuml/issues/117#issuecomment-6235450160)
+to distinguish different encoding types.
+
## Creating Diagrams
With PlantUML integration enabled and configured, we can start adding diagrams to
-our AsciiDoc snippets, wikis and repos using delimited blocks:
+our AsciiDoc snippets, wikis, and repositories using delimited blocks:
- **Markdown**
@@ -131,7 +144,7 @@ our AsciiDoc snippets, wikis and repos using delimited blocks:
- **AsciiDoc**
- ```text
+ ```plaintext
[plantuml, format="png", id="myDiagram", width="200px"]
----
Bob->Alice : hello
@@ -141,7 +154,7 @@ our AsciiDoc snippets, wikis and repos using delimited blocks:
- **reStructuredText**
- ```text
+ ```plaintext
.. plantuml::
:caption: Caption with **bold** and *italic*
@@ -169,10 +182,10 @@ diagram delimiters `@startuml`/`@enduml` as these are replaced by the AsciiDoc `
Some parameters can be added to the AsciiDoc block definition:
-- *format*: Can be either `png` or `svg`. Note that `svg` is not supported by
+- `format`: Can be either `png` or `svg`. Note that `svg` is not supported by
all browsers so use with care. The default is `png`.
-- *id*: A CSS id added to the diagram HTML tag.
-- *width*: Width attribute added to the image tag.
-- *height*: Height attribute added to the image tag.
+- `id`: A CSS ID added to the diagram HTML tag.
+- `width`: Width attribute added to the image tag.
+- `height`: Height attribute added to the image tag.
Markdown does not support any parameters and will always use PNG format.
diff --git a/doc/administration/integration/terminal.md b/doc/administration/integration/terminal.md
index 251e4ded8b4..bc5816ce120 100644
--- a/doc/administration/integration/terminal.md
+++ b/doc/administration/integration/terminal.md
@@ -8,7 +8,7 @@ Only project maintainers and owners can access web terminals.
With the introduction of the [Kubernetes integration](../../user/project/clusters/index.md),
GitLab gained the ability to store and use credentials for a Kubernetes cluster.
One of the things it uses these credentials for is providing access to
-[web terminals](../../ci/environments.md#web-terminals) for environments.
+[web terminals](../../ci/environments/index.md#web-terminals) for environments.
## How it works
diff --git a/doc/administration/issue_closing_pattern.md b/doc/administration/issue_closing_pattern.md
index 7b815143597..579b957eb47 100644
--- a/doc/administration/issue_closing_pattern.md
+++ b/doc/administration/issue_closing_pattern.md
@@ -28,7 +28,7 @@ Because Rubular doesn't understand `%{issue_ref}`, you can replace this by
expression of your liking:
```ruby
- gitlab_rails['gitlab_issue_closing_pattern'] = "\b((?:[Cc]los(?:e[sd]|ing)|\b[Ff]ix(?:e[sd]|ing)?) +(?:(?:issues? +)?%{issue_ref}(?:(?:, *| +and +)?))+)"
+ gitlab_rails['gitlab_issue_closing_pattern'] = "\b((?:[Cc]los(?:e[sd]?|ing)|\b[Ff]ix(?:e[sd]|ing)?|\b[Rr]esolv(?:e[sd]?|ing)|\b[Ii]mplement(?:s|ed|ing)?)(:?) +(?:(?:issues? +)?%{issue_ref}(?:(?: *,? +and +| *,? *)?)|([A-Z][A-Z0-9_]+-\d+))+)"
```
1. [Reconfigure](restart_gitlab.md#omnibus-gitlab-reconfigure) GitLab for the changes to take effect.
@@ -39,7 +39,7 @@ Because Rubular doesn't understand `%{issue_ref}`, you can replace this by
1. Change the value of `issue_closing_pattern`:
```yaml
- issue_closing_pattern: "\b((?:[Cc]los(?:e[sd]|ing)|\b[Ff]ix(?:e[sd]|ing)?) +(?:(?:issues? +)?%{issue_ref}(?:(?:, *| +and +)?))+)"
+ issue_closing_pattern: "\b((?:[Cc]los(?:e[sd]?|ing)|\b[Ff]ix(?:e[sd]|ing)?|\b[Rr]esolv(?:e[sd]?|ing)|\b[Ii]mplement(?:s|ed|ing)?)(:?) +(?:(?:issues? +)?%{issue_ref}(?:(?: *,? +and +| *,? *)?)|([A-Z][A-Z0-9_]+-\d+))+)"
```
1. [Restart](restart_gitlab.md#installations-from-source) GitLab for the changes to take effect.
diff --git a/doc/administration/job_artifacts.md b/doc/administration/job_artifacts.md
index 3777f551996..fbfad46ef65 100644
--- a/doc/administration/job_artifacts.md
+++ b/doc/administration/job_artifacts.md
@@ -378,7 +378,7 @@ and [projects APIs](../api/projects.md).
When GitLab receives an artifacts archive, an archive metadata file is also
generated by [GitLab Workhorse](https://gitlab.com/gitlab-org/gitlab-workhorse). This metadata file describes all the entries
that are located in the artifacts archive itself.
-The metadata file is in a binary format, with additional GZIP compression.
+The metadata file is in a binary format, with additional Gzip compression.
GitLab does not extract the artifacts archive in order to save space, memory
and disk I/O. It instead inspects the metadata file which contains all the
@@ -450,13 +450,13 @@ If you need to manually remove job artifacts associated with multiple jobs while
```ruby
project = Project.find_by_full_path('path/to/project')
- builds_with_artifacts = project.builds.with_artifacts_archive
+ builds_with_artifacts = project.builds.with_downloadable_artifacts
```
To select all jobs with artifacts across the entire GitLab instance:
```ruby
- builds_with_artifacts = Ci::Build.with_artifacts_archive
+ builds_with_artifacts = Ci::Build.with_downloadable_artifacts
```
1. Delete job artifacts older than a specific date:
diff --git a/doc/administration/job_logs.md b/doc/administration/job_logs.md
index 6020d1d2850..5c03ff1f4b2 100644
--- a/doc/administration/job_logs.md
+++ b/doc/administration/job_logs.md
@@ -12,8 +12,8 @@ In the following table you can see the phases a log goes through:
| Phase | State | Condition | Data flow | Stored path |
| -------------- | ------------ | ----------------------- | -----------------------------------------| ----------- |
-| 1: patching | log | When a job is running | GitLab Runner => Unicorn => file storage | `#{ROOT_PATH}/gitlab-ci/builds/#{YYYY_mm}/#{project_id}/#{job_id}.log` |
-| 2: overwriting | log | When a job is finished | GitLab Runner => Unicorn => file storage | `#{ROOT_PATH}/gitlab-ci/builds/#{YYYY_mm}/#{project_id}/#{job_id}.log` |
+| 1: patching | log | When a job is running | GitLab Runner => Puma => file storage | `#{ROOT_PATH}/gitlab-ci/builds/#{YYYY_mm}/#{project_id}/#{job_id}.log` |
+| 2: overwriting | log | When a job is finished | GitLab Runner => Puma => file storage | `#{ROOT_PATH}/gitlab-ci/builds/#{YYYY_mm}/#{project_id}/#{job_id}.log` |
| 3: archiving | archived log | After a job is finished | Sidekiq moves log to artifacts folder | `#{ROOT_PATH}/gitlab-rails/shared/artifacts/#{disk_hash}/#{YYYY_mm_dd}/#{job_id}/#{job_artifact_id}/job.log` |
| 4: uploading | archived log | After a log is archived | Sidekiq moves archived log to [object storage](#uploading-logs-to-object-storage) (if configured) | `#{bucket_name}/#{disk_hash}/#{YYYY_mm_dd}/#{job_id}/#{job_artifact_id}/job.log` |
diff --git a/doc/administration/lfs/index.md b/doc/administration/lfs/index.md
index 71c1ae22305..e2b982448ef 100644
--- a/doc/administration/lfs/index.md
+++ b/doc/administration/lfs/index.md
@@ -2,7 +2,7 @@
disqus_identifier: 'https://docs.gitlab.com/ee/workflow/lfs/lfs_administration.html'
---
-# GitLab Git LFS Administration
+# GitLab Git Large File Storage (LFS) Administration
Documentation on how to use Git LFS are under [Managing large binary files with Git LFS doc](../../topics/git/lfs/index.md).
@@ -50,7 +50,7 @@ In `config/gitlab.yml`:
## Storing LFS objects in remote object storage
-> [Introduced][ee-2760] in [GitLab Premium][eep] 10.0. Brought to GitLab Core in 10.7.
+> [Introduced](https://gitlab.com/gitlab-org/gitlab/-/merge_requests/2760) in [GitLab Premium](https://about.gitlab.com/pricing/) 10.0. Brought to GitLab Core in 10.7.
It is possible to store LFS objects in remote object storage which allows you
to offload local hard disk R/W operations, and free up disk space significantly.
@@ -129,7 +129,7 @@ Here is a configuration example with Rackspace Cloud Files.
NOTE: **Note:**
Regardless of whether the container has public access enabled or disabled, Fog will
use the TempURL method to grant access to LFS objects. If you see errors in logs referencing
-instantiating storage with a temp-url-key, ensure that you have set the key properly
+instantiating storage with a `temp-url-key`, ensure that you have set the key properly
on the Rackspace API and in `gitlab.rb`. You can verify the value of the key Rackspace
has set by sending a GET request with token header to the service access endpoint URL
and comparing the output of the returned headers.
@@ -141,18 +141,23 @@ There are two ways to manually do the same thing as automatic uploading (describ
**Option 1: Rake task**
```shell
-rake gitlab:lfs:migrate
+gitlab-rake gitlab:lfs:migrate
```
-**Option 2: rails console**
+**Option 2: Rails console**
+
+Log into the Rails console:
```shell
-$ sudo gitlab-rails console # Login to rails console
+sudo gitlab-rails console
+```
-> # Upload LFS files manually
-> LfsObject.where(file_store: [nil, 1]).find_each do |lfs_object|
-> lfs_object.file.migrate!(ObjectStorage::Store::REMOTE) if lfs_object.file.file.exists?
-> end
+Upload LFS files manually
+
+```ruby
+LfsObject.where(file_store: [nil, 1]).find_each do |lfs_object|
+ lfs_object.file.migrate!(ObjectStorage::Store::REMOTE) if lfs_object.file.file.exists?
+end
```
### S3 for Omnibus installations
@@ -177,7 +182,7 @@ On Omnibus installations, the settings are prefixed by `lfs_object_store_`:
}
```
-1. Save the file and [reconfigure GitLab]s for the changes to take effect.
+1. Save the file and [reconfigure GitLab](../restart_gitlab.md#omnibus-gitlab-reconfigure) for the changes to take effect.
1. Migrate any existing local LFS objects to the object storage:
```shell
@@ -213,7 +218,7 @@ For source installations the settings are nested under `lfs:` and then
path_style: true
```
-1. Save the file and [restart GitLab][] for the changes to take effect.
+1. Save the file and [restart GitLab](../restart_gitlab.md#installations-from-source) for the changes to take effect.
1. Migrate any existing local LFS objects to the object storage:
```shell
@@ -244,19 +249,29 @@ If LFS integration is configured with Google Cloud Storage and background upload
Sidekiq workers may encounter this error. This is because the uploading timed out with very large files.
LFS files up to 6Gb can be uploaded without any extra steps, otherwise you need to use the following workaround.
+Log into Rails console:
+
```shell
-$ sudo gitlab-rails console # Login to rails console
-
-> # Set up timeouts. 20 minutes is enough to upload 30GB LFS files.
-> # These settings are only in effect for the same session, i.e. they are not effective for sidekiq workers.
-> ::Google::Apis::ClientOptions.default.open_timeout_sec = 1200
-> ::Google::Apis::ClientOptions.default.read_timeout_sec = 1200
-> ::Google::Apis::ClientOptions.default.send_timeout_sec = 1200
-
-> # Upload LFS files manually. This process does not use sidekiq at all.
-> LfsObject.where(file_store: [nil, 1]).find_each do |lfs_object|
-> lfs_object.file.migrate!(ObjectStorage::Store::REMOTE) if lfs_object.file.file.exists?
-> end
+sudo gitlab-rails console
+```
+
+Set up timeouts:
+
+- These settings are only in effect for the same session. For example, they are not effective for Sidekiq workers.
+- 20 minutes (1200 sec) is enough to upload 30GB LFS files:
+
+```ruby
+::Google::Apis::ClientOptions.default.open_timeout_sec = 1200
+::Google::Apis::ClientOptions.default.read_timeout_sec = 1200
+::Google::Apis::ClientOptions.default.send_timeout_sec = 1200
+```
+
+Upload LFS files manually (this process does not use Sidekiq at all):
+
+```ruby
+LfsObject.where(file_store: [nil, 1]).find_each do |lfs_object|
+ lfs_object.file.migrate!(ObjectStorage::Store::REMOTE) if lfs_object.file.file.exists?
+end
```
See more information in [!19581](https://gitlab.com/gitlab-org/gitlab-foss/-/merge_requests/19581)
@@ -268,8 +283,3 @@ See more information in [!19581](https://gitlab.com/gitlab-org/gitlab-foss/-/mer
- Only compatible with the Git LFS client versions 1.1.0 and up, or 1.0.2.
- The storage statistics currently count each LFS object multiple times for
every project linking to it.
-
-[reconfigure gitlab]: ../restart_gitlab.md#omnibus-gitlab-reconfigure "How to reconfigure Omnibus GitLab"
-[restart gitlab]: ../restart_gitlab.md#installations-from-source "How to restart GitLab"
-[eep]: https://about.gitlab.com/pricing/ "GitLab Premium"
-[ee-2760]: https://gitlab.com/gitlab-org/gitlab/-/merge_requests/2760
diff --git a/doc/administration/libravatar.md b/doc/administration/libravatar.md
index c28e701dc25..e1c38b3409f 100644
--- a/doc/administration/libravatar.md
+++ b/doc/administration/libravatar.md
@@ -19,7 +19,7 @@ the configuration options as follows:
### For HTTP
-```yml
+```yaml
gravatar:
enabled: true
# gravatar URLs: possible placeholders: %{hash} %{size} %{email} %{username}
@@ -28,7 +28,7 @@ the configuration options as follows:
### For HTTPS
-```yml
+```yaml
gravatar:
enabled: true
# gravatar URLs: possible placeholders: %{hash} %{size} %{email} %{username}
diff --git a/doc/administration/logs.md b/doc/administration/logs.md
index e4a3514a539..57b12897979 100644
--- a/doc/administration/logs.md
+++ b/doc/administration/logs.md
@@ -1,3 +1,9 @@
+---
+stage: Monitor
+group: APM
+info: To determine the technical writer assigned to the Stage/Group associated with this page, see https://about.gitlab.com/handbook/engineering/ux/technical-writing/#designated-technical-writers
+---
+
# Log system
GitLab has an advanced log system where everything is logged, so you
@@ -333,6 +339,9 @@ only. For example:
## `audit_json.log`
+NOTE: **Note:**
+Most log entries only exist in [GitLab Starter](https://about.gitlab.com/pricing), however a few exist in GitLab Core.
+
This file lives in `/var/log/gitlab/gitlab-rails/audit_json.log` for
Omnibus GitLab packages or in `/home/git/gitlab/log/audit_json.log` for
installations from source.
@@ -356,7 +365,11 @@ Changes to group or project settings are logged to this file. For example:
}
```
-## `sidekiq.log`
+## Sidekiq Logs
+
+For Omnibus installations, some Sidekiq logs reside in `/var/log/gitlab/sidekiq/current` and as follows.
+
+### `sidekiq.log`
This file lives in `/var/log/gitlab/gitlab-rails/sidekiq.log` for
Omnibus GitLab packages or in `/home/git/gitlab/log/sidekiq.log` for
@@ -413,7 +426,7 @@ For source installations, edit the `gitlab.yml` and set the Sidekiq
log_format: json
```
-## `sidekiq_client.log`
+### `sidekiq_client.log`
> [Introduced](https://gitlab.com/gitlab-org/gitlab/-/merge_requests/26586) in GitLab 12.9.
@@ -424,21 +437,91 @@ installations from source.
This file contains logging information about jobs before they are start
being processed by Sidekiq, for example before being enqueued.
-This logfile follows the same structure as
+This log file follows the same structure as
[`sidekiq.log`](#sidekiqlog), so it will be structured as JSON if
you've configured this for Sidekiq as mentioned above.
## `gitlab-shell.log`
-This file lives in `/var/log/gitlab/gitaly/gitlab-shell.log` for
-Omnibus GitLab packages or in `/home/git/gitaly/gitlab-shell.log` for
-installations from source.
+GitLab Shell is used by GitLab for executing Git commands and provide SSH access to Git repositories.
+
+### For GitLab versions 12.10 and up
+
+For GitLab version 12.10 and later, there are 2 `gitlab-shell.log` files. Information containing `git-{upload-pack,receive-pack}` requests lives in `/var/log/gitlab/gitlab-shell/gitlab-shell.log`. Information about hooks to GitLab Shell from Gitaly lives in `/var/log/gitlab/gitaly/gitlab-shell.log`.
+
+Example log entries for `/var/log/gitlab/gitlab-shell/gitlab-shell.log`:
+
+```json
+{
+ "duration_ms": 74.104,
+ "level": "info",
+ "method": "POST",
+ "msg": "Finished HTTP request",
+ "time": "2020-04-17T20:28:46Z",
+ "url": "http://127.0.0.1:8080/api/v4/internal/allowed"
+}
+{
+ "command": "git-upload-pack",
+ "git_protocol": "",
+ "gl_project_path": "root/example",
+ "gl_repository": "project-1",
+ "level": "info",
+ "msg": "executing git command",
+ "time": "2020-04-17T20:28:46Z",
+ "user_id": "user-1",
+ "username": "root"
+}
+```
+
+Example log entries for `/var/log/gitlab/gitaly/gitlab-shell.log`:
+
+```json
+{
+ "method": "POST",
+ "url": "http://127.0.0.1:8080/api/v4/internal/allowed",
+ "duration": 0.058012959,
+ "gitaly_embedded": true,
+ "pid": 16636,
+ "level": "info",
+ "msg": "finished HTTP request",
+ "time": "2020-04-17T20:29:08+00:00"
+}
+{
+ "method": "POST",
+ "url": "http://127.0.0.1:8080/api/v4/internal/pre_receive",
+ "duration": 0.031022552,
+ "gitaly_embedded": true,
+ "pid": 16636,
+ "level": "info",
+ "msg": "finished HTTP request",
+ "time": "2020-04-17T20:29:08+00:00"
+}
+```
+
+### For GitLab versions 12.5 through 12.9
+
+For GitLab 12.5 to 12.9, this file lives in `/var/log/gitlab/gitaly/gitlab-shell.log` for Omnibus GitLab packages or in `/home/git/gitaly/gitlab-shell.log` for installations from source.
+
+Example log entries:
+
+```json
+{
+ "method": "POST",
+ "url": "http://127.0.0.1:8080/api/v4/internal/post_receive",
+ "duration": 0.031809164,
+ "gitaly_embedded": true,
+ "pid": 27056,
+ "level": "info",
+ "msg": "finished HTTP request",
+ "time": "2020-04-17T16:24:38+00:00"
+}
+```
+
+### For GitLab 12.5 and earlier
-NOTE: **Note:**
For GitLab 12.5 and earlier, the file lives in `/var/log/gitlab/gitlab-shell/gitlab-shell.log`.
-GitLab Shell is used by GitLab for executing Git commands and provide
-SSH access to Git repositories. For example:
+Example log entries:
```plaintext
I, [2015-02-13T06:17:00.671315 #9291] INFO -- : Adding project root/example.git at </var/opt/gitlab/git-data/repositories/root/dcdcdcdcd.git>.
@@ -447,15 +530,29 @@ I, [2015-02-13T06:17:00.679433 #9291] INFO -- : Moving existing hooks directory
User clone/fetch activity using SSH transport appears in this log as `executing git command <gitaly-upload-pack...`.
-## `current`
+## Gitaly Logs
-This file lives in `/var/log/gitlab/gitaly/current` and is produced by [runit](http://smarden.org/runit/). `runit` is packaged with Omnibus and a brief explanation of its purpose is available [in the omnibus documentation](https://docs.gitlab.com/omnibus/architecture/#runit). [Log files are rotated](http://smarden.org/runit/svlogd.8.html), renamed in unix timestamp format and `gzip`-compressed (e.g. `@1584057562.s`).
+This file lives in `/var/log/gitlab/gitaly/current` and is produced by [runit](http://smarden.org/runit/). `runit` is packaged with Omnibus and a brief explanation of its purpose is available [in the omnibus documentation](https://docs.gitlab.com/omnibus/architecture/#runit). [Log files are rotated](http://smarden.org/runit/svlogd.8.html), renamed in Unix timestamp format and `gzip`-compressed (e.g. `@1584057562.s`).
-## `unicorn_stderr.log`
+### `grpc.log`
-This file lives in `/var/log/gitlab/unicorn/unicorn_stderr.log` for
-Omnibus GitLab packages or in `/home/git/gitlab/log/unicorn_stderr.log` for
-installations from source.
+This file lives in `/var/log/gitlab/gitlab-rails/grpc.log` for Omnibus GitLab packages. Native [gRPC](https://grpc.io/) logging used by Gitaly.
+
+## `puma_stderr.log` & `puma_stdout.log`
+
+This file lives in `/var/log/gitlab/puma/puma_stderr.log` and `/var/log/gitlab/puma/puma_stdout.log` for
+Omnibus GitLab packages or in `/home/git/gitlab/log/puma_stderr.log` and `/home/git/gitlab/log/puma_stdout.log`
+for installations from source.
+
+## `unicorn_stderr.log` & `unicorn_stdout.log`
+
+NOTE: **Note:**
+Starting with GitLab 13.0, Puma is the default web server used in GitLab
+all-in-one package based installations as well as GitLab Helm chart deployments.
+
+This file lives in `/var/log/gitlab/unicorn/unicorn_stderr.log` and `/var/log/gitlab/unicorn/unicorn_stdout.log` for
+Omnibus GitLab packages or in `/home/git/gitlab/log/unicorn_stderr.log` and `/home/git/gitlab/log/unicorn_stdout.log`
+for installations from source.
Unicorn is a high-performance forking Web server which is used for
serving the GitLab application. You can look at this log if, for
@@ -484,7 +581,7 @@ This file lives in `/var/log/gitlab/gitlab-rails/repocheck.log` for
Omnibus GitLab packages or in `/home/git/gitlab/log/repocheck.log` for
installations from source.
-It logs information whenever a [repository check is run][repocheck] on a project.
+It logs information whenever a [repository check is run](repository_checks.md) on a project.
## `importer.log`
@@ -494,6 +591,8 @@ This file lives in `/var/log/gitlab/gitlab-rails/importer.log` for
Omnibus GitLab packages or in `/home/git/gitlab/log/importer.log` for
installations from source.
+It logs the progress of the import process.
+
## `auth.log`
> Introduced in GitLab 12.0.
@@ -504,9 +603,9 @@ installations from source.
This log records:
-- Information whenever [Rack Attack] registers an abusive request.
-- Requests over the [Rate Limit] on raw endpoints.
-- [Protected paths] abusive requests.
+- Information whenever [Rack Attack](../security/rack_attack.md) registers an abusive request.
+- Requests over the [Rate Limit](../user/admin_area/settings/rate_limits_on_raw_endpoints.md) on raw endpoints.
+- [Protected paths](../user/admin_area/settings/protected_paths.md) abusive requests.
NOTE: **Note:**
From [%12.1](https://gitlab.com/gitlab-org/gitlab-foss/issues/62756), user ID and username are also
@@ -523,7 +622,7 @@ installations from source.
GraphQL queries are recorded in that file. For example:
```json
-{"query_string":"query IntrospectionQuery{__schema {queryType { name },mutationType { name }}}...(etc)","variables":{"a":1,"b":2},"complexity":181,"depth":1,"duration":7}
+{"query_string":"query IntrospectionQuery{__schema {queryType { name },mutationType { name }}}...(etc)","variables":{"a":1,"b":2},"complexity":181,"depth":1,"duration_s":7}
```
## `migrations.log`
@@ -538,7 +637,7 @@ installations from source.
> [Introduced](https://gitlab.com/gitlab-org/gitlab/-/merge_requests/19186) in GitLab 12.6.
-This file lives in `/var/log/gitlab/mail_room/mail_room_json.log` for
+This file lives in `/var/log/gitlab/mailroom/mail_room_json.log` for
Omnibus GitLab packages or in `/home/git/gitlab/log/mail_room_json.log` for
installations from source.
@@ -562,7 +661,7 @@ will be generated in `/var/log/gitlab/gitlab-rails/sidekiq_exporter.log` for
Omnibus GitLab packages or in `/home/git/gitlab/log/sidekiq_exporter.log` for
installations from source.
-If Prometheus metrics and the Web Exporter are both enabled, Unicorn/Puma will
+If Prometheus metrics and the Web Exporter are both enabled, Puma/Unicorn will
start a Web server and listen to the defined port (default: `8083`). Access logs
will be generated in `/var/log/gitlab/gitlab-rails/web_exporter.log` for
Omnibus GitLab packages or in `/home/git/gitlab/log/web_exporter.log` for
@@ -578,7 +677,7 @@ It's stored at:
- `/var/log/gitlab/gitlab-rails/database_load_balancing.log` for Omnibus GitLab packages.
- `/home/git/gitlab/log/database_load_balancing.log` for installations from source.
-## `elasticsearch.log`
+## `elasticsearch.log` **(STARTER ONLY)**
> Introduced in GitLab 12.6.
@@ -648,7 +747,24 @@ Each line contains a JSON line that can be ingested by Elasticsearch. For exampl
}
```
-## `geo.log`
+## `service_measurement.log`
+
+> Introduced in GitLab 13.0.
+
+This file lives in `/var/log/gitlab/gitlab-rails/service_measurement.log` for
+Omnibus GitLab packages or in `/home/git/gitlab/log/service_measurement.log` for
+installations from source.
+
+It contain only a single structured log with measurements for each service execution.
+It will contain measurement such as: number of sql calls, execution_time, gc_stats, memory usage, etc...
+
+For example:
+
+```json
+{ "severity":"INFO", "time":"2020-04-22T16:04:50.691Z","correlation_id":"04f1366e-57a1-45b8-88c1-b00b23dc3616","class":"Projects::ImportExport::ExportService","current_user":"John Doe","project_full_path":"group1/test-export","file_path":"/path/to/archive","gc_stats":{"count":{"before":127,"after":127,"diff":0},"heap_allocated_pages":{"before":10369,"after":10369,"diff":0},"heap_sorted_length":{"before":10369,"after":10369,"diff":0},"heap_allocatable_pages":{"before":0,"after":0,"diff":0},"heap_available_slots":{"before":4226409,"after":4226409,"diff":0},"heap_live_slots":{"before":2542709,"after":2641420,"diff":98711},"heap_free_slots":{"before":1683700,"after":1584989,"diff":-98711},"heap_final_slots":{"before":0,"after":0,"diff":0},"heap_marked_slots":{"before":2542704,"after":2542704,"diff":0},"heap_eden_pages":{"before":10369,"after":10369,"diff":0},"heap_tomb_pages":{"before":0,"after":0,"diff":0},"total_allocated_pages":{"before":10369,"after":10369,"diff":0},"total_freed_pages":{"before":0,"after":0,"diff":0},"total_allocated_objects":{"before":24896308,"after":24995019,"diff":98711},"total_freed_objects":{"before":22353599,"after":22353599,"diff":0},"malloc_increase_bytes":{"before":140032,"after":6650240,"diff":6510208},"malloc_increase_bytes_limit":{"before":25804104,"after":25804104,"diff":0},"minor_gc_count":{"before":94,"after":94,"diff":0},"major_gc_count":{"before":33,"after":33,"diff":0},"remembered_wb_unprotected_objects":{"before":34284,"after":34284,"diff":0},"remembered_wb_unprotected_objects_limit":{"before":68568,"after":68568,"diff":0},"old_objects":{"before":2404725,"after":2404725,"diff":0},"old_objects_limit":{"before":4809450,"after":4809450,"diff":0},"oldmalloc_increase_bytes":{"before":140032,"after":6650240,"diff":6510208},"oldmalloc_increase_bytes_limit":{"before":68537556,"after":68537556,"diff":0}},"time_to_finish":0.12298400001600385,"number_of_sql_calls":70,"memory_usage":"0.0 MiB","label":"process_48616"}
+```
+
+## `geo.log` **(PREMIUM ONLY)**
> Introduced in 9.5.
@@ -677,7 +793,9 @@ For Omnibus installations, NGINX logs reside in:
- `/var/log/gitlab/nginx/gitlab_pages_access.log` contains a log of requests made to Pages static sites.
- `/var/log/gitlab/nginx/gitlab_pages_error.log` contains a log of NGINX errors for Pages static sites.
- `/var/log/gitlab/nginx/gitlab_registry_access.log` contains a log of requests made to the Container Registry.
-- `/var/log/gitlab/nginx/gitlab_registry_error.log` contains a log of NGINX errors for the Container Regsitry.
+- `/var/log/gitlab/nginx/gitlab_registry_error.log` contains a log of NGINX errors for the Container Registry.
+- `/var/log/gitlab/nginx/gitlab_mattermost_access.log` contains a log of requests made to Mattermost.
+- `/var/log/gitlab/nginx/gitlab_mattermost_error.log` contains a log of NGINX errors for Mattermost.
Below is the default GitLab NGINX access log format:
@@ -685,7 +803,51 @@ Below is the default GitLab NGINX access log format:
$remote_addr - $remote_user [$time_local] "$request" $status $body_bytes_sent "$http_referer" "$http_user_agent"
```
-[repocheck]: repository_checks.md
-[Rack Attack]: ../security/rack_attack.md
-[Rate Limit]: ../user/admin_area/settings/rate_limits_on_raw_endpoints.md
-[Protected paths]: ../user/admin_area/settings/protected_paths.md
+## Pages Logs
+
+For Omnibus installations, Pages logs reside in `/var/log/gitlab/gitlab-pages/current`.
+
+For example:
+
+```json
+{
+ "level": "info",
+ "msg": "GitLab Pages Daemon",
+ "revision": "52b2899",
+ "time": "2020-04-22T17:53:12Z",
+ "version": "1.17.0"
+}
+{
+ "level": "info",
+ "msg": "URL: https://gitlab.com/gitlab-org/gitlab-pages",
+ "time": "2020-04-22T17:53:12Z"
+}
+{
+ "gid": 998,
+ "in-place": false,
+ "level": "info",
+ "msg": "running the daemon as unprivileged user",
+ "time": "2020-04-22T17:53:12Z",
+ "uid": 998
+}
+```
+
+## Workhorse Logs
+
+For Omnibus installations, Workhorse logs reside in `/var/log/gitlab/gitlab-workhorse/current`.
+
+## PostgreSQL Logs
+
+For Omnibus installations, PostgreSQL logs reside in `/var/log/gitlab/postgresql/current`.
+
+## Prometheus Logs
+
+For Omnibus installations, Prometheus logs reside in `/var/log/gitlab/prometheus/current`.
+
+## Redis Logs
+
+For Omnibus installations, Redis logs reside in `/var/log/gitlab/redis/current`.
+
+## Mattermost Logs
+
+For Omnibus installations, Mattermost logs reside in `/var/log/gitlab/mattermost/mattermost.log`.
diff --git a/doc/administration/monitoring/gitlab_self_monitoring_project/index.md b/doc/administration/monitoring/gitlab_self_monitoring_project/index.md
index 884bb44cfda..091da46f316 100644
--- a/doc/administration/monitoring/gitlab_self_monitoring_project/index.md
+++ b/doc/administration/monitoring/gitlab_self_monitoring_project/index.md
@@ -15,7 +15,11 @@ All administrators at the time of creation of the project and group will be adde
as maintainers of the group and project, and as an admin, you'll be able to add new
members to the group in order to give them maintainer access to the project.
-This project will be used for self monitoring your GitLab instance.
+This project is used to self monitor your GitLab instance. Metrics are not yet
+fully integrated, and the dashboard does not aggregate any data on Omnibus installations. GitLab plans
+to provide integrated self-monitoring metrics in a future release. You can
+currently use the project to configure your own [custom metrics](../../../user/project/integrations/prometheus.md#adding-custom-metrics) using
+metrics exposed by the [GitLab exporter](../prometheus/gitlab_metrics.md#metrics-available).
## Creating the self monitoring project
@@ -42,7 +46,7 @@ The project will be automatically configured to connect to the
instance is present (should be the case if GitLab was installed via Omnibus
and you haven't disabled it).
-If that's not the case or if you have an external Prometheus instance or an HA setup,
+If that's not the case or if you have an external Prometheus instance or a customized setup,
you should
[configure it manually](../../../user/project/integrations/prometheus.md#manual-configuration-of-prometheus).
@@ -70,7 +74,7 @@ project creation to fail with the following error (which appears in the log file
when the first admin user is an
[external user](../../../user/permissions.md#external-users-core-only):
-```text
+```plaintext
Could not create instance administrators group. Errors: ["You don’t have permission to create groups."]
```
diff --git a/doc/administration/monitoring/performance/gitlab_configuration.md b/doc/administration/monitoring/performance/gitlab_configuration.md
index e8a6c661464..14119a5d8f3 100644
--- a/doc/administration/monitoring/performance/gitlab_configuration.md
+++ b/doc/administration/monitoring/performance/gitlab_configuration.md
@@ -1,17 +1,9 @@
# GitLab Configuration
-CAUTION: **InfluxDB is deprecated in favor of Prometheus:**
-InfluxDB support is scheduled to be removed in GitLab 13.0.
-You are advised to use [Prometheus](../prometheus/index.md) instead.
-
GitLab Performance Monitoring is disabled by default. To enable it and change any of its
settings, navigate to **Admin Area > Settings > Metrics and profiling**
(`/admin/application_settings/metrics_and_profiling`).
-The minimum required settings you need to set are the InfluxDB host and port.
-Make sure _Enable InfluxDB Metrics_ is checked and hit **Save** to save the
-changes.
-
![GitLab Performance Monitoring Admin Settings](img/metrics_gitlab_configuration_settings.png)
Finally, a restart of all GitLab processes is required for the changes to take
@@ -33,6 +25,4 @@ have been performed.
Read more on:
- [Introduction to GitLab Performance Monitoring](index.md)
-- [InfluxDB Configuration](influxdb_configuration.md)
-- [InfluxDB Schema](influxdb_schema.md)
- [Grafana Install/Configuration](grafana_configuration.md)
diff --git a/doc/administration/monitoring/performance/grafana_configuration.md b/doc/administration/monitoring/performance/grafana_configuration.md
index 3c26e399c7f..3438a564d2f 100644
--- a/doc/administration/monitoring/performance/grafana_configuration.md
+++ b/doc/administration/monitoring/performance/grafana_configuration.md
@@ -1,21 +1,12 @@
# Grafana Configuration
-CAUTION: **InfluxDB is deprecated in favor of Prometheus:**
-InfluxDB support is scheduled to be removed in GitLab 13.0.
-You are advised to use [Prometheus](../prometheus/index.md) instead.
-
[Grafana](https://grafana.com/) is a tool that allows you to visualize time
-series metrics through graphs and dashboards. It supports several backend
-data stores, including InfluxDB. GitLab writes performance data to InfluxDB
+series metrics through graphs and dashboards. GitLab writes performance data to Prometheus
and Grafana will allow you to query to display useful graphs.
-For the easiest installation and configuration, install Grafana on the same
-server as InfluxDB. For larger installations, you may want to split out these
-services.
-
## Installation
-[GitLab Omnibus can help you install Grafana (recommended)](https://docs.gitlab.com/omnibus/settings/grafana.html)
+[Omnibus GitLab can help you install Grafana (recommended)](https://docs.gitlab.com/omnibus/settings/grafana.html)
or Grafana supplies package repositories (Yum/Apt) for easy installation.
See [Grafana installation documentation](https://grafana.com/docs/grafana/latest/installation/)
for detailed steps.
@@ -33,49 +24,8 @@ in the top bar.
![Grafana empty data source page](img/grafana_data_source_empty.png)
-Fill in the configuration details for the InfluxDB data source. Save and
-Test Connection to ensure the configuration is correct.
-
-- **Name**: `InfluxDB`
-- **Default**: Checked
-- **Type**: `InfluxDB 0.9.x` (Even if you're using InfluxDB 0.10.x)
-- For the URL, use `https://localhost:8086`, or provide the remote URL if you've installed InfluxDB
- on a separate server
-- **Access**: `proxy`
-- **Database**: `gitlab`
-- **User**: `admin` (Or the username configured when setting up InfluxDB)
-- **Password**: The password configured when you set up InfluxDB
-
![Grafana data source configurations](img/grafana_data_source_configuration.png)
-## Apply retention policies and create continuous queries
-
-If you intend to import the GitLab provided Grafana dashboards, you will need to
-set up the right retention policies and continuous queries. The easiest way of
-doing this is by using the [InfluxDB Management](https://gitlab.com/gitlab-org/influxdb-management)
-repository.
-
-To use this repository you must first clone it:
-
-```shell
-git clone https://gitlab.com/gitlab-org/influxdb-management.git
-cd influxdb-management
-```
-
-Next you must install the required dependencies:
-
-```shell
-gem install bundler
-bundle install
-```
-
-Now you must configure the repository by first copying `.env.example` to `.env`
-and then editing the `.env` file to contain the correct InfluxDB settings. Once
-configured you can simply run `bundle exec rake` and the InfluxDB database will
-be configured for you.
-
-For more information see the [InfluxDB Management README](https://gitlab.com/gitlab-org/influxdb-management/blob/master/README.md).
-
## Import Dashboards
You can now import a set of default dashboards that will give you a good
@@ -164,5 +114,3 @@ Read more on:
- [Introduction to GitLab Performance Monitoring](index.md)
- [GitLab Configuration](gitlab_configuration.md)
-- [InfluxDB Installation/Configuration](influxdb_configuration.md)
-- [InfluxDB Schema](influxdb_schema.md)
diff --git a/doc/administration/monitoring/performance/index.md b/doc/administration/monitoring/performance/index.md
index 3305ea33f2e..02070287611 100644
--- a/doc/administration/monitoring/performance/index.md
+++ b/doc/administration/monitoring/performance/index.md
@@ -1,9 +1,5 @@
# GitLab Performance Monitoring
-CAUTION: **InfluxDB is deprecated in favor of Prometheus:**
-InfluxDB support is scheduled to be removed in GitLab 13.0.
-You are advised to use [Prometheus](../prometheus/index.md) instead.
-
GitLab comes with its own application performance measuring system as of GitLab
8.4, simply called "GitLab Performance Monitoring". GitLab Performance Monitoring is available in both the
Community and Enterprise editions.
@@ -12,17 +8,11 @@ Apart from this introduction, you are advised to read through the following
documents in order to understand and properly configure GitLab Performance Monitoring:
- [GitLab Configuration](gitlab_configuration.md)
-- [InfluxDB Install/Configuration](influxdb_configuration.md)
-- [InfluxDB Schema](influxdb_schema.md)
+- [Prometheus documentation](../prometheus/index.md)
- [Grafana Install/Configuration](grafana_configuration.md)
- [Performance bar](performance_bar.md)
- [Request profiling](request_profiling.md)
-NOTE: **Note:**
-Omnibus GitLab 8.16 includes Prometheus as an additional tool to collect
-metrics. It will eventually replace InfluxDB when their metrics collection is
-on par. Read more in the [Prometheus documentation](../prometheus/index.md).
-
## Introduction to GitLab Performance Monitoring
GitLab Performance Monitoring makes it possible to measure a wide variety of statistics
@@ -35,12 +25,6 @@ including (but not limited to):
- System statistics such as the process' memory usage and open file descriptors.
- Ruby garbage collection statistics.
-Metrics data is written to [InfluxDB](https://www.influxdata.com/products/influxdb-overview/)
-over [UDP](https://docs.influxdata.com/influxdb/v0.9/write_protocols/udp/).
-Stored data can be visualized using [Grafana](https://grafana.com) or any other application that
-supports reading data from InfluxDB. Alternatively data can be queried using the
-InfluxDB CLI.
-
## Metric Types
Two types of metrics are collected:
diff --git a/doc/administration/monitoring/performance/influxdb_configuration.md b/doc/administration/monitoring/performance/influxdb_configuration.md
index 8478272897b..d793e014a18 100644
--- a/doc/administration/monitoring/performance/influxdb_configuration.md
+++ b/doc/administration/monitoring/performance/influxdb_configuration.md
@@ -1,191 +1,5 @@
-# InfluxDB Configuration
-
-CAUTION: **InfluxDB is deprecated in favor of Prometheus:**
-InfluxDB support is scheduled to be removed in GitLab 13.0.
-You are advised to use [Prometheus](../prometheus/index.md) instead.
-
-The default settings provided by [InfluxDB](https://www.influxdata.com/products/influxdb-overview/)
-are not sufficient for a high traffic GitLab environment. The settings discussed in
-this document are based on the settings GitLab uses for GitLab.com. Depending on
-your own needs, you may need to further adjust them.
-
-If you are intending to run InfluxDB on the same server as GitLab, make sure
-you have plenty of RAM since InfluxDB can use quite a bit depending on traffic.
-
-Unless you are going with a budget setup, it's advised to run it separately.
-
-## Requirements
-
-- InfluxDB 0.9.5 or newer
-- A fairly modern version of Linux
-- At least 4GB of RAM
-- At least 10GB of storage for InfluxDB data
-
-Note that the RAM and storage requirements can differ greatly depending on the
-amount of data received/stored. To limit the amount of stored data users can
-look into
-[InfluxDB Retention Policies](https://docs.influxdata.com/influxdb/v0.9/query_language/database_management/#retention-policy-management).
-
-## Installation
-
-Installing InfluxDB is out of the scope of this document. Please refer to the
-[InfluxDB documentation](https://docs.influxdata.com/influxdb/v0.9/).
-
-## InfluxDB Server Settings
-
-Since InfluxDB has many settings that users may wish to customize themselves
-(e.g. what port to run InfluxDB on), we'll only cover the essentials.
-
-The configuration file in question is usually located at
-`/etc/influxdb/influxdb.conf`. Whenever you make a change in this file,
-InfluxDB needs to be restarted.
-
-### Storage Engine
-
-InfluxDB comes with different storage engines and as of InfluxDB 0.9.5 a new
-storage engine is available, called [TSM Tree](https://www.influxdata.com/blog/new-storage-engine-time-structured-merge-tree/).
-All users **must** use the new `tsm1` storage engine as this
-[will be the default engine](https://github.com/influxdata/influxdb/commit/15d723dc77651bac83e09e2b1c94be480966cb0d) in
-upcoming InfluxDB releases.
-
-Make sure you have the following in your configuration file:
-
-```toml
-[data]
- dir = "/var/lib/influxdb/data"
- engine = "tsm1"
-```
-
-### Admin Panel
-
-Production environments should have the InfluxDB admin panel **disabled**. This
-feature can be disabled by adding the following to your InfluxDB configuration
-file:
-
-```toml
-[admin]
- enabled = false
-```
-
-### HTTP
-
-HTTP is required when using the [InfluxDB CLI](https://docs.influxdata.com/influxdb/v0.9/tools/shell/)
-or other tools such as Grafana, thus it should be enabled. When enabling
-make sure to _also_ enable authentication:
-
-```toml
-[http]
- enabled = true
- auth-enabled = true
-```
-
-_**Note:** Before you enable authentication, you might want to [create an
-admin user](#create-a-new-admin-user)._
-
-### UDP
-
-GitLab writes data to InfluxDB via UDP and thus this must be enabled. Enabling
-UDP can be done using the following settings:
-
-```toml
-[[udp]]
- enabled = true
- bind-address = ":8089"
- database = "gitlab"
- batch-size = 1000
- batch-pending = 5
- batch-timeout = "1s"
- read-buffer = 209715200
-```
-
-This does the following:
-
-1. Enable UDP and bind it to port 8089 for all addresses.
-1. Store any data received in the `gitlab` database.
-1. Define a batch of points to be 1000 points in size and allow a maximum of
- 5 batches _or_ flush them automatically after 1 second.
-1. Define a UDP read buffer size of 200 MB.
-
-One of the most important settings here is the UDP read buffer size as if this
-value is set too low, packets will be dropped. You must also make sure the OS
-buffer size is set to the same value, the default value is almost never enough.
-
-To set the OS buffer size to 200 MB, on Linux you can run the following command:
-
-```shell
-sysctl -w net.core.rmem_max=209715200
-```
-
-To make this permanent, add the following to `/etc/sysctl.conf` and restart the
-server:
-
-```shell
-net.core.rmem_max=209715200
-```
-
-It is **very important** to make sure the buffer sizes are large enough to
-handle all data sent to InfluxDB as otherwise you _will_ lose data. The above
-buffer sizes are based on the traffic for GitLab.com. Depending on the amount of
-traffic, users may be able to use a smaller buffer size, but we highly recommend
-using _at least_ 100 MB.
-
-When enabling UDP, users should take care to not expose the port to the public,
-as doing so will allow anybody to write data into your InfluxDB database (as
-[InfluxDB's UDP protocol](https://docs.influxdata.com/influxdb/v0.9/write_protocols/udp/)
-doesn't support authentication). We recommend either
-whitelisting the allowed IP addresses/ranges, or setting up a VLAN and only
-allowing traffic from members of said VLAN.
-
-## Create a new admin user
-
-If you want to [enable authentication](#http), you might want to [create an
-admin user](https://docs.influxdata.com/influxdb/v0.9/administration/authentication_and_authorization/#create-a-new-admin-user):
-
-```shell
-influx -execute "CREATE USER jeff WITH PASSWORD '1234' WITH ALL PRIVILEGES"
-```
-
-## Create the `gitlab` database
-
-Once you get InfluxDB up and running, you need to create a database for GitLab.
-Make sure you have changed the [storage engine](#storage-engine) to `tsm1`
-before creating a database.
-
-_**Note:** If you [created an admin user](#create-a-new-admin-user) and enabled
-[HTTP authentication](#http), remember to append the username (`-username <username>`)
-and password (`-password <password>`) you set earlier to the commands below._
-
-Run the following command to create a database named `gitlab`:
-
-```shell
-influx -execute 'CREATE DATABASE gitlab'
-```
-
-The name **must** be `gitlab`, do not use any other name.
-
-Next, make sure that the database was successfully created:
-
-```shell
-influx -execute 'SHOW DATABASES'
-```
-
-The output should be similar to:
-
-```plaintext
-name: databases
----------------
-name
-_internal
-gitlab
-```
-
-That's it! Now your GitLab instance should send data to InfluxDB.
-
+---
+redirect_to: 'prometheus.md'
---
-Read more on:
-
-- [Introduction to GitLab Performance Monitoring](index.md)
-- [GitLab Configuration](gitlab_configuration.md)
-- [InfluxDB Schema](influxdb_schema.md)
-- [Grafana Install/Configuration](grafana_configuration.md)
+Support for InfluxDB was removed in GitLab 13.0. Use [Prometheus](prometheus.md) for performance monitoring.
diff --git a/doc/administration/monitoring/performance/influxdb_schema.md b/doc/administration/monitoring/performance/influxdb_schema.md
index adbccdaaeb8..d793e014a18 100644
--- a/doc/administration/monitoring/performance/influxdb_schema.md
+++ b/doc/administration/monitoring/performance/influxdb_schema.md
@@ -1,101 +1,5 @@
-# InfluxDB Schema
-
-CAUTION: **InfluxDB is deprecated in favor of Prometheus:**
-InfluxDB support is scheduled to be removed in GitLab 13.0.
-You are advised to use [Prometheus](../prometheus/index.md) instead.
-
-The following measurements are currently stored in InfluxDB:
-
-- `PROCESS_file_descriptors`
-- `PROCESS_gc_statistics`
-- `PROCESS_memory_usage`
-- `PROCESS_method_calls`
-- `PROCESS_object_counts`
-- `PROCESS_transactions`
-- `PROCESS_views`
-- `events`
-
-Here, `PROCESS` is replaced with either `rails` or `sidekiq` depending on the
-process type. In all series, any form of duration is stored in milliseconds.
-
-## PROCESS_file_descriptors
-
-This measurement contains the number of open file descriptors over time. The
-value field `value` contains the number of descriptors.
-
-## PROCESS_gc_statistics
-
-This measurement contains Ruby garbage collection statistics such as the amount
-of minor/major GC runs (relative to the last sampling interval), the time spent
-in garbage collection cycles, and all fields/values returned by `GC.stat`.
-
-## PROCESS_memory_usage
-
-This measurement contains the process' memory usage (in bytes) over time. The
-value field `value` contains the number of bytes.
-
-## PROCESS_method_calls
-
-This measurement contains the methods called during a transaction along with
-their duration, and a name of the transaction action that invoked the method (if
-available). The method call duration is stored in the value field `duration`,
-while the method name is stored in the tag `method`. The tag `action` contains
-the full name of the transaction action. Both the `method` and `action` fields
-are in the following format:
-
-```plaintext
-ClassName#method_name
-```
-
-For example, a method called by the `show` method in the `UsersController` class
-would have `action` set to `UsersController#show`.
-
-## PROCESS_object_counts
-
-This measurement is used to store retained Ruby objects (per class) and the
-amount of retained objects. The number of objects is stored in the `count` value
-field while the class name is stored in the `type` tag.
-
-## PROCESS_transactions
-
-This measurement is used to store basic transaction details such as the time it
-took to complete a transaction, how much time was spent in SQL queries, etc. The
-following value fields are available:
-
-| Value | Description |
-| ----- | ----------- |
-| `duration` | The total duration of the transaction |
-| `allocated_memory` | The amount of bytes allocated while the transaction was running. This value is only reliable when using single-threaded application servers |
-| `method_duration` | The total time spent in method calls |
-| `sql_duration` | The total time spent in SQL queries |
-| `view_duration` | The total time spent in views |
-
-## PROCESS_views
-
-This measurement is used to store view rendering timings for a transaction. The
-following value fields are available:
-
-| Value | Description |
-| ----- | ----------- |
-| `duration` | The rendering time of the view |
-| `view` | The path of the view, relative to the application's root directory |
-
-The `action` tag contains the action name of the transaction that rendered the
-view.
-
-## events
-
-This measurement is used to store generic events such as the number of Git
-pushes, Emails sent, etc. Each point in this measurement has a single value
-field called `count`. The value of this field is simply set to `1`. Each point
-also has at least one tag: `event`. This tag's value is set to the event name.
-Depending on the event type additional tags may be available as well.
-
+---
+redirect_to: 'prometheus.md'
---
-Read more on:
-
-- [Introduction to GitLab Performance Monitoring](index.md)
-- [GitLab Configuration](gitlab_configuration.md)
-- [InfluxDB Configuration](influxdb_configuration.md)
-- [Grafana Install/Configuration](grafana_configuration.md)
+Support for InfluxDB was removed in GitLab 13.0. Use [Prometheus](prometheus.md) for performance monitoring.
diff --git a/doc/administration/monitoring/prometheus/gitlab_exporter.md b/doc/administration/monitoring/prometheus/gitlab_exporter.md
index 9c5c67e7f67..3effca4a2bf 100644
--- a/doc/administration/monitoring/prometheus/gitlab_exporter.md
+++ b/doc/administration/monitoring/prometheus/gitlab_exporter.md
@@ -1,3 +1,9 @@
+---
+stage: Monitor
+group: APM
+info: To determine the technical writer assigned to the Stage/Group associated with this page, see https://about.gitlab.com/handbook/engineering/ux/technical-writing/#designated-technical-writers
+---
+
# GitLab exporter
>- Available since [Omnibus GitLab 8.17](https://gitlab.com/gitlab-org/omnibus-gitlab/-/merge_requests/1132).
diff --git a/doc/administration/monitoring/prometheus/gitlab_metrics.md b/doc/administration/monitoring/prometheus/gitlab_metrics.md
index 0833412ecb8..f725db9a039 100644
--- a/doc/administration/monitoring/prometheus/gitlab_metrics.md
+++ b/doc/administration/monitoring/prometheus/gitlab_metrics.md
@@ -1,3 +1,9 @@
+---
+stage: Monitor
+group: APM
+info: To determine the technical writer assigned to the Stage/Group associated with this page, see https://about.gitlab.com/handbook/engineering/ux/technical-writing/#designated-technical-writers
+---
+
# GitLab Prometheus metrics
>**Note:**
@@ -31,16 +37,17 @@ The following metrics are available:
| Metric | Type | Since | Description | Labels |
|:---------------------------------------------------------------|:----------|-----------------------:|:----------------------------------------------------------------------------------------------------|:----------------------------------------------------|
-| `gitlab_banzai_cached_render_real_duration_seconds` | Histogram | 9.4 | Duration of rendering Markdown into HTML when cached output exists | controller, action |
-| `gitlab_banzai_cacheless_render_real_duration_seconds` | Histogram | 9.4 | Duration of rendering Markdown into HTML when cached output does not exist | controller, action |
-| `gitlab_cache_misses_total` | Counter | 10.2 | Cache read miss | controller, action |
+| `gitlab_banzai_cached_render_real_duration_seconds` | Histogram | 9.4 | Duration of rendering Markdown into HTML when cached output exists | `controller`, `action` |
+| `gitlab_banzai_cacheless_render_real_duration_seconds` | Histogram | 9.4 | Duration of rendering Markdown into HTML when cached output does not exist | `controller`, `action` |
+| `gitlab_cache_misses_total` | Counter | 10.2 | Cache read miss | `controller`, `action` |
| `gitlab_cache_operation_duration_seconds` | Histogram | 10.2 | Cache access time | |
-| `gitlab_cache_operations_total` | Counter | 12.2 | Cache operations by controller/action | controller, action, operation |
-| `job_waiter_started_total` | Counter | 12.9 | Number of batches of jobs started where a web request is waiting for the jobs to complete | worker |
-| `job_waiter_timeouts_total` | Counter | 12.9 | Number of batches of jobs that timed out where a web request is waiting for the jobs to complete | worker |
+| `gitlab_cache_operations_total` | Counter | 12.2 | Cache operations by controller/action | `controller`, `action`, `operation` |
+| `gitlab_ci_pipeline_creation_duration_seconds` | Histogram | 13.0 | Time in seconds it takes to create a CI/CD pipeline | |
+| `job_waiter_started_total` | Counter | 12.9 | Number of batches of jobs started where a web request is waiting for the jobs to complete | `worker` |
+| `job_waiter_timeouts_total` | Counter | 12.9 | Number of batches of jobs that timed out where a web request is waiting for the jobs to complete | `worker` |
| `gitlab_database_transaction_seconds` | Histogram | 12.1 | Time spent in database transactions, in seconds | |
-| `gitlab_method_call_duration_seconds` | Histogram | 10.2 | Method calls real duration | controller, action, module, method |
-| `gitlab_page_out_of_bounds` | Counter | 12.8 | Counter for the PageLimiter pagination limit being hit | controller, action, bot |
+| `gitlab_method_call_duration_seconds` | Histogram | 10.2 | Method calls real duration | `controller`, `action`, `module`, `method` |
+| `gitlab_page_out_of_bounds` | Counter | 12.8 | Counter for the PageLimiter pagination limit being hit | `controller`, `action`, `bot` |
| `gitlab_rails_queue_duration_seconds` | Histogram | 9.4 | Measures latency between GitLab Workhorse forwarding a request to Rails | |
| `gitlab_sql_duration_seconds` | Histogram | 10.2 | SQL execution time, excluding SCHEMA operations and BEGIN / COMMIT | |
| `gitlab_transaction_allocated_memory_bytes` | Histogram | 10.2 | Allocated memory for all transactions (gitlab_transaction_* metrics) | |
@@ -48,27 +55,27 @@ The following metrics are available:
| `gitlab_transaction_cache_<key>_duration_total` | Counter | 10.2 | Counter for total time (seconds) spent in Rails cache calls (per key) | |
| `gitlab_transaction_cache_count_total` | Counter | 10.2 | Counter for total Rails cache calls (aggregate) | |
| `gitlab_transaction_cache_duration_total` | Counter | 10.2 | Counter for total time (seconds) spent in Rails cache calls (aggregate) | |
-| `gitlab_transaction_cache_read_hit_count_total` | Counter | 10.2 | Counter for cache hits for Rails cache calls | controller, action |
-| `gitlab_transaction_cache_read_miss_count_total` | Counter | 10.2 | Counter for cache misses for Rails cache calls | controller, action |
-| `gitlab_transaction_duration_seconds` | Histogram | 10.2 | Duration for all transactions (gitlab_transaction_* metrics) | controller, action |
+| `gitlab_transaction_cache_read_hit_count_total` | Counter | 10.2 | Counter for cache hits for Rails cache calls | `controller`, `action` |
+| `gitlab_transaction_cache_read_miss_count_total` | Counter | 10.2 | Counter for cache misses for Rails cache calls | `controller`, `action` |
+| `gitlab_transaction_duration_seconds` | Histogram | 10.2 | Duration for all transactions (gitlab_transaction_* metrics) | `controller`, `action` |
| `gitlab_transaction_event_build_found_total` | Counter | 9.4 | Counter for build found for API /jobs/request | |
| `gitlab_transaction_event_build_invalid_total` | Counter | 9.4 | Counter for build invalid due to concurrency conflict for API /jobs/request | |
| `gitlab_transaction_event_build_not_found_cached_total` | Counter | 9.4 | Counter for cached response of build not found for API /jobs/request | |
| `gitlab_transaction_event_build_not_found_total` | Counter | 9.4 | Counter for build not found for API /jobs/request | |
| `gitlab_transaction_event_change_default_branch_total` | Counter | 9.4 | Counter when default branch is changed for any repository | |
| `gitlab_transaction_event_create_repository_total` | Counter | 9.4 | Counter when any repository is created | |
-| `gitlab_transaction_event_etag_caching_cache_hit_total` | Counter | 9.4 | Counter for etag cache hit. | endpoint |
-| `gitlab_transaction_event_etag_caching_header_missing_total` | Counter | 9.4 | Counter for etag cache miss - header missing | endpoint |
-| `gitlab_transaction_event_etag_caching_key_not_found_total` | Counter | 9.4 | Counter for etag cache miss - key not found | endpoint |
-| `gitlab_transaction_event_etag_caching_middleware_used_total` | Counter | 9.4 | Counter for etag middleware accessed | endpoint |
-| `gitlab_transaction_event_etag_caching_resource_changed_total` | Counter | 9.4 | Counter for etag cache miss - resource changed | endpoint |
+| `gitlab_transaction_event_etag_caching_cache_hit_total` | Counter | 9.4 | Counter for etag cache hit. | `endpoint` |
+| `gitlab_transaction_event_etag_caching_header_missing_total` | Counter | 9.4 | Counter for etag cache miss - header missing | `endpoint` |
+| `gitlab_transaction_event_etag_caching_key_not_found_total` | Counter | 9.4 | Counter for etag cache miss - key not found | `endpoint` |
+| `gitlab_transaction_event_etag_caching_middleware_used_total` | Counter | 9.4 | Counter for etag middleware accessed | `endpoint` |
+| `gitlab_transaction_event_etag_caching_resource_changed_total` | Counter | 9.4 | Counter for etag cache miss - resource changed | `endpoint` |
| `gitlab_transaction_event_fork_repository_total` | Counter | 9.4 | Counter for repository forks (RepositoryForkWorker). Only incremented when source repository exists | |
| `gitlab_transaction_event_import_repository_total` | Counter | 9.4 | Counter for repository imports (RepositoryImportWorker) | |
| `gitlab_transaction_event_push_branch_total` | Counter | 9.4 | Counter for all branch pushes | |
-| `gitlab_transaction_event_push_commit_total` | Counter | 9.4 | Counter for commits | branch |
+| `gitlab_transaction_event_push_commit_total` | Counter | 9.4 | Counter for commits | `branch` |
| `gitlab_transaction_event_push_tag_total` | Counter | 9.4 | Counter for tag pushes | |
| `gitlab_transaction_event_rails_exception_total` | Counter | 9.4 | Counter for number of rails exceptions | |
-| `gitlab_transaction_event_receive_email_total` | Counter | 9.4 | Counter for received emails | handler |
+| `gitlab_transaction_event_receive_email_total` | Counter | 9.4 | Counter for received emails | `handler` |
| `gitlab_transaction_event_remote_mirrors_failed_total` | Counter | 10.8 | Counter for failed remote mirrors | |
| `gitlab_transaction_event_remote_mirrors_finished_total` | Counter | 10.8 | Counter for finished remote mirrors | |
| `gitlab_transaction_event_remote_mirrors_running_total` | Counter | 10.8 | Counter for running remote mirrors | |
@@ -76,15 +83,15 @@ The following metrics are available:
| `gitlab_transaction_event_remove_repository_total` | Counter | 9.4 | Counter when a repository is removed | |
| `gitlab_transaction_event_remove_tag_total` | Counter | 9.4 | Counter when a tag is remove for any repository | |
| `gitlab_transaction_event_sidekiq_exception_total` | Counter | 9.4 | Counter of Sidekiq exceptions | |
-| `gitlab_transaction_event_stuck_import_jobs_total` | Counter | 9.4 | Count of stuck import jobs | projects_without_jid_count, projects_with_jid_count |
-| `gitlab_transaction_event_update_build_total` | Counter | 9.4 | Counter for update build for API /jobs/request/:id | |
+| `gitlab_transaction_event_stuck_import_jobs_total` | Counter | 9.4 | Count of stuck import jobs | `projects_without_jid_count`, `projects_with_jid_count` |
+| `gitlab_transaction_event_update_build_total` | Counter | 9.4 | Counter for update build for API `/jobs/request/:id` | |
| `gitlab_transaction_new_redis_connections_total` | Counter | 9.4 | Counter for new Redis connections | |
| `gitlab_transaction_queue_duration_total` | Counter | 9.4 | Duration jobs were enqueued before processing | |
-| `gitlab_transaction_rails_queue_duration_total` | Counter | 9.4 | Measures latency between GitLab Workhorse forwarding a request to Rails | controller, action |
-| `gitlab_transaction_view_duration_total` | Counter | 9.4 | Duration for views | controller, action, view |
-| `gitlab_view_rendering_duration_seconds` | Histogram | 10.2 | Duration for views (histogram) | controller, action, view |
-| `http_requests_total` | Counter | 9.4 | Rack request count | method |
-| `http_request_duration_seconds` | Histogram | 9.4 | HTTP response time from rack middleware | method, status |
+| `gitlab_transaction_rails_queue_duration_total` | Counter | 9.4 | Measures latency between GitLab Workhorse forwarding a request to Rails | `controller`, `action` |
+| `gitlab_transaction_view_duration_total` | Counter | 9.4 | Duration for views | `controller`, `action`, `view` |
+| `gitlab_view_rendering_duration_seconds` | Histogram | 10.2 | Duration for views (histogram) | `controller`, `action`, `view` |
+| `http_requests_total` | Counter | 9.4 | Rack request count | `method` |
+| `http_request_duration_seconds` | Histogram | 9.4 | HTTP response time from rack middleware | `method`, `status` |
| `pipelines_created_total` | Counter | 9.4 | Counter of pipelines created | |
| `rack_uncaught_errors_total` | Counter | 9.4 | Rack connections handling uncaught errors count | |
| `user_session_logins_total` | Counter | 9.4 | Counter of how many users have logged in | |
@@ -112,49 +119,52 @@ configuration option in `gitlab.yml`. These metrics are served from the
| Metric | Type | Since | Description | Labels |
|:---------------------------------------------- |:------- |:----- |:----------- |:------ |
-| `sidekiq_jobs_cpu_seconds` | Histogram | 12.4 | Seconds of cpu time to run Sidekiq job | queue, boundary, external_dependencies, feature_category, job_status, urgency |
-| `sidekiq_jobs_completion_seconds` | Histogram | 12.2 | Seconds to complete Sidekiq job | queue, boundary, external_dependencies, feature_category, job_status, urgency |
-| `sidekiq_jobs_db_seconds` | Histogram | 12.9 | Seconds of DB time to run Sidekiq job | queue, boundary, external_dependencies, feature_category, job_status, urgency |
-| `sidekiq_jobs_gitaly_seconds` | Histogram | 12.9 | Seconds of Gitaly time to run Sidekiq job | queue, boundary, external_dependencies, feature_category, job_status, urgency |
-| `sidekiq_jobs_queue_duration_seconds` | Histogram | 12.5 | Duration in seconds that a Sidekiq job was queued before being executed | queue, boundary, external_dependencies, feature_category, urgency |
-| `sidekiq_jobs_failed_total` | Counter | 12.2 | Sidekiq jobs failed | queue, boundary, external_dependencies, feature_category, urgency |
-| `sidekiq_jobs_retried_total` | Counter | 12.2 | Sidekiq jobs retried | queue, boundary, external_dependencies, feature_category, urgency |
-| `sidekiq_running_jobs` | Gauge | 12.2 | Number of Sidekiq jobs running | queue, boundary, external_dependencies, feature_category, urgency |
+| `sidekiq_jobs_cpu_seconds` | Histogram | 12.4 | Seconds of cpu time to run Sidekiq job | `queue`, `boundary`, `external_dependencies`, `feature_category`, `job_status`, `urgency` |
+| `sidekiq_jobs_completion_seconds` | Histogram | 12.2 | Seconds to complete Sidekiq job | `queue`, `boundary`, `external_dependencies`, `feature_category`, `job_status`, `urgency` |
+| `sidekiq_jobs_db_seconds` | Histogram | 12.9 | Seconds of DB time to run Sidekiq job | `queue`, `boundary`, `external_dependencies`, `feature_category`, `job_status`, `urgency` |
+| `sidekiq_jobs_gitaly_seconds` | Histogram | 12.9 | Seconds of Gitaly time to run Sidekiq job | `queue`, `boundary`, `external_dependencies`, `feature_category`, `job_status`, `urgency` |
+| `sidekiq_jobs_queue_duration_seconds` | Histogram | 12.5 | Duration in seconds that a Sidekiq job was queued before being executed | `queue`, `boundary`, `external_dependencies`, `feature_category`, `urgency` |
+| `sidekiq_jobs_failed_total` | Counter | 12.2 | Sidekiq jobs failed | `queue`, `boundary`, `external_dependencies`, `feature_category`, `urgency` |
+| `sidekiq_jobs_retried_total` | Counter | 12.2 | Sidekiq jobs retried | `queue`, `boundary`, `external_dependencies`, `feature_category`, `urgency` |
+| `sidekiq_running_jobs` | Gauge | 12.2 | Number of Sidekiq jobs running | `queue`, `boundary`, `external_dependencies`, `feature_category`, `urgency` |
| `sidekiq_concurrency` | Gauge | 12.5 | Maximum number of Sidekiq jobs | |
-| `geo_db_replication_lag_seconds` | Gauge | 10.2 | Database replication lag (seconds) | url |
-| `geo_repositories` | Gauge | 10.2 | Total number of repositories available on primary | url |
-| `geo_repositories_synced` | Gauge | 10.2 | Number of repositories synced on secondary | url |
-| `geo_repositories_failed` | Gauge | 10.2 | Number of repositories failed to sync on secondary | url |
-| `geo_lfs_objects` | Gauge | 10.2 | Total number of LFS objects available on primary | url |
-| `geo_lfs_objects_synced` | Gauge | 10.2 | Number of LFS objects synced on secondary | url |
-| `geo_lfs_objects_failed` | Gauge | 10.2 | Number of LFS objects failed to sync on secondary | url |
-| `geo_attachments` | Gauge | 10.2 | Total number of file attachments available on primary | url |
-| `geo_attachments_synced` | Gauge | 10.2 | Number of attachments synced on secondary | url |
-| `geo_attachments_failed` | Gauge | 10.2 | Number of attachments failed to sync on secondary | url |
-| `geo_last_event_id` | Gauge | 10.2 | Database ID of the latest event log entry on the primary | url |
-| `geo_last_event_timestamp` | Gauge | 10.2 | UNIX timestamp of the latest event log entry on the primary | url |
-| `geo_cursor_last_event_id` | Gauge | 10.2 | Last database ID of the event log processed by the secondary | url |
-| `geo_cursor_last_event_timestamp` | Gauge | 10.2 | Last UNIX timestamp of the event log processed by the secondary | url |
-| `geo_status_failed_total` | Counter | 10.2 | Number of times retrieving the status from the Geo Node failed | url |
-| `geo_last_successful_status_check_timestamp` | Gauge | 10.2 | Last timestamp when the status was successfully updated | url |
-| `geo_lfs_objects_synced_missing_on_primary` | Gauge | 10.7 | Number of LFS objects marked as synced due to the file missing on the primary | url |
-| `geo_job_artifacts_synced_missing_on_primary` | Gauge | 10.7 | Number of job artifacts marked as synced due to the file missing on the primary | url |
-| `geo_attachments_synced_missing_on_primary` | Gauge | 10.7 | Number of attachments marked as synced due to the file missing on the primary | url |
-| `geo_repositories_checksummed_count` | Gauge | 10.7 | Number of repositories checksummed on primary | url |
-| `geo_repositories_checksum_failed_count` | Gauge | 10.7 | Number of repositories failed to calculate the checksum on primary | url |
-| `geo_wikis_checksummed_count` | Gauge | 10.7 | Number of wikis checksummed on primary | url |
-| `geo_wikis_checksum_failed_count` | Gauge | 10.7 | Number of wikis failed to calculate the checksum on primary | url |
-| `geo_repositories_verified_count` | Gauge | 10.7 | Number of repositories verified on secondary | url |
-| `geo_repositories_verification_failed_count` | Gauge | 10.7 | Number of repositories failed to verify on secondary | url |
-| `geo_repositories_checksum_mismatch_count` | Gauge | 10.7 | Number of repositories that checksum mismatch on secondary | url |
-| `geo_wikis_verified_count` | Gauge | 10.7 | Number of wikis verified on secondary | url |
-| `geo_wikis_verification_failed_count` | Gauge | 10.7 | Number of wikis failed to verify on secondary | url |
-| `geo_wikis_checksum_mismatch_count` | Gauge | 10.7 | Number of wikis that checksum mismatch on secondary | url |
-| `geo_repositories_checked_count` | Gauge | 11.1 | Number of repositories that have been checked via `git fsck` | url |
-| `geo_repositories_checked_failed_count` | Gauge | 11.1 | Number of repositories that have a failure from `git fsck` | url |
-| `geo_repositories_retrying_verification_count` | Gauge | 11.2 | Number of repositories verification failures that Geo is actively trying to correct on secondary | url |
-| `geo_wikis_retrying_verification_count` | Gauge | 11.2 | Number of wikis verification failures that Geo is actively trying to correct on secondary | url |
+| `geo_db_replication_lag_seconds` | Gauge | 10.2 | Database replication lag (seconds) | `url` |
+| `geo_repositories` | Gauge | 10.2 | Total number of repositories available on primary | `url` |
+| `geo_repositories_synced` | Gauge | 10.2 | Number of repositories synced on secondary | `url` |
+| `geo_repositories_failed` | Gauge | 10.2 | Number of repositories failed to sync on secondary | `url` |
+| `geo_lfs_objects` | Gauge | 10.2 | Total number of LFS objects available on primary | `url` |
+| `geo_lfs_objects_synced` | Gauge | 10.2 | Number of LFS objects synced on secondary | `url` |
+| `geo_lfs_objects_failed` | Gauge | 10.2 | Number of LFS objects failed to sync on secondary | `url` |
+| `geo_attachments` | Gauge | 10.2 | Total number of file attachments available on primary | `url` |
+| `geo_attachments_synced` | Gauge | 10.2 | Number of attachments synced on secondary | `url` |
+| `geo_attachments_failed` | Gauge | 10.2 | Number of attachments failed to sync on secondary | `url` |
+| `geo_last_event_id` | Gauge | 10.2 | Database ID of the latest event log entry on the primary | `url` |
+| `geo_last_event_timestamp` | Gauge | 10.2 | UNIX timestamp of the latest event log entry on the primary | `url` |
+| `geo_cursor_last_event_id` | Gauge | 10.2 | Last database ID of the event log processed by the secondary | `url` |
+| `geo_cursor_last_event_timestamp` | Gauge | 10.2 | Last UNIX timestamp of the event log processed by the secondary | `url` |
+| `geo_status_failed_total` | Counter | 10.2 | Number of times retrieving the status from the Geo Node failed | `url` |
+| `geo_last_successful_status_check_timestamp` | Gauge | 10.2 | Last timestamp when the status was successfully updated | `url` |
+| `geo_lfs_objects_synced_missing_on_primary` | Gauge | 10.7 | Number of LFS objects marked as synced due to the file missing on the primary | `url` |
+| `geo_job_artifacts_synced_missing_on_primary` | Gauge | 10.7 | Number of job artifacts marked as synced due to the file missing on the primary | `url` |
+| `geo_attachments_synced_missing_on_primary` | Gauge | 10.7 | Number of attachments marked as synced due to the file missing on the primary | `url` |
+| `geo_repositories_checksummed_count` | Gauge | 10.7 | Number of repositories checksummed on primary | `url` |
+| `geo_repositories_checksum_failed_count` | Gauge | 10.7 | Number of repositories failed to calculate the checksum on primary | `url` |
+| `geo_wikis_checksummed_count` | Gauge | 10.7 | Number of wikis checksummed on primary | `url` |
+| `geo_wikis_checksum_failed_count` | Gauge | 10.7 | Number of wikis failed to calculate the checksum on primary | `url` |
+| `geo_repositories_verified_count` | Gauge | 10.7 | Number of repositories verified on secondary | `url` |
+| `geo_repositories_verification_failed_count` | Gauge | 10.7 | Number of repositories failed to verify on secondary | `url` |
+| `geo_repositories_checksum_mismatch_count` | Gauge | 10.7 | Number of repositories that checksum mismatch on secondary | `url` |
+| `geo_wikis_verified_count` | Gauge | 10.7 | Number of wikis verified on secondary | `url` |
+| `geo_wikis_verification_failed_count` | Gauge | 10.7 | Number of wikis failed to verify on secondary | `url` |
+| `geo_wikis_checksum_mismatch_count` | Gauge | 10.7 | Number of wikis that checksum mismatch on secondary | `url` |
+| `geo_repositories_checked_count` | Gauge | 11.1 | Number of repositories that have been checked via `git fsck` | `url` |
+| `geo_repositories_checked_failed_count` | Gauge | 11.1 | Number of repositories that have a failure from `git fsck` | `url` |
+| `geo_repositories_retrying_verification_count` | Gauge | 11.2 | Number of repositories verification failures that Geo is actively trying to correct on secondary | `url` |
+| `geo_wikis_retrying_verification_count` | Gauge | 11.2 | Number of wikis verification failures that Geo is actively trying to correct on secondary | `url` |
| `global_search_bulk_cron_queue_size` | Gauge | 12.10 | Number of database records waiting to be synchronized to Elasticsearch | |
+| `package_files_count` | Gauge | 13.0 | Number of package files on primary | `url` |
+| `package_files_checksummed_count` | Gauge | 13.0 | Number of package files checksummed on primary | `url` |
+| `package_files_checksum_failed_count` | Gauge | 13.0 | Number of package files failed to calculate the checksum on primary
## Database load balancing metrics **(PREMIUM ONLY)**
@@ -168,17 +178,18 @@ The following metrics are available:
Some basic Ruby runtime metrics are available:
-| Metric | Type | Since | Description |
-|:------------------------------------ |:--------- |:----- |:----------- |
-| `ruby_gc_duration_seconds` | Counter | 11.1 | Time spent by Ruby in GC |
-| `ruby_gc_stat_...` | Gauge | 11.1 | Various metrics from [GC.stat](https://ruby-doc.org/core-2.6.5/GC.html#method-c-stat) |
-| `ruby_file_descriptors` | Gauge | 11.1 | File descriptors per process |
-| `ruby_memory_bytes` | Gauge | 11.1 | Memory usage by process |
-| `ruby_sampler_duration_seconds` | Counter | 11.1 | Time spent collecting stats |
-| `ruby_process_cpu_seconds_total` | Gauge | 12.0 | Total amount of CPU time per process |
-| `ruby_process_max_fds` | Gauge | 12.0 | Maximum number of open file descriptors per process |
-| `ruby_process_resident_memory_bytes` | Gauge | 12.0 | Memory usage by process |
-| `ruby_process_start_time_seconds` | Gauge | 12.0 | UNIX timestamp of process start time |
+| Metric | Type | Since | Description |
+|:---------------------------------------- |:--------- |:----- |:----------- |
+| `ruby_gc_duration_seconds` | Counter | 11.1 | Time spent by Ruby in GC |
+| `ruby_gc_stat_...` | Gauge | 11.1 | Various metrics from [GC.stat](https://ruby-doc.org/core-2.6.5/GC.html#method-c-stat) |
+| `ruby_file_descriptors` | Gauge | 11.1 | File descriptors per process |
+| `ruby_sampler_duration_seconds` | Counter | 11.1 | Time spent collecting stats |
+| `ruby_process_cpu_seconds_total` | Gauge | 12.0 | Total amount of CPU time per process |
+| `ruby_process_max_fds` | Gauge | 12.0 | Maximum number of open file descriptors per process |
+| `ruby_process_resident_memory_bytes` | Gauge | 12.0 | Memory usage by process (RSS/Resident Set Size) |
+| `ruby_process_unique_memory_bytes` | Gauge | 13.0 | Memory usage by process (USS/Unique Set Size) |
+| `ruby_process_proportional_memory_bytes` | Gauge | 13.0 | Memory usage by process (PSS/Proportional Set Size) |
+| `ruby_process_start_time_seconds` | Gauge | 12.0 | UNIX timestamp of process start time |
## Unicorn Metrics
diff --git a/doc/administration/monitoring/prometheus/index.md b/doc/administration/monitoring/prometheus/index.md
index 03d86012cc7..cb93aca6e4e 100644
--- a/doc/administration/monitoring/prometheus/index.md
+++ b/doc/administration/monitoring/prometheus/index.md
@@ -1,3 +1,9 @@
+---
+stage: Monitor
+group: APM
+info: To determine the technical writer assigned to the Stage/Group associated with this page, see https://about.gitlab.com/handbook/engineering/ux/technical-writing/#designated-technical-writers
+---
+
# Monitoring GitLab with Prometheus
> **Notes:**
@@ -49,7 +55,7 @@ To disable Prometheus and all of its exporters, as well as any added in the futu
### Changing the port and address Prometheus listens on
NOTE: **Note:**
-The following change was added in [GitLab Omnibus 8.17](https://gitlab.com/gitlab-org/omnibus-gitlab/-/merge_requests/1261). Although possible,
+The following change was added in [Omnibus GitLab 8.17](https://gitlab.com/gitlab-org/omnibus-gitlab/-/merge_requests/1261). Although possible,
it's not recommended to change the port Prometheus listens
on, as this might affect or conflict with other services running on the GitLab
server. Proceed at your own risk.
@@ -80,7 +86,7 @@ To change the address/port that Prometheus listens on:
### Adding custom scrape configs
-You can configure additional scrape targets for the GitLab Omnibus-bundled
+You can configure additional scrape targets for the Omnibus GitLab-bundled
Prometheus by editing `prometheus['scrape_configs']` in `/etc/gitlab/gitlab.rb`
using the [Prometheus scrape target configuration](https://prometheus.io/docs/prometheus/latest/configuration/configuration/#%3Cscrape_config%3E)
syntax.
@@ -108,7 +114,7 @@ prometheus['scrape_configs'] = [
NOTE: **Note:**
Prometheus and most exporters don't support authentication. We don't recommend exposing them outside the local network.
-A few configuration changes are required to allow GitLab to be monitored by an external Prometheus server. External servers are recommended for highly available deployments of GitLab with multiple nodes.
+A few configuration changes are required to allow GitLab to be monitored by an external Prometheus server. External servers are recommended for [GitLab deployments with multiple nodes](../../reference_architectures/index.md).
To use an external Prometheus server:
diff --git a/doc/administration/monitoring/prometheus/node_exporter.md b/doc/administration/monitoring/prometheus/node_exporter.md
index d75b04f1ccd..357303ee4e1 100644
--- a/doc/administration/monitoring/prometheus/node_exporter.md
+++ b/doc/administration/monitoring/prometheus/node_exporter.md
@@ -1,3 +1,9 @@
+---
+stage: Monitor
+group: APM
+info: To determine the technical writer assigned to the Stage/Group associated with this page, see https://about.gitlab.com/handbook/engineering/ux/technical-writing/#designated-technical-writers
+---
+
# Node exporter
>**Note:**
diff --git a/doc/administration/monitoring/prometheus/pgbouncer_exporter.md b/doc/administration/monitoring/prometheus/pgbouncer_exporter.md
index ba8e464efcb..92ba2d9bb52 100644
--- a/doc/administration/monitoring/prometheus/pgbouncer_exporter.md
+++ b/doc/administration/monitoring/prometheus/pgbouncer_exporter.md
@@ -1,3 +1,9 @@
+---
+stage: Monitor
+group: APM
+info: To determine the technical writer assigned to the Stage/Group associated with this page, see https://about.gitlab.com/handbook/engineering/ux/technical-writing/#designated-technical-writers
+---
+
# PgBouncer exporter
>**Note:**
diff --git a/doc/administration/monitoring/prometheus/postgres_exporter.md b/doc/administration/monitoring/prometheus/postgres_exporter.md
index 853e3837184..77ca502b21d 100644
--- a/doc/administration/monitoring/prometheus/postgres_exporter.md
+++ b/doc/administration/monitoring/prometheus/postgres_exporter.md
@@ -1,3 +1,9 @@
+---
+stage: Monitor
+group: APM
+info: To determine the technical writer assigned to the Stage/Group associated with this page, see https://about.gitlab.com/handbook/engineering/ux/technical-writing/#designated-technical-writers
+---
+
# PostgreSQL Server Exporter
>**Note:**
diff --git a/doc/administration/monitoring/prometheus/redis_exporter.md b/doc/administration/monitoring/prometheus/redis_exporter.md
index 76f4add0c1b..bef87400f5a 100644
--- a/doc/administration/monitoring/prometheus/redis_exporter.md
+++ b/doc/administration/monitoring/prometheus/redis_exporter.md
@@ -1,3 +1,9 @@
+---
+stage: Monitor
+group: APM
+info: To determine the technical writer assigned to the Stage/Group associated with this page, see https://about.gitlab.com/handbook/engineering/ux/technical-writing/#designated-technical-writers
+---
+
# Redis exporter
>**Note:**
diff --git a/doc/administration/monitoring/prometheus/registry_exporter.md b/doc/administration/monitoring/prometheus/registry_exporter.md
index 2e2440389ed..3d28b26b685 100644
--- a/doc/administration/monitoring/prometheus/registry_exporter.md
+++ b/doc/administration/monitoring/prometheus/registry_exporter.md
@@ -1,3 +1,9 @@
+---
+stage: Monitor
+group: APM
+info: To determine the technical writer assigned to the Stage/Group associated with this page, see https://about.gitlab.com/handbook/engineering/ux/technical-writing/#designated-technical-writers
+---
+
# Registry exporter
> [Introduced](https://gitlab.com/gitlab-org/omnibus-gitlab/-/merge_requests/2884) in GitLab 11.9.
diff --git a/doc/administration/object_storage.md b/doc/administration/object_storage.md
index 1b0c7f8a907..c4f9b672923 100644
--- a/doc/administration/object_storage.md
+++ b/doc/administration/object_storage.md
@@ -5,7 +5,7 @@ type: reference
# Object Storage
GitLab supports using an object storage service for holding numerous types of data.
-In a high availability setup, it's recommended over [NFS](high_availability/nfs.md) and
+It's recommended over NFS and
in general it's better in larger setups as object storage is
typically much more performant, reliable, and scalable.
@@ -37,8 +37,8 @@ For configuring GitLab to use Object Storage refer to the following guides:
### Other alternatives to filesystem storage
-If you're working to [scale out](scaling/index.md) your GitLab implementation,
-or add [fault tolerance and redundancy](high_availability/README.md) you may be
+If you're working to [scale out](reference_architectures/index.md) your GitLab implementation,
+or add fault tolerance and redundancy, you may be
looking at removing dependencies on block or network filesystems.
See the following guides and
[note that Pages requires disk storage](#gitlab-pages-requires-nfs):
@@ -77,7 +77,7 @@ with the Fog library that GitLab uses. Symptoms include:
### GitLab Pages requires NFS
-If you're working to add more GitLab servers for [scaling or fault tolerance](scaling/index.md)
+If you're working to add more GitLab servers for [scaling or fault tolerance](reference_architectures/index.md)
and one of your requirements is [GitLab Pages](../user/project/pages/index.md) this currently requires
NFS. There is [work in progress](https://gitlab.com/gitlab-org/gitlab-pages/issues/196)
to remove this dependency. In the future, GitLab Pages may use
@@ -98,9 +98,9 @@ object storage back end, like when Git clients request large files via LFS or wh
downloading CI artifacts and logs.
When the files are stored on local block storage or NFS, GitLab has to act as a proxy.
-This is not the default behaviour with object storage.
+This is not the default behavior with object storage.
-The `proxy_download` setting controls this behaviour: the default is generally `false`.
+The `proxy_download` setting controls this behavior: the default is generally `false`.
Verify this in the documentation for each use case. Set it to `true` so that GitLab proxies
the files.
diff --git a/doc/administration/operations/extra_sidekiq_processes.md b/doc/administration/operations/extra_sidekiq_processes.md
index 1c92a429982..8f54b82c325 100644
--- a/doc/administration/operations/extra_sidekiq_processes.md
+++ b/doc/administration/operations/extra_sidekiq_processes.md
@@ -1,13 +1,13 @@
-# Running multiple Sidekiq processes **(CORE ONLY)**
-
-NOTE: **Note:**
-The information in this page applies only to Omnibus GitLab.
+# Run multiple Sidekiq processes **(CORE ONLY)**
GitLab allows you to start multiple Sidekiq processes.
These processes can be used to consume a dedicated set
of queues. This can be used to ensure certain queues always have dedicated
workers, no matter the number of jobs that need to be processed.
+NOTE: **Note:**
+The information in this page applies only to Omnibus GitLab.
+
## Available Sidekiq queues
For a list of the existing Sidekiq queues, check the following files:
@@ -18,28 +18,27 @@ For a list of the existing Sidekiq queues, check the following files:
Each entry in the above files represents a queue on which Sidekiq processes
can be started.
-## Starting multiple processes
+## Start multiple processes
-To start multiple Sidekiq processes, you must enable `sidekiq-cluster`:
+> - [Introduced](https://gitlab.com/gitlab-org/omnibus-gitlab/-/merge_requests/4006) in GitLab 12.10, starting multiple processes with Sidekiq cluster.
+> - [Sidekiq cluster moved](https://gitlab.com/groups/gitlab-com/gl-infra/-/epics/181) to GitLab [Core](https://about.gitlab.com/pricing/#self-managed) in GitLab 12.10.
+> - [Sidekiq cluster became default](https://gitlab.com/gitlab-org/omnibus-gitlab/-/merge_requests/4140) in GitLab 13.0.
-1. Edit `/etc/gitlab/gitlab.rb` and add:
+To start multiple processes:
- ```ruby
- sidekiq_cluster['enable'] = true
- ```
-
-1. You will then need to specify how many additional processes to create via `sidekiq-cluster`
- and which queue they should handle via the `sidekiq_cluster['queue_groups']`
- array setting. Each item in the array equates to one additional Sidekiq
+1. Using the `sidekiq['queue_groups']` array setting, specify how many processes to
+ create using `sidekiq-cluster` and which queue they should handle.
+ Each item in the array equates to one additional Sidekiq
process, and values in each item determine the queues it works on.
- For example, the following setting adds additional Sidekiq processes to two
- queues, one to `elastic_indexer` and one to `mailers`:
+ For example, the following setting creates three Sidekiq processes, one to run on
+ `elastic_indexer`, one to run on `mailers`, and one process running all on queues:
```ruby
- sidekiq_cluster['queue_groups'] = [
+ sidekiq['queue_groups'] = [
"elastic_indexer",
- "mailers"
+ "mailers",
+ "*"
]
```
@@ -47,9 +46,10 @@ To start multiple Sidekiq processes, you must enable `sidekiq-cluster`:
queue names to its item delimited by commas. For example:
```ruby
- sidekiq_cluster['queue_groups'] = [
+ sidekiq['queue_groups'] = [
"elastic_indexer, elastic_commit_indexer",
- "mailers"
+ "mailers",
+ "*"
]
```
@@ -58,7 +58,7 @@ To start multiple Sidekiq processes, you must enable `sidekiq-cluster`:
processes, each handling all queues:
```ruby
- sidekiq_cluster['queue_groups'] = [
+ sidekiq['queue_groups'] = [
"*",
"*"
]
@@ -67,27 +67,35 @@ To start multiple Sidekiq processes, you must enable `sidekiq-cluster`:
`*` cannot be combined with concrete queue names - `*, mailers` will
just handle the `mailers` queue.
+ When `sidekiq-cluster` is only running on a single node, make sure that at least
+ one process is running on all queues using `*`. This means a process will
+ automatically pick up jobs in queues created in the future.
+
+ If `sidekiq-cluster` is running on more than one node, you can also use
+ [`--negate`](#negate-settings) and list all the queues that are already being
+ processed.
+
1. Save the file and reconfigure GitLab for the changes to take effect:
```shell
sudo gitlab-ctl reconfigure
```
-Once the extra Sidekiq processes are added, you can visit the
-**Admin Area > Monitoring > Background Jobs** (`/admin/background_jobs`) in GitLab.
+After the extra Sidekiq processes are added, navigate to
+**{admin}** **Admin Area > Monitoring > Background Jobs** (`/admin/background_jobs`) in GitLab.
![Multiple Sidekiq processes](img/sidekiq-cluster.png)
-## Negating settings
+## Negate settings
To have the additional Sidekiq processes work on every queue **except** the ones
you list:
-1. After you follow the steps for [starting extra processes](#starting-multiple-processes),
+1. After you follow the steps for [starting extra processes](#start-multiple-processes),
edit `/etc/gitlab/gitlab.rb` and add:
```ruby
- sidekiq_cluster['negate'] = true
+ sidekiq['negate'] = true
```
1. Save the file and reconfigure GitLab for the changes to take effect:
@@ -177,9 +185,9 @@ entire queue group selects all queues.
In `/etc/gitlab/gitlab.rb`:
```ruby
-sidekiq_cluster['enable'] = true
-sidekiq_cluster['experimental_queue_selector'] = true
-sidekiq_cluster['queue_groups'] = [
+sidekiq['enable'] = true
+sidekiq['experimental_queue_selector'] = true
+sidekiq['queue_groups'] = [
# Run all non-CPU-bound queues that are high urgency
'resource_boundary!=cpu&urgency=high',
# Run all continuous integration and pages queues that are not high urgency
@@ -189,35 +197,31 @@ sidekiq_cluster['queue_groups'] = [
]
```
-### Using Sidekiq cluster by default (experimental)
-
-> [Introduced](https://gitlab.com/gitlab-org/omnibus-gitlab/-/merge_requests/4006) in GitLab 12.10.
+### Disable Sidekiq cluster
CAUTION: **Warning:**
-This feature is experimental.
+Sidekiq cluster is [scheduled](https://gitlab.com/gitlab-com/gl-infra/scalability/-/issues/240)
+to be the only way to start Sidekiq in GitLab 14.0.
-We're moving [Sidekiq cluster to
-core](https://gitlab.com/groups/gitlab-com/gl-infra/-/epics/181) and
-plan to make it the default way of starting Sidekiq.
-
-Set the following to start Sidekiq (cluster)
-process for handling for all queues (`/etc/gitlab/gitlab.rb`):
+By default, the Sidekiq service will run `sidekiq-cluster`. To disable this behavior,
+add the following to the Sidekiq configuration:
```ruby
sidekiq['enable'] = true
-sidekiq['cluster'] = true
+sidekiq['cluster'] = false
```
-All of the aforementioned configuration options for `sidekiq_cluster`
-are also available. By default, they will be configured as follows:
+All of the aforementioned configuration options for `sidekiq`
+are available. By default, they will be configured as follows:
```ruby
sidekiq['experimental_queue_selector'] = false
sidekiq['interval'] = nil
-sidekiq['max_concurrency'] = nil
+sidekiq['max_concurrency'] = 50
sidekiq['min_concurrency'] = nil
sidekiq['negate'] = false
sidekiq['queue_groups'] = ['*']
+sidekiq['shutdown_timeout'] = 25
```
`sidekiq_cluster` must be disabled if you decide to configure the
@@ -231,7 +235,7 @@ setting `sidekiq['cluster'] = true`.
When using this feature, the service called `sidekiq` will now be
running `sidekiq-cluster`.
-The [concurrency](#managing-concurrency) and other options configured
+The [concurrency](#manage-concurrency) and other options configured
for Sidekiq will be respected.
By default, logs for `sidekiq-cluster` go to `/var/log/gitlab/sidekiq`
@@ -246,9 +250,9 @@ use all of its resources to perform those operations. To set up a separate
1. Edit `/etc/gitlab/gitlab.rb` and add:
```ruby
- sidekiq_cluster['enable'] = true
- sidekiq_cluster['negate'] = true
- sidekiq_cluster['queue_groups'] = [
+ sidekiq['enable'] = true
+ sidekiq['negate'] = true
+ sidekiq['queue_groups'] = [
"github_import_advance_stage",
"github_importer:github_import_import_diff_note",
"github_importer:github_import_import_issue",
@@ -274,12 +278,12 @@ use all of its resources to perform those operations. To set up a separate
## Number of threads
-Each process defined under `sidekiq_cluster` starts with a
+Each process defined under `sidekiq` starts with a
number of threads that equals the number of queues, plus one spare thread.
For example, a process that handles the `process_commit` and `post_receive`
queues will use three threads in total.
-## Managing concurrency
+## Manage concurrency
When setting the maximum concurrency, keep in mind this normally should
not exceed the number of CPU cores available. The values in the examples
@@ -290,29 +294,15 @@ latency and potentially cause client timeouts. See the [Sidekiq documentation
about Redis](https://github.com/mperham/sidekiq/wiki/Using-Redis) for more
details.
-### When running a single Sidekiq process (default)
-
-1. Edit `/etc/gitlab/gitlab.rb` and add:
-
- ```ruby
- sidekiq['concurrency'] = 25
- ```
+### When running Sidekiq cluster (default)
-1. Save the file and reconfigure GitLab for the changes to take effect:
-
- ```shell
- sudo gitlab-ctl reconfigure
- ```
-
-This will set the concurrency (number of threads) for the Sidekiq process.
-
-### When running Sidekiq cluster
+Running Sidekiq cluster is the default in GitLab 13.0 and later.
1. Edit `/etc/gitlab/gitlab.rb` and add:
```ruby
- sidekiq_cluster['min_concurrency'] = 15
- sidekiq_cluster['max_concurrency'] = 25
+ sidekiq['min_concurrency'] = 15
+ sidekiq['max_concurrency'] = 25
```
1. Save the file and reconfigure GitLab for the changes to take effect:
@@ -337,21 +327,44 @@ regardless of the number of queues.
When `min_concurrency` is greater than `max_concurrency`, it is treated as
being equal to `max_concurrency`.
-## Modifying the check interval
+### When running a single Sidekiq process
+
+Running a single Sidekiq process is the default in GitLab 12.10 and earlier.
+
+CAUTION: **Warning:**
+Running Sidekiq directly is scheduled to be removed in GitLab
+[14.0](https://gitlab.com/gitlab-com/gl-infra/scalability/-/issues/240).
+
+1. Edit `/etc/gitlab/gitlab.rb` and add:
+
+ ```ruby
+ sidekiq['cluster'] = false
+ sidekiq['concurrency'] = 25
+ ```
+
+1. Save the file and reconfigure GitLab for the changes to take effect:
+
+ ```shell
+ sudo gitlab-ctl reconfigure
+ ```
+
+This will set the concurrency (number of threads) for the Sidekiq process.
+
+## Modify the check interval
To modify the check interval for the additional Sidekiq processes:
1. Edit `/etc/gitlab/gitlab.rb` and add:
```ruby
- sidekiq_cluster['interval'] = 5
+ sidekiq['interval'] = 5
```
1. Save the file and [reconfigure GitLab](../restart_gitlab.md#omnibus-gitlab-reconfigure) for the changes to take effect.
This tells the additional processes how often to check for enqueued jobs.
-## Troubleshooting using the CLI
+## Troubleshoot using the CLI
CAUTION: **Warning:**
It's recommended to use `/etc/gitlab/gitlab.rb` to configure the Sidekiq processes.
@@ -399,7 +412,7 @@ you'd use the following:
/opt/gitlab/embedded/service/gitlab-rails/bin/sidekiq-cluster process_commit,post_receive gitlab_shell
```
-### Monitoring the `sidekiq-cluster` command
+### Monitor the `sidekiq-cluster` command
The `sidekiq-cluster` command will not terminate once it has started the desired
amount of Sidekiq processes. Instead, the process will continue running and
@@ -412,7 +425,7 @@ processes will terminate themselves after a few seconds. This ensures you don't
end up with zombie Sidekiq processes.
All of this makes monitoring the processes fairly easy. Simply hook up
-`sidekiq-cluster` to your supervisor of choice (e.g. runit) and you're good to
+`sidekiq-cluster` to your supervisor of choice (for example, runit) and you're good to
go.
If a child process died the `sidekiq-cluster` command will signal all remaining
diff --git a/doc/administration/operations/fast_ssh_key_lookup.md b/doc/administration/operations/fast_ssh_key_lookup.md
index 2d1e1c5bda8..6759c3f265d 100644
--- a/doc/administration/operations/fast_ssh_key_lookup.md
+++ b/doc/administration/operations/fast_ssh_key_lookup.md
@@ -68,11 +68,17 @@ sudo service sshd reload
```
Confirm that SSH is working by removing your user's SSH key in the UI, adding a
-new one, and attempting to pull a repo.
+new one, and attempting to pull a repository.
NOTE: **Note:** For Omnibus Docker, `AuthorizedKeysCommand` is setup by default in
GitLab 11.11 and later.
+NOTE: **Note:** For Installations from source, the command would be located at
+`/home/git/gitlab-shell/bin/gitlab-shell-authorized-keys-check` if [the install from source](../../install/installation.md#install-gitlab-shell) instructions were followed.
+You might want to consider creating a wrapper script somewhere else since this command needs to be
+owned by `root` and not be writable by group or others. You could also consider changing the ownership of this command
+as required, but that might require temporary ownership changes during `gitlab-shell` upgrades.
+
CAUTION: **Caution:** Do not disable writes until SSH is confirmed to be working
perfectly, because the file will quickly become out-of-date.
@@ -87,7 +93,7 @@ installation.
![Write to authorized keys setting](img/write_to_authorized_keys_setting.png)
Again, confirm that SSH is working by removing your user's SSH key in the UI,
-adding a new one, and attempting to pull a repo.
+adding a new one, and attempting to pull a repository.
Then you can backup and delete your `authorized_keys` file for best performance.
diff --git a/doc/administration/operations/index.md b/doc/administration/operations/index.md
index c27832e67ef..45b8e5ad448 100644
--- a/doc/administration/operations/index.md
+++ b/doc/administration/operations/index.md
@@ -12,6 +12,7 @@ Keep your GitLab instance up and running smoothly.
- [Sidekiq MemoryKiller](sidekiq_memory_killer.md): Configure Sidekiq MemoryKiller
to restart Sidekiq.
- [Multiple Sidekiq processes](extra_sidekiq_processes.md): Configure multiple Sidekiq processes to ensure certain queues always have dedicated workers, no matter the number of jobs that need to be processed. **(CORE ONLY)**
+- [Puma](puma.md): Understand Puma and puma-worker-killer.
- [Unicorn](unicorn.md): Understand Unicorn and unicorn-worker-killer.
- Speed up SSH operations by [Authorizing SSH users via a fast,
indexed lookup to the GitLab database](fast_ssh_key_lookup.md), and/or
diff --git a/doc/administration/operations/puma.md b/doc/administration/operations/puma.md
index 6f252a7d76e..af559cf00e9 100644
--- a/doc/administration/operations/puma.md
+++ b/doc/administration/operations/puma.md
@@ -3,7 +3,9 @@
## Puma
As of GitLab 12.9, [Puma](https://github.com/puma/puma) has replaced [Unicorn](https://yhbt.net/unicorn/).
-as the default web server.
+as the default web server. Starting with 13.0, both all-in-one package based
+installations as well as Helm chart based installations will run Puma instead of
+Unicorn unless explicitly specified not to.
## Why switch to Puma?
@@ -14,6 +16,12 @@ Most Rails applications requests normally include a proportion of I/O wait time.
During I/O wait time MRI Ruby will release the GVL (Global VM Lock) to other threads.
Multi-threaded Puma can therefore still serve more requests than a single process.
+## Configuring Puma to replace Unicorn
+
+If you are currently running Unicorn and would like to switch to Puma, server configuration
+will _not_ carry over automatically. For details on matching Unicorn configuration settings with
+the Puma equivalent, where applicable, see [Converting Unicorn settings to Puma](https://docs.gitlab.com/omnibus/settings/puma.html#converting-unicorn-settings-to-puma).
+
## Performance caveat when using Puma with Rugged
For deployments where NFS is used to store Git repository, we allow GitLab to use
diff --git a/doc/administration/operations/sidekiq_memory_killer.md b/doc/administration/operations/sidekiq_memory_killer.md
index 6438dbb9dab..fdccfacc8a9 100644
--- a/doc/administration/operations/sidekiq_memory_killer.md
+++ b/doc/administration/operations/sidekiq_memory_killer.md
@@ -2,13 +2,13 @@
The GitLab Rails application code suffers from memory leaks. For web requests
this problem is made manageable using
-[`unicorn-worker-killer`](https://github.com/kzk/unicorn-worker-killer) which
-restarts Unicorn worker processes in between requests when needed. The Sidekiq
+[`puma-worker-killer`](https://github.com/schneems/puma_worker_killer) which
+restarts Puma worker processes if it exceeds a memory limit. The Sidekiq
MemoryKiller applies the same approach to the Sidekiq processes used by GitLab
to process background jobs.
-Unlike unicorn-worker-killer, which is enabled by default for all GitLab
-installations since GitLab 6.4, the Sidekiq MemoryKiller is enabled by default
+Unlike puma-worker-killer, which is enabled by default for all GitLab
+installations since GitLab 13.0, the Sidekiq MemoryKiller is enabled by default
_only_ for Omnibus packages. The reason for this is that the MemoryKiller
relies on runit to restart Sidekiq after a memory-induced shutdown and GitLab
installations from source do not all use runit or an equivalent.
diff --git a/doc/administration/operations/ssh_certificates.md b/doc/administration/operations/ssh_certificates.md
index eaf0e4ab284..b652f282b7b 100644
--- a/doc/administration/operations/ssh_certificates.md
+++ b/doc/administration/operations/ssh_certificates.md
@@ -50,7 +50,7 @@ the GitLab server itself, but your setup may vary. If the CA is only
used for GitLab consider putting this in the `Match User git` section
(described below).
-The SSH certificates being issued by that CA **MUST** have a "key id"
+The SSH certificates being issued by that CA **MUST** have a "key ID"
corresponding to that user's username on GitLab, e.g. (some output
omitted for brevity):
@@ -77,7 +77,7 @@ own `AuthorizedPrincipalsCommand` to do that mapping instead of using
our provided default.
The important part is that the `AuthorizedPrincipalsCommand` must be
-able to map from the "key id" to a GitLab username in some way, the
+able to map from the "key ID" to a GitLab username in some way, the
default command we ship assumes there's a 1=1 mapping between the two,
since the whole point of this is to allow us to extract a GitLab
username from the key itself, instead of relying on something like the
@@ -122,7 +122,7 @@ into multiple lines of `authorized_keys` output, as described in the
Normally when using the `AuthorizedKeysCommand` with OpenSSH the
principal is some "group" that's allowed to log into that
server. However with GitLab it's only used to appease OpenSSH's
-requirement for it, we effectively only care about the "key id" being
+requirement for it, we effectively only care about the "key ID" being
correct. Once that's extracted GitLab will enforce its own ACLs for
that user (e.g. what projects the user can access).
diff --git a/doc/administration/operations/unicorn.md b/doc/administration/operations/unicorn.md
index bb817e71f5a..50481482f4c 100644
--- a/doc/administration/operations/unicorn.md
+++ b/doc/administration/operations/unicorn.md
@@ -1,5 +1,9 @@
# Understanding Unicorn and unicorn-worker-killer
+NOTE: **Note:**
+Starting with GitLab 13.0, Puma is the default web server used in GitLab
+all-in-one package based installations as well as GitLab Helm chart deployments.
+
## Unicorn
GitLab uses [Unicorn](https://yhbt.net/unicorn/), a pre-forking Ruby web
diff --git a/doc/administration/packages/container_registry.md b/doc/administration/packages/container_registry.md
index aaf1ca29084..503e5fb4a2a 100644
--- a/doc/administration/packages/container_registry.md
+++ b/doc/administration/packages/container_registry.md
@@ -18,16 +18,22 @@ You can read more about the Docker Registry at
**Omnibus GitLab installations**
-If you are using the Omnibus GitLab built in [Let's Encrypt integration](https://docs.gitlab.com/omnibus/settings/ssl.html#lets-encrypt-integration), as of GitLab 12.5, the Container Registry will be automatically enabled on port 5050 of the default domain.
+If you installed GitLab by using the Omnibus installation package, the Container Registry
+may or may not be available by default.
-If you would like to use a separate domain, all you have to do is configure the domain name under which the Container
-Registry will listen to. Read
-[#container-registry-domain-configuration](#container-registry-domain-configuration)
-and pick one of the two options that fits your case.
+The Container Registry is automatically enabled and available on your GitLab domain, port 5050 if:
+
+- You're using the built-in [Let's Encrypt integration](https://docs.gitlab.com/omnibus/settings/ssl.html#lets-encrypt-integration), and
+- You're using GitLab 12.5 or later.
+
+Otherwise, the Container Registry is not enabled. To enable it:
+
+- You can configure it for your [GitLab domain](#configure-container-registry-under-an-existing-gitlab-domain), or
+- You can configure it for [a different domain](#configure-container-registry-under-its-own-domain).
NOTE: **Note:**
-The container registry works under HTTPS by default. Using HTTP is possible
-but not recommended and out of the scope of this document.
+The Container Registry works under HTTPS by default. You can use HTTP
+but it's not recommended and is out of the scope of this document.
Read the [insecure Registry documentation](https://docs.docker.com/registry/insecure/)
if you want to implement this.
@@ -427,7 +433,7 @@ NOTE: **Note:**
By default, users accessing a registry configured with a remote backend are redirected to the default backend for the storage driver. For example, registries can be configured using the `s3` storage driver, which redirects requests to a remote S3 bucket to alleviate load on the GitLab server.
-However, this behaviour is undesirable for registries used by internal hosts that usually can't access public servers. To disable redirects, set the `disable` flag to true as follows. This makes all traffic to always go through the Registry service. This results in improved security (less surface attack as the storage backend is not publicly accessible), but worse performance (all traffic is redirected via the service).
+However, this behavior is undesirable for registries used by internal hosts that usually can't access public servers. To disable redirects, set the `disable` flag to true as follows. This makes all traffic to always go through the Registry service. This results in improved security (less surface attack as the storage backend is not publicly accessible), but worse performance (all traffic is redirected via the service).
**Omnibus GitLab installations**
@@ -454,7 +460,7 @@ However, this behaviour is undesirable for registries used by internal hosts tha
1. Add the `redirect` flag to your registry configuration YML file:
- ```yml
+ ```yaml
storage:
s3:
accesskey: 'AKIAKIAKI'
@@ -520,7 +526,7 @@ on how to achieve that.
NOTE: **Note:**
In using an external container registry, some features associated with the
-container registry may be unavailable or have [inherant risks](./../../user/packages/container_registry/index.md#use-with-external-container-registries)
+container registry may be unavailable or have [inherent risks](./../../user/packages/container_registry/index.md#use-with-external-container-registries)
**Omnibus GitLab**
@@ -650,13 +656,13 @@ notifications:
NOTE: **Note:**
The garbage collection tools are only available when you've installed GitLab
-via an Omnibus package or the cloud native chart.
+via an Omnibus package or the [cloud native chart](https://docs.gitlab.com/charts/charts/registry/#garbage-collection).
DANGER: **Danger:**
By running the built-in garbage collection command, it will cause downtime to
-the Container Registry. Running this command on an instance in an HA environment
-while one of your other instances is still writing to the Registry storage,
-will remove referenced manifests. To avoid that, make sure Registry is set to
+the Container Registry. If you run this command on an instance in an environment
+where one of your other instances is still writing to the Registry storage,
+referenced manifests will be removed. To avoid that, make sure Registry is set to
[read-only mode](#performing-garbage-collection-without-downtime) before proceeding.
Container Registry can use considerable amounts of disk space. To clear up
@@ -710,7 +716,7 @@ built-in command:
If you did not change the default location of the configuration file, run:
-```sh
+```shell
sudo gitlab-ctl registry-garbage-collect
```
@@ -719,7 +725,7 @@ layers you have stored.
If you changed the location of the Container Registry `config.yml`:
-```sh
+```shell
sudo gitlab-ctl registry-garbage-collect /path/to/config.yml
```
@@ -743,7 +749,7 @@ referenced by the registry tag. The `registry-garbage-collect` command supports
`-m` switch to allow you to remove all unreferenced manifests and layers that are
not directly accessible via `tag`:
-```sh
+```shell
sudo gitlab-ctl registry-garbage-collect -m
```
@@ -781,7 +787,7 @@ To enable the read-only mode:
1. Save and reconfigure GitLab:
- ```sh
+ ```shell
sudo gitlab-ctl reconfigure
```
@@ -789,7 +795,7 @@ To enable the read-only mode:
1. Next, trigger one of the garbage collect commands:
- ```sh
+ ```shell
# Recycling unused tags
sudo /opt/gitlab/embedded/bin/registry garbage-collect /var/opt/gitlab/registry/config.yml
@@ -816,7 +822,7 @@ To enable the read-only mode:
1. Save and reconfigure GitLab:
- ```sh
+ ```shell
sudo gitlab-ctl reconfigure
```
diff --git a/doc/administration/pages/index.md b/doc/administration/pages/index.md
index 84133205bd3..21d13be47bd 100644
--- a/doc/administration/pages/index.md
+++ b/doc/administration/pages/index.md
@@ -80,7 +80,7 @@ added `gitlab.io` [in 2016](https://gitlab.com/gitlab-com/infrastructure/issues/
### DNS configuration
GitLab Pages expect to run on their own virtual host. In your DNS server/provider
-you need to add a [wildcard DNS A record][wiki-wildcard-dns] pointing to the
+you need to add a [wildcard DNS A record](https://en.wikipedia.org/wiki/Wildcard_DNS_record) pointing to the
host that GitLab runs. For example, an entry would look like this:
```plaintext
@@ -95,8 +95,6 @@ IPv6 address. If you don't have IPv6, you can omit the AAAA record.
NOTE: **Note:**
You should not use the GitLab domain to serve user pages. For more information see the [security section](#security).
-[wiki-wildcard-dns]: https://en.wikipedia.org/wiki/Wildcard_DNS_record
-
## Configuration
Depending on your needs, you can set up GitLab Pages in 4 different ways.
@@ -354,7 +352,7 @@ This usually results in this error:
For installation from source this can be fixed by installing the custom Certificate
Authority (CA) in the system certificate store.
-For Omnibus, normally this would be fixed by [installing a custom CA in GitLab Omnibus](https://docs.gitlab.com/omnibus/settings/ssl.html#install-custom-public-certificates)
+For Omnibus, normally this would be fixed by [installing a custom CA in Omnibus GitLab](https://docs.gitlab.com/omnibus/settings/ssl.html#install-custom-public-certificates)
but a [bug](https://gitlab.com/gitlab-org/gitlab/issues/25411) is currently preventing
that method from working. Use the following workaround:
@@ -365,14 +363,14 @@ that method from working. Use the following workaround:
echo -n | openssl s_client -connect gitlab-domain-example.com:443 | sed -ne '/-BEGIN CERTIFICATE-/,/-END CERTIFICATE-/p' | sudo tee --append /opt/gitlab/embedded/ssl/certs/cacert.pem
```
-1. [Restart](../restart_gitlab.md) the GitLab Pages Daemon. For GitLab Omnibus instances:
+1. [Restart](../restart_gitlab.md) the GitLab Pages Daemon. For Omnibus GitLab instances:
```shell
sudo gitlab-ctl restart gitlab-pages
```
CAUTION: **Caution:**
-Some GitLab Omnibus upgrades will revert this workaround and you'll need to apply it again.
+Some Omnibus GitLab upgrades will revert this workaround and you'll need to apply it again.
## Activate verbose logging for daemon
@@ -457,9 +455,36 @@ You can run the GitLab Pages daemon on a separate server in order to decrease th
To configure GitLab Pages on a separate server:
+DANGER: **Danger:**
+The following procedure includes steps to back up and edit the
+`gitlab-secrets.json` file. This file contains secrets that control
+database encryption. Proceed with caution.
+
+1. On the **GitLab server**, to enable Pages, add the following to `/etc/gitlab/gitlab.rb`:
+
+ ```ruby
+ gitlab_pages['enable'] = true
+ ```
+
+1. Optionally, to enable [access control](#access-control), add the following to `/etc/gitlab/gitlab.rb`:
+
+ ```ruby
+ gitlab_pages['access_control'] = true
+ ```
+
+1. [Reconfigure the **GitLab server**](../restart_gitlab.md#omnibus-gitlab-reconfigure) for the
+ changes to take effect. The `gitlab-secrets.json` file is now updated with the
+ new configuration.
+
+1. Create a backup of the secrets file on the **GitLab server**:
+
+ ```shell
+ cp /etc/gitlab/gitlab-secrets.json /etc/gitlab/gitlab-secrets.json.bak
+ ```
+
1. Set up a new server. This will become the **Pages server**.
-1. Create an NFS share on the new server and configure this share to
+1. Create an [NFS share](../high_availability/nfs_host_client_setup.md) on the new server and configure this share to
allow access from your main **GitLab server**. For this example, we use the
default GitLab Pages folder `/var/opt/gitlab/gitlab-rails/shared/pages`
as the shared folder on the new server and we will mount it to `/mnt/pages`
@@ -474,7 +499,7 @@ To configure GitLab Pages on a separate server:
postgresql['enable'] = false
redis['enable'] = false
prometheus['enable'] = false
- unicorn['enable'] = false
+ puma['enable'] = false
sidekiq['enable'] = false
gitlab_workhorse['enable'] = false
gitaly['enable'] = false
@@ -483,6 +508,15 @@ To configure GitLab Pages on a separate server:
gitlab_rails['auto_migrate'] = false
```
+1. Create a backup of the secrets file on the **Pages server**:
+
+ ```shell
+ cp /etc/gitlab/gitlab-secrets.json /etc/gitlab/gitlab-secrets.json.bak
+ ```
+
+1. Copy the `/etc/gitlab/gitlab-secrets.json` file from the **GitLab server**
+ to the **Pages server**.
+
1. [Reconfigure GitLab](../restart_gitlab.md#omnibus-gitlab-reconfigure) for the changes to take effect.
1. On the **GitLab server**, make the following changes to `/etc/gitlab/gitlab.rb`:
@@ -502,61 +536,6 @@ configuring a load balancer to work at the IP level, and so on. If you wish to
set up GitLab Pages on multiple servers, perform the above procedure for each
Pages server.
-### Access control when running GitLab Pages on a separate server
-
-If you are [running GitLab Pages on a separate server](#running-gitlab-pages-on-a-separate-server),
-then you must use the following procedure to configure [access control](#access-control):
-
-1. On the **GitLab server**, add the following to `/etc/gitlab/gitlab.rb`:
-
- ```ruby
- gitlab_pages['enable'] = true
- gitlab_pages['access_control'] = true
- ```
-
-1. [Reconfigure GitLab](../restart_gitlab.md#omnibus-gitlab-reconfigure) for the
- changes to take effect. The `gitlab-secrets.json` file is now updated with the
- new configuration.
-
- DANGER: **Danger:**
- The `gitlab-secrets.json` file contains secrets that control database encryption.
- Do not edit or replace this file on the **GitLab server** or you might
- experience permanent data loss. Make a backup copy of this file before proceeding,
- as explained in the following steps.
-
-1. Create a backup of the secrets file on the **GitLab server**:
-
- ```shell
- cp /etc/gitlab/gitlab-secrets.json /etc/gitlab/gitlab-secrets.json.bak
- ```
-
-1. Create a backup of the secrets file on the **Pages server**:
-
- ```shell
- cp /etc/gitlab/gitlab-secrets.json /etc/gitlab/gitlab-secrets.json.bak
- ```
-
-1. Disable Pages on the **GitLab server** by setting the following in
- `/etc/gitlab/gitlab.rb`:
-
- ```ruby
- gitlab_pages['enable'] = false
- ```
-
-1. [Reconfigure GitLab](../restart_gitlab.md#omnibus-gitlab-reconfigure) for the changes to take effect.
-
-1. Copy the `/etc/gitlab/gitlab-secrets.json` file from the **GitLab server**
- to the **Pages server**.
-
-1. On your **Pages server**, add the following to `/etc/gitlab/gitlab.rb`:
-
- ```ruby
- gitlab_pages['gitlab_server'] = "https://<your-gitlab-server-URL>"
- gitlab_pages['access_control'] = true
- ```
-
-1. [Reconfigure GitLab](../restart_gitlab.md#omnibus-gitlab-reconfigure) for the changes to take effect.
-
## Backup
GitLab Pages are part of the [regular backup](../../raketasks/backup_restore.md), so there is no separate backup to configure.
diff --git a/doc/administration/pages/source.md b/doc/administration/pages/source.md
index 94d2c5420aa..1bb3b86b419 100644
--- a/doc/administration/pages/source.md
+++ b/doc/administration/pages/source.md
@@ -61,7 +61,7 @@ Before proceeding with the Pages configuration, make sure that:
### DNS configuration
GitLab Pages expect to run on their own virtual host. In your DNS server/provider
-you need to add a [wildcard DNS A record][wiki-wildcard-dns] pointing to the
+you need to add a [wildcard DNS A record](https://en.wikipedia.org/wiki/Wildcard_DNS_record) pointing to the
host that GitLab runs. For example, an entry would look like this:
```plaintext
@@ -75,8 +75,6 @@ and `192.0.2.1` is the IP address of your GitLab instance.
You should not use the GitLab domain to serve user pages. For more information
see the [security section](#security).
-[wiki-wildcard-dns]: https://en.wikipedia.org/wiki/Wildcard_DNS_record
-
## Configuration
Depending on your needs, you can set up GitLab Pages in 4 different ways.
diff --git a/doc/administration/pseudonymizer.md b/doc/administration/pseudonymizer.md
index 36bb446da78..22543a5c743 100644
--- a/doc/administration/pseudonymizer.md
+++ b/doc/administration/pseudonymizer.md
@@ -1,6 +1,6 @@
# Pseudonymizer **(ULTIMATE)**
-> [Introduced](https://gitlab.com/gitlab-org/gitlab/-/merge_requests/5532) in [GitLab Ultimate][ee] 11.1.
+> [Introduced](https://gitlab.com/gitlab-org/gitlab/-/merge_requests/5532) in [GitLab Ultimate](https://about.gitlab.com/pricing/) 11.1.
As GitLab's database hosts sensitive information, using it unfiltered for analytics
implies high security requirements. To help alleviate this constraint, the Pseudonymizer
@@ -101,5 +101,3 @@ This will produce some CSV files that might be very large, so make sure the
After the pseudonymizer has run, the output CSV files should be uploaded to the
configured object storage and deleted from the local disk.
-
-[ee]: https://about.gitlab.com/pricing/
diff --git a/doc/administration/raketasks/check.md b/doc/administration/raketasks/check.md
index c0edb43cc01..da5caf3966f 100644
--- a/doc/administration/raketasks/check.md
+++ b/doc/administration/raketasks/check.md
@@ -1,6 +1,8 @@
-# Integrity Check Rake Task
+# Integrity check Rake task **(CORE ONLY)**
-## Repository Integrity
+GitLab provides Rake tasks to check the integrity of various components.
+
+## Repository integrity
Even though Git is very resilient and tries to prevent data integrity issues,
there are times when things go wrong. The following Rake tasks intend to
@@ -43,7 +45,7 @@ sudo gitlab-rake gitlab:git:fsck
sudo -u git -H bundle exec rake gitlab:git:fsck RAILS_ENV=production
```
-## Uploaded Files Integrity
+## Uploaded files integrity
Various types of files can be uploaded to a GitLab installation by users.
These integrity checks can detect missing files. Additionally, for locally
@@ -127,11 +129,9 @@ Checking integrity of Uploads
Done!
```
-## LDAP Check
+## LDAP check
The LDAP check Rake task will test the bind DN and password credentials
(if configured) and will list a sample of LDAP users. This task is also
executed as part of the `gitlab:check` task, but can run independently.
See [LDAP Rake Tasks - LDAP Check](ldap.md#check) for details.
-
-[git-fsck]: https://git-scm.com/docs/git-fsck
diff --git a/doc/administration/raketasks/geo.md b/doc/administration/raketasks/geo.md
index 707ed1dbee0..71e4f922348 100644
--- a/doc/administration/raketasks/geo.md
+++ b/doc/administration/raketasks/geo.md
@@ -1,9 +1,11 @@
# Geo Rake Tasks **(PREMIUM ONLY)**
+The following Rake tasks are for [Geo installations](../geo/replication/index.md).
+
## Git housekeeping
There are few tasks you can run to schedule a Git housekeeping to start at the
-next repository sync in a **Secondary node**:
+next repository sync in a **secondary** node:
### Incremental Repack
diff --git a/doc/administration/raketasks/github_import.md b/doc/administration/raketasks/github_import.md
index 4c6521f38ec..83a3d2c8884 100644
--- a/doc/administration/raketasks/github_import.md
+++ b/doc/administration/raketasks/github_import.md
@@ -1,4 +1,4 @@
-# GitHub import
+# GitHub import **(CORE ONLY)**
> [Introduced]( https://gitlab.com/gitlab-org/gitlab-foss/-/merge_requests/10308) in GitLab 9.1.
@@ -9,7 +9,7 @@ which will become the owner of the project. You can resume an import
with the same command.
Bear in mind that the syntax is very specific. Remove any spaces within the argument block and
-before/after the brackets. Also, Some shells (e.g., zsh) can interpret the open/close brackets
+before/after the brackets. Also, some shells (for example, `zsh`) can interpret the open/close brackets
(`[]`) separately. You may need to either escape the brackets or use double quotes.
## Importing multiple projects
diff --git a/doc/administration/raketasks/ldap.md b/doc/administration/raketasks/ldap.md
index 361ab28f6d4..2871a9235a3 100644
--- a/doc/administration/raketasks/ldap.md
+++ b/doc/administration/raketasks/ldap.md
@@ -1,4 +1,6 @@
-# LDAP Rake Tasks
+# LDAP Rake tasks **(CORE ONLY)**
+
+The following are LDAP-related Rake tasks.
## Check
@@ -26,7 +28,7 @@ limit by passing a number to the check task:
rake gitlab:ldap:check[50]
```
-## Run a Group Sync
+## Run a group sync **(STARTER ONLY)**
> [Introduced](https://gitlab.com/gitlab-org/gitlab/-/merge_requests/14735) in [GitLab Starter](https://about.gitlab.com/pricing/) 12.2.
diff --git a/doc/administration/raketasks/maintenance.md b/doc/administration/raketasks/maintenance.md
index c79a1aa6ba1..eee68c0da0a 100644
--- a/doc/administration/raketasks/maintenance.md
+++ b/doc/administration/raketasks/maintenance.md
@@ -1,8 +1,11 @@
-# Maintenance Rake Tasks
+# Maintenance Rake tasks **(CORE ONLY)**
-## Gather information about GitLab and the system it runs on
+GitLab provides Rake tasks for general maintenance.
-This command gathers information about your GitLab installation and the System it runs on. These may be useful when asking for help or reporting issues.
+## Gather GitLab and system information
+
+This command gathers information about your GitLab installation and the system it runs on.
+These may be useful when asking for help or reporting issues.
**Omnibus Installation**
@@ -50,20 +53,23 @@ Git: /usr/bin/git
## Check GitLab configuration
-Runs the following Rake tasks:
+The `gitlab:check` Rake task runs the following Rake tasks:
- `gitlab:gitlab_shell:check`
- `gitlab:gitaly:check`
- `gitlab:sidekiq:check`
- `gitlab:app:check`
-It will check that each component was set up according to the installation guide and suggest fixes for issues found.
-This command must be run from your app server and will not work correctly on component servers like [Gitaly](../gitaly/index.md#running-gitaly-on-its-own-server).
+It will check that each component was set up according to the installation guide and suggest fixes
+for issues found. This command must be run from your application server and will not work correctly on
+component servers like [Gitaly](../gitaly/index.md#running-gitaly-on-its-own-server).
+
+You may also have a look at our troubleshooting guides for:
-You may also have a look at our Troubleshooting Guides:
+- [GitLab](../index.md#troubleshooting)
+- [Omnibus GitLab](https://docs.gitlab.com/omnibus/README.html#troubleshooting)
-- [Troubleshooting Guide (GitLab)](../index.md#troubleshooting)
-- [Troubleshooting Guide (Omnibus GitLab)](https://docs.gitlab.com/omnibus/README.html#troubleshooting)
+To run `gitlab:check`, run:
**Omnibus Installation**
@@ -77,7 +83,8 @@ sudo gitlab-rake gitlab:check
bundle exec rake gitlab:check RAILS_ENV=production
```
-NOTE: Use `SANITIZE=true` for `gitlab:check` if you want to omit project names from the output.
+NOTE: **Note:**
+Use `SANITIZE=true` for `gitlab:check` if you want to omit project names from the output.
Example output:
@@ -126,7 +133,7 @@ Checking GitLab ... Finished
## Rebuild authorized_keys file
-In some case it is necessary to rebuild the `authorized_keys` file.
+In some case it is necessary to rebuild the `authorized_keys` file. To do this, run:
**Omnibus Installation**
@@ -141,6 +148,8 @@ cd /home/git/gitlab
sudo -u git -H bundle exec rake gitlab:shell:setup RAILS_ENV=production
```
+Example output:
+
```plaintext
This will rebuild an authorized_keys file.
You will lose any data stored in authorized_keys file.
@@ -149,8 +158,8 @@ Do you want to continue (yes/no)? yes
## Clear Redis cache
-If for some reason the dashboard shows wrong information you might want to
-clear Redis' cache.
+If for some reason the dashboard displays the wrong information, you might want to
+clear Redis' cache. To do this, run:
**Omnibus Installation**
@@ -170,7 +179,7 @@ sudo -u git -H bundle exec rake cache:clear RAILS_ENV=production
Sometimes during version upgrades you might end up with some wrong CSS or
missing some icons. In that case, try to precompile the assets again.
-Note that this only applies to source installations and does NOT apply to
+This only applies to source installations and does NOT apply to
Omnibus packages.
**Source Installation**
@@ -187,25 +196,6 @@ production machine after installing the package, there should be no reason to re
`rake gitlab:assets:compile` on the production machine. If you suspect that assets
have been corrupted, you should reinstall the omnibus package.
-## Tracking Deployments
-
-GitLab provides a Rake task that lets you track deployments in GitLab
-Performance Monitoring. This Rake task simply stores the current GitLab version
-in the GitLab Performance Monitoring database.
-
-**Omnibus Installation**
-
-```shell
-sudo gitlab-rake gitlab:track_deployment
-```
-
-**Source Installation**
-
-```shell
-cd /home/git/gitlab
-sudo -u git -H bundle exec rake gitlab:track_deployment RAILS_ENV=production
-```
-
## Check TCP connectivity to a remote site
Sometimes you need to know if your GitLab installation can connect to a TCP
diff --git a/doc/administration/raketasks/praefect.md b/doc/administration/raketasks/praefect.md
index 4f9cf0e10ba..c3dadb6bc30 100644
--- a/doc/administration/raketasks/praefect.md
+++ b/doc/administration/raketasks/praefect.md
@@ -1,4 +1,4 @@
-# Praefect Rake Tasks
+# Praefect Rake Tasks **(CORE ONLY)**
> [Introduced]( https://gitlab.com/gitlab-org/gitlab/-/merge_requests/28369) in GitLab 12.10.
diff --git a/doc/administration/raketasks/project_import_export.md b/doc/administration/raketasks/project_import_export.md
index 6d874d596e1..2ab8b13e29e 100644
--- a/doc/administration/raketasks/project_import_export.md
+++ b/doc/administration/raketasks/project_import_export.md
@@ -3,11 +3,13 @@
> - [Introduced](https://gitlab.com/gitlab-org/gitlab-foss/issues/3050) in GitLab 8.9.
> - From GitLab 11.3, import/export can use object storage automatically.
-See also:
+GitLab provides Rake tasks relating to project import and export. For more information, see:
- [Project import/export documentation](../../user/project/settings/import_export.md).
- [Project import/export API](../../api/project_import_export.md).
+## Import/export tasks
+
The GitLab import/export version can be checked by using the following command:
```shell
@@ -28,8 +30,6 @@ sudo gitlab-rake gitlab:import_export:data
bundle exec rake gitlab:import_export:data RAILS_ENV=production
```
-## Important notes
-
Note the following:
- Importing is only possible if the version of the import and export GitLab instances are
diff --git a/doc/administration/raketasks/storage.md b/doc/administration/raketasks/storage.md
index 1e487a317c9..30f50c24138 100644
--- a/doc/administration/raketasks/storage.md
+++ b/doc/administration/raketasks/storage.md
@@ -1,214 +1,162 @@
-# Repository Storage Rake Tasks
+# Repository storage Rake tasks **(CORE ONLY)**
-This is a collection of Rake tasks you can use to help you list and migrate
-existing projects and attachments associated with it from Legacy storage to
-the new Hashed storage type.
+This is a collection of Rake tasks to help you list and migrate
+existing projects and their attachments to the new
+[hashed storage](../repository_storage_types.md) that GitLab
+uses to organize the Git data.
-You can read more about the storage types [here][storage-types].
+## List projects and attachments
-## Migrate existing projects to Hashed storage
+The following Rake tasks will list the projects and attachments that are
+available on legacy and hashed storage.
-Before migrating your existing projects, you should
-[enable hashed storage][storage-migration] for the new projects as well.
+### On legacy storage
-This task will schedule all your existing projects and attachments associated with it to be migrated to the
-**Hashed** storage type:
+To have a summary and then a list of projects and their attachments using legacy storage:
-**Omnibus Installation**
+- **Omnibus installation**
-```shell
-sudo gitlab-rake gitlab:storage:migrate_to_hashed
-```
-
-**Source Installation**
-
-```shell
-sudo -u git -H bundle exec rake gitlab:storage:migrate_to_hashed RAILS_ENV=production
-```
-
-They both also accept a range as environment variable:
-
-```shell
-# to migrate any non migrated project from ID 20 to 50.
-export ID_FROM=20
-export ID_TO=50
-```
-
-You can monitor the progress in the **Admin Area > Monitoring > Background Jobs** page.
-There is a specific Queue you can watch to see how long it will take to finish:
-`hashed_storage:hashed_storage_project_migrate`
-
-After it reaches zero, you can confirm every project has been migrated by running the commands bellow.
-If you find it necessary, you can run this migration script again to schedule missing projects.
-
-Any error or warning will be logged in Sidekiq's log file.
-
-NOTE: **Note:**
-If Geo is enabled, each project that is successfully migrated generates an event to replicate the changes on any **secondary** nodes.
-
-You only need the `gitlab:storage:migrate_to_hashed` Rake task to migrate your repositories, but we have additional
-commands below that helps you inspect projects and attachments in both legacy and hashed storage.
-
-## Rollback from Hashed storage to Legacy storage
-
-If you need to rollback the storage migration for any reason, you can follow the steps described here.
-
-NOTE: **Note:** Hashed Storage will be required in future version of GitLab.
-
-To prevent new projects from being created in the Hashed storage,
-you need to undo the [enable hashed storage][storage-migration] changes.
-
-This task will schedule all your existing projects and associated attachments to be rolled back to the
-Legacy storage type.
-
-For Omnibus installations, run the following:
-
-```shell
-sudo gitlab-rake gitlab:storage:rollback_to_legacy
-```
-
-For source installations, run the following:
-
-```shell
-sudo -u git -H bundle exec rake gitlab:storage:rollback_to_legacy RAILS_ENV=production
-```
-
-Both commands accept a range as environment variable:
-
-```shell
-# to rollback any migrated project from ID 20 to 50.
-export ID_FROM=20
-export ID_TO=50
-```
-
-You can monitor the progress in the **Admin Area > Monitoring > Background Jobs** page.
-On the **Queues** tab, you can watch the `hashed_storage:hashed_storage_project_rollback` queue to see how long the process will take to finish.
-
-After it reaches zero, you can confirm every project has been rolled back by running the commands bellow.
-If some projects weren't rolled back, you can run this rollback script again to schedule further rollbacks.
-
-Any error or warning will be logged in Sidekiq's log file.
-
-## List projects on Legacy storage
+ ```shell
+ # Projects
+ sudo gitlab-rake gitlab:storage:legacy_projects
+ sudo gitlab-rake gitlab:storage:list_legacy_projects
-To have a simple summary of projects using **Legacy** storage:
+ # Attachments
+ sudo gitlab-rake gitlab:storage:legacy_attachments
+ sudo gitlab-rake gitlab:storage:list_legacy_attachments
+ ```
-**Omnibus Installation**
+- **Source installation**
-```shell
-sudo gitlab-rake gitlab:storage:legacy_projects
-```
-
-**Source Installation**
-
-```shell
-sudo -u git -H bundle exec rake gitlab:storage:legacy_projects RAILS_ENV=production
-```
+ ```shell
+ # Projects
+ sudo -u git -H bundle exec rake gitlab:storage:legacy_projects RAILS_ENV=production
+ sudo -u git -H bundle exec rake gitlab:storage:list_legacy_projects RAILS_ENV=production
-To list projects using **Legacy** storage:
+ # Attachments
+ sudo -u git -H bundle exec rake gitlab:storage:legacy_attachments RAILS_ENV=production
+ sudo -u git -H bundle exec rake gitlab:storage:list_legacy_attachments RAILS_ENV=production
+ ```
-**Omnibus Installation**
-
-```shell
-sudo gitlab-rake gitlab:storage:list_legacy_projects
-```
-
-**Source Installation**
-
-```shell
-sudo -u git -H bundle exec rake gitlab:storage:list_legacy_projects RAILS_ENV=production
+### On hashed storage
-```
+To have a summary and then a list of projects and their attachments using hashed storage:
-## List projects on Hashed storage
+- **Omnibus installation**
-To have a simple summary of projects using **Hashed** storage:
+ ```shell
+ # Projects
+ sudo gitlab-rake gitlab:storage:hashed_projects
+ sudo gitlab-rake gitlab:storage:list_hashed_projects
-**Omnibus Installation**
+ # Attachments
+ sudo gitlab-rake gitlab:storage:hashed_attachments
+ sudo gitlab-rake gitlab:storage:list_hashed_attachments
+ ```
-```shell
-sudo gitlab-rake gitlab:storage:hashed_projects
-```
+- **Source installation**
-**Source Installation**
+ ```shell
+ # Projects
+ sudo -u git -H bundle exec rake gitlab:storage:hashed_projects RAILS_ENV=production
+ sudo -u git -H bundle exec rake gitlab:storage:list_hashed_projects RAILS_ENV=production
-```shell
-sudo -u git -H bundle exec rake gitlab:storage:hashed_projects RAILS_ENV=production
-```
+ # Attachments
+ sudo -u git -H bundle exec rake gitlab:storage:hashed_attachments RAILS_ENV=production
+ sudo -u git -H bundle exec rake gitlab:storage:list_hashed_attachments RAILS_ENV=production
+ ```
-To list projects using **Hashed** storage:
+## Migrate to hashed storage
-**Omnibus Installation**
+NOTE: **Note:**
+In GitLab 13.0, [hashed storage](../repository_storage_types.md#hashed-storage)
+is enabled by default and the legacy storage is deprecated.
+Support for legacy storage will be removed in GitLab 14.0. If you're on GitLab
+13.0 and later, switching new projects to legacy storage is not possible.
+The option to choose between hashed and legacy storage in the admin area has
+been disabled.
-```shell
-sudo gitlab-rake gitlab:storage:list_hashed_projects
-```
+This task will schedule all your existing projects and attachments associated
+with it to be migrated to the **Hashed** storage type:
-**Source Installation**
+- **Omnibus installation**
-```shell
-sudo -u git -H bundle exec rake gitlab:storage:list_hashed_projects RAILS_ENV=production
-```
+ ```shell
+ sudo gitlab-rake gitlab:storage:migrate_to_hashed
+ ```
-## List attachments on Legacy storage
+- **Source installation**
-To have a simple summary of project attachments using **Legacy** storage:
+ ```shell
+ sudo -u git -H bundle exec rake gitlab:storage:migrate_to_hashed RAILS_ENV=production
+ ```
-**Omnibus Installation**
+If you have any existing integration, you may want to do a small rollout first,
+to validate. You can do so by specifying an ID range with the operation by using
+the environment variables `ID_FROM` and `ID_TO`. For example, to limit the rollout
+to project IDs 50 to 100 in an Omnibus GitLab installation:
```shell
-sudo gitlab-rake gitlab:storage:legacy_attachments
+sudo gitlab-rake gitlab:storage:migrate_to_hashed ID_FROM=50 ID_TO=100
```
-**Source Installation**
+You can monitor the progress in the **{admin}** **Admin Area > Monitoring > Background Jobs** page.
+There is a specific queue you can watch to see how long it will take to finish:
+`hashed_storage:hashed_storage_project_migrate`.
-```shell
-sudo -u git -H bundle exec rake gitlab:storage:legacy_attachments RAILS_ENV=production
-```
-
-To list project attachments using **Legacy** storage:
-
-**Omnibus Installation**
+After it reaches zero, you can confirm every project has been migrated by running the commands bellow.
+If you find it necessary, you can run this migration script again to schedule missing projects.
-```shell
-sudo gitlab-rake gitlab:storage:list_legacy_attachments
-```
+Any error or warning will be logged in Sidekiq's log file.
-**Source Installation**
+NOTE: **Note:**
+If [Geo](../geo/replication/index.md) is enabled, each project that is successfully migrated
+generates an event to replicate the changes on any **secondary** nodes.
-```shell
-sudo -u git -H bundle exec rake gitlab:storage:list_legacy_attachments RAILS_ENV=production
-```
+You only need the `gitlab:storage:migrate_to_hashed` Rake task to migrate your repositories, but we have additional
+commands below that helps you inspect projects and attachments in both legacy and hashed storage.
-## List attachments on Hashed storage
+## Rollback from hashed storage to legacy storage
-To have a simple summary of project attachments using **Hashed** storage:
+NOTE: **Deprecated:**
+In GitLab 13.0, [hashed storage](../repository_storage_types.md#hashed-storage)
+is enabled by default and the legacy storage is deprecated.
+Support for legacy storage will be removed in GitLab 14.0. If you're on GitLab
+13.0 and later, switching new projects to legacy storage is not possible.
+The option to choose between hashed and legacy storage in the admin area has
+been disabled.
-**Omnibus Installation**
+This task will schedule all your existing projects and associated attachments to be rolled back to the
+legacy storage type.
-```shell
-sudo gitlab-rake gitlab:storage:hashed_attachments
-```
+- **Omnibus installation**
-**Source Installation**
+ ```shell
+ sudo gitlab-rake gitlab:storage:rollback_to_legacy
+ ```
-```shell
-sudo -u git -H bundle exec rake gitlab:storage:hashed_attachments RAILS_ENV=production
-```
+- **Source installation**
-To list project attachments using **Hashed** storage:
+ ```shell
+ sudo -u git -H bundle exec rake gitlab:storage:rollback_to_legacy RAILS_ENV=production
+ ```
-**Omnibus Installation**
+If you have any existing integration, you may want to do a small rollback first,
+to validate. You can do so by specifying an ID range with the operation by using
+the environment variables `ID_FROM` and `ID_TO`. For example, to limit the rollout
+to project IDs 50 to 100 in an Omnibus GitLab installation:
```shell
-sudo gitlab-rake gitlab:storage:list_hashed_attachments
+sudo gitlab-rake gitlab:storage:rollback_to_legacy ID_FROM=50 ID_TO=100
```
-**Source Installation**
+You can monitor the progress in the **{admin}** **Admin Area > Monitoring > Background Jobs** page.
+On the **Queues** tab, you can watch the `hashed_storage:hashed_storage_project_rollback` queue to see how long the process will take to finish.
-```shell
-sudo -u git -H bundle exec rake gitlab:storage:list_hashed_attachments RAILS_ENV=production
-```
+After it reaches zero, you can confirm every project has been rolled back by running the commands bellow.
+If some projects weren't rolled back, you can run this rollback script again to schedule further rollbacks.
+Any error or warning will be logged in Sidekiq's log file.
-[storage-types]: ../repository_storage_types.md
-[storage-migration]: ../repository_storage_types.md#how-to-migrate-to-hashed-storage
+If you have a Geo setup, the rollback will not be reflected automatically
+on the **secondary** node. You may need to wait for a backfill operation to kick-in and remove
+the remaining repositories from the special `@hashed/` folder manually.
diff --git a/doc/administration/raketasks/uploads/migrate.md b/doc/administration/raketasks/uploads/migrate.md
index 851305d433f..d58b802b024 100644
--- a/doc/administration/raketasks/uploads/migrate.md
+++ b/doc/administration/raketasks/uploads/migrate.md
@@ -1,21 +1,26 @@
-# Uploads Migrate Rake Tasks
+# Uploads migrate Rake tasks **(CORE ONLY)**
-## Migrate to Object Storage
+`gitlab:uploads:migrate` migrates uploads between different storage types.
-After [configuring the object storage](../../uploads.md#using-object-storage-core-only) for GitLab's uploads, you may use this task to migrate existing uploads from the local storage to the remote storage.
+## Migrate to object storage
->**Note:**
-All of the processing will be done in a background worker and requires **no downtime**.
+After [configuring the object storage](../../uploads.md#using-object-storage-core-only) for GitLab's
+uploads, use this task to migrate existing uploads from the local storage to the remote storage.
+
+Read more about using [object storage with GitLab](../../object_storage.md).
-[Read more about using object storage with GitLab](../../object_storage.md).
+NOTE: **Note:**
+All of the processing will be done in a background worker and requires **no downtime**.
### All-in-one Rake task
-GitLab provides a wrapper Rake task that migrates all uploaded files - avatars,
-logos, attachments, favicon, etc. - to object storage in one go. Under the hood,
-it invokes individual Rake tasks to migrate files falling under each of this
-category one by one. The specifications of these individual Rake tasks are
-described in the next section.
+GitLab provides a wrapper Rake task that migrates all uploaded files (for example avatars, logos,
+attachments, and favicon) to object storage in one step. The wrapper task invokes individual Rake
+tasks to migrate files falling under each of these categories one by one.
+
+These [individual Rake tasks](#individual-rake-tasks) are described in the next section.
+
+To migrate all uploads from local storage to object storage, run:
**Omnibus Installation**
@@ -31,26 +36,29 @@ sudo RAILS_ENV=production -u git -H bundle exec rake gitlab:uploads:migrate:all
### Individual Rake tasks
->**Note:**
-If you already ran the Rake task mentioned above, no need to run these individual Rake tasks as that has been done automatically.
+If you already ran the [all-in-one Rake task](#all-in-one-rake-task), there is no need to run these
+individual tasks.
+
+The Rake task uses three parameters to find uploads to migrate:
-The Rake task uses 3 parameters to find uploads to migrate.
+| Parameter | Type | Description |
+|:-----------------|:--------------|:-------------------------------------------------------|
+| `uploader_class` | string | Type of the uploader to migrate from. |
+| `model_class` | string | Type of the model to migrate from. |
+| `mount_point` | string/symbol | Name of the model's column the uploader is mounted on. |
-Parameter | Type | Description
---------- | ---- | -----------
-`uploader_class` | string | Type of the uploader to migrate from
-`model_class` | string | Type of the model to migrate from
-`mount_point` | string/symbol | Name of the model's column on which the uploader is mounted on.
+NOTE: **Note:**
+These parameters are mainly internal to GitLab's structure, you may want to refer to the task list
+instead below.
->**Note:**
-These parameters are mainly internal to GitLab's structure, you may want to refer to the task list instead below.
+This task also accepts an environment variable which you can use to override
+the default batch size:
-This task also accepts some environment variables which you can use to override
-certain values:
+| Variable | Type | Description |
+|:---------|:--------|:--------------------------------------------------|
+| `BATCH` | integer | Specifies the size of the batch. Defaults to 200. |
-Variable | Type | Description
--------- | ---- | -----------
-`BATCH` | integer | Specifies the size of the batch. Defaults to 200.
+The following shows how to run `gitlab:uploads:migrate` for individual types of uploads.
**Omnibus Installation**
@@ -76,13 +84,13 @@ gitlab-rake "gitlab:uploads:migrate[PersonalFileUploader, Snippet]"
gitlab-rake "gitlab:uploads:migrate[NamespaceFileUploader, Snippet]"
gitlab-rake "gitlab:uploads:migrate[FileUploader, MergeRequest]"
-# Design Management design thumbnails (EE)
+# Design Management design thumbnails
gitlab-rake "gitlab:uploads:migrate[DesignManagement::DesignV432x230Uploader, DesignManagement::Action, :image_v432x230]"
```
**Source Installation**
->**Note:**
+NOTE: **Note:**
Use `RAILS_ENV=production` for every task.
```shell
@@ -107,16 +115,14 @@ sudo -u git -H bundle exec rake "gitlab:uploads:migrate[PersonalFileUploader, Sn
sudo -u git -H bundle exec rake "gitlab:uploads:migrate[NamespaceFileUploader, Snippet]"
sudo -u git -H bundle exec rake "gitlab:uploads:migrate[FileUploader, MergeRequest]"
-# Design Management design thumbnails (EE)
+# Design Management design thumbnails
sudo -u git -H bundle exec rake "gitlab:uploads:migrate[DesignManagement::DesignV432x230Uploader, DesignManagement::Action]"
```
-## Migrate from object storage to local storage
+## Migrate to local storage
-If you need to disable Object Storage for any reason, first you need to migrate
-your data out of Object Storage and back into your local storage.
-
-**Before proceeding, it is important to disable both `direct_upload` and `background_upload` under `uploads` settings in `gitlab.rb`**
+If you need to disable [object storage](../../object_storage.md) for any reason, you must first
+migrate your data out of object storage and back into your local storage.
CAUTION: **Warning:**
**Extended downtime is required** so no new files are created in object storage during
@@ -126,23 +132,29 @@ To follow progress, see the [relevant issue](https://gitlab.com/gitlab-org/gitla
### All-in-one Rake task
-GitLab provides a wrapper Rake task that migrates all uploaded files - avatars,
-logos, attachments, favicon, etc. - to local storage in one go. Under the hood,
-it invokes individual Rake tasks to migrate files falling under each of this
-category one by one. For details on these Rake tasks please [refer to the section above](#individual-rake-tasks),
+GitLab provides a wrapper Rake task that migrates all uploaded files (for example, avatars, logos,
+attachments, and favicon) to local storage in one step. The wrapper task invokes individual Rake
+tasks to migrate files falling under each of these categories one by one.
+
+For details on these Rake tasks, refer to [Individual Rake tasks](#individual-rake-tasks),
keeping in mind the task name in this case is `gitlab:uploads:migrate_to_local`.
-**Omnibus Installation**
+To migrate uploads from object storage to local storage:
-```shell
-gitlab-rake "gitlab:uploads:migrate_to_local:all"
-```
+1. Disable both `direct_upload` and `background_upload` under `uploads` settings in `gitlab.rb`.
+1. Run the Rake task:
-**Source Installation**
+ **Omnibus Installation**
-```shell
-sudo RAILS_ENV=production -u git -H bundle exec rake gitlab:uploads:migrate_to_local:all
-```
+ ```shell
+ gitlab-rake "gitlab:uploads:migrate_to_local:all"
+ ```
+
+ **Source Installation**
+
+ ```shell
+ sudo RAILS_ENV=production -u git -H bundle exec rake gitlab:uploads:migrate_to_local:all
+ ```
-After this is done, you may disable Object Storage by undoing the changes described
-in the instructions to [configure object storage](../../uploads.md#using-object-storage-core-only)
+After running the Rake task, you can disable object storage by undoing the changes described
+in the instructions to [configure object storage](../../uploads.md#using-object-storage-core-only).
diff --git a/doc/administration/raketasks/uploads/sanitize.md b/doc/administration/raketasks/uploads/sanitize.md
index c00399a27cf..7c760b95c33 100644
--- a/doc/administration/raketasks/uploads/sanitize.md
+++ b/doc/administration/raketasks/uploads/sanitize.md
@@ -1,8 +1,13 @@
-# Uploads Sanitize tasks
+# Uploads sanitize Rake tasks **(CORE ONLY)**
+
+Since GitLab 11.9, EXIF data is automatically stripped from JPG or TIFF image uploads.
+
+EXIF data may contain sensitive information (for example, GPS location), so you
+can remove EXIF data from existing images that were uploaded to an earlier version of GitLab.
## Requirements
-You need `exiftool` installed on your system. If you installed GitLab:
+To run this Rake task, you need `exiftool` installed on your system. If you installed GitLab:
- Using the Omnibus package, you're all set.
- From source, make sure `exiftool` is installed:
@@ -17,48 +22,48 @@ You need `exiftool` installed on your system. If you installed GitLab:
## Remove EXIF data from existing uploads
-Since 11.9 EXIF data are automatically stripped from JPG or TIFF image uploads.
-Because EXIF data may contain sensitive information (e.g. GPS location), you
-can remove EXIF data also from existing images which were uploaded before
-with the following command:
+To remove EXIF data from existing uploads, run the following command:
```shell
sudo RAILS_ENV=production -u git -H bundle exec rake gitlab:uploads:sanitize:remove_exif
```
-This command by default runs in dry mode and it doesn't remove EXIF data. It can be used for
+By default, this command runs in "dry run" mode and doesn't remove EXIF data. It can be used for
checking if (and how many) images should be sanitized.
The Rake task accepts following parameters.
-Parameter | Type | Description
---------- | ---- | -----------
-`start_id` | integer | Only uploads with equal or greater ID will be processed
-`stop_id` | integer | Only uploads with equal or smaller ID will be processed
-`dry_run` | boolean | Do not remove EXIF data, only check if EXIF data are present or not, default: true
-`sleep_time` | float | Pause for number of seconds after processing each image, default: 0.3 seconds
-`uploader` | string | Run sanitization only for uploads of the given uploader (`FileUploader`, `PersonalFileUploader`, `NamespaceFileUploader`)
-`since` | date | Run sanitization only for uploads newer than given date (e.g. `2019-05-01`)
+| Parameter | Type | Description |
+|:-------------|:--------|:----------------------------------------------------------------------------------------------------------------------------|
+| `start_id` | integer | Only uploads with equal or greater ID will be processed |
+| `stop_id` | integer | Only uploads with equal or smaller ID will be processed |
+| `dry_run` | boolean | Do not remove EXIF data, only check if EXIF data are present or not. Defaults to `true` |
+| `sleep_time` | float | Pause for number of seconds after processing each image. Defaults to 0.3 seconds |
+| `uploader` | string | Run sanitization only for uploads of the given uploader: `FileUploader`, `PersonalFileUploader`, or `NamespaceFileUploader` |
+| `since` | date | Run sanitization only for uploads newer than given date. For example, `2019-05-01` |
-If you have too many uploads, you can speed up sanitization by setting
-`sleep_time` to a lower value or by running multiple Rake tasks in parallel,
-each with a separate range of upload IDs (by setting `start_id` and `stop_id`).
+If you have too many uploads, you can speed up sanitization by:
-To run the command without dry mode and remove EXIF data from all uploads, you can use:
+- Setting `sleep_time` to a lower value.
+- Running multiple Rake tasks in parallel, each with a separate range of upload IDs (by setting
+ `start_id` and `stop_id`).
+
+To remove EXIF data from all uploads, use:
```shell
sudo RAILS_ENV=production -u git -H bundle exec rake gitlab:uploads:sanitize:remove_exif[,,false,] 2>&1 | tee exif.log
```
-To run the command without dry mode on uploads with ID between 100 and 5000 and pause for 0.1 second, you can use:
+To remove EXIF data on uploads with an ID between 100 and 5000 and pause for 0.1 second after each file, use:
```shell
sudo RAILS_ENV=production -u git -H bundle exec rake gitlab:uploads:sanitize:remove_exif[100,5000,false,0.1] 2>&1 | tee exif.log
```
-Because the output of commands will be probably long, the output is written also into exif.log file.
+The output is written into an `exif.log` file because it will probably be long.
+
+If sanitization fails for an upload, an error message should be in the output of the Rake task.
+Typical reasons include that the file is missing in the storage or it's not a valid image.
-If sanitization fails for an upload, an error message should be in the output of the Rake task (typical reasons may
-be that the file is missing in the storage or it's not a valid image). Please
-[report](https://gitlab.com/gitlab-org/gitlab-foss/issues/new) any issues at `gitlab.com` and use
-prefix 'EXIF' in issue title with the error output and (if possible) the image.
+[Report](https://gitlab.com/gitlab-org/gitlab/issues/new) any issues and use the prefix 'EXIF' in
+the issue title with the error output and (if possible) the image.
diff --git a/doc/administration/reference_architectures/10k_users.md b/doc/administration/reference_architectures/10k_users.md
new file mode 100644
index 00000000000..7f31f336251
--- /dev/null
+++ b/doc/administration/reference_architectures/10k_users.md
@@ -0,0 +1,79 @@
+# Reference architecture: up to 10,000 users
+
+This page describes GitLab reference architecture for up to 10,000 users.
+For a full list of reference architectures, see
+[Available reference architectures](index.md#available-reference-architectures).
+
+> - **Supported users (approximate):** 10,000
+> - **High Availability:** True
+> - **Test RPS rates:** API: 200 RPS, Web: 20 RPS, Git: 20 RPS
+
+| Service | Nodes | Configuration ([8](#footnotes)) | GCP | AWS ([9](#footnotes)) | Azure([9](#footnotes)) |
+|--------------------------------------------------------------|-------|---------------------------------|----------------|-----------------------|------------------------|
+| GitLab Rails ([1](#footnotes)) | 3 | 32 vCPU, 28.8GB Memory | n1-highcpu-32 | c5.9xlarge | F32s v2 |
+| PostgreSQL | 3 | 4 vCPU, 15GB Memory | n1-standard-4 | m5.xlarge | D4s v3 |
+| PgBouncer | 3 | 2 vCPU, 1.8GB Memory | n1-highcpu-2 | c5.large | F2s v2 |
+| Gitaly ([2](#footnotes)) ([5](#footnotes)) ([7](#footnotes)) | X | 16 vCPU, 60GB Memory | n1-standard-16 | m5.4xlarge | D16s v3 |
+| Redis ([3](#footnotes)) - Cache | 3 | 4 vCPU, 15GB Memory | n1-standard-4 | m5.xlarge | D4s v3 |
+| Redis ([3](#footnotes)) - Queues / Shared State | 3 | 4 vCPU, 15GB Memory | n1-standard-4 | m5.xlarge | D4s v3 |
+| Redis Sentinel ([3](#footnotes)) - Cache | 3 | 1 vCPU, 1.7GB Memory | g1-small | t2.small | B1MS |
+| Redis Sentinel ([3](#footnotes)) - Queues / Shared State | 3 | 1 vCPU, 1.7GB Memory | g1-small | t2.small | B1MS |
+| Consul | 3 | 2 vCPU, 1.8GB Memory | n1-highcpu-2 | c5.large | F2s v2 |
+| Sidekiq | 4 | 4 vCPU, 15GB Memory | n1-standard-4 | m5.xlarge | D4s v3 |
+| Object Storage ([4](#footnotes)) | - | - | - | - | - |
+| NFS Server ([5](#footnotes)) ([7](#footnotes)) | 1 | 4 vCPU, 3.6GB Memory | n1-highcpu-4 | c5.xlarge | F4s v2 |
+| Monitoring node | 1 | 4 vCPU, 3.6GB Memory | n1-highcpu-4 | c5.xlarge | F4s v2 |
+| External load balancing node ([6](#footnotes)) | 1 | 2 vCPU, 1.8GB Memory | n1-highcpu-2 | c5.large | F2s v2 |
+| Internal load balancing node ([6](#footnotes)) | 1 | 2 vCPU, 1.8GB Memory | n1-highcpu-2 | c5.large | F2s v2 |
+
+## Footnotes
+
+1. In our architectures we run each GitLab Rails node using the Puma webserver
+ and have its number of workers set to 90% of available CPUs along with four threads. For
+ nodes that are running Rails with other components the worker value should be reduced
+ accordingly where we've found 50% achieves a good balance but this is dependent
+ on workload.
+
+1. Gitaly node requirements are dependent on customer data, specifically the number of
+ projects and their sizes. We recommend two nodes as an absolute minimum for HA environments
+ and at least four nodes should be used when supporting 50,000 or more users.
+ We also recommend that each Gitaly node should store no more than 5TB of data
+ and have the number of [`gitaly-ruby` workers](../gitaly/index.md#gitaly-ruby)
+ set to 20% of available CPUs. Additional nodes should be considered in conjunction
+ with a review of expected data size and spread based on the recommendations above.
+
+1. Recommended Redis setup differs depending on the size of the architecture.
+ For smaller architectures (less than 3,000 users) a single instance should suffice.
+ For medium sized installs (3,000 - 5,000) we suggest one Redis cluster for all
+ classes and that Redis Sentinel is hosted alongside Consul.
+ For larger architectures (10,000 users or more) we suggest running a separate
+ [Redis Cluster](../high_availability/redis.md#running-multiple-redis-clusters) for the Cache class
+ and another for the Queues and Shared State classes respectively. We also recommend
+ that you run the Redis Sentinel clusters separately for each Redis Cluster.
+
+1. For data objects such as LFS, Uploads, Artifacts, etc. We recommend an [Object Storage service](../object_storage.md)
+ over NFS where possible, due to better performance and availability.
+
+1. NFS can be used as an alternative for both repository data (replacing Gitaly) and
+ object storage but this isn't typically recommended for performance reasons. Note however it is required for
+ [GitLab Pages](https://gitlab.com/gitlab-org/gitlab-pages/issues/196).
+
+1. Our architectures have been tested and validated with [HAProxy](https://www.haproxy.org/)
+ as the load balancer. Although other load balancers with similar feature sets
+ could also be used, those load balancers have not been validated.
+
+1. We strongly recommend that any Gitaly or NFS nodes be set up with SSD disks over
+ HDD with a throughput of at least 8,000 IOPS for read operations and 2,000 IOPS for write
+ as these components have heavy I/O. These IOPS values are recommended only as a starter
+ as with time they may be adjusted higher or lower depending on the scale of your
+ environment's workload. If you're running the environment on a Cloud provider
+ you may need to refer to their documentation on how configure IOPS correctly.
+
+1. The architectures were built and tested with the [Intel Xeon E5 v3 (Haswell)](https://cloud.google.com/compute/docs/cpu-platforms)
+ CPU platform on GCP. On different hardware you may find that adjustments, either lower
+ or higher, are required for your CPU or Node counts accordingly. For more information, a
+ [Sysbench](https://github.com/akopytov/sysbench) benchmark of the CPU can be found
+ [here](https://gitlab.com/gitlab-org/quality/performance/-/wikis/Reference-Architectures/GCP-CPU-Benchmarks).
+
+1. AWS-equivalent and Azure-equivalent configurations are rough suggestions
+ and may change in the future. They have not yet been tested and validated.
diff --git a/doc/administration/reference_architectures/1k_users.md b/doc/administration/reference_architectures/1k_users.md
new file mode 100644
index 00000000000..b6aaffa9488
--- /dev/null
+++ b/doc/administration/reference_architectures/1k_users.md
@@ -0,0 +1,82 @@
+# Reference architecture: up to 1,000 users
+
+This page describes GitLab reference architecture for up to 1,000 users.
+For a full list of reference architectures, see
+[Available reference architectures](index.md#available-reference-architectures).
+
+> - **Supported users (approximate):** 1,000
+> - **High Availability:** False
+
+| Users | Configuration([8](#footnotes)) | GCP | AWS([9](#footnotes)) | Azure([9](#footnotes)) |
+|-------|--------------------------------|---------------|----------------------|------------------------|
+| 100 | 2 vCPU, 7.2GB Memory | n1-standard-2 | m5.large | D2s v3 |
+| 500 | 4 vCPU, 15GB Memory | n1-standard-4 | m5.xlarge | D4s v3 |
+| 1000 | 8 vCPU, 30GB Memory | n1-standard-8 | m5.2xlarge | D8s v3 |
+
+For situations where you need to serve up to 1,000 users, a single-node
+solution with [frequent backups](index.md#automated-backups-core-only) is appropriate
+for many organizations. With automatic backup of the GitLab repositories,
+configuration, and the database, if you don't have strict availability
+requirements, this is the ideal solution.
+
+## Setup instructions
+
+- For this default reference architecture, use the standard [installation instructions](../../install/README.md) to install GitLab.
+
+NOTE: **Note:**
+You can also optionally configure GitLab to use an
+[external PostgreSQL service](../external_database.md) or an
+[external object storage service](../high_availability/object_storage.md) for
+added performance and reliability at a reduced complexity cost.
+
+## Footnotes
+
+1. In our architectures we run each GitLab Rails node using the Puma webserver
+ and have its number of workers set to 90% of available CPUs along with four threads. For
+ nodes that are running Rails with other components the worker value should be reduced
+ accordingly where we've found 50% achieves a good balance but this is dependent
+ on workload.
+
+1. Gitaly node requirements are dependent on customer data, specifically the number of
+ projects and their sizes. We recommend two nodes as an absolute minimum for HA environments
+ and at least four nodes should be used when supporting 50,000 or more users.
+ We also recommend that each Gitaly node should store no more than 5TB of data
+ and have the number of [`gitaly-ruby` workers](../gitaly/index.md#gitaly-ruby)
+ set to 20% of available CPUs. Additional nodes should be considered in conjunction
+ with a review of expected data size and spread based on the recommendations above.
+
+1. Recommended Redis setup differs depending on the size of the architecture.
+ For smaller architectures (less than 3,000 users) a single instance should suffice.
+ For medium sized installs (3,000 - 5,000) we suggest one Redis cluster for all
+ classes and that Redis Sentinel is hosted alongside Consul.
+ For larger architectures (10,000 users or more) we suggest running a separate
+ [Redis Cluster](../high_availability/redis.md#running-multiple-redis-clusters) for the Cache class
+ and another for the Queues and Shared State classes respectively. We also recommend
+ that you run the Redis Sentinel clusters separately for each Redis Cluster.
+
+1. For data objects such as LFS, Uploads, Artifacts, etc. We recommend an [Object Storage service](../object_storage.md)
+ over NFS where possible, due to better performance and availability.
+
+1. NFS can be used as an alternative for both repository data (replacing Gitaly) and
+ object storage but this isn't typically recommended for performance reasons. Note however it is required for
+ [GitLab Pages](https://gitlab.com/gitlab-org/gitlab-pages/issues/196).
+
+1. Our architectures have been tested and validated with [HAProxy](https://www.haproxy.org/)
+ as the load balancer. Although other load balancers with similar feature sets
+ could also be used, those load balancers have not been validated.
+
+1. We strongly recommend that any Gitaly or NFS nodes be set up with SSD disks over
+ HDD with a throughput of at least 8,000 IOPS for read operations and 2,000 IOPS for write
+ as these components have heavy I/O. These IOPS values are recommended only as a starter
+ as with time they may be adjusted higher or lower depending on the scale of your
+ environment's workload. If you're running the environment on a Cloud provider
+ you may need to refer to their documentation on how configure IOPS correctly.
+
+1. The architectures were built and tested with the [Intel Xeon E5 v3 (Haswell)](https://cloud.google.com/compute/docs/cpu-platforms)
+ CPU platform on GCP. On different hardware you may find that adjustments, either lower
+ or higher, are required for your CPU or Node counts accordingly. For more information, a
+ [Sysbench](https://github.com/akopytov/sysbench) benchmark of the CPU can be found
+ [here](https://gitlab.com/gitlab-org/quality/performance/-/wikis/Reference-Architectures/GCP-CPU-Benchmarks).
+
+1. AWS-equivalent and Azure-equivalent configurations are rough suggestions
+ and may change in the future. They have not yet been tested and validated.
diff --git a/doc/administration/reference_architectures/25k_users.md b/doc/administration/reference_architectures/25k_users.md
new file mode 100644
index 00000000000..2ee692d635c
--- /dev/null
+++ b/doc/administration/reference_architectures/25k_users.md
@@ -0,0 +1,79 @@
+# Reference architecture: up to 25,000 users
+
+This page describes GitLab reference architecture for up to 25,000 users.
+For a full list of reference architectures, see
+[Available reference architectures](index.md#available-reference-architectures).
+
+> - **Supported users (approximate):** 25,000
+> - **High Availability:** True
+> - **Test RPS rates:** API: 500 RPS, Web: 50 RPS, Git: 50 RPS
+
+| Service | Nodes | Configuration ([8](#footnotes)) | GCP | AWS ([9](#footnotes)) | Azure([9](#footnotes)) |
+|--------------------------------------------------------------|-------|---------------------------------|----------------|-----------------------|------------------------|
+| GitLab Rails ([1](#footnotes)) | 5 | 32 vCPU, 28.8GB Memory | n1-highcpu-32 | c5.9xlarge | F32s v2 |
+| PostgreSQL | 3 | 8 vCPU, 30GB Memory | n1-standard-8 | m5.2xlarge | D8s v3 |
+| PgBouncer | 3 | 2 vCPU, 1.8GB Memory | n1-highcpu-2 | c5.large | F2s v2 |
+| Gitaly ([2](#footnotes)) ([5](#footnotes)) ([7](#footnotes)) | X | 32 vCPU, 120GB Memory | n1-standard-32 | m5.8xlarge | D32s v3 |
+| Redis ([3](#footnotes)) - Cache | 3 | 4 vCPU, 15GB Memory | n1-standard-4 | m5.xlarge | D4s v3 |
+| Redis ([3](#footnotes)) - Queues / Shared State | 3 | 4 vCPU, 15GB Memory | n1-standard-4 | m5.xlarge | D4s v3 |
+| Redis Sentinel ([3](#footnotes)) - Cache | 3 | 1 vCPU, 1.7GB Memory | g1-small | t2.small | B1MS |
+| Redis Sentinel ([3](#footnotes)) - Queues / Shared State | 3 | 1 vCPU, 1.7GB Memory | g1-small | t2.small | B1MS |
+| Consul | 3 | 2 vCPU, 1.8GB Memory | n1-highcpu-2 | c5.large | F2s v2 |
+| Sidekiq | 4 | 4 vCPU, 15GB Memory | n1-standard-4 | m5.xlarge | D4s v3 |
+| Object Storage ([4](#footnotes)) | - | - | - | - | - |
+| NFS Server ([5](#footnotes)) ([7](#footnotes)) | 1 | 4 vCPU, 3.6GB Memory | n1-highcpu-4 | c5.xlarge | F4s v2 |
+| Monitoring node | 1 | 4 vCPU, 3.6GB Memory | n1-highcpu-4 | c5.xlarge | F4s v2 |
+| External load balancing node ([6](#footnotes)) | 1 | 2 vCPU, 1.8GB Memory | n1-highcpu-2 | c5.large | F2s v2 |
+| Internal load balancing node ([6](#footnotes)) | 1 | 4 vCPU, 3.6GB Memory | n1-highcpu-4 | c5.xlarge | F4s v2 |
+
+## Footnotes
+
+1. In our architectures we run each GitLab Rails node using the Puma webserver
+ and have its number of workers set to 90% of available CPUs along with four threads. For
+ nodes that are running Rails with other components the worker value should be reduced
+ accordingly where we've found 50% achieves a good balance but this is dependent
+ on workload.
+
+1. Gitaly node requirements are dependent on customer data, specifically the number of
+ projects and their sizes. We recommend two nodes as an absolute minimum for HA environments
+ and at least four nodes should be used when supporting 50,000 or more users.
+ We also recommend that each Gitaly node should store no more than 5TB of data
+ and have the number of [`gitaly-ruby` workers](../gitaly/index.md#gitaly-ruby)
+ set to 20% of available CPUs. Additional nodes should be considered in conjunction
+ with a review of expected data size and spread based on the recommendations above.
+
+1. Recommended Redis setup differs depending on the size of the architecture.
+ For smaller architectures (less than 3,000 users) a single instance should suffice.
+ For medium sized installs (3,000 - 5,000) we suggest one Redis cluster for all
+ classes and that Redis Sentinel is hosted alongside Consul.
+ For larger architectures (10,000 users or more) we suggest running a separate
+ [Redis Cluster](../high_availability/redis.md#running-multiple-redis-clusters) for the Cache class
+ and another for the Queues and Shared State classes respectively. We also recommend
+ that you run the Redis Sentinel clusters separately for each Redis Cluster.
+
+1. For data objects such as LFS, Uploads, Artifacts, etc. We recommend an [Object Storage service](../object_storage.md)
+ over NFS where possible, due to better performance and availability.
+
+1. NFS can be used as an alternative for both repository data (replacing Gitaly) and
+ object storage but this isn't typically recommended for performance reasons. Note however it is required for
+ [GitLab Pages](https://gitlab.com/gitlab-org/gitlab-pages/issues/196).
+
+1. Our architectures have been tested and validated with [HAProxy](https://www.haproxy.org/)
+ as the load balancer. Although other load balancers with similar feature sets
+ could also be used, those load balancers have not been validated.
+
+1. We strongly recommend that any Gitaly or NFS nodes be set up with SSD disks over
+ HDD with a throughput of at least 8,000 IOPS for read operations and 2,000 IOPS for write
+ as these components have heavy I/O. These IOPS values are recommended only as a starter
+ as with time they may be adjusted higher or lower depending on the scale of your
+ environment's workload. If you're running the environment on a Cloud provider
+ you may need to refer to their documentation on how configure IOPS correctly.
+
+1. The architectures were built and tested with the [Intel Xeon E5 v3 (Haswell)](https://cloud.google.com/compute/docs/cpu-platforms)
+ CPU platform on GCP. On different hardware you may find that adjustments, either lower
+ or higher, are required for your CPU or Node counts accordingly. For more information, a
+ [Sysbench](https://github.com/akopytov/sysbench) benchmark of the CPU can be found
+ [here](https://gitlab.com/gitlab-org/quality/performance/-/wikis/Reference-Architectures/GCP-CPU-Benchmarks).
+
+1. AWS-equivalent and Azure-equivalent configurations are rough suggestions
+ and may change in the future. They have not yet been tested and validated.
diff --git a/doc/administration/reference_architectures/2k_users.md b/doc/administration/reference_architectures/2k_users.md
new file mode 100644
index 00000000000..874e00e6722
--- /dev/null
+++ b/doc/administration/reference_architectures/2k_users.md
@@ -0,0 +1,90 @@
+# Reference architecture: up to 2,000 users
+
+This page describes GitLab reference architecture for up to 2,000 users.
+For a full list of reference architectures, see
+[Available reference architectures](index.md#available-reference-architectures).
+
+> - **Supported users (approximate):** 2,000
+> - **High Availability:** False
+> - **Test RPS rates:** API: 40 RPS, Web: 4 RPS, Git: 4 RPS
+
+| Service | Nodes | Configuration ([8](#footnotes)) | GCP | AWS ([9](#footnotes)) | Azure([9](#footnotes)) |
+|--------------------------------------------------------------|-------|---------------------------------|---------------|-----------------------|----------------|
+| External load balancing node ([6](#footnotes)) | 1 | 2 vCPU, 1.8GB Memory | n1-highcpu-2 | c5.large | F2s v2 |
+| Object Storage ([4](#footnotes)) | - | - | - | - | - |
+| NFS Server ([5](#footnotes)) ([7](#footnotes)) | 1 | 4 vCPU, 3.6GB Memory | n1-highcpu-4 | c5.xlarge | F4s v2 |
+| PostgreSQL | 1 | 2 vCPU, 7.5GB Memory | n1-standard-2 | m5.large | D2s v3 |
+| Redis ([3](#footnotes)) | 1 | 1 vCPU, 3.75GB Memory | n1-standard-1 | m5.large | D2s v3 |
+| Gitaly ([5](#footnotes)) ([7](#footnotes)) | X ([2](#footnotes)) | 4 vCPU, 15GB Memory | n1-standard-4 | m5.xlarge | D4s v3 |
+| GitLab Rails ([1](#footnotes)) | 2 | 8 vCPU, 7.2GB Memory | n1-highcpu-8 | c5.2xlarge | F8s v2 |
+| Monitoring node | 1 | 2 vCPU, 1.8GB Memory | n1-highcpu-2 | c5.large | F2s v2 |
+
+## Setup instructions
+
+1. [Configure the external load balancing node](../high_availability/load_balancer.md)
+ that will handle the load balancing of the two GitLab application services nodes.
+1. [Configure the Object Storage](../object_storage.md) ([4](#footnotes)) used for shared data objects.
+1. (Optional) [Configure NFS](../high_availability/nfs.md) to have
+ shared disk storage service as an alternative to Gitaly and/or
+ [Object Storage](../object_storage.md) (although not recommended).
+ NFS is required for GitLab Pages, you can skip this step if you're not using that feature.
+1. [Configure PostgreSQL](../high_availability/load_balancer.md), the database for GitLab.
+1. [Configure Redis](../high_availability/redis.md).
+1. [Configure Gitaly](../gitaly/index.md#running-gitaly-on-its-own-server),
+ which is used to provide access to the Git repositories.
+1. [Configure the main GitLab Rails application](../high_availability/gitlab.md)
+ to run Puma/Unicorn, Workhorse, GitLab Shell, and to serve all
+ frontend requests (UI, API, Git over HTTP/SSH).
+1. [Configure Prometheus](../high_availability/monitoring_node.md) to monitor your GitLab environment.
+
+## Footnotes
+
+1. In our architectures we run each GitLab Rails node using the Puma webserver
+ and have its number of workers set to 90% of available CPUs along with four threads. For
+ nodes that are running Rails with other components the worker value should be reduced
+ accordingly where we've found 50% achieves a good balance but this is dependent
+ on workload.
+
+1. Gitaly node requirements are dependent on customer data, specifically the number of
+ projects and their sizes. We recommend two nodes as an absolute minimum for HA environments
+ and at least four nodes should be used when supporting 50,000 or more users.
+ We also recommend that each Gitaly node should store no more than 5TB of data
+ and have the number of [`gitaly-ruby` workers](../gitaly/index.md#gitaly-ruby)
+ set to 20% of available CPUs. Additional nodes should be considered in conjunction
+ with a review of expected data size and spread based on the recommendations above.
+
+1. Recommended Redis setup differs depending on the size of the architecture.
+ For smaller architectures (less than 3,000 users) a single instance should suffice.
+ For medium sized installs (3,000 - 5,000) we suggest one Redis cluster for all
+ classes and that Redis Sentinel is hosted alongside Consul.
+ For larger architectures (10,000 users or more) we suggest running a separate
+ [Redis Cluster](../high_availability/redis.md#running-multiple-redis-clusters) for the Cache class
+ and another for the Queues and Shared State classes respectively. We also recommend
+ that you run the Redis Sentinel clusters separately for each Redis Cluster.
+
+1. For data objects such as LFS, Uploads, Artifacts, etc. We recommend an [Object Storage service](../object_storage.md)
+ over NFS where possible, due to better performance and availability.
+
+1. NFS can be used as an alternative for both repository data (replacing Gitaly) and
+ object storage but this isn't typically recommended for performance reasons. Note however it is required for
+ [GitLab Pages](https://gitlab.com/gitlab-org/gitlab-pages/issues/196).
+
+1. Our architectures have been tested and validated with [HAProxy](https://www.haproxy.org/)
+ as the load balancer. Although other load balancers with similar feature sets
+ could also be used, those load balancers have not been validated.
+
+1. We strongly recommend that any Gitaly or NFS nodes be set up with SSD disks over
+ HDD with a throughput of at least 8,000 IOPS for read operations and 2,000 IOPS for write
+ as these components have heavy I/O. These IOPS values are recommended only as a starter
+ as with time they may be adjusted higher or lower depending on the scale of your
+ environment's workload. If you're running the environment on a Cloud provider
+ you may need to refer to their documentation on how configure IOPS correctly.
+
+1. The architectures were built and tested with the [Intel Xeon E5 v3 (Haswell)](https://cloud.google.com/compute/docs/cpu-platforms)
+ CPU platform on GCP. On different hardware you may find that adjustments, either lower
+ or higher, are required for your CPU or Node counts accordingly. For more information, a
+ [Sysbench](https://github.com/akopytov/sysbench) benchmark of the CPU can be found
+ [here](https://gitlab.com/gitlab-org/quality/performance/-/wikis/Reference-Architectures/GCP-CPU-Benchmarks).
+
+1. AWS-equivalent and Azure-equivalent configurations are rough suggestions
+ and may change in the future. They have not yet been tested and validated.
diff --git a/doc/administration/reference_architectures/3k_users.md b/doc/administration/reference_architectures/3k_users.md
new file mode 100644
index 00000000000..bd429fbc4b4
--- /dev/null
+++ b/doc/administration/reference_architectures/3k_users.md
@@ -0,0 +1,82 @@
+# Reference architecture: up to 3,000 users
+
+This page describes GitLab reference architecture for up to 3,000 users.
+For a full list of reference architectures, see
+[Available reference architectures](index.md#available-reference-architectures).
+
+NOTE: **Note:** The 3,000-user reference architecture documented below is
+designed to help your organization achieve a highly-available GitLab deployment.
+If you do not have the expertise or need to maintain a highly-available
+environment, you can have a simpler and less costly-to-operate environment by
+following the [2,000-user reference architecture](2k_users.md).
+
+> - **Supported users (approximate):** 3,000
+> - **High Availability:** True
+> - **Test RPS rates:** API: 60 RPS, Web: 6 RPS, Git: 6 RPS
+
+| Service | Nodes | Configuration ([8](#footnotes)) | GCP | AWS ([9](#footnotes)) | Azure([9](#footnotes)) |
+|--------------------------------------------------------------|-------|---------------------------------|---------------|-----------------------|------------------------|
+| GitLab Rails ([1](#footnotes)) | 3 | 8 vCPU, 7.2GB Memory | n1-highcpu-8 | c5.2xlarge | F8s v2 |
+| PostgreSQL | 3 | 2 vCPU, 7.5GB Memory | n1-standard-2 | m5.large | D2s v3 |
+| PgBouncer | 3 | 2 vCPU, 1.8GB Memory | n1-highcpu-2 | c5.large | F2s v2 |
+| Gitaly ([2](#footnotes)) ([5](#footnotes)) ([7](#footnotes)) | X | 4 vCPU, 15GB Memory | n1-standard-4 | m5.xlarge | D4s v3 |
+| Redis ([3](#footnotes)) | 3 | 2 vCPU, 7.5GB Memory | n1-standard-2 | m5.large | D2s v3 |
+| Consul + Sentinel ([3](#footnotes)) | 3 | 2 vCPU, 1.8GB Memory | n1-highcpu-2 | c5.large | F2s v2 |
+| Sidekiq | 4 | 2 vCPU, 7.5GB Memory | n1-standard-2 | m5.large | D2s v3 |
+| Object Storage ([4](#footnotes)) | - | - | - | - | - |
+| NFS Server ([5](#footnotes)) ([7](#footnotes)) | 1 | 4 vCPU, 3.6GB Memory | n1-highcpu-4 | c5.xlarge | F4s v2 |
+| Monitoring node | 1 | 2 vCPU, 1.8GB Memory | n1-highcpu-2 | c5.large | F2s v2 |
+| External load balancing node ([6](#footnotes)) | 1 | 2 vCPU, 1.8GB Memory | n1-highcpu-2 | c5.large | F2s v2 |
+| Internal load balancing node ([6](#footnotes)) | 1 | 2 vCPU, 1.8GB Memory | n1-highcpu-2 | c5.large | F2s v2 |
+
+## Footnotes
+
+1. In our architectures we run each GitLab Rails node using the Puma webserver
+ and have its number of workers set to 90% of available CPUs along with four threads. For
+ nodes that are running Rails with other components the worker value should be reduced
+ accordingly where we've found 50% achieves a good balance but this is dependent
+ on workload.
+
+1. Gitaly node requirements are dependent on customer data, specifically the number of
+ projects and their sizes. We recommend two nodes as an absolute minimum for HA environments
+ and at least four nodes should be used when supporting 50,000 or more users.
+ We also recommend that each Gitaly node should store no more than 5TB of data
+ and have the number of [`gitaly-ruby` workers](../gitaly/index.md#gitaly-ruby)
+ set to 20% of available CPUs. Additional nodes should be considered in conjunction
+ with a review of expected data size and spread based on the recommendations above.
+
+1. Recommended Redis setup differs depending on the size of the architecture.
+ For smaller architectures (less than 3,000 users) a single instance should suffice.
+ For medium sized installs (3,000 - 5,000) we suggest one Redis cluster for all
+ classes and that Redis Sentinel is hosted alongside Consul.
+ For larger architectures (10,000 users or more) we suggest running a separate
+ [Redis Cluster](../high_availability/redis.md#running-multiple-redis-clusters) for the Cache class
+ and another for the Queues and Shared State classes respectively. We also recommend
+ that you run the Redis Sentinel clusters separately for each Redis Cluster.
+
+1. For data objects such as LFS, Uploads, Artifacts, etc. We recommend an [Object Storage service](../object_storage.md)
+ over NFS where possible, due to better performance and availability.
+
+1. NFS can be used as an alternative for both repository data (replacing Gitaly) and
+ object storage but this isn't typically recommended for performance reasons. Note however it is required for
+ [GitLab Pages](https://gitlab.com/gitlab-org/gitlab-pages/issues/196).
+
+1. Our architectures have been tested and validated with [HAProxy](https://www.haproxy.org/)
+ as the load balancer. Although other load balancers with similar feature sets
+ could also be used, those load balancers have not been validated.
+
+1. We strongly recommend that any Gitaly or NFS nodes be set up with SSD disks over
+ HDD with a throughput of at least 8,000 IOPS for read operations and 2,000 IOPS for write
+ as these components have heavy I/O. These IOPS values are recommended only as a starter
+ as with time they may be adjusted higher or lower depending on the scale of your
+ environment's workload. If you're running the environment on a Cloud provider
+ you may need to refer to their documentation on how configure IOPS correctly.
+
+1. The architectures were built and tested with the [Intel Xeon E5 v3 (Haswell)](https://cloud.google.com/compute/docs/cpu-platforms)
+ CPU platform on GCP. On different hardware you may find that adjustments, either lower
+ or higher, are required for your CPU or Node counts accordingly. For more information, a
+ [Sysbench](https://github.com/akopytov/sysbench) benchmark of the CPU can be found
+ [here](https://gitlab.com/gitlab-org/quality/performance/-/wikis/Reference-Architectures/GCP-CPU-Benchmarks).
+
+1. AWS-equivalent and Azure-equivalent configurations are rough suggestions
+ and may change in the future. They have not yet been tested and validated.
diff --git a/doc/administration/reference_architectures/50k_users.md b/doc/administration/reference_architectures/50k_users.md
new file mode 100644
index 00000000000..67f773a021f
--- /dev/null
+++ b/doc/administration/reference_architectures/50k_users.md
@@ -0,0 +1,79 @@
+# Reference architecture: up to 50,000 users
+
+This page describes GitLab reference architecture for up to 50,000 users.
+For a full list of reference architectures, see
+[Available reference architectures](index.md#available-reference-architectures).
+
+> - **Supported users (approximate):** 50,000
+> - **High Availability:** True
+> - **Test RPS rates:** API: 1000 RPS, Web: 100 RPS, Git: 100 RPS
+
+| Service | Nodes | Configuration ([8](#footnotes)) | GCP | AWS ([9](#footnotes)) | Azure([9](#footnotes)) |
+|--------------------------------------------------------------|-------|---------------------------------|----------------|-----------------------|------------------------|
+| GitLab Rails ([1](#footnotes)) | 12 | 32 vCPU, 28.8GB Memory | n1-highcpu-32 | c5.9xlarge | F32s v2 |
+| PostgreSQL | 3 | 16 vCPU, 60GB Memory | n1-standard-16 | m5.4xlarge | D16s v3 |
+| PgBouncer | 3 | 2 vCPU, 1.8GB Memory | n1-highcpu-2 | c5.large | F2s v2 |
+| Gitaly ([2](#footnotes)) ([5](#footnotes)) ([7](#footnotes)) | X | 64 vCPU, 240GB Memory | n1-standard-64 | m5.16xlarge | D64s v3 |
+| Redis ([3](#footnotes)) - Cache | 3 | 4 vCPU, 15GB Memory | n1-standard-4 | m5.xlarge | D4s v3 |
+| Redis ([3](#footnotes)) - Queues / Shared State | 3 | 4 vCPU, 15GB Memory | n1-standard-4 | m5.xlarge | D4s v3 |
+| Redis Sentinel ([3](#footnotes)) - Cache | 3 | 1 vCPU, 1.7GB Memory | g1-small | t2.small | B1MS |
+| Redis Sentinel ([3](#footnotes)) - Queues / Shared State | 3 | 1 vCPU, 1.7GB Memory | g1-small | t2.small | B1MS |
+| Consul | 3 | 2 vCPU, 1.8GB Memory | n1-highcpu-2 | c5.large | F2s v2 |
+| Sidekiq | 4 | 4 vCPU, 15GB Memory | n1-standard-4 | m5.xlarge | D4s v3 |
+| NFS Server ([5](#footnotes)) ([7](#footnotes)) | 1 | 4 vCPU, 3.6GB Memory | n1-highcpu-4 | c5.xlarge | F4s v2 |
+| Object Storage ([4](#footnotes)) | - | - | - | - | - |
+| Monitoring node | 1 | 4 vCPU, 3.6GB Memory | n1-highcpu-4 | c5.xlarge | F4s v2 |
+| External load balancing node ([6](#footnotes)) | 1 | 2 vCPU, 1.8GB Memory | n1-highcpu-2 | c5.large | F2s v2 |
+| Internal load balancing node ([6](#footnotes)) | 1 | 8 vCPU, 7.2GB Memory | n1-highcpu-8 | c5.2xlarge | F8s v2 |
+
+## Footnotes
+
+1. In our architectures we run each GitLab Rails node using the Puma webserver
+ and have its number of workers set to 90% of available CPUs along with four threads. For
+ nodes that are running Rails with other components the worker value should be reduced
+ accordingly where we've found 50% achieves a good balance but this is dependent
+ on workload.
+
+1. Gitaly node requirements are dependent on customer data, specifically the number of
+ projects and their sizes. We recommend two nodes as an absolute minimum for HA environments
+ and at least four nodes should be used when supporting 50,000 or more users.
+ We also recommend that each Gitaly node should store no more than 5TB of data
+ and have the number of [`gitaly-ruby` workers](../gitaly/index.md#gitaly-ruby)
+ set to 20% of available CPUs. Additional nodes should be considered in conjunction
+ with a review of expected data size and spread based on the recommendations above.
+
+1. Recommended Redis setup differs depending on the size of the architecture.
+ For smaller architectures (less than 3,000 users) a single instance should suffice.
+ For medium sized installs (3,000 - 5,000) we suggest one Redis cluster for all
+ classes and that Redis Sentinel is hosted alongside Consul.
+ For larger architectures (10,000 users or more) we suggest running a separate
+ [Redis Cluster](../high_availability/redis.md#running-multiple-redis-clusters) for the Cache class
+ and another for the Queues and Shared State classes respectively. We also recommend
+ that you run the Redis Sentinel clusters separately for each Redis Cluster.
+
+1. For data objects such as LFS, Uploads, Artifacts, etc. We recommend an [Object Storage service](../object_storage.md)
+ over NFS where possible, due to better performance and availability.
+
+1. NFS can be used as an alternative for both repository data (replacing Gitaly) and
+ object storage but this isn't typically recommended for performance reasons. Note however it is required for
+ [GitLab Pages](https://gitlab.com/gitlab-org/gitlab-pages/issues/196).
+
+1. Our architectures have been tested and validated with [HAProxy](https://www.haproxy.org/)
+ as the load balancer. Although other load balancers with similar feature sets
+ could also be used, those load balancers have not been validated.
+
+1. We strongly recommend that any Gitaly or NFS nodes be set up with SSD disks over
+ HDD with a throughput of at least 8,000 IOPS for read operations and 2,000 IOPS for write
+ as these components have heavy I/O. These IOPS values are recommended only as a starter
+ as with time they may be adjusted higher or lower depending on the scale of your
+ environment's workload. If you're running the environment on a Cloud provider
+ you may need to refer to their documentation on how configure IOPS correctly.
+
+1. The architectures were built and tested with the [Intel Xeon E5 v3 (Haswell)](https://cloud.google.com/compute/docs/cpu-platforms)
+ CPU platform on GCP. On different hardware you may find that adjustments, either lower
+ or higher, are required for your CPU or Node counts accordingly. For more information, a
+ [Sysbench](https://github.com/akopytov/sysbench) benchmark of the CPU can be found
+ [here](https://gitlab.com/gitlab-org/quality/performance/-/wikis/Reference-Architectures/GCP-CPU-Benchmarks).
+
+1. AWS-equivalent and Azure-equivalent configurations are rough suggestions
+ and may change in the future. They have not yet been tested and validated.
diff --git a/doc/administration/reference_architectures/5k_users.md b/doc/administration/reference_architectures/5k_users.md
new file mode 100644
index 00000000000..41ef6f369c2
--- /dev/null
+++ b/doc/administration/reference_architectures/5k_users.md
@@ -0,0 +1,76 @@
+# Reference architecture: up to 5,000 users
+
+This page describes GitLab reference architecture for up to 5,000 users.
+For a full list of reference architectures, see
+[Available reference architectures](index.md#available-reference-architectures).
+
+> - **Supported users (approximate):** 5,000
+> - **High Availability:** True
+> - **Test RPS rates:** API: 100 RPS, Web: 10 RPS, Git: 10 RPS
+
+| Service | Nodes | Configuration ([8](#footnotes)) | GCP | AWS ([9](#footnotes)) | Azure([9](#footnotes)) |
+|--------------------------------------------------------------|-------|---------------------------------|---------------|-----------------------|------------------------|
+| GitLab Rails ([1](#footnotes)) | 3 | 16 vCPU, 14.4GB Memory | n1-highcpu-16 | c5.4xlarge | F16s v2 |
+| PostgreSQL | 3 | 2 vCPU, 7.5GB Memory | n1-standard-2 | m5.large | D2s v3 |
+| PgBouncer | 3 | 2 vCPU, 1.8GB Memory | n1-highcpu-2 | c5.large | F2s v2 |
+| Gitaly ([2](#footnotes)) ([5](#footnotes)) ([7](#footnotes)) | X | 8 vCPU, 30GB Memory | n1-standard-8 | m5.2xlarge | D8s v3 |
+| Redis ([3](#footnotes)) | 3 | 2 vCPU, 7.5GB Memory | n1-standard-2 | m5.large | D2s v3 |
+| Consul + Sentinel ([3](#footnotes)) | 3 | 2 vCPU, 1.8GB Memory | n1-highcpu-2 | c5.large | F2s v2 |
+| Sidekiq | 4 | 2 vCPU, 7.5GB Memory | n1-standard-2 | m5.large | D2s v3 |
+| Object Storage ([4](#footnotes)) | - | - | - | - | - |
+| NFS Server ([5](#footnotes)) ([7](#footnotes)) | 1 | 4 vCPU, 3.6GB Memory | n1-highcpu-4 | c5.xlarge | F4s v2 |
+| Monitoring node | 1 | 2 vCPU, 1.8GB Memory | n1-highcpu-2 | c5.large | F2s v2 |
+| External load balancing node ([6](#footnotes)) | 1 | 2 vCPU, 1.8GB Memory | n1-highcpu-2 | c5.large | F2s v2 |
+| Internal load balancing node ([6](#footnotes)) | 1 | 2 vCPU, 1.8GB Memory | n1-highcpu-2 | c5.large | F2s v2 |
+
+## Footnotes
+
+1. In our architectures we run each GitLab Rails node using the Puma webserver
+ and have its number of workers set to 90% of available CPUs along with four threads. For
+ nodes that are running Rails with other components the worker value should be reduced
+ accordingly where we've found 50% achieves a good balance but this is dependent
+ on workload.
+
+1. Gitaly node requirements are dependent on customer data, specifically the number of
+ projects and their sizes. We recommend two nodes as an absolute minimum for HA environments
+ and at least four nodes should be used when supporting 50,000 or more users.
+ We also recommend that each Gitaly node should store no more than 5TB of data
+ and have the number of [`gitaly-ruby` workers](../gitaly/index.md#gitaly-ruby)
+ set to 20% of available CPUs. Additional nodes should be considered in conjunction
+ with a review of expected data size and spread based on the recommendations above.
+
+1. Recommended Redis setup differs depending on the size of the architecture.
+ For smaller architectures (less than 3,000 users) a single instance should suffice.
+ For medium sized installs (3,000 - 5,000) we suggest one Redis cluster for all
+ classes and that Redis Sentinel is hosted alongside Consul.
+ For larger architectures (10,000 users or more) we suggest running a separate
+ [Redis Cluster](../high_availability/redis.md#running-multiple-redis-clusters) for the Cache class
+ and another for the Queues and Shared State classes respectively. We also recommend
+ that you run the Redis Sentinel clusters separately for each Redis Cluster.
+
+1. For data objects such as LFS, Uploads, Artifacts, etc. We recommend an [Object Storage service](../object_storage.md)
+ over NFS where possible, due to better performance and availability.
+
+1. NFS can be used as an alternative for both repository data (replacing Gitaly) and
+ object storage but this isn't typically recommended for performance reasons. Note however it is required for
+ [GitLab Pages](https://gitlab.com/gitlab-org/gitlab-pages/issues/196).
+
+1. Our architectures have been tested and validated with [HAProxy](https://www.haproxy.org/)
+ as the load balancer. Although other load balancers with similar feature sets
+ could also be used, those load balancers have not been validated.
+
+1. We strongly recommend that any Gitaly or NFS nodes be set up with SSD disks over
+ HDD with a throughput of at least 8,000 IOPS for read operations and 2,000 IOPS for write
+ as these components have heavy I/O. These IOPS values are recommended only as a starter
+ as with time they may be adjusted higher or lower depending on the scale of your
+ environment's workload. If you're running the environment on a Cloud provider
+ you may need to refer to their documentation on how configure IOPS correctly.
+
+1. The architectures were built and tested with the [Intel Xeon E5 v3 (Haswell)](https://cloud.google.com/compute/docs/cpu-platforms)
+ CPU platform on GCP. On different hardware you may find that adjustments, either lower
+ or higher, are required for your CPU or Node counts accordingly. For more information, a
+ [Sysbench](https://github.com/akopytov/sysbench) benchmark of the CPU can be found
+ [here](https://gitlab.com/gitlab-org/quality/performance/-/wikis/Reference-Architectures/GCP-CPU-Benchmarks).
+
+1. AWS-equivalent and Azure-equivalent configurations are rough suggestions
+ and may change in the future. They have not yet been tested and validated.
diff --git a/doc/administration/reference_architectures/img/reference-architectures.png b/doc/administration/reference_architectures/img/reference-architectures.png
new file mode 100644
index 00000000000..e15609e78e1
--- /dev/null
+++ b/doc/administration/reference_architectures/img/reference-architectures.png
Binary files differ
diff --git a/doc/administration/reference_architectures/index.md b/doc/administration/reference_architectures/index.md
new file mode 100644
index 00000000000..26244368234
--- /dev/null
+++ b/doc/administration/reference_architectures/index.md
@@ -0,0 +1,225 @@
+---
+type: reference, concepts
+---
+# Reference architectures
+
+<!-- TBD to be reviewed by Eric -->
+
+You can set up GitLab on a single server or scale it up to serve many users.
+This page details the recommended Reference Architectures that were built and verified by GitLab's Quality and Support teams.
+
+Below is a chart representing each architecture tier and the number of users they can handle. As your number of users grow with time, it’s recommended that you scale GitLab accordingly.
+
+![Reference Architectures](img/reference-architectures.png)
+<!-- Internal link: https://docs.google.com/spreadsheets/d/1obYP4fLKkVVDOljaI3-ozhmCiPtEeMblbBKkf2OADKs/edit#gid=1403207183 -->
+
+Testing on these reference architectures were performed with [GitLab's Performance Tool](https://gitlab.com/gitlab-org/quality/performance)
+at specific coded workloads, and the throughputs used for testing were calculated based on sample customer data.
+After selecting the reference architecture that matches your scale, refer to
+[Configure GitLab to Scale](#configure-gitlab-to-scale) to see the components
+involved, and how to configure them.
+
+Each endpoint type is tested with the following number of requests per second (RPS) per 1000 users:
+
+- API: 20 RPS
+- Web: 2 RPS
+- Git: 2 RPS
+
+For GitLab instances with less than 2,000 users, it's recommended that you use the [default setup](#automated-backups-core-only)
+by [installing GitLab](../../install/README.md) on a single machine to minimize maintenance and resource costs.
+
+If your organization has more than 2,000 users, the recommendation is to scale GitLab's components to multiple
+machine nodes. The machine nodes are grouped by component(s). The addition of these
+nodes increases the performance and scalability of to your GitLab instance.
+
+When scaling GitLab, there are several factors to consider:
+
+- Multiple application nodes to handle frontend traffic.
+- A load balancer is added in front to distribute traffic across the application nodes.
+- The application nodes connects to a shared file server and PostgreSQL and Redis services on the backend.
+
+NOTE: **Note:** Depending on your workflow, the following recommended
+reference architectures may need to be adapted accordingly. Your workload
+is influenced by factors including how active your users are,
+how much automation you use, mirroring, and repository/change size. Additionally the
+displayed memory values are provided by [GCP machine types](https://cloud.google.com/compute/docs/machine-types).
+For different cloud vendors, attempt to select options that best match the provided architecture.
+
+## Available reference architectures
+
+The following reference architectures are available:
+
+- [Up to 1,000 users](1k_users.md)
+- [Up to 2,000 users](2k_users.md)
+- [Up to 3,000 users](3k_users.md)
+- [Up to 5,000 users](5k_users.md)
+- [Up to 10,000 users](10k_users.md)
+- [Up to 25,000 users](25k_users.md)
+- [Up to 50,000 users](50k_users.md)
+
+## Availability Components
+
+GitLab comes with the following components for your use, listed from
+least to most complex:
+
+1. [Automated backups](#automated-backups-core-only)
+1. [Traffic load balancer](#traffic-load-balancer-starter-only)
+1. [Zero downtime updates](#zero-downtime-updates-starter-only)
+1. [Automated database failover](#automated-database-failover-premium-only)
+1. [Instance level replication with GitLab Geo](#instance-level-replication-with-gitlab-geo-premium-only)
+
+As you implement these components, begin with a single server and then do
+backups. Only after completing the first server should you proceed to the next.
+
+Also, not implementing extra servers for GitLab doesn't necessarily mean that you'll have
+more downtime. Depending on your needs and experience level, single servers can
+have more actual perceived uptime for your users.
+
+### Automated backups **(CORE ONLY)**
+
+> - Level of complexity: **Low**
+> - Required domain knowledge: PostgreSQL, GitLab configurations, Git
+> - Supported tiers: [GitLab Core, Starter, Premium, and Ultimate](https://about.gitlab.com/pricing/)
+
+This solution is appropriate for many teams that have the default GitLab installation.
+With automatic backups of the GitLab repositories, configuration, and the database,
+this can be an optimal solution if you don't have strict requirements.
+[Automated backups](../../raketasks/backup_restore.md#configuring-cron-to-make-daily-backups)
+is the least complex to setup. This provides a point-in-time recovery of a predetermined schedule.
+
+### Traffic load balancer **(STARTER ONLY)**
+
+> - Level of complexity: **Medium**
+> - Required domain knowledge: HAProxy, shared storage, distributed systems
+> - Supported tiers: [GitLab Starter, Premium, and Ultimate](https://about.gitlab.com/pricing/)
+
+This requires separating out GitLab into multiple application nodes with an added
+[load balancer](../high_availability/load_balancer.md). The load balancer will distribute traffic
+across GitLab application nodes. Meanwhile, each application node connects to a
+shared file server and database systems on the back end. This way, if one of the
+application servers fails, the workflow is not interrupted.
+[HAProxy](https://www.haproxy.org/) is recommended as the load balancer.
+
+With this added component you have a number of advantages compared
+to the default installation:
+
+- Increase the number of users.
+- Enable zero-downtime upgrades.
+- Increase availability.
+
+### Zero downtime updates **(STARTER ONLY)**
+
+> - Level of complexity: **Medium**
+> - Required domain knowledge: PostgreSQL, HAProxy, shared storage, distributed systems
+> - Supported tiers: [GitLab Starter, Premium, and Ultimate](https://about.gitlab.com/pricing/)
+
+GitLab supports [zero-downtime updates](https://docs.gitlab.com/omnibus/update/#zero-downtime-updates).
+Although you can perform zero-downtime updates with a single GitLab node, the recommendation is to separate GitLab into several application nodes.
+As long as at least one of each component is online and capable of handling the instance's usage load, your team's productivity will not be interrupted during the update.
+
+### Automated database failover **(PREMIUM ONLY)**
+
+> - Level of complexity: **High**
+> - Required domain knowledge: PgBouncer, Repmgr, shared storage, distributed systems
+> - Supported tiers: [GitLab Premium and Ultimate](https://about.gitlab.com/pricing/)
+
+By adding automatic failover for database systems, you can enable higher uptime
+with additional database nodes. This extends the default database with
+cluster management and failover policies.
+[PgBouncer](../../development/architecture.md#pgbouncer) in conjunction with
+[Repmgr](../high_availability/database.md) is recommended.
+
+### Instance level replication with GitLab Geo **(PREMIUM ONLY)**
+
+> - Level of complexity: **Very High**
+> - Required domain knowledge: Storage replication
+> - Supported tiers: [GitLab Premium and Ultimate](https://about.gitlab.com/pricing/)
+
+[GitLab Geo](../geo/replication/index.md) allows you to replicate your GitLab
+instance to other geographical locations as a read-only fully operational instance
+that can also be promoted in case of disaster.
+
+## Configure GitLab to scale
+
+The following components are the ones you need to configure in order to scale
+GitLab. They are listed in the order you'll typically configure them if they are
+required by your [reference architecture](#reference-architectures) of choice.
+
+Most of them are bundled in the GitLab deb/rpm package (called Omnibus GitLab),
+but depending on your system architecture, you may require some components which are
+not included in it. If required, those should be configured before
+setting up components provided by GitLab. Advice on how to select the right
+solution for your organization is provided in the configuration instructions
+column.
+
+| Component | Description | Configuration instructions | Bundled with Omnibus GitLab |
+|-----------|-------------|----------------------------|
+| Load balancer(s) ([6](#footnotes)) | Handles load balancing, typically when you have multiple GitLab application services nodes | [Load balancer configuration](../high_availability/load_balancer.md) ([6](#footnotes)) | No |
+| Object storage service ([4](#footnotes)) | Recommended store for shared data objects | [Object Storage configuration](../object_storage.md) | No |
+| NFS ([5](#footnotes)) ([7](#footnotes)) | Shared disk storage service. Can be used as an alternative Object Storage. Required for GitLab Pages | [NFS configuration](../high_availability/nfs.md) | No |
+| [Consul](../../development/architecture.md#consul) ([3](#footnotes)) | Service discovery and health checks/failover | [Consul configuration](../high_availability/consul.md) **(PREMIUM ONLY)** | Yes |
+| [PostgreSQL](../../development/architecture.md#postgresql) | Database | [PostgreSQL configuration](https://docs.gitlab.com/omnibus/settings/database.html) | Yes |
+| [PgBouncer](../../development/architecture.md#pgbouncer) | Database connection pooler | [PgBouncer configuration](../high_availability/pgbouncer.md#running-pgbouncer-as-part-of-a-non-ha-gitlab-installation) **(PREMIUM ONLY)** | Yes |
+| Repmgr | PostgreSQL cluster management and failover | [PostgreSQL and Repmgr configuration](../high_availability/database.md) | Yes |
+| [Redis](../../development/architecture.md#redis) ([3](#footnotes)) | Key/value store for fast data lookup and caching | [Redis configuration](../high_availability/redis.md) | Yes |
+| Redis Sentinel | Redis | [Redis Sentinel configuration](../high_availability/redis.md) | Yes |
+| [Gitaly](../../development/architecture.md#gitaly) ([2](#footnotes)) ([7](#footnotes)) ([10](#footnotes)) | Provides access to Git repositories | [Gitaly configuration](../gitaly/index.md#running-gitaly-on-its-own-server) | Yes |
+| [Sidekiq](../../development/architecture.md#sidekiq) | Asynchronous/background jobs | [Sidekiq configuration](../high_availability/sidekiq.md) | Yes |
+| [GitLab application services](../../development/architecture.md#unicorn)([1](#footnotes)) | Puma/Unicorn, Workhorse, GitLab Shell - serves front-end requests (UI, API, Git over HTTP/SSH) | [GitLab app scaling configuration](../high_availability/gitlab.md) | Yes |
+| [Prometheus](../../development/architecture.md#prometheus) and [Grafana](../../development/architecture.md#grafana) | GitLab environment monitoring | [Monitoring node for scaling](../high_availability/monitoring_node.md) | Yes |
+
+## Footnotes
+
+1. In our architectures we run each GitLab Rails node using the Puma webserver
+ and have its number of workers set to 90% of available CPUs along with four threads. For
+ nodes that are running Rails with other components the worker value should be reduced
+ accordingly where we've found 50% achieves a good balance but this is dependent
+ on workload.
+
+1. Gitaly node requirements are dependent on customer data, specifically the number of
+ projects and their sizes. We recommend two nodes as an absolute minimum,
+ and at least four nodes should be used when supporting 50,000 or more users.
+ We also recommend that each Gitaly node should store no more than 5TB of data
+ and have the number of [`gitaly-ruby` workers](../gitaly/index.md#gitaly-ruby)
+ set to 20% of available CPUs. Additional nodes should be considered in conjunction
+ with a review of expected data size and spread based on the recommendations above.
+
+1. Recommended Redis setup differs depending on the size of the architecture.
+ For smaller architectures (less than 3,000 users) a single instance should suffice.
+ For medium sized installs (3,000 - 5,000) we suggest one Redis cluster for all
+ classes and that Redis Sentinel is hosted alongside Consul.
+ For larger architectures (10,000 users or more) we suggest running a separate
+ [Redis Cluster](../high_availability/redis.md#running-multiple-redis-clusters) for the Cache class
+ and another for the Queues and Shared State classes respectively. We also recommend
+ that you run the Redis Sentinel clusters separately for each Redis Cluster.
+
+1. For data objects such as LFS, Uploads, Artifacts, etc. We recommend an [Object Storage service](../object_storage.md)
+ over NFS where possible, due to better performance.
+
+1. NFS can be used as an alternative for object storage but this isn't typically
+ recommended for performance reasons. Note however it is required for [GitLab
+ Pages](https://gitlab.com/gitlab-org/gitlab-pages/issues/196).
+
+1. Our architectures have been tested and validated with [HAProxy](https://www.haproxy.org/)
+ as the load balancer. Although other load balancers with similar feature sets
+ could also be used, those load balancers have not been validated.
+
+1. We strongly recommend that any Gitaly or NFS nodes be set up with SSD disks over
+ HDD with a throughput of at least 8,000 IOPS for read operations and 2,000 IOPS for write
+ as these components have heavy I/O. These IOPS values are recommended only as a starter
+ as with time they may be adjusted higher or lower depending on the scale of your
+ environment's workload. If you're running the environment on a Cloud provider
+ you may need to refer to their documentation on how configure IOPS correctly.
+
+1. The architectures were built and tested with the [Intel Xeon E5 v3 (Haswell)](https://cloud.google.com/compute/docs/cpu-platforms)
+ CPU platform on GCP. On different hardware you may find that adjustments, either lower
+ or higher, are required for your CPU or Node counts accordingly. For more information, a
+ [Sysbench](https://github.com/akopytov/sysbench) benchmark of the CPU can be found
+ [here](https://gitlab.com/gitlab-org/quality/performance/-/wikis/Reference-Architectures/GCP-CPU-Benchmarks).
+
+1. AWS-equivalent and Azure-equivalent configurations are rough suggestions
+ and may change in the future. They have not yet been tested and validated.
+
+1. From GitLab 13.0, using NFS for Git repositories is deprecated. In GitLab
+ 14.0, support for NFS for Git repositories is scheduled to be removed.
+ Upgrade to [Gitaly Cluster](../gitaly/praefect.md) as soon as possible.
diff --git a/doc/administration/reply_by_email_postfix_setup.md b/doc/administration/reply_by_email_postfix_setup.md
index 8a24514aec2..86854a2a7b6 100644
--- a/doc/administration/reply_by_email_postfix_setup.md
+++ b/doc/administration/reply_by_email_postfix_setup.md
@@ -1,7 +1,7 @@
# Set up Postfix for incoming email
This document will take you through the steps of setting up a basic Postfix mail
-server with IMAP authentication on Ubuntu, to be used with [incoming email].
+server with IMAP authentication on Ubuntu, to be used with [incoming email](incoming_email.md).
The instructions make the assumption that you will be using the email address `incoming@gitlab.example.com`, that is, username `incoming` on host `gitlab.example.com`. Don't forget to change it to your actual host when executing the example code snippets.
@@ -333,10 +333,8 @@ Courier, which we will install later to add IMAP authentication, requires mailbo
## Done
-If all the tests were successful, Postfix is all set up and ready to receive email! Continue with the [incoming email] guide to configure GitLab.
+If all the tests were successful, Postfix is all set up and ready to receive email! Continue with the [incoming email](incoming_email.md) guide to configure GitLab.
---
_This document was adapted from <https://help.ubuntu.com/community/PostfixBasicSetupHowto>, by contributors to the Ubuntu documentation wiki._
-
-[incoming email]: incoming_email.md
diff --git a/doc/administration/repository_checks.md b/doc/administration/repository_checks.md
index a647bc82660..6d9ab723d2f 100644
--- a/doc/administration/repository_checks.md
+++ b/doc/administration/repository_checks.md
@@ -1,14 +1,14 @@
# Repository checks
-> [Introduced][ce-3232] in GitLab 8.7.
+> [Introduced](https://gitlab.com/gitlab-org/gitlab-foss/-/merge_requests/3232) in GitLab 8.7.
-Git has a built-in mechanism, [`git fsck`][git-fsck], to verify the
+Git has a built-in mechanism, [`git fsck`](https://git-scm.com/docs/git-fsck), to verify the
integrity of all data committed to a repository. GitLab administrators
can trigger such a check for a project via the project page under the
admin panel. The checks run asynchronously so it may take a few minutes
before the check result is visible on the project admin page. If the
-checks failed you can see their output on the admin log page under
-'repocheck.log'.
+checks failed you can see their output on in the [`repocheck.log`
+file.](logs.md#repochecklog)
NOTE: **Note:**
It is OFF by default because it still causes too many false alarms.
@@ -31,17 +31,11 @@ panel.
## What to do if a check failed
If the repository check fails for some repository you should look up the error
-in `repocheck.log`:
+in the [`repocheck.log` file](logs.md#repochecklog) on disk:
-- in the [admin panel](logs.md#repochecklog)
-- or on disk, see:
- - `/var/log/gitlab/gitlab-rails` for Omnibus installations
- - `/home/git/gitlab/log` for installations from source
+- `/var/log/gitlab/gitlab-rails` for Omnibus installations
+- `/home/git/gitlab/log` for installations from source
If the periodic repository check causes false alarms, you can clear all repository check states by
navigating to **Admin Area > Settings > Repository**
(`/admin/application_settings/repository`) and clicking **Clear all repository checks**.
-
----
-[ce-3232]: https://gitlab.com/gitlab-org/gitlab-foss/-/merge_requests/3232 "Auto git fsck"
-[git-fsck]: https://git-scm.com/docs/git-fsck "git fsck documentation"
diff --git a/doc/administration/repository_storage_paths.md b/doc/administration/repository_storage_paths.md
index 857fb0b6a90..283401dafff 100644
--- a/doc/administration/repository_storage_paths.md
+++ b/doc/administration/repository_storage_paths.md
@@ -1,6 +1,6 @@
# Repository storage paths
-> [Introduced][ce-4578] in GitLab 8.10.
+> [Introduced](https://gitlab.com/gitlab-org/gitlab-foss/-/merge_requests/4578) in GitLab 8.10.
GitLab allows you to define multiple repository storage paths (sometimes called
storage shards) to distribute the storage load between several mount points.
@@ -34,7 +34,7 @@ storage2:
## Configure GitLab
> **Warning:**
-> In order for [backups] to work correctly, the storage path must **not** be a
+> In order for [backups](../raketasks/backup_restore.md) to work correctly, the storage path must **not** be a
> mount point and the GitLab user should have correct permissions for the parent
> directory of the path. In Omnibus GitLab this is taken care of automatically,
> but for source installations you should be extra careful.
@@ -47,7 +47,7 @@ storage2:
> `gitlab.yml`.
>
> This little detail matters because while restoring a backup, the current
-> contents of `/home/git/repositories` [are moved to][raketask] `/home/git/repositories.old`,
+> contents of `/home/git/repositories` [are moved to](https://gitlab.com/gitlab-org/gitlab/blob/033e5423a2594e08a7ebcd2379bd2331f4c39032/lib/backup/repository.rb#L54-56) `/home/git/repositories.old`,
> so if `/home/git/repositories` is the mount point, then `mv` would be moving
> things between mount points, and bad things could happen. Ideally,
> `/home/git` would be the mount point, so then things would be moving within the
@@ -79,10 +79,10 @@ NOTE: **Note:** This example uses NFS. We do not recommend using EFS for storage
path: /mnt/nfs2/repositories
```
-1. [Restart GitLab][restart-gitlab] for the changes to take effect.
+1. [Restart GitLab](restart_gitlab.md#installations-from-source) for the changes to take effect.
>**Note:**
-The [`gitlab_shell: repos_path` entry][repospath] in `gitlab.yml` will be
+The [`gitlab_shell: repos_path` entry](https://gitlab.com/gitlab-org/gitlab-foss/-/blob/8-9-stable/config/gitlab.yml.example#L457) in `gitlab.yml` will be
deprecated and replaced by `repositories: storages` in the future, so if you
are upgrading from a version prior to 8.10, make sure to add the configuration
as described in the step above. After you make the changes and confirm they are
@@ -114,9 +114,3 @@ Repository storage > Storage nodes for new repositories**.
Beginning with GitLab 8.13.4, multiple paths can be chosen. New repositories
will be randomly placed on one of the selected paths.
-
-[ce-4578]: https://gitlab.com/gitlab-org/gitlab-foss/-/merge_requests/4578
-[restart-gitlab]: restart_gitlab.md#installations-from-source
-[backups]: ../raketasks/backup_restore.md
-[raketask]: https://gitlab.com/gitlab-org/gitlab/blob/033e5423a2594e08a7ebcd2379bd2331f4c39032/lib/backup/repository.rb#L54-56
-[repospath]: https://gitlab.com/gitlab-org/gitlab-foss/-/blob/8-9-stable/config/gitlab.yml.example#L457
diff --git a/doc/administration/repository_storage_types.md b/doc/administration/repository_storage_types.md
index 2e2ed431c8b..a95178c01e2 100644
--- a/doc/administration/repository_storage_types.md
+++ b/doc/administration/repository_storage_types.md
@@ -1,16 +1,15 @@
-# Repository Storage Types
+# Repository storage types **(CORE ONLY)**
-> [Introduced][ce-28283] in GitLab 10.0.
-
-Two different storage layouts can be used
-to store the repositories on disk and their characteristics.
+> - [Introduced](https://gitlab.com/gitlab-org/gitlab-foss/issues/28283) in GitLab 10.0.
+> - Hashed storage became the default for new installations in GitLab 12.0
+> - Hashed storage is enabled by default for new and renamed projects in GitLab 13.0.
GitLab can be configured to use one or multiple repository storage paths/shard
locations that can be:
- Mounted to the local disk
- Exposed as an NFS shared volume
-- Accessed via [Gitaly] on its own machine.
+- Accessed via [Gitaly](gitaly/index.md) on its own machine.
In GitLab, this is configured in `/etc/gitlab/gitlab.rb` by the `git_data_dirs({})`
configuration hash. The storage layouts discussed here will apply to any shard
@@ -20,40 +19,17 @@ The `default` repository shard that is available in any installations
that haven't customized it, points to the local folder: `/var/opt/gitlab/git-data`.
Anything discussed below is expected to be part of that folder.
-## Legacy Storage
-
-Legacy Storage is the storage behavior prior to version 10.0. For historical
-reasons, GitLab replicated the same mapping structure from the projects URLs:
+## Hashed storage
-- Project's repository: `#{namespace}/#{project_name}.git`
-- Project's wiki: `#{namespace}/#{project_name}.wiki.git`
+NOTE: **Note:**
+In GitLab 13.0, hashed storage is enabled by default and the legacy storage is
+deprecated. Support for legacy storage will be removed in GitLab 14.0.
+If you haven't migrated yet, check the
+[migration instructions](raketasks/storage.md#migrate-to-hashed-storage).
+The option to choose between hashed and legacy storage in the admin area has
+been disabled.
-This structure made it simple to migrate from existing solutions to GitLab and
-easy for Administrators to find where the repository is stored.
-
-On the other hand this has some drawbacks:
-
-Storage location will concentrate huge amount of top-level namespaces. The
-impact can be reduced by the introduction of
-[multiple storage paths](repository_storage_paths.md).
-
-Because backups are a snapshot of the same URL mapping, if you try to recover a
-very old backup, you need to verify whether any project has taken the place of
-an old removed or renamed project sharing the same URL. This means that
-`mygroup/myproject` from your backup may not be the same original project that
-is at that same URL today.
-
-Any change in the URL will need to be reflected on disk (when groups / users or
-projects are renamed). This can add a lot of load in big installations,
-especially if using any type of network based filesystem.
-
-## Hashed Storage
-
-CAUTION: **Important:**
-Geo requires Hashed Storage since 12.0. If you haven't migrated yet,
-check the [migration instructions](#how-to-migrate-to-hashed-storage) ASAP.
-
-Hashed Storage is the new storage behavior we rolled out with 10.0. Instead
+Hashed storage is the storage behavior we rolled out with 10.0. Instead
of coupling project URL and the folder structure where the repository will be
stored on disk, we are coupling a hash, based on the project's ID. This makes
the folder structure immutable, and therefore eliminates any requirement to
@@ -124,7 +100,7 @@ GitLab server. For example, on a default Omnibus installation this would be
`/var/opt/gitlab/git-data/repositories/@hashed/b1/7e/b17ef6d19c7a5b1ee83b907c595526dcb1eb06db8227d650d5dda0a9f4ce8cd9.git`
with `.git` from the end of the directory name removed.
-The output includes the project id and the project name:
+The output includes the project ID and the project name:
```plaintext
=> #<Project id:16 it/supportteam/ticketsystem>
@@ -134,6 +110,11 @@ The output includes the project id and the project name:
> [Introduced](https://gitlab.com/gitlab-org/gitaly/issues/1606) in GitLab 12.1.
+DANGER: **Danger:**
+Do not run `git prune` or `git gc` in pool repositories! This can
+cause data loss in "real" repositories that depend on the pool in
+question.
+
Forks of public projects are deduplicated by creating a third repository, the
object pool, containing the objects from the source project. Using
`objects/info/alternates`, the source project and forks use the object pool for
@@ -145,71 +126,15 @@ when housekeeping is run on the source project.
"@pools/#{hash[0..1]}/#{hash[2..3]}/#{hash}.git"
```
-DANGER: **Danger:**
-Do not run `git prune` or `git gc` in pool repositories! This can
-cause data loss in "real" repositories that depend on the pool in
-question.
-
-### How to migrate to Hashed Storage
-
-To start a migration, enable Hashed Storage for new projects:
-
-1. Go to **Admin > Settings > Repository** and expand the **Repository Storage** section.
-1. Select the **Use hashed storage paths for newly created and renamed projects** checkbox.
-
-Check if the change breaks any existing integration you may have that
-either runs on the same machine as your repositories are located, or may login to that machine
-to access data (for example, a remote backup solution).
-
-To schedule a complete rollout, see the
-[Rake task documentation for storage migration][rake/migrate-to-hashed] for instructions.
-
-If you do have any existing integration, you may want to do a small rollout first,
-to validate. You can do so by specifying a range with the operation.
-
-This is an example of how to limit the rollout to Project IDs 50 to 100, running in
-an Omnibus GitLab installation:
-
-```shell
-sudo gitlab-rake gitlab:storage:migrate_to_hashed ID_FROM=50 ID_TO=100
-```
-
-Check the [documentation][rake/migrate-to-hashed] for additional information and instructions for
-source-based installation.
-
-#### Rollback
-
-Similar to the migration, to disable Hashed Storage for new
-projects:
-
-1. Go to **Admin > Settings > Repository** and expand the **Repository Storage** section.
-1. Uncheck the **Use hashed storage paths for newly created and renamed projects** checkbox.
-
-To schedule a complete rollback, see the
-[Rake task documentation for storage rollback](raketasks/storage.md#rollback-from-hashed-storage-to-legacy-storage) for instructions.
-
-The rollback task also supports specifying a range of Project IDs. Here is an example
-of limiting the rollout to Project IDs 50 to 100, in an Omnibus GitLab installation:
-
-```shell
-sudo gitlab-rake gitlab:storage:rollback_to_legacy ID_FROM=50 ID_TO=100
-```
+### Hashed storage coverage migration
-If you have a Geo setup, please note that the rollback will not be reflected automatically
-on the **secondary** node. You may need to wait for a backfill operation to kick-in and remove
-the remaining repositories from the special `@hashed/` folder manually.
-
-### Hashed Storage coverage
-
-We are incrementally moving every storable object in GitLab to the Hashed
-Storage pattern. You can check the current coverage status below (and also see
-the [issue][ce-2821]).
-
-Note that things stored in an S3 compatible endpoint will not have the downsides
+Files stored in an S3 compatible endpoint will not have the downsides
mentioned earlier, if they are not prefixed with `#{namespace}/#{project_name}`,
which is true for CI Cache and LFS Objects.
-| Storable Object | Legacy Storage | Hashed Storage | S3 Compatible | GitLab Version |
+In the table below, you can find the coverage of the migration to the hashed storage.
+
+| Storable Object | Legacy storage | Hashed storage | S3 Compatible | GitLab Version |
| --------------- | -------------- | -------------- | ------------- | -------------- |
| Repository | Yes | Yes | - | 10.0 |
| Attachments | Yes | Yes | - | 10.2 |
@@ -222,18 +147,16 @@ which is true for CI Cache and LFS Objects.
| LFS Objects | Yes | Similar | Yes | 10.0 / 10.7 |
| Repository pools| No | Yes | - | 11.6 |
-#### Implementation Details
-
-##### Avatars
+#### Avatars
Each file is stored in a folder with its `id` from the database. The filename is always `avatar.png` for user avatars.
When avatar is replaced, `Upload` model is destroyed and a new one takes place with different `id`.
-##### CI Artifacts
+#### CI artifacts
CI Artifacts are S3 compatible since **9.4** (GitLab Premium), and available in GitLab Core since **10.6**.
-##### LFS Objects
+#### LFS objects
[LFS Objects in GitLab](../topics/git/lfs/index.md) implement a similar
storage pattern using 2 chars, 2 level folders, following Git's own implementation:
@@ -247,7 +170,38 @@ storage pattern using 2 chars, 2 level folders, following Git's own implementati
LFS objects are also [S3 compatible](lfs/index.md#storing-lfs-objects-in-remote-object-storage).
-[ce-2821]: https://gitlab.com/gitlab-com/infrastructure/issues/2821
-[ce-28283]: https://gitlab.com/gitlab-org/gitlab-foss/issues/28283
-[rake/migrate-to-hashed]: raketasks/storage.md#migrate-existing-projects-to-hashed-storage
-[gitaly]: gitaly/index.md
+## Legacy storage
+
+NOTE: **Deprecated:**
+In GitLab 13.0, hashed storage is enabled by default and the legacy storage is
+deprecated. If you haven't migrated yet, check the
+[migration instructions](raketasks/storage.md#migrate-to-hashed-storage).
+Support for legacy storage will be removed in GitLab 14.0. If you're on GitLab
+13.0 and later, switching new projects to legacy storage is not possible.
+The option to choose between hashed and legacy storage in the admin area has
+been disabled.
+
+Legacy storage is the storage behavior prior to version 10.0. For historical
+reasons, GitLab replicated the same mapping structure from the projects URLs:
+
+- Project's repository: `#{namespace}/#{project_name}.git`
+- Project's wiki: `#{namespace}/#{project_name}.wiki.git`
+
+This structure made it simple to migrate from existing solutions to GitLab and
+easy for Administrators to find where the repository is stored.
+
+On the other hand this has some drawbacks:
+
+Storage location will concentrate huge amount of top-level namespaces. The
+impact can be reduced by the introduction of
+[multiple storage paths](repository_storage_paths.md).
+
+Because backups are a snapshot of the same URL mapping, if you try to recover a
+very old backup, you need to verify whether any project has taken the place of
+an old removed or renamed project sharing the same URL. This means that
+`mygroup/myproject` from your backup may not be the same original project that
+is at that same URL today.
+
+Any change in the URL will need to be reflected on disk (when groups / users or
+projects are renamed). This can add a lot of load in big installations,
+especially if using any type of network based filesystem.
diff --git a/doc/administration/restart_gitlab.md b/doc/administration/restart_gitlab.md
index 907d7bb307a..57f53fd6cc4 100644
--- a/doc/administration/restart_gitlab.md
+++ b/doc/administration/restart_gitlab.md
@@ -12,18 +12,18 @@ If you want the TL;DR versions, jump to:
## Omnibus installations
-If you have used the [Omnibus packages][omnibus-dl] to install GitLab, then
+If you have used the [Omnibus packages](https://about.gitlab.com/install/) to install GitLab, then
you should already have `gitlab-ctl` in your `PATH`.
`gitlab-ctl` interacts with the Omnibus packages and can be used to restart the
-GitLab Rails application (Unicorn) as well as the other components, like:
+GitLab Rails application (Puma) as well as the other components, like:
- GitLab Workhorse
- Sidekiq
- PostgreSQL (if you are using the bundled one)
- NGINX (if you are using the bundled one)
- Redis (if you are using the bundled one)
-- [Mailroom][]
+- [Mailroom](reply_by_email.md)
- Logrotate
### Omnibus GitLab restart
@@ -45,7 +45,7 @@ ok: run: nginx: (pid 11309) 0s
ok: run: postgresql: (pid 11316) 1s
ok: run: redis: (pid 11325) 0s
ok: run: sidekiq: (pid 11331) 1s
-ok: run: unicorn: (pid 11338) 0s
+ok: run: puma: (pid 11338) 0s
```
To restart a component separately, you can append its service name to the
@@ -86,7 +86,7 @@ sudo gitlab-ctl reconfigure
Reconfiguring GitLab should occur in the event that something in its
configuration (`/etc/gitlab/gitlab.rb`) has changed.
-When you run this command, [Chef], the underlying configuration management
+When you run this command, [Chef](https://www.chef.io/products/chef-infra/), the underlying configuration management
application that powers Omnibus GitLab, will make sure that all things like directories,
permissions, and services are in place and in the same shape that they were
initially shipped.
@@ -101,7 +101,7 @@ depend on those files.
## Installations from source
If you have followed the official installation guide to [install GitLab from
-source][install], run the following command to restart GitLab:
+source](../install/installation.md), run the following command to restart GitLab:
```shell
sudo service gitlab restart
@@ -110,46 +110,39 @@ sudo service gitlab restart
The output should be similar to this:
```plaintext
-Shutting down GitLab Unicorn
+Shutting down GitLab Puma
Shutting down GitLab Sidekiq
Shutting down GitLab Workhorse
Shutting down GitLab MailRoom
...
GitLab is not running.
-Starting GitLab Unicorn
+Starting GitLab Puma
Starting GitLab Sidekiq
Starting GitLab Workhorse
Starting GitLab MailRoom
...
-The GitLab Unicorn web server with pid 28059 is running.
+The GitLab Puma web server with pid 28059 is running.
The GitLab Sidekiq job dispatcher with pid 28176 is running.
The GitLab Workhorse with pid 28122 is running.
The GitLab MailRoom email processor with pid 28114 is running.
GitLab and all its components are up and running.
```
-This should restart Unicorn, Sidekiq, GitLab Workhorse, and [Mailroom][]
+This should restart Puma, Sidekiq, GitLab Workhorse, and [Mailroom](reply_by_email.md)
(if enabled). The init service file that does all the magic can be found on
your server in `/etc/init.d/gitlab`.
---
If you are using other init systems, like systemd, you can check the
-[GitLab Recipes][gl-recipes] repository for some unofficial services. These are
+[GitLab Recipes](https://gitlab.com/gitlab-org/gitlab-recipes/tree/master/init) repository for some unofficial services. These are
**not** officially supported so use them at your own risk.
-[omnibus-dl]: https://about.gitlab.com/install/ "Download the Omnibus packages"
-[install]: ../install/installation.md "Documentation to install GitLab from source"
-[mailroom]: reply_by_email.md "Used for replying by email in GitLab issues and merge requests"
-[chef]: https://www.chef.io/products/chef-infra/ "Chef official website"
-[src-service]: https://gitlab.com/gitlab-org/gitlab/blob/master/lib/support/init.d/gitlab "GitLab init service file"
-[gl-recipes]: https://gitlab.com/gitlab-org/gitlab-recipes/tree/master/init "GitLab Recipes repository"
-
## Helm chart installations
There is no single command to restart the entire GitLab application installed via
the [cloud native Helm Chart](https://docs.gitlab.com/charts/). Usually, it should be
-enough to restart a specific component separately (for example, `gitaly`, `unicorn`,
+enough to restart a specific component separately (for example, `gitaly`, `puma`,
`workhorse`, or `gitlab-shell`) by deleting all the pods related to it:
```shell
diff --git a/doc/administration/scaling/index.md b/doc/administration/scaling/index.md
index ec7492883cc..748373c8941 100644
--- a/doc/administration/scaling/index.md
+++ b/doc/administration/scaling/index.md
@@ -1,256 +1,5 @@
---
-type: reference, concepts
+redirect_to: ../reference_architectures/index.md
---
-# Scaling
-
-GitLab supports a number of scaling options to ensure that your self-managed
-instance is able to scale out to meet your organization's needs when scaling up
-a single-box GitLab installation is no longer practical or feasible.
-
-Please consult our [high availability documentation](../availability/index.md)
-if your organization requires fault tolerance and redundancy features, such as
-automatic database system failover.
-
-## GitLab components and scaling instructions
-
-Here's a list of components directly provided by Omnibus GitLab or installed as
-part of a source installation and their configuration instructions for scaling.
-
-| Component | Description | Configuration instructions |
-|-----------|-------------|----------------------------|
-| [PostgreSQL](../../development/architecture.md#postgresql) | Database | [PostgreSQL configuration](https://docs.gitlab.com/omnibus/settings/database.html) |
-| [Redis](../../development/architecture.md#redis) | Key/value store for fast data lookup and caching | [Redis configuration](../high_availability/redis.md) |
-| [GitLab application services](../../development/architecture.md#unicorn) | Unicorn/Puma, Workhorse, GitLab Shell - serves front-end requests (UI, API, Git over HTTP/SSH) | [GitLab app scaling configuration](../high_availability/gitlab.md) |
-| [PgBouncer](../../development/architecture.md#pgbouncer) | Database connection pooler | [PgBouncer configuration](../high_availability/pgbouncer.md#running-pgbouncer-as-part-of-a-non-ha-gitlab-installation) **(PREMIUM ONLY)** |
-| [Sidekiq](../../development/architecture.md#sidekiq) | Asynchronous/background jobs | [Sidekiq configuration](../high_availability/sidekiq.md) |
-| [Gitaly](../../development/architecture.md#gitaly) | Provides access to Git repositories | [Gitaly configuration](../gitaly/index.md#running-gitaly-on-its-own-server) |
-| [Prometheus](../../development/architecture.md#prometheus) and [Grafana](../../development/architecture.md#grafana) | GitLab environment monitoring | [Monitoring node for scaling](../high_availability/monitoring_node.md) |
-
-## Third-party services used for scaling
-
-Here's a list of third-party services you may require as part of scaling GitLab.
-The services can be provided by numerous applications or vendors and further
-advice is given on how best to select the right choice for your organization's
-needs.
-
-| Component | Description | Configuration instructions |
-|-----------|-------------|----------------------------|
-| Load balancer(s) | Handles load balancing, typically when you have multiple GitLab application services nodes | [Load balancer configuration](../high_availability/load_balancer.md) |
-| Object storage service | Recommended store for shared data objects | [Cloud Object Storage configuration](../high_availability/object_storage.md) |
-| NFS | Shared disk storage service. Can be used as an alternative for Gitaly or Object Storage. Required for GitLab Pages | [NFS configuration](../high_availability/nfs.md) |
-
-## Reference architectures
-
-- 1 - 1000 Users: A single-node [Omnibus](https://docs.gitlab.com/omnibus/) setup with frequent backups. Refer to the [Single-node Omnibus installation](#single-node-installation) section below.
-- 1000 to 50000+ Users: A [Scaled-out Omnibus installation with multiple servers](#multi-node-installation-scaled-out-for-availability), it can be with or without high-availability components applied.
- - To decide the level of Availability please refer to our [Availability](../availability/index.md) page.
-
-### Single-node installation
-
-This solution is appropriate for many teams that have a single server at their disposal. With automatic backup of the GitLab repositories, configuration, and the database, this can be an optimal solution if you don't have strict availability requirements.
-
-You can also optionally configure GitLab to use an [external PostgreSQL service](../external_database.md)
-or an [external object storage service](../high_availability/object_storage.md) for added
-performance and reliability at a relatively low complexity cost.
-
-References:
-
-- [Installation Docs](../../install/README.md)
-- [Backup/Restore Docs](https://docs.gitlab.com/omnibus/settings/backups.html#backup-and-restore-omnibus-gitlab-configuration)
-
-### Multi-node installation (scaled out for availability)
-
-This solution is appropriate for teams that are starting to scale out when
-scaling up is no longer meeting their needs. In this configuration, additional application nodes will handle frontend traffic, with a load balancer in front to distribute traffic across those nodes. Meanwhile, each application node connects to a shared file server and PostgreSQL and Redis services on the back end.
-
-The additional application servers adds limited fault tolerance to your GitLab
-instance. As long as one application node is online and capable of handling the
-instance's usage load, your team's productivity will not be interrupted. Having
-multiple application nodes also enables [zero-downtime updates](https://docs.gitlab.com/omnibus/update/#zero-downtime-updates).
-
-References:
-
-- [Configure your load balancer for GitLab](../high_availability/load_balancer.md)
-- [Configure your NFS server to work with GitLab](../high_availability/nfs.md)
-- [Configure packaged PostgreSQL server to listen on TCP/IP](https://docs.gitlab.com/omnibus/settings/database.html#configure-packaged-postgresql-server-to-listen-on-tcpip)
-- [Setting up a Redis-only server](https://docs.gitlab.com/omnibus/settings/redis.html#setting-up-a-redis-only-server)
-
-In this section we'll detail the Reference Architectures that can support large numbers
-of users. These were built, tested and verified by our Quality and Support teams.
-
-Testing was done with our GitLab Performance Tool at specific coded workloads, and the
-throughputs used for testing were calculated based on sample customer data. We
-test each endpoint type with the following number of requests per second (RPS)
-per 1000 users:
-
-- API: 20 RPS
-- Web: 2 RPS
-- Git: 2 RPS
-
-NOTE: **Note:** Note that depending on your workflow the below recommended
-reference architectures may need to be adapted accordingly. Your workload
-is influenced by factors such as - but not limited to - how active your users are,
-how much automation you use, mirroring, and repo/change size. Additionally the
-shown memory values are given directly by [GCP machine types](https://cloud.google.com/compute/docs/machine-types).
-On different cloud vendors a best effort like for like can be used.
-
-#### 2,000 user configuration
-
-- **Supported users (approximate):** 2,000
-- **Test RPS rates:** API: 40 RPS, Web: 4 RPS, Git: 4 RPS
-- **Known issues:** [List of known performance issues](https://gitlab.com/gitlab-org/gitlab/issues?label_name%5B%5D=Quality%3Aperformance-issues)
-
-| Service | Nodes | Configuration[^8] | GCP type | AWS type[^9] |
-| ----------------------------|-------|-----------------------|---------------|--------------|
-| GitLab Rails[^1] | 3 | 8 vCPU, 7.2GB Memory | n1-highcpu-8 | c5.2xlarge |
-| PostgreSQL | 3 | 2 vCPU, 7.5GB Memory | n1-standard-2 | m5.large |
-| PgBouncer | 3 | 2 vCPU, 1.8GB Memory | n1-highcpu-2 | c5.large |
-| Gitaly[^2] [^5] [^7] | X | 4 vCPU, 15GB Memory | n1-standard-4 | m5.xlarge |
-| Redis[^3] | 3 | 2 vCPU, 7.5GB Memory | n1-standard-2 | m5.large |
-| Consul + Sentinel[^3] | 3 | 2 vCPU, 1.8GB Memory | n1-highcpu-2 | c5.large |
-| Sidekiq | 4 | 2 vCPU, 7.5GB Memory | n1-standard-2 | m5.large |
-| Cloud Object Storage[^4] | - | - | - | - |
-| NFS Server[^5] [^7] | 1 | 4 vCPU, 3.6GB Memory | n1-highcpu-4 | c5.xlarge |
-| Monitoring node | 1 | 2 vCPU, 1.8GB Memory | n1-highcpu-2 | c5.large |
-| External load balancing node[^6] | 1 | 2 vCPU, 1.8GB Memory | n1-highcpu-2 | c5.large |
-| Internal load balancing node[^6] | 1 | 2 vCPU, 1.8GB Memory | n1-highcpu-2 | c5.large |
-
-#### 5,000 user configuration
-
-- **Supported users (approximate):** 5,000
-- **Test RPS rates:** API: 100 RPS, Web: 10 RPS, Git: 10 RPS
-- **Known issues:** [List of known performance issues](https://gitlab.com/gitlab-org/gitlab/issues?label_name%5B%5D=Quality%3Aperformance-issues)
-
-| Service | Nodes | Configuration[^8] | GCP type | AWS type[^9] |
-| ----------------------------|-------|------------------------|---------------|--------------|
-| GitLab Rails[^1] | 3 | 16 vCPU, 14.4GB Memory | n1-highcpu-16 | c5.4xlarge |
-| PostgreSQL | 3 | 2 vCPU, 7.5GB Memory | n1-standard-2 | m5.large |
-| PgBouncer | 3 | 2 vCPU, 1.8GB Memory | n1-highcpu-2 | c5.large |
-| Gitaly[^2] [^5] [^7] | X | 8 vCPU, 30GB Memory | n1-standard-8 | m5.2xlarge |
-| Redis[^3] | 3 | 2 vCPU, 7.5GB Memory | n1-standard-2 | m5.large |
-| Consul + Sentinel[^3] | 3 | 2 vCPU, 1.8GB Memory | n1-highcpu-2 | c5.large |
-| Sidekiq | 4 | 2 vCPU, 7.5GB Memory | n1-standard-2 | m5.large |
-| Cloud Object Storage[^4] | - | - | - | - |
-| NFS Server[^5] [^7] | 1 | 4 vCPU, 3.6GB Memory | n1-highcpu-4 | c5.xlarge |
-| Monitoring node | 1 | 2 vCPU, 1.8GB Memory | n1-highcpu-2 | c5.large |
-| External load balancing node[^6] | 1 | 2 vCPU, 1.8GB Memory | n1-highcpu-2 | c5.large |
-| Internal load balancing node[^6] | 1 | 2 vCPU, 1.8GB Memory | n1-highcpu-2 | c5.large |
-
-#### 10,000 user configuration
-
-- **Supported users (approximate):** 10,000
-- **Test RPS rates:** API: 200 RPS, Web: 20 RPS, Git: 20 RPS
-- **Known issues:** [List of known performance issues](https://gitlab.com/gitlab-org/gitlab/issues?label_name%5B%5D=Quality%3Aperformance-issues)
-
-| Service | Nodes | GCP Configuration[^8] | GCP type | AWS type[^9] |
-| ----------------------------|-------|------------------------|----------------|--------------|
-| GitLab Rails[^1] | 3 | 32 vCPU, 28.8GB Memory | n1-highcpu-32 | c5.9xlarge |
-| PostgreSQL | 3 | 4 vCPU, 15GB Memory | n1-standard-4 | m5.xlarge |
-| PgBouncer | 3 | 2 vCPU, 1.8GB Memory | n1-highcpu-2 | c5.large |
-| Gitaly[^2] [^5] [^7] | X | 16 vCPU, 60GB Memory | n1-standard-16 | m5.4xlarge |
-| Redis[^3] - Cache | 3 | 4 vCPU, 15GB Memory | n1-standard-4 | m5.xlarge |
-| Redis[^3] - Queues / Shared State | 3 | 4 vCPU, 15GB Memory | n1-standard-4 | m5.xlarge |
-| Redis Sentinel[^3] - Cache | 3 | 1 vCPU, 1.7GB Memory | g1-small | t2.small |
-| Redis Sentinel[^3] - Queues / Shared State | 3 | 1 vCPU, 1.7GB Memory | g1-small | t2.small |
-| Consul | 3 | 2 vCPU, 1.8GB Memory | n1-highcpu-2 | c5.large |
-| Sidekiq | 4 | 4 vCPU, 15GB Memory | n1-standard-4 | m5.xlarge |
-| Cloud Object Storage[^4] | - | - | - | - |
-| NFS Server[^5] [^7] | 1 | 4 vCPU, 3.6GB Memory | n1-highcpu-4 | c5.xlarge |
-| Monitoring node | 1 | 4 vCPU, 3.6GB Memory | n1-highcpu-4 | c5.xlarge |
-| External load balancing node[^6] | 1 | 2 vCPU, 1.8GB Memory | n1-highcpu-2 | c5.large |
-| Internal load balancing node[^6] | 1 | 2 vCPU, 1.8GB Memory | n1-highcpu-2 | c5.large |
-
-#### 25,000 user configuration
-
-- **Supported users (approximate):** 25,000
-- **Test RPS rates:** API: 500 RPS, Web: 50 RPS, Git: 50 RPS
-- **Known issues:** [List of known performance issues](https://gitlab.com/gitlab-org/gitlab/issues?label_name%5B%5D=Quality%3Aperformance-issues)
-
-| Service | Nodes | Configuration[^8] | GCP type | AWS type[^9] |
-| ----------------------------|-------|------------------------|----------------|--------------|
-| GitLab Rails[^1] | 5 | 32 vCPU, 28.8GB Memory | n1-highcpu-32 | c5.9xlarge |
-| PostgreSQL | 3 | 8 vCPU, 30GB Memory | n1-standard-8 | m5.2xlarge |
-| PgBouncer | 3 | 2 vCPU, 1.8GB Memory | n1-highcpu-2 | c5.large |
-| Gitaly[^2] [^5] [^7] | X | 32 vCPU, 120GB Memory | n1-standard-32 | m5.8xlarge |
-| Redis[^3] - Cache | 3 | 4 vCPU, 15GB Memory | n1-standard-4 | m5.xlarge |
-| Redis[^3] - Queues / Shared State | 3 | 4 vCPU, 15GB Memory | n1-standard-4 | m5.xlarge |
-| Redis Sentinel[^3] - Cache | 3 | 1 vCPU, 1.7GB Memory | g1-small | t2.small |
-| Redis Sentinel[^3] - Queues / Shared State | 3 | 1 vCPU, 1.7GB Memory | g1-small | t2.small |
-| Consul | 3 | 2 vCPU, 1.8GB Memory | n1-highcpu-2 | c5.large |
-| Sidekiq | 4 | 4 vCPU, 15GB Memory | n1-standard-4 | m5.xlarge |
-| Cloud Object Storage[^4] | - | - | - | - |
-| NFS Server[^5] [^7] | 1 | 4 vCPU, 3.6GB Memory | n1-highcpu-4 | c5.xlarge |
-| Monitoring node | 1 | 4 vCPU, 3.6GB Memory | n1-highcpu-4 | c5.xlarge |
-| External load balancing node[^6] | 1 | 2 vCPU, 1.8GB Memory | n1-highcpu-2 | c5.large |
-| Internal load balancing node[^6] | 1 | 4 vCPU, 3.6GB Memory | n1-highcpu-4 | c5.xlarge |
-
-#### 50,000 user configuration
-
-- **Supported users (approximate):** 50,000
-- **Test RPS rates:** API: 1000 RPS, Web: 100 RPS, Git: 100 RPS
-- **Known issues:** [List of known performance issues](https://gitlab.com/gitlab-org/gitlab/issues?label_name%5B%5D=Quality%3Aperformance-issues)
-
-| Service | Nodes | Configuration[^8] | GCP type | AWS type[^9] |
-| ----------------------------|-------|------------------------|----------------|--------------|
-| GitLab Rails[^1] | 12 | 32 vCPU, 28.8GB Memory | n1-highcpu-32 | c5.9xlarge |
-| PostgreSQL | 3 | 16 vCPU, 60GB Memory | n1-standard-16 | m5.4xlarge |
-| PgBouncer | 3 | 2 vCPU, 1.8GB Memory | n1-highcpu-2 | c5.large |
-| Gitaly[^2] [^5] [^7] | X | 64 vCPU, 240GB Memory | n1-standard-64 | m5.16xlarge |
-| Redis[^3] - Cache | 3 | 4 vCPU, 15GB Memory | n1-standard-4 | m5.xlarge |
-| Redis[^3] - Queues / Shared State | 3 | 4 vCPU, 15GB Memory | n1-standard-4 | m5.xlarge |
-| Redis Sentinel[^3] - Cache | 3 | 1 vCPU, 1.7GB Memory | g1-small | t2.small |
-| Redis Sentinel[^3] - Queues / Shared State | 3 | 1 vCPU, 1.7GB Memory | g1-small | t2.small |
-| Consul | 3 | 2 vCPU, 1.8GB Memory | n1-highcpu-2 | c5.large |
-| Sidekiq | 4 | 4 vCPU, 15GB Memory | n1-standard-4 | m5.xlarge |
-| NFS Server[^5] [^7] | 1 | 4 vCPU, 3.6GB Memory | n1-highcpu-4 | c5.xlarge |
-| Cloud Object Storage[^4] | - | - | - | - |
-| Monitoring node | 1 | 4 vCPU, 3.6GB Memory | n1-highcpu-4 | c5.xlarge |
-| External load balancing node[^6] | 1 | 2 vCPU, 1.8GB Memory | n1-highcpu-2 | c5.large |
-| Internal load balancing node[^6] | 1 | 8 vCPU, 7.2GB Memory | n1-highcpu-8 | c5.2xlarge |
-
-[^1]: In our architectures we run each GitLab Rails node using the Puma webserver
- and have its number of workers set to 90% of available CPUs along with 4 threads.
-
-[^2]: Gitaly node requirements are dependent on customer data, specifically the number of
- projects and their sizes. We recommend 2 nodes as an absolute minimum for HA environments
- and at least 4 nodes should be used when supporting 50,000 or more users.
- We also recommend that each Gitaly node should store no more than 5TB of data
- and have the number of [`gitaly-ruby` workers](../gitaly/index.md#gitaly-ruby)
- set to 20% of available CPUs. Additional nodes should be considered in conjunction
- with a review of expected data size and spread based on the recommendations above.
-
-[^3]: Recommended Redis setup differs depending on the size of the architecture.
- For smaller architectures (up to 5,000 users) we suggest one Redis cluster for all
- classes and that Redis Sentinel is hosted alongside Consul.
- For larger architectures (10,000 users or more) we suggest running a separate
- [Redis Cluster](../high_availability/redis.md#running-multiple-redis-clusters) for the Cache class
- and another for the Queues and Shared State classes respectively. We also recommend
- that you run the Redis Sentinel clusters separately as well for each Redis Cluster.
-
-[^4]: For data objects such as LFS, Uploads, Artifacts, etc. We recommend a [Cloud Object Storage service](../object_storage.md)
- over NFS where possible, due to better performance and availability.
-
-[^5]: NFS can be used as an alternative for both repository data (replacing Gitaly) and
- object storage but this isn't typically recommended for performance reasons. Note however it is required for
- [GitLab Pages](https://gitlab.com/gitlab-org/gitlab-pages/issues/196).
-
-[^6]: Our architectures have been tested and validated with [HAProxy](https://www.haproxy.org/)
- as the load balancer. However other reputable load balancers with similar feature sets
- should also work instead but be aware these aren't validated.
-
-[^7]: We strongly recommend that any Gitaly and / or NFS nodes are set up with SSD disks over
- HDD with a throughput of at least 8,000 IOPS for read operations and 2,000 IOPS for write
- as these components have heavy I/O. These IOPS values are recommended only as a starter
- as with time they may be adjusted higher or lower depending on the scale of your
- environment's workload. If you're running the environment on a Cloud provider
- you may need to refer to their documentation on how configure IOPS correctly.
-
-[^8]: The architectures were built and tested with the [Intel Xeon E5 v3 (Haswell)](https://cloud.google.com/compute/docs/cpu-platforms)
- CPU platform on GCP. On different hardware you may find that adjustments, either lower
- or higher, are required for your CPU or Node counts accordingly. For more information, a
- [Sysbench](https://github.com/akopytov/sysbench) benchmark of the CPU can be found
- [here](https://gitlab.com/gitlab-org/quality/performance/-/wikis/Reference-Architectures/GCP-CPU-Benchmarks).
-
-[^9]: AWS-equivalent configurations are rough suggestions and may change in the
- future. They have not yet been tested and validated.
+This document was moved to [another location](../reference_architectures/index.md).
diff --git a/doc/administration/server_hooks.md b/doc/administration/server_hooks.md
index 06c560a01ca..0b8c66805ae 100644
--- a/doc/administration/server_hooks.md
+++ b/doc/administration/server_hooks.md
@@ -11,12 +11,13 @@ disqus_identifier: 'https://docs.gitlab.com/ee/administration/custom_hooks.html'
> - Server hooks must be configured on the filesystem of the GitLab server. Only GitLab server administrators will be able to complete these tasks. Please explore [webhooks](../user/project/integrations/webhooks.md) and [GitLab CI/CD](../ci/README.md) as an option if you do not have filesystem access. For a user-configurable Git hook interface, see [Push Rules](../push_rules/push_rules.md), available in GitLab Starter **(STARTER)**.
> - Server hooks won't be replicated to secondary nodes if you use [GitLab Geo](geo/replication/index.md).
-Git natively supports hooks that are executed on different actions.
-Examples of server-side Git hooks include pre-receive, post-receive, and update.
-See [Git SCM Server-Side Hooks](https://git-scm.com/book/en/v2/Customizing-Git-Git-Hooks#Server-Side-Hooks) for more information about each hook type.
+Git natively supports hooks that are executed on different actions. These hooks run
+on the server and can be used to enforce specific commit policies or perform other
+tasks based on the state of the repository.
-As of GitLab Shell version 2.2.0 (which requires GitLab 7.5+), GitLab
-administrators can add custom Git hooks to any GitLab project.
+Examples of server-side Git hooks include `pre-receive`, `post-receive`, and `update`.
+See [Git SCM Server-Side Hooks](https://git-scm.com/book/en/v2/Customizing-Git-Git-Hooks#Server-Side-Hooks)
+for more information about each hook type.
## Create a server hook for a repository
@@ -24,14 +25,21 @@ Server-side Git hooks are typically placed in the repository's `hooks`
subdirectory. In GitLab, hook directories are symlinked to the GitLab Shell
`hooks` directory for ease of maintenance between GitLab Shell upgrades.
Server hooks are implemented differently, but the behavior is exactly the same
-once the hook is created. Follow the steps below to set up a server hook for a
+once the hook is created.
+
+NOTE: **Note:**
+If you are not using [hashed storage](repository_storage_types.md#hashed-storage), the project's
+repository directory might not exactly match the instructions below. In that case,
+for an installation from source the path is usually `/home/git/repositories/<group>/<project>.git`.
+For Omnibus installs the path is usually `/var/opt/gitlab/git-data/repositories/<group>/<project>.git`.
+
+Follow the steps below to set up a server hook for a
repository:
-1. Pick a project that needs a server hook.
-1. On the GitLab server, navigate to the project's repository directory.
- For an installation from source the path is usually
- `/home/git/repositories/<group>/<project>.git`. For Omnibus installs the path is
- usually `/var/opt/gitlab/git-data/repositories/<group>/<project>.git`.
+1. Find that project's path on the GitLab server, by navigating to the
+ **Admin area > Projects**. From there, select the project for which you
+ would like to add a hook. You can find the path to the project's repository
+ under **Gitaly relative path** on that page.
1. Create a new directory in this location called `custom_hooks`.
1. Inside the new `custom_hooks` directory, create a file with a name matching
the hook type. For a pre-receive hook the file name should be `pre-receive`
@@ -42,8 +50,7 @@ repository:
type. For example, if the script is in Ruby the shebang will probably be
`#!/usr/bin/env ruby`.
-That's it! Assuming the hook code is properly implemented the hook will fire
-as appropriate.
+Assuming the hook code is properly implemented the hook will run as appropriate.
## Set a global server hook for all repositories
diff --git a/doc/administration/smime_signing_email.md b/doc/administration/smime_signing_email.md
index ed7447c0da9..bab7c5c260d 100644
--- a/doc/administration/smime_signing_email.md
+++ b/doc/administration/smime_signing_email.md
@@ -18,6 +18,9 @@ files must be provided:
intervention.
- Only RSA keys are supported.
+Optionally, you can also provide a bundle of CA certs (PEM-encoded) to be
+included on each signature. This will typically be an intermediate CA.
+
NOTE: **Note:** Be mindful of the access levels for your private keys and visibility to
third parties.
@@ -29,6 +32,8 @@ third parties.
gitlab_rails['gitlab_email_smime_enabled'] = true
gitlab_rails['gitlab_email_smime_key_file'] = '/etc/gitlab/ssl/gitlab_smime.key'
gitlab_rails['gitlab_email_smime_cert_file'] = '/etc/gitlab/ssl/gitlab_smime.crt'
+ # Optional
+ gitlab_rails['gitlab_email_smime_ca_certs_file'] = '/etc/gitlab/ssl/gitlab_smime_cas.crt'
```
1. Save the file and [reconfigure GitLab](restart_gitlab.md#omnibus-gitlab-reconfigure) for the changes to take effect.
@@ -49,6 +54,9 @@ NOTE: **Note:** The key needs to be readable by the GitLab system user (`git` by
# S/MIME public certificate key in PEM format, will be attached to signed messages
# Default is '.gitlab_smime_cert' relative to Rails.root (i.e. root of the GitLab app).
cert_file: /etc/pki/smime/certs/gitlab.crt
+ # S/MIME extra CA public certificates in PEM format, will be attached to signed messages
+ # Optional
+ ca_certs_file: /etc/pki/smime/certs/gitlab_cas.crt
```
1. Save the file and [restart GitLab](restart_gitlab.md#installations-from-source) for the changes to take effect.
diff --git a/doc/administration/static_objects_external_storage.md b/doc/administration/static_objects_external_storage.md
index f649a1ebcd2..973f4304115 100644
--- a/doc/administration/static_objects_external_storage.md
+++ b/doc/administration/static_objects_external_storage.md
@@ -63,7 +63,7 @@ other CDNs or Function as a Service (FaaS) systems should work using the same pr
`pwgen -cn1 64` on a UNIX machine). Save this token for the admin panel, as
described in the [configuring](#configuring) section.
- ```js
+ ```javascript
const ORIGIN_HOSTNAME = 'gitlab.installation.com' // FIXME: SET CORRECT VALUE
const STORAGE_TOKEN = 'very-secure-token' // FIXME: SET CORRECT VALUE
const CACHE_PRIVATE_OBJECTS = false
diff --git a/doc/administration/terraform_state.md b/doc/administration/terraform_state.md
index 0956edaf252..77d5495418e 100644
--- a/doc/administration/terraform_state.md
+++ b/doc/administration/terraform_state.md
@@ -30,7 +30,7 @@ below.
gitlab_rails['terraform_state_storage_path'] = "/mnt/storage/terraform_state"
```
-1. Save the file and [reconfigure GitLab][] for the changes to take effect.
+1. Save the file and [reconfigure GitLab](restart_gitlab.md#omnibus-gitlab-reconfigure) for the changes to take effect.
**In installations from source:**
@@ -43,7 +43,7 @@ below.
storage_path: /mnt/storage/terraform_state
```
-1. Save the file and [restart GitLab][] for the changes to take effect.
+1. Save the file and [restart GitLab](restart_gitlab.md#installations-from-source) for the changes to take effect.
## Using object storage **(CORE ONLY)**
@@ -62,7 +62,7 @@ The following settings are:
| Setting | Description | Default |
|---------|-------------|---------|
-| `enabled` | Enable/disable object storage | `true` |
+| `enabled` | Enable/disable object storage | `false` |
| `remote_directory` | The bucket name where Terraform state files will be stored | |
| `connection` | Various connection options described below | |
@@ -111,7 +111,7 @@ The connection settings match those provided by [Fog](https://github.com/fog), a
}
```
-1. Save the file and [reconfigure GitLab][] for the changes to take effect.
+1. Save the file and [reconfigure GitLab](restart_gitlab.md#omnibus-gitlab-reconfigure) for the changes to take effect.
**In installations from source:**
@@ -131,7 +131,4 @@ The connection settings match those provided by [Fog](https://github.com/fog), a
region: eu-central-1
```
-1. Save the file and [restart GitLab][] for the changes to take effect.
-
-[reconfigure gitlab]: restart_gitlab.md#omnibus-gitlab-reconfigure "How to reconfigure Omnibus GitLab"
-[restart gitlab]: restart_gitlab.md#installations-from-source "How to restart GitLab"
+1. Save the file and [restart GitLab](restart_gitlab.md#installations-from-source) for the changes to take effect.
diff --git a/doc/administration/timezone.md b/doc/administration/timezone.md
index fa0fbb43433..d6112c45141 100644
--- a/doc/administration/timezone.md
+++ b/doc/administration/timezone.md
@@ -2,7 +2,7 @@
The global time zone configuration parameter can be changed in `config/gitlab.yml`:
-```text
+```plaintext
# time_zone: 'UTC'
```
@@ -15,7 +15,7 @@ To see all available time zones, run `bundle exec rake time:zones:all`.
For Omnibus installations, run `gitlab-rake time:zones:all`.
NOTE: **Note:**
-Currently, this Rake task does not list timezones in TZInfo format required by GitLab Omnibus during a reconfigure: [#58672](https://gitlab.com/gitlab-org/gitlab-foss/issues/58672).
+Currently, this Rake task does not list timezones in TZInfo format required by Omnibus GitLab during a reconfigure: [#58672](https://gitlab.com/gitlab-org/gitlab-foss/issues/58672).
## Changing time zone in Omnibus installations
diff --git a/doc/administration/troubleshooting/elasticsearch.md b/doc/administration/troubleshooting/elasticsearch.md
index 77a63112ea3..a39fe4ba8c3 100644
--- a/doc/administration/troubleshooting/elasticsearch.md
+++ b/doc/administration/troubleshooting/elasticsearch.md
@@ -1,5 +1,8 @@
# Troubleshooting Elasticsearch
+To install and configure Elasticsearch, and for common and known issues,
+visit the [administrator documentation](../../integration/elasticsearch.md).
+
Troubleshooting Elasticsearch requires:
- Knowledge of common terms.
@@ -203,7 +206,7 @@ The best place to start is to determine if the issue is with creating an empty i
If it is, check on the Elasticsearch side to determine if the `gitlab-production` (the
name for the GitLab index) exists. If it exists, manually delete it on the Elasticsearch
side and attempt to recreate it from the
-[`create_empty_index`](../../integration/elasticsearch.md#gitlab-elasticsearch-rake-tasks)
+[`recreate_index`](../../integration/elasticsearch.md#gitlab-elasticsearch-rake-tasks)
Rake task.
If you still encounter issues, try creating an index manually on the Elasticsearch
diff --git a/doc/administration/troubleshooting/gitlab_rails_cheat_sheet.md b/doc/administration/troubleshooting/gitlab_rails_cheat_sheet.md
index ba2224e3fc7..2cbc994fb4c 100644
--- a/doc/administration/troubleshooting/gitlab_rails_cheat_sheet.md
+++ b/doc/administration/troubleshooting/gitlab_rails_cheat_sheet.md
@@ -150,9 +150,9 @@ Project.update_all(visibility_level: 0)
#
projects = Project.where(pending_delete: true)
projects.each do |p|
- puts "Project name: #{p.id}"
+ puts "Project ID: #{p.id}"
puts "Project name: #{p.name}"
- puts "Repository path: #{p.repository.storage_path}"
+ puts "Repository path: #{p.repository.full_path}"
end
#
@@ -214,6 +214,20 @@ p.each do |project|
end
```
+### Bulk update to disable the Slack Notification service
+
+To disable notifications for all projects that have Slack service enabled, do:
+
+```ruby
+# Grab all projects that have the Slack notifications enabled
+p = Project.find_by_sql("SELECT p.id FROM projects p LEFT JOIN services s ON p.id = s.project_id WHERE s.type = 'SlackService' AND s.active = true")
+
+# Disable the service on each of the projects that were found.
+p.each do |project|
+ project.slack_service.update_attribute(:active, false)
+end
+```
+
## Wikis
### Recreate
@@ -558,29 +572,7 @@ Ci::Pipeline.where(project_id: p.id).where(status: 'pending').count
### Remove artifacts more than a week old
-The Latest version of these steps can be found in the [job artifacts documentation](../job_artifacts.md)
-
-```ruby
-### SELECTING THE BUILDS TO CLEAR
-# For a single project:
-project = Project.find_by_full_path('')
-builds_with_artifacts = project.builds.with_artifacts_archive
-
-# Instance-wide:
-builds_with_artifacts = Ci::Build.with_artifacts_archive
-
-# Prior to 10.6 the above lines would be:
-# builds_with_artifacts = project.builds.with_artifacts
-# builds_with_artifacts = Ci::Build.with_artifacts
-
-### CLEAR THEM OUT
-# Note that this will also erase artifacts that developers marked to "Keep"
-builds_to_clear = builds_with_artifacts.where("finished_at < ?", 1.week.ago)
-builds_to_clear.each do |build|
- build.artifacts_expire_at = Time.now
- build.erase_erasable_artifacts!
-end
-```
+This section has been moved to the [job artifacts troubleshooting documentation](../job_artifacts.md#delete-job-artifacts-from-jobs-completed-before-a-specific-date).
### Find reason failure (for when build trace is empty) (Introduced in 10.3.0)
@@ -603,6 +595,14 @@ m = project.merge_requests.find_by(iid: )
m.project.try(:ci_service)
```
+### Validate the `.gitlab-ci.yml`
+
+```ruby
+project = Project.find_by_full_path 'group/project'
+content = project.repository.gitlab_ci_yml_for(project.repository.root_ref_sha)
+Gitlab::Ci::YamlProcessor.validation_message(content, user: User.first)
+```
+
### Disable AutoDevOps on Existing Projects
```ruby
diff --git a/doc/administration/troubleshooting/kubernetes_cheat_sheet.md b/doc/administration/troubleshooting/kubernetes_cheat_sheet.md
index 30ef3da3a99..cab073b9924 100644
--- a/doc/administration/troubleshooting/kubernetes_cheat_sheet.md
+++ b/doc/administration/troubleshooting/kubernetes_cheat_sheet.md
@@ -52,7 +52,7 @@ and they will assist you with any issues you are having.
- Check logs via Kubectl:
```shell
- kubectl logs <unicorn pod> -c dependencies
+ kubectl logs <webservice pod> -c dependencies
```
- How to tail all Kubernetes cluster events in real time:
@@ -72,7 +72,7 @@ and they will assist you with any issues you are having.
This is the principle of Kubernetes, read [Twelve-factor app](https://12factor.net/)
for details.
-- How to get cronjobs configured on a cluster
+- How to get cron jobs configured on a cluster
```shell
kubectl get cronjobs
@@ -87,20 +87,20 @@ and they will assist you with any issues you are having.
- Minimal config that can be used to test a Kubernetes Helm chart can be found
[here](https://gitlab.com/gitlab-org/charts/gitlab/issues/620).
-- Tailing logs of a separate pod. An example for a Unicorn pod:
+- Tailing logs of a separate pod. An example for a Webservice pod:
```shell
- kubectl logs gitlab-unicorn-7656fdd6bf-jqzfs -c unicorn
+ kubectl logs gitlab-webservice-54fbf6698b-hpckq -c webservice
```
-- Tail and follow all pods that share a label (in this case, `unicorn`):
+- Tail and follow all pods that share a label (in this case, `webservice`):
```shell
- # all containers in the unicorn pods
- kubectl logs -f -l app=unicorn --all-containers=true --max-log-requests=50
+ # all containers in the webservice pods
+ kubectl logs -f -l app=webservice --all-containers=true --max-log-requests=50
- # only the unicorn containers in all unicorn pods
- kubectl logs -f -l app=unicorn -c unicorn --max-log-requests=50
+ # only the webservice containers in all webservice pods
+ kubectl logs -f -l app=webservice -c webservice --max-log-requests=50
```
- One can stream logs from all containers at once, similar to the Omnibus
@@ -132,7 +132,7 @@ and they will assist you with any issues you are having.
/srv/gitlab/bin/rails console
# source-style commands should also work
- /srv/gitlab && bundle exec rake gitlab:check RAILS_ENV=production
+ cd /srv/gitlab && bundle exec rake gitlab:check RAILS_ENV=production
# run GitLab check. Note that the output can be confusing and invalid because of the specific structure of GitLab installed via helm chart
/usr/local/bin/gitlab-rake gitlab:check
@@ -206,7 +206,7 @@ all Kubernetes resources and dependent charts:
helm get manifest <release name>
```
-## Installation of minimal GitLab config via Minukube on macOS
+## Installation of minimal GitLab config via Minikube on macOS
This section is based on [Developing for Kubernetes with Minikube](https://docs.gitlab.com/charts/development/minikube/index.html)
and [Helm](https://docs.gitlab.com/charts/installation/tools.html#helm). Refer
@@ -230,31 +230,33 @@ to those documents for details.
```shell
minikube start --cpus 3 --memory 8192 # minimum amount for GitLab to work
minikube addons enable ingress
- minikube addons enable kube-dns
```
- Install Helm via Homebrew and initialize it:
```shell
- brew install kubernetes-helm
- helm init --service-account tiller
+ brew install helm
```
-- Copy the file <https://gitlab.com/gitlab-org/charts/gitlab/raw/master/examples/values-minikube-minimum.yaml>
- to your workstation.
+- Copy the [Minikube minimum values YAML file](https://gitlab.com/gitlab-org/charts/gitlab/raw/master/examples/values-minikube-minimum.yaml)
+ to your workstation:
-- Find the IP address in the output of `minikube ip` and update the yaml file with
+ ```shell
+ curl --output values.yaml "https://gitlab.com/gitlab-org/charts/gitlab/raw/master/examples/values-minikube-minimum.yaml"
+ ```
+
+- Find the IP address in the output of `minikube ip` and update the YAML file with
this IP address.
- Install the GitLab Helm Chart:
```shell
helm repo add gitlab https://charts.gitlab.io
- helm install --name gitlab -f <path-to-yaml-file> gitlab/gitlab
+ helm install gitlab -f <path-to-yaml-file> gitlab/gitlab
```
If you want to modify some GitLab settings, you can use the above-mentioned config
- as a base and create your own yaml file.
+ as a base and create your own YAML file.
- Monitor the installation progress via `helm status gitlab` and `minikube dashboard`.
The installation could take up to 20-30 minutes depending on the amount of resources
@@ -263,7 +265,7 @@ to those documents for details.
- When all the pods show either a `Running` or `Completed` status, get the GitLab password as
described in [Initial login](https://docs.gitlab.com/charts/installation/deployment.html#initial-login),
and log in to GitLab via the UI. It will be accessible via `https://gitlab.domain`
- where `domain` is the value provided in the yaml file.
+ where `domain` is the value provided in the YAML file.
<!-- ## Troubleshooting
diff --git a/doc/administration/troubleshooting/log_parsing.md b/doc/administration/troubleshooting/log_parsing.md
index 0413e5ce953..dcd1df2f423 100644
--- a/doc/administration/troubleshooting/log_parsing.md
+++ b/doc/administration/troubleshooting/log_parsing.md
@@ -16,19 +16,19 @@ include use cases targeted for parsing GitLab log files.
#### Pipe colorized `jq` output into `less`
-```sh
+```shell
jq . <FILE> -C | less -R
```
#### Search for a term and pretty-print all matching lines
-```sh
+```shell
grep <TERM> <FILE> | jq .
```
#### Skip invalid lines of JSON
-```sh
+```shell
jq -cR 'fromjson?' file.json | jq <COMMAND>
```
@@ -39,49 +39,49 @@ This skips over all invalid lines and parses the rest.
#### Find all requests with a 5XX status code
-```sh
+```shell
jq 'select(status >= 500)' <FILE>
```
#### Top 10 slowest requests
-```sh
+```shell
jq -s 'sort_by(-.duration) | limit(10; .[])' <FILE>
```
#### Find and pretty print all requests related to a project
-```sh
+```shell
grep <PROJECT_NAME> <FILE> | jq .
```
#### Find all requests with a total duration > 5 seconds
-```sh
+```shell
jq 'select(.duration > 5000)' <FILE>
```
#### Find all project requests with more than 5 rugged calls
-```sh
+```shell
grep <PROJECT_NAME> <FILE> | jq 'select(.rugged_calls > 5)'
```
#### Find all requests with a Gitaly duration > 10 seconds
-```sh
+```shell
jq 'select(.gitaly_duration > 10000)' <FILE>
```
#### Find all requests with a queue duration > 10 seconds
-```sh
+```shell
jq 'select(.queue_duration > 10000)' <FILE>
```
#### Top 10 requests by # of Gitaly calls
-```sh
+```shell
jq -s 'map(select(.gitaly_calls != null)) | sort_by(-.gitaly_calls) | limit(10; .[])' <FILE>
```
@@ -89,7 +89,7 @@ jq -s 'map(select(.gitaly_calls != null)) | sort_by(-.gitaly_calls) | limit(10;
#### Print the top three controller methods by request volume and their three longest durations
-```sh
+```shell
jq -s -r 'group_by(.controller+.action) | sort_by(-length) | limit(3; .[]) | sort_by(-.duration) | "CT: \(length)\tMETHOD: \(.[0].controller)#\(.[0].action)\tDURS: \(.[0].duration), \(.[1].duration), \(.[2].duration)"' production_json.log
```
@@ -105,7 +105,7 @@ CT: 1328 METHOD: Projects::NotesController#index DURS: 403.99, 386.29, 384.3
#### Print top three routes with request count and their three longest durations
-```sh
+```shell
jq -s -r 'group_by(.route) | sort_by(-length) | limit(3; .[]) | sort_by(-.duration) | "CT: \(length)\tROUTE: \(.[0].route)\tDURS: \(.[0].duration), \(.[1].duration), \(.[2].duration)"' api_json.log
```
@@ -121,25 +121,25 @@ CT: 190 ROUTE: /api/:version/projects/:id/repository/commits DURS: 1079.02,
#### Find all Gitaly requests sent from web UI
-```sh
+```shell
jq 'select(."grpc.meta.client_name" == "gitlab-web")' current
```
#### Find all failed Gitaly requests
-```sh
+```shell
jq 'select(."grpc.code" != null and ."grpc.code" != "OK")' current
```
#### Find all requests that took longer than 30 seconds
-```sh
+```shell
jq 'select(."grpc.time_ms" > 30000)' current
```
#### Print top three projects by request volume and their three longest durations
-```sh
+```shell
jq -s -r 'map(select(."grpc.request.glProjectPath" != null and ."grpc.request.glProjectPath" != "" and ."grpc.time_ms" != null)) | group_by(."grpc.request.glProjectPath") | sort_by(-length) | limit(3; .[]) | sort_by(-."grpc.time_ms") | "CT: \(length)\tPROJECT: \(.[0]."grpc.request.glProjectPath")\tDURS: \(.[0]."grpc.time_ms"), \(.[1]."grpc.time_ms"), \(.[2]."grpc.time_ms")"' current
```
diff --git a/doc/administration/troubleshooting/postgresql.md b/doc/administration/troubleshooting/postgresql.md
index 8e7727ee214..65a6bffca44 100644
--- a/doc/administration/troubleshooting/postgresql.md
+++ b/doc/administration/troubleshooting/postgresql.md
@@ -33,7 +33,7 @@ This section is for links to information elsewhere in the GitLab documentation.
- [More about external PostgreSQL](../external_database.md)
-- [Running GEO with external PostgreSQL](../geo/replication/external_database.md)
+- [Running Geo with external PostgreSQL](../geo/replication/external_database.md)
- [Upgrades when running PostgreSQL configured for HA.](https://docs.gitlab.com/omnibus/settings/database.html#upgrading-a-gitlab-ha-cluster)
@@ -45,7 +45,7 @@ This section is for links to information elsewhere in the GitLab documentation.
- Managing Omnibus PostgreSQL versions [from the development docs](https://docs.gitlab.com/omnibus/development/managing-postgresql-versions.html)
-- [PostgreSQL scaling and HA](../high_availability/database.md)
+- [PostgreSQL scaling](../high_availability/database.md)
- including [troubleshooting](../high_availability/database.md#troubleshooting) `gitlab-ctl repmgr-check-master` and PgBouncer errors
- [Developer database documentation](../../development/README.md#database-guides) - some of which is absolutely not for production use. Including:
@@ -71,7 +71,7 @@ This section is for links to information elsewhere in the GitLab documentation.
HINT: Free one or increase max_replication_slots.
```
-- GEO [replication errors](../geo/replication/troubleshooting.md#fixing-replication-errors) including:
+- Geo [replication errors](../geo/replication/troubleshooting.md#fixing-replication-errors) including:
```plaintext
ERROR: replication slots can only be used if max_replication_slots > 0
@@ -83,11 +83,11 @@ This section is for links to information elsewhere in the GitLab documentation.
PANIC: could not write to file ‘pg_xlog/xlogtemp.123’: No space left on device
```
-- [Checking GEO configuration](../geo/replication/troubleshooting.md#checking-configuration) including
+- [Checking Geo configuration](../geo/replication/troubleshooting.md#checking-configuration) including
- reconfiguring hosts/ports
- checking and fixing user/password mappings
-- [Common GEO errors](../geo/replication/troubleshooting.md#fixing-common-errors)
+- [Common Geo errors](../geo/replication/troubleshooting.md#fixing-common-errors)
## Support topics
diff --git a/doc/administration/troubleshooting/sidekiq.md b/doc/administration/troubleshooting/sidekiq.md
index 31e41725834..ca21c038267 100644
--- a/doc/administration/troubleshooting/sidekiq.md
+++ b/doc/administration/troubleshooting/sidekiq.md
@@ -282,7 +282,8 @@ Commonly, `<condition>` references the job arguments, which depend on the type o
For example, `repository_import` has `project_id` as the job argument, while `update_merge_requests` has `project_id, user_id, oldrev, newrev, ref`.
-NOTE: **Note:** Arguments need to be referenced by their sequence id using `job.args[<id>]` because `job.args` is a list of all arguments provided to the Sidekiq job.
+NOTE: **Note:**
+Arguments need to be referenced by their sequence ID using `job.args[<id>]` because `job.args` is a list of all arguments provided to the Sidekiq job.
Here are some examples:
diff --git a/doc/administration/troubleshooting/ssl.md b/doc/administration/troubleshooting/ssl.md
index f230f047ded..e6c081e1eea 100644
--- a/doc/administration/troubleshooting/ssl.md
+++ b/doc/administration/troubleshooting/ssl.md
@@ -46,6 +46,44 @@ After configuring a GitLab instance with an internal CA certificate, you might n
If you have the problems listed above, add your certificate to `/etc/gitlab/trusted-certs` and run `sudo gitlab-ctl reconfigure`.
+## X.509 key values mismatch error
+
+After configuring your instance with a certificate bundle, NGINX may throw the
+following error:
+
+`SSL: error:0B080074:x509 certificate routines:X509_check_private_key:key values mismatch`
+
+This error means that the server certificate and key you have provided do not
+match. You can confirm this by running the following command and comparing the
+output:
+
+```shell
+openssl rsa -noout -modulus -in path/to/your/.key | openssl md5
+openssl x509 -noout -modulus -in path/to/your/.crt | openssl md5
+```
+
+The following is an example of an md5 output between a matching key and certificate. Note the
+matching md5 hashes:
+
+```shell
+$ openssl rsa -noout -modulus -in private.key | openssl md5
+4f49b61b25225abeb7542b29ae20e98c
+$ openssl x509 -noout -modulus -in public.crt | openssl md5
+4f49b61b25225abeb7542b29ae20e98c
+```
+
+This is an opposing output with a non-matching key and certificate which shows different md5 hashes:
+
+```shell
+$ openssl rsa -noout -modulus -in private.key | openssl md5
+d418865077299af27707b1d1fa83cd99
+$ openssl x509 -noout -modulus -in public.crt | openssl md5
+4f49b61b25225abeb7542b29ae20e98c
+```
+
+If the two outputs differ like the above example, there is a mismatch between the certificate
+and key. You should contact the provider of the SSL certificate for further support.
+
## Using GitLab Runner with a GitLab instance configured with internal CA certificate or self-signed certificate
Besides getting the errors mentioned in
diff --git a/doc/administration/uploads.md b/doc/administration/uploads.md
index f29deba3d40..0a300084342 100644
--- a/doc/administration/uploads.md
+++ b/doc/administration/uploads.md
@@ -25,7 +25,7 @@ _The uploads are stored by default in `/var/opt/gitlab/gitlab-rails/uploads`._
gitlab_rails['uploads_base_dir'] = "uploads"
```
-1. Save the file and [reconfigure GitLab][] for the changes to take effect.
+1. Save the file and [reconfigure GitLab](restart_gitlab.md#omnibus-gitlab-reconfigure) for the changes to take effect.
**In installations from source:**
@@ -41,13 +41,13 @@ _The uploads are stored by default in
base_dir: uploads
```
-1. Save the file and [restart GitLab][] for the changes to take effect.
+1. Save the file and [restart GitLab](restart_gitlab.md#installations-from-source) for the changes to take effect.
## Using object storage **(CORE ONLY)**
> **Notes:**
>
-> - [Introduced][ee-3867] in [GitLab Premium][eep] 10.5.
+> - [Introduced](https://gitlab.com/gitlab-org/gitlab/-/merge_requests/3867) in [GitLab Premium](https://about.gitlab.com/pricing/) 10.5.
> - [Introduced](https://gitlab.com/gitlab-org/gitlab-foss/-/merge_requests/17358) in [GitLab Core](https://about.gitlab.com/pricing/) 10.7.
> - Since version 11.1, we support direct_upload to S3.
@@ -65,7 +65,7 @@ For source installations the following settings are nested under `uploads:` and
|---------|-------------|---------|
| `enabled` | Enable/disable object storage | `false` |
| `remote_directory` | The bucket name where Uploads will be stored| |
-| `direct_upload` | Set to true to remove Unicorn from the Upload path. Workhorse handles the actual Artifact Upload to Object Storage while Unicorn does minimal processing to keep track of the upload. There is no need for local shared storage. The option may be removed if support for a single storage type for all files is introduced. Read more on [direct upload](../development/uploads.md#direct-upload). | `false` |
+| `direct_upload` | Set to true to remove Puma from the Upload path. Workhorse handles the actual Artifact Upload to Object Storage while Puma does minimal processing to keep track of the upload. There is no need for local shared storage. The option may be removed if support for a single storage type for all files is introduced. Read more on [direct upload](../development/uploads.md#direct-upload). | `false` |
| `background_upload` | Set to false to disable automatic upload. Option may be removed once upload is direct to S3 (if `direct_upload` is set to `true` it will override `background_upload`) | `true` |
| `proxy_download` | Set to true to enable proxying all files served. Option allows to reduce egress traffic as this allows clients to download directly from remote storage instead of proxying all data | `false` |
| `connection` | Various connection options described below | |
@@ -76,16 +76,16 @@ The connection settings match those provided by [Fog](https://github.com/fog), a
| Setting | Description | Default |
|---------|-------------|---------|
-| `provider` | Always `AWS` for compatible hosts | AWS |
+| `provider` | Always `AWS` for compatible hosts | `AWS` |
| `aws_access_key_id` | AWS credentials, or compatible | |
| `aws_secret_access_key` | AWS credentials, or compatible | |
-| `aws_signature_version` | AWS signature version to use. 2 or 4 are valid options. Digital Ocean Spaces and other providers may need 2. | 4 |
-| `enable_signature_v4_streaming` | Set to true to enable HTTP chunked transfers with [AWS v4 signatures](https://docs.aws.amazon.com/AmazonS3/latest/API/sigv4-streaming.html). Oracle Cloud S3 needs this to be false | true |
+| `aws_signature_version` | AWS signature version to use. `2` or `4` are valid options. Digital Ocean Spaces and other providers may need `2`. | `4` |
+| `enable_signature_v4_streaming` | Set to true to enable HTTP chunked transfers with [AWS v4 signatures](https://docs.aws.amazon.com/AmazonS3/latest/API/sigv4-streaming.html). Oracle Cloud S3 needs this to be `false`. | `true` |
| `region` | AWS region | us-east-1 |
-| `host` | S3 compatible host for when not using AWS, e.g. `localhost` or `storage.example.com` | s3.amazonaws.com |
+| `host` | S3 compatible host for when not using AWS, e.g. `localhost` or `storage.example.com` | `s3.amazonaws.com` |
| `endpoint` | Can be used when configuring an S3 compatible service such as [MinIO](https://min.io), by entering a URL such as `http://127.0.0.1:9000` | (optional) |
-| `path_style` | Set to true to use `host/bucket_name/object` style paths instead of `bucket_name.host/object`. Leave as false for AWS S3 | false |
-| `use_iam_profile` | Set to true to use IAM profile instead of access keys | false
+| `path_style` | Set to `true` to use `host/bucket_name/object` style paths instead of `bucket_name.host/object`. Leave as `false` for AWS S3. | `false` |
+| `use_iam_profile` | Set to `true` to use IAM profile instead of access keys | false
**In Omnibus installations:**
@@ -117,7 +117,7 @@ _The uploads are stored by default in
}
```
-1. Save the file and [reconfigure GitLab][] for the changes to take effect.
+1. Save the file and [reconfigure GitLab](restart_gitlab.md#omnibus-gitlab-reconfigure) for the changes to take effect.
1. Migrate any existing local uploads to the object storage using [`gitlab:uploads:migrate` Rake task](raketasks/uploads/migrate.md).
**In installations from source:**
@@ -140,7 +140,7 @@ _The uploads are stored by default in
region: eu-central-1
```
-1. Save the file and [restart GitLab][] for the changes to take effect.
+1. Save the file and [restart GitLab](restart_gitlab.md#installations-from-source) for the changes to take effect.
1. Migrate any existing local uploads to the object storage using [`gitlab:uploads:migrate` Rake task](raketasks/uploads/migrate.md).
### Oracle Cloud S3 connection settings
@@ -149,8 +149,8 @@ Note that Oracle Cloud S3 must be sure to use the following settings:
| Setting | Value |
|---------|-------|
-| `enable_signature_v4_streaming` | false |
-| `path_style` | true |
+| `enable_signature_v4_streaming` | `false` |
+| `path_style` | `true` |
If `enable_signature_v4_streaming` is set to `true`, you may see the
following error:
@@ -165,7 +165,7 @@ The connection settings match those provided by [Fog](https://github.com/fog), a
| Setting | Description | Default |
|---------|-------------|---------|
-| `provider` | Always `OpenStack` for compatible hosts | OpenStack |
+| `provider` | Always `OpenStack` for compatible hosts | `OpenStack` |
| `openstack_username` | OpenStack username | |
| `openstack_api_key` | OpenStack API key | |
| `openstack_temp_url_key` | OpenStack key for generating temporary urls | |
@@ -194,7 +194,7 @@ _The uploads are stored by default in
}
```
-1. Save the file and [reconfigure GitLab][] for the changes to take effect.
+1. Save the file and [reconfigure GitLab](restart_gitlab.md#omnibus-gitlab-reconfigure) for the changes to take effect.
1. Migrate any existing local uploads to the object storage using [`gitlab:uploads:migrate` Rake task](raketasks/uploads/migrate.md).
---
@@ -225,10 +225,5 @@ _The uploads are stored by default in
openstack_tenant: 'TENANT_ID'
```
-1. Save the file and [reconfigure GitLab][] for the changes to take effect.
+1. Save the file and [reconfigure GitLab](restart_gitlab.md#omnibus-gitlab-reconfigure) for the changes to take effect.
1. Migrate any existing local uploads to the object storage using [`gitlab:uploads:migrate` Rake task](raketasks/uploads/migrate.md).
-
-[reconfigure gitlab]: restart_gitlab.md#omnibus-gitlab-reconfigure "How to reconfigure Omnibus GitLab"
-[restart gitlab]: restart_gitlab.md#installations-from-source "How to restart GitLab"
-[eep]: https://about.gitlab.com/pricing/ "GitLab Premium"
-[ee-3867]: https://gitlab.com/gitlab-org/gitlab/-/merge_requests/3867
diff --git a/doc/api/README.md b/doc/api/README.md
index 3c8d3dc4902..34d496a37fe 100644
--- a/doc/api/README.md
+++ b/doc/api/README.md
@@ -58,7 +58,7 @@ Currently only API version v4 is available. Version v3 was removed in
## Basic usage
API requests should be prefixed with `api` and the API version. The API version
-is defined in [`lib/api.rb`][lib-api-url]. For example, the root of the v4 API
+is defined in [`lib/api.rb`](https://gitlab.com/gitlab-org/gitlab-foss/tree/master/lib/api/api.rb). For example, the root of the v4 API
is at `/api/v4`.
Example of a valid API request using cURL:
@@ -75,12 +75,13 @@ end of an API URL.
Most API requests require authentication, or will only return public data when
authentication is not provided. For
those cases where it is not required, this will be mentioned in the documentation
-for each individual endpoint. For example, the [`/projects/:id` endpoint](projects.md).
+for each individual endpoint. For example, the [`/projects/:id` endpoint](projects.md#get-single-project).
-There are four ways to authenticate with the GitLab API:
+There are several ways to authenticate with the GitLab API:
1. [OAuth2 tokens](#oauth2-tokens)
-1. [Personal access tokens](#personal-access-tokens)
+1. [Personal access tokens](../user/profile/personal_access_tokens.md)
+1. [Project access tokens](../user/project/settings/project_access_tokens.md) **(CORE ONLY)**
1. [Session cookie](#session-cookie)
1. [GitLab CI/CD job token](#gitlab-ci-job-token) **(Specific endpoints only)**
@@ -117,31 +118,29 @@ curl --header "Authorization: Bearer OAUTH-TOKEN" https://gitlab.example.com/api
Read more about [GitLab as an OAuth2 provider](oauth2.md).
-### Personal access tokens
+### Personal/project access tokens
-You can use a [personal access token][pat] to authenticate with the API by passing it in either the
-`private_token` parameter or the `Private-Token` header.
+Access tokens can be used to authenticate with the API by passing it in either the `private_token` parameter
+or the `Private-Token` header.
-Example of using the personal access token in a parameter:
+Example of using the personal/project access token in a parameter:
```shell
curl https://gitlab.example.com/api/v4/projects?private_token=<your_access_token>
```
-Example of using the personal access token in a header:
+Example of using the personal/project access token in a header:
```shell
curl --header "Private-Token: <your_access_token>" https://gitlab.example.com/api/v4/projects
```
-You can also use personal access tokens with OAuth-compliant headers:
+You can also use personal/project access tokens with OAuth-compliant headers:
```shell
curl --header "Authorization: Bearer <your_access_token>" https://gitlab.example.com/api/v4/projects
```
-Read more about [personal access tokens][pat].
-
### Session cookie
When signing in to the main GitLab application, a `_gitlab_session` cookie is
@@ -163,9 +162,9 @@ to authenticate with the API:
### Impersonation tokens
-> [Introduced][ce-9099] in GitLab 9.0. Needs admin permissions.
+> [Introduced](https://gitlab.com/gitlab-org/gitlab-foss/-/merge_requests/9099) in GitLab 9.0. Needs admin permissions.
-Impersonation tokens are a type of [personal access token][pat]
+Impersonation tokens are a type of [personal access token](../user/profile/personal_access_tokens.md)
that can only be created by an admin for a specific user. They are a great fit
if you want to build applications or scripts that authenticate with the API as a specific user.
@@ -417,7 +416,7 @@ The response header includes a link to the next page. For example:
```http
HTTP/1.1 200 OK
...
-Link: <https://gitlab.example.com/api/v4/projects?pagination=keyset&per_page=50&order_by=id&sort=asc&id_after=42>; rel="next"
+Links: <https://gitlab.example.com/api/v4/projects?pagination=keyset&per_page=50&order_by=id&sort=asc&id_after=42>; rel="next"
Status: 200 OK
...
```
@@ -425,7 +424,7 @@ Status: 200 OK
The link to the next page contains an additional filter `id_after=42` which excludes records we have retrieved already.
Note the type of filter depends on the `order_by` option used and we may have more than one additional filter.
-When the end of the collection has been reached and there are no additional records to retrieve, the `Link` header is absent and the resulting array is empty.
+When the end of the collection has been reached and there are no additional records to retrieve, the `Links` header is absent and the resulting array is empty.
We recommend using only the given link to retrieve the next page instead of building your own URL. Apart from the headers shown,
we don't expose additional pagination headers.
@@ -446,14 +445,20 @@ For example:
DELETE /projects/:id/share/:group_id
```
-The `:id` path parameter needs to be replaced with the project id, and the `:group_id` needs to be replaced with the id of the group. The colons `:` should not be included.
+The `:id` path parameter needs to be replaced with the project ID, and the `:group_id` needs to be replaced with the ID of the group. The colons `:` should not be included.
-The resulting cURL call for a project with id `5` and a group id of `17` is then:
+The resulting cURL call for a project with ID `5` and a group ID of `17` is then:
```shell
curl --request DELETE --header "PRIVATE-TOKEN: <your_access_token>" https://gitlab.example.com/api/v4/projects/5/share/17
```
+NOTE: **Note:**
+Path parameters that are required to be URL-encoded must be followed. If not,
+it will not match an API endpoint and respond with a 404. If there's something
+in front of the API (for example, Apache), ensure that it won't decode the URL-encoded
+path parameters.
+
## Namespaced path encoding
If using namespaced API calls, make sure that the `NAMESPACE/PROJECT_PATH` is
@@ -470,15 +475,16 @@ A project's **path** is not necessarily the same as its **name**. A
project's path can be found in the project's URL or in the project's settings
under **General > Advanced > Change path**.
-## Branches and tags name encoding
+## File path, branches, and tags name encoding
-If your branch or tag contains a `/`, make sure the branch/tag name is
-URL-encoded.
+If a file path, branch or tag contains a `/`, make sure it is URL-encoded.
For example, `/` is represented by `%2F`:
```plaintext
+GET /api/v4/projects/1/repository/files/src%2FREADME.md?ref=master
GET /api/v4/projects/1/branches/my%2Fbranch/commits
+GET /api/v4/projects/1/repository/tags/my%2Ftag
```
## Encoding API parameters of `array` and `hash` types
@@ -642,9 +648,3 @@ For administrator documentation on rate limit settings, see
[Rate limits](../security/rate_limits.md). To find the settings that are
specifically used by GitLab.com, see
[GitLab.com-specific rate limits](../user/gitlab_com/index.md#gitlabcom-specific-rate-limits).
-
-[lib-api-url]: https://gitlab.com/gitlab-org/gitlab-foss/tree/master/lib/api/api.rb
-[ce-3749]: https://gitlab.com/gitlab-org/gitlab-foss/-/merge_requests/3749
-[ce-5951]: https://gitlab.com/gitlab-org/gitlab-foss/-/merge_requests/5951
-[ce-9099]: https://gitlab.com/gitlab-org/gitlab-foss/-/merge_requests/9099
-[pat]: ../user/profile/personal_access_tokens.md
diff --git a/doc/api/api_resources.md b/doc/api/api_resources.md
index 8b3d8462cc9..2adf06a8e95 100644
--- a/doc/api/api_resources.md
+++ b/doc/api/api_resources.md
@@ -26,11 +26,14 @@ The following API resources are available in the project context:
| [Custom attributes](custom_attributes.md) | `/projects/:id/custom_attributes` (also available for groups and users) |
| [Dependencies](dependencies.md) **(ULTIMATE)** | `/projects/:id/dependencies` |
| [Deploy keys](deploy_keys.md) | `/projects/:id/deploy_keys` (also available standalone) |
+| [Freeze Periods](freeze_periods.md) | `/projects/:id/freeze_periods` |
| [Deployments](deployments.md) | `/projects/:id/deployments` |
| [Discussions](discussions.md) (threaded comments) | `/projects/:id/issues/.../discussions`, `/projects/:id/snippets/.../discussions`, `/projects/:id/merge_requests/.../discussions`, `/projects/:id/commits/.../discussions` (also available for groups) |
| [Environments](environments.md) | `/projects/:id/environments` |
| [Error Tracking](error_tracking.md) | `/projects/:id/error_tracking/settings` |
| [Events](events.md) | `/projects/:id/events` (also available for users and standalone) |
+| [Feature Flags](feature_flags.md) | `/projects/:id/feature_flags` |
+| [Feature Flag User Lists](feature_flag_user_lists.md) | `/projects/:id/feature_flags_user_lists` |
| [Issues](issues.md) | `/projects/:id/issues` (also available for groups and standalone) |
| [Issues Statistics](issues_statistics.md) | `/projects/:id/issues_statistics` (also available for groups and standalone) |
| [Issue boards](boards.md) | `/projects/:id/boards` |
@@ -56,7 +59,7 @@ The following API resources are available in the project context:
| [Project milestones](milestones.md) | `/projects/:id/milestones` |
| [Project snippets](project_snippets.md) | `/projects/:id/snippets` |
| [Project templates](project_templates.md) | `/projects/:id/templates` |
-| [Protected_environments](protected_environments.md) | `/projects/:id/protected_environments` |
+| [Protected environments](protected_environments.md) | `/projects/:id/protected_environments` |
| [Protected branches](protected_branches.md) | `/projects/:id/protected_branches` |
| [Protected tags](protected_tags.md) | `/projects/:id/protected_tags` |
| [Releases](releases/index.md) | `/projects/:id/releases` |
@@ -70,7 +73,8 @@ The following API resources are available in the project context:
| [Search](search.md) | `/projects/:id/search` (also available for groups and standalone) |
| [Services](services.md) | `/projects/:id/services` |
| [Tags](tags.md) | `/projects/:id/repository/tags` |
-| [Visual Review discussions](visual_review_discussions.md) **(STARTER**) | `/projects/:id/merge_requests/:merge_request_id/visual_review_discussions` |
+| [User-starred metrics dashboards](metrics_user_starred_dashboards.md ) | `/projects/:id/metrics/user_starred_dashboards` |
+| [Visual Review discussions](visual_review_discussions.md) **(STARTER)** | `/projects/:id/merge_requests/:merge_request_id/visual_review_discussions` |
| [Vulnerabilities](vulnerabilities.md) **(ULTIMATE)** | `/vulnerabilities/:id` |
| [Vulnerability exports](vulnerability_exports.md) **(ULTIMATE)** | `/projects/:id/vulnerability_exports` |
| [Project vulnerabilities](project_vulnerabilities.md) **(ULTIMATE)** | `/projects/:id/vulnerabilities` |
@@ -110,7 +114,8 @@ The following API resources are available outside of project and group contexts
| Resource | Available endpoints |
|:---------------------------------------------------|:------------------------------------------------------------------------|
-| [Admin Sidekiq queues](admin_sidekiq_queues.md) | `/admin/sidekiq/queues/:queue_name` |
+| [Instance-level CI/CD variables](instance_level_ci_variables.md) | `/admin/ci/variables` |
+| [Admin Sidekiq queues](admin_sidekiq_queues.md) | `/admin/sidekiq/queues/:queue_name` |
| [Appearance](appearance.md) **(CORE ONLY)** | `/application/appearance` |
| [Applications](applications.md) | `/applications` |
| [Audit Events](audit_events.md) **(PREMIUM ONLY)** | `/audit_events` |
@@ -130,11 +135,12 @@ The following API resources are available outside of project and group contexts
| [License](license.md) **(CORE ONLY)** | `/license` |
| [Markdown](markdown.md) | `/markdown` |
| [Merge requests](merge_requests.md) | `/merge_requests` (also available for groups and projects) |
-| [Metrics dashboard annotations](metrics_dashboard_annotations.md) | `/environments/:id/metrics_dashboard/annotations` |
+| [Metrics dashboard annotations](metrics_dashboard_annotations.md) | `/environments/:id/metrics_dashboard/annotations`, `/clusters/:id/metrics_dashboard/annotations` |
| [Namespaces](namespaces.md) | `/namespaces` |
| [Notification settings](notification_settings.md) | `/notification_settings` (also available for groups and projects) |
| [Pages domains](pages_domains.md) | `/pages/domains` (also available for projects) |
| [Projects](projects.md) | `/users/:id/projects` (also available for projects) |
+| [Project Repository Storage Moves](project_repository_storage_moves.md) | `/project_repository_storage_moves` |
| [Runners](runners.md) | `/runners` (also available for projects) |
| [Search](search.md) | `/search` (also available for groups and projects) |
| [Settings](settings.md) **(CORE ONLY)** | `/application/settings` |
diff --git a/doc/api/appearance.md b/doc/api/appearance.md
index f9ca6aed01a..733d71ee222 100644
--- a/doc/api/appearance.md
+++ b/doc/api/appearance.md
@@ -27,6 +27,7 @@ Example response:
"header_logo": "/uploads/-/system/appearance/header_logo/1/header.png",
"favicon": "/uploads/-/system/appearance/favicon/1/favicon.png",
"new_project_guidelines": "Please read the FAQs for help.",
+ "profile_image_guidelines": "Custom profile image guidelines",
"header_message": "",
"footer_message": "",
"message_background_color": "#e75e40",
@@ -51,6 +52,7 @@ PUT /application/appearance
| `header_logo` | mixed | no | Instance image used for the main navigation bar
| `favicon` | mixed | no | Instance favicon in .ico/.png format
| `new_project_guidelines` | string | no | Markdown text shown on the new project page
+| `profile_image_guidelines` | string | no | Markdown text shown on the profile page below Public Avatar
| `header_message` | string | no | Message within the system header bar
| `footer_message` | string | no | Message within the system footer bar
| `message_background_color` | string | no | Background color for the system header / footer bar
@@ -71,6 +73,7 @@ Example response:
"header_logo": "/uploads/-/system/appearance/header_logo/1/header.png",
"favicon": "/uploads/-/system/appearance/favicon/1/favicon.png",
"new_project_guidelines": "Please read the FAQs for help.",
+ "profile_image_guidelines": "Custom profile image guidelines",
"header_message": "test",
"footer_message": "",
"message_background_color": "#e75e40",
diff --git a/doc/api/applications.md b/doc/api/applications.md
index c7bfebb75fa..5d4a8b3a99f 100644
--- a/doc/api/applications.md
+++ b/doc/api/applications.md
@@ -16,7 +16,7 @@ Create an application by posting a JSON payload.
Returns `200` if the request succeeds.
-```text
+```plaintext
POST /applications
```
@@ -52,7 +52,7 @@ Example response:
List all registered applications.
-```text
+```plaintext
GET /applications
```
@@ -85,7 +85,7 @@ Delete a specific application.
Returns `204` if the request succeeds.
-```text
+```plaintext
DELETE /applications/:id
```
@@ -93,7 +93,7 @@ Parameters:
| Attribute | Type | Required | Description |
|:----------|:--------|:---------|:----------------------------------------------------|
-| `id` | integer | yes | The id of the application (not the application_id). |
+| `id` | integer | yes | The ID of the application (not the application_id). |
Example request:
diff --git a/doc/api/avatar.md b/doc/api/avatar.md
index 55d5f1e613a..308c0de25f4 100644
--- a/doc/api/avatar.md
+++ b/doc/api/avatar.md
@@ -15,7 +15,7 @@ If:
NOTE: **Note:**
This endpoint can be accessed without authentication.
-```text
+```plaintext
GET /avatar?email=admin@example.com
```
diff --git a/doc/api/award_emoji.md b/doc/api/award_emoji.md
index 7fb96f4f1c0..37b3cd32f89 100644
--- a/doc/api/award_emoji.md
+++ b/doc/api/award_emoji.md
@@ -20,7 +20,7 @@ See [Award Emoji on Comments](#award-emoji-on-comments) for information on using
Get a list of all award emoji for a specified awardable.
-```text
+```plaintext
GET /projects/:id/issues/:issue_iid/award_emoji
GET /projects/:id/merge_requests/:merge_request_iid/award_emoji
GET /projects/:id/snippets/:snippet_id/award_emoji
@@ -82,7 +82,7 @@ Example response:
Get a single award emoji from an issue, snippet, or merge request.
-```text
+```plaintext
GET /projects/:id/issues/:issue_iid/award_emoji/:award_id
GET /projects/:id/merge_requests/:merge_request_iid/award_emoji/:award_id
GET /projects/:id/snippets/:snippet_id/award_emoji/:award_id
@@ -127,7 +127,7 @@ Example response:
Create an award emoji on the specified awardable.
-```text
+```plaintext
POST /projects/:id/issues/:issue_iid/award_emoji
POST /projects/:id/merge_requests/:merge_request_iid/award_emoji
POST /projects/:id/snippets/:snippet_id/award_emoji
@@ -173,7 +173,7 @@ Sometimes it's just not meant to be and you'll have to remove the award.
NOTE: **Note:**
Only available to administrators or the author of the award.
-```text
+```plaintext
DELETE /projects/:id/issues/:issue_iid/award_emoji/:award_id
DELETE /projects/:id/merge_requests/:merge_request_iid/award_emoji/:award_id
DELETE /projects/:id/snippets/:snippet_id/award_emoji/:award_id
@@ -204,7 +204,7 @@ easily adapted for comments on a merge request or on a snippet. Therefore, you h
Get all award emoji for a comment (note).
-```text
+```plaintext
GET /projects/:id/issues/:issue_iid/notes/:note_id/award_emoji
```
@@ -249,7 +249,7 @@ Example response:
Get a single award emoji for a comment (note).
-```text
+```plaintext
GET /projects/:id/issues/:issue_iid/notes/:note_id/award_emoji/:award_id
```
@@ -293,7 +293,7 @@ Example response:
Create an award emoji on the specified comment (note).
-```text
+```plaintext
POST /projects/:id/issues/:issue_iid/notes/:note_id/award_emoji
```
@@ -340,7 +340,7 @@ Sometimes it's just not meant to be and you'll have to remove the award.
NOTE: **Note:**
Only available to administrators or the author of the award.
-```text
+```plaintext
DELETE /projects/:id/issues/:issue_iid/notes/:note_id/award_emoji/:award_id
```
diff --git a/doc/api/boards.md b/doc/api/boards.md
index b99f249cab1..8af23527931 100644
--- a/doc/api/boards.md
+++ b/doc/api/boards.md
@@ -229,7 +229,7 @@ Example response:
## Update a board **(STARTER)**
-> [Introduced][ee-5954] in [GitLab Starter](https://about.gitlab.com/pricing/) 11.1.
+> [Introduced](https://gitlab.com/gitlab-org/gitlab/-/merge_requests/5954) in [GitLab Starter](https://about.gitlab.com/pricing/) 11.1.
Updates a board.
@@ -511,5 +511,3 @@ DELETE /projects/:id/boards/:board_id/lists/:list_id
```shell
curl --request DELETE --header "PRIVATE-TOKEN: <your_access_token>" https://gitlab.example.com/api/v4/projects/5/boards/1/lists/1
```
-
-[ee-5954]: https://gitlab.com/gitlab-org/gitlab/-/merge_requests/5954
diff --git a/doc/api/branches.md b/doc/api/branches.md
index 2f9ca62ced6..7d14a3d54b9 100644
--- a/doc/api/branches.md
+++ b/doc/api/branches.md
@@ -12,7 +12,7 @@ Get a list of repository branches from a project, sorted by name alphabetically.
NOTE: **Note:**
This endpoint can be accessed without authentication if the repository is publicly accessible.
-```text
+```plaintext
GET /projects/:id/repository/branches
```
@@ -41,6 +41,7 @@ Example response:
"developers_can_push": false,
"developers_can_merge": false,
"can_push": true,
+ "web_url": "http://gitlab.example.com/my-group/my-project/-/tree/master",
"commit": {
"author_email": "john@example.com",
"author_name": "John Smith",
@@ -68,7 +69,7 @@ Get a single project repository branch.
NOTE: **Note:**
This endpoint can be accessed without authentication if the repository is publicly accessible.
-```text
+```plaintext
GET /projects/:id/repository/branches/:branch
```
@@ -96,6 +97,7 @@ Example response:
"developers_can_push": false,
"developers_can_merge": false,
"can_push": true,
+ "web_url": "http://gitlab.example.com/my-group/my-project/-/tree/master",
"commit": {
"author_email": "john@example.com",
"author_name": "John Smith",
@@ -128,7 +130,7 @@ for information on unprotecting repository branches.
Create a new branch in the repository.
-```text
+```plaintext
POST /projects/:id/repository/branches
```
@@ -171,7 +173,8 @@ Example response:
"default": false,
"developers_can_push": false,
"developers_can_merge": false,
- "can_push": true
+ "can_push": true,
+ "web_url": "http://gitlab.example.com/my-group/my-project/-/tree/newbranch"
}
```
@@ -182,7 +185,7 @@ Delete a branch from the repository.
NOTE: **Note:**
In the case of an error, an explanation message is provided.
-```text
+```plaintext
DELETE /projects/:id/repository/branches/:branch
```
@@ -206,7 +209,7 @@ Will delete all branches that are merged into the project's default branch.
NOTE: **Note:**
[Protected branches](../user/project/protected_branches.md) will not be deleted as part of this operation.
-```text
+```plaintext
DELETE /projects/:id/repository/merged_branches
```
diff --git a/doc/api/broadcast_messages.md b/doc/api/broadcast_messages.md
index 1ff40103750..db2198dc162 100644
--- a/doc/api/broadcast_messages.md
+++ b/doc/api/broadcast_messages.md
@@ -13,7 +13,7 @@ As of GitLab 12.8, GET requests do not require authentication. All other broadca
List all broadcast messages.
-```text
+```plaintext
GET /broadcast_messages
```
@@ -46,7 +46,7 @@ Example response:
Get a specific broadcast message.
-```text
+```plaintext
GET /broadcast_messages/:id
```
@@ -83,7 +83,7 @@ Example response:
Create a new broadcast message.
-```text
+```plaintext
POST /broadcast_messages
```
@@ -127,7 +127,7 @@ Example response:
Update an existing broadcast message.
-```text
+```plaintext
PUT /broadcast_messages/:id
```
diff --git a/doc/api/commits.md b/doc/api/commits.md
index 356f090f0ff..98a8e4ea2ce 100644
--- a/doc/api/commits.md
+++ b/doc/api/commits.md
@@ -65,7 +65,7 @@ Example response:
## Create a commit with multiple files and actions
-> [Introduced][ce-6096] in GitLab 8.13.
+> [Introduced](https://gitlab.com/gitlab-org/gitlab-foss/-/merge_requests/6096) in GitLab 8.13.
Create a commit by posting a JSON payload
@@ -94,7 +94,7 @@ POST /projects/:id/repository/commits
| `previous_path` | string | no | Original full path to the file being moved. Ex. `lib/class1.rb`. Only considered for `move` action. |
| `content` | string | no | File content, required for all except `delete`, `chmod`, and `move`. Move actions that do not specify `content` will preserve the existing file content, and any other value of `content` will overwrite the file content. |
| `encoding` | string | no | `text` or `base64`. `text` is default. |
-| `last_commit_id` | string | no | Last known file commit id. Will be only considered in update, move and delete actions. |
+| `last_commit_id` | string | no | Last known file commit ID. Will be only considered in update, move, and delete actions. |
| `execute_filemode` | boolean | no | When `true/false` enables/disables the execute flag on the file. Only considered for `chmod` action. |
```shell
@@ -245,7 +245,7 @@ Example response:
## Get references a commit is pushed to
-> [Introduced][ce-15026] in GitLab 10.6
+> [Introduced](https://gitlab.com/gitlab-org/gitlab-foss/-/merge_requests/15026) in GitLab 10.6
Get all references (from branches or tags) a commit is pushed to.
The pagination parameters `page` and `per_page` can be used to restrict the list of references.
@@ -280,7 +280,7 @@ Example response:
## Cherry pick a commit
-> [Introduced][ce-8047] in GitLab 8.15.
+> [Introduced](https://gitlab.com/gitlab-org/gitlab-foss/-/merge_requests/8047) in GitLab 8.15.
Cherry picks a commit to a given branch.
@@ -340,7 +340,7 @@ conflict.
## Revert a commit
-> [Introduced][ce-22919] in GitLab 11.5.
+> [Introduced](https://gitlab.com/gitlab-org/gitlab-foss/-/merge_requests/22919) in GitLab 11.5.
Reverts a commit in a given branch.
@@ -521,6 +521,62 @@ Example response:
}
```
+## Get the discussions of a commit
+
+Get the discussions of a commit in a project.
+
+```plaintext
+GET /projects/:id/repository/commits/:sha/discussions
+```
+
+Parameters:
+
+| Attribute | Type | Required | Description |
+| --------- | ---- | -------- | ----------- |
+| `id` | integer/string | yes | The ID or [URL-encoded path of the project](README.md#namespaced-path-encoding) owned by the authenticated user
+| `sha` | string | yes | The commit hash or name of a repository branch or tag |
+
+```shell
+curl --header "PRIVATE-TOKEN: <your_access_token>" "https://gitlab.example.com/api/v4/projects/5/repository/commits/4604744a1c64de00ff62e1e8a6766919923d2b41/discussions"
+```
+
+Example response:
+
+```json
+[
+ {
+ "id": "4604744a1c64de00ff62e1e8a6766919923d2b41",
+ "individual_note": true,
+ "notes": [
+ {
+ "id": 334686748,
+ "type": null,
+ "body": "I'm the Dude, so that's what you call me.",
+ "attachment": null,
+ "author" : {
+ "id" : 28,
+ "name" : "Jeff Lebowski",
+ "username" : "thedude",
+ "web_url" : "https://gitlab.example.com/thedude",
+ "state" : "active",
+ "avatar_url" : "https://gitlab.example.com/uploads/user/avatar/28/The-Big-Lebowski-400-400.png"
+ },
+ "created_at": "2020-04-30T18:48:11.432Z",
+ "updated_at": "2020-04-30T18:48:11.432Z",
+ "system": false,
+ "noteable_id": null,
+ "noteable_type": "Commit",
+ "resolvable": false,
+ "confidential": null,
+ "noteable_iid": null,
+ "commands_changes": {}
+ }
+ ]
+ }
+]
+
+```
+
## Commit status
Since GitLab 8.1, this is the new commit status API.
@@ -653,7 +709,7 @@ Example response:
## List Merge Requests associated with a commit
-> [Introduced][ce-18004] in GitLab 10.7.
+> [Introduced](https://gitlab.com/gitlab-org/gitlab-foss/-/merge_requests/18004) in GitLab 10.7.
Get a list of Merge Requests related to the specified commit.
@@ -755,7 +811,7 @@ Example response if commit is GPG signed:
}
```
-Example response if commit is x509 signed:
+Example response if commit is X.509 signed:
```json
{
@@ -785,9 +841,3 @@ Example response if commit is unsigned:
"message": "404 GPG Signature Not Found"
}
```
-
-[ce-6096]: https://gitlab.com/gitlab-org/gitlab-foss/-/merge_requests/6096 "Multi-file commit"
-[ce-8047]: https://gitlab.com/gitlab-org/gitlab-foss/-/merge_requests/8047
-[ce-15026]: https://gitlab.com/gitlab-org/gitlab-foss/-/merge_requests/15026
-[ce-18004]: https://gitlab.com/gitlab-org/gitlab-foss/-/merge_requests/18004
-[ce-22919]: https://gitlab.com/gitlab-org/gitlab-foss/-/merge_requests/22919
diff --git a/doc/api/container_registry.md b/doc/api/container_registry.md
index a41a71808ce..9ec4373c92c 100644
--- a/doc/api/container_registry.md
+++ b/doc/api/container_registry.md
@@ -18,6 +18,7 @@ GET /projects/:id/registry/repositories
| --------- | ---- | -------- | ----------- |
| `id` | integer/string | yes | The ID or [URL-encoded path of the project](README.md#namespaced-path-encoding) accessible by the authenticated user. |
| `tags` | boolean | no | If the parameter is included as true, each repository will include an array of `"tags"` in the response. |
+| `name` | string | no | Returns a list of repositories with a name that matches the value. ([Introduced](https://gitlab.com/gitlab-org/gitlab/-/merge_requests/29763) in GitLab 13.0). |
```shell
curl --header "PRIVATE-TOKEN: <your_access_token>" "https://gitlab.example.com/api/v4/projects/5/registry/repositories"
@@ -58,6 +59,7 @@ GET /groups/:id/registry/repositories
| --------- | ---- | -------- | ----------- |
| `id` | integer/string | yes | The ID or [URL-encoded path of the group](README.md#namespaced-path-encoding) accessible by the authenticated user. |
| `tags` | boolean | no | If the parameter is included as true, each repository will include an array of `"tags"` in the response. |
+| `name` | string | no | Returns a list of repositories with a name that matches the value. ([Introduced](https://gitlab.com/gitlab-org/gitlab/-/merge_requests/29763) in GitLab 13.0). |
```shell
curl --header "PRIVATE-TOKEN: <your_access_token>" "https://gitlab.example.com/api/v4/groups/2/registry/repositories?tags=1"
@@ -223,6 +225,9 @@ This action does not delete blobs. In order to delete them and recycle disk spac
Delete registry repository tags in bulk based on given criteria.
+<i class="fa fa-youtube-play youtube" aria-hidden="true"></i>
+For an overview, see [Utilize the Container Registry API to delete all tags except *](https://youtu.be/Hi19bKe_xsg).
+
```plaintext
DELETE /projects/:id/registry/repositories/:repository_id/tags
```
diff --git a/doc/api/deploy_keys.md b/doc/api/deploy_keys.md
index f6d00988c56..a7acc0c2b55 100644
--- a/doc/api/deploy_keys.md
+++ b/doc/api/deploy_keys.md
@@ -2,7 +2,7 @@
## List all deploy keys
-Get a list of all deploy keys across all projects of the GitLab instance. This endpoint requires admin access.
+Get a list of all deploy keys across all projects of the GitLab instance. This endpoint requires admin access and is not available on GitLab.com.
```plaintext
GET /deploy_keys
diff --git a/doc/api/deploy_tokens.md b/doc/api/deploy_tokens.md
index 461957847df..6e732a43da0 100644
--- a/doc/api/deploy_tokens.md
+++ b/doc/api/deploy_tokens.md
@@ -92,7 +92,7 @@ POST /projects/:id/deploy_tokens
| `name` | string | yes | New deploy token's name |
| `expires_at` | datetime | no | Expiration date for the deploy token. Does not expire if no value is provided. |
| `username` | string | no | Username for deploy token. Default is `gitlab+deploy-token-{n}` |
-| `scopes` | array of strings | yes | Indicates the deploy token scopes. Must be at least one of `read_repository`, `read_registry`, or `write_registry`. |
+| `scopes` | array of strings | yes | Indicates the deploy token scopes. Must be at least one of `read_repository`, `read_registry`, `write_registry`, `read_package_registry`, or `write_package_registry`. |
```shell
curl --request POST --header "PRIVATE-TOKEN: <your_access_token>" --header "Content-Type: application/json" --data '{"name": "My deploy token", "expires_at": "2021-01-01", "username": "custom-user", "scopes": ["read_repository"]}' "https://gitlab.example.com/api/v4/projects/5/deploy_tokens/"
@@ -193,7 +193,7 @@ POST /groups/:id/deploy_tokens
| `name` | string | yes | New deploy token's name |
| `expires_at` | datetime | no | Expiration date for the deploy token. Does not expire if no value is provided. |
| `username` | string | no | Username for deploy token. Default is `gitlab+deploy-token-{n}` |
-| `scopes` | array of strings | yes | Indicates the deploy token scopes. Must be at least one of `read_repository`, `read_registry`, or `write_registry`. |
+| `scopes` | array of strings | yes | Indicates the deploy token scopes. Must be at least one of `read_repository`, `read_registry`, `write_registry`, `read_package_registry`, or `write_package_registry`. |
Example request:
diff --git a/doc/api/epics.md b/doc/api/epics.md
index bf6a18fcedc..6ca6f04b741 100644
--- a/doc/api/epics.md
+++ b/doc/api/epics.md
@@ -1,5 +1,8 @@
# Epics API **(PREMIUM)**
+> - Introduced in [GitLab Ultimate](https://about.gitlab.com/pricing/) 10.2.
+> - Single-level Epics [were moved](https://gitlab.com/gitlab-org/gitlab/issues/37081) to [GitLab Premium](https://about.gitlab.com/pricing/) in 12.8.
+
Every API call to epic must be authenticated.
If a user is not a member of a group and the group is private, a `GET` request on that group will result to a `404` status code.
@@ -64,6 +67,7 @@ GET /groups/:id/epics?state=opened
| `updated_before` | datetime | no | Return epics updated on or before the given time |
| `include_ancestor_groups` | boolean | no | Include epics from the requested group's ancestors. Default is `false` |
| `include_descendant_groups` | boolean | no | Include epics from the requested group's descendants. Default is `true` |
+| `my_reaction_emoji` | string | no | Return epics reacted by the authenticated user by the given emoji. `None` returns epics not given a reaction. `Any` returns epics given at least one reaction. Introduced in [GitLab 13.0](https://gitlab.com/gitlab-org/gitlab/-/merge_requests/31479)|
```shell
curl --header "PRIVATE-TOKEN: <your_access_token>" https://gitlab.example.com/api/v4/groups/1/epics
@@ -246,7 +250,7 @@ POST /groups/:id/epics
| `start_date_fixed` | string | no | The fixed start date of an epic (since 11.3) |
| `due_date_is_fixed` | boolean | no | Whether due date should be sourced from `due_date_fixed` or from milestones (since 11.3) |
| `due_date_fixed` | string | no | The fixed due date of an epic (since 11.3) |
-| `parent_id` | integer/string | no | The id of a parent epic (since 11.11) |
+| `parent_id` | integer/string | no | The ID of a parent epic (since 11.11) |
```shell
curl --request POST --header "PRIVATE-TOKEN: <your_access_token>" https://gitlab.example.com/api/v4/groups/1/epics?title=Epic&description=Epic%20description
diff --git a/doc/api/events.md b/doc/api/events.md
index 431e96b2804..e6cb56f1339 100644
--- a/doc/api/events.md
+++ b/doc/api/events.md
@@ -62,10 +62,10 @@ Parameters:
| Attribute | Type | Required | Description |
| --------- | ---- | -------- | ----------- |
-| `action` | string | no | Include only events of a particular [action type][action-types] |
-| `target_type` | string | no | Include only events of a particular [target type][target-types] |
-| `before` | date | no | Include only events created before a particular date. Please see [here for the supported format][date-formatting] |
-| `after` | date | no | Include only events created after a particular date. Please see [here for the supported format][date-formatting] |
+| `action` | string | no | Include only events of a particular [action type](#action-types) |
+| `target_type` | string | no | Include only events of a particular [target type](#target-types) |
+| `before` | date | no | Include only events created before a particular date. Please see [here for the supported format](#date-formatting) |
+| `after` | date | no | Include only events created after a particular date. Please see [here for the supported format](#date-formatting) |
| `scope` | string | no | Include all events across a user's projects. |
| `sort` | string | no | Sort events in `asc` or `desc` order by `created_at`. Default is `desc` |
@@ -123,7 +123,7 @@ Example response:
### Get user contribution events
>**Notes:**
-> Documentation was formerly located in the [Users API pages][users-api].
+> Documentation was formerly located in the [Users API pages](users.md).
> `read_user` access was introduced in GitLab 11.3.
Get the contribution events for the specified user, sorted from newest to oldest. Scope `read_user` or `api` is required.
@@ -137,10 +137,10 @@ Parameters:
| Attribute | Type | Required | Description |
| --------- | ---- | -------- | ----------- |
| `id` | integer | yes | The ID or Username of the user |
-| `action` | string | no | Include only events of a particular [action type][action-types] |
-| `target_type` | string | no | Include only events of a particular [target type][target-types] |
-| `before` | date | no | Include only events created before a particular date. Please see [here for the supported format][date-formatting] |
-| `after` | date | no | Include only events created after a particular date. Please see [here for the supported format][date-formatting] |
+| `action` | string | no | Include only events of a particular [action type](#action-types) |
+| `target_type` | string | no | Include only events of a particular [target type](#target-types) |
+| `before` | date | no | Include only events created before a particular date. Please see [here for the supported format](#date-formatting) |
+| `after` | date | no | Include only events created after a particular date. Please see [here for the supported format](#date-formatting) |
| `sort` | string | no | Sort events in `asc` or `desc` order by `created_at`. Default is `desc` |
```shell
@@ -255,7 +255,7 @@ Example response:
## List a Project's visible events
->**Note:** This endpoint has been around longer than the others. Documentation was formerly located in the [Projects API pages][projects-api].
+>**Note:** This endpoint has been around longer than the others. Documentation was formerly located in the [Projects API pages](projects.md).
Get a list of visible events for a particular project.
@@ -268,10 +268,10 @@ Parameters:
| Attribute | Type | Required | Description |
| --------- | ---- | -------- | ----------- |
| `project_id` | integer/string | yes | The ID or [URL-encoded path of the project](README.md#namespaced-path-encoding) |
-| `action` | string | no | Include only events of a particular [action type][action-types] |
-| `target_type` | string | no | Include only events of a particular [target type][target-types] |
-| `before` | date | no | Include only events created before a particular date. Please see [here for the supported format][date-formatting] |
-| `after` | date | no | Include only events created after a particular date. Please see [here for the supported format][date-formatting] |
+| `action` | string | no | Include only events of a particular [action type](#action-types) |
+| `target_type` | string | no | Include only events of a particular [target type](#target-types) |
+| `before` | date | no | Include only events created before a particular date. Please see [here for the supported format](#date-formatting) |
+| `after` | date | no | Include only events created after a particular date. Please see [here for the supported format](#date-formatting) |
| `sort` | string | no | Sort events in `asc` or `desc` order by `created_at`. Default is `desc` |
Example request:
@@ -365,9 +365,3 @@ Example response:
}
]
```
-
-[target-types]: #target-types "Target Type parameter"
-[action-types]: #action-types "Action Type parameter"
-[date-formatting]: #date-formatting "Date Formatting guidance"
-[projects-api]: projects.md "Projects API pages"
-[users-api]: users.md "Users API pages"
diff --git a/doc/api/feature_flag_specs.md b/doc/api/feature_flag_specs.md
index 442d2c2c2d7..fbe99826b27 100644
--- a/doc/api/feature_flag_specs.md
+++ b/doc/api/feature_flag_specs.md
@@ -10,7 +10,7 @@ Users with Developer or higher [permissions](../user/permissions.md) can access
## List all effective feature flag specs under the specified environment
-Get all effective feature flag specs under the specified [environment](../ci/environments.md).
+Get all effective feature flag specs under the specified [environment](../ci/environments/index.md).
For instance, there are two specs, `staging` and `production`, for a feature flag.
When you pass `production` as a parameter to this endpoint, the system returns
@@ -23,7 +23,7 @@ GET /projects/:id/feature_flag_scopes
| Attribute | Type | Required | Description |
| ------------------- | ---------------- | ---------- | --------------------------------------------------------------------------------------------------------------------------- |
| `id` | integer/string | yes | The ID or [URL-encoded path of the project](README.md#namespaced-path-encoding). |
-| `environment` | string | yes | The [environment](../ci/environments.md) name |
+| `environment` | string | yes | The [environment](../ci/environments/index.md) name |
```shell
curl --header "PRIVATE-TOKEN: <your_access_token>" https://gitlab.example.com/api/v4/projects/1/feature_flag_scopes?environment=production
@@ -155,7 +155,7 @@ POST /projects/:id/feature_flags/:name/scopes
| ------------------- | ---------------- | ---------- | --------------------------------------------------------------------------------------------------------------------------- |
| `id` | integer/string | yes | The ID or [URL-encoded path of the project](README.md#namespaced-path-encoding). |
| `name` | string | yes | The name of the feature flag. |
-| `environment_scope` | string | yes | The [environment spec](../ci/environments.md#scoping-environments-with-specs) of the feature flag. |
+| `environment_scope` | string | yes | The [environment spec](../ci/environments/index.md#scoping-environments-with-specs) of the feature flag. |
| `active` | boolean | yes | Whether the spec is active. |
| `strategies` | json | yes | The [strategies](../user/project/operations/feature_flags.md#feature-flag-strategies) of the feature flag spec. |
@@ -202,7 +202,7 @@ GET /projects/:id/feature_flags/:name/scopes/:environment_scope
| ------------------- | ---------------- | ---------- | ---------------------------------------------------------------------------------------|
| `id` | integer/string | yes | The ID or [URL-encoded path of the project](README.md#namespaced-path-encoding). |
| `name` | string | yes | The name of the feature flag. |
-| `environment_scope` | string | yes | The URL-encoded [environment spec](../ci/environments.md#scoping-environments-with-specs) of the feature flag. |
+| `environment_scope` | string | yes | The URL-encoded [environment spec](../ci/environments/index.md#scoping-environments-with-specs) of the feature flag. |
```shell
curl --header "PRIVATE-TOKEN: <your_access_token>" https://gitlab.example.com/api/v4/projects/:id/feature_flags/new_live_trace/scopes/production
@@ -238,7 +238,7 @@ PUT /projects/:id/feature_flags/:name/scopes/:environment_scope
| ------------------- | ---------------- | ---------- | --------------------------------------------------------------------------------------------------------------------------- |
| `id` | integer/string | yes | The ID or [URL-encoded path of the project](README.md#namespaced-path-encoding). |
| `name` | string | yes | The name of the feature flag. |
-| `environment_scope` | string | yes | The URL-encoded [environment spec](../ci/environments.md#scoping-environments-with-specs) of the feature flag. |
+| `environment_scope` | string | yes | The URL-encoded [environment spec](../ci/environments/index.md#scoping-environments-with-specs) of the feature flag. |
| `active` | boolean | yes | Whether the spec is active. |
| `strategies` | json | yes | The [strategies](../user/project/operations/feature_flags.md#feature-flag-strategies) of the feature flag spec. |
@@ -284,7 +284,7 @@ DELETE /projects/:id/feature_flags/:name/scopes/:environment_scope
| ------------------- | ---------------- | ---------- | ---------------------------------------------------------------------------------------|
| `id` | integer/string | yes | The ID or [URL-encoded path of the project](README.md#namespaced-path-encoding). |
| `name` | string | yes | The name of the feature flag. |
-| `environment_scope` | string | yes | The URL-encoded [environment spec](../ci/environments.md#scoping-environments-with-specs) of the feature flag. |
+| `environment_scope` | string | yes | The URL-encoded [environment spec](../ci/environments/index.md#scoping-environments-with-specs) of the feature flag. |
```shell
curl --header "PRIVATE-TOKEN: <your_access_token>" --request DELETE https://gitlab.example.com/api/v4/projects/1/feature_flags/new_live_trace/scopes/production
diff --git a/doc/api/feature_flag_user_lists.md b/doc/api/feature_flag_user_lists.md
new file mode 100644
index 00000000000..04cad8f2d1d
--- /dev/null
+++ b/doc/api/feature_flag_user_lists.md
@@ -0,0 +1,181 @@
+# Feature flag user lists API **(PREMIUM)**
+
+> [Introduced](https://gitlab.com/gitlab-org/gitlab/-/issues/205409) in [GitLab Premium](https://about.gitlab.com/pricing/) 12.10.
+
+API for accessing GitLab Feature Flag User Lists.
+
+Users with Developer or higher [permissions](../user/permissions.md) can access the Feature Flag User Lists API.
+
+NOTE: **Note:**
+`GET` requests return twenty results at a time because the API results
+are [paginated](README.md#pagination). You can change this value.
+
+## List all feature flag user lists for a project
+
+Gets all feature flag user lists for the requested project.
+
+```plaintext
+GET /projects/:id/feature_flags_user_lists
+```
+
+| Attribute | Type | Required | Description |
+| ------------------- | ---------------- | ---------- | --------------------------------------------------------------------------------------------------------------------------- |
+| `id` | integer/string | yes | The ID or [URL-encoded path of the project](README.md#namespaced-path-encoding). |
+
+```shell
+curl --header "PRIVATE-TOKEN: <your_access_token>" https://gitlab.example.com/api/v4/projects/1/feature_flags_user_lists
+```
+
+Example response:
+
+```json
+[
+ {
+ "name": "user_list",
+ "user_xids": "user1,user2",
+ "id": 1,
+ "iid": 1,
+ "project_id": 1,
+ "created_at": "2020-02-04T08:13:51.423Z",
+ "updated_at": "2020-02-04T08:13:51.423Z"
+ },
+ {
+ "name": "test_users",
+ "user_xids": "user3,user4,user5",
+ "id": 2,
+ "iid": 2,
+ "project_id": 1,
+ "created_at": "2020-02-04T08:13:10.507Z",
+ "updated_at": "2020-02-04T08:13:10.507Z"
+ }
+]
+```
+
+## Create a feature flag user list
+
+Creates a feature flag user list.
+
+```plaintext
+POST /projects/:id/feature_flags_user_lists
+```
+
+| Attribute | Type | Required | Description |
+| ------------------- | ---------------- | ---------- | ---------------------------------------------------------------------------------------|
+| `id` | integer/string | yes | The ID or [URL-encoded path of the project](README.md#namespaced-path-encoding). |
+| `name` | string | yes | The name of the feature flag. |
+| `user_xids` | string | yes | A comma separated list of user IDs. |
+
+```shell
+curl https://gitlab.example.com/api/v4/projects/1/feature_flags_user_lists \
+ --header "PRIVATE-TOKEN: <your_access_token>" \
+ --header "Content-type: application/json" \
+ --data @- << EOF
+{
+ "name": "my_user_list",
+ "user_xids": "user1,user2,user3"
+}
+EOF
+```
+
+Example response:
+
+```json
+{
+ "name": "my_user_list",
+ "user_xids": "user1,user2,user3",
+ "id": 1,
+ "iid": 1,
+ "project_id": 1,
+ "created_at": "2020-02-04T08:32:27.288Z",
+ "updated_at": "2020-02-04T08:32:27.288Z"
+}
+```
+
+## Get a feature flag user list
+
+Gets a feature flag user list.
+
+```plaintext
+GET /projects/:id/feature_flags_user_lists/:iid
+```
+
+| Attribute | Type | Required | Description |
+| ------------------- | ---------------- | ---------- | ---------------------------------------------------------------------------------------|
+| `id` | integer/string | yes | The ID or [URL-encoded path of the project](README.md#namespaced-path-encoding). |
+| `iid` | integer/string | yes | The internal ID of the project's feature flag user list. |
+
+```shell
+curl --header "PRIVATE-TOKEN: <your_access_token>" https://gitlab.example.com/api/v4/projects/1/feature_flags_user_lists/1
+```
+
+Example response:
+
+```json
+{
+ "name": "my_user_list",
+ "user_xids": "123,456",
+ "id": 1,
+ "iid": 1,
+ "project_id": 1,
+ "created_at": "2020-02-04T08:13:10.507Z",
+ "updated_at": "2020-02-04T08:13:10.507Z",
+}
+```
+
+## Update a feature flag user list
+
+Updates a feature flag user list.
+
+```plaintext
+PUT /projects/:id/feature_flags_user_lists/:iid
+```
+
+| Attribute | Type | Required | Description |
+| ------------------- | ---------------- | ---------- | ---------------------------------------------------------------------------------------|
+| `id` | integer/string | yes | The ID or [URL-encoded path of the project](README.md#namespaced-path-encoding). |
+| `iid` | integer/string | yes | The internal ID of the project's feature flag user list. |
+| `name` | string | no | The name of the feature flag. |
+| `user_xids` | string | no | A comma separated list of user IDs. |
+
+```shell
+curl https://gitlab.example.com/api/v4/projects/1/feature_flags_user_lists/1 \
+ --header "PRIVATE-TOKEN: <your_access_token>" \
+ --header "Content-type: application/json" \
+ --request PUT \
+ --data @- << EOF
+{
+ "user_xids": "user2,user3,user4"
+}
+EOF
+```
+
+Example response:
+
+```json
+{
+ "name": "my_user_list",
+ "user_xids": "user2,user3,user4",
+ "id": 1,
+ "iid": 1,
+ "project_id": 1,
+ "created_at": "2020-02-04T08:32:27.288Z",
+ "updated_at": "2020-02-05T09:33:17.179Z"
+}
+```
+
+## Delete feature flag user list
+
+Deletes a feature flag user list.
+
+```plaintext
+DELETE /projects/:id/feature_flags_user_lists/:iid
+```
+
+| Attribute | Type | Required | Description |
+| ------------------- | ---------------- | ---------- | ---------------------------------------------------------------------------------------|
+| `id` | integer/string | yes | The ID or [URL-encoded path of the project](README.md#namespaced-path-encoding). |
+| `iid` | integer/string | yes | The internal ID of the project's feature flag user list |
+
+```shell
+curl --header "PRIVATE-TOKEN: <your_access_token>" --request DELETE https://gitlab.example.com/api/v4/projects/1/feature_flags_user_lists/1
+```
diff --git a/doc/api/feature_flags.md b/doc/api/feature_flags.md
index f95eb31c84c..3e1b0e05298 100644
--- a/doc/api/feature_flags.md
+++ b/doc/api/feature_flags.md
@@ -155,7 +155,7 @@ POST /projects/:id/feature_flags
| `name` | string | yes | The name of the feature flag. |
| `description` | string | no | The description of the feature flag. |
| `scopes` | JSON | no | The [feature flag specs](../user/project/operations/feature_flags.md#define-environment-specs) of the feature flag. |
-| `scopes:environment_scope` | string | no | The [environment spec](../ci/environments.md#scoping-environments-with-specs). |
+| `scopes:environment_scope` | string | no | The [environment spec](../ci/environments/index.md#scoping-environments-with-specs). |
| `scopes:active` | boolean | no | Whether the spec is active. |
| `scopes:strategies` | JSON | no | The [strategies](../user/project/operations/feature_flags.md#feature-flag-strategies) of the feature flag spec. |
diff --git a/doc/api/features.md b/doc/api/features.md
index a43f2daa93f..03f1663b987 100644
--- a/doc/api/features.md
+++ b/doc/api/features.md
@@ -1,5 +1,7 @@
# Features flags API
+This API is for managing Flipper-based [feature flags used in development of GitLab](../development/feature_flags/index.md).
+
All methods require administrator authorization.
Notice that currently the API only supports boolean and percentage-of-time gate
@@ -32,6 +34,16 @@ Example response:
]
},
{
+ "name": "my_user_feature",
+ "state": "on",
+ "gates": [
+ {
+ "key": "percentage_of_actors",
+ "value": 34
+ }
+ ]
+ },
+ {
"name": "new_library",
"state": "on",
"gates": [
@@ -58,6 +70,7 @@ POST /features/:name
| --------- | ---- | -------- | ----------- |
| `name` | string | yes | Name of the feature to create or update |
| `value` | integer/string | yes | `true` or `false` to enable/disable, or an integer for percentage of time |
+| `key` | string | no | `percentage_of_actors` or `percentage_of_time` (default) |
| `feature_group` | string | no | A Feature group name |
| `user` | string | no | A GitLab username |
| `group` | string | no | A GitLab group's path, for example `gitlab-org` |
@@ -89,6 +102,37 @@ Example response:
}
```
+### Set percentage of actors rollout
+
+Rollout to percentage of actors.
+
+```plaintext
+POST https://gitlab.example.com/api/v4/features/my_user_feature?private_token=<your_access_token>
+Content-Type: application/x-www-form-urlencoded
+value=42&key=percentage_of_actors&
+```
+
+Example response:
+
+```json
+{
+ "name": "my_user_feature",
+ "state": "conditional",
+ "gates": [
+ {
+ "key": "boolean",
+ "value": false
+ },
+ {
+ "key": "percentage_of_actors",
+ "value": 42
+ }
+ ]
+}
+```
+
+Rolls out the `my_user_feature` to `42%` of actors.
+
## Delete a feature
Removes a feature gate. Response is equal when the gate exists, or doesn't.
diff --git a/doc/api/freeze_periods.md b/doc/api/freeze_periods.md
new file mode 100644
index 00000000000..ee5e657c945
--- /dev/null
+++ b/doc/api/freeze_periods.md
@@ -0,0 +1,168 @@
+# Freeze Periods API
+
+> [Introduced](https://gitlab.com/gitlab-org/gitlab/-/merge_requests/29382) in GitLab 13.0.
+
+You can use the Freeze Periods API to manipulate GitLab's [Freeze Period](../user/project/releases/index.md#set-a-deploy-freeze) entries.
+
+## Permissions and security
+
+Only users with Maintainer [permissions](../user/permissions.md) can
+interact with the Freeze Period API endpoints.
+
+## List Freeze Periods
+
+Paginated list of Freeze Periods, sorted by `created_at` in ascending order.
+
+```plaintext
+GET /projects/:id/freeze_periods
+```
+
+| Attribute | Type | Required | Description |
+| ------------- | -------------- | -------- | ----------------------------------------------------------------------------------- |
+| `id` | integer/string | yes | The ID or [URL-encoded path of the project](README.md#namespaced-path-encoding). |
+
+Example request:
+
+```shell
+curl --header "PRIVATE-TOKEN: gVWYVHDRzXiRpN1rUC8T" "https://gitlab.example.com/api/v4/projects/19/freeze_periods"
+```
+
+Example response:
+
+```json
+[
+ {
+ "id":1,
+ "freeze_start":"0 23 * * 5",
+ "freeze_end":"0 8 * * 1",
+ "cron_timezone":"UTC",
+ "created_at":"2020-05-15T17:03:35.702Z",
+ "updated_at":"2020-05-15T17:06:41.566Z"
+ }
+]
+```
+
+## Get a Freeze Period by a `freeze_period_id`
+
+Get a Freeze Period for the given `freeze_period_id`.
+
+```plaintext
+GET /projects/:id/freeze_periods/:freeze_period_id
+```
+
+| Attribute | Type | Required | Description |
+| ------------- | -------------- | -------- | ----------------------------------------------------------------------------------- |
+| `id` | integer or string | yes | The ID or [URL-encoded path of the project](README.md#namespaced-path-encoding). |
+| `freeze_period_id` | string | yes | The database ID of the Freeze Period. |
+
+Example request:
+
+```shell
+curl --header "PRIVATE-TOKEN: gVWYVHDRzXiRpN1rUC8T" "https://gitlab.example.com/api/v4/projects/19/freeze_periods/1"
+```
+
+Example response:
+
+```json
+{
+ "id":1,
+ "freeze_start":"0 23 * * 5",
+ "freeze_end":"0 8 * * 1",
+ "cron_timezone":"UTC",
+ "created_at":"2020-05-15T17:03:35.702Z",
+ "updated_at":"2020-05-15T17:06:41.566Z"
+}
+```
+
+## Create a Freeze Period
+
+Create a Freeze Period.
+
+```plaintext
+POST /projects/:id/freeze_periods
+```
+
+| Attribute | Type | Required | Description |
+| -------------------| --------------- | -------- | -------------------------------------------------------------------------------------------------------------------------------- |
+| `id` | integer or string | yes | The ID or [URL-encoded path of the project](README.md#namespaced-path-encoding). |
+| `freeze_start` | string | yes | Start of the Freeze Period in [cron](https://crontab.guru/) format. |
+| `freeze_end` | string | yes | End of the Freeze Period in [cron](https://crontab.guru/) format. |
+| `cron_timezone` | string | no | The timezone for the cron fields, defaults to UTC if not provided. |
+
+Example request:
+
+```shell
+curl --header 'Content-Type: application/json' --header "PRIVATE-TOKEN: gVWYVHDRzXiRpN1rUC8T" \
+ --data '{ "freeze_start": "0 23 * * 5", "freeze_end": "0 7 * * 1", "cron_timezone": "UTC" }' \
+ --request POST https://gitlab.example.com/api/v4/projects/19/freeze_periods
+```
+
+Example response:
+
+```json
+{
+ "id":1,
+ "freeze_start":"0 23 * * 5",
+ "freeze_end":"0 7 * * 1",
+ "cron_timezone":"UTC",
+ "created_at":"2020-05-15T17:03:35.702Z",
+ "updated_at":"2020-05-15T17:03:35.702Z"
+}
+```
+
+## Update a Freeze Period
+
+Update a Freeze Period for the given `freeze_period_id`.
+
+```plaintext
+PUT /projects/:id/freeze_periods/:tag_name
+```
+
+| Attribute | Type | Required | Description |
+| ------------- | --------------- | -------- | ----------------------------------------------------------------------------------------------------------- |
+| `id` | integer or string | yes | The ID or [URL-encoded path of the project](README.md#namespaced-path-encoding). |
+| `freeze_period_id` | integer or string | yes | The database ID of the Freeze Period. |
+| `freeze_start` | string | no | Start of the Freeze Period in [cron](https://crontab.guru/) format. |
+| `freeze_end` | string | no | End of the Freeze Period in [cron](https://crontab.guru/) format. |
+| `cron_timezone` | string | no | The timezone for the cron fields. |
+
+Example request:
+
+```shell
+curl --header 'Content-Type: application/json' --header "PRIVATE-TOKEN: gVWYVHDRzXiRpN1rUC8T" \
+ --data '{ "freeze_end": "0 8 * * 1" }' \
+ --request PUT https://gitlab.example.com/api/v4/projects/19/freeze_periods/1
+```
+
+Example response:
+
+```json
+{
+ "id":1,
+ "freeze_start":"0 23 * * 5",
+ "freeze_end":"0 8 * * 1",
+ "cron_timezone":"UTC",
+ "created_at":"2020-05-15T17:03:35.702Z",
+ "updated_at":"2020-05-15T17:06:41.566Z"
+}
+```
+
+## Delete a Freeze Period
+
+Delete a Freeze Period for the given `freeze_period_id`.
+
+```plaintext
+DELETE /projects/:id/freeze_periods/:freeze_period_id
+```
+
+| Attribute | Type | Required | Description |
+| ------------- | -------------- | -------- | ----------------------------------------------------------------------------------- |
+| `id` | integer or string | yes | The ID or [URL-encoded path of the project](README.md#namespaced-path-encoding). |
+| `freeze_period_id` | string | yes | The database ID of the Freeze Period. |
+
+Example request:
+
+```shell
+curl --request DELETE --header "PRIVATE-TOKEN: gVWYVHDRzXiRpN1rUC8T" "https://gitlab.example.com/api/v4/projects/19/freeze_periods/1"
+
+```
diff --git a/doc/api/geo_nodes.md b/doc/api/geo_nodes.md
index baaa2e2f09f..820f78853d0 100644
--- a/doc/api/geo_nodes.md
+++ b/doc/api/geo_nodes.md
@@ -364,6 +364,9 @@ Example response:
"last_successful_status_check_timestamp": 1510125024,
"version": "10.3.0",
"revision": "33d33a096a",
+ "package_files_count": 10,
+ "package_files_checksummed_count": 10,
+ "package_files_checksum_failed_count": 0
},
{
"geo_node_id": 2,
@@ -431,7 +434,10 @@ Example response:
"cursor_last_event_timestamp": 1509681166,
"last_successful_status_check_timestamp": 1510125024,
"version": "10.3.0",
- "revision": "33d33a096a"
+ "revision": "33d33a096a",
+ "package_files_count": 10,
+ "package_files_checksummed_count": 10,
+ "package_files_checksum_failed_count": 0
}
]
```
diff --git a/doc/api/graphql/getting_started.md b/doc/api/graphql/getting_started.md
index 07147340ab4..901bf85ff40 100644
--- a/doc/api/graphql/getting_started.md
+++ b/doc/api/graphql/getting_started.md
@@ -2,7 +2,7 @@
This guide demonstrates basic usage of GitLab's GraphQL API.
-See the [GraphQL API StyleGuide](../../development/api_graphql_styleguide.md) for implementation details
+See the [GraphQL API style guide](../../development/api_graphql_styleguide.md) for implementation details
aimed at developers who wish to work on developing the API itself.
## Running examples
diff --git a/doc/api/graphql/reference/gitlab_schema.graphql b/doc/api/graphql/reference/gitlab_schema.graphql
index 9cdc83d584c..91f1413943c 100644
--- a/doc/api/graphql/reference/gitlab_schema.graphql
+++ b/doc/api/graphql/reference/gitlab_schema.graphql
@@ -33,12 +33,47 @@ type AddAwardEmojiPayload {
clientMutationId: String
"""
- Reasons why the mutation failed.
+ Errors encountered during execution of the mutation.
"""
errors: [String!]!
}
"""
+Autogenerated input type of AddProjectToSecurityDashboard
+"""
+input AddProjectToSecurityDashboardInput {
+ """
+ A unique identifier for the client performing the mutation.
+ """
+ clientMutationId: String
+
+ """
+ ID of the project to be added to Instance Security Dashboard
+ """
+ id: ID!
+}
+
+"""
+Autogenerated return type of AddProjectToSecurityDashboard
+"""
+type AddProjectToSecurityDashboardPayload {
+ """
+ A unique identifier for the client performing the mutation.
+ """
+ clientMutationId: String
+
+ """
+ Errors encountered during execution of the mutation.
+ """
+ errors: [String!]!
+
+ """
+ Project that was added to the Instance Security Dashboard
+ """
+ project: Project
+}
+
+"""
Autogenerated input type of AdminSidekiqQueuesDeleteJobs
"""
input AdminSidekiqQueuesDeleteJobsInput {
@@ -93,7 +128,7 @@ type AdminSidekiqQueuesDeleteJobsPayload {
clientMutationId: String
"""
- Reasons why the mutation failed.
+ Errors encountered during execution of the mutation.
"""
errors: [String!]!
@@ -104,6 +139,311 @@ type AdminSidekiqQueuesDeleteJobsPayload {
}
"""
+Describes an alert from the project's Alert Management
+"""
+type AlertManagementAlert {
+ """
+ Timestamp the alert was created
+ """
+ createdAt: Time
+
+ """
+ Description of the alert
+ """
+ description: String
+
+ """
+ Alert details
+ """
+ details: JSON
+
+ """
+ Timestamp the alert ended
+ """
+ endedAt: Time
+
+ """
+ Number of events of this alert
+ """
+ eventCount: Int
+
+ """
+ List of hosts the alert came from
+ """
+ hosts: [String!]
+
+ """
+ Internal ID of the alert
+ """
+ iid: ID!
+
+ """
+ Internal ID of the GitLab issue attached to the alert
+ """
+ issueIid: ID
+
+ """
+ Monitoring tool the alert came from
+ """
+ monitoringTool: String
+
+ """
+ Service the alert came from
+ """
+ service: String
+
+ """
+ Severity of the alert
+ """
+ severity: AlertManagementSeverity
+
+ """
+ Timestamp the alert was raised
+ """
+ startedAt: Time
+
+ """
+ Status of the alert
+ """
+ status: AlertManagementStatus
+
+ """
+ Title of the alert
+ """
+ title: String
+
+ """
+ Timestamp the alert was last updated
+ """
+ updatedAt: Time
+}
+
+"""
+The connection type for AlertManagementAlert.
+"""
+type AlertManagementAlertConnection {
+ """
+ A list of edges.
+ """
+ edges: [AlertManagementAlertEdge]
+
+ """
+ A list of nodes.
+ """
+ nodes: [AlertManagementAlert]
+
+ """
+ Information to aid in pagination.
+ """
+ pageInfo: PageInfo!
+}
+
+"""
+An edge in a connection.
+"""
+type AlertManagementAlertEdge {
+ """
+ A cursor for use in pagination.
+ """
+ cursor: String!
+
+ """
+ The item at the end of the edge.
+ """
+ node: AlertManagementAlert
+}
+
+"""
+Values for sorting alerts
+"""
+enum AlertManagementAlertSort {
+ """
+ Created time by ascending order
+ """
+ CREATED_TIME_ASC
+
+ """
+ Created time by descending order
+ """
+ CREATED_TIME_DESC
+
+ """
+ End time by ascending order
+ """
+ END_TIME_ASC
+
+ """
+ End time by descending order
+ """
+ END_TIME_DESC
+
+ """
+ Events count by ascending order
+ """
+ EVENTS_COUNT_ASC
+
+ """
+ Events count by descending order
+ """
+ EVENTS_COUNT_DESC
+
+ """
+ Severity by ascending order
+ """
+ SEVERITY_ASC
+
+ """
+ Severity by descending order
+ """
+ SEVERITY_DESC
+
+ """
+ Start time by ascending order
+ """
+ START_TIME_ASC
+
+ """
+ Start time by descending order
+ """
+ START_TIME_DESC
+
+ """
+ Status by ascending order
+ """
+ STATUS_ASC
+
+ """
+ Status by descending order
+ """
+ STATUS_DESC
+
+ """
+ Created time by ascending order
+ """
+ UPDATED_TIME_ASC
+
+ """
+ Created time by descending order
+ """
+ UPDATED_TIME_DESC
+
+ """
+ Created at ascending order
+ """
+ created_asc
+
+ """
+ Created at descending order
+ """
+ created_desc
+
+ """
+ Updated at ascending order
+ """
+ updated_asc
+
+ """
+ Updated at descending order
+ """
+ updated_desc
+}
+
+"""
+Represents total number of alerts for the represented categories
+"""
+type AlertManagementAlertStatusCountsType {
+ """
+ Number of alerts with status ACKNOWLEDGED for the project
+ """
+ acknowledged: Int
+
+ """
+ Total number of alerts for the project
+ """
+ all: Int
+
+ """
+ Number of alerts with status IGNORED for the project
+ """
+ ignored: Int
+
+ """
+ Number of alerts with status TRIGGERED or ACKNOWLEDGED for the project
+ """
+ open: Int
+
+ """
+ Number of alerts with status RESOLVED for the project
+ """
+ resolved: Int
+
+ """
+ Number of alerts with status TRIGGERED for the project
+ """
+ triggered: Int
+}
+
+"""
+Alert severity values
+"""
+enum AlertManagementSeverity {
+ """
+ Critical severity
+ """
+ CRITICAL
+
+ """
+ High severity
+ """
+ HIGH
+
+ """
+ Info severity
+ """
+ INFO
+
+ """
+ Low severity
+ """
+ LOW
+
+ """
+ Medium severity
+ """
+ MEDIUM
+
+ """
+ Unknown severity
+ """
+ UNKNOWN
+}
+
+"""
+Alert status values
+"""
+enum AlertManagementStatus {
+ """
+ Acknowledged status
+ """
+ ACKNOWLEDGED
+
+ """
+ Ignored status
+ """
+ IGNORED
+
+ """
+ Resolved status
+ """
+ RESOLVED
+
+ """
+ Triggered status
+ """
+ TRIGGERED
+}
+
+"""
An emoji awarded by a user.
"""
type AwardEmoji {
@@ -246,6 +586,31 @@ type Board {
id: ID!
"""
+ Lists of the project board
+ """
+ lists(
+ """
+ Returns the elements in the list that come after the specified cursor.
+ """
+ after: String
+
+ """
+ Returns the elements in the list that come before the specified cursor.
+ """
+ before: String
+
+ """
+ Returns the first _n_ elements from the list.
+ """
+ first: Int
+
+ """
+ Returns the last _n_ elements from the list.
+ """
+ last: Int
+ ): BoardListConnection
+
+ """
Name of the board
"""
name: String
@@ -291,6 +656,163 @@ type BoardEdge {
node: Board
}
+"""
+Represents a list for an issue board
+"""
+type BoardList {
+ """
+ Assignee in the list
+ """
+ assignee: User
+
+ """
+ Indicates if list is collapsed for this user
+ """
+ collapsed: Boolean
+
+ """
+ ID (global ID) of the list
+ """
+ id: ID!
+
+ """
+ Label of the list
+ """
+ label: Label
+
+ """
+ The current limit metric for the list
+ """
+ limitMetric: ListLimitMetric
+
+ """
+ Type of the list
+ """
+ listType: String!
+
+ """
+ Maximum number of issues in the list
+ """
+ maxIssueCount: Int
+
+ """
+ Maximum weight of issues in the list
+ """
+ maxIssueWeight: Int
+
+ """
+ Milestone of the list
+ """
+ milestone: Milestone
+
+ """
+ Position of list within the board
+ """
+ position: Int
+
+ """
+ Title of the list
+ """
+ title: String!
+}
+
+"""
+The connection type for BoardList.
+"""
+type BoardListConnection {
+ """
+ A list of edges.
+ """
+ edges: [BoardListEdge]
+
+ """
+ A list of nodes.
+ """
+ nodes: [BoardList]
+
+ """
+ Information to aid in pagination.
+ """
+ pageInfo: PageInfo!
+}
+
+"""
+An edge in a connection.
+"""
+type BoardListEdge {
+ """
+ A cursor for use in pagination.
+ """
+ cursor: String!
+
+ """
+ The item at the end of the edge.
+ """
+ node: BoardList
+}
+
+"""
+Autogenerated input type of BoardListUpdateLimitMetrics
+"""
+input BoardListUpdateLimitMetricsInput {
+ """
+ A unique identifier for the client performing the mutation.
+ """
+ clientMutationId: String
+
+ """
+ The new limit metric type for the list.
+ """
+ limitMetric: ListLimitMetric
+
+ """
+ The global ID of the list.
+ """
+ listId: ID!
+
+ """
+ The new maximum issue count limit.
+ """
+ maxIssueCount: Int
+
+ """
+ The new maximum issue weight limit.
+ """
+ maxIssueWeight: Int
+}
+
+"""
+Autogenerated return type of BoardListUpdateLimitMetrics
+"""
+type BoardListUpdateLimitMetricsPayload {
+ """
+ A unique identifier for the client performing the mutation.
+ """
+ clientMutationId: String
+
+ """
+ Errors encountered during execution of the mutation.
+ """
+ errors: [String!]!
+
+ """
+ The updated list
+ """
+ list: BoardList
+}
+
+type Branch {
+ """
+ Commit for the branch
+ """
+ commit: Commit
+
+ """
+ Name of the branch
+ """
+ name: String!
+}
+
type Commit {
"""
Author of the commit
@@ -403,12 +925,167 @@ type Commit {
title: String
"""
+ The GitLab Flavored Markdown rendering of `title`
+ """
+ titleHtml: String
+
+ """
Web URL of the commit
"""
webUrl: String!
}
"""
+Autogenerated input type of CreateAlertIssue
+"""
+input CreateAlertIssueInput {
+ """
+ A unique identifier for the client performing the mutation.
+ """
+ clientMutationId: String
+
+ """
+ The iid of the alert to mutate
+ """
+ iid: String!
+
+ """
+ The project the alert to mutate is in
+ """
+ projectPath: ID!
+}
+
+"""
+Autogenerated return type of CreateAlertIssue
+"""
+type CreateAlertIssuePayload {
+ """
+ The alert after mutation
+ """
+ alert: AlertManagementAlert
+
+ """
+ A unique identifier for the client performing the mutation.
+ """
+ clientMutationId: String
+
+ """
+ Errors encountered during execution of the mutation.
+ """
+ errors: [String!]!
+
+ """
+ The issue created after mutation
+ """
+ issue: Issue
+}
+
+"""
+Autogenerated input type of CreateAnnotation
+"""
+input CreateAnnotationInput {
+ """
+ A unique identifier for the client performing the mutation.
+ """
+ clientMutationId: String
+
+ """
+ The global id of the cluster to add an annotation to
+ """
+ clusterId: ID
+
+ """
+ The path to a file defining the dashboard on which the annotation should be added
+ """
+ dashboardPath: String!
+
+ """
+ The description of the annotation
+ """
+ description: String!
+
+ """
+ Timestamp indicating ending moment to which the annotation relates
+ """
+ endingAt: Time
+
+ """
+ The global id of the environment to add an annotation to
+ """
+ environmentId: ID
+
+ """
+ Timestamp indicating starting moment to which the annotation relates
+ """
+ startingAt: Time!
+}
+
+"""
+Autogenerated return type of CreateAnnotation
+"""
+type CreateAnnotationPayload {
+ """
+ The created annotation
+ """
+ annotation: MetricsDashboardAnnotation
+
+ """
+ A unique identifier for the client performing the mutation.
+ """
+ clientMutationId: String
+
+ """
+ Errors encountered during execution of the mutation.
+ """
+ errors: [String!]!
+}
+
+"""
+Autogenerated input type of CreateBranch
+"""
+input CreateBranchInput {
+ """
+ A unique identifier for the client performing the mutation.
+ """
+ clientMutationId: String
+
+ """
+ Name of the branch
+ """
+ name: String!
+
+ """
+ Project full path the branch is associated with
+ """
+ projectPath: ID!
+
+ """
+ Branch name or commit SHA to create branch from
+ """
+ ref: String!
+}
+
+"""
+Autogenerated return type of CreateBranch
+"""
+type CreateBranchPayload {
+ """
+ Branch after mutation
+ """
+ branch: Branch
+
+ """
+ A unique identifier for the client performing the mutation.
+ """
+ clientMutationId: String
+
+ """
+ Errors encountered during execution of the mutation.
+ """
+ errors: [String!]!
+}
+
+"""
Autogenerated input type of CreateDiffNote
"""
input CreateDiffNoteInput {
@@ -443,7 +1120,7 @@ type CreateDiffNotePayload {
clientMutationId: String
"""
- Reasons why the mutation failed.
+ Errors encountered during execution of the mutation.
"""
errors: [String!]!
@@ -528,7 +1205,7 @@ type CreateEpicPayload {
epic: Epic
"""
- Reasons why the mutation failed.
+ Errors encountered during execution of the mutation.
"""
errors: [String!]!
}
@@ -568,7 +1245,7 @@ type CreateImageDiffNotePayload {
clientMutationId: String
"""
- Reasons why the mutation failed.
+ Errors encountered during execution of the mutation.
"""
errors: [String!]!
@@ -579,6 +1256,61 @@ type CreateImageDiffNotePayload {
}
"""
+Autogenerated input type of CreateIteration
+"""
+input CreateIterationInput {
+ """
+ A unique identifier for the client performing the mutation.
+ """
+ clientMutationId: String
+
+ """
+ The description of the iteration
+ """
+ description: String
+
+ """
+ The end date of the iteration
+ """
+ dueDate: String
+
+ """
+ The target group for the iteration
+ """
+ groupPath: ID!
+
+ """
+ The start date of the iteration
+ """
+ startDate: String
+
+ """
+ The title of the iteration
+ """
+ title: String
+}
+
+"""
+Autogenerated return type of CreateIteration
+"""
+type CreateIterationPayload {
+ """
+ A unique identifier for the client performing the mutation.
+ """
+ clientMutationId: String
+
+ """
+ Errors encountered during execution of the mutation.
+ """
+ errors: [String!]!
+
+ """
+ The created iteration
+ """
+ iteration: Iteration
+}
+
+"""
Autogenerated input type of CreateNote
"""
input CreateNoteInput {
@@ -613,7 +1345,7 @@ type CreateNotePayload {
clientMutationId: String
"""
- Reasons why the mutation failed.
+ Errors encountered during execution of the mutation.
"""
errors: [String!]!
@@ -653,7 +1385,7 @@ type CreateRequirementPayload {
clientMutationId: String
"""
- Reasons why the mutation failed.
+ Errors encountered during execution of the mutation.
"""
errors: [String!]!
@@ -698,6 +1430,11 @@ input CreateSnippetInput {
title: String!
"""
+ The paths to files uploaded in the snippet description
+ """
+ uploadedFiles: [String!]
+
+ """
The visibility level of the snippet
"""
visibilityLevel: VisibilityLevelsEnum!
@@ -713,7 +1450,7 @@ type CreateSnippetPayload {
clientMutationId: String
"""
- Reasons why the mutation failed.
+ Errors encountered during execution of the mutation.
"""
errors: [String!]!
@@ -1258,7 +1995,7 @@ type DesignManagementDeletePayload {
clientMutationId: String
"""
- Reasons why the mutation failed.
+ Errors encountered during execution of the mutation.
"""
errors: [String!]!
@@ -1308,7 +2045,7 @@ type DesignManagementUploadPayload {
designs: [Design!]!
"""
- Reasons why the mutation failed.
+ Errors encountered during execution of the mutation.
"""
errors: [String!]!
@@ -1498,7 +2235,7 @@ type DestroyNotePayload {
clientMutationId: String
"""
- Reasons why the mutation failed.
+ Errors encountered during execution of the mutation.
"""
errors: [String!]!
@@ -1533,7 +2270,7 @@ type DestroySnippetPayload {
clientMutationId: String
"""
- Reasons why the mutation failed.
+ Errors encountered during execution of the mutation.
"""
errors: [String!]!
@@ -1862,7 +2599,7 @@ type DismissVulnerabilityPayload {
clientMutationId: String
"""
- Reasons why the mutation failed.
+ Errors encountered during execution of the mutation.
"""
errors: [String!]!
@@ -2415,7 +3152,7 @@ type EpicAddIssuePayload {
epicIssue: EpicIssue
"""
- Reasons why the mutation failed.
+ Errors encountered during execution of the mutation.
"""
errors: [String!]!
}
@@ -2650,6 +3387,11 @@ type EpicIssue implements Noteable {
iid: ID!
"""
+ Iteration of the issue
+ """
+ iteration: Iteration
+
+ """
Labels of the issue
"""
labels(
@@ -2940,7 +3682,7 @@ type EpicSetSubscriptionPayload {
epic: Epic
"""
- Reasons why the mutation failed.
+ Errors encountered during execution of the mutation.
"""
errors: [String!]!
}
@@ -3001,7 +3743,7 @@ input EpicTreeNodeFieldsInputType {
"""
The id of the epic_issue or issue that the actual epic or issue is switched with
"""
- adjacentReferenceId: ID!
+ adjacentReferenceId: ID
"""
The id of the epic_issue or epic that is being moved
@@ -3016,7 +3758,7 @@ input EpicTreeNodeFieldsInputType {
"""
The type of the switch, after or before allowed
"""
- relativePosition: MoveType!
+ relativePosition: MoveType
}
"""
@@ -3049,7 +3791,7 @@ type EpicTreeReorderPayload {
clientMutationId: String
"""
- Reasons why the mutation failed.
+ Errors encountered during execution of the mutation.
"""
errors: [String!]!
}
@@ -3091,6 +3833,36 @@ type GeoNode {
name: String
"""
+ Package file registries of the GeoNode. Available only when feature flag `geo_self_service_framework` is enabled
+ """
+ packageFileRegistries(
+ """
+ Returns the elements in the list that come after the specified cursor.
+ """
+ after: String
+
+ """
+ Returns the elements in the list that come before the specified cursor.
+ """
+ before: String
+
+ """
+ Returns the first _n_ elements from the list.
+ """
+ first: Int
+
+ """
+ Filters registries by their ID
+ """
+ ids: [ID!]
+
+ """
+ Returns the last _n_ elements from the list.
+ """
+ last: Int
+ ): PackageFileRegistryConnection
+
+ """
Indicates whether this Geo node is the primary
"""
primary: Boolean
@@ -3163,7 +3935,7 @@ type GrafanaIntegration {
enabled: Boolean!
"""
- Url for the Grafana host for the Grafana integration
+ URL for the Grafana host for the Grafana integration
"""
grafanaUrl: String!
@@ -3509,6 +4281,53 @@ type Group {
): IssueConnection
"""
+ Find iterations
+ """
+ iterations(
+ """
+ Returns the elements in the list that come after the specified cursor.
+ """
+ after: String
+
+ """
+ Returns the elements in the list that come before the specified cursor.
+ """
+ before: String
+
+ """
+ List items within a time frame where items.end_date is between startDate and
+ endDate parameters (startDate parameter must be present)
+ """
+ endDate: Time
+
+ """
+ Returns the first _n_ elements from the list.
+ """
+ first: Int
+
+ """
+ Returns the last _n_ elements from the list.
+ """
+ last: Int
+
+ """
+ List items within a time frame where items.start_date is between startDate
+ and endDate parameters (endDate parameter must be present)
+ """
+ startDate: Time
+
+ """
+ Filter iterations by state
+ """
+ state: IterationState
+
+ """
+ Fuzzy search by title
+ """
+ title: String
+ ): IterationConnection
+
+ """
Indicates if Large File Storage (LFS) is enabled for namespace
"""
lfsEnabled: Boolean
@@ -3544,6 +4363,11 @@ type Group {
first: Int
"""
+ Return also milestones in all subgroups and subprojects
+ """
+ includeDescendants: Boolean
+
+ """
Returns the last _n_ elements from the list.
"""
last: Int
@@ -3600,6 +4424,11 @@ type Group {
first: Int
"""
+ Returns only the projects which have vulnerabilities
+ """
+ hasVulnerabilities: Boolean = false
+
+ """
Include also subgroup projects
"""
includeSubgroups: Boolean = false
@@ -3650,9 +4479,14 @@ type Group {
before: String
"""
- List time logs within a time range where the logged date is before end_date parameter.
+ List time logs within a date range where the logged date is equal to or before endDate
"""
- endDate: Time!
+ endDate: Time
+
+ """
+ List time-logs within a time range where the logged time is equal to or before endTime
+ """
+ endTime: Time
"""
Returns the first _n_ elements from the list.
@@ -3665,9 +4499,14 @@ type Group {
last: Int
"""
- List time logs within a time range where the logged date is after start_date parameter.
+ List time logs within a date range where the logged date is equal to or after startDate
"""
- startDate: Time!
+ startDate: Time
+
+ """
+ List time-logs within a time range where the logged time is equal to or after startTime
+ """
+ startTime: Time
): TimelogConnection!
"""
@@ -3686,8 +4525,7 @@ type Group {
visibility: String
"""
- Vulnerabilities reported on the projects in the group and its subgroups.
- Available only when feature flag `first_class_vulnerabilities` is enabled
+ Vulnerabilities reported on the projects in the group and its subgroups
"""
vulnerabilities(
"""
@@ -3732,6 +4570,41 @@ type Group {
): VulnerabilityConnection
"""
+ Number of vulnerabilities per severity level, per day, for the projects in the group and its subgroups
+ """
+ vulnerabilitiesCountByDayAndSeverity(
+ """
+ Returns the elements in the list that come after the specified cursor.
+ """
+ after: String
+
+ """
+ Returns the elements in the list that come before the specified cursor.
+ """
+ before: String
+
+ """
+ Last day for which to fetch vulnerability history
+ """
+ endDate: ISO8601Date!
+
+ """
+ Returns the first _n_ elements from the list.
+ """
+ first: Int
+
+ """
+ Returns the last _n_ elements from the list.
+ """
+ last: Int
+
+ """
+ First day for which to fetch vulnerability history
+ """
+ startDate: ISO8601Date!
+ ): VulnerabilitiesCountByDayAndSeverityConnection
+
+ """
Web URL of the group
"""
webUrl: String!
@@ -3754,6 +4627,38 @@ enum HealthStatus {
}
"""
+An ISO 8601-encoded date
+"""
+scalar ISO8601Date
+
+type InstanceSecurityDashboard {
+ """
+ Projects selected in Instance Security Dashboard
+ """
+ projects(
+ """
+ Returns the elements in the list that come after the specified cursor.
+ """
+ after: String
+
+ """
+ Returns the elements in the list that come before the specified cursor.
+ """
+ before: String
+
+ """
+ Returns the first _n_ elements from the list.
+ """
+ first: Int
+
+ """
+ Returns the last _n_ elements from the list.
+ """
+ last: Int
+ ): ProjectConnection!
+}
+
+"""
State of a GitLab issue or merge request
"""
enum IssuableState {
@@ -3884,6 +4789,11 @@ type Issue implements Noteable {
iid: ID!
"""
+ Iteration of the issue
+ """
+ iteration: Iteration
+
+ """
Labels of the issue
"""
labels(
@@ -4164,7 +5074,7 @@ type IssueSetConfidentialPayload {
clientMutationId: String
"""
- Reasons why the mutation failed.
+ Errors encountered during execution of the mutation.
"""
errors: [String!]!
@@ -4209,7 +5119,52 @@ type IssueSetDueDatePayload {
clientMutationId: String
"""
- Reasons why the mutation failed.
+ Errors encountered during execution of the mutation.
+ """
+ errors: [String!]!
+
+ """
+ The issue after mutation
+ """
+ issue: Issue
+}
+
+"""
+Autogenerated input type of IssueSetIteration
+"""
+input IssueSetIterationInput {
+ """
+ A unique identifier for the client performing the mutation.
+ """
+ clientMutationId: String
+
+ """
+ The iid of the issue to mutate
+ """
+ iid: String!
+
+ """
+ The iteration to assign to the issue.
+ """
+ iterationId: ID
+
+ """
+ The project the issue to mutate is in
+ """
+ projectPath: ID!
+}
+
+"""
+Autogenerated return type of IssueSetIteration
+"""
+type IssueSetIterationPayload {
+ """
+ A unique identifier for the client performing the mutation.
+ """
+ clientMutationId: String
+
+ """
+ Errors encountered during execution of the mutation.
"""
errors: [String!]!
@@ -4254,7 +5209,7 @@ type IssueSetWeightPayload {
clientMutationId: String
"""
- Reasons why the mutation failed.
+ Errors encountered during execution of the mutation.
"""
errors: [String!]!
@@ -4279,6 +5234,36 @@ enum IssueSort {
DUE_DATE_DESC
"""
+ Label priority by ascending order
+ """
+ LABEL_PRIORITY_ASC
+
+ """
+ Label priority by descending order
+ """
+ LABEL_PRIORITY_DESC
+
+ """
+ Milestone due date by ascending order
+ """
+ MILESTONE_DUE_ASC
+
+ """
+ Milestone due date by descending order
+ """
+ MILESTONE_DUE_DESC
+
+ """
+ Priority by ascending order
+ """
+ PRIORITY_ASC
+
+ """
+ Priority by descending order
+ """
+ PRIORITY_DESC
+
+ """
Relative position by ascending order
"""
RELATIVE_POSITION_ASC
@@ -4324,18 +5309,124 @@ enum IssueState {
}
"""
+Represents an iteration object.
+"""
+type Iteration {
+ """
+ Timestamp of iteration creation
+ """
+ createdAt: Time!
+
+ """
+ Description of the iteration
+ """
+ description: String
+
+ """
+ Timestamp of the iteration due date
+ """
+ dueDate: Time
+
+ """
+ ID of the iteration
+ """
+ id: ID!
+
+ """
+ Timestamp of the iteration start date
+ """
+ startDate: Time
+
+ """
+ State of the iteration
+ """
+ state: IterationState!
+
+ """
+ Title of the iteration
+ """
+ title: String!
+
+ """
+ Timestamp of last iteration update
+ """
+ updatedAt: Time!
+
+ """
+ Web path of the iteration
+ """
+ webPath: String!
+
+ """
+ Web URL of the iteration
+ """
+ webUrl: String!
+}
+
+"""
+The connection type for Iteration.
+"""
+type IterationConnection {
+ """
+ A list of edges.
+ """
+ edges: [IterationEdge]
+
+ """
+ A list of nodes.
+ """
+ nodes: [Iteration]
+
+ """
+ Information to aid in pagination.
+ """
+ pageInfo: PageInfo!
+}
+
+"""
+An edge in a connection.
+"""
+type IterationEdge {
+ """
+ A cursor for use in pagination.
+ """
+ cursor: String!
+
+ """
+ The item at the end of the edge.
+ """
+ node: Iteration
+}
+
+"""
+State of a GitLab iteration
+"""
+enum IterationState {
+ all
+ closed
+ opened
+ started
+ upcoming
+}
+
+"""
Represents untyped JSON
"""
scalar JSON
type JiraImport {
"""
+ Timestamp of when the Jira import was created
+ """
+ createdAt: Time
+
+ """
Project key for the imported Jira project
"""
jiraProjectKey: String!
"""
- Timestamp of when the Jira import was created
+ Timestamp of when the Jira import was scheduled
"""
scheduledAt: Time
@@ -4415,7 +5506,7 @@ type JiraImportStartPayload {
clientMutationId: String
"""
- Reasons why the mutation failed.
+ Errors encountered during execution of the mutation.
"""
errors: [String!]!
@@ -4505,6 +5596,15 @@ type LabelEdge {
}
"""
+List limit metric setting
+"""
+enum ListLimitMetric {
+ all_metrics
+ issue_count
+ issue_weights
+}
+
+"""
Autogenerated input type of MarkAsSpamSnippet
"""
input MarkAsSpamSnippetInput {
@@ -4529,7 +5629,7 @@ type MarkAsSpamSnippetPayload {
clientMutationId: String
"""
- Reasons why the mutation failed.
+ Errors encountered during execution of the mutation.
"""
errors: [String!]!
@@ -5076,7 +6176,7 @@ type MergeRequestSetAssigneesPayload {
clientMutationId: String
"""
- Reasons why the mutation failed.
+ Errors encountered during execution of the mutation.
"""
errors: [String!]!
@@ -5126,7 +6226,7 @@ type MergeRequestSetLabelsPayload {
clientMutationId: String
"""
- Reasons why the mutation failed.
+ Errors encountered during execution of the mutation.
"""
errors: [String!]!
@@ -5171,7 +6271,7 @@ type MergeRequestSetLockedPayload {
clientMutationId: String
"""
- Reasons why the mutation failed.
+ Errors encountered during execution of the mutation.
"""
errors: [String!]!
@@ -5216,7 +6316,7 @@ type MergeRequestSetMilestonePayload {
clientMutationId: String
"""
- Reasons why the mutation failed.
+ Errors encountered during execution of the mutation.
"""
errors: [String!]!
@@ -5261,7 +6361,7 @@ type MergeRequestSetSubscriptionPayload {
clientMutationId: String
"""
- Reasons why the mutation failed.
+ Errors encountered during execution of the mutation.
"""
errors: [String!]!
@@ -5306,7 +6406,7 @@ type MergeRequestSetWipPayload {
clientMutationId: String
"""
- Reasons why the mutation failed.
+ Errors encountered during execution of the mutation.
"""
errors: [String!]!
@@ -5340,7 +6440,7 @@ type Metadata {
type MetricsDashboard {
"""
- Annotations added to the dashboard. Will always return `null` if `metrics_dashboard_annotations` feature flag is disabled
+ Annotations added to the dashboard
"""
annotations(
"""
@@ -5389,7 +6489,7 @@ type MetricsDashboardAnnotation {
"""
Timestamp marking end of annotated time span
"""
- endingAt: String
+ endingAt: Time
"""
ID of the annotation
@@ -5404,7 +6504,7 @@ type MetricsDashboardAnnotation {
"""
Timestamp marking start of annotated time span
"""
- startingAt: String
+ startingAt: Time
}
"""
@@ -5549,10 +6649,16 @@ enum MoveType {
type Mutation {
addAwardEmoji(input: AddAwardEmojiInput!): AddAwardEmojiPayload
+ addProjectToSecurityDashboard(input: AddProjectToSecurityDashboardInput!): AddProjectToSecurityDashboardPayload
adminSidekiqQueuesDeleteJobs(input: AdminSidekiqQueuesDeleteJobsInput!): AdminSidekiqQueuesDeleteJobsPayload
+ boardListUpdateLimitMetrics(input: BoardListUpdateLimitMetricsInput!): BoardListUpdateLimitMetricsPayload
+ createAlertIssue(input: CreateAlertIssueInput!): CreateAlertIssuePayload
+ createAnnotation(input: CreateAnnotationInput!): CreateAnnotationPayload
+ createBranch(input: CreateBranchInput!): CreateBranchPayload
createDiffNote(input: CreateDiffNoteInput!): CreateDiffNotePayload
createEpic(input: CreateEpicInput!): CreateEpicPayload
createImageDiffNote(input: CreateImageDiffNoteInput!): CreateImageDiffNotePayload
+ createIteration(input: CreateIterationInput!): CreateIterationPayload
createNote(input: CreateNoteInput!): CreateNotePayload
createRequirement(input: CreateRequirementInput!): CreateRequirementPayload
createSnippet(input: CreateSnippetInput!): CreateSnippetPayload
@@ -5566,6 +6672,7 @@ type Mutation {
epicTreeReorder(input: EpicTreeReorderInput!): EpicTreeReorderPayload
issueSetConfidential(input: IssueSetConfidentialInput!): IssueSetConfidentialPayload
issueSetDueDate(input: IssueSetDueDateInput!): IssueSetDueDatePayload
+ issueSetIteration(input: IssueSetIterationInput!): IssueSetIterationPayload
issueSetWeight(input: IssueSetWeightInput!): IssueSetWeightPayload
jiraImportStart(input: JiraImportStartInput!): JiraImportStartPayload
markAsSpamSnippet(input: MarkAsSpamSnippetInput!): MarkAsSpamSnippetPayload
@@ -5576,11 +6683,13 @@ type Mutation {
mergeRequestSetSubscription(input: MergeRequestSetSubscriptionInput!): MergeRequestSetSubscriptionPayload
mergeRequestSetWip(input: MergeRequestSetWipInput!): MergeRequestSetWipPayload
removeAwardEmoji(input: RemoveAwardEmojiInput!): RemoveAwardEmojiPayload
+ removeProjectFromSecurityDashboard(input: RemoveProjectFromSecurityDashboardInput!): RemoveProjectFromSecurityDashboardPayload
todoMarkDone(input: TodoMarkDoneInput!): TodoMarkDonePayload
todoRestore(input: TodoRestoreInput!): TodoRestorePayload
todoRestoreMany(input: TodoRestoreManyInput!): TodoRestoreManyPayload
todosMarkAllDone(input: TodosMarkAllDoneInput!): TodosMarkAllDonePayload
toggleAwardEmoji(input: ToggleAwardEmojiInput!): ToggleAwardEmojiPayload
+ updateAlertStatus(input: UpdateAlertStatusInput!): UpdateAlertStatusPayload
updateEpic(input: UpdateEpicInput!): UpdateEpicPayload
"""
@@ -5681,6 +6790,11 @@ type Namespace {
first: Int
"""
+ Returns only the projects which have vulnerabilities
+ """
+ hasVulnerabilities: Boolean = false
+
+ """
Include also subgroup projects
"""
includeSubgroups: Boolean = false
@@ -5934,6 +7048,188 @@ interface Noteable {
}
"""
+Represents a package
+"""
+type Package {
+ """
+ The created date
+ """
+ createdAt: Time!
+
+ """
+ The ID of the package
+ """
+ id: ID!
+
+ """
+ The name of the package
+ """
+ name: String!
+
+ """
+ The type of the package
+ """
+ packageType: PackageTypeEnum!
+
+ """
+ The update date
+ """
+ updatedAt: Time!
+
+ """
+ The version of the package
+ """
+ version: String
+}
+
+"""
+The connection type for Package.
+"""
+type PackageConnection {
+ """
+ A list of edges.
+ """
+ edges: [PackageEdge]
+
+ """
+ A list of nodes.
+ """
+ nodes: [Package]
+
+ """
+ Information to aid in pagination.
+ """
+ pageInfo: PageInfo!
+}
+
+"""
+An edge in a connection.
+"""
+type PackageEdge {
+ """
+ A cursor for use in pagination.
+ """
+ cursor: String!
+
+ """
+ The item at the end of the edge.
+ """
+ node: Package
+}
+
+"""
+Represents the sync and verification state of a package file
+"""
+type PackageFileRegistry {
+ """
+ Timestamp when the PackageFileRegistry was created
+ """
+ createdAt: Time
+
+ """
+ ID of the PackageFileRegistry
+ """
+ id: ID!
+
+ """
+ Error message during sync of the PackageFileRegistry
+ """
+ lastSyncFailure: String
+
+ """
+ Timestamp of the most recent successful sync of the PackageFileRegistry
+ """
+ lastSyncedAt: Time
+
+ """
+ ID of the PackageFile
+ """
+ packageFileId: ID!
+
+ """
+ Timestamp after which the PackageFileRegistry should be resynced
+ """
+ retryAt: Time
+
+ """
+ Number of consecutive failed sync attempts of the PackageFileRegistry
+ """
+ retryCount: Int
+
+ """
+ Sync state of the PackageFileRegistry
+ """
+ state: RegistryState
+}
+
+"""
+The connection type for PackageFileRegistry.
+"""
+type PackageFileRegistryConnection {
+ """
+ A list of edges.
+ """
+ edges: [PackageFileRegistryEdge]
+
+ """
+ A list of nodes.
+ """
+ nodes: [PackageFileRegistry]
+
+ """
+ Information to aid in pagination.
+ """
+ pageInfo: PageInfo!
+}
+
+"""
+An edge in a connection.
+"""
+type PackageFileRegistryEdge {
+ """
+ A cursor for use in pagination.
+ """
+ cursor: String!
+
+ """
+ The item at the end of the edge.
+ """
+ node: PackageFileRegistry
+}
+
+enum PackageTypeEnum {
+ """
+ Packages from the composer package manager
+ """
+ COMPOSER
+
+ """
+ Packages from the conan package manager
+ """
+ CONAN
+
+ """
+ Packages from the maven package manager
+ """
+ MAVEN
+
+ """
+ Packages from the npm package manager
+ """
+ NPM
+
+ """
+ Packages from the nuget package manager
+ """
+ NUGET
+
+ """
+ Packages from the pypi package manager
+ """
+ PYPI
+}
+
+"""
Information about pagination in a connection.
"""
type PageInfo {
@@ -6099,6 +7395,81 @@ enum PipelineStatusEnum {
type Project {
"""
+ A single Alert Management alert of the project
+ """
+ alertManagementAlert(
+ """
+ IID of the alert. For example, "1"
+ """
+ iid: String
+
+ """
+ Search criteria for filtering alerts. This will search on title, description, service, monitoring_tool.
+ """
+ search: String
+
+ """
+ Sort alerts by this criteria
+ """
+ sort: AlertManagementAlertSort
+
+ """
+ Alerts with the specified statues. For example, [TRIGGERED]
+ """
+ statuses: [AlertManagementStatus!]
+ ): AlertManagementAlert
+
+ """
+ Counts of alerts by status for the project
+ """
+ alertManagementAlertStatusCounts: AlertManagementAlertStatusCountsType
+
+ """
+ Alert Management alerts of the project
+ """
+ alertManagementAlerts(
+ """
+ Returns the elements in the list that come after the specified cursor.
+ """
+ after: String
+
+ """
+ Returns the elements in the list that come before the specified cursor.
+ """
+ before: String
+
+ """
+ Returns the first _n_ elements from the list.
+ """
+ first: Int
+
+ """
+ IID of the alert. For example, "1"
+ """
+ iid: String
+
+ """
+ Returns the last _n_ elements from the list.
+ """
+ last: Int
+
+ """
+ Search criteria for filtering alerts. This will search on title, description, service, monitoring_tool.
+ """
+ search: String
+
+ """
+ Sort alerts by this criteria
+ """
+ sort: AlertManagementAlertSort
+
+ """
+ Alerts with the specified statues. For example, [TRIGGERED]
+ """
+ statuses: [AlertManagementStatus!]
+ ): AlertManagementAlertConnection
+
+ """
Indicates the archived status of the project
"""
archived: Boolean
@@ -6571,6 +7942,31 @@ type Project {
openIssuesCount: Int
"""
+ Packages of the project
+ """
+ packages(
+ """
+ Returns the elements in the list that come after the specified cursor.
+ """
+ after: String
+
+ """
+ Returns the elements in the list that come before the specified cursor.
+ """
+ before: String
+
+ """
+ Returns the first _n_ elements from the list.
+ """
+ first: Int
+
+ """
+ Returns the last _n_ elements from the list.
+ """
+ last: Int
+ ): PackageConnection
+
+ """
Path of the project
"""
path: String!
@@ -6627,6 +8023,41 @@ type Project {
publicJobs: Boolean
"""
+ A single release of the project. Available only when feature flag `graphql_release_data` is enabled
+ """
+ release(
+ """
+ The name of the tag associated to the release
+ """
+ tagName: String!
+ ): Release
+
+ """
+ Releases of the project. Available only when feature flag `graphql_release_data` is enabled
+ """
+ releases(
+ """
+ Returns the elements in the list that come after the specified cursor.
+ """
+ after: String
+
+ """
+ Returns the elements in the list that come before the specified cursor.
+ """
+ before: String
+
+ """
+ Returns the first _n_ elements from the list.
+ """
+ first: Int
+
+ """
+ Returns the last _n_ elements from the list.
+ """
+ last: Int
+ ): ReleaseConnection
+
+ """
Indicates if `Delete source branch` option should be enabled by default for all new merge requests of the project
"""
removeSourceBranchAfterMerge: Boolean
@@ -6857,7 +8288,7 @@ type Project {
visibility: String
"""
- Vulnerabilities reported on the project. Available only when feature flag `first_class_vulnerabilities` is enabled
+ Vulnerabilities reported on the project
"""
vulnerabilities(
"""
@@ -6902,8 +8333,7 @@ type Project {
): VulnerabilityConnection
"""
- Counts for each severity of vulnerability of the project. Available only when
- feature flag `first_class_vulnerabilities` is enabled
+ Counts for each severity of vulnerability of the project
"""
vulnerabilitySeveritiesCount: VulnerabilitySeveritiesCount
@@ -7244,6 +8674,11 @@ type Query {
): Group
"""
+ Fields related to Instance Security Dashboard
+ """
+ instanceSecurityDashboard: InstanceSecurityDashboard
+
+ """
Metadata about GitLab
"""
metadata: Metadata
@@ -7269,6 +8704,41 @@ type Query {
): Project
"""
+ Find projects visible to the current user
+ """
+ projects(
+ """
+ Returns the elements in the list that come after the specified cursor.
+ """
+ after: String
+
+ """
+ Returns the elements in the list that come before the specified cursor.
+ """
+ before: String
+
+ """
+ Returns the first _n_ elements from the list.
+ """
+ first: Int
+
+ """
+ Returns the last _n_ elements from the list.
+ """
+ last: Int
+
+ """
+ Limit projects that the current user is a member of
+ """
+ membership: Boolean
+
+ """
+ Search criteria
+ """
+ search: String
+ ): ProjectConnection
+
+ """
Find Snippets visible to the current user
"""
snippets(
@@ -7367,6 +8837,173 @@ type Query {
"""
state: [VulnerabilityState!]
): VulnerabilityConnection
+
+ """
+ Number of vulnerabilities per severity level, per day, for the projects on the current user's instance security dashboard
+ """
+ vulnerabilitiesCountByDayAndSeverity(
+ """
+ Returns the elements in the list that come after the specified cursor.
+ """
+ after: String
+
+ """
+ Returns the elements in the list that come before the specified cursor.
+ """
+ before: String
+
+ """
+ Last day for which to fetch vulnerability history
+ """
+ endDate: ISO8601Date!
+
+ """
+ Returns the first _n_ elements from the list.
+ """
+ first: Int
+
+ """
+ Returns the last _n_ elements from the list.
+ """
+ last: Int
+
+ """
+ First day for which to fetch vulnerability history
+ """
+ startDate: ISO8601Date!
+ ): VulnerabilitiesCountByDayAndSeverityConnection
+}
+
+"""
+State of a Geo registry.
+"""
+enum RegistryState {
+ """
+ Registry that failed to sync
+ """
+ FAILED
+
+ """
+ Registry waiting to be synced
+ """
+ PENDING
+
+ """
+ Registry currently syncing
+ """
+ STARTED
+
+ """
+ Registry that is synced
+ """
+ SYNCED
+}
+
+type Release {
+ """
+ User that created the release
+ """
+ author: User
+
+ """
+ The commit associated with the release
+ """
+ commit: Commit
+
+ """
+ Timestamp of when the release was created
+ """
+ createdAt: Time
+
+ """
+ Description (also known as "release notes") of the release
+ """
+ description: String
+
+ """
+ The GitLab Flavored Markdown rendering of `description`
+ """
+ descriptionHtml: String
+
+ """
+ Milestones associated to the release
+ """
+ milestones(
+ """
+ Returns the elements in the list that come after the specified cursor.
+ """
+ after: String
+
+ """
+ Returns the elements in the list that come before the specified cursor.
+ """
+ before: String
+
+ """
+ Returns the first _n_ elements from the list.
+ """
+ first: Int
+
+ """
+ Returns the last _n_ elements from the list.
+ """
+ last: Int
+ ): MilestoneConnection
+
+ """
+ Name of the release
+ """
+ name: String
+
+ """
+ Timestamp of when the release was released
+ """
+ releasedAt: Time
+
+ """
+ Name of the tag associated with the release
+ """
+ tagName: String!
+
+ """
+ Relative web path to the tag associated with the release
+ """
+ tagPath: String
+}
+
+"""
+The connection type for Release.
+"""
+type ReleaseConnection {
+ """
+ A list of edges.
+ """
+ edges: [ReleaseEdge]
+
+ """
+ A list of nodes.
+ """
+ nodes: [Release]
+
+ """
+ Information to aid in pagination.
+ """
+ pageInfo: PageInfo!
+}
+
+"""
+An edge in a connection.
+"""
+type ReleaseEdge {
+ """
+ A cursor for use in pagination.
+ """
+ cursor: String!
+
+ """
+ The item at the end of the edge.
+ """
+ node: Release
}
"""
@@ -7404,7 +9041,37 @@ type RemoveAwardEmojiPayload {
clientMutationId: String
"""
- Reasons why the mutation failed.
+ Errors encountered during execution of the mutation.
+ """
+ errors: [String!]!
+}
+
+"""
+Autogenerated input type of RemoveProjectFromSecurityDashboard
+"""
+input RemoveProjectFromSecurityDashboardInput {
+ """
+ A unique identifier for the client performing the mutation.
+ """
+ clientMutationId: String
+
+ """
+ ID of the project to remove from the Instance Security Dashboard
+ """
+ id: ID!
+}
+
+"""
+Autogenerated return type of RemoveProjectFromSecurityDashboard
+"""
+type RemoveProjectFromSecurityDashboardPayload {
+ """
+ A unique identifier for the client performing the mutation.
+ """
+ clientMutationId: String
+
+ """
+ Errors encountered during execution of the mutation.
"""
errors: [String!]!
}
@@ -8123,7 +9790,6 @@ enum ServiceType {
HANGOUTS_CHAT_SERVICE
HIPCHAT_SERVICE
IRKER_SERVICE
- JENKINS_DEPRECATED_SERVICE
JENKINS_SERVICE
JIRA_SERVICE
MATTERMOST_SERVICE
@@ -8139,6 +9805,7 @@ enum ServiceType {
SLACK_SLASH_COMMANDS_SERVICE
TEAMCITY_SERVICE
UNIFY_CIRCUIT_SERVICE
+ WEBEX_TEAMS_SERVICE
YOUTRACK_SERVICE
}
@@ -8207,7 +9874,7 @@ type Snippet implements Noteable {
httpUrlToRepo: String
"""
- Id of the snippet
+ ID of the snippet
"""
id: ID!
@@ -8287,6 +9954,11 @@ type SnippetBlob {
binary: Boolean!
"""
+ Blob external storage
+ """
+ externalStorage: String
+
+ """
Blob mode
"""
mode: String
@@ -8312,6 +9984,11 @@ type SnippetBlob {
rawPath: String!
"""
+ Shows whether the blob is rendered as text
+ """
+ renderedAsText: Boolean!
+
+ """
Blob highlighted data
"""
richData: String
@@ -8653,7 +10330,7 @@ type Todo {
group: Group
"""
- Id of the todo
+ ID of the todo
"""
id: ID!
@@ -8743,7 +10420,7 @@ type TodoMarkDonePayload {
clientMutationId: String
"""
- Reasons why the mutation failed.
+ Errors encountered during execution of the mutation.
"""
errors: [String!]!
@@ -8793,7 +10470,7 @@ type TodoRestoreManyPayload {
clientMutationId: String
"""
- Reasons why the mutation failed.
+ Errors encountered during execution of the mutation.
"""
errors: [String!]!
@@ -8813,7 +10490,7 @@ type TodoRestorePayload {
clientMutationId: String
"""
- Reasons why the mutation failed.
+ Errors encountered during execution of the mutation.
"""
errors: [String!]!
@@ -8875,7 +10552,7 @@ type TodosMarkAllDonePayload {
clientMutationId: String
"""
- Reasons why the mutation failed.
+ Errors encountered during execution of the mutation.
"""
errors: [String!]!
@@ -8920,7 +10597,7 @@ type ToggleAwardEmojiPayload {
clientMutationId: String
"""
- Reasons why the mutation failed.
+ Errors encountered during execution of the mutation.
"""
errors: [String!]!
@@ -9092,6 +10769,56 @@ enum TypeEnum {
project
}
+"""
+Autogenerated input type of UpdateAlertStatus
+"""
+input UpdateAlertStatusInput {
+ """
+ A unique identifier for the client performing the mutation.
+ """
+ clientMutationId: String
+
+ """
+ The iid of the alert to mutate
+ """
+ iid: String!
+
+ """
+ The project the alert to mutate is in
+ """
+ projectPath: ID!
+
+ """
+ The status to set the alert
+ """
+ status: AlertManagementStatus!
+}
+
+"""
+Autogenerated return type of UpdateAlertStatus
+"""
+type UpdateAlertStatusPayload {
+ """
+ The alert after mutation
+ """
+ alert: AlertManagementAlert
+
+ """
+ A unique identifier for the client performing the mutation.
+ """
+ clientMutationId: String
+
+ """
+ Errors encountered during execution of the mutation.
+ """
+ errors: [String!]!
+
+ """
+ The issue created after mutation
+ """
+ issue: Issue
+}
+
input UpdateDiffImagePositionInput {
"""
Total height of the image
@@ -9199,7 +10926,7 @@ type UpdateEpicPayload {
epic: Epic
"""
- Reasons why the mutation failed.
+ Errors encountered during execution of the mutation.
"""
errors: [String!]!
}
@@ -9239,7 +10966,7 @@ type UpdateImageDiffNotePayload {
clientMutationId: String
"""
- Reasons why the mutation failed.
+ Errors encountered during execution of the mutation.
"""
errors: [String!]!
@@ -9304,7 +11031,7 @@ type UpdateIssuePayload {
clientMutationId: String
"""
- Reasons why the mutation failed.
+ Errors encountered during execution of the mutation.
"""
errors: [String!]!
@@ -9344,7 +11071,7 @@ type UpdateNotePayload {
clientMutationId: String
"""
- Reasons why the mutation failed.
+ Errors encountered during execution of the mutation.
"""
errors: [String!]!
@@ -9394,7 +11121,7 @@ type UpdateRequirementPayload {
clientMutationId: String
"""
- Reasons why the mutation failed.
+ Errors encountered during execution of the mutation.
"""
errors: [String!]!
@@ -9454,7 +11181,7 @@ type UpdateSnippetPayload {
clientMutationId: String
"""
- Reasons why the mutation failed.
+ Errors encountered during execution of the mutation.
"""
errors: [String!]!
@@ -9473,6 +11200,11 @@ type User {
avatarUrl: String
"""
+ ID of the user
+ """
+ id: ID!
+
+ """
Human-readable name of the user
"""
name: String!
@@ -9518,6 +11250,11 @@ type User {
): SnippetConnection
"""
+ State of the issue
+ """
+ state: String!
+
+ """
Todos of the user
"""
todos(
@@ -9643,6 +11380,61 @@ enum VisibilityScopesEnum {
}
"""
+Represents the number of vulnerabilities for a particular severity on a particular day
+"""
+type VulnerabilitiesCountByDayAndSeverity {
+ """
+ Number of vulnerabilities
+ """
+ count: Int
+
+ """
+ Date for the count
+ """
+ day: ISO8601Date
+
+ """
+ Severity of the counted vulnerabilities
+ """
+ severity: VulnerabilitySeverity
+}
+
+"""
+The connection type for VulnerabilitiesCountByDayAndSeverity.
+"""
+type VulnerabilitiesCountByDayAndSeverityConnection {
+ """
+ A list of edges.
+ """
+ edges: [VulnerabilitiesCountByDayAndSeverityEdge]
+
+ """
+ A list of nodes.
+ """
+ nodes: [VulnerabilitiesCountByDayAndSeverity]
+
+ """
+ Information to aid in pagination.
+ """
+ pageInfo: PageInfo!
+}
+
+"""
+An edge in a connection.
+"""
+type VulnerabilitiesCountByDayAndSeverityEdge {
+ """
+ A cursor for use in pagination.
+ """
+ cursor: String!
+
+ """
+ The item at the end of the edge.
+ """
+ node: VulnerabilitiesCountByDayAndSeverity
+}
+
+"""
Represents a vulnerability.
"""
type Vulnerability {
@@ -9657,10 +11449,9 @@ type Vulnerability {
id: ID!
"""
- The JSON location metadata for the vulnerability. Its format depends on the
- type of the security scan that found the vulnerability
+ Location metadata for the vulnerability. Its fields depend on the type of security scan that found the vulnerability
"""
- location: JSON
+ location: VulnerabilityLocation
"""
The project on which the vulnerability was found
@@ -9734,6 +11525,101 @@ type VulnerabilityEdge {
}
"""
+Represents a vulnerability location. The fields with data will depend on the vulnerability report type
+"""
+union VulnerabilityLocation = VulnerabilityLocationContainerScanning | VulnerabilityLocationDast | VulnerabilityLocationDependencyScanning | VulnerabilityLocationSast
+
+"""
+Represents the location of a vulnerability found by a container security scan
+"""
+type VulnerabilityLocationContainerScanning {
+ """
+ Dependency containing the vulnerability
+ """
+ dependency: VulnerableDependency
+
+ """
+ Name of the vulnerable container image
+ """
+ image: String
+
+ """
+ Operating system that runs on the vulnerable container image
+ """
+ operatingSystem: String
+}
+
+"""
+Represents the location of a vulnerability found by a DAST scan
+"""
+type VulnerabilityLocationDast {
+ """
+ Domain name of the vulnerable request
+ """
+ hostname: String
+
+ """
+ Query parameter for the URL on which the vulnerability occurred
+ """
+ param: String
+
+ """
+ URL path and query string of the vulnerable request
+ """
+ path: String
+
+ """
+ HTTP method of the vulnerable request
+ """
+ requestMethod: String
+}
+
+"""
+Represents the location of a vulnerability found by a dependency security scan
+"""
+type VulnerabilityLocationDependencyScanning {
+ """
+ Dependency containing the vulnerability
+ """
+ dependency: VulnerableDependency
+
+ """
+ Path to the vulnerable file
+ """
+ file: String
+}
+
+"""
+Represents the location of a vulnerability found by a SAST scan
+"""
+type VulnerabilityLocationSast {
+ """
+ Number of the last relevant line in the vulnerable file
+ """
+ endLine: String
+
+ """
+ Path to the vulnerable file
+ """
+ file: String
+
+ """
+ Number of the first relevant line in the vulnerable file
+ """
+ startLine: String
+
+ """
+ Class containing the vulnerability
+ """
+ vulnerableClass: String
+
+ """
+ Method containing the vulnerability
+ """
+ vulnerableMethod: String
+}
+
+"""
Check permissions for the current user on a vulnerability
"""
type VulnerabilityPermissions {
@@ -9843,4 +11729,29 @@ enum VulnerabilityState {
DETECTED
DISMISSED
RESOLVED
+}
+
+"""
+Represents a vulnerable dependency. Used in vulnerability location data
+"""
+type VulnerableDependency {
+ """
+ The package associated with the vulnerable dependency
+ """
+ package: VulnerablePackage
+
+ """
+ The version of the vulnerable dependency
+ """
+ version: String
+}
+
+"""
+Represents a vulnerable package. Used in vulnerability dependency data
+"""
+type VulnerablePackage {
+ """
+ The name of the vulnerable package
+ """
+ name: String
} \ No newline at end of file
diff --git a/doc/api/graphql/reference/gitlab_schema.json b/doc/api/graphql/reference/gitlab_schema.json
index 26a2acdb4c6..40bfa08cff3 100644
--- a/doc/api/graphql/reference/gitlab_schema.json
+++ b/doc/api/graphql/reference/gitlab_schema.json
@@ -93,7 +93,7 @@
},
{
"name": "errors",
- "description": "Reasons why the mutation failed.",
+ "description": "Errors encountered during execution of the mutation.",
"args": [
],
@@ -127,6 +127,108 @@
},
{
"kind": "INPUT_OBJECT",
+ "name": "AddProjectToSecurityDashboardInput",
+ "description": "Autogenerated input type of AddProjectToSecurityDashboard",
+ "fields": null,
+ "inputFields": [
+ {
+ "name": "id",
+ "description": "ID of the project to be added to Instance Security Dashboard",
+ "type": {
+ "kind": "NON_NULL",
+ "name": null,
+ "ofType": {
+ "kind": "SCALAR",
+ "name": "ID",
+ "ofType": null
+ }
+ },
+ "defaultValue": null
+ },
+ {
+ "name": "clientMutationId",
+ "description": "A unique identifier for the client performing the mutation.",
+ "type": {
+ "kind": "SCALAR",
+ "name": "String",
+ "ofType": null
+ },
+ "defaultValue": null
+ }
+ ],
+ "interfaces": null,
+ "enumValues": null,
+ "possibleTypes": null
+ },
+ {
+ "kind": "OBJECT",
+ "name": "AddProjectToSecurityDashboardPayload",
+ "description": "Autogenerated return type of AddProjectToSecurityDashboard",
+ "fields": [
+ {
+ "name": "clientMutationId",
+ "description": "A unique identifier for the client performing the mutation.",
+ "args": [
+
+ ],
+ "type": {
+ "kind": "SCALAR",
+ "name": "String",
+ "ofType": null
+ },
+ "isDeprecated": false,
+ "deprecationReason": null
+ },
+ {
+ "name": "errors",
+ "description": "Errors encountered during execution of the mutation.",
+ "args": [
+
+ ],
+ "type": {
+ "kind": "NON_NULL",
+ "name": null,
+ "ofType": {
+ "kind": "LIST",
+ "name": null,
+ "ofType": {
+ "kind": "NON_NULL",
+ "name": null,
+ "ofType": {
+ "kind": "SCALAR",
+ "name": "String",
+ "ofType": null
+ }
+ }
+ }
+ },
+ "isDeprecated": false,
+ "deprecationReason": null
+ },
+ {
+ "name": "project",
+ "description": "Project that was added to the Instance Security Dashboard",
+ "args": [
+
+ ],
+ "type": {
+ "kind": "OBJECT",
+ "name": "Project",
+ "ofType": null
+ },
+ "isDeprecated": false,
+ "deprecationReason": null
+ }
+ ],
+ "inputFields": null,
+ "interfaces": [
+
+ ],
+ "enumValues": null,
+ "possibleTypes": null
+ },
+ {
+ "kind": "INPUT_OBJECT",
"name": "AdminSidekiqQueuesDeleteJobsInput",
"description": "Autogenerated input type of AdminSidekiqQueuesDeleteJobs",
"fields": null,
@@ -241,7 +343,7 @@
},
{
"name": "errors",
- "description": "Reasons why the mutation failed.",
+ "description": "Errors encountered during execution of the mutation.",
"args": [
],
@@ -289,6 +391,651 @@
},
{
"kind": "OBJECT",
+ "name": "AlertManagementAlert",
+ "description": "Describes an alert from the project's Alert Management",
+ "fields": [
+ {
+ "name": "createdAt",
+ "description": "Timestamp the alert was created",
+ "args": [
+
+ ],
+ "type": {
+ "kind": "SCALAR",
+ "name": "Time",
+ "ofType": null
+ },
+ "isDeprecated": false,
+ "deprecationReason": null
+ },
+ {
+ "name": "description",
+ "description": "Description of the alert",
+ "args": [
+
+ ],
+ "type": {
+ "kind": "SCALAR",
+ "name": "String",
+ "ofType": null
+ },
+ "isDeprecated": false,
+ "deprecationReason": null
+ },
+ {
+ "name": "details",
+ "description": "Alert details",
+ "args": [
+
+ ],
+ "type": {
+ "kind": "SCALAR",
+ "name": "JSON",
+ "ofType": null
+ },
+ "isDeprecated": false,
+ "deprecationReason": null
+ },
+ {
+ "name": "endedAt",
+ "description": "Timestamp the alert ended",
+ "args": [
+
+ ],
+ "type": {
+ "kind": "SCALAR",
+ "name": "Time",
+ "ofType": null
+ },
+ "isDeprecated": false,
+ "deprecationReason": null
+ },
+ {
+ "name": "eventCount",
+ "description": "Number of events of this alert",
+ "args": [
+
+ ],
+ "type": {
+ "kind": "SCALAR",
+ "name": "Int",
+ "ofType": null
+ },
+ "isDeprecated": false,
+ "deprecationReason": null
+ },
+ {
+ "name": "hosts",
+ "description": "List of hosts the alert came from",
+ "args": [
+
+ ],
+ "type": {
+ "kind": "LIST",
+ "name": null,
+ "ofType": {
+ "kind": "NON_NULL",
+ "name": null,
+ "ofType": {
+ "kind": "SCALAR",
+ "name": "String",
+ "ofType": null
+ }
+ }
+ },
+ "isDeprecated": false,
+ "deprecationReason": null
+ },
+ {
+ "name": "iid",
+ "description": "Internal ID of the alert",
+ "args": [
+
+ ],
+ "type": {
+ "kind": "NON_NULL",
+ "name": null,
+ "ofType": {
+ "kind": "SCALAR",
+ "name": "ID",
+ "ofType": null
+ }
+ },
+ "isDeprecated": false,
+ "deprecationReason": null
+ },
+ {
+ "name": "issueIid",
+ "description": "Internal ID of the GitLab issue attached to the alert",
+ "args": [
+
+ ],
+ "type": {
+ "kind": "SCALAR",
+ "name": "ID",
+ "ofType": null
+ },
+ "isDeprecated": false,
+ "deprecationReason": null
+ },
+ {
+ "name": "monitoringTool",
+ "description": "Monitoring tool the alert came from",
+ "args": [
+
+ ],
+ "type": {
+ "kind": "SCALAR",
+ "name": "String",
+ "ofType": null
+ },
+ "isDeprecated": false,
+ "deprecationReason": null
+ },
+ {
+ "name": "service",
+ "description": "Service the alert came from",
+ "args": [
+
+ ],
+ "type": {
+ "kind": "SCALAR",
+ "name": "String",
+ "ofType": null
+ },
+ "isDeprecated": false,
+ "deprecationReason": null
+ },
+ {
+ "name": "severity",
+ "description": "Severity of the alert",
+ "args": [
+
+ ],
+ "type": {
+ "kind": "ENUM",
+ "name": "AlertManagementSeverity",
+ "ofType": null
+ },
+ "isDeprecated": false,
+ "deprecationReason": null
+ },
+ {
+ "name": "startedAt",
+ "description": "Timestamp the alert was raised",
+ "args": [
+
+ ],
+ "type": {
+ "kind": "SCALAR",
+ "name": "Time",
+ "ofType": null
+ },
+ "isDeprecated": false,
+ "deprecationReason": null
+ },
+ {
+ "name": "status",
+ "description": "Status of the alert",
+ "args": [
+
+ ],
+ "type": {
+ "kind": "ENUM",
+ "name": "AlertManagementStatus",
+ "ofType": null
+ },
+ "isDeprecated": false,
+ "deprecationReason": null
+ },
+ {
+ "name": "title",
+ "description": "Title of the alert",
+ "args": [
+
+ ],
+ "type": {
+ "kind": "SCALAR",
+ "name": "String",
+ "ofType": null
+ },
+ "isDeprecated": false,
+ "deprecationReason": null
+ },
+ {
+ "name": "updatedAt",
+ "description": "Timestamp the alert was last updated",
+ "args": [
+
+ ],
+ "type": {
+ "kind": "SCALAR",
+ "name": "Time",
+ "ofType": null
+ },
+ "isDeprecated": false,
+ "deprecationReason": null
+ }
+ ],
+ "inputFields": null,
+ "interfaces": [
+
+ ],
+ "enumValues": null,
+ "possibleTypes": null
+ },
+ {
+ "kind": "OBJECT",
+ "name": "AlertManagementAlertConnection",
+ "description": "The connection type for AlertManagementAlert.",
+ "fields": [
+ {
+ "name": "edges",
+ "description": "A list of edges.",
+ "args": [
+
+ ],
+ "type": {
+ "kind": "LIST",
+ "name": null,
+ "ofType": {
+ "kind": "OBJECT",
+ "name": "AlertManagementAlertEdge",
+ "ofType": null
+ }
+ },
+ "isDeprecated": false,
+ "deprecationReason": null
+ },
+ {
+ "name": "nodes",
+ "description": "A list of nodes.",
+ "args": [
+
+ ],
+ "type": {
+ "kind": "LIST",
+ "name": null,
+ "ofType": {
+ "kind": "OBJECT",
+ "name": "AlertManagementAlert",
+ "ofType": null
+ }
+ },
+ "isDeprecated": false,
+ "deprecationReason": null
+ },
+ {
+ "name": "pageInfo",
+ "description": "Information to aid in pagination.",
+ "args": [
+
+ ],
+ "type": {
+ "kind": "NON_NULL",
+ "name": null,
+ "ofType": {
+ "kind": "OBJECT",
+ "name": "PageInfo",
+ "ofType": null
+ }
+ },
+ "isDeprecated": false,
+ "deprecationReason": null
+ }
+ ],
+ "inputFields": null,
+ "interfaces": [
+
+ ],
+ "enumValues": null,
+ "possibleTypes": null
+ },
+ {
+ "kind": "OBJECT",
+ "name": "AlertManagementAlertEdge",
+ "description": "An edge in a connection.",
+ "fields": [
+ {
+ "name": "cursor",
+ "description": "A cursor for use in pagination.",
+ "args": [
+
+ ],
+ "type": {
+ "kind": "NON_NULL",
+ "name": null,
+ "ofType": {
+ "kind": "SCALAR",
+ "name": "String",
+ "ofType": null
+ }
+ },
+ "isDeprecated": false,
+ "deprecationReason": null
+ },
+ {
+ "name": "node",
+ "description": "The item at the end of the edge.",
+ "args": [
+
+ ],
+ "type": {
+ "kind": "OBJECT",
+ "name": "AlertManagementAlert",
+ "ofType": null
+ },
+ "isDeprecated": false,
+ "deprecationReason": null
+ }
+ ],
+ "inputFields": null,
+ "interfaces": [
+
+ ],
+ "enumValues": null,
+ "possibleTypes": null
+ },
+ {
+ "kind": "ENUM",
+ "name": "AlertManagementAlertSort",
+ "description": "Values for sorting alerts",
+ "fields": null,
+ "inputFields": null,
+ "interfaces": null,
+ "enumValues": [
+ {
+ "name": "updated_desc",
+ "description": "Updated at descending order",
+ "isDeprecated": false,
+ "deprecationReason": null
+ },
+ {
+ "name": "updated_asc",
+ "description": "Updated at ascending order",
+ "isDeprecated": false,
+ "deprecationReason": null
+ },
+ {
+ "name": "created_desc",
+ "description": "Created at descending order",
+ "isDeprecated": false,
+ "deprecationReason": null
+ },
+ {
+ "name": "created_asc",
+ "description": "Created at ascending order",
+ "isDeprecated": false,
+ "deprecationReason": null
+ },
+ {
+ "name": "START_TIME_ASC",
+ "description": "Start time by ascending order",
+ "isDeprecated": false,
+ "deprecationReason": null
+ },
+ {
+ "name": "START_TIME_DESC",
+ "description": "Start time by descending order",
+ "isDeprecated": false,
+ "deprecationReason": null
+ },
+ {
+ "name": "END_TIME_ASC",
+ "description": "End time by ascending order",
+ "isDeprecated": false,
+ "deprecationReason": null
+ },
+ {
+ "name": "END_TIME_DESC",
+ "description": "End time by descending order",
+ "isDeprecated": false,
+ "deprecationReason": null
+ },
+ {
+ "name": "CREATED_TIME_ASC",
+ "description": "Created time by ascending order",
+ "isDeprecated": false,
+ "deprecationReason": null
+ },
+ {
+ "name": "CREATED_TIME_DESC",
+ "description": "Created time by descending order",
+ "isDeprecated": false,
+ "deprecationReason": null
+ },
+ {
+ "name": "UPDATED_TIME_ASC",
+ "description": "Created time by ascending order",
+ "isDeprecated": false,
+ "deprecationReason": null
+ },
+ {
+ "name": "UPDATED_TIME_DESC",
+ "description": "Created time by descending order",
+ "isDeprecated": false,
+ "deprecationReason": null
+ },
+ {
+ "name": "EVENTS_COUNT_ASC",
+ "description": "Events count by ascending order",
+ "isDeprecated": false,
+ "deprecationReason": null
+ },
+ {
+ "name": "EVENTS_COUNT_DESC",
+ "description": "Events count by descending order",
+ "isDeprecated": false,
+ "deprecationReason": null
+ },
+ {
+ "name": "SEVERITY_ASC",
+ "description": "Severity by ascending order",
+ "isDeprecated": false,
+ "deprecationReason": null
+ },
+ {
+ "name": "SEVERITY_DESC",
+ "description": "Severity by descending order",
+ "isDeprecated": false,
+ "deprecationReason": null
+ },
+ {
+ "name": "STATUS_ASC",
+ "description": "Status by ascending order",
+ "isDeprecated": false,
+ "deprecationReason": null
+ },
+ {
+ "name": "STATUS_DESC",
+ "description": "Status by descending order",
+ "isDeprecated": false,
+ "deprecationReason": null
+ }
+ ],
+ "possibleTypes": null
+ },
+ {
+ "kind": "OBJECT",
+ "name": "AlertManagementAlertStatusCountsType",
+ "description": "Represents total number of alerts for the represented categories",
+ "fields": [
+ {
+ "name": "acknowledged",
+ "description": "Number of alerts with status ACKNOWLEDGED for the project",
+ "args": [
+
+ ],
+ "type": {
+ "kind": "SCALAR",
+ "name": "Int",
+ "ofType": null
+ },
+ "isDeprecated": false,
+ "deprecationReason": null
+ },
+ {
+ "name": "all",
+ "description": "Total number of alerts for the project",
+ "args": [
+
+ ],
+ "type": {
+ "kind": "SCALAR",
+ "name": "Int",
+ "ofType": null
+ },
+ "isDeprecated": false,
+ "deprecationReason": null
+ },
+ {
+ "name": "ignored",
+ "description": "Number of alerts with status IGNORED for the project",
+ "args": [
+
+ ],
+ "type": {
+ "kind": "SCALAR",
+ "name": "Int",
+ "ofType": null
+ },
+ "isDeprecated": false,
+ "deprecationReason": null
+ },
+ {
+ "name": "open",
+ "description": "Number of alerts with status TRIGGERED or ACKNOWLEDGED for the project",
+ "args": [
+
+ ],
+ "type": {
+ "kind": "SCALAR",
+ "name": "Int",
+ "ofType": null
+ },
+ "isDeprecated": false,
+ "deprecationReason": null
+ },
+ {
+ "name": "resolved",
+ "description": "Number of alerts with status RESOLVED for the project",
+ "args": [
+
+ ],
+ "type": {
+ "kind": "SCALAR",
+ "name": "Int",
+ "ofType": null
+ },
+ "isDeprecated": false,
+ "deprecationReason": null
+ },
+ {
+ "name": "triggered",
+ "description": "Number of alerts with status TRIGGERED for the project",
+ "args": [
+
+ ],
+ "type": {
+ "kind": "SCALAR",
+ "name": "Int",
+ "ofType": null
+ },
+ "isDeprecated": false,
+ "deprecationReason": null
+ }
+ ],
+ "inputFields": null,
+ "interfaces": [
+
+ ],
+ "enumValues": null,
+ "possibleTypes": null
+ },
+ {
+ "kind": "ENUM",
+ "name": "AlertManagementSeverity",
+ "description": "Alert severity values",
+ "fields": null,
+ "inputFields": null,
+ "interfaces": null,
+ "enumValues": [
+ {
+ "name": "CRITICAL",
+ "description": "Critical severity",
+ "isDeprecated": false,
+ "deprecationReason": null
+ },
+ {
+ "name": "HIGH",
+ "description": "High severity",
+ "isDeprecated": false,
+ "deprecationReason": null
+ },
+ {
+ "name": "MEDIUM",
+ "description": "Medium severity",
+ "isDeprecated": false,
+ "deprecationReason": null
+ },
+ {
+ "name": "LOW",
+ "description": "Low severity",
+ "isDeprecated": false,
+ "deprecationReason": null
+ },
+ {
+ "name": "INFO",
+ "description": "Info severity",
+ "isDeprecated": false,
+ "deprecationReason": null
+ },
+ {
+ "name": "UNKNOWN",
+ "description": "Unknown severity",
+ "isDeprecated": false,
+ "deprecationReason": null
+ }
+ ],
+ "possibleTypes": null
+ },
+ {
+ "kind": "ENUM",
+ "name": "AlertManagementStatus",
+ "description": "Alert status values",
+ "fields": null,
+ "inputFields": null,
+ "interfaces": null,
+ "enumValues": [
+ {
+ "name": "TRIGGERED",
+ "description": "Triggered status",
+ "isDeprecated": false,
+ "deprecationReason": null
+ },
+ {
+ "name": "ACKNOWLEDGED",
+ "description": "Acknowledged status",
+ "isDeprecated": false,
+ "deprecationReason": null
+ },
+ {
+ "name": "RESOLVED",
+ "description": "Resolved status",
+ "isDeprecated": false,
+ "deprecationReason": null
+ },
+ {
+ "name": "IGNORED",
+ "description": "Ignored status",
+ "isDeprecated": false,
+ "deprecationReason": null
+ }
+ ],
+ "possibleTypes": null
+ },
+ {
+ "kind": "OBJECT",
"name": "AwardEmoji",
"description": "An emoji awarded by a user.",
"fields": [
@@ -771,6 +1518,59 @@
"deprecationReason": null
},
{
+ "name": "lists",
+ "description": "Lists of the project board",
+ "args": [
+ {
+ "name": "after",
+ "description": "Returns the elements in the list that come after the specified cursor.",
+ "type": {
+ "kind": "SCALAR",
+ "name": "String",
+ "ofType": null
+ },
+ "defaultValue": null
+ },
+ {
+ "name": "before",
+ "description": "Returns the elements in the list that come before the specified cursor.",
+ "type": {
+ "kind": "SCALAR",
+ "name": "String",
+ "ofType": null
+ },
+ "defaultValue": null
+ },
+ {
+ "name": "first",
+ "description": "Returns the first _n_ elements from the list.",
+ "type": {
+ "kind": "SCALAR",
+ "name": "Int",
+ "ofType": null
+ },
+ "defaultValue": null
+ },
+ {
+ "name": "last",
+ "description": "Returns the last _n_ elements from the list.",
+ "type": {
+ "kind": "SCALAR",
+ "name": "Int",
+ "ofType": null
+ },
+ "defaultValue": null
+ }
+ ],
+ "type": {
+ "kind": "OBJECT",
+ "name": "BoardListConnection",
+ "ofType": null
+ },
+ "isDeprecated": false,
+ "deprecationReason": null
+ },
+ {
"name": "name",
"description": "Name of the board",
"args": [
@@ -919,6 +1719,429 @@
"possibleTypes": null
},
{
+ "kind": "OBJECT",
+ "name": "BoardList",
+ "description": "Represents a list for an issue board",
+ "fields": [
+ {
+ "name": "assignee",
+ "description": "Assignee in the list",
+ "args": [
+
+ ],
+ "type": {
+ "kind": "OBJECT",
+ "name": "User",
+ "ofType": null
+ },
+ "isDeprecated": false,
+ "deprecationReason": null
+ },
+ {
+ "name": "collapsed",
+ "description": "Indicates if list is collapsed for this user",
+ "args": [
+
+ ],
+ "type": {
+ "kind": "SCALAR",
+ "name": "Boolean",
+ "ofType": null
+ },
+ "isDeprecated": false,
+ "deprecationReason": null
+ },
+ {
+ "name": "id",
+ "description": "ID (global ID) of the list",
+ "args": [
+
+ ],
+ "type": {
+ "kind": "NON_NULL",
+ "name": null,
+ "ofType": {
+ "kind": "SCALAR",
+ "name": "ID",
+ "ofType": null
+ }
+ },
+ "isDeprecated": false,
+ "deprecationReason": null
+ },
+ {
+ "name": "label",
+ "description": "Label of the list",
+ "args": [
+
+ ],
+ "type": {
+ "kind": "OBJECT",
+ "name": "Label",
+ "ofType": null
+ },
+ "isDeprecated": false,
+ "deprecationReason": null
+ },
+ {
+ "name": "limitMetric",
+ "description": "The current limit metric for the list",
+ "args": [
+
+ ],
+ "type": {
+ "kind": "ENUM",
+ "name": "ListLimitMetric",
+ "ofType": null
+ },
+ "isDeprecated": false,
+ "deprecationReason": null
+ },
+ {
+ "name": "listType",
+ "description": "Type of the list",
+ "args": [
+
+ ],
+ "type": {
+ "kind": "NON_NULL",
+ "name": null,
+ "ofType": {
+ "kind": "SCALAR",
+ "name": "String",
+ "ofType": null
+ }
+ },
+ "isDeprecated": false,
+ "deprecationReason": null
+ },
+ {
+ "name": "maxIssueCount",
+ "description": "Maximum number of issues in the list",
+ "args": [
+
+ ],
+ "type": {
+ "kind": "SCALAR",
+ "name": "Int",
+ "ofType": null
+ },
+ "isDeprecated": false,
+ "deprecationReason": null
+ },
+ {
+ "name": "maxIssueWeight",
+ "description": "Maximum weight of issues in the list",
+ "args": [
+
+ ],
+ "type": {
+ "kind": "SCALAR",
+ "name": "Int",
+ "ofType": null
+ },
+ "isDeprecated": false,
+ "deprecationReason": null
+ },
+ {
+ "name": "milestone",
+ "description": "Milestone of the list",
+ "args": [
+
+ ],
+ "type": {
+ "kind": "OBJECT",
+ "name": "Milestone",
+ "ofType": null
+ },
+ "isDeprecated": false,
+ "deprecationReason": null
+ },
+ {
+ "name": "position",
+ "description": "Position of list within the board",
+ "args": [
+
+ ],
+ "type": {
+ "kind": "SCALAR",
+ "name": "Int",
+ "ofType": null
+ },
+ "isDeprecated": false,
+ "deprecationReason": null
+ },
+ {
+ "name": "title",
+ "description": "Title of the list",
+ "args": [
+
+ ],
+ "type": {
+ "kind": "NON_NULL",
+ "name": null,
+ "ofType": {
+ "kind": "SCALAR",
+ "name": "String",
+ "ofType": null
+ }
+ },
+ "isDeprecated": false,
+ "deprecationReason": null
+ }
+ ],
+ "inputFields": null,
+ "interfaces": [
+
+ ],
+ "enumValues": null,
+ "possibleTypes": null
+ },
+ {
+ "kind": "OBJECT",
+ "name": "BoardListConnection",
+ "description": "The connection type for BoardList.",
+ "fields": [
+ {
+ "name": "edges",
+ "description": "A list of edges.",
+ "args": [
+
+ ],
+ "type": {
+ "kind": "LIST",
+ "name": null,
+ "ofType": {
+ "kind": "OBJECT",
+ "name": "BoardListEdge",
+ "ofType": null
+ }
+ },
+ "isDeprecated": false,
+ "deprecationReason": null
+ },
+ {
+ "name": "nodes",
+ "description": "A list of nodes.",
+ "args": [
+
+ ],
+ "type": {
+ "kind": "LIST",
+ "name": null,
+ "ofType": {
+ "kind": "OBJECT",
+ "name": "BoardList",
+ "ofType": null
+ }
+ },
+ "isDeprecated": false,
+ "deprecationReason": null
+ },
+ {
+ "name": "pageInfo",
+ "description": "Information to aid in pagination.",
+ "args": [
+
+ ],
+ "type": {
+ "kind": "NON_NULL",
+ "name": null,
+ "ofType": {
+ "kind": "OBJECT",
+ "name": "PageInfo",
+ "ofType": null
+ }
+ },
+ "isDeprecated": false,
+ "deprecationReason": null
+ }
+ ],
+ "inputFields": null,
+ "interfaces": [
+
+ ],
+ "enumValues": null,
+ "possibleTypes": null
+ },
+ {
+ "kind": "OBJECT",
+ "name": "BoardListEdge",
+ "description": "An edge in a connection.",
+ "fields": [
+ {
+ "name": "cursor",
+ "description": "A cursor for use in pagination.",
+ "args": [
+
+ ],
+ "type": {
+ "kind": "NON_NULL",
+ "name": null,
+ "ofType": {
+ "kind": "SCALAR",
+ "name": "String",
+ "ofType": null
+ }
+ },
+ "isDeprecated": false,
+ "deprecationReason": null
+ },
+ {
+ "name": "node",
+ "description": "The item at the end of the edge.",
+ "args": [
+
+ ],
+ "type": {
+ "kind": "OBJECT",
+ "name": "BoardList",
+ "ofType": null
+ },
+ "isDeprecated": false,
+ "deprecationReason": null
+ }
+ ],
+ "inputFields": null,
+ "interfaces": [
+
+ ],
+ "enumValues": null,
+ "possibleTypes": null
+ },
+ {
+ "kind": "INPUT_OBJECT",
+ "name": "BoardListUpdateLimitMetricsInput",
+ "description": "Autogenerated input type of BoardListUpdateLimitMetrics",
+ "fields": null,
+ "inputFields": [
+ {
+ "name": "listId",
+ "description": "The global ID of the list.",
+ "type": {
+ "kind": "NON_NULL",
+ "name": null,
+ "ofType": {
+ "kind": "SCALAR",
+ "name": "ID",
+ "ofType": null
+ }
+ },
+ "defaultValue": null
+ },
+ {
+ "name": "limitMetric",
+ "description": "The new limit metric type for the list.",
+ "type": {
+ "kind": "ENUM",
+ "name": "ListLimitMetric",
+ "ofType": null
+ },
+ "defaultValue": null
+ },
+ {
+ "name": "maxIssueCount",
+ "description": "The new maximum issue count limit.",
+ "type": {
+ "kind": "SCALAR",
+ "name": "Int",
+ "ofType": null
+ },
+ "defaultValue": null
+ },
+ {
+ "name": "maxIssueWeight",
+ "description": "The new maximum issue weight limit.",
+ "type": {
+ "kind": "SCALAR",
+ "name": "Int",
+ "ofType": null
+ },
+ "defaultValue": null
+ },
+ {
+ "name": "clientMutationId",
+ "description": "A unique identifier for the client performing the mutation.",
+ "type": {
+ "kind": "SCALAR",
+ "name": "String",
+ "ofType": null
+ },
+ "defaultValue": null
+ }
+ ],
+ "interfaces": null,
+ "enumValues": null,
+ "possibleTypes": null
+ },
+ {
+ "kind": "OBJECT",
+ "name": "BoardListUpdateLimitMetricsPayload",
+ "description": "Autogenerated return type of BoardListUpdateLimitMetrics",
+ "fields": [
+ {
+ "name": "clientMutationId",
+ "description": "A unique identifier for the client performing the mutation.",
+ "args": [
+
+ ],
+ "type": {
+ "kind": "SCALAR",
+ "name": "String",
+ "ofType": null
+ },
+ "isDeprecated": false,
+ "deprecationReason": null
+ },
+ {
+ "name": "errors",
+ "description": "Errors encountered during execution of the mutation.",
+ "args": [
+
+ ],
+ "type": {
+ "kind": "NON_NULL",
+ "name": null,
+ "ofType": {
+ "kind": "LIST",
+ "name": null,
+ "ofType": {
+ "kind": "NON_NULL",
+ "name": null,
+ "ofType": {
+ "kind": "SCALAR",
+ "name": "String",
+ "ofType": null
+ }
+ }
+ }
+ },
+ "isDeprecated": false,
+ "deprecationReason": null
+ },
+ {
+ "name": "list",
+ "description": "The updated list",
+ "args": [
+
+ ],
+ "type": {
+ "kind": "OBJECT",
+ "name": "BoardList",
+ "ofType": null
+ },
+ "isDeprecated": false,
+ "deprecationReason": null
+ }
+ ],
+ "inputFields": null,
+ "interfaces": [
+
+ ],
+ "enumValues": null,
+ "possibleTypes": null
+ },
+ {
"kind": "SCALAR",
"name": "Boolean",
"description": "Represents `true` or `false` values.",
@@ -930,6 +2153,51 @@
},
{
"kind": "OBJECT",
+ "name": "Branch",
+ "description": null,
+ "fields": [
+ {
+ "name": "commit",
+ "description": "Commit for the branch",
+ "args": [
+
+ ],
+ "type": {
+ "kind": "OBJECT",
+ "name": "Commit",
+ "ofType": null
+ },
+ "isDeprecated": false,
+ "deprecationReason": null
+ },
+ {
+ "name": "name",
+ "description": "Name of the branch",
+ "args": [
+
+ ],
+ "type": {
+ "kind": "NON_NULL",
+ "name": null,
+ "ofType": {
+ "kind": "SCALAR",
+ "name": "String",
+ "ofType": null
+ }
+ },
+ "isDeprecated": false,
+ "deprecationReason": null
+ }
+ ],
+ "inputFields": null,
+ "interfaces": [
+
+ ],
+ "enumValues": null,
+ "possibleTypes": null
+ },
+ {
+ "kind": "OBJECT",
"name": "Commit",
"description": null,
"fields": [
@@ -1208,6 +2476,20 @@
"deprecationReason": null
},
{
+ "name": "titleHtml",
+ "description": "The GitLab Flavored Markdown rendering of `title`",
+ "args": [
+
+ ],
+ "type": {
+ "kind": "SCALAR",
+ "name": "String",
+ "ofType": null
+ },
+ "isDeprecated": false,
+ "deprecationReason": null
+ },
+ {
"name": "webUrl",
"description": "Web URL of the commit",
"args": [
@@ -1235,6 +2517,426 @@
},
{
"kind": "INPUT_OBJECT",
+ "name": "CreateAlertIssueInput",
+ "description": "Autogenerated input type of CreateAlertIssue",
+ "fields": null,
+ "inputFields": [
+ {
+ "name": "projectPath",
+ "description": "The project the alert to mutate is in",
+ "type": {
+ "kind": "NON_NULL",
+ "name": null,
+ "ofType": {
+ "kind": "SCALAR",
+ "name": "ID",
+ "ofType": null
+ }
+ },
+ "defaultValue": null
+ },
+ {
+ "name": "iid",
+ "description": "The iid of the alert to mutate",
+ "type": {
+ "kind": "NON_NULL",
+ "name": null,
+ "ofType": {
+ "kind": "SCALAR",
+ "name": "String",
+ "ofType": null
+ }
+ },
+ "defaultValue": null
+ },
+ {
+ "name": "clientMutationId",
+ "description": "A unique identifier for the client performing the mutation.",
+ "type": {
+ "kind": "SCALAR",
+ "name": "String",
+ "ofType": null
+ },
+ "defaultValue": null
+ }
+ ],
+ "interfaces": null,
+ "enumValues": null,
+ "possibleTypes": null
+ },
+ {
+ "kind": "OBJECT",
+ "name": "CreateAlertIssuePayload",
+ "description": "Autogenerated return type of CreateAlertIssue",
+ "fields": [
+ {
+ "name": "alert",
+ "description": "The alert after mutation",
+ "args": [
+
+ ],
+ "type": {
+ "kind": "OBJECT",
+ "name": "AlertManagementAlert",
+ "ofType": null
+ },
+ "isDeprecated": false,
+ "deprecationReason": null
+ },
+ {
+ "name": "clientMutationId",
+ "description": "A unique identifier for the client performing the mutation.",
+ "args": [
+
+ ],
+ "type": {
+ "kind": "SCALAR",
+ "name": "String",
+ "ofType": null
+ },
+ "isDeprecated": false,
+ "deprecationReason": null
+ },
+ {
+ "name": "errors",
+ "description": "Errors encountered during execution of the mutation.",
+ "args": [
+
+ ],
+ "type": {
+ "kind": "NON_NULL",
+ "name": null,
+ "ofType": {
+ "kind": "LIST",
+ "name": null,
+ "ofType": {
+ "kind": "NON_NULL",
+ "name": null,
+ "ofType": {
+ "kind": "SCALAR",
+ "name": "String",
+ "ofType": null
+ }
+ }
+ }
+ },
+ "isDeprecated": false,
+ "deprecationReason": null
+ },
+ {
+ "name": "issue",
+ "description": "The issue created after mutation",
+ "args": [
+
+ ],
+ "type": {
+ "kind": "OBJECT",
+ "name": "Issue",
+ "ofType": null
+ },
+ "isDeprecated": false,
+ "deprecationReason": null
+ }
+ ],
+ "inputFields": null,
+ "interfaces": [
+
+ ],
+ "enumValues": null,
+ "possibleTypes": null
+ },
+ {
+ "kind": "INPUT_OBJECT",
+ "name": "CreateAnnotationInput",
+ "description": "Autogenerated input type of CreateAnnotation",
+ "fields": null,
+ "inputFields": [
+ {
+ "name": "environmentId",
+ "description": "The global id of the environment to add an annotation to",
+ "type": {
+ "kind": "SCALAR",
+ "name": "ID",
+ "ofType": null
+ },
+ "defaultValue": null
+ },
+ {
+ "name": "clusterId",
+ "description": "The global id of the cluster to add an annotation to",
+ "type": {
+ "kind": "SCALAR",
+ "name": "ID",
+ "ofType": null
+ },
+ "defaultValue": null
+ },
+ {
+ "name": "startingAt",
+ "description": "Timestamp indicating starting moment to which the annotation relates",
+ "type": {
+ "kind": "NON_NULL",
+ "name": null,
+ "ofType": {
+ "kind": "SCALAR",
+ "name": "Time",
+ "ofType": null
+ }
+ },
+ "defaultValue": null
+ },
+ {
+ "name": "endingAt",
+ "description": "Timestamp indicating ending moment to which the annotation relates",
+ "type": {
+ "kind": "SCALAR",
+ "name": "Time",
+ "ofType": null
+ },
+ "defaultValue": null
+ },
+ {
+ "name": "dashboardPath",
+ "description": "The path to a file defining the dashboard on which the annotation should be added",
+ "type": {
+ "kind": "NON_NULL",
+ "name": null,
+ "ofType": {
+ "kind": "SCALAR",
+ "name": "String",
+ "ofType": null
+ }
+ },
+ "defaultValue": null
+ },
+ {
+ "name": "description",
+ "description": "The description of the annotation",
+ "type": {
+ "kind": "NON_NULL",
+ "name": null,
+ "ofType": {
+ "kind": "SCALAR",
+ "name": "String",
+ "ofType": null
+ }
+ },
+ "defaultValue": null
+ },
+ {
+ "name": "clientMutationId",
+ "description": "A unique identifier for the client performing the mutation.",
+ "type": {
+ "kind": "SCALAR",
+ "name": "String",
+ "ofType": null
+ },
+ "defaultValue": null
+ }
+ ],
+ "interfaces": null,
+ "enumValues": null,
+ "possibleTypes": null
+ },
+ {
+ "kind": "OBJECT",
+ "name": "CreateAnnotationPayload",
+ "description": "Autogenerated return type of CreateAnnotation",
+ "fields": [
+ {
+ "name": "annotation",
+ "description": "The created annotation",
+ "args": [
+
+ ],
+ "type": {
+ "kind": "OBJECT",
+ "name": "MetricsDashboardAnnotation",
+ "ofType": null
+ },
+ "isDeprecated": false,
+ "deprecationReason": null
+ },
+ {
+ "name": "clientMutationId",
+ "description": "A unique identifier for the client performing the mutation.",
+ "args": [
+
+ ],
+ "type": {
+ "kind": "SCALAR",
+ "name": "String",
+ "ofType": null
+ },
+ "isDeprecated": false,
+ "deprecationReason": null
+ },
+ {
+ "name": "errors",
+ "description": "Errors encountered during execution of the mutation.",
+ "args": [
+
+ ],
+ "type": {
+ "kind": "NON_NULL",
+ "name": null,
+ "ofType": {
+ "kind": "LIST",
+ "name": null,
+ "ofType": {
+ "kind": "NON_NULL",
+ "name": null,
+ "ofType": {
+ "kind": "SCALAR",
+ "name": "String",
+ "ofType": null
+ }
+ }
+ }
+ },
+ "isDeprecated": false,
+ "deprecationReason": null
+ }
+ ],
+ "inputFields": null,
+ "interfaces": [
+
+ ],
+ "enumValues": null,
+ "possibleTypes": null
+ },
+ {
+ "kind": "INPUT_OBJECT",
+ "name": "CreateBranchInput",
+ "description": "Autogenerated input type of CreateBranch",
+ "fields": null,
+ "inputFields": [
+ {
+ "name": "projectPath",
+ "description": "Project full path the branch is associated with",
+ "type": {
+ "kind": "NON_NULL",
+ "name": null,
+ "ofType": {
+ "kind": "SCALAR",
+ "name": "ID",
+ "ofType": null
+ }
+ },
+ "defaultValue": null
+ },
+ {
+ "name": "name",
+ "description": "Name of the branch",
+ "type": {
+ "kind": "NON_NULL",
+ "name": null,
+ "ofType": {
+ "kind": "SCALAR",
+ "name": "String",
+ "ofType": null
+ }
+ },
+ "defaultValue": null
+ },
+ {
+ "name": "ref",
+ "description": "Branch name or commit SHA to create branch from",
+ "type": {
+ "kind": "NON_NULL",
+ "name": null,
+ "ofType": {
+ "kind": "SCALAR",
+ "name": "String",
+ "ofType": null
+ }
+ },
+ "defaultValue": null
+ },
+ {
+ "name": "clientMutationId",
+ "description": "A unique identifier for the client performing the mutation.",
+ "type": {
+ "kind": "SCALAR",
+ "name": "String",
+ "ofType": null
+ },
+ "defaultValue": null
+ }
+ ],
+ "interfaces": null,
+ "enumValues": null,
+ "possibleTypes": null
+ },
+ {
+ "kind": "OBJECT",
+ "name": "CreateBranchPayload",
+ "description": "Autogenerated return type of CreateBranch",
+ "fields": [
+ {
+ "name": "branch",
+ "description": "Branch after mutation",
+ "args": [
+
+ ],
+ "type": {
+ "kind": "OBJECT",
+ "name": "Branch",
+ "ofType": null
+ },
+ "isDeprecated": false,
+ "deprecationReason": null
+ },
+ {
+ "name": "clientMutationId",
+ "description": "A unique identifier for the client performing the mutation.",
+ "args": [
+
+ ],
+ "type": {
+ "kind": "SCALAR",
+ "name": "String",
+ "ofType": null
+ },
+ "isDeprecated": false,
+ "deprecationReason": null
+ },
+ {
+ "name": "errors",
+ "description": "Errors encountered during execution of the mutation.",
+ "args": [
+
+ ],
+ "type": {
+ "kind": "NON_NULL",
+ "name": null,
+ "ofType": {
+ "kind": "LIST",
+ "name": null,
+ "ofType": {
+ "kind": "NON_NULL",
+ "name": null,
+ "ofType": {
+ "kind": "SCALAR",
+ "name": "String",
+ "ofType": null
+ }
+ }
+ }
+ },
+ "isDeprecated": false,
+ "deprecationReason": null
+ }
+ ],
+ "inputFields": null,
+ "interfaces": [
+
+ ],
+ "enumValues": null,
+ "possibleTypes": null
+ },
+ {
+ "kind": "INPUT_OBJECT",
"name": "CreateDiffNoteInput",
"description": "Autogenerated input type of CreateDiffNote",
"fields": null,
@@ -1317,7 +3019,7 @@
},
{
"name": "errors",
- "description": "Reasons why the mutation failed.",
+ "description": "Errors encountered during execution of the mutation.",
"args": [
],
@@ -1539,7 +3241,7 @@
},
{
"name": "errors",
- "description": "Reasons why the mutation failed.",
+ "description": "Errors encountered during execution of the mutation.",
"args": [
],
@@ -1655,7 +3357,7 @@
},
{
"name": "errors",
- "description": "Reasons why the mutation failed.",
+ "description": "Errors encountered during execution of the mutation.",
"args": [
],
@@ -1703,6 +3405,148 @@
},
{
"kind": "INPUT_OBJECT",
+ "name": "CreateIterationInput",
+ "description": "Autogenerated input type of CreateIteration",
+ "fields": null,
+ "inputFields": [
+ {
+ "name": "groupPath",
+ "description": "The target group for the iteration",
+ "type": {
+ "kind": "NON_NULL",
+ "name": null,
+ "ofType": {
+ "kind": "SCALAR",
+ "name": "ID",
+ "ofType": null
+ }
+ },
+ "defaultValue": null
+ },
+ {
+ "name": "title",
+ "description": "The title of the iteration",
+ "type": {
+ "kind": "SCALAR",
+ "name": "String",
+ "ofType": null
+ },
+ "defaultValue": null
+ },
+ {
+ "name": "description",
+ "description": "The description of the iteration",
+ "type": {
+ "kind": "SCALAR",
+ "name": "String",
+ "ofType": null
+ },
+ "defaultValue": null
+ },
+ {
+ "name": "startDate",
+ "description": "The start date of the iteration",
+ "type": {
+ "kind": "SCALAR",
+ "name": "String",
+ "ofType": null
+ },
+ "defaultValue": null
+ },
+ {
+ "name": "dueDate",
+ "description": "The end date of the iteration",
+ "type": {
+ "kind": "SCALAR",
+ "name": "String",
+ "ofType": null
+ },
+ "defaultValue": null
+ },
+ {
+ "name": "clientMutationId",
+ "description": "A unique identifier for the client performing the mutation.",
+ "type": {
+ "kind": "SCALAR",
+ "name": "String",
+ "ofType": null
+ },
+ "defaultValue": null
+ }
+ ],
+ "interfaces": null,
+ "enumValues": null,
+ "possibleTypes": null
+ },
+ {
+ "kind": "OBJECT",
+ "name": "CreateIterationPayload",
+ "description": "Autogenerated return type of CreateIteration",
+ "fields": [
+ {
+ "name": "clientMutationId",
+ "description": "A unique identifier for the client performing the mutation.",
+ "args": [
+
+ ],
+ "type": {
+ "kind": "SCALAR",
+ "name": "String",
+ "ofType": null
+ },
+ "isDeprecated": false,
+ "deprecationReason": null
+ },
+ {
+ "name": "errors",
+ "description": "Errors encountered during execution of the mutation.",
+ "args": [
+
+ ],
+ "type": {
+ "kind": "NON_NULL",
+ "name": null,
+ "ofType": {
+ "kind": "LIST",
+ "name": null,
+ "ofType": {
+ "kind": "NON_NULL",
+ "name": null,
+ "ofType": {
+ "kind": "SCALAR",
+ "name": "String",
+ "ofType": null
+ }
+ }
+ }
+ },
+ "isDeprecated": false,
+ "deprecationReason": null
+ },
+ {
+ "name": "iteration",
+ "description": "The created iteration",
+ "args": [
+
+ ],
+ "type": {
+ "kind": "OBJECT",
+ "name": "Iteration",
+ "ofType": null
+ },
+ "isDeprecated": false,
+ "deprecationReason": null
+ }
+ ],
+ "inputFields": null,
+ "interfaces": [
+
+ ],
+ "enumValues": null,
+ "possibleTypes": null
+ },
+ {
+ "kind": "INPUT_OBJECT",
"name": "CreateNoteInput",
"description": "Autogenerated input type of CreateNote",
"fields": null,
@@ -1781,7 +3625,7 @@
},
{
"name": "errors",
- "description": "Reasons why the mutation failed.",
+ "description": "Errors encountered during execution of the mutation.",
"args": [
],
@@ -1897,7 +3741,7 @@
},
{
"name": "errors",
- "description": "Reasons why the mutation failed.",
+ "description": "Errors encountered during execution of the mutation.",
"args": [
],
@@ -2022,6 +3866,24 @@
"defaultValue": null
},
{
+ "name": "uploadedFiles",
+ "description": "The paths to files uploaded in the snippet description",
+ "type": {
+ "kind": "LIST",
+ "name": null,
+ "ofType": {
+ "kind": "NON_NULL",
+ "name": null,
+ "ofType": {
+ "kind": "SCALAR",
+ "name": "String",
+ "ofType": null
+ }
+ }
+ },
+ "defaultValue": null
+ },
+ {
"name": "clientMutationId",
"description": "A unique identifier for the client performing the mutation.",
"type": {
@@ -2057,7 +3919,7 @@
},
{
"name": "errors",
- "description": "Reasons why the mutation failed.",
+ "description": "Errors encountered during execution of the mutation.",
"args": [
],
@@ -3679,7 +5541,7 @@
},
{
"name": "errors",
- "description": "Reasons why the mutation failed.",
+ "description": "Errors encountered during execution of the mutation.",
"args": [
],
@@ -3843,7 +5705,7 @@
},
{
"name": "errors",
- "description": "Reasons why the mutation failed.",
+ "description": "Errors encountered during execution of the mutation.",
"args": [
],
@@ -4350,7 +6212,7 @@
},
{
"name": "errors",
- "description": "Reasons why the mutation failed.",
+ "description": "Errors encountered during execution of the mutation.",
"args": [
],
@@ -4452,7 +6314,7 @@
},
{
"name": "errors",
- "description": "Reasons why the mutation failed.",
+ "description": "Errors encountered during execution of the mutation.",
"args": [
],
@@ -5459,7 +7321,7 @@
},
{
"name": "errors",
- "description": "Reasons why the mutation failed.",
+ "description": "Errors encountered during execution of the mutation.",
"args": [
],
@@ -7019,7 +8881,7 @@
},
{
"name": "errors",
- "description": "Reasons why the mutation failed.",
+ "description": "Errors encountered during execution of the mutation.",
"args": [
],
@@ -7696,6 +9558,20 @@
"deprecationReason": null
},
{
+ "name": "iteration",
+ "description": "Iteration of the issue",
+ "args": [
+
+ ],
+ "type": {
+ "kind": "OBJECT",
+ "name": "Iteration",
+ "ofType": null
+ },
+ "isDeprecated": false,
+ "deprecationReason": null
+ },
+ {
"name": "labels",
"description": "Labels of the issue",
"args": [
@@ -8550,7 +10426,7 @@
},
{
"name": "errors",
- "description": "Reasons why the mutation failed.",
+ "description": "Errors encountered during execution of the mutation.",
"args": [
],
@@ -8693,13 +10569,9 @@
"name": "adjacentReferenceId",
"description": "The id of the epic_issue or issue that the actual epic or issue is switched with",
"type": {
- "kind": "NON_NULL",
- "name": null,
- "ofType": {
- "kind": "SCALAR",
- "name": "ID",
- "ofType": null
- }
+ "kind": "SCALAR",
+ "name": "ID",
+ "ofType": null
},
"defaultValue": null
},
@@ -8707,13 +10579,9 @@
"name": "relativePosition",
"description": "The type of the switch, after or before allowed",
"type": {
- "kind": "NON_NULL",
- "name": null,
- "ofType": {
- "kind": "ENUM",
- "name": "MoveType",
- "ofType": null
- }
+ "kind": "ENUM",
+ "name": "MoveType",
+ "ofType": null
},
"defaultValue": null
},
@@ -8802,7 +10670,7 @@
},
{
"name": "errors",
- "description": "Reasons why the mutation failed.",
+ "description": "Errors encountered during execution of the mutation.",
"args": [
],
@@ -8952,6 +10820,77 @@
"deprecationReason": null
},
{
+ "name": "packageFileRegistries",
+ "description": "Package file registries of the GeoNode. Available only when feature flag `geo_self_service_framework` is enabled",
+ "args": [
+ {
+ "name": "ids",
+ "description": "Filters registries by their ID",
+ "type": {
+ "kind": "LIST",
+ "name": null,
+ "ofType": {
+ "kind": "NON_NULL",
+ "name": null,
+ "ofType": {
+ "kind": "SCALAR",
+ "name": "ID",
+ "ofType": null
+ }
+ }
+ },
+ "defaultValue": null
+ },
+ {
+ "name": "after",
+ "description": "Returns the elements in the list that come after the specified cursor.",
+ "type": {
+ "kind": "SCALAR",
+ "name": "String",
+ "ofType": null
+ },
+ "defaultValue": null
+ },
+ {
+ "name": "before",
+ "description": "Returns the elements in the list that come before the specified cursor.",
+ "type": {
+ "kind": "SCALAR",
+ "name": "String",
+ "ofType": null
+ },
+ "defaultValue": null
+ },
+ {
+ "name": "first",
+ "description": "Returns the first _n_ elements from the list.",
+ "type": {
+ "kind": "SCALAR",
+ "name": "Int",
+ "ofType": null
+ },
+ "defaultValue": null
+ },
+ {
+ "name": "last",
+ "description": "Returns the last _n_ elements from the list.",
+ "type": {
+ "kind": "SCALAR",
+ "name": "Int",
+ "ofType": null
+ },
+ "defaultValue": null
+ }
+ ],
+ "type": {
+ "kind": "OBJECT",
+ "name": "PackageFileRegistryConnection",
+ "ofType": null
+ },
+ "isDeprecated": false,
+ "deprecationReason": null
+ },
+ {
"name": "primary",
"description": "Indicates whether this Geo node is the primary",
"args": [
@@ -9161,7 +11100,7 @@
},
{
"name": "grafanaUrl",
- "description": "Url for the Grafana host for the Grafana integration",
+ "description": "URL for the Grafana host for the Grafana integration",
"args": [
],
@@ -10000,6 +11939,99 @@
"deprecationReason": null
},
{
+ "name": "iterations",
+ "description": "Find iterations",
+ "args": [
+ {
+ "name": "startDate",
+ "description": "List items within a time frame where items.start_date is between startDate and endDate parameters (endDate parameter must be present)",
+ "type": {
+ "kind": "SCALAR",
+ "name": "Time",
+ "ofType": null
+ },
+ "defaultValue": null
+ },
+ {
+ "name": "endDate",
+ "description": "List items within a time frame where items.end_date is between startDate and endDate parameters (startDate parameter must be present)",
+ "type": {
+ "kind": "SCALAR",
+ "name": "Time",
+ "ofType": null
+ },
+ "defaultValue": null
+ },
+ {
+ "name": "state",
+ "description": "Filter iterations by state",
+ "type": {
+ "kind": "ENUM",
+ "name": "IterationState",
+ "ofType": null
+ },
+ "defaultValue": null
+ },
+ {
+ "name": "title",
+ "description": "Fuzzy search by title",
+ "type": {
+ "kind": "SCALAR",
+ "name": "String",
+ "ofType": null
+ },
+ "defaultValue": null
+ },
+ {
+ "name": "after",
+ "description": "Returns the elements in the list that come after the specified cursor.",
+ "type": {
+ "kind": "SCALAR",
+ "name": "String",
+ "ofType": null
+ },
+ "defaultValue": null
+ },
+ {
+ "name": "before",
+ "description": "Returns the elements in the list that come before the specified cursor.",
+ "type": {
+ "kind": "SCALAR",
+ "name": "String",
+ "ofType": null
+ },
+ "defaultValue": null
+ },
+ {
+ "name": "first",
+ "description": "Returns the first _n_ elements from the list.",
+ "type": {
+ "kind": "SCALAR",
+ "name": "Int",
+ "ofType": null
+ },
+ "defaultValue": null
+ },
+ {
+ "name": "last",
+ "description": "Returns the last _n_ elements from the list.",
+ "type": {
+ "kind": "SCALAR",
+ "name": "Int",
+ "ofType": null
+ },
+ "defaultValue": null
+ }
+ ],
+ "type": {
+ "kind": "OBJECT",
+ "name": "IterationConnection",
+ "ofType": null
+ },
+ "isDeprecated": false,
+ "deprecationReason": null
+ },
+ {
"name": "lfsEnabled",
"description": "Indicates if Large File Storage (LFS) is enabled for namespace",
"args": [
@@ -10062,6 +12094,16 @@
"defaultValue": null
},
{
+ "name": "includeDescendants",
+ "description": "Return also milestones in all subgroups and subprojects",
+ "type": {
+ "kind": "SCALAR",
+ "name": "Boolean",
+ "ofType": null
+ },
+ "defaultValue": null
+ },
+ {
"name": "after",
"description": "Returns the elements in the list that come after the specified cursor.",
"type": {
@@ -10189,6 +12231,16 @@
"defaultValue": "false"
},
{
+ "name": "hasVulnerabilities",
+ "description": "Returns only the projects which have vulnerabilities",
+ "type": {
+ "kind": "SCALAR",
+ "name": "Boolean",
+ "ofType": null
+ },
+ "defaultValue": "false"
+ },
+ {
"name": "after",
"description": "Returns the elements in the list that come after the specified cursor.",
"type": {
@@ -10317,29 +12369,41 @@
"args": [
{
"name": "startDate",
- "description": "List time logs within a time range where the logged date is after start_date parameter.",
+ "description": "List time logs within a date range where the logged date is equal to or after startDate",
"type": {
- "kind": "NON_NULL",
- "name": null,
- "ofType": {
- "kind": "SCALAR",
- "name": "Time",
- "ofType": null
- }
+ "kind": "SCALAR",
+ "name": "Time",
+ "ofType": null
},
"defaultValue": null
},
{
"name": "endDate",
- "description": "List time logs within a time range where the logged date is before end_date parameter.",
+ "description": "List time logs within a date range where the logged date is equal to or before endDate",
"type": {
- "kind": "NON_NULL",
- "name": null,
- "ofType": {
- "kind": "SCALAR",
- "name": "Time",
- "ofType": null
- }
+ "kind": "SCALAR",
+ "name": "Time",
+ "ofType": null
+ },
+ "defaultValue": null
+ },
+ {
+ "name": "startTime",
+ "description": "List time-logs within a time range where the logged time is equal to or after startTime",
+ "type": {
+ "kind": "SCALAR",
+ "name": "Time",
+ "ofType": null
+ },
+ "defaultValue": null
+ },
+ {
+ "name": "endTime",
+ "description": "List time-logs within a time range where the logged time is equal to or before endTime",
+ "type": {
+ "kind": "SCALAR",
+ "name": "Time",
+ "ofType": null
},
"defaultValue": null
},
@@ -10444,7 +12508,7 @@
},
{
"name": "vulnerabilities",
- "description": "Vulnerabilities reported on the projects in the group and its subgroups. Available only when feature flag `first_class_vulnerabilities` is enabled",
+ "description": "Vulnerabilities reported on the projects in the group and its subgroups",
"args": [
{
"name": "projectId",
@@ -10568,6 +12632,87 @@
"deprecationReason": null
},
{
+ "name": "vulnerabilitiesCountByDayAndSeverity",
+ "description": "Number of vulnerabilities per severity level, per day, for the projects in the group and its subgroups",
+ "args": [
+ {
+ "name": "startDate",
+ "description": "First day for which to fetch vulnerability history",
+ "type": {
+ "kind": "NON_NULL",
+ "name": null,
+ "ofType": {
+ "kind": "SCALAR",
+ "name": "ISO8601Date",
+ "ofType": null
+ }
+ },
+ "defaultValue": null
+ },
+ {
+ "name": "endDate",
+ "description": "Last day for which to fetch vulnerability history",
+ "type": {
+ "kind": "NON_NULL",
+ "name": null,
+ "ofType": {
+ "kind": "SCALAR",
+ "name": "ISO8601Date",
+ "ofType": null
+ }
+ },
+ "defaultValue": null
+ },
+ {
+ "name": "after",
+ "description": "Returns the elements in the list that come after the specified cursor.",
+ "type": {
+ "kind": "SCALAR",
+ "name": "String",
+ "ofType": null
+ },
+ "defaultValue": null
+ },
+ {
+ "name": "before",
+ "description": "Returns the elements in the list that come before the specified cursor.",
+ "type": {
+ "kind": "SCALAR",
+ "name": "String",
+ "ofType": null
+ },
+ "defaultValue": null
+ },
+ {
+ "name": "first",
+ "description": "Returns the first _n_ elements from the list.",
+ "type": {
+ "kind": "SCALAR",
+ "name": "Int",
+ "ofType": null
+ },
+ "defaultValue": null
+ },
+ {
+ "name": "last",
+ "description": "Returns the last _n_ elements from the list.",
+ "type": {
+ "kind": "SCALAR",
+ "name": "Int",
+ "ofType": null
+ },
+ "defaultValue": null
+ }
+ ],
+ "type": {
+ "kind": "OBJECT",
+ "name": "VulnerabilitiesCountByDayAndSeverityConnection",
+ "ofType": null
+ },
+ "isDeprecated": false,
+ "deprecationReason": null
+ },
+ {
"name": "webUrl",
"description": "Web URL of the group",
"args": [
@@ -10665,6 +12810,86 @@
},
{
"kind": "SCALAR",
+ "name": "ISO8601Date",
+ "description": "An ISO 8601-encoded date",
+ "fields": null,
+ "inputFields": null,
+ "interfaces": null,
+ "enumValues": null,
+ "possibleTypes": null
+ },
+ {
+ "kind": "OBJECT",
+ "name": "InstanceSecurityDashboard",
+ "description": null,
+ "fields": [
+ {
+ "name": "projects",
+ "description": "Projects selected in Instance Security Dashboard",
+ "args": [
+ {
+ "name": "after",
+ "description": "Returns the elements in the list that come after the specified cursor.",
+ "type": {
+ "kind": "SCALAR",
+ "name": "String",
+ "ofType": null
+ },
+ "defaultValue": null
+ },
+ {
+ "name": "before",
+ "description": "Returns the elements in the list that come before the specified cursor.",
+ "type": {
+ "kind": "SCALAR",
+ "name": "String",
+ "ofType": null
+ },
+ "defaultValue": null
+ },
+ {
+ "name": "first",
+ "description": "Returns the first _n_ elements from the list.",
+ "type": {
+ "kind": "SCALAR",
+ "name": "Int",
+ "ofType": null
+ },
+ "defaultValue": null
+ },
+ {
+ "name": "last",
+ "description": "Returns the last _n_ elements from the list.",
+ "type": {
+ "kind": "SCALAR",
+ "name": "Int",
+ "ofType": null
+ },
+ "defaultValue": null
+ }
+ ],
+ "type": {
+ "kind": "NON_NULL",
+ "name": null,
+ "ofType": {
+ "kind": "OBJECT",
+ "name": "ProjectConnection",
+ "ofType": null
+ }
+ },
+ "isDeprecated": false,
+ "deprecationReason": null
+ }
+ ],
+ "inputFields": null,
+ "interfaces": [
+
+ ],
+ "enumValues": null,
+ "possibleTypes": null
+ },
+ {
+ "kind": "SCALAR",
"name": "Int",
"description": "Represents non-fractional signed whole numeric values. Int can represent values between -(2^31) and 2^31 - 1.",
"fields": null,
@@ -11038,6 +13263,20 @@
"deprecationReason": null
},
{
+ "name": "iteration",
+ "description": "Iteration of the issue",
+ "args": [
+
+ ],
+ "type": {
+ "kind": "OBJECT",
+ "name": "Iteration",
+ "ofType": null
+ },
+ "isDeprecated": false,
+ "deprecationReason": null
+ },
+ {
"name": "labels",
"description": "Labels of the issue",
"args": [
@@ -11864,7 +14103,7 @@
},
{
"name": "errors",
- "description": "Reasons why the mutation failed.",
+ "description": "Errors encountered during execution of the mutation.",
"args": [
],
@@ -11994,7 +14233,133 @@
},
{
"name": "errors",
- "description": "Reasons why the mutation failed.",
+ "description": "Errors encountered during execution of the mutation.",
+ "args": [
+
+ ],
+ "type": {
+ "kind": "NON_NULL",
+ "name": null,
+ "ofType": {
+ "kind": "LIST",
+ "name": null,
+ "ofType": {
+ "kind": "NON_NULL",
+ "name": null,
+ "ofType": {
+ "kind": "SCALAR",
+ "name": "String",
+ "ofType": null
+ }
+ }
+ }
+ },
+ "isDeprecated": false,
+ "deprecationReason": null
+ },
+ {
+ "name": "issue",
+ "description": "The issue after mutation",
+ "args": [
+
+ ],
+ "type": {
+ "kind": "OBJECT",
+ "name": "Issue",
+ "ofType": null
+ },
+ "isDeprecated": false,
+ "deprecationReason": null
+ }
+ ],
+ "inputFields": null,
+ "interfaces": [
+
+ ],
+ "enumValues": null,
+ "possibleTypes": null
+ },
+ {
+ "kind": "INPUT_OBJECT",
+ "name": "IssueSetIterationInput",
+ "description": "Autogenerated input type of IssueSetIteration",
+ "fields": null,
+ "inputFields": [
+ {
+ "name": "projectPath",
+ "description": "The project the issue to mutate is in",
+ "type": {
+ "kind": "NON_NULL",
+ "name": null,
+ "ofType": {
+ "kind": "SCALAR",
+ "name": "ID",
+ "ofType": null
+ }
+ },
+ "defaultValue": null
+ },
+ {
+ "name": "iid",
+ "description": "The iid of the issue to mutate",
+ "type": {
+ "kind": "NON_NULL",
+ "name": null,
+ "ofType": {
+ "kind": "SCALAR",
+ "name": "String",
+ "ofType": null
+ }
+ },
+ "defaultValue": null
+ },
+ {
+ "name": "iterationId",
+ "description": "The iteration to assign to the issue.\n",
+ "type": {
+ "kind": "SCALAR",
+ "name": "ID",
+ "ofType": null
+ },
+ "defaultValue": null
+ },
+ {
+ "name": "clientMutationId",
+ "description": "A unique identifier for the client performing the mutation.",
+ "type": {
+ "kind": "SCALAR",
+ "name": "String",
+ "ofType": null
+ },
+ "defaultValue": null
+ }
+ ],
+ "interfaces": null,
+ "enumValues": null,
+ "possibleTypes": null
+ },
+ {
+ "kind": "OBJECT",
+ "name": "IssueSetIterationPayload",
+ "description": "Autogenerated return type of IssueSetIteration",
+ "fields": [
+ {
+ "name": "clientMutationId",
+ "description": "A unique identifier for the client performing the mutation.",
+ "args": [
+
+ ],
+ "type": {
+ "kind": "SCALAR",
+ "name": "String",
+ "ofType": null
+ },
+ "isDeprecated": false,
+ "deprecationReason": null
+ },
+ {
+ "name": "errors",
+ "description": "Errors encountered during execution of the mutation.",
"args": [
],
@@ -12124,7 +14489,7 @@
},
{
"name": "errors",
- "description": "Reasons why the mutation failed.",
+ "description": "Errors encountered during execution of the mutation.",
"args": [
],
@@ -12203,6 +14568,42 @@
"deprecationReason": null
},
{
+ "name": "PRIORITY_ASC",
+ "description": "Priority by ascending order",
+ "isDeprecated": false,
+ "deprecationReason": null
+ },
+ {
+ "name": "PRIORITY_DESC",
+ "description": "Priority by descending order",
+ "isDeprecated": false,
+ "deprecationReason": null
+ },
+ {
+ "name": "LABEL_PRIORITY_ASC",
+ "description": "Label priority by ascending order",
+ "isDeprecated": false,
+ "deprecationReason": null
+ },
+ {
+ "name": "LABEL_PRIORITY_DESC",
+ "description": "Label priority by descending order",
+ "isDeprecated": false,
+ "deprecationReason": null
+ },
+ {
+ "name": "MILESTONE_DUE_ASC",
+ "description": "Milestone due date by ascending order",
+ "isDeprecated": false,
+ "deprecationReason": null
+ },
+ {
+ "name": "MILESTONE_DUE_DESC",
+ "description": "Milestone due date by descending order",
+ "isDeprecated": false,
+ "deprecationReason": null
+ },
+ {
"name": "DUE_DATE_ASC",
"description": "Due date by ascending order",
"isDeprecated": false,
@@ -12265,6 +14666,340 @@
"possibleTypes": null
},
{
+ "kind": "OBJECT",
+ "name": "Iteration",
+ "description": "Represents an iteration object.",
+ "fields": [
+ {
+ "name": "createdAt",
+ "description": "Timestamp of iteration creation",
+ "args": [
+
+ ],
+ "type": {
+ "kind": "NON_NULL",
+ "name": null,
+ "ofType": {
+ "kind": "SCALAR",
+ "name": "Time",
+ "ofType": null
+ }
+ },
+ "isDeprecated": false,
+ "deprecationReason": null
+ },
+ {
+ "name": "description",
+ "description": "Description of the iteration",
+ "args": [
+
+ ],
+ "type": {
+ "kind": "SCALAR",
+ "name": "String",
+ "ofType": null
+ },
+ "isDeprecated": false,
+ "deprecationReason": null
+ },
+ {
+ "name": "dueDate",
+ "description": "Timestamp of the iteration due date",
+ "args": [
+
+ ],
+ "type": {
+ "kind": "SCALAR",
+ "name": "Time",
+ "ofType": null
+ },
+ "isDeprecated": false,
+ "deprecationReason": null
+ },
+ {
+ "name": "id",
+ "description": "ID of the iteration",
+ "args": [
+
+ ],
+ "type": {
+ "kind": "NON_NULL",
+ "name": null,
+ "ofType": {
+ "kind": "SCALAR",
+ "name": "ID",
+ "ofType": null
+ }
+ },
+ "isDeprecated": false,
+ "deprecationReason": null
+ },
+ {
+ "name": "startDate",
+ "description": "Timestamp of the iteration start date",
+ "args": [
+
+ ],
+ "type": {
+ "kind": "SCALAR",
+ "name": "Time",
+ "ofType": null
+ },
+ "isDeprecated": false,
+ "deprecationReason": null
+ },
+ {
+ "name": "state",
+ "description": "State of the iteration",
+ "args": [
+
+ ],
+ "type": {
+ "kind": "NON_NULL",
+ "name": null,
+ "ofType": {
+ "kind": "ENUM",
+ "name": "IterationState",
+ "ofType": null
+ }
+ },
+ "isDeprecated": false,
+ "deprecationReason": null
+ },
+ {
+ "name": "title",
+ "description": "Title of the iteration",
+ "args": [
+
+ ],
+ "type": {
+ "kind": "NON_NULL",
+ "name": null,
+ "ofType": {
+ "kind": "SCALAR",
+ "name": "String",
+ "ofType": null
+ }
+ },
+ "isDeprecated": false,
+ "deprecationReason": null
+ },
+ {
+ "name": "updatedAt",
+ "description": "Timestamp of last iteration update",
+ "args": [
+
+ ],
+ "type": {
+ "kind": "NON_NULL",
+ "name": null,
+ "ofType": {
+ "kind": "SCALAR",
+ "name": "Time",
+ "ofType": null
+ }
+ },
+ "isDeprecated": false,
+ "deprecationReason": null
+ },
+ {
+ "name": "webPath",
+ "description": "Web path of the iteration",
+ "args": [
+
+ ],
+ "type": {
+ "kind": "NON_NULL",
+ "name": null,
+ "ofType": {
+ "kind": "SCALAR",
+ "name": "String",
+ "ofType": null
+ }
+ },
+ "isDeprecated": false,
+ "deprecationReason": null
+ },
+ {
+ "name": "webUrl",
+ "description": "Web URL of the iteration",
+ "args": [
+
+ ],
+ "type": {
+ "kind": "NON_NULL",
+ "name": null,
+ "ofType": {
+ "kind": "SCALAR",
+ "name": "String",
+ "ofType": null
+ }
+ },
+ "isDeprecated": false,
+ "deprecationReason": null
+ }
+ ],
+ "inputFields": null,
+ "interfaces": [
+
+ ],
+ "enumValues": null,
+ "possibleTypes": null
+ },
+ {
+ "kind": "OBJECT",
+ "name": "IterationConnection",
+ "description": "The connection type for Iteration.",
+ "fields": [
+ {
+ "name": "edges",
+ "description": "A list of edges.",
+ "args": [
+
+ ],
+ "type": {
+ "kind": "LIST",
+ "name": null,
+ "ofType": {
+ "kind": "OBJECT",
+ "name": "IterationEdge",
+ "ofType": null
+ }
+ },
+ "isDeprecated": false,
+ "deprecationReason": null
+ },
+ {
+ "name": "nodes",
+ "description": "A list of nodes.",
+ "args": [
+
+ ],
+ "type": {
+ "kind": "LIST",
+ "name": null,
+ "ofType": {
+ "kind": "OBJECT",
+ "name": "Iteration",
+ "ofType": null
+ }
+ },
+ "isDeprecated": false,
+ "deprecationReason": null
+ },
+ {
+ "name": "pageInfo",
+ "description": "Information to aid in pagination.",
+ "args": [
+
+ ],
+ "type": {
+ "kind": "NON_NULL",
+ "name": null,
+ "ofType": {
+ "kind": "OBJECT",
+ "name": "PageInfo",
+ "ofType": null
+ }
+ },
+ "isDeprecated": false,
+ "deprecationReason": null
+ }
+ ],
+ "inputFields": null,
+ "interfaces": [
+
+ ],
+ "enumValues": null,
+ "possibleTypes": null
+ },
+ {
+ "kind": "OBJECT",
+ "name": "IterationEdge",
+ "description": "An edge in a connection.",
+ "fields": [
+ {
+ "name": "cursor",
+ "description": "A cursor for use in pagination.",
+ "args": [
+
+ ],
+ "type": {
+ "kind": "NON_NULL",
+ "name": null,
+ "ofType": {
+ "kind": "SCALAR",
+ "name": "String",
+ "ofType": null
+ }
+ },
+ "isDeprecated": false,
+ "deprecationReason": null
+ },
+ {
+ "name": "node",
+ "description": "The item at the end of the edge.",
+ "args": [
+
+ ],
+ "type": {
+ "kind": "OBJECT",
+ "name": "Iteration",
+ "ofType": null
+ },
+ "isDeprecated": false,
+ "deprecationReason": null
+ }
+ ],
+ "inputFields": null,
+ "interfaces": [
+
+ ],
+ "enumValues": null,
+ "possibleTypes": null
+ },
+ {
+ "kind": "ENUM",
+ "name": "IterationState",
+ "description": "State of a GitLab iteration",
+ "fields": null,
+ "inputFields": null,
+ "interfaces": null,
+ "enumValues": [
+ {
+ "name": "upcoming",
+ "description": null,
+ "isDeprecated": false,
+ "deprecationReason": null
+ },
+ {
+ "name": "started",
+ "description": null,
+ "isDeprecated": false,
+ "deprecationReason": null
+ },
+ {
+ "name": "opened",
+ "description": null,
+ "isDeprecated": false,
+ "deprecationReason": null
+ },
+ {
+ "name": "closed",
+ "description": null,
+ "isDeprecated": false,
+ "deprecationReason": null
+ },
+ {
+ "name": "all",
+ "description": null,
+ "isDeprecated": false,
+ "deprecationReason": null
+ }
+ ],
+ "possibleTypes": null
+ },
+ {
"kind": "SCALAR",
"name": "JSON",
"description": "Represents untyped JSON",
@@ -12280,6 +15015,20 @@
"description": null,
"fields": [
{
+ "name": "createdAt",
+ "description": "Timestamp of when the Jira import was created",
+ "args": [
+
+ ],
+ "type": {
+ "kind": "SCALAR",
+ "name": "Time",
+ "ofType": null
+ },
+ "isDeprecated": false,
+ "deprecationReason": null
+ },
+ {
"name": "jiraProjectKey",
"description": "Project key for the imported Jira project",
"args": [
@@ -12299,7 +15048,7 @@
},
{
"name": "scheduledAt",
- "description": "Timestamp of when the Jira import was created",
+ "description": "Timestamp of when the Jira import was scheduled",
"args": [
],
@@ -12525,7 +15274,7 @@
},
{
"name": "errors",
- "description": "Reasons why the mutation failed.",
+ "description": "Errors encountered during execution of the mutation.",
"args": [
],
@@ -12842,6 +15591,35 @@
"possibleTypes": null
},
{
+ "kind": "ENUM",
+ "name": "ListLimitMetric",
+ "description": "List limit metric setting",
+ "fields": null,
+ "inputFields": null,
+ "interfaces": null,
+ "enumValues": [
+ {
+ "name": "all_metrics",
+ "description": null,
+ "isDeprecated": false,
+ "deprecationReason": null
+ },
+ {
+ "name": "issue_count",
+ "description": null,
+ "isDeprecated": false,
+ "deprecationReason": null
+ },
+ {
+ "name": "issue_weights",
+ "description": null,
+ "isDeprecated": false,
+ "deprecationReason": null
+ }
+ ],
+ "possibleTypes": null
+ },
+ {
"kind": "INPUT_OBJECT",
"name": "MarkAsSpamSnippetInput",
"description": "Autogenerated input type of MarkAsSpamSnippet",
@@ -12897,7 +15675,7 @@
},
{
"name": "errors",
- "description": "Reasons why the mutation failed.",
+ "description": "Errors encountered during execution of the mutation.",
"args": [
],
@@ -14490,7 +17268,7 @@
},
{
"name": "errors",
- "description": "Reasons why the mutation failed.",
+ "description": "Errors encountered during execution of the mutation.",
"args": [
],
@@ -14638,7 +17416,7 @@
},
{
"name": "errors",
- "description": "Reasons why the mutation failed.",
+ "description": "Errors encountered during execution of the mutation.",
"args": [
],
@@ -14768,7 +17546,7 @@
},
{
"name": "errors",
- "description": "Reasons why the mutation failed.",
+ "description": "Errors encountered during execution of the mutation.",
"args": [
],
@@ -14894,7 +17672,7 @@
},
{
"name": "errors",
- "description": "Reasons why the mutation failed.",
+ "description": "Errors encountered during execution of the mutation.",
"args": [
],
@@ -15024,7 +17802,7 @@
},
{
"name": "errors",
- "description": "Reasons why the mutation failed.",
+ "description": "Errors encountered during execution of the mutation.",
"args": [
],
@@ -15154,7 +17932,7 @@
},
{
"name": "errors",
- "description": "Reasons why the mutation failed.",
+ "description": "Errors encountered during execution of the mutation.",
"args": [
],
@@ -15291,7 +18069,7 @@
"fields": [
{
"name": "annotations",
- "description": "Annotations added to the dashboard. Will always return `null` if `metrics_dashboard_annotations` feature flag is disabled",
+ "description": "Annotations added to the dashboard",
"args": [
{
"name": "from",
@@ -15415,7 +18193,7 @@
],
"type": {
"kind": "SCALAR",
- "name": "String",
+ "name": "Time",
"ofType": null
},
"isDeprecated": false,
@@ -15461,7 +18239,7 @@
],
"type": {
"kind": "SCALAR",
- "name": "String",
+ "name": "Time",
"ofType": null
},
"isDeprecated": false,
@@ -15941,6 +18719,33 @@
"deprecationReason": null
},
{
+ "name": "addProjectToSecurityDashboard",
+ "description": null,
+ "args": [
+ {
+ "name": "input",
+ "description": null,
+ "type": {
+ "kind": "NON_NULL",
+ "name": null,
+ "ofType": {
+ "kind": "INPUT_OBJECT",
+ "name": "AddProjectToSecurityDashboardInput",
+ "ofType": null
+ }
+ },
+ "defaultValue": null
+ }
+ ],
+ "type": {
+ "kind": "OBJECT",
+ "name": "AddProjectToSecurityDashboardPayload",
+ "ofType": null
+ },
+ "isDeprecated": false,
+ "deprecationReason": null
+ },
+ {
"name": "adminSidekiqQueuesDeleteJobs",
"description": null,
"args": [
@@ -15968,6 +18773,114 @@
"deprecationReason": null
},
{
+ "name": "boardListUpdateLimitMetrics",
+ "description": null,
+ "args": [
+ {
+ "name": "input",
+ "description": null,
+ "type": {
+ "kind": "NON_NULL",
+ "name": null,
+ "ofType": {
+ "kind": "INPUT_OBJECT",
+ "name": "BoardListUpdateLimitMetricsInput",
+ "ofType": null
+ }
+ },
+ "defaultValue": null
+ }
+ ],
+ "type": {
+ "kind": "OBJECT",
+ "name": "BoardListUpdateLimitMetricsPayload",
+ "ofType": null
+ },
+ "isDeprecated": false,
+ "deprecationReason": null
+ },
+ {
+ "name": "createAlertIssue",
+ "description": null,
+ "args": [
+ {
+ "name": "input",
+ "description": null,
+ "type": {
+ "kind": "NON_NULL",
+ "name": null,
+ "ofType": {
+ "kind": "INPUT_OBJECT",
+ "name": "CreateAlertIssueInput",
+ "ofType": null
+ }
+ },
+ "defaultValue": null
+ }
+ ],
+ "type": {
+ "kind": "OBJECT",
+ "name": "CreateAlertIssuePayload",
+ "ofType": null
+ },
+ "isDeprecated": false,
+ "deprecationReason": null
+ },
+ {
+ "name": "createAnnotation",
+ "description": null,
+ "args": [
+ {
+ "name": "input",
+ "description": null,
+ "type": {
+ "kind": "NON_NULL",
+ "name": null,
+ "ofType": {
+ "kind": "INPUT_OBJECT",
+ "name": "CreateAnnotationInput",
+ "ofType": null
+ }
+ },
+ "defaultValue": null
+ }
+ ],
+ "type": {
+ "kind": "OBJECT",
+ "name": "CreateAnnotationPayload",
+ "ofType": null
+ },
+ "isDeprecated": false,
+ "deprecationReason": null
+ },
+ {
+ "name": "createBranch",
+ "description": null,
+ "args": [
+ {
+ "name": "input",
+ "description": null,
+ "type": {
+ "kind": "NON_NULL",
+ "name": null,
+ "ofType": {
+ "kind": "INPUT_OBJECT",
+ "name": "CreateBranchInput",
+ "ofType": null
+ }
+ },
+ "defaultValue": null
+ }
+ ],
+ "type": {
+ "kind": "OBJECT",
+ "name": "CreateBranchPayload",
+ "ofType": null
+ },
+ "isDeprecated": false,
+ "deprecationReason": null
+ },
+ {
"name": "createDiffNote",
"description": null,
"args": [
@@ -16049,6 +18962,33 @@
"deprecationReason": null
},
{
+ "name": "createIteration",
+ "description": null,
+ "args": [
+ {
+ "name": "input",
+ "description": null,
+ "type": {
+ "kind": "NON_NULL",
+ "name": null,
+ "ofType": {
+ "kind": "INPUT_OBJECT",
+ "name": "CreateIterationInput",
+ "ofType": null
+ }
+ },
+ "defaultValue": null
+ }
+ ],
+ "type": {
+ "kind": "OBJECT",
+ "name": "CreateIterationPayload",
+ "ofType": null
+ },
+ "isDeprecated": false,
+ "deprecationReason": null
+ },
+ {
"name": "createNote",
"description": null,
"args": [
@@ -16400,6 +19340,33 @@
"deprecationReason": null
},
{
+ "name": "issueSetIteration",
+ "description": null,
+ "args": [
+ {
+ "name": "input",
+ "description": null,
+ "type": {
+ "kind": "NON_NULL",
+ "name": null,
+ "ofType": {
+ "kind": "INPUT_OBJECT",
+ "name": "IssueSetIterationInput",
+ "ofType": null
+ }
+ },
+ "defaultValue": null
+ }
+ ],
+ "type": {
+ "kind": "OBJECT",
+ "name": "IssueSetIterationPayload",
+ "ofType": null
+ },
+ "isDeprecated": false,
+ "deprecationReason": null
+ },
+ {
"name": "issueSetWeight",
"description": null,
"args": [
@@ -16670,6 +19637,33 @@
"deprecationReason": null
},
{
+ "name": "removeProjectFromSecurityDashboard",
+ "description": null,
+ "args": [
+ {
+ "name": "input",
+ "description": null,
+ "type": {
+ "kind": "NON_NULL",
+ "name": null,
+ "ofType": {
+ "kind": "INPUT_OBJECT",
+ "name": "RemoveProjectFromSecurityDashboardInput",
+ "ofType": null
+ }
+ },
+ "defaultValue": null
+ }
+ ],
+ "type": {
+ "kind": "OBJECT",
+ "name": "RemoveProjectFromSecurityDashboardPayload",
+ "ofType": null
+ },
+ "isDeprecated": false,
+ "deprecationReason": null
+ },
+ {
"name": "todoMarkDone",
"description": null,
"args": [
@@ -16805,6 +19799,33 @@
"deprecationReason": null
},
{
+ "name": "updateAlertStatus",
+ "description": null,
+ "args": [
+ {
+ "name": "input",
+ "description": null,
+ "type": {
+ "kind": "NON_NULL",
+ "name": null,
+ "ofType": {
+ "kind": "INPUT_OBJECT",
+ "name": "UpdateAlertStatusInput",
+ "ofType": null
+ }
+ },
+ "defaultValue": null
+ }
+ ],
+ "type": {
+ "kind": "OBJECT",
+ "name": "UpdateAlertStatusPayload",
+ "ofType": null
+ },
+ "isDeprecated": false,
+ "deprecationReason": null
+ },
+ {
"name": "updateEpic",
"description": null,
"args": [
@@ -17155,6 +20176,16 @@
"defaultValue": "false"
},
{
+ "name": "hasVulnerabilities",
+ "description": "Returns only the projects which have vulnerabilities",
+ "type": {
+ "kind": "SCALAR",
+ "name": "Boolean",
+ "ofType": null
+ },
+ "defaultValue": "false"
+ },
+ {
"name": "after",
"description": "Returns the elements in the list that come after the specified cursor.",
"type": {
@@ -17997,6 +21028,527 @@
},
{
"kind": "OBJECT",
+ "name": "Package",
+ "description": "Represents a package",
+ "fields": [
+ {
+ "name": "createdAt",
+ "description": "The created date",
+ "args": [
+
+ ],
+ "type": {
+ "kind": "NON_NULL",
+ "name": null,
+ "ofType": {
+ "kind": "SCALAR",
+ "name": "Time",
+ "ofType": null
+ }
+ },
+ "isDeprecated": false,
+ "deprecationReason": null
+ },
+ {
+ "name": "id",
+ "description": "The ID of the package",
+ "args": [
+
+ ],
+ "type": {
+ "kind": "NON_NULL",
+ "name": null,
+ "ofType": {
+ "kind": "SCALAR",
+ "name": "ID",
+ "ofType": null
+ }
+ },
+ "isDeprecated": false,
+ "deprecationReason": null
+ },
+ {
+ "name": "name",
+ "description": "The name of the package",
+ "args": [
+
+ ],
+ "type": {
+ "kind": "NON_NULL",
+ "name": null,
+ "ofType": {
+ "kind": "SCALAR",
+ "name": "String",
+ "ofType": null
+ }
+ },
+ "isDeprecated": false,
+ "deprecationReason": null
+ },
+ {
+ "name": "packageType",
+ "description": "The type of the package",
+ "args": [
+
+ ],
+ "type": {
+ "kind": "NON_NULL",
+ "name": null,
+ "ofType": {
+ "kind": "ENUM",
+ "name": "PackageTypeEnum",
+ "ofType": null
+ }
+ },
+ "isDeprecated": false,
+ "deprecationReason": null
+ },
+ {
+ "name": "updatedAt",
+ "description": "The update date",
+ "args": [
+
+ ],
+ "type": {
+ "kind": "NON_NULL",
+ "name": null,
+ "ofType": {
+ "kind": "SCALAR",
+ "name": "Time",
+ "ofType": null
+ }
+ },
+ "isDeprecated": false,
+ "deprecationReason": null
+ },
+ {
+ "name": "version",
+ "description": "The version of the package",
+ "args": [
+
+ ],
+ "type": {
+ "kind": "SCALAR",
+ "name": "String",
+ "ofType": null
+ },
+ "isDeprecated": false,
+ "deprecationReason": null
+ }
+ ],
+ "inputFields": null,
+ "interfaces": [
+
+ ],
+ "enumValues": null,
+ "possibleTypes": null
+ },
+ {
+ "kind": "OBJECT",
+ "name": "PackageConnection",
+ "description": "The connection type for Package.",
+ "fields": [
+ {
+ "name": "edges",
+ "description": "A list of edges.",
+ "args": [
+
+ ],
+ "type": {
+ "kind": "LIST",
+ "name": null,
+ "ofType": {
+ "kind": "OBJECT",
+ "name": "PackageEdge",
+ "ofType": null
+ }
+ },
+ "isDeprecated": false,
+ "deprecationReason": null
+ },
+ {
+ "name": "nodes",
+ "description": "A list of nodes.",
+ "args": [
+
+ ],
+ "type": {
+ "kind": "LIST",
+ "name": null,
+ "ofType": {
+ "kind": "OBJECT",
+ "name": "Package",
+ "ofType": null
+ }
+ },
+ "isDeprecated": false,
+ "deprecationReason": null
+ },
+ {
+ "name": "pageInfo",
+ "description": "Information to aid in pagination.",
+ "args": [
+
+ ],
+ "type": {
+ "kind": "NON_NULL",
+ "name": null,
+ "ofType": {
+ "kind": "OBJECT",
+ "name": "PageInfo",
+ "ofType": null
+ }
+ },
+ "isDeprecated": false,
+ "deprecationReason": null
+ }
+ ],
+ "inputFields": null,
+ "interfaces": [
+
+ ],
+ "enumValues": null,
+ "possibleTypes": null
+ },
+ {
+ "kind": "OBJECT",
+ "name": "PackageEdge",
+ "description": "An edge in a connection.",
+ "fields": [
+ {
+ "name": "cursor",
+ "description": "A cursor for use in pagination.",
+ "args": [
+
+ ],
+ "type": {
+ "kind": "NON_NULL",
+ "name": null,
+ "ofType": {
+ "kind": "SCALAR",
+ "name": "String",
+ "ofType": null
+ }
+ },
+ "isDeprecated": false,
+ "deprecationReason": null
+ },
+ {
+ "name": "node",
+ "description": "The item at the end of the edge.",
+ "args": [
+
+ ],
+ "type": {
+ "kind": "OBJECT",
+ "name": "Package",
+ "ofType": null
+ },
+ "isDeprecated": false,
+ "deprecationReason": null
+ }
+ ],
+ "inputFields": null,
+ "interfaces": [
+
+ ],
+ "enumValues": null,
+ "possibleTypes": null
+ },
+ {
+ "kind": "OBJECT",
+ "name": "PackageFileRegistry",
+ "description": "Represents the sync and verification state of a package file",
+ "fields": [
+ {
+ "name": "createdAt",
+ "description": "Timestamp when the PackageFileRegistry was created",
+ "args": [
+
+ ],
+ "type": {
+ "kind": "SCALAR",
+ "name": "Time",
+ "ofType": null
+ },
+ "isDeprecated": false,
+ "deprecationReason": null
+ },
+ {
+ "name": "id",
+ "description": "ID of the PackageFileRegistry",
+ "args": [
+
+ ],
+ "type": {
+ "kind": "NON_NULL",
+ "name": null,
+ "ofType": {
+ "kind": "SCALAR",
+ "name": "ID",
+ "ofType": null
+ }
+ },
+ "isDeprecated": false,
+ "deprecationReason": null
+ },
+ {
+ "name": "lastSyncFailure",
+ "description": "Error message during sync of the PackageFileRegistry",
+ "args": [
+
+ ],
+ "type": {
+ "kind": "SCALAR",
+ "name": "String",
+ "ofType": null
+ },
+ "isDeprecated": false,
+ "deprecationReason": null
+ },
+ {
+ "name": "lastSyncedAt",
+ "description": "Timestamp of the most recent successful sync of the PackageFileRegistry",
+ "args": [
+
+ ],
+ "type": {
+ "kind": "SCALAR",
+ "name": "Time",
+ "ofType": null
+ },
+ "isDeprecated": false,
+ "deprecationReason": null
+ },
+ {
+ "name": "packageFileId",
+ "description": "ID of the PackageFile",
+ "args": [
+
+ ],
+ "type": {
+ "kind": "NON_NULL",
+ "name": null,
+ "ofType": {
+ "kind": "SCALAR",
+ "name": "ID",
+ "ofType": null
+ }
+ },
+ "isDeprecated": false,
+ "deprecationReason": null
+ },
+ {
+ "name": "retryAt",
+ "description": "Timestamp after which the PackageFileRegistry should be resynced",
+ "args": [
+
+ ],
+ "type": {
+ "kind": "SCALAR",
+ "name": "Time",
+ "ofType": null
+ },
+ "isDeprecated": false,
+ "deprecationReason": null
+ },
+ {
+ "name": "retryCount",
+ "description": "Number of consecutive failed sync attempts of the PackageFileRegistry",
+ "args": [
+
+ ],
+ "type": {
+ "kind": "SCALAR",
+ "name": "Int",
+ "ofType": null
+ },
+ "isDeprecated": false,
+ "deprecationReason": null
+ },
+ {
+ "name": "state",
+ "description": "Sync state of the PackageFileRegistry",
+ "args": [
+
+ ],
+ "type": {
+ "kind": "ENUM",
+ "name": "RegistryState",
+ "ofType": null
+ },
+ "isDeprecated": false,
+ "deprecationReason": null
+ }
+ ],
+ "inputFields": null,
+ "interfaces": [
+
+ ],
+ "enumValues": null,
+ "possibleTypes": null
+ },
+ {
+ "kind": "OBJECT",
+ "name": "PackageFileRegistryConnection",
+ "description": "The connection type for PackageFileRegistry.",
+ "fields": [
+ {
+ "name": "edges",
+ "description": "A list of edges.",
+ "args": [
+
+ ],
+ "type": {
+ "kind": "LIST",
+ "name": null,
+ "ofType": {
+ "kind": "OBJECT",
+ "name": "PackageFileRegistryEdge",
+ "ofType": null
+ }
+ },
+ "isDeprecated": false,
+ "deprecationReason": null
+ },
+ {
+ "name": "nodes",
+ "description": "A list of nodes.",
+ "args": [
+
+ ],
+ "type": {
+ "kind": "LIST",
+ "name": null,
+ "ofType": {
+ "kind": "OBJECT",
+ "name": "PackageFileRegistry",
+ "ofType": null
+ }
+ },
+ "isDeprecated": false,
+ "deprecationReason": null
+ },
+ {
+ "name": "pageInfo",
+ "description": "Information to aid in pagination.",
+ "args": [
+
+ ],
+ "type": {
+ "kind": "NON_NULL",
+ "name": null,
+ "ofType": {
+ "kind": "OBJECT",
+ "name": "PageInfo",
+ "ofType": null
+ }
+ },
+ "isDeprecated": false,
+ "deprecationReason": null
+ }
+ ],
+ "inputFields": null,
+ "interfaces": [
+
+ ],
+ "enumValues": null,
+ "possibleTypes": null
+ },
+ {
+ "kind": "OBJECT",
+ "name": "PackageFileRegistryEdge",
+ "description": "An edge in a connection.",
+ "fields": [
+ {
+ "name": "cursor",
+ "description": "A cursor for use in pagination.",
+ "args": [
+
+ ],
+ "type": {
+ "kind": "NON_NULL",
+ "name": null,
+ "ofType": {
+ "kind": "SCALAR",
+ "name": "String",
+ "ofType": null
+ }
+ },
+ "isDeprecated": false,
+ "deprecationReason": null
+ },
+ {
+ "name": "node",
+ "description": "The item at the end of the edge.",
+ "args": [
+
+ ],
+ "type": {
+ "kind": "OBJECT",
+ "name": "PackageFileRegistry",
+ "ofType": null
+ },
+ "isDeprecated": false,
+ "deprecationReason": null
+ }
+ ],
+ "inputFields": null,
+ "interfaces": [
+
+ ],
+ "enumValues": null,
+ "possibleTypes": null
+ },
+ {
+ "kind": "ENUM",
+ "name": "PackageTypeEnum",
+ "description": null,
+ "fields": null,
+ "inputFields": null,
+ "interfaces": null,
+ "enumValues": [
+ {
+ "name": "MAVEN",
+ "description": "Packages from the maven package manager",
+ "isDeprecated": false,
+ "deprecationReason": null
+ },
+ {
+ "name": "NPM",
+ "description": "Packages from the npm package manager",
+ "isDeprecated": false,
+ "deprecationReason": null
+ },
+ {
+ "name": "CONAN",
+ "description": "Packages from the conan package manager",
+ "isDeprecated": false,
+ "deprecationReason": null
+ },
+ {
+ "name": "NUGET",
+ "description": "Packages from the nuget package manager",
+ "isDeprecated": false,
+ "deprecationReason": null
+ },
+ {
+ "name": "PYPI",
+ "description": "Packages from the pypi package manager",
+ "isDeprecated": false,
+ "deprecationReason": null
+ },
+ {
+ "name": "COMPOSER",
+ "description": "Packages from the composer package manager",
+ "isDeprecated": false,
+ "deprecationReason": null
+ }
+ ],
+ "possibleTypes": null
+ },
+ {
+ "kind": "OBJECT",
"name": "PageInfo",
"description": "Information about pagination in a connection.",
"fields": [
@@ -18575,6 +22127,182 @@
"description": null,
"fields": [
{
+ "name": "alertManagementAlert",
+ "description": "A single Alert Management alert of the project",
+ "args": [
+ {
+ "name": "iid",
+ "description": "IID of the alert. For example, \"1\"",
+ "type": {
+ "kind": "SCALAR",
+ "name": "String",
+ "ofType": null
+ },
+ "defaultValue": null
+ },
+ {
+ "name": "statuses",
+ "description": "Alerts with the specified statues. For example, [TRIGGERED]",
+ "type": {
+ "kind": "LIST",
+ "name": null,
+ "ofType": {
+ "kind": "NON_NULL",
+ "name": null,
+ "ofType": {
+ "kind": "ENUM",
+ "name": "AlertManagementStatus",
+ "ofType": null
+ }
+ }
+ },
+ "defaultValue": null
+ },
+ {
+ "name": "sort",
+ "description": "Sort alerts by this criteria",
+ "type": {
+ "kind": "ENUM",
+ "name": "AlertManagementAlertSort",
+ "ofType": null
+ },
+ "defaultValue": null
+ },
+ {
+ "name": "search",
+ "description": "Search criteria for filtering alerts. This will search on title, description, service, monitoring_tool.",
+ "type": {
+ "kind": "SCALAR",
+ "name": "String",
+ "ofType": null
+ },
+ "defaultValue": null
+ }
+ ],
+ "type": {
+ "kind": "OBJECT",
+ "name": "AlertManagementAlert",
+ "ofType": null
+ },
+ "isDeprecated": false,
+ "deprecationReason": null
+ },
+ {
+ "name": "alertManagementAlertStatusCounts",
+ "description": "Counts of alerts by status for the project",
+ "args": [
+
+ ],
+ "type": {
+ "kind": "OBJECT",
+ "name": "AlertManagementAlertStatusCountsType",
+ "ofType": null
+ },
+ "isDeprecated": false,
+ "deprecationReason": null
+ },
+ {
+ "name": "alertManagementAlerts",
+ "description": "Alert Management alerts of the project",
+ "args": [
+ {
+ "name": "iid",
+ "description": "IID of the alert. For example, \"1\"",
+ "type": {
+ "kind": "SCALAR",
+ "name": "String",
+ "ofType": null
+ },
+ "defaultValue": null
+ },
+ {
+ "name": "statuses",
+ "description": "Alerts with the specified statues. For example, [TRIGGERED]",
+ "type": {
+ "kind": "LIST",
+ "name": null,
+ "ofType": {
+ "kind": "NON_NULL",
+ "name": null,
+ "ofType": {
+ "kind": "ENUM",
+ "name": "AlertManagementStatus",
+ "ofType": null
+ }
+ }
+ },
+ "defaultValue": null
+ },
+ {
+ "name": "sort",
+ "description": "Sort alerts by this criteria",
+ "type": {
+ "kind": "ENUM",
+ "name": "AlertManagementAlertSort",
+ "ofType": null
+ },
+ "defaultValue": null
+ },
+ {
+ "name": "search",
+ "description": "Search criteria for filtering alerts. This will search on title, description, service, monitoring_tool.",
+ "type": {
+ "kind": "SCALAR",
+ "name": "String",
+ "ofType": null
+ },
+ "defaultValue": null
+ },
+ {
+ "name": "after",
+ "description": "Returns the elements in the list that come after the specified cursor.",
+ "type": {
+ "kind": "SCALAR",
+ "name": "String",
+ "ofType": null
+ },
+ "defaultValue": null
+ },
+ {
+ "name": "before",
+ "description": "Returns the elements in the list that come before the specified cursor.",
+ "type": {
+ "kind": "SCALAR",
+ "name": "String",
+ "ofType": null
+ },
+ "defaultValue": null
+ },
+ {
+ "name": "first",
+ "description": "Returns the first _n_ elements from the list.",
+ "type": {
+ "kind": "SCALAR",
+ "name": "Int",
+ "ofType": null
+ },
+ "defaultValue": null
+ },
+ {
+ "name": "last",
+ "description": "Returns the last _n_ elements from the list.",
+ "type": {
+ "kind": "SCALAR",
+ "name": "Int",
+ "ofType": null
+ },
+ "defaultValue": null
+ }
+ ],
+ "type": {
+ "kind": "OBJECT",
+ "name": "AlertManagementAlertConnection",
+ "ofType": null
+ },
+ "isDeprecated": false,
+ "deprecationReason": null
+ },
+ {
"name": "archived",
"description": "Indicates the archived status of the project",
"args": [
@@ -19723,6 +23451,59 @@
"deprecationReason": null
},
{
+ "name": "packages",
+ "description": "Packages of the project",
+ "args": [
+ {
+ "name": "after",
+ "description": "Returns the elements in the list that come after the specified cursor.",
+ "type": {
+ "kind": "SCALAR",
+ "name": "String",
+ "ofType": null
+ },
+ "defaultValue": null
+ },
+ {
+ "name": "before",
+ "description": "Returns the elements in the list that come before the specified cursor.",
+ "type": {
+ "kind": "SCALAR",
+ "name": "String",
+ "ofType": null
+ },
+ "defaultValue": null
+ },
+ {
+ "name": "first",
+ "description": "Returns the first _n_ elements from the list.",
+ "type": {
+ "kind": "SCALAR",
+ "name": "Int",
+ "ofType": null
+ },
+ "defaultValue": null
+ },
+ {
+ "name": "last",
+ "description": "Returns the last _n_ elements from the list.",
+ "type": {
+ "kind": "SCALAR",
+ "name": "Int",
+ "ofType": null
+ },
+ "defaultValue": null
+ }
+ ],
+ "type": {
+ "kind": "OBJECT",
+ "name": "PackageConnection",
+ "ofType": null
+ },
+ "isDeprecated": false,
+ "deprecationReason": null
+ },
+ {
"name": "path",
"description": "Path of the project",
"args": [
@@ -19852,6 +23633,86 @@
"deprecationReason": null
},
{
+ "name": "release",
+ "description": "A single release of the project. Available only when feature flag `graphql_release_data` is enabled",
+ "args": [
+ {
+ "name": "tagName",
+ "description": "The name of the tag associated to the release",
+ "type": {
+ "kind": "NON_NULL",
+ "name": null,
+ "ofType": {
+ "kind": "SCALAR",
+ "name": "String",
+ "ofType": null
+ }
+ },
+ "defaultValue": null
+ }
+ ],
+ "type": {
+ "kind": "OBJECT",
+ "name": "Release",
+ "ofType": null
+ },
+ "isDeprecated": false,
+ "deprecationReason": null
+ },
+ {
+ "name": "releases",
+ "description": "Releases of the project. Available only when feature flag `graphql_release_data` is enabled",
+ "args": [
+ {
+ "name": "after",
+ "description": "Returns the elements in the list that come after the specified cursor.",
+ "type": {
+ "kind": "SCALAR",
+ "name": "String",
+ "ofType": null
+ },
+ "defaultValue": null
+ },
+ {
+ "name": "before",
+ "description": "Returns the elements in the list that come before the specified cursor.",
+ "type": {
+ "kind": "SCALAR",
+ "name": "String",
+ "ofType": null
+ },
+ "defaultValue": null
+ },
+ {
+ "name": "first",
+ "description": "Returns the first _n_ elements from the list.",
+ "type": {
+ "kind": "SCALAR",
+ "name": "Int",
+ "ofType": null
+ },
+ "defaultValue": null
+ },
+ {
+ "name": "last",
+ "description": "Returns the last _n_ elements from the list.",
+ "type": {
+ "kind": "SCALAR",
+ "name": "Int",
+ "ofType": null
+ },
+ "defaultValue": null
+ }
+ ],
+ "type": {
+ "kind": "OBJECT",
+ "name": "ReleaseConnection",
+ "ofType": null
+ },
+ "isDeprecated": false,
+ "deprecationReason": null
+ },
+ {
"name": "removeSourceBranchAfterMerge",
"description": "Indicates if `Delete source branch` option should be enabled by default for all new merge requests of the project",
"args": [
@@ -20428,7 +24289,7 @@
},
{
"name": "vulnerabilities",
- "description": "Vulnerabilities reported on the project. Available only when feature flag `first_class_vulnerabilities` is enabled",
+ "description": "Vulnerabilities reported on the project",
"args": [
{
"name": "projectId",
@@ -20553,7 +24414,7 @@
},
{
"name": "vulnerabilitySeveritiesCount",
- "description": "Counts for each severity of vulnerability of the project. Available only when feature flag `first_class_vulnerabilities` is enabled",
+ "description": "Counts for each severity of vulnerability of the project",
"args": [
],
@@ -21736,6 +25597,20 @@
"deprecationReason": null
},
{
+ "name": "instanceSecurityDashboard",
+ "description": "Fields related to Instance Security Dashboard",
+ "args": [
+
+ ],
+ "type": {
+ "kind": "OBJECT",
+ "name": "InstanceSecurityDashboard",
+ "ofType": null
+ },
+ "isDeprecated": false,
+ "deprecationReason": null
+ },
+ {
"name": "metadata",
"description": "Metadata about GitLab",
"args": [
@@ -21804,6 +25679,79 @@
"deprecationReason": null
},
{
+ "name": "projects",
+ "description": "Find projects visible to the current user",
+ "args": [
+ {
+ "name": "membership",
+ "description": "Limit projects that the current user is a member of",
+ "type": {
+ "kind": "SCALAR",
+ "name": "Boolean",
+ "ofType": null
+ },
+ "defaultValue": null
+ },
+ {
+ "name": "search",
+ "description": "Search criteria",
+ "type": {
+ "kind": "SCALAR",
+ "name": "String",
+ "ofType": null
+ },
+ "defaultValue": null
+ },
+ {
+ "name": "after",
+ "description": "Returns the elements in the list that come after the specified cursor.",
+ "type": {
+ "kind": "SCALAR",
+ "name": "String",
+ "ofType": null
+ },
+ "defaultValue": null
+ },
+ {
+ "name": "before",
+ "description": "Returns the elements in the list that come before the specified cursor.",
+ "type": {
+ "kind": "SCALAR",
+ "name": "String",
+ "ofType": null
+ },
+ "defaultValue": null
+ },
+ {
+ "name": "first",
+ "description": "Returns the first _n_ elements from the list.",
+ "type": {
+ "kind": "SCALAR",
+ "name": "Int",
+ "ofType": null
+ },
+ "defaultValue": null
+ },
+ {
+ "name": "last",
+ "description": "Returns the last _n_ elements from the list.",
+ "type": {
+ "kind": "SCALAR",
+ "name": "Int",
+ "ofType": null
+ },
+ "defaultValue": null
+ }
+ ],
+ "type": {
+ "kind": "OBJECT",
+ "name": "ProjectConnection",
+ "ofType": null
+ },
+ "isDeprecated": false,
+ "deprecationReason": null
+ },
+ {
"name": "snippets",
"description": "Find Snippets visible to the current user",
"args": [
@@ -22048,6 +25996,430 @@
},
"isDeprecated": false,
"deprecationReason": null
+ },
+ {
+ "name": "vulnerabilitiesCountByDayAndSeverity",
+ "description": "Number of vulnerabilities per severity level, per day, for the projects on the current user's instance security dashboard",
+ "args": [
+ {
+ "name": "startDate",
+ "description": "First day for which to fetch vulnerability history",
+ "type": {
+ "kind": "NON_NULL",
+ "name": null,
+ "ofType": {
+ "kind": "SCALAR",
+ "name": "ISO8601Date",
+ "ofType": null
+ }
+ },
+ "defaultValue": null
+ },
+ {
+ "name": "endDate",
+ "description": "Last day for which to fetch vulnerability history",
+ "type": {
+ "kind": "NON_NULL",
+ "name": null,
+ "ofType": {
+ "kind": "SCALAR",
+ "name": "ISO8601Date",
+ "ofType": null
+ }
+ },
+ "defaultValue": null
+ },
+ {
+ "name": "after",
+ "description": "Returns the elements in the list that come after the specified cursor.",
+ "type": {
+ "kind": "SCALAR",
+ "name": "String",
+ "ofType": null
+ },
+ "defaultValue": null
+ },
+ {
+ "name": "before",
+ "description": "Returns the elements in the list that come before the specified cursor.",
+ "type": {
+ "kind": "SCALAR",
+ "name": "String",
+ "ofType": null
+ },
+ "defaultValue": null
+ },
+ {
+ "name": "first",
+ "description": "Returns the first _n_ elements from the list.",
+ "type": {
+ "kind": "SCALAR",
+ "name": "Int",
+ "ofType": null
+ },
+ "defaultValue": null
+ },
+ {
+ "name": "last",
+ "description": "Returns the last _n_ elements from the list.",
+ "type": {
+ "kind": "SCALAR",
+ "name": "Int",
+ "ofType": null
+ },
+ "defaultValue": null
+ }
+ ],
+ "type": {
+ "kind": "OBJECT",
+ "name": "VulnerabilitiesCountByDayAndSeverityConnection",
+ "ofType": null
+ },
+ "isDeprecated": false,
+ "deprecationReason": null
+ }
+ ],
+ "inputFields": null,
+ "interfaces": [
+
+ ],
+ "enumValues": null,
+ "possibleTypes": null
+ },
+ {
+ "kind": "ENUM",
+ "name": "RegistryState",
+ "description": "State of a Geo registry.",
+ "fields": null,
+ "inputFields": null,
+ "interfaces": null,
+ "enumValues": [
+ {
+ "name": "PENDING",
+ "description": "Registry waiting to be synced",
+ "isDeprecated": false,
+ "deprecationReason": null
+ },
+ {
+ "name": "STARTED",
+ "description": "Registry currently syncing",
+ "isDeprecated": false,
+ "deprecationReason": null
+ },
+ {
+ "name": "SYNCED",
+ "description": "Registry that is synced",
+ "isDeprecated": false,
+ "deprecationReason": null
+ },
+ {
+ "name": "FAILED",
+ "description": "Registry that failed to sync",
+ "isDeprecated": false,
+ "deprecationReason": null
+ }
+ ],
+ "possibleTypes": null
+ },
+ {
+ "kind": "OBJECT",
+ "name": "Release",
+ "description": null,
+ "fields": [
+ {
+ "name": "author",
+ "description": "User that created the release",
+ "args": [
+
+ ],
+ "type": {
+ "kind": "OBJECT",
+ "name": "User",
+ "ofType": null
+ },
+ "isDeprecated": false,
+ "deprecationReason": null
+ },
+ {
+ "name": "commit",
+ "description": "The commit associated with the release",
+ "args": [
+
+ ],
+ "type": {
+ "kind": "OBJECT",
+ "name": "Commit",
+ "ofType": null
+ },
+ "isDeprecated": false,
+ "deprecationReason": null
+ },
+ {
+ "name": "createdAt",
+ "description": "Timestamp of when the release was created",
+ "args": [
+
+ ],
+ "type": {
+ "kind": "SCALAR",
+ "name": "Time",
+ "ofType": null
+ },
+ "isDeprecated": false,
+ "deprecationReason": null
+ },
+ {
+ "name": "description",
+ "description": "Description (also known as \"release notes\") of the release",
+ "args": [
+
+ ],
+ "type": {
+ "kind": "SCALAR",
+ "name": "String",
+ "ofType": null
+ },
+ "isDeprecated": false,
+ "deprecationReason": null
+ },
+ {
+ "name": "descriptionHtml",
+ "description": "The GitLab Flavored Markdown rendering of `description`",
+ "args": [
+
+ ],
+ "type": {
+ "kind": "SCALAR",
+ "name": "String",
+ "ofType": null
+ },
+ "isDeprecated": false,
+ "deprecationReason": null
+ },
+ {
+ "name": "milestones",
+ "description": "Milestones associated to the release",
+ "args": [
+ {
+ "name": "after",
+ "description": "Returns the elements in the list that come after the specified cursor.",
+ "type": {
+ "kind": "SCALAR",
+ "name": "String",
+ "ofType": null
+ },
+ "defaultValue": null
+ },
+ {
+ "name": "before",
+ "description": "Returns the elements in the list that come before the specified cursor.",
+ "type": {
+ "kind": "SCALAR",
+ "name": "String",
+ "ofType": null
+ },
+ "defaultValue": null
+ },
+ {
+ "name": "first",
+ "description": "Returns the first _n_ elements from the list.",
+ "type": {
+ "kind": "SCALAR",
+ "name": "Int",
+ "ofType": null
+ },
+ "defaultValue": null
+ },
+ {
+ "name": "last",
+ "description": "Returns the last _n_ elements from the list.",
+ "type": {
+ "kind": "SCALAR",
+ "name": "Int",
+ "ofType": null
+ },
+ "defaultValue": null
+ }
+ ],
+ "type": {
+ "kind": "OBJECT",
+ "name": "MilestoneConnection",
+ "ofType": null
+ },
+ "isDeprecated": false,
+ "deprecationReason": null
+ },
+ {
+ "name": "name",
+ "description": "Name of the release",
+ "args": [
+
+ ],
+ "type": {
+ "kind": "SCALAR",
+ "name": "String",
+ "ofType": null
+ },
+ "isDeprecated": false,
+ "deprecationReason": null
+ },
+ {
+ "name": "releasedAt",
+ "description": "Timestamp of when the release was released",
+ "args": [
+
+ ],
+ "type": {
+ "kind": "SCALAR",
+ "name": "Time",
+ "ofType": null
+ },
+ "isDeprecated": false,
+ "deprecationReason": null
+ },
+ {
+ "name": "tagName",
+ "description": "Name of the tag associated with the release",
+ "args": [
+
+ ],
+ "type": {
+ "kind": "NON_NULL",
+ "name": null,
+ "ofType": {
+ "kind": "SCALAR",
+ "name": "String",
+ "ofType": null
+ }
+ },
+ "isDeprecated": false,
+ "deprecationReason": null
+ },
+ {
+ "name": "tagPath",
+ "description": "Relative web path to the tag associated with the release",
+ "args": [
+
+ ],
+ "type": {
+ "kind": "SCALAR",
+ "name": "String",
+ "ofType": null
+ },
+ "isDeprecated": false,
+ "deprecationReason": null
+ }
+ ],
+ "inputFields": null,
+ "interfaces": [
+
+ ],
+ "enumValues": null,
+ "possibleTypes": null
+ },
+ {
+ "kind": "OBJECT",
+ "name": "ReleaseConnection",
+ "description": "The connection type for Release.",
+ "fields": [
+ {
+ "name": "edges",
+ "description": "A list of edges.",
+ "args": [
+
+ ],
+ "type": {
+ "kind": "LIST",
+ "name": null,
+ "ofType": {
+ "kind": "OBJECT",
+ "name": "ReleaseEdge",
+ "ofType": null
+ }
+ },
+ "isDeprecated": false,
+ "deprecationReason": null
+ },
+ {
+ "name": "nodes",
+ "description": "A list of nodes.",
+ "args": [
+
+ ],
+ "type": {
+ "kind": "LIST",
+ "name": null,
+ "ofType": {
+ "kind": "OBJECT",
+ "name": "Release",
+ "ofType": null
+ }
+ },
+ "isDeprecated": false,
+ "deprecationReason": null
+ },
+ {
+ "name": "pageInfo",
+ "description": "Information to aid in pagination.",
+ "args": [
+
+ ],
+ "type": {
+ "kind": "NON_NULL",
+ "name": null,
+ "ofType": {
+ "kind": "OBJECT",
+ "name": "PageInfo",
+ "ofType": null
+ }
+ },
+ "isDeprecated": false,
+ "deprecationReason": null
+ }
+ ],
+ "inputFields": null,
+ "interfaces": [
+
+ ],
+ "enumValues": null,
+ "possibleTypes": null
+ },
+ {
+ "kind": "OBJECT",
+ "name": "ReleaseEdge",
+ "description": "An edge in a connection.",
+ "fields": [
+ {
+ "name": "cursor",
+ "description": "A cursor for use in pagination.",
+ "args": [
+
+ ],
+ "type": {
+ "kind": "NON_NULL",
+ "name": null,
+ "ofType": {
+ "kind": "SCALAR",
+ "name": "String",
+ "ofType": null
+ }
+ },
+ "isDeprecated": false,
+ "deprecationReason": null
+ },
+ {
+ "name": "node",
+ "description": "The item at the end of the edge.",
+ "args": [
+
+ ],
+ "type": {
+ "kind": "OBJECT",
+ "name": "Release",
+ "ofType": null
+ },
+ "isDeprecated": false,
+ "deprecationReason": null
}
],
"inputFields": null,
@@ -22141,7 +26513,95 @@
},
{
"name": "errors",
- "description": "Reasons why the mutation failed.",
+ "description": "Errors encountered during execution of the mutation.",
+ "args": [
+
+ ],
+ "type": {
+ "kind": "NON_NULL",
+ "name": null,
+ "ofType": {
+ "kind": "LIST",
+ "name": null,
+ "ofType": {
+ "kind": "NON_NULL",
+ "name": null,
+ "ofType": {
+ "kind": "SCALAR",
+ "name": "String",
+ "ofType": null
+ }
+ }
+ }
+ },
+ "isDeprecated": false,
+ "deprecationReason": null
+ }
+ ],
+ "inputFields": null,
+ "interfaces": [
+
+ ],
+ "enumValues": null,
+ "possibleTypes": null
+ },
+ {
+ "kind": "INPUT_OBJECT",
+ "name": "RemoveProjectFromSecurityDashboardInput",
+ "description": "Autogenerated input type of RemoveProjectFromSecurityDashboard",
+ "fields": null,
+ "inputFields": [
+ {
+ "name": "id",
+ "description": "ID of the project to remove from the Instance Security Dashboard",
+ "type": {
+ "kind": "NON_NULL",
+ "name": null,
+ "ofType": {
+ "kind": "SCALAR",
+ "name": "ID",
+ "ofType": null
+ }
+ },
+ "defaultValue": null
+ },
+ {
+ "name": "clientMutationId",
+ "description": "A unique identifier for the client performing the mutation.",
+ "type": {
+ "kind": "SCALAR",
+ "name": "String",
+ "ofType": null
+ },
+ "defaultValue": null
+ }
+ ],
+ "interfaces": null,
+ "enumValues": null,
+ "possibleTypes": null
+ },
+ {
+ "kind": "OBJECT",
+ "name": "RemoveProjectFromSecurityDashboardPayload",
+ "description": "Autogenerated return type of RemoveProjectFromSecurityDashboard",
+ "fields": [
+ {
+ "name": "clientMutationId",
+ "description": "A unique identifier for the client performing the mutation.",
+ "args": [
+
+ ],
+ "type": {
+ "kind": "SCALAR",
+ "name": "String",
+ "ofType": null
+ },
+ "isDeprecated": false,
+ "deprecationReason": null
+ },
+ {
+ "name": "errors",
+ "description": "Errors encountered during execution of the mutation.",
"args": [
],
@@ -24587,25 +29047,25 @@
"deprecationReason": null
},
{
- "name": "YOUTRACK_SERVICE",
+ "name": "WEBEX_TEAMS_SERVICE",
"description": null,
"isDeprecated": false,
"deprecationReason": null
},
{
- "name": "GITHUB_SERVICE",
+ "name": "YOUTRACK_SERVICE",
"description": null,
"isDeprecated": false,
"deprecationReason": null
},
{
- "name": "JENKINS_SERVICE",
+ "name": "GITHUB_SERVICE",
"description": null,
"isDeprecated": false,
"deprecationReason": null
},
{
- "name": "JENKINS_DEPRECATED_SERVICE",
+ "name": "JENKINS_SERVICE",
"description": null,
"isDeprecated": false,
"deprecationReason": null
@@ -24787,7 +29247,7 @@
},
{
"name": "id",
- "description": "Id of the snippet",
+ "description": "ID of the snippet",
"args": [
],
@@ -25032,6 +29492,20 @@
"deprecationReason": null
},
{
+ "name": "externalStorage",
+ "description": "Blob external storage",
+ "args": [
+
+ ],
+ "type": {
+ "kind": "SCALAR",
+ "name": "String",
+ "ofType": null
+ },
+ "isDeprecated": false,
+ "deprecationReason": null
+ },
+ {
"name": "mode",
"description": "Blob mode",
"args": [
@@ -25106,6 +29580,24 @@
"deprecationReason": null
},
{
+ "name": "renderedAsText",
+ "description": "Shows whether the blob is rendered as text",
+ "args": [
+
+ ],
+ "type": {
+ "kind": "NON_NULL",
+ "name": null,
+ "ofType": {
+ "kind": "SCALAR",
+ "name": "Boolean",
+ "ofType": null
+ }
+ },
+ "isDeprecated": false,
+ "deprecationReason": null
+ },
+ {
"name": "richData",
"description": "Blob highlighted data",
"args": [
@@ -26214,7 +30706,7 @@
},
{
"name": "id",
- "description": "Id of the todo",
+ "description": "ID of the todo",
"args": [
],
@@ -26509,7 +31001,7 @@
},
{
"name": "errors",
- "description": "Reasons why the mutation failed.",
+ "description": "Errors encountered during execution of the mutation.",
"args": [
],
@@ -26658,7 +31150,7 @@
},
{
"name": "errors",
- "description": "Reasons why the mutation failed.",
+ "description": "Errors encountered during execution of the mutation.",
"args": [
],
@@ -26737,7 +31229,7 @@
},
{
"name": "errors",
- "description": "Reasons why the mutation failed.",
+ "description": "Errors encountered during execution of the mutation.",
"args": [
],
@@ -26893,7 +31385,7 @@
},
{
"name": "errors",
- "description": "Reasons why the mutation failed.",
+ "description": "Errors encountered during execution of the mutation.",
"args": [
],
@@ -27035,7 +31527,7 @@
},
{
"name": "errors",
- "description": "Reasons why the mutation failed.",
+ "description": "Errors encountered during execution of the mutation.",
"args": [
],
@@ -27559,6 +32051,150 @@
},
{
"kind": "INPUT_OBJECT",
+ "name": "UpdateAlertStatusInput",
+ "description": "Autogenerated input type of UpdateAlertStatus",
+ "fields": null,
+ "inputFields": [
+ {
+ "name": "projectPath",
+ "description": "The project the alert to mutate is in",
+ "type": {
+ "kind": "NON_NULL",
+ "name": null,
+ "ofType": {
+ "kind": "SCALAR",
+ "name": "ID",
+ "ofType": null
+ }
+ },
+ "defaultValue": null
+ },
+ {
+ "name": "iid",
+ "description": "The iid of the alert to mutate",
+ "type": {
+ "kind": "NON_NULL",
+ "name": null,
+ "ofType": {
+ "kind": "SCALAR",
+ "name": "String",
+ "ofType": null
+ }
+ },
+ "defaultValue": null
+ },
+ {
+ "name": "status",
+ "description": "The status to set the alert",
+ "type": {
+ "kind": "NON_NULL",
+ "name": null,
+ "ofType": {
+ "kind": "ENUM",
+ "name": "AlertManagementStatus",
+ "ofType": null
+ }
+ },
+ "defaultValue": null
+ },
+ {
+ "name": "clientMutationId",
+ "description": "A unique identifier for the client performing the mutation.",
+ "type": {
+ "kind": "SCALAR",
+ "name": "String",
+ "ofType": null
+ },
+ "defaultValue": null
+ }
+ ],
+ "interfaces": null,
+ "enumValues": null,
+ "possibleTypes": null
+ },
+ {
+ "kind": "OBJECT",
+ "name": "UpdateAlertStatusPayload",
+ "description": "Autogenerated return type of UpdateAlertStatus",
+ "fields": [
+ {
+ "name": "alert",
+ "description": "The alert after mutation",
+ "args": [
+
+ ],
+ "type": {
+ "kind": "OBJECT",
+ "name": "AlertManagementAlert",
+ "ofType": null
+ },
+ "isDeprecated": false,
+ "deprecationReason": null
+ },
+ {
+ "name": "clientMutationId",
+ "description": "A unique identifier for the client performing the mutation.",
+ "args": [
+
+ ],
+ "type": {
+ "kind": "SCALAR",
+ "name": "String",
+ "ofType": null
+ },
+ "isDeprecated": false,
+ "deprecationReason": null
+ },
+ {
+ "name": "errors",
+ "description": "Errors encountered during execution of the mutation.",
+ "args": [
+
+ ],
+ "type": {
+ "kind": "NON_NULL",
+ "name": null,
+ "ofType": {
+ "kind": "LIST",
+ "name": null,
+ "ofType": {
+ "kind": "NON_NULL",
+ "name": null,
+ "ofType": {
+ "kind": "SCALAR",
+ "name": "String",
+ "ofType": null
+ }
+ }
+ }
+ },
+ "isDeprecated": false,
+ "deprecationReason": null
+ },
+ {
+ "name": "issue",
+ "description": "The issue created after mutation",
+ "args": [
+
+ ],
+ "type": {
+ "kind": "OBJECT",
+ "name": "Issue",
+ "ofType": null
+ },
+ "isDeprecated": false,
+ "deprecationReason": null
+ }
+ ],
+ "inputFields": null,
+ "interfaces": [
+
+ ],
+ "enumValues": null,
+ "possibleTypes": null
+ },
+ {
+ "kind": "INPUT_OBJECT",
"name": "UpdateDiffImagePositionInput",
"description": null,
"fields": null,
@@ -27808,7 +32444,7 @@
},
{
"name": "errors",
- "description": "Reasons why the mutation failed.",
+ "description": "Errors encountered during execution of the mutation.",
"args": [
],
@@ -27916,7 +32552,7 @@
},
{
"name": "errors",
- "description": "Reasons why the mutation failed.",
+ "description": "Errors encountered during execution of the mutation.",
"args": [
],
@@ -28082,7 +32718,7 @@
},
{
"name": "errors",
- "description": "Reasons why the mutation failed.",
+ "description": "Errors encountered during execution of the mutation.",
"args": [
],
@@ -28198,7 +32834,7 @@
},
{
"name": "errors",
- "description": "Reasons why the mutation failed.",
+ "description": "Errors encountered during execution of the mutation.",
"args": [
],
@@ -28334,7 +32970,7 @@
},
{
"name": "errors",
- "description": "Reasons why the mutation failed.",
+ "description": "Errors encountered during execution of the mutation.",
"args": [
],
@@ -28486,7 +33122,7 @@
},
{
"name": "errors",
- "description": "Reasons why the mutation failed.",
+ "description": "Errors encountered during execution of the mutation.",
"args": [
],
@@ -28562,6 +33198,24 @@
"deprecationReason": null
},
{
+ "name": "id",
+ "description": "ID of the user",
+ "args": [
+
+ ],
+ "type": {
+ "kind": "NON_NULL",
+ "name": null,
+ "ofType": {
+ "kind": "SCALAR",
+ "name": "ID",
+ "ofType": null
+ }
+ },
+ "isDeprecated": false,
+ "deprecationReason": null
+ },
+ {
"name": "name",
"description": "Human-readable name of the user",
"args": [
@@ -28671,6 +33325,24 @@
"deprecationReason": null
},
{
+ "name": "state",
+ "description": "State of the issue",
+ "args": [
+
+ ],
+ "type": {
+ "kind": "NON_NULL",
+ "name": null,
+ "ofType": {
+ "kind": "SCALAR",
+ "name": "String",
+ "ofType": null
+ }
+ },
+ "isDeprecated": false,
+ "deprecationReason": null
+ },
+ {
"name": "todos",
"description": "Todos of the user",
"args": [
@@ -29100,6 +33772,173 @@
},
{
"kind": "OBJECT",
+ "name": "VulnerabilitiesCountByDayAndSeverity",
+ "description": "Represents the number of vulnerabilities for a particular severity on a particular day",
+ "fields": [
+ {
+ "name": "count",
+ "description": "Number of vulnerabilities",
+ "args": [
+
+ ],
+ "type": {
+ "kind": "SCALAR",
+ "name": "Int",
+ "ofType": null
+ },
+ "isDeprecated": false,
+ "deprecationReason": null
+ },
+ {
+ "name": "day",
+ "description": "Date for the count",
+ "args": [
+
+ ],
+ "type": {
+ "kind": "SCALAR",
+ "name": "ISO8601Date",
+ "ofType": null
+ },
+ "isDeprecated": false,
+ "deprecationReason": null
+ },
+ {
+ "name": "severity",
+ "description": "Severity of the counted vulnerabilities",
+ "args": [
+
+ ],
+ "type": {
+ "kind": "ENUM",
+ "name": "VulnerabilitySeverity",
+ "ofType": null
+ },
+ "isDeprecated": false,
+ "deprecationReason": null
+ }
+ ],
+ "inputFields": null,
+ "interfaces": [
+
+ ],
+ "enumValues": null,
+ "possibleTypes": null
+ },
+ {
+ "kind": "OBJECT",
+ "name": "VulnerabilitiesCountByDayAndSeverityConnection",
+ "description": "The connection type for VulnerabilitiesCountByDayAndSeverity.",
+ "fields": [
+ {
+ "name": "edges",
+ "description": "A list of edges.",
+ "args": [
+
+ ],
+ "type": {
+ "kind": "LIST",
+ "name": null,
+ "ofType": {
+ "kind": "OBJECT",
+ "name": "VulnerabilitiesCountByDayAndSeverityEdge",
+ "ofType": null
+ }
+ },
+ "isDeprecated": false,
+ "deprecationReason": null
+ },
+ {
+ "name": "nodes",
+ "description": "A list of nodes.",
+ "args": [
+
+ ],
+ "type": {
+ "kind": "LIST",
+ "name": null,
+ "ofType": {
+ "kind": "OBJECT",
+ "name": "VulnerabilitiesCountByDayAndSeverity",
+ "ofType": null
+ }
+ },
+ "isDeprecated": false,
+ "deprecationReason": null
+ },
+ {
+ "name": "pageInfo",
+ "description": "Information to aid in pagination.",
+ "args": [
+
+ ],
+ "type": {
+ "kind": "NON_NULL",
+ "name": null,
+ "ofType": {
+ "kind": "OBJECT",
+ "name": "PageInfo",
+ "ofType": null
+ }
+ },
+ "isDeprecated": false,
+ "deprecationReason": null
+ }
+ ],
+ "inputFields": null,
+ "interfaces": [
+
+ ],
+ "enumValues": null,
+ "possibleTypes": null
+ },
+ {
+ "kind": "OBJECT",
+ "name": "VulnerabilitiesCountByDayAndSeverityEdge",
+ "description": "An edge in a connection.",
+ "fields": [
+ {
+ "name": "cursor",
+ "description": "A cursor for use in pagination.",
+ "args": [
+
+ ],
+ "type": {
+ "kind": "NON_NULL",
+ "name": null,
+ "ofType": {
+ "kind": "SCALAR",
+ "name": "String",
+ "ofType": null
+ }
+ },
+ "isDeprecated": false,
+ "deprecationReason": null
+ },
+ {
+ "name": "node",
+ "description": "The item at the end of the edge.",
+ "args": [
+
+ ],
+ "type": {
+ "kind": "OBJECT",
+ "name": "VulnerabilitiesCountByDayAndSeverity",
+ "ofType": null
+ },
+ "isDeprecated": false,
+ "deprecationReason": null
+ }
+ ],
+ "inputFields": null,
+ "interfaces": [
+
+ ],
+ "enumValues": null,
+ "possibleTypes": null
+ },
+ {
+ "kind": "OBJECT",
"name": "Vulnerability",
"description": "Represents a vulnerability.",
"fields": [
@@ -29137,13 +33976,13 @@
},
{
"name": "location",
- "description": "The JSON location metadata for the vulnerability. Its format depends on the type of the security scan that found the vulnerability",
+ "description": "Location metadata for the vulnerability. Its fields depend on the type of security scan that found the vulnerability",
"args": [
],
"type": {
- "kind": "SCALAR",
- "name": "JSON",
+ "kind": "UNION",
+ "name": "VulnerabilityLocation",
"ofType": null
},
"isDeprecated": false,
@@ -29372,6 +34211,285 @@
"possibleTypes": null
},
{
+ "kind": "UNION",
+ "name": "VulnerabilityLocation",
+ "description": "Represents a vulnerability location. The fields with data will depend on the vulnerability report type",
+ "fields": null,
+ "inputFields": null,
+ "interfaces": null,
+ "enumValues": null,
+ "possibleTypes": [
+ {
+ "kind": "OBJECT",
+ "name": "VulnerabilityLocationContainerScanning",
+ "ofType": null
+ },
+ {
+ "kind": "OBJECT",
+ "name": "VulnerabilityLocationDast",
+ "ofType": null
+ },
+ {
+ "kind": "OBJECT",
+ "name": "VulnerabilityLocationDependencyScanning",
+ "ofType": null
+ },
+ {
+ "kind": "OBJECT",
+ "name": "VulnerabilityLocationSast",
+ "ofType": null
+ }
+ ]
+ },
+ {
+ "kind": "OBJECT",
+ "name": "VulnerabilityLocationContainerScanning",
+ "description": "Represents the location of a vulnerability found by a container security scan",
+ "fields": [
+ {
+ "name": "dependency",
+ "description": "Dependency containing the vulnerability",
+ "args": [
+
+ ],
+ "type": {
+ "kind": "OBJECT",
+ "name": "VulnerableDependency",
+ "ofType": null
+ },
+ "isDeprecated": false,
+ "deprecationReason": null
+ },
+ {
+ "name": "image",
+ "description": "Name of the vulnerable container image",
+ "args": [
+
+ ],
+ "type": {
+ "kind": "SCALAR",
+ "name": "String",
+ "ofType": null
+ },
+ "isDeprecated": false,
+ "deprecationReason": null
+ },
+ {
+ "name": "operatingSystem",
+ "description": "Operating system that runs on the vulnerable container image",
+ "args": [
+
+ ],
+ "type": {
+ "kind": "SCALAR",
+ "name": "String",
+ "ofType": null
+ },
+ "isDeprecated": false,
+ "deprecationReason": null
+ }
+ ],
+ "inputFields": null,
+ "interfaces": [
+
+ ],
+ "enumValues": null,
+ "possibleTypes": null
+ },
+ {
+ "kind": "OBJECT",
+ "name": "VulnerabilityLocationDast",
+ "description": "Represents the location of a vulnerability found by a DAST scan",
+ "fields": [
+ {
+ "name": "hostname",
+ "description": "Domain name of the vulnerable request",
+ "args": [
+
+ ],
+ "type": {
+ "kind": "SCALAR",
+ "name": "String",
+ "ofType": null
+ },
+ "isDeprecated": false,
+ "deprecationReason": null
+ },
+ {
+ "name": "param",
+ "description": "Query parameter for the URL on which the vulnerability occurred",
+ "args": [
+
+ ],
+ "type": {
+ "kind": "SCALAR",
+ "name": "String",
+ "ofType": null
+ },
+ "isDeprecated": false,
+ "deprecationReason": null
+ },
+ {
+ "name": "path",
+ "description": "URL path and query string of the vulnerable request",
+ "args": [
+
+ ],
+ "type": {
+ "kind": "SCALAR",
+ "name": "String",
+ "ofType": null
+ },
+ "isDeprecated": false,
+ "deprecationReason": null
+ },
+ {
+ "name": "requestMethod",
+ "description": "HTTP method of the vulnerable request",
+ "args": [
+
+ ],
+ "type": {
+ "kind": "SCALAR",
+ "name": "String",
+ "ofType": null
+ },
+ "isDeprecated": false,
+ "deprecationReason": null
+ }
+ ],
+ "inputFields": null,
+ "interfaces": [
+
+ ],
+ "enumValues": null,
+ "possibleTypes": null
+ },
+ {
+ "kind": "OBJECT",
+ "name": "VulnerabilityLocationDependencyScanning",
+ "description": "Represents the location of a vulnerability found by a dependency security scan",
+ "fields": [
+ {
+ "name": "dependency",
+ "description": "Dependency containing the vulnerability",
+ "args": [
+
+ ],
+ "type": {
+ "kind": "OBJECT",
+ "name": "VulnerableDependency",
+ "ofType": null
+ },
+ "isDeprecated": false,
+ "deprecationReason": null
+ },
+ {
+ "name": "file",
+ "description": "Path to the vulnerable file",
+ "args": [
+
+ ],
+ "type": {
+ "kind": "SCALAR",
+ "name": "String",
+ "ofType": null
+ },
+ "isDeprecated": false,
+ "deprecationReason": null
+ }
+ ],
+ "inputFields": null,
+ "interfaces": [
+
+ ],
+ "enumValues": null,
+ "possibleTypes": null
+ },
+ {
+ "kind": "OBJECT",
+ "name": "VulnerabilityLocationSast",
+ "description": "Represents the location of a vulnerability found by a SAST scan",
+ "fields": [
+ {
+ "name": "endLine",
+ "description": "Number of the last relevant line in the vulnerable file",
+ "args": [
+
+ ],
+ "type": {
+ "kind": "SCALAR",
+ "name": "String",
+ "ofType": null
+ },
+ "isDeprecated": false,
+ "deprecationReason": null
+ },
+ {
+ "name": "file",
+ "description": "Path to the vulnerable file",
+ "args": [
+
+ ],
+ "type": {
+ "kind": "SCALAR",
+ "name": "String",
+ "ofType": null
+ },
+ "isDeprecated": false,
+ "deprecationReason": null
+ },
+ {
+ "name": "startLine",
+ "description": "Number of the first relevant line in the vulnerable file",
+ "args": [
+
+ ],
+ "type": {
+ "kind": "SCALAR",
+ "name": "String",
+ "ofType": null
+ },
+ "isDeprecated": false,
+ "deprecationReason": null
+ },
+ {
+ "name": "vulnerableClass",
+ "description": "Class containing the vulnerability",
+ "args": [
+
+ ],
+ "type": {
+ "kind": "SCALAR",
+ "name": "String",
+ "ofType": null
+ },
+ "isDeprecated": false,
+ "deprecationReason": null
+ },
+ {
+ "name": "vulnerableMethod",
+ "description": "Method containing the vulnerability",
+ "args": [
+
+ ],
+ "type": {
+ "kind": "SCALAR",
+ "name": "String",
+ "ofType": null
+ },
+ "isDeprecated": false,
+ "deprecationReason": null
+ }
+ ],
+ "inputFields": null,
+ "interfaces": [
+
+ ],
+ "enumValues": null,
+ "possibleTypes": null
+ },
+ {
"kind": "OBJECT",
"name": "VulnerabilityPermissions",
"description": "Check permissions for the current user on a vulnerability",
@@ -29744,6 +34862,74 @@
},
{
"kind": "OBJECT",
+ "name": "VulnerableDependency",
+ "description": "Represents a vulnerable dependency. Used in vulnerability location data",
+ "fields": [
+ {
+ "name": "package",
+ "description": "The package associated with the vulnerable dependency",
+ "args": [
+
+ ],
+ "type": {
+ "kind": "OBJECT",
+ "name": "VulnerablePackage",
+ "ofType": null
+ },
+ "isDeprecated": false,
+ "deprecationReason": null
+ },
+ {
+ "name": "version",
+ "description": "The version of the vulnerable dependency",
+ "args": [
+
+ ],
+ "type": {
+ "kind": "SCALAR",
+ "name": "String",
+ "ofType": null
+ },
+ "isDeprecated": false,
+ "deprecationReason": null
+ }
+ ],
+ "inputFields": null,
+ "interfaces": [
+
+ ],
+ "enumValues": null,
+ "possibleTypes": null
+ },
+ {
+ "kind": "OBJECT",
+ "name": "VulnerablePackage",
+ "description": "Represents a vulnerable package. Used in vulnerability dependency data",
+ "fields": [
+ {
+ "name": "name",
+ "description": "The name of the vulnerable package",
+ "args": [
+
+ ],
+ "type": {
+ "kind": "SCALAR",
+ "name": "String",
+ "ofType": null
+ },
+ "isDeprecated": false,
+ "deprecationReason": null
+ }
+ ],
+ "inputFields": null,
+ "interfaces": [
+
+ ],
+ "enumValues": null,
+ "possibleTypes": null
+ },
+ {
+ "kind": "OBJECT",
"name": "__Directive",
"description": "A Directive provides a way to describe alternate runtime execution and type validation behavior in a GraphQL document.\n\nIn some cases, you need to provide options to alter GraphQL's execution behavior in ways field arguments will not suffice, such as conditionally including or skipping a field. Directives provide this by describing additional information to the executor.",
"fields": [
diff --git a/doc/api/graphql/reference/index.md b/doc/api/graphql/reference/index.md
index 4d3d77ba35f..4164c26e751 100644
--- a/doc/api/graphql/reference/index.md
+++ b/doc/api/graphql/reference/index.md
@@ -24,7 +24,17 @@ Autogenerated return type of AddAwardEmoji
| --- | ---- | ---------- |
| `awardEmoji` | AwardEmoji | The award emoji after mutation |
| `clientMutationId` | String | A unique identifier for the client performing the mutation. |
-| `errors` | String! => Array | Reasons why the mutation failed. |
+| `errors` | String! => Array | Errors encountered during execution of the mutation. |
+
+## AddProjectToSecurityDashboardPayload
+
+Autogenerated return type of AddProjectToSecurityDashboard
+
+| Name | Type | Description |
+| --- | ---- | ---------- |
+| `clientMutationId` | String | A unique identifier for the client performing the mutation. |
+| `errors` | String! => Array | Errors encountered during execution of the mutation. |
+| `project` | Project | Project that was added to the Instance Security Dashboard |
## AdminSidekiqQueuesDeleteJobsPayload
@@ -33,9 +43,44 @@ Autogenerated return type of AdminSidekiqQueuesDeleteJobs
| Name | Type | Description |
| --- | ---- | ---------- |
| `clientMutationId` | String | A unique identifier for the client performing the mutation. |
-| `errors` | String! => Array | Reasons why the mutation failed. |
+| `errors` | String! => Array | Errors encountered during execution of the mutation. |
| `result` | DeleteJobsResponse | Information about the status of the deletion request |
+## AlertManagementAlert
+
+Describes an alert from the project's Alert Management
+
+| Name | Type | Description |
+| --- | ---- | ---------- |
+| `createdAt` | Time | Timestamp the alert was created |
+| `description` | String | Description of the alert |
+| `details` | JSON | Alert details |
+| `endedAt` | Time | Timestamp the alert ended |
+| `eventCount` | Int | Number of events of this alert |
+| `hosts` | String! => Array | List of hosts the alert came from |
+| `iid` | ID! | Internal ID of the alert |
+| `issueIid` | ID | Internal ID of the GitLab issue attached to the alert |
+| `monitoringTool` | String | Monitoring tool the alert came from |
+| `service` | String | Service the alert came from |
+| `severity` | AlertManagementSeverity | Severity of the alert |
+| `startedAt` | Time | Timestamp the alert was raised |
+| `status` | AlertManagementStatus | Status of the alert |
+| `title` | String | Title of the alert |
+| `updatedAt` | Time | Timestamp the alert was last updated |
+
+## AlertManagementAlertStatusCountsType
+
+Represents total number of alerts for the represented categories
+
+| Name | Type | Description |
+| --- | ---- | ---------- |
+| `acknowledged` | Int | Number of alerts with status ACKNOWLEDGED for the project |
+| `all` | Int | Total number of alerts for the project |
+| `ignored` | Int | Number of alerts with status IGNORED for the project |
+| `open` | Int | Number of alerts with status TRIGGERED or ACKNOWLEDGED for the project |
+| `resolved` | Int | Number of alerts with status RESOLVED for the project |
+| `triggered` | Int | Number of alerts with status TRIGGERED for the project |
+
## AwardEmoji
An emoji awarded by a user.
@@ -79,6 +124,41 @@ Represents a project or group board
| `name` | String | Name of the board |
| `weight` | Int | Weight of the board |
+## BoardList
+
+Represents a list for an issue board
+
+| Name | Type | Description |
+| --- | ---- | ---------- |
+| `assignee` | User | Assignee in the list |
+| `collapsed` | Boolean | Indicates if list is collapsed for this user |
+| `id` | ID! | ID (global ID) of the list |
+| `label` | Label | Label of the list |
+| `limitMetric` | ListLimitMetric | The current limit metric for the list |
+| `listType` | String! | Type of the list |
+| `maxIssueCount` | Int | Maximum number of issues in the list |
+| `maxIssueWeight` | Int | Maximum weight of issues in the list |
+| `milestone` | Milestone | Milestone of the list |
+| `position` | Int | Position of list within the board |
+| `title` | String! | Title of the list |
+
+## BoardListUpdateLimitMetricsPayload
+
+Autogenerated return type of BoardListUpdateLimitMetrics
+
+| Name | Type | Description |
+| --- | ---- | ---------- |
+| `clientMutationId` | String | A unique identifier for the client performing the mutation. |
+| `errors` | String! => Array | Errors encountered during execution of the mutation. |
+| `list` | BoardList | The updated list |
+
+## Branch
+
+| Name | Type | Description |
+| --- | ---- | ---------- |
+| `commit` | Commit | Commit for the branch |
+| `name` | String! | Name of the branch |
+
## Commit
| Name | Type | Description |
@@ -94,8 +174,40 @@ Represents a project or group board
| `sha` | String! | SHA1 ID of the commit |
| `signatureHtml` | String | Rendered HTML of the commit signature |
| `title` | String | Title of the commit message |
+| `titleHtml` | String | The GitLab Flavored Markdown rendering of `title` |
| `webUrl` | String! | Web URL of the commit |
+## CreateAlertIssuePayload
+
+Autogenerated return type of CreateAlertIssue
+
+| Name | Type | Description |
+| --- | ---- | ---------- |
+| `alert` | AlertManagementAlert | The alert after mutation |
+| `clientMutationId` | String | A unique identifier for the client performing the mutation. |
+| `errors` | String! => Array | Errors encountered during execution of the mutation. |
+| `issue` | Issue | The issue created after mutation |
+
+## CreateAnnotationPayload
+
+Autogenerated return type of CreateAnnotation
+
+| Name | Type | Description |
+| --- | ---- | ---------- |
+| `annotation` | MetricsDashboardAnnotation | The created annotation |
+| `clientMutationId` | String | A unique identifier for the client performing the mutation. |
+| `errors` | String! => Array | Errors encountered during execution of the mutation. |
+
+## CreateBranchPayload
+
+Autogenerated return type of CreateBranch
+
+| Name | Type | Description |
+| --- | ---- | ---------- |
+| `branch` | Branch | Branch after mutation |
+| `clientMutationId` | String | A unique identifier for the client performing the mutation. |
+| `errors` | String! => Array | Errors encountered during execution of the mutation. |
+
## CreateDiffNotePayload
Autogenerated return type of CreateDiffNote
@@ -103,7 +215,7 @@ Autogenerated return type of CreateDiffNote
| Name | Type | Description |
| --- | ---- | ---------- |
| `clientMutationId` | String | A unique identifier for the client performing the mutation. |
-| `errors` | String! => Array | Reasons why the mutation failed. |
+| `errors` | String! => Array | Errors encountered during execution of the mutation. |
| `note` | Note | The note after mutation |
## CreateEpicPayload
@@ -114,7 +226,7 @@ Autogenerated return type of CreateEpic
| --- | ---- | ---------- |
| `clientMutationId` | String | A unique identifier for the client performing the mutation. |
| `epic` | Epic | The created epic |
-| `errors` | String! => Array | Reasons why the mutation failed. |
+| `errors` | String! => Array | Errors encountered during execution of the mutation. |
## CreateImageDiffNotePayload
@@ -123,9 +235,19 @@ Autogenerated return type of CreateImageDiffNote
| Name | Type | Description |
| --- | ---- | ---------- |
| `clientMutationId` | String | A unique identifier for the client performing the mutation. |
-| `errors` | String! => Array | Reasons why the mutation failed. |
+| `errors` | String! => Array | Errors encountered during execution of the mutation. |
| `note` | Note | The note after mutation |
+## CreateIterationPayload
+
+Autogenerated return type of CreateIteration
+
+| Name | Type | Description |
+| --- | ---- | ---------- |
+| `clientMutationId` | String | A unique identifier for the client performing the mutation. |
+| `errors` | String! => Array | Errors encountered during execution of the mutation. |
+| `iteration` | Iteration | The created iteration |
+
## CreateNotePayload
Autogenerated return type of CreateNote
@@ -133,7 +255,7 @@ Autogenerated return type of CreateNote
| Name | Type | Description |
| --- | ---- | ---------- |
| `clientMutationId` | String | A unique identifier for the client performing the mutation. |
-| `errors` | String! => Array | Reasons why the mutation failed. |
+| `errors` | String! => Array | Errors encountered during execution of the mutation. |
| `note` | Note | The note after mutation |
## CreateRequirementPayload
@@ -143,7 +265,7 @@ Autogenerated return type of CreateRequirement
| Name | Type | Description |
| --- | ---- | ---------- |
| `clientMutationId` | String | A unique identifier for the client performing the mutation. |
-| `errors` | String! => Array | Reasons why the mutation failed. |
+| `errors` | String! => Array | Errors encountered during execution of the mutation. |
| `requirement` | Requirement | The requirement after mutation |
## CreateSnippetPayload
@@ -153,7 +275,7 @@ Autogenerated return type of CreateSnippet
| Name | Type | Description |
| --- | ---- | ---------- |
| `clientMutationId` | String | A unique identifier for the client performing the mutation. |
-| `errors` | String! => Array | Reasons why the mutation failed. |
+| `errors` | String! => Array | Errors encountered during execution of the mutation. |
| `snippet` | Snippet | The snippet after mutation |
## DeleteJobsResponse
@@ -228,7 +350,7 @@ Autogenerated return type of DesignManagementDelete
| Name | Type | Description |
| --- | ---- | ---------- |
| `clientMutationId` | String | A unique identifier for the client performing the mutation. |
-| `errors` | String! => Array | Reasons why the mutation failed. |
+| `errors` | String! => Array | Errors encountered during execution of the mutation. |
| `version` | DesignVersion | The new version in which the designs are deleted |
## DesignManagementUploadPayload
@@ -239,7 +361,7 @@ Autogenerated return type of DesignManagementUpload
| --- | ---- | ---------- |
| `clientMutationId` | String | A unique identifier for the client performing the mutation. |
| `designs` | Design! => Array | The designs that were uploaded by the mutation |
-| `errors` | String! => Array | Reasons why the mutation failed. |
+| `errors` | String! => Array | Errors encountered during execution of the mutation. |
| `skippedDesigns` | Design! => Array | Any designs that were skipped from the upload due to there being no change to their content since their last version |
## DesignVersion
@@ -259,7 +381,7 @@ Autogenerated return type of DestroyNote
| Name | Type | Description |
| --- | ---- | ---------- |
| `clientMutationId` | String | A unique identifier for the client performing the mutation. |
-| `errors` | String! => Array | Reasons why the mutation failed. |
+| `errors` | String! => Array | Errors encountered during execution of the mutation. |
| `note` | Note | The note after mutation |
## DestroySnippetPayload
@@ -269,7 +391,7 @@ Autogenerated return type of DestroySnippet
| Name | Type | Description |
| --- | ---- | ---------- |
| `clientMutationId` | String | A unique identifier for the client performing the mutation. |
-| `errors` | String! => Array | Reasons why the mutation failed. |
+| `errors` | String! => Array | Errors encountered during execution of the mutation. |
| `snippet` | Snippet | The snippet after mutation |
## DetailedStatus
@@ -324,7 +446,7 @@ Autogenerated return type of DismissVulnerability
| Name | Type | Description |
| --- | ---- | ---------- |
| `clientMutationId` | String | A unique identifier for the client performing the mutation. |
-| `errors` | String! => Array | Reasons why the mutation failed. |
+| `errors` | String! => Array | Errors encountered during execution of the mutation. |
| `vulnerability` | Vulnerability | The vulnerability after dismissal |
## Environment
@@ -389,7 +511,7 @@ Autogenerated return type of EpicAddIssue
| `clientMutationId` | String | A unique identifier for the client performing the mutation. |
| `epic` | Epic | The epic after mutation |
| `epicIssue` | EpicIssue | The epic-issue relation |
-| `errors` | String! => Array | Reasons why the mutation failed. |
+| `errors` | String! => Array | Errors encountered during execution of the mutation. |
## EpicDescendantCount
@@ -443,6 +565,7 @@ Relationship between an epic and an issue
| `healthStatus` | HealthStatus | Current health status. Returns null if `save_issuable_health_status` feature flag is disabled. |
| `id` | ID | Global ID of the epic-issue relation |
| `iid` | ID! | Internal ID of the issue |
+| `iteration` | Iteration | Iteration of the issue |
| `milestone` | Milestone | Milestone of the issue |
| `reference` | String! | Internal reference of the issue. Returned in shortened format by default |
| `relationPath` | String | URI path of the epic-issue relation |
@@ -485,7 +608,7 @@ Autogenerated return type of EpicSetSubscription
| --- | ---- | ---------- |
| `clientMutationId` | String | A unique identifier for the client performing the mutation. |
| `epic` | Epic | The epic after mutation |
-| `errors` | String! => Array | Reasons why the mutation failed. |
+| `errors` | String! => Array | Errors encountered during execution of the mutation. |
## EpicTreeReorderPayload
@@ -494,7 +617,7 @@ Autogenerated return type of EpicTreeReorder
| Name | Type | Description |
| --- | ---- | ---------- |
| `clientMutationId` | String | A unique identifier for the client performing the mutation. |
-| `errors` | String! => Array | Reasons why the mutation failed. |
+| `errors` | String! => Array | Errors encountered during execution of the mutation. |
## GeoNode
@@ -521,7 +644,7 @@ Autogenerated return type of EpicTreeReorder
| --- | ---- | ---------- |
| `createdAt` | Time! | Timestamp of the issue's creation |
| `enabled` | Boolean! | Indicates whether Grafana integration is enabled |
-| `grafanaUrl` | String! | Url for the Grafana host for the Grafana integration |
+| `grafanaUrl` | String! | URL for the Grafana host for the Grafana integration |
| `id` | ID! | Internal ID of the Grafana integration |
| `token` **{warning-solid}** | String! | **Deprecated:** Plain text token has been masked for security reasons. Deprecated in 12.7 |
| `updatedAt` | Time! | Timestamp of the issue's last activity |
@@ -582,6 +705,7 @@ Autogenerated return type of EpicTreeReorder
| `epic` | Epic | Epic to which this issue belongs |
| `healthStatus` | HealthStatus | Current health status. Returns null if `save_issuable_health_status` feature flag is disabled. |
| `iid` | ID! | Internal ID of the issue |
+| `iteration` | Iteration | Iteration of the issue |
| `milestone` | Milestone | Milestone of the issue |
| `reference` | String! | Internal reference of the issue. Returned in shortened format by default |
| `relativePosition` | Int | Relative position of the issue (used for positioning in epic tree and issue boards) |
@@ -622,7 +746,7 @@ Autogenerated return type of IssueSetConfidential
| Name | Type | Description |
| --- | ---- | ---------- |
| `clientMutationId` | String | A unique identifier for the client performing the mutation. |
-| `errors` | String! => Array | Reasons why the mutation failed. |
+| `errors` | String! => Array | Errors encountered during execution of the mutation. |
| `issue` | Issue | The issue after mutation |
## IssueSetDueDatePayload
@@ -632,7 +756,17 @@ Autogenerated return type of IssueSetDueDate
| Name | Type | Description |
| --- | ---- | ---------- |
| `clientMutationId` | String | A unique identifier for the client performing the mutation. |
-| `errors` | String! => Array | Reasons why the mutation failed. |
+| `errors` | String! => Array | Errors encountered during execution of the mutation. |
+| `issue` | Issue | The issue after mutation |
+
+## IssueSetIterationPayload
+
+Autogenerated return type of IssueSetIteration
+
+| Name | Type | Description |
+| --- | ---- | ---------- |
+| `clientMutationId` | String | A unique identifier for the client performing the mutation. |
+| `errors` | String! => Array | Errors encountered during execution of the mutation. |
| `issue` | Issue | The issue after mutation |
## IssueSetWeightPayload
@@ -642,15 +776,33 @@ Autogenerated return type of IssueSetWeight
| Name | Type | Description |
| --- | ---- | ---------- |
| `clientMutationId` | String | A unique identifier for the client performing the mutation. |
-| `errors` | String! => Array | Reasons why the mutation failed. |
+| `errors` | String! => Array | Errors encountered during execution of the mutation. |
| `issue` | Issue | The issue after mutation |
+## Iteration
+
+Represents an iteration object.
+
+| Name | Type | Description |
+| --- | ---- | ---------- |
+| `createdAt` | Time! | Timestamp of iteration creation |
+| `description` | String | Description of the iteration |
+| `dueDate` | Time | Timestamp of the iteration due date |
+| `id` | ID! | ID of the iteration |
+| `startDate` | Time | Timestamp of the iteration start date |
+| `state` | IterationState! | State of the iteration |
+| `title` | String! | Title of the iteration |
+| `updatedAt` | Time! | Timestamp of last iteration update |
+| `webPath` | String! | Web path of the iteration |
+| `webUrl` | String! | Web URL of the iteration |
+
## JiraImport
| Name | Type | Description |
| --- | ---- | ---------- |
+| `createdAt` | Time | Timestamp of when the Jira import was created |
| `jiraProjectKey` | String! | Project key for the imported Jira project |
-| `scheduledAt` | Time | Timestamp of when the Jira import was created |
+| `scheduledAt` | Time | Timestamp of when the Jira import was scheduled |
| `scheduledBy` | User | User that started the Jira import |
## JiraImportStartPayload
@@ -660,7 +812,7 @@ Autogenerated return type of JiraImportStart
| Name | Type | Description |
| --- | ---- | ---------- |
| `clientMutationId` | String | A unique identifier for the client performing the mutation. |
-| `errors` | String! => Array | Reasons why the mutation failed. |
+| `errors` | String! => Array | Errors encountered during execution of the mutation. |
| `jiraImport` | JiraImport | The Jira import data after mutation |
## JiraService
@@ -688,7 +840,7 @@ Autogenerated return type of MarkAsSpamSnippet
| Name | Type | Description |
| --- | ---- | ---------- |
| `clientMutationId` | String | A unique identifier for the client performing the mutation. |
-| `errors` | String! => Array | Reasons why the mutation failed. |
+| `errors` | String! => Array | Errors encountered during execution of the mutation. |
| `snippet` | Snippet | The snippet after mutation |
## MergeRequest
@@ -767,7 +919,7 @@ Autogenerated return type of MergeRequestSetAssignees
| Name | Type | Description |
| --- | ---- | ---------- |
| `clientMutationId` | String | A unique identifier for the client performing the mutation. |
-| `errors` | String! => Array | Reasons why the mutation failed. |
+| `errors` | String! => Array | Errors encountered during execution of the mutation. |
| `mergeRequest` | MergeRequest | The merge request after mutation |
## MergeRequestSetLabelsPayload
@@ -777,7 +929,7 @@ Autogenerated return type of MergeRequestSetLabels
| Name | Type | Description |
| --- | ---- | ---------- |
| `clientMutationId` | String | A unique identifier for the client performing the mutation. |
-| `errors` | String! => Array | Reasons why the mutation failed. |
+| `errors` | String! => Array | Errors encountered during execution of the mutation. |
| `mergeRequest` | MergeRequest | The merge request after mutation |
## MergeRequestSetLockedPayload
@@ -787,7 +939,7 @@ Autogenerated return type of MergeRequestSetLocked
| Name | Type | Description |
| --- | ---- | ---------- |
| `clientMutationId` | String | A unique identifier for the client performing the mutation. |
-| `errors` | String! => Array | Reasons why the mutation failed. |
+| `errors` | String! => Array | Errors encountered during execution of the mutation. |
| `mergeRequest` | MergeRequest | The merge request after mutation |
## MergeRequestSetMilestonePayload
@@ -797,7 +949,7 @@ Autogenerated return type of MergeRequestSetMilestone
| Name | Type | Description |
| --- | ---- | ---------- |
| `clientMutationId` | String | A unique identifier for the client performing the mutation. |
-| `errors` | String! => Array | Reasons why the mutation failed. |
+| `errors` | String! => Array | Errors encountered during execution of the mutation. |
| `mergeRequest` | MergeRequest | The merge request after mutation |
## MergeRequestSetSubscriptionPayload
@@ -807,7 +959,7 @@ Autogenerated return type of MergeRequestSetSubscription
| Name | Type | Description |
| --- | ---- | ---------- |
| `clientMutationId` | String | A unique identifier for the client performing the mutation. |
-| `errors` | String! => Array | Reasons why the mutation failed. |
+| `errors` | String! => Array | Errors encountered during execution of the mutation. |
| `mergeRequest` | MergeRequest | The merge request after mutation |
## MergeRequestSetWipPayload
@@ -817,7 +969,7 @@ Autogenerated return type of MergeRequestSetWip
| Name | Type | Description |
| --- | ---- | ---------- |
| `clientMutationId` | String | A unique identifier for the client performing the mutation. |
-| `errors` | String! => Array | Reasons why the mutation failed. |
+| `errors` | String! => Array | Errors encountered during execution of the mutation. |
| `mergeRequest` | MergeRequest | The merge request after mutation |
## Metadata
@@ -838,10 +990,10 @@ Autogenerated return type of MergeRequestSetWip
| Name | Type | Description |
| --- | ---- | ---------- |
| `description` | String | Description of the annotation |
-| `endingAt` | String | Timestamp marking end of annotated time span |
+| `endingAt` | Time | Timestamp marking end of annotated time span |
| `id` | ID! | ID of the annotation |
| `panelId` | String | ID of a dashboard panel to which the annotation should be scoped |
-| `startingAt` | String | Timestamp marking start of annotated time span |
+| `startingAt` | Time | Timestamp marking start of annotated time span |
## Milestone
@@ -905,6 +1057,34 @@ Represents a milestone.
| `readNote` | Boolean! | Indicates the user can perform `read_note` on this resource |
| `resolveNote` | Boolean! | Indicates the user can perform `resolve_note` on this resource |
+## Package
+
+Represents a package
+
+| Name | Type | Description |
+| --- | ---- | ---------- |
+| `createdAt` | Time! | The created date |
+| `id` | ID! | The ID of the package |
+| `name` | String! | The name of the package |
+| `packageType` | PackageTypeEnum! | The type of the package |
+| `updatedAt` | Time! | The update date |
+| `version` | String | The version of the package |
+
+## PackageFileRegistry
+
+Represents the sync and verification state of a package file
+
+| Name | Type | Description |
+| --- | ---- | ---------- |
+| `createdAt` | Time | Timestamp when the PackageFileRegistry was created |
+| `id` | ID! | ID of the PackageFileRegistry |
+| `lastSyncFailure` | String | Error message during sync of the PackageFileRegistry |
+| `lastSyncedAt` | Time | Timestamp of the most recent successful sync of the PackageFileRegistry |
+| `packageFileId` | ID! | ID of the PackageFile |
+| `retryAt` | Time | Timestamp after which the PackageFileRegistry should be resynced |
+| `retryCount` | Int | Number of consecutive failed sync attempts of the PackageFileRegistry |
+| `state` | RegistryState | Sync state of the PackageFileRegistry |
+
## PageInfo
Information about pagination in a connection.
@@ -947,6 +1127,8 @@ Information about pagination in a connection.
| Name | Type | Description |
| --- | ---- | ---------- |
+| `alertManagementAlert` | AlertManagementAlert | A single Alert Management alert of the project |
+| `alertManagementAlertStatusCounts` | AlertManagementAlertStatusCountsType | Counts of alerts by status for the project |
| `archived` | Boolean | Indicates the archived status of the project |
| `autocloseReferencedIssues` | Boolean | Indicates if issues referenced by merge requests and commits within the default branch are closed automatically |
| `avatarUrl` | String | URL to avatar image file of the project |
@@ -980,6 +1162,7 @@ Information about pagination in a connection.
| `path` | String! | Path of the project |
| `printingMergeRequestLinkEnabled` | Boolean | Indicates if a link to create or view a merge request should display after a push to Git repositories of the project from the command line |
| `publicJobs` | Boolean | Indicates if there is public access to pipelines and job details of the project, including output logs and artifacts |
+| `release` | Release | A single release of the project. Available only when feature flag `graphql_release_data` is enabled |
| `removeSourceBranchAfterMerge` | Boolean | Indicates if `Delete source branch` option should be enabled by default for all new merge requests of the project |
| `repository` | Repository | Git repository of the project |
| `requestAccessEnabled` | Boolean | Indicates if users can request member access to the project |
@@ -998,7 +1181,7 @@ Information about pagination in a connection.
| `tagList` | String | List of project topics (not Git tags) |
| `userPermissions` | ProjectPermissions! | Permissions for the current user on the resource |
| `visibility` | String | Visibility of the project |
-| `vulnerabilitySeveritiesCount` | VulnerabilitySeveritiesCount | Counts for each severity of vulnerability of the project. Available only when feature flag `first_class_vulnerabilities` is enabled |
+| `vulnerabilitySeveritiesCount` | VulnerabilitySeveritiesCount | Counts for each severity of vulnerability of the project |
| `webUrl` | String | Web URL of the project |
| `wikiEnabled` | Boolean | Indicates if Wikis are enabled for the current user |
@@ -1061,6 +1244,20 @@ Information about pagination in a connection.
| `storageSize` | Float! | Storage size of the project |
| `wikiSize` | Float | Wiki size of the project |
+## Release
+
+| Name | Type | Description |
+| --- | ---- | ---------- |
+| `author` | User | User that created the release |
+| `commit` | Commit | The commit associated with the release |
+| `createdAt` | Time | Timestamp of when the release was created |
+| `description` | String | Description (also known as "release notes") of the release |
+| `descriptionHtml` | String | The GitLab Flavored Markdown rendering of `description` |
+| `name` | String | Name of the release |
+| `releasedAt` | Time | Timestamp of when the release was released |
+| `tagName` | String! | Name of the tag associated with the release |
+| `tagPath` | String | Relative web path to the tag associated with the release |
+
## RemoveAwardEmojiPayload
Autogenerated return type of RemoveAwardEmoji
@@ -1069,7 +1266,16 @@ Autogenerated return type of RemoveAwardEmoji
| --- | ---- | ---------- |
| `awardEmoji` | AwardEmoji | The award emoji after mutation |
| `clientMutationId` | String | A unique identifier for the client performing the mutation. |
-| `errors` | String! => Array | Reasons why the mutation failed. |
+| `errors` | String! => Array | Errors encountered during execution of the mutation. |
+
+## RemoveProjectFromSecurityDashboardPayload
+
+Autogenerated return type of RemoveProjectFromSecurityDashboard
+
+| Name | Type | Description |
+| --- | ---- | ---------- |
+| `clientMutationId` | String | A unique identifier for the client performing the mutation. |
+| `errors` | String! => Array | Errors encountered during execution of the mutation. |
## Repository
@@ -1256,7 +1462,7 @@ Represents a snippet entry
| `descriptionHtml` | String | The GitLab Flavored Markdown rendering of `description` |
| `fileName` | String | File Name of the snippet |
| `httpUrlToRepo` | String | HTTP URL to the snippet repository |
-| `id` | ID! | Id of the snippet |
+| `id` | ID! | ID of the snippet |
| `project` | Project | The project the snippet is associated with |
| `rawUrl` | String! | Raw URL of the snippet |
| `sshUrlToRepo` | String | SSH URL to the snippet repository |
@@ -1273,11 +1479,13 @@ Represents the snippet blob
| Name | Type | Description |
| --- | ---- | ---------- |
| `binary` | Boolean! | Shows whether the blob is binary |
+| `externalStorage` | String | Blob external storage |
| `mode` | String | Blob mode |
| `name` | String | Blob name |
| `path` | String | Blob path |
| `plainData` | String | Blob plain highlighted data |
| `rawPath` | String! | Blob raw content endpoint path |
+| `renderedAsText` | Boolean! | Shows whether the blob is rendered as text |
| `richData` | String | Blob highlighted data |
| `richViewer` | SnippetBlobViewer | Blob content rich viewer |
| `simpleViewer` | SnippetBlobViewer! | Blob content simple viewer |
@@ -1351,7 +1559,7 @@ Representing a todo entry
| `body` | String! | Body of the todo |
| `createdAt` | Time! | Timestamp this todo was created |
| `group` | Group | Group this todo is associated with |
-| `id` | ID! | Id of the todo |
+| `id` | ID! | ID of the todo |
| `project` | Project | The project this todo is associated with |
| `state` | TodoStateEnum! | State of the todo |
| `targetType` | TodoTargetEnum! | Target type of the todo |
@@ -1363,7 +1571,7 @@ Autogenerated return type of TodoMarkDone
| Name | Type | Description |
| --- | ---- | ---------- |
| `clientMutationId` | String | A unique identifier for the client performing the mutation. |
-| `errors` | String! => Array | Reasons why the mutation failed. |
+| `errors` | String! => Array | Errors encountered during execution of the mutation. |
| `todo` | Todo! | The requested todo |
## TodoRestoreManyPayload
@@ -1373,7 +1581,7 @@ Autogenerated return type of TodoRestoreMany
| Name | Type | Description |
| --- | ---- | ---------- |
| `clientMutationId` | String | A unique identifier for the client performing the mutation. |
-| `errors` | String! => Array | Reasons why the mutation failed. |
+| `errors` | String! => Array | Errors encountered during execution of the mutation. |
| `updatedIds` | ID! => Array | The ids of the updated todo items |
## TodoRestorePayload
@@ -1383,7 +1591,7 @@ Autogenerated return type of TodoRestore
| Name | Type | Description |
| --- | ---- | ---------- |
| `clientMutationId` | String | A unique identifier for the client performing the mutation. |
-| `errors` | String! => Array | Reasons why the mutation failed. |
+| `errors` | String! => Array | Errors encountered during execution of the mutation. |
| `todo` | Todo! | The requested todo |
## TodosMarkAllDonePayload
@@ -1393,7 +1601,7 @@ Autogenerated return type of TodosMarkAllDone
| Name | Type | Description |
| --- | ---- | ---------- |
| `clientMutationId` | String | A unique identifier for the client performing the mutation. |
-| `errors` | String! => Array | Reasons why the mutation failed. |
+| `errors` | String! => Array | Errors encountered during execution of the mutation. |
| `updatedIds` | ID! => Array | Ids of the updated todos |
## ToggleAwardEmojiPayload
@@ -1404,7 +1612,7 @@ Autogenerated return type of ToggleAwardEmoji
| --- | ---- | ---------- |
| `awardEmoji` | AwardEmoji | The award emoji after mutation |
| `clientMutationId` | String | A unique identifier for the client performing the mutation. |
-| `errors` | String! => Array | Reasons why the mutation failed. |
+| `errors` | String! => Array | Errors encountered during execution of the mutation. |
| `toggledOn` | Boolean! | Indicates the status of the emoji. True if the toggle awarded the emoji, and false if the toggle removed the emoji. |
## Tree
@@ -1427,6 +1635,17 @@ Represents a directory
| `type` | EntryType! | Type of tree entry |
| `webUrl` | String | Web URL for the tree entry (directory) |
+## UpdateAlertStatusPayload
+
+Autogenerated return type of UpdateAlertStatus
+
+| Name | Type | Description |
+| --- | ---- | ---------- |
+| `alert` | AlertManagementAlert | The alert after mutation |
+| `clientMutationId` | String | A unique identifier for the client performing the mutation. |
+| `errors` | String! => Array | Errors encountered during execution of the mutation. |
+| `issue` | Issue | The issue created after mutation |
+
## UpdateEpicPayload
Autogenerated return type of UpdateEpic
@@ -1435,7 +1654,7 @@ Autogenerated return type of UpdateEpic
| --- | ---- | ---------- |
| `clientMutationId` | String | A unique identifier for the client performing the mutation. |
| `epic` | Epic | The epic after mutation |
-| `errors` | String! => Array | Reasons why the mutation failed. |
+| `errors` | String! => Array | Errors encountered during execution of the mutation. |
## UpdateImageDiffNotePayload
@@ -1444,7 +1663,7 @@ Autogenerated return type of UpdateImageDiffNote
| Name | Type | Description |
| --- | ---- | ---------- |
| `clientMutationId` | String | A unique identifier for the client performing the mutation. |
-| `errors` | String! => Array | Reasons why the mutation failed. |
+| `errors` | String! => Array | Errors encountered during execution of the mutation. |
| `note` | Note | The note after mutation |
## UpdateIssuePayload
@@ -1454,7 +1673,7 @@ Autogenerated return type of UpdateIssue
| Name | Type | Description |
| --- | ---- | ---------- |
| `clientMutationId` | String | A unique identifier for the client performing the mutation. |
-| `errors` | String! => Array | Reasons why the mutation failed. |
+| `errors` | String! => Array | Errors encountered during execution of the mutation. |
| `issue` | Issue | The issue after mutation |
## UpdateNotePayload
@@ -1464,7 +1683,7 @@ Autogenerated return type of UpdateNote
| Name | Type | Description |
| --- | ---- | ---------- |
| `clientMutationId` | String | A unique identifier for the client performing the mutation. |
-| `errors` | String! => Array | Reasons why the mutation failed. |
+| `errors` | String! => Array | Errors encountered during execution of the mutation. |
| `note` | Note | The note after mutation |
## UpdateRequirementPayload
@@ -1474,7 +1693,7 @@ Autogenerated return type of UpdateRequirement
| Name | Type | Description |
| --- | ---- | ---------- |
| `clientMutationId` | String | A unique identifier for the client performing the mutation. |
-| `errors` | String! => Array | Reasons why the mutation failed. |
+| `errors` | String! => Array | Errors encountered during execution of the mutation. |
| `requirement` | Requirement | The requirement after mutation |
## UpdateSnippetPayload
@@ -1484,7 +1703,7 @@ Autogenerated return type of UpdateSnippet
| Name | Type | Description |
| --- | ---- | ---------- |
| `clientMutationId` | String | A unique identifier for the client performing the mutation. |
-| `errors` | String! => Array | Reasons why the mutation failed. |
+| `errors` | String! => Array | Errors encountered during execution of the mutation. |
| `snippet` | Snippet | The snippet after mutation |
## User
@@ -1492,7 +1711,9 @@ Autogenerated return type of UpdateSnippet
| Name | Type | Description |
| --- | ---- | ---------- |
| `avatarUrl` | String | URL of the user's avatar |
+| `id` | ID! | ID of the user |
| `name` | String! | Human-readable name of the user |
+| `state` | String! | State of the issue |
| `userPermissions` | UserPermissions! | Permissions for the current user on the resource |
| `username` | String! | Username of the user. Unique within this instance of GitLab |
| `webUrl` | String! | Web URL of the user |
@@ -1503,6 +1724,16 @@ Autogenerated return type of UpdateSnippet
| --- | ---- | ---------- |
| `createSnippet` | Boolean! | Indicates the user can perform `create_snippet` on this resource |
+## VulnerabilitiesCountByDayAndSeverity
+
+Represents the number of vulnerabilities for a particular severity on a particular day
+
+| Name | Type | Description |
+| --- | ---- | ---------- |
+| `count` | Int | Number of vulnerabilities |
+| `day` | ISO8601Date | Date for the count |
+| `severity` | VulnerabilitySeverity | Severity of the counted vulnerabilities |
+
## Vulnerability
Represents a vulnerability.
@@ -1511,7 +1742,7 @@ Represents a vulnerability.
| --- | ---- | ---------- |
| `description` | String | Description of the vulnerability |
| `id` | ID! | GraphQL ID of the vulnerability |
-| `location` | JSON | The JSON location metadata for the vulnerability. Its format depends on the type of the security scan that found the vulnerability |
+| `location` | VulnerabilityLocation | Location metadata for the vulnerability. Its fields depend on the type of security scan that found the vulnerability |
| `project` | Project | The project on which the vulnerability was found |
| `reportType` | VulnerabilityReportType | Type of the security report that found the vulnerability (SAST, DEPENDENCY_SCANNING, CONTAINER_SCANNING, DAST) |
| `severity` | VulnerabilitySeverity | Severity of the vulnerability (INFO, UNKNOWN, LOW, MEDIUM, HIGH, CRITICAL) |
@@ -1520,6 +1751,48 @@ Represents a vulnerability.
| `userPermissions` | VulnerabilityPermissions! | Permissions for the current user on the resource |
| `vulnerabilityPath` | String | URL to the vulnerability's details page |
+## VulnerabilityLocationContainerScanning
+
+Represents the location of a vulnerability found by a container security scan
+
+| Name | Type | Description |
+| --- | ---- | ---------- |
+| `dependency` | VulnerableDependency | Dependency containing the vulnerability |
+| `image` | String | Name of the vulnerable container image |
+| `operatingSystem` | String | Operating system that runs on the vulnerable container image |
+
+## VulnerabilityLocationDast
+
+Represents the location of a vulnerability found by a DAST scan
+
+| Name | Type | Description |
+| --- | ---- | ---------- |
+| `hostname` | String | Domain name of the vulnerable request |
+| `param` | String | Query parameter for the URL on which the vulnerability occurred |
+| `path` | String | URL path and query string of the vulnerable request |
+| `requestMethod` | String | HTTP method of the vulnerable request |
+
+## VulnerabilityLocationDependencyScanning
+
+Represents the location of a vulnerability found by a dependency security scan
+
+| Name | Type | Description |
+| --- | ---- | ---------- |
+| `dependency` | VulnerableDependency | Dependency containing the vulnerability |
+| `file` | String | Path to the vulnerable file |
+
+## VulnerabilityLocationSast
+
+Represents the location of a vulnerability found by a SAST scan
+
+| Name | Type | Description |
+| --- | ---- | ---------- |
+| `endLine` | String | Number of the last relevant line in the vulnerable file |
+| `file` | String | Path to the vulnerable file |
+| `startLine` | String | Number of the first relevant line in the vulnerable file |
+| `vulnerableClass` | String | Class containing the vulnerability |
+| `vulnerableMethod` | String | Method containing the vulnerability |
+
## VulnerabilityPermissions
Check permissions for the current user on a vulnerability
@@ -1547,3 +1820,20 @@ Represents vulnerability counts by severity
| `low` | Int | Number of vulnerabilities of LOW severity of the project |
| `medium` | Int | Number of vulnerabilities of MEDIUM severity of the project |
| `unknown` | Int | Number of vulnerabilities of UNKNOWN severity of the project |
+
+## VulnerableDependency
+
+Represents a vulnerable dependency. Used in vulnerability location data
+
+| Name | Type | Description |
+| --- | ---- | ---------- |
+| `package` | VulnerablePackage | The package associated with the vulnerable dependency |
+| `version` | String | The version of the vulnerable dependency |
+
+## VulnerablePackage
+
+Represents a vulnerable package. Used in vulnerability dependency data
+
+| Name | Type | Description |
+| --- | ---- | ---------- |
+| `name` | String | The name of the vulnerable package |
diff --git a/doc/api/group_badges.md b/doc/api/group_badges.md
index 6d5961a7eb8..5b7164cdd4d 100644
--- a/doc/api/group_badges.md
+++ b/doc/api/group_badges.md
@@ -7,7 +7,7 @@
Badges support placeholders that will be replaced in real time in both the link and image URL. The allowed placeholders are:
- **%{project_path}**: will be replaced by the project path.
-- **%{project_id}**: will be replaced by the project id.
+- **%{project_id}**: will be replaced by the project ID.
- **%{default_branch}**: will be replaced by the project default branch.
- **%{commit_sha}**: will be replaced by the last project's commit SHA.
diff --git a/doc/api/group_import_export.md b/doc/api/group_import_export.md
index 50e8bd9dcf2..355ecbfb98f 100644
--- a/doc/api/group_import_export.md
+++ b/doc/api/group_import_export.md
@@ -1,6 +1,6 @@
# Group Import/Export API
-> [Introduced](https://gitlab.com/gitlab-org/gitlab/-/merge_requests/20353) in GitLab 12.8 as an experimental feature. May change in future releases.
+> [Introduced](https://gitlab.com/gitlab-org/gitlab/-/merge_requests/20353) in GitLab 12.8.
Group Import/Export allows you to export group structure and import it to a new location.
When used with [Project Import/Export](project_import_export.md), you can preserve connections with
@@ -25,7 +25,7 @@ POST /groups/:id/export
| Attribute | Type | Required | Description |
| --------- | -------------- | -------- | ---------------------------------------- |
-| `id` | integer/string | yes | ID of the groupd owned by the authenticated user |
+| `id` | integer/string | yes | ID of the group owned by the authenticated user |
```shell
curl --request POST --header "PRIVATE-TOKEN: <your_access_token>" https://gitlab.example.com/api/v4/groups/1/export
@@ -41,7 +41,7 @@ curl --request POST --header "PRIVATE-TOKEN: <your_access_token>" https://gitlab
Download the finished export.
-```text
+```plaintext
GET /groups/:id/export/download
```
@@ -66,7 +66,7 @@ returns either:
## Import a file
-```text
+```plaintext
POST /groups/import
```
diff --git a/doc/api/group_milestones.md b/doc/api/group_milestones.md
index 7e2a6987208..10445acf881 100644
--- a/doc/api/group_milestones.md
+++ b/doc/api/group_milestones.md
@@ -1,6 +1,6 @@
# Group milestones API
-> [Introduced][ce-12819] in GitLab 9.5.
+> [Introduced](https://gitlab.com/gitlab-org/gitlab-foss/-/merge_requests/12819) in GitLab 9.5.
## List group milestones
@@ -21,10 +21,10 @@ Parameters:
| Attribute | Type | Required | Description |
| --------- | ------ | -------- | ----------- |
| `id` | integer/string | yes | The ID or [URL-encoded path of the group](README.md#namespaced-path-encoding) owned by the authenticated user |
-| `iids[]` | integer array | optional | Return only the milestones having the given `iid` |
-| `state` | string | optional | Return only `active` or `closed` milestones |
-| `title` | string | optional | Return only the milestones having the given `title` |
-| `search` | string | optional | Return only milestones with a title or description matching the provided string |
+| `iids[]` | integer array | no | Return only the milestones having the given `iid` |
+| `state` | string | no | Return only `active` or `closed` milestones |
+| `title` | string | no | Return only the milestones having the given `title` |
+| `search` | string | no | Return only milestones with a title or description matching the provided string |
```shell
curl --header "PRIVATE-TOKEN: <your_access_token>" https://gitlab.example.com/api/v4/groups/5/milestones
@@ -44,7 +44,8 @@ Example Response:
"start_date": "2013-11-10",
"state": "active",
"updated_at": "2013-10-02T09:24:18Z",
- "created_at": "2013-10-02T09:24:18Z"
+ "created_at": "2013-10-02T09:24:18Z",
+ "web_url": "https://gitlab.com/groups/gitlab-org/-/milestones/42"
}
]
```
@@ -59,8 +60,10 @@ GET /groups/:id/milestones/:milestone_id
Parameters:
-- `id` (required) - The ID or [URL-encoded path of the group](README.md#namespaced-path-encoding) owned by the authenticated user
-- `milestone_id` (required) - The ID of the group milestone
+| Attribute | Type | Required | Description |
+| --------- | ------ | -------- | ----------- |
+| `id` | integer/string | yes | The ID or [URL-encoded path of the group](README.md#namespaced-path-encoding) owned by the authenticated user |
+| `milestone_id` | integer | yes | The ID of the group milestone |
## Create new milestone
@@ -72,11 +75,13 @@ POST /groups/:id/milestones
Parameters:
-- `id` (required) - The ID or [URL-encoded path of the group](README.md#namespaced-path-encoding) owned by the authenticated user
-- `title` (required) - The title of a milestone
-- `description` (optional) - The description of the milestone
-- `due_date` (optional) - The due date of the milestone
-- `start_date` (optional) - The start date of the milestone
+| Attribute | Type | Required | Description |
+| --------- | ------ | -------- | ----------- |
+| `id` | integer/string | yes | The ID or [URL-encoded path of the group](README.md#namespaced-path-encoding) owned by the authenticated user |
+| `title` | string | yes | The title of a milestone |
+| `description` | string | no | The description of the milestone |
+| `due_date` | date | no | The due date of the milestone, in YYYY-MM-DD format (ISO 8601) |
+| `start_date` | date | no | The start date of the milestone, in YYYY-MM-DD format (ISO 8601) |
## Edit milestone
@@ -88,13 +93,15 @@ PUT /groups/:id/milestones/:milestone_id
Parameters:
-- `id` (required) - The ID or [URL-encoded path of the group](README.md#namespaced-path-encoding) owned by the authenticated user
-- `milestone_id` (required) - The ID of a group milestone
-- `title` (optional) - The title of a milestone
-- `description` (optional) - The description of a milestone
-- `due_date` (optional) - The due date of the milestone
-- `start_date` (optional) - The start date of the milestone
-- `state_event` (optional) - The state event of the milestone (close|activate)
+| Attribute | Type | Required | Description |
+| --------- | ------ | -------- | ----------- |
+| `id` | integer/string | yes | The ID or [URL-encoded path of the group](README.md#namespaced-path-encoding) owned by the authenticated user |
+| `milestone_id` | integer | yes | The ID of a group milestone |
+| `title` | string | no | The title of a milestone |
+| `description` | string | no | The description of a milestone |
+| `due_date` | date | no | The due date of the milestone, in YYYY-MM-DD format (ISO 8601) |
+| `start_date` | date | no | The start date of the milestone, in YYYY-MM-DD format (ISO 8601) |
+| `state_event` | string | no | The state event of the milestone _(`close` or `activate`)_ |
## Delete group milestone
@@ -106,8 +113,10 @@ DELETE /groups/:id/milestones/:milestone_id
Parameters:
-- `id` (required) - The ID or [URL-encoded path of the group](README.md#namespaced-path-encoding) owned by the authenticated user
-- `milestone_id` (required) - The ID of the group's milestone
+| Attribute | Type | Required | Description |
+| --------- | ------ | -------- | ----------- |
+| `id` | integer/string | yes | The ID or [URL-encoded path of the group](README.md#namespaced-path-encoding) owned by the authenticated user |
+| `milestone_id` | integer | yes | The ID of the group's milestone |
## Get all issues assigned to a single milestone
@@ -119,8 +128,10 @@ GET /groups/:id/milestones/:milestone_id/issues
Parameters:
-- `id` (required) - The ID or [URL-encoded path of the group](README.md#namespaced-path-encoding) owned by the authenticated user
-- `milestone_id` (required) - The ID of a group milestone
+| Attribute | Type | Required | Description |
+| --------- | ------ | -------- | ----------- |
+| `id` | integer/string | yes | The ID or [URL-encoded path of the group](README.md#namespaced-path-encoding) owned by the authenticated user |
+| `milestone_id` | integer | yes | The ID of a group milestone |
## Get all merge requests assigned to a single milestone
@@ -132,10 +143,10 @@ GET /groups/:id/milestones/:milestone_id/merge_requests
Parameters:
-- `id` (required) - The ID or [URL-encoded path of the group](README.md#namespaced-path-encoding) owned by the authenticated user
-- `milestone_id` (required) - The ID of a group milestone
-
-[ce-12819]: https://gitlab.com/gitlab-org/gitlab-foss/-/merge_requests/12819
+| Attribute | Type | Required | Description |
+| --------- | ------ | -------- | ----------- |
+| `id` | integer/string | yes | The ID or [URL-encoded path of the group](README.md#namespaced-path-encoding) owned by the authenticated user |
+| `milestone_id` | integer | yes | The ID of a group milestone |
## Get all burndown chart events for a single milestone **(STARTER)**
@@ -149,5 +160,7 @@ GET /groups/:id/milestones/:milestone_id/burndown_events
Parameters:
-- `id` (required) - The ID or [URL-encoded path of the group](README.md#namespaced-path-encoding) owned by the authenticated user
-- `milestone_id` (required) - The ID of a group milestone
+| Attribute | Type | Required | Description |
+| --------- | ------ | -------- | ----------- |
+| `id` | integer/string | yes | The ID or [URL-encoded path of the group](README.md#namespaced-path-encoding) owned by the authenticated user |
+| `milestone_id` | integer | yes | The ID of a group milestone |
diff --git a/doc/api/groups.md b/doc/api/groups.md
index 34c01766d06..bc7bff2964b 100644
--- a/doc/api/groups.md
+++ b/doc/api/groups.md
@@ -109,7 +109,7 @@ GET /groups?custom_attributes[key]=value&custom_attributes[other_key]=other_valu
## List a group's subgroups
-> [Introduced][ce-15142] in GitLab 10.3.
+> [Introduced](https://gitlab.com/gitlab-org/gitlab-foss/-/merge_requests/15142) in GitLab 10.3.
Get a list of visible direct subgroups in this group.
When accessed without authentication, only public groups are returned.
@@ -241,9 +241,142 @@ Example response:
```
NOTE: **Note:**
-
To distinguish between a project in the group and a project shared to the group, the `namespace` attribute can be used. When a project has been shared to the group, its `namespace` will be different from the group the request is being made for.
+## List a group's shared projects
+
+Get a list of projects shared to this group. When accessed without authentication, only public shared projects are returned.
+
+By default, this request returns 20 results at a time because the API results [are paginated](README.md#pagination).
+
+```plaintext
+GET /groups/:id/projects/shared
+```
+
+Parameters:
+
+| Attribute | Type | Required | Description |
+| ----------------------------- | -------------- | -------- | ----------- |
+| `id` | integer/string | yes | The ID or [URL-encoded path of the group](README.md#namespaced-path-encoding) owned by the authenticated user |
+| `archived` | boolean | no | Limit by archived status |
+| `visibility` | string | no | Limit by visibility `public`, `internal`, or `private` |
+| `order_by` | string | no | Return projects ordered by `id`, `name`, `path`, `created_at`, `updated_at`, or `last_activity_at` fields. Default is `created_at` |
+| `sort` | string | no | Return projects sorted in `asc` or `desc` order. Default is `desc` |
+| `search` | string | no | Return list of authorized projects matching the search criteria |
+| `simple` | boolean | no | Return only the ID, URL, name, and path of each project |
+| `starred` | boolean | no | Limit by projects starred by the current user |
+| `with_issues_enabled` | boolean | no | Limit by projects with issues feature enabled. Default is `false` |
+| `with_merge_requests_enabled` | boolean | no | Limit by projects with merge requests feature enabled. Default is `false` |
+| `min_access_level` | integer | no | Limit to projects where current user has at least this [access level](members.md) |
+| `with_custom_attributes` | boolean | no | Include [custom attributes](custom_attributes.md) in response (admins only) |
+
+Example response:
+
+```json
+[
+ {
+ "id":8,
+ "description":"Shared project for Html5 Boilerplate",
+ "name":"Html5 Boilerplate",
+ "name_with_namespace":"H5bp / Html5 Boilerplate",
+ "path":"html5-boilerplate",
+ "path_with_namespace":"h5bp/html5-boilerplate",
+ "created_at":"2020-04-27T06:13:22.642Z",
+ "default_branch":"master",
+ "tag_list":[
+
+ ],
+ "ssh_url_to_repo":"ssh://git@gitlab.com/h5bp/html5-boilerplate.git",
+ "http_url_to_repo":"http://gitlab.com/h5bp/html5-boilerplate.git",
+ "web_url":"http://gitlab.com/h5bp/html5-boilerplate",
+ "readme_url":"http://gitlab.com/h5bp/html5-boilerplate/-/blob/master/README.md",
+ "avatar_url":null,
+ "star_count":0,
+ "forks_count":4,
+ "last_activity_at":"2020-04-27T06:13:22.642Z",
+ "namespace":{
+ "id":28,
+ "name":"H5bp",
+ "path":"h5bp",
+ "kind":"group",
+ "full_path":"h5bp",
+ "parent_id":null,
+ "avatar_url":null,
+ "web_url":"http://gitlab.com/groups/h5bp"
+ },
+ "_links":{
+ "self":"http://gitlab.com/api/v4/projects/8",
+ "issues":"http://gitlab.com/api/v4/projects/8/issues",
+ "merge_requests":"http://gitlab.com/api/v4/projects/8/merge_requests",
+ "repo_branches":"http://gitlab.com/api/v4/projects/8/repository/branches",
+ "labels":"http://gitlab.com/api/v4/projects/8/labels",
+ "events":"http://gitlab.com/api/v4/projects/8/events",
+ "members":"http://gitlab.com/api/v4/projects/8/members"
+ },
+ "empty_repo":false,
+ "archived":false,
+ "visibility":"public",
+ "resolve_outdated_diff_discussions":false,
+ "container_registry_enabled":true,
+ "container_expiration_policy":{
+ "cadence":"7d",
+ "enabled":true,
+ "keep_n":null,
+ "older_than":null,
+ "name_regex":null,
+ "name_regex_keep":null,
+ "next_run_at":"2020-05-04T06:13:22.654Z"
+ },
+ "issues_enabled":true,
+ "merge_requests_enabled":true,
+ "wiki_enabled":true,
+ "jobs_enabled":true,
+ "snippets_enabled":true,
+ "can_create_merge_request_in":true,
+ "issues_access_level":"enabled",
+ "repository_access_level":"enabled",
+ "merge_requests_access_level":"enabled",
+ "forking_access_level":"enabled",
+ "wiki_access_level":"enabled",
+ "builds_access_level":"enabled",
+ "snippets_access_level":"enabled",
+ "pages_access_level":"enabled",
+ "emails_disabled":null,
+ "shared_runners_enabled":true,
+ "lfs_enabled":true,
+ "creator_id":1,
+ "import_status":"failed",
+ "open_issues_count":10,
+ "ci_default_git_depth":50,
+ "public_jobs":true,
+ "build_timeout":3600,
+ "auto_cancel_pending_pipelines":"enabled",
+ "build_coverage_regex":null,
+ "ci_config_path":null,
+ "shared_with_groups":[
+ {
+ "group_id":24,
+ "group_name":"Commit451",
+ "group_full_path":"Commit451",
+ "group_access_level":30,
+ "expires_at":null
+ }
+ ],
+ "only_allow_merge_if_pipeline_succeeds":false,
+ "request_access_enabled":true,
+ "only_allow_merge_if_all_discussions_are_resolved":false,
+ "remove_source_branch_after_merge":true,
+ "printing_merge_request_link_enabled":true,
+ "merge_method":"merge",
+ "suggestion_commit_message":null,
+ "auto_devops_enabled":true,
+ "auto_devops_deploy_strategy":"continuous",
+ "autoclose_referenced_issues":true,
+ "repository_storage":"default"
+ }
+]
+```
+
## Details of a group
Get all details of a group. This endpoint can be accessed without authentication
@@ -259,7 +392,11 @@ Parameters:
| ------------------------ | -------------- | -------- | ----------- |
| `id` | integer/string | yes | The ID or [URL-encoded path of the group](README.md#namespaced-path-encoding) owned by the authenticated user. |
| `with_custom_attributes` | boolean | no | Include [custom attributes](custom_attributes.md) in response (admins only). |
-| `with_projects` | boolean | no | Include details from projects that belong to the specified group (defaults to `true`). (Deprecated, [will be removed in 13.0](https://gitlab.com/gitlab-org/gitlab/-/issues/213797). To get the details of all projects within a group, use the [list a group's projects endpoint](#list-a-groups-projects).) |
+| `with_projects` | boolean | no | Include details from projects that belong to the specified group (defaults to `true`). (Deprecated, [will be removed in API v5](https://gitlab.com/gitlab-org/gitlab/-/issues/213797). To get the details of all projects within a group, use the [list a group's projects endpoint](#list-a-groups-projects).) |
+
+NOTE: **Note:**
+The `projects` and `shared_projects` attributes in the response are deprecated and will be [removed in API v5](https://gitlab.com/gitlab-org/gitlab/-/issues/213797).
+To get the details of all projects within a group, use either the [list a group's projects](#list-a-groups-projects) or the [list a group's shared projects](#list-a-groups-shared-projects) endpoint.
```shell
curl --header "PRIVATE-TOKEN: <your_access_token>" https://gitlab.example.com/api/v4/groups/4
@@ -290,7 +427,7 @@ Example response:
"file_template_project_id": 1,
"parent_id": null,
"created_at": "2020-01-15T12:36:29.590Z",
- "projects": [
+ "projects": [ // Deprecated and will be removed in API v5
{
"id": 7,
"description": "Voluptas veniam qui et beatae voluptas doloremque explicabo facilis.",
@@ -368,7 +505,7 @@ Example response:
"request_access_enabled": false
}
],
- "shared_projects": [
+ "shared_projects": [ // Deprecated and will be removed in API v5
{
"id": 8,
"description": "Velit eveniet provident fugiat saepe eligendi autem.",
@@ -571,6 +708,10 @@ PUT /groups/:id
| `shared_runners_minutes_limit` | integer | no | **(STARTER ONLY)** Pipeline minutes quota for this group. |
| `extra_shared_runners_minutes_limit` | integer | no | **(STARTER ONLY)** Extra pipeline minutes quota for this group. |
+NOTE: **Note:**
+The `projects` and `shared_projects` attributes in the response are deprecated and will be [removed in API v5](https://gitlab.com/gitlab-org/gitlab/-/issues/213797).
+To get the details of all projects within a group, use either the [list a group's projects](#list-a-groups-projects) or the [list a group's shared projects](#list-a-groups-shared-projects) endpoint.
+
```shell
curl --request PUT --header "PRIVATE-TOKEN: <your_access_token>" "https://gitlab.example.com/api/v4/groups/5?name=Experimental"
```
@@ -582,10 +723,6 @@ This endpoint returns:
and later. To get the details of all projects within a group, use the
[list a group's projects endpoint](#list-a-groups-projects) instead.
-NOTE: **Note:**
-
-The `projects` and `shared_projects` attributes [will be deprecated in GitLab 13.0](https://gitlab.com/gitlab-org/gitlab/-/issues/213797). To get the details of all projects within a group, use the [list a group's projects endpoint](#list-a-groups-projects) instead.
-
Example response:
```json
@@ -603,7 +740,7 @@ Example response:
"file_template_project_id": 1,
"parent_id": null,
"created_at": "2020-01-15T12:36:29.590Z",
- "projects": [
+ "projects": [ // Deprecated and will be removed in API v5
{
"id": 9,
"description": "foo",
@@ -944,8 +1081,6 @@ And to switch pages add:
/groups?per_page=100&page=2
```
-[ce-15142]: https://gitlab.com/gitlab-org/gitlab-foss/-/merge_requests/15142
-
## Group badges
Read more in the [Group Badges](group_badges.md) documentation.
diff --git a/doc/api/import.md b/doc/api/import.md
index 8db8dc7eea4..9640ba19cf9 100644
--- a/doc/api/import.md
+++ b/doc/api/import.md
@@ -12,8 +12,8 @@ POST /import/github
|------------|---------|----------|---------------------|
| `personal_access_token` | string | yes | GitHub personal access token |
| `repo_id` | integer | yes | GitHub repository ID |
-| `new_name` | string | no | New repo name |
-| `target_namespace` | string | yes | Namespace to import repo into |
+| `new_name` | string | no | New repository name |
+| `target_namespace` | string | yes | Namespace to import repository into |
```shell
curl --request POST --header "PRIVATE-TOKEN: 9koXpg98eAheJpvBs5tK" --data "personal_access_token=abc123&repo_id=12345&target_namespace=root" https://gitlab.example.com/api/v4/import/github
diff --git a/doc/api/instance_level_ci_variables.md b/doc/api/instance_level_ci_variables.md
new file mode 100644
index 00000000000..d0871fdf4a7
--- /dev/null
+++ b/doc/api/instance_level_ci_variables.md
@@ -0,0 +1,162 @@
+# Instance-level CI/CD variables API
+
+> - [Introduced](https://gitlab.com/gitlab-org/gitlab/-/issues/14108) in GitLab 13.0
+> - It's deployed behind a feature flag, enabled by default.
+> - It's enabled on GitLab.com.
+> - It's recommended for production use.
+> - For GitLab self-managed instances, GitLab administrators can opt to [disable it](#enable-or-disable-instance-level-cicd-variables-core-only). **(CORE ONLY)**
+
+## List all instance variables
+
+Get the list of all instance-level variables.
+
+```plaintext
+GET /admin/ci/variables
+```
+
+```shell
+curl --header "PRIVATE-TOKEN: <your_access_token>" "https://gitlab.example.com/api/v4/admin/ci/variables"
+```
+
+```json
+[
+ {
+ "key": "TEST_VARIABLE_1",
+ "variable_type": "env_var",
+ "value": "TEST_1",
+ "protected": false,
+ "masked": false
+ },
+ {
+ "key": "TEST_VARIABLE_2",
+ "variable_type": "env_var",
+ "value": "TEST_2",
+ "protected": false,
+ "masked": false
+ }
+]
+```
+
+## Show instance variable details
+
+Get the details of a specific instance-level variable.
+
+```plaintext
+GET /admin/ci/variables/:key
+```
+
+| Attribute | Type | required | Description |
+|-----------|---------|----------|-----------------------|
+| `key` | string | yes | The `key` of a variable |
+
+```shell
+curl --header "PRIVATE-TOKEN: <your_access_token>" "https://gitlab.example.com/api/v4/admin/ci/variables/TEST_VARIABLE_1"
+```
+
+```json
+{
+ "key": "TEST_VARIABLE_1",
+ "variable_type": "env_var",
+ "value": "TEST_1",
+ "protected": false,
+ "masked": false
+}
+```
+
+## Create instance variable
+
+Create a new instance-level variable.
+
+NOTE: **Note:**
+The maximum number of instance-level variables is [planned to be 25](https://gitlab.com/gitlab-org/gitlab/-/issues/216097).
+
+```plaintext
+POST /admin/ci/variables
+```
+
+| Attribute | Type | required | Description |
+|-----------------|---------|----------|-----------------------|
+| `key` | string | yes | The `key` of a variable. Max 255 characters, only `A-Z`, `a-z`, `0-9`, and `_` are allowed. |
+| `value` | string | yes | The `value` of a variable. |
+| `variable_type` | string | no | The type of a variable. Available types are: `env_var` (default) and `file`. |
+| `protected` | boolean | no | Whether the variable is protected. |
+| `masked` | boolean | no | Whether the variable is masked. |
+
+```shell
+curl --request POST --header "PRIVATE-TOKEN: <your_access_token>" "https://gitlab.example.com/api/v4/admin/ci/variables" --form "key=NEW_VARIABLE" --form "value=new value"
+```
+
+```json
+{
+ "key": "NEW_VARIABLE",
+ "value": "new value",
+ "variable_type": "env_var",
+ "protected": false,
+ "masked": false
+}
+```
+
+## Update instance variable
+
+Update an instance-level variable.
+
+```plaintext
+PUT /admin/ci/variables/:key
+```
+
+| Attribute | Type | required | Description |
+|-----------------|---------|----------|-------------------------|
+| `key` | string | yes | The `key` of a variable. |
+| `value` | string | yes | The `value` of a variable. |
+| `variable_type` | string | no | The type of a variable. Available types are: `env_var` (default) and `file`. |
+| `protected` | boolean | no | Whether the variable is protected. |
+| `masked` | boolean | no | Whether the variable is masked. |
+
+```shell
+curl --request PUT --header "PRIVATE-TOKEN: <your_access_token>" "https://gitlab.example.com/api/v4/admin/ci/variables/NEW_VARIABLE" --form "value=updated value"
+```
+
+```json
+{
+ "key": "NEW_VARIABLE",
+ "value": "updated value",
+ "variable_type": "env_var",
+ "protected": true,
+ "masked": true
+}
+```
+
+## Remove instance variable
+
+Remove an instance-level variable.
+
+```plaintext
+DELETE /admin/ci/variables/:key
+```
+
+| Attribute | Type | required | Description |
+|-----------|---------|----------|-------------------------|
+| `key` | string | yes | The `key` of a variable |
+
+```shell
+curl --request DELETE --header "PRIVATE-TOKEN: <your_access_token>" "https://gitlab.example.com/api/v4/admin/ci/variables/VARIABLE_1"
+```
+
+### Enable or disable instance-level CI/CD variables **(CORE ONLY)**
+
+Instance-level CI/CD variables is under development but ready for production use.
+It is deployed behind a feature flag that is **enabled by default**.
+[GitLab administrators with access to the GitLab Rails console](../administration/feature_flags.md)
+can opt to disable it for your instance.
+
+To disable it:
+
+```ruby
+Feature.disable(:ci_instance_level_variables)
+```
+
+To enable it:
+
+```ruby
+Feature.enable(:ci_instance_level_variables)
+```
diff --git a/doc/api/issues.md b/doc/api/issues.md
index 14f81d7d327..8e5882c4d4e 100644
--- a/doc/api/issues.md
+++ b/doc/api/issues.md
@@ -53,7 +53,7 @@ GET /issues?confidential=true
| `author_id` | integer | no | Return issues created by the given user `id`. Mutually exclusive with `author_username`. Combine with `scope=all` or `scope=assigned_to_me`. _([Introduced](https://gitlab.com/gitlab-org/gitlab-foss/-/merge_requests/13004) in GitLab 9.5)_ |
| `author_username` | string | no | Return issues created by the given `username`. Similar to `author_id` and mutually exclusive with `author_id`. |
| `assignee_id` | integer | no | Return issues assigned to the given user `id`. Mutually exclusive with `assignee_username`. `None` returns unassigned issues. `Any` returns issues with an assignee. _([Introduced](https://gitlab.com/gitlab-org/gitlab-foss/-/merge_requests/13004) in GitLab 9.5)_ |
-| `assignee_username` | string array | no | Return issues assigned to the given `username`. Similar to `assignee_id` and mutually exclusive with `assignee_id`. In CE version `assignee_username` array should only contain a single value or an invalid parameter error will be returned otherwise. |
+| `assignee_username` | string array | no | Return issues assigned to the given `username`. Similar to `assignee_id` and mutually exclusive with `assignee_id`. In GitLab CE `assignee_username` array should only contain a single value or an invalid parameter error will be returned otherwise. |
| `my_reaction_emoji` | string | no | Return issues reacted by the authenticated user by the given `emoji`. `None` returns issues not given a reaction. `Any` returns issues given at least one reaction. _([Introduced](https://gitlab.com/gitlab-org/gitlab-foss/-/merge_requests/14016) in GitLab 10.0)_ |
| `weight` **(STARTER)** | integer | no | Return issues with the specified `weight`. `None` returns issues with no weight assigned. `Any` returns issues with a weight assigned. |
| `iids[]` | integer array | no | Return only the issues having the given `iid` |
@@ -67,6 +67,7 @@ GET /issues?confidential=true
| `updated_before` | datetime | no | Return issues updated on or before the given time |
| `confidential` | boolean | no | Filter confidential or public issues. |
| `not` | Hash | no | Return issues that do not match the parameters supplied. Accepts: `labels`, `milestone`, `author_id`, `author_username`, `assignee_id`, `assignee_username`, `my_reaction_emoji`, `search`, `in` |
+| `non_archived` | boolean | no | Return issues only from non-archived projects. If `false`, response will return issues from both archived and non-archived projects. Default is `true`. _(Introduced in [GitLab 13.0](https://gitlab.com/gitlab-org/gitlab/-/issues/197170))_ |
```shell
curl --header "PRIVATE-TOKEN: <your_access_token>" https://gitlab.example.com/api/v4/issues
@@ -210,7 +211,7 @@ GET /groups/:id/issues?confidential=true
| `author_id` | integer | no | Return issues created by the given user `id`. Mutually exclusive with `author_username`. Combine with `scope=all` or `scope=assigned_to_me`. _([Introduced](https://gitlab.com/gitlab-org/gitlab-foss/-/merge_requests/13004) in GitLab 9.5)_ |
| `author_username` | string | no | Return issues created by the given `username`. Similar to `author_id` and mutually exclusive with `author_id`. |
| `assignee_id` | integer | no | Return issues assigned to the given user `id`. Mutually exclusive with `assignee_username`. `None` returns unassigned issues. `Any` returns issues with an assignee. _([Introduced](https://gitlab.com/gitlab-org/gitlab-foss/-/merge_requests/13004) in GitLab 9.5)_ |
-| `assignee_username` | string array | no | Return issues assigned to the given `username`. Similar to `assignee_id` and mutually exclusive with `assignee_id`. In CE version `assignee_username` array should only contain a single value or an invalid parameter error will be returned otherwise. |
+| `assignee_username` | string array | no | Return issues assigned to the given `username`. Similar to `assignee_id` and mutually exclusive with `assignee_id`. In GitLab CE `assignee_username` array should only contain a single value or an invalid parameter error will be returned otherwise. |
| `my_reaction_emoji` | string | no | Return issues reacted by the authenticated user by the given `emoji`. `None` returns issues not given a reaction. `Any` returns issues given at least one reaction. _([Introduced](https://gitlab.com/gitlab-org/gitlab-foss/-/merge_requests/14016) in GitLab 10.0)_ |
| `weight` **(STARTER)** | integer | no | Return issues with the specified `weight`. `None` returns issues with no weight assigned. `Any` returns issues with a weight assigned. |
| `order_by` | string | no | Return issues ordered by `created_at`, `updated_at`, `priority`, `due_date`, `relative_position`, `label_priority`, `milestone_due`, `popularity`, `weight` fields. Default is `created_at` |
@@ -365,7 +366,7 @@ GET /projects/:id/issues?confidential=true
| `author_id` | integer | no | Return issues created by the given user `id`. Mutually exclusive with `author_username`. Combine with `scope=all` or `scope=assigned_to_me`. _([Introduced](https://gitlab.com/gitlab-org/gitlab-foss/-/merge_requests/13004) in GitLab 9.5)_ |
| `author_username` | string | no | Return issues created by the given `username`. Similar to `author_id` and mutually exclusive with `author_id`. |
| `assignee_id` | integer | no | Return issues assigned to the given user `id`. Mutually exclusive with `assignee_username`. `None` returns unassigned issues. `Any` returns issues with an assignee. _([Introduced](https://gitlab.com/gitlab-org/gitlab-foss/-/merge_requests/13004) in GitLab 9.5)_ |
-| `assignee_username` | string array | no | Return issues assigned to the given `username`. Similar to `assignee_id` and mutually exclusive with `assignee_id`. In CE version `assignee_username` array should only contain a single value or an invalid parameter error will be returned otherwise. |
+| `assignee_username` | string array | no | Return issues assigned to the given `username`. Similar to `assignee_id` and mutually exclusive with `assignee_id`. In GitLab CE `assignee_username` array should only contain a single value or an invalid parameter error will be returned otherwise. |
| `my_reaction_emoji` | string | no | Return issues reacted by the authenticated user by the given `emoji`. `None` returns issues not given a reaction. `Any` returns issues given at least one reaction. _([Introduced](https://gitlab.com/gitlab-org/gitlab-foss/-/merge_requests/14016) in GitLab 10.0)_ |
| `weight` **(STARTER)** | integer | no | Return issues with the specified `weight`. `None` returns issues with no weight assigned. `Any` returns issues with a weight assigned. |
| `order_by` | string | no | Return issues ordered by `created_at`, `updated_at`, `priority`, `due_date`, `relative_position`, `label_priority`, `milestone_due`, `popularity`, `weight` fields. Default is `created_at` |
@@ -630,7 +631,7 @@ the `epic` property:
**Note**: The `closed_by` attribute was [introduced in GitLab 10.6](https://gitlab.com/gitlab-org/gitlab-foss/-/merge_requests/17042). This value will only be present for issues which were closed after GitLab 10.6 and when the user account that closed the issue still exists.
-**Note**: The `epic_iid` attribute is deprecated and [will be removed in 13.0](https://gitlab.com/gitlab-org/gitlab/issues/35157).
+**Note**: The `epic_iid` attribute is deprecated and [will be removed in version 5](https://gitlab.com/gitlab-org/gitlab/issues/35157).
Please use `iid` of the `epic` attribute instead.
## New issue
@@ -657,7 +658,7 @@ POST /projects/:id/issues
| `discussion_to_resolve` | string | no | The ID of a discussion to resolve. This will fill in the issue with a default description and mark the discussion as resolved. Use in combination with `merge_request_to_resolve_discussions_of`. |
| `weight` **(STARTER)** | integer | no | The weight of the issue. Valid values are greater than or equal to 0. |
| `epic_id` **(ULTIMATE)** | integer | no | ID of the epic to add the issue to. Valid values are greater than or equal to 0. |
-| `epic_iid` **(ULTIMATE)** | integer | no | IID of the epic to add the issue to. Valid values are greater than or equal to 0. (deprecated, [will be removed in 13.0](https://gitlab.com/gitlab-org/gitlab/issues/35157)) |
+| `epic_iid` **(ULTIMATE)** | integer | no | IID of the epic to add the issue to. Valid values are greater than or equal to 0. (deprecated, [will be removed in version 5](https://gitlab.com/gitlab-org/gitlab/issues/35157)) |
```shell
curl --request POST --header "PRIVATE-TOKEN: <your_access_token>" https://gitlab.example.com/api/v4/projects/4/issues?title=Issues%20with%20auth&labels=bug
@@ -773,7 +774,7 @@ PUT /projects/:id/issues/:issue_iid
| `weight` **(STARTER)** | integer | no | The weight of the issue. Valid values are greater than or equal to 0. 0 |
| `discussion_locked` | boolean | no | Flag indicating if the issue's discussion is locked. If the discussion is locked only project members can add or edit comments. |
| `epic_id` **(ULTIMATE)** | integer | no | ID of the epic to add the issue to. Valid values are greater than or equal to 0. |
-| `epic_iid` **(ULTIMATE)** | integer | no | IID of the epic to add the issue to. Valid values are greater than or equal to 0. (deprecated, [will be removed in 13.0](https://gitlab.com/gitlab-org/gitlab/issues/35157)) |
+| `epic_iid` **(ULTIMATE)** | integer | no | IID of the epic to add the issue to. Valid values are greater than or equal to 0. (deprecated, [will be removed in version 5](https://gitlab.com/gitlab-org/gitlab/issues/35157)) |
```shell
curl --request PUT --header "PRIVATE-TOKEN: <your_access_token>" https://gitlab.example.com/api/v4/projects/4/issues/85?state_event=close
diff --git a/doc/api/issues_statistics.md b/doc/api/issues_statistics.md
index 699eda174d2..8db99e93f79 100644
--- a/doc/api/issues_statistics.md
+++ b/doc/api/issues_statistics.md
@@ -34,7 +34,7 @@ GET /issues_statistics?confidential=true
| `author_id` | integer | no | Return issues created by the given user `id`. Mutually exclusive with `author_username`. Combine with `scope=all` or `scope=assigned_to_me`. |
| `author_username` | string | no | Return issues created by the given `username`. Similar to `author_id` and mutually exclusive with `author_id`. |
| `assignee_id` | integer | no | Return issues assigned to the given user `id`. Mutually exclusive with `assignee_username`. `None` returns unassigned issues. `Any` returns issues with an assignee. |
-| `assignee_username` | string array | no | Return issues assigned to the given `username`. Similar to `assignee_id` and mutually exclusive with `assignee_id`. In CE version `assignee_username` array should only contain a single value or an invalid parameter error will be returned otherwise. |
+| `assignee_username` | string array | no | Return issues assigned to the given `username`. Similar to `assignee_id` and mutually exclusive with `assignee_id`. In GitLab CE `assignee_username` array should only contain a single value or an invalid parameter error will be returned otherwise. |
| `my_reaction_emoji` | string | no | Return issues reacted by the authenticated user by the given `emoji`. `None` returns issues not given a reaction. `Any` returns issues given at least one reaction. |
| `iids[]` | integer array | no | Return only the issues having the given `iid` |
| `search` | string | no | Search issues against their `title` and `description` |
@@ -92,7 +92,7 @@ GET /groups/:id/issues_statistics?confidential=true
| `author_id` | integer | no | Return issues created by the given user `id`. Mutually exclusive with `author_username`. Combine with `scope=all` or `scope=assigned_to_me`. |
| `author_username` | string | no | Return issues created by the given `username`. Similar to `author_id` and mutually exclusive with `author_id`. |
| `assignee_id` | integer | no | Return issues assigned to the given user `id`. Mutually exclusive with `assignee_username`. `None` returns unassigned issues. `Any` returns issues with an assignee. |
-| `assignee_username` | string array | no | Return issues assigned to the given `username`. Similar to `assignee_id` and mutually exclusive with `assignee_id`. In CE version `assignee_username` array should only contain a single value or an invalid parameter error will be returned otherwise. |
+| `assignee_username` | string array | no | Return issues assigned to the given `username`. Similar to `assignee_id` and mutually exclusive with `assignee_id`. In GitLab CE `assignee_username` array should only contain a single value or an invalid parameter error will be returned otherwise. |
| `my_reaction_emoji` | string | no | Return issues reacted by the authenticated user by the given `emoji`. `None` returns issues not given a reaction. `Any` returns issues given at least one reaction. |
| `search` | string | no | Search group issues against their `title` and `description` |
| `created_after` | datetime | no | Return issues created on or after the given time |
@@ -148,7 +148,7 @@ GET /projects/:id/issues_statistics?confidential=true
| `author_id` | integer | no | Return issues created by the given user `id`. Mutually exclusive with `author_username`. Combine with `scope=all` or `scope=assigned_to_me`. |
| `author_username` | string | no | Return issues created by the given `username`. Similar to `author_id` and mutually exclusive with `author_id`. |
| `assignee_id` | integer | no | Return issues assigned to the given user `id`. Mutually exclusive with `assignee_username`. `None` returns unassigned issues. `Any` returns issues with an assignee. |
-| `assignee_username` | string array | no | Return issues assigned to the given `username`. Similar to `assignee_id` and mutually exclusive with `assignee_id`. In CE version `assignee_username` array should only contain a single value or an invalid parameter error will be returned otherwise. |
+| `assignee_username` | string array | no | Return issues assigned to the given `username`. Similar to `assignee_id` and mutually exclusive with `assignee_id`. In GitLab CE `assignee_username` array should only contain a single value or an invalid parameter error will be returned otherwise. |
| `my_reaction_emoji` | string | no | Return issues reacted by the authenticated user by the given `emoji`. `None` returns issues not given a reaction. `Any` returns issues given at least one reaction. |
| `search` | string | no | Search project issues against their `title` and `description` |
| `created_after` | datetime | no | Return issues created on or after the given time |
diff --git a/doc/api/jobs.md b/doc/api/jobs.md
index 4827eafd790..c06dc56407c 100644
--- a/doc/api/jobs.md
+++ b/doc/api/jobs.md
@@ -345,7 +345,7 @@ Example of response
> **Notes**:
>
-> - [Introduced][ce-2893] in GitLab 8.5.
+> - [Introduced](https://gitlab.com/gitlab-org/gitlab-foss/-/merge_requests/2893) in GitLab 8.5.
> - The use of `CI_JOB_TOKEN` in the artifacts download API was [introduced](https://gitlab.com/gitlab-org/gitlab/-/merge_requests/2346)
> in [GitLab Premium](https://about.gitlab.com/pricing/) 9.5.
@@ -399,13 +399,11 @@ Possible response status codes:
| 200 | Serves the artifacts file. |
| 404 | Build not found or no artifacts.|
-[ce-2893]: https://gitlab.com/gitlab-org/gitlab-foss/-/merge_requests/2893
-
## Download the artifacts archive
> **Notes**:
>
-> - [Introduced][ce-5347] in GitLab 8.10.
+> - [Introduced](https://gitlab.com/gitlab-org/gitlab-foss/-/merge_requests/5347) in GitLab 8.10.
> - The use of `CI_JOB_TOKEN` in the artifacts download API was [introduced](https://gitlab.com/gitlab-org/gitlab/-/merge_requests/2346)
> in [GitLab Premium](https://about.gitlab.com/pricing/) 9.5.
@@ -466,8 +464,6 @@ Possible response status codes:
| 200 | Serves the artifacts file. |
| 404 | Build not found or no artifacts.|
-[ce-5347]: https://gitlab.com/gitlab-org/gitlab-foss/-/merge_requests/5347
-
## Download a single artifact file by job ID
> Introduced in GitLab 10.0
@@ -547,8 +543,8 @@ GET /projects/:id/jobs/:job_id/trace
| Attribute | Type | Required | Description |
|-----------|----------------|----------|------------------------------------------------------------------------------------------------------------------|
-| id | integer/string | yes | ID or [URL-encoded path of the project](README.md#namespaced-path-encoding) owned by the authenticated user. |
-| job_id | integer | yes | ID of a job. |
+| `id` | integer/string | yes | ID or [URL-encoded path of the project](README.md#namespaced-path-encoding) owned by the authenticated user. |
+| `job_id` | integer | yes | ID of a job. |
```shell
curl --location --header "PRIVATE-TOKEN: <your_access_token>" "https://gitlab.example.com/api/v4/projects/1/jobs/8/trace"
diff --git a/doc/api/keys.md b/doc/api/keys.md
index d4eb1161a97..81ebd70be52 100644
--- a/doc/api/keys.md
+++ b/doc/api/keys.md
@@ -4,7 +4,7 @@
Get SSH key with user by ID of an SSH key. Note only administrators can lookup SSH key with user by ID of an SSH key.
-```text
+```plaintext
GET /keys/:id
```
@@ -63,7 +63,7 @@ curl --header "PRIVATE-TOKEN: <your_access_token>" 'https://gitlab.example.com/a
You can search for a user that owns a specific SSH key. Note only administrators can lookup SSH key with the fingerprint of an SSH key.
-```text
+```plaintext
GET /keys
```
diff --git a/doc/api/labels.md b/doc/api/labels.md
index e3f367daaca..3ced7da8ed5 100644
--- a/doc/api/labels.md
+++ b/doc/api/labels.md
@@ -7,6 +7,8 @@ The `description_html` - was added to response JSON in [GitLab 12.7](https://git
Get all labels for a given project.
+By default, this request returns 20 results at a time because the API results [are paginated](README.md#pagination).
+
```plaintext
GET /projects/:id/labels
```
@@ -109,7 +111,7 @@ GET /projects/:id/labels/:label_id
| Attribute | Type | Required | Description |
| --------- | ------- | -------- | --------------------- |
| `id` | integer/string | yes | The ID or [URL-encoded path of the project](README.md#namespaced-path-encoding) owned by the authenticated user |
-| `label_id` | integer or string | yes | The ID or title of a group's label. |
+| `label_id` | integer or string | yes | The ID or title of a project's label. |
| `include_ancestor_groups` | boolean | no | Include ancestor groups. Defaults to `true`. |
```shell
diff --git a/doc/api/lint.md b/doc/api/lint.md
index 4ecce92df26..f0e4ad5655a 100644
--- a/doc/api/lint.md
+++ b/doc/api/lint.md
@@ -1,6 +1,6 @@
# Validate the `.gitlab-ci.yml` (API)
-> [Introduced][ce-5953] in GitLab 8.12.
+> [Introduced](https://gitlab.com/gitlab-org/gitlab-foss/-/merge_requests/5953) in GitLab 8.12.
Checks if your `.gitlab-ci.yml` file is valid.
@@ -47,5 +47,3 @@ Example responses:
"error": "content is missing"
}
```
-
-[ce-5953]: https://gitlab.com/gitlab-org/gitlab-foss/-/merge_requests/5953
diff --git a/doc/api/managed_licenses.md b/doc/api/managed_licenses.md
index 66125d23a82..13eb3a3fea7 100644
--- a/doc/api/managed_licenses.md
+++ b/doc/api/managed_licenses.md
@@ -90,7 +90,7 @@ Example response:
## Delete a managed license
-Deletes a managed license with a given id.
+Deletes a managed license with a given ID.
```plaintext
DELETE /projects/:id/managed_licenses/:managed_license_id
diff --git a/doc/api/members.md b/doc/api/members.md
index e9131e2d4c3..afeda7780d7 100644
--- a/doc/api/members.md
+++ b/doc/api/members.md
@@ -282,6 +282,78 @@ Example response:
}
```
+### Set override flag for a member of a group
+
+> [Introduced](https://gitlab.com/gitlab-org/gitlab/issues/4875) in GitLab 12.10.
+
+By default, the access level of LDAP group members is set to the value specified
+by LDAP through Group Sync. You can allow access level overrides by calling this endpoint.
+
+```plaintext
+POST /groups/:id/members/:user_id/override
+```
+
+| Attribute | Type | Required | Description |
+| --------- | ---- | -------- | ----------- |
+| `id` | integer/string | yes | The ID or [URL-encoded path of the group](README.md#namespaced-path-encoding) owned by the authenticated user |
+| `user_id` | integer | yes | The user ID of the member |
+
+```bash
+curl --request PUT --header "PRIVATE-TOKEN: 9koXpg98eAheJpvBs5tK" https://gitlab.example.com/api/v4/groups/:id/members/:user_id/override
+```
+
+Example response:
+
+```json
+{
+ "id": 1,
+ "username": "raymond_smith",
+ "name": "Raymond Smith",
+ "state": "active",
+ "avatar_url": "https://www.gravatar.com/avatar/c2525a7f58ae3776070e44c106c48e15?s=80&d=identicon",
+ "web_url": "http://192.168.1.8:3000/root",
+ "expires_at": "2012-10-22T14:13:35Z",
+ "access_level": 40,
+ "override": true
+}
+```
+
+### Remove override for a member of a group
+
+> [Introduced](https://gitlab.com/gitlab-org/gitlab/issues/4875) in GitLab 12.10.
+
+Sets the override flag to false and allows LDAP Group Sync to reset the access
+level to the LDAP-prescribed value.
+
+```plaintext
+DELETE /groups/:id/members/:user_id/override
+```
+
+| Attribute | Type | Required | Description |
+| --------- | ---- | -------- | ----------- |
+| `id` | integer/string | yes | The ID or [URL-encoded path of the group](README.md#namespaced-path-encoding) owned by the authenticated user |
+| `user_id` | integer | yes | The user ID of the member |
+
+```bash
+curl --request PUT --header "PRIVATE-TOKEN: 9koXpg98eAheJpvBs5tK" https://gitlab.example.com/api/v4/groups/:id/members/:user_id/override
+```
+
+Example response:
+
+```json
+{
+ "id": 1,
+ "username": "raymond_smith",
+ "name": "Raymond Smith",
+ "state": "active",
+ "avatar_url": "https://www.gravatar.com/avatar/c2525a7f58ae3776070e44c106c48e15?s=80&d=identicon",
+ "web_url": "http://192.168.1.8:3000/root",
+ "expires_at": "2012-10-22T14:13:35Z",
+ "access_level": 40,
+ "override": false
+}
+```
+
## Remove a member from a group or project
Removes a user from a group or project.
diff --git a/doc/api/merge_request_approvals.md b/doc/api/merge_request_approvals.md
index 84f5e41496c..c07e52451d2 100644
--- a/doc/api/merge_request_approvals.md
+++ b/doc/api/merge_request_approvals.md
@@ -51,7 +51,7 @@ POST /projects/:id/approvals
| `approvals_before_merge` | integer | no | How many approvals are required before an MR can be merged. Deprecated in 12.0 in favor of Approval Rules API. |
| `reset_approvals_on_push` | boolean | no | Reset approvals on a new push |
| `disable_overriding_approvers_per_merge_request` | boolean | no | Allow/Disallow overriding approvers per MR |
-| `merge_requests_author_approval` | boolean | no | Allow/Disallow authors from self approving merge requests; `true` means authors cannot self approve |
+| `merge_requests_author_approval` | boolean | no | Allow/Disallow authors from self approving merge requests; `true` means authors can self approve |
| `merge_requests_disable_committers_approval` | boolean | no | Allow/Disallow committers from self approving merge requests |
| `require_password_to_approve` | boolean | no | Require approver to enter a password in order to authenticate before adding the approval |
diff --git a/doc/api/merge_requests.md b/doc/api/merge_requests.md
index b0ffeae2b55..3834bb6fee3 100644
--- a/doc/api/merge_requests.md
+++ b/doc/api/merge_requests.md
@@ -12,7 +12,7 @@ NOTE: **Note**
## List merge requests
-> [Introduced][ce-13060] in GitLab 9.5.
+> [Introduced](https://gitlab.com/gitlab-org/gitlab-foss/-/merge_requests/13060) in GitLab 9.5.
Get all merge requests the authenticated user has access to. By
default it returns only merge requests created by the current user. To
@@ -47,17 +47,18 @@ Parameters:
| `view` | string | no | If `simple`, returns the `iid`, URL, title, description, and basic state of merge request |
| `labels` | string | no | Return merge requests matching a comma separated list of labels. `None` lists all merge requests with no labels. `Any` lists all merge requests with at least one label. `No+Label` (Deprecated) lists all merge requests with no labels. Predefined names are case-insensitive. |
| `with_labels_details` | boolean | no | If `true`, response will return more details for each label in labels field: `:name`, `:color`, `:description`, `:description_html`, `:text_color`. Default is `false`. Introduced in [GitLab 12.7](https://gitlab.com/gitlab-org/gitlab/-/merge_requests/21413) |
+| `with_merge_status_recheck` | boolean | no | If `true`, this projection requests (but does not guarantee) that the `merge_status` field be recalculated asynchronously. Default is `false`. Introduced in [GitLab 13.0](https://gitlab.com/gitlab-org/gitlab/-/merge_requests/31890) |
| `created_after` | datetime | no | Return merge requests created on or after the given time |
| `created_before` | datetime | no | Return merge requests created on or before the given time |
| `updated_after` | datetime | no | Return merge requests updated on or after the given time |
| `updated_before` | datetime | no | Return merge requests updated on or before the given time |
| `scope` | string | no | Return merge requests for the given scope: `created_by_me`, `assigned_to_me` or `all`. Defaults to `created_by_me`<br> For versions before 11.0, use the now deprecated `created-by-me` or `assigned-to-me` scopes instead. |
| `author_id` | integer | no | Returns merge requests created by the given user `id`. Mutually exclusive with `author_username`. Combine with `scope=all` or `scope=assigned_to_me`
-| `author_username` | string | no | Returns merge requests created by the given `username`. Mutually exclusive with `author_id`. _([Introduced][ce-13060] in GitLab 12.10)_ | |
+| `author_username` | string | no | Returns merge requests created by the given `username`. Mutually exclusive with `author_id`. _([Introduced](https://gitlab.com/gitlab-org/gitlab-foss/-/merge_requests/13060) in GitLab 12.10)_ | |
| `assignee_id` | integer | no | Returns merge requests assigned to the given user `id`. `None` returns unassigned merge requests. `Any` returns merge requests with an assignee. |
| `approver_ids` **(STARTER)** | integer array | no | Returns merge requests which have specified all the users with the given `id`s as individual approvers. `None` returns merge requests without approvers. `Any` returns merge requests with an approver. |
| `approved_by_ids` **(STARTER)** | integer array | no | Returns merge requests which have been approved by all the users with the given `id`s (Max: 5). `None` returns merge requests with no approvals. `Any` returns merge requests with an approval. |
-| `my_reaction_emoji` | string | no | Return merge requests reacted by the authenticated user by the given `emoji`. `None` returns issues not given a reaction. `Any` returns issues given at least one reaction. _([Introduced][ce-14016] in GitLab 10.0)_ |
+| `my_reaction_emoji` | string | no | Return merge requests reacted by the authenticated user by the given `emoji`. `None` returns issues not given a reaction. `Any` returns issues given at least one reaction. _([Introduced](https://gitlab.com/gitlab-org/gitlab-foss/-/merge_requests/14016) in GitLab 10.0)_ |
| `source_branch` | string | no | Return merge requests with the given source branch |
| `target_branch` | string | no | Return merge requests with the given target branch |
| `search` | string | no | Search merge requests against their `title` and `description` |
@@ -65,6 +66,13 @@ Parameters:
| `wip` | string | no | Filter merge requests against their `wip` status. `yes` to return *only* WIP merge requests, `no` to return *non* WIP merge requests |
NOTE: **Note:**
+[Starting in GitLab 13.0](https://gitlab.com/gitlab-org/gitlab/-/merge_requests/31890),
+listing merge requests may not proactively update the `merge_status` field
+(which also affects the `has_conflicts` field), as this can be an expensive
+operation. If you are interested in the value of these fields from this
+endpoint, set the `with_merge_status_recheck` parameter to `true` in the query.
+
+NOTE: **Note:**
[Starting in GitLab 12.8](https://gitlab.com/gitlab-org/gitlab/issues/29984),
when `async_merge_request_check_mergeability` feature flag is enabled, the
mergeability (`merge_status`) of each merge request will be checked
@@ -227,17 +235,18 @@ Parameters:
| `view` | string | no | If `simple`, returns the `iid`, URL, title, description, and basic state of merge request |
| `labels` | string | no | Return merge requests matching a comma separated list of labels. `None` lists all merge requests with no labels. `Any` lists all merge requests with at least one label. `No+Label` (Deprecated) lists all merge requests with no labels. Predefined names are case-insensitive. |
| `with_labels_details` | boolean | no | If `true`, response will return more details for each label in labels field: `:name`, `:color`, `:description`, `:description_html`, `:text_color`. Default is `false`. Introduced in [GitLab 12.7](https://gitlab.com/gitlab-org/gitlab/-/merge_requests/21413) |
+| `with_merge_status_recheck` | boolean | no | If `true`, this projection requests (but does not guarantee) that the `merge_status` field be recalculated asynchronously. Default is `false`. Introduced in [GitLab 13.0](https://gitlab.com/gitlab-org/gitlab/-/merge_requests/31890) |
| `created_after` | datetime | no | Return merge requests created on or after the given time |
| `created_before` | datetime | no | Return merge requests created on or before the given time |
| `updated_after` | datetime | no | Return merge requests updated on or after the given time |
| `updated_before` | datetime | no | Return merge requests updated on or before the given time |
-| `scope` | string | no | Return merge requests for the given scope: `created_by_me`, `assigned_to_me` or `all`.<br> For versions before 11.0, use the now deprecated `created-by-me` or `assigned-to-me` scopes instead.<br> _([Introduced][ce-13060] in GitLab 9.5. [Changed to snake_case][ce-18935] in GitLab 11.0)_ |
-| `author_id` | integer | no | Returns merge requests created by the given user `id`. Mutually exclusive with `author_username`. _([Introduced][ce-13060] in GitLab 9.5)_
-| `author_username` | string | no | Returns merge requests created by the given `username`. Mutually exclusive with `author_id`. _([Introduced][ce-13060] in GitLab 12.10)_ | |
-| `assignee_id` | integer | no | Returns merge requests assigned to the given user `id`. `None` returns unassigned merge requests. `Any` returns merge requests with an assignee. _([Introduced][ce-13060] in GitLab 9.5)_ |
+| `scope` | string | no | Return merge requests for the given scope: `created_by_me`, `assigned_to_me` or `all`.<br> For versions before 11.0, use the now deprecated `created-by-me` or `assigned-to-me` scopes instead.<br> _([Introduced](https://gitlab.com/gitlab-org/gitlab-foss/-/merge_requests/13060) in GitLab 9.5. [Changed to snake_case](https://gitlab.com/gitlab-org/gitlab-foss/-/merge_requests/18935) in GitLab 11.0)_ |
+| `author_id` | integer | no | Returns merge requests created by the given user `id`. Mutually exclusive with `author_username`. _([Introduced](https://gitlab.com/gitlab-org/gitlab-foss/-/merge_requests/13060) in GitLab 9.5)_
+| `author_username` | string | no | Returns merge requests created by the given `username`. Mutually exclusive with `author_id`. _([Introduced](https://gitlab.com/gitlab-org/gitlab-foss/-/merge_requests/13060) in GitLab 12.10)_ | |
+| `assignee_id` | integer | no | Returns merge requests assigned to the given user `id`. `None` returns unassigned merge requests. `Any` returns merge requests with an assignee. _([Introduced](https://gitlab.com/gitlab-org/gitlab-foss/-/merge_requests/13060) in GitLab 9.5)_ |
| `approver_ids` **(STARTER)** | integer array | no | Returns merge requests which have specified all the users with the given `id`s as individual approvers. `None` returns merge requests without approvers. `Any` returns merge requests with an approver. |
| `approved_by_ids` **(STARTER)** | integer array | no | Returns merge requests which have been approved by all the users with the given `id`s (Max: 5). `None` returns merge requests with no approvals. `Any` returns merge requests with an approval. |
-| `my_reaction_emoji` | string | no | Return merge requests reacted by the authenticated user by the given `emoji`. `None` returns issues not given a reaction. `Any` returns issues given at least one reaction. _([Introduced][ce-14016] in GitLab 10.0)_ |
+| `my_reaction_emoji` | string | no | Return merge requests reacted by the authenticated user by the given `emoji`. `None` returns issues not given a reaction. `Any` returns issues given at least one reaction. _([Introduced](https://gitlab.com/gitlab-org/gitlab-foss/-/merge_requests/14016) in GitLab 10.0)_ |
| `source_branch` | string | no | Return merge requests with the given source branch |
| `target_branch` | string | no | Return merge requests with the given target branch |
| `search` | string | no | Search merge requests against their `title` and `description` |
@@ -390,17 +399,18 @@ Parameters:
| `view` | string | no | If `simple`, returns the `iid`, URL, title, description, and basic state of merge request |
| `labels` | string | no | Return merge requests matching a comma separated list of labels. `None` lists all merge requests with no labels. `Any` lists all merge requests with at least one label. `No+Label` (Deprecated) lists all merge requests with no labels. Predefined names are case-insensitive. |
| `with_labels_details` | boolean | no | If `true`, response will return more details for each label in labels field: `:name`, `:color`, `:description`, `:description_html`, `:text_color`. Default is `false`. Introduced in [GitLab 12.7](https://gitlab.com/gitlab-org/gitlab/-/merge_requests/21413)|
+| `with_merge_status_recheck` | boolean | no | If `true`, this projection requests (but does not guarantee) that the `merge_status` field be recalculated asynchronously. Default is `false`. Introduced in [GitLab 13.0](https://gitlab.com/gitlab-org/gitlab/-/merge_requests/31890) |
| `created_after` | datetime | no | Return merge requests created on or after the given time |
| `created_before` | datetime | no | Return merge requests created on or before the given time |
| `updated_after` | datetime | no | Return merge requests updated on or after the given time |
| `updated_before` | datetime | no | Return merge requests updated on or before the given time |
| `scope` | string | no | Return merge requests for the given scope: `created_by_me`, `assigned_to_me` or `all`.<br> |
-| `author_id` | integer | no | Returns merge requests created by the given user `id`. Mutually exclusive with `author_username`. _([Introduced][ce-13060] in GitLab 9.5)_
-| `author_username` | string | no | Returns merge requests created by the given `username`. Mutually exclusive with `author_id`. _([Introduced][ce-13060] in GitLab 12.10)_ | |
-| `assignee_id` | integer | no | Returns merge requests assigned to the given user `id`. `None` returns unassigned merge requests. `Any` returns merge requests with an assignee. _([Introduced][ce-13060] in GitLab 9.5)_ |
+| `author_id` | integer | no | Returns merge requests created by the given user `id`. Mutually exclusive with `author_username`. _([Introduced](https://gitlab.com/gitlab-org/gitlab-foss/-/merge_requests/13060) in GitLab 9.5)_
+| `author_username` | string | no | Returns merge requests created by the given `username`. Mutually exclusive with `author_id`. _([Introduced](https://gitlab.com/gitlab-org/gitlab-foss/-/merge_requests/13060) in GitLab 12.10)_ | |
+| `assignee_id` | integer | no | Returns merge requests assigned to the given user `id`. `None` returns unassigned merge requests. `Any` returns merge requests with an assignee. _([Introduced](https://gitlab.com/gitlab-org/gitlab-foss/-/merge_requests/13060) in GitLab 9.5)_ |
| `approver_ids` **(STARTER)** | integer array | no | Returns merge requests which have specified all the users with the given `id`s as individual approvers. `None` returns merge requests without approvers. `Any` returns merge requests with an approver. |
| `approved_by_ids` **(STARTER)** | integer array | no | Returns merge requests which have been approved by all the users with the given `id`s (Max: 5). `None` returns merge requests with no approvals. `Any` returns merge requests with an approval. |
-| `my_reaction_emoji` | string | no | Return merge requests reacted by the authenticated user by the given `emoji`. `None` returns issues not given a reaction. `Any` returns issues given at least one reaction. _([Introduced][ce-14016] in GitLab 10.0)_ |
+| `my_reaction_emoji` | string | no | Return merge requests reacted by the authenticated user by the given `emoji`. `None` returns issues not given a reaction. `Any` returns issues given at least one reaction. _([Introduced](https://gitlab.com/gitlab-org/gitlab-foss/-/merge_requests/14016) in GitLab 10.0)_ |
| `source_branch` | string | no | Return merge requests with the given source branch |
| `target_branch` | string | no | Return merge requests with the given target branch |
| `search` | string | no | Search merge requests against their `title` and `description` |
@@ -871,7 +881,7 @@ Parameters:
## List MR pipelines
-> [Introduced][ce-15454] in GitLab 10.5.0.
+> [Introduced](https://gitlab.com/gitlab-org/gitlab-foss/-/merge_requests/15454) in GitLab 10.5.0.
Get a list of merge request pipelines.
@@ -972,7 +982,7 @@ POST /projects/:id/merge_requests
| `assignee_id` | integer | no | Assignee user ID |
| `assignee_ids` | integer array | no | The ID of the user(s) to assign the MR to. Set to `0` or provide an empty value to unassign all assignees. |
| `description` | string | no | Description of MR. Limited to 1,048,576 characters. |
-| `target_project_id` | integer | no | The target project (numeric id) |
+| `target_project_id` | integer | no | The target project (numeric ID) |
| `labels` | string | no | Labels for MR as a comma-separated list |
| `milestone_id` | integer | no | The global ID of a milestone |
| `remove_source_branch` | boolean | no | Flag indicating if a merge request should remove the source branch when merging |
@@ -1130,6 +1140,8 @@ PUT /projects/:id/merge_requests/:merge_request_iid
| `assignee_ids` | integer array | no | The ID of the user(s) to assign the MR to. Set to `0` or provide an empty value to unassign all assignees. |
| `milestone_id` | integer | no | The global ID of a milestone to assign the merge request to. Set to `0` or provide an empty value to unassign a milestone.|
| `labels` | string | no | Comma-separated label names for a merge request. Set to an empty string to unassign all labels. |
+| `add_labels` | string | no | Comma-separated label names to add to a merge request. |
+| `remove_labels` | string | no | Comma-separated label names to remove from a merge request. |
| `description` | string | no | Description of MR. Limited to 1,048,576 characters. |
| `state_event` | string | no | New state (close/reopen) |
| `remove_source_branch` | boolean | no | Flag indicating if a merge request should remove the source branch when merging |
@@ -2435,11 +2447,6 @@ Example response:
}
```
-[ce-13060]: https://gitlab.com/gitlab-org/gitlab-foss/-/merge_requests/13060
-[ce-14016]: https://gitlab.com/gitlab-org/gitlab-foss/-/merge_requests/14016
-[ce-15454]: https://gitlab.com/gitlab-org/gitlab-foss/-/merge_requests/15454
-[ce-18935]: https://gitlab.com/gitlab-org/gitlab-foss/-/merge_requests/18935
-
## Approvals **(STARTER)**
For approvals, please see [Merge Request Approvals](merge_request_approvals.md)
diff --git a/doc/api/metrics_dashboard_annotations.md b/doc/api/metrics_dashboard_annotations.md
index d8a018fe6c3..09187a096ef 100644
--- a/doc/api/metrics_dashboard_annotations.md
+++ b/doc/api/metrics_dashboard_annotations.md
@@ -4,22 +4,16 @@
Metrics dashboard annotations allow you to indicate events on your graphs at a single point in time or over a timespan.
-## Enable the metrics dashboard annotations API
-
-The `:metrics_dashboard_annotations` feature flag is disabled by default.
-To turn on this API, ask a GitLab administrator with Rails console
-access to run the following command:
-
-```ruby
-Feature.enable(:metrics_dashboard_annotations)
-```
-
## Create a new annotation
```plaintext
POST /environments/:id/metrics_dashboard/annotations/
+POST /clusters/:id/metrics_dashboard/annotations/
```
+NOTE: **Note:**
+The value of `dashboard_path` will be treated as a CGI-escaped path, and automatically unescaped.
+
Parameters:
| Attribute | Type | Required | Description |
diff --git a/doc/api/metrics_user_starred_dashboards.md b/doc/api/metrics_user_starred_dashboards.md
new file mode 100644
index 00000000000..dd9144d1319
--- /dev/null
+++ b/doc/api/metrics_user_starred_dashboards.md
@@ -0,0 +1,61 @@
+# User-starred metrics dashboards API
+
+The starred dashboard feature makes navigating to frequently-used dashboards easier
+by displaying favorited dashboards at the top of the select list.
+
+## Add a star to a dashboard
+
+> - [Introduced](https://gitlab.com/gitlab-org/gitlab/-/merge_requests/31316) in GitLab 13.0.
+
+```plaintext
+POST /projects/:id/metrics/user_starred_dashboards
+```
+
+Parameters:
+
+| Attribute | Type | Required | Description |
+|:---------------|:---------------|:---------|:-----------------------------------------------------------------------------|
+| `dashboard_path` | string | yes | URL-encoded path to file defining the dashboard which should be marked as favorite. |
+
+```shell
+curl --header 'Private-Token: <your_access_token>' https://gitlab.example.com/api/v4/projects/20/metrics/user_starred_dashboards \
+ --data-urlencode "dashboard_path=config/prometheus/dashboards/common_metrics.yml"
+```
+
+Example Response:
+
+```json
+{
+ "id": 5,
+ "dashboard_path": "config/prometheus/common_metrics.yml",
+ "user_id": 1,
+ "project_id": 20
+}
+```
+
+## Remove a star from a dashboard
+
+> - [Introduced](https://gitlab.com/gitlab-org/gitlab/-/merge_requests/31892) in GitLab 13.0.
+
+```plaintext
+DELETE /projects/:id/metrics/user_starred_dashboards
+```
+
+Parameters:
+
+| Attribute | Type | Required | Description |
+|:---------------|:---------------|:---------|:-----------------------------------------------------------------------------|
+| `dashboard_path` | string | no | URL-encoded path to file defining the dashboard which should no longer be marked as favorite. When not supplied all dashboards within given projects will be removed from favorites. |
+
+```shell
+curl --request DELETE --header 'Private-Token: <your_access_token>' https://gitlab.example.com/api/v4/projects/20/metrics/user_starred_dashboards \
+ --data-urlencode "dashboard_path=config/prometheus/dashboards/common_metrics.yml"
+```
+
+Example Response:
+
+```json
+{
+ "deleted_rows": 1
+}
+```
diff --git a/doc/api/oauth2.md b/doc/api/oauth2.md
index 959773b217d..a146fdd0d0c 100644
--- a/doc/api/oauth2.md
+++ b/doc/api/oauth2.md
@@ -4,7 +4,7 @@ This document covers using the [OAuth2](https://oauth.net/2/) protocol to allow
other services to access GitLab resources on user's behalf.
If you want GitLab to be an OAuth authentication service provider to sign into
-other services, see the [OAuth2 provider](../integration/oauth_provider.md)
+other services, see the [OAuth2 authentication service provider](../integration/oauth_provider.md)
documentation. This functionality is based on the
[doorkeeper Ruby gem](https://github.com/doorkeeper-gem/doorkeeper).
diff --git a/doc/api/packages.md b/doc/api/packages.md
index 8671de006d2..784343d29fd 100644
--- a/doc/api/packages.md
+++ b/doc/api/packages.md
@@ -68,6 +68,7 @@ GET /groups/:id/packages
| `order_by`| string | no | The field to use as order. One of `created_at` (default), `name`, `version`, `type`, or `project_path`. |
| `sort` | string | no | The direction of the order, either `asc` (default) for ascending order or `desc` for descending order. |
| `package_type` | string | no | Filter the returned packages by type. One of `conan`, `maven`, `npm`, `pypi` or `nuget`. (_Introduced in GitLab 12.9_) |
+| `package_name` | string | no | Filter the project packages with a fuzzy search by name. (_[Introduced](https://gitlab.com/gitlab-org/gitlab/-/merge_requests/30980) in GitLab 13.0_)
```shell
curl --header "PRIVATE-TOKEN: <your_access_token>" https://gitlab.example.com/api/v4/groups/:id/packages?exclude_subgroups=true
@@ -187,7 +188,27 @@ Example response:
"name": "Administrator",
"avatar_url": "https://www.gravatar.com/avatar/e64c7d89f26bd1972efa854d13d7dd61?s=80&d=identicon"
}
- }
+ },
+ "versions": [
+ {
+ "id":2,
+ "version":"2.0-SNAPSHOT",
+ "created_at":"2020-04-28T04:42:11.573Z",
+ "pipeline": {
+ "id": 234,
+ "status": "pending",
+ "ref": "new-pipeline",
+ "sha": "a91957a858320c0e17f3a0eca7cfacbff50ea29a",
+ "web_url": "https://example.com/foo/bar/pipelines/58",
+ "created_at": "2016-08-11T11:28:34.085Z",
+ "updated_at": "2016-08-11T11:32:35.169Z",
+ "user": {
+ "name": "Administrator",
+ "avatar_url": "https://www.gravatar.com/avatar/e64c7d89f26bd1972efa854d13d7dd61?s=80&d=identicon"
+ }
+ }
+ }
+ ]
}
```
diff --git a/doc/api/pages.md b/doc/api/pages.md
index 627c1b284f4..db39ab04d9d 100644
--- a/doc/api/pages.md
+++ b/doc/api/pages.md
@@ -8,7 +8,7 @@ The GitLab Pages feature must be enabled to use these endpoints. Find out more a
Remove pages. The user must have admin privileges.
-```text
+```plaintext
DELETE /projects/:id/pages
```
diff --git a/doc/api/pages_domains.md b/doc/api/pages_domains.md
index 8a047afc3b0..43bb5a9b774 100644
--- a/doc/api/pages_domains.md
+++ b/doc/api/pages_domains.md
@@ -8,7 +8,7 @@ The GitLab Pages feature must be enabled to use these endpoints. Find out more a
Get a list of all pages domains. The user must have admin permissions.
-```text
+```plaintext
GET /pages/domains
```
@@ -35,7 +35,7 @@ curl --header "PRIVATE-TOKEN: <your_access_token>" https://gitlab.example.com/ap
Get a list of project pages domains. The user must have permissions to view pages domains.
-```text
+```plaintext
GET /projects/:id/pages/domains
```
@@ -71,7 +71,7 @@ curl --header "PRIVATE-TOKEN: <your_access_token>" https://gitlab.example.com/ap
Get a single project pages domain. The user must have permissions to view pages domains.
-```text
+```plaintext
GET /projects/:id/pages/domains/:domain
```
@@ -113,7 +113,7 @@ curl --header "PRIVATE-TOKEN: <your_access_token>" https://gitlab.example.com/ap
Creates a new pages domain. The user must have permissions to create new pages domains.
-```text
+```plaintext
POST /projects/:id/pages/domains
```
@@ -155,7 +155,7 @@ curl --request POST --header "PRIVATE-TOKEN: <your_access_token>" --form "domain
Updates an existing project pages domain. The user must have permissions to change an existing pages domains.
-```text
+```plaintext
PUT /projects/:id/pages/domains/:domain
```
@@ -225,7 +225,7 @@ curl --request PUT --header "PRIVATE-TOKEN: <your_access_token>" --form "certifi
Deletes an existing project pages domain.
-```text
+```plaintext
DELETE /projects/:id/pages/domains/:domain
```
diff --git a/doc/api/pipeline_schedules.md b/doc/api/pipeline_schedules.md
index 859e88d0945..36a18411ceb 100644
--- a/doc/api/pipeline_schedules.md
+++ b/doc/api/pipeline_schedules.md
@@ -54,7 +54,7 @@ GET /projects/:id/pipeline_schedules/:pipeline_schedule_id
| Attribute | Type | required | Description |
|--------------|---------|----------|--------------------------|
| `id` | integer/string | yes | The ID or [URL-encoded path of the project](README.md#namespaced-path-encoding) owned by the authenticated user |
-| `pipeline_schedule_id` | integer | yes | The pipeline schedule id |
+| `pipeline_schedule_id` | integer | yes | The pipeline schedule ID |
```shell
curl --header "PRIVATE-TOKEN: k5ESFgWY2Qf5xEvDcFxZ" "https://gitlab.example.com/api/v4/projects/29/pipeline_schedules/13"
@@ -150,7 +150,7 @@ PUT /projects/:id/pipeline_schedules/:pipeline_schedule_id
| Attribute | Type | required | Description |
|---------------|---------|----------|--------------------------|
| `id` | integer/string | yes | The ID or [URL-encoded path of the project](README.md#namespaced-path-encoding) owned by the authenticated user |
-| `pipeline_schedule_id` | integer | yes | The pipeline schedule id |
+| `pipeline_schedule_id` | integer | yes | The pipeline schedule ID |
| `description` | string | no | The description of pipeline schedule |
| `ref` | string | no | The branch/tag name will be triggered |
| `cron` | string | no | The cron (e.g. `0 1 * * *`) ([Cron syntax](https://en.wikipedia.org/wiki/Cron)) |
@@ -200,7 +200,7 @@ POST /projects/:id/pipeline_schedules/:pipeline_schedule_id/take_ownership
| Attribute | Type | required | Description |
|---------------|---------|----------|--------------------------|
| `id` | integer/string | yes | The ID or [URL-encoded path of the project](README.md#namespaced-path-encoding) owned by the authenticated user |
-| `pipeline_schedule_id` | integer | yes | The pipeline schedule id |
+| `pipeline_schedule_id` | integer | yes | The pipeline schedule ID |
```shell
curl --request POST --header "PRIVATE-TOKEN: hf2CvZXB9w8Uc5pZKpSB" "https://gitlab.example.com/api/v4/projects/29/pipeline_schedules/13/take_ownership"
@@ -245,7 +245,7 @@ DELETE /projects/:id/pipeline_schedules/:pipeline_schedule_id
| Attribute | Type | required | Description |
|----------------|---------|----------|--------------------------|
| `id` | integer/string | yes | The ID or [URL-encoded path of the project](README.md#namespaced-path-encoding) owned by the authenticated user |
-| `pipeline_schedule_id` | integer | yes | The pipeline schedule id |
+| `pipeline_schedule_id` | integer | yes | The pipeline schedule ID |
```shell
curl --request DELETE --header "PRIVATE-TOKEN: k5ESFgWY2Qf5xEvDcFxZ" "https://gitlab.example.com/api/v4/projects/29/pipeline_schedules/13"
@@ -286,18 +286,18 @@ curl --request DELETE --header "PRIVATE-TOKEN: k5ESFgWY2Qf5xEvDcFxZ" "https://gi
Trigger a new scheduled pipeline, which runs immediately. The next scheduled run
of this pipeline is not affected.
-```text
+```plaintext
POST /projects/:id/pipeline_schedules/:pipeline_schedule_id/play
```
| Attribute | Type | required | Description |
| ---------------- | --------- | ---------- | -------------------------- |
| `id` | integer/string | yes | The ID or [URL-encoded path of the project](README.md#namespaced-path-encoding) owned by the authenticated user |
-| `pipeline_schedule_id` | integer | yes | The pipeline schedule id |
+| `pipeline_schedule_id` | integer | yes | The pipeline schedule ID |
Example request:
-```sh
+```shell
curl --request POST --header "PRIVATE-TOKEN: <your_access_token>" 'https://gitlab.example.com/api/v4/projects/42/pipeline_schedules/1/play
```
@@ -324,7 +324,7 @@ POST /projects/:id/pipeline_schedules/:pipeline_schedule_id/variables
| Attribute | Type | required | Description |
|------------------------|----------------|----------|--------------------------|
| `id` | integer/string | yes | The ID or [URL-encoded path of the project](README.md#namespaced-path-encoding) owned by the authenticated user |
-| `pipeline_schedule_id` | integer | yes | The pipeline schedule id |
+| `pipeline_schedule_id` | integer | yes | The pipeline schedule ID |
| `key` | string | yes | The `key` of a variable; must have no more than 255 characters; only `A-Z`, `a-z`, `0-9`, and `_` are allowed |
| `value` | string | yes | The `value` of a variable |
| `variable_type` | string | no | The type of a variable. Available types are: `env_var` (default) and `file` |
@@ -352,7 +352,7 @@ PUT /projects/:id/pipeline_schedules/:pipeline_schedule_id/variables/:key
| Attribute | Type | required | Description |
|------------------------|----------------|----------|--------------------------|
| `id` | integer/string | yes | The ID or [URL-encoded path of the project](README.md#namespaced-path-encoding) owned by the authenticated user |
-| `pipeline_schedule_id` | integer | yes | The pipeline schedule id |
+| `pipeline_schedule_id` | integer | yes | The pipeline schedule ID |
| `key` | string | yes | The `key` of a variable |
| `value` | string | yes | The `value` of a variable |
| `variable_type` | string | no | The type of a variable. Available types are: `env_var` (default) and `file` |
@@ -380,7 +380,7 @@ DELETE /projects/:id/pipeline_schedules/:pipeline_schedule_id/variables/:key
| Attribute | Type | required | Description |
|------------------------|----------------|----------|--------------------------|
| `id` | integer/string | yes | The ID or [URL-encoded path of the project](README.md#namespaced-path-encoding) owned by the authenticated user |
-| `pipeline_schedule_id` | integer | yes | The pipeline schedule id |
+| `pipeline_schedule_id` | integer | yes | The pipeline schedule ID |
| `key` | string | yes | The `key` of a variable |
```shell
diff --git a/doc/api/pipeline_triggers.md b/doc/api/pipeline_triggers.md
index 55c6e37c164..1a63a04be71 100644
--- a/doc/api/pipeline_triggers.md
+++ b/doc/api/pipeline_triggers.md
@@ -43,7 +43,7 @@ GET /projects/:id/triggers/:trigger_id
| Attribute | Type | required | Description |
|--------------|---------|----------|--------------------------|
| `id` | integer/string | yes | The ID or [URL-encoded path of the project](README.md#namespaced-path-encoding) owned by the authenticated user |
-| `trigger_id` | integer | yes | The trigger id |
+| `trigger_id` | integer | yes | The trigger ID |
```shell
curl --header "PRIVATE-TOKEN: <your_access_token>" "https://gitlab.example.com/api/v4/projects/1/triggers/5"
@@ -101,7 +101,7 @@ PUT /projects/:id/triggers/:trigger_id
| Attribute | Type | required | Description |
|---------------|---------|----------|--------------------------|
| `id` | integer/string | yes | The ID or [URL-encoded path of the project](README.md#namespaced-path-encoding) owned by the authenticated user |
-| `trigger_id` | integer | yes | The trigger id |
+| `trigger_id` | integer | yes | The trigger ID |
| `description` | string | no | The trigger name |
```shell
@@ -131,7 +131,7 @@ DELETE /projects/:id/triggers/:trigger_id
| Attribute | Type | required | Description |
|----------------|---------|----------|--------------------------|
| `id` | integer/string | yes | The ID or [URL-encoded path of the project](README.md#namespaced-path-encoding) owned by the authenticated user |
-| `trigger_id` | integer | yes | The trigger id |
+| `trigger_id` | integer | yes | The trigger ID |
```shell
curl --request DELETE --header "PRIVATE-TOKEN: <your_access_token>" "https://gitlab.example.com/api/v4/projects/1/triggers/5"
diff --git a/doc/api/pipelines.md b/doc/api/pipelines.md
index 5b67df14ace..e84d7663bdb 100644
--- a/doc/api/pipelines.md
+++ b/doc/api/pipelines.md
@@ -1,5 +1,12 @@
# Pipelines API
+## Pipelines pagination
+
+By default, `GET` requests return 20 results at a time because the API results
+are paginated.
+
+Read more on [pagination](README.md#pagination).
+
## List project pipelines
> [Introduced](https://gitlab.com/gitlab-org/gitlab-foss/-/merge_requests/5837) in GitLab 8.11
@@ -130,6 +137,62 @@ Example of response
]
```
+### Get a pipeline's test report
+
+> [Introduced](https://gitlab.com/gitlab-org/gitlab/-/issues/202525) in GitLab 13.0.
+
+CAUTION: **Caution:**
+This API route is part of the [JUnit test report](../ci/junit_test_reports.md) feature. It is protected by a [feature flag](../development/feature_flags/index.md) that is **disabled** due to performance issues with very large data sets. See [the documentation for the feature](../ci/junit_test_reports.md#enabling-the-feature) for further details.
+
+```plaintext
+GET /projects/:id/pipelines/:pipeline_id/test_report
+```
+
+| Attribute | Type | Required | Description |
+|------------|---------|----------|---------------------|
+| `id` | integer/string | yes | The ID or [URL-encoded path of the project](README.md#namespaced-path-encoding) owned by the authenticated user |
+| `pipeline_id` | integer | yes | The ID of a pipeline |
+
+Sample request:
+
+```shell
+curl --header "PRIVATE-TOKEN: <your_access_token>" "https://gitlab.example.com/api/v4/projects/1/pipelines/46/test_report"
+```
+
+Sample response:
+
+```json
+{
+ "total_time": 5,
+ "total_count": 1,
+ "success_count": 1,
+ "failed_count": 0,
+ "skipped_count": 0,
+ "error_count": 0,
+ "test_suites": [
+ {
+ "name": "Secure",
+ "total_time": 5,
+ "total_count": 1,
+ "success_count": 1,
+ "failed_count": 0,
+ "skipped_count": 0,
+ "error_count": 0,
+ "test_cases": [
+ {
+ "status": "success",
+ "name": "Security Reports can create an auto-remediation MR",
+ "classname": "vulnerability_management_spec",
+ "execution_time": 5,
+ "system_output": null,
+ "stack_trace": null
+ }
+ ]
+ }
+ ]
+}
+```
+
## Create a new pipeline
> [Introduced](https://gitlab.com/gitlab-org/gitlab-foss/-/merge_requests/7209) in GitLab 8.14
@@ -225,7 +288,7 @@ Response:
}
```
-## Cancel a pipelines jobs
+## Cancel a pipeline's jobs
> [Introduced](https://gitlab.com/gitlab-org/gitlab-foss/-/merge_requests/5837) in GitLab 8.11
diff --git a/doc/api/project_badges.md b/doc/api/project_badges.md
index 4afb898eb91..4adeb50deca 100644
--- a/doc/api/project_badges.md
+++ b/doc/api/project_badges.md
@@ -7,7 +7,7 @@
Badges support placeholders that will be replaced in real time in both the link and image URL. The allowed placeholders are:
- **%{project_path}**: will be replaced by the project path.
-- **%{project_id}**: will be replaced by the project id.
+- **%{project_id}**: will be replaced by the project ID.
- **%{default_branch}**: will be replaced by the project default branch.
- **%{commit_sha}**: will be replaced by the last project's commit sha.
diff --git a/doc/api/project_import_export.md b/doc/api/project_import_export.md
index 21a90670aa6..ae2fbcec0ff 100644
--- a/doc/api/project_import_export.md
+++ b/doc/api/project_import_export.md
@@ -18,7 +18,7 @@ data file uploads to the final server.
From GitLab 10.7, the `upload[url]` parameter is required if the `upload` parameter is present.
-```text
+```plaintext
POST /projects/:id/export
```
@@ -42,11 +42,14 @@ curl --request POST --header "PRIVATE-TOKEN: <your_access_token>" https://gitlab
}
```
+NOTE: **Note:**
+The upload request will be sent with `Content-Type: application/gzip` header. Ensure that your pre-signed URL includes this as part of the signature.
+
## Export status
Get the status of export.
-```text
+```plaintext
GET /projects/:id/export
```
@@ -99,7 +102,7 @@ an email notifying the user to download the file, uploading the exported file to
Download the finished export.
-```text
+```plaintext
GET /projects/:id/export/download
```
@@ -118,7 +121,7 @@ ls *export.tar.gz
## Import a file
-```text
+```plaintext
POST /projects/import
```
@@ -182,7 +185,7 @@ requests.post(url, headers=headers, data=data, files=files)
Get the status of an import.
-```text
+```plaintext
GET /projects/:id/import
```
diff --git a/doc/api/project_repository_storage_moves.md b/doc/api/project_repository_storage_moves.md
new file mode 100644
index 00000000000..8df472f193f
--- /dev/null
+++ b/doc/api/project_repository_storage_moves.md
@@ -0,0 +1,80 @@
+# Project repository storage move API
+
+> [Introduced](https://gitlab.com/gitlab-org/gitlab/-/merge_requests/31285) in GitLab 13.0.
+
+Project repository storage can be moved. To retrieve project repository storage moves using the API, you must [authenticate yourself](README.md#authentication) as an administrator.
+
+## Retrieve all project repository storage moves
+
+```plaintext
+GET /project_repository_storage_moves
+```
+
+By default, `GET` requests return 20 results at a time because the API results
+are [paginated](README.md#pagination).
+
+Example request:
+
+```shell
+curl --header "PRIVATE-TOKEN: <your_access_token>" 'https://primary.example.com/api/v4/project_repository_storage_moves'
+```
+
+Example response:
+
+```json
+[
+ {
+ "id": 1,
+ "created_at": "2020-05-07T04:27:17.234Z",
+ "state": "scheduled",
+ "source_storage_name": "default",
+ "destination_storage_name": "storage2",
+ "project": {
+ "id": 1,
+ "description": null,
+ "name": "project1",
+ "name_with_namespace": "John Doe2 / project1",
+ "path": "project1",
+ "path_with_namespace": "namespace1/project1",
+ "created_at": "2020-05-07T04:27:17.016Z"
+ }
+]
+```
+
+## Get a single project repository storage move
+
+```plaintext
+GET /project_repository_storage_moves/:id
+```
+
+Parameters:
+
+| Attribute | Type | Required | Description |
+| --------- | ---- | -------- | ----------- |
+| `id` | integer | yes | The ID of the project repository storage move |
+
+Example request:
+
+```shell
+curl --header "PRIVATE-TOKEN: <your_access_token>" 'https://primary.example.com/api/v4/project_repository_storage_moves/1'
+```
+
+Example response:
+
+```json
+{
+ "id": 1,
+ "created_at": "2020-05-07T04:27:17.234Z",
+ "state": "scheduled",
+ "source_storage_name": "default",
+ "destination_storage_name": "storage2",
+ "project": {
+ "id": 1,
+ "description": null,
+ "name": "project1",
+ "name_with_namespace": "John Doe2 / project1",
+ "path": "project1",
+ "path_with_namespace": "namespace1/project1",
+ "created_at": "2020-05-07T04:27:17.016Z"
+}
+```
diff --git a/doc/api/project_snippets.md b/doc/api/project_snippets.md
index 9a37c675615..e435f87dcdb 100644
--- a/doc/api/project_snippets.md
+++ b/doc/api/project_snippets.md
@@ -60,7 +60,9 @@ Parameters:
},
"updated_at": "2012-06-28T10:52:04Z",
"created_at": "2012-06-28T10:52:04Z",
- "web_url": "http://example.com/example/example/snippets/1"
+ "project_id": 1,
+ "web_url": "http://example.com/example/example/snippets/1",
+ "raw_url": "http://example.com/example/example/snippets/1/raw"
}
```
diff --git a/doc/api/projects.md b/doc/api/projects.md
index 16c8569349c..8cb8961bafa 100644
--- a/doc/api/projects.md
+++ b/doc/api/projects.md
@@ -9,10 +9,8 @@ Values for the project visibility level are:
- `private`:
Project access must be granted explicitly for each user.
-
- `internal`:
The project can be cloned by any logged in user.
-
- `public`:
The project can be accessed without any authentication.
@@ -22,11 +20,9 @@ There are currently three options for `merge_method` to choose from:
- `merge`:
A merge commit is created for every merge, and merging is allowed as long as there are no conflicts.
-
- `rebase_merge`:
A merge commit is created for every merge, but merging is only allowed if fast-forward merge is possible.
This way you could make sure that if this merge request would build, after merging to target branch it would also build.
-
- `ff`:
No merge commits are created and all merges are fast-forwarded, which means that merging is only allowed if the branch could be fast-forwarded.
@@ -162,7 +158,7 @@ When the user is authenticated and `simple` is not set this returns something li
"merge_method": "merge",
"autoclose_referenced_issues": true,
"suggestion_commit_message": null,
- "marked_for_deletion_at": "2020-04-03", // to be deprecated in GitLab 13.0 in favor of marked_for_deletion_on
+ "marked_for_deletion_at": "2020-04-03", // Deprecated and will be removed in API v5 in favor of marked_for_deletion_on
"marked_for_deletion_on": "2020-04-03",
"statistics": {
"commit_count": 37,
@@ -288,7 +284,7 @@ When the user is authenticated and `simple` is not set this returns something li
```
NOTE: **Note:**
-For users on GitLab [Silver, Premium, or higher](https://about.gitlab.com/pricing/) the `marked_for_deletion_at` attribute will be deprecated in GitLab 13.0 in favor of the `marked_for_deletion_on` attribute.
+For users on GitLab [Silver, Premium, or higher](https://about.gitlab.com/pricing/) the `marked_for_deletion_at` attribute has been deprecated and will be removed in API v5 in favor of the `marked_for_deletion_on` attribute.
Users on GitLab [Starter, Bronze, or higher](https://about.gitlab.com/pricing/) will also see
the `approvals_before_merge` parameter:
@@ -411,7 +407,7 @@ This endpoint supports [keyset pagination](README.md#keyset-based-pagination) fo
"merge_method": "merge",
"autoclose_referenced_issues": true,
"suggestion_commit_message": null,
- "marked_for_deletion_at": "2020-04-03", // to be deprecated in GitLab 13.0 in favor of marked_for_deletion_on
+ "marked_for_deletion_at": "2020-04-03", // Deprecated and will be removed in API v5 in favor of marked_for_deletion_on
"marked_for_deletion_on": "2020-04-03",
"statistics": {
"commit_count": 37,
@@ -798,7 +794,9 @@ GET /projects/:id
"enabled": false,
"keep_n": null,
"older_than": null,
- "name_regex": null,
+ "name_regex": null, // to be deprecated in GitLab 13.0 in favor of `name_regex_delete`
+ "name_regex_delete": null,
+ "name_regex_keep": null,
"next_run_at": "2020-01-07T21:42:58.658Z"
},
"created_at": "2013-09-30T13:46:02Z",
@@ -877,7 +875,7 @@ GET /projects/:id
"service_desk_address": null,
"autoclose_referenced_issues": true,
"suggestion_commit_message": null,
- "marked_for_deletion_at": "2020-04-03", // to be deprecated in GitLab 13.0 in favor of marked_for_deletion_on
+ "marked_for_deletion_at": "2020-04-03", // Deprecated and will be removed in API v5 in favor of marked_for_deletion_on
"marked_for_deletion_on": "2020-04-03",
"statistics": {
"commit_count": 37,
@@ -912,7 +910,7 @@ the `approvals_before_merge` parameter:
}
```
-**Note**: The `web_url` and `avatar_url` attributes on `namespace` were [introduced][ce-27427] in GitLab 11.11.
+**Note**: The `web_url` and `avatar_url` attributes on `namespace` were [introduced](https://gitlab.com/gitlab-org/gitlab-foss/-/merge_requests/27427) in GitLab 11.11.
If the project is a fork, and you provide a valid token to authenticate, the
`forked_from_project` field will appear in the response.
@@ -1031,9 +1029,10 @@ POST /projects
| `snippets_access_level` | string | no | One of `disabled`, `private` or `enabled` |
| `pages_access_level` | string | no | One of `disabled`, `private`, `enabled` or `public` |
| `emails_disabled` | boolean | no | Disable email notifications |
+| `show_default_award_emojis` | boolean | no | Show default award emojis |
| `resolve_outdated_diff_discussions` | boolean | no | Automatically resolve merge request diffs discussions on lines changed with a push |
| `container_registry_enabled` | boolean | no | Enable container registry for this project |
-| `container_expiration_policy_attributes` | hash | no | Update the container expiration policy for this project. Accepts: `cadence` (string), `keep_n` (string), `older_than` (string), `name_regex` (string), `enabled` (boolean) |
+| `container_expiration_policy_attributes` | hash | no | Update the image expiration policy for this project. Accepts: `cadence` (string), `keep_n` (string), `older_than` (string), `name_regex` (string), `name_regex_delete` (string), `name_regex_keep` (string), `enabled` (boolean) |
| `shared_runners_enabled` | boolean | no | Enable shared runners for this project |
| `visibility` | string | no | See [project visibility level](#project-visibility-level) |
| `import_url` | string | no | URL to import repository from |
@@ -1100,6 +1099,7 @@ POST /projects/user/:user_id
| `snippets_access_level` | string | no | One of `disabled`, `private` or `enabled` |
| `pages_access_level` | string | no | One of `disabled`, `private`, `enabled` or `public` |
| `emails_disabled` | boolean | no | Disable email notifications |
+| `show_default_award_emojis` | boolean | no | Show default award emojis |
| `resolve_outdated_diff_discussions` | boolean | no | Automatically resolve merge request diffs discussions on lines changed with a push |
| `container_registry_enabled` | boolean | no | Enable container registry for this project |
| `shared_runners_enabled` | boolean | no | Enable shared runners for this project |
@@ -1168,9 +1168,10 @@ PUT /projects/:id
| `snippets_access_level` | string | no | One of `disabled`, `private` or `enabled` |
| `pages_access_level` | string | no | One of `disabled`, `private`, `enabled` or `public` |
| `emails_disabled` | boolean | no | Disable email notifications |
+| `show_default_award_emojis` | boolean | no | Show default award emojis |
| `resolve_outdated_diff_discussions` | boolean | no | Automatically resolve merge request diffs discussions on lines changed with a push |
| `container_registry_enabled` | boolean | no | Enable container registry for this project |
-| `container_expiration_policy_attributes` | hash | no | Update the container expiration policy for this project. Accepts: `cadence` (string), `keep_n` (string), `older_than` (string), `name_regex` (string), `enabled` (boolean) |
+| `container_expiration_policy_attributes` | hash | no | Update the image expiration policy for this project. Accepts: `cadence` (string), `keep_n` (string), `older_than` (string), `name_regex` (string), `name_regex_delete` (string), `name_regex_keep` (string), `enabled` (boolean) |
| `shared_runners_enabled` | boolean | no | Enable shared runners for this project |
| `visibility` | string | no | See [project visibility level](#project-visibility-level) |
| `import_url` | string | no | URL to import repository from |
@@ -2252,5 +2253,3 @@ GET /projects/:id/snapshot
| --------- | ---- | -------- | ----------- |
| `id` | integer/string | yes | The ID or [URL-encoded path of the project](README.md#namespaced-path-encoding) |
| `wiki` | boolean | no | Whether to download the wiki, rather than project, repository |
-
-[ce-27427]: https://gitlab.com/gitlab-org/gitlab-foss/-/merge_requests/27427
diff --git a/doc/api/releases/index.md b/doc/api/releases/index.md
index 47cef0e5fa0..9c3cac3c64f 100644
--- a/doc/api/releases/index.md
+++ b/doc/api/releases/index.md
@@ -96,7 +96,6 @@ Example response:
],
"commit_path":"/root/awesome-app/commit/588440f66559714280628a4f9799f0c4eb880a4a",
"tag_path":"/root/awesome-app/-/tags/v0.11.1",
- "evidence_sha":"760d6cdfb0879c3ffedec13af470e0f71cf52c6cde4d",
"assets":{
"count":6,
"sources":[
@@ -133,6 +132,13 @@ Example response:
],
"evidence_file_path":"https://gitlab.example.com/root/awesome-app/-/releases/v0.2/evidence.json"
},
+ "evidences":[
+ {
+ sha: "760d6cdfb0879c3ffedec13af470e0f71cf52c6cde4d",
+ filepath: "https://gitlab.example.com/root/awesome-app/-/releases/v0.2/evidence.json",
+ collected_at: "2019-01-03T01:56:19.539Z"
+ }
+ ]
},
{
"tag_name":"v0.1",
@@ -165,7 +171,6 @@ Example response:
"committer_email":"admin@example.com",
"committed_date":"2019-01-03T01:53:28.000Z"
},
- "evidence_sha":"760d6cdfb0879c3ffedec13af470e0f71cf52c6cde4d",
"assets":{
"count":4,
"sources":[
@@ -191,6 +196,13 @@ Example response:
],
"evidence_file_path":"https://gitlab.example.com/root/awesome-app/-/releases/v0.1/evidence.json"
},
+ "evidences":[
+ {
+ sha: "c3ffedec13af470e760d6cdfb08790f71cf52c6cde4d",
+ filepath: "https://gitlab.example.com/root/awesome-app/-/releases/v0.1/evidence.json",
+ collected_at: "2019-01-03T01:55:18.203Z"
+ }
+ ]
}
]
```
@@ -286,7 +298,6 @@ Example response:
],
"commit_path":"/root/awesome-app/commit/588440f66559714280628a4f9799f0c4eb880a4a",
"tag_path":"/root/awesome-app/-/tags/v0.11.1",
- "evidence_sha":"760d6cdfb0879c3ffedec13af470e0f71cf52c6cde4d",
"assets":{
"count":5,
"sources":[
@@ -314,9 +325,15 @@ Example response:
"url":"https://gitlab.example.com/root/awesome-app/-/tags/v0.11.1/binaries/linux-amd64",
"external":true
}
- ],
- "evidence_url":"https://gitlab.example.com/root/awesome-app/-/releases/v0.1/evidence.json"
+ ]
},
+ "evidences":[
+ {
+ sha: "760d6cdfb0879c3ffedec13af470e0f71cf52c6cde4d",
+ filepath: "https://gitlab.example.com/root/awesome-app/-/releases/v0.1/evidence.json",
+ collected_at: "2019-07-16T14:00:12.256Z"
+ }
+ ]
}
```
@@ -338,7 +355,7 @@ POST /projects/:id/releases
| `milestones` | array of string | no | The title of each milestone the release is associated with. |
| `assets:links` | array of hash | no | An array of assets links. |
| `assets:links:name`| string | required by: `assets:links` | The name of the link. |
-| `assets:links:url` | string | required by: `assets:links` | The url of the link. |
+| `assets:links:url` | string | required by: `assets:links` | The URL of the link. |
| `assets:links:filepath` | string | no | Optional path for a [Direct Asset link](../../user/project/releases.md).
| `released_at` | datetime | no | The date when the release will be/was ready. Defaults to the current time. Expected in ISO 8601 format (`2019-03-15T08:00:00Z`). |
diff --git a/doc/api/releases/links.md b/doc/api/releases/links.md
index bf882ef35c0..f380ba5a1b2 100644
--- a/doc/api/releases/links.md
+++ b/doc/api/releases/links.md
@@ -21,7 +21,7 @@ GET /projects/:id/releases/:tag_name/assets/links
Example request:
```shell
-curl --header "PRIVATE-TOKEN: gDybLx3yrUK_HLp3qPjS" "https://gitlab.example.com/api/v4/projects/24/releases/v0.1/assets/links"
+curl --header "PRIVATE-TOKEN: n671WNGecHugsdEDPsyo" "https://gitlab.example.com/api/v4/projects/24/releases/v0.1/assets/links"
```
Example response:
@@ -55,12 +55,12 @@ GET /projects/:id/releases/:tag_name/assets/links/:link_id
| ------------- | -------------- | -------- | --------------------------------------- |
| `id` | integer/string | yes | The ID or [URL-encoded path of the project](../README.md#namespaced-path-encoding). |
| `tag_name` | string | yes | The tag associated with the Release. |
-| `link_id` | integer | yes | The id of the link. |
+| `link_id` | integer | yes | The ID of the link. |
Example request:
```shell
-curl --header "PRIVATE-TOKEN: gDybLx3yrUK_HLp3qPjS" "https://gitlab.example.com/api/v4/projects/24/releases/v0.1/assets/links/1"
+curl --header "PRIVATE-TOKEN: n671WNGecHugsdEDPsyo" "https://gitlab.example.com/api/v4/projects/24/releases/v0.1/assets/links/1"
```
Example response:
@@ -93,7 +93,7 @@ Example request:
```shell
curl --request POST \
- --header "PRIVATE-TOKEN: gDybLx3yrUK_HLp3qPjS" \
+ --header "PRIVATE-TOKEN: n671WNGecHugsdEDPsyo" \
--data name="awesome-v0.2.dmg" \
--data url="http://192.168.10.15:3000" \
"https://gitlab.example.com/api/v4/projects/24/releases/v0.1/assets/links"
@@ -122,7 +122,7 @@ PUT /projects/:id/releases/:tag_name/assets/links/:link_id
| ------------- | -------------- | -------- | --------------------------------------- |
| `id` | integer/string | yes | The ID or [URL-encoded path of the project](../README.md#namespaced-path-encoding). |
| `tag_name` | string | yes | The tag associated with the Release. |
-| `link_id` | integer | yes | The id of the link. |
+| `link_id` | integer | yes | The ID of the link. |
| `name` | string | no | The name of the link. |
| `url` | string | no | The URL of the link. |
@@ -132,7 +132,7 @@ You have to specify at least one of `name` or `url`
Example request:
```shell
-curl --request PUT --data name="new name" --header "PRIVATE-TOKEN: gDybLx3yrUK_HLp3qPjS" "https://gitlab.example.com/api/v4/projects/24/releases/v0.1/assets/links/1"
+curl --request PUT --data name="new name" --header "PRIVATE-TOKEN: n671WNGecHugsdEDPsyo" "https://gitlab.example.com/api/v4/projects/24/releases/v0.1/assets/links/1"
```
Example response:
@@ -158,12 +158,12 @@ DELETE /projects/:id/releases/:tag_name/assets/links/:link_id
| ------------- | -------------- | -------- | --------------------------------------- |
| `id` | integer/string | yes | The ID or [URL-encoded path of the project](../README.md#namespaced-path-encoding). |
| `tag_name` | string | yes | The tag associated with the Release. |
-| `link_id` | integer | yes | The id of the link. |
+| `link_id` | integer | yes | The ID of the link. |
Example request:
```shell
-curl --request DELETE --header "PRIVATE-TOKEN: gDybLx3yrUK_HLp3qPjS" "https://gitlab.example.com/api/v4/projects/24/releases/v0.1/assets/links/1"
+curl --request DELETE --header "PRIVATE-TOKEN: n671WNGecHugsdEDPsyo" "https://gitlab.example.com/api/v4/projects/24/releases/v0.1/assets/links/1"
```
Example response:
diff --git a/doc/api/remote_mirrors.md b/doc/api/remote_mirrors.md
index 0ffff194976..e46a890cbd4 100644
--- a/doc/api/remote_mirrors.md
+++ b/doc/api/remote_mirrors.md
@@ -11,13 +11,13 @@ outlined below.
Returns an Array of remote mirrors and their statuses:
-```text
+```plaintext
GET /projects/:id/remote_mirrors
```
Example request:
-```sh
+```shell
curl --header "PRIVATE-TOKEN: <your_access_token>" 'https://gitlab.example.com/api/v4/projects/42/remote_mirrors'
```
@@ -33,6 +33,7 @@ Example response:
"last_update_at": "2020-01-06T17:32:02.823Z",
"last_update_started_at": "2020-01-06T17:31:55.864Z",
"only_protected_branches": true,
+ "keep_divergent_refs": true,
"update_status": "finished",
"url": "https://*****:*****@gitlab.com/gitlab-org/security/gitlab.git"
}
@@ -49,7 +50,7 @@ and password information.
Create a remote mirror for a project. The mirror will be disabled by default. You can enable it by including the optional parameter `enabled` when creating it:
-```text
+```plaintext
POST /projects/:id/remote_mirrors
```
@@ -58,10 +59,11 @@ POST /projects/:id/remote_mirrors
| `url` | String | yes | The URL of the remote repository to be mirrored. |
| `enabled` | Boolean | no | Determines if the mirror is enabled. |
| `only_protected_branches` | Boolean | no | Determines if only protected branches are mirrored. |
+| `keep_divergent_refs` | Boolean | no | Determines if divergent refs are skipped. |
Example request:
-```sh
+```shell
curl --request POST --data "url=https://username:token@example.com/gitlab/example.git" --header "PRIVATE-TOKEN: <your_access_token>" 'https://gitlab.example.com/api/v4/projects/42/remote_mirrors'
```
@@ -76,6 +78,7 @@ Example response:
"last_update_at": null,
"last_update_started_at": null,
"only_protected_branches": false,
+ "keep_divergent_refs": false,
"update_status": "none",
"url": "https://*****:*****@example.com/gitlab/example.git"
}
@@ -88,7 +91,7 @@ Example response:
Toggle a remote mirror on or off, or change which types of branches are
mirrored:
-```text
+```plaintext
PUT /projects/:id/remote_mirrors/:mirror_id
```
@@ -97,10 +100,11 @@ PUT /projects/:id/remote_mirrors/:mirror_id
| `mirror_id` | Integer | yes | The remote mirror ID. |
| `enabled` | Boolean | no | Determines if the mirror is enabled. |
| `only_protected_branches` | Boolean | no | Determines if only protected branches are mirrored. |
+| `keep_divergent_refs` | Boolean | no | Determines if divergent refs are skipped. |
Example request:
-```sh
+```shell
curl --request PUT --data "enabled=false" --header "PRIVATE-TOKEN: <your_access_token>" 'https://gitlab.example.com/api/v4/projects/42/remote_mirrors/101486'
```
@@ -115,6 +119,7 @@ Example response:
"last_update_at": "2020-01-06T17:32:02.823Z",
"last_update_started_at": "2020-01-06T17:31:55.864Z",
"only_protected_branches": true,
+ "keep_divergent_refs": true,
"update_status": "finished",
"url": "https://*****:*****@gitlab.com/gitlab-org/security/gitlab.git"
}
diff --git a/doc/api/repositories.md b/doc/api/repositories.md
index f261c9ab9f7..440db06792c 100644
--- a/doc/api/repositories.md
+++ b/doc/api/repositories.md
@@ -107,6 +107,8 @@ Parameters:
Get an archive of the repository. This endpoint can be accessed without
authentication if the repository is publicly accessible.
+This endpoint has a rate limit threshold of 5 requests per minute.
+
```plaintext
GET /projects/:id/repository/archive[.format]
```
diff --git a/doc/api/repository_files.md b/doc/api/repository_files.md
index e6425c3fe17..a8b58d90c34 100644
--- a/doc/api/repository_files.md
+++ b/doc/api/repository_files.md
@@ -47,7 +47,7 @@ Example response:
Parameters:
-- `file_path` (required) - Url encoded full path to new file. Ex. lib%2Fclass%2Erb
+- `file_path` (required) - URL encoded full path to new file. Ex. lib%2Fclass%2Erb
- `ref` (required) - The name of branch, tag or commit
NOTE: **Note:**
@@ -65,7 +65,7 @@ curl --head --header 'PRIVATE-TOKEN: <your_access_token>' 'https://gitlab.exampl
Example response:
-```text
+```plaintext
HTTP/1.1 200 OK
...
X-Gitlab-Blob-Id: 79f7bbd25901e8334750839545a9bd021f0e4c83
@@ -122,7 +122,7 @@ Example response:
Parameters:
-- `file_path` (required) - Url encoded full path to new file. Ex. lib%2Fclass%2Erb
+- `file_path` (required) - URL encoded full path to new file. Ex. lib%2Fclass%2Erb
- `ref` (required) - The name of branch, tag or commit
NOTE: **Note:**
@@ -134,7 +134,7 @@ curl --head --header 'PRIVATE-TOKEN: <your_access_token>' 'https://gitlab.exampl
Example response:
-```text
+```plaintext
HTTP/1.1 200 OK
...
X-Gitlab-Blob-Id: 79f7bbd25901e8334750839545a9bd021f0e4c83
@@ -161,7 +161,7 @@ curl --header 'PRIVATE-TOKEN: <your_access_token>' 'https://gitlab.example.com/a
Parameters:
-- `file_path` (required) - Url encoded full path to new file. Ex. lib%2Fclass%2Erb
+- `file_path` (required) - URL encoded full path to new file. Ex. lib%2Fclass%2Erb
- `ref` (required) - The name of branch, tag or commit
NOTE: **Note:**
@@ -193,7 +193,7 @@ Example response:
Parameters:
-- `file_path` (required) - Url encoded full path to new file. Ex. lib%2Fclass%2Erb
+- `file_path` (required) - URL encoded full path to new file. Ex. lib%2Fclass%2Erb
- `branch` (required) - Name of the branch
- `start_branch` (optional) - Name of the branch to start the new commit from
- `encoding` (optional) - Change encoding to 'base64'. Default is text.
@@ -228,7 +228,7 @@ Example response:
Parameters:
-- `file_path` (required) - Url encoded full path to new file. Ex. lib%2Fclass%2Erb
+- `file_path` (required) - URL encoded full path to new file. Ex. lib%2Fclass%2Erb
- `branch` (required) - Name of the branch
- `start_branch` (optional) - Name of the branch to start the new commit from
- `encoding` (optional) - Change encoding to 'base64'. Default is text.
@@ -236,7 +236,7 @@ Parameters:
- `author_name` (optional) - Specify the commit author's name
- `content` (required) - New file content
- `commit_message` (required) - Commit message
-- `last_commit_id` (optional) - Last known file commit id
+- `last_commit_id` (optional) - Last known file commit ID
If the commit fails for any reason we return a 400 error with a non-specific
error message. Possible causes for a failed commit include:
@@ -265,10 +265,10 @@ curl --request DELETE --header 'PRIVATE-TOKEN: <your_access_token>' --header "Co
Parameters:
-- `file_path` (required) - Url encoded full path to new file. Ex. lib%2Fclass%2Erb
+- `file_path` (required) - URL encoded full path to new file. Ex. lib%2Fclass%2Erb
- `branch` (required) - Name of the branch
- `start_branch` (optional) - Name of the branch to start the new commit from
- `author_email` (optional) - Specify the commit author's email address
- `author_name` (optional) - Specify the commit author's name
- `commit_message` (required) - Commit message
-- `last_commit_id` (optional) - Last known file commit id
+- `last_commit_id` (optional) - Last known file commit ID
diff --git a/doc/api/runners.md b/doc/api/runners.md
index 21d768a1605..5db1f116f6c 100644
--- a/doc/api/runners.md
+++ b/doc/api/runners.md
@@ -162,9 +162,9 @@ GET /runners/:id
curl --header "PRIVATE-TOKEN: <your_access_token>" "https://gitlab.example.com/api/v4/runners/6"
```
-CAUTION: **Deprecation**
-The `token` attribute in the response is deprecated [since GitLab 12.10](https://gitlab.com/gitlab-org/gitlab/-/issues/214320).
-It will be removed in [GitLab 13.0](https://gitlab.com/gitlab-org/gitlab/-/issues/214322).
+NOTE: **Note:**
+The `token` attribute in the response was deprecated [in GitLab 12.10](https://gitlab.com/gitlab-org/gitlab/-/issues/214320).
+and removed in [GitLab 13.0](https://gitlab.com/gitlab-org/gitlab/-/issues/214322).
Example response:
@@ -190,7 +190,6 @@ Example response:
"path_with_namespace": "gitlab-org/gitlab-foss"
}
],
- "token": "205086a8e3b9a2b818ffac9b89d102",
"revision": null,
"tag_list": [
"ruby",
@@ -225,9 +224,9 @@ PUT /runners/:id
curl --request PUT --header "PRIVATE-TOKEN: <your_access_token>" "https://gitlab.example.com/api/v4/runners/6" --form "description=test-1-20150125-test" --form "tag_list=ruby,mysql,tag1,tag2"
```
-CAUTION: **Deprecation**
-The `token` attribute in the response is deprecated [since GitLab 12.10](https://gitlab.com/gitlab-org/gitlab/-/issues/214320).
-It will be removed in [GitLab 13.0](https://gitlab.com/gitlab-org/gitlab/-/issues/214322).
+NOTE: **Note:**
+The `token` attribute in the response was deprecated [in GitLab 12.10](https://gitlab.com/gitlab-org/gitlab/-/issues/214320).
+and removed in [GitLab 13.0](https://gitlab.com/gitlab-org/gitlab/-/issues/214322).
Example response:
@@ -253,7 +252,6 @@ Example response:
"path_with_namespace": "gitlab-org/gitlab-foss"
}
],
- "token": "205086a8e3b9a2b818ffac9b89d102",
"revision": null,
"tag_list": [
"ruby",
diff --git a/doc/api/scim.md b/doc/api/scim.md
index eaa56b0d0dd..4300c9efa3d 100644
--- a/doc/api/scim.md
+++ b/doc/api/scim.md
@@ -4,6 +4,9 @@
The SCIM API implements the [the RFC7644 protocol](https://tools.ietf.org/html/rfc7644).
+CAUTION: **Caution:**
+This API is for internal system use for connecting with a SCIM provider. While it can be used directly, it is subject to change without notice.
+
NOTE: **Note:**
[Group SSO](../user/group/saml_sso/index.md) must be enabled for the group. For more information, see [SCIM setup documentation](../user/group/saml_sso/scim_setup.md#requirements).
@@ -13,7 +16,7 @@ NOTE: **Note:**
This endpoint is used as part of the SCIM syncing mechanism and it only returns
a single user based on a unique ID which should match the `extern_uid` of the user.
-```text
+```plaintext
GET /api/scim/v2/groups/:group_path/Users
```
@@ -69,7 +72,7 @@ Example response:
## Get a single SAML user
-```text
+```plaintext
GET /api/scim/v2/groups/:group_path/Users/:id
```
@@ -110,7 +113,7 @@ Example response:
## Create a SAML user
-```text
+```plaintext
POST /api/scim/v2/groups/:group_path/Users/
```
@@ -158,15 +161,15 @@ Returns a `201` status code if successful.
Fields that can be updated are:
-| SCIM/IdP field | GitLab field |
-|:----------|:--------|
-| id/externalId | extern_uid |
-| name.formatted | name |
-| emails\[type eq "work"\].value | email |
-| active | Identity removal if `active = false` |
-| userName | username |
+| SCIM/IdP field | GitLab field |
+|:---------------------------------|:---------------------------------------|
+| `id/externalId` | `extern_uid` |
+| `name.formatted` | `name` |
+| `emails\[type eq "work"\].value` | `email` |
+| `active` | Identity removal if `active` = `false` |
+| `userName` | `username` |
-```text
+```plaintext
PATCH /api/scim/v2/groups/:group_path/Users/:id
```
@@ -190,7 +193,7 @@ Returns an empty response with a `204` status code if successful.
Removes the user's SSO identity and group membership.
-```text
+```plaintext
DELETE /api/scim/v2/groups/:group_path/Users/:id
```
diff --git a/doc/api/search.md b/doc/api/search.md
index e1c70fe56a7..7940a2fa4e3 100644
--- a/doc/api/search.md
+++ b/doc/api/search.md
@@ -17,7 +17,7 @@ GET /search
| `scope` | string | yes | The scope to search in |
| `search` | string | yes | The search query |
-Search the expression within the specified scope. Currently these scopes are supported: projects, issues, merge_requests, milestones, snippet_titles, snippet_blobs, users.
+Search the expression within the specified scope. Currently these scopes are supported: projects, issues, merge_requests, milestones, snippet_titles, users.
If Elasticsearch is enabled additional scopes available are blobs, wiki_blobs and commits. Find more about [the feature](../integration/elasticsearch.md). **(STARTER)**
@@ -253,39 +253,6 @@ Example response:
]
```
-### Scope: snippet_blobs
-
-This scope will be disabled after GitLab 13.0.
-
-```shell
-curl --header "PRIVATE-TOKEN: <your_access_token>" https://gitlab.example.com/api/v4/search?scope=snippet_blobs&search=test
-```
-
-Example response:
-
-```json
-[
- {
- "id": 50,
- "title": "Sample file",
- "file_name": "file.rb",
- "description": "Simple ruby file",
- "author": {
- "id": 1,
- "name": "Administrator",
- "username": "root",
- "state": "active",
- "avatar_url": "https://www.gravatar.com/avatar/e64c7d89f26bd1972efa854d13d7dd61?s=80&d=identicon",
- "web_url": "http://localhost:3000/root"
- },
- "updated_at": "2018-02-06T12:49:29.104Z",
- "created_at": "2017-11-28T08:20:18.071Z",
- "project_id": 9,
- "web_url": "http://localhost:3000/root/jira-test/snippets/50"
- }
-]
-```
-
### Scope: wiki_blobs **(STARTER)**
This scope is available only if [Elasticsearch](../integration/elasticsearch.md) is enabled.
diff --git a/doc/api/services.md b/doc/api/services.md
index 3a5268f4271..d435dffa651 100644
--- a/doc/api/services.md
+++ b/doc/api/services.md
@@ -189,9 +189,9 @@ Parameters:
| Parameter | Type | Required | Description |
| --------- | ---- | -------- | ----------- |
-| `new_issue_url` | string | true | New Issue url |
-| `issues_url` | string | true | Issue url |
-| `project_url` | string | true | Project url |
+| `new_issue_url` | string | true | New Issue URL |
+| `issues_url` | string | true | Issue URL |
+| `project_url` | string | true | Project URL |
| `description` | string | false | Description |
| `title` | string | false | Title |
| `push_events` | boolean | false | Enable notifications for push events |
@@ -331,6 +331,51 @@ Get Unify Circuit service settings for a project.
GET /projects/:id/services/unify-circuit
```
+## Webex Teams
+
+Webex Teams collaboration tool.
+
+### Create/Edit Webex Teams service
+
+Set Webex Teams service for a project.
+
+```plaintext
+PUT /projects/:id/services/webex-teams
+```
+
+Parameters:
+
+| Parameter | Type | Required | Description |
+| --------- | ---- | -------- | ----------- |
+| `webhook` | string | true | The Webex Teams webhook. For example, `https://api.ciscospark.com/v1/webhooks/incoming/...`. |
+| `notify_only_broken_pipelines` | boolean | false | Send notifications for broken pipelines |
+| `branches_to_be_notified` | string | all | Branches to send notifications for. Valid options are "all", "default", "protected", and "default_and_protected" |
+| `push_events` | boolean | false | Enable notifications for push events |
+| `issues_events` | boolean | false | Enable notifications for issue events |
+| `confidential_issues_events` | boolean | false | Enable notifications for confidential issue events |
+| `merge_requests_events` | boolean | false | Enable notifications for merge request events |
+| `tag_push_events` | boolean | false | Enable notifications for tag push events |
+| `note_events` | boolean | false | Enable notifications for note events |
+| `confidential_note_events` | boolean | false | Enable notifications for confidential note events |
+| `pipeline_events` | boolean | false | Enable notifications for pipeline events |
+| `wiki_page_events` | boolean | false | Enable notifications for wiki page events |
+
+### Delete Webex Teams service
+
+Delete Webex Teams service for a project.
+
+```plaintext
+DELETE /projects/:id/services/webex-teams
+```
+
+### Get Webex Teams service settings
+
+Get Webex Teams service settings for a project.
+
+```plaintext
+GET /projects/:id/services/webex-teams
+```
+
## Custom Issue Tracker
Custom issue tracker
@@ -347,11 +392,11 @@ Parameters:
| Parameter | Type | Required | Description |
| --------- | ---- | -------- | ----------- |
-| `new_issue_url` | string | true | New Issue url
-| `issues_url` | string | true | Issue url
-| `project_url` | string | true | Project url
-| `description` | string | false | Description
-| `title` | string | false | Title
+| `new_issue_url` | string | true | New Issue URL |
+| `issues_url` | string | true | Issue URL |
+| `project_url` | string | true | Project URL |
+| `description` | string | false | Description |
+| `title` | string | false | Title |
| `push_events` | boolean | false | Enable notifications for push events |
### Delete Custom Issue Tracker service
@@ -567,7 +612,7 @@ Set Hangouts Chat service for a project.
PUT /projects/:id/services/hangouts-chat
```
->**Note:** Specific event parameters (for example, `push_events` flag) were [introduced in v10.4][11435]
+>**Note:** Specific event parameters (for example, `push_events` flag) were [introduced in v10.4](https://gitlab.com/gitlab-org/gitlab-foss/-/merge_requests/11435)
Parameters:
@@ -1034,9 +1079,9 @@ Parameters:
| Parameter | Type | Required | Description |
| --------- | ---- | -------- | ----------- |
-| `new_issue_url` | string | true | New Issue url |
-| `project_url` | string | true | Project url |
-| `issues_url` | string | true | Issue url |
+| `new_issue_url` | string | true | New Issue URL |
+| `project_url` | string | true | Project URL |
+| `issues_url` | string | true | Issue URL |
| `description` | string | false | Description |
| `push_events` | boolean | false | Enable notifications for push events |
@@ -1068,7 +1113,7 @@ Set Slack service for a project.
PUT /projects/:id/services/slack
```
->**Note:** Specific event parameters (for example, `push_events` flag and `push_channel`) were [introduced in v10.4][11435]
+>**Note:** Specific event parameters (for example, `push_events` flag and `push_channel`) were [introduced in v10.4](https://gitlab.com/gitlab-org/gitlab-foss/-/merge_requests/11435)
Parameters:
@@ -1177,7 +1222,7 @@ Set Mattermost service for a project.
PUT /projects/:id/services/mattermost
```
->**Note:** Specific event parameters (for example, `push_events` flag and `push_channel`) were [introduced in v10.4][11435]
+>**Note:** Specific event parameters (for example, `push_events` flag and `push_channel`) were [introduced in v10.4](https://gitlab.com/gitlab-org/gitlab-foss/-/merge_requests/11435)
Parameters:
@@ -1303,6 +1348,9 @@ GET /projects/:id/services/jenkins
A continuous integration and build server
+NOTE: **Note:**
+This service was [removed in v13.0](https://gitlab.com/gitlab-org/gitlab/-/issues/1600)
+
### Create/Edit Jenkins CI (Deprecated) service
Set Jenkins CI (Deprecated) service for a project.
@@ -1369,8 +1417,6 @@ Get MockCI service settings for a project.
GET /projects/:id/services/mock-ci
```
-[11435]: https://gitlab.com/gitlab-org/gitlab-foss/-/merge_requests/11435
-
## YouTrack
YouTrack issue tracker
@@ -1387,8 +1433,8 @@ Parameters:
| Parameter | Type | Required | Description |
| --------- | ---- | -------- | ----------- |
-| `issues_url` | string | true | Issue url |
-| `project_url` | string | true | Project url |
+| `issues_url` | string | true | Issue URL |
+| `project_url` | string | true | Project URL |
| `description` | string | false | Description |
| `push_events` | boolean | false | Enable notifications for push events |
diff --git a/doc/api/settings.md b/doc/api/settings.md
index cf48048c830..f63d126742a 100644
--- a/doc/api/settings.md
+++ b/doc/api/settings.md
@@ -69,7 +69,9 @@ Example response:
"asset_proxy_enabled": true,
"asset_proxy_url": "https://assets.example.com",
"asset_proxy_whitelist": ["example.com", "*.example.com", "your-instance.com"],
- "npm_package_requests_forwarding": true
+ "npm_package_requests_forwarding": true,
+ "issues_create_limit": 300,
+ "raw_blob_request_limit": 300
}
```
@@ -156,7 +158,9 @@ Example response:
"allow_local_requests_from_hooks_and_services": true,
"allow_local_requests_from_web_hooks_and_services": true,
"allow_local_requests_from_system_hooks": false,
- "npm_package_requests_forwarding": true
+ "npm_package_requests_forwarding": true,
+ "issues_create_limit": 300,
+ "raw_blob_request_limit": 300
}
```
@@ -228,12 +232,15 @@ are listed in the descriptions of the relevant settings.
| `elasticsearch_aws` | boolean | no | **(PREMIUM)** Enable the use of AWS hosted Elasticsearch |
| `elasticsearch_aws_region` | string | no | **(PREMIUM)** The AWS region the Elasticsearch domain is configured |
| `elasticsearch_aws_secret_access_key` | string | no | **(PREMIUM)** AWS IAM secret access key |
+| `elasticsearch_indexed_field_length_limit` | integer | no | **(PREMIUM)** Maximum size of text fields that will be indexed by Elasticsearch. 0 value means no limit. This does not apply to repository and wiki indexing. |
| `elasticsearch_indexing` | boolean | no | **(PREMIUM)** Enable Elasticsearch indexing |
| `elasticsearch_limit_indexing` | boolean | no | **(PREMIUM)** Limit Elasticsearch to index certain namespaces and projects |
+| `elasticsearch_max_bulk_concurrency` | integer | no | **(PREMIUM)** Maximum concurrency of Elasticsearch bulk requests per indexing operation. This only applies to repository indexing operations. |
+| `elasticsearch_max_bulk_size_mb` | integer | no | **(PREMIUM)** Maximum size of Elasticsearch bulk indexing requests in MB. This only applies to repository indexing operations. |
| `elasticsearch_namespace_ids` | array of integers | no | **(PREMIUM)** The namespaces to index via Elasticsearch if `elasticsearch_limit_indexing` is enabled. |
| `elasticsearch_project_ids` | array of integers | no | **(PREMIUM)** The projects to index via Elasticsearch if `elasticsearch_limit_indexing` is enabled. |
| `elasticsearch_search` | boolean | no | **(PREMIUM)** Enable Elasticsearch search |
-| `elasticsearch_url` | string | no | **(PREMIUM)** The url to use for connecting to Elasticsearch. Use a comma-separated list to support cluster (for example, `http://localhost:9200, http://localhost:9201"`). If your Elasticsearch instance is password protected, pass the `username:password` in the URL (for example, `http://<username>:<password>@<elastic_host>:9200/`). |
+| `elasticsearch_url` | string | no | **(PREMIUM)** The URL to use for connecting to Elasticsearch. Use a comma-separated list to support cluster (for example, `http://localhost:9200, http://localhost:9201"`). If your Elasticsearch instance is password protected, pass the `username:password` in the URL (for example, `http://<username>:<password>@<elastic_host>:9200/`). |
| `email_additional_text` | string | no | **(PREMIUM)** Additional text added to the bottom of every email for legal/auditing/compliance reasons |
| `email_author_in_body` | boolean | no | Some email servers do not support overriding the email sender name. Enable this option to include the name of the author of the issue, merge request or comment in the email body instead. |
| `enabled_git_access_protocol` | string | no | Enabled protocols for Git access. Allowed values are: `ssh`, `http`, and `nil` to allow both protocols. |
@@ -255,7 +262,7 @@ are listed in the descriptions of the relevant settings.
| `grafana_enabled` | boolean | no | Enable Grafana. |
| `grafana_url` | string | no | Grafana URL. |
| `gravatar_enabled` | boolean | no | Enable Gravatar. |
-| `hashed_storage_enabled` | boolean | no | Create new projects using hashed storage paths: Enable immutable, hash-based paths and repository names to store repositories on disk. This prevents repositories from having to be moved or renamed when the Project URL changes and may improve disk I/O performance. (EXPERIMENTAL) |
+| `hashed_storage_enabled` | boolean | no | Create new projects using hashed storage paths: Enable immutable, hash-based paths and repository names to store repositories on disk. This prevents repositories from having to be moved or renamed when the Project URL changes and may improve disk I/O performance. (Always enabled since 13.0, configuration will be removed in 14.0) |
| `help_page_hide_commercial_content` | boolean | no | Hide marketing-related entries from help. |
| `help_page_support_url` | string | no | Alternate support URL for help page and help dropdown. |
| `help_page_text` | string | no | Custom text displayed on the help page. |
@@ -275,14 +282,7 @@ are listed in the descriptions of the relevant settings.
| `max_attachment_size` | integer | no | Limit attachment size in MB |
| `max_pages_size` | integer | no | Maximum size of pages repositories in MB |
| `max_personal_access_token_lifetime` | integer | no | **(ULTIMATE ONLY)** Maximum allowable lifetime for personal access tokens in days |
-| `metrics_enabled` | boolean | no | (**If enabled, requires:** `metrics_host`, `metrics_method_call_threshold`, `metrics_packet_size`, `metrics_pool_size`, `metrics_port`, `metrics_sample_interval` and `metrics_timeout`) Enable influxDB metrics. |
-| `metrics_host` | string | required by: `metrics_enabled` | InfluxDB host. |
-| `metrics_method_call_threshold` | integer | required by: `metrics_enabled` | A method call is only tracked when it takes longer than the given amount of milliseconds. |
-| `metrics_packet_size` | integer | required by: `metrics_enabled` | The amount of data points to send in a single UDP packet. |
-| `metrics_pool_size` | integer | required by: `metrics_enabled` | The amount of InfluxDB connections to keep open. |
-| `metrics_port` | integer | required by: `metrics_enabled` | The UDP port to use for connecting to InfluxDB. |
-| `metrics_sample_interval` | integer | required by: `metrics_enabled` | The sampling interval in seconds. |
-| `metrics_timeout` | integer | required by: `metrics_enabled` | The amount of seconds after which InfluxDB will time out. |
+| `metrics_method_call_threshold` | integer | no | A method call is only tracked when it takes longer than the given amount of milliseconds. |
| `mirror_available` | boolean | no | Allow repository mirroring to configured by project Maintainers. If disabled, only Admins will be able to configure repository mirroring. |
| `mirror_capacity_threshold` | integer | no | **(PREMIUM)** Minimum capacity to be available before scheduling more mirrors preemptively |
| `mirror_max_capacity` | integer | no | **(PREMIUM)** Maximum number of mirrors that can be synchronizing at the same time. |
@@ -324,13 +324,13 @@ are listed in the descriptions of the relevant settings.
| `sign_in_text` | string | no | Text on the login page. |
| `signup_enabled` | boolean | no | Enable registration. Default is `true`. |
| `slack_app_enabled` | boolean | no | **(PREMIUM)** (**If enabled, requires:** `slack_app_id`, `slack_app_secret` and `slack_app_secret`) Enable Slack app. |
-| `slack_app_id` | string | required by: `slack_app_enabled` | **(PREMIUM)** The app id of the Slack-app. |
+| `slack_app_id` | string | required by: `slack_app_enabled` | **(PREMIUM)** The app ID of the Slack-app. |
| `slack_app_secret` | string | required by: `slack_app_enabled` | **(PREMIUM)** The app secret of the Slack-app. |
| `slack_app_verification_token` | string | required by: `slack_app_enabled` | **(PREMIUM)** The verification token of the Slack-app. |
| `snowplow_collector_hostname` | string | required by: `snowplow_enabled` | The Snowplow collector hostname. (for example, `snowplow.trx.gitlab.net`) |
| `snowplow_cookie_domain` | string | no | The Snowplow cookie domain. (for example, `.gitlab.com`) |
| `snowplow_enabled` | boolean | no | Enable snowplow tracking. |
-| `snowplow_app_id` | string | no | The Snowplow site name / application id. (for example, `gitlab`) |
+| `snowplow_app_id` | string | no | The Snowplow site name / application ID. (for example, `gitlab`) |
| `snowplow_iglu_registry_url` | string | no | The Snowplow base Iglu Schema Registry URL to use for custom context and self describing events'|
| `sourcegraph_enabled` | boolean | no | Enables Sourcegraph integration. Default is `false`. **If enabled, requires** `sourcegraph_url`. |
| `sourcegraph_url` | string | required by: `sourcegraph_enabled` | The Sourcegraph instance URL for integration. |
@@ -357,5 +357,7 @@ are listed in the descriptions of the relevant settings.
| `user_oauth_applications` | boolean | no | Allow users to register any application to use GitLab as an OAuth provider. |
| `user_show_add_ssh_key_message` | boolean | no | When set to `false` disable the "You won't be able to pull or push project code via SSH" warning shown to users with no uploaded SSH key. |
| `version_check_enabled` | boolean | no | Let GitLab inform you when an update is available. |
-| `web_ide_clientside_preview_enabled` | boolean | no | Client side evaluation (allow live previews of JavaScript projects in the Web IDE using CodeSandbox client side evaluation). |
+| `web_ide_clientside_preview_enabled` | boolean | no | Live Preview (allow live previews of JavaScript projects in the Web IDE using CodeSandbox Live Preview). |
| `snippet_size_limit` | integer | no | Max snippet content size in **bytes**. Default: 52428800 Bytes (50MB).|
+| `issues_create_limit` | integer | no | Max number of issue creation requests per minute per user. Default: 300. To disable throttling set to 0.|
+| `raw_blob_request_limit` | integer | no | Max number of requests per minute for each raw path. Default: 300. To disable throttling set to 0.|
diff --git a/doc/api/snippets.md b/doc/api/snippets.md
index 0b41ab557ad..e2e39de412b 100644
--- a/doc/api/snippets.md
+++ b/doc/api/snippets.md
@@ -21,7 +21,7 @@ Valid values for snippet visibility levels are:
Get a list of the current user's snippets.
-```text
+```plaintext
GET /snippets
```
@@ -47,13 +47,13 @@ Example response:
"username": "user0",
"state": "active",
"avatar_url": "https://www.gravatar.com/avatar/52e4ce24a915fb7e51e1ad3b57f4b00a?s=80&d=identicon",
- "web_url": "http://localhost:3000/user0"
+ "web_url": "http://example.com/user0"
},
"updated_at": "2018-09-18T01:12:26.383Z",
"created_at": "2018-09-18T01:12:26.383Z",
"project_id": null,
- "web_url": "http://localhost:3000/snippets/42",
- "raw_url": "http://localhost:3000/snippets/42/raw"
+ "web_url": "http://example.com/snippets/42",
+ "raw_url": "http://example.com/snippets/42/raw"
},
{
"id": 41,
@@ -67,13 +67,13 @@ Example response:
"username": "user0",
"state": "active",
"avatar_url": "https://www.gravatar.com/avatar/52e4ce24a915fb7e51e1ad3b57f4b00a?s=80&d=identicon",
- "web_url": "http://localhost:3000/user0"
+ "web_url": "http://example.com/user0"
},
"updated_at": "2018-09-18T01:12:26.360Z",
"created_at": "2018-09-18T01:12:26.360Z",
- "project_id": null,
- "web_url": "http://localhost:3000/snippets/41",
- "raw_url": "http://localhost:3000/snippets/41/raw"
+ "project_id": 1,
+ "web_url": "http://example.com/gitlab-org/gitlab-test/snippets/41",
+ "raw_url": "http://example.com/gitlab-org/gitlab-test/snippets/41/raw"
}
]
```
@@ -82,7 +82,7 @@ Example response:
Get a single snippet.
-```text
+```plaintext
GET /snippets/:id
```
@@ -118,7 +118,9 @@ Example response:
"expires_at": null,
"updated_at": "2012-06-28T10:52:04Z",
"created_at": "2012-06-28T10:52:04Z",
+ "project_id": null,
"web_url": "http://example.com/snippets/1",
+ "raw_url": "http://example.com/snippets/1/raw"
}
```
@@ -126,7 +128,7 @@ Example response:
Get a single snippet's raw contents.
-```text
+```plaintext
GET /snippets/:id/raw
```
@@ -144,7 +146,7 @@ curl --header "PRIVATE-TOKEN: <your_access_token>" https://gitlab.example.com/ap
Example response:
-```text
+```plaintext
Hello World snippet
```
@@ -155,7 +157,7 @@ Create a new snippet.
NOTE: **Note:**
The user must have permission to create new snippets.
-```text
+```plaintext
POST /snippets
```
@@ -199,7 +201,9 @@ Example response:
"expires_at": null,
"updated_at": "2012-06-28T10:52:04Z",
"created_at": "2012-06-28T10:52:04Z",
+ "project_id": null,
"web_url": "http://example.com/snippets/1",
+ "raw_url": "http://example.com/snippets/1/raw"
}
```
@@ -210,7 +214,7 @@ Update an existing snippet.
NOTE: **Note:**
The user must have permission to change an existing snippet.
-```text
+```plaintext
PUT /snippets/:id
```
@@ -255,7 +259,9 @@ Example response:
"expires_at": null,
"updated_at": "2012-06-28T10:52:04Z",
"created_at": "2012-06-28T10:52:04Z",
+ "project_id": null,
"web_url": "http://example.com/snippets/1",
+ "raw_url": "http://example.com/snippets/1/raw"
}
```
@@ -263,7 +269,7 @@ Example response:
Delete an existing snippet.
-```text
+```plaintext
DELETE /snippets/:id
```
@@ -290,7 +296,7 @@ The following are possible return codes:
List all public snippets.
-```text
+```plaintext
GET /snippets/public
```
@@ -318,15 +324,16 @@ Example response:
"name": "Libby Rolfson",
"state": "active",
"username": "elton_wehner",
- "web_url": "http://localhost:3000/elton_wehner"
+ "web_url": "http://example.com/elton_wehner"
},
"created_at": "2016-11-25T16:53:34.504Z",
"file_name": "oconnerrice.rb",
"id": 49,
- "raw_url": "http://localhost:3000/snippets/49/raw",
"title": "Ratione cupiditate et laborum temporibus.",
"updated_at": "2016-11-25T16:53:34.504Z",
- "web_url": "http://localhost:3000/snippets/49"
+ "project_id": null,
+ "web_url": "http://example.com/snippets/49",
+ "raw_url": "http://example.com/snippets/49/raw"
},
{
"author": {
@@ -335,15 +342,16 @@ Example response:
"name": "Llewellyn Flatley",
"state": "active",
"username": "adaline",
- "web_url": "http://localhost:3000/adaline"
+ "web_url": "http://example.com/adaline"
},
"created_at": "2016-11-25T16:53:34.479Z",
"file_name": "muellershields.rb",
"id": 48,
- "raw_url": "http://localhost:3000/snippets/48/raw",
"title": "Minus similique nesciunt vel fugiat qui ullam sunt.",
"updated_at": "2016-11-25T16:53:34.479Z",
- "web_url": "http://localhost:3000/snippets/48",
+ "project_id": null,
+ "web_url": "http://example.com/snippets/48",
+ "raw_url": "http://example.com/snippets/49/raw",
"visibility": "public"
}
]
@@ -356,7 +364,7 @@ Example response:
NOTE: **Note:**
Available only for administrators.
-```text
+```plaintext
GET /snippets/:id/user_agent_detail
```
diff --git a/doc/api/templates/gitignores.md b/doc/api/templates/gitignores.md
index 3acd666ad66..6e2e3e2d07f 100644
--- a/doc/api/templates/gitignores.md
+++ b/doc/api/templates/gitignores.md
@@ -2,7 +2,7 @@
type: reference
---
-# .gitignore API
+# `.gitignore` API
In GitLab, there is an API endpoint available for `.gitignore`. For more
information on `gitignore`, see the
diff --git a/doc/api/todos.md b/doc/api/todos.md
index 2711766c634..cd6f2468cc6 100644
--- a/doc/api/todos.md
+++ b/doc/api/todos.md
@@ -101,7 +101,8 @@ Example Response:
"target_url": "https://gitlab.example.com/gitlab-org/gitlab-foss/-/merge_requests/7",
"body": "Dolores in voluptatem tenetur praesentium omnis repellendus voluptatem quaerat.",
"state": "pending",
- "created_at": "2016-06-17T07:52:35.225Z"
+ "created_at": "2016-06-17T07:52:35.225Z",
+ "updated_at": "2016-06-17T07:52:35.225Z"
},
{
"id": 98,
@@ -174,7 +175,8 @@ Example Response:
"target_url": "https://gitlab.example.com/gitlab-org/gitlab-foss/-/merge_requests/7",
"body": "Dolores in voluptatem tenetur praesentium omnis repellendus voluptatem quaerat.",
"state": "pending",
- "created_at": "2016-06-17T07:49:24.624Z"
+ "created_at": "2016-06-17T07:49:24.624Z",
+ "updated_at": "2016-06-17T07:49:24.624Z"
}
]
```
@@ -272,7 +274,8 @@ Example Response:
"target_url": "https://gitlab.example.com/gitlab-org/gitlab-foss/-/merge_requests/7",
"body": "Dolores in voluptatem tenetur praesentium omnis repellendus voluptatem quaerat.",
"state": "done",
- "created_at": "2016-06-17T07:52:35.225Z"
+ "created_at": "2016-06-17T07:52:35.225Z",
+ "updated_at": "2016-06-17T07:52:35.225Z"
}
```
diff --git a/doc/api/users.md b/doc/api/users.md
index 90aafcef035..28d233ce6b3 100644
--- a/doc/api/users.md
+++ b/doc/api/users.md
@@ -611,7 +611,7 @@ GET /users/:id_or_username/status
| Attribute | Type | Required | Description |
| ---------------- | ------ | -------- | ----------- |
-| `id_or_username` | string | yes | The id or username of the user to get a status of |
+| `id_or_username` | string | yes | The ID or username of the user to get a status of |
```shell
curl "https://gitlab.example.com/users/janedoe/status"
@@ -723,7 +723,7 @@ GET /users/:id_or_username/keys
| Attribute | Type | Required | Description |
| ---------------- | ------ | -------- | ----------- |
-| `id_or_username` | string | yes | The id or username of the user to get the SSH keys for. |
+| `id_or_username` | string | yes | The ID or username of the user to get the SSH keys for. |
## Single SSH key
@@ -794,7 +794,7 @@ POST /users/:id/keys
Parameters:
-- `id` (required) - id of specified user
+- `id` (required) - ID of specified user
- `title` (required) - new SSH Key's title
- `key` (required) - new SSH key
@@ -821,7 +821,7 @@ DELETE /users/:id/keys/:key_id
Parameters:
-- `id` (required) - id of specified user
+- `id` (required) - ID of specified user
- `key_id` (required) - SSH key ID
## List all GPG keys
@@ -1070,7 +1070,7 @@ GET /users/:id/emails
Parameters:
-- `id` (required) - id of specified user
+- `id` (required) - ID of specified user
## Single email
@@ -1133,7 +1133,7 @@ POST /users/:id/emails
Parameters:
-- `id` (required) - id of specified user
+- `id` (required) - ID of specified user
- `email` (required) - email address
- `skip_confirmation` (optional) - Skip confirmation and assume e-mail is verified - true or false (default)
@@ -1160,7 +1160,7 @@ DELETE /users/:id/emails/:email_id
Parameters:
-- `id` (required) - id of specified user
+- `id` (required) - ID of specified user
- `email_id` (required) - email ID
## Block user
@@ -1173,7 +1173,7 @@ POST /users/:id/block
Parameters:
-- `id` (required) - id of specified user
+- `id` (required) - ID of specified user
Returns:
@@ -1191,7 +1191,7 @@ POST /users/:id/unblock
Parameters:
-- `id` (required) - id of specified user
+- `id` (required) - ID of specified user
Will return `201 OK` on success, `404 User Not Found` is user cannot be found or
`403 Forbidden` when trying to unblock a user blocked by LDAP synchronization.
@@ -1208,7 +1208,7 @@ POST /users/:id/deactivate
Parameters:
-- `id` (required) - id of specified user
+- `id` (required) - ID of specified user
Returns:
@@ -1230,7 +1230,7 @@ POST /users/:id/activate
Parameters:
-- `id` (required) - id of specified user
+- `id` (required) - ID of specified user
Returns:
@@ -1409,6 +1409,7 @@ The activities that update the timestamp are:
- User logging in into GitLab
- User visiting pages related to Dashboards, Projects, Issues, and Merge Requests ([introduced](https://gitlab.com/gitlab-org/gitlab-foss/issues/54947) in GitLab 11.8)
- User using the API
+- User using the GraphQL API
By default, it shows the activity for all users in the last 6 months, but this can be
amended by using the `from` parameter.
diff --git a/doc/api/vulnerability_exports.md b/doc/api/vulnerability_exports.md
index f2666783087..2c9ac5d65eb 100644
--- a/doc/api/vulnerability_exports.md
+++ b/doc/api/vulnerability_exports.md
@@ -1,6 +1,6 @@
-# Project Vulnerabilities API **(ULTIMATE)**
+# Vulnerability export API **(ULTIMATE)**
-> [Introduced](https://gitlab.com/gitlab-org/gitlab/issues/197494) in [GitLab Ultimate](https://about.gitlab.com/pricing/) 12.10.
+> [Introduced](https://gitlab.com/gitlab-org/gitlab/issues/197494) in [GitLab Ultimate](https://about.gitlab.com/pricing/) 12.10. [Updated](https://gitlab.com/gitlab-org/gitlab/-/merge_requests/30397) in [GitLab Ultimate](https://about.gitlab.com/pricing/) 13.0.
CAUTION: **Caution:**
This API is currently in development and is protected by a **disabled**
@@ -17,21 +17,21 @@ across GitLab releases.
Every API call to vulnerability exports must be [authenticated](README.md#authentication).
+## Create a project-level vulnerability export
+
+Creates a new vulnerability export for a project.
+
Vulnerability export permissions inherit permissions from their project. If a project is
private and a user isn't a member of the project to which the vulnerability
belongs, requests to that project return a `404 Not Found` status code.
Vulnerability exports can be only accessed by the export's author.
-## Create vulnerability export
-
-Creates a new vulnerability export.
-
If an authenticated user doesn't have permission to
[create a new vulnerability](../user/permissions.md#project-members-permissions),
this request results in a `403` status code.
```plaintext
-POST /projects/:id/vulnerability_exports
+POST /security/projects/:id/vulnerability_exports
```
| Attribute | Type | Required | Description |
@@ -39,10 +39,10 @@ POST /projects/:id/vulnerability_exports
| `id` | integer or string | yes | The ID or [URL-encoded path](README.md#namespaced-path-encoding) of the project which the authenticated user is a member of |
```shell
-curl --header POST "PRIVATE-TOKEN: <your_access_token>" https://gitlab.example.com/api/v4/projects/1/vulnerability_exports
+curl --header POST "PRIVATE-TOKEN: <your_access_token>" https://gitlab.example.com/api/v4/security/projects/1/vulnerability_exports
```
-The created vulnerability export will be automatically deleted after 1 hour.
+The created vulnerability export is automatically deleted after 1 hour.
Example response:
@@ -51,13 +51,93 @@ Example response:
"id": 2,
"created_at": "2020-03-30T09:35:38.746Z",
"project_id": 1,
+ "group_id": null,
"format": "csv",
"status": "created",
"started_at": null,
"finished_at": null,
"_links": {
- "self": "https://gitlab.example.com/api/v4/projects/1/vulnerability_exports/2",
- "download": "https://gitlab.example.com/api/v4/projects/1/vulnerability_exports/2/download"
+ "self": "https://gitlab.example.com/api/v4/security/vulnerability_exports/2",
+ "download": "https://gitlab.example.com/api/v4/security/vulnerability_exports/2/download"
+ }
+}
+```
+
+## Create a group-level vulnerability export
+
+Creates a new vulnerability export for a group.
+
+Vulnerability export permissions inherit permissions from their group. If a group is
+private and a user isn't a member of the group to which the vulnerability
+belongs, requests to that group return a `404 Not Found` status code.
+Vulnerability exports can be only accessed by the export's author.
+
+If an authenticated user doesn't have permission to
+[create a new vulnerability](../user/permissions.md#group-members-permissions),
+this request results in a `403` status code.
+
+```plaintext
+POST /security/groups/:id/vulnerability_exports
+```
+
+| Attribute | Type | Required | Description |
+| ------------------- | ----------------- | ---------- | -----------------------------------------------------------------------------------------------------------------------------|
+| `id` | integer or string | yes | The ID or [URL-encoded path](README.md#namespaced-path-encoding) of the group which the authenticated user is a member of |
+
+```shell
+curl --header POST "PRIVATE-TOKEN: <your_access_token>" https://gitlab.example.com/api/v4/security/groups/1/vulnerability_exports
+```
+
+The created vulnerability export is automatically deleted after 1 hour.
+
+Example response:
+
+```json
+{
+ "id": 2,
+ "created_at": "2020-03-30T09:35:38.746Z",
+ "project_id": null,
+ "group_id": 1,
+ "format": "csv",
+ "status": "created",
+ "started_at": null,
+ "finished_at": null,
+ "_links": {
+ "self": "https://gitlab.example.com/api/v4/security/vulnerability_exports/2",
+ "download": "https://gitlab.example.com/api/v4/security/vulnerability_exports/2/download"
+ }
+}
+```
+
+## Create an instance-level vulnerability export
+
+Creates a new vulnerability export for the projects of the user selected in the Security Dashboard.
+
+```plaintext
+POST /security/vulnerability_exports
+```
+
+```shell
+curl --header POST "PRIVATE-TOKEN: <your_access_token>" https://gitlab.example.com/api/v4/security/vulnerability_exports
+```
+
+The created vulnerability export is automatically deleted after one hour.
+
+Example response:
+
+```json
+{
+ "id": 2,
+ "created_at": "2020-03-30T09:35:38.746Z",
+ "project_id": null,
+ "group_id": null,
+ "format": "csv",
+ "status": "created",
+ "started_at": null,
+ "finished_at": null,
+ "_links": {
+ "self": "https://gitlab.example.com/api/v4/security/vulnerability_exports/2",
+ "download": "https://gitlab.example.com/api/v4/security/vulnerability_exports/2/download"
}
}
```
@@ -67,16 +147,15 @@ Example response:
Gets a single vulnerability export.
```plaintext
-POST /projects/:id/vulnerability_exports/:vulnerability_export_id
+GET /security/vulnerability_exports/:id
```
| Attribute | Type | Required | Description |
| --------- | ---- | -------- | ----------- |
-| `id` | integer or string | yes | The vulnerability's ID |
-| `vulnerability_export_id` | integer or string | yes | The vulnerability export's ID |
+| `id` | integer or string | yes | The vulnerability export's ID |
```shell
-curl --header "PRIVATE-TOKEN: <your_access_token>" https://gitlab.example.com/api/v4/projects/1/vulnerability_exports/2
+curl --header "PRIVATE-TOKEN: <your_access_token>" https://gitlab.example.com/api/v4/security/vulnerability_exports/2
```
If the vulnerability export isn't finished, the response is `202 Accepted`.
@@ -88,13 +167,14 @@ Example response:
"id": 2,
"created_at": "2020-03-30T09:35:38.746Z",
"project_id": 1,
+ "group_id": null,
"format": "csv",
"status": "finished",
"started_at": "2020-03-30T09:36:54.469Z",
"finished_at": "2020-03-30T09:36:55.008Z",
"_links": {
- "self": "https://gitlab.example.com/api/v4/projects/1/vulnerability_exports/2",
- "download": "https://gitlab.example.com/api/v4/projects/1/vulnerability_exports/2/download"
+ "self": "https://gitlab.example.com/api/v4/security/vulnerability_exports/2",
+ "download": "https://gitlab.example.com/api/v4/security/vulnerability_exports/2/download"
}
}
```
@@ -104,16 +184,15 @@ Example response:
Downloads a single vulnerability export.
```plaintext
-POST /projects/:id/vulnerability_exports/:vulnerability_export_id/download
+GET /security/vulnerability_exports/:id/download
```
| Attribute | Type | Required | Description |
| --------- | ---- | -------- | ----------- |
-| `id` | integer or string | yes | The vulnerability's ID |
-| `vulnerability_export_id` | integer or string | yes | The vulnerability export's ID |
+| `id` | integer or string | yes | The vulnerability export's ID |
```shell
-curl --header "PRIVATE-TOKEN: <your_access_token>" https://gitlab.example.com/api/v4/projects/1/vulnerability_exports/2/download
+curl --header "PRIVATE-TOKEN: <your_access_token>" https://gitlab.example.com/api/v4/security/vulnerability_exports/2/download
```
The response will be `404 Not Found` if the vulnerability export is not finished yet or was not found.
@@ -121,18 +200,18 @@ The response will be `404 Not Found` if the vulnerability export is not finished
Example response:
```csv
-Scanner Type,Scanner Name,Status,Vulnerability,Details,Additional Info,Severity,CVE
-container_scanning,Clair,confirmed,CVE-2017-16997 in glibc,,CVE-2017-16997 in glibc,critical,CVE-2017-16997
-container_scanning,Clair,detected,CVE-2017-18269 in glibc,,CVE-2017-18269 in glibc,critical,CVE-2017-18269
-container_scanning,Clair,detected,CVE-2018-1000001 in glibc,,CVE-2018-1000001 in glibc,high,CVE-2018-1000001
-container_scanning,Clair,detected,CVE-2016-10228 in glibc,,CVE-2016-10228 in glibc,medium,CVE-2016-10228
-container_scanning,Clair,confirmed,CVE-2010-4052 in glibc,,CVE-2010-4052 in glibc,low,CVE-2010-4052
-container_scanning,Clair,detected,CVE-2018-18520 in elfutils,,CVE-2018-18520 in elfutils,low,CVE-2018-18520
-container_scanning,Clair,detected,CVE-2018-16869 in nettle,,CVE-2018-16869 in nettle,unknown,CVE-2018-16869
-dependency_scanning,Gemnasium,detected,Regular Expression Denial of Service in debug,,Regular Expression Denial of Service in debug,unknown,yarn.lock:debug:gemnasium:37283ed4-0380-40d7-ada7-2d994afcc62a
-dependency_scanning,Gemnasium,detected,Authentication bypass via incorrect DOM traversal and canonicalization in saml2-js,,Authentication bypass via incorrect DOM traversal and canonicalization in saml2-js,unknown,yarn.lock:saml2-js:gemnasium:9952e574-7b5b-46fa-a270-aeb694198a98
-sast,Find Security Bugs,detected,Predictable pseudorandom number generator,,Predictable pseudorandom number generator,medium,818bf5dacb291e15d9e6dc3c5ac32178:PREDICTABLE_RANDOM:src/main/java/com/gitlab/security_products/tests/App.java:47
-sast,Find Security Bugs,detected,Cipher with no integrity,,Cipher with no integrity,medium,e6449b89335daf53c0db4c0219bc1634:CIPHER_INTEGRITY:src/main/java/com/gitlab/security_products/tests/App.java:29
-sast,Find Security Bugs,detected,Predictable pseudorandom number generator,,Predictable pseudorandom number generator,medium,e8ff1d01f74cd372f78da8f5247d3e73:PREDICTABLE_RANDOM:src/main/java/com/gitlab/security_products/tests/App.java:41
-sast,Find Security Bugs,confirmed,ECB mode is insecure 2,,ECB mode is insecure,medium,ea0f905fc76f2739d5f10a1fd1e37a10:ECB_MODE:src/main/java/com/gitlab/security_products/tests/App.java:29
-```
+Group Name,Project Name,Scanner Type,Scanner Name,Status,Vulnerability,Details,Additional Info,Severity,CVE
+Gitlab.org,Defend,container_scanning,Clair,confirmed,CVE-2017-16997 in glibc,,CVE-2017-16997 in glibc,critical,CVE-2017-16997
+Gitlab.org,Defend,container_scanning,Clair,detected,CVE-2017-18269 in glibc,,CVE-2017-18269 in glibc,critical,CVE-2017-18269
+Gitlab.org,Defend,container_scanning,Clair,detected,CVE-2018-1000001 in glibc,,CVE-2018-1000001 in glibc,high,CVE-2018-1000001
+Gitlab.org,Defend,container_scanning,Clair,detected,CVE-2016-10228 in glibc,,CVE-2016-10228 in glibc,medium,CVE-2016-10228
+Gitlab.org,Defend,container_scanning,Clair,confirmed,CVE-2010-4052 in glibc,,CVE-2010-4052 in glibc,low,CVE-2010-4052
+Gitlab.org,Defend,container_scanning,Clair,detected,CVE-2018-18520 in elfutils,,CVE-2018-18520 in elfutils,low,CVE-2018-18520
+Gitlab.org,Defend,container_scanning,Clair,detected,CVE-2018-16869 in nettle,,CVE-2018-16869 in nettle,unknown,CVE-2018-16869
+Gitlab.org,Defend,dependency_scanning,Gemnasium,detected,Regular Expression Denial of Service in debug,,Regular Expression Denial of Service in debug,unknown,yarn.lock:debug:gemnasium:37283ed4-0380-40d7-ada7-2d994afcc62a
+Gitlab.org,Defend,dependency_scanning,Gemnasium,detected,Authentication bypass via incorrect DOM traversal and canonicalization in saml2-js,,Authentication bypass via incorrect DOM traversal and canonicalization in saml2-js,unknown,yarn.lock:saml2-js:gemnasium:9952e574-7b5b-46fa-a270-aeb694198a98
+Gitlab.org,Defend,sast,Find Security Bugs,detected,Predictable pseudorandom number generator,,Predictable pseudorandom number generator,medium,818bf5dacb291e15d9e6dc3c5ac32178:PREDICTABLE_RANDOM:src/main/java/com/gitlab/security_products/tests/App.java:47
+Gitlab.org,Defend,sast,Find Security Bugs,detected,Cipher with no integrity,,Cipher with no integrity,medium,e6449b89335daf53c0db4c0219bc1634:CIPHER_INTEGRITY:src/main/java/com/gitlab/security_products/tests/App.java:29
+Gitlab.org,Defend,sast,Find Security Bugs,detected,Predictable pseudorandom number generator,,Predictable pseudorandom number generator,medium,e8ff1d01f74cd372f78da8f5247d3e73:PREDICTABLE_RANDOM:src/main/java/com/gitlab/security_products/tests/App.java:41
+Gitlab.org,Defend,sast,Find Security Bugs,confirmed,ECB mode is insecure 2,,ECB mode is insecure,medium,ea0f905fc76f2739d5f10a1fd1e37a10:ECB_MODE:src/main/java/com/gitlab/security_products/tests/App.java:29
+Gitlab.org,Defend,```
diff --git a/doc/api/wikis.md b/doc/api/wikis.md
index cdaf95fc291..48b04fefd39 100644
--- a/doc/api/wikis.md
+++ b/doc/api/wikis.md
@@ -1,6 +1,6 @@
# Wikis API
-> [Introduced][ce-13372] in GitLab 10.0.
+> [Introduced](https://gitlab.com/gitlab-org/gitlab-foss/-/merge_requests/13372) in GitLab 10.0.
Available only in APIv4.
@@ -153,8 +153,6 @@ curl --request DELETE --header "PRIVATE-TOKEN: <your_access_token>" "https://git
On success the HTTP status code is `204` and no JSON response is expected.
-[ce-13372]: https://gitlab.com/gitlab-org/gitlab-foss/-/merge_requests/13372
-
## Upload an attachment to the wiki repository
Uploads a file to the attachment folder inside the wiki's repository. The
diff --git a/doc/ci/README.md b/doc/ci/README.md
index 30971d422cc..fce0ad15b70 100644
--- a/doc/ci/README.md
+++ b/doc/ci/README.md
@@ -37,14 +37,17 @@ your app.
For a complete overview of these methodologies and GitLab CI/CD,
read the [Introduction to CI/CD with GitLab](introduction/index.md).
-<i class="fa fa-youtube-play youtube" aria-hidden="true"></i>
-For a video demonstration of GitLab CI/CD, see [Demo: CI/CD with GitLab](https://www.youtube.com/watch?v=1iXFbchozdY).
+<div class="video-fallback">
+ Video demonstration of GitLab CI/CD: <a href="https://www.youtube.com/watch?v=1iXFbchozdY">Demo: CI/CD with GitLab</a>.
+</div>
+<figure class="video-container">
+ <iframe src="https://www.youtube.com/embed/1iXFbchozdY" frameborder="0" allowfullscreen="true"> </iframe>
+</figure>
## Getting started
GitLab CI/CD is configured by a file called `.gitlab-ci.yml` placed
-at the repository's root. The scripts set in this file are executed
-by the [GitLab Runner](https://docs.gitlab.com/runner/).
+at the repository's root. This file creates a [pipeline](pipelines/index.md), which runs for changes to the code in the repository. Pipelines consist of one or more stages that run in order and can each contain one or more jobs that run in parallel. These jobs (or scripts) get executed by the [GitLab Runner](https://docs.gitlab.com/runner/) agent.
To get started with GitLab CI/CD, we recommend you read through
the following documents:
@@ -74,25 +77,32 @@ for all the attributes you can set and use.
NOTE: **Note:**
GitLab CI/CD and [shared runners](runners/README.md#shared-specific-and-group-runners) are enabled in GitLab.com and available for all users, limited only to the [user's pipelines quota](../user/gitlab_com/index.md#shared-runners).
-## Configuration
+## Concepts
-GitLab CI/CD supports numerous configuration options:
+GitLab CI/CD uses a number of concepts to describe and run your build and deploy.
-| Configuration | Description |
+| Concept | Description |
|:--------------|:-------------|
| [Pipelines](pipelines/index.md) | Structure your CI/CD process through pipelines. |
| [Environment variables](variables/README.md) | Reuse values based on a variable/value key pair. |
-| [Environments](environments.md) | Deploy your application to different environments (e.g., staging, production). |
+| [Environments](environments/index.md) | Deploy your application to different environments (e.g., staging, production). |
| [Job artifacts](pipelines/job_artifacts.md) | Output, use, and reuse job artifacts. |
| [Cache dependencies](caching/index.md) | Cache your dependencies for a faster execution. |
+| [GitLab Runner](https://docs.gitlab.com/runner/) | Configure your own GitLab Runners to execute your scripts. |
+
+## Configuration
+
+GitLab CI/CD supports numerous configuration options:
+
+| Configuration | Description |
+|:--------------|:-------------|
| [Schedule pipelines](pipelines/schedules.md) | Schedule pipelines to run as often as you need. |
| [Custom path for `.gitlab-ci.yml`](pipelines/settings.md#custom-ci-configuration-path) | Define a custom path for the CI/CD configuration file. |
| [Git submodules for CI/CD](git_submodules.md) | Configure jobs for using Git submodules.|
| [SSH keys for CI/CD](ssh_keys/README.md) | Using SSH keys in your CI pipelines. |
-| [Pipelines triggers](triggers/README.md) | Trigger pipelines through the API. |
+| [Pipeline triggers](triggers/README.md) | Trigger pipelines through the API. |
| [Pipelines for Merge Requests](merge_request_pipelines/index.md) | Design a pipeline structure for running a pipeline in merge requests. |
| [Integrate with Kubernetes clusters](../user/project/clusters/index.md) | Connect your project to Google Kubernetes Engine (GKE) or an existing Kubernetes cluster. |
-| [GitLab Runner](https://docs.gitlab.com/runner/) | Configure your own GitLab Runners to execute your scripts. |
| [Optimize GitLab and Runner for large repositories](large_repositories/index.md) | Recommended strategies for handling large repos. |
| [`.gitlab-ci.yml` full reference](yaml/README.md) | All the attributes you can use with GitLab CI/CD. |
diff --git a/doc/ci/caching/index.md b/doc/ci/caching/index.md
index 12267b4ab9f..16cabae353e 100644
--- a/doc/ci/caching/index.md
+++ b/doc/ci/caching/index.md
@@ -39,8 +39,9 @@ runtime dependencies needed to compile the project:
- `artifacts`: **Use for stage results that will be passed between stages.**
Artifacts are files generated by a job which are stored and uploaded, and can then
- be fetched and used by jobs in later stages of the **same pipeline**. This data
- will not be available in different pipelines, but is available to be downloaded
+ be fetched and used by jobs in later stages of the **same pipeline**. In other words,
+ [you can't create an artifact in job-A in stage-1, and then use this artifact in job-B in stage-1](https://gitlab.com/gitlab-org/gitlab/-/issues/25837).
+ This data will not be available in different pipelines, but is available to be downloaded
from the UI.
The name `artifacts` sounds like it's only useful outside of the job, like for downloading
diff --git a/doc/ci/ci_cd_for_external_repos/bitbucket_integration.md b/doc/ci/ci_cd_for_external_repos/bitbucket_integration.md
index 3437f867cb8..2836f9932c6 100644
--- a/doc/ci/ci_cd_for_external_repos/bitbucket_integration.md
+++ b/doc/ci/ci_cd_for_external_repos/bitbucket_integration.md
@@ -29,7 +29,7 @@ To use GitLab CI/CD with a Bitbucket Cloud repository:
The web hook URL should be set to the GitLab API to trigger pull mirroring,
using the Personal Access Token we just generated for authentication.
- ```text
+ ```plaintext
https://gitlab.com/api/v4/projects/<PROJECT_ID>/mirror/pull?private_token=<PERSONAL_ACCESS_TOKEN>
```
diff --git a/doc/ci/cloud_deployment/index.md b/doc/ci/cloud_deployment/index.md
index f70998a5f49..8883ad5dfd6 100644
--- a/doc/ci/cloud_deployment/index.md
+++ b/doc/ci/cloud_deployment/index.md
@@ -1,4 +1,7 @@
---
+stage: Release
+group: Progressive Delivery
+info: To determine the technical writer assigned to the Stage/Group associated with this page, see https://about.gitlab.com/handbook/engineering/ux/technical-writing/#designated-technical-writers
type: howto
---
@@ -36,7 +39,7 @@ Some credentials are required to be able to run `aws` commands:
1. You can now use `aws` commands in the `.gitlab-ci.yml` file of this project:
- ```yml
+ ```yaml
deploy:
stage: deploy
image: registry.gitlab.com/gitlab-org/cloud-deploy/aws-base:latest # see the note below
diff --git a/doc/ci/directed_acyclic_graph/index.md b/doc/ci/directed_acyclic_graph/index.md
index d4b87648f49..8722efd3b40 100644
--- a/doc/ci/directed_acyclic_graph/index.md
+++ b/doc/ci/directed_acyclic_graph/index.md
@@ -4,7 +4,8 @@ type: reference
# Directed Acyclic Graph
-> [Introduced](https://gitlab.com/gitlab-org/gitlab-foss/issues/47063) in GitLab 12.2.
+> - [Introduced](https://gitlab.com/gitlab-org/gitlab-foss/issues/47063) in GitLab 12.2.
+> - [Feature flag removed](https://gitlab.com/gitlab-org/gitlab/-/issues/206902) in GitLab 12.10.
A [directed acyclic graph](https://www.techopedia.com/definition/5739/directed-acyclic-graph-dag) can be
used in the context of a CI/CD pipeline to build relationships between jobs such that
diff --git a/doc/ci/docker/using_docker_build.md b/doc/ci/docker/using_docker_build.md
index 427f61deb29..f992af6c8a5 100644
--- a/doc/ci/docker/using_docker_build.md
+++ b/doc/ci/docker/using_docker_build.md
@@ -476,7 +476,7 @@ which can be avoided if a different driver is used, for example `overlay2`.
On Ubuntu systems, this is done by editing `/etc/modules`. Just add the
following line into it:
- ```text
+ ```plaintext
overlay
```
diff --git a/doc/ci/docker/using_docker_images.md b/doc/ci/docker/using_docker_images.md
index 90e5c77063e..51139da2d16 100644
--- a/doc/ci/docker/using_docker_images.md
+++ b/doc/ci/docker/using_docker_images.md
@@ -26,7 +26,20 @@ test them on a dedicated CI server.
To use GitLab Runner with Docker you need to [register a new Runner](https://docs.gitlab.com/runner/register/)
to use the `docker` executor.
-A one-line example can be seen below:
+An example can be seen below. First we set up a temporary template to supply the services:
+
+```shell
+cat > /tmp/test-config.template.toml << EOF
+[[runners]]
+[runners.docker]
+[[runners.docker.services]]
+name = "postgres:latest"
+[[runners.docker.services]]
+name = "mysql:latest"
+EOF
+```
+
+Then we register the runner using the template that was just created:
```shell
sudo gitlab-runner register \
@@ -34,9 +47,8 @@ sudo gitlab-runner register \
--registration-token "PROJECT_REGISTRATION_TOKEN" \
--description "docker-ruby:2.6" \
--executor "docker" \
- --docker-image ruby:2.6 \
- --docker-services postgres:latest \
- --docker-services mysql:latest
+ --template-config /tmp/test-config.template.toml \
+ --docker-image ruby:2.6
```
The registered runner will use the `ruby:2.6` Docker image and will run two
@@ -197,7 +209,7 @@ default:
image: ruby:2.6
services:
- - postgres:9.3
+ - postgres:11.7
before_script:
- bundle install
@@ -207,6 +219,12 @@ test:
- bundle exec rake spec
```
+The image name must be in one of the following formats:
+
+- `image: <image-name>` (Same as using `<image-name>` with the `latest` tag)
+- `image: <image-name>:<tag>`
+- `image: <image-name>@<digest>`
+
It is also possible to define different images and services per job:
```yaml
@@ -217,14 +235,14 @@ default:
test:2.6:
image: ruby:2.6
services:
- - postgres:9.3
+ - postgres:11.7
script:
- bundle exec rake spec
test:2.7:
image: ruby:2.7
services:
- - postgres:9.4
+ - postgres:12.2
script:
- bundle exec rake spec
```
@@ -239,7 +257,7 @@ default:
entrypoint: ["/bin/bash"]
services:
- - name: my-postgres:9.4
+ - name: my-postgres:11.7
alias: db-postgres
entrypoint: ["/usr/local/bin/db-postgres"]
command: ["start"]
@@ -271,7 +289,7 @@ variables:
POSTGRES_INITDB_ARGS: "--encoding=UTF8 --data-checksums"
services:
-- name: postgres:9.4
+- name: postgres:11.7
alias: db
entrypoint: ["docker-entrypoint.sh"]
command: ["postgres"]
diff --git a/doc/ci/docker/using_kaniko.md b/doc/ci/docker/using_kaniko.md
index 5a6f7490266..587f1f91f72 100644
--- a/doc/ci/docker/using_kaniko.md
+++ b/doc/ci/docker/using_kaniko.md
@@ -82,6 +82,7 @@ store:
```yaml
before_script:
+ - mkdir -p /kaniko/.docker
- echo "{\"auths\":{\"$CI_REGISTRY\":{\"username\":\"$CI_REGISTRY_USER\",\"password\":\"$CI_REGISTRY_PASSWORD\"}}}" > /kaniko/.docker/config.json
- |
echo "-----BEGIN CERTIFICATE-----
@@ -89,6 +90,18 @@ store:
-----END CERTIFICATE-----" >> /kaniko/ssl/certs/ca-certificates.crt
```
+## Video walkthrough of a working example
+
+The [Least Privilege Container Builds with Kaniko on GitLab](https://www.youtube.com/watch?v=d96ybcELpFs)
+video is a walkthrough of the [Kaniko Docker Build](https://gitlab.com/guided-explorations/containers/kaniko-docker-build)
+Guided Exploration project pipeline. It was tested on:
+
+- [GitLab.com Shared Runners](../../user/gitlab_com/index.md#shared-runners)
+- [The Kubernetes Runner executor](https://docs.gitlab.com/runner/executors/kubernetes.html)
+
+The example can be copied to your own group or instance for testing. More details
+on what other GitLab CI patterns are demonstrated are available at the project page.
+
<!-- ## Troubleshooting
Include any troubleshooting steps that you can foresee. If you know beforehand what issues
diff --git a/doc/ci/environments.md b/doc/ci/environments.md
index fdd2791aa1d..b1dc9af6b5b 100644
--- a/doc/ci/environments.md
+++ b/doc/ci/environments.md
@@ -1,984 +1,5 @@
---
-type: reference
+redirect_to: 'environments/index.md'
---
-# Environments and deployments
-
-> Introduced in GitLab 8.9.
-
-Environments allow control of the continuous deployment of your software,
-all within GitLab.
-
-## Introduction
-
-There are many stages required in the software development process before the software is ready
-for public consumption.
-
-For example:
-
-1. Develop your code.
-1. Test your code.
-1. Deploy your code into a testing or staging environment before you release it to the public.
-
-This helps find bugs in your software, and also in the deployment process as well.
-
-GitLab CI/CD is capable of not only testing or building your projects, but also
-deploying them in your infrastructure, with the added benefit of giving you a
-way to track your deployments. In other words, you will always know what is
-currently being deployed or has been deployed on your servers.
-
-It's important to know that:
-
-- Environments are like tags for your CI jobs, describing where code gets deployed.
-- Deployments are created when [jobs](yaml/README.md#introduction) deploy versions of code to environments,
- so every environment can have one or more deployments.
-
-GitLab:
-
-- Provides a full history of your deployments for each environment.
-- Keeps track of your deployments, so you always know what is currently being deployed on your
- servers.
-
-If you have a deployment service such as [Kubernetes](../user/project/clusters/index.md)
-associated with your project, you can use it to assist with your deployments, and
-can even access a [web terminal](#web-terminals) for your environment from within GitLab!
-
-## Configuring environments
-
-Configuring environments involves:
-
-1. Understanding how [pipelines](pipelines/index.md) work.
-1. Defining environments in your project's [`.gitlab-ci.yml`](yaml/README.md) file.
-1. Creating a job configured to deploy your application. For example, a deploy job configured with [`environment`](yaml/README.md#environment) to deploy your application to a [Kubernetes cluster](../user/project/clusters/index.md).
-
-The rest of this section illustrates how to configure environments and deployments using
-an example scenario. It assumes you have already:
-
-- Created a [project](../gitlab-basics/create-project.md) in GitLab.
-- Set up [a Runner](runners/README.md).
-
-In the scenario:
-
-- We are developing an application.
-- We want to run tests and build our app on all branches.
-- Our default branch is `master`.
-- We deploy the app only when a pipeline on `master` branch is run.
-
-### Defining environments
-
-Let's consider the following `.gitlab-ci.yml` example:
-
-```yaml
-stages:
- - test
- - build
- - deploy
-
-test:
- stage: test
- script: echo "Running tests"
-
-build:
- stage: build
- script: echo "Building the app"
-
-deploy_staging:
- stage: deploy
- script:
- - echo "Deploy to staging server"
- environment:
- name: staging
- url: https://staging.example.com
- only:
- - master
-```
-
-We have defined three [stages](yaml/README.md#stages):
-
-- `test`
-- `build`
-- `deploy`
-
-The jobs assigned to these stages will run in this order. If any job fails, then
-the pipeline fails and jobs that are assigned to the next stage won't run.
-
-In our case:
-
-- The `test` job will run first.
-- Then the `build` job.
-- Lastly the `deploy_staging` job.
-
-With this configuration, we:
-
-- Check that the tests pass.
-- Ensure that our app is able to be built successfully.
-- Lastly we deploy to the staging server.
-
-NOTE: **Note:**
-The `environment` keyword defines where the app is deployed.
-The environment `name` and `url` is exposed in various places
-within GitLab. Each time a job that has an environment specified
-succeeds, a deployment is recorded, along with the Git SHA, and environment name.
-
-CAUTION: **Caution**:
-Some characters are not allowed in environment names. Use only letters,
-numbers, spaces, and `-`, `_`, `/`, `{`, `}`, or `.`. Also, it must not start nor end with `/`.
-
-In summary, with the above `.gitlab-ci.yml` we have achieved the following:
-
-- All branches will run the `test` and `build` jobs.
-- The `deploy_staging` job will run [only](yaml/README.md#onlyexcept-basic) on the `master`
- branch, which means all merge requests that are created from branches don't
- get deployed to the staging server.
-- When a merge request is merged, all jobs will run and the `deploy_staging`
- job will deploy our code to a staging server while the deployment
- will be recorded in an environment named `staging`.
-
-#### Environment variables and Runner
-
-Starting with GitLab 8.15, the environment name is exposed to the Runner in
-two forms:
-
-- `$CI_ENVIRONMENT_NAME`. The name given in `.gitlab-ci.yml` (with any variables
- expanded).
-- `$CI_ENVIRONMENT_SLUG`. A "cleaned-up" version of the name, suitable for use in URLs,
- DNS, etc.
-
-If you change the name of an existing environment, the:
-
-- `$CI_ENVIRONMENT_NAME` variable will be updated with the new environment name.
-- `$CI_ENVIRONMENT_SLUG` variable will remain unchanged to prevent unintended side
- effects.
-
-Starting with GitLab 9.3, the environment URL is exposed to the Runner via
-`$CI_ENVIRONMENT_URL`. The URL is expanded from either:
-
-- `.gitlab-ci.yml`.
-- The external URL from the environment if not defined in `.gitlab-ci.yml`.
-
-#### Set dynamic environment URLs after a job finishes
-
-> [Introduced](https://gitlab.com/gitlab-org/gitlab/issues/17066) in GitLab 12.9.
-
-In a job script, you can specify a static [environment URL](#using-the-environment-url).
-However, there may be times when you want a dynamic URL. For example,
-if you deploy a Review App to an external hosting
-service that generates a random URL per deployment, like `https://94dd65b.amazonaws.com/qa-lambda-1234567`,
-you don't know the URL before the deployment script finishes.
-If you want to use the environment URL in GitLab, you would have to update it manually.
-
-To address this problem, you can configure a deployment job to report back a set of
-variables, including the URL that was dynamically-generated by the external service.
-GitLab supports [dotenv](https://github.com/bkeepers/dotenv) file as the format,
-and expands the `environment:url` value with variables defined in the dotenv file.
-
-To use this feature, specify the
-[`artifacts:reports:dotenv`](yaml/README.md#artifactsreportsdotenv) keyword in `.gitlab-ci.yml`.
-
-##### Example of setting dynamic environment URLs
-
-The following example shows a Review App that creates a new environment
-per merge request. The `review` job is triggered by every push, and
-creates or updates an environment named `review/your-branch-name`.
-The environment URL is set to `$DYNAMIC_ENVIRONMENT_URL`:
-
-```yaml
-review:
- script:
- - DYNAMIC_ENVIRONMENT_URL=$(deploy-script) # In script, get the environment URL.
- - echo "DYNAMIC_ENVIRONMENT_URL=$DYNAMIC_ENVIRONMENT_URL" >> deploy.env # Add the value to a dotenv file.
- artifacts:
- reports:
- dotenv: deploy.env # Report back dotenv file to rails.
- environment:
- name: review/$CI_COMMIT_REF_SLUG
- url: $DYNAMIC_ENVIRONMENT_URL # and set the variable produced in script to `environment:url`
- on_stop: stop_review
-
-stop_review:
- script:
- - ./teardown-environment
- when: manual
- environment:
- name: review/$CI_COMMIT_REF_SLUG
- action: stop
-```
-
-As soon as the `review` job finishes, GitLab updates the `review/your-branch-name`
-environment's URL.
-It parses the report artifact `deploy.env`, registers a list of variables as runtime-created,
-uses it for expanding `environment:url: $DYNAMIC_ENVIRONMENT_URL` and sets it to the environment URL.
-You can also specify a static part of the URL at `environment:url:`, such as
-`https://$DYNAMIC_ENVIRONMENT_URL`. If the value of `DYNAMIC_ENVIRONMENT_URL` is
-`123.awesome.com`, the final result will be `https://123.awesome.com`.
-
-The assigned URL for the `review/your-branch-name` environment is visible in the UI.
-[See where the environment URL is displayed](#using-the-environment-url).
-
-> **Notes:**
->
-> - `stop_review` doesn't generate a dotenv report artifact, so it won't recognize the `DYNAMIC_ENVIRONMENT_URL` variable. Therefore you should not set `environment:url:` in the `stop_review` job.
-> - If the environment URL is not valid (for example, the URL is malformed), the system doesn't update the environment URL.
-
-### Configuring manual deployments
-
-Adding `when: manual` to an automatically executed job's configuration converts it to
-a job requiring manual action.
-
-To expand on the [previous example](#defining-environments), the following includes
-another job that deploys our app to a production server and is
-tracked by a `production` environment.
-
-The `.gitlab-ci.yml` file for this is as follows:
-
-```yaml
-stages:
- - test
- - build
- - deploy
-
-test:
- stage: test
- script: echo "Running tests"
-
-build:
- stage: build
- script: echo "Building the app"
-
-deploy_staging:
- stage: deploy
- script:
- - echo "Deploy to staging server"
- environment:
- name: staging
- url: https://staging.example.com
- only:
- - master
-
-deploy_prod:
- stage: deploy
- script:
- - echo "Deploy to production server"
- environment:
- name: production
- url: https://example.com
- when: manual
- only:
- - master
-```
-
-The `when: manual` action:
-
-- Exposes a "play" button in GitLab's UI for that job.
-- Means the `deploy_prod` job will only be triggered when the "play" button is clicked.
-
-You can find the "play" button in the pipelines, environments, deployments, and jobs views.
-
-| View | Screenshot |
-|:----------------|:-------------------------------------------------------------------------------|
-| Pipelines | ![Pipelines manual action](img/environments_manual_action_pipelines.png) |
-| Single pipeline | ![Pipelines manual action](img/environments_manual_action_single_pipeline.png) |
-| Environments | ![Environments manual action](img/environments_manual_action_environments.png) |
-| Deployments | ![Deployments manual action](img/environments_manual_action_deployments.png) |
-| Jobs | ![Builds manual action](img/environments_manual_action_jobs.png) |
-
-Clicking on the play button in any view will trigger the `deploy_prod` job, and the
-deployment will be recorded as a new environment named `production`.
-
-NOTE: **Note:**
-If your environment's name is `production` (all lowercase),
-it will get recorded in [Value Stream Analytics](../user/project/cycle_analytics.md).
-
-### Configuring dynamic environments
-
-Regular environments are good when deploying to "stable" environments like staging or production.
-
-However, for environments for branches other than `master`, dynamic environments
-can be used. Dynamic environments make it possible to create environments on the fly by
-declaring their names dynamically in `.gitlab-ci.yml`.
-
-Dynamic environments are a fundamental part of [Review apps](review_apps/index.md).
-
-### Configuring incremental rollouts
-
-Learn how to release production changes to only a portion of your Kubernetes pods with
-[incremental rollouts](environments/incremental_rollouts.md).
-
-#### Allowed variables
-
-The `name` and `url` parameters for dynamic environments can use most available CI/CD variables,
-including:
-
-- [Predefined environment variables](variables/README.md#predefined-environment-variables)
-- [Project and group variables](variables/README.md#gitlab-cicd-environment-variables)
-- [`.gitlab-ci.yml` variables](yaml/README.md#variables)
-
-However, you cannot use variables defined:
-
-- Under `script`.
-- On the Runner's side.
-
-There are also other variables that are unsupported in the context of `environment:name`.
-For more information, see [Where variables can be used](variables/where_variables_can_be_used.md).
-
-#### Example configuration
-
-GitLab Runner exposes various [environment variables](variables/README.md) when a job runs, so
-you can use them as environment names.
-
-In the following example, the job will deploy to all branches except `master`:
-
-```yaml
-deploy_review:
- stage: deploy
- script:
- - echo "Deploy a review app"
- environment:
- name: review/$CI_COMMIT_REF_NAME
- url: https://$CI_ENVIRONMENT_SLUG.example.com
- only:
- - branches
- except:
- - master
-```
-
-In this example:
-
-- The job's name is `deploy_review` and it runs on the `deploy` stage.
-- We set the `environment` with the `environment:name` as `review/$CI_COMMIT_REF_NAME`.
- Since the [environment name](yaml/README.md#environmentname) can contain slashes (`/`), we can
- use this pattern to distinguish between dynamic and regular environments.
-- We tell the job to run [`only`](yaml/README.md#onlyexcept-basic) on branches,
- [`except`](yaml/README.md#onlyexcept-basic) `master`.
-
-For the value of:
-
-- `environment:name`, the first part is `review`, followed by a `/` and then `$CI_COMMIT_REF_NAME`,
- which receives the value of the branch name.
-- `environment:url`, we want a specific and distinct URL for each branch. `$CI_COMMIT_REF_NAME`
- may contain a `/` or other characters that would be invalid in a domain name or URL,
- so we use `$CI_ENVIRONMENT_SLUG` to guarantee that we get a valid URL.
-
- For example, given a `$CI_COMMIT_REF_NAME` of `100-Do-The-Thing`, the URL will be something
- like `https://100-do-the-4f99a2.example.com`. Again, the way you set up
- the web server to serve these requests is based on your setup.
-
- We have used `$CI_ENVIRONMENT_SLUG` here because it is guaranteed to be unique. If
- you're using a workflow like [GitLab Flow](../topics/gitlab_flow.md), collisions
- are unlikely and you may prefer environment names to be more closely based on the
- branch name. In that case, you could use `$CI_COMMIT_REF_NAME` in `environment:url` in
- the example above: `https://$CI_COMMIT_REF_NAME.example.com`, which would give a URL
- of `https://100-do-the-thing.example.com`.
-
-NOTE: **Note:**
-You are not required to use the same prefix or only slashes (`/`) in the dynamic environments'
-names. However, using this format will enable the [grouping similar environments](#grouping-similar-environments)
-feature.
-
-### Configuring Kubernetes deployments
-
-> [Introduced](https://gitlab.com/gitlab-org/gitlab/issues/27630) in GitLab 12.6.
-
-If you are deploying to a [Kubernetes cluster](../user/project/clusters/index.md)
-associated with your project, you can configure these deployments from your
-`gitlab-ci.yml` file.
-
-The following configuration options are supported:
-
-- [`namespace`](https://kubernetes.io/docs/concepts/overview/working-with-objects/namespaces/)
-
-In the following example, the job will deploy your application to the
-`production` Kubernetes namespace.
-
-```yaml
-deploy:
- stage: deploy
- script:
- - echo "Deploy to production server"
- environment:
- name: production
- url: https://example.com
- kubernetes:
- namespace: production
- only:
- - master
-```
-
-When deploying to a Kubernetes cluster using GitLab's Kubernetes integration,
-information about the cluster and namespace will be displayed above the job
-trace on the deployment job page:
-
-![Deployment cluster information](img/environments_deployment_cluster_v12_8.png)
-
-NOTE: **Note:**
-Kubernetes configuration is not supported for Kubernetes clusters
-that are [managed by GitLab](../user/project/clusters/index.md#gitlab-managed-clusters).
-To follow progress on support for GitLab-managed clusters, see the
-[relevant issue](https://gitlab.com/gitlab-org/gitlab/issues/38054).
-
-### Complete example
-
-The configuration in this section provides a full development workflow where your app is:
-
-- Tested.
-- Built.
-- Deployed as a Review App.
-- Deployed to a staging server once the merge request is merged.
-- Finally, able to be manually deployed to the production server.
-
-The following combines the previous configuration examples, including:
-
-- Defining [simple environments](#defining-environments) for testing, building, and deployment to staging.
-- Adding [manual actions](#configuring-manual-deployments) for deployment to production.
-- Creating [dynamic environments](#configuring-dynamic-environments) for deployments for reviewing.
-
-```yaml
-stages:
- - test
- - build
- - deploy
-
-test:
- stage: test
- script: echo "Running tests"
-
-build:
- stage: build
- script: echo "Building the app"
-
-deploy_review:
- stage: deploy
- script:
- - echo "Deploy a review app"
- environment:
- name: review/$CI_COMMIT_REF_NAME
- url: https://$CI_ENVIRONMENT_SLUG.example.com
- only:
- - branches
- except:
- - master
-
-deploy_staging:
- stage: deploy
- script:
- - echo "Deploy to staging server"
- environment:
- name: staging
- url: https://staging.example.com
- only:
- - master
-
-deploy_prod:
- stage: deploy
- script:
- - echo "Deploy to production server"
- environment:
- name: production
- url: https://example.com
- when: manual
- only:
- - master
-```
-
-A more realistic example would also include copying files to a location where a
-webserver (for example, NGINX) could then access and serve them.
-
-The example below will copy the `public` directory to `/srv/nginx/$CI_COMMIT_REF_SLUG/public`:
-
-```yaml
-review_app:
- stage: deploy
- script:
- - rsync -av --delete public /srv/nginx/$CI_COMMIT_REF_SLUG
- environment:
- name: review/$CI_COMMIT_REF_NAME
- url: https://$CI_COMMIT_REF_SLUG.example.com
-```
-
-This example requires that NGINX and GitLab Runner are set up on the server this job will run on.
-
-NOTE: **Note:**
-See the [limitations](#limitations) section for some edge cases regarding the naming of
-your branches and Review Apps.
-
-The complete example provides the following workflow to developers:
-
-- Create a branch locally.
-- Make changes and commit them.
-- Push the branch to GitLab.
-- Create a merge request.
-
-Behind the scenes, GitLab Runner will:
-
-- Pick up the changes and start running the jobs.
-- Run the jobs sequentially as defined in `stages`:
- - First, run the tests.
- - If the tests succeed, build the app.
- - If the build succeeds, the app is deployed to an environment with a name specific to the
- branch.
-
-So now, every branch:
-
-- Gets its own environment.
-- Is deployed to its own unique location, with the added benefit of:
- - Having a [history of deployments](#viewing-deployment-history).
- - Being able to [rollback changes](#retrying-and-rolling-back) if needed.
-
-For more information, see [Using the environment URL](#using-the-environment-url).
-
-### Protected environments
-
-Environments can be "protected", restricting access to them.
-
-For more information, see [Protected environments](environments/protected_environments.md).
-
-## Working with environments
-
-Once environments are configured, GitLab provides many features for working with them,
-as documented below.
-
-### Viewing environments and deployments
-
-A list of environments and deployment statuses is available on each project's **Operations > Environments** page.
-
-For example:
-
-![Environment view](img/environments_available.png)
-
-This example shows:
-
-- The environment's name with a link to its deployments.
-- The last deployment ID number and who performed it.
-- The job ID of the last deployment with its respective job name.
-- The commit information of the last deployment, such as who committed it, to what
- branch, and the Git SHA of the commit.
-- The exact time the last deployment was performed.
-- A button that takes you to the URL that you defined under the `environment` keyword
- in `.gitlab-ci.yml`.
-- A button that re-deploys the latest deployment, meaning it runs the job
- defined by the environment name for that specific commit.
-
-The information shown in the **Environments** page is limited to the latest
-deployments, but an environment can have multiple deployments.
-
-> **Notes:**
->
-> - While you can create environments manually in the web interface, we recommend
-> that you define your environments in `.gitlab-ci.yml` first. They will
-> be automatically created for you after the first deploy.
-> - The environments page can only be viewed by users with [Reporter permission](../user/permissions.md#project-members-permissions)
-> and above. For more information on permissions, see the [permissions documentation](../user/permissions.md).
-> - Only deploys that happen after your `.gitlab-ci.yml` is properly configured
-> will show up in the **Environment** and **Last deployment** lists.
-
-### Viewing deployment history
-
-GitLab keeps track of your deployments, so you:
-
-- Always know what is currently being deployed on your servers.
-- Can have the full history of your deployments for every environment.
-
-Clicking on an environment shows the history of its deployments. Here's an example **Environments** page
-with multiple deployments:
-
-![Deployments](img/deployments_view.png)
-
-This view is similar to the **Environments** page, but all deployments are shown. Also in this view
-is a **Rollback** button. For more information, see [Retrying and rolling back](#retrying-and-rolling-back).
-
-### Retrying and rolling back
-
-If there is a problem with a deployment, you can retry it or roll it back.
-
-To retry or rollback a deployment:
-
-1. Navigate to **Operations > Environments**.
-1. Click on the environment.
-1. In the deployment history list for the environment, click the:
- - **Retry** button next to the last deployment, to retry that deployment.
- - **Rollback** button next to a previously successful deployment, to roll back to that deployment.
-
-#### What to expect with a rollback
-
-Pressing the **Rollback** button on a specific commit will trigger a _new_ deployment with its
-own unique job ID.
-
-This means that you will see a new deployment that points to the commit you are rolling back to.
-
-NOTE: **Note:**
-The defined deployment process in the job's `script` determines whether the rollback succeeds or not.
-
-### Using the environment URL
-
-The [environment URL](yaml/README.md#environmenturl) is exposed in a few
-places within GitLab:
-
-- In a merge request widget as a link:
- ![Environment URL in merge request](img/environments_mr_review_app.png)
-- In the Environments view as a button:
- ![Environment URL in environments](img/environments_available.png)
-- In the Deployments view as a button:
- ![Environment URL in deployments](img/deployments_view.png)
-
-You can see this information in a merge request itself if:
-
-- The merge request is eventually merged to the default branch (usually `master`).
-- That branch also deploys to an environment (for example, `staging` or `production`).
-
-For example:
-
-![Environment URLs in merge request](img/environments_link_url_mr.png)
-
-#### Going from source files to public pages
-
-With GitLab's [Route Maps](review_apps/index.md#route-maps) you can go directly
-from source files to public pages in the environment set for Review Apps.
-
-### Stopping an environment
-
-Stopping an environment:
-
-- Moves it from the list of **Available** environments to the list of **Stopped**
- environments on the [**Environments** page](#viewing-environments-and-deployments).
-- Executes an [`on_stop` action](yaml/README.md#environmenton_stop), if defined.
-
-This is often used when multiple developers are working on a project at the same time,
-each of them pushing to their own branches, causing many dynamic environments to be created.
-
-NOTE: **Note:**
-Starting with GitLab 8.14, dynamic environments are stopped automatically
-when their associated branch is deleted.
-
-#### Automatically stopping an environment
-
-Environments can be stopped automatically using special configuration.
-
-Consider the following example where the `deploy_review` job calls `stop_review`
-to clean up and stop the environment:
-
-```yaml
-deploy_review:
- stage: deploy
- script:
- - echo "Deploy a review app"
- environment:
- name: review/$CI_COMMIT_REF_NAME
- url: https://$CI_ENVIRONMENT_SLUG.example.com
- on_stop: stop_review
- only:
- - branches
- except:
- - master
-
-stop_review:
- stage: deploy
- variables:
- GIT_STRATEGY: none
- script:
- - echo "Remove review app"
- when: manual
- environment:
- name: review/$CI_COMMIT_REF_NAME
- action: stop
-```
-
-Setting the [`GIT_STRATEGY`](yaml/README.md#git-strategy) to `none` is necessary in the
-`stop_review` job so that the [GitLab Runner](https://docs.gitlab.com/runner/) won't
-try to check out the code after the branch is deleted.
-
-When you have an environment that has a stop action defined (typically when
-the environment describes a Review App), GitLab will automatically trigger a
-stop action when the associated branch is deleted. The `stop_review` job must
-be in the same `stage` as the `deploy_review` job in order for the environment
-to automatically stop.
-
-You can read more in the [`.gitlab-ci.yml` reference](yaml/README.md#environmenton_stop).
-
-#### Environments auto-stop
-
-> [Introduced](https://gitlab.com/gitlab-org/gitlab/issues/20956) in GitLab 12.8.
-
-You can set a expiry time to environments and stop them automatically after a certain period.
-
-For example, consider the use of this feature with Review Apps environments.
-When you set up Review Apps, sometimes they keep running for a long time
-because some merge requests are left as open. An example for this situation is when the author of the merge
-request is not actively working on it, due to priority changes or a different approach was decided on, and the merge requests was simply forgotten.
-Idle environments waste resources, therefore they
-should be terminated as soon as possible.
-
-To address this problem, you can specify an optional expiration date for
-Review Apps environments. When the expiry time is reached, GitLab will automatically trigger a job
-to stop the environment, eliminating the need of manually doing so. In case an environment is updated, the expiration is renewed
-ensuring that only active merge requests keep running Review Apps.
-
-To enable this feature, you need to specify the [`environment:auto_stop_in`](yaml/README.md#environmentauto_stop_in) keyword in `.gitlab-ci.yml`.
-You can specify a human-friendly date as the value, such as `1 hour and 30 minutes` or `1 day`.
-`auto_stop_in` uses the same format of [`artifacts:expire_in` docs](yaml/README.md#artifactsexpire_in).
-
-##### Auto-stop example
-
-In the following example, there is a basic review app setup that creates a new environment
-per merge request. The `review_app` job is triggered by every push and
-creates or updates an environment named `review/your-branch-name`.
-The environment keeps running until `stop_review_app` is executed:
-
-```yaml
-review_app:
- script: deploy-review-app
- environment:
- name: review/$CI_COMMIT_REF_NAME
- on_stop: stop_review_app
- auto_stop_in: 1 week
-
-stop_review_app:
- script: stop-review-app
- environment:
- name: review/$CI_COMMIT_REF_NAME
- action: stop
- when: manual
-```
-
-As long as a merge request is active and keeps getting new commits,
-the review app will not stop, so developers don't need to worry about
-re-initiating review app.
-
-On the other hand, since `stop_review_app` is set to `auto_stop_in: 1 week`,
-if a merge request becomes inactive for more than a week,
-GitLab automatically triggers the `stop_review_app` job to stop the environment.
-
-You can also check the expiration date of environments through the GitLab UI. To do so,
-go to **Operations > Environments > Environment**. You can see the auto-stop period
-at the left-top section and a pin-mark button at the right-top section. This pin-mark
-button can be used to prevent auto-stopping the environment. By clicking this button, the `auto_stop_in` setting is over-written
-and the environment will be active until it's stopped manually.
-
-![Environment auto stop](img/environment_auto_stop_v12_8.png)
-
-NOTE: **NOTE**
-Due to the resource limitation, a background worker for stopping environments only
-runs once every hour. This means environments will not be stopped at the exact
-timestamp as the specified period, but will be stopped when the hourly cron worker
-detects expired environments.
-
-#### Delete a stopped environment
-
-> [Introduced](https://gitlab.com/gitlab-org/gitlab/-/merge_requests/22629) in GitLab 12.9.
-
-You can delete [stopped environments](#stopping-an-environment) in one of two
-ways: through the GitLab UI or through the API.
-
-##### Delete environments through the UI
-
-To view the list of **Stopped** environments, navigate to **Operations > Environments**
-and click the **Stopped** tab.
-
-From there, you can click the **Delete** button directly, or you can click the
-environment name to see its details and **Delete** it from there.
-
-You can also delete environments by viewing the details for a
-stopped environment:
-
- 1. Navigate to **Operations > Environments**.
- 1. Click on the name of an environment within the **Stopped** environments list.
- 1. Click on the **Delete** button that appears at the top for all stopped environments.
- 1. Finally, confirm your chosen environment in the modal that appears to delete it.
-
-##### Delete environments through the API
-
-Environments can also be deleted by using the [Environments API](../api/environments.md#delete-an-environment).
-
-### Grouping similar environments
-
-> [Introduced](https://gitlab.com/gitlab-org/gitlab-foss/-/merge_requests/7015) in GitLab 8.14.
-
-As documented in [Configuring dynamic environments](#configuring-dynamic-environments), you can
-prepend environment name with a word, followed by a `/`, and finally the branch
-name, which is automatically defined by the `CI_COMMIT_REF_NAME` variable.
-
-In short, environments that are named like `type/foo` are all presented under the same
-group, named `type`.
-
-In our [minimal example](#example-configuration), we named the environments `review/$CI_COMMIT_REF_NAME`
-where `$CI_COMMIT_REF_NAME` is the branch name. Here is a snippet of the example:
-
-```yaml
-deploy_review:
- stage: deploy
- script:
- - echo "Deploy a review app"
- environment:
- name: review/$CI_COMMIT_REF_NAME
-```
-
-In this case, if you visit the **Environments** page and the branches
-exist, you should see something like:
-
-![Environment groups](img/environments_dynamic_groups.png)
-
-### Monitoring environments
-
-If you have enabled [Prometheus for monitoring system and response metrics](../user/project/integrations/prometheus.md),
-you can monitor the behavior of your app running in each environment. For the monitoring
-dashboard to appear, you need to Configure Prometheus to collect at least one
-[supported metric](../user/project/integrations/prometheus_library/index.md).
-
-NOTE: **Note:**
-Since GitLab 9.2, all deployments to an environment are shown directly on the monitoring dashboard.
-
-Once configured, GitLab will attempt to retrieve [supported performance metrics](../user/project/integrations/prometheus_library/index.md)
-for any environment that has had a successful deployment. If monitoring data was
-successfully retrieved, a **Monitoring** button will appear for each environment.
-
-![Environment Detail with Metrics](img/deployments_view.png)
-
-Clicking on the **Monitoring** button will display a new page showing up to the last
-8 hours of performance data. It may take a minute or two for data to appear
-after initial deployment.
-
-All deployments to an environment are shown directly on the monitoring dashboard,
-which allows easy correlation between any changes in performance and new
-versions of the app, all without leaving GitLab.
-
-![Monitoring dashboard](img/environments_monitoring.png)
-
-#### Linking to external dashboard
-
-Add a [button to the Monitoring dashboard](../user/project/operations/linking_to_an_external_dashboard.md) linking directly to your existing external dashboards.
-
-#### Embedding metrics in GitLab Flavored Markdown
-
-Metric charts can be embedded within GitLab Flavored Markdown. See [Embedding Metrics within GitLab Flavored Markdown](../user/project/integrations/prometheus.md#embedding-metric-charts-within-gitlab-flavored-markdown) for more details.
-
-### Web terminals
-
-> Web terminals were added in GitLab 8.15 and are only available to project Maintainers and Owners.
-
-If you deploy to your environments with the help of a deployment service (for example,
-the [Kubernetes integration](../user/project/clusters/index.md)), GitLab can open
-a terminal session to your environment.
-
-This is a powerful feature that allows you to debug issues without leaving the comfort
-of your web browser. To enable it, just follow the instructions given in the service integration
-documentation.
-
-Once enabled, your environments will gain a "terminal" button:
-
-![Terminal button on environment index](img/environments_terminal_button_on_index.png)
-
-You can also access the terminal button from the page for a specific environment:
-
-![Terminal button for an environment](img/environments_terminal_button_on_show.png)
-
-Wherever you find it, clicking the button will take you to a separate page to
-establish the terminal session:
-
-![Terminal page](img/environments_terminal_page.png)
-
-This works just like any other terminal. You'll be in the container created
-by your deployment so you can:
-
-- Run shell commands and get responses in real time.
-- Check the logs.
-- Try out configuration or code tweaks etc.
-
-You can open multiple terminals to the same environment, they each get their own shell
-session and even a multiplexer like `screen` or `tmux`.
-
-NOTE: **Note:**
-Container-based deployments often lack basic tools (like an editor), and may
-be stopped or restarted at any time. If this happens, you will lose all your
-changes. Treat this as a debugging tool, not a comprehensive online IDE.
-
-### Check out deployments locally
-
-Since GitLab 8.13, a reference in the Git repository is saved for each deployment, so
-knowing the state of your current environments is only a `git fetch` away.
-
-In your Git configuration, append the `[remote "<your-remote>"]` block with an extra
-fetch line:
-
-```text
-fetch = +refs/environments/*:refs/remotes/origin/environments/*
-```
-
-### Scoping environments with specs
-
-> - [Introduced](https://gitlab.com/gitlab-org/gitlab/-/merge_requests/2112) in [GitLab Premium](https://about.gitlab.com/pricing/) 9.4.
-> - [Scoping for environment variables was moved to Core](https://gitlab.com/gitlab-org/gitlab-foss/-/merge_requests/30779) to Core in GitLab 12.2.
-
-You can limit the environment scope of a variable by
-defining which environments it can be available for.
-
-Wildcards can be used, and the default environment scope is `*`, which means
-any jobs will have this variable, not matter if an environment is defined or
-not.
-
-For example, if the environment scope is `production`, then only the jobs
-having the environment `production` defined would have this specific variable.
-Wildcards (`*`) can be used along with the environment name, therefore if the
-environment scope is `review/*` then any jobs with environment names starting
-with `review/` would have that particular variable.
-
-Some GitLab features can behave differently for each environment.
-For example, you can
-[create a secret variable to be injected only into a production environment](variables/README.md#limiting-environment-scopes-of-environment-variables).
-
-In most cases, these features use the _environment specs_ mechanism, which offers
-an efficient way to implement scoping within each environment group.
-
-Let's say there are four environments:
-
-- `production`
-- `staging`
-- `review/feature-1`
-- `review/feature-2`
-
-Each environment can be matched with the following environment spec:
-
-| Environment Spec | `production` | `staging` | `review/feature-1` | `review/feature-2` |
-|:-----------------|:-------------|:----------|:-------------------|:-------------------|
-| * | Matched | Matched | Matched | Matched |
-| production | Matched | | | |
-| staging | | Matched | | |
-| review/* | | | Matched | Matched |
-| review/feature-1 | | | Matched | |
-
-As you can see, you can use specific matching for selecting a particular environment,
-and also use wildcard matching (`*`) for selecting a particular environment group,
-such as [Review Apps](review_apps/index.md) (`review/*`).
-
-NOTE: **Note:**
-The most _specific_ spec takes precedence over the other wildcard matching.
-In this case, `review/feature-1` spec takes precedence over `review/*` and `*` specs.
-
-### Environments Dashboard **(PREMIUM)**
-
-See [Environments Dashboard](environments/environments_dashboard.md) for a summary of each
-environment's operational health.
-
-## Limitations
-
-In the `environment: name`, you are limited to only the [predefined environment variables](variables/predefined_variables.md).
-Re-using variables defined inside `script` as part of the environment name will not work.
-
-## Further reading
-
-Below are some links you may find interesting:
-
-- [The `.gitlab-ci.yml` definition of environments](yaml/README.md#environment)
-- [A blog post on Deployments & Environments](https://about.gitlab.com/blog/2016/08/26/ci-deployment-and-environments/)
-- [Review Apps - Use dynamic environments to deploy your code for every branch](review_apps/index.md)
-- [Deploy Boards for your applications running on Kubernetes](../user/project/deploy_boards.md) **(PREMIUM)**
-
-<!-- ## Troubleshooting
-
-Include any troubleshooting steps that you can foresee. If you know beforehand what issues
-one might have when setting this up, or when something is changed, or on upgrading, it's
-important to describe those, too. Think of things that may go wrong and include them here.
-This is important to minimize requests for support, and to avoid doc comments with
-questions that you know someone might ask.
-
-Each scenario can be a third-level heading, e.g. `### Getting error message X`.
-If you have none to add when creating a doc, leave this section in place
-but commented out to help encourage others to add to it in the future. -->
+This document was moved to [another location](environments/index.md).
diff --git a/doc/ci/environments/environments_dashboard.md b/doc/ci/environments/environments_dashboard.md
index f82728cd587..4a72c0d6d62 100644
--- a/doc/ci/environments/environments_dashboard.md
+++ b/doc/ci/environments/environments_dashboard.md
@@ -1,4 +1,7 @@
---
+stage: Release
+group: Release Management
+info: To determine the technical writer assigned to the Stage/Group associated with this page, see https://about.gitlab.com/handbook/engineering/ux/technical-writing/#designated-technical-writers
type: reference
---
diff --git a/doc/ci/environments/incremental_rollouts.md b/doc/ci/environments/incremental_rollouts.md
index 0fa0af6a9fb..016a6ac7cad 100644
--- a/doc/ci/environments/incremental_rollouts.md
+++ b/doc/ci/environments/incremental_rollouts.md
@@ -46,7 +46,7 @@ application will be deployed to a single pod while the remaining 9 will present
First we [define the template as manual](https://gitlab.com/gl-release/incremental-rollout-example/blob/master/.gitlab-ci.yml#L100-103):
-```yml
+```yaml
.manual_rollout_template: &manual_rollout_template
<<: *rollout_template
stage: production
@@ -55,7 +55,7 @@ First we [define the template as manual](https://gitlab.com/gl-release/increment
Then we [define the rollout amount for each step](https://gitlab.com/gl-release/incremental-rollout-example/blob/master/.gitlab-ci.yml#L152-155):
-```yml
+```yaml
rollout 10%:
<<: *manual_rollout_template
variables:
@@ -86,7 +86,7 @@ countdown and then deploy.
First we [define the template as timed](https://gitlab.com/gl-release/timed-rollout-example/blob/master/.gitlab-ci.yml#L86-89):
-```yml
+```yaml
.timed_rollout_template: &timed_rollout_template
<<: *rollout_template
when: delayed
@@ -95,13 +95,13 @@ First we [define the template as timed](https://gitlab.com/gl-release/timed-roll
We can define the delay period using the `start_in` key:
-```yml
+```yaml
start_in: 1 minutes
```
Then we [define the rollout amount for each step](https://gitlab.com/gl-release/timed-rollout-example/blob/master/.gitlab-ci.yml#L97-101):
-```yml
+```yaml
timed rollout 30%:
<<: *timed_rollout_template
stage: timed rollout 30%
diff --git a/doc/ci/environments/index.md b/doc/ci/environments/index.md
new file mode 100644
index 00000000000..b6ec30b5571
--- /dev/null
+++ b/doc/ci/environments/index.md
@@ -0,0 +1,991 @@
+---
+stage: Release
+group: Release Management
+info: To determine the technical writer assigned to the Stage/Group associated with this page, see https://about.gitlab.com/handbook/engineering/ux/technical-writing/#designated-technical-writers
+type: reference
+disqus_identifier: 'https://docs.gitlab.com/ee/ci/environments.html'
+---
+
+# Environments and deployments
+
+> Introduced in GitLab 8.9.
+
+Environments allow control of the continuous deployment of your software,
+all within GitLab.
+
+## Introduction
+
+There are many stages required in the software development process before the software is ready
+for public consumption.
+
+For example:
+
+1. Develop your code.
+1. Test your code.
+1. Deploy your code into a testing or staging environment before you release it to the public.
+
+This helps find bugs in your software, and also in the deployment process as well.
+
+GitLab CI/CD is capable of not only testing or building your projects, but also
+deploying them in your infrastructure, with the added benefit of giving you a
+way to track your deployments. In other words, you will always know what is
+currently being deployed or has been deployed on your servers.
+
+It's important to know that:
+
+- Environments are like tags for your CI jobs, describing where code gets deployed.
+- Deployments are created when [jobs](../yaml/README.md#introduction) deploy versions of code to environments,
+ so every environment can have one or more deployments.
+
+GitLab:
+
+- Provides a full history of your deployments for each environment.
+- Keeps track of your deployments, so you always know what is currently being deployed on your
+ servers.
+
+If you have a deployment service such as [Kubernetes](../../user/project/clusters/index.md)
+associated with your project, you can use it to assist with your deployments, and
+can even access a [web terminal](#web-terminals) for your environment from within GitLab!
+
+## Configuring environments
+
+Configuring environments involves:
+
+1. Understanding how [pipelines](../pipelines/index.md) work.
+1. Defining environments in your project's [`.gitlab-ci.yml`](../yaml/README.md) file.
+1. Creating a job configured to deploy your application. For example, a deploy job configured with [`environment`](../yaml/README.md#environment) to deploy your application to a [Kubernetes cluster](../../user/project/clusters/index.md).
+
+The rest of this section illustrates how to configure environments and deployments using
+an example scenario. It assumes you have already:
+
+- Created a [project](../../gitlab-basics/create-project.md) in GitLab.
+- Set up [a Runner](../runners/README.md).
+
+In the scenario:
+
+- We are developing an application.
+- We want to run tests and build our app on all branches.
+- Our default branch is `master`.
+- We deploy the app only when a pipeline on `master` branch is run.
+
+### Defining environments
+
+Let's consider the following `.gitlab-ci.yml` example:
+
+```yaml
+stages:
+ - test
+ - build
+ - deploy
+
+test:
+ stage: test
+ script: echo "Running tests"
+
+build:
+ stage: build
+ script: echo "Building the app"
+
+deploy_staging:
+ stage: deploy
+ script:
+ - echo "Deploy to staging server"
+ environment:
+ name: staging
+ url: https://staging.example.com
+ only:
+ - master
+```
+
+We have defined three [stages](../yaml/README.md#stages):
+
+- `test`
+- `build`
+- `deploy`
+
+The jobs assigned to these stages will run in this order. If any job fails, then
+the pipeline fails and jobs that are assigned to the next stage won't run.
+
+In our case:
+
+- The `test` job will run first.
+- Then the `build` job.
+- Lastly the `deploy_staging` job.
+
+With this configuration, we:
+
+- Check that the tests pass.
+- Ensure that our app is able to be built successfully.
+- Lastly we deploy to the staging server.
+
+NOTE: **Note:**
+The `environment` keyword defines where the app is deployed.
+The environment `name` and `url` is exposed in various places
+within GitLab. Each time a job that has an environment specified
+succeeds, a deployment is recorded, along with the Git SHA, and environment name.
+
+CAUTION: **Caution**:
+Some characters are not allowed in environment names. Use only letters,
+numbers, spaces, and `-`, `_`, `/`, `{`, `}`, or `.`. Also, it must not start nor end with `/`.
+
+In summary, with the above `.gitlab-ci.yml` we have achieved the following:
+
+- All branches will run the `test` and `build` jobs.
+- The `deploy_staging` job will run [only](../yaml/README.md#onlyexcept-basic) on the `master`
+ branch, which means all merge requests that are created from branches don't
+ get deployed to the staging server.
+- When a merge request is merged, all jobs will run and the `deploy_staging`
+ job will deploy our code to a staging server while the deployment
+ will be recorded in an environment named `staging`.
+
+#### Environment variables and Runner
+
+Starting with GitLab 8.15, the environment name is exposed to the Runner in
+two forms:
+
+- `$CI_ENVIRONMENT_NAME`. The name given in `.gitlab-ci.yml` (with any variables
+ expanded).
+- `$CI_ENVIRONMENT_SLUG`. A "cleaned-up" version of the name, suitable for use in URLs,
+ DNS, etc.
+
+If you change the name of an existing environment, the:
+
+- `$CI_ENVIRONMENT_NAME` variable will be updated with the new environment name.
+- `$CI_ENVIRONMENT_SLUG` variable will remain unchanged to prevent unintended side
+ effects.
+
+Starting with GitLab 9.3, the environment URL is exposed to the Runner via
+`$CI_ENVIRONMENT_URL`. The URL is expanded from either:
+
+- `.gitlab-ci.yml`.
+- The external URL from the environment if not defined in `.gitlab-ci.yml`.
+
+#### Set dynamic environment URLs after a job finishes
+
+> [Introduced](https://gitlab.com/gitlab-org/gitlab/issues/17066) in GitLab 12.9.
+
+In a job script, you can specify a static [environment URL](#using-the-environment-url).
+However, there may be times when you want a dynamic URL. For example,
+if you deploy a Review App to an external hosting
+service that generates a random URL per deployment, like `https://94dd65b.amazonaws.com/qa-lambda-1234567`,
+you don't know the URL before the deployment script finishes.
+If you want to use the environment URL in GitLab, you would have to update it manually.
+
+To address this problem, you can configure a deployment job to report back a set of
+variables, including the URL that was dynamically-generated by the external service.
+GitLab supports [dotenv](https://github.com/bkeepers/dotenv) file as the format,
+and expands the `environment:url` value with variables defined in the dotenv file.
+
+To use this feature, specify the
+[`artifacts:reports:dotenv`](../pipelines/job_artifacts.md#artifactsreportsdotenv) keyword in `.gitlab-ci.yml`.
+
+<i class="fa fa-youtube-play youtube" aria-hidden="true"></i>
+For an overview, see [Set dynamic URLs after a job finished](https://youtu.be/70jDXtOf4Ig).
+
+##### Example of setting dynamic environment URLs
+
+The following example shows a Review App that creates a new environment
+per merge request. The `review` job is triggered by every push, and
+creates or updates an environment named `review/your-branch-name`.
+The environment URL is set to `$DYNAMIC_ENVIRONMENT_URL`:
+
+```yaml
+review:
+ script:
+ - DYNAMIC_ENVIRONMENT_URL=$(deploy-script) # In script, get the environment URL.
+ - echo "DYNAMIC_ENVIRONMENT_URL=$DYNAMIC_ENVIRONMENT_URL" >> deploy.env # Add the value to a dotenv file.
+ artifacts:
+ reports:
+ dotenv: deploy.env # Report back dotenv file to rails.
+ environment:
+ name: review/$CI_COMMIT_REF_SLUG
+ url: $DYNAMIC_ENVIRONMENT_URL # and set the variable produced in script to `environment:url`
+ on_stop: stop_review
+
+stop_review:
+ script:
+ - ./teardown-environment
+ when: manual
+ environment:
+ name: review/$CI_COMMIT_REF_SLUG
+ action: stop
+```
+
+As soon as the `review` job finishes, GitLab updates the `review/your-branch-name`
+environment's URL.
+It parses the report artifact `deploy.env`, registers a list of variables as runtime-created,
+uses it for expanding `environment:url: $DYNAMIC_ENVIRONMENT_URL` and sets it to the environment URL.
+You can also specify a static part of the URL at `environment:url:`, such as
+`https://$DYNAMIC_ENVIRONMENT_URL`. If the value of `DYNAMIC_ENVIRONMENT_URL` is
+`123.awesome.com`, the final result will be `https://123.awesome.com`.
+
+The assigned URL for the `review/your-branch-name` environment is visible in the UI.
+[See where the environment URL is displayed](#using-the-environment-url).
+
+> **Notes:**
+>
+> - `stop_review` doesn't generate a dotenv report artifact, so it won't recognize the `DYNAMIC_ENVIRONMENT_URL` variable. Therefore you should not set `environment:url:` in the `stop_review` job.
+> - If the environment URL is not valid (for example, the URL is malformed), the system doesn't update the environment URL.
+
+### Configuring manual deployments
+
+Adding `when: manual` to an automatically executed job's configuration converts it to
+a job requiring manual action.
+
+To expand on the [previous example](#defining-environments), the following includes
+another job that deploys our app to a production server and is
+tracked by a `production` environment.
+
+The `.gitlab-ci.yml` file for this is as follows:
+
+```yaml
+stages:
+ - test
+ - build
+ - deploy
+
+test:
+ stage: test
+ script: echo "Running tests"
+
+build:
+ stage: build
+ script: echo "Building the app"
+
+deploy_staging:
+ stage: deploy
+ script:
+ - echo "Deploy to staging server"
+ environment:
+ name: staging
+ url: https://staging.example.com
+ only:
+ - master
+
+deploy_prod:
+ stage: deploy
+ script:
+ - echo "Deploy to production server"
+ environment:
+ name: production
+ url: https://example.com
+ when: manual
+ only:
+ - master
+```
+
+The `when: manual` action:
+
+- Exposes a "play" button in GitLab's UI for that job.
+- Means the `deploy_prod` job will only be triggered when the "play" button is clicked.
+
+You can find the "play" button in the pipelines, environments, deployments, and jobs views.
+
+| View | Screenshot |
+|:----------------|:-------------------------------------------------------------------------------|
+| Pipelines | ![Pipelines manual action](../img/environments_manual_action_pipelines.png) |
+| Single pipeline | ![Pipelines manual action](../img/environments_manual_action_single_pipeline.png) |
+| Environments | ![Environments manual action](../img/environments_manual_action_environments.png) |
+| Deployments | ![Deployments manual action](../img/environments_manual_action_deployments.png) |
+| Jobs | ![Builds manual action](../img/environments_manual_action_jobs.png) |
+
+Clicking on the play button in any view will trigger the `deploy_prod` job, and the
+deployment will be recorded as a new environment named `production`.
+
+NOTE: **Note:**
+If your environment's name is `production` (all lowercase),
+it will get recorded in [Value Stream Analytics](../../user/project/cycle_analytics.md).
+
+### Configuring dynamic environments
+
+Regular environments are good when deploying to "stable" environments like staging or production.
+
+However, for environments for branches other than `master`, dynamic environments
+can be used. Dynamic environments make it possible to create environments on the fly by
+declaring their names dynamically in `.gitlab-ci.yml`.
+
+Dynamic environments are a fundamental part of [Review apps](../review_apps/index.md).
+
+### Configuring incremental rollouts
+
+Learn how to release production changes to only a portion of your Kubernetes pods with
+[incremental rollouts](../environments/incremental_rollouts.md).
+
+#### Allowed variables
+
+The `name` and `url` parameters for dynamic environments can use most available CI/CD variables,
+including:
+
+- [Predefined environment variables](../variables/README.md#predefined-environment-variables)
+- [Project and group variables](../variables/README.md#gitlab-cicd-environment-variables)
+- [`.gitlab-ci.yml` variables](../yaml/README.md#variables)
+
+However, you cannot use variables defined:
+
+- Under `script`.
+- On the Runner's side.
+
+There are also other variables that are unsupported in the context of `environment:name`.
+For more information, see [Where variables can be used](../variables/where_variables_can_be_used.md).
+
+#### Example configuration
+
+GitLab Runner exposes various [environment variables](../variables/README.md) when a job runs, so
+you can use them as environment names.
+
+In the following example, the job will deploy to all branches except `master`:
+
+```yaml
+deploy_review:
+ stage: deploy
+ script:
+ - echo "Deploy a review app"
+ environment:
+ name: review/$CI_COMMIT_REF_NAME
+ url: https://$CI_ENVIRONMENT_SLUG.example.com
+ only:
+ - branches
+ except:
+ - master
+```
+
+In this example:
+
+- The job's name is `deploy_review` and it runs on the `deploy` stage.
+- We set the `environment` with the `environment:name` as `review/$CI_COMMIT_REF_NAME`.
+ Since the [environment name](../yaml/README.md#environmentname) can contain slashes (`/`), we can
+ use this pattern to distinguish between dynamic and regular environments.
+- We tell the job to run [`only`](../yaml/README.md#onlyexcept-basic) on branches,
+ [`except`](../yaml/README.md#onlyexcept-basic) `master`.
+
+For the value of:
+
+- `environment:name`, the first part is `review`, followed by a `/` and then `$CI_COMMIT_REF_NAME`,
+ which receives the value of the branch name.
+- `environment:url`, we want a specific and distinct URL for each branch. `$CI_COMMIT_REF_NAME`
+ may contain a `/` or other characters that would be invalid in a domain name or URL,
+ so we use `$CI_ENVIRONMENT_SLUG` to guarantee that we get a valid URL.
+
+ For example, given a `$CI_COMMIT_REF_NAME` of `100-Do-The-Thing`, the URL will be something
+ like `https://100-do-the-4f99a2.example.com`. Again, the way you set up
+ the web server to serve these requests is based on your setup.
+
+ We have used `$CI_ENVIRONMENT_SLUG` here because it is guaranteed to be unique. If
+ you're using a workflow like [GitLab Flow](../../topics/gitlab_flow.md), collisions
+ are unlikely and you may prefer environment names to be more closely based on the
+ branch name. In that case, you could use `$CI_COMMIT_REF_NAME` in `environment:url` in
+ the example above: `https://$CI_COMMIT_REF_NAME.example.com`, which would give a URL
+ of `https://100-do-the-thing.example.com`.
+
+NOTE: **Note:**
+You are not required to use the same prefix or only slashes (`/`) in the dynamic environments'
+names. However, using this format will enable the [grouping similar environments](#grouping-similar-environments)
+feature.
+
+### Configuring Kubernetes deployments
+
+> [Introduced](https://gitlab.com/gitlab-org/gitlab/issues/27630) in GitLab 12.6.
+
+If you are deploying to a [Kubernetes cluster](../../user/project/clusters/index.md)
+associated with your project, you can configure these deployments from your
+`gitlab-ci.yml` file.
+
+The following configuration options are supported:
+
+- [`namespace`](https://kubernetes.io/docs/concepts/overview/working-with-objects/namespaces/)
+
+In the following example, the job will deploy your application to the
+`production` Kubernetes namespace.
+
+```yaml
+deploy:
+ stage: deploy
+ script:
+ - echo "Deploy to production server"
+ environment:
+ name: production
+ url: https://example.com
+ kubernetes:
+ namespace: production
+ only:
+ - master
+```
+
+When deploying to a Kubernetes cluster using GitLab's Kubernetes integration,
+information about the cluster and namespace will be displayed above the job
+trace on the deployment job page:
+
+![Deployment cluster information](../img/environments_deployment_cluster_v12_8.png)
+
+NOTE: **Note:**
+Kubernetes configuration is not supported for Kubernetes clusters
+that are [managed by GitLab](../../user/project/clusters/index.md#gitlab-managed-clusters).
+To follow progress on support for GitLab-managed clusters, see the
+[relevant issue](https://gitlab.com/gitlab-org/gitlab/issues/38054).
+
+### Complete example
+
+The configuration in this section provides a full development workflow where your app is:
+
+- Tested.
+- Built.
+- Deployed as a Review App.
+- Deployed to a staging server once the merge request is merged.
+- Finally, able to be manually deployed to the production server.
+
+The following combines the previous configuration examples, including:
+
+- Defining [simple environments](#defining-environments) for testing, building, and deployment to staging.
+- Adding [manual actions](#configuring-manual-deployments) for deployment to production.
+- Creating [dynamic environments](#configuring-dynamic-environments) for deployments for reviewing.
+
+```yaml
+stages:
+ - test
+ - build
+ - deploy
+
+test:
+ stage: test
+ script: echo "Running tests"
+
+build:
+ stage: build
+ script: echo "Building the app"
+
+deploy_review:
+ stage: deploy
+ script:
+ - echo "Deploy a review app"
+ environment:
+ name: review/$CI_COMMIT_REF_NAME
+ url: https://$CI_ENVIRONMENT_SLUG.example.com
+ only:
+ - branches
+ except:
+ - master
+
+deploy_staging:
+ stage: deploy
+ script:
+ - echo "Deploy to staging server"
+ environment:
+ name: staging
+ url: https://staging.example.com
+ only:
+ - master
+
+deploy_prod:
+ stage: deploy
+ script:
+ - echo "Deploy to production server"
+ environment:
+ name: production
+ url: https://example.com
+ when: manual
+ only:
+ - master
+```
+
+A more realistic example would also include copying files to a location where a
+webserver (for example, NGINX) could then access and serve them.
+
+The example below will copy the `public` directory to `/srv/nginx/$CI_COMMIT_REF_SLUG/public`:
+
+```yaml
+review_app:
+ stage: deploy
+ script:
+ - rsync -av --delete public /srv/nginx/$CI_COMMIT_REF_SLUG
+ environment:
+ name: review/$CI_COMMIT_REF_NAME
+ url: https://$CI_COMMIT_REF_SLUG.example.com
+```
+
+This example requires that NGINX and GitLab Runner are set up on the server this job will run on.
+
+NOTE: **Note:**
+See the [limitations](#limitations) section for some edge cases regarding the naming of
+your branches and Review Apps.
+
+The complete example provides the following workflow to developers:
+
+- Create a branch locally.
+- Make changes and commit them.
+- Push the branch to GitLab.
+- Create a merge request.
+
+Behind the scenes, GitLab Runner will:
+
+- Pick up the changes and start running the jobs.
+- Run the jobs sequentially as defined in `stages`:
+ - First, run the tests.
+ - If the tests succeed, build the app.
+ - If the build succeeds, the app is deployed to an environment with a name specific to the
+ branch.
+
+So now, every branch:
+
+- Gets its own environment.
+- Is deployed to its own unique location, with the added benefit of:
+ - Having a [history of deployments](#viewing-deployment-history).
+ - Being able to [rollback changes](#retrying-and-rolling-back) if needed.
+
+For more information, see [Using the environment URL](#using-the-environment-url).
+
+### Protected environments
+
+Environments can be "protected", restricting access to them.
+
+For more information, see [Protected environments](protected_environments.md).
+
+## Working with environments
+
+Once environments are configured, GitLab provides many features for working with them,
+as documented below.
+
+### Viewing environments and deployments
+
+A list of environments and deployment statuses is available on each project's **Operations > Environments** page.
+
+For example:
+
+![Environment view](../img/environments_available.png)
+
+This example shows:
+
+- The environment's name with a link to its deployments.
+- The last deployment ID number and who performed it.
+- The job ID of the last deployment with its respective job name.
+- The commit information of the last deployment, such as who committed it, to what
+ branch, and the Git SHA of the commit.
+- The exact time the last deployment was performed.
+- A button that takes you to the URL that you defined under the `environment` keyword
+ in `.gitlab-ci.yml`.
+- A button that re-deploys the latest deployment, meaning it runs the job
+ defined by the environment name for that specific commit.
+
+The information shown in the **Environments** page is limited to the latest
+deployments, but an environment can have multiple deployments.
+
+> **Notes:**
+>
+> - While you can create environments manually in the web interface, we recommend
+> that you define your environments in `.gitlab-ci.yml` first. They will
+> be automatically created for you after the first deploy.
+> - The environments page can only be viewed by users with [Reporter permission](../../user/permissions.md#project-members-permissions)
+> and above. For more information on permissions, see the [permissions documentation](../../user/permissions.md).
+> - Only deploys that happen after your `.gitlab-ci.yml` is properly configured
+> will show up in the **Environment** and **Last deployment** lists.
+
+### Viewing deployment history
+
+GitLab keeps track of your deployments, so you:
+
+- Always know what is currently being deployed on your servers.
+- Can have the full history of your deployments for every environment.
+
+Clicking on an environment shows the history of its deployments. Here's an example **Environments** page
+with multiple deployments:
+
+![Deployments](../img/deployments_view.png)
+
+This view is similar to the **Environments** page, but all deployments are shown. Also in this view
+is a **Rollback** button. For more information, see [Retrying and rolling back](#retrying-and-rolling-back).
+
+### Retrying and rolling back
+
+If there is a problem with a deployment, you can retry it or roll it back.
+
+To retry or rollback a deployment:
+
+1. Navigate to **Operations > Environments**.
+1. Click on the environment.
+1. In the deployment history list for the environment, click the:
+ - **Retry** button next to the last deployment, to retry that deployment.
+ - **Rollback** button next to a previously successful deployment, to roll back to that deployment.
+
+#### What to expect with a rollback
+
+Pressing the **Rollback** button on a specific commit will trigger a _new_ deployment with its
+own unique job ID.
+
+This means that you will see a new deployment that points to the commit you are rolling back to.
+
+NOTE: **Note:**
+The defined deployment process in the job's `script` determines whether the rollback succeeds or not.
+
+### Using the environment URL
+
+The [environment URL](../yaml/README.md#environmenturl) is exposed in a few
+places within GitLab:
+
+- In a merge request widget as a link:
+ ![Environment URL in merge request](../img/environments_mr_review_app.png)
+- In the Environments view as a button:
+ ![Environment URL in environments](../img/environments_available.png)
+- In the Deployments view as a button:
+ ![Environment URL in deployments](../img/deployments_view.png)
+
+You can see this information in a merge request itself if:
+
+- The merge request is eventually merged to the default branch (usually `master`).
+- That branch also deploys to an environment (for example, `staging` or `production`).
+
+For example:
+
+![Environment URLs in merge request](../img/environments_link_url_mr.png)
+
+#### Going from source files to public pages
+
+With GitLab's [Route Maps](../review_apps/index.md#route-maps) you can go directly
+from source files to public pages in the environment set for Review Apps.
+
+### Stopping an environment
+
+Stopping an environment:
+
+- Moves it from the list of **Available** environments to the list of **Stopped**
+ environments on the [**Environments** page](#viewing-environments-and-deployments).
+- Executes an [`on_stop` action](../yaml/README.md#environmenton_stop), if defined.
+
+This is often used when multiple developers are working on a project at the same time,
+each of them pushing to their own branches, causing many dynamic environments to be created.
+
+NOTE: **Note:**
+Starting with GitLab 8.14, dynamic environments are stopped automatically
+when their associated branch is deleted.
+
+#### Automatically stopping an environment
+
+Environments can be stopped automatically using special configuration.
+
+Consider the following example where the `deploy_review` job calls `stop_review`
+to clean up and stop the environment:
+
+```yaml
+deploy_review:
+ stage: deploy
+ script:
+ - echo "Deploy a review app"
+ environment:
+ name: review/$CI_COMMIT_REF_NAME
+ url: https://$CI_ENVIRONMENT_SLUG.example.com
+ on_stop: stop_review
+ only:
+ - branches
+ except:
+ - master
+
+stop_review:
+ stage: deploy
+ variables:
+ GIT_STRATEGY: none
+ script:
+ - echo "Remove review app"
+ when: manual
+ environment:
+ name: review/$CI_COMMIT_REF_NAME
+ action: stop
+```
+
+Setting the [`GIT_STRATEGY`](../yaml/README.md#git-strategy) to `none` is necessary in the
+`stop_review` job so that the [GitLab Runner](https://docs.gitlab.com/runner/) won't
+try to check out the code after the branch is deleted.
+
+When you have an environment that has a stop action defined (typically when
+the environment describes a Review App), GitLab will automatically trigger a
+stop action when the associated branch is deleted. The `stop_review` job must
+be in the same `stage` as the `deploy_review` job in order for the environment
+to automatically stop.
+
+You can read more in the [`.gitlab-ci.yml` reference](../yaml/README.md#environmenton_stop).
+
+#### Environments auto-stop
+
+> [Introduced](https://gitlab.com/gitlab-org/gitlab/issues/20956) in GitLab 12.8.
+
+You can set a expiry time to environments and stop them automatically after a certain period.
+
+For example, consider the use of this feature with Review Apps environments.
+When you set up Review Apps, sometimes they keep running for a long time
+because some merge requests are left as open. An example for this situation is when the author of the merge
+request is not actively working on it, due to priority changes or a different approach was decided on, and the merge requests was simply forgotten.
+Idle environments waste resources, therefore they
+should be terminated as soon as possible.
+
+To address this problem, you can specify an optional expiration date for
+Review Apps environments. When the expiry time is reached, GitLab will automatically trigger a job
+to stop the environment, eliminating the need of manually doing so. In case an environment is updated, the expiration is renewed
+ensuring that only active merge requests keep running Review Apps.
+
+To enable this feature, you need to specify the [`environment:auto_stop_in`](../yaml/README.md#environmentauto_stop_in) keyword in `.gitlab-ci.yml`.
+You can specify a human-friendly date as the value, such as `1 hour and 30 minutes` or `1 day`.
+`auto_stop_in` uses the same format of [`artifacts:expire_in` docs](../yaml/README.md#artifactsexpire_in).
+
+##### Auto-stop example
+
+In the following example, there is a basic review app setup that creates a new environment
+per merge request. The `review_app` job is triggered by every push and
+creates or updates an environment named `review/your-branch-name`.
+The environment keeps running until `stop_review_app` is executed:
+
+```yaml
+review_app:
+ script: deploy-review-app
+ environment:
+ name: review/$CI_COMMIT_REF_NAME
+ on_stop: stop_review_app
+ auto_stop_in: 1 week
+
+stop_review_app:
+ script: stop-review-app
+ environment:
+ name: review/$CI_COMMIT_REF_NAME
+ action: stop
+ when: manual
+```
+
+As long as a merge request is active and keeps getting new commits,
+the review app will not stop, so developers don't need to worry about
+re-initiating review app.
+
+On the other hand, since `stop_review_app` is set to `auto_stop_in: 1 week`,
+if a merge request becomes inactive for more than a week,
+GitLab automatically triggers the `stop_review_app` job to stop the environment.
+
+You can also check the expiration date of environments through the GitLab UI. To do so,
+go to **Operations > Environments > Environment**. You can see the auto-stop period
+at the left-top section and a pin-mark button at the right-top section. This pin-mark
+button can be used to prevent auto-stopping the environment. By clicking this button, the `auto_stop_in` setting is over-written
+and the environment will be active until it's stopped manually.
+
+![Environment auto stop](../img/environment_auto_stop_v12_8.png)
+
+NOTE: **NOTE**
+Due to the resource limitation, a background worker for stopping environments only
+runs once every hour. This means environments will not be stopped at the exact
+timestamp as the specified period, but will be stopped when the hourly cron worker
+detects expired environments.
+
+#### Delete a stopped environment
+
+> [Introduced](https://gitlab.com/gitlab-org/gitlab/-/merge_requests/22629) in GitLab 12.9.
+
+You can delete [stopped environments](#stopping-an-environment) in one of two
+ways: through the GitLab UI or through the API.
+
+##### Delete environments through the UI
+
+To view the list of **Stopped** environments, navigate to **Operations > Environments**
+and click the **Stopped** tab.
+
+From there, you can click the **Delete** button directly, or you can click the
+environment name to see its details and **Delete** it from there.
+
+You can also delete environments by viewing the details for a
+stopped environment:
+
+ 1. Navigate to **Operations > Environments**.
+ 1. Click on the name of an environment within the **Stopped** environments list.
+ 1. Click on the **Delete** button that appears at the top for all stopped environments.
+ 1. Finally, confirm your chosen environment in the modal that appears to delete it.
+
+##### Delete environments through the API
+
+Environments can also be deleted by using the [Environments API](../../api/environments.md#delete-an-environment).
+
+### Grouping similar environments
+
+> [Introduced](https://gitlab.com/gitlab-org/gitlab-foss/-/merge_requests/7015) in GitLab 8.14.
+
+As documented in [Configuring dynamic environments](#configuring-dynamic-environments), you can
+prepend environment name with a word, followed by a `/`, and finally the branch
+name, which is automatically defined by the `CI_COMMIT_REF_NAME` variable.
+
+In short, environments that are named like `type/foo` are all presented under the same
+group, named `type`.
+
+In our [minimal example](#example-configuration), we named the environments `review/$CI_COMMIT_REF_NAME`
+where `$CI_COMMIT_REF_NAME` is the branch name. Here is a snippet of the example:
+
+```yaml
+deploy_review:
+ stage: deploy
+ script:
+ - echo "Deploy a review app"
+ environment:
+ name: review/$CI_COMMIT_REF_NAME
+```
+
+In this case, if you visit the **Environments** page and the branches
+exist, you should see something like:
+
+![Environment groups](../img/environments_dynamic_groups.png)
+
+### Monitoring environments
+
+If you have enabled [Prometheus for monitoring system and response metrics](../../user/project/integrations/prometheus.md),
+you can monitor the behavior of your app running in each environment. For the monitoring
+dashboard to appear, you need to Configure Prometheus to collect at least one
+[supported metric](../../user/project/integrations/prometheus_library/index.md).
+
+NOTE: **Note:**
+Since GitLab 9.2, all deployments to an environment are shown directly on the monitoring dashboard.
+
+Once configured, GitLab will attempt to retrieve [supported performance metrics](../../user/project/integrations/prometheus_library/index.md)
+for any environment that has had a successful deployment. If monitoring data was
+successfully retrieved, a **Monitoring** button will appear for each environment.
+
+![Environment Detail with Metrics](../img/deployments_view.png)
+
+Clicking on the **Monitoring** button will display a new page showing up to the last
+8 hours of performance data. It may take a minute or two for data to appear
+after initial deployment.
+
+All deployments to an environment are shown directly on the monitoring dashboard,
+which allows easy correlation between any changes in performance and new
+versions of the app, all without leaving GitLab.
+
+![Monitoring dashboard](../img/environments_monitoring.png)
+
+#### Linking to external dashboard
+
+Add a [button to the Monitoring dashboard](../../user/project/operations/linking_to_an_external_dashboard.md) linking directly to your existing external dashboards.
+
+#### Embedding metrics in GitLab Flavored Markdown
+
+Metric charts can be embedded within GitLab Flavored Markdown. See [Embedding Metrics within GitLab Flavored Markdown](../../user/project/integrations/prometheus.md#embedding-metric-charts-within-gitlab-flavored-markdown) for more details.
+
+### Web terminals
+
+> Web terminals were added in GitLab 8.15 and are only available to project Maintainers and Owners.
+
+If you deploy to your environments with the help of a deployment service (for example,
+the [Kubernetes integration](../../user/project/clusters/index.md)), GitLab can open
+a terminal session to your environment.
+
+This is a powerful feature that allows you to debug issues without leaving the comfort
+of your web browser. To enable it, just follow the instructions given in the service integration
+documentation.
+
+Once enabled, your environments will gain a "terminal" button:
+
+![Terminal button on environment index](../img/environments_terminal_button_on_index.png)
+
+You can also access the terminal button from the page for a specific environment:
+
+![Terminal button for an environment](../img/environments_terminal_button_on_show.png)
+
+Wherever you find it, clicking the button will take you to a separate page to
+establish the terminal session:
+
+![Terminal page](../img/environments_terminal_page.png)
+
+This works just like any other terminal. You'll be in the container created
+by your deployment so you can:
+
+- Run shell commands and get responses in real time.
+- Check the logs.
+- Try out configuration or code tweaks etc.
+
+You can open multiple terminals to the same environment, they each get their own shell
+session and even a multiplexer like `screen` or `tmux`.
+
+NOTE: **Note:**
+Container-based deployments often lack basic tools (like an editor), and may
+be stopped or restarted at any time. If this happens, you will lose all your
+changes. Treat this as a debugging tool, not a comprehensive online IDE.
+
+### Check out deployments locally
+
+Since GitLab 8.13, a reference in the Git repository is saved for each deployment, so
+knowing the state of your current environments is only a `git fetch` away.
+
+In your Git configuration, append the `[remote "<your-remote>"]` block with an extra
+fetch line:
+
+```plaintext
+fetch = +refs/environments/*:refs/remotes/origin/environments/*
+```
+
+### Scoping environments with specs
+
+> - [Introduced](https://gitlab.com/gitlab-org/gitlab/-/merge_requests/2112) in [GitLab Premium](https://about.gitlab.com/pricing/) 9.4.
+> - [Scoping for environment variables was moved to Core](https://gitlab.com/gitlab-org/gitlab-foss/-/merge_requests/30779) to Core in GitLab 12.2.
+
+You can limit the environment scope of a variable by
+defining which environments it can be available for.
+
+Wildcards can be used, and the default environment scope is `*`, which means
+any jobs will have this variable, not matter if an environment is defined or
+not.
+
+For example, if the environment scope is `production`, then only the jobs
+having the environment `production` defined would have this specific variable.
+Wildcards (`*`) can be used along with the environment name, therefore if the
+environment scope is `review/*` then any jobs with environment names starting
+with `review/` would have that particular variable.
+
+Some GitLab features can behave differently for each environment.
+For example, you can
+[create a secret variable to be injected only into a production environment](../variables/README.md#limit-the-environment-scopes-of-environment-variables).
+
+In most cases, these features use the _environment specs_ mechanism, which offers
+an efficient way to implement scoping within each environment group.
+
+Let's say there are four environments:
+
+- `production`
+- `staging`
+- `review/feature-1`
+- `review/feature-2`
+
+Each environment can be matched with the following environment spec:
+
+| Environment Spec | `production` | `staging` | `review/feature-1` | `review/feature-2` |
+|:-----------------|:-------------|:----------|:-------------------|:-------------------|
+| * | Matched | Matched | Matched | Matched |
+| production | Matched | | | |
+| staging | | Matched | | |
+| review/* | | | Matched | Matched |
+| review/feature-1 | | | Matched | |
+
+As you can see, you can use specific matching for selecting a particular environment,
+and also use wildcard matching (`*`) for selecting a particular environment group,
+such as [Review Apps](../review_apps/index.md) (`review/*`).
+
+NOTE: **Note:**
+The most _specific_ spec takes precedence over the other wildcard matching.
+In this case, `review/feature-1` spec takes precedence over `review/*` and `*` specs.
+
+### Environments Dashboard **(PREMIUM)**
+
+See [Environments Dashboard](../environments/environments_dashboard.md) for a summary of each
+environment's operational health.
+
+## Limitations
+
+In the `environment: name`, you are limited to only the [predefined environment variables](../variables/predefined_variables.md).
+Re-using variables defined inside `script` as part of the environment name will not work.
+
+## Further reading
+
+Below are some links you may find interesting:
+
+- [The `.gitlab-ci.yml` definition of environments](../yaml/README.md#environment)
+- [A blog post on Deployments & Environments](https://about.gitlab.com/blog/2016/08/26/ci-deployment-and-environments/)
+- [Review Apps - Use dynamic environments to deploy your code for every branch](../review_apps/index.md)
+- [Deploy Boards for your applications running on Kubernetes](../../user/project/deploy_boards.md) **(PREMIUM)**
+
+<!-- ## Troubleshooting
+
+Include any troubleshooting steps that you can foresee. If you know beforehand what issues
+one might have when setting this up, or when something is changed, or on upgrading, it's
+important to describe those, too. Think of things that may go wrong and include them here.
+This is important to minimize requests for support, and to avoid doc comments with
+questions that you know someone might ask.
+
+Each scenario can be a third-level heading, e.g. `### Getting error message X`.
+If you have none to add when creating a doc, leave this section in place
+but commented out to help encourage others to add to it in the future. -->
diff --git a/doc/ci/environments/protected_environments.md b/doc/ci/environments/protected_environments.md
index fb6cf2a710f..43ac42ea0c7 100644
--- a/doc/ci/environments/protected_environments.md
+++ b/doc/ci/environments/protected_environments.md
@@ -1,4 +1,7 @@
---
+stage: Release
+group: Release Management
+info: To determine the technical writer assigned to the Stage/Group associated with this page, see https://about.gitlab.com/handbook/engineering/ux/technical-writing/#designated-technical-writers
type: concepts, howto
---
@@ -8,7 +11,7 @@ type: concepts, howto
## Overview
-[Environments](../environments.md) can be used for different reasons:
+[Environments](../environments/index.md) can be used for different reasons:
- Some of them are just for testing.
- Others are for production.
diff --git a/doc/ci/examples/authenticating-with-hashicorp-vault/index.md b/doc/ci/examples/authenticating-with-hashicorp-vault/index.md
index 2986afe8284..089babc8945 100644
--- a/doc/ci/examples/authenticating-with-hashicorp-vault/index.md
+++ b/doc/ci/examples/authenticating-with-hashicorp-vault/index.md
@@ -20,7 +20,7 @@ You will need to replace the `vault.example.com` URL below with the URL of your
## How it works
-Each job has JSON Web Token (JWT) provided as environment variable named `CI_JOB_JWT`. This JWT can be used to authenticate with Vault using the [JWT Auth](https://www.vaultproject.io/docs/auth/jwt/#jwt-authentication) method.
+Each job has JSON Web Token (JWT) provided as environment variable named `CI_JOB_JWT`. This JWT can be used to authenticate with Vault using the [JWT Auth](https://www.vaultproject.io/docs/auth/jwt#jwt-authentication) method.
The JWT's payload looks like this:
@@ -51,7 +51,7 @@ The JWT is encoded by using RS256 and signed with your GitLab instance's OpenID
You can use this JWT and your instance's JWKS endpoint (`https://gitlab.example.com/-/jwks`) to authenticate with a Vault server that is configured to allow the JWT Authentication method for authentication.
-When configuring roles in Vault, you can use [bound_claims](https://www.vaultproject.io/docs/auth/jwt/#bound-claims) to match against the JWT's claims and restrict which secrets each CI job has access to.
+When configuring roles in Vault, you can use [bound_claims](https://www.vaultproject.io/docs/auth/jwt#bound-claims) to match against the JWT's claims and restrict which secrets each CI job has access to.
To communicate with Vault, you can use either its CLI client or perform API requests (using `curl` or another client).
@@ -70,7 +70,7 @@ $ vault kv get -field=password secret/myproject/production/db
real-pa$$w0rd
```
-To configure your Vault server, start by enabling the [JWT Auth](https://www.vaultproject.io/docs/auth/jwt/) method:
+To configure your Vault server, start by enabling the [JWT Auth](https://www.vaultproject.io/docs/auth/jwt) method:
```shell
$ vault auth enable jwt
diff --git a/doc/ci/examples/devops_and_game_dev_with_gitlab_ci_cd/index.md b/doc/ci/examples/devops_and_game_dev_with_gitlab_ci_cd/index.md
index 848808f65ea..610220a6bff 100644
--- a/doc/ci/examples/devops_and_game_dev_with_gitlab_ci_cd/index.md
+++ b/doc/ci/examples/devops_and_game_dev_with_gitlab_ci_cd/index.md
@@ -269,7 +269,7 @@ we know we will need to access everything in the `built` folder, given by GitLab
We'll also cache `node_modules` to avoid having to do a full re-pull of those dependencies:
just pack them up in the cache. Here is the full `build` job:
-```yml
+```yaml
build:
stage: build
script:
@@ -296,7 +296,7 @@ the previous job. Lastly, by convention, we let GitLab CI/CD know this needs to
the `build` job by giving it a `test` [stage](../../../ci/yaml/README.md#stages).
Following the YAML structure, the `test` job should look like this:
-```yml
+```yaml
test:
stage: test
script:
@@ -318,7 +318,7 @@ we've added test artifacts and a test stage to our GitLab CI/CD pipeline using `
allowing us to run our tests by every push.
Our entire `.gitlab-ci.yml` file should now look like this:
-```yml
+```yaml
image: node:10
build:
@@ -417,7 +417,7 @@ credentials, which will be the same two credentials (Key ID and Secret). It's a
fully understand [IAM Best Practices in AWS](https://docs.aws.amazon.com/IAM/latest/UserGuide/best-practices.html). We need to add these credentials to GitLab:
1. Log into your AWS account and go to the [Security Credentials page](https://console.aws.amazon.com/iam/home#/security_credential)
-1. Click the **Access Keys** section and **Create New Access Key**. Create the key and keep the id and secret around, you'll need them later
+1. Click the **Access Keys** section and **Create New Access Key**. Create the key and keep the ID and secret around, you'll need them later
![AWS Access Key Config](img/aws_config_window.png)
@@ -426,7 +426,7 @@ fully understand [IAM Best Practices in AWS](https://docs.aws.amazon.com/IAM/lat
![GitLab Secret Config](img/gitlab_config.png)
-1. Add a key named `AWS_KEY_ID` and copy the key id from Step 2 into the **Value** textbox
+1. Add a key named `AWS_KEY_ID` and copy the key ID from Step 2 into the **Value** textbox
1. Add a key named `AWS_KEY_SECRET` and copy the key secret from Step 2 into the **Value** textbox
### Deploy your game with GitLab CI/CD
@@ -440,7 +440,7 @@ we add directives to ensure deployment `only` happens on pushes to `master`. Thi
single branch still runs through CI, and only merging (or committing directly) to master will
trigger the `deploy` job of our pipeline. Put these together to get the following:
-```yml
+```yaml
deploy:
stage: deploy
variables:
@@ -459,7 +459,7 @@ deploy:
Be sure to update the region and S3 URL in that last script command to fit your setup.
Our final configuration file `.gitlab-ci.yml` looks like:
-```yml
+```yaml
image: node:10
build:
@@ -516,7 +516,7 @@ Errors can be easily debugged through GitLab's build logs, and within minutes of
you can see the changes live on your game.
Setting up Continuous Integration and Continuous Deployment from the start with Dark Nova enables
-rapid but stable development. We can easily test changes in a separate [environment](../../environments.md),
+rapid but stable development. We can easily test changes in a separate [environment](../../environments/index.md),
or multiple environments if needed. Balancing and updating a multiplayer game can be ongoing
and tedious, but having faith in a stable deployment with GitLab CI/CD allows
a lot of breathing room in quickly getting changes to players.
diff --git a/doc/ci/examples/laravel_with_gitlab_and_envoy/index.md b/doc/ci/examples/laravel_with_gitlab_and_envoy/index.md
index 07b054dd2cb..0331fa70f06 100644
--- a/doc/ci/examples/laravel_with_gitlab_and_envoy/index.md
+++ b/doc/ci/examples/laravel_with_gitlab_and_envoy/index.md
@@ -376,7 +376,7 @@ You might want to create another Envoy task to do that for you.
We also create the `.env` file in the same path to set up our production environment variables for Laravel.
These are persistent data and will be shared to every new release.
-Now, we would need to deploy our app by running `envoy run deploy`, but it won't be necessary since GitLab can handle that for us with CI's [environments](../../environments.md), which will be described [later](#setting-up-gitlab-cicd) in this tutorial.
+Now, we would need to deploy our app by running `envoy run deploy`, but it won't be necessary since GitLab can handle that for us with CI's [environments](../../environments/index.md), which will be described [later](#setting-up-gitlab-cicd) in this tutorial.
Now it's time to commit [Envoy.blade.php](https://gitlab.com/mehranrasulian/laravel-sample/blob/master/Envoy.blade.php) and push it to the `master` branch.
To keep things simple, we commit directly to `master`, without using [feature-branches](../../../topics/gitlab_flow.md#github-flow-as-a-simpler-alternative) since collaboration is beyond the scope of this tutorial.
diff --git a/doc/ci/examples/test-and-deploy-python-application-to-heroku.md b/doc/ci/examples/test-and-deploy-python-application-to-heroku.md
index 6d05c37390a..2c626cb458a 100644
--- a/doc/ci/examples/test-and-deploy-python-application-to-heroku.md
+++ b/doc/ci/examples/test-and-deploy-python-application-to-heroku.md
@@ -76,14 +76,21 @@ To build this project you also need to have [GitLab Runner](https://docs.gitlab.
You can use public runners available on `gitlab.com` or you can register your own:
```shell
+cat > /tmp/test-config.template.toml << EOF
+[[runners]]
+[runners.docker]
+[[runners.docker.services]]
+name = "postgres:latest"
+EOF
+
gitlab-runner register \
--non-interactive \
--url "https://gitlab.com/" \
--registration-token "PROJECT_REGISTRATION_TOKEN" \
--description "python-3.5" \
--executor "docker" \
- --docker-image python:3.5 \
- --docker-services postgres:latest
+ --template-config /tmp/test-config.template.toml \
+ --docker-image python:3.5
```
With the command above, you create a runner that uses the [`python:3.5`](https://hub.docker.com/_/python) image and uses a [PostgreSQL](https://hub.docker.com/_/postgres) database.
diff --git a/doc/ci/examples/test-and-deploy-ruby-application-to-heroku.md b/doc/ci/examples/test-and-deploy-ruby-application-to-heroku.md
index e480f4565ce..5df407f19fe 100644
--- a/doc/ci/examples/test-and-deploy-ruby-application-to-heroku.md
+++ b/doc/ci/examples/test-and-deploy-ruby-application-to-heroku.md
@@ -64,7 +64,19 @@ You can do this through the [Heroku Dashboard](https://dashboard.heroku.com/).
First install [Docker Engine](https://docs.docker.com/installation/).
To build this project you also need to have [GitLab Runner](https://docs.gitlab.com/runner/).
-You can use public runners available on `gitlab.com` or register your own:
+You can use public runners available on `gitlab.com` or register your own. Start by
+creating a template configuration file in order to pass complex configuration:
+
+```shell
+cat > /tmp/test-config.template.toml << EOF
+[[runners]]
+[runners.docker]
+[[runners.docker.services]]
+name = "postgres:latest"
+EOF
+```
+
+Finally, register the runner, passing the newly-created template configuration file:
```shell
gitlab-runner register \
@@ -73,8 +85,8 @@ gitlab-runner register \
--registration-token "PROJECT_REGISTRATION_TOKEN" \
--description "ruby:2.6" \
--executor "docker" \
- --docker-image ruby:2.6 \
- --docker-services latest
+ --template-config /tmp/test-config.template.toml \
+ --docker-image ruby:2.6
```
With the command above, you create a Runner that uses the [`ruby:2.6`](https://hub.docker.com/_/ruby) image and uses a [PostgreSQL](https://hub.docker.com/_/postgres) database.
diff --git a/doc/ci/examples/test-scala-application.md b/doc/ci/examples/test-scala-application.md
index 92fb69286cf..1831417e48e 100644
--- a/doc/ci/examples/test-scala-application.md
+++ b/doc/ci/examples/test-scala-application.md
@@ -14,7 +14,7 @@ The following `.gitlab-ci.yml` should be added in the root of your
repository to trigger CI:
``` yaml
-image: java:8
+image: openjdk:8
stages:
- test
@@ -74,5 +74,5 @@ in the `.gitlab-ci.yml` file with your application's name.
## Heroku API key
You can look up your Heroku API key in your
-[account](https://dashboard.heroku.com/account). Add a [protected variable](../variables/README.md#protected-environment-variables) with
+[account](https://dashboard.heroku.com/account). Add a [protected variable](../variables/README.md#protect-a-custom-variable) with
this value in **Project âž” Variables** with key `HEROKU_API_KEY`.
diff --git a/doc/ci/examples/test_phoenix_app_with_gitlab_ci_cd/index.md b/doc/ci/examples/test_phoenix_app_with_gitlab_ci_cd/index.md
index 6d92c86c819..cd1ad923249 100644
--- a/doc/ci/examples/test_phoenix_app_with_gitlab_ci_cd/index.md
+++ b/doc/ci/examples/test_phoenix_app_with_gitlab_ci_cd/index.md
@@ -375,7 +375,7 @@ see if our latest code is running without errors.
When we finish this edition, GitLab will start another build and show a **build running** badge. It
is expected, after all we just configured GitLab CI/CD to do this for every push! But you may think
"Why run build and tests for simple things like editing README.md?" and it is a good question.
-For changes that don't affect your application, you can add the keyword [`[ci skip]`](../../yaml/README.md#skipping-jobs)
+For changes that don't affect your application, you can add the keyword [`[ci skip]`](../../yaml/README.md#skip-pipeline)
to commit message and the build related to that commit will be skipped.
In the end, we finally got our pretty green build succeeded badge! By outputting the result on the
diff --git a/doc/ci/img/metrics_reports.png b/doc/ci/img/metrics_reports.png
deleted file mode 100644
index ffd9f6830a2..00000000000
--- a/doc/ci/img/metrics_reports.png
+++ /dev/null
Binary files differ
diff --git a/doc/ci/img/metrics_reports_v13_0.png b/doc/ci/img/metrics_reports_v13_0.png
new file mode 100644
index 00000000000..1597031db0b
--- /dev/null
+++ b/doc/ci/img/metrics_reports_v13_0.png
Binary files differ
diff --git a/doc/ci/introduction/index.md b/doc/ci/introduction/index.md
index b16cde54b93..b7f1837d83e 100644
--- a/doc/ci/introduction/index.md
+++ b/doc/ci/introduction/index.md
@@ -76,6 +76,9 @@ to apply all the continuous methods (Continuous Integration,
Delivery, and Deployment) to your software with no third-party
application or integration needed.
+<i class="fa fa-youtube-play youtube" aria-hidden="true"></i>
+For an overview, see [Introduction to GitLab CI](https://www.youtube.com/watch?v=l5705U8s_nQ&t=397) from a recent GitLab meetup.
+
### How GitLab CI/CD works
To use GitLab CI/CD, all you need is an application codebase hosted in a
@@ -105,7 +108,7 @@ The scripts are grouped into **jobs**, and together they compose
a **pipeline**. A minimalist example of `.gitlab-ci.yml` file
could contain:
-```yml
+```yaml
before_script:
- apt-get install rubygems ruby-dev -y
@@ -133,7 +136,7 @@ displayed by GitLab:
![pipeline status](img/pipeline_status.png)
At the end, if anything goes wrong, you can easily
-[roll back](../environments.md#retrying-and-rolling-back) all the changes:
+[roll back](../environments/index.md#retrying-and-rolling-back) all the changes:
![rollback button](img/rollback.png)
@@ -204,7 +207,7 @@ according to each stage (Verify, Package, Release).
With GitLab CI/CD you can also:
- Easily set up your app's entire lifecycle with [Auto DevOps](../../topics/autodevops/index.md).
-- Deploy your app to different [environments](../environments.md).
+- Deploy your app to different [environments](../environments/index.md).
- Install your own [GitLab Runner](https://docs.gitlab.com/runner/).
- [Schedule pipelines](../pipelines/schedules.md).
- Check for app vulnerabilities with [Security Test reports](../../user/application_security/index.md). **(ULTIMATE)**
@@ -212,7 +215,7 @@ With GitLab CI/CD you can also:
To see all CI/CD features, navigate back to the [CI/CD index](../README.md).
<i class="fa fa-youtube-play youtube" aria-hidden="true"></i>
-Watch the video [GitLab CI Live Demo](https://www.youtube.com/watch?v=pBe4t1CD8Fc) with a deeper overview of GitLab CI/CD.
+Watch the video [GitLab CI Live Demo](https://youtu.be/l5705U8s_nQ?t=369) with a deeper overview of GitLab CI/CD.
### Setting up GitLab CI/CD for the first time
diff --git a/doc/ci/jenkins/index.md b/doc/ci/jenkins/index.md
index 551e32ac816..c4346005138 100644
--- a/doc/ci/jenkins/index.md
+++ b/doc/ci/jenkins/index.md
@@ -13,6 +13,10 @@ First of all, our [Quick Start Guide](../quick_start/README.md) contains a good
You may also be interested in [Auto DevOps](../../topics/autodevops/index.md) which can potentially be used to build, test,
and deploy your applications with little to no configuration needed at all.
+For an example of how to convert a Jenkins pipeline into a GitLab CI/CD pipeline,
+or how to use Auto DevOps to test your code automatically, watch the
+[Migrating from Jenkins to GitLab](https://www.youtube.com/watch?v=RlEVGOpYF5Y) video.
+
For advanced CI/CD teams, [templates](#templates) can enable the reuse of pipeline configurations.
Otherwise, read on for important information that will help you get the ball rolling. Welcome
@@ -40,6 +44,15 @@ things we have found that helps this:
of the improvements that GitLab offers, and this requires (eventually) updating
your implementation as part of the transition.
+## JenkinsFile Wrapper
+
+We are building a [JenkinsFile Wrapper](https://gitlab.com/gitlab-org/jfr-container-builder/) which will allow
+you to run a complete Jenkins instance inside of a GitLab job, including plugins. This can help ease the process
+of transition, by letting you delay the migration of less urgent pipelines for a period of time.
+
+If you are interested, join our [public testing issue](https://gitlab.com/gitlab-org/gitlab/-/issues/215675) to
+If you are interested, you might be able to [help GitLab test the wrapper](https://gitlab.com/gitlab-org/gitlab/-/issues/215675).
+
## Important product differences
There are some high level differences between the products worth mentioning:
@@ -113,7 +126,7 @@ There are some important differences in the way Runners work in comparison to ag
If you are using `gitlab.com`, you can take advantage of our [shared Runner fleet](../../user/gitlab_com/index.md#shared-runners)
to run jobs without provisioning your own Runners. We are investigating making them
-[available for self-managed instances](https://gitlab.com/gitlab-org/customers-gitlab-com/issues/414)
+[available for self-managed instances](https://gitlab.com/groups/gitlab-org/-/epics/835)
as well.
## Groovy vs. YAML
@@ -282,7 +295,7 @@ my_job:
In GitLab, we use the [`variables` keyword](../yaml/README.md#variables) to define different variables at runtime.
These can also be set up through the GitLab UI, under CI/CD settings. See also our [general documentation on variables](../variables/README.md),
-including the section on [protected variables](../variables/README.md#protected-environment-variables) which can be used
+including the section on [protected variables](../variables/README.md#protect-a-custom-variable) which can be used
to limit access to certain variables to certain environments or runners:
```yaml
diff --git a/doc/ci/junit_test_reports.md b/doc/ci/junit_test_reports.md
index c79db6dfbea..a77044e849d 100644
--- a/doc/ci/junit_test_reports.md
+++ b/doc/ci/junit_test_reports.md
@@ -18,6 +18,8 @@ You can configure your job to use JUnit test reports, and GitLab will display a
report on the merge request so that it's easier and faster to identify the
failure without having to check the entire log.
+If you don't use Merge Requests but still want to see the JUnit output without searching through job logs, the full [JUnit test reports](#viewing-junit-test-reports-on-gitlab) are available in the pipeline detail view.
+
## Use cases
Consider the following workflow:
@@ -66,7 +68,7 @@ For a list of supported languages on JUnit tests, check the
[Wikipedia article](https://en.wikipedia.org/wiki/JUnit#Ports).
To enable the JUnit reports in merge requests, you need to add
-[`artifacts:reports:junit`](yaml/README.md#artifactsreportsjunit)
+[`artifacts:reports:junit`](pipelines/job_artifacts.md#artifactsreportsjunit)
in `.gitlab-ci.yml`, and specify the path(s) of the generated test reports.
In the following examples, the job in the `test` stage runs and GitLab
@@ -235,15 +237,44 @@ You can view all the known test suites and click on each of these to see further
details, including the cases that makeup the suite. Cases are ordered by status,
with failed showing at the top, skipped next and successful cases last.
+You can also retrieve the reports via the [GitLab API](../api/pipelines.md#get-a-pipelines-test-report).
+
### Enabling the feature
This feature comes with the `:junit_pipeline_view` feature flag disabled by default. This
feature is disabled due to some performance issues with very large data sets.
When [the performance is improved](https://gitlab.com/groups/gitlab-org/-/epics/2854), the feature will be enabled by default.
-To enable this feature, ask a GitLab administrator with Rails console access to run the
+To enable this feature, ask a GitLab administrator with [Rails console access](../administration/feature_flags.md#how-to-enable-and-disable-features-behind-flags) to run the
following command:
```ruby
Feature.enable(:junit_pipeline_view)
```
+
+## Viewing JUnit screenshots on GitLab
+
+> [Introduced](https://gitlab.com/gitlab-org/gitlab/-/issues/202114) in GitLab 13.0.
+
+If JUnit XML files contain an `attachment` tag, GitLab parses the attachment.
+
+Upload your screenshots as [artifacts](pipelines/job_artifacts.md#artifactsreportsjunit) to GitLab. The `attachment` tag **must** contain the absolute path to the screenshots you uploaded.
+
+```xml
+<testcase time="1.00" name="Test">
+ <system-out>[[ATTACHMENT|/absolute/path/to/some/file]]</system-out>
+</testcase>
+```
+
+When [this issue](https://gitlab.com/gitlab-org/gitlab/-/issues/6061) is complete, the attached file will be visible on the pipeline details page.
+
+### Enabling the feature
+
+This feature comes with the `:junit_pipeline_screenshots_view` feature flag disabled by default.
+
+To enable this feature, ask a GitLab administrator with [Rails console access](../administration/feature_flags.md#how-to-enable-and-disable-features-behind-flags) to run the
+following command:
+
+```ruby
+Feature.enable(:junit_pipeline_screenshots_view)
+```
diff --git a/doc/ci/large_repositories/index.md b/doc/ci/large_repositories/index.md
index 6ac3fa2c92d..b4059fc252b 100644
--- a/doc/ci/large_repositories/index.md
+++ b/doc/ci/large_repositories/index.md
@@ -130,7 +130,7 @@ other using `docker` executor.
### `shell` executor example
-Let's assume that you have the following [config.toml](https://docs.gitlab.com/runner/configuration/advanced-configuration.html).
+Let's assume that you have the following [`config.toml`](https://docs.gitlab.com/runner/configuration/advanced-configuration.html).
```toml
concurrent = 4
@@ -155,7 +155,7 @@ This `config.toml`:
### `docker` executor example
-Let's assume that you have the following [config.toml](https://docs.gitlab.com/runner/configuration/advanced-configuration.html).
+Let's assume that you have the following [`config.toml`](https://docs.gitlab.com/runner/configuration/advanced-configuration.html).
```toml
concurrent = 4
@@ -216,7 +216,7 @@ but this brings administrative overhead as the `.gitlab-ci.yml` needs to be upda
In such cases, it might be desirable to keep the `.gitlab-ci.yml` clone path agnostic, but make it
a configuration of Runner.
-We can extend our [config.toml](https://docs.gitlab.com/runner/configuration/advanced-configuration.html)
+We can extend our [`config.toml`](https://docs.gitlab.com/runner/configuration/advanced-configuration.html)
with the following specification that will be used by Runner if `.gitlab-ci.yml` will not override it:
```toml
diff --git a/doc/ci/merge_request_pipelines/img/merge_request_pipelines_doubled_MR_v12_09.png b/doc/ci/merge_request_pipelines/img/merge_request_pipelines_doubled_MR_v12_09.png
new file mode 100644
index 00000000000..3e4c72b9279
--- /dev/null
+++ b/doc/ci/merge_request_pipelines/img/merge_request_pipelines_doubled_MR_v12_09.png
Binary files differ
diff --git a/doc/ci/merge_request_pipelines/img/merge_request_pipelines_doubled_branch_v12_09.png b/doc/ci/merge_request_pipelines/img/merge_request_pipelines_doubled_branch_v12_09.png
new file mode 100644
index 00000000000..dd70c3f8c20
--- /dev/null
+++ b/doc/ci/merge_request_pipelines/img/merge_request_pipelines_doubled_branch_v12_09.png
Binary files differ
diff --git a/doc/ci/merge_request_pipelines/index.md b/doc/ci/merge_request_pipelines/index.md
index b57340347d2..a724bf416b6 100644
--- a/doc/ci/merge_request_pipelines/index.md
+++ b/doc/ci/merge_request_pipelines/index.md
@@ -13,93 +13,47 @@ changes are pushed to a branch.
If you want the pipeline to run jobs **only** when merge requests are created or updated,
you can use *pipelines for merge requests*.
-In the UI, these pipelines are labeled as `detached`.
+In the UI, these pipelines are labeled as `detached`. Otherwise, these pipelines appear the same
+as other pipelines.
-![Merge request page](img/merge_request.png)
-
-A few notes:
-
-- Pipelines for merge requests are incompatible with
- [CI/CD for external repositories](../ci_cd_for_external_repos/index.md).
-- [Since GitLab 11.10](https://gitlab.com/gitlab-org/gitlab-foss/-/merge_requests/25504), pipelines for merge requests require GitLab Runner 11.9.
-- If you use this feature with [merge when pipeline succeeds](../../user/project/merge_requests/merge_when_pipeline_succeeds.md),
- pipelines for merge requests take precedence over the other regular pipelines.
-
-## Configuring pipelines for merge requests
-
-To configure pipelines for merge requests, configure your [CI/CD configuration file](../yaml/README.md).
-There are a few different ways to do this.
-
-### Enable pipelines for merge requests for all jobs
-
-The recommended method for enabling pipelines for merge requests for all jobs in
-a pipeline is to use [`workflow:rules`](../yaml/README.md#workflowrules).
-
-In this example, the pipeline always runs for all merge requests, as well as for all changes
-to the master branch:
-
-```yaml
-workflow:
- rules:
- - if: $CI_MERGE_REQUEST_ID # Execute jobs in merge request context
- - if: $CI_COMMIT_BRANCH == 'master' # Execute jobs when a new commit is pushed to master branch
+Any user who has developer [permissions](../../user/permissions.md)
+can run a pipeline for merge requests.
-build:
- stage: build
- script: ./build
-
-test:
- stage: test
- script: ./test
+![Merge request page](img/merge_request.png)
-deploy:
- stage: deploy
- script: ./deploy
-```
+NOTE: **Note**:
+If you use this feature with [merge when pipeline succeeds](../../user/project/merge_requests/merge_when_pipeline_succeeds.md),
+pipelines for merge requests take precedence over the other regular pipelines.
-### Enable pipelines for merge requests for specific jobs
+## Prerequisites
-To enable pipelines for merge requests for specific jobs, you can use
-[`rules`](../yaml/README.md#rules).
+To enable pipelines for merge requests:
-In the following example:
+- You must have maintainer [permissions](../../user/permissions.md).
+- Your repository must be a GitLab repository, not an
+ [external repository](../ci_cd_for_external_repos/index.md).
+- [In GitLab 11.10 and later](https://gitlab.com/gitlab-org/gitlab-foss/-/merge_requests/25504),
+ you must be using GitLab Runner 11.9.
-- The `build` job runs for all changes to the `master` branch, as well as for all merge requests.
-- The `test` job runs for all merge requests.
-- The `deploy` job runs for all changes to the `master` branch, but does *not* run
- for merge requests.
+## Configuring pipelines for merge requests
-```yaml
-build:
- stage: build
- script: ./build
- rules:
- - if: $CI_COMMIT_BRANCH == 'master' # Execute jobs when a new commit is pushed to master branch
- - if: $CI_MERGE_REQUEST_ID # Execute jobs in merge request context
+To configure pipelines for merge requests you need to configure your [CI/CD configuration file](../yaml/README.md).
+There are a few different ways to do this:
-test:
- stage: test
- script: ./test
- rules:
- - if: $CI_MERGE_REQUEST_ID # Execute jobs in merge request context
+### Use `rules` to run pipelines for merge requests
-deploy:
- stage: deploy
- script: ./deploy
- rules:
- - if: $CI_COMMIT_BRANCH == 'master' # Execute jobs when a new commit is pushed to master branch
-```
+When using `rules`, which is the preferred method, we recommend starting with one
+of the [`workflow:rules` templates](../yaml/README.md#workflowrules-templates) to ensure
+your basic configuration is correct. Instructions on how to do this, as well as how
+to customize, are available at that link.
### Use `only` or `except` to run pipelines for merge requests
-NOTE: **Note**:
-The [`only` / `except`](../yaml/README.md#onlyexcept-basic) keywords are going to be deprecated
-and you should not use them.
-
-To enable pipelines for merge requests, you can use `only / except`. When you use this method,
-you have to specify `only: - merge_requests` for each job.
+If you want to continue using `only/except`, this is possible but please review the drawbacks
+below.
-In this example, the pipeline contains a `test` job that is configured to run on merge requests.
+When you use this method, you have to specify `only: - merge_requests` for each job. In this
+example, the pipeline contains a `test` job that is configured to run on merge requests.
The `build` and `deploy` jobs don't have the `only: - merge_requests` parameter,
so they will not run on merge requests.
@@ -245,14 +199,24 @@ to integrate your job with [GitLab Merge Request API](../../api/merge_requests.m
You can find the list of available variables in [the reference sheet](../variables/predefined_variables.md).
The variable names begin with the `CI_MERGE_REQUEST_` prefix.
-<!-- ## Troubleshooting
+## Troubleshooting
+
+### Two pipelines created when pushing to a merge request
+
+If you are experiencing duplicated pipelines when using `rules`, take a look at
+the [key details when using `rules`](../yaml/README.md#key-details-when-using-rules),
+which will help you get your starting configuration correct.
+
+If you are seeing two pipelines when using `only/except`, please see the caveats
+related to using `only/except` above (or, consider moving to `rules`).
+
+### Two pipelines created when pushing an invalid CI configuration file
+
+Pushing to a branch with an invalid CI configuration file can trigger
+the creation of two types of failed pipelines. One pipeline is a failed merge request
+pipeline, and the other is a failed branch pipeline, but both are caused by the same
+invalid configuration.
-Include any troubleshooting steps that you can foresee. If you know beforehand what issues
-one might have when setting this up, or when something is changed, or on upgrading, it's
-important to describe those, too. Think of things that may go wrong and include them here.
-This is important to minimize requests for support, and to avoid doc comments with
-questions that you know someone might ask.
+In rare cases, duplicate pipelines are created.
-Each scenario can be a third-level heading, e.g. `### Getting error message X`.
-If you have none to add when creating a doc, leave this section in place
-but commented out to help encourage others to add to it in the future. -->
+See [this issue](https://gitlab.com/gitlab-org/gitlab/-/issues/201845) for details.
diff --git a/doc/ci/merge_request_pipelines/pipelines_for_merged_results/index.md b/doc/ci/merge_request_pipelines/pipelines_for_merged_results/index.md
index fb5c7830ac2..c35a5d0a07e 100644
--- a/doc/ci/merge_request_pipelines/pipelines_for_merged_results/index.md
+++ b/doc/ci/merge_request_pipelines/pipelines_for_merged_results/index.md
@@ -34,14 +34,18 @@ In these cases, the pipeline runs as a [pipeline for merge requests](../index.md
and is labeled as `detached`. If these cases no longer exist, new pipelines will
again run against the merged results.
-## Requirements and limitations
+Any user who has developer [permissions](../../../user/permissions.md) can run a
+pipeline for merged results.
-Pipelines for merged results have the following requirements and limitations:
+## Prerequisites
-- Pipelines for merged results require [GitLab Runner](https://gitlab.com/gitlab-org/gitlab-runner) 11.9 or newer.
-- Forking/cross-repo workflows are not currently supported. To follow progress,
+To enable pipelines for merge results:
+
+- You must have maintainer [permissions](../../../user/permissions.md).
+- You must be using [GitLab Runner](https://gitlab.com/gitlab-org/gitlab-runner) 11.9 or later.
+- You must not be forking or using cross-repo workflows. To follow progress,
see [#11934](https://gitlab.com/gitlab-org/gitlab/issues/11934).
-- This feature is not available for
+- You must not be using
[fast forward merges](../../../user/project/merge_requests/fast_forward_merge.md) yet.
To follow progress, see [#58226](https://gitlab.com/gitlab-org/gitlab/-/issues/26996).
@@ -93,7 +97,6 @@ canceled.
Can be caused by some disabled feature flags. Please make sure that
the following feature flags are enabled on your GitLab instance:
-- `:ci_use_merge_request_ref`
- `:merge_ref_auto_sync`
To check and set these feature flag values, please ask an administrator to:
@@ -107,14 +110,12 @@ To check and set these feature flag values, please ask an administrator to:
1. Check if the flags are enabled or not:
```ruby
- Feature.enabled?(:ci_use_merge_request_ref)
Feature.enabled?(:merge_ref_auto_sync)
```
1. If needed, enable the feature flags:
```ruby
- Feature.enable(:ci_use_merge_request_ref)
Feature.enable(:merge_ref_auto_sync)
```
diff --git a/doc/ci/merge_request_pipelines/pipelines_for_merged_results/merge_trains/index.md b/doc/ci/merge_request_pipelines/pipelines_for_merged_results/merge_trains/index.md
index 641192afea1..d921b75aa44 100644
--- a/doc/ci/merge_request_pipelines/pipelines_for_merged_results/merge_trains/index.md
+++ b/doc/ci/merge_request_pipelines/pipelines_for_merged_results/merge_trains/index.md
@@ -30,6 +30,14 @@ If the pipeline for the merge request at the front of the train completes succes
the changes are merged into the target branch, and the other pipelines continue to
run.
+To add a merge request to a merge train, you need [permissions](../../../../user/permissions.md) to push to the target branch.
+
+NOTE: **Note:**
+Each merge train can run a maximum of **twenty** pipelines in parallel.
+If more than twenty merge requests are added to the merge train, the merge requests
+will be queued until a slot in the merge train is free. There is no limit to the
+number of merge requests that can be queued.
+
## Merge train example
Three merge requests (`A`, `B` and `C`) are added to a merge train in order, which
@@ -55,16 +63,13 @@ Watch this video for a demonstration on [how parallel execution
of Merge Trains can prevent commits from breaking the default
branch](https://www.youtube.com/watch?v=D4qCqXgZkHQ).
-## Requirements and limitations
+## Prerequisites
-Merge trains have the following requirements and limitations:
+To enable merge trains:
-- Merge trains require [GitLab Runner](https://gitlab.com/gitlab-org/gitlab-runner) 11.9 or newer.
-- GitLab 12.0 and later requires [Redis](https://redis.io/) 3.2 or higher.
-- Each merge train can run a maximum of **twenty** pipelines in parallel.
- If more than twenty merge requests are added to the merge train, the merge requests
- will be queued until a slot in the merge train is free. There is no limit to the
- number of merge requests that can be queued.
+- You must have maintainer [permissions](../../../../user/permissions.md).
+- You must be using [GitLab Runner](https://gitlab.com/gitlab-org/gitlab-runner) 11.9 or later.
+- In GitLab 12.0 and later, you need [Redis](https://redis.io/) 3.2 or later.
## Enable merge trains
diff --git a/doc/ci/metrics_reports.md b/doc/ci/metrics_reports.md
index 871f8c55e83..f353aa2670f 100644
--- a/doc/ci/metrics_reports.md
+++ b/doc/ci/metrics_reports.md
@@ -12,7 +12,7 @@ GitLab provides a lot of great reporting tools for [merge requests](../user/proj
You can configure your job to use custom Metrics Reports, and GitLab will display a report on the merge request so that it's easier and faster to identify changes without having to check the entire log.
-![Metrics Reports](img/metrics_reports.png)
+![Metrics Reports](img/metrics_reports_v13_0.png)
## Use cases
@@ -34,7 +34,7 @@ All values are considered strings and string compare is used to find differences
## How to set it up
-Add a job that creates a [metrics report](yaml/README.md#artifactsreportsmetrics-premium) (default filename: `metrics.txt`). The file should conform to the [OpenMetrics](https://openmetrics.io/) format.
+Add a job that creates a [metrics report](pipelines/job_artifacts.md#artifactsreportsmetrics-premium) (default filename: `metrics.txt`). The file should conform to the [OpenMetrics](https://openmetrics.io/) format.
For example:
diff --git a/doc/ci/parent_child_pipelines.md b/doc/ci/parent_child_pipelines.md
index 2bc897901fa..e28adc2bc01 100644
--- a/doc/ci/parent_child_pipelines.md
+++ b/doc/ci/parent_child_pipelines.md
@@ -43,6 +43,9 @@ Child pipelines work well with other GitLab CI/CD features:
All of this will work with the [`include:`](yaml/README.md#include) feature so you can compose
the child pipeline configuration.
+<i class="fa fa-youtube-play youtube" aria-hidden="true"></i>
+For an overview, see [Parent-Child Pipelines feature demo](https://youtu.be/n8KpBSqZNbk).
+
## Examples
The simplest case is [triggering a child pipeline](yaml/README.md#trigger) using a
@@ -136,6 +139,9 @@ your own script to generate a YAML file, which is then [used to trigger a child
This technique can be very powerful in generating pipelines targeting content that changed or to
build a matrix of targets and architectures.
+<i class="fa fa-youtube-play youtube" aria-hidden="true"></i>
+For an overview, see [Create child pipelines using dynamically generated configurations](https://youtu.be/nMdfus2JWHM).
+
In GitLab 12.9, the child pipeline could fail to be created in certain cases, causing the parent pipeline to fail.
This is [resolved in GitLab 12.10](https://gitlab.com/gitlab-org/gitlab/-/issues/209070).
diff --git a/doc/ci/pipelines/img/pipelines_index.png b/doc/ci/pipelines/img/pipelines_index.png
deleted file mode 100644
index e168e7e23df..00000000000
--- a/doc/ci/pipelines/img/pipelines_index.png
+++ /dev/null
Binary files differ
diff --git a/doc/ci/pipelines/img/pipelines_index_v13_0.png b/doc/ci/pipelines/img/pipelines_index_v13_0.png
new file mode 100644
index 00000000000..1dc5ea5d55c
--- /dev/null
+++ b/doc/ci/pipelines/img/pipelines_index_v13_0.png
Binary files differ
diff --git a/doc/ci/pipelines/index.md b/doc/ci/pipelines/index.md
index 27e968c0a40..731cb196eaa 100644
--- a/doc/ci/pipelines/index.md
+++ b/doc/ci/pipelines/index.md
@@ -74,7 +74,7 @@ You can also configure specific aspects of your pipelines through the GitLab UI.
- [Pipeline settings](settings.md) for each project.
- [Pipeline schedules](schedules.md).
-- [Custom CI/CD variables](../variables/README.md#creating-a-custom-environment-variable).
+- [Custom CI/CD variables](../variables/README.md#custom-environment-variables).
### View pipelines
@@ -82,7 +82,7 @@ You can find the current and historical pipeline runs under your project's
**CI/CD > Pipelines** page. You can also access pipelines for a merge request by navigating
to its **Pipelines** tab.
-![Pipelines index page](img/pipelines_index.png)
+![Pipelines index page](img/pipelines_index_v13_0.png)
Clicking a pipeline will bring you to the **Pipeline Details** page and show
the jobs that were run for that pipeline. From here you can cancel a running pipeline,
@@ -93,6 +93,12 @@ latest pipeline for the last commit of a given branch is available at `/project/
Also, `/project/pipelines/latest` will redirect you to the latest pipeline for the last commit
on the project's default branch.
+[Starting in GitLab 13.0](https://gitlab.com/gitlab-org/gitlab/-/issues/215367),
+you can filter the pipeline list by:
+
+- Trigger author
+- Branch name
+
### Run a pipeline manually
Pipelines can be manually executed, with predefined or manually-specified [variables](../variables/README.md).
@@ -153,7 +159,7 @@ You can do this straight from the pipeline graph. Just click the play button
to execute that particular job.
For example, your pipeline might start automatically, but it requires manual action to
-[deploy to production](../environments.md#configuring-manual-deployments). In the example below, the `production`
+[deploy to production](../environments/index.md#configuring-manual-deployments). In the example below, the `production`
stage has a job with a manual action.
![Pipelines example](img/pipelines.png)
@@ -216,7 +222,7 @@ In the example:
Visually, it can be viewed as:
-```text
+```plaintext
0 1 2 3 4 5 6 7
AAAAAAA
BBBBBBB
@@ -225,7 +231,7 @@ Visually, it can be viewed as:
The union of A, B, and C is (1, 4) and (6, 7). Therefore, the total running time is:
-```text
+```plaintext
(4 - 1) + (7 - 6) => 4
```
@@ -343,7 +349,7 @@ build ruby 2/3:
stage: build
script:
- echo "ruby2"
-
+
build ruby 3/3:
stage: build
script:
@@ -367,10 +373,14 @@ evaluates the job names: `\d+[\s:\/\\]+\d+\s*`.
When running manual jobs you can supply additional job specific variables.
You can do this from the job page of the manual job you want to run with
-additional variables.
+additional variables. To access this page, click on the **name** of the manual job in
+the pipeline view, *not* the play (**{play}**) button.
-This is useful when you want to alter the execution of a job by using
-environment variables.
+This is useful when you want to alter the execution of a job that uses
+[custom environment variables](../variables/README.md#custom-environment-variables).
+Adding a variable name (key) and value here will override the value defined in
+[the UI or `.gitlab-ci.yml`](../variables/README.md#custom-environment-variables),
+for a single run of the manual job.
![Manual job variables](img/manual_job_variables.png)
@@ -387,7 +397,7 @@ For example, if you start rolling out new code and:
- Users do not experience trouble, GitLab can automatically complete the deployment from 0% to 100%.
- Users experience trouble with the new code, you can stop the timed incremental rollout by canceling the pipeline
- and [rolling](../environments.md#retrying-and-rolling-back) back to the last stable version.
+ and [rolling](../environments/index.md#retrying-and-rolling-back) back to the last stable version.
![Pipelines example](img/pipeline_incremental_rollout.png)
@@ -545,15 +555,3 @@ To illustrate its life cycle:
even if the commit history of the `example` branch has been overwritten by force-push.
1. GitLab Runner fetches the persistent pipeline ref and gets source code from the checkout-SHA.
1. When the pipeline finished, its persistent ref is cleaned up in a background process.
-
-NOTE: **NOTE**: At this moment, this feature is on by default and can be manually disabled
-by disabling `depend_on_persistent_pipeline_ref` feature flag. If you're interested in
-manually disabling this behavior, please ask the administrator
-to execute the following commands in rails console.
-
-```shell
-> sudo gitlab-rails console # Login to Rails console of GitLab instance.
-> project = Project.find_by_full_path('namespace/project-name') # Get the project instance.
-> Feature.disable(:depend_on_persistent_pipeline_ref, project) # Disable the feature flag for specific project
-> Feature.disable(:depend_on_persistent_pipeline_ref) # Disable the feature flag system-wide
-```
diff --git a/doc/ci/pipelines/job_artifacts.md b/doc/ci/pipelines/job_artifacts.md
index ed791ea9c4a..d4774016d9c 100644
--- a/doc/ci/pipelines/job_artifacts.md
+++ b/doc/ci/pipelines/job_artifacts.md
@@ -6,9 +6,9 @@ type: reference, howto
# Job artifacts
> - Introduced in GitLab 8.2 and GitLab Runner 0.7.0.
-> - Starting with GitLab 8.4 and GitLab Runner 1.0, the artifacts archive format changed to `ZIP`, and it is now possible to browse its contents, with the added ability of downloading the files separately.
+> - Starting with GitLab 8.4 and GitLab Runner 1.0, the artifacts archive format changed to `ZIP`, and it's now possible to browse its contents, with the added ability of downloading the files separately.
> - In GitLab 8.17, builds were renamed to jobs.
-> - The artifacts browser will be available only for new artifacts that are sent to GitLab using GitLab Runner version 1.0 and up. It will not be possible to browse old artifacts already uploaded to GitLab.
+> - The artifacts browser will be available only for new artifacts that are sent to GitLab using GitLab Runner version 1.0 and up. It won't be possible to browse old artifacts already uploaded to GitLab.
Job artifacts are a list of files and directories created by a job
once it finishes. This feature is [enabled by default](../../administration/job_artifacts.md) in all
@@ -34,7 +34,7 @@ pdf:
expire_in: 1 week
```
-A job named `pdf` calls the `xelatex` command in order to build a pdf file from
+A job named `pdf` calls the `xelatex` command in order to build a PDF file from
the latex source file `mycv.tex`. We then define the `artifacts` paths which in
turn are defined with the `paths` keyword. All paths to files and directories
are relative to the repository that was cloned during the build.
@@ -42,28 +42,229 @@ are relative to the repository that was cloned during the build.
The artifacts will be uploaded when the job succeeds by default, but can be set to upload
when the job fails, or always, if the [`artifacts:when`](../yaml/README.md#artifactswhen)
parameter is used. These uploaded artifacts will be kept in GitLab for 1 week as defined
-by the `expire_in` definition. You have the option to keep the artifacts from expiring
+by the `expire_in` definition. You can keep the artifacts from expiring
via the [web interface](#browsing-artifacts). If the expiry time is not defined, it defaults
to the [instance wide setting](../../user/admin_area/settings/continuous_integration.md#default-artifacts-expiration-core-only).
For more examples on artifacts, follow the [artifacts reference in
`.gitlab-ci.yml`](../yaml/README.md#artifacts).
+### `artifacts:reports`
+
+> - [Introduced](https://gitlab.com/gitlab-org/gitlab-foss/-/merge_requests/20390) in GitLab 11.2.
+> - Requires GitLab Runner 11.2 and above.
+
+The `artifacts:reports` keyword is used for collecting test reports, code quality
+reports, and security reports from jobs. It also exposes these reports in GitLab's
+UI (merge requests, pipeline views, and security dashboards).
+
+NOTE: **Note:**
+The test reports are collected regardless of the job results (success or failure).
+You can use [`artifacts:expire_in`](../yaml/README.md#artifactsexpire_in) to set up an expiration
+date for their artifacts.
+
+NOTE: **Note:**
+If you also want the ability to browse the report output files, include the
+[`artifacts:paths`](../yaml/README.md#artifactspaths) keyword.
+
+#### `artifacts:reports:junit`
+
+> - [Introduced](https://gitlab.com/gitlab-org/gitlab-foss/-/merge_requests/20390) in GitLab 11.2.
+> - Requires GitLab Runner 11.2 and above.
+
+The `junit` report collects [JUnit XML files](https://www.ibm.com/support/knowledgecenter/en/SSQ2R2_14.1.0/com.ibm.rsar.analysis.codereview.cobol.doc/topics/cac_useresults_junit.html)
+as artifacts. Although JUnit was originally developed in Java, there are many
+[third party ports](https://en.wikipedia.org/wiki/JUnit#Ports) for other
+languages like JavaScript, Python, Ruby, and so on.
+
+See [JUnit test reports](../junit_test_reports.md) for more details and examples.
+Below is an example of collecting a JUnit XML file from Ruby's RSpec test tool:
+
+```yaml
+rspec:
+ stage: test
+ script:
+ - bundle install
+ - rspec --format RspecJunitFormatter --out rspec.xml
+ artifacts:
+ reports:
+ junit: rspec.xml
+```
+
+The collected JUnit reports will be uploaded to GitLab as an artifact and will
+be automatically shown in merge requests.
+
+NOTE: **Note:**
+In case the JUnit tool you use exports to multiple XML files, you can specify
+multiple test report paths within a single job and they will be automatically
+concatenated into a single file. Use a filename pattern (`junit: rspec-*.xml`),
+an array of filenames (`junit: [rspec-1.xml, rspec-2.xml, rspec-3.xml]`), or a
+combination thereof (`junit: [rspec.xml, test-results/TEST-*.xml]`).
+
+#### `artifacts:reports:dotenv`
+
+> - [Introduced](https://gitlab.com/gitlab-org/gitlab/issues/17066) in GitLab 12.9.
+> - Requires GitLab Runner 11.5 and later.
+
+The `dotenv` report collects a set of environment variables as artifacts.
+
+The collected variables are registered as runtime-created variables of the job,
+which is useful to [set dynamic environment URLs after a job finishes](../environments/index.md#set-dynamic-environment-urls-after-a-job-finishes).
+
+There are a couple of limitations on top of the [original dotenv rules](https://github.com/motdotla/dotenv#rules).
+
+- The variable key can contain only letters, digits and underscore ('_').
+- The size of the dotenv file must be smaller than 5 kilobytes.
+- The number of variables must be less than 10.
+- It does not support variable substitution in the dotenv file itself.
+- It does not support empty lines and comments (`#`) in dotenv file.
+- It does not support quote escape, spaces in a quote, a new line expansion in a quote, in dotenv file.
+
+#### `artifacts:reports:cobertura`
+
+> - [Introduced](https://gitlab.com/gitlab-org/gitlab/issues/3708) in GitLab 12.9.
+> - Requires [GitLab Runner](https://docs.gitlab.com/runner/) 11.5 and above.
+
+The `cobertura` report collects [Cobertura coverage XML files](../../user/project/merge_requests/test_coverage_visualization.md).
+The collected Cobertura coverage reports will be uploaded to GitLab as an artifact
+and will be automatically shown in merge requests.
+
+Cobertura was originally developed for Java, but there are many
+third party ports for other languages like JavaScript, Python, Ruby, and so on.
+
+#### `artifacts:reports:terraform`
+
+> - [Introduced](https://gitlab.com/gitlab-org/gitlab/-/issues/207528) in GitLab 13.0.
+> - Requires [GitLab Runner](https://docs.gitlab.com/runner/) 11.5 and above.
+
+The `terraform` report obtains a Terraform `tfplan.json` file. The collected Terraform
+plan report will be uploaded to GitLab as an artifact and will be automatically shown
+in merge requests.
+
+#### `artifacts:reports:codequality` **(STARTER)**
+
+> - Introduced in GitLab 11.5.
+> - Requires GitLab Runner 11.5 and above.
+
+The `codequality` report collects [CodeQuality issues](../../user/project/merge_requests/code_quality.md)
+as artifacts.
+
+The collected Code Quality report will be uploaded to GitLab as an artifact and will
+be summarized in merge requests.
+
+#### `artifacts:reports:sast` **(ULTIMATE)**
+
+> - Introduced in GitLab 11.5.
+> - Requires GitLab Runner 11.5 and above.
+
+The `sast` report collects [SAST vulnerabilities](../../user/application_security/sast/index.md)
+as artifacts.
+
+The collected SAST report will be uploaded to GitLab as an artifact and will be summarized
+in the merge requests and pipeline view. It's also used to provide data for security
+dashboards.
+
+#### `artifacts:reports:dependency_scanning` **(ULTIMATE)**
+
+> - Introduced in GitLab 11.5.
+> - Requires GitLab Runner 11.5 and above.
+
+The `dependency_scanning` report collects [Dependency Scanning vulnerabilities](../../user/application_security/dependency_scanning/index.md)
+as artifacts.
+
+The collected Dependency Scanning report will be uploaded to GitLab as an artifact and will
+be summarized in the merge requests and pipeline view. It's also used to provide data for security
+dashboards.
+
+#### `artifacts:reports:container_scanning` **(ULTIMATE)**
+
+> - Introduced in GitLab 11.5.
+> - Requires GitLab Runner 11.5 and above.
+
+The `container_scanning` report collects [Container Scanning vulnerabilities](../../user/application_security/container_scanning/index.md)
+as artifacts.
+
+The collected Container Scanning report will be uploaded to GitLab as an artifact and will
+be summarized in the merge requests and pipeline view. It's also used to provide data for security
+dashboards.
+
+#### `artifacts:reports:dast` **(ULTIMATE)**
+
+> - Introduced in GitLab 11.5.
+> - Requires GitLab Runner 11.5 and above.
+
+The `dast` report collects [DAST vulnerabilities](../../user/application_security/dast/index.md)
+as artifacts.
+
+The collected DAST report will be uploaded to GitLab as an artifact and will
+be summarized in the merge requests and pipeline view. It's also used to provide data for security
+dashboards.
+
+#### `artifacts:reports:license_management` **(ULTIMATE)**
+
+> - Introduced in GitLab 11.5.
+> - Requires GitLab Runner 11.5 and above.
+
+CAUTION: **Warning:**
+This artifact is still valid but is **deprecated** in favor of the
+[artifacts:reports:license_scanning](../pipelines/job_artifacts.md#artifactsreportslicense_scanning-ultimate)
+introduced in GitLab 12.8.
+
+The `license_management` report collects [Licenses](../../user/compliance/license_compliance/index.md)
+as artifacts.
+
+The collected License Compliance report will be uploaded to GitLab as an artifact and will
+be summarized in the merge requests and pipeline view. It's also used to provide data for security
+dashboards.
+
+#### `artifacts:reports:license_scanning` **(ULTIMATE)**
+
+> - Introduced in GitLab 12.8.
+> - Requires GitLab Runner 11.5 and above.
+
+The `license_scanning` report collects [Licenses](../../user/compliance/license_compliance/index.md)
+as artifacts.
+
+The License Compliance report will be uploaded to GitLab as an artifact and will
+be automatically shown in merge requests, pipeline view and provide data for security
+dashboards.
+
+#### `artifacts:reports:performance` **(PREMIUM)**
+
+> - Introduced in GitLab 11.5.
+> - Requires GitLab Runner 11.5 and above.
+
+The `performance` report collects [Performance metrics](../../user/project/merge_requests/browser_performance_testing.md)
+as artifacts.
+
+The collected Performance report will be uploaded to GitLab as an artifact and will
+be automatically shown in merge requests.
+
+#### `artifacts:reports:metrics` **(PREMIUM)**
+
+> Introduced in GitLab 11.10.
+
+The `metrics` report collects [Metrics](../metrics_reports.md)
+as artifacts.
+
+The collected Metrics report will be uploaded to GitLab as an artifact and will
+be automatically shown in merge requests.
+
## Browsing artifacts
-> - From GitLab 9.2, PDFs, images, videos and other formats can be previewed directly in the job artifacts browser without the need to download them.
+> - From GitLab 9.2, PDFs, images, videos, and other formats can be previewed directly in the job artifacts browser without the need to download them.
> - Introduced in [GitLab 10.1](https://gitlab.com/gitlab-org/gitlab-foss/-/merge_requests/14399), HTML files in a public project can be previewed directly in a new tab without the need to download them when [GitLab Pages](../../administration/pages/index.md) is enabled. The same applies for textual formats (currently supported extensions: `.txt`, `.json`, and `.log`).
> - Introduced in [GitLab 12.4](https://gitlab.com/gitlab-org/gitlab/-/merge_requests/16675), artifacts in private projects can be previewed when [GitLab Pages access control](../../administration/pages/index.md#access-control) is enabled.
After a job finishes, if you visit the job's specific page, there are three
buttons. You can download the artifacts archive or browse its contents, whereas
-the **Keep** button appears only if you have set an [expiry date](../yaml/README.md#artifactsexpire_in) to the
+the **Keep** button appears only if you've set an [expiry date](../yaml/README.md#artifactsexpire_in) to the
artifacts in case you changed your mind and want to keep them.
![Job artifacts browser button](img/job_artifacts_browser_button.png)
The archive browser shows the name and the actual file size of each file in the
-archive. If your artifacts contained directories, then you are also able to
+archive. If your artifacts contained directories, then you're also able to
browse inside them.
Below you can see what browsing looks like. In this case we have browsed inside
@@ -75,20 +276,20 @@ one HTML file that you can view directly online when
## Downloading artifacts
-If you need to download the whole archive, there are buttons in various places
+If you need to download an artifact or the whole archive, there are buttons in various places
in the GitLab UI to do this:
1. While on the pipelines page, you can see the download icon for each job's
- artifacts archive in the right corner:
+ artifacts and archive in the right corner:
![Job artifacts in Pipelines page](img/job_artifacts_pipelines_page.png)
1. While on the **Jobs** page, you can see the download icon for each job's
- artifacts archive in the right corner:
+ artifacts and archive in the right corner:
![Job artifacts in Builds page](img/job_artifacts_builds_page.png)
-1. While inside a specific job, you are presented with a download button
+1. While inside a specific job, you're presented with a download button
along with the one that browses the archive:
![Job artifacts browser button](img/job_artifacts_browser_button.png)
@@ -100,7 +301,7 @@ in the GitLab UI to do this:
## Downloading the latest artifacts
-It is possible to download the latest artifacts of a job via a well known URL
+It's possible to download the latest artifacts of a job via a well known URL
so you can use it for scripting purposes.
NOTE: **Note:**
@@ -151,7 +352,7 @@ For example:
https://gitlab.com/gitlab-org/gitlab/-/jobs/artifacts/master/browse?job=coverage
```
-There is also a URL to specific files, including html files that
+There is also a URL to specific files, including HTML files that
are shown in [GitLab Pages](../../administration/pages/index.md):
```plaintext
diff --git a/doc/ci/pipelines/schedules.md b/doc/ci/pipelines/schedules.md
index 0ca794c5411..0c0a613c628 100644
--- a/doc/ci/pipelines/schedules.md
+++ b/doc/ci/pipelines/schedules.md
@@ -21,6 +21,15 @@ Pipeline schedules can be used to also run [pipelines](index.md) at specific int
In addition to using the GitLab UI, pipeline schedules can be maintained using the
[Pipeline schedules API](../../api/pipeline_schedules.md).
+## Prerequisites
+
+In order for a scheduled pipeline to be created successfully:
+
+- The schedule owner must have [permissions](../../user/permissions.md) to merge into the target branch.
+- The pipeline configuration must be valid.
+
+Otherwise the pipeline is not created.
+
## Configuring pipeline schedules
To schedule a pipeline for project:
diff --git a/doc/ci/pipelines/settings.md b/doc/ci/pipelines/settings.md
index 3221023f73a..0a859b5b68f 100644
--- a/doc/ci/pipelines/settings.md
+++ b/doc/ci/pipelines/settings.md
@@ -130,6 +130,16 @@ in the jobs table.
A few examples of known coverage tools for a variety of languages can be found
in the pipelines settings page.
+### Download test coverage history
+
+> [Introduced](https://gitlab.com/gitlab-org/gitlab/-/issues/209121) in GitLab 12.10.
+
+If you want to see the evolution of your project code coverage over time,
+you can download a CSV file with this data. From your project:
+
+1. Go to **{chart}** **Project Analytics > Repository**.
+1. Click **Download raw data (.csv)**
+
### Removing color codes
Some test coverage tools output with ANSI color codes that won't be
@@ -162,6 +172,9 @@ This also determines the visibility of these related features:
- Job artifacts
- The [pipeline security dashboard](../../user/application_security/security_dashboard/index.md#pipeline-security) **(ULTIMATE)**
+NOTE: **Note:**
+Currently, job logs and artifacts are [not yet visible for guest users and non-project members](https://gitlab.com/gitlab-org/gitlab/-/issues/25649).
+
If **Public pipelines** is enabled (default):
- For **public** projects, anyone can view the pipelines and related features.
@@ -237,7 +250,7 @@ Depending on the status of your job, a badge can have the following values:
You can access a pipeline status badge image using the following link:
-```text
+```plaintext
https://example.gitlab.com/<namespace>/<project>/badges/<branch>/pipeline.svg
```
@@ -249,7 +262,7 @@ pipeline can have the test coverage percentage value defined.
The test coverage badge can be accessed using following link:
-```text
+```plaintext
https://example.gitlab.com/<namespace>/<project>/badges/<branch>/coverage.svg
```
@@ -268,7 +281,7 @@ Pipeline badges can be rendered in different styles by adding the `style=style_n
#### Flat (default)
-```text
+```plaintext
https://example.gitlab.com/<namespace>/<project>/badges/<branch>/coverage.svg?style=flat
```
@@ -278,7 +291,7 @@ https://example.gitlab.com/<namespace>/<project>/badges/<branch>/coverage.svg?st
> [Introduced](https://gitlab.com/gitlab-org/gitlab-foss/issues/30120) in GitLab 11.8.
-```text
+```plaintext
https://example.gitlab.com/<namespace>/<project>/badges/<branch>/coverage.svg?style=flat-square
```
diff --git a/doc/ci/review_apps/index.md b/doc/ci/review_apps/index.md
index c3bdd524bff..ad0a4270f43 100644
--- a/doc/ci/review_apps/index.md
+++ b/doc/ci/review_apps/index.md
@@ -1,4 +1,7 @@
---
+stage: Release
+group: Progressive Delivery
+info: To determine the technical writer assigned to the Stage/Group associated with this page, see https://about.gitlab.com/handbook/engineering/ux/technical-writing/#designated-technical-writers
type: reference
---
@@ -29,7 +32,7 @@ In the above example:
## How Review Apps work
-A Review App is a mapping of a branch with an [environment](../environments.md).
+A Review App is a mapping of a branch with an [environment](../environments/index.md).
Access to the Review App is made available as a link on the [merge request](../../user/project/merge_requests.md) relevant to the branch.
The following is an example of a merge request with an environment set dynamically.
@@ -49,7 +52,7 @@ After adding Review Apps to your workflow, you follow the branched Git flow. Tha
## Configuring Review Apps
-Review Apps are built on [dynamic environments](../environments.md#configuring-dynamic-environments), which allow you to dynamically create a new environment for each branch.
+Review Apps are built on [dynamic environments](../environments/index.md#configuring-dynamic-environments), which allow you to dynamically create a new environment for each branch.
The process of configuring Review Apps is as follows:
@@ -58,7 +61,7 @@ The process of configuring Review Apps is as follows:
1. Set up a job in `.gitlab-ci.yml` that uses the [predefined CI environment variable](../variables/README.md) `${CI_COMMIT_REF_NAME}`
to create dynamic environments and restrict it to run only on branches.
Alternatively, you can get a YML template for this job by [enabling review apps](#enable-review-apps-button) for your project.
-1. Optionally, set a job that [manually stops](../environments.md#stopping-an-environment) the Review Apps.
+1. Optionally, set a job that [manually stops](../environments/index.md#stopping-an-environment) the Review Apps.
### Enable Review Apps button
@@ -82,7 +85,7 @@ you can copy and paste into `.gitlab-ci.yml` as a starting point. To do so:
## Review Apps auto-stop
-See how to [configure Review Apps environments to expire and auto-stop](../environments.md#environments-auto-stop)
+See how to [configure Review Apps environments to expire and auto-stop](../environments/index.md#environments-auto-stop)
after a given period of time.
## Review Apps examples
@@ -100,7 +103,7 @@ See also the video [Demo: Cloud Native Development with GitLab](https://www.yout
> Introduced in GitLab 8.17. In GitLab 11.5, the file links are available in the merge request widget.
Route Maps allows you to go directly from source files
-to public pages on the [environment](../environments.md) defined for
+to public pages on the [environment](../environments/index.md) defined for
Review Apps.
Once set up, the review app link in the merge request
@@ -205,7 +208,7 @@ if [route maps](#route-maps) are configured in the project.
![review button](img/review_button.png)
-The provided script should be added to the `<head>` of you application and
+The provided script should be added to the `<head>` of your application and
consists of some project and merge request specific values. Here's what it
looks like:
@@ -267,7 +270,7 @@ to your review app.
​After determining the ID for the merge request to link to a visual review app, you
can supply the ID by either:​​
-- Hardcoding it in the script tag via the data attribute `data-merge-request-id` of the app.
+- Hard-coding it in the script tag via the data attribute `data-merge-request-id` of the app.
- Dynamically adding the `data-merge-request-id` value during the build of the app.
- Supplying it manually through the visual review form in the app.
@@ -278,7 +281,7 @@ can supply the ID by either:​​
To enable visual reviews for private and internal projects, set the
[`data-require-auth` variable](#configuring-visual-reviews) to `true`. When enabled,
the user must enter a [personal access token](../../user/profile/personal_access_tokens.md)
-with `read_api` scope before submitting feedback.
+with `api` scope before submitting feedback.
### Using Visual Reviews
@@ -301,4 +304,4 @@ automatically in the respective merge request.
## Limitations
-Review App limitations are the same as [environments limitations](../environments.md#limitations).
+Review App limitations are the same as [environments limitations](../environments/index.md#limitations).
diff --git a/doc/ci/services/mysql.md b/doc/ci/services/mysql.md
index e21b62e93cf..dcfd863709e 100644
--- a/doc/ci/services/mysql.md
+++ b/doc/ci/services/mysql.md
@@ -27,7 +27,7 @@ variables:
NOTE: **Note:**
The `MYSQL_DATABASE` and `MYSQL_ROOT_PASSWORD` variables can't be set in the GitLab UI.
-To set them, assign them to a variable [in the UI](../variables/README.md#via-the-ui),
+To set them, assign them to a variable [in the UI](../variables/README.md#create-a-custom-variable-in-the-ui),
and then assign that variable to the
`MYSQL_DATABASE` and `MYSQL_ROOT_PASSWORD` variables in your `.gitlab-ci.yml`.
diff --git a/doc/ci/services/postgres.md b/doc/ci/services/postgres.md
index cf34c28497e..2f92bd969ff 100644
--- a/doc/ci/services/postgres.md
+++ b/doc/ci/services/postgres.md
@@ -29,7 +29,7 @@ variables:
NOTE: **Note:**
The `POSTGRES_DB`, `POSTGRES_USER`, `POSTGRES_PASSWORD` and `POSTGRES_HOST_AUTH_METHOD`
variables can't be set in the GitLab UI. To set them, assign them to a variable
-[in the UI](../variables/README.md#via-the-ui), and then assign that
+[in the UI](../variables/README.md#create-a-custom-variable-in-the-ui), and then assign that
variable to the `POSTGRES_DB`, `POSTGRES_USER`, `POSTGRES_PASSWORD` and `POSTGRES_HOST_AUTH_METHOD`
variables in your `.gitlab-ci.yml`.
diff --git a/doc/ci/variables/README.md b/doc/ci/variables/README.md
index 99fbc2134a4..3e31a2169e2 100644
--- a/doc/ci/variables/README.md
+++ b/doc/ci/variables/README.md
@@ -4,149 +4,41 @@ type: reference
# GitLab CI/CD environment variables
-After a brief overview of the use of environment
-variables, this document teaches you how to use GitLab CI/CD's
-variables, presents the full reference for predefined variables,
-and dives into more advanced applications.
-
-## Overview
-
-An environment variable is a dynamic-named value that can
-affect the way running processes will behave on an operating
+An environment variable is a dynamically-named value that can
+affect the way running processes behave on an operating
system.
-They are part of the environment in which a process runs.
+Environment variables are part of the environment in which a process runs.
For example, a running process can query the value of the
`TEMP` environment variable to discover a suitable location
to store temporary files, or to define a `URL` for a database
that can be reused in different scripts.
-Variables are useful for customizing your jobs in GitLab
-CI/CD's pipelines. Using variables means no hardcoded values.
+Variables are useful for customizing your jobs in GitLab CI/CD.
+When you use variables, you don't have to hard-code values.
-### Predefined environment variables
+## Predefined environment variables
GitLab CI/CD has a [default set of predefined variables](predefined_variables.md)
-which can be used without any specification needed.
-You can call issues numbers, user names, branch names,
+that you can use without any additional specification.
+You can call issue numbers, user names, branch names,
pipeline and commit IDs, and much more.
-Predefined environment variables are the ones that GitLab
-provides out of the box for the local environment of the Runner.
-
-GitLab reads the `.gitlab-ci.yml` file, sends the information
-to the Runner (which runs the script commands), under which
-the variables are exposed.
-
-For example, two jobs under the same pipeline can share the same
-`CI_PIPELINE_ID` variable, but each one has its own `CI_JOB_ID`
-variable.
-
-NOTE: **Note:**
-Find here the full [**predefined variables reference table**](predefined_variables.md).
-
-### Custom environment variables
-
-When your use case requires a specific variable, you can
-[set them up easily from the UI](#creating-a-custom-environment-variable)
-or directly in the `.gitlab-ci.yml` file and reuse them as you wish.
-
-That can be very powerful as it can be used for scripting without
-the need to specify the value itself.
-
-#### Types of variables
-
-> [Introduced](https://gitlab.com/gitlab-org/gitlab-foss/issues/46806) in GitLab 11.11.
-
-There are two types of variables supported by GitLab:
-
-- [Variable type](#variable-type): The Runner will create an environment variable named the same as the
- variable key and set its value to the variable value.
-- [File type](#file-type): The Runner will write the variable value to a temporary file and set the
- path to this file as the value of an environment variable, named the same as the variable key.
-
-##### Variable type
-
-Many tools (like [AWS CLI](https://docs.aws.amazon.com/cli/latest/userguide/cli-configure-envvars.html)
-and [kubectl](https://kubernetes.io/docs/concepts/configuration/organize-cluster-access-kubeconfig/#the-kubeconfig-environment-variable))
-provide the ability to customise configuration using files by either providing the
-file path as a command line argument or an environment variable. In the past, the
-common pattern was to read the value of a CI variable, save it in a file, and then
-use the newly created file in your script:
-
-```shell
-# Read certificate stored in $KUBE_CA_PEM variable and save it in a new file
-echo "$KUBE_CA_PEM" > "$(pwd)/kube.ca.pem"
-# Pass the newly created file to kubectl
-kubectl config set-cluster e2e --server="$KUBE_URL" --certificate-authority="$(pwd)/kube.ca.pem"
-```
-
-There are [some predefined variables](#custom-variables-validated-by-gitlab) of this type, which may be further validated. They will appear when you add or update a variable.
-
-##### File type
-
-The example above can now be simplified by creating a "File" type variable, and using
-it directly. For example, let's say we have the following variables:
-
-![CI/CD settings - variable types usage example](img/variable_types_usage_example.png)
-
-We can then call them from `.gitlab-ci.yml` like this:
-
-```shell
-kubectl config set-cluster e2e --server="$KUBE_URL" --certificate-authority="$KUBE_CA_PEM"
-```
-
-Variable types can be set via the [UI](#via-the-ui) or the [API](../../api/project_level_variables.md#create-variable), but not in `.gitlab-ci.yml`.
-
-#### Masked variables
-
-> [Introduced](https://gitlab.com/gitlab-org/gitlab-foss/issues/13784) in GitLab 11.10
-
-Variables can be created as masked variables.
-This means that the value of the variable will be hidden in job logs,
-though it must match certain requirements to do so:
-
-- The value must be in a single line.
-- The value must only consist of characters from the Base64 alphabet (RFC4648).
-
- [In GitLab 12.2](https://gitlab.com/gitlab-org/gitlab-foss/issues/63043)
- and newer, `@` and `:` are also valid values.
-- The value must be at least 8 characters long.
-- The value must not use variables.
-
-If the value does not meet the requirements above, then the CI variable will fail to save.
-In order to save, either alter the value to meet the masking requirements
-or disable **Masked** for the variable.
-
-#### Custom variables validated by GitLab
-
-Some variables are listed in the UI so you can choose them more quickly.
-GitLab validates the values of these variables to ensure they are in the correct format.
-
-| Variable | Allowed Values | Introduced in |
-|-------------------------|----------------------------------------------------|---------------|
-| `AWS_ACCESS_KEY_ID` | 20 characters: letters, digits | 12.10 |
-| `AWS_DEFAULT_REGION` | Any | 12.10 |
-| `AWS_SECRET_ACCESS_KEY` | 40 characters: letters, digits, special characters | 12.10 |
+Predefined environment variables are provided by GitLab
+for the local environment of the Runner.
-NOTE: **Note:**
-When you store credentials, there are security implications. If you are using AWS keys,
-for example, follow their [best practices](https://docs.aws.amazon.com/general/latest/gr/aws-access-keys-best-practices.html).
+GitLab reads the `.gitlab-ci.yml` file and sends the information
+to the Runner, where the variables are exposed. The Runner then runs the script commands.
-## Getting started
+### Use predefined environment variables
-To get started with environment variables in the scope of GitLab
-CI/CD, let's go over a few examples.
+You can choose one of the existing predefined variables
+to be output by the Runner.
-### Using predefined environment variables
+This example shows how to output a job's stage by using the predefined variable `CI_JOB_STAGE`.
-To get started, choose one of the existing
-[predefined variables](predefined_variables.md)
-to be output by the Runner. For example, let's say that you want
-a given job you're running through your script to output the
-stage that job is running for. In your `.gitlab-ci.yml` file,
-call the variable from your script according to the [syntaxes](#syntax-of-environment-variables-in-job-scripts) available. To
-output the job stage, use the predefined variable `CI_JOB_STAGE`:
+In your `.gitlab-ci.yml` file, call the variable from your script. Ensure
+you use the correct [syntax](#syntax-of-environment-variables-in-job-scripts).
```yaml
test_variable:
@@ -155,14 +47,14 @@ test_variable:
- echo $CI_JOB_STAGE
```
-For this case, the Runner will output the `stage` for the
+In this case, the Runner outputs the `stage` for the
job `test_variable`, which is `test`:
![Output `$CI_JOB_STAGE`](img/ci_job_stage_output_example.png)
As another example, let's say you're using your own GitLab
-instance you want to know what domain your GitLab Pages are
-served under. You can easily call it with the predefined
+instance and you want to know what domain your GitLab Pages are
+served under. You can call it by using the predefined
variable `$CI_PAGES_DOMAIN` in your script:
```yaml
@@ -176,47 +68,54 @@ For GitLab.com users, the output will be `gitlab.io`. For your
private instance, the output will be whatever your sysadmin has
defined.
-### Creating a custom environment variable
+## Custom environment variables
-Assume you have something you want to repeat through your scripts
-in GitLab CI/CD's configuration file. To keep this example simple,
-let's say you want to output `HELLO WORLD` for a `TEST` variable.
+When you need a specific custom environment variable, you can
+[set it up in the UI](#create-a-custom-variable-in-the-ui), in [the API](../../api/project_level_variables.md),
+or directly [in the `.gitlab-ci.yml` file](#create-a-custom-variable-in-gitlab-ciyml).
-You can either set the variable directly in the `.gitlab-ci.yml`
-file or through the UI.
+The variables are used by the Runner any time the pipeline runs.
+You can also [override variable values manually for a specific pipeline](../pipelines/index.md#specifying-variables-when-running-manual-jobs).
-NOTE: **Note:**
-It is possible to [specify variables when running manual jobs](../pipelines/index.md#specifying-variables-when-running-manual-jobs).
+There are two types of variables: **Variable** and **File**. You cannot set types in
+the `.gitlab-ci.yml` file, but you can set them in the UI and API.
-#### Via `.gitlab-ci.yml`
+### Create a custom variable in `.gitlab-ci.yml`
-To create a new custom `env_var` variable via [`.gitlab-ci.yml`](../yaml/README.md#variables), define their variable/value pair under
-`variables`:
+To create a custom `env_var` variable in the [`.gitlab-ci.yml`](../yaml/README.md#variables) file,
+define the variable/value pair under `variables`:
```yaml
variables:
TEST: "HELLO WORLD"
```
-For a deeper look into them, see [`.gitlab-ci.yml` defined variables](#gitlab-ciyml-defined-variables).
+You can then call its value in your script:
+
+```yaml
+ script:
+ - echo "$TEST"
+```
+
+For more details, see [`.gitlab-ci.yml` defined variables](#gitlab-ciyml-defined-variables).
-#### Via the UI
+### Create a custom variable in the UI
From within the UI, you can add or update custom environment variables:
1. Go to your project's **Settings > CI/CD** and expand the **Variables** section.
-1. Click the **Add variable** button. In the **Add variable** modal, fill in the details:
+1. Click the **Add Variable** button. In the **Add variable** modal, fill in the details:
- **Key**: Must be one line, with no spaces, using only letters, numbers, `-` or `_`.
- **Value**: No limitations.
- **Type**: `File` or `Variable`.
- **Environment scope**: `All`, or specific environments.
- **Protect variable** (Optional): If selected, the variable will only be available in pipelines that run on protected branches or tags.
- - **Mask variable** (Optional): If selected, the variable's **Value** will be masked in job logs. The variable will fail to save if the value does not meet the [masking requirements](#masked-variables).
+ - **Mask variable** (Optional): If selected, the variable's **Value** will be masked in job logs. The variable fails to save if the value does not meet the [masking requirements](#masked-variable-requirements).
-After a variable is created, you can update any of the details by clicking on the **{pencil}** **Edit** button.
+After a variable is created, you can update any of the details by clicking the **{pencil}** **Edit** button.
-Once you've set the variables, call them from the `.gitlab-ci.yml` file:
+After you set a variable, call it from the `.gitlab-ci.yml` file:
```yaml
test_variable:
@@ -232,7 +131,110 @@ The output will be:
![Output custom variable](img/custom_variables_output.png)
-### Syntax of environment variables in job scripts
+### Custom environment variables of type Variable
+
+> [Introduced](https://gitlab.com/gitlab-org/gitlab-foss/issues/46806) in GitLab 11.11.
+
+For variables with the type **Variable**, the Runner creates an environment variable
+that uses the key for the name and the value for the value.
+
+There are [some predefined variables](#custom-variables-validated-by-gitlab) of this type,
+which may be further validated. They appear when you add or update a variable in the UI.
+
+### Custom environment variables of type File
+
+> [Introduced](https://gitlab.com/gitlab-org/gitlab-foss/issues/46806) in GitLab 11.11.
+
+For variables with the type **File**, the Runner creates an environment variable that uses the key for the name.
+For the value, the Runner writes the variable value to a temporary file and uses this path.
+
+You can use tools like [the AWS CLI](https://docs.aws.amazon.com/cli/latest/userguide/cli-configure-envvars.html)
+and [kubectl](https://kubernetes.io/docs/concepts/configuration/organize-cluster-access-kubeconfig/#the-kubeconfig-environment-variable)
+to customize your configuration by using **File** type variables.
+
+In the past, a common pattern was to read the value of a CI variable, save it in a file, and then
+use the newly created file in your script:
+
+```shell
+# Read certificate stored in $KUBE_CA_PEM variable and save it in a new file
+echo "$KUBE_CA_PEM" > "$(pwd)/kube.ca.pem"
+# Pass the newly created file to kubectl
+kubectl config set-cluster e2e --server="$KUBE_URL" --certificate-authority="$(pwd)/kube.ca.pem"
+```
+
+Instead of this, you can use a **File** type variable. For example, if you have the following variables:
+
+- A variable of type **Variable**: `KUBE_URL` with the value `https://example.com`.
+- A variable of type **File**: `KUBE_CA_PEM` with a certificate as the value.
+
+You can call them from `.gitlab-ci.yml`, like this:
+
+```shell
+kubectl config set-cluster e2e --server="$KUBE_URL" --certificate-authority="$KUBE_CA_PEM"
+```
+
+### Mask a custom variable
+
+> [Introduced](https://gitlab.com/gitlab-org/gitlab-foss/issues/13784) in GitLab 11.10
+
+Variables can be masked so that the value of the variable will be hidden in job logs.
+
+To mask a variable:
+
+1. Go to **Settings > CI/CD**.
+1. Expand the **Variables** section.
+1. Next to the variable you want to protect, click **Edit**.
+1. Select the **Mask variable** check box.
+1. Click **Update variable**.
+
+#### Masked variable requirements
+
+The value of the variable must:
+
+- Be in a single line.
+- Be at least 8 characters long.
+- Not be a predefined or custom environment variable.
+- Consist only of characters from the Base64 alphabet (RFC4648).
+ [In GitLab 12.2](https://gitlab.com/gitlab-org/gitlab-foss/issues/63043)
+ and newer, `@` and `:` are also valid values.
+
+You can't mask variables that don't meet these requirements.
+
+### Protect a custom variable
+
+> Introduced in GitLab 9.3.
+
+Variables can be protected. When a variable is
+protected, it is securely passed to pipelines running on
+[protected branches](../../user/project/protected_branches.md) or [protected tags](../../user/project/protected_tags.md) only. The other pipelines do not get
+the protected variable.
+
+To protect a variable:
+
+1. Go to **Settings > CI/CD**.
+1. Expand the **Variables** section.
+1. Next to the variable you want to protect, click **Edit**.
+1. Select the **Protect variable** check box.
+1. Click **Update variable**.
+
+The variable is available for all subsequent pipelines.
+
+### Custom variables validated by GitLab
+
+Some variables are listed in the UI so you can choose them more quickly.
+GitLab validates the values of these variables to ensure they are in the correct format.
+
+| Variable | Allowed Values | Introduced in |
+|-------------------------|----------------------------------------------------|---------------|
+| `AWS_ACCESS_KEY_ID` | 20 characters: letters, digits | 12.10 |
+| `AWS_DEFAULT_REGION` | Any | 12.10 |
+| `AWS_SECRET_ACCESS_KEY` | 40 characters: letters, digits, special characters | 12.10 |
+
+NOTE: **Note:**
+When you store credentials, there are security implications. If you are using AWS keys,
+for example, follow their [best practices](https://docs.aws.amazon.com/general/latest/gr/aws-access-keys-best-practices.html).
+
+## Syntax of environment variables in job scripts
All variables are set as environment variables in the build environment, and
they are accessible with normal methods that are used to access such variables.
@@ -329,14 +331,14 @@ export GITLAB_USER_EMAIL="user@example.com"
export GITLAB_USER_ID="42"
```
-### `.gitlab-ci.yml` defined variables
+## `.gitlab-ci.yml` defined variables
NOTE: **Note:**
This feature requires GitLab Runner 0.5.0 or higher and GitLab 7.14 or higher.
-GitLab CI/CD allows you to add to `.gitlab-ci.yml` variables that are set in the
-build environment. The variables are hence saved in the repository, and they
-are meant to store non-sensitive project configuration. For example, `RAILS_ENV` or
+You can add variables that are set in the build environment to `.gitlab-ci.yml`.
+These variables are saved in the repository, and they
+are meant to store non-sensitive project configuration, like `RAILS_ENV` or
`DATABASE_URL`.
For example, if you set the variable below globally (not inside a job), it will
@@ -348,7 +350,7 @@ variables:
```
The YAML-defined variables are also set to all created
-[service containers](../docker/using_docker_images.md), thus allowing to fine
+[service containers](../docker/using_docker_images.md), so that you can fine
tune them.
Variables can be defined at a global level, but also at a job level. To turn off
@@ -369,11 +371,11 @@ script:
- 'eval $LS_CMD' # will execute 'ls -al $TMP_DIR'
```
-### Group-level environment variables
+## Group-level environment variables
> Introduced in GitLab 9.4.
-GitLab CI/CD allows you to define per-project or per-group variables
+You can define per-project or per-group variables
that are set in the pipeline environment. Group-level variables are stored out of
the repository (not in `.gitlab-ci.yml`) and are securely passed to GitLab Runner
making them available during a pipeline run. It's the **recommended method** to
@@ -382,7 +384,7 @@ use for storing things like passwords, SSH keys, and credentials.
Group-level variables can be added by:
1. Navigating to your group's **Settings > CI/CD** page.
-1. Inputing variable types, keys, and values in the **Variables** section.
+1. Inputting variable types, keys, and values in the **Variables** section.
Any variables of [subgroups](../../user/group/subgroups/index.md) will be inherited recursively.
Once you set them, they will be available for all subsequent pipelines. Any group-level user defined variables can be viewed in projects by:
@@ -392,6 +394,73 @@ Once you set them, they will be available for all subsequent pipelines. Any grou
![CI/CD settings - inherited variables](img/inherited_group_variables_v12_5.png)
+### Inherit environment variables
+
+> - [Introduced](https://gitlab.com/gitlab-org/gitlab/-/issues/22638) in GitLab 13.0.
+> - It's deployed behind a feature flag (`ci_dependency_variables`), disabled by default.
+
+You can inherit environment variables from dependent jobs.
+
+This feature makes use of the [`artifacts:reports:dotenv`](../pipelines/job_artifacts.md#artifactsreportsdotenv) report feature.
+
+Example with [`dependencies`](../yaml/README.md#dependencies) keyword.
+
+```yaml
+build:
+ stage: build
+ script:
+ - echo "BUILD_VERSION=hello" >> build.env
+ artifacts:
+ reports:
+ dotenv: build.env
+
+deploy:
+ stage: deploy
+ script:
+ - echo $BUILD_VERSION # => hello
+ dependencies:
+ - build
+```
+
+Example with the [`needs`](../yaml/README.md#artifact-downloads-with-needs) keyword:
+
+```yaml
+build:
+ stage: build
+ script:
+ - echo "BUILD_VERSION=hello" >> build.env
+ artifacts:
+ reports:
+ dotenv: build.env
+
+deploy:
+ stage: deploy
+ script:
+ - echo $BUILD_VERSION # => hello
+ needs:
+ - job: build
+ artifacts: true
+```
+
+### Enable inherited environment variables **(CORE ONLY)**
+
+The Inherited Environment Variables feature is under development and not ready for production use. It is
+deployed behind a feature flag that is **disabled by default**.
+[GitLab administrators with access to the GitLab Rails console](../../administration/feature_flags.md)
+can enable it for your instance.
+
+To enable it:
+
+```ruby
+Feature.enable(:ci_dependency_variables)
+```
+
+To disable it:
+
+```ruby
+Feature.disable(:ci_dependency_variables)
+```
+
## Priority of environment variables
Variables of different types can take precedence over other
@@ -400,8 +469,9 @@ variables, depending on where they are defined.
The order of precedence for variables is (from highest to lowest):
1. [Trigger variables](../triggers/README.md#making-use-of-trigger-variables) or [scheduled pipeline variables](../pipelines/schedules.md#using-variables).
-1. Project-level [variables](#creating-a-custom-environment-variable) or [protected variables](#protected-environment-variables).
-1. Group-level [variables](#group-level-environment-variables) or [protected variables](#protected-environment-variables).
+1. Project-level [variables](#custom-environment-variables) or [protected variables](#protect-a-custom-variable).
+1. Group-level [variables](#group-level-environment-variables) or [protected variables](#protect-a-custom-variable).
+1. [Inherited environment variables](#inherit-environment-variables).
1. YAML-defined [job-level variables](../yaml/README.md#variables).
1. YAML-defined [global variables](../yaml/README.md#variables).
1. [Deployment variables](#deployment-environment-variables).
@@ -426,27 +496,12 @@ Click [here](where_variables_can_be_used.md) for a section that describes where
## Advanced use
-### Protected environment variables
-
-> Introduced in GitLab 9.3.
-
-Variables can be protected. Whenever a variable is
-protected, it would only be securely passed to pipelines running on the
-[protected branches](../../user/project/protected_branches.md) or [protected tags](../../user/project/protected_tags.md). The other pipelines would not get any
-protected variables.
-
-Protected variables can be added by going to your project's
-**Settings > CI/CD**, then finding the section called
-**Variables**, and check "Protected".
-
-Once you set them, they will be available for all subsequent pipelines.
-
-### Limiting environment scopes of environment variables
+### Limit the environment scopes of environment variables
You can limit the environment scope of a variable by
-[defining which environments](../environments.md) it can be available for.
+[defining which environments](../environments/index.md) it can be available for.
-To learn more about scoping environments, see [Scoping environments with specs](../environments.md#scoping-environments-with-specs).
+To learn more about scoping environments, see [Scoping environments with specs](../environments/index.md#scoping-environments-with-specs).
### Deployment environment variables
@@ -455,7 +510,7 @@ To learn more about scoping environments, see [Scoping environments with specs](
[Integrations](../../user/project/integrations/overview.md) that are
responsible for deployment configuration may define their own variables that
are set in the build environment. These variables are only defined for
-[deployment jobs](../environments.md). Please consult the documentation of
+[deployment jobs](../environments/index.md). Please consult the documentation of
the integrations that you are using to learn which variables they define.
An example integration that defines deployment variables is the
@@ -478,22 +533,23 @@ CAUTION: **Caution:**
Variables with multiline values are not currently supported due to
limitations with the current Auto DevOps scripting environment.
-### Environment variables triggered manually
+### Override a variable by manually running a pipeline
> [Introduced](https://gitlab.com/gitlab-org/gitlab-foss/issues/44059) in GitLab 10.8.
-[Manually triggered pipelines](../pipelines/index.md#run-a-pipeline-manually) allow you to override the value of a current variable.
+You can override the value of a current variable by
+[running a pipeline manually](../pipelines/index.md#run-a-pipeline-manually).
+
+For instance, suppose you added a custom variable named `$TEST`
+and you want to override it in a manual pipeline.
-For instance, suppose you added a
-[custom variable `$TEST`](#creating-a-custom-environment-variable)
-as exemplified above and you want to override it in a manual pipeline.
Navigate to your project's **CI/CD > Pipelines** and click **Run pipeline**.
-Choose the branch you want to run the pipeline for, then add a new variable through the UI:
+Choose the branch you want to run the pipeline for, then add a variable and its value in the UI:
![Override variable value](img/override_variable_manual_pipeline.png)
-The Runner will override the value previously set and use the custom
-value you set for this specific pipeline:
+The Runner overrides the value previously set and uses the custom
+value for this specific pipeline.
![Manually overridden variable output](img/override_value_via_manual_pipeline_output.png)
@@ -502,10 +558,10 @@ value you set for this specific pipeline:
> - [Introduced](https://gitlab.com/gitlab-org/gitlab-foss/issues/37397) in GitLab 10.7 for [the `only` and `except` CI keywords](../yaml/README.md#onlyexcept-advanced)
> - [Expanded](https://gitlab.com/gitlab-org/gitlab/issues/27863) in GitLab 12.3 with [the `rules` keyword](../yaml/README.md#rules)
-Variable expressions can be used to limit what jobs are going to be created
-within a pipeline after pushing changes to GitLab.
+Use variable expressions to limit which jobs are created
+within a pipeline after changes are pushed to GitLab.
-In `.gitlab-ci.yml`, they work with both
+In `.gitlab-ci.yml`, variable expressions work with both:
- [`rules`](../yaml/README.md#rules), which is the recommended approach, and
- [`only` and `except`](../yaml/README.md#onlyexcept-basic), which are candidates for deprecation.
@@ -523,15 +579,15 @@ deploy:
- $STAGING
```
-Each expression provided is going to be evaluated before creating a pipeline.
+Each expression provided is evaluated before a pipeline is created.
-If any of the conditions in `variables` evaluates to truth when using `only`,
-a new job is going to be created. If any of the expressions evaluates to truth
-when `except` is being used, a job is not going to be created.
+If any of the conditions in `variables` evaluates to true when using `only`,
+a new job is created. If any of the expressions evaluates to true
+when `except` is being used, a job is not created.
-This follows usual rules for [`only` / `except` policies](../yaml/README.md#onlyexcept-advanced).
+This follows the usual rules for [`only` / `except` policies](../yaml/README.md#onlyexcept-advanced).
-### Supported syntax
+### Syntax of environment variable expressions
Below you can find supported syntax reference:
@@ -679,7 +735,7 @@ If a job isn't working as expected, this can make the problem difficult to
investigate; in these cases, you can enable debug tracing in `.gitlab-ci.yml`.
Available on GitLab Runner v1.7+, this feature enables the shell's execution
log, resulting in a verbose job log listing all commands that were run,
-variables that were set, etc.
+variables that were set, and so on.
Before enabling this, you should ensure jobs are visible to
[team members only](../../user/permissions.md#project-features). You should
@@ -864,3 +920,10 @@ if [[ -d "/builds/gitlab-examples/ci-debug-trace/.git" ]]; then
...
```
+
+## Video walkthrough of a working example
+
+The [Managing the Complex Configuration Data Management Monster Using GitLab](https://www.youtube.com/watch?v=v4ZOJ96hAck) video is a walkthrough of the [Complex Config Data Monorepo](https://gitlab.com/guided-explorations/config-data-top-scope/config-data-subscope/config-data-monorepo) working example project. It explains how multiple levels of group CI/CD variables can be combined with environment-scoped project variables for complex configuration of application builds or deployments.
+
+The example can be copied to your own group or instance for testing. More details
+on what other GitLab CI patterns are demonstrated are available at the project page.
diff --git a/doc/ci/variables/img/ci_job_stage_output_example.png b/doc/ci/variables/img/ci_job_stage_output_example.png
index 056238d5693..e333da57121 100644
--- a/doc/ci/variables/img/ci_job_stage_output_example.png
+++ b/doc/ci/variables/img/ci_job_stage_output_example.png
Binary files differ
diff --git a/doc/ci/variables/img/inherited_group_variables_v12_5.png b/doc/ci/variables/img/inherited_group_variables_v12_5.png
index fd41859605f..a13ba711083 100644
--- a/doc/ci/variables/img/inherited_group_variables_v12_5.png
+++ b/doc/ci/variables/img/inherited_group_variables_v12_5.png
Binary files differ
diff --git a/doc/ci/variables/img/override_value_via_manual_pipeline_output.png b/doc/ci/variables/img/override_value_via_manual_pipeline_output.png
index 02369d57fb8..8a13bb3849e 100644
--- a/doc/ci/variables/img/override_value_via_manual_pipeline_output.png
+++ b/doc/ci/variables/img/override_value_via_manual_pipeline_output.png
Binary files differ
diff --git a/doc/ci/variables/img/override_variable_manual_pipeline.png b/doc/ci/variables/img/override_variable_manual_pipeline.png
index c77c5cb7764..de768105aec 100644
--- a/doc/ci/variables/img/override_variable_manual_pipeline.png
+++ b/doc/ci/variables/img/override_variable_manual_pipeline.png
Binary files differ
diff --git a/doc/ci/variables/img/variable_types_usage_example.png b/doc/ci/variables/img/variable_types_usage_example.png
deleted file mode 100644
index c2ae32fd048..00000000000
--- a/doc/ci/variables/img/variable_types_usage_example.png
+++ /dev/null
Binary files differ
diff --git a/doc/ci/variables/predefined_variables.md b/doc/ci/variables/predefined_variables.md
index f53fd371c10..d4d3a13bb2a 100644
--- a/doc/ci/variables/predefined_variables.md
+++ b/doc/ci/variables/predefined_variables.md
@@ -56,7 +56,7 @@ future GitLab releases.**
| `CI_EXTERNAL_PULL_REQUEST_SOURCE_BRANCH_SHA` | 12.3 | all | The HEAD SHA of the source branch of the pull request if [the pipelines are for external pull requests](../ci_cd_for_external_repos/index.md#pipelines-for-external-pull-requests). Available only if `only: [external_pull_requests]` or [`rules`](../yaml/README.md#rules) syntax is used and the pull request is open. |
| `CI_EXTERNAL_PULL_REQUEST_TARGET_BRANCH_NAME` | 12.3 | all | The target branch name of the pull request if [the pipelines are for external pull requests](../ci_cd_for_external_repos/index.md#pipelines-for-external-pull-requests). Available only if `only: [external_pull_requests]` or [`rules`](../yaml/README.md#rules) syntax is used and the pull request is open. |
| `CI_EXTERNAL_PULL_REQUEST_TARGET_BRANCH_SHA` | 12.3 | all | The HEAD SHA of the target branch of the pull request if [the pipelines are for external pull requests](../ci_cd_for_external_repos/index.md#pipelines-for-external-pull-requests). Available only if `only: [external_pull_requests]` or [`rules`](../yaml/README.md#rules) syntax is used and the pull request is open. |
-| `CI_JOB_ID` | 9.0 | all | The unique id of the current job that GitLab CI/CD uses internally |
+| `CI_JOB_ID` | 9.0 | all | The unique ID of the current job that GitLab CI/CD uses internally |
| `CI_JOB_IMAGE` | 12.9 | 12.9 | The name of the image running the CI job |
| `CI_JOB_MANUAL` | 8.12 | all | The flag to indicate that job was manually started |
| `CI_JOB_NAME` | 9.0 | 0.5 | The name of the job as defined in `.gitlab-ci.yml` |
@@ -64,6 +64,7 @@ future GitLab releases.**
| `CI_JOB_TOKEN` | 9.0 | 1.2 | Token used for authenticating with the [GitLab Container Registry](../../user/packages/container_registry/index.md) and downloading [dependent repositories](../../user/project/new_ci_build_permissions_model.md#dependent-repositories) |
| `CI_JOB_JWT` | 12.10 | all | RS256 JSON web token that can be used for authenticating with third party systems that support JWT authentication, for example [HashiCorp's Vault](../examples/authenticating-with-hashicorp-vault). |
| `CI_JOB_URL` | 11.1 | 0.5 | Job details URL |
+| `CI_KUBERNETES_ACTIVE` | 13.0 | all | Included with the value `true` only if the pipeline has a Kubernetes cluster available for deployments. Not included if no cluster is availble. Can be used as an alternative to [`only:kubernetes`/`except:kubernetes`](../yaml/README.md#onlykubernetesexceptkubernetes) with [`rules:if`](../yaml/README.md#rulesif) |
| `CI_MERGE_REQUEST_ASSIGNEES` | 11.9 | all | Comma-separated list of username(s) of assignee(s) for the merge request if [the pipelines are for merge requests](../merge_request_pipelines/index.md). Available only if `only: [merge_requests]` or [`rules`](../yaml/README.md#rules) syntax is used and the merge request is created. |
| `CI_MERGE_REQUEST_CHANGED_PAGE_PATHS` | 12.9 | all | Comma-separated list of paths of changed pages in a deployed [Review App](../review_apps/index.md) for a [Merge Request](../merge_request_pipelines/index.md). A [Route Map](../review_apps/index.md#route-maps) must be configured. |
| `CI_MERGE_REQUEST_CHANGED_PAGE_URLS` | 12.9 | all | Comma-separated list of URLs of changed pages in a deployed [Review App](../review_apps/index.md) for a [Merge Request](../merge_request_pipelines/index.md). A [Route Map](../review_apps/index.md#route-maps) must be configured. |
@@ -88,13 +89,13 @@ future GitLab releases.**
| `CI_NODE_TOTAL` | 11.5 | all | Total number of instances of this job running in parallel. If the job is not parallelized, this variable is set to `1`. |
| `CI_PAGES_DOMAIN` | 11.8 | all | The configured domain that hosts GitLab Pages. |
| `CI_PAGES_URL` | 11.8 | all | URL to GitLab Pages-built pages. Always belongs to a subdomain of `CI_PAGES_DOMAIN`. |
-| `CI_PIPELINE_ID` | 8.10 | all | The unique id of the current pipeline that GitLab CI/CD uses internally |
-| `CI_PIPELINE_IID` | 11.0 | all | The unique id of the current pipeline scoped to project |
+| `CI_PIPELINE_ID` | 8.10 | all | The unique ID of the current pipeline that GitLab CI/CD uses internally |
+| `CI_PIPELINE_IID` | 11.0 | all | The unique ID of the current pipeline scoped to project |
| `CI_PIPELINE_SOURCE` | 10.0 | all | Indicates how the pipeline was triggered. Possible options are: `push`, `web`, `trigger`, `schedule`, `api`, `pipeline`, `parent_pipeline`, `external`, `chat`, `merge_request_event`, and `external_pull_request_event`. For pipelines created before GitLab 9.5, this will show as `unknown` |
| `CI_PIPELINE_TRIGGERED` | all | all | The flag to indicate that job was [triggered](../triggers/README.md) |
| `CI_PIPELINE_URL` | 11.1 | 0.5 | Pipeline details URL |
| `CI_PROJECT_DIR` | all | all | The full path where the repository is cloned and where the job is run. If the GitLab Runner `builds_dir` parameter is set, this variable is set relative to the value of `builds_dir`. For more information, see [Advanced configuration](https://docs.gitlab.com/runner/configuration/advanced-configuration.html#the-runners-section) for GitLab Runner. |
-| `CI_PROJECT_ID` | all | all | The unique id of the current project that GitLab CI/CD uses internally |
+| `CI_PROJECT_ID` | all | all | The unique ID of the current project that GitLab CI/CD uses internally |
| `CI_PROJECT_NAME` | 8.10 | 0.5 | The name of the directory for the project that is currently being built. For example, if the project URL is `gitlab.example.com/group-name/project-1`, the `CI_PROJECT_NAME` would be `project-1`. |
| `CI_PROJECT_NAMESPACE` | 8.10 | 0.5 | The project namespace (username or groupname) that is currently being built |
| `CI_PROJECT_PATH` | 8.10 | 0.5 | The namespace with project name |
@@ -110,7 +111,7 @@ future GitLab releases.**
| `CI_REPOSITORY_URL` | 9.0 | all | The URL to clone the Git repository |
| `CI_RUNNER_DESCRIPTION` | 8.10 | 0.5 | The description of the runner as saved in GitLab |
| `CI_RUNNER_EXECUTABLE_ARCH` | all | 10.6 | The OS/architecture of the GitLab Runner executable (note that this is not necessarily the same as the environment of the executor) |
-| `CI_RUNNER_ID` | 8.10 | 0.5 | The unique id of runner being used |
+| `CI_RUNNER_ID` | 8.10 | 0.5 | The unique ID of runner being used |
| `CI_RUNNER_REVISION` | all | 10.6 | GitLab Runner revision that is executing the current job |
| `CI_RUNNER_SHORT_TOKEN` | all | 12.3 | First eight characters of GitLab Runner's token used to authenticate new job requests. Used as Runner's unique ID |
| `CI_RUNNER_TAGS` | 8.10 | 0.5 | The defined runner tags |
@@ -131,7 +132,7 @@ future GitLab releases.**
| `GITLAB_CI` | all | all | Mark that job is executed in GitLab CI/CD environment |
| `GITLAB_FEATURES` | 10.6 | all | The comma separated list of licensed features available for your instance and plan |
| `GITLAB_USER_EMAIL` | 8.12 | all | The email of the user who started the job |
-| `GITLAB_USER_ID` | 8.12 | all | The id of the user who started the job |
+| `GITLAB_USER_ID` | 8.12 | all | The ID of the user who started the job |
| `GITLAB_USER_LOGIN` | 10.0 | all | The login username of the user who started the job |
| `GITLAB_USER_NAME` | 10.0 | all | The real name of the user who started the job |
| `RESTORE_CACHE_ATTEMPTS` | 8.15 | 1.9 | Number of attempts to restore the cache running a job |
diff --git a/doc/ci/yaml/README.md b/doc/ci/yaml/README.md
index 31459735101..c40580cbfb7 100644
--- a/doc/ci/yaml/README.md
+++ b/doc/ci/yaml/README.md
@@ -65,7 +65,7 @@ project namespace. For example, `https://gitlab.example.com/gitlab-org/project-1
### Unavailable names for jobs
Each job must have a unique name, but there are a few **reserved `keywords` that
-cannot be used as job names**:
+can't be used as job names**:
- `image`
- `services`
@@ -90,41 +90,44 @@ A job is defined as a list of parameters that define the job's behavior.
The following table lists available parameters for jobs:
-| Keyword | Description |
-|:---------------------------------------------------|:----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------|
-| [`script`](#script) | Shell script which is executed by Runner. |
-| [`image`](#image) | Use docker images. Also available: `image:name` and `image:entrypoint`. |
-| [`services`](#services) | Use docker services images. Also available: `services:name`, `services:alias`, `services:entrypoint`, and `services:command`. |
-| [`before_script`](#before_script-and-after_script) | Override a set of commands that are executed before job. |
-| [`after_script`](#before_script-and-after_script) | Override a set of commands that are executed after job. |
-| [`stages`](#stages) | Define stages in a pipeline. |
-| [`stage`](#stage) | Defines a job stage (default: `test`). |
-| [`only`](#onlyexcept-basic) | Limit when jobs are created. Also available: [`only:refs`, `only:kubernetes`, `only:variables`, and `only:changes`](#onlyexcept-advanced). |
-| [`except`](#onlyexcept-basic) | Limit when jobs are not created. Also available: [`except:refs`, `except:kubernetes`, `except:variables`, and `except:changes`](#onlyexcept-advanced). |
-| [`rules`](#rules) | List of conditions to evaluate and determine selected attributes of a job, and whether or not it is created. May not be used alongside `only`/`except`. |
-| [`tags`](#tags) | List of tags which are used to select Runner. |
-| [`allow_failure`](#allow_failure) | Allow job to fail. Failed job doesn't contribute to commit status. |
-| [`when`](#when) | When to run job. Also available: `when:manual` and `when:delayed`. |
-| [`environment`](#environment) | Name of an environment to which the job deploys. Also available: `environment:name`, `environment:url`, `environment:on_stop`, `environment:auto_stop_in` and `environment:action`. |
-| [`cache`](#cache) | List of files that should be cached between subsequent runs. Also available: `cache:paths`, `cache:key`, `cache:untracked`, and `cache:policy`. |
-| [`artifacts`](#artifacts) | List of files and directories to attach to a job on success. Also available: `artifacts:paths`, `artifacts:expose_as`, `artifacts:name`, `artifacts:untracked`, `artifacts:when`, `artifacts:expire_in`, `artifacts:reports`, `artifacts:reports:junit`, and `artifacts:reports:cobertura`.<br><br>In GitLab [Enterprise Edition](https://about.gitlab.com/pricing/), these are available: `artifacts:reports:codequality`, `artifacts:reports:sast`, `artifacts:reports:dependency_scanning`, `artifacts:reports:container_scanning`, `artifacts:reports:dast`, `artifacts:reports:license_management`, `artifacts:reports:performance` and `artifacts:reports:metrics`. |
-| [`dependencies`](#dependencies) | Restrict which artifacts are passed to a specific job by providing a list of jobs to fetch artifacts from. |
-| [`coverage`](#coverage) | Code coverage settings for a given job. |
-| [`retry`](#retry) | When and how many times a job can be auto-retried in case of a failure. |
-| [`timeout`](#timeout) | Define a custom job-level timeout that takes precedence over the project-wide setting. |
-| [`parallel`](#parallel) | How many instances of a job should be run in parallel. |
-| [`trigger`](#trigger) | Defines a downstream pipeline trigger. |
-| [`include`](#include) | Allows this job to include external YAML files. Also available: `include:local`, `include:file`, `include:template`, and `include:remote`. |
-| [`extends`](#extends) | Configuration entries that this job is going to inherit from. |
-| [`pages`](#pages) | Upload the result of a job to use with GitLab Pages. |
-| [`variables`](#variables) | Define job variables on a job level. |
-| [`interruptible`](#interruptible) | Defines if a job can be canceled when made redundant by a newer run. |
-| [`resource_group`](#resource_group) | Limit job concurrency. |
+| Keyword | Description |
+|:---------------------------------------------------|:------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------|
+| [`script`](#script) | Shell script which is executed by Runner. |
+| [`image`](#image) | Use docker images. Also available: `image:name` and `image:entrypoint`. |
+| [`services`](#services) | Use docker services images. Also available: `services:name`, `services:alias`, `services:entrypoint`, and `services:command`. |
+| [`before_script`](#before_script-and-after_script) | Override a set of commands that are executed before job. |
+| [`after_script`](#before_script-and-after_script) | Override a set of commands that are executed after job. |
+| [`stage`](#stage) | Defines a job stage (default: `test`). |
+| [`only`](#onlyexcept-basic) | Limit when jobs are created. Also available: [`only:refs`, `only:kubernetes`, `only:variables`, and `only:changes`](#onlyexcept-advanced). |
+| [`except`](#onlyexcept-basic) | Limit when jobs are not created. Also available: [`except:refs`, `except:kubernetes`, `except:variables`, and `except:changes`](#onlyexcept-advanced). |
+| [`rules`](#rules) | List of conditions to evaluate and determine selected attributes of a job, and whether or not it's created. May not be used alongside `only`/`except`. |
+| [`tags`](#tags) | List of tags which are used to select Runner. |
+| [`allow_failure`](#allow_failure) | Allow job to fail. Failed job does not contribute to commit status. |
+| [`when`](#when) | When to run job. Also available: `when:manual` and `when:delayed`. |
+| [`environment`](#environment) | Name of an environment to which the job deploys. Also available: `environment:name`, `environment:url`, `environment:on_stop`, `environment:auto_stop_in` and `environment:action`. |
+| [`cache`](#cache) | List of files that should be cached between subsequent runs. Also available: `cache:paths`, `cache:key`, `cache:untracked`, and `cache:policy`. |
+| [`artifacts`](#artifacts) | List of files and directories to attach to a job on success. Also available: `artifacts:paths`, `artifacts:expose_as`, `artifacts:name`, `artifacts:untracked`, `artifacts:when`, `artifacts:expire_in`, `artifacts:reports`, `artifacts:reports:junit`, `artifacts:reports:cobertura`, and `artifacts:reports:terraform`.<br><br>In GitLab [Enterprise Edition](https://about.gitlab.com/pricing/), these are available: `artifacts:reports:codequality`, `artifacts:reports:sast`, `artifacts:reports:dependency_scanning`, `artifacts:reports:container_scanning`, `artifacts:reports:dast`, `artifacts:reports:license_scanning`, `artifacts:reports:license_management` (removed in GitLab 13.0),`artifacts:reports:performance` and `artifacts:reports:metrics`. |
+| [`dependencies`](#dependencies) | Restrict which artifacts are passed to a specific job by providing a list of jobs to fetch artifacts from. |
+| [`coverage`](#coverage) | Code coverage settings for a given job. |
+| [`retry`](#retry) | When and how many times a job can be auto-retried in case of a failure. |
+| [`timeout`](#timeout) | Define a custom job-level timeout that takes precedence over the project-wide setting. |
+| [`parallel`](#parallel) | How many instances of a job should be run in parallel. |
+| [`trigger`](#trigger) | Defines a downstream pipeline trigger. |
+| [`include`](#include) | Allows this job to include external YAML files. Also available: `include:local`, `include:file`, `include:template`, and `include:remote`. |
+| [`extends`](#extends) | Configuration entries that this job is going to inherit from. |
+| [`pages`](#pages) | Upload the result of a job to use with GitLab Pages. |
+| [`variables`](#variables) | Define job variables on a job level. |
+| [`interruptible`](#interruptible) | Defines if a job can be canceled when made redundant by a newer run. |
+| [`resource_group`](#resource_group) | Limit job concurrency. |
NOTE: **Note:**
Parameters `types` and `type` are [deprecated](#deprecated-parameters).
-## Setting default parameters
+## Global parameters
+
+Some parameters must be defined at a global level, affecting all jobs in the pipeline.
+
+### Global defaults
Some parameters can be set globally as the default for all jobs using the
`default:` keyword. Default parameters can then be overridden by job-specific
@@ -158,7 +161,7 @@ rspec 2.6:
script: bundle exec rspec
```
-### `inherit`
+#### `inherit`
> [Introduced](https://gitlab.com/gitlab-org/gitlab/issues/207484) in GitLab 12.9.
@@ -198,13 +201,13 @@ In the example below:
- **will** inherit: Nothing.
- `rspec`:
- **will** inherit: the default `image` and the `WEBHOOK_URL` variable.
- - **will not** inherit: the default `before_script` and the `DOMAIN` variable.
+ - will **not** inherit: the default `before_script` and the `DOMAIN` variable.
- `capybara`:
- **will** inherit: the default `before_script` and `image`.
- - **will not** inherit: the `DOMAIN` and `WEBHOOK_URL` variables.
+ - will **not** inherit: the `DOMAIN` and `WEBHOOK_URL` variables.
- `karma`:
- **will** inherit: the default `image` and `before_script`, and the `DOMAIN` variable.
- - **will not** inherit: `WEBHOOK_URL` variable.
+ - will **not** inherit: `WEBHOOK_URL` variable.
```yaml
default:
@@ -240,49 +243,274 @@ karma:
script: karma
```
-## Parameter details
+### `stages`
-The following are detailed explanations for parameters used to configure CI/CD pipelines.
+`stages` is used to define stages that can be used by jobs and is defined
+globally.
-### `script`
+The specification of `stages` allows for having flexible multi stage pipelines.
+The ordering of elements in `stages` defines the ordering of jobs' execution:
-`script` is the only required keyword that a job needs. It's a shell script
-which is executed by the Runner. For example:
+1. Jobs of the same stage are run in parallel.
+1. Jobs of the next stage are run after the jobs from the previous stage
+ complete successfully.
+
+Let's consider the following example, which defines 3 stages:
```yaml
-job:
- script: "bundle exec rspec"
+stages:
+ - build
+ - test
+ - deploy
```
-[YAML anchors for scripts](#yaml-anchors-for-script) are available.
+1. First, all jobs of `build` are executed in parallel.
+1. If all jobs of `build` succeed, the `test` jobs are executed in parallel.
+1. If all jobs of `test` succeed, the `deploy` jobs are executed in parallel.
+1. If all jobs of `deploy` succeed, the commit is marked as `passed`.
+1. If any of the previous jobs fails, the commit is marked as `failed` and no
+ jobs of further stage are executed.
-This parameter can also contain several commands using an array:
+There are also two edge cases worth mentioning:
+
+1. If no `stages` are defined in `.gitlab-ci.yml`, then the `build`,
+ `test` and `deploy` are allowed to be used as job's stage by default.
+1. If a job does not specify a `stage`, the job is assigned the `test` stage.
+
+### `workflow:rules`
+
+> [Introduced](https://gitlab.com/gitlab-org/gitlab/issues/29654) in GitLab 12.5
+
+The top-level `workflow:` key applies to the entirety of a pipeline, and will
+determine whether or not a pipeline is created. It currently accepts a single
+`rules:` key that operates similarly to [`rules:` defined within jobs](#rules),
+enabling dynamic configuration of the pipeline.
+
+#### `workflow:rules` templates
+
+> [Introduced](https://gitlab.com/gitlab-org/gitlab/-/issues/217732) in GitLab 13.0.
+
+We provide pre-made templates for use with your pipelines that set up `workflow: rules`
+for common scenarios. Usage of these will make things easier and prevent duplicate pipelines from running.
+
+The [`Branch-Pipelines` template](https://gitlab.com/gitlab-org/gitlab/-/tree/master/lib/gitlab/ci/templates/Workflows/Branch-Pipelines.gitlab-ci.yml)
+makes your pipelines run for branches and tags.
+
+Branch pipeline status will be displayed within merge requests that use that branch
+as a source, but this pipeline type does not support any features offered by
+[Merge Request Pipelines](../merge_request_pipelines/) like
+[Pipelines for Merge Results](../merge_request_pipelines/#pipelines-for-merged-results-premium)
+or [Merge Trains](../merge_request_pipelines/pipelines_for_merged_results/merge_trains/).
+Use this template if you are intentionally avoiding those features.
+
+It is [included](#include) as follows:
```yaml
-job:
- script:
- - uname -a
- - bundle exec rspec
+include:
+ - template: 'Workflows/Branch-Pipelines.gitlab-ci.yml'
+```
+
+The [`MergeRequest-Pipelines` include](https://gitlab.com/gitlab-org/gitlab/-/tree/master/lib/gitlab/ci/templates/Workflows/MergeRequest-Pipelines.gitlab-ci.yml) sets your pipelines to run for the default branch (usually `master`), tags, and
+The [`MergeRequest-Pipelines` template](https://gitlab.com/gitlab-org/gitlab/-/tree/master/lib/gitlab/ci/templates/Workflows/MergeRequest-Pipelines.gitlab-ci.yml)
+makes your pipelines run for the default branch (usually `master`), tags, and
+all types of merge request pipelines. Use this template if you use any of the
+the [Pipelines for Merge Requests features](../merge_request_pipelines/), as mentioned
+above.
+
+It is [included](#include) as follows:
+
+```yaml
+include:
+ - template: 'Workflows/MergeRequest-Pipelines.gitlab-ci.yml'
+```
+
+If you prefer to define your own rules, the configuration options currently available are:​
+
+- [`if`](#rulesif): Define a rule.
+- [`when`](#when): May be set to `always` or `never` only. If not provided, the default value is `always`​.
+
+The list of `if` rules is evaluated until a single one is matched. If none
+match, the last `when` will be used:
+
+```yaml
+workflow:
+ rules:
+ - if: $CI_COMMIT_REF_NAME =~ /-wip$/
+ when: never
+ - if: $CI_COMMIT_TAG
+ when: never
+ - when: always
```
+### `include`
+
+> - Introduced in [GitLab Premium](https://about.gitlab.com/pricing/) 10.5.
+> - Available for Starter, Premium and Ultimate since 10.6.
+> - [Moved](https://gitlab.com/gitlab-org/gitlab-foss/-/issues/42861) to GitLab Core in 11.4.
+
+Using the `include` keyword allows the inclusion of external YAML files. This helps
+to break down the CI/CD configuration into multiple files and increases readability for long configuration files.
+It's also possible to have template files stored in a central repository and projects include their
+configuration files. This helps avoid duplicated configuration, for example, global default variables for all projects.
+
+`include` requires the external YAML file to have the extensions `.yml` or `.yaml`,
+otherwise the external file won't be included.
+
+`include` supports the following inclusion methods:
+
+| Method | Description |
+|:--------------------------------|:------------------------------------------------------------------|
+| [`local`](#includelocal) | Include a file from the local project repository. |
+| [`file`](#includefile) | Include a file from a different project repository. |
+| [`remote`](#includeremote) | Include a file from a remote URL. Must be publicly accessible. |
+| [`template`](#includetemplate) | Include templates which are provided by GitLab. |
+
NOTE: **Note:**
-Sometimes, `script` commands will need to be wrapped in single or double quotes.
-For example, commands that contain a colon (`:`) need to be wrapped in quotes so
-that the YAML parser knows to interpret the whole thing as a string rather than
-a "key: value" pair. Be careful when using special characters:
-`:`, `{`, `}`, `[`, `]`, `,`, `&`, `*`, `#`, `?`, `|`, `-`, `<`, `>`, `=`, `!`, `%`, `@`, `` ` ``.
+`.gitlab-ci.yml` configuration included by all methods is evaluated at pipeline creation.
+The configuration is a snapshot in time and persisted in the database. Any changes to
+referenced `.gitlab-ci.yml` configuration won't be reflected in GitLab until the next pipeline is created.
-If any of the script commands return an exit code different from zero, the job
-will fail and further commands will not be executed. This behavior can be avoided by
-storing the exit code in a variable:
+The files defined in `include` are:
+
+- Deep merged with those in `.gitlab-ci.yml`.
+- Always evaluated first and merged with the content of `.gitlab-ci.yml`,
+ regardless of the position of the `include` keyword.
+
+TIP: **Tip:**
+Use merging to customize and override included CI/CD configurations with local
+definitions.
+
+NOTE: **Note:**
+Using YAML aliases across different YAML files sourced by `include` is not
+supported. You must only refer to aliases in the same file. Instead
+of using YAML anchors, you can use the [`extends` keyword](#extends).
+
+#### `include:local`
+
+`include:local` includes a file from the same repository as `.gitlab-ci.yml`.
+It's referenced using full paths relative to the root directory (`/`).
+
+You can only use files that are currently tracked by Git on the same branch
+your configuration file is on. In other words, when using a `include:local`, make
+sure that both `.gitlab-ci.yml` and the local file are on the same branch.
+
+All [nested includes](#nested-includes) will be executed in the scope of the same project,
+so it's possible to use local, project, remote, or template includes.
+
+NOTE: **Note:**
+Including local files through Git submodules paths is not supported.
+
+Example:
```yaml
-job:
- script:
- - false || exit_code=$?
- - if [ $exit_code -ne 0 ]; then echo "Previous command failed"; fi;
+include:
+ - local: '/templates/.gitlab-ci-template.yml'
+```
+
+TIP: **Tip:**
+Local includes can be used as a replacement for symbolic links which are not followed.
+
+This can be defined as a short local include:
+
+```yaml
+include: '.gitlab-ci-production.yml'
+```
+
+#### `include:file`
+
+> [Introduced](https://gitlab.com/gitlab-org/gitlab-foss/issues/53903) in GitLab 11.7.
+
+To include files from another private project under the same GitLab instance,
+use `include:file`. This file is referenced using full paths relative to the
+root directory (`/`). For example:
+
+```yaml
+include:
+ - project: 'my-group/my-project'
+ file: '/templates/.gitlab-ci-template.yml'
```
+You can also specify `ref`, with the default being the `HEAD` of the project:
+
+```yaml
+include:
+ - project: 'my-group/my-project'
+ ref: master
+ file: '/templates/.gitlab-ci-template.yml'
+
+ - project: 'my-group/my-project'
+ ref: v1.0.0
+ file: '/templates/.gitlab-ci-template.yml'
+
+ - project: 'my-group/my-project'
+ ref: 787123b47f14b552955ca2786bc9542ae66fee5b # Git SHA
+ file: '/templates/.gitlab-ci-template.yml'
+```
+
+All [nested includes](#nested-includes) will be executed in the scope of the target project,
+so it's possible to use local (relative to target project), project, remote
+or template includes.
+
+#### `include:remote`
+
+`include:remote` can be used to include a file from a different location,
+using HTTP/HTTPS, referenced by using the full URL. The remote file must be
+publicly accessible through a simple GET request as authentication schemas
+in the remote URL are not supported. For example:
+
+```yaml
+include:
+ - remote: 'https://gitlab.com/awesome-project/raw/master/.gitlab-ci-template.yml'
+```
+
+All [nested includes](#nested-includes) will be executed without context as public user, so only another remote
+or public project, or template, is allowed.
+
+#### `include:template`
+
+> [Introduced](https://gitlab.com/gitlab-org/gitlab-foss/issues/53445) in GitLab 11.7.
+
+`include:template` can be used to include `.gitlab-ci.yml` templates that are
+[shipped with GitLab](https://gitlab.com/gitlab-org/gitlab/tree/master/lib/gitlab/ci/templates).
+
+For example:
+
+```yaml
+# File sourced from GitLab's template collection
+include:
+ - template: Auto-DevOps.gitlab-ci.yml
+```
+
+Multiple `include:template` files:
+
+```yaml
+include:
+ - template: Android-Fastlane.gitlab-ci.yml
+ - template: Auto-DevOps.gitlab-ci.yml
+```
+
+All [nested includes](#nested-includes) will be executed only with the permission of the user,
+so it's possible to use project, remote or template includes.
+
+#### Nested includes
+
+> [Introduced](https://gitlab.com/gitlab-org/gitlab-foss/issues/56836) in GitLab 11.9.
+
+Nested includes allow you to compose a set of includes.
+
+A total of 100 includes is allowed, but duplicate includes are considered a configuration error.
+
+Since [GitLab 12.4](https://gitlab.com/gitlab-org/gitlab/issues/28212), the time limit
+for resolving all files is 30 seconds.
+
+#### Additional `includes` examples
+
+There is a list of [additional `includes` examples](includes.md) available.
+
+## Parameter details
+
+The following are detailed explanations for parameters used to configure CI/CD pipelines.
+
### `image`
Used to specify [a Docker image](../docker/using_docker_images.md#what-is-an-image) to use for the job.
@@ -304,7 +532,7 @@ An [extended docker configuration option](../docker/using_docker_images.md#exten
For more information, see [Available settings for `image`](../docker/using_docker_images.md#available-settings-for-image).
-### `services`
+#### `services`
Used to specify a [service Docker image](../docker/using_docker_images.md#what-is-a-service), linked to a base image specified in [`image`](#image).
@@ -314,31 +542,70 @@ For:
- Detailed usage information, refer to [Docker integration](../docker/README.md) documentation.
- For example services, see [GitLab CI/CD Services](../services/README.md).
-#### `services:name`
+##### `services:name`
An [extended docker configuration option](../docker/using_docker_images.md#extended-docker-configuration-options).
For more information, see [Available settings for `services`](../docker/using_docker_images.md#available-settings-for-services).
-#### `services:alias`
+##### `services:alias`
An [extended docker configuration option](../docker/using_docker_images.md#extended-docker-configuration-options).
For more information, see [Available settings for `services`](../docker/using_docker_images.md#available-settings-for-services).
-#### `services:entrypoint`
+##### `services:entrypoint`
An [extended docker configuration option](../docker/using_docker_images.md#extended-docker-configuration-options).
For more information, see [Available settings for `services`](../docker/using_docker_images.md#available-settings-for-services).
-#### `services:command`
+##### `services:command`
An [extended docker configuration option](../docker/using_docker_images.md#extended-docker-configuration-options).
For more information, see [Available settings for `services`](../docker/using_docker_images.md#available-settings-for-services).
-### `before_script` and `after_script`
+### `script`
+
+`script` is the only required keyword that a job needs. It's a shell script
+which is executed by the Runner. For example:
+
+```yaml
+job:
+ script: "bundle exec rspec"
+```
+
+[YAML anchors for scripts](#yaml-anchors-for-script) are available.
+
+This parameter can also contain several commands using an array:
+
+```yaml
+job:
+ script:
+ - uname -a
+ - bundle exec rspec
+```
+
+NOTE: **Note:**
+Sometimes, `script` commands will need to be wrapped in single or double quotes.
+For example, commands that contain a colon (`:`) need to be wrapped in quotes so
+that the YAML parser knows to interpret the whole thing as a string rather than
+a "key: value" pair. Be careful when using special characters:
+`:`, `{`, `}`, `[`, `]`, `,`, `&`, `*`, `#`, `?`, `|`, `-`, `<`, `>`, `=`, `!`, `%`, `@`, `` ` ``.
+
+If any of the script commands return an exit code different from zero, the job
+will fail and further commands won't be executed. This behavior can be avoided by
+storing the exit code in a variable:
+
+```yaml
+job:
+ script:
+ - false || exit_code=$?
+ - if [ $exit_code -ne 0 ]; then echo "Previous command failed"; fi;
+```
+
+#### `before_script` and `after_script`
> Introduced in GitLab 8.7 and requires GitLab Runner v1.2.
@@ -362,7 +629,7 @@ Scripts specified in `after_script` are executed in a new shell, separate from a
software installed by a `before_script` or `script` script.
- Have a separate timeout, which is hard coded to 5 minutes. See
[related issue](https://gitlab.com/gitlab-org/gitlab-runner/issues/2716) for details.
-- Do not affect the job's exit code. If the `script` section succeeds and the
+- Don't affect the job's exit code. If the `script` section succeeds and the
`after_script` times out or fails, the job will exit with code `0` (`Job Succeeded`).
It's possible to overwrite a globally defined `before_script` or `after_script`
@@ -384,39 +651,53 @@ job:
[YAML anchors for `before_script` and `after_script`](#yaml-anchors-for-before_script-and-after_script) are available.
-### `stages`
-
-`stages` is used to define stages that can be used by jobs and is defined
-globally.
-
-The specification of `stages` allows for having flexible multi stage pipelines.
-The ordering of elements in `stages` defines the ordering of jobs' execution:
-
-1. Jobs of the same stage are run in parallel.
-1. Jobs of the next stage are run after the jobs from the previous stage
- complete successfully.
+### `stage`
-Let's consider the following example, which defines 3 stages:
+`stage` is defined per-job and relies on [`stages`](#stages) which is defined
+globally. It allows to group jobs into different stages, and jobs of the same
+`stage` are executed in parallel (subject to [certain conditions](#using-your-own-runners)). For example:
```yaml
stages:
- build
- test
- deploy
+
+job 0:
+ stage: .pre
+ script: make something useful before build stage
+
+job 1:
+ stage: build
+ script: make build dependencies
+
+job 2:
+ stage: build
+ script: make build artifacts
+
+job 3:
+ stage: test
+ script: make test
+
+job 4:
+ stage: deploy
+ script: make deploy
+
+job 5:
+ stage: .post
+ script: make something useful at the end of pipeline
```
-1. First, all jobs of `build` are executed in parallel.
-1. If all jobs of `build` succeed, the `test` jobs are executed in parallel.
-1. If all jobs of `test` succeed, the `deploy` jobs are executed in parallel.
-1. If all jobs of `deploy` succeed, the commit is marked as `passed`.
-1. If any of the previous jobs fails, the commit is marked as `failed` and no
- jobs of further stage are executed.
+#### Using your own Runners
-There are also two edge cases worth mentioning:
+When using your own Runners, GitLab Runner runs only one job at a time by default (see the
+`concurrent` flag in [Runner global settings](https://docs.gitlab.com/runner/configuration/advanced-configuration.html#the-global-section)
+for more information).
-1. If no `stages` are defined in `.gitlab-ci.yml`, then the `build`,
- `test` and `deploy` are allowed to be used as job's stage by default.
-1. If a job doesn't specify a `stage`, the job is assigned the `test` stage.
+Jobs will run on your own Runners in parallel only if:
+
+- Run on different Runners.
+- The Runner's `concurrent` setting has been changed.
#### `.pre` and `.post`
@@ -429,12 +710,12 @@ The following stages are available to every pipeline:
User-defined stages are executed after `.pre` and before `.post`.
-The order of `.pre` and `.post` cannot be changed, even if defined out of order in `.gitlab-ci.yml`.
+The order of `.pre` and `.post` can't be changed, even if defined out of order in `.gitlab-ci.yml`.
For example, the following are equivalent configuration:
- Configured in order:
- ```yml
+ ```yaml
stages:
- .pre
- a
@@ -444,7 +725,7 @@ For example, the following are equivalent configuration:
- Configured out of order:
- ```yml
+ ```yaml
stages:
- a
- .pre
@@ -454,69 +735,390 @@ For example, the following are equivalent configuration:
- Not explicitly configured:
- ```yml
+ ```yaml
stages:
- a
- b
```
NOTE: **Note:**
-A pipeline will not be created if it only contains jobs in `.pre` or `.post` stages.
+A pipeline won't be created if it only contains jobs in `.pre` or `.post` stages.
-### `stage`
+### `extends`
-`stage` is defined per-job and relies on [`stages`](#stages) which is defined
-globally. It allows to group jobs into different stages, and jobs of the same
-`stage` are executed in parallel (subject to [certain conditions](#using-your-own-runners)). For example:
+> Introduced in GitLab 11.3.
+
+`extends` defines entry names that a job that uses `extends` is going to
+inherit from.
+
+It's an alternative to using [YAML anchors](#anchors) and is a little
+more flexible and readable:
```yaml
-stages:
- - build
- - test
- - deploy
+.tests:
+ script: rake test
+ stage: test
+ only:
+ refs:
+ - branches
-job 0:
- stage: .pre
- script: make something useful before build stage
+rspec:
+ extends: .tests
+ script: rake rspec
+ only:
+ variables:
+ - $RSPEC
+```
-job 1:
- stage: build
- script: make build dependencies
+In the example above, the `rspec` job inherits from the `.tests` template job.
+GitLab will perform a reverse deep merge based on the keys. GitLab will:
-job 2:
- stage: build
- script: make build artifacts
+- Merge the `rspec` contents into `.tests` recursively.
+- Not merge the values of the keys.
-job 3:
+This results in the following `rspec` job:
+
+```yaml
+rspec:
+ script: rake rspec
stage: test
- script: make test
+ only:
+ refs:
+ - branches
+ variables:
+ - $RSPEC
+```
-job 4:
- stage: deploy
- script: make deploy
+NOTE: **Note:**
+Note that `script: rake test` has been overwritten by `script: rake rspec`.
-job 5:
- stage: .post
- script: make something useful at the end of pipeline
+If you do want to include the `rake test`, see [`before_script` and `after_script`](#before_script-and-after_script).
+
+`.tests` in this example is a [hidden job](#hide-jobs), but it's
+possible to inherit from regular jobs as well.
+
+`extends` supports multi-level inheritance, however it's not recommended to
+use more than three levels. The maximum nesting level that is supported is 10.
+The following example has two levels of inheritance:
+
+```yaml
+.tests:
+ only:
+ - pushes
+
+.rspec:
+ extends: .tests
+ script: rake rspec
+
+rspec 1:
+ variables:
+ RSPEC_SUITE: '1'
+ extends: .rspec
+
+rspec 2:
+ variables:
+ RSPEC_SUITE: '2'
+ extends: .rspec
+
+spinach:
+ extends: .tests
+ script: rake spinach
```
-#### Using your own Runners
+In GitLab 12.0 and later, it's also possible to use multiple parents for
+`extends`. The algorithm used for merge is "closest scope wins", so
+keys from the last member will always shadow anything defined on other
+levels. For example:
-When using your own Runners, GitLab Runner runs only one job at a time by default (see the
-`concurrent` flag in [Runner global settings](https://docs.gitlab.com/runner/configuration/advanced-configuration.html#the-global-section)
-for more information).
+```yaml
+.only-important:
+ only:
+ - master
+ - stable
+ tags:
+ - production
-Jobs will run on your own Runners in parallel only if:
+.in-docker:
+ tags:
+ - docker
+ image: alpine
-- Run on different Runners.
-- The Runner's `concurrent` setting has been changed.
+rspec:
+ extends:
+ - .only-important
+ - .in-docker
+ script:
+ - rake rspec
+```
+
+This results in the following `rspec` job:
+
+```yaml
+rspec:
+ only:
+ - master
+ - stable
+ tags:
+ - docker
+ image: alpine
+ script:
+ - rake rspec
+```
+
+#### Using `extends` and `include` together
+
+`extends` works across configuration files combined with `include`.
+
+For example, if you have a local `included.yml` file:
+
+```yaml
+.template:
+ script:
+ - echo Hello!
+```
+
+Then, in `.gitlab-ci.yml` you can use it like this:
+
+```yaml
+include: included.yml
+
+useTemplate:
+ image: alpine
+ extends: .template
+```
+
+This will run a job called `useTemplate` that runs `echo Hello!` as defined in
+the `.template` job, and uses the `alpine` Docker image as defined in the local job.
+
+### `rules`
+
+> [Introduced](https://gitlab.com/gitlab-org/gitlab/-/issues/27863) in GitLab 12.3.
+
+`rules` allows for a list of individual rule objects to be evaluated
+*in order*, until one matches and dynamically provides attributes to the job.
+
+CAUTION: **Caution:**
+`rules` can't be used in combination with `only/except` as it is a replacement for that functionality. If you attempt to do this, the linter will return a
+`key may not be used with rules` error.
+
+#### Key details when using `rules`
+
+A very important difference between `rules` and `only/except`, is that jobs defined
+with `rules` trigger merge request pipelines by default, but `only/except` jobs do not.
+This may be surprising if migrating from `only` and `except`, so new users of `rules`
+can use one of the [`workflow: rules` templates](#workflowrules-templates) to get started.
+This will ensure that the behavior is more stable as you start adding additional `rules`
+blocks, and will avoid issues like creating a duplicate, merge request (detached) pipeline.
+
+We don't recomment mixing `only/except` jobs with `rules` jobs in the same pipeline.
+It may not cause YAML errors, but debugging the exact execution behavior can be complex
+due to the different default behaviors of `only/except` and `rules`.
+
+### Rules clauses
+
+Available rule clauses include:
+
+- [`if`](#rulesif) (similar to [`only:variables`](#onlyvariablesexceptvariables))
+- [`changes`](#ruleschanges) (same as [`only:changes`](#onlychangesexceptchanges))
+- [`exists`](#rulesexists)
+
+For example, using `if`. This configuration specifies that `job` should be built
+and run for every pipeline on merge requests targeting `master`, regardless of
+the status of other builds:
+
+```yaml
+job:
+ script: "echo Hello, Rules!"
+ rules:
+ - if: '$CI_MERGE_REQUEST_TARGET_BRANCH_NAME == "master"'
+ when: always
+ - if: '$VAR =~ /pattern/'
+ when: manual
+ - when: on_success
+```
+
+In this example, if the first rule:
+
+- Matches, the job will be given the `when:always` attribute.
+- Does not match, the second and third rules will be evaluated sequentially
+ until a match is found. That is, the job will be given either the:
+ - `when: manual` attribute if the second rule matches. **The stage won't complete until this manual job is triggered and completes successfully.**
+ - `when: on_success` attribute if the second rule does not match. The third
+ rule will always match when reached because it has no conditional clauses.
+
+#### `rules:if`
+
+`rules:if` differs slightly from `only:variables` by accepting only a single
+expression string, rather than an array of them. Any set of expressions to be
+evaluated should be conjoined into a single expression using `&&` or `||`, and use
+the [variable matching syntax](../variables/README.md#syntax-of-environment-variable-expressions).
+
+For example:
+
+```yaml
+job:
+ script: "echo Hello, Rules!"
+ rules:
+ - if: '$CI_MERGE_REQUEST_SOURCE_BRANCH_NAME =~ /^feature/ && $CI_MERGE_REQUEST_TARGET_BRANCH_NAME == "master"' # This rule will be evaluated
+ when: always
+ - if: '$CI_MERGE_REQUEST_SOURCE_BRANCH_NAME =~ /^feature/' # This rule will only be evaluated if the target branch is not "master"
+ when: manual
+ - if: '$CI_MERGE_REQUEST_SOURCE_BRANCH_NAME' # If neither of the first two match but the simple presence does, we set to "on_success" by default
+```
+
+If none of the provided rules match, the job will be set to `when:never`, and
+not included in the pipeline. If `rules:when` is not included in the configuration
+at all, the behavior defaults to `job:when`, which continues to default to
+`on_success`.
+
+#### `rules:changes`
+
+`rules: changes` works exactly the same way as `only: changes` and `except: changes`,
+accepting an array of paths. Similarly, it will always return true if there is no
+Git push event. See [`only/except: changes`](#onlychangesexceptchanges) for more information.
+
+For example:
+
+```yaml
+docker build:
+ script: docker build -t my-image:$CI_COMMIT_REF_SLUG .
+ rules:
+ - changes: # Will include the job and set to when:manual if any of the follow paths match a modified file.
+ - Dockerfile
+ when: manual
+ - if: '$VAR == "string value"'
+ when: manual # Will include the job and set to when:manual if the expression evaluates to true, after the `changes:` rule fails to match.
+ - when: on_success # If neither of the first rules match, set to on_success
+```
+
+In this example, a job either set to:
+
+- Run manually if `Dockerfile` has changed OR `$VAR == "string value"`.
+- `when:on_success` by the last rule, where no earlier clauses evaluate to true.
+
+#### `rules:exists`
+
+> [Introduced](https://gitlab.com/gitlab-org/gitlab/-/issues/24021) in GitLab 12.4.
+
+`exists` accepts an array of paths and will match if any of these paths exist
+as files in the repository.
+
+For example:
+
+```yaml
+job:
+ script: docker build -t my-image:$CI_COMMIT_REF_SLUG .
+ rules:
+ - exists:
+ - Dockerfile
+```
+
+You can also use glob patterns to match multiple files in any directory within
+the repository.
+
+For example:
+
+```yaml
+job:
+ script: bundle exec rspec
+ rules:
+ - exists:
+ - spec/**.rb
+```
+
+NOTE: **Note:**
+For performance reasons, using `exists` with patterns is limited to 10000
+checks. After the 10000th check, rules with patterned globs will always match.
+
+#### `rules:allow_failure`
+
+> [Introduced](https://gitlab.com/gitlab-org/gitlab/issues/30235) in GitLab 12.8.
+
+You can use [`allow_failure: true`](#allow_failure) within `rules:` to allow a job to fail, or a manual job to
+wait for action, without stopping the pipeline itself. All jobs using `rules:` default to `allow_failure: false`
+if `allow_failure:` is not defined.
+
+```yaml
+job:
+ script: "echo Hello, Rules!"
+ rules:
+ - if: '$CI_MERGE_REQUEST_TARGET_BRANCH_NAME == "master"'
+ when: manual
+ allow_failure: true
+```
+
+In this example, if the first rule matches, then the job will have `when: manual` and `allow_failure: true`.
+
+#### Complex rule clauses
+
+To conjoin `if`, `changes`, and `exists` clauses with an AND, use them in the
+same rule.
+
+In the following example:
+
+- We run the job manually if `Dockerfile` or any file in `docker/scripts/`
+ has changed AND `$VAR == "string value"`.
+- Otherwise, the job won't be included in the pipeline.
+
+```yaml
+docker build:
+ script: docker build -t my-image:$CI_COMMIT_REF_SLUG .
+ rules:
+ - if: '$VAR == "string value"'
+ changes: # Will include the job and set to when:manual if any of the follow paths match a modified file.
+ - Dockerfile
+ - docker/scripts/*
+ when: manual
+ # - when: never would be redundant here, this is implied any time rules are listed.
+```
+
+The only clauses currently available are:
+
+- `if`
+- `changes`
+- `exists`
+
+Keywords such as `branches` or `refs` that are currently available for
+`only`/`except` are not yet available in `rules` as they are being individually
+considered for their usage and behavior in this context. Future keyword improvements
+are being discussed in our [epic for improving `rules`](https://gitlab.com/groups/gitlab-org/-/epics/2783),
+where anyone can add suggestions or requests.
+
+#### Permitted attributes
+
+The only job attributes currently set by `rules` are:
+
+- `when`.
+- `start_in`, if `when` is set to `delayed`.
+- `allow_failure`.
+
+A job will be included in a pipeline if `when` is evaluated to any value
+except `never`.
+
+Delayed jobs require a `start_in` value, so rule objects do as well. For
+example:
+
+```yaml
+docker build:
+ script: docker build -t my-image:$CI_COMMIT_REF_SLUG .
+ rules:
+ - changes: # Will include the job and delay 3 hours when the Dockerfile has changed
+ - Dockerfile
+ when: delayed
+ start_in: '3 hours'
+ - when: on_success # Otherwise include the job and set to run normally
+```
+
+Additional job configuration may be added to rules in the future. If something
+useful is not available, please
+[open an issue](https://gitlab.com/gitlab-org/gitlab/issues).
### `only`/`except` (basic)
NOTE: **Note:**
-The [`rules`](#rules) syntax is now the preferred method of setting job policies.
-`only` and `except` are [candidates for deprecation](https://gitlab.com/gitlab-org/gitlab/issues/27449),
-and may be removed in the future.
+The [`rules`](#rules) syntax is an improved, more powerful solution for defining
+when jobs should run or not. Consider using `rules` instead of `only/except` to get
+the most out of your pipelines.
`only` and `except` are two parameters that set a job policy to limit when
jobs are created:
@@ -604,7 +1206,7 @@ The above example will run `job` for all branches on `gitlab-org/gitlab`,
except `master` and those with names prefixed with `release/`.
If a job does not have an `only` rule, `only: ['branches', 'tags']` is set by
-default. If it doesn't have an `except` rule, it is empty.
+default. If it does not have an `except` rule, it's empty.
For example,
@@ -643,7 +1245,7 @@ matching only a substring of the tag name or branch name.
For example, `/^issue-.*$/` is equivalent to `/^issue-/`,
while just `/issue/` would also match a branch called `severe-issues`.
-### Supported `only`/`except` regexp syntax
+#### Supported `only`/`except` regexp syntax
CAUTION: **Warning:**
This is a breaking change that was introduced with GitLab 11.9.4.
@@ -667,7 +1269,7 @@ Feature.enable(:allow_unsafe_ruby_regexp)
### `only`/`except` (advanced)
CAUTION: **Warning:**
-This is an _alpha_ feature, and it is subject to change at any time without
+This is an _alpha_ feature, and is subject to change at any time without
prior notice!
GitLab supports both simple and complex strategies, so it's possible to use an
@@ -719,7 +1321,7 @@ This means the keys are treated as if joined by an OR. This relationship could b
In the example below, the `test` job will **not** be created when **any** of the following are true:
- The pipeline runs for the `master`.
-- There are changes to the `README.md` file in the root directory of the repo.
+- There are changes to the `README.md` file in the root directory of the repository.
```yaml
test:
@@ -813,7 +1415,7 @@ This means the `only:changes` policy is useful for pipelines where:
If there is no Git push event, such as for pipelines with
[sources other than the three above](../variables/predefined_variables.md#variables-reference),
-`changes` cannot determine if a given file is new or old, and will always
+`changes` can't determine if a given file is new or old, and will always
return true.
A basic example of using `only: changes`:
@@ -840,10 +1442,10 @@ commits contains changes to any of the following:
CAUTION: **Warning:**
If using `only:changes` with [only allow merge requests to be merged if the pipeline succeeds](../../user/project/merge_requests/merge_when_pipeline_succeeds.md#only-allow-merge-requests-to-be-merged-if-the-pipeline-succeeds),
-undesired behavior could result if you do not [also use `only:merge_requests`](#using-onlychanges-with-pipelines-for-merge-requests).
+undesired behavior could result if you don't [also use `only:merge_requests`](#using-onlychanges-with-pipelines-for-merge-requests).
You can also use glob patterns to match multiple files in either the root directory
-of the repo, or in _any_ directory within the repo, but they must be wrapped
+of the repository, or in _any_ directory within the repository, but they must be wrapped
in double quotes or GitLab will fail to parse the `.gitlab-ci.yml`. For example:
```yaml
@@ -856,7 +1458,7 @@ test:
```
The following example will skip the `build` job if a change is detected in any file
-in the root directory of the repo with a `.md` extension:
+in the root directory of the repository with a `.md` extension:
```yaml
build:
@@ -877,7 +1479,7 @@ There are some points to be aware of when
##### Using `only:changes` with pipelines for merge requests
With [pipelines for merge requests](../merge_request_pipelines/index.md),
-it is possible to define a job to be created based on files modified
+it's possible to define a job to be created based on files modified
in a merge request.
In order to deduce the correct base SHA of the source branch, we recommend combining
@@ -919,7 +1521,7 @@ docker build service one:
In the example above, a pipeline could fail due to changes to a file in `service-one/**/*`.
A later commit could then be pushed that does not include any changes to this file,
-but includes changes to the `Dockerfile`, and this pipeline could pass because it is only
+but includes changes to the `Dockerfile`, and this pipeline could pass because it's only
testing the changes to the `Dockerfile`. GitLab checks the **most recent pipeline**,
that **passed**, and will show the merge request as mergeable, despite the earlier
failed pipeline caused by a change that was not yet corrected.
@@ -944,279 +1546,189 @@ This could result in some unexpected behavior, including:
All files are considered to have "changed" when a scheduled pipeline
runs.
-### `rules`
-
-> [Introduced](https://gitlab.com/gitlab-org/gitlab-foss/-/merge_requests/29011) in GitLab 12.3.
+### `needs`
-`rules` allows for a list of individual rule objects to be evaluated
-*in order*, until one matches and dynamically provides attributes to the job.
-Note that `rules` cannot be used in combination with `only/except` since it is intended
-to replace that functionality. If you attempt to do this the linter will return a
-`key may not be used with rules` error.
+> - [Introduced](https://gitlab.com/gitlab-org/gitlab-foss/issues/47063) in GitLab 12.2.
+> - In GitLab 12.3, maximum number of jobs in `needs` array raised from five to 50.
+> - [Introduced](https://gitlab.com/gitlab-org/gitlab/issues/30631) in GitLab 12.8, `needs: []` lets jobs start immediately.
-Available rule clauses include:
+The `needs:` keyword enables executing jobs out-of-order, allowing you to implement
+a [directed acyclic graph](../directed_acyclic_graph/index.md) in your `.gitlab-ci.yml`.
-- [`if`](#rulesif) (similar to [`only:variables`](#onlyvariablesexceptvariables))
-- [`changes`](#ruleschanges) (same as [`only:changes`](#onlychangesexceptchanges))
-- [`exists`](#rulesexists)
+This lets you run some jobs without waiting for other ones, disregarding stage ordering
+so you can have multiple stages running concurrently.
-For example, using `if`. This configuration specifies that `job` should be built
-and run for every pipeline on merge requests targeting `master`, regardless of
-the status of other builds:
+Let's consider the following example:
```yaml
-job:
- script: "echo Hello, Rules!"
- rules:
- - if: '$CI_MERGE_REQUEST_TARGET_BRANCH_NAME == "master"'
- when: always
- - if: '$VAR =~ /pattern/'
- when: manual
- - when: on_success
-```
-
-In this example, if the first rule:
-
-- Matches, the job will be given the `when:always` attribute.
-- Does not match, the second and third rules will be evaluated sequentially
- until a match is found. That is, the job will be given either the:
- - `when: manual` attribute if the second rule matches. **The stage will not complete until this manual job is triggered and completes successfully.**
- - `when: on_success` attribute if the second rule does not match. The third
- rule will always match when reached because it has no conditional clauses.
-
-#### `rules:if`
-
-`rules:if` differs slightly from `only:variables` by accepting only a single
-expression string, rather than an array of them. Any set of expressions to be
-evaluated should be conjoined into a single expression using `&&` or `||`, and use
-the [variable matching syntax](../variables/README.md#supported-syntax).
+linux:build:
+ stage: build
-For example:
+mac:build:
+ stage: build
-```yaml
-job:
- script: "echo Hello, Rules!"
- rules:
- - if: '$CI_MERGE_REQUEST_SOURCE_BRANCH_NAME =~ /^feature/ && $CI_MERGE_REQUEST_TARGET_BRANCH_NAME == "master"' # This rule will be evaluated
- when: always
- - if: '$CI_MERGE_REQUEST_SOURCE_BRANCH_NAME =~ /^feature/' # This rule will only be evaluated if the target branch is not "master"
- when: manual
- - if: '$CI_MERGE_REQUEST_SOURCE_BRANCH_NAME' # If neither of the first two match but the simple presence does, we set to "on_success" by default
-```
+lint:
+ stage: test
+ needs: []
-If none of the provided rules match, the job will be set to `when:never`, and
-not included in the pipeline. If `rules:when` is not included in the configuration
-at all, the behavior defaults to `job:when`, which continues to default to
-`on_success`.
+linux:rspec:
+ stage: test
+ needs: ["linux:build"]
-#### `rules:changes`
+linux:rubocop:
+ stage: test
+ needs: ["linux:build"]
-`rules: changes` works exactly the same way as `only: changes` and `except: changes`,
-accepting an array of paths. Similarly, it will always return true if there is no
-Git push event. See [`only/except: changes`](#onlychangesexceptchanges) for more information.
+mac:rspec:
+ stage: test
+ needs: ["mac:build"]
-For example:
+mac:rubocop:
+ stage: test
+ needs: ["mac:build"]
-```yaml
-docker build:
- script: docker build -t my-image:$CI_COMMIT_REF_SLUG .
- rules:
- - changes: # Will include the job and set to when:manual if any of the follow paths match a modified file.
- - Dockerfile
- when: manual
- - if: '$VAR == "string value"'
- when: manual # Will include the job and set to when:manual if the expression evaluates to true, after the `changes:` rule fails to match.
- - when: on_success # If neither of the first rules match, set to on_success
+production:
+ stage: deploy
```
-In this example, a job either set to:
+This example creates four paths of execution:
-- Run manually if `Dockerfile` has changed OR `$VAR == "string value"`.
-- `when:on_success` by the last rule, where no earlier clauses evaluate to true.
+- Linter: the `lint` job will run immediately without waiting for the `build` stage to complete because it has no needs (`needs: []`).
-#### `rules:exists`
+- Linux path: the `linux:rspec` and `linux:rubocop` jobs will be run as soon
+ as the `linux:build` job finishes without waiting for `mac:build` to finish.
-> [Introduced](https://gitlab.com/gitlab-org/gitlab/-/merge_requests/16574) in GitLab 12.4.
+- macOS path: the `mac:rspec` and `mac:rubocop` jobs will be run as soon
+ as the `mac:build` job finishes, without waiting for `linux:build` to finish.
-`exists` accepts an array of paths and will match if any of these paths exist
-as files in the repository.
+- The `production` job will be executed as soon as all previous jobs
+ finish; in this case: `linux:build`, `linux:rspec`, `linux:rubocop`,
+ `mac:build`, `mac:rspec`, `mac:rubocop`.
-For example:
+#### Requirements and limitations
-```yaml
-job:
- script: docker build -t my-image:$CI_COMMIT_REF_SLUG .
- rules:
- - exists:
- - Dockerfile
-```
+- If `needs:` is set to point to a job that is not instantiated
+ because of `only/except` rules or otherwise does not exist, the
+ pipeline will be created with YAML error.
+- The maximum number of jobs that a single job can need in the `needs:` array is limited:
+ - For GitLab.com, the limit is ten. For more information, see our
+ [infrastructure issue](https://gitlab.com/gitlab-com/gl-infra/infrastructure/issues/7541).
+ - For self-managed instances, the limit is:
+ - 10, if the `ci_dag_limit_needs` feature flag is enabled (default).
+ - 50, if the `ci_dag_limit_needs` feature flag is disabled.
+- If `needs:` refers to a job that is marked as `parallel:`.
+ the current job will depend on all parallel jobs created.
+- `needs:` is similar to `dependencies:` in that it needs to use jobs from prior stages,
+ meaning it's impossible to create circular dependencies. Depending on jobs in the
+ current stage is not possible either, but support [is planned](https://gitlab.com/gitlab-org/gitlab/issues/30632).
+- Related to the above, stages must be explicitly defined for all jobs
+ that have the keyword `needs:` or are referred to by one.
-You can also use glob patterns to match multiple files in any directory within
-the repository.
+##### Changing the `needs:` job limit
-For example:
+The maximum number of jobs that can be defined within `needs:` defaults to 10, but
+can be changed to 50 via a feature flag. To change the limit to 50,
+[start a Rails console session](../../administration/troubleshooting/debug.md#starting-a-rails-console-session)
+and run:
-```yaml
-job:
- script: bundle exec rspec
- rules:
- - exists:
- - spec/**.rb
+```ruby
+Feature::disable(:ci_dag_limit_needs)
```
-NOTE: **Note:**
-For performance reasons, using `exists` with patterns is limited to 10000
-checks. After the 10000th check, rules with patterned globs will always match.
-
-#### `rules:allow_failure`
-
-> [Introduced](https://gitlab.com/gitlab-org/gitlab/issues/30235) in GitLab 12.8.
-
-You can use [`allow_failure: true`](#allow_failure) within `rules:` to allow a job to fail, or a manual job to
-wait for action, without stopping the pipeline itself. All jobs using `rules:` default to `allow_failure: false`
-if `allow_failure:` is not defined.
+To set it back to 10, run the opposite command:
-```yaml
-job:
- script: "echo Hello, Rules!"
- rules:
- - if: '$CI_MERGE_REQUEST_TARGET_BRANCH_NAME == "master"'
- when: manual
- allow_failure: true
+```ruby
+Feature::enable(:ci_dag_limit_needs)
```
-In this example, if the first rule matches, then the job will have `when: manual` and `allow_failure: true`.
-
-#### Exclude jobs with `rules:` from certain pipelines
-
-Jobs with `rules:` can cause two pipelines to be created unexpectedly:
-
-- One pipeline from pushing a commit to a branch.
-- A second ["detached" pipeline for a merge request](../merge_request_pipelines/index.md).
-
-`only` and `except` jobs do not trigger merge request pipelines by default, but this
-is not the case for jobs with `rules:`, which may be surprising if migrating from `only`
-and `except` to `rules:`.
-
-If you are using `rules:` and you see two pipelines for commits to branches that have
-a merge request, you have two options:
-
-- Individually exclude each job that uses `rules:` from merge request pipelines. The
- example below will cause the job to **not** run in *pipelines for merge requests*,
- but it **will** run in pipelines for *new tags and pipelines running on branch refs*:
+#### Artifact downloads with `needs`
- ```yaml
- job:
- rules:
- - if: $CI_MERGE_REQUEST_ID
- when: never
- - when: manual
- script:
- - echo hello
- ```
+> [Introduced](https://gitlab.com/gitlab-org/gitlab/issues/14311) in GitLab v12.6.
-- Add a global [`workflow: rules`](#workflowrules) to allow pipelines in only certain
- situations. The example below will only run pipelines for merge requests, new tags and
- changes to master. It will **not** run any pipelines *on any branch except master*, but
- it will run **detached merge request pipelines** for any merge request, targeting any branch:
+When using `needs`, artifact downloads are controlled with `artifacts: true` or `artifacts: false`.
+The `dependencies` keyword should not be used with `needs`, as this is deprecated since GitLab 12.6.
- ```yaml
- workflow:
- rules:
- - if: $CI_MERGE_REQUEST_ID
- - if: $CI_COMMIT_TAG
- - if: $CI_COMMIT_BRANCH == "master"
- ```
+In the example below, the `rspec` job will download the `build_job` artifacts, while the
+`rubocop` job won't:
-#### Complex rule clauses
+```yaml
+build_job:
+ stage: build
+ artifacts:
+ paths:
+ - binaries/
-To conjoin `if`, `changes`, and `exists` clauses with an AND, use them in the
-same rule.
+rspec:
+ stage: test
+ needs:
+ - job: build_job
+ artifacts: true
-In the following example:
+rubocop:
+ stage: test
+ needs:
+ - job: build_job
+ artifacts: false
+```
-- We run the job manually if `Dockerfile` or any file in `docker/scripts/`
- has changed AND `$VAR == "string value"`.
-- Otherwise, the job will not be included in the pipeline.
+Additionally, in the three syntax examples below, the `rspec` job will download the artifacts
+from all three `build_jobs`, as `artifacts` is true for `build_job_1`, and will
+**default** to true for both `build_job_2` and `build_job_3`.
```yaml
-docker build:
- script: docker build -t my-image:$CI_COMMIT_REF_SLUG .
- rules:
- - if: '$VAR == "string value"'
- changes: # Will include the job and set to when:manual if any of the follow paths match a modified file.
- - Dockerfile
- - docker/scripts/*
- when: manual
- # - when: never would be redundant here, this is implied any time rules are listed.
+rspec:
+ needs:
+ - job: build_job_1
+ artifacts: true
+ - job: build_job_2
+ - build_job_3
```
-The only clauses currently available are:
-
-- `if`
-- `changes`
-- `exists`
-
-Keywords such as `branches` or `refs` that are currently available for
-`only`/`except` are not yet available in `rules` as they are being individually
-considered for their usage and behavior in this context.
-
-#### Permitted attributes
-
-The only job attributes currently set by `rules` are:
-
-- `when`.
-- `start_in`, if `when` is set to `delayed`.
-- `allow_failure`.
+#### Cross project artifact downloads with `needs` **(PREMIUM)**
-A job will be included in a pipeline if `when` is evaluated to any value
-except `never`.
+> [Introduced](https://gitlab.com/gitlab-org/gitlab/issues/14311) in GitLab v12.7.
-Delayed jobs require a `start_in` value, so rule objects do as well. For
-example:
+`needs` can be used to download artifacts from up to five jobs in pipelines on
+[other refs in the same project](#artifact-downloads-between-pipelines-in-the-same-project),
+or pipelines in different projects:
```yaml
-docker build:
- script: docker build -t my-image:$CI_COMMIT_REF_SLUG .
- rules:
- - changes: # Will include the job and delay 3 hours when the Dockerfile has changed
- - Dockerfile
- when: delayed
- start_in: '3 hours'
- - when: on_success # Otherwise include the job and set to run normally
+build_job:
+ stage: build
+ script:
+ - ls -lhR
+ needs:
+ - project: group/project-name
+ job: build-1
+ ref: master
+ artifacts: true
```
-Additional job configuration may be added to rules in the future. If something
-useful isn't available, please
-[open an issue](https://gitlab.com/gitlab-org/gitlab/issues).
-
-### `workflow:rules`
-
-> [Introduced](https://gitlab.com/gitlab-org/gitlab/issues/29654) in GitLab 12.5
-
-The top-level `workflow:` key applies to the entirety of a pipeline, and will
-determine whether or not a pipeline is created. It currently accepts a single
-`rules:` key that operates similarly to [`rules:` defined within jobs](#rules),
-enabling dynamic configuration of the pipeline.
-
-The configuration options currently available for `workflow:rules` are:​
+`build_job` will download the artifacts from the latest successful `build-1` job
+on the `master` branch in the `group/project-name` project.
-- [`if`](#rulesif): Define a rule.
-- [`when`](#when): May be set to `always` or `never` only. If not provided, the default value is `always`​.
+##### Artifact downloads between pipelines in the same project
-The list of `if` rules is evaluated until a single one is matched. If none
-match, the last `when` will be used:
+`needs` can be used to download artifacts from different pipelines in the current project
+by setting the `project` keyword as the current project's name, and specifying a ref.
+In the example below, `build_job` will download the artifacts for the latest successful
+`build-1` job with the `other-ref` ref:
```yaml
-workflow:
- rules:
- - if: $CI_COMMIT_REF_NAME =~ /-wip$/
- when: never
- - if: $CI_COMMIT_TAG
- when: never
- - when: always
+build_job:
+ stage: build
+ script:
+ - ls -lhR
+ needs:
+ - project: group/same-project-name
+ job: build-1
+ ref: other-ref
+ artifacts: true
```
+NOTE: **Note:**
+Downloading artifacts from jobs that are run in [`parallel:`](#parallel) is not supported.
+
### `tags`
`tags` is used to select specific Runners from the list of all Runners that are
@@ -1277,7 +1789,7 @@ show the same orange warning. However, the associated commit will be marked
"passed", without warnings.
In the example below, `job1` and `job2` will run in parallel, but if `job1`
-fails, it will not stop the next stage from running, since it's marked with
+fails, it won't stop the next stage from running, since it's marked with
`allow_failure: true`:
```yaml
@@ -1372,21 +1884,21 @@ Manual actions are a special type of job that are not executed automatically,
they need to be explicitly started by a user. An example usage of manual actions
would be a deployment to a production environment. Manual actions can be started
from the pipeline, job, environment, and deployment views. Read more at the
-[environments documentation](../environments.md#configuring-manual-deployments).
+[environments documentation](../environments/index.md#configuring-manual-deployments).
Manual actions can be either optional or blocking. Blocking manual actions will
block the execution of the pipeline at the stage this action is defined in. It's
possible to resume execution of the pipeline when someone executes a blocking
manual action by clicking a _play_ button.
-When a pipeline is blocked, it will not be merged if Merge When Pipeline Succeeds
+When a pipeline is blocked, it won't be merged if Merge When Pipeline Succeeds
is set. Blocked pipelines also do have a special status, called _manual_.
When the `when:manual` syntax is used, manual actions are non-blocking by
-default. If you want to make manual action blocking, it is necessary to add
+default. If you want to make manual action blocking, it's necessary to add
`allow_failure: false` to the job's definition in `.gitlab-ci.yml`.
Optional manual actions have `allow_failure: true` set by default and their
-Statuses do not contribute to the overall pipeline status. So, if a manual
+Statuses don't contribute to the overall pipeline status. So, if a manual
action fails, the pipeline will eventually succeed.
NOTE: **Note:**
@@ -1396,7 +1908,7 @@ Manual actions are considered to be write actions, so permissions for
[protected branches](../../user/project/protected_branches.md) are used when
a user wants to trigger an action. In other words, in order to trigger a manual
action assigned to a branch that the pipeline is running for, the user needs to
-have the ability to merge to this branch. It is possible to use protected environments
+have the ability to merge to this branch. It's possible to use protected environments
to more strictly [protect manual deployments](#protecting-manual-jobs-premium) from being
run by unauthorized users.
@@ -1409,7 +1921,7 @@ being used.
It's possible to use [protected environments](../environments/protected_environments.md)
to define a precise list of users authorized to run a manual job. By allowing only
-users associated with a protected environment to trigger manual jobs, it is possible
+users associated with a protected environment to trigger manual jobs, it's possible
to implement some special use cases, such as:
- More precisely limiting who can deploy to an environment.
@@ -1439,13 +1951,13 @@ To do this, you must:
who are always able to use protected environments.
Additionally, if a manual job is defined as blocking by adding `allow_failure: false`,
-the next stages of the pipeline will not run until the manual job is triggered. This
+the next stages of the pipeline won't run until the manual job is triggered. This
can be used as a way to have a defined list of users allowed to "approve" later pipeline
stages by triggering the blocking manual job.
#### `when:delayed`
-> [Introduced](https://gitlab.com/gitlab-org/gitlab-foss/-/merge_requests/21767) in GitLab 11.4.
+> [Introduced](https://gitlab.com/gitlab-org/gitlab-foss/-/issues/51352) in GitLab 11.4.
Delayed job are for executing scripts after a certain period.
This is useful if you want to avoid jobs entering `pending` state immediately.
@@ -1459,11 +1971,11 @@ provided. `start_in` key must be less than or equal to one week. Examples of val
- `1 day`
- `1 week`
-When there is a delayed job in a stage, the pipeline will not progress until the delayed job has finished.
+When there is a delayed job in a stage, the pipeline won't progress until the delayed job has finished.
This means this keyword can also be used for inserting delays between different stages.
The timer of a delayed job starts immediately after the previous stage has completed.
-Similar to other types of jobs, a delayed job's timer will not start unless the previous stage passed.
+Similar to other types of jobs, a delayed job's timer won't start unless the previous stage passed.
The following example creates a job named `timed rollout 10%` that is executed 30 minutes after the previous stage has completed:
@@ -1475,7 +1987,7 @@ timed rollout 10%:
start_in: 30 minutes
```
-You can stop the active timer of a delayed job by clicking the **Unschedule** button.
+You can stop the active timer of a delayed job by clicking the **{time-out}** (**Unschedule**) button.
This job will never be executed in the future unless you execute the job manually.
You can start a delayed job immediately by clicking the **Play** button.
@@ -1485,7 +1997,7 @@ GitLab Runner will pick your job soon and start the job.
> - Introduced in GitLab 8.9.
> - You can read more about environments and find more examples in the
-> [documentation about environments](../environments.md).
+> [documentation about environments](../environments/index.md).
`environment` is used to define that a job deploys to a specific environment.
If `environment` is specified and no environment under that name exists, a new
@@ -1511,7 +2023,7 @@ deployment to the `production` environment.
> `name` keyword.
> - The `name` parameter can use any of the defined CI variables,
> including predefined, secure variables and `.gitlab-ci.yml` [`variables`](#variables).
-> You however cannot use variables defined under `script`.
+> You however can't use variables defined under `script`.
The `environment` name can contain:
@@ -1529,7 +2041,7 @@ Common names are `qa`, `staging`, and `production`, but you can use whatever
name works with your workflow.
Instead of defining the name of the environment right after the `environment`
-keyword, it is also possible to define it as a separate value. For that, use
+keyword, it's also possible to define it as a separate value. For that, use
the `name` keyword under `environment`:
```yaml
@@ -1547,7 +2059,7 @@ deploy to production:
> recommended way now is to define it in `.gitlab-ci.yml`.
> - The `url` parameter can use any of the defined CI variables,
> including predefined, secure variables and `.gitlab-ci.yml` [`variables`](#variables).
-> You however cannot use variables defined under `script`.
+> You however can't use variables defined under `script`.
This is an optional value that when set, it exposes buttons in various places
in GitLab which when clicked take you to the defined URL.
@@ -1567,7 +2079,7 @@ deploy to production:
#### `environment:on_stop`
-> - [Introduced](https://gitlab.com/gitlab-org/gitlab-foss/-/merge_requests/6669) in GitLab 8.13.
+> - [Introduced](https://gitlab.com/gitlab-org/gitlab-foss/-/issues/22191) in GitLab 8.13.
> - Starting with GitLab 8.14, when you have an environment that has a stop action
> defined, GitLab will automatically trigger a stop action when the associated
> branch is deleted.
@@ -1580,7 +2092,7 @@ Read the `environment:action` section for an example.
#### `environment:action`
-> [Introduced](https://gitlab.com/gitlab-org/gitlab-foss/-/merge_requests/6669) in GitLab 8.13.
+> [Introduced](https://gitlab.com/gitlab-org/gitlab-foss/-/issues/22191) in GitLab 8.13.
The `action` keyword is to be used in conjunction with `on_stop` and is defined
in the job that is called to close the environment.
@@ -1615,7 +2127,7 @@ GitLab's web interface in order to run.
Also in the example, `GIT_STRATEGY` is set to `none` so that GitLab Runner won’t
try to check out the code after the branch is deleted when the `stop_review_app`
-job is [automatically triggered](../environments.md#automatically-stopping-an-environment).
+job is [automatically triggered](../environments/index.md#automatically-stopping-an-environment).
NOTE: **Note:**
The above example overwrites global variables. If your stop environment job depends
@@ -1651,7 +2163,7 @@ When `review_app` job is executed and a review app is created, a life period of
the environment is set to `1 day`.
For more information, see
-[the environments auto-stop documentation](../environments.md#environments-auto-stop)
+[the environments auto-stop documentation](../environments/index.md#environments-auto-stop)
#### `environment:kubernetes`
@@ -1677,7 +2189,7 @@ environment, using the `production`
[Kubernetes namespace](https://kubernetes.io/docs/concepts/overview/working-with-objects/namespaces/).
For more information, see
-[Available settings for `kubernetes`](../environments.md#configuring-kubernetes-deployments).
+[Available settings for `kubernetes`](../environments/index.md#configuring-kubernetes-deployments).
NOTE: **Note:**
Kubernetes configuration is not supported for Kubernetes clusters
@@ -1687,11 +2199,11 @@ To follow progress on support for GitLab-managed clusters, see the
#### Dynamic environments
-> - [Introduced](https://gitlab.com/gitlab-org/gitlab-foss/-/merge_requests/6323) in GitLab 8.12 and GitLab Runner 1.6.
-> - The `$CI_ENVIRONMENT_SLUG` was [introduced](https://gitlab.com/gitlab-org/gitlab-foss/-/merge_requests/7983) in GitLab 8.15.
+> - [Introduced](https://gitlab.com/gitlab-org/gitlab-foss/-/issues/21971) in GitLab 8.12 and GitLab Runner 1.6.
+> - The `$CI_ENVIRONMENT_SLUG` was [introduced](https://gitlab.com/gitlab-org/gitlab-foss/-/issues/22864) in GitLab 8.15.
> - The `name` and `url` parameters can use any of the defined CI variables,
> including predefined, secure variables and `.gitlab-ci.yml` [`variables`](#variables).
-> You however cannot use variables defined under `script`.
+> You however can't use variables defined under `script`.
For example:
@@ -1735,15 +2247,20 @@ Read how caching works and find out some good practices in the
cached between jobs. You can only use paths that are within the local working
copy.
-If `cache` is defined outside the scope of jobs, it means it is set
+If `cache` is defined outside the scope of jobs, it means it's set
globally and all jobs will use that definition.
#### `cache:paths`
Use the `paths` directive to choose which files or directories will be cached. Paths
-are relative to the project directory (`$CI_PROJECT_DIR`) and cannot directly link outside it.
+are relative to the project directory (`$CI_PROJECT_DIR`) and can't directly link outside it.
Wildcards can be used that follow the [glob](https://en.wikipedia.org/wiki/Glob_(programming))
-patterns and [`filepath.Match`](https://golang.org/pkg/path/filepath/#Match).
+patterns and:
+
+- In [GitLab Runner 13.0](https://gitlab.com/gitlab-org/gitlab-runner/-/issues/2620) and later,
+[`doublestar.Glob`](https://pkg.go.dev/github.com/bmatcuk/doublestar@v1.2.2?tab=doc#Match).
+- In GitLab Runner 12.10 and earlier,
+[`filepath.Match`](https://pkg.go.dev/path/filepath/#Match).
Cache all files in `binaries` that end in `.apk` and the `.config` file:
@@ -1795,7 +2312,7 @@ set, is just literal `default` which means everything is shared between
pipelines and jobs by default, starting from GitLab 9.0.
NOTE: **Note:**
-The `cache:key` variable cannot contain the `/` character, or the equivalent
+The `cache:key` variable can't contain the `/` character, or the equivalent
URI-encoded `%2F`; a value made only of dots (`.`, `%2E`) is also forbidden.
For example, to enable per-branch caching:
@@ -1841,7 +2358,7 @@ cache:
- node_modules
```
-In this example we are creating a cache for Ruby and Node.js dependencies that
+In this example we're creating a cache for Ruby and Node.js dependencies that
is tied to current versions of the `Gemfile.lock` and `package.json` files. Whenever one of
these files changes, a new cache key is computed and a new cache is created. Any future
job runs using the same `Gemfile.lock` and `package.json` with `cache:key:files` will
@@ -1912,12 +2429,12 @@ rspec:
> Introduced in GitLab 9.4.
-The default behaviour of a caching job is to download the files at the start of
+The default behavior of a caching job is to download the files at the start of
execution, and to re-upload them at the end. This allows any changes made by the
job to be persisted for future runs, and is known as the `pull-push` cache
policy.
-If you know the job doesn't alter the cached files, you can skip the upload step
+If you know the job does not alter the cached files, you can skip the upload step
by setting `policy: pull` in the job specification. Typically, this would be
twinned with an ordinary cache job at an earlier stage to ensure the cache
is updated from time to time:
@@ -1973,7 +2490,7 @@ be available for download in the GitLab UI.
#### `artifacts:paths`
-Paths are relative to the project directory (`$CI_PROJECT_DIR`) and cannot directly
+Paths are relative to the project directory (`$CI_PROJECT_DIR`) and can't directly
link outside it. Wildcards can be used that follow the [glob](https://en.wikipedia.org/wiki/Glob_(programming))
patterns and [`filepath.Match`](https://golang.org/pkg/path/filepath/#Match).
@@ -2000,7 +2517,7 @@ job:
You may want to create artifacts only for tagged releases to avoid filling the
build server storage with temporary build artifacts.
-Create artifacts only for tags (`default-job` will not create artifacts):
+Create artifacts only for tags (`default-job` won't create artifacts):
```yaml
default-job:
@@ -2037,7 +2554,7 @@ in the [merge request](../../user/project/merge_requests/index.md) UI.
For example, to match a single file:
-```yml
+```yaml
test:
script: [ 'echo 1' ]
artifacts:
@@ -2050,7 +2567,7 @@ that points to `file1.txt`.
An example that will match an entire directory:
-```yml
+```yaml
test:
script: [ 'echo 1' ]
artifacts:
@@ -2081,7 +2598,7 @@ The default name is `artifacts`, which becomes `artifacts.zip` when downloaded.
NOTE: **Note:**
If your branch-name contains forward slashes
-(e.g. `feature/my-feature`) it is advised to use `$CI_COMMIT_REF_SLUG`
+(for example `feature/my-feature`) it's advised to use `$CI_COMMIT_REF_SLUG`
instead of `$CI_COMMIT_REF_NAME` for proper naming of the artifact.
To create an archive with a name of the current job:
@@ -2212,15 +2729,15 @@ After their expiry, artifacts are deleted hourly by default (via a cron job),
and are not accessible anymore.
The value of `expire_in` is an elapsed time in seconds, unless a unit is
-provided. Examples of parsable values:
+provided. Examples of valid values:
-- '42'
-- '3 mins 4 sec'
-- '2 hrs 20 min'
-- '2h20min'
-- '6 mos 1 day'
-- '47 yrs 6 mos and 4d'
-- '3 weeks and 2 days'
+- `42`
+- `3 mins 4 sec`
+- `2 hrs 20 min`
+- `2h20min`
+- `6 mos 1 day`
+- `47 yrs 6 mos and 4d`
+- `3 weeks and 2 days`
To expire artifacts 1 week after being uploaded:
@@ -2230,187 +2747,35 @@ job:
expire_in: 1 week
```
-#### `artifacts:reports`
-
-> [Introduced](https://gitlab.com/gitlab-org/gitlab-foss/-/merge_requests/20390) in GitLab 11.2. Requires GitLab Runner 11.2 and above.
-
-The `reports` keyword is used for collecting test reports, code quality reports, and security reports from jobs.
-It also exposes these reports in GitLab's UI (merge requests, pipeline views, and security dashboards).
-
-NOTE: **Note:**
-The test reports are collected regardless of the job results (success or failure).
-You can use [`artifacts:expire_in`](#artifactsexpire_in) to set up an expiration
-date for their artifacts.
-
-NOTE: **Note:**
-If you also want the ability to browse the report output files, include the
-[`artifacts:paths`](#artifactspaths) keyword.
-
-##### `artifacts:reports:junit`
-
-> [Introduced](https://gitlab.com/gitlab-org/gitlab-foss/-/merge_requests/20390) in GitLab 11.2. Requires GitLab Runner 11.2 and above.
-
-The `junit` report collects [JUnit XML files](https://www.ibm.com/support/knowledgecenter/en/SSQ2R2_14.1.0/com.ibm.rsar.analysis.codereview.cobol.doc/topics/cac_useresults_junit.html)
-as artifacts. Although JUnit was originally developed in Java, there are many
-[third party ports](https://en.wikipedia.org/wiki/JUnit#Ports) for other
-languages like JavaScript, Python, Ruby, etc.
-
-See [JUnit test reports](../junit_test_reports.md) for more details and examples.
-Below is an example of collecting a JUnit XML file from Ruby's RSpec test tool:
-
-```yaml
-rspec:
- stage: test
- script:
- - bundle install
- - rspec --format RspecJunitFormatter --out rspec.xml
- artifacts:
- reports:
- junit: rspec.xml
-```
-
-The collected JUnit reports will be uploaded to GitLab as an artifact and will
-be automatically shown in merge requests.
-
NOTE: **Note:**
-In case the JUnit tool you use exports to multiple XML files, you can specify
-multiple test report paths within a single job and they will be automatically
-concatenated into a single file. Use a filename pattern (`junit: rspec-*.xml`),
-an array of filenames (`junit: [rspec-1.xml, rspec-2.xml, rspec-3.xml]`), or a
-combination thereof (`junit: [rspec.xml, test-results/TEST-*.xml]`).
-
-##### `artifacts:reports:dotenv`
-
-> [Introduced](https://gitlab.com/gitlab-org/gitlab/issues/17066) in GitLab 12.9. Requires GitLab Runner 11.5 and later.
-
-The `dotenv` report collects a set of environment variables as artifacts.
-
-The collected variables are registered as runtime-created variables of the job,
-which is useful to [set dynamic environment URLs after a job finishes](../environments.md#set-dynamic-environment-urls-after-a-job-finishes).
-It is not available for download through the web interface.
-
-There are a couple of limitations on top of the [original dotenv rules](https://github.com/motdotla/dotenv#rules).
-
-- The variable key can contain only letters, digits and underscore ('_').
-- The size of dotenv file must be smaller than 5 kilobytes.
-- The number of variables must be less than 10.
-- It doesn't support variable substitution in the dotenv file itself.
-- It doesn't support empty lines and comments (`#`) in dotenv file.
-- It doesn't support quote escape, spaces in a quote, a new line expansion in a quote, in dotenv file.
-
-##### `artifacts:reports:cobertura`
-
-> [Introduced](https://gitlab.com/gitlab-org/gitlab/issues/3708) in GitLab 12.9. Requires [GitLab Runner](https://docs.gitlab.com/runner/) 11.5 and above.
-
-The `cobertura` report collects [Cobertura coverage XML files](../../user/project/merge_requests/test_coverage_visualization.md).
-The collected Cobertura coverage reports will be uploaded to GitLab as an artifact
-and will be automatically shown in merge requests.
-
-Cobertura was originally developed for Java, but there are many
-third party ports for other languages like JavaScript, Python, Ruby, etc.
-
-##### `artifacts:reports:codequality` **(STARTER)**
-
-> Introduced in GitLab 11.5. Requires GitLab Runner 11.5 and above.
-
-The `codequality` report collects [CodeQuality issues](../../user/project/merge_requests/code_quality.md)
-as artifacts.
-
-The collected Code Quality report will be uploaded to GitLab as an artifact and will
-be summarized in merge requests. It is not available for download through the web interface.
-
-##### `artifacts:reports:sast` **(ULTIMATE)**
-
-> Introduced in GitLab 11.5. Requires GitLab Runner 11.5 and above.
-
-The `sast` report collects [SAST vulnerabilities](../../user/application_security/sast/index.md)
-as artifacts.
-
-The collected SAST report will be uploaded to GitLab as an artifact and will
-be summarized in the merge requests and pipeline view. It is also used to provide data for security
-dashboards. It is not available for download through the web interface.
-
-##### `artifacts:reports:dependency_scanning` **(ULTIMATE)**
-
-> Introduced in GitLab 11.5. Requires GitLab Runner 11.5 and above.
-
-The `dependency_scanning` report collects [Dependency Scanning vulnerabilities](../../user/application_security/dependency_scanning/index.md)
-as artifacts.
-
-The collected Dependency Scanning report will be uploaded to GitLab as an artifact and will
-be summarized in the merge requests and pipeline view. It is also used to provide data for security
-dashboards. It is not available for download through the web interface.
-
-##### `artifacts:reports:container_scanning` **(ULTIMATE)**
-
-> Introduced in GitLab 11.5. Requires GitLab Runner 11.5 and above.
-
-The `container_scanning` report collects [Container Scanning vulnerabilities](../../user/application_security/container_scanning/index.md)
-as artifacts.
-
-The collected Container Scanning report will be uploaded to GitLab as an artifact and will
-be summarized in the merge requests and pipeline view. It is also used to provide data for security
-dashboards. It is not available for download through the web interface.
-
-##### `artifacts:reports:dast` **(ULTIMATE)**
-
-> Introduced in GitLab 11.5. Requires GitLab Runner 11.5 and above.
-
-The `dast` report collects [DAST vulnerabilities](../../user/application_security/dast/index.md)
-as artifacts.
-
-The collected DAST report will be uploaded to GitLab as an artifact and will
-be summarized in the merge requests and pipeline view. It is also used to provide data for security
-dashboards. It is not available for download through the web interface.
-
-##### `artifacts:reports:license_management` **(ULTIMATE)**
-
-CAUTION: **Warning:**
-This artifact is still valid but was **deprecated** in favor of the
-[artifacts:reports:license_scanning](#artifactsreportslicense_scanning-ultimate)
-introduced in GitLab 12.8.
-
-> Introduced in GitLab 11.5. Requires GitLab Runner 11.5 and above.
-
-The `license_management` report collects [Licenses](../../user/compliance/license_compliance/index.md)
-as artifacts.
-
-The collected License Compliance report will be uploaded to GitLab as an artifact and will
-be summarized in the merge requests and pipeline view. It is also used to provide data for security
-dashboards. It is not available for download through the web interface.
-
-##### `artifacts:reports:license_scanning` **(ULTIMATE)**
-
-> Introduced in GitLab 12.8. Requires GitLab Runner 11.5 and above.
+For artifacts created in [GitLab 13.0](https://gitlab.com/gitlab-org/gitlab/-/issues/16267)
+and later, the latest artifact for a ref is always kept, regardless of the expiry time.
-The `license_scanning` report collects [Licenses](../../user/compliance/license_compliance/index.md)
-as artifacts.
-
-The License Compliance report will be uploaded to GitLab as an artifact and will
-be automatically shown in merge requests, pipeline view and provide data for security
-dashboards.
-
-##### `artifacts:reports:performance` **(PREMIUM)**
-
-> Introduced in GitLab 11.5. Requires GitLab Runner 11.5 and above.
-
-The `performance` report collects [Performance metrics](../../user/project/merge_requests/browser_performance_testing.md)
-as artifacts.
-
-The collected Performance report will be uploaded to GitLab as an artifact and will
-be automatically shown in merge requests. It is not available for download through the web interface.
-
-##### `artifacts:reports:metrics` **(PREMIUM)**
-
-> Introduced in GitLab 11.10.
-
-The `metrics` report collects [Metrics](../../ci/metrics_reports.md)
-as artifacts.
+#### `artifacts:reports`
-The collected Metrics report will be uploaded to GitLab as an artifact and will
-be automatically shown in merge requests. It is not available for download through the web interface.
+The [`artifacts:reports` keyword](../pipelines/job_artifacts.md#artifactsreports)
+is used for collecting test reports, code quality reports, and security reports from jobs.
+It also exposes these reports in GitLab's UI (merge requests, pipeline views, and security dashboards).
-### `dependencies`
+These are the available report types:
+
+| Parameter | Description |
+|--------------------------------------------------------------------------------------------------------------------------------------|-------------|
+| [`artifacts:reports:junit`](../pipelines/job_artifacts.md#artifactsreportsjunit) | The `junit` report collects JUnit XML files. |
+| [`artifacts:reports:dotenv`](../pipelines/job_artifacts.md#artifactsreportsdotenv) | The `dotenv` report collects a set of environment variables. |
+| [`artifacts:reports:cobertura`](../pipelines/job_artifacts.md#artifactsreportscobertura) | The `cobertura` report collects Cobertura coverage XML files. |
+| [`artifacts:reports:terraform`](../pipelines/job_artifacts.md#artifactsreportsterraform) | The `terraform` report collects Terraform `tfplan.json` files. |
+| [`artifacts:reports:codequality`](../pipelines/job_artifacts.md#artifactsreportscodequality-starter) **(STARTER)** | The `codequality` report collects CodeQuality issues. |
+| [`artifacts:reports:sast`](../pipelines/job_artifacts.md#artifactsreportssast-ultimate) **(ULTIMATE)** | The `sast` report collects Static Application Security Testing vulnerabilities. |
+| [`artifacts:reports:dependency_scanning`](../pipelines/job_artifacts.md#artifactsreportsdependency_scanning-ultimate) **(ULTIMATE)** | The `dependency_scanning` report collects Dependency Scanning vulnerabilities. |
+| [`artifacts:reports:container_scanning`](../pipelines/job_artifacts.md#artifactsreportscontainer_scanning-ultimate) **(ULTIMATE)** | The `container_scanning` report collects Container Scanning vulnerabilities. |
+| [`artifacts:reports:dast`](../pipelines/job_artifacts.md#artifactsreportsdast-ultimate) **(ULTIMATE)** | The `dast` report collects Dynamic Application Security Testing vulnerabilities. |
+| [`artifacts:reports:license_management`](../pipelines/job_artifacts.md#artifactsreportslicense_management-ultimate) **(ULTIMATE)** | The `license_management` report collects Licenses (*removed from GitLab 13.0*). |
+| [`artifacts:reports:license_scanning`](../pipelines/job_artifacts.md#artifactsreportslicense_scanning-ultimate) **(ULTIMATE)** | The `license_scanning` report collects Licenses. |
+| [`artifacts:reports:performance`](../pipelines/job_artifacts.md#artifactsreportsperformance-premium) **(PREMIUM)** | The `performance` report collects Performance metrics. |
+| [`artifacts:reports:metrics`](../pipelines/job_artifacts.md#artifactsreportsmetrics-premium) **(PREMIUM)** | The `metrics` report collects Metrics. |
+
+#### `dependencies`
> Introduced in GitLab 8.6 and GitLab Runner v1.1.1.
@@ -2424,7 +2789,7 @@ You can only define jobs from stages that are executed before the current one.
An error will be shown if you define jobs from the current stage or next ones.
Defining an empty array will skip downloading any artifacts for that job.
The status of the previous job is not considered when using `dependencies`, so
-if it failed or it is a manual job that was not run, no error occurs.
+if it failed or it's a manual job that was not run, no error occurs.
In the following example, we define two jobs with artifacts, `build:osx` and
`build:linux`. When the `test:osx` is executed, the artifacts from `build:osx`
@@ -2466,7 +2831,7 @@ deploy:
script: make deploy
```
-#### When a dependent job will fail
+##### When a dependent job will fail
> Introduced in GitLab 10.3.
@@ -2480,193 +2845,9 @@ You can ask your administrator to
[flip this switch](../../administration/job_artifacts.md#validation-for-dependencies)
and bring back the old behavior.
-### `needs`
-
-> - [Introduced](https://gitlab.com/gitlab-org/gitlab-foss/issues/47063) in GitLab 12.2.
-> - In GitLab 12.3, maximum number of jobs in `needs` array raised from five to 50.
-> - [Introduced](https://gitlab.com/gitlab-org/gitlab/issues/30631) in GitLab 12.8, `needs: []` lets jobs start immediately.
-
-The `needs:` keyword enables executing jobs out-of-order, allowing you to implement
-a [directed acyclic graph](../directed_acyclic_graph/index.md) in your `.gitlab-ci.yml`.
-
-This lets you run some jobs without waiting for other ones, disregarding stage ordering
-so you can have multiple stages running concurrently.
-
-Let's consider the following example:
-
-```yaml
-linux:build:
- stage: build
-
-mac:build:
- stage: build
-
-lint:
- stage: test
- needs: []
-
-linux:rspec:
- stage: test
- needs: ["linux:build"]
-
-linux:rubocop:
- stage: test
- needs: ["linux:build"]
-
-mac:rspec:
- stage: test
- needs: ["mac:build"]
-
-mac:rubocop:
- stage: test
- needs: ["mac:build"]
-
-production:
- stage: deploy
-```
-
-This example creates four paths of execution:
-
-- Linter: the `lint` job will run immediately without waiting for the `build` stage to complete because it has no needs (`needs: []`).
-
-- Linux path: the `linux:rspec` and `linux:rubocop` jobs will be run as soon
- as the `linux:build` job finishes without waiting for `mac:build` to finish.
-
-- macOS path: the `mac:rspec` and `mac:rubocop` jobs will be run as soon
- as the `mac:build` job finishes, without waiting for `linux:build` to finish.
-
-- The `production` job will be executed as soon as all previous jobs
- finish; in this case: `linux:build`, `linux:rspec`, `linux:rubocop`,
- `mac:build`, `mac:rspec`, `mac:rubocop`.
-
-#### Requirements and limitations
-
-- If `needs:` is set to point to a job that is not instantiated
- because of `only/except` rules or otherwise does not exist, the
- pipeline will be created with YAML error.
-- We are temporarily limiting the maximum number of jobs that a single job can
- need in the `needs:` array:
- - For GitLab.com, the limit is ten. For more information, see our
- [infrastructure issue](https://gitlab.com/gitlab-com/gl-infra/infrastructure/issues/7541).
- - For self-managed instances, the limit is:
- - 10, if the `ci_dag_limit_needs` feature flag is enabled (default).
- - 50, if the `ci_dag_limit_needs` feature flag is disabled.
-- If `needs:` refers to a job that is marked as `parallel:`.
- the current job will depend on all parallel jobs created.
-- `needs:` is similar to `dependencies:` in that it needs to use jobs from prior stages,
- meaning it is impossible to create circular dependencies. Depending on jobs in the
- current stage is not possible either, but support [is planned](https://gitlab.com/gitlab-org/gitlab/issues/30632).
-- Related to the above, stages must be explicitly defined for all jobs
- that have the keyword `needs:` or are referred to by one.
-
-##### Changing the `needs:` job limit
-
-The maximum number of jobs that can be defined within `needs:` defaults to 10, but
-can be changed to 50 via a feature flag. To change the limit to 50,
-[start a Rails console session](../../administration/troubleshooting/debug.md#starting-a-rails-console-session)
-and run:
-
-```ruby
-Feature::disable(:ci_dag_limit_needs)
-```
-
-To set it back to 10, run the opposite command:
-
-```ruby
-Feature::enable(:ci_dag_limit_needs)
-```
-
-#### Artifact downloads with `needs`
-
-> [Introduced](https://gitlab.com/gitlab-org/gitlab/issues/14311) in GitLab v12.6.
-
-When using `needs`, artifact downloads are controlled with `artifacts: true` or `artifacts: false`.
-The `dependencies` keyword should not be used with `needs`, as this is deprecated since GitLab 12.6.
-
-In the example below, the `rspec` job will download the `build_job` artifacts, while the
-`rubocop` job will not:
-
-```yaml
-build_job:
- stage: build
- artifacts:
- paths:
- - binaries/
-
-rspec:
- stage: test
- needs:
- - job: build_job
- artifacts: true
-
-rubocop:
- stage: test
- needs:
- - job: build_job
- artifacts: false
-```
-
-Additionally, in the three syntax examples below, the `rspec` job will download the artifacts
-from all three `build_jobs`, as `artifacts` is true for `build_job_1`, and will
-**default** to true for both `build_job_2` and `build_job_3`.
-
-```yaml
-rspec:
- needs:
- - job: build_job_1
- artifacts: true
- - job: build_job_2
- - build_job_3
-```
-
-#### Cross project artifact downloads with `needs` **(PREMIUM)**
-
-> [Introduced](https://gitlab.com/gitlab-org/gitlab/issues/14311) in GitLab v12.7.
-
-`needs` can be used to download artifacts from up to five jobs in pipelines on
-[other refs in the same project](#artifact-downloads-between-pipelines-in-the-same-project),
-or pipelines in different projects:
-
-```yaml
-build_job:
- stage: build
- script:
- - ls -lhR
- needs:
- - project: group/project-name
- job: build-1
- ref: master
- artifacts: true
-```
-
-`build_job` will download the artifacts from the latest successful `build-1` job
-on the `master` branch in the `group/project-name` project.
-
-##### Artifact downloads between pipelines in the same project
-
-`needs` can be used to download artifacts from different pipelines in the current project
-by setting the `project` keyword as the current project's name, and specifying a ref.
-In the example below, `build_job` will download the artifacts for the latest successful
-`build-1` job with the `other-ref` ref:
-
-```yaml
-build_job:
- stage: build
- script:
- - ls -lhR
- needs:
- - project: group/same-project-name
- job: build-1
- ref: other-ref
- artifacts: true
-```
-
-NOTE: **Note:**
-Downloading artifacts from jobs that are run in [`parallel:`](#parallel) is not supported.
-
### `coverage`
-> [Introduced](https://gitlab.com/gitlab-org/gitlab-foss/-/merge_requests/7447) in GitLab 8.17.
+> [Introduced](https://gitlab.com/gitlab-org/gitlab-foss/-/issues/20428) in GitLab 8.17.
`coverage` allows you to configure how code coverage will be extracted from the
job output.
@@ -2686,13 +2867,13 @@ job1:
### `retry`
-> - [Introduced](https://gitlab.com/gitlab-org/gitlab-foss/-/merge_requests/12909) in GitLab 9.5.
-> - [Behaviour expanded](https://gitlab.com/gitlab-org/gitlab-foss/-/merge_requests/21758) in GitLab 11.5 to control on which failures to retry.
+> - [Introduced](https://gitlab.com/gitlab-org/gitlab-foss/-/issues/3442) in GitLab 9.5.
+> - [Behavior expanded](https://gitlab.com/gitlab-org/gitlab-runner/-/issues/3515) in GitLab 11.5 to control on which failures to retry.
`retry` allows you to configure how many times a job is going to be retried in
case of a failure.
-When a job fails and has `retry` configured, it is going to be processed again
+When a job fails and has `retry` configured, it's going to be processed again
up to the amount of times specified by the `retry` keyword.
If `retry` is set to 2, and a job succeeds in a second run (first retry), it won't be retried
@@ -2744,7 +2925,7 @@ Possible values for `when` are:
Please make sure to update `RETRY_WHEN_IN_DOCUMENTATION` array in
`spec/lib/gitlab/ci/config/entry/retry_spec.rb` if you change any of
the documented values below. The test there makes sure that all documented
- values are really valid as a config option and therefore should always
+ values are really valid as a configuration option and therefore should always
stay in sync with this documentation.
-->
@@ -2753,16 +2934,18 @@ Possible values for `when` are:
- `script_failure`: Retry when the script failed.
- `api_failure`: Retry on API failure.
- `stuck_or_timeout_failure`: Retry when the job got stuck or timed out.
-- `runner_system_failure`: Retry if there was a runner system failure (e.g. setting up the job failed).
+- `runner_system_failure`: Retry if there was a runner system failure (for example, job setup failed).
- `missing_dependency_failure`: Retry if a dependency was missing.
- `runner_unsupported`: Retry if the runner was unsupported.
- `stale_schedule`: Retry if a delayed job could not be executed.
- `job_execution_timeout`: Retry if the script exceeded the maximum execution time set for the job.
-- `archived_failure`: Retry if the job is archived and cannot be run.
+- `archived_failure`: Retry if the job is archived and can't be run.
- `unmet_prerequisites`: Retry if the job failed to complete prerequisite tasks.
- `scheduler_failure`: Retry if the scheduler failed to assign the job to a runner.
- `data_integrity_failure`: Retry if there was a structural integrity problem detected.
+You can specify the number of [retry attempts for certain stages of job execution](#job-stages-attempts) using variables.
+
### `timeout`
> [Introduced](https://gitlab.com/gitlab-org/gitlab/issues/14887) in GitLab 12.3.
@@ -2780,17 +2963,17 @@ test:
```
The job-level timeout can exceed the
-[project-level timeout](../pipelines/settings.md#timeout) but can not
+[project-level timeout](../pipelines/settings.md#timeout) but can't
exceed the Runner-specific timeout.
### `parallel`
-> [Introduced](https://gitlab.com/gitlab-org/gitlab-foss/-/merge_requests/22631) in GitLab 11.5.
+> [Introduced](https://gitlab.com/gitlab-org/gitlab-foss/-/issues/21480) in GitLab 11.5.
`parallel` allows you to configure how many instances of a job to run in
parallel. This value has to be greater than or equal to two (2) and less than or equal to 50.
-This creates N instances of the same job that run in parallel. They're named
+This creates N instances of the same job that run in parallel. They are named
sequentially from `job_name 1/N` to `job_name N/N`.
For every job, `CI_NODE_INDEX` and `CI_NODE_TOTAL` [environment variables](../variables/README.md#predefined-environment-variables) are set.
@@ -2868,7 +3051,7 @@ staging:
#### Complex `trigger` syntax for multi-project pipelines
-It is possible to configure a branch name that GitLab will use to create
+It's possible to configure a branch name that GitLab will use to create
a downstream pipeline with:
```yaml
@@ -2883,7 +3066,7 @@ staging:
branch: stable
```
-It is possible to mirror the status from a triggered pipeline:
+It's possible to mirror the status from a triggered pipeline:
```yaml
trigger_job:
@@ -2892,7 +3075,7 @@ trigger_job:
strategy: depend
```
-It is possible to mirror the status from an upstream pipeline:
+It's possible to mirror the status from an upstream pipeline:
```yaml
upstream_bridge:
@@ -2915,7 +3098,7 @@ trigger_job:
```
Similar to [multi-project pipelines](../multi_project_pipelines.md#mirroring-status-from-triggered-pipeline),
-it is possible to mirror the status from a triggered pipeline:
+it's possible to mirror the status from a triggered pipeline:
```yaml
trigger_job:
@@ -2971,9 +3154,18 @@ This can help keep your pipeline execution linear. In the example above, jobs fr
subsequent stages will wait for the triggered pipeline to successfully complete before
starting, at the cost of reduced parallelization.
+#### Trigger a pipeline by API call
+
+Triggers can be used to force a rebuild of a specific branch, tag or commit,
+with an API call when a pipeline gets created using a trigger token.
+
+Not to be confused with the [`trigger`](#trigger) parameter.
+
+[Read more in the triggers documentation.](../triggers/README.md)
+
### `interruptible`
-> [Introduced](https://gitlab.com/gitlab-org/gitlab-foss/-/merge_requests/23464) in GitLab 12.3.
+> [Introduced](https://gitlab.com/gitlab-org/gitlab/-/issues/32022) in GitLab 12.3.
`interruptible` is used to indicate that a job should be canceled if made redundant by a newer pipeline run. Defaults to `false`.
This value will only be used if the [automatic cancellation of redundant pipelines feature](../pipelines/settings.md#auto-cancel-pending-pipelines)
@@ -2981,8 +3173,8 @@ is enabled.
When enabled, a pipeline on the same branch will be canceled when:
-- It is made redundant by a newer pipeline run.
-- Either all jobs are set as interruptible, or any uninterruptible jobs have not started.
+- it's made redundant by a newer pipeline run.
+- Either all jobs are set as interruptible, or any uninterruptible jobs haven't started.
Pending jobs are always considered interruptible.
@@ -3031,7 +3223,7 @@ Sometimes running multiples jobs or pipelines at the same time in an environment
can lead to errors during the deployment.
To avoid these errors, the `resource_group` attribute can be used to ensure that
-the Runner will not run certain jobs simultaneously.
+the Runner won't run certain jobs simultaneously.
When the `resource_group` key is defined for a job in `.gitlab-ci.yml`,
job executions are mutually exclusive across different pipelines for the same project.
@@ -3048,7 +3240,7 @@ deploy-to-production:
```
In this case, if a `deploy-to-production` job is running in a pipeline, and a new
-`deploy-to-production` job is created in a different pipeline, it will not run until
+`deploy-to-production` job is created in a different pipeline, it won't run until
the currently running/pending `deploy-to-production` job is finished. As a result,
you can ensure that concurrent deployments will never happen to the production environment.
@@ -3057,514 +3249,8 @@ is when deploying to physical devices. You may have more than one physical devic
one can be deployed to, but there can be only one deployment per device at any given time.
NOTE: **Note:**
-This key can only contain letters, digits, `-`, `_`, `/`, `$`, `{`, `}`, `.`, and spaces, but it cannot start or end with `/`.
-
-### `include`
-
-> - Introduced in [GitLab Premium](https://about.gitlab.com/pricing/) 10.5.
-> - Available for Starter, Premium and Ultimate since 10.6.
-> - [Moved](https://gitlab.com/gitlab-org/gitlab-foss/-/merge_requests/21603) to GitLab Core in 11.4.
-
-Using the `include` keyword, you can allow the inclusion of external YAML files.
-`include` requires the external YAML file to have the extensions `.yml` or `.yaml`,
-otherwise the external file will not be included.
-
-The files defined in `include` are:
-
-- Deep merged with those in `.gitlab-ci.yml`.
-- Always evaluated first and merged with the content of `.gitlab-ci.yml`,
- regardless of the position of the `include` keyword.
-
-TIP: **Tip:**
-Use merging to customize and override included CI/CD configurations with local
-definitions.
-
-NOTE: **Note:**
-Using YAML aliases across different YAML files sourced by `include` is not
-supported. You must only refer to aliases in the same file. Instead
-of using YAML anchors, you can use the [`extends` keyword](#extends).
-
-`include` supports four include methods:
-
-- [`local`](#includelocal)
-- [`file`](#includefile)
-- [`template`](#includetemplate)
-- [`remote`](#includeremote)
-
-See [usage examples](#include-examples).
-
-NOTE: **Note:**
-`.gitlab-ci.yml` configuration included by all methods is evaluated at pipeline creation.
-The configuration is a snapshot in time and persisted in the database. Any changes to
-referenced `.gitlab-ci.yml` configuration will not be reflected in GitLab until the next pipeline is created.
-
-#### `include:local`
-
-`include:local` includes a file from the same repository as `.gitlab-ci.yml`.
-It's referenced using full paths relative to the root directory (`/`).
-
-You can only use files that are currently tracked by Git on the same branch
-your configuration file is on. In other words, when using a `include:local`, make
-sure that both `.gitlab-ci.yml` and the local file are on the same branch.
-
-All [nested includes](#nested-includes) will be executed in the scope of the same project,
-so it is possible to use local, project, remote, or template includes.
-
-NOTE: **Note:**
-Including local files through Git submodules paths is not supported.
-
-Example:
-
-```yaml
-include:
- - local: '/templates/.gitlab-ci-template.yml'
-```
-
-#### `include:file`
-
-> [Introduced](https://gitlab.com/gitlab-org/gitlab-foss/issues/53903) in GitLab 11.7.
-
-To include files from another private project under the same GitLab instance,
-use `include:file`. This file is referenced using full paths relative to the
-root directory (`/`). For example:
-
-```yaml
-include:
- - project: 'my-group/my-project'
- file: '/templates/.gitlab-ci-template.yml'
-```
-
-You can also specify `ref`, with the default being the `HEAD` of the project:
-
-```yaml
-include:
- - project: 'my-group/my-project'
- ref: master
- file: '/templates/.gitlab-ci-template.yml'
-
- - project: 'my-group/my-project'
- ref: v1.0.0
- file: '/templates/.gitlab-ci-template.yml'
-
- - project: 'my-group/my-project'
- ref: 787123b47f14b552955ca2786bc9542ae66fee5b # Git SHA
- file: '/templates/.gitlab-ci-template.yml'
-```
-
-All [nested includes](#nested-includes) will be executed in the scope of the target project,
-so it is possible to use local (relative to target project), project, remote
-or template includes.
-
-#### `include:template`
-
-> [Introduced](https://gitlab.com/gitlab-org/gitlab-foss/issues/53445) in GitLab 11.7.
-
-`include:template` can be used to include `.gitlab-ci.yml` templates that are
-[shipped with GitLab](https://gitlab.com/gitlab-org/gitlab/tree/master/lib/gitlab/ci/templates).
-
-For example:
-
-```yaml
-# File sourced from GitLab's template collection
-include:
- - template: Auto-DevOps.gitlab-ci.yml
-```
-
-Multiple `include:template` files:
-
-```yaml
-include:
- - template: Android-Fastlane.gitlab-ci.yml
- - template: Auto-DevOps.gitlab-ci.yml
-```
-
-All [nested includes](#nested-includes) will be executed only with the permission of the user,
-so it is possible to use project, remote or template includes.
-
-#### `include:remote`
-
-`include:remote` can be used to include a file from a different location,
-using HTTP/HTTPS, referenced by using the full URL. The remote file must be
-publicly accessible through a simple GET request as authentication schemas
-in the remote URL is not supported. For example:
-
-```yaml
-include:
- - remote: 'https://gitlab.com/awesome-project/raw/master/.gitlab-ci-template.yml'
-```
-
-All nested includes will be executed without context as public user, so only another remote,
-or public project, or template is allowed.
-
-#### Nested includes
-
-> [Introduced](https://gitlab.com/gitlab-org/gitlab-foss/issues/56836) in GitLab 11.9.
-
-Nested includes allow you to compose a set of includes.
-A total of 100 includes is allowed.
-Duplicate includes are considered a configuration error.
-
-> [Introduced](https://gitlab.com/gitlab-org/gitlab/issues/28212) in GitLab 12.4.
-
-A hard limit of 30 seconds was set for resolving all files.
-
-#### `include` examples
-
-Here are a few more `include` examples.
-
-##### Single string or array of multiple values
-
-You can include your extra YAML file(s) either as a single string or
-an array of multiple values. The following examples are all valid.
-
-Single string with the `include:local` method implied:
-
-```yaml
-include: '/templates/.after-script-template.yml'
-```
-
-Array with `include` method implied:
-
-```yaml
-include:
- - 'https://gitlab.com/awesome-project/raw/master/.before-script-template.yml'
- - '/templates/.after-script-template.yml'
-```
-
-Single string with `include` method specified explicitly:
-
-```yaml
-include:
- remote: 'https://gitlab.com/awesome-project/raw/master/.before-script-template.yml'
-```
-
-Array with `include:remote` being the single item:
-
-```yaml
-include:
- - remote: 'https://gitlab.com/awesome-project/raw/master/.before-script-template.yml'
-```
-
-Array with multiple `include` methods specified explicitly:
-
-```yaml
-include:
- - remote: 'https://gitlab.com/awesome-project/raw/master/.before-script-template.yml'
- - local: '/templates/.after-script-template.yml'
- - template: Auto-DevOps.gitlab-ci.yml
-```
-
-Array mixed syntax:
-
-```yaml
-include:
- - 'https://gitlab.com/awesome-project/raw/master/.before-script-template.yml'
- - '/templates/.after-script-template.yml'
- - template: Auto-DevOps.gitlab-ci.yml
- - project: 'my-group/my-project'
- ref: master
- file: '/templates/.gitlab-ci-template.yml'
-```
-
-##### Re-using a `before_script` template
-
-In the following example, the content of `.before-script-template.yml` will be
-automatically fetched and evaluated along with the content of `.gitlab-ci.yml`.
-
-Content of `https://gitlab.com/awesome-project/raw/master/.before-script-template.yml`:
-
-```yaml
-before_script:
- - apt-get update -qq && apt-get install -y -qq sqlite3 libsqlite3-dev nodejs
- - gem install bundler --no-document
- - bundle install --jobs $(nproc) "${FLAGS[@]}"
-```
-
-Content of `.gitlab-ci.yml`:
-
-```yaml
-include: 'https://gitlab.com/awesome-project/raw/master/.before-script-template.yml'
-
-rspec:
- script:
- - bundle exec rspec
-```
-
-##### Overriding external template values
-
-The following example shows specific YAML-defined variables and details of the
-`production` job from an include file being customized in `.gitlab-ci.yml`.
-
-Content of `https://company.com/autodevops-template.yml`:
-
-```yaml
-variables:
- POSTGRES_USER: user
- POSTGRES_PASSWORD: testing_password
- POSTGRES_DB: $CI_ENVIRONMENT_SLUG
-
-production:
- stage: production
- script:
- - install_dependencies
- - deploy
- environment:
- name: production
- url: https://$CI_PROJECT_PATH_SLUG.$KUBE_INGRESS_BASE_DOMAIN
- only:
- - master
-```
-
-Content of `.gitlab-ci.yml`:
-
-```yaml
-include: 'https://company.com/autodevops-template.yml'
-
-image: alpine:latest
-
-variables:
- POSTGRES_USER: root
- POSTGRES_PASSWORD: secure_password
-
-stages:
- - build
- - test
- - production
-
-production:
- environment:
- url: https://domain.com
-```
-
-In this case, the variables `POSTGRES_USER` and `POSTGRES_PASSWORD` along
-with the environment url of the `production` job defined in
-`autodevops-template.yml` have been overridden by new values defined in
-`.gitlab-ci.yml`.
-
-The merging lets you extend and override dictionary mappings, but
-you cannot add or modify items to an included array. For example, to add
-an additional item to the production job script, you must repeat the
-existing script items:
-
-Content of `https://company.com/autodevops-template.yml`:
-
-```yaml
-production:
- stage: production
- script:
- - install_dependencies
- - deploy
-```
-
-Content of `.gitlab-ci.yml`:
-
-```yaml
-include: 'https://company.com/autodevops-template.yml'
-
-stages:
- - production
-
-production:
- script:
- - install_dependencies
- - deploy
- - notify_owner
-```
-
-In this case, if `install_dependencies` and `deploy` were not repeated in
-`.gitlab-ci.yml`, they would not be part of the script for the `production`
-job in the combined CI configuration.
-
-##### Using nested includes
-
-The examples below show how includes can be nested from different sources
-using a combination of different methods.
-
-In this example, `.gitlab-ci.yml` includes local the file `/.gitlab-ci/another-config.yml`:
-
-```yaml
-include:
- - local: /.gitlab-ci/another-config.yml
-```
-
-The `/.gitlab-ci/another-config.yml` includes a template and the `/templates/docker-workflow.yml` file
-from another project:
-
-```yaml
-include:
- - template: Bash.gitlab-ci.yml
- - project: group/my-project
- file: /templates/docker-workflow.yml
-```
-
-The `/templates/docker-workflow.yml` present in `group/my-project` includes two local files
-of the `group/my-project`:
-
-```yaml
-include:
- - local: /templates/docker-build.yml
- - local: /templates/docker-testing.yml
-```
-
-Our `/templates/docker-build.yml` present in `group/my-project` adds a `docker-build` job:
-
-```yaml
-docker-build:
- script: docker build -t my-image .
-```
-
-Our second `/templates/docker-test.yml` present in `group/my-project` adds a `docker-test` job:
-
-```yaml
-docker-test:
- script: docker run my-image /run/tests.sh
-```
-
-### `extends`
-
-> Introduced in GitLab 11.3.
-
-`extends` defines entry names that a job that uses `extends` is going to
-inherit from.
-
-It is an alternative to using [YAML anchors](#anchors) and is a little
-more flexible and readable:
-
-```yaml
-.tests:
- script: rake test
- stage: test
- only:
- refs:
- - branches
-
-rspec:
- extends: .tests
- script: rake rspec
- only:
- variables:
- - $RSPEC
-```
-
-In the example above, the `rspec` job inherits from the `.tests` template job.
-GitLab will perform a reverse deep merge based on the keys. GitLab will:
-
-- Merge the `rspec` contents into `.tests` recursively.
-- Not merge the values of the keys.
-
-This results in the following `rspec` job:
-
-```yaml
-rspec:
- script: rake rspec
- stage: test
- only:
- refs:
- - branches
- variables:
- - $RSPEC
-```
-
-NOTE: **Note:**
-Note that `script: rake test` has been overwritten by `script: rake rspec`.
-
-If you do want to include the `rake test`, see [`before_script` and `after_script`](#before_script-and-after_script).
-
-`.tests` in this example is a [hidden key](#hidden-keys-jobs), but it's
-possible to inherit from regular jobs as well.
-
-`extends` supports multi-level inheritance, however it is not recommended to
-use more than three levels. The maximum nesting level that is supported is 10.
-The following example has two levels of inheritance:
-
-```yaml
-.tests:
- only:
- - pushes
-
-.rspec:
- extends: .tests
- script: rake rspec
-
-rspec 1:
- variables:
- RSPEC_SUITE: '1'
- extends: .rspec
-
-rspec 2:
- variables:
- RSPEC_SUITE: '2'
- extends: .rspec
-
-spinach:
- extends: .tests
- script: rake spinach
-```
-
-In GitLab 12.0 and later, it's also possible to use multiple parents for
-`extends`. The algorithm used for merge is "closest scope wins", so
-keys from the last member will always shadow anything defined on other
-levels. For example:
-
-```yaml
-.only-important:
- only:
- - master
- - stable
- tags:
- - production
-
-.in-docker:
- tags:
- - docker
- image: alpine
-
-rspec:
- extends:
- - .only-important
- - .in-docker
- script:
- - rake rspec
-```
-
-This results in the following `rspec` job:
-
-```yaml
-rspec:
- only:
- - master
- - stable
- tags:
- - docker
- image: alpine
- script:
- - rake rspec
-```
-
-### Using `extends` and `include` together
-
-`extends` works across configuration files combined with `include`.
-
-For example, if you have a local `included.yml` file:
-
-```yaml
-.template:
- script:
- - echo Hello!
-```
-
-Then, in `.gitlab-ci.yml` you can use it like this:
-
-```yaml
-include: included.yml
-
-useTemplate:
- image: alpine
- extends: .template
-```
-
-This will run a job called `useTemplate` that runs `echo Hello!` as defined in
-the `.template` job, and uses the `alpine` Docker image as defined in the local job.
+This key can only contain letters, digits, `-`, `_`, `/`, `$`, `{`, `}`, `.`, and spaces.
+It can't start or end with `/`.
### `pages`
@@ -3576,7 +3262,7 @@ requirements below must be met:
- `artifacts` with a path to the `public/` directory must be defined.
The example below simply moves all files from the root of the project to the
-`public/` directory. The `.public` workaround is so `cp` doesn't also copy
+`public/` directory. The `.public` workaround is so `cp` does not also copy
`public/` to itself in an infinite loop:
```yaml
@@ -3595,13 +3281,13 @@ pages:
Read more on [GitLab Pages user documentation](../../user/project/pages/index.md).
-### `variables`
+## `variables`
> Introduced in GitLab Runner v0.5.0.
NOTE: **Note:**
Integers (as well as strings) are legal both for variable's name and value.
-Floats are not legal and cannot be used.
+Floats are not legal and can't be used.
GitLab CI/CD allows you to define variables inside `.gitlab-ci.yml` that are
then passed in the job environment. They can be set globally and per-job.
@@ -3632,7 +3318,7 @@ which can be set in GitLab's UI.
Learn more about [variables and their priority](../variables/README.md).
-#### Git strategy
+### Git strategy
> - Introduced in GitLab 8.9 as an experimental feature.
> - `GIT_STRATEGY=none` requires GitLab Runner v1.7+.
@@ -3655,7 +3341,7 @@ variables:
```
`fetch` is faster as it re-uses the local working copy (falling back to `clone`
-if it doesn't exist). `git clean` is used to undo any changes made by the last
+if it does not exist). `git clean` is used to undo any changes made by the last
job, and `git fetch` is used to retrieve commits made since the last job ran.
```yaml
@@ -3664,9 +3350,9 @@ variables:
```
`none` also re-uses the local working copy, but skips all Git operations
-(including GitLab Runner's pre-clone script, if present). It is mostly useful
-for jobs that operate exclusively on artifacts (e.g., `deploy`). Git repository
-data may be present, but it is certain to be out of date, so you should only
+(including GitLab Runner's pre-clone script, if present). It's mostly useful
+for jobs that operate exclusively on artifacts (for examples `deploy`). Git repository
+data may be present, but it's certain to be out of date, so you should only
rely on files brought into the local working copy from cache or artifacts.
```yaml
@@ -3679,7 +3365,7 @@ NOTE: **Note:** `GIT_STRATEGY` is not supported for
but may be in the future. See the [support Git strategy with Kubernetes executor feature proposal](https://gitlab.com/gitlab-org/gitlab-runner/issues/3847)
for updates.
-#### Git submodule strategy
+### Git submodule strategy
> Requires GitLab Runner v1.10+.
@@ -3689,10 +3375,10 @@ globally or per-job in the [`variables`](#variables) section.
There are three possible values: `none`, `normal`, and `recursive`:
-- `none` means that submodules will not be included when fetching the project
+- `none` means that submodules won't be included when fetching the project
code. This is the default, which matches the pre-v1.10 behavior.
-- `normal` means that only the top-level submodules will be included. It is
+- `normal` means that only the top-level submodules will be included. It's
equivalent to:
```shell
@@ -3703,7 +3389,7 @@ There are three possible values: `none`, `normal`, and `recursive`:
- `recursive` means that all submodules (including submodules of submodules)
will be included. This feature needs Git v1.8.1 and later. When using a
GitLab Runner with an executor not based on Docker, make sure the Git version
- meets that requirement. It is equivalent to:
+ meets that requirement. It's equivalent to:
```shell
git submodule sync --recursive
@@ -3717,7 +3403,7 @@ Note that for this feature to work correctly, the submodules must be configured
- a relative path to another repository on the same GitLab server. See the
[Git submodules](../git_submodules.md) documentation.
-#### Git checkout
+### Git checkout
> Introduced in GitLab Runner 9.3.
@@ -3746,7 +3432,7 @@ script:
- git merge $CI_COMMIT_SHA
```
-#### Git clean flags
+### Git clean flags
> Introduced in GitLab Runner 11.10
@@ -3773,7 +3459,7 @@ script:
- ls -al cache/
```
-#### Job stages attempts
+### Job stages attempts
> Introduced in GitLab, it requires GitLab Runner v1.9+.
@@ -3798,7 +3484,7 @@ variables:
You can set them globally or per-job in the [`variables`](#variables) section.
-#### Shallow cloning
+### Shallow cloning
> Introduced in GitLab 8.9 as an experimental feature.
@@ -3816,7 +3502,7 @@ jobs, jobs may fail.
Since Git fetching and cloning is based on a ref, such as a branch name, Runners
can't clone a specific commit SHA. If there are multiple jobs in the queue, or
-you are retrying an old job, the commit to be tested needs to be within the
+you're retrying an old job, the commit to be tested needs to be within the
Git history that is cloned. Setting too small a value for `GIT_DEPTH` can make
it impossible to run these old commits. You will see `unresolved reference` in
job logs. You should then reconsider changing `GIT_DEPTH` to a higher value.
@@ -3833,46 +3519,9 @@ variables:
You can set it globally or per-job in the [`variables`](#variables) section.
-## Deprecated parameters
-
-The following parameters are deprecated.
-
-### Globally-defined `types`
-
-CAUTION: **Deprecated:**
-`types` is deprecated, and could be removed in a future release.
-Use [`stages`](#stages) instead.
-
-### Job-defined `type`
-
-CAUTION: **Deprecated:**
-`type` is deprecated, and could be removed in one of the future releases.
-Use [`stage`](#stage) instead.
-
-### Globally-defined `image`, `services`, `cache`, `before_script`, `after_script`
-
-Defining `image`, `services`, `cache`, `before_script`, and
-`after_script` globally is deprecated. Support could be removed
-from a future release.
-
-Use [`default:`](#setting-default-parameters) instead. For example:
-
-```yaml
-default:
- image: ruby:2.5
- services:
- - docker:dind
- cache:
- paths: [vendor/]
- before_script:
- - bundle install --path vendor/
- after_script:
- - rm -rf tmp/
-```
-
-## Custom build directories
+### Custom build directories
-> [Introduced](https://gitlab.com/gitlab-org/gitlab-runner/-/merge_requests/1267) in GitLab Runner 11.10
+> [Introduced](https://gitlab.com/gitlab-org/gitlab-runner/-/issues/2211) in GitLab Runner 11.10
NOTE: **Note:**
This can only be used when `custom_build_dir` is enabled in the [Runner's
@@ -3885,7 +3534,7 @@ specific directory (Go projects, for example). In that case, you can specify
the `GIT_CLONE_PATH` variable to tell the Runner in which directory to clone the
repository:
-```yml
+```yaml
variables:
GIT_CLONE_PATH: $CI_BUILDS_DIR/project-name
@@ -3898,12 +3547,12 @@ The `GIT_CLONE_PATH` has to always be within `$CI_BUILDS_DIR`. The directory set
is dependent on executor and configuration of [runners.builds_dir](https://docs.gitlab.com/runner/configuration/advanced-configuration.html#the-runners-section)
setting.
-### Handling concurrency
+#### Handling concurrency
An executor using a concurrency greater than `1` might lead
to failures because multiple jobs might be working on the same directory if the `builds_dir`
is shared between jobs.
-GitLab Runner does not try to prevent this situation. It is up to the administrator
+GitLab Runner does not try to prevent this situation. It's up to the administrator
and developers to comply with the requirements of Runner configuration.
To avoid this scenario, you can use a unique path within `$CI_BUILDS_DIR`, because Runner
@@ -3915,7 +3564,7 @@ exposes two additional variables that provide a unique `ID` of concurrency:
The most stable configuration that should work well in any scenario and on any executor
is to use `$CI_CONCURRENT_ID` in the `GIT_CLONE_PATH`. For example:
-```yml
+```yaml
variables:
GIT_CLONE_PATH: $CI_BUILDS_DIR/$CI_CONCURRENT_ID/project-name
@@ -3927,7 +3576,7 @@ test:
The `$CI_CONCURRENT_PROJECT_ID` should be used in conjunction with `$CI_PROJECT_PATH`
as the `$CI_PROJECT_PATH` provides a path of a repository. That is, `group/subgroup/project`. For example:
-```yml
+```yaml
variables:
GIT_CLONE_PATH: $CI_BUILDS_DIR/$CI_CONCURRENT_ID/$CI_PROJECT_PATH
@@ -3936,15 +3585,15 @@ test:
- pwd
```
-### Nested paths
+#### Nested paths
The value of `GIT_CLONE_PATH` is expanded once and nesting variables
-within it is not supported.
+within is not supported.
For example, you define both the variables below in your
`.gitlab-ci.yml` file:
-```yml
+```yaml
variables:
GOPATH: $CI_BUILDS_DIR/go
GIT_CLONE_PATH: $GOPATH/src/namespace/project
@@ -3962,39 +3611,13 @@ of `.gitlab-ci.yml`.
Read more about the various [YAML features](https://learnxinyminutes.com/docs/yaml/).
-### Hidden keys (jobs)
-
-> Introduced in GitLab 8.6 and GitLab Runner v1.1.1.
-
-If you want to temporarily 'disable' a job, rather than commenting out all the
-lines where the job is defined:
-
-```yaml
-#hidden_job:
-# script:
-# - run test
-```
-
-you can instead start its name with a dot (`.`) and it will not be processed by
-GitLab CI/CD. In the following example, `.hidden_job` will be ignored:
-
-```yaml
-.hidden_job:
- script:
- - run test
-```
-
-Use this feature to ignore jobs, or use the
-[special YAML features](#special-yaml-features) and transform the hidden keys
-into templates.
-
### Anchors
> Introduced in GitLab 8.6 and GitLab Runner v1.1.1.
YAML has a handy feature called 'anchors', which lets you easily duplicate
content across your document. Anchors can be used to duplicate/inherit
-properties, and is a perfect example to be used with [hidden keys](#hidden-keys-jobs)
+properties, and is a perfect example to be used with [hidden jobs](#hide-jobs)
to provide templates for your jobs.
The following example uses anchors and map merging. It will create two jobs,
@@ -4108,7 +3731,7 @@ test:mysql:
- ruby
```
-You can see that the hidden keys are conveniently used as templates.
+You can see that the hidden jobs are conveniently used as templates.
NOTE: **Note:**
You can't use YAML anchors across multiple files when leveraging the [`include`](#include)
@@ -4183,14 +3806,39 @@ job_no_git_strategy:
script: echo $SAMPLE_VARIABLE
```
-## Triggers
+### Hide jobs
-Triggers can be used to force a rebuild of a specific branch, tag or commit,
-with an API call when a pipeline gets created using a trigger token.
+> Introduced in GitLab 8.6 and GitLab Runner v1.1.1.
-Not to be confused with [`trigger`](#trigger).
+If you want to temporarily 'disable' a job, rather than commenting out all the
+lines where the job is defined:
-[Read more in the triggers documentation.](../triggers/README.md)
+```yaml
+#hidden_job:
+# script:
+# - run test
+```
+
+you can instead start its name with a dot (`.`) and it won't be processed by
+GitLab CI/CD. In the following example, `.hidden_job` will be ignored:
+
+```yaml
+.hidden_job:
+ script:
+ - run test
+```
+
+Use this feature to ignore jobs, or use the
+[special YAML features](#special-yaml-features) and transform the hidden jobs
+into templates.
+
+## Skip Pipeline
+
+If your commit message contains `[ci skip]` or `[skip ci]`, using any
+capitalization, the commit will be created but the pipeline will be skipped.
+
+Alternatively, one can pass the `ci.skip` [Git push option](../../user/project/push_options.md#push-options-for-gitlab-cicd)
+if using Git 2.10 or newer.
## Processing Git pushes
@@ -4201,13 +3849,42 @@ This limitation does not affect any of the updated Merge Request pipelines,
all updated Merge Requests will have a pipeline created when using
[pipelines for merge requests](../merge_request_pipelines/index.md).
-## Skipping jobs
+## Deprecated parameters
-If your commit message contains `[ci skip]` or `[skip ci]`, using any
-capitalization, the commit will be created but the pipeline will be skipped.
+The following parameters are deprecated.
-Alternatively, one can pass the `ci.skip` [Git push option](../../user/project/push_options.md#push-options-for-gitlab-cicd)
-if using Git 2.10 or newer.
+### Globally-defined `types`
+
+CAUTION: **Deprecated:**
+`types` is deprecated, and could be removed in a future release.
+Use [`stages`](#stages) instead.
+
+### Job-defined `type`
+
+CAUTION: **Deprecated:**
+`type` is deprecated, and could be removed in one of the future releases.
+Use [`stage`](#stage) instead.
+
+### Globally-defined `image`, `services`, `cache`, `before_script`, `after_script`
+
+Defining `image`, `services`, `cache`, `before_script`, and
+`after_script` globally is deprecated. Support could be removed
+from a future release.
+
+Use [`default:`](#global-defaults) instead. For example:
+
+```yaml
+default:
+ image: ruby:2.5
+ services:
+ - docker:dind
+ cache:
+ paths: [vendor/]
+ before_script:
+ - bundle install --path vendor/
+ after_script:
+ - rm -rf tmp/
+```
<!-- ## Troubleshooting
diff --git a/doc/ci/yaml/includes.md b/doc/ci/yaml/includes.md
new file mode 100644
index 00000000000..a7b626bdd7c
--- /dev/null
+++ b/doc/ci/yaml/includes.md
@@ -0,0 +1,213 @@
+# GitLab CI/CD YAML includes
+
+In addition to the [`includes` examples](README.md#include) listed in the
+[GitLab CI YAML reference](README.md), this page lists more variations of `include`
+usage.
+
+## Single string or array of multiple values
+
+You can include your extra YAML file(s) either as a single string or
+an array of multiple values. The following examples are all valid.
+
+Single string with the `include:local` method implied:
+
+```yaml
+include: '/templates/.after-script-template.yml'
+```
+
+Array with `include` method implied:
+
+```yaml
+include:
+ - 'https://gitlab.com/awesome-project/raw/master/.before-script-template.yml'
+ - '/templates/.after-script-template.yml'
+```
+
+Single string with `include` method specified explicitly:
+
+```yaml
+include:
+ remote: 'https://gitlab.com/awesome-project/raw/master/.before-script-template.yml'
+```
+
+Array with `include:remote` being the single item:
+
+```yaml
+include:
+ - remote: 'https://gitlab.com/awesome-project/raw/master/.before-script-template.yml'
+```
+
+Array with multiple `include` methods specified explicitly:
+
+```yaml
+include:
+ - remote: 'https://gitlab.com/awesome-project/raw/master/.before-script-template.yml'
+ - local: '/templates/.after-script-template.yml'
+ - template: Auto-DevOps.gitlab-ci.yml
+```
+
+Array mixed syntax:
+
+```yaml
+include:
+ - 'https://gitlab.com/awesome-project/raw/master/.before-script-template.yml'
+ - '/templates/.after-script-template.yml'
+ - template: Auto-DevOps.gitlab-ci.yml
+ - project: 'my-group/my-project'
+ ref: master
+ file: '/templates/.gitlab-ci-template.yml'
+```
+
+## Re-using a `before_script` template
+
+In the following example, the content of `.before-script-template.yml` will be
+automatically fetched and evaluated along with the content of `.gitlab-ci.yml`.
+
+Content of `https://gitlab.com/awesome-project/raw/master/.before-script-template.yml`:
+
+```yaml
+before_script:
+ - apt-get update -qq && apt-get install -y -qq sqlite3 libsqlite3-dev nodejs
+ - gem install bundler --no-document
+ - bundle install --jobs $(nproc) "${FLAGS[@]}"
+```
+
+Content of `.gitlab-ci.yml`:
+
+```yaml
+include: 'https://gitlab.com/awesome-project/raw/master/.before-script-template.yml'
+
+rspec:
+ script:
+ - bundle exec rspec
+```
+
+## Overriding external template values
+
+The following example shows specific YAML-defined variables and details of the
+`production` job from an include file being customized in `.gitlab-ci.yml`.
+
+Content of `https://company.com/autodevops-template.yml`:
+
+```yaml
+variables:
+ POSTGRES_USER: user
+ POSTGRES_PASSWORD: testing_password
+ POSTGRES_DB: $CI_ENVIRONMENT_SLUG
+
+production:
+ stage: production
+ script:
+ - install_dependencies
+ - deploy
+ environment:
+ name: production
+ url: https://$CI_PROJECT_PATH_SLUG.$KUBE_INGRESS_BASE_DOMAIN
+ only:
+ - master
+```
+
+Content of `.gitlab-ci.yml`:
+
+```yaml
+include: 'https://company.com/autodevops-template.yml'
+
+image: alpine:latest
+
+variables:
+ POSTGRES_USER: root
+ POSTGRES_PASSWORD: secure_password
+
+stages:
+ - build
+ - test
+ - production
+
+production:
+ environment:
+ url: https://domain.com
+```
+
+In this case, the variables `POSTGRES_USER` and `POSTGRES_PASSWORD` along
+with the environment URL of the `production` job defined in
+`autodevops-template.yml` have been overridden by new values defined in
+`.gitlab-ci.yml`.
+
+The merging lets you extend and override dictionary mappings, but
+you cannot add or modify items to an included array. For example, to add
+an additional item to the production job script, you must repeat the
+existing script items:
+
+Content of `https://company.com/autodevops-template.yml`:
+
+```yaml
+production:
+ stage: production
+ script:
+ - install_dependencies
+ - deploy
+```
+
+Content of `.gitlab-ci.yml`:
+
+```yaml
+include: 'https://company.com/autodevops-template.yml'
+
+stages:
+ - production
+
+production:
+ script:
+ - install_dependencies
+ - deploy
+ - notify_owner
+```
+
+In this case, if `install_dependencies` and `deploy` were not repeated in
+`.gitlab-ci.yml`, they would not be part of the script for the `production`
+job in the combined CI configuration.
+
+## Using nested includes
+
+The examples below show how includes can be nested from different sources
+using a combination of different methods.
+
+In this example, `.gitlab-ci.yml` includes local the file `/.gitlab-ci/another-config.yml`:
+
+```yaml
+include:
+ - local: /.gitlab-ci/another-config.yml
+```
+
+The `/.gitlab-ci/another-config.yml` includes a template and the `/templates/docker-workflow.yml` file
+from another project:
+
+```yaml
+include:
+ - template: Bash.gitlab-ci.yml
+ - project: group/my-project
+ file: /templates/docker-workflow.yml
+```
+
+The `/templates/docker-workflow.yml` present in `group/my-project` includes two local files
+of the `group/my-project`:
+
+```yaml
+include:
+ - local: /templates/docker-build.yml
+ - local: /templates/docker-testing.yml
+```
+
+Our `/templates/docker-build.yml` present in `group/my-project` adds a `docker-build` job:
+
+```yaml
+docker-build:
+ script: docker build -t my-image .
+```
+
+Our second `/templates/docker-test.yml` present in `group/my-project` adds a `docker-test` job:
+
+```yaml
+docker-test:
+ script: docker run my-image /run/tests.sh
+```
diff --git a/doc/development/README.md b/doc/development/README.md
index b041e48f0c2..22cc21e12b9 100644
--- a/doc/development/README.md
+++ b/doc/development/README.md
@@ -9,7 +9,7 @@ description: 'Learn how to contribute to GitLab.'
- Set up GitLab's development environment with [GitLab Development Kit (GDK)](https://gitlab.com/gitlab-org/gitlab-development-kit/blob/master/doc/howto/README.md)
- [GitLab contributing guide](contributing/index.md)
- - [Issues workflow](contributing/issue_workflow.md). For information on:
+ - [Issues workflow](contributing/issue_workflow.md) for more information on:
- Issue tracker guidelines.
- Triaging.
- Labels.
@@ -17,7 +17,7 @@ description: 'Learn how to contribute to GitLab.'
- Issue weight.
- Regression issues.
- Technical or UX debt.
- - [Merge requests workflow](contributing/merge_request_workflow.md). For
+ - [Merge requests workflow](contributing/merge_request_workflow.md) for more
information on:
- Merge request guidelines.
- Contribution acceptance criteria.
@@ -57,10 +57,8 @@ Complementary reads:
- [GitLab utilities](utilities.md)
- [Issuable-like Rails models](issuable-like-models.md)
- [Logging](logging.md)
-- [API styleguide](api_styleguide.md) Use this styleguide if you are
- contributing to the API
-- [GraphQL API styleguide](api_graphql_styleguide.md) Use this
- styleguide if you are contributing to the [GraphQL API](../api/graphql/index.md)
+- [API style guide](api_styleguide.md) for contributing to the API
+- [GraphQL API style guide](api_graphql_styleguide.md) for contributing to the [GraphQL API](../api/graphql/index.md)
- [Sidekiq guidelines](sidekiq_style_guide.md) for working with Sidekiq workers
- [Working with Gitaly](gitaly.md)
- [Manage feature flags](feature_flags/index.md)
@@ -100,6 +98,7 @@ Complementary reads:
- [Rails initializers](rails_initializers.md)
- [Code comments](code_comments.md)
- [Renaming features](renaming_features.md)
+- [Windows Development on GCP](windows.md)
## Performance guides
@@ -117,7 +116,7 @@ Complementary reads:
### Tooling
- [Understanding EXPLAIN plans](understanding_explain_plans.md)
-- [explain.depesz.com](https://explain.depesz.com/) for visualising the output
+- [explain.depesz.com](https://explain.depesz.com/) for visualizing the output
of `EXPLAIN`
- [pgFormatter](http://sqlformat.darold.net/) a PostgreSQL SQL syntax beautifier
@@ -178,7 +177,7 @@ Complementary reads:
## Documentation guides
- [Writing documentation](documentation/index.md)
-- [Documentation styleguide](documentation/styleguide.md)
+- [Documentation style guide](documentation/styleguide.md)
- [Markdown](../user/markdown.md)
## Internationalization (i18n) guides
@@ -189,11 +188,11 @@ Complementary reads:
## Telemetry guides
-- [Introduction](../telemetry/index.md)
-- [Frontend tracking guide](../telemetry/frontend.md)
-- [Backend tracking guide](../telemetry/backend.md)
+- [Telemetry guide](telemetry/index.md)
+- [Usage Ping guide](telemetry/usage_ping.md)
+- [Snowplow guide](telemetry/snowplow.md)
-## Experiment Guide
+## Experiment guide
- [Introduction](experiment_guide/index.md)
@@ -221,9 +220,10 @@ Complementary reads:
- [Defining relations between files using projections](projections.md)
- [Reference processing](./reference_processing.md)
+- [Compatibility with multiple versions of the application running at the same time](multi_version_compatibility.md)
## Other GitLab Development Kit (GDK) guides
- [Run full Auto DevOps cycle in a GDK instance](https://gitlab.com/gitlab-org/gitlab-development-kit/blob/master/doc/howto/auto_devops.md)
-- [Using GitLab Runner with GDK](https://gitlab.com/gitlab-org/gitlab-development-kit/blob/master/doc/howto/runner.md)
-- [Using the Web IDE terminal with GDK](https://gitlab.com/gitlab-org/gitlab-development-kit/-/blob/master/doc/howto/web_ide_terminal_gdk_setup.md)
+- [Using GitLab Runner with the GDK](https://gitlab.com/gitlab-org/gitlab-development-kit/blob/master/doc/howto/runner.md)
+- [Using the Web IDE terminal with the GDK](https://gitlab.com/gitlab-org/gitlab-development-kit/-/blob/master/doc/howto/web_ide_terminal_gdk_setup.md)
diff --git a/doc/development/api_graphql_styleguide.md b/doc/development/api_graphql_styleguide.md
index 036eddd7c37..6d3c0cf0eac 100644
--- a/doc/development/api_graphql_styleguide.md
+++ b/doc/development/api_graphql_styleguide.md
@@ -1,6 +1,6 @@
-# GraphQL API
+# GraphQL API style guide
-This document outlines the styleguide for GitLab's [GraphQL API](../api/graphql/index.md).
+This document outlines the style guide for GitLab's [GraphQL API](../api/graphql/index.md).
## How GitLab implements GraphQL
@@ -12,7 +12,7 @@ which is exposed as an API endpoint at `/api/graphql`.
## Deep Dive
-In March 2019, Nick Thomas hosted a [Deep Dive](https://gitlab.com/gitlab-org/create-stage/issues/1)
+In March 2019, Nick Thomas hosted a Deep Dive (GitLab team members only: `https://gitlab.com/gitlab-org/create-stage/issues/1`)
on GitLab's [GraphQL API](../api/graphql/index.md) to share his domain specific knowledge
with anyone who may work in this part of the code base in the future. You can find the
[recording on YouTube](https://www.youtube.com/watch?v=-9L_1MWrjkg), and the slides on
@@ -102,7 +102,7 @@ be `id` fields.
Further reading:
- [GraphQL Best Practices Guide](https://graphql.org/learn/best-practices/#nullability)
-- [Using nullability in GraphQL](https://blog.apollographql.com/using-nullability-in-graphql-2254f84c4ed7)
+- [Using nullability in GraphQL](https://www.apollographql.com/blog/using-nullability-in-graphql-2254f84c4ed7)
### Exposing Global IDs
@@ -110,7 +110,7 @@ When exposing an `ID` field on a type, we will by default try to
expose a global ID by calling `to_global_id` on the resource being
rendered.
-To override this behaviour, you can implement an `id` method on the
+To override this behavior, you can implement an `id` method on the
type for which you are exposing an ID. Please make sure that when
exposing a `GraphQL::ID_TYPE` using a custom method that it is
globally unique.
@@ -365,7 +365,7 @@ field :token, GraphQL::STRING_TYPE, null: true,
The original `description:` of the field should be maintained, and should
_not_ be updated to mention the deprecation.
-### Deprecation reason styleguide
+### Deprecation reason style guide
Where the reason for deprecation is due to the field being replaced
with another field, the `reason` must be:
@@ -446,7 +446,7 @@ Descriptions of fields and arguments are viewable to users through:
- The [GraphiQL explorer](#graphiql).
- The [static GraphQL API reference](../api/graphql/#reference).
-### Description styleguide
+### Description style guide
To ensure consistency, the following should be followed whenever adding or updating
descriptions:
@@ -598,7 +598,7 @@ ID. Otherwise use a [globally unique ID](#exposing-global-ids).
We already have a `FullPathLoader` that can be included in other
resolvers to quickly find Projects and Namespaces which will have a
-lot of dependant objects.
+lot of dependent objects.
To limit the amount of queries performed, we can use `BatchLoader`.
diff --git a/doc/development/api_styleguide.md b/doc/development/api_styleguide.md
index 78e35023766..e9a41e7ec58 100644
--- a/doc/development/api_styleguide.md
+++ b/doc/development/api_styleguide.md
@@ -1,6 +1,6 @@
-# API styleguide
+# API style guide
-This styleguide recommends best practices for API development.
+This style guide recommends best practices for API development.
## Instance variables
@@ -9,7 +9,7 @@ to access them as we do in Rails views), local variables are fine.
## Entities
-Always use an [Entity] to present the endpoint's payload.
+Always use an [Entity](https://gitlab.com/gitlab-org/gitlab/blob/master/lib/api/entities) to present the endpoint's payload.
## Documentation
@@ -29,7 +29,7 @@ for a good example):
- If the endpoint is deprecated, and if so, when will it be removed
- `params` for the method parameters. This acts as description,
- [validation, and coercion of the parameters]
+ [validation, and coercion of the parameters](https://github.com/ruby-grape/grape#parameter-validation-and-coercion)
A good example is as follows:
@@ -100,13 +100,13 @@ Model.create(foo: params[:foo])
## Using HTTP status helpers
-For non-200 HTTP responses, use the provided helpers in `lib/api/helpers.rb` to ensure correct behaviour (`not_found!`, `no_content!` etc.). These will `throw` inside Grape and abort the execution of your endpoint.
+For non-200 HTTP responses, use the provided helpers in `lib/api/helpers.rb` to ensure correct behavior (`not_found!`, `no_content!` etc.). These will `throw` inside Grape and abort the execution of your endpoint.
For `DELETE` requests, you should also generally use the `destroy_conditionally!` helper which by default returns a `204 No Content` response on success, or a `412 Precondition Failed` response if the given `If-Unmodified-Since` header is out of range. This helper calls `#destroy` on the passed resource, but you can also implement a custom deletion method by passing a block.
## Using API path helpers in GitLab Rails codebase
-Because we support [installing GitLab under a relative URL], one must take this
+Because we support [installing GitLab under a relative URL](../install/relative_url.md), one must take this
into account when using API path helpers generated by Grape. Any such API path
helper usage must be in wrapped into the `expose_path` helper call.
@@ -121,10 +121,6 @@ For instance:
The [internal API](./internal_api.md) is documented for internal use. Please keep it up to date so we know what endpoints
different components are making use of.
-[Entity]: https://gitlab.com/gitlab-org/gitlab/blob/master/lib/api/entities
-[validation, and coercion of the parameters]: https://github.com/ruby-grape/grape#parameter-validation-and-coercion
-[installing GitLab under a relative URL]: https://docs.gitlab.com/ee/install/relative_url.html
-
## Avoiding N+1 problems
In order to avoid N+1 problems that are common when returning collections
@@ -146,7 +142,7 @@ data](https://gitlab.com/gitlab-org/gitlab/blob/19f74903240e209736c7668132e6a5a7
for `Todo` _targets_ when returned in the Todos API.
For more context and discussion about preloading see
-[this merge request](https://gitlab.com/gitlab-org/gitlab-foss/merge_requests/25711)
+[this merge request](https://gitlab.com/gitlab-org/gitlab-foss/-/merge_requests/25711)
which introduced the scope.
### Verifying with tests
diff --git a/doc/development/application_limits.md b/doc/development/application_limits.md
index b6e777dee15..b13e2994c52 100644
--- a/doc/development/application_limits.md
+++ b/doc/development/application_limits.md
@@ -11,7 +11,7 @@ coordinate with others to [document](../administration/instance_limits.md)
and communicate those limits.
There is a guide about [introducing application
-limits](https://about.gitlab.com/handbook/product/#introducing-application-limits).
+limits](https://about.gitlab.com/handbook/product/product-management/process/index.html#introducing-application-limits).
## Development
@@ -90,7 +90,7 @@ project.actual_limits.exceeded?(:project_hooks, 10)
#### `Limitable` concern
-The [`Limitable` concern](https://gitlab.com/gitlab-org/gitlab/blob/master/ee/app/models/concerns/limitable.rb)
+The [`Limitable` concern](https://gitlab.com/gitlab-org/gitlab/blob/master/app/models/concerns/limitable.rb)
can be used to validate that a model does not exceed the limits. It ensures
that the count of the records for the current model does not exceed the defined
limit.
diff --git a/doc/development/architecture.md b/doc/development/architecture.md
index bbd5ca3c494..f52a5d30c1f 100644
--- a/doc/development/architecture.md
+++ b/doc/development/architecture.md
@@ -8,7 +8,7 @@ New versions of GitLab are released in stable branches and the master branch is
For information, see the [GitLab Release Process](https://gitlab.com/gitlab-org/release/docs/-/tree/master#gitlab-release-process).
-Both EE and CE require some add-on components called GitLab Shell and Gitaly. These components are available from the [GitLab Shell](https://gitlab.com/gitlab-org/gitlab-shell/-/tree/master) and [Gitaly](https://gitlab.com/gitlab-org/gitaly/-/tree/master) repositories respectively. New versions are usually tags but staying on the master branch will give you the latest stable version. New releases are generally around the same time as GitLab CE releases with exception for informal security updates deemed critical.
+Both EE and CE require some add-on components called GitLab Shell and Gitaly. These components are available from the [GitLab Shell](https://gitlab.com/gitlab-org/gitlab-shell/-/tree/master) and [Gitaly](https://gitlab.com/gitlab-org/gitaly/-/tree/master) repositories respectively. New versions are usually tags but staying on the master branch will give you the latest stable version. New releases are generally around the same time as GitLab CE releases with the exception of informal security updates deemed critical.
## Components
diff --git a/doc/development/auto_devops.md b/doc/development/auto_devops.md
index dba68974dd5..7d0c020ef96 100644
--- a/doc/development/auto_devops.md
+++ b/doc/development/auto_devops.md
@@ -1,7 +1,11 @@
# Auto DevOps development guide
This document provides a development guide for contributors to
-[Auto DevOps](../topics/autodevops/index.md)
+[Auto DevOps](../topics/autodevops/index.md).
+
+<i class="fa fa-youtube-play youtube" aria-hidden="true"></i>
+An [Auto DevOps technical walk-through](https://youtu.be/G7RTLeToz9E)
+is also available on YouTube.
## Development
diff --git a/doc/development/background_migrations.md b/doc/development/background_migrations.md
index c28008c94b8..0747224db30 100644
--- a/doc/development/background_migrations.md
+++ b/doc/development/background_migrations.md
@@ -157,7 +157,7 @@ roughly be as follows:
enough times to be marked as dead.
1. Remove the old column.
-This may also require a bump to the [import/export version][import-export], if
+This may also require a bump to the [import/export version](../user/project/settings/import_export.md), if
importing a project from a prior version of GitLab requires the data to be in
the new format.
@@ -283,7 +283,7 @@ end
The final step runs for any un-migrated rows after all of the jobs have been
processed. This is in case a Sidekiq process running the background migrations
received SIGKILL, leading to the jobs being lost. (See
-[more reliable Sidekiq queue][reliable-sidekiq] for more information.)
+[more reliable Sidekiq queue](https://gitlab.com/gitlab-org/gitlab-foss/issues/36791) for more information.)
If the application does not depend on the data being 100% migrated (for
instance, the data is advisory, and not mission-critical), then this final step
@@ -312,7 +312,7 @@ to migrate you database down and up, which can result in other background
migrations being called. That means that using `spy` test doubles with
`have_received` is encouraged, instead of using regular test doubles, because
your expectations defined in a `it` block can conflict with what is being
-called in RSpec hooks. See [issue #35351][issue-rspec-hooks]
+called in RSpec hooks. See [issue #35351](https://gitlab.com/gitlab-org/gitlab/issues/18839)
for more details.
## Best practices
@@ -329,8 +329,3 @@ for more details.
1. Make sure to discuss the numbers with a database specialist, the migration may add
more pressure on DB than you expect (measure on staging,
or ask someone to measure on production).
-
-[migrations-readme]: https://gitlab.com/gitlab-org/gitlab/blob/master/spec/migrations/README.md
-[issue-rspec-hooks]: https://gitlab.com/gitlab-org/gitlab/issues/18839
-[reliable-sidekiq]: https://gitlab.com/gitlab-org/gitlab-foss/issues/36791
-[import-export]: ../user/project/settings/import_export.md
diff --git a/doc/development/changelog.md b/doc/development/changelog.md
index 2007c26403c..a0e9f9d3be3 100644
--- a/doc/development/changelog.md
+++ b/doc/development/changelog.md
@@ -101,20 +101,20 @@ automatically.
Its simplest usage is to provide the value for `title`:
-```text
+```plaintext
bin/changelog 'Hey DZ, I added a feature to GitLab!'
```
If you want to generate a changelog entry for GitLab EE, you will need to pass
the `--ee` option:
-```text
+```plaintext
bin/changelog --ee 'Hey DZ, I added a feature to GitLab!'
```
At this point the script would ask you to select the category of the change (mapped to the `type` field in the entry):
-```text
+```plaintext
>> Please specify the category of your change:
1. New feature
2. Bug fix
@@ -132,7 +132,7 @@ the command above on a branch called `feature/hey-dz`, it will generate a
The command will output the path of the generated file and its contents:
-```text
+```plaintext
create changelogs/unreleased/my-feature.yml
---
title: Hey DZ, I added a feature to GitLab!
@@ -162,7 +162,7 @@ If you use **`--amend`** and don't provide a title, it will automatically use
the "subject" of the previous commit, which is the first line of the commit
message:
-```text
+```plaintext
$ git show --oneline
ab88683 Added an awesome new feature to GitLab
@@ -180,7 +180,7 @@ type:
Use **`--force`** or **`-f`** to overwrite an existing changelog entry if it
already exists.
-```text
+```plaintext
$ bin/changelog 'Hey DZ, I added a feature to GitLab!'
error changelogs/unreleased/feature-hey-dz.yml already exists! Use `--force` to overwrite.
@@ -198,7 +198,7 @@ type:
Use the **`--merge-request`** or **`-m`** argument to provide the
`merge_request` value:
-```text
+```plaintext
$ bin/changelog 'Hey DZ, I added a feature to GitLab!' -m 1983
create changelogs/unreleased/feature-hey-dz.yml
---
@@ -213,7 +213,7 @@ type:
Use the **`--dry-run`** or **`-n`** argument to prevent actually writing or
committing anything:
-```text
+```plaintext
$ bin/changelog --amend --dry-run
create changelogs/unreleased/feature-hey-dz.yml
---
@@ -230,7 +230,7 @@ $ ls changelogs/unreleased/
Use the **`--git-username`** or **`-u`** argument to automatically fill in the
`author` value with your configured Git `user.name` value:
-```text
+```plaintext
$ git config user.name
Jane Doe
@@ -247,7 +247,7 @@ type:
Use the **`--type`** or **`-t`** argument to provide the `type` value:
-```text
+```plaintext
$ bin/changelog 'Hey DZ, I added a feature to GitLab!' -t added
create changelogs/unreleased/feature-hey-dz.yml
---
diff --git a/doc/development/cicd/img/ci_architecture.png b/doc/development/cicd/img/ci_architecture.png
new file mode 100644
index 00000000000..b888f2f07aa
--- /dev/null
+++ b/doc/development/cicd/img/ci_architecture.png
Binary files differ
diff --git a/doc/development/cicd/index.md b/doc/development/cicd/index.md
index ed33eb01308..db3f58d1d22 100644
--- a/doc/development/cicd/index.md
+++ b/doc/development/cicd/index.md
@@ -2,6 +2,81 @@
Development guides that are specific to CI/CD are listed here.
+## CI Architecture overview
+
+The following is a simplified diagram of the CI architecture. Some details are left out in order to focus on
+the main components.
+
+![CI software architecture](img/ci_architecture.png)
+<!-- Editable diagram available at https://app.diagrams.net/#G1LFl-KW4fgpBPzz8VIH9rsOlAH4t0xwKj -->
+
+On the left side we have the events that can trigger a pipeline based on various events (trigged by a user or automation):
+
+- A `git push` is the most common event that triggers a pipeline.
+- The [Web API](../../api/pipelines.md#create-a-new-pipeline).
+- A user clicking the "Run Pipeline" button in the UI.
+- When a [merge request is created or updated](../../ci/merge_request_pipelines/index.md#pipelines-for-merge-requests).
+- When an MR is added to a [Merge Train](../../ci/merge_request_pipelines/pipelines_for_merged_results/merge_trains/index.md#merge-trains-premium).
+- A [scheduled pipeline](../../ci/pipelines/schedules.md#pipeline-schedules).
+- When project is [subscribed to an upstream project](../../ci/multi_project_pipelines.md#trigger-a-pipeline-when-an-upstream-project-is-rebuilt).
+- When [Auto DevOps](../../topics/autodevops/index.md) is enabled.
+- When GitHub integration is used with [external pull requests](../../ci/ci_cd_for_external_repos/index.md#pipelines-for-external-pull-requests).
+- When an upstream pipeline contains a [bridge job](../../ci/yaml/README.md#trigger) which triggers a downstream pipeline.
+
+Triggering any of these events will invoke the [`CreatePipelineService`](https://gitlab.com/gitlab-org/gitlab/-/blob/master/app/services/ci/create_pipeline_service.rb)
+which takes as input event data and the user triggering it, then will attempt to create a pipeline.
+
+The `CreatePipelineService` relies heavily on the [`YAML Processor`](https://gitlab.com/gitlab-org/gitlab/-/blob/master/lib/gitlab/ci/yaml_processor.rb)
+component, which is responsible for taking in a YAML blob as input and returns the abstract data structure of a
+pipeline (including stages and all jobs). This component also validates the structure of the YAML while
+processing it, and returns any syntax or semantic errors. The `YAML Processor` component is where we define
+[all the keywords](../../ci/yaml/README.md) available to structure a pipeline.
+
+The `CreatePipelineService` receives the abstract data structure returned by the `YAML Processor`,
+which then converts it to persisted models (pipeline, stages, jobs, etc.). After that, the pipeline is ready
+to be processed. Processing a pipeline means running the jobs in order of execution (stage or DAG)
+until either one of the following:
+
+- All expected jobs have been executed.
+- Failures interrupt the pipeline execution.
+
+The component that processes a pipeline is [`ProcessPipelineService`](https://gitlab.com/gitlab-org/gitlab/-/blob/master/app/services/ci/process_pipeline_service.rb),
+which is responsible for moving all the pipeline's jobs to a completed state. When a pipeline is created, all its
+jobs are initially in `created` state. This services looks at what jobs in `created` stage are eligible
+to be processed based on the pipeline structure. Then it moves them into the `pending` state, which means
+they can now [be picked up by a Runner](#job-scheduling). After a job has been executed it can complete
+successfully or fail. Each status transition for job within a pipeline triggers this service again, which
+looks for the next jobs to be transitioned towards completion. While doing that, `ProcessPipelineService`
+updates the status of jobs, stages and the overall pipeline.
+
+On the right side of the diagram we have a list of [Runners](../../ci/runners/README.md#configuring-gitlab-runners)
+connected to the GitLab instance. These can be Shared Runners, Group Runners or Project-specific Runners.
+The communication between Runners and the Rails server occurs through a set of API endpoints, grouped as
+the `Runner API Gateway`.
+
+We can register, delete and verify Runners, which also causes read/write queries to the database. After a Runner is connected,
+it keeps asking for the next job to execute. This invokes the [`RegisterJobService`](https://gitlab.com/gitlab-org/gitlab/blob/master/app/services/ci/register_job_service.rb)
+which will pick the next job and assign it to the Runner. At this point the job will transition to a
+`running` state, which again triggers `ProcessPipelineService` due to the status change.
+For more details read [Job scheduling](#job-scheduling)).
+
+While a job is being executed, the Runner sends logs back to the server as well any possible artifacts
+that need to be stored. Also, a job may depend on artifacts from previous jobs in order to run. In this
+case the Runner will download them using a dedicated API endpoint.
+
+Artifacts are stored in object storage, while metadata is kept in the database. An important example of artifacts
+is reports (JUnit, SAST, DAST, etc.) which are parsed and rendered in the merge request.
+
+Job status transitions are not all automated. A user may run [manual jobs](../../ci/yaml/README.md#whenmanual), cancel a pipeline, retry
+specific failed jobs or the entire pipeline. Anything that
+causes a job to change status will trigger `ProcessPipelineService`, as it's responsible for
+tracking the status of the entire pipeline.
+
+A special type of job is the [bridge job](../../ci/yaml/README.md#trigger) which is executed server-side
+when transitioning to the `pending` state. This job is responsible for creating a downstream pipeline, such as
+a multi-project or child pipeline. The workflow loop starts again
+from the `CreatePipelineService` every time a downstream pipeline is triggered.
+
## Job scheduling
When a Pipeline is created all its jobs are created at once for all stages, with an initial state of `created`. This makes it possible to visualize the full content of a pipeline.
diff --git a/doc/development/code_review.md b/doc/development/code_review.md
index 5220f29bd60..a5ad7dc0f46 100644
--- a/doc/development/code_review.md
+++ b/doc/development/code_review.md
@@ -64,7 +64,7 @@ It picks reviewers and maintainers from the list at the
page, with these behaviors:
1. It will not pick people whose [GitLab status](../user/profile/index.md#current-status)
- contains the string 'OOO'.
+ contains the string 'OOO', or the emoji is `:palm_tree:` or `:beach:`.
1. [Trainee maintainers](https://about.gitlab.com/handbook/engineering/workflow/code-review/#trainee-maintainer)
are three times as likely to be picked as other reviewers.
1. It always picks the same reviewers and maintainers for the same
@@ -76,26 +76,36 @@ page, with these behaviors:
As described in the section on the responsibility of the maintainer below, you
are recommended to get your merge request approved and merged by maintainer(s)
- with [domain expertise](#domain-experts).
+with [domain expertise](#domain-experts).
-1. If your merge request includes backend changes [^1], it must be
+1. If your merge request includes backend changes (*1*), it must be
**approved by a [backend maintainer](https://about.gitlab.com/handbook/engineering/projects/#gitlab_maintainers_backend)**.
-1. If your merge request includes database migrations or changes to expensive queries [^2], it must be
+1. If your merge request includes database migrations or changes to expensive queries (*2*), it must be
**approved by a [database maintainer](https://about.gitlab.com/handbook/engineering/projects/#gitlab_maintainers_database)**.
Read the [database review guidelines](database_review.md) for more details.
-1. If your merge request includes frontend changes [^1], it must be
+1. If your merge request includes frontend changes (*1*), it must be
**approved by a [frontend maintainer](https://about.gitlab.com/handbook/engineering/projects/#gitlab_maintainers_frontend)**.
-1. If your merge request includes UX changes [^1], it must be
+1. If your merge request includes UX changes (*1*), it must be
**approved by a [UX team member](https://about.gitlab.com/company/team/)**.
-1. If your merge request includes adding a new JavaScript library [^1], it must be
+1. If your merge request includes adding a new JavaScript library (*1*), it must be
**approved by a [frontend lead](https://about.gitlab.com/company/team/)**.
-1. If your merge request includes adding a new UI/UX paradigm [^1], it must be
+1. If your merge request includes adding a new UI/UX paradigm (*1*), it must be
**approved by a [UX lead](https://about.gitlab.com/company/team/)**.
1. If your merge request includes a new dependency or a filesystem change, it must be
**approved by a [Distribution team member](https://about.gitlab.com/company/team/)**. See how to work with the [Distribution team](https://about.gitlab.com/handbook/engineering/development/enablement/distribution/#how-to-work-with-distribution) for more details.
1. If your merge request includes documentation changes, it must be **approved
by a [Technical writer](https://about.gitlab.com/handbook/engineering/ux/technical-writing/#designated-technical-writers)**, based on
the appropriate [product category](https://about.gitlab.com/handbook/product/categories/).
+1. If your merge request includes Quality and non-Quality-related changes (*3*), it must be **approved
+ by a [Software Engineer in Test](https://about.gitlab.com/handbook/engineering/quality/#individual-contributors)**.
+1. If your merge request includes _only_ Quality-related changes (*3*), it must be **approved
+ by a [Quality maintainer](https://about.gitlab.com/handbook/engineering/projects/#gitlab_maintainers_qa)**.
+
+- (*1*): Please note that specs other than JavaScript specs are considered backend code.
+- (*2*): We encourage you to seek guidance from a database maintainer if your merge
+ request is potentially introducing expensive queries. It is most efficient to comment
+ on the line of code in question with the SQL queries so they can give their advice.
+- (*3*): Quality-related changes include all files within the `qa` directory.
#### Security requirements
@@ -314,7 +324,25 @@ Before taking the decision to merge:
- Set the milestone.
- Consider warnings and errors from danger bot, code quality, and other reports.
Unless a strong case can be made for the violation, these should be resolved
- before merge. A comment must to be posted if the MR is merged with any failed job.
+ before merging. A comment must to be posted if the MR is merged with any failed job.
+- If the MR contains both Quality and non-Quality-related changes, the MR should be merged by the relevant maintainer for user-facing changes (backend, frontend, or database) after the Quality related changes are approved by a Software Engineer in Test.
+
+If a merge request is fundamentally ready, but needs only trivial fixes (such as
+typos), consider demonstrating a [bias for
+action](https://about.gitlab.com/handbook/values/#bias-for-action) by making
+those changes directly without going back to the author. You can do this by
+using the [suggest changes](../user/discussions/index.md#suggest-changes) feature to apply
+your own suggestions to the merge request. Note that:
+
+- If the changes are not straightforward, please prefer assigning the merge request back
+ to the author.
+- **Before applying suggestions**, edit the merge request to make sure
+ [squash and
+ merge](../user/project/merge_requests/squash_and_merge.md#squash-and-merge)
+ is enabled, otherwise, the pipeline's Danger job will fail.
+ - If a merge request does not have squash and merge enabled, and it
+ has more than one commit, then see the note below about rewriting
+ commit history.
When ready to merge:
@@ -463,6 +491,21 @@ When a merge request author has been blocked for longer than
the `Review-response` SLO, they are free to remind the reviewer through Slack or assign
another reviewer.
+### Customer critical merge requests
+
+A merge request may benefit from being considered a customer critical priority because there is a significant benefit to the business in doing so.
+
+Properties of customer critical merge requests:
+
+- The [Senior Director of Development](https://about.gitlab.com/job-families/engineering/engineering-management/#senior-director-engineering) ([@clefelhocz1](https://gitlab.com/clefelhocz1)) is the DRI for deciding if a merge request will be customer critical.
+- The DRI will assign the `customer-critical-merge-request` label to the merge request.
+- It is required that the reviewer(s) and maintainer(s) involved with a customer critical merge request are engaged as soon as this decision is made.
+- It is required to prioritize work for those involved on a customer critical merge request so that they have the time available necessary to focus on it.
+- It is required to adhere to GitLab [values](https://about.gitlab.com/handbook/values.md) and processes when working on customer critical merge requests, taking particular note of family and friends first/work second, definition of done, iteration, and release when it's ready.
+- Customer critical merge requests are required to not reduce security, introduce data-loss risk, reduce availability, nor break existing functionality per the process for [prioritizing technical decisions](https://about.gitlab.com/handbook/engineering/#prioritizing-technical-decisions.md).
+- On customer critical requests, it is _recommended_ that those involved _consider_ coordinating synchronously (Zoom, Slack) in addition to asynchronously (merge requests comments) if they believe this will reduce elapsed time to merge even though this _may_ sacrifice [efficiency](https://about.gitlab.com/company/culture/all-remote/asynchronous/#evaluating-efficiency.md).
+- After a customer critical merge request is merged, a retrospective must be completed with the intention of reducing the frequency of future customer critical merge requests.
+
## Examples
How code reviews are conducted can surprise new contributors. Here are some examples of code reviews that should help to orient you as to what to expect.
@@ -495,6 +538,3 @@ Largely based on the [thoughtbot code review guide](https://github.com/thoughtbo
---
[Return to Development documentation](README.md)
-
-[^1]: Please note that specs other than JavaScript specs are considered backend code.
-[^2]: We encourage you to seek guidance from a database maintainer if your merge request is potentially introducing expensive queries. It is most efficient to comment on the line of code in question with the SQL queries so they can give their advice.
diff --git a/doc/development/contributing/index.md b/doc/development/contributing/index.md
index 8270e52e0ab..aebff633f58 100644
--- a/doc/development/contributing/index.md
+++ b/doc/development/contributing/index.md
@@ -3,35 +3,40 @@
Thank you for your interest in contributing to GitLab. This guide details how
to contribute to GitLab in a way that is easy for everyone.
-For a first-time step-by-step guide to the contribution process, please see
-["Contributing to GitLab"](https://about.gitlab.com/community/contribute/).
+For a first-time step-by-step guide to the contribution process, see our
+[Contributing to GitLab](https://about.gitlab.com/community/contribute/) page.
-Looking for something to work on? Look for issues with the label [`Accepting merge requests`](#i-want-to-contribute).
+Looking for something to work on? Look for issues with the label
+[`~Accepting merge requests`](#how-to-contribute).
-GitLab comes in two flavors, GitLab Community Edition (CE) our free and open
-source edition, and GitLab Enterprise Edition (EE) which is our commercial
-edition. Throughout this guide you will see references to CE and EE for
-abbreviation.
+GitLab comes in two flavors:
-To get an overview of GitLab community membership including those that would be reviewing or merging your contributions, please visit [the community roles page](community_roles.md).
+- GitLab Community Edition (CE), our free and open source edition.
+- GitLab Enterprise Edition (EE), which is our commercial edition.
+
+Throughout this guide you will see references to CE and EE for abbreviation.
+
+To get an overview of GitLab community membership, including those that would review or merge
+your contributions, visit [the community roles page](community_roles.md).
If you want to know how the GitLab [core team](https://about.gitlab.com/community/core-team/)
-operates please see [the GitLab contributing process](https://gitlab.com/gitlab-org/gitlab/blob/master/PROCESS.md).
+operates, see [the GitLab contributing process](https://gitlab.com/gitlab-org/gitlab/blob/master/PROCESS.md).
-[GitLab Inc engineers should refer to the engineering workflow document](https://about.gitlab.com/handbook/engineering/workflow/)
+GitLab Inc engineers should refer to the [engineering workflow document](https://about.gitlab.com/handbook/engineering/workflow/).
## Security vulnerability disclosure
-Please report suspected security vulnerabilities in private to
+Report suspected security vulnerabilities in private to
`support@gitlab.com`, also see the
[disclosure section on the GitLab.com website](https://about.gitlab.com/security/disclosure/).
-Please do **NOT** create publicly viewable issues for suspected security
-vulnerabilities.
+
+DANGER: **Danger:**
+Do **NOT** create publicly viewable issues for suspected security vulnerabilities.
## Code of conduct
We want to create a welcoming environment for everyone who is interested in contributing.
-Please visit our [Code of Conduct page](https://about.gitlab.com/community/contribute/code-of-conduct/) to learn more about our commitment to an open and welcoming environment.
+Visit our [Code of Conduct page](https://about.gitlab.com/community/contribute/code-of-conduct/) to learn more about our commitment to an open and welcoming environment.
## Closing policy for issues and merge requests
@@ -40,39 +45,56 @@ and merge requests is limited. Out of respect for our volunteers, issues and
merge requests not in line with the guidelines listed in this document may be
closed without notice.
-Please treat our volunteers with courtesy and respect, it will go a long way
+Treat our volunteers with courtesy and respect, it will go a long way
towards getting your issue resolved.
Issues and merge requests should be in English and contain appropriate language
for audiences of all ages.
-If a contributor is no longer actively working on a submitted merge request
-we can decide that the merge request will be finished by one of our
-[Merge request coaches](https://about.gitlab.com/company/team/) or close the merge request. We make this decision
-based on how important the change is for our product vision. If a merge request
-coach is going to finish the merge request we assign the
-~"coach will finish" label. When a team member picks up a community contribution,
+If a contributor is no longer actively working on a submitted merge request,
+we can:
+
+- Decide that the merge request will be finished by one of our
+ [Merge request coaches](https://about.gitlab.com/company/team/).
+- Close the merge request.
+
+We make this decision based on how important the change is for our product vision. If a merge
+request coach is going to finish the merge request, we assign the
+`~coach will finish` label.
+
+When a team member picks up a community contribution,
we credit the original author by adding a changelog entry crediting the author
and optionally include the original author on at least one of the commits
within the MR.
## Helping others
-Please help other GitLab users when you can.
-The methods people will use to seek help can be found on the [getting help page](https://about.gitlab.com/get-help/).
+Help other GitLab users when you can.
+The methods people use to seek help can be found on the [getting help page](https://about.gitlab.com/get-help/).
-Sign up for the mailing list, answer GitLab questions on StackOverflow or
-respond in the IRC channel.
+Sign up for the mailing list, answer GitLab questions on StackOverflow or respond in the IRC channel.
-## I want to contribute
+## How to contribute
If you want to contribute to GitLab,
-[issues with the `Accepting merge requests` label](issue_workflow.md#label-for-community-contributors)
+[issues with the `~Accepting merge requests` label](issue_workflow.md#label-for-community-contributors)
are a great place to start.
+
If you have any questions or need help visit [Getting Help](https://about.gitlab.com/get-help/) to
-learn how to communicate with GitLab. If you're looking for a Gitter or Slack channel
-please consider we favor
-[asynchronous communication](https://about.gitlab.com/handbook/communication/#internal-communication) over real time communication. Thanks for your contribution!
+learn how to communicate with GitLab. We have a [Gitter channel for contributors](https://gitter.im/gitlab/contributors),
+however we favor
+[asynchronous communication](https://about.gitlab.com/handbook/communication/#internal-communication) over real time communication.
+
+Thanks for your contribution!
+
+### GitLab Development Kit
+
+The GitLab Development Kit (GDK) helps contributors run a local GitLab instance with all the
+required dependencies. It can be used to test changes to GitLab and related projects before raising
+a Merge Request.
+
+For more information, see the [`gitlab-development-kit`](https://gitlab.com/gitlab-org/gitlab-development-kit)
+project.
## Contribution Flow
@@ -92,7 +114,7 @@ When submitting code to GitLab, you may feel that your contribution requires the
When your code contains more than 500 changes, any major breaking changes, or an external library, `@mention` a maintainer in the merge request. If you are not sure who to mention, the reviewer will add one early in the merge request process.
-## Issues workflow
+### Issues workflow
This [documentation](issue_workflow.md) outlines the current issue workflow:
@@ -105,7 +127,7 @@ This [documentation](issue_workflow.md) outlines the current issue workflow:
- [Technical and UX debt](issue_workflow.md#technical-and-ux-debt)
- [Technical debt in follow-up issues](issue_workflow.md#technical-debt-in-follow-up-issues)
-## Merge requests workflow
+### Merge requests workflow
This [documentation](merge_request_workflow.md) outlines the current merge request process.
@@ -120,13 +142,15 @@ This [documentation](style_guides.md) outlines the current style guidelines.
## Implement design & UI elements
-This [design documentation](design.md) outlines the current process for implementing
-design & UI elements.
+This [design documentation](design.md) outlines the current process for implementing design and UI
+elements.
-## Getting an Enterprise Edition License
+## Contribute documentation
-If you need a license for contributing to an EE-feature, please [follow these instructions](https://about.gitlab.com/handbook/marketing/community-relations/code-contributor-program/#for-contributors-to-the-gitlab-enterprise-edition-ee).
+For information on how to contribute documentation, see GitLab
+[documentation guidelines](../documentation/index.md).
----
+## Getting an Enterprise Edition License
-[Return to Development documentation](../README.md)
+If you need a license for contributing to an EE-feature, see
+[relevant information](https://about.gitlab.com/handbook/marketing/community-relations/code-contributor-program/#for-contributors-to-the-gitlab-enterprise-edition-ee).
diff --git a/doc/development/contributing/issue_workflow.md b/doc/development/contributing/issue_workflow.md
index 13ff35ed65c..be416bf636e 100644
--- a/doc/development/contributing/issue_workflow.md
+++ b/doc/development/contributing/issue_workflow.md
@@ -36,7 +36,7 @@ project.
To allow for asynchronous issue handling, we use [milestones](https://gitlab.com/groups/gitlab-org/-/milestones)
and [labels](https://gitlab.com/gitlab-org/gitlab/-/labels). Leads and product managers handle most of the
-scheduling into milestones. Labelling is a task for everyone.
+scheduling into milestones. Labeling is a task for everyone.
Most issues will have labels for at least one of the following:
@@ -156,9 +156,9 @@ As a team needs some way to collect the work their members are planning to be as
Normally there is a 1:1 relationship between Stage labels and Group labels. In
the spirit of "Everyone can contribute", any issue can be picked up by any group,
depending on current priorities. When picking up an issue belonging to a different
-group, it should be relabelled. For example, if an issue labelled `~"devops::create"`
+group, it should be relabeled. For example, if an issue labeled `~"devops::create"`
and `~"group::knowledge"` is picked up by someone in the Access group of the Plan stage,
-the issue should be relabelled as `~"group::access"` while keeping the original
+the issue should be relabeled as `~"group::access"` while keeping the original
`~"devops::create"` unchanged.
We also use stage and group labels to help quantify our [throughput](https://about.gitlab.com/handbook/engineering/management/throughput/).
@@ -264,7 +264,7 @@ release. There are three levels of Release Scoping labels:
milestone. If these issues are not done in the current release, they will
strongly be considered for the next release.
- ~"Next Patch Release": Issues to put in the next patch release. Work on these
- first, and add the "Pick Into X" label to the merge request, along with the
+ first, and add the `~"Pick into X.Y"` label to the merge request, along with the
appropriate milestone.
Each issue scheduled for the current milestone should be labeled ~Deliverable
@@ -347,7 +347,7 @@ there is the ~"stewardship" label.
This label is to be used for issues in which the stewardship of GitLab
is a topic of discussion. For instance if GitLab Inc. is planning to add
-features from GitLab EE to GitLab CE, related issues would be labelled with
+features from GitLab EE to GitLab CE, related issues would be labeled with
~"stewardship".
A recent example of this was the issue for
@@ -461,7 +461,7 @@ fixing in the same MR, but not worth creating a follow-up issue for. Renaming a
method that is used in many places to make its intent slightly clearer may be
worth fixing, but it should not happen in the same MR, and is generally not
worth the overhead of having an issue of its own. These issues would invariably
-be labelled `~P4 ~S4` if we were to create them.
+be labeled `~P4 ~S4` if we were to create them.
More severe technical debt can have implications for development velocity. If
it isn't addressed in a timely manner, the codebase becomes needlessly difficult
diff --git a/doc/development/contributing/merge_request_workflow.md b/doc/development/contributing/merge_request_workflow.md
index 0ac08ec2eae..a70fadfe8a9 100644
--- a/doc/development/contributing/merge_request_workflow.md
+++ b/doc/development/contributing/merge_request_workflow.md
@@ -17,10 +17,10 @@ wireframes of the proposed feature if it will also change the UI.
Merge requests should be submitted to the appropriate project at GitLab.com, for example
[GitLab](https://gitlab.com/gitlab-org/gitlab/-/merge_requests),
[GitLab Runner](https://gitlab.com/gitlab-org/gitlab-runner/-/merge_requests),
-[GitLab Omnibus](https://gitlab.com/gitlab-org/omnibus-gitlab/-/merge_requests), etc.
+[Omnibus GitLab](https://gitlab.com/gitlab-org/omnibus-gitlab/-/merge_requests), etc.
If you are new to GitLab development (or web development in general), see the
-[I want to contribute!](index.md#i-want-to-contribute) section to get started with
+[how to contribute](index.md#how-to-contribute) section to get started with
some potentially easy issues.
To start developing GitLab, download the [GitLab Development Kit](https://gitlab.com/gitlab-org/gitlab-development-kit)
@@ -138,7 +138,7 @@ For more information see [How to Write a Git Commit Message](https://chris.beams
Example commit message template that can be used on your machine that embodies the above (guide for [how to apply template](https://codeinthehole.com/tips/a-useful-template-for-commit-messages/)):
-```text
+```plaintext
# (If applied, this commit will...) <subject> (Max 50 char)
# |<---- Using a Maximum Of 50 Characters ---->|
@@ -244,7 +244,7 @@ request:
1. The [CI environment preparation](https://gitlab.com/gitlab-org/gitlab/blob/master/scripts/prepare_build.sh).
1. The [Omnibus package creator](https://gitlab.com/gitlab-org/omnibus-gitlab).
-### Incremental improvements
+## Incremental improvements
We allow engineering time to fix small problems (with or without an
issue) that are incremental improvements, such as:
diff --git a/doc/development/contributing/style_guides.md b/doc/development/contributing/style_guides.md
index 9a0b654f47f..9e4870eadc4 100644
--- a/doc/development/contributing/style_guides.md
+++ b/doc/development/contributing/style_guides.md
@@ -53,7 +53,7 @@ should enable all RuboCop rules to avoid style-related
discussions/nitpicking/back-and-forth in reviews.
Additionally, we have a dedicated
-[newlines styleguide](../newlines_styleguide.md), as well as dedicated
+[newlines style guide](../newlines_styleguide.md), as well as dedicated
[test-specific style guides and best practices](../testing_guide/index.md).
## Database migrations
diff --git a/doc/development/creating_enums.md b/doc/development/creating_enums.md
index 79ed465b121..e2ebad538d9 100644
--- a/doc/development/creating_enums.md
+++ b/doc/development/creating_enums.md
@@ -13,3 +13,92 @@ def change
add_column :ci_job_artifacts, :file_format, :integer, limit: 2
end
```
+
+## All of the key/value pairs should be defined in FOSS
+
+**Summary:** All enums needs to be defined in FOSS, if a model is also part of the FOSS.
+
+```ruby
+class Model < ApplicationRecord
+ enum platform: {
+ aws: 0,
+ gcp: 1 # EE-only
+ }
+end
+```
+
+When you add a new key/value pair to a `enum` and if it's EE-specific, you might be
+tempted to organize the `enum` as the following:
+
+```ruby
+# Define `failure_reason` enum in `Pipeline` model:
+class Pipeline < ApplicationRecord
+ enum failure_reason: ::PipelineEnums.failure_reasons
+end
+```
+
+```ruby
+# Define key/value pairs that used in FOSS and EE:
+module PipelineEnums
+ def self.failure_reasons
+ { unknown_failure: 0, config_error: 1 }
+ end
+end
+
+PipelineEnums.prepend_if_ee('EE::PipelineEnums')
+```
+
+```ruby
+# Define key/value pairs that used in EE only:
+module EE
+ module PipelineEnums
+ override :failure_reasons
+ def failure_reasons
+ super.merge(activity_limit_exceeded: 2)
+ end
+ end
+end
+```
+
+This works as-is, however, it has a couple of downside that:
+
+- Someone could define a key/value pair in EE that is **conflicted** with a value defined in FOSS.
+ e.g. Define `activity_limit_exceeded: 1` in `EE::PipelineEnums`.
+- When it happens, the feature works totally different.
+ e.g. We cannot figure out `failure_reason` is either `config_error` or `activity_limit_exceeded`.
+- When it happens, we have to ship a database migration to fix the data integrity,
+ which might be impossible if you cannot recover the original value.
+
+Also, you might observe a workaround for this concern by setting an offset in EE's values.
+For example, this example sets `1000` as the offset:
+
+```ruby
+module EE
+ module PipelineEnums
+ override :failure_reasons
+ def failure_reasons
+ super.merge(activity_limit_exceeded: 1_000, size_limit_exceeded: 1_001)
+ end
+ end
+end
+```
+
+This looks working as a workaround, however, this approach has some donwside that:
+
+- Features could move from EE to FOSS or vice versa. Therefore, the offset might be mixed between FOSS and EE in the future.
+ e.g. When you move `activity_limit_exceeded` to FOSS, you'll see `{ unknown_failure: 0, config_error: 1, activity_limit_exceeded: 1_000 }`.
+- The integer column for the `enum` is likely created [as `SMALLINT`](#creating-enums).
+ Therefore, you need to be careful of that the offset doesn't exceed the maximum value of 2 bytes integer.
+
+As a conclusion, you should define all of the key/value pairs in FOSS.
+For example, you can simply write the following code in the above case:
+
+```ruby
+class Pipeline < ApplicationRecord
+ enum failure_reason: {
+ unknown_failure: 0,
+ config_error: 1,
+ activity_limit_exceeded: 2
+ }
+end
+```
diff --git a/doc/development/dangerbot.md b/doc/development/dangerbot.md
index e090281f2bf..eda9e1e21cc 100644
--- a/doc/development/dangerbot.md
+++ b/doc/development/dangerbot.md
@@ -65,7 +65,7 @@ First, be aware of GitLab's [commitment to dogfooding](https://about.gitlab.com/
The code we write for Danger is GitLab-specific, and it **may not** be most
appropriate place to implement functionality that addresses a need we encounter.
Our users, customers, and even our own satellite projects, such as [Gitaly](https://gitlab.com/gitlab-org/gitaly),
-often face similar challenges, after all. Think about how you could fulfil the
+often face similar challenges, after all. Think about how you could fulfill the
same need while ensuring everyone can benefit from the work, and do that instead
if you can.
@@ -145,7 +145,7 @@ at GitLab so far:
fork. That way the danger comments will be made from CI using that
API token instead.
Making the variable
- [masked](../ci/variables/README.md#masked-variables) will make sure
+ [masked](../ci/variables/README.md#mask-a-custom-variable) will make sure
it doesn't show up in the job logs. The variable cannot be
- [protected](../ci/variables/README.md#protected-environment-variables),
+ [protected](../ci/variables/README.md#protect-a-custom-variable),
as it needs to be present for all feature branches.
diff --git a/doc/development/database/add_foreign_key_to_existing_column.md b/doc/development/database/add_foreign_key_to_existing_column.md
index e08f0a3bd1e..b8817eddeec 100644
--- a/doc/development/database/add_foreign_key_to_existing_column.md
+++ b/doc/development/database/add_foreign_key_to_existing_column.md
@@ -64,7 +64,7 @@ class AddNotValidForeignKeyToEmailsUser < ActiveRecord::Migration[5.2]
def up
# safe to use: it requires short lock on the table since we don't validate the foreign key
- add_foreign_key :emails, :users, on_delete: :cascade, validate: false # rubocop:disable Migration/AddConcurrentForeignKey
+ add_foreign_key :emails, :users, on_delete: :cascade, validate: false
end
def down
diff --git a/doc/development/database_debugging.md b/doc/development/database_debugging.md
index 46cb869fea3..3753baa3c63 100644
--- a/doc/development/database_debugging.md
+++ b/doc/development/database_debugging.md
@@ -5,27 +5,33 @@ run into some head-banging database problems.
An easy first step is to search for your error in Slack, or search for `GitLab <my error>` with Google.
----
+Available `RAILS_ENV`:
-Available `RAILS_ENV`
-
-- `production` (generally not for your main GDK db, but you may need this for e.g. Omnibus)
-- `development` (this is your main GDK db)
-- `test` (used for tests like rspec)
+- `production` (generally not for your main GDK database, but you may need this for other installations such as Omnibus).
+- `development` (this is your main GDK db).
+- `test` (used for tests like RSpec).
## Nuke everything and start over
-If you just want to delete everything and start over with an empty DB (~1 minute):
+If you just want to delete everything and start over with an empty DB (approximately 1 minute):
-- `bundle exec rake db:reset RAILS_ENV=development`
+```shell
+bundle exec rake db:reset RAILS_ENV=development
+```
-If you just want to delete everything and start over with dummy data (~4 minutes). This also does `db:reset` and runs DB-specific migrations:
+If you just want to delete everything and start over with dummy data (approximately 4 minutes). This
+also does `db:reset` and runs DB-specific migrations:
-- `bundle exec rake dev:setup RAILS_ENV=development`
+```shell
+bundle exec rake dev:setup RAILS_ENV=development
+```
-If your test DB is giving you problems, it is safe to nuke it because it doesn't contain important data:
+If your test DB is giving you problems, it is safe to nuke it because it doesn't contain important
+data:
-- `bundle exec rake db:reset RAILS_ENV=test`
+```shell
+bundle exec rake db:reset RAILS_ENV=test
+```
## Migration wrangling
diff --git a/doc/development/database_review.md b/doc/development/database_review.md
index f2db0ab4fd5..aa7ebb3756f 100644
--- a/doc/development/database_review.md
+++ b/doc/development/database_review.md
@@ -74,12 +74,12 @@ the following preparations into account.
#### Preparation when adding migrations
-- Ensure `db/structure.sql` is updated.
+- Ensure `db/structure.sql` is updated as [documented](migration_style_guide.md#schema-changes).
- Make migrations reversible by using the `change` method or include a `down` method when using `up`.
- Include either a rollback procedure or describe how to rollback changes.
-- Add the output of both migrating and rolling back for all migrations into the MR description
- - Ensure the down method reverts the changes in `db/structure.sql`
- - Update the migration output whenever you modify the migrations during the review process
+- Add the output of both migrating and rolling back for all migrations into the MR description.
+ - Ensure the down method reverts the changes in `db/structure.sql`.
+ - Update the migration output whenever you modify the migrations during the review process.
- Add tests for the migration in `spec/migrations` if necessary. See [Testing Rails migrations at GitLab](testing_guide/testing_migrations_guide.md) for more details.
- When [high-traffic](https://gitlab.com/gitlab-org/gitlab/-/blob/master/rubocop/migration_helpers.rb#L12) tables are involved in the migration, use the [`with_lock_retries`](migration_style_guide.md#retry-mechanism-when-acquiring-database-locks) helper method. Review the relevant [examples in our documentation](migration_style_guide.md#examples) for use cases and solutions.
- Ensure RuboCop checks are not disabled unless there's a valid reason to.
diff --git a/doc/development/diffs.md b/doc/development/diffs.md
index 3f855d90756..e065e0acc6f 100644
--- a/doc/development/diffs.md
+++ b/doc/development/diffs.md
@@ -8,12 +8,7 @@ Currently we rely on different sources to present diffs, these include:
## Deep Dive
-In January 2019, Oswaldo Ferreira hosted a [Deep Dive] on GitLab's Diffs and Commenting on Diffs functionality to share his domain specific knowledge with anyone who may work in this part of the code base in the future. You can find the [recording on YouTube], and the slides on [Google Slides] and in [PDF]. Everything covered in this deep dive was accurate as of GitLab 11.7, and while specific details may have changed since then, it should still serve as a good introduction.
-
-[Deep Dive]: https://gitlab.com/gitlab-org/create-stage/issues/1
-[recording on YouTube]: https://www.youtube.com/watch?v=K6G3gMcFyek
-[Google Slides]: https://docs.google.com/presentation/d/1bGutFH2AT3bxOPZuLMGl1ANWHqFnrxwQwjiwAZkF-TU/edit
-[PDF]: https://gitlab.com/gitlab-org/create-stage/uploads/b5ad2f336e0afcfe0f99db0af0ccc71a/Create_Deep_Dive__Diffs_and_commenting_on_diffs.pdf
+In January 2019, Oswaldo Ferreira hosted a Deep Dive (GitLab team members only: `https://gitlab.com/gitlab-org/create-stage/issues/1`) on GitLab's Diffs and Commenting on Diffs functionality to share his domain specific knowledge with anyone who may work in this part of the code base in the future. You can find the [recording on YouTube](https://www.youtube.com/watch?v=K6G3gMcFyek), and the slides on [Google Slides](https://docs.google.com/presentation/d/1bGutFH2AT3bxOPZuLMGl1ANWHqFnrxwQwjiwAZkF-TU/edit) and in [PDF](https://gitlab.com/gitlab-org/create-stage/uploads/b5ad2f336e0afcfe0f99db0af0ccc71a/). Everything covered in this deep dive was accurate as of GitLab 11.7, and while specific details may have changed since then, it should still serve as a good introduction.
## Architecture overview
diff --git a/doc/development/documentation/feature_flags.md b/doc/development/documentation/feature_flags.md
new file mode 100644
index 00000000000..373c5a4ee7d
--- /dev/null
+++ b/doc/development/documentation/feature_flags.md
@@ -0,0 +1,188 @@
+---
+type: reference
+description: "GitLab development - how to document features deployed behind feature flags"
+---
+
+# Document features deployed behind feature flags
+
+GitLab uses [Feature Flags](../feature_flags/index.md) to strategically roll
+out the deployment of its own features. The way we document a feature behind a
+feature flag depends on its state (enabled or disabled). When the state
+changes, the developer who made the change **must update the documentation**
+accordingly.
+
+## Criteria
+
+According to the process of [deploying GitLab features behind feature flags](../feature_flags/process.md):
+
+> - _By default, feature flags should be off._
+> - _Feature flags should remain in the codebase for a short period as possible to reduce the need for feature flag accounting._
+> - _In order to build a final release and present the feature for self-managed users, the feature flag should be at least defaulted to on._
+
+See how to document them below, according to the state of the flag:
+
+- [Features disabled by default](#features-disabled-by-default).
+- [Features that became enabled by default](#features-that-became-enabled-by-default).
+- [Features directly enabled by default](#features-directly-enabled-by-default).
+- [Features with the feature flag removed](#features-with-flag-removed).
+
+NOTE: **Note:**
+The [`**(CORE ONLY)**`](styleguide.md#product-badges) badge or equivalent for
+the feature's tier should be added to the line and heading that refers to
+enabling/disabling feature flags as Admin access is required to do so,
+therefore, it indicates that it cannot be done by regular users of GitLab.com.
+
+### Features disabled by default
+
+For features disabled by default, if they cannot be used yet due to lack of
+completeness, or if they're still under internal evaluation (for example, for
+performance implications) do **not document them**: add (or merge) the docs
+only when the feature is safe and ready to use and test by end users.
+
+For feature flags disabled by default, if they can be used by end users:
+
+- Say that it's disabled by default.
+- Say whether it's enabled on GitLab.com.
+- Say whether it's recommended for production use.
+- Document how to enable and disable it.
+
+For example, for a feature disabled by default, disabled on GitLab.com, and
+not ready for production use:
+
+````markdown
+# Feature Name
+
+> - [Introduced](link-to-issue) in GitLab 12.0.
+> - It's deployed behind a feature flag, disabled by default.
+> - It's disabled on GitLab.com.
+> - It's not recommended for production use.
+> - To use it in GitLab self-managed instances, ask a GitLab administrator to [enable it](#anchor-to-section). **(CORE ONLY)**
+
+(...)
+
+### Enable or disable <Feature Name> **(CORE ONLY)**
+
+<Feature Name> is under development and not ready for production use. It is
+deployed behind a feature flag that is **disabled by default**.
+[GitLab administrators with access to the GitLab Rails console](../path/to/administration/feature_flags.md)
+can enable it for your instance.
+
+To enable it:
+
+```ruby
+Feature.enable(:<feature flag>)
+```
+
+To disable it:
+
+```ruby
+Feature.disable(:<feature flag>)
+```
+````
+
+Adjust the blurb according to the state of the feature you're documenting.
+
+### Features that became enabled by default
+
+For features that became enabled by default:
+
+- Say that it became enabled by default.
+- Say whether it's enabled on GitLab.com.
+- Say whether it's recommended for production use.
+- Document how to disable and enable it.
+
+For example, for a feature initially deployed disabled by default, that became enabled by default, that is enabled on GitLab.com, and ready for production use:
+
+````markdown
+# Feature Name
+
+> - [Introduced](link-to-issue) in GitLab 12.0.
+> - It was deployed behind a feature flag, disabled by default.
+> - [Became enabled by default](link-to-issue) on GitLab 12.1.
+> - It's enabled on GitLab.com.
+> - It's recommended for production use.
+> - For GitLab self-managed instances, GitLab administrators can opt to [disable it](#anchor-to-section). **(CORE ONLY)**
+
+(...)
+
+### Enable or disable <Feature Name> **(CORE ONLY)**
+
+<Feature Name> is under development but ready for production use.
+It is deployed behind a feature flag that is **enabled by default**.
+[GitLab administrators with access to the GitLab Rails console](..path/to/administration/feature_flags.md)
+can opt to disable it for your instance.
+
+To disable it:
+
+```ruby
+Feature.disable(:<feature flag>)
+```
+
+To enable it:
+
+```ruby
+Feature.enable(:<feature flag>)
+```
+````
+
+Adjust the blurb according to the state of the feature you're documenting.
+
+### Features directly enabled by default
+
+For features enabled by default:
+
+- Say it's enabled by default.
+- Say whether it's enabled on GitLab.com.
+- Say whether it's recommended for production use.
+- Document how to disable and enable it.
+
+For example, for a feature enabled by default, enabled on GitLab.com, and ready for production use:
+
+````markdown
+# Feature Name
+
+> - [Introduced](link-to-issue) in GitLab 12.0.
+> - It's deployed behind a feature flag, enabled by default.
+> - It's enabled on GitLab.com.
+> - It's recommended for production use.
+> - For GitLab self-managed instances, GitLab administrators can opt to [disable it](#anchor-to-section). **(CORE ONLY)**
+
+(...)
+
+### Enable or disable <Feature Name> **(CORE ONLY)**
+
+<Feature Name> is under development but ready for production use.
+It is deployed behind a feature flag that is **enabled by default**.
+[GitLab administrators with access to the GitLab Rails console](..path/to/administration/feature_flags.md)
+can opt to disable it for your instance.
+
+To disable it:
+
+```ruby
+Feature.disable(:<feature flag>)
+```
+
+To enable it:
+
+```ruby
+Feature.enable(:<feature flag>)
+```
+````
+
+Adjust the blurb according to the state of the feature you're documenting.
+
+### Features with flag removed
+
+Once the feature is ready and the flag has been removed, clean up the
+documentation. Remove the feature flag mention keeping only a note that
+mentions the flag in the version history notes:
+
+````markdown
+# Feature Name
+
+> - [Introduced](link-to-issue) in GitLab 12.0.
+> - [Feature flag removed](link-to-issue) in GitLab 12.2.
+
+(...)
+
+````
diff --git a/doc/development/documentation/index.md b/doc/development/documentation/index.md
index 7a0e187b70a..256d3f5d86c 100644
--- a/doc/development/documentation/index.md
+++ b/doc/development/documentation/index.md
@@ -6,13 +6,14 @@ description: Learn how to contribute to GitLab Documentation.
GitLab's documentation is [intended as the single source of truth (SSOT)](https://about.gitlab.com/handbook/documentation/) for information about how to configure, use, and troubleshoot GitLab. The documentation contains use cases and usage instructions for every GitLab feature, organized by product area and subject. This includes topics and workflows that span multiple GitLab features, and the use of GitLab with other applications.
-In addition to this page, the following resources can help you craft and contribute documentation:
+In addition to this page, the following resources can help you craft and contribute to documentation:
- [Style Guide](styleguide.md) - What belongs in the docs, language guidelines, Markdown standards to follow, links, and more.
- [Structure and template](structure.md) - Learn the typical parts of a doc page and how to write each one.
- [Documentation process](workflow.md).
- [Markdown Guide](../../user/markdown.md) - A reference for all Markdown syntax supported by GitLab.
- [Site architecture](site_architecture/index.md) - How <https://docs.gitlab.com> is built.
+- [Documentation for feature flags](feature_flags.md) - How to write and update documentation for GitLab features deployed behind feature flags.
## Source files and rendered web locations
@@ -51,7 +52,7 @@ docs-only merge requests using the following guide:
[Contributions to GitLab docs](workflow.md) are welcome from the entire GitLab community.
-To ensure that GitLab docs are current, there are special processes and responsibilities for all [feature changes](feature-change-workflow.md)—i.e. development work that impacts the appearance, usage, or administration of a feature.
+To ensure that GitLab docs are current, there are special processes and responsibilities for all [feature changes](feature-change-workflow.md), that is development work that impacts the appearance, usage, or administration of a feature.
However, anyone can contribute [documentation improvements](improvement-workflow.md) that are not associated with a feature change. For example, adding a new doc on how to accomplish a use case that's already possible with GitLab or with third-party tools and GitLab.
@@ -76,13 +77,24 @@ whether the move is necessary), and ensure that a technical writer reviews this
change prior to merging.
If you indeed need to change a document's location, do not remove the old
-document, but instead replace all of its content with a new line:
+document, but instead replace all of its content with the following:
-```md
-This document was moved to [another location](path/to/new_doc.md).
+```markdown
+---
+redirect_to: '../path/to/file/index.md'
+---
+
+This document was moved to [another location](../path/to/file/index.md).
```
-where `path/to/new_doc.md` is the relative path to the root directory `doc/`.
+Where `../path/to/file/index.md` is usually the relative path to the old document.
+
+The `redirect_to` variable supports both full and relative URLs, for example
+`https://docs.gitlab.com/ee/path/to/file.html`, `../path/to/file.html`, `path/to/file.md`.
+It ensures that the redirect will work for <https://docs.gitlab.com> and any `*.md` paths
+will be compiled to `*.html`.
+The new line underneath the front matter informs the user that the document
+changed location and is useful for someone that browses that file from the repository.
For example, if you move `doc/workflow/lfs/index.md` to
`doc/administration/lfs.md`, then the steps would be:
@@ -90,13 +102,17 @@ For example, if you move `doc/workflow/lfs/index.md` to
1. Copy `doc/workflow/lfs/index.md` to `doc/administration/lfs.md`
1. Replace the contents of `doc/workflow/lfs/index.md` with:
- ```md
+ ```markdown
+ ---
+ redirect_to: '../../administration/lfs.md'
+ ---
+
This document was moved to [another location](../../administration/lfs.md).
```
1. Find and replace any occurrences of the old location with the new one.
- A quick way to find them is to use `git grep`. First go to the root directory
- where you cloned the `gitlab` repository and then do:
+ A quick way to find them is to use `git grep` on the repository you changed
+ the file from:
```shell
git grep -n "workflow/lfs/lfs_administration"
@@ -123,24 +139,6 @@ Things to note:
built-in help page, that's why we omit it in `git grep`.
- Use the checklist on the "Change documentation location" MR description template.
-### Alternative redirection method
-
-You can also replace the content
-of the old file with a frontmatter containing a redirect link:
-
-```yaml
----
-redirect_to: '../path/to/file/README.md'
----
-```
-
-It supports both full and relative URLs, e.g. `https://docs.gitlab.com/ee/path/to/file.html`, `../path/to/file.html`, `path/to/file.md`. Note that any `*.md` paths will be compiled to `*.html`.
-
-NOTE: **Note:**
-This redirection method will not provide a redirect fallback on GitLab `/help`. When using
-it, make sure to add a link to the new page on the doc, otherwise it's a dead end for users that
-land on the doc via `/help`.
-
### Redirections for pages with Disqus comments
If the documentation page being relocated already has Disqus comments,
@@ -150,12 +148,12 @@ Disqus uses an identifier per page, and for <https://docs.gitlab.com>, the page
is configured to be the page URL. Therefore, when we change the document location,
we need to preserve the old URL as the same Disqus identifier.
-To do that, add to the frontmatter the variable `disqus_identifier`,
-using the old URL as value. For example, let's say I moved the document
+To do that, add to the front matter the variable `disqus_identifier`,
+using the old URL as value. For example, let's say we moved the document
available under `https://docs.gitlab.com/my-old-location/README.html` to a new location,
`https://docs.gitlab.com/my-new-location/index.html`.
-Into the **new document** frontmatter add the following:
+Into the **new document** front matter, we add the following:
```yaml
---
@@ -173,8 +171,8 @@ Before getting started, make sure you read the introductory section
[documentation workflow](workflow.md).
- Use the current [merge request description template](https://gitlab.com/gitlab-org/gitlab/blob/master/.gitlab/merge_request_templates/Documentation.md)
-- Label the MR `Documentation`
-- Assign the correct milestone (see note below)
+- Label the MR `Documentation` (can only be done by people with `developer` access, for example, GitLab team members)
+- Assign the correct milestone per note below (can only be done by people with `developer` access, for example, GitLab team members)
Documentation will be merged if it is an improvement on existing content,
represents a good-faith effort to follow the template and style standards,
@@ -185,7 +183,7 @@ in a follow-up MR or issue.
NOTE: **Note:**
If the release version you want to add the documentation to has already been
-frozen or released, use the label `Pick into X.Y` to get it merged into
+frozen or released, use the label `~"Pick into X.Y"` to get it merged into
the correct release. Avoid picking into a past release as much as you can, as
it increases the work of the release managers.
@@ -205,7 +203,7 @@ to the MR.
For example, let's say your merge request has a milestone set to 11.3, which
will be released on 2018-09-22. If it gets merged on 2018-09-15, it will be
available online on 2018-09-15, but, as the feature freeze date has passed, if
-the MR does not have a "pick into 11.3" label, the milestone has to be changed
+the MR does not have a `~"Pick into 11.3"` label, the milestone has to be changed
to 11.4 and it will be shipped with all GitLab packages only on 2018-10-22,
with GitLab 11.4. Meaning, it will only be available under `/help` from GitLab
11.4 onward, but available on <https://docs.gitlab.com/> on the same day it was merged.
@@ -277,7 +275,7 @@ You can combine one or more of the following:
### GitLab `/help` tests
-Several [rspec tests](https://gitlab.com/gitlab-org/gitlab/blob/master/spec/features/help_pages_spec.rb)
+Several [RSpec tests](https://gitlab.com/gitlab-org/gitlab/blob/master/spec/features/help_pages_spec.rb)
are run to ensure GitLab documentation renders and works correctly. In particular, that [main docs landing page](../../README.md) will work correctly from `/help`.
For example, [GitLab.com's `/help`](https://gitlab.com/help).
@@ -300,8 +298,8 @@ To preview your changes to documentation locally, follow this
The live preview is currently enabled for the following projects:
-- <https://gitlab.com/gitlab-org/gitlab>
-- <https://gitlab.com/gitlab-org/gitlab-runner>
+- [`gitlab`](https://gitlab.com/gitlab-org/gitlab)
+- [`gitlab-runner`](https://gitlab.com/gitlab-org/gitlab-runner)
If your merge request has docs changes, you can use the manual `review-docs-deploy` job
to deploy the docs review app for your merge request.
@@ -407,8 +405,6 @@ merge request with new or changed docs is submitted, are:
- [`internal_anchors`](https://gitlab.com/gitlab-org/gitlab/blob/master/.gitlab/ci/docs.gitlab-ci.yml#L69)
checks that all internal anchors (ex: `[link](../index.md#internal_anchor)`)
are valid.
-- If any code or the `doc/README.md` file is changed, a full pipeline will run, which
- runs tests for [`/help`](#gitlab-help-tests).
### Running tests
@@ -489,14 +485,14 @@ markdownlint can be used [on the command line](https://github.com/igorshubovych/
either on a single Markdown file or on all Markdown files in a project. For example, to run
markdownlint on all documentation in the [`gitlab` project](https://gitlab.com/gitlab-org/gitlab),
run the following commands from within your `gitlab` project root directory, which will
-automatically detect the [`.markdownlint.json`](#markdownlint-configuration) config
+automatically detect the [`.markdownlint.json`](#markdownlint-configuration) configuration
file in the root of the project, and test all files in `/doc` and its subdirectories:
```shell
markdownlint 'doc/**/*.md'
```
-If you wish to use a different config file, use the `-c` flag:
+If you wish to use a different configuration file, use the `-c` flag:
```shell
markdownlint -c <config-file-name> 'doc/**/*.md'
@@ -510,7 +506,7 @@ such as:
- [Atom](https://atom.io/packages/linter-node-markdownlint)
It is best to use the [same configuration file](#markdownlint-configuration) as what
-is in use in the four repos that are the sources for <https://docs.gitlab.com>. Each
+is in use in the four repositories that are the sources for <https://docs.gitlab.com>. Each
plugin/extension has different requirements regarding the configuration file, which
is explained in each editor's docs.
@@ -519,12 +515,12 @@ is explained in each editor's docs.
Each formatting issue that markdownlint checks has an associated
[rule](https://github.com/DavidAnson/markdownlint/blob/master/doc/Rules.md#rules).
These rules are configured in the `.markdownlint.json` files located in the root of
-four repos that are the sources for <https://docs.gitlab.com>:
+four repositories that are the sources for <https://docs.gitlab.com>:
-- <https://gitlab.com/gitlab-org/gitlab/blob/master/.markdownlint.json>
-- <https://gitlab.com/gitlab-org/gitlab-runner/blob/master/.markdownlint.json>
-- <https://gitlab.com/gitlab-org/omnibus-gitlab/blob/master/.markdownlint.json>
-- <https://gitlab.com/charts/gitlab/blob/master/.markdownlint.json>
+- [`gitlab`](https://gitlab.com/gitlab-org/gitlab/blob/master/.markdownlint.json)
+- [`gitlab-runner`](https://gitlab.com/gitlab-org/gitlab-runner/blob/master/.markdownlint.json)
+- [`omnibus-gitlab`](https://gitlab.com/gitlab-org/omnibus-gitlab/blob/master/.markdownlint.json)
+- [`charts`](https://gitlab.com/charts/gitlab/blob/master/.markdownlint.json)
By default all rules are enabled, so the configuration file is used to disable unwanted
rules, and also to configure optional parameters for enabled rules as needed. You can
@@ -534,7 +530,7 @@ on or off when markdownlint was enabled on the docs.
#### Vale
-[Vale](https://errata-ai.github.io/vale/) is a grammar, style, and word usage linter
+[Vale](https://errata-ai.gitbook.io/vale/) is a grammar, style, and word usage linter
for the English language. Vale's configuration is stored in the
[`.vale.ini`](https://gitlab.com/gitlab-org/gitlab/blob/master/.vale.ini) file
located in the root directory of the [GitLab repository](https://gitlab.com/gitlab-org/gitlab).
@@ -554,10 +550,28 @@ You can also
[configure the text editor of your choice](https://errata-ai.github.io/vale/#local-use-by-a-single-writer)
to display the results.
+Vale's test results [are displayed](#testing) in CI pipelines.
+
+##### Disable a Vale test
+
+You can disable a specific Vale linting rule or all Vale linting rules for any portion of a document:
+
+- To disable a specific rule, add a `<!-- vale gitlab.rulename = NO -->` tag
+ before the text, and a `<!-- vale gitlab.rulename = YES -->` tag after the text,
+ replacing `rulename` with the filename of a test in the [GitLab styles](https://gitlab.com/gitlab-org/gitlab/-/tree/master/doc/.linting/vale/styles/gitlab) directory.
+- To disable all Vale linting rules, add a `<!-- vale off -->` tag before the text,
+ and a `<!-- vale on -->` tag after the text.
+
+Whenever possible, exclude only the problematic rule and line(s).
+In some cases, such as list items, you may need to disable linting for the entire
+list until ["Ignore comments are not honored in a Markdown file"](https://github.com/errata-ai/vale/issues/175) is resolved.
+
+For more information, see [Vale's documentation](https://errata-ai.gitbook.io/vale/getting-started/markup#markup-based-configuration).
+
## Danger Bot
GitLab uses [Danger](https://github.com/danger/danger) for some elements in
code review. For docs changes in merge requests, whenever a change to files under `/doc`
is made, Danger Bot leaves a comment with further instructions about the documentation
-process. This is configured in the Dangerfile in the GitLab repo under
+process. This is configured in the `Dangerfile` in the GitLab repository under
[/danger/documentation/](https://gitlab.com/gitlab-org/gitlab/tree/master/danger/documentation).
diff --git a/doc/development/documentation/site_architecture/release_process.md b/doc/development/documentation/site_architecture/release_process.md
index a8da565124b..13d6540fa35 100644
--- a/doc/development/documentation/site_architecture/release_process.md
+++ b/doc/development/documentation/site_architecture/release_process.md
@@ -20,7 +20,7 @@ Although build images are built automatically via GitLab CI/CD, you can build
and tag all tooling images locally:
1. Make sure you have [Docker installed](https://docs.docker.com/install/).
-1. Make sure you're on the `dockerfiles/` directory of the `gitlab-docs` repo.
+1. Make sure you're in the `dockerfiles/` directory of the `gitlab-docs` repository.
1. Build the images:
```shell
@@ -46,7 +46,7 @@ products, we need to add a
1. Check that there is a [stable branch created](https://gitlab.com/gitlab-org/charts/gitlab/-/branches)
for the new chart version. If you're unsure or can't find it, drop a line in
the `#g_delivery` channel.
-1. Make sure you're on the root path of the `gitlab-docs` repo.
+1. Make sure you're in the root path of the `gitlab-docs` repository.
1. Open `content/_data/chart_versions.yaml` and add the new stable branch version using the
version mapping. Note that only the `major.minor` version is needed.
1. Create a new merge request and merge it.
@@ -61,14 +61,14 @@ this first step.
The single docs version must be created before the release merge request, but
this needs to happen when the stable branches for all products have been created.
-1. Make sure you're on the root path of the `gitlab-docs` repo.
+1. Make sure you're in the root path of the `gitlab-docs` repository.
1. Make sure your `master` is updated:
```shell
git pull origin master
```
-1. Run the raketask to create the single version:
+1. Run the Rake task to create the single version:
```shell
./bin/rake "release:single[12.0]"
@@ -109,7 +109,7 @@ Visit `http://localhost:4000/12.0/` to see if everything works correctly.
Now it's time to create the monthly release merge request that adds the new
version and rotates the old one:
-1. Make sure you're on the root path of the `gitlab-docs` repo.
+1. Make sure you're in the root path of the `gitlab-docs` repository.
1. Create a branch `release-X-Y`:
```shell
@@ -158,10 +158,10 @@ the dropdown are included in the unmerged `release-X-Y` branch.
The content of `content/_data/versions.yaml` needs to change for all online
versions:
-1. Run the raketask that will create all the respective merge requests needed to
+1. Run the Rake task that will create all the respective merge requests needed to
update the dropdowns and will be set to automatically be merged when their
pipelines succeed. The `release-X-Y` branch needs to be present locally,
- otherwise the raketask will fail:
+ otherwise the Rake task will fail:
```shell
./bin/rake release:dropdowns
diff --git a/doc/development/documentation/structure.md b/doc/development/documentation/structure.md
index 84ba47eba78..d19383bee27 100644
--- a/doc/development/documentation/structure.md
+++ b/doc/development/documentation/structure.md
@@ -20,7 +20,7 @@ Every feature or use case document should include the following content in the f
with exceptions and details noted below and in the template included on this page.
- **Title**: Top-level heading with the feature name, or a use case name, which would start with
- a verb, like Configuring, Enabling, etc.
+ a verb, like Configuring, Enabling, and so on.
- **Introduction**: A couple sentences about the subject matter and what's to be found
on this page. Describe what the feature or topic is, what it does, and in what context it should
be used. There is no need to add a title called "Introduction" or "Overview," because people rarely
@@ -41,7 +41,7 @@ and other logical divisions such as pre- and post-deployment steps.
To start a new document, respect the file tree and file name guidelines,
as well as the style guidelines. Use the following template:
-```md
+```markdown
<!--Follow the Style Guide when working on this document. https://docs.gitlab.com/ee/development/documentation/styleguide.html
When done, remove all of this commented-out text, except a commented-out Troubleshooting section,
which, if empty, can be left in place to encourage future use.-->
@@ -130,7 +130,7 @@ Notes:
## Help and feedback section
The "help and feedback" section (introduced by [!319](https://gitlab.com/gitlab-org/gitlab-docs/-/merge_requests/319)) displayed at the end of each document
-can be omitted from the doc by adding a key into the its frontmatter:
+can be omitted from the doc by adding a key into the its front matter:
```yaml
---
@@ -148,7 +148,7 @@ We also have integrated the docs site with Disqus (introduced by
allowing our users to post comments.
To omit only the comments from the feedback section, use the following
-key on the frontmatter:
+key on the front matter:
```yaml
---
@@ -159,7 +159,7 @@ comments: false
We are only hiding comments in main index pages, such as [the main documentation index](../../README.md), since its content is too broad to comment on. Before omitting Disqus,
you must check with a technical writer.
-Note that once `feedback: false` is added to the frontmatter, it will automatically omit
+Note that once `feedback: false` is added to the front matter, it will automatically omit
Disqus, therefore, don't add both keys to the same document.
The click events in the feedback section are tracked with Google Tag Manager. The
diff --git a/doc/development/documentation/styleguide.md b/doc/development/documentation/styleguide.md
index 0007f6d6e2f..6d146051e13 100644
--- a/doc/development/documentation/styleguide.md
+++ b/doc/development/documentation/styleguide.md
@@ -17,9 +17,9 @@ that apply to all GitLab content, not just documentation.
### Why a single source of truth
-The documentation of GitLab products and features is the SSOT for all information related to implementation, usage, and troubleshooting. It evolves continually, in keeping with new products and features, and with improvements for clarity, accuracy, and completeness.
+The documentation of GitLab products and features is the SSOT for all information related to implementation, usage, and troubleshooting. It evolves continuously, in keeping with new products and features, and with improvements for clarity, accuracy, and completeness.
-This policy prevents information silos, ensuring that it remains easy to find information about GitLab products.
+This policy prevents information silos, making it easier to find information about GitLab products.
It also informs decisions about the kinds of content we include in our documentation.
@@ -46,12 +46,12 @@ In the software industry, it is a best practice to organize documentation in dif
1. Explanation
1. Reference (for example, a glossary)
-At GitLab, we have so many product changes in our monthly releases that we can't afford to continually update multiple types of information.
+At GitLab, we have so many product changes in our monthly releases that we can't afford to continuously update multiple types of information.
If we have multiple types, the information will become outdated. Therefore, we have a [single template](structure.md) for documentation.
We currently do not distinguish specific document types, although we are open to reconsidering this policy
once the documentation has reached a future stage of maturity and quality. If you are reading this, then despite our
-continual improvement efforts, that point hasn't been reached.
+continuous improvement efforts, that point hasn't been reached.
### Link instead of summarize
@@ -61,7 +61,7 @@ Instead, link to the SSOT and explain why it is important to consume the informa
### Organize by topic, not by type
-Beyond top-level audience-type folders (for example, `administration`), we organize content by topic, not by type, so that it can be located as easily as possible within the single-source-of-truth (SSOT) section for the subject matter.
+Beyond top-level audience-type folders (for example, `administration`), we organize content by topic, not by type, so it can be located as easily as possible within the single-source-of-truth (SSOT) section for the subject matter.
For example, do not create groupings of similar media types. For example:
@@ -76,7 +76,7 @@ and cross-link between any related content.
### Docs-first methodology
-We employ a **docs-first methodology** to help ensure that the docs remain a complete and trusted resource, and to make communicating about the use of GitLab more efficient.
+We employ a **docs-first methodology** to help ensure the docs remain a complete and trusted resource, and to make communicating about the use of GitLab more efficient.
- If the answer to a question exists in documentation, share the link to the docs instead of rephrasing the information.
- When you encounter new information not available in GitLab’s documentation (for example, when working on a support case or testing a feature), your first step should be to create a merge request (MR) to add this information to the docs. You can then share the MR in order to communicate this information.
@@ -129,13 +129,13 @@ correctly, but is not the current standard for GitLab documentation).
A rule that could cause confusion is `MD044/proper-names`, as it might not be immediately
clear what caused markdownlint to fail, or how to correct the failure. This rule
checks a list of known words, listed in the `.markdownlint.json` file in each project,
-to verify that proper capitalization and backticks are used. Words in backticks will
+to verify proper use of capitalization and backticks. Words in backticks will
be ignored by markdownlint.
In general, product names should follow the exact capitalization of the official names
of the products, protocols, and so on.
-Some examples that will fail if incorrect capitalization is used:
+Some examples fail if incorrect capitalization is used:
- MinIO (needs capital `IO`)
- NGINX (needs all capitals)
@@ -248,10 +248,6 @@ GitLab documentation should be clear and easy to understand.
- Be clear, concise, and stick to the goal of the documentation.
- Write in US English with US grammar.
- Use inclusive language.
-- Avoid jargon.
-- Avoid uncommon words.
-- Don't write in the first person singular.
- - Instead of "I" or "me," use "we," "you," "us," or "one."
### Point of view
@@ -285,21 +281,53 @@ because it’s friendly and easy to understand.
Some features are also objects. For example, "GitLab's Merge Requests support X" and
"Create a new merge request for Z."
+### Language to avoid
+
+When creating documentation, limit or avoid the use of the following verb
+tenses, words, and phrases:
+
+- Avoid jargon.
+- Avoid uncommon words.
+- Don't write in the first person singular.
+ - Instead of "I" or "me," use "we," "you," "us," or "one."
+ - When possible, stay user focused by writing in the second person ("you" or
+ the imperative).
+- Don't overuse "that". In many cases, you can remove "that" from a sentence
+ and improve readability.
- Avoid use of the future tense:
- - Instead of "after you execute this command, GitLab will display the result", use "after you execute this command, GitLab displays the result".
- - Only use the future tense to convey when the action or result will actually occur at a future time.
-- Do not use slashes to clump different words together or as a replacement for the word "or":
- - Instead of "and/or," consider using "or," or use another sensible construction.
- - Other examples include "clone/fetch," author/assignee," and "namespace/repository name." Break apart any such instances in an appropriate way.
- - Exceptions to this rule include commonly accepted technical terms such as CI/CD, TCP/IP, and so on.
-- Do not use "may" and "might" interchangeably:
- - Use "might" to indicate the probability of something occurring. "If you skip this step, the import process might fail."
- - Use "may" to indicate giving permission for someone to do something, or consider using "can" instead. "You may select either option on this screen." Or, "you can select either option on this screen."
-- We discourage use of Latin abbreviations, such as "e.g.," "i.e.," or "etc.,"
-as even native users of English might misunderstand them.
+ - Instead of "after you execute this command, GitLab will display the
+ result", use "after you execute this command, GitLab displays the result".
+ - Only use the future tense to convey when the action or result will actually
+ occur at a future time.
+- Don't use slashes to clump different words together or as a replacement for
+ the word "or":
+ - Instead of "and/or," consider using "or," or use another sensible
+ construction.
+ - Other examples include "clone/fetch," author/assignee," and
+ "namespace/repository name." Break apart any such instances in an
+ appropriate way.
+ - Exceptions to this rule include commonly accepted technical terms, such as
+ CI/CD and TCP/IP.
+- <!-- vale gitlab.LatinTerms = NO -->
+ We discourage use of Latin abbreviations, such as "e.g.," "i.e.," or "etc.,"
+ as even native users of English might misunderstand them.
- Instead of "i.e.," use "that is."
- Instead of "e.g.," use "for example," "such as," "for instance," or "like."
- - Instead of "etc.," either use "and so on" or consider editing it out, since it can be vague.
+ - Instead of "etc.," either use "and so on" or consider editing it out, since
+ it can be vague.
+ <!-- vale gitlab.rulename = NO -->
+- Avoid using the word *currently* when talking about the product or its
+ features. The documentation describes the product as it is, and not as it
+ will be at some indeterminate point in the future.
+
+### Word usage clarifications
+
+- Don't use "may" and "might" interchangeably:
+ - Use "might" to indicate the probability of something occurring. "If you
+ skip this step, the import process might fail."
+ - Use "may" to indicate giving permission for someone to do something, or
+ consider using "can" instead. "You may select either option on this
+ screen." Or, "You can select either option on this screen."
### Contractions
@@ -353,6 +381,8 @@ as even native users of English might misunderstand them.
| Requests to localhost are not allowed | Requests to localhost aren't allowed |
| Specified URL cannot be used | Specified URL can't be used |
+<!-- vale on -->
+
## Text
- [Write in Markdown](#markdown).
@@ -360,7 +390,7 @@ as even native users of English might misunderstand them.
- Insert an empty line for new paragraphs.
- Insert an empty line between different markups (for example, after every paragraph, header, list, and so on). Example:
- ```md
+ ```markdown
## Header
Paragraph.
@@ -417,7 +447,7 @@ Only use ordered lists when their items describe a sequence of steps to follow.
Do:
-```md
+```markdown
These are the steps to do something:
1. First, do the first step.
@@ -427,7 +457,7 @@ These are the steps to do something:
Don't:
-```md
+```markdown
This is a list of available features:
1. Feature 1
@@ -453,7 +483,7 @@ This is a list of available features:
all with a period.
- Separate list items from explanatory text with a colon (`:`). For example:
- ```md
+ ```markdown
The list is as follows:
- First item: this explains the first item.
@@ -511,7 +541,7 @@ In unordered lists (using `-`), this means two spaces for each level of indentat
- Unordered list item 3
- ```text
+ ```plaintext
a codeblock that will next inside list item 3
```
@@ -534,7 +564,7 @@ For ordered lists, use three spaces for each level of indentation:
1. Ordered list item 3
- ```text
+ ```plaintext
a codeblock that will next inside list item 3
```
@@ -560,9 +590,47 @@ to mix types, that is also possible, as long as you don't mix items at the same
- Unordered list item three.
```
+## Tables
+
+Tables should be used to describe complex information in a straightforward
+manner. Note that in many cases, an unordered list is sufficient to describe a
+list of items with a single, simple description per item. But, if you have data
+that is best described by a matrix, tables are the best choice for use.
+
+### Creation guidelines
+
+Due to accessibility and scanability requirements, tables should not have any
+empty cells. If there is no otherwise meaningful value for a cell, consider entering
+*N/A* (for 'not applicable') or *none*.
+
+To help tables be easier to maintain, consider adding additional spaces to the
+column widths to make them consistent. For example:
+
+```markdown
+| App name | Description | Requirements |
+|:---------|:---------------------|:---------------|
+| App 1 | Description text 1. | Requirements 1 |
+| App 2 | Description text 2. | None |
+```
+
+Consider installing a plugin or extension in your editor for formatting tables:
+
+- [Markdown Table Prettifier](https://marketplace.visualstudio.com/items?itemName=darkriszty.markdown-table-prettify) for Visual Studio Code
+- [Markdown Table Formatter](https://packagecontrol.io/packages/Markdown%20Table%20Formatter) for Sublime Text
+- [Markdown Table Formatter](https://atom.io/packages/markdown-table-formatter) for Atom
+
+### Feature tables
+
+When creating tables of lists of features (such as whether or not features are
+available to certain roles on the [Permissions](../../user/permissions.md#project-members-permissions)
+page), use the following phrases (based on the SVG icons):
+
+- *No*: **{dotted-circle}** No
+- *Yes*: **{check-circle}** Yes
+
## Quotes
-Valid for Markdown content only, not for frontmatter entries:
+Valid for Markdown content only, not for front matter entries:
- Standard quotes: double quotes (`"`). Example: "This is wrapped in double quotes".
- Quote within a quote: double quotes (`"`) wrap single quotes (`'`). Example: "I am 'quoting' something within a quote".
@@ -724,15 +792,17 @@ Instead:
Example:
-```md
+```markdown
For more information, see the [confidential issue](../../user/project/issues/confidential_issues.md) `https://gitlab.com/gitlab-org/gitlab-foss/issues/<issue_number>`.
```
### Link to specific lines of code
-When linking to specifics lines within a file, link to a commit instead of to the branch.
+When linking to specific lines within a file, link to a commit instead of to the branch.
Lines of code change through time, therefore, linking to a line by using the commit link
-ensures the user lands on the line you're referring to.
+ensures the user lands on the line you're referring to. The **Permalink** button, which is
+available when viewing a file within a project, makes it easy to generate a link to the
+most recent commit of the given file.
- **Do:** `[link to line 3](https://gitlab.com/gitlab-org/gitlab/-/blob/11f17c56d8b7f0b752562d78a4298a3a95b5ce66/.gitlab/issue_templates/Feature%20proposal.md#L3)`
- **Don't:** `[link to line 3](https://gitlab.com/gitlab-org/gitlab/-/blob/master/.gitlab/issue_templates/Feature%20proposal.md#L3).`
@@ -801,7 +871,7 @@ known tool is [`pngquant`](https://pngquant.org/), which is cross-platform and
open source. Install it by visiting the official website and following the
instructions for your OS.
-GitLab has a [raketask](https://gitlab.com/gitlab-org/gitlab/-/blob/master/lib/tasks/pngquant.rake)
+GitLab has a [Rake task](https://gitlab.com/gitlab-org/gitlab/-/blob/master/lib/tasks/pngquant.rake)
that you can use to automate the process. In the root directory of your local
copy of `https://gitlab.com/gitlab-org/gitlab`, run in a terminal:
@@ -838,7 +908,7 @@ Do not upload videos to the product repositories. [Link](#link-to-video) or [emb
To link out to a video, include a YouTube icon so that readers can
quickly and easily scan the page for videos before reading:
-```md
+```markdown
<i class="fa fa-youtube-play youtube" aria-hidden="true"></i>
For an overview, see [Video Title](link-to-video).
```
@@ -965,7 +1035,7 @@ of language classes available.
| `ruby` | Alias: `rb`. |
| `shell` | Aliases: `bash` or `sh`. |
| `sql` | |
-| `toml` | Runner configuration examples, and other toml formatted configuration files. |
+| `toml` | Runner configuration examples, and other TOML-formatted configuration files. |
| `typescript` | Alias: `ts`. |
| `xml` | |
| `yaml` | Alias: `yml`. |
@@ -1012,7 +1082,7 @@ This will ensure that the source Markdown remains readable and should help with
The following are examples of source Markdown for menu items with their published output:
-```md
+```markdown
1. Go to **{home}** **Project overview > Details**
1. Go to **{doc-text}** **Repository > Branches**
1. Go to **{issues}** **Issues > List**
@@ -1073,7 +1143,7 @@ of users.
Weigh the costs of distracting users to whom the content is not relevant against
the cost of users missing the content if it were not expressed as a note.
-```md
+```markdown
NOTE: **Note:**
This is something to note.
```
@@ -1085,7 +1155,7 @@ This is something to note.
### Tip
-```md
+```markdown
TIP: **Tip:**
This is a tip.
```
@@ -1097,7 +1167,7 @@ This is a tip.
### Caution
-```md
+```markdown
CAUTION: **Caution:**
This is something to be cautious about.
```
@@ -1109,7 +1179,7 @@ This is something to be cautious about.
### Danger
-```md
+```markdown
DANGER: **Danger:**
This is a breaking change, a bug, or something very important to note.
```
@@ -1123,7 +1193,7 @@ This is a breaking change, a bug, or something very important to note.
For highlighting a text within a blue blockquote, use this format:
-```md
+```markdown
> This is a blockquote.
```
@@ -1135,7 +1205,7 @@ If the text spans across multiple lines it's OK to split the line.
For multiple paragraphs, use the symbol `>` before every line:
-```md
+```markdown
> This is the first paragraph.
>
> This is the second paragraph.
@@ -1216,7 +1286,7 @@ a helpful link back to how the feature was developed.
to the following should be added immediately below the heading as a blockquote:
- `> Introduced in GitLab 11.3.`.
-- Whenever possible, version text should have a link to the issue, merge request, or epic that introduced the feature.
+- Whenever possible, version text should have a link to the _completed_ issue, merge request, or epic that introduced the feature.
An issue is preferred over a merge request, and a merge request is preferred over an epic. For example:
- `> [Introduced](<link-to-issue>) in GitLab 11.3.`.
@@ -1228,21 +1298,35 @@ a helpful link back to how the feature was developed.
- If listing information for multiple version as a feature evolves, add the information to a
block-quoted bullet list. For example:
- ```md
+ ```markdown
> - [Introduced](<link-to-issue>) in GitLab 11.3.
> - Enabled by default in GitLab 11.4.
```
- If a feature is moved to another tier:
- ```md
+ ```markdown
> - [Introduced](<link-to-issue>) in [GitLab Premium](https://about.gitlab.com/pricing/) 11.5.
> - [Moved](<link-to-issue>) to [GitLab Starter](https://about.gitlab.com/pricing/) in 11.8.
> - [Moved](<link-to-issue>) to GitLab Core in 12.0.
```
NOTE: **Note:**
-Version text must be on its own line and surounded by blank lines to render correctly.
+Version text must be on its own line and surrounded by blank lines to render correctly.
+
+### Versions in the past or future
+
+When describing functionality available in past or future versions, use:
+
+- **Earlier**, and not **older** or **before**.
+- **Later**, and not **newer** or **after**.
+
+For example:
+
+- Available in GitLab 12.3 and earlier.
+- Available in GitLab 12.4 and later.
+- If using GitLab 11.4 or earlier, ...
+- If using GitLab 10.6 or later, ...
### Importance of referencing GitLab versions and tiers
@@ -1333,7 +1417,7 @@ avoid duplication, link to the special document that can be found in
[`doc/administration/restart_gitlab.md`](../../administration/restart_gitlab.md).
Usually the text will read like:
-```md
+```markdown
Save the file and [reconfigure GitLab](../../administration/restart_gitlab.md)
for the changes to take effect.
```
@@ -1369,7 +1453,7 @@ When there is a list of steps to perform, usually that entails editing the
configuration file and reconfiguring/restarting GitLab. In such case, follow
the style below as a guide:
-````md
+````markdown
**For Omnibus installations**
1. Edit `/etc/gitlab/gitlab.rb`:
@@ -1416,38 +1500,8 @@ can facilitate this by making sure the troubleshooting content addresses:
## Feature flags
-Sometimes features are shipped with feature flags, either:
-
-- On by default, but providing the option to turn the feature off.
-- Off by default, but providing the option to turn the feature on.
-
-When documenting feature flags for a feature, include:
-
-- Why a feature flag is necessary. Some of the reasons are
- [outlined in the handbook](https://about.gitlab.com/handbook/product/#alpha-beta-ga).
-- That administrative access is required to make a feature flag change.
-- What to ask for when requesting a change to a feature flag's state.
-
-NOTE: **Note:**
-The [Product Manager for the relevant group](https://about.gitlab.com/handbook/product/categories/#devops-stages)
-must review and approve the addition or removal of any mentions of using feature flags before the doc change is merged.
-
-The following is sample text for adding feature flag documentation for a feature that is
-off by default:
-
-````md
-### Enabling the feature
-
-This feature comes with the `:feature_flag` feature flag disabled by default. In some cases,
-this feature is incompatible with an old configuration. To turn on the feature,
-ask a GitLab administrator with Rails console access to run the following command:
-
-```ruby
-Feature.enable(:feature_flag)
-```
-````
-
-For guidance on developing with feature flags, see
+Learn how to [document features deployed behind flags](feature_flags.md).
+For guidance on developing GitLab with feature flags, see
[Feature flags in development of GitLab](../feature_flags/index.md).
## API
@@ -1475,7 +1529,7 @@ The following can be used as a template to get started:
One or two sentence description of what endpoint does.
-```text
+```plaintext
METHOD /endpoint
```
@@ -1537,7 +1591,7 @@ You can use the following fake tokens as examples.
Use the following table headers to describe the methods. Attributes should
always be in code blocks using backticks (`` ` ``).
-```md
+```markdown
| Attribute | Type | Required | Description |
|:----------|:-----|:---------|:------------|
```
@@ -1614,7 +1668,7 @@ curl --request POST --header "PRIVATE-TOKEN: <your_access_token>" --form "title=
```
The above example is run by and administrator and will add an SSH public key
-titled `ssh-key` to user's account which has an id of 25.
+titled `ssh-key` to user's account which has an ID of 25.
#### Escape special characters
diff --git a/doc/development/documentation/workflow.md b/doc/development/documentation/workflow.md
index 01f528dcfa4..ab6200155bf 100644
--- a/doc/development/documentation/workflow.md
+++ b/doc/development/documentation/workflow.md
@@ -1,19 +1,20 @@
# Documentation process
-The process for creating and maintaining GitLab product documentation depends on whether the
-documentation is associated with:
+The process for creating and maintaining GitLab product documentation allows
+anyone to contribute a merge request or create an issue for GitLab's
+documentation.
-- [A new feature or feature enhancement](#for-a-product-change).
-
- Delivered for a specific milestone and associated with specific code changes. This documentation
- has the highest priority.
+NOTE: **Note:**
+Documentation updates relating to new features or feature enhancements must
+use the [feature workflow process](https://about.gitlab.com/handbook/engineering/ux/technical-writing/workflow/#for-a-product-change) described in the GitLab Handbook.
-- [Changes outside a specific milestone](#for-all-other-documentation).
+## Who updates the docs?
- It is usually not associated with a specific code change and has a lower priority.
+*Anyone* can contribute! You can create a merge request for documentation when:
-Documentation is not usually required when a "backstage feature" is added or changed, and does not
-directly affect the way that any user or administrator interacts with GitLab.
+- You find errors or other room for improvement in existing documentation.
+- You have an idea for all-new documentation that would help a GitLab user or administrator to
+ accomplish their work with GitLab.
## Documentation labels
@@ -32,372 +33,17 @@ The following are also added by members of the Technical Writing team:
`docs::` prefix. For example, `~docs::improvement`.
- The `~Technical Writing` [team label](../contributing/issue_workflow.md#team-labels).
-## For a product change
-
-This documentation is required for any new or changed feature and is:
-
-- Created or updated as part of feature development, almost always in the same merge request as the
- feature code. Including documentation in the same merge request as the code eliminates the
- possibility that code and documentation get out of sync.
-- Required with the delivery of a feature for a specific milestone as part of GitLab's
- [definition of done](../contributing/merge_request_workflow.md#definition-of-done).
-- Often linked from the release post.
-
-### Roles and responsibilities
-
-Documentation for specific milestones involves the:
-
-- Developer of a feature or enhancement.
-- Product Manager for the group delivering the new feature or feature enhancement.
-- Technical Writer assigned to the group.
-
-Each role is described below.
-
-#### Developers
-
-Developers are the primary author of documentation for a feature or feature enhancement. They are
-responsible for:
-
-- Developing initial content required for a feature.
-- Liaising with their Product Manager to understand what documentation must be delivered, and when.
-- Requesting technical reviews from other developers within their group.
-- Requesting documentation reviews from the Technical Writer
- [assigned to the DevOps stage group](https://about.gitlab.com/handbook/engineering/ux/technical-writing/#assignments)
- that is delivering the new feature or feature enhancements.
-
-TIP: **Tip:**
-Community Contributors can ask for additional help from GitLab team members.
-
-##### Authoring
-
-Because the documentation is an essential part of the product, if a ~feature issue also contains the
-~documentation label, you must ship the new or updated documentation with the code of the feature.
-
-Technical Writers are happy to help, as requested and planned on an issue-by-issue basis.
-
-For feature issues requiring documentation, follow the process below unless otherwise agreed with
-the Product Manager and Technical Writer for a given issue:
-
-- Include any new and edited documentation, either in:
- - The merge request introducing the code.
- - A separate merge request raised around the same time.
-- Use the [documentation requirements](#documentation-requirements) developed by the Product Manager
- in the issue and discuss any further documentation plans or ideas as needed.
-
- If the new or changed documentation requires extensive collaboration or conversation, a
- separate, linked issue can be used for the planning process.
-
-- Use the [Documentation guidelines](index.md), as well as other resources linked from there,
- including:
- - Documentation [Structure and template](structure.md) page.
- - [Style Guide](styleguide.md).
- - [Markdown Guide](https://about.gitlab.com/handbook/engineering/ux/technical-writing/markdown-guide/).
-- Contact the Technical Writer for the relevant [DevOps stage](https://about.gitlab.com/handbook/engineering/ux/technical-writing/#assignments)
- in your issue or merge request, or within `#docs` on GitLab Slack, if you:
- - Need any help to choose the correct place for documentation.
- - Want to discuss a documentation idea or outline.
- - Want to request any other help.
-- If you are working on documentation in a separate merge request, ensure the documentation is
- merged as close as possible to the code merge.
-- A policy for documenting [feature-flagged](../feature_flags/index.md) issues is forthcoming and you
- are welcome to join the [discussion](https://gitlab.com/gitlab-org/gitlab/issues/26347).
-
-##### Reviews and merging
-
-Reviewers help ensure:
-
-- Accuracy.
-- Clarity.
-- Completeness.
-- Adherence to:
- - [Documentation requirements](#documentation-requirements) in the issue.
- - [Documentation guidelines](index.md).
- - [Style guide](styleguide.md).
-
-Prior to merging, documentation changes committed by the developer must be reviewed by:
-
-- The code reviewer for the merge request. This is known as a technical review.
-- Optionally, others involved in the work such as other developers or the Product Manager.
-- The Technical Writer for the DevOps stage group, except in exceptional circumstances where a
- [post-merge review](#post-merge-reviews) can be requested.
-- A maintainer of the project.
-
-#### Product Managers
-
-Product Managers are responsible for the [documentation requirements](#documentation-requirements)
-for a feature or feature enhancement. They can also:
-
-- Liaise with the Technical Writer for discussion and collaboration.
-- Review documentation themselves.
-
-For issues requiring any new or updated documentation, the Product Manager must:
-
-- Add the ~documentation label.
-- Confirm or add the [documentation requirements](#documentation-requirements).
-- Ensure the issue contains:
- - Any new or updated feature name.
- - Overview, description, and use cases when applicable (as required by the
- [documentation structure and template](structure.md)).
-
-Everyone is encouraged to draft the documentation requirements in the issue. However, a Product
-Manager will:
-
-- When the issue is assigned a release milestone, review and update the Documentation details.
-- By the kickoff, finalize the documentation details.
-
-#### Technical Writers
-
-Technical Writers are responsible for:
-
-- Participating in issues discussions and reviewing MRs for the upcoming milestone.
-- Reviewing documentation requirements in issues when called upon.
-- Answering questions, and helping and providing advice throughout the authoring and editing
- process.
-- Reviewing all significant new and updated documentation content, whether before merge or after it
- is merged.
-- Assisting the developer and Product Manager with feature documentation delivery.
-
-##### Planning
-
-The Technical Writer:
-
-- Reviews their group's `~feature` issues that are part of the next milestone to get a sense of the
- scope of content likely to be authored.
-- Recommends the `~documentation` label on issues from that list which don't have it but should, or
- inquires with the PM to determine if documentation is truly required.
-- For `~direction` issues from that list, reads the full issue and reviews its Documentation
- requirements section. Addresses any recommendations or questions with the PMs and others
- collaborating on the issue in order to refine or expand the Documentation requirements.
-
-##### Collaboration
-
-By default, the developer will work on documentation changes independently, but
-the developer, Product Manager, or Technical Writer can propose a broader collaboration for
-any given issue.
-
-Additionally, Technical Writers are available for questions at any time.
-
-##### Review
-
-Technical Writers:
-
-- Provide non-blocking reviews of all documentation changes, before or after the change is merged.
-- Confirm that the documentation is:
- - Clear.
- - Grammatically correct.
- - Discoverable.
- - Navigable.
-- Ensures that the documentation avoids:
- - Redundancy.
- - Bad file locations.
- - Typos.
- - Broken links.
-
-The Technical Writer will review the documentation to check that the developer and
-code reviewer have ensured:
-
-- Clarity.
-- Appropriate location, making sure the documentation is in the correct directories (often
- reflecting how the product is structured) and has the correct name.
-- Syntax, typos, and broken links.
-- Improvements to the content.
-- Accordance with the:
- - [Documentation Style Guide](styleguide.md).
- - [Structure and Template](structure.md) doc.
-
-### When documentation is required
-
-Documentation [is required](../contributing/merge_request_workflow.md#definition-of-done) for a
-milestone when:
-
-- A new or enhanced feature is shipped that impacts the user or administrator experience.
-- There are changes to the UI or API.
-- A process, workflow, or previously documented feature is changed.
-- A feature is deprecated or removed.
-
-NOTE: **Note:**
-Documentation refactoring unrelated to a feature change is covered in the
-[other process](#for-all-other-documentation), so that time-sensitive documentation updates are
-prioritized.
-
-### Documentation requirements
-
-Requirements for the documentation of a feature should be included as part of the
-issue for planning that feature in a **Documentation** section within the issue description. Issues
-created using the [**Feature Proposal** template](https://gitlab.com/gitlab-org/gitlab/raw/master/.gitlab/issue_templates/Feature%20proposal.md)
-have this section by default.
-
-Anyone can add these details, but the Product Manager who assigns the issue to a specific release
-milestone will ensure these details are present and finalized by the time of that milestone's kickoff.
-
-Developers, Technical Writers, and others may help further refine this plan at any time on request.
-
-The following details should be included:
-
-- What concepts and procedures should the documentation guide and enable the user to understand or
- accomplish?
-- To this end, what new page(s) are needed, if any? What pages or subsections need updates?
- Consider changes and additions to user, admin, and API documentation.
-- For any guide or instruction set, should it help address a single use case, or be flexible to
- address a certain range of use cases?
-- Do we need to update a previously recommended workflow? Should we link the new feature from
- various relevant locations? Consider all ways documentation should be affected.
-- Are there any key terms or task descriptions that should be included so that the documentation is
- found in relevant searches?
-- Include suggested titles of any pages or subsection headings, if applicable.
-- List any documentation that should be cross-linked, if applicable.
-
-### Including docs with code
-
-Currently, the Technical Writing team strongly encourages including documentation in
-the same merge request as the code that it relates to, but this is not strictly mandatory.
-It's still common for documentation to be added in an MR separate from the feature MR.
-
-Engineering teams may elect to adopt a workflow where it is **mandatory** that docs
-are included in the code MR, as part of their [definition of done](../contributing/merge_request_workflow.md#definition-of-done).
-When a team adopts this workflow, that team's engineers must include their docs in the **same**
-MR as their feature code, at all times.
-
-#### Downsides of separate docs MRs
-
-A workflow that has documentation separated into its own MR has many downsides.
-
-If the documentation merges **before** the feature:
-
-- GitLab.com users might try to use the feature before it's released, driving support tickets.
-- If the feature is delayed, the documentation might not be pulled/reverted in time and could be
- accidentally included in the self-managed package for that release.
-
-If the documentation merges **after** the feature:
-
-- The feature might be included in the self-managed package, but without any documentation
- if the docs MR misses the cutoff.
-- A feature might show up in the GitLab.com UI before any documentation exists for it.
- Users surprised by this feature will search for documentation and won't find it, possibly driving
- support tickets.
-
-Having two separate MRs means:
-
-- Two different people might be responsible for merging one feature, which is not workable
- with an asynchronous work style. The feature might merge while the technical writer is asleep,
- creating a potentially lengthy delay between the two merges.
-- If the docs MR is assigned to the same maintainer as responsible for the feature
- code MR, they will have to review and juggle two MRs instead of dealing with just one.
-
-Documentation quality might be lower, because:
-
-- Having docs in a separate MR will mean far fewer people will see and verify them,
- increasing the likelihood that issues will be missed.
-- In a "split" workflow, engineers might only create the documentation MR once the
- feature MR is ready, or almost ready. This gives the technical writer little time
- to learn about the feature in order to do a good review. It also increases pressure
- on them to review and merge faster than desired, letting problems slip in due to haste.
-
-#### Benefits of always including docs with code
-
-Including docs with code (and doing it early in the development process) has many benefits:
-
-- There are no timing issues connected to releases:
- - If a feature slips to the next release, the documentation slips too.
- - If the feature *just* makes it into a release, the docs *just* make it in too.
- - If a feature makes it to GitLab.com early, the documentation will be ready for
- our early adopters.
-- Only a single person will be responsible for merging the feature (the code maintainer).
-- The technical writer will have more time to gain an understanding of the feature
- and will be better able to verify the content of the docs in the Review App or GDK.
- They will also be able to offer advice for improving the UI text or offer additional use cases.
-- The documentation will have increased visibility:
- - Everyone involved in the merge request will see the docs. This could include product
- managers, multiple engineers with deep domain knowledge, as well as the code reviewers
- and maintainer. They will be more likely to catch issues with examples, as well
- as background or concepts that the technical writer may not be aware of.
- - Increasing visibility of the documentation also has the side effect of improving
- *other* engineers' documentation. By reviewing each other's MRs, each engineer's
- own documentation skills will improve.
-- Thinking about the documentation early can help engineers generate better examples,
- as they will need to think about what examples a user will want, and will need to
- make sure the code they write implements that example properly.
-
-#### Docs with code as a workflow
-
-In order to have docs included with code as a mandatory workflow, some changes might
-need to happen to a team's current workflow:
-
-- The engineers must strive to include the docs early in the development process,
- to give ample time for review, not just from the technical writer, but also the
- code reviewer and maintainer.
-- Reviewers and maintainers must also review the docs during code reviews, to make
- sure the described processes match the expected use of the feature, and that examples
- are correct. They do *not* need to worry about style, grammar, and so on.
-- The technical writer must be assigned the MR directly and not only pinged. Thanks
- to the ability to have [multiple assignees for any MR](../../user/project/merge_requests/getting_started.md#multiple-assignees-starter),
- this can be done at any time, but must be before the code maintainer review. It's
- common to have both the docs and code reviews happening at the same time, with the
- author, reviewer and technical writer discussing the docs together.
-- When the docs are ready, the technical writer will click **Approve** and usually
- will no longer be involved in the MR. If the feature changes during code review and
- the docs are updated, the technical writer must be reassigned the MR to verify the
- update.
-- Maintainers are allowed to merge features with the docs "as-is", even if the technical
- writer has not given final approval yet. The **docs reviews must not be blockers**. Therefore
- it's important to get the docs included and assigned to the technical writers early.
- If the feature is merged before final docs approval, the maintainer must create
- a [post-merge follow-up issue](#post-merge-reviews), and assign it to both the engineer
- and technical writer.
-
-Maintainers are allowed to merge features with the docs "as-is" even if the
-technical writer has not given final approval yet but the merge request has
-all other required approvals.
-
-You can visualize the parallel workflow for code and docs reviews as:
-
-```mermaid
-graph TD
- A("Feature MR Created (Engineer)") --> |Assign| B("Code Review (reviewer)")
- B --> |"Approve / Reassign"| C("Code Review (maintainer)")
- C --> |Approve| F("Merge (maintainer)")
- A --> D("Docs Added (Engineer)")
- D --> |Assign| E("Docs Review (Tech Writer)")
- E --> |Approve| F
-```
-
-For complex features split over multiple merge requests:
-
-- If a merge request is implementing components for a future feature, but the components
- are not accessible to users yet, then no documentation should be included.
-- If a merge request will expose a feature to users in any way, such as an enabled
- UI element, an API endpoint, or anything similar, then that MR **must** have docs.
- Note that this may mean multiple docs additions could happen in the buildup to the
- implementation of a single large feature, for example API docs and feature usage docs.
-- If it's unclear which engineer should add the feature documentation into their
- MR, the engineering manager should decide during planning, and tie the documentation
- to the last MR that must be merged before a feature is considered released.
- This is often, but not always, a frontend MR.
-
-## For all other documentation
-
Documentation changes that are not associated with the release of a new or updated feature
do not take the `~feature` label, but still need the `~documentation` label.
They may include:
- Documentation created or updated to improve accuracy, completeness, ease of use, or any reason
- other than a [feature change](#for-a-product-change).
+ other than a [feature change](https://about.gitlab.com/handbook/engineering/ux/technical-writing/workflow/#for-a-product-change).
- Addressing gaps in existing documentation, or making improvements to existing documentation.
- Work on special projects related to the documentation.
-TIP: **Tip:**
-Anyone can contribute a merge request or create an issue for GitLab's documentation.
-
-### Who updates the docs
-
-Anyone can contribute! You can create a merge request for documentation when:
-
-- You find errors or other room for improvement in existing documentation.
-- You have an idea for all-new documentation that would help a GitLab user or administrator to
- accomplish their work with GitLab.
-
-### How to update the docs
+## How to update the docs
To update GitLab documentation:
@@ -464,7 +110,7 @@ The process involves the following:
The process is reflected in the **Documentation**
[merge request template](https://gitlab.com/gitlab-org/gitlab/blob/master/.gitlab/merge_request_templates/Documentation.md).
-### Other ways to help
+## Other ways to help
If you have ideas for further documentation resources please
[create an issue](https://gitlab.com/gitlab-org/gitlab/issues/new?issuable_template=Documentation)
diff --git a/doc/development/ee_features.md b/doc/development/ee_features.md
index bd70d5b87ba..3951b0516e8 100644
--- a/doc/development/ee_features.md
+++ b/doc/development/ee_features.md
@@ -3,7 +3,8 @@
- **Write the code and the tests.**: As with any code, EE features should have
good test coverage to prevent regressions.
- **Write documentation.**: Add documentation to the `doc/` directory. Describe
- the feature and include screenshots, if applicable.
+ the feature and include screenshots, if applicable. Indicate [what editions](documentation/styleguide.md#product-badges)
+ the feature applies to.
- **Submit a MR to the `www-gitlab-com` project.**: Add the new feature to the
[EE features list](https://about.gitlab.com/features/).
@@ -111,7 +112,7 @@ There are a few gotchas with it:
smaller methods. Or create a "hook" method that is empty in CE,
and with the EE-specific implementation in EE.
- when the original implementation contains a guard clause (e.g.
- `return unless condition`), we cannot easily extend the behaviour by
+ `return unless condition`), we cannot easily extend the behavior by
overriding the method, because we can't know when the overridden method
(i.e. calling `super` in the overriding method) would want to stop early.
In this case, we shouldn't just override it, but update the original method
@@ -131,7 +132,7 @@ There are a few gotchas with it:
```
Instead of just overriding `Base#execute`, we should update it and extract
- the behaviour into another method:
+ the behavior into another method:
```ruby
class Base
@@ -513,7 +514,7 @@ do that, so we'll follow regular object-oriented practices that we define the
interface first here.
For example, suppose we have a few more optional parameters for EE. We can move the
-paramters out of the `Grape::API` class to a helper module, so we can inject it
+parameters out of the `Grape::API` class to a helper module, so we can inject it
before it would be used in the class.
```ruby
@@ -613,9 +614,9 @@ module EE
end
```
-#### EE-specific behaviour
+#### EE-specific behavior
-Sometimes we need EE-specific behaviour in some of the APIs. Normally we could
+Sometimes we need EE-specific behavior in some of the APIs. Normally we could
use EE methods to override CE methods, however API routes are not methods and
therefore can't be simply overridden. We need to extract them into a standalone
method, or introduce some "hooks" where we could inject behavior in the CE
diff --git a/doc/development/elasticsearch.md b/doc/development/elasticsearch.md
index 758cecce315..185f536fc01 100644
--- a/doc/development/elasticsearch.md
+++ b/doc/development/elasticsearch.md
@@ -7,7 +7,7 @@ the [Elasticsearch integration documentation](../integration/elasticsearch.md#en
## Deep Dive
-In June 2019, Mario de la Ossa hosted a [Deep Dive](https://gitlab.com/gitlab-org/create-stage/issues/1) on GitLab's [Elasticsearch integration](../integration/elasticsearch.md) to share his domain specific knowledge with anyone who may work in this part of the code base in the future. You can find the [recording on YouTube](https://www.youtube.com/watch?v=vrvl-tN2EaA), and the slides on [Google Slides](https://docs.google.com/presentation/d/1H-pCzI_LNrgrL5pJAIQgvLX8Ji0-jIKOg1QeJQzChug/edit) and in [PDF](https://gitlab.com/gitlab-org/create-stage/uploads/c5aa32b6b07476fa8b597004899ec538/Elasticsearch_Deep_Dive.pdf). Everything covered in this deep dive was accurate as of GitLab 12.0, and while specific details may have changed since then, it should still serve as a good introduction.
+In June 2019, Mario de la Ossa hosted a Deep Dive (GitLab team members only: `https://gitlab.com/gitlab-org/create-stage/issues/1`) on GitLab's [Elasticsearch integration](../integration/elasticsearch.md) to share his domain specific knowledge with anyone who may work in this part of the code base in the future. You can find the [recording on YouTube](https://www.youtube.com/watch?v=vrvl-tN2EaA), and the slides on [Google Slides](https://docs.google.com/presentation/d/1H-pCzI_LNrgrL5pJAIQgvLX8Ji0-jIKOg1QeJQzChug/edit) and in [PDF](https://gitlab.com/gitlab-org/create-stage/uploads/c5aa32b6b07476fa8b597004899ec538/Elasticsearch_Deep_Dive.pdf). Everything covered in this deep dive was accurate as of GitLab 12.0, and while specific details may have changed since then, it should still serve as a good introduction.
## Supported Versions
diff --git a/doc/development/emails.md b/doc/development/emails.md
index a84895eef5b..e6e2ea8aae7 100644
--- a/doc/development/emails.md
+++ b/doc/development/emails.md
@@ -14,13 +14,11 @@ Please note that [S/MIME signed](../administration/smime_signing_email.md) email
Rails provides a way to preview our mailer templates in HTML and plaintext using
dummy data.
-The previews live in [`app/mailers/previews`][previews] and can be viewed at
+The previews live in [`app/mailers/previews`](https://gitlab.com/gitlab-org/gitlab-foss/tree/master/app/mailers/previews) and can be viewed at
[`/rails/mailers`](http://localhost:3000/rails/mailers).
See the [Rails guides](https://guides.rubyonrails.org/action_mailer_basics.html#previewing-emails) for more information.
-[previews]: https://gitlab.com/gitlab-org/gitlab-foss/tree/master/app/mailers/previews
-
## Incoming email
1. Go to the GitLab installation directory.
@@ -59,6 +57,9 @@ See the [Rails guides](https://guides.rubyonrails.org/action_mailer_basics.html#
mailbox: "inbox"
# The IDLE command timeout.
idle_timeout: 60
+
+ # Whether to expunge (permanently remove) messages from the mailbox when they are deleted after delivery
+ expunge_deleted: false
```
As mentioned, the part after `+` is ignored, and this will end up in the mailbox for `gitlab-incoming@gmail.com`.
@@ -87,7 +88,7 @@ for the format of the email key:
- Actions are always at the end, separated by `-`. For example `-issue` or `-merge-request`
- If your feature is related to a project, the key begins with the project identifiers (project path slug
- and project id), separated by `-`. For example, `gitlab-org-gitlab-foss-20`
+ and project ID), separated by `-`. For example, `gitlab-org-gitlab-foss-20`
- Additional information, such as an author's token, can be added between the project identifiers and
the action, separated by `-`. For example, `gitlab-org-gitlab-foss-20-Author_Token12345678-issue`
- You register your handlers in `lib/gitlab/email/handler.rb`
diff --git a/doc/development/event_tracking/backend.md b/doc/development/event_tracking/backend.md
index dc4d7279671..93e135772ef 100644
--- a/doc/development/event_tracking/backend.md
+++ b/doc/development/event_tracking/backend.md
@@ -1,5 +1,5 @@
---
-redirect_to: '../../telemetry/backend.md'
+redirect_to: '../telemetry/index.md'
---
-This document was moved to [another location](../../telemetry/backend.md).
+This document was moved to [another location](../telemetry/index.md).
diff --git a/doc/development/event_tracking/frontend.md b/doc/development/event_tracking/frontend.md
index 0e98daf15bb..93e135772ef 100644
--- a/doc/development/event_tracking/frontend.md
+++ b/doc/development/event_tracking/frontend.md
@@ -1,5 +1,5 @@
---
-redirect_to: '../../telemetry/frontend.md'
+redirect_to: '../telemetry/index.md'
---
-This document was moved to [another location](../../telemetry/frontend.md).
+This document was moved to [another location](../telemetry/index.md).
diff --git a/doc/development/event_tracking/index.md b/doc/development/event_tracking/index.md
index ae555e99c6b..93e135772ef 100644
--- a/doc/development/event_tracking/index.md
+++ b/doc/development/event_tracking/index.md
@@ -1,5 +1,5 @@
---
-redirect_to: '../../telemetry/index.md'
+redirect_to: '../telemetry/index.md'
---
-This document was moved to [another location](../../telemetry/index.md).
+This document was moved to [another location](../telemetry/index.md).
diff --git a/doc/development/experiment_guide/index.md b/doc/development/experiment_guide/index.md
index ffa95d86876..f0e05139cba 100644
--- a/doc/development/experiment_guide/index.md
+++ b/doc/development/experiment_guide/index.md
@@ -32,10 +32,9 @@ The author then adds a comment to this piece of code and adds a link to the issu
#...
},
# Add your experiment here:
- sign_up_flow: {
- feature_toggle: :experimental_sign_up_flow, # Feature flag that will be used
- environment: ::Gitlab.dev_env_or_com?, # Target environment
- enabled_ratio: 0.1 # Percentage of users that will be part of the experiment. 10% of the users would be part of this experiments.
+ signup_flow: {
+ environment: ::Gitlab.dev_env_or_com?, # Target environment, defaults to enabled for development and GitLab.com
+ tracking_category: 'Growth::Acquisition::Experiment::SignUpFlow' # Used for providing the category when setting up tracking data
}
}.freeze
```
@@ -46,7 +45,7 @@ The author then adds a comment to this piece of code and adds a link to the issu
class RegistrationController < Applicationcontroller
def show
# experiment_enabled?(:feature_name) is also available in views and helpers
- if experiment_enabled?(:sign_up_flow)
+ if experiment_enabled?(:signup_flow)
# render the experiment
else
# render the original version
@@ -55,13 +54,18 @@ The author then adds a comment to this piece of code and adds a link to the issu
end
```
-- Track necessary events. See the [telemetry guide](../../telemetry/index.md) for details.
+- Track necessary events. See the [telemetry guide](../telemetry/index.md) for details.
- After the merge request is merged, use [`chatops`](../../ci/chatops/README.md) in the
-[appropriate channel](../feature_flags/controls.md#where-to-run-commands) to enable the feature flag and start the experiment.
+[appropriate channel](../feature_flags/controls.md#communicate-the-change) to start the experiment for 10% of the users.
+The feature flag should have the name of the experiment with the `_experiment_percentage` suffix appended.
For visibility, please also share any commands run against production in the `#s_growth` channel:
```shell
- /chatops run feature set --project=gitlab-org/gitlab experimental_sign_up_flow true
+ /chatops run feature set signup_flow_experiment_percentage 10
```
- If you notice issues with the experiment, you can disable the experiment by setting the feature flag to `false` again.
+ If you notice issues with the experiment, you can disable the experiment by removing the feature flag:
+
+ ```shell
+ /chatops run feature delete signup_flow_experiment_percentage
+ ```
diff --git a/doc/development/fe_guide/accessibility.md b/doc/development/fe_guide/accessibility.md
index 4fd9a4fed60..998c71135fb 100644
--- a/doc/development/fe_guide/accessibility.md
+++ b/doc/development/fe_guide/accessibility.md
@@ -2,19 +2,12 @@
## Resources
-[Chrome Accessibility Developer Tools][chrome-accessibility-developer-tools]
+[Chrome Accessibility Developer Tools](https://github.com/GoogleChrome/accessibility-developer-tools)
are useful for testing for potential accessibility problems in GitLab.
-The [axe][axe-website] browser extension (available for [Firefox][axe-firefox-extension] and [Chrome][axe-chrome-extension]) is
+The [axe](https://www.deque.com/axe/) browser extension (available for [Firefox](https://addons.mozilla.org/en-US/firefox/addon/axe-devtools/) and [Chrome](https://chrome.google.com/webstore/detail/axe-web-accessibility-tes/lhdoppojpmngadmnindnejefpokejbdd)) is
also a handy tool for running audits and getting feedback on markup, CSS and even potentially problematic color usages.
Accessibility best-practices and more in-depth information are available on
-[the Audit Rules page][audit-rules] for the Chrome Accessibility Developer Tools. The "[awesome a11y][awesome-a11y]" list is also a
+[the Audit Rules page](https://github.com/GoogleChrome/accessibility-developer-tools/wiki/Audit-Rules) for the Chrome Accessibility Developer Tools. The [Awesome Accessibility](https://github.com/brunopulis/awesome-a11y) list is also a
useful compilation of accessibility-related material.
-
-[chrome-accessibility-developer-tools]: https://github.com/GoogleChrome/accessibility-developer-tools
-[audit-rules]: https://github.com/GoogleChrome/accessibility-developer-tools/wiki/Audit-Rules
-[axe-website]: https://www.deque.com/axe/
-[axe-firefox-extension]: https://addons.mozilla.org/en-US/firefox/addon/axe-devtools/
-[axe-chrome-extension]: https://chrome.google.com/webstore/detail/axe-web-accessibility-tes/lhdoppojpmngadmnindnejefpokejbdd
-[awesome-a11y]: https://github.com/brunopulis/awesome-a11y
diff --git a/doc/development/fe_guide/axios.md b/doc/development/fe_guide/axios.md
index 6e7cf523f36..f8d301dac5e 100644
--- a/doc/development/fe_guide/axios.md
+++ b/doc/development/fe_guide/axios.md
@@ -1,13 +1,13 @@
# Axios
-We use [axios][axios] to communicate with the server in Vue applications and most new code.
+We use [axios](https://github.com/axios/axios) to communicate with the server in Vue applications and most new code.
In order to guarantee all defaults are set you *should not use `axios` directly*, you should import `axios` from `axios_utils`.
## CSRF token
All our request require a CSRF token.
-To guarantee this token is set, we are importing [axios][axios], setting the token, and exporting `axios` .
+To guarantee this token is set, we are importing [axios](https://github.com/axios/axios), setting the token, and exporting `axios` .
This exported module should be used instead of directly using `axios` to ensure the token is set.
@@ -32,19 +32,16 @@ This exported module should be used instead of directly using `axios` to ensure
## Mock axios response in tests
-To help us mock the responses we are using [axios-mock-adapter][axios-mock-adapter].
+To help us mock the responses we are using [axios-mock-adapter](https://github.com/ctimmerm/axios-mock-adapter).
-Advantages over [`spyOn()`]:
+Advantages over [`spyOn()`](https://jasmine.github.io/api/edge/global.html#spyOn):
- no need to create response objects
- does not allow call through (which we want to avoid)
- simple API to test error cases
- provides `replyOnce()` to allow for different responses
-We have also decided against using [axios interceptors] because they are not suitable for mocking.
-
-[axios interceptors]: https://github.com/axios/axios#interceptors
-[`spyOn()`]: https://jasmine.github.io/api/edge/global.html#spyOn
+We have also decided against using [axios interceptors](https://github.com/axios/axios#interceptors) because they are not suitable for mocking.
### Example
@@ -77,8 +74,3 @@ Because polling function requires a header object, we need to always include an
```javascript
mock.onGet('/users').reply(200, { foo: 'bar' }, {});
```
-
-[axios]: https://github.com/axios/axios
-[axios-instance]: #creating-an-instance
-[axios-interceptors]: https://github.com/axios/axios#interceptors
-[axios-mock-adapter]: https://github.com/ctimmerm/axios-mock-adapter
diff --git a/doc/development/fe_guide/design_patterns.md b/doc/development/fe_guide/design_patterns.md
index 72a7861ffcb..5b7b16a9a8a 100644
--- a/doc/development/fe_guide/design_patterns.md
+++ b/doc/development/fe_guide/design_patterns.md
@@ -75,6 +75,4 @@ class Foo {
new Foo({ container: '.my-element' });
```
-You can find an example of the above in this [class][container-class-example];
-
-[container-class-example]: https://gitlab.com/gitlab-org/gitlab/blob/master/app/assets/javascripts/mini_pipeline_graph_dropdown.js
+You can find an example of the above in this [class](https://gitlab.com/gitlab-org/gitlab/blob/master/app/assets/javascripts/mini_pipeline_graph_dropdown.js);
diff --git a/doc/development/fe_guide/droplab/droplab.md b/doc/development/fe_guide/droplab/droplab.md
index 4d7c882dc09..83bc4216403 100644
--- a/doc/development/fe_guide/droplab/droplab.md
+++ b/doc/development/fe_guide/droplab/droplab.md
@@ -26,7 +26,7 @@ If you do not provide any arguments, it will globally query and instantiate all
<ul>
```
-```js
+```javascript
const droplab = new DropLab();
droplab.init();
```
@@ -47,7 +47,7 @@ You can add static list items.
<ul>
```
-```js
+```javascript
const droplab = new DropLab();
droplab.init();
```
@@ -65,7 +65,7 @@ a non-global instance of DropLab using the `DropLab.prototype.init` method.
<ul>
```
-```js
+```javascript
const trigger = document.getElementById('trigger');
const list = document.getElementById('list');
@@ -83,7 +83,7 @@ You can also add hooks to an existing DropLab instance using `DropLab.prototype.
<ul id="list" data-dropdown><!-- ... --><ul>
```
-```js
+```javascript
const droplab = new DropLab();
droplab.init();
@@ -114,7 +114,7 @@ for all `data-dynamic` dropdown lists tracked by that DropLab instance.
</ul>
```
-```js
+```javascript
const droplab = new DropLab();
droplab.init().addData([{
@@ -137,7 +137,7 @@ the data as the second argument and the `id` of the trigger element as the first
</ul>
```
-```js
+```javascript
const droplab = new DropLab();
droplab.init().addData('trigger', [{
@@ -167,7 +167,7 @@ dropdown lists, one of which is dynamic.
</div>
```
-```js
+```javascript
const droplab = new DropLab();
droplab.init().addData('trigger', [{
@@ -224,7 +224,7 @@ Some plugins require configuration values, the config object can be passed as th
<ul id="list" data-dropdown><!-- ... --><ul>
```
-```js
+```javascript
const droplab = new DropLab();
const trigger = document.getElementById('trigger');
@@ -249,7 +249,7 @@ droplab.init(trigger, list, [droplabAjax], {
When plugins are initialised for a droplab trigger+dropdown, DropLab will
call the plugins `init` function, so this must be implemented in the plugin.
-```js
+```javascript
class MyPlugin {
static init() {
this.someProp = 'someProp';
diff --git a/doc/development/fe_guide/droplab/plugins/ajax.md b/doc/development/fe_guide/droplab/plugins/ajax.md
index 77ba6f739e6..abc208e7568 100644
--- a/doc/development/fe_guide/droplab/plugins/ajax.md
+++ b/doc/development/fe_guide/droplab/plugins/ajax.md
@@ -18,7 +18,7 @@ Add the `Ajax` object to the plugins array of a `DropLab.prototype.init` or `Dro
<ul id="list" data-dropdown><!-- ... --><ul>
```
-```js
+```javascript
const droplab = new DropLab();
const trigger = document.getElementById('trigger');
diff --git a/doc/development/fe_guide/droplab/plugins/filter.md b/doc/development/fe_guide/droplab/plugins/filter.md
index b867394a241..876149e4872 100644
--- a/doc/development/fe_guide/droplab/plugins/filter.md
+++ b/doc/development/fe_guide/droplab/plugins/filter.md
@@ -18,7 +18,7 @@ Add the `Filter` object to the plugins array of a `DropLab.prototype.init` or `D
<ul>
```
-```js
+```javascript
const droplab = new DropLab();
const trigger = document.getElementById('trigger');
diff --git a/doc/development/fe_guide/droplab/plugins/input_setter.md b/doc/development/fe_guide/droplab/plugins/input_setter.md
index db492da478a..9b2e1e8faab 100644
--- a/doc/development/fe_guide/droplab/plugins/input_setter.md
+++ b/doc/development/fe_guide/droplab/plugins/input_setter.md
@@ -23,7 +23,7 @@ You can also set the `InputSetter` config to an array of objects, which will all
<ul>
```
-```js
+```javascript
const droplab = new DropLab();
const trigger = document.getElementById('trigger');
diff --git a/doc/development/fe_guide/event_tracking.md b/doc/development/fe_guide/event_tracking.md
index ae555e99c6b..93e135772ef 100644
--- a/doc/development/fe_guide/event_tracking.md
+++ b/doc/development/fe_guide/event_tracking.md
@@ -1,5 +1,5 @@
---
-redirect_to: '../../telemetry/index.md'
+redirect_to: '../telemetry/index.md'
---
-This document was moved to [another location](../../telemetry/index.md).
+This document was moved to [another location](../telemetry/index.md).
diff --git a/doc/development/fe_guide/frontend_faq.md b/doc/development/fe_guide/frontend_faq.md
index 3b0b1d8f0da..8f8f162609a 100644
--- a/doc/development/fe_guide/frontend_faq.md
+++ b/doc/development/fe_guide/frontend_faq.md
@@ -39,7 +39,7 @@ bundle exec rake routes | grep "issues"
### 2. `modal_copy_button` vs `clipboard_button`
-The `clipboard_button` uses the `copy_to_clipboard.js` behaviour, which is
+The `clipboard_button` uses the `copy_to_clipboard.js` behavior, which is
initialized on page load, so if there are vue-based clipboard buttons that
don't exist at page load (such as ones in a `GlModal`), they do not have the
click handlers associated with the clipboard package.
diff --git a/doc/development/fe_guide/graphql.md b/doc/development/fe_guide/graphql.md
index 8ccc734ee35..caf84d04490 100644
--- a/doc/development/fe_guide/graphql.md
+++ b/doc/development/fe_guide/graphql.md
@@ -10,13 +10,13 @@ their execution by clicking **Execute query** button on the top left:
![GraphiQL interface](img/graphiql_explorer_v12_4.png)
-We use [Apollo] and [Vue Apollo][vue-apollo] for working with GraphQL
+We use [Apollo](https://www.apollographql.com/) and [Vue Apollo](https://github.com/vuejs/vue-apollo) for working with GraphQL
on the frontend.
## Apollo Client
To save duplicated clients getting created in different apps, we have a
-[default client][default-client] that should be used. This setups the
+[default client](https://gitlab.com/gitlab-org/gitlab/blob/master/app/assets/javascripts/lib/graphql.js) that should be used. This setups the
Apollo client with the correct URL and also sets the CSRF headers.
Default client accepts two parameters: `resolvers` and `config`.
@@ -73,7 +73,7 @@ More about fragments:
## Usage in Vue
-To use Vue Apollo, import the [Vue Apollo][vue-apollo] plugin as well
+To use Vue Apollo, import the [Vue Apollo](https://github.com/vuejs/vue-apollo) plugin as well
as the default client. This should be created at the same point
the Vue application is mounted.
@@ -94,7 +94,7 @@ new Vue({
});
```
-Read more about [Vue Apollo][vue-apollo] in the [Vue Apollo documentation](https://vue-apollo.netlify.com/guide/).
+Read more about [Vue Apollo](https://github.com/vuejs/vue-apollo) in the [Vue Apollo documentation](https://vue-apollo.netlify.com/guide/).
### Local state with Apollo
@@ -212,7 +212,7 @@ Read more about local state management with Apollo in the [Vue Apollo documentat
When Apollo Client is used within Vuex and fetched data is stored in the Vuex store, there is no need in keeping Apollo Client cache enabled. Otherwise we would have data from the API stored in two places - Vuex store and Apollo Client cache. More to say, with Apollo default settings, a subsequent fetch from the GraphQL API could result in fetching data from Apollo cache (in the case where we have the same query and variables). To prevent this behavior, we need to disable Apollo Client cache passing a valid `fetchPolicy` option to its constructor:
-```js
+```javascript
import fetchPolicies from '~/graphql_shared/fetch_policy_constants';
export const gqClient = createGqClient(
@@ -298,7 +298,8 @@ handleClick() {
GitLab's GraphQL API uses [Relay-style cursor pagination](https://www.apollographql.com/docs/react/data/pagination/#cursor-based)
for connection types. This means a "cursor" is used to keep track of where in the data
-set the next items should be fetched from.
+set the next items should be fetched from. [GraphQL Ruby Connection Concepts](https://graphql-ruby.org/pagination/connection_concepts.html)
+is a good overview and introduction to connections.
Every connection type (for example, `DesignConnection` and `DiscussionConnection`) has a field `pageInfo` that contains an information required for pagination:
@@ -426,7 +427,7 @@ This should be resolved in the scope of the issue
#### Mocking response as component data
-With [Vue test utils][vue-test-utils] it is easy to quickly test components that
+With [Vue test utils](https://vue-test-utils.vuejs.org/) it is easy to quickly test components that
fetch GraphQL queries. The simplest way is to use `shallowMount` and then set
the data on the component
@@ -598,11 +599,4 @@ defaultClient.query({ query })
.then(result => console.log(result));
```
-Read more about the [Apollo] client in the [Apollo documentation](https://www.apollographql.com/docs/tutorial/client/).
-
-[Apollo]: https://www.apollographql.com/
-[vue-apollo]: https://github.com/vuejs/vue-apollo
-[feature-flags]: ../feature_flags.md
-[default-client]: https://gitlab.com/gitlab-org/gitlab/blob/master/app/assets/javascripts/lib/graphql.js
-[vue-test-utils]: https://vue-test-utils.vuejs.org/
-[apollo-link-state]: https://www.apollographql.com/docs/link/links/state.html
+Read more about the [Apollo](https://www.apollographql.com/) client in the [Apollo documentation](https://www.apollographql.com/docs/tutorial/client/).
diff --git a/doc/development/fe_guide/icons.md b/doc/development/fe_guide/icons.md
index ea321330c41..4fb738f5466 100644
--- a/doc/development/fe_guide/icons.md
+++ b/doc/development/fe_guide/icons.md
@@ -1,8 +1,8 @@
# Icons and SVG Illustrations
-We manage our own Icon and Illustration library in the [`gitlab-svgs`][gitlab-svgs] repository.
-This repository is published on [npm][npm] and managed as a dependency via yarn.
-You can browse all available Icons and Illustrations [here][svg-preview].
+We manage our own Icon and Illustration library in the [`gitlab-svgs`](https://gitlab.com/gitlab-org/gitlab-svgs) repository.
+This repository is published on [npm](https://www.npmjs.com/package/@gitlab/svgs) and managed as a dependency via yarn.
+You can browse all available Icons and Illustrations [here](https://gitlab-org.gitlab.io/gitlab-svgs).
To upgrade to a new version run `yarn upgrade @gitlab/svgs`.
## Icons
@@ -22,7 +22,7 @@ sprite_icon(icon_name, size: nil, css_class: '')
```
- **icon_name** Use the icon_name that you can find in the SVG Sprite
- ([Overview is available here][svg-preview]).
+ ([Overview is available here](https://gitlab-org.gitlab.io/gitlab-svgs)).
- **size (optional)** Use one of the following sizes : 16, 24, 32, 48, 72 (this will be translated into a `s16` class)
- **css_class (optional)** If you want to add additional css classes
@@ -42,7 +42,7 @@ sprite_icon(icon_name, size: nil, css_class: '')
### Usage in Vue
-[GitLab UI][gitlab-ui], our components library, provides a component to display sprite icons.
+[GitLab UI](https://gitlab-org.gitlab.io/gitlab-ui/), our components library, provides a component to display sprite icons.
Sample usage :
@@ -65,7 +65,7 @@ export default {
</template>
```
-- **name** Name of the Icon in the SVG Sprite ([Overview is available here][svg-preview]).
+- **name** Name of the Icon in the SVG Sprite ([Overview is available here](https://gitlab-org.gitlab.io/gitlab-svgs)).
- **size (optional)** Number value for the size which is then mapped to a specific CSS class
(Available Sizes: 8, 12, 16, 18, 24, 32, 48, 72 are mapped to `sXX` css classes)
- **css-classes (optional)** Additional CSS Classes to add to the svg tag.
@@ -111,8 +111,3 @@ export default {
<img :src="svgIllustrationPath" />
</template>
```
-
-[npm]: https://www.npmjs.com/package/@gitlab/svgs
-[gitlab-svgs]: https://gitlab.com/gitlab-org/gitlab-svgs
-[svg-preview]: https://gitlab-org.gitlab.io/gitlab-svgs
-[gitlab-ui]: https://gitlab-org.gitlab.io/gitlab-ui/
diff --git a/doc/development/fe_guide/index.md b/doc/development/fe_guide/index.md
index 790cd94cec4..8fd6ba459b9 100644
--- a/doc/development/fe_guide/index.md
+++ b/doc/development/fe_guide/index.md
@@ -5,17 +5,17 @@ across GitLab's frontend team.
## Overview
-GitLab is built on top of [Ruby on Rails](https://rubyonrails.org) using [Haml][haml] and also a JavaScript based Frontend with [Vue.js](https://vuejs.org).
-Be wary of [the limitations that come with using Hamlit][hamlit-limits]. We also use [SCSS](https://sass-lang.com) and plain JavaScript with
-modern ECMAScript standards supported through [Babel][babel] and ES module support through [webpack][webpack].
+GitLab is built on top of [Ruby on Rails](https://rubyonrails.org) using [Haml](http://haml.info/) and also a JavaScript based Frontend with [Vue.js](https://vuejs.org).
+Be wary of [the limitations that come with using Hamlit](https://github.com/k0kubun/hamlit/blob/master/REFERENCE.md#limitations). We also use [SCSS](https://sass-lang.com) and plain JavaScript with
+modern ECMAScript standards supported through [Babel](https://babeljs.io/) and ES module support through [webpack](https://webpack.js.org/).
Working with our frontend assets requires Node (v10.13.0 or greater) and Yarn
(v1.10.0 or greater). You can find information on how to install these on our
-[installation guide][install].
+[installation guide](../../install/installation.md#4-node).
### Browser Support
-For our currently-supported browsers, see our [requirements][requirements].
+For our currently-supported browsers, see our [requirements](../../install/requirements.md#supported-web-browsers).
Use [BrowserStack](https://www.browserstack.com/) to test with our supported browsers. Login to BrowserStack with the credentials saved in GitLab's [shared 1Password account](https://about.gitlab.com/handbook/security/#1password-for-teams).
@@ -83,7 +83,7 @@ Read the [frontend's FAQ](frontend_faq.md) for common small pieces of helpful in
See the relevant style guides for our guidelines and for information on linting:
- [JavaScript](style/javascript.md). Our guide is based on
-the excellent [Airbnb][airbnb-js-style-guide] style guide with a few small
+the excellent [Airbnb](https://github.com/airbnb/javascript) style guide with a few small
changes.
- [SCSS](style/scss.md): our SCSS conventions which are enforced through [`scss-lint`](https://github.com/sds/scss-lint).
- [HTML](style/html.md). Guidelines for writing HTML code consistent with the rest of the codebase.
@@ -109,14 +109,3 @@ Our accessibility standards and resources.
Frontend internationalization support is described in [this document](../i18n/).
The [externalization part of the guide](../i18n/externalization.md) explains the helpers/methods available.
-
-[haml]: http://haml.info/
-[hamlit]: https://github.com/k0kubun/hamlit
-[hamlit-limits]: https://github.com/k0kubun/hamlit/blob/master/REFERENCE.md#limitations
-[babel]: https://babeljs.io/
-[webpack]: https://webpack.js.org/
-[jquery]: https://jquery.com/
-[axios]: https://github.com/axios/axios
-[airbnb-js-style-guide]: https://github.com/airbnb/javascript
-[install]: ../../install/installation.md#4-node
-[requirements]: ../../install/requirements.md#supported-web-browsers
diff --git a/doc/development/fe_guide/performance.md b/doc/development/fe_guide/performance.md
index fcba8758c2f..aaa6bb16fab 100644
--- a/doc/development/fe_guide/performance.md
+++ b/doc/development/fe_guide/performance.md
@@ -41,9 +41,9 @@ But in general it should be handled automatically through a `MutationObserver` i
Only animate `opacity` & `transform` properties. Other properties (such as `top`, `left`, `margin`, and `padding`) all cause
Layout to be recalculated, which is much more expensive. For details on this, see "Styles that Affect Layout" in
-[High Performance Animations][high-perf-animations].
+[High Performance Animations](https://www.html5rocks.com/en/tutorials/speed/high-performance-animations/).
-If you _do_ need to change layout (e.g. a sidebar that pushes main content over), prefer [FLIP][flip] to change expensive
+If you _do_ need to change layout (e.g. a sidebar that pushes main content over), prefer [FLIP](https://aerotwist.com/blog/flip-your-animations/) to change expensive
properties once, and handle the actual animation with transforms.
## Reducing Asset Footprint
@@ -160,18 +160,13 @@ General tips:
- If some functionality can reasonably be achieved without adding extra libraries, avoid them.
- Use page-specific JavaScript as described above to load libraries that are only needed on certain pages.
- Use code-splitting dynamic imports wherever possible to lazy-load code that is not needed initially.
-- [High Performance Animations][high-perf-animations]
+- [High Performance Animations](https://www.html5rocks.com/en/tutorials/speed/high-performance-animations/)
---
## Additional Resources
- [WebPage Test](https://www.webpagetest.org) for testing site loading time and size.
-- [Google PageSpeed Insights][pagespeed-insights] grades web pages and provides feedback to improve the page.
+- [Google PageSpeed Insights](https://developers.google.com/speed/pagespeed/insights/) grades web pages and provides feedback to improve the page.
- [Profiling with Chrome DevTools](https://developers.google.com/web/tools/chrome-devtools/)
-- [Browser Diet][browser-diet] is a community-built guide that catalogues practical tips for improving web page performance.
-
-[pagespeed-insights]: https://developers.google.com/speed/pagespeed/insights/
-[browser-diet]: https://browserdiet.com/
-[high-perf-animations]: https://www.html5rocks.com/en/tutorials/speed/high-performance-animations/
-[flip]: https://aerotwist.com/blog/flip-your-animations/
+- [Browser Diet](https://browserdiet.com/) is a community-built guide that catalogues practical tips for improving web page performance.
diff --git a/doc/development/fe_guide/security.md b/doc/development/fe_guide/security.md
index 7dba61d6b45..a001dd83ab7 100644
--- a/doc/development/fe_guide/security.md
+++ b/doc/development/fe_guide/security.md
@@ -2,8 +2,8 @@
## Resources
-[Mozilla’s HTTP Observatory CLI][observatory-cli] and the
-[Qualys SSL Labs Server Test][qualys-ssl] are good resources for finding
+[Mozilla’s HTTP Observatory CLI](https://github.com/mozilla/http-observatory-cli) and the
+[Qualys SSL Labs Server Test](https://www.ssllabs.com/ssltest/analyze.html) are good resources for finding
potential problems and ensuring compliance with security best practices.
<!-- Uncomment these sections when CSP/SRI are implemented.
@@ -29,14 +29,14 @@ Some exceptions include:
- Connecting with GitHub, Bitbucket, GitLab.com, etc. to allow project importing.
- Connecting with Google, Twitter, GitHub, etc. to allow OAuth authentication.
-We use [the Secure Headers gem][secure_headers] to enable Content
+We use [the Secure Headers gem](https://github.com/twitter/secureheaders) to enable Content
Security Policy headers in the GitLab Rails app.
Some resources on implementing Content Security Policy:
-- [MDN Article on CSP][mdn-csp]
-- [GitHub’s CSP Journey on the GitHub Engineering Blog][github-eng-csp]
-- The Dropbox Engineering Blog's series on CSP: [1][dropbox-csp-1], [2][dropbox-csp-2], [3][dropbox-csp-3], [4][dropbox-csp-4]
+- [MDN Article on CSP](https://developer.mozilla.org/en-US/docs/Web/Security/CSP)
+- [GitHub’s CSP Journey on the GitHub Engineering Blog](http://githubengineering.com/githubs-csp-journey/)
+- The Dropbox Engineering Blog's series on CSP: [1](https://blogs.dropbox.com/tech/2015/09/on-csp-reporting-and-filtering/), [2](https://blogs.dropbox.com/tech/2015/09/unsafe-inline-and-nonce-deployment/), [3](https://blogs.dropbox.com/tech/2015/09/csp-the-unexpected-eval/), [4](https://blogs.dropbox.com/tech/2015/09/csp-third-party-integrations-and-privilege-separation/)
### Subresource Integrity (SRI)
@@ -52,8 +52,8 @@ All CSS and JavaScript assets should use Subresource Integrity.
Some resources on implementing Subresource Integrity:
-- [MDN Article on SRI][mdn-sri]
-- [Subresource Integrity on the GitHub Engineering Blog][github-eng-sri]
+- [MDN Article on SRI](https://developer.mozilla.org/en-us/docs/web/security/subresource_integrity)
+- [Subresource Integrity on the GitHub Engineering Blog](http://githubengineering.com/subresource-integrity/)
-->
@@ -67,7 +67,7 @@ such as with reCAPTCHA, which cannot be used without an `iframe`.
## Avoiding inline scripts and styles
-In order to protect users from [XSS vulnerabilities][xss], we will disable
+In order to protect users from [XSS vulnerabilities](https://en.wikipedia.org/wiki/Cross-site_scripting), we will disable
inline scripts in the future using Content Security Policy.
While inline scripts can be useful, they're also a security concern. If
@@ -77,16 +77,3 @@ inject scripts into the web app.
Inline styles should be avoided in almost all cases, they should only be used
when no alternatives can be found. This allows reusability of styles as well as
readability.
-
-[observatory-cli]: https://github.com/mozilla/http-observatory-cli
-[qualys-ssl]: https://www.ssllabs.com/ssltest/analyze.html
-[secure_headers]: https://github.com/twitter/secureheaders
-[mdn-csp]: https://developer.mozilla.org/en-US/docs/Web/Security/CSP
-[github-eng-csp]: http://githubengineering.com/githubs-csp-journey/
-[dropbox-csp-1]: https://blogs.dropbox.com/tech/2015/09/on-csp-reporting-and-filtering/
-[dropbox-csp-2]: https://blogs.dropbox.com/tech/2015/09/unsafe-inline-and-nonce-deployment/
-[dropbox-csp-3]: https://blogs.dropbox.com/tech/2015/09/csp-the-unexpected-eval/
-[dropbox-csp-4]: https://blogs.dropbox.com/tech/2015/09/csp-third-party-integrations-and-privilege-separation/
-[mdn-sri]: https://developer.mozilla.org/en-US/docs/Web/Security/Subresource_Integrity
-[github-eng-sri]: http://githubengineering.com/subresource-integrity/
-[xss]: https://en.wikipedia.org/wiki/Cross-site_scripting
diff --git a/doc/development/fe_guide/style/javascript.md b/doc/development/fe_guide/style/javascript.md
index 7951c702601..b69a6f1941c 100644
--- a/doc/development/fe_guide/style/javascript.md
+++ b/doc/development/fe_guide/style/javascript.md
@@ -184,6 +184,9 @@ This can help to quickly understand the control flow.
// bad
if (isThingNull) return '';
+if (isThingNull)
+ return '';
+
// good
if (isThingNull) {
return '';
@@ -192,7 +195,7 @@ if (isThingNull) {
## ESLint
-ESLint behaviour can be found in our [tooling guide](../tooling.md).
+ESLint behavior can be found in our [tooling guide](../tooling.md).
## IIFEs
diff --git a/doc/development/fe_guide/style/scss.md b/doc/development/fe_guide/style/scss.md
index 6c0247ad00c..336c9b8ca35 100644
--- a/doc/development/fe_guide/style/scss.md
+++ b/doc/development/fe_guide/style/scss.md
@@ -9,30 +9,25 @@ easy to maintain, and performant for the end-user.
## Rules
+Our CSS is a mixture of current and legacy approaches. That means sometimes it may be difficult to follow this guide to the letter; it means you will definitely run into exceptions, where following the guide is difficult to impossible without outsized effort. In those cases, you may work with your reviewers and maintainers to identify an approach that does not fit these rules. Please endeavor to limit these cases.
+
### Utility Classes
-As part of the effort for [cleaning up our CSS and moving our components into `gitlab-ui`](https://gitlab.com/groups/gitlab-org/-/epics/950)
-led by the [GitLab UI WG](https://gitlab.com/gitlab-com/www-gitlab-com/-/merge_requests/20623) we prefer the use of utility classes over adding new CSS. However, complex CSS can be addressed by adding component classes.
+In order to reduce the generation of more CSS as our site grows, prefer the use of utility classes over adding new CSS. In complex cases, CSS can be addressed by adding component classes.
#### Where are utility classes defined?
-- [Bootstrap's Utility Classes](https://getbootstrap.com/docs/4.3/utilities/)
-- [`common.scss`](https://gitlab.com/gitlab-org/gitlab/blob/master/app/assets/stylesheets/framework/common.scss) (old)
-- [`utilities.scss`](https://gitlab.com/gitlab-org/gitlab/blob/master/app/assets/stylesheets/utilities.scss) (new)
+Prefer the use of [utility classes defined in GitLab UI](https://gitlab.com/gitlab-org/gitlab-ui/-/blob/master/doc/css.md#utilities). An easy list of classes can also be [seen on Unpkg](https://unpkg.com/browse/@gitlab/ui/src/scss/utilities.scss).
-#### Where should I put new utility classes?
+Classes in [`utilities.scss`](https://gitlab.com/gitlab-org/gitlab/blob/master/app/assets/stylesheets/utilities.scss) and [`common.scss`](https://gitlab.com/gitlab-org/gitlab/blob/master/app/assets/stylesheets/framework/common.scss) are being deprecated. Classes in [`common.scss`](https://gitlab.com/gitlab-org/gitlab/blob/master/app/assets/stylesheets/framework/common.scss) that use non-design system values should be avoided in favor of conformant values.
-New utility classes should be added to [`utilities.scss`](https://gitlab.com/gitlab-org/gitlab/blob/master/app/assets/stylesheets/utilities.scss). Existing classes include:
+Avoid [Bootstrap's Utility Classes](https://getbootstrap.com/docs/4.3/utilities/).
-| Name | Pattern | Example |
-|------|---------|---------|
-| Background color | `.bg-{variant}-{shade}` | `.bg-warning-400` |
-| Text color | `.text-{variant}-{shade}` | `.text-success-500` |
-| Font size | `.text-{size}` | `.text-2` |
+#### Where should I put new utility classes?
-- `{variant}` is one of 'primary', 'secondary', 'success', 'warning', 'error'
-- `{shade}` is one of the shades listed on [colors](https://design.gitlab.com/product-foundations/colors/)
-- `{size}` is a number from 1-6 from our [Type scale](https://design.gitlab.com/product-foundations/typography/)
+If a class you need has not been added to GitLab UI, you get to add it! Follow the naming patterns documented in the [utility files](https://gitlab.com/gitlab-org/gitlab-ui/-/tree/master/src/scss/utility-mixins) and refer to [GitLab UI's CSS documentation](https://gitlab.com/gitlab-org/gitlab-ui/-/blob/master/doc/contributing/adding_css.md#adding-utility-mixins) for more details, especially about adding responsive and stateful rules.
+
+If it is not possible to wait for a GitLab UI update (generally one day), add the class to [`utilities.scss`](https://gitlab.com/gitlab-org/gitlab/blob/master/app/assets/stylesheets/utilities.scss) following the same naming conventions documented in GitLab UI. A follow—up issue to backport the class to GitLab UI and delete it from GitLab should be opened.
#### When should I create component classes?
@@ -77,6 +72,24 @@ CSS classes should use the `lowercase-hyphenated` format rather than
}
```
+Class names should be used instead of tag name selectors.
+Using tag name selectors are discouraged in CSS because
+they can affect unintended elements in the hierarchy.
+Also, since they are not meaningful names, they do not
+add meaning to the code.
+
+```scss
+// Bad
+ul {
+ color: #fff;
+}
+
+// Good
+.class-name {
+ color: #fff;
+}
+```
+
### Formatting
You should always use a space before a brace, braces should be on the same
@@ -254,8 +267,8 @@ documentation includes [a full list of their linters](https://github.com/sds/scs
### Fixing issues
If you want to automate changing a large portion of the codebase to conform to
-the SCSS style guide, you can use [CSSComb][csscomb]. First install
-[Node][node] and [NPM][npm], then run `npm install csscomb -g` to install
+the SCSS style guide, you can use [CSSComb](https://github.com/csscomb/csscomb.js). First install
+[Node](https://github.com/nodejs/node) and [NPM](https://www.npmjs.com/), then run `npm install csscomb -g` to install
CSSComb globally (system-wide). Run it in the GitLab directory with
`csscomb app/assets/stylesheets` to automatically fix issues with CSS/SCSS.
@@ -279,7 +292,3 @@ Make sure a comment is added on the line above the `disable` rule, otherwise the
linter will throw a warning. `DisableLinterReason` is enabled to make sure the
style guide isn't being ignored, and to communicate to others why the style
guide is ignored in this instance.
-
-[csscomb]: https://github.com/csscomb/csscomb.js
-[node]: https://github.com/nodejs/node
-[npm]: https://www.npmjs.com/
diff --git a/doc/development/fe_guide/style/vue.md b/doc/development/fe_guide/style/vue.md
index a7d8fc61752..46305cc7217 100644
--- a/doc/development/fe_guide/style/vue.md
+++ b/doc/development/fe_guide/style/vue.md
@@ -53,7 +53,7 @@ Please check this [rules](https://github.com/vuejs/eslint-plugin-vue#bulb-rules)
## Naming
-1. **Extensions**: Use `.vue` extension for Vue components. Do not use `.js` as file extension ([#34371]).
+1. **Extensions**: Use `.vue` extension for Vue components. Do not use `.js` as file extension ([#34371](https://gitlab.com/gitlab-org/gitlab-foss/issues/34371)).
1. **Reference Naming**: Use PascalCase for their instances:
```javascript
@@ -89,8 +89,6 @@ Please check this [rules](https://github.com/vuejs/eslint-plugin-vue#bulb-rules)
<component my-prop="prop" />
```
-[#34371]: https://gitlab.com/gitlab-org/gitlab-foss/issues/34371
-
## Alignment
1. Follow these alignment styles for the template method:
@@ -327,7 +325,7 @@ When using `v-for` you need to provide a *unique* `:key` attribute for each item
</div>
```
-1. When the elements being iterated don't have a unique id, you can use the array index as the `:key` attribute
+1. When the elements being iterated don't have a unique ID, you can use the array index as the `:key` attribute
```html
<div
diff --git a/doc/development/fe_guide/tooling.md b/doc/development/fe_guide/tooling.md
index 0e8f5ed05ed..585cd969c96 100644
--- a/doc/development/fe_guide/tooling.md
+++ b/doc/development/fe_guide/tooling.md
@@ -56,13 +56,13 @@ When declaring multiple globals, always use one `/* global [name] */` line per v
## Formatting with Prettier
-Our code is automatically formatted with [Prettier](https://prettier.io) to follow our style guides. Prettier is taking care of formatting .js, .vue, and .scss files based on the standard prettier rules. You can find all settings for Prettier in `.prettierrc`.
+Our code is automatically formatted with [Prettier](https://prettier.io) to follow our style guides. Prettier is taking care of formatting `.js`, `.vue`, and `.scss` files based on the standard prettier rules. You can find all settings for Prettier in `.prettierrc`.
### Editor
The easiest way to include prettier in your workflow is by setting up your preferred editor (all major editors are supported) accordingly. We suggest setting up prettier to run automatically when each file is saved. Find [here](https://prettier.io/docs/en/editors.html) the best way to set it up in your preferred editor.
-Please take care that you only let Prettier format the same file types as the global Yarn script does (.js, .vue, and .scss). In VSCode by example you can easily exclude file formats in your settings file:
+Please take care that you only let Prettier format the same file types as the global Yarn script does (`.js`, `.vue`, and `.scss`). In VSCode by example you can easily exclude file formats in your settings file:
```json
"prettier.disableLanguages": [
diff --git a/doc/development/fe_guide/vue.md b/doc/development/fe_guide/vue.md
index aeedd57fd83..972c2ded9c9 100644
--- a/doc/development/fe_guide/vue.md
+++ b/doc/development/fe_guide/vue.md
@@ -12,17 +12,17 @@ What is described in the following sections can be found in these examples:
## Vue architecture
-All new features built with Vue.js must follow a [Flux architecture][flux].
+All new features built with Vue.js must follow a [Flux architecture](https://facebook.github.io/flux/).
The main goal we are trying to achieve is to have only one data flow and only one data entry.
In order to achieve this goal we use [vuex](#vuex).
-You can also read about this architecture in vue docs about [state management][state-management]
-and about [one way data flow][one-way-data-flow].
+You can also read about this architecture in vue docs about [state management](https://vuejs.org/v2/guide/state-management.html#Simple-State-Management-from-Scratch)
+and about [one way data flow](https://vuejs.org/v2/guide/components.html#One-Way-Data-Flow).
### Components and Store
-In some features implemented with Vue.js, like the [issue board][issue-boards]
-or [environments table][environments-table]
+In some features implemented with Vue.js, like the [issue board](https://gitlab.com/gitlab-org/gitlab-foss/tree/master/app/assets/javascripts/boards)
+or [environments table](https://gitlab.com/gitlab-org/gitlab-foss/tree/master/app/assets/javascripts/environments)
you can find a clear separation of concerns:
```plaintext
@@ -47,7 +47,7 @@ of the new feature should be.
The Store and the Service should be imported and initialized in this file and
provided as a prop to the main component.
-Be sure to read about [page-specific JavaScript][page_specific_javascript].
+Be sure to read about [page-specific JavaScript](./performance.md#page-specific-javascript).
### Bootstrapping Gotchas
@@ -162,7 +162,7 @@ For example, tables are used in a quite amount of places across GitLab, a table
would be a good fit for a component. On the other hand, a table cell used only
in one table would not be a good use of this pattern.
-You can read more about components in Vue.js site, [Component System][component-system]
+You can read more about components in Vue.js site, [Component System](https://vuejs.org/v2/guide/#Composing-with-Components).
### A folder for the Store
@@ -189,96 +189,135 @@ Each Vue component has a unique output. This output is always present in the ren
Although we can test each method of a Vue component individually, our goal must be to test the output
of the render/template function, which represents the state at all times.
-Make use of the [axios mock adapter](axios.md#mock-axios-response-in-tests) to mock data returned.
-
-Here's how we would test the Todo App above:
+Here's an example of a well structured unit test for [this Vue component](#appendix---vue-component-subject-under-test):
```javascript
-import Vue from 'vue';
-import axios from '~/lib/utils/axios_utils';
+import { shallowMount } from '@vue/test-utils';
+import { GlLoadingIcon } from '@gitlab/ui';
import MockAdapter from 'axios-mock-adapter';
+import axios from '~/lib/utils/axios_utils';
+import App from '~/todos/app.vue';
+
+const TEST_TODOS = [
+ { text: 'Lorem ipsum test text' },
+ { text: 'Lorem ipsum 2' },
+];
+const TEST_NEW_TODO = 'New todo title';
+const TEST_TODO_PATH = '/todos';
-describe('Todos App', () => {
- let vm;
+describe('~/todos/app.vue', () => {
+ let wrapper;
let mock;
beforeEach(() => {
- // Create a mock adapter for stubbing axios API requests
+ // IMPORTANT: Use axios-mock-adapter for stubbing axios API requests
mock = new MockAdapter(axios);
-
- const Component = Vue.extend(component);
-
- // Mount the Component
- vm = new Component().$mount();
+ mock.onGet(TEST_TODO_PATH).reply(200, TEST_TODOS);
+ mock.onPost(TEST_TODO_PATH).reply(200);
});
afterEach(() => {
- // Reset the mock adapter
- mock.restore();
- // Destroy the mounted component
- vm.$destroy();
- });
+ // IMPORTANT: Clean up the component instance and axios mock adapter
+ wrapper.destroy();
+ wrapper = null;
- it('should render the loading state while the request is being made', () => {
- expect(vm.$el.querySelector('i.fa-spin')).toBeDefined();
+ mock.restore();
});
- it('should render todos returned by the endpoint', done => {
- // Mock the get request on the API endpoint to return data
- mock.onGet('/todos').replyOnce(200, [
- {
- title: 'This is a todo',
- text: 'This is the text',
+ // NOTE: It is very helpful to separate setting up the component from
+ // its collaborators (i.e. Vuex, axios, etc.)
+ const createWrapper = (props = {}) => {
+ wrapper = shallowMount(App, {
+ propsData: {
+ path: TEST_TODO_PATH,
+ ...props,
},
- ]);
+ });
+ };
+ // NOTE: Helper methods greatly help test maintainability and readability.
+ const findLoader = () => wrapper.find(GlLoadingIcon);
+ const findAddButton = () => wrapper.find('[data-testid="add-button"]');
+ const findTextInput = () => wrapper.find('[data-testid="text-input"]');
+ const findTodoData = () => wrapper.findAll('[data-testid="todo-item"]').wrappers.map(wrapper => ({ text: wrapper.text() }));
+
+ describe('when mounted and loading', () => {
+ beforeEach(() => {
+ // Create request which will never resolve
+ mock.onGet(TEST_TODO_PATH).reply(() => new Promise(() => {}));
+ createWrapper();
+ });
- Vue.nextTick(() => {
- const items = vm.$el.querySelectorAll('.js-todo-list div')
- expect(items.length).toBe(1);
- expect(items[0].textContent).toContain('This is the text');
- done();
+ it('should render the loading state', () => {
+ expect(findLoader().exists()).toBe(true);
});
});
- it('should add a todos on button click', (done) => {
+ describe('when todos are loaded', () => {
+ beforeEach(() => {
+ createWrapper();
+ // IMPORTANT: This component fetches data asynchronously on mount, so let's wait for the Vue template to update
+ return wrapper.vm.$nextTick();
+ });
- // Mock the put request and check that the sent data object is correct
- mock.onPut('/todos').replyOnce((req) => {
- expect(req.data).toContain('text');
- expect(req.data).toContain('title');
+ it('should not show loading', () => {
+ expect(findLoader().exists()).toBe(false);
+ });
- return [201, {}];
+ it('should render todos', () => {
+ expect(findTodoData()).toEqual(TEST_TODOS);
});
- vm.$el.querySelector('.js-add-todo').click();
+ it('when todo is added, should post new todo', () => {
+ findTextInput().vm.$emit('update', TEST_NEW_TODO)
+ findAddButton().vm.$emit('click');
- // Add a new interceptor to mock the add Todo request
- Vue.nextTick(() => {
- expect(vm.$el.querySelectorAll('.js-todo-list div').length).toBe(2);
- done();
+ return wrapper.vm.$nextTick()
+ .then(() => {
+ expect(mock.history.post.map(x => JSON.parse(x.data))).toEqual([{ text: TEST_NEW_TODO }]);
+ });
});
});
});
```
-### `mountComponent` helper
+### Test the component's output
-There is a helper in `spec/javascripts/helpers/vue_mount_component_helper.js` that allows you to mount a component with the given props:
+The main return value of a Vue component is the rendered output. In order to test the component we
+need to test the rendered output. [Vue](https://vuejs.org/v2/guide/unit-testing.html) guide's to unit test show us exactly that:
+
+### Events
+
+We should test for events emitted in response to an action within our component, this is useful to verify the correct events are being fired with the correct arguments.
+
+For any DOM events we should use [`trigger`](https://vue-test-utils.vuejs.org/api/wrapper/#trigger) to fire out event.
```javascript
-import Vue from 'vue';
-import mountComponent from 'spec/helpers/vue_mount_component_helper'
-import component from 'component.vue'
+// Assuming SomeButton renders: <button>Some button</button>
+wrapper = mount(SomeButton);
-const Component = Vue.extend(component);
-const data = {prop: 'foo'};
-const vm = mountComponent(Component, data);
+...
+it('should fire the click event', () => {
+ const btn = wrapper.find('button')
+
+ btn.trigger('click');
+ ...
+})
```
-### Test the component's output
+When we need to fire a Vue event, we should use [`emit`](https://vuejs.org/v2/guide/components-custom-events.html) to fire our event.
-The main return value of a Vue component is the rendered output. In order to test the component we
-need to test the rendered output. [Vue][vue-test] guide's to unit test show us exactly that:
+```javascript
+wrapper = shallowMount(DropdownItem);
+
+...
+
+it('should fire the itemClicked event', () => {
+ DropdownItem.vm.$emit('itemClicked');
+ ...
+})
+```
+
+We should verify an event has been fired by asserting against the result of the [`emitted()`](https://vue-test-utils.vuejs.org/api/wrapper/#emitted) method
## Vue.js Expert Role
@@ -287,15 +326,50 @@ One should apply to be a Vue.js expert by opening an MR when the Merge Request's
- Deep understanding of Vue and Vuex reactivity
- Vue and Vuex code are structured according to both official and our guidelines
- Full understanding of testing a Vue and Vuex application
-- Vuex code follows the [documented pattern](vuex.md#actions-pattern-request-and-receive-namespaces)
+- Vuex code follows the [documented pattern](vuex.md#naming-pattern-request-and-receive-namespaces)
- Knowledge about the existing Vue and Vuex applications and existing reusable components
-[issue-boards]: https://gitlab.com/gitlab-org/gitlab-foss/tree/master/app/assets/javascripts/boards
-[environments-table]: https://gitlab.com/gitlab-org/gitlab-foss/tree/master/app/assets/javascripts/environments
-[page_specific_javascript]: ./performance.md#page-specific-javascript
-[component-system]: https://vuejs.org/v2/guide/#Composing-with-Components
-[state-management]: https://vuejs.org/v2/guide/state-management.html#Simple-State-Management-from-Scratch
-[one-way-data-flow]: https://vuejs.org/v2/guide/components.html#One-Way-Data-Flow
-[vue-test]: https://vuejs.org/v2/guide/unit-testing.html
-[flux]: https://facebook.github.io/flux/
-[axios]: https://github.com/axios/axios
+## Vue 2 -> Vue 3 Migration
+
+> This section is added temporarily to support the efforts to migrate the codebase from Vue 2.x to Vue 3.x
+
+Currently, we recommend to minimize adding certain features to the codebase to prevent increasing the tech debt for the eventual migration:
+
+- filters;
+- event buses;
+- functional templated
+- `slot` attributes
+
+You can find more details on [Migration to Vue 3](vue3_migration.md)
+
+## Appendix - Vue component subject under test
+
+This is the template for the example component which is tested in the [Testing Vue components](#testing-vue-components) section:
+
+```html
+<template>
+ <div class="content">
+ <gl-loading-icon v-if="isLoading" />
+ <template v-else>
+ <div
+ v-for="todo in todos"
+ :key="todo.id"
+ :class="{ 'gl-strike': todo.isDone }"
+ data-testid="todo-item"
+ >{{ toddo.text }}</div>
+ <footer class="gl-border-t-1 gl-mt-3 gl-pt-3">
+ <gl-form-input
+ type="text"
+ v-model="todoText"
+ data-testid="text-input"
+ >
+ <gl-button
+ variant="success"
+ data-testid="add-button"
+ @click="addTodo"
+ >Add</gl-button>
+ </footer>
+ </template>
+ </div>
+</template>
+```
diff --git a/doc/development/fe_guide/vue3_migration.md b/doc/development/fe_guide/vue3_migration.md
new file mode 100644
index 00000000000..7ab48db7f76
--- /dev/null
+++ b/doc/development/fe_guide/vue3_migration.md
@@ -0,0 +1,124 @@
+# Migration to Vue 3
+
+In order to prepare for the eventual migration to Vue 3.x, we should be wary about adding the following features to the codebase:
+
+## Vue filters
+
+**Why?**
+
+Filters [are removed](https://github.com/vuejs/rfcs/blob/master/active-rfcs/0015-remove-filters.md) from the Vue 3 API completely.
+
+**What to use instead**
+
+Component's computed properties / methods or external helpers.
+
+## Event hub
+
+**Why?**
+
+`$on`, `$once`, and `$off` methods [are removed](https://github.com/vuejs/rfcs/blob/master/active-rfcs/0020-events-api-change.md) from the Vue instance, so in Vue 3 it can't be used to create an event hub.
+
+**What to use instead**
+
+Vue docs recommend using [mitt](https://github.com/developit/mitt) library. It's relatively small (200 bytes gzipped) and has a simple API:
+
+```javascript
+import mitt from 'mitt'
+
+const emitter = mitt()
+
+// listen to an event
+emitter.on('foo', e => console.log('foo', e) )
+
+// listen to all events
+emitter.on('*', (type, e) => console.log(type, e) )
+
+// fire an event
+emitter.emit('foo', { a: 'b' })
+
+// working with handler references:
+function onFoo() {}
+
+emitter.on('foo', onFoo) // listen
+emitter.off('foo', onFoo) // unlisten
+```
+
+**Event hub factory**
+
+To make it easier for you to migrate existing event hubs to the new recommended approach, or simply
+to create new ones, we have created a factory that you can use to instantiate a new mitt-based
+event hub.
+
+```javascript
+import createEventHub from '~/helpers/event_hub_factory';
+
+export default createEventHub();
+```
+
+Event hubs created with the factory expose the same methods as Vue 2 event hubs (`$on`, `$once`, `$off` and
+`$emit`), making them backward compatible with our previous approach.
+
+## \<template functional>
+
+**Why?**
+
+In Vue 3, `{ functional: true }` option [is removed](https://github.com/vuejs/rfcs/blob/functional-async-api-change/active-rfcs/0007-functional-async-api-change.md) and `<template functional>` is no longer supported.
+
+**What to use instead**
+
+Functional components must be written as plain functions:
+
+```javascript
+import { h } from 'vue'
+
+const FunctionalComp = (props, slots) => {
+ return h('div', `Hello! ${props.name}`)
+}
+```
+
+## Old slots syntax with `slot` attribute
+
+**Why?**
+
+In Vue 2.6 `slot` attribute was already deprecated in favor of `v-slot` directive but its usage is still allowed and sometimes we prefer using them because it simplifies unit tests (with old syntax, slots are rendered on `shallowMount`). However, in Vue 3 we can't use old syntax anymore.
+
+**What to use instead**
+
+The syntax with `v-slot` directive. To fix rendering slots in `shallowMount`, we need to stub a child component with slots explicitly.
+
+```html
+<!-- MyAwesomeComponent.vue -->
+<script>
+import SomeChildComponent from './some_child_component.vue'
+
+export default {
+ components: {
+ SomeChildComponent
+ }
+}
+
+</script>
+
+<template>
+ <div>
+ <h1>Hello GitLab!</h1>
+ <some-child-component>
+ <template #header>
+ Header content
+ </template>
+ </some-child-component>
+ </div>
+</template>
+```
+
+```javascript
+// MyAwesomeComponent.spec.js
+
+import SomeChildComponent from '~/some_child_component.vue'
+
+shallowMount(MyAwesomeComponent, {
+ stubs: {
+ SomeChildComponent
+ }
+})
+```
diff --git a/doc/development/fe_guide/vuex.md b/doc/development/fe_guide/vuex.md
index 65661b0cc3b..e7be67b8da5 100644
--- a/doc/development/fe_guide/vuex.md
+++ b/doc/development/fe_guide/vuex.md
@@ -1,6 +1,6 @@
# Vuex
-When there's a clear benefit to separating state management from components (e.g. due to state complexity) we recommend using [Vuex][vuex-docs] over any other Flux pattern. Otherwise, feel free to manage state within the components.
+When there's a clear benefit to separating state management from components (e.g. due to state complexity) we recommend using [Vuex](https://vuex.vuejs.org) over any other Flux pattern. Otherwise, feel free to manage state within the components.
Vuex should be strongly considered when:
@@ -9,7 +9,7 @@ Vuex should be strongly considered when:
- There are complex interactions with Backend, e.g. multiple API calls
- The app involves interacting with backend via both traditional REST API and GraphQL (especially when moving the REST API over to GraphQL is a pending backend task)
-_Note:_ All of the below is explained in more detail in the official [Vuex documentation][vuex-docs].
+_Note:_ All of the below is explained in more detail in the official [Vuex documentation](https://vuex.vuejs.org).
## Separation of concerns
@@ -86,71 +86,35 @@ You can use `mapState` to access state properties in the components.
An action is a payload of information to send data from our application to our store.
-An action is usually composed by a `type` and a `payload` and they describe what happened.
-Enforcing that every change is described as an action lets us have a clear understanding of what is going on in the app.
+An action is usually composed by a `type` and a `payload` and they describe what happened. Unlike [mutations](#mutationsjs), actions can contain asynchronous operations - that's why we always need to handle asynchronous logic in actions.
-In this file, we will write the actions that will call the respective mutations:
+In this file, we will write the actions that will call mutations for handling a list of users:
```javascript
import * as types from './mutation_types';
import axios from '~/lib/utils/axios_utils';
import createFlash from '~/flash';
- export const requestUsers = ({ commit }) => commit(types.REQUEST_USERS);
- export const receiveUsersSuccess = ({ commit }, data) => commit(types.RECEIVE_USERS_SUCCESS, data);
- export const receiveUsersError = ({ commit }, error) => commit(types.RECEIVE_USERS_ERROR, error);
-
export const fetchUsers = ({ state, dispatch }) => {
- dispatch('requestUsers');
+ commit(types.REQUEST_USERS);
axios.get(state.endpoint)
- .then(({ data }) => dispatch('receiveUsersSuccess', data))
+ .then(({ data }) => commit(types.RECEIVE_USERS_SUCCESS, data))
.catch((error) => {
- dispatch('receiveUsersError', error)
+ commit(types.RECEIVE_USERS_ERROR, error)
createFlash('There was an error')
});
}
- export const requestAddUser = ({ commit }) => commit(types.REQUEST_ADD_USER);
- export const receiveAddUserSuccess = ({ commit }, data) => commit(types.RECEIVE_ADD_USER_SUCCESS, data);
- export const receiveAddUserError = ({ commit }, error) => commit(types.REQUEST_ADD_USER_ERROR, error);
-
export const addUser = ({ state, dispatch }, user) => {
- dispatch('requestAddUser');
+ commit(types.REQUEST_ADD_USER);
axios.post(state.endpoint, user)
- .then(({ data }) => dispatch('receiveAddUserSuccess', data))
- .catch((error) => dispatch('receiveAddUserError', error));
+ .then(({ data }) => commit(types.RECEIVE_ADD_USER_SUCCESS, data))
+ .catch((error) => commit(types.REQUEST_ADD_USER_ERROR, error));
}
```
-#### Actions Pattern: `request` and `receive` namespaces
-
-When a request is made we often want to show a loading state to the user.
-
-Instead of creating an action to toggle the loading state and dispatch it in the component,
-create:
-
-1. An action `requestSomething`, to toggle the loading state
-1. An action `receiveSomethingSuccess`, to handle the success callback
-1. An action `receiveSomethingError`, to handle the error callback
-1. An action `fetchSomething` to make the request.
- 1. In case your application does more than a `GET` request you can use these as examples:
- - `POST`: `createSomething`
- - `PUT`: `updateSomething`
- - `DELETE`: `deleteSomething`
-
-The component MUST only dispatch the `fetchNamespace` action. Actions namespaced with `request` or `receive` should not be called from the component
-The `fetch` action will be responsible to dispatch `requestNamespace`, `receiveNamespaceSuccess` and `receiveNamespaceError`
-
-By following this pattern we guarantee:
-
-1. All applications follow the same pattern, making it easier for anyone to maintain the code
-1. All data in the application follows the same lifecycle pattern
-1. Actions are contained and human friendly
-1. Unit tests are easier
-1. Actions are simple and straightforward
-
#### Dispatching actions
To dispatch an action from a component, use the `mapActions` helper:
@@ -181,6 +145,8 @@ Remember that actions only describe that something happened, they don't describe
**Never commit a mutation directly from a component**
+Instead, you should create an action that will commit a mutation.
+
```javascript
import * as types from './mutation_types';
@@ -210,6 +176,31 @@ Remember that actions only describe that something happened, they don't describe
};
```
+#### Naming Pattern: `REQUEST` and `RECEIVE` namespaces
+
+When a request is made we often want to show a loading state to the user.
+
+Instead of creating an mutation to toggle the loading state, we should:
+
+1. A mutation with type `REQUEST_SOMETHING`, to toggle the loading state
+1. A mutation with type `RECEIVE_SOMETHING_SUCCESS`, to handle the success callback
+1. A mutation with type `RECEIVE_SOMETHING_ERROR`, to handle the error callback
+1. An action `fetchSomething` to make the request and commit mutations on mentioned cases
+ 1. In case your application does more than a `GET` request you can use these as examples:
+ - `POST`: `createSomething`
+ - `PUT`: `updateSomething`
+ - `DELETE`: `deleteSomething`
+
+As a result, we can dispatch the `fetchNamespace` action from the component and it will be responsible to commit `REQUEST_NAMESPACE`, `RECEIVE_NAMESPACE_SUCCESS` and `RECEIVE_NAMESPACE_ERROR` mutations.
+
+> Previously, we were dispatching actions from the `fetchNamespace` action instead of committing mutation, so please don't be confused if you find a different pattern in the older parts of the codebase. However, we encourage leveraging a new pattern whenever you write new Vuex stores
+
+By following this pattern we guarantee:
+
+1. All applications follow the same pattern, making it easier for anyone to maintain the code
+1. All data in the application follows the same lifecycle pattern
+1. Unit tests are easier
+
### `getters.js`
Sometimes we may need to get derived state based on store state, like filtering for a specific prop.
@@ -477,8 +468,6 @@ To prevent this error from happening, you need to export an empty function as `d
export default () => {};
```
-[vuex-docs]: https://vuex.vuejs.org
-
### Two way data binding
When storing form data in Vuex, it is sometimes necessary to update the value stored. The store should never be mutated directly, and an action should be used instead.
diff --git a/doc/development/feature_flags/controls.md b/doc/development/feature_flags/controls.md
index aa32285b27b..309aecd7978 100644
--- a/doc/development/feature_flags/controls.md
+++ b/doc/development/feature_flags/controls.md
@@ -16,32 +16,6 @@ run:
/chatops run feature --help
```
-## Where to run commands
-
-To increase visibility, we recommend that GitLab team members run feature flag
-related Chatops commands within certain Slack channels based on the environment
-and related feature. For the [staging](https://staging.gitlab.com)
-and [development](https://dev.gitlab.org) environments of GitLab.com,
-the commands should run in a channel for the stage the feature is relevant too.
-
-For example, use the `#s_monitor` channel for features developed by the
-Monitor stage, Health group.
-
-For all production environment Chatops commands, use the `#production` channel.
-
-As per the template, where a feature would have a (potentially) significant user
-impact and the feature is being enabled instance wide prior to release, please copy
-the Slack message and repost in the `#support_gitlab-com` channel for added visibility
-and awareness, preferably with a link to the issue, MR, or docs.
-
-Regardless of the channel in which the Chatops command is ran, any feature flag change that affects GitLab.com will automatically be logged in an issue.
-
-The issue is created in the [gl-infra/feature-flag-log](https://gitlab.com/gitlab-com/gl-infra/feature-flag-log/issues?scope=all&utf8=%E2%9C%93&state=closed) project, and it will at minimum log the Slack handle of person enabling a feature flag, the time, and the name of the flag being changed.
-
-The issue is then also posted to GitLab Inc. internal [Grafana dashboard](https://dashboards.gitlab.net/) as an annotation marker to make the change even more visible.
-
-Changes to the issue format can be submitted in the [Chatops project](https://gitlab.com/gitlab-com/chatops).
-
## Rolling out changes
When the changes are deployed to the environments it is time to start
@@ -67,19 +41,11 @@ If you get an error "Whoops! This action is not allowed. This incident
will be reported." that means your Slack account is not allowed to
change feature flags or you do not [have access](#access).
-### Enabling feature for preproduction testing
+### Enabling a feature for preproduction testing
As a first step in a feature rollout, you should enable the feature on <https://staging.gitlab.com>
and <https://dev.gitlab.org>.
-For example, to enable a feature for 25% of all users, run the following in
-Slack:
-
-```shell
-/chatops run feature set new_navigation_bar 25 --dev
-/chatops run feature set new_navigation_bar 25 --staging
-```
-
These two environments have different scopes.
`dev.gitlab.org` is a production CE environment that has internal GitLab Inc.
traffic and is used for some development and other related work.
@@ -89,13 +55,65 @@ a (very) rough estimate of how your feature will look/behave on GitLab.com.
Both of these instances are connected to Sentry so make sure you check the projects
there for any exceptions while testing your feature after enabling the feature flag.
-Once you are confident enough that these environments are in a good state with your
-feature enabled, you can roll out the change to GitLab.com.
+For these preproduction environments, the commands should be run in a
+Slack channel for the stage the feature is relevant to. For example, use the
+`#s_monitor` channel for features developed by the Monitor stage, Health
+group.
+
+To enable a feature for 25% of all users, run the following in Slack:
+
+```shell
+/chatops run feature set new_navigation_bar 25 --dev
+/chatops run feature set new_navigation_bar 25 --staging
+```
### Enabling a feature for GitLab.com
-Similar to above, to enable a feature for 25% of all users, run the following in
-Slack:
+When a feature has successfully been
+[enabled on a preproduction](#enabling-a-feature-for-preproduction-testing)
+environment and verified as safe and working, you can roll out the
+change to GitLab.com (production).
+
+#### Communicate the change
+
+Some feature flag changes on GitLab.com should be communicated with
+parts of the company. The developer responsible needs to determine
+whether this is necessary and the appropriate level of communication.
+This depends on the feature and what sort of impact it might have.
+
+As a guideline:
+
+- For simple features that are low-risk, and easily rolled back, then
+ just proceed to [enabling the feature in `#production`](#process).
+- For features that will impact user experience consider notifying
+ `#support_gitlab-com` beforehand.
+- For features with significant downstream effects (e.g.: turning on/off
+ Elasticsearch indexing) consider coordinating with `#production`
+ beforehand.
+
+#### Process
+
+Before toggling any feature flag, check that there are no ongoing
+significant incidents on GitLab.com. You can do this by checking the
+`#production` and `#incident-management` Slack channels, or looking for
+[open incident issues](https://gitlab.com/gitlab-com/gl-infra/production/issues/?scope=all&utf8=%E2%9C%93&state=opened&label_name[]=incident)
+(although check the dates and times).
+
+We do not want to introduce changes during an incident, as it can make
+diagnosis and resolution of the incident much harder to achieve, and
+also will largely invalidate your rollout process as you will be unable
+to assess whether the rollout was without problems or not.
+
+If there is any doubt, ask in `#production`.
+
+The following `/chatops` commands should be performed in the Slack
+`#production` channel.
+
+When you begin to enable the feature, please link to the relevant
+Feature Flag Rollout Issue within a Slack thread of the first `/chatops`
+command you make so people can understand the change if they need to.
+
+To enable a feature for 25% of all users, run the following in Slack:
```shell
/chatops run feature set new_navigation_bar 25
@@ -150,6 +168,23 @@ NOTE: **Note:**
**Percentage of time** rollout is not a good idea if what you want is to make sure a feature
is always on or off to the users.
+### Feature flag change logging
+
+Any feature flag change that affects GitLab.com (production) will
+automatically be logged in an issue.
+
+The issue is created in the
+[gl-infra/feature-flag-log](https://gitlab.com/gitlab-com/gl-infra/feature-flag-log/issues?scope=all&utf8=%E2%9C%93&state=closed)
+project, and it will at minimum log the Slack handle of person enabling
+a feature flag, the time, and the name of the flag being changed.
+
+The issue is then also posted to GitLab's internal
+[Grafana dashboard](https://dashboards.gitlab.net/) as an annotation
+marker to make the change even more visible.
+
+Changes to the issue format can be submitted in the
+[Chatops project](https://gitlab.com/gitlab-com/chatops).
+
## Cleaning up
Once the change is deemed stable, submit a new merge request to remove the
@@ -157,7 +192,7 @@ feature flag. This ensures the change is available to all users and self-managed
instances. Make sure to add the ~"feature flag" label to this merge request so
release managers are aware the changes are hidden behind a feature flag. If the
merge request has to be picked into a stable branch, make sure to also add the
-appropriate "Pick into X" label (e.g. "Pick into XX.X").
+appropriate `~"Pick into X.Y"` label (e.g. `~"Pick into 13.0"`).
See [the process document](process.md#including-a-feature-behind-feature-flag-in-the-final-release) for further details.
When a feature gate has been removed from the code base, the feature
diff --git a/doc/development/feature_flags/index.md b/doc/development/feature_flags/index.md
index f5915f2c0a8..bd0bd8f2018 100644
--- a/doc/development/feature_flags/index.md
+++ b/doc/development/feature_flags/index.md
@@ -1,6 +1,7 @@
# Feature flags in development of GitLab
-Feature flags can be used to gradually roll out changes, be
+[Feature Flags](../../user/project/operations/feature_flags.md)
+can be used to gradually roll out changes, be
it a new feature, or a performance improvement. By using feature flags, we can
comfortably measure the impact of our changes, while still being able to easily
disable those changes, without having to revert an entire release.
@@ -10,6 +11,5 @@ Before using feature flags for GitLab's development, read through the following:
- [Process for using features flags](process.md).
- [Developing with feature flags](development.md).
- [Controlling feature flags](controls.md).
-
-When documenting feature flags, see [Feature flags](../documentation/styleguide.md#feature-flags)
-in the Documentation Style Guide.
+- [Documenting features deployed behind feature flags](../documentation/feature_flags.md).
+- [How GitLab administrators can enable and disable features behind flags](../../administration/feature_flags.md).
diff --git a/doc/development/feature_flags/process.md b/doc/development/feature_flags/process.md
index 0cca4117f1f..57360f5b771 100644
--- a/doc/development/feature_flags/process.md
+++ b/doc/development/feature_flags/process.md
@@ -63,6 +63,9 @@ from when the merge request is first reviewed to when the change is deployed to
GitLab.com. However, it is recommended to allow 10-14 days for this activity to
account for unforeseen problems.
+Feature flags must be [documented according to their state (enabled/disabled)](../documentation/feature_flags.md),
+and when the state changes, docs **must** be updated accordingly.
+
NOTE: **Note:**
Take into consideration that such action can make the feature available on
GitLab.com shortly after the change to the feature flag is merged.
diff --git a/doc/development/file_storage.md b/doc/development/file_storage.md
index 230288844d7..e8ae5a11d48 100644
--- a/doc/development/file_storage.md
+++ b/doc/development/file_storage.md
@@ -21,7 +21,7 @@ There are many places where file uploading is used, according to contexts:
- CI Artifacts (archive, metadata, trace)
- LFS Objects
- Merge request diffs
- - Design Management design thumbnails (EE)
+ - Design Management design thumbnails
## Disk storage
@@ -30,18 +30,18 @@ they are still not 100% standardized. You can see them below:
| Description | In DB? | Relative path (from CarrierWave.root) | Uploader class | model_type |
| ------------------------------------- | ------ | ----------------------------------------------------------- | ---------------------- | ---------- |
-| Instance logo | yes | uploads/-/system/appearance/logo/:id/:filename | `AttachmentUploader` | Appearance |
-| Header logo | yes | uploads/-/system/appearance/header_logo/:id/:filename | `AttachmentUploader` | Appearance |
-| Group avatars | yes | uploads/-/system/group/avatar/:id/:filename | `AvatarUploader` | Group |
-| User avatars | yes | uploads/-/system/user/avatar/:id/:filename | `AvatarUploader` | User |
-| User snippet attachments | yes | uploads/-/system/personal_snippet/:id/:random_hex/:filename | `PersonalFileUploader` | Snippet |
-| Project avatars | yes | uploads/-/system/project/avatar/:id/:filename | `AvatarUploader` | Project |
-| Issues/MR/Notes Markdown attachments | yes | uploads/:project_path_with_namespace/:random_hex/:filename | `FileUploader` | Project |
-| Issues/MR/Notes Legacy Markdown attachments | no | uploads/-/system/note/attachment/:id/:filename | `AttachmentUploader` | Note |
-| Design Management design thumbnails (EE) | yes | uploads/-/system/design_management/action/image_v432x230/:id/:filename | `DesignManagement::DesignV432x230Uploader` | DesignManagement::Action |
-| CI Artifacts (CE) | yes | `shared/artifacts/:disk_hash[0..1]/:disk_hash[2..3]/:disk_hash/:year_:month_:date/:job_id/:job_artifact_id` (:disk_hash is SHA256 digest of project_id) | `JobArtifactUploader` | Ci::JobArtifact |
-| LFS Objects (CE) | yes | shared/lfs-objects/:hex/:hex/:object_hash | `LfsObjectUploader` | LfsObject |
-| External merge request diffs | yes | shared/external-diffs/merge_request_diffs/mr-:parent_id/diff-:id | `ExternalDiffUploader` | MergeRequestDiff |
+| Instance logo | yes | `uploads/-/system/appearance/logo/:id/:filename` | `AttachmentUploader` | Appearance |
+| Header logo | yes | `uploads/-/system/appearance/header_logo/:id/:filename` | `AttachmentUploader` | Appearance |
+| Group avatars | yes | `uploads/-/system/group/avatar/:id/:filename` | `AvatarUploader` | Group |
+| User avatars | yes | `uploads/-/system/user/avatar/:id/:filename` | `AvatarUploader` | User |
+| User snippet attachments | yes | `uploads/-/system/personal_snippet/:id/:random_hex/:filename` | `PersonalFileUploader` | Snippet |
+| Project avatars | yes | `uploads/-/system/project/avatar/:id/:filename` | `AvatarUploader` | Project |
+| Issues/MR/Notes Markdown attachments | yes | `uploads/:project_path_with_namespace/:random_hex/:filename` | `FileUploader` | Project |
+| Issues/MR/Notes Legacy Markdown attachments | no | `uploads/-/system/note/attachment/:id/:filename` | `AttachmentUploader` | Note |
+| Design Management design thumbnails | yes | `uploads/-/system/design_management/action/image_v432x230/:id/:filename` | `DesignManagement::DesignV432x230Uploader` | DesignManagement::Action |
+| CI Artifacts (CE) | yes | `shared/artifacts/:disk_hash[0..1]/:disk_hash[2..3]/:disk_hash/:year_:month_:date/:job_id/:job_artifact_id` (`:disk_hash` is SHA256 digest of `project_id`) | `JobArtifactUploader` | Ci::JobArtifact |
+| LFS Objects (CE) | yes | `shared/lfs-objects/:hex/:hex/:object_hash` | `LfsObjectUploader` | LfsObject |
+| External merge request diffs | yes | `shared/external-diffs/merge_request_diffs/mr-:parent_id/diff-:id` | `ExternalDiffUploader` | MergeRequestDiff |
CI Artifacts and LFS Objects behave differently in CE and EE. In CE they inherit the `GitlabUploader`
while in EE they inherit the `ObjectStorage` and store files in and S3 API compatible object store.
diff --git a/doc/development/filtering_by_label.md b/doc/development/filtering_by_label.md
index 6f6d7afc040..19dece0d5c9 100644
--- a/doc/development/filtering_by_label.md
+++ b/doc/development/filtering_by_label.md
@@ -80,7 +80,7 @@ it did not improve query performance.
## Attempt B: Denormalize using an array column
Having [removed MySQL support in GitLab 12.1](https://about.gitlab.com/blog/2019/06/27/removing-mysql-support/),
-using [PostgreSQL's arrays](https://www.postgresql.org/docs/9.6/arrays.html) became more
+using [PostgreSQL's arrays](https://www.postgresql.org/docs/11/arrays.html) became more
tractable as we didn't have to support two databases. We discussed denormalizing
the `label_links` table for querying in
[issue #49651](https://gitlab.com/gitlab-org/gitlab-foss/issues/49651),
@@ -91,7 +91,7 @@ and `epics`: `issues.label_ids` would be an array column of label IDs, and
`issues.label_titles` would be an array of label titles.
These array columns can be complemented with [GIN
-indexes](https://www.postgresql.org/docs/9.6/gin-intro.html) to improve
+indexes](https://www.postgresql.org/docs/11/gin-intro.html) to improve
matching.
### Attempt B1: store label IDs for each object
diff --git a/doc/development/geo.md b/doc/development/geo.md
index b922fdfa119..bf56340f8ec 100644
--- a/doc/development/geo.md
+++ b/doc/development/geo.md
@@ -216,15 +216,11 @@ bundle exec rake geo:db:migrate
Foreign Data Wrapper ([FDW](#fdw)) is used by the [Geo Log Cursor](#geo-log-cursor) and improves
the performance of many synchronization operations.
-FDW is a PostgreSQL extension ([`postgres_fdw`](https://www.postgresql.org/docs/current/postgres-fdw.html)) that is enabled within
+FDW is a PostgreSQL extension ([`postgres_fdw`](https://www.postgresql.org/docs/11/postgres-fdw.html)) that is enabled within
the Geo Tracking Database (on a **secondary** node), which allows it
to connect to the readonly database replica and perform queries and filter
data from both instances.
-While FDW is available in older versions of PostgreSQL, we needed to
-raise the minimum required version to 9.6 as this includes many
-performance improvements to the FDW implementation.
-
This persistent connection is configured as an FDW server
named `gitlab_secondary`. This configuration exists within the database's user
context only. To access the `gitlab_secondary`, GitLab needs to use the
diff --git a/doc/development/geo/framework.md b/doc/development/geo/framework.md
index 026d3543955..d72f7cc4cc1 100644
--- a/doc/development/geo/framework.md
+++ b/doc/development/geo/framework.md
@@ -84,10 +84,8 @@ module Geo
model_record.file
end
- private
-
# Specify the model this replicator belongs to
- def model
+ def self.model
::Packages::PackageFile
end
end
@@ -163,47 +161,7 @@ state.
For example, to add support for files referenced by a `Widget` model with a
`widgets` table, you would perform the following steps:
-1. Add verification state fields to the `widgets` table so the Geo primary can
- track verification state:
-
- ```ruby
- # frozen_string_literal: true
-
- class AddVerificationStateToWidgets < ActiveRecord::Migration[6.0]
- DOWNTIME = false
-
- def change
- add_column :widgets, :verification_retry_at, :datetime_with_timezone
- add_column :widgets, :verified_at, :datetime_with_timezone
- add_column :widgets, :verification_checksum, :string
- add_column :widgets, :verification_failure, :string
- add_column :widgets, :verification_retry_count, :integer
- end
- end
- ```
-
-1. Add a partial index on `verification_failure` to ensure re-verification can
- be performed efficiently:
-
- ```ruby
- # frozen_string_literal: true
-
- class AddVerificationFailureIndexToWidgets < ActiveRecord::Migration[6.0]
- include Gitlab::Database::MigrationHelpers
-
- DOWNTIME = false
-
- disable_ddl_transaction!
-
- def up
- add_concurrent_index :widgets, :verification_failure, where: "(verification_failure IS NOT NULL)", name: "widgets_verification_failure_partial"
- end
-
- def down
- remove_concurrent_index :widgets, :verification_failure
- end
- end
- ```
+#### Replication
1. Include `Gitlab::Geo::ReplicableModel` in the `Widget` class, and specify
the Replicator class `with_replicator Geo::WidgetReplicator`.
@@ -270,7 +228,7 @@ For example, to add support for files referenced by a `Widget` model with a
```ruby
# frozen_string_literal: true
- class CreateWidgetRegistry < ActiveRecord::Migration[5.2]
+ class CreateWidgetRegistry < ActiveRecord::Migration[6.0]
DOWNTIME = false
def change
@@ -334,7 +292,7 @@ For example, to add support for files referenced by a `Widget` model with a
end
```
-1. Create `ee/spec/models/geo/widget_registry.rb`:
+1. Create `ee/spec/models/geo/widget_registry_spec.rb`:
```ruby
# frozen_string_literal: true
@@ -350,4 +308,206 @@ For example, to add support for files referenced by a `Widget` model with a
end
```
-Widget files should now be replicated and verified by Geo!
+Widgets should now be replicated by Geo!
+
+#### Verification
+
+1. Add verification state fields to the `widgets` table so the Geo primary can
+ track verification state:
+
+ ```ruby
+ # frozen_string_literal: true
+
+ class AddVerificationStateToWidgets < ActiveRecord::Migration[6.0]
+ DOWNTIME = false
+
+ def change
+ add_column :widgets, :verification_retry_at, :datetime_with_timezone
+ add_column :widgets, :verified_at, :datetime_with_timezone
+ add_column :widgets, :verification_checksum, :binary, using: 'verification_checksum::bytea'
+ add_column :widgets, :verification_failure, :string
+ add_column :widgets, :verification_retry_count, :integer
+ end
+ end
+ ```
+
+1. Add a partial index on `verification_failure` and `verification_checksum` to ensure
+ re-verification can be performed efficiently:
+
+ ```ruby
+ # frozen_string_literal: true
+
+ class AddVerificationFailureIndexToWidgets < ActiveRecord::Migration[6.0]
+ include Gitlab::Database::MigrationHelpers
+
+ DOWNTIME = false
+
+ disable_ddl_transaction!
+
+ def up
+ add_concurrent_index :widgets, :verification_failure, where: "(verification_failure IS NOT NULL)", name: "widgets_verification_failure_partial"
+ add_concurrent_index :widgets, :verification_checksum, where: "(verification_checksum IS NOT NULL)", name: "widgets_verification_checksum_partial"
+ end
+
+ def down
+ remove_concurrent_index :widgets, :verification_failure
+ remove_concurrent_index :widgets, :verification_checksum
+ end
+ end
+ ```
+
+1. Add fields `widget_count`, `widget_checksummed_count`, and `widget_checksum_failed_count`
+ to `GeoNodeStatus#RESOURCE_STATUS_FIELDS` array in `ee/app/models/geo_node_status.rb`.
+1. Add the same fields to `GeoNodeStatus#PROMETHEUS_METRICS` hash in
+ `ee/app/models/geo_node_status.rb`.
+1. Add the same fields to `Sidekiq metrics` table in
+ `doc/administration/monitoring/prometheus/gitlab_metrics.md`.
+1. Add the same fields to `GET /geo_nodes/status` example response in `doc/api/geo_nodes.md`.
+1. Modify `GeoNodeStatus#load_verification_data` to make sure the fields mantioned above
+ are set:
+
+ ```ruby
+ self.widget_count = Geo::WidgetReplicator.model.count
+ self.widget_checksummed_count = Geo::WidgetReplicator.checksummed.count
+ self.widget_checksum_failed_count = Geo::WidgetReplicator.checksum_failed.count
+ ```
+
+1. Make sure `Widget` model has `checksummed` and `checksum_failed` scopes.
+1. Update `ee/spec/fixtures/api/schemas/public_api/v4/geo_node_status.json` with new fields.
+1. Update `GeoNodeStatus#PROMETHEUS_METRICS` hash in `ee/app/models/geo_node_status.rb` with new fields.
+1. Update `Sidekiq metrics` table in `doc/administration/monitoring/prometheus/gitlab_metrics.md` with new fields.
+1. Update `GET /geo_nodes/status` example response in `doc/api/geo_nodes.md` with new fields.
+1. Update `ee/spec/models/geo_node_status_spec.rb` and `ee/spec/factories/geo_node_statuses.rb` with new fields.
+
+To do: Add verification on secondaries. This should be done as part of
+[Geo: Self Service Framework - First Implementation for Package File verification](https://gitlab.com/groups/gitlab-org/-/epics/1817)
+
+Widgets should now be verified by Geo!
+
+#### GraphQL API
+
+1. Add a new field to `GeoNodeType` in
+ `ee/app/graphql/types/geo/geo_node_type.rb`:
+
+ ```ruby
+ field :widget_registries, ::Types::Geo::WidgetRegistryType.connection_type,
+ null: true,
+ resolver: ::Resolvers::Geo::WidgetRegistriesResolver,
+ description: 'Find widget registries on this Geo node',
+ feature_flag: :geo_self_service_framework
+ ```
+
+1. Add the new `widget_registries` field name to the `expected_fields` array in
+ `ee/spec/graphql/types/geo/geo_node_type_spec.rb`.
+
+1. Create `ee/app/graphql/resolvers/geo/widget_registries_resolver.rb`:
+
+ ```ruby
+ # frozen_string_literal: true
+
+ module Resolvers
+ module Geo
+ class WidgetRegistriesResolver < BaseResolver
+ include RegistriesResolver
+ end
+ end
+ end
+ ```
+
+1. Create `ee/spec/graphql/resolvers/geo/widget_registries_resolver_spec.rb`:
+
+ ```ruby
+ # frozen_string_literal: true
+
+ require 'spec_helper'
+
+ describe Resolvers::Geo::WidgetRegistriesResolver do
+ it_behaves_like 'a Geo registries resolver', :widget_registry
+ end
+ ```
+
+1. Create `ee/app/finders/geo/widget_registry_finder.rb`:
+
+ ```ruby
+ # frozen_string_literal: true
+
+ module Geo
+ class WidgetRegistryFinder
+ include FrameworkRegistryFinder
+ end
+ end
+ ```
+
+1. Create `ee/spec/finders/geo/widget_registry_finder_spec.rb`:
+
+ ```ruby
+ # frozen_string_literal: true
+
+ require 'spec_helper'
+
+ describe Geo::WidgetRegistryFinder do
+ it_behaves_like 'a framework registry finder', :widget_registry
+ end
+ ```
+
+1. Create `ee/app/graphql/types/geo/package_file_registry_type.rb`:
+
+ ```ruby
+ # frozen_string_literal: true
+
+ module Types
+ module Geo
+ # rubocop:disable Graphql/AuthorizeTypes because it is included
+ class WidgetRegistryType < BaseObject
+ include ::Types::Geo::RegistryType
+
+ graphql_name 'WidgetRegistry'
+ description 'Represents the sync and verification state of a widget'
+
+ field :widget_id, GraphQL::ID_TYPE, null: false, description: 'ID of the Widget'
+ end
+ end
+ end
+ ```
+
+1. Create `ee/spec/graphql/types/geo/widget_registry_type_spec.rb`:
+
+ ```ruby
+ # frozen_string_literal: true
+
+ require 'spec_helper'
+
+ describe GitlabSchema.types['WidgetRegistry'] do
+ it_behaves_like 'a Geo registry type'
+
+ it 'has the expected fields (other than those included in RegistryType)' do
+ expected_fields = %i[widget_id]
+
+ expect(described_class).to have_graphql_fields(*expected_fields).at_least
+ end
+ end
+ ```
+
+1. Add integration tests for providing Widget registry data to the frontend via
+ the GraphQL API, by duplicating and modifying the following shared examples
+ in `ee/spec/requests/api/graphql/geo/registries_spec.rb`:
+
+ ```ruby
+ it_behaves_like 'gets registries for', {
+ field_name: 'widgetRegistries',
+ registry_class_name: 'WidgetRegistry',
+ registry_factory: :widget_registry,
+ registry_foreign_key_field_name: 'widgetId'
+ }
+ ```
+
+Individual widget synchronization and verification data should now be available
+via the GraphQL API!
+
+#### Admin UI
+
+To do: This should be done as part of
+[Geo: Implement frontend for Self-Service Framework replicables](https://gitlab.com/groups/gitlab-org/-/epics/2525)
+
+Widget sync and verification data (aggregate and individual) should now be
+available in the Admin UI!
diff --git a/doc/development/gitaly.md b/doc/development/gitaly.md
index 43e4d4d0e72..5e5cae7228b 100644
--- a/doc/development/gitaly.md
+++ b/doc/development/gitaly.md
@@ -5,7 +5,7 @@ Workhorse and GitLab-Shell.
## Deep Dive
-In May 2019, Bob Van Landuyt hosted a [Deep Dive](https://gitlab.com/gitlab-org/create-stage/issues/1)
+In May 2019, Bob Van Landuyt hosted a Deep Dive (GitLab team members only: `https://gitlab.com/gitlab-org/create-stage/issues/1`)
on GitLab's [Gitaly project](https://gitlab.com/gitlab-org/gitaly) and how to contribute to it as a
Ruby developer, to share his domain specific knowledge with anyone who may work in this part of the
code base in the future.
@@ -77,7 +77,7 @@ If your test-suite is failing with Gitaly issues, as a first step, try running:
rm -rf tmp/tests/gitaly
```
-During rspec tests, the Gitaly instance will write logs to `gitlab/log/gitaly-test.log`.
+During RSpec tests, the Gitaly instance will write logs to `gitlab/log/gitaly-test.log`.
## Legacy Rugged code
diff --git a/doc/development/go_guide/index.md b/doc/development/go_guide/index.md
index c1dfb220df8..fe69a4205f8 100644
--- a/doc/development/go_guide/index.md
+++ b/doc/development/go_guide/index.md
@@ -63,15 +63,20 @@ file and ask your manager to review and merge.
```yaml
projects:
gitlab: reviewer go
- gitlab-foss: reviewer go
```
## Code style and format
- Avoid global variables, even in packages. By doing so you will introduce side
effects if the package is included multiple times.
-- Use `go fmt` before committing ([Gofmt](https://golang.org/cmd/gofmt/) is a
- tool that automatically formats Go source code).
+- Use `goimports` before committing.
+ [goimports](https://godoc.org/golang.org/x/tools/cmd/goimports)
+ is a tool that automatically formats Go source code using
+ [Gofmt](https://golang.org/cmd/gofmt/), in addition to formatting import lines,
+ adding missing ones and removing unreferenced ones.
+
+ Most editors/IDEs will allow you to run commands before/after saving a file, you can set it
+ up to run `goimports` so that it's applied to every file when saving.
- Place private methods below the first caller method in the source file.
### Automatic linting
@@ -244,6 +249,59 @@ Programs handling a lot of IO or complex operations should always include
[benchmarks](https://golang.org/pkg/testing/#hdr-Benchmarks), to ensure
performance consistency over time.
+## Error handling
+
+### Adding context
+
+Adding context before you return the error can be helpful, instead of
+just returning the error. This allows developers to understand what the
+program was trying to do when it entered the error state making it much
+easier to debug.
+
+For example:
+
+```go
+// Wrap the error
+return nil, fmt.Errorf("get cache %s: %w", f.Name, err)
+
+// Just add context
+return nil, fmt.Errorf("saving cache %s: %v", f.Name, err)
+```
+
+A few things to keep in mind when adding context:
+
+- Decide if you want to expose the underlying error
+ to the caller. If so, use `%w`, if not, you can use `%v`.
+- Don't use words like `failed`, `error`, `didn't`. As it's an error,
+ the user already knows that something failed and this might lead to
+ having strings like `failed xx failed xx failed xx`. Explain _what_
+ failed instead.
+- Error strings should not be capitalized or end with punctuation or a
+ newline. You can use `golint` to check for this.
+
+### Naming
+
+- When using sentinel errors they should always be named like `ErrXxx`.
+- When creating a new error type they should always be named like
+ `XxxError`.
+
+### Checking Error types
+
+- To check error equality don't use `==`. Use
+ [`errors.Is`](https://pkg.go.dev/errors?tab=doc#Is) instead (for Go
+ versions >= 1.13).
+- To check if the error is of a certain type don't use type assertion,
+ use [`errors.As`](https://pkg.go.dev/errors?tab=doc#As) instead (for
+ Go versions >= 1.13).
+
+### References for working with errors
+
+- [Go 1.13 errors](https://blog.golang.org/go1.13-errors).
+- [Programing with
+ errors](https://peter.bourgon.org/blog/2019/09/11/programming-with-errors.html).
+- [Don’t just check errors, handle them
+ gracefully](https://dave.cheney.net/2016/04/27/dont-just-check-errors-handle-them-gracefully).
+
## CLIs
Every Go program is launched from the command line.
@@ -368,13 +426,13 @@ Once you've picked a new Go version to use, the steps to update Omnibus and CNG
are:
- [Create a merge request in the CNG project](https://gitlab.com/gitlab-org/build/CNG/edit/master/ci_files/variables.yml?branch_name=update-go-version),
- updating the `GO_VERSION` in `ci_files/variables.yml`.
+ updating the `GO_VERSION` in `ci_files/variables.yml`.
- Create a merge request in the [`gitlab-omnibus-builder` project](https://gitlab.com/gitlab-org/gitlab-omnibus-builder),
- updating every file in the `docker/` directory so the `GO_VERSION` is set
- appropriately. [Here's an example](https://gitlab.com/gitlab-org/gitlab-omnibus-builder/-/merge_requests/125/diffs).
+ updating every file in the `docker/` directory so the `GO_VERSION` is set
+ appropriately. [Here's an example](https://gitlab.com/gitlab-org/gitlab-omnibus-builder/-/merge_requests/125/diffs).
- Tag a new release of `gitlab-omnibus-builder` containing the change.
-- [Create a merge request in the `gitlab-omnibus` project](https://gitlab.com/gitlab-org/omnibus-gitlab/edit/master/.gitlab-ci.yml?branch_name=update-gitlab-omnibus-builder-version),
- updating the `BUILDER_IMAGE_REVISION` to match the newly-created tag.
+- [Create a merge request in the `omnibus-gitlab` project](https://gitlab.com/gitlab-org/omnibus-gitlab/edit/master/.gitlab-ci.yml?branch_name=update-gitlab-omnibus-builder-version),
+ updating the `BUILDER_IMAGE_REVISION` to match the newly-created tag.
To reduce unnecessary differences between two distribution methods, Omnibus and
CNG **should always use the same Go version**.
diff --git a/doc/development/hash_indexes.md b/doc/development/hash_indexes.md
index 417ea18e22f..bc962ac0cd6 100644
--- a/doc/development/hash_indexes.md
+++ b/doc/development/hash_indexes.md
@@ -14,7 +14,7 @@ documentation:
> answers to queries that subsequently use them. For these reasons, hash index
> use is presently discouraged.
-RuboCop is configured to register an offence when it detects the use of a hash
+RuboCop is configured to register an offense when it detects the use of a hash
index.
Instead of using hash indexes you should use regular btree indexes.
diff --git a/doc/development/i18n/externalization.md b/doc/development/i18n/externalization.md
index 7ddcd426fd7..a81e656fc27 100644
--- a/doc/development/i18n/externalization.md
+++ b/doc/development/i18n/externalization.md
@@ -20,7 +20,7 @@ The following tools are used:
1. [`gettext_i18n_rails`](https://github.com/grosser/gettext_i18n_rails): this
gem allow us to translate content from models, views and controllers. Also
- it gives us access to the following raketasks:
+ it gives us access to the following Rake tasks:
- `rake gettext:find`: Parses almost all the files from the
Rails application looking for content that has been marked for
translation. Finally, it updates the PO files with the new content that
@@ -30,7 +30,7 @@ The following tools are used:
1. [`gettext_i18n_rails_js`](https://github.com/webhippie/gettext_i18n_rails_js):
this gem is useful to make the translations available in JavaScript. It
- provides the following raketask:
+ provides the following Rake task:
- `rake gettext:po_to_json`: Reads the contents from the PO files and
generates JSON files containing all the available translations.
@@ -131,7 +131,7 @@ You can mark that content for translation with:
In JavaScript we added the `__()` (double underscore parenthesis) function that
you can import from the `~/locale` file. For instance:
-```js
+```javascript
import { __ } from '~/locale';
const label = __('Subscribe');
```
@@ -167,7 +167,7 @@ For example use `%{created_at}` in Ruby but `%{createdAt}` in JavaScript. Make s
- In JavaScript (when Vue cannot be used):
- ```js
+ ```javascript
import { __, sprintf } from '~/locale';
sprintf(__('Hello %{username}'), { username: 'Joe' }); // => 'Hello Joe'
@@ -180,7 +180,7 @@ For example use `%{created_at}` in Ruby but `%{createdAt}` in JavaScript. Make s
escape any interpolated dynamic values yourself, for instance using
`escape` from `lodash`.
- ```js
+ ```javascript
import { escape } from 'lodash';
import { __, sprintf } from '~/locale';
@@ -220,14 +220,14 @@ For example use `%{created_at}` in Ruby but `%{createdAt}` in JavaScript. Make s
- In JavaScript:
- ```js
+ ```javascript
n__('Apple', 'Apples', 3)
// => 'Apples'
```
Using interpolation:
- ```js
+ ```javascript
n__('Last day', 'Last %d days', x)
// => When x == 1: 'Last day'
// => When x == 2: 'Last 2 days'
@@ -274,7 +274,7 @@ Namespaces should be PascalCase.
- In JavaScript:
- ```js
+ ```javascript
s__('OpenedNDaysAgo|Opened')
```
@@ -285,7 +285,7 @@ guidelines for more details](translation.md#namespaced-strings).
- In JavaScript:
-```js
+```javascript
import { createDateTimeFormat } from '~/locale';
const dateFormat = createDateTimeFormat({ year: 'numeric', month: 'long', day: 'numeric' });
@@ -372,7 +372,7 @@ structure is the same in all languages.
For instance, the following:
-```js
+```javascript
{{ s__("mrWidget|Set by") }}
{{ author.name }}
{{ s__("mrWidget|to be merged automatically when the pipeline succeeds") }}
@@ -380,7 +380,7 @@ For instance, the following:
should be externalized as follows:
-```js
+```javascript
{{ sprintf(s__("mrWidget|Set by %{author} to be merged automatically when the pipeline succeeds"), { author: author.name }) }}
```
@@ -439,7 +439,7 @@ This also applies when using links in between translated sentences, otherwise th
- In JavaScript (when Vue cannot be used), instead of:
- ```js
+ ```javascript
{{
sprintf(s__("ClusterIntegration|Learn more about %{link}"), {
link: '<a href="https://cloud.google.com/compute/docs/regions-zones/regions-zones" target="_blank" rel="noopener noreferrer">zones</a>'
@@ -449,7 +449,7 @@ This also applies when using links in between translated sentences, otherwise th
Set the link starting and ending HTML fragments as placeholders like so:
- ```js
+ ```javascript
{{
sprintf(s__("ClusterIntegration|Learn more about %{linkStart}zones%{linkEnd}"), {
linkStart: '<a href="https://cloud.google.com/compute/docs/regions-zones/regions-zones" target="_blank" rel="noopener noreferrer">',
@@ -536,9 +536,9 @@ The linter will take the following into account:
- Variable usage
- Only one unnamed (`%d`) variable, since the order of variables might change
in different languages
- - All variables used in the message-id are used in the translation
+ - All variables used in the message ID are used in the translation
- There should be no variables used in a translation that aren't in the
- message-id
+ message ID
- Errors during translation.
The errors are grouped per file, and per message ID:
@@ -562,7 +562,7 @@ Errors in `locale/zh_TW/gitlab.po`:
In this output the `locale/zh_HK/gitlab.po` has syntax errors.
The `locale/zh_TW/gitlab.po` has variables that are used in the translation that
-aren't in the message with id `1 pipeline`.
+aren't in the message with ID `1 pipeline`.
## Adding a new language
diff --git a/doc/development/i18n/proofreader.md b/doc/development/i18n/proofreader.md
index 1170103490b..837da349f7e 100644
--- a/doc/development/i18n/proofreader.md
+++ b/doc/development/i18n/proofreader.md
@@ -87,7 +87,7 @@ are very appreciative of the work done by translators and proofreaders!
- Mark Minakou - [GitLab](https://gitlab.com/sandzhaj), [Crowdin](https://crowdin.com/profile/sandzhaj)
- NickVolynkin - [Crowdin](https://crowdin.com/profile/NickVolynkin)
- Andrey Komarov - [GitLab](https://gitlab.com/elkamarado), [Crowdin](https://crowdin.com/profile/kamarado)
- - Iaroslav Postovalov - [GitLab](https://gitlab/CMDR_Tvis), [Crowdin](https://crowdin.com/profile/CMDR_Tvis)
+ - Iaroslav Postovalov - [GitLab](https://gitlab.com/CMDR_Tvis), [Crowdin](https://crowdin.com/profile/CMDR_Tvis)
- Serbian (Cyrillic)
- Proofreaders needed.
- Serbian (Latin)
diff --git a/doc/development/img/snowplow_flow.png b/doc/development/img/snowplow_flow.png
new file mode 100644
index 00000000000..5996cf01537
--- /dev/null
+++ b/doc/development/img/snowplow_flow.png
Binary files differ
diff --git a/doc/development/img/telemetry_system_overview.png b/doc/development/img/telemetry_system_overview.png
new file mode 100644
index 00000000000..1667039e8cd
--- /dev/null
+++ b/doc/development/img/telemetry_system_overview.png
Binary files differ
diff --git a/doc/development/import_export.md b/doc/development/import_export.md
index 5a7d176d1d3..6d1b6929667 100644
--- a/doc/development/import_export.md
+++ b/doc/development/import_export.md
@@ -331,3 +331,78 @@ module Projects
wiki_repo_saver, lfs_saver].all?(&:save)
end
```
+
+## Test fixtures
+
+Fixtures used in Import/Export specs live in `spec/fixtures/lib/gitlab/import_export`. There are both Project and Group fixtures.
+
+There are two versions of each of these fixtures:
+
+- A human readable single JSON file with all objects, called either `project.json` or `group.json`.
+- A folder named `tree`, containing a tree of files in `ndjson` format. **Please do not edit files under this folder manually unless strictly necessary.**
+
+The tools to generate the NDJSON tree from the human-readable JSON files live in the [`gitlab-org/memory-team/team-tools`](https://gitlab.com/gitlab-org/memory-team/team-tools/-/blob/master/import-export/) project.
+
+### Project
+
+**Please use `legacy-project-json-to-ndjson.sh` to generate the NDJSON tree.**
+
+The NDJSON tree will look like this:
+
+```shell
+tree
+├── project
+│   ├── auto_devops.ndjson
+│   ├── boards.ndjson
+│   ├── ci_cd_settings.ndjson
+│   ├── ci_pipelines.ndjson
+│   ├── container_expiration_policy.ndjson
+│   ├── custom_attributes.ndjson
+│   ├── error_tracking_setting.ndjson
+│   ├── external_pull_requests.ndjson
+│   ├── issues.ndjson
+│   ├── labels.ndjson
+│   ├── merge_requests.ndjson
+│   ├── milestones.ndjson
+│   ├── pipeline_schedules.ndjson
+│   ├── project_badges.ndjson
+│   ├── project_feature.ndjson
+│   ├── project_members.ndjson
+│   ├── protected_branches.ndjson
+│   ├── protected_tags.ndjson
+│   ├── releases.ndjson
+│   ├── services.ndjson
+│   ├── snippets.ndjson
+│   └── triggers.ndjson
+└── project.json
+```
+
+### Group
+
+**Please use `legacy-group-json-to-ndjson.rb` to generate the NDJSON tree.**
+
+The NDJSON tree will look like this:
+
+```shell
+tree
+└── groups
+ ├── 4351
+ │   ├── badges.ndjson
+ │   ├── boards.ndjson
+ │   ├── epics.ndjson
+ │   ├── labels.ndjson
+ │   ├── members.ndjson
+ │   └── milestones.ndjson
+ ├── 4352
+ │   ├── badges.ndjson
+ │   ├── boards.ndjson
+ │   ├── epics.ndjson
+ │   ├── labels.ndjson
+ │   ├── members.ndjson
+ │   └── milestones.ndjson
+ ├── _all.ndjson
+ ├── 4351.json
+ └── 4352.json
+```
+
+CAUTION: **Caution:** When updating these fixtures, please ensure you update both `json` files and `tree` folder, as the tests apply to both.
diff --git a/doc/development/import_project.md b/doc/development/import_project.md
index 78efc6ce2ab..f222a6533e8 100644
--- a/doc/development/import_project.md
+++ b/doc/development/import_project.md
@@ -61,10 +61,9 @@ Parameters:
| `namespace_path` | string | yes | Namespace path |
| `project_path` | string | yes | Project name |
| `archive_path` | string | yes | Path to the exported project tarball you want to import |
-| `measurement_enabled` | boolean | no | Measure execution time, number of SQL calls and GC count |
```shell
-bundle exec rake "gitlab:import_export:import[root, root, testingprojectimport, /path/to/file.tar.gz, true]"
+bundle exec rake "gitlab:import_export:import[root, root, testingprojectimport, /path/to/file.tar.gz]"
```
### Importing via the Rails console
diff --git a/doc/development/instrumentation.md b/doc/development/instrumentation.md
index fd01d0ea405..d72e1c6635e 100644
--- a/doc/development/instrumentation.md
+++ b/doc/development/instrumentation.md
@@ -5,7 +5,7 @@ blocks of Ruby code. Method instrumentation is the primary form of
instrumentation with block-based instrumentation only being used when we want to
drill down to specific regions of code within a method.
-Please refer to [Telemetry](../telemetry/index.md) if you are tracking product usage patterns.
+Please refer to [Telemetry](telemetry/index.md) if you are tracking product usage patterns.
## Instrumenting Methods
diff --git a/doc/development/integrations/example_vuln.png b/doc/development/integrations/example_vuln.png
new file mode 100644
index 00000000000..f7a3c8b38f2
--- /dev/null
+++ b/doc/development/integrations/example_vuln.png
Binary files differ
diff --git a/doc/development/integrations/secure.md b/doc/development/integrations/secure.md
index b38e45778fb..b0e1e28ba8b 100644
--- a/doc/development/integrations/secure.md
+++ b/doc/development/integrations/secure.md
@@ -2,14 +2,23 @@
Integrating a security scanner into GitLab consists of providing end users
with a [CI job definition](../../ci/yaml/README.md#introduction)
-they can add to their CI configuration files, to scan their GitLab projects.
+they can add to their CI configuration files to scan their GitLab projects.
+This CI job should then output its results in a GitLab-specified format. These results are then
+automatically presented in various places in GitLab, such as the Pipeline view, Merge Request
+widget, and Security Dashboard.
+
The scanning job is usually based on a [Docker image](https://docs.docker.com/)
that contains the scanner and all its dependencies in a self-contained environment.
-This page documents requirements and guidelines for writing CI jobs implementing a security scanner,
-as well as requirements and guidelines for the Docker image itself.
+
+This page documents requirements and guidelines for writing CI jobs that implement a security
+scanner, as well as requirements and guidelines for the Docker image.
## Job definition
+This section desribes several important fields to add to the security scanner's job
+definition file. Full documentation on these and other available fields can be viewed
+in the [CI documentation](../../ci/yaml/README.md#image).
+
### Name
For consistency, scanning jobs should be named after the scanner, in lower case.
@@ -26,8 +35,8 @@ containing the security scanner.
### Script
The [`script`](../../ci/yaml/README.md#script) keyword
-is used to specify the command that the job runs.
-Because the `script` cannot be left empty, it must be set to the command that performs the scan.
+is used to specify the commands to run the scanner.
+Because the `script` entry can't be left empty, it must be set to the command that performs the scan.
It is not possible to rely on the predefined `ENTRYPOINT` and `CMD` of the Docker image
to perform the scan automatically, without passing any command.
@@ -53,44 +62,41 @@ so the [`allow_failure`](../../ci/yaml/README.md#allow_failure) parameter should
### Artifacts
Scanning jobs must declare a report that corresponds to the type of scanning they perform,
-using the [`artifacts:reports`](../../ci/yaml/README.md#artifactsreports) keyword.
+using the [`artifacts:reports`](../../ci/pipelines/job_artifacts.md#artifactsreports) keyword.
Valid reports are: `dependency_scanning`, `container_scanning`, `dast`, and `sast`.
For example, here is the definition of a SAST job that generates a file named `gl-sast-report.json`,
and uploads it as a SAST report:
```yaml
-mysec_dependency_scanning:
+mysec_sast_scanning:
image: registry.gitlab.com/secure/mysec
artifacts:
reports:
sast: gl-sast-report.json
```
-`gl-sast-report.json` is an example file path. See [the Output file section](#output-file) for more details.
-It is processed as a SAST report because it is declared as such in the job definition.
+Note that `gl-sast-report.json` is an example file path but any other file name can be used. See
+[the Output file section](#output-file) for more details. It's processed as a SAST report because
+it's declared under the `reports:sast` key in the job definition, not because of the file name.
### Policies
-Scanning jobs should be skipped unless the corresponding feature is listed
-in the `GITLAB_FEATURES` variable (comma-separated list of values).
-So Dependency Scanning, Container Scanning, SAST, and DAST should be skipped
-unless `GITLAB_FEATURES` contains `dependency_scanning`, `container_scanning`, `sast`, and `dast`, respectively.
-See [GitLab CI/CD predefined variables](../../ci/variables/predefined_variables.md).
-
-Also, scanning jobs should be skipped when the corresponding variable prefixed with `_DISABLED` is present.
-See `DEPENDENCY_SCANNING_DISABLED`, `CONTAINER_SCANNING_DISABLED`, `SAST_DISABLED`, and `DAST_DISABLED`
-in [Auto DevOps documentation](../../topics/autodevops/customize.md#disable-jobs).
+Certain GitLab workflows, such as [AutoDevOps](../../topics/autodevops/customize.md#disable-jobs),
+define variables to indicate that given scans should be disabled. You can check for this by looking
+for variables such as `DEPENDENCY_SCANNING_DISABLED`, `CONTAINER_SCANNING_DISABLED`,
+`SAST_DISABLED`, and `DAST_DISABLED`. If appropriate based on the scanner type, you should then
+disable running the custom scanner.
-Finally, SAST and Dependency Scanning job definitions should use
-`CI_PROJECT_REPOSITORY_LANGUAGES` (comma-separated list of values)
-in order to skip the job when the language or technology is not supported.
+GitLab also defines a `CI_PROJECT_REPOSITORY_LANGUAGES` variable, which provides the list of
+languages in the repo. Depending on this value, your scanner may or may not do something different.
Language detection currently relies on the [`linguist`](https://github.com/github/linguist) Ruby gem.
See [GitLab CI/CD prefined variables](../../ci/variables/predefined_variables.md#variables-reference).
-For instance, here is how to skip the Dependency Scanning job `mysec_dependency_scanning`
-unless the project repository contains Java source code,
-and the `dependency_scanning` feature is enabled:
+#### Policy checking example
+
+This example shows how to skip a custom Dependency Scanning job, `mysec_dependency_scanning`, unless
+the project repository contains Java source code and the `dependency_scanning` feature is enabled:
```yaml
mysec_dependency_scanning:
@@ -111,6 +117,8 @@ for a particular branch or when a particular set of files changes.
The Docker image is a self-contained environment that combines
the scanner with all the libraries and tools it depends on.
+Packaging your scanner into a Docker image makes its dependencies and configuration always present,
+regardless of the individual machine the scanner runs on.
### Image size
@@ -144,7 +152,7 @@ It also generates text output on the standard output and standard error streams,
All CI variables are passed to the scanner as environment variables.
The scanned project is described by the [predefined CI variables](../../ci/variables/README.md).
-#### SAST, Dependency Scanning
+#### SAST and Dependency Scanning
SAST and Dependency Scanning scanners must scan the files in the project directory, given by the `CI_PROJECT_DIR` variable.
@@ -178,7 +186,7 @@ It is recommended to name the output file after the type of scanning, and to use
Since all Secure reports are JSON files, it is recommended to use `.json` as a file extension.
For instance, a suggested file name for a Dependency Scanning report is `gl-dependency-scanning.json`.
-The [`artifacts:reports`](../../ci/yaml/README.md#artifactsreports) keyword
+The [`artifacts:reports`](../../ci/pipelines/job_artifacts.md#artifactsreports) keyword
of the job definition must be consistent with the file path where the Security report is written.
For instance, if a Dependency Scanning analyzer writes its report to the CI project directory,
and if this report file name is `depscan.json`,
@@ -223,11 +231,8 @@ The DAST variant of the report JSON format is not documented at the moment.
### Version
-The documentation of
-[SAST](../../user/application_security/sast/index.md#reports-json-format),
-[Dependency Scanning](../../user/application_security/dependency_scanning/index.md#reports-json-format),
-and [Container Scanning](../../user/application_security/container_scanning/index.md#reports-json-format)
-describes the Secure report format version.
+This field specifies the version of the report schema you are using. Please reference individual scanner
+pages for the specific versions to use.
### Vulnerabilities
@@ -251,12 +256,17 @@ The `id` should not collide with any other scanner another integrator would prov
#### Name, message, and description
-The `name` and `message` fields contain a short description of the vulnerability,
-whereas the `description` field provides more details.
+The `name` and `message` fields contain a short description of the vulnerability.
+The `description` field provides more details.
-The `name` is context-free and contains no information on where the vulnerability has been found,
+The `name` field is context-free and contains no information on where the vulnerability has been found,
whereas the `message` may repeat the location.
+As a visual example, this screenshot highlights where these fields are used when viewing a
+vulnerability as part of a pipeline view.
+
+![Example Vulnerability](example_vuln.png)
+
For instance, a `message` for a vulnerability
reported by Dependency Scanning gives information on the vulnerable dependency,
which is redundant with the `location` field of the vulnerability.
@@ -288,21 +298,17 @@ It should not repeat the other fields of the vulnerability object.
In particular, the `description` should not repeat the `location` (what is affected)
or the `solution` (how to mitigate the risk).
-There is a proposal to remove either the `name` or the `message`, to remove ambiguities.
-See [issue #36779](https://gitlab.com/gitlab-org/gitlab/issues/36779).
-
#### Solution
-The `solution` field may contain instructions users should follow to fix the vulnerability or to mitigate the risk.
-It is intended for users whereas the `remediations` objects are processed automatically by GitLab.
+You can use the `solution` field to instruct users how to fix the identified vulnerability or to mitigate
+the risk. End-users interact with this field, whereas GitLab automatically processes the
+`remediations` objects.
#### Identifiers
-The `identifiers` array describes the vulnerability flaw that has been detected.
-An identifier object has a `type` and a `value`;
-these technical fields are used to tell if two identifiers are the same.
-It also has a `name` and a `url`;
-these fields are used to display the identifier in the user interface.
+The `identifiers` array describes the detected vulnerability. An identifier object's `type` and
+`value` fields are used to tell if two identifiers are the same. The user interface uses the
+object's `name` and `url` fields to display the identifier.
It is recommended to reuse the identifiers the GitLab scanners already define:
@@ -316,18 +322,15 @@ It is recommended to reuse the identifiers the GitLab scanners already define:
| [RHSA](https://access.redhat.com/errata/#/) | `rhsa` | RHSA-2020:0111 |
| [ELSA](https://linux.oracle.com/security/) | `elsa` | ELSA-2020-0085 |
-The generic identifiers listed above are defined in the [common library](https://gitlab.com/gitlab-org/security-products/analyzers/common);
-this library is shared by the analyzers maintained by GitLab,
-and this is where you can [contribute](https://gitlab.com/gitlab-org/security-products/analyzers/common/blob/master/issue/identifier.go) new generic identifiers.
-Analyzers may also produce vendor-specific or product-specific identifiers;
-these do not belong to the [common library](https://gitlab.com/gitlab-org/security-products/analyzers/common).
+The generic identifiers listed above are defined in the [common library](https://gitlab.com/gitlab-org/security-products/analyzers/common),
+which is shared by the analyzers that GitLab maintains. You can [contribute](https://gitlab.com/gitlab-org/security-products/analyzers/common/blob/master/issue/identifier.go)
+new generic identifiers to if needed. Analyzers may also produce vendor-specific or product-specific
+identifiers, which don't belong in the [common library](https://gitlab.com/gitlab-org/security-products/analyzers/common).
The first item of the `identifiers` array is called the primary identifier.
The primary identifier is particularly important, because it is used to
-[track vulnerabilities](#tracking-merging-vulnerabilities)
-as new commits are pushed to the repository.
-
-Identifiers are used to [merge duplicate vulnerabilities](#tracking-merging-vulnerabilities)
+[track vulnerabilities](#tracking-and-merging-vulnerabilities) as new commits are pushed to the repository.
+Identifiers are also used to [merge duplicate vulnerabilities](#tracking-and-merging-vulnerabilities)
reported for the same commit, except for `CWE` and `WASC`.
### Location
@@ -336,7 +339,7 @@ The `location` indicates where the vulnerability has been detected.
The format of the location depends on the type of scanning.
Internally GitLab extracts some attributes of the `location` to generate the **location fingerprint**,
-which is used to [track vulnerabilities](#tracking-merging-vulnerabilities)
+which is used to track vulnerabilities
as new commits are pushed to the repository.
The attributes used to generate the location fingerprint also depend on the type of scanning.
@@ -426,12 +429,12 @@ combines `file`, `start_line`, and `end_line`,
so these attributes are mandatory.
All other attributes are optional.
-### Tracking, merging vulnerabilities
+### Tracking and merging vulnerabilities
Users may give feedback on a vulnerability:
-- they may dismiss a vulnerability if it does not apply to their projects
-- or they may create an issue for a vulnerability, if there is a possible threat
+- They may dismiss a vulnerability if it doesn't apply to their projects
+- They may create an issue for a vulnerability if there's a possible threat
GitLab tracks vulnerabilities so that user feedback is not lost
when new Git commits are pushed to the repository.
@@ -470,18 +473,57 @@ Valid values are: `Ignore`, `Unknown`, `Experimental`, `Low`, `Medium`, `High`,
### Remediations
The `remediations` field of the report is an array of remediation objects.
-Each remediation describes a patch that can be applied to automatically fix
+Each remediation describes a patch that can be applied to
+[automatically fix](../../user/application_security/#solutions-for-vulnerabilities-auto-remediation)
a set of vulnerabilities.
+Here is an example of a report that contains remediations.
+
+```json
+{
+ "vulnerabilities": [
+ {
+ "category": "dependency_scanning",
+ "name": "Regular Expression Denial of Service",
+ "id": "123e4567-e89b-12d3-a456-426655440000",
+ "solution": "Upgrade to new versions.",
+ "scanner": {
+ "id": "gemnasium",
+ "name": "Gemnasium"
+ },
+ "identifiers": [
+ {
+ "type": "gemnasium",
+ "name": "Gemnasium-642735a5-1425-428d-8d4e-3c854885a3c9",
+ "value": "642735a5-1425-428d-8d4e-3c854885a3c9"
+ }
+ ]
+ }
+ ],
+ "remediations": [
+ {
+ "fixes": [
+ {
+ "id": "123e4567-e89b-12d3-a456-426655440000"
+ }
+ ],
+ "summary": "Upgrade to new version",
+ "diff": "ZGlmZiAtLWdpdCBhL3lhcm4ubG9jayBiL3lhcm4ubG9jawppbmRleCAwZWNjOTJmLi43ZmE0NTU0IDEwMDY0NAotLS0gYS95Y=="
+ }
+ ]
+}
+```
+
#### Summary
-The `summary` field is an overview of how the vulnerabilities can be fixed.
+The `summary` field is an overview of how the vulnerabilities can be fixed. This field is required.
#### Fixed vulnerabilities
The `fixes` field is an array of objects that reference the vulnerabilities fixed by the
-remediation. `fixes[].id` contains a fixed vulnerability's unique identifier.
+remediation. `fixes[].id` contains a fixed vulnerability's [unique identifier](#id). This field is required.
#### Diff
-The `diff` field is a base64-encoded remediation code diff, compatible with [`git apply`](https://git-scm.com/docs/git-format-patch#_discussion).
+The `diff` field is a base64-encoded remediation code diff, compatible with
+[`git apply`](https://git-scm.com/docs/git-format-patch#_discussion). This field is required.
diff --git a/doc/development/integrations/secure_partner_integration.md b/doc/development/integrations/secure_partner_integration.md
index d8badda4125..59336b0e6a1 100644
--- a/doc/development/integrations/secure_partner_integration.md
+++ b/doc/development/integrations/secure_partner_integration.md
@@ -68,7 +68,7 @@ and complete an intgration with the Secure stage.
1. Ensure your pipeline jobs create a report artifact that GitLab can process
to successfully display your own product's results with the rest of GitLab.
- See detailed [technical directions](secure.md) for this step.
- - Read more about [job report artifacts](../../ci/yaml/README.md#artifactsreports).
+ - Read more about [job report artifacts](../../ci/pipelines/job_artifacts.md#artifactsreports).
- Read about [job artifacts](../../user/project/pipelines/job_artifacts.md).
- Your report artifact must be in one of our currently supported formats.
For more information, see the [documentation on reports](secure.md#report).
diff --git a/doc/development/interacting_components.md b/doc/development/interacting_components.md
index 4639bd7cc20..697c64986b1 100644
--- a/doc/development/interacting_components.md
+++ b/doc/development/interacting_components.md
@@ -10,7 +10,7 @@ when making _backend_ changes that might involve multiple features or [component
## Uploads
GitLab supports uploads to [object storage](https://docs.gitlab.com/charts/advanced/external-object-storage/). That means every feature and
-change that affects uploads should also be tested against [object storage],
+change that affects uploads should also be tested against [object storage](https://docs.gitlab.com/charts/advanced/external-object-storage/),
which is _not_ enabled by default in [GDK](https://gitlab.com/gitlab-org/gitlab-development-kit).
When working on a related feature, make sure to enable and test it
diff --git a/doc/development/internal_api.md b/doc/development/internal_api.md
index e8fca7535b0..5b53f223eb0 100644
--- a/doc/development/internal_api.md
+++ b/doc/development/internal_api.md
@@ -40,7 +40,7 @@ POST /internal/allowed
| Attribute | Type | Required | Description |
|:----------|:-------|:---------|:------------|
-| `key_id` | string | no | Id of the SSH-key used to connect to GitLab-shell |
+| `key_id` | string | no | ID of the SSH-key used to connect to GitLab-shell |
| `username` | string | no | Username from the certificate used to connect to GitLab-Shell |
| `project` | string | no (if `gl_repository` is passed) | Path to the project |
| `gl_repository` | string | no (if `project` is passed) | Path to the project |
@@ -93,7 +93,7 @@ information for LFS clients when the repository is accessed over SSH.
| Attribute | Type | Required | Description |
|:----------|:-------|:---------|:------------|
-| `key_id` | string | no | Id of the SSH-key used to connect to GitLab-shell |
+| `key_id` | string | no | ID of the SSH-key used to connect to GitLab-shell |
| `username`| string | no | Username from the certificate used to connect to GitLab-Shell |
| `project` | string | no | Path to the project |
@@ -151,14 +151,14 @@ Example response:
- GitLab-shell
-## Get user for user id or key
+## Get user for user ID or key
This endpoint is used when a user performs `ssh git@gitlab.com`. It
discovers the user associated with an SSH key.
| Attribute | Type | Required | Description |
|:----------|:-------|:---------|:------------|
-| `key_id` | integer | no | The id of the SSH key used as found in the authorized-keys file or through the `/authorized_keys` check |
+| `key_id` | integer | no | The ID of the SSH key used as found in the authorized-keys file or through the `/authorized_keys` check |
| `username` | string | no | Username of the user being looked up, used by GitLab-shell when authenticating using a certificate |
```plaintext
@@ -223,7 +223,7 @@ recovery codes based on their SSH key
| Attribute | Type | Required | Description |
|:----------|:-------|:---------|:------------|
-| `key_id` | integer | no | The id of the SSH key used as found in the authorized-keys file or through the `/authorized_keys` check |
+| `key_id` | integer | no | The ID of the SSH key used as found in the authorized-keys file or through the `/authorized_keys` check |
| `user_id` | integer | no | **Deprecated** User_id for which to generate new recovery codes |
```plaintext
diff --git a/doc/development/lfs.md b/doc/development/lfs.md
index e64bc0f7d3a..32e2e3d1bde 100644
--- a/doc/development/lfs.md
+++ b/doc/development/lfs.md
@@ -2,7 +2,7 @@
## Deep Dive
-In April 2019, Francisco Javier López hosted a [Deep Dive](https://gitlab.com/gitlab-org/create-stage/issues/1)
+In April 2019, Francisco Javier López hosted a Deep Dive (GitLab team members only: `https://gitlab.com/gitlab-org/create-stage/issues/1`)
on GitLab's [Git LFS](../topics/git/lfs/index.md) implementation to share his domain
specific knowledge with anyone who may work in this part of the code base in the future.
You can find the [recording on YouTube](https://www.youtube.com/watch?v=Yyxwcksr0Qc),
diff --git a/doc/development/licensed_feature_availability.md b/doc/development/licensed_feature_availability.md
index 29e4ace157b..4f962b6f5e2 100644
--- a/doc/development/licensed_feature_availability.md
+++ b/doc/development/licensed_feature_availability.md
@@ -35,3 +35,7 @@ the instance license.
```ruby
License.feature_available?(:feature_symbol)
```
+
+## Enabling promo features on GitLab.com
+
+A paid feature can be made available to everyone on GitLab.com by enabling the feature flag `"promo_#{feature}"`.
diff --git a/doc/development/licensing.md b/doc/development/licensing.md
index 06a71967d06..e1be1faa61b 100644
--- a/doc/development/licensing.md
+++ b/doc/development/licensing.md
@@ -42,66 +42,6 @@ For all of the above, please include `--why "Reason"` and `--who "My Name"` so t
More detailed information on how the gem and its commands work is available in the [License Finder README](https://github.com/pivotal/LicenseFinder).
-## Acceptable Licenses
+## Additional information
-Libraries with the following licenses are acceptable for use:
-
-- [MIT License](https://choosealicense.com/licenses/mit/) (the MIT Expat License specifically): The MIT License requires that the license itself is included with all copies of the source. It is a permissive (non-copyleft) license as defined by the Open Source Initiative.
-- [GNU Lesser General Public License (GNU LGPL)](https://choosealicense.com/licenses/lgpl-3.0/) (version 2, version 3): GPL constraints regarding modification and redistribution under the same license are not required of projects using an LGPL library, only upon modification of the LGPL-licensed library itself.
-- [Apache 2.0 License](https://choosealicense.com/licenses/apache-2.0/): A permissive license that also provides an express grant of patent rights from contributors to users.
-- [Ruby 1.8 License](https://github.com/ruby/ruby/blob/ruby_1_8_6/COPYING): Dual-licensed under either itself or the GPLv2, defer to the Ruby License itself. Acceptable because of point 3b: "You may distribute the software in object code or binary form, provided that you do at least ONE of the following: b) accompany the distribution with the machine-readable source of the software."
-- [Ruby 1.9 License](https://www.ruby-lang.org/en/about/license.txt): Dual-licensed under either itself or the BSD 2-Clause License, defer to BSD 2-Clause.
-- [BSD 2-Clause License](https://opensource.org/licenses/BSD-2-Clause): A permissive (non-copyleft) license as defined by the Open Source Initiative.
-- [BSD 3-Clause License](https://opensource.org/licenses/BSD-3-Clause) (also known as New BSD or Modified BSD): A permissive (non-copyleft) license as defined by the Open Source Initiative
-- [ISC License](https://opensource.org/licenses/ISC) (also known as the OpenBSD License): A permissive (non-copyleft) license as defined by the Open Source Initiative.
-- [Creative Commons Zero (CC0)](https://creativecommons.org/publicdomain/zero/1.0/): A public domain dedication, recommended as a way to disclaim copyright on your work to the maximum extent possible.
-- [Unlicense](https://unlicense.org): Another public domain dedication.
-- [OWFa 1.0](http://www.openwebfoundation.org/legal/the-owf-1-0-agreements/owfa-1-0): An open-source license and patent grant designed for specifications.
-- [JSON License](https://www.json.org/license.html): Equivalent to the MIT license plus the statement, "The Software shall be used for Good, not Evil."
-
-## Unacceptable Licenses
-
-Libraries with the following licenses require legal approval for use:
-
-- [GNU GPL](https://choosealicense.com/licenses/gpl-3.0/) (version 1, [version 2](http://www.gnu.org/licenses/gpl-2.0.txt), [version 3](http://www.gnu.org/licenses/gpl-3.0.txt), or any future versions): GPL-licensed libraries cannot be linked to from non-GPL projects.
-- [GNU AGPLv3](https://choosealicense.com/licenses/agpl-3.0/): AGPL-licensed libraries cannot be linked to from non-GPL projects.
-- [Open Software License (OSL)](https://opensource.org/licenses/OSL-3.0): is a copyleft license. In addition, the FSF [recommend against its use](https://www.gnu.org/licenses/license-list.en.html#OSL).
-- [WTFPL](https://wtfpl.net): is a public domain dedication [rejected by the OSI (3.2)](https://opensource.org/minutes20090304). Also has a strong language which is not in accordance with our diversity policy.
-
-## GPL Cooperation Commitment
-
-Before filing or continuing to prosecute any legal proceeding or claim (other than a Defensive Action) arising from termination of a Covered License, GitLab commits to extend to the person or entity (“youâ€) accused of violating the Covered License the following provisions regarding cure and reinstatement, taken from GPL version 3. As used here, the term ‘this License’ refers to the specific Covered License being enforced.
-
-However, if you cease all violation of this License, then your license from a particular copyright holder is reinstated (a) provisionally, unless and until the copyright holder explicitly and finally terminates your license, and (b) permanently, if the copyright holder fails to notify you of the violation by some reasonable means prior to 60 days after the cessation.
-
-Moreover, your license from a particular copyright holder is reinstated permanently if the copyright holder notifies you of the violation by some reasonable means, this is the first time you have received notice of violation of this License (for any work) from that copyright holder, and you cure the violation prior to 30 days after your receipt of the notice.
-
-GitLab intends this Commitment to be irrevocable, and binding and enforceable against GitLab and assignees of or successors to GitLab’s copyrights.
-
-GitLab may modify this Commitment by publishing a new edition on this page or a successor location.
-
-Definitions
-
-‘Covered License’ means the GNU General Public License, version 2 (GPLv2), the GNU Lesser General Public License, version 2.1 (LGPLv2.1), or the GNU Library General Public License, version 2 (LGPLv2), all as published by the Free Software Foundation.
-
-‘Defensive Action’ means a legal proceeding or claim that GitLab brings against you in response to a prior proceeding or claim initiated by you or your affiliate.
-
-GitLab means GitLab Inc. and its affiliates and subsidiaries.
-
-## Requesting Approval for Licenses or any other Intellectual Property
-
-Libraries that are not already approved and listed on the [Acceptable Licenses](#acceptable-licenses) list or that may be listed on the [Unacceptable Licenses](#unacceptable-licenses) list may be submitted to the legal team for review and use on a case-by-case basis. Please email `legal@gitlab.com` with the details of how the software will be used, whether or not it will be modified, and how it will be distributed (if at all). After a decision has been made, the original requestor is responsible for updating this document, if applicable. Not all approvals will be approved for universal use and may continue to remain on the Unacceptable License list.
-
-All inquiries relating to patents should be directed to the Legal team.
-
-## Notes
-
-Decisions regarding the GNU GPL licenses are based on information provided by [The GNU Project](http://www.gnu.org/licenses/gpl-faq.html#IfLibraryIsGPL), as well as [the Open Source Initiative](https://opensource.org/faq#linking-proprietary-code), which both state that linking GPL libraries makes the program itself GPL.
-
-If a gem uses a license which is not listed above, open an issue and ask. If a license is not included in the "acceptable" list, operate under the assumption that it is not acceptable.
-
-Keep in mind that each license has its own restrictions (typically defined in their body text). Please make sure to comply with those restrictions at all times whenever an external library is used.
-
-Dependencies which are only used in development or test environment are exempt from license requirements, as they're not distributed for use in production.
-
-**NOTE:** This document is **not** legal advice, nor is it comprehensive. It should not be taken as such.
+Please see the [Open Source](https://about.gitlab.com/handbook/engineering/open-source/#using-open-source-libraries) page for more information on licensing.
diff --git a/doc/development/logging.md b/doc/development/logging.md
index ef2d2d7022d..e7d48e4d278 100644
--- a/doc/development/logging.md
+++ b/doc/development/logging.md
@@ -167,7 +167,8 @@ Resources:
#### Logging durations
Similar to timezones, choosing the right time unit to log can impose avoidable overhead. So, whenever
-challenged to choose between seconds, milliseconds or any other unit, lean towards _seconds_ as float.
+challenged to choose between seconds, milliseconds or any other unit, lean towards _seconds_ as float
+(with microseconds precision, i.e. `Gitlab::InstrumentationHelper::DURATION_PRECISION`).
In order to make it easier to track timings in the logs, make sure the log key has `_s` as
suffix and `duration` within its name (e.g., `view_duration_s`).
@@ -260,6 +261,8 @@ I, [2020-01-13T19:01:17.091Z #11056] INFO -- : {"message"=>"Message", "project_
lifecycle, which can then be added to the web request
or Sidekiq logs.
+The API, Rails and Sidekiq logs contain fields starting with `meta.` with this context information.
+
Entry points can be seen at:
- [`ApplicationController`](https://gitlab.com/gitlab-org/gitlab/blob/master/app/controllers/application_controller.rb)
@@ -360,4 +363,4 @@ end
project. See [this example](https://gitlab.com/gitlab-cookbooks/gitlab_fluentd/-/merge_requests/51/diffs).
1. Be sure to update the [GitLab CE/EE documentation](../administration/logs.md) and the [GitLab.com
- runbooks](https://gitlab.com/gitlab-com/runbooks/blob/master/howto/logging.md).
+ runbooks](https://gitlab.com/gitlab-com/runbooks/blob/master/docs/logging/README.md).
diff --git a/doc/development/mass_insert.md b/doc/development/mass_insert.md
index 47f993a921e..c19850ca67e 100644
--- a/doc/development/mass_insert.md
+++ b/doc/development/mass_insert.md
@@ -1,7 +1,7 @@
-# Mass Inserting Rails Models
+# Mass inserting Rails models
-Setting the environment variable [`MASS_INSERT=1`](rake_tasks.md#env-variables)
-when running `rake setup` will create millions of records, but these records
+Setting the environment variable [`MASS_INSERT=1`](rake_tasks.md#environment-variables)
+when running [`rake setup`](rake_tasks.md) will create millions of records, but these records
aren't visible to the `root` user by default.
To make any number of the mass-inserted projects visible to the `root` user, run
diff --git a/doc/development/merge_request_performance_guidelines.md b/doc/development/merge_request_performance_guidelines.md
index 4a9b5d26aef..b51fc681e27 100644
--- a/doc/development/merge_request_performance_guidelines.md
+++ b/doc/development/merge_request_performance_guidelines.md
@@ -24,7 +24,7 @@ The term `SHOULD` per the [RFC 2119](https://www.ietf.org/rfc/rfc2119.txt) means
> carefully weighed before choosing a different course.
Ideally, each of these tradeoffs should be documented
-in the separate issues, labelled accordingly and linked
+in the separate issues, labeled accordingly and linked
to original issue and epic.
## Impact Analysis
diff --git a/doc/development/migration_style_guide.md b/doc/development/migration_style_guide.md
index 3e993243855..4cf546173de 100644
--- a/doc/development/migration_style_guide.md
+++ b/doc/development/migration_style_guide.md
@@ -35,16 +35,36 @@ and post-deployment migrations (`db/post_migrate`) are run after the deployment
## Schema Changes
-Migrations that make changes to the database schema (e.g. adding a column) can
-only be added in the monthly release, patch releases may only contain data
-migrations _unless_ schema changes are absolutely required to solve a problem.
+Changes to the schema should be commited to `db/structure.sql`. This
+file is automatically generated by Rails, so you normally should not
+edit this file by hand. If your migration is adding a column to a
+table, that column will be added at the bottom. Please do not reorder
+columns manually for existing tables as this will cause confusing to
+other people using `db/structure.sql` generated by Rails.
+
+When your local database in your GDK is diverging from the schema from
+`master` it might be hard to cleanly commit the schema changes to
+Git. In that case you can use the `scripts/regenerate-schema` script to
+regenerate a clean `db/structure.sql` for the migrations you're
+adding. This script will apply all migrations found in `db/migrate`
+or `db/post_migrate`, so if there are any migrations you don't want to
+commit to the schema, rename or remove them. If your branch is not
+targetting `master` you can set the `TARGET` environment variable.
+
+```shell
+# Regenerate schema against `master`
+scripts/regenerate-schema
+
+# Regenerate schema against `12-9-stable-ee`
+TARGET=12-9-stable-ee scripts/regenerate-schema
+```
## What Requires Downtime?
The document ["What Requires Downtime?"](what_requires_downtime.md) specifies
various database operations, such as
-- [adding, dropping, and renaming columns](what_requires_downtime.md#adding-columns)
+- [dropping and renaming columns](what_requires_downtime.md#dropping-columns)
- [changing column constraints and types](what_requires_downtime.md#changing-column-constraints)
- [adding and dropping indexes, tables, and foreign keys](what_requires_downtime.md#adding-indexes)
@@ -307,6 +327,34 @@ def down
end
```
+**Usage with `disable_ddl_transaction!`**
+
+Generally the `with_lock_retries` helper should work with `disabled_ddl_transaction!`. A custom RuboCop rule ensures that only allowed methods can be placed within the lock retries block.
+
+```ruby
+disable_ddl_transaction!
+
+def up
+ with_lock_retries do
+ add_column :users, :name, :text
+ end
+
+ add_text_limit :users, :name, 255 # Includes constraint validation (full table scan)
+end
+```
+
+The RuboCop rule generally allows standard Rails migration methods, listed below. This example will cause a rubocop offense:
+
+```ruby
+disabled_ddl_transaction!
+
+def up
+ with_lock_retries do
+ add_concurrent_index :users, :name
+ end
+end
+```
+
### When to use the helper method
The `with_lock_retries` helper method can be used when you normally use
@@ -330,8 +378,6 @@ Example changes:
- `change_column_default`
- `create_table` / `drop_table`
-**Note:** `with_lock_retries` method **cannot** be used with `disable_ddl_transaction!`.
-
**Note:** `with_lock_retries` method **cannot** be used within the `change` method, you must manually define the `up` and `down` methods to make the migration reversible.
### How the helper method works
@@ -506,34 +552,12 @@ You can read more about adding [foreign key constraints to an existing column](d
## Adding Columns With Default Values
-When adding columns with default values to non-empty tables, you must use
-`add_column_with_default`. This method ensures the table is updated without
-requiring downtime. This method is not reversible so you must manually define
-the `up` and `down` methods in your migration class.
-
-For example, to add the column `foo` to the `projects` table with a default
-value of `10` you'd write the following:
-
-```ruby
-class MyMigration < ActiveRecord::Migration[4.2]
- include Gitlab::Database::MigrationHelpers
- disable_ddl_transaction!
-
- def up
- add_column_with_default(:projects, :foo, :integer, default: 10)
- end
-
- def down
- remove_column(:projects, :foo)
- end
-end
-```
+With PostgreSQL 11 being the minimum version since GitLab 13.0, adding columns with default values has become much easier and
+the standard `add_column` helper should be used in all cases.
-Keep in mind that this operation can easily take 10-15 minutes to complete on
-larger installations (e.g. GitLab.com). As a result, you should only add
-default values if absolutely necessary. There is a RuboCop cop that will fail if
-this method is used on some tables that are very large on GitLab.com, which
-would cause other issues.
+Before PostgreSQL 11, adding a column with a default was problematic as it would
+have caused a full table rewrite. The corresponding helper `add_column_with_default`
+has been deprecated and will be removed in a later release.
## Changing the column default
@@ -574,8 +598,7 @@ without requiring `disable_ddl_transaction!`.
## Updating an existing column
To update an existing column to a particular value, you can use
-`update_column_in_batches` (`add_column_with_default` uses this internally to
-fill in the default value). This will split the updates into batches, so we
+`update_column_in_batches`. This will split the updates into batches, so we
don't update too many rows at in a single statement.
This updates the column `foo` in the `projects` table to 10, where `some_column`
@@ -701,7 +724,7 @@ set the limit to 8-bytes. This will allow the column to hold a value up to
Rails migration example:
```ruby
-add_column_with_default(:projects, :foo, :integer, default: 10, limit: 8)
+add_column(:projects, :foo, :integer, default: 10, limit: 8)
```
## Timestamp column type
diff --git a/doc/development/multi_version_compatibility.md b/doc/development/multi_version_compatibility.md
new file mode 100644
index 00000000000..aedd5c1ffb7
--- /dev/null
+++ b/doc/development/multi_version_compatibility.md
@@ -0,0 +1,62 @@
+# Compatibility with multiple versions of the application running at the same time
+
+When adding or changing features, we must be aware that there may be multiple versions of the application running
+at the same time and connected to the same PostgreSQL and Redis databases. This could happen during a rolling deploy
+when the servers are updated one by one.
+
+During a rolling deploy, post-deployment DB migrations are run after all the servers have been updated. This means the
+servers could be in these intermediate states:
+
+1. Old application code running with new DB migrations already executed
+1. New application code running with new DB migrations but without new post-deployment DB migrations
+
+We must make sure that the application works properly in these states.
+
+For GitLab.com, we also run a set of canary servers which run a more recent version of the application. Users with
+the canary cookie set would be handled by these servers. Some URL patterns may also be forced to the canary servers,
+even without the cookie being set. This also means that some pages may match the pattern and get handled by canary servers,
+but AJAX requests to URLs (like the GraphQL endpoint) won't match the pattern.
+
+With this canary setup, we'd be in this mixed-versions state for an extended period of time until canary is promoted to
+production and post-deployment migrations run.
+
+## Examples of previous incidents
+
+### Some links to issues and MRs were broken
+
+When we moved MR routes, users on the new servers were redirected to the new URLs. When these users shared these new URLs in
+Markdown (or anywhere else), they were broken links for users on the old servers.
+
+For more information, see [the relevant issue](https://gitlab.com/gitlab-org/gitlab/-/issues/118840).
+
+### Stale cache in issue or merge request descriptions and comments
+
+We bumped the Markdown cache version and found a bug when a user edited a description or comment which was generated from a different Markdown
+cache version. The cached HTML wasn't generated properly after saving. In most cases, this wouldn't have happened because users would have
+viewed the Markdown before clicking **Edit** and that would mean the Markdown cache is refreshed. But because we run mixed versions, this is
+more likely to happen. Another user on a different version could view the same page and refresh the cache to the other version behind the scenes.
+
+For more information, see [the relevant issue](https://gitlab.com/gitlab-org/gitlab/-/issues/208255).
+
+### Project service templates incorrectly copied
+
+We changed the column which indicates whether a service is a template. When we create services, we copy attributes from the template
+and set this column to `false`. The old servers were still updating the old column, but that was fine because we had a DB trigger
+that updated the new column from the old one. For the new servers though, they were only updating the new column and that same trigger
+was now working against us and setting it back to the wrong value.
+
+For more information, see [the relevant issue](https://gitlab.com/gitlab-com/gl-infra/infrastructure/-/issues/9176).
+
+### Sidebar wasn't loading for some users
+
+We changed the data type of one GraphQL field. When a user opened an issue page from the new servers and the GraphQL AJAX request went
+to the old servers, a type mismatch happened, which resulted in a JavaScript error that prevented the sidebar from loading.
+
+For more information, see [the relevant issue](https://gitlab.com/gitlab-com/gl-infra/production/-/issues/1772).
+
+### CI artifact uploads were failing
+
+We added a `NOT NULL` constraint to a column and marked it as a `NOT VALID` constraint so that it is not enforced on existing rows.
+But even with that, this was still a problem because the old servers were still inserting new rows with null values.
+
+For more information, see [the relevant issue](https://gitlab.com/gitlab-com/gl-infra/production/-/issues/1944).
diff --git a/doc/development/namespaces_storage_statistics.md b/doc/development/namespaces_storage_statistics.md
index f175739e55e..3065d4f84a2 100644
--- a/doc/development/namespaces_storage_statistics.md
+++ b/doc/development/namespaces_storage_statistics.md
@@ -38,7 +38,7 @@ alternative method.
### Attempt A: PostgreSQL materialized view
-Model can be updated through a refresh strategy based on a project routes SQL and a [materialized view](https://www.postgresql.org/docs/9.6/rules-materializedviews.html):
+Model can be updated through a refresh strategy based on a project routes SQL and a [materialized view](https://www.postgresql.org/docs/11/rules-materializedviews.html):
```sql
SELECT split_part("rs".path, '/', 1) as root_path,
@@ -111,11 +111,11 @@ Directly relate the root namespace to its child namespaces, so
whenever a namespace is created without a parent, this one is tagged
with the root namespace ID:
-| id | root_id | parent_id
-|:---|:--------|:----------
-| 1 | 1 | NULL
-| 2 | 1 | 1
-| 3 | 1 | 2
+| ID | root ID | parent ID |
+|:---|:--------|:----------|
+| 1 | 1 | NULL |
+| 2 | 1 | 1 |
+| 3 | 1 | 2 |
To aggregate the statistics inside a namespace we'd execute something like:
diff --git a/doc/development/new_fe_guide/development/accessibility.md b/doc/development/new_fe_guide/development/accessibility.md
index 8d9f7ca5069..aa76a9fec07 100644
--- a/doc/development/new_fe_guide/development/accessibility.md
+++ b/doc/development/new_fe_guide/development/accessibility.md
@@ -6,13 +6,13 @@ Using semantic HTML plays a key role when it comes to accessibility.
WAI-ARIA (the Accessible Rich Internet Applications specification) defines a way to make Web content and Web applications more accessible to people with disabilities.
-> Note: It is [recommended][using-aria] to use semantic elements as the primary method to achieve accessibility rather than adding aria attributes. Adding aria attributes should be seen as a secondary method for creating accessible elements.
+> Note: It is [recommended](https://www.w3.org/TR/using-aria/#notes2) to use semantic elements as the primary method to achieve accessibility rather than adding aria attributes. Adding aria attributes should be seen as a secondary method for creating accessible elements.
### Role
The `role` attribute describes the role the element plays in the context of the document.
-Check the list of WAI-ARIA roles [here][roles]
+Check the list of WAI-ARIA roles [here](https://www.w3.org/TR/wai-aria-1.1/#landmark_roles)
## Icons
@@ -36,20 +36,11 @@ In forms we should use the `for` attribute in the label statement:
## Testing
-1. On MacOS you can use [VoiceOver][voice-over] by pressing `cmd+F5`.
-1. On Windows you can use [Narrator][narrator] by pressing Windows logo key + Ctrl + Enter.
+1. On MacOS you can use [VoiceOver](https://www.apple.com/accessibility/mac/vision/) by pressing `cmd+F5`.
+1. On Windows you can use [Narrator](https://www.microsoft.com/en-us/accessibility/windows) by pressing Windows logo key + Ctrl + Enter.
## Online resources
-- [Chrome Accessibility Developer Tools][dev-tools] for testing accessibility
-- [Audit Rules Page][audit-rules] for best practices
-- [Lighthouse Accessibility Score][lighthouse] for accessibility audits
-
-[using-aria]: https://www.w3.org/TR/using-aria/#notes2
-[dev-tools]: https://github.com/GoogleChrome/accessibility-developer-tools
-[audit-rules]: https://github.com/GoogleChrome/accessibility-developer-tools/wiki/Audit-Rules
-[aria-w3c]: https://www.w3.org/TR/wai-aria-1.1/
-[roles]: https://www.w3.org/TR/wai-aria-1.1/#landmark_roles
-[voice-over]: https://www.apple.com/accessibility/mac/vision/
-[narrator]: https://www.microsoft.com/en-us/accessibility/windows
-[lighthouse]: https://developers.google.com/web/tools/lighthouse/scoring#a11y
+- [Chrome Accessibility Developer Tools](https://github.com/GoogleChrome/accessibility-developer-tools) for testing accessibility
+- [Audit Rules Page](https://github.com/GoogleChrome/accessibility-developer-tools/wiki/Audit-Rules) for best practices
+- [Lighthouse Accessibility Score](https://developers.google.com/web/tools/lighthouse/scoring#a11y) for accessibility audits
diff --git a/doc/development/new_fe_guide/modules/dirty_submit.md b/doc/development/new_fe_guide/modules/dirty_submit.md
index 30cf8017820..dd336ad3a90 100644
--- a/doc/development/new_fe_guide/modules/dirty_submit.md
+++ b/doc/development/new_fe_guide/modules/dirty_submit.md
@@ -13,7 +13,7 @@ within the GitLab project.
## Usage
-```js
+```javascript
import dirtySubmitFactory from './dirty_submit/dirty_submit_form';
new DirtySubmitForm(document.querySelector('form'));
diff --git a/doc/development/newlines_styleguide.md b/doc/development/newlines_styleguide.md
index a13adc2f13e..e298ba1d935 100644
--- a/doc/development/newlines_styleguide.md
+++ b/doc/development/newlines_styleguide.md
@@ -1,4 +1,4 @@
-# Newlines styleguide
+# Newlines style guide
This style guide recommends best practices for newlines in Ruby code.
diff --git a/doc/development/ordering_table_columns.md b/doc/development/ordering_table_columns.md
index cbfd05e731d..b68602ea30d 100644
--- a/doc/development/ordering_table_columns.md
+++ b/doc/development/ordering_table_columns.md
@@ -77,64 +77,64 @@ always be at the end of a table.
Let's use the `events` table as an example, which currently has the following
layout:
-| Column | Type | Size |
-|:------------|:----------------------------|:---------|
-| id | integer | 4 bytes |
-| target_type | character varying | variable |
-| target_id | integer | 4 bytes |
-| title | character varying | variable |
-| data | text | variable |
-| project_id | integer | 4 bytes |
-| created_at | timestamp without time zone | 8 bytes |
-| updated_at | timestamp without time zone | 8 bytes |
-| action | integer | 4 bytes |
-| author_id | integer | 4 bytes |
+| Column | Type | Size |
+|:--------------|:----------------------------|:---------|
+| `id` | integer | 4 bytes |
+| `target_type` | character varying | variable |
+| `target_id` | integer | 4 bytes |
+| `title` | character varying | variable |
+| `data` | text | variable |
+| `project_id` | integer | 4 bytes |
+| `created_at` | timestamp without time zone | 8 bytes |
+| `updated_at` | timestamp without time zone | 8 bytes |
+| `action` | integer | 4 bytes |
+| `author_id` | integer | 4 bytes |
After adding padding to align the columns this would translate to columns being
divided into fixed size chunks as follows:
-| Chunk Size | Columns |
-|:-----------|:------------------|
-| 8 bytes | id |
-| variable | target_type |
-| 8 bytes | target_id |
-| variable | title |
-| variable | data |
-| 8 bytes | project_id |
-| 8 bytes | created_at |
-| 8 bytes | updated_at |
-| 8 bytes | action, author_id |
+| Chunk Size | Columns |
+|:-----------|:----------------------|
+| 8 bytes | `id` |
+| variable | `target_type` |
+| 8 bytes | `target_id` |
+| variable | `title` |
+| variable | `data` |
+| 8 bytes | `project_id` |
+| 8 bytes | `created_at` |
+| 8 bytes | `updated_at` |
+| 8 bytes | `action`, `author_id` |
This means that excluding the variable sized data and tuple header, we need at
least 8 * 6 = 48 bytes per row.
We can optimise this by using the following column order instead:
-| Column | Type | Size |
-|:------------|:----------------------------|:---------|
-| created_at | timestamp without time zone | 8 bytes |
-| updated_at | timestamp without time zone | 8 bytes |
-| id | integer | 4 bytes |
-| target_id | integer | 4 bytes |
-| project_id | integer | 4 bytes |
-| action | integer | 4 bytes |
-| author_id | integer | 4 bytes |
-| target_type | character varying | variable |
-| title | character varying | variable |
-| data | text | variable |
+| Column | Type | Size |
+|:--------------|:----------------------------|:---------|
+| `created_at` | timestamp without time zone | 8 bytes |
+| `updated_at` | timestamp without time zone | 8 bytes |
+| `id` | integer | 4 bytes |
+| `target_id` | integer | 4 bytes |
+| `project_id` | integer | 4 bytes |
+| `action` | integer | 4 bytes |
+| `author_id` | integer | 4 bytes |
+| `target_type` | character varying | variable |
+| `title` | character varying | variable |
+| `data` | text | variable |
This would produce the following chunks:
-| Chunk Size | Columns |
-|:-----------|:-------------------|
-| 8 bytes | created_at |
-| 8 bytes | updated_at |
-| 8 bytes | id, target_id |
-| 8 bytes | project_id, action |
-| 8 bytes | author_id |
-| variable | target_type |
-| variable | title |
-| variable | data |
+| Chunk Size | Columns |
+|:-----------|:-----------------------|
+| 8 bytes | `created_at` |
+| 8 bytes | `updated_at` |
+| 8 bytes | `id`, `target_id` |
+| 8 bytes | `project_id`, `action` |
+| 8 bytes | `author_id` |
+| variable | `target_type` |
+| variable | `title` |
+| variable | `data` |
Here we only need 40 bytes per row excluding the variable sized data and 24-byte
tuple header. 8 bytes being saved may not sound like much, but for tables as
diff --git a/doc/development/packages.md b/doc/development/packages.md
index 66b50ce12c8..11fc3faf4ab 100644
--- a/doc/development/packages.md
+++ b/doc/development/packages.md
@@ -74,7 +74,7 @@ It is using the top-level group or namespace as the defining portion of the name
To avoid name conflict for instance-level endpoints you will need to define a package naming convention
that gives a way to identify the project that the package belongs to. This generally involves using the project
-id or full project path in the package name. See
+ID or full project path in the package name. See
[Conan's naming convention](../user/packages/conan_repository/index.md#package-recipe-naming-convention) as an example.
For group and project-level endpoints, naming can be less constrained and it will be up to the group and project
@@ -110,7 +110,7 @@ The way new package systems are integrated in GitLab is using an [MVC](https://a
Required actions are all the additional requests that GitLab will need to handle so the corresponding package manager CLI can work properly. It could be a search feature or an endpoint providing meta information about a package. For example:
- For NuGet, the search request was implemented during the first MVC iteration, to support Visual Studio.
-- For NPM, there is a metadata endpoint used by `npm` to get the tarball url.
+- For NPM, there is a metadata endpoint used by `npm` to get the tarball URL.
For the first MVC iteration, it's recommended to stay at the project level of the [remote hierarchy](#remote-hierarchy). Other levels can be tackled with [future Merge Requests](#future-work).
@@ -139,7 +139,7 @@ During this phase, the idea is to collect as much information as possible about
- **Requests**: Which requests are needed to have a working MVC. Ideally, produce
a list of all the requests needed for the MVC (including required actions). Further
investigation could provide an example for each request with the request and the response bodies.
-- **Upload**: Carefully analyse how the upload process works. This will probably be the most
+- **Upload**: Carefully analyze how the upload process works. This will probably be the most
complex request to implement. A detailed analysis is desired here as uploads can be
encoded in different ways (body or multipart) and can even be in a totally different
format (for example, a JSON structure where the package file is a Base64 value of
@@ -159,7 +159,7 @@ During this phase, the idea is to collect as much information as possible about
The analysis usually takes a full milestone to complete, though it's not impossible to start the implementation in the same milestone.
-In particular, the upload request can have some [requirements in the GitLab Workhorse project](#file-uploads). This project has a different release cycle than the rails backend. It's **strongly** recommended that you open an issue there as soon as the upload request analysis is done. This way GitLab Worhorse is already ready when the upload request is implemented on the rails backend.
+In particular, the upload request can have some [requirements in the GitLab Workhorse project](#file-uploads). This project has a different release cycle than the rails backend. It's **strongly** recommended that you open an issue there as soon as the upload request analysis is done. This way GitLab Workhorse is already ready when the upload request is implemented on the rails backend.
### Implementation
@@ -196,8 +196,8 @@ information like the file `name`, `side`, `sha1`, etc.
If there is specific data necessary to be stored for only one package system support,
consider creating a separate metadata model. See `packages_maven_metadata` table
-and `Packages::MavenMetadatum` model as an example for package specific data, and `packages_conan_file_metadata` table
-and `Packages::ConanFileMetadatum` model as an example for package file specific data.
+and `Packages::Maven::Metadatum` model as an example for package specific data, and `packages_conan_file_metadata` table
+and `Packages::Conan::FileMetadatum` model as an example for package file specific data.
If there is package specific behavior for a given package manager, add those methods to the metadata models and
delegate from the package model.
diff --git a/doc/development/performance.md b/doc/development/performance.md
index 5068103ff16..2e73161a11f 100644
--- a/doc/development/performance.md
+++ b/doc/development/performance.md
@@ -46,7 +46,7 @@ GitLab provides built-in tools to help improve performance and availability:
GitLab team members can use [GitLab.com's performance monitoring systems](https://about.gitlab.com/handbook/engineering/monitoring/) located at
<https://dashboards.gitlab.net>, this requires you to log in using your
`@gitlab.com` email address. Non-GitLab team-members are advised to set up their
-own InfluxDB and Grafana stack.
+own Prometheus and Grafana stack.
## Benchmarks
@@ -109,7 +109,7 @@ In short:
By collecting snapshots of process state at regular intervals, profiling allows
you to see where time is spent in a process. The [StackProf](https://github.com/tmm1/stackprof)
gem is included in GitLab's development environment, allowing you to investigate
-the behaviour of suspect code in detail.
+the behavior of suspect code in detail.
It's important to note that profiling an application *alters its performance*,
and will generally be done *in an unrepresentative environment*. In particular,
diff --git a/doc/development/permissions.md b/doc/development/permissions.md
index bca137337fc..0772389bf9e 100644
--- a/doc/development/permissions.md
+++ b/doc/development/permissions.md
@@ -9,9 +9,9 @@ anything that deals with permissions, all of them should be considered.
Groups and projects can have the following visibility levels:
-- public (20) - an entity is visible to everyone
-- internal (10) - an entity is visible to logged in users
-- private (0) - an entity is visible only to the approved members of the entity
+- public (`20`) - an entity is visible to everyone
+- internal (`10`) - an entity is visible to logged in users
+- private (`0`) - an entity is visible only to the approved members of the entity
The visibility level of a group can be changed only if all subgroups and
subprojects have the same or lower visibility level. (e.g., a group can be set
diff --git a/doc/development/pipelines.md b/doc/development/pipelines.md
index 9ba6dfc110a..39ca846c1cc 100644
--- a/doc/development/pipelines.md
+++ b/doc/development/pipelines.md
@@ -19,6 +19,9 @@ The current stages are:
<https://gitlab.com/gitlab-org/gitlab-foss>.
- `prepare`: This stage includes jobs that prepare artifacts that are needed by
jobs in subsequent stages.
+- `build-images`: This stage includes jobs that prepare docker images
+ that are needed by jobs in subsequent stages or downstream pipelines.
+- `fixtures`: This stage includes jobs that prepare fixtures needed by frontend tests.
- `test`: This stage includes most of the tests, DB/migration jobs, and static analysis jobs.
- `post-test`: This stage includes jobs that build reports or gather data from
the `test` stage's jobs (e.g. coverage, Knapsack metadata etc.).
@@ -30,7 +33,6 @@ The current stages are:
that is deployed in the previous stage.
- `post-qa`: This stage includes jobs that build reports or gather data from
the `qa` stage's jobs (e.g. Review App performance report).
-- `notification`: This stage includes jobs that sends notifications about pipeline status.
- `pages`: This stage includes a job that deploys the various reports as
GitLab Pages (e.g. <https://gitlab-org.gitlab.io/gitlab/coverage-ruby/>,
<https://gitlab-org.gitlab.io/gitlab/coverage-javascript/>,
@@ -68,12 +70,9 @@ that are scoped to a single [configuration parameter](../ci/yaml/README.md#confi
| `.default-retry` | Allows a job to [retry](../ci/yaml/README.md#retry) upon `unknown_failure`, `api_failure`, `runner_system_failure`, `job_execution_timeout`, or `stuck_or_timeout_failure`. |
| `.default-before_script` | Allows a job to use a default `before_script` definition suitable for Ruby/Rails tasks that may need a database running (e.g. tests). |
| `.default-cache` | Allows a job to use a default `cache` definition suitable for Ruby/Rails and frontend tasks. |
-| `.use-pg9` | Allows a job to use the `postgres:9.6.17` and `redis:alpine` services. |
-| `.use-pg10` | Allows a job to use the `postgres:10.12` and `redis:alpine` services. |
| `.use-pg11` | Allows a job to use the `postgres:11.6` and `redis:alpine` services. |
-| `.use-pg9-ee` | Same as `.use-pg9` but also use the `docker.elastic.co/elasticsearch/elasticsearch:6.4.2` services. |
-| `.use-pg10-ee` | Same as `.use-pg10` but also use the `docker.elastic.co/elasticsearch/elasticsearch:6.4.2` services. |
| `.use-pg11-ee` | Same as `.use-pg11` but also use the `docker.elastic.co/elasticsearch/elasticsearch:6.4.2` services. |
+| `.use-kaniko` | Allows a job to use the `kaniko` tool to build Docker images. |
| `.as-if-foss` | Simulate the FOSS project by setting the `FOSS_ONLY='1'` environment variable. |
## `workflow:rules`
@@ -129,11 +128,12 @@ and included in `rules` definitions via [YAML anchors](../ci/yaml/README.md#anch
| `changes:` patterns | Description |
|------------------------------|--------------------------------------------------------------------------|
+| `ci-patterns` | Only create job for CI config-related changes. |
| `yaml-patterns` | Only create job for YAML-related changes. |
| `docs-patterns` | Only create job for docs-related changes. |
-| `frontend-dependency-patterns` | Only create job when frontend dependencies are updated (i.e. `package.json`, and `yarn.lock`). changes. |
+| `frontend-dependency-patterns` | Only create job when frontend dependencies are updated (i.e. `package.json`, and `yarn.lock`). changes. |
| `frontend-patterns` | Only create job for frontend-related changes. |
-| `backstage-patterns` | Only create job for backstage-related changes (i.e. Danger, fixtures, RuboCop, specs). |
+| `backstage-patterns` | Only create job for backstage-related changes (i.e. Danger, fixtures, RuboCop, specs). |
| `code-patterns` | Only create job for code-related changes. |
| `qa-patterns` | Only create job for QA-related changes. |
| `code-backstage-patterns` | Combination of `code-patterns` and `backstage-patterns`. |
@@ -151,112 +151,360 @@ request, be sure to start the `dont-interrupt-me` job before pushing.
## PostgreSQL versions testing
+### Current versions testing
+
+| Where? | PostgreSQL version |
+| ------ | ------ |
+| MRs | 11 |
+| `master` (non-scheduled pipelines) | 11 |
+| 2-hourly scheduled pipelines | 11 |
+
+### Long-term plan
+
We follow the [PostgreSQL versions shipped with Omnibus GitLab](https://docs.gitlab.com/omnibus/package-information/postgresql_versions.html):
-| | 12.10 (April 2020) | 13.0 (May 2020) | 13.1 (June 2020) | 13.2 (July 2020) | 13.3 (August 2020) | 13.4, 13.5 | 13.6 (November 2020) | 14.0 (May 2021?) |
+| PostgreSQL version | 12.10 (April 2020) | 13.0 (May 2020) | 13.1 (June 2020) | 13.2 (July 2020) | 13.3 (August 2020) | 13.4, 13.5 | 13.6 (November 2020) | 14.0 (May 2021?) |
| ------ | ------------------ | --------------- | ---------------- | ---------------- | ------------------ | ------------ | -------------------- | ---------------- |
-| PG9.6 | nightly | - | - | - | - | - | - | - |
-| PG10 | `master` | - | - | - | - | - | - | - |
-| PG11 | MRs/`master` | MRs/`master` | MRs/`master` | MRs/`master` | MRs/`master` | MRs/`master` | nightly | - |
-| PG12 | - | - | - | - | `master` | `master` | MRs/`master` | `master` |
-| PG13 | - | - | - | - | - | - | - | MRs/`master` |
+| PG9.6 | MRs/`master`/`2-hour`/`nightly` | - | - | - | - | - | - | - |
+| PG10 | `nightly` | - | - | - | - | - | - | - |
+| PG11 | `master`/`2-hour` | MRs/`master`/`2-hour`/`nightly` | MRs/`master`/`2-hour`/`nightly` | MRs/`master`/`2-hour`/`nightly` | MRs/`master`/`2-hour`/`nightly` | MRs/`master`/`2-hour`/`nightly` | `nightly` | - |
+| PG12 | - | - | - | - | `master`/`2-hour` | `master`/`2-hour` | MRs/`master`/`2-hour`/`nightly` | `master`/`2-hour` |
+| PG13 | - | - | - | - | - | - | - | MRs/`master`/`2-hour`/`nightly` |
+
+## Pipeline types
+
+Since we use the [`rules:`](../ci/yaml/README.md#rules) and [`needs:`](../ci/yaml/README.md#needs) keywords extensively,
+we have four main pipeline types which are described below. Note that an MR that includes multiple types of changes would
+have a pipelines that include jobs from multiple types (e.g. a combination of docs-only and code-only pipelines).
+
+### Docs-only MR pipeline
+
+Reference pipeline: <https://gitlab.com/gitlab-org/gitlab/pipelines/135236627>
+
+```mermaid
+graph LR
+ subgraph "No needed jobs";
+ 1-1["danger-review (3.5 minutes)"];
+ click 1-1 "https://app.periscopedata.com/app/gitlab/652085/Engineering-Productivity---Pipeline-Build-Durations?widget=8100542&udv=0"
+ 1-50["docs lint (6.75 minutes)"];
+ click 1-50 "https://app.periscopedata.com/app/gitlab/652085/Engineering-Productivity---Pipeline-Build-Durations?widget=8356757&udv=0"
+ end
+```
+
+### Code-only MR pipeline
+
+Reference pipeline: <https://gitlab.com/gitlab-org/gitlab/pipelines/136295694>
+
+```mermaid
+graph RL;
+ classDef criticalPath fill:#f66;
+
+ subgraph "No needed jobs";
+ 1-1["danger-review (3.5 minutes)"];
+ click 1-1 "https://app.periscopedata.com/app/gitlab/652085/Engineering-Productivity---Pipeline-Build-Durations?widget=8100542&udv=0"
+ 1-2["build-qa-image (3.4 minutes)"];
+ click 1-2 "https://app.periscopedata.com/app/gitlab/652085/Engineering-Productivity---Pipeline-Build-Durations?widget=6914325&udv=0"
+ 1-3["compile-assets pull-cache (9.06 minutes)"];
+ click 1-3 "https://app.periscopedata.com/app/gitlab/652085/Engineering-Productivity---Pipeline-Build-Durations?widget=6914317&udv=0"
+ 1-4["compile-assets pull-cache as-if-foss (8.35 minutes)"];
+ click 1-4 "https://app.periscopedata.com/app/gitlab/652085/Engineering-Productivity---Pipeline-Build-Durations?widget=8356616&udv=0"
+ 1-5["gitlab:assets:compile pull-cache (22 minutes)"];
+ click 1-5 "https://app.periscopedata.com/app/gitlab/652085/Engineering-Productivity---Pipeline-Build-Durations?widget=6914312&udv=0"
+ 1-6["setup-test-env (8.22 minutes)"];
+ click 1-6 "https://app.periscopedata.com/app/gitlab/652085/Engineering-Productivity---Pipeline-Build-Durations?widget=6914315&udv=0"
+ 1-7["review-stop-failed-deployment"];
+ 1-8["dependency_scanning"];
+ 1-9["qa:internal, qa:internal-as-if-foss"];
+ 1-11["qa:selectors, qa:selectors-as-if-foss"];
+ 1-14["retrieve-tests-metadata (1.5 minutes)"];
+ click 1-14 "https://app.periscopedata.com/app/gitlab/652085/Engineering-Productivity---Pipeline-Build-Durations?widget=8356697&udv=0"
+ 1-15["code_quality"];
+ 1-16["brakeman-sast"];
+ 1-17["eslint-sast"];
+ 1-18["kubesec-sast"];
+ 1-19["nodejs-scan-sast"];
+ 1-20["secrets-sast"];
+
+ class 1-3 criticalPath;
+ class 1-6 criticalPath;
+ end
+
+ 2_1-1["graphql-reference-verify (5 minutes)"];
+ click 2_1-1 "https://app.periscopedata.com/app/gitlab/652085/Engineering-Productivity---Pipeline-Build-Durations?widget=8356715&udv=0"
+ 2_1-2["memory-static (4.75 minutes)"];
+ click 2_1-2 "https://app.periscopedata.com/app/gitlab/652085/Engineering-Productivity---Pipeline-Build-Durations?widget=8356721&udv=0"
+ 2_1-3["run-dev-fixtures (5 minutes)"];
+ click 2_1-3 "https://app.periscopedata.com/app/gitlab/652085/Engineering-Productivity---Pipeline-Build-Durations?widget=8356729&udv=0"
+ 2_1-4["run-dev-fixtures-ee (5 minutes)"];
+ click 2_1-4 "https://app.periscopedata.com/app/gitlab/652085/Engineering-Productivity---Pipeline-Build-Durations?widget=8356731&udv=0"
+ subgraph "Needs `setup-test-env`";
+ 2_1-1 & 2_1-2 & 2_1-3 & 2_1-4 --> 1-6;
+ end
+
+ 2_2-1["static-analysis (17 minutes)"];
+ click 2_2-1 "https://app.periscopedata.com/app/gitlab/652085/Engineering-Productivity---Pipeline-Build-Durations?widget=6914471&udv=0"
+ 2_2-2["frontend-fixtures (17.2 minutes)"];
+ class 2_2-2 criticalPath;
+ click 2_2-2 "https://app.periscopedata.com/app/gitlab/652085/Engineering-Productivity---Pipeline-Build-Durations?widget=7910143&udv=0"
+ 2_2-3["frontend-fixtures-as-if-foss (8.75 minutes)"];
+ click 2_2-3 "https://app.periscopedata.com/app/gitlab/652085/Engineering-Productivity---Pipeline-Build-Durations?widget=7910154&udv=0"
+ 2_2-4["memory-on-boot (7.19 minutes)"];
+ click 2_2-4 "https://app.periscopedata.com/app/gitlab/652085/Engineering-Productivity---Pipeline-Build-Durations?widget=8356727&udv=0"
+ 2_2-5["webpack-dev-server (6.1 minutes)"];
+ click 2_2-5 "https://app.periscopedata.com/app/gitlab/652085/Engineering-Productivity---Pipeline-Build-Durations?widget=8404303&udv=0"
+ subgraph "Needs `setup-test-env` & `compile-assets`";
+ 2_2-1 & 2_2-2 & 2_2-4 & 2_2-5 --> 1-6 & 1-3;
+ 2_2-3 --> 1-6 & 1-4;
+ end
+
+ 2_3-1["build-assets-image (2.5 minutes)"];
+ subgraph "Needs `gitlab:assets:compile`";
+ 2_3-1 --> 1-5
+ end
+
+ 2_4-1["package-and-qa (manual)"];
+ subgraph "Needs `build-qa-image`";
+ 2_4-1 --> 1-2;
+ click 2_4-1 "https://app.periscopedata.com/app/gitlab/652085/Engineering-Productivity---Pipeline-Build-Durations?widget=6914305&udv=0"
+ end
+
+ 2_5-1["rspec & db jobs (12-22 minutes)"];
+ subgraph "Needs `compile-assets`, `setup-test-env`, & `retrieve-tests-metadata`";
+ 2_5-1 --> 1-3 & 1-6 & 1-14;
+ class 2_5-1 criticalPath;
+ click 2_5-1 "https://app.periscopedata.com/app/gitlab/652085/Engineering-Productivity---Pipeline-Build-Durations"
+ end
+
+ 3_1-1["jest (15 minutes)"];
+ class 3_1-1 criticalPath;
+ click 3_1-1 "https://app.periscopedata.com/app/gitlab/652085/Engineering-Productivity---Pipeline-Build-Durations?widget=6914204&udv=0"
+ 3_1-2["karma (8 minutes)"];
+ click 3_1-3 "https://app.periscopedata.com/app/gitlab/652085/Engineering-Productivity---Pipeline-Build-Durations?widget=6914200&udv=0"
+ 3_1-3["jest-as-if-foss (19.7 minutes)"];
+ click 3_1-3 "https://app.periscopedata.com/app/gitlab/652085/Engineering-Productivity---Pipeline-Build-Durations?widget=6914205&udv=0"
+ 3_1-4["karma-as-if-foss (7.5 minutes)"];
+ click 3_1-4 "https://app.periscopedata.com/app/gitlab/652085/Engineering-Productivity---Pipeline-Build-Durations?widget=6914203&udv=0"
+ subgraph "Needs `frontend-fixtures`";
+ 3_1-1 & 3_1-2 --> 2_2-2;
+ 3_1-3 & 3_1-4 --> 2_2-3;
+ end
+
+ 3_2-1["rspec:coverage (6.5 minutes)"];
+ subgraph "Depends on `rspec` jobs";
+ 3_2-1 -.->|"(don't use needs because of limitations)"| 2_5-1;
+ class 3_2-1 criticalPath;
+ click 3_2-1 "https://app.periscopedata.com/app/gitlab/652085/Engineering-Productivity---Pipeline-Build-Durations?widget=7248745&udv=0"
+ end
+
+ 4_1-1["coverage-frontend (3.6 minutes)"];
+ subgraph "Needs `jest`";
+ 4_1-1 --> 3_1-1;
+ class 4_1-1 criticalPath;
+ click 4_1-1 "https://app.periscopedata.com/app/gitlab/652085/Engineering-Productivity---Pipeline-Build-Durations?widget=7910777&udv=0"
+ end
+```
+
+### Frontend-only MR pipeline
+
+Reference pipeline: <https://gitlab.com/gitlab-org/gitlab/pipelines/134661039>
+
+```mermaid
+graph RL;
+ classDef criticalPath fill:#f66;
+
+ subgraph "No needed jobs";
+ 1-1["danger-review (3.5 minutes)"];
+ click 1-1 "https://app.periscopedata.com/app/gitlab/652085/Engineering-Productivity---Pipeline-Build-Durations?widget=8100542&udv=0"
+ 1-2["build-qa-image (3.4 minutes)"];
+ click 1-2 "https://app.periscopedata.com/app/gitlab/652085/Engineering-Productivity---Pipeline-Build-Durations?widget=6914325&udv=0"
+ 1-3["compile-assets pull-cache (9.06 minutes)"];
+ click 1-3 "https://app.periscopedata.com/app/gitlab/652085/Engineering-Productivity---Pipeline-Build-Durations?widget=6914317&udv=0"
+ 1-4["compile-assets pull-cache as-if-foss (8.35 minutes)"];
+ click 1-4 "https://app.periscopedata.com/app/gitlab/652085/Engineering-Productivity---Pipeline-Build-Durations?widget=8356616&udv=0"
+ 1-5["gitlab:assets:compile pull-cache (22 minutes)"];
+ click 1-5 "https://app.periscopedata.com/app/gitlab/652085/Engineering-Productivity---Pipeline-Build-Durations?widget=6914312&udv=0"
+ 1-6["setup-test-env (8.22 minutes)"];
+ click 1-6 "https://app.periscopedata.com/app/gitlab/652085/Engineering-Productivity---Pipeline-Build-Durations?widget=6914315&udv=0"
+ 1-7["review-stop-failed-deployment"];
+ 1-8["dependency_scanning"];
+ 1-9["qa:internal, qa:internal-as-if-foss"];
+ 1-11["qa:selectors, qa:selectors-as-if-foss"];
+ 1-14["retrieve-tests-metadata (1.5 minutes)"];
+ click 1-14 "https://app.periscopedata.com/app/gitlab/652085/Engineering-Productivity---Pipeline-Build-Durations?widget=8356697&udv=0"
+ 1-15["code_quality"];
+ 1-16["brakeman-sast"];
+ 1-17["eslint-sast"];
+ 1-18["kubesec-sast"];
+ 1-19["nodejs-scan-sast"];
+ 1-20["secrets-sast"];
+
+ class 1-3 criticalPath;
+ class 1-5 criticalPath;
+ class 1-6 criticalPath;
+ end
+
+ 2_1-1["graphql-reference-verify (5 minutes)"];
+ click 2_1-1 "https://app.periscopedata.com/app/gitlab/652085/Engineering-Productivity---Pipeline-Build-Durations?widget=8356715&udv=0"
+ 2_1-2["memory-static (4.75 minutes)"];
+ click 2_1-2 "https://app.periscopedata.com/app/gitlab/652085/Engineering-Productivity---Pipeline-Build-Durations?widget=8356721&udv=0"
+ 2_1-3["run-dev-fixtures (5 minutes)"];
+ click 2_1-3 "https://app.periscopedata.com/app/gitlab/652085/Engineering-Productivity---Pipeline-Build-Durations?widget=8356729&udv=0"
+ 2_1-4["run-dev-fixtures-ee (5 minutes)"];
+ click 2_1-4 "https://app.periscopedata.com/app/gitlab/652085/Engineering-Productivity---Pipeline-Build-Durations?widget=8356731&udv=0"
+ subgraph "Needs `setup-test-env`";
+ 2_1-1 & 2_1-2 & 2_1-3 & 2_1-4 --> 1-6;
+ end
+
+ 2_2-1["static-analysis (17 minutes)"];
+ click 2_2-1 "https://app.periscopedata.com/app/gitlab/652085/Engineering-Productivity---Pipeline-Build-Durations?widget=6914471&udv=0"
+ 2_2-2["frontend-fixtures (17.2 minutes)"];
+ class 2_2-2 criticalPath;
+ click 2_2-2 "https://app.periscopedata.com/app/gitlab/652085/Engineering-Productivity---Pipeline-Build-Durations?widget=7910143&udv=0"
+ 2_2-3["frontend-fixtures-as-if-foss (8.75 minutes)"];
+ click 2_2-3 "https://app.periscopedata.com/app/gitlab/652085/Engineering-Productivity---Pipeline-Build-Durations?widget=7910154&udv=0"
+ 2_2-4["memory-on-boot (7.19 minutes)"];
+ click 2_2-4 "https://app.periscopedata.com/app/gitlab/652085/Engineering-Productivity---Pipeline-Build-Durations?widget=8356727&udv=0"
+ 2_2-5["webpack-dev-server (6.1 minutes)"];
+ click 2_2-5 "https://app.periscopedata.com/app/gitlab/652085/Engineering-Productivity---Pipeline-Build-Durations?widget=8404303&udv=0"
+ subgraph "Needs `setup-test-env` & `compile-assets`";
+ 2_2-1 & 2_2-2 & 2_2-4 & 2_2-5 --> 1-6 & 1-3;
+ 2_2-3 --> 1-6 & 1-4;
+ end
+
+ 2_3-1["build-assets-image (2.5 minutes)"];
+ class 2_3-1 criticalPath;
+ subgraph "Needs `gitlab:assets:compile`";
+ 2_3-1 --> 1-5
+ end
-## Directed acyclic graph
+ 2_4-1["package-and-qa (manual)"];
+ subgraph "Needs `build-qa-image` & `build-assets-image`";
+ 2_4-1 --> 1-2 & 2_3-1;
+ click 2_4-1 "https://app.periscopedata.com/app/gitlab/652085/Engineering-Productivity---Pipeline-Build-Durations?widget=6914305&udv=0"
+ end
+
+ 2_5-1["rspec & db jobs (12-22 minutes)"];
+ subgraph "Needs `compile-assets`, `setup-test-env, & `retrieve-tests-metadata`";
+ 2_5-1 --> 1-3 & 1-6 & 1-14;
+ class 2_5-1 criticalPath;
+ click 2_5-1 "https://app.periscopedata.com/app/gitlab/652085/Engineering-Productivity---Pipeline-Build-Durations"
+ end
+
+ 2_6-1["review-build-cng (27.3 minutes)"];
+ subgraph "Needs `build-assets-image`";
+ 2_6-1 --> 2_3-1;
+ class 2_6-1 criticalPath;
+ click 2_6-1 "https://app.periscopedata.com/app/gitlab/652085/Engineering-Productivity---Pipeline-Build-Durations?widget=6914314&udv=0"
+ end
+
+ 3_1-1["jest (15 minutes)"];
+ class 3_1-1 criticalPath;
+ click 3_1-1 "https://app.periscopedata.com/app/gitlab/652085/Engineering-Productivity---Pipeline-Build-Durations?widget=6914204&udv=0"
+ 3_1-2["karma (8 minutes)"];
+ click 3_1-3 "https://app.periscopedata.com/app/gitlab/652085/Engineering-Productivity---Pipeline-Build-Durations?widget=6914200&udv=0"
+ 3_1-3["jest-as-if-foss (19.7 minutes)"];
+ click 3_1-3 "https://app.periscopedata.com/app/gitlab/652085/Engineering-Productivity---Pipeline-Build-Durations?widget=6914205&udv=0"
+ 3_1-4["karma-as-if-foss (7.5 minutes)"];
+ click 3_1-4 "https://app.periscopedata.com/app/gitlab/652085/Engineering-Productivity---Pipeline-Build-Durations?widget=6914203&udv=0"
+ subgraph "Needs `frontend-fixtures`";
+ 3_1-1 & 3_1-3 --> 2_2-2;
+ 3_1-2 & 3_1-4 --> 2_2-3;
+ end
+
+ 3_2-1["rspec:coverage (6.5 minutes)"];
+ subgraph "Depends on `rspec` jobs";
+ 3_2-1 -.->|"(don't use needs because of limitations)"| 2_5-1;
+ class 3_2-1 criticalPath;
+ click 3_2-1 "https://app.periscopedata.com/app/gitlab/652085/Engineering-Productivity---Pipeline-Build-Durations?widget=7248745&udv=0"
+ end
-We're using the [`needs:`](../ci/yaml/README.md#needs) keyword to
-execute jobs out of order for the following jobs:
+ 4_1-1["coverage-frontend (3.6 minutes)"];
+ subgraph "Needs `jest`";
+ 4_1-1 --> 3_1-1;
+ class 4_1-1 criticalPath;
+ click 4_1-1 "https://app.periscopedata.com/app/gitlab/652085/Engineering-Productivity---Pipeline-Build-Durations?widget=7910777&udv=0"
+ end
+
+ 3_3-1["review-deploy (6 minutes)"];
+ subgraph "Played by `review-build-cng`";
+ 3_3-1 --> 2_6-1;
+ class 3_3-1 criticalPath;
+ click 3_3-1 "https://app.periscopedata.com/app/gitlab/652085/Engineering-Productivity---Pipeline-Build-Durations?widget=6721130&udv=0"
+ end
+
+ 4_2-1["review-qa-smoke (8 minutes)"];
+ click 4_2-1 "https://app.periscopedata.com/app/gitlab/652085/Engineering-Productivity---Pipeline-Build-Durations?widget=6729805&udv=0"
+ 4_2-2["review-performance (4 minutes)"];
+ click 4_2-2 "https://app.periscopedata.com/app/gitlab/652085/Engineering-Productivity---Pipeline-Build-Durations?widget=8356817&udv=0"
+ 4_2-3["dast (18 minutes)"];
+ click 4_2-3 "https://app.periscopedata.com/app/gitlab/652085/Engineering-Productivity---Pipeline-Build-Durations?widget=8356819&udv=0"
+ class 4_2-3 criticalPath;
+ subgraph "Played by `review-deploy`";
+ 4_2-1 & 4_2-2 & 4_2-3 -.->|"(don't use needs because of limitations)"| 3_3-1;
+ end
+```
+
+### QA-only MR pipeline
+
+Reference pipeline: <https://gitlab.com/gitlab-org/gitlab/pipelines/134645109>
```mermaid
graph RL;
- A[setup-test-env];
- B["gitlab:assets:compile pull-push-cache<br/>(canonical master only)"];
- C["gitlab:assets:compile pull-cache<br/>(canonical default refs only)"];
- D["cache gems<br/>(master and tags only)"];
- E[review-build-cng];
- F[build-qa-image];
- G[review-deploy];
- I["karma, jest"];
- I2["karma-as-if-foss, jest-as-if-foss<br/>(EE default refs only)"];
- J["compile-assets pull-push-cache<br/>(master only)"];
- J2["compile-assets pull-push-cache as-if-foss<br/>(EE master only)"];
- K[compile-assets pull-cache];
- K2["compile-assets pull-cache as-if-foss<br/>(EE default refs only)"];
- U[frontend-fixtures];
- U2["frontend-fixtures-as-if-foss<br/>(EE default refs only)"];
- V["webpack-dev-server, static-analysis"];
- M[coverage];
- O[coverage-frontend];
- N["pages (master only)"];
- Q[package-and-qa];
- S["RSpec<br/>(e.g. rspec unit pg10)"]
- T[retrieve-tests-metadata];
- QA["qa:internal, qa:selectors"];
- QA2["qa:internal-as-if-foss, qa:selectors-as-if-foss<br/>(EE default refs only)"];
- X["docs lint, code_quality, sast, dependency_scanning, danger-review"];
-
-subgraph "`prepare` stage"
- A
- B
- C
- F
- K
- K2
- J
- J2
- T
- end
-
-subgraph "`fixture` stage"
- U -.-> |needs and depends on| A;
- U -.-> |needs and depends on| K;
- U2 -.-> |needs and depends on| A;
- U2 -.-> |needs and depends on| K2;
- end
-
-subgraph "`test` stage"
- D -.-> |needs| A;
- I -.-> |needs and depends on| U;
- I2 -.-> |needs and depends on| U2;
- L -.-> |needs and depends on| A;
- S -.-> |needs and depends on| A;
- S -.-> |needs and depends on| K;
- S -.-> |needs and depends on| T;
- L["db:*, gitlab:setup, graphql-docs-verify, downtime_check"] -.-> |needs| A;
- V -.-> |needs and depends on| K;
- X -.-> |needs| T;
- QA -.-> |needs| T;
- QA2 -.-> |needs| T;
- end
-
-subgraph "`post-test` stage"
- M --> |happens after| S
- O --> |needs `jest`| I
- end
-
-subgraph "`review-prepare` stage"
- E -.-> |needs| C;
- end
-
-subgraph "`review` stage"
- G -.-> |needs| E
- end
-
-subgraph "`qa` stage"
- Q -.-> |needs| C;
- Q -.-> |needs| F;
- QA1["review-qa-smoke, review-qa-all, review-performance, dast"] -.-> |needs| G;
- end
-
-subgraph "`post-qa` stage"
- PQA1["parallel-spec-reports"] -.-> |depends on `review-qa-all`| QA1;
+ classDef criticalPath fill:#f66;
+
+ subgraph "No needed jobs";
+ 1-1["danger-review (3.5 minutes)"];
+ click 1-1 "https://app.periscopedata.com/app/gitlab/652085/Engineering-Productivity---Pipeline-Build-Durations?widget=8100542&udv=0"
+ 1-2["build-qa-image (3.4 minutes)"];
+ click 1-2 "https://app.periscopedata.com/app/gitlab/652085/Engineering-Productivity---Pipeline-Build-Durations?widget=6914325&udv=0"
+ 1-3["compile-assets pull-cache (9.06 minutes)"];
+ click 1-3 "https://app.periscopedata.com/app/gitlab/652085/Engineering-Productivity---Pipeline-Build-Durations?widget=6914317&udv=0"
+ 1-4["compile-assets pull-cache as-if-foss (8.35 minutes)"];
+ click 1-4 "https://app.periscopedata.com/app/gitlab/652085/Engineering-Productivity---Pipeline-Build-Durations?widget=8356616&udv=0"
+ 1-5["gitlab:assets:compile pull-cache (22 minutes)"];
+ click 1-5 "https://app.periscopedata.com/app/gitlab/652085/Engineering-Productivity---Pipeline-Build-Durations?widget=6914312&udv=0"
+ 1-6["setup-test-env (8.22 minutes)"];
+ click 1-6 "https://app.periscopedata.com/app/gitlab/652085/Engineering-Productivity---Pipeline-Build-Durations?widget=6914315&udv=0"
+ 1-7["review-stop-failed-deployment"];
+ 1-8["dependency_scanning"];
+ 1-9["qa:internal, qa:internal-as-if-foss"];
+ 1-11["qa:selectors, qa:selectors-as-if-foss"];
+ 1-14["retrieve-tests-metadata (1.5 minutes)"];
+ click 1-14 "https://app.periscopedata.com/app/gitlab/652085/Engineering-Productivity---Pipeline-Build-Durations?widget=8356697&udv=0"
+ 1-15["code_quality"];
+ 1-16["brakeman-sast"];
+ 1-17["eslint-sast"];
+ 1-18["kubesec-sast"];
+ 1-19["nodejs-scan-sast"];
+ 1-20["secrets-sast"];
+
+ class 1-5 criticalPath;
+ end
+
+ 2_1-1["graphql-reference-verify (5 minutes)"];
+ click 2_1-1 "https://app.periscopedata.com/app/gitlab/652085/Engineering-Productivity---Pipeline-Build-Durations?widget=8356715&udv=0"
+ subgraph "Needs `setup-test-env`";
+ 2_1-1 --> 1-6;
+ end
+
+ 2_2-1["static-analysis (17 minutes)"];
+ click 2_2-1 "https://app.periscopedata.com/app/gitlab/652085/Engineering-Productivity---Pipeline-Build-Durations?widget=6914471&udv=0"
+ subgraph "Needs `setup-test-env` & `compile-assets`";
+ 2_2-1 --> 1-6 & 1-3;
+ end
+
+ 2_3-1["build-assets-image (2.5 minutes)"];
+ subgraph "Needs `gitlab:assets:compile`";
+ 2_3-1 --> 1-5
+ class 2_3-1 criticalPath;
end
-subgraph "`pages` stage"
- N -.-> |depends on| C;
- N -.-> |depends on karma| I;
- N -.-> |depends on| M;
- N --> |happens after| PQA1
- end
+ 2_4-1["package-and-qa (108 minutes)"];
+ subgraph "Needs `build-qa-image` & `build-assets-image`";
+ 2_4-1 --> 1-2 & 2_3-1;
+ class 2_4-1 criticalPath;
+ click 2_4-1 "https://app.periscopedata.com/app/gitlab/652085/Engineering-Productivity---Pipeline-Build-Durations?widget=6914305&udv=0"
+ end
```
## Test jobs
@@ -268,6 +516,87 @@ for more information.
Consult the [Review Apps](testing_guide/review_apps.md) dedicated page for more information.
+## As-if-FOSS jobs
+
+The `* as-if-foss` jobs allows to run GitLab's test suite "as-if-FOSS", meaning as if the jobs would run in the context
+of the `gitlab-org/gitlab-foss` project. These jobs are only created in the following cases:
+
+- `master` commits (pushes and scheduled pipelines).
+- `gitlab-org/security/gitlab` merge requests.
+- Merge requests which include `RUN AS-IF-FOSS` in their title.
+- Merge requests that changes the CI config.
+
+The `* as-if-foss` jobs have the `FOSS_ONLY='1'` variable set and gets their EE-specific
+folders removed before the tests start running.
+
+The intent is to ensure that a change won't introduce a failure once the `gitlab-org/gitlab` project will be synced to
+the `gitlab-org/gitlab-foss` project.
+
+## Pre-clone step
+
+The `gitlab-org/gitlab` project on GitLab.com uses a [pre-clone step](https://gitlab.com/gitlab-org/gitlab/issues/39134)
+to seed the project with a recent archive of the repository. This is done for
+several reasons:
+
+- It speeds up builds because a 800 MB download only takes seconds, as opposed to a full Git clone.
+- It significantly reduces load on the file server, as smaller deltas mean less time spent in `git pack-objects`.
+
+The pre-clone step works by using the `CI_PRE_CLONE_SCRIPT` variable
+[defined by GitLab.com shared runners](../user/gitlab_com/index.md#pre-clone-script).
+
+The `CI_PRE_CLONE_SCRIPT` is currently defined as a project CI/CD
+variable:
+
+```shell
+echo "Downloading archived master..."
+wget -O /tmp/gitlab.tar.gz https://storage.googleapis.com/gitlab-ci-git-repo-cache/project-278964/gitlab-master.tar.gz
+
+if [ ! -f /tmp/gitlab.tar.gz ]; then
+ echo "Repository cache not available, cloning a new directory..."
+ exit
+fi
+
+rm -rf $CI_PROJECT_DIR
+echo "Extracting tarball into $CI_PROJECT_DIR..."
+mkdir -p $CI_PROJECT_DIR
+cd $CI_PROJECT_DIR
+tar xzf /tmp/gitlab.tar.gz
+rm -f /tmp/gitlab.tar.gz
+chmod a+w $CI_PROJECT_DIR
+```
+
+The first step of the script downloads `gitlab-master.tar.gz` from
+Google Cloud Storage. There is a [GitLab CI job named `cache-repo`](https://gitlab.com/gitlab-org/gitlab/blob/master/.gitlab/ci/cache-repo.gitlab-ci.yml#L5)
+that is responsible for keeping that archive up-to-date. Every two hours
+on a scheduled pipeline, it does the following:
+
+1. Creates a fresh clone of the `gitlab-org/gitlab` repository on GitLab.com.
+1. Saves the data as a `.tar.gz`.
+1. Uploads it into the Google Cloud Storage bucket.
+
+When a CI job runs with this configuration, you'll see something like
+this:
+
+```shell
+$ eval "$CI_PRE_CLONE_SCRIPT"
+Downloading archived master...
+Extracting tarball into /builds/group/project...
+Fetching changes...
+Reinitialized existing Git repository in /builds/group/project/.git/
+```
+
+Note that the `Reinitialized existing Git repository` message shows that
+the pre-clone step worked. The runner runs `git init`, which
+overwrites the Git configuration with the appropriate settings to fetch
+from the GitLab repository.
+
+`CI_REPO_CACHE_CREDENTIALS` contains the Google Cloud service account
+JSON for uploading to the `gitlab-ci-git-repo-cache` bucket. These
+credentials are stored in the 1Password GitLab.com Production vault.
+
+Note that this bucket should be located in the same continent as the
+runner, or [network egress charges will apply](https://cloud.google.com/storage/pricing).
+
---
[Return to Development documentation](README.md)
diff --git a/doc/development/policies.md b/doc/development/policies.md
index 4d045411156..62442de825a 100644
--- a/doc/development/policies.md
+++ b/doc/development/policies.md
@@ -95,7 +95,7 @@ Each line represents a rule that was evaluated. There are a few things to note:
Here you can see that the first four rules were evaluated `false` for
which user and subject. For example, you can see in the last line that
-the rule was activated because the user `root` had Reporter access to
+the rule was activated because the user `john` had Reporter access to
`Project/4`.
When a policy is asked whether a particular ability is allowed
@@ -123,7 +123,7 @@ class FooPolicy < BasePolicy
end
```
-Naively, if we call `Ability.can?(user1, :some_ability, foo)` and `Ability.can?(user2, :some_ability, foo)`, we would have to calculate the condition twice - since they are for different users. But if we use the `scope: :subject` option:
+Naively, if we call `Ability.allowed?(user1, :some_ability, foo)` and `Ability.allowed?(user2, :some_ability, foo)`, we would have to calculate the condition twice - since they are for different users. But if we use the `scope: :subject` option:
```ruby
condition(:expensive_condition, scope: :subject) { @subject.expensive_query? }
diff --git a/doc/development/polymorphic_associations.md b/doc/development/polymorphic_associations.md
index 5d69c8add38..b6567704d8e 100644
--- a/doc/development/polymorphic_associations.md
+++ b/doc/development/polymorphic_associations.md
@@ -4,7 +4,7 @@
Rails makes it possible to define so called "polymorphic associations". This
usually works by adding two columns to a table: a target type column, and a
-target id. For example, at the time of writing we have such a setup for
+target ID. For example, at the time of writing we have such a setup for
`members` with the following columns:
- `source_type`: a string defining the model to use, can be either `Project` or
diff --git a/doc/development/profiling.md b/doc/development/profiling.md
index 04713055117..2589329fc83 100644
--- a/doc/development/profiling.md
+++ b/doc/development/profiling.md
@@ -87,7 +87,7 @@ printer.print(File.open('/tmp/profile.html', 'w'))
[GitLab-Profiler](https://gitlab.com/gitlab-com/gitlab-profiler) is a project
that builds on this to add some additional niceties, such as allowing
-configuration with a single Yaml file for multiple URLs, and uploading of the
+configuration with a single YAML file for multiple URLs, and uploading of the
profile and log output to S3.
For GitLab.com, you can find the latest results here (restricted to GitLab Team members only):
diff --git a/doc/development/query_recorder.md b/doc/development/query_recorder.md
index f2c5b8b9848..b28fdb1252b 100644
--- a/doc/development/query_recorder.md
+++ b/doc/development/query_recorder.md
@@ -47,71 +47,71 @@ This could lead to false successes where subsequent "requests" could have querie
There are multiple ways to find the source of queries.
-1. The `QueryRecorder` `data` attribute stores queries by `file_name:line_number:method_name`.
- Each entry is a `hash` with the following fields:
-
- - `count`: the number of times a query from this `file_name:line_number:method_name` was called
- - `occurrences`: the actual `SQL` of each call
- - `backtrace`: the stack trace of each call (if either of the two following options were enabled)
-
- `QueryRecorder#find_query` allows filtering queries by their `file_name:line_number:method_name` and
- `count` attributes. For example:
-
- ```ruby
- control = ActiveRecord::QueryRecorder.new(skip_cached: false) { visit_some_page }
- control.find_query(/.*note.rb.*/, 0, first_only: true)
- ```
-
- `QueryRecorder#occurrences_by_line_method` returns a sorted array based on `data`, sorted by `count`.
-
-1. You can output the call backtrace for the specific `QueryRecorder` instance you want
- by using `ActiveRecord::QueryRecorder.new(query_recorder_debug: true)`. The output
- will be in `test.log`
-
-1. Using the environment variable `QUERY_RECORDER_DEBUG`, the call backtrace will be output for all tests.
-
- To enable this, run the specs with the `QUERY_RECORDER_DEBUG` environment variable set. For example:
-
- ```shell
- QUERY_RECORDER_DEBUG=1 bundle exec rspec spec/requests/api/projects_spec.rb
- ```
-
- This will log calls to QueryRecorder into the `test.log` file. For example:
-
- ```plaintext
- QueryRecorder SQL: SELECT COUNT(*) FROM "issues" WHERE "issues"."deleted_at" IS NULL AND "issues"."project_id" = $1 AND ("issues"."state" IN ('opened')) AND "issues"."confidential" = $2
- --> /home/user/gitlab/gdk/gitlab/spec/support/query_recorder.rb:19:in `callback'
- --> /home/user/.rbenv/versions/2.3.5/lib/ruby/gems/2.3.0/gems/activesupport-4.2.8/lib/active_support/notifications/fanout.rb:127:in `finish'
- --> /home/user/.rbenv/versions/2.3.5/lib/ruby/gems/2.3.0/gems/activesupport-4.2.8/lib/active_support/notifications/fanout.rb:46:in `block in finish'
- --> /home/user/.rbenv/versions/2.3.5/lib/ruby/gems/2.3.0/gems/activesupport-4.2.8/lib/active_support/notifications/fanout.rb:46:in `each'
- --> /home/user/.rbenv/versions/2.3.5/lib/ruby/gems/2.3.0/gems/activesupport-4.2.8/lib/active_support/notifications/fanout.rb:46:in `finish'
- --> /home/user/.rbenv/versions/2.3.5/lib/ruby/gems/2.3.0/gems/activesupport-4.2.8/lib/active_support/notifications/instrumenter.rb:36:in `finish'
- --> /home/user/.rbenv/versions/2.3.5/lib/ruby/gems/2.3.0/gems/activesupport-4.2.8/lib/active_support/notifications/instrumenter.rb:25:in `instrument'
- --> /home/user/.rbenv/versions/2.3.5/lib/ruby/gems/2.3.0/gems/activerecord-4.2.8/lib/active_record/connection_adapters/abstract_adapter.rb:478:in `log'
- --> /home/user/.rbenv/versions/2.3.5/lib/ruby/gems/2.3.0/gems/activerecord-4.2.8/lib/active_record/connection_adapters/postgresql_adapter.rb:601:in `exec_cache'
- --> /home/user/.rbenv/versions/2.3.5/lib/ruby/gems/2.3.0/gems/activerecord-4.2.8/lib/active_record/connection_adapters/postgresql_adapter.rb:585:in `execute_and_clear'
- --> /home/user/.rbenv/versions/2.3.5/lib/ruby/gems/2.3.0/gems/activerecord-4.2.8/lib/active_record/connection_adapters/postgresql/database_statements.rb:160:in `exec_query'
- --> /home/user/.rbenv/versions/2.3.5/lib/ruby/gems/2.3.0/gems/activerecord-4.2.8/lib/active_record/connection_adapters/abstract/database_statements.rb:356:in `select'
- --> /home/user/.rbenv/versions/2.3.5/lib/ruby/gems/2.3.0/gems/activerecord-4.2.8/lib/active_record/connection_adapters/abstract/database_statements.rb:32:in `select_all'
- --> /home/user/.rbenv/versions/2.3.5/lib/ruby/gems/2.3.0/gems/activerecord-4.2.8/lib/active_record/connection_adapters/abstract/query_cache.rb:68:in `block in select_all'
- --> /home/user/.rbenv/versions/2.3.5/lib/ruby/gems/2.3.0/gems/activerecord-4.2.8/lib/active_record/connection_adapters/abstract/query_cache.rb:83:in `cache_sql'
- --> /home/user/.rbenv/versions/2.3.5/lib/ruby/gems/2.3.0/gems/activerecord-4.2.8/lib/active_record/connection_adapters/abstract/query_cache.rb:68:in `select_all'
- --> /home/user/.rbenv/versions/2.3.5/lib/ruby/gems/2.3.0/gems/activerecord-4.2.8/lib/active_record/relation/calculations.rb:270:in `execute_simple_calculation'
- --> /home/user/.rbenv/versions/2.3.5/lib/ruby/gems/2.3.0/gems/activerecord-4.2.8/lib/active_record/relation/calculations.rb:227:in `perform_calculation'
- --> /home/user/.rbenv/versions/2.3.5/lib/ruby/gems/2.3.0/gems/activerecord-4.2.8/lib/active_record/relation/calculations.rb:133:in `calculate'
- --> /home/user/.rbenv/versions/2.3.5/lib/ruby/gems/2.3.0/gems/activerecord-4.2.8/lib/active_record/relation/calculations.rb:48:in `count'
- --> /home/user/gitlab/gdk/gitlab/app/services/base_count_service.rb:20:in `uncached_count'
- --> /home/user/gitlab/gdk/gitlab/app/services/base_count_service.rb:12:in `block in count'
- --> /home/user/.rbenv/versions/2.3.5/lib/ruby/gems/2.3.0/gems/activesupport-4.2.8/lib/active_support/cache.rb:299:in `block in fetch'
- --> /home/user/.rbenv/versions/2.3.5/lib/ruby/gems/2.3.0/gems/activesupport-4.2.8/lib/active_support/cache.rb:585:in `block in save_block_result_to_cache'
- --> /home/user/.rbenv/versions/2.3.5/lib/ruby/gems/2.3.0/gems/activesupport-4.2.8/lib/active_support/cache.rb:547:in `block in instrument'
- --> /home/user/.rbenv/versions/2.3.5/lib/ruby/gems/2.3.0/gems/activesupport-4.2.8/lib/active_support/notifications.rb:166:in `instrument'
- --> /home/user/.rbenv/versions/2.3.5/lib/ruby/gems/2.3.0/gems/activesupport-4.2.8/lib/active_support/cache.rb:547:in `instrument'
- --> /home/user/.rbenv/versions/2.3.5/lib/ruby/gems/2.3.0/gems/activesupport-4.2.8/lib/active_support/cache.rb:584:in `save_block_result_to_cache'
- --> /home/user/.rbenv/versions/2.3.5/lib/ruby/gems/2.3.0/gems/activesupport-4.2.8/lib/active_support/cache.rb:299:in `fetch'
- --> /home/user/gitlab/gdk/gitlab/app/services/base_count_service.rb:12:in `count'
- --> /home/user/gitlab/gdk/gitlab/app/models/project.rb:1296:in `open_issues_count'
- ```
+- Inspect the `QueryRecorder` `data` attribute. It stores queries by `file_name:line_number:method_name`.
+ Each entry is a `hash` with the following fields:
+
+ - `count`: the number of times a query from this `file_name:line_number:method_name` was called
+ - `occurrences`: the actual `SQL` of each call
+ - `backtrace`: the stack trace of each call (if either of the two following options were enabled)
+
+ `QueryRecorder#find_query` allows filtering queries by their `file_name:line_number:method_name` and
+ `count` attributes. For example:
+
+ ```ruby
+ control = ActiveRecord::QueryRecorder.new(skip_cached: false) { visit_some_page }
+ control.find_query(/.*note.rb.*/, 0, first_only: true)
+ ```
+
+ `QueryRecorder#occurrences_by_line_method` returns a sorted array based on `data`, sorted by `count`.
+
+- View the call backtrace for the specific `QueryRecorder` instance you want
+ by using `ActiveRecord::QueryRecorder.new(query_recorder_debug: true)`. The output
+ will be in `test.log`
+
+- Enable the call backtrace for all tests using the `QUERY_RECORDER_DEBUG` environment variable.
+
+ To enable this, run the specs with the `QUERY_RECORDER_DEBUG` environment variable set. For example:
+
+ ```shell
+ QUERY_RECORDER_DEBUG=1 bundle exec rspec spec/requests/api/projects_spec.rb
+ ```
+
+ This will log calls to QueryRecorder into the `test.log` file. For example:
+
+ ```sql
+ QueryRecorder SQL: SELECT COUNT(*) FROM "issues" WHERE "issues"."deleted_at" IS NULL AND "issues"."project_id" = $1 AND ("issues"."state" IN ('opened')) AND "issues"."confidential" = $2
+ --> /home/user/gitlab/gdk/gitlab/spec/support/query_recorder.rb:19:in `callback'
+ --> /home/user/.rbenv/versions/2.3.5/lib/ruby/gems/2.3.0/gems/activesupport-4.2.8/lib/active_support/notifications/fanout.rb:127:in `finish'
+ --> /home/user/.rbenv/versions/2.3.5/lib/ruby/gems/2.3.0/gems/activesupport-4.2.8/lib/active_support/notifications/fanout.rb:46:in `block in finish'
+ --> /home/user/.rbenv/versions/2.3.5/lib/ruby/gems/2.3.0/gems/activesupport-4.2.8/lib/active_support/notifications/fanout.rb:46:in `each'
+ --> /home/user/.rbenv/versions/2.3.5/lib/ruby/gems/2.3.0/gems/activesupport-4.2.8/lib/active_support/notifications/fanout.rb:46:in `finish'
+ --> /home/user/.rbenv/versions/2.3.5/lib/ruby/gems/2.3.0/gems/activesupport-4.2.8/lib/active_support/notifications/instrumenter.rb:36:in `finish'
+ --> /home/user/.rbenv/versions/2.3.5/lib/ruby/gems/2.3.0/gems/activesupport-4.2.8/lib/active_support/notifications/instrumenter.rb:25:in `instrument'
+ --> /home/user/.rbenv/versions/2.3.5/lib/ruby/gems/2.3.0/gems/activerecord-4.2.8/lib/active_record/connection_adapters/abstract_adapter.rb:478:in `log'
+ --> /home/user/.rbenv/versions/2.3.5/lib/ruby/gems/2.3.0/gems/activerecord-4.2.8/lib/active_record/connection_adapters/postgresql_adapter.rb:601:in `exec_cache'
+ --> /home/user/.rbenv/versions/2.3.5/lib/ruby/gems/2.3.0/gems/activerecord-4.2.8/lib/active_record/connection_adapters/postgresql_adapter.rb:585:in `execute_and_clear'
+ --> /home/user/.rbenv/versions/2.3.5/lib/ruby/gems/2.3.0/gems/activerecord-4.2.8/lib/active_record/connection_adapters/postgresql/database_statements.rb:160:in `exec_query'
+ --> /home/user/.rbenv/versions/2.3.5/lib/ruby/gems/2.3.0/gems/activerecord-4.2.8/lib/active_record/connection_adapters/abstract/database_statements.rb:356:in `select'
+ --> /home/user/.rbenv/versions/2.3.5/lib/ruby/gems/2.3.0/gems/activerecord-4.2.8/lib/active_record/connection_adapters/abstract/database_statements.rb:32:in `select_all'
+ --> /home/user/.rbenv/versions/2.3.5/lib/ruby/gems/2.3.0/gems/activerecord-4.2.8/lib/active_record/connection_adapters/abstract/query_cache.rb:68:in `block in select_all'
+ --> /home/user/.rbenv/versions/2.3.5/lib/ruby/gems/2.3.0/gems/activerecord-4.2.8/lib/active_record/connection_adapters/abstract/query_cache.rb:83:in `cache_sql'
+ --> /home/user/.rbenv/versions/2.3.5/lib/ruby/gems/2.3.0/gems/activerecord-4.2.8/lib/active_record/connection_adapters/abstract/query_cache.rb:68:in `select_all'
+ --> /home/user/.rbenv/versions/2.3.5/lib/ruby/gems/2.3.0/gems/activerecord-4.2.8/lib/active_record/relation/calculations.rb:270:in `execute_simple_calculation'
+ --> /home/user/.rbenv/versions/2.3.5/lib/ruby/gems/2.3.0/gems/activerecord-4.2.8/lib/active_record/relation/calculations.rb:227:in `perform_calculation'
+ --> /home/user/.rbenv/versions/2.3.5/lib/ruby/gems/2.3.0/gems/activerecord-4.2.8/lib/active_record/relation/calculations.rb:133:in `calculate'
+ --> /home/user/.rbenv/versions/2.3.5/lib/ruby/gems/2.3.0/gems/activerecord-4.2.8/lib/active_record/relation/calculations.rb:48:in `count'
+ --> /home/user/gitlab/gdk/gitlab/app/services/base_count_service.rb:20:in `uncached_count'
+ --> /home/user/gitlab/gdk/gitlab/app/services/base_count_service.rb:12:in `block in count'
+ --> /home/user/.rbenv/versions/2.3.5/lib/ruby/gems/2.3.0/gems/activesupport-4.2.8/lib/active_support/cache.rb:299:in `block in fetch'
+ --> /home/user/.rbenv/versions/2.3.5/lib/ruby/gems/2.3.0/gems/activesupport-4.2.8/lib/active_support/cache.rb:585:in `block in save_block_result_to_cache'
+ --> /home/user/.rbenv/versions/2.3.5/lib/ruby/gems/2.3.0/gems/activesupport-4.2.8/lib/active_support/cache.rb:547:in `block in instrument'
+ --> /home/user/.rbenv/versions/2.3.5/lib/ruby/gems/2.3.0/gems/activesupport-4.2.8/lib/active_support/notifications.rb:166:in `instrument'
+ --> /home/user/.rbenv/versions/2.3.5/lib/ruby/gems/2.3.0/gems/activesupport-4.2.8/lib/active_support/cache.rb:547:in `instrument'
+ --> /home/user/.rbenv/versions/2.3.5/lib/ruby/gems/2.3.0/gems/activesupport-4.2.8/lib/active_support/cache.rb:584:in `save_block_result_to_cache'
+ --> /home/user/.rbenv/versions/2.3.5/lib/ruby/gems/2.3.0/gems/activesupport-4.2.8/lib/active_support/cache.rb:299:in `fetch'
+ --> /home/user/gitlab/gdk/gitlab/app/services/base_count_service.rb:12:in `count'
+ --> /home/user/gitlab/gdk/gitlab/app/models/project.rb:1296:in `open_issues_count'
+ ```
## See also
diff --git a/doc/development/rake_tasks.md b/doc/development/rake_tasks.md
index 61d5277b1e6..96acce5e4df 100644
--- a/doc/development/rake_tasks.md
+++ b/doc/development/rake_tasks.md
@@ -1,8 +1,10 @@
# Rake tasks for developers
-## Set up db with developer seeds
+Rake tasks are available for developers and others contributing to GitLab.
-Note that if your db user does not have advanced privileges you must create the db manually before running this command.
+## Set up database with developer seeds
+
+Note that if your database user does not have advanced privileges, you must create the database manually before running this command.
```shell
bundle exec rake setup
@@ -12,13 +14,15 @@ The `setup` task is an alias for `gitlab:setup`.
This tasks calls `db:reset` to create the database, and calls `db:seed_fu` to seed the database.
Note: `db:setup` calls `db:seed` but this does nothing.
-### Env variables
+### Environment variables
**MASS_INSERT**: Create millions of users (2m), projects (5m) and its
relations. It's highly recommended to run the seed with it to catch slow queries
while developing. Expect the process to take up to 20 extra minutes.
-**LARGE_PROJECTS**: Create large projects (through import) from a predefined set of urls.
+See also [Mass inserting Rails models](mass_insert.md).
+
+**LARGE_PROJECTS**: Create large projects (through import) from a predefined set of URLs.
### Seeding issues for all or a given project
@@ -86,7 +90,7 @@ echo 'yes' | bundle exec rake setup
To save you from answering `yes` manually.
-### Discard stdout
+### Discard `stdout`
Since the script would print a lot of information, it could be slowing down
your terminal, and it would generate more than 20G logs if you just redirect
@@ -97,8 +101,8 @@ it to a file. If we don't care about the output, we could just redirect it to
echo 'yes' | bundle exec rake setup > /dev/null
```
-Note that since you can't see the questions from stdout, you might just want
-to `echo 'yes'` to keep it running. It would still print the errors on stderr
+Note that since you can't see the questions from `stdout`, you might just want
+to `echo 'yes'` to keep it running. It would still print the errors on `stderr`
so no worries about missing errors.
### Extra Project seed options
@@ -113,33 +117,35 @@ There are a few environment flags you can pass to change how projects are seeded
In order to run the test you can use the following commands:
-- `bin/rake spec` to run the rspec suite
+- `bin/rake spec` to run the RSpec suite
- `bin/rake spec:unit` to run only the unit tests
- `bin/rake spec:integration` to run only the integration tests
- `bin/rake spec:system` to run only the system tests
- `bin/rake karma` to run the Karma test suite
-Note: `bin/rake spec` takes significant time to pass.
-Instead of running full test suite locally you can save a lot of time by running
-a single test or directory related to your changes. After you submit merge request
+`bin/rake spec` takes significant time to pass.
+Instead of running the full test suite locally, you can save a lot of time by running
+a single test or directory related to your changes. After you submit a merge request,
CI will run full test suite for you. Green CI status in the merge request means
full test suite is passed.
-Note: You can't run `rspec .` since this will try to run all the `_spec.rb`
+You can't run `rspec .` since this will try to run all the `_spec.rb`
files it can find, also the ones in `/tmp`
-Note: You can pass RSpec command line options to the `spec:unit`,
-`spec:integration`, and `spec:system` tasks, e.g. `bin/rake "spec:unit[--tag ~geo --dry-run]"`.
+You can pass RSpec command line options to the `spec:unit`,
+`spec:integration`, and `spec:system` tasks. For example, `bin/rake "spec:unit[--tag ~geo --dry-run]"`.
-To run a single test file you can use:
+For an RSpec test, to run a single test file you can run:
-- `bin/rspec spec/controllers/commit_controller_spec.rb` for a rspec test
+```shell
+bin/rspec spec/controllers/commit_controller_spec.rb
+```
To run several tests inside one directory:
-- `bin/rspec spec/requests/api/` for the rspec tests if you want to test API only
+- `bin/rspec spec/requests/api/` for the RSpec tests if you want to test API only
-### Speed-up tests, Rake tasks, and migrations
+### Speed up tests, Rake tasks, and migrations
[Spring](https://github.com/rails/spring) is a Rails application preloader. It
speeds up development by keeping your application running in the background so
@@ -172,18 +178,16 @@ This will compile and minify all JavaScript and CSS assets and copy them along
with all other frontend assets (images, fonts, etc) into `/public/assets` where
they can be easily inspected.
-## Updating Emoji Aliases
+## Emoji tasks
-To update the Emoji aliases file (used for Emoji autocomplete) you must run the
+To update the Emoji aliases file (used for Emoji autocomplete), run the
following:
```shell
bundle exec rake gemojione:aliases
```
-## Updating Emoji Digests
-
-To update the Emoji digests file (used for Emoji autocomplete) you must run the
+To update the Emoji digests file (used for Emoji autocomplete), run the
following:
```shell
@@ -193,9 +197,7 @@ bundle exec rake gemojione:digests
This will update the file `fixtures/emojis/digests.json` based on the currently
available Emoji.
-## Emoji Sprites
-
-Generating a sprite file containing all the Emoji can be done by running:
+To generate a sprite file containing all the Emoji, run:
```shell
bundle exec rake gemojione:sprite
@@ -206,7 +208,7 @@ such changes, first generate the `emoji.png` spritesheet with the above Rake
task, then check the dimensions of the new spritesheet and update the
`SPRITESHEET_WIDTH` and `SPRITESHEET_HEIGHT` constants accordingly.
-## Updating project templates
+## Update project templates
Starting a project from a template needs this project to be exported. On a
up to date master branch run:
@@ -249,7 +251,7 @@ bundle exec rake db:obsolete_ignored_columns
Feel free to remove their definitions from their `ignored_columns` definitions.
-## Update GraphQL Documentation and Schema definitions
+## Update GraphQL documentation and schema definitions
To generate GraphQL documentation based on the GitLab schema, run:
diff --git a/doc/development/reactive_caching.md b/doc/development/reactive_caching.md
index bc5fbb58af9..f3386305e93 100644
--- a/doc/development/reactive_caching.md
+++ b/doc/development/reactive_caching.md
@@ -85,6 +85,9 @@ The ReactiveCaching concern can be used in models as well as `project_services`
1. Implement the `calculate_reactive_cache` method in your model/service.
1. Call `with_reactive_cache` in your model/service where the cached value is needed.
+1. If the `calculate_reactive_cache` method above submits requests to external services
+(e.g. Prometheus, K8s), make sure to change the
+[`reactive_cache_work_type` accordingly](#selfreactive_cache_work_type).
### In controllers
@@ -244,6 +247,13 @@ and will silently raise `ReactiveCaching::ExceededReactiveCacheLimit` on Sentry.
self.reactive_cache_hard_limit = 5.megabytes
```
+#### `self.reactive_cache_work_type`
+
+- This is the type of work performed by the `calculate_reactive_cache` method. Based on this attribute,
+it's able to pick the right worker to process the caching job. Make sure to
+set it as `:external_dependency` if the work performs any external request
+(e.g. Kubernetes, Sentry).
+
#### `self.reactive_cache_worker_finder`
- This is the method used by the background worker to find or generate the object on
@@ -256,7 +266,7 @@ which `calculate_reactive_cache` can be called.
end
```
-- The default behaviour can be overridden by defining a custom `reactive_cache_worker_finder`.
+- The default behavior can be overridden by defining a custom `reactive_cache_worker_finder`.
```ruby
class Foo < ApplicationRecord
diff --git a/doc/development/repository_mirroring.md b/doc/development/repository_mirroring.md
index 0a0c91821cf..1d4dbe88399 100644
--- a/doc/development/repository_mirroring.md
+++ b/doc/development/repository_mirroring.md
@@ -2,9 +2,10 @@
## Deep Dive
-In December 2018, Tiago Botelho hosted a [Deep Dive] on GitLab's [Pull Repository Mirroring functionality] to share his domain specific knowledge with anyone who may work in this part of the code base in the future. You can find the [recording on YouTube], and the slides in [PDF]. Everything covered in this deep dive was accurate as of GitLab 11.6, and while specific details may have changed since then, it should still serve as a good introduction.
-
-[Deep Dive]: https://gitlab.com/gitlab-org/create-stage/issues/1
-[Pull Repository Mirroring functionality]: ../user/project/repository/repository_mirroring.md#pulling-from-a-remote-repository-starter
-[recording on YouTube]: https://www.youtube.com/watch?v=sSZq0fpdY-Y
-[PDF]: https://gitlab.com/gitlab-org/create-stage/uploads/8693404888a941fd851f8a8ecdec9675/Gitlab_Create_-_Pull_Mirroring_Deep_Dive.pdf
+In December 2018, Tiago Botelho hosted a Deep Dive (GitLab team members only: `https://gitlab.com/gitlab-org/create-stage/issues/1`)
+on GitLab's [Pull Repository Mirroring functionality](../user/project/repository/repository_mirroring.md#pulling-from-a-remote-repository-starter)
+to share his domain specific knowledge with anyone who may work in this part of the
+code base in the future. You can find the [recording on YouTube](https://www.youtube.com/watch?v=sSZq0fpdY-Y),
+and the slides in [PDF](https://gitlab.com/gitlab-org/create-stage/uploads/8693404888a941fd851f8a8ecdec9675/Gitlab_Create_-_Pull_Mirroring_Deep_Dive.pdf).
+Everything covered in this deep dive was accurate as of GitLab 11.6, and while specific
+details may have changed since then, it should still serve as a good introduction.
diff --git a/doc/development/scalability.md b/doc/development/scalability.md
index 9607bce3bc4..ba25e169d66 100644
--- a/doc/development/scalability.md
+++ b/doc/development/scalability.md
@@ -214,7 +214,7 @@ Redis process.
Single-core: Like PgBouncer, a single Redis process can only use one
core. It does not support multi-threading.
-Dumb secondaries: Redis secondaries (aka slaves) don't actually
+Dumb secondaries: Redis secondaries (aka replicas) don't actually
handle any load. Unlike PostgreSQL secondaries, they don't even serve
read queries. They simply replicate data from the primary and take over
only when the primary fails.
@@ -231,8 +231,8 @@ election to determine a new leader.
No leader: A Redis cluster can get into a mode where there are no
primaries. For example, this can happen if Redis nodes are misconfigured
to follow the wrong node. Sometimes this requires forcing one node to
-become a primary via the [`SLAVEOF NO ONE`
-command](https://redis.io/commands/slaveof).
+become a primary via the [`REPLICAOF NO ONE`
+command](https://redis.io/commands/replicaof).
### Sidekiq
diff --git a/doc/development/secure_coding_guidelines.md b/doc/development/secure_coding_guidelines.md
index 0367db8939a..b473c310647 100644
--- a/doc/development/secure_coding_guidelines.md
+++ b/doc/development/secure_coding_guidelines.md
@@ -1,4 +1,4 @@
-# Security Guidelines
+# Secure Coding Guidelines
This document contains descriptions and guidelines for addressing security
vulnerabilities commonly identified in the GitLab codebase. They are intended
@@ -26,7 +26,7 @@ Improper permission handling can have significant impacts on the security of an
Some situations may reveal [sensitive data](https://gitlab.com/gitlab-com/gl-infra/production/issues/477) or allow a malicious actor to perform [harmful actions](https://gitlab.com/gitlab-org/gitlab/issues/8180).
The overall impact depends heavily on what resources can be accessed or modified improperly.
-A common vulnerability when permission checks are missing is called [IDOR](https://www.owasp.org/index.php/Testing_for_Insecure_Direct_Object_References_(OTG-AUTHZ-004)) for Insecure Direct Object References.
+A common vulnerability when permission checks are missing is called [IDOR](https://owasp.org/www-project-web-security-testing-guide/latest/4-Web_Application_Security_Testing/05-Authorization_Testing/04-Testing_for_Insecure_Direct_Object_References) for Insecure Direct Object References.
### When to Consider
@@ -49,8 +49,8 @@ Be careful to **also test [visibility levels](https://gitlab.com/gitlab-org/gitl
Some example of well implemented access controls and tests:
1. [example1](https://dev.gitlab.org/gitlab/gitlab-ee/merge_requests/710/diffs?diff_id=13750#af40ef0eaae3c1e018809e1d88086e32bccaca40_43_43)
-1. [example2](https://dev.gitlab.org/gitlab/gitlabhq/merge_requests/2511/diffs#ed3aaab1510f43b032ce345909a887e5b167e196_142_155)
-1. [example3](https://dev.gitlab.org/gitlab/gitlabhq/merge_requests/3170/diffs?diff_id=17494)
+1. [example2](https://dev.gitlab.org/gitlab/gitlabhq/-/merge_requests/2511/diffs#ed3aaab1510f43b032ce345909a887e5b167e196_142_155)
+1. [example3](https://dev.gitlab.org/gitlab/gitlabhq/-/merge_requests/3170/diffs?diff_id=17494)
**NB:** any input from development team is welcome, e.g. about rubocop rules.
@@ -74,11 +74,11 @@ text = "foo\nbar"
p text.match /^bar$/
```
-The output of this example is `#<MatchData "bar">`, as Ruby treats the input `text` line by line. In order to match the whole __string__ the Regex anchors `\A` and `\z` should be used according to [Rubular](https://rubular.com/).
+The output of this example is `#<MatchData "bar">`, as Ruby treats the input `text` line by line. In order to match the whole __string__ the Regex anchors `\A` and `\z` should be used.
#### Impact
-This Ruby Regex speciality can have security impact, as often regular expressions are used for validations or to impose restrictions on user-input.
+This Ruby Regex specialty can have security impact, as often regular expressions are used for validations or to impose restrictions on user-input.
#### Examples
@@ -104,15 +104,64 @@ Here `params[:ip]` should not contain anything else but numbers and dots. Howeve
In most cases the anchors `\A` for beginning of text and `\z` for end of text should be used instead of `^` and `$`.
-### Further Links
+## Denial of Service (ReDoS)
+
+[ReDoS](https://owasp.org/www-community/attacks/Regular_expression_Denial_of_Service_-_ReDoS) is a possible attack if the attacker knows
+or controls the regular expression (regex) used, and is able to enter user input to match against the bad regular expression.
+
+### Impact
+
+The resource, for example Unicorn, Puma, or Sidekiq, can be made to hang as it takes a long time to evaulate the bad regex match.
+
+### Examples
+
+GitLab-specific examples can be found in the following merge requests:
+
+- [MR25314](https://gitlab.com/gitlab-org/gitlab/-/merge_requests/25314)
+- [MR25122](https://gitlab.com/gitlab-org/gitlab/-/merge_requests/25122#note_289087459)
+
+Consider the following example application, which defines a check using a regular expression. A user entering `user@aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa!.com` as the email on a form will hang the web server.
+
+```ruby
+class Email < ApplicationRecord
+ DOMAIN_MATCH = Regexp.new('([a-zA-Z0-9]+)+\.com')
+
+ validates :domain_matches
+
+ private
+
+ def domain_matches
+ errors.add(:email, 'does not match') if email =~ DOMAIN_MATCH
+ end
+```
+
+### Mitigation
+
+GitLab has `Gitlab::UntrustedRegexp` which internally uses the [`re2`](https://github.com/google/re2/wiki/Syntax) library.
+By utilizing `re2`, we get a strict limit on total execution time, and a smaller subset of available regex features.
+
+All user-provided regexes should use `Gitlab::UntrustedRegexp`.
+
+For other regexes, here are a few guidelines:
+
+- Remove unnecessary backtracking.
+- Avoid nested quantifiers if possible.
+- Try to be as precise as possible in your regex and avoid the `.` if something else can be used (e.g.: Use `_[^_]+_` instead of `_.*_` to match `_text here_`).
+
+An example can be found [in this commit](https://gitlab.com/gitlab-org/gitlab/commit/717824144f8181bef524592eab882dd7525a60ef).
+
+## Further Links
- [Rubular](https://rubular.com/) is a nice online tool to fiddle with Ruby Regexps.
+- [Runaway Regular Expressions](https://www.regular-expressions.info/catastrophic.html)
+- [The impact of regular expression denial of service (ReDoS) in practice: an empirical study at the ecosystem scale](http://people.cs.vt.edu/~davisjam/downloads/publications/DavisCoghlanServantLee-EcosystemREDOS-ESECFSE18.pdf). This research paper discusses approaches to automatically detect ReDoS vulnerabilities.
+- [Freezing the web: A study of redos vulnerabilities in JavaScript-based web servers](https://www.usenix.org/system/files/conference/usenixsecurity18/sec18-staicu.pdf). Another research paper about detecting ReDoS vulnerabilities.
## Server Side Request Forgery (SSRF)
### Description
-A [Server-side Request Forgery (SSRF)][1] is an attack in which an attacker
+A [Server-side Request Forgery (SSRF)](https://www.hackerone.com/blog-How-To-Server-Side-Request-Forgery-SSRF) is an attack in which an attacker
is able coerce a application into making an outbound request to an unintended
resource. This resource is usually internal. In GitLab, the connection most
commonly uses HTTP, but an SSRF can be performed with any protocol, such as
@@ -122,8 +171,6 @@ With an SSRF attack, the UI may or may not show the response. The latter is
called a Blind SSRF. While the impact is reduced, it can still be useful for
attackers, especially for mapping internal network services as part of recon.
-[1]: https://www.hackerone.com/blog-How-To-Server-Side-Request-Forgery-SSRF
-
### Impact
The impact of an SSRF can vary, depending on what the application server
@@ -155,16 +202,14 @@ The preferred SSRF mitigations within GitLab are:
#### GitLab HTTP Library
-The [GitLab::HTTP][2] wrapper library has grown to include mitigations for all of the GitLab-known SSRF vectors. It is also configured to respect the
+The [GitLab::HTTP](https://gitlab.com/gitlab-org/gitlab/-/blob/master/lib/gitlab/http.rb) wrapper library has grown to include mitigations for all of the GitLab-known SSRF vectors. It is also configured to respect the
`Outbound requests` options that allow instance administrators to block all internal connections, or limit the networks to which connections can be made.
In some cases, it has been possible to configure GitLab::HTTP as the HTTP
connection library for 3rd-party gems. This is preferrable over re-implementing
the mitigations for a new feature.
-- [More details](https://dev.gitlab.org/gitlab/gitlabhq/merge_requests/2530/diffs)
-
-[2]: https://gitlab.com/gitlab-org/gitlab/-/blob/master/lib/gitlab/http.rb
+- [More details](https://dev.gitlab.org/gitlab/gitlabhq/-/merge_requests/2530/diffs)
#### Feature-specific Mitigations
@@ -185,9 +230,7 @@ For situtions in which a whitelist or GitLab:HTTP cannot be used, it will be nec
- For HTTP connections: Disable redirects or validate the redirect destination
- To mitigate DNS rebinding attacks, validate and use the first IP address received
-See [url_blocker_spec.rb][3] for examples of SSRF payloads
-
-[3]: https://gitlab.com/gitlab-org/gitlab/-/blob/master/spec/lib/gitlab/url_blocker_spec.rb
+See [url_blocker_spec.rb](https://gitlab.com/gitlab-org/gitlab/-/blob/master/spec/lib/gitlab/url_blocker_spec.rb) for examples of SSRF payloads
## XSS guidelines
@@ -236,7 +279,7 @@ For any and all input fields, ensure to define expectations on the type/format o
- Validate the input using a [whitelist approach](https://youtu.be/2VFavqfDS6w?t=7816) to only allow characters through which you are expecting to receive for the field.
- Input which fails validation should be **rejected**, and not sanitized.
-Note that blacklists should be avoided, as it is near impossible to block all [variations of XSS](https://www.owasp.org/index.php/XSS_Filter_Evasion_Cheat_Sheet).
+Note that blacklists should be avoided, as it is near impossible to block all [variations of XSS](https://owasp.org/www-community/xss-filter-evasion-cheatsheet).
#### Output encoding
diff --git a/doc/development/shell_scripting_guide/index.md b/doc/development/shell_scripting_guide/index.md
index 387ef01bdcf..99cd1b9d67f 100644
--- a/doc/development/shell_scripting_guide/index.md
+++ b/doc/development/shell_scripting_guide/index.md
@@ -80,7 +80,20 @@ We format shell scripts according to the [Google Shell Style Guide](https://goog
so the following `shfmt` invocation should be applied to the project's script files:
```shell
-shfmt -i 2 -ci scripts/**/*.sh
+shfmt -i 2 -ci -w scripts/**/*.sh
+```
+
+In addition to the [Linting](#linting) GitLab CI/CD job, all projects with shell scripts should also
+use this job:
+
+```yaml
+shfmt:
+ image: mvdan/shfmt:v3.1.0-alpine
+ stage: test
+ before_script:
+ - shfmt -version
+ script:
+ - shfmt -i 2 -ci -d scripts # path to your shell scripts
```
TIP: **Tip:**
@@ -88,11 +101,6 @@ By default, shfmt will use the [shell detection](https://github.com/mvdan/sh#shf
and ignore files starting with a period. To override this, use `-ln` flag to specify the shell dialect:
`-ln posix` or `-ln bash`.
-NOTE: **Note:**
-Currently, the `shfmt` tool [is not shipped](https://github.com/mvdan/sh/issues/68) as a Docker image containing
-a Linux shell. This makes it impossible to use the [official Docker image](https://hub.docker.com/r/mvdan/shfmt)
-in GitLab Runner. This [may change](https://github.com/mvdan/sh/issues/68#issuecomment-507721371) in future.
-
## Testing
NOTE: **Note:**
diff --git a/doc/development/sidekiq_style_guide.md b/doc/development/sidekiq_style_guide.md
index 0ca25f43345..a5d0eecdc7b 100644
--- a/doc/development/sidekiq_style_guide.md
+++ b/doc/development/sidekiq_style_guide.md
@@ -82,6 +82,11 @@ As a general rule, a worker can be considered idempotent if:
A good example of that would be a cache expiration worker.
+NOTE: **Note:**
+A job scheduled for an idempotent worker will automatically be
+[deduplicated](#deduplication) when an unstarted job with the same
+arguments is already in the queue.
+
### Ensuring a worker is idempotent
Make sure the worker tests pass using the following shared example:
@@ -118,8 +123,50 @@ It's encouraged to only have the `idempotent!` call in the top-most worker class
the `perform` method is defined in another class or module.
NOTE: **Note:**
-Note that a cop will fail if the worker class is not marked as idempotent.
-Consider skipping the cop if you're not confident your job can safely run multiple times.
+If the worker class is not marked as idempotent, a cop will fail.
+Consider skipping the cop if you're not confident your job can safely
+run multiple times.
+
+### Deduplication
+
+When a job for an idempotent worker is enqueued while another
+unstarted job is already in the queue, GitLab drops the second
+job. The work is skipped because the same work would be
+done by the job that was scheduled first; by the time the second
+job executed, the first job would do nothing.
+
+For example, `AuthorizedProjectsWorker` takes a user ID. When the
+worker runs, it recalculates a user's authorizations. GitLab schedules
+this job each time an action potentially changes a user's
+authorizations. If the same user is added to two projects at the
+same time, the second job can be skipped if the first job hasn't
+begun, because when the first job runs, it creates the
+authorizations for both projects.
+
+GitLab doesn't skip jobs scheduled in the future, as we assume that
+the state will have changed by the time the job is scheduled to
+execute.
+
+More [deduplication strategies have been suggested](https://gitlab.com/gitlab-com/gl-infra/scalability/issues/195). If you are implementing a worker that
+could benefit from a different strategy, please comment in the issue.
+
+If the automatic deduplication were to cause issues in certain
+queues. This can be temporarily disabled by enabling a feature flag
+named `disable_<queue name>_deduplication`. For example to disable
+deduplication for the `AuthorizedProjectsWorker`, we would enable the
+feature flag `disable_authorized_projects_deduplication`.
+
+From chatops:
+
+```shell
+/chatops run feature set disable_authorized_projects_deduplication true
+```
+
+From the rails console:
+
+```ruby
+Feature.enable!(:disable_authorized_projects_deduplication)
+```
## Job urgency
diff --git a/doc/development/telemetry/index.md b/doc/development/telemetry/index.md
new file mode 100644
index 00000000000..32f63d5221e
--- /dev/null
+++ b/doc/development/telemetry/index.md
@@ -0,0 +1,165 @@
+# Telemetry Guide
+
+At GitLab, we collect telemetry for the purpose of helping us build a better GitLab. Data about how GitLab is used is collected to better understand what parts of GitLab needs improvement and what features to build next. Telemetry also helps our team better understand the reasons why people use GitLab and with this knowledge we are able to make better product decisions.
+
+We also encourage users to enable tracking, and we embrace full transparency with our tracking approach so it can be easily understood and trusted. By enabling tracking, users can:
+
+- Contribute back to the wider community.
+- Help GitLab improve on the product.
+
+This documentation consists of three guides providing an overview of Telemetry at GitLab.
+
+Telemetry Guide:
+
+ 1. [Our tracking tools](#our-tracking-tools)
+ 1. [What data can be tracked](#what-data-can-be-tracked)
+ 1. [Telemetry systems overview](#telemetry-systems-overview)
+
+[Usage Ping Guide](usage_ping.md)
+
+ 1. [What is Usage Ping](usage_ping.md#what-is-usage-ping)
+ 1. [Usage Ping payload](usage_ping.md#usage-ping-payload)
+ 1. [Disabling Usage Ping](usage_ping.md#disabling-usage-ping)
+ 1. [Usage Ping request flow](usage_ping.md#usage-ping-request-flow)
+ 1. [How Usage Ping works](usage_ping.md#how-usage-ping-works)
+ 1. [Implementing Usage Ping](usage_ping.md#implementing-usage-ping)
+ 1. [Developing and testing usage ping](usage_ping.md#developing-and-testing-usage-ping)
+
+[Snowplow Guide](snowplow.md)
+
+1. [What is Snowplow](snowplow.md#what-is-snowplow)
+1. [Snowplow schema](snowplow.md#snowplow-schema)
+1. [Enabling Snowplow](snowplow.md#enabling-snowplow)
+1. [Snowplow request flow](snowplow.md#snowplow-request-flow)
+1. [Implementing Snowplow JS (Frontend) tracking](snowplow.md#implementing-snowplow-js-frontend-tracking)
+1. [Implementing Snowplow Ruby (Backend) tracking](snowplow.md#implementing-snowplow-ruby-backend-tracking)
+1. [Developing and testing Snowplow](snowplow.md#developing-and-testing-snowplow)
+
+More useful links:
+
+- [Telemetry Direction](https://about.gitlab.com/direction/telemetry/)
+- [Data Analysis Process](https://about.gitlab.com/handbook/business-ops/data-team/#-data-analysis-process)
+- [Data for Product Managers](https://about.gitlab.com/handbook/business-ops/data-team/data-for-product-managers/)
+- [Data Infrastructure](https://about.gitlab.com/handbook/business-ops/data-team/data-infrastructure/)
+
+## Our tracking tools
+
+In this section we will explain the six different technologies we use to gather product usage data.
+
+**Snowplow JS (Frontend)**
+
+Snowplow is an enterprise-grade marketing and product analytics platform which helps track the way users engage with our website and application. [Snowplow JS](https://github.com/snowplow/snowplow/wiki/javascript-tracker) is a frontend tracker for client-side events.
+
+**Snowplow Ruby (Backend)**
+
+Snowplow is an enterprise-grade marketing and product analytics platform which helps track the way users engage with our website and application. [Snowplow Ruby](https://github.com/snowplow/snowplow/wiki/ruby-tracker) is a backend tracker for server-side events.
+
+**Usage Ping**
+
+Usage Ping is a method for GitLab Inc to collect usage data on a GitLab instance. Usage Ping is primarily composed of row counts for different tables in the instance’s database. By comparing these counts month over month (or week over week), we can get a rough sense for how an instance is using the different features within the product. This high-level data is used to help our product, support, and sales teams.
+
+Read more about how this works in the [Usage Ping guide](usage_ping.md)
+
+**Database import**
+
+Database imports are full imports of data into GitLab's data warehouse. For GitLab.com, the PostgreSQL database is loaded into Snowflake data warehouse every 6 hours. For more details, see the [data team handbook](https://about.gitlab.com/handbook/business-ops/data-team/#extract-and-load).
+
+**Log system**
+
+System logs are the application logs generated from running the GitLab Rails application. For more details, see the [log system](../../administration/logs.md) and [logging infrastructure](https://gitlab.com/gitlab-com/runbooks/tree/master/logging/doc#logging-infrastructure-overview).
+
+## What data can be tracked
+
+Our different tracking tools allows us to track different types of events. The event types and examples of what data can be tracked are outlined below.
+
+| Event Type | Snowplow JS (Frontend) | Snowplow Ruby (Backend) | Usage Ping | Database import | Log system |
+| ------ | ------ | ------ | ------ | ------ | ------ |
+| Database counts | ⌠| ⌠| ✅ | ✅ | ⌠|
+| Pageview events | ✅ | ✅ | ⌠| ⌠| ⌠|
+| UI events | ✅ | ⌠| ⌠| ⌠| ⌠|
+| CRUD and API events | ⌠| ✅ | ⌠| ⌠| ⌠|
+| Event funnels | ✅ | ✅ | ⌠| ⌠| ⌠|
+| PostgreSQL Data | ⌠| ⌠| ⌠| ✅ | ⌠|
+| Logs | ⌠| ⌠| ⌠| ⌠| ✅ |
+| External services | ⌠| ⌠| ⌠| ⌠| ⌠|
+
+**Database counts**
+
+- How many Projects have been created by unique users
+- How many users logged in the past 28 day
+
+Database counts are row counts for different tables in an instance’s database. These are SQL count queries which have been filtered, grouped, or aggregated which provide high level usage data. The full list of available tables can be found in [structure.sql](https://gitlab.com/gitlab-org/gitlab/-/blob/master/db/structure.sql)
+
+**Pageview events**
+
+- How many sessions visited the /dashboard/groups page
+
+**UI Events**
+
+- How many sessions clicked on a button or link
+- How many sessions closed a modal
+
+UI events are any interface-driven actions from the browser including click data.
+
+**CRUD or API events**
+
+- How many Git pushes were made
+- How many GraphQL queries were made
+- How many requests were made to a Rails action or controller.
+
+These are backend events that include the creation, read, update, deletion of records and other events that might be triggered from layers that aren't necessarily only available in the interface.
+
+**Event funnels**
+
+- How many sessions performed action A, B, then C
+- What is our conversion rate from step A to B?
+
+**PostgreSQL data**
+
+These are raw database records which can be explored using business intelligence tools like Sisense. The full list of available tables can be found in [structure.sql](https://gitlab.com/gitlab-org/gitlab/-/blob/master/db/structure.sql)
+
+**Logs**
+
+These are raw logs such as the [Production logs](../../administration/logs.md#production_jsonlog), [API logs](../../administration/logs.md#api_jsonlog), or [Sidekiq logs](../../administration/logs.md#sidekiqlog). See the [overview of Logging Infrastructure](https://gitlab.com/gitlab-com/runbooks/tree/master/logging/doc#logging-infrastructure-overview) for more details.
+
+**External services**
+
+These are external services a GitLab instance interacts with such as an [external storage provider](../../administration/static_objects_external_storage.md) or an [external container registry](../../administration/packages/container_registry.md#use-an-external-container-registry-with-gitlab-as-an-auth-endpoint). These services must be able to send data back into a GitLab instance for data to be tracked.
+
+## Telemetry systems overview
+
+The systems overview is a simplified diagram showing the interactions between GitLab Inc and self-managed nstances.
+
+![Telemetry_Overview](../img/telemetry_system_overview.png)
+
+[Source file](https://app.diagrams.net/#G13DVpN-XnhWGz9tqReIj8pp1UE4ehk_EC)
+
+### GitLab Inc
+
+For Telemetry purposes, GitLab Inc has three major components:
+
+1. [Data Infrastructure](https://about.gitlab.com/handbook/business-ops/data-team/data-infrastructure/): This contains everything managed by our data team including Sisense Dashboards for visualization, Snowflake for Data Warehousing, incoming data sources such as PostgreSQL Pipeline and S3 Bucket, and lastly our data collectors [GitLab.com's Snowplow Collector](https://about.gitlab.com/handbook/engineering/infrastructure/library/snowplow/) and GitLab's Versions Application.
+1. GitLab.com: This is the production GitLab application which is made up of a Client and Server. On the Client or browser side, a Snowplow JS Tracker (Frontend) is used to track client-side events. On the Server or application side, a Snowplow Ruby Tracker (Backend) is used to track server-side events. The server also contains Usage Ping which leverages a PostgreSQL database and a Redis in-memory data store to report on usage data. Lastly, the server also contains System Logs which are generated from running the GitLab application.
+1. [Monitoring infrastructure](https://about.gitlab.com/handbook/engineering/monitoring/): This is the infrastructure used to ensure GitLab.com is operating smoothly. System Logs are sent from GitLab.com to our monitoring infrastructure and collected by a FluentD collector. From FluentD, logs are either sent to long term Google Cloud Services cold storage via Stackdriver, or, they are sent to our Elastic Cluster via Cloud Pub/Sub which can be explored in real-time using Kibana
+
+### Self-managed
+
+For Telemetry purposes, self-managed instances have two major components:
+
+1. Data infrastructure: Having a data infrastructure setup is optional on self-managed instances. If you'd like to collect Snowplow tracking events for your self-managed instance, you can setup your own self-managed Snowplow collector and configure your Snowplow events to point to your own collector.
+1. GitLab: A self-managed GitLab instance contains all of the same components as GitLab.com mentioned above.
+
+### Differences between GitLab Inc and Self-managed
+
+As shown by the orange lines, on GitLab.com Snowplow JS, Snowplow Ruby, Usage Ping, and PostgreSQL database imports all flow into GitLab Inc's data fnfrastructure. However, on self-managed, only Usage Ping flows into GitLab Inc's data infrastructure.
+
+As shown by the green lines, on GitLab.com system logs flow into GitLab Inc's monitoring infrastructure. On self-managed, there are no logs sent to GitLab Inc's monitoring infrastructure.
+
+The differences between GitLab.com and self-managed are summarized below:
+
+| Environment | Snowplow JS (Frontend) | Snowplow Ruby (Backend) | Usage Ping | Database import | Logs system |
+| ------ | ------ | ------ | ------ | ------ | ------ |
+| GitLab.com | ✅ | ✅ | ✅ | ✅ | ✅ |
+| Self-Managed | âŒ(1) | âŒ(1) | ✅ | ⌠| ⌠|
+
+Note (1): Snowplow JS and Snowplow Ruby are available on self-managed, however, the Snowplow Collector endpoint is set to a self-managed Snowplow Collector which GitLab Inc does not have access to.
diff --git a/doc/development/telemetry/snowplow.md b/doc/development/telemetry/snowplow.md
new file mode 100644
index 00000000000..aeaad6e5624
--- /dev/null
+++ b/doc/development/telemetry/snowplow.md
@@ -0,0 +1,393 @@
+# Snowplow Guide
+
+This guide provides a details about how Snowplow works. It includes the following sections:
+
+1. [What is Snowplow](#what-is-snowplow)
+1. [Snowplow schema](#snowplow-schema)
+1. [Enabling Snowplow](#enabling-snowplow)
+1. [Snowplow request flow](#snowplow-request-flow)
+1. [Implementing Snowplow JS (Frontend) tracking](#implementing-snowplow-js-frontend-tracking)
+1. [Implementing Snowplow Ruby (Backend) tracking](#implementing-snowplow-ruby-backend-tracking)
+1. [Developing and testing Snowplow](#developing-and-testing-snowplow)
+
+For more information about Telemetry, see:
+
+- [Telemetry Guide](index.md)
+- [Usage Ping Guide](usage_ping.md)
+
+More useful links:
+
+- [Telemetry Direction](https://about.gitlab.com/direction/telemetry/)
+- [Data Analysis Process](https://about.gitlab.com/handbook/business-ops/data-team/#-data-analysis-process)
+- [Data for Product Managers](https://about.gitlab.com/handbook/business-ops/data-team/data-for-product-managers/)
+- [Data Infrastructure](https://about.gitlab.com/handbook/business-ops/data-team/data-infrastructure/)
+
+## What is Snowplow
+
+Snowplow is an enterprise-grade marketing and product analytics platform which helps track the way users engage with our website and application.
+
+From [Snowplow's documentation](https://github.com/snowplow/snowplow), Snowplow consists of six loosely-coupled sub-systems:
+
+- **Trackers** fire Snowplow events. Currently Snowplow has 12 trackers, covering web, mobile, desktop, server and IoT
+- **Collectors** receive Snowplow events from trackers. Currently we have three different event collectors, sinking events either to Amazon S3, Apache Kafka or Amazon Kinesis
+- **Enrich** cleans up the raw Snowplow events, enriches them and puts them into storage. Currently we have a Hadoop-based enrichment process, and a Kinesis- or Kafka-based process
+- **Storage** is where the Snowplow events live. Currently we store the Snowplow events in a flat file structure on S3, and in the Redshift and PostgreSQL databases
+- **Data modeling** is where event-level data is joined with other data sets and aggregated into smaller data sets, and business logic is applied. This produces a clean set of tables which make it easier to perform analysis on the data. We have data models for Redshift and Looker
+- **Analytics** are performed on the Snowplow events or on the aggregate tables.
+
+![snowplow_flow](../img/snowplow_flow.png)
+> ![snowplow_flow](../img/snowplow_flow.png)
+
+## Snowplow schema
+
+We currently have many definitions of Snowplow's schema. We have an active issue to [standardize this schema](https://gitlab.com/gitlab-org/gitlab/issues/207930) including the following definitions:
+
+- Frontend and backend taxonomy as listed below
+- [Feature instrumentation taxonomy](https://about.gitlab.com/handbook/product/feature-instrumentation/#taxonomy)
+- [Self describing events](https://github.com/snowplow/snowplow/wiki/Custom-events#self-describing-events)
+- [Iglu schema](https://gitlab.com/gitlab-org/iglu/)
+- [Snowplow authored events](https://github.com/snowplow/snowplow/wiki/Snowplow-authored-events)
+
+## Enabling Snowplow
+
+Tracking can be enabled at:
+
+- The instance level, which will enable tracking on both the frontend and backend layers.
+- User level, though user tracking can be disabled on a per-user basis. GitLab tracking respects the [Do Not Track](https://www.eff.org/issues/do-not-track) standard, so any user who has enabled the Do Not Track option in their browser will also not be tracked from a user level.
+
+We utilize Snowplow for the majority of our tracking strategy and it is enabled on GitLab.com. On a self-managed instance, Snowplow can be enabled by navigating to:
+
+- **Admin Area > Settings > Integrations** in the UI.
+- `admin/application_settings/integrations` in your browser.
+
+The following configuration is required:
+
+| Name | Value |
+| ------------- | ------------------------- |
+| Collector | `snowplow.trx.gitlab.net` |
+| Site ID | `gitlab` |
+| Cookie domain | `.gitlab.com` |
+
+## Snowplow request flow
+
+The following example shows a basic request/response flow between a Snowplow JS / Ruby Trackers on GitLab.com, [the GitLab.com Snowplow Collector](https://about.gitlab.com/handbook/engineering/infrastructure/library/snowplow/), GitLab's S3 Bucket, GitLab's Snowflake Data Warehouse, and Sisense.:
+
+```mermaid
+sequenceDiagram
+ participant Snowplow JS (Frontend)
+ participant Snowplow Ruby (Backend)
+ participant GitLab.com Snowplow Collector
+ participant S3 Bucket
+ participant Snowflake DW
+ participant Sisense Dashboards
+ Snowplow JS (Frontend) ->> GitLab.com Snowplow Collector: FE Tracking event
+ Snowplow Ruby (Backend) ->> GitLab.com Snowplow Collector: BE Tracking event
+ loop Process using Kinesis Stream
+ GitLab.com Snowplow Collector ->> GitLab.com Snowplow Collector: Log raw events
+ GitLab.com Snowplow Collector ->> GitLab.com Snowplow Collector: Enrich events
+ GitLab.com Snowplow Collector ->> GitLab.com Snowplow Collector: Write to disk
+ end
+ GitLab.com Snowplow Collector ->> S3 Bucket: Kinesis Firehose
+ S3 Bucket->>Snowflake DW: Import data
+ Snowflake DW->>Snowflake DW: Transform data using dbt
+ Snowflake DW->>Sisense Dashboards: Data available for querying
+```
+
+## Implementing Snowplow JS (Frontend) tracking
+
+GitLab provides `Tracking`, an interface that wraps the [Snowplow JavaScript Tracker](https://github.com/snowplow/snowplow/wiki/javascript-tracker) for tracking custom events. There are a few ways to utilize tracking, but each generally requires at minimum, a `category` and an `action`. Additional data can be provided that adheres to our [Feature instrumentation taxonomy](https://about.gitlab.com/handbook/product/feature-instrumentation/#taxonomy).
+
+| field | type | default value | description |
+|:-----------|:-------|:---------------------------|:------------|
+| `category` | string | document.body.dataset.page | Page or subsection of a page that events are being captured within. |
+| `action` | string | 'generic' | Action the user is taking. Clicks should be `click` and activations should be `activate`, so for example, focusing a form field would be `activate_form_input`, and clicking a button would be `click_button`. |
+| `data` | object | {} | Additional data such as `label`, `property`, `value`, and `context` as described [in our Feature Instrumentation taxonomy](https://about.gitlab.com/handbook/product/feature-instrumentation/#taxonomy). |
+
+### Tracking in HAML (or Vue Templates)
+
+When working within HAML (or Vue templates) we can add `data-track-*` attributes to elements of interest. All elements that have a `data-track-event` attribute will automatically have event tracking bound on clicks.
+
+Below is an example of `data-track-*` attributes assigned to a button:
+
+```haml
+%button.btn{ data: { track: { event: "click_button", label: "template_preview", property: "my-template" } } }
+```
+
+```html
+<button class="btn"
+ data-track-event="click_button"
+ data-track-label="template_preview"
+ data-track-property="my-template"
+/>
+```
+
+Event listeners are bound at the document level to handle click events on or within elements with these data attributes. This allows for them to be properly handled on rerendering and changes to the DOM, but it's important to know that because of the way these events are bound, click events shouldn't be stopped from propagating up the DOM tree. If for any reason click events are being stopped from propagating, you'll need to implement your own listeners and follow the instructions in [Tracking in raw JavaScript](#tracking-in-raw-javascript).
+
+Below is a list of supported `data-track-*` attributes:
+
+| attribute | required | description |
+|:----------------------|:---------|:------------|
+| `data-track-event` | true | Action the user is taking. Clicks must be prepended with `click` and activations must be prepended with `activate`. For example, focusing a form field would be `activate_form_input` and clicking a button would be `click_button`. |
+| `data-track-label` | false | The `label` as described [in our Feature Instrumentation taxonomy](https://about.gitlab.com/handbook/product/feature-instrumentation/#taxonomy). |
+| `data-track-property` | false | The `property` as described [in our Feature Instrumentation taxonomy](https://about.gitlab.com/handbook/product/feature-instrumentation/#taxonomy). |
+| `data-track-value` | false | The `value` as described [in our Feature Instrumentation taxonomy](https://about.gitlab.com/handbook/product/feature-instrumentation/#taxonomy). If omitted, this will be the element's `value` property or an empty string. For checkboxes, the default value will be the element's checked attribute or `false` when unchecked. |
+| `data-track-context` | false | The `context` as described [in our Feature Instrumentation taxonomy](https://about.gitlab.com/handbook/product/feature-instrumentation/#taxonomy). |
+
+### Tracking within Vue components
+
+There's a tracking Vue mixin that can be used in components if more complex tracking is required. To use it, first import the `Tracking` library and request a mixin.
+
+```javascript
+import Tracking from '~/tracking';
+const trackingMixin = Tracking.mixin({ label: 'right_sidebar' });
+```
+
+You can provide default options that will be passed along whenever an event is tracked from within your component. For instance, if all events within a component should be tracked with a given `label`, you can provide one at this time. Available defaults are `category`, `label`, `property`, and `value`. If no category is specified, `document.body.dataset.page` is used as the default.
+
+You can then use the mixin normally in your component with the `mixin`, Vue declaration. The mixin also provides the ability to specify tracking options in `data` or `computed`. These will override any defaults and allows the values to be dynamic from props, or based on state.
+
+```javascript
+export default {
+ mixins: [trackingMixin],
+ // ...[component implementation]...
+ data() {
+ return {
+ expanded: false,
+ tracking: {
+ label: 'left_sidebar'
+ }
+ };
+ },
+}
+```
+
+The mixin provides a `track` method that can be called within the template, or from component methods. An example of the whole implementation might look like the following.
+
+```javascript
+export default {
+ mixins: [Tracking.mixin({ label: 'right_sidebar' })],
+ data() {
+ return {
+ expanded: false,
+ };
+ },
+ methods: {
+ toggle() {
+ this.expanded = !this.expanded;
+ this.track('click_toggle', { value: this.expanded })
+ }
+ }
+};
+```
+
+And if needed within the template, you can use the `track` method directly as well.
+
+```html
+<template>
+ <div>
+ <a class="toggle" @click.prevent="toggle">Toggle</a>
+ <div v-if="expanded">
+ <p>Hello world!</p>
+ <a @click.prevent="track('click_action')">Track an event</a>
+ </div>
+ </div>
+</template>
+```
+
+### Tracking in raw JavaScript
+
+Custom event tracking and instrumentation can be added by directly calling the `Tracking.event` static function. The following example demonstrates tracking a click on a button by calling `Tracking.event` manually.
+
+```javascript
+import Tracking from '~/tracking';
+
+const button = document.getElementById('create_from_template_button');
+button.addEventListener('click', () => {
+ Tracking.event('dashboard:projects:index', 'click_button', {
+ label: 'create_from_template',
+ property: 'template_preview',
+ value: 'rails',
+ });
+})
+```
+
+### Tests and test helpers
+
+In Jest particularly in vue tests, you can use the following:
+
+```javascript
+import { mockTracking } from 'helpers/tracking_helper';
+
+describe('MyTracking', () => {
+ let spy;
+
+ beforeEach(() => {
+ spy = mockTracking('_category_', wrapper.element, jest.spyOn);
+ });
+
+ it('tracks an event when clicked on feedback', () => {
+ wrapper.find('.discover-feedback-icon').trigger('click');
+
+ expect(spy).toHaveBeenCalledWith('_category_', 'click_button', {
+ label: 'security-discover-feedback-cta',
+ property: '0',
+ });
+ });
+});
+
+```
+
+In obsolete Karma tests it's used as below:
+
+```javascript
+import { mockTracking, triggerEvent } from 'spec/helpers/tracking_helper';
+
+describe('my component', () => {
+ let trackingSpy;
+
+ beforeEach(() => {
+ trackingSpy = mockTracking('_category_', vm.$el, spyOn);
+ });
+
+ const triggerEvent = () => {
+ // action which should trigger a event
+ };
+
+ it('tracks an event when toggled', () => {
+ expect(trackingSpy).not.toHaveBeenCalled();
+
+ triggerEvent('a.toggle');
+
+ expect(trackingSpy).toHaveBeenCalledWith('_category_', 'click_edit_button', {
+ label: 'right_sidebar',
+ property: 'confidentiality',
+ });
+ });
+});
+```
+
+## Implementing Snowplow Ruby (Backend) tracking
+
+GitLab provides `Gitlab::Tracking`, an interface that wraps the [Snowplow Ruby Tracker](https://github.com/snowplow/snowplow/wiki/ruby-tracker) for tracking custom events.
+
+Custom event tracking and instrumentation can be added by directly calling the `GitLab::Tracking.event` class method, which accepts the following arguments:
+
+| argument | type | default value | description |
+|:-----------|:-------|:---------------------------|:------------|
+| `category` | string | 'application' | Area or aspect of the application. This could be `HealthCheckController` or `Lfs::FileTransformer` for instance. |
+| `action` | string | 'generic' | The action being taken, which can be anything from a controller action like `create` to something like an Active Record callback. |
+| `data` | object | {} | Additional data such as `label`, `property`, `value`, and `context` as described [in our Feature Instrumentation taxonomy](https://about.gitlab.com/handbook/product/feature-instrumentation/#taxonomy). These will be set as empty strings if you don't provide them. |
+
+Tracking can be viewed as either tracking user behavior, or can be utilized for instrumentation to monitor and visual performance over time in an area or aspect of code.
+
+For example:
+
+```ruby
+class Projects::CreateService < BaseService
+ def execute
+ project = Project.create(params)
+
+ Gitlab::Tracking.event('Projects::CreateService', 'create_project',
+ label: project.errors.full_messages.to_sentence,
+ value: project.valid?
+ )
+ end
+end
+```
+
+### Performance
+
+We use the [AsyncEmitter](https://github.com/snowplow/snowplow/wiki/Ruby-Tracker#52-the-asyncemitter-class) when tracking events, which allows for instrumentation calls to be run in a background thread. This is still an active area of development.
+
+## Developing and testing Snowplow
+
+There are several tools for developing and testing Snowplow Event
+
+| Testing Tool | Frontend Tracking | Backend Tracking | Local Development Environment | Production Environment |
+| ------ | ------ | ------ | ------ | ------ |
+| Snowplow Analytics Debugger Chrome Extension | ✅ | ⌠| ✅ | ✅ |
+| Snowplow Inspector Chrome Extension | ✅ | ⌠| ✅ | ✅ |
+| Snowplow Micro | ✅ | ✅ | ✅ | ⌠|
+| Snowplow Mini | ✅ | ✅ | ⌠| ✅ |
+
+### Snowplow Analytics Debugger Chrome Extension
+
+Snowplow Analytics Debugger is a browser extension for testing frontend events. This works on production, staging and local development environments.
+
+1. Install [Snowplow Analytics Debugger](https://chrome.google.com/webstore/detail/snowplow-analytics-debugg/jbnlcgeengmijcghameodeaenefieedm) chrome browser extension
+1. Open Chrome DevTools to the Snowplow Analytics Debugger tab
+1. Learn more at [Igloo Analytics](https://www.iglooanalytics.com/blog/snowplow-analytics-debugger-chrome-extension.html)
+
+### Snowplow Inspector Chrome Extension
+
+Snowplow Inspector Chrome Extension is a browser extension for testing frontend events. This works on production, staging and local development environments.
+
+1. Install [Snowplow Inspector](https://chrome.google.com/webstore/detail/snowplow-inspector/maplkdomeamdlngconidoefjpogkmljm?hl=en)
+1. Open the chrome extension by pressing the Snowplow Inspector icon beside the address bar
+1. Click around on a webpage with Snowplow and you should see JavaScript events firing in the inspector window.
+
+### Snowplow Micro
+
+Snowplow Micro is a very small version of a full Snowplow data collection pipeline: small enough that it can be launched by a test suite. Events can be recorded into Snowplow Micro just as they can a full Snowplow pipeline. Micro then exposes an API that can be queried.
+
+Snowplow Micro is a docker-based solution for testing frontend and backend events in a local development environment. You need to modify GDK using the instructions below to set this up.
+
+- Read [Introducing Snowplow Micro](https://snowplowanalytics.com/blog/2019/07/17/introducing-snowplow-micro/)
+- Look at the [Snowplow Micro repo](https://github.com/snowplow-incubator/snowplow-micro)
+- Watch our [installation guide recording](https://www.youtube.com/watch?v=OX46fo_A0Ag)
+
+1. Install [Snowplow Micro](https://github.com/snowplow-incubator/snowplow-micro)
+
+``` bash
+docker run --mount type=bind,source=$(pwd)/example,destination=/config -p 9090:9090 snowplow/snowplow-micro:latest --collector-config /config/micro.conf --iglu /config/iglu.json
+```
+
+1. Install snowplow micro by cloning the settings in [this project](https://gitlab.com/a_akgun/snowplow-micro).
+
+ ``` bash
+ git clone git@gitlab.com:a_akgun/snowplow-micro.git
+ ./snowplow-micro.sh
+ ```
+
+1. Update port in SQL (needed to set 9090)
+
+ ``` bash
+ gdk psql -d gitlabhq_development
+ update application_settings set snowplow_collector_hostname='localhost:9090', snowplow_enabled=true, snowplow_cookie_domain='.gitlab.com';
+ ```
+
+1. Update `app/assets/javascripts/tracking.js` to [remove this line](https://gitlab.com/snippets/1918635):
+
+ ``` javascript
+ forceSecureTracker: true
+ ```
+
+1. Update `lib/gitlab/tracking.rb` to [add these lines](https://gitlab.com/snippets/1918635):
+
+ ``` ruby
+ protocol: 'http',
+ port: 9090,
+ ```
+
+1. Update `lib/gitlab/tracking.rb` to [change async emitter from https to http](https://gitlab.com/snippets/1918635):
+
+ ``` ruby
+ SnowplowTracker::AsyncEmitter.new(Gitlab::CurrentSettings.snowplow_collector_hostname, protocol: 'http'),
+ ```
+
+1. Enable Snowplow in the admin area, Settings::Integrations::Snowplow to point to:
+ `http://localhost:3000/admin/application_settings/integrations#js-snowplow-settings`
+1. `gdk restart`
+1. Send a test Snowplow event from the Rails console
+
+ ``` ruby
+ Gitlab::Tracking.self_describing_event('iglu:com.gitlab/pageview_context/jsonschema/1-0-0', { page_type: ‘MY_TYPE' }, context: nil )
+ ```
+
+### Snowplow Mini
+
+[Snowplow Mini](https://github.com/snowplow/snowplow-mini) is an easily-deployable, single-instance version of Snowplow.
+
+Snowplow Mini can be used for testing frontend and backend events on a production, staging and local development environment.
+
+For GitLab.com, we are currently setting up a [QA and Testing environment](https://gitlab.com/gitlab-org/telemetry/-/issues/266) using Snowplow Mini.
diff --git a/doc/development/telemetry/usage_ping.md b/doc/development/telemetry/usage_ping.md
new file mode 100644
index 00000000000..e9b959eaa96
--- /dev/null
+++ b/doc/development/telemetry/usage_ping.md
@@ -0,0 +1,489 @@
+# Usage Ping Guide
+
+> - [Introduced][ee-557] in GitLab Enterprise Edition 8.10.
+> - More statistics [were added][ee-735] in GitLab Enterprise Edition 8.12.
+> - [Moved to GitLab Core][ce-23361] in 9.1.
+> - More statistics [were added][ee-6602] in GitLab Ultimate 11.2.
+
+This guide provides a details about how usage ping works. It includes the following sections:
+
+1. [What is Usage Ping](#what-is-usage-ping)
+1. [Usage Ping payload](#usage-ping-payload)
+1. [Disabling Usage Ping](#disabling-usage-ping)
+1. [Usage Ping request flow](#usage-ping-request-flow)
+1. [How Usage Ping works](#how-usage-ping-works)
+1. [Implementing Usage Ping](#implementing-usage-ping)
+1. [Developing and testing usage ping](#developing-and-testing-usage-ping)
+
+For more information about Telemetry, see:
+
+- [Telemetry Guide](index.md)
+- [Snowplow Guide](snowplow.md)
+
+More useful links:
+
+- [Telemetry Direction](https://about.gitlab.com/direction/telemetry/)
+- [Data Analysis Process](https://about.gitlab.com/handbook/business-ops/data-team/#-data-analysis-process)
+- [Data for Product Managers](https://about.gitlab.com/handbook/business-ops/data-team/data-for-product-managers/)
+- [Data Infrastructure](https://about.gitlab.com/handbook/business-ops/data-team/data-infrastructure/)
+
+## What is Usage Ping
+
+- GitLab sends a weekly payload containing usage data to GitLab Inc. The usage ping uses high-level data to help our product, support, and sales teams. It does not send any project names, usernames, or any other specific data. The information from the usage ping is not anonymous, it is linked to the hostname of the instance. Sending usage ping is optional, and any instance can disable analytics.
+- The usage data is primarily composed of row counts for different tables in the instance’s database. By comparing these counts month over month (or week over week), we can get a rough sense for how an instance is using the different features within the product.
+- Usage ping is important to GitLab as we use it to calculate our and Stage Monthly Active Users (SMAU) which helps us measure the success of our stages and features.
+- Once usage ping is enabled, GitLab will gather data from the other instances and will be able to show usage statistics of your instance to your users.
+
+### Why Should We Enable Usage Ping?
+
+- The main purpose of Usage Ping is to build a better GitLab. Data about how GitLab is used is collected to better understand feature/stage adoption and usage, which helps us understand how GitLab is adding value and helps our team better understand the reasons why people use GitLab and with this knowledge we are able to make better product decisions.
+- As a benefit of having the usage ping active, GitLab lets you analyze the users’ activities over time of your GitLab installation.
+- As a benefit of having the usage ping active, GitLab provides you with The DevOps Score,which gives you an overview of your entire instance’s adoption of Concurrent DevOps from planning to monitoring.
+- You will get better, more proactive support. (assuming that our TAMs and support organization used the data to deliver more value)
+- You will get insight and advice into how to get the most value out of your investment in GitLab. Wouldn't you want to know that a number of features or values are not being adopted in your organization?
+- You get a report that illustrates how you compare against other similar organizations (anonymized), with specific advice and recommendations on how to improve your DevOps processes.
+
+### Limitations
+
+- Usage Ping does not track frontend events things like page views, link clicks, or user sessions and only focuses on aggregated backend events.
+- Because of these limitations we recommend instrumenting your products with Snowplow for more detailed analytics on GitLab.com and use Usage Ping to track aggregated backend events on self-managed.
+
+## Usage Ping payload
+
+You can view the exact JSON payload sent to GitLab Inc. in the administration panel. To view the payload:
+
+1. Navigate to the **Admin Area > Settings > Metrics and profiling**.
+1. Expand the **Usage statistics** section.
+1. Click the **Preview payload** button.
+
+Here is an example of the payload structure
+
+``` json
+{
+ "uuid": "0000000-0000-0000-0000-000000000000",
+ "hostname": "example.com",
+ "version": "12.10.0-pre",
+ "installation_type": "omnibus-gitlab",
+ "active_user_count": 999,
+ "recorded_at": "2020-04-17T07:43:54.162+00:00",
+ "edition": "EEU",
+ "license_md5": "00000000000000000000000000000000",
+ "license_id": null,
+ "historical_max_users": 999,
+ "licensee": {
+ "Name": "ABC, Inc.",
+ "Email": "email@example.com",
+ "Company": "ABC, Inc."
+ },
+ "license_user_count": 999,
+ "license_starts_at": "2020-01-01",
+ "license_expires_at": "2021-01-01",
+ "license_plan": "ultimate",
+ "license_add_ons": {
+ },
+ "license_trial": false,
+ "counts": {
+ "assignee_lists": 999,
+ "boards": 999,
+ "ci_builds": 999,
+ ...
+ },
+ "container_registry_enabled": true,
+ "dependency_proxy_enabled": false,
+ "gitlab_shared_runners_enabled": true,
+ "gravatar_enabled": true,
+ "influxdb_metrics_enabled": true,
+ "ldap_enabled": false,
+ "mattermost_enabled": false,
+ "omniauth_enabled": true,
+ "prometheus_metrics_enabled": false,
+ "reply_by_email_enabled": "incoming+%{key}@incoming.gitlab.com",
+ "signup_enabled": true,
+ "web_ide_clientside_preview_enabled": true,
+ "ingress_modsecurity_enabled": true,
+ "projects_with_expiration_policy_disabled": 999,
+ "projects_with_expiration_policy_enabled": 999,
+ ...
+ "elasticsearch_enabled": true,
+ "license_trial_ends_on": null,
+ "geo_enabled": false,
+ "git": {
+ "version": {
+ "major": 2,
+ "minor": 26,
+ "patch": 1
+ }
+ },
+ "gitaly": {
+ "version": "12.10.0-rc1-93-g40980d40",
+ "servers": 56,
+ "filesystems": [
+ "EXT_2_3_4"
+ ]
+ },
+ "gitlab_pages": {
+ "enabled": true,
+ "version": "1.17.0"
+ },
+ "database": {
+ "adapter": "postgresql",
+ "version": "9.6.15"
+ },
+ "app_server": {
+ "type": "console"
+ },
+ "avg_cycle_analytics": {
+ "issue": {
+ "average": 999,
+ "sd": 999,
+ "missing": 999
+ },
+ "plan": {
+ "average": null,
+ "sd": 999,
+ "missing": 999
+ },
+ "code": {
+ "average": null,
+ "sd": 999,
+ "missing": 999
+ },
+ "test": {
+ "average": null,
+ "sd": 999,
+ "missing": 999
+ },
+ "review": {
+ "average": null,
+ "sd": 999,
+ "missing": 999
+ },
+ "staging": {
+ "average": null,
+ "sd": 999,
+ "missing": 999
+ },
+ "production": {
+ "average": null,
+ "sd": 999,
+ "missing": 999
+ },
+ "total": 999
+ },
+ "usage_activity_by_stage": {
+ "configure": {
+ "project_clusters_enabled": 999,
+ ...
+ },
+ "create": {
+ "merge_requests": 999,
+ ...
+ },
+ "manage": {
+ "events": 999,
+ ...
+ },
+ "monitor": {
+ "clusters": 999,
+ ...
+ },
+ "package": {
+ "projects_with_packages": 999
+ },
+ "plan": {
+ "issues": 999,
+ ...
+ },
+ "release": {
+ "deployments": 999,
+ ...
+ },
+ "secure": {
+ "user_container_scanning_jobs": 999,
+ ...
+ },
+ "verify": {
+ "ci_builds": 999,
+ ...
+ }
+ },
+ "usage_activity_by_stage_monthly": {
+ "configure": {
+ "project_clusters_enabled": 999,
+ ...
+ },
+ "create": {
+ "merge_requests": 999,
+ ...
+ },
+ "manage": {
+ "events": 999,
+ ...
+ },
+ "monitor": {
+ "clusters": 999,
+ ...
+ },
+ "package": {
+ "projects_with_packages": 999
+ },
+ "plan": {
+ "issues": 999,
+ ...
+ },
+ "release": {
+ "deployments": 999,
+ ...
+ },
+ "secure": {
+ "user_container_scanning_jobs": 999,
+ ...
+ },
+ "verify": {
+ "ci_builds": 999,
+ ...
+ }
+ }
+}
+```
+
+## Disabling usage ping
+
+The usage ping is opt-out. If you want to deactivate this feature, go to the Settings page of your administration panel and uncheck the Usage Ping checkbox.
+
+To disable the usage ping and prevent it from being configured in future through the administration panel, Omnibus installs can set the following in [`gitlab.rb`](https://docs.gitlab.com/omnibus/settings/configuration.html#configuration-options):
+
+```ruby
+gitlab_rails['usage_ping_enabled'] = false
+```
+
+And source installs can set the following in `gitlab.yml`:
+
+```yaml
+production: &base
+ # ...
+ gitlab:
+ # ...
+ usage_ping_enabled: false
+```
+
+## Usage Ping Request Flow
+
+The following example shows a basic request/response flow between a GitLab Instance, the Versions Application, the License Application, Salesforce, GitLab's S3 Bucket, GitLab's Snowflake Data Warehouse, and Sisense.:
+
+```mermaid
+sequenceDiagram
+ participant GitLab Instance
+ participant Versions Application
+ participant Licenses Application
+ participant Salesforce
+ participant S3 Bucket
+ participant Snowflake DW
+ participant Sisense Dashboards
+ GitLab Instance->>Versions Application: Send Usage Ping
+ loop Process usage data
+ Versions Application->>Versions Application: Parse usage data
+ Versions Application->>Versions Application: Write to database
+ Versions Application->>Versions Application: Update license ping time
+ end
+ loop Process data for Salesforce
+ Versions Application-xLicenses Application: Request Zuora subscription id
+ Licenses Application-xVersions Application: Zuora subscription id
+ Versions Application-xSalesforce: Request Zuora account id by Zuora subscription id
+ Salesforce-xVersions Application: Zuora account id
+ Versions Application-xSalesforce: Usage data for the Zuora account
+ end
+ Versions Application->>S3 Bucket: Export Versions database
+ S3 Bucket->>Snowflake DW: Import data
+ Snowflake DW->>Snowflake DW: Transform data using dbt
+ Snowflake DW->>Sisense Dashboards: Data available for querying
+ Versions Application->>GitLab Instance: DevOps Score (Conversational Development Index)
+```
+
+## How Usage Ping works
+
+1. The Usage Ping [cron job](https://gitlab.com/gitlab-org/gitlab/-/blob/master/app/workers/gitlab_usage_ping_worker.rb#L30) is set in Sidekiq to run weekly.
+1. When the cron job runs, it calls [GitLab::UsageData.to_json](https://gitlab.com/gitlab-org/gitlab/-/blob/master/app/services/submit_usage_ping_service.rb#L22).
+1. GitLab::UsageData.to_json [cascades down](https://gitlab.com/gitlab-org/gitlab/-/blob/master/lib/gitlab/usage_data.rb#L22) to ~400+ other counter method calls.
+1. The response of all methods calls are [merged together](https://gitlab.com/gitlab-org/gitlab/-/blob/master/lib/gitlab/usage_data.rb#L14) into a single JSON payload in GitLab::UsageData.to_json.
+1. The JSON payload is then [posted to the Versions application]( https://gitlab.com/gitlab-org/gitlab/-/blob/master/app/services/submit_usage_ping_service.rb#L20).
+
+## Implementing Usage Ping
+
+Usage Ping consists of four types of counters which are all found in `usage_data.rb`:
+
+- **Ordinary Batch Counters:** Simple count of a given ActiveRecord_Relation
+- **Distinct Batch Counters:** Distinct count of a given ActiveRecord_Relation on given column
+- **Alternative Counters:** Used for settings and configurations
+- **Redis Counters:** Used for in-memory counts. This method is being deprecated due to data inaccuracies and will be replaced with a persistent method.
+
+Note: Only use the provided counter methods. Each counter method contains a built in fail safe to isolate each counter to avoid breaking the entire Usage Ping.
+
+### Why batch counting
+
+For large tables, PostgreSQL can take a long time to count rows due to MVCC [(Multi-version Concurrency Control)](https://en.wikipedia.org/wiki/Multiversion_concurrency_control). Batch counting is a counting method where a single large query is broken into multiple smaller queries. For example, instead of a single query querying 1,000,000 records, with batch counting, you can execute 100 queries of 10,000 records each. Batch counting is useful for avoiding database timeouts as each batch query is significantly shorter than one single long running query.
+
+For GitLab.com, there are extremely large tables with 15 second query timeouts, so, we use batch counting to avoid encountering timeouts. Here are the sizes of some GitLab.com tables:
+
+| Table | Row counts in millions |
+| ------ | ------ |
+| merge_request_diff_commits | 2280 |
+| ci_build_trace_sections | 1764 |
+| merge_request_diff_files | 1082 |
+| events | 514 |
+
+There are two batch counting methods provided, `Ordinary Batch Counters` and `Distinct Batch Counters`. Batch counting requires indexes on columns to calculate max, min, and range queries. In some cases, a specialized index may need to be added on the columns involved in a counter.
+
+### Ordinary Batch Counters
+
+Handles `ActiveRecord::StatementInvalid` error
+
+Simple count of a given ActiveRecord_Relation
+
+Method: `count(relation, column = nil, batch: true, start: nil, finish: nil)`
+
+Arguments:
+
+- `relation` the ActiveRecord_Relation to perform the count
+- `column` the column to perform the count on, by default is the primary key
+- `batch`: default `true` in order to use batch counting
+- `start`: custom start of the batch counting in order to avoid complex min calculations
+- `end`: custom end of the batch counting in order to avoid complex min calculations
+
+Examples:
+
+```ruby
+count(User.active)
+count(::Clusters::Cluster.aws_installed.enabled, :cluster_id)
+count(::Clusters::Cluster.aws_installed.enabled, :cluster_id, start: ::Clusters::Cluster.minimum(:id), finish: ::Clusters::Cluster.maximum(:id))
+```
+
+### Distinct Batch Counters
+
+Handles `ActiveRecord::StatementInvalid` error
+
+Distinct count of a given ActiveRecord_Relation on given column
+
+Method: `distinct_count(relation, column = nil, batch: true, start: nil, finish: nil)`
+
+Arguments:
+
+- `relation` the ActiveRecord_Relation to perform the count
+- `column` the column to perform the distinct count, by default is the primary key
+- `batch`: default `true` in order to use batch counting
+- `start`: custom start of the batch counting in order to avoid complex min calculations
+- `end`: custom end of the batch counting in order to avoid complex min calculations
+
+Examples:
+
+```ruby
+distinct_count(::Project, :creator_id)
+distinct_count(::Note.with_suggestions.where(time_period), :author_id, start: ::User.minimum(:id), finish: ::User.maximum(:id))
+distinct_count(::Clusters::Applications::CertManager.where(time_period).available.joins(:cluster), 'clusters.user_id')
+```
+
+### Redis Counters
+
+Handles `::Redis::CommandError` and `Gitlab::UsageDataCounters::BaseCounter::UnknownEvent`
+returns -1 when a block is sent or hash with all values -1 when a `counter(Gitlab::UsageDataCounters)` is sent
+different behavior due to 2 different implementations of Redis counter
+
+Method: `redis_usage_data(counter, &block)`
+
+Arguments:
+
+- `counter`: a counter from `Gitlab::UsageDataCounters`, that has `fallback_totals` method implemented
+- or a `block`: wich is evaluated
+
+Example of usage:
+
+```ruby
+redis_usage_data(Gitlab::UsageDataCounters::WikiPageCounter)
+redis_usage_data { ::Gitlab::UsageCounters::PodLogs.usage_totals[:total] }
+```
+
+Note that Redis counters are in the [process of being deprecated](https://gitlab.com/gitlab-org/gitlab/-/issues/216330) and you should instead try to use Snowplow events instead. We're in the process of building [self-managed event tracking](https://gitlab.com/gitlab-org/telemetry/-/issues/373) and once this is available, we will convert all Redis counters into Snowplow events.
+
+### Alternative Counters
+
+Handles `StandardError` and fallbacks into -1 this way not all measures fail if we encounter one exception.
+Mainly used for settings and configurations.
+
+Method: `alt_usage_data(value = nil, fallback: -1, &block)`
+
+Arguments:
+
+- `value`: a simple static value in wich case the value is simply returned.
+- or a `block`: wich is evaluated
+- `fallback: -1`: the common value used for any metrics that are failing.
+
+Example of usage:
+
+```ruby
+alt_usage_data { Gitlab::VERSION }
+alt_usage_data { Gitlab::CurrentSettings.uuid }
+alt_usage_data(999)
+```
+
+## Developing and testing Usage Ping
+
+### 1. Use your Rails console to manually test counters
+
+```ruby
+# count
+Gitlab::UsageData.count(User.active)
+Gitlab::UsageData.count(::Clusters::Cluster.aws_installed.enabled, :cluster_id)
+
+# count distinct
+Gitlab::UsageData.distinct_count(::Project, :creator_id)
+Gitlab::UsageData.distinct_count(::Note.with_suggestions.where(time_period), :author_id, start: ::User.minimum(:id), finish: ::User.maximum(:id))
+```
+
+### 2. Generate the SQL query
+
+Your Rails console will give back the generated SQL queries.
+
+Example:
+
+```ruby
+ pry(main)> Gitlab::UsageData.count(User.active)
+ (0.4ms) SELECT "features"."key" FROM "features"
+ (0.7ms) SELECT MIN("users"."id") FROM "users" WHERE ("users"."state" IN ('active')) AND (ghost IS NOT TRUE) AND ("users"."user_type" IS NULL OR "users"."user_type" NOT IN (2, 1, 3))
+ (0.6ms) SELECT MAX("users"."id") FROM "users" WHERE ("users"."state" IN ('active')) AND (ghost IS NOT TRUE) AND ("users"."user_type" IS NULL OR "users"."user_type" NOT IN (2, 1, 3))
+ (0.5ms) SELECT COUNT("users"."id") FROM "users" WHERE ("users"."state" IN ('active')) AND (ghost IS NOT TRUE) AND ("users"."user_type" IS NULL OR "users"."user_type" NOT IN (2, 1, 3)) AND "users"."id" BETWEEN 0 AND 99999
+```
+
+### 3. Optimize queries with #database-lab
+
+Paste the SQL query into `#database-lab` to see how the query performs at scale.
+
+- #database-lab is a Slack channel which uses a production-sized environment to test your queries
+- GitLab.com’s production database has a 15 second timeout.
+- For each query we require an execution time of under 1 second due do cold caches which can 10x this time.
+- Add a specialized index on columns involved to reduce your the execution time.
+
+In order to have an understanding of the queries execution we add in the MR description the following information
+
+For counters that have a `time_period` test and add information for both cases.
+
+- with `time_period = {}` for all time period
+- and `time_period = { created_at: 28.days.ago..Time.current }` for last 28 days period
+
+Execution plan and query time before and after optimization
+
+Using database-lab and [explain.depesz.com](https://explain.depesz.com/) see more details in [database review guide](../database_review.md#preparation-when-adding-or-modifying-queries)
+
+Query generated for the index and time
+
+Using database-lab
+
+Migration output for up and down execution
+
+Examples of query optimization work:
+
+- [Example 1](https://gitlab.com/gitlab-org/gitlab/-/merge_requests/26445)
+- [Example 2](https://gitlab.com/gitlab-org/gitlab/-/merge_requests/26871)
+
+### 4. Ask for a Telemetry Review
+
+On GitLab.com, we have DangerBot setup to monitor Telemetry related files and DangerBot will recommend a Telemetry review. Simply `@gitlab-org/growth/telemetry/engineers` in your MR for a review.
diff --git a/doc/development/testing_guide/best_practices.md b/doc/development/testing_guide/best_practices.md
index 62767180077..f0137e542cc 100644
--- a/doc/development/testing_guide/best_practices.md
+++ b/doc/development/testing_guide/best_practices.md
@@ -17,24 +17,22 @@ our test design. We can find some helpful heuristics documented in the Handbook
## Test speed
-GitLab has a massive test suite that, without [parallelization], can take hours
+GitLab has a massive test suite that, without [parallelization](ci.md#test-suite-parallelization-on-the-ci), can take hours
to run. It's important that we make an effort to write tests that are accurate
and effective _as well as_ fast.
Here are some things to keep in mind regarding test performance:
-- `double` and `spy` are faster than `FactoryBot.build(...)`
+- `instance_double` and `spy` are faster than `FactoryBot.build(...)`
- `FactoryBot.build(...)` and `.build_stubbed` are faster than `.create`.
- Don't `create` an object when `build`, `build_stubbed`, `attributes_for`,
- `spy`, or `double` will do. Database persistence is slow!
+ `spy`, or `instance_double` will do. Database persistence is slow!
- Don't mark a feature as requiring JavaScript (through `:js` in RSpec) unless it's _actually_ required for the test
to be valid. Headless browser testing is slow!
-[parallelization]: ci.md#test-suite-parallelization-on-the-ci
-
## RSpec
-To run rspec tests:
+To run RSpec tests:
```shell
# run all tests
@@ -71,6 +69,8 @@ FDOC=1 bin/rspec spec/[path]/[to]/[spec].rb
- Use `Gitlab.config.gitlab.host` rather than hard coding `'localhost'`
- Don't assert against the absolute value of a sequence-generated attribute (see
[Gotchas](../gotchas.md#do-not-assert-against-the-absolute-value-of-a-sequence-generated-attribute)).
+- Avoid using `expect_any_instance_of` or `allow_any_instance_of` (see
+ [Gotchas](../gotchas.md#do-not-assert-against-the-absolute-value-of-a-sequence-generated-attribute)).
- Don't supply the `:each` argument to hooks since it's the default.
- On `before` and `after` hooks, prefer it scoped to `:context` over `:all`
- When using `evaluate_script("$('.js-foo').testSomething()")` (or `execute_script`) which acts on a given element,
@@ -320,26 +320,48 @@ stub_feature_flags(ci_live_trace: false)
Feature.enabled?(:ci_live_trace) # => false
```
-If you wish to set up a test where a feature flag is disabled for some
-actors and not others, you can specify this in options passed to the
-helper. For example, to disable the `ci_live_trace` feature flag for a
-specifc project:
+If you wish to set up a test where a feature flag is enabled only
+for some actors and not others, you can specify this in options
+passed to the helper. For example, to enable the `ci_live_trace`
+feature flag for a specifc project:
```ruby
project1, project2 = build_list(:project, 2)
-# Feature will only be disabled for project1
-stub_feature_flags(ci_live_trace: { enabled: false, thing: project1 })
+# Feature will only be enabled for project1
+stub_feature_flags(ci_live_trace: project1)
+
+Feature.enabled?(:ci_live_trace) # => false
+Feature.enabled?(:ci_live_trace, project1) # => true
+Feature.enabled?(:ci_live_trace, project2) # => false
+```
+
+This represents an actual behavior of FlipperGate:
-Feature.enabled?(:ci_live_trace, project1) # => false
-Feature.enabled?(:ci_live_trace, project2) # => true
+1. You can enable an override for a specified actor to be enabled
+1. You can disable (remove) an override for a specified actor,
+ fallbacking to default state
+1. There's no way to model that you explicitly disable a specified actor
+
+```ruby
+Feature.enable(:my_feature)
+Feature.disable(:my_feature, project1)
+Feature.enabled?(:my_feature) # => true
+Feature.enabled?(:my_feature, project1) # => true
+```
+
+```ruby
+Feature.disable(:my_feature2)
+Feature.enable(:my_feature2, project1)
+Feature.enabled?(:my_feature2) # => false
+Feature.enabled?(:my_feature2, project1) # => true
```
### Pristine test environments
The code exercised by a single GitLab test may access and modify many items of
data. Without careful preparation before a test runs, and cleanup afterward,
-data can be changed by a test in such a way that it affects the behaviour of
+data can be changed by a test in such a way that it affects the behavior of
following tests. This should be avoided at all costs! Fortunately, the existing
test framework handles most cases already.
@@ -493,7 +515,7 @@ range of inputs. By specifying the test case once, alongside a table of inputs
and the expected output for each, your tests can be made easier to read and more
compact.
-We use the [rspec-parameterized](https://github.com/tomykaira/rspec-parameterized)
+We use the [RSpec::Parameterized](https://github.com/tomykaira/rspec-parameterized)
gem. A short example, using the table syntax and checking Ruby equality for a
range of inputs, might look like this:
@@ -526,7 +548,7 @@ objects, FactoryBot-created objects etc. can lead to
### Prometheus tests
Prometheus metrics may be preserved from one test run to another. To ensure that metrics are
-reset before each example, add the `:prometheus` tag to the Rspec test.
+reset before each example, add the `:prometheus` tag to the RSpec test.
### Matchers
@@ -651,7 +673,7 @@ end
### Factories
-GitLab uses [factory_bot] as a test fixture replacement.
+GitLab uses [factory_bot](https://github.com/thoughtbot/factory_bot) as a test fixture replacement.
- Factory definitions live in `spec/factories/`, named using the pluralization
of their corresponding model (`User` factories are defined in `users.rb`).
@@ -666,8 +688,6 @@ GitLab uses [factory_bot] as a test fixture replacement.
- Factories don't have to be limited to `ActiveRecord` objects.
[See example](https://gitlab.com/gitlab-org/gitlab-foss/commit/0b8cefd3b2385a21cfed779bd659978c0402766d).
-[factory_bot]: https://github.com/thoughtbot/factory_bot
-
### Fixtures
All fixtures should be placed under `spec/fixtures/`.
diff --git a/doc/development/testing_guide/end_to_end/beginners_guide.md b/doc/development/testing_guide/end_to_end/beginners_guide.md
new file mode 100644
index 00000000000..73960a2f74d
--- /dev/null
+++ b/doc/development/testing_guide/end_to_end/beginners_guide.md
@@ -0,0 +1,340 @@
+# Beginner's guide to writing end-to-end tests
+
+In this tutorial, you will learn about the creation of end-to-end (_e2e_) tests
+for [GitLab Community Edition](https://about.gitlab.com/install/?version=ce) and
+[GitLab Enterprise Edition](https://about.gitlab.com/install/).
+
+By the end of this tutorial, you will be able to:
+
+- Determine whether an end-to-end test is needed.
+- Understand the directory structure within `qa/`.
+- Write a basic end-to-end test that will validate login features.
+- Develop any missing [page object](page_objects.md) libraries.
+
+## Before you write a test
+
+Before you write tests, your
+[GitLab Development Kit (GDK)](https://gitlab.com/gitlab-org/gitlab-development-kit)
+must be configured to run the specs. The end-to-end tests:
+
+- Are contained within the `qa/` directory.
+- Should be independent and
+ [idempotent](https://en.wikipedia.org/wiki/Idempotence#Computer_science_meaning).
+- Create [resources](resources.md) (such as project, issue, user) on an ad-hoc basis.
+- Test the UI and API interfaces, and use the API to efficiently set up the UI tests.
+
+TIP: **Tip:**
+For more information, see [End-to-end testing Best Practices](best_practices.md).
+
+## Determine if end-to-end tests are needed
+
+Check the code coverage of a specific feature before writing end-to-end tests,
+for both [GitLab Community Edition](https://gitlab-org.gitlab.io/gitlab-foss/coverage-ruby/#_AllFiles)
+and [GitLab Enterprise Edition](https://gitlab-org.gitlab.io/gitlab/coverage-ruby/#_AllFiles) projects.
+Does sufficient test coverage exist at the unit, feature, or integration levels?
+If you answered *yes*, then you *don't* need an end-to-end test.
+
+For information about the distribution of tests per level in GitLab, see
+[Testing Levels](https://gitlab.com/gitlab-org/gitlab/blob/master/doc/development/testing_guide/testing_levels.md).
+
+- See the
+ [How to test at the correct level?](https://gitlab.com/gitlab-org/gitlab/blob/master/doc/development/testing_guide/testing_levels.md#how-to-test-at-the-correct-level)
+ section of the [Testing levels](https://gitlab.com/gitlab-org/gitlab/blob/master/doc/development/testing_guide/testing_levels.md) document.
+- Review how often the feature changes. Stable features that don't change very often
+ might not be worth covering with end-to-end tests if they are already covered
+ in lower level tests.
+- Finally, discuss the proposed test with the developer(s) involved in implementing
+ the feature and the lower-level tests.
+
+CAUTION: **Caution:**
+Check both [GitLab Community Edition](https://gitlab-org.gitlab.io/gitlab-foss/coverage-ruby/#_AllFiles) and
+[GitLab Enterprise Edition](https://gitlab-org.gitlab.io/gitlab/coverage-ruby/#_AllFiles) coverage projects
+for previously-written tests for this feature. For analyzing the code coverage,
+you must understand which application files implement specific features.
+
+NOTE: **Note:**
+In this tutorial we're writing a login end-to-end test, even though it has been
+sufficiently covered by lower-level testing, because it's the first step for most
+end-to-end flows, and is easiest to understand.
+
+## Identify the DevOps stage
+
+The GitLab QA end-to-end tests are organized by the different
+[stages in the DevOps lifecycle](https://gitlab.com/gitlab-org/gitlab-foss/tree/master/qa/qa/specs/features/browser_ui).
+Determine where the test should be placed by
+[stage](https://about.gitlab.com/handbook/product/categories/#devops-stages),
+determine which feature the test will belong to, and then place it in a subdirectory
+under the stage.
+
+![DevOps lifecycle by stages](img/gl-devops-lifecycle-by-stage-numbers_V12_10.png)
+
+NOTE: **Note:**
+If the test is Enterprise Edition only, the test will be created in the `features/ee`
+directory, but follow the same DevOps lifecycle format.
+
+## Create a skeleton test
+
+In the first part of this tutorial we will be testing login, which is owned by the
+Manage stage. Inside `qa/specs/features/browser_ui/1_manage/login`, create a
+file `basic_login_spec.rb`.
+
+### The outer `context` block
+
+Specs have an outer `context` indicating the DevOps stage.
+
+```ruby
+# frozen_string_literal: true
+
+module QA
+ context 'Manage' do
+
+ end
+end
+```
+
+### The `describe` block
+
+Inside of our outer `context`, describe the feature to test. In this case, `Login`.
+
+```ruby
+# frozen_string_literal: true
+
+module QA
+ context 'Manage' do
+ describe 'Login' do
+
+ end
+ end
+end
+```
+
+### The `it` blocks (examples)
+
+Every test suite contains at least one `it` block (example). A good way to start
+writing end-to-end tests is to write test case descriptions as `it` blocks:
+
+```ruby
+module QA
+ context 'Manage' do
+ describe 'Login' do
+ it 'can login' do
+
+ end
+
+ it 'can logout' do
+
+ end
+ end
+ end
+end
+```
+
+## Write the test
+
+An important question is "What do we test?" and even more importantly, "How do we test?"
+
+Begin by logging in.
+
+```ruby
+# frozen_string_literal: true
+
+module QA
+ context 'Manage' do
+ describe 'Login' do
+ it 'can login' do
+ Flow::Login.sign_in
+
+ end
+
+ it 'can logout' do
+ Flow::Login.sign_in
+
+ end
+ end
+ end
+end
+```
+
+After [running the spec](#run-the-spec), our test should login and end; then we
+should answer the question "What do we test?"
+
+```ruby
+# frozen_string_literal: true
+
+module QA
+ context 'Manage' do
+ describe 'Login' do
+ it 'can login' do
+ Flow::Login.sign_in
+
+ Page::Main::Menu.perform do |menu|
+ expect(menu).to be_signed_in
+ end
+ end
+
+ it 'can logout' do
+ Flow::Login.sign_in
+
+ Page::Main::Menu.perform do |menu|
+ menu.sign_out
+
+ expect(menu).not_to be_signed_in
+ end
+ end
+ end
+ end
+end
+```
+
+**What do we test?**
+
+1. Can we log in?
+1. Can we log out?
+
+**How do we test?**
+
+1. Check if the user avatar appears in the top navigation.
+1. Check if the user avatar *does not* appear in the top navigation.
+
+NOTE: **Note:**
+Behind the scenes, `be_signed_in` is a
+[predicate matcher](https://relishapp.com/rspec/rspec-expectations/v/3-8/docs/built-in-matchers/predicate-matchers)
+that [implements checking the user avatar](https://gitlab.com/gitlab-org/gitlab/-/blob/master/qa/qa/page/main/menu.rb#L74).
+
+## De-duplicate your code
+
+Refactor your test to use a `before` block for test setup, since it's duplicating
+a call to `sign_in`.
+
+```ruby
+# frozen_string_literal: true
+
+module QA
+ context 'Manage' do
+ describe 'Login' do
+ before do
+ Flow::Login.sign_in
+ end
+
+ it 'can login' do
+ Page::Main::Menu.perform do |menu|
+ expect(menu).to be_signed_in
+ end
+ end
+
+ it 'can logout' do
+ Page::Main::Menu.perform do |menu|
+ menu.sign_out
+
+ expect(menu).not_to be_signed_in
+ end
+ end
+ end
+ end
+end
+```
+
+The `before` block is essentially a `before(:each)` and is run before each example,
+ensuring we now log in at the beginning of each test.
+
+## Test setup using resources and page objects
+
+Next, let's test something other than Login. Let's test Issues, which are owned by the Plan
+stage, so [create a file](#identify-the-devops-stage) in
+`qa/specs/features/browser_ui/3_create/issues` called `issues_spec.rb`.
+
+```ruby
+# frozen_string_literal: true
+
+module QA
+ context 'Plan' do
+ describe 'Issues' do
+ let(:issue) do
+ Resource::Issue.fabricate_via_api! do |issue|
+ issue.title = 'My issue'
+ issue.description = 'This is an issue specific to this test'
+ end
+ end
+
+ before do
+ Flow::Login.sign_in
+ issue.visit!
+ end
+
+ it 'can close an issue' do
+ Page::Project::Issue::Show.perform do |show|
+ show.click_close_issue_button
+
+ expect(show).to be_closed
+ end
+ end
+ end
+ end
+end
+```
+
+Note the following important points:
+
+- At the start of our example, we will be at the `page/issue/show.rb` [page](page_objects.md).
+- Our test fabricates only what it needs, when it needs it.
+- The issue is fabricated through the API to save time.
+- GitLab prefers `let()` over instance variables. See
+ [best practices](../best_practices.md#let-variables).
+- `be_closed` is not implemented in `page/project/issue/show.rb` yet, but will be
+ implemented in the next step.
+
+The issue is fabricated as a [Resource](resources.md), which is a GitLab entity
+you can create through the UI or API. Other examples include:
+
+- A [Merge Request](https://gitlab.com/gitlab-org/gitlab/-/blob/master/qa/qa/resource/merge_request.rb).
+- A [User](https://gitlab.com/gitlab-org/gitlab/-/blob/master/qa/qa/resource/user.rb).
+- A [Project](https://gitlab.com/gitlab-org/gitlab/-/blob/master/qa/qa/resource/project.rb).
+- A [Group](https://gitlab.com/gitlab-org/gitlab/-/blob/master/qa/qa/resource/group.rb).
+
+## Write the page object
+
+A [Page Object](page_objects.md) is a class in our suite that represents a page
+within GitLab. The **Login** page would be one example. Since our page object for
+the **Issue Show** page already exists, add the `closed?` method.
+
+```ruby
+module Page::Project::Issue
+ class Show
+ view 'app/views/projects/issues/show.html.haml' do
+ element :closed_status_box
+ end
+
+ def closed?
+ has_element?(:closed_status_box)
+ end
+ end
+end
+```
+
+Next, define the element `closed_status_box` within your view, so your Page Object
+can see it.
+
+```haml
+-#=> app/views/projects/issues/show.html.haml
+.issuable-status-box.status-box.status-box-issue-closed{ ..., data: { qa_selector: 'closed_status_box' } }
+```
+
+## Run the spec
+
+Before running the spec, confirm:
+
+- The GDK is installed.
+- The GDK is running on port 3000 locally.
+- No additional [RSpec metadata tags](rspec_metadata_tests.md) have been applied.
+- Your working directory is `qa/` within your GDK GitLab installation.
+
+To run the spec, run the following command:
+
+```ruby
+bundle exec bin/qa Test::Instance::All http://localhost:3000 -- <test_file>
+```
+
+Where `<test_file>` is:
+
+- `qa/specs/features/browser_ui/1_manage/login/login_spec.rb` when running the Login example.
+- `qa/specs/features/browser_ui/2_plan/issues/issue_spec.rb` when running the Issue example.
diff --git a/doc/development/testing_guide/end_to_end/img/gl-devops-lifecycle-by-stage-numbers_V12_10.png b/doc/development/testing_guide/end_to_end/img/gl-devops-lifecycle-by-stage-numbers_V12_10.png
new file mode 100644
index 00000000000..d11305c3686
--- /dev/null
+++ b/doc/development/testing_guide/end_to_end/img/gl-devops-lifecycle-by-stage-numbers_V12_10.png
Binary files differ
diff --git a/doc/development/testing_guide/end_to_end/index.md b/doc/development/testing_guide/end_to_end/index.md
index 443b7b06a24..e6a683e9148 100644
--- a/doc/development/testing_guide/end_to_end/index.md
+++ b/doc/development/testing_guide/end_to_end/index.md
@@ -180,7 +180,7 @@ instance-level scenarios](https://gitlab.com/gitlab-org/gitlab-foss/tree/master/
Continued reading:
-- [Quick Start Guide](quick_start_guide.md)
+- [Beginner's Guide](beginners_guide.md)
- [Style Guide](style_guide.md)
- [Best Practices](best_practices.md)
- [Testing with feature flags](feature_flags.md)
diff --git a/doc/development/testing_guide/end_to_end/page_objects.md b/doc/development/testing_guide/end_to_end/page_objects.md
index 22e7375be1f..9d4fa5316c4 100644
--- a/doc/development/testing_guide/end_to_end/page_objects.md
+++ b/doc/development/testing_guide/end_to_end/page_objects.md
@@ -238,6 +238,53 @@ the view for code in a library.
In such rare cases it's reasonable to use CSS selectors in page object methods,
with a comment explaining why an `element` can't be added.
+### Define Page concerns
+
+Some pages share common behaviors, and/or are prepended with EE-specific modules that adds EE-specific methods.
+
+These modules must:
+
+1. Extend from the `QA::Page::PageConcern` module, with `extend QA::Page::PageConcern`.
+1. Override the `self.prepended` method if they need to `include`/`prepend` other modules themselves, and/or define
+ `view` or `elements`.
+1. Call `super` as the first thing in `self.prepended`.
+1. Include/prepend other modules and define their `view`/`elements` in a `base.class_eval` block to ensure they're
+ defined in the class that prepends the module.
+
+These steps ensure the sanity selectors check will detect problems properly.
+
+For example, `qa/qa/ee/page/merge_request/show.rb` adds EE-specific methods to `qa/qa/page/merge_request/show.rb` (with
+`QA::Page::MergeRequest::Show.prepend_if_ee('QA::EE::Page::MergeRequest::Show')`) and following is how it's implemented
+(only showing the relevant part and refering to the 4 steps described above with inline comments):
+
+```ruby
+module QA
+ module EE
+ module Page
+ module MergeRequest
+ module Show
+ extend QA::Page::PageConcern # 1.
+
+ def self.prepended(base) # 2.
+ super # 3.
+
+ base.class_eval do # 4.
+ prepend Page::Component::LicenseManagement
+
+ view 'app/assets/javascripts/vue_merge_request_widget/components/states/sha_mismatch.vue' do
+ element :head_mismatch, "The source branch HEAD has recently changed."
+ end
+
+ [...]
+ end
+ end
+ end
+ end
+ end
+ end
+end
+```
+
## Running the test locally
During development, you can run the `qa:selectors` test by running
diff --git a/doc/development/testing_guide/end_to_end/quick_start_guide.md b/doc/development/testing_guide/end_to_end/quick_start_guide.md
deleted file mode 100644
index 0ae3f375284..00000000000
--- a/doc/development/testing_guide/end_to_end/quick_start_guide.md
+++ /dev/null
@@ -1,621 +0,0 @@
-# Writing end-to-end tests step-by-step
-
-In this tutorial, you will find different examples, and the steps involved, in the creation of end-to-end (_e2e_) tests for GitLab CE and GitLab EE, using GitLab QA.
-
-When referring to end-to-end tests in this document, this means testing a specific feature end-to-end such as:
-
-- A user logging in.
-- The creation of a project.
-- The management of labels.
-- Breaking down epics into sub-epics and issues.
-
-## Important information before we start writing tests
-
-It's important to understand that end-to-end tests of isolated features, such as the ones described in the above note, doesn't mean that everything needs to happen through the GUI.
-
-If you don't exactly understand what we mean by **not everything needs to happen through the GUI,** please make sure you've read the [best practices](best_practices.md) before moving on.
-
-## This document covers the following items
-
-- [0.](#0-are-end-to-end-tests-needed) Identifying if end-to-end tests are really needed
-- [1.](#1-identifying-the-devops-stage) Identifying the [DevOps stage](https://about.gitlab.com/stages-devops-lifecycle/) of the feature that you are going to cover with end-to-end tests
-- [2.](#2-test-skeleton) Creating the skeleton of the test file (`*_spec.rb`)
-- [3.](#3-test-cases-mvc) The [MVC](https://about.gitlab.com/handbook/values/#minimum-viable-change-mvc) of the test cases' logic
-- [4.](#4-extracting-duplicated-code) Extracting duplicated code into methods
-- [5.](#5-tests-pre-conditions-using-resources-and-page-objects) Tests' pre-conditions (`before :context` and `before`) using resources and [Page Objects](page_objects.md)
-- [6.](#6-optimization) Optimizing the test suite
-- [7.](#7-resources) Using and implementing resources
-- [8.](#8-page-objects) Moving element definitions and methods to [Page Objects](page_objects.md)
-
-### 0. Are end-to-end tests needed?
-
-At GitLab we respect the [test pyramid](https://gitlab.com/gitlab-org/gitlab/blob/master/doc/development/testing_guide/testing_levels.md), and so, we recommend you check the code coverage of a specific feature before writing end-to-end tests, for both [CE](https://gitlab-org.gitlab.io/gitlab-foss/coverage-ruby/#_AllFiles) and [EE](https://gitlab-org.gitlab.io/gitlab/coverage-ruby/#_AllFiles) projects.
-
-Sometimes you may notice that there is already good coverage in lower test levels, and we can stay confident that if we break a feature, we will still have quick feedback about it, even without having end-to-end tests.
-
-> For analyzing the code coverage, you will also need to understand which application files implement specific functionalities.
-
-#### Some other guidelines are as follows
-
-- Take a look at the [How to test at the correct level?](https://gitlab.com/gitlab-org/gitlab/blob/master/doc/development/testing_guide/testing_levels.md#how-to-test-at-the-correct-level) section of the [Testing levels](https://gitlab.com/gitlab-org/gitlab/blob/master/doc/development/testing_guide/testing_levels.md) document
-
-- Look into the frequency in which such a feature is changed (_Stable features that don't change very often might not be worth covering with end-to-end tests if they're already covered in lower levels_)
-
-- Finally, discuss with the developer(s) involved in developing the feature and the tests themselves, to get their feeling
-
-If after this analysis you still think that end-to-end tests are needed, keep reading.
-
-### 1. Identifying the DevOps stage
-
-The GitLab QA end-to-end tests are organized by the different [stages in the DevOps lifecycle](https://gitlab.com/gitlab-org/gitlab-foss/tree/master/qa/qa/specs/features/browser_ui), and so, if you are creating tests for issue creation, for instance, you would locate the spec files under the `qa/qa/specs/features/browser_ui/2_plan/` directory since issue creation is part of the Plan stage.
-
- In another case of a test for listing merged merge requests (MRs), the test should go under the `qa/qa/specs/features/browser_ui/3_create/` directory since merge requests are a feature from the Create stage.
-
-> There may be sub-directories inside the stages directories, for different features. For example: `.../browser_ui/2_plan/ee_epics/` and `.../browser_ui/2_plan/issues/`.
-
-Now, let's say we want to create tests for the [scoped labels](https://about.gitlab.com/releases/2019/04/22/gitlab-11-10-released/#scoped-labels) feature, available on GitLab EE Premium (this feature is part of the Plan stage.)
-
-> Because these tests are for a feature available only on GitLab EE, we need to create them in the [EE repository](https://gitlab.com/gitlab-org/gitlab).
-
-Since [there is no specific directory for this feature](https://gitlab.com/gitlab-org/gitlab/tree/master/qa/qa/specs/features/browser_ui/2_plan), we should create a sub-directory for it.
-
-Under `.../browser_ui/2_plan/`, let's create a sub-directory called `ee_scoped_labels/`.
-
-> Notice that since this feature is only available for GitLab EE we prefix the sub-directory with `ee_`.
-
-### 2. Test skeleton
-
-Inside the newly created sub-directory, let's create a file describing the test suite (e.g. `editing_scoped_labels_spec.rb`.)
-
-#### The `context` and `describe` blocks
-
-Specs have an outer `context` that indicates the DevOps stage. The next level is the `describe` block, that briefly states the subject of the test suite. See the following example:
-
-```ruby
-module QA
- context 'Plan' do
- describe 'Editing scoped labels on issues' do
- end
- end
-end
-```
-
-#### The `it` blocks
-
-Every test suite is composed of at least one `it` block, and a good way to start writing end-to-end tests is by writing test cases descriptions as `it` blocks. These might help you to think of different test scenarios. Take a look at the following example:
-
-```ruby
-module QA
- context 'Plan' do
- describe 'Editing scoped labels on issues' do
- it 'replaces an existing label if it has the same key' do
- end
-
- it 'keeps both scoped labels when adding a label with a different key' do
- end
- end
- end
-end
-```
-
-### 3. Test cases MVC
-
-For the [MVC](https://about.gitlab.com/handbook/values/#minimum-viable-change-mvc) of our test cases, let's say that we already have the application in the state needed for the tests, and then let's focus on the logic of the test cases only.
-
-To evolve the test cases drafted on step 2, let's imagine that the user is already logged into a GitLab EE instance, they already have at least a Premium license in use, there is already a project created, there is already an issue opened in the project, the issue already has a scoped label (e.g. `animal::fox`), there are other scoped labels (for the same scope and for a different scope (e.g. `animal::dolphin` and `plant::orchid`), and finally, the user is already on the issue's page. Let's also suppose that for every test case the application is in a clean state, meaning that one test case won't affect another.
-
-> Note: there are different approaches to creating an application state for end-to-end tests. Some of them are very time consuming and subject to failures, such as when using the GUI for all the pre-conditions of the tests. On the other hand, other approaches are more efficient, such as using the public APIs. The latter is more efficient since it doesn't depend on the GUI. We won't focus on this part yet, but it's good to keep it in mind.
-
-Let's now focus on the first test case.
-
-```ruby
-it 'replaces an existing label if it has the same key' do
- # This implementation is only for tutorial purposes. We normally encapsulate elements in Page Objects (which we cover on section 8).
- page.find('.block.labels .edit-link').click
- page.find('.dropdown-menu-labels .dropdown-input-field').send_keys ['animal::dolphin', :enter]
- page.find('#content-body').click
- page.refresh
-
- labels_block = page.find(%q([data-qa-selector="labels_block"]))
-
- expect(labels_block).to have_content('animal::dolphin')
- expect(labels_block).not_to have_content('animal::fox')
- expect(page).to have_content('added animal::dolphin label and removed animal::fox')
-end
-```
-
-> Notice that the test itself is simple. The most challenging part is the creation of the application state, which will be covered later.
->
-> The exemplified test case's MVC is not enough for the change to be merged, but it helps to build up the test logic. The reason is that we do not want to use locators directly in the tests, and tests **must** use [Page Objects](page_objects.md) before they can be merged. This way we better separate the responsibilities, where the Page Objects encapsulate elements and methods that allow us to interact with pages, while the spec files describe the test cases in more business-related language.
-
-Below are the steps that the test covers:
-
-1. The test finds the 'Edit' link for the labels and clicks on it.
-1. Then it fills in the 'Assign labels' input field with the value 'animal::dolphin' and press enters.
-1. Then it clicks in the content body to apply the label and refreshes the page.
-1. Finally, the expectations check that the previous scoped label was removed and that the new one was added.
-
-Let's now see how the second test case would look.
-
-```ruby
-it 'keeps both scoped labels when adding a label with a different key' do
- # This implementation is only for tutorial purposes. We normally encapsulate elements in Page Objects (which we cover on section 8).
- page.find('.block.labels .edit-link').click
- page.find('.dropdown-menu-labels .dropdown-input-field').send_keys ['plant::orchid', :enter]
- page.find('#content-body').click
- page.refresh
-
- labels_block = page.find(%q([data-qa-selector="labels_block"]))
-
- expect(labels_block).to have_content('animal::fox')
- expect(labels_block).to have_content('plant::orchid')
- expect(page).to have_content('added animal::fox')
- expect(page).to have_content('added plant::orchid')
-end
-```
-
-> Note that elements are always located using CSS selectors, and a good practice is to add test-specific selectors (this is called "testability"). For example, the `labels_block` element uses the CSS selector [`data-qa-selector="labels_block"`](page_objects.md#data-qa-selector-vs-qa-selector), which was added specifically for testing purposes.
-
-Below are the steps that the test covers:
-
-1. The test finds the 'Edit' link for the labels and clicks on it.
-1. Then it fills in the 'Assign labels' input field with the value 'plant::orchid' and press enters.
-1. Then it clicks in the content body to apply the label and refreshes the page.
-1. Finally, the expectations check that both scoped labels are present.
-
-> Similar to the previous test, this one is also very straightforward, but there is some code duplication. Let's address it.
-
-### 4. Extracting duplicated code
-
-If we refactor the tests created on step 3 we could come up with something like this:
-
-```ruby
-before do
- ...
-
- @initial_label = 'animal::fox'
- @new_label_same_scope = 'animal::dolphin'
- @new_label_different_scope = 'plant::orchid'
-
- ...
-end
-
-it 'replaces an existing label if it has the same key' do
- select_label_and_refresh @new_label_same_scope
-
- labels_block = page.find(%q([data-qa-selector="labels_block"]))
-
- expect(labels_block).to have_content(@new_label_same_scope)
- expect(labels_block).not_to have_content(@initial_label)
- expect(page).to have_content("added #{@new_label_same_scope}")
- expect(page).to have_content("and removed #{@initial_label}")
-end
-
-it 'keeps both scoped label when adding a label with a different key' do
- select_label_and_refresh @new_label_different_scope
-
- labels_block = page.find(%q([data-qa-selector="labels_block"]))
-
- expect(labels_blocks).to have_content(@new_label_different_scope)
- expect(labels_blocks).to have_content(@initial_label)
- expect(page).to have_content("added #{@new_label_different_scope}")
- expect(page).to have_content("added #{@initial_label}")
-end
-
-def select_label_and_refresh(label)
- page.find('.block.labels .edit-link').click
- page.find('.dropdown-menu-labels .dropdown-input-field').send_keys [label, :enter]
- page.find('#content-body').click
- page.refresh
-end
-```
-
-First, we remove the duplication of strings by defining the global variables `@initial_label`, `@new_label_same_scope` and `@new_label_different_scope` in the `before` block, and by using them in the expectations.
-
-Then, by creating a reusable `select_label_and_refresh` method, we remove the code duplication of this action, and later we can move this method to a Page Object class that will be created for easier maintenance purposes.
-
-Notice that the reusable method is created at the bottom of the file. This helps readability,
-where reading the code should be similar to reading a newspaper:
-
-- High-level information is at the top, like the title and summary of the news.
-- Low level, or more specific information, is at the bottom.
-
-### 5. Tests' pre-conditions using resources and Page Objects
-
-In this section, we will address the previously mentioned subject of creating the application state for the tests, using the `before :context` and `before` blocks, together with resources and Page Objects.
-
-#### `before :context`
-
-A pre-condition for the entire test suite is defined in the `before :context` block.
-
-> For our test suite, due to the need of the tests being completely independent of each other, we won't use the `before :context` block. The `before :context` block would make the tests dependent on each other because the first test changes the label of the issue, and the second one depends on the `'animal::fox'` label being set.
-
-TIP: **Tip:** In case of a test suite with only one `it` block it's ok to use only the `before` block (see below) with all the test's pre-conditions.
-
-#### `before`
-
-As the pre-conditions for our test suite, the things that needs to happen before each test starts are:
-
-- The user logging in;
-- A premium license already being set;
-- A project being created with an issue and labels already set;
-- The issue page being opened with only one scoped label applied to it.
-
-> When running end-to-end tests as part of the GitLab's continuous integration process [a license is already set as an environment variable](https://gitlab.com/gitlab-org/gitlab/blob/1a60d926740db10e3b5724713285780a4f470531/qa/qa/ee/strategy.rb#L20). For running tests locally you can set up such license by following the document [what tests can be run?](https://gitlab.com/gitlab-org/gitlab-qa/blob/master/docs/what_tests_can_be_run.md), based on the [supported GitLab environment variables](https://gitlab.com/gitlab-org/gitlab-qa/blob/master/docs/what_tests_can_be_run.md#supported-gitlab-environment-variables).
-
-#### Implementation
-
-In the following code we will focus only on the test suite's pre-conditions:
-
-```ruby
-module QA
- context 'Plan' do
- describe 'Editing scoped labels on issues' do
- before do
- Runtime::Browser.visit(:gitlab, Page::Main::Login)
- Page::Main::Login.perform(&:sign_in_using_credentials)
-
- @initial_label = 'animal::fox'
- @new_label_same_scope = 'animal::dolphin'
- @new_label_different_scope = 'plant::orchid'
-
- issue = Resource::Issue.fabricate_via_api! do |issue|
- issue.title = 'Issue to test the scoped labels'
- issue.labels = [@initial_label]
- end
-
- [@new_label_same_scope, @new_label_different_scope].each do |label|
- Resource::Label.fabricate_via_api! do |l|
- l.project = issue.project
- l.title = label
- end
- end
-
- issue.visit!
- end
-
- it 'replaces an existing label if it has the same key' do
- ...
- end
-
- it 'keeps both scoped labels when adding a label with a different key' do
- ...
- end
-
- def select_label_and_refresh(label)
- ...
- end
- end
- end
-end
-```
-
-In the `before` block we create all the application state needed for the tests to run. We do that by using the `Runtime::Browser.visit` method to go to the login page, by performing a `sign_in_using_credentials` from the `Login` Page Object, by fabricating resources via APIs (`issue`, and `Resource::Label`), and by using the `issue.visit!` to visit the issue page.
-
-> A project is created in the background by creating the `issue` resource.
->
-> When creating the [Resources](resources.md), notice that when calling the `fabricate_via_api` method, we pass some attribute:values, like `title`, and `labels` for the `issue` resource; and `project` and `title` for the `label` resource.
->
-> What's important to understand here is that by creating the application state mostly using the public APIs we save a lot of time in the test suite setup stage.
->
-> Soon we will cover the use of the already existing resources' methods and the creation of your own `fabricate_via_api` methods for resources where this is still not available, but first, let's optimize our implementation.
-
-### 6. Optimization
-
-As already mentioned in the [best practices](best_practices.md) document, end-to-end tests are very costly in terms of execution time, and it's our responsibility as software engineers to ensure that we optimize them as much as possible.
-
-> Note that end-to-end tests are slow to run and so they can have several actions and assertions in a single test, which helps us get feedback from the tests sooner. In comparison, unit tests are much faster to run and can exercise every little piece of the application in isolation, and so they usually have only one assertion per test.
-
-Some improvements that we could make in our test suite to optimize its time to run are:
-
-1. Having a single test case (an `it` block) that exercises both scenarios to avoid "wasting" time in the tests' pre-conditions, instead of having two different test cases.
-1. Making the selection of labels more performant by allowing for the selection of more than one label in the same reusable method.
-
-Let's look at a suggestion that addresses the above points, one by one:
-
-```ruby
-module QA
- context 'Plan' do
- describe 'Editing scoped labels on issues' do
- before do
- ...
- end
-
- it 'correctly applies scoped labels depending on if they are from the same or a different scope' do
- select_labels_and_refresh [@new_label_same_scope, @new_label_different_scope]
-
- labels_block = page.all(%q([data-qa-selector="labels_block"]))
-
- expect(labels_block).to have_content(@new_label_same_scope)
- expect(labels_block).to have_content(@new_label_different_scope)
- expect(labels_block).not_to have_content(@initial_label)
- expect(page).to have_content("added #{@initial_label}")
- expect(page).to have_content("added #{@new_label_same_scope} #{@new_label_different_scope} labels and removed #{@initial_label}")
- end
-
- def select_labels_and_refresh(labels)
- find('.block.labels .edit-link').click
- labels.each do |label|
- find('.dropdown-menu-labels .dropdown-input-field').send_keys [label, :enter]
- end
- find('#content-body').click
- refresh
- end
- end
- end
- end
-```
-
-To address point 1, we changed the test implementation from two `it` blocks into a single one that exercises both scenarios. Now the new test description is: `'correctly applies the scoped labels depending if they are from the same or a different scope'`. It's a long description, but it describes well what the test does.
-
-> Notice that the implementation of the new and unique `it` block had to change a little bit. Below we describe in details what it does.
-
-1. It selects two scoped labels simultaneously, one from the same scope of the one already applied in the issue during the setup phase (in the `before` block), and another one from a different scope.
-1. It asserts that the correct labels are visible in the `labels_block`, and that the labels were correctly added and removed;
-1. Finally, the `select_label_and_refresh` method is changed to `select_labels_and_refresh`, which accepts an array of labels instead of a single label, and it iterates on them for faster label selection (this is what is used in step 1 explained above.)
-
-### 7. Resources
-
-**Note:** When writing this document, some code that is now merged to master was not implemented yet, but we left them here for the readers to understand the whole process of end-to-end test creation.
-
-You can think of [Resources](resources.md) as anything that can be created on GitLab CE or EE, either through the GUI, the API, or the CLI.
-
-With that in mind, resources can be a project, an epic, an issue, a label, a commit, etc.
-
-As you saw in the tests' pre-conditions and the optimization sections, we're already creating some of these resources. We are doing that by calling the `fabricate_via_api!` method.
-
-> We could be using the `fabricate!` method instead, which would use the `fabricate_via_api!` method if it exists, and fallback to GUI fabrication otherwise, but we recommend being explicit to make it clear what the test does. Also, we always recommend fabricating resources via API since this makes tests faster and more reliable.
-
-For our test suite example, the resources that we need to create don't have the necessary code for the `fabricate_via_api!` method to correctly work (e.g., the issue and label resources), so we will have to create them.
-
-#### Implementation
-
-In the following we describe the changes needed in each of the resource files mentioned above.
-
-**Issue resource**
-
-Now, let's make it possible to create an issue resource through the API.
-
-First, in the [issue resource](https://gitlab.com/gitlab-org/gitlab/blob/d3584e80b4236acdf393d815d604801573af72cc/qa/qa/resource/issue.rb), let's expose its id and labels attributes.
-
-Add the following `attribute :id` and `attribute :labels` right above the [`attribute :title`](https://gitlab.com/gitlab-org/gitlab/blob/d3584e80b4236acdf393d815d604801573af72cc/qa/qa/resource/issue.rb#L15).
-
-> This line is needed to allow for the issue fabrication, and for labels to be automatically added to the issue when fabricating it via API.
->
-> We add the attributes above the existing attribute to keep them alphabetically organized.
-
-Then, let's initialize an instance variable for labels to allow an empty array as default value when such information is not passed during the resource fabrication, since this optional. [Between the attributes and the `fabricate!` method](https://gitlab.com/gitlab-org/gitlab/blob/1a1f1408728f19b2aa15887cd20bddab7e70c8bd/qa/qa/resource/issue.rb#L18), add the following:
-
-```ruby
-def initialize
- @labels = []
-end
-```
-
-Next, add the following code right below the [`fabricate!`](https://gitlab.com/gitlab-org/gitlab/blob/d3584e80b4236acdf393d815d604801573af72cc/qa/qa/resource/issue.rb#L27) method.
-
-```ruby
-def api_get_path
- "/projects/#{project.id}/issues/#{id}"
-end
-
-def api_post_path
- "/projects/#{project.id}/issues"
-end
-
-def api_post_body
- {
- labels: labels,
- title: title
- }
-end
-```
-
-By defining the `api_get_path` method, we allow the [`ApiFabricator`](https://gitlab.com/gitlab-org/gitlab/blob/master/qa/qa/resource/api_fabricator.rb) module to know which path to use to get a single issue.
-
-> This `GET` path can be found in the [public API documentation](../../../api/issues.md#single-issue).
-
-By defining the `api_post_path` method, we allow the [`ApiFabricator`](https://gitlab.com/gitlab-org/gitlab/blob/master/qa/qa/resource/api_fabricator.rb) module to know which path to use to create a new issue in a specific project.
-
-> This `POST` path can be found in the [public API documentation](../../../api/issues.md#new-issue).
-
-By defining the `api_post_body` method, we allow the [`ApiFabricator.api_post`](https://gitlab.com/gitlab-org/gitlab/blob/a9177ca1812bac57e2b2fa4560e1d5dd8ffac38b/qa/qa/resource/api_fabricator.rb#L68) method to know which data to send when making the `POST` request.
-
-> Notice that we pass both `labels` and `title` attributes in the `api_post_body`, where `labels` receives an array of labels, and [`title` is required](../../../api/issues.md#new-issue). Also, notice that we keep them alphabetically organized.
-
-**Label resource**
-
-Finally, let's make it possible to create label resources through the API.
-
-Add the following code right below the [`fabricate!`](https://gitlab.com/gitlab-org/gitlab/blob/a9177ca1812bac57e2b2fa4560e1d5dd8ffac38b/qa/qa/resource/label.rb#L36) method.
-
-```ruby
-def resource_web_url(resource)
- super
-rescue ResourceURLMissingError
- # this particular resource does not expose a web_url property
-end
-
-def api_get_path
- raise NotImplementedError, "The Labels API doesn't expose a single-resource endpoint so this method cannot be properly implemented."
-end
-
-def api_post_path
- "/projects/#{project.id}/labels"
-end
-
-def api_post_body
- {
- color: @color,
- name: @title
- }
-end
-```
-
-By defining the `resource_web_url(resource)` method, we override the one from the [`ApiFabricator`](https://gitlab.com/gitlab-org/gitlab/blob/master/qa/qa/resource/api_fabricator.rb#L44) module. We do that to avoid failing the test due to this particular resource not exposing a `web_url` property.
-
-By defining the `api_get_path` method, we **would** allow for the [`ApiFabricator`](https://gitlab.com/gitlab-org/gitlab/blob/master/qa/qa/resource/api_fabricator.rb) module to know which path to use to get a single label, but since there's no path available for that in the public API, we raise a `NotImplementedError` instead.
-
-By defining the `api_post_path` method, we allow for the [`ApiFabricator`](https://gitlab.com/gitlab-org/gitlab/blob/master/qa/qa/resource/api_fabricator.rb) module to know which path to use to create a new label in a specific project.
-
-By defining the `api_post_body` method, we allow for the [`ApiFabricator.api_post`](https://gitlab.com/gitlab-org/gitlab/blob/a9177ca1812bac57e2b2fa4560e1d5dd8ffac38b/qa/qa/resource/api_fabricator.rb#L68) method to know which data to send when making the `POST` request.
-
-> Notice that we pass both `color` and `name` attributes in the `api_post_body` since [those are required](../../../api/labels.md#create-a-new-label). Also, notice that we keep them alphabetically organized.
-
-### 8. Page Objects
-
-Page Objects are used in end-to-end tests for maintenance reasons, where a page's elements and methods are defined to be reused in any test.
-
-> Page Objects are auto-loaded in the [`qa/qa.rb`](https://gitlab.com/gitlab-org/gitlab/blob/master/qa/qa.rb) file and available in all the test files (`*_spec.rb`).
-
-Take a look at the [Page Objects](page_objects.md) documentation.
-
-Now, let's go back to our example.
-
-As you may have noticed, we are defining elements with CSS selectors and the `select_labels_and_refresh` method directly in the test file, and this is an anti-pattern since we need to better separate the responsibilities.
-
-To address this issue, we will move the implementation to Page Objects, and the test suite will only focus on the business rules that we are testing.
-
-#### Updates in the test file
-
-As in a test-driven development approach, let's start changing the test file even before the Page Object implementation is in place.
-
-Replace the code of the `it` block in the test file by the following:
-
-```ruby
-module QA
- context 'Plan' do
- describe 'Editing scoped labels on issues' do
- before do
- ...
- end
-
- it 'correctly applies scoped labels depending on if they are from the same or a different scope' do
- Page::Project::Issue::Show.perform do |issue_page|
- issue_page.select_labels_and_refresh [@new_label_same_scope, @new_label_different_scope]
-
- expect(page).to have_content("added #{@initial_label}")
- expect(page).to have_content("added #{@new_label_same_scope} #{@new_label_different_scope} labels and removed #{@initial_label}")
- expect(issue_page.text_of_labels_block).to have_content(@new_label_same_scope)
- expect(issue_page.text_of_labels_block).to have_content(@new_label_different_scope)
- expect(issue_page.text_of_labels_block).not_to have_content(@initial_label)
- end
- end
- end
- end
-end
-```
-
-Notice that `select_labels_and_refresh` is now a method from the issue Page Object (which is not yet implemented), and that we verify the labels' text by using `text_of_labels_block`, instead of via the `labels_block` element. The `text_of_labels_block` method will also be implemented in the issue Page Object.
-
-Let's now update the Issue Page Object.
-
-#### Updates in the Issue Page Object
-
-> Page Objects are located in the `qa/qa/page/` directory, and its sub-directories.
-
-The file we will have to change is the [Issue Page Object](https://gitlab.com/gitlab-org/gitlab/blob/master/qa/qa/page/project/issue/show.rb).
-
-First, add the following code right below the definition of an already implemented view (keep in mind that view's definitions and their elements should be alphabetically ordered):
-
-```ruby
-view 'app/helpers/dropdowns_helper.rb' do
- element :dropdown_input_field
-end
-
-view 'app/views/shared/issuable/_sidebar.html.haml' do
- element :dropdown_menu_labels
- element :edit_link_labels
- element :labels_block
-end
-```
-
-Similarly to what we did before, let's first change the Page Object even without the elements being defined in the view (`_sidebar.html.haml`) and the `dropdowns_helper.rb` files, and later we will update them by adding the appropriate CSS selectors.
-
-Now, let's implement the methods `select_labels_and_refresh` and `text_of_labels_block`.
-
-Somewhere between the definition of the views and the private methods, add the following snippet of code (these should also be alphabetically ordered for organization reasons):
-
-```ruby
-def select_labels_and_refresh(labels)
- click_element(:edit_link_labels)
- labels.each do |label|
- within_element(:dropdown_menu_labels, text: label) do
- send_keys_to_element(:dropdown_input_field, [label, :enter])
- end
- end
- click_body
- labels.each do |label|
- has_element?(:labels_block, text: label)
- end
- refresh
-end
-
-def text_of_labels_block
- find_element(:labels_block)
-end
-```
-
-##### Details of `select_labels_and_refresh`
-
-Notice that we have not only moved the `select_labels_and_refresh` method, but we have also changed its implementation to:
-
-1. Click the `:edit_link_labels` element previously defined, instead of using `find('.block.labels .edit-link').click`
-1. Use `within_element(:dropdown_menu_labels, text: label)`, and inside of it, we call `send_keys_to_element(:dropdown_input_field, [label, :enter])`, which is a method that we will implement in the `QA::Page::Base` class to replace `find('.dropdown-menu-labels .dropdown-input-field').send_keys [label, :enter]`
-1. Use `click_body` after iterating on each label, instead of using `find('#content-body').click`
-1. Iterate on every label again, and then we use `has_element?(:labels_block, text: label)` after clicking the page body (which applies the labels), and before refreshing the page, to avoid test flakiness due to refreshing too fast.
-
-##### Details of `text_of_labels_block`
-
-The `text_of_labels_block` method is a simple method that returns the `:labels_block` element (`find_element(:labels_block)`).
-
-#### Updates in the view (*.html.haml) and `dropdowns_helper.rb` files
-
-Now let's change the view and the `dropdowns_helper` files to add the selectors that relate to the [Page Objects](page_objects.md).
-
-In [`app/views/shared/issuable/_sidebar.html.haml:105`](https://gitlab.com/gitlab-org/gitlab/blob/7ca12defc7a965987b162a6ebef302f95dc8867f/app/views/shared/issuable/_sidebar.html.haml#L105), add a `data: { qa_selector: 'edit_link_labels' }` data attribute.
-
-The code should look like this:
-
-```haml
-= link_to _('Edit'), '#', class: 'js-sidebar-dropdown-toggle edit-link float-right', data: { qa_selector: 'edit_link_labels' }
-```
-
-In the same file, on [line 121](https://gitlab.com/gitlab-org/gitlab/blob/7ca12defc7a965987b162a6ebef302f95dc8867f/app/views/shared/issuable/_sidebar.html.haml#L121), add a `data: { qa_selector: 'dropdown_menu_labels' }` data attribute.
-
-The code should look like this:
-
-```haml
-.dropdown-menu.dropdown-select.dropdown-menu-paging.dropdown-menu-labels.dropdown-menu-selectable.dropdown-extended-height{ data: { qa_selector: 'dropdown_menu_labels' } }
-```
-
-In [`app/helpers/dropdowns_helper.rb:94`](https://gitlab.com/gitlab-org/gitlab/blob/7ca12defc7a965987b162a6ebef302f95dc8867f/app/helpers/dropdowns_helper.rb#L94), add a `data: { qa_selector: 'dropdown_input_field' }` data attribute.
-
-The code should look like this:
-
-```ruby
-filter_output = search_field_tag search_id, nil, class: "dropdown-input-field", placeholder: placeholder, autocomplete: 'off', data: { qa_selector: 'dropdown_input_field' }
-```
-
-> `data-qa-*` data attributes and CSS classes starting with `qa-` are used solely for the purpose of QA and testing.
-> By defining these, we add **testability** to the application.
->
-> When defining a data attribute like: `qa_selector: 'labels_block'`, it should match the element definition: `element :labels_block`. We use a [sanity test](https://gitlab.com/gitlab-org/gitlab-foss/tree/master/doc/development/testing_guide/end_to_end/page_objects.md#how-did-we-solve-fragile-tests-problem) to check that defined elements have their respective selectors in the specified views.
-
-#### Updates in the `QA::Page::Base` class
-
-The last thing that we have to do is to update `QA::Page::Base` class to add the `send_keys_to_element` method on it.
-
-Add the following snippet of code somewhere where class methods are defined (remember to organize methods alphabetically, and if you see a place where this standard is not being followed, it would be helpful if you could rearrange it):
-
-```ruby
-def send_keys_to_element(name, keys)
- find_element(name).send_keys(keys)
-end
-```
-
-This method receives an element (`name`) and the `keys` that it will send to that element, and the keys are an array that can receive strings, or "special" keys, like `:enter`.
-
-As you might remember, in the Issue Page Object we call this method like this: `send_keys_to_element(:dropdown_input_field, [label, :enter])`.
-
-With that, you should be able to start writing end-to-end tests yourself. *Congratulations!*
diff --git a/doc/development/testing_guide/end_to_end/rspec_metadata_tests.md b/doc/development/testing_guide/end_to_end/rspec_metadata_tests.md
index f8dc3366904..b7c93d205a3 100644
--- a/doc/development/testing_guide/end_to_end/rspec_metadata_tests.md
+++ b/doc/development/testing_guide/end_to_end/rspec_metadata_tests.md
@@ -14,3 +14,5 @@ This is a partial list of the [RSpec metadata](https://relishapp.com/rspec/rspec
| `:reliable` | The test has been [promoted to a reliable test](https://about.gitlab.com/handbook/engineering/quality/guidelines/reliable-tests/#promoting-an-existing-test-to-reliable) meaning it passes consistently in all pipelines, including merge requests. |
| `:requires_admin` | The test requires an admin account. Tests with the tag are excluded when run against Canary and Production environments. |
| `:runner` | The test depends on and will set up a GitLab Runner instance, typically to run a pipeline. |
+| `:gitaly_ha` | The test will run against a GitLab instance where repositories are stored on redundant Gitaly nodes behind a Praefect node. All nodes are [separate containers](../../../administration/gitaly/praefect.md#requirements-for-configuring-a-gitaly-cluster). Tests that use this tag have a longer setup time since there are three additional containers that need to be started. |
+| `:skip_live_env` | The test will be excluded when run against live deployed environments such as Staging, Canary, and Production. |
diff --git a/doc/development/testing_guide/end_to_end/style_guide.md b/doc/development/testing_guide/end_to_end/style_guide.md
index 7f4616f394b..9c02af12d5d 100644
--- a/doc/development/testing_guide/end_to_end/style_guide.md
+++ b/doc/development/testing_guide/end_to_end/style_guide.md
@@ -66,6 +66,7 @@ We follow a simple formula roughly based on hungarian notation.
- `_placeholder`: a temporary element that appears while content is loading. For example, the elements that are displayed instead of discussions while the discussions are being fetched.
- `_radio`
- `_tab`
+ - `_menu_item`
*Note: If none of the listed types are suitable, please open a merge request to add an appropriate type to the list.*
diff --git a/doc/development/testing_guide/flaky_tests.md b/doc/development/testing_guide/flaky_tests.md
index 1e53e92fad5..6c1b06ce59a 100644
--- a/doc/development/testing_guide/flaky_tests.md
+++ b/doc/development/testing_guide/flaky_tests.md
@@ -40,7 +40,7 @@ Quarantined tests are run on the CI in dedicated jobs that are allowed to fail:
## Automatic retries and flaky tests detection
-On our CI, we use [rspec-retry](https://github.com/NoRedInk/rspec-retry) to automatically retry a failing example a few
+On our CI, we use [RSpec::Retry](https://github.com/NoRedInk/rspec-retry) to automatically retry a failing example a few
times (see [`spec/spec_helper.rb`](https://gitlab.com/gitlab-org/gitlab/blob/master/spec/spec_helper.rb) for the precise retries count).
We also use a home-made `RspecFlaky::Listener` listener which records flaky
diff --git a/doc/development/testing_guide/frontend_testing.md b/doc/development/testing_guide/frontend_testing.md
index 2a3fcf122a6..52d538c7159 100644
--- a/doc/development/testing_guide/frontend_testing.md
+++ b/doc/development/testing_guide/frontend_testing.md
@@ -5,7 +5,7 @@ at GitLab. We use Karma with Jasmine and Jest for JavaScript unit and integratio
and RSpec feature tests with Capybara for e2e (end-to-end) integration testing.
Unit and feature tests need to be written for all new features.
-Most of the time, you should use [RSpec] for your feature tests.
+Most of the time, you should use [RSpec](https://github.com/rspec/rspec-rails#feature-specs) for your feature tests.
Regression tests should be written for bug fixes to prevent them from recurring
in the future.
@@ -15,7 +15,7 @@ information on general testing practices at GitLab.
## Vue.js testing
-If you are looking for a guide on Vue component testing, you can jump right away to this [section][vue-test].
+If you are looking for a guide on Vue component testing, you can jump right away to this [section](../fe_guide/vue.md#testing-vue-components).
## Jest
@@ -30,7 +30,7 @@ Jest tests can be found in `/spec/frontend` and `/ee/spec/frontend` in EE.
## Karma test suite
-While GitLab is switching over to [Jest][jest] you'll still find Karma tests in our application. [Karma][karma] is a test runner which uses [Jasmine] as its test
+While GitLab is switching over to [Jest](https://jestjs.io) you'll still find Karma tests in our application. [Karma](http://karma-runner.github.io/) is a test runner which uses [Jasmine](https://jasmine.github.io/) as its test
framework. Jest also uses Jasmine as foundation, that's why it's looking quite similar.
Karma tests live in `spec/javascripts/` and `/ee/spec/javascripts` in EE.
@@ -549,7 +549,8 @@ TBU
Jasmine provides stubbing and mocking capabilities. There are some subtle differences in how to use it within Karma and Jest.
-Stubs or spies are often used synonymously. In Jest it's quite easy thanks to the `.spyOn` method. [Official docs][jestspy]
+Stubs or spies are often used synonymously. In Jest it's quite easy thanks to the `.spyOn` method.
+[Official docs](https://jestjs.io/docs/en/jest-object#jestspyonobject-methodname)
The more challenging part are mocks, which can be used for functions or even dependencies.
### Manual module mocks
@@ -637,12 +638,12 @@ Karma allows something similar, but it's way more costly.
Running Karma with `yarn run karma-start` will compile the JavaScript
assets and run a server at `http://localhost:9876/` where it will automatically
-run the tests on any browser which connects to it. You can enter that url on
+run the tests on any browser which connects to it. You can enter that URL on
multiple browsers at once to have it run the tests on each in parallel.
While Karma is running, any changes you make will instantly trigger a recompile
and retest of the **entire test suite**, so you can see instantly if you've broken
-a test with your changes. You can use [Jasmine focused][jasmine-focus] or
+a test with your changes. You can use [Jasmine focused](https://jasmine.github.io/2.5/focused_specs.html) or
excluded tests (with `fdescribe` or `xdescribe`) to get Karma to run only the
tests you want while you're working on a specific feature, but make sure to
remove these directives when you commit your code.
@@ -831,43 +832,6 @@ testAction(
Check an example in [spec/javascripts/ide/stores/actions_spec.jsspec/javascripts/ide/stores/actions_spec.js](https://gitlab.com/gitlab-org/gitlab/blob/master/spec/javascripts/ide/stores/actions_spec.js).
-### Vue Helper: `mountComponent`
-
-To make mounting a Vue component easier and more readable, we have a few helpers available in `spec/helpers/vue_mount_component_helper`:
-
-- `createComponentWithStore`
-- `mountComponentWithStore`
-
-Examples of usage:
-
-```javascript
-beforeEach(() => {
- vm = createComponentWithStore(Component, store);
-
- vm.$store.state.currentBranchId = 'master';
-
- vm.$mount();
-});
-```
-
-```javascript
-beforeEach(() => {
- vm = mountComponentWithStore(Component, {
- el: '#dummy-element',
- store,
- props: { badge },
- });
-});
-```
-
-Don't forget to clean up:
-
-```javascript
-afterEach(() => {
- vm.$destroy();
-});
-```
-
### Wait until axios requests finish
The axios utils mock module located in `spec/frontend/mocks/ce/lib/utils/axios_utils.js` contains two helper methods for Jest tests that spawn HTTP requests.
@@ -906,13 +870,3 @@ You can download any older version of Firefox from the releases FTP server, <htt
---
[Return to Testing documentation](index.md)
-
-<!-- URL References -->
-
-[jasmine-focus]: https://jasmine.github.io/2.5/focused_specs.html
-[karma]: http://karma-runner.github.io/
-[vue-test]: ../fe_guide/vue.md#testing-vue-components
-[rspec]: https://github.com/rspec/rspec-rails#feature-specs
-[jasmine]: https://jasmine.github.io/
-[jest]: https://jestjs.io
-[jestspy]: https://jestjs.io/docs/en/jest-object#jestspyonobject-methodname
diff --git a/doc/development/testing_guide/index.md b/doc/development/testing_guide/index.md
index 25da72b6b6a..f0fb06910f8 100644
--- a/doc/development/testing_guide/index.md
+++ b/doc/development/testing_guide/index.md
@@ -4,15 +4,15 @@ This document describes various guidelines and best practices for automated
testing of the GitLab project.
It is meant to be an _extension_ of the [thoughtbot testing
-styleguide](https://github.com/thoughtbot/guides/tree/master/style/testing). If
+style guide](https://github.com/thoughtbot/guides/tree/master/style/testing). If
this guide defines a rule that contradicts the thoughtbot guide, this guide
takes precedence. Some guidelines may be repeated verbatim to stress their
importance.
## Overview
-GitLab is built on top of [Ruby on Rails](https://rubyonrails.org/), and we're using [RSpec] for all
-the backend tests, with [Capybara] for end-to-end integration testing.
+GitLab is built on top of [Ruby on Rails](https://rubyonrails.org/), and we're using [RSpec](https://github.com/rspec/rspec-rails#feature-specs) for all
+the backend tests, with [Capybara](https://github.com/teamcapybara/capybara) for end-to-end integration testing.
On the frontend side, we're using [Jest](https://jestjs.io/) and [Karma](http://karma-runner.github.io/)/[Jasmine](https://jasmine.github.io/) for JavaScript unit and
integration testing.
@@ -58,14 +58,10 @@ Everything you should know about how to test Rake tasks.
## [End-to-end tests](end_to_end/index.md)
Everything you should know about how to run end-to-end tests using
-[GitLab QA][gitlab-qa] testing framework.
+[GitLab QA](ttps://gitlab.com/gitlab-org/gitlab-qa) testing framework.
## [Migrations tests](testing_migrations_guide.md)
Everything you should know about how to test migrations.
[Return to Development documentation](../README.md)
-
-[RSpec]: https://github.com/rspec/rspec-rails#feature-specs
-[Capybara]: https://github.com/teamcapybara/capybara
-[gitlab-qa]: https://gitlab.com/gitlab-org/gitlab-qa
diff --git a/doc/development/testing_guide/review_apps.md b/doc/development/testing_guide/review_apps.md
index 9eb5d5add8a..58acf937d77 100644
--- a/doc/development/testing_guide/review_apps.md
+++ b/doc/development/testing_guide/review_apps.md
@@ -102,10 +102,10 @@ subgraph "CNG-mirror pipeline"
### Auto-stopping of Review Apps
Review Apps are automatically stopped 2 days after the last deployment thanks to
-the [Environment auto-stop](../../ci/environments.md#environments-auto-stop) feature.
+the [Environment auto-stop](../../ci/environments/index.md#environments-auto-stop) feature.
If you need your Review App to stay up for a longer time, you can
-[pin its environment](../../ci/environments.md#auto-stop-example) or retry the
+[pin its environment](../../ci/environments/index.md#auto-stop-example) or retry the
`review-deploy` job to update the "latest deployed at" time.
The `review-cleanup` job that automatically runs in scheduled
@@ -153,7 +153,13 @@ used by the `review-deploy` and `review-stop` jobs.
### Get access to the GCP Review Apps cluster
You need to [open an access request (internal link)](https://gitlab.com/gitlab-com/access-requests/issues/new)
-for the `gcp-review-apps-sg` GCP group.
+for the `gcp-review-apps-sg` GCP group. In order to join a group, you must specify the desired GCP role in your access request.
+The role is what will grant you specific permissions in order to engage with Review App containers.
+
+Here are some permissions you may want to have, and the roles that grant them:
+
+- `container.pods.getLogs` - Required to [retrieve pod logs](#dig-into-a-pods-logs). Granted by [Viewer (`roles/viewer`)](https://cloud.google.com/iam/docs/understanding-roles#kubernetes-engine-roles).
+- `container.pods.exec` - Required to [run a Rails console](#run-a-rails-console). Granted by [Kubernetes Engine Developer (`roles/container.developer`)](https://cloud.google.com/iam/docs/understanding-roles#kubernetes-engine-roles).
### Log into my Review App
@@ -175,7 +181,7 @@ secure note named `gitlab-{ce,ee} Review App's root password`.
### Run a Rails console
-1. Make sure you [have access to the cluster](#get-access-to-the-gcp-review-apps-cluster) first.
+1. Make sure you [have access to the cluster](#get-access-to-the-gcp-review-apps-cluster) and the `container.pods.exec` permission first.
1. [Filter Workloads by your Review App slug](https://console.cloud.google.com/kubernetes/workload?project=gitlab-review-apps),
e.g. `review-qa-raise-e-12chm0`.
1. Find and open the `task-runner` Deployment, e.g. `review-qa-raise-e-12chm0-task-runner`.
@@ -191,7 +197,7 @@ secure note named `gitlab-{ce,ee} Review App's root password`.
### Dig into a Pod's logs
-1. Make sure you [have access to the cluster](#get-access-to-the-gcp-review-apps-cluster) first.
+1. Make sure you [have access to the cluster](#get-access-to-the-gcp-review-apps-cluster) and the `container.pods.getLogs` permission first.
1. [Filter Workloads by your Review App slug](https://console.cloud.google.com/kubernetes/workload?project=gitlab-review-apps),
e.g. `review-qa-raise-e-12chm0`.
1. Find and open the `migrations` Deployment, e.g.
@@ -209,6 +215,31 @@ Leading indicators may be health check failures leading to restarts or majority
The [Review Apps Overview dashboard](https://app.google.stackdriver.com/dashboards/6798952013815386466?project=gitlab-review-apps&timeDomain=1d)
aids in identifying load spikes on the cluster, and if nodes are problematic or the entire cluster is trending towards unhealthy.
+### Release failed with `ImagePullBackOff`
+
+**Potential cause:**
+
+If you see an `ImagePullBackoff` status, check for a missing Docker image.
+
+**Where to look for further debugging:**
+
+To check that the Docker images were created, run the following Docker command:
+
+```shell
+`DOCKER_CLI_EXPERIMENTAL=enabled docker manifest repository:tag`
+```
+
+The output of this command indicates if the Docker image exists. For example:
+
+```shell
+DOCKER_CLI_EXPERIMENTAL=enabled docker manifest inspect registry.gitlab.com/gitlab-org/build/cng-mirror/gitlab-rails-ee:39467-allow-a-release-s-associated-milestones-to-be-edited-thro
+```
+
+If the Docker image does not exist:
+
+- Verify the `image.repository` and `image.tag` options in the `helm upgrade --install` command match the repository names used by CNG-mirror pipeline.
+- Look further in the corresponding downstream CNG-mirror pipeline in `review-build-cng` job.
+
### Node count is always increasing (i.e. never stabilizing or decreasing)
**Potential cause:**
diff --git a/doc/development/testing_guide/testing_levels.md b/doc/development/testing_guide/testing_levels.md
index 295aa6609a8..9285a910ecf 100644
--- a/doc/development/testing_guide/testing_levels.md
+++ b/doc/development/testing_guide/testing_levels.md
@@ -312,9 +312,7 @@ of a controller test. Testing a fat controller usually involves a lot of stubbin
controller.instance_variable_set(:@user, user)
```
-and use methods which are deprecated in Rails 5 ([#23768]).
-
-[#23768]: https://gitlab.com/gitlab-org/gitlab/-/issues/16260
+and use methods which are deprecated in Rails 5 ([#23768](https://gitlab.com/gitlab-org/gitlab/-/issues/16260)).
### About Karma
@@ -356,7 +354,7 @@ possible).
| Tests path | Testing engine | Notes |
| ---------- | -------------- | ----- |
-| `spec/features/` | [Capybara] + [RSpec] | If your test has the `:js` metadata, the browser driver will be [Poltergeist], otherwise it's using [RackTest]. |
+| `spec/features/` | [Capybara](https://github.com/teamcapybara/capybara) + [RSpec](https://github.com/rspec/rspec-rails#feature-specs) | If your test has the `:js` metadata, the browser driver will be [Poltergeist](https://github.com/teamcapybara/capybara#poltergeist), otherwise it's using [RackTest](https://github.com/teamcapybara/capybara#racktest). |
### Frontend feature tests
@@ -460,9 +458,6 @@ The reasons why we should follow these best practices are as follows:
of tests). This is slower than transactions, however, so we want to use
truncation only when necessary.
-[Poltergeist]: https://github.com/teamcapybara/capybara#poltergeist
-[RackTest]: https://github.com/teamcapybara/capybara#racktest
-
## Black-box tests at the system level, aka end-to-end tests
Formal definitions:
@@ -470,11 +465,11 @@ Formal definitions:
- <https://en.wikipedia.org/wiki/System_testing>
- <https://en.wikipedia.org/wiki/Black-box_testing>
-GitLab consists of [multiple pieces] such as [GitLab Shell], [GitLab Workhorse],
-[Gitaly], [GitLab Pages], [GitLab Runner], and GitLab Rails. All theses pieces
-are configured and packaged by [GitLab Omnibus].
+GitLab consists of [multiple pieces](../architecture.md#components) such as [GitLab Shell](https://gitlab.com/gitlab-org/gitlab-shell), [GitLab Workhorse](https://gitlab.com/gitlab-org/gitlab-workhorse),
+[Gitaly](https://gitlab.com/gitlab-org/gitaly), [GitLab Pages](https://gitlab.com/gitlab-org/gitlab-pages), [GitLab Runner](https://gitlab.com/gitlab-org/gitlab-runner), and GitLab Rails. All theses pieces
+are configured and packaged by [Omnibus GitLab](https://gitlab.com/gitlab-org/omnibus-gitlab).
-The QA framework and instance-level scenarios are [part of GitLab Rails] so that
+The QA framework and instance-level scenarios are [part of GitLab Rails](https://gitlab.com/gitlab-org/gitlab-foss/tree/master/qa) so that
they're always in-sync with the codebase (especially the views).
Note that:
@@ -483,11 +478,11 @@ Note that:
- data needed for the tests can only be created using the GUI or the API
- expectations can only be made against the browser page and API responses
-Every new feature should come with a [test plan].
+Every new feature should come with a [test plan](https://gitlab.com/gitlab-org/gitlab/tree/master/.gitlab/issue_templates/Test%20plan.md).
| Tests path | Testing engine | Notes |
| ---------- | -------------- | ----- |
-| `qa/qa/specs/features/` | [Capybara] + [RSpec] + Custom QA framework | Tests should be placed under their corresponding [Product category] |
+| `qa/qa/specs/features/` | [Capybara](https://github.com/teamcapybara/capybara) + [RSpec](https://github.com/rspec/rspec-rails#feature-specs) + Custom QA framework | Tests should be placed under their corresponding [Product category](https://about.gitlab.com/handbook/product/categories/) |
> See [end-to-end tests](end_to_end/index.md) for more information.
@@ -495,17 +490,6 @@ Note that `qa/spec` contains unit tests of the QA framework itself, not to be
confused with the application's [unit tests](#unit-tests) or
[end-to-end tests](#black-box-tests-at-the-system-level-aka-end-to-end-tests).
-[multiple pieces]: ../architecture.md#components
-[GitLab Shell]: https://gitlab.com/gitlab-org/gitlab-shell
-[GitLab Workhorse]: https://gitlab.com/gitlab-org/gitlab-workhorse
-[Gitaly]: https://gitlab.com/gitlab-org/gitaly
-[GitLab Pages]: https://gitlab.com/gitlab-org/gitlab-pages
-[GitLab Runner]: https://gitlab.com/gitlab-org/gitlab-runner
-[GitLab Omnibus]: https://gitlab.com/gitlab-org/omnibus-gitlab
-[part of GitLab Rails]: https://gitlab.com/gitlab-org/gitlab-foss/tree/master/qa
-[test plan]: https://gitlab.com/gitlab-org/gitlab/tree/master/.gitlab/issue_templates/Test%20plan.md
-[Product category]: https://about.gitlab.com/handbook/product/categories/
-
### Smoke tests
Smoke tests are quick tests that may be run at any time (especially after the
@@ -517,14 +501,11 @@ These tests run against the UI and ensure that basic functionality is working.
### GitLab QA orchestrator
-[GitLab QA orchestrator] is a tool that allows to test that all these pieces
+[GitLab QA orchestrator](https://gitlab.com/gitlab-org/gitlab-qa) is a tool that allows to test that all these pieces
integrate well together by building a Docker image for a given version of GitLab
Rails and running end-to-end tests (i.e. using Capybara) against it.
-Learn more in the [GitLab QA orchestrator README][gitlab-qa-readme].
-
-[GitLab QA orchestrator]: https://gitlab.com/gitlab-org/gitlab-qa
-[gitlab-qa-readme]: https://gitlab.com/gitlab-org/gitlab-qa/tree/master/README.md
+Learn more in the [GitLab QA orchestrator README](https://gitlab.com/gitlab-org/gitlab-qa/tree/master/README.md).
## EE-specific tests
@@ -538,7 +519,9 @@ trade-off:
- Unit tests are usually cheap, and you should consider them like the basement
of your house: you need them to be confident that your code is behaving
correctly. However if you run only unit tests without integration / system
- tests, you might [miss] the [big] / [picture] !
+ tests, you might [miss](https://twitter.com/ThePracticalDev/status/850748070698651649) the
+ [big](https://twitter.com/timbray/status/822470746773409794) /
+ [picture](https://twitter.com/withzombies/status/829716565834752000) !
- Integration tests are a bit more expensive, but don't abuse them. A system test
is often better than an integration test that is stubbing a lot of internals.
- System tests are expensive (compared to unit tests), even more if they require
@@ -546,8 +529,8 @@ trade-off:
section.
Another way to see it is to think about the "cost of tests", this is well
-explained [in this article][tests-cost] and the basic idea is that the cost of a
-test includes:
+explained [in this article](https://medium.com/table-xi/high-cost-tests-and-high-value-tests-a86e27a54df#.2ulyh3a4e)
+and the basic idea is that the cost of a test includes:
- The time it takes to write the test
- The time it takes to run the test every time the suite runs
@@ -557,18 +540,11 @@ test includes:
### Frontend-related tests
-There are cases where the behaviour you are testing is not worth the time spent
+There are cases where the behavior you are testing is not worth the time spent
running the full application, for example, if you are testing styling, animation,
edge cases or small actions that don't involve the backend,
you should write an integration test using Jasmine.
-[miss]: https://twitter.com/ThePracticalDev/status/850748070698651649
-[big]: https://twitter.com/timbray/status/822470746773409794
-[picture]: https://twitter.com/withzombies/status/829716565834752000
-[tests-cost]: https://medium.com/table-xi/high-cost-tests-and-high-value-tests-a86e27a54df#.2ulyh3a4e
-[RSpec]: https://github.com/rspec/rspec-rails#feature-specs
-[Capybara]: https://github.com/teamcapybara/capybara
-
---
[Return to Testing documentation](index.md)
diff --git a/doc/development/uploads.md b/doc/development/uploads.md
index fbdb15d82e1..1dd6ab5496a 100644
--- a/doc/development/uploads.md
+++ b/doc/development/uploads.md
@@ -11,7 +11,7 @@ The following graph explains machine boundaries in a scalable GitLab installatio
```mermaid
graph TB
subgraph "load balancers"
- LB(HA Proxy)
+ LB(Proxy)
end
subgraph "Shared storage"
diff --git a/doc/development/value_stream_analytics.md b/doc/development/value_stream_analytics.md
index 3ceb523ab73..69b07eb7c86 100644
--- a/doc/development/value_stream_analytics.md
+++ b/doc/development/value_stream_analytics.md
@@ -185,7 +185,7 @@ Currently supported parents:
The [original implementation](https://gitlab.com/gitlab-org/gitlab/issues/847) of value stream analytics defined 7 stages. These stages are always available for each parent, however altering these stages is not possible.
​
-To make things efficient and reduce the number of records created, the default stages are expressed as in-memory objects (not persisted). When the user creates a custom stage for the first time, all the stages will be persisted. This behaviour is implemented in the value stream analytics service objects.
+To make things efficient and reduce the number of records created, the default stages are expressed as in-memory objects (not persisted). When the user creates a custom stage for the first time, all the stages will be persisted. This behavior is implemented in the value stream analytics service objects.
​
The reason for this was that we'd like to add the abilities to hide and order stages later on.
diff --git a/doc/development/verifying_database_capabilities.md b/doc/development/verifying_database_capabilities.md
index 1413c782c5d..f6c78e51299 100644
--- a/doc/development/verifying_database_capabilities.md
+++ b/doc/development/verifying_database_capabilities.md
@@ -2,7 +2,7 @@
Sometimes certain bits of code may only work on a certain database
version. While we try to avoid such code as much as possible sometimes it is
-necessary to add database (version) specific behaviour.
+necessary to add database (version) specific behavior.
To facilitate this we have the following methods that you can use:
@@ -12,7 +12,7 @@ To facilitate this we have the following methods that you can use:
This allows you to write code such as:
```ruby
-if Gitlab::Database.version.to_f >= 9.6
+if Gitlab::Database.version.to_f >= 11.7
run_really_fast_query
else
run_fast_query
diff --git a/doc/development/what_requires_downtime.md b/doc/development/what_requires_downtime.md
index 9ece6eff41e..8ea9f70fc7a 100644
--- a/doc/development/what_requires_downtime.md
+++ b/doc/development/what_requires_downtime.md
@@ -5,38 +5,6 @@ GitLab offline, others do require a downtime period. This guide describes
various operations, their impact, and how to perform them without requiring
downtime.
-## Adding Columns
-
-You can safely add a new column to an existing table as long as it does **not**
-have a default value. For example, this query would not require downtime:
-
-```sql
-ALTER TABLE projects ADD COLUMN random_value int;
-```
-
-Add a column _with_ a default however does require downtime. For example,
-consider this query:
-
-```sql
-ALTER TABLE projects ADD COLUMN random_value int DEFAULT 42;
-```
-
-This requires updating every single row in the `projects` table so that
-`random_value` is set to `42` by default. This requires updating all rows and
-indexes in a table. This in turn acquires enough locks on the table for it to
-effectively block any other queries.
-
-Adding a column with a default value _can_ be done without requiring downtime
-when using the migration helper method
-`Gitlab::Database::MigrationHelpers#add_column_with_default`. This method works
-similar to `add_column` except it updates existing rows in batches without
-blocking access to the table being modified. See ["Adding Columns With Default
-Values"](migration_style_guide.md#adding-columns-with-default-values) for more
-information on how to use this method.
-
-Note that usage of `add_column_with_default` with `allow_null: false` to also add
-a `NOT NULL` constraint is [discouraged](https://gitlab.com/gitlab-org/gitlab/issues/38060).
-
## Dropping Columns
Removing columns is tricky because running GitLab processes may still be using
@@ -171,8 +139,39 @@ Adding or removing a NOT NULL clause (or another constraint) can typically be
done without requiring downtime. However, this does require that any application
changes are deployed _first_. Thus, changing the constraints of a column should
happen in a post-deployment migration.
-NOTE: Avoid using `change_column` as it produces inefficient query because it re-defines
-the whole column type. For example, to add a NOT NULL constraint, prefer `change_column_null`
+
+NOTE: Avoid using `change_column` as it produces an inefficient query because it re-defines
+the whole column type.
+
+To add a NOT NULL constraint, use the `add_not_null_constraint` migration helper:
+
+```ruby
+# A post-deployment migration in db/post_migrate
+class AddNotNull < ActiveRecord::Migration[4.2]
+ include Gitlab::Database::MigrationHelpers
+
+ disable_ddl_transaction!
+
+ def up
+ add_not_null_constraint :users, :username
+ end
+
+ def down
+ remove_not_null_constraint :users, :username
+ end
+end
+```
+
+If the column to be updated requires cleaning first (e.g. there are `NULL` values), you should:
+
+1. Add the `NOT NULL` constraint with `validate: false`
+
+ `add_not_null_constraint :users, :username, validate: false`
+
+1. Clean up the data with a data migration
+1. Validate the `NOT NULL` constraint with a followup migration
+
+ `validate_not_null_constraint :users, :username`
## Changing Column Types
diff --git a/doc/development/windows.md b/doc/development/windows.md
new file mode 100644
index 00000000000..b5309002222
--- /dev/null
+++ b/doc/development/windows.md
@@ -0,0 +1,139 @@
+---
+type: reference, howto
+---
+
+# Windows Development
+
+There are times in development where a Windows development machine is needed.
+This is a guide for how to get a Windows development virtual machine on Google Cloud Platform
+(GCP) with the same preinstalled tools as the GitLab shared Windows runners.
+
+## Why Windows in Google Cloud?
+
+Use of Microsoft Windows operating systems on company laptops is banned under GitLab's [Approved Operating Systems policy](https://about.gitlab.com/handbook/security/approved_os.html#windows).
+
+This can make it difficult to develop features for the Windows platforms. Using GCP will allow us to have a temporary Windows machine that can be removed once we're done with it.
+
+## Shared Windows runners
+
+You can use the shared Windows runners in the case that you don't need a full Windows development machine.
+The [GitLab 12.7 Release Post](https://about.gitlab.com/releases/2020/01/22/gitlab-12-7-released/#windows-shared-runners-on-gitlabcom-beta)
+and [Windows shared runner beta blog post](https://about.gitlab.com/blog/2020/01/21/windows-shared-runner-beta/#getting-started) both
+outline quite a bit of useful information.
+
+To use the shared Windows runners add the following `tags` to relevant jobs in your `.gitlab-ci.yml` file:
+
+```yaml
+tags:
+ - shared-windows
+ - windows
+ - windows-1809
+```
+
+A list of software preinstalled on the Windows images is available at: [Preinstalled software](https://gitlab.com/gitlab-org/ci-cd/shared-runners/images/gcp/windows-containers/blob/master/cookbooks/preinstalled-software/README.md).
+
+## GCP Windows image for development
+
+The [shared Windows GitLab
+runners](https://about.gitlab.com/releases/2020/01/22/gitlab-12-7-released/#windows-shared-runners-on-gitlabcom-beta)
+are built with [Packer](https://www.packer.io/).
+
+The Infrastructure as Code repository for building the Google Cloud images is available at:
+[GitLab Google Cloud Platform Shared Runner Images](https://gitlab.com/gitlab-org/ci-cd/shared-runners/images/gcp/windows-containers).
+
+### Build image
+
+There is a chance that your Google Cloud group may already have an image
+built. Search the available images before you do the work to build your
+own.
+
+Build a Google Cloud image with the above shared runners repo by doing the following:
+
+1. Install [Packer](https://www.packer.io/) (tested to work with version 1.5.1).
+1. Install Packer Windows Update Provisioner.
+ 1. Clone the repository <https://github.com/rgl/packer-provisioner-windows-update> and `cd` into the cloned directory.
+ 1. Run the command `go build -o packer-provisioner-windows-update` (requires `go` to be installed).
+ 1. Verify `packer-provisioner-windows-update` is in the `PATH` environment variable.
+1. Add all [required environment variables](https://gitlab.com/gitlab-org/ci-cd/shared-runners/images/gcp/windows-containers/-/blob/master/packer.json#L2-10)
+ in the `packer.json` file to your environment (perhaps use [direnv](https://direnv.net/)).
+1. Build the image by running the command: `packer build packer.json`.
+
+## How to use a Windows image in GCP
+
+1. In a web browser, go to <https://console.cloud.google.com/compute/images>.
+1. Filter images by the name you used when creating image, `windows` is likely all you need to filter by.
+1. Click the image's name.
+1. Click the **CREATE INSTANCE** link.
+1. Important: Change name to what you'd like as you can't change it later.
+1. Optional: Change Region to be closest to you as well as any other option you'd like.
+1. Click **Create** at the bottom of the page.
+1. Click the name of your newly created VM Instance (optionally you can filter to find it).
+1. Click **Set Windows password**.
+1. Optional: Set a username or use default.
+1. Click **Next**.
+1. Copy and save the password as it won't be shown again.
+1. Click **RDP** down arrow.
+1. Click **Download the RDP file**.
+1. Open the downloaded RDP file with the Windows remote desktop app (<https://docs.microsoft.com/en-us/windows-server/remote/remote-desktop-services/clients/remote-desktop-clients>).
+1. Click **Continue** to accept the certificate.
+1. Enter the password and click **Next**.
+
+You should now be remoted into a Windows machine with a command prompt.
+
+### Optional: Use GCP VM Instance as a runner
+
+- Register the runner with a project: `gitlab-runner.exe register`.
+- Install the runner:`gitlab-runner.exe install`.
+- Start the runner: `gitlab-runner.exe start`.
+
+For more information, see [Install GitLab Runner on Windows](https://docs.gitlab.com/runner/install/windows.html)
+and [Registering Runners](https://docs.gitlab.com/runner/register/index.html).
+
+## Developer tips
+
+Here are a few tips on GCP and Windows.
+
+### GCP cost savings
+
+To minimise the cost of your GCP VM instance, stop it when you're not using it.
+If you do, you'll need to redownload the RDP file from the console as the IP
+address changes every time you stop and start it.
+
+### chocolatey
+
+Chocolatey is a package manager for Windows. You can search for packages on <https://chocolatey.org/>.
+
+- `choco install vim`
+
+### Visual Studio (install / usage for full GUI)
+
+You can install Visual Studio and run it within the Windows Remote Desktop app.
+
+Install it by running: `choco install visualstudio2019community`
+
+Start it by running: `"C:\Program Files (x86)\Microsoft Visual Studio\2019\Community\Common7\IDE\devenv.exe" .`
+
+### .NET 3 support
+
+You can install .NET version 3 support with the following `DISM` command:
+
+`DISM /Online /Enable-Feature /FeatureName:NetFx3 /All`
+
+### nix -> Windows cmd tips
+
+The first tip for using the Windows command shell is to open Powershell and use that instead.
+
+Start Powershell: `start powershell`.
+
+Powershell has aliases for all of the following commands so you don't have to learn the native commands:
+
+- `ls` ---> `dir`
+- `rm` ---> `del`
+- `rm -rf nonemptydir` ---> `rmdir /S nonemptydir`
+- `/` ---> `\` (path separator)
+- `cat` ---> `type`
+- `mv` ---> `move`
+- Redirection works the same (i.e. `>` and `2>&1`)
+- `.\some.exe` to call a local executable
+- curl is available
+- `..` and `.` are available
diff --git a/doc/downgrade_ee_to_ce/README.md b/doc/downgrade_ee_to_ce/README.md
index 29f9baae5c4..cba21668816 100644
--- a/doc/downgrade_ee_to_ce/README.md
+++ b/doc/downgrade_ee_to_ce/README.md
@@ -48,15 +48,18 @@ to avoid getting this error, you need to remove all instances of the
**Omnibus Installation**
```shell
-sudo gitlab-rails runner "Service.where(type: ['JenkinsService', 'JenkinsDeprecatedService', 'GithubService']).delete_all"
+sudo gitlab-rails runner "Service.where(type: ['JenkinsService', 'GithubService']).delete_all"
```
**Source Installation**
```shell
-bundle exec rails runner "Service.where(type: ['JenkinsService', 'JenkinsDeprecatedService', 'GithubService']).delete_all" production
+bundle exec rails runner "Service.where(type: ['JenkinsService', 'GithubService']).delete_all" production
```
+NOTE: **Note:**
+If you are running `GitLab =< v13.0` you need to also remove `JenkinsDeprecatedService` records.
+
### Variables environment scopes
If you're using this feature and there are variables sharing the same
diff --git a/doc/gitlab-basics/command-line-commands.md b/doc/gitlab-basics/command-line-commands.md
index fed91046c32..b9426ec0849 100644
--- a/doc/gitlab-basics/command-line-commands.md
+++ b/doc/gitlab-basics/command-line-commands.md
@@ -2,7 +2,7 @@
type: howto, reference
---
-# Command Line basic commands
+# Edit files through the command line
When [working with Git from the command line](start-using-git.md), you will need to
use more than just the Git commands. There are several basic commands that you should
diff --git a/doc/gitlab-basics/create-project.md b/doc/gitlab-basics/create-project.md
index 1febe8337bc..152fb24699e 100644
--- a/doc/gitlab-basics/create-project.md
+++ b/doc/gitlab-basics/create-project.md
@@ -145,7 +145,7 @@ git push --set-upstream https://gitlab.example.com/namespace/nonexistent-project
Once the push finishes successfully, a remote message will indicate
the command to set the remote and the URL to the new project:
-```text
+```plaintext
remote:
remote: The private project namespace/nonexistent-project was created.
remote:
diff --git a/doc/gitlab-basics/create-your-ssh-keys.md b/doc/gitlab-basics/create-your-ssh-keys.md
index 9b3431a5a42..248f39b842c 100644
--- a/doc/gitlab-basics/create-your-ssh-keys.md
+++ b/doc/gitlab-basics/create-your-ssh-keys.md
@@ -11,7 +11,7 @@ In order to use SSH, you will need to:
## Creating your SSH key pair
-1. Go to your [command line](start-using-git.md#open-a-shell).
+1. Go to your [command line](start-using-git.md#command-shell).
1. Follow the [instructions](../ssh/README.md#generating-a-new-ssh-key-pair) to generate
your SSH key pair.
diff --git a/doc/gitlab-basics/start-using-git.md b/doc/gitlab-basics/start-using-git.md
index 9a05c7fff14..9ebcf258ee9 100644
--- a/doc/gitlab-basics/start-using-git.md
+++ b/doc/gitlab-basics/start-using-git.md
@@ -1,81 +1,104 @@
---
type: howto, tutorial
+description: "Introduction to using Git through the command line."
+last_updated: 2020-04-22
---
# Start using Git on the command line
-While GitLab has a powerful user interface, if you want to use Git itself, you will
-have to do so from the command line. If you want to start using Git and GitLab together,
-make sure that you have created and/or signed into an account on GitLab.
+[Git](https://git-scm.com/) is an open-source distributed version control system designed to
+handle everything from small to very large projects with speed and efficiency. GitLab is built
+on top of Git.
-## Open a shell
+While GitLab has a powerful user interface from which you can do a great amount of Git operations
+directly in the browser, you’ll eventually need to use Git through the command line for advanced
+tasks.
-Depending on your operating system, you will need to use a shell of your preference.
-Here are some suggestions:
+For example, if you need to fix complex merge conflicts, rebase branches,
+merge manually, or undo and roll back commits, you'll need to use Git from
+the command line and then push your changes to the remote server.
-- [Terminal](https://blog.teamtreehouse.com/introduction-to-the-mac-os-x-command-line) on macOS
-- [GitBash](https://msysgit.github.io) on Windows
-- [Linux Terminal](https://www.howtogeek.com/140679/beginner-geek-how-to-start-using-the-linux-terminal/) on Linux
+This guide will help you get started with Git through the command line and can be your reference
+for Git commands in the future. If you're only looking for a quick reference of Git commands, you
+can download GitLab's [Git Cheat Sheet](https://about.gitlab.com/images/press/git-cheat-sheet.pdf).
-## Check if Git has already been installed
+TIP: **Tip:**
+To help you visualize what you're doing locally, there are
+[Git GUI apps](https://git-scm.com/download/gui/) you can install.
-Git is usually preinstalled on Mac and Linux, so run the following command:
+## Requirements
-```shell
-git --version
-```
+You don't need a GitLab account to use Git locally, but for the purpose of this guide we
+recommend registering and signing into your account before starting. Some commands need a
+connection between the files in your computer and their version on a remote server.
-You should receive a message that tells you which Git version you have on your computer.
-If you don’t receive a "Git version" message, it means that you need to
-[download Git](https://git-scm.com/book/en/v2/Getting-Started-Installing-Git).
+You'll also need to open a [command shell](#command-shell) and have
+[Git installed](#install-git) in your computer.
-After you are finished installing Git, open a new shell and type `git --version` again
-to verify that it was correctly installed.
+### Command shell
-## Add your Git username and set your email
+To execute Git commands in your computer, you'll need to open a command shell (also known as command
+prompt, terminal, and command line) of your preference. Here are some suggestions:
-It is important to configure your Git username and email address, since every Git
-commit will use this information to identify you as the author.
+- For macOS users:
+ - Built-in: [Terminal](https://blog.teamtreehouse.com/introduction-to-the-mac-os-x-command-line). Press <kbd>⌘ command</kbd> + <kbd>space</kbd> and type "terminal" to find it.
+ - [iTerm2](https://www.iterm2.com/), which you can integrate with [zsh](https://git-scm.com/book/id/v2/Appendix-A%3A-Git-in-Other-Environments-Git-in-Zsh) and [oh my zsh](https://ohmyz.sh/) for color highlighting, among other handy features for Git users.
+- For Windows users:
+ - Built-in: **cmd**. Click the search icon on the bottom navbar on Windows and type "cmd" to find it.
+ - [PowerShell](https://docs.microsoft.com/en-us/powershell/scripting/install/installing-windows-powershell?view=powershell-7): a Windows "powered up" shell, from which you can execute a greater number of commands.
+ - Git Bash: it comes built into [Git for Windows](https://gitforwindows.org/).
+- For Linux users:
+ - Built-in: [Linux Terminal](https://www.howtogeek.com/140679/beginner-geek-how-to-start-using-the-linux-terminal/).
-In your shell, type the following command to add your username:
+### Install Git
+
+Open a command shell and run the following command to check if Git is already installed in your
+computer:
```shell
-git config --global user.name "YOUR_USERNAME"
+git --version
```
-Then verify that you have the correct username:
+If you have Git installed, the output will be:
```shell
-git config --global user.name
+git version X.Y.Z
```
-To set your email address, type the following command:
+If your computer doesn't recognize `git` as a command, you'll need to [install Git](../topics/git/how_to_install_git/index.md).
+After that, run `git --version` again to verify whether it was correctly installed.
-```shell
-git config --global user.email "your_email_address@example.com"
-```
+## Configure Git
+
+To start using Git from your computer, you'll need to enter your credentials (user name and email)
+to identify you as the author of your work. The user name and email should match the ones you're
+using on GitLab.
-To verify that you entered your email correctly, type:
+In your shell, add your user name:
```shell
-git config --global user.email
+git config --global user.name "your_username"
```
-You'll need to do this only once, since you are using the `--global` option. It
-tells Git to always use this information for anything you do on that system. If
-you want to override this with a different username or email address for specific
-projects or repositories, you can run the command without the `--global` option
-when you’re in that project, and that will default to `--local`. You can read more
-on how Git manages configurations in the [Git Config](https://git-scm.com/book/en/v2/Customizing-Git-Git-Configuration) documentation.
+And your email address:
-## Check your information
+```shell
+git config --global user.email "your_email_address@example.com"
+```
-To view the information that you entered, along with other global options, type:
+To check the configuration, run:
```shell
git config --global --list
```
+The `--global` option tells Git to always use this information for anything you do on your system.
+If you omit `--global` or use `--local`, the configuration will be applied only to the current
+repository.
+
+You can read more on how Git manages configurations in the
+[Git Config](https://git-scm.com/book/en/v2/Customizing-Git-Git-Configuration) documentation.
+
## Basic Git commands
Start using Git via the command line with the most basic commands as described below.
@@ -103,11 +126,11 @@ To start working locally on an existing remote repository, clone it with the com
files to your local computer, automatically preserving the Git connection with the
remote repository.
-You can either clone it via HTTPS or [SSH](../ssh/README.md). If you chose to clone
-it via HTTPS, you'll have to enter your credentials every time you pull and push.
+You can either clone it via [HTTPS](#clone-via-https) or [SSH](#clone-via-ssh). If you chose to
+clone it via HTTPS, you'll have to enter your credentials every time you pull and push.
You can read more about credential storage in the
[Git Credentials documentation](https://git-scm.com/book/en/v2/Git-Tools-Credential-Storage).
-With SSH, you enter your credentials only once.
+With [SSH](../ssh/README.md), you enter your credentials only once.
You can find both paths (HTTPS and SSH) by navigating to your project's landing page
and clicking **Clone**. GitLab will prompt you with both paths, from which you can copy
@@ -119,24 +142,38 @@ As an example, consider this repository path:
- SSH: `git@gitlab.com:gitlab-org/gitlab.git`
To get started, open a terminal window in the directory you wish to clone the
-repository files into, and run one of the following commands.
+repository files into, and run one of the `git clone` commands as described below.
+
+Both commands will download a copy of the files in a folder named after the project's
+name. You can then navigate to the new directory and start working on it locally.
+
+#### Clone via HTTPS
-Clone via HTTPS:
+To clone `https://gitlab.com/gitlab-org/gitlab.git` via HTTPS:
```shell
git clone https://gitlab.com/gitlab-org/gitlab.git
```
-Clone via SSH:
+You'll have to add your password every time you clone through HTTPS. If you have 2FA enabled
+for your account, you'll have to use a [Personal Access Token](../user/profile/personal_access_tokens.md)
+with **read_repository** or **write_repository** permissions instead of your account's password.
+
+If you don't have 2FA enabled, use your account's password.
+
+TIP: **Troubleshooting:**
+On Windows, if you entered incorrect passwords multiple times and GitLab is responding `Access denied`,
+you may have to add your namespace (user name or group name) to clone through HTTPS:
+`git clone https://namespace@gitlab.com/gitlab-org/gitlab.git`.
+
+#### Clone via SSH
+
+To clone `git@gitlab.com:gitlab-org/gitlab.git` via SSH:
```shell
git clone git@gitlab.com:gitlab-org/gitlab.git
```
-Both commands will download a copy of the files in a folder named after the project's
-name. You can then navigate to the directory and start working
-on it locally.
-
### Switch to the master branch
You are always in a branch when working with Git. The main branch is the master
diff --git a/doc/gitlab-geo/ha.md b/doc/gitlab-geo/ha.md
index 23ed11eaf09..0be70791d45 100644
--- a/doc/gitlab-geo/ha.md
+++ b/doc/gitlab-geo/ha.md
@@ -1,5 +1,5 @@
---
-redirect_to: '../administration/geo/replication/high_availability.md'
+redirect_to: '../administration/geo/replication/multiple_servers.md'
---
-This document was moved to [another location](../administration/geo/replication/high_availability.md).
+This document was moved to [another location](../administration/geo/replication/multiple_servers.md).
diff --git a/doc/install/README.md b/doc/install/README.md
index 83910b0a281..f1ef368685e 100644
--- a/doc/install/README.md
+++ b/doc/install/README.md
@@ -14,15 +14,15 @@ and cost of hosting.
There are many ways you can install GitLab depending on your platform:
1. **Omnibus GitLab**: The official deb/rpm packages that contain a bundle of GitLab
- and the various components it depends on like PostgreSQL, Redis, Sidekiq, etc.
+ and the various components it depends on, like PostgreSQL, Redis, Sidekiq, etc.
1. **GitLab Helm chart**: The cloud native Helm chart for installing GitLab and all
its components on Kubernetes.
1. **Docker**: The Omnibus GitLab packages dockerized.
1. **Source**: Install GitLab and all its components from scratch.
TIP: **If in doubt, choose Omnibus:**
-The Omnibus GitLab packages are mature, scalable, support
-[high availability](../administration/availability/index.md) and are used
+The Omnibus GitLab packages are mature,
+[scalable](../administration/reference_architectures/index.md) and are used
today on GitLab.com. The Helm charts are recommended for those who are familiar
with Kubernetes.
@@ -36,7 +36,7 @@ The Omnibus GitLab package uses our official deb/rpm repositories. This is
recommended for most users.
If you need additional flexibility and resilience, we recommend deploying
-GitLab as described in our [High Availability documentation](../administration/availability/index.md).
+GitLab as described in our [reference architecture documentation](../administration/reference_architectures/index.md).
[**> Install GitLab using the Omnibus GitLab package.**](https://about.gitlab.com/install/)
@@ -66,8 +66,8 @@ GitLab maintains a set of official Docker images based on the Omnibus GitLab pac
## Installing GitLab from source
-If the GitLab Omnibus package is not available in your distribution, you can
-install GitLab from source: Useful for unsupported systems like *BSD. For an
+If the Omnibus GitLab package is not available in your distribution, you can
+install GitLab from source: Useful for unsupported systems like \*BSD. For an
overview of the directory structure, read the [structure documentation](structure.md).
[**> Install GitLab from source.**](installation.md)
diff --git a/doc/install/aws/index.md b/doc/install/aws/index.md
index 48de5e274b0..41f8d7babac 100644
--- a/doc/install/aws/index.md
+++ b/doc/install/aws/index.md
@@ -2,9 +2,9 @@
type: howto
---
-# Installing GitLab HA on Amazon Web Services (AWS)
+# Installing GitLab on Amazon Web Services (AWS)
-This page offers a walkthrough of a common HA (Highly Available) configuration
+This page offers a walkthrough of a common configuration
for GitLab on AWS. You should customize it to accommodate your needs.
NOTE: **Note**
@@ -13,11 +13,10 @@ For organizations with 300 users or less, the recommended AWS installation metho
## Introduction
GitLab on AWS can leverage many of the services that are already
-configurable with GitLab High Availability (HA). These services offer a great deal of
-flexibility and can be adapted to the needs of most companies, while enabling the
-automation of both vertical and horizontal scaling.
+configurable. These services offer a great deal of
+flexibility and can be adapted to the needs of most companies.
-In this guide, we'll go through a basic HA setup where we'll start by
+In this guide, we'll go through a multi-node setup where we'll start by
configuring our Virtual Private Cloud and subnets to later integrate
services such as RDS for our database server and ElastiCache as a Redis
cluster to finally manage them within an auto scaling group with custom
@@ -54,26 +53,60 @@ Here's a list of the AWS services we will use, with links to pricing information
[Amazon S3 pricing](https://aws.amazon.com/s3/pricing/).
- **ELB**: A Classic Load Balancer will be used to route requests to the
GitLab instances. See the [Amazon ELB pricing](https://aws.amazon.com/elasticloadbalancing/pricing/).
-- **RDS**: An Amazon Relational Database Service using PostgreSQL will be used
- to provide a High Availability database configuration. See the
+- **RDS**: An Amazon Relational Database Service using PostgreSQL will be used. See the
[Amazon RDS pricing](https://aws.amazon.com/rds/postgresql/pricing/).
- **ElastiCache**: An in-memory cache environment will be used to provide a
- High Availability Redis configuration. See the
+ Redis configuration. See the
[Amazon ElastiCache pricing](https://aws.amazon.com/elasticache/pricing/).
NOTE: **Note:** Please note that while we will be using EBS for storage, we do not recommend using EFS as it may negatively impact GitLab's performance. You can review the [relevant documentation](../../administration/high_availability/nfs.md#avoid-using-awss-elastic-file-system-efs) for more details.
-## Creating an IAM EC2 instance role and profile
+## Create an IAM EC2 instance role and profile
+
+As we'll be using [Amazon S3 object storage](#amazon-s3-object-storage), our EC2 instances need to have read, write, and list permissions for our S3 buckets. To avoid embedding AWS keys in our GitLab config, we'll make use of an [IAM Role](https://docs.aws.amazon.com/IAM/latest/UserGuide/id_roles.html) to allow our GitLab instance with this access. We'll need to create an IAM policy to attach to our IAM role:
+
+### Create an IAM Policy
+
+1. Navigate to the IAM dashboard and click on **Policies** in the left menu.
+1. Click **Create policy**, select the `JSON` tab, and add a policy. We want to [follow security best practices and grant _least privilege_](https://docs.aws.amazon.com/IAM/latest/UserGuide/best-practices.html#grant-least-privilege), giving our role only the permissions needed to perform the required actions.
+ 1. Assuming you prefix the S3 bucket names with `gl-` as shown in the diagram, add the following policy:
+
+```json
+{
+ "Version": "2012-10-17",
+ "Statement": [
+ {
+ "Effect": "Allow",
+ "Action": [
+ "s3:AbortMultipartUpload",
+ "s3:CompleteMultipartUpload",
+ "s3:ListBucket",
+ "s3:PutObject",
+ "s3:GetObject",
+ "s3:DeleteObject",
+ "s3:PutObjectAcl"
+ ],
+ "Resource": [
+ "arn:aws:s3:::gl-*/*"
+ ]
+ }
+ ]
+}
+```
+
+1. Click **Review policy**, give your policy a name (we'll use `gl-s3-policy`), and click **Create policy**.
-To minimize the permissions of the user, we'll create a new [IAM](https://docs.aws.amazon.com/IAM/latest/UserGuide/introduction.html)
-role with limited access:
+### Create an IAM Role
-1. Navigate to the IAM dashboard <https://console.aws.amazon.com/iam/home> and
+1. Still on the IAM dashboard, click on **Roles** in the left menu, and
click **Create role**.
1. Create a new role by selecting **AWS service > EC2**, then click
**Next: Permissions**.
-1. Choose **AmazonEC2FullAccess** and **AmazonS3FullAccess**, then click **Next: Review**.
-1. Give the role the name `GitLabAdmin` and click **Create role**.
+1. In the policy filter, search for the `gl-s3-policy` we created above, select it, and click **Tags**.
+1. Add tags if needed and click **Review**.
+1. Give the role a name (we'll use `GitLabS3Access`) and click **Create Role**.
+
+We'll use this role when we [create a launch configuration](#create-a-launch-configuration) later on.
## Configuring the network
@@ -94,6 +127,8 @@ We'll now create a VPC, a virtual networking environment that you'll control:
![Create VPC](img/create_vpc.png)
+1. Select the VPC, click **Actions**, click **Edit DNS resolution**, and enable DNS resolution. Hit **Save** when done.
+
### Subnets
Now, let's create some subnets in different Availability Zones. Make sure
@@ -106,7 +141,7 @@ RDS instances as well:
1. Select **Subnets** from the left menu.
1. Click **Create subnet**. Give it a descriptive name tag based on the IP,
- for example `gitlab-public-10.0.0.0`, select the VPC we created previously,
+ for example `gitlab-public-10.0.0.0`, select the VPC we created previously, select an availability zone (we'll use `us-west-2a`),
and at the IPv4 CIDR block let's give it a 24 subnet `10.0.0.0/24`:
![Create subnet](img/create_subnet.png)
@@ -120,18 +155,8 @@ RDS instances as well:
| `gitlab-public-10.0.2.0` | public | `us-west-2b` | `10.0.2.0/24` |
| `gitlab-private-10.0.3.0` | private | `us-west-2b` | `10.0.3.0/24` |
-### Create NAT Gateways
-
-Instances deployed in our private subnets need to connect to the internet for updates, but should not be reachable from the public internet. To achieve this, we'll make use of [NAT Gateways](https://docs.aws.amazon.com/vpc/latest/userguide/vpc-nat-gateway.html) deployed in each of our public subnets:
-
-1. Navigate to the VPC dashboard and click on **NAT Gateways** in the left menu bar.
-1. Click **Create NAT Gateway** and complete the following:
- 1. **Subnet**: Select `gitlab-public-10.0.0.0` from the dropdown.
- 1. **Elastic IP Allocation ID**: Enter an existing Elastic IP or click **Allocate Elastic IP address** to allocate a new IP to your NAT gateway.
- 1. Add tags if needed.
- 1. Click **Create NAT Gateway**.
-
-Create a second NAT gateway but this time place it in the second public subnet, `gitlab-public-10.0.2.0`.
+1. Once all the subnets are created, enable **Auto-assign IPv4** for the two public subnets:
+ 1. Select each public subnet in turn, click **Actions**, and click **Modify auto-assign IP settings**. Enable the option and save.
### Internet Gateway
@@ -148,6 +173,19 @@ create a new one:
1. Choose `gitlab-vpc` from the list and hit **Attach**.
+### Create NAT Gateways
+
+Instances deployed in our private subnets need to connect to the internet for updates, but should not be reachable from the public internet. To achieve this, we'll make use of [NAT Gateways](https://docs.aws.amazon.com/vpc/latest/userguide/vpc-nat-gateway.html) deployed in each of our public subnets:
+
+1. Navigate to the VPC dashboard and click on **NAT Gateways** in the left menu bar.
+1. Click **Create NAT Gateway** and complete the following:
+ 1. **Subnet**: Select `gitlab-public-10.0.0.0` from the dropdown.
+ 1. **Elastic IP Allocation ID**: Enter an existing Elastic IP or click **Allocate Elastic IP address** to allocate a new IP to your NAT gateway.
+ 1. Add tags if needed.
+ 1. Click **Create NAT Gateway**.
+
+Create a second NAT gateway but this time place it in the second public subnet, `gitlab-public-10.0.2.0`.
+
### Route Tables
#### Public Route Table
@@ -179,13 +217,13 @@ Next, we must associate the **public** subnets to the route table:
We also need to create two private route tables so that instances in each private subnet can reach the internet via the NAT gateway in the corresponding public subnet in the same availability zone.
-1. Follow the same steps as above to create two private route tables. Name them `gitlab-public-a` and `gitlab-public-b` respectively.
+1. Follow the same steps as above to create two private route tables. Name them `gitlab-private-a` and `gitlab-private-b` respectively.
1. Next, add a new route to each of the private route tables where the destination is `0.0.0.0/0` and the target is one of the NAT gateways we created earlier.
- 1. Add the NAT gateway we created in `gitlab-public-10.0.0.0` as the target for the new route in the `gitlab-public-a` route table.
- 1. Similarly, add the NAT gateway in `gitlab-public-10.0.2.0` as the target for the new route in the `gitlab-public-b`.
+ 1. Add the NAT gateway we created in `gitlab-public-10.0.0.0` as the target for the new route in the `gitlab-private-a` route table.
+ 1. Similarly, add the NAT gateway in `gitlab-public-10.0.2.0` as the target for the new route in the `gitlab-private-b`.
1. Lastly, associate each private subnet with a private route table.
- 1. Associate `gitlab-private-10.0.1.0` with `gitlab-public-a`.
- 1. Associate `gitlab-private-10.0.3.0` with `gitlab-public-b`.
+ 1. Associate `gitlab-private-10.0.1.0` with `gitlab-private-a`.
+ 1. Associate `gitlab-private-10.0.3.0` with `gitlab-private-b`.
## Load Balancer
@@ -198,7 +236,7 @@ On the EC2 dashboard, look for Load Balancer in the left navigation bar:
1. In the **Select Subnets** section, select both public subnets from the list.
1. Click **Assign Security Groups** and select **Create a new security group**, give it a name
(we'll use `gitlab-loadbalancer-sec-group`) and description, and allow both HTTP and HTTPS traffic
- from anywhere (`0.0.0.0/0, ::/0`).
+ from anywhere (`0.0.0.0/0, ::/0`). Also allow SSH traffic from a single IP address or an IP address range in CIDR notation.
1. Click **Configure Security Settings** and select an SSL/TLS certificate from ACM or upload a certificate to IAM.
1. Click **Configure Health Check** and set up a health check for your EC2 instances.
1. For **Ping Protocol**, select HTTP.
@@ -232,19 +270,9 @@ On the Route 53 dashboard, click **Hosted zones** in the left navigation bar:
## PostgreSQL with RDS
For our database server we will use Amazon RDS which offers Multi AZ
-for redundancy. Let's start by creating a subnet group and then we'll
+for redundancy. First we'll create a security group and subnet group, then we'll
create the actual RDS instance.
-### RDS Subnet Group
-
-1. Navigate to the RDS dashboard and select **Subnet Groups** from the left menu.
-1. Click on **Create DB Subnet Group**.
-1. Under **Subnet group details**, enter a name (we'll use `gitlab-rds-group`), a description, and choose the `gitlab-vpc` from the VPC dropdown.
-1. Under **Add subnets**, click **Add all the subnets related to this VPC** and remove the public ones, we only want the **private subnets**. In the end, you should see `10.0.1.0/24` and `10.0.3.0/24` (as we defined them in the [subnets section](#subnets)).
-1. Click **Create** when ready.
-
- ![RDS Subnet Group](img/rds_subnet_group.png)
-
### RDS Security Group
We need a security group for our database that will allow inbound traffic from the instances we'll deploy in our `gitlab-loadbalancer-sec-group` later on:
@@ -255,21 +283,33 @@ We need a security group for our database that will allow inbound traffic from t
1. In the **Inbound rules** section, click **Add rule** and add a **PostgreSQL** rule, and set the "Custom" source as the `gitlab-loadbalancer-sec-group` we created earlier. The default PostgreSQL port is `5432`, which we'll also use when creating our database below.
1. When done, click **Create security group**.
+### RDS Subnet Group
+
+1. Navigate to the RDS dashboard and select **Subnet Groups** from the left menu.
+1. Click on **Create DB Subnet Group**.
+1. Under **Subnet group details**, enter a name (we'll use `gitlab-rds-group`), a description, and choose the `gitlab-vpc` from the VPC dropdown.
+1. Under **Add subnets**, click **Add all the subnets related to this VPC** and remove the public ones, we only want the **private subnets**. In the end, you should see `10.0.1.0/24` and `10.0.3.0/24` (as we defined them in the [subnets section](#subnets)).
+1. Click **Create** when ready.
+
+ ![RDS Subnet Group](img/rds_subnet_group.png)
+
### Create the database
+DANGER: **Danger:** Avoid using burstable instances (t class instances) for the database as this could lead to performance issues due to CPU credits running out during sustained periods of high load.
+
Now, it's time to create the database:
-1. Select **Databases** from the left menu and click **Create database**.
+1. Navigate to the RDS dashboard, select **Databases** from the left menu, and click **Create database**.
1. Select **Standard Create** for the database creation method.
1. Select **PostgreSQL** as the database engine and select **PostgreSQL 10.9-R1** from the version dropdown menu (check the [database requirements](../../install/requirements.md#postgresql-requirements) to see if there are any updates on this for your chosen version of GitLab).
1. Since this is a production server, let's choose **Production** from the **Templates** section.
1. Under **Settings**, set a DB instance identifier, a master username, and a master password. We'll use `gitlab-db-ha`, `gitlab`, and a very secure password respectively. Make a note of these as we'll need them later.
1. For the DB instance size, select **Standard classes** and select an instance size that meets your requirements from the dropdown menu. We'll use a `db.m4.large` instance.
1. Under **Storage**, configure the following:
- 1. Select **Provisioned IOPS (SSD)** from the storage type dropdown menu. Provisioned IOPS (SSD) storage is best suited for HA (though you can choose General Purpose (SSD) to reduce the costs). Read more about it at [Storage for Amazon RDS](https://docs.aws.amazon.com/AmazonRDS/latest/UserGuide/CHAP_Storage.html).
+ 1. Select **Provisioned IOPS (SSD)** from the storage type dropdown menu. Provisioned IOPS (SSD) storage is best suited for this use (though you can choose General Purpose (SSD) to reduce the costs). Read more about it at [Storage for Amazon RDS](https://docs.aws.amazon.com/AmazonRDS/latest/UserGuide/CHAP_Storage.html).
1. Allocate storage and set provisioned IOPS. We'll use the minimum values, `100` and `1000`, respectively.
1. Enable storage autoscaling (optional) and set a maximum storage threshold.
-1. Under **Availability & durability**, select **Create a standby instance** to have a standby RDS instance provisioned in a different Availability Zone. Read more at [High Availability (Multi-AZ)](https://docs.aws.amazon.com/AmazonRDS/latest/UserGuide/Concepts.MultiAZ.html).
+1. Under **Availability & durability**, select **Create a standby instance** to have a standby RDS instance provisioned in a different [Availability Zone](https://docs.aws.amazon.com/AmazonRDS/latest/UserGuide/Concepts.MultiAZ.html).
1. Under **Connectivity**, configure the following:
1. Select the VPC we created earlier (`gitlab-vpc`) from the **Virtual Private Cloud (VPC)** dropdown menu.
1. Expand the **Additional connectivity configuration** section and select the subnet group (`gitlab-rds-group`) we created earlier.
@@ -291,15 +331,6 @@ Now that the database is created, let's move on to setting up Redis with ElastiC
ElastiCache is an in-memory hosted caching solution. Redis maintains its own
persistence and is used for certain types of the GitLab application.
-### Redis Subnet Group
-
-1. Navigate to the ElastiCache dashboard from your AWS console.
-1. Go to **Subnet Groups** in the left menu, and create a new subnet group.
- Make sure to select our VPC and its [private subnets](#subnets). Click
- **Create** when ready.
-
- ![ElastiCache subnet](img/ec_subnet.png)
-
### Create a Redis Security Group
1. Navigate to the EC2 dashboard.
@@ -309,6 +340,15 @@ persistence and is used for certain types of the GitLab application.
1. In the **Inbound rules** section, click **Add rule** and add a **Custom TCP** rule, set port `6379`, and set the "Custom" source as the `gitlab-loadbalancer-sec-group` we created earlier.
1. When done, click **Create security group**.
+### Redis Subnet Group
+
+1. Navigate to the ElastiCache dashboard from your AWS console.
+1. Go to **Subnet Groups** in the left menu, and create a new subnet group (we'll name ours `gitlab-redis-group`).
+ Make sure to select our VPC and its [private subnets](#subnets). Click
+ **Create** when ready.
+
+ ![ElastiCache subnet](img/ec_subnet.png)
+
### Create the Redis Cluster
1. Navigate back to the ElastiCache dashboard.
@@ -392,7 +432,7 @@ From the EC2 dashboard:
1. Select an instance type based on your workload. Consult the [hardware requirements](../../install/requirements.md#hardware-requirements) to choose one that fits your needs (at least `c5.xlarge`, which is sufficient to accommodate 100 users).
1. Click **Configure Instance Details**:
1. In the **Network** dropdown, select `gitlab-vpc`, the VPC we created earlier.
- 1. In the **Subnet** dropdown, `select gitlab-private-10.0.1.0` from the list of subnets we created earlier.
+ 1. In the **Subnet** dropdown, select `gitlab-private-10.0.1.0` from the list of subnets we created earlier.
1. Double check that **Auto-assign Public IP** is set to `Use subnet setting (Disable)`.
1. Click **Add Storage**.
1. The root volume is 8GiB by default and should be enough given that we won’t store any data there.
@@ -405,6 +445,22 @@ From the EC2 dashboard:
Connect to your GitLab instance via **Bastion Host A** using [SSH Agent Forwarding](#use-ssh-agent-forwarding). Once connected, add the following custom configuration:
+#### Disable Let's Encrypt
+
+Since we're adding our SSL certificate at the load balancer, we do not need GitLab's built-in support for Let's Encrypt. Let's Encrypt [is enabled by default](https://docs.gitlab.com/omnibus/settings/ssl.html#lets-encrypt-integration) when using an `https` domain since GitLab 10.7, so we need to explicitly disable it:
+
+1. Open `/etc/gitlab/gitlab.rb` and disable it:
+
+ ```ruby
+ letsencrypt['enable'] = false
+ ```
+
+1. Save the file and reconfigure for the changes to take effect:
+
+ ```shell
+ sudo gitlab-ctl reconfigure
+ ```
+
#### Install the `pg_trgm` extension for PostgreSQL
From your GitLab instance, connect to the RDS instance to verify access and to install the required `pg_trgm` extension.
@@ -477,7 +533,7 @@ gitlab=# \q
#### Set up Gitaly
-CAUTION: **Caution:** In this architecture, having a single Gitaly server creates a single point of failure. This limitation will be removed once [Gitaly HA](https://gitlab.com/groups/gitlab-org/-/epics/842) is released.
+CAUTION: **Caution:** In this architecture, having a single Gitaly server creates a single point of failure. This limitation will be removed once [Gitaly Cluster](https://gitlab.com/groups/gitlab-org/-/epics/1489) is released.
Gitaly is a service that provides high-level RPC access to Git repositories.
It should be enabled and configured on a separate EC2 instance in one of the
@@ -499,6 +555,7 @@ Let's create an EC2 instance where we'll install Gitaly:
1. Click on **Configure Security Group** and let's **Create a new security group**.
1. Give your security group a name and description. We'll use `gitlab-gitaly-sec-group` for both.
1. Create a **Custom TCP** rule and add port `8075` to the **Port Range**. For the **Source**, select the `gitlab-loadbalancer-sec-group`.
+ 1. Also add an inbound rule for SSH from the `bastion-sec-group` so that we can connect using [SSH Agent Forwarding](#use-ssh-agent-forwarding) from the Bastion hosts.
1. Click **Review and launch** followed by **Launch** if you're happy with your settings.
1. Finally, acknowledge that you have access to the selected private key file or create a new one. Click **Launch Instances**.
@@ -512,48 +569,51 @@ As we are terminating SSL at our [load balancer](#load-balancer), follow the ste
Remember to run `sudo gitlab-ctl reconfigure` after saving the changes to the `gitlab.rb` file.
-#### Disable Let's Encrypt
+#### Fast lookup of authorized SSH keys
-Since we're adding our SSL certificate at the load balancer, we do not need GitLab's built-in support for Let's Encrypt. Let's Encrypt [is enabled by default](https://docs.gitlab.com/omnibus/settings/ssl.html#lets-encrypt-integration) when using an `https` domain since GitLab 10.7, so we need to explicitly disable it:
+The public SSH keys for users allowed to access GitLab are stored in `/var/opt/gitlab/.ssh/authorized_keys`. Typically we'd use shared storage so that all the instances are able to access this file when a user performs a Git action over SSH. Since we do not have shared storage in our setup, we'll update our configuration to authorize SSH users via indexed lookup in the GitLab database.
-1. Open `/etc/gitlab/gitlab.rb` and disable it:
+Follow the instructions at [Setting up fast lookup via GitLab Shell](../../administration/operations/fast_ssh_key_lookup.md#setting-up-fast-lookup-via-gitlab-shell) to switch from using the `authorized_keys` file to the database.
- ```ruby
- letsencrypt['enable'] = false
- ```
+If you do not configure fast lookup, Git actions over SSH will result in the following error:
-1. Save the file and reconfigure for the changes to take effect:
+```shell
+Permission denied (publickey).
+fatal: Could not read from remote repository.
- ```shell
- sudo gitlab-ctl reconfigure
- ```
+Please make sure you have the correct access rights
+and the repository exists.
+```
#### Configure host keys
-Ordinarily we would manually copy the contents (primary and public keys) of `/etc/ssh/` on the primary application server to `/etc/ssh` on all secondary servers. This prevents false man-in-the-middle-attack alerts when accessing servers in your High Availability cluster behind a load balancer.
+Ordinarily we would manually copy the contents (primary and public keys) of `/etc/ssh/` on the primary application server to `/etc/ssh` on all secondary servers. This prevents false man-in-the-middle-attack alerts when accessing servers in your cluster behind a load balancer.
We'll automate this by creating static host keys as part of our custom AMI. As these host keys are also rotated every time an EC2 instance boots up, "hard coding" them into our custom AMI serves as a handy workaround.
On your GitLab instance run the following:
```shell
-mkdir /etc/ssh_static
-cp -R /etc/ssh/* /etc/ssh_static
+sudo mkdir /etc/ssh_static
+sudo cp -R /etc/ssh/* /etc/ssh_static
```
In `/etc/ssh/sshd_config` update the following:
```bash
- # HostKeys for protocol version 2
- HostKey /etc/ssh_static/ssh_host_rsa_key
- HostKey /etc/ssh_static/ssh_host_dsa_key
- HostKey /etc/ssh_static/ssh_host_ecdsa_key
- HosstKey /etc/ssh_static/ssh_host_ed25519_key
+# HostKeys for protocol version 2
+HostKey /etc/ssh_static/ssh_host_rsa_key
+HostKey /etc/ssh_static/ssh_host_dsa_key
+HostKey /etc/ssh_static/ssh_host_ecdsa_key
+HostKey /etc/ssh_static/ssh_host_ed25519_key
```
#### Amazon S3 object storage
-Since we're not using NFS for shared storage, we will use [Amazon S3](https://aws.amazon.com/s3/) buckets to store backups, artifacts, LFS objects, uploads, merge request diffs, container registry images, and more. Our [documentation includes configuration instructions](../../administration/object_storage.md) for each of these, and other information about using object storage with GitLab.
+Since we're not using NFS for shared storage, we will use [Amazon S3](https://aws.amazon.com/s3/) buckets to store backups, artifacts, LFS objects, uploads, merge request diffs, container registry images, and more. Our documentation includes [instructions on how to configure object storage](../../administration/object_storage.md) for each of these data types, and other information about using object storage with GitLab.
+
+NOTE: **Note:**
+Since we are using the [AWS IAM profile](#create-an-iam-role) we created earlier, be sure to omit the AWS access key and secret access key/value pairs when configuring object storage. Instead, use `'use_iam_profile' => true` in your configuration as shown in the object storage documentation linked above.
Remember to run `sudo gitlab-ctl reconfigure` after saving the changes to the `gitlab.rb` file.
@@ -589,7 +649,7 @@ From the EC2 dashboard:
1. Select an instance type best suited for your needs (at least a `c5.xlarge`) and click **Configure details**.
1. Enter a name for your launch configuration (we'll use `gitlab-ha-launch-config`).
1. **Do not** check **Request Spot Instance**.
-1. From the **IAM Role** dropdown, pick the `GitLabAdmin` instance role we [created earlier](#creating-an-iam-ec2-instance-role-and-profile).
+1. From the **IAM Role** dropdown, pick the `GitLabAdmin` instance role we [created earlier](#create-an-iam-ec2-instance-role-and-profile).
1. Leave the rest as defaults and click **Add Storage**.
1. The root volume is 8GiB by default and should be enough given that we won’t store any data there. Click **Configure Security Group**.
1. Check **Select and existing security group** and select the `gitlab-loadbalancer-sec-group` we created earlier.
@@ -604,7 +664,7 @@ From the EC2 dashboard:
1. Select the `gitlab-vpc` from the **Network** dropdown.
1. Add both the private [subnets we created earlier](#subnets).
1. Expand the **Advanced Details** section and check the **Receive traffic from one or more load balancers** option.
-1. From the **Classic Load Balancers** dropdown, Select the load balancer we created earlier.
+1. From the **Classic Load Balancers** dropdown, select the load balancer we created earlier.
1. For **Health Check Type**, select **ELB**.
1. We'll leave our **Health Check Grace Period** as the default `300` seconds. Click **Configure scaling policies**.
1. Check **Use scaling policies to adjust the capacity of this group**.
@@ -635,7 +695,7 @@ GitLab provides its own integrated monitoring solution based on Prometheus.
For more information on how to set it up, visit the
[GitLab Prometheus documentation](../../administration/monitoring/prometheus/index.md)
-GitLab also has various [health check endpoints](../..//user/admin_area/monitoring/health_check.md)
+GitLab also has various [health check endpoints](../../user/admin_area/monitoring/health_check.md)
that you can ping and get reports.
## GitLab Runners
@@ -648,8 +708,8 @@ Read more on configuring an
## Backup and restore
-GitLab provides [a tool to backup](../../raketasks/backup_restore.md#creating-a-backup-of-the-gitlab-system)
-and restore its Git data, database, attachments, LFS objects, etc.
+GitLab provides [a tool to back up](../../raketasks/backup_restore.md#back-up-gitlab)
+and restore its Git data, database, attachments, LFS objects, and so on.
Some important things to know:
@@ -675,7 +735,7 @@ For GitLab 12.1 and earlier, use `gitlab-rake gitlab:backup:create`.
### Restoring GitLab from a backup
-To restore GitLab, first review the [restore documentation](../../raketasks/backup_restore.md#restore),
+To restore GitLab, first review the [restore documentation](../../raketasks/backup_restore.md#restore-gitlab),
and primarily the restore prerequisites. Then, follow the steps under the
[Omnibus installations section](../../raketasks/backup_restore.md#restore-for-omnibus-gitlab-installations).
@@ -708,7 +768,7 @@ After a few minutes, the new version should be up and running.
In this guide, we went mostly through scaling and some redundancy options,
your mileage may vary.
-Keep in mind that all Highly Available solutions come with a trade-off between
+Keep in mind that all solutions come with a trade-off between
cost/complexity and uptime. The more uptime you want, the more complex the solution.
And the more complex the solution, the more work is involved in setting up and
maintaining it.
@@ -717,8 +777,8 @@ Have a read through these other resources and feel free to
[open an issue](https://gitlab.com/gitlab-org/gitlab/issues/new)
to request additional material:
-- [Scaling GitLab](../../administration/scaling/index.md):
- GitLab supports several different types of clustering and high-availability.
+- [Scaling GitLab](../../administration/reference_architectures/index.md):
+ GitLab supports several different types of clustering.
- [Geo replication](../../administration/geo/replication/index.md):
Geo is the solution for widely distributed development teams.
- [Omnibus GitLab](https://docs.gitlab.com/omnibus/) - Everything you need to know
diff --git a/doc/install/azure/index.md b/doc/install/azure/index.md
index b782f1c82cd..fbc81da20d4 100644
--- a/doc/install/azure/index.md
+++ b/doc/install/azure/index.md
@@ -21,7 +21,7 @@ First, you'll need an account on Azure. There are three ways to do this:
services, exploring Microsoft's cloud for free. Even after the first 30 days, you never have to pay
anything unless you decide to transition to paid services with a Pay-As-You-Go Azure subscription.
This is a great way to try out Azure and cloud computing, and you can
- [read more in their comprehensive FAQ][Azure-Free-Account-FAQ].
+ [read more in their comprehensive FAQ](https://azure.microsoft.com/en-us/free/free-account-faq/).
- If you have an MSDN subscription, you can activate your Azure subscriber benefits. Your MSDN
subscription gives you recurring Azure credits every month, so why not put those credits to use and
try out GitLab right now?
@@ -73,7 +73,7 @@ The first items we need to configure are the basic settings of the underlying vi
_(read the [SSH documentation](../../ssh/README.md) to learn more about how to set up SSH
public keys)_
1. If you chose **Password** - enter the password you wish to use _(this is the password that you
- will use later in this tutorial to [SSH] into the VM, so make sure it's a strong password/passphrase)_
+ will use later in this tutorial to [SSH](https://en.wikipedia.org/wiki/Secure_Shell) into the VM, so make sure it's a strong password/passphrase)_
1. Choose the appropriate `Subscription` tier for your Azure account
1. Choose an existing `Resource Group` or create a new one - e.g. **"GitLab-CE-Azure"**
@@ -177,7 +177,7 @@ Click **"Save"** for the changes to take effect.
domain registrar which points to the public IP address of your Azure VM. If you do this, you'll need
to make sure your VM is configured to use a _static_ public IP address (i.e. not a _dynamic_ one)
or you will have to reconfigure the DNS `A` record each time Azure reassigns your VM a new public IP
-address. Read [IP address types and allocation methods in Azure][Azure-IP-Address-Types] to learn more.
+address. Read [IP address types and allocation methods in Azure](https://docs.microsoft.com/en-us/azure/virtual-network/virtual-network-ip-addresses-overview-arm) to learn more.
## Let's open some ports
@@ -216,7 +216,7 @@ ports to enable public internet access to two services in particular:
public access to the instance of GitLab running on our VM.
1. **SSH** (port 22) - opening port 22 will enable our VM to respond to SSH connection requests,
allowing public access (with authentication) to remote terminal sessions
- _(you'll see why we need [SSH] access to our VM [later on in this tutorial](#maintaining-your-gitlab-instance))_
+ _(you'll see why we need [SSH](https://en.wikipedia.org/wiki/Secure_Shell) access to our VM [later on in this tutorial](#maintaining-your-gitlab-instance))_
### Open HTTP on Port 80
@@ -233,7 +233,7 @@ connections:
### Open SSH on Port 22
Repeat the above process, adding a second Inbound security rule to open port 22, enabling our VM to
-accept [SSH] connections:
+accept [SSH](https://en.wikipedia.org/wiki/Secure_Shell) connections:
![Azure - Add inbound security rules - SSH](img/azure-add-inbound-sec-rule-ssh.png)
@@ -327,7 +327,7 @@ process will still be the same.
To perform an update, we need to connect directly to our Azure VM instance and run some commands
from the terminal. Our Azure VM is actually a server running Linux (Ubuntu), so we'll need to
-connect to it using SSH ([Secure Shell][SSH]).
+connect to it using SSH ([Secure Shell](https://en.wikipedia.org/wiki/Secure_Shell)).
If you're running Windows, you'll need to connect using [PuTTY](https://www.putty.org) or an equivalent Windows SSH client.
If you're running Linux or macOS, then you already have an SSH client installed.
@@ -341,7 +341,7 @@ If you're running Linux or macOS, then you already have an SSH client installed.
#### SSH from the command-line
-If you're running [SSH] from the command-line (terminal), then type in the following command to
+If you're running [SSH](https://en.wikipedia.org/wiki/Secure_Shell) from the command-line (terminal), then type in the following command to
connect to your VM, substituting `username` and `your-azure-domain-name.com` for the correct values.
Again, remember that your Azure VM domain name will be the one you
@@ -356,8 +356,8 @@ Provide your password at the prompt to authenticate.
#### SSH from Windows (PuTTY)
-If you're using [PuTTY](https://www.putty.org) in Windows as your [SSH] client, then you might want to take a quick
-read on [using PuTTY in Windows][Using-SSH-In-Putty].
+If you're using [PuTTY](https://www.putty.org) in Windows as your [SSH](https://en.wikipedia.org/wiki/Secure_Shell) client, then you might want to take a quick
+read on [using PuTTY in Windows](https://mediatemple.net/community/products/dv/204404604/using-ssh-in-putty-).
### Updating GitLab
@@ -412,31 +412,16 @@ Check out our other [Technical Articles](../../articles/index.md) or browse the
### Useful links
-- [GitLab Community Edition][CE]
-- [GitLab Enterprise Edition][EE]
-- [Microsoft Azure][Azure]
- - [Azure - Free Account FAQ][Azure-Free-Account-FAQ]
+- [GitLab Community Edition](https://about.gitlab.com/features/)
+- [GitLab Enterprise Edition](https://about.gitlab.com/features/#ee-starter)
+- [Microsoft Azure](https://azure.microsoft.com/en-us/)
+ - [Azure - Free Account FAQ](https://azure.microsoft.com/en-us/free/free-account-faq/)
- [Azure - Marketplace](https://azuremarketplace.microsoft.com/en-us/marketplace/)
- - [Azure Portal][Azure-Portal]
- - [Azure - Pricing Calculator][Azure-Pricing-Calculator]
+ - [Azure Portal](https://portal.azure.com)
+ - [Azure - Pricing Calculator](https://azure.microsoft.com/en-us/pricing/calculator/)
- [Azure - Troubleshoot SSH Connections to an Azure Linux VM](https://docs.microsoft.com/en-us/azure/virtual-machines/troubleshooting/troubleshoot-ssh-connection)
- [Azure - Properly Shutdown an Azure VM](https://build5nines.com/properly-shutdown-azure-vm-to-save-money/)
-- [SSH], [PuTTY](https://www.putty.org) and [Using SSH in PuTTY][Using-SSH-In-Putty]
-
-[Original-Blog-Post]: https://about.gitlab.com/blog/2016/07/13/how-to-setup-a-gitlab-instance-on-microsoft-azure/ "How to Set up a GitLab Instance on Microsoft Azure"
-[CE]: https://about.gitlab.com/features/
-[EE]: https://about.gitlab.com/features/#ee-starter
-
-[Azure-Troubleshoot-Linux-VM]: https://docs.microsoft.com/en-us/azure/virtual-machines/linux/troubleshoot-app-connection "Troubleshoot application connectivity issues on a Linux virtual machine in Azure"
-[Azure-IP-Address-Types]: https://docs.microsoft.com/en-us/azure/virtual-network/virtual-network-ip-addresses-overview-arm "IP address types and allocation methods in Azure"
-[Azure-How-To-Open-Ports]: https://docs.microsoft.com/en-us/azure/virtual-machines/windows/nsg-quickstart-portal "How to open ports to a virtual machine with the Azure portal"
-[Azure]: https://azure.microsoft.com/en-us/
-[Azure-Free-Account-FAQ]: https://azure.microsoft.com/en-us/free/free-account-faq/
-[Azure-Portal]: https://portal.azure.com
-[Azure-Pricing-Calculator]: https://azure.microsoft.com/en-us/pricing/calculator/
-
-[SSH]: https://en.wikipedia.org/wiki/Secure_Shell
-[Using-SSH-In-Putty]: https://mediatemple.net/community/products/dv/204404604/using-ssh-in-putty-
+- [SSH](https://en.wikipedia.org/wiki/Secure_Shell), [PuTTY](https://www.putty.org) and [Using SSH in PuTTY](https://mediatemple.net/community/products/dv/204404604/using-ssh-in-putty-)
<!-- ## Troubleshooting
diff --git a/doc/install/google_cloud_platform/index.md b/doc/install/google_cloud_platform/index.md
index 9fcf6e6420c..433eeda00b1 100644
--- a/doc/install/google_cloud_platform/index.md
+++ b/doc/install/google_cloud_platform/index.md
@@ -5,7 +5,7 @@ type: howto
# Installing GitLab on Google Cloud Platform
-This guide will help you install GitLab on a [Google Cloud Platform (GCP)][gcp] instance.
+This guide will help you install GitLab on a [Google Cloud Platform (GCP)](https://cloud.google.com/) instance.
NOTE: **Alternative installation method:**
Google provides a whitepaper for [deploying production-ready GitLab on
@@ -19,7 +19,7 @@ There are only two prerequisites in order to install GitLab on GCP:
1. You need to have a Google account.
1. You need to sign up for the GCP program. If this is your first time, Google
- gives you [$300 credit for free][freetrial] to consume over a 60-day period.
+ gives you [$300 credit for free](https://console.cloud.google.com/freetrial) to consume over a 60-day period.
Once you have performed those two steps, you can [create a VM](#creating-the-vm).
@@ -37,7 +37,7 @@ To deploy GitLab on GCP you first need to create a virtual machine:
![Launch on Compute Engine](img/vm_details.png)
-1. Click **Change** under Boot disk to select the size, type, and desired operating system. GitLab supports a [variety of linux operating systems][req], including Ubuntu and Debian. Click **Select** when finished.
+1. Click **Change** under Boot disk to select the size, type, and desired operating system. GitLab supports a [variety of linux operating systems](../requirements.md), including Ubuntu and Debian. Click **Select** when finished.
![Deploy in progress](img/boot_disk.png)
@@ -85,7 +85,7 @@ here's how you configure GitLab to be aware of the change:
![SSH button](img/vm_created.png)
- In the future you might want to set up [connecting with an SSH key][ssh]
+ In the future you might want to set up [connecting with an SSH key](https://cloud.google.com/compute/docs/instances/connecting-to-instance)
instead.
1. Edit the config file of Omnibus GitLab using your favorite text editor:
@@ -114,13 +114,13 @@ here's how you configure GitLab to be aware of the change:
### Configuring HTTPS with the domain name
Although not needed, it's strongly recommended to secure GitLab with a TLS
-certificate. Follow the steps in the [Omnibus documentation][omni-ssl].
+certificate. Follow the steps in the [Omnibus documentation](https://docs.gitlab.com/omnibus/settings/nginx.html#enable-https).
### Configuring the email SMTP settings
You need to configure the email SMTP settings correctly otherwise GitLab will
not be able to send notification emails, like comments, and password changes.
-Check the [Omnibus documentation][omni-smtp] how to do so.
+Check the [Omnibus documentation](https://docs.gitlab.com/omnibus/settings/smtp.html#smtp-settings) how to do so.
## Further reading
@@ -132,13 +132,6 @@ Kerberos, etc. Here are some documents you might be interested in reading:
- [GitLab Pages configuration](../../administration/pages/index.md)
- [GitLab Container Registry configuration](../../administration/packages/container_registry.md)
-[freetrial]: https://console.cloud.google.com/freetrial "GCP free trial"
-[gcp]: https://cloud.google.com/ "Google Cloud Platform"
-[req]: ../requirements.md "GitLab hardware and software requirements"
-[ssh]: https://cloud.google.com/compute/docs/instances/connecting-to-instance "Connecting to Linux Instances"
-[omni-smtp]: https://docs.gitlab.com/omnibus/settings/smtp.html#smtp-settings "Omnibus GitLab SMTP settings"
-[omni-ssl]: https://docs.gitlab.com/omnibus/settings/nginx.html#enable-https "Omnibus GitLab enable HTTPS"
-
<!-- ## Troubleshooting
Include any troubleshooting steps that you can foresee. If you know beforehand what issues
diff --git a/doc/install/installation.md b/doc/install/installation.md
index 740cd12b7fd..dd619e9e7b3 100644
--- a/doc/install/installation.md
+++ b/doc/install/installation.md
@@ -142,21 +142,31 @@ Starting with GitLab 12.0, Git is required to be compiled with `libpcre2`.
Find out if that's the case:
```shell
-ldd /usr/local/bin/git | grep pcre2
+ldd $(which git) | grep pcre2
```
-The output should be similar to:
+The output should contain `libpcre2-8.so.0`.
-```plaintext
-libpcre2-8.so.0 => /usr/lib/libpcre2-8.so.0 (0x00007f08461c3000)
+Is the system packaged Git too old, or not compiled with pcre2?
+Remove it:
+
+```shell
+sudo apt-get remove git-core
```
-Is the system packaged Git too old, or not compiled with pcre2? Remove it and compile from source:
+On Ubuntu, install Git from [its official PPA](https://git-scm.com/download/linux):
```shell
-# Remove packaged Git
-sudo apt-get remove git-core
+# run as root!
+add-apt-repository ppa:git-core/ppa
+apt update
+apt install git
+# repeat libpcre2 check as above
+```
+
+On Debian, use the following compilation instructions:
+```shell
# Install dependencies
sudo apt-get install -y libcurl4-openssl-dev libexpat1-dev gettext libz-dev libssl-dev build-essential
@@ -180,7 +190,7 @@ make prefix=/usr/local all
# Install into /usr/local/bin
sudo make prefix=/usr/local install
-# When editing config/gitlab.yml (Step 5), change the git -> bin_path to /usr/local/bin/git
+# When editing config/gitlab.yml later, change the git -> bin_path to /usr/local/bin/git
```
For the [Custom Favicon](../user/admin_area/appearance.md#favicon) to work, GraphicsMagick
@@ -205,7 +215,7 @@ The Ruby interpreter is required to run GitLab.
**Note:** The current supported Ruby (MRI) version is 2.6.x. GitLab 12.2
dropped support for Ruby 2.5.x.
-The use of Ruby version managers such as [RVM], [rbenv](https://github.com/rbenv/rbenv) or [chruby] with GitLab
+The use of Ruby version managers such as [RVM](https://rvm.io/), [rbenv](https://github.com/rbenv/rbenv) or [chruby](https://github.com/postmodern/chruby) with GitLab
in production, frequently leads to hard to diagnose problems. For example,
GitLab Shell is called from OpenSSH, and having a version manager can prevent
pushing and pulling over SSH. Version managers are not supported and we strongly
@@ -360,10 +370,10 @@ use of extensions and concurrent index removal, you need at least PostgreSQL 9.2
## 7. Redis
-GitLab requires at least Redis 2.8.
+GitLab requires at least Redis 5.0.
-If you are using Debian 8 or Ubuntu 14.04 and up, you can simply install
-Redis 2.8 with:
+If you are using Debian 10 or Ubuntu 20.04 and up, you can install
+Redis 5.0 with:
```shell
sudo apt-get install redis-server
@@ -966,7 +976,7 @@ If you want to switch back to Unicorn, follow these steps:
sudo -u git -H cp config/unicorn.rb.example config/unicorn.rb
```
-1. Edit the system `init.d` script to set the `USE_UNICORN=1` flag. If you have `/etc/default/gitlab`, then you should edit it instead.
+1. Edit the system `init.d` script and set `USE_WEB_SERVER="unicorn"`. If you have `/etc/default/gitlab`, then you should edit it instead.
1. Restart GitLab.
### Using Sidekiq instead of Sidekiq Cluster
@@ -1035,6 +1045,3 @@ On RedHat/CentOS:
```shell
sudo yum groupinstall 'Development Tools'
```
-
-[RVM]: https://rvm.io/ "RVM Homepage"
-[chruby]: https://github.com/postmodern/chruby "chruby on GitHub"
diff --git a/doc/install/openshift_and_gitlab/index.md b/doc/install/openshift_and_gitlab/index.md
index a71d839e742..fd81b7f6caf 100644
--- a/doc/install/openshift_and_gitlab/index.md
+++ b/doc/install/openshift_and_gitlab/index.md
@@ -14,7 +14,7 @@ for details.
## Introduction
[OpenShift Origin](https://www.okd.io/) (**Note:** renamed to OKD in Aug 2018) is an open source container application
-platform created by [RedHat], based on [Kubernetes](https://kubernetes.io/) and [Docker]. That means
+platform created by [RedHat](https://www.redhat.com/en), based on [Kubernetes](https://kubernetes.io/) and [Docker](https://www.docker.com). That means
you can host your own PaaS for free and almost with no hassle.
In this tutorial, we will see how to deploy GitLab in OpenShift using GitLab's
@@ -34,16 +34,16 @@ offered by the OpenShift developers and managed by Vagrant. If you haven't done
already, go ahead and install the following components as they are essential to
test OpenShift easily:
-- [VirtualBox]
-- [Vagrant]
+- [VirtualBox](https://www.virtualbox.org/wiki/Downloads)
+- [Vagrant](https://www.vagrantup.com/downloads.html)
- [OpenShift Client](https://docs.okd.io/3.11/cli_reference/get_started_cli.html) (`oc` for short)
It is also important to mention that for the purposes of this tutorial, the
latest Origin release is used:
-- **oc** `v1.3.0` (must be [installed][oc-gh] locally on your computer)
-- **OpenShift** `v1.3.0` (is pre-installed in the [VM image][vm-new])
-- **Kubernetes** `v1.3.0` (is pre-installed in the [VM image][vm-new])
+- **oc** `v1.3.0` (must be [installed](https://github.com/openshift/origin/releases/tag/v1.3.0) locally on your computer)
+- **OpenShift** `v1.3.0` (is pre-installed in the [VM image](https://app.vagrantup.com/openshift/boxes/origin-all-in-one))
+- **Kubernetes** `v1.3.0` (is pre-installed in the [VM image](https://app.vagrantup.com/openshift/boxes/origin-all-in-one))
>**Note:**
If you intend to deploy GitLab on a production OpenShift cluster, there are some
@@ -302,7 +302,7 @@ template:
- `gitlab-ce-postgresql`
While PostgreSQL and Redis are bundled in Omnibus GitLab, the template is using
-separate images as you can see from [this line][line] in the template.
+separate images as you can see from [this line](https://gitlab.com/gitlab-org/omnibus-gitlab/blob/658c065c8d022ce858dd63eaeeadb0b2ddc8deea/docker/openshift-template.json#L239) in the template.
The predefined values have been calculated for the purposes of testing out
GitLab in the all-in-one VM. You don't need to change anything here, hit
@@ -371,7 +371,7 @@ running scaled to 2.
Upping the GitLab pods is actually like adding new application servers to your
cluster. You can see how that would work if you didn't use GitLab with
-OpenShift by following the [HA documentation][ha] for the application servers.
+OpenShift by following the [HA documentation](../../administration/high_availability/gitlab.md) for the application servers.
Bare in mind that you may need more resources (CPU, RAM, disk space) when you
scale up. If a pod is in pending state for too long, you can navigate to
@@ -505,14 +505,3 @@ And remember that in this tutorial we just scratched the surface of what Origin
is capable of. As always, you can refer to the detailed
[documentation](https://docs.okd.io) to learn more about deploying your own OpenShift
PaaS and managing your applications with the ease of containers.
-
-[RedHat]: https://www.redhat.com/en "RedHat website"
-[vm-new]: https://app.vagrantup.com/openshift/boxes/origin-all-in-one "Official OpenShift Vagrant box on Vagrant Cloud"
-[template]: https://gitlab.com/gitlab-org/omnibus-gitlab/blob/master/docker/openshift-template.json "OpenShift template for GitLab"
-[Docker]: https://www.docker.com "Docker website"
-[VirtualBox]: https://www.virtualbox.org/wiki/Downloads "VirtualBox downloads"
-[Vagrant]: https://www.vagrantup.com/downloads.html "Vagrant downloads"
-[old-post]: https://blog.openshift.com/deploy-gitlab-openshift/ "Old post - Deploy GitLab on OpenShift"
-[line]: https://gitlab.com/gitlab-org/omnibus-gitlab/blob/658c065c8d022ce858dd63eaeeadb0b2ddc8deea/docker/openshift-template.json#L239 "GitLab - OpenShift template"
-[oc-gh]: https://github.com/openshift/origin/releases/tag/v1.3.0 "OpenShift Origin 1.3.0 release on GitHub"
-[ha]: ../../administration/high_availability/gitlab.md "Documentation - GitLab High Availability"
diff --git a/doc/install/relative_url.md b/doc/install/relative_url.md
index 595304e27e2..f8ff8e75c4d 100644
--- a/doc/install/relative_url.md
+++ b/doc/install/relative_url.md
@@ -10,7 +10,7 @@ be installed under a relative URL, for example `https://example.com/gitlab`.
This document describes how to run GitLab under a relative URL for installations
from source. If you are using an Omnibus package,
-[the steps are different][omnibus-rel]. Use this guide along with the
+[the steps are different](https://docs.gitlab.com/omnibus/settings/configuration.html#configuring-a-relative-url-for-gitlab). Use this guide along with the
[installation guide](installation.md) if you are installing GitLab for the
first time.
@@ -30,7 +30,7 @@ serve GitLab under a relative URL is:
- `/home/git/gitlab-shell/config.yml`
- `/etc/default/gitlab`
-After all the changes you need to recompile the assets and [restart GitLab].
+After all the changes you need to recompile the assets and [restart GitLab](../administration/restart_gitlab.md#installations-from-source).
## Relative URL requirements
@@ -112,7 +112,7 @@ Make sure to follow all steps below:
If you are using a custom init script, make sure to edit the above
GitLab Workhorse setting as needed.
-1. [Restart GitLab][] for the changes to take effect.
+1. [Restart GitLab](../administration/restart_gitlab.md#installations-from-source) for the changes to take effect.
## Disable relative URL in GitLab
@@ -123,9 +123,6 @@ To disable the relative URL:
1. Follow the same as above starting from 2. and set up the
GitLab URL to one that doesn't contain a relative path.
-[omnibus-rel]: https://docs.gitlab.com/omnibus/settings/configuration.html#configuring-a-relative-url-for-gitlab "How to set up relative URL in Omnibus GitLab"
-[restart gitlab]: ../administration/restart_gitlab.md#installations-from-source "How to restart GitLab"
-
<!-- ## Troubleshooting
Include any troubleshooting steps that you can foresee. If you know beforehand what issues
diff --git a/doc/install/requirements.md b/doc/install/requirements.md
index f78525659f2..09ad5f9afd7 100644
--- a/doc/install/requirements.md
+++ b/doc/install/requirements.md
@@ -11,15 +11,15 @@ as the hardware requirements that are needed to install and use GitLab.
### Supported Linux distributions
-- Ubuntu
-- Debian
-- CentOS
-- openSUSE
+- Ubuntu (16.04/18.04)
+- Debian (8/9/10)
+- CentOS (6/7/8)
+- openSUSE (Leap 15.1/Enterprise Server 12.2)
- Red Hat Enterprise Linux (please use the CentOS packages and instructions)
- Scientific Linux (please use the CentOS packages and instructions)
- Oracle Linux (please use the CentOS packages and instructions)
-For the installations options, see [the main installation page](README.md).
+For the installation options, see [the main installation page](README.md).
### Unsupported Linux distributions and Unix-like operating systems
@@ -68,10 +68,14 @@ GitLab uses [webpack](https://webpack.js.org/) to compile frontend assets, which
version of Node.js 10.13.0.
You can check which version you are running with `node -v`. If you are running
-a version older than `v10.13.0`, you need to update to a newer version. You
+a version older than `v10.13.0`, you need to update it to a newer version. You
can find instructions to install from community maintained packages or compile
from source at the [Node.js website](https://nodejs.org/en/download/).
+## Redis versions
+
+GitLab requires Redis 5.0+. Beginning in GitLab 13.0, lower versions are not supported.
+
## Hardware requirements
### Storage
@@ -95,7 +99,7 @@ This is the recommended minimum hardware for a handful of example GitLab user ba
- 4 cores supports up to 500 users
- 8 cores supports up to 1,000 users
- 32 cores supports up to 5,000 users
-- More users? Run it high-availability on [multiple application servers](https://about.gitlab.com/solutions/high-availability/)
+- More users? Consult the [reference architectures page](../administration/reference_architectures/index.md)
### Memory
@@ -112,7 +116,7 @@ errors during usage.
- 16GB RAM supports up to 500 users
- 32GB RAM supports up to 1,000 users
- 128GB RAM supports up to 5,000 users
-- More users? Run it high-availability on [multiple application servers](https://about.gitlab.com/solutions/high-availability/)
+- More users? Consult the [reference architectures page](../administration/reference_architectures/index.md)
We recommend having at least [2GB of swap on your server](https://askubuntu.com/a/505344/310789), even if you currently have
enough available RAM. Having swap will help reduce the chance of errors occurring
@@ -122,7 +126,7 @@ available when needed.
Our [Memory Team](https://about.gitlab.com/handbook/engineering/development/enablement/memory/) is actively working to reduce the memory requirement.
-NOTE: **Note:** The 25 workers of Sidekiq will show up as separate processes in your process overview (such as `top` or `htop`) but they share the same RAM allocation since Sidekiq is a multithreaded application. Please see the section below about Unicorn workers for information about how many you need of those.
+NOTE: **Note:** The 25 workers of Sidekiq will show up as separate processes in your process overview (such as `top` or `htop`) but they share the same RAM allocation since Sidekiq is a multithreaded application. Please see the section below about Unicorn workers for information about how many you need for those.
## Database
@@ -139,9 +143,12 @@ MySQL/MariaDB are advised to [migrate to PostgreSQL](../update/mysql_to_postgres
### PostgreSQL Requirements
-As of GitLab 10.0, PostgreSQL 9.6 or newer is required, and earlier versions are
-not supported. We highly recommend users to use PostgreSQL 9.6 as this
-is the PostgreSQL version used for development and testing.
+We highly recommend users to use the minimum PostgreSQL versions specified below as these are the versions used for development and testing.
+
+GitLab version | Minimum PostgreSQL version
+-|-
+10.0 | 9.6
+12.10 | 11
Users using PostgreSQL must ensure the `pg_trgm` extension is loaded into every
GitLab database. This extension can be enabled (using a PostgreSQL super user)
@@ -167,7 +174,7 @@ If you are using [GitLab Geo](../development/geo.md):
- The
[tracking database](../development/geo.md#using-the-tracking-database)
requires the
- [postgres_fdw](https://www.postgresql.org/docs/9.6/postgres-fdw.html)
+ [postgres_fdw](https://www.postgresql.org/docs/11/postgres-fdw.html)
extension.
```sql
@@ -180,28 +187,50 @@ For most instances we recommend using: (CPU cores * 1.5) + 1 = Unicorn workers.
For example a node with 4 cores would have 7 Unicorn workers.
For all machines that have 2GB and up we recommend a minimum of three Unicorn workers.
-If you have a 1GB machine we recommend to configure only two Unicorn workers to prevent excessive swapping.
+If you have a 1GB machine we recommend to configure only two Unicorn workers to prevent excessive
+swapping.
+
+As long as you have enough available CPU and memory capacity, it's okay to increase the number of
+Unicorn workers and this will usually help to reduce the response time of the applications and
+increase the ability to handle parallel requests.
+
+To change the Unicorn workers when you have the Omnibus package (which defaults to the
+recommendation above) please see [the Unicorn settings in the Omnibus GitLab documentation](https://docs.gitlab.com/omnibus/settings/unicorn.html).
+
+## Puma settings
+
+The recommended settings for Puma are determined by the infrastructure on which it's running.
+Omnibus GitLab defaults to the recommended Puma settings. Regardless of installation method, you can
+tune the Puma settings.
-As long as you have enough available CPU and memory capacity, it's okay to increase the number of Unicorn workers and this will usually help to reduce the response time of the applications and increase the ability to handle parallel requests.
+If you're using Omnibus GitLab, see [Puma settings](https://docs.gitlab.com/omnibus/settings/puma.html)
+for instructions on changing the Puma settings.
-To change the Unicorn workers when you have the Omnibus package (which defaults to the recommendation above) please see [the Unicorn settings in the Omnibus GitLab documentation](https://docs.gitlab.com/omnibus/settings/unicorn.html).
+### Puma workers
-## Puma Workers
+The recommended number of workers is calculated as the highest of the following:
-For most instances we recommend using: max(CPU cores * 0.9, 2) = Puma workers.
-For example a node with 4 cores would have 3 Puma workers.
+- `2`
+- Number of CPU cores - 1
-For all machines that have 4GB and up we recommend a minimum of three Puma workers.
-If you have a 2GB machine we recommend to configure only one Puma worker to prevent excessive swapping.
+For example a node with 4 cores should be configured with 3 Puma workers.
-By default each Puma worker runs with 4 threads. We do not recommend setting more,
-due to how [Ruby MRI multi-threading](https://en.wikipedia.org/wiki/Global_interpreter_lock) works.
+You can increase the number of Puma workers, providing enough CPU and memory capacity is available.
+A higher number of Puma workers will usually help to reduce the response time of the application
+and increase the ability to handle parallel requests. You must perform testing to verify the
+optimal settings for your infrastructure.
-For cases when you have to use [Legacy Rugged code](../development/gitaly.md#legacy-rugged-code) it is recommended to set number of threads to 1.
+### Puma threads
-As long as you have enough available CPU and memory capacity, it's okay to increase the number of Puma workers and this will usually help to reduce the response time of the applications and increase the ability to handle parallel requests.
+The recommended number of threads is dependent on several factors, including total memory, and use
+of [legacy Rugged code](../development/gitaly.md#legacy-rugged-code).
-To change the Puma workers when you have the Omnibus package (which defaults to the recommendation above) please see [the Puma settings in the Omnibus GitLab documentation](https://docs.gitlab.com/omnibus/settings/puma.html).
+- If the operating system has a maximum 2 GB of memory, the recommended number of threads is `1`.
+ A higher value will result in excess swapping, and decrease performance.
+- If legacy Rugged code is in use, the recommended number of threads is `1`.
+- In all other cases, the recommended number of threads is `4`. We do not recommend setting this
+higher, due to how [Ruby MRI multi-threading](https://en.wikipedia.org/wiki/Global_interpreter_lock)
+works.
## Redis and Sidekiq
diff --git a/doc/integration/README.md b/doc/integration/README.md
index bba38922b9d..f06cb5e4bb6 100644
--- a/doc/integration/README.md
+++ b/doc/integration/README.md
@@ -83,7 +83,7 @@ at Super User also has relevant information.
**Omnibus Trusted Chain**
[Install the self signed certificate or custom certificate authorities](https://docs.gitlab.com/omnibus/common_installation_problems/README.html#using-self-signed-certificate-or-custom-certificate-authorities)
-in to GitLab Omnibus.
+in to Omnibus GitLab.
It is enough to concatenate the certificate to the main trusted certificate
however it may be overwritten during upgrades:
diff --git a/doc/integration/azure.md b/doc/integration/azure.md
index 75469fcbb98..809a3b703fc 100644
--- a/doc/integration/azure.md
+++ b/doc/integration/azure.md
@@ -22,7 +22,7 @@ To enable the Microsoft Azure OAuth2 OmniAuth provider you must register your ap
1. Add a "Reply URL" pointing to the Azure OAuth callback of your GitLab installation (e.g. `https://gitlab.mycompany.com/users/auth/azure_oauth2/callback`).
-1. Create a "Client secret" by selecting a duration, the secret will be generated as soon as you click the "Save" button in the bottom menu..
+1. Create a "Client secret" by selecting a duration, the secret will be generated as soon as you click the "Save" button in the bottom menu.
1. Note the "CLIENT ID" and the "CLIENT SECRET".
diff --git a/doc/integration/elasticsearch.md b/doc/integration/elasticsearch.md
index fcd1c03a556..ae9d5bd8f60 100644
--- a/doc/integration/elasticsearch.md
+++ b/doc/integration/elasticsearch.md
@@ -48,12 +48,12 @@ For indexing Git repository data, GitLab uses an [indexer written in Go](https:/
The way you install the Go indexer depends on your version of GitLab:
-- For GitLab Omnibus 11.8 and above, see [GitLab Omnibus](#gitlab-omnibus).
-- For installations from source or older versions of GitLab Omnibus, install the indexer [From Source](#from-source).
+- For Omnibus GitLab 11.8 and above, see [Omnibus GitLab](#omnibus-gitlab).
+- For installations from source or older versions of Omnibus GitLab, install the indexer [From Source](#from-source).
-### GitLab Omnibus
+### Omnibus GitLab
-Since GitLab 11.8 the Go indexer is included in GitLab Omnibus.
+Since GitLab 11.8 the Go indexer is included in Omnibus GitLab.
The former Ruby-based indexer was removed in [GitLab 12.3](https://gitlab.com/gitlab-org/gitlab/issues/6481).
### From source
@@ -152,8 +152,8 @@ The following Elasticsearch settings are available:
| `AWS Access Key` | The AWS access key. |
| `AWS Secret Access Key` | The AWS secret access key. |
| `Maximum field length` | See [the explanation in instance limits.](../administration/instance_limits.md#maximum-field-length). |
-| `Maximum bulk request size (MiB)` | Repository indexing uses the Elasticsearch bulk request API. This setting determines the maximum size of an individual bulk request during these operations. |
-| `Bulk request concurrency` | Each repository indexing operation may submit bulk requests in parallel. This increases indexing performance, but fills the Elasticsearch bulk requests queue faster. |
+| `Maximum bulk request size (MiB)` | The Maximum Bulk Request size is used by GitLab's Golang-based indexer processes and indicates how much data it ought to collect (and store in memory) in a given indexing process before submitting the payload to Elasticsearch’s Bulk API. This setting works in tandem with the Bulk request concurrency setting (see below) and needs to accomodate the resource constraints of both the Elasticsearch host(s) and the host(s) running GitLab's Golang-based indexer either from the `gitlab-rake` command or the Sidekiq tasks. |
+| `Bulk request concurrency` | The Bulk request concurrency indicates how many of GitLab's Golang-based indexer processes (or threads) can run in parallel to collect data to subsequently submit to Elasticsearch’s Bulk API. This increases indexing performance, but fills the Elasticsearch bulk requests queue faster. This setting works in tandem with the Maximum bulk request size setting (see above) and needs to accomodate the resource constraints of both the Elasticsearch host(s) and the host(s) running GitLab's Golang-based indexer either from the `gitlab-rake` command or the Sidekiq tasks. |
### Limiting namespaces and projects
@@ -174,7 +174,7 @@ If no namespaces or projects are selected, no Elasticsearch indexing will take p
CAUTION: **Warning**:
If you have already indexed your instance, you will have to regenerate the index in order to delete all existing data
-for filtering to work correctly. To do this run the Rake tasks `gitlab:elastic:create_empty_index` and
+for filtering to work correctly. To do this run the Rake tasks `gitlab:elastic:recreate_index` and
`gitlab:elastic:clear_index_status`. Afterwards, removing a namespace or a project from the list will delete the data
from the Elasticsearch index as expected.
@@ -235,6 +235,8 @@ To index via the Admin Area:
### Indexing through Rake tasks
+Indexing can be performed using Rake tasks.
+
#### Indexing small instances
CAUTION: **Warning**:
@@ -400,43 +402,32 @@ or creating [extra Sidekiq processes](../administration/operations/extra_sidekiq
For repository and snippet files, GitLab will only index up to 1 MiB of content, in order to avoid indexing timeouts.
-## GitLab Elasticsearch Rake Tasks
-
-There are several Rake tasks available to you via the command line:
-
-- [`sudo gitlab-rake gitlab:elastic:index`](https://gitlab.com/gitlab-org/gitlab/blob/master/ee/lib/tasks/gitlab/elastic.rake)
- - This is a wrapper task. It does the following:
- - `sudo gitlab-rake gitlab:elastic:create_empty_index`
- - `sudo gitlab-rake gitlab:elastic:clear_index_status`
- - `sudo gitlab-rake gitlab:elastic:index_projects`
- - `sudo gitlab-rake gitlab:elastic:index_snippets`
-- [`sudo gitlab-rake gitlab:elastic:index_projects`](https://gitlab.com/gitlab-org/gitlab/blob/master/ee/lib/tasks/gitlab/elastic.rake)
- - This iterates over all projects and queues Sidekiq jobs to index them in the background.
-- [`sudo gitlab-rake gitlab:elastic:index_projects_status`](https://gitlab.com/gitlab-org/gitlab/blob/master/ee/lib/tasks/gitlab/elastic.rake)
- - This determines the overall status of the indexing. It is done by counting the total number of indexed projects, dividing by a count of the total number of projects, then multiplying by 100.
-- [`sudo gitlab-rake gitlab:elastic:create_empty_index`](https://gitlab.com/gitlab-org/gitlab/blob/master/ee/lib/tasks/gitlab/elastic.rake)
- - This generates an empty index on the Elasticsearch side, deleting the existing one if present.
-- [`sudo gitlab-rake gitlab:elastic:clear_index_status`](https://gitlab.com/gitlab-org/gitlab/blob/master/ee/lib/tasks/gitlab/elastic.rake)
- - This deletes all instances of IndexStatus for all projects.
-- [`sudo gitlab-rake gitlab:elastic:delete_index`](https://gitlab.com/gitlab-org/gitlab/blob/master/ee/lib/tasks/gitlab/elastic.rake)
- - This removes the GitLab index on the Elasticsearch instance.
-- [`sudo gitlab-rake gitlab:elastic:recreate_index`](https://gitlab.com/gitlab-org/gitlab/blob/master/ee/lib/tasks/gitlab/elastic.rake)
- - Does the same thing as `sudo gitlab-rake gitlab:elastic:create_empty_index`
-- [`sudo gitlab-rake gitlab:elastic:index_snippets`](https://gitlab.com/gitlab-org/gitlab/blob/master/ee/lib/tasks/gitlab/elastic.rake)
- - Performs an Elasticsearch import that indexes the snippets data.
-- [`sudo gitlab-rake gitlab:elastic:projects_not_indexed`](https://gitlab.com/gitlab-org/gitlab/blob/master/ee/lib/tasks/gitlab/elastic.rake)
- - Displays which projects are not indexed.
-- [`sudo gitlab-rake gitlab:elastic:reindex_to_another_cluster[<SOURCE_CLUSTER_URL>,<DESTINATION_CLUSTER_URL>]`](https://gitlab.com/gitlab-org/gitlab/blob/master/ee/lib/tasks/gitlab/elastic.rake)
- - Creates a new index in the destination cluster from the source index using
- Elasticsearch "reindex from remote", where the source index is copied to the
- destination. This is useful when migrating to a new cluster because it should be
- quicker than reindexing via GitLab.
-
- NOTE: **Note:**
- Your source cluster must be whitelisted in your destination cluster's Elasticsearch
- settings. See [Reindex from remote](https://www.elastic.co/guide/en/elasticsearch/reference/current/docs-reindex.html#reindex-from-remote).
-
-### Environment Variables
+## GitLab Elasticsearch Rake tasks
+
+Rake tasks are available to:
+
+- [Build and install](#building-and-installing) the indexer.
+- Delete indexes when [disabling Elasticsearch](#disabling-elasticsearch).
+- [Add GitLab data](#adding-gitlabs-data-to-the-elasticsearch-index) to an index.
+
+The following are some available Rake tasks:
+
+| Task | Description |
+|:--------------------------------------------------------------------------------------------------------------------------------------------------------|:------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------|
+| [`sudo gitlab-rake gitlab:elastic:index`](https://gitlab.com/gitlab-org/gitlab/blob/master/ee/lib/tasks/gitlab/elastic.rake) | Wrapper task for `gitlab:elastic:create_empty_index`, `gitlab:elastic:clear_index_status`, `gitlab:elastic:index_projects`, and `gitlab:elastic:index_snippets`. |
+| [`sudo gitlab-rake gitlab:elastic:index_projects`](https://gitlab.com/gitlab-org/gitlab/blob/master/ee/lib/tasks/gitlab/elastic.rake) | Iterates over all projects and queues Sidekiq jobs to index them in the background. |
+| [`sudo gitlab-rake gitlab:elastic:index_projects_status`](https://gitlab.com/gitlab-org/gitlab/blob/master/ee/lib/tasks/gitlab/elastic.rake) | Determines the overall status of the indexing. It is done by counting the total number of indexed projects, dividing by a count of the total number of projects, then multiplying by 100. |
+| [`sudo gitlab-rake gitlab:elastic:clear_index_status`](https://gitlab.com/gitlab-org/gitlab/blob/master/ee/lib/tasks/gitlab/elastic.rake) | Deletes all instances of IndexStatus for all projects. |
+| [`sudo gitlab-rake gitlab:elastic:create_empty_index[<INDEX_NAME>]`](https://gitlab.com/gitlab-org/gitlab/blob/master/ee/lib/tasks/gitlab/elastic.rake) | Generates an empty index on the Elasticsearch side only if it doesn't already exist. |
+| [`sudo gitlab-rake gitlab:elastic:delete_index[<INDEX_NAME>]`](https://gitlab.com/gitlab-org/gitlab/blob/master/ee/lib/tasks/gitlab/elastic.rake) | Removes the GitLab index on the Elasticsearch instance. |
+| [`sudo gitlab-rake gitlab:elastic:recreate_index[<INDEX_NAME>]`](https://gitlab.com/gitlab-org/gitlab/blob/master/ee/lib/tasks/gitlab/elastic.rake) | Wrapper task for `gitlab:elastic:delete_index[<INDEX_NAME>]` and `gitlab:elastic:create_empty_index[<INDEX_NAME>]`. |
+| [`sudo gitlab-rake gitlab:elastic:index_snippets`](https://gitlab.com/gitlab-org/gitlab/blob/master/ee/lib/tasks/gitlab/elastic.rake) | Performs an Elasticsearch import that indexes the snippets data. |
+| [`sudo gitlab-rake gitlab:elastic:projects_not_indexed`](https://gitlab.com/gitlab-org/gitlab/blob/master/ee/lib/tasks/gitlab/elastic.rake) | Displays which projects are not indexed. |
+
+NOTE: **Note:**
+The `INDEX_NAME` parameter is optional and will use the default index name from the current `RAILS_ENV` if not set.
+
+### Environment variables
In addition to the Rake tasks, there are some environment variables that can be used to modify the process:
@@ -456,7 +447,7 @@ Indexing project repositories...I, [2019-03-04T21:27:03.083410 #3384] INFO -- :
I, [2019-03-04T21:27:05.215266 #3384] INFO -- : Indexing GitLab User / test (ID=33) is done!
```
-## Elasticsearch Index Scopes
+## Elasticsearch index scopes
When performing a search, the GitLab index will use the following scopes:
@@ -506,6 +497,8 @@ However, some larger installations may wish to tune the merge policy settings:
## Troubleshooting
+### Common issues
+
Here are some common pitfalls and how to overcome them:
- **How can I verify my GitLab instance is using Elasticsearch?**
@@ -631,9 +624,13 @@ Here are some common pitfalls and how to overcome them:
Gitlab::Elastic::Indexer::Error: time="2020-01-23T09:13:00Z" level=fatal msg="health check timeout: no Elasticsearch node available"
```
- You probably have not used either `http://` or `https://` as part of your value in the **"URL"** field of the Elasticseach Integration Menu. Please make sure you are using either `http://` or `https://` in this field as the [Elasticsearch client for Go](https://github.com/olivere/elastic) that we are using [needs the prefix for the URL to be accepted as valid](https://github.com/olivere/elastic/commit/a80af35aa41856dc2c986204e2b64eab81ccac3a).
+ You probably have not used either `http://` or `https://` as part of your value in the **"URL"** field of the Elasticsearch Integration Menu. Please make sure you are using either `http://` or `https://` in this field as the [Elasticsearch client for Go](https://github.com/olivere/elastic) that we are using [needs the prefix for the URL to be accepted as valid](https://github.com/olivere/elastic/commit/a80af35aa41856dc2c986204e2b64eab81ccac3a).
Once you have corrected the formatting of the URL, delete the index (via the [dedicated Rake task](#gitlab-elasticsearch-rake-tasks)) and [reindex the content of your instance](#adding-gitlabs-data-to-the-elasticsearch-index).
+### Low level troubleshooting
+
+There is more [low level troubleshooting documentation](../administration/troubleshooting/elasticsearch.md) for when you experience other issues, including poor performance.
+
### Known Issues
- **[Elasticsearch `code_analyzer` doesn't account for all code cases](https://gitlab.com/gitlab-org/gitlab/issues/10693)**
diff --git a/doc/integration/github.md b/doc/integration/github.md
index c957fc24eb8..68971c510d5 100644
--- a/doc/integration/github.md
+++ b/doc/integration/github.md
@@ -138,11 +138,8 @@ You will also need to disable Git SSL verification on the server hosting GitLab.
git config --global http.sslVerify false
```
-For the changes to take effect, [reconfigure GitLab] if you installed
-via Omnibus, or [restart GitLab] if you installed from source.
-
-[reconfigure GitLab]: ../administration/restart_gitlab.md#omnibus-gitlab-reconfigure
-[restart GitLab]: ../administration/restart_gitlab.md#installations-from-source
+For the changes to take effect, [reconfigure GitLab](../administration/restart_gitlab.md#omnibus-gitlab-reconfigure) if you installed
+via Omnibus, or [restart GitLab](../administration/restart_gitlab.md#installations-from-source) if you installed from source.
## Troubleshooting
diff --git a/doc/integration/gitlab.md b/doc/integration/gitlab.md
index 4e62f6389c8..f423863ce8e 100644
--- a/doc/integration/gitlab.md
+++ b/doc/integration/gitlab.md
@@ -24,9 +24,8 @@ GitLab.com will generate an application ID and secret key for you to use.
1. Select **Save application**.
-1. You should now see a **Application Id** and **Secret** near the top right of the page (see screenshot).
- Keep this page open as you continue configuration.
- ![GitLab app](img/gitlab_app.png)
+1. You should now see an **Application ID** and **Secret**. Keep this page open as you continue
+ configuration.
1. On your GitLab server, open the configuration file.
diff --git a/doc/integration/img/gitlab_app.png b/doc/integration/img/gitlab_app.png
deleted file mode 100644
index 228e8a01305..00000000000
--- a/doc/integration/img/gitlab_app.png
+++ /dev/null
Binary files differ
diff --git a/doc/integration/img/jenkins_gitlab_service_settings.png b/doc/integration/img/jenkins_gitlab_service_settings.png
deleted file mode 100644
index 5a12e9cb39a..00000000000
--- a/doc/integration/img/jenkins_gitlab_service_settings.png
+++ /dev/null
Binary files differ
diff --git a/doc/integration/jenkins.md b/doc/integration/jenkins.md
index 5514b756c00..485ca67c9fc 100644
--- a/doc/integration/jenkins.md
+++ b/doc/integration/jenkins.md
@@ -1,119 +1,163 @@
# Jenkins CI service **(STARTER)**
->**Note:**
-In GitLab 8.3, Jenkins integration using the
-[GitLab Hook Plugin](https://wiki.jenkins.io/display/JENKINS/GitLab+Hook+Plugin)
-was deprecated in favor of the
-[GitLab Plugin](https://wiki.jenkins.io/display/JENKINS/GitLab+Plugin).
-The deprecated integration has been renamed to [Jenkins CI (Deprecated)](jenkins_deprecated.md) in the
-integration settings. We may remove this in a future release and recommend
-using the new 'Jenkins CI' integration instead which is described in this
-document.
-
-## Overview
-
-[Jenkins](https://jenkins.io/) is a great Continuous Integration tool, similar to our built-in
-[GitLab CI/CD](../ci/README.md).
-
-GitLab's Jenkins integration allows you to trigger a Jenkins build when you
-push code to a repository, or when a merge request is created. Additionally,
-it shows the pipeline status on merge requests widgets and on the project's home page.
-
-Videos are also available on [GitLab workflow with Jira issues and Jenkins pipelines](https://youtu.be/Jn-_fyra7xQ)
-and [Migrating from Jenkins to GitLab](https://www.youtube.com/watch?v=RlEVGOpYF5Y).
-
-## Use cases
-
-- Suppose you are new to GitLab, and want to keep using Jenkins until you prepare
- your projects to build with [GitLab CI/CD](../ci/README.md). You set up the
- integration between GitLab and Jenkins, then you migrate to GitLab CI/CD later. While
- you organize yourself and your team to onboard GitLab, you keep your pipelines
- running with Jenkins, but view the results in your project's repository in GitLab.
-- Your team uses [Jenkins Plugins](https://plugins.jenkins.io/) for other proceedings,
- therefore, you opt for keep using Jenkins to build your apps. Show the results of your
- pipelines directly in GitLab.
+NOTE: **Note:**
+This documentation focuses only on how to **configure** a Jenkins *integration* with
+GitLab. Learn how to **migrate** from Jenkins to GitLab CI/CD in our
+[Migrating from Jenkins](../ci/jenkins/index.md) documentation.
+
+From GitLab, you can trigger a Jenkins build when you push code to a repository, or when a merge
+request is created. In return, Jenkins shows the pipeline status on merge requests widgets and
+on the GitLab project's home page.
+
+To better understand GitLab's Jenkins integration, watch the following video:
+
+- [GitLab workflow with Jira issues and Jenkins pipelines](https://youtu.be/Jn-_fyra7xQ)
+Use the Jenkins integration with GitLab when:
+
+- You plan to migrate your CI from Jenkins to [GitLab CI/CD](../ci/README.md) in the future, but
+need an interim solution.
+- You're invested in [Jenkins Plugins](https://plugins.jenkins.io/) and choose to keep using Jenkins
+to build your apps.
For a real use case, read the blog post [Continuous integration: From Jenkins to GitLab using Docker](https://about.gitlab.com/blog/2017/07/27/docker-my-precious/).
-NOTE: **Moving from a traditional CI plug-in to a single application for the entire software development lifecycle can decrease hours spent on maintaining toolchains by 10% or more.**
-Visit the ['GitLab vs. Jenkins' comparison page](https://about.gitlab.com/devops-tools/jenkins-vs-gitlab.html) to learn how our built-in CI compares to Jenkins.
+Moving from a traditional CI plug-in to a single application for the entire software development
+life cycle can decrease hours spent on maintaining toolchains by 10% or more. For more details, see
+the ['GitLab vs. Jenkins' comparison page](https://about.gitlab.com/devops-tools/jenkins-vs-gitlab.html).
-## Requirements
+## Configure GitLab integration with Jenkins
-- [Jenkins GitLab Plugin](https://wiki.jenkins.io/display/JENKINS/GitLab+Plugin)
-- [Jenkins Git Plugin](https://wiki.jenkins.io/display/JENKINS/Git+Plugin)
-- Git clone access for Jenkins from the GitLab repository
-- GitLab API access to report build status
+GitLab's Jenkins integration requires installation and configuration in both GitLab and Jenkins.
+In GitLab, you need to grant Jenkins access to the relevant projects. In Jenkins, you need to
+install and configure several plugins.
-## Configure GitLab users
+### GitLab requirements
-Create a user or choose an existing user that Jenkins will use to interact
-through the GitLab API. This user will need to be a global Admin or added
-as a member to each Group/Project. Developer permission is required for reporting
-build status. This is because a successful build status can trigger a merge
-when 'Merge when pipeline succeeds' feature is used. Some features of the GitLab
-Plugin may require additional privileges. For example, there is an option to
-accept a merge request if the build is successful. Using this feature would
-require developer, maintainer or owner-level permission.
+- [Grant Jenkins permission to GitLab project](#grant-jenkins-access-to-gitlab-project)
+- [Configure GitLab API access](#configure-gitlab-api-access)
+- [Configure the GitLab project](#configure-the-gitlab-project)
-Copy the private API token from **Profile Settings -> Account**. You will need this
-when configuring the Jenkins server later.
+### Jenkins requirements
-## Configure the Jenkins server
+- [Configure the Jenkins server](#configure-the-jenkins-server)
+- [Configure the Jenkins project](#configure-the-jenkins-project)
-Install [Jenkins GitLab Plugin](https://wiki.jenkins.io/display/JENKINS/GitLab+Plugin)
-and [Jenkins Git Plugin](https://wiki.jenkins.io/display/JENKINS/Git+Plugin).
+## Grant Jenkins access to GitLab project
-Go to Manage Jenkins -> Configure System and scroll down to the 'GitLab' section.
-Enter the GitLab server URL in the 'GitLab host URL' field and paste the API token
-copied earlier in the 'API Token' field.
+Grant a GitLab user access to the select GitLab projects.
-For more information, see GitLab Plugin documentation about
-[Jenkins-to-GitLab authentication](https://github.com/jenkinsci/gitlab-plugin#jenkins-to-gitlab-authentication)
+1. Create a new GitLab user, or choose an existing GitLab user.
-![Jenkins GitLab plugin configuration](img/jenkins_gitlab_plugin_config.png)
+ This account will be used by Jenkins to access the GitLab projects. We recommend creating a GitLab
+ user for only this purpose. If you use a person's account, and their account is deactivated or
+ deleted, the GitLab-Jenkins integration will stop working.
-## Configure a Jenkins project
+1. Grant the user permission to the GitLab projects.
-Follow the GitLab Plugin documentation about [Jenkins Job Configuration](https://github.com/jenkinsci/gitlab-plugin#jenkins-job-configuration).
+ If you're integrating Jenkins with many GitLab projects, consider granting the user global
+ Admin permission. Otherwise, add the user to each project, and grant Developer permission.
-NOTE: **Note:**
-Be sure to include the steps about [Build status configuration](https://github.com/jenkinsci/gitlab-plugin#build-status-configuration).
-The 'Publish build status to GitLab' post-build step is required to view
-Jenkins build status in GitLab Merge Requests.
+## Configure GitLab API access
-## Configure a GitLab project
+Create a personal access token to authorize Jenkins' access to GitLab.
-Create a new GitLab project or choose an existing one. Then, go to **Integrations ->
-Jenkins CI**.
+1. Log in to GitLab as the user to be used with Jenkins.
+1. Click your avatar, then **Settings.
+1. Click **Access Tokens** in the sidebar.
+1. Create a personal access token with the **API** scope checkbox checked. For more details, see
+ [Personal access tokens](../user/profile/personal_access_tokens.md).
+1. Record the personal access token's value, because it's required in [Configure the Jenkins server](#configure-the-jenkins-server).
+
+## Configure the Jenkins server
-Check the 'Active' box. Select whether you want GitLab to trigger a build
-on push, Merge Request creation, tag push, or any combination of these. We
-recommend unchecking 'Merge Request events' unless you have a specific use-case
-that requires re-building a commit when a merge request is created. With 'Push
-events' selected, GitLab will build the latest commit on each push and the build
-status will be displayed in the merge request.
+Install and configure the Jenkins plugins. Both plugins must be installed and configured to
+authorize the connection to GitLab.
-Enter the Jenkins URL and Project name. The project name should be URL-friendly
-where spaces are replaced with underscores. To be safe, copy the project name
-from the URL bar of your browser while viewing the Jenkins project.
+1. On the Jenkins server, go to **Manage Jenkins > Manage Plugins**.
+1. Install the [Jenkins GitLab Plugin](https://wiki.jenkins.io/display/JENKINS/GitLab+Plugin).
+1. Go to **Manage Jenkins > Configure System**.
+1. In the **GitLab** section, check the **Enable authentication for ‘/project’ end-point** checkbox.
+1. Click **Add**, then choose **Jenkins Credential Provider**.
+1. Choose **GitLab API token** as the token type.
+1. Enter the GitLab personal access token's value in the **API Token** field and click **Add**.
+1. Enter the GitLab server's URL in the **GitLab host URL** field.
+1. Click **Test Connection**, ensuring the connection is successful before proceeding.
-Optionally, enter a username and password if your Jenkins server requires
-authentication.
+For more information, see GitLab Plugin documentation about
+[Jenkins-to-GitLab authentication](https://github.com/jenkinsci/gitlab-plugin#jenkins-to-gitlab-authentication).
+
+![Jenkins GitLab plugin configuration](img/jenkins_gitlab_plugin_config.png)
-![GitLab service settings](img/jenkins_gitlab_service_settings.png)
+## Configure the Jenkins project
+
+Set up the Jenkins project you’re going to run your build on.
+
+1. On your Jenkins instance, go to **New Item**.
+1. Enter the project's name.
+1. Choose between **Freestyle** or **Pipeline** and click **OK**.
+ We recommend a Freestyle project, because the Jenkins plugin will update the build status on
+ GitLab. In a Pipeline project, you must configure a script to update the status on GitLab.
+1. Choose your GitLab connection from the dropdown.
+1. Check the **Build when a change is pushed to GitLab** checkbox.
+1. Check the following checkboxes:
+ - **Accepted Merge Request Events**
+ - **Closed Merge Request Events**
+1. Specify how build status is reported to GitLab:
+ - If you created a **Freestyle** project, in the **Post-build Actions** section, choose
+ **Publish build status to GitLab**.
+ - If you created a **Pipeline** project, you must use a Jenkins Pipeline script to update the status on
+ GitLab.
+
+ Example Jenkins Pipeline script:
+
+ ```groovy
+ pipeline {
+ agent any
+
+ stages {
+ stage('gitlab') {
+ steps {
+ echo 'Notify GitLab'
+ updateGitlabCommitStatus name: 'build', state: 'pending'
+ updateGitlabCommitStatus name: 'build', state: 'success'
+ }
+ }
+ }
+ }
+ ```
+
+## Configure the GitLab project
+
+Configure the GitLab integration with Jenkins.
+
+1. Create a new GitLab project or choose an existing one.
+1. Go to **Settings > Integrations**, then select **Jenkins CI**.
+1. Turn on the **Active** toggle.
+1. Select the events you want GitLab to trigger a Jenkins build for:
+ - Push
+ - Merge request
+ - Tag push
+1. Enter the **Jenkins URL**.
+1. Enter the **Project name**.
+
+ The project name should be URL-friendly, where spaces are replaced with underscores. To ensure
+ the project name is valid, copy it from your browser's address bar while viewing the Jenkins
+ project.
+1. Enter the **Username** and **Password** if your Jenkins server requires
+ authentication.
+1. Click **Test settings and save changes**. GitLab tests the connection to Jenkins.
## Plugin functional overview
GitLab does not contain a database table listing commits. Commits are always
-read from the repository directly. Therefore, it is not possible to retain the
+read from the repository directly. Therefore, it's not possible to retain the
build status of a commit in GitLab. This is overcome by requesting build
information from the integrated CI tool. The CI tool is responsible for creating
and storing build status for Commits and Merge Requests.
### Steps required to implement a similar integration
->**Note:**
+**Note:**
All steps are implemented using AJAX requests on the merge request page.
1. In order to display the build status in a merge request you must create a project service in GitLab.
@@ -133,7 +177,7 @@ receive a build status update via the API. Either Jenkins was not properly
configured or there was an error reporting the status via the API.
1. [Configure the Jenkins server](#configure-the-jenkins-server) for GitLab API access
-1. [Configure a Jenkins project](#configure-a-jenkins-project), including the
+1. [Configure the Jenkins project](#configure-the-jenkins-project), including the
'Publish build status to GitLab' post-build action.
### Merge Request event does not trigger a Jenkins Pipeline
diff --git a/doc/integration/jenkins_deprecated.md b/doc/integration/jenkins_deprecated.md
index af7f847f803..31bb271782a 100644
--- a/doc/integration/jenkins_deprecated.md
+++ b/doc/integration/jenkins_deprecated.md
@@ -6,6 +6,9 @@ was deprecated in favor of the
[GitLab Plugin](https://wiki.jenkins.io/display/JENKINS/GitLab+Plugin).
Please use documentation for the new [Jenkins CI service](jenkins.md).
+NOTE: **Note:**
+This service was [removed in v13.0](https://gitlab.com/gitlab-org/gitlab/-/issues/1600)
+
Integration includes:
- Trigger Jenkins build after push to repo
diff --git a/doc/integration/jira_development_panel.md b/doc/integration/jira_development_panel.md
index 7753629c371..e9b1c9676bd 100644
--- a/doc/integration/jira_development_panel.md
+++ b/doc/integration/jira_development_panel.md
@@ -56,13 +56,16 @@ There are no special requirements if you are using GitLab.com.
this would be `https://gitlab.com/login/oauth/callback`.
NOTE: **Note**:
- If using a GitLab version earlier than 11.3 the `Redirect URI` value should be `https://<your-gitlab-instance-domain>/-/jira/login/oauth/callback`.
+ If using a GitLab version earlier than 11.3, the `Redirect URI` must be
+ `https://<your-gitlab-instance-domain>/-/jira/login/oauth/callback`. If you want Jira
+ to have access to all projects, GitLab recommends an administrator creates the
+ Application.
![GitLab Application setup](img/jira_dev_panel_gl_setup_1.png)
- Check `api` in the Scopes section.
-1. Click `Save application`. You will see the generated 'Application Id' and 'Secret' values.
+1. Click `Save application`. You will see the generated 'Application ID' and 'Secret' values.
Copy these values that you will use on the Jira configuration side.
## Jira Configuration
@@ -164,7 +167,7 @@ Click the links to see your GitLab repository data.
## Limitations
-- This integration is currently not supported on GitLab instances under a [relative url](https://docs.gitlab.com/omnibus/settings/configuration.html#configuring-a-relative-url-for-gitlab) (for example, `http://example.com/gitlab`).
+- This integration is currently not supported on GitLab instances under a [relative URL](https://docs.gitlab.com/omnibus/settings/configuration.html#configuring-a-relative-url-for-gitlab) (for example, `http://example.com/gitlab`).
## Changelog
diff --git a/doc/integration/oauth_provider.md b/doc/integration/oauth_provider.md
index 6c9b272f35b..fd1c21d725d 100644
--- a/doc/integration/oauth_provider.md
+++ b/doc/integration/oauth_provider.md
@@ -5,6 +5,8 @@ to sign in to other services.
If you want to use:
+- The [OAuth2](https://oauth.net/2/) protocol to access GitLab resources on user's behalf,
+ see [OAuth2 provider](../api/oauth2.md)
- Other OAuth authentication service providers to sign in to
GitLab, see the [OAuth2 client documentation](omniauth.md).
- The related API, see [Applications API](../api/applications.md).
diff --git a/doc/integration/omniauth.md b/doc/integration/omniauth.md
index 5634ad95cf7..2afdeccb764 100644
--- a/doc/integration/omniauth.md
+++ b/doc/integration/omniauth.md
@@ -51,7 +51,7 @@ that are in common for all providers that we need to consider.
automatically create an account. It defaults to `false`. If `false` users must
be created manually or they will not be able to sign in via OmniAuth.
- `auto_link_ldap_user` can be used if you have [LDAP / ActiveDirectory](ldap.md)
- integration enabled. It defaults to false. When enabled, users automatically
+ integration enabled. It defaults to `false`. When enabled, users automatically
created through an OmniAuth provider will have their LDAP identity created in GitLab as well.
- `block_auto_created_users` defaults to `true`. If `true` auto created users will
be blocked by default and will have to be unblocked by an administrator before
diff --git a/doc/integration/recaptcha.md b/doc/integration/recaptcha.md
index 4f3140e4cb9..bf2e6568ea6 100644
--- a/doc/integration/recaptcha.md
+++ b/doc/integration/recaptcha.md
@@ -27,7 +27,7 @@ configuration variable:
proxy_set_header X-GitLab-Show-Login-Captcha 1;
```
-In GitLab Omnibus, this can be configured via `/etc/gitlab/gitlab.rb`:
+In Omnibus GitLab, this can be configured via `/etc/gitlab/gitlab.rb`:
```ruby
nginx['proxy_set_headers'] = { 'X-GitLab-Show-Login-Captcha' => 1 }
diff --git a/doc/integration/slash_commands.md b/doc/integration/slash_commands.md
index 11abf3ca203..c73db32a42a 100644
--- a/doc/integration/slash_commands.md
+++ b/doc/integration/slash_commands.md
@@ -14,11 +14,11 @@ Taking the trigger term as `project-name`, the commands are:
| ------- | ------ |
| `/project-name help` | Shows all available slash commands |
| `/project-name issue new <title> <shift+return> <description>` | Creates a new issue with title `<title>` and description `<description>` |
-| `/project-name issue show <id>` | Shows the issue with id `<id>` |
-| `/project-name issue close <id>` | Closes the issue with id `<id>` |
+| `/project-name issue show <id>` | Shows the issue with ID `<id>` |
+| `/project-name issue close <id>` | Closes the issue with ID `<id>` |
| `/project-name issue search <query>` | Shows up to 5 issues matching `<query>` |
| `/project-name issue move <id> to <project>` | Moves issue ID `<id>` to `<project>` |
-| `/project-name issue comment <id> <shift+return> <comment>` | Adds a new comment to an issue with id `<id>` and comment body `<comment>` |
+| `/project-name issue comment <id> <shift+return> <comment>` | Adds a new comment to an issue with ID `<id>` and comment body `<comment>` |
| `/project-name deploy <from> to <to>` | Deploy from the `<from>` environment to the `<to>` environment |
| `/project-name run <job name> <arguments>` | Execute [ChatOps](../ci/chatops/README.md) job `<job name>` on `master` |
diff --git a/doc/integration/sourcegraph.md b/doc/integration/sourcegraph.md
index 5da9dd1fbc9..90e9c7b9534 100644
--- a/doc/integration/sourcegraph.md
+++ b/doc/integration/sourcegraph.md
@@ -107,7 +107,7 @@ When visiting one of these views, you can now hover over a code reference to see
## Sourcegraph for GitLab.com
-Sourcegraph powered code intelligence is avaialable for all public projects on GitLab.com.
+Sourcegraph powered code intelligence is available for all public projects on GitLab.com.
Support for private projects is currently not available for GitLab.com;
follow the epic [&2201](https://gitlab.com/groups/gitlab-org/-/epics/2201)
@@ -117,7 +117,7 @@ for updates.
### Sourcegraph isn't working
-If you enabled Sourcegraph for your project but still it doesn't looklike it's working, it might be because Sourcegraph has not indexed theproject yet. You can check for Sourcegraph's availability of your project by visiting `https://sourcegraph.com/gitlab.com/<project-path>`replacing `<project-path>` with the path to your GitLab project.
+If you enabled Sourcegraph for your project but still it doesn't look like it's working, it might be because Sourcegraph has not indexed the project yet. You can check for Sourcegraph's availability of your project by visiting `https://sourcegraph.com/gitlab.com/<project-path>`replacing `<project-path>` with the path to your GitLab project.
## Sourcegraph and Privacy
diff --git a/doc/integration/vault.md b/doc/integration/vault.md
index c29df9a24dc..0a6ced42925 100644
--- a/doc/integration/vault.md
+++ b/doc/integration/vault.md
@@ -1,4 +1,7 @@
---
+stage: Release
+group: Release Management
+info: To determine the technical writer assigned to the Stage/Group associated with this page, see https://about.gitlab.com/handbook/engineering/ux/technical-writing/#designated-technical-writers
type: reference, howto
---
diff --git a/doc/monitoring/performance/influxdb_configuration.md b/doc/monitoring/performance/influxdb_configuration.md
index ae5f4c7e9df..0bcb18bdfe7 100644
--- a/doc/monitoring/performance/influxdb_configuration.md
+++ b/doc/monitoring/performance/influxdb_configuration.md
@@ -1,5 +1,5 @@
---
-redirect_to: '../../administration/monitoring/performance/influxdb_configuration.md'
+redirect_to: '../../administration/monitoring/performance/prometheus.md'
---
-This document was moved to [administration/monitoring/performance/influxdb_configuration](../../administration/monitoring/performance/influxdb_configuration.md).
+Support for InfluxDB was removed in GitLab 13.0. Use [Prometheus](../../administration/monitoring/performance/prometheus.md) for performance monitoring.
diff --git a/doc/monitoring/performance/influxdb_schema.md b/doc/monitoring/performance/influxdb_schema.md
index 57fb74cb6cd..0bcb18bdfe7 100644
--- a/doc/monitoring/performance/influxdb_schema.md
+++ b/doc/monitoring/performance/influxdb_schema.md
@@ -1,5 +1,5 @@
---
-redirect_to: '../../administration/monitoring/performance/influxdb_schema.md'
+redirect_to: '../../administration/monitoring/performance/prometheus.md'
---
-This document was moved to [administration/monitoring/performance/influxdb_schema](../../administration/monitoring/performance/influxdb_schema.md).
+Support for InfluxDB was removed in GitLab 13.0. Use [Prometheus](../../administration/monitoring/performance/prometheus.md) for performance monitoring.
diff --git a/doc/push_rules/push_rules.md b/doc/push_rules/push_rules.md
index eca1f8c24a4..5685e848a33 100644
--- a/doc/push_rules/push_rules.md
+++ b/doc/push_rules/push_rules.md
@@ -9,11 +9,11 @@ regular expressions to reject pushes based on commit contents, branch names or f
## Overview
-GitLab already offers [protected branches][protected-branches], but there are
+GitLab already offers [protected branches](../user/project/protected_branches.md), but there are
cases when you need some specific rules like preventing Git tag removal or
enforcing a special format for commit messages.
-Push rules are essentially [pre-receive Git hooks][hooks] that are easy to
+Push rules are essentially [pre-receive Git hooks](https://git-scm.com/book/en/v2/Customizing-Git-Git-Hooks) that are easy to
enable in a user-friendly interface. They are defined globally if you are an
admin or per project so you can have different rules applied to different
projects depending on your needs.
@@ -43,11 +43,18 @@ you want the branches to start with a certain name because you have different
GitLab CI/CD jobs (`feature`, `hotfix`, `docker`, `android`, etc.) that rely on the
branch name.
-Your developers however, don't always remember that policy, so they push
-various branches and CI pipelines do not work as expected. By restricting the
-branch names globally in Push Rules, you can now sleep without the anxiety
-of your developers' mistakes. Every branch that doesn't match your push rule
-will get rejected.
+Your developers, however, don't always remember that policy, so they might push to
+various branches, and CI pipelines might not work as expected. By restricting the
+branch names globally in Push Rules, such mistakes are prevented.
+Any branch name that doesn't match your push rule will get rejected.
+
+Note that the name of your default branch is always allowed, regardless of the branch naming
+regular expression (regex) specified. GitLab is configured this way
+because merges typically have the default branch as their target.
+If you have other target branches, include them in your regex. (See [Enabling push rules](#enabling-push-rules)).
+
+The default branch also defaults to being a [protected branch](../user/project/protected_branches.md),
+which already limits users from pushing directly.
### Custom Push Rules **(CORE ONLY)**
@@ -61,7 +68,7 @@ See [server hooks](../administration/server_hooks.md) for more information.
NOTE: **Note:**
GitLab administrators can set push rules globally under
**Admin Area > Push Rules** that all new projects will inherit. You can later
-override them in a project's settings.
+override them in a project's settings. They can be also set on a [group level](../user/group/index.md#group-push-rules-starter).
1. Navigate to your project's **Settings > Repository** and expand **Push Rules**
1. Set the rule you want
@@ -74,7 +81,7 @@ The following options are available.
| Removal of tags with `git push` | **Starter** 7.10 | Forbid users to remove Git tags with `git push`. Tags will still be able to be deleted through the web UI. |
| Check whether author is a GitLab user | **Starter** 7.10 | Restrict commits by author (email) to existing GitLab users. |
| Committer restriction | **Premium** 10.2 | GitLab will reject any commit that was not committed by the current authenticated user |
-| Check whether commit is signed through GPG | **Premium** 10.1 | Reject commit when it is not signed through GPG. Read [signing commits with GPG][signing-commits]. |
+| Check whether commit is signed through GPG | **Premium** 10.1 | Reject commit when it is not signed through GPG. Read [signing commits with GPG](../user/project/repository/gpg_signed_commits/index.md). |
| Prevent committing secrets to Git | **Starter** 8.12 | GitLab will reject any files that are likely to contain secrets. Read [what files are forbidden](#prevent-pushing-secrets-to-the-repository). |
| Restrict by commit message | **Starter** 7.10 | Only commit messages that match this regular expression are allowed to be pushed. Leave empty to allow any commit message. Uses multiline mode, which can be disabled using `(?-m)`. |
| Restrict by commit message (negative match)| **Starter** 11.1 | Only commit messages that do not match this regular expression are allowed to be pushed. Leave empty to allow any commit message. Uses multiline mode, which can be disabled using `(?-m)`. |
@@ -88,14 +95,15 @@ GitLab uses [RE2 syntax](https://github.com/google/re2/wiki/Syntax) for regular
## Prevent pushing secrets to the repository
-> [Introduced][ee-385] in [GitLab Starter][ee] 8.12.
+> [Introduced](https://gitlab.com/gitlab-org/gitlab/issues/385) in [GitLab Starter](https://about.gitlab.com/pricing/) 8.12.
-You can turn on a predefined blacklist of files which won't be allowed to be
-pushed to a repository.
+Secrets such as credential files, SSH private keys, and other files containing secrets should never be committed to source control.
+GitLab allows you to turn on a predefined blacklist of files which won't be allowed to be
+pushed to a repository, stopping those commits from reaching the remote repository.
By selecting the checkbox *Prevent committing secrets to Git*, GitLab prevents
pushes to the repository when a file matches a regular expression as read from
-[`files_blacklist.yml`][list] (make sure you are at the right branch
+[`files_blacklist.yml`](https://gitlab.com/gitlab-org/gitlab/blob/master/ee/lib/gitlab/checks/files_blacklist.yml) (make sure you are at the right branch
as your GitLab version when viewing this file).
NOTE: **Note:**
@@ -171,10 +179,3 @@ questions that you know someone might ask.
Each scenario can be a third-level heading, e.g. `### Getting error message X`.
If you have none to add when creating a doc, leave this section in place
but commented out to help encourage others to add to it in the future. -->
-
-[protected-branches]: ../user/project/protected_branches.md
-[signing-commits]: ../user/project/repository/gpg_signed_commits/index.md
-[ee-385]: https://gitlab.com/gitlab-org/gitlab/issues/385
-[list]: https://gitlab.com/gitlab-org/gitlab/blob/master/ee/lib/gitlab/checks/files_blacklist.yml
-[hooks]: https://git-scm.com/book/en/v2/Customizing-Git-Git-Hooks
-[ee]: https://about.gitlab.com/pricing/
diff --git a/doc/raketasks/README.md b/doc/raketasks/README.md
index 1670849016c..4bba4f2bc5a 100644
--- a/doc/raketasks/README.md
+++ b/doc/raketasks/README.md
@@ -15,25 +15,27 @@ GitLab Rake tasks are performed using:
The following are available Rake tasks:
-| Tasks | Description |
-|:-------------------------------------------------------------------------------------------|:------------------------------------------------------------------------------------------|
-| [Back up and restore](backup_restore.md) | Back up, restore, and migrate GitLab instances between servers. |
-| [Clean up](cleanup.md) | Clean up unneeded items GitLab instances. |
-| [Development](../development/rake_tasks.md) | Tasks for GitLab contributors. |
-| [Elasticsearch](../integration/elasticsearch.md#gitlab-elasticsearch-rake-tasks) | Maintain Elasticsearch in a GitLab instance. |
-| [Enable namespaces](features.md) | Enable usernames and namespaces for user projects. |
-| [General maintenance](../administration/raketasks/maintenance.md) | General maintenance and self-check tasks. |
-| [Geo maintenance](../administration/raketasks/geo.md) **(PREMIUM ONLY)** | [Geo](../administration/geo/replication/index.md)-related maintenance. |
-| [GitHub import](../administration/raketasks/github_import.md) | Retrieve and import repositories from GitHub. |
-| [Import repositories](import.md) | Import bare repositories into your GitLab instance. |
-| [Import large project exports](../development/import_project.md#importing-via-a-rake-task) | Import large GitLab [project exports](../user/project/settings/import_export.md). |
-| [Integrity checks](../administration/raketasks/check.md) | Check the integrity of repositories, files, and LDAP. |
-| [LDAP maintenance](../administration/raketasks/ldap.md) | [LDAP](../administration/auth/ldap.md)-related tasks. |
-| [List repositories](list_repos.md) | List of all GitLab-managed Git repositories on disk. |
-| [Project import/export](../administration/raketasks/project_import_export.md) | Prepare for [project exports and imports](../user/project/settings/import_export.md). |
-| [Repository storage](../administration/raketasks/storage.md) | List and migrate existing projects and attachments from legacy storage to hashed storage. |
-| [Uploads migrate](../administration/raketasks/uploads/migrate.md) | Migrate uploads between storage local and object storage. |
-| [Uploads sanitize](../administration/raketasks/uploads/sanitize.md) | Remove EXIF data from images uploaded to earlier versions of GitLab. |
-| [User management](user_management.md) | Perform user management tasks. |
-| [Webhooks administration](web_hooks.md) | Maintain project Webhooks. |
-| [X509 signatures](x509_signatures.md) | Update x509 commit signatures, useful if certificate store has changed. |
+| Tasks | Description |
+|:----------------------------------------------------------------------------------------------------|:------------------------------------------------------------------------------------------|
+| [Back up and restore](backup_restore.md) | Back up, restore, and migrate GitLab instances between servers. |
+| [Clean up](cleanup.md) | Clean up unneeded items from GitLab instances. |
+| [Development](../development/rake_tasks.md) | Tasks for GitLab contributors. |
+| [Elasticsearch](../integration/elasticsearch.md#gitlab-elasticsearch-rake-tasks) **(STARTER ONLY)** | Maintain Elasticsearch in a GitLab instance. |
+| [Enable namespaces](features.md) | Enable usernames and namespaces for user projects. |
+| [General maintenance](../administration/raketasks/maintenance.md) | General maintenance and self-check tasks. |
+| [Geo maintenance](../administration/raketasks/geo.md) **(PREMIUM ONLY)** | [Geo](../administration/geo/replication/index.md)-related maintenance. |
+| [GitHub import](../administration/raketasks/github_import.md) | Retrieve and import repositories from GitHub. |
+| [Import repositories](import.md) | Import bare repositories into your GitLab instance. |
+| [Import large project exports](../development/import_project.md#importing-via-a-rake-task) | Import large GitLab [project exports](../user/project/settings/import_export.md). |
+| [Integrity checks](../administration/raketasks/check.md) | Check the integrity of repositories, files, and LDAP. |
+| [LDAP maintenance](../administration/raketasks/ldap.md) | [LDAP](../administration/auth/ldap.md)-related tasks. |
+| [List repositories](list_repos.md) | List of all GitLab-managed Git repositories on disk. |
+| [Project import/export](../administration/raketasks/project_import_export.md) | Prepare for [project exports and imports](../user/project/settings/import_export.md). |
+| [Sample Prometheus data](generate_sample_prometheus_data.md) | Generate sample Prometheus data. |
+| [Repository storage](../administration/raketasks/storage.md) | List and migrate existing projects and attachments from legacy storage to hashed storage. |
+| [Uploads migrate](../administration/raketasks/uploads/migrate.md) | Migrate uploads between storage local and object storage. |
+| [Uploads sanitize](../administration/raketasks/uploads/sanitize.md) | Remove EXIF data from images uploaded to earlier versions of GitLab. |
+| [User management](user_management.md) | Perform user management tasks. |
+| [Webhooks administration](web_hooks.md) | Maintain project Webhooks. |
+| [X.509 signatures](x509_signatures.md) | Update X.509 commit signatures, useful if certificate store has changed. |
+| [Migrate Snippets to Git](migrate_snippets.md) | Migrate GitLab Snippets to Git repositories and show migration status |
diff --git a/doc/raketasks/backup_hrz.png b/doc/raketasks/backup_hrz.png
deleted file mode 100644
index 32690b2904c..00000000000
--- a/doc/raketasks/backup_hrz.png
+++ /dev/null
Binary files differ
diff --git a/doc/raketasks/backup_restore.md b/doc/raketasks/backup_restore.md
index 24e4ed43543..3c81bab644e 100644
--- a/doc/raketasks/backup_restore.md
+++ b/doc/raketasks/backup_restore.md
@@ -1,6 +1,6 @@
-# Backing up and restoring GitLab
+# Backing up and restoring GitLab **(CORE ONLY)**
-![backup banner](backup_hrz.png)
+GitLab provides Rake tasks for backing up and restoring GitLab instances.
An application data backup creates an archive file that contains the database,
all repositories and all attachments.
@@ -14,31 +14,26 @@ from one server to another is through backup restore.
In order to be able to backup and restore, you need two essential tools
installed on your system.
-### Rsync
+- **Rsync**: If you installed GitLab:
+ - Using the Omnibus package, you're all set.
+ - From source, make sure `rsync` is installed. For example:
-If you installed GitLab:
+ ```shell
+ # Debian/Ubuntu
+ sudo apt-get install rsync
-- Using the Omnibus package, you're all set.
-- From source, make sure `rsync` is installed:
+ # RHEL/CentOS
+ sudo yum install rsync
+ ```
- ```shell
- # Debian/Ubuntu
- sudo apt-get install rsync
+- **Tar**: Backup and restore tasks use `tar` under the hood to create and extract
+ archives. Ensure you have version 1.30 or above of `tar` available in your
+ system. To check the version, run:
- # RHEL/CentOS
- sudo yum install rsync
+ ```shell
+ tar --version
```
-### Tar
-
-Backup and restore tasks use `tar` under the hood to create and extract
-archives. Ensure you have version 1.30 or above of `tar` available in your
-system. To check the version, run:
-
-```shell
-tar --version
-```
-
## Backup timestamp
NOTE: **Note:**
@@ -56,9 +51,9 @@ available.
For example, if the backup name is `1493107454_2018_04_25_10.6.4-ce_gitlab_backup.tar`,
then the timestamp is `1493107454_2018_04_25_10.6.4-ce`.
-## Creating a backup of the GitLab system
+## Back up GitLab
-GitLab provides a simple command line interface to backup your whole instance.
+GitLab provides a simple command line interface to back up your whole instance.
It backs up your:
- Database
@@ -142,12 +137,12 @@ Deleting tmp directories...[DONE]
Deleting old backups... [SKIPPING]
```
-## Storing configuration files
+### Storing configuration files
-A backup performed by the [raketask GitLab provides](#creating-a-backup-of-the-gitlab-system)
+The [backup Rake task](#back-up-gitlab) GitLab provides
does **not** store your configuration files. The primary reason for this is that your
database contains encrypted information for two-factor authentication, the CI/CD
-'secure variables', etc. Storing encrypted information along with its key in the
+'secure variables', and so on. Storing encrypted information along with its key in the
same place defeats the purpose of using encryption in the first place.
CAUTION: **Warning:**
@@ -182,11 +177,11 @@ If you use Omnibus GitLab, see some additional information
In the unlikely event that the secrets file is lost, see the
[troubleshooting section](#when-the-secrets-file-is-lost).
-## Backup options
+### Backup options
The command line tool GitLab provides to backup your instance can take more options.
-### Backup strategy option
+#### Backup strategy option
> [Introduced](https://gitlab.com/gitlab-org/gitlab-foss/-/merge_requests/8728) in GitLab 8.17.
@@ -214,7 +209,7 @@ sudo gitlab-backup create STRATEGY=copy
NOTE: **Note**
For GitLab 12.1 and earlier, use `gitlab-rake gitlab:backup:create`.
-### Backup filename
+#### Backup filename
CAUTION: **Warning:**
If you use a custom backup filename, you will not be able to
@@ -231,7 +226,7 @@ For GitLab 12.1 and earlier, use `gitlab-rake gitlab:backup:create`.
The resulting file will then be `dump_gitlab_backup.tar`. This is useful for systems that make use of rsync and incremental backups, and will result in considerably faster transfer speeds.
-### Rsyncable
+#### Rsyncable
To make sure the generated archive is intelligently transferable by rsync, the `GZIP_RSYNCABLE=yes` option can be set. This will set the `--rsyncable` option to `gzip`. This is only useful in combination with setting [the Backup filename option](#backup-filename).
@@ -244,7 +239,7 @@ sudo gitlab-backup create BACKUP=dump GZIP_RSYNCABLE=yes
NOTE: **Note**
For GitLab 12.1 and earlier, use `gitlab-rake gitlab:backup:create`.
-### Excluding specific directories from the backup
+#### Excluding specific directories from the backup
You can choose what should be exempt from the backup up by adding the environment variable `SKIP`.
The available options are:
@@ -278,7 +273,7 @@ For installations from source:
sudo -u git -H bundle exec rake gitlab:backup:create SKIP=db,uploads RAILS_ENV=production
```
-### Skipping tar creation
+#### Skipping tar creation
The last part of creating a backup is generation of a `.tar` file containing
all the parts. In some cases (for example, if the backup is picked up by other
@@ -303,19 +298,19 @@ For installations from source:
sudo -u git -H bundle exec rake gitlab:backup:create SKIP=tar RAILS_ENV=production
```
-### Uploading backups to a remote (cloud) storage
+#### Uploading backups to a remote (cloud) storage
-Starting with GitLab 7.4 you can let the backup script upload the '.tar' file it creates.
+Starting with GitLab 7.4 you can let the backup script upload the `.tar` file it creates.
It uses the [Fog library](http://fog.io/) to perform the upload.
In the example below we use Amazon S3 for storage, but Fog also lets you use
[other storage providers](http://fog.io/storage/). GitLab
[imports cloud drivers](https://gitlab.com/gitlab-org/gitlab/blob/30f5b9a5b711b46f1065baf755e413ceced5646b/Gemfile#L88)
-for AWS, Google, OpenStack Swift, Rackspace and Aliyun as well. A local driver is
+for AWS, Google, OpenStack Swift, Rackspace, and Aliyun as well. A local driver is
[also available](#uploading-to-locally-mounted-shares).
[Read more about using object storage with GitLab](../administration/object_storage.md).
-#### Using Amazon S3
+##### Using Amazon S3
For Omnibus GitLab packages:
@@ -333,9 +328,9 @@ For Omnibus GitLab packages:
gitlab_rails['backup_upload_remote_directory'] = 'my.s3.bucket'
```
-1. [Reconfigure GitLab] for the changes to take effect
+1. [Reconfigure GitLab](../administration/restart_gitlab.md#omnibus-gitlab-reconfigure) for the changes to take effect
-#### Digital Ocean Spaces
+##### Digital Ocean Spaces
This example can be used for a bucket in Amsterdam (AMS3).
@@ -352,7 +347,7 @@ This example can be used for a bucket in Amsterdam (AMS3).
gitlab_rails['backup_upload_remote_directory'] = 'my.s3.bucket'
```
-1. [Reconfigure GitLab] for the changes to take effect
+1. [Reconfigure GitLab](../administration/restart_gitlab.md#omnibus-gitlab-reconfigure) for the changes to take effect
NOTE: **Note:**
If you see `400 Bad Request` by using Digital Ocean Spaces, the cause may be the
@@ -360,7 +355,7 @@ usage of backup encryption. Remove or comment the line that
contains `gitlab_rails['backup_encryption']` since Digital Ocean Spaces
doesn't support encryption.
-#### Other S3 Providers
+##### Other S3 Providers
Not all S3 providers are fully-compatible with the Fog library. For example,
if you see `411 Length Required` errors after attempting to upload, you may
@@ -397,7 +392,7 @@ For installations from source:
# storage_class: 'STANDARD'
```
-1. [Restart GitLab] for the changes to take effect
+1. [Restart GitLab](../administration/restart_gitlab.md#installations-from-source) for the changes to take effect
If you are uploading your backups to S3 you will probably want to create a new
IAM user with restricted access rights. To give the upload user access only for
@@ -450,7 +445,7 @@ with the name of your bucket:
}
```
-#### Using Google Cloud Storage
+##### Using Google Cloud Storage
If you want to use Google Cloud Storage to save backups, you'll have to create
an access key from the Google console first:
@@ -476,7 +471,7 @@ For Omnibus GitLab packages:
gitlab_rails['backup_upload_remote_directory'] = 'my.google.bucket'
```
-1. [Reconfigure GitLab] for the changes to take effect
+1. [Reconfigure GitLab](../administration/restart_gitlab.md#omnibus-gitlab-reconfigure) for the changes to take effect
For installations from source:
@@ -492,9 +487,9 @@ For installations from source:
remote_directory: 'my.google.bucket'
```
-1. [Restart GitLab] for the changes to take effect
+1. [Restart GitLab](../administration/restart_gitlab.md#installations-from-source) for the changes to take effect
-#### Specifying a custom directory for backups
+##### Specifying a custom directory for backups
Note: This option only works for remote storage. If you want to group your backups
you can pass a `DIRECTORY` environment variable:
@@ -507,9 +502,9 @@ sudo gitlab-backup create DIRECTORY=weekly
NOTE: **Note**
For GitLab 12.1 and earlier, use `gitlab-rake gitlab:backup:create`.
-### Uploading to locally mounted shares
+#### Uploading to locally mounted shares
-You may also send backups to a mounted share (`NFS` / `CIFS` / `SMB` / etc.) by
+You may also send backups to a mounted share (for example, `NFS`,`CIFS`, or `SMB`) by
using the Fog [`Local`](https://github.com/fog/fog-local#usage) storage provider.
The directory pointed to by the `local_root` key **must** be owned by the `git`
user **when mounted** (mounting with the `uid=` of the `git` user for `CIFS` and
@@ -539,7 +534,7 @@ For Omnibus GitLab packages:
gitlab_rails['backup_upload_remote_directory'] = 'gitlab_backups'
```
-1. [Reconfigure GitLab] for the changes to take effect.
+1. [Reconfigure GitLab](../administration/restart_gitlab.md#omnibus-gitlab-reconfigure) for the changes to take effect.
For installations from source:
@@ -557,9 +552,9 @@ For installations from source:
remote_directory: 'gitlab_backups'
```
-1. [Restart GitLab] for the changes to take effect.
+1. [Restart GitLab](../administration/restart_gitlab.md#installations-from-source) for the changes to take effect.
-### Backup archive permissions
+#### Backup archive permissions
The backup archives created by GitLab (`1393513186_2014_02_27_gitlab_backup.tar`)
will have owner/group `git`/`git` and 0600 permissions by default.
@@ -574,7 +569,7 @@ For Omnibus GitLab packages:
gitlab_rails['backup_archive_permissions'] = 0644 # Makes the backup archives world-readable
```
-1. [Reconfigure GitLab] for the changes to take effect.
+1. [Reconfigure GitLab](../administration/restart_gitlab.md#omnibus-gitlab-reconfigure) for the changes to take effect.
For installations from source:
@@ -585,9 +580,9 @@ For installations from source:
archive_permissions: 0644 # Makes the backup archives world-readable
```
-1. [Restart GitLab] for the changes to take effect.
+1. [Restart GitLab](../administration/restart_gitlab.md#installations-from-source) for the changes to take effect.
-### Configuring cron to make daily backups
+#### Configuring cron to make daily backups
CAUTION: **Warning:**
The following cron jobs do not [backup your GitLab configuration files](#storing-configuration-files)
@@ -669,9 +664,9 @@ For installations from source:
keep_time: 604800
```
-1. [Restart GitLab] for the changes to take effect.
+1. [Restart GitLab](../administration/restart_gitlab.md#installations-from-source) for the changes to take effect.
-## Restore
+## Restore GitLab
GitLab provides a simple command line interface to restore your whole installation,
and is flexible enough to fit your needs.
@@ -692,7 +687,7 @@ before restoring the backup.
You need to have a working GitLab installation before you can perform
a restore. This is mainly because the system user performing the
restore actions (`git`) is usually not allowed to create or delete
-the SQL database it needs to import data into ('gitlabhq_production').
+the SQL database it needs to import data into (`gitlabhq_production`).
All existing data will be either erased (SQL) or moved to a separate
directory (repositories, uploads).
@@ -718,7 +713,7 @@ more of the following options:
Read what the [backup timestamp is about](#backup-timestamp).
- `force=yes` - Does not ask if the authorized_keys file should get regenerated and assumes 'yes' for warning that database tables will be removed, enabling the "Write to authorized_keys file" setting, and updating LDAP providers.
-If you are restoring into directories that are mountpoints you will need to make
+If you are restoring into directories that are mount points, you will need to make
sure these directories are empty before attempting a restore. Otherwise GitLab
will attempt to move these directories before restoring the new data and this
would cause an error.
@@ -902,6 +897,8 @@ will have all your repositories, but not any other data.
## Troubleshooting
+The following are possible problems you might encounter with possible solutions.
+
### Restoring database backup using Omnibus packages outputs warnings
If you are using backup restore procedures you might encounter the following warnings:
@@ -1079,12 +1076,9 @@ If you have changed the default filesystem location for the registry, you will
want to run the `chown` against your custom location instead of
`/var/opt/gitlab/gitlab-rails/shared/registry/docker`.
-[reconfigure GitLab]: ../administration/restart_gitlab.md#omnibus-gitlab-reconfigure
-[restart GitLab]: ../administration/restart_gitlab.md#installations-from-source
-
### Backup fails to complete with Gzip error
-While running the backup, you may receive a gzip error:
+While running the backup, you may receive a Gzip error:
```shell
sudo /opt/gitlab/bin/gitlab-backup create
@@ -1098,5 +1092,5 @@ Backup failed
If this happens, check the following:
-1. Confirm there is sufficient disk space for the gzip operation.
+1. Confirm there is sufficient disk space for the Gzip operation.
1. If NFS is being used, check if the mount option `timeout` is set. The default is `600`, and changing this to smaller values have resulted in this error.
diff --git a/doc/raketasks/cleanup.md b/doc/raketasks/cleanup.md
index ef69b1cce15..7908af8da84 100644
--- a/doc/raketasks/cleanup.md
+++ b/doc/raketasks/cleanup.md
@@ -1,4 +1,4 @@
-# Clean up
+# Clean up **(CORE ONLY)**
GitLab provides Rake tasks for cleaning up GitLab instances.
@@ -7,9 +7,11 @@ GitLab provides Rake tasks for cleaning up GitLab instances.
> [Introduced](https://gitlab.com/gitlab-org/gitlab/issues/36628) in GitLab 12.10.
DANGER: **Danger:**
-Do not run this within 12 hours of a GitLab upgrade. This is to ensure that all background migrations have finished, which otherwise may lead to data loss.
+Do not run this within 12 hours of a GitLab upgrade. This is to ensure that all background migrations
+have finished, which otherwise may lead to data loss.
-When you remove LFS files from a repository's history, they become orphaned and continue to consume disk space. With this Rake task, you can remove invalid references from the database, which
+When you remove LFS files from a repository's history, they become orphaned and continue to consume
+disk space. With this Rake task, you can remove invalid references from the database, which
will allow garbage collection of LFS files.
For example:
@@ -177,3 +179,8 @@ sudo gitlab-rake gitlab:cleanup:sessions:active_sessions_lookup_keys
# installation from source
bundle exec rake gitlab:cleanup:sessions:active_sessions_lookup_keys RAILS_ENV=production
```
+
+## Container Registry garbage collection
+
+Container Registry can use considerable amounts of disk space. To clear up
+unused layers, the registry includes a [garbage collect command](../administration/packages/container_registry.md#container-registry-garbage-collection).
diff --git a/doc/raketasks/features.md b/doc/raketasks/features.md
index 5425a0a0667..1d68671a545 100644
--- a/doc/raketasks/features.md
+++ b/doc/raketasks/features.md
@@ -1,19 +1,21 @@
-# Namespaces
+# Namespaces **(CORE ONLY)**
+
+This Rake task enables [namespaces](../user/group/index.md#namespaces) for projects.
## Enable usernames and namespaces for user projects
-This command will enable the namespaces feature introduced in v4.0. It will move every project in its namespace folder.
+This command will enable the namespaces feature introduced in GitLab 4.0. It will move every project in its namespace folder.
Note:
-- Because the **repository location will change**, you will need to **update all your Git URLs** to point to the new location.
-- Username can be changed at **Profile âž” Account**.
-
-**Example:**
+- The **repository location will change**, so you will need to **update all your Git URLs** to
+ point to the new location.
+- The username can be changed at **Profile > Account**.
-Old path: `git@example.org:myrepo.git`
+For example:
-New path: `git@example.org:username/myrepo.git` or `git@example.org:groupname/myrepo.git`
+- Old path: `git@example.org:myrepo.git`.
+- New path: `git@example.org:username/myrepo.git` or `git@example.org:groupname/myrepo.git`.
```shell
bundle exec rake gitlab:enable_namespaces RAILS_ENV=production
diff --git a/doc/raketasks/generate_sample_prometheus_data.md b/doc/raketasks/generate_sample_prometheus_data.md
index 78e8ef188bf..902be6862c8 100644
--- a/doc/raketasks/generate_sample_prometheus_data.md
+++ b/doc/raketasks/generate_sample_prometheus_data.md
@@ -1,15 +1,25 @@
-# Generate Sample Prometheus Data
+# Generate sample Prometheus data **(CORE ONLY)**
This command will run Prometheus queries for each of the metrics of a specific environment
-for a series of time intervals: 30 minutes, 3 hours, 8 hours, 24 hours, 72 hours, and 7 days
-to now. The results of each of query are stored under a `sample_metrics` directory as a yaml
+for a series of time intervals to now:
+
+- 30 minutes
+- 3 hours
+- 8 hours
+- 24 hours
+- 72 hours
+- 7 days
+
+The results of each of query are stored under a `sample_metrics` directory as a YAML
file named by the metric's `identifier`. When the environmental variable `USE_SAMPLE_METRICS`
is set, the Prometheus API query is re-routed to `Projects::Environments::SampleMetricsController`
which loads the appropriate data set if it is present within the `sample_metrics` directory.
-- This command requires an id from an Environment with an available Prometheus installation.
+This command requires an ID from an environment with an available Prometheus installation.
+
+## Example
-**Example:**
+The following example demonstrates how to run the Rake task:
```shell
bundle exec rake gitlab:generate_sample_prometheus_data[21]
diff --git a/doc/raketasks/import.md b/doc/raketasks/import.md
index cda742b6077..5229ce2ab08 100644
--- a/doc/raketasks/import.md
+++ b/doc/raketasks/import.md
@@ -1,61 +1,63 @@
-# Import bare repositories into your GitLab instance
+# Import bare repositories **(CORE ONLY)**
-## Notes
+Rake tasks are available to import bare repositories into a GitLab instance.
-- The owner of the project will be the first admin
-- The groups will be created as needed, including subgroups
-- The owner of the group will be the first admin
-- Existing projects will be skipped
-- Projects in hashed storage may be skipped (see [Importing bare repositories from hashed storage](#importing-bare-repositories-from-hashed-storage))
-- The existing Git repos will be moved from disk (removed from the original path)
+Note that:
-## How to use
+- The owner of the project will be the first administrator.
+- The groups will be created as needed, including subgroups.
+- The owner of the group will be the first administrator.
+- Existing projects will be skipped.
+- Projects in hashed storage may be skipped. For more information, see
+ [Importing bare repositories from hashed storage](#importing-bare-repositories-from-hashed-storage).
+- The existing Git repositories will be moved from disk (removed from the original path).
-### Create a new folder to import your Git repositories from
+To import bare repositories into a GitLab instance:
-The new folder needs to have Git user ownership and read/write/execute access for Git user and its group:
+1. Create a new folder to import your Git repositories from. The new folder needs to have Git user
+ ownership and read/write/execute access for Git user and its group:
-```shell
-sudo -u git mkdir -p /var/opt/gitlab/git-data/repository-import-<date>/new_group
-```
-
-### Copy your bare repositories inside this newly created folder
+ ```shell
+ sudo -u git mkdir -p /var/opt/gitlab/git-data/repository-import-<date>/new_group
+ ```
-- Any `.git` repositories found on any of the subfolders will be imported as projects
-- Groups will be created as needed, these could be nested folders. Example:
+1. Copy your bare repositories inside this newly created folder. Note:
-If we copy the repos to `/var/opt/gitlab/git-data/repository-import-<date>`, and repo A needs to be under the groups G1 and G2, it will
-have to be created under those folders: `/var/opt/gitlab/git-data/repository-import-<date>/G1/G2/A.git`.
+ - Any `.git` repositories found on any of the subfolders will be imported as projects.
+ - Groups will be created as needed, these could be nested folders.
-```shell
-sudo cp -r /old/git/foo.git /var/opt/gitlab/git-data/repository-import-<date>/new_group/
+ For example, if we copy the repositories to `/var/opt/gitlab/git-data/repository-import-<date>`,
+ and repository `A` needs to be under the groups `G1` and `G2`, it must be created under those folders:
+ `/var/opt/gitlab/git-data/repository-import-<date>/G1/G2/A.git`.
-# Do this once when you are done copying git repositories
-sudo chown -R git:git /var/opt/gitlab/git-data/repository-import-<date>
-```
+ ```shell
+ sudo cp -r /old/git/foo.git /var/opt/gitlab/git-data/repository-import-<date>/new_group/
-`foo.git` needs to be owned by the `git` user and `git` users group.
+ # Do this once when you are done copying git repositories
+ sudo chown -R git:git /var/opt/gitlab/git-data/repository-import-<date>
+ ```
-If you are using an installation from source, replace `/var/opt/gitlab/` with `/home/git`.
+ `foo.git` needs to be owned by the `git` user and `git` users group.
-### Run the command below depending on your type of installation
+ If you are using an installation from source, replace `/var/opt/gitlab/` with `/home/git`.
-#### Omnibus Installation
+1. Run the following command depending on your type of installation:
-```shell
-sudo gitlab-rake gitlab:import:repos['/var/opt/gitlab/git-data/repository-import-<date>']
-```
+ - Omnibus Installation
-#### Installation from source
+ ```shell
+ sudo gitlab-rake gitlab:import:repos['/var/opt/gitlab/git-data/repository-import-<date>']
+ ```
-Before running this command you need to change the directory to where your GitLab installation is located:
+ - Installation from source. Before running this command you need to change to the directory where
+ your GitLab installation is located:
-```shell
-cd /home/git/gitlab
-sudo -u git -H bundle exec rake gitlab:import:repos['/var/opt/gitlab/git-data/repository-import-<date>'] RAILS_ENV=production
-```
+ ```shell
+ cd /home/git/gitlab
+ sudo -u git -H bundle exec rake gitlab:import:repos['/var/opt/gitlab/git-data/repository-import-<date>'] RAILS_ENV=production
+ ```
-#### Example output
+## Example output
```plaintext
Processing /var/opt/gitlab/git-data/repository-import-1/a/b/c/blah.git
@@ -73,8 +75,6 @@ Processing /var/opt/gitlab/git-data/repository-import-1/group/xyz.git
## Importing bare repositories from hashed storage
-### Background
-
Projects in legacy storage have a directory structure that mirrors their full
project path in GitLab, including their namespace structure. This information is
leveraged by the bare repository importer to import projects into their proper
@@ -86,17 +86,17 @@ improved performance and data integrity. See
[Repository Storage Types](../administration/repository_storage_types.md) for
more details.
-### Which repositories are importable?
+The repositories that are importable depends on the version of GitLab.
-#### GitLab 10.3 or earlier
+### GitLab 10.3 or earlier
Importing bare repositories from hashed storage is unsupported.
-#### GitLab 10.4 and later
+### GitLab 10.4 and later
To support importing bare repositories from hashed storage, GitLab 10.4 and
later stores the full project path with each repository, in a special section of
-the Git repository's config file. This section is formatted as follows:
+the Git repository's configuration file. This section is formatted as follows:
```ini
[gitlab]
diff --git a/doc/raketasks/list_repos.md b/doc/raketasks/list_repos.md
index 10e6cb04bfa..411de52e379 100644
--- a/doc/raketasks/list_repos.md
+++ b/doc/raketasks/list_repos.md
@@ -1,7 +1,8 @@
-# Listing repository directories
+# Listing repository directories **(CORE ONLY)**
-You can print a list of all Git repositories on disk managed by
-GitLab with the following command:
+You can print a list of all Git repositories on disk managed by GitLab.
+
+To print a list, run the following command:
```shell
# Omnibus
@@ -12,10 +13,13 @@ cd /home/git/gitlab
sudo -u git -H bundle exec rake gitlab:list_repos RAILS_ENV=production
```
-If you only want to list projects with recent activity you can pass
-a date with the 'SINCE' environment variable. The time you specify
-is parsed by the Rails [TimeZone#parse
-function](https://api.rubyonrails.org/classes/ActiveSupport/TimeZone.html#method-i-parse).
+NOTE: **Note:**
+The results use the default ordering of the GitLab Rails application.
+
+## Limit search results
+
+To list only projects with recent activity, pass a date with the `SINCE` environment variable. The
+time you specify is parsed by the Rails [TimeZone#parse function](https://api.rubyonrails.org/classes/ActiveSupport/TimeZone.html#method-i-parse).
```shell
# Omnibus
@@ -25,6 +29,3 @@ sudo gitlab-rake gitlab:list_repos SINCE='Sep 1 2015'
cd /home/git/gitlab
sudo -u git -H bundle exec rake gitlab:list_repos RAILS_ENV=production SINCE='Sep 1 2015'
```
-
-Note that the projects listed are NOT sorted by activity; they use
-the default ordering of the GitLab Rails application.
diff --git a/doc/raketasks/migrate_snippets.md b/doc/raketasks/migrate_snippets.md
new file mode 100644
index 00000000000..fce91ab703e
--- /dev/null
+++ b/doc/raketasks/migrate_snippets.md
@@ -0,0 +1,96 @@
+# Migration to Versioned Snippets **(CORE ONLY)**
+
+> [Introduced](https://gitlab.com/gitlab-org/gitlab/-/issues/215861) in GitLab 13.0.
+
+In GitLab 13.0, [GitLab Snippets are backed by Git repositories](../user/snippets.md#versioned-snippets).
+This means that the snippet content will be stored in the repository
+and users can update it directly through Git.
+
+Nevertheless, existing GitLab Snippets have to be migrated to this new functionality.
+For each snippet, a new repository is created and the snippet content is committed
+to the repository inside a file whose name is the file name used in the snippet
+as well.
+
+GitLab performs this migration through a [Background Migration](../development/background_migrations.md)
+automatically when the GitLab instance is upgrade to 13.0 or a higher version.
+However, if the migration fails for any of the snippets, they still need
+to be migrated individually.
+
+The following Rake tasks will help with that process.
+
+## Migrate specific snippets to Git
+
+In case you want to migrate a range of snippets, run the tasks as described below.
+
+For Omnibus installations, run:
+
+```shell
+sudo gitlab-rake gitlab:snippets:migrate SNIPPET_IDS=1,2,3,4
+```
+
+For installations from source code, run:
+
+```shell
+bundle exec rake gitlab:snippets:migrate SNIPPET_IDS=1,2,3,4
+```
+
+There is a default limit (100) to the number of ids supported in the migration
+process. You can modify this limit by using the env variable `LIMIT`.
+
+```shell
+sudo gitlab-rake gitlab:snippets:migrate SNIPPET_IDS=1,2,3,4 LIMIT=50
+```
+
+For installations from source code, run:
+
+```shell
+bundle exec rake gitlab:snippets:migrate SNIPPET_IDS=1,2,3,4 LIMIT=50
+```
+
+## Show whether the snippet background migration is running
+
+In case you want to check the status of the snippet background migration,
+whether it is running or not, you can use the following task.
+
+For Omnibus installations, run:
+
+```shell
+sudo gitlab-rake gitlab:snippets:migration_status
+```
+
+For installations from source code, run:
+
+```shell
+bundle exec rake gitlab:snippets:migration_status RAILS_ENV=production
+```
+
+## List non-migrated snippets
+
+With the following task, you can get the ids of all of the snippets
+that haven't been migrated yet or failed to migrate.
+
+For Omnibus installations, run:
+
+```shell
+sudo gitlab-rake gitlab:snippets:list_non_migrated
+```
+
+For installations from source code, run:
+
+```shell
+bundle exec rake gitlab:snippets:list_non_migrated RAILS_ENV=production
+```
+
+As the number of non-migrated snippets can be large, we limit
+by default the size of the number of ids returned to 100. You can
+modify this limit by using the env variable `LIMIT`.
+
+```shell
+sudo gitlab-rake gitlab:snippets:list_non_migrated LIMIT=200
+```
+
+For installations from source code, run:
+
+```shell
+bundle exec rake gitlab:snippets:list_non_migrated RAILS_ENV=production LIMIT=200
+```
diff --git a/doc/raketasks/user_management.md b/doc/raketasks/user_management.md
index ffb1baa0b6e..61e30f9ddb1 100644
--- a/doc/raketasks/user_management.md
+++ b/doc/raketasks/user_management.md
@@ -1,7 +1,11 @@
-# User management
+# User management **(CORE ONLY)**
+
+GitLab provides Rake tasks for user management.
## Add user as a developer to all projects
+To add a user as a developer to all projects, run:
+
```shell
# omnibus-gitlab
sudo gitlab-rake gitlab:import:user_to_projects[username@domain.tld]
@@ -12,9 +16,7 @@ bundle exec rake gitlab:import:user_to_projects[username@domain.tld] RAILS_ENV=p
## Add all users to all projects
-Notes:
-
-- admin users are added as maintainers
+To add all users to all projects, run:
```shell
# omnibus-gitlab
@@ -24,8 +26,13 @@ sudo gitlab-rake gitlab:import:all_users_to_all_projects
bundle exec rake gitlab:import:all_users_to_all_projects RAILS_ENV=production
```
+NOTE: **Note:**
+Admin users are added as maintainers.
+
## Add user as a developer to all groups
+To add a user as a developer to all groups, run:
+
```shell
# omnibus-gitlab
sudo gitlab-rake gitlab:import:user_to_groups[username@domain.tld]
@@ -36,9 +43,7 @@ bundle exec rake gitlab:import:user_to_groups[username@domain.tld] RAILS_ENV=pro
## Add all users to all groups
-Notes:
-
-- admin users are added as owners so they can add additional users to the group
+To add all users to all groups, run:
```shell
# omnibus-gitlab
@@ -48,19 +53,25 @@ sudo gitlab-rake gitlab:import:all_users_to_all_groups
bundle exec rake gitlab:import:all_users_to_all_groups RAILS_ENV=production
```
-## Maintain tight control over the number of active users on your GitLab installation
+NOTE: **Note:**
+Admin users are added as owners so they can add additional users to the group.
+
+## Control the number of active users
-- Enable this setting to keep new users blocked until they have been cleared by the admin (default: false).
+Enable this setting to keep new users blocked until they have been cleared by the administrator.
+Defaults to `false`:
```plaintext
block_auto_created_users: false
```
-## Disable Two-factor Authentication (2FA) for all users
+## Disable two-factor authentication for all users
-This task will disable 2FA for all users that have it enabled. This can be
+This task disables two-factor authentication (2FA) for all users that have it enabled. This can be
useful if GitLab's `config/secrets.yml` file has been lost and users are unable
-to login, for example.
+to log in, for example.
+
+To disable two-factor authentication for all users, run:
```shell
# omnibus-gitlab
@@ -70,65 +81,65 @@ sudo gitlab-rake gitlab:two_factor:disable_for_all_users
bundle exec rake gitlab:two_factor:disable_for_all_users RAILS_ENV=production
```
-## Rotate Two-factor Authentication (2FA) encryption key
+## Rotate two-factor authentication encryption key
-GitLab stores the secret data enabling 2FA to work in an encrypted database
-column. The encryption key for this data is known as `otp_key_base`, and is
+GitLab stores the secret data required for two-factor authentication (2FA) in an encrypted
+database column. The encryption key for this data is known as `otp_key_base`, and is
stored in `config/secrets.yml`.
If that file is leaked, but the individual 2FA secrets have not, it's possible
to re-encrypt those secrets with a new encryption key. This allows you to change
the leaked key without forcing all users to change their 2FA details.
-First, look up the old key. This is in the `config/secrets.yml` file, but
-**make sure you're working with the production section**. The line you're
-interested in will look like this:
+To rotate the two-factor authentication encryption key:
-```yaml
-production:
- otp_key_base: ffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff
-```
+1. Look up the old key. This is in the `config/secrets.yml` file, but **make sure you're working
+ with the production section**. The line you're interested in will look like this:
-Next, generate a new secret:
+ ```yaml
+ production:
+ otp_key_base: ffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff
+ ```
-```shell
-# omnibus-gitlab
-sudo gitlab-rake secret
+1. Generate a new secret:
-# installation from source
-bundle exec rake secret RAILS_ENV=production
-```
+ ```shell
+ # omnibus-gitlab
+ sudo gitlab-rake secret
-Now you need to stop the GitLab server, back up the existing secrets file and
-update the database:
+ # installation from source
+ bundle exec rake secret RAILS_ENV=production
+ ```
-```shell
-# omnibus-gitlab
-sudo gitlab-ctl stop
-sudo cp config/secrets.yml config/secrets.yml.bak
-sudo gitlab-rake gitlab:two_factor:rotate_key:apply filename=backup.csv old_key=<old key> new_key=<new key>
+1. Stop the GitLab server, back up the existing secrets file, and update the database:
-# installation from source
-sudo /etc/init.d/gitlab stop
-cp config/secrets.yml config/secrets.yml.bak
-bundle exec rake gitlab:two_factor:rotate_key:apply filename=backup.csv old_key=<old key> new_key=<new key> RAILS_ENV=production
-```
+ ```shell
+ # omnibus-gitlab
+ sudo gitlab-ctl stop
+ sudo cp config/secrets.yml config/secrets.yml.bak
+ sudo gitlab-rake gitlab:two_factor:rotate_key:apply filename=backup.csv old_key=<old key> new_key=<new key>
-The `<old key>` value can be read from `config/secrets.yml`; `<new key>` was
-generated earlier. The **encrypted** values for the user 2FA secrets will be
-written to the specified `filename` - you can use this to rollback in case of
-error.
+ # installation from source
+ sudo /etc/init.d/gitlab stop
+ cp config/secrets.yml config/secrets.yml.bak
+ bundle exec rake gitlab:two_factor:rotate_key:apply filename=backup.csv old_key=<old key> new_key=<new key> RAILS_ENV=production
+ ```
-Finally, change `config/secrets.yml` to set `otp_key_base` to `<new key>` and
-restart. Again, make sure you're operating in the **production** section.
+ The `<old key>` value can be read from `config/secrets.yml` (`<new key>` was
+ generated earlier). The **encrypted** values for the user 2FA secrets will be
+ written to the specified `filename`. You can use this to rollback in case of
+ error.
-```shell
-# omnibus-gitlab
-sudo gitlab-ctl start
+1. Change `config/secrets.yml` to set `otp_key_base` to `<new key>` and restart. Again, make sure
+ you're operating in the **production** section.
-# installation from source
-sudo /etc/init.d/gitlab start
-```
+ ```shell
+ # omnibus-gitlab
+ sudo gitlab-ctl start
+
+ # installation from source
+ sudo /etc/init.d/gitlab start
+ ```
If there are any problems (perhaps using the wrong value for `old_key`), you can
restore your backup of `config/secrets.yml` and rollback the changes:
diff --git a/doc/raketasks/web_hooks.md b/doc/raketasks/web_hooks.md
index 22084c862ba..7bd2ed311d2 100644
--- a/doc/raketasks/web_hooks.md
+++ b/doc/raketasks/web_hooks.md
@@ -1,60 +1,78 @@
# Webhooks administration **(CORE ONLY)**
-## Add a webhook for **ALL** projects
+GitLab provides Rake tasks for webhooks management.
+
+Requests to the [local network by webhooks](../security/webhooks.md) can be allowed or blocked by an
+administrator.
+
+## Add a webhook to all projects
+
+To add a webhook to all projects, run:
```shell
# omnibus-gitlab
sudo gitlab-rake gitlab:web_hook:add URL="http://example.com/hook"
+
# source installations
bundle exec rake gitlab:web_hook:add URL="http://example.com/hook" RAILS_ENV=production
```
-## Add a webhook for projects in a given **NAMESPACE**
+## Add a webhook to projects in a namespace
+
+To add a webhook to all projects in a specific namespace, run:
```shell
# omnibus-gitlab
-sudo gitlab-rake gitlab:web_hook:add URL="http://example.com/hook" NAMESPACE=acme
+sudo gitlab-rake gitlab:web_hook:add URL="http://example.com/hook" NAMESPACE=<namespace>
+
# source installations
-bundle exec rake gitlab:web_hook:add URL="http://example.com/hook" NAMESPACE=acme RAILS_ENV=production
+bundle exec rake gitlab:web_hook:add URL="http://example.com/hook" NAMESPACE=<namespace> RAILS_ENV=production
```
-## Remove a webhook from **ALL** projects using
+## Remove a webhook from projects
+
+To remove a webhook from all projects, run:
```shell
# omnibus-gitlab
sudo gitlab-rake gitlab:web_hook:rm URL="http://example.com/hook"
+
# source installations
bundle exec rake gitlab:web_hook:rm URL="http://example.com/hook" RAILS_ENV=production
```
-## Remove a webhook from projects in a given **NAMESPACE**
+## Remove a webhook from projects in a namespace
+
+To remove a webhook from projects in a specific namespace, run:
```shell
# omnibus-gitlab
-sudo gitlab-rake gitlab:web_hook:rm URL="http://example.com/hook" NAMESPACE=acme
+sudo gitlab-rake gitlab:web_hook:rm URL="http://example.com/hook" NAMESPACE=<namespace>
+
# source installations
-bundle exec rake gitlab:web_hook:rm URL="http://example.com/hook" NAMESPACE=acme RAILS_ENV=production
+bundle exec rake gitlab:web_hook:rm URL="http://example.com/hook" NAMESPACE=<namespace> RAILS_ENV=production
```
-## List **ALL** webhooks
+## List all webhooks
+
+To list all webhooks, run:
```shell
# omnibus-gitlab
sudo gitlab-rake gitlab:web_hook:list
+
# source installations
bundle exec rake gitlab:web_hook:list RAILS_ENV=production
```
-## List the webhooks from projects in a given **NAMESPACE**
+## List webhooks for projects in a namespace
+
+To list all webhook for projects in a specified namespace, run:
```shell
# omnibus-gitlab
-sudo gitlab-rake gitlab:web_hook:list NAMESPACE=acme
+sudo gitlab-rake gitlab:web_hook:list NAMESPACE=<namespace>
+
# source installations
-bundle exec rake gitlab:web_hook:list NAMESPACE=acme RAILS_ENV=production
+bundle exec rake gitlab:web_hook:list NAMESPACE=<namespace> RAILS_ENV=production
```
-
-## Local requests in webhooks
-
-[Requests to local network by webhooks](../security/webhooks.md) can be allowed
-or blocked by an administrator.
diff --git a/doc/raketasks/x509_signatures.md b/doc/raketasks/x509_signatures.md
index dd518997f7b..f7c47794690 100644
--- a/doc/raketasks/x509_signatures.md
+++ b/doc/raketasks/x509_signatures.md
@@ -1,21 +1,24 @@
-# X509 signatures
+# X.509 signatures **(CORE ONLY)**
-When [signing commits with x509](../user/project/repository/x509_signed_commits/index.md)
-the trust anchor might change and the signatures stored within the database have
-to be updated.
+> [Introduced](https://gitlab.com/gitlab-org/gitlab/-/issues/122159) in GitLab 12.10.
-## Update all x509 signatures
+When [signing commits with X.509](../user/project/repository/x509_signed_commits/index.md),
+the trust anchor might change and the signatures stored within the database must be updated.
-This task loops through all X509 signed commits and updates their verification
-based on current certificate store.
+## Update all X.509 signatures
-**Omnibus Installation**
+This task loops through all X.509 signed commits and updates their verification based on current
+certificate store.
+
+To update all X.509 signatures, run:
+
+**Omnibus Installations:**
```shell
sudo gitlab-rake gitlab:x509:update_signatures
```
-**Source Installation**
+**Source Installations:**
```shell
sudo -u git -H bundle exec rake gitlab:x509:update_signatures RAILS_ENV=production
diff --git a/doc/security/README.md b/doc/security/README.md
index c21d99658b8..e2375c0f0b5 100644
--- a/doc/security/README.md
+++ b/doc/security/README.md
@@ -23,6 +23,4 @@ type: index
## Securing your GitLab installation
-To make sure your GitLab instance is safe and secure, please consider implementing
-[Sign up restrictions](../user/admin_area/settings/sign_up_restrictions.md) to avoid
-malicious users creating accounts.
+Consider access control features like [Sign up restrictions](../user/admin_area/settings/sign_up_restrictions.md) and [Authentication options](../topics/authentication/) to harden your GitLab instance and minimize the risk of unwanted user account creation.
diff --git a/doc/security/asset_proxy.md b/doc/security/asset_proxy.md
index d042fb3efe7..91a35c2f2a9 100644
--- a/doc/security/asset_proxy.md
+++ b/doc/security/asset_proxy.md
@@ -63,6 +63,6 @@ For example, the following is a link to an image in Markdown:
The following is an example of a source link that could result:
-```text
+```plaintext
http://proxy.gitlab.example.com/f9dd2b40157757eb82afeedbf1290ffb67a3aeeb/68747470733a2f2f61626f75742e6769746c61622e636f6d2f696d616765732f70726573732f6c6f676f2f6a70672f6769746c61622d69636f6e2d7267622e6a7067
```
diff --git a/doc/security/crime_vulnerability.md b/doc/security/crime_vulnerability.md
index 93edbc69eb0..2496029d93e 100644
--- a/doc/security/crime_vulnerability.md
+++ b/doc/security/crime_vulnerability.md
@@ -14,14 +14,14 @@ authenticated web session, allowing the launching of further attacks.
The TLS Protocol CRIME Vulnerability affects systems that use data compression
over HTTPS. Your system might be vulnerable to the CRIME vulnerability if you use
-SSL Compression (for example, gzip) or SPDY (which optionally uses compression).
+SSL Compression (for example, Gzip) or SPDY (which optionally uses compression).
-GitLab supports both gzip and [SPDY][ngx-spdy] and mitigates the CRIME
-vulnerability by deactivating gzip when HTTPS is enabled. The sources of the
+GitLab supports both Gzip and [SPDY](http://nginx.org/en/docs/http/ngx_http_spdy_module.html) and mitigates the CRIME
+vulnerability by deactivating Gzip when HTTPS is enabled. The sources of the
files are here:
-- [Source installation NGINX file][source-nginx]
-- [Omnibus installation NGINX file][omnibus-nginx]
+- [Source installation NGINX file](https://gitlab.com/gitlab-org/gitlab/blob/master/lib/support/nginx/gitlab-ssl)
+- [Omnibus installation NGINX file](https://gitlab.com/gitlab-org/omnibus-gitlab/blob/master/files/gitlab-cookbooks/gitlab/templates/default/nginx-gitlab-http.conf.erb)
Although SPDY is enabled in Omnibus installations, CRIME relies on compression
(the 'C') and the default compression level in NGINX's SPDY module is 0
@@ -29,7 +29,7 @@ Although SPDY is enabled in Omnibus installations, CRIME relies on compression
## Nessus
-The Nessus scanner, [reports a possible CRIME vulnerability][nessus] in GitLab
+The Nessus scanner, [reports a possible CRIME vulnerability](https://www.tenable.com/plugins/index.php?view=single&id=62565) in GitLab
similar to the following format:
```plaintext
@@ -55,15 +55,9 @@ vulnerability.
## References
-- NGINX ["Module ngx_http_spdy_module"][ngx-spdy]
-- Tenable Network Security, Inc. ["Transport Layer Security (TLS) Protocol CRIME Vulnerability"][nessus]
-- Wikipedia contributors, ["CRIME"][wiki-crime] Wikipedia, The Free Encyclopedia
-
-[source-nginx]: https://gitlab.com/gitlab-org/gitlab/blob/master/lib/support/nginx/gitlab-ssl
-[omnibus-nginx]: https://gitlab.com/gitlab-org/omnibus-gitlab/blob/master/files/gitlab-cookbooks/gitlab/templates/default/nginx-gitlab-http.conf.erb
-[ngx-spdy]: http://nginx.org/en/docs/http/ngx_http_spdy_module.html
-[nessus]: https://www.tenable.com/plugins/index.php?view=single&id=62565
-[wiki-crime]: https://en.wikipedia.org/wiki/CRIME
+- NGINX ["Module ngx_http_spdy_module"](http://nginx.org/en/docs/http/ngx_http_spdy_module.html)
+- Tenable Network Security, Inc. ["Transport Layer Security (TLS) Protocol CRIME Vulnerability"](https://www.tenable.com/plugins/index.php?view=single&id=62565)
+- Wikipedia contributors, ["CRIME"](https://en.wikipedia.org/wiki/CRIME) Wikipedia, The Free Encyclopedia
<!-- ## Troubleshooting
diff --git a/doc/security/rack_attack.md b/doc/security/rack_attack.md
index 9ce2a9bb1ae..5d18746e4e0 100644
--- a/doc/security/rack_attack.md
+++ b/doc/security/rack_attack.md
@@ -36,27 +36,20 @@ will be enabled:
### Protected paths throttle
-NOTE: **Note:** Omnibus GitLab protected paths throttle is deprecated and is scheduled for removal in
-GitLab 13.0. Please refer to [Migrate settings from GitLab 12.3 and earlier](../user/admin_area/settings/protected_paths.md#migrate-settings-from-gitlab-123-and-earlier).
-
GitLab responds with HTTP status code `429` to POST requests at protected paths
that exceed 10 requests per minute per IP address.
By default, protected paths are:
-```ruby
-default['gitlab']['gitlab-rails']['rack_attack_protected_paths'] = [
- '/users/password',
- '/users/sign_in',
- '/api/#{API::API.version}/session.json',
- '/api/#{API::API.version}/session',
- '/users',
- '/users/confirmation',
- '/unsubscribes/',
- '/import/github/personal_access_token',
- '/admin/session'
-]
-```
+- `/users/password`
+- `/users/sign_in`
+- `/api/#{API::API.version}/session.json`
+- `/api/#{API::API.version}/session`
+- `/users`
+- `/users/confirmation`
+- `/unsubscribes/`
+- `/import/github/personal_access_token`
+- `/admin/session`
This header is included in responses to blocked requests:
@@ -141,9 +134,6 @@ taken in order to enable protection for your GitLab instance:
config.middleware.use Rack::Attack
```
-1. Copy `config/initializers/rack_attack.rb.example` to `config/initializers/rack_attack.rb`
-1. Open `config/initializers/rack_attack.rb`, review the
- `paths_to_be_protected`, and add any other path you need protecting
1. Restart GitLab:
```shell
diff --git a/doc/security/webhooks.md b/doc/security/webhooks.md
index bd05cbff05e..27f79dbdf66 100644
--- a/doc/security/webhooks.md
+++ b/doc/security/webhooks.md
@@ -78,7 +78,7 @@ will whitelist all ports on all IPs in that range.
Example:
-```text
+```plaintext
example.com;gitlab.example.com
127.0.0.1,1:0:0:0:0:0:0:1
127.0.0.0/8 1:0:0:0:0:0:0:0/124
diff --git a/doc/subscriptions/index.md b/doc/subscriptions/index.md
index c81310edc44..3978506cbf9 100644
--- a/doc/subscriptions/index.md
+++ b/doc/subscriptions/index.md
@@ -75,9 +75,10 @@ count as active users in the subscription period in which they were originally a
- Members with Guest permissions on an Ultimate subscription.
- GitLab-created service accounts: `Ghost User` and `Support Bot`.
-##### User Statistics
+##### Users statistics
-A breakdown of the users within your instance including active, billable and blocked can be found by navigating to **Admin Area > Overview > Dashboard** and selecting `Users Statistics` button within the `Users` widget..
+To view a breakdown of the users within your instance, including active, billable, and blocked, go to **Admin Area > Overview > Dashboard** and select **Users statistics** in the **Users** section.
+For more details, see [Users statistics](../user/admin_area/index.md#users-statistics).
NOTE: **Note:**
If you have LDAP integration enabled, anyone in the configured domain can sign up for a GitLab account. This can result in an unexpected bill at time of renewal. Consider [disabling new signups](../user/admin_area/settings/sign_up_restrictions.md) and managing new users manually instead.
@@ -126,6 +127,7 @@ instance, ensure you're purchasing enough seats to
With the [Customers Portal](https://customers.gitlab.com/) you can:
- [Change billing information](#change-billing-information)
+- [Change the payment method](#change-payment-method)
- [Change the linked account](#change-the-linked-account)
- [Change the associated namespace](#change-the-associated-namespace)
@@ -143,6 +145,15 @@ Future purchases will use the information in this section.
The email listed in this section is used for the Customers Portal
login and for license-related email communication.
+### Change payment method
+
+To change payment method or update credit card information:
+
+1. Log in to the [Customers Portal](https://customers.gitlab.com/customers/sign_in).
+1. Select the **My account** drop-down and click on **Payment methods**.
+1. **Edit** the existing payment method information or **Add new payment method**.
+1. Save changes.
+
### Change the linked account
To change the GitLab.com account associated with a Customers Portal
@@ -171,6 +182,8 @@ Subscription charges are calculated based on the total number of users in a grou
## View your subscription
+### View your GitLab.com subscription
+
To see the status of your GitLab.com subscription, log into GitLab.com and go to the **Billing** section of the relevant namespace:
- For individuals:
@@ -190,6 +203,13 @@ The following table describes details of your subscription for groups:
| Subscription start date | Date your subscription started. If this is for a Free plan, is the date you transitioned off your group's paid plan. |
| Subscription end date | Date your current subscription will end. Does not apply to Free plans. |
+### View your self-managed subscription
+
+To view the status of your self-managed subscription, log into the self-managed instance and go to the **License** page.
+
+ 1. Go to **{admin}** **Admin Area**.
+ 1. From the left-hand menu, select **License**.
+
## Renew your subscription
To renew your subscription, [prepare for renewal by reviewing your account](#prepare-for-renewal-by-reviewing-your-account), then do one of the following:
diff --git a/doc/telemetry/backend.md b/doc/telemetry/backend.md
deleted file mode 100644
index c571439af8a..00000000000
--- a/doc/telemetry/backend.md
+++ /dev/null
@@ -1,34 +0,0 @@
-# Backend tracking guide
-
-GitLab provides `Gitlab::Tracking`, an interface that wraps the [Snowplow Ruby Tracker](https://github.com/snowplow/snowplow/wiki/ruby-tracker) for tracking custom events.
-
-## Tracking in Ruby
-
-Custom event tracking and instrumentation can be added by directly calling the `GitLab::Tracking.event` class method, which accepts the following arguments:
-
-| argument | type | default value | description |
-|:-----------|:-------|:---------------------------|:------------|
-| `category` | string | 'application' | Area or aspect of the application. This could be `HealthCheckController` or `Lfs::FileTransformer` for instance. |
-| `action` | string | 'generic' | The action being taken, which can be anything from a controller action like `create` to something like an Active Record callback. |
-| `data` | object | {} | Additional data such as `label`, `property`, `value`, and `context` as described [in our Feature Instrumentation taxonomy](https://about.gitlab.com/handbook/product/feature-instrumentation/#taxonomy). These will be set as empty strings if you don't provide them. |
-
-Tracking can be viewed as either tracking user behavior, or can be utilized for instrumentation to monitor and visual performance over time in an area or aspect of code.
-
-For example:
-
-```ruby
-class Projects::CreateService < BaseService
- def execute
- project = Project.create(params)
-
- Gitlab::Tracking.event('Projects::CreateService', 'create_project',
- label: project.errors.full_messages.to_sentence,
- value: project.valid?
- )
- end
-end
-```
-
-### Performance
-
-We use the [AsyncEmitter](https://github.com/snowplow/snowplow/wiki/Ruby-Tracker#52-the-asyncemitter-class) when tracking events, which allows for instrumentation calls to be run in a background thread. This is still an active area of development.
diff --git a/doc/telemetry/frontend.md b/doc/telemetry/frontend.md
deleted file mode 100644
index fcd394500ec..00000000000
--- a/doc/telemetry/frontend.md
+++ /dev/null
@@ -1,167 +0,0 @@
-# Frontend tracking guide
-
-GitLab provides `Tracking`, an interface that wraps the [Snowplow JavaScript Tracker](https://github.com/snowplow/snowplow/wiki/javascript-tracker) for tracking custom events. There are a few ways to utilize tracking, but each generally requires at minimum, a `category` and an `action`. Additional data can be provided that adheres to our [Feature instrumentation taxonomy](https://about.gitlab.com/handbook/product/feature-instrumentation/#taxonomy).
-
-| field | type | default value | description |
-|:-----------|:-------|:---------------------------|:------------|
-| `category` | string | document.body.dataset.page | Page or subsection of a page that events are being captured within. |
-| `action` | string | 'generic' | Action the user is taking. Clicks should be `click` and activations should be `activate`, so for example, focusing a form field would be `activate_form_input`, and clicking a button would be `click_button`. |
-| `data` | object | {} | Additional data such as `label`, `property`, `value`, and `context` as described [in our Feature Instrumentation taxonomy](https://about.gitlab.com/handbook/product/feature-instrumentation/#taxonomy). |
-
-## Tracking in HAML (or Vue Templates)
-
-When working within HAML (or Vue templates) we can add `data-track-*` attributes to elements of interest. All elements that have a `data-track-event` attribute will automatically have event tracking bound on clicks.
-
-Below is an example of `data-track-*` attributes assigned to a button:
-
-```haml
-%button.btn{ data: { track: { event: "click_button", label: "template_preview", property: "my-template" } } }
-```
-
-```html
-<button class="btn"
- data-track-event="click_button"
- data-track-label="template_preview"
- data-track-property="my-template"
-/>
-```
-
-Event listeners are bound at the document level to handle click events on or within elements with these data attributes. This allows for them to be properly handled on rerendering and changes to the DOM, but it's important to know that because of the way these events are bound, click events shouldn't be stopped from propagating up the DOM tree. If for any reason click events are being stopped from propagating, you'll need to implement your own listeners and follow the instructions in [Tracking in raw JavaScript](#tracking-in-raw-javascript).
-
-Below is a list of supported `data-track-*` attributes:
-
-| attribute | required | description |
-|:----------------------|:---------|:------------|
-| `data-track-event` | true | Action the user is taking. Clicks must be prepended with `click` and activations must be prepended with `activate`. For example, focusing a form field would be `activate_form_input` and clicking a button would be `click_button`. |
-| `data-track-label` | false | The `label` as described [in our Feature Instrumentation taxonomy](https://about.gitlab.com/handbook/product/feature-instrumentation/#taxonomy). |
-| `data-track-property` | false | The `property` as described [in our Feature Instrumentation taxonomy](https://about.gitlab.com/handbook/product/feature-instrumentation/#taxonomy). |
-| `data-track-value` | false | The `value` as described [in our Feature Instrumentation taxonomy](https://about.gitlab.com/handbook/product/feature-instrumentation/#taxonomy). If omitted, this will be the elements `value` property or an empty string. For checkboxes, the default value will be the element's checked attribute or `false` when unchecked. |
-| `data-track-context` | false | The `context` as described [in our Feature Instrumentation taxonomy](https://about.gitlab.com/handbook/product/feature-instrumentation/#taxonomy). |
-
-## Tracking within Vue components
-
-There's a tracking Vue mixin that can be used in components if more complex tracking is required. To use it, first import the `Tracking` library and request a mixin.
-
-```javascript
-import Tracking from '~/tracking';
-const trackingMixin = Tracking.mixin({ label: 'right_sidebar' });
-```
-
-You can provide default options that will be passed along whenever an event is tracked from within your component. For instance, if all events within a component should be tracked with a given `label`, you can provide one at this time. Available defaults are `category`, `label`, `property`, and `value`. If no category is specified, `document.body.dataset.page` is used as the default.
-
-You can then use the mixin normally in your component with the `mixin`, Vue declaration. The mixin also provides the ability to specify tracking options in `data` or `computed`. These will override any defaults and allows the values to be dynamic from props, or based on state.
-
-```javascript
-export default {
- mixins: [trackingMixin],
- // ...[component implementation]...
- data() {
- return {
- expanded: false,
- tracking: {
- label: 'left_sidebar'
- }
- };
- },
-}
-```
-
-The mixin provides a `track` method that can be called within the template, or from component methods. An example of the whole implementation might look like the following.
-
-```javascript
-export default {
- mixins: [Tracking.mixin({ label: 'right_sidebar' })],
- data() {
- return {
- expanded: false,
- };
- },
- methods: {
- toggle() {
- this.expanded = !this.expanded;
- this.track('click_toggle', { value: this.expanded })
- }
- }
-};
-```
-
-And if needed within the template, you can use the `track` method directly as well.
-
-```html
-<template>
- <div>
- <a class="toggle" @click.prevent="toggle">Toggle</a>
- <div v-if="expanded">
- <p>Hello world!</p>
- <a @click.prevent="track('click_action')">Track an event</a>
- </div>
- </div>
-</template>
-```
-
-## Tracking in raw JavaScript
-
-Custom event tracking and instrumentation can be added by directly calling the `Tracking.event` static function. The following example demonstrates tracking a click on a button by calling `Tracking.event` manually.
-
-```javascript
-import Tracking from '~/tracking';
-
-const button = document.getElementById('create_from_template_button');
-button.addEventListener('click', () => {
- Tracking.event('dashboard:projects:index', 'click_button', {
- label: 'create_from_template',
- property: 'template_preview',
- value: 'rails',
- });
-})
-```
-
-## Tests and test helpers
-
-In Jest particularly in vue tests, you can use the following:
-
-```javascript
-import { mockTracking } from 'helpers/tracking_helper';
-
-describe('MyTracking', () => {
- let spy;
-
- beforeEach(() => {
- spy = mockTracking('_category_', wrapper.element, jest.spyOn);
- });
-
- it('tracks an event when clicked on feedback', () => {
- wrapper.find('.discover-feedback-icon').trigger('click');
-
- expect(spy).toHaveBeenCalledWith('_category_', 'click_button', {
- label: 'security-discover-feedback-cta',
- property: '0',
- });
- });
-});
-
-```
-
-In obsolete Karma tests it's used as below:
-
-```javascript
-import { mockTracking, triggerEvent } from 'spec/helpers/tracking_helper';
-
-describe('my component', () => {
- let trackingSpy;
-
- beforeEach(() => {
- const vm = mountComponent(MyComponent);
- trackingSpy = mockTracking('_category_', vm.$el, spyOn);
- });
-
- it('tracks an event when toggled', () => {
- triggerEvent('a.toggle');
-
- expect(trackingSpy).toHaveBeenCalledWith('_category_', 'click_edit_button', {
- label: 'right_sidebar',
- property: 'confidentiality',
- });
- });
-});
-```
diff --git a/doc/telemetry/index.md b/doc/telemetry/index.md
index 55a7fad86be..977b93b712e 100644
--- a/doc/telemetry/index.md
+++ b/doc/telemetry/index.md
@@ -1,72 +1,5 @@
-# Event tracking
+---
+redirect_to: '../development/telemetry/index.md'
+---
-At GitLab, we encourage event tracking so we can iterate on and improve the project and user experience.
-
-We do this by running experiments, and collecting analytics for features and feature variations. This is:
-
-- So we generally know engagement.
-- A way to approach A/B testing.
-
-As developers, we should attempt to add tracking and instrumentation where possible. This enables the Product team to better understand:
-
-- User engagement.
-- Usage patterns.
-- Other metrics that can potentially be improved on.
-
-To maintain consistency and not adversely effect performance, we have some basic tracking functionality exposed at both the frontend and backend layers that can be utilized while building new features or updating existing features.
-
-We also encourage users to enable tracking, and we embrace full transparency with our tracking approach so it can be easily understood and trusted. By enabling tracking, users can:
-
-- Contribute back to the wider community.
-- Help GitLab improve on the product.
-
-## Implementing tracking
-
-Event tracking can be implemented on either the frontend or the backend layers, and each can be approached slightly differently since they have slightly different concerns.
-
-In GitLab, many actions can be initiated via the web interface, but they can also be initiated via an API client (an iOS applications is a good example of this), or via `git` directly. Crucially, this means that tracking should be considered holistically for the feature that's being instrumented.
-
-The data team should be involved when defining analytics and can be consulted when coming up with ways of presenting data that's being tracked. This allows our event data to be considered carefully and presented in ways that may reveal details about user engagement that may not be fully understood or interactions where we can make improvements. You can [contact the data team](https://about.gitlab.com/handbook/business-ops/data-team/#contact-us) and consult with them when defining tracking strategies.
-
-### Frontend
-
-Generally speaking, the frontend can track user actions and events, like:
-
-- Clicking links or buttons.
-- Submitting forms.
-- Other typically interface-driven actions.
-
-See [Frontend tracking guide](frontend.md).
-
-### Backend
-
-From the backend, the events that are tracked will likely consist of things like the creation or deletion of records and other events that might be triggered from layers that aren't necessarily only available in the interface.
-
-See [Backend tracking guide](backend.md).
-
-Also, see [Instrumenting Ruby code](../development/instrumentation.md) if you are instrumenting application performance metrics for Ruby code.
-
-## Enabling tracking
-
-Tracking can be enabled at:
-
-- The instance level, which will enable tracking on both the frontend and backend layers.
-- User level, though user tracking can be disabled on a per-user basis. GitLab tracking respects the [Do Not Track](https://www.eff.org/issues/do-not-track) standard, so any user who has enabled the Do Not Track option in their browser will also not be tracked from a user level.
-
-We utilize Snowplow for the majority of our tracking strategy, and it can be enabled by navigating to:
-
-- **Admin Area > Settings > Integrations** in the UI.
-- `admin/application_settings/integrations` in your browser.
-
-The following configuration is required:
-
-| Name | Value |
-| ------------- | ------------------------- |
-| Collector | `snowplow.trx.gitlab.net` |
-| Site ID | `gitlab` |
-| Cookie domain | `.gitlab.com` |
-
-Once enabled, tracking events can be inspected locally by either:
-
-- Looking at the network panel of the browser's development tools
-- Using the [Snowplow Chrome Extension](https://chrome.google.com/webstore/detail/snowplow-inspector/maplkdomeamdlngconidoefjpogkmljm).
+This document was moved to [another location](../development/telemetry/index.md).
diff --git a/doc/telemetry/snowplow.md b/doc/telemetry/snowplow.md
new file mode 100644
index 00000000000..977b93b712e
--- /dev/null
+++ b/doc/telemetry/snowplow.md
@@ -0,0 +1,5 @@
+---
+redirect_to: '../development/telemetry/index.md'
+---
+
+This document was moved to [another location](../development/telemetry/index.md).
diff --git a/doc/tools/email.md b/doc/tools/email.md
index e9ff88152ba..eacf63ecdcd 100644
--- a/doc/tools/email.md
+++ b/doc/tools/email.md
@@ -26,6 +26,9 @@ at their primary email address.
![compose an email](email2.png)
+NOTE: **Note:**
+[Starting with GitLab 13.0](https://gitlab.com/gitlab-org/gitlab/-/issues/31509), email notifications can be sent only once every 10 minutes. This helps minimize performance issues.
+
## Unsubscribing from emails
Users can choose to unsubscribe from receiving emails from GitLab by following
diff --git a/doc/topics/airgap/index.md b/doc/topics/airgap/index.md
index e712e3bb6b5..854e0103a69 100644
--- a/doc/topics/airgap/index.md
+++ b/doc/topics/airgap/index.md
@@ -30,9 +30,75 @@ example of such a transfer:
1. Transfer images to offline environment.
1. Load transferred images into offline Docker registry.
-### Example image packager script
+### Using the official GitLab template
-```sh
+GitLab provides a [vendored template](../../ci/yaml/README.md#includetemplate)
+to ease this process.
+
+This template should be used in a new, empty project, with a `gitlab-ci.yml` file containing:
+
+```yaml
+include:
+ - template: Secure-Binaries.gitlab-ci.yml
+```
+
+The pipeline downloads the Docker images needed for the Security Scanners and saves them as
+[job artifacts](../../ci/pipelines/job_artifacts.md) or pushes them to the [Container Registry](../../user/packages/container_registry/index.md)
+of the project where the pipeline is executed. These archives can be transferred to another location
+and [loaded](https://docs.docker.com/engine/reference/commandline/load/) in a Docker daemon.
+This method requires a GitLab Runner with access to both `gitlab.com` (including
+`registry.gitlab.com`) and the local offline instance. This runner must run in
+[privileged mode](https://docs.gitlab.com/runner/executors/docker.html#use-docker-in-docker-with-privileged-mode)
+to be able to use the `docker` command inside the jobs. This runner can be installed in a DMZ or on
+a bastion, and used only for this specific project.
+
+#### Scheduling the updates
+
+By default, this project's pipeline will run only once, when the `.gitlab-ci.yml` is added to the
+repo. To update the GitLab security scanners and signatures, it's necessary to run this pipeline
+regularly. GitLab provides a way to [schedule pipelines](../../ci/pipelines/schedules.md). For
+example, you can set this up to download and store the Docker images every week.
+
+Some images can be updated more frequently than others. For example, the [vulnerability database](https://hub.docker.com/r/arminc/clair-db/tags)
+for Container Scanning is updated daily. To update this single image, create a new Scheduled
+Pipeline that runs daily and set `SECURE_BINARIES_ANALYZERS` to `clair-vulnerabilities-db`. Only
+this job will be triggered, and the image will be updated daily and made available in the project
+registry.
+
+#### Using the secure bundle created
+
+The project using the `Secure-Binaries.gitlab-ci.yml` template should now host all the required
+images and resources needed to run GitLab Security features.
+
+The next step is to tell the offline instance to use these resources instead of the default ones on
+`gitlab.com`. This can be done by setting the right environment variables:
+`SAST_ANALYZER_IMAGE_PREFIX` for SAST analyzers, `DS_ANALYZER_IMAGE_PREFIX` for Dependency Scanning,
+and so on.
+
+You can set these variables in the project's `.gitlab-ci.yml` files by using the bundle directly, or
+in the GitLab UI at the project or group level. See the [GitLab CI/CD environment variables page](../../ci/variables/README.md#custom-environment-variables)
+for more information.
+
+#### Variables
+
+The following table shows which variables you can use with the `Secure-Binaries.gitlab-ci.yml`
+template:
+
+| VARIABLE | Description | Default value |
+|-------------------------------------------|-----------------------------------------------|-----------------------------------|
+| `SECURE_BINARIES_ANALYZERS` | Comma-separated list of analyzers to download | `"bandit, brakeman, gosec, and so on..."` |
+| `SECURE_BINARIES_DOWNLOAD_IMAGES` | Used to disable jobs | `"true"` |
+| `SECURE_BINARIES_PUSH_IMAGES` | Push files to the project registry | `"true"` |
+| `SECURE_BINARIES_SAVE_ARTIFACTS` | Also save image archives as artifacts | `"false"` |
+| `SECURE_BINARIES_ANALYZER_VERSION` | Default analyzer version (docker tag) | `"2"` |
+
+### Alternate way without the official template
+
+If it's not possible to follow the above method, the images can be transferred manually instead:
+
+#### Example image packager script
+
+```shell
#!/bin/bash
set -ux
@@ -49,12 +115,12 @@ do
done
```
-### Example image loader script
+#### Example image loader script
This example loads the images from a bastion host to an offline host. In certain configurations,
physical media may be needed for such a transfer:
-```sh
+```shell
#!/bin/bash
set -ux
diff --git a/doc/topics/authentication/index.md b/doc/topics/authentication/index.md
index 429658984ab..e4b86a39385 100644
--- a/doc/topics/authentication/index.md
+++ b/doc/topics/authentication/index.md
@@ -38,7 +38,8 @@ This page gathers all the resources for the topic **Authentication** within GitL
## API
- [OAuth 2 Tokens](../../api/README.md#oauth2-tokens)
-- [Personal access tokens](../../api/README.md#personal-access-tokens)
+- [Personal access tokens](../../api/README.md#personalproject-access-tokens)
+- [Project access tokens](../../api/README.md#personalproject-access-tokens) **(CORE ONLY)**
- [Impersonation tokens](../../api/README.md#impersonation-tokens)
- [GitLab as an OAuth2 provider](../../api/oauth2.md#gitlab-as-an-oauth2-provider)
diff --git a/doc/topics/autodevops/customize.md b/doc/topics/autodevops/customize.md
index 7c587ad3444..056b4c1caf4 100644
--- a/doc/topics/autodevops/customize.md
+++ b/doc/topics/autodevops/customize.md
@@ -2,7 +2,7 @@
While [Auto DevOps](index.md) provides great defaults to get you started, you can customize
almost everything to fit your needs. Auto DevOps offers everything from custom
-[buildpacks](#custom-buildpacks), to [`Dockerfiles](#custom-dockerfile), and
+[buildpacks](#custom-buildpacks), to [Dockerfiles](#custom-dockerfile), and
[Helm charts](#custom-helm-chart). You can even copy the complete
[CI/CD configuration](#customizing-gitlab-ciyml) into your project to enable
staging and canary deployments, and more.
@@ -146,7 +146,7 @@ to override the default chart values by setting `HELM_UPGRADE_EXTRA_ARGS` to `--
## Custom Helm chart per environment
You can specify the use of a custom Helm chart per environment by scoping the environment variable
-to the desired environment. See [Limiting environment scopes of variables](../../ci/variables/README.md#limiting-environment-scopes-of-environment-variables).
+to the desired environment. See [Limiting environment scopes of variables](../../ci/variables/README.md#limit-the-environment-scopes-of-environment-variables).
## Customizing `.gitlab-ci.yml`
@@ -179,7 +179,7 @@ into your project and edit it as needed.
For clusters not managed by GitLab, you can customize the namespace in
`.gitlab-ci.yml` by specifying
-[`environment:kubernetes:namespace`](../../ci/environments.md#configuring-kubernetes-deployments).
+[`environment:kubernetes:namespace`](../../ci/environments/index.md#configuring-kubernetes-deployments).
For example, the following configuration overrides the namespace used for
`production` deployments:
@@ -227,6 +227,8 @@ If your `.gitlab-ci.yml` extends these Auto DevOps templates and override the `o
`except` keywords, you must migrate your templates to use the
[`rules`](../../ci/yaml/README.md#rules) syntax after the
base template is migrated to use the `rules` syntax.
+For users who cannot migrate just yet, you can alternatively pin your templates to
+the [GitLab 12.10 based templates](https://gitlab.com/gitlab-org/auto-devops-v12-10).
## PostgreSQL database support
@@ -243,23 +245,19 @@ postgres://user:password@postgres-host:postgres-port/postgres-database
CAUTION: **Deprecation**
The variable `AUTO_DEVOPS_POSTGRES_CHANNEL` that controls default provisioned
-PostgreSQL currently defaults to `1`. This value is scheduled to change to `2` in
-[GitLab 13.0](https://gitlab.com/gitlab-org/gitlab/-/issues/210499).
+PostgreSQL was changed to `2` in [GitLab 13.0](https://gitlab.com/gitlab-org/gitlab/-/issues/210499).
+To keep using the old PostgreSQL, set the `AUTO_DEVOPS_POSTGRES_CHANNEL` variable to
+`1`.
The version of the chart used to provision PostgreSQL:
+- Is 8.2.1 in GitLab 13.0 and later, but can be set back to 0.7.1 if needed.
+- Can be set to from 0.7.1 to 8.2.1 in GitLab 12.9 and 12.10.
- Is 0.7.1 in GitLab 12.8 and earlier.
-- Can be set to from 0.7.1 to 8.2.1 in GitLab 12.9 and later.
GitLab encourages users to [migrate their database](upgrading_postgresql.md)
to the newer PostgreSQL.
-To use the new PostgreSQL:
-
-- New projects can set the `AUTO_DEVOPS_POSTGRES_CHANNEL` variable to `2`.
-- Old projects can be upgraded by following the guide to
- [upgrading PostgresSQL](upgrading_postgresql.md).
-
### Using external PostgreSQL database providers
While Auto DevOps provides out-of-the-box support for a PostgreSQL container for
@@ -271,10 +269,9 @@ You must define environment-scoped variables for `POSTGRES_ENABLED` and
`DATABASE_URL` in your project's CI/CD settings:
1. Disable the built-in PostgreSQL installation for the required environments using
- scoped [environment variables](../../ci/environments.md#scoping-environments-with-specs).
+ scoped [environment variables](../../ci/environments/index.md#scoping-environments-with-specs).
For this use case, it's likely that only `production` will need to be added to this
- list. The built-in PostgreSQL setup for Review Apps and staging is sufficient,
- because a high availability setup is not required.
+ list. The built-in PostgreSQL setup for Review Apps and staging is sufficient.
![Auto Metrics](img/disable_postgres.png)
@@ -303,6 +300,7 @@ applications.
|-----------------------------------------|------------------------------------|
| `ADDITIONAL_HOSTS` | Fully qualified domain names specified as a comma-separated list that are added to the Ingress hosts. |
| `<ENVIRONMENT>_ADDITIONAL_HOSTS` | For a specific environment, the fully qualified domain names specified as a comma-separated list that are added to the Ingress hosts. This takes precedence over `ADDITIONAL_HOSTS`. |
+| `AUTO_DEVOPS_ATOMIC_RELEASE` | As of GitLab 13.0, Auto DevOps uses [`--atomic`](https://v2.helm.sh/docs/helm/#options-43) for Helm deployments by default. Set this variable to `false` to disable the use of `--atomic` |
| `AUTO_DEVOPS_BUILD_IMAGE_CNB_ENABLED` | When set to a non-empty value and no `Dockerfile` is present, Auto Build builds your application using Cloud Native Buildpacks instead of Herokuish. [More details](stages.md#auto-build-using-cloud-native-buildpacks-beta). |
| `AUTO_DEVOPS_BUILD_IMAGE_EXTRA_ARGS` | Extra arguments to be passed to the `docker build` command. Note that using quotes won't prevent word splitting. [More details](#passing-arguments-to-docker-build). |
| `AUTO_DEVOPS_BUILD_IMAGE_FORWARDED_CI_VARIABLES` | A [comma-separated list of CI variable names](#passing-secrets-to-docker-build) to be passed to the `docker build` command as secrets. |
@@ -318,7 +316,7 @@ applications.
| `CANARY_REPLICAS` | Number of canary replicas to deploy for [Canary Deployments](../../user/project/canary_deployments.md). Defaults to 1. |
| `HELM_RELEASE_NAME` | From GitLab 12.1, allows the `helm` release name to be overridden. Can be used to assign unique release names when deploying multiple projects to a single namespace. |
| `HELM_UPGRADE_VALUES_FILE` | From GitLab 12.6, allows the `helm upgrade` values file to be overridden. Defaults to `.gitlab/auto-deploy-values.yaml`. |
-| `HELM_UPGRADE_EXTRA_ARGS` | From GitLab 11.11, allows extra arguments in `helm` commands when deploying the application. Note that using quotes won't prevent word splitting. **Tip:** you can use this variable to [customize the Auto Deploy Helm chart](#custom-helm-chart) by applying custom override values with `--values my-values.yaml`. |
+| `HELM_UPGRADE_EXTRA_ARGS` | From GitLab 11.11, allows extra arguments in `helm` commands when deploying the application. Note that using quotes won't prevent word splitting. |
| `INCREMENTAL_ROLLOUT_MODE` | From GitLab 11.4, if present, can be used to enable an [incremental rollout](#incremental-rollout-to-production-premium) of your application for the production environment. Set to `manual` for manual deployment jobs or `timed` for automatic rollout deployments with a 5 minute delay each one. |
| `K8S_SECRET_*` | From GitLab 11.7, any variable prefixed with [`K8S_SECRET_`](#application-secret-variables) will be made available by Auto DevOps as environment variables to the deployed application. |
| `KUBE_INGRESS_BASE_DOMAIN` | From GitLab 11.8, can be used to set a domain per cluster. See [cluster domains](../../user/project/clusters/index.md#base-domain) for more information. |
@@ -329,9 +327,9 @@ applications.
| `STAGING_ENABLED` | From GitLab 10.8, used to define a [deploy policy for staging and production environments](#deploy-policy-for-staging-and-production-environments). |
TIP: **Tip:**
-Set up the replica variables using a
-[project variable](../../ci/variables/README.md#gitlab-cicd-environment-variables)
-and scale your application by only redeploying it.
+After you set up your replica variables using a
+[project variable](../../ci/variables/README.md#gitlab-cicd-environment-variables),
+you can scale your application by redeploying it.
CAUTION: **Caution:**
You should *not* scale your application using Kubernetes directly. This can
@@ -350,15 +348,7 @@ The following table lists variables related to the database.
| `POSTGRES_USER` | The PostgreSQL user. Defaults to `user`. Set it to use a custom username. |
| `POSTGRES_PASSWORD` | The PostgreSQL password. Defaults to `testing-password`. Set it to use a custom password. |
| `POSTGRES_DB` | The PostgreSQL database name. Defaults to the value of [`$CI_ENVIRONMENT_SLUG`](../../ci/variables/README.md#predefined-environment-variables). Set it to use a custom database name. |
-| `POSTGRES_VERSION` | Tag for the [`postgres` Docker image](https://hub.docker.com/_/postgres) to use. Defaults to `9.6.2`. |
-
-### Security tools
-
-The following table lists variables related to security tools.
-
-| **Variable** | **Description** |
-|-----------------------------------------|------------------------------------|
-| `SAST_CONFIDENCE_LEVEL` | Minimum confidence level of security issues you want to be reported; `1` for Low, `2` for Medium, `3` for High. Defaults to `3`. |
+| `POSTGRES_VERSION` | Tag for the [`postgres` Docker image](https://hub.docker.com/_/postgres) to use. Defaults to `9.6.16` for tests and deployments as of GitLab 13.0 (previously `9.6.2`). If `AUTO_DEVOPS_POSTGRES_CHANNEL` is set to `1`, deployments will use the default version `9.6.2`. |
### Disable jobs
@@ -544,7 +534,7 @@ required to go from `10%` to `100%`, you can jump to whatever job you want.
You can also scale down by running a lower percentage job, just before hitting
`100%`. Once you get to `100%`, you can't scale down, and you'd have to roll
back by redeploying the old version using the
-[rollback button](../../ci/environments.md#retrying-and-rolling-back) in the
+[rollback button](../../ci/environments/index.md#retrying-and-rolling-back) in the
environment page.
Below, you can see how the pipeline will look if the rollout or staging
diff --git a/doc/topics/autodevops/index.md b/doc/topics/autodevops/index.md
index 7ed6625bea3..e7165136cf0 100644
--- a/doc/topics/autodevops/index.md
+++ b/doc/topics/autodevops/index.md
@@ -3,17 +3,25 @@
> - [Introduced](https://gitlab.com/gitlab-org/gitlab-foss/issues/37115) in GitLab 10.0.
> - Generally available on GitLab 11.0.
-Auto DevOps provides pre-defined CI/CD configuration which allows you to automatically detect, build, test,
-deploy, and monitor your applications. Leveraging CI/CD best practices and tools, Auto DevOps aims
-to simplify the setup and execution of a mature & modern software development lifecycle.
+Auto DevOps provides pre-defined CI/CD configuration allowing you to automatically
+detect, build, test, deploy, and monitor your applications. Leveraging CI/CD
+best practices and tools, Auto DevOps aims to simplify the setup and execution
+of a mature and modern software development lifecycle.
## Overview
-With Auto DevOps, the software development process becomes easier to set up
-as every project can have a complete workflow from verification to monitoring
-with minimal configuration. Just push your code and GitLab takes
-care of everything else. This makes it easier to start new projects and brings
-consistency to how applications are set up throughout a company.
+You can spend a lot of effort to set up the workflow and processes required to
+build, deploy, and monitor your project. It gets worse when your company has
+hundreds, if not thousands, of projects to maintain. With new projects
+constantly starting up, the entire software development process becomes
+impossibly complex to manage.
+
+Auto DevOps provides you a seamless software development process by
+automatically detecting all dependencies and language technologies required to
+test, build, package, deploy, and monitor every project with minimal
+configuration. Automation enables consistency across your projects, seamless
+management of processes, and faster creation of new projects: push your code,
+and GitLab does the rest, improving your productivity and efficiency.
For an introduction to Auto DevOps, watch [AutoDevOps in GitLab 11.0](https://youtu.be/0Tc0YYBxqi4).
@@ -21,14 +29,14 @@ For an introduction to Auto DevOps, watch [AutoDevOps in GitLab 11.0](https://yo
> [Introduced](https://gitlab.com/gitlab-org/gitlab-foss/issues/41729) in GitLab 11.3.
-Auto DevOps is enabled by default for all projects and will attempt to run on all pipelines
-in each project. This default can be enabled or disabled by an instance administrator in the
+Auto DevOps is enabled by default for all projects and attempts to run on all pipelines
+in each project. An instance administrator can enable or disable this default in the
[Auto DevOps settings](../../user/admin_area/settings/continuous_integration.md#auto-devops-core-only).
-It will be automatically disabled in individual projects on their first pipeline failure,
+Auto DevOps automatically disables in individual projects on their first pipeline failure,
if it has not been explicitly enabled for the project.
Since [GitLab 12.7](https://gitlab.com/gitlab-org/gitlab/issues/26655), Auto DevOps
-will run on pipelines automatically only if a [`Dockerfile` or matching buildpack](stages.md#auto-build)
+runs on pipelines automatically only if a [`Dockerfile` or matching buildpack](stages.md#auto-build)
exists.
If a [CI/CD configuration file](../../ci/yaml/README.md) is present in the project,
@@ -36,18 +44,21 @@ it will continue to be used, whether or not Auto DevOps is enabled.
## Quick start
-If you are using GitLab.com, see the [quick start guide](quick_start_guide.md)
-for how to use Auto DevOps with GitLab.com and a Kubernetes cluster on Google Kubernetes
+If you're using GitLab.com, see the [quick start guide](quick_start_guide.md)
+for setting up Auto DevOps with GitLab.com and a Kubernetes cluster on Google Kubernetes
Engine (GKE).
-If you are using a self-managed instance of GitLab, you will need to configure the
+If you use a self-managed instance of GitLab, you must configure the
[Google OAuth2 OmniAuth Provider](../../integration/google.md) before
-you can configure a cluster on GKE. Once this is set up, you can follow the steps on the
-[quick start guide](quick_start_guide.md) to get started.
+configuring a cluster on GKE. After configuring the provider, you can follow
+the steps in the [quick start guide](quick_start_guide.md) to get started.
+
+In [GitLab 13.0](https://gitlab.com/gitlab-org/gitlab/-/issues/208132) and later, it is
+possible to leverage Auto DevOps to deploy to [AWS ECS](#aws-ecs).
## Comparison to application platforms and PaaS
-Auto DevOps provides functionality that is often included in an application
+Auto DevOps provides features often included in an application
platform or a Platform as a Service (PaaS). It takes inspiration from the
innovative work done by [Heroku](https://www.heroku.com/) and goes beyond it
in multiple ways:
@@ -60,7 +71,7 @@ in multiple ways:
- Auto DevOps has more features including security testing, performance testing,
and code quality testing.
- Auto DevOps offers an incremental graduation path. If you need advanced customizations,
- you can start modifying the templates without having to start over on a
+ you can start modifying the templates without starting over on a
completely different platform. Review the [customizing](customize.md) documentation for more information.
## Features
@@ -81,7 +92,7 @@ project in a simple and automatic way:
1. [Auto Browser Performance Testing](stages.md#auto-browser-performance-testing-premium) **(PREMIUM)**
1. [Auto Monitoring](stages.md#auto-monitoring)
-As Auto DevOps relies on many different components, it's good to have a basic
+As Auto DevOps relies on many different components, you should have a basic
knowledge of the following:
- [Kubernetes](https://kubernetes.io/docs/home/)
@@ -102,101 +113,137 @@ Auto DevOps.
## Requirements
-To make full use of Auto DevOps, you will need:
+### Kubernetes
+
+To make full use of Auto DevOps with Kubernetes, you need:
-- **Kubernetes** (for Auto Review Apps, Auto Deploy, and Auto Monitoring)
+- **Kubernetes** (for [Auto Review Apps](stages.md#auto-review-apps),
+ [Auto Deploy](stages.md#auto-deploy), and [Auto Monitoring](stages.md#auto-monitoring))
- To enable deployments, you will need:
+ To enable deployments, you need:
- 1. A [Kubernetes 1.12+ cluster](../../user/project/clusters/index.md) for the project. The easiest
- way is to create a [new cluster using the GitLab UI](../../user/project/clusters/add_remove_clusters.md#create-new-cluster).
- For Kubernetes 1.16+ clusters, there is some additional configuration for [Auto Deploy for Kubernetes 1.16+](stages.md#kubernetes-116).
+ 1. A [Kubernetes 1.12+ cluster](../../user/project/clusters/index.md) for your
+ project. The easiest way is to create a
+ [new cluster using the GitLab UI](../../user/project/clusters/add_remove_clusters.md#create-new-cluster).
+ For Kubernetes 1.16+ clusters, you must perform additional configuration for
+ [Auto Deploy for Kubernetes 1.16+](stages.md#kubernetes-116).
1. NGINX Ingress. You can deploy it to your Kubernetes cluster by installing
the [GitLab-managed app for Ingress](../../user/clusters/applications.md#ingress),
- once you have configured GitLab's Kubernetes integration in the previous step.
+ after configuring GitLab's Kubernetes integration in the previous step.
Alternatively, you can use the
[`nginx-ingress`](https://github.com/helm/charts/tree/master/stable/nginx-ingress)
Helm chart to install Ingress manually.
NOTE: **Note:**
- If you are using your own Ingress instead of the one provided by GitLab's managed
- apps, ensure you are running at least version 0.9.0 of NGINX Ingress and
+ If you use your own Ingress instead of the one provided by GitLab's managed
+ apps, ensure you're running at least version 0.9.0 of NGINX Ingress and
[enable Prometheus metrics](https://github.com/helm/charts/tree/master/stable/nginx-ingress#prometheus-metrics)
- in order for the response metrics to appear. You will also have to
+ for the response metrics to appear. You must also
[annotate](https://kubernetes.io/docs/concepts/overview/working-with-objects/annotations/)
the NGINX Ingress deployment to be scraped by Prometheus using
`prometheus.io/scrape: "true"` and `prometheus.io/port: "10254"`.
-- **Base domain** (for Auto Review Apps, Auto Deploy, and Auto Monitoring)
+- **Base domain** (for [Auto Review Apps](stages.md#auto-review-apps),
+ [Auto Deploy](stages.md#auto-deploy), and [Auto Monitoring](stages.md#auto-monitoring))
- You will need a domain configured with wildcard DNS which is going to be used
- by all of your Auto DevOps applications. If you're using the
+ You need a domain configured with wildcard DNS, which all of your Auto DevOps
+ applications will use. If you're using the
[GitLab-managed app for Ingress](../../user/clusters/applications.md#ingress),
- the URL endpoint will be automatically configured for you.
+ the URL endpoint is automatically configured for you.
- You will then need to [specify the Auto DevOps base domain](#auto-devops-base-domain).
+ You must also [specify the Auto DevOps base domain](#auto-devops-base-domain).
- **GitLab Runner** (for all stages)
- Your Runner needs to be configured to be able to run Docker. Generally this
- means using either the [Docker](https://docs.gitlab.com/runner/executors/docker.html)
+ Your Runner must be configured to run Docker, usually with either the
+ [Docker](https://docs.gitlab.com/runner/executors/docker.html)
or [Kubernetes](https://docs.gitlab.com/runner/executors/kubernetes.html) executors, with
[privileged mode enabled](https://docs.gitlab.com/runner/executors/docker.html#use-docker-in-docker-with-privileged-mode).
- The Runners do not need to be installed in the Kubernetes cluster, but the
- Kubernetes executor is easy to use and is automatically autoscaling.
- Docker-based Runners can be configured to autoscale as well, using [Docker
- Machine](https://docs.gitlab.com/runner/install/autoscaling.html).
+ The Runners don't need to be installed in the Kubernetes cluster, but the
+ Kubernetes executor is easy to use and automatically autoscales.
+ You can configure Docker-based Runners to autoscale as well, using
+ [Docker Machine](https://docs.gitlab.com/runner/install/autoscaling.html).
- If you have configured GitLab's Kubernetes integration in the first step, you
+ If you've configured GitLab's Kubernetes integration in the first step, you
can deploy it to your cluster by installing the
[GitLab-managed app for GitLab Runner](../../user/clusters/applications.md#gitlab-runner).
Runners should be registered as [shared Runners](../../ci/runners/README.md#registering-a-shared-runner)
for the entire GitLab instance, or [specific Runners](../../ci/runners/README.md#registering-a-specific-runner)
- that are assigned to specific projects (the default if you have installed the
+ that are assigned to specific projects (the default if you've installed the
GitLab Runner managed application).
-- **Prometheus** (for Auto Monitoring)
+- **Prometheus** (for [Auto Monitoring](stages.md#auto-monitoring))
- To enable Auto Monitoring, you will need Prometheus installed somewhere
- (inside or outside your cluster) and configured to scrape your Kubernetes cluster.
- If you have configured GitLab's Kubernetes integration, you can deploy it to
+ To enable Auto Monitoring, you need Prometheus installed either inside or
+ outside your cluster, and configured to scrape your Kubernetes cluster.
+ If you've configured GitLab's Kubernetes integration, you can deploy it to
your cluster by installing the
[GitLab-managed app for Prometheus](../../user/clusters/applications.md#prometheus).
The [Prometheus service](../../user/project/integrations/prometheus.md)
- integration needs to be enabled for the project (or enabled as a
+ integration must be enabled for the project, or enabled as a
[default service template](../../user/project/integrations/services_templates.md)
- for the entire GitLab installation).
+ for the entire GitLab installation.
- To get response metrics (in addition to system metrics), you need to
+ To get response metrics (in addition to system metrics), you must
[configure Prometheus to monitor NGINX](../../user/project/integrations/prometheus_library/nginx_ingress.md#configuring-nginx-ingress-monitoring).
- **cert-manager** (optional, for TLS/HTTPS)
- To enable HTTPS endpoints for your application, you need to install cert-manager,
- a native Kubernetes certificate management controller that helps with issuing certificates.
- Installing cert-manager on your cluster will issue a certificate by
- [Let’s Encrypt](https://letsencrypt.org/) and ensure that certificates are valid and up-to-date.
- If you have configured GitLab's Kubernetes integration, you can deploy it to
- your cluster by installing the
+ To enable HTTPS endpoints for your application, you must install cert-manager,
+ a native Kubernetes certificate management controller that helps with issuing
+ certificates. Installing cert-manager on your cluster issues a
+ [Let’s Encrypt](https://letsencrypt.org/) certificate and ensures the
+ certificates are valid and up-to-date. If you've configured GitLab's Kubernetes
+ integration, you can deploy it to your cluster by installing the
[GitLab-managed app for cert-manager](../../user/clusters/applications.md#cert-manager).
-If you do not have Kubernetes or Prometheus installed, then Auto Review Apps,
-Auto Deploy, and Auto Monitoring will be silently skipped.
+If you don't have Kubernetes or Prometheus installed, then
+[Auto Review Apps](stages.md#auto-review-apps),
+[Auto Deploy](stages.md#auto-deploy), and [Auto Monitoring](stages.md#auto-monitoring)
+are skipped.
+
+After all requirements are met, you can [enable Auto DevOps](#enablingdisabling-auto-devops).
+
+### AWS ECS
+
+> [Introduced](https://gitlab.com/gitlab-org/gitlab/-/issues/208132) in GitLab 13.0.
+
+You can choose to target [AWS ECS](../../ci/cloud_deployment/index.md) as a deployment platform instead of using Kubernetes.
+
+To get started on Auto DevOps to ECS, you'll have to add a specific Environment
+Variable. To do so, follow these steps:
-One all requirements are met, you can go ahead and [enable Auto DevOps](#enablingdisabling-auto-devops).
+1. In your project, go to **Settings > CI / CD** and expand the **Variables**
+ section.
+
+1. Specify which AWS platform to target during the Auto DevOps deployment
+ by adding the `AUTO_DEVOPS_PLATFORM_TARGET` variable.
+
+1. Give this variable the value `ECS` before saving it.
+
+When you trigger a pipeline, if you have AutoDev Ops enabled and if you have correctly
+[entered AWS credentials as environment variables](../../ci/cloud_deployment/index.md#deploy-your-application-to-aws-elastic-container-service-ecs),
+your application will be deployed to AWS ECS.
+
+NOTE: **Note:**
+If you have both a valid `AUTO_DEVOPS_PLATFORM_TARGET` variable and a Kubernetes cluster tied to your project,
+only the deployment to Kubernetes will run.
## Auto DevOps base domain
-The Auto DevOps base domain is required if you want to make use of
+The Auto DevOps base domain is required to use
[Auto Review Apps](stages.md#auto-review-apps), [Auto Deploy](stages.md#auto-deploy), and
-[Auto Monitoring](stages.md#auto-monitoring). It can be defined in any of the following
-places:
-
-- either under the cluster's settings, whether for [projects](../../user/project/clusters/index.md#base-domain) or [groups](../../user/group/clusters/index.md#base-domain)
-- or in instance-wide settings in the **Admin Area > Settings** under the "Continuous Integration and Delivery" section
+[Auto Monitoring](stages.md#auto-monitoring). You can define the base domain in
+any of the following places:
+
+- either under the cluster's settings, whether for
+ [projects](../../user/project/clusters/index.md#base-domain) or
+ [groups](../../user/group/clusters/index.md#base-domain)
+- or in instance-wide settings in **{admin}** **Admin Area > Settings** under the
+ **Continuous Integration and Delivery** section
- or at the project level as a variable: `KUBE_INGRESS_BASE_DOMAIN`
- or at the group level as a variable: `KUBE_INGRESS_BASE_DOMAIN`.
@@ -204,55 +251,57 @@ The base domain variable `KUBE_INGRESS_BASE_DOMAIN` follows the same order of pr
as other environment [variables](../../ci/variables/README.md#priority-of-environment-variables).
TIP: **Tip:**
-If you're using the [GitLab managed app for Ingress](../../user/clusters/applications.md#ingress),
-the URL endpoint should be automatically configured for you. All you have to do
+If you use the [GitLab managed app for Ingress](../../user/clusters/applications.md#ingress),
+the URL endpoint should be automatically configured for you. All you must do
is use its value for the `KUBE_INGRESS_BASE_DOMAIN` variable.
NOTE: **Note:**
`AUTO_DEVOPS_DOMAIN` was [deprecated in GitLab 11.8](https://gitlab.com/gitlab-org/gitlab-foss/issues/52363)
-and replaced with `KUBE_INGRESS_BASE_DOMAIN`. It was removed in
+and replaced with `KUBE_INGRESS_BASE_DOMAIN`, and removed in
[GitLab 12.0](https://gitlab.com/gitlab-org/gitlab-foss/issues/56959).
-A wildcard DNS A record matching the base domain(s) is required, for example,
-given a base domain of `example.com`, you'd need a DNS entry like:
+Auto DevOps requires a wildcard DNS A record matching the base domain(s). For
+a base domain of `example.com`, you'd need a DNS entry like:
-```text
+```plaintext
*.example.com 3600 A 1.2.3.4
```
-In this case, `example.com` is the domain name under which the deployed apps will be served,
-and `1.2.3.4` is the IP address of your load balancer; generally NGINX
-([see requirements](#requirements)). How to set up the DNS record is beyond
-the scope of this document; you should check with your DNS provider.
+In this case, the deployed applications are served from `example.com`, and `1.2.3.4`
+is the IP address of your load balancer; generally NGINX ([see requirements](#requirements)).
+Setting up the DNS record is beyond the scope of this document; check with your
+DNS provider for information.
-Alternatively you can use free public services like [nip.io](https://nip.io)
-which provide automatic wildcard DNS without any configuration. Just set the
-Auto DevOps base domain to `1.2.3.4.nip.io`.
+Alternatively, you can use free public services like [nip.io](https://nip.io)
+which provide automatic wildcard DNS without any configuration. For [nip.io](https://nip.io),
+set the Auto DevOps base domain to `1.2.3.4.nip.io`.
-Once set up, all requests will hit the load balancer, which in turn will route
-them to the Kubernetes pods that run your application(s).
+After completing setup, all requests hit the load balancer, which routes requests
+to the Kubernetes pods running your application.
## Enabling/Disabling Auto DevOps
-When first using Auto DevOps, review the [requirements](#requirements) to ensure all necessary components to make
-full use of Auto DevOps are available. If this is your fist time, we recommend you follow the
-[quick start guide](quick_start_guide.md).
+When first using Auto DevOps, review the [requirements](#requirements) to ensure
+all the necessary components to make full use of Auto DevOps are available. First-time
+users should follow the [quick start guide](quick_start_guide.md).
-GitLab.com users can enable/disable Auto DevOps at the project-level only. Self-managed users
-can enable/disable Auto DevOps at the project-level, group-level or instance-level.
+GitLab.com users can enable or disable Auto DevOps only at the project level.
+Self-managed users can enable or disable Auto DevOps at the project, group, or
+instance level.
### At the project level
-If enabling, check that your project doesn't have a `.gitlab-ci.yml`, or if one exists, remove it.
+If enabling, check that your project does not have a `.gitlab-ci.yml`, or if one exists, remove it.
-1. Go to your project's **Settings > CI/CD > Auto DevOps**.
-1. Toggle the **Default to Auto DevOps pipeline** checkbox (checked to enable, unchecked to disable)
-1. When enabling, it's optional but recommended to add in the [base domain](#auto-devops-base-domain)
- that will be used by Auto DevOps to [deploy your application](stages.md#auto-deploy)
+1. Go to your project's **{settings}** **Settings > CI/CD > Auto DevOps**.
+1. Select the **Default to Auto DevOps pipeline** checkbox to enable it.
+1. (Optional, but recommended) When enabling, you can add in the
+ [base domain](#auto-devops-base-domain) Auto DevOps uses to
+ [deploy your application](stages.md#auto-deploy),
and choose the [deployment strategy](#deployment-strategy).
1. Click **Save changes** for the changes to take effect.
-When the feature has been enabled, an Auto DevOps pipeline is triggered on the default branch.
+After enabling the feature, an Auto DevOps pipeline is triggered on the default branch.
### At the group level
@@ -260,48 +309,50 @@ When the feature has been enabled, an Auto DevOps pipeline is triggered on the d
Only administrators and group owners can enable or disable Auto DevOps at the group level.
-To enable or disable Auto DevOps at the group-level:
+When enabling or disabling Auto DevOps at group level, group configuration is
+implicitly used for the subgroups and projects inside that group, unless Auto DevOps
+is specifically enabled or disabled on the subgroup or project.
-1. Go to group's **Settings > CI/CD > Auto DevOps** page.
-1. Toggle the **Default to Auto DevOps pipeline** checkbox (checked to enable, unchecked to disable).
-1. Click **Save changes** button for the changes to take effect.
+To enable or disable Auto DevOps at the group level:
-When enabling or disabling Auto DevOps at group-level, group configuration will be implicitly used for
-the subgroups and projects inside that group, unless Auto DevOps is specifically enabled or disabled on
-the subgroup or project.
+1. Go to your group's **{settings}** **Settings > CI/CD > Auto DevOps** page.
+1. Select the **Default to Auto DevOps pipeline** checkbox to enable it.
+1. Click **Save changes** for the changes to take effect.
### At the instance level (Administrators only)
Even when disabled at the instance level, group owners and project maintainers can still enable
Auto DevOps at the group and project level, respectively.
-1. Go to **Admin Area > Settings > Continuous Integration and Deployment**.
-1. Toggle the checkbox labeled **Default to Auto DevOps pipeline for all projects**.
-1. If enabling, optionally set up the Auto DevOps [base domain](#auto-devops-base-domain) which will be used for Auto Deploy and Auto Review Apps.
+1. Go to **{admin}** **Admin Area > Settings > Continuous Integration and Deployment**.
+1. Select **Default to Auto DevOps pipeline for all projects** to enable it.
+1. (Optional) You can set up the Auto DevOps [base domain](#auto-devops-base-domain),
+ for Auto Deploy and Auto Review Apps to use.
1. Click **Save changes** for the changes to take effect.
### Enable for a percentage of projects
-There is also a feature flag to enable Auto DevOps by default to your chosen percentage of projects.
+You can use a feature flag to enable Auto DevOps by default to your desired percentage
+of projects. From the console, enter the following command, replacing `10` with
+your desired percentage:
-This can be enabled from the console with the following, which uses the example of 10%:
-
-`Feature.get(:force_autodevops_on_by_default).enable_percentage_of_actors(10)`
+```ruby
+Feature.get(:force_autodevops_on_by_default).enable_percentage_of_actors(10)
+```
### Deployment strategy
> [Introduced](https://gitlab.com/gitlab-org/gitlab-foss/issues/38542) in GitLab 11.0.
You can change the deployment strategy used by Auto DevOps by going to your
-project's **Settings > CI/CD > Auto DevOps**.
-
-The available options are:
+project's **{settings}** **Settings > CI/CD > Auto DevOps**. The following options
+are available:
- **Continuous deployment to production**: Enables [Auto Deploy](stages.md#auto-deploy)
with `master` branch directly deployed to production.
- **Continuous deployment to production using timed incremental rollout**: Sets the
[`INCREMENTAL_ROLLOUT_MODE`](customize.md#timed-incremental-rollout-to-production-premium) variable
- to `timed`, and production deployment will be executed with a 5 minute delay between
+ to `timed`. Production deployments execute with a 5 minute delay between
each increment in rollout.
- **Automatic deployment to staging, manual deployment to production**: Sets the
[`STAGING_ENABLED`](customize.md#deploy-policy-for-staging-and-production-environments) and
@@ -313,63 +364,61 @@ The available options are:
## Using multiple Kubernetes clusters **(PREMIUM)**
-When using Auto DevOps, you may want to deploy different environments to
-different Kubernetes clusters. This is possible due to the 1:1 connection that
-[exists between them](../../user/project/clusters/index.md#multiple-kubernetes-clusters-premium).
+When using Auto DevOps, you can deploy different environments to
+different Kubernetes clusters, due to the 1:1 connection
+[existing between them](../../user/project/clusters/index.md#multiple-kubernetes-clusters-premium).
-In the [Auto DevOps template](https://gitlab.com/gitlab-org/gitlab/blob/master/lib/gitlab/ci/templates/Auto-DevOps.gitlab-ci.yml) (used behind the scenes by Auto DevOps), there
-are currently 3 defined environment names that you need to know:
+The [template](https://gitlab.com/gitlab-org/gitlab/blob/master/lib/gitlab/ci/templates/Auto-DevOps.gitlab-ci.yml)
+used by Auto DevOps currently defines 3 environment names:
- `review/` (every environment starting with `review/`)
- `staging`
- `production`
-Those environments are tied to jobs that use [Auto Deploy](stages.md#auto-deploy), so
-except for the environment scope, they would also need to have a different
-domain they would be deployed to. This is why you need to define a separate
-`KUBE_INGRESS_BASE_DOMAIN` variable for all the above
-[based on the environment](../../ci/variables/README.md#limiting-environment-scopes-of-environment-variables).
+Those environments are tied to jobs using [Auto Deploy](stages.md#auto-deploy), so
+except for the environment scope, they must have a different deployment domain.
+You must define a separate `KUBE_INGRESS_BASE_DOMAIN` variable for each of the above
+[based on the environment](../../ci/variables/README.md#limit-the-environment-scopes-of-environment-variables).
-The following table is an example of how the three different clusters would
-be configured.
+The following table is an example of how to configure the three different clusters:
| Cluster name | Cluster environment scope | `KUBE_INGRESS_BASE_DOMAIN` variable value | Variable environment scope | Notes |
|--------------|---------------------------|-------------------------------------------|----------------------------|---|
-| review | `review/*` | `review.example.com` | `review/*` | The review cluster which will run all [Review Apps](../../ci/review_apps/index.md). `*` is a wildcard, which means it will be used by every environment name starting with `review/`. |
-| staging | `staging` | `staging.example.com` | `staging` | (Optional) The staging cluster which will run the deployments of the staging environments. You need to [enable it first](customize.md#deploy-policy-for-staging-and-production-environments). |
-| production | `production` | `example.com` | `production` | The production cluster which will run the deployments of the production environment. You can use [incremental rollouts](customize.md#incremental-rollout-to-production-premium). |
+| review | `review/*` | `review.example.com` | `review/*` | The review cluster which runs all [Review Apps](../../ci/review_apps/index.md). `*` is a wildcard, used by every environment name starting with `review/`. |
+| staging | `staging` | `staging.example.com` | `staging` | (Optional) The staging cluster which runs the deployments of the staging environments. You must [enable it first](customize.md#deploy-policy-for-staging-and-production-environments). |
+| production | `production` | `example.com` | `production` | The production cluster which runs the production environment deployments. You can use [incremental rollouts](customize.md#incremental-rollout-to-production-premium). |
To add a different cluster for each environment:
-1. Navigate to your project's **Operations > Kubernetes** and create the Kubernetes clusters
- with their respective environment scope as described from the table above.
+1. Navigate to your project's **{cloud-gear}** **Operations > Kubernetes**.
+1. Create the Kubernetes clusters with their respective environment scope, as
+ described from the table above.
![Auto DevOps multiple clusters](img/autodevops_multiple_clusters.png)
-1. After the clusters are created, navigate to each one and install Helm Tiller
+1. After creating the clusters, navigate to each cluster and install Helm Tiller
and Ingress. Wait for the Ingress IP address to be assigned.
-1. Make sure you have [configured your DNS](#auto-devops-base-domain) with the
+1. Make sure you've [configured your DNS](#auto-devops-base-domain) with the
specified Auto DevOps domains.
-1. Navigate to each cluster's page, through **Operations > Kubernetes**,
+1. Navigate to each cluster's page, through **{cloud-gear}** **Operations > Kubernetes**,
and add the domain based on its Ingress IP address.
-Now that all is configured, you can test your setup by creating a merge request
-and verifying that your app is deployed as a review app in the Kubernetes
+After completing configuration, you can test your setup by creating a merge request
+and verifying your application is deployed as a Review App in the Kubernetes
cluster with the `review/*` environment scope. Similarly, you can check the
other environments.
## Currently supported languages
Note that not all buildpacks support Auto Test yet, as it's a relatively new
-enhancement. All of Heroku's [officially supported
-languages](https://devcenter.heroku.com/articles/heroku-ci#currently-supported-languages)
-support it, and some third-party buildpacks as well e.g., Go, Node, Java, PHP,
-Python, Ruby, Gradle, Scala, and Elixir all support Auto Test, but notably the
-multi-buildpack does not.
+enhancement. All of Heroku's
+[officially supported languages](https://devcenter.heroku.com/articles/heroku-ci#supported-languages)
+support Auto Test. The languages supported by Heroku's Herokuish buildpacks all
+support Auto Test, but notably the multi-buildpack does not.
As of GitLab 10.0, the supported buildpacks are:
-```text
+```plaintext
- heroku-buildpack-multi v1.0.0
- heroku-buildpack-ruby v168
- heroku-buildpack-nodejs v99
@@ -385,24 +434,27 @@ As of GitLab 10.0, the supported buildpacks are:
- buildpack-nginx v8
```
+If your application needs a buildpack that is not in the above list, you
+might want to use a [custom buildpack](customize.md#custom-buildpacks).
+
## Limitations
The following restrictions apply.
### Private registry support
-There is no documented way of using private container registry with Auto DevOps.
-We strongly advise using GitLab Container Registry with Auto DevOps in order to
+No documented way of using private container registry with Auto DevOps exists.
+We strongly advise using GitLab Container Registry with Auto DevOps to
simplify configuration and prevent any unforeseen issues.
### Installing Helm behind a proxy
-GitLab does not yet support installing [Helm as a GitLab-managed App](../../user/clusters/applications.md#helm) when
-behind a proxy. Users who wish to do so must inject their proxy settings
-into the installation pods at runtime, for example by using a
+GitLab does not support installing [Helm as a GitLab-managed App](../../user/clusters/applications.md#helm) when
+behind a proxy. Users who want to do so must inject their proxy settings
+into the installation pods at runtime, such as by using a
[`PodPreset`](https://kubernetes.io/docs/concepts/workloads/pods/podpreset/):
-```yml
+```yaml
apiVersion: settings.k8s.io/v1alpha1
kind: PodPreset
metadata:
@@ -418,28 +470,52 @@ spec:
## Troubleshooting
-- Auto Build and Auto Test may fail to detect your language or framework with the
- following error:
-
- ```plaintext
- Step 5/11 : RUN /bin/herokuish buildpack build
- ---> Running in eb468cd46085
- -----> Unable to select a buildpack
- The command '/bin/sh -c /bin/herokuish buildpack build' returned a non-zero code: 1
- ```
-
- The following are possible reasons:
-
- - Your application may be missing the key files the buildpack is looking for. For
- example, for Ruby applications you must have a `Gemfile` to be properly detected,
- even though it is possible to write a Ruby app without a `Gemfile`.
- - There may be no buildpack for your application. Try specifying a
- [custom buildpack](customize.md#custom-buildpacks).
-- Auto Test may fail because of a mismatch between testing frameworks. In this
- case, you may need to customize your `.gitlab-ci.yml` with your test commands.
-- Auto Deploy will fail if GitLab can not create a Kubernetes namespace and
- service account for your project. For help debugging this issue, see
- [Troubleshooting failed deployment jobs](../../user/project/clusters/index.md#troubleshooting).
+### Unable to select a buildpack
+
+Auto Build and Auto Test may fail to detect your language or framework with the
+following error:
+
+```plaintext
+Step 5/11 : RUN /bin/herokuish buildpack build
+ ---> Running in eb468cd46085
+ -----> Unable to select a buildpack
+The command '/bin/sh -c /bin/herokuish buildpack build' returned a non-zero code: 1
+```
+
+The following are possible reasons:
+
+- Your application may be missing the key files the buildpack is looking for.
+ Ruby applications require a `Gemfile` to be properly detected,
+ even though it's possible to write a Ruby app without a `Gemfile`.
+- No buildpack may exist for your application. Try specifying a
+ [custom buildpack](customize.md#custom-buildpacks).
+
+### Mismatch between testing frameworks
+
+Auto Test may fail because of a mismatch between testing frameworks. In this
+case, you may need to customize your `.gitlab-ci.yml` with your test commands.
+
+### Pipeline that extends Auto DevOps with only / except fails
+
+If your pipeline fails with the following message:
+
+```plaintext
+Found errors in your .gitlab-ci.yml:
+
+ jobs:test config key may not be used with `rules`: only
+```
+
+This error appears when the included job’s rules configuration has been overridden with the `only` or `except` syntax.
+To fix this issue, you must either:
+
+- Transition your `only/except` syntax to rules.
+- (Temporarily) Pin your templates to the [GitLab 12.10 based templates](https://gitlab.com/gitlab-org/auto-devops-v12-10).
+
+### Failure to create a Kubernetes namespace
+
+Auto Deploy will fail if GitLab can't create a Kubernetes namespace and
+service account for your project. For help debugging this issue, see
+[Troubleshooting failed deployment jobs](../../user/project/clusters/index.md#troubleshooting).
## Development guides
diff --git a/doc/topics/autodevops/quick_start_guide.md b/doc/topics/autodevops/quick_start_guide.md
index 53d5e664bc1..859219689f9 100644
--- a/doc/topics/autodevops/quick_start_guide.md
+++ b/doc/topics/autodevops/quick_start_guide.md
@@ -88,7 +88,7 @@ to deploy this project to.
[Cloud Run](../../user/project/clusters/add_gke_clusters.md#cloud-run-for-anthos),
Istio, and HTTP Load Balancing add-ons for this cluster.
- **GitLab-managed cluster** - Select this checkbox to
- [allow GitLab to manage namespace and service accounts](../..//user/project/clusters/index.md#gitlab-managed-clusters) for this cluster.
+ [allow GitLab to manage namespace and service accounts](../../user/project/clusters/index.md#gitlab-managed-clusters) for this cluster.
1. Click **Create Kubernetes cluster**.
@@ -215,12 +215,12 @@ you to common environment tasks:
about the Kubernetes cluster and how the application
affects it in terms of memory usage, CPU usage, and latency
- **Deploy to** (**{play}** **{angle-down}**) - Displays a list of environments you can deploy to
-- **Terminal** (**{terminal}**) - Opens a [web terminal](../../ci/environments.md#web-terminals)
+- **Terminal** (**{terminal}**) - Opens a [web terminal](../../ci/environments/index.md#web-terminals)
session inside the container where the application is running
- **Re-deploy to environment** (**{repeat}**) - For more information, see
- [Retrying and rolling back](../../ci/environments.md#retrying-and-rolling-back)
+ [Retrying and rolling back](../../ci/environments/index.md#retrying-and-rolling-back)
- **Stop environment** (**{stop}**) - For more information, see
- [Stopping an environment](../../ci/environments.md#stopping-an-environment)
+ [Stopping an environment](../../ci/environments/index.md#stopping-an-environment)
GitLab displays the [Deploy Board](../../user/project/deploy_boards.md) below the
environment's information, with squares representing pods in your
diff --git a/doc/topics/autodevops/stages.md b/doc/topics/autodevops/stages.md
index 66b76dcc05a..8c56a87ba30 100644
--- a/doc/topics/autodevops/stages.md
+++ b/doc/topics/autodevops/stages.md
@@ -1,47 +1,50 @@
# Stages of Auto DevOps
-The following sections describe the stages of Auto DevOps. Read them carefully
-to understand how each one works.
+The following sections describe the stages of [Auto DevOps](index.md).
+Read them carefully to understand how each one works.
## Auto Build
Auto Build creates a build of the application using an existing `Dockerfile` or
-Heroku buildpacks.
-
-Either way, the resulting Docker image is automatically pushed to the
-[Container Registry](../../user/packages/container_registry/index.md) and tagged with the commit SHA or tag.
+Heroku buildpacks. The resulting Docker image is pushed to the
+[Container Registry](../../user/packages/container_registry/index.md), and tagged
+with the commit SHA or tag.
### Auto Build using a Dockerfile
-If a project's repository contains a `Dockerfile` at its root, Auto Build will use
+If a project's repository contains a `Dockerfile` at its root, Auto Build uses
`docker build` to create a Docker image.
-If you are also using Auto Review Apps and Auto Deploy and choose to provide
-your own `Dockerfile`, make sure you expose your application to port
-`5000` as this is the port assumed by the
-[default Helm chart](https://gitlab.com/gitlab-org/charts/auto-deploy-app). Alternatively you can override the default values by [customizing the Auto Deploy Helm chart](customize.md#custom-helm-chart)
+If you're also using Auto Review Apps and Auto Deploy, and you choose to provide
+your own `Dockerfile`, you must either:
+
+- Expose your application to port `5000`, as the
+ [default Helm chart](https://gitlab.com/gitlab-org/charts/auto-deploy-app)
+ assumes this port is available.
+- Override the default values by
+ [customizing the Auto Deploy Helm chart](customize.md#custom-helm-chart).
### Auto Build using Heroku buildpacks
-Auto Build builds an application using a project's `Dockerfile` if present, or
-otherwise it will use [Herokuish](https://github.com/gliderlabs/herokuish)
+Auto Build builds an application using a project's `Dockerfile` if present. If no
+`Dockerfile` is present, it uses [Herokuish](https://github.com/gliderlabs/herokuish)
and [Heroku buildpacks](https://devcenter.heroku.com/articles/buildpacks)
-to automatically detect and build the application into a Docker image.
+to detect and build the application into a Docker image.
-Each buildpack requires certain files to be in your project's repository for
-Auto Build to successfully build your application. For example, the following
-files are required at the root of your application's repository, depending on
-the language:
+Each buildpack requires your project's repository to contain certain files for
+Auto Build to build your application successfully. For example, your application's
+root directory must contain the appropriate file for your application's
+language:
-- A `Pipfile` or `requirements.txt` file for Python projects.
-- A `Gemfile` or `Gemfile.lock` file for Ruby projects.
+- For Python projects, a `Pipfile` or `requirements.txt` file.
+- For Ruby projects, a `Gemfile` or `Gemfile.lock` file.
For the requirements of other languages and frameworks, read the
-[buildpacks docs](https://devcenter.heroku.com/articles/buildpacks#officially-supported-buildpacks).
+[Heroku buildpacks documentation](https://devcenter.heroku.com/articles/buildpacks#officially-supported-buildpacks).
TIP: **Tip:**
If Auto Build fails despite the project meeting the buildpack requirements, set
-a project variable `TRACE=true` to enable verbose logging, which may help to
+a project variable `TRACE=true` to enable verbose logging, which may help you
troubleshoot.
### Auto Build using Cloud Native Buildpacks (beta)
@@ -73,13 +76,13 @@ yet part of the Cloud Native Buildpack specification. For more information, see
## Auto Test
-Auto Test automatically runs the appropriate tests for your application using
-[Herokuish](https://github.com/gliderlabs/herokuish) and [Heroku
-buildpacks](https://devcenter.heroku.com/articles/buildpacks) by analyzing
+Auto Test runs the appropriate tests for your application using
+[Herokuish](https://github.com/gliderlabs/herokuish) and
+[Heroku buildpacks](https://devcenter.heroku.com/articles/buildpacks) by analyzing
your project to detect the language and framework. Several languages and
frameworks are detected automatically, but if your language is not detected,
-you may succeed with a [custom buildpack](customize.md#custom-buildpacks). Check the
-[currently supported languages](index.md#currently-supported-languages).
+you may be able to create a [custom buildpack](customize.md#custom-buildpacks).
+Check the [currently supported languages](index.md#currently-supported-languages).
Auto Test uses tests you already have in your application. If there are no
tests, it's up to you to add them.
@@ -88,12 +91,10 @@ tests, it's up to you to add them.
Auto Code Quality uses the
[Code Quality image](https://gitlab.com/gitlab-org/ci-cd/codequality) to run
-static analysis and other code checks on the current code. The report is
-created, and is uploaded as an artifact which you can later download and check
-out.
-
-Any differences between the source and target branches are also
-[shown in the merge request widget](../../user/project/merge_requests/code_quality.md).
+static analysis and other code checks on the current code. After creating the
+report, it's uploaded as an artifact which you can later download and check
+out. The merge request widget also displays any
+[differences between the source and target branches](../../user/project/merge_requests/code_quality.md).
## Auto SAST **(ULTIMATE)**
@@ -101,14 +102,17 @@ Any differences between the source and target branches are also
Static Application Security Testing (SAST) uses the
[SAST Docker image](https://gitlab.com/gitlab-org/security-products/sast) to run static
-analysis on the current code and checks for potential security issues. The
-Auto SAST stage will be skipped on licenses other than Ultimate and requires GitLab Runner 11.5 or above.
+analysis on the current code, and checks for potential security issues. The
+Auto SAST stage will be skipped on licenses other than
+[Ultimate](https://about.gitlab.com/pricing/), and requires
+[GitLab Runner](https://docs.gitlab.com/runner/) 11.5 or above.
-Once the report is created, it's uploaded as an artifact which you can later download and
-check out.
+After creating the report, it's uploaded as an artifact which you can later
+download and check out. The merge request widget also displays any security
+warnings.
-Any security warnings are also shown in the merge request widget. Read more how
-[SAST works](../../user/application_security/sast/index.md).
+To learn more about [how SAST works](../../user/application_security/sast/index.md),
+see the documentation.
## Auto Dependency Scanning **(ULTIMATE)**
@@ -116,16 +120,17 @@ Any security warnings are also shown in the merge request widget. Read more how
Dependency Scanning uses the
[Dependency Scanning Docker image](https://gitlab.com/gitlab-org/security-products/dependency-scanning)
-to run analysis on the project dependencies and checks for potential security issues.
-The Auto Dependency Scanning stage will be skipped on licenses other than Ultimate
-and requires GitLab Runner 11.5 or above.
+to run analysis on the project dependencies and check for potential security issues.
+The Auto Dependency Scanning stage is skipped on licenses other than
+[Ultimate](https://about.gitlab.com/pricing/) and requires
+[GitLab Runner](https://docs.gitlab.com/runner/) 11.5 or above.
-Once the
-report is created, it's uploaded as an artifact which you can later download and
-check out.
+After creating the report, it's uploaded as an artifact which you can later download and
+check out. The merge request widget displays any security warnings detected,
-Any security warnings are also shown in the merge request widget. Read more about
-[Dependency Scanning](../../user/application_security/dependency_scanning/index.md).
+To learn more about
+[Dependency Scanning](../../user/application_security/dependency_scanning/index.md),
+see the documentation.
## Auto License Compliance **(ULTIMATE)**
@@ -134,60 +139,57 @@ Any security warnings are also shown in the merge request widget. Read more abou
License Compliance uses the
[License Compliance Docker image](https://gitlab.com/gitlab-org/security-products/license-management)
to search the project dependencies for their license. The Auto License Compliance stage
-will be skipped on licenses other than Ultimate.
+is skipped on licenses other than [Ultimate](https://about.gitlab.com/pricing/).
-Once the
-report is created, it's uploaded as an artifact which you can later download and
-check out.
+After creating the report, it's uploaded as an artifact which you can later download and
+check out. The merge request displays any detected licenses.
-Any licenses are also shown in the merge request widget. Read more how
-[License Compliance works](../../user/compliance/license_compliance/index.md).
+To learn more about
+[License Compliance](../../user/compliance/license_compliance/index.md), see the
+documentation.
## Auto Container Scanning **(ULTIMATE)**
> Introduced in GitLab 10.4.
-Vulnerability Static Analysis for containers uses
-[Clair](https://github.com/quay/clair) to run static analysis on a
-Docker image and checks for potential security issues. The Auto Container Scanning stage
-will be skipped on licenses other than Ultimate.
+Vulnerability Static Analysis for containers uses [Clair](https://github.com/quay/clair)
+to check for potential security issues on Docker images. The Auto Container Scanning
+stage is skipped on licenses other than [Ultimate](https://about.gitlab.com/pricing/).
-Once the report is
-created, it's uploaded as an artifact which you can later download and
-check out.
+After creating the report, it's uploaded as an artifact which you can later download and
+check out. The merge request displays any detected security issues.
-Any security warnings are also shown in the merge request widget. Read more how
-[Container Scanning works](../../user/application_security/container_scanning/index.md).
+To learn more about
+[Container Scanning](../../user/application_security/container_scanning/index.md),
+see the documentation.
## Auto Review Apps
-This is an optional step, since many projects do not have a Kubernetes cluster
-available. If the [requirements](index.md#requirements) are not met, the job will
-silently be skipped.
+This is an optional step, since many projects don't have a Kubernetes cluster
+available. If the [requirements](index.md#requirements) are not met, the job is
+silently skipped.
[Review Apps](../../ci/review_apps/index.md) are temporary application environments based on the
branch's code so developers, designers, QA, product managers, and other
reviewers can actually see and interact with code changes as part of the review
process. Auto Review Apps create a Review App for each branch.
-Auto Review Apps will deploy your app to your Kubernetes cluster only. When no cluster
-is available, no deployment will occur.
+Auto Review Apps deploy your application to your Kubernetes cluster only. If no cluster
+is available, no deployment occurs.
-The Review App will have a unique URL based on the project ID, the branch or tag
-name, and a unique number, combined with the Auto DevOps base domain. For
-example, `13083-review-project-branch-123456.example.com`. A link to the Review App shows
-up in the merge request widget for easy discovery. When the branch or tag is deleted,
-for example after the merge request is merged, the Review App will automatically
-be deleted.
+The Review App has a unique URL based on a combination of the project ID, the branch
+or tag name, a unique number, and the Auto DevOps base domain, such as
+`13083-review-project-branch-123456.example.com`. The merge request widget displays
+a link to the Review App for easy discovery. When the branch or tag is deleted,
+such as after merging a merge request, the Review App is also deleted.
Review apps are deployed using the
[auto-deploy-app](https://gitlab.com/gitlab-org/charts/auto-deploy-app) chart with
-Helm, which can be [customized](customize.md#custom-helm-chart). The app will be deployed into the [Kubernetes
-namespace](../../user/project/clusters/index.md#deployment-variables)
+Helm, which you can [customize](customize.md#custom-helm-chart). The application deploys
+into the [Kubernetes namespace](../../user/project/clusters/index.md#deployment-variables)
for the environment.
-Since GitLab 11.4, a [local
-Tiller](https://gitlab.com/gitlab-org/gitlab-foss/-/merge_requests/22036) is
+Since GitLab 11.4, [local Tiller](https://gitlab.com/gitlab-org/gitlab-foss/-/merge_requests/22036) is
used. Previous versions of GitLab had a Tiller installed in the project
namespace.
@@ -196,52 +198,64 @@ Your apps should *not* be manipulated outside of Helm (using Kubernetes directly
This can cause confusion with Helm not detecting the change and subsequent
deploys with Auto DevOps can undo your changes. Also, if you change something
and want to undo it by deploying again, Helm may not detect that anything changed
-in the first place, and thus not realize that it needs to re-apply the old config.
+in the first place, and thus not realize that it needs to re-apply the old configuration.
## Auto DAST **(ULTIMATE)**
> Introduced in [GitLab Ultimate](https://about.gitlab.com/pricing/) 10.4.
-Dynamic Application Security Testing (DAST) uses the
-popular open source tool [OWASP ZAProxy](https://github.com/zaproxy/zaproxy)
-to perform an analysis on the current code and checks for potential security
-issues. The Auto DAST stage will be skipped on licenses other than Ultimate.
+Dynamic Application Security Testing (DAST) uses the popular open source tool
+[OWASP ZAProxy](https://github.com/zaproxy/zaproxy) to analyze the current code
+and check for potential security issues. The Auto DAST stage is skipped on
+licenses other than [Ultimate](https://about.gitlab.com/pricing/).
-Once the DAST scan is complete, any security warnings are shown
-on the [Security Dashboard](../../user/application_security/security_dashboard/index.md)
-and the Merge Request Widget. Read how
-[DAST works](../../user/application_security/dast/index.md).
+- On your default branch, DAST scans an application deployed specifically for that purpose
+ unless you [override the target branch](#overriding-the-dast-target).
+ The app is deleted after DAST has run.
+- On feature branches, DAST scans the [review app](#auto-review-apps).
-On your default branch, DAST scans an app deployed specifically for that purpose.
-The app is deleted after DAST has run.
+After the DAST scan completes, any security warnings are displayed
+on the [Security Dashboard](../../user/application_security/security_dashboard/index.md)
+and the merge request widget.
-On feature branches, DAST scans the [review app](#auto-review-apps).
+To learn more about
+[Dynamic Application Security Testing](../../user/application_security/dast/index.md),
+see the documentation.
### Overriding the DAST target
To use a custom target instead of the auto-deployed review apps,
set a `DAST_WEBSITE` environment variable to the URL for DAST to scan.
-NOTE: **Note:**
-If [DAST Full Scan](../../user/application_security/dast/index.md#full-scan) is enabled, it is strongly advised **not**
+DANGER: **Danger:**
+If [DAST Full Scan](../../user/application_security/dast/index.md#full-scan) is
+enabled, GitLab strongly advises **not**
to set `DAST_WEBSITE` to any staging or production environment. DAST Full Scan
-actively attacks the target, which can take down the application and lead to
+actively attacks the target, which can take down your application and lead to
data loss or corruption.
### Disabling Auto DAST
-DAST can be disabled:
+You can disable DAST:
- On all branches by setting the `DAST_DISABLED` environment variable to `"true"`.
-- Only on the default branch by setting the `DAST_DISABLED_FOR_DEFAULT_BRANCH` environment variable to `"true"`.
+- Only on the default branch by setting the `DAST_DISABLED_FOR_DEFAULT_BRANCH`
+ environment variable to `"true"`.
+- Only on feature branches by setting `REVIEW_DISABLED` environment variable to
+ `"true"`. This also disables the Review App.
## Auto Browser Performance Testing **(PREMIUM)**
> Introduced in [GitLab Premium](https://about.gitlab.com/pricing/) 10.4.
-Auto Browser Performance Testing utilizes the [Sitespeed.io container](https://hub.docker.com/r/sitespeedio/sitespeed.io/) to measure the performance of a web page. A JSON report is created and uploaded as an artifact, which includes the overall performance score for each page. By default, the root page of Review and Production environments will be tested. If you would like to add additional URL's to test, simply add the paths to a file named `.gitlab-urls.txt` in the root directory, one per line. For example:
+Auto Browser Performance Testing measures the performance of a web page with the
+[Sitespeed.io container](https://hub.docker.com/r/sitespeedio/sitespeed.io/),
+creates a JSON report including the overall performance score for each page, and
+uploads the report as an artifact. By default, it tests the root page of your Review and
+Production environments. If you want to test additional URLs, add the paths to a
+file named `.gitlab-urls.txt` in the root directory, one file per line. For example:
-```text
+```plaintext
/
/features
/direction
@@ -252,30 +266,31 @@ Any performance differences between the source and target branches are also
## Auto Deploy
-This is an optional step, since many projects do not have a Kubernetes cluster
-available. If the [requirements](index.md#requirements) are not met, the job will
-silently be skipped.
+This is an optional step, since many projects don't have a Kubernetes cluster
+available. If the [requirements](index.md#requirements) are not met, the job is skipped.
After a branch or merge request is merged into the project's default branch (usually
`master`), Auto Deploy deploys the application to a `production` environment in
the Kubernetes cluster, with a namespace based on the project name and unique
-project ID, for example `project-4321`.
+project ID, such as `project-4321`.
-Auto Deploy doesn't include deployments to staging or canary by default, but the
-[Auto DevOps template](https://gitlab.com/gitlab-org/gitlab/blob/master/lib/gitlab/ci/templates/Auto-DevOps.gitlab-ci.yml) contains job definitions for these tasks if you want to
-enable them.
+Auto Deploy does not include deployments to staging or canary environments by
+default, but the
+[Auto DevOps template](https://gitlab.com/gitlab-org/gitlab/blob/master/lib/gitlab/ci/templates/Auto-DevOps.gitlab-ci.yml)
+contains job definitions for these tasks if you want to enable them.
-You can make use of [environment variables](customize.md#environment-variables) to automatically
-scale your pod replicas and to apply custom arguments to the Auto DevOps `helm upgrade` commands. This is an easy way to [customize the Auto Deploy Helm chart](customize.md#custom-helm-chart).
+You can use [environment variables](customize.md#environment-variables) to automatically
+scale your pod replicas, and to apply custom arguments to the Auto DevOps `helm upgrade`
+commands. This is an easy way to
+[customize the Auto Deploy Helm chart](customize.md#custom-helm-chart).
-Apps are deployed using the
-[auto-deploy-app](https://gitlab.com/gitlab-org/charts/auto-deploy-app) chart with
-Helm. The app will be deployed into the [Kubernetes
-namespace](../../user/project/clusters/index.md#deployment-variables)
+Helm uses the [auto-deploy-app](https://gitlab.com/gitlab-org/charts/auto-deploy-app)
+chart to deploy the application into the
+[Kubernetes namespace](../../user/project/clusters/index.md#deployment-variables)
for the environment.
-Since GitLab 11.4, a [local
-Tiller](https://gitlab.com/gitlab-org/gitlab-foss/-/merge_requests/22036) is
+Since GitLab 11.4, a
+[local Tiller](https://gitlab.com/gitlab-org/gitlab-foss/-/merge_requests/22036) is
used. Previous versions of GitLab had a Tiller installed in the project
namespace.
@@ -284,76 +299,85 @@ Your apps should *not* be manipulated outside of Helm (using Kubernetes directly
This can cause confusion with Helm not detecting the change and subsequent
deploys with Auto DevOps can undo your changes. Also, if you change something
and want to undo it by deploying again, Helm may not detect that anything changed
-in the first place, and thus not realize that it needs to re-apply the old config.
+in the first place, and thus not realize that it needs to re-apply the old configuration.
+
+### GitLab deploy tokens
> [Introduced](https://gitlab.com/gitlab-org/gitlab-foss/-/merge_requests/19507) in GitLab 11.0.
-For internal and private projects a [GitLab Deploy Token](../../user/project/deploy_tokens/index.md#gitlab-deploy-token)
-will be automatically created, when Auto DevOps is enabled and the Auto DevOps settings are saved. This Deploy Token
-can be used for permanent access to the registry. When the GitLab Deploy Token has been manually revoked, it won't be automatically created.
+[GitLab Deploy Tokens](../../user/project/deploy_tokens/index.md#gitlab-deploy-token)
+are created for internal and private projects when Auto DevOps is enabled, and the
+Auto DevOps settings are saved. You can use a Deploy Token for permanent access to
+the registry. After you manually revoke the GitLab Deploy Token, it won't be
+automatically created.
+
+If the GitLab Deploy Token can't be found, `CI_REGISTRY_PASSWORD` is
+used.
-If the GitLab Deploy Token cannot be found, `CI_REGISTRY_PASSWORD` is
-used. Note that `CI_REGISTRY_PASSWORD` is only valid during deployment.
-This means that Kubernetes will be able to successfully pull the
-container image during deployment but in cases where the image needs to
-be pulled again, e.g. after pod eviction, Kubernetes will fail to do so
-as it will be attempting to fetch the image using
-`CI_REGISTRY_PASSWORD`.
+NOTE: **Note:**
+`CI_REGISTRY_PASSWORD` is only valid during deployment. Kubernetes will be able
+to successfully pull the container image during deployment, but if the image must
+be pulled again, such as after pod eviction, Kubernetes will fail to do so
+as it attempts to fetch the image using `CI_REGISTRY_PASSWORD`.
### Kubernetes 1.16+
> - [Introduced](https://gitlab.com/gitlab-org/charts/auto-deploy-app/-/merge_requests/51) in GitLab 12.8.
> - Support for deploying a PostgreSQL version that supports Kubernetes 1.16+ was [introduced](https://gitlab.com/gitlab-org/cluster-integration/auto-deploy-image/-/merge_requests/49) in GitLab 12.9.
+> - Supported out of the box for new deployments as of GitLab 13.0.
CAUTION: **Deprecation**
-The default value of `extensions/v1beta1` for the `deploymentApiVersion` setting is
-deprecated, and is scheduled to be changed to a new default of `apps/v1` in
-[GitLab 13.0](https://gitlab.com/gitlab-org/charts/auto-deploy-app/issues/47).
+The default value for the `deploymentApiVersion` setting was changed from
+`extensions/v1beta` to `apps/v1` in [GitLab 13.0](https://gitlab.com/gitlab-org/charts/auto-deploy-app/issues/47).
-In Kubernetes 1.16 onwards, a number of [APIs were removed](https://kubernetes.io/blog/2019/07/18/api-deprecations-in-1-16/),
+In Kubernetes 1.16 and later, a number of
+[APIs were removed](https://kubernetes.io/blog/2019/07/18/api-deprecations-in-1-16/),
including support for `Deployment` in the `extensions/v1beta1` version.
-To use Auto Deploy on a Kubernetes 1.16+ cluster, you must:
+To use Auto Deploy on a Kubernetes 1.16+ cluster:
-1. Set the following in the [`.gitlab/auto-deploy-values.yaml` file](customize.md#customize-values-for-helm-chart):
+1. If you are deploying your application for the first time on GitLab 13.0 or
+ newer, no configuration should be required.
- ```yml
+1. On GitLab 12.10 or older, set the following in the [`.gitlab/auto-deploy-values.yaml` file](customize.md#customize-values-for-helm-chart):
+
+ ```yaml
deploymentApiVersion: apps/v1
```
-1. Set the:
-
- - `AUTO_DEVOPS_POSTGRES_CHANNEL` variable to `2`.
- - `POSTGRES_VERSION` variable to `9.6.16` or higher.
+1. If you have an in-cluster PostgreSQL database installed with
+ `AUTO_DEVOPS_POSTGRES_CHANNEL` set to `1`, follow the [guide to upgrade
+ PostgreSQL](upgrading_postgresql.md).
- This will opt-in to using a version of the PostgreSQL chart that supports Kubernetes
- 1.16 and higher.
+1. If you are deploying your application for the first time and are using
+ GitLab 12.9 or 12.10, set `AUTO_DEVOPS_POSTGRES_CHANNEL` to `2`.
-DANGER: **Danger:** Opting into `AUTO_DEVOPS_POSTGRES_CHANNEL` version
-`2` will delete the version `1` PostgreSQL database. Please follow the
-guide on [upgrading PostgreSQL](upgrading_postgresql.md) to backup and
-restore your database before opting into version `2`.
+DANGER: **Danger:** On GitLab 12.9 and 12.10, opting into
+`AUTO_DEVOPS_POSTGRES_CHANNEL` version `2` deletes the version `1` PostgreSQL
+database. Follow the [guide to upgrading PostgreSQL](upgrading_postgresql.md)
+to back up and restore your database before opting into version `2` (On
+GitLab 13.0, an additional variable is required to trigger the database
+deletion).
### Migrations
> [Introduced](https://gitlab.com/gitlab-org/gitlab-foss/-/merge_requests/21955) in GitLab 11.4
-Database initialization and migrations for PostgreSQL can be configured to run
+You can configure database initialization and migrations for PostgreSQL to run
within the application pod by setting the project variables `DB_INITIALIZE` and
`DB_MIGRATE` respectively.
-If present, `DB_INITIALIZE` will be run as a shell command within an
-application pod as a Helm post-install hook. As some applications will
-not run without a successful database initialization step, GitLab will
-deploy the first release without the application deployment and only the
-database initialization step. After the database initialization completes,
-GitLab will deploy a second release with the application deployment as
-normal.
+If present, `DB_INITIALIZE` is run as a shell command within an application pod
+as a Helm post-install hook. As some applications can't run without a successful
+database initialization step, GitLab deploys the first release without the
+application deployment, and only the database initialization step. After the database
+initialization completes, GitLab deploys a second release with the application
+deployment as normal.
Note that a post-install hook means that if any deploy succeeds,
-`DB_INITIALIZE` will not be processed thereafter.
+`DB_INITIALIZE` won't be processed thereafter.
-If present, `DB_MIGRATE` will be run as a shell command within an application pod as
+If present, `DB_MIGRATE` is run as a shell command within an application pod as
a Helm pre-upgrade hook.
For example, in a Rails application in an image built with
@@ -362,38 +386,39 @@ For example, in a Rails application in an image built with
- `DB_INITIALIZE` can be set to `RAILS_ENV=production /bin/herokuish procfile exec bin/rails db:setup`
- `DB_MIGRATE` can be set to `RAILS_ENV=production /bin/herokuish procfile exec bin/rails db:migrate`
-Unless you have a `Dockerfile` in your repo, your image is built with
-Herokuish, and you must prefix commands run in these images with `/bin/herokuish
-procfile exec` to replicate the environment where your application will run.
+Unless your repository contains a `Dockerfile`, your image is built with
+Herokuish, and you must prefix commands run in these images with
+`/bin/herokuish procfile exec` to replicate the environment where your application
+will run.
### Workers
-Some web applications need to run extra deployments for "worker processes". For
-example, it is common in a Rails application to have a separate worker process
+Some web applications must run extra deployments for "worker processes". For
+example, Rails applications commonly use separate worker processes
to run background tasks like sending emails.
The [default Helm chart](https://gitlab.com/gitlab-org/charts/auto-deploy-app)
-used in Auto Deploy [has support for running worker
-processes](https://gitlab.com/gitlab-org/charts/auto-deploy-app/-/merge_requests/9).
+used in Auto Deploy
+[has support for running worker processes](https://gitlab.com/gitlab-org/charts/auto-deploy-app/-/merge_requests/9).
-In order to run a worker, you'll need to ensure that it is able to respond to
+To run a worker, you must ensure the worker can respond to
the standard health checks, which expect a successful HTTP response on port
-`5000`. For [Sidekiq](https://github.com/mperham/sidekiq), you could make use of
-the [`sidekiq_alive` gem](https://rubygems.org/gems/sidekiq_alive) to do this.
+`5000`. For [Sidekiq](https://github.com/mperham/sidekiq), you can use
+the [`sidekiq_alive` gem](https://rubygems.org/gems/sidekiq_alive).
-In order to work with Sidekiq, you'll also need to ensure your deployments have
-access to a Redis instance. Auto DevOps won't deploy this for you so you'll
-need to:
+To work with Sidekiq, you must also ensure your deployments have
+access to a Redis instance. Auto DevOps won't deploy this instance for you, so
+you must:
- Maintain your own Redis instance.
-- Set a CI variable `K8S_SECRET_REDIS_URL`, which the URL of this instance to
- ensure it's passed into your deployments.
+- Set a CI variable `K8S_SECRET_REDIS_URL`, which is the URL of this instance,
+ to ensure it's passed into your deployments.
-Once you have configured your worker to respond to health checks, run a Sidekiq
+After configuring your worker to respond to health checks, run a Sidekiq
worker for your Rails application. You can enable workers by setting the
following in the [`.gitlab/auto-deploy-values.yaml` file](customize.md#customize-values-for-helm-chart):
-```yml
+```yaml
workers:
sidekiq:
replicaCount: 1
@@ -417,7 +442,7 @@ workers:
By default, all Kubernetes pods are
[non-isolated](https://kubernetes.io/docs/concepts/services-networking/network-policies/#isolated-and-non-isolated-pods),
-meaning that they will accept traffic to and from any source. You can use
+and accept traffic to and from any source. You can use
[NetworkPolicy](https://kubernetes.io/docs/concepts/services-networking/network-policies/)
to restrict connections to and from selected pods, namespaces, and the Internet.
@@ -437,13 +462,13 @@ networkPolicy:
enabled: true
```
-The default policy deployed by the auto deploy pipeline will allow
-traffic within a local namespace and from the `gitlab-managed-apps`
-namespace. All other inbound connection will be blocked. Outbound
+The default policy deployed by the Auto Deploy pipeline allows
+traffic within a local namespace, and from the `gitlab-managed-apps`
+namespace. All other inbound connections are blocked. Outbound
traffic (for example, to the Internet) is not affected by the default policy.
You can also provide a custom [policy specification](https://kubernetes.io/docs/reference/generated/kubernetes-api/v1.16/#networkpolicyspec-v1-networking-k8s-io)
-via the `.gitlab/auto-deploy-values.yaml` file, for example:
+in the `.gitlab/auto-deploy-values.yaml` file, for example:
```yaml
networkPolicy:
@@ -461,16 +486,19 @@ networkPolicy:
app.gitlab.com/managed_by: gitlab
```
-For more information on how to install Network Policies, see
+For more information on installing Network Policies, see
[Install Cilium using GitLab CI/CD](../../user/clusters/applications.md#install-cilium-using-gitlab-cicd).
### Web Application Firewall (ModSecurity) customization
> [Introduced](https://gitlab.com/gitlab-org/charts/auto-deploy-app/-/merge_requests/44) in GitLab 12.8.
-Customization on an [Ingress](https://kubernetes.io/docs/concepts/services-networking/ingress/) or on a deployment base is available for clusters with [ModSecurity installed](../../user/clusters/applications.md#web-application-firewall-modsecurity).
+Customization on an [Ingress](https://kubernetes.io/docs/concepts/services-networking/ingress/)
+or on a deployment base is available for clusters with
+[ModSecurity installed](../../user/clusters/applications.md#web-application-firewall-modsecurity).
-To enable ModSecurity with Auto Deploy, you need to create a `.gitlab/auto-deploy-values.yaml` file in your project with the following attributes.
+To enable ModSecurity with Auto Deploy, you must create a `.gitlab/auto-deploy-values.yaml`
+file in your project with the following attributes.
|Attribute | Description | Default |
-----------|-------------|---------|
@@ -481,7 +509,7 @@ To enable ModSecurity with Auto Deploy, you need to create a `.gitlab/auto-deplo
In the following `auto-deploy-values.yaml` example, some custom settings
are enabled for ModSecurity. Those include setting its engine to
process rules instead of only logging them, while adding two specific
-rules which are header-based:
+header-based rules:
```yaml
ingress:
@@ -500,17 +528,17 @@ ingress:
### Running commands in the container
Applications built with [Auto Build](#auto-build) using Herokuish, the default
-unless you have [a custom Dockerfile](#auto-build-using-a-dockerfile), may require
-commands to be wrapped as follows:
+unless your repository contains [a custom Dockerfile](#auto-build-using-a-dockerfile),
+may require commands to be wrapped as follows:
```shell
/bin/herokuish procfile exec $COMMAND
```
-This might be necessary, for example, when:
+Some of the reasons you may need to wrap commands:
- Attaching using `kubectl exec`.
-- Using GitLab's [Web Terminal](../../ci/environments.md#web-terminals).
+- Using GitLab's [Web Terminal](../../ci/environments/index.md#web-terminals).
For example, to start a Rails console from the application root directory, run:
@@ -520,12 +548,12 @@ For example, to start a Rails console from the application root directory, run:
## Auto Monitoring
-Once your application is deployed, Auto Monitoring makes it possible to monitor
+After your application deploys, Auto Monitoring helps you monitor
your application's server and response metrics right out of the box. Auto
Monitoring uses [Prometheus](../../user/project/integrations/prometheus.md) to
-get system metrics such as CPU and memory usage directly from
+retrieve system metrics, such as CPU and memory usage, directly from
[Kubernetes](../../user/project/integrations/prometheus_library/kubernetes.md),
-and response metrics such as HTTP error rates, latency, and throughput from the
+and response metrics, such as HTTP error rates, latency, and throughput, from the
[NGINX server](../../user/project/integrations/prometheus_library/nginx_ingress.md).
The metrics include:
@@ -538,14 +566,14 @@ GitLab provides some initial alerts for you after you install Prometheus:
- Ingress status code `500` > 0.1%
- NGINX status code `500` > 0.1%
-To make use of Auto Monitoring:
+To use Auto Monitoring:
1. [Install and configure the requirements](index.md#requirements).
-1. [Enable Auto DevOps](index.md#enablingdisabling-auto-devops) if you haven't done already.
-1. Finally, go to your project's **CI/CD > Pipelines** and run a pipeline.
-1. Once the pipeline finishes successfully, open the
- [monitoring dashboard for a deployed environment](../../ci/environments.md#monitoring-environments)
+1. [Enable Auto DevOps](index.md#enablingdisabling-auto-devops), if you haven't done already.
+1. Navigate to your project's **{rocket}** **CI/CD > Pipelines** and click **Run Pipeline**.
+1. After the pipeline finishes successfully, open the
+ [monitoring dashboard for a deployed environment](../../ci/environments/index.md#monitoring-environments)
to view the metrics of your deployed application. To view the metrics of the
- whole Kubernetes cluster, navigate to **Operations > Metrics**.
+ whole Kubernetes cluster, navigate to **{cloud-gear}** **Operations > Metrics**.
![Auto Metrics](img/auto_monitoring.png)
diff --git a/doc/topics/autodevops/upgrading_postgresql.md b/doc/topics/autodevops/upgrading_postgresql.md
index ccb009905eb..893f7ba7cde 100644
--- a/doc/topics/autodevops/upgrading_postgresql.md
+++ b/doc/topics/autodevops/upgrading_postgresql.md
@@ -39,7 +39,7 @@ being modified after the database dump is created.
1. Get the Kubernetes namespace for the environment. It typically looks like `<project-name>-<project-id>-<environment>`.
In our example, the namespace is called `minimal-ruby-app-4349298-production`.
- ```sh
+ ```shell
$ kubectl get ns
NAME STATUS AGE
@@ -48,13 +48,13 @@ being modified after the database dump is created.
1. For ease of use, export the namespace name:
- ```sh
+ ```shell
export APP_NAMESPACE=minimal-ruby-app-4349298-production
```
1. Get the deployment name for your application with the following command. In our example, the deployment name is `production`.
- ```sh
+ ```shell
$ kubectl get deployment --namespace "$APP_NAMESPACE"
NAME READY UP-TO-DATE AVAILABLE AGE
production 2/2 2 2 7d21h
@@ -64,7 +64,7 @@ being modified after the database dump is created.
1. To prevent the database from being modified, set replicas to 0 for the deployment with the following command.
We use the deployment name from the previous step (`deployments/<DEPLOYMENT_NAME>`).
- ```sh
+ ```shell
$ kubectl scale --replicas=0 deployments/production --namespace "$APP_NAMESPACE"
deployment.extensions/production scaled
```
@@ -75,7 +75,7 @@ being modified after the database dump is created.
1. Get the service name for PostgreSQL. The name of the service should end with `-postgres`. In our example the service name is `production-postgres`.
- ```sh
+ ```shell
$ kubectl get svc --namespace "$APP_NAMESPACE"
NAME TYPE CLUSTER-IP EXTERNAL-IP PORT(S) AGE
production-auto-deploy ClusterIP 10.30.13.90 <none> 5000/TCP 7d14h
@@ -84,7 +84,7 @@ being modified after the database dump is created.
1. Get the pod name for PostgreSQL with the following command. In our example, the pod name is `production-postgres-5db86568d7-qxlxv`.
- ```sh
+ ```shell
$ kubectl get pod --namespace "$APP_NAMESPACE" -l app=production-postgres
NAME READY STATUS RESTARTS AGE
production-postgres-5db86568d7-qxlxv 1/1 Running 0 7d14h
@@ -92,7 +92,7 @@ being modified after the database dump is created.
1. Connect to the pod with:
- ```sh
+ ```shell
kubectl exec -it production-postgres-5db86568d7-qxlxv --namespace "$APP_NAMESPACE" bash
```
@@ -104,7 +104,7 @@ being modified after the database dump is created.
- You will be asked for the database password, the default is `testing-password`.
- ```sh
+ ```shell
## Format is:
# pg_dump -h SERVICE_NAME -U USERNAME DATABASE_NAME > /tmp/backup.sql
@@ -115,7 +115,7 @@ being modified after the database dump is created.
1. Download the dump file with the following command:
- ```sh
+ ```shell
kubectl cp --namespace "$APP_NAMESPACE" production-postgres-5db86568d7-qxlxv:/tmp/backup.sql backup.sql
```
@@ -126,12 +126,12 @@ volumes](https://kubernetes.io/docs/concepts/storage/persistent-volumes/)
used to store the underlying data for PostgreSQL is marked as `Delete`
when the pods and pod claims that use the volume is deleted.
-This is signficant as, when you opt into the newer 8.2.1 PostgreSQL, the older 0.7.1 PostgreSQL is
+This is significant as, when you opt into the newer 8.2.1 PostgreSQL, the older 0.7.1 PostgreSQL is
deleted causing the persistent volumes to be deleted as well.
You can verify this by using the following command:
-```sh
+```shell
$ kubectl get pv
NAME CAPACITY ACCESS MODES RECLAIM POLICY STATUS CLAIM STORAGECLASS REASON AGE
pvc-0da80c08-5239-11ea-9c8d-42010a8e0096 8Gi RWO Delete Bound minimal-ruby-app-4349298-staging/staging-postgres standard 7d22h
@@ -145,7 +145,7 @@ interested in keeping the volumes for the staging and production of the
`minimal-ruby-app-4349298` application, the volume names here are
`pvc-0da80c08-5239-11ea-9c8d-42010a8e0096` and `pvc-9085e3d3-5239-11ea-9c8d-42010a8e0096`:
-```sh
+```shell
$ kubectl patch pv pvc-0da80c08-5239-11ea-9c8d-42010a8e0096 -p '{"spec":{"persistentVolumeReclaimPolicy":"Retain"}}'
persistentvolume/pvc-0da80c08-5239-11ea-9c8d-42010a8e0096 patched
$ kubectl patch pv pvc-9085e3d3-5239-11ea-9c8d-42010a8e0096 -p '{"spec":{"persistentVolumeReclaimPolicy":"Retain"}}'
@@ -164,17 +164,19 @@ deleted, you can choose to retain the [persistent
volume](#retain-persistent-volumes).
TIP: **Tip:** You can also
-[scope](../../ci/environments.md#scoping-environments-with-specs) the
-`AUTO_DEVOPS_POSTGRES_CHANNEL` and `POSTGRES_VERSION` variables to
-specific environments, e.g. `staging`.
+[scope](../../ci/environments/index.md#scoping-environments-with-specs) the
+`AUTO_DEVOPS_POSTGRES_CHANNEL`, `AUTO_DEVOPS_POSTGRES_DELETE_V1` and
+`POSTGRES_VERSION` variables to specific environments, e.g. `staging`.
1. Set `AUTO_DEVOPS_POSTGRES_CHANNEL` to `2`. This opts into using the
newer 8.2.1-based PostgreSQL, and removes the older 0.7.1-based
PostgreSQL.
-1. Set `POSTGRES_VERSION` to `9.6.16`. This is the minimum PostgreSQL
+1. Set `AUTO_DEVOPS_POSTGRES_DELETE_V1` to a non-empty value. This flag is a
+ safeguard to prevent accidental deletion of databases.
+1. Set `POSTGRES_VERSION` to `11.7`. This is the minimum PostgreSQL
version supported.
1. Set `PRODUCTION_REPLICAS` to `0`. For other environments, use
- `REPLICAS` with an [environment scope](../../ci/environments.md#scoping-environments-with-specs).
+ `REPLICAS` with an [environment scope](../../ci/environments/index.md#scoping-environments-with-specs).
1. If you have set the `DB_INITIALIZE` or `DB_MIGRATE` variables, either
remove the variables, or rename the variables temporarily to
`XDB_INITIALIZE` or the `XDB_MIGRATE` to effectively disable them.
@@ -190,7 +192,7 @@ specific environments, e.g. `staging`.
1. Get the pod name for the new PostgreSQL, in our example, the pod name is
`production-postgresql-0`:
- ```sh
+ ```shell
$ kubectl get pod --namespace "$APP_NAMESPACE" -l app=postgresql
NAME READY STATUS RESTARTS AGE
production-postgresql-0 1/1 Running 0 19m
@@ -198,13 +200,13 @@ specific environments, e.g. `staging`.
1. Copy the dump file from the backup steps to the pod:
- ```sh
+ ```shell
kubectl cp --namespace "$APP_NAMESPACE" backup.sql production-postgresql-0:/tmp/backup.sql
```
1. Connect to the pod:
- ```sh
+ ```shell
kubectl exec -it production-postgresql-0 --namespace "$APP_NAMESPACE" bash
```
@@ -214,7 +216,7 @@ specific environments, e.g. `staging`.
- `USERNAME` is the username you have configured for PostgreSQL. The default is `user`.
- `DATABASE_NAME` is usually the environment name.
- ```sh
+ ```shell
## Format is:
# psql -U USERNAME -d DATABASE_NAME < /tmp/backup.sql
diff --git a/doc/topics/git/how_to_install_git/index.md b/doc/topics/git/how_to_install_git/index.md
index 2c3cd61f050..75ea6183a32 100644
--- a/doc/topics/git/how_to_install_git/index.md
+++ b/doc/topics/git/how_to_install_git/index.md
@@ -6,7 +6,7 @@ article_type: user guide
date: 2017-05-15
description: 'This article describes how to install Git on macOS, Ubuntu Linux and Windows.'
type: howto
-last_updated: 2019-05-31
+last_updated: 2020-04-22
---
# Installing Git
@@ -32,34 +32,47 @@ use one of the aforementioned methods.
### Installing Xcode
-Xcode is needed by Homebrew to build dependencies.
-You can install [XCode](https://developer.apple.com/xcode/)
-through the macOS App Store.
+To build dependencies, Homebrew needs the XCode Command Line Tools. Install
+it by running in your terminal:
+
+```shell
+xcode-select --install
+```
+
+Click **Install** to download and install it. Alternatively, you can install
+the entire [XCode](https://developer.apple.com/xcode/) package through the
+macOS App Store.
### Installing Homebrew
-Once Xcode is installed browse to the [Homebrew website](https://brew.sh/index.html)
+With Xcode installed, browse to the [Homebrew website](https://brew.sh/index.html)
for the official Homebrew installation instructions.
### Installing Git via Homebrew
-With Homebrew installed you are now ready to install Git.
-Open a Terminal and enter in the following command:
+With Homebrew installed, you are now ready to install Git.
+Open a terminal and enter the following command:
```shell
brew install git
```
-Congratulations you should now have Git installed via Homebrew.
+Congratulations! You should now have Git installed via Homebrew.
+
+To verify that Git works on your system, run:
+
+```shell
+git --version
+```
-Next read our article on [adding an SSH key to GitLab](../../../ssh/README.md).
+Next, read our article on [adding an SSH key to GitLab](../../../ssh/README.md).
## Install Git on Ubuntu Linux
On Ubuntu and other Linux operating systems
-it is recommended to use the built in package manager to install Git.
+it is recommended to use the built-in package manager to install Git.
-Open a Terminal and enter in the following commands
+Open a terminal and enter the following commands
to install the latest Git from the official Git maintained package archives:
```shell
@@ -68,15 +81,21 @@ sudo apt-get update
sudo apt-get install git
```
-Congratulations you should now have Git installed via the Ubuntu package manager.
+Congratulations! You should now have Git installed via the Ubuntu package manager.
+
+To verify that Git works on your system, run:
+
+```shell
+git --version
+```
-Next read our article on [adding an SSH key to GitLab](../../../ssh/README.md).
+Next, read our article on [adding an SSH key to GitLab](../../../ssh/README.md).
## Installing Git on Windows from the Git website
-Browse to the [Git website](https://git-scm.com/) and download and install Git for Windows.
+Open the [Git website](https://git-scm.com/) and download and install Git for Windows.
-Next read our article on [adding an SSH key to GitLab](../../../ssh/README.md).
+Next, read our article on [adding an SSH key to GitLab](../../../ssh/README.md).
<!-- ## Troubleshooting
diff --git a/doc/topics/git/index.md b/doc/topics/git/index.md
index 019194d1bba..9e6875312f3 100644
--- a/doc/topics/git/index.md
+++ b/doc/topics/git/index.md
@@ -27,21 +27,27 @@ The following resources will help you get started with Git:
- [Git on the Server - GitLab](https://git-scm.com/book/en/v2/Git-on-the-Server-GitLab)
- [How to install Git](how_to_install_git/index.md)
- [Start using Git on the command line](../../gitlab-basics/start-using-git.md)
-- [Command Line basic commands](../../gitlab-basics/command-line-commands.md)
+- [Command line file editing basic commands](../../gitlab-basics/command-line-commands.md)
- [GitLab Git Cheat Sheet (download)](https://about.gitlab.com/images/press/git-cheat-sheet.pdf)
- Commits:
- [Revert a commit](../../user/project/merge_requests/revert_changes.md#reverting-a-commit)
- [Cherry-picking a commit](../../user/project/merge_requests/cherry_pick_changes.md#cherry-picking-a-commit)
- [Squashing commits](../gitlab_flow.md#squashing-commits-with-rebase)
+ - [Squash-and-merge](../../user/project/merge_requests/squash_and_merge.md)
+ - [Signing commits](../../user/project/repository/gpg_signed_commits/index.md)
+- [Git stash](../../university/training/topics/stash.md)
+- [Git file blame](../../user/project/repository/git_blame.md)
+- [Git file history](../../user/project/repository/git_history.md)
+- [Git tags](../../university/training/user_training.md#tags)
### Concepts
-The following are resources about version control concepts:
+The following are resources on version control concepts:
- [Git concepts](../../university/training/user_training.md#git-concepts)
- [Why Git is Worth the Learning Curve](https://about.gitlab.com/blog/2017/05/17/learning-curve-is-the-biggest-challenge-developers-face-with-git/)
- [The future of SaaS hosted Git repository pricing](https://about.gitlab.com/blog/2016/05/11/git-repository-pricing/)
-- [Git website topic about version control](https://git-scm.com/book/en/v2/Getting-Started-About-Version-Control)
+- [Git website on version control](https://git-scm.com/book/en/v2/Getting-Started-About-Version-Control)
- [GitLab University presentation about Version Control](https://docs.google.com/presentation/d/16sX7hUrCZyOFbpvnrAFrg6tVO5_yT98IgdAqOmXwBho/edit?usp=sharing)
## Git tips
@@ -61,9 +67,10 @@ If you have problems with Git, the following may help:
## Branching strategies
+- [Feature branch workflow](../../gitlab-basics/feature_branch_workflow.md)
+- [GitLab Flow](../gitlab_flow.md)
- [Git Branching - Branches in a Nutshell](https://git-scm.com/book/en/v2/Git-Branching-Branches-in-a-Nutshell)
- [Git Branching - Branching Workflows](https://git-scm.com/book/en/v2/Git-Branching-Branching-Workflows)
-- [GitLab Flow](https://about.gitlab.com/blog/2014/09/29/gitlab-flow/)
## Advanced use
@@ -79,7 +86,7 @@ The following are advanced topics for those who want to get the most out of Git:
[Gitignore templates](../../api/templates/gitignores.md) API allow for
Git-related queries from GitLab.
-## Git LFS
+## Git Large File Storage (LFS)
The following relate to Git Large File Storage:
@@ -88,5 +95,5 @@ The following relate to Git Large File Storage:
- [Removing objects from LFS](lfs/index.md#removing-objects-from-lfs)
- [GitLab Git LFS user documentation](lfs/index.md)
- [GitLab Git LFS admin documentation](../../administration/lfs/index.md)
-- [git-annex to Git-LFS migration guide](lfs/migrate_from_git_annex_to_git_lfs.md)
+- [Git Annex to Git LFS migration guide](lfs/migrate_from_git_annex_to_git_lfs.md)
- [Towards a production quality open source Git LFS server](https://about.gitlab.com/blog/2015/08/13/towards-a-production-quality-open-source-git-lfs-server/)
diff --git a/doc/topics/git/lfs/index.md b/doc/topics/git/lfs/index.md
index 325a87215c4..33b7fa45691 100644
--- a/doc/topics/git/lfs/index.md
+++ b/doc/topics/git/lfs/index.md
@@ -2,7 +2,7 @@
disqus_identifier: 'https://docs.gitlab.com/ee/workflow/lfs/lfs/index.html'
---
-# Git LFS
+# Git Large File Storage (LFS)
Managing large files such as audio, video and graphics files has always been one
of the shortcomings of Git. The general recommendation is to not have Git repositories
@@ -154,7 +154,7 @@ also edit it:
git lfs unlock images/banner.png
```
-You can also unlock by id:
+You can also unlock by ID:
```shell
git lfs unlock --id=123
@@ -217,10 +217,10 @@ If you push a LFS object to a project and you receive an error similar to:
the LFS client is trying to reach GitLab through HTTPS. However, your GitLab
instance is being served on HTTP.
-This behaviour is caused by Git LFS using HTTPS connections by default when a
+This behavior is caused by Git LFS using HTTPS connections by default when a
`lfsurl` is not set in the Git config.
-To prevent this from happening, set the lfs url in project Git config:
+To prevent this from happening, set the lfs URL in project Git config:
```shell
git config --add lfs.url "http://gitlab.example.com/group/project.git/info/lfs"
@@ -265,7 +265,7 @@ If you are storing LFS files outside of GitLab you can disable LFS on the projec
### Hosting LFS objects externally
-It is possible to host LFS objects externally by setting a custom LFS url with `git config -f .lfsconfig lfs.url https://example.com/<project>.git/info/lfs`.
+It is possible to host LFS objects externally by setting a custom LFS URL with `git config -f .lfsconfig lfs.url https://example.com/<project>.git/info/lfs`.
You might choose to do this if you are using an appliance like a Sonatype Nexus to store LFS data. If you choose to use an external LFS store,
GitLab will not be able to verify LFS objects which means that pushes will fail if you have GitLab LFS support enabled.
diff --git a/doc/topics/git/lfs/migrate_from_git_annex_to_git_lfs.md b/doc/topics/git/lfs/migrate_from_git_annex_to_git_lfs.md
index d23199fa556..05b749d7b24 100644
--- a/doc/topics/git/lfs/migrate_from_git_annex_to_git_lfs.md
+++ b/doc/topics/git/lfs/migrate_from_git_annex_to_git_lfs.md
@@ -1,16 +1,16 @@
# Migration guide from Git Annex to Git LFS
>**Note:**
-Git Annex support [has been removed][issue-remove-annex] in GitLab Enterprise
+Git Annex support [has been removed](https://gitlab.com/gitlab-org/gitlab/issues/1648) in GitLab Enterprise
Edition 9.0 (2017/03/22).
-Both [Git Annex][] and [Git LFS][] are tools to manage large files in Git.
+Both [Git Annex](http://git-annex.branchable.com/) and [Git LFS](https://git-lfs.github.com/) are tools to manage large files in Git.
## History
-Git Annex [was introduced in GitLab Enterprise Edition 7.8][post-3], at a time
+Git Annex [was introduced in GitLab Enterprise Edition 7.8](https://about.gitlab.com/blog/2015/02/17/gitlab-annex-solves-the-problem-of-versioning-large-binaries-with-git/), at a time
where Git LFS didn't yet exist. A few months later, GitLab brought support for
-Git LFS in [GitLab 8.2][post-2] and is available for both Community and
+Git LFS in [GitLab 8.2](https://about.gitlab.com/blog/2015/11/23/announcing-git-lfs-support-in-gitlab/) and is available for both Community and
Enterprise editions.
## Differences between Git Annex and Git LFS
@@ -71,13 +71,13 @@ Before changing anything, make sure you have a backup of your repository first.
There are a couple of ways to do that, but you can simply clone it to another
local path and maybe push it to GitLab if you want a remote backup as well.
Here you'll find a guide on
-[how to back up a **git-annex** repository to an external hard drive][bkp-ext-drive].
+[how to back up a **git-annex** repository to an external hard drive](https://www.thomas-krenn.com/en/wiki/Git-annex_Repository_on_an_External_Hard_Drive).
Since Annex files are stored as objects with symlinks and cannot be directly
modified, we need to first remove those symlinks.
NOTE: **Note:**
-Make sure the you read about the [`direct` mode][annex-direct] as it contains
+Make sure the you read about the [`direct` mode](https://git-annex.branchable.com/direct_mode/) as it contains
useful information that may fit in your use case. Note that `annex direct` is
deprecated in Git Annex version 6, so you may need to upgrade your repository
if the server also has Git Annex 6 installed. Read more in the
@@ -114,7 +114,7 @@ if the server also has Git Annex 6 installed. Read more in the
direct ok
```
-1. Disable Git Annex with [`annex uninit`][uninit]:
+1. Disable Git Annex with [`annex uninit`](https://git-annex.branchable.com/git-annex-uninit/):
```shell
git annex uninit
@@ -170,7 +170,7 @@ GitLab.com), therefore, you don't need to do anything server-side.
```
If the terminal doesn't prompt you with a full response on `git-lfs` commands,
- [install the Git LFS client][install-lfs] first.
+ [install the Git LFS client](https://git-lfs.github.com/) first.
1. Inside the repo, run the following command to initiate LFS:
@@ -235,19 +235,8 @@ git annex uninit
## Further Reading
-- (Blog Post) [Getting Started with Git FLS][post-1]
-- (Blog Post) [Announcing LFS Support in GitLab][post-2]
-- (Blog Post) [GitLab Annex Solves the Problem of Versioning Large Binaries with Git][post-3]
+- (Blog Post) [Getting Started with Git FLS](https://about.gitlab.com/blog/2017/01/30/getting-started-with-git-lfs-tutorial/)
+- (Blog Post) [Announcing LFS Support in GitLab](https://about.gitlab.com/blog/2015/11/23/announcing-git-lfs-support-in-gitlab/)
+- (Blog Post) [GitLab Annex Solves the Problem of Versioning Large Binaries with Git](https://about.gitlab.com/blog/2015/02/17/gitlab-annex-solves-the-problem-of-versioning-large-binaries-with-git/)
- (GitLab Docs) [Git Annex](../../../administration/git_annex.md)
- (GitLab Docs) [Git LFS](index.md)
-
-[annex-direct]: https://git-annex.branchable.com/direct_mode/
-[bkp-ext-drive]: https://www.thomas-krenn.com/en/wiki/Git-annex_Repository_on_an_External_Hard_Drive
-[Git Annex]: http://git-annex.branchable.com/
-[Git LFS]: https://git-lfs.github.com/
-[install-lfs]: https://git-lfs.github.com/
-[issue-remove-annex]: https://gitlab.com/gitlab-org/gitlab/issues/1648
-[post-1]: https://about.gitlab.com/blog/2017/01/30/getting-started-with-git-lfs-tutorial/
-[post-2]: https://about.gitlab.com/blog/2015/11/23/announcing-git-lfs-support-in-gitlab/
-[post-3]: https://about.gitlab.com/blog/2015/02/17/gitlab-annex-solves-the-problem-of-versioning-large-binaries-with-git/
-[uninit]: https://git-annex.branchable.com/git-annex-uninit/
diff --git a/doc/topics/git/numerous_undo_possibilities_in_git/index.md b/doc/topics/git/numerous_undo_possibilities_in_git/index.md
index 9d175b2edb1..8597325db7b 100644
--- a/doc/topics/git/numerous_undo_possibilities_in_git/index.md
+++ b/doc/topics/git/numerous_undo_possibilities_in_git/index.md
@@ -24,7 +24,7 @@ Git is really deleted](https://git-scm.com/book/en/v2/Git-Internals-Maintenance-
This means that until Git automatically cleans detached commits (which cannot be
accessed by branch or tag) it will be possible to view them with `git reflog` command
-and access them with direct commit-id. Read more about _[redoing the undo](#redoing-the-undo)_ on the section below.
+and access them with direct commit ID. Read more about _[redoing the undo](#redoing-the-undo)_ in the section below.
## Introduction
@@ -36,12 +36,12 @@ a file into the **staged** state, which is then committed (`git commit`) to your
local repository. After that, file can be shared with other developers (`git push`).
Here's what we'll cover in this tutorial:
-- [Undo local changes](#undo-local-changes) which were not pushed to remote repository:
+- [Undo local changes](#undo-local-changes) which were not pushed to a remote repository:
- Before you commit, in both unstaged and staged state.
- After you committed.
-- Undo changes after they are pushed to remote repository:
+- Undo changes after they are pushed to a remote repository:
- [Without history modification](#undo-remote-changes-without-changing-history) (preferred way).
- [With history modification](#undo-remote-changes-with-modifying-history) (requires
@@ -68,7 +68,7 @@ joined development of the same feature by multiple developers by default.
When multiple developers develop the same feature on the same branch, clashing
with every synchronization is unavoidable, but a proper or chosen Git Workflow will
-prevent that anything is lost or out of sync when feature is complete.
+prevent that anything is lost or out of sync when the feature is complete.
You can also
read through this blog post on [Git Tips & Tricks](https://about.gitlab.com/blog/2016/12/08/git-tips-and-tricks/)
@@ -83,7 +83,7 @@ can be on various stages and each stage has a different approach on how to tackl
### Unstaged local changes (before you commit)
When a change is made, but it is not added to the staged tree, Git itself
-proposes a solution to discard changes to certain file.
+proposes a solution to discard changes to a certain file.
Suppose you edited a file to change the content using your favorite editor:
@@ -233,7 +233,7 @@ last known good commit (we assume `A`) and first known bad commit (where bug was
git bisect A..E
```
-Bisect will provide us with commit-id of the middle commit to test, and then guide us
+Bisect will provide us with commit ID of the middle commit to test, and then guide us
through simple bisection process. You can read more about it [in official Git Tools](https://git-scm.com/book/en/v2/Git-Tools-Debugging-with-Git)
In our example we will end up with commit `B`, that introduced bug/error. We have
4 options on how to remove it (or part of it) from our repository.
@@ -332,7 +332,7 @@ history](#how-modifying-history-is-done)
Sometimes you realize that the changes you undid were useful and you want them
back. Well because of first paragraph you are in luck. Command `git reflog`
enables you to *recall* detached local commits by referencing or applying them
-via commit-id. Although, do not expect to see really old commits in reflog, because
+via commit ID. Although, do not expect to see really old commits in reflog, because
Git regularly [cleans the commits which are *unreachable* by branches or tags](https://git-scm.com/book/en/v2/Git-Internals-Maintenance-and-Data-Recovery).
To view repository history and to track older commits you can use below command:
@@ -353,7 +353,7 @@ eb37e74 HEAD@{6}: rebase -i (pick): Commit C
6e43d59 HEAD@{16}: commit: Commit B
```
-Output of command shows repository history. In first column there is commit-id,
+Output of command shows repository history. In first column there is commit ID,
in following column, number next to `HEAD` indicates how many commits ago something
was made, after that indicator of action that was made (commit, rebase, merge, ...)
and then on end description of that action.
@@ -393,7 +393,7 @@ passwords, SSH keys, etc. It is and should not be used to hide mistakes, as
it will make it harder to debug in case there are some other bugs. The main
reason for this is that you loose the real development progress. **Also keep in
mind that, even with modified history, commits are just detached and can still be
-accessed through commit-id** - at least until all repositories perform
+accessed through commit ID** - at least until all repositories perform
the cleanup of detached commits (happens automatically).
![Modifying history causes problems on remote branch](img/rebase_reset.png)
@@ -426,7 +426,7 @@ Never modify the commit history of `master` or shared branch.
After you know what you want to modify (how far in history or how which range of
old commits), use `git rebase -i commit-id`. This command will then display all the commits from
-current version to chosen commit-id and allow modification, squashing, deletion
+current version to chosen commit ID and allow modification, squashing, deletion
of that commits.
```shell
diff --git a/doc/topics/git/partial_clone.md b/doc/topics/git/partial_clone.md
index a48e1c19f5c..46318a7f30d 100644
--- a/doc/topics/git/partial_clone.md
+++ b/doc/topics/git/partial_clone.md
@@ -9,22 +9,7 @@ is a performance optimization that "allows Git to function without having a
complete copy of the repository. The goal of this work is to allow Git better
handle extremely large repositories."
-## Enabling partial clone
-
-> [Introduced](https://gitlab.com/gitlab-org/gitaly/issues/1553) in GitLab 12.4.
-
-To enable partial clone, use the [feature flags API](../../api/features.md).
-For example:
-
-```shell
-curl --data "value=true" --header "PRIVATE-TOKEN: <your_access_token>" https://gitlab.example.com/api/v4/features/gitaly_upload_pack_filter
-```
-
-Alternatively, flip the switch and enable the feature flag:
-
-```ruby
-Feature.enable(:gitaly_upload_pack_filter)
-```
+Git 2.22.0 or later is required.
## Filter by file size
@@ -82,7 +67,7 @@ reduce the size of your working copy.
```plaintext
# Clone the repo excluding all files
-$ git clone --filter=blob:none --sparse git@gitlab.com:gitlab-com/www-gitlab-com/git
+$ git clone --filter=blob:none --sparse git@gitlab.com:gitlab-com/www-gitlab-com.git
Cloning into 'www-gitlab-com'...
remote: Enumerating objects: 678296, done.
remote: Counting objects: 100% (678296/678296), done.
@@ -135,7 +120,7 @@ enabled on the Git server:
many applications, each in a different subdirectory in the root. Create a file
`shiny-app/.filterspec` using the GitLab web interface:
- ```.gitignore
+ ```plaintext
# Only the paths listed in the file will be downloaded when performing a
# partial clone using `--filter=sparse:oid=shiny-app/.gitfilterspec`
@@ -153,7 +138,7 @@ enabled on the Git server:
shared-component-b/
```
-1. *Create a new Git repository and fetch.* Support for `--filter=sparse:oid`
+1. **Create a new Git repository and fetch.** Support for `--filter=sparse:oid`
using the clone command is incomplete, so we will emulate the clone command
by hand, using `git init` and `git fetch`. Follow
[issue tracking support for `--filter=sparse:oid`](https://gitlab.com/gitlab-org/git/issues/4)
diff --git a/doc/topics/git/troubleshooting_git.md b/doc/topics/git/troubleshooting_git.md
index f8d812d37e6..010a811bd33 100644
--- a/doc/topics/git/troubleshooting_git.md
+++ b/doc/topics/git/troubleshooting_git.md
@@ -13,7 +13,7 @@ with Git.
'Broken pipe' errors can occur when attempting to push to a remote repository.
When pushing you will usually see:
-```text
+```plaintext
Write failed: Broken pipe
fatal: The remote end hung up unexpectedly
```
@@ -56,7 +56,7 @@ Configuring *both* the client and the server is unnecessary.
- On UNIX, edit `~/.ssh/config` (create the file if it doesn’t exist) and
add or edit:
- ```text
+ ```plaintext
Host your-gitlab-instance-url.com
ServerAliveInterval 60
ServerAliveCountMax 5
@@ -68,7 +68,7 @@ Configuring *both* the client and the server is unnecessary.
**To configure SSH on the server side**, edit `/etc/ssh/sshd_config` and add:
-```text
+```plaintext
ClientAliveInterval 60
ClientAliveCountMax 5
```
@@ -93,7 +93,7 @@ to >= 2.9 (see [Broken pipe when pushing to Git repository](https://stackoverflo
Users may experience the following error when attempting to push or pull
using Git over SSH:
-```text
+```plaintext
Please make sure you have the correct access rights
and the repository exists.
...
@@ -103,7 +103,7 @@ fatal: Could not read from remote repository.
or
-```text
+```plaintext
ssh_exchange_identification: Connection closed by remote host
fatal: The remote end hung up unexpectedly
```
@@ -117,7 +117,7 @@ beginning. The default value is `10`.
Increase `MaxStartups` on the GitLab server
by adding or modifying the value in `/etc/ssh/sshd_config`:
-```text
+```plaintext
MaxStartups 100:30:200
```
@@ -140,7 +140,7 @@ If pulling/pushing from/to your repository ends up taking more than 50 seconds,
a timeout will be issued with a log of the number of operations performed
and their respective timings, like the example below:
-```text
+```plaintext
remote: Running checks for branch: master
remote: Scanning for LFS objects... (153ms)
remote: Calculating new repository size... (cancelled after 729ms)
@@ -153,7 +153,7 @@ and provide GitLab with more information on how to improve the service.
If the buffer size is lower than what is allowed in the request, the action will fail with an error similar to the one below:
-```text
+```plaintext
error: RPC failed; curl 18 transfer closed with outstanding read data remaining
fatal: The remote end hung up unexpectedly
fatal: early EOF
diff --git a/doc/topics/git/useful_git_commands.md b/doc/topics/git/useful_git_commands.md
index 63defb24eaf..711567dbd06 100644
--- a/doc/topics/git/useful_git_commands.md
+++ b/doc/topics/git/useful_git_commands.md
@@ -116,7 +116,7 @@ git log <file>
If you get this error message:
-```text
+```plaintext
fatal: ambiguous argument <file_name>: unknown revision or path not in the working tree.
Use '--' to separate paths from revisions, like this:
```
diff --git a/doc/topics/gitlab_flow.md b/doc/topics/gitlab_flow.md
index fb47899c585..4738c5b23d7 100644
--- a/doc/topics/gitlab_flow.md
+++ b/doc/topics/gitlab_flow.md
@@ -253,7 +253,7 @@ If you need to utilize some code that was introduced in `master` after you creat
If your feature branch has a merge conflict, creating a merge commit is a standard way of solving this.
NOTE: **Note:**
-Sometimes you can use .gitattributes to reduce merge conflicts.
+Sometimes you can use `.gitattributes` to reduce merge conflicts.
For example, you can set your changelog file to use the [union merge driver](https://git-scm.com/docs/gitattributes#gitattributes-union) so that multiple new entries don't conflict with each other.
The last reason for creating merge commits is to keep long-running feature branches up-to-date with the latest state of the project.
diff --git a/doc/topics/web_application_firewall/img/guide_waf_ingress_disabled_settings_v12_10.png b/doc/topics/web_application_firewall/img/guide_waf_ingress_disabled_settings_v12_10.png
new file mode 100644
index 00000000000..2dd6df3d37b
--- /dev/null
+++ b/doc/topics/web_application_firewall/img/guide_waf_ingress_disabled_settings_v12_10.png
Binary files differ
diff --git a/doc/university/support/README.md b/doc/university/support/README.md
index a60107dc4e1..5e824bcb6f9 100644
--- a/doc/university/support/README.md
+++ b/doc/university/support/README.md
@@ -62,14 +62,14 @@ Sometimes we need to upgrade customers from old versions of GitLab to latest, so
- Users
- Groups
- Projects
- - [Backup using our Backup Rake task](../../raketasks/backup_restore.md#creating-a-backup-of-the-gitlab-system)
+ - [Back up using our backup Rake task](../../raketasks/backup_restore.md#back-up-gitlab)
- [Upgrade to 5.0 source using our Upgrade documentation](https://gitlab.com/gitlab-org/gitlab/blob/master/doc/update/4.2-to-5.0.md)
- [Upgrade to 5.1 source](https://gitlab.com/gitlab-org/gitlab/blob/master/doc/update/5.0-to-5.1.md)
- [Upgrade to 6.0 source](https://gitlab.com/gitlab-org/gitlab/blob/master/doc/update/5.1-to-6.0.md)
- [Upgrade to 7.14 source](https://gitlab.com/gitlab-org/gitlab/blob/master/doc/update/6.x-or-7.x-to-7.14.md)
- [Perform the MySQL to PostgreSQL migration to convert your backup](../../update/mysql_to_postgresql.md)
- [Upgrade to Omnibus 7.14](https://docs.gitlab.com/omnibus/update/README.html#upgrading-from-a-non-omnibus-installation-to-an-omnibus-installation)
- - [Restore backup using our Restore Rake task](../../raketasks/backup_restore.md#restore)
+ - [Restore backup using our Restore Rake task](../../raketasks/backup_restore.md#restore-gitlab)
- [Upgrade to latest EE](https://about.gitlab.com/update/)
- (GitLab inc. only) Acquire and apply a license for the Enterprise Edition product, ask in #support
- Perform a downgrade from [EE to CE](../../downgrade_ee_to_ce/README.md)
@@ -103,7 +103,7 @@ Our integrations add great value to GitLab. User questions often relate to integ
### Learn about the Support process
-Zendesk is our Support Centre and our main communication line with our Customers. We communicate with customers through several other channels too
+Zendesk is our Support Center and our main communication line with our Customers. We communicate with customers through several other channels too
- Familiarize yourself with ZenDesk:
- [UI Overview](https://support.zendesk.com/hc/en-us/articles/203661806-Introduction-to-the-Zendesk-agent-interface)
diff --git a/doc/university/training/end-user/README.md b/doc/university/training/end-user/README.md
index 5fc65bb8a58..0465f48bb34 100644
--- a/doc/university/training/end-user/README.md
+++ b/doc/university/training/end-user/README.md
@@ -278,7 +278,7 @@ git rm '*.txt'
git rm -r <dirname>
```
-If we want to remove a file from the repository but keep it on disk, say we forgot to add it to our .gitignore file then use `--cache`:
+If we want to remove a file from the repository but keep it on disk, say we forgot to add it to our `.gitignore` file then use `--cache`:
```shell
git rm <filename> --cache
diff --git a/doc/university/training/topics/agile_git.md b/doc/university/training/topics/agile_git.md
index 6434b710159..c5634bec27b 100644
--- a/doc/university/training/topics/agile_git.md
+++ b/doc/university/training/topics/agile_git.md
@@ -20,7 +20,7 @@ Branching in an Agile environment usually happens around user stories with one
or more developers working on it.
If more than one developer then another branch for each developer is also used
-with their initials, and US id.
+with their initials, and US ID.
After its tested merge into master and remove the branch.
diff --git a/doc/update/README.md b/doc/update/README.md
index c7be3f3cb2b..8056460ceaa 100644
--- a/doc/update/README.md
+++ b/doc/update/README.md
@@ -13,7 +13,7 @@ Based on your installation, choose a section below that fits your needs.
## Omnibus Packages
-- The [Omnibus update guide][omni-update]
+- The [Omnibus update guide](https://docs.gitlab.com/omnibus/update/README.html)
contains the steps needed to update an Omnibus GitLab package.
## Installation from source
@@ -29,14 +29,14 @@ In the past we used separate documents for the upgrading instructions, but we
have since switched to using a single document. The old upgrading guidelines
can still be found in the Git repository:
-- [Old upgrading guidelines for Community Edition][old-ce-upgrade-docs]
-- [Old upgrading guidelines for Enterprise Edition][old-ee-upgrade-docs]
+- [Old upgrading guidelines for Community Edition](https://gitlab.com/gitlab-org/gitlab-foss/tree/11-8-stable/doc/update)
+- [Old upgrading guidelines for Enterprise Edition](https://gitlab.com/gitlab-org/gitlab/tree/11-8-stable-ee/doc/update)
## Installation using Docker
GitLab provides official Docker images for both Community and Enterprise
editions. They are based on the Omnibus package and instructions on how to
-update them are in [a separate document][omni-docker].
+update them are in [a separate document](https://docs.gitlab.com/omnibus/docker/README.html).
## Upgrading without downtime
@@ -106,7 +106,7 @@ meet the other online upgrade requirements mentioned above.
### Steps
-Steps to [upgrade without downtime][omni-zero-downtime].
+Steps to [upgrade without downtime](https://docs.gitlab.com/omnibus/update/README.html#zero-downtime-updates).
## Checking for background migrations before upgrading
@@ -163,8 +163,8 @@ of the `background_migration` queue, [check for background migrations before upg
## Upgrading between editions
-GitLab comes in two flavors: [Community Edition][ce] which is MIT licensed,
-and [Enterprise Edition][ee] which builds on top of the Community Edition and
+GitLab comes in two flavors: [Community Edition](https://about.gitlab.com/features/#community) which is MIT licensed,
+and [Enterprise Edition](https://about.gitlab.com/features/#enterprise) which builds on top of the Community Edition and
includes extra features mainly aimed at organizations with more than 100 users.
Below you can find some guides to help you change editions easily.
@@ -177,17 +177,17 @@ The following guides are for subscribers of the Enterprise Edition only.
If you wish to upgrade your GitLab installation from Community to Enterprise
Edition, follow the guides below based on the installation method:
-- [Source CE to EE update guides][source-ce-to-ee] - The steps are very similar
+- [Source CE to EE update guides](upgrading_from_ce_to_ee.md) - The steps are very similar
to a version upgrade: stop the server, get the code, update config files for
the new functionality, install libraries and do migrations, update the init
script, start the application and check its status.
-- [Omnibus CE to EE][omni-ce-ee] - Follow this guide to update your Omnibus
+- [Omnibus CE to EE](https://docs.gitlab.com/omnibus/update/README.html#updating-community-edition-to-enterprise-edition) - Follow this guide to update your Omnibus
GitLab Community Edition to the Enterprise Edition.
### Enterprise to Community Edition
If you need to downgrade your Enterprise Edition installation back to Community
-Edition, you can follow [this guide][ee-ce] to make the process as smooth as
+Edition, you can follow [this guide](../downgrade_ee_to_ce/README.md) to make the process as smooth as
possible.
## Version specific upgrading instructions
@@ -227,15 +227,3 @@ for more information.
- [Restoring from backup after a failed upgrade](restore_after_failure.md)
- [Upgrading PostgreSQL Using Slony](upgrading_postgresql_using_slony.md), for
upgrading a PostgreSQL database with minimal downtime.
-
-[omnidocker]: https://docs.gitlab.com/omnibus/docker/README.html
-[old-ee-upgrade-docs]: https://gitlab.com/gitlab-org/gitlab/tree/11-8-stable-ee/doc/update
-[old-ce-upgrade-docs]: https://gitlab.com/gitlab-org/gitlab-foss/tree/11-8-stable/doc/update
-[source-ce-to-ee]: upgrading_from_ce_to_ee.md
-[ee-ce]: ../downgrade_ee_to_ce/README.md
-[ce]: https://about.gitlab.com/features/#community
-[ee]: https://about.gitlab.com/features/#enterprise
-[omni-ce-ee]: https://docs.gitlab.com/omnibus/update/README.html#updating-community-edition-to-enterprise-edition
-[omni-docker]: https://docs.gitlab.com/omnibus/docker/README.html
-[omni-update]: https://docs.gitlab.com/omnibus/update/README.html
-[omni-zero-downtime]: https://docs.gitlab.com/omnibus/update/README.html#zero-downtime-updates
diff --git a/doc/update/patch_versions.md b/doc/update/patch_versions.md
index aaa464dfdcb..f226ad9ac28 100644
--- a/doc/update/patch_versions.md
+++ b/doc/update/patch_versions.md
@@ -12,7 +12,7 @@ You can select the tag in the version dropdown in the top left corner of GitLab
### 0. Backup
-It's useful to make a backup just in case things go south. Depending on the installation method, backup commands vary, see the [backing up and restoring GitLab](../raketasks/backup_restore.md#creating-a-backup-of-the-gitlab-system) documentation.
+It's useful to make a backup just in case things go south. Depending on the installation method, backup commands vary. See the [backing up and restoring GitLab](../raketasks/backup_restore.md) documentation.
### 1. Stop server
diff --git a/doc/update/restore_after_failure.md b/doc/update/restore_after_failure.md
index e01ae409bb3..c850bbff1cf 100644
--- a/doc/update/restore_after_failure.md
+++ b/doc/update/restore_after_failure.md
@@ -10,9 +10,9 @@ the previous version you were using.
First, roll back the code or package. For source installations this involves
checking out the older version (branch or tag). For Omnibus installations this
-means installing the older .deb or .rpm package. Then, restore from a backup.
+means installing the older `.deb` or `.rpm` package. Then, restore from a backup.
Follow the instructions in the
-[Backup and Restore](../raketasks/backup_restore.md#restore)
+[Backup and Restore](../raketasks/backup_restore.md#restore-gitlab)
documentation.
## Potential problems on the next upgrade
diff --git a/doc/update/upgrading_from_ce_to_ee.md b/doc/update/upgrading_from_ce_to_ee.md
index 28d5fe7aa5f..2415d3b2741 100644
--- a/doc/update/upgrading_from_ce_to_ee.md
+++ b/doc/update/upgrading_from_ce_to_ee.md
@@ -7,7 +7,7 @@ comments: false
NOTE: **NOTE** In the past we used separate documents for upgrading from
Community Edition to Enterprise Edition. These documents can be found in the
[`doc/update` directory of Enterprise Edition's source
-code][old-ee-upgrade-docs].
+code](https://gitlab.com/gitlab-org/gitlab/tree/11-8-stable-ee/doc/update).
If you want to upgrade the version only, for example 11.8 to 11.9, *without* changing the
GitLab edition you are using (Community or Enterprise), see the
@@ -133,5 +133,3 @@ Example:
Additional instructions here.
-->
-
-[old-ee-upgrade-docs]: https://gitlab.com/gitlab-org/gitlab/tree/11-8-stable-ee/doc/update
diff --git a/doc/user/admin_area/abuse_reports.md b/doc/user/admin_area/abuse_reports.md
index 36de817a29b..77f4a84cf6f 100644
--- a/doc/user/admin_area/abuse_reports.md
+++ b/doc/user/admin_area/abuse_reports.md
@@ -46,7 +46,7 @@ Blocking a user:
The user will be notified with the
[following message](https://gitlab.com/gitlab-org/gitlab/blob/master/app/workers/email_receiver_worker.rb#L38):
-```text
+```plaintext
Your account has been blocked. If you believe this is in error, contact a staff member.
```
diff --git a/doc/user/admin_area/broadcast_messages.md b/doc/user/admin_area/broadcast_messages.md
index ed03e8a57f3..5427b0c5200 100644
--- a/doc/user/admin_area/broadcast_messages.md
+++ b/doc/user/admin_area/broadcast_messages.md
@@ -20,10 +20,19 @@ You can style a message's content using the `a` and `br` HTML tags. The `br` tag
## Banners
-Banners are shown on the top of a page.
+Banners are shown on the top of a page and in Git remote responses.
![Broadcast Message Banner](img/broadcast_messages_banner_v12_10.png)
+```bash
+$ git push
+...
+remote:
+remote: **Welcome** to GitLab :wave:
+remote:
+...
+```
+
## Notifications
Notifications are shown on the bottom right of a page and can contain placeholders. A placeholder is replaced with an attribute of the active user. Placeholders must be surrounded by curly braces, for example `{{name}}`.
diff --git a/doc/user/admin_area/index.md b/doc/user/admin_area/index.md
index 204573da02d..e63d0c7c882 100644
--- a/doc/user/admin_area/index.md
+++ b/doc/user/admin_area/index.md
@@ -136,19 +136,22 @@ you must provide the complete email address.
#### Users statistics
-The **Users statistics** page provides an overview of user accounts by role. Use this information
-when validating seat usage of your subscription.
+The **Users statistics** page provides an overview of user accounts by role, such as _Users with
+highest role Maintainer_.
-The page displays subtotals of all users matching criteria such as _Users with highest role
-Maintainer_ and _Blocked users_.
+The following totals are also included:
-The **Total users** is calculated as: **Active users** + **Blocked users**.
+- Active users
+- Blocked users
+- Total users
-GitLab billing is based on the number of active users. For details of active users, see
+GitLab billing is based on the number of **Active users**, calculated as **Total users** -
+**Blocked users**. For details of active users, see
[Choosing the number of users](../../subscriptions/index.md#choosing-the-number-of-users).
-**Please note** that during the initial stage, the information won't be 100% accurate given that
-background processes are still recollecting data.
+NOTE: **Note:**
+Users statistics are calculated daily, so user changes made since the last update won't be
+reflected in the statistics.
### Administering Groups
diff --git a/doc/user/admin_area/license.md b/doc/user/admin_area/license.md
index fd74b2f311f..59b71b05b16 100644
--- a/doc/user/admin_area/license.md
+++ b/doc/user/admin_area/license.md
@@ -10,8 +10,8 @@ by **signing into your GitLab instance as an admin** or add it at
installation time.
The license has the form of a base64 encoded ASCII text with a `.gitlab-license`
-extension and can be obtained when you [purchase one][pricing] or when you sign
-up for a [free trial].
+extension and can be obtained when you [purchase one](https://about.gitlab.com/pricing/) or when you sign
+up for a [free trial](https://about.gitlab.com/free-trial/).
NOTE: **Note:**
As of GitLab Enterprise Edition 9.4.0, a newly-installed instance without an
@@ -104,9 +104,6 @@ expired license(s).
It's possible to upload and view more than one license,
but only the latest license will be used as the active license.
-[free trial]: https://about.gitlab.com/free-trial/
-[pricing]: https://about.gitlab.com/pricing/
-
<!-- ## Troubleshooting
Include any troubleshooting steps that you can foresee. If you know beforehand what issues
diff --git a/doc/user/admin_area/monitoring/health_check.md b/doc/user/admin_area/monitoring/health_check.md
index f69584fcb94..91e29118e3e 100644
--- a/doc/user/admin_area/monitoring/health_check.md
+++ b/doc/user/admin_area/monitoring/health_check.md
@@ -4,8 +4,8 @@ type: concepts, howto
# Health Check **(CORE ONLY)**
-> - Liveness and readiness probes were [introduced][ce-10416] in GitLab 9.1.
-> - The `health_check` endpoint was [introduced][ce-3888] in GitLab 8.8 and was
+> - Liveness and readiness probes were [introduced](https://gitlab.com/gitlab-org/gitlab-foss/-/merge_requests/10416) in GitLab 9.1.
+> - The `health_check` endpoint was [introduced](https://gitlab.com/gitlab-org/gitlab-foss/-/merge_requests/3888) in GitLab 8.8 and was
> deprecated in GitLab 9.1.
> - [Access token](#access-token-deprecated) has been deprecated in GitLab 9.4
> in favor of [IP whitelist](#ip-whitelist).
@@ -25,15 +25,15 @@ For details, see [how to add IPs to a whitelist for the monitoring endpoints](..
With default whitelist settings, the probes can be accessed from localhost using the following URLs:
-```text
+```plaintext
GET http://localhost/-/health
```
-```text
+```plaintext
GET http://localhost/-/readiness
```
-```text
+```plaintext
GET http://localhost/-/liveness
```
@@ -45,7 +45,7 @@ are running. This endpoint circumvents Rails Controllers
and is implemented as additional middleware `BasicHealthCheck`
very early into the request processing lifecycle.
-```text
+```plaintext
GET /-/health
```
@@ -57,7 +57,7 @@ curl 'https://gitlab.example.com/-/health'
Example response:
-```text
+```plaintext
GitLab OK
```
@@ -71,7 +71,7 @@ If the `all=1` parameter is specified, the check will also validate
the dependent services (Database, Redis, Gitaly etc.)
and gives a status for each.
-```text
+```plaintext
GET /-/readiness
GET /-/readiness?all=1
```
@@ -111,7 +111,7 @@ Checks whether the application server is running.
This probe is used to know if Rails Controllers
are not deadlocked due to a multi-threading.
-```text
+```plaintext
GET /-/liveness
```
@@ -148,7 +148,7 @@ accepted token can be found under the **Admin Area > Monitoring > Health check**
The access token can be passed as a URL parameter:
-```text
+```plaintext
https://gitlab.example.com/-/readiness?token=ACCESS_TOKEN
```
@@ -163,9 +163,3 @@ questions that you know someone might ask.
Each scenario can be a third-level heading, e.g. `### Getting error message X`.
If you have none to add when creating a doc, leave this section in place
but commented out to help encourage others to add to it in the future. -->
-
-[ce-10416]: https://gitlab.com/gitlab-org/gitlab-foss/-/merge_requests/10416
-[ce-3888]: https://gitlab.com/gitlab-org/gitlab-foss/-/merge_requests/3888
-[pingdom]: https://www.pingdom.com
-[nagios-health]: https://nagios-plugins.org/doc/man/check_http.html
-[newrelic-health]: https://docs.newrelic.com/docs/alerts/alert-policies/downtime-alerts/availability-monitoring
diff --git a/doc/user/admin_area/settings/account_and_limit_settings.md b/doc/user/admin_area/settings/account_and_limit_settings.md
index f05ef18da8e..83b29597bec 100644
--- a/doc/user/admin_area/settings/account_and_limit_settings.md
+++ b/doc/user/admin_area/settings/account_and_limit_settings.md
@@ -44,7 +44,7 @@ For instance, consider the following workflow:
1. Your team develops apps which require large files to be stored in
the application repository.
-1. Although you have enabled [Git LFS](../../../topics/git/lfs/index.md#git-lfs)
+1. Although you have enabled [Git LFS](../../../topics/git/lfs/index.md#git-large-file-storage-lfs)
to your project, your storage has grown significantly.
1. Before you exceed available storage, you set up a limit of 10 GB
per repository.
diff --git a/doc/user/admin_area/settings/email.md b/doc/user/admin_area/settings/email.md
index 4fab560e081..a99897657ae 100644
--- a/doc/user/admin_area/settings/email.md
+++ b/doc/user/admin_area/settings/email.md
@@ -12,7 +12,7 @@ The logo in the header of some emails can be customized, see the [logo customiza
## Custom additional text **(PREMIUM ONLY)**
-> [Introduced][ee-5031] in [GitLab Premium][eep] 10.7.
+> [Introduced](https://gitlab.com/gitlab-org/gitlab/-/merge_requests/5031) in [GitLab Premium](https://about.gitlab.com/pricing/) 10.7.
The additional text will appear at the bottom of any email and can be used for
legal/auditing/compliance reasons.
@@ -22,9 +22,6 @@ legal/auditing/compliance reasons.
1. Enter your text in the **Additional text** field.
1. Click **Save**.
-[ee-5031]: https://gitlab.com/gitlab-org/gitlab/-/merge_requests/5031
-[eep]: https://about.gitlab.com/pricing/
-
## Custom hostname for private commit emails
> [Introduced](https://gitlab.com/gitlab-org/gitlab-foss/-/merge_requests/22560) in GitLab 11.5.
diff --git a/doc/user/admin_area/settings/external_authorization.md b/doc/user/admin_area/settings/external_authorization.md
index abf3d79b9ed..3a03e46fa1f 100644
--- a/doc/user/admin_area/settings/external_authorization.md
+++ b/doc/user/admin_area/settings/external_authorization.md
@@ -33,7 +33,7 @@ authorization service.
Whenever access is granted or denied this is logged in a logfile called
`external-policy-access-control.log`.
-Read more about logs GitLab keeps in the [omnibus documentation][omnibus-log-docs].
+Read more about logs GitLab keeps in the [omnibus documentation](https://docs.gitlab.com/omnibus/settings/logs.html).
## Configuration
@@ -62,7 +62,7 @@ The available required properties are:
When using TLS Authentication with a self signed certificate, the CA certificate
needs to be trusted by the openssl installation. When using GitLab installed using
Omnibus, learn to install a custom CA in the
-[omnibus documentation][omnibus-ssl-docs]. Alternatively learn where to install
+[omnibus documentation](https://docs.gitlab.com/omnibus/settings/ssl.html). Alternatively learn where to install
custom certificates using `openssl version -d`.
## How it works
@@ -127,6 +127,3 @@ questions that you know someone might ask.
Each scenario can be a third-level heading, e.g. `### Getting error message X`.
If you have none to add when creating a doc, leave this section in place
but commented out to help encourage others to add to it in the future. -->
-
-[omnibus-ssl-docs]: https://docs.gitlab.com/omnibus/settings/ssl.html
-[omnibus-log-docs]: https://docs.gitlab.com/omnibus/settings/logs.html
diff --git a/doc/user/admin_area/settings/index.md b/doc/user/admin_area/settings/index.md
index f0e7bf272a7..761d640c535 100644
--- a/doc/user/admin_area/settings/index.md
+++ b/doc/user/admin_area/settings/index.md
@@ -25,7 +25,7 @@ Access the default page for admin area settings by navigating to
| [Terms of Service and Privacy Policy](terms.md) | Include a Terms of Service agreement and Privacy Policy that all users must accept. |
| [External Authentication](external_authorization.md#configuration) | External Classification Policy Authorization |
| [Web terminal](../../../administration/integration/terminal.md#limiting-websocket-connection-time) | Set max session time for web terminal. |
-| [Web IDE](../../project/web_ide/index.md#enabling-client-side-evaluation) | Manage Web IDE Features. |
+| [Web IDE](../../project/web_ide/index.md#enabling-live-preview) | Manage Web IDE Features. |
## Integrations
@@ -35,7 +35,7 @@ Access the default page for admin area settings by navigating to
| [PlantUML](../../../administration/integration/plantuml.md#gitlab) | Allow rendering of PlantUML diagrams in Asciidoc documents. |
| [Slack application](../../../user/project/integrations/gitlab_slack_application.md#configuration) **(FREE ONLY)** | Slack integration allows you to interact with GitLab via slash commands in a chat window. This option is only available on GitLab.com, though it may be [available for self-managed instances in the future](https://gitlab.com/gitlab-org/gitlab/-/issues/28164). |
| [Third party offers](third_party_offers.md) | Control the display of third party offers. |
-| [Snowplow](../../../telemetry/index.md#enabling-tracking) | Configure the Snowplow integration. |
+| [Snowplow](../../../development/telemetry/snowplow.md) | Configure the Snowplow integration. |
| [Google GKE](../../project/clusters/add_gke_clusters.md) | Google GKE integration allows you to provision GKE clusters from GitLab. |
| [Amazon EKS](../../project/clusters/add_eks_clusters.md) | Amazon EKS integration allows you to provision EKS clusters from GitLab. |
@@ -44,7 +44,7 @@ Access the default page for admin area settings by navigating to
| Option | Description |
| ------ | ----------- |
| [Repository mirror](visibility_and_access_controls.md#allow-mirrors-to-be-set-up-for-projects) | Configure repository mirroring. |
-| [Repository storage](../../../administration/repository_storage_types.md#how-to-migrate-to-hashed-storage) | Configure storage path settings. |
+| [Repository storage](../../../administration/repository_storage_types.md) | Configure storage path settings. |
| Repository maintenance | ([Repository checks](../../../administration/repository_checks.md) and [Housekeeping](../../../administration/housekeeping.md)). Configure automatic Git checks and housekeeping on repositories. |
| [Repository static objects](../../../administration/static_objects_external_storage.md) | Serve repository static objects (for example, archives, blobs, ...) from an external storage (for example, a CDN). |
@@ -74,7 +74,6 @@ Access the default page for admin area settings by navigating to
| Option | Description |
| ------ | ----------- |
-| [Metrics - Influx](../../../administration/monitoring/performance/gitlab_configuration.md) | Enable and configure InfluxDB metrics. |
| [Metrics - Prometheus](../../../administration/monitoring/prometheus/gitlab_metrics.md) | Enable and configure Prometheus metrics. |
| [Metrics - Grafana](../../../administration/monitoring/performance/grafana_configuration.md#integration-with-gitlab-ui) | Enable and configure Grafana. |
| [Profiling - Performance bar](../../../administration/monitoring/performance/performance_bar.md#enable-the-performance-bar-via-the-admin-panel) | Enable access to the Performance Bar for a given group. |
@@ -103,7 +102,7 @@ Access the default page for admin area settings by navigating to
| Option | Description |
| ------ | ----------- |
| [Email](email.md) | Various email settings. |
-| [Help page](../../../customization/help_message.md) | Help page text and support page url. |
+| [Help page](../../../customization/help_message.md) | Help page text and support page URL. |
| [Pages](../../../administration/pages/index.md#custom-domain-verification) | Size and domain settings for static websites |
| [Real-time features](../../../administration/polling.md) | Change this value to influence how frequently the GitLab UI polls for updates. |
| [Gitaly timeouts](gitaly_timeouts.md) | Configure Gitaly timeouts. |
diff --git a/doc/user/admin_area/settings/instance_template_repository.md b/doc/user/admin_area/settings/instance_template_repository.md
index b297d48cb0a..bef4e31259f 100644
--- a/doc/user/admin_area/settings/instance_template_repository.md
+++ b/doc/user/admin_area/settings/instance_template_repository.md
@@ -40,7 +40,7 @@ are supported:
Each template must go in its respective subdirectory, have the correct
extension and not be empty. So, the hierarchy should look like this:
-```text
+```plaintext
|-- README.md
|-- Dockerfile
|-- custom_dockerfile.dockerfile
diff --git a/doc/user/admin_area/settings/protected_paths.md b/doc/user/admin_area/settings/protected_paths.md
index 56f99d3e725..0cfaf5843d0 100644
--- a/doc/user/admin_area/settings/protected_paths.md
+++ b/doc/user/admin_area/settings/protected_paths.md
@@ -54,24 +54,3 @@ customized on **Admin > Network > Protected Paths**, along with these options:
![protected-paths](img/protected_paths.png)
Requests over the rate limit are logged into `auth.log`.
-
-## Migrate settings from GitLab 12.3 and earlier
-
-Omnibus GitLab protected paths throttle is deprecated and is scheduled for removal in
-GitLab 13.0. Please see the [GitLab issue](https://gitlab.com/gitlab-org/gitlab/issues/29952) and the [Omnibus GitLab issue](https://gitlab.com/gitlab-org/omnibus-gitlab/issues/4688) for more information.
-
-NOTE: **Note:** If Omnibus settings are present, applications settings will be automatically ignored to avoid generating multiple requests blocks.
-
-To migrate from Omnibus GitLab 12.3 and earlier settings:
-
-1. Customize and enable your protected paths settings by following [Configure using GitLab UI](#configure-using-gitlab-ui) section.
-
-1. SSH into your frontend nodes and add to `/etc/gitlab/gitlab.rb`:
-
- ```ruby
- gitlab_rails['rack_attack_admin_area_protected_paths_enabled'] = true
- ```
-
-1. [Reconfigure GitLab](../../../administration/restart_gitlab.md#omnibus-gitlab-reconfigure) for the changes to take effect.
-
-That's it. Protected paths throttle are now managed by GitLab admin settings.
diff --git a/doc/user/admin_area/settings/rate_limit_on_issues_creation.md b/doc/user/admin_area/settings/rate_limit_on_issues_creation.md
index 96a20681b2f..dc23865e730 100644
--- a/doc/user/admin_area/settings/rate_limit_on_issues_creation.md
+++ b/doc/user/admin_area/settings/rate_limit_on_issues_creation.md
@@ -4,7 +4,7 @@ type: reference
# Rate limits on issue creation
-> [Introduced](https://gitlab.com/gitlab-org/gitlab/-/issues/55241) in GitLab 12.10.
+> [Introduced](https://gitlab.com/gitlab-org/gitlab/-/merge_requests/28129) in GitLab 12.10.
This setting allows you to rate limit the requests to the issue creation endpoint.
It defaults to 300 requests per minute.
diff --git a/doc/user/admin_area/settings/sign_up_restrictions.md b/doc/user/admin_area/settings/sign_up_restrictions.md
index a7a6b0ecefd..8ef5ac8dc8f 100644
--- a/doc/user/admin_area/settings/sign_up_restrictions.md
+++ b/doc/user/admin_area/settings/sign_up_restrictions.md
@@ -50,14 +50,14 @@ the minimum number of characters a user must have in their password using the Gi
## Whitelist email domains
-> [Introduced][ce-598] in GitLab 7.11.0
+> [Introduced](https://gitlab.com/gitlab-org/gitlab-foss/-/merge_requests/598) in GitLab 7.11.0
You can restrict users to only sign up using email addresses matching the given
domains list.
## Blacklist email domains
-> [Introduced][ce-5259] in GitLab 8.10.
+> [Introduced](https://gitlab.com/gitlab-org/gitlab-foss/-/merge_requests/5259) in GitLab 8.10.
With this feature enabled, you can block email addresses of a specific domain
from creating an account on your GitLab server. This is particularly useful
@@ -94,6 +94,3 @@ questions that you know someone might ask.
Each scenario can be a third-level heading, e.g. `### Getting error message X`.
If you have none to add when creating a doc, leave this section in place
but commented out to help encourage others to add to it in the future. -->
-
-[ce-5259]: https://gitlab.com/gitlab-org/gitlab-foss/-/merge_requests/5259
-[ce-598]: https://gitlab.com/gitlab-org/gitlab-foss/-/merge_requests/598
diff --git a/doc/user/admin_area/settings/terms.md b/doc/user/admin_area/settings/terms.md
index d89381fd810..77c172aa927 100644
--- a/doc/user/admin_area/settings/terms.md
+++ b/doc/user/admin_area/settings/terms.md
@@ -8,6 +8,8 @@ type: reference
An admin can enforce acceptance of a terms of service and privacy policy. When this option is enabled, new and existing users must accept the terms.
+If configured, the Terms of Service page can be viewed via `https://your-instance.com/-/users/terms` at anytime.
+
## Configuration
To enforce acceptance of a Terms of Service and Privacy Policy:
@@ -21,7 +23,7 @@ To enforce acceptance of a Terms of Service and Privacy Policy:
1. Click **Save changes**.
1. When you are presented with the **Terms of Service** statement, click **Accept terms**.
-![Enable enforcing Terms of Service](img/enforce_terms.png).
+![Enable enforcing Terms of Service](img/enforce_terms.png)
For each update to the terms, a new version is stored. When a user accepts or declines the terms,
GitLab will record which version they accepted or declined.
diff --git a/doc/user/admin_area/settings/usage_statistics.md b/doc/user/admin_area/settings/usage_statistics.md
index 7869f7de1b6..f3eb094887e 100644
--- a/doc/user/admin_area/settings/usage_statistics.md
+++ b/doc/user/admin_area/settings/usage_statistics.md
@@ -58,75 +58,7 @@ sequenceDiagram
## Usage Ping **(CORE ONLY)**
-> - [Introduced][ee-557] in GitLab Enterprise Edition 8.10.
-> - More statistics [were added][ee-735] in GitLab Enterprise Edition 8.12.
-> - [Moved to GitLab Core][ce-23361] in 9.1.
-> - More statistics [were added][ee-6602] in GitLab Ultimate 11.2.
-
-GitLab sends a weekly payload containing usage data to GitLab Inc. The usage
-ping uses high-level data to help our product, support, and sales teams. It does
-not send any project names, usernames, or any other specific data. The
-information from the usage ping is not anonymous, it is linked to the hostname
-of the instance.
-
-You can view the exact JSON payload in the administration panel. To view the payload:
-
-1. Navigate to the **Admin Area > Settings > Metrics and profiling**.
-1. Expand the **Usage statistics** section.
-1. Click the **Preview payload** button.
-
-You can see how [the usage ping data maps to different stages of the product](https://gitlab.com/gitlab-data/analytics/blob/master/transform/snowflake-dbt/data/version_usage_stats_to_stage_mappings.csv).
-
-Usage ping is important to GitLab as we use it to calculate our [Action Monthly Active Users (AMAU)](https://about.gitlab.com/handbook/product/metrics/#action-monthly-active-users-amau) which helps us measure the success of our features.
-
-### Request flow example
-
-The following example shows a basic request/response flow between the self-managed GitLab instance, GitLab Version Application,
-GitLab License Application and Salesforce:
-
-```mermaid
-sequenceDiagram
- participant GitLab instance
- participant Version Application
- participant License Application
- participant Salesforce
- GitLab instance->>Version Application: Usage Ping data
- loop Process Usage Data
- Version Application->>Version Application: Parse Usage Data
- Version Application->>Version Application: Record Usage Data
- Version Application->>Version Application: Update license ping time
- end
- Version Application-xLicense Application: Request Zuora subscription id
- License Application-xVersion Application: Zuora subscription id
- Version Application-xSalesforce: Request Zuora account id by Zuora subscription id
- Salesforce-xVersion Application: Zuora account id
- Version Application-xSalesforce: Usage data for the Zuora account
- Version Application->>GitLab instance: Conversational Development Index
-```
-
-### Deactivate the usage ping
-
-The usage ping is opt-out. If you want to deactivate this feature, go to
-the Settings page of your administration panel and uncheck the Usage ping
-checkbox.
-
-To disable the usage ping and prevent it from being configured in future through
-the administration panel, Omnibus installs can set the following in
-[`gitlab.rb`](https://docs.gitlab.com/omnibus/settings/configuration.html#configuration-options):
-
-```ruby
-gitlab_rails['usage_ping_enabled'] = false
-```
-
-And source installs can set the following in `gitlab.yml`:
-
-```yaml
-production: &base
- # ...
- gitlab:
- # ...
- usage_ping_enabled: false
-```
+See [Usage Ping guide](../../../development/telemetry/usage_ping.md).
## Instance statistics visibility **(CORE ONLY)**
@@ -148,307 +80,3 @@ questions that you know someone might ask.
Each scenario can be a third-level heading, e.g. `### Getting error message X`.
If you have none to add when creating a doc, leave this section in place
but commented out to help encourage others to add to it in the future. -->
-
-[ee-557]: https://gitlab.com/gitlab-org/gitlab/-/merge_requests/557
-[ee-735]: https://gitlab.com/gitlab-org/gitlab/-/merge_requests/735
-[ce-23361]: https://gitlab.com/gitlab-org/gitlab-foss/issues/23361
-[ee-6602]: https://gitlab.com/gitlab-org/gitlab/-/merge_requests/6602
-
-## Usage Statistics Collected
-
-| Statistic | Section | Stage | Description |
-|---|---|---|---|
-|uuid|||
-|hostname|||
-|version|||
-|installation_type|||
-|active_user_count|||
-|recorded_at|||
-|edition|||
-|license_md5|||
-|license_id|||
-|historical_max_users|||
-|Name|licensee||
-|Email|licensee||
-|Company|licensee||
-|license_user_count|||
-|license_starts_at|||
-|license_expires_at|||
-|license_plan|||
-|license_trial|||
-|assignee_lists|counts||
-|boards|counts||
-|ci_builds|counts||
-|ci_internal_pipelines|counts||
-|ci_external_pipelines|counts||
-|ci_pipeline_config_auto_devops|counts||
-|ci_pipeline_config_repository|counts||
-|ci_runners|counts||
-|ci_triggers|counts||
-|ci_pipeline_schedules|counts||
-|auto_devops_enabled|counts||
-|auto_devops_disabled|counts||
-|deploy_keys|counts||
-|deployments|counts||
-|successful_deployments|counts||
-|failed_deployments|counts||
-|environments|counts||
-|clusters|counts||
-|clusters_enabled|counts||
-|project_clusters_enabled|counts||
-|group_clusters_enabled|counts||
-|instance_clusters_enabled|counts||
-|clusters_disabled|counts||
-|project_clusters_disabled|counts||
-|group_clusters_disabled|counts||
-|instance_clusters_disabled|counts||
-|clusters_platforms_eks|counts||
-|clusters_platforms_gke|counts||
-|clusters_platforms_user|counts||
-|clusters_applications_helm|counts||
-|clusters_applications_ingress|counts||
-|clusters_applications_cert_managers|counts||
-|clusters_applications_crossplane|counts||
-|clusters_applications_prometheus|counts||
-|clusters_applications_runner|counts||
-|clusters_applications_knative|counts||
-|clusters_applications_elastic_stack|counts||
-|clusters_management_project|counts||
-|in_review_folder|counts||
-|grafana_integrated_projects|counts||
-|groups|counts||
-|issues|counts||
-|issues_created_from_gitlab_error_tracking_ui|counts||
-|issues_with_associated_zoom_link|counts||
-|issues_using_zoom_quick_actions|counts||
-|issues_with_embedded_grafana_charts_approx|counts||
-|issues_with_health_status|counts||
-|keys|counts||
-|label_lists|counts||
-|lfs_objects|counts||
-|milestone_lists|counts||
-|milestones|counts||
-|pages_domains|counts||
-|pool_repositories|counts||
-|projects|counts||
-|projects_imported_from_github|counts||
-|projects_with_repositories_enabled|counts||
-|projects_with_error_tracking_enabled|counts||
-|protected_branches|counts||
-|releases|counts||
-|remote_mirrors|counts||
-|snippets|counts||
-|suggestions|counts||
-|todos|counts||
-|uploads|counts||
-|web_hooks|counts||
-|projects_alerts_active|counts||
-|projects_asana_active|counts||
-|projects_assembla_active|counts||
-|projects_bamboo_active|counts||
-|projects_bugzilla_active|counts||
-|projects_buildkite_active|counts||
-|projects_campfire_active|counts||
-|projects_custom_issue_tracker_active|counts||
-|projects_discord_active|counts||
-|projects_drone_ci_active|counts||
-|projects_emails_on_push_active|counts||
-|projects_external_wiki_active|counts||
-|projects_flowdock_active|counts||
-|projects_github_active|counts||
-|projects_hangouts_chat_active|counts||
-|projects_hipchat_active|counts||
-|projects_irker_active|counts||
-|projects_jenkins_active|counts||
-|projects_jenkins_deprecated_active|counts||
-|projects_jira_active -|counts||
-|projects_mattermost_active|counts||
-|projects_mattermost_slash_commands_active|counts||
-|projects_microsoft_teams_active|counts||
-|projects_packagist_active|counts||
-|projects_pipelines_email_active|counts||
-|projects_pivotaltracker_active|counts||
-|projects_prometheus_active|counts||
-|projects_pushover_active|counts||
-|projects_redmine_active|counts||
-|projects_slack_active|counts||
-|projects_slack_slash_commands_active|counts||
-|projects_teamcity_active|counts||
-|projects_unify_circuit_active|counts||
-|projects_youtrack_active|counts||
-|projects_slack_notifications_active|counts||
-|projects_slack_slash_active|counts||
-|projects_jira_server_active|counts||
-|projects_jira_cloud_active|counts||
-|projects_jira_dvcs_cloud_active|counts||
-|projects_jira_dvcs_server_active|counts||
-|labels|counts||
-|merge_requests|counts||
-|notes|counts||
-|wiki_pages_create|counts||
-|wiki_pages_update|counts||
-|wiki_pages_delete|counts||
-|web_ide_commits|counts||
-|web_ide_views|counts||
-|web_ide_merge_requests|counts||
-|web_ide_previews|counts||
-|snippet_comment|counts||
-|commit_comment|counts||
-|merge_request_comment|counts||
-|snippet_create|counts||
-|snippet_update|counts||
-|navbar_searches|counts||
-|cycle_analytics_views|counts||
-|productivity_analytics_views|counts||
-|source_code_pushes|counts||
-|merge_request_create|counts||
-|design_management_designs_create|counts||
-|design_management_designs_update|counts||
-|design_management_designs_delete|counts||
-|licenses_list_views|counts||
-|user_preferences_group_overview_details|counts||
-|user_preferences_group_overview_security_dashboard|counts||
-|ingress_modsecurity_blocking|counts||
-|ingress_modsecurity_disabled|counts||
-|dependency_list_usages_total|counts||
-|epics|counts||
-|feature_flags|counts||
-|geo_nodes|counts||
-|incident_issues|counts||
-|ldap_group_links|counts||
-|ldap_keys|counts||
-|ldap_users|counts||
-|pod_logs_usages_total|counts||
-|projects_enforcing_code_owner_approval|counts||
-|projects_mirrored_with_pipelines_enabled|counts||
-|projects_reporting_ci_cd_back_to_github|counts||
-|projects_with_packages|counts||
-|projects_with_prometheus_alerts|counts||
-|projects_with_tracing_enabled|counts||
-|projects_with_alerts_service_enabled|counts||
-|template_repositories|counts||
-|container_scanning_jobs|counts||
-|dependency_scanning_jobs|counts||
-|license_management_jobs|counts||
-|sast_jobs|counts||
-|status_page_projects|counts|monitor|
-|status_page_issues|counts|monitor|
-|epics_deepest_relationship_level|counts||
-|operations_dashboard_default_dashboard|counts||
-|operations_dashboard_users_with_projects_added|counts||
-|container_registry_enabled|||
-|dependency_proxy_enabled|||
-|gitlab_shared_runners_enabled|||
-|gravatar_enabled|||
-|influxdb_metrics_enabled|||
-|ldap_enabled|||
-|mattermost_enabled|||
-|omniauth_enabled|||
-|prometheus_metrics_enabled|||
-|reply_by_email_enabled|||
-|signup_enabled|||
-|web_ide_clientside_preview_enabled|||
-|ingress_modsecurity_enabled|||
-|elasticsearch_enabled|||
-|license_trial_ends_on|||
-|geo_enabled|||
-|version|Git||
-|version|Gitaly||
-|servers|Gitaly||
-|filesystems|Gitaly||
-|enabled|gitlab_pages||
-|version|gitlab_pages||
-|adapter|database||
-|version|database||
-|average|avg_cycle_analytics - issue||
-|sd|avg_cycle_analytics - issue||
-|missing|avg_cycle_analytics - issue||
-|average|avg_cycle_analytics - plan||
-|sd|avg_cycle_analytics - plan||
-|missing|avg_cycle_analytics - plan||
-|average|avg_cycle_analytics - code||
-|sd|avg_cycle_analytics - code||
-|missing|avg_cycle_analytics - code||
-|average|avg_cycle_analytics - test||
-|sd|avg_cycle_analytics - test||
-|missing|avg_cycle_analytics - test||
-|average|avg_cycle_analytics - review||
-|sd|avg_cycle_analytics - review||
-|missing|avg_cycle_analytics - review||
-|average|avg_cycle_analytics - staging||
-|sd|avg_cycle_analytics - staging||
-|missing|avg_cycle_analytics - staging||
-|average|avg_cycle_analytics - production||
-|sd|avg_cycle_analytics - production||
-|missing|avg_cycle_analytics - production||
-|total|avg_cycle_analytics||
-|clusters_applications_cert_managers|usage_activity_by_stage|configure|
-|clusters_applications_helm|usage_activity_by_stage|configure|
-|clusters_applications_ingress|usage_activity_by_stage|configure|
-|clusters_applications_knative|usage_activity_by_stage|configure|
-|clusters_management_project|usage_activity_by_stage|configure|
-|clusters_disabled|usage_activity_by_stage|configure|
-|clusters_enabled|usage_activity_by_stage|configure|
-|clusters_platforms_gke|usage_activity_by_stage|configure|
-|clusters_platforms_eks|usage_activity_by_stage|configure|
-|clusters_platforms_user|usage_activity_by_stage|configure|
-|instance_clusters_disabled|usage_activity_by_stage|configure|
-|instance_clusters_enabled|usage_activity_by_stage|configure|
-|group_clusters_disabled|usage_activity_by_stage|configure|
-|group_clusters_enabled|usage_activity_by_stage|configure|
-|project_clusters_disabled|usage_activity_by_stage|configure|
-|project_clusters_enabled|usage_activity_by_stage|configure|
-|projects_slack_notifications_active|usage_activity_by_stage|configure|
-|projects_slack_slash_active|usage_activity_by_stage|configure|
-|projects_with_prometheus_alerts: 0|usage_activity_by_stage|configure|
-|deploy_keys|usage_activity_by_stage|create|
-|keys|usage_activity_by_stage|create|
-|merge_requests|usage_activity_by_stage|create|
-|projects_enforcing_code_owner_approval|usage_activity_by_stage|create|
-|projects_imported_from_github|usage_activity_by_stage|create|
-|projects_with_repositories_enabled|usage_activity_by_stage|create|
-|protected_branches|usage_activity_by_stage|create|
-|remote_mirrors|usage_activity_by_stage|create|
-|snippets|usage_activity_by_stage|create|
-|suggestions:|usage_activity_by_stage|create|
-|groups|usage_activity_by_stage|manage|
-|ldap_keys|usage_activity_by_stage|manage|
-|ldap_users: 0|usage_activity_by_stage|manage|
-|users_created|usage_activity_by_stage|manage|
-|clusters|usage_activity_by_stage|monitor|
-|clusters_applications_prometheus|usage_activity_by_stage|monitor|
-|operations_dashboard_default_dashboard|usage_activity_by_stage|monitor|
-|operations_dashboard_users_with_projects_added|usage_activity_by_stage|monitor|
-|projects_prometheus_active|usage_activity_by_stage|monitor|
-|projects_with_error_tracking_enabled|usage_activity_by_stage|monitor|
-|projects_with_tracing_enabled: 0|usage_activity_by_stage|monitor|
-|projects_with_packages: 0|usage_activity_by_stage|package|
-|assignee_lists|usage_activity_by_stage|plan|
-|epics|usage_activity_by_stage|plan|
-|issues|usage_activity_by_stage|plan|
-|label_lists|usage_activity_by_stage|plan|
-|milestone_lists|usage_activity_by_stage|plan|
-|notes|usage_activity_by_stage|plan|
-|projects|usage_activity_by_stage|plan|
-|projects_jira_active|usage_activity_by_stage|plan|
-|projects_jira_dvcs_cloud_active|usage_activity_by_stage|plan|
-|projects_jira_dvcs_server_active|usage_activity_by_stage|plan|
-|service_desk_enabled_projects|usage_activity_by_stage|plan|
-|service_desk_issues|usage_activity_by_stage|plan|
-|todos: 0|usage_activity_by_stage|plan|
-|deployments|usage_activity_by_stage|release|
-|failed_deployments|usage_activity_by_stage|release|
-|projects_mirrored_with_pipelines_enabled|usage_activity_by_stage|release|
-|releases|usage_activity_by_stage|release|
-|successful_deployments: 0|usage_activity_by_stage|release|
-|user_preferences_group_overview_security_dashboard: 0|usage_activity_by_stage|secure|
-|ci_builds|usage_activity_by_stage|verify|
-|ci_external_pipelines|usage_activity_by_stage|verify|
-|ci_internal_pipelines|usage_activity_by_stage|verify|
-|ci_pipeline_config_auto_devops|usage_activity_by_stage|verify|
-|ci_pipeline_config_repository|usage_activity_by_stage|verify|
-|ci_pipeline_schedules|usage_activity_by_stage|verify|
-|ci_pipelines|usage_activity_by_stage|verify|
-|ci_triggers|usage_activity_by_stage|verify|
-|clusters_applications_runner|usage_activity_by_stage|verify|
-|projects_reporting_ci_cd_back_to_github: 0|usage_activity_by_stage|verify|
diff --git a/doc/user/admin_area/settings/visibility_and_access_controls.md b/doc/user/admin_area/settings/visibility_and_access_controls.md
index f827fed833b..95748f68a55 100644
--- a/doc/user/admin_area/settings/visibility_and_access_controls.md
+++ b/doc/user/admin_area/settings/visibility_and_access_controls.md
@@ -28,6 +28,21 @@ For more details, see [Protected branches](../../project/protected_branches.md).
To change this setting for a specific group, see [Default branch protection for groups](../../group/index.md#changing-the-default-branch-protection-of-a-group)
+### Disable group owners from updating default branch protection **(PREMIUM ONLY)**
+
+> [Introduced](https://gitlab.com/gitlab-org/gitlab/-/issues/211944) in GitLab 13.0.
+
+By default, group owners are allowed to override the branch protection set at the global level.
+
+In [GitLab Premium or higher](https://about.gitlab.com/pricing/), GitLab administrators can disable this privilege of group owners.
+
+To do this:
+
+1. Uncheck the **Allow owners to manage default branch protection per group** checkbox.
+
+NOTE: **Note:**
+GitLab administrators can still update the default branch protection of a group.
+
## Default project creation protection
Project creation protection specifies which roles can create projects.
@@ -62,6 +77,13 @@ To change this period:
1. Select the desired option.
1. Click **Save changes**.
+### Override default deletion adjourned period
+
+Alternatively, projects that are marked for removal can be deleted immediately. To do so:
+
+1. [Restore the project](../../project/settings/#restore-a-project-premium).
+1. Delete the project as described in the [Administering Projects page](../../admin_area/#administering-projects).
+
## Default project visibility
To set the default visibility levels for new projects:
diff --git a/doc/user/analytics/code_review_analytics.md b/doc/user/analytics/code_review_analytics.md
index bb74e673b56..e0aa01c29b2 100644
--- a/doc/user/analytics/code_review_analytics.md
+++ b/doc/user/analytics/code_review_analytics.md
@@ -1,7 +1,12 @@
---
description: "Learn how long your open merge requests have spent in code review, and what distinguishes the longest-running." # Up to ~200 chars long. They will be displayed in Google Search snippets. It may help to write the page intro first, and then reuse it here.
+stage: Manage
+group: Analytics
+To determine the technical writer assigned to the Stage/Group associated with this page, see:
+ https://about.gitlab.com/handbook/engineering/ux/technical-writing/#designated-technical-writers
---
+
# Code Review Analytics **(STARTER)**
> [Introduced](https://gitlab.com/gitlab-org/gitlab/issues/38062) in [GitLab Starter](https://about.gitlab.com/pricing/) 12.7.
diff --git a/doc/user/analytics/img/repository_analytics_v13_0.png b/doc/user/analytics/img/repository_analytics_v13_0.png
new file mode 100644
index 00000000000..b70b63a6239
--- /dev/null
+++ b/doc/user/analytics/img/repository_analytics_v13_0.png
Binary files differ
diff --git a/doc/user/analytics/index.md b/doc/user/analytics/index.md
index b2f7da234ad..48df7bc340a 100644
--- a/doc/user/analytics/index.md
+++ b/doc/user/analytics/index.md
@@ -1,3 +1,10 @@
+---
+stage: Manage
+group: Analytics
+To determine the technical writer assigned to the Stage/Group associated with this page, see:
+ https://about.gitlab.com/handbook/engineering/ux/technical-writing/#designated-technical-writers
+---
+
# Analytics
## Analytics workspace
@@ -32,6 +39,6 @@ The following analytics features are available at the project level:
- [Code Review](code_review_analytics.md). **(STARTER)**
- [Insights](../group/insights/index.md). **(ULTIMATE)**
- [Issues](../group/issues_analytics/index.md). **(PREMIUM)**
-- Repository. **(STARTER)**
+- [Repository](repository_analytics.md).
- [Value Stream](value_stream_analytics.md), enabled with the `cycle_analytics`
[feature flag](../../development/feature_flags/development.md#enabling-a-feature-flag-in-development). **(STARTER)**
diff --git a/doc/user/analytics/productivity_analytics.md b/doc/user/analytics/productivity_analytics.md
index 0fa990150d7..d0fda61d6a5 100644
--- a/doc/user/analytics/productivity_analytics.md
+++ b/doc/user/analytics/productivity_analytics.md
@@ -1,3 +1,10 @@
+---
+stage: Manage
+group: Analytics
+To determine the technical writer assigned to the Stage/Group associated with this page, see:
+ https://about.gitlab.com/handbook/engineering/ux/technical-writing/#designated-technical-writers
+---
+
# Productivity Analytics **(PREMIUM)**
> [Introduced](https://gitlab.com/gitlab-org/gitlab/issues/12079) in [GitLab Premium](https://about.gitlab.com/pricing/) 12.3.
diff --git a/doc/user/analytics/repository_analytics.md b/doc/user/analytics/repository_analytics.md
new file mode 100644
index 00000000000..17032990b09
--- /dev/null
+++ b/doc/user/analytics/repository_analytics.md
@@ -0,0 +1,40 @@
+---
+stage: Manage
+group: Analytics
+To determine the technical writer assigned to the Stage/Group associated with this page, see:
+ https://about.gitlab.com/handbook/engineering/ux/technical-writing/#designated-technical-writers
+---
+
+# Repository Analytics
+
+Get high-level overview of the project's Git repository.
+
+![Repository Analytics](img/repository_analytics_v13_0.png)
+
+## Availability
+
+Repository Analytics is part of [GitLab Community Edition](https://gitlab.com/gitlab-org/gitlab-foss). It's available to anyone who has permission to clone the repository.
+
+The feature requires:
+
+- An initialized Git repository.
+- At least one commit in the default branch (`master` by default).
+
+## Overview
+
+You can find Repository Analytics in the project's sidebar. To access the page, go to **{chart}** **Analytics > Repository**.
+
+NOTE: **Note:**
+Without a Git commit in the default branch, the menu item won't be visible.
+
+### Charts
+
+The data in the charts are updated soon after each commit in the default branch.
+
+Available charts:
+
+- Programming languages used in the repository
+- Commit statistics (last month)
+- Commits per day of month
+- Commits per weekday
+- Commits per day hour (UTC)
diff --git a/doc/user/analytics/value_stream_analytics.md b/doc/user/analytics/value_stream_analytics.md
index 32f393d342b..90a4f96f00b 100644
--- a/doc/user/analytics/value_stream_analytics.md
+++ b/doc/user/analytics/value_stream_analytics.md
@@ -1,3 +1,10 @@
+---
+stage: Manage
+group: Analytics
+To determine the technical writer assigned to the Stage/Group associated with this page, see:
+ https://about.gitlab.com/handbook/engineering/ux/technical-writing/#designated-technical-writers
+---
+
# Value Stream Analytics
> - Introduced as Cycle Analytics prior to GitLab 12.3 at the project level.
@@ -11,9 +18,6 @@ spent in each stage defined in the process.
For information on how to contribute to the development of Value Stream Analytics, see our [contributor documentation](../../development/value_stream_analytics.md).
-NOTE: **Note:**
-Use the `cycle_analytics` feature flag to enable at the group level.
-
Value Stream Analytics is useful in order to quickly determine the velocity of a given
project. It points to bottlenecks in the development process, enabling management
to uncover, triage, and identify the root cause of slowdowns in the software development life cycle.
@@ -26,7 +30,7 @@ calculates a separate median for each stage.
Value Stream Analytics is available:
- From GitLab 12.9, at the group level via **Group > Analytics > Value Stream**. **(PREMIUM)**
-- At the project level via **Project > Value Stream Analytics**.
+- At the project level via **Project > Analytics > Value Stream**.
There are seven stages that are tracked as part of the Value Stream Analytics calculations.
@@ -69,7 +73,7 @@ Each stage of Value Stream Analytics is further described in the table below.
| **Stage** | **Description** |
| --------- | --------------- |
-| Issue | Measures the median time between creating an issue and taking action to solve it, by either labeling it or adding it to a milestone, whatever comes first. The label will be tracked only if it already has an [Issue Board list](../project/issue_board.md#creating-a-new-list) created for it. |
+| Issue | Measures the median time between creating an issue and taking action to solve it, by either labeling it or adding it to a milestone, whatever comes first. The label will be tracked only if it already has an [Issue Board list](../project/issue_board.md) created for it. |
| Plan | Measures the median time between the action you took for the previous stage, and pushing the first commit to the branch. The very first commit of the branch is the one that triggers the separation between **Plan** and **Code**, and at least one of the commits in the branch needs to contain the related issue number (e.g., `#42`). If none of the commits in the branch mention the related issue number, it is not considered to the measurement time of the stage. |
| Code | Measures the median time between pushing a first commit (previous stage) and creating a merge request (MR) related to that commit. The key to keep the process tracked is to include the [issue closing pattern](../project/issues/managing_issues.md#closing-issues-automatically) to the description of the merge request (for example, `Closes #xxx`, where `xxx` is the number of the issue related to this merge request). If the issue closing pattern is not present in the merge request description, the MR is not considered to the measurement time of the stage. |
| Test | Measures the median time to run the entire pipeline for that project. It's related to the time GitLab CI/CD takes to run every job for the commits pushed to that merge request defined in the previous stage. It is basically the start->finish time for all pipelines. |
@@ -165,6 +169,21 @@ development workflow.
NOTE: **Note:**
Customizability is [only available for group-level](https://gitlab.com/gitlab-org/gitlab/-/issues/35823#note_272558950) Value Stream Analytics.
+### Stage path
+
+> [Introduced](https://gitlab.com/gitlab-org/gitlab/-/issues/210315) in GitLab 13.0.
+
+Stages are visually depicted as a horizontal process flow. Selecting a stage will update the
+the content below the value stream.
+
+This is disabled by default. If you have a self-managed instance, an
+administrator can [open a Rails console](../../administration/troubleshooting/navigating_gitlab_via_rails_console.md)
+and enable it with the following command:
+
+```ruby
+Feature.enable(:value_stream_analytics_path_navigation)
+```
+
### Adding a stage
In the following example we're creating a new stage that measures and tracks issues from creation
@@ -293,15 +312,6 @@ toggled to show data for merge requests and further refined for specific group-l
By default the top group-level labels (max. 10) are pre-selected, with the ability to
select up to a total of 15 labels.
-### Disabling chart
-
-This chart is enabled by default. If you have a self-managed instance, an
-administrator can open a Rails console and disable it with the following command:
-
-```ruby
-Feature.disable(:tasks_by_type_chart)
-```
-
## Permissions
The current permissions on the Project Value Stream Analytics dashboard are:
@@ -324,14 +334,6 @@ For Value Stream Analytics functionality introduced in GitLab 12.3 and later:
- Features are available only on
[Premium or Silver tiers](https://about.gitlab.com/pricing/) and above.
-## Troubleshooting
-
-If you see an error as listed in the following table, try the noted solution:
-
-| Error | Solution |
-|---------------------------------------------|------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------|
-| There was an error fetching the top labels. | Manually enable tasks by type feature in the [rails console](../../administration/troubleshooting/navigating_gitlab_via_rails_console.md#starting-a-rails-console-session), specifically `Feature.enable(:tasks_by_type_chart)`. |
-
## More resources
Learn more about Value Stream Analytics in the following resources:
diff --git a/doc/user/application_security/configuration/index.md b/doc/user/application_security/configuration/index.md
index 131247910ab..dfddbde379d 100644
--- a/doc/user/application_security/configuration/index.md
+++ b/doc/user/application_security/configuration/index.md
@@ -11,7 +11,7 @@ type: reference, howto
The security configuration page displays the configuration state of each of the security
features and can be accessed through a project's sidebar nav.
-![Screenshot of security configuration page](../img/security_configuration_page_v12_9.png)
+![Screenshot of security configuration page](../img/security_configuration_page_v13_1.png)
The page uses the project's latest default branch [CI pipeline](../../../ci/pipelines/index.md) to determine the configuration
state of each feature. If a job with the expected security report artifact exists in the pipeline,
diff --git a/doc/user/application_security/container_scanning/img/container_scanning_v12_9.png b/doc/user/application_security/container_scanning/img/container_scanning_v12_9.png
deleted file mode 100644
index 13cacc6a489..00000000000
--- a/doc/user/application_security/container_scanning/img/container_scanning_v12_9.png
+++ /dev/null
Binary files differ
diff --git a/doc/user/application_security/container_scanning/img/container_scanning_v13_0.png b/doc/user/application_security/container_scanning/img/container_scanning_v13_0.png
new file mode 100644
index 00000000000..7a079a65072
--- /dev/null
+++ b/doc/user/application_security/container_scanning/img/container_scanning_v13_0.png
Binary files differ
diff --git a/doc/user/application_security/container_scanning/index.md b/doc/user/application_security/container_scanning/index.md
index 68ad2d427dd..6e52d7dbdcf 100644
--- a/doc/user/application_security/container_scanning/index.md
+++ b/doc/user/application_security/container_scanning/index.md
@@ -22,7 +22,7 @@ GitLab checks the Container Scanning report, compares the found vulnerabilities
between the source and target branches, and shows the information right on the
merge request.
-![Container Scanning Widget](img/container_scanning_v12_9.png)
+![Container Scanning Widget](img/container_scanning_v13_0.png)
## Contribute your scanner
@@ -59,7 +59,7 @@ To enable Container Scanning in your pipeline, you need:
[predefined environment variables](../../../ci/variables/predefined_variables.md)
as defined below:
- ```text
+ ```plaintext
$CI_REGISTRY_IMAGE/$CI_COMMIT_REF_SLUG:$CI_COMMIT_SHA
```
@@ -103,7 +103,7 @@ The included template will:
and scan it for possible vulnerabilities.
The results will be saved as a
-[Container Scanning report artifact](../../../ci/yaml/README.md#artifactsreportscontainer_scanning-ultimate)
+[Container Scanning report artifact](../../../ci/pipelines/job_artifacts.md#artifactsreportscontainer_scanning-ultimate)
that you can later download and analyze.
Due to implementation limitations, we always take the latest Container Scanning
artifact available. Behind the scenes, the
@@ -169,6 +169,7 @@ using environment variables.
| Environment Variable | Description | Default |
| ------ | ------ | ------ |
+| `SECURE_ANALYZERS_PREFIX` | Set the Docker registry base address from which to download the analyzer. | `"registry.gitlab.com/gitlab-org/security-products/analyzers"` |
| `KLAR_TRACE` | Set to true to enable more verbose output from klar. | `"false"` |
| `CLAIR_TRACE` | Set to true to enable more verbose output from the clair server process. | `"false"` |
| `DOCKER_USER` | Username for accessing a Docker registry requiring authentication. | `$CI_REGISTRY_USER` |
@@ -179,11 +180,11 @@ using environment variables.
| `CLAIR_VULNERABILITIES_DB_URL` | (**DEPRECATED - use `CLAIR_DB_CONNECTION_STRING` instead**) This variable is explicitly set in the [services section](https://gitlab.com/gitlab-org/gitlab/-/blob/898c5da43504eba87b749625da50098d345b60d6/lib/gitlab/ci/templates/Security/Container-Scanning.gitlab-ci.yml#L23) of the `Container-Scanning.gitlab-ci.yml` file and defaults to `clair-vulnerabilities-db`. This value represents the address that the [PostgreSQL server hosting the vulnerabilities definitions](https://hub.docker.com/r/arminc/clair-db) is running on and **shouldn't be changed** unless you're running the image locally as described in the [Running the standalone Container Scanning Tool](#running-the-standalone-container-scanning-tool) section. | `clair-vulnerabilities-db` |
| `CLAIR_DB_CONNECTION_STRING` | This variable represents the [connection string](https://www.postgresql.org/docs/9.3/libpq-connect.html#AEN39692) to the [PostgreSQL server hosting the vulnerabilities definitions](https://hub.docker.com/r/arminc/clair-db) database and **shouldn't be changed** unless you're running the image locally as described in the [Running the standalone Container Scanning Tool](#running-the-standalone-container-scanning-tool) section. The host value for the connection string must match the [alias](https://gitlab.com/gitlab-org/gitlab/-/blob/898c5da43504eba87b749625da50098d345b60d6/lib/gitlab/ci/templates/Security/Container-Scanning.gitlab-ci.yml#L23) value of the `Container-Scanning.gitlab-ci.yml` template file, which defaults to `clair-vulnerabilities-db`. | `postgresql://postgres:password@clair-vulnerabilities-db:5432/postgres?sslmode=disable&statement_timeout=60000` |
| `CI_APPLICATION_REPOSITORY` | Docker repository URL for the image to be scanned. | `$CI_REGISTRY_IMAGE/$CI_COMMIT_REF_SLUG` |
-| `CI_APPLICATION_TAG` | Docker respository tag for the image to be scanned. | `$CI_COMMIT_SHA` |
+| `CI_APPLICATION_TAG` | Docker repository tag for the image to be scanned. | `$CI_COMMIT_SHA` |
| `CLAIR_DB_IMAGE` | The Docker image name and tag for the [PostgreSQL server hosting the vulnerabilities definitions](https://hub.docker.com/r/arminc/clair-db). It can be useful to override this value with a specific version, for example, to provide a consistent set of vulnerabilities for integration testing purposes, or to refer to a locally hosted vulnerabilities database for an on-premise offline installation. | `arminc/clair-db:latest` |
| `CLAIR_DB_IMAGE_TAG` | (**DEPRECATED - use `CLAIR_DB_IMAGE` instead**) The Docker image tag for the [PostgreSQL server hosting the vulnerabilities definitions](https://hub.docker.com/r/arminc/clair-db). It can be useful to override this value with a specific version, for example, to provide a consistent set of vulnerabilities for integration testing purposes. | `latest` |
| `DOCKERFILE_PATH` | The path to the `Dockerfile` to be used for generating remediations. By default, the scanner will look for a file named `Dockerfile` in the root directory of the project, so this variable should only be configured if your `Dockerfile` is in a non-standard location, such as a subdirectory. See [Solutions for vulnerabilities](#solutions-for-vulnerabilities-auto-remediation) for more details. | `Dockerfile` |
-| `ADDITIONAL_CA_CERT_BUNDLE` | Bundle of CA certs that you want to trust. | "" |
+| `ADDITIONAL_CA_CERT_BUNDLE` | Bundle of CA certs that you want to trust. | "" |
### Overriding the Container Scanning template
@@ -229,25 +230,29 @@ To use Container Scanning in an offline environment, you need:
NOTE: **Note:**
GitLab Runner has a [default `pull policy` of `always`](https://docs.gitlab.com/runner/executors/docker.html#using-the-always-pull-policy),
-meaning the runner may try to pull remote images even if a local copy is available. Set GitLab
-Runner's [`pull_policy` to `if-not-present`](https://docs.gitlab.com/runner/executors/docker.html#using-the-if-not-present-pull-policy)
-in an offline environment if you prefer using only locally available Docker images.
+meaning the Runner tries to pull Docker images from the GitLab container registry even if a local
+copy is available. GitLab Runner's [`pull_policy` can be set to `if-not-present`](https://docs.gitlab.com/runner/executors/docker.html#using-the-if-not-present-pull-policy)
+in an offline environment if you prefer using only locally available Docker images. However, we
+recommend keeping the pull policy setting to `always` if not in an offline environment, as this
+enables the use of updated scanners in your CI/CD pipelines.
#### Make GitLab Container Scanning analyzer images available inside your Docker registry
-For Container Scanning, import and host the following images from `registry.gitlab.com` to your
-offline [local Docker container registry](../../packages/container_registry/index.md):
+For Container Scanning, import the following default images from `registry.gitlab.com` into your
+[local Docker container registry](../../packages/container_registry/index.md):
-- [arminc/clair-db vulnerabilities database](https://hub.docker.com/r/arminc/clair-db)
-- GitLab klar analyzer: `registry.gitlab.com/gitlab-org/security-products/analyzers/klar`
+```plaintext
+registry.gitlab.com/gitlab-org/security-products/analyzers/klar
+https://hub.docker.com/r/arminc/clair-db
+```
The process for importing Docker images into a local offline Docker registry depends on
**your network security policy**. Please consult your IT staff to find an accepted and approved
-process by which external resources can be imported or temporarily accessed.
-
-Note that these scanners are [updated periodically](../index.md#maintenance-and-update-of-the-vulnerabilities-database)
+process by which you can import or temporarily access external resources. Note that these scanners
+are [updated periodically](../index.md#maintenance-and-update-of-the-vulnerabilities-database)
with new definitions, so consider if you are able to make periodic updates yourself.
-You can read more specific steps on how to do this [below](#automating-container-scanning-vulnerability-database-updates-with-a-pipeline).
+
+For more information, see [the specific steps on how to update an image with a pipeline](#automating-container-scanning-vulnerability-database-updates-with-a-pipeline).
For details on saving and transporting Docker images as a file, see Docker's documentation on
[`docker save`](https://docs.docker.com/engine/reference/commandline/save/), [`docker load`](https://docs.docker.com/engine/reference/commandline/load/),
@@ -255,8 +260,6 @@ For details on saving and transporting Docker images as a file, see Docker's doc
#### Set Container Scanning CI job variables to use local Container Scanner analyzers
-Container Scanning can be executed on an offline GitLab Ultimate installation using the following process:
-
1. [Override the container scanning template](#overriding-the-container-scanning-template) in your `.gitlab-ci.yml` file to refer to the Docker images hosted on your local Docker container registry:
```yaml
@@ -412,11 +415,11 @@ the report JSON unless stated otherwise. Presence of optional fields depends on
| `vulnerabilities[].message` | A short text that describes the vulnerability, it may include occurrence's specific information. Optional. |
| `vulnerabilities[].description` | A long text that describes the vulnerability. Optional. |
| `vulnerabilities[].cve` | (**DEPRECATED - use `vulnerabilities[].id` instead**) A fingerprint string value that represents a concrete occurrence of the vulnerability. It's used to determine whether two vulnerability occurrences are same or different. May not be 100% accurate. **This is NOT a [CVE](https://cve.mitre.org/)**. |
-| `vulnerabilities[].severity` | How much the vulnerability impacts the software. Possible values: `Undefined` (an analyzer has not provided this information), `Info`, `Unknown`, `Low`, `Medium`, `High`, `Critical`. **Note:** Our current container scanning tool based on [klar](https://github.com/optiopay/klar) only provides the following levels: `Unknown`, `Low`, `Medium`, `High`, `Critical`. |
-| `vulnerabilities[].confidence` | How reliable the vulnerability's assessment is. Possible values: `Undefined` (an analyzer has not provided this information), `Ignore`, `Unknown`, `Experimental`, `Low`, `Medium`, `High`, `Confirmed`. **Note:** Our current container scanning tool based on [klar](https://github.com/optiopay/klar) does not provide a confidence level, so this value is currently hardcoded to `Unknown`. |
+| `vulnerabilities[].severity` | How much the vulnerability impacts the software. Possible values: `Info`, `Unknown`, `Low`, `Medium`, `High`, `Critical`. **Note:** Our current container scanning tool based on [klar](https://github.com/optiopay/klar) only provides the following levels: `Unknown`, `Low`, `Medium`, `High`, `Critical`. |
+| `vulnerabilities[].confidence` | How reliable the vulnerability's assessment is. Possible values: `Ignore`, `Unknown`, `Experimental`, `Low`, `Medium`, `High`, `Confirmed`. **Note:** Our current container scanning tool based on [klar](https://github.com/optiopay/klar) does not provide a confidence level, so this value is currently hardcoded to `Unknown`. |
| `vulnerabilities[].solution` | Explanation of how to fix the vulnerability. Optional. |
| `vulnerabilities[].scanner` | A node that describes the analyzer used to find this vulnerability. |
-| `vulnerabilities[].scanner.id` | Id of the scanner as a snake_case string. |
+| `vulnerabilities[].scanner.id` | ID of the scanner as a snake_case string. |
| `vulnerabilities[].scanner.name` | Name of the scanner, for display purposes. |
| `vulnerabilities[].location` | A node that tells where the vulnerability is located. |
| `vulnerabilities[].location.dependency` | A node that describes the dependency of a project where the vulnerability is located. |
@@ -435,7 +438,7 @@ the report JSON unless stated otherwise. Presence of optional fields depends on
| `vulnerabilities[].links[].url` | URL of the vulnerability details document. Optional. |
| `remediations` | An array of objects containing information on cured vulnerabilities along with patch diffs to apply. Empty if no remediations provided by an underlying analyzer. |
| `remediations[].fixes` | An array of strings that represent references to vulnerabilities fixed by this particular remediation. |
-| `remediations[].fixes[].id` | The id of a fixed vulnerability. |
+| `remediations[].fixes[].id` | The ID of a fixed vulnerability. |
| `remediations[].fixes[].cve` | (**DEPRECATED - use `remediations[].fixes[].id` instead**) A string value that describes a fixed vulnerability in the same format as `vulnerabilities[].cve`. |
| `remediations[].summary` | Overview of how the vulnerabilities have been fixed. |
| `remediations[].diff` | base64-encoded remediation code diff, compatible with [`git apply`](https://git-scm.com/docs/git-format-patch#_discussion). |
@@ -476,7 +479,7 @@ When the GitLab Runner uses the Docker executor and NFS is used
(for example, `/var/lib/docker` is on an NFS mount), Container Scanning might fail with
an error like the following:
-```text
+```plaintext
docker: Error response from daemon: failed to copy xattrs: failed to set xattr "security.selinux" on /path/to/file: operation not supported.
```
diff --git a/doc/user/application_security/dast/img/dast_all_v12_9.png b/doc/user/application_security/dast/img/dast_all_v12_9.png
deleted file mode 100644
index 548cea3f7f9..00000000000
--- a/doc/user/application_security/dast/img/dast_all_v12_9.png
+++ /dev/null
Binary files differ
diff --git a/doc/user/application_security/dast/img/dast_all_v13_0.png b/doc/user/application_security/dast/img/dast_all_v13_0.png
new file mode 100644
index 00000000000..7b67fc44fae
--- /dev/null
+++ b/doc/user/application_security/dast/img/dast_all_v13_0.png
Binary files differ
diff --git a/doc/user/application_security/dast/img/dast_single_v12_9.png b/doc/user/application_security/dast/img/dast_single_v12_9.png
deleted file mode 100644
index a8a4b1c1d4f..00000000000
--- a/doc/user/application_security/dast/img/dast_single_v12_9.png
+++ /dev/null
Binary files differ
diff --git a/doc/user/application_security/dast/img/dast_single_v13_0.png b/doc/user/application_security/dast/img/dast_single_v13_0.png
new file mode 100644
index 00000000000..91ed4d916ab
--- /dev/null
+++ b/doc/user/application_security/dast/img/dast_single_v13_0.png
Binary files differ
diff --git a/doc/user/application_security/dast/index.md b/doc/user/application_security/dast/index.md
index abf194aae48..1d5f96d96bb 100644
--- a/doc/user/application_security/dast/index.md
+++ b/doc/user/application_security/dast/index.md
@@ -19,42 +19,48 @@ Dynamic Application Security Testing (DAST) comes into place.
## Overview
-If you are using [GitLab CI/CD](../../../ci/README.md), you can analyze your running web application(s)
+If you're using [GitLab CI/CD](../../../ci/README.md), you can analyze your running web applications
for known vulnerabilities using Dynamic Application Security Testing (DAST).
-
You can take advantage of DAST by either [including the CI job](#configuration) in
your existing `.gitlab-ci.yml` file or by implicitly using
-[Auto DAST](../../../topics/autodevops/stages.md#auto-dast-ultimate)
-that is provided by [Auto DevOps](../../../topics/autodevops/index.md).
+[Auto DAST](../../../topics/autodevops/stages.md#auto-dast-ultimate),
+provided by [Auto DevOps](../../../topics/autodevops/index.md).
GitLab checks the DAST report, compares the found vulnerabilities between the source and target
-branches, and shows the information right on the merge request.
+branches, and shows the information on the merge request.
NOTE: **Note:**
This comparison logic uses only the latest pipeline executed for the target branch's base commit.
Running the pipeline on any other commit has no effect on the merge request.
-![DAST Widget](img/dast_all_v12_9.png)
+![DAST Widget](img/dast_all_v13_0.png)
-By clicking on one of the detected linked vulnerabilities, you will be able to
+By clicking on one of the detected linked vulnerabilities, you can
see the details and the URL(s) affected.
-![DAST Widget Clicked](img/dast_single_v12_9.png)
+![DAST Widget Clicked](img/dast_single_v13_0.png)
[Dynamic Application Security Testing (DAST)](https://en.wikipedia.org/wiki/Dynamic_Application_Security_Testing)
-is using the popular open source tool [OWASP ZAProxy](https://github.com/zaproxy/zaproxy)
+uses the popular open source tool [OWASP ZAProxy](https://github.com/zaproxy/zaproxy)
to perform an analysis on your running web application.
-By default, DAST executes [ZAP Baseline Scan](https://github.com/zaproxy/zaproxy/wiki/ZAP-Baseline-Scan) and will perform passive scanning only. It will not actively attack your application.
-
+By default, DAST executes [ZAP Baseline Scan](https://github.com/zaproxy/zaproxy/wiki/ZAP-Baseline-Scan)
+and performs passive scanning only. It won't actively attack your application.
However, DAST can be [configured](#full-scan)
-to also perform a so-called "active scan". That is, attack your application and produce a more extensive security report.
+to also perform an *active scan*: attack your application and produce a more extensive security report.
It can be very useful combined with [Review Apps](../../../ci/review_apps/index.md).
+NOTE: **Note:**
+A pipeline may consist of multiple jobs, including SAST and DAST scanning. If any
+job fails to finish for any reason, the security dashboard won't show DAST scanner
+output. For example, if the DAST job finishes but the SAST job fails, the security
+dashboard won't show DAST results. The analyzer will output an
+[exit code](../../../development/integrations/secure.md#exit-code) on failure.
+
## Use cases
It helps you automatically find security vulnerabilities in your running web
-applications while you are developing and testing your applications.
+applications while you're developing and testing your applications.
## Requirements
@@ -66,9 +72,8 @@ To run a DAST job, you need GitLab Runner with the
For GitLab 11.9 and later, to enable DAST, you must
[include](../../../ci/yaml/README.md#includetemplate) the
[`DAST.gitlab-ci.yml` template](https://gitlab.com/gitlab-org/gitlab/blob/master/lib/gitlab/ci/templates/Security/DAST.gitlab-ci.yml)
-that's provided as a part of your GitLab installation.
-For GitLab versions earlier than 11.9, you can copy and use the job as defined
-in that template.
+that's provided as a part of your GitLab installation. For GitLab versions earlier
+than 11.9, you can copy and use the job as defined in that template.
Add the following to your `.gitlab-ci.yml` file:
@@ -85,32 +90,39 @@ There are two ways to define the URL to be scanned by DAST:
1. Set the `DAST_WEBSITE` [variable](../../../ci/yaml/README.md#variables).
1. Add it in an `environment_url.txt` file at the root of your project.
- This is great for testing in dynamic environments. In order to run DAST against
- an app that is dynamically created during a GitLab CI/CD pipeline, have the app
- persist its domain in an `environment_url.txt` file, and DAST will
- automatically parse that file to find its scan target.
- You can see an [example](https://gitlab.com/gitlab-org/gitlab/blob/master/lib/gitlab/ci/templates/Jobs/Deploy.gitlab-ci.yml)
- of this in our Auto DevOps CI YML.
+ This is great for testing in dynamic environments. In order to run DAST against
+ an app dynamically created during a GitLab CI/CD pipeline, have the app
+ persist its domain in an `environment_url.txt` file, and DAST
+ automatically parses that file to find its scan target.
+ You can see an [example](https://gitlab.com/gitlab-org/gitlab/blob/master/lib/gitlab/ci/templates/Jobs/Deploy.gitlab-ci.yml)
+ of this in our Auto DevOps CI YAML.
-If both values are set, the `DAST_WEBSITE` value will take precedence.
+If both values are set, the `DAST_WEBSITE` value takes precedence.
-The included template will create a `dast` job in your CI/CD pipeline and scan
+The included template creates a `dast` job in your CI/CD pipeline and scans
your project's source code for possible vulnerabilities.
-The results will be saved as a
-[DAST report artifact](../../../ci/yaml/README.md#artifactsreportsdast-ultimate)
+The results are saved as a
+[DAST report artifact](../../../ci/pipelines/job_artifacts.md#artifactsreportsdast-ultimate)
that you can later download and analyze. Due to implementation limitations we
always take the latest DAST artifact available. Behind the scenes, the
[GitLab DAST Docker image](https://gitlab.com/gitlab-org/security-products/dast)
is used to run the tests on the specified URL and scan it for possible vulnerabilities.
-By default, the DAST template will use the latest major version of the DAST Docker image. Using the `DAST_VERSION` variable,
-you can choose to automatically update DAST with new features and fixes by pinning to a major version (e.g. 1), only update fixes by pinning to a minor version (e.g. 1.6) or prevent all updates by pinning to a specific version (e.g. 1.6.4).
+By default, the DAST template will use the latest major version of the DAST Docker
+image. Using the `DAST_VERSION` variable, you can choose how DAST updates:
+
+- Automatically update DAST with new features and fixes by pinning to a major version (such as `1`).
+- Only update fixes by pinning to a minor version (such as `1.6`).
+- Prevent all updates by pinning to a specific version (such as `1.6.4`).
+
Find the latest DAST versions on the [Releases](https://gitlab.com/gitlab-org/security-products/dast/-/releases) page.
### When DAST scans run
-When using `DAST.gitlab-ci.yml` template, the `dast` job is run last as shown in the example below. To ensure DAST is scanning the latest code, your CI pipeline should deploy changes to the web server in one of the jobs preceeding the `dast` job.
+When using `DAST.gitlab-ci.yml` template, the `dast` job is run last as shown in
+the example below. To ensure DAST is scanning the latest code, your CI pipeline
+should deploy changes to the web server in one of the jobs preceding the `dast` job.
```yaml
stages:
@@ -120,12 +132,15 @@ stages:
- dast
```
-Be aware that if your pipeline is configured to deploy to the same webserver in each run, running a pipeline while another is still running, could cause a race condition
-where one pipeline overwrites the code from another pipeline. The site to be scanned should be excluded from changes for the duration of a DAST scan.
-The only changes to the site should be from the DAST scanner. Be aware that any changes that users, scheduled tasks, database or code changes, other pipelines, or other scanners make to
+Be aware that if your pipeline is configured to deploy to the same webserver in
+each run, running a pipeline while another is still running could cause a race condition
+where one pipeline overwrites the code from another pipeline. The site to be scanned
+should be excluded from changes for the duration of a DAST scan.
+The only changes to the site should be from the DAST scanner. Be aware that any
+changes that users, scheduled tasks, database changes, code changes, other pipelines, or other scanners make to
the site during a scan could lead to inaccurate results.
-### Authenticated scan
+### Authentication
It's also possible to authenticate the user before performing the DAST checks:
@@ -144,12 +159,15 @@ variables:
```
The results will be saved as a
-[DAST report artifact](../../../ci/yaml/README.md#artifactsreportsdast-ultimate)
+[DAST report artifact](../../../ci/pipelines/job_artifacts.md#artifactsreportsdast-ultimate)
that you can later download and analyze.
Due to implementation limitations, we always take the latest DAST artifact available.
DANGER: **Danger:**
-**DO NOT** run an authenticated scan against a production server. When an authenticated scan is run, it may perform *any* function that the authenticated user can. This includes modifying and deleting data, submitting forms, following links, and so on. Only run an authenticated scan against a test server.
+**NEVER** run an authenticated scan against a production server. When an authenticated
+scan is run, it may perform *any* function that the authenticated user can. This
+includes actions like modifying and deleting data, submitting forms, and following links.
+Only run an authenticated scan against a test server.
### Full scan
@@ -170,7 +188,8 @@ The DAST job can be run anywhere, which means you can accidentally hit live web
and potentially damage them. You could even take down your production environment.
For that reason, you should use domain validation.
-Domain validation is not required by default. It can be required by setting the [environment variable](#available-variables) `DAST_FULL_SCAN_DOMAIN_VALIDATION_REQUIRED` to true.
+Domain validation is not required by default. It can be required by setting the
+[environment variable](#available-variables) `DAST_FULL_SCAN_DOMAIN_VALIDATION_REQUIRED` to `"true"`.
```yaml
include:
@@ -181,19 +200,23 @@ variables:
DAST_FULL_SCAN_DOMAIN_VALIDATION_REQUIRED: "true"
```
-Since ZAP full scan actively attacks the target application, DAST sends a ping to the target (normally defined in `DAST_WEBSITE` or `environment_url.txt`) beforehand.
+Since ZAP full scan actively attacks the target application, DAST sends a ping
+to the target (normally defined in `DAST_WEBSITE` or `environment_url.txt`) beforehand.
-If `DAST_FULL_SCAN_DOMAIN_VALIDATION_REQUIRED` is false or unset, the scan will _proceed_ unless the response to the ping
-includes a `Gitlab-DAST-Permission` header with a value of `deny`.
+- If `DAST_FULL_SCAN_DOMAIN_VALIDATION_REQUIRED` is `false` or unset, the scan will
+ proceed unless the response to the ping includes a `Gitlab-DAST-Permission`
+ header with a value of `deny`.
+- If `DAST_FULL_SCAN_DOMAIN_VALIDATION_REQUIRED` is `true`, the scan will exit
+ unless the response to the ping includes a `Gitlab-DAST-Permission` header with
+ a value of `allow`.
-If `DAST_FULL_SCAN_DOMAIN_VALIDATION_REQUIRED` is true, the scan will _exit_ unless the response to the ping
-includes a `Gitlab-DAST-Permission` header with a value of `allow`.
-
-Here are some examples of adding the `Gitlab-DAST-Permission` header to a response in Rails, Django, and Node (with Express).
+Here are some examples of adding the `Gitlab-DAST-Permission` header to a response
+in Rails, Django, and Node (with Express).
##### Ruby on Rails
-Here's how you would add a [custom header in Ruby on Rails](https://guides.rubyonrails.org/action_controller_overview.html#setting-custom-headers):
+Here's how you would add a
+[custom header in Ruby on Rails](https://guides.rubyonrails.org/action_controller_overview.html#setting-custom-headers):
```ruby
class DastWebsiteTargetController < ActionController::Base
@@ -207,7 +230,8 @@ end
##### Django
-Here's how you would add a [custom header in Django](https://docs.djangoproject.com/en/2.2/ref/request-response/#setting-header-fields):
+Here's how you would add a
+[custom header in Django](https://docs.djangoproject.com/en/2.2/ref/request-response/#setting-header-fields):
```python
class DastWebsiteTargetView(View):
@@ -220,7 +244,8 @@ class DastWebsiteTargetView(View):
##### Node (with Express)
-Here's how you would add a [custom header in Node (with Express)](http://expressjs.com/en/5x/api.html#res.append):
+Here's how you would add a
+[custom header in Node (with Express)](http://expressjs.com/en/5x/api.html#res.append):
```javascript
app.get('/dast-website-target', function(req, res) {
@@ -235,7 +260,8 @@ It's also possible to add the `Gitlab-DAST-Permission` header via a proxy.
###### NGINX
-The following config allows NGINX to act as a reverse proxy and add the `Gitlab-DAST-Permission` [header](http://nginx.org/en/docs/http/ngx_http_headers_module.html#add_header):
+The following configuration allows NGINX to act as a reverse proxy and add the
+`Gitlab-DAST-Permission` [header](http://nginx.org/en/docs/http/ngx_http_headers_module.html#add_header):
```nginx
# default.conf
@@ -287,9 +313,9 @@ API scans support OpenAPI V2 and OpenAPI V3 specifications. You can define these
#### Import API specification from a URL
If your API specification is accessible at a URL, you can pass that URL in directly as the target.
-The specification doesn't have to be hosted on the same host as the API being tested.
+The specification does not have to be hosted on the same host as the API being tested.
-```yml
+```yaml
include:
- template: DAST.gitlab-ci.yml
@@ -299,9 +325,11 @@ variables:
#### Import API specification from a file
-If your API specification is in your repository, you can provide the specification's filename directly as the target. The specification file is expected to be in the `/zap/wrk` directory.
+If your API specification is in your repository, you can provide the specification's
+filename directly as the target. The specification file is expected to be in the
+`/zap/wrk` directory.
-```yml
+```yaml
dast:
script:
- mkdir -p /zap/wrk
@@ -314,23 +342,27 @@ dast:
#### Full scan
-API scans support full scanning, which can be enabled by using the `DAST_FULL_SCAN_ENABLED` environment variable. Domain validation isn't supported for full API scans.
+API scans support full scanning, which can be enabled by using the `DAST_FULL_SCAN_ENABLED`
+environment variable. Domain validation is not supported for full API scans.
#### Host override
-Specifications often define a host, which contains a domain name and a port. The host referenced may be different than the host of the API's review instance.
-This can cause incorrect URLs to be imported, or a scan on an incorrect host. Use the `DAST_API_HOST_OVERRIDE` environment variable to override these values.
+Specifications often define a host, which contains a domain name and a port. The
+host referenced may be different than the host of the API's review instance.
+This can cause incorrect URLs to be imported, or a scan on an incorrect host.
+Use the `DAST_API_HOST_OVERRIDE` environment variable to override these values.
For example, with a OpenAPI V3 specification containing:
-```yml
+```yaml
servers:
- url: https://api.host.com
```
-If the test version of the API is running at `https://api-test.host.com`, then the following DAST configuration can be used:
+If the test version of the API is running at `https://api-test.host.com`, then
+the following DAST configuration can be used:
-```yml
+```yaml
include:
- template: DAST.gitlab-ci.yml
@@ -343,9 +375,11 @@ Note that `DAST_API_HOST_OVERRIDE` is only applied to specifications imported by
#### Authentication using headers
-Tokens in request headers are often used as a way to authenticate API requests. You can achieve this by using the `DAST_REQUEST_HEADERS` environment variable. Headers are applied to every request DAST makes.
+Tokens in request headers are often used as a way to authenticate API requests.
+You can achieve this by using the `DAST_REQUEST_HEADERS` environment variable.
+Headers are applied to every request DAST makes.
-```yml
+```yaml
include:
- template: DAST.gitlab-ci.yml
@@ -404,7 +438,8 @@ don't forget to add `stage: dast` when you override the template job definition.
DAST can be [configured](#customizing-the-dast-settings) using environment variables.
| Environment variable | Required | Description |
-|-----------------------------| ----------|--------------------------------------------------------------------------------|
+|-----------------------------| -----------|--------------------------------------------------------------------------------|
+| `SECURE_ANALYZERS_PREFIX` | no | Set the Docker registry base address from which to download the analyzer. |
| `DAST_WEBSITE` | no| The URL of the website to scan. `DAST_API_SPECIFICATION` must be specified if this is omitted. |
| `DAST_API_SPECIFICATION` | no | The API specification to import. `DAST_WEBSITE` must be specified if this is omitted. |
| `DAST_AUTH_URL` | no | The authentication URL of the website to scan. Not supported for API scans. |
@@ -416,14 +451,16 @@ DAST can be [configured](#customizing-the-dast-settings) using environment varia
| `DAST_TARGET_AVAILABILITY_TIMEOUT` | no | Time limit in seconds to wait for target availability. Scan is attempted nevertheless if it runs out. Integer. Defaults to `60`. |
| `DAST_FULL_SCAN_ENABLED` | no | Switches the tool to execute [ZAP Full Scan](https://github.com/zaproxy/zaproxy/wiki/ZAP-Full-Scan) instead of [ZAP Baseline Scan](https://github.com/zaproxy/zaproxy/wiki/ZAP-Baseline-Scan). Boolean. `true`, `True`, or `1` are considered as true value, otherwise false. Defaults to `false`. |
| `DAST_FULL_SCAN_DOMAIN_VALIDATION_REQUIRED` | no | Requires [domain validation](#domain-validation) when running DAST full scans. Boolean. `true`, `True`, or `1` are considered as true value, otherwise false. Defaults to `false`. Not supported for API scans. |
-| `DAST_AUTO_UPDATE_ADDONS` | no | Set to `false` to pin the versions of ZAProxy add-ons to those provided with the DAST image. Defaults to `true`. |
+| `DAST_AUTO_UPDATE_ADDONS` | no | By default the versions of ZAP add-ons are pinned to those provided with the DAST image. Set to `true` to allow ZAP to download the latest versions. |
| `DAST_API_HOST_OVERRIDE` | no | Used to override domains defined in API specification files. |
| `DAST_EXCLUDE_RULES` | no | Set to a comma-separated list of Vulnerability Rule IDs to exclude them from scans. Rule IDs are numbers and can be found from the DAST log or on the [ZAP project](https://github.com/zaproxy/zaproxy/blob/master/docs/scanners.md). For example, `HTTP Parameter Override` has a rule ID of `10026`. |
| `DAST_REQUEST_HEADERS` | no | Set to a comma-separated list of request header names and values. For example, `Cache-control: no-cache,User-Agent: DAST/1.0` |
+| `DAST_ZAP_USE_AJAX_SPIDER` | no | Use the AJAX spider in addition to the traditional spider, useful for crawling sites that require JavaScript. Boolean. `true`, `True`, or `1` are considered as true value, otherwise false. Defaults to `false`. |
### DAST command-line options
-Not all DAST configuration is available via environment variables. To find out all possible options, run the following configuration.
+Not all DAST configuration is available via environment variables. To find out all
+possible options, run the following configuration.
Available command-line options will be printed to the job log:
```yaml
@@ -435,7 +472,8 @@ dast:
- /analyze --help
```
-You must then overwrite the `script` command to pass in the appropriate argument. For example, AJAX spidering can be enabled by using `-j`, as shown in the following configuration:
+You must then overwrite the `script` command to pass in the appropriate argument.
+For example, debug messages can be enabled by using `-d`, as shown in the following configuration:
```yaml
include:
@@ -444,14 +482,16 @@ include:
dast:
script:
- export DAST_WEBSITE=${DAST_WEBSITE:-$(cat environment_url.txt)}
- - /analyze -j -t $DAST_WEBSITE
+ - /analyze -d -t $DAST_WEBSITE
```
### Custom ZAProxy configuration
The ZAProxy server contains many [useful configurable values](https://gitlab.com/gitlab-org/gitlab/issues/36437#note_245801885).
-Many key/values for `-config` remain undocumented, but there is an untested list of [possible keys](https://gitlab.com/gitlab-org/gitlab/issues/36437#note_244981023).
-Note that these options are not supported by DAST, and may break the DAST scan when used. An example of how to rewrite the Authorization header value with `TOKEN` follows:
+Many key/values for `-config` remain undocumented, but there is an untested list of
+[possible keys](https://gitlab.com/gitlab-org/gitlab/issues/36437#note_244981023).
+Note that these options are not supported by DAST, and may break the DAST scan
+when used. An example of how to rewrite the Authorization header value with `TOKEN` follows:
```yaml
include:
@@ -479,50 +519,52 @@ successfully run. For more information, see [Offline environments](../offline_de
To use DAST in an offline environment, you need:
- GitLab Runner with the [`docker` or `kubernetes` executor](#requirements).
-- Docker Container Registry with a locally available copy of the DAST [container image](https://gitlab.com/gitlab-org/security-products/dast), found in the [DAST container registry](https://gitlab.com/gitlab-org/security-products/dast/container_registry).
+- Docker Container Registry with a locally available copy of the DAST
+ [container image](https://gitlab.com/gitlab-org/security-products/dast), found in the
+ [DAST container registry](https://gitlab.com/gitlab-org/security-products/dast/container_registry).
NOTE: **Note:**
GitLab Runner has a [default `pull policy` of `always`](https://docs.gitlab.com/runner/executors/docker.html#using-the-always-pull-policy),
-meaning the runner may try to pull remote images even if a local copy is available. Set GitLab
-Runner's [`pull_policy` to `if-not-present`](https://docs.gitlab.com/runner/executors/docker.html#using-the-if-not-present-pull-policy)
-in an offline environment if you prefer using only locally available Docker images.
+meaning the Runner tries to pull Docker images from the GitLab container registry even if a local
+copy is available. GitLab Runner's [`pull_policy` can be set to `if-not-present`](https://docs.gitlab.com/runner/executors/docker.html#using-the-if-not-present-pull-policy)
+in an offline environment if you prefer using only locally available Docker images. However, we
+recommend keeping the pull policy setting to `always` if not in an offline environment, as this
+enables the use of updated scanners in your CI/CD pipelines.
### Make GitLab DAST analyzer images available inside your Docker registry
-For DAST, import the following default DAST analyzer image from `registry.gitlab.com` to your local "offline"
-registry:
+For DAST, import the following default DAST analyzer image from `registry.gitlab.com` to your [local Docker container registry](../../packages/container_registry/index.md):
- `registry.gitlab.com/gitlab-org/security-products/dast:latest`
The process for importing Docker images into a local offline Docker registry depends on
**your network security policy**. Please consult your IT staff to find an accepted and approved
-process by which external resources can be imported or temporarily accessed. Note that these scanners are [updated periodically](../index.md#maintenance-and-update-of-the-vulnerabilities-database)
-with new definitions, so consider if you are able to make periodic updates yourself.
+process by which external resources can be imported or temporarily accessed. Note
+that these scanners are [updated periodically](../index.md#maintenance-and-update-of-the-vulnerabilities-database)
+with new definitions, so consider if you're able to make periodic updates yourself.
For details on saving and transporting Docker images as a file, see Docker's documentation on
-[`docker save`](https://docs.docker.com/engine/reference/commandline/save/), [`docker load`](https://docs.docker.com/engine/reference/commandline/load/),
-[`docker export`](https://docs.docker.com/engine/reference/commandline/export/), and [`docker import`](https://docs.docker.com/engine/reference/commandline/import/).
+[`docker save`](https://docs.docker.com/engine/reference/commandline/save/),
+[`docker load`](https://docs.docker.com/engine/reference/commandline/load/),
+[`docker export`](https://docs.docker.com/engine/reference/commandline/export/), and
+[`docker import`](https://docs.docker.com/engine/reference/commandline/import/).
### Set DAST CI job variables to use local DAST analyzers
-1. Add the following configuration to your `.gitlab-ci.yml` file. You must replace `image` to refer
- to the DAST Docker image hosted on your local Docker container registry:
+Add the following configuration to your `.gitlab-ci.yml` file. You must replace `image` to refer to
+the DAST Docker image hosted on your local Docker container registry:
- ```yaml
- include:
- - template: DAST.gitlab-ci.yml
-
- dast:
- image: registry.example.com/namespace/dast:latest
- script:
- - export DAST_WEBSITE=${DAST_WEBSITE:-$(cat environment_url.txt)}
- - /analyze -t $DAST_WEBSITE --auto-update-addons false -z"-silent"
- ```
+```yaml
+include:
+ - template: DAST.gitlab-ci.yml
+dast:
+ image: registry.example.com/namespace/dast:latest
+```
-The option `--auto-update-addons false` instructs ZAP not to update add-ons.
+The DAST job should now use local copies of the DAST analyzers to scan your code and generate
+security reports without requiring internet access.
-The option `-z` passes the quoted `-silent` parameter to ZAP. The `-silent` parameter ensures ZAP
-does not make any unsolicited requests including checking for updates.
+Alternatively, you can use the variable `SECURE_ANALYZERS_PREFIX` to override the base registry address of the `dast` image.
## Reports
@@ -530,7 +572,8 @@ The DAST job can emit various reports.
### List of URLs scanned
-When DAST completes scanning, the merge request page states the number of URLs that were scanned. Click **View details** to view the web console output which includes the list of scanned URLs.
+When DAST completes scanning, the merge request page states the number of URLs scanned.
+Click **View details** to view the web console output which includes the list of scanned URLs.
![DAST Widget](img/dast_urls_scanned_v12_10.png)
@@ -539,9 +582,14 @@ When DAST completes scanning, the merge request page states the number of URLs t
CAUTION: **Caution:**
The JSON report artifacts are not a public API of DAST and their format is expected to change in the future.
-The DAST tool always emits a JSON report file called `gl-dast-report.json` and sample reports can be found in the [DAST repository](https://gitlab.com/gitlab-org/security-products/dast/-/tree/master/test/end-to-end/expect).
+The DAST tool always emits a JSON report file called `gl-dast-report.json` and
+sample reports can be found in the
+[DAST repository](https://gitlab.com/gitlab-org/security-products/dast/-/tree/master/test/end-to-end/expect).
-There are two formats of data in the JSON report that are used side by side: the proprietary ZAP format which will be eventually deprecated, and a "common" format which will be the default in the future.
+There are two formats of data in the JSON report that are used side by side:
+
+- The proprietary ZAP format that will be eventually deprecated.
+- A common format that will be the default in the future.
### Other formats
@@ -574,7 +622,9 @@ vulnerabilities in your groups, projects and pipelines. Read more about the
## Bleeding-edge vulnerability definitions
-ZAProxy first creates rules in the `alpha` class. After a testing period with the community, they are promoted to `beta`. DAST uses `beta` definitions by default. To request `alpha` definitions, use `-a` as shown in the following configuration:
+ZAProxy first creates rules in the `alpha` class. After a testing period with the
+community, they are promoted to `beta`. DAST uses `beta` definitions by default.
+To request `alpha` definitions, use `-a` as shown in the following configuration:
```yaml
include:
@@ -608,6 +658,18 @@ Each scenario can be a third-level heading, e.g. `### Getting error message X`.
If you have none to add when creating a doc, leave this section in place
but commented out to help encourage others to add to it in the future. -->
+## Optimizing DAST
+
+By default, DAST will download all artifacts defined by previous jobs in the pipeline. If
+your DAST job does not rely on `environment_url.txt` to define the URL under test or any other files created
+in previous jobs, we recommend you don't download artifacts. To avoid downloading
+artifacts, add the following to your `gitlab-ci.yml` file:
+
+```json
+dast:
+ dependencies: []
+```
+
## Troubleshooting
### Running out of memory
@@ -615,14 +677,14 @@ but commented out to help encourage others to add to it in the future. -->
By default, ZAProxy, which DAST relies on, is allocated memory that sums to 25%
of the total memory on the host.
Since it keeps most of its information in memory during a scan,
-it is possible for DAST to run out of memory while scanning large applications.
+it's possible for DAST to run out of memory while scanning large applications.
This results in the following error:
```plaintext
[zap.out] java.lang.OutOfMemoryError: Java heap space
```
-Fortunately, it is straightforward to increase the amount of memory available
+Fortunately, it's straightforward to increase the amount of memory available
for DAST by overwriting the `script` key in the DAST template:
```yaml
diff --git a/doc/user/application_security/dependency_scanning/analyzers.md b/doc/user/application_security/dependency_scanning/analyzers.md
index 26352f21cfb..474f9339d0b 100644
--- a/doc/user/application_security/dependency_scanning/analyzers.md
+++ b/doc/user/application_security/dependency_scanning/analyzers.md
@@ -43,7 +43,7 @@ include:
template: Dependency-Scanning.gitlab-ci.yml
variables:
- DS_ANALYZER_IMAGE_PREFIX: my-docker-registry/gl-images
+ SECURE_ANALYZERS_PREFIX: my-docker-registry/gl-images
```
This configuration requires that your custom registry provides images for all
diff --git a/doc/user/application_security/dependency_scanning/img/dependency_scanning.png b/doc/user/application_security/dependency_scanning/img/dependency_scanning.png
deleted file mode 100644
index 18df356f846..00000000000
--- a/doc/user/application_security/dependency_scanning/img/dependency_scanning.png
+++ /dev/null
Binary files differ
diff --git a/doc/user/application_security/dependency_scanning/img/dependency_scanning_v13_0.png b/doc/user/application_security/dependency_scanning/img/dependency_scanning_v13_0.png
new file mode 100644
index 00000000000..9f3990df957
--- /dev/null
+++ b/doc/user/application_security/dependency_scanning/img/dependency_scanning_v13_0.png
Binary files differ
diff --git a/doc/user/application_security/dependency_scanning/index.md b/doc/user/application_security/dependency_scanning/index.md
index cda621e61a6..53462cf232e 100644
--- a/doc/user/application_security/dependency_scanning/index.md
+++ b/doc/user/application_security/dependency_scanning/index.md
@@ -7,25 +7,24 @@ type: reference, howto
> [Introduced](https://gitlab.com/gitlab-org/gitlab/issues/5105) in [GitLab Ultimate](https://about.gitlab.com/pricing/) 10.7.
Dependency Scanning helps to automatically find security vulnerabilities in your dependencies
-while you are developing and testing your applications, for example when your
+while you're developing and testing your applications, such as when your
application is using an external (open source) library which is known to be vulnerable.
## Overview
-If you are using [GitLab CI/CD](../../../ci/README.md), you can analyze your dependencies for known
+If you're using [GitLab CI/CD](../../../ci/README.md), you can analyze your dependencies for known
vulnerabilities using Dependency Scanning.
All dependencies are scanned, including the transitive dependencies (also known as nested dependencies).
-
You can take advantage of Dependency Scanning by either [including the Dependency Scanning template](#configuration)
in your existing `.gitlab-ci.yml` file or by implicitly using
-[Auto Dependency Scanning](../../../topics/autodevops/stages.md#auto-dependency-scanning-ultimate)
-that is provided by [Auto DevOps](../../../topics/autodevops/index.md).
+the [Auto Dependency Scanning](../../../topics/autodevops/stages.md#auto-dependency-scanning-ultimate)
+provided by [Auto DevOps](../../../topics/autodevops/index.md).
GitLab checks the Dependency Scanning report, compares the found vulnerabilities
between the source and target branches, and shows the information on the
merge request.
-![Dependency Scanning Widget](img/dependency_scanning.png)
+![Dependency Scanning Widget](img/dependency_scanning_v13_0.png)
The results are sorted by the severity of the vulnerability:
@@ -38,17 +37,16 @@ The results are sorted by the severity of the vulnerability:
## Requirements
-To run a Dependency Scanning job, by default, you need GitLab Runner with the
-[`docker`](https://docs.gitlab.com/runner/executors/docker.html#use-docker-in-docker-with-privileged-mode) or
-[`kubernetes`](https://docs.gitlab.com/runner/install/kubernetes.html#running-privileged-containers-for-the-runners)
-executor running in privileged mode. If you're using the shared Runners on GitLab.com,
-this is enabled by default.
+To run Dependency Scanning jobs, by default, you need GitLab Runner with the
+[`docker`](https://docs.gitlab.com/runner/executors/docker.html) or
+[`kubernetes`](https://docs.gitlab.com/runner/install/kubernetes.html) executor.
+If you're using the shared Runners on GitLab.com, this is enabled by default.
CAUTION: **Caution:**
-If you use your own Runners, make sure that the Docker version you have installed
+If you use your own Runners, make sure your installed version of Docker
is **not** `19.03.0`. See [troubleshooting information](#error-response-from-daemon-error-processing-tar-file-docker-tar-relocation-error) for details.
-Privileged mode is not necessary if you've [disabled Docker in Docker for Dependency Scanning](#disabling-docker-in-docker-for-dependency-scanning)
+Beginning with GitLab 13.0, Docker privileged mode is necessary only if you've [enabled Docker-in-Docker for Dependency Scanning](#enabling-docker-in-docker).
## Supported languages and package managers
@@ -56,16 +54,16 @@ The following languages and dependency managers are supported.
| Language (package managers) | Supported | Scan tool(s) |
|----------------------------- | --------- | ------------ |
-| Java ([Gradle](https://gradle.org/)) | yes | [gemnasium](https://gitlab.com/gitlab-org/security-products/gemnasium) |
-| Java ([Maven](https://maven.apache.org/)) | yes | [gemnasium](https://gitlab.com/gitlab-org/security-products/gemnasium) |
-| JavaScript ([npm](https://www.npmjs.com/), [yarn](https://classic.yarnpkg.com/en/)) | yes | [gemnasium](https://gitlab.com/gitlab-org/security-products/gemnasium), [Retire.js](https://retirejs.github.io/retire.js/) |
-| PHP ([Composer](https://getcomposer.org/)) | yes | [gemnasium](https://gitlab.com/gitlab-org/security-products/gemnasium) |
-| Python ([pip](https://pip.pypa.io/en/stable/)) | yes | [gemnasium](https://gitlab.com/gitlab-org/security-products/gemnasium) |
+| Java ([Gradle](https://gradle.org/)) | yes | [Gemnasium](https://gitlab.com/gitlab-org/security-products/gemnasium) |
+| Java ([Maven](https://maven.apache.org/)) | yes | [Gemnasium](https://gitlab.com/gitlab-org/security-products/gemnasium) |
+| JavaScript ([npm](https://www.npmjs.com/), [yarn](https://classic.yarnpkg.com/en/)) | yes | [Gemnasium](https://gitlab.com/gitlab-org/security-products/gemnasium), [Retire.js](https://retirejs.github.io/retire.js/) |
+| PHP ([Composer](https://getcomposer.org/)) | yes | [Gemnasium](https://gitlab.com/gitlab-org/security-products/gemnasium) |
+| Python ([pip](https://pip.pypa.io/en/stable/)) | yes | [Gemnasium](https://gitlab.com/gitlab-org/security-products/gemnasium) |
| Python ([Pipfile](https://pipenv.kennethreitz.org/en/latest/basics/)) | not currently ([issue](https://gitlab.com/gitlab-org/gitlab/issues/11756 "Pipfile.lock support for Dependency Scanning"))| not available |
| Python ([poetry](https://python-poetry.org/)) | not currently ([issue](https://gitlab.com/gitlab-org/gitlab/issues/7006 "Support Poetry in Dependency Scanning")) | not available |
-| Ruby ([gem](https://rubygems.org/)) | yes | [gemnasium](https://gitlab.com/gitlab-org/security-products/gemnasium), [bundler-audit](https://github.com/rubysec/bundler-audit) |
-| Scala ([sbt](https://www.scala-sbt.org/)) | yes | [gemnasium](https://gitlab.com/gitlab-org/security-products/gemnasium) |
-| Go ([Go Modules](https://github.com/golang/go/wiki/Modules)) | yes ([alpha](https://gitlab.com/gitlab-org/gitlab/issues/7132)) | [gemnasium](https://gitlab.com/gitlab-org/security-products/gemnasium) |
+| Ruby ([gem](https://rubygems.org/)) | yes | [Gemnasium](https://gitlab.com/gitlab-org/security-products/gemnasium), [bundler-audit](https://github.com/rubysec/bundler-audit) |
+| Scala ([sbt](https://www.scala-sbt.org/)) | yes | [Gemnasium](https://gitlab.com/gitlab-org/security-products/gemnasium) |
+| Go ([Go Modules](https://github.com/golang/go/wiki/Modules)) | yes ([alpha](https://gitlab.com/gitlab-org/gitlab/issues/7132)) | [Gemnasium](https://gitlab.com/gitlab-org/security-products/gemnasium) |
## Contribute your scanner
@@ -73,7 +71,7 @@ The [Security Scanner Integration](../../../development/integrations/secure.md)
## Configuration
-For GitLab 11.9 and later, to enable Dependency Scanning, you must
+To enable Dependency Scanning for GitLab 11.9 and later, you must
[include](../../../ci/yaml/README.md#includetemplate) the
[`Dependency-Scanning.gitlab-ci.yml` template](https://gitlab.com/gitlab-org/gitlab/blob/master/lib/gitlab/ci/templates/Security/Dependency-Scanning.gitlab-ci.yml)
that's provided as a part of your GitLab installation.
@@ -87,11 +85,10 @@ include:
- template: Dependency-Scanning.gitlab-ci.yml
```
-The included template will create a `dependency_scanning` job in your CI/CD
+The included template will create Dependency Scanning jobs in your CI/CD
pipeline and scan your project's source code for possible vulnerabilities.
-
The results will be saved as a
-[Dependency Scanning report artifact](../../../ci/yaml/README.md#artifactsreportsdependency_scanning-ultimate)
+[Dependency Scanning report artifact](../../../ci/pipelines/job_artifacts.md#artifactsreportsdependency_scanning-ultimate)
that you can later download and analyze. Due to implementation limitations, we
always take the latest Dependency Scanning artifact available.
@@ -99,7 +96,6 @@ always take the latest Dependency Scanning artifact available.
The Dependency Scanning settings can be changed through [environment variables](#available-variables) by using the
[`variables`](../../../ci/yaml/README.md#variables) parameter in `.gitlab-ci.yml`.
-
For example:
```yaml
@@ -113,23 +109,24 @@ variables:
Because template is [evaluated before](../../../ci/yaml/README.md#include) the pipeline
configuration, the last mention of the variable will take precedence.
-### Overriding the Dependency Scanning template
+### Overriding Dependency Scanning jobs
CAUTION: **Deprecation:**
Beginning in GitLab 13.0, the use of [`only` and `except`](../../../ci/yaml/README.md#onlyexcept-basic)
is no longer supported. When overriding the template, you must use [`rules`](../../../ci/yaml/README.md#rules) instead.
-If you want to override the job definition (for example, change properties like
-`variables` or `dependencies`), you need to declare a `dependency_scanning` job
-after the template inclusion and specify any additional keys under it. For example:
+To override a job definition (for example, to change properties like `variables` or `dependencies`),
+declare a new job with the same name as the one to override. Place this new job after the template
+inclusion and specify any additional keys under it. For example, this disables `DS_REMEDIATE` for
+the `gemnasium` analyzer:
```yaml
include:
- template: Dependency-Scanning.gitlab-ci.yml
-dependency_scanning:
+gemnasium-dependency_scanning:
variables:
- CI_DEBUG_TRACE: "true"
+ DS_REMEDIATE: "false"
```
### Available variables
@@ -143,19 +140,20 @@ The following variables allow configuration of global dependency scanning settin
| Environment variable | Description |
| --------------------------------------- |------------ |
-| `DS_ANALYZER_IMAGE_PREFIX` | Override the name of the Docker registry providing the official default images (proxy). Read more about [customizing analyzers](analyzers.md). |
+| `SECURE_ANALYZERS_PREFIX` | Override the name of the Docker registry providing the official default images (proxy). Read more about [customizing analyzers](analyzers.md). |
+| `DS_ANALYZER_IMAGE_PREFIX` | **DEPRECATED:** Use `SECURE_ANALYZERS_PREFIX` instead. |
| `DS_DEFAULT_ANALYZERS` | Override the names of the official default images. Read more about [customizing analyzers](analyzers.md). |
-| `DS_DISABLE_DIND` | Disable Docker-in-Docker and run analyzers [individually](#disabling-docker-in-docker-for-dependency-scanning).|
+| `DS_DISABLE_DIND` | Disable Docker-in-Docker and run analyzers [individually](#enabling-docker-in-docker). This variable is `true` by default. |
| `ADDITIONAL_CA_CERT_BUNDLE` | Bundle of CA certs to trust. |
| `DS_EXCLUDED_PATHS` | Exclude vulnerabilities from output based on the paths. A comma-separated list of patterns. Patterns can be globs, or file or folder paths (for example, `doc,spec`). Parent directories also match patterns. |
#### Configuring Docker-in-Docker orchestrator
-The following variables configure the Docker-in-Docker orchestrator.
+The following variables configure the Docker-in-Docker orchestrator, and therefore are only used when the Docker-in-Docker mode is [enabled](#enabling-docker-in-docker).
| Environment variable | Default | Description |
| --------------------------------------- | ----------- | ----------- |
-| `DS_ANALYZER_IMAGES` | | Comma separated list of custom images. The official default images are still enabled. Read more about [customizing analyzers](analyzers.md). |
+| `DS_ANALYZER_IMAGES` | | Comma-separated list of custom images. The official default images are still enabled. Read more about [customizing analyzers](analyzers.md). |
| `DS_ANALYZER_IMAGE_TAG` | | Override the Docker tag of the official default images. Read more about [customizing analyzers](analyzers.md). |
| `DS_PULL_ANALYZER_IMAGES` | | Pull the images from the Docker registry (set to `0` to disable). |
| `DS_DOCKER_CLIENT_NEGOTIATION_TIMEOUT` | 2m | Time limit for Docker client negotiation. Timeouts are parsed using Go's [`ParseDuration`](https://golang.org/pkg/time/#ParseDuration). Valid time units are `ns`, `us` (or `µs`), `ms`, `s`, `m`, or `h`. For example, `300ms`, `1.5h`, or `2h45m`. |
@@ -168,20 +166,20 @@ The following variables are used for configuring specific analyzers (used for a
| Environment variable | Analyzer | Default | Description |
| --------------------------------------- | ------------------ | ---------------------------- |------------ |
-| `GEMNASIUM_DB_LOCAL_PATH` | `gemnasium` | `/gemnasium-db` | Path to local gemnasium database. |
-| `GEMNASIUM_DB_REMOTE_URL` | `gemnasium` | `https://gitlab.com/gitlab-org/security-products/gemnasium-db.git` | Repository URL for fetching the gemnasium database. |
+| `GEMNASIUM_DB_LOCAL_PATH` | `gemnasium` | `/gemnasium-db` | Path to local Gemnasium database. |
+| `GEMNASIUM_DB_REMOTE_URL` | `gemnasium` | `https://gitlab.com/gitlab-org/security-products/gemnasium-db.git` | Repository URL for fetching the Gemnasium database. |
| `GEMNASIUM_DB_REF_NAME` | `gemnasium` | `master` | Branch name for remote repository database. `GEMNASIUM_DB_REMOTE_URL` is required. |
-| `DS_REMEDIATE` | `gemnasium` | `"true"` | Enable automatic remediation of vulnerable dependencies. |
+| `DS_REMEDIATE` | `gemnasium` | `"true"` | Enable automatic remediation of vulnerable dependencies. |
| `PIP_INDEX_URL` | `gemnasium-python` | `https://pypi.org/simple` | Base URL of Python Package Index. |
-| `PIP_EXTRA_INDEX_URL` | `gemnasium-python` | | Array of [extra URLs](https://pip.pypa.io/en/stable/reference/pip_install/#cmdoption-extra-index-url) of package indexes to use in addition to `PIP_INDEX_URL`. Comma separated. |
+| `PIP_EXTRA_INDEX_URL` | `gemnasium-python` | | Array of [extra URLs](https://pip.pypa.io/en/stable/reference/pip_install/#cmdoption-extra-index-url) of package indexes to use in addition to `PIP_INDEX_URL`. Comma-separated. |
| `PIP_REQUIREMENTS_FILE` | `gemnasium-python` | | Pip requirements file to be scanned. |
| `DS_PIP_VERSION` | `gemnasium-python` | | Force the install of a specific pip version (example: `"19.3"`), otherwise the pip installed in the Docker image is used. ([Introduced](https://gitlab.com/gitlab-org/gitlab/issues/12811) in GitLab 12.7) |
| `DS_PIP_DEPENDENCY_PATH` | `gemnasium-python` | | Path to load Python pip dependencies from. ([Introduced](https://gitlab.com/gitlab-org/gitlab/issues/12412) in GitLab 12.2) |
| `DS_PYTHON_VERSION` | `retire.js` | | Version of Python. If set to 2, dependencies are installed using Python 2.7 instead of Python 3.6. ([Introduced](https://gitlab.com/gitlab-org/gitlab/issues/12296) in GitLab 12.1)|
-| `MAVEN_CLI_OPTS` | `gemnasium-maven` | `"-DskipTests --batch-mode"` | List of command line arguments that will be passed to `maven` by the analyzer. See an example for [using private repos](../index.md#using-private-maven-repos). |
-| `GRADLE_CLI_OPTS` | `gemnasium-maven` | | List of command line arguments that will be passed to `gradle` by the analyzer. |
-| `SBT_CLI_OPTS` | `gemnasium-maven` | | List of command-line arguments that the analyzer will pass to `sbt`. |
-| `BUNDLER_AUDIT_UPDATE_DISABLED` | `bundler-audit` | `"false"` | Disable automatic updates for the `bundler-audit` analyzer. Useful if you're running Dependency Scanning in an offline, air-gapped environment.|
+| `MAVEN_CLI_OPTS` | `gemnasium-maven` | `"-DskipTests --batch-mode"` | List of command line arguments that will be passed to `maven` by the analyzer. See an example for [using private repositories](../index.md#using-private-maven-repos). |
+| `GRADLE_CLI_OPTS` | `gemnasium-maven` | | List of command line arguments that will be passed to `gradle` by the analyzer. |
+| `SBT_CLI_OPTS` | `gemnasium-maven` | | List of command-line arguments that the analyzer will pass to `sbt`. |
+| `BUNDLER_AUDIT_UPDATE_DISABLED` | `bundler-audit` | `"false"` | Disable automatic updates for the `bundler-audit` analyzer. Useful if you're running Dependency Scanning in an offline, air-gapped environment.|
| `BUNDLER_AUDIT_ADVISORY_DB_URL` | `bundler-audit` | `https://github.com/rubysec/ruby-advisory-db` | URL of the advisory database used by bundler-audit. |
| `BUNDLER_AUDIT_ADVISORY_DB_REF_NAME` | `bundler-audit` | `master` | Git ref for the advisory database specified by `BUNDLER_AUDIT_ADVISORY_DB_URL`. |
| `RETIREJS_JS_ADVISORY_DB` | `retire.js` | `https://raw.githubusercontent.com/RetireJS/retire.js/master/repository/jsrepository.json` | Path or URL to `retire.js` JS vulnerability data file. Note that if the URL hosting the data file uses a custom SSL certificate, for example in an offline installation, you can pass the certificate in the `ADDITIONAL_CA_CERT_BUNDLE` environment variable. |
@@ -190,39 +188,31 @@ The following variables are used for configuring specific analyzers (used for a
### Using private Maven repos
-If you have a private Maven repository which requires login credentials,
+If your private Maven repository requires login credentials,
you can use the `MAVEN_CLI_OPTS` environment variable.
-Read more on [how to use private Maven repos](../index.md#using-private-maven-repos).
+Read more on [how to use private Maven repositories](../index.md#using-private-maven-repos).
-### Disabling Docker in Docker for Dependency Scanning
+### Enabling Docker-in-Docker
> [Introduced](https://gitlab.com/gitlab-org/gitlab/issues/12487) in GitLab Ultimate 12.5.
-You can avoid the need for Docker in Docker by running the individual analyzers.
-This does not require running the executor in privileged mode. For example:
+If needed, you can enable Docker-in-Docker to restore the Dependency Scanning behavior that existed
+prior to GitLab 13.0. Follow these steps to do so:
-```yaml
-include:
- - template: Dependency-Scanning.gitlab-ci.yml
+1. Configure GitLab Runner with Docker-in-Docker in [privileged mode](https://docs.gitlab.com/runner/executors/docker.html#use-docker-in-docker-with-privileged-mode).
+1. Set the `DS_DISABLE_DIND` variable to `false`:
-variables:
- DS_DISABLE_DIND: "true"
-```
+ ```yaml
+ include:
+ - template: Dependency-Scanning.gitlab-ci.yml
-This will create individual `<analyzer-name>-dependency_scanning` jobs for each analyzer that runs in your CI/CD pipeline.
+ variables:
+ DS_DISABLE_DIND: "false"
+ ```
-By removing Docker-in-Docker (DIND), GitLab relies on [Linguist](https://github.com/github/linguist)
-to start relevant analyzers depending on the detected repository language(s) instead of the
-[orchestrator](https://gitlab.com/gitlab-org/security-products/dependency-scanning/). However, there
-are some differences in the way repository languages are detected between DIND and non-DIND. You can
-observe these differences by checking both Linguist and the common library. For instance, Linguist
-looks for `*.java` files to spin up the [gemnasium-maven](https://gitlab.com/gitlab-org/security-products/analyzers/gemnasium-maven)
-image, while orchestrator only looks for the existence of `pom.xml` or `build.gradle`. GitLab uses
-Linguist to detect new file types in the default branch. This means that when introducing files or
-dependencies for a new language or package manager, the corresponding scans won't be triggered in
-the MR and will only run on the default branch once the MR is merged. This will be addressed by
-[#211702](https://gitlab.com/gitlab-org/gitlab/-/issues/211702).
+This creates a single `dependency_scanning` job in your CI/CD pipeline instead of multiple
+`<analyzer-name>-dependency_scanning` jobs.
## Interacting with the vulnerabilities
@@ -232,9 +222,8 @@ Once a vulnerability is found, you can interact with it. Read more on how to
## Solutions for vulnerabilities (auto-remediation)
Some vulnerabilities can be fixed by applying the solution that GitLab
-automatically generates.
-
-Read more about the [solutions for vulnerabilities](../index.md#solutions-for-vulnerabilities-auto-remediation).
+automatically generates. Read more about the
+[solutions for vulnerabilities](../index.md#solutions-for-vulnerabilities-auto-remediation).
## Security Dashboard
@@ -371,33 +360,33 @@ it highlighted:
CAUTION: **Deprecation:**
Beginning with GitLab 12.9, dependency scanning no longer reports `undefined` severity and confidence levels.
-Here is the description of the report file structure nodes and their meaning. All fields are mandatory to be present in
-the report JSON unless stated otherwise. Presence of optional fields depends on the underlying analyzers being used.
+This table describes the report file structure nodes and their meaning. All fields are mandatory to be present in
+the report JSON, unless stated otherwise. The presence of optional fields depends on the underlying analyzers used.
| Report JSON node | Description |
|------------------------------------------------------|-------------|
| `version` | Report syntax version used to generate this JSON. |
| `vulnerabilities` | Array of vulnerability objects. |
| `vulnerabilities[].id` | Unique identifier of the vulnerability. |
-| `vulnerabilities[].category` | Where this vulnerability belongs (SAST, Dependency Scanning etc.). For Dependency Scanning, it will always be `dependency_scanning`. |
-| `vulnerabilities[].name` | Name of the vulnerability, this must not include the occurrence's specific information. Optional. |
-| `vulnerabilities[].message` | A short text that describes the vulnerability, it may include occurrence's specific information. Optional. |
+| `vulnerabilities[].category` | Where this vulnerability belongs, such as SAST or Dependency Scanning. For Dependency Scanning, it will always be `dependency_scanning`. |
+| `vulnerabilities[].name` | Name of the vulnerability. Must not include the occurrence's specific information. Optional. |
+| `vulnerabilities[].message` | A short text that describes the vulnerability. May include occurrence's specific information. Optional. |
| `vulnerabilities[].description` | A long text that describes the vulnerability. Optional. |
-| `vulnerabilities[].cve` | (**DEPRECATED - use `vulnerabilities[].id` instead**) A fingerprint string value that represents a concrete occurrence of the vulnerability. It's used to determine whether two vulnerability occurrences are same or different. May not be 100% accurate. **This is NOT a [CVE](https://cve.mitre.org/)**. |
-| `vulnerabilities[].severity` | How much the vulnerability impacts the software. Possible values: `Undefined` (an analyzer has not provided this information), `Info`, `Unknown`, `Low`, `Medium`, `High`, `Critical`. |
-| `vulnerabilities[].confidence` | How reliable the vulnerability's assessment is. Possible values: `Undefined` (an analyzer has not provided this information), `Ignore`, `Unknown`, `Experimental`, `Low`, `Medium`, `High`, `Confirmed`. |
+| `vulnerabilities[].cve` | (**DEPRECATED - use `vulnerabilities[].id` instead**) A fingerprint string value that represents a concrete occurrence of the vulnerability. Used to determine whether two vulnerability occurrences are same or different. May not be 100% accurate. **This is NOT a [CVE](https://cve.mitre.org/)**. |
+| `vulnerabilities[].severity` | How much the vulnerability impacts the software. Possible values: `Info`, `Unknown`, `Low`, `Medium`, `High`, `Critical`. |
+| `vulnerabilities[].confidence` | How reliable the vulnerability's assessment is. Possible values: `Ignore`, `Unknown`, `Experimental`, `Low`, `Medium`, `High`, `Confirmed`. |
| `vulnerabilities[].solution` | Explanation of how to fix the vulnerability. Optional. |
| `vulnerabilities[].scanner` | A node that describes the analyzer used to find this vulnerability. |
-| `vulnerabilities[].scanner.id` | Id of the scanner as a snake_case string. |
+| `vulnerabilities[].scanner.id` | ID of the scanner as a `snake_case` string. |
| `vulnerabilities[].scanner.name` | Name of the scanner, for display purposes. |
| `vulnerabilities[].location` | A node that tells where the vulnerability is located. |
-| `vulnerabilities[].location.file` | Path to the dependencies file (e.g., `yarn.lock`). Optional. |
+| `vulnerabilities[].location.file` | Path to the dependencies file (such as `yarn.lock`). Optional. |
| `vulnerabilities[].location.dependency` | A node that describes the dependency of a project where the vulnerability is located. |
| `vulnerabilities[].location.dependency.package` | A node that provides the information on the package where the vulnerability is located. |
| `vulnerabilities[].location.dependency.package.name` | Name of the package where the vulnerability is located. Optional. |
| `vulnerabilities[].location.dependency.version` | Version of the vulnerable package. Optional. |
| `vulnerabilities[].identifiers` | An ordered array of references that identify a vulnerability on internal or external DBs. |
-| `vulnerabilities[].identifiers[].type` | Type of the identifier. Possible values: common identifier types (among `cve`, `cwe`, `osvdb`, and `usn`) or analyzer-dependent ones (e.g. `gemnasium` for [Gemnasium](https://gitlab.com/gitlab-org/security-products/analyzers/gemnasium/)). |
+| `vulnerabilities[].identifiers[].type` | Type of the identifier. Possible values: common identifier types (among `cve`, `cwe`, `osvdb`, and `usn`) or analyzer-dependent ones (such as `gemnasium` for [Gemnasium](https://gitlab.com/gitlab-org/security-products/analyzers/gemnasium/)). |
| `vulnerabilities[].identifiers[].name` | Name of the identifier for display purpose. |
| `vulnerabilities[].identifiers[].value` | Value of the identifier for matching purpose. |
| `vulnerabilities[].identifiers[].url` | URL to identifier's documentation. Optional. |
@@ -406,10 +395,10 @@ the report JSON unless stated otherwise. Presence of optional fields depends on
| `vulnerabilities[].links[].url` | URL of the vulnerability details document. Optional. |
| `remediations` | An array of objects containing information on cured vulnerabilities along with patch diffs to apply. Empty if no remediations provided by an underlying analyzer. |
| `remediations[].fixes` | An array of strings that represent references to vulnerabilities fixed by this particular remediation. |
-| `remediations[].fixes[].id` | The id of a fixed vulnerability. |
+| `remediations[].fixes[].id` | The ID of a fixed vulnerability. |
| `remediations[].fixes[].cve` | (**DEPRECATED - use `remediations[].fixes[].id` instead**) A string value that describes a fixed vulnerability in the same format as `vulnerabilities[].cve`. |
| `remediations[].summary` | Overview of how the vulnerabilities have been fixed. |
-| `remediations[].diff` | base64-encoded remediation code diff, compatible with [`git apply`](https://git-scm.com/docs/git-format-patch#_discussion). |
+| `remediations[].diff` | Base64-encoded remediation code diff, compatible with [`git apply`](https://git-scm.com/docs/git-format-patch#_discussion). |
## Versioning and release process
@@ -424,32 +413,33 @@ You can also [submit new vulnerabilities](https://gitlab.com/gitlab-org/security
## Running Dependency Scanning in an offline environment
For self-managed GitLab instances in an environment with limited, restricted, or intermittent access
-to external resources through the internet, some adjustments are required for dependency scannings jobs to run successfully. For more information, see [Offline environments](../offline_deployments/index.md).
+to external resources through the internet, some adjustments are required for Dependency Scanning
+jobs to run successfully. For more information, see [Offline environments](../offline_deployments/index.md).
### Requirements for offline Dependency Scanning
Here are the requirements for using Dependency Scanning in an offline environment:
-- [Disable Docker-In-Docker](#disabling-docker-in-docker-for-dependency-scanning)
+- Keep Docker-In-Docker disabled (default).
- GitLab Runner with the [`docker` or `kubernetes` executor](#requirements).
-- Docker Container Registry with locally available copies of dependency scanning [analyzer](https://gitlab.com/gitlab-org/security-products/analyzers) images.
+- Docker Container Registry with locally available copies of Dependency Scanning [analyzer](https://gitlab.com/gitlab-org/security-products/analyzers) images.
- Host an offline Git copy of the [gemnasium-db advisory database](https://gitlab.com/gitlab-org/security-products/gemnasium-db/)
- _Only if scanning Ruby projects_: Host an offline Git copy of the [advisory database](https://github.com/rubysec/ruby-advisory-db).
- _Only if scanning npm/yarn projects_: Host an offline copy of the [retire.js](https://github.com/RetireJS/retire.js/) [node](https://github.com/RetireJS/retire.js/blob/master/repository/npmrepository.json) and [js](https://github.com/RetireJS/retire.js/blob/master/repository/jsrepository.json) advisory databases.
NOTE: **Note:**
GitLab Runner has a [default `pull policy` of `always`](https://docs.gitlab.com/runner/executors/docker.html#using-the-always-pull-policy),
-meaning the runner will try to pull Docker images from the GitLab container registry even if a local
+meaning the Runner tries to pull Docker images from the GitLab container registry even if a local
copy is available. GitLab Runner's [`pull_policy` can be set to `if-not-present`](https://docs.gitlab.com/runner/executors/docker.html#using-the-if-not-present-pull-policy)
in an offline environment if you prefer using only locally available Docker images. However, we
-recommend keeping the pull policy setting to `always` as it will better enable updated scanners to
-be utilized within your CI/CD pipelines.
+recommend keeping the pull policy setting to `always` if not in an offline environment, as this
+enables the use of updated scanners in your CI/CD pipelines.
### Make GitLab Dependency Scanning analyzer images available inside your Docker registry
-For Dependency Scanning, import docker images ([supported languages and frameworks](#supported-languages-and-package-managers))
-from `registry.gitlab.com` to your offline docker registry. The Dependency Scanning analyzer
-docker images are:
+For Dependency Scanning with all [supported languages and frameworks](#supported-languages-and-package-managers),
+import the following default Dependency Scanning analyzer images from `registry.gitlab.com` into
+your [local Docker container registry](../../packages/container_registry/index.md):
```plaintext
registry.gitlab.com/gitlab-org/security-products/analyzers/gemnasium:2
@@ -461,39 +451,34 @@ registry.gitlab.com/gitlab-org/security-products/analyzers/bundler-audit:2
The process for importing Docker images into a local offline Docker registry depends on
**your network security policy**. Please consult your IT staff to find an accepted and approved
-process by which external resources can be imported or temporarily accessed. Note that these scanners are [updated periodically](../index.md#maintenance-and-update-of-the-vulnerabilities-database)
-with new definitions, so consider if you are able to make periodic updates yourself.
+process by which external resources can be imported or temporarily accessed.
+Note that these scanners are [updated periodically](../index.md#maintenance-and-update-of-the-vulnerabilities-database)
+with new definitions, so consider if you can make periodic updates yourself.
For details on saving and transporting Docker images as a file, see Docker's documentation on
[`docker save`](https://docs.docker.com/engine/reference/commandline/save/), [`docker load`](https://docs.docker.com/engine/reference/commandline/load/),
[`docker export`](https://docs.docker.com/engine/reference/commandline/export/), and [`docker import`](https://docs.docker.com/engine/reference/commandline/import/).
-### Set Dependency Scanning CI config for "offline" use
+### Set Dependency Scanning CI job variables to use local Dependency Scanning analyzers
-Below is a general `.gitlab-ci.yml` template to configure your environment for running Dependency
-Scanning offline:
+Add the following configuration to your `.gitlab-ci.yml` file. You must replace
+`DS_ANALYZER_IMAGE_PREFIX` to refer to your local Docker container registry:
```yaml
include:
- template: Dependency-Scanning.gitlab-ci.yml
variables:
- DS_DISABLE_DIND: "true"
DS_ANALYZER_IMAGE_PREFIX: "docker-registry.example.com/analyzers"
+ GEMNASIUM_DB_REMOTE_URL: "gitlab.example.com/gemnasium-db.git"
+ GIT_SSL_NO_VERIFY: "true"
```
See explanations of the variables above in the [configuration section](#configuration).
### Specific settings for languages and package managers
-For every language and package manager, add the following to the variables section of
-`.gitlab-ci.yml`:
-
-```yaml
-GEMNASIUM_DB_REMOTE_URL: "gitlab.example.com/gemnasium-db.git"
-```
-
-See the following sections for additional instructions on specific languages and package managers.
+See the following sections for configuring specific languages and package managers.
#### JavaScript (npm and yarn) projects
@@ -515,10 +500,12 @@ BUNDLER_AUDIT_ADVISORY_DB_URL: "gitlab.example.com/ruby-advisory-db.git"
#### Java (Maven) projects
-When using a self-signed certificates, add the following to the variables section of`.gitlab-ci.yml`:
+When using self-signed certificates, add the following job section to the `.gitlab-ci.yml`:
```yaml
-MAVEN_CLI_OPTS="-Dmaven.wagon.http.ssl.insecure=true -Dmaven.wagon.http.ssl.allowall=true -Dmaven.wagon.http.ssl.ignore.validity.dates=true"`
+gemnasium-maven-dependency_scanning:
+ variables:
+ MAVEN_CLI_OPTS: "-s settings.xml -Dmaven.wagon.http.ssl.insecure=true -Dmaven.wagon.http.ssl.allowall=true -Dmaven.wagon.http.ssl.ignore.validity.dates=true"
```
#### Java (Gradle) projects
@@ -527,13 +514,12 @@ When using self-signed certificates, add the following job section to the `.gitl
```yaml
gemnasium-maven-dependency_scanning:
- variables:
- before_script:
- - echo -n | openssl s_client -connect maven-repo.example.com:443 | sed -ne '/-BEGIN CERTIFICATE-/,/-END CERTIFICATE-/p' > /tmp/internal.crt
- - keytool -importcert -file /tmp/internal.crt -cacerts -storepass changeit -noprompt
+ before_script:
+ - echo -n | openssl s_client -connect maven-repo.example.com:443 | sed -ne '/-BEGIN CERTIFICATE-/,/-END CERTIFICATE-/p' > /tmp/internal.crt
+ - keytool -importcert -file /tmp/internal.crt -cacerts -storepass changeit -noprompt
```
-This adds the self-signed certificates of your maven repository to the Java Key Store of the analyzer's docker image.
+This adds the self-signed certificates of your Maven repository to the Java KeyStore of the analyzer's Docker image.
#### Scala (sbt) projects
@@ -541,41 +527,20 @@ When using self-signed certificates, add the following job section to the `.gitl
```yaml
gemnasium-maven-dependency_scanning:
- variables:
- before_script:
- - echo -n | openssl s_client -connect gitlab-airgap-test.us-west1-b.c.group-secure-a89fe7.internal:443 | sed -ne '/-BEGIN CERTIFICATE-/,/-END CERTIFICATE-/p' > /tmp/internal.crt
- - keytool -importcert -file /tmp/internal.crt -cacerts -storepass changeit -noprompt
-```
-
-This adds the self-signed certificates of your maven repository to the Java Key Store of the analyzer's docker image.
-
-#### Python (pip) and Python (Pipfile) projects
-
-Add the following `pip.conf` to your repository to define your index URL and trust its self-signed
-certificate:
-
-```toml
-[global]
-index-url = https://pypi.example.com
-trusted-host = pypi.example.com
-```
-
-Add the following job section to `.gitlab-ci.yml`:
-
-```yaml
-gemnasium-python-dependency_scanning:
before_script:
- - mkdir ~/.config/pip
- - cp pip.conf ~/.config/pip/pip.conf
+ - echo -n | openssl s_client -connect maven-repo.example.com:443 | sed -ne '/-BEGIN CERTIFICATE-/,/-END CERTIFICATE-/p' > /tmp/internal.crt
+ - keytool -importcert -file /tmp/internal.crt -cacerts -storepass changeit -noprompt
```
+This adds the self-signed certificates of your Maven repository to the Java KeyStore of the analyzer's Docker image.
+
#### Python (setuptools)
-When using self-signed certificates for your private PyPi repo no extra job configuration (aside
+When using self-signed certificates for your private PyPi repository, no extra job configuration (aside
from the template `.gitlab-ci.yml` above) is needed. However, you must update your `setup.py` to
-ensure that it can reach your private repo. Here is an example configuration:
+ensure that it can reach your private repository. Here is an example configuration:
-1. Update `setup.py` to create a `dependency_links` attribute pointing at your private repo for each
+1. Update `setup.py` to create a `dependency_links` attribute pointing at your private repository for each
dependency in the `install_requires` list:
```python
@@ -596,11 +561,27 @@ ensure that it can reach your private repo. Here is an example configuration:
setuptools.ssl_support.cert_paths = ['internal.crt']
```
+## Limitations
+
+### Referencing local dependencies using a path in JavaScript projects
+
+The [Retire.js](https://gitlab.com/gitlab-org/security-products/analyzers/retire.js) analyzer
+doesn't support dependency references made with [local paths](https://docs.npmjs.com/files/package.json#local-paths)
+in the `package.json` of JavaScript projects. The dependency scan outputs the following error for
+such references:
+
+```plaintext
+ERROR: Could not find dependencies: <dependency-name>. You may need to run npm install
+```
+
+As a workaround, remove the [`retire.js`](analyzers.md#selecting-specific-analyzers) analyzer from
+[DS_DEFAULT_ANALYZERS](#configuring-dependency-scanning).
+
## Troubleshooting
### Error response from daemon: error processing tar file: docker-tar: relocation error
-This error occurs when the Docker version used to run the SAST job is `19.03.0`.
-You are advised to update to Docker `19.03.1` or greater. Older versions are not
+This error occurs when the Docker version that runs the Dependency Scanning job is `19.03.00`.
+Consider updating to Docker `19.03.1` or greater. Older versions are not
affected. Read more in
[this issue](https://gitlab.com/gitlab-org/gitlab/issues/13830#note_211354992 "Current SAST container fails").
diff --git a/doc/user/application_security/img/adding_a_dismissal_reason_v13_0.png b/doc/user/application_security/img/adding_a_dismissal_reason_v13_0.png
new file mode 100644
index 00000000000..385236d08f2
--- /dev/null
+++ b/doc/user/application_security/img/adding_a_dismissal_reason_v13_0.png
Binary files differ
diff --git a/doc/user/application_security/img/dismissed_info_v12_3.png b/doc/user/application_security/img/dismissed_info_v12_3.png
deleted file mode 100644
index 92037493eaa..00000000000
--- a/doc/user/application_security/img/dismissed_info_v12_3.png
+++ /dev/null
Binary files differ
diff --git a/doc/user/application_security/img/interacting_with_vulnerability_v13_0.png b/doc/user/application_security/img/interacting_with_vulnerability_v13_0.png
new file mode 100644
index 00000000000..866ad74d42c
--- /dev/null
+++ b/doc/user/application_security/img/interacting_with_vulnerability_v13_0.png
Binary files differ
diff --git a/doc/user/application_security/img/interactive_reports.png b/doc/user/application_security/img/interactive_reports.png
deleted file mode 100644
index 1b2ef0d3da9..00000000000
--- a/doc/user/application_security/img/interactive_reports.png
+++ /dev/null
Binary files differ
diff --git a/doc/user/application_security/img/security_configuration_page_v12_9.png b/doc/user/application_security/img/security_configuration_page_v12_9.png
deleted file mode 100644
index a81d82e03c3..00000000000
--- a/doc/user/application_security/img/security_configuration_page_v12_9.png
+++ /dev/null
Binary files differ
diff --git a/doc/user/application_security/img/security_configuration_page_v13_1.png b/doc/user/application_security/img/security_configuration_page_v13_1.png
new file mode 100644
index 00000000000..0147084f705
--- /dev/null
+++ b/doc/user/application_security/img/security_configuration_page_v13_1.png
Binary files differ
diff --git a/doc/user/application_security/index.md b/doc/user/application_security/index.md
index dadff8583db..eefa52eb5c3 100644
--- a/doc/user/application_security/index.md
+++ b/doc/user/application_security/index.md
@@ -44,6 +44,12 @@ To add Container Scanning, follow the steps listed in the [Container Scanning do
To further configure any of the other scanners, refer to each scanner's documentation.
+### Override the default registry base address
+
+By default, GitLab security scanners use `registry.gitlab.com/gitlab-org/security-products/analyzers` as the
+base address for Docker images. You can override this globally by setting the variable
+`SECURE_ANALYZERS_PREFIX` to another location. Note that this affects all scanners at once.
+
## Security scanning tools
GitLab uses the following tools to scan and report known vulnerabilities found in your project.
@@ -54,6 +60,7 @@ GitLab uses the following tools to scan and report known vulnerabilities found i
| [Dependency List](dependency_list/index.md) **(ULTIMATE)** | View your project's dependencies and their known vulnerabilities. |
| [Dependency Scanning](dependency_scanning/index.md) **(ULTIMATE)** | Analyze your dependencies for known vulnerabilities. |
| [Dynamic Application Security Testing (DAST)](dast/index.md) **(ULTIMATE)** | Analyze running web applications for known vulnerabilities. |
+| [Secret Detection](secret_detection/index.md) **(ULTIMATE)** | Analyze Git history for leaked secrets. |
| [Security Dashboard](security_dashboard/index.md) **(ULTIMATE)** | View vulnerabilities in all your projects and groups. |
| [Static Application Security Testing (SAST)](sast/index.md) **(ULTIMATE)** | Analyze source code for known vulnerabilities. |
@@ -69,9 +76,10 @@ The scanning tools and vulnerabilities database are updated regularly.
| [Static Application Security Testing (SAST)](sast/index.md) | Relies exclusively on [the tools GitLab wraps](sast/index.md#supported-languages-and-frameworks). The underlying analyzers are updated at least once per month if a relevant update is available. The vulnerabilities database is updated by the upstream tools. |
Currently, you do not have to update GitLab to benefit from the latest vulnerabilities definitions.
-The security tools are released as Docker images. The vendored job definitions to enable them use
-the `x-y-stable` image tags that get overridden each time a new release of the tools is pushed. The
-Docker images are updated to match the previous GitLab releases, so users automatically get the
+The security tools are released as Docker images. The vendored job definitions that enable them use
+major release tags according to [Semantic Versioning](https://semver.org/). Each new release of the
+tools overrides these tags.
+The Docker images are updated to match the previous GitLab releases, so users automatically get the
latest versions of the scanning tools without having to do anything. There are some known issues
with this approach, however, and there is a
[plan to resolve them](https://gitlab.com/gitlab-org/gitlab/issues/9725).
@@ -80,9 +88,6 @@ with this approach, however, and there is a
> Introduced in [GitLab Ultimate](https://about.gitlab.com/pricing/) 10.8.
-CAUTION: **Warning:**
-This feature is currently [Alpha](https://about.gitlab.com/handbook/product/#alpha-beta-ga) and while you can start using it, it may receive important changes in the future.
-
Each security vulnerability in the merge request report or the
[Security Dashboard](security_dashboard/index.md) is actionable. Click an entry to view detailed
information with several options:
@@ -95,25 +100,27 @@ information with several options:
- [Solution](#solutions-for-vulnerabilities-auto-remediation): For some vulnerabilities,
a solution is provided for how to fix the vulnerability.
-![Interacting with security reports](img/interactive_reports.png)
+![Interacting with security reports](img/interacting_with_vulnerability_v13_0.png)
### Dismissing a vulnerability
-You can dismiss vulnerabilities by clicking the **Dismiss vulnerability** button.
-This will dismiss the vulnerability and re-render it to reflect its dismissed state.
-If you wish to undo this dismissal, you can click the **Undo dismiss** button.
+To dismiss a vulnerability, you must set its status to Dismissed. Follow these steps to do so:
+
+1. Select the vulnerability in the Security Dashboard.
+1. Select **Dismissed** from the **Status** selector menu at the top-right.
+
+You can undo this action by selecting a different status from the same menu.
#### Adding a dismissal reason
> Introduced in [GitLab Ultimate](https://about.gitlab.com/pricing/) 12.0.
-When dismissing a vulnerability, it's often helpful to provide a reason for doing so.
-If you press the comment button next to **Dismiss vulnerability** in the modal,
-a text box appears for you to add a comment with your dismissal.
-Once added, you can edit or delete it. This allows you to add and update
-context for a vulnerability as you learn more over time.
+When dismissing a vulnerability, it's often helpful to provide a reason for doing so. Upon setting a
+vulnerability's status to Dismissed, a text box appears for you to add a comment with your
+dismissal. Once added, you can edit or delete it. This allows you to add and update context for a
+vulnerability as you learn more over time.
-![Dismissed vulnerability comment](img/dismissed_info_v12_3.png)
+![Dismissed vulnerability comment](img/adding_a_dismissal_reason_v13_0.png)
#### Dismissing multiple vulnerabilities
@@ -193,9 +200,19 @@ security team when a merge request would introduce one of the following security
- A security vulnerability
- A software license compliance violation
-This threshold is defined as `high`, `critical`, or `unknown` severity. When any vulnerabilities are
-present within a merge request, an approval is required from the `Vulnerability-Check` approver
-group.
+The security vulnerability threshold is defined as `high`, `critical`, or `unknown` severity. The
+`Vulnerability-Check` approver group must approve merge requests that contain vulnerabilities.
+
+When GitLab can assess vulnerability severity, the rating can be one of the following:
+
+- `unknown`
+- `low`
+- `medium`
+- `high`
+- `critical`
+
+The rating `unknown` indicates that the underlying scanner doesn't contain or provide a severity
+rating.
### Enabling Security Approvals within a project
@@ -209,7 +226,7 @@ Any code changes cause the approvals required to reset.
An approval is required when a security report:
-- Contains a new vulnerability of `high`, `critical`, or `unknown` severity.
+- Contains a new vulnerability of `high`, `critical`, or `unknown` severity, regardless of dismissal.
- Is not generated during pipeline execution.
An approval is optional when a security report:
@@ -259,7 +276,7 @@ to pass a username and password. You can set it under your project's settings
so that your credentials aren't exposed in `.gitlab-ci.yml`.
If the username is `myuser` and the password is `verysecret` then you would
-[set the following variable](../../ci/variables/README.md#via-the-ui)
+[set the following variable](../../ci/variables/README.md#create-a-custom-variable-in-the-ui)
under your project's settings:
| Type | Key | Value |
@@ -313,7 +330,8 @@ You can do it quickly by following the hyperlink given to run a new pipeline.
### Getting error message `sast job: stage parameter should be [some stage name here]`
-When including a security job template like [`SAST`](sast/index.md#configuration),
+When [including](../../ci/yaml/README.md#includetemplate) a `.gitlab-ci.yml` template
+like [`SAST.gitlab-ci.yml`](https://gitlab.com/gitlab-org/gitlab/blob/master/lib/gitlab/ci/templates/Security/SAST.gitlab-ci.yml),
the following error may occur, depending on your GitLab CI/CD configuration:
```plaintext
@@ -326,15 +344,115 @@ This error appears when the included job's stage (named `test`) isn't declared i
To fix this issue, you can either:
- Add a `test` stage in your `.gitlab-ci.yml`.
-- Change the default stage of the included security jobs. For example, with `SAST`:
+- Change the default stage of the included security jobs. For example, with SpotBugs (SAST):
```yaml
include:
template: SAST.gitlab-ci.yml
- sast:
+ spotbugs-sast:
stage: unit-tests
```
-[Learn more on overriding the SAST template](sast/index.md#overriding-the-sast-template).
+[Learn more on overriding SAST jobs](sast/index.md#overriding-sast-jobs).
All the security scanning tools define their stage, so this error can occur with all of them.
+
+### Getting error message `sast job: config key may not be used with 'rules': only/except`
+
+When [including](../../ci/yaml/README.md#includetemplate) a `.gitlab-ci.yml` template
+like [`SAST.gitlab-ci.yml`](https://gitlab.com/gitlab-org/gitlab/blob/master/lib/gitlab/ci/templates/Security/SAST.gitlab-ci.yml),
+the following error may occur, depending on your GitLab CI/CD configuration:
+
+```plaintext
+Found errors in your .gitlab-ci.yml:
+
+ jobs:sast config key may not be used with `rules`: only/except
+```
+
+This error appears when the included job's `rules` configuration has been [overridden](sast/index.md#overriding-sast-jobs)
+with [the deprecated `only` or `except` syntax.](../../ci/yaml/README.md#onlyexcept-basic)
+To fix this issue, you must either:
+
+- [Transition your `only/except` syntax to `rules`](#transitioning-your-onlyexcept-syntax-to-rules).
+- (Temporarily) [Pin your templates to the deprecated versions](#pin-your-templates-to-the-deprecated-versions)
+
+[Learn more on overriding SAST jobs](sast/index.md#overriding-sast-jobs).
+
+#### Transitioning your `only/except` syntax to `rules`
+
+When overriding the template to control job execution, previous instances of
+[`only` or `except`](../../ci/yaml/README.md#onlyexcept-basic) are no longer compatible
+and must be transitioned to [the `rules` syntax](../../ci/yaml/README.md#rules).
+
+If your override is aimed at limiting jobs to only run on `master`, the previous syntax
+would look similar to:
+
+```yaml
+include:
+ - template: SAST.gitlab-ci.yml
+
+# Ensure that the scanning is only executed on master or merge requests
+spotbugs-sast:
+ only:
+ refs:
+ - master
+ - merge_requests
+```
+
+To transition the above configuration to the new `rules` syntax, the override
+would be written as follows:
+
+```yaml
+include:
+ - template: SAST.gitlab-ci.yml
+
+# Ensure that the scanning is only executed on master or merge requests
+spotbugs-sast:
+ rules:
+ - if: $CI_COMMIT_BRANCH == "master"
+ - if: $CI_MERGE_REQUEST_ID
+```
+
+If your override is aimed at limiting jobs to only run on branches, not tags,
+it would look similar to:
+
+```yaml
+include:
+ - template: SAST.gitlab-ci.yml
+
+# Ensure that the scanning is not executed on tags
+spotbugs-sast:
+ except:
+ - tags
+```
+
+To transition to the new `rules` syntax, the override would be rewritten as:
+
+```yaml
+include:
+ - template: SAST.gitlab-ci.yml
+
+# Ensure that the scanning is not executed on tags
+spotbugs-sast:
+ rules:
+ - if: $CI_COMMIT_TAG == null
+```
+
+[Learn more on the usage of `rules`](../../ci/yaml/README.md#rules).
+
+#### Pin your templates to the deprecated versions
+
+To ensure the latest support, we **strongly** recommend that you migrate to [`rules`](../../ci/yaml/README.md#rules).
+
+If you're unable to immediately update your CI configuration, there are several workarounds that
+involve pinning to the previous template versions, for example:
+
+ ```yaml
+ include:
+ remote: 'https://gitlab.com/gitlab-org/gitlab/-/raw/12-10-stable-ee/lib/gitlab/ci/templates/Security/SAST.gitlab-ci.yml'
+ ```
+
+Additionally, we provide a dedicated project containing the versioned legacy templates.
+This can be useful for offline setups or anyone wishing to use [Auto DevOps](../../topics/autodevops/index.md).
+
+Instructions are available in the [legacy template project](https://gitlab.com/gitlab-org/auto-devops-v12-10).
diff --git a/doc/user/application_security/offline_deployments/index.md b/doc/user/application_security/offline_deployments/index.md
index 61b34901849..3deb38902f2 100644
--- a/doc/user/application_security/offline_deployments/index.md
+++ b/doc/user/application_security/offline_deployments/index.md
@@ -61,6 +61,14 @@ Once a vulnerability is found, you can interact with it. Read more on how to
Please note that in some cases the reported vulnerabilities provide metadata that can contain
external links exposed in the UI. These links might not be accessible within an offline environment.
+### Suggested Solutions for vulnerabilities
+
+The [suggested solutions](../index.md#solutions-for-vulnerabilities-auto-remediation) feature
+(auto-remediation) is available for Dependency Scanning and Container Scanning, but may not work
+depending on your instance's configuration. We can only suggest solutions, which are generally more
+current versions that have been patched, when we are able to access up-to-date registry services
+hosting the latest versions of that dependency or image.
+
### Scanner signature and rule updates
When connected to the internet, some scanners will reference public databases
@@ -79,3 +87,4 @@ above. You can find more information at each of the pages below:
- [SAST offline directions](../sast/index.md#running-sast-in-an-offline-environment)
- [DAST offline directions](../dast/index.md#running-dast-in-an-offline-environment)
- [License Compliance offline directions](../../compliance/license_compliance/index.md#running-license-compliance-in-an-offline-environment)
+- [Dependency Scanning offline directions](../dependency_scanning/index.md#running-dependency-scanning-in-an-offline-environment)
diff --git a/doc/user/application_security/sast/analyzers.md b/doc/user/application_security/sast/analyzers.md
index 9010a7cae0b..08078a66719 100644
--- a/doc/user/application_security/sast/analyzers.md
+++ b/doc/user/application_security/sast/analyzers.md
@@ -52,7 +52,7 @@ include:
- template: SAST.gitlab-ci.yml
variables:
- SAST_ANALYZER_IMAGE_PREFIX: my-docker-registry/gl-images
+ SECURE_ANALYZERS_PREFIX: my-docker-registry/gl-images
```
This configuration requires that your custom registry provides images for all
@@ -149,7 +149,7 @@ The [Security Scanner Integration](../../../development/integrations/secure.md)
| End line | ✓ | ✓ | ð„‚ | ✓ | ✓ | ð„‚ | ð„‚ | ð„‚ | ð„‚ | ð„‚ | ð„‚ | ð„‚ | ✓ |
| Start column | ✓ | ð„‚ | ð„‚ | ✓ | ✓ | ✓ | ✓ | ð„‚ | ð„‚ | ✓ | ✓ | ð„‚ | ✓ |
| End column | ✓ | ð„‚ | ð„‚ | ✓ | ✓ | ð„‚ | ð„‚ | ð„‚ | ð„‚ | ð„‚ | ð„‚ | ð„‚ | ✓ |
-| External id (e.g. CVE) | ð„‚ | ð„‚ | âš  | ð„‚ | âš  | ✓ | ð„‚ | ð„‚ | ð„‚ | ð„‚ | ð„‚ | ð„‚ | ð„‚ |
+| External ID (e.g. CVE) | ð„‚ | ð„‚ | âš  | ð„‚ | âš  | ✓ | ð„‚ | ð„‚ | ð„‚ | ð„‚ | ð„‚ | ð„‚ | ð„‚ |
| URLs | ✓ | ð„‚ | ✓ | ð„‚ | âš  | ð„‚ | âš  | ð„‚ | ð„‚ | ð„‚ | ð„‚ | ð„‚ | ð„‚ |
| Internal doc/explanation | ✓ | âš  | ✓ | ð„‚ | ✓ | ð„‚ | ð„‚ | ð„‚ | ð„‚ | ð„‚ | ð„‚ | ✓ | ð„‚ |
| Solution | ✓ | ð„‚ | ð„‚ | ð„‚ | âš  | ✓ | ð„‚ | ð„‚ | ð„‚ | ð„‚ | ð„‚ | ð„‚ | ð„‚ |
diff --git a/doc/user/application_security/sast/img/sast_v12_9.png b/doc/user/application_security/sast/img/sast_v12_9.png
deleted file mode 100644
index 3c6ee7a276b..00000000000
--- a/doc/user/application_security/sast/img/sast_v12_9.png
+++ /dev/null
Binary files differ
diff --git a/doc/user/application_security/sast/img/sast_v13_0.png b/doc/user/application_security/sast/img/sast_v13_0.png
new file mode 100644
index 00000000000..b4aea6ea466
--- /dev/null
+++ b/doc/user/application_security/sast/img/sast_v13_0.png
Binary files differ
diff --git a/doc/user/application_security/sast/index.md b/doc/user/application_security/sast/index.md
index a6457d58fe2..370c6d0e8e7 100644
--- a/doc/user/application_security/sast/index.md
+++ b/doc/user/application_security/sast/index.md
@@ -13,7 +13,7 @@ to learn how to protect your organization.
## Overview
-If you are using [GitLab CI/CD](../../../ci/README.md), you can analyze your source code for known
+If you're using [GitLab CI/CD](../../../ci/README.md), you can analyze your source code for known
vulnerabilities using Static Application Security Testing (SAST).
You can take advantage of SAST by doing one of the following:
@@ -25,7 +25,7 @@ You can take advantage of SAST by doing one of the following:
GitLab checks the SAST report, compares the found vulnerabilities between the
source and target branches, and shows the information right on the merge request.
-![SAST Widget](img/sast_v12_9.png)
+![SAST Widget](img/sast_v13_0.png)
The results are sorted by the priority of the vulnerability:
@@ -36,6 +36,9 @@ The results are sorted by the priority of the vulnerability:
1. Unknown
1. Everything else
+NOTE: **Note:**
+A pipeline consists of multiple jobs, including SAST and DAST scanning. If any job fails to finish for any reason, the security dashboard won't show SAST scanner output. For example, if the SAST job finishes but the DAST job fails, the security dashboard won't show SAST results. The analyzer will output an [exit code](../../../development/integrations/secure.md#exit-code) on failure.
+
## Use cases
- Your code has a potentially dangerous attribute in a class, or unsafe code
@@ -45,19 +48,17 @@ The results are sorted by the priority of the vulnerability:
## Requirements
-To run a SAST job, by default, you need GitLab Runner with the
-[`docker`](https://docs.gitlab.com/runner/executors/docker.html#use-docker-in-docker-with-privileged-mode) or
-[`kubernetes`](https://docs.gitlab.com/runner/install/kubernetes.html#running-privileged-containers-for-the-runners)
-executor running in privileged mode. If you're using the shared Runners on GitLab.com,
-this is enabled by default.
+To run SAST jobs, by default, you need GitLab Runner with the
+[`docker`](https://docs.gitlab.com/runner/executors/docker.html) or
+[`kubernetes`](https://docs.gitlab.com/runner/install/kubernetes.html) executor.
+If you're using the shared Runners on GitLab.com, this is enabled by default.
-Privileged mode is not necessary if you've [disabled Docker in Docker
-for SAST](#disabling-docker-in-docker-for-sast)
+Beginning with GitLab 13.0, Docker privileged mode is necessary only if you've [enabled Docker-in-Docker for SAST](#enabling-docker-in-docker).
CAUTION: **Caution:** Our SAST jobs currently expect a Linux container type. Windows containers are not yet supported.
CAUTION: **Caution:**
-If you use your own Runners, make sure that the Docker version you have installed
+If you use your own Runners, make sure the Docker version installed
is **not** `19.03.0`. See [troubleshooting information](#error-response-from-daemon-error-processing-tar-file-docker-tar-relocation-error) for details.
## Supported languages and frameworks
@@ -66,9 +67,10 @@ The following table shows which languages, package managers and frameworks are s
| Language (package managers) / framework | Scan tool | Introduced in GitLab Version |
|-----------------------------------------------------------------------------|----------------------------------------------------------------------------------------|------------------------------|
-| .NET | [Security Code Scan](https://security-code-scan.github.io) | 11.0 |
+| .NET Core | [Security Code Scan](https://security-code-scan.github.io) | 11.0 |
+| .NET Framework | [Security Code Scan](https://security-code-scan.github.io) | 13.0 |
| Any | [Gitleaks](https://github.com/zricethezav/gitleaks) and [TruffleHog](https://github.com/dxa4481/truffleHog) | 11.9 |
-| Apex (Salesforce) | [pmd](https://pmd.github.io/pmd/index.html) | 12.1 |
+| Apex (Salesforce) | [PMD](https://pmd.github.io/pmd/index.html) | 12.1 |
| C/C++ | [Flawfinder](https://dwheeler.com/flawfinder/) | 10.7 |
| Elixir (Phoenix) | [Sobelow](https://github.com/nccgroup/sobelow) | 11.10 |
| Go | [Gosec](https://github.com/securego/gosec) | 10.7 |
@@ -82,7 +84,7 @@ The following table shows which languages, package managers and frameworks are s
| React | [ESLint react plugin](https://github.com/yannickcr/eslint-plugin-react) | 12.5 |
| Ruby on Rails | [brakeman](https://brakemanscanner.org) | 10.3 |
| Scala ([Ant](https://ant.apache.org/), [Gradle](https://gradle.org/), [Maven](https://maven.apache.org/) and [SBT](https://www.scala-sbt.org/)) | [SpotBugs](https://spotbugs.github.io/) with the [find-sec-bugs](https://find-sec-bugs.github.io/) plugin | 11.0 (SBT) & 11.9 (Ant, Gradle, Maven) |
-| TypeScript | [TSLint config security](https://github.com/webschik/tslint-config-security/) | 11.9 |
+| TypeScript | [`tslint-config-security`](https://github.com/webschik/tslint-config-security/) | 11.9 |
NOTE: **Note:**
The Java analyzers can also be used for variants like the
@@ -101,7 +103,7 @@ provided by [Auto DevOps](../../../topics/autodevops/index.md).
For GitLab 11.9 and later, to enable SAST you must [include](../../../ci/yaml/README.md#includetemplate)
the [`SAST.gitlab-ci.yml` template](https://gitlab.com/gitlab-org/gitlab/blob/master/lib/gitlab/ci/templates/Security/SAST.gitlab-ci.yml)
-that is provided as a part of your GitLab installation. For GitLab versions earlier than 11.9, you
+provided as a part of your GitLab installation. For GitLab versions earlier than 11.9, you
can copy and use the job as defined that template.
Add the following to your `.gitlab-ci.yml` file:
@@ -111,22 +113,19 @@ include:
- template: SAST.gitlab-ci.yml
```
-The included template will create a `sast` job in your CI/CD pipeline and scan
+The included template will create SAST jobs in your CI/CD pipeline and scan
your project's source code for possible vulnerabilities.
The results will be saved as a
-[SAST report artifact](../../../ci/yaml/README.md#artifactsreportssast-ultimate)
+[SAST report artifact](../../../ci/pipelines/job_artifacts.md#artifactsreportssast-ultimate)
that you can later download and analyze. Due to implementation limitations, we
-always take the latest SAST artifact available. Behind the scenes, the
-[GitLab SAST Docker image](https://gitlab.com/gitlab-org/security-products/sast)
-is used to detect the languages/frameworks and in turn runs the matching scan tools.
+always take the latest SAST artifact available.
### Customizing the SAST settings
The SAST settings can be changed through [environment variables](#available-variables)
by using the
[`variables`](../../../ci/yaml/README.md#variables) parameter in `.gitlab-ci.yml`.
-
In the following example, we include the SAST template and at the same time we
set the `SAST_GOSEC_LEVEL` variable to `2`:
@@ -139,25 +138,26 @@ variables:
```
Because the template is [evaluated before](../../../ci/yaml/README.md#include)
-the pipeline configuration, the last mention of the variable will take precedence.
+the pipeline configuration, the last mention of the variable takes precedence.
-### Overriding the SAST template
+### Overriding SAST jobs
CAUTION: **Deprecation:**
Beginning in GitLab 13.0, the use of [`only` and `except`](../../../ci/yaml/README.md#onlyexcept-basic)
is no longer supported. When overriding the template, you must use [`rules`](../../../ci/yaml/README.md#rules) instead.
-If you want to override the job definition (for example, change properties like
-`variables` or `dependencies`), you need to declare a `sast` job after the
-template inclusion and specify any additional keys under it. For example:
+To override a job definition, (for example, change properties like `variables` or `dependencies`),
+declare a job with the same name as the SAST job to override. Place this new job after the template
+inclusion and specify any additional keys under it. For example, this enables `FAIL_NEVER` for the
+`spotbugs` analyzer:
```yaml
include:
- template: SAST.gitlab-ci.yml
-sast:
+spotbugs-sast:
variables:
- CI_DEBUG_TRACE: "true"
+ FAIL_NEVER: 1
```
### Using environment variables to pass credentials for private repositories
@@ -170,57 +170,42 @@ it via [custom environment variables](#custom-environment-variables).
#### Using a variable to pass username and password to a private Maven repository
-If you have a private Maven repository which requires login credentials,
+If your private Maven repository requires login credentials,
you can use the `MAVEN_CLI_OPTS` environment variable.
-Read more on [how to use private Maven repos](../index.md#using-private-maven-repos).
+Read more on [how to use private Maven repositories](../index.md#using-private-maven-repos).
-### Disabling Docker in Docker for SAST
+### Enabling Docker-in-Docker
-You can avoid the need for Docker in Docker by running the individual analyzers.
-This does not require running the executor in privileged mode. For example:
+If needed, you can enable Docker-in-Docker to restore the SAST behavior that existed prior to GitLab
+13.0. Follow these steps to do so:
-```yaml
-include:
- - template: SAST.gitlab-ci.yml
+1. Configure GitLab Runner with Docker-inDocker in [privileged mode](https://docs.gitlab.com/runner/executors/docker.html#use-docker-in-docker-with-privileged-mode).
+1. Set the variable `SAST_DISABLE_DIND` set to `false`:
-variables:
- SAST_DISABLE_DIND: "true"
-```
+ ```yaml
+ include:
+ - template: SAST.gitlab-ci.yml
-This will create individual `<analyzer-name>-sast` jobs for each analyzer that runs in your CI/CD pipeline.
+ variables:
+ SAST_DISABLE_DIND: "false"
+ ```
-By removing Docker-in-Docker (DIND), GitLab relies on [Linguist](https://github.com/github/linguist)
-to start relevant analyzers depending on the detected repository language(s) instead of the
-[orchestrator](https://gitlab.com/gitlab-org/security-products/dependency-scanning/). However, there
-are some differences in the way repository languages are detected between DIND and non-DIND. You can
-observe these differences by checking both Linguist and the common library. For instance, Linguist
-looks for `*.java` files to spin up the [spotbugs](https://gitlab.com/gitlab-org/security-products/analyzers/spotbugs)
-image, while orchestrator only looks for the existence of `pom.xml`, `build.xml`, `gradlew`,
-`grailsw`, or `mvnw`. GitLab uses Linguist to detect new file types in the default branch. This
-means that when introducing files or dependencies for a new language or package manager, the
-corresponding scans won't be triggered in the MR and will only run on the default branch once the
-MR is merged. This will be addressed by [#211702](https://gitlab.com/gitlab-org/gitlab/-/issues/211702).
-
-NOTE: **Note:**
-With the current language detection logic, any new languages or frameworks introduced within the
-context of a merge request don't trigger a corresponding scan. These scans only occur once the code
-is committed to the default branch.
+This creates a single `sast` job in your CI/CD pipeline instead of multiple `<analyzer-name>-sast`
+jobs.
-#### Enabling kubesec analyzer
+#### Enabling Kubesec analyzer
> [Introduced](https://gitlab.com/gitlab-org/gitlab/issues/12752) in GitLab Ultimate 12.6.
-When [Docker in Docker is disabled](#disabling-docker-in-docker-for-sast),
-you will need to set `SCAN_KUBERNETES_MANIFESTS` to `"true"` to enable the
-kubesec analyzer. In `.gitlab-ci.yml`, define:
+You need to set `SCAN_KUBERNETES_MANIFESTS` to `"true"` to enable the
+Kubesec analyzer. In `.gitlab-ci.yml`, define:
```yaml
include:
- template: SAST.gitlab-ci.yml
variables:
- SAST_DISABLE_DIND: "true"
SCAN_KUBERNETES_MANIFESTS: "true"
```
@@ -246,9 +231,6 @@ stages:
include:
- template: SAST.gitlab-ci.yml
-variables:
- SAST_DISABLE_DIND: "true"
-
build:
stage: build
script:
@@ -291,10 +273,11 @@ The following are Docker image-related variables.
| Environment variable | Description |
|------------------------------|--------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------|
-| `SAST_ANALYZER_IMAGE_PREFIX` | Override the name of the Docker registry providing the default images (proxy). Read more about [customizing analyzers](analyzers.md). |
-| `SAST_ANALYZER_IMAGE_TAG` | **DEPRECATED:** Override the Docker tag of the default images. Read more about [customizing analyzers](analyzers.md). |
+| `SECURE_ANALYZERS_PREFIX` | Override the name of the Docker registry providing the default images (proxy). Read more about [customizing analyzers](analyzers.md). |
+| `SAST_ANALYZER_IMAGE_PREFIX` | **DEPRECATED**: Use `SECURE_ANALYZERS_PREFIX` instead. |
+| `SAST_ANALYZER_IMAGE_TAG` | **DEPRECATED:** Override the Docker tag of the default images. Read more about [customizing analyzers](analyzers.md). |
| `SAST_DEFAULT_ANALYZERS` | Override the names of default images. Read more about [customizing analyzers](analyzers.md). |
-| `SAST_DISABLE_DIND` | Disable Docker in Docker and run analyzers [individually](#disabling-docker-in-docker-for-sast). |
+| `SAST_DISABLE_DIND` | Disable Docker-in-Docker and run analyzers [individually](#enabling-docker-in-docker). This variable is `true` by default. |
#### Vulnerability filters
@@ -307,25 +290,22 @@ Some analyzers make it possible to filter out vulnerabilities under a given thre
| `SAST_BRAKEMAN_LEVEL` | 1 | Ignore Brakeman vulnerabilities under given confidence level. Integer, 1=Low 3=High. |
| `SAST_FLAWFINDER_LEVEL` | 1 | Ignore Flawfinder vulnerabilities under given risk level. Integer, 0=No risk, 5=High risk. |
| `SAST_GITLEAKS_ENTROPY_LEVEL` | 8.0 | Minimum entropy for secret detection. Float, 0.0 = low, 8.0 = high. |
-| `SAST_GOSEC_LEVEL` | 0 | Ignore gosec vulnerabilities under given confidence level. Integer, 0=Undefined, 1=Low, 2=Medium, 3=High. |
-| `SAST_GITLEAKS_COMMIT_FROM` | - | The commit a gitleaks scan starts at. |
-| `SAST_GITLEAKS_COMMIT_TO` | - | The commit a gitleaks scan ends at. |
-| `SAST_GITLEAKS_HISTORIC_SCAN` | false | Flag to enable a historic gitleaks scan. |
+| `SAST_GOSEC_LEVEL` | 0 | Ignore Gosec vulnerabilities under given confidence level. Integer, 0=Undefined, 1=Low, 2=Medium, 3=High. |
+| `SAST_GITLEAKS_COMMIT_FROM` | - | The commit a Gitleaks scan starts at. |
+| `SAST_GITLEAKS_COMMIT_TO` | - | The commit a Gitleaks scan ends at. |
+| `SAST_GITLEAKS_HISTORIC_SCAN` | false | Flag to enable a historic Gitleaks scan. |
#### Docker-in-Docker orchestrator
-The following variables configure the Docker-in-Docker orchestrator.
+The following variables configure the Docker-in-Docker orchestrator, and therefore are only used when the Docker-in-Docker mode is [enabled](#enabling-docker-in-docker).
| Environment variable | Default value | Description |
|------------------------------------------|---------------|-------------|
-| `SAST_ANALYZER_IMAGES` | | Comma-separated list of custom images. Default images are still enabled. Read more about [customizing analyzers](analyzers.md). Not available when [Docker-in-Docker is disabled](#disabling-docker-in-docker-for-sast). |
-| `SAST_PULL_ANALYZER_IMAGES` | 1 | Pull the images from the Docker registry (set to 0 to disable). Read more about [customizing analyzers](analyzers.md). Not available when [Docker-in-Docker is disabled](#disabling-docker-in-docker-for-sast). |
-| `SAST_DOCKER_CLIENT_NEGOTIATION_TIMEOUT` | 2m | Time limit for Docker client negotiation. Timeouts are parsed using Go's [`ParseDuration`](https://golang.org/pkg/time/#ParseDuration). Valid time units are "ns", "us" (or "µs"), "ms", "s", "m", "h". For example, "300ms", "1.5h" or "2h45m". |
-| `SAST_PULL_ANALYZER_IMAGE_TIMEOUT` | 5m | Time limit when pulling the image of an analyzer. Timeouts are parsed using Go's [`ParseDuration`](https://golang.org/pkg/time/#ParseDuration). Valid time units are "ns", "us" (or "µs"), "ms", "s", "m", "h". For example, "300ms", "1.5h" or "2h45m". |
-| `SAST_RUN_ANALYZER_TIMEOUT` | 20m | Time limit when running an analyzer. Timeouts are parsed using Go's [`ParseDuration`](https://golang.org/pkg/time/#ParseDuration). Valid time units are "ns", "us" (or "µs"), "ms", "s", "m", "h". For example, "300ms", "1.5h" or "2h45m".|
-
-NOTE: **Note:**
-Timeout variables are not applicable for setups with [disabled Docker In Docker](index.md#disabling-docker-in-docker-for-sast).
+| `SAST_ANALYZER_IMAGES` | | Comma-separated list of custom images. Default images are still enabled. Read more about [customizing analyzers](analyzers.md). |
+| `SAST_PULL_ANALYZER_IMAGES` | 1 | Pull the images from the Docker registry (set to 0 to disable). Read more about [customizing analyzers](analyzers.md). |
+| `SAST_DOCKER_CLIENT_NEGOTIATION_TIMEOUT` | 2m | Time limit for Docker client negotiation. Timeouts are parsed using Go's [`ParseDuration`](https://golang.org/pkg/time/#ParseDuration). Valid time units are `ns`, `us` (or `µs`), `ms`, `s`, `m`, `h`. For example, `300ms`, `1.5h` or `2h45m`. |
+| `SAST_PULL_ANALYZER_IMAGE_TIMEOUT` | 5m | Time limit when pulling the image of an analyzer. Timeouts are parsed using Go's [`ParseDuration`](https://golang.org/pkg/time/#ParseDuration). Valid time units are `ns`, `us` (or `µs`), `ms`, `s`, `m`, `h`. For example, `300ms`, `1.5h` or `2h45m`. |
+| `SAST_RUN_ANALYZER_TIMEOUT` | 20m | Time limit when running an analyzer. Timeouts are parsed using Go's [`ParseDuration`](https://golang.org/pkg/time/#ParseDuration). Valid time units are `ns`, `us` (or `µs`), `ms`, `s`, `m`, `h`. For example, `300ms`, `1.5h` or `2h45m`.|
#### Analyzer settings
@@ -333,25 +313,26 @@ Some analyzers can be customized with environment variables.
| Environment variable | Analyzer | Description |
|-----------------------------|----------|-------------|
-| `SCAN_KUBERNETES_MANIFESTS` | kubesec | Set to `"true"` to scan Kubernetes manifests when [Docker in Docker](#disabling-docker-in-docker-for-sast) is disabled. |
-| `ANT_HOME` | spotbugs | The `ANT_HOME` environment variable. |
-| `ANT_PATH` | spotbugs | Path to the `ant` executable. |
-| `GRADLE_PATH` | spotbugs | Path to the `gradle` executable. |
-| `JAVA_OPTS` | spotbugs | Additional arguments for the `java` executable. |
-| `JAVA_PATH` | spotbugs | Path to the `java` executable. |
-| `SAST_JAVA_VERSION` | spotbugs | Which Java version to use. Supported versions are `8` and `11`. Defaults to `8`. |
-| `MAVEN_CLI_OPTS` | spotbugs | Additional arguments for the `mvn` or `mvnw` executable. |
-| `MAVEN_PATH` | spotbugs | Path to the `mvn` executable. |
-| `MAVEN_REPO_PATH` | spotbugs | Path to the Maven local repository (shortcut for the `maven.repo.local` property). |
-| `SBT_PATH` | spotbugs | Path to the `sbt` executable. |
-| `FAIL_NEVER` | spotbugs | Set to `1` to ignore compilation failure. |
+| `SCAN_KUBERNETES_MANIFESTS` | Kubesec | Set to `"true"` to scan Kubernetes manifests. |
+| `ANT_HOME` | SpotBugs | The `ANT_HOME` environment variable. |
+| `ANT_PATH` | SpotBugs | Path to the `ant` executable. |
+| `GRADLE_PATH` | SpotBugs | Path to the `gradle` executable. |
+| `JAVA_OPTS` | SpotBugs | Additional arguments for the `java` executable. |
+| `JAVA_PATH` | SpotBugs | Path to the `java` executable. |
+| `SAST_JAVA_VERSION` | SpotBugs | Which Java version to use. Supported versions are `8` and `11`. Defaults to `8`. |
+| `MAVEN_CLI_OPTS` | SpotBugs | Additional arguments for the `mvn` or `mvnw` executable. |
+| `MAVEN_PATH` | SpotBugs | Path to the `mvn` executable. |
+| `MAVEN_REPO_PATH` | SpotBugs | Path to the Maven local repository (shortcut for the `maven.repo.local` property). |
+| `SBT_PATH` | SpotBugs | Path to the `sbt` executable. |
+| `FAIL_NEVER` | SpotBugs | Set to `1` to ignore compilation failure. |
+| `SAST_GOSEC_CONFIG` | Gosec | Path to configuration for Gosec (optional). |
#### Custom environment variables
> [Introduced](https://gitlab.com/gitlab-org/gitlab/-/merge_requests/18193) in GitLab Ultimate 12.5.
In addition to the aforementioned SAST configuration variables,
-all [custom environment variables](../../../ci/variables/README.md#creating-a-custom-environment-variable) are propagated
+all [custom environment variables](../../../ci/variables/README.md#custom-environment-variables) are propagated
to the underlying SAST analyzer images if
[the SAST vendored template](#configuration) is used.
@@ -451,16 +432,16 @@ the report JSON unless stated otherwise. Presence of optional fields depends on
| `version` | Report syntax version used to generate this JSON. |
| `vulnerabilities` | Array of vulnerability objects. |
| `vulnerabilities[].id` | Unique identifier of the vulnerability. |
-| `vulnerabilities[].category` | Where this vulnerability belongs (SAST, Dependency Scanning etc.). For SAST, it will always be `sast`. |
-| `vulnerabilities[].name` | Name of the vulnerability, this must not include the occurrence's specific information. Optional. |
+| `vulnerabilities[].category` | Where this vulnerability belongs (such as SAST, Dependency Scanning). For SAST, it will always be `sast`. |
+| `vulnerabilities[].name` | Name of the vulnerability. Must not include the occurrence's specific information. Optional. |
| `vulnerabilities[].message` | A short text that describes the vulnerability, it may include the occurrence's specific information. Optional. |
| `vulnerabilities[].description` | A long text that describes the vulnerability. Optional. |
| `vulnerabilities[].cve` | (**DEPRECATED - use `vulnerabilities[].id` instead**) A fingerprint string value that represents a concrete occurrence of the vulnerability. It's used to determine whether two vulnerability occurrences are same or different. May not be 100% accurate. **This is NOT a [CVE](https://cve.mitre.org/)**. |
-| `vulnerabilities[].severity` | How much the vulnerability impacts the software. Possible values: `Undefined` (an analyzer has not provided this information), `Info`, `Unknown`, `Low`, `Medium`, `High`, `Critical`. |
-| `vulnerabilities[].confidence` | How reliable the vulnerability's assessment is. Possible values: `Undefined` (an analyzer has not provided this information), `Ignore`, `Unknown`, `Experimental`, `Low`, `Medium`, `High`, `Confirmed`. |
+| `vulnerabilities[].severity` | How much the vulnerability impacts the software. Possible values: `Info`, `Unknown`, `Low`, `Medium`, `High`, `Critical`. |
+| `vulnerabilities[].confidence` | How reliable the vulnerability's assessment is. Possible values: `Ignore`, `Unknown`, `Experimental`, `Low`, `Medium`, `High`, `Confirmed`. |
| `vulnerabilities[].solution` | Explanation of how to fix the vulnerability. Optional. |
| `vulnerabilities[].scanner` | A node that describes the analyzer used to find this vulnerability. |
-| `vulnerabilities[].scanner.id` | Id of the scanner as a snake_case string. |
+| `vulnerabilities[].scanner.id` | ID of the scanner as a snake_case string. |
| `vulnerabilities[].scanner.name` | Name of the scanner, for display purposes. |
| `vulnerabilities[].location` | A node that tells where the vulnerability is located. |
| `vulnerabilities[].location.file` | Path to the file where the vulnerability is located. Optional. |
@@ -468,29 +449,15 @@ the report JSON unless stated otherwise. Presence of optional fields depends on
| `vulnerabilities[].location.end_line` | The last line of the code affected by the vulnerability. Optional. |
| `vulnerabilities[].location.class` | If specified, provides the name of the class where the vulnerability is located. Optional. |
| `vulnerabilities[].location.method` | If specified, provides the name of the method where the vulnerability is located. Optional. |
-| `vulnerabilities[].identifiers` | An ordered array of references that identify a vulnerability on internal or external DBs. |
-| `vulnerabilities[].identifiers[].type` | Type of the identifier. Possible values: common identifier types (among `cve`, `cwe`, `osvdb`, and `usn`) or analyzer-dependent ones (e.g., `bandit_test_id` for [Bandit analyzer](https://wiki.openstack.org/wiki/Security/Projects/Bandit)). |
+| `vulnerabilities[].identifiers` | An ordered array of references that identify a vulnerability on internal or external databases. |
+| `vulnerabilities[].identifiers[].type` | Type of the identifier. Possible values: common identifier types (among `cve`, `cwe`, `osvdb`, and `usn`) or analyzer-dependent ones (like `bandit_test_id` for [Bandit analyzer](https://wiki.openstack.org/wiki/Security/Projects/Bandit)). |
| `vulnerabilities[].identifiers[].name` | Name of the identifier for display purposes. |
| `vulnerabilities[].identifiers[].value` | Value of the identifier for matching purposes. |
| `vulnerabilities[].identifiers[].url` | URL to identifier's documentation. Optional. |
## Secret detection
-GitLab is also able to detect secrets and credentials that have been unintentionally pushed to the
-repository (for example, an API key that allows write access to third-party deployment
-environments).
-
-This check is performed by a specific analyzer during the `sast` job. It runs regardless of the programming
-language of your app, and you don't need to change anything to your
-CI/CD configuration file to turn it on. Results are available in the SAST report.
-
-GitLab currently includes [Gitleaks](https://github.com/zricethezav/gitleaks) and [TruffleHog](https://github.com/dxa4481/truffleHog) checks.
-
-NOTE: **Note:**
-The secrets analyzer will ignore "Password in URL" vulnerabilities if the password begins
-with a dollar sign (`$`) as this likely indicates the password being used is an environment
-variable. For example, `https://username:$password@example.com/path/to/repo` will not be
-detected, whereas `https://username:password@example.com/path/to/repo` would be detected.
+Learn more about [Secret Detection](../secret_detection).
## Security Dashboard
@@ -503,7 +470,12 @@ vulnerabilities in your groups, projects and pipelines. Read more about the
Once a vulnerability is found, you can interact with it. Read more on how to
[interact with the vulnerabilities](../index.md#interacting-with-the-vulnerabilities).
-## Vulnerabilities database update
+## Vulnerabilities database
+
+Vulnerabilities contained within the vulnerability database can be searched
+and viewed at the [GitLab vulnerability advisory database](https://advisories.gitlab.com).
+
+### Vulnerabilities database update
For more information about the vulnerabilities database update, check the
[maintenance table](../index.md#maintenance-and-update-of-the-vulnerabilities-database).
@@ -512,29 +484,29 @@ For more information about the vulnerabilities database update, check the
For self-managed GitLab instances in an environment with limited, restricted, or intermittent access
to external resources through the internet, some adjustments are required for the SAST job to
-successfully run. For more information, see [Offline environments](../offline_deployments/index.md).
+run successfully. For more information, see [Offline environments](../offline_deployments/index.md).
### Requirements for offline SAST
To use SAST in an offline environment, you need:
-- [Disable Docker-In-Docker](#disabling-docker-in-docker-for-sast)
+- To keep Docker-In-Docker disabled (default).
- GitLab Runner with the [`docker` or `kubernetes` executor](#requirements).
- Docker Container Registry with locally available copies of SAST [analyzer](https://gitlab.com/gitlab-org/security-products/analyzers) images.
NOTE: **Note:**
GitLab Runner has a [default `pull policy` of `always`](https://docs.gitlab.com/runner/executors/docker.html#using-the-always-pull-policy),
-meaning the runner will try to pull Docker images from the GitLab container registry even if a local
+meaning the Runner tries to pull Docker images from the GitLab container registry even if a local
copy is available. GitLab Runner's [`pull_policy` can be set to `if-not-present`](https://docs.gitlab.com/runner/executors/docker.html#using-the-if-not-present-pull-policy)
in an offline environment if you prefer using only locally available Docker images. However, we
-recommend keeping the pull policy setting to `always` as it will better enable updated scanners to
-be utilized within your CI/CD pipelines.
+recommend keeping the pull policy setting to `always` if not in an offline environment, as this
+enables the use of updated scanners in your CI/CD pipelines.
### Make GitLab SAST analyzer images available inside your Docker registry
For SAST with all [supported languages and frameworks](#supported-languages-and-frameworks),
-import the following default SAST analyzer images from `registry.gitlab.com` to your local "offline"
-registry:
+import the following default SAST analyzer images from `registry.gitlab.com` into your
+[local Docker container registry](../../packages/container_registry/index.md):
```plaintext
registry.gitlab.com/gitlab-org/security-products/analyzers/bandit:2
@@ -557,7 +529,7 @@ registry.gitlab.com/gitlab-org/security-products/analyzers/tslint:2
The process for importing Docker images into a local offline Docker registry depends on
**your network security policy**. Please consult your IT staff to find an accepted and approved
process by which external resources can be imported or temporarily accessed. Note that these scanners are [updated periodically](../index.md#maintenance-and-update-of-the-vulnerabilities-database)
-with new definitions, so consider if you are able to make periodic updates yourself.
+with new definitions, so consider if you're able to make periodic updates yourself.
For details on saving and transporting Docker images as a file, see Docker's documentation on
[`docker save`](https://docs.docker.com/engine/reference/commandline/save/), [`docker load`](https://docs.docker.com/engine/reference/commandline/load/),
@@ -565,18 +537,15 @@ For details on saving and transporting Docker images as a file, see Docker's doc
### Set SAST CI job variables to use local SAST analyzers
-[Override SAST environment variables](#customizing-the-sast-settings) to use to your [local container registry](./analyzers.md#using-a-custom-docker-mirror)
-as the source for SAST analyzer images.
-
-For example, assuming a local Docker registry repository of `localhost:5000/analyzers`:
+Add the following configuration to your `.gitlab-ci.yml` file. You must replace
+`SAST_ANALYZER_IMAGE_PREFIX` to refer to your local Docker container registry:
```yaml
include:
- template: SAST.gitlab-ci.yml
variables:
- SAST_ANALYZER_IMAGE_PREFIX: "localhost:5000/analyzers"
- SAST_DISABLE_DIND: "true"
+ SECURE_ANALYZERS_PREFIX: "localhost:5000/analyzers"
```
The SAST job should now use local copies of the SAST analyzers to scan your code and generate
@@ -586,7 +555,7 @@ security reports without requiring internet access.
### Error response from daemon: error processing tar file: docker-tar: relocation error
-This error occurs when the Docker version used to run the SAST job is `19.03.0`.
-You are advised to update to Docker `19.03.1` or greater. Older versions are not
+This error occurs when the Docker version that runs the SAST job is `19.03.0`.
+Consider updating to Docker `19.03.1` or greater. Older versions are not
affected. Read more in
[this issue](https://gitlab.com/gitlab-org/gitlab/issues/13830#note_211354992 "Current SAST container fails").
diff --git a/doc/user/application_security/secret_detection/img/secret-detection-merge-request-ui.png b/doc/user/application_security/secret_detection/img/secret-detection-merge-request-ui.png
new file mode 100644
index 00000000000..17893610f10
--- /dev/null
+++ b/doc/user/application_security/secret_detection/img/secret-detection-merge-request-ui.png
Binary files differ
diff --git a/doc/user/application_security/secret_detection/index.md b/doc/user/application_security/secret_detection/index.md
new file mode 100644
index 00000000000..74dd3c89984
--- /dev/null
+++ b/doc/user/application_security/secret_detection/index.md
@@ -0,0 +1,67 @@
+---
+type: reference, howto
+---
+
+# Secret Detection **(ULTIMATE)**
+
+> [Introduced](https://about.gitlab.com/releases/2019/03/22/gitlab-11-9-released/#detect-secrets-and-credentials-in-the-repository) in [GitLab Ultimate](https://about.gitlab.com/pricing/) 11.9.
+
+## Overview
+
+A recurring problem when developing applications is that developers may unintentionally commit
+secrets and credentials to their remote repositories. If other people have access to the source,
+or if the project is public, the sensitive information is then exposed and can be leveraged by
+malicious users to gain access to resources like deployment environments.
+
+GitLab 11.9 includes a new check called Secret Detection. It scans the content of the repository
+to find API keys and other information that should not be there.
+
+GitLab displays identified secrets as part of the SAST reports visibly in a few places:
+
+- [Security Dashboard](../security_dashboard/)
+- Pipelines' **Security** tab
+- Report in the merge request widget
+
+![Secret Detection in merge request widget](img/secret-detection-merge-request-ui.png)
+
+## Use cases
+
+- Detecting accidental commit of secrets like keys, passwords, and API tokens.
+- Performing a single or recurring scan of the full history of your repository for secrets.
+
+## Configuration
+
+If you already have SAST enabled for your app, you don’t need to take any action to benefit from this
+new feature. It is also included in the Auto DevOps default configuration.
+
+Secret Detection is performed by a [specific analyzer](https://gitlab.com/gitlab-org/gitlab/-/blob/master/lib/gitlab/ci/templates/Security/SAST.gitlab-ci.yml#L180)
+during the `sast` job. It runs regardless of the programming
+language of your app, and you don't need to change your
+CI/CD configuration file to enable it. Results are available in the SAST report.
+
+The Secret Detection analyzer includes [Gitleaks](https://github.com/zricethezav/gitleaks) and [TruffleHog](https://github.com/dxa4481/truffleHog) checks.
+
+NOTE: **Note:**
+The Secret Detection analyzer will ignore "Password in URL" vulnerabilities if the password begins
+with a dollar sign (`$`) as this likely indicates the password being used is an environment
+variable. For example, `https://username:$password@example.com/path/to/repo` won't be
+detected, whereas `https://username:password@example.com/path/to/repo` would be detected.
+
+## Full History Secret Scan
+
+GitLab 12.11 introduced support for scanning the full history of a reposity. This new functionality
+is particularly useful when you are enabling Secret Detection in a repository for the first time and you
+want to perform a full secret scan. Running a secret scan on the full history can take a long time,
+especially for larger repositories with lengthy Git histories. We recommend not setting this variable
+as part of your normal job defintion.
+
+A new configuration variable ([`SAST_GITLEAKS_HISTORIC_SCAN`](../sast/#vulnerability-filters))
+can be set to change the behavior of the GitLab Secret Detection scan to run on the entire Git history of a repository.
+
+We have created a [short video walkthrough](https://youtu.be/wDtc_K00Y0A) showcasing how you can perform a full history secret scan.
+<div class="video-fallback">
+ See the video: <a href="https://www.youtube.com/watch?v=wDtc_K00Y0A">Walkthrough of historical secret scan</a>.
+</div>
+<figure class="video-container">
+ <iframe src="https://www.youtube.com/embed/wDtc_K00Y0A" frameborder="0" allowfullscreen="true"> </iframe>
+</figure>
diff --git a/doc/user/application_security/security_dashboard/img/group_security_dashboard_v12_6.png b/doc/user/application_security/security_dashboard/img/group_security_dashboard_v12_6.png
deleted file mode 100644
index c93a3ce8c35..00000000000
--- a/doc/user/application_security/security_dashboard/img/group_security_dashboard_v12_6.png
+++ /dev/null
Binary files differ
diff --git a/doc/user/application_security/security_dashboard/img/group_security_dashboard_v13_0.png b/doc/user/application_security/security_dashboard/img/group_security_dashboard_v13_0.png
new file mode 100644
index 00000000000..c788e2165ad
--- /dev/null
+++ b/doc/user/application_security/security_dashboard/img/group_security_dashboard_v13_0.png
Binary files differ
diff --git a/doc/user/application_security/security_dashboard/img/instance_security_dashboard_export_csv_v13_0.png b/doc/user/application_security/security_dashboard/img/instance_security_dashboard_export_csv_v13_0.png
new file mode 100644
index 00000000000..77e75551bd9
--- /dev/null
+++ b/doc/user/application_security/security_dashboard/img/instance_security_dashboard_export_csv_v13_0.png
Binary files differ
diff --git a/doc/user/application_security/security_dashboard/img/instance_security_dashboard_with_projects_v12_8.png b/doc/user/application_security/security_dashboard/img/instance_security_dashboard_with_projects_v12_8.png
deleted file mode 100644
index fd0548d0b34..00000000000
--- a/doc/user/application_security/security_dashboard/img/instance_security_dashboard_with_projects_v12_8.png
+++ /dev/null
Binary files differ
diff --git a/doc/user/application_security/security_dashboard/img/instance_security_dashboard_with_projects_v13_0.png b/doc/user/application_security/security_dashboard/img/instance_security_dashboard_with_projects_v13_0.png
new file mode 100644
index 00000000000..a500f186c2b
--- /dev/null
+++ b/doc/user/application_security/security_dashboard/img/instance_security_dashboard_with_projects_v13_0.png
Binary files differ
diff --git a/doc/user/application_security/security_dashboard/img/project_security_dashboard_export_csv_v12.10.png b/doc/user/application_security/security_dashboard/img/project_security_dashboard_export_csv_v12_10.png
index 07b41b471d4..07b41b471d4 100644
--- a/doc/user/application_security/security_dashboard/img/project_security_dashboard_export_csv_v12.10.png
+++ b/doc/user/application_security/security_dashboard/img/project_security_dashboard_export_csv_v12_10.png
Binary files differ
diff --git a/doc/user/application_security/security_dashboard/img/project_security_dashboard_v12_3.png b/doc/user/application_security/security_dashboard/img/project_security_dashboard_v12_3.png
deleted file mode 100644
index 51e80bdb50d..00000000000
--- a/doc/user/application_security/security_dashboard/img/project_security_dashboard_v12_3.png
+++ /dev/null
Binary files differ
diff --git a/doc/user/application_security/security_dashboard/img/project_security_dashboard_v13_0.png b/doc/user/application_security/security_dashboard/img/project_security_dashboard_v13_0.png
new file mode 100644
index 00000000000..bb88b7f371c
--- /dev/null
+++ b/doc/user/application_security/security_dashboard/img/project_security_dashboard_v13_0.png
Binary files differ
diff --git a/doc/user/application_security/security_dashboard/index.md b/doc/user/application_security/security_dashboard/index.md
index 42b28b7b9f2..2988b3642ef 100644
--- a/doc/user/application_security/security_dashboard/index.md
+++ b/doc/user/application_security/security_dashboard/index.md
@@ -30,7 +30,7 @@ To use the instance, group, project, or pipeline security dashboard:
1. At least one project inside a group must be configured with at least one of
the [supported reports](#supported-reports).
-1. The configured jobs must use the [new `reports` syntax](../../../ci/yaml/README.md#artifactsreports).
+1. The configured jobs must use the [new `reports` syntax](../../../ci/pipelines/job_artifacts.md#artifactsreports).
1. [GitLab Runner](https://docs.gitlab.com/runner/) 11.5 or newer must be used.
If you're using the shared Runners on GitLab.com, this is already the case.
@@ -44,15 +44,17 @@ Visit the page for any pipeline which has run any of the [supported reports](#su
![Pipeline Security Dashboard](img/pipeline_security_dashboard_v12_6.png)
+NOTE: **Note:**
+A pipeline consists of multiple jobs, including SAST and DAST scanning. If any job fails to finish for any reason, the security dashboard will not show SAST scanner output. For example, if the SAST job finishes but the DAST job fails, the security dashboard will not show SAST results. The analyzer will output an [exit code](../../../development/integrations/secure.md#exit-code) on failure.
+
## Project Security Dashboard
> [Introduced](https://gitlab.com/gitlab-org/gitlab/issues/6165) in [GitLab Ultimate](https://about.gitlab.com/pricing/) 11.1.
-At the project level, the Security Dashboard displays the latest security reports
-for your project from the last successful pipeline. Use it to find and fix vulnerabilities affecting the
-[default branch](../../project/repository/branches/index.md#default-branch).
+At the project level, the Security Dashboard displays the latest security reports for your project.
+Use it to find and fix vulnerabilities.
-![Project Security Dashboard](img/project_security_dashboard_v12_3.png)
+![Project Security Dashboard](img/project_security_dashboard_v13_0.png)
### Export vulnerabilities
@@ -64,7 +66,7 @@ NOTE: **Note:**
It may take several minutes for the download to start if your project consists
of thousands of vulnerabilities. Do not close the page until the download finishes.
-![CSV Export Button](img/project_security_dashboard_export_csv_v12.10.png)
+![CSV Export Button](img/project_security_dashboard_export_csv_v12_10.png)
## Group Security Dashboard
@@ -78,32 +80,27 @@ First, navigate to the Security Dashboard found under your group's
Once you're on the dashboard, at the top you should see a series of filters for:
+- Status
- Severity
-- Confidence
- Report type
-- Project
-
-To the right of the filters, you should see a **Hide dismissed** toggle button.
NOTE: **Note:**
-The dashboard only shows projects with [security reports](#supported-reports) enabled in a group
-according to the last successful projects' pipelines.
+The dashboard only shows projects with [security reports](#supported-reports) enabled in a group.
-![dashboard with action buttons and metrics](img/group_security_dashboard_v12_6.png)
+![Dashboard with action buttons and metrics](img/group_security_dashboard_v13_0.png)
-Selecting one or more filters will filter the results in this page. Disabling the **Hide dismissed**
-toggle button will let you also see vulnerabilities that have been dismissed.
+Selecting one or more filters will filter the results in this page.
The main section is a list of all the vulnerabilities in the group, sorted by severity.
In that list, you can see the severity of the vulnerability, its name, its
confidence (likelihood of the vulnerability to be a positive one), and the project
it's from.
-If you hover over a row, there will appear some actions you can take:
+If you hover over a row, the following actions appear:
-- "More info"
-- "Create issue"
-- "Dismiss vulnerability"
+- More info
+- Create issue
+- Dismiss vulnerability
Next to the list is a timeline chart that shows how many open
vulnerabilities your projects had at various points in time. You can filter among 30, 60, and
@@ -147,7 +144,23 @@ To add projects to the dashboard:
Once added, the dashboard will display the vulnerabilities found in your chosen
projects.
-![Instance Security Dashboard with projects](img/instance_security_dashboard_with_projects_v12_8.png)
+![Instance Security Dashboard with projects](img/instance_security_dashboard_with_projects_v13_0.png)
+
+### Export vulnerabilities
+
+> [Introduced](https://gitlab.com/gitlab-org/gitlab/-/issues/213014) in [GitLab Ultimate](https://about.gitlab.com/pricing/) 13.0.
+
+You can export all your vulnerabilities as CSV by clicking the **{upload}** **Export**
+button located at top right of the **Instance Security Dashboard**. After the report
+is built, the CSV report downloads to your local machine. The report contains all
+vulnerabilities for the projects defined in the **Instance Security Dashboard**,
+as filters don't apply to the export function.
+
+NOTE: **Note:**
+It may take several minutes for the download to start if your project contains
+thousands of vulnerabilities. Do not close the page until the download finishes.
+
+![CSV Export Button](img/instance_security_dashboard_export_csv_v13_0.png)
## Keeping the dashboards up to date
diff --git a/doc/user/application_security/threat_monitoring/index.md b/doc/user/application_security/threat_monitoring/index.md
index 482fceea680..7bd148edd15 100644
--- a/doc/user/application_security/threat_monitoring/index.md
+++ b/doc/user/application_security/threat_monitoring/index.md
@@ -21,7 +21,7 @@ The Web Application Firewall section provides metrics for the NGINX
Ingress controller and ModSecurity firewall. This section has the
following prerequisites:
-- Project has to have at least one [environment](../../../ci/environments.md).
+- Project has to have at least one [environment](../../../ci/environments/index.md).
- [Web Application Firewall](../../clusters/applications.md#web-application-firewall-modsecurity) has to be enabled.
- [Elastic Stack](../../clusters/applications.md#web-application-firewall-modsecurity) has to be installed.
@@ -38,7 +38,7 @@ about your Ingress traffic:
If a significant percentage of traffic is anomalous, you should
investigate it for potential threats by
-[examining the application logs](../../clusters/applications.md#web-application-firewall-modsecurity).
+[examining the Web Application Firewall logs](../../clusters/applications.md#web-application-firewall-modsecurity).
## Container Network Policy
@@ -48,7 +48,7 @@ The **Container Network Policy** section provides packet flow metrics for
your application's Kubernetes namespace. This section has the following
prerequisites:
-- Your project contains at least one [environment](../../../ci/environments.md)
+- Your project contains at least one [environment](../../../ci/environments/index.md)
- You've [installed Cilium](../../clusters/applications.md#install-cilium-using-gitlab-cicd)
- You've configured the [Prometheus service](../../project/integrations/prometheus.md#enabling-prometheus-integration)
diff --git a/doc/user/application_security/vulnerabilities/index.md b/doc/user/application_security/vulnerabilities/index.md
index 5cb4f16e0d8..b691a97fc32 100644
--- a/doc/user/application_security/vulnerabilities/index.md
+++ b/doc/user/application_security/vulnerabilities/index.md
@@ -4,11 +4,7 @@ type: reference, howto
# Standalone Vulnerability pages
-> [Introduced](https://gitlab.com/gitlab-org/gitlab/issues/13561) in [GitLab Ultimate](https://about.gitlab.com/pricing/) 12.10.
-
-CAUTION: **Warning:**
-This feature is currently [Alpha](https://about.gitlab.com/handbook/product/#alpha-beta-ga).
-You can begin using it, but it may receive important changes in the future.
+> [Introduced](https://gitlab.com/gitlab-org/gitlab/issues/13561) in [GitLab Ultimate](https://about.gitlab.com/pricing/) 13.0.
Each security vulnerability in the [Vulnerability List](../dependency_list/index.md) has its own standalone
page.
diff --git a/doc/user/clusters/applications.md b/doc/user/clusters/applications.md
index 47cbc0d4a1e..9ede9d9fdef 100644
--- a/doc/user/clusters/applications.md
+++ b/doc/user/clusters/applications.md
@@ -1,10 +1,16 @@
+---
+stage: Configure
+group: Configure
+info: To determine the technical writer assigned to the Stage/Group associated with this page, see https://about.gitlab.com/handbook/engineering/ux/technical-writing/#designated-technical-writers
+---
+
# GitLab Managed Apps
GitLab provides **GitLab Managed Apps**, a one-click install for various applications which can
be added directly to your configured cluster.
These applications are needed for [Review Apps](../../ci/review_apps/index.md)
-and [deployments](../../ci/environments.md) when using [Auto DevOps](../../topics/autodevops/index.md).
+and [deployments](../../ci/environments/index.md) when using [Auto DevOps](../../topics/autodevops/index.md).
You can install them after you
[create a cluster](../project/clusters/add_remove_clusters.md).
@@ -128,9 +134,9 @@ before deploying one.
NOTE: **Note:**
The [`runner/gitlab-runner`](https://gitlab.com/gitlab-org/charts/gitlab-runner)
-chart is used to install this application with a
-[`values.yaml`](https://gitlab.com/gitlab-org/gitlab/blob/master/vendor/runner/values.yaml)
-file. Customizing installation by modifying this file is not supported.
+chart is used to install this application, using
+[a preconfigured `values.yaml`](https://gitlab.com/gitlab-org/charts/gitlab-runner/-/blob/master/values.yaml)
+file. Customizing the installation by modifying this file is not supported.
### Ingress
@@ -314,6 +320,16 @@ To change your WAF's mode:
1. Under **Global default**, select your desired mode.
1. Click **Save changes**.
+##### WAF version updates
+
+Enabling, disabling, or changing the logging mode for **ModSecurity** is only allowed within same version of [Ingress](#ingress) due to limitations in [Helm](https://helm.sh/) which might be overcome in future releases.
+
+**ModSecurity** UI controls are disabled if the version deployed differs from the one available in GitLab, while actions at the [Ingress](#ingress) level, such as uninstalling, can still be performed:
+
+![WAF settings disabled](../../topics/web_application_firewall/img/guide_waf_ingress_disabled_settings_v12_10.png)
+
+Updating [Ingress](#ingress) to the most recent version enables you to take advantage of bug fixes, security fixes, and performance improvements. To update [Ingress application](#ingress), you must first uninstall it, and then re-install it as described in [Install ModSecurity](../../topics/web_application_firewall/quick_start_guide.md).
+
##### Viewing Web Application Firewall traffic
> [Introduced](https://gitlab.com/gitlab-org/gitlab/issues/14707) in [GitLab Ultimate](https://about.gitlab.com/pricing/) 12.9.
@@ -356,7 +372,7 @@ will also see ready-to-use DevOps Runbooks built with Nurtch's [Rubix library](h
More information on
creating executable runbooks can be found in [our Runbooks
-documentation](../project/clusters/runbooks/index.md#executable-runbooks). Note that
+documentation](../project/clusters/runbooks/index.md#configure-an-executable-runbook-with-gitlab). Note that
Ingress must be installed and have an IP address assigned before
JupyterHub can be installed.
@@ -487,18 +503,25 @@ and you will have access to more advanced querying capabilities.
Log data is automatically deleted after 30 days using [Curator](https://www.elastic.co/guide/en/elasticsearch/client/curator/5.5/about.html).
-To enable log shipping, install Elastic Stack into the cluster with the **Install** button.
+To enable log shipping:
+
+1. Ensure your cluster contains at least 3 nodes of instance types larger than
+ `f1-micro`, `g1-small`, or `n1-standard-1`.
+1. Navigate to **{cloud-gear}** **Operations > Kubernetes**.
+1. In **Kubernetes Cluster**, select a cluster.
+1. In the **Applications** section, find **Elastic Stack** and click **Install**.
NOTE: **Note:**
-The [`stable/elastic-stack`](https://github.com/helm/charts/tree/master/stable/elastic-stack)
+The [`gitlab/elastic-stack`](https://gitlab.com/gitlab-org/charts/elastic-stack)
chart is used to install this application with a
[`values.yaml`](https://gitlab.com/gitlab-org/gitlab/blob/master/vendor/elastic_stack/values.yaml)
file.
NOTE: **Note:**
-The chart will deploy 5 Elasticsearch nodes: 2 masters, 2 data and 1 client node,
-with resource requests totalling 0.125 CPU and 4.5GB RAM. Each data node requests 1.5GB of memory,
-which makes it incompatible with clusters of `f1-micro` and `g1-small` instance types.
+The chart deploys 3 identical Elasticsearch pods which can't be colocated, and each
+require 1 CPU and 2 GB of RAM, making them incompatible with clusters containing
+fewer than 3 nodes or consisting of `f1-micro`, `g1-small`, `n1-standard-1`, or
+`*-highcpu-2` instance types.
NOTE: **Note:**
The Elastic Stack cluster application is intended as a log aggregation solution and is not related to our
@@ -517,25 +540,25 @@ Save the following to `kibana.yml`:
elasticsearch:
enabled: false
-logstash:
+filebeat:
enabled: false
kibana:
enabled: true
- env:
- ELASTICSEARCH_HOSTS: http://elastic-stack-elasticsearch-client.gitlab-managed-apps.svc.cluster.local:9200
+ elasticsearchHosts: http://elastic-stack-elasticsearch-master.gitlab-managed-apps.svc.cluster.local:9200
```
Then install it on your cluster:
```shell
-helm install --name kibana stable/elastic-stack --values kibana.yml
+helm repo add gitlab https://charts.gitlab.io
+helm install --name kibana gitlab/elastic-stack --values kibana.yml
```
-To access kibana, forward the port to your local machine:
+To access Kibana, forward the port to your local machine:
```shell
-kubectl port-forward svc/kibana 5601:443
+kubectl port-forward svc/kibana-kibana 5601:5601
```
Then, you can visit Kibana at `http://localhost:5601`.
@@ -556,11 +579,10 @@ To enable Fluentd:
1. Provide the host domain name or URL in **SIEM Hostname**.
1. Provide the host port number in **SIEM Port**.
1. Select a **SIEM Protocol**.
-1. Check **Send ModSecurity Logs**. If you do not select this checkbox, the **Install**
- button is disabled.
+1. Select at least one of the available logs (such as WAF or Cilium).
1. Click **Save changes**.
-![Fluentd input fields](img/fluentd_v12_10.png)
+![Fluentd input fields](img/fluentd_v13_0.png)
### Future apps
@@ -777,7 +799,7 @@ In order for GitLab Runner to function, you **must** specify the following:
- `runnerRegistrationToken` - The registration token for adding new Runners to GitLab. This must be
[retrieved from your GitLab instance](../../ci/runners/README.md).
-These values can be specifed using [CI variables](../../ci/variables/README.md):
+These values can be specified using [CI variables](../../ci/variables/README.md):
- `GITLAB_RUNNER_GITLAB_URL` will be used for `gitlabUrl`.
- `GITLAB_RUNNER_REGISTRATION_TOKEN` will be used for `runnerRegistrationToken`
@@ -792,10 +814,12 @@ available configuration options.
> [Introduced](https://gitlab.com/gitlab-org/cluster-integration/cluster-applications/-/merge_requests/22) in GitLab 12.8.
-[Cilium](https://cilium.io/) is a networking plugin for Kubernetes
-that you can use to implement support for
-[NetworkPolicy](https://kubernetes.io/docs/concepts/services-networking/network-policies/)
-resources. For more information on [Network Policies](../../topics/autodevops/stages.md#network-policy), see the documentation.
+[Cilium](https://cilium.io/) is a networking plugin for Kubernetes that you can use to implement
+support for [NetworkPolicy](https://kubernetes.io/docs/concepts/services-networking/network-policies/)
+resources. For more information, see [Network Policies](../../topics/autodevops/stages.md#network-policy).
+
+<i class="fa fa-youtube-play youtube" aria-hidden="true"></i>
+For an overview, see the [Container Network Security Demo for GitLab 12.8](https://www.youtube.com/watch?v=pgUEdhdhoUI).
Enable Cilium in the `.gitlab/managed-apps/config.yaml` file to install it:
@@ -822,7 +846,8 @@ management project. Refer to the
for the available configuration options.
CAUTION: **Caution:**
-Installation and removal of the Cilium [requires restart](https://cilium.readthedocs.io/en/stable/gettingstarted/k8s-install-gke/#restart-remaining-pods)
+Installation and removal of the Cilium requires a **manual**
+[restart](https://cilium.readthedocs.io/en/stable/gettingstarted/k8s-install-gke/#restart-remaining-pods)
of all affected pods in all namespaces to ensure that they are
[managed](https://cilium.readthedocs.io/en/stable/troubleshooting/#ensure-pod-is-managed-by-cilium)
by the correct networking plugin.
@@ -908,15 +933,15 @@ vault:
installed: true
```
-By default you will get a basic Vault setup with no high availability nor any scalable
-storage backend. This is enough for simple testing and small scale deployments, though has limits
+By default you will get a basic Vault setup with no scalable
+storage backend. This is enough for simple testing and small-scale deployments, though has limits
to how much it can scale, and as it is a single instance deployment, you will experience downtime
when upgrading the Vault application.
To optimally use Vault in a production environment, it's ideal to have a good understanding
of the internals of Vault and how to configure it. This can be done by reading the
[the Vault documentation](https://www.vaultproject.io/docs/internals/) as well as
-the Vault Helm chart [values.yaml file](https://github.com/hashicorp/vault-helm/blob/v0.3.3/values.yaml).
+the Vault Helm chart [`values.yaml` file](https://github.com/hashicorp/vault-helm/blob/v0.3.3/values.yaml).
At a minimum you will likely set up:
@@ -1009,11 +1034,11 @@ In addition, the following variables must be specified using [CI variables](../.
| CI Variable | Description |
|:---------------------------------------|:--------------------------------------------------------------------------------------------------------------------------------------------------------------------|
-| `JUPYTERHUB_PROXY_SECRET_TOKEN` | Sets [`proxy.secretToken`](https://zero-to-jupyterhub.readthedocs.io/en/stable/reference.html#proxy-secrettoken). Generate using `openssl rand -hex 32`. |
-| `JUPYTERHUB_COOKIE_SECRET` | Sets [`hub.cookieSecret`](https://zero-to-jupyterhub.readthedocs.io/en/stable/reference.html#hub-cookiesecret). Generate using `openssl rand -hex 32`. |
+| `JUPYTERHUB_PROXY_SECRET_TOKEN` | Secure string used for signing communications from the hub. See[`proxy.secretToken`](https://zero-to-jupyterhub.readthedocs.io/en/stable/reference/reference.html#proxy-secrettoken). |
+| `JUPYTERHUB_COOKIE_SECRET` | Secure string used for signing secure cookies. See [`hub.cookieSecret`](https://zero-to-jupyterhub.readthedocs.io/en/stable/reference/reference.html#hub-cookiesecret). |
| `JUPYTERHUB_HOST` | Hostname used for the installation. For example, `jupyter.gitlab.example.com`. |
| `JUPYTERHUB_GITLAB_HOST` | Hostname of the GitLab instance used for authentication. For example, `gitlab.example.com`. |
-| `JUPYTERHUB_AUTH_CRYPTO_KEY` | Sets [`auth.state.cryptoKey`](https://zero-to-jupyterhub.readthedocs.io/en/stable/reference.html#auth-state-cryptokey). Generate using `openssl rand -hex 32`. |
+| `JUPYTERHUB_AUTH_CRYPTO_KEY` | A 32-byte encryption key used to set [`auth.state.cryptoKey`](https://zero-to-jupyterhub.readthedocs.io/en/stable/reference/reference.html#auth-state-cryptokey). |
| `JUPYTERHUB_AUTH_GITLAB_CLIENT_ID` | "Application ID" for the OAuth Application. |
| `JUPYTERHUB_AUTH_GITLAB_CLIENT_SECRET` | "Secret" for the OAuth Application. |
@@ -1042,12 +1067,12 @@ elasticStack:
Elastic Stack is installed into the `gitlab-managed-apps` namespace of your cluster.
-You can check the default [values.yaml](https://gitlab.com/gitlab-org/gitlab/-/blob/master/vendor/elastic_stack/values.yaml) we set for this chart.
+You can check the default [`values.yaml`](https://gitlab.com/gitlab-org/gitlab/-/blob/master/vendor/elastic_stack/values.yaml) we set for this chart.
You can customize the installation of Elastic Stack by defining
`.gitlab/managed-apps/elastic-stack/values.yaml` file in your cluster
management project. Refer to the
-[chart](https://github.com/helm/charts/blob/master/stable/elastic-stack/values.yaml) for the
+[chart](https://gitlab.com/gitlab-org/charts/elastic-stack) for the
available configuration options.
NOTE: **Note:**
@@ -1070,7 +1095,7 @@ Crossplane:
Crossplane is installed into the `gitlab-managed-apps` namespace of your cluster.
You can check the default
-[values.yaml](https://github.com/crossplane/crossplane/blob/master/cluster/charts/crossplane/values.yaml.tmpl)
+[`values.yaml`](https://github.com/crossplane/crossplane/blob/master/cluster/charts/crossplane/values.yaml.tmpl)
we set for this chart.
You can customize the installation of Crossplane by defining
@@ -1090,7 +1115,7 @@ Fluentd:
installed: true
```
-You can also review the default values set for this chart in the [values.yaml](https://github.com/helm/charts/blob/master/stable/fluentd/values.yaml) file.
+You can also review the default values set for this chart in the [`values.yaml`](https://github.com/helm/charts/blob/master/stable/fluentd/values.yaml) file.
You can customize the installation of Fluentd by defining
`.gitlab/managed-apps/fluentd/values.yaml` file in your cluster management
@@ -1207,7 +1232,7 @@ epic](https://gitlab.com/groups/gitlab-org/-/epics/1201).
Applications can fail with the following error:
-```text
+```plaintext
Error: remote error: tls: bad certificate
```
diff --git a/doc/user/clusters/crossplane.md b/doc/user/clusters/crossplane.md
index 4e2ae87ecb9..a9a5f768ec8 100644
--- a/doc/user/clusters/crossplane.md
+++ b/doc/user/clusters/crossplane.md
@@ -1,3 +1,9 @@
+---
+stage: Configure
+group: Configure
+info: To determine the technical writer assigned to the Stage/Group associated with this page, see https://about.gitlab.com/handbook/engineering/ux/technical-writing/#designated-technical-writers
+---
+
# Crossplane configuration
Once Crossplane [is installed](applications.md#crossplane), it must be configured for
@@ -161,7 +167,7 @@ metadata:
specTemplate:
writeConnectionSecretsToNamespace: gitlab-managed-apps
forProvider:
- databaseVersion: POSTGRES_9_6
+ databaseVersion: POSTGRES_11_7
region: $REGION
settings:
tier: db-custom-1-3840
@@ -183,7 +189,7 @@ metadata:
specTemplate:
writeConnectionSecretsToNamespace: gitlab-managed-apps
forProvider:
- databaseVersion: POSTGRES_9_6
+ databaseVersion: POSTGRES_11_7
region: $REGION
settings:
tier: db-custom-1-3840
diff --git a/doc/user/clusters/environments.md b/doc/user/clusters/environments.md
index f83be85726a..a2adf238dda 100644
--- a/doc/user/clusters/environments.md
+++ b/doc/user/clusters/environments.md
@@ -1,9 +1,15 @@
+---
+stage: Configure
+group: Configure
+info: To determine the technical writer assigned to the Stage/Group associated with this page, see https://about.gitlab.com/handbook/engineering/ux/technical-writing/#designated-technical-writers
+---
+
# Cluster Environments **(PREMIUM)**
> - [Introduced](https://gitlab.com/gitlab-org/gitlab/issues/13392) for group-level clusters in [GitLab Premium](https://about.gitlab.com/pricing/) 12.3.
> - [Introduced](https://gitlab.com/gitlab-org/gitlab/issues/14809) for instance-level clusters in [GitLab Premium](https://about.gitlab.com/pricing/) 12.4.
-Cluster environments provide a consolidated view of which CI [environments](../../ci/environments.md) are
+Cluster environments provide a consolidated view of which CI [environments](../../ci/environments/index.md) are
deployed to the Kubernetes cluster and it:
- Shows the project and the relevant environment related to the deployment.
diff --git a/doc/user/clusters/img/fluentd_v12_10.png b/doc/user/clusters/img/fluentd_v12_10.png
deleted file mode 100644
index e8c5c832020..00000000000
--- a/doc/user/clusters/img/fluentd_v12_10.png
+++ /dev/null
Binary files differ
diff --git a/doc/user/clusters/img/fluentd_v13_0.png b/doc/user/clusters/img/fluentd_v13_0.png
new file mode 100644
index 00000000000..edc73285238
--- /dev/null
+++ b/doc/user/clusters/img/fluentd_v13_0.png
Binary files differ
diff --git a/doc/user/clusters/management_project.md b/doc/user/clusters/management_project.md
index 2b8ed83bdb2..03b4dc45015 100644
--- a/doc/user/clusters/management_project.md
+++ b/doc/user/clusters/management_project.md
@@ -1,3 +1,9 @@
+---
+stage: Configure
+group: Configure
+info: To determine the technical writer assigned to the Stage/Group associated with this page, see https://about.gitlab.com/handbook/engineering/ux/technical-writing/#designated-technical-writers
+---
+
# Cluster management project (alpha)
CAUTION: **Warning:**
diff --git a/doc/user/compliance/license_compliance/img/license_compliance.png b/doc/user/compliance/license_compliance/img/license_compliance.png
deleted file mode 100644
index cdce6b5fe38..00000000000
--- a/doc/user/compliance/license_compliance/img/license_compliance.png
+++ /dev/null
Binary files differ
diff --git a/doc/user/compliance/license_compliance/img/license_compliance_add_license_v12_3.png b/doc/user/compliance/license_compliance/img/license_compliance_add_license_v12_3.png
deleted file mode 100644
index ea4db16284c..00000000000
--- a/doc/user/compliance/license_compliance/img/license_compliance_add_license_v12_3.png
+++ /dev/null
Binary files differ
diff --git a/doc/user/compliance/license_compliance/img/license_compliance_add_license_v13_0.png b/doc/user/compliance/license_compliance/img/license_compliance_add_license_v13_0.png
new file mode 100644
index 00000000000..992c08edcd3
--- /dev/null
+++ b/doc/user/compliance/license_compliance/img/license_compliance_add_license_v13_0.png
Binary files differ
diff --git a/doc/user/compliance/license_compliance/img/license_compliance_decision.png b/doc/user/compliance/license_compliance/img/license_compliance_decision.png
deleted file mode 100644
index fbf90bec7fd..00000000000
--- a/doc/user/compliance/license_compliance/img/license_compliance_decision.png
+++ /dev/null
Binary files differ
diff --git a/doc/user/compliance/license_compliance/img/license_compliance_decision_v13_0.png b/doc/user/compliance/license_compliance/img/license_compliance_decision_v13_0.png
new file mode 100644
index 00000000000..d6c6142c0e7
--- /dev/null
+++ b/doc/user/compliance/license_compliance/img/license_compliance_decision_v13_0.png
Binary files differ
diff --git a/doc/user/compliance/license_compliance/img/license_compliance_pipeline_tab_v12_3.png b/doc/user/compliance/license_compliance/img/license_compliance_pipeline_tab_v12_3.png
deleted file mode 100644
index fd519d63b3e..00000000000
--- a/doc/user/compliance/license_compliance/img/license_compliance_pipeline_tab_v12_3.png
+++ /dev/null
Binary files differ
diff --git a/doc/user/compliance/license_compliance/img/license_compliance_pipeline_tab_v13_0.png b/doc/user/compliance/license_compliance/img/license_compliance_pipeline_tab_v13_0.png
new file mode 100644
index 00000000000..9ae59e2b96b
--- /dev/null
+++ b/doc/user/compliance/license_compliance/img/license_compliance_pipeline_tab_v13_0.png
Binary files differ
diff --git a/doc/user/compliance/license_compliance/img/license_compliance_search_v12_3.png b/doc/user/compliance/license_compliance/img/license_compliance_search_v12_3.png
deleted file mode 100644
index 4a7cff2e85c..00000000000
--- a/doc/user/compliance/license_compliance/img/license_compliance_search_v12_3.png
+++ /dev/null
Binary files differ
diff --git a/doc/user/compliance/license_compliance/img/license_compliance_search_v13_0.png b/doc/user/compliance/license_compliance/img/license_compliance_search_v13_0.png
new file mode 100644
index 00000000000..8ee55003768
--- /dev/null
+++ b/doc/user/compliance/license_compliance/img/license_compliance_search_v13_0.png
Binary files differ
diff --git a/doc/user/compliance/license_compliance/img/license_compliance_settings_v12_3.png b/doc/user/compliance/license_compliance/img/license_compliance_settings_v12_3.png
deleted file mode 100644
index 72d0888a9dc..00000000000
--- a/doc/user/compliance/license_compliance/img/license_compliance_settings_v12_3.png
+++ /dev/null
Binary files differ
diff --git a/doc/user/compliance/license_compliance/img/license_compliance_settings_v13_0.png b/doc/user/compliance/license_compliance/img/license_compliance_settings_v13_0.png
new file mode 100644
index 00000000000..52b26abd9c5
--- /dev/null
+++ b/doc/user/compliance/license_compliance/img/license_compliance_settings_v13_0.png
Binary files differ
diff --git a/doc/user/compliance/license_compliance/img/license_compliance_v13_0.png b/doc/user/compliance/license_compliance/img/license_compliance_v13_0.png
new file mode 100644
index 00000000000..dc227bf05ef
--- /dev/null
+++ b/doc/user/compliance/license_compliance/img/license_compliance_v13_0.png
Binary files differ
diff --git a/doc/user/compliance/license_compliance/img/license_list_v12_6.png b/doc/user/compliance/license_compliance/img/license_list_v12_6.png
deleted file mode 100644
index 8f2b510be0d..00000000000
--- a/doc/user/compliance/license_compliance/img/license_list_v12_6.png
+++ /dev/null
Binary files differ
diff --git a/doc/user/compliance/license_compliance/img/license_list_v13_0.png b/doc/user/compliance/license_compliance/img/license_list_v13_0.png
new file mode 100644
index 00000000000..3964c837c6a
--- /dev/null
+++ b/doc/user/compliance/license_compliance/img/license_list_v13_0.png
Binary files differ
diff --git a/doc/user/compliance/license_compliance/img/policies_maintainer_add_v12_9.png b/doc/user/compliance/license_compliance/img/policies_maintainer_add_v12_9.png
deleted file mode 100644
index ad5a49eebe5..00000000000
--- a/doc/user/compliance/license_compliance/img/policies_maintainer_add_v12_9.png
+++ /dev/null
Binary files differ
diff --git a/doc/user/compliance/license_compliance/img/policies_maintainer_add_v13_0.png b/doc/user/compliance/license_compliance/img/policies_maintainer_add_v13_0.png
new file mode 100644
index 00000000000..8070e2cb1a5
--- /dev/null
+++ b/doc/user/compliance/license_compliance/img/policies_maintainer_add_v13_0.png
Binary files differ
diff --git a/doc/user/compliance/license_compliance/img/policies_maintainer_edit_v12_9.png b/doc/user/compliance/license_compliance/img/policies_maintainer_edit_v12_9.png
deleted file mode 100644
index 4f2380a0bf6..00000000000
--- a/doc/user/compliance/license_compliance/img/policies_maintainer_edit_v12_9.png
+++ /dev/null
Binary files differ
diff --git a/doc/user/compliance/license_compliance/img/policies_maintainer_edit_v13_0.png b/doc/user/compliance/license_compliance/img/policies_maintainer_edit_v13_0.png
new file mode 100644
index 00000000000..741d1237751
--- /dev/null
+++ b/doc/user/compliance/license_compliance/img/policies_maintainer_edit_v13_0.png
Binary files differ
diff --git a/doc/user/compliance/license_compliance/img/policies_v12_9.png b/doc/user/compliance/license_compliance/img/policies_v12_9.png
deleted file mode 100644
index b3bca716ae5..00000000000
--- a/doc/user/compliance/license_compliance/img/policies_v12_9.png
+++ /dev/null
Binary files differ
diff --git a/doc/user/compliance/license_compliance/img/policies_v13_0.png b/doc/user/compliance/license_compliance/img/policies_v13_0.png
new file mode 100644
index 00000000000..4712d2b7aba
--- /dev/null
+++ b/doc/user/compliance/license_compliance/img/policies_v13_0.png
Binary files differ
diff --git a/doc/user/compliance/license_compliance/index.md b/doc/user/compliance/license_compliance/index.md
index 2e771a17163..cbabed00283 100644
--- a/doc/user/compliance/license_compliance/index.md
+++ b/doc/user/compliance/license_compliance/index.md
@@ -30,12 +30,20 @@ will be displayed in the merge request area. That is the case when you add the
Consecutive merge requests will have something to compare to and the license
compliance report will be shown properly.
-![License Compliance Widget](img/license_compliance.png)
+![License Compliance Widget](img/license_compliance_v13_0.png)
If you are a project or group Maintainer, you can click on a license to be given
the choice to allow it or deny it.
-![License approval decision](img/license_compliance_decision.png)
+![License approval decision](img/license_compliance_decision_v13_0.png)
+
+When GitLab detects a **Denied** license, you can view it in the [license list](#license-list).
+
+![License List](img/license_list_v13_0.png)
+
+You can view and modify existing policies from the [policies](#policies) tab.
+
+![Edit Policy](img/policies_maintainer_edit_v13_0.png)
## Use cases
@@ -49,19 +57,30 @@ The following languages and package managers are supported.
| Language | Package managers | Scan Tool |
|------------|-------------------------------------------------------------------|----------------------------------------------------------|
-| JavaScript | [Bower](https://bower.io/), [npm](https://www.npmjs.com/), [yarn](https://yarnpkg.com/) ([experimental support](https://github.com/pivotal/LicenseFinder#experimental-project-types)) |[License Finder](https://github.com/pivotal/LicenseFinder)|
-| Go | [Godep](https://github.com/tools/godep), go get ([experimental support](https://github.com/pivotal/LicenseFinder#experimental-project-types)), gvt ([experimental support](https://github.com/pivotal/LicenseFinder#experimental-project-types)), glide ([experimental support](https://github.com/pivotal/LicenseFinder#experimental-project-types)), dep ([experimental support](https://github.com/pivotal/LicenseFinder#experimental-project-types)), trash ([experimental support](https://github.com/pivotal/LicenseFinder#experimental-project-types)) and govendor ([experimental support](https://github.com/pivotal/LicenseFinder#experimental-project-types)), [go mod](https://github.com/golang/go/wiki/Modules) ([experimental support](https://github.com/pivotal/LicenseFinder#experimental-project-types)) |[License Finder](https://github.com/pivotal/LicenseFinder)|
+| JavaScript | [Bower](https://bower.io/), [npm](https://www.npmjs.com/) |[License Finder](https://github.com/pivotal/LicenseFinder)|
+| Go | [Godep](https://github.com/tools/godep), [go mod](https://github.com/golang/go/wiki/Modules) |[License Finder](https://github.com/pivotal/LicenseFinder)|
| Java | [Gradle](https://gradle.org/), [Maven](https://maven.apache.org/) |[License Finder](https://github.com/pivotal/LicenseFinder)|
-| .NET | [Nuget](https://www.nuget.org/) (.NET Framework is supported via the [mono project](https://www.mono-project.com/). Windows specific dependencies are not supported at this time.) |[License Finder](https://github.com/pivotal/LicenseFinder)|
+| .NET | [Nuget](https://www.nuget.org/) (.NET Framework is supported via the [mono project](https://www.mono-project.com/). Windows specific dependencies are not supported at this time.) |[License Finder](https://github.com/pivotal/LicenseFinder)|
| Python | [pip](https://pip.pypa.io/en/stable/) (Python is supported through [requirements.txt](https://pip.readthedocs.io/en/1.1/requirements.html) and [Pipfile.lock](https://github.com/pypa/pipfile#pipfilelock).) |[License Finder](https://github.com/pivotal/LicenseFinder)|
-| Ruby | [gem](https://rubygems.org/) |[License Finder](https://github.com/pivotal/LicenseFinder)|
-| Erlang | [rebar](https://www.rebar3.org/) ([experimental support](https://github.com/pivotal/LicenseFinder#experimental-project-types))|[License Finder](https://github.com/pivotal/LicenseFinder)|
-| Objective-C, Swift | [Carthage](https://github.com/Carthage/Carthage) , [CocoaPods v0.39 and below](https://cocoapods.org/) ([experimental support](https://github.com/pivotal/LicenseFinder#experimental-project-types)) |[License Finder](https://github.com/pivotal/LicenseFinder)|
-| Elixir | [mix](https://elixir-lang.org/getting-started/mix-otp/introduction-to-mix.html) ([experimental support](https://github.com/pivotal/LicenseFinder#experimental-project-types)) |[License Finder](https://github.com/pivotal/LicenseFinder)|
-| C++/C | [conan](https://conan.io/) ([experimental support](https://github.com/pivotal/LicenseFinder#experimental-project-types))|[License Finder](https://github.com/pivotal/LicenseFinder)|
-| Scala | [sbt](https://www.scala-sbt.org/) ([experimental support](https://github.com/pivotal/LicenseFinder#experimental-project-types))|[License Finder](https://github.com/pivotal/LicenseFinder)|
-| Rust | [cargo](https://crates.io/) ([experimental support](https://github.com/pivotal/LicenseFinder#experimental-project-types))|[License Finder](https://github.com/pivotal/LicenseFinder)|
-| PHP | [composer](https://getcomposer.org/) ([experimental support](https://github.com/pivotal/LicenseFinder#experimental-project-types))|[License Finder](https://github.com/pivotal/LicenseFinder)|
+| Ruby | [gem](https://rubygems.org/) |[License Finder](https://github.com/pivotal/LicenseFinder)|
+| Objective-C, Swift | [Carthage](https://github.com/Carthage/Carthage) |[License Finder](https://github.com/pivotal/LicenseFinder)|
+
+### Experimental support
+
+The following languages and package managers are [supported experimentally](https://github.com/pivotal/LicenseFinder#experimental-project-types),
+which means that the reported licenses might be incomplete or inaccurate.
+
+| Language | Package managers | Scan Tool |
+|------------|-------------------------------------------------------------------|----------------------------------------------------------|
+| JavaScript | [yarn](https://yarnpkg.com/)|[License Finder](https://github.com/pivotal/LicenseFinder)|
+| Go | go get, gvt, glide, dep, trash, govendor |[License Finder](https://github.com/pivotal/LicenseFinder)|
+| Erlang | [rebar](https://www.rebar3.org/) |[License Finder](https://github.com/pivotal/LicenseFinder)|
+| Objective-C, Swift | [CocoaPods](https://cocoapods.org/) v0.39 and below |[License Finder](https://github.com/pivotal/LicenseFinder)|
+| Elixir | [mix](https://elixir-lang.org/getting-started/mix-otp/introduction-to-mix.html) |[License Finder](https://github.com/pivotal/LicenseFinder)|
+| C++/C | [conan](https://conan.io/) |[License Finder](https://github.com/pivotal/LicenseFinder)|
+| Scala | [sbt](https://www.scala-sbt.org/) |[License Finder](https://github.com/pivotal/LicenseFinder)|
+| Rust | [cargo](https://crates.io/) |[License Finder](https://github.com/pivotal/LicenseFinder)|
+| PHP | [composer](https://getcomposer.org/) |[License Finder](https://github.com/pivotal/LicenseFinder)|
## Requirements
@@ -81,7 +100,7 @@ For GitLab versions earlier than 11.9, you can copy and use the job as defined
that template.
NOTE: **Note:**
-In GitLab 13.0, the `License-Management.gitlab-ci.yml` template is scheduled to be removed.
+GitLab 13.0 removes the `License-Management.gitlab-ci.yml` template.
Use `License-Scanning.gitlab-ci.yml` instead.
Add the following to your `.gitlab-ci.yml` file:
@@ -96,12 +115,12 @@ and scan your dependencies to find their licenses.
NOTE: **Note:**
Before GitLab 12.8, the `license_scanning` job was named `license_management`.
-In GitLab 13.0, the `license_management` job is scheduled to be removed completely,
+GitLab 13.0 removes the `license_management` job,
so you're advised to migrate to the `license_scanning` job and used the new
`License-Scanning.gitlab-ci.yml` template.
The results will be saved as a
-[License Compliance report artifact](../../../ci/yaml/README.md#artifactsreportslicense_scanning-ultimate)
+[License Compliance report artifact](../../../ci/pipelines/job_artifacts.md#artifactsreportslicense_scanning-ultimate)
that you can later download and analyze. Due to implementation limitations, we
always take the latest License Compliance artifact available. Behind the scenes, the
[GitLab License Compliance Docker image](https://gitlab.com/gitlab-org/security-products/license-management)
@@ -114,15 +133,17 @@ The License Compliance settings can be changed through [environment variables](#
License Compliance can be configured using environment variables.
-| Environment variable | Required | Description |
-|-----------------------|----------|-------------|
-| `MAVEN_CLI_OPTS` | no | Additional arguments for the mvn executable. If not supplied, defaults to `-DskipTests`. |
-| `LICENSE_FINDER_CLI_OPTS` | no | Additional arguments for the `license_finder` executable. For example, if your project has both Golang and Ruby code stored in different directories and you want to only scan the Ruby code, you can update your `.gitlab-ci-yml` template to specify which project directories to scan, like `LICENSE_FINDER_CLI_OPTS: '--debug --aggregate-paths=. ruby'`. |
-| `LM_JAVA_VERSION` | no | Version of Java. If set to `11`, Maven and Gradle use Java 11 instead of Java 8. |
-| `LM_PYTHON_VERSION` | no | Version of Python. If set to `3`, dependencies are installed using Python 3 instead of Python 2.7. |
-| `SETUP_CMD` | no | Custom setup for the dependency installation. (experimental) |
-| `PIP_INDEX_URL` | no | Base URL of Python Package Index (default: `https://pypi.org/simple/`). |
-| `ADDITIONAL_CA_CERT_BUNDLE` | no | Bundle of trusted CA certificates (currently supported in Python projects). |
+| Environment variable | Required | Description |
+|-----------------------------|----------|-------------|
+| `SECURE_ANALYZERS_PREFIX` | no | Set the Docker registry base address to download the analyzer from. |
+| `ADDITIONAL_CA_CERT_BUNDLE` | no | Bundle of trusted CA certificates (currently supported in Pip, Pipenv, Maven, Gradle, Yarn, and NPM projects). |
+| `GRADLE_CLI_OPTS` | no | Additional arguments for the gradle executable. If not supplied, defaults to `--exclude-task=test`. |
+| `LICENSE_FINDER_CLI_OPTS` | no | Additional arguments for the `license_finder` executable. For example, if your project has both Golang and Ruby code stored in different directories and you want to only scan the Ruby code, you can update your `.gitlab-ci-yml` template to specify which project directories to scan, like `LICENSE_FINDER_CLI_OPTS: '--debug --aggregate-paths=. ruby'`. |
+| `LM_JAVA_VERSION` | no | Version of Java. If set to `11`, Maven and Gradle use Java 11 instead of Java 8. |
+| `LM_PYTHON_VERSION` | no | Version of Python. If set to `3`, dependencies are installed using Python 3 instead of Python 2.7. |
+| `MAVEN_CLI_OPTS` | no | Additional arguments for the mvn executable. If not supplied, defaults to `-DskipTests`. |
+| `PIP_INDEX_URL` | no | Base URL of Python Package Index (default: `https://pypi.org/simple/`). |
+| `SETUP_CMD` | no | Custom setup for the dependency installation (experimental). |
### Installing custom dependencies
@@ -263,21 +284,80 @@ license_scanning:
The [`pip.conf`](https://pip.pypa.io/en/stable/reference/pip/) allows you to specify a list of
[trusted hosts](https://pip.pypa.io/en/stable/reference/pip/#cmdoption-trusted-host):
-```text
+```plaintext
[global]
trusted-host = pypi.example.com
```
+#### Using private Python repos
+
+If you have a private Python repository you can use the `PIP_INDEX_URL` [environment variable](#available-variables)
+to specify its location. It's also possible to provide a custom `pip.conf` for
+[additional configuration](#custom-root-certificates-for-python).
+
+### Configuring NPM projects
+
+You can configure NPM projects by using an [`.npmrc`](https://docs.npmjs.com/configuring-npm/npmrc.html)
+file.
+
+#### Using private NPM registries
+
+If you have a private NPM registry you can use the
+[`registry`](https://docs.npmjs.com/using-npm/config#registry)
+setting to specify its location.
+
+For example:
+
+```plaintext
+registry = https://npm.example.com
+```
+
+#### Custom root certificates for NPM
+
+You can supply a custom root certificate to complete TLS verification by using the
+`ADDITIONAL_CA_CERT_BUNDLE` [environment variable](#available-variables).
+
+To disable TLS verification you can provide the [`strict-ssl`](https://docs.npmjs.com/using-npm/config#strict-ssl)
+setting.
+
+For example:
+
+```plaintext
+strict-ssl = false
+```
+
+### Configuring Yarn projects
+
+You can configure Yarn projects by using a [`.yarnrc.yml`](https://yarnpkg.com/configuration/yarnrc)
+file.
+
+#### Using private Yarn registries
+
+If you have a private Yarn registry you can use the
+[`npmRegistryServer`](https://yarnpkg.com/configuration/yarnrc#npmRegistryServer)
+setting to specify its location.
+
+For example:
+
+```text
+npmRegistryServer: "https://npm.example.com"
+```
+
+#### Custom root certificates for Yarn
+
+You can supply a custom root certificate to complete TLS verification by using the
+`ADDITIONAL_CA_CERT_BUNDLE` [environment variable](#available-variables).
+
### Migration from `license_management` to `license_scanning`
In GitLab 12.8 a new name for `license_management` job was introduced. This change was made to improve clarity around the purpose of the scan, which is to scan and collect the types of licenses present in a projects dependencies.
-The support of `license_management` is scheduled to be dropped in GitLab 13.0.
+GitLab 13.0 drops support for `license_management`.
If you're using a custom setup for License Compliance, you're required
to update your CI config accordingly:
1. Change the CI template to `License-Scanning.gitlab-ci.yml`.
1. Change the job name to `license_scanning` (if you mention it in `.gitlab-ci.yml`).
-1. Change the artifact name to `gl-license-scanning-report.json` (if you mention it in `.gitlab-ci.yml`).
+1. Change the artifact name to `license_scanning`, and the file name to `gl-license-scanning-report.json` (if you mention it in `.gitlab-ci.yml`).
For example, the following `.gitlab-ci.yml`:
@@ -303,11 +383,21 @@ license_scanning:
license_scanning: gl-license-scanning-report.json
```
+If you use the `license_management` artifact in GitLab 13.0 or later, the License Compliance job generates this error:
+
+```plaintext
+WARNING: Uploading artifacts to coordinator... failed id=:id responseStatus=400 Bad Request status=400 Bad Request token=:sha
+
+FATAL: invalid_argument
+```
+
+If you encounter this error, follow the instructions described in this section.
+
## Running License Compliance in an offline environment
For self-managed GitLab instances in an environment with limited, restricted, or intermittent access
to external resources through the internet, some adjustments are required for the License Compliance job to
-successfully run.
+successfully run. For more information, see [Offline environments](../../application_security/offline_deployments/index.md).
### Requirements for offline License Compliance
@@ -318,11 +408,11 @@ To use License Compliance in an offline environment, you need:
NOTE: **Note:**
GitLab Runner has a [default `pull policy` of `always`](https://docs.gitlab.com/runner/executors/docker.html#using-the-always-pull-policy),
-meaning the runner will try to pull Docker images from the GitLab container registry even if a local
+meaning the Runner tries to pull Docker images from the GitLab container registry even if a local
copy is available. GitLab Runner's [`pull_policy` can be set to `if-not-present`](https://docs.gitlab.com/runner/executors/docker.html#using-the-if-not-present-pull-policy)
in an offline environment if you prefer using only locally available Docker images. However, we
-recommend leaving the pull policy set to `always`, as it better enables updated scanners to be used
-within your CI/CD pipelines.
+recommend keeping the pull policy setting to `always` if not in an offline environment, as this
+enables the use of updated scanners in your CI/CD pipelines.
### Make GitLab License Compliance analyzer images available inside your Docker registry
@@ -345,10 +435,8 @@ For details on saving and transporting Docker images as a file, see Docker's doc
### Set License Compliance CI job variables to use local License Compliance analyzers
-Override License Compliance environment variables to use to your local container registry
-as the source for License Compliance analyzer images.
-
-For example, this assumes a local Docker registry repository of `localhost:5000/analyzers`:
+Add the following configuration to your `.gitlab-ci.yml` file. You must replace `image` to refer to
+the License Compliance Docker image hosted on your local Docker container registry:
```yaml
include:
@@ -362,8 +450,8 @@ license_scanning:
The License Compliance job should now use local copies of the License Compliance analyzers to scan
your code and generate security reports, without requiring internet access.
-Additional [configuration](#using-private-maven-repos) may be needed for connecting to private Maven
-repositories.
+Additional configuration may be needed for connecting to [private Maven repositories](#using-private-maven-repos),
+[private NPM registries](#using-private-npm-registries), [private Yarn registries](#using-private-yarn-registries), and [private Python repositories](#using-private-python-repos).
Exact name matches are required for [project policies](#project-policies-for-license-compliance)
when running in an offline environment ([see related issue](https://gitlab.com/gitlab-org/gitlab/-/issues/212388)).
@@ -384,7 +472,7 @@ To allow or deny a license:
**License Compliance** section.
1. Click the **Add a license** button.
- ![License Compliance Add License](img/license_compliance_add_license_v12_3.png)
+ ![License Compliance Add License](img/license_compliance_add_license_v13_0.png)
1. In the **License name** dropdown, either:
- Select one of the available licenses. You can search for licenses in the field
@@ -398,23 +486,23 @@ To modify an existing license:
1. In the **License Compliance** list, click the **Allow/Deny** dropdown to change it to the desired status.
- ![License Compliance Settings](img/license_compliance_settings_v12_3.png)
+ ![License Compliance Settings](img/license_compliance_settings_v13_0.png)
Searching for Licenses:
1. Use the **Search** box to search for a specific license.
- ![License Compliance Search](img/license_compliance_search_v12_3.png)
+ ![License Compliance Search](img/license_compliance_search_v13_0.png)
## License Compliance report under pipelines
> [Introduced](https://gitlab.com/gitlab-org/gitlab/issues/5491) in [GitLab Ultimate](https://about.gitlab.com/pricing/) 11.2.
From your project's left sidebar, navigate to **CI/CD > Pipelines** and click on the
-pipeline ID that has a `license_management` job to see the Licenses tab with the listed
+pipeline ID that has a `license_scanning` job to see the Licenses tab with the listed
licenses (if any).
-![License Compliance Pipeline Tab](img/license_compliance_pipeline_tab_v12_3.png)
+![License Compliance Pipeline Tab](img/license_compliance_pipeline_tab_v13_0.png)
<!-- ## Troubleshooting
@@ -447,8 +535,9 @@ in your project's sidebar, and you'll see the licenses displayed, where:
- **Name:** The name of the license.
- **Component:** The components which have this license.
+- **Policy Violation:** The license has a [license policy](#policies) marked as **Deny**.
-![License List](img/license_list_v12_6.png)
+![License List](img/license_list_v13_0.png)
## Policies
@@ -459,9 +548,9 @@ and the associated classifications for each.
Policies can be configured by maintainers of the project.
-![Edit Policy](img/policies_maintainer_edit_v12_9.png)
-![Add Policy](img/policies_maintainer_add_v12_9.png)
+![Edit Policy](img/policies_maintainer_edit_v13_0.png)
+![Add Policy](img/policies_maintainer_add_v13_0.png)
Developers of the project can view the policies configured in a project.
-![View Policies](img/policies_v12_9.png)
+![View Policies](img/policies_v13_0.png)
diff --git a/doc/user/feature_highlight.md b/doc/user/feature_highlight.md
index df85a129ce1..47f8671afae 100644
--- a/doc/user/feature_highlight.md
+++ b/doc/user/feature_highlight.md
@@ -1,6 +1,6 @@
# Feature highlight
-> [Introduced][ce-16379] in GitLab 10.5
+> [Introduced](https://gitlab.com/gitlab-org/gitlab-foss/-/merge_requests/16379) in GitLab 10.5
Feature highlights are represented by a pulsing blue dot. Hovering over the dot
will open up callout with more information.
@@ -11,5 +11,3 @@ at the bottom of the callout. There isn't a way to restore the feature highlight
after it has been dismissed.
![Clusters feature highlight](img/feature_highlight_example.png)
-
-[ce-16379]: https://gitlab.com/gitlab-org/gitlab-foss/-/merge_requests/16379
diff --git a/doc/user/gitlab_com/index.md b/doc/user/gitlab_com/index.md
index 7c005a60bea..d615b67f3d2 100644
--- a/doc/user/gitlab_com/index.md
+++ b/doc/user/gitlab_com/index.md
@@ -28,12 +28,12 @@ gitlab.com ecdsa-sha2-nistp256 AAAAE2VjZHNhLXNoYTItbmlzdHAyNTYAAAAIbmlzdHAyNTYAA
## Mail configuration
-GitLab.com sends emails from the `mg.gitlab.com` domain via [Mailgun] and has
+GitLab.com sends emails from the `mg.gitlab.com` domain via [Mailgun](https://www.mailgun.com/) and has
its own dedicated IP address (`198.61.254.240`).
## Alternative SSH port
-GitLab.com can be reached via a [different SSH port][altssh] for `git+ssh`.
+GitLab.com can be reached via a [different SSH port](https://about.gitlab.com/blog/2016/02/18/gitlab-dot-com-now-supports-an-alternate-git-plus-ssh-port/) for `git+ssh`.
| Setting | Value |
| --------- | ------------------- |
@@ -89,7 +89,7 @@ or over the size limit, you can [reduce your repository size with Git](../projec
| Repository size including LFS | 10G | Unlimited |
NOTE: **Note:**
-A single `git push` is limited to 5GB. LFS is not affected by this limit.
+`git push` and GitLab project imports are limited to 5GB per request. Git LFS and imports other than a file upload are not affected by this limit.
## IP range
@@ -115,9 +115,12 @@ A limit of:
GitLab offers Linux and Windows shared runners hosted on GitLab.com for executing your pipelines.
+NOTE: **Note:**
+Shared Runners provided by GitLab are **not** configurable. Consider [installing your own Runner](https://docs.gitlab.com/runner/install/) if you have specific configuration needs.
+
### Linux Shared Runners
-Linux Shared Runners on GitLab.com run in [autoscale mode] and are powered by Google Cloud Platform.
+Linux Shared Runners on GitLab.com run in [autoscale mode](https://docs.gitlab.com/runner/configuration/autoscale.html) and are powered by Google Cloud Platform.
Autoscaling means reduced waiting times to spin up CI/CD jobs, and isolated VMs for each project,
thus maximizing security. They're free to use for public open source projects and limited
to 2000 CI minutes per month per group for private projects. More minutes
@@ -133,16 +136,36 @@ The `gitlab-shared-runners-manager-X.gitlab.com` fleet of Runners are dedicated
Jobs handled by the shared Runners on GitLab.com (`shared-runners-manager-X.gitlab.com`),
**will be timed out after 3 hours**, regardless of the timeout configured in a
-project. Check the issues [4010] and [4070] for the reference.
+project. Check the issues [4010](https://gitlab.com/gitlab-com/infrastructure/issues/4010) and [4070](https://gitlab.com/gitlab-com/infrastructure/issues/4070) for the reference.
Below are the shared Runners settings.
| Setting | GitLab.com | Default |
| ----------- | ----------------- | ---------- |
-| [GitLab Runner] | [Runner versions dashboard](https://dashboards.gitlab.com/d/000000159/ci?from=now-1h&to=now&refresh=5m&orgId=1&panelId=12&fullscreen&theme=light) | - |
+| [GitLab Runner](https://gitlab.com/gitlab-org/gitlab-runner) | [Runner versions dashboard](https://dashboards.gitlab.com/d/000000159/ci?from=now-1h&to=now&refresh=5m&orgId=1&panelId=12&fullscreen&theme=light) | - |
| Executor | `docker+machine` | - |
| Default Docker image | `ruby:2.5` | - |
-| `privileged` (run [Docker in Docker]) | `true` | `false` |
+| `privileged` (run [Docker in Docker](https://hub.docker.com/_/docker/)) | `true` | `false` |
+
+#### Pre-clone script
+
+Linux Shared Runners on GitLab.com provide a way to run commands in a CI
+job before the Runner attempts to run `git init` and `git fetch` to
+download a GitLab repository. The
+[pre_clone_script](https://docs.gitlab.com/runner/configuration/advanced-configuration.html#the-runners-section)
+can be used for:
+
+- Seeding the build directory with repository data
+- Sending a request to a server
+- Downloading assets from a CDN
+- Any other commands that must run before the `git init`
+
+To use this feature, define a [CI/CD variable](../../ci/variables/README.md#create-a-custom-variable-in-the-ui) called
+`CI_PRE_CLONE_SCRIPT` that contains a bash script.
+
+[This example](../../development/pipelines.md#pre-clone-step)
+demonstrates how you might use a pre-clone step to seed the build
+directory.
#### `config.toml`
@@ -164,6 +187,7 @@ sentry_dsn = "X"
request_concurrency = X
url = "https://gitlab.com/"
token = "SHARED_RUNNER_TOKEN"
+ pre_clone_script = "eval \"$CI_PRE_CLONE_SCRIPT\""
executor = "docker+machine"
environment = [
"DOCKER_DRIVER=overlay2",
@@ -432,7 +456,7 @@ of proposed changes can be found at
## Unicorn
-GitLab.com adjusts the memory limits for the [unicorn-worker-killer][unicorn-worker-killer] gem.
+GitLab.com adjusts the memory limits for the [unicorn-worker-killer](https://rubygems.org/gems/unicorn-worker-killer) gem.
Base default:
@@ -600,13 +624,3 @@ Service discovery:
High Performance TCP/HTTP Load Balancer:
- [`gitlab-cookbooks` / `gitlab-haproxy` · GitLab](https://gitlab.com/gitlab-cookbooks/gitlab-haproxy)
-
-[autoscale mode]: https://docs.gitlab.com/runner/configuration/autoscale.html "How Autoscale works"
-[runners-post]: https://about.gitlab.com/blog/2016/04/05/shared-runners/ "Shared Runners on GitLab.com"
-[GitLab Runner]: https://gitlab.com/gitlab-org/gitlab-runner
-[altssh]: https://about.gitlab.com/blog/2016/02/18/gitlab-dot-com-now-supports-an-alternate-git-plus-ssh-port/ "GitLab.com now supports an alternate git+ssh port"
-[docker in docker]: https://hub.docker.com/_/docker/ "Docker in Docker at DockerHub"
-[mailgun]: https://www.mailgun.com/ "Mailgun website"
-[unicorn-worker-killer]: https://rubygems.org/gems/unicorn-worker-killer "unicorn-worker-killer"
-[4010]: https://gitlab.com/gitlab-com/infrastructure/issues/4010 "Find a good value for maximum timeout for Shared Runners"
-[4070]: https://gitlab.com/gitlab-com/infrastructure/issues/4070 "Configure per-runner timeout for shared-runners-manager-X on GitLab.com"
diff --git a/doc/user/group/clusters/index.md b/doc/user/group/clusters/index.md
index 224836b20b5..5e6ff980c8b 100644
--- a/doc/user/group/clusters/index.md
+++ b/doc/user/group/clusters/index.md
@@ -1,13 +1,14 @@
---
type: reference
+stage: Configure
+group: Configure
+info: To determine the technical writer assigned to the Stage/Group associated with this page, see https://about.gitlab.com/handbook/engineering/ux/technical-writing/#designated-technical-writers
---
# Group-level Kubernetes clusters
> [Introduced](https://gitlab.com/gitlab-org/gitlab-foss/issues/34758) in GitLab 11.6.
-## Overview
-
Similar to [project-level](../../project/clusters/index.md) and
[instance-level](../../instance/clusters/index.md) Kubernetes clusters,
group-level Kubernetes clusters allow you to connect a Kubernetes cluster to
@@ -22,47 +23,43 @@ and troubleshooting applications for your group cluster, see
## RBAC compatibility
-For each project under a group with a Kubernetes cluster, GitLab will
-create a restricted service account with [`edit`
-privileges](https://kubernetes.io/docs/reference/access-authn-authz/rbac/#user-facing-roles)
-in the project namespace.
+> - [Introduced](https://gitlab.com/gitlab-org/gitlab-foss/issues/29398) in GitLab 11.4.
+> - [Project namespace restriction](https://gitlab.com/gitlab-org/gitlab-foss/issues/51716) was introduced in GitLab 11.5.
-NOTE: **Note:**
-RBAC support was introduced in
-[GitLab 11.4](https://gitlab.com/gitlab-org/gitlab-foss/issues/29398), and
-Project namespace restriction was introduced in
-[GitLab 11.5](https://gitlab.com/gitlab-org/gitlab-foss/issues/51716).
+For each project under a group with a Kubernetes cluster, GitLab creates a restricted
+service account with [`edit` privileges](https://kubernetes.io/docs/reference/access-authn-authz/rbac/#user-facing-roles)
+in the project namespace.
## Cluster precedence
-GitLab will use the project's cluster before using any cluster belonging
-to the group containing the project if the project's cluster is available and not disabled.
-
-In the case of sub-groups, GitLab will use the cluster of the closest ancestor group
+If the project's cluster is available and not disabled, GitLab uses the
+project's cluster before using any cluster belonging to the group containing
+the project.
+In the case of sub-groups, GitLab uses the cluster of the closest ancestor group
to the project, provided the cluster is not disabled.
## Multiple Kubernetes clusters **(PREMIUM)**
-With GitLab Premium, you can associate more than one Kubernetes clusters to your
-group. That way you can have different clusters for different environments,
-like dev, staging, production, etc.
+With [GitLab Premium](https://about.gitlab.com/pricing/premium/), you can associate
+more than one Kubernetes cluster to your group, and maintain different clusters
+for different environments, such as development, staging, and production.
-Add another cluster similar to the first one and make sure to
-[set an environment scope](#environment-scopes-premium) that will
-differentiate the new cluster from the rest.
+When adding another cluster,
+[set an environment scope](#environment-scopes-premium) to help
+differentiate the new cluster from your other clusters.
## GitLab-managed clusters
> - [Introduced](https://gitlab.com/gitlab-org/gitlab-foss/-/merge_requests/22011) in GitLab 11.5.
> - Became [optional](https://gitlab.com/gitlab-org/gitlab-foss/-/merge_requests/26565) in GitLab 11.11.
-You can choose to allow GitLab to manage your cluster for you. If your cluster is
-managed by GitLab, resources for your projects will be automatically created. See the
-[Access controls](../../project/clusters/add_remove_clusters.md#access-controls) section for details on which resources will
-be created.
+You can choose to allow GitLab to manage your cluster for you. If GitLab manages
+your cluster, resources for your projects will be automatically created. See the
+[Access controls](../../project/clusters/add_remove_clusters.md#access-controls)
+section for details on which resources GitLab creates for you.
-For clusters not managed by GitLab, project-specific resources will not be created
-automatically. If you are using [Auto DevOps](../../../topics/autodevops/index.md)
+For clusters not managed by GitLab, project-specific resources won't be created
+automatically. If you're using [Auto DevOps](../../../topics/autodevops/index.md)
for deployments with a cluster not managed by GitLab, you must ensure:
- The project's deployment service account has permissions to deploy to
@@ -72,8 +69,8 @@ for deployments with a cluster not managed by GitLab, you must ensure:
`KUBE_NAMESPACE` directly is discouraged.
NOTE: **Note:**
-If you [install applications](#installing-applications) on your cluster, GitLab will create
-the resources required to run these even if you have chosen to manage your own cluster.
+If you [install applications](#installing-applications) on your cluster, GitLab creates
+the resources required to run them even if you choose to manage your own cluster.
### Clearing the cluster cache
@@ -86,7 +83,8 @@ your cluster, which can cause deployment jobs to fail.
To clear the cache:
-1. Navigate to your group’s **Kubernetes** page, and select your cluster.
+1. Navigate to your group’s **{cloud-gear}** **Kubernetes** page,
+ and select your cluster.
1. Expand the **Advanced settings** section.
1. Click **Clear cluster cache**.
@@ -105,17 +103,17 @@ The domain should have a wildcard DNS configured to the Ingress IP address.
When adding more than one Kubernetes cluster to your project, you need to differentiate
them with an environment scope. The environment scope associates clusters with
-[environments](../../../ci/environments.md) similar to how the
-[environment-specific variables](../../../ci/variables/README.md#limiting-environment-scopes-of-environment-variables)
+[environments](../../../ci/environments/index.md) similar to how the
+[environment-specific variables](../../../ci/variables/README.md#limit-the-environment-scopes-of-environment-variables)
work.
While evaluating which environment matches the environment scope of a
-cluster, [cluster precedence](#cluster-precedence) will take
-effect. The cluster at the project level will take precedence, followed
+cluster, [cluster precedence](#cluster-precedence) takes
+effect. The cluster at the project level takes precedence, followed
by the closest ancestor group, followed by that groups' parent and so
on.
-For example, let's say we have the following Kubernetes clusters:
+For example, if your project has the following Kubernetes clusters:
| Cluster | Environment scope | Where |
| ---------- | ------------------- | ----------|
@@ -151,23 +149,22 @@ deploy to production:
url: https://example.com/
```
-The result will then be:
+The result is:
-- The Project cluster will be used for the `test` job.
-- The Staging cluster will be used for the `deploy to staging` job.
-- The Production cluster will be used for the `deploy to production` job.
+- The Project cluster is used for the `test` job.
+- The Staging cluster is used for the `deploy to staging` job.
+- The Production cluster is used for the `deploy to production` job.
## Cluster environments **(PREMIUM)**
-For a consolidated view of which CI [environments](../../../ci/environments.md)
+For a consolidated view of which CI [environments](../../../ci/environments/index.md)
are deployed to the Kubernetes cluster, see the documentation for
[cluster environments](../../clusters/environments.md).
## Security of Runners
For important information about securely configuring GitLab Runners, see
-[Security of
-Runners](../../project/clusters/add_remove_clusters.md#security-of-gitlab-runners)
+[Security of Runners](../../project/clusters/add_remove_clusters.md#security-of-gitlab-runners)
documentation for project-level clusters.
## More information
diff --git a/doc/user/group/contribution_analytics/index.md b/doc/user/group/contribution_analytics/index.md
index 1bbc40a14a4..03f0ad6ad1c 100644
--- a/doc/user/group/contribution_analytics/index.md
+++ b/doc/user/group/contribution_analytics/index.md
@@ -1,7 +1,10 @@
---
type: reference
+stage: Manage
+group: Analytics
+To determine the technical writer assigned to the Stage/Group associated with this page, see:
+ https://about.gitlab.com/handbook/engineering/ux/technical-writing/#designated-technical-writers
---
-
# Contribution Analytics **(STARTER)**
> - Introduced in [GitLab Starter](https://about.gitlab.com/pricing/) 8.3.
diff --git a/doc/user/group/epics/img/epic_view_v12.3.png b/doc/user/group/epics/img/epic_view_v12.3.png
deleted file mode 100644
index 79758cf3d52..00000000000
--- a/doc/user/group/epics/img/epic_view_v12.3.png
+++ /dev/null
Binary files differ
diff --git a/doc/user/group/epics/img/epic_view_v13.0.png b/doc/user/group/epics/img/epic_view_v13.0.png
new file mode 100644
index 00000000000..b25a91d318a
--- /dev/null
+++ b/doc/user/group/epics/img/epic_view_v13.0.png
Binary files differ
diff --git a/doc/user/group/epics/index.md b/doc/user/group/epics/index.md
index 024f346ad47..d53acaf502a 100644
--- a/doc/user/group/epics/index.md
+++ b/doc/user/group/epics/index.md
@@ -5,25 +5,14 @@ type: reference, howto
# Epics **(PREMIUM)**
> - Introduced in [GitLab Ultimate](https://about.gitlab.com/pricing/) 10.2.
-> - In [GitLab 12.8](https://gitlab.com/gitlab-org/gitlab/issues/37081), single-level Epics were moved to the Premium tier.
+> - Single-level Epics [were moved](https://gitlab.com/gitlab-org/gitlab/issues/37081) to [GitLab Premium](https://about.gitlab.com/pricing/) in 12.8.
Epics let you manage your portfolio of projects more efficiently and with less
effort by tracking groups of issues that share a theme, across projects and
milestones.
-## Relationships between epics and issues
-
-The possible relationships between epics and issues are:
-
-- An epic is the parent of one or more issues.
-- An epic is the parent of one or more child epics. For details see [Multi-level child epics](#multi-level-child-epics-ultimate). **(ULTIMATE)**
-
-```mermaid
-graph TD
- Parent_epic --> Issue1
- Parent_epic --> Child_epic
- Child_epic --> Issue2
-```
+<!-- Possibly swap this file with one of a single epic -->
+![epics list view](img/epics_list_view_v12.5.png)
## Use cases
@@ -31,75 +20,37 @@ graph TD
- Track when the work for the group of issues is targeted to begin, and when it's targeted to end.
- Discuss and collaborate on feature ideas and scope at a high level.
-![epics list view](img/epics_list_view_v12.5.png)
-
-## Creating an epic
-
-A paginated list of epics is available in each group from where you can create
-a new epic. The list of epics includes also epics from all subgroups of the
-selected group. From your group page:
-
-1. Go to **Epics**.
-1. Click **New epic**.
-1. Enter a descriptive title and click **Create epic**.
-
-You will be taken to the new epic where can edit the following details:
-
-- Title
-- Description
-- Start date
-- Due date
-- Labels
-
-An epic's page contains the following tabs:
-
-- **Epics and Issues**: epics and issues added to this epic. Child epics, and their issues, are shown in a tree view.
- - Click on the <kbd>></kbd> beside a parent epic to reveal the child epics and issues.
- - Hover over the total counts to see a breakdown of open and closed items.
-- **Roadmap**: a roadmap view of child epics which have start and due dates.
-
-![epic view](img/epic_view_v12.3.png)
-
-## Adding an issue to an epic
-
-You can add an existing issue to an epic, or, from an epic's page, create a new issue that's automatically added to the epic.
+## Manage epics
-### Adding an existing issue to an epic
+To learn what you can do with an epic, see [Manage epics](manage_epics.md). Possible actions include:
-Existing issues that belong to a project in an epic's group, or any of the epic's
-subgroups, are eligible to be added to the epic. Newly added issues appear at the top of the list of issues in the **Epics and Issues** tab.
+- [Create an epic](manage_epics.md#create-an-epic)
+- [Bulk-edit epics](manage_epics.md#bulk-edit-epics)
+- [Delete an epic](manage_epics.md#delete-an-epic)
+- [Close an epic](manage_epics.md#close-an-epic)
+- [Reopen a closed epic](manage_epics.md#reopen-a-closed-epic)
+- [Go to an epic from an issue](manage_epics.md#go-to-an-epic-from-an-issue)
+- [Search for an epic from epics list page](manage_epics.md#search-for-an-epic-from-epics-list-page)
+- [Make an epic confidential](manage_epics.md#make-an-epic-confidential)
+- [Manage issues assigned to an epic](manage_epics.md#manage-issues-assigned-to-an-epic)
+- [Manage multi-level child epics **(ULTIMATE)**](manage_epics.md#manage-multi-level-child-epics-ultimate)
-An epic contains a list of issues and an issue can be associated with at most
-one epic. When you add an issue that's already linked to an epic,
-the issue is automatically unlinked from its current parent.
-
-To add an issue to an epic:
-
-1. Click **Add an issue**.
-1. Identify the issue to be added, using either of the following methods:
- - Paste the link of the issue.
- - Search for the desired issue by entering part of the issue's title, then selecting the desired match. ([From GitLab 12.5](https://gitlab.com/gitlab-org/gitlab/issues/9126))
-
- If there are multiple issues to be added, press <kbd>Spacebar</kbd> and repeat this step.
-1. Click **Add**.
-
-### Creating an issue from an epic
-
-> [Introduced](https://gitlab.com/gitlab-org/gitlab/issues/5419) in GitLab 12.7.
-
-Creating an issue from an epic enables you to maintain focus on the broader context of the epic while dividing work into smaller parts.
+## Relationships between epics and issues
-To create an issue from an epic:
+The possible relationships between epics and issues are:
-1. On the epic's page, under **Epics and Issues**, click the arrow next to **Add an issue** and select **Create new issue**.
-1. Under **Title**, enter the title for the new issue.
-1. From the **Project** dropdown, select the project in which the issue should be created.
-1. Click **Create issue**.
+- An epic is the parent of one or more issues.
+- An epic is the parent of one or more child epics. For details see [Multi-level child epics](#multi-level-child-epics-ultimate). **(ULTIMATE)**
-To remove an issue from an epic:
+```mermaid
+graph TD
+ Parent_epic --> Issue1
+ Parent_epic --> Child_epic
+ Child_epic --> Issue2
+```
-1. Click on the <kbd>x</kbd> button in the epic's issue list.
-1. Click **Remove** in the **Remove issue** warning message.
+See [Manage issues assigned to an epic](manage_epics.md#manage-issues-assigned-to-an-epic) for steps
+to add an issue to an epic, reorder issues, move issues between epics, or promote an issue to an epic.
## Issue health status in Epic tree **(ULTIMATE)**
@@ -118,28 +69,15 @@ This feature comes with a feature flag enabled by default. For steps to disable
> [Introduced](https://gitlab.com/gitlab-org/gitlab/issues/8333) in GitLab Ultimate 11.7.
-Any epic that belongs to a group, or subgroup of the parent epic's group, is
-eligible to be added. New child epics appear at the top of the list of epics in the **Epics and Issues** tab.
+Any epic that belongs to a group, or subgroup of the parent epic's group, is eligible to be added.
+New child epics appear at the top of the list of epics in the **Epics and Issues** tab.
When you add an epic that's already linked to a parent epic, the link to its current parent is removed.
-An epic can have multiple child epics with
-the maximum depth being 5.
-
-To add a child epic to an epic:
-
-1. Click **Add an epic**.
-1. Identify the epic to be added, using either of the following methods:
- - Paste the link of the epic.
- - Search for the desired issue by entering part of the epic's title, then selecting the desired match. ([From GitLab 12.5](https://gitlab.com/gitlab-org/gitlab/issues/9126))
+An epic can have multiple child epics up to the maximum depth of five.
- If there are multiple epics to be added, press <kbd>Spacebar</kbd> and repeat this step.
-1. Click **Add**.
-
-To remove a child epic from a parent epic:
-
-1. Click on the <kbd>x</kbd> button in the parent epic's list of epics.
-1. Click **Remove** in the **Remove epic** warning message.
+See [Manage multi-level child epics](manage_epics.md#manage-multi-level-child-epics-ultimate) for
+steps to create, move, reorder, or delete child epics.
## Start date and due date
@@ -197,137 +135,6 @@ have a [start or due date](#start-date-and-due-date), a
![Child epics roadmap](img/epic_view_roadmap_v12_9.png)
----
-
-## Reordering issues and child epics
-
-> [Introduced](https://gitlab.com/gitlab-org/gitlab/issues/9367) in GitLab 12.5.
-
-New issues and child epics are added to the top of their respective lists in the **Epics and Issues** tab. You can reorder the list of issues and the list of child epics. Issues and child epics cannot be intermingled.
-
-To reorder issues assigned to an epic:
-
-1. Go to the **Epics and Issues** tab.
-1. Drag and drop issues into the desired order.
-
-To reorder child epics assigned to an epic:
-
-1. Go to the **Epics and Issues** tab.
-1. Drag and drop epics into the desired order.
-
-## Updating epics
-
-### Using bulk editing
-
-To apply labels across multiple epics:
-
-1. Go to the Epics list.
-1. Click **Edit epics**.
- - Checkboxes will appear beside each epic.
- - A sidebar on the right-hand side will appear, with an editable field for labels.
-1. Check the checkbox beside each epic to be edited.
-1. Select the desired labels.
-1. Click **Update all**.
-
-![bulk editing](img/bulk_editing.png)
-
-## Deleting an epic
-
-NOTE: **Note:**
-To delete an epic, you need to be an [Owner](../../permissions.md#group-members-permissions) of a group/subgroup.
-
-When inside a single epic view, click the **Delete** button to delete the epic.
-A modal will pop-up to confirm your action.
-
-Deleting an epic releases all existing issues from their associated epic in the
-system.
-
-## Closing and reopening epics
-
-### Using buttons
-
-Whenever you decide that there is no longer need for that epic,
-close the epic using the close button:
-
-![close epic - button](img/button_close_epic.png)
-
-You can always reopen it using the reopen button.
-
-![reopen epic - button](img/button_reopen_epic.png)
-
----
-
-### Using quick actions
-
-You can close or reopen an epic using [Quick actions](../../project/quick_actions.md)
-
-## Navigating to an epic from an issue
-
-If an issue belongs to an epic, you can navigate to the containing epic with the
-link in the issue sidebar.
-
-![containing epic](img/containing_epic.png)
-
----
-
-## Promoting an issue to an epic
-
-> - [Introduced](https://gitlab.com/gitlab-org/gitlab/issues/3777) in [GitLab Ultimate](https://about.gitlab.com/pricing/) 11.6.
-> - In [GitLab 12.8](https://gitlab.com/gitlab-org/gitlab/issues/37081), it was moved to the Premium tier.
-
-If you have [permissions](../../permissions.md) to close an issue and create an
-epic in the parent group, you can promote an issue to an epic with the `/promote`
-[quick action](../../project/quick_actions.md#quick-actions-for-issues-merge-requests-and-epics).
-Only issues from projects that are in groups can be promoted. When attempting to promote a confidential
-issue, a warning will display. Promoting a confidential issue to an epic will make all information
-related to the issue public as epics are public to group members.
-
-When the quick action is executed:
-
-- An epic is created in the same group as the project of the issue.
-- Subscribers of the issue are notified that the epic was created.
-
-The following issue metadata will be copied to the epic:
-
-- Title, description, activity/comment thread.
-- Upvotes/downvotes.
-- Participants.
-- Group labels that the issue already has.
-
-## Searching for an epic from epics list page
-
-> - Introduced in [GitLab Ultimate](https://about.gitlab.com/pricing/) 10.5.
-> - In [GitLab 12.8](https://gitlab.com/gitlab-org/gitlab/issues/37081), it was moved to the Premium tier.
-
-You can search for an epic from the list of epics using filtered search bar (similar to
-that of Issues and Merge Requests) based on following parameters:
-
-- Title or description
-- Author name / username
-- Labels
-
-![epics search](img/epics_search.png)
-
-To search, go to the list of epics and click on the field **Search or filter results**.
-It will display a dropdown menu, from which you can add an author. You can also enter plain
-text to search by epic title or description. When done, press <kbd>Enter</kbd> on your
-keyboard to filter the list.
-
-You can also sort epics list by:
-
-- Created date
-- Last updated
-- Start date
-- Due date
-
-Each option contains a button that can toggle the order between **Ascending** and **Descending**.
-The sort option and order is saved and used wherever you browse epics, including the
-[Roadmap](../roadmap/index.md).
-
-![epics sort](img/epics_sort.png)
-
----
-
## Permissions
If you have access to view an epic and have access to view an issue already
diff --git a/doc/user/group/epics/manage_epics.md b/doc/user/group/epics/manage_epics.md
new file mode 100644
index 00000000000..50eb0c64f4b
--- /dev/null
+++ b/doc/user/group/epics/manage_epics.md
@@ -0,0 +1,302 @@
+---
+type: howto
+---
+
+<!-- When adding a new section here, remember to mention it in index.md#manage-epics -->
+
+# Manage epics **(PREMIUM)**
+
+This page collects instructions for all the things you can do with [epics](index.md) or in relation
+to them.
+
+## Create an epic
+
+A paginated list of epics is available in each group from where you can create
+a new epic. The list of epics includes also epics from all subgroups of the
+selected group. From your group page:
+
+1. Go to **Epics**.
+1. Click **New epic**.
+1. Enter a descriptive title.
+1. Click **Create epic**.
+
+You will be taken to the new epic where can edit the following details:
+
+- Title
+- Description
+- Start date
+- Due date
+- Labels
+
+An epic's page contains the following tabs:
+
+- **Epics and Issues**: epics and issues added to this epic. Child epics, and their issues, are shown in a tree view.
+ - Click the <kbd>></kbd> beside a parent epic to reveal the child epics and issues.
+ - Hover over the total counts to see a breakdown of open and closed items.
+- **Roadmap**: a roadmap view of child epics which have start and due dates.
+
+![epic view](img/epic_view_v13.0.png)
+
+## Bulk-edit epics
+
+You can edit multiple epics at once. For example, to apply labels to multiple epics:
+
+1. Go to the Epics list.
+1. Click **Edit epics**.
+ - Checkboxes appear next to each epic.
+ - A sidebar on the right-hand side appears with an editable field for labels.
+1. Select the checkbox next to each epic to be edited.
+1. Select the labels you want.
+1. Click **Update all**.
+
+![bulk editing](img/bulk_editing.png)
+
+## Delete an epic
+
+NOTE: **Note:**
+To delete an epic, you need to be an [Owner](../../permissions.md#group-members-permissions) of a group/subgroup.
+
+When editing the description of an epic, click the **Delete** button to delete the epic.
+A modal appears to confirm your action.
+
+Deleting an epic releases all existing issues from their associated epic in the system.
+
+## Close an epic
+
+Whenever you decide that there is no longer need for that epic,
+close the epic by:
+
+- Clicking the **Close epic** button.
+
+ ![close epic - button](img/button_close_epic.png)
+
+- Using a [quick action](../../project/quick_actions.md).
+
+## Reopen a closed epic
+
+You can reopen an epic that was closed by:
+
+- Clicking the **Reopen epic** button.
+
+ ![reopen epic - button](img/button_reopen_epic.png)
+
+- Using a [quick action](../../project/quick_actions.md).
+
+## Go to an epic from an issue
+
+If an issue belongs to an epic, you can navigate to the containing epic with the
+link in the issue sidebar.
+
+![containing epic](img/containing_epic.png)
+
+## Search for an epic from epics list page
+
+> - Introduced in [GitLab Ultimate](https://about.gitlab.com/pricing/) 10.5.
+> - [Moved](https://gitlab.com/gitlab-org/gitlab/issues/37081) to the [Premium](https://about.gitlab.com/pricing/) tier in GitLab 12.8.
+
+You can search for an epic from the list of epics using filtered search bar (similar to
+that of Issues and Merge Requests) based on following parameters:
+
+- Title or description
+- Author name / username
+- Labels
+
+![epics search](img/epics_search.png)
+
+To search, go to the list of epics and click the field **Search or filter results**.
+It will display a dropdown menu, from which you can add an author. You can also enter plain
+text to search by epic title or description. When done, press <kbd>Enter</kbd> on your
+keyboard to filter the list.
+
+You can also sort epics list by:
+
+- Created date
+- Last updated
+- Start date
+- Due date
+
+Each option contains a button that can toggle the order between **Ascending** and **Descending**.
+The sort option and order is saved and used wherever you browse epics, including the
+[Roadmap](../roadmap/index.md).
+
+![epics sort](img/epics_sort.png)
+
+## Make an epic confidential
+
+> - [Introduced](https://gitlab.com/gitlab-org/gitlab/-/issues/213068) in [GitLab Ultimate](https://about.gitlab.com/pricing/) 13.0.
+> - It's deployed behind a feature flag, disabled by default.
+> - It's disabled on GitLab.com.
+> - It's not recommended for production use.
+> - To use it in GitLab self-managed instances, ask a GitLab administrator to [enable it](#enable-confidential-epics-premium-only). **(PREMIUM ONLY)**
+
+When you're creating an epic, you can make it confidential by selecting the **Make this epic
+confidential** checkbox.
+
+### Enable Confidential Epics **(PREMIUM ONLY)**
+
+The Confidential Epics feature is under development and not ready for production use. It's deployed behind a
+feature flag that is **disabled by default**.
+[GitLab administrators with access to the GitLab Rails console](../../../administration/feature_flags.md)
+can enable it for your instance.
+
+To enable it:
+
+```ruby
+Feature.enable(:confidential_epics)
+```
+
+To disable it:
+
+```ruby
+Feature.disable(:confidential_epics)
+```
+
+## Manage issues assigned to an epic
+
+### Add an issue to an epic
+
+You can add an existing issue to an epic, or, create a new issue that's
+automatically added to the epic.
+
+#### Add an existing issue to an epic
+
+Existing issues that belong to a project in an epic's group, or any of the epic's
+subgroups, are eligible to be added to the epic. Newly added issues appear at the top of the list of
+issues in the **Epics and Issues** tab.
+
+An epic contains a list of issues and an issue can be associated with at most one epic.
+When you add an issue that's already linked to an epic, the issue is automatically unlinked from its
+current parent.
+
+To add an issue to an epic:
+
+1. Click the **Add** dropdown button.
+1. Click **Add an issue**.
+1. Identify the issue to be added, using either of the following methods:
+ - Paste the link of the issue.
+ - Search for the desired issue by entering part of the issue's title, then selecting the desired
+ match (introduced in [GitLab 12.5](https://gitlab.com/gitlab-org/gitlab/issues/9126)).
+
+ If there are multiple issues to be added, press <kbd>Spacebar</kbd> and repeat this step.
+1. Click **Add**.
+
+#### Create an issue from an epic
+
+> [Introduced](https://gitlab.com/gitlab-org/gitlab/issues/5419) in GitLab 12.7.
+
+Creating an issue from an epic enables you to maintain focus on the broader context of the epic
+while dividing work into smaller parts.
+
+To create an issue from an epic:
+
+1. On the epic's page, under **Epics and Issues**, click the **Add** dropdown button and select
+ **Create new issue**.
+1. Under **Title**, enter the title for the new issue.
+1. From the **Project** dropdown, select the project in which the issue should be created.
+1. Click **Create issue**.
+
+To remove an issue from an epic:
+
+1. Click the <kbd>x</kbd> button in the epic's issue list.
+1. Click **Remove** in the **Remove issue** warning message.
+
+### Reorder issues assigned to an epic
+
+> [Introduced](https://gitlab.com/gitlab-org/gitlab/issues/9367) in GitLab 12.5.
+
+New issues are added to the top of their list in the **Epics and Issues** tab.
+You can reorder the list of issues. Issues and child epics cannot be intermingled.
+
+To reorder issues assigned to an epic:
+
+1. Go to the **Epics and Issues** tab.
+1. Drag and drop issues into the desired order.
+
+To reorder child epics assigned to an epic:
+
+1. Go to the **Epics and Issues** tab.
+1. Drag and drop epics into the desired order.
+
+### Move issues between epics **(ULTIMATE)**
+
+> [Introduced](https://gitlab.com/gitlab-org/gitlab/-/issues/33039) in [GitLab Ultimate](https://about.gitlab.com/pricing/) 13.0.
+
+New issues are added to the top of their list in the **Epics and Issues**
+tab. You can move issues from one epic to another. Issues and child epics cannot be intermingled.
+
+To move an issue to another epic:
+
+1. Go to the **Epics and Issues** tab.
+1. Drag and drop issues into the desired parent epic.
+
+### Promote an issue to an epic
+
+> - [Introduced](https://gitlab.com/gitlab-org/gitlab/issues/3777) in [GitLab Ultimate](https://about.gitlab.com/pricing/) 11.6.
+> - In [GitLab 12.8](https://gitlab.com/gitlab-org/gitlab/issues/37081), it was moved to the Premium tier.
+
+If you have [permissions](../../permissions.md) to close an issue and create an
+epic in the parent group, you can promote an issue to an epic with the `/promote`
+[quick action](../../project/quick_actions.md#quick-actions-for-issues-merge-requests-and-epics).
+Only issues from projects that are in groups can be promoted. When attempting to promote a confidential
+issue, a warning will display. Promoting a confidential issue to an epic will make all information
+related to the issue public as epics are public to group members.
+
+When the quick action is executed:
+
+- An epic is created in the same group as the project of the issue.
+- Subscribers of the issue are notified that the epic was created.
+
+The following issue metadata will be copied to the epic:
+
+- Title, description, activity/comment thread.
+- Upvotes/downvotes.
+- Participants.
+- Group labels that the issue already has.
+
+## Manage multi-level child epics **(ULTIMATE)**
+
+### Add a child epic to an epic
+
+To add a child epic to an epic:
+
+1. Click the **Add** dropdown button.
+1. Click **Add an epic**.
+1. Identify the epic to be added, using either of the following methods:
+ - Paste the link of the epic.
+ - Search for the desired issue by entering part of the epic's title, then selecting the desired match (introduced in [GitLab 12.5](https://gitlab.com/gitlab-org/gitlab/issues/9126)).
+
+ If there are multiple epics to be added, press <kbd>Spacebar</kbd> and repeat this step.
+1. Click **Add**.
+
+### Move child epics between epics
+
+> [Introduced](https://gitlab.com/gitlab-org/gitlab/-/issues/33039) in [GitLab Ultimate](https://about.gitlab.com/pricing/) 13.0.
+
+New child epics are added to the top of their list in the **Epics and Issues** tab.
+You can move child epics from one epic to another.
+When you add an epic that's already linked to a parent epic, the link to its current parent is removed.
+Issues and child epics cannot be intermingled.
+
+To move child epics to another epic:
+
+1. Go to the **Epics and Issues** tab.
+1. Drag and drop epics into the desired parent epic.
+
+### Reorder child epics assigned to an epic
+
+> [Introduced](https://gitlab.com/gitlab-org/gitlab/issues/9367) in GitLab 12.5.
+
+New child epics are added to the top of their list in the **Epics and Issues** tab.
+You can reorder the list of child epics. Issues and child epics cannot be intermingled.
+
+To reorder child epics assigned to an epic:
+
+1. Go to the **Epics and Issues** tab.
+1. Drag and drop epics into the desired order.
+
+### Remove a child epic from a parent epic
+
+To remove a child epic from a parent epic:
+
+1. Click on the <kbd>x</kbd> button in the parent epic's list of epics.
+1. Click **Remove** in the **Remove epic** warning message.
diff --git a/doc/user/group/index.md b/doc/user/group/index.md
index b819e3e971e..f36f3b3fd4f 100644
--- a/doc/user/group/index.md
+++ b/doc/user/group/index.md
@@ -196,6 +196,9 @@ To change this setting for a specific group:
To change this setting globally, see [Default branch protection](../admin_area/settings/visibility_and_access_controls.md#default-branch-protection).
+NOTE: **Note:**
+In [GitLab Premium or higher](https://about.gitlab.com/pricing/), GitLab administrators can choose to [disable group owners from updating the default branch protection](../admin_area/settings/visibility_and_access_controls.md#disable-group-owners-from-updating-default-branch-protection-premium-only).
+
## Add projects to a group
There are two different ways to add a new project to a group:
@@ -211,8 +214,8 @@ There are two different ways to add a new project to a group:
### Default project-creation level
-> - [Introduced][ee-2534] in [GitLab Premium][ee] 10.5.
-> - Brought to [GitLab Starter][ee] in 10.7.
+> - [Introduced](https://gitlab.com/gitlab-org/gitlab/issues/2534) in [GitLab Premium](https://about.gitlab.com/pricing/) 10.5.
+> - Brought to [GitLab Starter](https://about.gitlab.com/pricing/) in 10.7.
> - [Moved](https://gitlab.com/gitlab-org/gitlab-foss/-/merge_requests/25975) to [GitLab Core](https://about.gitlab.com/pricing/) in 11.10.
By default, [Developers and Maintainers](../permissions.md#group-members-permissions) can create projects under a group.
@@ -308,7 +311,7 @@ See [the GitLab Enterprise Edition documentation](../../integration/ldap.md) for
## Epics **(ULTIMATE)**
-> Introduced in [GitLab Ultimate][ee] 10.2.
+> Introduced in [GitLab Ultimate](https://about.gitlab.com/pricing/) 10.2.
Epics let you manage your portfolio of projects more efficiently and with less
effort by tracking groups of issues that share a theme, across projects and
@@ -593,6 +596,29 @@ For performance reasons, we may delay the update up to 1 hour and 30 minutes.
If your namespace shows `N/A` as the total storage usage, you can trigger a recalculation by pushing a commit to any project in that namespace.
+#### Group push rules **(STARTER)**
+
+> [Introduced](https://gitlab.com/gitlab-org/gitlab/issues/34370) in [GitLab Starter](https://about.gitlab.com/pricing/) 12.8.
+
+Group push rules allow group maintainers to set
+[push rules](../../push_rules/push_rules.md) for newly created projects within the specific group.
+
+To configure push rules for a group, navigate to **{push-rules}** on the group's
+sidebar.
+
+When set, new subgroups have push rules set for them based on either:
+
+- The closest parent group with push rules defined.
+- Push rules set at the instance level, if no parent groups have push rules defined.
+
+##### Enabling the feature
+
+This feature comes with the `:group_push_rules` feature flag disabled by default. It can be enabled for specific group using feature flag [API endpoint](../../api/features.md#set-or-create-a-feature) or by GitLab administrator with Rails console access by running:
+
+```ruby
+Feature.enable(:group_push_rules)
+```
+
### Maximum artifacts size **(CORE ONLY)**
For information about setting a maximum artifact size for a group, see
@@ -623,6 +649,3 @@ questions that you know someone might ask.
Each scenario can be a third-level heading, e.g. `### Getting error message X`.
If you have none to add when creating a doc, leave this section in place
but commented out to help encourage others to add to it in the future. -->
-
-[ee]: https://about.gitlab.com/pricing/
-[ee-2534]: https://gitlab.com/gitlab-org/gitlab/issues/2534
diff --git a/doc/user/group/insights/index.md b/doc/user/group/insights/index.md
index edbb85962ed..cffbc013e66 100644
--- a/doc/user/group/insights/index.md
+++ b/doc/user/group/insights/index.md
@@ -1,5 +1,9 @@
---
type: reference, howto
+stage: Manage
+group: Analytics
+To determine the technical writer assigned to the Stage/Group associated with this page, see:
+ https://about.gitlab.com/handbook/engineering/ux/technical-writing/#designated-technical-writers
---
# Insights **(ULTIMATE)**
diff --git a/doc/user/group/issues_analytics/index.md b/doc/user/group/issues_analytics/index.md
index 4477b9bb1e6..df96f2626e1 100644
--- a/doc/user/group/issues_analytics/index.md
+++ b/doc/user/group/issues_analytics/index.md
@@ -1,5 +1,9 @@
---
type: reference
+stage: Manage
+group: Analytics
+To determine the technical writer assigned to the Stage/Group associated with this page, see:
+ https://about.gitlab.com/handbook/engineering/ux/technical-writing/#designated-technical-writers
---
# Issues Analytics **(PREMIUM)**
diff --git a/doc/user/group/roadmap/img/roadmap_view_v12_10.png b/doc/user/group/roadmap/img/roadmap_view_v12_10.png
deleted file mode 100644
index 69579fd1c1e..00000000000
--- a/doc/user/group/roadmap/img/roadmap_view_v12_10.png
+++ /dev/null
Binary files differ
diff --git a/doc/user/group/roadmap/img/roadmap_view_v13_0.png b/doc/user/group/roadmap/img/roadmap_view_v13_0.png
new file mode 100644
index 00000000000..a5b76b84418
--- /dev/null
+++ b/doc/user/group/roadmap/img/roadmap_view_v13_0.png
Binary files differ
diff --git a/doc/user/group/roadmap/index.md b/doc/user/group/roadmap/index.md
index 9f068adcd47..6bee552d433 100644
--- a/doc/user/group/roadmap/index.md
+++ b/doc/user/group/roadmap/index.md
@@ -7,7 +7,8 @@ type: reference
> - Introduced in [GitLab Ultimate](https://about.gitlab.com/pricing/) 10.5.
> - In [GitLab 12.9](https://gitlab.com/gitlab-org/gitlab/issues/198062), Roadmaps were moved to the Premium tier.
> - In [GitLab 12.9](https://gitlab.com/gitlab-org/gitlab/issues/5164) and later, the epic bars show epics' title, progress, and completed weight percentage.
-> - In [GitLab 12.10](https://gitlab.com/gitlab-org/gitlab/-/issues/6802), and later, milestones appear in Roadmaps.
+> - Milestones appear in Roadmaps in [GitLab 12.10](https://gitlab.com/gitlab-org/gitlab/-/issues/6802), and later.
+> - Feature flag removed in [GitLab 13.0](https://gitlab.com/gitlab-org/gitlab/-/merge_requests/29641).
Epics and milestones within a group containing **Start date** and/or **Due date**
can be visualized in a form of a timeline (that is, a Gantt chart). The Roadmap page
@@ -23,7 +24,7 @@ You can click the chevron **{chevron-down}** next to the epic title to expand an
On top of the milestone bars, you can see their title. When you hover a milestone bar or title, a popover appears with its title, start date and due date.
-![roadmap view](img/roadmap_view_v12_10.png)
+![roadmap view](img/roadmap_view_v13_0.png)
A dropdown menu allows you to show only open or closed epics. By default, all epics are shown.
diff --git a/doc/user/group/saml_sso/index.md b/doc/user/group/saml_sso/index.md
index f49dd225146..a3d9a14df10 100644
--- a/doc/user/group/saml_sso/index.md
+++ b/doc/user/group/saml_sso/index.md
@@ -59,7 +59,7 @@ We recommend setting the NameID format to `Persistent` unless using a field (suc
With this option enabled, users must use your group's GitLab single sign on URL to be added to the group or be added via SCIM. Users cannot be added manually, and may only access project/group resources via the UI by signing in through the SSO URL.
-However, users will not be prompted to log via SSO on each visit. GitLab will check whether a user has authenticated through the SSO link, and will only prompt the user to login via SSO if it has been longer than 7 days.
+However, users will not be prompted to log via SSO on each visit. GitLab will check whether a user has authenticated through the SSO link, and will only prompt the user to login via SSO if the session has expired.
We intend to add a similar SSO requirement for [Git and API activity](https://gitlab.com/gitlab-org/gitlab/issues/9152) in the future.
@@ -189,6 +189,9 @@ Once you've set up your identity provider to work with GitLab, you'll need to co
![Group SAML Settings for GitLab.com](img/group_saml_settings.png)
+NOTE: **Note:**
+Please note that the certificate [fingerprint algorithm](#additional-setup-options) must be in SHA1. When configuring the identity provider, use a secure [signature algorithm](#additional-setup-options).
+
## User access and management
Once Group SSO is configured and enabled, users can access the GitLab.com group through the identity provider's dashboard. If [SCIM](scim_setup.md) is configured, please see the [user access and linking setup section on the SCIM page](scim_setup.md#user-access-and-linking-setup).
@@ -254,6 +257,9 @@ Set other user attributes and claims according to the [assertions table](#assert
### Okta setup notes
+<i class="fa fa-youtube-play youtube" aria-hidden="true"></i>
+For a demo of the Okta SAML setup including SCIM, see [Demo: Okta Group SAML & SCIM setup](https://youtu.be/0ES9HsZq0AQ).
+
| GitLab Setting | Okta Field |
|--------------|----------------|
| Identifier | Audience URI |
@@ -297,7 +303,7 @@ GitLab [isn't limited to the SAML providers listed above](#my-identity-provider-
| SAML Request Binding | HTTP Redirect | GitLab (the service provider) redirects users to your Identity Provider with a base64 encoded `SAMLRequest` HTTP parameter. |
| SAML Response Binding | HTTP POST | Your Identity Provider responds to users with an HTTP form including the `SAMLResponse`, which a user's browser submits back to GitLab. |
| Sign SAML Response | Yes | We require this to prevent tampering. |
-| X509 Certificate in response | Yes | This is used to sign the response and checked against the provided fingerprint. |
+| X.509 Certificate in response | Yes | This is used to sign the response and checked against the provided fingerprint. |
| Fingerprint Algorithm | SHA-1 | We need a SHA-1 hash of the certificate used to sign the SAML Response. |
| Signature Algorithm | SHA-1/SHA-256/SHA-384/SHA-512 | Also known as the Digest Method, this can be specified in the SAML response. It determines how a response is signed. |
| Encrypt SAML Assertion | No | TLS is used between your Identity Provider, the user's browser, and GitLab. |
diff --git a/doc/user/group/saml_sso/scim_setup.md b/doc/user/group/saml_sso/scim_setup.md
index e333fd19c1b..f66c8a788b6 100644
--- a/doc/user/group/saml_sso/scim_setup.md
+++ b/doc/user/group/saml_sso/scim_setup.md
@@ -12,25 +12,28 @@ that group is synchronized between GitLab and the identity provider.
GitLab's [SCIM API](../../../api/scim.md) implements part of [the RFC7644 protocol](https://tools.ietf.org/html/rfc7644).
+## Features
+
Currently, the following actions are available:
-- CREATE
-- UPDATE
-- DELETE (deprovisioning)
+- Create users
+- Update users (Azure only)
+- Deactivate users
The following identity providers are supported:
- Azure
+- Okta
## Requirements
-- [Group SSO](index.md) must be configured.
+- [Group Single Sign-On](index.md) must be configured.
## GitLab configuration
-Once [Single sign-on](index.md) has been configured, we can:
+Once [Group Single Sign-On](index.md) has been configured, we can:
-1. Navigate to the group and click **Settings > SAML SSO**.
+1. Navigate to the group and click **Administration > SAML SSO**.
1. Click on the **Generate a SCIM token** button.
1. Save the token and URL so they can be used in the next step.
@@ -38,9 +41,12 @@ Once [Single sign-on](index.md) has been configured, we can:
## Identity Provider configuration
-### Azure
+- [Azure](#azure-configuration-steps)
+- [Okta](#okta-configuration-steps)
+
+### Azure configuration steps
-The SAML application that was created during [Single sign-on](index.md) setup now needs to be set up for SCIM.
+The SAML application that was created during [Single sign-on](index.md) setup for [Azure](https://docs.microsoft.com/en-us/azure/active-directory/manage-apps/configure-single-sign-on-non-gallery-applications) now needs to be set up for SCIM.
1. Check the configuration for your GitLab SAML app and ensure that **Name identifier value** (NameID) points to `user.objectid` or another unique identifier. This will match the `extern_uid` used on GitLab.
@@ -109,6 +115,43 @@ bottom of the **Provisioning** screen, together with a link to the audit logs.
CAUTION: **Warning:**
Once synchronized, changing the field mapped to `id` and `externalId` will likely cause provisioning errors, duplicate users, and prevent existing users from accessing the GitLab group.
+### Okta configuration steps
+
+The SAML application that was created during [Single sign-on](index.md#okta-setup-notes) setup for [Okta](https://developer.okta.com/docs/guides/saml-application-setup/overview/) now needs to be set up for SCIM.
+Before proceeding, be sure to complete the [GitLab configuration](#gitlab-configuration) process.
+
+1. Sign in to Okta.
+1. If you see an **Admin** button in the top right, click the button. This will
+ ensure you are in the Admin area.
+
+ TIP: **Tip:** If you're using the Developer Console, click **Developer Console** in the top
+ bar and select **Classic UI**. Otherwise, you may not see the buttons described
+ in the following steps:
+
+1. In the **Application** tab, click **Add Application**.
+1. Search for **GitLab**, find and click on the 'GitLab' application.
+1. On the GitLab application overview page, click **Add**.
+1. Under **Application Visibility** select both check boxes. Currently the GitLab application does not support SAML authentication so the icon should not be shown to users.
+1. Click **Done** to finish adding the application.
+1. In the **Provisioning** tab, click **Configure API integration**.
+1. Select **Enable API integration**.
+ - For **Base URL** enter the URL obtained from the GitLab SCIM configuration page
+ - For **API Token** enter the SCIM token obtained from the GitLab SCIM configuration page
+1. Click 'Test API Credentials' to verify configuration.
+1. Click **Save** to apply the settings.
+1. After saving the API integration details, new settings tabs will appear on the left. Choose **To App**.
+1. Click **Edit**.
+1. Check the box to **Enable** for both **Create Users** and **Deactivate Users**.
+1. Click **Save**.
+1. Assign users in the **Assignments** tab. Assigned users will be created and
+ managed in your GitLab group.
+
+#### Okta Known Issues
+
+The Okta GitLab application currently only supports SCIM. Continue
+using the separate Okta [SAML SSO](index.md) configuration along with the new SCIM
+application described above.
+
## User access and linking setup
As long as [Group SAML](index.md) has been configured, prior to turning on sync, existing GitLab.com users can link to their accounts in one of the following ways, before synchronization is active:
@@ -135,7 +178,9 @@ Upon the next sync, the user will be deprovisioned, which means that the user wi
This section contains possible solutions for problems you might encounter.
-### How do I verify my SCIM configuration is correct?
+### Azure
+
+#### How do I verify my SCIM configuration is correct?
Review the following:
@@ -148,11 +193,11 @@ Review the following SCIM parameters for sensible values:
- `displayName`
- `emails[type eq "work"].value`
-### Testing Azure connection: invalid credentials
+#### Testing Azure connection: invalid credentials
When testing the connection, you may encounter an error: **You appear to have entered invalid credentials. Please confirm you are using the correct information for an administrative account**. If `Tenant URL` and `secret token` are correct, check whether your group path contains characters that may be considered invalid JSON primitives (such as `.`). Removing such characters from the group path typically resolves the error.
-### Azure: (Field) can't be blank sync error
+#### Azure: (Field) can't be blank sync error
When checking the Audit Logs for the Provisioning, you can sometimes see the
error `Namespace can't be blank, Name can't be blank, and User can't be blank.`
@@ -165,14 +210,7 @@ As a workaround, try an alternate mapping:
1. Delete the `name.formatted` target attribute entry.
1. Change the `displayName` source attribute to have `name.formatted` target attribute.
-### Message: "SAML authentication failed: Email has already been taken"
-
-This message may be caused by the following:
-
-- Existing users have not yet signed into the new app.
-- The identity provider attempts to create a new user account in GitLab with an email address that already exists in GitLab.com.
-
-### How do I diagnose why a user is unable to sign in
+#### How do I diagnose why a user is unable to sign in
The **Identity** (`extern_uid`) value stored by GitLab is updated by SCIM whenever `id` or `externalId` changes. Users won't be able to sign in unless the GitLab Identity (`extern_uid`) value matches the `NameId` sent by SAML.
@@ -180,7 +218,7 @@ This value is also used by SCIM to match users on the `id`, and is updated by SC
It is important that this SCIM `id` and SCIM `externalId` are configured to the same value as the SAML `NameId`. SAML responses can be traced using [debugging tools](./index.md#saml-debugging-tools), and any errors can be checked against our [SAML troubleshooting docs](./index.md#troubleshooting).
-### How do I verify user's SAML NameId matches the SCIM externalId
+#### How do I verify user's SAML NameId matches the SCIM externalId
Group owners can see the list of users and the `externalId` stored for each user in the group SAML SSO Settings page.
@@ -194,7 +232,7 @@ curl 'https://example.gitlab.com/api/scim/v2/groups/GROUP_NAME/Users?startIndex=
To see how this compares to the value returned as the SAML NameId, you can have the user use a [SAML Tracer](index.md#saml-debugging-tools).
-### Update or fix mismatched SCIM externalId and SAML NameId
+#### Update or fix mismatched SCIM externalId and SAML NameId
Whether the value was changed or you need to map to a different field, ensure `id`, `externalId`, and `NameId` all map to the same field.
@@ -220,7 +258,7 @@ curl --verbose --request PATCH 'https://gitlab.com/api/scim/v2/groups/YOUR_GROUP
It is important not to update these to incorrect values, since this will cause users to be unable to sign in. It is also important not to assign a value to the wrong user, as this would cause users to get signed into the wrong account.
-### I need to change my SCIM app
+#### I need to change my SCIM app
Individual users can follow the instructions in the ["SAML authentication failed: User has already been taken"](./index.md#i-need-to-change-my-saml-app) section.
diff --git a/doc/user/group/settings/img/export_panel.png b/doc/user/group/settings/img/export_panel.png
new file mode 100644
index 00000000000..2a987f04e35
--- /dev/null
+++ b/doc/user/group/settings/img/export_panel.png
Binary files differ
diff --git a/doc/user/group/settings/import_export.md b/doc/user/group/settings/import_export.md
new file mode 100644
index 00000000000..3f14fea673b
--- /dev/null
+++ b/doc/user/group/settings/import_export.md
@@ -0,0 +1,98 @@
+# Group Import/Export
+
+> - [Introduced](https://gitlab.com/groups/gitlab-org/-/epics/2888) in GitLab 13.0 as an experimental feature. May change in future releases.
+
+Existing groups running on any GitLab instance or GitLab.com can be exported with all their related data and moved to a
+new GitLab instance.
+
+The **GitLab import/export** button is displayed if the group import option in enabled.
+
+See also:
+
+- [Group Import/Export API](../../../api/group_import_export.md)
+- [Project Import/Export](../../project/settings/import_export.md)
+- [Project Import/Export API](../../../api/project_import_export.md)
+
+To enable GitLab import/export:
+
+1. Navigate to **{admin}** **Admin Area >** **{settings}** **Settings > Visibility and access controls**.
+1. Scroll to **Import sources**
+1. Enable desired **Import sources**
+
+## Important Notes
+
+Note the following:
+
+- Exports are stored in a temporary [shared directory](../../../development/shared_files.md) and are deleted every 24 hours by a specific worker.
+- To preserve group-level relationships from imported projects, run the Group Import/Export first, to allow projects to
+be imported into the desired group structure.
+- Imported groups are given a `private` visibility level, unless imported into a parent group.
+- If imported into a parent group, subgroups will inherit the same level of visibility unless otherwise restricted.
+- To preserve the member list and their respective permissions on imported groups, review the users in these groups. Make
+sure these users exist before importing the desired groups.
+
+### Exported Contents
+
+The following items will be exported:
+
+- Milestones
+- Labels
+- Boards and Board Lists
+- Badges
+- Subgroups (including all the aforementioned data)
+- Epics
+- Events
+
+The following items will NOT be exported:
+
+- Projects
+- Runners token
+- SAML discovery tokens
+
+NOTE: **Note:**
+For more details on the specific data persisted in a group export, see the
+[`import_export.yml`](https://gitlab.com/gitlab-org/gitlab/blob/master/lib/gitlab/import_export/group/import_export.yml) file.
+
+## Exporting a Group
+
+1. Navigate to your group's homepage.
+
+1. Click **{settings}** **Settings** in the sidebar.
+
+1. In the **Advanced** section, click the **Export Group** button.
+
+ ![Export group panel](img/export_panel.png)
+
+1. Once the export is generated, you should receive an e-mail with a link to the [exported contents](#exported-contents)
+ in a compressed tar archive, with contents in JSON format.
+
+1. Alternatively, you can come back to the project settings and download the
+ file from there by clicking **Download export**, or generate a new file by clicking **Regenerate export**.
+
+### Between CE and EE
+
+You can export groups from the [Community Edition to the Enterprise Edition](https://about.gitlab.com/install/ce-or-ee/) and vice versa.
+
+If you're exporting a group from the Enterprise Edition to the Community Edition, you may lose data that is retained only in the Enterprise Edition. For more information, see [downgrading from EE to CE](../../../README.md).
+
+## Version history
+
+GitLab can import bundles that were exported from a different GitLab deployment.
+This ability is limited to two previous GitLab [minor](../../../policy/maintenance.md#versioning)
+releases, which is similar to our process for [Security Releases](../../../policy/maintenance.md#security-releases).
+
+For example:
+
+| Current version | Can import bundles exported from |
+|-----------------|----------------------------------|
+| 13.0 | 13.0, 12.10, 12.9 |
+| 13.1 | 13.1, 13.0, 12.10 |
+
+## Rate Limits
+
+To help avoid abuse, users are rate limited to:
+
+| Request Type | Limit |
+| ---------------- | ------------------------------ |
+| Export | 1 group every 5 minutes |
+| Download export | 10 downloads every 10 minutes |
diff --git a/doc/user/group/subgroups/index.md b/doc/user/group/subgroups/index.md
index e73623a2f0e..49f6ddc3986 100644
--- a/doc/user/group/subgroups/index.md
+++ b/doc/user/group/subgroups/index.md
@@ -191,11 +191,6 @@ Here's a list of what you can't do with subgroups:
with `group`, but can be shared with `group/subgroup02` or
`group/subgroup01/subgroup03`.
-[ce-2772]: https://gitlab.com/gitlab-org/gitlab-foss/issues/2772
-[permissions]: ../../permissions.md#group-members-permissions
-[reserved]: ../../reserved_names.md
-[issue]: https://gitlab.com/gitlab-org/gitlab-foss/issues/30472#note_27747600
-
<!-- ## Troubleshooting
Include any troubleshooting steps that you can foresee. If you know beforehand what issues
diff --git a/doc/user/img/gitlab_snippet.png b/doc/user/img/gitlab_snippet.png
deleted file mode 100644
index 718347fc2d4..00000000000
--- a/doc/user/img/gitlab_snippet.png
+++ /dev/null
Binary files differ
diff --git a/doc/user/img/gitlab_snippet_v13_0.png b/doc/user/img/gitlab_snippet_v13_0.png
new file mode 100644
index 00000000000..33194c512df
--- /dev/null
+++ b/doc/user/img/gitlab_snippet_v13_0.png
Binary files differ
diff --git a/doc/user/img/snippet_clone_button_v13_0.png b/doc/user/img/snippet_clone_button_v13_0.png
new file mode 100644
index 00000000000..bf681e7349b
--- /dev/null
+++ b/doc/user/img/snippet_clone_button_v13_0.png
Binary files differ
diff --git a/doc/user/incident_management/index.md b/doc/user/incident_management/index.md
index 8505afb375e..73d6e0e055d 100644
--- a/doc/user/incident_management/index.md
+++ b/doc/user/incident_management/index.md
@@ -1,141 +1,112 @@
---
-description: "GitLab - Incident Management. GitLab offers solutions for handling incidents in your applications and services"
+stage: Monitor
+group: Health
+info: To determine the technical writer assigned to the Stage/Group associated with this page, see https://about.gitlab.com/handbook/engineering/ux/technical-writing/#designated-technical-writers
---
# Incident Management
GitLab offers solutions for handling incidents in your applications and services,
-from setting up an alert with Prometheus, to receiving a notification via a
-monitoring tool like Slack, and automatically setting up Zoom calls with your
-support team.
+from setting up an alert with Prometheus, to receiving a notification through a
+monitoring tool like Slack, and [setting up Zoom calls](#zoom-integration-in-issues) with your
+support team. Incidents can display [metrics](#embed-metrics-in-incidents-and-issues)
+and [logs](#view-logs-from-metrics-panel).
-## Configuring incidents **(ULTIMATE)**
+## Configure incidents **(ULTIMATE)**
> [Introduced](https://gitlab.com/gitlab-org/gitlab/issues/4925) in GitLab Ultimate 11.11.
-The Incident Management features can be enabled and disabled via your project's
-**Settings > Operations > Incidents**.
+You can enable or disable Incident Management features in your project's
+**{settings}** **Settings > Operations > Incidents**. Issues can be created for
+each alert triggered, and separate email notifications can be sent to users with
+[Developer permissions](../permissions.md). Appropriately configured alerts include an
+[embedded chart](../project/integrations/prometheus.md#embedding-metrics-based-on-alerts-in-incident-issues)
+for the query corresponding to the alert. You can also configure GitLab to
+[close issues](../project/integrations/prometheus.md#taking-action-on-incidents-ultimate)
+when you receive notification that the alert is resolved.
![Incident Management Settings](img/incident_management_settings.png)
-### Automatically create issues from alerts
+### Create issues from alerts
-GitLab issues can automatically be created as a result of an alert notification.
-An issue created this way will contain the error information to help you further
-debug it.
+You can create GitLab issues from an alert notification. These issues contain
+information about the alerts to help you diagnose the source of the alerts.
-### Issue templates
-
-You can create your own [issue templates](../project/description_templates.md#creating-issue-templates)
-that can be [used within Incident Management](../project/integrations/prometheus.md#taking-action-on-incidents-ultimate).
-
-To select your issue template for use within Incident Management:
-
-1. Visit your project's **Settings > Operations > Incidents**.
+1. Visit your project's **{settings}** **Settings > Operations > Incidents**.
+1. Select the **Create an issue** checkbox for GitLab to create an issue from
+ the incident.
1. Select the template from the **Issue Template** dropdown.
+ You can create your own [issue templates](../project/description_templates.md#creating-issue-templates)
+ to [use within Incident Management](../project/integrations/prometheus.md#taking-action-on-incidents-ultimate).
+1. Click **Save changes**.
-## Alerting
+## Notify developers of alerts
-GitLab can react to the alerts that your applications and services may be
-triggering by automatically creating issues, and alerting developers via email.
+GitLab can react to the alerts triggered from your applications and services
+by creating issues and alerting developers through email. GitLab sends these emails
+to [owners and maintainers](../permissions.md) of the project. They contain details
+of the alert, and a link for more information.
-The emails will be sent to [owners and maintainers](../permissions.md) of the project and will contain details on the alert as well as a link to see more information.
+### Configure Prometheus alerts
-### Prometheus alerts
+You can set up Prometheus alerts in:
-Prometheus alerts can be set up in both:
-
-- [GitLab-managed Prometheus](../project/integrations/prometheus.md#setting-up-alerts-for-prometheus-metrics) and
+- [GitLab-managed Prometheus](../project/integrations/prometheus.md#setting-up-alerts-for-prometheus-metrics) installations.
- [Self-managed Prometheus](../project/integrations/prometheus.md#external-prometheus-instances) installations.
-### Alert endpoint
-
-GitLab can accept alerts from any source via a generic webhook receiver. When
-you set up the generic alerts integration, a unique endpoint will
-be created which can receive a payload in JSON format.
-
-[Read more on setting this up, including how to customize the payload](../project/integrations/generic_alerts.md).
+Prometheus alerts are created by the special Alert Bot user. You can't remove this
+user, but it does not count toward your license limit.
-### Recovery alerts
+### Configure external generic alerts
-GitLab can [automatically close issues](../project/integrations/prometheus.md#taking-action-on-incidents-ultimate)
-that have been automatically created when you receive notification that the
-alert is resolved.
+GitLab can accept alerts from any source through a generic webhook receiver. When
+[configuring the generic alerts integration](../project/integrations/generic_alerts.md),
+GitLab creates a unique endpoint which receives a JSON-formatted, customizable payload.
-## Embedded metrics
+## Embed metrics in incidents and issues
-Metrics can be embedded anywhere where GitLab Markdown is used, for example,
-descriptions and comments on issues and merge requests.
-
-This can be useful for when you're sharing metrics, such as for discussing
-an incident or performance issues, so you can output the dashboard directly
+You can embed metrics anywhere GitLab Markdown is used, such as descriptions,
+comments on issues, and merge requests. Embedding metrics helps you share them
+when discussing incidents or performance issues. You can output the dashboard directly
into any issue, merge request, epic, or any other Markdown text field in GitLab
-by simply [copying and pasting the link to the metrics dashboard](../project/integrations/prometheus.md#embedding-gitlab-managed-kubernetes-metrics).
-
-TIP: **Tip:**
-Both GitLab-hosted and Grafana metrics can also be
-[embedded in issue templates](../project/integrations/prometheus.md#embedding-metrics-in-issue-templates).
-
-### GitLab-hosted metrics
+by [copying and pasting the link to the metrics dashboard](../project/integrations/prometheus.md#embedding-gitlab-managed-kubernetes-metrics).
-Learn how to embed [GitLab hosted metric charts](../project/integrations/prometheus.md#embedding-metric-charts-within-gitlab-flavored-markdown).
+You can embed both
+[GitLab-hosted metrics](../project/integrations/prometheus.md#embedding-metric-charts-within-gitlab-flavored-markdown) and
+[Grafana metrics](../project/integrations/prometheus.md#embedding-grafana-charts)
+in incidents and issue templates.
-#### Context menu
+### Context menu
From each of the embedded metrics panels, you can access more details
-about the data you are viewing from a context menu.
-
-You can access the context menu by clicking the **{ellipsis_v}** **More actions**
-dropdown box above the upper right corner of the panel:
-
-The options are:
+about the data you're viewing from a context menu. You can access the context menu
+by clicking the **{ellipsis_v}** **More actions** dropdown box above the
+upper right corner of the panel. The options are:
-- [View logs](#view-logs)
-- [Download CSV](#download-csv)
+- [View logs](#view-logs-from-metrics-panel).
+- **Download CSV** - Data from embedded charts can be
+ [downloaded as CSV](../project/integrations/prometheus.md#downloading-data-as-csv).
-##### View logs
+#### View logs from metrics panel
> - [Introduced](https://gitlab.com/gitlab-org/gitlab/issues/201846) in GitLab Ultimate 12.8.
> - [Moved](https://gitlab.com/gitlab-org/gitlab/-/merge_requests/25455) to [GitLab Core](https://about.gitlab.com/pricing/) 12.9.
-This can be useful if you are triaging an application incident and need to
-[explore logs](../project/integrations/prometheus.md#view-logs-ultimate)
-from across your application. It also helps you to understand
-what is affecting your application's performance and quickly resolve any problems.
-
-##### Download CSV
-
-Data from embedded charts can be [downloaded as CSV](../project/integrations/prometheus.md#downloading-data-as-csv).
-
-### Grafana metrics
-
-Learn how to embed [Grafana hosted metric charts](../project/integrations/prometheus.md#embedding-grafana-charts).
+Viewing logs from a metrics panel can be useful if you're triaging an application
+incident and need to [explore logs](../project/integrations/prometheus.md#view-logs-ultimate)
+from across your application. These logs help you understand what is affecting
+your application's performance and resolve any problems.
## Slack integration
-Slack slash commands allow you to control GitLab and view content right inside
-Slack, without having to leave it.
+Slack slash commands allow you to control GitLab and view GitLab content without leaving Slack.
Learn how to [set up Slack slash commands](../project/integrations/slack_slash_commands.md)
-and how to [use them](../../integration/slash_commands.md).
-
-### Slash commands
-
-Please refer to a list of [available slash commands](../../integration/slash_commands.md) and associated descriptions.
-
-## Zoom in issues
-
-In order to communicate synchronously for incidents management, GitLab allows you to
-associate a Zoom meeting with an issue. Once you start a Zoom call for a fire-fight,
-you need a way to associate the conference call with an issue, so that your team
-members can join swiftly without requesting a link.
-
-Read more how to [add or remove a zoom meeting](../project/issues/associate_zoom_meeting.md).
-
-### Configuring Incidents
-
-Incident Management features can be easily enabled & disabled via the Project settings page. Head to Project -> Settings -> Operations -> Incidents.
+and how to [use the available slash commands](../../integration/slash_commands.md).
-#### Auto-creation
+## Zoom integration in issues
-You can automatically create GitLab issues from an Alert notification. Issues created this way contain error information to help you debug the error. Appropriately configured alerts include an [embedded chart](../project/integrations/prometheus.md#embedding-metrics-based-on-alerts-in-incident-issues) for the query corresponding to the alert.
+GitLab enables you to [associate a Zoom meeting with an issue](../project/issues/associate_zoom_meeting.md)
+for synchronous communication during incident management. After starting a Zoom
+call for an incident, you can associate the conference call with an issue, so your
+team members can join without requesting a link.
diff --git a/doc/user/infrastructure/img/terraform_plan_log_v13_0.png b/doc/user/infrastructure/img/terraform_plan_log_v13_0.png
new file mode 100644
index 00000000000..c3c6f6b2f8b
--- /dev/null
+++ b/doc/user/infrastructure/img/terraform_plan_log_v13_0.png
Binary files differ
diff --git a/doc/user/infrastructure/img/terraform_plan_widget_v13_0.png b/doc/user/infrastructure/img/terraform_plan_widget_v13_0.png
new file mode 100644
index 00000000000..62bf4b279b2
--- /dev/null
+++ b/doc/user/infrastructure/img/terraform_plan_widget_v13_0.png
Binary files differ
diff --git a/doc/user/infrastructure/index.md b/doc/user/infrastructure/index.md
index a50cdf1cf0e..65237bf24e0 100644
--- a/doc/user/infrastructure/index.md
+++ b/doc/user/infrastructure/index.md
@@ -1,6 +1,343 @@
-# Infrastructure as Code
+---
+stage: Configure
+group: Configure
+info: To determine the technical writer assigned to the Stage/Group associated with this page, see https://about.gitlab.com/handbook/engineering/ux/technical-writing/#designated-technical-writers
+---
-GitLab can be used to manage infrastructure as code. The following are some examples:
+# Infrastructure as code with Terraform and GitLab
-- [A generic tutorial for Terraform with GitLab](https://medium.com/@timhberry/terraform-pipelines-in-gitlab-415b9d842596).
-- [Terraform at GitLab](https://about.gitlab.com/blog/2019/11/12/gitops-part-2/).
+## GitLab managed Terraform State
+
+[Terraform remote backends](https://www.terraform.io/docs/backends/index.html)
+enable you to store the state file in a remote, shared store. GitLab uses the
+[Terraform HTTP backend](https://www.terraform.io/docs/backends/types/http.html)
+to securely store the state files in local storage (the default) or
+[the remote store of your choice](../../administration/terraform_state.md).
+
+The GitLab managed Terraform state backend can store your Terraform state easily and
+securely, and spares you from setting up additional remote resources like
+Amazon S3 or Google Cloud Storage. Its features include:
+
+- Supporting encryption of the state file both in transit and at rest.
+- Locking and unlocking state.
+- Remote Terraform plan and apply execution.
+
+To get started, there are two different options when using GitLab managed Terraform State.
+
+- Use a local machine
+- Use GitLab CI
+
+## Get Started using local development
+
+If you are planning to only run `terraform plan` and `terraform apply` commands from your local machine, this is a simple way to get started.
+
+First, create your project on your GitLab instance.
+
+Next, define the Terraform backend in your Terraform project to be:
+
+```hcl
+terraform {
+ backend "http" {
+ }
+}
+```
+
+Finally, you need to run `terraform init` on your local machine and pass in the following options. The below example is using GitLab.com:
+
+```bash
+terraform init \
+ -backend-config="address=https://gitlab.com/api/v4/projects/<YOUR-PROJECT-ID>/terraform/state/<YOUR-PROJECT-NAME>" \
+ -backend-config="lock_address=https://gitlab.com/api/v4/projects/<YOUR-PROJECT-ID>/terraform/state/<YOUR-PROJECT-NAME>/lock" \
+ -backend-config="unlock_address=https://gitlab.com/api/v4/projects/<YOUR-PROJECT-ID>/terraform/state/<YOUR-PROJECT-NAME>/lock" \
+ -backend-config="username=<YOUR-USERNAME>" \
+ -backend-config="password=<YOUR-ACCESS-TOKEN>" \
+ -backend-config="lock_method=POST" \
+ -backend-config="unlock_method=DELETE" \
+ -backend-config="retry_wait_min=5"
+```
+
+This will initialize your Terraform state and store that state within your GitLab project.
+
+NOTE: YOUR-PROJECT-ID and YOUR-PROJECT-NAME can be accessed from the project main page.
+
+## Get Started using a GitLab CI
+
+Another route is to leverage GitLab CI to run your `terraform plan` and `terraform apply` commands.
+
+### Configure the CI variables
+
+To use the Terraform backend, [first create a Personal Access Token](../profile/personal_access_tokens.md) with the `api` scope. Keep in mind that the Terraform backend is restricted to tokens with [Maintainer access](../permissions.md) to the repository.
+
+To keep the Personal Access Token secure, add it as a [CI/CD environment variable](../../ci/variables/README.md). In this example we set ours to the ENV: `GITLAB_TF_PASSWORD`.
+
+If you are planning to use the ENV on a branch which is not protected, make sure to set the variable protection settings correctly.
+
+### Configure the Terraform backend
+
+Next we need to define the [http backend](https://www.terraform.io/docs/backends/types/http.html). In your Terraform project add the following code block in a `.tf` file such as `backend.tf` or wherever you desire to define the remote backend:
+
+```hcl
+terraform {
+ backend "http" {
+ }
+}
+```
+
+### Configure the CI YAML file
+
+Finally, configure a `.gitlab-ci.yaml`, which lives in the root of your project repository.
+
+In our case we are using a pre-built image:
+
+```yaml
+image:
+ name: hashicorp/terraform:light
+ entrypoint:
+ - '/usr/bin/env'
+ - 'PATH=/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin'
+```
+
+We then define some environment variables to make life easier. `GITLAB_TF_ADDRESS` is the URL of the GitLab instance where this pipeline runs, and `TF_ROOT` is the directory where the Terraform commands must be executed.
+
+```yaml
+variables:
+ GITLAB_TF_ADDRESS: ${CI_API_V4_URL}/projects/${CI_PROJECT_ID}/terraform/state/${CI_PROJECT_NAME}
+ TF_ROOT: ${CI_PROJECT_DIR}/environments/cloudflare/production
+
+cache:
+ paths:
+ - .terraform
+```
+
+In a `before_script`, pass a `terraform init` call containing configuration parameters.
+These parameters correspond to variables required by the
+[http backend](https://www.terraform.io/docs/backends/types/http.html):
+
+```yaml
+before_script:
+ - cd ${TF_ROOT}
+ - terraform --version
+ - terraform init -backend-config="address=${GITLAB_TF_ADDRESS}" -backend-config="lock_address=${GITLAB_TF_ADDRESS}/lock" -backend-config="unlock_address=${GITLAB_TF_ADDRESS}/lock" -backend-config="username=${GITLAB_USER_LOGIN}" -backend-config="password=${GITLAB_TF_PASSWORD}" -backend-config="lock_method=POST" -backend-config="unlock_method=DELETE" -backend-config="retry_wait_min=5"
+
+stages:
+ - validate
+ - build
+ - test
+ - deploy
+
+validate:
+ stage: validate
+ script:
+ - terraform validate
+
+plan:
+ stage: build
+ script:
+ - terraform plan
+ - terraform show
+
+apply:
+ stage: deploy
+ environment:
+ name: production
+ script:
+ - terraform apply
+ dependencies:
+ - plan
+ when: manual
+ only:
+ - master
+```
+
+### Push to GitLab
+
+Pushing your project to GitLab triggers a CI job pipeline, which runs the `terraform init`, `terraform validate`, and `terraform plan` commands automatically.
+
+The output from the above `terraform` commands should be viewable in the job logs.
+
+## Example project
+
+See [this reference project](https://gitlab.com/nicholasklick/gitlab-terraform-aws) using GitLab and Terraform to deploy a basic AWS EC2 within a custom VPC.
+
+## Output Terraform Plan information into a merge request
+
+Using the [GitLab Terraform Report Artifact](../../ci/pipelines/job_artifacts.md#artifactsreportsterraform),
+you can expose details from `terraform plan` runs directly into a merge request widget,
+enabling you to see statistics about the resources that Terraform will create,
+modify, or destroy.
+
+Let's explore how to configure a GitLab Terraform Report Artifact:
+
+1. First, for simplicity, let's define a few reusable variables to allow us to
+ refer to these files multiple times:
+
+ ```yaml
+ variables:
+ PLAN: plan.tfplan
+ PLAN_JSON: tfplan.json
+ ```
+
+1. Next we need to install `jq`, a [lightweight and flexible command-line JSON processor](https://stedolan.github.io/jq/). We will also create an alias for a specific `jq` command that parses out the extact information we want to extract from the `terraform plan` output:
+
+```yaml
+before_script:
+ - apk --no-cache add jq
+ - alias convert_report="jq -r '([.resource_changes[]?.change.actions?]|flatten)|{\"create\":(map(select(.==\"create\"))|length),\"update\":(map(select(.==\"update\"))|length),\"delete\":(map(select(.==\"delete\"))|length)}'"
+```
+
+1. Finally, we define a `script` that runs `terraform plan` and also a `terraform show` which pipes the output and converts the relevant bits into a store variable `PLAN_JSON`. This json is then leveraged to create a [GitLab Terraform Report Artifact](../../ci/pipelines/job_artifacts.md#artifactsreportsterraform).
+
+The terraform report obtains a Terraform tfplan.json file. The collected Terraform plan report will be uploaded to GitLab as an artifact and will be automatically shown in merge requests.
+
+```yaml
+plan:
+ stage: build
+ script:
+ - terraform plan -out=$PLAN
+ - terraform show --json $PLAN | convert_report > $PLAN_JSON
+ artifacts:
+ name: plan
+ paths:
+ - $PLAN
+ reports:
+ terraform: $PLAN_JSON
+```
+
+A full `.gitlab-ci.yaml` file could look like this:
+
+```yaml
+image:
+ name: hashicorp/terraform:light
+ entrypoint:
+ - '/usr/bin/env'
+ - 'PATH=/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin'
+
+# Default output file for Terraform plan
+variables:
+ GITLAB_TF_ADDRESS: ${CI_API_V4_URL}/projects/${CI_PROJECT_ID}/terraform/state/${CI_PROJECT_NAME}
+ PLAN: plan.tfplan
+ PLAN_JSON: tfplan.json
+ TF_ROOT: ${CI_PROJECT_DIR}
+
+cache:
+ paths:
+ - .terraform
+
+before_script:
+ - apk --no-cache add jq
+ - alias convert_report="jq -r '([.resource_changes[]?.change.actions?]|flatten)|{\"create\":(map(select(.==\"create\"))|length),\"update\":(map(select(.==\"update\"))|length),\"delete\":(map(select(.==\"delete\"))|length)}'"
+ - cd ${TF_ROOT}
+ - terraform --version
+ - terraform init -backend-config="address=${GITLAB_TF_ADDRESS}" -backend-config="lock_address=${GITLAB_TF_ADDRESS}/lock" -backend-config="unlock_address=${GITLAB_TF_ADDRESS}/lock" -backend-config="username=${GITLAB_USER_LOGIN}" -backend-config="password=${GITLAB_TF_PASSWORD}" -backend-config="lock_method=POST" -backend-config="unlock_method=DELETE" -backend-config="retry_wait_min=5"
+
+stages:
+ - validate
+ - build
+ - deploy
+
+validate:
+ stage: validate
+ script:
+ - terraform validate
+
+plan:
+ stage: build
+ script:
+ - terraform plan -out=$PLAN
+ - terraform show --json $PLAN | convert_report > $PLAN_JSON
+ artifacts:
+ name: plan
+ paths:
+ - ${TF_ROOT}/plan.tfplan
+ reports:
+ terraform: ${TF_ROOT}/tfplan.json
+
+# Separate apply job for manual launching Terraform as it can be destructive
+# action.
+apply:
+ stage: deploy
+ environment:
+ name: production
+ script:
+ - terraform apply -input=false $PLAN
+ dependencies:
+ - plan
+ when: manual
+ only:
+ - master
+
+```
+
+1. Running the pipeline displays the widget in the merge request, like this:
+
+ ![MR Terraform widget](img/terraform_plan_widget_v13_0.png)
+
+1. Clicking the **View Full Log** button in the widget takes you directly to the
+ plan output present in the pipeline logs:
+
+ ![Terraform plan logs](img/terraform_plan_log_v13_0.png)
+
+### Example `.gitlab-ci.yaml` file
+
+```yaml
+image:
+ name: hashicorp/terraform:light
+ entrypoint:
+ - '/usr/bin/env'
+ - 'PATH=/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin'
+
+# Default output file for Terraform plan
+variables:
+ GITLAB_TF_ADDRESS: ${CI_API_V4_URL}/projects/${CI_PROJECT_ID}/terraform/state/${CI_PROJECT_NAME}
+ PLAN: plan.tfplan
+ PLAN_JSON: tfplan.json
+ TF_ROOT: ${CI_PROJECT_DIR}
+
+cache:
+ paths:
+ - .terraform
+
+before_script:
+ - apk --no-cache add jq
+ - alias convert_report="jq -r '([.resource_changes[]?.change.actions?]|flatten)|{\"create\":(map(select(.==\"create\"))|length),\"update\":(map(select(.==\"update\"))|length),\"delete\":(map(select(.==\"delete\"))|length)}'"
+ - cd ${TF_ROOT}
+ - terraform --version
+ - terraform init -backend-config="address=${GITLAB_TF_ADDRESS}" -backend-config="lock_address=${GITLAB_TF_ADDRESS}/lock" -backend-config="unlock_address=${GITLAB_TF_ADDRESS}/lock" -backend-config="username=${GITLAB_USER_LOGIN}" -backend-config="password=${GITLAB_TF_PASSWORD}" -backend-config="lock_method=POST" -backend-config="unlock_method=DELETE" -backend-config="retry_wait_min=5"
+
+stages:
+ - validate
+ - build
+ - deploy
+
+validate:
+ stage: validate
+ script:
+ - terraform validate
+
+plan:
+ stage: build
+ script:
+ - terraform plan -out=$PLAN
+ - terraform show --json $PLAN | convert_report > $PLAN_JSON
+ artifacts:
+ name: plan
+ paths:
+ - ${TF_ROOT}/plan.tfplan
+ reports:
+ terraform: ${TF_ROOT}/tfplan.json
+
+# Separate apply job for manual launching Terraform as it can be destructive
+# action.
+apply:
+ stage: deploy
+ environment:
+ name: production
+ script:
+ - terraform apply -input=false $PLAN
+ dependencies:
+ - plan
+ when: manual
+ only:
+ - master
+
+```
diff --git a/doc/user/instance/clusters/index.md b/doc/user/instance/clusters/index.md
index 7b5ba14a5ae..495bfdeed6e 100644
--- a/doc/user/instance/clusters/index.md
+++ b/doc/user/instance/clusters/index.md
@@ -12,7 +12,7 @@ projects.
## Cluster precedence
-GitLab will try [to match](../../../ci/environments.md#scoping-environments-with-specs) clusters in
+GitLab will try [to match](../../../ci/environments/index.md#scoping-environments-with-specs) clusters in
the following order:
- Project-level clusters.
@@ -20,11 +20,11 @@ the following order:
- Instance-level clusters.
To be selected, the cluster must be enabled and
-match the [environment selector](../../../ci/environments.md#scoping-environments-with-specs).
+match the [environment selector](../../../ci/environments/index.md#scoping-environments-with-specs).
## Cluster environments **(PREMIUM)**
-For a consolidated view of which CI [environments](../../../ci/environments.md)
+For a consolidated view of which CI [environments](../../../ci/environments/index.md)
are deployed to the Kubernetes cluster, see the documentation for
[cluster environments](../../clusters/environments.md).
diff --git a/doc/user/instance_statistics/user_cohorts.md b/doc/user/instance_statistics/user_cohorts.md
index 45140d5e852..b3ef99473e4 100644
--- a/doc/user/instance_statistics/user_cohorts.md
+++ b/doc/user/instance_statistics/user_cohorts.md
@@ -26,3 +26,4 @@ How do we measure the activity of users? GitLab considers a user active if:
- The user has Git activity (whether push or pull).
- The user visits pages related to Dashboards, Projects, Issues, and Merge Requests ([introduced](https://gitlab.com/gitlab-org/gitlab-foss/issues/54947) in GitLab 11.8).
- The user uses the API
+- The user uses the GraphQL API
diff --git a/doc/user/markdown.md b/doc/user/markdown.md
index a1f83a47015..102eb1f8f53 100644
--- a/doc/user/markdown.md
+++ b/doc/user/markdown.md
@@ -105,7 +105,7 @@ changing how standard Markdown is used:
| Standard Markdown | Extended Markdown in GitLab |
| ------------------------------------- | ------------------------- |
-| [blockquotes](#blockquotes) | [multiline blockquotes](#multiline-blockquote) |
+| [blockquotes](#blockquotes) | [multi-line blockquotes](#multiline-blockquote) |
| [code blocks](#code-spans-and-blocks) | [colored code and syntax highlighting](#colored-code-and-syntax-highlighting) |
| [emphasis](#emphasis) | [multiple underscores in words](#multiple-underscores-in-words-and-mid-word-emphasis)
| [headers](#headers) | [linkable Header IDs](#header-ids-and-links) |
@@ -228,7 +228,7 @@ To make PlantUML available in GitLab, a GitLab administrator needs to enable it
> If this is not rendered correctly, [view it in GitLab itself](https://gitlab.com/gitlab-org/gitlab/blob/master/doc/user/markdown.md#emoji).
-```md
+```markdown
Sometimes you want to :monkey: around a bit and add some :star2: to your :speech_balloon:. Well we have a gift for you:
:zap: You can use emoji anywhere GFM is supported. :v:
@@ -353,7 +353,7 @@ However, the wrapping tags can't be mixed:
- [- deletion -}
```
-If your diff includes words in `` `code` `` font, make sure to escape each bactick `` ` `` with a
+If your diff includes words in `` `code` `` font, make sure to escape each backtick `` ` `` with a
backslash `\`, otherwise the diff highlight won't render correctly:
```markdown
@@ -396,8 +396,8 @@ a^2+b^2=c^2
_Be advised that KaTeX only supports a [subset](https://katex.org/docs/supported.html) of LaTeX._
-NOTE: **Note:** This also works for the asciidoctor `:stem: latexmath`. For details see
-the [asciidoctor user manual](https://asciidoctor.org/docs/user-manual/#activating-stem-support).
+NOTE: **Note:** This also works for the Asciidoctor `:stem: latexmath`. For details see
+the [Asciidoctor user manual](https://asciidoctor.org/docs/user-manual/#activating-stem-support).
### Special GitLab references
@@ -608,7 +608,7 @@ Quote break.
> If this is not rendered correctly, [view it in GitLab itself](https://gitlab.com/gitlab-org/gitlab/blob/master/doc/user/markdown.md#multiline-blockquote).
-GFM extends the standard Markdown standard by also supporting multiline blockquotes
+GFM extends the standard Markdown standard by also supporting multi-line blockquotes
fenced by `>>>`:
```markdown
@@ -804,7 +804,7 @@ but_emphasis is_desired _here_
If you wish to emphasize only a part of a word, it can still be done with asterisks:
-```md
+```markdown
perform*complicated*task
do*this*and*do*that*and*another thing
@@ -828,23 +828,31 @@ Reference tags can use letters and other characters. Avoid using lowercase `w` o
(`_`) in footnote tag names until [this bug](https://gitlab.com/gitlab-org/gitlab/issues/24423) is
resolved.
-```markdown
-A footnote reference tag looks like this: [^1]
+<!--
+Do not edit the following codeblock. It uses HTML to skip the Vale ReferenceLinks test.
+-->
+
+<pre class="highlight"><code>A footnote reference tag looks like this: [^1]
This reference tag is a mix of letters and numbers. [^footnote-42]
-[^1]: This is the text inside a footnote.
+&#91;^1]: This is the text inside a footnote.
-[^footnote-42]: This is another footnote.
-```
+&#91;^footnote-42]: This is another footnote.
+</code></pre>
A footnote reference tag looks like this:[^1]
This reference tag is a mix of letters and numbers.[^footnote-42]
-[^1]: This is the text inside a footnote.
+<!--
+Do not delete the single space before the [^1] and [^footnotes] references below.
+These are used to force the Vale ReferenceLinks check to skip these examples.
+-->
+
+ [^1]: This is the text inside a footnote.
-[^footnote-42]: This is another footnote.
+ [^footnote-42]: This is another footnote.
### Headers
@@ -928,8 +936,11 @@ ___
Examples:
-```markdown
-Inline-style (hover to see title text):
+<!--
+Do not edit the following codeblock. It uses HTML to skip the Vale ReferenceLinks test.
+-->
+
+<pre class="highlight"><code>Inline-style (hover to see title text):
![alt text](img/markdown_logo.png "Title Text")
@@ -937,12 +948,12 @@ Reference-style (hover to see title text):
![alt text1][logo]
-[logo]: img/markdown_logo.png "Title Text"
-```
+&#91;logo]: img/markdown_logo.png "Title Text"
+</code></pre>
<!--
-DO NOT change the name of markdown_logo.png. This is used for a test
-in spec/controllers/help_controller_spec.rb.
+DO NOT change the name of markdown_logo.png. This is used for a test in
+spec/controllers/help_controller_spec.rb.
-->
Inline-style (hover to see title text):
@@ -951,9 +962,12 @@ Inline-style (hover to see title text):
Reference-style (hover to see title text):
-![alt text][logo]
+<!--
+The example below uses an in-line link to pass the Vale ReferenceLinks test.
+Do not change to a reference style link.
+-->
-[logo]: img/markdown_logo.png "Title Text"
+![alt text](img/markdown_logo.png "Title Text")
#### Videos
@@ -962,7 +976,7 @@ Reference-style (hover to see title text):
Image tags that link to files with a video extension are automatically converted to
a video player. The valid video extensions are `.mp4`, `.m4v`, `.mov`, `.webm`, and `.ogv`:
-```md
+```markdown
Here's a sample video:
![Sample Video](img/markdown_video.mp4)
@@ -979,7 +993,7 @@ Here's a sample video:
Similar to videos, link tags for files with an audio extension are automatically converted to
an audio player. The valid audio extensions are `.mp3`, `.oga`, `.ogg`, `.spx`, and `.wav`:
-```md
+```markdown
Here's a sample audio clip:
![Sample Audio](img/markdown_audio.mp3)
@@ -1036,7 +1050,10 @@ are separated into their own lines:
</dl>
```
-<!-- Note: The example below uses HTML to force correct rendering on docs.gitlab.com, Markdown will be fine in GitLab -->
+<!--
+Note: The example below uses HTML to force correct rendering on docs.gitlab.com,
+Markdown will be fine in GitLab.
+-->
<dl>
<dt>Markdown in HTML</dt>
@@ -1102,7 +1119,10 @@ PASTE LOGS HERE
</details>
````
-<!-- Note: The example below uses HTML to force correct rendering on docs.gitlab.com, Markdown will be fine in GitLab -->
+<!--
+The example below uses HTML to force correct rendering on docs.gitlab.com, Markdown
+will work correctly in GitLab.
+-->
<details>
<summary>Click this to collapse/fold.</summary>
@@ -1167,8 +1187,11 @@ A new line due to the previous backslash.
There are two ways to create links, inline-style and reference-style:
-```markdown
-- This is an [inline-style link](https://www.google.com)
+<!--
+Do not edit the following codeblock. It uses HTML to skip the Vale ReferenceLinks test.
+-->
+
+<pre class="highlight"><code>- This is an [inline-style link](https://www.google.com)
- This is a [link to a repository file in the same directory](index.md)
- This is a [relative link to a readme one directory higher](../README.md)
- This is a [link that also has title text](https://www.google.com "This link takes you to Google!")
@@ -1186,14 +1209,14 @@ Using references:
Some text to show that the reference links can follow later.
-[arbitrary case-insensitive reference text]: https://www.mozilla.org/en-US/
-[1]: https://slashdot.org
-[link text itself]: https://www.reddit.com
-```
+&#91;arbitrary case-insensitive reference text]: https://www.mozilla.org/en-US/
+&#91;1]: https://slashdot.org
+&#91;link text itself]: https://www.reddit.com
+</code></pre>
- This is an [inline-style link](https://www.google.com)
- This is a [link to a repository file in the same directory](index.md)
-- This is a [relative link to a readme one directory higher](../README.md)
+- This is a [relative link to a README one directory higher](../README.md)
- This is a [link that also has title text](https://www.google.com "This link takes you to Google!")
Using header ID anchors:
@@ -1203,15 +1226,16 @@ Using header ID anchors:
Using references:
-- This is a [reference-style link, see below][Arbitrary case-insensitive reference text]
-- You can [use numbers for reference-style link definitions, see below][1]
-- Or leave it empty and use the [link text itself][], see below.
+<!--
+The example below uses in-line links to pass the Vale ReferenceLinks test.
+Do not change to reference style links.
+-->
-Some text to show that the reference links can follow later.
+- This is a [reference-style link, see below](https://www.mozilla.org/en-US/)
+- You can [use numbers for reference-style link definitions, see below](https://slashdot.org)
+- Or leave it empty and use the [link text itself](https://www.reddit.com), see below.
-[arbitrary case-insensitive reference text]: https://www.mozilla.org/en-US/
-[1]: https://slashdot.org
-[link text itself]: https://www.reddit.com
+Some text to show that the reference links can follow later.
NOTE: **Note:** Relative links do not allow the referencing of project files in a wiki
page, or a wiki page in a project file. The reason for this is that a wiki is always
@@ -1220,7 +1244,7 @@ will point the link to `wikis/style` only when the link is inside of a wiki Mark
#### URL auto-linking
-GFM will autolink almost any URL you put into your text:
+GFM will auto-link almost any URL you put into your text:
```markdown
- https://www.google.com
@@ -1251,7 +1275,7 @@ number, and count up from there.
Examples:
-```md
+```markdown
1. First ordered list item
2. Another item
- Unordered sub-list.
@@ -1261,8 +1285,11 @@ Examples:
4. And another item.
```
-<!-- The "2." and "4." in the example above are changed to "1." below, to match the style standards on docs.gitlab.com -->
-<!-- See https://docs.gitlab.com/ee/development/documentation/styleguide.html#lists -->
+<!--
+The "2." and "4." in the example above are changed to "1." below, to match the style
+standards on docs.gitlab.com.
+See https://docs.gitlab.com/ee/development/documentation/styleguide.html#lists
+-->
1. First ordered list item
1. Another item
@@ -1275,7 +1302,7 @@ Examples:
For an unordered list, add a `-`, `*` or `+`, followed by a space, at the start of
each line for unordered lists, but you should not use a mix of them.
-```md
+```markdown
Unordered lists can:
- use
@@ -1292,8 +1319,11 @@ They can even:
+ pluses
```
-<!-- The "*" and "+" in the example above are changed to "-" below, to match the style standards on docs.gitlab.com -->
-<!-- See https://docs.gitlab.com/ee/development/documentation/styleguide.html#lists -->
+<!--
+The "*" and "+" in the example above are changed to "-" below, to match the style
+standards on docs.gitlab.com.
+See https://docs.gitlab.com/ee/development/documentation/styleguide.html#lists
+-->
Unordered lists can:
diff --git a/doc/user/packages/conan_repository/index.md b/doc/user/packages/conan_repository/index.md
index ffbd8a848b5..a56a67d4959 100644
--- a/doc/user/packages/conan_repository/index.md
+++ b/doc/user/packages/conan_repository/index.md
@@ -20,7 +20,7 @@ by default. To enable it for existing projects, or if you want to disable it:
1. Find the Packages feature and enable or disable it.
1. Click on **Save changes** for the changes to take effect.
-You should then be able to see the **Packages** section on the left sidebar.
+You should then be able to see the **Packages & Registries** section on the left sidebar.
## Getting started
@@ -108,14 +108,19 @@ conan search Hello* --all --remote=gitlab
## Authenticating to the GitLab Conan Repository
-You will need to generate a [personal access token](../../../user/profile/personal_access_tokens.md) with the scope set to `api` for repository authentication.
+You will need a personal access token or deploy token.
+
+For repository authentication:
+
+- You can generate a [personal access token](../../../user/profile/personal_access_tokens.md) with the scope set to `api`.
+- You can generate a [deploy token](./../../project/deploy_tokens/index.md) with the scope set to `read_package_registry`, `write_package_registry`, or both.
### Adding a Conan user to the GitLab remote
Once you have a personal access token and have [set your Conan remote](#adding-the-gitlab-package-registry-as-a-conan-remote), you can associate the token with the remote so you do not have to explicitly add them to each Conan command you run:
```shell
-conan user <gitlab-username> -r gitlab -p <personal_access_token>
+conan user <gitlab_username or deploy_token_username> -r gitlab -p <personal_access_token or deploy_token>
```
Note: **Note**
@@ -130,7 +135,7 @@ Alternatively, you could explicitly include your credentials in any given comman
For example:
```shell
-CONAN_LOGIN_USERNAME=<gitlab-username> CONAN_PASSWORD=<personal_access_token> conan upload Hello/0.1@my-group+my-project/beta --all --remote=gitlab
+CONAN_LOGIN_USERNAME=<gitlab_username or deploy_token_username> CONAN_PASSWORD=<personal_access_token or deploy_token> conan upload Hello/0.1@my-group+my-project/beta --all --remote=gitlab
```
### Setting a default remote to your project (optional)
@@ -148,7 +153,7 @@ This functionality is best suited for when you want to consume or install packag
The rest of the example commands in this documentation assume that you have added a Conan user with your credentials to the `gitlab` remote and will not include the explicit credentials or remote option, but be aware that any of the commands could be run without having added a user or default remote:
```shell
-`CONAN_LOGIN_USERNAME=<gitlab-username> CONAN_PASSWORD=<personal_access_token> <conan command> --remote=gitlab
+`CONAN_LOGIN_USERNAME=<gitlab_username or deploy_token_username> CONAN_PASSWORD=<personal_access_token or deploy_token> <conan command> --remote=gitlab
```
## Uploading a package
@@ -274,7 +279,7 @@ To work with Conan commands within [GitLab CI/CD](./../../../ci/README.md), you
It is easiest to provide the `CONAN_LOGIN_USERNAME` and `CONAN_PASSWORD` with each
Conan command in your `.gitlab-ci.yml` file:
-```yml
+```yaml
image: conanio/gcc7
create_package:
diff --git a/doc/user/packages/container_registry/img/container_registry_group_repositories_v12_10.png b/doc/user/packages/container_registry/img/container_registry_group_repositories_v12_10.png
deleted file mode 100644
index e2b606d024f..00000000000
--- a/doc/user/packages/container_registry/img/container_registry_group_repositories_v12_10.png
+++ /dev/null
Binary files differ
diff --git a/doc/user/packages/container_registry/img/container_registry_group_repositories_v13_0.png b/doc/user/packages/container_registry/img/container_registry_group_repositories_v13_0.png
new file mode 100644
index 00000000000..14119abd56a
--- /dev/null
+++ b/doc/user/packages/container_registry/img/container_registry_group_repositories_v13_0.png
Binary files differ
diff --git a/doc/user/packages/container_registry/img/container_registry_repositories_v12_10.png b/doc/user/packages/container_registry/img/container_registry_repositories_v12_10.png
deleted file mode 100644
index 9e113be0a26..00000000000
--- a/doc/user/packages/container_registry/img/container_registry_repositories_v12_10.png
+++ /dev/null
Binary files differ
diff --git a/doc/user/packages/container_registry/img/container_registry_repositories_v13_0.png b/doc/user/packages/container_registry/img/container_registry_repositories_v13_0.png
new file mode 100644
index 00000000000..7286007b953
--- /dev/null
+++ b/doc/user/packages/container_registry/img/container_registry_repositories_v13_0.png
Binary files differ
diff --git a/doc/user/packages/container_registry/img/container_registry_repositories_with_quickstart_v12_10.png b/doc/user/packages/container_registry/img/container_registry_repositories_with_quickstart_v12_10.png
deleted file mode 100644
index e94aab58a1d..00000000000
--- a/doc/user/packages/container_registry/img/container_registry_repositories_with_quickstart_v12_10.png
+++ /dev/null
Binary files differ
diff --git a/doc/user/packages/container_registry/img/container_registry_repositories_with_quickstart_v13_0.png b/doc/user/packages/container_registry/img/container_registry_repositories_with_quickstart_v13_0.png
new file mode 100644
index 00000000000..f7c3aafcc8e
--- /dev/null
+++ b/doc/user/packages/container_registry/img/container_registry_repositories_with_quickstart_v13_0.png
Binary files differ
diff --git a/doc/user/packages/container_registry/img/container_registry_repository_details_v12.10.png b/doc/user/packages/container_registry/img/container_registry_repository_details_v12.10.png
deleted file mode 100644
index b911ffea935..00000000000
--- a/doc/user/packages/container_registry/img/container_registry_repository_details_v12.10.png
+++ /dev/null
Binary files differ
diff --git a/doc/user/packages/container_registry/img/container_registry_repository_details_v13.0.png b/doc/user/packages/container_registry/img/container_registry_repository_details_v13.0.png
new file mode 100644
index 00000000000..088e80221de
--- /dev/null
+++ b/doc/user/packages/container_registry/img/container_registry_repository_details_v13.0.png
Binary files differ
diff --git a/doc/user/packages/container_registry/img/container_registry_tags_v12_10.png b/doc/user/packages/container_registry/img/container_registry_tags_v12_10.png
deleted file mode 100644
index 40abc7eda9b..00000000000
--- a/doc/user/packages/container_registry/img/container_registry_tags_v12_10.png
+++ /dev/null
Binary files differ
diff --git a/doc/user/packages/container_registry/img/expiration-policy-app.png b/doc/user/packages/container_registry/img/expiration-policy-app.png
deleted file mode 100644
index e2d3d668e38..00000000000
--- a/doc/user/packages/container_registry/img/expiration-policy-app.png
+++ /dev/null
Binary files differ
diff --git a/doc/user/packages/container_registry/img/expiration_policy_app_v13_0.png b/doc/user/packages/container_registry/img/expiration_policy_app_v13_0.png
new file mode 100644
index 00000000000..93c9e00a8f5
--- /dev/null
+++ b/doc/user/packages/container_registry/img/expiration_policy_app_v13_0.png
Binary files differ
diff --git a/doc/user/packages/container_registry/index.md b/doc/user/packages/container_registry/index.md
index 5505a4503ca..5e642e1e21c 100644
--- a/doc/user/packages/container_registry/index.md
+++ b/doc/user/packages/container_registry/index.md
@@ -8,6 +8,7 @@
> login to GitLab's Container Registry.
> - Multiple level image names support was added in GitLab 9.1.
> - The group level Container Registry was [introduced](https://gitlab.com/gitlab-org/gitlab/-/issues/23315) in GitLab 12.10.
+> - Searching by image repository name was [introduced](https://gitlab.com/gitlab-org/gitlab/-/merge_requests/31322) in GitLab 13.0.
NOTE: **Note:**
This document is the user guide. To learn how to enable GitLab Container
@@ -19,14 +20,14 @@ have its own space to store its Docker images.
You can read more about Docker Registry at <https://docs.docker.com/registry/introduction/>.
-![Container Registry repositories](img/container_registry_repositories_v12_10.png)
+![Container Registry repositories](img/container_registry_repositories_v13_0.png)
## Enable the Container Registry for your project
CAUTION: **Warning:**
The Container Registry follows the visibility settings of the project. If the project is public, so is the Container Registry.
-If you cannot find the **Packages > Container Registry** entry under your
+If you cannot find the **Packages & Registries > Container Registry** entry under your
project's sidebar, it is not enabled in your GitLab instance. Ask your
administrator to enable GitLab Container Registry following the
[administration documentation](../../../administration/packages/container_registry.md).
@@ -44,7 +45,7 @@ project:
projects this might be enabled by default. For existing projects
(prior GitLab 8.8), you will have to explicitly enable it.
1. Press **Save changes** for the changes to take effect. You should now be able
- to see the **Packages > Container Registry** link in the sidebar.
+ to see the **Packages & Registries > Container Registry** link in the sidebar.
## Control Container Registry from within GitLab
@@ -53,13 +54,14 @@ for both projects and groups.
### Control Container Registry for your project
-Navigate to your project's **{package}** **Packages > Container Registry**.
+Navigate to your project's **{package}** **Packages & Registries > Container Registry**.
-![Container Registry project repositories](img/container_registry_repositories_with_quickstart_v12_10.png)
+![Container Registry project repositories](img/container_registry_repositories_with_quickstart_v13_0.png)
This view will:
- Show all the image repositories that belong to the project.
+- Allow you to filter image repositories by their name.
- Allow you to [delete](#delete-images-from-within-gitlab) one or more image repository.
- Allow you to navigate to the image repository details page.
- Show a **Quick start** dropdown with the most common commands to log in, build and push
@@ -67,9 +69,9 @@ This view will:
### Control Container Registry for your group
-Navigate to your groups's **{package}** **Packages > Container Registry**.
+Navigate to your groups's **{package}** **Packages & Registries > Container Registry**.
-![Container Registry group repositories](img/container_registry_group_repositories_v12_10.png)
+![Container Registry group repositories](img/container_registry_group_repositories_v13_0.png)
This view will:
@@ -81,7 +83,7 @@ This view will:
Clicking on the name of any image repository will navigate to the details.
-![Container Registry project repository details](img/container_registry_repository_details_v12.10.png)
+![Container Registry project repository details](img/container_registry_repository_details_v13.0.png)
NOTE: **Note:**
The following page has the same functionalities both in the **Group level container registry**
@@ -108,7 +110,7 @@ For more information on running Docker containers, visit the
## Authenticating to the GitLab Container Registry
-If you visit the **Packages > Container Registry** link under your project's
+If you visit the **Packages & Registries > Container Registry** link under your project's
menu, you can see the explicit instructions to login to the Container Registry
using your GitLab credentials.
@@ -135,7 +137,7 @@ The minimum scope needed for both of them is `read_registry`.
Example of using a token:
-```sh
+```shell
docker login registry.example.com -u <username> -p <token>
```
@@ -152,14 +154,14 @@ docker push registry.example.com/group/project/image
Your image will be named after the following scheme:
-```text
+```plaintext
<registry URL>/<namespace>/<project>/<image>
```
GitLab supports up to three levels of image repository names.
The following examples of image tags are valid:
-```text
+```plaintext
registry.example.com/group/project:some-tag
registry.example.com/group/project/image:latest
registry.example.com/group/project/my/image:rc1
@@ -204,7 +206,7 @@ Available for all projects, though more suitable for public ones:
your Docker images and has read/write access to the Registry. This is ephemeral,
so it's only valid for one job. You can use the following example as-is:
- ```sh
+ ```shell
docker login -u $CI_REGISTRY_USER -p $CI_REGISTRY_PASSWORD $CI_REGISTRY
```
@@ -219,7 +221,7 @@ For private and internal projects:
Replace the `<username>` and `<access_token>` in the following example:
- ```sh
+ ```shell
docker login -u <username> -p <access_token> $CI_REGISTRY
```
@@ -229,7 +231,7 @@ For private and internal projects:
Once created, you can use the special environment variables, and GitLab CI/CD
will fill them in for you. You can use the following example as-is:
- ```sh
+ ```shell
docker login -u $CI_DEPLOY_USER -p $CI_DEPLOY_PASSWORD $CI_REGISTRY
```
@@ -389,7 +391,7 @@ the deleted images.
To delete images from within GitLab:
-1. Navigate to your project's or group's **{package}** **Packages > Container Registry**.
+1. Navigate to your project's or group's **{package}** **Packages & Registries > Container Registry**.
1. From the **Container Registry** page, you can select what you want to delete,
by either:
@@ -401,7 +403,7 @@ To delete images from within GitLab:
1. In the dialog box, click **Remove tag**.
- ![Container Registry tags](img/container_registry_tags_v12_10.png)
+ ![Container Registry tags](img/container_registry_repository_details_v13.0.png)
### Delete images using the API
@@ -490,7 +492,7 @@ older tags and images are regularly removed from the Container Registry.
NOTE: **Note:**
Expiration policies for projects created before GitLab 12.8 may be enabled by an
admin in the [CI/CD Package Registry settings](./../../admin_area/settings/index.md#cicd).
-Note the inherant [risks involved](./index.md#use-with-external-container-registries).
+Note the inherent [risks involved](./index.md#use-with-external-container-registries).
It is possible to create a per-project expiration policy, so that you can make sure that
older tags and images are regularly removed from the Container Registry.
@@ -503,23 +505,25 @@ then goes through a process of excluding tags from it until only the ones to be
1. Evaluates the `name_regex`, excluding non-matching names from the list.
1. Excludes any tags that do not have a manifest (not part of the options).
1. Orders the remaining tags by `created_date`.
-1. Excludes from the list the N tags based on the `keep_n` value (Expiration latest).
+1. Excludes from the list the N tags based on the `keep_n` value (Number of tags to retain).
1. Excludes from the list the tags older than the `older_than` value (Expiration interval).
+1. Excludes from the list any tags matching the `name_regex_keep` value (Images to preserve).
1. Finally, the remaining tags in the list are deleted from the Container Registry.
### Managing project expiration policy through the UI
To manage project expiration policy, navigate to **{settings}** **Settings > CI/CD > Container Registry tag expiration policy**.
-![Expiration Policy App](img/expiration-policy-app.png)
+![Expiration Policy App](img/expiration_policy_app_v13_0.png)
The UI allows you to configure the following:
- **Expiration policy:** enable or disable the expiration policy.
- **Expiration interval:** how long tags are exempt from being deleted.
- **Expiration schedule:** how often the cron job checking the tags should run.
-- **Expiration latest:** how many tags to _always_ keep for each image.
+- **Number of tags to retain:** how many tags to _always_ keep for each image.
- **Docker tags with names matching this regex pattern will expire:** the regex used to determine what tags should be expired. To qualify all tags for expiration, use the default value of `.*`.
+- **Docker tags with names matching this regex pattern will be preserved:** the regex used to determine what tags should be preserved. To preserve all tags, use the default value of `.*`.
### Managing project expiration policy through the API
@@ -527,16 +531,10 @@ You can set, update, and disable the expiration policies using the GitLab API.
Examples:
-- Select all tags, keep at least 1 tag per image, expire any tag older than 14 days, run once a month, and the policy is enabled:
+- Select all tags, keep at least 1 tag per image, expire any tag older than 14 days, run once a month, preserve any images with the name `master` and the policy is enabled:
```shell
- curl --request PUT --header 'Content-Type: application/json;charset=UTF-8' --header "PRIVATE-TOKEN: <your_access_token>" --data-binary '{"container_expiration_policy_attributes":{"cadence":"1month","enabled":true,"keep_n":1,"older_than":"14d","name_regex":".*"}}' 'https://gitlab.example.com/api/v4/projects/2'
- ```
-
-- Select only tags with a name that contains `stable`, keep at least 50 tag per image, expire any tag older than 7 days, run every day, and the policy is enabled:
-
- ```shell
- curl --request PUT --header 'Content-Type: application/json;charset=UTF-8' --header "PRIVATE-TOKEN: <your_access_token>" --data-binary '{"container_expiration_policy_attributes":{"cadence":"1day","enabled":true,"keep_n":50"older_than":"7d","name_regex":"*stable"}}' 'https://gitlab.example.com/api/v4/projects/2'
+ curl --request PUT --header 'Content-Type: application/json;charset=UTF-8' --header "PRIVATE-TOKEN: <your_access_token>" --data-binary '{"container_expiration_policy_attributes":{"cadence":"1month","enabled":true,"keep_n":1,"older_than":"14d","name_regex":"","name_regex_delete":".*","name_regex_keep":".*-master"}}' 'https://gitlab.example.com/api/v4/projects/2'
```
See the API documentation for further details: [Edit project](../../../api/projects.md#edit-project).
@@ -544,7 +542,7 @@ See the API documentation for further details: [Edit project](../../../api/proje
### Use with external container registries
When using an [external container registry](./../../../administration/packages/container_registry.md#use-an-external-container-registry-with-gitlab-as-an-auth-endpoint),
-running an experation policy on a project may have some performance risks. If a project is going to run
+running an expiration policy on a project may have some performance risks. If a project is going to run
a policy that will remove large quantities of tags (in the thousands), the GitLab background jobs that
run the policy may get backed up or fail completely. It is recommended you only enable container expiration
policies for projects that were created before GitLab 12.8 if you are confident the amount of tags
@@ -552,10 +550,12 @@ being cleaned up will be minimal.
## Limitations
-Moving or renaming existing Container Registry repositories is not supported
+- Moving or renaming existing Container Registry repositories is not supported
once you have pushed images, because the images are signed, and the
signature includes the repository name. To move or rename a repository with a
Container Registry, you will have to delete all existing images.
+- Prior to GitLab 12.10, any tags that use the same image ID as the `latest` tag
+will not be deleted by the expiration policy.
## Troubleshooting the GitLab Container Registry
@@ -577,3 +577,47 @@ Troubleshooting the GitLab Container Registry, most of the times, requires
administration access to the GitLab server.
[Read how to troubleshoot the Container Registry](../../../administration/packages/container_registry.md#troubleshooting).
+
+### Unable to change path or transfer a project
+
+If you try to change a project's path or transfer a project to a new namespace,
+you may receive one of the following errors:
+
+- "Project cannot be transferred, because tags are present in its container registry."
+- "Namespace cannot be moved because at least one project has tags in container registry."
+
+This issue occurs when the project has images in the Container Registry.
+You must delete or move these images before you can change the path or transfer
+the project.
+
+The following procedure uses these sample project names:
+
+- For the current project: `example.gitlab.com/org/build/sample_project/cr:v2.9.1`
+- For the new project: `example.gitlab.com/new_org/build/new_sample_project/cr:v2.9.1`
+
+Use your own URLs to complete the following steps:
+
+1. Download the Docker images on your computer:
+
+ ```shell
+ docker login example.gitlab.com
+ docker pull example.gitlab.com/org/build/sample_project/cr:v2.9.1
+ ```
+
+1. Rename the images to match the new project name:
+
+ ```shell
+ docker tag example.gitlab.com/org/build/sample_project/cr:v2.9.1 example.gitlab.com/new_org/build/new_sample_project/cr:v2.9.1
+ ```
+
+1. Delete the images in both projects by using the [UI](#delete-images) or [API](../../../api/packages.md#delete-a-project-package).
+ There may be a delay while the images are queued and deleted.
+1. Change the path or transfer the project by going to **Settings > General**
+ and expanding **Advanced**.
+1. Restore the images:
+
+ ```shell
+ docker push example.gitlab.com/new_org/build/new_sample_project/cr:v2.9.1
+ ```
+
+Follow [this issue](https://gitlab.com/gitlab-org/gitlab/-/issues/18383) for details.
diff --git a/doc/user/packages/dependency_proxy/img/group_dependency_proxy.png b/doc/user/packages/dependency_proxy/img/group_dependency_proxy.png
index 0b94efdd83e..e550d296d5a 100644
--- a/doc/user/packages/dependency_proxy/img/group_dependency_proxy.png
+++ b/doc/user/packages/dependency_proxy/img/group_dependency_proxy.png
Binary files differ
diff --git a/doc/user/packages/dependency_proxy/index.md b/doc/user/packages/dependency_proxy/index.md
index cfdcd9821fb..be9710053dd 100644
--- a/doc/user/packages/dependency_proxy/index.md
+++ b/doc/user/packages/dependency_proxy/index.md
@@ -12,7 +12,7 @@ receiving a request and returning the upstream image from a registry, acting
as a pull-through cache.
The dependency proxy is available in the group level. To access it, navigate to
-a group's **Packages > Dependency Proxy**.
+a group's **Packages & Registries > Dependency Proxy**.
![Dependency Proxy group page](img/group_dependency_proxy.png)
@@ -33,7 +33,7 @@ The following dependency proxies are supported.
With the Docker dependency proxy, you can use GitLab as a source for a Docker image.
To get a Docker image into the dependency proxy:
-1. Find the proxy URL on your group's page under **Packages > Dependency Proxy**,
+1. Find the proxy URL on your group's page under **Packages & Registries > Dependency Proxy**,
for example `gitlab.com/groupname/dependency_proxy/containers`.
1. Trigger GitLab to pull the Docker image you want (e.g., `alpine:latest` or
`linuxserver/nextcloud:latest`) and store it in the proxy storage by using
diff --git a/doc/user/packages/img/group_packages_list_v12_10.png b/doc/user/packages/img/group_packages_list_v12_10.png
deleted file mode 100644
index ba9f2892961..00000000000
--- a/doc/user/packages/img/group_packages_list_v12_10.png
+++ /dev/null
Binary files differ
diff --git a/doc/user/packages/img/group_packages_list_v13_0.png b/doc/user/packages/img/group_packages_list_v13_0.png
new file mode 100644
index 00000000000..8cf3fd1a131
--- /dev/null
+++ b/doc/user/packages/img/group_packages_list_v13_0.png
Binary files differ
diff --git a/doc/user/packages/img/package_detail_v12_10.png b/doc/user/packages/img/package_detail_v12_10.png
deleted file mode 100644
index b2cd8e31955..00000000000
--- a/doc/user/packages/img/package_detail_v12_10.png
+++ /dev/null
Binary files differ
diff --git a/doc/user/packages/img/package_detail_v13_0.png b/doc/user/packages/img/package_detail_v13_0.png
new file mode 100644
index 00000000000..dcfbc0a4787
--- /dev/null
+++ b/doc/user/packages/img/package_detail_v13_0.png
Binary files differ
diff --git a/doc/user/packages/img/project_packages_list_v12_10.png b/doc/user/packages/img/project_packages_list_v12_10.png
deleted file mode 100644
index 2dfb92fa796..00000000000
--- a/doc/user/packages/img/project_packages_list_v12_10.png
+++ /dev/null
Binary files differ
diff --git a/doc/user/packages/img/project_packages_list_v13_0.png b/doc/user/packages/img/project_packages_list_v13_0.png
new file mode 100644
index 00000000000..468a6fe6467
--- /dev/null
+++ b/doc/user/packages/img/project_packages_list_v13_0.png
Binary files differ
diff --git a/doc/user/packages/index.md b/doc/user/packages/index.md
index 8e98dd70346..132a64d99a3 100644
--- a/doc/user/packages/index.md
+++ b/doc/user/packages/index.md
@@ -18,26 +18,28 @@ The Packages feature allows GitLab to act as a repository for the following:
## Enable the Package Registry for your project
-If you cannot find the **{package}** **Packages > List** entry under your
-project's sidebar, it is not enabled in your GitLab instance. Ask your
-administrator to enable GitLab Package Registry following the administration
-documentation.
+If you cannot find the **{package}** **Packages & Registries > Package
+Registry** entry under your project's sidebar, ensure that:
-Once enabled for your GitLab instance, to enable Package Registry for your
-project:
+1. The GitLab Package Registry has been enabled by your administrator (following
+ [this documentation](../../administration/packages/index.md)); and
+1. The Package Registry has been enabled for your project.
+
+Once an administrator has enabled the GitLab Package Registry for your GitLab
+instance, to enable Package Registry for your project:
1. Go to your project's **Settings > General** page.
1. Expand the **Visibility, project features, permissions** section and enable the
**Packages** feature on your project.
1. Press **Save changes** for the changes to take effect. You should now be able to
-see the **Packages > List** link in the sidebar.
+see the **Packages & Registries > Package Registry** link in the sidebar.
### View Packages for your project
-Navigating to your project's **{package}** **Packages > List** will show a list
+Navigating to your project's **{package}** **Packages & Registries > Package Registry** will show a list
of all packages that have been added to your project.
-![Project Packages list](img/project_packages_list_v12_10.png)
+![Project Packages list](img/project_packages_list_v13_0.png)
On this page, you can:
@@ -51,9 +53,9 @@ On this page, you can:
### View Packages for your group
You can view all packages belonging to a group by navigating to **{package}**
-**Packages > List** from the group sidebar.
+**Packages & Registries > Package Registry** from the group sidebar.
-![Group Packages list](img/group_packages_list_v12_10.png)
+![Group Packages list](img/group_packages_list_v13_0.png)
On this page, you can:
@@ -68,7 +70,7 @@ On this page, you can:
Additional package information can be viewed by browsing to the package details
page from the either the project or group list.
-![Package detail](img/package_detail_v12_10.png)
+![Package detail](img/package_detail_v13_0.png)
On this page you can:
@@ -112,7 +114,6 @@ are adding support for [PHP](https://gitlab.com/gitlab-org/gitlab/-/merge_reques
| [Opkg](https://gitlab.com/gitlab-org/gitlab/issues/36894) | Optimize your work with OpenWrt using Opkg repositories. |
| [P2](https://gitlab.com/gitlab-org/gitlab/issues/36895) | Host all your Eclipse plugins in your own GitLab P2 repository. |
| [Puppet](https://gitlab.com/gitlab-org/gitlab/issues/36897) | Configuration management meets repository management with Puppet repositories. |
-| [PyPi](https://gitlab.com/gitlab-org/gitlab/issues/10483) | Host PyPi distributions. |
| [RPM](https://gitlab.com/gitlab-org/gitlab/issues/5932) | Distribute RPMs directly from GitLab. |
| [RubyGems](https://gitlab.com/gitlab-org/gitlab/issues/803) | Use GitLab to host your own gems. |
| [SBT](https://gitlab.com/gitlab-org/gitlab/issues/36898) | Resolve dependencies from and deploy build output to SBT repositories when running SBT builds. |
diff --git a/doc/user/packages/maven_repository/index.md b/doc/user/packages/maven_repository/index.md
index b4aec11d029..51e62dc871e 100644
--- a/doc/user/packages/maven_repository/index.md
+++ b/doc/user/packages/maven_repository/index.md
@@ -21,17 +21,19 @@ to disable it:
1. Find the Packages feature and enable or disable it.
1. Click on **Save changes** for the changes to take effect.
-You should then be able to see the **Packages** section on the left sidebar.
+You should then be able to see the **Packages & Registries** section on the left sidebar.
Next, you must configure your project to authorize with the GitLab Maven
repository.
-## Getting Started
+## Getting Started with Maven
This section will cover installing Maven and building a package. This is a
quickstart to help if you're new to building Maven packages. If you're already
using Maven and understand how to build your own packages, move onto the
[next section](#adding-the-gitlab-package-registry-as-a-maven-remote).
+Maven repositories work well with Gradle, too. Move onto [getting started with Gradle](#getting-started-with-gradle) if you want to setup a Gradle project.
+
### Installing Maven
The required minimum versions are:
@@ -96,20 +98,129 @@ your project has been set up successfully:
You should see a new directory where you ran this command matching your
`DartifactId` parameter (in this case it should be `my-project`).
+## Getting started with Gradle
+
+This section will cover installing Gradle and initializing a Java project. This is a
+quickstart to help if you're new to Gradle. If you're already
+using Gradle and understand how to build your own packages, move onto the
+[next section](#adding-the-gitlab-package-registry-as-a-maven-remote).
+
+### Installing Gradle
+
+Installation is needed only if you want to create a new Gradle project. Follow
+instructions at [gradle.org](https://gradle.org/install/) to download and install
+Gradle for your local development environment.
+
+Verify you can use Gradle in your terminal by running:
+
+```shell
+gradle -version
+```
+
+If you want to use an existing Gradle project, installation is not necessary.
+Simply execute `gradlew` (on Linux) or `gradlew.bat` (on Windows) in the project
+directory instead.
+
+You should see something imilar to the below printed in the output:
+
+```plaintext
+------------------------------------------------------------
+Gradle 6.0.1
+------------------------------------------------------------
+
+Build time: 2019-11-18 20:25:01 UTC
+Revision: fad121066a68c4701acd362daf4287a7c309a0f5
+
+Kotlin: 1.3.50
+Groovy: 2.5.8
+Ant: Apache Ant(TM) version 1.10.7 compiled on September 1 2019
+JVM: 11.0.5 (Oracle Corporation 11.0.5+10)
+OS: Windows 10 10.0 amd64
+```
+
+### Creating a project in Gradle
+
+Understanding how to create a full Java project in Gradle is outside the scope of this
+guide, but you can follow the steps below to create a new project that can be
+published to the GitLab Package Registry.
+
+Start by opening your terminal and creating a directory where you would like to
+store the project in your environment. From inside the directory, you can run
+the following Maven command to initialize a new package:
+
+```shell
+gradle init
+```
+
+The output should be
+
+```plaintext
+Select type of project to generate:
+ 1: basic
+ 2: application
+ 3: library
+ 4: Gradle plugin
+Enter selection (default: basic) [1..4]
+```
+
+Enter `3` to create a new Library project. The output should be:
+
+```plaintext
+Select implementation language:
+ 1: C++
+ 2: Groovy
+ 3: Java
+ 4: Kotlin
+ 5: Scala
+ 6: Swift
+```
+
+Enter `3` to create a new Java Library project. The output should be:
+
+```plaintext
+Select build script DSL:
+ 1: Groovy
+ 2: Kotlin
+Enter selection (default: Groovy) [1..2]
+```
+
+Choose `1` to create a new Java Library project which will be described in Groovy DSL. The output should be:
+
+```plaintext
+Select test framework:
+ 1: JUnit 4
+ 2: TestNG
+ 3: Spock
+ 4: JUnit Jupiter
+```
+
+Choose `1` to initialize the project with JUnit 4 testing libraries. The output should be:
+
+```plaintext
+Project name (default: test):
+```
+
+Enter a project name or hit enter to use the directory name as project name.
+
## Adding the GitLab Package Registry as a Maven remote
The next step is to add the GitLab Package Registry as a Maven remote. If a
project is private or you want to upload Maven artifacts to GitLab,
credentials will need to be provided for authorization too. Support is available
-for [personal access tokens](#authenticating-with-a-personal-access-token) and
-[CI job tokens](#authenticating-with-a-ci-job-token) only.
-[Deploy tokens](../../project/deploy_tokens/index.md) and regular username/password
+for [personal access tokens](#authenticating-with-a-personal-access-token),
+[CI job tokens](#authenticating-with-a-ci-job-token), and
+[deploy tokens](../../project/deploy_tokens/index.md) only. Regular username/password
credentials do not work.
### Authenticating with a personal access token
To authenticate with a [personal access token](../../profile/personal_access_tokens.md),
-set the scope to `api` and add a corresponding section to your
+set the scope to `api` when creating one, and add it to your Maven or Gradle configuration
+files.
+
+#### Authenticating with a personal access token in Maven
+
+Add a corresponding section to your
[`settings.xml`](https://maven.apache.org/settings.html) file:
```xml
@@ -130,13 +241,43 @@ set the scope to `api` and add a corresponding section to your
</settings>
```
+#### Authenticating with a personal access token in Gradle
+
+Create a file `~/.gradle/gradle.properties` with the following content:
+
+```groovy
+gitLabPrivateToken=REPLACE_WITH_YOUR_PERSONAL_ACCESS_TOKEN
+```
+
+Add a repositories section to your
+[`build.gradle`](https://docs.gradle.org/current/userguide/tutorial_using_tasks.html)
+file:
+
+```groovy
+repositories {
+ maven {
+ url "https://<gitlab-url>/api/v4/groups/<group>/-/packages/maven"
+ name "GitLab"
+ credentials(HttpHeaderCredentials) {
+ name = 'Private-Token'
+ value = gitLabPrivateToken
+ }
+ authentication {
+ header(HttpHeaderAuthentication)
+ }
+ }
+}
+```
+
You should now be able to upload Maven artifacts to your project.
### Authenticating with a CI job token
-If you're using Maven with GitLab CI/CD, a CI job token can be used instead
+If you're using GitLab CI/CD, a CI job token can be used instead
of a personal access token.
+#### Authenticating with a CI job token in Maven
+
To authenticate with a CI job token, add a corresponding section to your
[`settings.xml`](https://maven.apache.org/settings.html) file:
@@ -161,6 +302,81 @@ To authenticate with a CI job token, add a corresponding section to your
You can read more on
[how to create Maven packages using GitLab CI/CD](#creating-maven-packages-with-gitlab-cicd).
+#### Authenticating with a CI job token in Gradle
+
+To authenticate with a CI job token, add a repositories section to your
+[`build.gradle`](https://docs.gradle.org/current/userguide/tutorial_using_tasks.html)
+file:
+
+```groovy
+repositories {
+ maven {
+ url "https://<gitlab-url>/api/v4/groups/<group>/-/packages/maven"
+ name "GitLab"
+ credentials(HttpHeaderCredentials) {
+ name = 'Job-Token'
+ value = '${CI_JOB_TOKEN}'
+ }
+ authentication {
+ header(HttpHeaderAuthentication)
+ }
+ }
+}
+```
+
+### Authenticating with a deploy token
+
+> [Introduced](https://gitlab.com/gitlab-org/gitlab/-/issues/213566) in [GitLab Premium](https://about.gitlab.com/pricing/) 13.0.
+
+To authenticate with a [deploy token](./../../project/deploy_tokens/index.md),
+set the scope to `api` when creating one, and add it to your Maven or Gradle configuration
+files.
+
+#### Authenticating with a deploy token in Maven
+
+Add a corresponding section to your
+[`settings.xml`](https://maven.apache.org/settings.html) file:
+
+```xml
+<settings>
+ <servers>
+ <server>
+ <id>gitlab-maven</id>
+ <configuration>
+ <httpHeaders>
+ <property>
+ <name>Deploy-Token</name>
+ <value>REPLACE_WITH_YOUR_DEPLOY_TOKEN</value>
+ </property>
+ </httpHeaders>
+ </configuration>
+ </server>
+ </servers>
+</settings>
+```
+
+#### Authenticating with a deploy token in Gradle
+
+To authenticate with a deploy token, add a repositories section to your
+[`build.gradle`](https://docs.gradle.org/current/userguide/tutorial_using_tasks.html)
+file:
+
+```groovy
+repositories {
+ maven {
+ url "https://<gitlab-url>/api/v4/groups/<group>/-/packages/maven"
+ name "GitLab"
+ credentials(HttpHeaderCredentials) {
+ name = 'Deploy-Token'
+ value = '<deploy-token>'
+ }
+ authentication {
+ header(HttpHeaderAuthentication)
+ }
+ }
+}
+```
+
## Configuring your project to use the GitLab Maven repository URL
To download and upload packages from GitLab, you need a `repository` and
@@ -185,7 +401,7 @@ the `distributionManagement` section.
### Project level Maven endpoint
The example below shows how the relevant `repository` section of your `pom.xml`
-would look like:
+would look like in Maven:
```xml
<repositories>
@@ -206,6 +422,17 @@ would look like:
</distributionManagement>
```
+The corresponding section in Gradle would look like this:
+
+```groovy
+repositories {
+ maven {
+ url "https://gitlab.com/api/v4/projects/PROJECT_ID/packages/maven"
+ name "GitLab"
+ }
+}
+```
+
The `id` must be the same with what you
[defined in `settings.xml`](#adding-the-gitlab-package-registry-as-a-maven-remote).
@@ -223,7 +450,7 @@ project's ID can be used for uploading.
### Group level Maven endpoint
-> [Introduced](https://gitlab.com/gitlab-org/gitlab/-/merge_requests/8798) in GitLab Premium 11.7.
+> [Introduced](https://gitlab.com/gitlab-org/gitlab/-/merge_requests/8798) in [GitLab Premium](https://about.gitlab.com/pricing/) 11.7.
If you rely on many packages, it might be inefficient to include the `repository` section
with a unique URL for each package. Instead, you can use the group level endpoint for
@@ -259,6 +486,17 @@ the `distributionManagement` section:
</distributionManagement>
```
+For Gradle, the corresponding repositories section would look like:
+
+```groovy
+repositories {
+ maven {
+ url "https://gitlab.com/api/v4/groups/GROUP_ID/-/packages/maven"
+ name "GitLab"
+ }
+}
+```
+
The `id` must be the same with what you
[defined in `settings.xml`](#adding-the-gitlab-package-registry-as-a-maven-remote).
@@ -275,7 +513,7 @@ For retrieving artifacts, you can use either the
### Instance level Maven endpoint
-> [Introduced](https://gitlab.com/gitlab-org/gitlab/-/merge_requests/8274) in GitLab Premium 11.7.
+> [Introduced](https://gitlab.com/gitlab-org/gitlab/-/merge_requests/8274) in [GitLab Premium](https://about.gitlab.com/pricing/) 11.7.
If you rely on many packages, it might be inefficient to include the `repository` section
with a unique URL for each package. Instead, you can use the instance level endpoint for
@@ -314,6 +552,17 @@ the `distributionManagement` section:
</distributionManagement>
```
+The corresponding repositories section in Gradle would look like:
+
+```groovy
+repositories {
+ maven {
+ url "https://gitlab.com/api/v4/packages/maven"
+ name "GitLab"
+ }
+}
+```
+
The `id` must be the same with what you
[defined in `settings.xml`](#adding-the-gitlab-package-registry-as-a-maven-remote).
@@ -333,7 +582,9 @@ project's ID can be used for uploading.
Once you have set up the [remote and authentication](#adding-the-gitlab-package-registry-as-a-maven-remote)
and [configured your project](#configuring-your-project-to-use-the-gitlab-maven-repository-url),
-test to upload a Maven artifact from a project of yours:
+test to upload a Maven artifact from a project of yours.
+
+### Upload using Maven
```shell
mvn deploy
@@ -353,7 +604,51 @@ You should also see that the upload was uploaded to the correct registry:
Uploading to gitlab-maven: https://gitlab.com/api/v4/projects/PROJECT_ID/packages/maven/com/mycompany/mydepartment/my-project/1.0-SNAPSHOT/my-project-1.0-20200128.120857-1.jar
```
-You can then navigate to your project's **Packages** page and see the uploaded
+### Upload using Gradle
+
+Add the Gradle plugin [`maven-publish`](https://docs.gradle.org/current/userguide/publishing_maven.html) to the plugins section:
+
+```groovy
+plugins {
+ id 'java'
+ id 'maven-publish'
+}
+```
+
+Add a `publishing` section:
+
+```groovy
+publishing {
+ publications {
+ library(MavenPublication) {
+ from components.java
+ }
+ }
+ repositories {
+ maven {
+ url "https://gitlab.com/api/v4/projects/<PROJECT_ID>/packages/maven"
+ credentials(HttpHeaderCredentials) {
+ name = "Private-Token"
+ value = gitLabPrivateToken // the variable resides in ~/.gradle/gradle.properties
+ }
+ authentication {
+ header(HttpHeaderAuthentication)
+ }
+ }
+ }
+}
+```
+
+Replace `PROJECT_ID` with your project ID which can be found on the home page
+of your project.
+
+Run the publish task:
+
+```shell
+gradle publish
+```
+
+You can then navigate to your project's **Packages & Registries** page and see the uploaded
artifacts or even delete them.
## Installing a package
@@ -362,7 +657,7 @@ Installing a package from the GitLab Package Registry requires that you set up
the [remote and authentication](#adding-the-gitlab-package-registry-as-a-maven-remote)
as above. Once this is completed, there are two ways for installaing a package.
-### Install with `mvn install`
+### Install using Maven with `mvn install`
Add the dependency manually to your project `pom.xml` file. To add the example
created above, the XML would look like:
@@ -388,7 +683,7 @@ downloaded from the GitLab Package Registry:
Downloading from gitlab-maven: http://gitlab.com/api/v4/projects/PROJECT_ID/packages/maven/com/mycompany/mydepartment/my-project/1.0-SNAPSHOT/my-project-1.0-20200128.120857-1.pom
```
-### Install with `mvn dependency:get`
+#### Install with `mvn dependency:get`
The second way to install packages is to use the Maven commands directly.
Inside your project directory, run:
@@ -404,6 +699,16 @@ TIP: **Tip:**
Both the XML block and Maven command are readily copy and pastable from the
Package details page, allowing for quick and easy installation.
+### Install using Gradle
+
+Add a [dependency](https://docs.gradle.org/current/userguide/declaring_dependencies.html) to build.gradle in the dependencies section:
+
+```groovy
+dependencies {
+ implementation 'com.mycompany.mydepartment:my-project:1.0-SNAPSHOT'
+}
+```
+
## Removing a package
In the packages view of your project page, you can delete packages by clicking
@@ -413,11 +718,15 @@ page.
## Creating Maven packages with GitLab CI/CD
Once you have your repository configured to use the GitLab Maven Repository,
-you can configure GitLab CI/CD to build new packages automatically. The example below
-shows how to create a new package each time the `master` branch is updated:
+you can configure GitLab CI/CD to build new packages automatically.
+
+### Creating Maven packages with GitLab CI/CD using Maven
+
+The example below shows how to create a new package each time the `master` branch
+is updated:
1. Create a `ci_settings.xml` file that will serve as Maven's `settings.xml` file.
- Add the server section with the same id you defined in your `pom.xml` file.
+ Add the server section with the same ID you defined in your `pom.xml` file.
For example, in our case it's `gitlab-maven`:
```xml
@@ -481,6 +790,31 @@ user's home location (in this case the user is `root` since it runs in a
Docker container), and Maven will utilize the configured CI
[environment variables](../../../ci/variables/README.md#predefined-environment-variables).
+### Creating Maven packages with GitLab CI/CD using Gradle
+
+The example below shows how to create a new package each time the `master` branch
+is updated:
+
+1. Make sure you use the Job-Token authentication as described in ["Authenticating with a CI job token in Gradle"](#authenticating-with-a-ci-job-token-in-gradle).
+
+1. Add a `deploy` job to your `.gitlab-ci.yml` file:
+
+ ```yaml
+ deploy:
+ image: gradle:latest
+ script:
+ - 'gradle publish'
+ only:
+ - master
+ ```
+
+1. Push those files to your repository.
+
+The next time the `deploy` job runs, it will copy `ci_settings.xml` to the
+user's home location (in this case the user is `root` since it runs in a
+Docker container), and Maven will use the configured CI
+[environment variables](../../../ci/variables/README.md#predefined-environment-variables).
+
## Troubleshooting
### Useful Maven command line options
diff --git a/doc/user/packages/npm_registry/index.md b/doc/user/packages/npm_registry/index.md
index e66b3d1ac63..b909646431b 100644
--- a/doc/user/packages/npm_registry/index.md
+++ b/doc/user/packages/npm_registry/index.md
@@ -23,7 +23,7 @@ by default. To enable it for existing projects, or if you want to disable it:
1. Find the Packages feature and enable or disable it.
1. Click on **Save changes** for the changes to take effect.
-You should then be able to see the **Packages** section on the left sidebar.
+You should then be able to see the **Packages & Registries** section on the left sidebar.
Before proceeding to authenticating with the GitLab NPM Registry, you should
get familiar with the package naming convention.
@@ -100,14 +100,15 @@ configure GitLab as a remote registry.
If a project is private or you want to upload an NPM package to GitLab,
credentials will need to be provided for authentication. [Personal access tokens](../../profile/personal_access_tokens.md)
+and [deploy tokens](../../project/deploy_tokens/index.md)
are preferred, but support is available for [OAuth tokens](../../../api/oauth2.md#resource-owner-password-credentials-flow).
-CAUTION: **2FA is only supported with personal access tokens:**
-If you have 2FA enabled, you need to use a [personal access token](../../profile/personal_access_tokens.md) with OAuth headers with the scope set to `api`. Standard OAuth tokens won't be able to authenticate to the GitLab NPM Registry.
+CAUTION: **Two-factor authentication (2FA) is only supported with personal access tokens:**
+If you have 2FA enabled, you need to use a [personal access token](../../profile/personal_access_tokens.md) with OAuth headers with the scope set to `api` or a [deploy token](../../project/deploy_tokens/index.md) with `read_package_registry` or `write_package_registry` scopes. Standard OAuth tokens won't be able to authenticate to the GitLab NPM Registry.
-### Authenticating with a personal access token
+### Authenticating with a personal access token or deploy token
-To authenticate with a [personal access token](../../profile/personal_access_tokens.md),
+To authenticate with a [personal access token](../../profile/personal_access_tokens.md) or [deploy token](../../project/deploy_tokens/index.md),
set your NPM configuration:
```shell
@@ -125,7 +126,7 @@ npm config set '//gitlab.com/api/v4/projects/<your_project_id>/packages/npm/:_au
```
Replace `<your_project_id>` with your project ID which can be found on the home page
-of your project and `<your_token>` with your personal access token.
+of your project and `<your_token>` with your personal access token or deploy token.
If you have a self-managed GitLab installation, replace `gitlab.com` with your
domain name.
@@ -160,7 +161,7 @@ Then, you could run `npm publish` either locally or via GitLab CI/CD:
> [Introduced](https://gitlab.com/gitlab-org/gitlab/issues/9104) in GitLab Premium 12.5.
-If you’re using NPM with GitLab CI/CD, a CI job token can be used instead of a personal access token.
+If you’re using NPM with GitLab CI/CD, a CI job token can be used instead of a personal access token or deploy token.
The token will inherit the permissions of the user that generates the pipeline.
Add a corresponding section to your `.npmrc` file:
@@ -195,7 +196,7 @@ you can upload an NPM package to your project:
npm publish
```
-You can then navigate to your project's **Packages** page and see the uploaded
+You can then navigate to your project's **Packages & Registries** page and see the uploaded
packages or even delete them.
If you attempt to publish a package with a name that already exists within
@@ -286,11 +287,11 @@ page.
## Publishing a package with CI/CD
To work with NPM commands within [GitLab CI/CD](./../../../ci/README.md), you can use
-`CI_JOB_TOKEN` in place of the personal access token in your commands.
+`CI_JOB_TOKEN` in place of the personal access token or deploy token in your commands.
A simple example `.gitlab-ci.yml` file for publishing NPM packages:
-```yml
+```yaml
image: node:latest
stages:
@@ -323,9 +324,9 @@ info Visit https://classic.yarnpkg.com/en/docs/cli/install for documentation abo
```
In this case, try adding this to your `.npmrc` file (and replace `<your_token>`
-with your personal access token):
+with your personal access token or deploy token):
-```text
+```plaintext
//gitlab.com/api/v4/projects/:_authToken=<your_token>
```
@@ -363,6 +364,14 @@ You do not need a token to run `npm install` unless your project is private (the
NPM_TOKEN=<your_token> npm install
```
+### `npm install` returns `npm ERR! 403 Forbidden`
+
+- Check that your token is not expired and has appropriate permissions.
+- Check if you have attempted to publish a package with a name that already exists within a given scope.
+- Ensure the scoped packages URL includes a trailing slash:
+ - Correct: `//gitlab.com/api/v4/packages/npm/`
+ - Incorrect: `//gitlab.com/api/v4/packages/npm`
+
## NPM dependencies metadata
> [Introduced](https://gitlab.com/gitlab-org/gitlab/issues/11867) in GitLab Premium 12.6.
diff --git a/doc/user/packages/nuget_repository/index.md b/doc/user/packages/nuget_repository/index.md
index ed936b546d2..d9efb3239a8 100644
--- a/doc/user/packages/nuget_repository/index.md
+++ b/doc/user/packages/nuget_repository/index.md
@@ -61,14 +61,16 @@ by default. To enable it for existing projects, or if you want to disable it:
1. Find the Packages feature and enable or disable it.
1. Click on **Save changes** for the changes to take effect.
-You should then be able to see the **Packages** section on the left sidebar.
+You should then be able to see the **Packages & Registries** section on the left sidebar.
## Adding the GitLab NuGet Repository as a source to NuGet
You will need the following:
- Your GitLab username.
-- A personal access token. You can generate a [personal access token](../../../user/profile/personal_access_tokens.md) with the scope set to `api` for repository authentication.
+- A personal access token or deploy token. For repository authentication:
+ - You can generate a [personal access token](../../../user/profile/personal_access_tokens.md) with the scope set to `api`.
+ - You can generate a [deploy token](./../../project/deploy_tokens/index.md) with the scope set to `read_package_registry`, `write_package_registry`, or both.
- A suitable name for your source.
- Your project ID which can be found on the home page of your project.
@@ -83,7 +85,7 @@ You can now add a new source to NuGet with:
To add the GitLab NuGet Repository as a source with `nuget`:
```shell
-nuget source Add -Name <source_name> -Source "https://gitlab-instance.example.com/api/v4/projects/<your_project_id>/packages/nuget/index.json" -UserName <gitlab_username> -Password <gitlab_personal_access_token>
+nuget source Add -Name <source_name> -Source "https://gitlab-instance.example.com/api/v4/projects/<your_project_id>/packages/nuget/index.json" -UserName <gitlab_username or deploy_token_username> -Password <gitlab_personal_access_token or deploy_token>
```
Where:
@@ -107,8 +109,8 @@ nuget source Add -Name "GitLab" -Source "https//gitlab.example/api/v4/projects/1
- **Location**: `https://gitlab.com/api/v4/projects/<your_project_id>/packages/nuget/index.json`
- Replace `<your_project_id>` with your project ID.
- If you have a self-managed GitLab installation, replace `gitlab.com` with your domain name.
- - **Username**: Your GitLab username
- - **Password**: Your personal access token
+ - **Username**: Your GitLab username or deploy token username
+ - **Password**: Your personal access token or deploy token
![Visual Studio Adding a NuGet source](img/visual_studio_adding_nuget_source.png)
@@ -131,8 +133,8 @@ To add the GitLab NuGet Repository as a source for .NET, create a file named `nu
</packageSources>
<packageSourceCredentials>
<gitlab>
- <add key="Username" value="<gitlab_username>" />
- <add key="ClearTextPassword" value="<gitlab_personal_access_token>" />
+ <add key="Username" value="<gitlab_username or deploy_token_username>" />
+ <add key="ClearTextPassword" value="<gitlab_personal_access_token or deploy_token>" />
</gitlab>
</packageSourceCredentials>
</configuration>
@@ -201,7 +203,7 @@ nuget install <package_id> -OutputDirectory <output_directory> \
Where:
-- `<package_id>` is the package id.
+- `<package_id>` is the package ID.
- `<output_directory>` is the output directory, where the package will be installed.
- `<package_version>` (Optional) is the package version.
- `<source_name>` (Optional) is the source name.
@@ -222,5 +224,5 @@ dotnet add package <package_id> \
Where:
-- `<package_id>` is the package id.
+- `<package_id>` is the package ID.
- `<package_version>` (Optional) is the package version.
diff --git a/doc/user/packages/pypi_repository/index.md b/doc/user/packages/pypi_repository/index.md
index 11d7b828813..979f7a3c966 100644
--- a/doc/user/packages/pypi_repository/index.md
+++ b/doc/user/packages/pypi_repository/index.md
@@ -26,10 +26,132 @@ by default. To enable it for existing projects, or if you want to disable it:
1. Find the Packages feature and enable or disable it.
1. Click on **Save changes** for the changes to take effect.
-You should then be able to see the **Packages** section on the left sidebar.
+You should then be able to see the **Packages & Registries** section on the left sidebar.
+
+## Getting started
+
+This section will cover creating a new example PyPi package to upload. This is a
+quickstart to test out the **GitLab PyPi Registry**. If you already understand how
+to build and publish your own packages, move on to the [next section](#adding-the-gitlab-pypi-repository-as-a-source).
+
+### Create a project
+
+Understanding how to create a full Python project is outside the scope of this
+guide, but you can create a small package to test out the registry. Start by
+creating a new directory called `MyPyPiPackage`:
+
+```shell
+mkdir MyPyPiPackage && cd MyPyPiPackage
+```
+
+After creating this, create another directory inside:
+
+```shell
+mkdir mypypipackage && cd mypypipackage
+```
+
+Create two new files inside this directory to set up the basic project:
+
+```shell
+touch __init__.py
+touch greet.py
+```
+
+Inside `greet.py`, add the following code:
+
+```python
+def SayHello():
+ print("Hello from MyPyPiPackage")
+ return
+```
+
+Inside the `__init__.py` file, add the following:
+
+```python
+from .greet import SayHello
+```
+
+Now that the basics of our project is completed, we can test that the code runs.
+Start the Python prompt inside your top `MyPyPiPackage` directory. Then run:
+
+```python
+>>> from mypypipackage import SayHello
+>>> SayHello()
+```
+
+You should see an output similar to the following:
+
+```plaintext
+Python 3.8.2 (v3.8.2:7b3ab5921f, Feb 24 2020, 17:52:18)
+[Clang 6.0 (clang-600.0.57)] on darwin
+Type "help", "copyright", "credits" or "license" for more information.
+>>> from mypypipackage import SayHello
+>>> SayHello()
+Hello from MyPyPiPackage
+```
+
+Once we've verified that the sample project is working as above, we can next
+work on creating a package.
+
+### Create a package
+
+Inside your `MyPyPiPackage` directory, we need to create a `setup.py` file. Run
+the following:
+
+```shell
+touch setup.py
+```
+
+This file contains all the information about our package. For more information
+about this file, see [creating setup.py](https://packaging.python.org/tutorials/packaging-projects/#creating-setup-py).
+For this guide, we don't need to extensively fill out this file, simply add the
+below to your `setup.py`:
+
+```python
+import setuptools
+
+setuptools.setup(
+ name="mypypipackage",
+ version="0.0.1",
+ author="Example Author",
+ author_email="author@example.com",
+ description="A small example package",
+ packages=setuptools.find_packages(),
+ classifiers=[
+ "Programming Language :: Python :: 3",
+ "License :: OSI Approved :: MIT License",
+ "Operating System :: OS Independent",
+ ],
+ python_requires='>=3.6',
+)
+```
+
+Save the file, then execute the setup like so:
+
+```shell
+python3 setup.py sdist bdist_wheel
+```
+
+If successful, you should be able to see the output in a newly created `dist`
+folder. Run:
+
+```shell
+ls dist
+```
+
+And confirm your output matches the below:
+
+```plaintext
+mypypipackage-0.0.1-py3-none-any.whl mypypipackage-0.0.1.tar.gz
+```
+
+Our package is now all set up and ready to be uploaded to the **GitLab PyPi
+Package Registry**. Before we do so, we next need to set up authentication.
## Adding the GitLab PyPi Repository as a source
+### Authenticating with a personal access token
+
You will need the following:
- A personal access token. You can generate a [personal access token](../../../user/profile/personal_access_tokens.md) with the scope set to `api` for repository authentication.
@@ -39,12 +161,37 @@ You will need the following:
Edit your `~/.pypirc` file and add the following:
```ini
+[distutils]
+index-servers =
+ gitlab
+
[gitlab]
repository = https://gitlab.com/api/v4/projects/<project_id>/packages/pypi
username = __token__
password = <your personal access token>
```
+### Authenticating with a deploy token
+
+You will need the following:
+
+- A deploy token. You can generate a [deploy token](./../../project/deploy_tokens/index.md) with the `read_package_registry` and/or `write_package_registry` scopes for repository authentication.
+- A suitable name for your source.
+- Your project ID which can be found on the home page of your project.
+
+Edit your `~/.pypirc` file and add the following:
+
+```ini
+[distutils]
+index-servers =
+ gitlab
+
+[gitlab]
+repository = https://gitlab.com/api/v4/projects/<project_id>/packages/pypi
+username = <deploy token username>
+password = <deploy token>
+```
+
## Uploading packages
When uploading packages, note that:
@@ -52,13 +199,33 @@ When uploading packages, note that:
- The maximum allowed size is 50 Megabytes.
- If you upload the same package with the same version multiple times, each consecutive upload
is saved as a separate file. When installing a package, GitLab will serve the most recent file.
-- When uploading packages to GitLab, they will not be displayed in the packages UI of your project
- immediately. It can take up to 10 minutes to process a package.
### Upload packages with Twine
-This section assumes that your project is properly built and you already [created a PyPi package with setuptools](https://packaging.python.org/tutorials/packaging-projects/).
-Upload your package using the following command:
+If you were following the guide above, then the `MyPyPiPackage` package should
+be ready to be uploaded. Run the following command:
+
+```shell
+python3 -m twine upload --repository gitlab dist/*
+```
+
+If successful, you should see the following:
+
+```plaintext
+Uploading distributions to https://gitlab.com/api/v4/projects/<your_project_id>/packages/pypi
+Uploading mypypipackage-0.0.1-py3-none-any.whl
+100%|███████████████████████████████████████████████████████████████████████████████████████████| 4.58k/4.58k [00:00<00:00, 10.9kB/s]
+Uploading mypypipackage-0.0.1.tar.gz
+100%|███████████████████████████████████████████████████████████████████████████████████████████| 4.24k/4.24k [00:00<00:00, 11.0kB/s]
+```
+
+This indicates that the package was uploaded successfully. You can then navigate
+to your project's **Packages & Registries** page and see the uploaded packages.
+
+If you did not follow the guide above, the you'll need to ensure your package
+has been properly built and you [created a PyPi package with setuptools](https://packaging.python.org/tutorials/packaging-projects/).
+
+You can then upload your package using the following command:
```shell
python -m twine upload --repository <source_name> dist/<package_file>
@@ -80,5 +247,22 @@ pip install --index-url https://__token__:<personal_access_token>@gitlab.com/api
Where:
- `<package_name>` is the package name.
-- `<personal_access_token>` is your personal access token.
-- `<project_id>` is your project id number.
+- `<personal_access_token>` is a personal access token with the `read_api` scope.
+- `<project_id>` is the project ID.
+
+If you were following the guide above and want to test installing the
+`MyPyPiPackage` package, you can run the following:
+
+```shell
+pip install mypypipackage --no-deps --index-url https://__token__:<personal_access_token>@gitlab.com/api/v4/projects/<your_project_id>/packages/pypi/simple
+```
+
+This should result in the following:
+
+```plaintext
+Looking in indexes: https://__token__:****@gitlab.com/api/v4/projects/<your_project_id>/packages/pypi/simple
+Collecting mypypipackage
+ Downloading https://gitlab.com/api/v4/projects/<your_project_id>/packages/pypi/files/d53334205552a355fee8ca35a164512ef7334f33d309e60240d57073ee4386e6/mypypipackage-0.0.1-py3-none-any.whl (1.6 kB)
+Installing collected packages: mypypipackage
+Successfully installed mypypipackage-0.0.1
+```
diff --git a/doc/user/permissions.md b/doc/user/permissions.md
index 5a631cc59e3..e5893b291dc 100644
--- a/doc/user/permissions.md
+++ b/doc/user/permissions.md
@@ -42,7 +42,7 @@ NOTE: **Note:**
In GitLab 11.0, the Master role was renamed to Maintainer.
While Maintainer is the highest project-level role, some actions can only be performed by a personal namespace or group owner,
-or an instance admin, who receives all permissions.
+or an instance admin, who receives all permissions. For more information, see [projects members documentation](project/members/index.md).
The following table depicts the various user permission levels in a project.
@@ -50,7 +50,6 @@ The following table depicts the various user permission levels in a project.
|---------------------------------------------------|---------|------------|-------------|----------|--------|
| Download project | ✓ (*1*) | ✓ | ✓ | ✓ | ✓ |
| Leave comments | ✓ (*1*) | ✓ | ✓ | ✓ | ✓ |
-| View Insights charts **(ULTIMATE)** | ✓ | ✓ | ✓ | ✓ | ✓ |
| View approved/blacklisted licenses **(ULTIMATE)** | ✓ (*1*) | ✓ | ✓ | ✓ | ✓ |
| View License Compliance reports **(ULTIMATE)** | ✓ (*1*) | ✓ | ✓ | ✓ | ✓ |
| View Security reports **(ULTIMATE)** | ✓ (*3*) | ✓ | ✓ | ✓ | ✓ |
@@ -71,6 +70,7 @@ The following table depicts the various user permission levels in a project.
| View confidential issues | (*2*) | ✓ | ✓ | ✓ | ✓ |
| View [Releases](project/releases/index.md) | ✓ (*6*) | ✓ | ✓ | ✓ | ✓ |
| View requirements **(ULTIMATE)** | ✓ | ✓ | ✓ | ✓ | ✓ |
+| Manage user-starred metrics dashboards (*7*) | ✓ | ✓ | ✓ | ✓ | ✓ |
| Assign issues | | ✓ | ✓ | ✓ | ✓ |
| Label issues | | ✓ | ✓ | ✓ | ✓ |
| Set issue weight | | ✓ | ✓ | ✓ | ✓ |
@@ -83,15 +83,13 @@ The following table depicts the various user permission levels in a project.
| See a container registry | | ✓ | ✓ | ✓ | ✓ |
| See environments | | ✓ | ✓ | ✓ | ✓ |
| See a list of merge requests | | ✓ | ✓ | ✓ | ✓ |
-| View project statistics | | ✓ | ✓ | ✓ | ✓ |
+| View project statistics | | | ✓ | ✓ | ✓ |
| View Error Tracking list | | ✓ | ✓ | ✓ | ✓ |
| Create new merge request | | ✓ | ✓ | ✓ | ✓ |
| View metrics dashboard annotations | | ✓ | ✓ | ✓ | ✓ |
| Create/edit requirements **(ULTIMATE)** | | ✓ | ✓ | ✓ | ✓ |
-| Pull [packages](packages/index.md) | | ✓ | ✓ | ✓ | ✓ |
-| Publish [packages](packages/index.md) | | | ✓ | ✓ | ✓ |
-| Pull from [Conan repository](packages/conan_repository/index.md), [Maven repository](packages/maven_repository/index.md), or [NPM registry](packages/npm_registry/index.md) **(PREMIUM)** | | ✓ | ✓ | ✓ | ✓ |
-| Publish to [Conan repository](packages/conan_repository/index.md), [Maven repository](packages/maven_repository/index.md), or [NPM registry](packages/npm_registry/index.md) **(PREMIUM)** | | | ✓ | ✓ | ✓ |
+| Pull [packages](packages/index.md) **(PREMIUM)** | | ✓ | ✓ | ✓ | ✓ |
+| Publish [packages](packages/index.md) **(PREMIUM)**| | | ✓ | ✓ | ✓ |
| Upload [Design Management](project/issues/design_management.md) files | | | ✓ | ✓ | ✓ |
| Create/edit/delete [Releases](project/releases/index.md)| | | ✓ | ✓ | ✓ |
| Create new branches | | | ✓ | ✓ | ✓ |
@@ -142,20 +140,28 @@ The following table depicts the various user permission levels in a project.
| Manage GitLab Pages domains and certificates | | | | ✓ | ✓ |
| Remove GitLab Pages | | | | ✓ | ✓ |
| Manage clusters | | | | ✓ | ✓ |
-| View Pods logs **(ULTIMATE)** | | | | ✓ | ✓ |
+| View Pods logs | | | | ✓ | ✓ |
| Manage license policy **(ULTIMATE)** | | | | ✓ | ✓ |
| Edit comments (posted by any user) | | | | ✓ | ✓ |
| Manage Error Tracking | | | | ✓ | ✓ |
| Delete wiki pages | | | | ✓ | ✓ |
| View project Audit Events | | | | ✓ | ✓ |
| Manage [push rules](../push_rules/push_rules.md) | | | | ✓ | ✓ |
+| Manage [project access tokens](./project/settings/project_access_tokens.md) **(CORE ONLY)** | | | | ✓ | ✓ |
| Switch visibility level | | | | | ✓ |
| Transfer project to another namespace | | | | | ✓ |
+| Remove fork relationship | | | | | ✓ |
| Remove project | | | | | ✓ |
| Delete issues | | | | | ✓ |
| Disable notification emails | | | | | ✓ |
| Force push to protected branches (*4*) | | | | | |
| Remove protected branches (*4*) | | | | | |
+| View CI\CD analytics | | ✓ | ✓ | ✓ | ✓ |
+| View Code Review analytics **(STARTER)** | | ✓ | ✓ | ✓ | ✓ |
+| View Insights **(ULTIMATE)** | ✓ | ✓ | ✓ | ✓ | ✓ |
+| View Issues analytics **(PREMIUM)** | ✓ | ✓ | ✓ | ✓ | ✓ |
+| View Repository analytics | | ✓ | ✓ | ✓ | ✓ |
+| View Value Stream analytics | ✓ | ✓ | ✓ | ✓ | ✓ |
\* Owner permission is only available at the group or personal namespace level (and for instance admins) and is inherited by its projects.
@@ -165,6 +171,7 @@ The following table depicts the various user permission levels in a project.
1. Not allowed for Guest, Reporter, Developer, Maintainer, or Owner. See [Protected Branches](./project/protected_branches.md).
1. If the [branch is protected](./project/protected_branches.md#using-the-allowed-to-merge-and-allowed-to-push-settings), this depends on the access Developers and Maintainers are given.
1. Guest users can access GitLab [**Releases**](project/releases/index.md) for downloading assets but are not allowed to download the source code nor see repository information like tags and commits.
+1. Actions are limited only to records owned (referenced) by user.
## Project features permissions
@@ -228,6 +235,8 @@ group.
| Create/edit group epic **(ULTIMATE)** | | ✓ | ✓ | ✓ | ✓ |
| Manage group labels | | ✓ | ✓ | ✓ | ✓ |
| See a container registry | | ✓ | ✓ | ✓ | ✓ |
+| Pull [packages](packages/index.md) **(PREMIUM)** | | ✓ | ✓ | ✓ | ✓ |
+| Publish [packages](packages/index.md) **(PREMIUM)** | | | ✓ | ✓ | ✓ |
| View metrics dashboard annotations | | ✓ | ✓ | ✓ | ✓ |
| Create project in group | | | ✓ (3) | ✓ (3) | ✓ (3) |
| Create/edit/delete group milestones | | | ✓ | ✓ | ✓ |
@@ -244,6 +253,11 @@ group.
| Delete group epic **(ULTIMATE)** | | | | | ✓ |
| View group Audit Events | | | | | ✓ |
| Disable notification emails | | | | | ✓ |
+| View Contribution analytics | ✓ | ✓ | ✓ | ✓ | ✓ |
+| View Insights **(ULTIMATE)** | ✓ | ✓ | ✓ | ✓ | ✓ |
+| View Issues analytics **(PREMIUM)** | ✓ | ✓ | ✓ | ✓ | ✓ |
+| View Productivity analytics **(PREMIUM)** | | ✓ | ✓ | ✓ | ✓ |
+| View Value Stream analytics | ✓ | ✓ | ✓ | ✓ | ✓ |
1. Groups can be set to [allow either Owners or Owners and
Maintainers to create subgroups](group/subgroups/index.md#creating-a-subgroup)
@@ -391,7 +405,9 @@ instance and project. In addition, all admins can use the admin interface under
| See events in the system | | | | ✓ |
| Admin interface | | | | ✓ |
-1. Only if the job was triggered by the user
+1. Only if the job was:
+ - Triggered by the user
+ - [Since GitLab 13.0](https://gitlab.com/gitlab-org/gitlab/-/issues/35069), not run for a protected branch
### Job permissions
diff --git a/doc/user/profile/img/change_password_v13_0.png b/doc/user/profile/img/change_password_v13_0.png
new file mode 100644
index 00000000000..f63b32557ac
--- /dev/null
+++ b/doc/user/profile/img/change_password_v13_0.png
Binary files differ
diff --git a/doc/user/profile/img/unknown_sign_in_email_v13_0.png b/doc/user/profile/img/unknown_sign_in_email_v13_0.png
new file mode 100644
index 00000000000..51a7c29cdfa
--- /dev/null
+++ b/doc/user/profile/img/unknown_sign_in_email_v13_0.png
Binary files differ
diff --git a/doc/user/profile/index.md b/doc/user/profile/index.md
index 66ee19437ae..383c7fe73aa 100644
--- a/doc/user/profile/index.md
+++ b/doc/user/profile/index.md
@@ -17,6 +17,11 @@ There are several ways to create users on GitLab. See the [creating users docume
There are several ways to sign into your GitLab account.
See the [authentication topic](../../topics/authentication/index.md) for more details.
+### Unknown sign-in
+
+GitLab will notify you if a sign-in occurs that is from an unknown IP address.
+See [Unknown Sign-In Notification](unknown_sign_in_notification.md) for more details.
+
## User profile
To access your profile:
@@ -44,6 +49,7 @@ To access your profile settings:
From there, you can:
- Update your personal information
+- Change your [password](#changing-your-password)
- Set a [custom status](#current-status) for your profile
- Manage your [commit email](#commit-email) for your profile
- Manage [2FA](account/two_factor_authentication.md)
@@ -60,6 +66,18 @@ From there, you can:
- [View your active sessions](active_sessions.md) and revoke any of them if necessary
- Access your audit log, a security log of important events involving your account
+## Changing your password
+
+1. Navigate to your [profile's](#profile-settings) **Settings > Password**.
+1. Enter your current password in the 'Current password' field.
+1. Enter your desired new password twice, once in the 'New password' field and
+ once in the 'Password confirmation' field.
+1. Click the 'Save password' button.
+
+If you don't know your current password, select the 'I forgot my password' link.
+
+![Change your password](./img/change_password_v13_0.png)
+
## Changing your username
Your `username` is a unique [`namespace`](../group/index.md#namespaces)
diff --git a/doc/user/profile/notifications.md b/doc/user/profile/notifications.md
index 1d92f15552d..ae00f3ace57 100644
--- a/doc/user/profile/notifications.md
+++ b/doc/user/profile/notifications.md
@@ -83,6 +83,9 @@ Or:
1. Click the notification dropdown, marked with a bell icon.
1. Select the desired [notification level](#notification-levels).
+<i class="fa fa-youtube-play youtube" aria-hidden="true"></i>
+For a demonstration of how to be notified when a new release is available, see [Notification for releases](https://www.youtube.com/watch?v=qyeNkGgqmH4).
+
#### Group notifications
You can select a notification level and email address for each group.
@@ -208,17 +211,17 @@ The following table lists all GitLab-specific email headers:
| Header | Description |
|------------------------------------|-------------------------------------------------------------------------|
-| X-GitLab-Group-Id **(PREMIUM)** | The group's ID. Only present on notification emails for epics. |
-| X-GitLab-Group-Path **(PREMIUM)** | The group's path. Only present on notification emails for epics. |
-| X-GitLab-Project | The name of the project the notification belongs to. |
-| X-GitLab-Project-Id | The project's ID. |
-| X-GitLab-Project-Path | The project's path. |
-| X-GitLab-(Resource)-ID | The ID of the resource the notification is for. The resource, for example, can be `Issue`, `MergeRequest`, `Commit`, or another such resource. |
-| X-GitLab-Discussion-ID | The ID of the thread the comment belongs to, in notification emails for comments. |
-| X-GitLab-Pipeline-Id | The ID of the pipeline the notification is for, in notification emails for pipelines. |
-| X-GitLab-Reply-Key | A unique token to support reply by email. |
-| X-GitLab-NotificationReason | The reason for the notification. This can be `mentioned`, `assigned`, or `own_activity`. |
-| List-Id | The path of the project in an RFC 2919 mailing list identifier. This is useful for email organization with filters, for example. |
+| `X-GitLab-Group-Id` **(PREMIUM)** | The group's ID. Only present on notification emails for epics. |
+| `X-GitLab-Group-Path` **(PREMIUM)** | The group's path. Only present on notification emails for epics. |
+| `X-GitLab-Project` | The name of the project the notification belongs to. |
+| `X-GitLab-Project-Id` | The project's ID. |
+| `X-GitLab-Project-Path` | The project's path. |
+| `X-GitLab-(Resource)-ID` | The ID of the resource the notification is for. The resource, for example, can be `Issue`, `MergeRequest`, `Commit`, or another such resource. |
+| `X-GitLab-Discussion-ID` | The ID of the thread the comment belongs to, in notification emails for comments. |
+| `X-GitLab-Pipeline-Id` | The ID of the pipeline the notification is for, in notification emails for pipelines. |
+| `X-GitLab-Reply-Key` | A unique token to support reply by email. |
+| `X-GitLab-NotificationReason` | The reason for the notification. This can be `mentioned`, `assigned`, or `own_activity`. |
+| `List-Id` | The path of the project in an RFC 2919 mailing list identifier. This is useful for email organization with filters, for example. |
### X-GitLab-NotificationReason
diff --git a/doc/user/profile/personal_access_tokens.md b/doc/user/profile/personal_access_tokens.md
index 1223f7b801a..87c1fe4007a 100644
--- a/doc/user/profile/personal_access_tokens.md
+++ b/doc/user/profile/personal_access_tokens.md
@@ -4,15 +4,22 @@ type: concepts, howto
# Personal access tokens
-> [Introduced](https://gitlab.com/gitlab-org/gitlab-foss/-/merge_requests/3749) in GitLab 8.8.
+> - [Introduced](https://gitlab.com/gitlab-org/gitlab-foss/-/merge_requests/3749) in GitLab 8.8.
+> - [Notifications about expiring tokens](https://gitlab.com/gitlab-org/gitlab/-/issues/3649) added in GitLab 12.6.
+> - [Token lifetime limits](https://gitlab.com/gitlab-org/gitlab/-/issues/3649) added in [GitLab Ultimate](https://about.gitlab.com/pricing/) 12.6.
-If you're unable to use [OAuth2](../../api/oauth2.md), you can use a personal access token to authenticate with the [GitLab API](../../api/README.md#personal-access-tokens).
+If you're unable to use [OAuth2](../../api/oauth2.md), you can use a personal access token to authenticate with the [GitLab API](../../api/README.md#personalproject-access-tokens).
You can also use personal access tokens with Git to authenticate over HTTP or SSH. Personal access tokens are required when [Two-Factor Authentication (2FA)](../account/two_factor_authentication.md) is enabled. In both cases, you can authenticate with a token in place of your password.
Personal access tokens expire on the date you define, at midnight UTC.
-For examples of how you can use a personal access token to authenticate with the API, see the following section from our [API Docs](../../api/README.md#personal-access-tokens).
+- GitLab runs a check at 01:00 AM UTC every day to identify personal access tokens that will expire in under seven days. The owners of these tokens are notified by email.
+- In GitLab Ultimate, administrators may [limit the lifetime of personal access tokens](../admin_area/settings/account_and_limit_settings.md#limiting-lifetime-of-personal-access-tokens-ultimate-only).
+
+For examples of how you can use a personal access token to authenticate with the API, see the following section from our [API Docs](../../api/README.md#personalproject-access-tokens).
+
+GitLab also offers [impersonation tokens](../../api/README.md#impersonation-tokens) which are created by administrators via the API. They're a great fit for automated authentication as a specific user.
## Creating a personal access token
diff --git a/doc/user/profile/preferences.md b/doc/user/profile/preferences.md
index cd195e6e7a1..55781b48a27 100644
--- a/doc/user/profile/preferences.md
+++ b/doc/user/profile/preferences.md
@@ -55,6 +55,11 @@ The default syntax theme is White, and you can choose among 5 different themes:
![Profile preferences syntax highlighting themes](img/profile-preferences-syntax-themes.png)
+[Introduced](https://gitlab.com/groups/gitlab-org/-/epics/2389) in 13.0, the theme
+you choose also applies to the [Web IDE](../project/web_ide/index.md)'s code editor and [Snippets](../snippets.md).
+The themes are available only in the Web IDE file editor, except for the [dark theme](https://gitlab.com/gitlab-org/gitlab/-/issues/209808),
+which applies to the entire Web IDE screen.
+
## Behavior
The following settings allow you to customize the behavior of GitLab's layout
diff --git a/doc/user/profile/unknown_sign_in_notification.md b/doc/user/profile/unknown_sign_in_notification.md
new file mode 100644
index 00000000000..9400ead1922
--- /dev/null
+++ b/doc/user/profile/unknown_sign_in_notification.md
@@ -0,0 +1,16 @@
+# Email notification for unknown sign-ins
+
+When a user successfully signs in from a previously unknown IP address,
+GitLab notifies the user by email. In this way, GitLab proactively alerts users of potentially
+malicious or unauthorized sign-ins.
+
+There are two methods used to identify a known sign-in:
+
+- Last sign-in IP: The current sign-in IP address is checked against the last sign-in
+ IP address.
+- Current active sessions: If the user has an existing active session from the
+ same IP address. See [Active Sessions](active_sessions.md).
+
+## Example email
+
+![Unknown sign in email](./img/unknown_sign_in_email_v13_0.png)
diff --git a/doc/user/project/clusters/add_eks_clusters.md b/doc/user/project/clusters/add_eks_clusters.md
index 7fa8ec6c5f3..712f8ea0adc 100644
--- a/doc/user/project/clusters/add_eks_clusters.md
+++ b/doc/user/project/clusters/add_eks_clusters.md
@@ -1,3 +1,9 @@
+---
+stage: Configure
+group: Configure
+info: To determine the technical writer assigned to the Stage/Group associated with this page, see https://about.gitlab.com/handbook/engineering/ux/technical-writing/#designated-technical-writers
+---
+
# Adding EKS clusters
GitLab supports adding new and existing EKS clusters.
diff --git a/doc/user/project/clusters/add_gke_clusters.md b/doc/user/project/clusters/add_gke_clusters.md
index 1195421f8fb..4094828323a 100644
--- a/doc/user/project/clusters/add_gke_clusters.md
+++ b/doc/user/project/clusters/add_gke_clusters.md
@@ -1,3 +1,9 @@
+---
+stage: Configure
+group: Configure
+info: To determine the technical writer assigned to the Stage/Group associated with this page, see https://about.gitlab.com/handbook/engineering/ux/technical-writing/#designated-technical-writers
+---
+
# Adding GKE clusters
GitLab supports adding new and existing GKE clusters.
diff --git a/doc/user/project/clusters/add_remove_clusters.md b/doc/user/project/clusters/add_remove_clusters.md
index dce273ce602..fddc9873f17 100644
--- a/doc/user/project/clusters/add_remove_clusters.md
+++ b/doc/user/project/clusters/add_remove_clusters.md
@@ -1,3 +1,9 @@
+---
+stage: Configure
+group: Configure
+info: To determine the technical writer assigned to the Stage/Group associated with this page, see https://about.gitlab.com/handbook/engineering/ux/technical-writing/#designated-technical-writers
+---
+
# Adding and removing Kubernetes clusters
GitLab offers integrated cluster creation for the following Kubernetes providers:
diff --git a/doc/user/project/clusters/index.md b/doc/user/project/clusters/index.md
index 74a58b93442..1298a24fcac 100644
--- a/doc/user/project/clusters/index.md
+++ b/doc/user/project/clusters/index.md
@@ -1,3 +1,9 @@
+---
+stage: Monitor
+group: APM
+info: To determine the technical writer assigned to the Stage/Group associated with this page, see https://about.gitlab.com/handbook/engineering/ux/technical-writing/#designated-technical-writers
+---
+
# Kubernetes clusters
> - [Introduced](https://gitlab.com/gitlab-org/gitlab-foss/issues/35954) in GitLab 10.1 for projects.
@@ -30,10 +36,28 @@ Using the GitLab project Kubernetes integration, you can:
- View [Logs](#logs).
- Run serverless workloads on [Kubernetes with Knative](serverless/index.md).
+### Supported cluster versions
+
+GitLab is committed to support at least two production-ready Kubernetes minor versions at any given time. We regularly review the versions we support, and provide a four-month deprecation period before we remove support of a specific version. The range of supported versions is based on the evaluation of:
+
+- Our own needs.
+- The versions supported by major managed Kubernetes providers.
+- The versions [supported by the Kubernetes community](https://kubernetes.io/docs/setup/release/version-skew-policy/#supported-versions).
+
+Currently, GitLab supports the following Kubernetes versions:
+
+- 1.15
+- 1.14
+- 1.13 (deprecated, support ends on November 22, 2020)
+- 1.12 (deprecated, support ends on September 22, 2020)
+
+NOTE: **Note:**
+Some GitLab features may support versions outside the range provided here.
+
### Deploy Boards **(PREMIUM)**
GitLab's Deploy Boards offer a consolidated view of the current health and
-status of each CI [environment](../../../ci/environments.md) running on Kubernetes,
+status of each CI [environment](../../../ci/environments/index.md) running on Kubernetes,
displaying the status of the pods in the deployment. Developers and other
teammates can view the progress and status of a rollout, pod by pod, in the
workflow they already use without any need to access Kubernetes.
@@ -78,8 +102,8 @@ Kubernetes clusters can be used without Auto DevOps.
> Introduced in GitLab 8.15.
-When enabled, the Kubernetes integration adds [web terminal](../../../ci/environments.md#web-terminals)
-support to your [environments](../../../ci/environments.md). This is based on the `exec` functionality found in
+When enabled, the Kubernetes integration adds [web terminal](../../../ci/environments/index.md#web-terminals)
+support to your [environments](../../../ci/environments/index.md). This is based on the `exec` functionality found in
Docker and Kubernetes, so you get a new shell session within your existing
containers. To use this integration, you should deploy to Kubernetes using
the deployment variables above, ensuring any deployments, replica sets, and
@@ -181,8 +205,8 @@ you can either:
### Setting the environment scope **(PREMIUM)**
When adding more than one Kubernetes cluster to your project, you need to differentiate
-them with an environment scope. The environment scope associates clusters with [environments](../../../ci/environments.md) similar to how the
-[environment-specific variables](../../../ci/variables/README.md#limiting-environment-scopes-of-environment-variables) work.
+them with an environment scope. The environment scope associates clusters with [environments](../../../ci/environments/index.md) similar to how the
+[environment-specific variables](../../../ci/variables/README.md#limit-the-environment-scopes-of-environment-variables) work.
The default environment scope is `*`, which means all jobs, regardless of their
environment, will use that cluster. Each scope can only be used by a single
@@ -262,7 +286,7 @@ A Kubernetes cluster can be the destination for a deployment job. If
the cluster from your jobs using tools such as `kubectl` or `helm`.
- You don't use GitLab's cluster integration you can still deploy to your
cluster. However, you will need configure Kubernetes tools yourself
- using [environment variables](../../../ci/variables/README.md#creating-a-custom-environment-variable)
+ using [environment variables](../../../ci/variables/README.md#custom-environment-variables)
before you can interact with the cluster from your jobs.
### Deployment variables
@@ -297,7 +321,7 @@ of the form `<project_name>-<project_id>-<environment>` (see [Deployment
variables](#deployment-variables)).
For **non**-GitLab-managed clusters, the namespace can be customized using
-[`environment:kubernetes:namespace`](../../../ci/environments.md#configuring-kubernetes-deployments)
+[`environment:kubernetes:namespace`](../../../ci/environments/index.md#configuring-kubernetes-deployments)
in `.gitlab-ci.yml`.
NOTE: **Note:** When using a [GitLab-managed cluster](#gitlab-managed-clusters), the
@@ -314,7 +338,7 @@ the deployment job:
However, sometimes GitLab can not create them. In such instances, your job will fail with the message:
-```text
+```plaintext
This job failed because the necessary resources were not successfully created.
```
@@ -325,7 +349,7 @@ Reasons for failure include:
- The token you gave GitLab does not have [`cluster-admin`](https://kubernetes.io/docs/reference/access-authn-authz/rbac/#user-facing-roles)
privileges required by GitLab.
- Missing `KUBECONFIG` or `KUBE_TOKEN` variables. To be passed to your job, they must have a matching
- [`environment:name`](../../../ci/environments.md#defining-environments). If your job has no
+ [`environment:name`](../../../ci/environments/index.md#defining-environments). If your job has no
`environment:name` set, it will not be passed the Kubernetes credentials.
NOTE: **NOTE:**
diff --git a/doc/user/project/clusters/kubernetes_pod_logs.md b/doc/user/project/clusters/kubernetes_pod_logs.md
index 5543187b6de..8577231b69b 100644
--- a/doc/user/project/clusters/kubernetes_pod_logs.md
+++ b/doc/user/project/clusters/kubernetes_pod_logs.md
@@ -1,24 +1,36 @@
+---
+stage: Configure
+group: Configure
+info: To determine the technical writer assigned to the Stage/Group associated with this page, see https://about.gitlab.com/handbook/engineering/ux/technical-writing/#designated-technical-writers
+---
+
# Kubernetes Logs
> - [Introduced](https://gitlab.com/gitlab-org/gitlab/issues/4752) in [GitLab Ultimate](https://about.gitlab.com/pricing/) 11.0.
-> - [Moved](https://gitlab.com/gitlab-org/gitlab/-/merge_requests/25455) to [GitLab Core](https://about.gitlab.com/pricing/) 12.9.
+> - [Moved](https://gitlab.com/gitlab-org/gitlab/-/merge_requests/26383) to [GitLab Core](https://about.gitlab.com/pricing/) 12.9.
GitLab makes it easy to view the logs of running pods in [connected Kubernetes clusters](index.md).
-By displaying the logs directly in GitLab, developers can avoid having to manage console tools or jump to a different interface.
+By displaying the logs directly in GitLab in the **Log Explorer**, developers can avoid
+managing console tools or jumping to a different interface.
NOTE: **Kubernetes + GitLab**
-Everything you need to build, test, deploy, and run your app at scale.
+Everything you need to build, test, deploy, and run your application at scale.
[Learn more](https://about.gitlab.com/solutions/kubernetes/).
## Overview
-[Kubernetes](https://kubernetes.io) logs can be viewed directly within GitLab.
+[Kubernetes](https://kubernetes.io) logs can be viewed directly within GitLab with
+the **Log Explorer**.
![Pod logs](img/kubernetes_pod_logs_v12_10.png)
+<i class="fa fa-youtube-play youtube" aria-hidden="true"></i>
+To learn more, see [APM - Log Explorer](https://www.youtube.com/watch?v=hWclZHA7Dgw).
+
## Requirements
-[Deploying to a Kubernetes environment](../deploy_boards.md#enabling-deploy-boards) is required in order to be able to use Logs.
+[Deploying to a Kubernetes environment](../deploy_boards.md#enabling-deploy-boards)
+is required to use Logs.
## Usage
@@ -30,7 +42,8 @@ You can access them in two ways.
> [Introduced](https://gitlab.com/gitlab-org/gitlab-foss/-/merge_requests/22011) in GitLab 12.5.
-Go to **{cloud-gear}** **Operations > Logs** on the sidebar menu.
+Go to **{cloud-gear}** **Operations > Pod logs** on the sidebar menu to display
+the **Log Explorer**.
![Sidebar menu](img/sidebar_menu_pod_logs_v12_10.png)
@@ -38,34 +51,42 @@ Go to **{cloud-gear}** **Operations > Logs** on the sidebar menu.
Logs can be displayed by clicking on a specific pod from [Deploy Boards](../deploy_boards.md):
-1. Go to **{cloud-gear}** **Operations > Environments** and find the environment which contains the desired pod, like `production`.
-1. On the **Environments** page, you should see the status of the environment's pods with [Deploy Boards](../deploy_boards.md).
-1. When mousing over the list of pods, a tooltip will appear with the exact pod name and status.
+1. Go to **{cloud-gear}** **Operations > Environments** and find the environment
+ which contains the desired pod, like `production`.
+1. On the **Environments** page, you should see the status of the environment's
+ pods with [Deploy Boards](../deploy_boards.md).
+1. When mousing over the list of pods, a tooltip will appear with the exact pod name
+ and status.
![Deploy Boards pod list](img/pod_logs_deploy_board.png)
-1. Click on the desired pod to bring up the logs view.
+1. Click on the desired pod to display the **Log Explorer**.
### Logs view
-The logs view lets you filter the logs by:
+The **Log Explorer** lets you filter the logs by:
- Pods.
- [From GitLab 12.4](https://gitlab.com/gitlab-org/gitlab/issues/5769), environments.
-- [From GitLab 12.7](https://gitlab.com/gitlab-org/gitlab/-/merge_requests/21656), [full text search](#full-text-search).
+- [From GitLab 12.7](https://gitlab.com/gitlab-org/gitlab/-/merge_requests/21656),
+ [full text search](#full-text-search).
- [From GitLab 12.8](https://gitlab.com/gitlab-org/gitlab/issues/197879), dates.
-Loading more than 500 log lines is possible from [GitLab 12.9](https://gitlab.com/gitlab-org/gitlab/-/issues/198050) onwards.
+Loading more than 500 log lines is possible from
+[GitLab 12.9](https://gitlab.com/gitlab-org/gitlab/-/issues/198050) onward.
-Support for pods with multiple containers is coming [in a future release](https://gitlab.com/gitlab-org/gitlab/issues/13404).
+Support for pods with multiple containers is coming
+[in a future release](https://gitlab.com/gitlab-org/gitlab/issues/13404).
-Support for historical data is coming [in a future release](https://gitlab.com/gitlab-org/gitlab/issues/196191).
+Support for historical data is coming
+[in a future release](https://gitlab.com/gitlab-org/gitlab/issues/196191).
### Filter by date
> [Introduced](https://gitlab.com/gitlab-org/gitlab/issues/197879) in GitLab 12.8.
-When you enable [Elastic Stack](../../clusters/applications.md#elastic-stack) on your cluster, you can filter by date.
+When you enable [Elastic Stack](../../clusters/applications.md#elastic-stack)
+on your cluster, you can filter logs displayed in the **Log Explorer** by date.
-Click on **Show last** to see the available options.
+Click **Show last** in the **Log Explorer** to see the available options.
### Full text search
@@ -74,7 +95,8 @@ Click on **Show last** to see the available options.
When you enable [Elastic Stack](../../clusters/applications.md#elastic-stack) on your cluster,
you can search the content of your logs through a search bar.
-The search is passed on to Elasticsearch using the [simple_query_string](https://www.elastic.co/guide/en/elasticsearch/reference/current/query-dsl-simple-query-string-query.html)
+The search is passed on to Elasticsearch using the
+[simple_query_string](https://www.elastic.co/guide/en/elasticsearch/reference/current/query-dsl-simple-query-string-query.html)
Elasticsearch function, which supports the following operators:
| Operator | Description |
diff --git a/doc/user/project/clusters/runbooks/index.md b/doc/user/project/clusters/runbooks/index.md
index 5575cd1d2d4..dfed43470bc 100644
--- a/doc/user/project/clusters/runbooks/index.md
+++ b/doc/user/project/clusters/runbooks/index.md
@@ -1,14 +1,19 @@
+---
+stage: Configure
+group: Configure
+info: To determine the technical writer assigned to the Stage/Group associated with this page, see https://about.gitlab.com/handbook/engineering/ux/technical-writing/#designated-technical-writers
+---
+
# Runbooks
Runbooks are a collection of documented procedures that explain how to
carry out a particular process, be it starting, stopping, debugging,
or troubleshooting a particular system.
-Using [Jupyter Notebooks](https://jupyter.org/) and the [Rubix library](https://github.com/Nurtch/rubix),
+Using [Jupyter Notebooks](https://jupyter.org/) and the
+[Rubix library](https://github.com/Nurtch/rubix),
users can get started writing their own executable runbooks.
-## Overview
-
Historically, runbooks took the form of a decision tree or a detailed
step-by-step guide depending on the condition or system.
@@ -22,121 +27,128 @@ pre-written code blocks or database queries against a given environment.
The JupyterHub app offered via GitLab’s Kubernetes integration now ships
with Nurtch’s Rubix library, providing a simple way to create DevOps
-runbooks. A sample runbook is provided, showcasing common operations. While Rubix makes it
-simple to create common Kubernetes and AWS workflows, you can also create them manually without
-Rubix.
+runbooks. A sample runbook is provided, showcasing common operations. While
+Rubix makes it simple to create common Kubernetes and AWS workflows, you can
+also create them manually without Rubix.
-**<i class="fa fa-youtube-play youtube" aria-hidden="true"></i>
+<i class="fa fa-youtube-play youtube" aria-hidden="true"></i>
Watch this [video](https://www.youtube.com/watch?v=Q_OqHIIUPjE)
-for an overview of how this is accomplished in GitLab!**
+for an overview of how this is accomplished in GitLab!
## Requirements
To create an executable runbook, you will need:
-1. **Kubernetes** - A Kubernetes cluster is required to deploy the rest of the applications.
- The simplest way to get started is to add a cluster using one of [GitLab's integrations](../add_remove_clusters.md#create-new-cluster).
-1. **Helm Tiller** - Helm is a package manager for Kubernetes and is required to install
- all the other applications. It is installed in its own pod inside the cluster which
- can run the Helm CLI in a safe environment.
-1. **Ingress** - Ingress can provide load balancing, SSL termination, and name-based
- virtual hosting. It acts as a web proxy for your applications.
-1. **JupyterHub** - [JupyterHub](https://jupyterhub.readthedocs.io/) is a multi-user service for managing notebooks across
- a team. Jupyter Notebooks provide a web-based interactive programming environment
- used for data analysis, visualization, and machine learning.
+- **Kubernetes** - A Kubernetes cluster is required to deploy the rest of the
+ applications. The simplest way to get started is to add a cluster using one
+ of [GitLab's integrations](../add_remove_clusters.md#create-new-cluster).
+- **Helm Tiller** - Helm is a package manager for Kubernetes and is required to
+ install all the other applications. It's installed in its own pod inside the
+ cluster which can run the Helm CLI in a safe environment.
+- **Ingress** - Ingress can provide load balancing, SSL termination, and name-based
+ virtual hosting. It acts as a web proxy for your applications.
+- **JupyterHub** - [JupyterHub](https://jupyterhub.readthedocs.io/) is a multi-user
+ service for managing notebooks across a team. Jupyter Notebooks provide a
+ web-based interactive programming environment used for data analysis,
+ visualization, and machine learning.
## Nurtch
-Nurtch is the company behind the [Rubix library](https://github.com/Nurtch/rubix). Rubix is
-an open-source Python library that makes it easy to perform common DevOps tasks inside Jupyter Notebooks.
-Tasks such as plotting Cloudwatch metrics and rolling your ECS/Kubernetes app are simplified
-down to a couple of lines of code. See the [Nurtch Documentation](http://docs.nurtch.com/en/latest/)
-for more information.
+Nurtch is the company behind the [Rubix library](https://github.com/Nurtch/rubix).
+Rubix is an open-source Python library that makes it easy to perform common
+DevOps tasks inside Jupyter Notebooks. Tasks such as plotting Cloudwatch metrics
+and rolling your ECS/Kubernetes app are simplified down to a couple of lines of
+code. See the [Nurtch Documentation](http://docs.nurtch.com/en/latest/) for more
+information.
## Configure an executable runbook with GitLab
Follow this step-by-step guide to configure an executable runbook in GitLab using
-the components outlined above and the preloaded demo runbook.
-
-### 1. Add a Kubernetes cluster
-
-Follow the steps outlined in [Create new cluster](../add_remove_clusters.md#create-new-cluster)
-to add a Kubernetes cluster to your project.
-
-### 2. Install Helm Tiller, Ingress, and JupyterHub
-
-Once the cluster has been provisioned in GKE, click the **Install** button next to the **Helm Tiller** app.
-
-![install helm](img/helm-install.png)
+the components outlined above and the pre-loaded demo runbook.
-Once Tiller has been installed successfully, click the **Install** button next to the **Ingress** app.
+1. Add a Kubernetes cluster to your project by following the steps outlined in
+ [Create new cluster](../add_remove_clusters.md#create-new-cluster).
+1. After the cluster has been provisioned in GKE, click the **Install** button
+ next to the **Helm Tiller** application to install Helm Tiller.
-![install ingress](img/ingress-install.png)
+ ![install helm](img/helm-install.png)
-Once Ingress has been installed successfully, click the **Install** button next to the **JupyterHub** app.
+1. After Helm Tiller has been installed successfully, click the **Install** button next
+ to the **Ingress** application.
-![install jupyterhub](img/jupyterhub-install.png)
+ ![install ingress](img/ingress-install.png)
-### 3. Login to JupyterHub and start the server
+1. After Ingress has been installed successfully, click the **Install** button next
+ to the **JupyterHub** application. You will need the **Jupyter Hostname** provided
+ here in the next step.
-Once JupyterHub has been installed successfully, navigate to the displayed **Jupyter Hostname** URL and click
-**Sign in with GitLab**. Authentication is automatically enabled for any user of the GitLab instance via OAuth2. This
-will redirect to GitLab in order to authorize JupyterHub to use your GitLab account. Click **Authorize**.
+ ![install JupyterHub](img/jupyterhub-install.png)
-![authorize jupyter](img/authorize-jupyter.png)
+1. After JupyterHub has been installed successfully, open the **Jupyter Hostname**
+ in your browser. Click the **Sign in with GitLab** button to log in to
+ JupyterHub and start the server. Authentication is enabled for any user of the
+ GitLab instance with OAuth2. This button redirects you to a page at GitLab
+ requesting authorization for JupyterHub to use your GitLab account.
-Once the application has been authorized you will taken back to the JupyterHub application. Click **Start My Server**.
-The server will take a couple of seconds to start.
+ ![authorize Jupyter](img/authorize-jupyter.png)
-### 4. Configure access
+1. Click **Authorize**, and you will be redirected to the JupyterHub application.
+1. Click **Start My Server**, and the server will start in a few seconds.
+1. To configure the runbook's access to your GitLab project, you must enter your
+ [GitLab Access Token](../../../profile/personal_access_tokens.md)
+ and your Project ID in the **Setup** section of the demo runbook:
-In order for the runbook to access your GitLab project, you will need to enter a
-[GitLab Access Token](../../../profile/personal_access_tokens.md)
-as well as your Project ID in the **Setup** section of the demo runbook.
+ 1. Double-click the **DevOps-Runbook-Demo** folder located on the left panel.
-Double-click the **DevOps-Runbook-Demo** folder located on the left panel.
+ ![demo runbook](img/demo-runbook.png)
-![demo runbook](img/demo-runbook.png)
+ 1. Double-click the `Nurtch-DevOps-Demo.ipynb` runbook.
-Double-click the "Nurtch-DevOps-Demo.ipynb" runbook.
+ ![sample runbook](img/sample-runbook.png)
-![sample runbook](img/sample-runbook.png)
+ Jupyter displays the runbook's contents in the right-hand side of the screen.
+ The **Setup** section displays your `PRIVATE_TOKEN` and your `PROJECT_ID`.
+ Enter these values, maintaining the single quotes as follows:
-The contents on the runbook will be displayed on the right side of the screen. Under the "Setup" section, you will find
-entries for both your `PRIVATE_TOKEN` and your `PROJECT_ID`. Enter both these values, conserving the single quotes as follows:
+ ```sql
+ PRIVATE_TOKEN = 'n671WNGecHugsdEDPsyo'
+ PROJECT_ID = '1234567'
+ ```
-```sql
-PRIVATE_TOKEN = 'n671WNGecHugsdEDPsyo'
-PROJECT_ID = '1234567'
-```
+ 1. Update the `VARIABLE_NAME` on the last line of this section to match the name of
+ the variable you're using for your access token. In this example, our variable
+ name is `PRIVATE_TOKEN`.
-Update the `VARIABLE_NAME` on the last line of this section to match the name of the variable you are using for your
-access token. In this example our variable name is `PRIVATE_TOKEN`.
+ ```sql
+ VARIABLE_VALUE = project.variables.get('PRIVATE_TOKEN').value
+ ```
-```sql
-VARIABLE_VALUE = project.variables.get('PRIVATE_TOKEN').value
-```
+1. To configure the operation of a runbook, create and configure variables:
-### 5. Configure an operation
+ NOTE: **Note:**
+ For this example, we are using the **Run SQL queries in Notebook** section in the
+ sample runbook to query a PostgreSQL database. The first four lines of the following
+ code block define the variables that are required for this query to function:
-For this example we'll use the "**Run SQL queries in Notebook**" section in the sample runbook to query
-a PostgreSQL database. The first 4 lines of the section define the variables that are required for this query to function.
+ ```sql
+ %env DB_USER={project.variables.get('DB_USER').value}
+ %env DB_PASSWORD={project.variables.get('DB_PASSWORD').value}
+ %env DB_ENDPOINT={project.variables.get('DB_ENDPOINT').value}
+ %env DB_NAME={project.variables.get('DB_NAME').value}
+ ```
-```sql
-%env DB_USER={project.variables.get('DB_USER').value}
-%env DB_PASSWORD={project.variables.get('DB_PASSWORD').value}
-%env DB_ENDPOINT={project.variables.get('DB_ENDPOINT').value}
-%env DB_NAME={project.variables.get('DB_NAME').value}
-```
+ 1. Navigate to **{settings}** **Settings >> CI/CD >> Variables** to create
+ the variables in your project.
-Create the matching variables in your project's **Settings >> CI/CD >> Variables**
+ ![GitLab variables](img/gitlab-variables.png)
-![gitlab variables](img/gitlab-variables.png)
+ 1. Click **Save variables**.
-Back in Jupyter, click the "Run SQL queries in Notebook" heading and the click *Run*. The results will be
-displayed in-line as follows:
+ 1. In Jupyter, click the **Run SQL queries in Notebook** heading, and then click
+ **Run**. The results are displayed inline as follows:
-![PostgreSQL query](img/postgres-query.png)
+ ![PostgreSQL query](img/postgres-query.png)
-You can try other operations such as running shell scripts or interacting with a Kubernetes cluster. Visit the
+You can try other operations, such as running shell scripts or interacting with a
+Kubernetes cluster. Visit the
[Nurtch Documentation](http://docs.nurtch.com/) for more information.
diff --git a/doc/user/project/clusters/serverless/aws.md b/doc/user/project/clusters/serverless/aws.md
index 3df57e3a7a5..124a0d4bf9f 100644
--- a/doc/user/project/clusters/serverless/aws.md
+++ b/doc/user/project/clusters/serverless/aws.md
@@ -1,3 +1,9 @@
+---
+stage: Configure
+group: Configure
+info: To determine the technical writer assigned to the Stage/Group associated with this page, see https://about.gitlab.com/handbook/engineering/ux/technical-writing/#designated-technical-writers
+---
+
# Deploying AWS Lambda function using GitLab CI/CD
GitLab allows users to easily deploy AWS Lambda functions and create rich serverless applications.
@@ -150,7 +156,7 @@ endpoints:
Running the following `curl` command should trigger your function.
NOTE: **Note:**
- Your url should be the one retrieved from the GitLab deploy stage log.
+Your URL should be the one retrieved from the GitLab deploy stage log.
```shell
curl https://u768nzby1j.execute-api.us-east-1.amazonaws.com/production/hello
diff --git a/doc/user/project/clusters/serverless/index.md b/doc/user/project/clusters/serverless/index.md
index 418e16aa0c1..2156d96f92a 100644
--- a/doc/user/project/clusters/serverless/index.md
+++ b/doc/user/project/clusters/serverless/index.md
@@ -1,3 +1,9 @@
+---
+stage: Configure
+group: Configure
+info: To determine the technical writer assigned to the Stage/Group associated with this page, see https://about.gitlab.com/handbook/engineering/ux/technical-writing/#designated-technical-writers
+---
+
# Serverless
> Introduced in GitLab 11.5.
diff --git a/doc/user/project/code_owners.md b/doc/user/project/code_owners.md
index 49083be77dc..45d9e8f04e0 100644
--- a/doc/user/project/code_owners.md
+++ b/doc/user/project/code_owners.md
@@ -9,6 +9,33 @@ in [GitLab Starter](https://about.gitlab.com/pricing/) 11.3.
> - [Support for group namespaces](https://gitlab.com/gitlab-org/gitlab-foss/issues/53182) added in GitLab Starter 12.1.
> - Code Owners for Merge Request approvals was [introduced](https://gitlab.com/gitlab-org/gitlab/issues/4418) in [GitLab Premium](https://about.gitlab.com/pricing/) 11.9.
+## Introduction
+
+When contributing to a project, it can often be difficult
+to find out who should review or approve merge requests.
+Additionally, if you have a question over a specific file or
+code block, it may be difficult to know who to find the answer from.
+
+GitLab Code Owners is a feature to define who owns specific
+files or paths in a repository, allowing other users to understand
+who is responsible for each file or path.
+
+## Why is this useful?
+
+Code Owners allows for a version controlled single source of
+truth file outlining the exact GitLab users or groups that
+own certain files or paths in a repository. Code Owners can be
+utilized in the merge request approval process which can streamline
+the process of finding the right reviewers and approvers for a given
+merge request.
+
+In larger organizations or popular open source projects, Code Owners
+can also be useful to understand who to contact if you have
+a question that may not be related to code review or a merge request
+approval.
+
+## How to set up Code Owners
+
You can use a `CODEOWNERS` file to specify users or
[shared groups](members/share_project_with_groups.md)
that are responsible for certain files in a repository.
@@ -41,7 +68,7 @@ The user that would show for `README.md` would be `@user2`.
## Approvals by Code Owners
Once you've set Code Owners to a project, you can configure it to
-receive approvals:
+be used for merge request approvals:
- As [merge request eligible approvers](merge_requests/merge_request_approvals.md#code-owners-as-eligible-approvers).
- As required approvers for [protected branches](protected_branches.md#protected-branches-approval-by-code-owners-premium). **(PREMIUM)**
@@ -50,6 +77,9 @@ Once set, Code Owners are displayed in merge requests widgets:
![MR widget - Code Owners](img/code_owners_mr_widget_v12_4.png)
+NOTE: **Note**:
+ While the`CODEOWNERS` file can be used in addition to Merge Request [Approval Rules](merge_requests/merge_request_approvals.md#approval-rules) it can also be used as the sole driver of a Merge Request approval (without using [Approval Rules](merge_requests/merge_request_approvals.md#approval-rules)) by simply creating the file in one of the three locations specified above, configuring the Code Owners to be required approvers for [protected branches](protected_branches.md#protected-branches-approval-by-code-owners-premium) and then using [the syntax of Code Owners files](code_owners.md#the-syntax-of-code-owners-files) to specify the actual owners and granular permissions.
+
## The syntax of Code Owners files
Files can be specified using the same kind of patterns you would use
@@ -69,23 +99,28 @@ escaped using `\#` to address files for which the name starts with a
Example `CODEOWNERS` file:
```plaintext
-# This is an example code owners file, lines starting with a `#` will
-# be ignored.
+# This is an example of a code owners file
+# lines starting with a `#` will be ignored.
# app/ @commented-rule
# We can specify a default match using wildcards:
* @default-codeowner
+# We can also specify "multiple tab or space" separated codeowners:
+* @multiple @code @owners
+
# Rules defined later in the file take precedence over the rules
# defined before.
# This will match all files for which the file name ends in `.rb`
*.rb @ruby-owner
-# Files with a `#` can still be accesssed by escaping the pound sign
+# Files with a `#` can still be accessed by escaping the pound sign
\#file_with_pound.rb @owner-file-with-pound
# Multiple codeowners can be specified, separated by spaces or tabs
+# In the following case the CODEOWNERS file from the root of the repo
+# has 3 code owners (@multiple @code @owners)
CODEOWNERS @multiple @code @owners
# Both usernames or email addresses can be used to match
diff --git a/doc/user/project/deploy_boards.md b/doc/user/project/deploy_boards.md
index c479f610ff1..8f7bb844e37 100644
--- a/doc/user/project/deploy_boards.md
+++ b/doc/user/project/deploy_boards.md
@@ -3,7 +3,7 @@
> [Introduced](https://gitlab.com/gitlab-org/gitlab/issues/1589) in [GitLab Premium](https://about.gitlab.com/pricing/) 9.0.
GitLab's Deploy Boards offer a consolidated view of the current health and
-status of each CI [environment](../../ci/environments.md) running on [Kubernetes](https://kubernetes.io), displaying the status
+status of each CI [environment](../../ci/environments/index.md) running on [Kubernetes](https://kubernetes.io), displaying the status
of the pods in the deployment. Developers and other teammates can view the
progress and status of a rollout, pod by pod, in the workflow they already use
without any need to access Kubernetes.
@@ -57,9 +57,9 @@ specific environment, there are a lot of use cases. To name a few:
## Enabling Deploy Boards
-To display the Deploy Boards for a specific [environment](../../ci/environments.md) you should:
+To display the Deploy Boards for a specific [environment](../../ci/environments/index.md) you should:
-1. Have [defined an environment](../../ci/environments.md#defining-environments) with a deploy stage.
+1. Have [defined an environment](../../ci/environments/index.md#defining-environments) with a deploy stage.
1. Have a Kubernetes cluster up and running.
@@ -113,7 +113,7 @@ metadata:
name: "APPLICATION_NAME"
annotations:
app.gitlab.com/app: ${CI_PROJECT_PATH_SLUG}
- app.gitlab.com/env: ${CI_ENVIRONMENT_SLUG}
+ app.gitlab.com/env: ${CI_ENVIRONMENT_SLUG}
spec:
replicas: 1
selector:
@@ -130,6 +130,11 @@ spec:
The annotations will be applied to the deployments, replica sets, and pods. By changing the number of replicas, like `kubectl scale --replicas=3 deploy APPLICATION_NAME -n ${KUBE_NAMESPACE}`, you can follow the instances' pods from the board.
+NOTE: **Note:**
+The YAML file is static. If you apply it using `kubectl apply`, you must
+manually provide the project and environment slugs, or create a script to
+replace the variables in the YAML before applying.
+
## Canary Deployments
A popular CI strategy, where a small portion of the fleet is updated to the new
@@ -141,5 +146,5 @@ version of your application.
- [GitLab Autodeploy](../../topics/autodevops/stages.md#auto-deploy)
- [GitLab CI/CD environment variables](../../ci/variables/README.md)
-- [Environments and deployments](../../ci/environments.md)
+- [Environments and deployments](../../ci/environments/index.md)
- [Kubernetes deploy example](https://gitlab.com/gitlab-examples/kubernetes-deploy)
diff --git a/doc/user/project/deploy_tokens/index.md b/doc/user/project/deploy_tokens/index.md
index ebb12a6ed5d..2d42debed68 100644
--- a/doc/user/project/deploy_tokens/index.md
+++ b/doc/user/project/deploy_tokens/index.md
@@ -3,8 +3,10 @@
> - [Introduced](https://gitlab.com/gitlab-org/gitlab-foss/-/merge_requests/17894) in GitLab 10.7.
> - [Moved](https://gitlab.com/gitlab-org/gitlab/issues/199370) from **Settings > Repository** in GitLab 12.9.
> - [Added `write_registry` scope](https://gitlab.com/gitlab-org/gitlab/-/issues/22743) in GitLab 12.10.
+> - [Moved](https://gitlab.com/gitlab-org/gitlab/-/merge_requests/29280) from **Settings > CI / CD** in GitLab 12.10.1.
+> - [Added package registry scopes](https://gitlab.com/gitlab-org/gitlab/-/issues/213566) from **Settings > CI / CD** in GitLab 13.0.
-Deploy tokens allow you to download (`git clone`) or push and pull the container registry images of a project without having a user and a password.
+Deploy tokens allow you to download (`git clone`) or push and pull packages and container registry images of a project without having a user and a password.
Deploy tokens can be managed by [maintainers only](../../permissions.md).
@@ -16,7 +18,7 @@ You can create as many deploy tokens as you like from the settings of your proje
1. Log in to your GitLab account.
1. Go to the project (or group) you want to create Deploy Tokens for.
-1. Go to **{settings}** **Settings** > **CI / CD**.
+1. Go to **{settings}** **Settings** > **Repository**.
1. Click on "Expand" on **Deploy Tokens** section.
1. Choose a name, expiry date (optional), and username (optional) for the token.
1. Choose the [desired scopes](#limiting-scopes-of-a-deploy-token).
@@ -65,7 +67,7 @@ To download a repository using a Deploy Token, you just need to:
1. `git clone` the project using the Deploy Token:
```shell
- git clone http://<username>:<deploy_token>@gitlab.example.com/tanuki/awesome_project.git
+ git clone https://<username>:<deploy_token>@gitlab.example.com/tanuki/awesome_project.git
```
Replace `<username>` and `<deploy_token>` with the proper values.
@@ -100,6 +102,22 @@ To push the container registry images, you'll need to:
Just replace `<username>` and `<deploy_token>` with the proper values. Then you can simply
push images to your Container Registry.
+### Read or pull packages
+
+To pull packages in the GitLab package registry, you'll need to:
+
+1. Create a Deploy Token with `read_package_registry` as a scope.
+1. Take note of your `username` and `token`.
+1. For the [package type of your choice](./../../packages/index.md), follow the authentication instructions for deploy tokens.
+
+### Push or upload packages
+
+To upload packages in the GitLab package registry, you'll need to:
+
+1. Create a Deploy Token with `write_package_registry` as a scope.
+1. Take note of your `username` and `token`.
+1. For the [package type of your choice](./../../packages/index.md), follow the authentication instructions for deploy tokens.
+
### Group Deploy Token
> [Introduced](https://gitlab.com/gitlab-org/gitlab/issues/21765) in GitLab 12.9.
@@ -107,6 +125,9 @@ push images to your Container Registry.
A deploy token created at the group level can be used across all projects that
belong either to the specific group or to one of its subgroups.
+<i class="fa fa-youtube-play youtube" aria-hidden="true"></i>
+For an overview, see [Group Deploy Tokens](https://youtu.be/8kxTJvaD9ks).
+
To use a group deploy token:
1. [Create](#creating-a-deploy-token) a deploy token for a group.
@@ -132,3 +153,6 @@ those variables:
```shell
docker login -u $CI_DEPLOY_USER -p $CI_DEPLOY_PASSWORD $CI_REGISTRY
```
+
+NOTE: **Note:**
+The special handling for the `gitlab-deploy-token` deploy token is not currently implemented for group deploy tokens. For the deploy token to be available for CI/CD jobs, it must be created at the project level. See [this issue](https://gitlab.com/gitlab-org/gitlab/-/issues/214014) for details.
diff --git a/doc/user/project/description_templates.md b/doc/user/project/description_templates.md
index a02dc016f03..16ac53a2b52 100644
--- a/doc/user/project/description_templates.md
+++ b/doc/user/project/description_templates.md
@@ -39,6 +39,26 @@ templates of the default branch will be taken into account.
Create a new Markdown (`.md`) file inside the `.gitlab/issue_templates/`
directory in your repository. Commit and push to your default branch.
+To create a Markdown file:
+
+ 1. Click the `+` button next to `master` and click **New file**.
+ 1. Add the name of your issue template to the **File name** text field next to `master`.
+ Make sure words are separated with underscores and that your file has the `.md` extension, for
+ example `feature_request.md`.
+ 1. Commit and push to your default branch.
+
+If you don't have a `.gitlab/issue_templates` directory in your repository, you'll need to create it.
+
+To create the `.gitlab/issue_templates` directory:
+
+ 1. Click the `+` button next to `master` and select **New directory**.
+ 1. Name this new directory `.gitlab` and commit to your default branch.
+ 1. Click the `+` button next to `master` again and select **New directory**.This time, n
+ 1. Name your directory `issue_templates` and commit to your default branch.
+
+To check if this has worked correctly, [create a new issue](./issues/managing_issues.md#create-a-new-issue)
+and see if you can choose a description template.
+
## Creating merge request templates
Similarly to issue templates, create a new Markdown (`.md`) file inside the
diff --git a/doc/user/project/file_lock.md b/doc/user/project/file_lock.md
index d5f35051e9a..9069a231db4 100644
--- a/doc/user/project/file_lock.md
+++ b/doc/user/project/file_lock.md
@@ -2,9 +2,10 @@
> [Introduced](https://gitlab.com/gitlab-org/gitlab/-/merge_requests/440) in [GitLab Premium](https://about.gitlab.com/pricing/) 8.9.
-File Locking helps you avoid merge conflicts and better manage your binary files.
-Lock any file or directory, make your changes, and then unlock it so another
-member of the team can edit it.
+Working with multiple people on the same file can be a risk. Conflicts when merging a non-text file are hard to overcome and will require a lot of manual work to resolve. File Locking helps you avoid these merge conflicts and better manage your binary files.
+
+With File Locking, you can lock any file or directory, make your changes, and
+then unlock it so another member of the team can edit it.
## Overview
diff --git a/doc/user/project/git_attributes.md b/doc/user/project/git_attributes.md
index 21ef94e61f7..260e618ba03 100644
--- a/doc/user/project/git_attributes.md
+++ b/doc/user/project/git_attributes.md
@@ -1,6 +1,6 @@
# Git Attributes
-GitLab supports defining custom [Git attributes][gitattributes] such as what
+GitLab supports defining custom [Git attributes](https://git-scm.com/docs/gitattributes) such as what
files to treat as binary, and what language to use for syntax highlighting
diffs.
@@ -18,5 +18,3 @@ ignored.
The `.gitattributes` file can be used to define which language to use when
syntax highlighting files and diffs. See ["Syntax
Highlighting"](highlighting.md) for more information.
-
-[gitattributes]: https://git-scm.com/docs/gitattributes
diff --git a/doc/user/project/img/service_desk_custom_email_address_v13_0.png b/doc/user/project/img/service_desk_custom_email_address_v13_0.png
new file mode 100644
index 00000000000..6ce8bf45085
--- /dev/null
+++ b/doc/user/project/img/service_desk_custom_email_address_v13_0.png
Binary files differ
diff --git a/doc/user/project/import/bitbucket.md b/doc/user/project/import/bitbucket.md
index 77fc2761e07..56717858b53 100644
--- a/doc/user/project/import/bitbucket.md
+++ b/doc/user/project/import/bitbucket.md
@@ -24,7 +24,7 @@ Import your projects from Bitbucket Cloud to GitLab with minimal effort.
## Requirements
-The [Bitbucket Cloud integration][bb-import] must be first enabled in order to be
+The [Bitbucket Cloud integration](../../../integration/bitbucket.md) must be first enabled in order to be
able to import your projects from Bitbucket Cloud. Ask your GitLab administrator
to enable this if not already.
@@ -42,7 +42,7 @@ The importer will create any new namespaces (groups) if they don't exist or in
the case the namespace is taken, the repository will be imported under the user's
namespace that started the import process.
-## Importing your Bitbucket repositories
+## Import your Bitbucket repositories
1. Sign in to GitLab and go to your dashboard.
1. Click on **New project**.
@@ -61,5 +61,11 @@ namespace that started the import process.
![Import projects](img/bitbucket_import_select_project_v12_3.png)
-[bb-import]: ../../../integration/bitbucket.md
-[social sign-in]: ../../profile/account/social_sign_in.md
+## Troubleshooting
+
+If you have more than one Bitbucket account, be sure to sign in to the correct account.
+If you've accidentally started the import process with the wrong account, follow these steps:
+
+1. Revoke GitLab access to your Bitbucket account, essentially reversing the process in the following procedure: [Import your Bitbucket repositories](#import-your-bitbucket-repositories).
+
+1. Sign out of the Bitbucket account. Follow the procedure linked from the previous step.
diff --git a/doc/user/project/import/bitbucket_server.md b/doc/user/project/import/bitbucket_server.md
index 99179c3190e..55df2d7294d 100644
--- a/doc/user/project/import/bitbucket_server.md
+++ b/doc/user/project/import/bitbucket_server.md
@@ -73,3 +73,7 @@ namespace that started the import process.
imported.
![Import projects](img/bitbucket_server_import_select_project_v12_3.png)
+
+## Troubleshooting
+
+See the [troubleshooting](bitbucket.md#troubleshooting) section for [Bitbucket](bitbucket.md).
diff --git a/doc/user/project/import/github.md b/doc/user/project/import/github.md
index db8d33b58da..4c213f21920 100644
--- a/doc/user/project/import/github.md
+++ b/doc/user/project/import/github.md
@@ -8,7 +8,7 @@ your self-managed GitLab instance.
NOTE: **Note:**
These instructions work for users on GitLab.com, but if you are an
administrator of a self-managed GitLab instance or if you are importing from GitHub Enterprise,
-you must enable [GitHub integration][gh-import]. GitHub integration is the only method for
+you must enable [GitHub integration](../../../integration/github.md). GitHub integration is the only method for
importing from GitHub Enterprise. If you are using GitLab.com, you can alternatively import
GitHub repositories using a [personal access token](#using-a-github-token),
but this method is not recommended because it cannot associate all user activity
@@ -81,7 +81,7 @@ the user account that is performing the import.
NOTE: **Note:**
If you are using a self-managed GitLab instance or if you are importing from GitHub Enterprise, this process requires that you have configured
-[GitHub integration][gh-import].
+[GitHub integration](../../../integration/github.md).
1. From the top navigation bar, click **+** and select **New project**.
1. Select the **Import project** tab and then select **GitHub**.
@@ -155,5 +155,3 @@ servers. For 4 servers with 8 cores this means you can import up to 32 objects (
Reducing the time spent in cloning a repository can be done by increasing network throughput, CPU capacity, and disk
performance (e.g., by using high performance SSDs) of the disks that store the Git repositories (for your GitLab instance).
Increasing the number of Sidekiq workers will *not* reduce the time spent cloning repositories.
-
-[gh-import]: ../../../integration/github.md "GitHub integration"
diff --git a/doc/user/project/import/jira.md b/doc/user/project/import/jira.md
index 49224001fe6..db48282a8f3 100644
--- a/doc/user/project/import/jira.md
+++ b/doc/user/project/import/jira.md
@@ -9,6 +9,15 @@ Jira issues import is an MVC, project-level feature, meaning that issues from mu
Jira projects can be imported into a GitLab project. MVC version imports issue title and description
as well as some other issue metadata as a section in the issue description.
+## Future iterations
+
+As of GitLab 12.10, the Jira issue importer only brings across the title and description of
+an issue.
+
+There is an [epic](https://gitlab.com/groups/gitlab-org/-/epics/2738) tracking the
+addition of items such as issue assignees, labels, comments, user mapping, and much more.
+These will be included in the future iterations of the GitLab Jira importer.
+
## Prerequisites
### Permissions
diff --git a/doc/user/project/index.md b/doc/user/project/index.md
index 585c45e35df..50272f0e007 100644
--- a/doc/user/project/index.md
+++ b/doc/user/project/index.md
@@ -242,7 +242,7 @@ field.
For example:
-```text
+```plaintext
machine example.gitlab.com
login <gitlab_user_name>
password <personal_access_token>
diff --git a/doc/user/project/integrations/bamboo.md b/doc/user/project/integrations/bamboo.md
index 8c7e6edbf38..db8f24fc1e1 100644
--- a/doc/user/project/integrations/bamboo.md
+++ b/doc/user/project/integrations/bamboo.md
@@ -27,7 +27,7 @@ need to be configured in a Bamboo build plan before GitLab can integrate.
1. In the left pane, select a build stage. If you have multiple build stages
you want to select the last stage that contains the Git checkout task.
1. Select the 'Miscellaneous' tab.
-1. Under 'Pattern Match Labelling' put `${bamboo.repository.revision.number}`
+1. Under 'Pattern Match Labeling' put `${bamboo.repository.revision.number}`
in the 'Labels' box.
1. Save
diff --git a/doc/user/project/integrations/generic_alerts.md b/doc/user/project/integrations/generic_alerts.md
index 1a000fd1c44..2234727dd82 100644
--- a/doc/user/project/integrations/generic_alerts.md
+++ b/doc/user/project/integrations/generic_alerts.md
@@ -1,3 +1,9 @@
+---
+stage: Monitor
+group: Health
+info: To determine the technical writer assigned to the Stage/Group associated with this page, see https://about.gitlab.com/handbook/engineering/ux/technical-writing/#designated-technical-writers
+---
+
# Generic alerts integration
> - [Introduced](https://gitlab.com/gitlab-org/gitlab/issues/13203) in [GitLab Ultimate](https://about.gitlab.com/pricing/) 12.4.
@@ -26,12 +32,16 @@ You can customize the payload by sending the following parameters. All fields ar
| Property | Type | Description |
| -------- | ---- | ----------- |
-| `title` | String | The title of the incident. If none is provided, then `New: Incident #N` will be used, where `#N` is the number of incident |
+| `title` | String | The title of the incident. Required. |
| `description` | String | A high-level summary of the problem. |
| `start_time` | DateTime | The time of the incident. If none is provided, a timestamp of the issue will be used. |
| `service` | String | The affected service. |
| `monitoring_tool` | String | The name of the associated monitoring tool. |
| `hosts` | String or Array | One or more hosts, as to where this incident occurred. |
+| `severity` | String | The severity of the alert. Must be one of `critical`, `high`, `medium`, `low`, `info`, `unknown`. Default is `critical`. |
+
+TIP: **Payload size:**
+Ensure your requests are smaller than the [payload application limits](../../../administration/instance_limits.md#generic-alert-json-payloads).
Example request:
diff --git a/doc/user/project/integrations/gitlab_slack_application.md b/doc/user/project/integrations/gitlab_slack_application.md
index 1ef2f593621..49b6a3f6450 100644
--- a/doc/user/project/integrations/gitlab_slack_application.md
+++ b/doc/user/project/integrations/gitlab_slack_application.md
@@ -1,13 +1,14 @@
# GitLab Slack application **(FREE ONLY)**
+> - Introduced in GitLab 9.4.
+> - Distributed to Slack App Directory in GitLab 10.2.
+
NOTE: **Note:**
The GitLab Slack application is only configurable for GitLab.com. It will **not**
work for on-premises installations where you can configure the
[Slack slash commands](slack_slash_commands.md) service instead. We're planning
to make this configurable for all GitLab installations, but there's
no ETA - see [#28164](https://gitlab.com/gitlab-org/gitlab/issues/28164).
-It was first introduced in GitLab 9.4 and distributed to Slack App Directory in
-GitLab 10.2.
Slack provides a native application which you can enable via your project's
integrations on GitLab.com.
@@ -35,12 +36,30 @@ docs on [Adding an app to your team](https://slack.com/help/articles/202035138).
To enable GitLab's service for your Slack team:
-1. Go to your project's **Settings > Integration > Slack application** (only
- visible on GitLab.com)
-1. Click the "Add to Slack" button
+1. Go to your project's **{settings}** **Settings > Integration > Slack application** (only
+ visible on GitLab.com).
+1. Click **Add to Slack**.
That's all! You can now start using the Slack slash commands.
+## Create a project alias for Slack
+
+To create a project alias on GitLab.com for Slack integration:
+
+1. Go to your project's home page.
+1. Navigate to **{settings}** **Settings > Integrations** (only visible on GitLab.com)
+1. On the **Integrations** page, click **Slack application**.
+1. The current **Project Alias**, if any, is displayed. To edit this value,
+ click **Edit**.
+1. Enter your desired alias, and click **Save changes**.
+
+Some Slack commands require a project alias, and fail with the following error
+if the project alias is incorrect or missing from the command:
+
+```plaintext
+GitLab error: project or alias not found
+```
+
## Usage
After confirming the installation, you, and everyone else in your Slack team,
diff --git a/doc/user/project/integrations/img/metrics_dashboard_annotations_ui_v13.0.png b/doc/user/project/integrations/img/metrics_dashboard_annotations_ui_v13.0.png
new file mode 100644
index 00000000000..a042fbbcf4e
--- /dev/null
+++ b/doc/user/project/integrations/img/metrics_dashboard_annotations_ui_v13.0.png
Binary files differ
diff --git a/doc/user/project/integrations/img/panel_context_menu_v12_10.png b/doc/user/project/integrations/img/panel_context_menu_v12_10.png
deleted file mode 100644
index 096262fea91..00000000000
--- a/doc/user/project/integrations/img/panel_context_menu_v12_10.png
+++ /dev/null
Binary files differ
diff --git a/doc/user/project/integrations/img/panel_context_menu_v13_0.png b/doc/user/project/integrations/img/panel_context_menu_v13_0.png
new file mode 100644
index 00000000000..2d7cb923981
--- /dev/null
+++ b/doc/user/project/integrations/img/panel_context_menu_v13_0.png
Binary files differ
diff --git a/doc/user/project/integrations/img/prometheus_dashboard_select_v_13_0.png b/doc/user/project/integrations/img/prometheus_dashboard_select_v_13_0.png
new file mode 100644
index 00000000000..2f0309ce664
--- /dev/null
+++ b/doc/user/project/integrations/img/prometheus_dashboard_select_v_13_0.png
Binary files differ
diff --git a/doc/user/project/integrations/img/toggle_metrics_user_starred_dashboard_v13_0.png b/doc/user/project/integrations/img/toggle_metrics_user_starred_dashboard_v13_0.png
new file mode 100644
index 00000000000..59dc9ccfd30
--- /dev/null
+++ b/doc/user/project/integrations/img/toggle_metrics_user_starred_dashboard_v13_0.png
Binary files differ
diff --git a/doc/user/project/integrations/img/webex_teams_configuration.png b/doc/user/project/integrations/img/webex_teams_configuration.png
new file mode 100644
index 00000000000..66993e0887d
--- /dev/null
+++ b/doc/user/project/integrations/img/webex_teams_configuration.png
Binary files differ
diff --git a/doc/user/project/integrations/mattermost_slash_commands.md b/doc/user/project/integrations/mattermost_slash_commands.md
index a23d8d7306d..6a202c9a130 100644
--- a/doc/user/project/integrations/mattermost_slash_commands.md
+++ b/doc/user/project/integrations/mattermost_slash_commands.md
@@ -17,21 +17,21 @@ If you have the Omnibus GitLab package installed, Mattermost is already bundled
in it. All you have to do is configure it. Read more in the
[Omnibus GitLab Mattermost documentation](https://docs.gitlab.com/omnibus/gitlab-mattermost/).
-## Automated Configuration
+## Automated configuration
If Mattermost is installed on the same server as GitLab, the configuration process can be
done for you by GitLab.
Go to the Mattermost Slash Command service on your project and click the 'Add to Mattermost' button.
-## Manual Configuration
+## Manual configuration
The configuration consists of two parts. First you need to enable the slash
commands in Mattermost and then enable the service in GitLab.
### Step 1. Enable custom slash commands in Mattermost
-This step is only required when using a source install, omnibus installs will be
+This step is only required when using a source install, Omnibus installs will be
preconfigured with the right settings.
The first thing to do in Mattermost is to enable custom slash commands from
@@ -145,6 +145,15 @@ trigger word followed by <kbd>help</kbd>. Example: `/gitlab help`
The permissions to run the [available commands](#available-slash-commands) derive from
the [permissions you have on the project](../../permissions.md#project-members-permissions).
+## Troubleshooting
+
+If an event is not being triggered, confirm that the channel you're using is a public one, as
+Mattermost webhooks do not have access to private channels.
+
+If a private channel is required, you can edit the webhook's channel in Mattermost and
+select a private channel. It is not possible to use different channels for
+different types of notifications - all events will be sent to the specified channel.
+
## Further reading
- [Mattermost slash commands documentation](https://docs.mattermost.com/developer/slash-commands.html)
diff --git a/doc/user/project/integrations/overview.md b/doc/user/project/integrations/overview.md
index b973abbe210..88668ab6c7d 100644
--- a/doc/user/project/integrations/overview.md
+++ b/doc/user/project/integrations/overview.md
@@ -47,7 +47,7 @@ Click on the service links to see further configuration instructions and details
| [Microsoft teams](microsoft_teams.md) | Receive notifications for actions that happen on GitLab into a room on Microsoft Teams using Office 365 Connectors | No |
| Packagist | Update your project on Packagist, the main Composer repository | Yes |
| Pipelines emails | Email the pipeline status to a list of recipients | No |
-| [Slack Notifications](slack.md) | Send GitLab events (e.g. issue created) to Slack as notifications | No |
+| [Slack Notifications](slack.md) | Send GitLab events (for example, an issue was created) to Slack as notifications | No |
| [Slack slash commands](slack_slash_commands.md) **(CORE ONLY)** | Use slash commands in Slack to control GitLab | No |
| [GitLab Slack application](gitlab_slack_application.md) **(FREE ONLY)** | Use Slack's official application | No |
| PivotalTracker | Project Management Software (Source Commits Endpoint) | No |
@@ -55,6 +55,7 @@ Click on the service links to see further configuration instructions and details
| Pushover | Pushover makes it easy to get real-time notifications on your Android device, iPhone, iPad, and Desktop | No |
| [Redmine](redmine.md) | Redmine issue tracker | No |
| [Unify Circuit](unify_circuit.md) | Receive events notifications in Unify Circuit | No |
+| [Webex Teams](webex_teams.md) | Receive events notifications in Webex Teams | No |
| [YouTrack](youtrack.md) | YouTrack issue tracker | No |
## Push hooks limit
@@ -93,6 +94,15 @@ From this page, you can repeat delivery with the same data by clicking **Resend
![Recent deliveries](img/webhook_logs.png)
+### Uninitialized repositories
+
+Some integrations fail with an error `Test Failed. Save Anyway` when you attempt to set them up on
+uninitialized repositories. This is because the default service test uses push data to build the
+payload for the test request, and it fails, because there are no push events for the project.
+
+To resolve this error, initialize the repository by pushing a test file to the project and set up
+the integration again.
+
## Contributing to integrations
Because GitLab is open source we can ship with the code and tests for all
@@ -100,9 +110,6 @@ plugins. This allows the community to keep the plugins up to date so that they
always work in newer GitLab versions.
For an overview of what integrations are available, please see the
-[project_services source directory][projects-code].
+[project_services source directory](https://gitlab.com/gitlab-org/gitlab/tree/master/app/models/project_services).
Contributions are welcome!
-
-[projects-code]: https://gitlab.com/gitlab-org/gitlab-foss/tree/master/app/models/project_services
-[permissions]: ../../permissions.md
diff --git a/doc/user/project/integrations/prometheus.md b/doc/user/project/integrations/prometheus.md
index bbed14ea93f..6c40e5b9696 100644
--- a/doc/user/project/integrations/prometheus.md
+++ b/doc/user/project/integrations/prometheus.md
@@ -1,3 +1,9 @@
+---
+stage: Monitor
+group: APM
+info: To determine the technical writer assigned to the Stage/Group associated with this page, see https://about.gitlab.com/handbook/engineering/ux/technical-writing/#designated-technical-writers
+---
+
# Prometheus integration
> [Introduced](https://gitlab.com/gitlab-org/gitlab-foss/-/merge_requests/8935) in GitLab 9.0.
@@ -66,6 +72,25 @@ It enables you to search as you type through all environments and select the one
![Monitoring Dashboard Environments](img/prometheus_dashboard_environments_v12_8.png)
+##### Select a dashboard
+
+The **dashboard** dropdown box above the dashboard displays the list of all dashboards available for the project.
+It enables you to search as you type through all dashboards and select the one you're looking for.
+
+![Monitoring Dashboard select](img/prometheus_dashboard_select_v_13_0.png)
+
+##### Mark a dashboard as favorite
+
+> [Introduced](https://gitlab.com/gitlab-org/gitlab/-/issues/214582) in GitLab 13.0.
+
+When viewing a dashboard, click the empty **Star dashboard** **{star-o}** button to mark a
+dashboard as a favorite. Starred dashboards display a solid star **{star}** button,
+and appear at the top of the dashboard select list.
+
+To remove dashboard from the favorites list, click the solid **Unstar Dashboard** **{star}** button.
+
+![Monitoring Dashboard favorite state toggle](img/toggle_metrics_user_starred_dashboard_v13_0.png)
+
#### About managed Prometheus deployments
Prometheus is deployed into the `gitlab-managed-apps` namespace, using the [official Helm chart](https://github.com/helm/charts/tree/master/stable/prometheus). Prometheus is only accessible within the cluster, with GitLab communicating through the [Kubernetes API](https://kubernetes.io/docs/concepts/overview/kubernetes-api/).
@@ -93,7 +118,7 @@ Integration with Prometheus requires the following:
#### Getting started
-Installing and configuring Prometheus to monitor applications is fairly straight forward.
+Installing and configuring Prometheus to monitor applications is fairly straightforward.
1. [Install Prometheus](https://prometheus.io/docs/prometheus/latest/installation/)
1. Set up one of the [supported monitoring targets](prometheus_library/index.md)
@@ -123,14 +148,32 @@ to integrate with.
1. Provide the domain name or IP address of your server, for example `http://thanos.example.com/` or `http://192.0.2.1/`.
1. Click **Save changes**.
+### Precedence with multiple Prometheus configurations
+
+Although you can enable both a [manual configuration](#manual-configuration-of-prometheus)
+and [auto configuration](#managed-prometheus-on-kubernetes) of Prometheus, only
+one of them will be used:
+
+- If you have enabled a
+ [Prometheus manual configuration](#manual-configuration-of-prometheus)
+ and a [managed Prometheus on Kubernetes](#managed-prometheus-on-kubernetes),
+ the manual configuration takes precedence and is used to run queries from
+ [dashboards](#defining-custom-dashboards-per-project) and [custom metrics](#adding-custom-metrics).
+- If you have managed Prometheus applications installed on Kubernetes clusters
+ at **different** levels (project, group, instance), the order of precedence is described in
+ [Cluster precedence](../../instance/clusters/index.md#cluster-precedence).
+- If you have managed Prometheus applications installed on multiple Kubernetes
+ clusters at the **same** level, the Prometheus application of a cluster with a
+ matching [environment scope](../../../ci/environments/index.md#scoping-environments-with-specs) is used.
+
## Monitoring CI/CD Environments
Once configured, GitLab will attempt to retrieve performance metrics for any
environment which has had a successful deployment.
-GitLab will automatically scan the Prometheus server for metrics from known servers like Kubernetes and NGINX, and attempt to identify individual environment. The supported metrics and scan process is detailed in our [Prometheus Metrics Library documentation](prometheus_library/index.md).
+GitLab will automatically scan the Prometheus server for metrics from known servers like Kubernetes and NGINX, and attempt to identify individual environments. The supported metrics and scan process is detailed in our [Prometheus Metrics Library documentation](prometheus_library/index.md).
-You can view the performance dashboard for an environment by [clicking on the monitoring button](../../../ci/environments.md#monitoring-environments).
+You can view the performance dashboard for an environment by [clicking on the monitoring button](../../../ci/environments/index.md#monitoring-environments).
### Adding custom metrics
@@ -152,10 +195,12 @@ A few fields are required:
- **Y-axis label**: Y axis title to display on the dashboard.
- **Unit label**: Query units, for example `req / sec`. Shown next to the value.
-Multiple metrics can be displayed on the same chart if the fields **Name**, **Type**, and **Y-axis label** match between metrics. For example, a metric with **Name** `Requests Rate`, **Type** `Business`, and **Y-axis label** `rec / sec` would display on the same chart as a second metric with the same values. A **Legend label** is suggested if this feature used.
+Multiple metrics can be displayed on the same chart if the fields **Name**, **Type**, and **Y-axis label** match between metrics. For example, a metric with **Name** `Requests Rate`, **Type** `Business`, and **Y-axis label** `rec / sec` would display on the same chart as a second metric with the same values. A **Legend label** is suggested if this feature is used.
#### Query Variables
+##### Predefined variables
+
GitLab supports a limited set of [CI variables](../../../ci/variables/README.md) in the Prometheus query. This is particularly useful for identifying a specific environment, for example with `ci_environment_slug`. The supported variables are:
- `ci_environment_slug`
@@ -168,10 +213,34 @@ GitLab supports a limited set of [CI variables](../../../ci/variables/README.md)
NOTE: **Note:**
Variables for Prometheus queries must be lowercase.
-There are 2 methods to specify a variable in a query or dashboard:
+##### User-defined variables
+
+[Variables can be defined](#templating-templating-properties) in a custom dashboard YAML file.
+
+##### Using variables
+
+Variables can be specified using double curly braces, such as `"{{ci_environment_slug}}"` ([added](https://gitlab.com/gitlab-org/gitlab/-/merge_requests/20793) in GitLab 12.7).
+
+Support for the `"%{ci_environment_slug}"` format was
+[removed](https://gitlab.com/gitlab-org/gitlab/-/merge_requests/31581) in GitLab 13.0.
+Queries that continue to use the old format will show no data.
+
+#### Query Variables from URL
-1. Variables can be specified using the [Liquid template format](https://shopify.dev/docs/liquid/reference/basics), for example `{{ci_environment_slug}}` ([added](https://gitlab.com/gitlab-org/gitlab/-/merge_requests/20793) in GitLab 12.6).
-1. You can also enclose it in quotation marks with curly braces with a leading percent, for example `"%{ci_environment_slug}"`. This method is deprecated though and support will be [removed in the next major release](https://gitlab.com/gitlab-org/gitlab/issues/37990).
+> [Introduced](https://gitlab.com/gitlab-org/gitlab/-/issues/214500) in GitLab 13.0.
+
+GitLab supports setting custom variables through URL parameters. Surround the variable
+name with double curly braces (`{{example}}`) to interpolate the variable in a query:
+
+```plaintext
+avg(sum(container_memory_usage_bytes{container_name!="{{pod}}"}) by (job)) without (job) /1024/1024/1024'
+```
+
+The URL for this query would be:
+
+```plaintext
+http://gitlab.com/<user>/<project>/-/environments/<environment_id>/metrics?dashboard=.gitlab%2Fdashboards%2Fcustom.yml&pod=POD
+```
#### Editing additional metrics from the dashboard
@@ -261,19 +330,29 @@ If you select another branch, this branch should be merged to your **default** b
Dashboards have several components:
-- Panel groups, which comprise of panels.
+- Templating variables.
+- Panel groups, which consist of panels.
- Panels, which support one or more metrics.
The following tables outline the details of expected properties.
-**Dashboard properties:**
+##### **Dashboard (top-level) properties**
| Property | Type | Required | Description |
| ------ | ------ | ------ | ------ |
| `dashboard` | string | yes | Heading for the dashboard. Only one dashboard should be defined per file. |
| `panel_groups` | array | yes | The panel groups which should be on the dashboard. |
+| `templating` | Hash | no | Top level key under which templating related options can be added. |
+
+##### **Templating (`templating`) properties**
+
+| Property | Type | Required | Description |
+| -------- | ---- | -------- | ----------- |
+| `variables` | Hash | no | Variables can be defined here. |
-**Panel group (`panel_groups`) properties:**
+Read the documentation on [templating](#templating-variables-for-metrics-dashboards).
+
+##### **Panel group (`panel_groups`) properties**
| Property | Type | Required | Description |
| ------ | ------ | ------ | ------ |
@@ -281,7 +360,7 @@ The following tables outline the details of expected properties.
| `priority` | number | optional, defaults to order in file | Order to appear on the dashboard. Higher number means higher priority, which will be higher on the page. Numbers do not need to be consecutive. |
| `panels` | array | required | The panels which should be in the panel group. |
-**Panel (`panels`) properties:**
+##### **Panel (`panels`) properties**
| Property | Type | Required | Description |
| ------ | ------ | ------ | ------- |
@@ -293,19 +372,19 @@ The following tables outline the details of expected properties.
| `weight` | number | no, defaults to order in file | Order to appear within the grouping. Lower number means higher priority, which will be higher on the page. Numbers do not need to be consecutive. |
| `metrics` | array | yes | The metrics which should be displayed in the panel. Any number of metrics can be displayed when `type` is `area-chart` or `line-chart`, whereas only 3 can be displayed when `type` is `anomaly-chart`. |
-**Axis (`panels[].y_axis`) properties:**
+##### **Axis (`panels[].y_axis`) properties**
-| Property | Type | Required | Description |
-| ----------- | ------ | ------------------------- | -------------------------------------------------------------------- |
-| `name` | string | no, but highly encouraged | Y-Axis label for the panel, it will replace `y_label` if set. |
-| `format` | string | no, defaults to `number` | Unit format used. See the [full list of units](prometheus_units.md). |
-| `precision` | number | no, defaults to `2` | Number of decimals to display in the number. |
+| Property | Type | Required | Description |
+| ----------- | ------ | ----------------------------- | -------------------------------------------------------------------- |
+| `name` | string | no, but highly encouraged | Y-Axis label for the panel. Replaces `y_label` if set. |
+| `format` | string | no, defaults to `engineering` | Unit format used. See the [full list of units](prometheus_units.md). |
+| `precision` | number | no, defaults to `2` | Number of decimal places to display in the number. | |
-**Metrics (`metrics`) properties:**
+##### **Metrics (`metrics`) properties**
| Property | Type | Required | Description |
| ------ | ------ | ------ | ------ |
-| `id` | string | no | Used for associating dashboard metrics with database records. Must be unique across dashboard configuration files. Required for [alerting](#setting-up-alerts-for-prometheus-metrics) (support not yet enabled, see [relevant issue](https://gitlab.com/gitlab-org/gitlab-foss/issues/60319)). |
+| `id` | string | no | Used for associating dashboard metrics with database records. Must be unique across dashboard configuration files. Required for [alerting](#setting-up-alerts-for-prometheus-metrics) (support not yet enabled, see [relevant issue](https://gitlab.com/gitlab-org/gitlab/-/issues/27980)). |
| `unit` | string | yes | Defines the unit of the query's return data. |
| `label` | string | no, but highly encouraged | Defines the legend-label for the query. Should be unique within the panel's metrics. Can contain time series labels as interpolated variables. |
| `query` | string | yes if `query_range` is not defined | Defines the Prometheus query to be used to populate the chart/panel. If defined, the `query` endpoint of the [Prometheus API](https://prometheus.io/docs/prometheus/latest/querying/api/) will be utilized. |
@@ -610,21 +689,122 @@ Note the following properties:
![heatmap panel type](img/heatmap_panel_type.png)
+### Templating variables for metrics dashboards
+
+Templating variables can be used to make your metrics dashboard more versatile.
+
+#### Templating variable types
+
+`templating` is a top-level key in the
+[dashboard YAML](#dashboard-top-level-properties).
+Define your variables in the `variables` key, under `templating`. The value of
+the `variables` key should be a hash, and each key under `variables`
+defines a templating variable on the dashboard.
+
+A variable can be used in a Prometheus query in the same dashboard using the syntax
+described [here](#using-variables).
+
+##### `text` variable type
+
+CAUTION: **Warning:**
+This variable type is an _alpha_ feature, and is subject to change at any time
+without prior notice!
+
+For each `text` variable defined in the dashboard YAML, there will be a free text
+box on the dashboard UI, allowing you to enter a value for each variable.
+
+The `text` variable type supports a simple and a full syntax.
+
+###### Simple syntax
+
+This example creates a variable called `variable1`, with a default value
+of `default value`:
+
+```yaml
+templating:
+ variables:
+ variable1: 'default value' # `text` type variable with `default value` as its default.
+```
+
+###### Full syntax
+
+This example creates a variable called `variable1`, with a default value of `default`.
+The label for the text box on the UI will be the value of the `label` key:
+
+```yaml
+templating:
+ variables:
+ variable1: # The variable name that can be used in queries.
+ label: 'Variable 1' # (Optional) label that will appear in the UI for this text box.
+ type: text
+ options:
+ default_value: 'default' # (Optional) default value.
+```
+
+##### `custom` variable type
+
+CAUTION: **Warning:**
+This variable type is an _alpha_ feature, and is subject to change at any time
+without prior notice!
+
+Each `custom` variable defined in the dashboard YAML creates a dropdown
+selector on the dashboard UI, allowing you to select a value for each variable.
+
+The `custom` variable type supports a simple and a full syntax.
+
+###### Simple syntax
+
+This example creates a variable called `variable1`, with a default value of `value1`.
+The dashboard UI will display a dropdown with `value1`, `value2` and `value3`
+as the choices.
+
+```yaml
+templating:
+ variables:
+ variable1: ['value1', 'value2', 'value3']
+```
+
+###### Full syntax
+
+This example creates a variable called `variable1`, with a default value of `var1_option_2`.
+The label for the text box on the UI will be the value of the `label` key.
+The dashboard UI will display a dropdown with `Option 1` and `Option 2`
+as the choices.
+
+If you select `Option 1` from the dropdown, the variable will be replaced with `value option 1`.
+Similarly, if you select `Option 2`, the variable will be replaced with `value_option_2`:
+
+```yaml
+templating:
+ variables:
+ variable1: # The variable name that can be used in queries.
+ label: 'Variable 1' # (Optional) label that will appear in the UI for this dropdown.
+ type: custom
+ options:
+ values:
+ - value: 'value option 1' # The value that will replace the variable in queries.
+ text: 'Option 1' # (Optional) Text that will appear in the UI dropdown.
+ - value: 'value_option_2'
+ text: 'Option 2'
+ default: true # (Optional) This option should be the default value of this variable.
+```
+
### View and edit the source file of a custom dashboard
> [Introduced](https://gitlab.com/gitlab-org/gitlab/issues/34779) in GitLab 12.5.
When viewing a custom dashboard of a project, you can view the original
-`.yml` file by clicking on **Edit dashboard** button.
+`.yml` file by clicking on the **Edit dashboard** button.
### Chart Context Menu
From each of the panels in the dashboard, you can access the context menu by clicking the **{ellipsis_v}** **More actions** dropdown box above the upper right corner of the panel to take actions related to the chart's data.
-![Context Menu](img/panel_context_menu_v12_10.png)
+![Context Menu](img/panel_context_menu_v13_0.png)
The options are:
+- [Expand panel](#expand-panel)
- [View logs](#view-logs-ultimate)
- [Download CSV](#downloading-data-as-csv)
- [Copy link to chart](#embedding-gitlab-managed-kubernetes-metrics)
@@ -632,7 +812,8 @@ The options are:
### Dashboard Annotations
-> [Introduced](https://gitlab.com/gitlab-org/gitlab/issues/211330) in GitLab 12.10 (enabled by feature flag `metrics_dashboard_annotations`).
+> - [Introduced](https://gitlab.com/gitlab-org/gitlab/issues/211330) in GitLab 12.10 (enabled by feature flag `metrics_dashboard_annotations`).
+> - [Feature flag removed](https://gitlab.com/gitlab-org/gitlab/-/issues/215224) in GitLab 13.0.
You can use **Metrics Dashboard Annotations** to mark any important events on
every metrics dashboard by adding annotations to it. While viewing a dashboard,
@@ -644,6 +825,18 @@ its description.
You can create annotations by making requests to the
[Metrics dashboard annotations API](../../../api/metrics_dashboard_annotations.md)
+![Annotations UI](img/metrics_dashboard_annotations_ui_v13.0.png)
+
+### Expand panel
+
+> [Introduced](https://gitlab.com/groups/gitlab-org/-/epics/3100) in GitLab 13.0.
+
+To view a larger version of a visualization, expand the panel by clicking the
+**{ellipsis_v}** **More actions** icon and selecting **Expand panel**.
+
+To return to the metrics dashboard, click the **Back** button in your
+browser, or pressing the <kbd>Esc</kbd> key.
+
### View Logs **(ULTIMATE)**
> [Introduced](https://gitlab.com/gitlab-org/gitlab/issues/122013) in GitLab 12.8.
@@ -708,14 +901,14 @@ receivers:
...
```
-In order for GitLab to associate your alerts with an [environment](../../../ci/environments.md), you need to configure a `gitlab_environment_name` label on the alerts you set up in Prometheus. The value of this should match the name of your Environment in GitLab.
+In order for GitLab to associate your alerts with an [environment](../../../ci/environments/index.md), you need to configure a `gitlab_environment_name` label on the alerts you set up in Prometheus. The value of this should match the name of your Environment in GitLab.
### Taking action on incidents **(ULTIMATE)**
>- [Introduced](https://gitlab.com/gitlab-org/gitlab/issues/4925) in [GitLab Ultimate](https://about.gitlab.com/pricing/) 11.11.
>- [From GitLab Ultimate 12.5](https://gitlab.com/gitlab-org/gitlab/issues/13401), when GitLab receives a recovery alert, it will automatically close the associated issue.
-Alerts can be used to trigger actions, like open an issue automatically (enabled by default since `12.1`). To configure the actions:
+Alerts can be used to trigger actions, like opening an issue automatically (enabled by default since `12.1`). To configure the actions:
1. Navigate to your project's **Settings > Operations > Incidents**.
1. Enable the option to create issues.
@@ -734,7 +927,7 @@ Once enabled, an issue will be opened automatically when an alert is triggered w
- Optional list of attached annotations extracted from `annotations/*`
- Alert [GFM](../../markdown.md): GitLab Flavored Markdown from `annotations/gitlab_incident_markdown`
-When GitLab receives a **Recovery Alert**, it will automatically close the associated issue. This action will be recorded as a system message on the issue indicated that it was closed automatically by the GitLab Alert bot.
+When GitLab receives a **Recovery Alert**, it will automatically close the associated issue. This action will be recorded as a system message on the issue indicating that it was closed automatically by the GitLab Alert bot.
To further customize the issue, you can add labels, mentions, or any other supported [quick action](../quick_actions.md) in the selected issue template, which will apply to all incidents. To limit quick actions or other information to only specific types of alerts, use the `annotations/gitlab_incident_markdown` field.
@@ -812,6 +1005,8 @@ Metric charts may also be hidden:
![Show Hide](img/hide_embedded_metrics_v12_10.png)
+You can open the link directly into your browser for a [detailed view of the data](#expand-panel).
+
### Embedding metrics in issue templates
It is also possible to embed either the default dashboard metrics or individual metrics in issue templates. For charts to render side-by-side, links to the entire metrics dashboard or individual metrics should be separated by either a comma or a space.
@@ -907,12 +1102,20 @@ Prerequisites for embedding from a Grafana instance:
1. In the upper-left corner of the page, select a specific value for each variable required for the queries in the chart.
![Select Query Variables](img/select_query_variables_v12_5.png)
1. In Grafana, click on a panel's title, then click **Share** to open the panel's sharing dialog to the **Link** tab. If you click the _dashboard's_ share panel instead, GitLab will attempt to embed the first supported panel on the dashboard (if available).
-1. If your Prometheus queries use Grafana's custom template variables, ensure that "Template variables" option is toggled to **On**. Of Grafana global template variables, only `$__interval`, `$__from`, and `$__to` are currently supported. Toggle **On** the "Current time range" option to specify the time range of the chart. Otherwise, the default range will be the last 8 hours.
+1. If your Prometheus queries use Grafana's custom template variables, ensure that the "Template variables" option is toggled to **On**. Of Grafana global template variables, only `$__interval`, `$__from`, and `$__to` are currently supported. Toggle **On** the "Current time range" option to specify the time range of the chart. Otherwise, the default range will be the last 8 hours.
![Grafana Sharing Dialog](img/grafana_sharing_dialog_v12_5.png)
1. Click **Copy** to copy the URL to the clipboard.
1. In GitLab, paste the URL into a Markdown field and save. The chart will take a few moments to render.
![GitLab Rendered Grafana Panel](img/rendered_grafana_embed_v12_5.png)
+## Metrics dashboard visibility
+
+> [Introduced](https://gitlab.com/gitlab-org/gitlab/-/issues/201924) in GitLab 13.0.
+
+You can set the visibility of the metrics dashboard to **Only Project Members**
+or **Everyone With Access**. When set to **Everyone with Access**, the metrics
+dashboard is visible to authenticated and non-authenticated users.
+
## Troubleshooting
When troubleshooting issues with a managed Prometheus app, it is often useful to
diff --git a/doc/user/project/integrations/prometheus_library/cloudwatch.md b/doc/user/project/integrations/prometheus_library/cloudwatch.md
index 143130aebea..911493cdae9 100644
--- a/doc/user/project/integrations/prometheus_library/cloudwatch.md
+++ b/doc/user/project/integrations/prometheus_library/cloudwatch.md
@@ -1,3 +1,9 @@
+---
+stage: Monitor
+group: APM
+info: To determine the technical writer assigned to the Stage/Group associated with this page, see https://about.gitlab.com/handbook/engineering/ux/technical-writing/#designated-technical-writers
+---
+
# Monitoring AWS Resources
> [Introduced](https://gitlab.com/gitlab-org/gitlab-foss/-/merge_requests/12621) in GitLab 9.4
diff --git a/doc/user/project/integrations/prometheus_library/haproxy.md b/doc/user/project/integrations/prometheus_library/haproxy.md
index fa3590af8cf..712805b75f2 100644
--- a/doc/user/project/integrations/prometheus_library/haproxy.md
+++ b/doc/user/project/integrations/prometheus_library/haproxy.md
@@ -1,3 +1,9 @@
+---
+stage: Monitor
+group: APM
+info: To determine the technical writer assigned to the Stage/Group associated with this page, see https://about.gitlab.com/handbook/engineering/ux/technical-writing/#designated-technical-writers
+---
+
# Monitoring HAProxy
> [Introduced](https://gitlab.com/gitlab-org/gitlab-foss/-/merge_requests/12621) in GitLab 9.4
diff --git a/doc/user/project/integrations/prometheus_library/index.md b/doc/user/project/integrations/prometheus_library/index.md
index c2b3676b23f..6f2c2477eee 100644
--- a/doc/user/project/integrations/prometheus_library/index.md
+++ b/doc/user/project/integrations/prometheus_library/index.md
@@ -1,3 +1,9 @@
+---
+stage: Monitor
+group: APM
+info: To determine the technical writer assigned to the Stage/Group associated with this page, see https://about.gitlab.com/handbook/engineering/ux/technical-writing/#designated-technical-writers
+---
+
# Prometheus Metrics library
> [Introduced](https://gitlab.com/gitlab-org/gitlab-foss/-/merge_requests/8935) in GitLab 9.0.
diff --git a/doc/user/project/integrations/prometheus_library/kubernetes.md b/doc/user/project/integrations/prometheus_library/kubernetes.md
index ca1555c793b..29efe08e53d 100644
--- a/doc/user/project/integrations/prometheus_library/kubernetes.md
+++ b/doc/user/project/integrations/prometheus_library/kubernetes.md
@@ -1,3 +1,9 @@
+---
+stage: Monitor
+group: APM
+info: To determine the technical writer assigned to the Stage/Group associated with this page, see https://about.gitlab.com/handbook/engineering/ux/technical-writing/#designated-technical-writers
+---
+
# Monitoring Kubernetes
> [Introduced](https://gitlab.com/gitlab-org/gitlab-foss/-/merge_requests/8935) in GitLab 9.0.
diff --git a/doc/user/project/integrations/prometheus_library/nginx.md b/doc/user/project/integrations/prometheus_library/nginx.md
index d5f078f3ddf..eda6f64ccac 100644
--- a/doc/user/project/integrations/prometheus_library/nginx.md
+++ b/doc/user/project/integrations/prometheus_library/nginx.md
@@ -1,3 +1,9 @@
+---
+stage: Monitor
+group: APM
+info: To determine the technical writer assigned to the Stage/Group associated with this page, see https://about.gitlab.com/handbook/engineering/ux/technical-writing/#designated-technical-writers
+---
+
# Monitoring NGINX
> [Introduced](https://gitlab.com/gitlab-org/gitlab-foss/-/merge_requests/12621) in GitLab 9.4
diff --git a/doc/user/project/integrations/prometheus_library/nginx_ingress.md b/doc/user/project/integrations/prometheus_library/nginx_ingress.md
index 62f8c08e298..b2bc217e8bf 100644
--- a/doc/user/project/integrations/prometheus_library/nginx_ingress.md
+++ b/doc/user/project/integrations/prometheus_library/nginx_ingress.md
@@ -1,3 +1,9 @@
+---
+stage: Monitor
+group: APM
+info: To determine the technical writer assigned to the Stage/Group associated with this page, see https://about.gitlab.com/handbook/engineering/ux/technical-writing/#designated-technical-writers
+---
+
# Monitoring NGINX Ingress Controller
> [Introduced](https://gitlab.com/gitlab-org/gitlab-foss/-/merge_requests/22133) in GitLab 11.7.
diff --git a/doc/user/project/integrations/prometheus_library/nginx_ingress_vts.md b/doc/user/project/integrations/prometheus_library/nginx_ingress_vts.md
index af3b725deb6..6ba0a7610f6 100644
--- a/doc/user/project/integrations/prometheus_library/nginx_ingress_vts.md
+++ b/doc/user/project/integrations/prometheus_library/nginx_ingress_vts.md
@@ -1,3 +1,9 @@
+---
+stage: Monitor
+group: APM
+info: To determine the technical writer assigned to the Stage/Group associated with this page, see https://about.gitlab.com/handbook/engineering/ux/technical-writing/#designated-technical-writers
+---
+
# Monitoring NGINX Ingress Controller with VTS metrics
> [Introduced](https://gitlab.com/gitlab-org/gitlab-foss/-/merge_requests/13438) in GitLab 9.5.
diff --git a/doc/user/project/integrations/prometheus_units.md b/doc/user/project/integrations/prometheus_units.md
index 9df9f52ceb1..691d20e5de2 100644
--- a/doc/user/project/integrations/prometheus_units.md
+++ b/doc/user/project/integrations/prometheus_units.md
@@ -1,3 +1,9 @@
+---
+stage: Monitor
+group: APM
+info: To determine the technical writer assigned to the Stage/Group associated with this page, see https://about.gitlab.com/handbook/engineering/ux/technical-writing/#designated-technical-writers
+---
+
# Unit formats reference
> [Introduced](https://gitlab.com/gitlab-org/gitlab/issues/201999) in GitLab 12.9.
@@ -5,19 +11,78 @@
You can select units to format your charts by adding `format` to your
[axis configuration](prometheus.md#dashboard-yaml-properties).
+## Internationalization and localization
+
+Currently, your [internationalization and localization options](https://en.wikipedia.org/wiki/Internationalization_and_localization) for number formatting are dependent on the system you are using i.e. your OS or browser.
+
+## Engineering Notation
+
+For generic or default data, numbers are formatted according to the current locale in [engineering notation](https://en.wikipedia.org/wiki/Engineering_notation).
+
+While an [engineering notation exists for the web](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/NumberFormat), GitLab uses a version based off the [scientific notation](https://en.wikipedia.org/wiki/Scientific_notation). GitLab formatting acts in accordance with SI prefixes. For example, using GitLab notation, `1500.00` becomes `1.5k` instead of `1.5E3`. Keep this distinction in mind when using the engineering notation for your metrics.
+
+Formats: `engineering`
+
+SI prefixes:
+
+| Name | Symbol | Value |
+| ---------- | ------- | -------------------------- |
+| `yotta` | Y | 1000000000000000000000000 |
+| `zetta` | Z | 1000000000000000000000 |
+| `exa` | E | 1000000000000000000 |
+| `peta` | P | 1000000000000000 |
+| `tera` | T | 1000000000000 |
+| `giga` | G | 1000000000 |
+| `mega` | M | 1000000 |
+| `kilo` | k | 1000 |
+| `milli` | m | 0.001 |
+| `micro` | μ | 0.000001 |
+| `nano` | n | 0.000000001 |
+| `pico` | p | 0.000000000001 |
+| `femto` | f | 0.000000000000001 |
+| `atto` | a | 0.000000000000000001 |
+| `zepto` | z | 0.000000000000000000001 |
+| `yocto` | y | 0.000000000000000000000001 |
+
+**Examples:**
+
+| Data | Displayed |
+| --------------------------------- | --------- |
+| `0.000000000000000000000008` | 8y |
+| `0.000000000000000000008` | 8z |
+| `0.000000000000000008` | 8a |
+| `0.000000000000008` | 8f |
+| `0.000000000008` | 8p |
+| `0.000000008` | 8n |
+| `0.000008` | 8μ |
+| `0.008` | 8m |
+| `10` | 10 |
+| `1080` | 1.08k |
+| `18000` | 18k |
+| `18888` | 18.9k |
+| `188888` | 189k |
+| `18888888` | 18.9M |
+| `1888888888` | 1.89G |
+| `1888888888888` | 1.89T |
+| `1888888888888888` | 1.89P |
+| `1888888888888888888` | 1.89E |
+| `1888888888888888888888` | 1.89Z |
+| `1888888888888888888888888` | 1.89Y |
+| `1888888888888888888888888888` | 1.89e+27 |
+
## Numbers
-For generic data, numbers are formatted according to the current locale.
+For number data, numbers are formatted according to the current locale.
Formats: `number`
**Examples:**
-| Data | Displayed |
-| --------- | --------- |
-| `10` | 1 |
-| `1000` | 1,000 |
-| `1000000` | 1,000,000 |
+| Data | Displayed |
+| ---------- | --------- |
+| `10` | 1 |
+| `1000` | 1,000 |
+| `1000000` | 1,000,000 |
## Percentage
diff --git a/doc/user/project/integrations/slack.md b/doc/user/project/integrations/slack.md
index ba2a8f55d84..419340c14ef 100644
--- a/doc/user/project/integrations/slack.md
+++ b/doc/user/project/integrations/slack.md
@@ -41,7 +41,7 @@ an error message and keep troubleshooting from there.
You may see an entry similar to the following in your Sidekiq log:
-```text
+```plaintext
2019-01-10_13:22:08.42572 2019-01-10T13:22:08.425Z 6877 TID-abcdefg ProjectServiceWorker JID-3bade5fb3dd47a85db6d78c5 ERROR: {:class=>"ProjectServiceWorker", :service_class=>"SlackService", :message=>"SSL_connect returned=1 errno=0 state=error: certificate verify failed"}
```
diff --git a/doc/user/project/integrations/webex_teams.md b/doc/user/project/integrations/webex_teams.md
new file mode 100644
index 00000000000..a6e688887b6
--- /dev/null
+++ b/doc/user/project/integrations/webex_teams.md
@@ -0,0 +1,24 @@
+# Webex Teams service
+
+You can configure GitLab to send notifications to a Webex Teams space.
+
+## Create a webhook for the space
+
+1. Go to the [Incoming Webooks app page](https://apphub.webex.com/teams/applications/incoming-webhooks-cisco-systems).
+1. Click **Connect** and log in to Webex Teams, if required.
+1. Enter a name for the webhook and select the space that will receive the notifications.
+1. Click **ADD**.
+1. Copy the **Webhook URL**.
+
+## Configure settings in GitLab
+
+Once you have a webhook URL for your Webex Teams space, you can configure GitLab to send notifications.
+
+1. Navigate to **Project > Settings > Integrations**.
+1. Select the **Webex Teams** integration.
+1. Ensure that the **Active** toggle is enabled.
+1. Select the checkboxes corresponding to the GitLab events you want to receive in Webex Teams.
+1. Paste the **Webhook** URL for the Webex Teams space.
+1. Configure the remaining options and then click **Test settings and save changes**.
+
+The Webex Teams space will begin to receive all applicable GitLab events.
diff --git a/doc/user/project/integrations/webhooks.md b/doc/user/project/integrations/webhooks.md
index 6dd2fd3b61b..89e84dda0e8 100644
--- a/doc/user/project/integrations/webhooks.md
+++ b/doc/user/project/integrations/webhooks.md
@@ -1294,7 +1294,7 @@ X-Gitlab-Event: Job Hook
}
```
-Note that `commit.id` is the id of the pipeline, not the id of the commit.
+Note that `commit.id` is the ID of the pipeline, not the ID of the commit.
## Image URL rewriting
diff --git a/doc/user/project/integrations/youtrack.md b/doc/user/project/integrations/youtrack.md
index a72eaaa9ff1..119a53f5c35 100644
--- a/doc/user/project/integrations/youtrack.md
+++ b/doc/user/project/integrations/youtrack.md
@@ -14,8 +14,8 @@ To enable YouTrack integration in a project:
| Field | Description |
|:----------------|:------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------|
| **Description** | Name for the issue tracker (to differentiate between instances, for example). |
- | **Project url** | URL to the project in YouTrack which is being linked to this GitLab project. |
- | **Issues url** | URL to the issue in YouTrack project that is linked to this GitLab project. Note that the **Issues url** requires `:id` in the URL. This ID is used by GitLab as a placeholder to replace the issue number. |
+ | **Project URL** | URL to the project in YouTrack which is being linked to this GitLab project. |
+ | **Issues URL** | URL to the issue in YouTrack project that is linked to this GitLab project. Note that the **Issues URL** requires `:id` in the URL. This ID is used by GitLab as a placeholder to replace the issue number. |
1. Click the **Save changes** button.
diff --git a/doc/user/project/issue_board.md b/doc/user/project/issue_board.md
index 5bc71337e44..9903a711987 100644
--- a/doc/user/project/issue_board.md
+++ b/doc/user/project/issue_board.md
@@ -9,220 +9,271 @@ organize, and visualize a workflow for a feature or product release.
It can be used as a [Kanban](https://en.wikipedia.org/wiki/Kanban_(development)) or a
[Scrum](https://en.wikipedia.org/wiki/Scrum_(software_development)) board.
-It provides perfect pairing between issue tracking and project management,
+It pairs issue tracking and project management,
keeping everything in the same place, so that you don't need to jump
between different platforms to organize your workflow.
-With GitLab Issue Boards, you organize your issues in lists that correspond to
+With issue boards, you organize your issues in lists that correspond to
their assigned labels, visualizing issues designed as cards throughout those lists.
-You define your process and GitLab organizes it for you. You add your labels
+You define your process, and GitLab organizes it for you. You add your labels
then create the corresponding list to pull in your existing issues. When
you're ready, you can drag and drop your issue cards from one step to the next.
-![GitLab Issue Board - Core](img/issue_boards_core.png)
+![GitLab issue board - Core](img/issue_boards_core.png)
-**<i class="fa fa-youtube-play youtube" aria-hidden="true"></i>
+<i class="fa fa-youtube-play youtube" aria-hidden="true"></i>
Watch a [video presentation](https://youtu.be/UWsJ8tkHAa8) of
-Issue Boards** (version introduced in GitLab 8.11 - August 2016).
+the Issue Board feature (introduced in GitLab 8.11 - August 2016).
-### Advanced features of Issue Boards
+### Advanced features of issue boards
- Create multiple issue boards per project.
- Create multiple issue boards per group. **(PREMIUM)**
- Add lists for [assignees](#assignee-lists-premium) and [milestones](#milestone-lists-premium). **(PREMIUM)**
-Check all the [advanced features of Issue Boards](#gitlab-enterprise-features-for-issue-boards)
-below.
+Check all the [GitLab Enterprise features for issue boards](#gitlab-enterprise-features-for-issue-boards).
-![GitLab Issue Boards - Premium](img/issue_boards_premium.png)
+![GitLab issue boards - Premium](img/issue_boards_premium.png)
+
+---
## How it works
-The Issue Board builds on GitLab's existing
+The Issue Board feature builds on GitLab's existing
[issue tracking functionality](issues/index.md#issues-list) and
-leverages the power of [labels](labels.md) by utilizing them as lists of the scrum board.
+[labels](labels.md) by using them as lists of the Scrum board.
-With the Issue Board you can have a different view of your issues while
+With issue boards you can have a different view of your issues while
maintaining the same filtering and sorting abilities you see across the
-issue tracker. An Issue Board is based on its project's label structure, therefore, it
+issue tracker. An issue board is based on its project's label structure, so it
applies the same descriptive labels to indicate placement on the board, keeping
consistency throughout the entire development lifecycle.
-An Issue Board shows you what issues your team is working on, who is assigned to each,
+An issue board shows you what issues your team is working on, who is assigned to each,
and where in the workflow those issues are.
You create issues, host code, perform reviews, build, test,
-and deploy from one single platform. Issue Boards help you to visualize
-and manage the entire process _in_ GitLab.
+and deploy from one single platform. Issue boards help you to visualize
+and manage the entire process in GitLab.
-With [Multiple Issue Boards](#use-cases-for-multiple-issue-boards),
+With [multiple issue boards](#use-cases-for-multiple-issue-boards),
you go even further, as you can not only keep yourself and your project
-organized from a broader perspective with one Issue Board per project,
+organized from a broader perspective with one issue board per project,
but also allow your team members to organize their own workflow by creating
-multiple Issue Boards within the same project.
+multiple issue boards within the same project.
## Use cases
-There are many ways to use GitLab Issue Boards tailored to your own preferred workflow.
-Here are some common use cases for Issue Boards.
+There are many ways to use GitLab issue boards tailored to your own preferred workflow.
+Here are some common use cases for issue boards.
-### Use cases for a single Issue Board
+### Use cases for a single issue board
-GitLab Workflow allows you to discuss proposals in issues, categorize them
-with labels, and from there organize and prioritize them with Issue Boards.
+With the GitLab Workflow you can discuss proposals in issues, categorize them
+with labels, and from there, organize and prioritize them with issue boards.
For example, let's consider this simplified development workflow:
-1. You have a repository hosting your app's codebase
- and your team actively contributing to code
-1. Your **backend** team starts working a new
- implementation, gathers feedback and approval, and pass it over to **frontend**
-1. When frontend is complete, the new feature is deployed to **staging** to be tested
-1. When successful, it is deployed to **production**
+1. You have a repository that hosts your application's codebase, and your team actively contributes code.
+1. Your **backend** team starts working on a new implementation, gathers feedback and approval, and
+ passes it over to the **frontend** team.
+1. When frontend is complete, the new feature is deployed to a **staging** environment to be tested.
+1. When successful, it's deployed to **production**.
-If we have the labels "**backend**", "**frontend**", "**staging**", and
-"**production**", and an Issue Board with a list for each, we can:
+If you have the labels "**backend**", "**frontend**", "**staging**", and
+"**production**", and an issue board with a list for each, you can:
-- Visualize the entire flow of implementations since the
- beginning of the development life cycle until deployed to production
-- Prioritize the issues in a list by moving them vertically
-- Move issues between lists to organize them according to the labels you've set
-- Add multiple issues to lists in the board by selecting one or more existing issues
+- Visualize the entire flow of implementations since the beginning of the development life cycle
+ until deployed to production.
+- Prioritize the issues in a list by moving them vertically.
+- Move issues between lists to organize them according to the labels you've set.
+- Add multiple issues to lists in the board by selecting one or more existing issues.
![issue card moving](img/issue_board_move_issue_card_list.png)
-### Use cases for Multiple Issue Boards
+---
+
+### Use cases for multiple issue boards
-With [Multiple Issue Boards](#multiple-issue-boards),
+With [multiple issue boards](#multiple-issue-boards),
each team can have their own board to organize their workflow individually.
#### Scrum team
-With Multiple Issue Boards, each team has one board. Now you can move issues through each
+With multiple issue boards, each team has one board. Now you can move issues through each
part of the process. For instance: **To Do**, **Doing**, and **Done**.
#### Organization of topics
-Create lists to order things by topic and quickly change them between topics or groups,
-such as between **UX**, **Frontend**, and **Backend**. The changes will be reflected across boards,
-as changing lists will update the label accordingly.
+Create lists to order issues by topic and quickly change them between topics or groups,
+such as between **UX**, **Frontend**, and **Backend**. The changes are reflected across boards,
+as changing lists updates the labels on each issue accordingly.
#### Advanced team handover
-For example, suppose we have a UX team with an Issue Board that contains:
+For example, suppose we have a UX team with an issue board that contains:
- **To Do**
- **Doing**
- **Frontend**
-When done with something, they move the card to **Frontend**. The Frontend team's board looks like:
+When finished with something, they move the card to **Frontend**. The Frontend team's board looks like:
- **Frontend**
- **Doing**
- **Done**
-Cards finished by the UX team will automatically appear in the **Frontend** column when they're ready for them.
+Cards finished by the UX team automatically appear in the **Frontend** column when they are ready
+for them.
NOTE: **Note:**
For a broader use case, please see the blog post
[GitLab Workflow, an Overview](https://about.gitlab.com/blog/2016/10/25/gitlab-workflow-an-overview/#gitlab-workflow-use-case-scenario).
For a real use case example, you can read why
-[Codepen decided to adopt Issue Boards](https://about.gitlab.com/blog/2017/01/27/codepen-welcome-to-gitlab/#project-management-everything-in-one-place)
+[Codepen decided to adopt issue boards](https://about.gitlab.com/blog/2017/01/27/codepen-welcome-to-gitlab/#project-management-everything-in-one-place)
to improve their workflow with multiple boards.
#### Quick assignments
-Create lists for each of your team members and quickly drag-and-drop issues onto each team member.
+Create lists for each of your team members and quickly drag and drop issues onto each team member's
+list.
+
+## Issue board terminology
+
+An **issue board** represents a unique view of your issues. It can have multiple lists with each
+list consisting of issues represented by cards.
+
+A **list** is a column on the issue board that displays issues matching certain attributes.
+In addition to the default "Open" and "Closed" lists, each additional list shows issues matching
+your chosen label, assignee, or milestone. On the top of each list you can see the number of issues
+that belong to it. Types of lists include:
+
+- **Open** (default): all open issues that do not belong to one of the other lists.
+ Always appears as the leftmost list.
+- **Closed** (default): all closed issues. Always appears as the rightmost list.
+- **Label list**: all open issues for a label.
+- [**Assignee list**](#assignee-lists-premium): all open issues assigned to a user.
+- [**Milestone list**](#milestone-lists-premium): all open issues for a milestone.
-## Issue Board terminology
+A **Card** is a box on a list, and it represents an issue. You can drag cards from one list to
+another to change their label, assignee, or milestone. The information you can see on a
+card includes:
-- **Issue Board** - Each board represents a unique view for your issues. It can have multiple lists with each list consisting of issues represented by cards.
-- **List** - A column on the issue board that displays issues matching certain attributes. In addition to the default lists of 'Open' and 'Closed' issue, each additional list will show issues matching your chosen label or assignee. On the top of that list you can see the number of issues that belong to it.
- - **Label list**: a list based on a label. It shows all opened issues with that label.
- - **Assignee list**: a list which includes all issues assigned to a user.
- - **Open** (default): shows all open issues that do not belong to one of the other lists. Always appears as the leftmost list.
- - **Closed** (default): shows all closed issues. Always appears as the rightmost list.
-- **Card** - A box in the list that represents an individual issue. The information you can see on a card consists of the issue number, the issue title, the assignee, and the labels associated with the issue. You can drag cards from one list to another to change their label or assignee from that of the source list to that of the destination list.
+- Issue title
+- Associated labels
+- Issue number
+- Assignee
## Permissions
-[Reporters and up](../permissions.md) can use all the functionality of the
-Issue Board to create or delete lists, and drag issues from one list to another.
+Users with the [Reporter and higher roles](../permissions.md) can use all the functionality of the
+Issue Board feature to create or delete lists and drag issues from one list to another.
-## GitLab Enterprise features for Issue Boards
+## GitLab Enterprise features for issue boards
-GitLab Issue Boards are available on GitLab Core and GitLab.com Free, but some
-advanced functionalities are only present in higher tiers: GitLab.com Bronze,
-Silver, or Gold, or GitLab self-managed Starter, Premium, and Ultimate, as described
-in the following sections.
+GitLab issue boards are available on GitLab Core and GitLab.com Free tiers, but some
+advanced functionality is present in [higher tiers only](https://about.gitlab.com/pricing/).
-For a collection of [features per tier](#summary-of-features-per-tier), check the summary below.
+### Summary of features per tier
+
+Different issue board features are available in different [GitLab tiers](https://about.gitlab.com/pricing/),
+as shown in the following table:
-### Multiple Issue Boards
+| Tier | Number of Project issue boards | Number of Group issue boards | Configurable issue boards | Assignee lists |
+|------------------|--------------------------------|------------------------------|---------------------------|----------------|
+| Core / Free | Multiple | 1 | No | No |
+| Starter / Bronze | Multiple | 1 | Yes | No |
+| Premium / Silver | Multiple | Multiple | Yes | Yes |
+| Ultimate / Gold | Multiple | Multiple | Yes | Yes |
-> - Multiple Issue Boards per project [moved](https://gitlab.com/gitlab-org/gitlab-foss/issues/53811) to [GitLab Core](https://about.gitlab.com/pricing/) in GitLab 12.1.
-> - Multiple Issue Boards per group is available in [GitLab Premium Edition](https://about.gitlab.com/pricing/).
+### Multiple issue boards
-Multiple Issue Boards, as the name suggests, allow for more than one Issue Board
-for a given project or group. This is great for large projects with more than one team
-or in situations where a repository is used to host the code of multiple
-products.
+> - [Introduced](https://about.gitlab.com/releases/2016/10/22/gitlab-8-13-released/) in GitLab 8.13.
+> - Multiple issue boards per project [moved](https://gitlab.com/gitlab-org/gitlab-foss/issues/53811) to [GitLab Core](https://about.gitlab.com/pricing/) in GitLab 12.1.
+> - Multiple issue boards per group are available in [GitLab Premium](https://about.gitlab.com/pricing/).
+
+Multiple issue boards allow for more than one issue board for a given project or group.
+This is great for large projects with more than one team or in situations where a repository is used
+to host the code of multiple products.
-Clicking on the current board name in the upper left corner will reveal a
-menu from where you can create another Issue Board or delete the existing one.
Using the search box at the top of the menu, you can filter the listed boards.
-When you have 10 or more boards available, a "Recent" section is also shown in the menu.
-These are shortcuts to your last 4 visited boards.
+When you have ten or more boards available, a **Recent** section is also shown in the menu, with
+shortcuts to your last four visited boards.
-![Multiple Issue Boards](img/issue_boards_multiple.png)
+![Multiple issue boards](img/issue_boards_multiple.png)
When you're revisiting an issue board in a project or group with multiple boards,
-GitLab will automatically load the last board you visited.
+GitLab automatically loads the last board you visited.
+
+#### Create an issue board
-### Configurable Issue Boards **(STARTER)**
+To create a new issue board:
-> Introduced in [GitLab Starter Edition 10.2](https://about.gitlab.com/releases/2017/11/22/gitlab-10-2-released/#issue-boards-configuration).
+1. Click the dropdown with the current board name in the upper left corner of the Issue Boards page.
+1. Click **Create new board**.
+1. Enter the new board's name and select its scope: milestone, labels, assignee, or weight.
-An Issue Board can be associated with a GitLab [Milestone](milestones/index.md#milestones),
+#### Delete an issue board
+
+To delete the currently active issue board:
+
+1. Click the dropdown with the current board name in the upper left corner of the Issue Boards page.
+1. Click **Delete board**.
+1. Click **Delete** to confirm.
+
+### Configurable issue boards **(STARTER)**
+
+> [Introduced](https://about.gitlab.com/releases/2017/11/22/gitlab-10-2-released/#issue-boards-configuration) in [GitLab Starter](https://about.gitlab.com/pricing/) 10.2.
+
+An issue board can be associated with a GitLab [Milestone](milestones/index.md#milestones),
[Labels](labels.md), Assignee and Weight
which will automatically filter the Board issues according to these fields.
This allows you to create unique boards according to your team's need.
![Create scoped board](img/issue_board_creation.png)
-You can define the scope of your board when creating it or by clicking on the "Edit board" button.
-Once a milestone, assignee or weight is assigned to an Issue Board, you will no longer be able to
+You can define the scope of your board when creating it or by clicking the "Edit board" button.
+Once a milestone, assignee or weight is assigned to an issue board, you will no longer be able to
filter through these in the search bar. In order to do that, you need to remove the desired scope
-(for example, milestone, assignee, or weight) from the Issue Board.
+(for example, milestone, assignee, or weight) from the issue board.
![Edit board configuration](img/issue_board_edit_button.png)
-If you don't have editing permission in a board, you're still able to see the configuration by clicking on "View scope".
+If you don't have editing permission in a board, you're still able to see the configuration by
+clicking **View scope**.
![Viewing board configuration](img/issue_board_view_scope.png)
+---
+
### Focus mode
-> - Introduced in [GitLab Starter 9.1](https://about.gitlab.com/releases/2017/04/22/gitlab-9-1-released/#issue-boards-focus-mode-ees-eep).
-> - [Moved](https://gitlab.com/gitlab-org/gitlab/-/issues/212331) to GitLab Core in 12.10.
+> - [Introduced]((https://about.gitlab.com/releases/2017/04/22/gitlab-9-1-released/#issue-boards-focus-mode-ees-eep)) in [GitLab Starter](https://about.gitlab.com/pricing/) 9.1.
+> - [Moved](https://gitlab.com/gitlab-org/gitlab/-/merge_requests/28597) to the Free tier of GitLab.com in 12.10.
+> - [Moved](https://gitlab.com/gitlab-org/gitlab/-/issues/212331) to GitLab Core in 13.0.
-Click the button at the top right to toggle focus mode on and off. In focus mode, the navigation UI is hidden, allowing you to focus on issues in the board.
+Click the button at the top right to toggle focus mode on and off. In focus mode, the navigation UI
+is hidden, allowing you to focus on issues in the board.
![Board focus mode](img/issue_board_focus_mode.gif)
-### Sum of Issue Weights **(STARTER)**
+---
+
+### Sum of issue weights **(STARTER)**
The top of each list indicates the sum of issue weights for the issues that
belong to that list. This is useful when using boards for capacity allocation,
especially in combination with [assignee lists](#assignee-lists-premium).
-![Issue Board summed weights](img/issue_board_summed_weights.png)
+![issue board summed weights](img/issue_board_summed_weights.png)
+
+---
-### Group Issue Boards **(PREMIUM)**
+### Group issue boards **(PREMIUM)**
-> Introduced in [GitLab Premium 10.0](https://about.gitlab.com/releases/2017/09/22/gitlab-10-0-released/#group-issue-boards).
+> [Introduced](https://about.gitlab.com/releases/2017/09/22/gitlab-10-0-released/#group-issue-boards) in [GitLab Premium](https://about.gitlab.com/pricing/) 10.0.
Accessible at the group navigation level, a group issue board offers the same features as a project-level board,
but it can display issues from all projects in that
@@ -231,102 +282,114 @@ boards. When updating milestones and labels for an issue through the sidebar upd
group-level objects are available.
NOTE: **Note:**
-Multiple group issue boards were originally introduced in [GitLab 10.0 Premium](https://about.gitlab.com/releases/2017/09/22/gitlab-10-0-released/#group-issue-boards) and
-one group issue board per group was made available in GitLab 10.6 Core.
+Multiple group issue boards were originally [introduced](https://about.gitlab.com/releases/2017/09/22/gitlab-10-0-released/#group-issue-boards) in [GitLab Premium](https://about.gitlab.com/pricing/) 10.0, and one group issue board per group was made available in GitLab Core 10.6.
![Group issue board](img/group_issue_board.png)
+---
+
### Assignee lists **(PREMIUM)**
-> [Introduced](https://gitlab.com/gitlab-org/gitlab/issues/5784) in GitLab 11.0 Premium.
+> [Introduced](https://gitlab.com/gitlab-org/gitlab/issues/5784) in [GitLab Premium](https://about.gitlab.com/pricing/) 11.0.
-Like a regular list that shows all issues that have the list label, you can add
-an assignee list that shows all issues assigned to the given user.
+Like in a regular list that shows all issues with a chosen label, you can add
+an assignee list that shows all issues assigned to a user.
You can have a board with both label lists and assignee lists. To add an
assignee list:
1. Click **Add list**.
1. Select the **Assignee list** tab.
-1. Search and click on the user you want to add as an assignee.
+1. Search and click the user you want to add as an assignee.
Now that the assignee list is added, you can assign or unassign issues to that user
-by [dragging issues](#dragging-issues-between-lists) to and/or from an assignee list.
+by [dragging issues](#drag-issues-between-lists) to and from an assignee list.
To remove an assignee list, just as with a label list, click the trash icon.
![Assignee lists](img/issue_board_assignee_lists.png)
+---
+
### Milestone lists **(PREMIUM)**
-> [Introduced](https://gitlab.com/gitlab-org/gitlab/issues/6469) in GitLab 11.2 Premium.
+> [Introduced](https://gitlab.com/gitlab-org/gitlab/issues/6469) in [GitLab Premium](https://about.gitlab.com/pricing/) 11.2.
-As of 11.2, you're also able to create lists of a milestone. As the name states,
-these are lists that filter issues by the assigned milestone, giving you more
-freedom and visibility on the Issue Board. To do so:
+You're also able to create lists of a milestone. These are lists that filter issues by the assigned
+milestone, giving you more freedom and visibility on the issue board. To add a milestone list:
1. Click **Add list**.
1. Select the **Milestone** tab.
-1. Search and click on the milestone.
+1. Search and click the milestone.
-Similar to the assignee lists, you're now able to [drag issues](#dragging-issues-between-lists)
-to and/or from a milestone list to manipulate the milestone of the dragged issues.
-As on another list types, click on the trash icon to remove it.
+Similar to the assignee lists, you're now able to [drag issues](#drag-issues-between-lists)
+to and from a milestone list to manipulate the milestone of the dragged issues.
+As in other list types, click the trash icon to remove a list.
![Milestone lists](img/issue_board_milestone_lists.png)
+---
+
## Work In Progress limits **(STARTER)**
> [Introduced](https://gitlab.com/gitlab-org/gitlab/issues/11403) in GitLab 12.7
-You can set Work In Progress (WIP) limits per issues list. When a limit is set, the list's header shows the number of issues in the list and the soft limit of issues. For example, for a list with 4 issues, and a limit of 5, the header will show `4/5`. If you exceed the limit, the current number of issues is shown in red. For example, you have a list with 5 issues with a limit of 5. When you move another issue to that list, the list's header displays `6/5`, with the `6` shown in red.
+You can set Work In Progress (WIP) limits per issues list. When a limit is set, the list's header
+shows the number of issues in the list and the soft limit of issues.
-To set a WIP limit for a list:
+Examples:
-1. Navigate to a Project or Group board for which you have membership and click on the Settings icon (gear) in a list's header.
-1. Next to **Work In Progress Limit**, click **Edit** and enter the maximum number of issues. Press `Enter` to save.
+- You have a list with four issues, and a limit of five, the header will show **4/5**.
+ If you exceed the limit, the current number of issues is shown in red.
+- You have a list with five issues with a limit of five. When you move another issue to that list,
+ the list's header displays **6/5**, with the six shown in red.
-### Summary of features per tier
-
-Different issue board features are available in different [GitLab tiers](https://about.gitlab.com/pricing/), as shown in the following table:
+To set a WIP limit for a list:
-| Tier | Number of Project Issue Boards | Number of Group Issue Boards | Configurable Issue Boards | Assignee Lists |
-|----------|--------------------------------|------------------------------|---------------------------|----------------|
-| Core / Free | Multiple | 1 | No | No |
-| Starter / Bronze | Multiple | 1 | Yes | No |
-| Premium / Silver | Multiple | Multiple | Yes | Yes |
-| Ultimate / Gold | Multiple | Multiple | Yes | Yes |
+1. Navigate to a Project or Group board of which you're a member.
+1. Click the Settings icon (**{settings}**) in a list's header.
+1. Next to **Work In Progress Limit**, click **Edit**.
+1. Enter the maximum number of issues.
+1. Press <kbd>Enter</kbd> to save.
## Blocked issues
> [Introduced](https://gitlab.com/gitlab-org/gitlab/issues/34723) in GitLab 12.8.
-If an issue is blocked by another issue, an icon will display next to its title to differentiate it from unblocked issues.
+If an issue is blocked by another issue, an icon appears next to its title to indicate its blocked
+status.
![Blocked issues](img/issue_boards_blocked_icon_v12_8.png)
-## Actions you can take on an Issue Board
+---
+
+## Actions you can take on an issue board
+
+- [Create a new list](#create-a-new-list).
+- [Delete an existing list](#delete-a-list).
+- [Add issues to a list](#add-issues-to-a-list).
+- [Remove an issue from a list](#remove-an-issue-from-a-list).
+- [Filter issues](#filter-issues) that appear across your issue board.
+- [Create workflows](#create-workflows).
+- [Drag issues between lists](#drag-issues-between-lists).
+- [Mulit-select issue cards](#multi-select-issue-cards).
+- [Re-order issues in lists](#issue-ordering-in-a-list).
+- Drag and reorder the lists.
+- Change issue labels (by dragging an issue between lists).
+- Close an issue (by dragging it to the **Done** list).
-- [Create a new list](#creating-a-new-list).
-- [Delete an existing list](#deleting-a-list).
-- Drag issues between lists.
-- Re-order issues in lists.
-- Drag and reorder the lists themselves.
-- Change issue labels on-the-fly while dragging issues between lists.
-- Close an issue if you drag it to the **Done** list.
-- Create a new list from a non-existing label by [creating the label on-the-fly](#creating-a-new-list)
- within the Issue Board.
-- [Filter issues](#filtering-issues) that appear across your Issue Board.
+If you're not able to do some of the things above, make sure you have the right
+[permissions](#permissions).
-If you are not able to perform one or more of the things above, make sure you
-have the right [permissions](#permissions).
+### First time using an issue board
-### First time using the Issue Board
+The first time you open an issue board, you are presented with
+the default lists (**Open** and **Closed**) and a welcome message that gives
+you two options. You can either:
-The first time you navigate to your Issue Board, you will be presented with
-a default list (**Done**) and a welcoming message that gives
-you two options. You can either create a predefined set of labels and create
-their corresponding lists to the Issue Board or opt-out and use your own lists.
+- Create a predefined set of labels (by default: **To Do** and **Doing**) and create their
+ corresponding lists to the issue board.
+- Opt-out and use your own lists.
-![Issue Board welcome message](img/issue_board_welcome_message.png)
+![issue board welcome message](img/issue_board_welcome_message.png)
If you choose to use and create the predefined lists, they will appear as empty
because the labels associated to them will not exist up until that moment,
@@ -334,99 +397,74 @@ which means the system has no way of populating them automatically. That's of
course if the predefined labels don't already exist. If any of them does exist,
the list will be created and filled with the issues that have that label.
-### Creating a new list
+### Create a new list
-Create a new list by clicking on the **Add list** button at the upper
-right corner of the Issue Board.
+Create a new list by clicking the **Add list** button in the upper right corner of the issue board.
-![Issue Board welcome message](img/issue_board_add_list.png)
+![issue board welcome message](img/issue_board_add_list.png)
-Simply choose the label or user to create the list from. The new list will be inserted
+Then, choose the label or user to create the list from. The new list will be inserted
at the end of the lists, before **Done**. Moving and reordering lists is as
easy as dragging them around.
-To create a list for a label that doesn't yet exist, simply create the label by
-choosing **Create new label**. The label will be created on-the-fly and it will
-be immediately added to the dropdown. You can now choose it to create a list.
+To create a list for a label that doesn't yet exist, create the label by
+choosing **Create new label**. This creates the label immediately and adds it to the dropdown.
+You can now choose it to create a list.
-### Deleting a list
+### Delete a list
-To delete a list from the Issue Board use the small trash icon that is present
+To delete a list from the issue board, use the small trash icon present
in the list's heading. A confirmation dialog will appear for you to confirm.
Deleting a list doesn't have any effect in issues and labels, it's just the
list view that is removed. You can always add it back later if you need.
-### Adding issues to a list
+### Add issues to a list
-You can add issues to a list by clicking the **Add issues** button that is
-present in the upper right corner of the Issue Board. This will open up a modal
+You can add issues to a list by clicking the **Add issues** button
+present in the upper right corner of the issue board. This will open up a modal
window where you can see all the issues that do not belong to any list.
-Select one or more issues by clicking on the cards and then click **Add issues**
+Select one or more issues by clicking the cards and then click **Add issues**
to add them to the selected list. You can limit the issues you want to add to
the list by filtering by author, assignee, milestone, and label.
![Bulk adding issues to lists](img/issue_boards_add_issues_modal.png)
-### Removing an issue from a list
-
-Removing an issue from a list can be done by clicking on the issue card and then
-clicking the **Remove from board** button in the sidebar. Under the hood, the
-respective label is removed, and as such it's also removed from the list and the
-board itself.
-
-![Remove issue from list](img/issue_boards_remove_issue.png)
-
-### Issue ordering in a list
+---
-When visiting a board, issues appear ordered in any list. You are able to change
-that order simply by dragging and dropping the issues. The changed order will be saved
-to the system so that anybody who visits the same board later will see the reordering,
-with some exceptions.
+### Remove an issue from a list
-The first time a given issue appears in any board (that is, the first time a user
-loads a board containing that issue), it is ordered with
-respect to other issues in that list according to [Priority order](labels.md#label-priority).
+Removing an issue from a list can be done by clicking the issue card and then
+clicking the **Remove from board** button in the sidebar. The
+respective label is removed.
-At that point, that issue is assigned a relative order value by the system
-representing its relative order with respect to the other issues in the list. Any time
-you drag-and-drop reorder that issue, its relative order value changes accordingly.
-
-Also, any time that issue appears in any board when it's loaded by a user,
-the updated relative order value is used for the ordering. (It's only the first
-time an issue appears that it takes from the Priority order mentioned above.) This means that
-if issue `A` is drag-and-drop reordered to be above issue `B` by any user in
-a given board inside your GitLab instance, any time those two issues are subsequently
-loaded in any board in the same instance (could be a different project board or a different group
-board, for example), that ordering is maintained.
+![Remove issue from list](img/issue_boards_remove_issue.png)
-This ordering also affects [issue lists](issues/sorting_issue_lists.md).
-Changing the order in an issue board changes the ordering in an issue list,
-and vice versa.
+---
-### Filtering issues
+### Filter issues
-You should be able to use the filters on top of your Issue Board to show only
+You should be able to use the filters on top of your issue board to show only
the results you want. This is similar to the filtering used in the issue tracker
-since the metadata from the issues and labels are re-used in the Issue Board.
+since the metadata from the issues and labels are re-used in the issue board.
You can filter by author, assignee, milestone, and label.
-### Creating workflows
+### Create workflows
-By reordering your lists, you can create workflows. As lists in Issue Boards are
+By reordering your lists, you can create workflows. As lists in issue boards are
based on labels, it works out of the box with your existing issues. So if you've
already labeled things with 'Backend' and 'Frontend', the issue appears in
the lists as you create them. In addition, this means you can easily move
something between lists by changing a label.
-A typical workflow of using the Issue Board would be:
+A typical workflow of using an issue board would be:
1. You have [created](labels.md#label-management) and [prioritized](labels.md#label-priority)
labels so that you can easily categorize your issues.
1. You have a bunch of issues (ideally labeled).
-1. You visit the Issue Board and start [creating lists](#creating-a-new-list) to
+1. You visit the issue board and start [creating lists](#create-a-new-list) to
create a workflow.
1. You move issues around in lists so that your team knows who should be working
on what issue.
@@ -446,9 +484,11 @@ issue.
This process can be seen clearly when visiting an issue since with every move
to another list the label changes and a system not is recorded.
-![Issue Board system notes](img/issue_board_system_notes.png)
+![issue board system notes](img/issue_board_system_notes.png)
-### Dragging issues between lists
+---
+
+### Drag issues between lists
When dragging issues between lists, different behavior occurs depending on the source list and the target list.
@@ -472,6 +512,35 @@ To select and move multiple cards:
![Multi-select Issue Cards](img/issue_boards_multi_select_v12_4.png)
+---
+
+### Issue ordering in a list
+
+When visiting a board, issues appear ordered in any list. You're able to change
+that order by dragging and dropping the issues. The changed order will be saved
+to the system so that anybody who visits the same board later will see the reordering,
+with some exceptions.
+
+The first time a given issue appears in any board (that is, the first time a user
+loads a board containing that issue), it is ordered with
+respect to other issues in that list according to [Priority order](labels.md#label-priority).
+
+At that point, that issue is assigned a relative order value by the system
+representing its relative order with respect to the other issues in the list. Any time
+you drag-and-drop reorder that issue, its relative order value changes accordingly.
+
+Also, any time that issue appears in any board when it's loaded by a user,
+the updated relative order value is used for the ordering. (It's only the first
+time an issue appears that it takes from the Priority order mentioned above.) This means that
+if issue `A` is drag-and-drop reordered to be above issue `B` by any user in
+a given board inside your GitLab instance, any time those two issues are subsequently
+loaded in any board in the same instance (could be a different project board or a different group
+board, for example), that ordering is maintained.
+
+This ordering also affects [issue lists](issues/sorting_issue_lists.md).
+Changing the order in an issue board changes the ordering in an issue list,
+and vice versa.
+
## Tips
A few things to remember:
@@ -480,8 +549,8 @@ A few things to remember:
and adds the label from the list it goes to.
- An issue can exist in multiple lists if it has more than one label.
- Lists are populated with issues automatically if the issues are labeled.
-- Clicking on the issue title inside a card takes you to that issue.
-- Clicking on a label inside a card quickly filters the entire Issue Board
+- Clicking the issue title inside a card takes you to that issue.
+- Clicking a label inside a card quickly filters the entire issue board
and show only the issues from all lists that have that label.
- For performance and visibility reasons, each list shows the first 20 issues
by default. If you have more than 20 issues, start scrolling down and the next
diff --git a/doc/user/project/issues/csv_export.md b/doc/user/project/issues/csv_export.md
index ec53b3dbbba..af01534a67f 100644
--- a/doc/user/project/issues/csv_export.md
+++ b/doc/user/project/issues/csv_export.md
@@ -70,7 +70,7 @@ Data will be encoded with a comma as the column delimiter, with `"` used to quot
| Labels | Title of any labels joined with a `,` |
| Time Estimate | [Time estimate](../time_tracking.md#estimates) in seconds |
| Time Spent | [Time spent](../time_tracking.md#time-spent) in seconds |
-| Epic ID | Id of the parent epic **(ULTIMATE)**, introduced in 12.7 |
+| Epic ID | ID of the parent epic **(ULTIMATE)**, introduced in 12.7 |
| Epic Title | Title of the parent epic **(ULTIMATE)**, introduced in 12.7 |
## Limitations
diff --git a/doc/user/project/issues/design_management.md b/doc/user/project/issues/design_management.md
index 1078d0410ed..64f56221632 100644
--- a/doc/user/project/issues/design_management.md
+++ b/doc/user/project/issues/design_management.md
@@ -1,6 +1,7 @@
-# Design Management **(PREMIUM)**
+# Design Management
-> [Introduced](https://gitlab.com/groups/gitlab-org/-/epics/660) in [GitLab Premium](https://about.gitlab.com/pricing/) 12.2.
+> - [Introduced](https://gitlab.com/groups/gitlab-org/-/epics/660) in [GitLab Premium](https://about.gitlab.com/pricing/) 12.2.
+> - [Moved](https://gitlab.com/gitlab-org/gitlab/-/issues/212566) to GitLab Core in 13.0.
CAUTION: **Warning:**
This an **alpha** feature and is subject to change at any time without
@@ -86,6 +87,7 @@ Copy-and-pasting has some limitations:
- You can paste only one image at a time. When copy/pasting multiple files, only the first one will be uploaded.
- All images will be converted to `png` format under the hood, so when you want to copy/paste `gif` file, it will result in broken animation.
+- If you are pasting a screenshot from the clipboard, it will be renamed to `design_<timestamp>.png`
- Copy/pasting designs is not supported on Internet Explorer.
Designs with the same filename as an existing uploaded design will create a new version
diff --git a/doc/user/project/issues/due_dates.md b/doc/user/project/issues/due_dates.md
index f70597f6875..0be0cdd11bd 100644
--- a/doc/user/project/issues/due_dates.md
+++ b/doc/user/project/issues/due_dates.md
@@ -24,6 +24,11 @@ Changes are saved immediately.
![Edit a due date via the sidebar](img/due_dates_edit_sidebar.png)
+The last way to set a due date is by using [quick actions](../quick_actions.md), directly in an issue's description or comment:
+
+- `/due <date>`: set due date. Examples of valid `<date>` include `in 2 days`, `this Friday`, and `December 31st`.
+- `/remove_due_date`: remove due date.
+
## Making use of due dates
Issues that have a due date can be easily seen in the issue tracker,
diff --git a/doc/user/project/issues/index.md b/doc/user/project/issues/index.md
index 0f9295e1afd..22221531360 100644
--- a/doc/user/project/issues/index.md
+++ b/doc/user/project/issues/index.md
@@ -28,6 +28,11 @@ you can also view all the issues collectively at the group level.
See also [Always start a discussion with an issue](https://about.gitlab.com/blog/2016/03/03/start-with-an-issue/).
+<i class="fa fa-youtube-play youtube" aria-hidden="true"></i>
+To learn how GitLab's Strategic Marketing department uses GitLab issues with [labels](../labels.md) and
+[issue boards](../issue_board.md), see the video on
+[Managing Commitments with Issues](https://www.youtube.com/watch?v=cuIHNintg1o&t=3).
+
## Parts of an issue
Issues contain a variety of content and metadata, enabling a large range of flexibility
diff --git a/doc/user/project/issues/managing_issues.md b/doc/user/project/issues/managing_issues.md
index 4ee29f3357d..4e329889e7c 100644
--- a/doc/user/project/issues/managing_issues.md
+++ b/doc/user/project/issues/managing_issues.md
@@ -50,7 +50,7 @@ create issues for the same project.
![Create issue from group-level issue tracker](img/create_issue_from_group_level_issue_tracker.png)
-### New issue via Service Desk **(PREMIUM)**
+### New issue via Service Desk **(STARTER)**
Enable [Service Desk](../service_desk.md) for your project and offer email support.
By doing so, when your customer sends a new email, a new issue can be created in
@@ -92,9 +92,17 @@ field values using query string parameters in a URL. This is useful for embeddin
a URL in an external HTML page, and also certain scenarios where you want the user to
create an issue with certain fields prefilled.
-The title, description, and description template fields can be prefilled using
-this method. You cannot pre-fill both the description and description template fields
-in the same URL (since a description template also populates the description field).
+The title, description, description template, and confidential fields can be prefilled
+using this method. You cannot pre-fill both the description and description template
+fields in the same URL (since a description template also populates the description
+field).
+
+| Field | URL Parameter Name | Notes |
+|----------------------|-----------------------|-------------------------------------------------------|
+| title | `issue[title]` | |
+| description | `issue[description]` | |
+| description template | `issuable_template` | |
+| confidential | `issue[confidential]` | Parameter value must be `true` to set to confidential |
Follow these examples to form your new issue URL with prefilled fields.
@@ -102,6 +110,8 @@ Follow these examples to form your new issue URL with prefilled fields.
and a pre-filled description, the URL would be `https://gitlab.com/gitlab-org/gitlab-foss/issues/new?issue[title]=Validate%20new%20concept&issue[description]=Research%20idea`
- For a new issue in the GitLab Community Edition project with a pre-filled title
and a pre-filled description template, the URL would be `https://gitlab.com/gitlab-org/gitlab-foss/issues/new?issue[title]=Validate%20new%20concept&issuable_template=Research%20proposal`
+- For a new issue in the GitLab Community Edition project with a pre-filled title,
+ a pre-filled description, and the confidential flag set, the URL would be `https://gitlab.com/gitlab-org/gitlab-foss/issues/new?issue[title]=Validate%20new%20concept&issue[description]=Research%20idea&issue[confidential]=true`
## Moving Issues
@@ -117,7 +127,10 @@ The "Move issue" button is at the bottom of the right-sidebar when viewing the i
If you have advanced technical skills you can also bulk move all the issues from one project to another in the rails console. The below script will move all the issues from one project to another that are not in status **closed**.
-To access rails console run `sudo gitlab-rails console` on the GitLab server and run the below script. Please be sure to change **project**, **admin_user** and **target_project** to your values. We do also recommend [creating a backup](../../../raketasks/backup_restore.md#creating-a-backup-of-the-gitlab-system) before attempting any changes in the console.
+To access rails console run `sudo gitlab-rails console` on the GitLab server and run the below
+script. Please be sure to change **project**, **admin_user** and **target_project** to your values.
+We do also recommend [creating a backup](../../../raketasks/backup_restore.md#back-up-gitlab) before
+attempting any changes in the console.
```ruby
project = Project.find_by_full_path('full path of the project where issues are moved from')
@@ -170,7 +183,7 @@ but it will not close automatically.
If the issue is in a different repository than the MR, add the full URL for the issue(s):
-```md
+```markdown
Closes #4, #6, and https://gitlab.com/<username>/<projectname>/issues/<xxx>
```
@@ -179,7 +192,7 @@ Closes #4, #6, and https://gitlab.com/<username>/<projectname>/issues/<xxx>
When not specified, the default issue closing pattern as shown below will be used:
```shell
-((?:[Cc]los(?:e[sd]?|ing)|[Ff]ix(?:e[sd]|ing)?|[Rr]esolv(?:e[sd]?|ing)|[Ii]mplement(?:s|ed|ing)?)(:?) +(?:(?:issues? +)?%{issue_ref}(?:(?:, *| +and +)?)|([A-Z][A-Z0-9_]+-\d+))+)
+\b((?:[Cc]los(?:e[sd]?|ing)|\b[Ff]ix(?:e[sd]|ing)?|\b[Rr]esolv(?:e[sd]?|ing)|\b[Ii]mplement(?:s|ed|ing)?)(:?) +(?:(?:issues? +)?%{issue_ref}(?:(?: *,? +and +| *,? *)?)|([A-Z][A-Z0-9_]+-\d+))+)
```
This translates to the following keywords:
diff --git a/doc/user/project/issues/related_issues.md b/doc/user/project/issues/related_issues.md
index 5fba73c2971..8002b528a80 100644
--- a/doc/user/project/issues/related_issues.md
+++ b/doc/user/project/issues/related_issues.md
@@ -10,12 +10,14 @@ The relationship only shows up in the UI if the user can see both issues.
## Adding a related issue
+> - [Introduced](https://gitlab.com/gitlab-org/gitlab/-/issues/2035) in [GitLab Starter](https://about.gitlab.com/pricing/) 12.8.
+> - [Improved](https://gitlab.com/gitlab-org/gitlab/-/issues/34239) in [GitLab Starter](https://about.gitlab.com/pricing/) 13.0.
+> When you try to close an issue with open blockers, you'll see a warning that you can dismiss.
+
You can relate one issue to another by clicking the related issues "+" button
in the header of the related issue block. Then, input the issue reference number
or paste in the full URL of the issue.
-> [Introduced](https://gitlab.com/gitlab-org/gitlab/issues/2035) in GitLab 12.8.
-
Additionally, you can select whether the current issue relates to, blocks, or is blocked by the issues being entered.
![Adding a related issue](img/related_issues_add_v12_8.png)
diff --git a/doc/user/project/labels.md b/doc/user/project/labels.md
index 9cf50d3aaed..f3b59147d5b 100644
--- a/doc/user/project/labels.md
+++ b/doc/user/project/labels.md
@@ -2,13 +2,18 @@
## Overview
-Labels allow you to categorize epics, issues, and merge requests using descriptive titles like
-`bug`, `feature request`, or `docs`, as well as customizable colors. They allow you to quickly
-and dynamically filter and manage epics, issues, and merge requests, and are a key
-part of [issue boards](issue_board.md).
+As your count of issues, merge requests, and epics grows in GitLab, it's more and more challenging
+to keep track of those items. Especially as your organization grows from just a few people to
+hundreds or thousands. This is where labels come in. They help you organize and tag your work
+so you can track and find the work items you're interested in.
-You can use labels to help [search](../search/index.md#issues-and-merge-requests) in
-lists of issues, merge requests, and epics, as well as [search in issue boards](../search/index.md#issue-boards).
+Labels are a key part of [issue boards](issue_board.md). With labels you can:
+
+- Categorize epics, issues, and merge requests using colors and descriptive titles like
+`bug`, `feature request`, or `docs`.
+- Dynamically filter and manage epics, issues, and merge requests.
+- [Search lists of issues, merge requests, and epics](../search/index.md#issues-and-merge-requests),
+ as well as [issue boards](../search/index.md#issue-boards).
## Project labels and group labels
@@ -164,7 +169,7 @@ Suppose you have the labels `workflow::development`, `workflow::review`, and
applied, and a developer wanted to advance the issue to `workflow::review`, they
would simply apply that label, and the `workflow::development` label would
automatically be removed. This behavior already exists when you move issues
-across label lists in an [issue board](issue_board.md#creating-workflows), but
+across label lists in an [issue board](issue_board.md#create-workflows), but
now, team members who may not be working in an issue board directly would still
be able to advance workflow states consistently in issues themselves.
diff --git a/doc/user/project/members/index.md b/doc/user/project/members/index.md
index 27a5701e6c2..9020dc335b6 100644
--- a/doc/user/project/members/index.md
+++ b/doc/user/project/members/index.md
@@ -1,4 +1,4 @@
-# Project's members
+# Members of a project
You can manage the groups and users and their access levels in all of your
projects. You can also personalize the access level you give each user,
diff --git a/doc/user/project/merge_requests/accessibility_testing.md b/doc/user/project/merge_requests/accessibility_testing.md
index 755bf0447e3..427761281f6 100644
--- a/doc/user/project/merge_requests/accessibility_testing.md
+++ b/doc/user/project/merge_requests/accessibility_testing.md
@@ -18,6 +18,23 @@ measuring the accessibility of web sites, and has built a simple
This job outputs accessibility violations, warnings, and notices for each page
analyzed to a file called `accessibility`.
+## Accessibility Merge Request widget
+
+[Since GitLab 13.0](https://gitlab.com/gitlab-org/gitlab/-/issues/39425), in addition to the report artifact that is created, GitLab will also show the
+Accessibility Report in the merge request widget area:
+
+![Accessibility Merge Request Widget](img/accessibility_mr_widget_v13_0.png)
+
+This widget comes with the `:accessibility_report_view` feature flag disabled by default while we test feature stability.
+Once we have determined the widget is stable, this feature will be enabled by default.
+
+To enable this feature, ask a GitLab administrator with [Rails console access](../../../administration/feature_flags.md#how-to-enable-and-disable-features-behind-flags) to run the
+following command:
+
+```ruby
+Feature.enable(:accessibility_report_view)
+```
+
## Configure Accessibility Testing
This example shows how to run [pa11y](https://pa11y.org/)
@@ -46,10 +63,13 @@ Pa11y against the webpages defined in `a11y_urls`, and builds an HTML report for
The report for each URL is saved as an artifact that can be [viewed directly in your browser](../../../ci/pipelines/job_artifacts.md#browsing-artifacts).
-A single `accessibility.json` artifact is created and saved along with the individual HTML reports.
+A single `gl-accessibility.json` artifact is created and saved along with the individual HTML reports.
It includes report data for all URLs scanned.
NOTE: **Note:**
+For GitLab 12.10 and earlier, the [artifact generated is named `accessibility.json`](https://gitlab.com/gitlab-org/ci-cd/accessibility/-/merge_requests/9).
+
+NOTE: **Note:**
For GitLab versions earlier than 12.9, you can use `include:remote` and use a
link to the [current template in `master`](https://gitlab.com/gitlab-org/gitlab/-/raw/master/lib/gitlab/ci/templates/Verify/Accessibility.gitlab-ci.yml)
diff --git a/doc/user/project/merge_requests/browser_performance_testing.md b/doc/user/project/merge_requests/browser_performance_testing.md
index 1bca5d2a212..0fa3d7be265 100644
--- a/doc/user/project/merge_requests/browser_performance_testing.md
+++ b/doc/user/project/merge_requests/browser_performance_testing.md
@@ -6,55 +6,50 @@ type: reference, howto
> [Introduced](https://gitlab.com/gitlab-org/gitlab/-/merge_requests/3507) in [GitLab Premium](https://about.gitlab.com/pricing/) 10.3.
-If your application offers a web interface and you are using
+If your application offers a web interface and you're using
[GitLab CI/CD](../../../ci/README.md), you can quickly determine the performance
impact of pending code changes.
## Overview
GitLab uses [Sitespeed.io](https://www.sitespeed.io), a free and open source
-tool for measuring the performance of web sites, and has built a simple
-[Sitespeed plugin](https://gitlab.com/gitlab-org/gl-performance)
-which outputs the results in a file called `performance.json`. This plugin
-outputs the performance score for each page that is analyzed.
-
+tool, for measuring the performance of web sites. GitLab has built a simple
+[Sitespeed plugin](https://gitlab.com/gitlab-org/gl-performance) which outputs
+the performance score for each page analyzed in a file called `performance.json`.
The [Sitespeed.io performance score](https://examples.sitespeed.io/6.0/2017-11-23-23-43-35/help.html)
-is a composite value based on best practices, and we will be expanding support
-for [additional metrics](https://gitlab.com/gitlab-org/gitlab/issues/4370)
-in a future release.
+is a composite value based on best practices.
-Going a step further, GitLab can show the Performance report right
-in the merge request widget area (see below).
+GitLab can [show the Performance report](#how-browser-performance-testing-works)
+in the merge request widget area.
## Use cases
-For instance, consider the following workflow:
+Consider the following workflow:
1. A member of the marketing team is attempting to track engagement by adding a new tool.
1. With browser performance metrics, they see how their changes are impacting the usability
of the page for end users.
-1. The metrics show that after their changes the performance score of the page has gone down.
-1. When looking at the detailed report, they see that the new JavaScript library was
- included in `<head>` which affects loading page speed.
-1. They ask a front end developer to help them, who sets the library to load asynchronously.
-1. The frontend developer approves the merge request and authorizes its deployment to production.
-
-## How it works
+1. The metrics show that after their changes, the performance score of the page has gone down.
+1. When looking at the detailed report, they see the new JavaScript library was
+ included in `<head>`, which affects loading page speed.
+1. They ask for help from a front end developer, who sets the library to load asynchronously.
+1. The frontend developer approves the merge request, and authorizes its deployment to production.
-First of all, you need to define a job in your `.gitlab-ci.yml` file that generates the
-[Performance report artifact](../../../ci/yaml/README.md#artifactsreportsperformance-premium).
-For more information on how the Performance job should look like, check the
-example on [Configuring Browser Performance Testing](#configuring-browser-performance-testing).
+## How browser performance testing works
+First, define a job in your `.gitlab-ci.yml` file that generates the
+[Performance report artifact](../../../ci/pipelines/job_artifacts.md#artifactsreportsperformance-premium).
GitLab then checks this report, compares key performance metrics for each page
-between the source and target branches, and shows the information right on the merge request.
+between the source and target branches, and shows the information in the merge request.
+
+For an example Performance job, see
+[Configuring Browser Performance Testing](#configuring-browser-performance-testing).
NOTE: **Note:**
-If the Performance report doesn't have anything to compare to, no information
-will be displayed in the merge request area. That is the case when you add the
-Performance job in your `.gitlab-ci.yml` for the very first time.
-Consecutive merge requests will have something to compare to, and the Performance
-report will be shown properly.
+If the Performance report has no data to compare, such as when you add the
+Performance job in your `.gitlab-ci.yml` for the very first time, no information
+displays in the merge request widget area. Consecutive merge requests will have data for
+comparison, and the Performance report will be shown properly.
![Performance Widget](img/browser_performance_testing.png)
@@ -64,52 +59,51 @@ This example shows how to run the [sitespeed.io container](https://hub.docker.co
on your code by using GitLab CI/CD and [sitespeed.io](https://www.sitespeed.io)
using Docker-in-Docker.
-First, you need GitLab Runner with
-[docker-in-docker build](../../../ci/docker/using_docker_build.md#use-docker-in-docker-workflow-with-docker-executor).
+1. First, set up GitLab Runner with a
+ [docker-in-docker build](../../../ci/docker/using_docker_build.md#use-docker-in-docker-workflow-with-docker-executor).
+1. After configuring the Runner, add a new job to `.gitlab-ci.yml` that generates
+ the expected report.
+1. Define the `performance` job according to your version of GitLab:
-Once you set up the Runner, add a new job to `.gitlab-ci.yml` that generates the
-expected report.
+ - For GitLab 12.4 and later - [include](../../../ci/yaml/README.md#includetemplate) the
+ [`Browser-Performance.gitlab-ci.yml` template](https://gitlab.com/gitlab-org/gitlab/blob/master/lib/gitlab/ci/templates/Verify/Browser-Performance.gitlab-ci.yml) provided as a part of your GitLab installation.
+ - For GitLab versions earlier than 12.4 - Copy and use the job as defined in the
+ [`Browser-Performance.gitlab-ci.yml` template](https://gitlab.com/gitlab-org/gitlab/blob/master/lib/gitlab/ci/templates/Verify/Browser-Performance.gitlab-ci.yml).
-For GitLab 12.4 and later, to define the `performance` job, you must
-[include](../../../ci/yaml/README.md#includetemplate) the
-[`Browser-Performance.gitlab-ci.yml` template](https://gitlab.com/gitlab-org/gitlab/blob/master/lib/gitlab/ci/templates/Verify/Browser-Performance.gitlab-ci.yml)
-that's provided as a part of your GitLab installation.
-For GitLab versions earlier than 12.4, you can copy and use the job as defined
-in that template.
+ CAUTION: **Caution:**
+ The job definition provided by the template does not support Kubernetes yet.
+ For a complete example of a more complex setup that works in Kubernetes, see
+ [`Browser-Performance-Testing.gitlab-ci.yml`](https://gitlab.com/gitlab-org/gitlab/blob/master/lib/gitlab/ci/templates/Jobs/Browser-Performance-Testing.gitlab-ci.yml).
-CAUTION: **Caution:**
-The job definition provided by the template does not support Kubernetes yet. For a complete example of a more complex setup
-that works in Kubernetes, see [here](https://gitlab.com/gitlab-org/gitlab/blob/master/lib/gitlab/ci/templates/Jobs/Browser-Performance-Testing.gitlab-ci.yml).
+1. Add the following to your `.gitlab-ci.yml` file:
-Add the following to your `.gitlab-ci.yml` file:
+ ```yaml
+ include:
+ template: Verify/Browser-Performance.gitlab-ci.yml
-```yaml
-include:
- template: Verify/Browser-Performance.gitlab-ci.yml
+ performance:
+ variables:
+ URL: https://example.com
+ ```
-performance:
- variables:
- URL: https://example.com
-```
-
-CAUTION: **Caution:**
-The job definition provided by the template is supported in GitLab 11.5 and later versions.
-It also requires GitLab Runner 11.5 or later. For earlier versions, use the
-[previous job definitions](#previous-job-definitions).
+ CAUTION: **Caution:**
+ The job definition provided by the template is supported in GitLab 11.5 and later versions.
+ It also requires GitLab Runner 11.5 or later. For earlier versions, use the
+ [previous job definitions](#previous-job-definitions).
-The above example will create a `performance` job in your CI/CD pipeline and will run
+The above example creates a `performance` job in your CI/CD pipeline and runs
sitespeed.io against the webpage you defined in `URL` to gather key metrics.
The [GitLab plugin for sitespeed.io](https://gitlab.com/gitlab-org/gl-performance)
-is downloaded in order to save the report as a [Performance report artifact](../../../ci/yaml/README.md#artifactsreportsperformance-premium)
-that you can later download and analyze. Due to implementation limitations we always
+is downloaded to save the report as a [Performance report artifact](../../../ci/pipelines/job_artifacts.md#artifactsreportsperformance-premium)
+that you can later download and analyze. Due to implementation limitations, we always
take the latest Performance artifact available.
-The full HTML sitespeed.io report will also be saved as an artifact, and if you have
-[GitLab Pages](../pages/index.md) enabled, it can be viewed directly in your browser.
+The full HTML sitespeed.io report is saved as an artifact, and if
+[GitLab Pages](../pages/index.md) is enabled, it can be viewed directly in your browser.
-It is also possible to customize options by setting the `SITESPEED_OPTIONS` variable.
-For example, this is how to override the number of runs sitespeed.io
-will make on the given URL:
+You can also customize options by setting the `SITESPEED_OPTIONS` variable.
+For example, you can override the number of runs sitespeed.io
+makes on the given URL:
```yaml
include:
@@ -122,27 +116,47 @@ performance:
```
For further customization options for sitespeed.io, including the ability to provide a
-list of URLs to test, please see the [Sitespeed.io Configuration](https://www.sitespeed.io/documentation/sitespeed.io/configuration/)
+list of URLs to test, please see the
+[Sitespeed.io Configuration](https://www.sitespeed.io/documentation/sitespeed.io/configuration/)
documentation.
TIP: **Tip:**
Key metrics are automatically extracted and shown in the merge request widget.
+### Configuring degradation threshold
+
+> [Introduced](https://gitlab.com/gitlab-org/gitlab/issues/27599) in GitLab 13.0.
+
+You can configure the sensitivity of degradation alerts to avoid getting alerts for minor drops in metrics.
+This is done by setting the `DEGRADATION_THRESHOLD` variable. In the example below, the alert will only show up
+if the `Total Score` metric degrades by 5 points or more:
+
+```yaml
+include:
+ template: Verify/Browser-Performance.gitlab-ci.yml
+
+performance:
+ variables:
+ URL: https://example.com
+ DEGRADATION_THRESHOLD: 5
+```
+
+The `Total Score` metric is based on sitespeed.io's [coach performance score](https://www.sitespeed.io/documentation/sitespeed.io/metrics/#performance-score). There is more information in [the coach documentation](https://www.sitespeed.io/documentation/coach/how-to/#what-do-the-coach-do).
+
### Performance testing on Review Apps
-The above CI YML is great for testing against static environments, and it can
-be extended for dynamic environments. There are a few extra steps to take to
-set this up:
+The above CI YAML configuration is great for testing against static environments, and it can
+be extended for dynamic environments, but a few extra steps are required:
1. The `performance` job should run after the dynamic environment has started.
1. In the `review` job, persist the hostname and upload it as an artifact so
- it's available to the `performance` job (the same can be done for static
- environments like staging and production to unify the code path). Saving it
- as an artifact is as simple as `echo $CI_ENVIRONMENT_URL > environment_url.txt`
+ it's available to the `performance` job. The same can be done for static
+ environments like staging and production to unify the code path. You can save it
+ as an artifact with `echo $CI_ENVIRONMENT_URL > environment_url.txt`
in your job's `script`.
1. In the `performance` job, read the previous artifact into an environment
- variable, in this case `$URL` because this is what our sitespeed.io command
- uses for the URL parameter. Because Review App URLs are dynamic, we define
+ variable. In this case, use `$URL` because the sitespeed.io command
+ uses it for the URL parameter. Because Review App URLs are dynamic, define
the `URL` variable through `before_script` instead of `variables`.
1. You can now run the sitespeed.io container against the desired hostname and
paths.
@@ -183,11 +197,11 @@ performance:
### Previous job definitions
CAUTION: **Caution:**
-Before GitLab 11.5, Performance job and artifact had to be named specifically
+Before GitLab 11.5, the Performance job and artifact had to be named specifically
to automatically extract report data and show it in the merge request widget.
-While these old job definitions are still maintained they have been deprecated
+While these old job definitions are still maintained, they have been deprecated
and may be removed in next major release, GitLab 12.0.
-You are advised to update your current `.gitlab-ci.yml` configuration to reflect that change.
+GitLab recommends you update your current `.gitlab-ci.yml` configuration to reflect that change.
For GitLab 11.4 and earlier, the job should look like:
diff --git a/doc/user/project/merge_requests/code_quality.md b/doc/user/project/merge_requests/code_quality.md
index a7e712a4c0a..beb90e30906 100644
--- a/doc/user/project/merge_requests/code_quality.md
+++ b/doc/user/project/merge_requests/code_quality.md
@@ -27,7 +27,19 @@ in the merge request widget area:
![Code Quality Widget](img/code_quality.png)
-For more information, see the Code Climate list of [Supported Languages for Maintainability](https://docs.codeclimate.com/docs/supported-languages-for-maintainability).
+Watch a quick walkthrough of Code Quality in action:
+
+<div class="video-fallback">
+ See the video: <a href="https://www.youtube.com/watch?v=B32LxtJKo9M">Code Quality: Speed Run</a>.
+</div>
+<figure class="video-container">
+ <iframe src="https://www.youtube.com/embed/B32LxtJKo9M" frameborder="0" allowfullscreen="true"> </iframe>
+</figure>
+
+NOTE: **Note:**
+For one customer, the auditor found that having Code Quality, SAST, and Container Scanning all automated in GitLab CI/CD was almost better than a manual review! [Read more](https://about.gitlab.com/customers/bi_worldwide/).
+
+See also the Code Climate list of [Supported Languages for Maintainability](https://docs.codeclimate.com/docs/supported-languages-for-maintainability).
## Use cases
@@ -37,7 +49,7 @@ For instance, consider the following workflow:
feature in your app faster.
1. With Code Quality reports, they analyze how their implementation is impacting
the code quality.
-1. The metrics show that their code degrade the quality in 10 points.
+1. The metrics show that their code degrades the quality by 10 points.
1. You ask a co-worker to help them with this modification.
1. They both work on the changes until Code Quality report displays no
degradations, only improvements.
@@ -53,10 +65,10 @@ also requires the GitLab Runner 11.5 or later. For earlier versions, use the
This example shows how to run Code Quality on your code by using GitLab CI/CD and Docker.
-First, you need GitLab Runner with:
+First, you need GitLab Runner configured:
-- The [docker-in-docker executor](../../../ci/docker/using_docker_build.md#use-docker-in-docker-workflow-with-docker-executor).
-- Enough disk space to handle generated Code Quality files. For example on the [GitLab project](https://gitlab.com/gitlab-org/gitlab) the files are approximately 7 GB.
+- For the [docker-in-docker workflow](../../../ci/docker/using_docker_build.md#use-docker-in-docker-workflow-with-docker-executor).
+- With enough disk space to handle generated Code Quality files. For example on the [GitLab project](https://gitlab.com/gitlab-org/gitlab) the files are approximately 7 GB.
Once you set up the Runner, include the CodeQuality template in your CI config:
@@ -67,7 +79,7 @@ include:
The above example will create a `code_quality` job in your CI/CD pipeline which
will scan your source code for code quality issues. The report will be saved as a
-[Code Quality report artifact](../../../ci/yaml/README.md#artifactsreportscodequality-starter)
+[Code Quality report artifact](../../../ci/pipelines/job_artifacts.md#artifactsreportscodequality-starter)
that you can later download and analyze. Due to implementation limitations we always
take the latest Code Quality artifact available.
@@ -227,7 +239,7 @@ do this:
1. Define a job in your `.gitlab-ci.yml` file that generates the
[Code Quality report
- artifact](../../../ci/yaml/README.md#artifactsreportscodequality-starter).
+ artifact](../../../ci/pipelines/job_artifacts.md#artifactsreportscodequality-starter).
1. Configure your tool to generate the Code Quality report artifact as a JSON
file that implements subset of the [Code Climate
spec](https://github.com/codeclimate/platform/blob/master/spec/analyzers/SPEC.md#data-types).
diff --git a/doc/user/project/merge_requests/creating_merge_requests.md b/doc/user/project/merge_requests/creating_merge_requests.md
index d8a2b427288..544727380e7 100644
--- a/doc/user/project/merge_requests/creating_merge_requests.md
+++ b/doc/user/project/merge_requests/creating_merge_requests.md
@@ -171,7 +171,7 @@ When the changes are merged, your changes are added to the upstream repository a
the branch as per specification. After your work is merged, if you don't intend to
make any other contributions to the upstream project, you can unlink your
fork from its upstream project in the **Settings > Advanced Settings** section by
-[removing the forking relashionship](../settings/index.md#removing-a-fork-relationship).
+[removing the forking relationship](../settings/index.md#removing-a-fork-relationship).
For further details, [see the forking workflow documentation](../repository/forking_workflow.md).
diff --git a/doc/user/project/merge_requests/img/accessibility_mr_widget_v13_0.png b/doc/user/project/merge_requests/img/accessibility_mr_widget_v13_0.png
new file mode 100644
index 00000000000..c52bf9964f1
--- /dev/null
+++ b/doc/user/project/merge_requests/img/accessibility_mr_widget_v13_0.png
Binary files differ
diff --git a/doc/user/project/merge_requests/img/code_quality.png b/doc/user/project/merge_requests/img/code_quality.png
index a20f6476fb8..3c6c92baad2 100644
--- a/doc/user/project/merge_requests/img/code_quality.png
+++ b/doc/user/project/merge_requests/img/code_quality.png
Binary files differ
diff --git a/doc/user/project/merge_requests/reviewing_and_managing_merge_requests.md b/doc/user/project/merge_requests/reviewing_and_managing_merge_requests.md
index bb5aadfa9b9..fdcb1049ef7 100644
--- a/doc/user/project/merge_requests/reviewing_and_managing_merge_requests.md
+++ b/doc/user/project/merge_requests/reviewing_and_managing_merge_requests.md
@@ -64,6 +64,15 @@ list.
![Merge request diff file navigation](img/merge_request_diff_file_navigation.png)
+### Merge requests commit navigation
+
+> [Introduced](https://gitlab.com/gitlab-org/gitlab/-/issues/18140) in GitLab 13.0.
+
+To seamlessly navigate among commits in a merge request, from the **Commits** tab, click one of
+the commits to open the single-commit view. From there, you can navigate among the commits
+by clicking the **Prev** and **Next** buttons on the top-right of the page or by using the
+<kbd>X</kbd> and <kbd>C</kbd> keyboard shortcuts.
+
### Incrementally expand merge request diffs
By default, the diff shows only the parts of a file which are changed.
@@ -102,7 +111,7 @@ you will be able to see:
- Both pre and post-merge pipelines and the environment information if any.
- Which deployments are in progress.
-If there's an [environment](../../../ci/environments.md) and the application is
+If there's an [environment](../../../ci/environments/index.md) and the application is
successfully deployed to it, the deployed environment and the link to the
Review App will be shown as well.
diff --git a/doc/user/project/merge_requests/test_coverage_visualization.md b/doc/user/project/merge_requests/test_coverage_visualization.md
index 71fbdaf112f..84d60fca72d 100644
--- a/doc/user/project/merge_requests/test_coverage_visualization.md
+++ b/doc/user/project/merge_requests/test_coverage_visualization.md
@@ -17,14 +17,14 @@ MR is merged.
## How test coverage visualization works
Collecting the coverage information is done via GitLab CI/CD's
-[artifacts reports feature](../../../ci/yaml/README.md#artifactsreports).
+[artifacts reports feature](../../../ci/pipelines/job_artifacts.md#artifactsreports).
You can specify one or more coverage reports to collect, including wildcard paths.
GitLab will then take the coverage information in all the files and combine it
together.
For the coverage analysis to work, you have to provide a properly formatted
[Cobertura XML](https://cobertura.github.io/cobertura/) report to
-[`artifacts:reports:cobertura`](../../../ci/yaml/README.md#artifactsreportscobertura).
+[`artifacts:reports:cobertura`](../../../ci/pipelines/job_artifacts.md#artifactsreportscobertura).
This format was originally developed for Java, but most coverage analysis frameworks
for other languages have plugins to add support for it, like:
diff --git a/doc/user/project/merge_requests/versions.md b/doc/user/project/merge_requests/versions.md
index 2f51af24a95..84934148bdc 100644
--- a/doc/user/project/merge_requests/versions.md
+++ b/doc/user/project/merge_requests/versions.md
@@ -57,7 +57,7 @@ source and target branch can be shown mixed together making it hard to
understand which changes are being added and which already exist in the
target branch.
-In GitLab 12.10, we added an **experimental** comparison mode, which
+In GitLab 12.10, we added a comparison mode, which
shows a diff calculated by simulating how it would look like once merged - a more accurate
representation of the changes rather than using the base of the two
branches. The new mode is available from the comparison target drop down
@@ -67,26 +67,6 @@ current default comparison.
![Merge request versions compare HEAD](img/versions_compare_head_v12_10.png)
-### Enable or disable `HEAD` comparison mode **(CORE ONLY)**
-
-`HEAD` comparison mode is under development and not ready for production use. It is
-deployed behind a feature flag that is **disabled by default**.
-[GitLab administrators with access to the GitLab Rails console](../../../administration/troubleshooting/navigating_gitlab_via_rails_console.md#starting-a-rails-console-session)
-can enable it for your instance. You're welcome to test it, but use it at your
-own risk.
-
-To enable it:
-
-```ruby
-Feature.enable(:diff_compare_with_head)
-```
-
-To disable it:
-
-```ruby
-Feature.disable(:diff_compare_with_head)
-```
-
<!-- ## Troubleshooting
Include any troubleshooting steps that you can foresee. If you know beforehand what issues
diff --git a/doc/user/project/new_ci_build_permissions_model.md b/doc/user/project/new_ci_build_permissions_model.md
index c5ab8d66852..120c7a35cb2 100644
--- a/doc/user/project/new_ci_build_permissions_model.md
+++ b/doc/user/project/new_ci_build_permissions_model.md
@@ -229,5 +229,5 @@ to projects and their project permissions.
### API
-GitLab API cannot be used via `CI_JOB_TOKEN` but there is a [proposal](https://gitlab.com/gitlab-org/gitlab-foss/issues/29566)
+GitLab API cannot be used via `CI_JOB_TOKEN` but there is a [proposal](https://gitlab.com/gitlab-org/gitlab/-/issues/35067)
to support it.
diff --git a/doc/user/project/operations/alert_management.md b/doc/user/project/operations/alert_management.md
new file mode 100644
index 00000000000..2dcf72aaf01
--- /dev/null
+++ b/doc/user/project/operations/alert_management.md
@@ -0,0 +1,79 @@
+# Alert Management
+
+> [Introduced](https://gitlab.com/groups/gitlab-org/-/epics/2877) in GitLab 13.0.
+
+Alert Management enables developers to easily discover and view the alerts
+generated by their application. By surfacing alert information where the code is
+being developed, efficiency and awareness can be increased.
+
+## Enable Alert Management
+
+NOTE: **Note:**
+You will need at least Maintainer [permissions](../../permissions.md) to enable the Alert Management feature.
+
+1. Follow the [instructions for toggling generic alerts](../integrations/generic_alerts.md#setting-up-generic-alerts)
+1. You can now visit **{cloud-gear}** **Operations > Alerts** in your project's sidebar to [view a list](#alert-management-list) of alerts.
+
+![Alert Management Toggle](img/alert_management_1_v13_1.png)
+
+## Populate Alert data
+
+To populate data, see the instructions for
+[customizing the payload](../integrations/generic_alerts.md) and making a
+request to the alerts endpoint.
+
+## Alert Management severity
+
+Each level of alert contains a uniquely shaped and color-coded icon to help
+you identify the severity of a particular alert. These severity icons help you
+immediately identify which alerts you should prioritize investigating:
+
+![Alert Management Severity System](img/alert_management_severity_v13_0.png)
+
+Alerts contain one of the following icons:
+
+- **Critical**: **{severity-critical}** and hexadecimal color `#8b2615`
+- **High**: **{severity-high}** and hexadecimal color `#c0341d`
+- **Medium**: **{severity-medium}** and hexadecimal color `#fca429`
+- **Low**: **{severity-low}** and hexadecimal color `#fdbc60`
+- **Info**: **{severity-info}** and hexadecimal color `#418cd8`
+- **Unknown**: **{severity-unknown}** and hexadecimal color `#bababa`
+
+## Alert Management list
+
+NOTE: **Note:**
+You will need at least Developer [permissions](../../permissions.md) to view the Alert Management list.
+
+You can find the Alert Management list at **{cloud-gear}** **Operations > Alerts** in your project's sidebar.
+Each alert contains the following metrics:
+
+![Alert Management List](img/alert_management_1_v13_0.png)
+
+- **Severity** - The current importance of a alert and how much attention it should receive.
+- **Start time** - How long ago the alert fired. This field uses the standard GitLab pattern of `X time ago`, but is supported by a granular date/time tooltip depending on the user's locale.
+- **End time** - How long ago the alert fired was resolved. This field uses the standard GitLab pattern of `X time ago`, but is supported by a granular date/time tooltip depending on the user's locale.
+- **Alert description** - The description of the alert, which attempts to capture the most meaningful data.
+- **Event count** - The number of times that an alert has fired.
+- **Status** - The [current status](#alert-management-statuses) of the alert.
+
+### Alert Management statuses
+
+Each alert contains a status dropdown to indicate which alerts need investigation.
+Standard alert statuses include `triggered`, `acknowledged`, and `resolved`:
+
+- **Triggered**: No one has begun investigation.
+- **Acknowledged**: Someone is actively investigating the problem.
+- **Resolved**: No further work is required.
+
+## Alert Management details
+
+NOTE: **Note:**
+You will need at least Developer [permissions](../../permissions.md) to view Alert Management details.
+
+Navigate to the Alert Management detail view by visiting the [Alert Management list](#alert-management-list) and selecting an Alert from the list.
+
+![Alert Management Detail View](img/alert_detail_v13_0.png)
+
+### Update an Alert's status
+
+The Alert Management detail view allows users to update the Alert Status. See [Alert Management statuses](#alert-management-statuses) for more details.
diff --git a/doc/user/project/operations/error_tracking.md b/doc/user/project/operations/error_tracking.md
index a95459e093a..23a50fd7766 100644
--- a/doc/user/project/operations/error_tracking.md
+++ b/doc/user/project/operations/error_tracking.md
@@ -1,3 +1,9 @@
+---
+stage: Monitor
+group: Health
+info: To determine the technical writer assigned to the Stage/Group associated with this page, see https://about.gitlab.com/handbook/engineering/ux/technical-writing/#designated-technical-writers
+---
+
# Error Tracking
> [Introduced](https://gitlab.com/groups/gitlab-org/-/epics/169) in GitLab 11.8.
@@ -53,7 +59,7 @@ From error list, users can navigate to the error details page by clicking the ti
This page has:
- A link to the Sentry issue.
-- A link to the GitLab commit if the Sentry [release id/version](https://docs.sentry.io/workflow/releases/?platform=javascript#configure-sdk) on the Sentry Issue's first release matches a commit SHA in your GitLab hosted project.
+- A link to the GitLab commit if the Sentry [release ID/version](https://docs.sentry.io/workflow/releases/?platform=javascript#configure-sdk) on the Sentry Issue's first release matches a commit SHA in your GitLab hosted project.
- Other details about the issue, including a full stack trace.
- In [GitLab 12.7 and newer](https://gitlab.com/gitlab-org/gitlab/issues/36246), language and urgency are displayed.
diff --git a/doc/user/project/operations/feature_flags.md b/doc/user/project/operations/feature_flags.md
index f13fbcb2722..8716d5feb4a 100644
--- a/doc/user/project/operations/feature_flags.md
+++ b/doc/user/project/operations/feature_flags.md
@@ -1,3 +1,9 @@
+---
+stage: Release
+group: Progressive Delivery
+info: To determine the technical writer assigned to the Stage/Group associated with this page, see https://about.gitlab.com/handbook/engineering/ux/technical-writing/#designated-technical-writers
+---
+
# Feature Flags **(PREMIUM)**
> [Introduced](https://gitlab.com/gitlab-org/gitlab/-/merge_requests/7433) in GitLab 11.4.
@@ -15,6 +21,13 @@ This helps reducing risk and allows you to easily manage which features to enabl
GitLab offers a Feature Flags interface that allows you to create, toggle and
remove feature flags.
+<div class="video-fallback">
+ <a href="https://www.youtube.com/watch?v=5tw2p6lwXxo">Watch</a> a use case between Feature Flags and Sentry Error Tracking
+</div>
+<figure class="video-container">
+ <iframe src="https://www.youtube.com/embed/5tw2p6lwXxo" frameborder="0" allowfullscreen="true"> </iframe>
+</figure>
+
## How it works
Underneath, GitLab uses [unleash](https://github.com/Unleash/unleash), a feature
@@ -36,10 +49,10 @@ To add a new feature flag:
1. Click on the **New Feature Flag** button.
1. Give it a name.
- NOTE: **Note:**
- A name can contain only lowercase letters, digits, underscores (`_`)
- and dashes (`-`), must start with a letter, and cannot end with a dash (`-`)
- or an underscore (`_`).
+ NOTE: **Note:**
+ A name can contain only lowercase letters, digits, underscores (`_`)
+ and dashes (`-`), must start with a letter, and cannot end with a dash (`-`)
+ or an underscore (`_`).
1. Give it a description (optional, 255 characters max).
1. Define environment [specs](#define-environment-specs). If you want the flag on by default, enable the catch-all [wildcard spec (`*`)](#define-environment-specs)
@@ -66,24 +79,59 @@ For example, you may not want to enable a feature flag on production until your
first confirmed that the feature is working correctly on testing environments.
To handle these situations, you can enable a feature flag on a particular environment
-with [Environment specs](../../../ci/environments.md#scoping-environments-with-specs).
+with [Environment specs](../../../ci/environments/index.md#scoping-environments-with-specs).
You can define multiple specs per flag so that you can control your feature flag more granularly.
To define specs for each environment:
1. Navigate to your project's **Operations > Feature Flags**.
1. Click on the **New Feature Flag** button or edit an existing flag.
-1. Set the status of the default [spec](../../../ci/environments.md#scoping-environments-with-specs) (`*`). Choose a rollout strategy. This status and rollout strategy combination will be used for _all_ environments.
-1. If you want to enable/disable the feature on a specific environment, create a new [spec](../../../ci/environments.md#scoping-environments-with-specs) and type the environment name.
+1. Set the status of the default [spec](../../../ci/environments/index.md#scoping-environments-with-specs) (`*`). Choose a rollout strategy. This status and rollout strategy combination will be used for _all_ environments.
+1. If you want to enable/disable the feature on a specific environment, create a new [spec](../../../ci/environments/index.md#scoping-environments-with-specs) and type the environment name.
1. Set the status and rollout strategy of the additional spec. This status and rollout strategy combination takes precedence over the default spec since we always use the most specific match available.
1. Click **Create feature flag** or **Update feature flag**.
![Feature flag specs list](img/specs_list_v12_6.png)
NOTE: **NOTE**
-We'd highly recommend you to use the [Environment](../../../ci/environments.md)
+We'd highly recommend you to use the [Environment](../../../ci/environments/index.md)
feature in order to quickly assess which flag is enabled per environment.
+## Feature flag behavior change in 13.0
+
+> [Introduced](https://gitlab.com/gitlab-org/gitlab/-/issues/35555) in GitLab 13.0.
+
+Starting in GitLab 13.0, you can apply a feature flag strategy across multiple environment specs,
+without defining the strategy multiple times.
+
+This feature is under development and not ready for production use. It is
+deployed behind a feature flag that is **disabled by default**.
+[GitLab administrators with access to the GitLab Rails console](../../../administration/feature_flags.md)
+can enable it for your instance.
+
+To enable it:
+
+```ruby
+Feature.enable(:feature_flags_new_version)
+```
+
+To disable it:
+
+```ruby
+Feature.disable(:feature_flags_new_version)
+```
+
+### Applying a strategy to environments
+
+After a strategy is defined, it applies to **All Environments** by default. To
+make a strategy apply to a specific environment spec:
+
+1. Click the **Add Environment** button.
+1. Create a new
+ [spec](../../../ci/environments/index.md#scoping-environments-with-specs).
+
+To apply the strategy to multiple environment specs, repeat these steps.
+
## Feature Flag strategies
GitLab Feature Flag adopts [Unleash](https://unleash.github.io)
@@ -148,12 +196,12 @@ To get the access credentials that your application will need to talk to GitLab:
1. Navigate to your project's **Operations > Feature Flags**.
1. Click on the **Configure** button to see the values:
- - **API URL**: URL where the client (application) connects to get a list of feature flags.
- - **Instance ID**: Unique token that authorizes the retrieval of the feature flags.
- - **Application name**: The name of the running environment. For instance,
- if the application runs for production server, application name would be
- `production` or similar. This value is used for
- [the environment spec evaluation](#define-environment-specs).
+ - **API URL**: URL where the client (application) connects to get a list of feature flags.
+ - **Instance ID**: Unique token that authorizes the retrieval of the feature flags.
+ - **Application name**: The name of the running environment. For instance,
+ if the application runs for a production server, application name would be
+ `production` or similar. This value is used for
+ [the environment spec evaluation](#define-environment-specs).
NOTE: **Note:**
The meaning of these fields might change over time. For example, we are not sure
@@ -231,7 +279,7 @@ func main() {
Here's an example of how to integrate the feature flags in a Ruby application.
-The Unleash client is given a user id for use with a **Percent rollout (logged in users)** rollout strategy or a list of **Target Users**.
+The Unleash client is given a user ID for use with a **Percent rollout (logged in users)** rollout strategy or a list of **Target Users**.
```ruby
#!/usr/bin/env ruby
@@ -265,3 +313,4 @@ to control them in an automated flow:
- [Feature Flags API](../../../api/feature_flags.md)
- [Feature Flag Specs API](../../../api/feature_flag_specs.md)
+- [Feature Flag User Lists API](../../../api/feature_flag_user_lists.md)
diff --git a/doc/user/project/operations/img/alert_detail_v13_0.png b/doc/user/project/operations/img/alert_detail_v13_0.png
new file mode 100644
index 00000000000..7da09407cd5
--- /dev/null
+++ b/doc/user/project/operations/img/alert_detail_v13_0.png
Binary files differ
diff --git a/doc/user/project/operations/img/alert_management_1_v13_0.png b/doc/user/project/operations/img/alert_management_1_v13_0.png
new file mode 100644
index 00000000000..dbc1e795b16
--- /dev/null
+++ b/doc/user/project/operations/img/alert_management_1_v13_0.png
Binary files differ
diff --git a/doc/user/project/operations/img/alert_management_1_v13_1.png b/doc/user/project/operations/img/alert_management_1_v13_1.png
new file mode 100644
index 00000000000..c01b4749eda
--- /dev/null
+++ b/doc/user/project/operations/img/alert_management_1_v13_1.png
Binary files differ
diff --git a/doc/user/project/operations/img/alert_management_severity_v13_0.png b/doc/user/project/operations/img/alert_management_severity_v13_0.png
new file mode 100644
index 00000000000..f996d6e88f4
--- /dev/null
+++ b/doc/user/project/operations/img/alert_management_severity_v13_0.png
Binary files differ
diff --git a/doc/user/project/operations/index.md b/doc/user/project/operations/index.md
index df7ce61525e..954f88fe4f2 100644
--- a/doc/user/project/operations/index.md
+++ b/doc/user/project/operations/index.md
@@ -4,7 +4,7 @@ GitLab provides a variety of tools to help operate and maintain
your applications:
- Collect [Prometheus metrics](../integrations/prometheus_library/index.md).
-- Deploy to different [environments](../../../ci/environments.md).
+- Deploy to different [environments](../../../ci/environments/index.md).
- Connect your project to a [Kubernetes cluster](../clusters/index.md).
- Manage your infrastructure with [Infrastructure as Code](../../infrastructure/index.md) approaches.
- Discover and view errors generated by your applications with [Error Tracking](error_tracking.md).
diff --git a/doc/user/project/operations/linking_to_an_external_dashboard.md b/doc/user/project/operations/linking_to_an_external_dashboard.md
index 620d9d70e4d..8e1117de4c7 100644
--- a/doc/user/project/operations/linking_to_an_external_dashboard.md
+++ b/doc/user/project/operations/linking_to_an_external_dashboard.md
@@ -13,7 +13,7 @@ You can add a button to the Monitoring dashboard linking directly to your existi
![External Dashboard Settings](img/external_dashboard_settings.png)
1. There should now be a button on your
- [Monitoring dashboard](../../../ci/environments.md#monitoring-environments) which
+ [Monitoring dashboard](../../../ci/environments/index.md#monitoring-environments) which
will open the URL you entered in the above step.
![External Dashboard Link](img/external_dashboard_link.png)
diff --git a/doc/user/project/operations/tracing.md b/doc/user/project/operations/tracing.md
index 8282a980ced..07f60c37f1b 100644
--- a/doc/user/project/operations/tracing.md
+++ b/doc/user/project/operations/tracing.md
@@ -1,3 +1,9 @@
+---
+stage: Monitor
+group: APM
+info: To determine the technical writer assigned to the Stage/Group associated with this page, see https://about.gitlab.com/handbook/engineering/ux/technical-writing/#designated-technical-writers
+---
+
# Tracing **(ULTIMATE)**
> [Introduced](https://gitlab.com/gitlab-org/gitlab/-/merge_requests/7903) in GitLab Ultimate 11.5.
diff --git a/doc/user/project/pages/custom_domains_ssl_tls_certification/dns_concepts.md b/doc/user/project/pages/custom_domains_ssl_tls_certification/dns_concepts.md
index 172f5d4377f..6e48afad96a 100644
--- a/doc/user/project/pages/custom_domains_ssl_tls_certification/dns_concepts.md
+++ b/doc/user/project/pages/custom_domains_ssl_tls_certification/dns_concepts.md
@@ -1,5 +1,8 @@
---
type: concepts
+stage: Release
+group: Release Management
+info: To determine the technical writer assigned to the Stage/Group associated with this page, see https://about.gitlab.com/handbook/engineering/ux/technical-writing/#designated-technical-writers
---
# DNS records overview
diff --git a/doc/user/project/pages/custom_domains_ssl_tls_certification/index.md b/doc/user/project/pages/custom_domains_ssl_tls_certification/index.md
index 07bd2a61eb8..a5fc5b00b53 100644
--- a/doc/user/project/pages/custom_domains_ssl_tls_certification/index.md
+++ b/doc/user/project/pages/custom_domains_ssl_tls_certification/index.md
@@ -2,6 +2,9 @@
last_updated: 2019-07-04
type: reference, howto
disqus_identifier: 'https://docs.gitlab.com/ee/user/project/pages/getting_started_part_three.html'
+stage: Release
+group: Release Management
+info: To determine the technical writer assigned to the Stage/Group associated with this page, see https://about.gitlab.com/handbook/engineering/ux/technical-writing/#designated-technical-writers
---
# Custom domains and SSL/TLS Certificates
@@ -60,6 +63,11 @@ according to the type of domain you want to use with your Pages site:
- [For subdomains](#for-subdomains), `subdomain.example.com`.
- [For both](#for-both-root-and-subdomains).
+NOTE: **Note:**
+You can [configure IPv6 on self-managed instances](../../../../administration/pages/index.md#advanced-configuration),
+but IPv6 is not currently configured for Pages on GitLab.com.
+Follow [this issue](https://gitlab.com/gitlab-org/gitlab/-/issues/214718) for details.
+
##### For root domains
Root domains (`example.com`) require:
diff --git a/doc/user/project/pages/custom_domains_ssl_tls_certification/lets_encrypt_integration.md b/doc/user/project/pages/custom_domains_ssl_tls_certification/lets_encrypt_integration.md
index f80b741fb77..8b180d438ef 100644
--- a/doc/user/project/pages/custom_domains_ssl_tls_certification/lets_encrypt_integration.md
+++ b/doc/user/project/pages/custom_domains_ssl_tls_certification/lets_encrypt_integration.md
@@ -1,6 +1,9 @@
---
type: reference
description: "Automatic Let's Encrypt SSL certificates for GitLab Pages."
+stage: Release
+group: Release Management
+info: To determine the technical writer assigned to the Stage/Group associated with this page, see https://about.gitlab.com/handbook/engineering/ux/technical-writing/#designated-technical-writers
---
# GitLab Pages integration with Let's Encrypt
diff --git a/doc/user/project/pages/custom_domains_ssl_tls_certification/ssl_tls_concepts.md b/doc/user/project/pages/custom_domains_ssl_tls_certification/ssl_tls_concepts.md
index bc51a9c90d2..e307b807aaa 100644
--- a/doc/user/project/pages/custom_domains_ssl_tls_certification/ssl_tls_concepts.md
+++ b/doc/user/project/pages/custom_domains_ssl_tls_certification/ssl_tls_concepts.md
@@ -1,5 +1,8 @@
---
type: concepts
+stage: Release
+group: Release Management
+info: To determine the technical writer assigned to the Stage/Group associated with this page, see https://about.gitlab.com/handbook/engineering/ux/technical-writing/#designated-technical-writers
---
# SSL/TLS Certificates
diff --git a/doc/user/project/pages/getting_started/fork_sample_project.md b/doc/user/project/pages/getting_started/fork_sample_project.md
index ef30606a9ab..00cea846f91 100644
--- a/doc/user/project/pages/getting_started/fork_sample_project.md
+++ b/doc/user/project/pages/getting_started/fork_sample_project.md
@@ -1,5 +1,8 @@
---
type: reference, howto
+stage: Release
+group: Release Management
+info: To determine the technical writer assigned to the Stage/Group associated with this page, see https://about.gitlab.com/handbook/engineering/ux/technical-writing/#designated-technical-writers
---
# New Pages website from a forked sample
diff --git a/doc/user/project/pages/getting_started/new_or_existing_website.md b/doc/user/project/pages/getting_started/new_or_existing_website.md
index 027a238bf02..9a00b724753 100644
--- a/doc/user/project/pages/getting_started/new_or_existing_website.md
+++ b/doc/user/project/pages/getting_started/new_or_existing_website.md
@@ -1,5 +1,8 @@
---
type: reference, howto
+stage: Release
+group: Release Management
+info: To determine the technical writer assigned to the Stage/Group associated with this page, see https://about.gitlab.com/handbook/engineering/ux/technical-writing/#designated-technical-writers
---
# Start a new Pages website from scratch or deploy an existing website
diff --git a/doc/user/project/pages/getting_started/pages_bundled_template.md b/doc/user/project/pages/getting_started/pages_bundled_template.md
index 9c097c9acdb..745c50e8d65 100644
--- a/doc/user/project/pages/getting_started/pages_bundled_template.md
+++ b/doc/user/project/pages/getting_started/pages_bundled_template.md
@@ -1,5 +1,8 @@
---
type: reference, howto
+stage: Release
+group: Release Management
+info: To determine the technical writer assigned to the Stage/Group associated with this page, see https://about.gitlab.com/handbook/engineering/ux/technical-writing/#designated-technical-writers
---
# New Pages website from a bundled template
diff --git a/doc/user/project/pages/getting_started_part_four.md b/doc/user/project/pages/getting_started_part_four.md
index 645cc1c5795..4e95b5d5a69 100644
--- a/doc/user/project/pages/getting_started_part_four.md
+++ b/doc/user/project/pages/getting_started_part_four.md
@@ -1,6 +1,9 @@
---
last_updated: 2020-01-06
type: reference, howto
+stage: Release
+group: Release Management
+info: To determine the technical writer assigned to the Stage/Group associated with this page, see https://about.gitlab.com/handbook/engineering/ux/technical-writing/#designated-technical-writers
---
# Creating and Tweaking GitLab CI/CD for GitLab Pages
@@ -37,8 +40,8 @@ anything for them to work.
Explaining [every detail of GitLab CI/CD](../../../ci/yaml/README.md)
and GitLab Runner is out of the scope of this guide, but we'll
need to understand just a few things to be able to write our own
-`.gitlab-ci.yml` or tweak an existing one. It's an
-[Yaml](https://docs.ansible.com/ansible/latest/reference_appendices/YAMLSyntax.html) file,
+`.gitlab-ci.yml` or tweak an existing one. It's a
+[YAML](https://docs.ansible.com/ansible/latest/reference_appendices/YAMLSyntax.html) file,
with its own syntax. You can always check your CI syntax with
the [GitLab CI/CD Lint Tool](https://gitlab.com/ci/lint).
@@ -60,7 +63,7 @@ jekyll build
### Script
-To transpose this script to Yaml, it would be like this:
+To transpose this script to YAML, it would be like this:
```yaml
script:
@@ -364,7 +367,7 @@ from Jekyll `_config.yml` file, otherwise Jekyll will
understand it as a regular directory to build
together with the site:
-```yml
+```yaml
exclude:
- vendor
```
diff --git a/doc/user/project/pages/getting_started_part_one.md b/doc/user/project/pages/getting_started_part_one.md
index b876e547ba5..c0bdd4b7f0b 100644
--- a/doc/user/project/pages/getting_started_part_one.md
+++ b/doc/user/project/pages/getting_started_part_one.md
@@ -1,6 +1,9 @@
---
last_updated: 2018-06-04
type: concepts, reference
+stage: Release
+group: Release Management
+info: To determine the technical writer assigned to the Stage/Group associated with this page, see https://about.gitlab.com/handbook/engineering/ux/technical-writing/#designated-technical-writers
---
# GitLab Pages domain names, URLs, and baseurls
diff --git a/doc/user/project/pages/index.md b/doc/user/project/pages/index.md
index 02bb8e13f4a..e81c9699153 100644
--- a/doc/user/project/pages/index.md
+++ b/doc/user/project/pages/index.md
@@ -2,6 +2,9 @@
description: 'Learn how to use GitLab Pages to deploy a static website at no additional cost.'
last_updated: 2019-06-04
type: index, reference
+stage: Release
+group: Release Management
+info: To determine the technical writer assigned to the Stage/Group associated with this page, see https://about.gitlab.com/handbook/engineering/ux/technical-writing/#designated-technical-writers
---
# GitLab Pages
diff --git a/doc/user/project/pages/introduction.md b/doc/user/project/pages/introduction.md
index f77c572664a..e36dfd89ab3 100644
--- a/doc/user/project/pages/introduction.md
+++ b/doc/user/project/pages/introduction.md
@@ -1,6 +1,9 @@
---
type: reference
last_updated: 2020-01-06
+stage: Release
+group: Release Management
+info: To determine the technical writer assigned to the Stage/Group associated with this page, see https://about.gitlab.com/handbook/engineering/ux/technical-writing/#designated-technical-writers
---
# Exploring GitLab Pages
@@ -86,11 +89,9 @@ When using Pages under the general domain of a GitLab instance (`*.example.io`),
you _cannot_ use HTTPS with sub-subdomains. That means that if your
username/groupname contains a dot, for example `foo.bar`, the domain
`https://foo.bar.example.io` will _not_ work. This is a limitation of the
-[HTTP Over TLS protocol][rfc]. HTTP pages will continue to work provided you
+[HTTP Over TLS protocol](https://tools.ietf.org/html/rfc2818#section-3.1). HTTP pages will continue to work provided you
don't redirect HTTP to HTTPS.
-[rfc]: https://tools.ietf.org/html/rfc2818#section-3.1 "HTTP Over TLS RFC"
-
GitLab Pages [does **not** support group websites for subgroups](../../group/subgroups/index.md#limitations).
You can only create the highest-level group website.
@@ -169,13 +170,10 @@ pages:
- pages
```
-See an example that has different files in the [`master` branch][jekyll-master]
-and the source files for Jekyll are in a [`pages` branch][jekyll-pages] which
+See an example that has different files in the [`master` branch](https://gitlab.com/pages/jekyll-branched/tree/master)
+and the source files for Jekyll are in a [`pages` branch](https://gitlab.com/pages/jekyll-branched/tree/pages) which
also includes `.gitlab-ci.yml`.
-[jekyll-master]: https://gitlab.com/pages/jekyll-branched/tree/master
-[jekyll-pages]: https://gitlab.com/pages/jekyll-branched/tree/pages
-
### Serving compressed assets
Most modern browsers support downloading files in a compressed format. This
diff --git a/doc/user/project/pages/pages_access_control.md b/doc/user/project/pages/pages_access_control.md
index 1d8119cfe87..7fe4c4c051d 100644
--- a/doc/user/project/pages/pages_access_control.md
+++ b/doc/user/project/pages/pages_access_control.md
@@ -1,5 +1,8 @@
---
type: reference, howto
+stage: Release
+group: Release Management
+info: To determine the technical writer assigned to the Stage/Group associated with this page, see https://about.gitlab.com/handbook/engineering/ux/technical-writing/#designated-technical-writers
---
# GitLab Pages Access Control
@@ -11,6 +14,9 @@ You can enable Pages access control on your project, so that only
[members of your project](../../permissions.md#project-members-permissions)
(at least Guest) can access your website:
+<i class="fa fa-youtube-play youtube" aria-hidden="true"></i>
+For a demonstration, see [Pages access controls](https://www.youtube.com/watch?v=tSPAr5mQYc8).
+
1. Navigate to your project's **Settings > General** and expand **Visibility, project features, permissions**.
1. Toggle the **Pages** button to enable the access control.
diff --git a/doc/user/project/protected_tags.md b/doc/user/project/protected_tags.md
index 6f68a2c9907..b134d283ba9 100644
--- a/doc/user/project/protected_tags.md
+++ b/doc/user/project/protected_tags.md
@@ -4,7 +4,7 @@ type: reference, howto
# Protected Tags
-> [Introduced][ce-10356] in GitLab 9.1.
+> [Introduced](https://gitlab.com/gitlab-org/gitlab-foss/-/merge_requests/10356) in GitLab 9.1.
Protected Tags allow control over who has permission to create tags as well as preventing accidental update or deletion once created. Each rule allows you to match either an individual tag name, or use wildcards to control multiple tags at once.
@@ -67,5 +67,3 @@ questions that you know someone might ask.
Each scenario can be a third-level heading, e.g. `### Getting error message X`.
If you have none to add when creating a doc, leave this section in place
but commented out to help encourage others to add to it in the future. -->
-
-[ce-10356]: https://gitlab.com/gitlab-org/gitlab-foss/-/merge_requests/10356 "Protected Tags"
diff --git a/doc/user/project/push_options.md b/doc/user/project/push_options.md
index e8d94a05e7e..eab88d59867 100644
--- a/doc/user/project/push_options.md
+++ b/doc/user/project/push_options.md
@@ -33,10 +33,10 @@ git push -o <push_option>
You can use push options to skip a CI/CD pipeline, or pass environment variables.
-| Push option | Description |
-| ------------------------------ | ------------------------------------------------------------------------------------------- |
-| `ci.skip` | Do not create a CI pipeline for the latest push. |
-| `ci.variable="<name>=<value>"` | Provide [environment variables](../../ci/variables/README.md) to be used in a CI pipeline, if one is created due to the push. |
+| Push option | Description | Introduced in version |
+| ------------------------------ | ------------------------------------------------------------------------------------------- |---------------------- |
+| `ci.skip` | Do not create a CI pipeline for the latest push. | [11.7](https://gitlab.com/gitlab-org/gitlab-foss/-/merge_requests/15643) |
+| `ci.variable="<name>=<value>"` | Provide [environment variables](../../ci/variables/README.md) to be used in a CI pipeline, if one is created due to the push. | [12.6](https://gitlab.com/gitlab-org/gitlab/issues/27983) |
An example of using `ci.skip`:
diff --git a/doc/user/project/quick_actions.md b/doc/user/project/quick_actions.md
index 7937b7193d2..e2d0b616e4b 100644
--- a/doc/user/project/quick_actions.md
+++ b/doc/user/project/quick_actions.md
@@ -42,7 +42,7 @@ The following quick actions are applicable to descriptions, discussions and thre
| `/remove_milestone` | ✓ | ✓ | | Remove milestone |
| `/label ~label1 ~label2` | ✓ | ✓ | ✓ | Add label(s). Label names can also start without `~` but mixed syntax is not supported |
| `/relabel ~label1 ~label2` | ✓ | ✓ | ✓ | Replace existing label(s) with those specified |
-| `/unlabel ~label1 ~label2` | ✓ | ✓ | ✓ | Remove all or specific label(s) |
+| `/unlabel ~label1 ~label2` or `/remove_label ~label1 ~label2` | ✓ | ✓ | ✓ | Remove all or specific label(s) |
| `/copy_metadata <#issue>` | ✓ | ✓ | | Copy labels and milestone from another issue in the project |
| `/copy_metadata <!merge_request>` | ✓ | ✓ | | Copy labels and milestone from another merge request in the project |
| `/estimate <<W>w <DD>d <hh>h <mm>m>` | ✓ | ✓ | | Set time estimate. For example, `/estimate 1w 3d 2h 14m` |
diff --git a/doc/user/project/releases/img/edit_release_page_v13_0.png b/doc/user/project/releases/img/edit_release_page_v13_0.png
new file mode 100644
index 00000000000..1b4343019af
--- /dev/null
+++ b/doc/user/project/releases/img/edit_release_page_v13_0.png
Binary files differ
diff --git a/doc/user/project/releases/img/release_milestone_dropdown_v13_0.png b/doc/user/project/releases/img/release_milestone_dropdown_v13_0.png
new file mode 100644
index 00000000000..453c7ca93cc
--- /dev/null
+++ b/doc/user/project/releases/img/release_milestone_dropdown_v13_0.png
Binary files differ
diff --git a/doc/user/project/releases/index.md b/doc/user/project/releases/index.md
index ca28a79abf4..bdb99d16625 100644
--- a/doc/user/project/releases/index.md
+++ b/doc/user/project/releases/index.md
@@ -1,5 +1,8 @@
---
type: reference, howto
+stage: Release
+group: Release Management
+info: To determine the technical writer assigned to the Stage/Group associated with this page, see https://about.gitlab.com/handbook/engineering/ux/technical-writing/#designated-technical-writers
---
# Releases
@@ -102,12 +105,15 @@ The physical location of the asset can change at any time and the direct link wi
### Releases associated with milestones
-> [Introduced](https://gitlab.com/gitlab-org/gitlab/issues/29020) in GitLab 12.5.
+> - [Introduced](https://gitlab.com/gitlab-org/gitlab/issues/29020) in GitLab 12.5.
+> - [Updated](https://gitlab.com/gitlab-org/gitlab/-/issues/39467) to edit milestones in the UI in GitLab 13.0.
Releases can optionally be associated with one or more
[project milestones](../milestones/index.md#project-milestones-and-group-milestones)
by including a `milestones` array in your requests to the
-[Releases API](../../../api/releases/index.md#create-a-release).
+[Releases API](../../../api/releases/index.md#create-a-release) or by using the dropdown in the [Edit Release](#editing-a-release) page.
+
+![Release edit page with milestones dropdown expanded](img/release_milestone_dropdown_v13_0.png)
Releases display this association with the **Milestone** indicator in the top
section of the Release block on the **Project overview > Releases** page, along
@@ -190,22 +196,13 @@ the edit button (pencil icon) in the top-right corner of the release you want to
This will bring you to the **Edit Release** page, from which you can
change some of the release's details.
-![Edit release page](img/edit_release_page_v12_10.png)
+![Edit release page](img/edit_release_page_v13_0.png)
-Currently, it is only possible to edit the release title, notes, and asset
-links. To change other release information, such as its tag, associated
-milestones, or release date, use the [Releases
+Currently, it is only possible to edit the release title, notes, associated milestones, and asset
+links. To change other release information, such as its tag, or release date, use the [Releases
API](../../../api/releases/index.md#update-a-release). Editing this information
through the **Edit Release** page is planned for a future version of GitLab.
-Please note that the ability to edit asset links is currently behind a feature
-flag which is disabled by default. For self-managed instances, it can be enabled
-through the Rails console by a GitLab administrator with the following command:
-
-```ruby
-Feature.enable(:release_asset_link_editing)
-```
-
## Notification for Releases
> [Introduced](https://gitlab.com/gitlab-org/gitlab/issues/26001) in GitLab 12.4.
@@ -258,6 +255,9 @@ generate Release Evidence for an existing release. Because of this, [each releas
can have multiple Release Evidence snapshots. You can view the Release Evidence and
its details on the Release page.
+NOTE: **Note:**
+When the issue tracker is disabled, release evidence [is not collected](https://gitlab.com/gitlab-org/gitlab/-/issues/208397).
+
Release Evidence is stored as a JSON object, so you can compare evidence by using
commonly-available tools.
@@ -360,6 +360,39 @@ terminal.
Read the [GitLab Releaser documentation](https://gitlab.com/gitlab-org/gitlab-releaser/-/tree/master/docs/index.md)
for details.
+## Set a deploy freeze
+
+> [Introduced](https://gitlab.com/gitlab-org/gitlab/-/merge_requests/29382) in GitLab 13.0.
+
+With a deploy freeze, you can prevent an unintended production release during a
+period of time you specify, whether a company event or public holiday. You can
+now rely on the enforcement of policies that are typically outside the scope of
+GitLab to reduce uncertainty and risk when automating deployments.
+
+Deploy freeze periods are set at the Project level, and may be created and
+managed using the [Freeze Periods API](../../../api/freeze_periods.md).
+Each Freeze Period has a `freeze_start` and a `freeze_end`, which are defined
+as [crontab](https://crontab.guru/) entries. If a project contains multiple
+freeze periods, all will apply, and should they overlap, the freeze covers the
+complete overlapped period.
+
+During pipeline processing, GitLab CI creates an environment variable named
+`$CI_DEPLOY_FREEZE` if the currently executing job is within a
+Freeze Period.
+
+To take advantage of this variable, create a `rules` entry in your
+`gitlab-ci.yaml` to prevent the deployment job from executing.
+
+For example:
+
+```yaml
+deploy_to_production:
+ stage: deploy
+ script: deploy_to_prod.sh
+ rules:
+ - if: $CI_DEPLOY_FREEZE == null
+```
+
<!-- ## Troubleshooting
Include any troubleshooting steps that you can foresee. If you know beforehand what issues
diff --git a/doc/user/project/repository/file_finder.md b/doc/user/project/repository/file_finder.md
index 91e6d2912d1..ac10071e578 100644
--- a/doc/user/project/repository/file_finder.md
+++ b/doc/user/project/repository/file_finder.md
@@ -4,7 +4,7 @@ disqus_identifier: 'https://docs.gitlab.com/ee/workflow/file_finder.html'
# File finder
-> [Introduced][gh-9889] in GitLab 8.4.
+> [Introduced](https://github.com/gitlabhq/gitlabhq/pull/9889) in GitLab 8.4.
The file finder feature allows you to search for a file in a repository using the
GitLab UI.
@@ -12,7 +12,7 @@ GitLab UI.
You can find the **Find File** button when in the **Files** section of a
project.
-![Find file button](img/file_finder_find_button.png)
+![Find file button](img/file_finder_find_button_v12_10.png)
For those who prefer to keep their fingers on the keyboard, there is a
[shortcut button](../../shortcuts.md) as well, which you can invoke from _anywhere_
@@ -23,23 +23,20 @@ Press `t` to launch the File search function when in **Issues**,
Start typing what you are searching for and watch the magic happen. With the
up/down arrows, you go up and down the results, with `Esc` you close the search
-and go back to **Files**.
+and go back to **Files**
## How it works
The File finder feature is powered by the [Fuzzy filter](https://github.com/jeancroy/fuzz-aldrin-plus) library.
-It implements a fuzzy search with highlight, and tries to provide intuitive
+It implements a fuzzy search with the highlight and tries to provide intuitive
results by recognizing patterns that people use while searching.
-For example, consider the [GitLab CE repository][ce] and that we want to open
+For example, consider the [GitLab FOSS repository](https://gitlab.com/gitlab-org/gitlab-foss/tree/master) and that we want to open
the `app/controllers/admin/deploy_keys_controller.rb` file.
-Using fuzzy search, we start by typing letters that get us closer to the file.
+Using a fuzzy search, we start by typing letters that get us closer to the file.
**Tip:** To narrow down your search, include `/` in your search terms.
-![Find file button](img/file_finder_find_file.png)
-
-[gh-9889]: https://github.com/gitlabhq/gitlabhq/pull/9889 "File finder pull request"
-[ce]: https://gitlab.com/gitlab-org/gitlab-foss/tree/master "GitLab CE repository"
+![Find file button](img/file_finder_find_file_v12_10.png)
diff --git a/doc/user/project/repository/forking_workflow.md b/doc/user/project/repository/forking_workflow.md
index 126144de703..a49701017f3 100644
--- a/doc/user/project/repository/forking_workflow.md
+++ b/doc/user/project/repository/forking_workflow.md
@@ -72,5 +72,3 @@ changes are added to the repository and branch you're merging into.
## Removing a fork relationship
You can unlink your fork from its upstream project in the [advanced settings](../settings/index.md#removing-a-fork-relationship).
-
-[gitlab flow]: https://about.gitlab.com/blog/2014/09/29/gitlab-flow/ "GitLab Flow blog post"
diff --git a/doc/user/project/repository/img/file_finder_find_button.png b/doc/user/project/repository/img/file_finder_find_button.png
deleted file mode 100644
index 0c2d7d7bc73..00000000000
--- a/doc/user/project/repository/img/file_finder_find_button.png
+++ /dev/null
Binary files differ
diff --git a/doc/user/project/repository/img/file_finder_find_button_v12_10.png b/doc/user/project/repository/img/file_finder_find_button_v12_10.png
new file mode 100644
index 00000000000..e93db946005
--- /dev/null
+++ b/doc/user/project/repository/img/file_finder_find_button_v12_10.png
Binary files differ
diff --git a/doc/user/project/repository/img/file_finder_find_file.png b/doc/user/project/repository/img/file_finder_find_file.png
deleted file mode 100644
index c2212c7cd9e..00000000000
--- a/doc/user/project/repository/img/file_finder_find_file.png
+++ /dev/null
Binary files differ
diff --git a/doc/user/project/repository/img/file_finder_find_file_v12_10.png b/doc/user/project/repository/img/file_finder_find_file_v12_10.png
new file mode 100644
index 00000000000..1404ccc6d0b
--- /dev/null
+++ b/doc/user/project/repository/img/file_finder_find_file_v12_10.png
Binary files differ
diff --git a/doc/user/project/repository/index.md b/doc/user/project/repository/index.md
index f9c953db3e3..055443daa1f 100644
--- a/doc/user/project/repository/index.md
+++ b/doc/user/project/repository/index.md
@@ -162,7 +162,7 @@ Via command line, you can commit multiple times before pushing.
you will trigger a pipeline per push, not per commit.
- **Skip pipelines:**
You can add to you commit message the keyword
- [`[ci skip]`](../../../ci/yaml/README.md#skipping-jobs)
+ [`[ci skip]`](../../../ci/yaml/README.md#skip-pipeline)
and GitLab CI/CD will skip that pipeline.
- **Cross-link issues and merge requests:**
[Cross-linking](../issues/crosslinking_issues.md#from-commit-messages)
@@ -199,7 +199,7 @@ of commits to the fewest, and displayed on a nice graph:
## Repository graph
-The repository graph displays visually the Git flow strategy used in that repository:
+The repository graph displays the history of the repository network visually, including branches and merges. This can help you visualize the Git flow strategy used in the repository:
![repository Git flow](img/repo_graph.png)
@@ -215,7 +215,7 @@ minutes.
![Repository Languages bar](img/repository_languages_v12_2.gif)
Not all files are detected, among others; documentation,
-vendored code, and most markup languages are excluded. This behaviour can be
+vendored code, and most markup languages are excluded. This behavior can be
adjusted by overriding the default. For example, to enable `.proto` files to be
detected, add the following to `.gitattributes` in the root of your repository.
diff --git a/doc/user/project/repository/repository_mirroring.md b/doc/user/project/repository/repository_mirroring.md
index 8064eacf404..fdbea385998 100644
--- a/doc/user/project/repository/repository_mirroring.md
+++ b/doc/user/project/repository/repository_mirroring.md
@@ -55,6 +55,7 @@ For an existing project, you can set up push mirroring as follows:
1. Select **Push** from the **Mirror direction** dropdown.
1. Select an authentication method from the **Authentication method** dropdown, if necessary.
1. Check the **Only mirror protected branches** box, if necessary.
+1. Check the **Keep divergent refs** box, if desired.
1. Click the **Mirror repository** button to save the configuration.
![Repository mirroring push settings screen](img/repository_mirroring_push_settings.png)
@@ -88,6 +89,27 @@ You can choose to only push your protected branches from GitLab to your remote r
To use this option, check the **Only mirror protected branches** box when creating a repository
mirror.
+### Keep divergent refs **(CORE)**
+
+> [Introduced](https://gitlab.com/gitlab-org/gitlab/-/issues/208828) in GitLab 13.0.
+
+By default, if any ref on the remote mirror has diverged from the local
+repository, the *entire push* will fail, and nothing will be updated.
+
+For example, if a repository has `master`, `develop`, and `stable` branches that
+have been mirrored to a remote, and then a new commit is added to `develop` on
+the mirror, the next push attempt will fail, leaving `master` and `stable`
+out-of-date despite not having diverged. No change on any branch can be mirrored
+until the divergence is resolved.
+
+With the **Keep divergent refs** option enabled, the `develop` branch is
+skipped, allowing `master` and `stable` to be updated. The mirror status will
+reflect that `develop` has diverged and was skipped, and be marked as a failed
+update.
+
+NOTE: **Note:**
+After the mirror is created, this option can currently only be modified via the [API](../../../api/remote_mirrors.md).
+
## Setting up a push mirror from GitLab to GitHub **(CORE)**
To set up a mirror from GitLab to GitHub, you need to follow these steps:
diff --git a/doc/user/project/repository/x509_signed_commits/index.md b/doc/user/project/repository/x509_signed_commits/index.md
index 19238839a5e..20143af0b33 100644
--- a/doc/user/project/repository/x509_signed_commits/index.md
+++ b/doc/user/project/repository/x509_signed_commits/index.md
@@ -2,21 +2,21 @@
type: concepts, howto
---
-# Signing commits with x509
+# Signing commits and tags with X.509
-[x509](https://en.wikipedia.org/wiki/X.509) is a standard format for public key
+[X.509](https://en.wikipedia.org/wiki/X.509) is a standard format for public key
certificates issued by a public or private Public Key Infrastructure (PKI).
-Personal x509 certificates are used for authentication or signing purposes
+Personal X.509 certificates are used for authentication or signing purposes
such as SMIME, but Git also supports signing of commits and tags
-with x509 certificates in a similar way as with [GPG](../gpg_signed_commits/index.md).
-The main difference is the trust anchor which is the PKI for x509 certificates
+with X.509 certificates in a similar way as with [GPG](../gpg_signed_commits/index.md).
+The main difference is the trust anchor which is the PKI for X.509 certificates
instead of a web of trust with GPG.
-## How GitLab handles x509
+## How GitLab handles X.509
GitLab uses its own certificate store and therefore defines the trust chain.
-For a commit to be *verified* by GitLab:
+For a commit or tag to be *verified* by GitLab:
- The signing certificate email must match a verified email address used by the committer in GitLab.
- The Certificate Authority has to be trusted by the GitLab instance, see also
@@ -25,9 +25,14 @@ For a commit to be *verified* by GitLab:
which is usually up to three years.
- The signing time is equal or later then commit time.
-NOTE: **Note:** There is no certificate revocation list check in place at the moment.
+NOTE: **Note:** Certificate revocation lists are checked on a daily basis via background worker.
-## Obtaining an x509 key pair
+NOTE: **Note:** Self signed certificates without `authorityKeyIdentifier`,
+`subjectKeyIdentifier`, and `crlDistributionPoints` are not supported. We
+recommend using certificates from a PKI that are in line with
+[RFC 5280](https://tools.ietf.org/html/rfc5280).
+
+## Obtaining an X.509 key pair
If your organization has Public Key Infrastructure (PKI), that PKI will provide
an S/MIME key.
@@ -37,12 +42,12 @@ own self-signed one, or purchase one. MozillaZine keeps a nice collection
of [S/MIME-capable signing authorities](http://kb.mozillazine.org/Getting_an_SMIME_certificate)
and some of them generate keys for free.
-## Associating your x509 certificate with Git
+## Associating your X.509 certificate with Git
-To take advantage of X509 signing, you will need Git 2.19.0 or later. You can
+To take advantage of X.509 signing, you will need Git 2.19.0 or later. You can
check your Git version with:
-```sh
+```shell
git --version
```
@@ -52,7 +57,7 @@ If you have the correct version, you can proceed to configure Git.
Configure Git to use your key for signing:
-```sh
+```shell
signingkey = $( gpgsm --list-secret-keys | egrep '(key usage|ID)' | grep -B 1 digitalSignature | awk '/ID/ {print $2}' )
git config --global user.signingkey $signingkey
git config --global gpg.format x509
@@ -64,21 +69,21 @@ Install [smimesign](https://github.com/github/smimesign) by downloading the
installer or via `brew install smimesign` on MacOS.
Get the ID of your certificate with `smimesign --list-keys` and set your
-signingkey `git config --global user.signingkey ID`, then configure x509:
+signingkey `git config --global user.signingkey ID`, then configure X.509:
-```sh
+```shell
git config --global gpg.x509.program smimesign
git config --global gpg.format x509
```
## Signing commits
-After you have [associated your x509 certificate with Git](#associating-your-x509-certificate-with-git) you
+After you have [associated your X.509 certificate with Git](#associating-your-x509-certificate-with-git) you
can start signing your commits:
1. Commit like you used to, the only difference is the addition of the `-S` flag:
- ```sh
+ ```shell
git commit -S -m "feat: x509 signed commits"
```
@@ -87,7 +92,7 @@ can start signing your commits:
If you don't want to type the `-S` flag every time you commit, you can tell Git
to sign your commits automatically:
-```sh
+```shell
git config --global commit.gpgsign true
```
@@ -95,6 +100,34 @@ git config --global commit.gpgsign true
To verify that a commit is signed, you can use the `--show-signature` flag:
-```sh
+```shell
git log --show-signature
```
+
+## Signing tags
+
+After you have [associated your X.509 certificate with Git](#associating-your-x509-certificate-with-git) you
+can start signing your tags:
+
+1. Tag like you used to, the only difference is the addition of the `-s` flag:
+
+ ```shell
+ git tag -s v1.1.1 -m "My signed tag"
+ ```
+
+1. Push to GitLab and check that your tags [are verified](#verifying-tags).
+
+If you don't want to type the `-s` flag every time you tag, you can tell Git
+to sign your tags automatically:
+
+```shell
+git config --global tag.gpgsign true
+```
+
+## Verifying tags
+
+To verify that a tag is signed, you can use the `--verify` flag:
+
+```shell
+git tag --verify v1.1.1
+```
diff --git a/doc/user/project/requirements/index.md b/doc/user/project/requirements/index.md
index 8f4ec7bbbed..50343e52a68 100644
--- a/doc/user/project/requirements/index.md
+++ b/doc/user/project/requirements/index.md
@@ -10,6 +10,9 @@ Requirements allow you to create criteria to check your products against. They
can be based on users, stakeholders, system, software, or anything else you
find important to capture.
+<i class="fa fa-youtube-play youtube" aria-hidden="true"></i>
+For an overview, see [GitLab 12.10 Introduces Requirements Management](https://www.youtube.com/watch?v=uSS7oUNSEoU).
+
![requirements list view](img/requirements_list_view_v12_10.png)
## Create a requirement
diff --git a/doc/user/project/service_desk.md b/doc/user/project/service_desk.md
index f18a202c63b..d021f259015 100644
--- a/doc/user/project/service_desk.md
+++ b/doc/user/project/service_desk.md
@@ -1,4 +1,4 @@
-# Service Desk **(PREMIUM)**
+# Service Desk **(STARTER)**
> [Introduced](https://gitlab.com/gitlab-org/gitlab/issues/149) in [GitLab Premium 9.1](https://about.gitlab.com/releases/2017/04/22/gitlab-9-1-released/#service-desk-eep).
@@ -95,18 +95,18 @@ directory in your repository. Commit and push to your default branch.
The **Thank you email** is the email sent to a user after they submit an issue.
The file name of the template has to be `thank_you.md`.
-You can use `%{ISSUE_ID}` placeholder which will be replaced by an issue iid in the email and
-`%{ISSUE_PATH}` placeholder which will be replaced by project path and the issue iid.
+You can use `%{ISSUE_ID}` placeholder which will be replaced by an issue IID in the email and
+`%{ISSUE_PATH}` placeholder which will be replaced by project path and the issue IID.
As the service desk issues are created as confidential (only project members can see them)
-the response email doesn't provide the issue link.
+the response email does not provide the issue link.
#### New note email
The **New note email** is the email sent to a user when the issue they submitted has a new comment.
The file name of the template has to be `new_note.md`.
-You can use `%{ISSUE_ID}` placeholder which will be replaced by an issue iid
+You can use `%{ISSUE_ID}` placeholder which will be replaced by an issue IID
in the email, `%{ISSUE_PATH}` placeholder which will be replaced by
- project path and the issue iid and `%{NOTE_TEXT}` placeholder which will be replaced by the note text.
+ project path and the issue IID and `%{NOTE_TEXT}` placeholder which will be replaced by the note text.
### Using custom email display name
@@ -115,11 +115,67 @@ in the email, `%{ISSUE_PATH}` placeholder which will be replaced by
You can customize the email display name. Emails sent from Service Desk will have
this name in the `From` header. The default display name is `GitLab Support Bot`.
+### Using custom email address
+
+> [Introduced](https://gitlab.com/gitlab-org/gitlab/issues/2201) in [GitLab Premium](https://about.gitlab.com/pricing/) 13.0.
+
+NOTE: **Note:**
+This feature is disabled by default. For steps to enable it, see [Enable custom email address](#enable-custom-email-address).
+
+If the `service_desk_email` feature flag is enabled in your configuration,
+then it's possible to create Service Desk issues by sending emails to the
+custom Service Desk email address, which should have the following format:
+`project_contact+%{key}@example.com`.
+
+The `%{key}` part is used to find the project where the issue should be created. The
+`%{key}` part combines the path to the project and configurable project name suffix:
+`<project_full_path>-<project_name_suffix>`.
+
+You can set the project name suffix in your project's Service Desk settings.
+It can contain only lowercase letters (`a-z`), numbers (`0-9`), or underscores (`_`).
+
+![Setting custom Service Desk email address](img/service_desk_custom_email_address_v13_0.png)
+
+For example, suppose you add the following to your configuration:
+
+```yaml
+service_desk_email:
+ enabled: true
+ address: "project_contact+%{key}@example.com"
+ user: "project_support@example.com"
+ password: "[REDACTED]"
+ host: "imap.gmail.com"
+ port: 993
+ ssl: true
+ start_tls: false
+ log_path: "log/mailroom.log"
+ mailbox: "inbox"
+ idle_timeout: 60
+ expunge_deleted: true
+```
+
+In this case, suppose the `mygroup/myproject` project Service Desk settings has the project name
+suffix set to `support`, and a user sends an email to `project_contact+mygroup-myproject-support@example.com`.
+As a result, a new Service Desk issue is created from this email in the `mygroup/myproject` project.
+
+#### Enable custom email address
+
+This feature comes with the `service_desk_email` feature flag disabled by default.
+To turn on the feature, ask a GitLab administrator with Rails console access to run the following
+command:
+
+```ruby
+Feature.enable(service_desk_email)
+```
+
+The configuration options are the same as for configuring
+[incoming email](../../administration/incoming_email.md#set-it-up).
+
## Using Service Desk
### As an end user (issue creator)
-To create a Service Desk issue, an end user doesn't need to know anything about
+To create a Service Desk issue, an end user does not need to know anything about
the GitLab instance. They just send an email to the address they are given, and
receive an email back confirming receipt:
@@ -136,7 +192,7 @@ And any responses they send will be displayed in the issue itself.
### As a responder to the issue
-For responders to the issue, everything works as usual. They'll see a familiar looking
+For responders to the issue, everything works as usual. They will see a familiar looking
issue tracker, where they can see issues created via customer support requests and
filter and interact with them just like other GitLab issues.
diff --git a/doc/user/project/settings/import_export.md b/doc/user/project/settings/import_export.md
index 7f241a74820..e9521a0567e 100644
--- a/doc/user/project/settings/import_export.md
+++ b/doc/user/project/settings/import_export.md
@@ -12,6 +12,7 @@ See also:
- [Project import/export API](../../../api/project_import_export.md)
- [Project import/export administration Rake tasks](../../../administration/raketasks/project_import_export.md) **(CORE ONLY)**
+- [Group import/export](../../group/settings/import_export.md)
- [Group import/export API](../../../api/group_import_export.md)
To set up a project import/export:
@@ -24,6 +25,8 @@ To set up a project import/export:
Note the following:
+- Imports from a newer version of GitLab are not supported.
+ The Importing GitLab version must be greater than or equal to the Exporting GitLab version.
- Imports will fail unless the import and export GitLab instances are
compatible as described in the [Version history](#version-history).
- Exports are stored in a temporary [shared directory](../../../development/shared_files.md)
@@ -41,11 +44,24 @@ Note the following:
## Version history
-The following table lists updates to Import/Export:
+Starting with GitLab 13.0, GitLab can import bundles that were exported from a different GitLab deployment.
+This ability is limited to two previous GitLab [minor](../../../policy/maintenance.md#versioning)
+releases, which is similar to our process for [Security Releases](../../../policy/maintenance.md#security-releases).
+
+For example:
+
+| Current version | Can import bundles exported from |
+|-----------------|----------------------------------|
+| 13.0 | 13.0, 12.10, 12.9 |
+| 13.1 | 13.1, 13.0, 12.10 |
+
+### 12.x
+
+Prior to 13.0 this was a defined compatibility table:
| Exporting GitLab version | Importing GitLab version |
| -------------------------- | -------------------------- |
-| 11.7 to current | 11.7 to current |
+| 11.7 to 13.0 | 11.7 to 13.0 |
| 11.1 to 11.6 | 11.1 to 11.6 |
| 10.8 to 11.0 | 10.8 to 11.0 |
| 10.4 to 10.7 | 10.4 to 10.7 |
@@ -66,6 +82,13 @@ Projects can be exported and imported only between versions of GitLab with match
For example, 8.10.3 and 8.11 have the same Import/Export version (0.1.3)
and the exports between them will be compatible.
+## Between CE and EE
+
+You can export projects from the [Community Edition to the Enterprise Edition](https://about.gitlab.com/install/ce-or-ee/) and vice versa.
+This assumes [version history](#version-history) requirements are met.
+
+If you're exporting a project from the Enterprise Edition to the Community Edition, you may lose data that is retained only in the Enterprise Edition. For more information, see [downgrading from EE to CE](../../../README.md).
+
## Exported contents
The following items will be exported:
diff --git a/doc/user/project/settings/index.md b/doc/user/project/settings/index.md
index ce94a8f5521..0c98772237b 100644
--- a/doc/user/project/settings/index.md
+++ b/doc/user/project/settings/index.md
@@ -20,6 +20,16 @@ Adjust your project's name, description, avatar, [default branch](../repository/
The project description also partially supports [standard Markdown](../../markdown.md#standard-markdown-and-extensions-in-gitlab). You can use [emphasis](../../markdown.md#emphasis), [links](../../markdown.md#links), and [line-breaks](../../markdown.md#line-breaks) to add more context to the project description.
+#### Compliance framework **(ULTIMATE)**
+
+You can select a framework label to identify that your project has certain compliance requirements or needs additional oversight. Available labels include:
+
+- GDPR - General Data Protection Regulation
+- HIPAA - Health Insurance Portability and Accountability Act
+- PCI-DSS - Payment Card Industry-Data Security Standard
+- SOC 2 - Service Organization Control 2
+- SOX - Sarbanes-Oxley
+
### Sharing and permissions
For your repository, you can set up features such as public access, repository features,
@@ -46,18 +56,19 @@ Use the switches to enable or disable the following features:
| **Forks** | ✓ | Enables [forking](../index.md#fork-a-project) functionality |
| **Pipelines** | ✓ | Enables [CI/CD](../../../ci/README.md) functionality |
| **Container Registry** | | Activates a [registry](../../packages/container_registry/) for your docker images |
-| **Git Large File Storage** | | Enables the use of [large files](../../../topics/git/lfs/index.md#git-lfs) |
+| **Git Large File Storage** | | Enables the use of [large files](../../../topics/git/lfs/index.md#git-large-file-storage-lfs) |
| **Packages** | | Supports configuration of a [package registry](../../../administration/packages/index.md#gitlab-package-registry-administration-premium-only) functionality |
| **Wiki** | ✓ | Enables a separate system for [documentation](../wiki/) |
| **Snippets** | ✓ | Enables [sharing of code and text](../../snippets.md) |
| **Pages** | ✓ | Allows you to [publish static websites](../pages/) |
+| **Metrics Dashboard** | ✓ | Control access to [metrics dashboard](../integrations/prometheus.md)
Some features depend on others:
- If you disable the **Issues** option, GitLab also removes the following
features:
- **Issue Boards**
- - [**Service Desk**](#service-desk-premium) **(PREMIUM)**
+ - [**Service Desk**](#service-desk-starter) **(STARTER)**
NOTE: **Note:**
When the **Issues** option is disabled, you can still access **Milestones**
@@ -70,13 +81,15 @@ Some features depend on others:
- If you disable **Repository** functionality, GitLab also disables the following
features for your project:
-
- **Merge Requests**
- **Pipelines**
- **Container Registry**
- **Git Large File Storage**
- **Packages**
+- Metrics dashboard access requires reading both project environments and deployments.
+ Users with access to the metrics dashboard can also access environments and deployments.
+
#### Disabling email notifications
Project owners can disable all [email notifications](../../profile/notifications.md#gitlab-notification-emails)
@@ -96,7 +109,7 @@ Set up your project's merge request settings:
![project's merge request settings](img/merge_requests_settings.png)
-### Service Desk **(PREMIUM)**
+### Service Desk **(STARTER)**
Enable [Service Desk](../service_desk.md) for your project to offer customer support.
@@ -247,7 +260,7 @@ To do so:
1. Confirm the action by typing the project's path as instructed.
NOTE: **Note:**
-Only project maintainers have the [permissions](../../permissions.md#project-members-permissions)
+Only project owners have the [permissions](../../permissions.md#project-members-permissions)
to remove a fork relationship.
## Operations settings
diff --git a/doc/user/project/settings/project_access_tokens.md b/doc/user/project/settings/project_access_tokens.md
new file mode 100644
index 00000000000..303a6f6d3be
--- /dev/null
+++ b/doc/user/project/settings/project_access_tokens.md
@@ -0,0 +1,55 @@
+# Project access tokens **(CORE ONLY)**
+
+> [Introduced](https://gitlab.com/groups/gitlab-org/-/epics/2587) in GitLab 13.0.
+
+Project access tokens are scoped to a project and can be used to authenticate with the [GitLab API](../../../api/README.md#personalproject-access-tokens).
+
+You can also use project access tokens with Git to authenticate over HTTP or SSH.
+
+Project access tokens expire on the date you define, at midnight UTC.
+
+For examples of how you can use a project access token to authenticate with the API, see the following section from our [API Docs](../../../api/README.md#personalproject-access-tokens).
+
+## Creating a project access token
+
+1. Log in to GitLab.
+1. Navigate to the project you would like to create an access token for.
+1. In the **{settings}** **Settings** menu choose **Access Tokens**.
+1. Choose a name and optional expiry date for the token.
+1. Choose the [desired scopes](#limiting-scopes-of-a-project-access-token).
+1. Click the **Create project access token** button.
+1. Save the project access token somewhere safe. Once you leave or refresh
+ the page, you won't be able to access it again.
+
+## Project bot users
+
+For each project access token created, a bot user will also be created and added to the project with
+["Maintainer" level permissions](../../permissions.md#project-members-permissions). API calls made with a
+project access token will be associated to the corresponding bot user.
+
+These users will appear in **{settings}** **Settings > Members** but can not be modified.
+Furthermore, the bot user can not be added to any other project.
+
+When the project access token is [revoked](#revoking-a-project-access-token) the bot user will be deleted and all
+records will be moved to a system-wide user with the username "Ghost User". For more information,
+see [Associated Records](../../profile/account/delete_account.md#associated-records).
+
+## Revoking a project access token
+
+At any time, you can revoke any project access token by clicking the
+respective **Revoke** button in **{settings}** **Settings > Access Tokens**.
+
+## Limiting scopes of a project access token
+
+Project access tokens can be created with one or more scopes that allow various
+actions that a given token can perform. The available scopes are depicted in
+the following table.
+
+| Scope | Description |
+| ------------------ | ----------- |
+| `api` | Grants complete read/write access to the scoped project API. |
+| `read_api` | Grants read access to the scoped project API. |
+| `read_registry` | Allows read-access (pull) to [container registry](../../packages/container_registry/index.md) images if a project is private and authorization is required. |
+| `write_registry` | Allows write-access (push) to [container registry](../../packages/container_registry/index.md). |
+| `read_repository` | Allows read-only access (pull) to the repository. |
+| `write_repository` | Allows read-write access (pull, push) to the repository. |
diff --git a/doc/user/project/static_site_editor/img/static_site_editor_v12_10.png b/doc/user/project/static_site_editor/img/static_site_editor_v12_10.png
deleted file mode 100644
index 130dff9f30d..00000000000
--- a/doc/user/project/static_site_editor/img/static_site_editor_v12_10.png
+++ /dev/null
Binary files differ
diff --git a/doc/user/project/static_site_editor/img/wysiwyg_editor_v13_0.png b/doc/user/project/static_site_editor/img/wysiwyg_editor_v13_0.png
new file mode 100644
index 00000000000..a2f5248bf39
--- /dev/null
+++ b/doc/user/project/static_site_editor/img/wysiwyg_editor_v13_0.png
Binary files differ
diff --git a/doc/user/project/static_site_editor/index.md b/doc/user/project/static_site_editor/index.md
index f186ff9919e..e2e498b605a 100644
--- a/doc/user/project/static_site_editor/index.md
+++ b/doc/user/project/static_site_editor/index.md
@@ -5,7 +5,14 @@ description: "The static site editor enables users to edit content on static web
# Static Site Editor
-> [Introduced](https://gitlab.com/gitlab-org/gitlab/-/merge_requests/28758) in GitLab 12.10.
+> - [Introduced](https://gitlab.com/gitlab-org/gitlab/-/merge_requests/28758) in GitLab 12.10.
+> - WYSIWYG editor [introduced](https://gitlab.com/gitlab-org/gitlab/-/issues/214559) in GitLab 13.0.
+
+DANGER: **Danger:**
+In GitLab 13.0, we [introduced breaking changes](https://gitlab.com/gitlab-org/gitlab/-/issues/213282)
+to the URL structure of the Static Site Editor. Follow the instructions in this
+[snippet](https://gitlab.com/gitlab-org/project-templates/static-site-editor-middleman/snippets/1976539)
+to update your project with the latest changes.
Static Site Editor enables users to edit content on static websites without
prior knowledge of the underlying templating language, site architecture, or
@@ -44,7 +51,7 @@ When clicking it, GitLab will open up an editor window from which the content
can be directly edited. When you're ready, you can submit your changes in a
click of a button:
-![Static Site Editor](img/static_site_editor_v12_10.png)
+![Static Site Editor](img/wysiwyg_editor_v13_0.png)
When an editor submits their changes, in the background, GitLab automatically
creates a new branch, commits their changes, and opens a merge request. The
@@ -79,7 +86,8 @@ company and a new feature has been added to the company product.
1. You are assigned the task of updating the documentation.
1. You visit a page and see content that needs to be edited.
1. Click the **Edit this page** button on the production site.
-1. The file is opened in the Static Site Editor.
+1. The file is opened in the Static Site Editor in **WYSIWYG** mode. If you wish to edit the raw Markdown
+ instead, you can toggle the **Markdown** mode in the bottom-right corner.
1. You edit the file right there and click **Submit changes**.
1. A new merge request is automatically created and you assign it to your colleague for review.
diff --git a/doc/user/project/status_page/index.md b/doc/user/project/status_page/index.md
index d292ca94ba9..8ebfb638894 100644
--- a/doc/user/project/status_page/index.md
+++ b/doc/user/project/status_page/index.md
@@ -1,6 +1,12 @@
-# GitLab Status Page
+---
+stage: Monitor
+group: Health
+info: To determine the technical writer assigned to the Stage/Group associated with this page, see https://about.gitlab.com/handbook/engineering/ux/technical-writing/#designated-technical-writers
+---
-> [Introduced](https://gitlab.com/groups/gitlab-org/-/epics/2479) in GitLab 12.10.
+# GitLab Status Page **(ULTIMATE)**
+
+> [Introduced](https://gitlab.com/groups/gitlab-org/-/epics/2479) in [GitLab Ultimate](https://about.gitlab.com/pricing/) 12.10.
GitLab Status Page allows you to create and deploy a static website to communicate efficiently to users during an incident.
@@ -35,15 +41,15 @@ To use GitLab Status Page you first need to set up your account details for your
### Status Page project
-To deploy the status page to AWS S3 you need to add the Status Page project & configure the necessary CI variables.
+To deploy the Status Page to AWS S3 you need to add the Status Page project & configure the necessary CI variables.
1. Fork the [Status Page](https://gitlab.com/gitlab-org/status-page) project. This can also be done via [Repository Mirroring](https://gitlab.com/gitlab-org/status-page#repository-mirroring) which will ensure you get the up-to-date Status Page features.
1. Add the following variables in **Settings > CI/CD > Variables**. (To get these variables from Amazon, use your Amazon Console):
- - `S3_BUCKET_NAME` - name of the Amazon S3 bucket
+ - `S3_BUCKET_NAME` - name of the Amazon S3 bucket (If a bucket with the provided name doesn't exist, the first pipeline run will create one and configure it for [static website hosting](https://docs.aws.amazon.com/AmazonS3/latest/dev/HostingWebsiteOnS3Setup.html))
- `AWS_DEFAULT_REGION` - the AWS region
- `AWS_ACCESS_KEY_ID` - the AWS access key ID
- `AWS_SECRET_ACCESS_KEY` - the AWS secret
-1. Run the pipeline to deploy the status page to S3.
+1. Run the pipeline to deploy the Status Page to S3.
### Syncing incidents to the Status Page
@@ -55,7 +61,7 @@ Once the CI/CD variables are set, you'll need to set up the Project you want to
## Status Page UI
-The Status page landing page shows you an overview of the recent incidents. Clicking on an incident will take you to the incident's detail page.
+The Status Page landing page shows you an overview of the recent incidents. Clicking on an incident will take you to the incident's detail page.
![Status Page landing page](../img/status_page_incidents_v12_10.png)
@@ -64,8 +70,8 @@ The Status page landing page shows you an overview of the recent incidents. Clic
The incident detail page shows detailed information about a particular incident. For example:
- Status on the incident, including when the incident was last updated.
-- The incident title.
-- The description of the incident.
+- The incident title, including any emojis.
+- The description of the incident, including emojis and static images.
- A chronological ordered list of updates to the incident.
![Status Page detail](../img/status_page_detail_v12_10.png)
@@ -76,7 +82,9 @@ The incident detail page shows detailed information about a particular incident.
To publish an Incident, you first need to create an issue in the Project you enabled the Status Page settings in.
-Once this issue is created, a background worker will publish the issue onto the status page using the credentials you provided during setup.
+Once this issue is created, a background worker will publish the issue onto the Status Page using the credentials you provided during setup.
+Since all incidents are published publicly, user and group mentions are anonymized with `Incident Responder`,
+and titles of non-public [GitLab references](../../markdown.md#special-gitlab-references) are removed.
NOTE: **Note:**
Confidential issues are not published. If a published issue is made confidential it will be unpublished.
@@ -99,4 +107,4 @@ Anyone with access to view the Issue can add an Emoji Award to a comment, so you
### Changing the Incident status
-To change the incident status from `open` to `closed`, close the incident issue within GitLab. This will then be updated shortly on the Status page website.
+To change the incident status from `open` to `closed`, close the incident issue within GitLab. This will then be updated shortly on the Status Page website.
diff --git a/doc/user/project/web_ide/img/admin_clientside_evaluation.png b/doc/user/project/web_ide/img/admin_clientside_evaluation.png
deleted file mode 100644
index a930490398b..00000000000
--- a/doc/user/project/web_ide/img/admin_clientside_evaluation.png
+++ /dev/null
Binary files differ
diff --git a/doc/user/project/web_ide/img/admin_live_preview_v13_0.png b/doc/user/project/web_ide/img/admin_live_preview_v13_0.png
new file mode 100644
index 00000000000..90129d240bc
--- /dev/null
+++ b/doc/user/project/web_ide/img/admin_live_preview_v13_0.png
Binary files differ
diff --git a/doc/user/project/web_ide/img/dark_theme_v13.0.png b/doc/user/project/web_ide/img/dark_theme_v13.0.png
new file mode 100644
index 00000000000..6034bc52c05
--- /dev/null
+++ b/doc/user/project/web_ide/img/dark_theme_v13.0.png
Binary files differ
diff --git a/doc/user/project/web_ide/img/clientside_evaluation.png b/doc/user/project/web_ide/img/live_preview_v13_0.png
index bd04d3d644b..bd04d3d644b 100644
--- a/doc/user/project/web_ide/img/clientside_evaluation.png
+++ b/doc/user/project/web_ide/img/live_preview_v13_0.png
Binary files differ
diff --git a/doc/user/project/web_ide/img/solarized_light_theme_v13.0.png b/doc/user/project/web_ide/img/solarized_light_theme_v13.0.png
new file mode 100644
index 00000000000..f3c4aa142a4
--- /dev/null
+++ b/doc/user/project/web_ide/img/solarized_light_theme_v13.0.png
Binary files differ
diff --git a/doc/user/project/web_ide/index.md b/doc/user/project/web_ide/index.md
index c98448ff904..d4daca0e1e4 100644
--- a/doc/user/project/web_ide/index.md
+++ b/doc/user/project/web_ide/index.md
@@ -1,6 +1,6 @@
# Web IDE
-> - [Introduced](https://gitlab.com/gitlab-org/gitlab/issues/4539) in [GitLab Ultimate][ee] 10.4.
+> - [Introduced](https://gitlab.com/gitlab-org/gitlab/issues/4539) in [GitLab Ultimate](https://about.gitlab.com/pricing/) 10.4.
> - [Brought to GitLab Core](https://gitlab.com/gitlab-org/gitlab-foss/issues/44157) in 10.7.
The Web IDE editor makes it faster and easier to contribute changes to your
@@ -15,7 +15,7 @@ and from merge requests.
## File finder
-> [Introduced](https://gitlab.com/gitlab-org/gitlab-foss/-/merge_requests/18323) in [GitLab Core][ce] 10.8.
+> [Introduced](https://gitlab.com/gitlab-org/gitlab-foss/-/merge_requests/18323) in [GitLab Core](https://about.gitlab.com/pricing/) 10.8.
The file finder allows you to quickly open files in the current branch by
searching. The file finder is launched using the keyboard shortcut `Command-p`,
@@ -43,6 +43,20 @@ you can find a more complete list of supported languages in the
NOTE: **Note:**
Single file editing is based on the [Ace Editor](https://ace.c9.io).
+### Themes
+
+> [Introduced](https://gitlab.com/groups/gitlab-org/-/epics/2389) in GitLab 13.0.
+
+All the themes GitLab supports for syntax highlighting are added to the Web IDE's code editor.
+You can pick a theme from your [profile preferences](../../profile/preferences.md).
+
+The themes are available only in the Web IDE file editor, except for the [dark theme](https://gitlab.com/gitlab-org/gitlab/-/issues/209808),
+which applies to the entire Web IDE screen.
+
+| Solarized Light Theme | Dark Theme |
+|---------------------------------------------------------------|-----------------------------------------|
+| ![Solarized Light Theme](img/solarized_light_theme_v13.0.png) | ![Dark Theme](img/dark_theme_v13.0.png) |
+
## Commit changes
> - [Introduced](https://gitlab.com/gitlab-org/gitlab/issues/4539) in [GitLab Ultimate](https://about.gitlab.com/pricing/) 10.4 and [brought to GitLab Core](https://gitlab.com/gitlab-org/gitlab-foss/issues/44157) in 10.7.
@@ -73,7 +87,7 @@ shows you a preview of the merge request diff if you commit your changes.
## View CI job logs
-> [Introduced](https://gitlab.com/gitlab-org/gitlab-foss/-/merge_requests/19279) in [GitLab Core][ce] 11.0.
+> [Introduced](https://gitlab.com/gitlab-org/gitlab-foss/-/merge_requests/19279) in [GitLab Core](https://about.gitlab.com/pricing/) 11.0.
You can use the Web IDE to quickly fix failing tests by opening
the branch or merge request in the Web IDE and opening the logs of the failed
@@ -86,7 +100,7 @@ left.
## Switching merge requests
-> [Introduced](https://gitlab.com/gitlab-org/gitlab-foss/-/merge_requests/19318) in [GitLab Core][ce] 11.0.
+> [Introduced](https://gitlab.com/gitlab-org/gitlab-foss/-/merge_requests/19318) in [GitLab Core](https://about.gitlab.com/pricing/) 11.0.
To switch between your authored and assigned merge requests, click the
dropdown in the top of the sidebar to open a list of merge requests. You will
@@ -95,34 +109,35 @@ request.
## Switching branches
-> [Introduced](https://gitlab.com/gitlab-org/gitlab-foss/-/merge_requests/20850) in [GitLab Core][ce] 11.2.
+> [Introduced](https://gitlab.com/gitlab-org/gitlab-foss/-/merge_requests/20850) in [GitLab Core](https://about.gitlab.com/pricing/) 11.2.
To switch between branches of the current project repository, click the dropdown
in the top of the sidebar to open a list of branches.
You will need to commit or discard all your changes before switching to a
different branch.
-## Client Side Evaluation
+## Live Preview
-> [Introduced](https://gitlab.com/gitlab-org/gitlab-foss/-/merge_requests/19764) in [GitLab Core][ce] 11.2.
+> - [Introduced](https://gitlab.com/gitlab-org/gitlab-foss/-/merge_requests/19764) in [GitLab Core](https://about.gitlab.com/pricing/) 11.2.
+> - [Renamed](https://gitlab.com/gitlab-org/gitlab/-/issues/213853) from _Client Side Evaluation_ to _Live Preview_ in GitLab 13.0.
You can use the Web IDE to preview JavaScript projects right in the browser.
This feature uses CodeSandbox to compile and bundle the JavaScript used to
preview the web application.
-![Web IDE Client Side Evaluation](img/clientside_evaluation.png)
+![Web IDE Live Preview](img/live_preview_v13_0.png)
Additionally, for public projects an **Open in CodeSandbox** button is available
to transfer the contents of the project into a public CodeSandbox project to
quickly share your project with others.
-### Enabling Client Side Evaluation
+### Enabling Live Preview
-The Client Side Evaluation feature needs to be enabled in the GitLab instances
-admin settings. Client Side Evaluation is enabled for all projects on
+The Live Preview feature needs to be enabled in the GitLab instances
+admin settings. Live Preview is enabled for all projects on
GitLab.com
-![Admin Client Side Evaluation setting](img/admin_clientside_evaluation.png)
+![Admin Live Preview setting](img/admin_live_preview_v13_0.png)
Once you have done that, you can preview projects with a `package.json` file and
a `main` entry point inside the Web IDE. An example `package.json` is shown
@@ -302,6 +317,3 @@ active terminal at a time.
- If the terminal displays **Connection Failure**, then the terminal could not
connect to the runner. Please try to stop and restart the terminal. If the
problem persists, double check your runner configuration.
-
-[ce]: https://about.gitlab.com/pricing/
-[ee]: https://about.gitlab.com/pricing/
diff --git a/doc/user/project/wiki/index.md b/doc/user/project/wiki/index.md
index 88b729272ec..fa3ad4536ef 100644
--- a/doc/user/project/wiki/index.md
+++ b/doc/user/project/wiki/index.md
@@ -126,7 +126,7 @@ found. The list is ordered alphabetically.
![Wiki sidebar](img/wiki_sidebar.png)
If you have many pages, not all will be listed in the sidebar. Click on
-**More pages** to see all of them.
+**View All Pages** to see all of them.
## Viewing the history of a wiki page
@@ -189,7 +189,24 @@ instructions.
On the project's Wiki page, there is a right side navigation that renders the full Wiki pages list by default, with hierarchy.
-If the Wiki repository contains a `_sidebar` page, the file of this page replaces the default side navigation.
-This custom file serves to render it's custom content, fully replacing the standard sidebar.
+To customize the sidebar, you can create a file named `_sidebar` to fully replace the default navigation.
+
+CAUTION: **Warning:**
+Unless you link the `_sidebar` file from your custom nav, to edit it you'll have to access it directly
+from the browser's address bar by typing: `https://gitlab.com/<namespace>/<project_name>/-/wikis/_sidebar` (for self-managed GitLab instances, replace `gitlab.com` with your instance's URL).
+
+Example for `_sidebar` (using Markdown format):
+
+```markdown
+### [Home](home)
+
+- [Hello World](hello)
+- [Foo](foo)
+- [Bar](bar)
+
+---
+
+- [Sidebar](_sidebar)
+```
Support for displaying a generated TOC with a custom side navigation is planned.
diff --git a/doc/user/search/advanced_global_search.md b/doc/user/search/advanced_global_search.md
index f38cf8f139e..fac1702f2ac 100644
--- a/doc/user/search/advanced_global_search.md
+++ b/doc/user/search/advanced_global_search.md
@@ -1,15 +1,17 @@
# Advanced Global Search **(STARTER ONLY)**
> - [Introduced](https://gitlab.com/gitlab-org/gitlab/-/merge_requests/109) in GitLab [Starter](https://about.gitlab.com/pricing/) 8.4.
-> - This is the user documentation. To install and configure Elasticsearch,
-> visit the [administrator documentation](../../integration/elasticsearch.md).
NOTE: **Note**
-Advanced Global Search (powered by Elasticsearch) is not yet available on GitLab.com. We are working on adding it. [Follow this epic for the latest updates](https://gitlab.com/groups/gitlab-org/-/epics/153).
+Advanced Global Search (powered by Elasticsearch) is not yet available on GitLab.com. We are working on adding it.
+[Follow this epic for the latest updates](https://gitlab.com/groups/gitlab-org/-/epics/153).
Leverage Elasticsearch for faster, more advanced code search across your entire
GitLab instance.
+This is the user documentation. To install and configure Elasticsearch,
+visit the [administrator documentation](../../integration/elasticsearch.md).
+
## Overview
The Advanced Global Search in GitLab is a powerful search service that saves
diff --git a/doc/user/search/advanced_search_syntax.md b/doc/user/search/advanced_search_syntax.md
index ebb957ad8e2..5113578af9e 100644
--- a/doc/user/search/advanced_search_syntax.md
+++ b/doc/user/search/advanced_search_syntax.md
@@ -3,14 +3,16 @@
> **Notes:**
>
> - Introduced in [GitLab Enterprise Starter](https://about.gitlab.com/pricing/) 9.2
-> - This is the user documentation. To install and configure Elasticsearch,
-> visit the [administrator documentation](../../integration/elasticsearch.md).
NOTE: **Note**
-Advanced Global Search (powered by Elasticsearch) is not yet available on GitLab.com. We are working on adding it. [Follow this epic for the latest updates](https://gitlab.com/groups/gitlab-org/-/epics/153).
+Advanced Global Search (powered by Elasticsearch) is not yet available on GitLab.com. We are working on adding it.
+[Follow this epic for the latest updates](https://gitlab.com/groups/gitlab-org/-/epics/153).
Use advanced queries for more targeted search results.
+This is the user documentation. To install and configure Elasticsearch,
+visit the [administrator documentation](../../integration/elasticsearch.md).
+
## Overview
The Advanced Syntax Search is a subset of the
diff --git a/doc/user/search/img/filter_approved_by_merge_requests_v13_0.png b/doc/user/search/img/filter_approved_by_merge_requests_v13_0.png
new file mode 100644
index 00000000000..ef4c65aea4b
--- /dev/null
+++ b/doc/user/search/img/filter_approved_by_merge_requests_v13_0.png
Binary files differ
diff --git a/doc/user/search/index.md b/doc/user/search/index.md
index ed2a4b36372..f5efa3519d9 100644
--- a/doc/user/search/index.md
+++ b/doc/user/search/index.md
@@ -41,7 +41,10 @@ groups:
- [Label](../project/labels.md)
- My-reaction
- Confidential
- - Epic ([Introduced](https://gitlab.com/gitlab-org/gitlab/issues/195704) in GitLab 12.9)
+ - Epic ([introduced](https://gitlab.com/gitlab-org/gitlab/-/issues/195704) in GitLab 12.9),
+ including [child epic](../group/epics/index.md#multi-level-child-epics-ultimate)
+ ([introduced](https://gitlab.com/gitlab-org/gitlab/-/issues/9029) in
+ [GitLab Ultimate](https://about.gitlab.com/pricing/) 13.0)
- Search for this text
1. Select or type the operator to use for filtering the attribute. The following operators are
available:
@@ -94,10 +97,19 @@ You can filter the **Issues** list to individual instances by their ID. For exam
> [Introduced](https://gitlab.com/gitlab-org/gitlab/-/merge_requests/9468) in [GitLab Starter](https://about.gitlab.com/pricing/) 11.9.
To filter merge requests by an individual approver, you can type (or select from
-the dropdown) `approver` and select the user.
+the dropdown) **Approver** and select the user.
![Filter MRs by an approver](img/filter_approver_merge_requests.png)
+### Filtering merge requests by "approved by" **(STARTER)**
+
+> [Introduced](https://gitlab.com/gitlab-org/gitlab/-/merge_requests/30335) in [GitLab Starter](https://about.gitlab.com/pricing/) 13.0.
+
+To filter merge requests already approved by a specific individual, you can type (or select from
+the dropdown) **Approved-By** and select the user.
+
+![Filter MRs by approved by](img/filter_approved_by_merge_requests_v13_0.png)
+
## Search history
You can view recent searches by clicking on the little arrow-clock icon, which is to the left of the search input. Click the search entry to run that search again. This feature is available for issues and merge requests. Searches are stored locally in your browser.
diff --git a/doc/user/snippets.md b/doc/user/snippets.md
index 0372bb42038..96c8dba11e5 100644
--- a/doc/user/snippets.md
+++ b/doc/user/snippets.md
@@ -2,7 +2,7 @@
With GitLab Snippets you can store and share bits of code and text with other users.
-![GitLab Snippet](img/gitlab_snippet.png)
+![GitLab Snippet](img/gitlab_snippet_v13_0.png)
Snippets can be maintained using [snippets API](../api/snippets.md).
@@ -49,6 +49,67 @@ CAUTION: **Warning:**
Make sure to add the file name to get code highlighting and to avoid this
[copy-pasting bug](https://gitlab.com/gitlab-org/gitlab/-/issues/22870).
+## Versioned Snippets
+
+> [Introduced](https://gitlab.com/groups/gitlab-org/-/epics/239) in GitLab 13.0.
+
+Starting in 13.0, snippets (both personal and project snippets)
+have version control enabled by default.
+
+This means that all snippets get their own underlying repository initialized with
+a `master` branch at the moment the snippet is created. Whenever a change to the snippet is saved, a
+new commit to the master branch is recorded. Commit messages are automatically
+generated. The snippet's repository has only one branch (master) by default, deleting
+it or creating other branches is not supported.
+
+Existing snippets will be automatically migrated in 13.0. Their current
+content will be saved as the initial commit to the snippets' repository.
+
+### File names
+
+Snippets support syntax highlighting based on the filename and
+extension provided for them. While it is possible to submit a snippet
+without specifying a filename and extension, it needs a valid name so the
+content can be created as a file in the snippet's repository.
+
+In case the user does not attribute a filename and extension to a snippet,
+GitLab automatically adds a filename in the format `snippetfile<x>.txt`
+where `<x>` represents a number added to the file, starting with 1. This
+number increases incrementally when more snippets without an attributed
+filename are added.
+
+When upgrading from an earlier version of GitLab to 13.0, existing snippets
+without a supported filename will be renamed to a compatible format. For
+example, if the snippet's filename is `http://a-weird-filename.me` it will
+be changed to `http-a-weird-filename-me` to be included in the snippet's
+repository. As snippets are stored by ID, changing their filenames will not break
+direct or embedded links to the snippet.
+
+### Cloning snippets
+
+Snippets can be cloned as a regular Git repository using SSH or HTTPS. Click the **Clone**
+button above the snippet content to copy the URL of your choice.
+
+![Clone Snippet](img/snippet_clone_button_v13_0.png)
+
+This allows you to have a local copy of the snippet's repository and make
+changes as needed. You can commit those changes and push them to the remote
+master branch.
+
+### Limitations
+
+- Binary files are not supported.
+- Creating or deleting branches is not supported. Only a default *master*.
+branch is used.
+- Git tags are not supported in snippet repositories.
+- Snippets' repositories are limited to one file. Attempting to push more
+than one file will result in an error.
+- Revisions are not *yet* visible to the user on the GitLab UI, but
+it's planned to be added in future iterations. See the [revisions tab issue](https://gitlab.com/gitlab-org/gitlab/-/issues/39271)
+for updates.
+- The [maximum size for a snippet](../administration/snippets/index.md#snippets-content-size-limit)
+is 50 MB, by default.
+
## Discover snippets
There are two main ways of how you can discover snippets in GitLab.
@@ -87,7 +148,7 @@ snippet was created using the GitLab web interface the original line ending is W
> Introduced in GitLab 10.8.
Public snippets can not only be shared, but also embedded on any website. This
-allows to reuse a GitLab snippet in multiple places and any change to the source
+allows us to reuse a GitLab snippet in multiple places and any change to the source
is automatically reflected in the embedded snippet.
To embed a snippet, first make sure that:
@@ -111,6 +172,6 @@ Here's how an embedded snippet looks like:
<script src="https://gitlab.com/gitlab-org/gitlab-foss/snippets/1717978.js"></script>
-Embedded snippets are displayed with a header that shows the file name if defined,
+Embedded snippets are displayed with a header that shows the file name is defined,
the snippet size, a link to GitLab, and the actual snippet content. Actions in
the header allow users to see the snippet in raw format and download it.
diff --git a/doc/user/todos.md b/doc/user/todos.md
index d19f1b8f14b..84a7b6afdac 100644
--- a/doc/user/todos.md
+++ b/doc/user/todos.md
@@ -38,7 +38,7 @@ A To Do appears on your To-Do List when:
- Epic **(ULTIMATE)**
- You are `@mentioned` in a comment on a:
- Commit
- - Design **(PREMIUM)**
+ - Design
- A job in the CI pipeline running for your merge request failed, but this
job is not allowed to fail
- An open merge request becomes unmergeable due to conflict, and you are either:
@@ -137,7 +137,7 @@ There are four kinds of filters you can use on your To-Do List.
| Project | Filter by project |
| Group | Filter by group |
| Author | Filter by the author that triggered the To Do |
-| Type | Filter by issue, merge request, or epic **(ULTIMATE)** |
+| Type | Filter by issue, merge request, design, or epic **(ULTIMATE)** |
| Action | Filter by the action that triggered the To Do |
You can also filter by more than one of these at the same time. The possible Actions are
diff --git a/haml_lint/linter/no_plain_nodes.rb b/haml_lint/linter/no_plain_nodes.rb
index c39f61fa80d..d0d9c165a19 100644
--- a/haml_lint/linter/no_plain_nodes.rb
+++ b/haml_lint/linter/no_plain_nodes.rb
@@ -51,6 +51,7 @@ module HamlLint
attributes = node.attributes_source.map(&:last)
attributes.each { |attribute| text = text.gsub(attribute, '') }
+ text = strip_html_entities(text)
text.strip
end
@@ -77,7 +78,12 @@ module HamlLint
def text_node?(node)
return false unless plain_node?(node)
- !node.text.empty?
+ text = strip_html_entities(node.text)
+ !text.empty?
+ end
+
+ def strip_html_entities(text)
+ text.gsub(/&([a-z0-9]+|#[0-9]{1,6}|#x[0-9a-f]{1,6});/i, "")
end
end
end
diff --git a/jest.config.base.js b/jest.config.base.js
new file mode 100644
index 00000000000..1a1fd4e7b62
--- /dev/null
+++ b/jest.config.base.js
@@ -0,0 +1,92 @@
+const IS_EE = require('./config/helpers/is_ee_env');
+const isESLint = require('./config/helpers/is_eslint');
+
+module.exports = path => {
+ const reporters = ['default'];
+
+ // To have consistent date time parsing both in local and CI environments we set
+ // the timezone of the Node process. https://gitlab.com/gitlab-org/gitlab-foss/merge_requests/27738
+ process.env.TZ = 'GMT';
+
+ if (process.env.CI) {
+ reporters.push([
+ 'jest-junit',
+ {
+ output: './junit_jest.xml',
+ },
+ ]);
+ }
+
+ const glob = `${path}/**/*_spec.js`;
+ let testMatch = [`<rootDir>/${glob}`];
+ if (IS_EE) {
+ testMatch.push(`<rootDir>/ee/${glob}`);
+ }
+
+ // workaround for eslint-import-resolver-jest only resolving in test files
+ // see https://github.com/JoinColony/eslint-import-resolver-jest#note
+ if (isESLint(module)) {
+ testMatch = testMatch.map(path => path.replace('_spec.js', ''));
+ }
+
+ const moduleNameMapper = {
+ '^~(/.*)$': '<rootDir>/app/assets/javascripts$1',
+ '^ee_component(/.*)$':
+ '<rootDir>/app/assets/javascripts/vue_shared/components/empty_component.js',
+ '^ee_else_ce(/.*)$': '<rootDir>/app/assets/javascripts$1',
+ '^helpers(/.*)$': '<rootDir>/spec/frontend/helpers$1',
+ '^vendor(/.*)$': '<rootDir>/vendor/assets/javascripts$1',
+ '\\.(jpg|jpeg|png|svg|css)$': '<rootDir>/spec/frontend/__mocks__/file_mock.js',
+ 'emojis(/.*).json': '<rootDir>/fixtures/emojis$1.json',
+ '^spec/test_constants$': '<rootDir>/spec/frontend/helpers/test_constants',
+ '^jest/(.*)$': '<rootDir>/spec/frontend/$1',
+ };
+
+ const collectCoverageFrom = ['<rootDir>/app/assets/javascripts/**/*.{js,vue}'];
+
+ if (IS_EE) {
+ const rootDirEE = '<rootDir>/ee/app/assets/javascripts$1';
+ Object.assign(moduleNameMapper, {
+ '^ee(/.*)$': rootDirEE,
+ '^ee_component(/.*)$': rootDirEE,
+ '^ee_else_ce(/.*)$': rootDirEE,
+ '^ee_jest/(.*)$': '<rootDir>/ee/spec/frontend/$1',
+ });
+
+ collectCoverageFrom.push(rootDirEE.replace('$1', '/**/*.{js,vue}'));
+ }
+
+ const coverageDirectory = () => {
+ if (process.env.CI_NODE_INDEX && process.env.CI_NODE_TOTAL) {
+ return `<rootDir>/coverage-frontend/jest-${process.env.CI_NODE_INDEX}-${process.env.CI_NODE_TOTAL}`;
+ }
+
+ return '<rootDir>/coverage-frontend/';
+ };
+
+ return {
+ clearMocks: true,
+ testMatch,
+ moduleFileExtensions: ['js', 'json', 'vue'],
+ moduleNameMapper,
+ collectCoverageFrom,
+ coverageDirectory: coverageDirectory(),
+ coverageReporters: ['json', 'lcov', 'text-summary', 'clover'],
+ cacheDirectory: '<rootDir>/tmp/cache/jest',
+ modulePathIgnorePatterns: ['<rootDir>/.yarn-cache/'],
+ reporters,
+ setupFilesAfterEnv: ['<rootDir>/spec/frontend/test_setup.js', 'jest-canvas-mock'],
+ restoreMocks: true,
+ transform: {
+ '^.+\\.(gql|graphql)$': 'jest-transform-graphql',
+ '^.+\\.js$': 'babel-jest',
+ '^.+\\.vue$': 'vue-jest',
+ },
+ transformIgnorePatterns: ['node_modules/(?!(@gitlab/ui|bootstrap-vue|three|monaco-editor)/)'],
+ timers: 'fake',
+ testEnvironment: '<rootDir>/spec/frontend/environment.js',
+ testEnvironmentOptions: {
+ IS_EE,
+ },
+ };
+};
diff --git a/jest.config.integration.js b/jest.config.integration.js
new file mode 100644
index 00000000000..573002c1a34
--- /dev/null
+++ b/jest.config.integration.js
@@ -0,0 +1,5 @@
+const baseConfig = require('./jest.config.base');
+
+module.exports = {
+ ...baseConfig('spec/frontend_integration'),
+};
diff --git a/jest.config.js b/jest.config.js
deleted file mode 100644
index e9e1a598608..00000000000
--- a/jest.config.js
+++ /dev/null
@@ -1,103 +0,0 @@
-const IS_EE = require('./config/helpers/is_ee_env');
-
-const reporters = ['default'];
-
-// To have consistent date time parsing both in local and CI environments we set
-// the timezone of the Node process. https://gitlab.com/gitlab-org/gitlab-foss/merge_requests/27738
-process.env.TZ = 'GMT';
-
-if (process.env.CI) {
- reporters.push([
- 'jest-junit',
- {
- output: './junit_jest.xml',
- },
- ]);
-}
-
-let testMatch = ['<rootDir>/spec/frontend/**/*_spec.js'];
-if (IS_EE) {
- testMatch.push('<rootDir>/ee/spec/frontend/**/*_spec.js');
-}
-
-// workaround for eslint-import-resolver-jest only resolving in test files
-// see https://github.com/JoinColony/eslint-import-resolver-jest#note
-const { filename: parentModuleName } = module.parent;
-const isESLint = parentModuleName && parentModuleName.includes('/eslint-import-resolver-jest/');
-if (isESLint) {
- testMatch = testMatch.map(path => path.replace('_spec.js', ''));
-}
-
-const moduleNameMapper = {
- '^~(/.*)$': '<rootDir>/app/assets/javascripts$1',
- '^ee_component(/.*)$':
- '<rootDir>/app/assets/javascripts/vue_shared/components/empty_component.js',
- '^ee_else_ce(/.*)$': '<rootDir>/app/assets/javascripts$1',
- '^helpers(/.*)$': '<rootDir>/spec/frontend/helpers$1',
- '^vendor(/.*)$': '<rootDir>/vendor/assets/javascripts$1',
- '\\.(jpg|jpeg|png|svg|css)$': '<rootDir>/spec/frontend/__mocks__/file_mock.js',
- 'emojis(/.*).json': '<rootDir>/fixtures/emojis$1.json',
- '^spec/test_constants$': '<rootDir>/spec/frontend/helpers/test_constants',
- '^jest/(.*)$': '<rootDir>/spec/frontend/$1',
-};
-
-const collectCoverageFrom = ['<rootDir>/app/assets/javascripts/**/*.{js,vue}'];
-
-if (IS_EE) {
- const rootDirEE = '<rootDir>/ee/app/assets/javascripts$1';
- Object.assign(moduleNameMapper, {
- '^ee(/.*)$': rootDirEE,
- '^ee_component(/.*)$': rootDirEE,
- '^ee_else_ce(/.*)$': rootDirEE,
- '^ee_jest/(.*)$': '<rootDir>/ee/spec/frontend/$1',
- });
-
- collectCoverageFrom.push(rootDirEE.replace('$1', '/**/*.{js,vue}'));
-}
-
-const coverageDirectory = () => {
- if (process.env.CI_NODE_INDEX && process.env.CI_NODE_TOTAL) {
- return `<rootDir>/coverage-frontend/jest-${process.env.CI_NODE_INDEX}-${process.env.CI_NODE_TOTAL}`;
- }
-
- return '<rootDir>/coverage-frontend/';
-};
-
-// eslint-disable-next-line import/no-commonjs
-module.exports = {
- clearMocks: true,
- testMatch,
- moduleFileExtensions: ['js', 'json', 'vue'],
- moduleNameMapper,
- collectCoverageFrom,
- coverageDirectory: coverageDirectory(),
- coverageReporters: ['json', 'lcov', 'text-summary', 'clover'],
- cacheDirectory: '<rootDir>/tmp/cache/jest',
- modulePathIgnorePatterns: ['<rootDir>/.yarn-cache/'],
- reporters,
- setupFilesAfterEnv: ['<rootDir>/spec/frontend/test_setup.js', 'jest-canvas-mock'],
- restoreMocks: true,
- transform: {
- '^.+\\.(gql|graphql)$': 'jest-transform-graphql',
- '^.+\\.js$': 'babel-jest',
- '^.+\\.vue$': 'vue-jest',
- },
- transformIgnorePatterns: ['node_modules/(?!(@gitlab/ui|bootstrap-vue|three|monaco-editor)/)'],
- timers: 'fake',
- testEnvironment: '<rootDir>/spec/frontend/environment.js',
- testEnvironmentOptions: {
- IS_EE,
- },
-};
-
-const karmaTestFile = process.argv.find(arg => arg.includes('spec/javascripts/'));
-if (karmaTestFile) {
- console.error(`
-Files in spec/javascripts/ and ee/spec/javascripts need to be run with Karma.
-Please use the following command instead:
-
-yarn karma -f ${karmaTestFile}
-
-`);
- process.exit(1);
-}
diff --git a/jest.config.unit.js b/jest.config.unit.js
new file mode 100644
index 00000000000..4627462c730
--- /dev/null
+++ b/jest.config.unit.js
@@ -0,0 +1,17 @@
+const baseConfig = require('./jest.config.base');
+
+module.exports = {
+ ...baseConfig('spec/frontend'),
+};
+
+const karmaTestFile = process.argv.find(arg => arg.includes('spec/javascripts/'));
+if (karmaTestFile) {
+ console.error(`
+Files in spec/javascripts/ and ee/spec/javascripts need to be run with Karma.
+Please use the following command instead:
+
+yarn karma -f ${karmaTestFile}
+
+`);
+ process.exit(1);
+}
diff --git a/lib/api/admin/ci/variables.rb b/lib/api/admin/ci/variables.rb
new file mode 100644
index 00000000000..df731148bac
--- /dev/null
+++ b/lib/api/admin/ci/variables.rb
@@ -0,0 +1,137 @@
+# frozen_string_literal: true
+
+module API
+ module Admin
+ module Ci
+ class Variables < Grape::API
+ include PaginationParams
+
+ before { authenticated_as_admin! }
+
+ namespace 'admin' do
+ namespace 'ci' do
+ namespace 'variables' do
+ desc 'Get instance-level variables' do
+ success Entities::Variable
+ end
+ params do
+ use :pagination
+ end
+ get '/' do
+ variables = ::Ci::InstanceVariable.all
+
+ present paginate(variables), with: Entities::Variable
+ end
+
+ desc 'Get a specific variable from a group' do
+ success Entities::Variable
+ end
+ params do
+ requires :key, type: String, desc: 'The key of the variable'
+ end
+ get ':key' do
+ key = params[:key]
+ variable = ::Ci::InstanceVariable.find_by_key(key)
+
+ break not_found!('InstanceVariable') unless variable
+
+ present variable, with: Entities::Variable
+ end
+
+ desc 'Create a new instance-level variable' do
+ success Entities::Variable
+ end
+ params do
+ requires :key,
+ type: String,
+ desc: 'The key of the variable'
+
+ requires :value,
+ type: String,
+ desc: 'The value of the variable'
+
+ optional :protected,
+ type: String,
+ desc: 'Whether the variable is protected'
+
+ optional :masked,
+ type: String,
+ desc: 'Whether the variable is masked'
+
+ optional :variable_type,
+ type: String,
+ values: ::Ci::InstanceVariable.variable_types.keys,
+ desc: 'The type of variable, must be one of env_var or file. Defaults to env_var'
+ end
+ post '/' do
+ variable_params = declared_params(include_missing: false)
+
+ variable = ::Ci::InstanceVariable.new(variable_params)
+
+ if variable.save
+ present variable, with: Entities::Variable
+ else
+ render_validation_error!(variable)
+ end
+ end
+
+ desc 'Update an existing instance-variable' do
+ success Entities::Variable
+ end
+ params do
+ optional :key,
+ type: String,
+ desc: 'The key of the variable'
+
+ optional :value,
+ type: String,
+ desc: 'The value of the variable'
+
+ optional :protected,
+ type: String,
+ desc: 'Whether the variable is protected'
+
+ optional :masked,
+ type: String,
+ desc: 'Whether the variable is masked'
+
+ optional :variable_type,
+ type: String,
+ values: ::Ci::InstanceVariable.variable_types.keys,
+ desc: 'The type of variable, must be one of env_var or file'
+ end
+ put ':key' do
+ variable = ::Ci::InstanceVariable.find_by_key(params[:key])
+
+ break not_found!('InstanceVariable') unless variable
+
+ variable_params = declared_params(include_missing: false).except(:key)
+
+ if variable.update(variable_params)
+ present variable, with: Entities::Variable
+ else
+ render_validation_error!(variable)
+ end
+ end
+
+ desc 'Delete an existing instance-level variable' do
+ success Entities::Variable
+ end
+ params do
+ requires :key, type: String, desc: 'The key of the variable'
+ end
+ delete ':key' do
+ variable = ::Ci::InstanceVariable.find_by_key(params[:key])
+ not_found!('InstanceVariable') unless variable
+
+ variable.destroy
+
+ no_content!
+ end
+ end
+ end
+ end
+ end
+ end
+ end
+end
diff --git a/lib/api/api.rb b/lib/api/api.rb
index de9a3120d90..b8135539cda 100644
--- a/lib/api/api.rb
+++ b/lib/api/api.rb
@@ -24,7 +24,8 @@ module API
Gitlab::GrapeLogging::Loggers::ExceptionLogger.new,
Gitlab::GrapeLogging::Loggers::QueueDurationLogger.new,
Gitlab::GrapeLogging::Loggers::PerfLogger.new,
- Gitlab::GrapeLogging::Loggers::CorrelationIdLogger.new
+ Gitlab::GrapeLogging::Loggers::CorrelationIdLogger.new,
+ Gitlab::GrapeLogging::Loggers::ContextLogger.new
]
allow_access_with_scope :api
@@ -97,6 +98,15 @@ module API
handle_api_exception(exception)
end
+ # This is a specific exception raised by `rack-timeout` gem when Puma
+ # requests surpass its timeout. Given it inherits from Exception, we
+ # should rescue it separately. For more info, see:
+ # - https://github.com/sharpstone/rack-timeout/blob/master/doc/exceptions.md
+ # - https://github.com/ruby-grape/grape#exception-handling
+ rescue_from Rack::Timeout::RequestTimeoutException do |exception|
+ handle_api_exception(exception)
+ end
+
format :json
content_type :txt, "text/plain"
@@ -111,6 +121,7 @@ module API
# Keep in alphabetical order
mount ::API::AccessRequests
+ mount ::API::Admin::Ci::Variables
mount ::API::Admin::Sidekiq
mount ::API::Appearance
mount ::API::Applications
@@ -131,6 +142,7 @@ module API
mount ::API::Events
mount ::API::Features
mount ::API::Files
+ mount ::API::FreezePeriods
mount ::API::GroupBoards
mount ::API::GroupClusters
mount ::API::GroupExport
@@ -153,6 +165,7 @@ module API
mount ::API::MergeRequestDiffs
mount ::API::MergeRequests
mount ::API::Metrics::Dashboard::Annotations
+ mount ::API::Metrics::UserStarredDashboards
mount ::API::Namespaces
mount ::API::Notes
mount ::API::Discussions
@@ -169,6 +182,7 @@ module API
mount ::API::ProjectImport
mount ::API::ProjectHooks
mount ::API::ProjectMilestones
+ mount ::API::ProjectRepositoryStorageMoves
mount ::API::Projects
mount ::API::ProjectSnapshots
mount ::API::ProjectSnippets
diff --git a/lib/api/api_guard.rb b/lib/api/api_guard.rb
index 9dd2de5c7ba..c6557fce541 100644
--- a/lib/api/api_guard.rb
+++ b/lib/api/api_guard.rb
@@ -65,7 +65,8 @@ module API
end
def find_user_from_sources
- find_user_from_access_token ||
+ deploy_token_from_request ||
+ find_user_from_access_token ||
find_user_from_job_token ||
find_user_from_warden
end
@@ -90,12 +91,16 @@ module API
end
def api_access_allowed?(user)
- Gitlab::UserAccess.new(user).allowed? && user.can?(:access_api)
+ user_allowed_or_deploy_token?(user) && user.can?(:access_api)
end
def api_access_denied_message(user)
Gitlab::Auth::UserAccessDeniedReason.new(user).rejection_message
end
+
+ def user_allowed_or_deploy_token?(user)
+ Gitlab::UserAccess.new(user).allowed? || user.is_a?(DeployToken)
+ end
end
class_methods do
diff --git a/lib/api/appearance.rb b/lib/api/appearance.rb
index a775102e87d..71a35bb4493 100644
--- a/lib/api/appearance.rb
+++ b/lib/api/appearance.rb
@@ -27,7 +27,8 @@ module API
optional :logo, type: File, desc: 'Instance image used on the sign in / sign up page' # rubocop:disable Scalability/FileUploads
optional :header_logo, type: File, desc: 'Instance image used for the main navigation bar' # rubocop:disable Scalability/FileUploads
optional :favicon, type: File, desc: 'Instance favicon in .ico/.png format' # rubocop:disable Scalability/FileUploads
- optional :new_project_guidelines, type: String, desc: 'Markmarkdown text shown on the new project page'
+ optional :new_project_guidelines, type: String, desc: 'Markdown text shown on the new project page'
+ optional :profile_image_guidelines, type: String, desc: 'Markdown text shown on the profile page below Public Avatar'
optional :header_message, type: String, desc: 'Message within the system header bar'
optional :footer_message, type: String, desc: 'Message within the system footer bar'
optional :message_background_color, type: String, desc: 'Background color for the system header / footer bar'
diff --git a/lib/api/branches.rb b/lib/api/branches.rb
index 999bf1627c1..081e8ffe4f0 100644
--- a/lib/api/branches.rb
+++ b/lib/api/branches.rb
@@ -8,6 +8,8 @@ module API
BRANCH_ENDPOINT_REQUIREMENTS = API::NAMESPACE_OR_PROJECT_REQUIREMENTS.merge(branch: API::NO_SLASH_URL_PART_REGEX)
+ after_validation { content_type "application/json" }
+
before do
require_repository_enabled!
authorize! :download_code, user_project
diff --git a/lib/api/deploy_tokens.rb b/lib/api/deploy_tokens.rb
index f3a08ae970a..0fbbd96cf02 100644
--- a/lib/api/deploy_tokens.rb
+++ b/lib/api/deploy_tokens.rb
@@ -11,6 +11,8 @@ module API
result_hash = Hashie::Mash.new
result_hash[:read_registry] = scopes.include?('read_registry')
result_hash[:write_registry] = scopes.include?('write_registry')
+ result_hash[:read_package_registry] = scopes.include?('read_package_registry')
+ result_hash[:write_package_registry] = scopes.include?('write_package_registry')
result_hash[:read_repository] = scopes.include?('read_repository')
result_hash
end
@@ -55,7 +57,7 @@ module API
params do
requires :name, type: String, desc: "New deploy token's name"
requires :scopes, type: Array[String], values: ::DeployToken::AVAILABLE_SCOPES.map(&:to_s),
- desc: 'Indicates the deploy token scopes. Must be at least one of "read_repository", "read_registry", or "write_registry".'
+ desc: 'Indicates the deploy token scopes. Must be at least one of "read_repository", "read_registry", "write_registry", "read_package_registry", or "write_package_registry".'
optional :expires_at, type: DateTime, desc: 'Expiration date for the deploy token. Does not expire if no value is provided.'
optional :username, type: String, desc: 'Username for deploy token. Default is `gitlab+deploy-token-{n}`'
end
@@ -118,7 +120,7 @@ module API
params do
requires :name, type: String, desc: 'The name of the deploy token'
requires :scopes, type: Array[String], values: ::DeployToken::AVAILABLE_SCOPES.map(&:to_s),
- desc: 'Indicates the deploy token scopes. Must be at least one of "read_repository", "read_registry", or "write_registry".'
+ desc: 'Indicates the deploy token scopes. Must be at least one of "read_repository", "read_registry", "write_registry", "read_package_registry", or "write_package_registry".'
optional :expires_at, type: DateTime, desc: 'Expiration date for the deploy token. Does not expire if no value is provided.'
optional :username, type: String, desc: 'Username for deploy token. Default is `gitlab+deploy-token-{n}`'
end
diff --git a/lib/api/entities/appearance.rb b/lib/api/entities/appearance.rb
index c3cffc8d05c..a09faf55f48 100644
--- a/lib/api/entities/appearance.rb
+++ b/lib/api/entities/appearance.rb
@@ -19,6 +19,7 @@ module API
end
expose :new_project_guidelines
+ expose :profile_image_guidelines
expose :header_message
expose :footer_message
expose :message_background_color
diff --git a/lib/api/entities/branch.rb b/lib/api/entities/branch.rb
index 1d5017ac702..f9d06082ad6 100644
--- a/lib/api/entities/branch.rb
+++ b/lib/api/entities/branch.rb
@@ -3,6 +3,8 @@
module API
module Entities
class Branch < Grape::Entity
+ include Gitlab::Routing
+
expose :name
expose :commit, using: Entities::Commit do |repo_branch, options|
@@ -36,6 +38,10 @@ module API
expose :default do |repo_branch, options|
options[:project].default_branch == repo_branch.name
end
+
+ expose :web_url do |repo_branch|
+ project_tree_url(options[:project], repo_branch.name)
+ end
end
end
end
diff --git a/lib/api/entities/design_management/design.rb b/lib/api/entities/design_management/design.rb
new file mode 100644
index 00000000000..183fe06d8f1
--- /dev/null
+++ b/lib/api/entities/design_management/design.rb
@@ -0,0 +1,16 @@
+# frozen_string_literal: true
+
+module API
+ module Entities
+ module DesignManagement
+ class Design < Grape::Entity
+ expose :id
+ expose :project_id
+ expose :filename
+ expose :image_url do |design|
+ ::Gitlab::UrlBuilder.build(design)
+ end
+ end
+ end
+ end
+end
diff --git a/lib/api/entities/freeze_period.rb b/lib/api/entities/freeze_period.rb
new file mode 100644
index 00000000000..9b5f08925db
--- /dev/null
+++ b/lib/api/entities/freeze_period.rb
@@ -0,0 +1,11 @@
+# frozen_string_literal: true
+
+module API
+ module Entities
+ class FreezePeriod < Grape::Entity
+ expose :id
+ expose :freeze_start, :freeze_end, :cron_timezone
+ expose :created_at, :updated_at
+ end
+ end
+end
diff --git a/lib/api/entities/job_request/artifacts.rb b/lib/api/entities/job_request/artifacts.rb
index c6871fdd875..0d27f5a9189 100644
--- a/lib/api/entities/job_request/artifacts.rb
+++ b/lib/api/entities/job_request/artifacts.rb
@@ -7,6 +7,7 @@ module API
expose :name
expose :untracked
expose :paths
+ expose :exclude, expose_nil: false
expose :when
expose :expire_in
expose :artifact_type
diff --git a/lib/api/entities/merge_request_basic.rb b/lib/api/entities/merge_request_basic.rb
index 4610220e4f6..1a89a83a619 100644
--- a/lib/api/entities/merge_request_basic.rb
+++ b/lib/api/entities/merge_request_basic.rb
@@ -50,8 +50,10 @@ module API
# 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)
+ #
+ # For list endpoints, we skip the recheck by default, since it's expensive
+ expose :merge_status do |merge_request, options|
+ merge_request.check_mergeability(async: true) unless options[:skip_merge_status_recheck]
merge_request.public_merge_status
end
expose :diff_head_sha, as: :sha
diff --git a/lib/api/entities/metrics/user_starred_dashboard.rb b/lib/api/entities/metrics/user_starred_dashboard.rb
new file mode 100644
index 00000000000..d774160e3ea
--- /dev/null
+++ b/lib/api/entities/metrics/user_starred_dashboard.rb
@@ -0,0 +1,11 @@
+# frozen_string_literal: true
+
+module API
+ module Entities
+ module Metrics
+ class UserStarredDashboard < Grape::Entity
+ expose :id, :dashboard_path, :user_id, :project_id
+ end
+ end
+ end
+end
diff --git a/lib/api/entities/personal_snippet.rb b/lib/api/entities/personal_snippet.rb
index eb0266e61e6..03ab6c0809c 100644
--- a/lib/api/entities/personal_snippet.rb
+++ b/lib/api/entities/personal_snippet.rb
@@ -3,9 +3,6 @@
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/project_repository_storage_move.rb b/lib/api/entities/project_repository_storage_move.rb
new file mode 100644
index 00000000000..25643651a14
--- /dev/null
+++ b/lib/api/entities/project_repository_storage_move.rb
@@ -0,0 +1,14 @@
+# frozen_string_literal: true
+
+module API
+ module Entities
+ class ProjectRepositoryStorageMove < Grape::Entity
+ expose :id
+ expose :created_at
+ expose :human_state_name, as: :state
+ expose :source_storage_name
+ expose :destination_storage_name
+ expose :project, using: Entities::ProjectIdentity
+ end
+ end
+end
diff --git a/lib/api/entities/release.rb b/lib/api/entities/release.rb
index edcd9bc6505..99fa496d368 100644
--- a/lib/api/entities/release.rb
+++ b/lib/api/entities/release.rb
@@ -21,7 +21,6 @@ module API
expose :milestones, using: Entities::MilestoneWithStats, 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, _|
@@ -32,7 +31,6 @@ module API
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 :evidences, using: Entities::Releases::Evidence, expose_nil: false, if: ->(_, _) { can_download_code? }
expose :_links do
diff --git a/lib/api/entities/remote_mirror.rb b/lib/api/entities/remote_mirror.rb
index 18d51726bab..87daef9a05c 100644
--- a/lib/api/entities/remote_mirror.rb
+++ b/lib/api/entities/remote_mirror.rb
@@ -12,9 +12,7 @@ module API
expose :last_successful_update_at
expose :last_error
expose :only_protected_branches
- expose :keep_divergent_refs, if: -> (mirror, _options) do
- ::Feature.enabled?(:keep_divergent_refs, mirror.project)
- end
+ expose :keep_divergent_refs
end
end
end
diff --git a/lib/api/entities/runner_details.rb b/lib/api/entities/runner_details.rb
index 2bb143253fe..1dd8543d595 100644
--- a/lib/api/entities/runner_details.rb
+++ b/lib/api/entities/runner_details.rb
@@ -11,9 +11,12 @@ module API
expose :version, :revision, :platform, :architecture
expose :contacted_at
- # @deprecated in 12.10 https://gitlab.com/gitlab-org/gitlab/-/issues/214320
- # will be removed by 13.0 https://gitlab.com/gitlab-org/gitlab/-/issues/214322
- expose :token, if: lambda { |runner, options| options[:current_user].admin? || !runner.instance_type? }
+ # Will be removed: https://gitlab.com/gitlab-org/gitlab/-/issues/217105
+ expose(:token, if: ->(runner, options) do
+ return false if ::Feature.enabled?(:hide_token_from_runners_api, default_enabled: true)
+
+ options[:current_user].admin? || !runner.instance_type?
+ end)
# rubocop: disable CodeReuse/ActiveRecord
expose :projects, with: Entities::BasicProjectDetails do |runner, options|
diff --git a/lib/api/entities/snippet.rb b/lib/api/entities/snippet.rb
index 8a13b4746a9..19c89603cbc 100644
--- a/lib/api/entities/snippet.rb
+++ b/lib/api/entities/snippet.rb
@@ -3,14 +3,20 @@
module API
module Entities
class Snippet < Grape::Entity
- expose :id, :title, :file_name, :description, :visibility
+ expose :id, :title, :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
- expose :ssh_url_to_repo, :http_url_to_repo, if: ->(snippet) { snippet.versioned_enabled_for?(options[:current_user]) }
+ expose :raw_url do |snippet|
+ Gitlab::UrlBuilder.build(snippet, raw: true)
+ end
+ expose :ssh_url_to_repo, :http_url_to_repo, if: ->(snippet) { snippet.repository_exists? }
+ expose :file_name do |snippet|
+ snippet.file_name_on_repo || snippet.file_name
+ end
end
end
end
diff --git a/lib/api/entities/todo.rb b/lib/api/entities/todo.rb
index abfdde89bf1..0acbb4cb704 100644
--- a/lib/api/entities/todo.rb
+++ b/lib/api/entities/todo.rb
@@ -22,6 +22,7 @@ module API
expose :body
expose :state
expose :created_at
+ expose :updated_at
def todo_target_class(target_type)
# false as second argument prevents looking up in module hierarchy
@@ -30,6 +31,8 @@ module API
end
def todo_target_url(todo)
+ return design_todo_target_url(todo) if todo.for_design?
+
target_type = todo.target_type.underscore
target_url = "#{todo.resource_parent.class.to_s.underscore}_#{target_type}_url"
@@ -41,6 +44,16 @@ module API
def todo_target_anchor(todo)
"note_#{todo.note_id}" if todo.note_id?
end
+
+ def design_todo_target_url(todo)
+ design = todo.target
+ path_options = {
+ anchor: todo_target_anchor(todo),
+ vueroute: design.filename
+ }
+
+ ::Gitlab::Routing.url_helpers.designs_project_issue_url(design.project, design.issue, path_options)
+ end
end
end
end
diff --git a/lib/api/entities/user_basic.rb b/lib/api/entities/user_basic.rb
index e063aa42855..80f3ee7b502 100644
--- a/lib/api/entities/user_basic.rb
+++ b/lib/api/entities/user_basic.rb
@@ -18,3 +18,5 @@ module API
end
end
end
+
+API::Entities::UserBasic.prepend_if_ee('EE::API::Entities::UserBasic')
diff --git a/lib/api/entities/user_path.rb b/lib/api/entities/user_path.rb
index 7d922b39654..3f007659813 100644
--- a/lib/api/entities/user_path.rb
+++ b/lib/api/entities/user_path.rb
@@ -12,3 +12,5 @@ module API
end
end
end
+
+API::Entities::UserPath.prepend_if_ee('EE::API::Entities::UserPath')
diff --git a/lib/api/features.rb b/lib/api/features.rb
index 69b751e9bdb..f507919b055 100644
--- a/lib/api/features.rb
+++ b/lib/api/features.rb
@@ -16,6 +16,15 @@ module API
end
end
+ def gate_key(params)
+ case params[:key]
+ when 'percentage_of_actors'
+ :percentage_of_actors
+ else
+ :percentage_of_time
+ end
+ end
+
def gate_targets(params)
Feature::Target.new(params).targets
end
@@ -40,15 +49,22 @@ module API
end
params do
requires :value, type: String, desc: '`true` or `false` to enable/disable, an integer for percentage of time'
+ optional :key, type: String, desc: '`percentage_of_actors` or the default `percentage_of_time`'
optional :feature_group, type: String, desc: 'A Feature group name'
optional :user, type: String, desc: 'A GitLab username'
optional :group, type: String, desc: "A GitLab group's path, such as 'gitlab-org'"
optional :project, type: String, desc: 'A projects path, like gitlab-org/gitlab-ce'
+
+ mutually_exclusive :key, :feature_group
+ mutually_exclusive :key, :user
+ mutually_exclusive :key, :group
+ mutually_exclusive :key, :project
end
post ':name' do
feature = Feature.get(params[:name])
targets = gate_targets(params)
value = gate_value(params)
+ key = gate_key(params)
case value
when true
@@ -64,7 +80,11 @@ module API
feature.disable
end
else
- feature.enable_percentage_of_time(value)
+ if key == :percentage_of_actors
+ feature.enable_percentage_of_actors(value)
+ else
+ feature.enable_percentage_of_time(value)
+ end
end
present feature, with: Entities::Feature, current_user: current_user
diff --git a/lib/api/freeze_periods.rb b/lib/api/freeze_periods.rb
new file mode 100644
index 00000000000..9c7e5a5832d
--- /dev/null
+++ b/lib/api/freeze_periods.rb
@@ -0,0 +1,107 @@
+# frozen_string_literal: true
+
+module API
+ class FreezePeriods < Grape::API
+ include PaginationParams
+
+ before { authenticate! }
+
+ params do
+ requires :id, type: String, desc: 'The ID of a project'
+ end
+
+ resource :projects, requirements: API::NAMESPACE_OR_PROJECT_REQUIREMENTS do
+ desc 'Get project freeze periods' do
+ detail 'This feature was introduced in GitLab 13.0.'
+ success Entities::FreezePeriod
+ end
+ params do
+ use :pagination
+ end
+
+ get ":id/freeze_periods" do
+ authorize! :read_freeze_period, user_project
+
+ freeze_periods = ::FreezePeriodsFinder.new(user_project, current_user).execute
+
+ present paginate(freeze_periods), with: Entities::FreezePeriod, current_user: current_user
+ end
+
+ desc 'Get a single freeze period' do
+ detail 'This feature was introduced in GitLab 13.0.'
+ success Entities::FreezePeriod
+ end
+ params do
+ requires :freeze_period_id, type: Integer, desc: 'The ID of a project freeze period'
+ end
+ get ":id/freeze_periods/:freeze_period_id" do
+ authorize! :read_freeze_period, user_project
+
+ present freeze_period, with: Entities::FreezePeriod, current_user: current_user
+ end
+
+ desc 'Create a new freeze period' do
+ detail 'This feature was introduced in GitLab 13.0.'
+ success Entities::FreezePeriod
+ end
+ params do
+ requires :freeze_start, type: String, desc: 'Freeze Period start'
+ requires :freeze_end, type: String, desc: 'Freeze Period end'
+ optional :cron_timezone, type: String, desc: 'Timezone'
+ end
+ post ':id/freeze_periods' do
+ authorize! :create_freeze_period, user_project
+
+ freeze_period_params = declared(params, include_parent_namespaces: false)
+
+ freeze_period = user_project.freeze_periods.create(freeze_period_params)
+
+ if freeze_period.persisted?
+ present freeze_period, with: Entities::FreezePeriod
+ else
+ render_validation_error!(freeze_period)
+ end
+ end
+
+ desc 'Update a freeze period' do
+ detail 'This feature was introduced in GitLab 13.0.'
+ success Entities::FreezePeriod
+ end
+ params do
+ optional :freeze_start, type: String, desc: 'Freeze Period start'
+ optional :freeze_end, type: String, desc: 'Freeze Period end'
+ optional :cron_timezone, type: String, desc: 'Freeze Period Timezone'
+ end
+ put ':id/freeze_periods/:freeze_period_id' do
+ authorize! :update_freeze_period, user_project
+
+ freeze_period_params = declared(params, include_parent_namespaces: false, include_missing: false)
+
+ if freeze_period.update(freeze_period_params)
+ present freeze_period, with: Entities::FreezePeriod
+ else
+ render_validation_error!(freeze_period)
+ end
+ end
+
+ desc 'Delete a freeze period' do
+ detail 'This feature was introduced in GitLab 13.0.'
+ success Entities::FreezePeriod
+ end
+ params do
+ requires :freeze_period_id, type: Integer, desc: 'Freeze Period ID'
+ end
+ delete ':id/freeze_periods/:freeze_period_id' do
+ authorize! :destroy_freeze_period, user_project
+
+ destroy_conditionally!(freeze_period)
+ end
+ end
+
+ helpers do
+ def freeze_period
+ @freeze_period ||= user_project.freeze_periods.find(params[:freeze_period_id])
+ end
+ end
+ end
+end
diff --git a/lib/api/groups.rb b/lib/api/groups.rb
index d375c35e8c0..353c8b4b242 100644
--- a/lib/api/groups.rb
+++ b/lib/api/groups.rb
@@ -60,18 +60,14 @@ module API
.execute
end
- def find_group_projects(params)
+ def find_group_projects(params, finder_options)
group = find_group!(params[:id])
- options = {
- only_owned: !params[:with_shared],
- include_subgroups: params[:include_subgroups]
- }
projects = GroupProjectsFinder.new(
group: group,
current_user: current_user,
params: project_finder_params,
- options: options
+ options: finder_options
).execute
projects = projects.with_issues_available_for_user(current_user) if params[:with_issues_enabled]
projects = projects.with_merge_requests_enabled if params[:with_merge_requests_enabled]
@@ -80,11 +76,22 @@ module API
paginate(projects)
end
+ def present_projects(params, projects)
+ options = {
+ with: params[:simple] ? Entities::BasicProjectDetails : Entities::Project,
+ current_user: current_user
+ }
+
+ projects, options = with_custom_attributes(projects, options)
+
+ present options[:with].prepare_relation(projects), options
+ end
+
def present_groups(params, groups)
options = {
with: Entities::Group,
current_user: current_user,
- statistics: params[:statistics] && current_user.admin?
+ statistics: params[:statistics] && current_user&.admin?
}
groups = groups.with_statistics if options[:statistics]
@@ -226,16 +233,42 @@ module API
use :optional_projects_params
end
get ":id/projects" do
- projects = find_group_projects(params)
-
- options = {
- with: params[:simple] ? Entities::BasicProjectDetails : Entities::Project,
- current_user: current_user
+ finder_options = {
+ only_owned: !params[:with_shared],
+ include_subgroups: params[:include_subgroups]
}
- projects, options = with_custom_attributes(projects, options)
+ projects = find_group_projects(params, finder_options)
- present options[:with].prepare_relation(projects), options
+ present_projects(params, projects)
+ end
+
+ desc 'Get a list of shared projects in this group' do
+ success Entities::Project
+ end
+ params do
+ optional :archived, type: Boolean, default: false, desc: 'Limit by archived status'
+ optional :visibility, type: String, values: Gitlab::VisibilityLevel.string_values,
+ desc: 'Limit by visibility'
+ optional :search, type: String, desc: 'Return list of authorized projects matching the search criteria'
+ optional :order_by, type: String, values: %w[id name path created_at updated_at last_activity_at],
+ default: 'created_at', desc: 'Return projects ordered by field'
+ optional :sort, type: String, values: %w[asc desc], default: 'desc',
+ desc: 'Return projects sorted in ascending and descending order'
+ optional :simple, type: Boolean, default: false,
+ desc: 'Return only the ID, URL, name, and path of each project'
+ optional :starred, type: Boolean, default: false, desc: 'Limit by starred status'
+ optional :with_issues_enabled, type: Boolean, default: false, desc: 'Limit by enabled issues feature'
+ optional :with_merge_requests_enabled, type: Boolean, default: false, desc: 'Limit by enabled merge requests feature'
+ optional :min_access_level, type: Integer, values: Gitlab::Access.all_values, desc: 'Limit by minimum access level of authenticated user on projects'
+
+ use :pagination
+ use :with_custom_attributes
+ end
+ get ":id/projects/shared" do
+ projects = find_group_projects(params, { only_shared: true })
+
+ present_projects(params, projects)
end
desc 'Get a list of subgroups in this group.' do
diff --git a/lib/api/helpers.rb b/lib/api/helpers.rb
index 42b82aac1c4..c6f6dc255d4 100644
--- a/lib/api/helpers.rb
+++ b/lib/api/helpers.rb
@@ -11,6 +11,7 @@ module API
SUDO_PARAM = :sudo
API_USER_ENV = 'gitlab.api.user'
API_EXCEPTION_ENV = 'gitlab.api.exception'
+ API_RESPONSE_STATUS_CODE = 'gitlab.api.response_status_code'
def declared_params(options = {})
options = { include_parent_namespaces: false }.merge(options)
@@ -178,6 +179,14 @@ module API
end
end
+ def find_tag!(tag_name)
+ if Gitlab::GitRefValidator.validate(tag_name)
+ user_project.repository.find_tag(tag_name) || not_found!('Tag')
+ else
+ render_api_error!('The tag refname is invalid', 400)
+ end
+ end
+
# rubocop: disable CodeReuse/ActiveRecord
def find_project_issue(iid, project_id = nil)
project = project_id ? find_project!(project_id) : user_project
@@ -416,6 +425,11 @@ module API
end
def render_api_error!(message, status)
+ # grape-logging doesn't pass the status code, so this is a
+ # workaround for getting that information in the loggers:
+ # https://github.com/aserafin/grape_logging/issues/71
+ env[API_RESPONSE_STATUS_CODE] = Rack::Utils.status_code(status)
+
error!({ 'message' => message }, status, header)
end
@@ -595,8 +609,8 @@ module API
header(*Gitlab::Workhorse.send_git_archive(repository, **kwargs))
end
- def send_artifacts_entry(build, entry)
- header(*Gitlab::Workhorse.send_artifacts_entry(build, entry))
+ def send_artifacts_entry(file, entry)
+ header(*Gitlab::Workhorse.send_artifacts_entry(file, entry))
end
# The Grape Error Middleware only has access to `env` but not `params` nor
diff --git a/lib/api/helpers/internal_helpers.rb b/lib/api/helpers/internal_helpers.rb
index 31272c537a3..b05e82a541d 100644
--- a/lib/api/helpers/internal_helpers.rb
+++ b/lib/api/helpers/internal_helpers.rb
@@ -51,7 +51,7 @@ module API
def parse_env
return {} if params[:env].blank?
- JSON.parse(params[:env])
+ Gitlab::Json.parse(params[:env])
rescue JSON::ParserError
{}
end
diff --git a/lib/api/helpers/merge_requests_helpers.rb b/lib/api/helpers/merge_requests_helpers.rb
index 73711a7e0ba..9dab2a88f0b 100644
--- a/lib/api/helpers/merge_requests_helpers.rb
+++ b/lib/api/helpers/merge_requests_helpers.rb
@@ -27,6 +27,7 @@ module API
coerce_with: Validations::Types::LabelsList.coerce,
desc: 'Comma-separated list of label names'
optional :with_labels_details, type: Boolean, desc: 'Return titles of labels and other details', default: false
+ optional :with_merge_status_recheck, type: Boolean, desc: 'Request that stale merge statuses be rechecked asynchronously', default: false
optional :created_after, type: DateTime, desc: 'Return merge requests created after the specified time'
optional :created_before, type: DateTime, desc: 'Return merge requests created before the specified time'
optional :updated_after, type: DateTime, desc: 'Return merge requests updated after the specified time'
diff --git a/lib/api/helpers/pagination_strategies.rb b/lib/api/helpers/pagination_strategies.rb
index 6bebb4bfeac..823891d6fe7 100644
--- a/lib/api/helpers/pagination_strategies.rb
+++ b/lib/api/helpers/pagination_strategies.rb
@@ -3,19 +3,24 @@
module API
module Helpers
module PaginationStrategies
- def paginate_with_strategies(relation)
- paginator = paginator(relation)
+ def paginate_with_strategies(relation, request_scope)
+ paginator = paginator(relation, request_scope)
yield(paginator.paginate(relation)).tap do |records, _|
paginator.finalize(records)
end
end
- def paginator(relation)
- return Gitlab::Pagination::OffsetPagination.new(self) unless keyset_pagination_enabled?
+ def paginator(relation, request_scope = nil)
+ return keyset_paginator(relation) if keyset_pagination_enabled?
- request_context = Gitlab::Pagination::Keyset::RequestContext.new(self)
+ offset_paginator(relation, request_scope)
+ end
+
+ private
+ def keyset_paginator(relation)
+ request_context = Gitlab::Pagination::Keyset::RequestContext.new(self)
unless Gitlab::Pagination::Keyset.available?(request_context, relation)
return error!('Keyset pagination is not yet available for this type of request', 405)
end
@@ -23,11 +28,28 @@ module API
Gitlab::Pagination::Keyset::Pager.new(request_context)
end
- private
+ def offset_paginator(relation, request_scope)
+ offset_limit = limit_for_scope(request_scope)
+ if Gitlab::Pagination::Keyset.available_for_type?(relation) && offset_limit_exceeded?(offset_limit)
+ return error!("Offset pagination has a maximum allowed offset of #{offset_limit} " \
+ "for requests that return objects of type #{relation.klass}. " \
+ "Remaining records can be retrieved using keyset pagination.", 405)
+ end
+
+ Gitlab::Pagination::OffsetPagination.new(self)
+ end
def keyset_pagination_enabled?
params[:pagination] == 'keyset'
end
+
+ def limit_for_scope(scope)
+ (scope || Plan.default).actual_limits.offset_pagination_limit
+ end
+
+ def offset_limit_exceeded?(offset_limit)
+ offset_limit.positive? && params[:page] * params[:per_page] > offset_limit
+ end
end
end
end
diff --git a/lib/api/helpers/projects_helpers.rb b/lib/api/helpers/projects_helpers.rb
index 14c83114f32..5afdb34da97 100644
--- a/lib/api/helpers/projects_helpers.rb
+++ b/lib/api/helpers/projects_helpers.rb
@@ -31,6 +31,7 @@ module API
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 :show_default_award_emojis, type: Boolean, desc: 'Show default award emojis'
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'
diff --git a/lib/api/helpers/search_helpers.rb b/lib/api/helpers/search_helpers.rb
index de8cbe62106..936684ea1f8 100644
--- a/lib/api/helpers/search_helpers.rb
+++ b/lib/api/helpers/search_helpers.rb
@@ -5,7 +5,7 @@ module API
module SearchHelpers
def self.global_search_scopes
# This is a separate method so that EE can redefine it.
- %w(projects issues merge_requests milestones snippet_titles snippet_blobs users)
+ %w(projects issues merge_requests milestones snippet_titles users)
end
def self.group_search_scopes
diff --git a/lib/api/helpers/services_helpers.rb b/lib/api/helpers/services_helpers.rb
index 4c44aca2de4..02e60ff5db5 100644
--- a/lib/api/helpers/services_helpers.rb
+++ b/lib/api/helpers/services_helpers.rb
@@ -724,6 +724,15 @@ module API
desc: 'The Unify Circuit webhook. e.g. https://circuit.com/rest/v2/webhooks/incoming/…'
},
chat_notification_events
+ ].flatten,
+ 'webex-teams' => [
+ {
+ required: true,
+ name: :webhook,
+ type: String,
+ desc: 'The Webex Teams webhook. e.g. https://api.ciscospark.com/v1/webhooks/incoming/…'
+ },
+ chat_notification_events
].flatten
}
end
diff --git a/lib/api/helpers/snippets_helpers.rb b/lib/api/helpers/snippets_helpers.rb
new file mode 100644
index 00000000000..20aeca6a9d3
--- /dev/null
+++ b/lib/api/helpers/snippets_helpers.rb
@@ -0,0 +1,17 @@
+# frozen_string_literal: true
+
+module API
+ module Helpers
+ module SnippetsHelpers
+ def content_for(snippet)
+ if snippet.empty_repo?
+ snippet.content
+ else
+ blob = snippet.blobs.first
+ blob.load_all_data!
+ blob.data
+ end
+ end
+ end
+ end
+end
diff --git a/lib/api/internal/base.rb b/lib/api/internal/base.rb
index 0d50a310b37..79c407b9581 100644
--- a/lib/api/internal/base.rb
+++ b/lib/api/internal/base.rb
@@ -30,10 +30,6 @@ module API
project.http_url_to_repo
end
- def ee_post_receive_response_hook(response)
- # Hook for EE to add messages
- end
-
def check_allowed(params)
# This is a separate method so that EE can alter its behaviour more
# easily.
@@ -73,13 +69,14 @@ module API
}
# Custom option for git-receive-pack command
+ if Feature.enabled?(:gitaly_upload_pack_filter, project, default_enabled: true)
+ payload[:git_config_options] << "uploadpack.allowFilter=true" << "uploadpack.allowAnySHA1InWant=true"
+ end
+
receive_max_input_size = Gitlab::CurrentSettings.receive_max_input_size.to_i
+
if receive_max_input_size > 0
payload[:git_config_options] << "receive.maxInputSize=#{receive_max_input_size.megabytes}"
-
- if Feature.enabled?(:gitaly_upload_pack_filter, project)
- payload[:git_config_options] << "uploadpack.allowFilter=true" << "uploadpack.allowAnySHA1InWant=true"
- end
end
response_with_status(**payload)
@@ -116,10 +113,6 @@ module API
# check_ip - optional, only in EE version, may limit access to
# group resources based on its IP restrictions
post "/allowed" do
- if repo_type.snippet? && params[:protocol] != 'web' && Feature.disabled?(:version_snippets, actor.user)
- break response_with_status(code: 401, success: false, message: 'Snippet git access is disabled.')
- end
-
# It was moved to a separate method so that EE can alter its behaviour more
# easily.
check_allowed(params)
@@ -216,8 +209,6 @@ module API
response = PostReceiveService.new(actor.user, repository, project, params).execute
- ee_post_receive_response_hook(response)
-
present response, with: Entities::InternalPostReceive::Response
end
end
diff --git a/lib/api/issues.rb b/lib/api/issues.rb
index f27afd0055f..be50c3e0381 100644
--- a/lib/api/issues.rb
+++ b/lib/api/issues.rb
@@ -95,6 +95,8 @@ module API
use :issues_params
optional :scope, type: String, values: %w[created-by-me assigned-to-me created_by_me assigned_to_me all], default: 'created_by_me',
desc: 'Return issues for the given scope: `created_by_me`, `assigned_to_me` or `all`'
+ optional :non_archived, type: Boolean, default: true,
+ desc: 'Return issues from non archived projects'
end
get do
authenticate! unless params[:scope] == 'all'
diff --git a/lib/api/job_artifacts.rb b/lib/api/job_artifacts.rb
index 920938ad453..6a82256cc96 100644
--- a/lib/api/job_artifacts.rb
+++ b/lib/api/job_artifacts.rb
@@ -54,7 +54,7 @@ module API
bad_request! unless path.valid?
- send_artifacts_entry(build, path)
+ send_artifacts_entry(build.artifacts_file, path)
end
desc 'Download the artifacts archive from a job' do
@@ -90,7 +90,7 @@ module API
bad_request! unless path.valid?
- send_artifacts_entry(build, path)
+ send_artifacts_entry(build.artifacts_file, path)
end
desc 'Keep the artifacts to prevent them from being deleted' do
diff --git a/lib/api/members.rb b/lib/api/members.rb
index 2e49b4be45c..37d4ca29b68 100644
--- a/lib/api/members.rb
+++ b/lib/api/members.rb
@@ -160,3 +160,5 @@ module API
end
end
end
+
+API::Members.prepend_if_ee('EE::API::Members')
diff --git a/lib/api/merge_requests.rb b/lib/api/merge_requests.rb
index d45786cdd3d..ff4ad85115b 100644
--- a/lib/api/merge_requests.rb
+++ b/lib/api/merge_requests.rb
@@ -26,6 +26,8 @@ module API
assignee_ids
description
labels
+ add_labels
+ remove_labels
milestone_id
remove_source_branch
state_event
@@ -91,6 +93,9 @@ module API
options[:with] = Entities::MergeRequestSimple
else
options[:issuable_metadata] = issuable_meta_data(merge_requests, 'MergeRequest', current_user)
+ if Feature.enabled?(:mr_list_api_skip_merge_status_recheck, default_enabled: true)
+ options[:skip_merge_status_recheck] = !declared_params[:with_merge_status_recheck]
+ end
end
options
@@ -180,6 +185,8 @@ module API
optional :assignee_ids, type: Array[Integer], desc: 'The array of user IDs to assign issue'
optional :milestone_id, type: Integer, desc: 'The ID of a milestone to assign the merge request'
optional :labels, type: Array[String], coerce_with: Validations::Types::LabelsList.coerce, desc: 'Comma-separated list of label names'
+ optional :add_labels, type: Array[String], coerce_with: Validations::Types::LabelsList.coerce, desc: 'Comma-separated list of label names'
+ optional :remove_labels, type: Array[String], coerce_with: Validations::Types::LabelsList.coerce, desc: 'Comma-separated list of label names'
optional :remove_source_branch, type: Boolean, desc: 'Remove source branch when merging'
optional :allow_collaboration, type: Boolean, desc: 'Allow commits from members who can merge to the target branch'
optional :allow_maintainer_to_push, type: Boolean, as: :allow_collaboration, desc: '[deprecated] See allow_collaboration'
diff --git a/lib/api/metrics/dashboard/annotations.rb b/lib/api/metrics/dashboard/annotations.rb
index 691abac863a..c8ec4d29657 100644
--- a/lib/api/metrics/dashboard/annotations.rb
+++ b/lib/api/metrics/dashboard/annotations.rb
@@ -8,30 +8,37 @@ module API
success Entities::Metrics::Dashboard::Annotation
end
- params do
- requires :starting_at, type: DateTime,
- desc: 'Date time indicating starting moment to which the annotation relates.'
- optional :ending_at, type: DateTime,
- desc: 'Date time indicating ending moment to which the annotation relates.'
- requires :dashboard_path, type: String,
- desc: 'The path to a file defining the dashboard on which the annotation should be added'
- requires :description, type: String, desc: 'The description of the annotation'
- end
+ ANNOTATIONS_SOURCES = [
+ { class: ::Environment, resource: :environments, create_service_param_key: :environment },
+ { class: Clusters::Cluster, resource: :clusters, create_service_param_key: :cluster }
+ ].freeze
+
+ ANNOTATIONS_SOURCES.each do |annotations_source|
+ resource annotations_source[:resource] do
+ params do
+ requires :starting_at, type: DateTime,
+ desc: 'Date time indicating starting moment to which the annotation relates.'
+ optional :ending_at, type: DateTime,
+ desc: 'Date time indicating ending moment to which the annotation relates.'
+ requires :dashboard_path, type: String, coerce_with: -> (val) { CGI.unescape(val) },
+ desc: 'The path to a file defining the dashboard on which the annotation should be added'
+ requires :description, type: String, desc: 'The description of the annotation'
+ end
- resource :environments do
- post ':id/metrics_dashboard/annotations' do
- environment = ::Environment.find(params[:id])
+ post ':id/metrics_dashboard/annotations' do
+ annotations_source_object = annotations_source[:class].find(params[:id])
- not_found! unless Feature.enabled?(:metrics_dashboard_annotations, environment.project)
+ forbidden! unless can?(current_user, :create_metrics_dashboard_annotation, annotations_source_object)
- forbidden! unless can?(current_user, :create_metrics_dashboard_annotation, environment)
+ create_service_params = declared(params).merge(annotations_source[:create_service_param_key] => annotations_source_object)
- result = ::Metrics::Dashboard::Annotations::CreateService.new(current_user, declared(params).merge(environment: environment)).execute
+ result = ::Metrics::Dashboard::Annotations::CreateService.new(current_user, create_service_params).execute
- if result[:status] == :success
- present result[:annotation], with: Entities::Metrics::Dashboard::Annotation
- else
- error!(result, 400)
+ if result[:status] == :success
+ present result[:annotation], with: Entities::Metrics::Dashboard::Annotation
+ else
+ error!(result, 400)
+ end
end
end
end
diff --git a/lib/api/metrics/user_starred_dashboards.rb b/lib/api/metrics/user_starred_dashboards.rb
new file mode 100644
index 00000000000..85fc0f33ed8
--- /dev/null
+++ b/lib/api/metrics/user_starred_dashboards.rb
@@ -0,0 +1,46 @@
+# frozen_string_literal: true
+
+module API
+ module Metrics
+ class UserStarredDashboards < Grape::API
+ resource :projects do
+ desc 'Marks selected metrics dashboard as starred' do
+ success Entities::Metrics::UserStarredDashboard
+ end
+
+ params do
+ requires :dashboard_path, type: String, allow_blank: false, coerce_with: ->(val) { CGI.unescape(val) },
+ desc: 'Url encoded path to a file defining the dashboard to which the star should be added'
+ end
+
+ post ':id/metrics/user_starred_dashboards' do
+ result = ::Metrics::UsersStarredDashboards::CreateService.new(current_user, user_project, params[:dashboard_path]).execute
+
+ if result.success?
+ present result.payload, with: Entities::Metrics::UserStarredDashboard
+ else
+ error!({ errors: result.message }, 400)
+ end
+ end
+
+ desc 'Remove star from selected metrics dashboard'
+
+ params do
+ optional :dashboard_path, type: String, allow_blank: false, coerce_with: ->(val) { CGI.unescape(val) },
+ desc: 'Url encoded path to a file defining the dashboard from which the star should be removed'
+ end
+
+ delete ':id/metrics/user_starred_dashboards' do
+ result = ::Metrics::UsersStarredDashboards::DeleteService.new(current_user, user_project, params[:dashboard_path]).execute
+
+ if result.success?
+ status :ok
+ result.payload
+ else
+ status :bad_request
+ end
+ end
+ end
+ end
+ end
+end
diff --git a/lib/api/pipelines.rb b/lib/api/pipelines.rb
index 06f8920b37c..c09bca26a41 100644
--- a/lib/api/pipelines.rb
+++ b/lib/api/pipelines.rb
@@ -108,6 +108,21 @@ module API
present pipeline.variables, with: Entities::Variable
end
+ desc 'Gets the test report for a given pipeline' do
+ detail 'This feature was introduced in GitLab 13.0. Disabled by default behind feature flag `junit_pipeline_view`'
+ success TestReportEntity
+ end
+ params do
+ requires :pipeline_id, type: Integer, desc: 'The pipeline ID'
+ end
+ get ':id/pipelines/:pipeline_id/test_report' do
+ not_found! unless Feature.enabled?(:junit_pipeline_view, user_project)
+
+ authorize! :read_build, pipeline
+
+ present pipeline.test_reports, with: TestReportEntity
+ end
+
desc 'Deletes a pipeline' do
detail 'This feature was introduced in GitLab 11.6'
http_codes [[204, 'Pipeline was deleted'], [403, 'Forbidden']]
diff --git a/lib/api/project_repository_storage_moves.rb b/lib/api/project_repository_storage_moves.rb
new file mode 100644
index 00000000000..1a63e984fbf
--- /dev/null
+++ b/lib/api/project_repository_storage_moves.rb
@@ -0,0 +1,34 @@
+# frozen_string_literal: true
+
+module API
+ class ProjectRepositoryStorageMoves < Grape::API
+ include PaginationParams
+
+ before { authenticated_as_admin! }
+
+ resource :project_repository_storage_moves do
+ desc 'Get a list of all project repository storage moves' do
+ detail 'This feature was introduced in GitLab 13.0.'
+ success Entities::ProjectRepositoryStorageMove
+ end
+ params do
+ use :pagination
+ end
+ get do
+ storage_moves = ProjectRepositoryStorageMove.with_projects.order_created_at_desc
+
+ present paginate(storage_moves), with: Entities::ProjectRepositoryStorageMove, current_user: current_user
+ end
+
+ desc 'Get a project repository storage move' do
+ detail 'This feature was introduced in GitLab 13.0.'
+ success Entities::ProjectRepositoryStorageMove
+ end
+ get ':id' do
+ storage_move = ProjectRepositoryStorageMove.find(params[:id])
+
+ present storage_move, with: Entities::ProjectRepositoryStorageMove, current_user: current_user
+ end
+ end
+ end
+end
diff --git a/lib/api/project_snippets.rb b/lib/api/project_snippets.rb
index e8234a9285c..68f4a0dcb65 100644
--- a/lib/api/project_snippets.rb
+++ b/lib/api/project_snippets.rb
@@ -11,6 +11,7 @@ module API
requires :id, type: String, desc: 'The ID of a project'
end
resource :projects, requirements: API::NAMESPACE_OR_PROJECT_REQUIREMENTS do
+ helpers Helpers::SnippetsHelpers
helpers do
def check_snippets_enabled
forbidden! unless user_project.feature_available?(:snippets, current_user)
@@ -54,30 +55,27 @@ module API
success Entities::ProjectSnippet
end
params do
- requires :title, type: String, desc: 'The title of the snippet'
+ requires :title, type: String, allow_blank: false, desc: 'The title of the snippet'
requires :file_name, type: String, desc: 'The file name of the snippet'
- optional :code, type: String, allow_blank: false, desc: 'The content of the snippet (deprecated in favor of "content")'
- optional :content, type: String, allow_blank: false, desc: 'The content of the snippet'
+ requires :content, type: String, allow_blank: false, desc: 'The content of the snippet'
optional :description, type: String, desc: 'The description of a snippet'
requires :visibility, type: String,
values: Gitlab::VisibilityLevel.string_values,
desc: 'The visibility of the snippet'
- mutually_exclusive :code, :content
end
post ":id/snippets" do
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?
service_response = ::Snippets::CreateService.new(user_project, current_user, snippet_params).execute
snippet = service_response.payload[:snippet]
- render_spam_error! if snippet.spam?
-
- if snippet.persisted?
+ if service_response.success?
present snippet, with: Entities::ProjectSnippet
else
- render_validation_error!(snippet)
+ render_spam_error! if snippet.spam?
+
+ render_api_error!({ error: service_response.message }, service_response.http_status)
end
end
@@ -86,16 +84,14 @@ module API
end
params do
requires :snippet_id, type: Integer, desc: 'The ID of a project snippet'
- optional :title, type: String, desc: 'The title of the snippet'
+ optional :title, type: String, allow_blank: false, desc: 'The title of the snippet'
optional :file_name, type: String, desc: 'The file name of the snippet'
- optional :code, type: String, allow_blank: false, desc: 'The content of the snippet (deprecated in favor of "content")'
optional :content, type: String, allow_blank: false, desc: 'The content of the snippet'
optional :description, type: String, desc: 'The description of a snippet'
optional :visibility, type: String,
values: Gitlab::VisibilityLevel.string_values,
desc: 'The visibility of the snippet'
- at_least_one_of :title, :file_name, :code, :content, :visibility_level
- mutually_exclusive :code, :content
+ at_least_one_of :title, :file_name, :content, :visibility_level
end
# rubocop: disable CodeReuse/ActiveRecord
put ":id/snippets/:snippet_id" do
@@ -107,17 +103,15 @@ module API
snippet_params = declared_params(include_missing: false)
.merge(request: request, api: true)
- snippet_params[:content] = snippet_params.delete(:code) if snippet_params[:code].present?
-
service_response = ::Snippets::UpdateService.new(user_project, current_user, snippet_params).execute(snippet)
snippet = service_response.payload[:snippet]
- render_spam_error! if snippet.spam?
-
- if snippet.valid?
+ if service_response.success?
present snippet, with: Entities::ProjectSnippet
else
- render_validation_error!(snippet)
+ render_spam_error! if snippet.spam?
+
+ render_api_error!({ error: service_response.message }, service_response.http_status)
end
end
# rubocop: enable CodeReuse/ActiveRecord
@@ -155,7 +149,7 @@ module API
env['api.format'] = :txt
content_type 'text/plain'
- present snippet.content
+ present content_for(snippet)
end
# rubocop: enable CodeReuse/ActiveRecord
diff --git a/lib/api/project_templates.rb b/lib/api/project_templates.rb
index 119902a189c..cfcc7f5212d 100644
--- a/lib/api/project_templates.rb
+++ b/lib/api/project_templates.rb
@@ -5,6 +5,10 @@ module API
include PaginationParams
TEMPLATE_TYPES = %w[dockerfiles gitignores gitlab_ci_ymls licenses].freeze
+ # The regex is needed to ensure a period (e.g. agpl-3.0)
+ # isn't confused with a format type. We also need to allow encoded
+ # values (e.g. C%2B%2B for C++), so allow % and + as well.
+ TEMPLATE_NAMES_ENDPOINT_REQUIREMENTS = API::NAMESPACE_OR_PROJECT_REQUIREMENTS.merge(name: /[\w%.+-]+/)
before { authenticate_non_get! }
@@ -12,7 +16,7 @@ module API
requires :id, type: String, desc: 'The ID of a project'
requires :type, type: String, values: TEMPLATE_TYPES, desc: 'The type (dockerfiles|gitignores|gitlab_ci_ymls|licenses) of the template'
end
- resource :projects do
+ resource :projects, requirements: API::NAMESPACE_OR_PROJECT_REQUIREMENTS do
desc 'Get a list of templates available to this project' do
detail 'This endpoint was introduced in GitLab 11.4'
end
@@ -36,10 +40,8 @@ module API
optional :project, type: String, desc: 'The project name to use when expanding placeholders in the template. Only affects licenses'
optional :fullname, type: String, desc: 'The full name of the copyright holder to use when expanding placeholders in the template. Only affects licenses'
end
- # The regex is needed to ensure a period (e.g. agpl-3.0)
- # isn't confused with a format type. We also need to allow encoded
- # values (e.g. C%2B%2B for C++), so allow % and + as well.
- get ':id/templates/:type/:name', requirements: { name: /[\w%.+-]+/ } do
+
+ get ':id/templates/:type/:name', requirements: TEMPLATE_NAMES_ENDPOINT_REQUIREMENTS do
template = TemplateFinder
.build(params[:type], user_project, name: params[:name])
.execute
diff --git a/lib/api/projects.rb b/lib/api/projects.rb
index ee0731a331f..732453cf1c4 100644
--- a/lib/api/projects.rb
+++ b/lib/api/projects.rb
@@ -95,7 +95,7 @@ module API
projects = reorder_projects(projects)
projects = apply_filters(projects)
- records, options = paginate_with_strategies(projects) do |projects|
+ records, options = paginate_with_strategies(projects, options[:request_scope]) do |projects|
projects, options = with_custom_attributes(projects, options)
options = options.reverse_merge(
@@ -313,7 +313,7 @@ module API
get ':id/forks' do
forks = ForkProjectsFinder.new(user_project, params: project_finder_params, current_user: current_user).execute
- present_projects forks
+ present_projects forks, request_scope: user_project
end
desc 'Check pages access of this project'
diff --git a/lib/api/remote_mirrors.rb b/lib/api/remote_mirrors.rb
index 7e484eb8885..0808541d3c7 100644
--- a/lib/api/remote_mirrors.rb
+++ b/lib/api/remote_mirrors.rb
@@ -34,7 +34,6 @@ module API
end
post ':id/remote_mirrors' do
create_params = declared_params(include_missing: false)
- create_params.delete(:keep_divergent_refs) unless ::Feature.enabled?(:keep_divergent_refs, user_project)
new_mirror = user_project.remote_mirrors.create(create_params)
@@ -59,7 +58,6 @@ module API
mirror_params = declared_params(include_missing: false)
mirror_params[:id] = mirror_params.delete(:mirror_id)
- mirror_params.delete(:keep_divergent_refs) unless ::Feature.enabled?(:keep_divergent_refs, user_project)
update_params = { remote_mirrors_attributes: mirror_params }
diff --git a/lib/api/search.rb b/lib/api/search.rb
index ed52a4fc8f2..3d2d4527e30 100644
--- a/lib/api/search.rb
+++ b/lib/api/search.rb
@@ -17,7 +17,6 @@ module API
blobs: Entities::Blob,
wiki_blobs: Entities::Blob,
snippet_titles: Entities::Snippet,
- snippet_blobs: Entities::Snippet,
users: Entities::UserBasic
}.freeze
@@ -36,7 +35,7 @@ module API
end
def snippets?
- %w(snippet_blobs snippet_titles).include?(params[:scope]).to_s
+ %w(snippet_titles).include?(params[:scope]).to_s
end
def entity
diff --git a/lib/api/settings.rb b/lib/api/settings.rb
index 5362b3060c1..e3a8f0671ef 100644
--- a/lib/api/settings.rb
+++ b/lib/api/settings.rb
@@ -84,16 +84,7 @@ module API
optional :max_artifacts_size, type: Integer, desc: "Set the maximum file size for each job's artifacts"
optional :max_attachment_size, type: Integer, desc: 'Maximum attachment size in MB'
optional :max_pages_size, type: Integer, desc: 'Maximum size of pages in MB'
- optional :metrics_enabled, type: Boolean, desc: 'Enable the InfluxDB metrics'
- given metrics_enabled: ->(val) { val } do
- requires :metrics_host, type: String, desc: 'The InfluxDB host'
- requires :metrics_method_call_threshold, type: Integer, desc: 'A method call is only tracked when it takes longer to complete than the given amount of milliseconds.'
- requires :metrics_packet_size, type: Integer, desc: 'The amount of points to store in a single UDP packet'
- requires :metrics_pool_size, type: Integer, desc: 'The amount of InfluxDB connections to open'
- requires :metrics_port, type: Integer, desc: 'The UDP port to use for connecting to InfluxDB'
- requires :metrics_sample_interval, type: Integer, desc: 'The sampling interval in seconds'
- requires :metrics_timeout, type: Integer, desc: 'The amount of seconds after which an InfluxDB connection will time out'
- end
+ optional :metrics_method_call_threshold, type: Integer, desc: 'A method call is only tracked when it takes longer to complete than the given amount of milliseconds.'
optional :password_authentication_enabled, type: Boolean, desc: 'Flag indicating if password authentication is enabled for the web interface' # support legacy names, can be removed in v5
optional :password_authentication_enabled_for_web, type: Boolean, desc: 'Flag indicating if password authentication is enabled for the web interface'
mutually_exclusive :password_authentication_enabled_for_web, :password_authentication_enabled, :signin_enabled
@@ -153,6 +144,8 @@ module API
optional :snowplow_cookie_domain, type: String, desc: 'The Snowplow cookie domain'
optional :snowplow_app_id, type: String, desc: 'The Snowplow site name / application id'
end
+ optional :issues_create_limit, type: Integer, desc: "Maximum number of issue creation requests allowed per minute per user. Set to 0 for unlimited requests per minute."
+ optional :raw_blob_request_limit, type: Integer, desc: "Maximum number of requests per minute for each raw path. Set to 0 for unlimited requests per minute."
ApplicationSetting::SUPPORTED_KEY_TYPES.each do |type|
optional :"#{type}_key_restriction",
@@ -192,6 +185,9 @@ module API
attrs[:allow_local_requests_from_web_hooks_and_services] = attrs.delete(:allow_local_requests_from_hooks_and_services)
end
+ # since 13.0 it's not possible to disable hashed storage - support can be removed in 14.0
+ attrs.delete(:hashed_storage_enabled) if attrs.has_key?(:hashed_storage_enabled)
+
attrs = filter_attributes_using_license(attrs)
if ApplicationSettings::UpdateService.new(current_settings, current_user, attrs).execute
diff --git a/lib/api/snippets.rb b/lib/api/snippets.rb
index 0aaab9a812f..be58b832f97 100644
--- a/lib/api/snippets.rb
+++ b/lib/api/snippets.rb
@@ -8,6 +8,7 @@ module API
before { authenticate! }
resource :snippets do
+ helpers Helpers::SnippetsHelpers
helpers do
def snippets_for_current_user
SnippetsFinder.new(current_user, author: current_user).execute
@@ -24,13 +25,13 @@ module API
desc 'Get a snippets list for authenticated user' do
detail 'This feature was introduced in GitLab 8.15.'
- success Entities::PersonalSnippet
+ success Entities::Snippet
end
params do
use :pagination
end
get do
- present paginate(snippets_for_current_user), with: Entities::PersonalSnippet
+ present paginate(snippets_for_current_user), with: Entities::Snippet
end
desc 'List all public personal snippets current_user has access to' do
@@ -64,9 +65,9 @@ module API
success Entities::PersonalSnippet
end
params do
- requires :title, type: String, desc: 'The title of a snippet'
+ requires :title, type: String, allow_blank: false, desc: 'The title of a snippet'
requires :file_name, type: String, desc: 'The name of a snippet file'
- requires :content, type: String, desc: 'The content of a snippet'
+ requires :content, type: String, allow_blank: false, desc: 'The content of a snippet'
optional :description, type: String, desc: 'The description of a snippet'
optional :visibility, type: String,
values: Gitlab::VisibilityLevel.string_values,
@@ -80,12 +81,12 @@ module API
service_response = ::Snippets::CreateService.new(nil, current_user, attrs).execute
snippet = service_response.payload[:snippet]
- render_spam_error! if snippet.spam?
-
- if snippet.persisted?
+ if service_response.success?
present snippet, with: Entities::PersonalSnippet
else
- render_validation_error!(snippet)
+ render_spam_error! if snippet.spam?
+
+ render_api_error!({ error: service_response.message }, service_response.http_status)
end
end
@@ -95,9 +96,9 @@ module API
end
params do
requires :id, type: Integer, desc: 'The ID of a snippet'
- optional :title, type: String, desc: 'The title of a snippet'
+ optional :title, type: String, allow_blank: false, desc: 'The title of a snippet'
optional :file_name, type: String, desc: 'The name of a snippet file'
- optional :content, type: String, desc: 'The content of a snippet'
+ optional :content, type: String, allow_blank: false, desc: 'The content of a snippet'
optional :description, type: String, desc: 'The description of a snippet'
optional :visibility, type: String,
values: Gitlab::VisibilityLevel.string_values,
@@ -114,12 +115,12 @@ module API
service_response = ::Snippets::UpdateService.new(nil, current_user, attrs).execute(snippet)
snippet = service_response.payload[:snippet]
- render_spam_error! if snippet.spam?
-
- if snippet.persisted?
+ if service_response.success?
present snippet, with: Entities::PersonalSnippet
else
- render_validation_error!(snippet)
+ render_spam_error! if snippet.spam?
+
+ render_api_error!({ error: service_response.message }, service_response.http_status)
end
end
@@ -159,7 +160,7 @@ module API
env['api.format'] = :txt
content_type 'text/plain'
header['Content-Disposition'] = 'attachment'
- present snippet.content
+ present content_for(snippet)
end
desc 'Get the user agent details for a snippet' do
diff --git a/lib/api/wikis.rb b/lib/api/wikis.rb
index a2146406690..884e3019a2d 100644
--- a/lib/api/wikis.rb
+++ b/lib/api/wikis.rb
@@ -70,7 +70,7 @@ module API
post ':id/wikis' do
authorize! :create_wiki, user_project
- page = WikiPages::CreateService.new(user_project, current_user, params).execute
+ page = WikiPages::CreateService.new(container: user_project, current_user: current_user, params: params).execute
if page.valid?
present page, with: Entities::WikiPage
@@ -91,7 +91,7 @@ module API
put ':id/wikis/:slug' do
authorize! :create_wiki, user_project
- page = WikiPages::UpdateService.new(user_project, current_user, params).execute(wiki_page)
+ page = WikiPages::UpdateService.new(container: user_project, current_user: current_user, params: params).execute(wiki_page)
if page.valid?
present page, with: Entities::WikiPage
@@ -107,7 +107,7 @@ module API
delete ':id/wikis/:slug' do
authorize! :admin_wiki, user_project
- WikiPages::DestroyService.new(user_project, current_user).execute(wiki_page)
+ WikiPages::DestroyService.new(container: user_project, current_user: current_user).execute(wiki_page)
no_content!
end
@@ -123,9 +123,11 @@ module API
post ":id/wikis/attachments" do
authorize! :create_wiki, user_project
- result = ::Wikis::CreateAttachmentService.new(user_project,
- current_user,
- commit_params(declared_params(include_missing: false))).execute
+ result = ::Wikis::CreateAttachmentService.new(
+ container: user_project,
+ current_user: current_user,
+ params: commit_params(declared_params(include_missing: false))
+ ).execute
if result[:status] == :success
status(201)
diff --git a/lib/banzai/filter/issue_reference_filter.rb b/lib/banzai/filter/issue_reference_filter.rb
index 09a4d71b5f6..37e66387f2e 100644
--- a/lib/banzai/filter/issue_reference_filter.rb
+++ b/lib/banzai/filter/issue_reference_filter.rb
@@ -28,8 +28,24 @@ module Banzai
def parent_records(parent, ids)
parent.issues.where(iid: ids.to_a)
end
+
+ def object_link_text_extras(issue, matches)
+ super + design_link_extras(issue, matches.named_captures['path'])
+ end
+
+ private
+
+ def design_link_extras(issue, path)
+ if path == '/designs' && read_designs?(issue)
+ ['designs']
+ else
+ []
+ end
+ end
+
+ def read_designs?(issue)
+ Ability.allowed?(current_user, :read_design, issue)
+ end
end
end
end
-
-Banzai::Filter::IssueReferenceFilter.prepend_if_ee('EE::Banzai::Filter::IssueReferenceFilter')
diff --git a/lib/banzai/filter/upload_link_filter.rb b/lib/banzai/filter/upload_link_filter.rb
index 023c1288367..762371e1418 100644
--- a/lib/banzai/filter/upload_link_filter.rb
+++ b/lib/banzai/filter/upload_link_filter.rb
@@ -50,6 +50,10 @@ module Banzai
Addressable::URI.join(Gitlab.config.gitlab.base_url, path).to_s
end
+ if html_attr.name == 'href'
+ html_attr.parent.set_attribute('data-link', 'true')
+ end
+
html_attr.parent.add_class('gfm')
end
diff --git a/lib/banzai/filter/user_reference_filter.rb b/lib/banzai/filter/user_reference_filter.rb
index 8cda67867a8..9268ff1a827 100644
--- a/lib/banzai/filter/user_reference_filter.rb
+++ b/lib/banzai/filter/user_reference_filter.rb
@@ -106,7 +106,7 @@ module Banzai
end
def link_class
- reference_class(:project_member, tooltip: false)
+ [reference_class(:project_member, tooltip: false), "js-user-link"].join(" ")
end
def link_to_all(link_content: nil)
diff --git a/lib/banzai/pipeline.rb b/lib/banzai/pipeline.rb
index 8fdbc044861..01cadb11e83 100644
--- a/lib/banzai/pipeline.rb
+++ b/lib/banzai/pipeline.rb
@@ -9,7 +9,7 @@ module Banzai
# Examples:
# Pipeline[nil] # => Banzai::Pipeline::FullPipeline
# Pipeline[:label] # => Banzai::Pipeline::LabelPipeline
- # Pipeline[StatusPage::PostProcessPipeline] # => StatusPage::PostProcessPipeline
+ # Pipeline[StatusPage::Pipeline::PostProcessPipeline] # => StatusPage::Pipeline::PostProcessPipeline
#
# Pipeline['label'] # => raises ArgumentError - unsupport type
# Pipeline[Project] # => raises ArgumentError - not a subclass of BasePipeline
diff --git a/lib/banzai/reference_parser/design_parser.rb b/lib/banzai/reference_parser/design_parser.rb
new file mode 100644
index 00000000000..04e878756d8
--- /dev/null
+++ b/lib/banzai/reference_parser/design_parser.rb
@@ -0,0 +1,31 @@
+# frozen_string_literal: true
+
+module Banzai
+ module ReferenceParser
+ class DesignParser < BaseParser
+ self.reference_type = :design
+
+ def references_relation
+ DesignManagement::Design
+ end
+
+ def nodes_visible_to_user(user, nodes)
+ issues = issues_for_nodes(nodes)
+ issue_attr = 'data-issue'
+
+ nodes.select do |node|
+ if node.has_attribute?(issue_attr)
+ can?(user, :read_design, issues[node])
+ else
+ true
+ end
+ end
+ end
+
+ def issues_for_nodes(nodes)
+ relation = Issue.includes(project: [:project_feature])
+ grouped_objects_for_nodes(nodes, relation, 'data-issue')
+ end
+ end
+ end
+end
diff --git a/lib/banzai/renderer.rb b/lib/banzai/renderer.rb
index 3cb9ec21e8f..fbbd6135959 100644
--- a/lib/banzai/renderer.rb
+++ b/lib/banzai/renderer.rb
@@ -138,15 +138,18 @@ module Banzai
#
# html - String to process
# context - Hash of options to customize output
- # :pipeline - Symbol pipeline type
+ # :pipeline - Symbol pipeline type - for context transform only, defaults to :full
# :project - Project
# :user - User object
+ # :post_process_pipeline - pipeline to use for post_processing - defaults to PostProcessPipeline
#
# Returns an HTML-safe String
def self.post_process(html, context)
context = Pipeline[context[:pipeline]].transform_context(context)
- pipeline = Pipeline[:post_process]
+ # Use a passed class for the pipeline or default to PostProcessPipeline
+ pipeline = context.delete(:post_process_pipeline) || ::Banzai::Pipeline::PostProcessPipeline
+
if context[:xhtml]
pipeline.to_document(html, context).to_html(save_with: Nokogiri::XML::Node::SaveOptions::AS_XHTML)
else
diff --git a/lib/container_registry/client.rb b/lib/container_registry/client.rb
index 56f556c229a..118eb8e2d7c 100644
--- a/lib/container_registry/client.rb
+++ b/lib/container_registry/client.rb
@@ -13,6 +13,8 @@ module ContainerRegistry
DOCKER_DISTRIBUTION_MANIFEST_V2_TYPE = 'application/vnd.docker.distribution.manifest.v2+json'
OCI_MANIFEST_V1_TYPE = 'application/vnd.oci.image.manifest.v1+json'
CONTAINER_IMAGE_V1_TYPE = 'application/vnd.docker.container.image.v1+json'
+ REGISTRY_VERSION_HEADER = 'gitlab-container-registry-version'
+ REGISTRY_FEATURES_HEADER = 'gitlab-container-registry-features'
ACCEPTED_TYPES = [DOCKER_DISTRIBUTION_MANIFEST_V2_TYPE, OCI_MANIFEST_V1_TYPE].freeze
@@ -24,6 +26,21 @@ module ContainerRegistry
@options = options
end
+ def registry_info
+ response = faraday.get("/v2/")
+
+ return {} unless response&.success?
+
+ version = response.headers[REGISTRY_VERSION_HEADER]
+ features = response.headers.fetch(REGISTRY_FEATURES_HEADER, '')
+
+ {
+ version: version,
+ features: features.split(',').map(&:strip),
+ vendor: version ? 'gitlab' : 'other'
+ }
+ end
+
def repository_tags(name)
response_body faraday.get("/v2/#{name}/tags/list")
end
@@ -83,7 +100,7 @@ module ContainerRegistry
image = {
config: {}
}
- image, image_digest = upload_raw_blob(path, JSON.pretty_generate(image))
+ image, image_digest = upload_raw_blob(path, Gitlab::Json.pretty_generate(image))
return unless image
{
@@ -109,7 +126,7 @@ module ContainerRegistry
def put_tag(name, reference, manifest)
response = faraday.put("/v2/#{name}/manifests/#{reference}") do |req|
req.headers['Content-Type'] = DOCKER_DISTRIBUTION_MANIFEST_V2_TYPE
- req.body = JSON.pretty_generate(manifest)
+ req.body = Gitlab::Json.pretty_generate(manifest)
end
response.headers['docker-content-digest'] if response.success?
diff --git a/lib/container_registry/config.rb b/lib/container_registry/config.rb
index 740c0e13da0..40dd92befd2 100644
--- a/lib/container_registry/config.rb
+++ b/lib/container_registry/config.rb
@@ -6,7 +6,7 @@ module ContainerRegistry
def initialize(tag, blob)
@tag, @blob = tag, blob
- @data = JSON.parse(blob.data)
+ @data = Gitlab::Json.parse(blob.data)
end
def [](key)
diff --git a/lib/csv_builder.rb b/lib/csv_builder.rb
index 7df4e3bf85d..a9ef5a83ae8 100644
--- a/lib/csv_builder.rb
+++ b/lib/csv_builder.rb
@@ -14,6 +14,9 @@
# CsvBuilder.new(@posts, columns).render
#
class CsvBuilder
+ DEFAULT_ORDER_BY = 'id'.freeze
+ DEFAULT_BATCH_SIZE = 1000
+
attr_reader :rows_written
#
@@ -68,6 +71,12 @@ class CsvBuilder
}
end
+ protected
+
+ def each(&block)
+ @collection.find_each(&block) # rubocop: disable CodeReuse/ActiveRecord
+ end
+
private
def headers
@@ -91,7 +100,7 @@ class CsvBuilder
def write_csv(csv, until_condition:)
csv << headers
- @collection.find_each do |object|
+ each do |object|
csv << row(object)
@rows_written += 1
diff --git a/lib/csv_builders/single_batch.rb b/lib/csv_builders/single_batch.rb
new file mode 100644
index 00000000000..bed6b7424b3
--- /dev/null
+++ b/lib/csv_builders/single_batch.rb
@@ -0,0 +1,11 @@
+# frozen_string_literal: true
+
+module CsvBuilders
+ class SingleBatch < CsvBuilder
+ protected
+
+ def each(&block)
+ @collection.each(&block)
+ end
+ end
+end
diff --git a/lib/declarative_policy.rb b/lib/declarative_policy.rb
index e51f30af581..bd1c121fe79 100644
--- a/lib/declarative_policy.rb
+++ b/lib/declarative_policy.rb
@@ -72,18 +72,17 @@ module DeclarativePolicy
end
def compute_class_for_class(subject_class)
+ if subject_class.respond_to?(:declarative_policy_class)
+ return subject_class.declarative_policy_class.constantize
+ end
+
subject_class.ancestors.each do |klass|
- next unless klass.name
+ name = klass.name
+
+ next unless name
begin
- klass_name =
- if subject_class.respond_to?(:declarative_policy_class)
- subject_class.declarative_policy_class
- else
- "#{klass.name}Policy"
- end
-
- policy_class = klass_name.constantize
+ policy_class = "#{name}Policy".constantize
# NOTE: the < operator here tests whether policy_class
# inherits from Base. We can't use #is_a? because that
diff --git a/lib/feature.rb b/lib/feature.rb
index 60a5c03a839..dc7e8da8f35 100644
--- a/lib/feature.rb
+++ b/lib/feature.rb
@@ -134,11 +134,7 @@ class Feature
end
def l1_cache_backend
- if Gitlab::Utils.to_boolean(ENV['USE_THREAD_MEMORY_CACHE'])
- Gitlab::ThreadMemoryCache.cache_backend
- else
- Gitlab::ProcessMemoryCache.cache_backend
- end
+ Gitlab::ProcessMemoryCache.cache_backend
end
def l2_cache_backend
diff --git a/lib/gitlab/alert_management/alert_params.rb b/lib/gitlab/alert_management/alert_params.rb
new file mode 100644
index 00000000000..982479784a9
--- /dev/null
+++ b/lib/gitlab/alert_management/alert_params.rb
@@ -0,0 +1,41 @@
+# frozen_string_literal: true
+
+module Gitlab
+ module AlertManagement
+ class AlertParams
+ MONITORING_TOOLS = {
+ prometheus: 'Prometheus'
+ }.freeze
+
+ def self.from_generic_alert(project:, payload:)
+ parsed_payload = Gitlab::Alerting::NotificationPayloadParser.call(payload).with_indifferent_access
+ annotations = parsed_payload[:annotations]
+
+ {
+ project_id: project.id,
+ title: annotations[:title],
+ description: annotations[:description],
+ monitoring_tool: annotations[:monitoring_tool],
+ service: annotations[:service],
+ hosts: Array(annotations[:hosts]),
+ payload: payload,
+ started_at: parsed_payload['startsAt'],
+ severity: annotations[:severity]
+ }
+ end
+
+ def self.from_prometheus_alert(project:, parsed_alert:)
+ {
+ project_id: project.id,
+ title: parsed_alert.title,
+ description: parsed_alert.description,
+ monitoring_tool: MONITORING_TOOLS[:prometheus],
+ payload: parsed_alert.payload,
+ started_at: parsed_alert.starts_at,
+ ended_at: parsed_alert.ends_at,
+ fingerprint: parsed_alert.gitlab_fingerprint
+ }
+ end
+ end
+ end
+end
diff --git a/lib/gitlab/alert_management/alert_status_counts.rb b/lib/gitlab/alert_management/alert_status_counts.rb
new file mode 100644
index 00000000000..382026236e0
--- /dev/null
+++ b/lib/gitlab/alert_management/alert_status_counts.rb
@@ -0,0 +1,53 @@
+# frozen_string_literal: true
+
+module Gitlab
+ module AlertManagement
+ # Represents counts of each status or category of statuses
+ class AlertStatusCounts
+ include Gitlab::Utils::StrongMemoize
+
+ STATUSES = ::AlertManagement::Alert::STATUSES
+
+ attr_reader :project
+
+ def self.declarative_policy_class
+ 'AlertManagement::AlertPolicy'
+ end
+
+ def initialize(current_user, project, params)
+ @project = project
+ @current_user = current_user
+ @params = params
+ end
+
+ # Define method for each status
+ STATUSES.each_key do |status|
+ define_method(status) { counts[status] }
+ end
+
+ def open
+ counts[:triggered] + counts[:acknowledged]
+ end
+
+ def all
+ counts.values.sum # rubocop:disable CodeReuse/ActiveRecord
+ end
+
+ private
+
+ attr_reader :current_user, :params
+
+ def counts
+ strong_memoize(:counts) do
+ Hash.new(0).merge(counts_by_status)
+ end
+ end
+
+ def counts_by_status
+ ::AlertManagement::AlertsFinder
+ .counts_by_status(current_user, project, params)
+ .transform_keys { |status_id| STATUSES.key(status_id) }
+ end
+ end
+ end
+end
diff --git a/lib/gitlab/alerting/alert.rb b/lib/gitlab/alerting/alert.rb
index 7d97bd1bb52..d859ca89418 100644
--- a/lib/gitlab/alerting/alert.rb
+++ b/lib/gitlab/alerting/alert.rb
@@ -105,6 +105,10 @@ module Gitlab
metric_id.present?
end
+ def gitlab_fingerprint
+ Digest::SHA1.hexdigest(plain_gitlab_fingerprint)
+ end
+
def valid?
payload.respond_to?(:dig) && project && title && starts_at
end
@@ -115,6 +119,14 @@ module Gitlab
private
+ def plain_gitlab_fingerprint
+ if gitlab_managed?
+ [metric_id, starts_at].join('/')
+ else # self managed
+ [starts_at, title, full_query].join('/')
+ end
+ end
+
def parse_environment_from_payload
environment_name = payload&.dig('labels', 'gitlab_environment_name')
diff --git a/lib/gitlab/alerting/notification_payload_parser.rb b/lib/gitlab/alerting/notification_payload_parser.rb
index a54bb44d66a..c79d69613f3 100644
--- a/lib/gitlab/alerting/notification_payload_parser.rb
+++ b/lib/gitlab/alerting/notification_payload_parser.rb
@@ -6,6 +6,7 @@ module Gitlab
BadPayloadError = Class.new(StandardError)
DEFAULT_TITLE = 'New: Incident'
+ DEFAULT_SEVERITY = 'critical'
def initialize(payload)
@payload = payload.to_h.with_indifferent_access
@@ -30,6 +31,10 @@ module Gitlab
payload[:title].presence || DEFAULT_TITLE
end
+ def severity
+ payload[:severity].presence || DEFAULT_SEVERITY
+ end
+
def annotations
primary_params
.reverse_merge(flatten_secondary_params)
@@ -43,7 +48,8 @@ module Gitlab
'description' => payload[:description],
'monitoring_tool' => payload[:monitoring_tool],
'service' => payload[:service],
- 'hosts' => hosts.presence
+ 'hosts' => hosts.presence,
+ 'severity' => severity
}
end
diff --git a/lib/gitlab/analytics/cycle_analytics/median.rb b/lib/gitlab/analytics/cycle_analytics/median.rb
index 41883a80338..9fcaeadf351 100644
--- a/lib/gitlab/analytics/cycle_analytics/median.rb
+++ b/lib/gitlab/analytics/cycle_analytics/median.rb
@@ -15,7 +15,11 @@ module Gitlab
@query = @query.select(median_duration_in_seconds.as('median'))
result = execute_query(@query).first || {}
- result['median'] ? result['median'].to_i : nil
+ result['median'] || nil
+ end
+
+ def days
+ seconds ? seconds.fdiv(1.day) : nil
end
private
diff --git a/lib/gitlab/auth.rb b/lib/gitlab/auth.rb
index 8e14d21f591..44e8c9c04b9 100644
--- a/lib/gitlab/auth.rb
+++ b/lib/gitlab/auth.rb
@@ -337,6 +337,10 @@ module Gitlab
REGISTRY_SCOPES
end
+ def resource_bot_scopes
+ Gitlab::Auth::API_SCOPES + Gitlab::Auth::REPOSITORY_SCOPES + Gitlab::Auth.registry_scopes - [:read_user]
+ end
+
private
def non_admin_available_scopes
diff --git a/lib/gitlab/auth/auth_finders.rb b/lib/gitlab/auth/auth_finders.rb
index f0ca6491bd0..b7e78189d37 100644
--- a/lib/gitlab/auth/auth_finders.rb
+++ b/lib/gitlab/auth/auth_finders.rb
@@ -18,8 +18,6 @@ module Gitlab
end
module AuthFinders
- prepend_if_ee('::EE::Gitlab::Auth::AuthFinders') # rubocop: disable Cop/InjectEnterpriseEditionModule
-
include Gitlab::Utils::StrongMemoize
include ActionController::HttpAuthentication::Basic
@@ -27,6 +25,7 @@ module Gitlab
PRIVATE_TOKEN_PARAM = :private_token
JOB_TOKEN_HEADER = 'HTTP_JOB_TOKEN'.freeze
JOB_TOKEN_PARAM = :job_token
+ DEPLOY_TOKEN_HEADER = 'HTTP_DEPLOY_TOKEN'.freeze
RUNNER_TOKEN_PARAM = :token
RUNNER_JOB_TOKEN_PARAM = :token
@@ -103,6 +102,25 @@ module Gitlab
access_token.user || raise(UnauthorizedError)
end
+ # This returns a deploy token, not a user since a deploy token does not
+ # belong to a user.
+ #
+ # deploy tokens are accepted with deploy token headers and basic auth headers
+ def deploy_token_from_request
+ return unless route_authentication_setting[:deploy_token_allowed]
+
+ token = current_request.env[DEPLOY_TOKEN_HEADER].presence || parsed_oauth_token
+
+ if has_basic_credentials?(current_request)
+ _, token = user_name_and_password(current_request)
+ end
+
+ deploy_token = DeployToken.active.find_by_token(token)
+ @current_authenticated_deploy_token = deploy_token # rubocop:disable Gitlab/ModuleWithInstanceVariables
+
+ deploy_token
+ end
+
def find_runner_from_token
return unless api_request?
@@ -113,6 +131,9 @@ module Gitlab
end
def validate_access_token!(scopes: [])
+ # return early if we've already authenticated via a deploy token
+ return if @current_authenticated_deploy_token.present? # rubocop:disable Gitlab/ModuleWithInstanceVariables
+
return unless access_token
case AccessTokenValidationService.new(access_token, request: request).validate(scopes: scopes)
@@ -249,3 +270,5 @@ module Gitlab
end
end
end
+
+Gitlab::Auth::AuthFinders.prepend_if_ee('::EE::Gitlab::Auth::AuthFinders')
diff --git a/lib/gitlab/auth/ldap/access.rb b/lib/gitlab/auth/ldap/access.rb
index 98eec0e4a7b..66d20ee2b59 100644
--- a/lib/gitlab/auth/ldap/access.rb
+++ b/lib/gitlab/auth/ldap/access.rb
@@ -8,8 +8,6 @@ module Gitlab
module Auth
module Ldap
class Access
- prepend_if_ee('::EE::Gitlab::Auth::Ldap::Access') # rubocop: disable Cop/InjectEnterpriseEditionModule
-
attr_reader :provider, :user, :ldap_identity
def self.open(user, &block)
@@ -118,3 +116,5 @@ module Gitlab
end
end
end
+
+Gitlab::Auth::Ldap::Access.prepend_if_ee('::EE::Gitlab::Auth::Ldap::Access')
diff --git a/lib/gitlab/auth/ldap/adapter.rb b/lib/gitlab/auth/ldap/adapter.rb
index c5ec4e1981b..f64fcd822c6 100644
--- a/lib/gitlab/auth/ldap/adapter.rb
+++ b/lib/gitlab/auth/ldap/adapter.rb
@@ -4,8 +4,6 @@ module Gitlab
module Auth
module Ldap
class Adapter
- prepend_if_ee('::EE::Gitlab::Auth::Ldap::Adapter') # rubocop: disable Cop/InjectEnterpriseEditionModule
-
SEARCH_RETRY_FACTOR = [1, 1, 2, 3].freeze
MAX_SEARCH_RETRIES = Rails.env.test? ? 1 : SEARCH_RETRY_FACTOR.size.freeze
@@ -142,3 +140,5 @@ module Gitlab
end
end
end
+
+Gitlab::Auth::Ldap::Adapter.prepend_if_ee('::EE::Gitlab::Auth::Ldap::Adapter')
diff --git a/lib/gitlab/auth/ldap/config.rb b/lib/gitlab/auth/ldap/config.rb
index b8874e18a0b..7677189eb9f 100644
--- a/lib/gitlab/auth/ldap/config.rb
+++ b/lib/gitlab/auth/ldap/config.rb
@@ -5,8 +5,6 @@ module Gitlab
module Auth
module Ldap
class Config
- prepend_if_ee('::EE::Gitlab::Auth::Ldap::Config') # rubocop: disable Cop/InjectEnterpriseEditionModule
-
NET_LDAP_ENCRYPTION_METHOD = {
simple_tls: :simple_tls,
start_tls: :start_tls,
@@ -288,3 +286,5 @@ module Gitlab
end
end
end
+
+Gitlab::Auth::Ldap::Config.prepend_if_ee('::EE::Gitlab::Auth::Ldap::Config')
diff --git a/lib/gitlab/auth/ldap/person.rb b/lib/gitlab/auth/ldap/person.rb
index 430f94a9a28..e4a4900c37a 100644
--- a/lib/gitlab/auth/ldap/person.rb
+++ b/lib/gitlab/auth/ldap/person.rb
@@ -4,8 +4,6 @@ module Gitlab
module Auth
module Ldap
class Person
- prepend_if_ee('::EE::Gitlab::Auth::Ldap::Person') # rubocop: disable Cop/InjectEnterpriseEditionModule
-
# Active Directory-specific LDAP filter that checks if bit 2 of the
# userAccountControl attribute is set.
# Source: http://ctogonewild.com/2009/09/03/bitmask-searches-in-ldap/
@@ -122,3 +120,5 @@ module Gitlab
end
end
end
+
+Gitlab::Auth::Ldap::Person.prepend_if_ee('::EE::Gitlab::Auth::Ldap::Person')
diff --git a/lib/gitlab/auth/ldap/user.rb b/lib/gitlab/auth/ldap/user.rb
index df14e5fc3dc..1405fb4ab95 100644
--- a/lib/gitlab/auth/ldap/user.rb
+++ b/lib/gitlab/auth/ldap/user.rb
@@ -11,8 +11,6 @@ module Gitlab
module Ldap
class User < Gitlab::Auth::OAuth::User
extend ::Gitlab::Utils::Override
- prepend_if_ee('::EE::Gitlab::Auth::Ldap::User') # rubocop: disable Cop/InjectEnterpriseEditionModule
-
class << self
# rubocop: disable CodeReuse/ActiveRecord
def find_by_uid_and_provider(uid, provider)
@@ -64,3 +62,5 @@ module Gitlab
end
end
end
+
+Gitlab::Auth::Ldap::User.prepend_if_ee('::EE::Gitlab::Auth::Ldap::User')
diff --git a/lib/gitlab/auth/o_auth/auth_hash.rb b/lib/gitlab/auth/o_auth/auth_hash.rb
index b37a9225dd7..46ff6b2ccab 100644
--- a/lib/gitlab/auth/o_auth/auth_hash.rb
+++ b/lib/gitlab/auth/o_auth/auth_hash.rb
@@ -6,8 +6,6 @@ module Gitlab
module Auth
module OAuth
class AuthHash
- prepend_if_ee('::EE::Gitlab::Auth::OAuth::AuthHash') # rubocop: disable Cop/InjectEnterpriseEditionModule
-
attr_reader :auth_hash
def initialize(auth_hash)
@auth_hash = auth_hash
@@ -93,3 +91,5 @@ module Gitlab
end
end
end
+
+Gitlab::Auth::OAuth::AuthHash.prepend_if_ee('::EE::Gitlab::Auth::OAuth::AuthHash')
diff --git a/lib/gitlab/auth/o_auth/provider.rb b/lib/gitlab/auth/o_auth/provider.rb
index f0811098b15..6d699d37a8c 100644
--- a/lib/gitlab/auth/o_auth/provider.rb
+++ b/lib/gitlab/auth/o_auth/provider.rb
@@ -66,7 +66,10 @@ module Gitlab
nil
end
else
- Gitlab.config.omniauth.providers.find { |provider| provider.name == name }
+ provider = Gitlab.config.omniauth.providers.find { |provider| provider.name == name }
+ merge_provider_args_with_defaults!(provider)
+
+ provider
end
end
@@ -81,6 +84,15 @@ module Gitlab
config = config_for(name)
config && config['icon']
end
+
+ def self.merge_provider_args_with_defaults!(provider)
+ return unless provider
+
+ provider['args'] ||= {}
+
+ defaults = Gitlab::OmniauthInitializer.default_arguments_for(provider['name'])
+ provider['args'].deep_merge!(defaults.deep_stringify_keys)
+ end
end
end
end
diff --git a/lib/gitlab/auth/o_auth/user.rb b/lib/gitlab/auth/o_auth/user.rb
index df595da1536..8a60d6ef482 100644
--- a/lib/gitlab/auth/o_auth/user.rb
+++ b/lib/gitlab/auth/o_auth/user.rb
@@ -9,8 +9,6 @@ module Gitlab
module Auth
module OAuth
class User
- prepend_if_ee('::EE::Gitlab::Auth::OAuth::User') # rubocop: disable Cop/InjectEnterpriseEditionModule
-
SignupDisabledError = Class.new(StandardError)
SigninDisabledForProviderError = Class.new(StandardError)
@@ -275,3 +273,5 @@ module Gitlab
end
end
end
+
+Gitlab::Auth::OAuth::User.prepend_if_ee('::EE::Gitlab::Auth::OAuth::User')
diff --git a/lib/gitlab/auth/result.rb b/lib/gitlab/auth/result.rb
index 0fe91f9f3c8..757a0e671c3 100644
--- a/lib/gitlab/auth/result.rb
+++ b/lib/gitlab/auth/result.rb
@@ -3,8 +3,6 @@
module Gitlab
module Auth
Result = Struct.new(:actor, :project, :type, :authentication_abilities) do
- prepend_if_ee('::EE::Gitlab::Auth::Result') # rubocop: disable Cop/InjectEnterpriseEditionModule
-
def ci?(for_project)
type == :ci &&
project &&
@@ -26,3 +24,5 @@ module Gitlab
end
end
end
+
+Gitlab::Auth::Result.prepend_if_ee('::EE::Gitlab::Auth::Result')
diff --git a/lib/gitlab/auth/saml/config.rb b/lib/gitlab/auth/saml/config.rb
index ed2f3f158c1..67a53fa3205 100644
--- a/lib/gitlab/auth/saml/config.rb
+++ b/lib/gitlab/auth/saml/config.rb
@@ -4,8 +4,6 @@ module Gitlab
module Auth
module Saml
class Config
- prepend_if_ee('::EE::Gitlab::Auth::Saml::Config') # rubocop: disable Cop/InjectEnterpriseEditionModule
-
class << self
def options
Gitlab::Auth::OAuth::Provider.config_for('saml')
@@ -31,3 +29,5 @@ module Gitlab
end
end
end
+
+Gitlab::Auth::Saml::Config.prepend_if_ee('::EE::Gitlab::Auth::Saml::Config')
diff --git a/lib/gitlab/auth/saml/user.rb b/lib/gitlab/auth/saml/user.rb
index 1ba36ad95b4..37bc3f9bed0 100644
--- a/lib/gitlab/auth/saml/user.rb
+++ b/lib/gitlab/auth/saml/user.rb
@@ -9,8 +9,6 @@ module Gitlab
module Auth
module Saml
class User < Gitlab::Auth::OAuth::User
- prepend_if_ee('::EE::Gitlab::Auth::Saml::User') # rubocop: disable Cop/InjectEnterpriseEditionModule
-
extend ::Gitlab::Utils::Override
def save
@@ -63,3 +61,5 @@ module Gitlab
end
end
end
+
+Gitlab::Auth::Saml::User.prepend_if_ee('::EE::Gitlab::Auth::Saml::User')
diff --git a/lib/gitlab/background_migration/backfill_environment_id_deployment_merge_requests.rb b/lib/gitlab/background_migration/backfill_environment_id_deployment_merge_requests.rb
new file mode 100644
index 00000000000..4fd3b81fda3
--- /dev/null
+++ b/lib/gitlab/background_migration/backfill_environment_id_deployment_merge_requests.rb
@@ -0,0 +1,47 @@
+# frozen_string_literal: true
+
+module Gitlab
+ module BackgroundMigration
+ # BackfillEnvironmentIdDeploymentMergeRequests deletes duplicates
+ # from deployment_merge_requests table and backfills environment_id
+ class BackfillEnvironmentIdDeploymentMergeRequests
+ def perform(_start_mr_id, _stop_mr_id)
+ # no-op
+
+ # Background migration removed due to
+ # https://gitlab.com/gitlab-org/gitlab/-/issues/217191
+ end
+
+ def backfill_range(start_mr_id, stop_mr_id)
+ start_mr_id = Integer(start_mr_id)
+ stop_mr_id = Integer(stop_mr_id)
+
+ ActiveRecord::Base.connection.execute(<<~SQL)
+ DELETE FROM deployment_merge_requests
+ WHERE (deployment_id, merge_request_id) in (
+ SELECT t.deployment_id, t.merge_request_id FROM (
+ SELECT mrd.merge_request_id, mrd.deployment_id, ROW_NUMBER() OVER w AS rnum
+ FROM deployment_merge_requests as mrd
+ INNER JOIN "deployments" ON "deployments"."id" = "mrd"."deployment_id"
+ WHERE mrd.merge_request_id BETWEEN #{start_mr_id} AND #{stop_mr_id}
+ WINDOW w AS (
+ PARTITION BY merge_request_id, deployments.environment_id
+ ORDER BY deployments.id
+ )
+ ) t
+ WHERE t.rnum > 1
+ );
+ SQL
+
+ ActiveRecord::Base.connection.execute(<<~SQL)
+ UPDATE deployment_merge_requests
+ SET environment_id = deployments.environment_id
+ FROM deployments
+ WHERE deployments.id = "deployment_merge_requests".deployment_id
+ AND "deployment_merge_requests".environment_id IS NULL
+ AND "deployment_merge_requests".merge_request_id BETWEEN #{start_mr_id} AND #{stop_mr_id}
+ SQL
+ end
+ end
+ end
+end
diff --git a/lib/gitlab/background_migration/backfill_snippet_repositories.rb b/lib/gitlab/background_migration/backfill_snippet_repositories.rb
index fa6453abefb..21538000fec 100644
--- a/lib/gitlab/background_migration/backfill_snippet_repositories.rb
+++ b/lib/gitlab/background_migration/backfill_snippet_repositories.rb
@@ -8,22 +8,41 @@ module Gitlab
MAX_RETRIES = 2
def perform(start_id, stop_id)
- Snippet.includes(:author, snippet_repository: :shard).where(id: start_id..stop_id).find_each do |snippet|
+ snippets = snippet_relation.where(id: start_id..stop_id)
+
+ migrate_snippets(snippets)
+ end
+
+ def perform_by_ids(snippet_ids)
+ snippets = snippet_relation.where(id: snippet_ids)
+
+ migrate_snippets(snippets)
+ end
+
+ private
+
+ def migrate_snippets(snippets)
+ snippets.find_each do |snippet|
# We need to expire the exists? value for the cached method in case it was cached
snippet.repository.expire_exists_cache
next if repository_present?(snippet)
retry_index = 0
+ @invalid_path_error = false
+ @invalid_signature_error = false
begin
create_repository_and_files(snippet)
logger.info(message: 'Snippet Migration: repository created and migrated', snippet: snippet.id)
rescue => e
+ set_file_path_error(e)
+ set_signature_error(e)
+
retry_index += 1
- retry if retry_index < MAX_RETRIES
+ retry if retry_index < max_retries
logger.error(message: "Snippet Migration: error migrating snippet. Reason: #{e.message}", snippet: snippet.id)
@@ -33,7 +52,9 @@ module Gitlab
end
end
- private
+ def snippet_relation
+ @snippet_relation ||= Snippet.includes(:author, snippet_repository: :shard)
+ end
def repository_present?(snippet)
snippet.snippet_repository && !snippet.empty_repo?
@@ -44,16 +65,19 @@ module Gitlab
create_commit(snippet)
end
+ # Removing the db record
def destroy_snippet_repository(snippet)
- # Removing the db record
- snippet.snippet_repository&.destroy
+ snippet.snippet_repository&.delete
rescue => e
logger.error(message: "Snippet Migration: error destroying snippet repository. Reason: #{e.message}", snippet: snippet.id)
end
+ # Removing the repository in disk
def delete_repository(snippet)
- # Removing the repository in disk
- snippet.repository.remove if snippet.repository_exists?
+ return unless snippet.repository_exists?
+
+ snippet.repository.remove
+ snippet.repository.expire_exists_cache
rescue => e
logger.error(message: "Snippet Migration: error deleting repository. Reason: #{e.message}", snippet: snippet.id)
end
@@ -70,7 +94,10 @@ module Gitlab
end
def filename(snippet)
- snippet.file_name.presence || empty_file_name
+ file_name = snippet.file_name
+ file_name = file_name.parameterize if @invalid_path_error
+
+ file_name.presence || empty_file_name
end
def empty_file_name
@@ -82,7 +109,56 @@ module Gitlab
end
def create_commit(snippet)
- snippet.snippet_repository.multi_files_action(snippet.author, snippet_action(snippet), commit_attrs)
+ snippet.snippet_repository.multi_files_action(commit_author(snippet), snippet_action(snippet), commit_attrs)
+ end
+
+ # If the user is not allowed to access git or update the snippet
+ # because it is blocked, internal, ghost, ... we cannot commit
+ # files because these users are not allowed to, but we need to
+ # migrate their snippets as well.
+ # In this scenario the migration bot user will be the one that will commit the files.
+ def commit_author(snippet)
+ return migration_bot_user if snippet_content_size_over_limit?(snippet)
+ return migration_bot_user if @invalid_signature_error
+
+ if Gitlab::UserAccessSnippet.new(snippet.author, snippet: snippet).can_do_action?(:update_snippet)
+ snippet.author
+ else
+ migration_bot_user
+ end
+ end
+
+ def migration_bot_user
+ @migration_bot_user ||= User.migration_bot
+ end
+
+ # We sometimes receive invalid path errors from Gitaly if the Snippet filename
+ # cannot be parsed into a valid git path.
+ # In this situation, we need to parameterize the file name of the Snippet so that
+ # the migration can succeed, to achieve that, we'll identify in migration retries
+ # that the path is invalid
+ def set_file_path_error(error)
+ @invalid_path_error ||= error.is_a?(SnippetRepository::InvalidPathError)
+ end
+
+ # We sometimes receive invalid signature from Gitaly if the commit author
+ # name or email is invalid to create the commit signature.
+ # In this situation, we set the error and use the migration_bot since
+ # the information used to build it is valid
+ def set_signature_error(error)
+ @invalid_signature_error ||= error.is_a?(SnippetRepository::InvalidSignatureError)
+ end
+
+ # In the case where the snippet file_name is invalid and also the
+ # snippet author has invalid commit info, we need to increase the
+ # number of retries by 1, because we will receive two errors
+ # from Gitaly and, in the third one, we will commit successfully.
+ def max_retries
+ MAX_RETRIES + (@invalid_signature_error && @invalid_path_error ? 1 : 0)
+ end
+
+ def snippet_content_size_over_limit?(snippet)
+ snippet.content.size > Gitlab::CurrentSettings.snippet_size_limit
end
end
end
diff --git a/lib/gitlab/background_migration/migrate_issue_trackers_sensitive_data.rb b/lib/gitlab/background_migration/migrate_issue_trackers_sensitive_data.rb
index 14e14f28439..956f9daa493 100644
--- a/lib/gitlab/background_migration/migrate_issue_trackers_sensitive_data.rb
+++ b/lib/gitlab/background_migration/migrate_issue_trackers_sensitive_data.rb
@@ -79,7 +79,7 @@ module Gitlab
data = { 'jira_tracker_data' => [], 'issue_tracker_data' => [] }
select_all(query).each do |service|
begin
- properties = JSON.parse(service['properties'])
+ properties = Gitlab::Json.parse(service['properties'])
rescue JSON::ParserError
logger.warn(
message: 'Properties data not parsed - invalid json',
diff --git a/lib/gitlab/background_migration/populate_user_highest_roles_table.rb b/lib/gitlab/background_migration/populate_user_highest_roles_table.rb
index 0c9e15b5a80..16386ebf9c3 100644
--- a/lib/gitlab/background_migration/populate_user_highest_roles_table.rb
+++ b/lib/gitlab/background_migration/populate_user_highest_roles_table.rb
@@ -20,6 +20,8 @@ module Gitlab
end
def perform(from_id, to_id)
+ return unless User.column_names.include?('bot_type')
+
(from_id..to_id).each_slice(BATCH_SIZE) do |ids|
execute(
<<-EOF
diff --git a/lib/gitlab/background_migration/remove_undefined_occurrence_confidence_level.rb b/lib/gitlab/background_migration/remove_undefined_occurrence_confidence_level.rb
new file mode 100644
index 00000000000..3920e8dc2de
--- /dev/null
+++ b/lib/gitlab/background_migration/remove_undefined_occurrence_confidence_level.rb
@@ -0,0 +1,13 @@
+# frozen_string_literal: true
+# rubocop:disable Style/Documentation
+
+module Gitlab
+ module BackgroundMigration
+ class RemoveUndefinedOccurrenceConfidenceLevel
+ def perform(start_id, stop_id)
+ end
+ end
+ end
+end
+
+Gitlab::BackgroundMigration::RemoveUndefinedOccurrenceConfidenceLevel.prepend_if_ee('EE::Gitlab::BackgroundMigration::RemoveUndefinedOccurrenceConfidenceLevel')
diff --git a/lib/gitlab/background_migration/remove_undefined_vulnerability_confidence_level.rb b/lib/gitlab/background_migration/remove_undefined_vulnerability_confidence_level.rb
new file mode 100644
index 00000000000..f6ea61f4502
--- /dev/null
+++ b/lib/gitlab/background_migration/remove_undefined_vulnerability_confidence_level.rb
@@ -0,0 +1,13 @@
+# frozen_string_literal: true
+# rubocop:disable Style/Documentation
+
+module Gitlab
+ module BackgroundMigration
+ class RemoveUndefinedVulnerabilityConfidenceLevel
+ def perform(start_id, stop_id)
+ end
+ end
+ end
+end
+
+Gitlab::BackgroundMigration::RemoveUndefinedVulnerabilityConfidenceLevel.prepend_if_ee('EE::Gitlab::BackgroundMigration::RemoveUndefinedVulnerabilityConfidenceLevel')
diff --git a/lib/gitlab/blob_helper.rb b/lib/gitlab/blob_helper.rb
index fc579ad8d2a..57d632afd74 100644
--- a/lib/gitlab/blob_helper.rb
+++ b/lib/gitlab/blob_helper.rb
@@ -3,6 +3,8 @@
# This has been extracted from https://github.com/github/linguist/blob/master/lib/linguist/blob_helper.rb
module Gitlab
module BlobHelper
+ include Gitlab::Utils::StrongMemoize
+
def extname
File.extname(name.to_s)
end
@@ -120,8 +122,18 @@ module Gitlab
end
def encoded_newlines_re
- @encoded_newlines_re ||=
- Regexp.union(["\r\n", "\r", "\n"].map { |nl| nl.encode(ruby_encoding, "ASCII-8BIT").force_encoding(data.encoding) })
+ strong_memoize(:encoded_newlines_re) do
+ newlines = ["\r\n", "\r", "\n"]
+ data_encoding = data&.encoding
+
+ if ruby_encoding && data_encoding
+ newlines.map! do |nl|
+ nl.encode(ruby_encoding, "ASCII-8BIT").force_encoding(data_encoding)
+ end
+ end
+
+ Regexp.union(newlines)
+ end
end
def ruby_encoding
diff --git a/lib/gitlab/chat/responder/mattermost.rb b/lib/gitlab/chat/responder/mattermost.rb
new file mode 100644
index 00000000000..0488c98e422
--- /dev/null
+++ b/lib/gitlab/chat/responder/mattermost.rb
@@ -0,0 +1,131 @@
+# frozen_string_literal: true
+
+module Gitlab
+ module Chat
+ module Responder
+ class Mattermost < Responder::Base
+ SUCCESS_COLOR = '#00c100'
+ FAILURE_COLOR = '#e40303'
+
+ # Slack breaks messages apart if they're around 4 KB in size. We use a
+ # slightly smaller limit here to account for user mentions.
+ MESSAGE_SIZE_LIMIT = 3.5.kilobytes
+
+ # Sends a response back to Mattermost
+ #
+ # body - The message payload to send back to Mattermost, as a Hash.
+ def send_response(body)
+ Gitlab::HTTP.post(
+ pipeline.chat_data.response_url,
+ {
+ headers: { 'Content-Type': 'application/json' },
+ body: body.to_json
+ }
+ )
+ end
+
+ # Sends the output for a build that completed successfully.
+ #
+ # output - The output produced by the chat command.
+ def success(output)
+ return if output.empty?
+
+ send_response(
+ response_type: :in_channel,
+ attachments: [
+ {
+ color: SUCCESS_COLOR,
+ text: "ChatOps job started by #{user_ref} completed successfully",
+ fields: [
+ {
+ short: true,
+ title: "ID",
+ value: "#{build_ref}"
+ },
+ {
+ short: true,
+ title: "Name",
+ value: build.name
+ },
+ {
+ short: false,
+ title: "Output",
+ value: success_message(output)
+ }
+ ]
+ }
+ ]
+ )
+ end
+
+ # Sends the output for a build that failed.
+ def failure
+ send_response(
+ response_type: :in_channel,
+ attachments: [
+ {
+ color: FAILURE_COLOR,
+ text: "ChatOps job started by #{user_ref} failed!",
+ fields: [
+ {
+ short: true,
+ title: "ID",
+ value: "#{build_ref}"
+ },
+ {
+ short: true,
+ title: "Name",
+ value: build.name
+ }
+ ]
+ }
+ ]
+ )
+ end
+
+ # Returns the output to send back after a command has been scheduled.
+ def scheduled_output
+ {
+ response_type: :ephemeral,
+ text: "Your ChatOps job #{build_ref} has been created!"
+ }
+ end
+
+ private
+
+ def success_message(output)
+ <<~HEREDOC.chomp
+ ```shell
+ #{strip_ansi_colorcodes(limit_output(output))}
+ ```
+ HEREDOC
+ end
+
+ def limit_output(output)
+ if output.bytesize <= MESSAGE_SIZE_LIMIT
+ output
+ else
+ "The output is too large to be sent back directly!"
+ end
+ end
+
+ def strip_ansi_colorcodes(output)
+ output.gsub(/\x1b\[[0-9;]*m/, '')
+ end
+
+ def user_ref
+ user = pipeline.chat_data.chat_name.user
+ user_url = ::Gitlab::Routing.url_helpers.user_url(user)
+
+ "[#{user.name}](#{user_url})"
+ end
+
+ def build_ref
+ build_url = ::Gitlab::Routing.url_helpers.project_build_url(project, build)
+
+ "[##{build.id}](#{build_url})"
+ end
+ end
+ end
+ end
+end
diff --git a/lib/gitlab/chat_name_token.rb b/lib/gitlab/chat_name_token.rb
index 8b3c5dc9e8b..9b4cb9d0134 100644
--- a/lib/gitlab/chat_name_token.rb
+++ b/lib/gitlab/chat_name_token.rb
@@ -16,7 +16,7 @@ module Gitlab
def get
Gitlab::Redis::SharedState.with do |redis|
data = redis.get(redis_shared_state_key)
- JSON.parse(data, symbolize_names: true) if data
+ Gitlab::Json.parse(data, symbolize_names: true) if data
end
end
diff --git a/lib/gitlab/checks/base_checker.rb b/lib/gitlab/checks/base_checker.rb
index a14fa02c2a4..0045d8a4113 100644
--- a/lib/gitlab/checks/base_checker.rb
+++ b/lib/gitlab/checks/base_checker.rb
@@ -3,7 +3,6 @@
module Gitlab
module Checks
class BaseChecker
- prepend_if_ee('EE::Gitlab::Checks::BaseChecker') # rubocop: disable Cop/InjectEnterpriseEditionModule
include Gitlab::Utils::StrongMemoize
attr_reader :change_access
@@ -57,3 +56,5 @@ module Gitlab
end
end
end
+
+Gitlab::Checks::BaseChecker.prepend_if_ee('EE::Gitlab::Checks::BaseChecker')
diff --git a/lib/gitlab/checks/change_access.rb b/lib/gitlab/checks/change_access.rb
index 14a445fcb96..8bb5ac94e45 100644
--- a/lib/gitlab/checks/change_access.rb
+++ b/lib/gitlab/checks/change_access.rb
@@ -3,8 +3,6 @@
module Gitlab
module Checks
class ChangeAccess
- prepend_if_ee('EE::Gitlab::Checks::ChangeAccess') # rubocop: disable Cop/InjectEnterpriseEditionModule
-
ATTRIBUTES = %i[user_access project skip_authorization
skip_lfs_integrity_check protocol oldrev newrev ref
branch_name tag_name logger commits].freeze
@@ -55,3 +53,5 @@ module Gitlab
end
end
end
+
+Gitlab::Checks::ChangeAccess.prepend_if_ee('EE::Gitlab::Checks::ChangeAccess')
diff --git a/lib/gitlab/checks/diff_check.rb b/lib/gitlab/checks/diff_check.rb
index a73f243e946..8780b410a07 100644
--- a/lib/gitlab/checks/diff_check.rb
+++ b/lib/gitlab/checks/diff_check.rb
@@ -4,7 +4,6 @@ module Gitlab
module Checks
class DiffCheck < BaseChecker
include Gitlab::Utils::StrongMemoize
- prepend_if_ee('EE::Gitlab::Checks::DiffCheck') # rubocop: disable Cop/InjectEnterpriseEditionModule
LOG_MESSAGES = {
validate_file_paths: "Validating diffs' file paths...",
@@ -97,3 +96,5 @@ module Gitlab
end
end
end
+
+Gitlab::Checks::DiffCheck.prepend_if_ee('EE::Gitlab::Checks::DiffCheck')
diff --git a/lib/gitlab/ci/ansi2html.rb b/lib/gitlab/ci/ansi2html.rb
index 3a05feee156..e145bd2e9df 100644
--- a/lib/gitlab/ci/ansi2html.rb
+++ b/lib/gitlab/ci/ansi2html.rb
@@ -353,7 +353,7 @@ module Gitlab
def restore_state(new_state, stream)
state = Base64.urlsafe_decode64(new_state)
- state = JSON.parse(state, symbolize_names: true)
+ state = Gitlab::Json.parse(state, symbolize_names: true)
return if state[:offset].to_i > stream.size
STATE_PARAMS.each do |param|
diff --git a/lib/gitlab/ci/ansi2json/state.rb b/lib/gitlab/ci/ansi2json/state.rb
index 7e1a8102a35..38d36e6950c 100644
--- a/lib/gitlab/ci/ansi2json/state.rb
+++ b/lib/gitlab/ci/ansi2json/state.rb
@@ -90,7 +90,7 @@ module Gitlab
decoded_state = Base64.urlsafe_decode64(state)
return unless decoded_state.present?
- JSON.parse(decoded_state)
+ Gitlab::Json.parse(decoded_state)
end
end
end
diff --git a/lib/gitlab/ci/build/artifacts/metadata.rb b/lib/gitlab/ci/build/artifacts/metadata.rb
index 1c3ce08be76..c5afb16ab1a 100644
--- a/lib/gitlab/ci/build/artifacts/metadata.rb
+++ b/lib/gitlab/ci/build/artifacts/metadata.rb
@@ -32,7 +32,7 @@ module Gitlab
raise ParserError, 'Errors field not found!' unless errors
begin
- JSON.parse(errors)
+ Gitlab::Json.parse(errors)
rescue JSON::ParserError
raise ParserError, 'Invalid errors field!'
end
@@ -71,7 +71,7 @@ module Gitlab
next unless path =~ match_pattern
next if path =~ INVALID_PATH_PATTERN
- entries[path] = JSON.parse(meta, symbolize_names: true)
+ entries[path] = Gitlab::Json.parse(meta, symbolize_names: true)
rescue JSON::ParserError, Encoding::CompatibilityError
next
end
diff --git a/lib/gitlab/ci/build/artifacts/metadata/entry.rb b/lib/gitlab/ci/build/artifacts/metadata/entry.rb
index 80e69cdcc95..ef354832e8e 100644
--- a/lib/gitlab/ci/build/artifacts/metadata/entry.rb
+++ b/lib/gitlab/ci/build/artifacts/metadata/entry.rb
@@ -50,7 +50,7 @@ module Gitlab
end
def basename
- (directory? && !blank_node?) ? name + '/' : name
+ directory? && !blank_node? ? name + '/' : name
end
def name
diff --git a/lib/gitlab/ci/config/entry/artifacts.rb b/lib/gitlab/ci/config/entry/artifacts.rb
index 241c73db3bb..a9a9636637f 100644
--- a/lib/gitlab/ci/config/entry/artifacts.rb
+++ b/lib/gitlab/ci/config/entry/artifacts.rb
@@ -12,7 +12,7 @@ module Gitlab
include ::Gitlab::Config::Entry::Validatable
include ::Gitlab::Config::Entry::Attributable
- ALLOWED_KEYS = %i[name untracked paths reports when expire_in expose_as].freeze
+ ALLOWED_KEYS = %i[name untracked paths reports when expire_in expose_as exclude].freeze
EXPOSE_AS_REGEX = /\A\w[-\w ]*\z/.freeze
EXPOSE_AS_ERROR_MESSAGE = "can contain only letters, digits, '-', '_' and spaces"
@@ -35,6 +35,8 @@ module Gitlab
}, if: :expose_as_present?
validates :expose_as, type: String, length: { maximum: 100 }, if: :expose_as_present?
validates :expose_as, format: { with: EXPOSE_AS_REGEX, message: EXPOSE_AS_ERROR_MESSAGE }, if: :expose_as_present?
+ validates :exclude, array_of_strings: true, if: :exclude_enabled?
+ validates :exclude, absence: { message: 'feature is disabled' }, unless: :exclude_enabled?
validates :reports, type: Hash
validates :when,
inclusion: { in: %w[on_success on_failure always],
@@ -50,8 +52,6 @@ module Gitlab
end
def expose_as_present?
- return false unless Feature.enabled?(:ci_expose_arbitrary_artifacts_in_mr, default_enabled: true)
-
# This duplicates the `validates :config, type: Hash` above,
# but Validatable currently doesn't halt the validation
# chain if it encounters a validation error.
@@ -59,6 +59,10 @@ module Gitlab
!@config[:expose_as].nil?
end
+
+ def exclude_enabled?
+ ::Gitlab::Ci::Features.artifacts_exclude_enabled?
+ end
end
end
end
diff --git a/lib/gitlab/ci/config/entry/reports.rb b/lib/gitlab/ci/config/entry/reports.rb
index 8ccee3b5b2b..1a871e043a6 100644
--- a/lib/gitlab/ci/config/entry/reports.rb
+++ b/lib/gitlab/ci/config/entry/reports.rb
@@ -14,7 +14,7 @@ module Gitlab
ALLOWED_KEYS =
%i[junit codequality sast dependency_scanning container_scanning
dast performance license_management license_scanning metrics lsif
- dotenv cobertura terraform].freeze
+ dotenv cobertura terraform accessibility cluster_applications].freeze
attributes ALLOWED_KEYS
@@ -37,6 +37,8 @@ module Gitlab
validates :dotenv, array_of_strings_or_string: true
validates :cobertura, array_of_strings_or_string: true
validates :terraform, array_of_strings_or_string: true
+ validates :accessibility, array_of_strings_or_string: true
+ validates :cluster_applications, array_of_strings_or_string: true
end
end
diff --git a/lib/gitlab/ci/config/entry/trigger.rb b/lib/gitlab/ci/config/entry/trigger.rb
index 7202784842a..c6ba53adfd7 100644
--- a/lib/gitlab/ci/config/entry/trigger.rb
+++ b/lib/gitlab/ci/config/entry/trigger.rb
@@ -25,8 +25,7 @@ module Gitlab
strategy :CrossProjectTrigger, if: -> (config) { !config.key?(:include) }
strategy :SameProjectTrigger, if: -> (config) do
- ::Feature.enabled?(:ci_parent_child_pipeline, default_enabled: true) &&
- config.key?(:include)
+ config.key?(:include)
end
class CrossProjectTrigger < ::Gitlab::Config::Entry::Node
@@ -72,11 +71,7 @@ module Gitlab
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
+ ['config must specify either project or include']
end
end
end
diff --git a/lib/gitlab/ci/cron_parser.rb b/lib/gitlab/ci/cron_parser.rb
index 1d7e7ea0f9a..efd48a9b29f 100644
--- a/lib/gitlab/ci/cron_parser.rb
+++ b/lib/gitlab/ci/cron_parser.rb
@@ -12,8 +12,11 @@ module Gitlab
end
def next_time_from(time)
- @cron_line ||= try_parse_cron(@cron, @cron_timezone)
- @cron_line.next_time(time).utc.in_time_zone(Time.zone) if @cron_line.present?
+ cron_line.next_time(time).utc.in_time_zone(Time.zone) if cron_line.present?
+ end
+
+ def previous_time_from(time)
+ cron_line.previous_time(time).utc.in_time_zone(Time.zone) if cron_line.present?
end
def cron_valid?
@@ -49,6 +52,10 @@ module Gitlab
def try_parse_cron(cron, cron_timezone)
Fugit::Cron.parse("#{cron} #{cron_timezone}")
end
+
+ def cron_line
+ @cron_line ||= try_parse_cron(@cron, @cron_timezone)
+ end
end
end
end
diff --git a/lib/gitlab/ci/features.rb b/lib/gitlab/ci/features.rb
new file mode 100644
index 00000000000..48f3d4fdd2f
--- /dev/null
+++ b/lib/gitlab/ci/features.rb
@@ -0,0 +1,18 @@
+# frozen_string_literal: true
+
+module Gitlab
+ module Ci
+ ##
+ # Ci::Features is a class that aggregates all CI/CD feature flags in one place.
+ #
+ module Features
+ def self.artifacts_exclude_enabled?
+ ::Feature.enabled?(:ci_artifacts_exclude, default_enabled: false)
+ end
+
+ def self.ensure_scheduling_type_enabled?
+ ::Feature.enabled?(:ci_ensure_scheduling_type, default_enabled: true)
+ end
+ end
+ end
+end
diff --git a/lib/gitlab/ci/parsers.rb b/lib/gitlab/ci/parsers.rb
index a44105d53c2..0e44475607b 100644
--- a/lib/gitlab/ci/parsers.rb
+++ b/lib/gitlab/ci/parsers.rb
@@ -3,14 +3,14 @@
module Gitlab
module Ci
module Parsers
- prepend_if_ee('::EE::Gitlab::Ci::Parsers') # rubocop: disable Cop/InjectEnterpriseEditionModule
-
ParserNotFoundError = Class.new(ParserError)
def self.parsers
{
junit: ::Gitlab::Ci::Parsers::Test::Junit,
- cobertura: ::Gitlab::Ci::Parsers::Coverage::Cobertura
+ cobertura: ::Gitlab::Ci::Parsers::Coverage::Cobertura,
+ terraform: ::Gitlab::Ci::Parsers::Terraform::Tfplan,
+ accessibility: ::Gitlab::Ci::Parsers::Accessibility::Pa11y
}
end
@@ -22,3 +22,5 @@ module Gitlab
end
end
end
+
+Gitlab::Ci::Parsers.prepend_if_ee('::EE::Gitlab::Ci::Parsers')
diff --git a/lib/gitlab/ci/parsers/accessibility/pa11y.rb b/lib/gitlab/ci/parsers/accessibility/pa11y.rb
new file mode 100644
index 00000000000..953b5a91258
--- /dev/null
+++ b/lib/gitlab/ci/parsers/accessibility/pa11y.rb
@@ -0,0 +1,33 @@
+# frozen_string_literal: true
+
+module Gitlab
+ module Ci
+ module Parsers
+ module Accessibility
+ class Pa11y
+ def parse!(json_data, accessibility_report)
+ root = Gitlab::Json.parse(json_data).with_indifferent_access
+
+ parse_all(root, accessibility_report)
+ rescue JSON::ParserError => e
+ accessibility_report.set_error_message("JSON parsing failed: #{e}")
+ rescue StandardError => e
+ accessibility_report.set_error_message("Pa11y parsing failed: #{e}")
+ end
+
+ private
+
+ def parse_all(root, accessibility_report)
+ return unless root.present?
+
+ root.dig("results").each do |url, value|
+ accessibility_report.add_url(url, value)
+ end
+
+ accessibility_report
+ end
+ end
+ end
+ end
+ end
+end
diff --git a/lib/gitlab/ci/parsers/terraform/tfplan.rb b/lib/gitlab/ci/parsers/terraform/tfplan.rb
new file mode 100644
index 00000000000..26a18c6603e
--- /dev/null
+++ b/lib/gitlab/ci/parsers/terraform/tfplan.rb
@@ -0,0 +1,35 @@
+# frozen_string_literal: true
+
+module Gitlab
+ module Ci
+ module Parsers
+ module Terraform
+ class Tfplan
+ TfplanParserError = Class.new(Gitlab::Ci::Parsers::ParserError)
+
+ def parse!(json_data, terraform_reports, artifact:)
+ tfplan = Gitlab::Json.parse(json_data).tap do |parsed_data|
+ parsed_data['job_path'] = Gitlab::Routing.url_helpers.project_job_path(
+ artifact.job.project, artifact.job
+ )
+ end
+
+ raise TfplanParserError, 'Tfplan missing required key' unless valid_supported_keys?(tfplan)
+
+ terraform_reports.add_plan(artifact.filename, tfplan)
+ rescue JSON::ParserError
+ raise TfplanParserError, 'JSON parsing failed'
+ rescue
+ raise TfplanParserError, 'Tfplan parsing failed'
+ end
+
+ private
+
+ def valid_supported_keys?(tfplan)
+ tfplan.keys == %w[create update delete job_path]
+ end
+ end
+ end
+ end
+ end
+end
diff --git a/lib/gitlab/ci/parsers/test/junit.rb b/lib/gitlab/ci/parsers/test/junit.rb
index 33140b4c7fd..5746f38ae5b 100644
--- a/lib/gitlab/ci/parsers/test/junit.rb
+++ b/lib/gitlab/ci/parsers/test/junit.rb
@@ -15,10 +15,10 @@ module Gitlab
test_case = create_test_case(test_case, args)
test_suite.add_test_case(test_case)
end
- rescue Nokogiri::XML::SyntaxError
- raise JunitParserError, "XML parsing failed"
- rescue
- raise JunitParserError, "JUnit parsing failed"
+ rescue Nokogiri::XML::SyntaxError => e
+ test_suite.set_suite_error("JUnit XML parsing failed: #{e}")
+ rescue StandardError => e
+ test_suite.set_suite_error("JUnit data parsing failed: #{e}")
end
private
diff --git a/lib/gitlab/ci/pipeline/chain/command.rb b/lib/gitlab/ci/pipeline/chain/command.rb
index fa46114615c..73187401903 100644
--- a/lib/gitlab/ci/pipeline/chain/command.rb
+++ b/lib/gitlab/ci/pipeline/chain/command.rb
@@ -76,6 +76,21 @@ module Gitlab
def parent_pipeline
bridge&.parent_pipeline
end
+
+ def duration_histogram
+ strong_memoize(:duration_histogram) do
+ name = :gitlab_ci_pipeline_creation_duration_seconds
+ comment = 'Pipeline creation duration'
+ labels = {}
+ buckets = [0.01, 0.05, 0.1, 0.5, 1.0, 2.0, 5.0, 20.0, 50.0, 240.0]
+
+ Gitlab::Metrics.histogram(name, comment, labels, buckets)
+ end
+ end
+
+ def observe_creation_duration(duration)
+ duration_histogram.observe({}, duration.seconds)
+ end
end
end
end
diff --git a/lib/gitlab/ci/pipeline/chain/sequence.rb b/lib/gitlab/ci/pipeline/chain/sequence.rb
index 99780409085..a7c671e76d3 100644
--- a/lib/gitlab/ci/pipeline/chain/sequence.rb
+++ b/lib/gitlab/ci/pipeline/chain/sequence.rb
@@ -10,6 +10,7 @@ module Gitlab
@command = command
@sequence = sequence
@completed = []
+ @start = Time.now
end
def build!
@@ -24,6 +25,8 @@ module Gitlab
@pipeline.tap do
yield @pipeline, self if block_given?
+
+ @command.observe_creation_duration(Time.now - @start)
end
end
diff --git a/lib/gitlab/ci/pipeline/seed/build/resource_group.rb b/lib/gitlab/ci/pipeline/seed/build/resource_group.rb
index 3bec6d1e8b6..c0641d9ff0a 100644
--- a/lib/gitlab/ci/pipeline/seed/build/resource_group.rb
+++ b/lib/gitlab/ci/pipeline/seed/build/resource_group.rb
@@ -16,7 +16,6 @@ module Gitlab
end
def to_resource
- return unless Feature.enabled?(:ci_resource_group, build.project, default_enabled: true)
return unless resource_group_key.present?
resource_group = build.project.resource_groups
diff --git a/lib/gitlab/ci/reports/accessibility_reports.rb b/lib/gitlab/ci/reports/accessibility_reports.rb
new file mode 100644
index 00000000000..1901ba3b102
--- /dev/null
+++ b/lib/gitlab/ci/reports/accessibility_reports.rb
@@ -0,0 +1,46 @@
+# frozen_string_literal: true
+
+module Gitlab
+ module Ci
+ module Reports
+ class AccessibilityReports
+ attr_reader :urls, :error_message
+
+ def initialize
+ @urls = {}
+ @error_message = nil
+ end
+
+ def add_url(url, data)
+ if url.empty?
+ set_error_message("Empty URL detected in gl-accessibility.json")
+ else
+ urls[url] = data
+ end
+ end
+
+ def scans_count
+ @urls.size
+ end
+
+ def passes_count
+ @urls.count { |url, errors| errors.empty? }
+ end
+
+ # rubocop: disable CodeReuse/ActiveRecord
+ def errors_count
+ @urls.sum { |url, errors| errors.size }
+ end
+ # rubocop: enable CodeReuse/ActiveRecord
+
+ def set_error_message(error)
+ @error_message = error
+ end
+
+ def all_errors
+ @urls.values.flatten
+ end
+ end
+ end
+ end
+end
diff --git a/lib/gitlab/ci/reports/accessibility_reports_comparer.rb b/lib/gitlab/ci/reports/accessibility_reports_comparer.rb
new file mode 100644
index 00000000000..fa6337166d5
--- /dev/null
+++ b/lib/gitlab/ci/reports/accessibility_reports_comparer.rb
@@ -0,0 +1,55 @@
+# frozen_string_literal: true
+
+module Gitlab
+ module Ci
+ module Reports
+ class AccessibilityReportsComparer
+ include Gitlab::Utils::StrongMemoize
+
+ STATUS_SUCCESS = 'success'
+ STATUS_FAILED = 'failed'
+
+ attr_reader :base_reports, :head_reports
+
+ def initialize(base_reports, head_reports)
+ @base_reports = base_reports || AccessibilityReports.new
+ @head_reports = head_reports
+ end
+
+ def status
+ head_reports.errors_count.positive? ? STATUS_FAILED : STATUS_SUCCESS
+ end
+
+ def existing_errors
+ strong_memoize(:existing_errors) do
+ base_reports.all_errors
+ end
+ end
+
+ def new_errors
+ strong_memoize(:new_errors) do
+ head_reports.all_errors - base_reports.all_errors
+ end
+ end
+
+ def resolved_errors
+ strong_memoize(:resolved_errors) do
+ base_reports.all_errors - head_reports.all_errors
+ end
+ end
+
+ def errors_count
+ head_reports.errors_count
+ end
+
+ def resolved_count
+ resolved_errors.size
+ end
+
+ def total_count
+ existing_errors.size + new_errors.size
+ end
+ end
+ end
+ end
+end
diff --git a/lib/gitlab/ci/reports/terraform_reports.rb b/lib/gitlab/ci/reports/terraform_reports.rb
new file mode 100644
index 00000000000..f955d007daf
--- /dev/null
+++ b/lib/gitlab/ci/reports/terraform_reports.rb
@@ -0,0 +1,27 @@
+# frozen_string_literal: true
+
+module Gitlab
+ module Ci
+ module Reports
+ class TerraformReports
+ attr_reader :plans
+
+ def initialize
+ @plans = {}
+ end
+
+ def pick(keys)
+ terraform_plans = plans.select do |key|
+ keys.include?(key)
+ end
+
+ { plans: terraform_plans }
+ end
+
+ def add_plan(name, plan)
+ plans[name] = plan
+ end
+ end
+ end
+ end
+end
diff --git a/lib/gitlab/ci/reports/test_reports.rb b/lib/gitlab/ci/reports/test_reports.rb
index 72323c4343d..86ba725c71e 100644
--- a/lib/gitlab/ci/reports/test_reports.rb
+++ b/lib/gitlab/ci/reports/test_reports.rb
@@ -42,6 +42,12 @@ module Gitlab
self
end
+ def suite_errors
+ test_suites.each_with_object({}) do |(name, suite), errors|
+ errors[suite.name] = suite.suite_error if suite.suite_error
+ end
+ end
+
TestCase::STATUS_TYPES.each do |status_type|
define_method("#{status_type}_count") do
# rubocop: disable CodeReuse/ActiveRecord
diff --git a/lib/gitlab/ci/reports/test_suite.rb b/lib/gitlab/ci/reports/test_suite.rb
index cf43c5313c0..8bbf2e0f6cf 100644
--- a/lib/gitlab/ci/reports/test_suite.rb
+++ b/lib/gitlab/ci/reports/test_suite.rb
@@ -7,6 +7,7 @@ module Gitlab
attr_reader :name
attr_reader :test_cases
attr_reader :total_time
+ attr_reader :suite_error
def initialize(name = nil)
@name = name
@@ -25,12 +26,16 @@ module Gitlab
# rubocop: disable CodeReuse/ActiveRecord
def total_count
+ return 0 if suite_error
+
test_cases.values.sum(&:count)
end
# rubocop: enable CodeReuse/ActiveRecord
def total_status
- if failed_count > 0 || error_count > 0
+ if suite_error
+ TestCase::STATUS_ERROR
+ elsif failed_count > 0 || error_count > 0
TestCase::STATUS_FAILED
else
TestCase::STATUS_SUCCESS
@@ -49,14 +54,22 @@ module Gitlab
TestCase::STATUS_TYPES.each do |status_type|
define_method("#{status_type}") do
- test_cases[status_type] || {}
+ return {} if suite_error || test_cases[status_type].nil?
+
+ test_cases[status_type]
end
define_method("#{status_type}_count") do
- test_cases[status_type]&.length.to_i
+ return 0 if suite_error || test_cases[status_type].nil?
+
+ test_cases[status_type].length
end
end
+ def set_suite_error(msg)
+ @suite_error = msg
+ end
+
private
def existing_key?(test_case)
diff --git a/lib/gitlab/ci/status/build/failed.rb b/lib/gitlab/ci/status/build/failed.rb
index b0b01538a30..76ad113aad9 100644
--- a/lib/gitlab/ci/status/build/failed.rb
+++ b/lib/gitlab/ci/status/build/failed.rb
@@ -29,8 +29,6 @@ module Gitlab
private_constant :REASONS
- prepend_if_ee('::EE::Gitlab::Ci::Status::Build::Failed') # rubocop: disable Cop/InjectEnterpriseEditionModule
-
def status_tooltip
base_message
end
@@ -65,3 +63,5 @@ module Gitlab
end
end
end
+
+Gitlab::Ci::Status::Build::Failed.prepend_if_ee('::EE::Gitlab::Ci::Status::Build::Failed')
diff --git a/lib/gitlab/ci/templates/Auto-DevOps.gitlab-ci.yml b/lib/gitlab/ci/templates/Auto-DevOps.gitlab-ci.yml
index a9f29bda9b9..5017037fb5a 100644
--- a/lib/gitlab/ci/templates/Auto-DevOps.gitlab-ci.yml
+++ b/lib/gitlab/ci/templates/Auto-DevOps.gitlab-ci.yml
@@ -48,7 +48,6 @@ variables:
POSTGRES_PASSWORD: testing-password
POSTGRES_ENABLED: "true"
POSTGRES_DB: $CI_ENVIRONMENT_SLUG
- POSTGRES_VERSION: 9.6.2
DOCKER_DRIVER: overlay2
@@ -159,5 +158,5 @@ include:
- template: Security/DAST.gitlab-ci.yml # https://gitlab.com/gitlab-org/gitlab-foss/blob/master/lib/gitlab/ci/templates/Security/DAST.gitlab-ci.yml
- template: Security/Container-Scanning.gitlab-ci.yml # https://gitlab.com/gitlab-org/gitlab-foss/blob/master/lib/gitlab/ci/templates/Security/Container-Scanning.gitlab-ci.yml
- template: Security/Dependency-Scanning.gitlab-ci.yml # https://gitlab.com/gitlab-org/gitlab-foss/blob/master/lib/gitlab/ci/templates/Security/Dependency-Scanning.gitlab-ci.yml
- - template: Security/License-Management.gitlab-ci.yml # https://gitlab.com/gitlab-org/gitlab-foss/blob/master/lib/gitlab/ci/templates/Security/License-Management.gitlab-ci.yml
+ - template: Security/License-Scanning.gitlab-ci.yml # https://gitlab.com/gitlab-org/gitlab-foss/blob/master/lib/gitlab/ci/templates/Security/License-Scanning.gitlab-ci.yml
- template: Security/SAST.gitlab-ci.yml # https://gitlab.com/gitlab-org/gitlab-foss/blob/master/lib/gitlab/ci/templates/Security/SAST.gitlab-ci.yml
diff --git a/lib/gitlab/ci/templates/Deploy-ECS.gitlab-ci.yml b/lib/gitlab/ci/templates/Deploy-ECS.gitlab-ci.yml
index a41b399032f..82b2f5c035e 100644
--- a/lib/gitlab/ci/templates/Deploy-ECS.gitlab-ci.yml
+++ b/lib/gitlab/ci/templates/Deploy-ECS.gitlab-ci.yml
@@ -5,32 +5,9 @@ stages:
- deploy
- production
+variables:
+ AUTO_DEVOPS_PLATFORM_TARGET: ECS
+
include:
- template: Jobs/Build.gitlab-ci.yml
-
-.deploy_to_ecs:
- image: registry.gitlab.com/gitlab-org/cloud-deploy/aws-ecs:latest
- script:
- - ecs update-task-definition
-
-review:
- extends: .deploy_to_ecs
- stage: review
- environment:
- name: review/$CI_COMMIT_REF_NAME
- only:
- refs:
- - branches
- - tags
- except:
- refs:
- - master
-
-production:
- extends: .deploy_to_ecs
- stage: production
- environment:
- name: production
- only:
- refs:
- - master
+ - template: Jobs/Deploy/ECS.gitlab-ci.yml
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 d85078c0a40..adbf9731e43 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
@@ -30,11 +30,9 @@ performance:
paths:
- performance.json
- sitespeed-results/
- only:
- refs:
- - branches
- - tags
- kubernetes: active
- except:
- variables:
- - $PERFORMANCE_DISABLED
+ rules:
+ - if: '$CI_KUBERNETES_ACTIVE == null || $CI_KUBERNETES_ACTIVE == ""'
+ when: never
+ - if: '$PERFORMANCE_DISABLED'
+ when: never
+ - if: '$CI_COMMIT_TAG || $CI_COMMIT_BRANCH'
diff --git a/lib/gitlab/ci/templates/Jobs/Build.gitlab-ci.yml b/lib/gitlab/ci/templates/Jobs/Build.gitlab-ci.yml
index 3949b87bbda..787f07521e0 100644
--- a/lib/gitlab/ci/templates/Jobs/Build.gitlab-ci.yml
+++ b/lib/gitlab/ci/templates/Jobs/Build.gitlab-ci.yml
@@ -15,6 +15,5 @@ build:
export CI_APPLICATION_TAG=${CI_APPLICATION_TAG:-$CI_COMMIT_TAG}
fi
- /build/build.sh
- only:
- - branches
- - tags
+ rules:
+ - if: '$CI_COMMIT_TAG || $CI_COMMIT_BRANCH'
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 9c4699f1f44..24e75c56a75 100644
--- a/lib/gitlab/ci/templates/Jobs/Code-Quality.gitlab-ci.yml
+++ b/lib/gitlab/ci/templates/Jobs/Code-Quality.gitlab-ci.yml
@@ -26,10 +26,7 @@ code_quality:
codequality: gl-code-quality-report.json
expire_in: 1 week
dependencies: []
- only:
- refs:
- - branches
- - tags
- except:
- variables:
- - $CODE_QUALITY_DISABLED
+ rules:
+ - if: '$CODE_QUALITY_DISABLED'
+ when: never
+ - if: '$CI_COMMIT_TAG || $CI_COMMIT_BRANCH'
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 3cf4910fe86..5174aed04ba 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.10.0"
+ image: "registry.gitlab.com/gitlab-org/cluster-integration/auto-deploy-image:v0.15.0"
dast_environment_deploy:
extends: .dast-auto-deploy
@@ -18,17 +18,16 @@ dast_environment_deploy:
on_stop: stop_dast_environment
artifacts:
paths: [environment_url.txt]
- only:
- refs:
- - branches
- variables:
- - $GITLAB_FEATURES =~ /\bdast\b/
- kubernetes: active
- except:
- variables:
- - $CI_DEFAULT_BRANCH != $CI_COMMIT_REF_NAME
- - $DAST_DISABLED || $DAST_DISABLED_FOR_DEFAULT_BRANCH
- - $DAST_WEBSITE # we don't need to create a review app if a URL is already given
+ rules:
+ - if: $CI_DEFAULT_BRANCH != $CI_COMMIT_REF_NAME
+ when: never
+ - if: $DAST_DISABLED || $DAST_DISABLED_FOR_DEFAULT_BRANCH
+ when: never
+ - if: $DAST_WEBSITE # we don't need to create a review app if a URL is already given
+ when: never
+ - if: $CI_COMMIT_BRANCH &&
+ $CI_KUBERNETES_ACTIVE &&
+ $GITLAB_FEATURES =~ /\bdast\b/
stop_dast_environment:
extends: .dast-auto-deploy
@@ -42,14 +41,13 @@ stop_dast_environment:
name: dast-default
action: stop
needs: ["dast"]
- only:
- refs:
- - branches
- variables:
- - $GITLAB_FEATURES =~ /\bdast\b/
- kubernetes: active
- except:
- variables:
- - $CI_DEFAULT_BRANCH != $CI_COMMIT_REF_NAME
- - $DAST_DISABLED || $DAST_DISABLED_FOR_DEFAULT_BRANCH
- - $DAST_WEBSITE
+ rules:
+ - if: $CI_DEFAULT_BRANCH != $CI_COMMIT_REF_NAME
+ when: never
+ - if: $DAST_DISABLED || $DAST_DISABLED_FOR_DEFAULT_BRANCH
+ when: never
+ - if: $DAST_WEBSITE # we don't need to create a review app if a URL is already given
+ when: never
+ - if: $CI_COMMIT_BRANCH &&
+ $CI_KUBERNETES_ACTIVE &&
+ $GITLAB_FEATURES =~ /\bdast\b/
diff --git a/lib/gitlab/ci/templates/Jobs/Deploy.gitlab-ci.yml b/lib/gitlab/ci/templates/Jobs/Deploy.gitlab-ci.yml
index 9bf0d31409a..b4e5a41a34d 100644
--- a/lib/gitlab/ci/templates/Jobs/Deploy.gitlab-ci.yml
+++ b/lib/gitlab/ci/templates/Jobs/Deploy.gitlab-ci.yml
@@ -1,5 +1,8 @@
.auto-deploy:
- image: "registry.gitlab.com/gitlab-org/cluster-integration/auto-deploy-image:v0.13.0"
+ image: "registry.gitlab.com/gitlab-org/cluster-integration/auto-deploy-image:v0.15.0"
+
+include:
+ - template: Jobs/Deploy/ECS.gitlab-ci.yml
review:
extends: .auto-deploy
@@ -18,16 +21,14 @@ review:
on_stop: stop_review
artifacts:
paths: [environment_url.txt]
- only:
- refs:
- - branches
- - tags
- kubernetes: active
- except:
- refs:
- - master
- variables:
- - $REVIEW_DISABLED
+ rules:
+ - if: '$CI_KUBERNETES_ACTIVE == null || $CI_KUBERNETES_ACTIVE == ""'
+ when: never
+ - if: '$CI_COMMIT_BRANCH == "master"'
+ when: never
+ - if: '$REVIEW_DISABLED'
+ when: never
+ - if: '$CI_COMMIT_TAG || $CI_COMMIT_BRANCH'
stop_review:
extends: .auto-deploy
@@ -41,18 +42,16 @@ stop_review:
name: review/$CI_COMMIT_REF_NAME
action: stop
dependencies: []
- when: manual
allow_failure: true
- only:
- refs:
- - branches
- - tags
- kubernetes: active
- except:
- refs:
- - master
- variables:
- - $REVIEW_DISABLED
+ rules:
+ - if: '$CI_KUBERNETES_ACTIVE == null || $CI_KUBERNETES_ACTIVE == ""'
+ when: never
+ - if: '$CI_COMMIT_BRANCH == "master"'
+ when: never
+ - if: '$REVIEW_DISABLED'
+ when: never
+ - if: '$CI_COMMIT_TAG || $CI_COMMIT_BRANCH'
+ when: manual
# Staging deploys are disabled by default since
# continuous deployment to production is enabled by default
@@ -73,12 +72,12 @@ staging:
environment:
name: staging
url: http://$CI_PROJECT_PATH_SLUG-staging.$KUBE_INGRESS_BASE_DOMAIN
- only:
- refs:
- - master
- kubernetes: active
- variables:
- - $STAGING_ENABLED
+ rules:
+ - if: '$CI_KUBERNETES_ACTIVE == null || $CI_KUBERNETES_ACTIVE == ""'
+ when: never
+ - if: '$CI_COMMIT_BRANCH != "master"'
+ when: never
+ - if: '$STAGING_ENABLED'
# Canaries are disabled by default, but if you want them,
# and know what the downsides are, you can enable this by setting
@@ -97,13 +96,13 @@ canary:
environment:
name: production
url: http://$CI_PROJECT_PATH_SLUG.$KUBE_INGRESS_BASE_DOMAIN
- when: manual
- only:
- refs:
- - master
- kubernetes: active
- variables:
- - $CANARY_ENABLED
+ rules:
+ - if: '$CI_KUBERNETES_ACTIVE == null || $CI_KUBERNETES_ACTIVE == ""'
+ when: never
+ - if: '$CI_COMMIT_BRANCH != "master"'
+ when: never
+ - if: '$CANARY_ENABLED'
+ when: manual
.production: &production_template
extends: .auto-deploy
@@ -126,32 +125,33 @@ canary:
production:
<<: *production_template
- only:
- refs:
- - master
- kubernetes: active
- except:
- variables:
- - $STAGING_ENABLED
- - $CANARY_ENABLED
- - $INCREMENTAL_ROLLOUT_ENABLED
- - $INCREMENTAL_ROLLOUT_MODE
+ rules:
+ - if: '$CI_KUBERNETES_ACTIVE == null || $CI_KUBERNETES_ACTIVE == ""'
+ when: never
+ - if: '$STAGING_ENABLED'
+ when: never
+ - if: '$CANARY_ENABLED'
+ when: never
+ - if: '$INCREMENTAL_ROLLOUT_ENABLED'
+ when: never
+ - if: '$INCREMENTAL_ROLLOUT_MODE'
+ when: never
+ - if: '$CI_COMMIT_BRANCH == "master"'
production_manual:
<<: *production_template
- when: manual
allow_failure: false
- only:
- refs:
- - master
- kubernetes: active
- variables:
- - $STAGING_ENABLED
- - $CANARY_ENABLED
- except:
- variables:
- - $INCREMENTAL_ROLLOUT_ENABLED
- - $INCREMENTAL_ROLLOUT_MODE
+ rules:
+ - if: '$CI_KUBERNETES_ACTIVE == null || $CI_KUBERNETES_ACTIVE == ""'
+ when: never
+ - if: '$INCREMENTAL_ROLLOUT_ENABLED'
+ when: never
+ - if: '$INCREMENTAL_ROLLOUT_MODE'
+ when: never
+ - if: '$CI_COMMIT_BRANCH == "master" && $STAGING_ENABLED'
+ when: manual
+ - if: '$CI_COMMIT_BRANCH == "master" && $CANARY_ENABLED'
+ when: manual
# This job implements incremental rollout on for every push to `master`.
@@ -176,29 +176,29 @@ production_manual:
.manual_rollout_template: &manual_rollout_template
<<: *rollout_template
stage: production
- when: manual
- # This selectors are backward compatible mode with $INCREMENTAL_ROLLOUT_ENABLED (before 11.4)
- only:
- refs:
- - master
- kubernetes: active
- variables:
- - $INCREMENTAL_ROLLOUT_MODE == "manual"
- - $INCREMENTAL_ROLLOUT_ENABLED
- except:
- variables:
- - $INCREMENTAL_ROLLOUT_MODE == "timed"
+ rules:
+ - if: '$CI_KUBERNETES_ACTIVE == null || $CI_KUBERNETES_ACTIVE == ""'
+ when: never
+ - if: '$INCREMENTAL_ROLLOUT_MODE == "timed"'
+ when: never
+ - if: '$CI_COMMIT_BRANCH != "master"'
+ when: never
+ # $INCREMENTAL_ROLLOUT_ENABLED is for compamtibilty with pre-GitLab 11.4 syntax
+ - if: '$INCREMENTAL_ROLLOUT_MODE == "manual" || $INCREMENTAL_ROLLOUT_ENABLED'
+ when: manual
.timed_rollout_template: &timed_rollout_template
<<: *rollout_template
- when: delayed
- start_in: 5 minutes
- only:
- refs:
- - master
- kubernetes: active
- variables:
- - $INCREMENTAL_ROLLOUT_MODE == "timed"
+ rules:
+ - if: '$CI_KUBERNETES_ACTIVE == null || $CI_KUBERNETES_ACTIVE == ""'
+ when: never
+ - if: '$INCREMENTAL_ROLLOUT_MODE == "manual"'
+ when: never
+ - if: '$CI_COMMIT_BRANCH != "master"'
+ when: never
+ - if: '$INCREMENTAL_ROLLOUT_MODE == "timed"'
+ when: delayed
+ start_in: 5 minutes
timed rollout 10%:
<<: *timed_rollout_template
diff --git a/lib/gitlab/ci/templates/Jobs/Deploy/ECS.gitlab-ci.yml b/lib/gitlab/ci/templates/Jobs/Deploy/ECS.gitlab-ci.yml
new file mode 100644
index 00000000000..642f0ebeaf7
--- /dev/null
+++ b/lib/gitlab/ci/templates/Jobs/Deploy/ECS.gitlab-ci.yml
@@ -0,0 +1,30 @@
+.deploy_to_ecs:
+ image: 'registry.gitlab.com/gitlab-org/cloud-deploy/aws-ecs:latest'
+ script:
+ - ecs update-task-definition
+
+review_ecs:
+ extends: .deploy_to_ecs
+ stage: review
+ environment:
+ name: review/$CI_COMMIT_REF_NAME
+ rules:
+ - if: '$AUTO_DEVOPS_PLATFORM_TARGET != "ECS"'
+ when: never
+ - if: '$CI_KUBERNETES_ACTIVE'
+ when: never
+ - if: '$REVIEW_DISABLED'
+ when: never
+ - if: '$CI_COMMIT_BRANCH != "master"'
+
+production_ecs:
+ extends: .deploy_to_ecs
+ stage: production
+ environment:
+ name: production
+ rules:
+ - if: '$AUTO_DEVOPS_PLATFORM_TARGET != "ECS"'
+ when: never
+ - if: '$CI_KUBERNETES_ACTIVE'
+ when: never
+ - if: '$CI_COMMIT_BRANCH == "master"'
diff --git a/lib/gitlab/ci/templates/Jobs/Test.gitlab-ci.yml b/lib/gitlab/ci/templates/Jobs/Test.gitlab-ci.yml
index a0ddd273552..3b87d53f165 100644
--- a/lib/gitlab/ci/templates/Jobs/Test.gitlab-ci.yml
+++ b/lib/gitlab/ci/templates/Jobs/Test.gitlab-ci.yml
@@ -1,10 +1,12 @@
test:
- services:
- - "postgres:${POSTGRES_VERSION}"
variables:
+ POSTGRES_VERSION: 9.6.16
POSTGRES_DB: test
+ services:
+ - "postgres:${POSTGRES_VERSION}"
stage: test
image: gliderlabs/herokuish:latest
+ needs: []
script:
- |
if [ -z ${KUBERNETES_PORT+x} ]; then
@@ -15,9 +17,7 @@ test:
- export DATABASE_URL="postgres://${POSTGRES_USER}:${POSTGRES_PASSWORD}@${DB_HOST}:5432/${POSTGRES_DB}"
- cp -R . /tmp/app
- /bin/herokuish buildpack test
- only:
- - branches
- - tags
- except:
- variables:
- - $TEST_DISABLED
+ rules:
+ - if: '$TEST_DISABLED'
+ when: never
+ - if: '$CI_COMMIT_TAG || $CI_COMMIT_BRANCH'
diff --git a/lib/gitlab/ci/templates/Scala.gitlab-ci.yml b/lib/gitlab/ci/templates/Scala.gitlab-ci.yml
index b4208ed9d7d..e081e20564a 100644
--- a/lib/gitlab/ci/templates/Scala.gitlab-ci.yml
+++ b/lib/gitlab/ci/templates/Scala.gitlab-ci.yml
@@ -1,7 +1,7 @@
-# Official Java image. Look for the different tagged releases at
-# https://hub.docker.com/r/library/java/tags/ . A Java image is not required
+# Official OpenJDK Java image. Look for the different tagged releases at
+# https://hub.docker.com/_/openjdk/ . A Java image is not required
# but an image with a JVM speeds up the build a bit.
-image: java:8
+image: openjdk:8
before_script:
# Enable the usage of sources over https
@@ -14,7 +14,7 @@ before_script:
- apt-get update -yqq
- apt-get install sbt -yqq
# Log the sbt version
- - sbt sbt-version
+ - sbt sbtVersion
test:
script:
diff --git a/lib/gitlab/ci/templates/Security/Container-Scanning.gitlab-ci.yml b/lib/gitlab/ci/templates/Security/Container-Scanning.gitlab-ci.yml
index 6efb6b4e273..21bcdd8d9b5 100644
--- a/lib/gitlab/ci/templates/Security/Container-Scanning.gitlab-ci.yml
+++ b/lib/gitlab/ci/templates/Security/Container-Scanning.gitlab-ci.yml
@@ -1,16 +1,20 @@
# Read more about this feature here: https://docs.gitlab.com/ee/user/application_security/container_scanning/
variables:
+ # Setting this variable will affect all Security templates
+ # (SAST, Dependency Scanning, ...)
+ SECURE_ANALYZERS_PREFIX: "registry.gitlab.com/gitlab-org/security-products/analyzers"
+
CS_MAJOR_VERSION: 2
container_scanning:
stage: test
- image: registry.gitlab.com/gitlab-org/security-products/analyzers/klar:$CS_MAJOR_VERSION
+ image: $SECURE_ANALYZERS_PREFIX/klar:$CS_MAJOR_VERSION
variables:
# By default, use the latest clair vulnerabilities database, however, allow it to be overridden here with a specific image
# to enable container scanning to run offline, or to provide a consistent list of vulnerabilities for integration testing purposes
CLAIR_DB_IMAGE_TAG: "latest"
- CLAIR_DB_IMAGE: "arminc/clair-db:$CLAIR_DB_IMAGE_TAG"
+ CLAIR_DB_IMAGE: "$SECURE_ANALYZERS_PREFIX/clair-vulnerabilities-db:$CLAIR_DB_IMAGE_TAG"
# Override the GIT_STRATEGY variable in your `.gitlab-ci.yml` file and set it to `fetch` if you want to provide a `clair-whitelist.yml`
# file. See https://docs.gitlab.com/ee/user/application_security/container_scanning/index.html#overriding-the-container-scanning-template
# for details
@@ -25,11 +29,8 @@ container_scanning:
reports:
container_scanning: gl-container-scanning-report.json
dependencies: []
- only:
- refs:
- - branches
- variables:
- - $GITLAB_FEATURES =~ /\bcontainer_scanning\b/
- except:
- variables:
- - $CONTAINER_SCANNING_DISABLED
+ rules:
+ - if: $CONTAINER_SCANNING_DISABLED
+ when: never
+ - if: $CI_COMMIT_BRANCH &&
+ $GITLAB_FEATURES =~ /\bcontainer_scanning\b/
diff --git a/lib/gitlab/ci/templates/Security/DAST.gitlab-ci.yml b/lib/gitlab/ci/templates/Security/DAST.gitlab-ci.yml
index 0e3d7660bdf..07399216597 100644
--- a/lib/gitlab/ci/templates/Security/DAST.gitlab-ci.yml
+++ b/lib/gitlab/ci/templates/Security/DAST.gitlab-ci.yml
@@ -12,11 +12,14 @@ stages:
variables:
DAST_VERSION: 1
+ # Setting this variable will affect all Security templates
+ # (SAST, Dependency Scanning, ...)
+ SECURE_ANALYZERS_PREFIX: "registry.gitlab.com/gitlab-org/security-products/analyzers"
dast:
stage: dast
image:
- name: "registry.gitlab.com/gitlab-org/security-products/dast:$DAST_VERSION"
+ name: "$SECURE_ANALYZERS_PREFIX/dast:$DAST_VERSION"
variables:
GIT_STRATEGY: none
allow_failure: true
@@ -27,12 +30,15 @@ dast:
artifacts:
reports:
dast: gl-dast-report.json
- only:
- refs:
- - branches
- variables:
- - $GITLAB_FEATURES =~ /\bdast\b/
- except:
- variables:
- - $DAST_DISABLED
- - $DAST_DISABLED_FOR_DEFAULT_BRANCH && $CI_DEFAULT_BRANCH == $CI_COMMIT_REF_NAME
+ rules:
+ - if: $DAST_DISABLED
+ when: never
+ - if: $DAST_DISABLED_FOR_DEFAULT_BRANCH &&
+ $CI_DEFAULT_BRANCH == $CI_COMMIT_REF_NAME
+ when: never
+ - if: $CI_DEFAULT_BRANCH != $CI_COMMIT_REF_NAME &&
+ $REVIEW_DISABLED && $DAST_WEBSITE == null &&
+ $DAST_API_SPECIFICATION == null
+ when: never
+ - if: $CI_COMMIT_BRANCH &&
+ $GITLAB_FEATURES =~ /\bdast\b/
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 0ecf37b37a3..616966b4f04 100644
--- a/lib/gitlab/ci/templates/Security/Dependency-Scanning.gitlab-ci.yml
+++ b/lib/gitlab/ci/templates/Security/Dependency-Scanning.gitlab-ci.yml
@@ -5,11 +5,16 @@
# How to set: https://docs.gitlab.com/ee/ci/yaml/#variables
variables:
- SECURITY_SCANNER_IMAGE_PREFIX: "registry.gitlab.com/gitlab-org/security-products"
- DS_ANALYZER_IMAGE_PREFIX: "$SECURITY_SCANNER_IMAGE_PREFIX/analyzers"
+ # Setting this variable will affect all Security templates
+ # (SAST, Dependency Scanning, ...)
+ SECURE_ANALYZERS_PREFIX: "registry.gitlab.com/gitlab-org/security-products/analyzers"
+
+ # Deprecated, use SECURE_ANALYZERS_PREFIX instead
+ DS_ANALYZER_IMAGE_PREFIX: "$SECURE_ANALYZERS_PREFIX"
+
DS_DEFAULT_ANALYZERS: "bundler-audit, retire.js, gemnasium, gemnasium-maven, gemnasium-python"
DS_MAJOR_VERSION: 2
- DS_DISABLE_DIND: "false"
+ DS_DISABLE_DIND: "true"
dependency_scanning:
stage: test
@@ -21,7 +26,6 @@ dependency_scanning:
services:
- docker:stable-dind
script:
- - export DS_VERSION=${SP_VERSION:-$(echo "$CI_SERVER_VERSION" | sed 's/^\([0-9]*\)\.\([0-9]*\).*/\1-\2-stable/')}
- |
if ! docker info &>/dev/null; then
if [ -z "$DOCKER_HOST" -a "$KUBERNETES_PORT" ]; then
@@ -68,28 +72,25 @@ dependency_scanning:
) \
--volume "$PWD:/code" \
--volume /var/run/docker.sock:/var/run/docker.sock \
- "$SECURITY_SCANNER_IMAGE_PREFIX/dependency-scanning:$DS_VERSION" /code
+ "registry.gitlab.com/gitlab-org/security-products/dependency-scanning:$DS_MAJOR_VERSION" /code
artifacts:
reports:
dependency_scanning: gl-dependency-scanning-report.json
dependencies: []
- only:
- refs:
- - branches
- variables:
- - $GITLAB_FEATURES =~ /\bdependency_scanning\b/
- except:
- variables:
- - $DEPENDENCY_SCANNING_DISABLED
- - $DS_DISABLE_DIND == 'true'
+ rules:
+ - if: $DEPENDENCY_SCANNING_DISABLED || $DS_DISABLE_DIND == 'true'
+ when: never
+ - if: $CI_COMMIT_BRANCH &&
+ $GITLAB_FEATURES =~ /\bdependency_scanning\b/
.ds-analyzer:
extends: dependency_scanning
services: []
- except:
- variables:
- - $DEPENDENCY_SCANNING_DISABLED
- - $DS_DISABLE_DIND == 'false'
+ rules:
+ - if: $DEPENDENCY_SCANNING_DISABLED || $DS_DISABLE_DIND == 'false'
+ when: never
+ - if: $CI_COMMIT_BRANCH &&
+ $GITLAB_FEATURES =~ /\bdependency_scanning\b/
script:
- /analyzer run
@@ -97,48 +98,81 @@ gemnasium-dependency_scanning:
extends: .ds-analyzer
image:
name: "$DS_ANALYZER_IMAGE_PREFIX/gemnasium:$DS_MAJOR_VERSION"
- only:
- variables:
- - $GITLAB_FEATURES =~ /\bdependency_scanning\b/ &&
- $DS_DEFAULT_ANALYZERS =~ /gemnasium([^-]|$)/ &&
- $CI_PROJECT_REPOSITORY_LANGUAGES =~ /ruby|javascript|php|\bgo\b/
+ rules:
+ - if: $DEPENDENCY_SCANNING_DISABLED || $DS_DISABLE_DIND == 'false'
+ when: never
+ - if: $CI_COMMIT_BRANCH &&
+ $GITLAB_FEATURES =~ /\bdependency_scanning\b/ &&
+ $DS_DEFAULT_ANALYZERS =~ /gemnasium([^-]|$)/
+ exists:
+ - '{Gemfile.lock,*/Gemfile.lock,*/*/Gemfile.lock}'
+ - '{composer.lock,*/composer.lock,*/*/composer.lock}'
+ - '{gems.locked,*/gems.locked,*/*/gems.locked}'
+ - '{go.sum,*/go.sum,*/*/go.sum}'
+ - '{npm-shrinkwrap.json,*/npm-shrinkwrap.json,*/*/npm-shrinkwrap.json}'
+ - '{package-lock.json,*/package-lock.json,*/*/package-lock.json}'
+ - '{yarn.lock,*/yarn.lock,*/*/yarn.lock}'
gemnasium-maven-dependency_scanning:
extends: .ds-analyzer
image:
name: "$DS_ANALYZER_IMAGE_PREFIX/gemnasium-maven:$DS_MAJOR_VERSION"
- only:
- variables:
- - $GITLAB_FEATURES =~ /\bdependency_scanning\b/ &&
- $DS_DEFAULT_ANALYZERS =~ /gemnasium-maven/ &&
- $CI_PROJECT_REPOSITORY_LANGUAGES =~ /\b(java|scala)\b/
+ rules:
+ - if: $DEPENDENCY_SCANNING_DISABLED || $DS_DISABLE_DIND == 'false'
+ when: never
+ - if: $CI_COMMIT_BRANCH &&
+ $GITLAB_FEATURES =~ /\bdependency_scanning\b/ &&
+ $DS_DEFAULT_ANALYZERS =~ /gemnasium-maven/
+ exists:
+ - '{build.gradle,*/build.gradle,*/*/build.gradle}'
+ - '{build.sbt,*/build.sbt,*/*/build.sbt}'
+ - '{pom.xml,*/pom.xml,*/*/pom.xml}'
gemnasium-python-dependency_scanning:
extends: .ds-analyzer
image:
name: "$DS_ANALYZER_IMAGE_PREFIX/gemnasium-python:$DS_MAJOR_VERSION"
- only:
- variables:
- - $GITLAB_FEATURES =~ /\bdependency_scanning\b/ &&
- $DS_DEFAULT_ANALYZERS =~ /gemnasium-python/ &&
- $CI_PROJECT_REPOSITORY_LANGUAGES =~ /python/
+ rules:
+ - if: $DEPENDENCY_SCANNING_DISABLED || $DS_DISABLE_DIND == 'false'
+ when: never
+ - if: $CI_COMMIT_BRANCH &&
+ $GITLAB_FEATURES =~ /\bdependency_scanning\b/ &&
+ $DS_DEFAULT_ANALYZERS =~ /gemnasium-python/
+ exists:
+ - '{requirements.txt,*/requirements.txt,*/*/requirements.txt}'
+ - '{requirements.pip,*/requirements.pip,*/*/requirements.pip}'
+ - '{Pipfile,*/Pipfile,*/*/Pipfile}'
+ - '{requires.txt,*/requires.txt,*/*/requires.txt}'
+ - '{setup.py,*/setup.py,*/*/setup.py}'
+ # Support passing of $PIP_REQUIREMENTS_FILE
+ # See https://docs.gitlab.com/ee/user/application_security/dependency_scanning/#configuring-specific-analyzers-used-by-dependency-scanning
+ - if: $CI_COMMIT_BRANCH &&
+ $GITLAB_FEATURES =~ /\bdependency_scanning\b/ &&
+ $DS_DEFAULT_ANALYZERS =~ /gemnasium-python/ &&
+ $PIP_REQUIREMENTS_FILE
bundler-audit-dependency_scanning:
extends: .ds-analyzer
image:
name: "$DS_ANALYZER_IMAGE_PREFIX/bundler-audit:$DS_MAJOR_VERSION"
- only:
- variables:
- - $GITLAB_FEATURES =~ /\bdependency_scanning\b/ &&
- $DS_DEFAULT_ANALYZERS =~ /bundler-audit/ &&
- $CI_PROJECT_REPOSITORY_LANGUAGES =~ /ruby/
+ rules:
+ - if: $DEPENDENCY_SCANNING_DISABLED || $DS_DISABLE_DIND == 'false'
+ when: never
+ - if: $CI_COMMIT_BRANCH &&
+ $GITLAB_FEATURES =~ /\bdependency_scanning\b/ &&
+ $DS_DEFAULT_ANALYZERS =~ /bundler-audit/
+ exists:
+ - '{Gemfile.lock,*/Gemfile.lock,*/*/Gemfile.lock}'
retire-js-dependency_scanning:
extends: .ds-analyzer
image:
name: "$DS_ANALYZER_IMAGE_PREFIX/retire.js:$DS_MAJOR_VERSION"
- only:
- variables:
- - $GITLAB_FEATURES =~ /\bdependency_scanning\b/ &&
- $DS_DEFAULT_ANALYZERS =~ /retire.js/ &&
- $CI_PROJECT_REPOSITORY_LANGUAGES =~ /javascript/
+ rules:
+ - if: $DEPENDENCY_SCANNING_DISABLED || $DS_DISABLE_DIND == 'false'
+ when: never
+ - if: $CI_COMMIT_BRANCH &&
+ $GITLAB_FEATURES =~ /\bdependency_scanning\b/ &&
+ $DS_DEFAULT_ANALYZERS =~ /retire.js/
+ exists:
+ - '{package.json,*/package.json,*/*/package.json}'
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 58fd018a82d..87f78d0c887 100644
--- a/lib/gitlab/ci/templates/Security/License-Management.gitlab-ci.yml
+++ b/lib/gitlab/ci/templates/Security/License-Management.gitlab-ci.yml
@@ -1,29 +1,13 @@
# 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.
+include:
+ - template: License-Scanning.gitlab-ci.yml
-license_management:
- 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:
- - echo "This template is deprecated, please use License-Scanning.gitlab-ci.yml template instead."
- - /run.sh analyze .
- artifacts:
- reports:
- license_management: gl-license-management-report.json
- dependencies: []
- only:
- refs:
- - branches
- variables:
- - $GITLAB_FEATURES =~ /\blicense_management\b/
- except:
- variables:
- - $LICENSE_MANAGEMENT_DISABLED
+license_scanning:
+ before_script:
+ - |
+ echo "As of GitLab 12.8, we deprecated the License-Management.gitlab.ci.yml template.
+ Please replace it with the License-Scanning.gitlab-ci.yml template instead.
+ For more details visit
+ https://docs.gitlab.com/ee/user/compliance/license_compliance/#migration-from-license_management-to-license_scanning"
diff --git a/lib/gitlab/ci/templates/Security/License-Scanning.gitlab-ci.yml b/lib/gitlab/ci/templates/Security/License-Scanning.gitlab-ci.yml
index 2333fb4e947..b86014c1ebc 100644
--- a/lib/gitlab/ci/templates/Security/License-Scanning.gitlab-ci.yml
+++ b/lib/gitlab/ci/templates/Security/License-Scanning.gitlab-ci.yml
@@ -5,29 +5,30 @@
# How to set: https://docs.gitlab.com/ee/ci/yaml/#variables
variables:
+ # Setting this variable will affect all Security templates
+ # (SAST, Dependency Scanning, ...)
+ SECURE_ANALYZERS_PREFIX: "registry.gitlab.com/gitlab-org/security-products/analyzers"
+
LICENSE_MANAGEMENT_SETUP_CMD: '' # If needed, specify a command to setup your environment with a custom package manager.
+ LICENSE_MANAGEMENT_VERSION: 3
license_scanning:
stage: test
image:
- name: "registry.gitlab.com/gitlab-org/security-products/license-management:$CI_SERVER_VERSION_MAJOR-$CI_SERVER_VERSION_MINOR-stable"
+ name: "$SECURE_ANALYZERS_PREFIX/license-finder:$LICENSE_MANAGEMENT_VERSION"
entrypoint: [""]
variables:
+ LM_REPORT_FILE: gl-license-scanning-report.json
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
+ license_scanning: $LM_REPORT_FILE
dependencies: []
- only:
- refs:
- - branches
- variables:
- - $GITLAB_FEATURES =~ /\blicense_scanning\b/
- except:
- variables:
- - $LICENSE_MANAGEMENT_DISABLED
+ rules:
+ - if: $LICENSE_MANAGEMENT_DISABLED
+ when: never
+ - if: $CI_COMMIT_BRANCH &&
+ $GITLAB_FEATURES =~ /\blicense_scanning\b/
diff --git a/lib/gitlab/ci/templates/Security/SAST.gitlab-ci.yml b/lib/gitlab/ci/templates/Security/SAST.gitlab-ci.yml
index 03b9720747d..47f68118ee0 100644
--- a/lib/gitlab/ci/templates/Security/SAST.gitlab-ci.yml
+++ b/lib/gitlab/ci/templates/Security/SAST.gitlab-ci.yml
@@ -5,10 +5,16 @@
# How to set: https://docs.gitlab.com/ee/ci/yaml/#variables
variables:
- SAST_ANALYZER_IMAGE_PREFIX: "registry.gitlab.com/gitlab-org/security-products/analyzers"
+ # Setting this variable will affect all Security templates
+ # (SAST, Dependency Scanning, ...)
+ SECURE_ANALYZERS_PREFIX: "registry.gitlab.com/gitlab-org/security-products/analyzers"
+
+ # Deprecated, use SECURE_ANALYZERS_PREFIX instead
+ SAST_ANALYZER_IMAGE_PREFIX: "$SECURE_ANALYZERS_PREFIX"
+
SAST_DEFAULT_ANALYZERS: "bandit, brakeman, gosec, spotbugs, flawfinder, phpcs-security-audit, security-code-scan, nodejs-scan, eslint, tslint, secrets, sobelow, pmd-apex, kubesec"
SAST_ANALYZER_IMAGE_TAG: 2
- SAST_DISABLE_DIND: "false"
+ SAST_DISABLE_DIND: "true"
SCAN_KUBERNETES_MANIFESTS: "false"
sast:
@@ -17,19 +23,18 @@ sast:
artifacts:
reports:
sast: gl-sast-report.json
- only:
- refs:
- - branches
- variables:
- - $GITLAB_FEATURES =~ /\bsast\b/
+ rules:
+ - if: $SAST_DISABLED || $SAST_DISABLE_DIND == 'true'
+ when: never
+ - if: $CI_COMMIT_BRANCH && $GITLAB_FEATURES =~ /\bsast\b/
image: docker:stable
variables:
+ SEARCH_MAX_DEPTH: 4
DOCKER_DRIVER: overlay2
DOCKER_TLS_CERTDIR: ""
services:
- docker:stable-dind
script:
- - export SAST_VERSION=${SP_VERSION:-$(echo "$CI_SERVER_VERSION" | sed 's/^\([0-9]*\)\.\([0-9]*\).*/\1-\2-stable/')}
- |
if ! docker info &>/dev/null; then
if [ -z "$DOCKER_HOST" -a "$KUBERNETES_PORT" ]; then
@@ -41,19 +46,16 @@ sast:
$(awk 'BEGIN{for(v in ENVIRON) print v}' | grep -v -E '^(DOCKER_|CI|GITLAB_|FF_|HOME|PWD|OLDPWD|PATH|SHLVL|HOSTNAME)' | awk '{printf " -e %s", $0}') \
--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
- except:
- variables:
- - $SAST_DISABLED
- - $SAST_DISABLE_DIND == 'true'
+ "registry.gitlab.com/gitlab-org/security-products/sast:$SAST_ANALYZER_IMAGE_TAG" /app/bin/run /code
.sast-analyzer:
extends: sast
services: []
- except:
- variables:
- - $SAST_DISABLED
- - $SAST_DISABLE_DIND == 'false'
+ rules:
+ - if: $SAST_DISABLED || $SAST_DISABLE_DIND == 'false'
+ when: never
+ - if: $CI_COMMIT_BRANCH &&
+ $GITLAB_FEATURES =~ /\bsast\b/
script:
- /analyzer run
@@ -61,49 +63,65 @@ bandit-sast:
extends: .sast-analyzer
image:
name: "$SAST_ANALYZER_IMAGE_PREFIX/bandit:$SAST_ANALYZER_IMAGE_TAG"
- only:
- variables:
- - $GITLAB_FEATURES =~ /\bsast\b/ &&
- $SAST_DEFAULT_ANALYZERS =~ /bandit/&&
- $CI_PROJECT_REPOSITORY_LANGUAGES =~ /\bpython\b/
+ rules:
+ - if: $SAST_DISABLED || $SAST_DISABLE_DIND == 'false'
+ when: never
+ - if: $CI_COMMIT_BRANCH &&
+ $GITLAB_FEATURES =~ /\bsast\b/ &&
+ $SAST_DEFAULT_ANALYZERS =~ /bandit/
+ exists:
+ - '**/*.py'
brakeman-sast:
extends: .sast-analyzer
image:
name: "$SAST_ANALYZER_IMAGE_PREFIX/brakeman:$SAST_ANALYZER_IMAGE_TAG"
- only:
- variables:
- - $GITLAB_FEATURES =~ /\bsast\b/ &&
- $SAST_DEFAULT_ANALYZERS =~ /brakeman/ &&
- $CI_PROJECT_REPOSITORY_LANGUAGES =~ /\bruby\b/
+ rules:
+ - if: $SAST_DISABLED || $SAST_DISABLE_DIND == 'false'
+ when: never
+ - if: $CI_COMMIT_BRANCH &&
+ $GITLAB_FEATURES =~ /\bsast\b/ &&
+ $SAST_DEFAULT_ANALYZERS =~ /brakeman/
+ exists:
+ - '**/*.rb'
eslint-sast:
extends: .sast-analyzer
image:
name: "$SAST_ANALYZER_IMAGE_PREFIX/eslint:$SAST_ANALYZER_IMAGE_TAG"
- only:
- variables:
- - $GITLAB_FEATURES =~ /\bsast\b/ &&
- $SAST_DEFAULT_ANALYZERS =~ /eslint/ &&
- $CI_PROJECT_REPOSITORY_LANGUAGES =~ /\bjavascript\b/
+ rules:
+ - if: $SAST_DISABLED || $SAST_DISABLE_DIND == 'false'
+ when: never
+ - if: $CI_COMMIT_BRANCH &&
+ $GITLAB_FEATURES =~ /\bsast\b/ &&
+ $SAST_DEFAULT_ANALYZERS =~ /eslint/
+ exists:
+ - '**/*.html'
+ - '**/*.js'
flawfinder-sast:
extends: .sast-analyzer
image:
name: "$SAST_ANALYZER_IMAGE_PREFIX/flawfinder:$SAST_ANALYZER_IMAGE_TAG"
- only:
- variables:
- - $GITLAB_FEATURES =~ /\bsast\b/ &&
- $SAST_DEFAULT_ANALYZERS =~ /flawfinder/ &&
- $CI_PROJECT_REPOSITORY_LANGUAGES =~ /(c(\+\+)?,)|(c(\+\+)?$)/
+ rules:
+ - if: $SAST_DISABLED || $SAST_DISABLE_DIND == 'false'
+ when: never
+ - if: $CI_COMMIT_BRANCH &&
+ $GITLAB_FEATURES =~ /\bsast\b/ &&
+ $SAST_DEFAULT_ANALYZERS =~ /flawfinder/
+ exists:
+ - '**/*.c'
+ - '**/*.cpp'
kubesec-sast:
extends: .sast-analyzer
image:
name: "$SAST_ANALYZER_IMAGE_PREFIX/kubesec:$SAST_ANALYZER_IMAGE_TAG"
- only:
- variables:
- - $GITLAB_FEATURES =~ /\bsast\b/ &&
+ rules:
+ - if: $SAST_DISABLED || $SAST_DISABLE_DIND == 'false'
+ when: never
+ - if: $CI_COMMIT_BRANCH &&
+ $GITLAB_FEATURES =~ /\bsast\b/ &&
$SAST_DEFAULT_ANALYZERS =~ /kubesec/ &&
$SCAN_KUBERNETES_MANIFESTS == 'true'
@@ -111,87 +129,117 @@ gosec-sast:
extends: .sast-analyzer
image:
name: "$SAST_ANALYZER_IMAGE_PREFIX/gosec:$SAST_ANALYZER_IMAGE_TAG"
- only:
- variables:
- - $GITLAB_FEATURES =~ /\bsast\b/ &&
- $SAST_DEFAULT_ANALYZERS =~ /gosec/ &&
- $CI_PROJECT_REPOSITORY_LANGUAGES =~ /\bgo\b/
+ rules:
+ - if: $SAST_DISABLED || $SAST_DISABLE_DIND == 'false'
+ when: never
+ - if: $CI_COMMIT_BRANCH &&
+ $GITLAB_FEATURES =~ /\bsast\b/ &&
+ $SAST_DEFAULT_ANALYZERS =~ /gosec/
+ exists:
+ - '**/*.go'
nodejs-scan-sast:
extends: .sast-analyzer
image:
name: "$SAST_ANALYZER_IMAGE_PREFIX/nodejs-scan:$SAST_ANALYZER_IMAGE_TAG"
- only:
- variables:
- - $GITLAB_FEATURES =~ /\bsast\b/ &&
- $SAST_DEFAULT_ANALYZERS =~ /nodejs-scan/ &&
- $CI_PROJECT_REPOSITORY_LANGUAGES =~ /\bjavascript\b/
+ rules:
+ - if: $SAST_DISABLED || $SAST_DISABLE_DIND == 'false'
+ when: never
+ - if: $CI_COMMIT_BRANCH &&
+ $GITLAB_FEATURES =~ /\bsast\b/ &&
+ $SAST_DEFAULT_ANALYZERS =~ /nodejs-scan/
+ exists:
+ - '**/*.js'
phpcs-security-audit-sast:
extends: .sast-analyzer
image:
name: "$SAST_ANALYZER_IMAGE_PREFIX/phpcs-security-audit:$SAST_ANALYZER_IMAGE_TAG"
- only:
- variables:
- - $GITLAB_FEATURES =~ /\bsast\b/ &&
- $SAST_DEFAULT_ANALYZERS =~ /phpcs-security-audit/ &&
- $CI_PROJECT_REPOSITORY_LANGUAGES =~ /\bphp\b/
+ rules:
+ - if: $SAST_DISABLED || $SAST_DISABLE_DIND == 'false'
+ when: never
+ - if: $CI_COMMIT_BRANCH &&
+ $GITLAB_FEATURES =~ /\bsast\b/ &&
+ $SAST_DEFAULT_ANALYZERS =~ /phpcs-security-audit/
+ exists:
+ - '**/*.php'
pmd-apex-sast:
extends: .sast-analyzer
image:
name: "$SAST_ANALYZER_IMAGE_PREFIX/pmd-apex:$SAST_ANALYZER_IMAGE_TAG"
- only:
- variables:
- - $GITLAB_FEATURES =~ /\bsast\b/ &&
- $SAST_DEFAULT_ANALYZERS =~ /pmd-apex/ &&
- $CI_PROJECT_REPOSITORY_LANGUAGES =~ /\bapex\b/
+ rules:
+ - if: $SAST_DISABLED || $SAST_DISABLE_DIND == 'false'
+ when: never
+ - if: $CI_COMMIT_BRANCH &&
+ $GITLAB_FEATURES =~ /\bsast\b/ &&
+ $SAST_DEFAULT_ANALYZERS =~ /pmd-apex/
+ exists:
+ - '**/*.cls'
secrets-sast:
extends: .sast-analyzer
image:
name: "$SAST_ANALYZER_IMAGE_PREFIX/secrets:$SAST_ANALYZER_IMAGE_TAG"
- only:
- variables:
- - $GITLAB_FEATURES =~ /\bsast\b/ &&
+ rules:
+ - if: $SAST_DISABLED || $SAST_DISABLE_DIND == 'false'
+ when: never
+ - if: $CI_COMMIT_BRANCH &&
+ $GITLAB_FEATURES =~ /\bsast\b/ &&
$SAST_DEFAULT_ANALYZERS =~ /secrets/
security-code-scan-sast:
extends: .sast-analyzer
image:
name: "$SAST_ANALYZER_IMAGE_PREFIX/security-code-scan:$SAST_ANALYZER_IMAGE_TAG"
- only:
- variables:
- - $GITLAB_FEATURES =~ /\bsast\b/ &&
- $SAST_DEFAULT_ANALYZERS =~ /security-code-scan/ &&
- $CI_PROJECT_REPOSITORY_LANGUAGES =~ /\b(c\#|visual basic\b)/
+ rules:
+ - if: $SAST_DISABLED || $SAST_DISABLE_DIND == 'false'
+ when: never
+ - if: $CI_COMMIT_BRANCH &&
+ $GITLAB_FEATURES =~ /\bsast\b/ &&
+ $SAST_DEFAULT_ANALYZERS =~ /security-code-scan/
+ exists:
+ - '**/*.csproj'
+ - '**/*.vbproj'
sobelow-sast:
extends: .sast-analyzer
image:
name: "$SAST_ANALYZER_IMAGE_PREFIX/sobelow:$SAST_ANALYZER_IMAGE_TAG"
- only:
- variables:
- - $GITLAB_FEATURES =~ /\bsast\b/ &&
- $SAST_DEFAULT_ANALYZERS =~ /sobelow/ &&
- $CI_PROJECT_REPOSITORY_LANGUAGES =~ /\belixir\b/
+ rules:
+ - if: $SAST_DISABLED || $SAST_DISABLE_DIND == 'false'
+ when: never
+ - if: $CI_COMMIT_BRANCH &&
+ $GITLAB_FEATURES =~ /\bsast\b/ &&
+ $SAST_DEFAULT_ANALYZERS =~ /sobelow/
+ exists:
+ - '**/*.ex'
+ - '**/*.exs'
spotbugs-sast:
extends: .sast-analyzer
image:
name: "$SAST_ANALYZER_IMAGE_PREFIX/spotbugs:$SAST_ANALYZER_IMAGE_TAG"
- only:
- variables:
- - $GITLAB_FEATURES =~ /\bsast\b/ &&
- $SAST_DEFAULT_ANALYZERS =~ /spotbugs/ &&
- $CI_PROJECT_REPOSITORY_LANGUAGES =~ /\b(groovy|java|scala)\b/
+ rules:
+ - if: $SAST_DISABLED || $SAST_DISABLE_DIND == 'false'
+ when: never
+ - if: $CI_COMMIT_BRANCH &&
+ $GITLAB_FEATURES =~ /\bsast\b/ &&
+ $SAST_DEFAULT_ANALYZERS =~ /spotbugs/
+ exists:
+ - '**/*.groovy'
+ - '**/*.java'
+ - '**/*.scala'
tslint-sast:
extends: .sast-analyzer
image:
name: "$SAST_ANALYZER_IMAGE_PREFIX/tslint:$SAST_ANALYZER_IMAGE_TAG"
- only:
- variables:
- - $GITLAB_FEATURES =~ /\bsast\b/ &&
- $SAST_DEFAULT_ANALYZERS =~ /tslint/ &&
- $CI_PROJECT_REPOSITORY_LANGUAGES =~ /\btypescript\b/
+ rules:
+ - if: $SAST_DISABLED || $SAST_DISABLE_DIND == 'false'
+ when: never
+ - if: $CI_COMMIT_BRANCH &&
+ $GITLAB_FEATURES =~ /\bsast\b/ &&
+ $SAST_DEFAULT_ANALYZERS =~ /tslint/
+ exists:
+ - '**/*.ts'
diff --git a/lib/gitlab/ci/templates/Security/Secure-Binaries.gitlab-ci.yml b/lib/gitlab/ci/templates/Security/Secure-Binaries.gitlab-ci.yml
new file mode 100644
index 00000000000..b6c05c61db1
--- /dev/null
+++ b/lib/gitlab/ci/templates/Security/Secure-Binaries.gitlab-ci.yml
@@ -0,0 +1,246 @@
+# This template should be used when Security Products (https://about.gitlab.com/handbook/engineering/development/secure/#security-products)
+# have to be downloaded and stored locally.
+#
+# Usage:
+#
+# ```
+# include:
+# - template: Secure-Binaries.gitlab-ci.yml
+# ```
+#
+# Docs: https://docs.gitlab.com/ee/topics/airgap/
+
+
+variables:
+ SECURE_BINARIES_ANALYZERS: >-
+ bandit, brakeman, gosec, spotbugs, flawfinder, phpcs-security-audit, security-code-scan, nodejs-scan, eslint, tslint, secrets, sobelow, pmd-apex, kubesec,
+ bundler-audit, retire.js, gemnasium, gemnasium-maven, gemnasium-python,
+ klar, clair-vulnerabilities-db,
+ license-finder,
+ dast
+
+ SECURE_BINARIES_DOWNLOAD_IMAGES: "true"
+ SECURE_BINARIES_PUSH_IMAGES: "true"
+ SECURE_BINARIES_SAVE_ARTIFACTS: "false"
+
+ SECURE_BINARIES_ANALYZER_VERSION: "2"
+
+.download_images:
+ allow_failure: true
+ image: docker:stable
+ only:
+ refs:
+ - branches
+ variables:
+ DOCKER_DRIVER: overlay2
+ DOCKER_TLS_CERTDIR: ""
+ services:
+ - docker:stable-dind
+ script:
+ - docker info
+ - env
+ - if [ -z "$SECURE_BINARIES_IMAGE" ]; then export SECURE_BINARIES_IMAGE=${SECURE_BINARIES_IMAGE:-"registry.gitlab.com/gitlab-org/security-products/analyzers/${CI_JOB_NAME}:${SECURE_BINARIES_ANALYZER_VERSION}"}; fi
+ - docker pull ${SECURE_BINARIES_IMAGE}
+ - mkdir -p output/$(dirname ${CI_JOB_NAME})
+ - |
+ if [ "$SECURE_BINARIES_SAVE_ARTIFACTS" = "true" ]; then
+ docker save ${SECURE_BINARIES_IMAGE} | gzip > output/${CI_JOB_NAME}_${SECURE_BINARIES_ANALYZER_VERSION}.tar.gz
+ sha256sum output/${CI_JOB_NAME}_${SECURE_BINARIES_ANALYZER_VERSION}.tar.gz > output/${CI_JOB_NAME}_${SECURE_BINARIES_ANALYZER_VERSION}.tar.gz.sha256sum
+ fi
+ - |
+ if [ "$SECURE_BINARIES_PUSH_IMAGES" = "true" ]; then
+ docker login -u gitlab-ci-token -p $CI_JOB_TOKEN $CI_REGISTRY
+ docker tag ${SECURE_BINARIES_IMAGE} ${CI_REGISTRY_IMAGE}/${CI_JOB_NAME}:${SECURE_BINARIES_ANALYZER_VERSION}
+ docker push ${CI_REGISTRY_IMAGE}/${CI_JOB_NAME}:${SECURE_BINARIES_ANALYZER_VERSION}
+ fi
+
+ artifacts:
+ paths:
+ - output/
+
+#
+# SAST jobs
+#
+
+bandit:
+ extends: .download_images
+ only:
+ variables:
+ - $SECURE_BINARIES_DOWNLOAD_IMAGES == "true" &&
+ $SECURE_BINARIES_ANALYZERS =~ /\bbandit\b/
+
+brakeman:
+ extends: .download_images
+ only:
+ variables:
+ - $SECURE_BINARIES_DOWNLOAD_IMAGES == "true" &&
+ $SECURE_BINARIES_ANALYZERS =~ /\bbrakeman\b/
+
+gosec:
+ extends: .download_images
+ only:
+ variables:
+ - $SECURE_BINARIES_DOWNLOAD_IMAGES == "true" &&
+ $SECURE_BINARIES_ANALYZERS =~ /\bgosec\b/
+
+spotbugs:
+ extends: .download_images
+ only:
+ variables:
+ - $SECURE_BINARIES_DOWNLOAD_IMAGES == "true" &&
+ $SECURE_BINARIES_ANALYZERS =~ /\bspotbugs\b/
+
+flawfinder:
+ extends: .download_images
+ only:
+ variables:
+ - $SECURE_BINARIES_DOWNLOAD_IMAGES == "true" &&
+ $SECURE_BINARIES_ANALYZERS =~ /\bflawfinder\b/
+
+phpcs-security-audit:
+ extends: .download_images
+ only:
+ variables:
+ - $SECURE_BINARIES_DOWNLOAD_IMAGES == "true" &&
+ $SECURE_BINARIES_ANALYZERS =~ /\bphpcs-security-audit\b/
+
+security-code-scan:
+ extends: .download_images
+ only:
+ variables:
+ - $SECURE_BINARIES_DOWNLOAD_IMAGES == "true" &&
+ $SECURE_BINARIES_ANALYZERS =~ /\bsecurity-code-scan\b/
+
+nodejs-scan:
+ extends: .download_images
+ only:
+ variables:
+ - $SECURE_BINARIES_DOWNLOAD_IMAGES == "true" &&
+ $SECURE_BINARIES_ANALYZERS =~ /\bnodejs-scan\b/
+
+eslint:
+ extends: .download_images
+ only:
+ variables:
+ - $SECURE_BINARIES_DOWNLOAD_IMAGES == "true" &&
+ $SECURE_BINARIES_ANALYZERS =~ /\beslint\b/
+
+tslint:
+ extends: .download_images
+ only:
+ variables:
+ - $SECURE_BINARIES_DOWNLOAD_IMAGES == "true" &&
+ $SECURE_BINARIES_ANALYZERS =~ /\btslint\b/
+
+secrets:
+ extends: .download_images
+ only:
+ variables:
+ - $SECURE_BINARIES_DOWNLOAD_IMAGES == "true" &&
+ $SECURE_BINARIES_ANALYZERS =~ /\bsecrets\b/
+
+sobelow:
+ extends: .download_images
+ only:
+ variables:
+ - $SECURE_BINARIES_DOWNLOAD_IMAGES == "true" &&
+ $SECURE_BINARIES_ANALYZERS =~ /\bsobelow\b/
+
+pmd-apex:
+ extends: .download_images
+ only:
+ variables:
+ - $SECURE_BINARIES_DOWNLOAD_IMAGES == "true" &&
+ $SECURE_BINARIES_ANALYZERS =~ /\bsecrets\b/
+
+kubesec:
+ extends: .download_images
+ only:
+ variables:
+ - $SECURE_BINARIES_DOWNLOAD_IMAGES == "true" &&
+ $SECURE_BINARIES_ANALYZERS =~ /\bkubesec\b/
+#
+# Container Scanning jobs
+#
+
+klar:
+ extends: .download_images
+ only:
+ variables:
+ - $SECURE_BINARIES_DOWNLOAD_IMAGES == "true" &&
+ $SECURE_BINARIES_ANALYZERS =~ /\bklar\b/
+
+clair-vulnerabilities-db:
+ extends: .download_images
+ only:
+ variables:
+ - $SECURE_BINARIES_DOWNLOAD_IMAGES == "true" &&
+ $SECURE_BINARIES_ANALYZERS =~ /\bclair-vulnerabilities-db\b/
+ variables:
+ SECURE_BINARIES_IMAGE: arminc/clair-db
+ SECURE_BINARIES_ANALYZER_VERSION: latest
+
+#
+# Dependency Scanning jobs
+#
+
+bundler-audit:
+ extends: .download_images
+ only:
+ variables:
+ - $SECURE_BINARIES_DOWNLOAD_IMAGES == "true" &&
+ $SECURE_BINARIES_ANALYZERS =~ /\bbundler-audit\b/
+
+retire.js:
+ extends: .download_images
+ only:
+ variables:
+ - $SECURE_BINARIES_DOWNLOAD_IMAGES == "true" &&
+ $SECURE_BINARIES_ANALYZERS =~ /\bretire\.js\b/
+
+gemnasium:
+ extends: .download_images
+ only:
+ variables:
+ - $SECURE_BINARIES_DOWNLOAD_IMAGES == "true" &&
+ $SECURE_BINARIES_ANALYZERS =~ /\bgemnasium\b/
+
+gemnasium-maven:
+ extends: .download_images
+ only:
+ variables:
+ - $SECURE_BINARIES_DOWNLOAD_IMAGES == "true" &&
+ $SECURE_BINARIES_ANALYZERS =~ /\bgemnasium-maven\b/
+
+gemnasium-python:
+ extends: .download_images
+ only:
+ variables:
+ - $SECURE_BINARIES_DOWNLOAD_IMAGES == "true" &&
+ $SECURE_BINARIES_ANALYZERS =~ /\bgemnasium-python\b/
+
+#
+# License Scanning
+#
+
+license-finder:
+ extends: .download_images
+ variables:
+ SECURE_BINARIES_ANALYZER_VERSION: "3"
+ only:
+ variables:
+ - $SECURE_BINARIES_DOWNLOAD_IMAGES == "true" &&
+ $SECURE_BINARIES_ANALYZERS =~ /\blicense-finder\b/
+
+#
+# DAST
+#
+
+dast:
+ extends: .download_images
+ variables:
+ SECURE_BINARIES_ANALYZER_VERSION: "1"
+ only:
+ variables:
+ - $SECURE_BINARIES_DOWNLOAD_IMAGES == "true" &&
+ $SECURE_BINARIES_ANALYZERS =~ /\bdast\b/
diff --git a/lib/gitlab/ci/templates/Terraform.gitlab-ci.yml b/lib/gitlab/ci/templates/Terraform.gitlab-ci.yml
index 83483108fde..a0832718214 100644
--- a/lib/gitlab/ci/templates/Terraform.gitlab-ci.yml
+++ b/lib/gitlab/ci/templates/Terraform.gitlab-ci.yml
@@ -19,7 +19,7 @@ cache:
- .terraform
before_script:
- - alias convert_report="jq -r '([.resource_changes[].change.actions?]|flatten)|{\"create\":(map(select(.==\"create\"))|length),\"update\":(map(select(.==\"update\"))|length),\"delete\":(map(select(.==\"delete\"))|length)}'"
+ - alias convert_report="jq -r '([.resource_changes[]?.change.actions?]|flatten)|{\"create\":(map(select(.==\"create\"))|length),\"update\":(map(select(.==\"update\"))|length),\"delete\":(map(select(.==\"delete\"))|length)}'"
- terraform --version
- terraform init
diff --git a/lib/gitlab/ci/templates/Verify/Accessibility.gitlab-ci.yml b/lib/gitlab/ci/templates/Verify/Accessibility.gitlab-ci.yml
index 5d9d3c74def..e8a99a6ea06 100644
--- a/lib/gitlab/ci/templates/Verify/Accessibility.gitlab-ci.yml
+++ b/lib/gitlab/ci/templates/Verify/Accessibility.gitlab-ci.yml
@@ -8,12 +8,14 @@ stages:
a11y:
stage: accessibility
- image: registry.gitlab.com/gitlab-org/ci-cd/accessibility:5.3.0-gitlab.2
+ image: registry.gitlab.com/gitlab-org/ci-cd/accessibility:5.3.0-gitlab.3
script: /gitlab-accessibility.sh $a11y_urls
allow_failure: true
artifacts:
when: always
expose_as: 'Accessibility Reports'
paths: ['reports/']
+ reports:
+ accessibility: reports/gl-accessibility.json
rules:
- if: $a11y_urls
diff --git a/lib/gitlab/ci/templates/Workflows/Branch-Pipelines.gitlab-ci.yml b/lib/gitlab/ci/templates/Workflows/Branch-Pipelines.gitlab-ci.yml
new file mode 100644
index 00000000000..05635cf71be
--- /dev/null
+++ b/lib/gitlab/ci/templates/Workflows/Branch-Pipelines.gitlab-ci.yml
@@ -0,0 +1,7 @@
+# Read more on when to use this template at
+# https://docs.gitlab.com/ee/ci/yaml/#workflowrules
+
+workflow:
+ rules:
+ - if: $CI_COMMIT_TAG
+ - if: $CI_COMMIT_BRANCH
diff --git a/lib/gitlab/ci/templates/Workflows/MergeRequest-Pipelines.gitlab-ci.yml b/lib/gitlab/ci/templates/Workflows/MergeRequest-Pipelines.gitlab-ci.yml
new file mode 100644
index 00000000000..50ff4c1f60b
--- /dev/null
+++ b/lib/gitlab/ci/templates/Workflows/MergeRequest-Pipelines.gitlab-ci.yml
@@ -0,0 +1,8 @@
+# Read more on when to use this template at
+# https://docs.gitlab.com/ee/ci/yaml/#workflowrules
+
+workflow:
+ rules:
+ - if: $CI_MERGE_REQUEST_IID
+ - if: $CI_COMMIT_TAG
+ - if: $CI_COMMIT_BRANCH == $CI_DEFAULT_BRANCH
diff --git a/lib/gitlab/ci/yaml_processor.rb b/lib/gitlab/ci/yaml_processor.rb
index 933504ea82f..5816ac3bc54 100644
--- a/lib/gitlab/ci/yaml_processor.rb
+++ b/lib/gitlab/ci/yaml_processor.rb
@@ -157,7 +157,7 @@ module Gitlab
return unless job[:stage]
unless job[:stage].is_a?(String) && job[:stage].in?(@stages)
- raise ValidationError, "#{name} job: stage parameter should be #{@stages.join(", ")}"
+ raise ValidationError, "#{name} job: chosen stage does not exist; available stages are #{@stages.join(", ")}"
end
end
diff --git a/lib/gitlab/cleanup/orphan_lfs_file_references.rb b/lib/gitlab/cleanup/orphan_lfs_file_references.rb
index a9961cb8968..3df243e319e 100644
--- a/lib/gitlab/cleanup/orphan_lfs_file_references.rb
+++ b/lib/gitlab/cleanup/orphan_lfs_file_references.rb
@@ -35,6 +35,8 @@ module Gitlab
count += relation.delete_all
end
+ ProjectCacheWorker.perform_async(project.id, [], [:lfs_objects_size])
+
log_info("Removed invalid references: #{count}")
end
end
diff --git a/lib/gitlab/code_navigation_path.rb b/lib/gitlab/code_navigation_path.rb
index 8dd2e9cb1bb..57aeb6c4fb2 100644
--- a/lib/gitlab/code_navigation_path.rb
+++ b/lib/gitlab/code_navigation_path.rb
@@ -5,7 +5,7 @@ module Gitlab
include Gitlab::Utils::StrongMemoize
include Gitlab::Routing
- CODE_NAVIGATION_JOB_NAME = 'code_navigation'
+ LATEST_COMMITS_LIMIT = 10
def initialize(project, commit_sha)
@project = project
@@ -16,7 +16,7 @@ module Gitlab
return if Feature.disabled?(:code_navigation, project)
return unless build
- raw_project_job_artifacts_path(project, build, path: "lsif/#{path}.json")
+ raw_project_job_artifacts_path(project, build, path: "lsif/#{path}.json", file_type: :lsif)
end
private
@@ -25,10 +25,14 @@ module Gitlab
def build
strong_memoize(:build) do
- artifact = ::Ci::JobArtifact
- .for_sha(commit_sha, project.id)
- .for_job_name(CODE_NAVIGATION_JOB_NAME)
- .last
+ latest_commits_shas =
+ project.repository.commits(commit_sha, limit: LATEST_COMMITS_LIMIT).map(&:sha)
+
+ artifact =
+ ::Ci::JobArtifact
+ .with_file_types(['lsif'])
+ .for_sha(latest_commits_shas, project.id)
+ .last
artifact&.job
end
diff --git a/lib/gitlab/config_checker/external_database_checker.rb b/lib/gitlab/config_checker/external_database_checker.rb
new file mode 100644
index 00000000000..795082a10a0
--- /dev/null
+++ b/lib/gitlab/config_checker/external_database_checker.rb
@@ -0,0 +1,27 @@
+# frozen_string_literal: true
+
+module Gitlab
+ module ConfigChecker
+ module ExternalDatabaseChecker
+ extend self
+
+ # DB is considered deprecated if it is below version 11
+ def db_version_deprecated?
+ Gitlab::Database.version.to_f < 11
+ end
+
+ def check
+ return [] unless db_version_deprecated?
+
+ [
+ {
+ type: 'warning',
+ message: _('Note that PostgreSQL 11 will become the minimum required PostgreSQL version in GitLab 13.0 (May 2020). '\
+ 'PostgreSQL 9.6 and PostgreSQL 10 will no longer be supported in GitLab 13.0. '\
+ 'Please consider upgrading your PostgreSQL version (%{db_version}) soon.') % { db_version: Gitlab::Database.version.to_s }
+ }
+ ]
+ end
+ end
+ end
+end
diff --git a/lib/gitlab/cycle_analytics/group_stage_summary.rb b/lib/gitlab/cycle_analytics/group_stage_summary.rb
deleted file mode 100644
index 09b33d01846..00000000000
--- a/lib/gitlab/cycle_analytics/group_stage_summary.rb
+++ /dev/null
@@ -1,54 +0,0 @@
-# frozen_string_literal: true
-
-module Gitlab
- module CycleAnalytics
- class GroupStageSummary
- attr_reader :group, :current_user, :options
-
- def initialize(group, options:)
- @group = group
- @current_user = options[:current_user]
- @options = options
- end
-
- def data
- [issue_stats,
- deploy_stats,
- deployment_frequency_stats]
- end
-
- private
-
- def issue_stats
- serialize(
- Summary::Group::Issue.new(
- group: group, current_user: current_user, options: options)
- )
- end
-
- def deployments_summary
- @deployments_summary ||=
- Summary::Group::Deploy.new(group: group, options: options)
- end
-
- def deploy_stats
- serialize deployments_summary
- end
-
- def deployment_frequency_stats
- serialize(
- Summary::Group::DeploymentFrequency.new(
- deployments: deployments_summary.value,
- group: group,
- options: options),
- with_unit: true
- )
- end
-
- def serialize(summary_object, with_unit: false)
- AnalyticsSummarySerializer.new.represent(
- summary_object, with_unit: with_unit)
- end
- end
- end
-end
diff --git a/lib/gitlab/cycle_analytics/stage_summary.rb b/lib/gitlab/cycle_analytics/stage_summary.rb
index 564feb0319f..7559cd376bf 100644
--- a/lib/gitlab/cycle_analytics/stage_summary.rb
+++ b/lib/gitlab/cycle_analytics/stage_summary.rb
@@ -28,8 +28,7 @@ module Gitlab
end
def deployments_summary
- @deployments_summary ||=
- Summary::Deploy.new(project: @project, from: @from, to: @to)
+ @deployments_summary ||= Summary::Deploy.new(project: @project, from: @from, to: @to)
end
def deploy_stats
@@ -39,7 +38,7 @@ module Gitlab
def deployment_frequency_stats
serialize(
Summary::DeploymentFrequency.new(
- deployments: deployments_summary.value,
+ deployments: deployments_summary.value.raw_value,
from: @from,
to: @to),
with_unit: true
diff --git a/lib/gitlab/cycle_analytics/summary/commit.rb b/lib/gitlab/cycle_analytics/summary/commit.rb
index 76049c6b742..1f426b81800 100644
--- a/lib/gitlab/cycle_analytics/summary/commit.rb
+++ b/lib/gitlab/cycle_analytics/summary/commit.rb
@@ -9,7 +9,7 @@ module Gitlab
end
def value
- @value ||= count_commits
+ @value ||= commits_count ? Value::PrettyNumeric.new(commits_count) : Value::None.new
end
private
@@ -18,10 +18,10 @@ module Gitlab
# a limit. Since we need a commit count, we _can't_ enforce a limit, so
# the easiest way forward is to replicate the relevant portions of the
# `log` function here.
- def count_commits
+ def commits_count
return unless ref
- gitaly_commit_client.commit_count(ref, after: @from, before: @to)
+ @commits_count ||= gitaly_commit_client.commit_count(ref, after: @from, before: @to)
end
def gitaly_commit_client
diff --git a/lib/gitlab/cycle_analytics/summary/deploy.rb b/lib/gitlab/cycle_analytics/summary/deploy.rb
index 5ff8d881143..8544ea1a91e 100644
--- a/lib/gitlab/cycle_analytics/summary/deploy.rb
+++ b/lib/gitlab/cycle_analytics/summary/deploy.rb
@@ -4,18 +4,20 @@ module Gitlab
module CycleAnalytics
module Summary
class Deploy < Base
- include Gitlab::Utils::StrongMemoize
-
def title
n_('Deploy', 'Deploys', value)
end
def value
- strong_memoize(:value) do
- query = @project.deployments.success.where("created_at >= ?", @from)
- query = query.where("created_at <= ?", @to) if @to
- query.count
- end
+ @value ||= Value::PrettyNumeric.new(deployments_count)
+ end
+
+ private
+
+ def deployments_count
+ query = @project.deployments.success.where("created_at >= ?", @from)
+ query = query.where("created_at <= ?", @to) if @to
+ query.count
end
end
end
diff --git a/lib/gitlab/cycle_analytics/summary/deployment_frequency.rb b/lib/gitlab/cycle_analytics/summary/deployment_frequency.rb
index 436dc91bd6b..00676a02a6f 100644
--- a/lib/gitlab/cycle_analytics/summary/deployment_frequency.rb
+++ b/lib/gitlab/cycle_analytics/summary/deployment_frequency.rb
@@ -17,8 +17,7 @@ module Gitlab
end
def value
- @value ||=
- frequency(@deployments, @from, @to || Time.now)
+ @value ||= frequency(@deployments, @from, @to || Time.now)
end
def unit
diff --git a/lib/gitlab/cycle_analytics/summary/group/base.rb b/lib/gitlab/cycle_analytics/summary/group/base.rb
deleted file mode 100644
index f1d20d5aefa..00000000000
--- a/lib/gitlab/cycle_analytics/summary/group/base.rb
+++ /dev/null
@@ -1,26 +0,0 @@
-# frozen_string_literal: true
-
-module Gitlab
- module CycleAnalytics
- module Summary
- module Group
- class Base
- attr_reader :group, :options
-
- def initialize(group:, options:)
- @group = group
- @options = options
- end
-
- def title
- raise NotImplementedError.new("Expected #{self.name} to implement title")
- end
-
- def value
- raise NotImplementedError.new("Expected #{self.name} to implement value")
- end
- end
- end
- end
- end
-end
diff --git a/lib/gitlab/cycle_analytics/summary/group/deploy.rb b/lib/gitlab/cycle_analytics/summary/group/deploy.rb
deleted file mode 100644
index 11a9152cf0c..00000000000
--- a/lib/gitlab/cycle_analytics/summary/group/deploy.rb
+++ /dev/null
@@ -1,31 +0,0 @@
-# frozen_string_literal: true
-
-module Gitlab
- module CycleAnalytics
- module Summary
- module Group
- class Deploy < Group::Base
- include GroupProjectsProvider
-
- def title
- n_('Deploy', 'Deploys', value)
- end
-
- def value
- @value ||= find_deployments
- end
-
- private
-
- def find_deployments
- deployments = Deployment.joins(:project).merge(Project.inside_path(group.full_path))
- deployments = deployments.where(projects: { id: options[:projects] }) if options[:projects]
- deployments = deployments.where("deployments.created_at > ?", options[:from])
- deployments = deployments.where("deployments.created_at < ?", options[:to]) if options[:to]
- deployments.success.count
- end
- end
- end
- end
- end
-end
diff --git a/lib/gitlab/cycle_analytics/summary/group/deployment_frequency.rb b/lib/gitlab/cycle_analytics/summary/group/deployment_frequency.rb
deleted file mode 100644
index 9fbbbb5a1ec..00000000000
--- a/lib/gitlab/cycle_analytics/summary/group/deployment_frequency.rb
+++ /dev/null
@@ -1,33 +0,0 @@
-# frozen_string_literal: true
-
-module Gitlab
- module CycleAnalytics
- module Summary
- module Group
- class DeploymentFrequency < Group::Base
- include GroupProjectsProvider
- include SummaryHelper
-
- def initialize(deployments:, group:, options:)
- @deployments = deployments
-
- super(group: group, options: options)
- end
-
- def title
- _('Deployment Frequency')
- end
-
- def value
- @value ||=
- frequency(@deployments, options[:from], options[:to] || Time.now)
- end
-
- def unit
- _('per day')
- end
- end
- end
- end
- end
-end
diff --git a/lib/gitlab/cycle_analytics/summary/group/issue.rb b/lib/gitlab/cycle_analytics/summary/group/issue.rb
deleted file mode 100644
index 4d5ee1d43ca..00000000000
--- a/lib/gitlab/cycle_analytics/summary/group/issue.rb
+++ /dev/null
@@ -1,44 +0,0 @@
-# frozen_string_literal: true
-
-module Gitlab
- module CycleAnalytics
- module Summary
- module Group
- class Issue < Group::Base
- attr_reader :group, :current_user, :options
-
- def initialize(group:, current_user:, options:)
- @group = group
- @current_user = current_user
- @options = options
- end
-
- def title
- n_('New Issue', 'New Issues', value)
- end
-
- def value
- @value ||= find_issues
- end
-
- private
-
- def find_issues
- issues = IssuesFinder.new(current_user, finder_params).execute
- issues = issues.where(projects: { id: options[:projects] }) if options[:projects]
- issues.count
- end
-
- def finder_params
- {
- group_id: group.id,
- include_subgroups: true,
- created_after: options[:from],
- created_before: options[:to]
- }.compact
- end
- end
- end
- end
- end
-end
diff --git a/lib/gitlab/cycle_analytics/summary/issue.rb b/lib/gitlab/cycle_analytics/summary/issue.rb
index 52892eb5a1a..ce7788590b9 100644
--- a/lib/gitlab/cycle_analytics/summary/issue.rb
+++ b/lib/gitlab/cycle_analytics/summary/issue.rb
@@ -16,7 +16,16 @@ module Gitlab
end
def value
- @value ||= IssuesFinder.new(@current_user, project_id: @project.id, created_after: @from, created_before: @to).execute.count
+ @value ||= Value::PrettyNumeric.new(issues_count)
+ end
+
+ private
+
+ def issues_count
+ IssuesFinder
+ .new(@current_user, project_id: @project.id, created_after: @from, created_before: @to)
+ .execute
+ .count
end
end
end
diff --git a/lib/gitlab/cycle_analytics/summary/value.rb b/lib/gitlab/cycle_analytics/summary/value.rb
new file mode 100644
index 00000000000..ce32132e048
--- /dev/null
+++ b/lib/gitlab/cycle_analytics/summary/value.rb
@@ -0,0 +1,42 @@
+# frozen_string_literal: true
+
+module Gitlab
+ module CycleAnalytics
+ module Summary
+ class Value
+ attr_reader :value
+
+ def raw_value
+ value
+ end
+
+ def to_s
+ raise NotImplementedError
+ end
+
+ class None < self
+ def to_s
+ '-'
+ end
+ end
+
+ class Numeric < self
+ def initialize(value)
+ @value = value
+ end
+
+ def to_s
+ value.zero? ? '0' : value.to_s
+ end
+ end
+
+ class PrettyNumeric < Numeric
+ def to_s
+ # 0 is shown as -
+ (value || 0).nonzero? ? super : None.new.to_s
+ end
+ end
+ end
+ end
+ end
+end
diff --git a/lib/gitlab/cycle_analytics/summary_helper.rb b/lib/gitlab/cycle_analytics/summary_helper.rb
index 06abcd151d4..3cf9f463024 100644
--- a/lib/gitlab/cycle_analytics/summary_helper.rb
+++ b/lib/gitlab/cycle_analytics/summary_helper.rb
@@ -4,10 +4,11 @@ module Gitlab
module CycleAnalytics
module SummaryHelper
def frequency(count, from, to)
- return count if count.zero?
+ return Summary::Value::None.new if count.zero?
freq = (count / days(from, to)).round(1)
- freq.zero? ? '0' : freq
+
+ Summary::Value::Numeric.new(freq)
end
def days(from, to)
diff --git a/lib/gitlab/danger/changelog.rb b/lib/gitlab/danger/changelog.rb
index d64177f9565..85f386594be 100644
--- a/lib/gitlab/danger/changelog.rb
+++ b/lib/gitlab/danger/changelog.rb
@@ -14,10 +14,6 @@ module Gitlab
@found ||= git.added_files.find { |path| path =~ %r{\A(ee/)?(changelogs/unreleased)(-ee)?/} }
end
- def presented_no_changelog_labels
- NO_CHANGELOG_LABELS.map { |label| "~#{label}" }.join(', ')
- end
-
def sanitized_mr_title
gitlab.mr_json["title"].gsub(/^WIP: */, '').gsub(/`/, '\\\`')
end
diff --git a/lib/gitlab/danger/commit_linter.rb b/lib/gitlab/danger/commit_linter.rb
index 616c05d0a02..58db2b58560 100644
--- a/lib/gitlab/danger/commit_linter.rb
+++ b/lib/gitlab/danger/commit_linter.rb
@@ -18,7 +18,7 @@ module Gitlab
PROBLEMS = {
subject_too_short: "The %s must contain at least #{MIN_SUBJECT_WORDS_COUNT} words",
subject_too_long: "The %s may not be longer than #{MAX_LINE_LENGTH} characters",
- subject_above_warning: "The %s length is acceptable, but please try to [reduce it to #{WARN_SUBJECT_LENGTH} characters](#{URL_LIMIT_SUBJECT}).",
+ subject_above_warning: "The %s length is acceptable, but please try to [reduce it to #{WARN_SUBJECT_LENGTH} characters](#{URL_LIMIT_SUBJECT})",
subject_starts_with_lowercase: "The %s must start with a capital letter",
subject_ends_with_a_period: "The %s must not end with a period",
separator_missing: "The commit subject and body must be separated by a blank line",
diff --git a/lib/gitlab/danger/emoji_checker.rb b/lib/gitlab/danger/emoji_checker.rb
index e31a6ae5011..a2867087428 100644
--- a/lib/gitlab/danger/emoji_checker.rb
+++ b/lib/gitlab/danger/emoji_checker.rb
@@ -1,6 +1,6 @@
# frozen_string_literal: true
-require 'json'
+require_relative '../json'
module Gitlab
module Danger
@@ -25,8 +25,8 @@ module Gitlab
)}x.freeze
def initialize
- names = JSON.parse(File.read(DIGESTS)).keys +
- JSON.parse(File.read(ALIASES)).keys
+ names = Gitlab::Json.parse(File.read(DIGESTS)).keys +
+ Gitlab::Json.parse(File.read(ALIASES)).keys
@emoji = names.map { |name| ":#{name}:" }
end
diff --git a/lib/gitlab/danger/helper.rb b/lib/gitlab/danger/helper.rb
index aa2737262be..0f0af5f777b 100644
--- a/lib/gitlab/danger/helper.rb
+++ b/lib/gitlab/danger/helper.rb
@@ -191,6 +191,23 @@ module Gitlab
gitlab_helper.mr_json['web_url'].include?('/gitlab-org/security/')
end
+ def mr_has_labels?(*labels)
+ return false unless gitlab_helper
+
+ labels = labels.flatten.uniq
+ (labels & gitlab_helper.mr_labels) == labels
+ end
+
+ def labels_list(labels, sep: ', ')
+ labels.map { |label| %Q{~"#{label}"} }.join(sep)
+ end
+
+ def prepare_labels_for_mr(labels)
+ return '' unless labels.any?
+
+ "/label #{labels_list(labels, sep: ' ')}"
+ end
+
private
def has_database_scoped_labels?(current_mr_labels)
diff --git a/lib/gitlab/danger/request_helper.rb b/lib/gitlab/danger/request_helper.rb
index 06da4ed9ad3..ef51c3f2052 100644
--- a/lib/gitlab/danger/request_helper.rb
+++ b/lib/gitlab/danger/request_helper.rb
@@ -16,7 +16,7 @@ module Gitlab
raise HTTPError, "Failed to read #{url}: #{rsp.code} #{rsp.message}"
end
- JSON.parse(rsp.body)
+ Gitlab::Json.parse(rsp.body)
end
end
end
diff --git a/lib/gitlab/danger/teammate.rb b/lib/gitlab/danger/teammate.rb
index 55476cd9789..651b002d2bf 100644
--- a/lib/gitlab/danger/teammate.rb
+++ b/lib/gitlab/danger/teammate.rb
@@ -1,12 +1,19 @@
# frozen_string_literal: true
require 'cgi'
+require 'set'
module Gitlab
module Danger
class Teammate
attr_reader :name, :username, :role, :projects
+ AT_CAPACITY_EMOJI = Set.new(%w[red_circle]).freeze
+ OOO_EMOJI = Set.new(%w[
+ palm_tree
+ beach beach_umbrella beach_with_umbrella
+ ]).freeze
+
def initialize(options = {})
@username = options['username']
@name = options['name'] || @username
@@ -37,10 +44,14 @@ module Gitlab
end
def status
- api_endpoint = "https://gitlab.com/api/v4/users/#{CGI.escape(username)}/status"
- @status ||= Gitlab::Danger::RequestHelper.http_get_json(api_endpoint)
- rescue Gitlab::Danger::RequestHelper::HTTPError, JSON::ParserError
- nil # better no status than a crashing Danger
+ return @status if defined?(@status)
+
+ @status ||=
+ begin
+ Gitlab::Danger::RequestHelper.http_get_json(status_api_endpoint)
+ rescue Gitlab::Danger::RequestHelper::HTTPError, JSON::ParserError
+ nil # better no status than a crashing Danger
+ end
end
# @return [Boolean]
@@ -50,14 +61,22 @@ module Gitlab
private
+ def status_api_endpoint
+ "https://gitlab.com/api/v4/users/#{CGI.escape(username)}/status"
+ end
+
+ def status_emoji
+ status&.dig("emoji")
+ end
+
# @return [Boolean]
def out_of_office?
- status&.dig("message")&.match?(/OOO/i) || false
+ status&.dig("message")&.match?(/OOO/i) || OOO_EMOJI.include?(status_emoji)
end
# @return [Boolean]
def has_capacity?
- status&.dig("emoji") != 'red_circle'
+ !AT_CAPACITY_EMOJI.include?(status_emoji)
end
def has_capability?(project, category, kind, labels)
diff --git a/lib/gitlab/data_builder/wiki_page.rb b/lib/gitlab/data_builder/wiki_page.rb
index 9368446fa59..8aee25e9fe6 100644
--- a/lib/gitlab/data_builder/wiki_page.rb
+++ b/lib/gitlab/data_builder/wiki_page.rb
@@ -8,6 +8,9 @@ module Gitlab
def build(wiki_page, user, action)
wiki = wiki_page.wiki
+ # TODO: group hooks https://gitlab.com/gitlab-org/gitlab/-/issues/216904
+ return {} if wiki.container.is_a?(Group)
+
{
object_kind: wiki_page.class.name.underscore,
user: user.hook_attrs,
diff --git a/lib/gitlab/database/batch_count.rb b/lib/gitlab/database/batch_count.rb
index 2359dceae48..ab069ce1da1 100644
--- a/lib/gitlab/database/batch_count.rb
+++ b/lib/gitlab/database/batch_count.rb
@@ -91,11 +91,17 @@ module Gitlab
def batch_fetch(start, finish, mode)
# rubocop:disable GitlabSecurity/PublicSend
- @relation.select(@column).public_send(mode).where(@column => start..(finish - 1)).count
+ @relation.select(@column).public_send(mode).where(between_condition(start, finish)).count
end
private
+ def between_condition(start, finish)
+ return @column.between(start..(finish - 1)) if @column.is_a?(Arel::Attributes::Attribute)
+
+ { @column => start..(finish - 1) }
+ end
+
def actual_start(start)
start || @relation.minimum(@column) || 0
end
diff --git a/lib/gitlab/database/count/reltuples_count_strategy.rb b/lib/gitlab/database/count/reltuples_count_strategy.rb
index 6cd90c01ab2..e226ed7613a 100644
--- a/lib/gitlab/database/count/reltuples_count_strategy.rb
+++ b/lib/gitlab/database/count/reltuples_count_strategy.rb
@@ -72,7 +72,7 @@ module Gitlab
# @param [Array] table names
# @returns [Hash] Table name to count mapping (e.g. { 'projects' => 5, 'users' => 100 })
def get_statistics(table_names, check_statistics: true)
- time = 1.hour.ago
+ time = 6.hours.ago
query = PgClass.joins("LEFT JOIN pg_stat_user_tables USING (relname)")
.where(relname: table_names)
diff --git a/lib/gitlab/database/migration_helpers.rb b/lib/gitlab/database/migration_helpers.rb
index cf5ff8ddb7b..96be057f77e 100644
--- a/lib/gitlab/database/migration_helpers.rb
+++ b/lib/gitlab/database/migration_helpers.rb
@@ -265,12 +265,19 @@ module Gitlab
# or `RESET ALL` is executed
def disable_statement_timeout
if block_given?
- begin
- execute('SET statement_timeout TO 0')
-
+ if statement_timeout_disabled?
+ # Don't do anything if the statement_timeout is already disabled
+ # Allows for nested calls of disable_statement_timeout without
+ # resetting the timeout too early (before the outer call ends)
yield
- ensure
- execute('RESET ALL')
+ else
+ begin
+ execute('SET statement_timeout TO 0')
+
+ yield
+ ensure
+ execute('RESET ALL')
+ end
end
else
unless transaction_open?
@@ -378,7 +385,7 @@ module Gitlab
# make things _more_ complex).
#
# `batch_column_name` option is for tables without primary key, in this
- # case an other unique integer column can be used. Example: :user_id
+ # case another unique integer column can be used. Example: :user_id
#
# rubocop: disable Metrics/AbcSize
def update_column_in_batches(table, column, value, batch_size: nil, batch_column_name: :id)
@@ -444,66 +451,13 @@ module Gitlab
# Adds a column with a default value without locking an entire table.
#
- # This method runs the following steps:
- #
- # 1. Add the column with a default value of NULL.
- # 2. Change the default value of the column to the specified value.
- # 3. Update all existing rows in batches.
- # 4. Set a `NOT NULL` constraint on the column if desired (the default).
- #
- # These steps ensure a column can be added to a large and commonly used
- # table without locking the entire table for the duration of the table
- # modification.
- #
- # table - The name of the table to update.
- # column - The name of the column to add.
- # type - The column type (e.g. `:integer`).
- # default - The default value for the column.
- # limit - Sets a column limit. For example, for :integer, the default is
- # 4-bytes. Set `limit: 8` to allow 8-byte integers.
- # allow_null - When set to `true` the column will allow NULL values, the
- # default is to not allow NULL values.
- #
- # This method can also take a block which is passed directly to the
- # `update_column_in_batches` method.
- def add_column_with_default(table, column, type, default:, limit: nil, allow_null: false, update_column_in_batches_args: {}, &block)
- if transaction_open?
- raise 'add_column_with_default can not be run inside a transaction, ' \
- 'you can disable transactions by calling disable_ddl_transaction! ' \
- 'in the body of your migration class'
- end
-
- disable_statement_timeout do
- transaction do
- if limit
- add_column(table, column, type, default: nil, limit: limit)
- else
- add_column(table, column, type, default: nil)
- end
-
- # Changing the default before the update ensures any newly inserted
- # rows already use the proper default value.
- change_column_default(table, column, default)
- end
-
- begin
- default_after_type_cast = connection.type_cast(default, column_for(table, column))
+ # @deprecated With PostgreSQL 11, adding columns with a default does not lead to a table rewrite anymore.
+ # As such, this method is not needed anymore and the default `add_column` helper should be used.
+ # This helper is subject to be removed in a >13.0 release.
+ def add_column_with_default(table, column, type, default:, limit: nil, allow_null: false)
+ raise 'Deprecated: add_column_with_default does not support being passed blocks anymore' if block_given?
- if update_column_in_batches_args.any?
- update_column_in_batches(table, column, default_after_type_cast, **update_column_in_batches_args, &block)
- else
- update_column_in_batches(table, column, default_after_type_cast, &block)
- end
-
- change_column_null(table, column, false) unless allow_null
- # We want to rescue _all_ exceptions here, even those that don't inherit
- # from StandardError.
- rescue Exception => error # rubocop: disable all
- remove_column(table, column)
-
- raise error
- end
- end
+ add_column(table, column, type, default: default, limit: limit, null: allow_null)
end
# Renames a column without requiring downtime.
@@ -519,14 +473,20 @@ module Gitlab
# new - The new column name.
# type - The type of the new column. If no type is given the old column's
# type is used.
- def rename_column_concurrently(table, old, new, type: nil)
+ # batch_column_name - option is for tables without primary key, in this
+ # case another unique integer column can be used. Example: :user_id
+ def rename_column_concurrently(table, old, new, type: nil, batch_column_name: :id)
+ unless column_exists?(table, batch_column_name)
+ raise "Column #{batch_column_name} does not exist on #{table}"
+ end
+
if transaction_open?
raise 'rename_column_concurrently can not be run inside a transaction'
end
check_trigger_permissions!(table)
- create_column_from(table, old, new, type: type)
+ create_column_from(table, old, new, type: type, batch_column_name: batch_column_name)
install_rename_triggers(table, old, new)
end
@@ -626,14 +586,20 @@ module Gitlab
# new - The new column name.
# type - The type of the old column. If no type is given the new column's
# type is used.
- def undo_cleanup_concurrent_column_rename(table, old, new, type: nil)
+ # batch_column_name - option is for tables without primary key, in this
+ # case another unique integer column can be used. Example: :user_id
+ def undo_cleanup_concurrent_column_rename(table, old, new, type: nil, batch_column_name: :id)
+ unless column_exists?(table, batch_column_name)
+ raise "Column #{batch_column_name} does not exist on #{table}"
+ end
+
if transaction_open?
raise 'undo_cleanup_concurrent_column_rename can not be run inside a transaction'
end
check_trigger_permissions!(table)
- create_column_from(table, new, old, type: type)
+ create_column_from(table, new, old, type: type, batch_column_name: batch_column_name)
install_rename_triggers(table, old, new)
end
@@ -1063,6 +1029,8 @@ into similar problems in the future (e.g. when new tables are created).
# batch_size - The maximum number of rows per job
# other_arguments - Other arguments to send to the job
#
+ # *Returns the final migration delay*
+ #
# Example:
#
# class Route < ActiveRecord::Base
@@ -1079,7 +1047,7 @@ into similar problems in the future (e.g. when new tables are created).
# # do something
# end
# end
- def queue_background_migration_jobs_by_range_at_intervals(model_class, job_class_name, delay_interval, batch_size: BACKGROUND_MIGRATION_BATCH_SIZE, other_arguments: [])
+ def queue_background_migration_jobs_by_range_at_intervals(model_class, job_class_name, delay_interval, batch_size: BACKGROUND_MIGRATION_BATCH_SIZE, other_job_arguments: [], initial_delay: 0)
raise "#{model_class} does not have an ID to use for batch ranges" unless model_class.column_names.include?('id')
# To not overload the worker too much we enforce a minimum interval both
@@ -1088,14 +1056,19 @@ into similar problems in the future (e.g. when new tables are created).
delay_interval = BackgroundMigrationWorker.minimum_interval
end
+ final_delay = 0
+
model_class.each_batch(of: batch_size) do |relation, index|
start_id, end_id = relation.pluck(Arel.sql('MIN(id), MAX(id)')).first
# `BackgroundMigrationWorker.bulk_perform_in` schedules all jobs for
# the same time, which is not helpful in most cases where we wish to
# spread the work over time.
- migrate_in(delay_interval * index, job_class_name, [start_id, end_id] + other_arguments)
+ final_delay = initial_delay + delay_interval * index
+ migrate_in(final_delay, job_class_name, [start_id, end_id] + other_job_arguments)
end
+
+ final_delay
end
# Fetches indexes on a column by name for postgres.
@@ -1315,12 +1288,73 @@ into similar problems in the future (e.g. when new tables are created).
check_constraint_exists?(table, text_limit_name(table, column, name: constraint_name))
end
+ # Migration Helpers for managing not null constraints
+ def add_not_null_constraint(table, column, constraint_name: nil, validate: true)
+ if column_is_nullable?(table, column)
+ add_check_constraint(
+ table,
+ "#{column} IS NOT NULL",
+ not_null_constraint_name(table, column, name: constraint_name),
+ validate: validate
+ )
+ else
+ warning_message = <<~MESSAGE
+ NOT NULL check constraint was not created:
+ column #{table}.#{column} is already defined as `NOT NULL`
+ MESSAGE
+
+ Rails.logger.warn warning_message
+ end
+ end
+
+ def validate_not_null_constraint(table, column, constraint_name: nil)
+ validate_check_constraint(
+ table,
+ not_null_constraint_name(table, column, name: constraint_name)
+ )
+ end
+
+ def remove_not_null_constraint(table, column, constraint_name: nil)
+ remove_check_constraint(
+ table,
+ not_null_constraint_name(table, column, name: constraint_name)
+ )
+ end
+
+ def check_not_null_constraint_exists?(table, column, constraint_name: nil)
+ check_constraint_exists?(
+ table,
+ not_null_constraint_name(table, column, name: constraint_name)
+ )
+ end
+
private
+ def statement_timeout_disabled?
+ # This is a string of the form "100ms" or "0" when disabled
+ connection.select_value('SHOW statement_timeout') == "0"
+ end
+
+ def column_is_nullable?(table, column)
+ # Check if table.column has not been defined with NOT NULL
+ check_sql = <<~SQL
+ SELECT c.is_nullable
+ FROM information_schema.columns c
+ WHERE c.table_name = '#{table}'
+ AND c.column_name = '#{column}'
+ SQL
+
+ connection.select_value(check_sql) == 'YES'
+ end
+
def text_limit_name(table, column, name: nil)
name.presence || check_constraint_name(table, column, 'max_length')
end
+ def not_null_constraint_name(table, column, name: nil)
+ name.presence || check_constraint_name(table, column, 'not_null')
+ end
+
def missing_schema_object_message(table, type, name)
<<~MESSAGE
Could not find #{type} "#{name}" on table "#{table}" which was referenced during the migration.
@@ -1348,7 +1382,7 @@ into similar problems in the future (e.g. when new tables are created).
"ON DELETE #{on_delete.upcase}"
end
- def create_column_from(table, old, new, type: nil)
+ def create_column_from(table, old, new, type: nil, batch_column_name: :id)
old_col = column_for(table, old)
new_type = type || old_col.type
@@ -1362,9 +1396,9 @@ into similar problems in the future (e.g. when new tables are created).
# necessary since we copy over old values further down.
change_column_default(table, new, old_col.default) unless old_col.default.nil?
- update_column_in_batches(table, new, Arel::Table.new(table)[old])
+ update_column_in_batches(table, new, Arel::Table.new(table)[old], batch_column_name: batch_column_name)
- change_column_null(table, new, false) unless old_col.null
+ add_not_null_constraint(table, new) unless old_col.null
copy_indexes(table, old, new)
copy_foreign_keys(table, old, new)
diff --git a/lib/gitlab/database/partitioning_migration_helpers.rb b/lib/gitlab/database/partitioning_migration_helpers.rb
new file mode 100644
index 00000000000..55649ebbf8a
--- /dev/null
+++ b/lib/gitlab/database/partitioning_migration_helpers.rb
@@ -0,0 +1,122 @@
+# frozen_string_literal: true
+
+module Gitlab
+ module Database
+ module PartitioningMigrationHelpers
+ include SchemaHelpers
+
+ def add_partitioned_foreign_key(from_table, to_table, column: nil, primary_key: :id, on_delete: :cascade)
+ cascade_delete = extract_cascade_option(on_delete)
+
+ update_foreign_keys(from_table, to_table, column, primary_key, cascade_delete) do |current_keys, existing_key, specified_key|
+ if existing_key.nil?
+ unless specified_key.save
+ raise "failed to create foreign key: #{specified_key.errors.full_messages.to_sentence}"
+ end
+
+ current_keys << specified_key
+ else
+ Rails.logger.warn "foreign key not added because it already exists: #{specified_key}" # rubocop:disable Gitlab/RailsLogger
+ current_keys
+ end
+ end
+ end
+
+ def remove_partitioned_foreign_key(from_table, to_table, column: nil, primary_key: :id)
+ update_foreign_keys(from_table, to_table, column, primary_key) do |current_keys, existing_key, specified_key|
+ if existing_key
+ existing_key.destroy!
+ current_keys.delete(existing_key)
+ else
+ Rails.logger.warn "foreign key not removed because it doesn't exist: #{specified_key}" # rubocop:disable Gitlab/RailsLogger
+ end
+
+ current_keys
+ end
+ end
+
+ def fk_function_name(table)
+ object_name(table, 'fk_cascade_function')
+ end
+
+ def fk_trigger_name(table)
+ object_name(table, 'fk_cascade_trigger')
+ end
+
+ private
+
+ def fk_from_spec(from_table, to_table, from_column, to_column, cascade_delete)
+ PartitionedForeignKey.new(from_table: from_table.to_s, to_table: to_table.to_s, from_column: from_column.to_s,
+ to_column: to_column.to_s, cascade_delete: cascade_delete)
+ end
+
+ def update_foreign_keys(from_table, to_table, from_column, to_column, cascade_delete = nil)
+ if transaction_open?
+ raise 'partitioned foreign key operations can not be run inside a transaction block, ' \
+ 'you can disable transaction blocks by calling disable_ddl_transaction! ' \
+ 'in the body of your migration class'
+ end
+
+ from_column ||= "#{to_table.to_s.singularize}_id"
+ specified_key = fk_from_spec(from_table, to_table, from_column, to_column, cascade_delete)
+
+ current_keys = PartitionedForeignKey.by_referenced_table(to_table).to_a
+ existing_key = find_existing_key(current_keys, specified_key)
+
+ final_keys = yield current_keys, existing_key, specified_key
+
+ fn_name = fk_function_name(to_table)
+ trigger_name = fk_trigger_name(to_table)
+
+ with_lock_retries do
+ drop_trigger(to_table, trigger_name, if_exists: true)
+
+ if final_keys.empty?
+ drop_function(fn_name, if_exists: true)
+ else
+ create_or_replace_fk_function(fn_name, final_keys)
+ create_function_trigger(trigger_name, fn_name, fires: "AFTER DELETE ON #{to_table}")
+ end
+ end
+ end
+
+ def extract_cascade_option(on_delete)
+ case on_delete
+ when :cascade then true
+ when :nullify then false
+ else raise ArgumentError, "invalid option #{on_delete} for :on_delete"
+ end
+ end
+
+ def with_lock_retries(&block)
+ Gitlab::Database::WithLockRetries.new({
+ klass: self.class,
+ logger: Gitlab::BackgroundMigration::Logger
+ }).run(&block)
+ end
+
+ def find_existing_key(keys, key)
+ keys.find { |k| k.from_table == key.from_table && k.from_column == key.from_column }
+ end
+
+ def create_or_replace_fk_function(fn_name, fk_specs)
+ create_trigger_function(fn_name, replace: true) do
+ cascade_statements = build_cascade_statements(fk_specs)
+ cascade_statements << 'RETURN OLD;'
+
+ cascade_statements.join("\n")
+ end
+ end
+
+ def build_cascade_statements(foreign_keys)
+ foreign_keys.map do |fks|
+ if fks.cascade_delete?
+ "DELETE FROM #{fks.from_table} WHERE #{fks.from_column} = OLD.#{fks.to_column};"
+ else
+ "UPDATE #{fks.from_table} SET #{fks.from_column} = NULL WHERE #{fks.from_column} = OLD.#{fks.to_column};"
+ end
+ end
+ end
+ end
+ end
+end
diff --git a/lib/gitlab/database/partitioning_migration_helpers/partitioned_foreign_key.rb b/lib/gitlab/database/partitioning_migration_helpers/partitioned_foreign_key.rb
new file mode 100644
index 00000000000..f9a90511f9b
--- /dev/null
+++ b/lib/gitlab/database/partitioning_migration_helpers/partitioned_foreign_key.rb
@@ -0,0 +1,13 @@
+# frozen_string_literal: true
+
+module Gitlab
+ module Database
+ module PartitioningMigrationHelpers
+ class PartitionedForeignKey < ApplicationRecord
+ validates_with PartitionedForeignKeyValidator
+
+ scope :by_referenced_table, ->(table) { where(to_table: table) }
+ end
+ end
+ end
+end
diff --git a/lib/gitlab/database/partitioning_migration_helpers/partitioned_foreign_key_validator.rb b/lib/gitlab/database/partitioning_migration_helpers/partitioned_foreign_key_validator.rb
new file mode 100644
index 00000000000..089cf2b8931
--- /dev/null
+++ b/lib/gitlab/database/partitioning_migration_helpers/partitioned_foreign_key_validator.rb
@@ -0,0 +1,28 @@
+# frozen_string_literal: true
+
+module Gitlab
+ module Database
+ module PartitioningMigrationHelpers
+ class PartitionedForeignKeyValidator < ActiveModel::Validator
+ def validate(record)
+ validate_key_part(record, :from_table, :from_column)
+ validate_key_part(record, :to_table, :to_column)
+ end
+
+ private
+
+ def validate_key_part(record, table_field, column_field)
+ if !connection.table_exists?(record[table_field])
+ record.errors.add(table_field, 'must be a valid table')
+ elsif !connection.column_exists?(record[table_field], record[column_field])
+ record.errors.add(column_field, 'must be a valid column')
+ end
+ end
+
+ def connection
+ ActiveRecord::Base.connection
+ end
+ end
+ end
+ end
+end
diff --git a/lib/gitlab/database/rename_reserved_paths_migration/v1/rename_base.rb b/lib/gitlab/database/rename_reserved_paths_migration/v1/rename_base.rb
index 565f34b78b7..2c9d0d6c0d1 100644
--- a/lib/gitlab/database/rename_reserved_paths_migration/v1/rename_base.rb
+++ b/lib/gitlab/database/rename_reserved_paths_migration/v1/rename_base.rb
@@ -157,7 +157,7 @@ module Gitlab
failed_reverts = []
while rename_info = redis.lpop(key)
- path_before_rename, path_after_rename = JSON.parse(rename_info)
+ path_before_rename, path_after_rename = Gitlab::Json.parse(rename_info)
say "renaming #{type} from #{path_after_rename} back to #{path_before_rename}"
begin
yield(path_before_rename, path_after_rename)
diff --git a/lib/gitlab/database/schema_helpers.rb b/lib/gitlab/database/schema_helpers.rb
new file mode 100644
index 00000000000..f8d01c78ae8
--- /dev/null
+++ b/lib/gitlab/database/schema_helpers.rb
@@ -0,0 +1,52 @@
+# frozen_string_literal: true
+
+module Gitlab
+ module Database
+ module SchemaHelpers
+ def create_trigger_function(name, replace: true)
+ replace_clause = optional_clause(replace, "OR REPLACE")
+ execute(<<~SQL)
+ CREATE #{replace_clause} FUNCTION #{name}()
+ RETURNS TRIGGER AS
+ $$
+ BEGIN
+ #{yield}
+ END
+ $$ LANGUAGE PLPGSQL
+ SQL
+ end
+
+ def create_function_trigger(name, fn_name, fires: nil)
+ execute(<<~SQL)
+ CREATE TRIGGER #{name}
+ #{fires}
+ FOR EACH ROW
+ EXECUTE PROCEDURE #{fn_name}()
+ SQL
+ end
+
+ def drop_function(name, if_exists: true)
+ exists_clause = optional_clause(if_exists, "IF EXISTS")
+ execute("DROP FUNCTION #{exists_clause} #{name}()")
+ end
+
+ def drop_trigger(table_name, name, if_exists: true)
+ exists_clause = optional_clause(if_exists, "IF EXISTS")
+ execute("DROP TRIGGER #{exists_clause} #{name} ON #{table_name}")
+ end
+
+ def object_name(table, type)
+ identifier = "#{table}_#{type}"
+ hashed_identifier = Digest::SHA256.hexdigest(identifier).first(10)
+
+ "#{type}_#{hashed_identifier}"
+ end
+
+ private
+
+ def optional_clause(flag, clause)
+ flag ? clause : ""
+ end
+ end
+ end
+end
diff --git a/lib/gitlab/database/with_lock_retries.rb b/lib/gitlab/database/with_lock_retries.rb
index 2f36bfa1480..bebcba6f42e 100644
--- a/lib/gitlab/database/with_lock_retries.rb
+++ b/lib/gitlab/database/with_lock_retries.rb
@@ -78,12 +78,18 @@ module Gitlab
run_block_with_transaction
rescue ActiveRecord::LockWaitTimeout
if retry_with_lock_timeout?
+ disable_idle_in_transaction_timeout
wait_until_next_retry
+ reset_db_settings
retry
else
+ reset_db_settings
run_block_without_lock_timeout
end
+
+ ensure
+ reset_db_settings
end
end
@@ -153,6 +159,14 @@ module Gitlab
def current_sleep_time_in_seconds
timing_configuration[current_iteration - 1][1].to_f
end
+
+ def disable_idle_in_transaction_timeout
+ execute("SET LOCAL idle_in_transaction_session_timeout TO '0'")
+ end
+
+ def reset_db_settings
+ execute('RESET idle_in_transaction_session_timeout; RESET lock_timeout')
+ end
end
end
end
diff --git a/lib/gitlab/dependency_linker/json_linker.rb b/lib/gitlab/dependency_linker/json_linker.rb
index 298d214df61..86dc7efb0d9 100644
--- a/lib/gitlab/dependency_linker/json_linker.rb
+++ b/lib/gitlab/dependency_linker/json_linker.rb
@@ -39,7 +39,7 @@ module Gitlab
end
def json
- @json ||= JSON.parse(plain_text) rescue nil
+ @json ||= Gitlab::Json.parse(plain_text) rescue nil
end
end
end
diff --git a/lib/gitlab/diff/file.rb b/lib/gitlab/diff/file.rb
index 4fc5bfddf0c..d1398ddb642 100644
--- a/lib/gitlab/diff/file.rb
+++ b/lib/gitlab/diff/file.rb
@@ -314,6 +314,10 @@ module Gitlab
@rich_viewer = rich_viewer_class&.new(self)
end
+ def alternate_viewer
+ alternate_viewer_class&.new(self)
+ end
+
def rendered_as_text?(ignore_errors: true)
simple_viewer.is_a?(DiffViewer::Text) && (ignore_errors || simple_viewer.render_error.nil?)
end
@@ -353,7 +357,7 @@ module Gitlab
def fetch_blob(sha, path)
return unless sha
- Blob.lazy(repository.project, sha, path)
+ Blob.lazy(repository, sha, path)
end
def total_blob_lines(blob)
@@ -419,6 +423,17 @@ module Gitlab
return if collapsed?
return unless diffable?
return unless modified_file?
+
+ find_renderable_viewer_class(classes)
+ end
+
+ def alternate_viewer_class
+ return unless viewer.class == DiffViewer::Renamed
+
+ find_renderable_viewer_class(RICH_VIEWERS) || (DiffViewer::Text if text?)
+ end
+
+ def find_renderable_viewer_class(classes)
return if different_type? || external_storage_error?
verify_binary = !stored_externally?
diff --git a/lib/gitlab/diff/formatters/text_formatter.rb b/lib/gitlab/diff/formatters/text_formatter.rb
index 728457b3139..9ea9bdfdf15 100644
--- a/lib/gitlab/diff/formatters/text_formatter.rb
+++ b/lib/gitlab/diff/formatters/text_formatter.rb
@@ -45,7 +45,8 @@ module Gitlab
def ==(other)
other.is_a?(self.class) &&
new_line == other.new_line &&
- old_line == other.old_line
+ old_line == other.old_line &&
+ line_range == other.line_range
end
end
end
diff --git a/lib/gitlab/diff/highlight_cache.rb b/lib/gitlab/diff/highlight_cache.rb
index 01ec9798fe4..ccf09b37b9b 100644
--- a/lib/gitlab/diff/highlight_cache.rb
+++ b/lib/gitlab/diff/highlight_cache.rb
@@ -156,7 +156,7 @@ module Gitlab
end
results.map! do |result|
- JSON.parse(extract_data(result), symbolize_names: true) unless result.nil?
+ Gitlab::Json.parse(extract_data(result), symbolize_names: true) unless result.nil?
end
file_paths.zip(results).to_h
diff --git a/lib/gitlab/diff/position.rb b/lib/gitlab/diff/position.rb
index 8b99fd5cd42..10ad23b7774 100644
--- a/lib/gitlab/diff/position.rb
+++ b/lib/gitlab/diff/position.rb
@@ -68,7 +68,7 @@ module Gitlab
end
def to_json(opts = nil)
- JSON.generate(formatter.to_h, opts)
+ Gitlab::Json.generate(formatter.to_h, opts)
end
def as_json(opts = nil)
diff --git a/lib/gitlab/discussions_diff/highlight_cache.rb b/lib/gitlab/discussions_diff/highlight_cache.rb
index 1f64883cb69..75d5a5df74b 100644
--- a/lib/gitlab/discussions_diff/highlight_cache.rb
+++ b/lib/gitlab/discussions_diff/highlight_cache.rb
@@ -42,7 +42,7 @@ module Gitlab
content.map! do |lines|
next unless lines
- JSON.parse(lines).map! do |line|
+ Gitlab::Json.parse(lines).map! do |line|
Gitlab::Diff::Line.safe_init_from_hash(line)
end
end
diff --git a/lib/gitlab/elasticsearch/logs/lines.rb b/lib/gitlab/elasticsearch/logs/lines.rb
index fb32a6c9fcd..ff9185dd331 100644
--- a/lib/gitlab/elasticsearch/logs/lines.rb
+++ b/lib/gitlab/elasticsearch/logs/lines.rb
@@ -13,7 +13,7 @@ module Gitlab
@client = client
end
- def pod_logs(namespace, pod_name: nil, container_name: nil, search: nil, start_time: nil, end_time: nil, cursor: nil)
+ def pod_logs(namespace, pod_name: nil, container_name: nil, search: nil, start_time: nil, end_time: nil, cursor: nil, chart_above_v2: true)
query = { bool: { must: [] } }.tap do |q|
filter_pod_name(q, pod_name)
filter_namespace(q, namespace)
@@ -22,7 +22,7 @@ module Gitlab
filter_times(q, start_time, end_time)
end
- body = build_body(query, cursor)
+ body = build_body(query, cursor, chart_above_v2)
response = @client.search body: body
format_response(response)
@@ -30,13 +30,14 @@ module Gitlab
private
- def build_body(query, cursor = nil)
+ def build_body(query, cursor = nil, chart_above_v2 = true)
+ offset_field = chart_above_v2 ? "log.offset" : "offset"
body = {
query: query,
# reverse order so we can query N-most recent records
sort: [
{ "@timestamp": { order: :desc } },
- { "offset": { order: :desc } }
+ { "#{offset_field}": { order: :desc } }
],
# only return these fields in the response
_source: ["@timestamp", "message", "kubernetes.pod.name"],
diff --git a/lib/gitlab/email/handler.rb b/lib/gitlab/email/handler.rb
index e9a7c9bcf5c..7f8dd815103 100644
--- a/lib/gitlab/email/handler.rb
+++ b/lib/gitlab/email/handler.rb
@@ -3,18 +3,16 @@
module Gitlab
module Email
module Handler
- prepend_if_ee('::EE::Gitlab::Email::Handler') # rubocop: disable Cop/InjectEnterpriseEditionModule
-
def self.handlers
@handlers ||= load_handlers
end
def self.load_handlers
[
- UnsubscribeHandler,
CreateNoteHandler,
- CreateMergeRequestHandler,
- CreateIssueHandler
+ CreateIssueHandler,
+ UnsubscribeHandler,
+ CreateMergeRequestHandler
]
end
@@ -27,3 +25,5 @@ module Gitlab
end
end
end
+
+Gitlab::Email::Handler.prepend_if_ee('::EE::Gitlab::Email::Handler')
diff --git a/lib/gitlab/email/hook/smime_signature_interceptor.rb b/lib/gitlab/email/hook/smime_signature_interceptor.rb
index 61c9c984f8e..fe39589d019 100644
--- a/lib/gitlab/email/hook/smime_signature_interceptor.rb
+++ b/lib/gitlab/email/hook/smime_signature_interceptor.rb
@@ -10,6 +10,7 @@ module Gitlab
signed_message = Gitlab::Email::Smime::Signer.sign(
cert: certificate.cert,
key: certificate.key,
+ ca_certs: certificate.ca_certs,
data: message.encoded)
signed_email = Mail.new(signed_message)
@@ -21,7 +22,7 @@ module Gitlab
private
def certificate
- @certificate ||= Gitlab::Email::Smime::Certificate.from_files(key_path, cert_path)
+ @certificate ||= Gitlab::Email::Smime::Certificate.from_files(key_path, cert_path, ca_certs_path)
end
def key_path
@@ -32,6 +33,10 @@ module Gitlab
Gitlab.config.gitlab.email_smime.cert_file
end
+ def ca_certs_path
+ Gitlab.config.gitlab.email_smime.ca_certs_file
+ end
+
def overwrite_body(message, signed_email)
# since this is a multipart email, assignment to nil is important,
# otherwise Message#body will add a new mail part
diff --git a/lib/gitlab/email/message/repository_push.rb b/lib/gitlab/email/message/repository_push.rb
index ec412e7a8b1..d55cf3202a6 100644
--- a/lib/gitlab/email/message/repository_push.rb
+++ b/lib/gitlab/email/message/repository_push.rb
@@ -52,7 +52,7 @@ module Gitlab
end
def compare
- @opts[:compare] if @opts[:compare]
+ @opts[:compare]
end
def diff_refs
diff --git a/lib/gitlab/email/smime/certificate.rb b/lib/gitlab/email/smime/certificate.rb
index 59d7b0c3c5b..3607b95b4bc 100644
--- a/lib/gitlab/email/smime/certificate.rb
+++ b/lib/gitlab/email/smime/certificate.rb
@@ -4,29 +4,53 @@ module Gitlab
module Email
module Smime
class Certificate
- attr_reader :key, :cert
+ CERT_REGEX = /-----BEGIN CERTIFICATE-----(?:.|\n)+?-----END CERTIFICATE-----/.freeze
+
+ attr_reader :key, :cert, :ca_certs
def key_string
- @key.to_s
+ key.to_s
end
def cert_string
- @cert.to_pem
+ cert.to_pem
+ end
+
+ def ca_certs_string
+ ca_certs.map(&:to_pem).join('\n') unless ca_certs.blank?
end
- def self.from_strings(key_string, cert_string)
+ def self.from_strings(key_string, cert_string, ca_certs_string = nil)
key = OpenSSL::PKey::RSA.new(key_string)
cert = OpenSSL::X509::Certificate.new(cert_string)
- new(key, cert)
+ ca_certs = load_ca_certs_bundle(ca_certs_string)
+
+ new(key, cert, ca_certs)
end
- def self.from_files(key_path, cert_path)
- from_strings(File.read(key_path), File.read(cert_path))
+ def self.from_files(key_path, cert_path, ca_certs_path = nil)
+ ca_certs_string = File.read(ca_certs_path) if ca_certs_path
+
+ from_strings(File.read(key_path), File.read(cert_path), ca_certs_string)
+ end
+
+ # Returns an array of OpenSSL::X509::Certificate objects, empty array if none found
+ #
+ # Ruby OpenSSL::X509::Certificate.new will only load the first
+ # certificate if a bundle is presented, this allows to parse multiple certs
+ # in the same file
+ def self.load_ca_certs_bundle(ca_certs_string)
+ return [] unless ca_certs_string
+
+ ca_certs_string.scan(CERT_REGEX).map do |ca_cert_string|
+ OpenSSL::X509::Certificate.new(ca_cert_string)
+ end
end
- def initialize(key, cert)
+ def initialize(key, cert, ca_certs = nil)
@key = key
@cert = cert
+ @ca_certs = ca_certs
end
end
end
diff --git a/lib/gitlab/email/smime/signer.rb b/lib/gitlab/email/smime/signer.rb
index db03e383ecf..6a445730463 100644
--- a/lib/gitlab/email/smime/signer.rb
+++ b/lib/gitlab/email/smime/signer.rb
@@ -7,19 +7,32 @@ module Gitlab
module Smime
# Tooling for signing and verifying data with SMIME
class Signer
- def self.sign(cert:, key:, data:)
- signed_data = OpenSSL::PKCS7.sign(cert, key, data, nil, OpenSSL::PKCS7::DETACHED)
+ # The `ca_certs` parameter, if provided, is an array of CA certificates
+ # that will be attached in the signature together with the main `cert`.
+ # This will be typically intermediate CAs
+ def self.sign(cert:, key:, ca_certs: nil, data:)
+ signed_data = OpenSSL::PKCS7.sign(cert, key, data, Array.wrap(ca_certs), 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:)
+ # Return nil if data cannot be verified, otherwise the signed content data
+ #
+ # Be careful with the `ca_certs` parameter, it will implicitly trust all the CAs
+ # in the array by creating a trusted store, stopping validation at the first match
+ # This is relevant when using intermediate CAs, `ca_certs` should only
+ # include the trusted, root CA
+ def self.verify_signature(ca_certs: nil, signed_data:)
store = OpenSSL::X509::Store.new
store.set_default_paths
- store.add_cert(ca_cert) if ca_cert
+ Array.wrap(ca_certs).compact.each { |ca_cert| store.add_cert(ca_cert) }
signed_smime = OpenSSL::PKCS7.read_smime(signed_data)
- signed_smime if signed_smime.verify([cert], store)
+
+ # The S/MIME certificate(s) are included in the message and the trusted
+ # CAs are in the store parameter, so we pass no certs as parameters
+ # to `PKCS7.verify`
+ # See https://www.openssl.org/docs/manmaster/man3/PKCS7_verify.html
+ signed_smime if signed_smime.verify(nil, store)
end
end
end
diff --git a/lib/gitlab/emoji.rb b/lib/gitlab/emoji.rb
index 305bcada2f5..bcf92b35720 100644
--- a/lib/gitlab/emoji.rb
+++ b/lib/gitlab/emoji.rb
@@ -21,7 +21,7 @@ module Gitlab
end
def emojis_aliases
- @emoji_aliases ||= JSON.parse(File.read(Rails.root.join('fixtures', 'emojis', 'aliases.json')))
+ @emoji_aliases ||= Gitlab::Json.parse(File.read(Rails.root.join('fixtures', 'emojis', 'aliases.json')))
end
def emoji_filename(name)
@@ -63,7 +63,7 @@ module Gitlab
def emoji_unicode_versions_by_name
@emoji_unicode_versions_by_name ||=
- JSON.parse(File.read(Rails.root.join('fixtures', 'emojis', 'emoji-unicode-version-map.json')))
+ Gitlab::Json.parse(File.read(Rails.root.join('fixtures', 'emojis', 'emoji-unicode-version-map.json')))
end
end
end
diff --git a/lib/gitlab/exclusive_lease.rb b/lib/gitlab/exclusive_lease.rb
index 425ef5d738a..1d2c1c69423 100644
--- a/lib/gitlab/exclusive_lease.rb
+++ b/lib/gitlab/exclusive_lease.rb
@@ -12,6 +12,9 @@ module Gitlab
# ExclusiveLease.
#
class ExclusiveLease
+ PREFIX = 'gitlab:exclusive_lease'
+ NoKey = Class.new(ArgumentError)
+
LUA_CANCEL_SCRIPT = <<~EOS.freeze
local key, uuid = KEYS[1], ARGV[1]
if redis.call("get", key) == uuid then
@@ -34,13 +37,21 @@ module Gitlab
end
def self.cancel(key, uuid)
+ return unless key.present?
+
Gitlab::Redis::SharedState.with do |redis|
- redis.eval(LUA_CANCEL_SCRIPT, keys: [redis_shared_state_key(key)], argv: [uuid])
+ redis.eval(LUA_CANCEL_SCRIPT, keys: [ensure_prefixed_key(key)], argv: [uuid])
end
end
def self.redis_shared_state_key(key)
- "gitlab:exclusive_lease:#{key}"
+ "#{PREFIX}:#{key}"
+ end
+
+ def self.ensure_prefixed_key(key)
+ raise NoKey unless key.present?
+
+ key.start_with?(PREFIX) ? key : redis_shared_state_key(key)
end
# Removes any existing exclusive_lease from redis
@@ -94,6 +105,11 @@ module Gitlab
ttl if ttl.positive?
end
end
+
+ # Gives up this lease, allowing it to be obtained by others.
+ def cancel
+ self.class.cancel(@redis_shared_state_key, @uuid)
+ end
end
end
diff --git a/lib/gitlab/exclusive_lease_helpers.rb b/lib/gitlab/exclusive_lease_helpers.rb
index 61eb030563d..10762d83588 100644
--- a/lib/gitlab/exclusive_lease_helpers.rb
+++ b/lib/gitlab/exclusive_lease_helpers.rb
@@ -6,29 +6,38 @@ module Gitlab
FailedToObtainLockError = Class.new(StandardError)
##
- # This helper method blocks a process/thread until the other process cancel the obrainted lease key.
+ # This helper method blocks a process/thread until the lease can be acquired, either due to
+ # the lease TTL expiring, or due to the current holder explicitly releasing
+ # their hold.
#
- # Note: It's basically discouraged to use this method in the unicorn's thread,
- # because it holds the connection until all `retries` is consumed.
+ # If the lease cannot be obtained, raises `FailedToObtainLockError`.
+ #
+ # @param [String] key The lock the thread will try to acquire. Only one thread
+ # in one process across all Rails instances can hold this named lock at any
+ # one time.
+ # @param [Float] ttl: The length of time the lock will be valid for. The lock
+ # will be automatically be released after this time, so any work should be
+ # completed within this time.
+ # @param [Integer] retries: The maximum number of times we will re-attempt
+ # to acquire the lock. The maximum number of attempts will be `retries + 1`:
+ # one for the initial attempt, and then one for every re-try.
+ # @param [Float|Proc] sleep_sec: Either a number of seconds to sleep, or
+ # a proc that computes the sleep time given the number of preceding attempts
+ # (from 1 to retries - 1)
+ #
+ # Note: It's basically discouraged to use this method in a unicorn thread,
+ # because this ties up all thread related resources until all `retries` are consumed.
# This could potentially eat up all connection pools.
def in_lock(key, ttl: 1.minute, retries: 10, sleep_sec: 0.01.seconds)
raise ArgumentError, 'Key needs to be specified' unless key
- lease = Gitlab::ExclusiveLease.new(key, timeout: ttl)
- retried = false
-
- until uuid = lease.try_obtain
- # Keep trying until we obtain the lease. To prevent hammering Redis too
- # much we'll wait for a bit.
- sleep(sleep_sec)
- (retries -= 1) < 0 ? break : retried ||= true
- end
+ lease = SleepingLock.new(key, timeout: ttl, delay: sleep_sec)
- raise FailedToObtainLockError, 'Failed to obtain a lock' unless uuid
+ lease.obtain(1 + retries)
- yield(retried)
+ yield(lease.retried?)
ensure
- Gitlab::ExclusiveLease.cancel(key, uuid)
+ lease&.cancel
end
end
end
diff --git a/lib/gitlab/exclusive_lease_helpers/sleeping_lock.rb b/lib/gitlab/exclusive_lease_helpers/sleeping_lock.rb
new file mode 100644
index 00000000000..8213c9bc042
--- /dev/null
+++ b/lib/gitlab/exclusive_lease_helpers/sleeping_lock.rb
@@ -0,0 +1,50 @@
+# frozen_string_literal: true
+
+module Gitlab
+ module ExclusiveLeaseHelpers
+ # Wrapper around ExclusiveLease that adds retry logic
+ class SleepingLock
+ delegate :cancel, to: :@lease
+
+ def initialize(key, timeout:, delay:)
+ @lease = ::Gitlab::ExclusiveLease.new(key, timeout: timeout)
+ @delay = delay
+ @attempts = 0
+ end
+
+ def obtain(max_attempts)
+ until held?
+ raise FailedToObtainLockError, 'Failed to obtain a lock' if attempts >= max_attempts
+
+ sleep(sleep_sec) unless first_attempt?
+ try_obtain
+ end
+ end
+
+ def retried?
+ attempts > 1
+ end
+
+ private
+
+ attr_reader :delay, :attempts
+
+ def held?
+ @uuid.present?
+ end
+
+ def try_obtain
+ @uuid ||= @lease.try_obtain
+ @attempts += 1
+ end
+
+ def first_attempt?
+ attempts.zero?
+ end
+
+ def sleep_sec
+ delay.respond_to?(:call) ? delay.call(attempts) : delay
+ end
+ end
+ end
+end
diff --git a/lib/gitlab/experimentation.rb b/lib/gitlab/experimentation.rb
index 68f3c9e332e..3495b4a0b72 100644
--- a/lib/gitlab/experimentation.rb
+++ b/lib/gitlab/experimentation.rb
@@ -2,48 +2,53 @@
# == Experimentation
#
-# Utility module used for A/B testing experimental features. Define your experiments in the `EXPERIMENTS` constant.
-# The feature_toggle and environment keys are optional. If the feature_toggle is not set, a feature with the name of
-# the experiment will be checked, with a default value of true. The enabled_ratio is required and should be
-# the ratio for the number of users for which this experiment is enabled. For example: a ratio of 0.1 will
-# enable the experiment for 10% of the users (determined by the `experimentation_subject_index`).
+# Utility module for A/B testing experimental features. Define your experiments in the `EXPERIMENTS` constant.
+# Experiment options:
+# - environment (optional, defaults to enabled for development and GitLab.com)
+# - tracking_category (optional, used to set the category when tracking an experiment event)
+#
+# The experiment is controlled by a Feature Flag (https://docs.gitlab.com/ee/development/feature_flags/controls.html),
+# which is named "#{experiment_key}_experiment_percentage" and *must* be set with a percentage and not be used for other purposes.
+#
+# To enable the experiment for 10% of the users:
+#
+# chatops: `/chatops run feature set experiment_key_experiment_percentage 10`
+# console: `Feature.get(:experiment_key_experiment_percentage).enable_percentage_of_time(10)`
+#
+# To disable the experiment:
+#
+# chatops: `/chatops run feature delete experiment_key_experiment_percentage`
+# console: `Feature.get(:experiment_key_experiment_percentage).remove`
+#
+# To check the current rollout percentage:
+#
+# chatops: `/chatops run feature get experiment_key_experiment_percentage`
+# console: `Feature.get(:experiment_key_experiment_percentage).percentage_of_time_value`
#
module Gitlab
module Experimentation
EXPERIMENTS = {
signup_flow: {
- feature_toggle: :experimental_separate_sign_up_flow,
- environment: ::Gitlab.dev_env_or_com?,
- enabled_ratio: 1,
tracking_category: 'Growth::Acquisition::Experiment::SignUpFlow'
},
- paid_signup_flow: {
- feature_toggle: :paid_signup_flow,
- environment: ::Gitlab.dev_env_or_com?,
- enabled_ratio: 1,
- tracking_category: 'Growth::Acquisition::Experiment::PaidSignUpFlow'
+ onboarding_issues: {
+ tracking_category: 'Growth::Conversion::Experiment::OnboardingIssues'
},
suggest_pipeline: {
- feature_toggle: :suggest_pipeline,
- environment: ::Gitlab.dev_env_or_com?,
- enabled_ratio: 0.1,
tracking_category: 'Growth::Expansion::Experiment::SuggestPipeline'
},
ci_notification_dot: {
- feature_toggle: :ci_notification_dot,
- environment: ::Gitlab.dev_env_or_com?,
- enabled_ratio: 0.1,
tracking_category: 'Growth::Expansion::Experiment::CiNotificationDot'
},
buy_ci_minutes_version_a: {
- feature_toggle: :buy_ci_minutes_version_a,
- environment: ::Gitlab.dev_env_or_com?,
- enabled_ratio: 0.2,
tracking_category: 'Growth::Expansion::Experiment::BuyCiMinutesVersionA'
+ },
+ upgrade_link_in_user_menu_a: {
+ tracking_category: 'Growth::Expansion::Experiment::UpgradeLinkInUserMenuA'
}
}.freeze
- # Controller concern that checks if an experimentation_subject_id cookie is present and sets it if absent.
+ # Controller concern that checks if an `experimentation_subject_id cookie` is present and sets it if absent.
# Used for A/B testing of experimental features. Exposes the `experiment_enabled?(experiment_name)` method
# to controllers and views. It returns true when the experiment is enabled and the user is selected as part
# of the experimental group.
@@ -144,7 +149,7 @@ module Gitlab
return false unless EXPERIMENTS.key?(experiment_key)
experiment = experiment(experiment_key)
- experiment.feature_toggle_enabled? && experiment.enabled_for_environment?
+ experiment.enabled? && experiment.enabled_for_environment?
end
def enabled_for_user?(experiment_key, experimentation_subject_index)
@@ -153,23 +158,28 @@ module Gitlab
end
end
- Experiment = Struct.new(:key, :feature_toggle, :environment, :enabled_ratio, :tracking_category, keyword_init: true) do
- def feature_toggle_enabled?
- return Feature.enabled?(key, default_enabled: true) if feature_toggle.nil?
-
- Feature.enabled?(feature_toggle)
+ Experiment = Struct.new(:key, :environment, :tracking_category, keyword_init: true) do
+ def enabled?
+ experiment_percentage.positive?
end
def enabled_for_environment?
- return true if environment.nil?
+ return ::Gitlab.dev_env_or_com? if environment.nil?
environment
end
def enabled_for_experimentation_subject?(experimentation_subject_index)
- return false if enabled_ratio.nil? || experimentation_subject_index.blank?
+ return false if experimentation_subject_index.blank?
+
+ experimentation_subject_index <= experiment_percentage
+ end
+
+ private
- experimentation_subject_index <= enabled_ratio * 100
+ # When a feature does not exist, the `percentage_of_time_value` method will return 0
+ def experiment_percentage
+ @experiment_percentage ||= Feature.get(:"#{key}_experiment_percentage").percentage_of_time_value
end
end
end
diff --git a/lib/gitlab/external_authorization/response.rb b/lib/gitlab/external_authorization/response.rb
index 4f3fe5882db..04f9688fad0 100644
--- a/lib/gitlab/external_authorization/response.rb
+++ b/lib/gitlab/external_authorization/response.rb
@@ -28,7 +28,7 @@ module Gitlab
end
def parse_response!
- JSON.parse(@excon_response.body)
+ Gitlab::Json.parse(@excon_response.body)
rescue JSON::JSONError
# The JSON response is optional, so don't fail when it's missing
nil
diff --git a/lib/gitlab/git/attributes_parser.rb b/lib/gitlab/git/attributes_parser.rb
index 8b9d74ae8e7..630b1aba2f5 100644
--- a/lib/gitlab/git/attributes_parser.rb
+++ b/lib/gitlab/git/attributes_parser.rb
@@ -85,6 +85,8 @@ module Gitlab
yield line.strip
end
+ # Catch invalid byte sequences
+ rescue ArgumentError
end
private
diff --git a/lib/gitlab/git/commit.rb b/lib/gitlab/git/commit.rb
index 605084f1ec2..a554dc0b667 100644
--- a/lib/gitlab/git/commit.rb
+++ b/lib/gitlab/git/commit.rb
@@ -57,11 +57,8 @@ module Gitlab
# Already a commit?
return commit_id if commit_id.is_a?(Gitlab::Git::Commit)
- # Some weird thing?
- return unless commit_id.is_a?(String)
-
# This saves us an RPC round trip.
- return if commit_id.include?(':')
+ return unless valid?(commit_id)
commit = find_commit(repo, commit_id)
@@ -431,6 +428,15 @@ module Gitlab
def fetch_body_from_gitaly
self.class.get_message(@repository, id)
end
+
+ def self.valid?(commit_id)
+ commit_id.is_a?(String) && !(
+ commit_id.start_with?('-') ||
+ commit_id.include?(':') ||
+ commit_id.include?("\x00") ||
+ commit_id.match?(/\s/)
+ )
+ end
end
end
end
diff --git a/lib/gitlab/git/tag.rb b/lib/gitlab/git/tag.rb
index 08dbd52e3fb..da86d6baf4a 100644
--- a/lib/gitlab/git/tag.rb
+++ b/lib/gitlab/git/tag.rb
@@ -66,6 +66,27 @@ module Gitlab
@raw_tag.tagger
end
+ def has_signature?
+ signature_type != :NONE
+ end
+
+ def signature_type
+ @raw_tag.signature_type || :NONE
+ end
+
+ def signature
+ return unless has_signature?
+
+ case signature_type
+ when :PGP
+ nil # not implemented, see https://gitlab.com/gitlab-org/gitlab/issues/19260
+ when :X509
+ X509::Tag.new(@raw_tag).signature
+ else
+ nil
+ end
+ end
+
private
def message_from_gitaly_tag
diff --git a/lib/gitlab/git_access_design.rb b/lib/gitlab/git_access_design.rb
new file mode 100644
index 00000000000..36604bd0b3b
--- /dev/null
+++ b/lib/gitlab/git_access_design.rb
@@ -0,0 +1,28 @@
+# frozen_string_literal: true
+
+module Gitlab
+ class GitAccessDesign < GitAccess
+ def check(_cmd, _changes)
+ check_protocol!
+ check_can_create_design!
+
+ success_result
+ end
+
+ private
+
+ def check_protocol!
+ if protocol != 'web'
+ raise ::Gitlab::GitAccess::ForbiddenError, "Designs are only accessible using the web interface"
+ end
+ end
+
+ def check_can_create_design!
+ unless user&.can?(:create_design, project)
+ raise ::Gitlab::GitAccess::ForbiddenError, "You are not allowed to manage designs of this project"
+ end
+ end
+ end
+end
+
+Gitlab::GitAccessDesign.prepend_if_ee('EE::Gitlab::GitAccessDesign')
diff --git a/lib/gitlab/git_access_snippet.rb b/lib/gitlab/git_access_snippet.rb
index d05ffe9b508..70db4271469 100644
--- a/lib/gitlab/git_access_snippet.rb
+++ b/lib/gitlab/git_access_snippet.rb
@@ -61,6 +61,13 @@ module Gitlab
end
end
+ override :can_read_project?
+ def can_read_project?
+ return true if user&.migration_bot?
+
+ super
+ end
+
override :check_download_access!
def check_download_access!
passed = guest_can_download_code? || user_can_download_code?
@@ -99,7 +106,7 @@ module Gitlab
def check_single_change_access(change)
Checks::SnippetCheck.new(change, logger: logger).validate!
- Checks::PushFileCountCheck.new(change, repository: repository, limit: Snippet::MAX_FILE_COUNT, logger: logger).validate!
+ Checks::PushFileCountCheck.new(change, repository: repository, limit: Snippet.max_file_limit(user), logger: logger).validate!
rescue Checks::TimedLogger::TimeoutError
raise TimeoutError, logger.full_message
end
@@ -121,5 +128,12 @@ module Gitlab
def check_custom_action(cmd)
nil
end
+
+ override :check_size_limit?
+ def check_size_limit?
+ return false if user&.migration_bot?
+
+ super
+ end
end
end
diff --git a/lib/gitlab/gitaly_client.rb b/lib/gitlab/gitaly_client.rb
index 697c943b4ec..3aaed0edb87 100644
--- a/lib/gitlab/gitaly_client.rb
+++ b/lib/gitlab/gitaly_client.rb
@@ -130,7 +130,7 @@ module Gitlab
end
def self.address_metadata(storage)
- Base64.strict_encode64(JSON.dump(storage => connection_data(storage)))
+ Base64.strict_encode64(Gitlab::Json.dump(storage => connection_data(storage)))
end
def self.connection_data(storage)
@@ -209,7 +209,8 @@ module Gitlab
end
def self.query_time
- SafeRequestStore[:gitaly_query_time] ||= 0
+ query_time = SafeRequestStore[:gitaly_query_time] ||= 0
+ query_time.round(Gitlab::InstrumentationHelper::DURATION_PRECISION)
end
def self.query_time=(duration)
@@ -457,7 +458,7 @@ module Gitlab
def self.filesystem_id_from_disk(storage)
metadata_file = File.read(storage_metadata_file_path(storage))
- metadata_hash = JSON.parse(metadata_file)
+ metadata_hash = Gitlab::Json.parse(metadata_file)
metadata_hash['gitaly_filesystem_id']
rescue Errno::ENOENT, Errno::EACCES, JSON::ParserError
nil
diff --git a/lib/gitlab/github_import/parallel_importer.rb b/lib/gitlab/github_import/parallel_importer.rb
index 8fb99c996b3..1b4750da868 100644
--- a/lib/gitlab/github_import/parallel_importer.rb
+++ b/lib/gitlab/github_import/parallel_importer.rb
@@ -5,8 +5,6 @@ module Gitlab
# The ParallelImporter schedules the importing of a GitHub project using
# Sidekiq.
class ParallelImporter
- prepend_if_ee('::EE::Gitlab::GithubImport::ParallelImporter') # rubocop: disable Cop/InjectEnterpriseEditionModule
-
attr_reader :project
def self.async?
@@ -41,3 +39,5 @@ module Gitlab
end
end
end
+
+Gitlab::GithubImport::ParallelImporter.prepend_if_ee('::EE::Gitlab::GithubImport::ParallelImporter')
diff --git a/lib/gitlab/gl_repository.rb b/lib/gitlab/gl_repository.rb
index 26440e6f82d..8abefad1ef3 100644
--- a/lib/gitlab/gl_repository.rb
+++ b/lib/gitlab/gl_repository.rb
@@ -23,11 +23,18 @@ module Gitlab
project_resolver: -> (snippet) { snippet&.project },
guest_read_ability: :read_snippet
).freeze
+ DESIGN = ::Gitlab::GlRepository::RepoType.new(
+ name: :design,
+ access_checker_class: ::Gitlab::GitAccessDesign,
+ repository_resolver: -> (project) { ::DesignManagement::Repository.new(project) },
+ suffix: :design
+ ).freeze
TYPES = {
PROJECT.name.to_s => PROJECT,
WIKI.name.to_s => WIKI,
- SNIPPET.name.to_s => SNIPPET
+ SNIPPET.name.to_s => SNIPPET,
+ DESIGN.name.to_s => DESIGN
}.freeze
def self.types
@@ -58,5 +65,3 @@ module Gitlab
private_class_method :instance
end
end
-
-Gitlab::GlRepository.prepend_if_ee('::EE::Gitlab::GlRepository')
diff --git a/lib/gitlab/gl_repository/repo_type.rb b/lib/gitlab/gl_repository/repo_type.rb
index 052ce578881..64c329b15ae 100644
--- a/lib/gitlab/gl_repository/repo_type.rb
+++ b/lib/gitlab/gl_repository/repo_type.rb
@@ -57,6 +57,10 @@ module Gitlab
self == SNIPPET
end
+ def design?
+ self == DESIGN
+ end
+
def path_suffix
suffix ? ".#{suffix}" : ''
end
@@ -87,5 +91,3 @@ module Gitlab
end
end
end
-
-Gitlab::GlRepository::RepoType.prepend_if_ee('EE::Gitlab::GlRepository::RepoType')
diff --git a/lib/gitlab/gon_helper.rb b/lib/gitlab/gon_helper.rb
index e4e69241bd9..fbbfed7279d 100644
--- a/lib/gitlab/gon_helper.rb
+++ b/lib/gitlab/gon_helper.rb
@@ -42,11 +42,11 @@ 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(:snippets_vue, default_enabled: true)
push_frontend_feature_flag(:monaco_blobs, default_enabled: false)
push_frontend_feature_flag(:monaco_ci, default_enabled: false)
push_frontend_feature_flag(:snippets_edit_vue, default_enabled: false)
+ push_frontend_feature_flag(:webperf_experiment, default_enabled: false)
end
# Exposes the state of a feature flag to the frontend code.
diff --git a/lib/gitlab/grape_logging/loggers/cloudflare_logger.rb b/lib/gitlab/grape_logging/loggers/cloudflare_logger.rb
new file mode 100644
index 00000000000..3abb0100a86
--- /dev/null
+++ b/lib/gitlab/grape_logging/loggers/cloudflare_logger.rb
@@ -0,0 +1,18 @@
+# frozen_string_literal: true
+
+module Gitlab
+ module GrapeLogging
+ module Loggers
+ class CloudflareLogger < ::GrapeLogging::Loggers::Base
+ include ::Gitlab::Logging::CloudflareHelper
+
+ def parameters(request, _response)
+ data = {}
+ store_cloudflare_headers!(data, request)
+
+ data
+ end
+ end
+ end
+ end
+end
diff --git a/lib/gitlab/grape_logging/loggers/context_logger.rb b/lib/gitlab/grape_logging/loggers/context_logger.rb
new file mode 100644
index 00000000000..0a8f0872fbe
--- /dev/null
+++ b/lib/gitlab/grape_logging/loggers/context_logger.rb
@@ -0,0 +1,14 @@
+# frozen_string_literal: true
+
+# This module adds additional correlation id the grape logger
+module Gitlab
+ module GrapeLogging
+ module Loggers
+ class ContextLogger < ::GrapeLogging::Loggers::Base
+ def parameters(_, _)
+ Labkit::Context.current.to_h
+ end
+ end
+ end
+ end
+end
diff --git a/lib/gitlab/grape_logging/loggers/exception_logger.rb b/lib/gitlab/grape_logging/loggers/exception_logger.rb
index 606b7c0dbce..14147769422 100644
--- a/lib/gitlab/grape_logging/loggers/exception_logger.rb
+++ b/lib/gitlab/grape_logging/loggers/exception_logger.rb
@@ -4,14 +4,16 @@ module Gitlab
module GrapeLogging
module Loggers
class ExceptionLogger < ::GrapeLogging::Loggers::Base
- def parameters(request, _)
+ def parameters(request, response_body)
+ data = {}
+ data[:api_error] = format_body(response_body) if bad_request?(request)
+
# grape-logging attempts to pass the logger the exception
# (https://github.com/aserafin/grape_logging/blob/v1.7.0/lib/grape_logging/middleware/request_logger.rb#L63),
# but it appears that the rescue_all in api.rb takes
# precedence so the logger never sees it. We need to
# store and retrieve the exception from the environment.
exception = request.env[::API::Helpers::API_EXCEPTION_ENV]
- data = {}
return data unless exception.is_a?(Exception)
@@ -19,6 +21,28 @@ module Gitlab
data
end
+
+ private
+
+ def format_body(response_body)
+ # https://github.com/rack/rack/blob/master/SPEC.rdoc#label-The+Body:
+ # The response_body must respond to each, but just in case we
+ # guard against errors here.
+ response_body = Array(response_body) unless response_body.respond_to?(:each)
+
+ # To avoid conflicting types in Elasticsearch, convert every
+ # element into an Array of strings. A response body is usually
+ # an array of Strings so that the response can be sent in
+ # chunks.
+ body = []
+ # each_with_object doesn't work with Rack::BodyProxy
+ response_body.each { |chunk| body << chunk.to_s }
+ body
+ end
+
+ def bad_request?(request)
+ request.env[::API::Helpers::API_RESPONSE_STATUS_CODE] == 400
+ end
end
end
end
diff --git a/lib/gitlab/graphql/authorize/authorize_field_service.rb b/lib/gitlab/graphql/authorize/authorize_field_service.rb
index c7f430490d6..61668b634fd 100644
--- a/lib/gitlab/graphql/authorize/authorize_field_service.rb
+++ b/lib/gitlab/graphql/authorize/authorize_field_service.rb
@@ -70,7 +70,10 @@ module Gitlab
end
def filter_allowed(current_user, resolved_type, authorizing_object)
- if authorizing_object
+ if resolved_type.nil?
+ # We're not rendering anything, for example when a record was not found
+ # no need to do anything
+ elsif authorizing_object
# Authorizing fields representing scalars, or a simple field with an object
resolved_type if allowed_access?(current_user, authorizing_object)
elsif @field.connection?
@@ -83,9 +86,6 @@ module Gitlab
resolved_type.select do |single_object_type|
allowed_access?(current_user, single_object_type.object)
end
- elsif resolved_type.nil?
- # We're not rendering anything, for example when a record was not found
- # no need to do anything
else
raise "Can't authorize #{@field}"
end
diff --git a/lib/gitlab/graphql/pagination/keyset/connection.rb b/lib/gitlab/graphql/pagination/keyset/connection.rb
index 5466924a794..1a32ab468b1 100644
--- a/lib/gitlab/graphql/pagination/keyset/connection.rb
+++ b/lib/gitlab/graphql/pagination/keyset/connection.rb
@@ -128,7 +128,7 @@ module Gitlab
end
def ordering_from_encoded_json(cursor)
- JSON.parse(decode(cursor))
+ Gitlab::Json.parse(decode(cursor))
rescue JSON::ParserError
raise Gitlab::Graphql::Errors::ArgumentError, "Please provide a valid cursor"
end
diff --git a/lib/gitlab/graphql/pagination/keyset/order_info.rb b/lib/gitlab/graphql/pagination/keyset/order_info.rb
index 876d6114f3c..12bcc4993b5 100644
--- a/lib/gitlab/graphql/pagination/keyset/order_info.rb
+++ b/lib/gitlab/graphql/pagination/keyset/order_info.rb
@@ -40,6 +40,8 @@ module Gitlab
end
if order_list.count > 2
+ # Keep in mind an order clause for primary key is added if one is not present
+ # lib/gitlab/graphql/pagination/keyset/connection.rb:97
raise ArgumentError.new('A maximum of 2 ordering fields are allowed')
end
diff --git a/lib/gitlab/graphql/query_analyzers/logger_analyzer.rb b/lib/gitlab/graphql/query_analyzers/logger_analyzer.rb
index 327a9c549d5..6f705239fa3 100644
--- a/lib/gitlab/graphql/query_analyzers/logger_analyzer.rb
+++ b/lib/gitlab/graphql/query_analyzers/logger_analyzer.rb
@@ -34,7 +34,7 @@ module Gitlab
memo[:depth] = depth
memo[:complexity] = complexity
- memo[:duration] = duration(memo[:time_started]).round(1)
+ memo[:duration_s] = duration(memo[:time_started]).round(1)
GraphqlLogger.info(memo.except!(:time_started, :query))
rescue => e
@@ -62,7 +62,7 @@ module Gitlab
query_string: nil,
query: query,
variables: nil,
- duration: nil
+ duration_s: nil
}
end
end
diff --git a/lib/gitlab/graphql/variables.rb b/lib/gitlab/graphql/variables.rb
index b13ea37c21f..1c6fb011012 100644
--- a/lib/gitlab/graphql/variables.rb
+++ b/lib/gitlab/graphql/variables.rb
@@ -20,7 +20,7 @@ module Gitlab
case ambiguous_param
when String
if ambiguous_param.present?
- ensure_hash(JSON.parse(ambiguous_param))
+ ensure_hash(Gitlab::Json.parse(ambiguous_param))
else
{}
end
diff --git a/lib/gitlab/group_search_results.rb b/lib/gitlab/group_search_results.rb
index 8597903ad00..eb4361cdc53 100644
--- a/lib/gitlab/group_search_results.rb
+++ b/lib/gitlab/group_search_results.rb
@@ -4,8 +4,8 @@ module Gitlab
class GroupSearchResults < SearchResults
attr_reader :group
- def initialize(current_user, limit_projects, group, query, default_project_filter: false, per_page: 20)
- super(current_user, limit_projects, query, default_project_filter: default_project_filter, per_page: per_page)
+ def initialize(current_user, limit_projects, group, query, default_project_filter: false)
+ super(current_user, limit_projects, query, default_project_filter: default_project_filter)
@group = group
end
diff --git a/lib/gitlab/health_checks/puma_check.rb b/lib/gitlab/health_checks/puma_check.rb
index 9f09070a57d..2dc8a093572 100644
--- a/lib/gitlab/health_checks/puma_check.rb
+++ b/lib/gitlab/health_checks/puma_check.rb
@@ -21,7 +21,7 @@ module Gitlab
return unless Gitlab::Runtime.puma?
stats = Puma.stats
- stats = JSON.parse(stats)
+ stats = Gitlab::Json.parse(stats)
# If `workers` is missing this means that
# Puma server is running in single mode
diff --git a/lib/gitlab/i18n.rb b/lib/gitlab/i18n.rb
index 7e0398f09af..18f4cb559c5 100644
--- a/lib/gitlab/i18n.rb
+++ b/lib/gitlab/i18n.rb
@@ -5,28 +5,28 @@ module Gitlab
extend self
AVAILABLE_LANGUAGES = {
+ 'bg' => 'Bulgarian - българÑки',
+ 'cs_CZ' => 'Czech - ÄeÅ¡tina',
+ 'de' => 'German - Deutsch',
'en' => 'English',
- 'es' => 'Español',
- 'gl_ES' => 'Galego',
- 'de' => 'Deutsch',
- 'fr' => 'Français',
- 'pt_BR' => 'Português (Brasil)',
- 'zh_CN' => '简体中文',
- 'zh_HK' => 'ç¹é«”中文 (香港)',
- 'zh_TW' => 'ç¹é«”中文 (臺ç£)',
- 'bg' => 'българÑки',
- 'ru' => 'РуÑÑкий',
- 'eo' => 'Esperanto',
- 'it' => 'Italiano',
- 'uk' => 'УкраїнÑька',
- 'ja' => '日本語',
- 'ko' => '한국어',
- 'nl_NL' => 'Nederlands',
- 'tr_TR' => 'Türkçe',
- 'id_ID' => 'Bahasa Indonesia',
+ 'eo' => 'Esperanto - esperanto',
+ 'es' => 'Spanish - español',
'fil_PH' => 'Filipino',
- 'pl_PL' => 'Polski',
- 'cs_CZ' => 'Čeština'
+ 'fr' => 'French - français',
+ 'gl_ES' => 'Galician - galego',
+ 'id_ID' => 'Indonesian - Bahasa Indonesia',
+ 'it' => 'Italian - italiano',
+ 'ja' => 'Japanese - 日本語',
+ 'ko' => 'Korean - 한국어',
+ 'nl_NL' => 'Dutch - Nederlands',
+ 'pl_PL' => 'Polish - polski',
+ 'pt_BR' => 'Portuguese (Brazil) - português (Brasil)',
+ 'ru' => 'Russian - РуÑÑкий',
+ 'tr_TR' => 'Turkish - Türkçe',
+ 'uk' => 'Ukrainian - українÑька',
+ 'zh_CN' => 'Chinese, Simplified - 简体中文',
+ 'zh_HK' => 'Chinese, Traditional (Hong Kong) - ç¹é«”中文 (香港)',
+ 'zh_TW' => 'Chinese, Traditional (Taiwan) - ç¹é«”中文 (å°ç£)'
}.freeze
def available_locales
diff --git a/lib/gitlab/import_export.rb b/lib/gitlab/import_export.rb
index 52102b6f508..921072a4970 100644
--- a/lib/gitlab/import_export.rb
+++ b/lib/gitlab/import_export.rb
@@ -42,6 +42,10 @@ module Gitlab
"project.wiki.bundle"
end
+ def design_repo_bundle_filename
+ 'project.design.bundle'
+ end
+
def snippet_repo_bundle_dir
'snippets'
end
@@ -88,6 +92,10 @@ module Gitlab
'group.json'
end
+ def legacy_group_config_file
+ Rails.root.join('lib/gitlab/import_export/group/legacy_import_export.yml')
+ end
+
def group_config_file
Rails.root.join('lib/gitlab/import_export/group/import_export.yml')
end
diff --git a/lib/gitlab/import_export/after_export_strategies/web_upload_strategy.rb b/lib/gitlab/import_export/after_export_strategies/web_upload_strategy.rb
index fd98bc2caad..e2dba831661 100644
--- a/lib/gitlab/import_export/after_export_strategies/web_upload_strategy.rb
+++ b/lib/gitlab/import_export/after_export_strategies/web_upload_strategy.rb
@@ -52,7 +52,10 @@ module Gitlab
end
def headers
- { 'Content-Length' => export_size.to_s }
+ {
+ 'Content-Type' => 'application/gzip',
+ 'Content-Length' => export_size.to_s
+ }
end
def export_size
diff --git a/lib/gitlab/import_export/design_repo_restorer.rb b/lib/gitlab/import_export/design_repo_restorer.rb
new file mode 100644
index 00000000000..a702c58a7c2
--- /dev/null
+++ b/lib/gitlab/import_export/design_repo_restorer.rb
@@ -0,0 +1,15 @@
+# frozen_string_literal: true
+
+module Gitlab
+ module ImportExport
+ class DesignRepoRestorer < RepoRestorer
+ def initialize(project:, shared:, path_to_bundle:)
+ super(project: project, shared: shared, path_to_bundle: path_to_bundle)
+
+ @repository = project.design_repository
+ end
+
+ # `restore` method is handled in super class
+ end
+ end
+end
diff --git a/lib/gitlab/import_export/design_repo_saver.rb b/lib/gitlab/import_export/design_repo_saver.rb
new file mode 100644
index 00000000000..db9ebee6a13
--- /dev/null
+++ b/lib/gitlab/import_export/design_repo_saver.rb
@@ -0,0 +1,19 @@
+# frozen_string_literal: true
+
+module Gitlab
+ module ImportExport
+ class DesignRepoSaver < RepoSaver
+ def save
+ @repository = project.design_repository
+
+ super
+ end
+
+ private
+
+ def bundle_full_path
+ File.join(shared.export_path, ::Gitlab::ImportExport.design_repo_bundle_filename)
+ end
+ end
+ end
+end
diff --git a/lib/gitlab/import_export/group/group_restorer.rb b/lib/gitlab/import_export/group/group_restorer.rb
new file mode 100644
index 00000000000..b338950fb71
--- /dev/null
+++ b/lib/gitlab/import_export/group/group_restorer.rb
@@ -0,0 +1,71 @@
+# frozen_string_literal: true
+
+module Gitlab
+ module ImportExport
+ module Group
+ class GroupRestorer
+ def initialize(
+ user:,
+ shared:,
+ group:,
+ attributes:,
+ importable_path:,
+ relation_reader:,
+ reader:
+ )
+ @user = user
+ @shared = shared
+ @group = group
+ @group_attributes = attributes
+ @importable_path = importable_path
+ @relation_reader = relation_reader
+ @reader = reader
+ end
+
+ def restore
+ # consume_relation returns a list of [relation, index]
+ @group_members = @relation_reader
+ .consume_relation(@importable_path, 'members')
+ .map(&:first)
+
+ return unless members_mapper.map
+
+ restorer.restore
+ end
+
+ private
+
+ def restorer
+ @relation_tree_restorer ||= RelationTreeRestorer.new(
+ user: @user,
+ shared: @shared,
+ relation_reader: @relation_reader,
+ members_mapper: members_mapper,
+ object_builder: object_builder,
+ relation_factory: relation_factory,
+ reader: @reader,
+ importable: @group,
+ importable_attributes: @group_attributes,
+ importable_path: @importable_path
+ )
+ end
+
+ def members_mapper
+ @members_mapper ||= Gitlab::ImportExport::MembersMapper.new(
+ exported_members: @group_members,
+ user: @user,
+ importable: @group
+ )
+ end
+
+ def relation_factory
+ Gitlab::ImportExport::Group::RelationFactory
+ end
+
+ def object_builder
+ Gitlab::ImportExport::Group::ObjectBuilder
+ end
+ end
+ end
+ end
+end
diff --git a/lib/gitlab/import_export/group/import_export.yml b/lib/gitlab/import_export/group/import_export.yml
index 5008639077c..e30206dc509 100644
--- a/lib/gitlab/import_export/group/import_export.yml
+++ b/lib/gitlab/import_export/group/import_export.yml
@@ -27,9 +27,7 @@ included_attributes:
excluded_attributes:
group:
- - :id
- :owner_id
- - :parent_id
- :created_at
- :updated_at
- :runners_token
diff --git a/lib/gitlab/import_export/group/legacy_import_export.yml b/lib/gitlab/import_export/group/legacy_import_export.yml
new file mode 100644
index 00000000000..5008639077c
--- /dev/null
+++ b/lib/gitlab/import_export/group/legacy_import_export.yml
@@ -0,0 +1,86 @@
+# Model relationships to be included in the group import/export
+#
+# This list _must_ only contain relationships that are available to both FOSS and
+# Enterprise editions. EE specific relationships must be defined in the `ee` section further
+# down below.
+tree:
+ group:
+ - :milestones
+ - :badges
+ - labels:
+ - :priorities
+ - boards:
+ - lists:
+ - label:
+ - :priorities
+ - :board
+ - members:
+ - :user
+
+included_attributes:
+ user:
+ - :id
+ - :email
+ - :username
+ author:
+ - :name
+
+excluded_attributes:
+ group:
+ - :id
+ - :owner_id
+ - :parent_id
+ - :created_at
+ - :updated_at
+ - :runners_token
+ - :runners_token_encrypted
+ - :saml_discovery_token
+ - :visibility_level
+ - :trial_ends_on
+ - :shared_runners_minute_limit
+ - :extra_shared_runners_minutes_limit
+ epics:
+ - :state_id
+
+methods:
+ labels:
+ - :type
+ label:
+ - :type
+ badges:
+ - :type
+ notes:
+ - :type
+ events:
+ - :action
+ lists:
+ - :list_type
+ epics:
+ - :state
+
+preloads:
+
+# EE specific relationships and settings to include. All of this will be merged
+# into the previous structures if EE is used.
+ee:
+ tree:
+ group:
+ - epics:
+ - :parent
+ - :award_emoji
+ - events:
+ - :push_event_payload
+ - notes:
+ - :author
+ - :award_emoji
+ - events:
+ - :push_event_payload
+ - boards:
+ - :board_assignee
+ - :milestone
+ - labels:
+ - :priorities
+ - lists:
+ - milestone:
+ - events:
+ - :push_event_payload
diff --git a/lib/gitlab/import_export/group/legacy_tree_restorer.rb b/lib/gitlab/import_export/group/legacy_tree_restorer.rb
index 5d96a0f3c0a..5499b79cee6 100644
--- a/lib/gitlab/import_export/group/legacy_tree_restorer.rb
+++ b/lib/gitlab/import_export/group/legacy_tree_restorer.rb
@@ -122,7 +122,7 @@ module Gitlab
@reader ||= Gitlab::ImportExport::Reader.new(
shared: @shared,
config: Gitlab::ImportExport::Config.new(
- config: Gitlab::ImportExport.group_config_file
+ config: Gitlab::ImportExport.legacy_group_config_file
).to_h
)
end
diff --git a/lib/gitlab/import_export/group/legacy_tree_saver.rb b/lib/gitlab/import_export/group/legacy_tree_saver.rb
index 3776ef0d8f5..7ab81c09885 100644
--- a/lib/gitlab/import_export/group/legacy_tree_saver.rb
+++ b/lib/gitlab/import_export/group/legacy_tree_saver.rb
@@ -43,7 +43,7 @@ module Gitlab
@reader ||= Gitlab::ImportExport::Reader.new(
shared: @shared,
config: Gitlab::ImportExport::Config.new(
- config: Gitlab::ImportExport.group_config_file
+ config: Gitlab::ImportExport.legacy_group_config_file
).to_h
)
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..d0c0999f291
--- /dev/null
+++ b/lib/gitlab/import_export/group/tree_restorer.rb
@@ -0,0 +1,140 @@
+# frozen_string_literal: true
+
+module Gitlab
+ module ImportExport
+ module Group
+ class TreeRestorer
+ include Gitlab::Utils::StrongMemoize
+
+ attr_reader :user, :shared
+
+ def initialize(user:, shared:, group:)
+ @user = user
+ @shared = shared
+ @top_level_group = group
+ @groups_mapping = {}
+ end
+
+ def restore
+ group_ids = relation_reader.consume_relation('groups', '_all').map { |value, _idx| Integer(value) }
+ root_group_id = group_ids.delete_at(0)
+
+ process_root(root_group_id)
+
+ group_ids.each do |group_id|
+ process_child(group_id)
+ end
+
+ true
+ rescue => e
+ shared.error(e)
+ false
+ end
+
+ class GroupAttributes
+ attr_reader :attributes, :group_id, :id, :path
+
+ def initialize(group_id, relation_reader)
+ @group_id = group_id
+
+ @path = "groups/#{group_id}"
+ @attributes = relation_reader.consume_attributes(@path)
+ @id = @attributes.delete('id')
+
+ unless @id == @group_id
+ raise ArgumentError, "Invalid group_id for #{group_id}"
+ end
+ end
+
+ def delete_attribute(name)
+ attributes.delete(name)
+ end
+
+ def delete_attributes(*names)
+ names.map(&method(:delete_attribute))
+ end
+ end
+ private_constant :GroupAttributes
+
+ private
+
+ def process_root(group_id)
+ group_attributes = GroupAttributes.new(group_id, relation_reader)
+
+ # name and path are not imported on the root group to avoid conflict
+ # with existing groups name and/or path.
+ group_attributes.delete_attributes('name', 'path')
+
+ restore_group(@top_level_group, group_attributes)
+ end
+
+ def process_child(group_id)
+ group_attributes = GroupAttributes.new(group_id, relation_reader)
+
+ group = create_group(group_attributes)
+
+ restore_group(group, group_attributes)
+ end
+
+ def create_group(group_attributes)
+ parent_id = group_attributes.delete_attribute('parent_id')
+ name = group_attributes.delete_attribute('name')
+ path = group_attributes.delete_attribute('path')
+
+ parent_group = @groups_mapping.fetch(parent_id) { raise(ArgumentError, 'Parent group not found') }
+
+ ::Groups::CreateService.new(
+ user,
+ name: name,
+ path: path,
+ parent_id: parent_group.id,
+ visibility_level: sub_group_visibility_level(group_attributes.attributes, parent_group)
+ ).execute
+ end
+
+ def restore_group(group, group_attributes)
+ @groups_mapping[group_attributes.id] = group
+
+ Group::GroupRestorer.new(
+ user: user,
+ shared: shared,
+ group: group,
+ attributes: group_attributes.attributes,
+ importable_path: group_attributes.path,
+ relation_reader: relation_reader,
+ reader: reader
+ ).restore
+ end
+
+ def relation_reader
+ strong_memoize(:relation_reader) do
+ ImportExport::JSON::NdjsonReader.new(
+ File.join(shared.export_path, 'tree')
+ )
+ end
+ 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 reader
+ strong_memoize(:reader) do
+ Gitlab::ImportExport::Reader.new(
+ shared: @shared,
+ config: Gitlab::ImportExport::Config.new(
+ config: Gitlab::ImportExport.group_config_file
+ ).to_h
+ )
+ end
+ end
+ end
+ end
+ end
+end
diff --git a/lib/gitlab/import_export/group/tree_saver.rb b/lib/gitlab/import_export/group/tree_saver.rb
new file mode 100644
index 00000000000..d538de33c51
--- /dev/null
+++ b/lib/gitlab/import_export/group/tree_saver.rb
@@ -0,0 +1,72 @@
+# frozen_string_literal: true
+
+module Gitlab
+ module ImportExport
+ module Group
+ class TreeSaver
+ attr_reader :full_path, :shared
+
+ def initialize(group:, current_user:, shared:, params: {})
+ @params = params
+ @current_user = current_user
+ @shared = shared
+ @group = group
+ @full_path = File.join(@shared.export_path, 'tree')
+ end
+
+ def save
+ all_groups = Enumerator.new do |group_ids|
+ groups.each do |group|
+ serialize(group)
+ group_ids << group.id
+ end
+ end
+
+ json_writer.write_relation_array('groups', '_all', all_groups)
+
+ true
+ rescue => e
+ @shared.error(e)
+ false
+ ensure
+ json_writer&.close
+ end
+
+ private
+
+ def groups
+ @groups ||= Gitlab::ObjectHierarchy
+ .new(::Group.where(id: @group.id))
+ .base_and_descendants(with_depth: true)
+ .order_by(:depth)
+ end
+
+ def serialize(group)
+ ImportExport::JSON::StreamingSerializer.new(
+ group,
+ group_tree,
+ json_writer,
+ exportable_path: "groups/#{group.id}"
+ ).execute
+ end
+
+ def group_tree
+ @group_tree ||= Gitlab::ImportExport::Reader.new(
+ shared: @shared,
+ config: group_config
+ ).group_tree
+ end
+
+ def group_config
+ Gitlab::ImportExport::Config.new(
+ config: Gitlab::ImportExport.group_config_file
+ ).to_h
+ end
+
+ def json_writer
+ @json_writer ||= ImportExport::JSON::NdjsonWriter.new(@full_path)
+ end
+ end
+ end
+ end
+end
diff --git a/lib/gitlab/import_export/importer.rb b/lib/gitlab/import_export/importer.rb
index 4b761eb86ae..b1219384732 100644
--- a/lib/gitlab/import_export/importer.rb
+++ b/lib/gitlab/import_export/importer.rb
@@ -34,7 +34,7 @@ module Gitlab
attr_accessor :archive_file, :current_user, :project, :shared
def restorers
- [repo_restorer, wiki_restorer, project_tree, avatar_restorer,
+ [repo_restorer, wiki_restorer, project_tree, avatar_restorer, design_repo_restorer,
uploads_restorer, lfs_restorer, statistics_restorer, snippets_repo_restorer]
end
@@ -71,6 +71,12 @@ module Gitlab
wiki_enabled: project.wiki_enabled?)
end
+ def design_repo_restorer
+ Gitlab::ImportExport::DesignRepoRestorer.new(path_to_bundle: design_repo_path,
+ shared: shared,
+ project: project)
+ end
+
def uploads_restorer
Gitlab::ImportExport::UploadsRestorer.new(project: project, shared: shared)
end
@@ -101,6 +107,10 @@ module Gitlab
File.join(shared.export_path, Gitlab::ImportExport.wiki_repo_bundle_filename)
end
+ def design_repo_path
+ File.join(shared.export_path, Gitlab::ImportExport.design_repo_bundle_filename)
+ end
+
def remove_import_file
upload = project.import_export_upload
@@ -111,13 +121,17 @@ module Gitlab
end
def overwrite_project
- return unless can?(current_user, :admin_namespace, project.namespace)
+ return true unless overwrite_project?
- if overwrite_project?
- ::Projects::OverwriteProjectService.new(project, current_user)
- .execute(project_to_overwrite)
+ unless can?(current_user, :admin_namespace, project.namespace)
+ message = "User #{current_user&.username} (#{current_user&.id}) cannot overwrite a project in #{project.namespace.path}"
+ @shared.error(::Projects::ImportService::PermissionError.new(message))
+ return false
end
+ ::Projects::OverwriteProjectService.new(project, current_user)
+ .execute(project_to_overwrite)
+
true
end
@@ -137,5 +151,3 @@ module Gitlab
end
end
end
-
-Gitlab::ImportExport::Importer.prepend_if_ee('EE::Gitlab::ImportExport::Importer')
diff --git a/lib/gitlab/import_export/project/base_task.rb b/lib/gitlab/import_export/project/base_task.rb
index 6a7b24421c9..356e261e251 100644
--- a/lib/gitlab/import_export/project/base_task.rb
+++ b/lib/gitlab/import_export/project/base_task.rb
@@ -11,17 +11,27 @@ module Gitlab
@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)
- @measurement = Gitlab::Utils::Measuring.new(logger: logger) if @measurement_enabled
@logger = logger
end
private
- attr_reader :measurement, :project, :namespace, :current_user, :file_path, :project_path, :logger
+ attr_reader :project, :namespace, :current_user, :file_path, :project_path, :logger
- def measurement_enabled?
- @measurement_enabled
+ def disable_upload_object_storage
+ overwrite_uploads_setting('enabled', false) do
+ yield
+ end
+ end
+
+ def overwrite_uploads_setting(key, value)
+ old_value = Settings.uploads.object_store[key]
+ Settings.uploads.object_store[key] = value
+
+ yield
+
+ ensure
+ Settings.uploads.object_store[key] = old_value
end
def success(message)
diff --git a/lib/gitlab/import_export/project/export_task.rb b/lib/gitlab/import_export/project/export_task.rb
index ec287380c48..5e105b4653d 100644
--- a/lib/gitlab/import_export/project/export_task.rb
+++ b/lib/gitlab/import_export/project/export_task.rb
@@ -19,7 +19,11 @@ module Gitlab
.execute(Gitlab::ImportExport::AfterExportStrategies::MoveFileStrategy.new(archive_path: file_path))
end
+ return error(project.import_export_shared.errors.join(', ')) if project.import_export_shared.errors.any?
+
success('Done!')
+ rescue Gitlab::ImportExport::Error => e
+ error(e.message)
end
private
@@ -32,8 +36,13 @@ module Gitlab
def with_export
with_request_store do
- ::Gitlab::GitalyClient.allow_n_plus_1_calls do
- measurement_enabled? ? measurement.with_measuring { yield } : yield
+ # We are disabling ObjectStorage for `export`
+ # since when direct upload is enabled, remote storage will be used
+ # and Gitlab::ImportExport::AfterExportStrategies::MoveFileStrategy will fail to copy exported archive
+ disable_upload_object_storage do
+ ::Gitlab::GitalyClient.allow_n_plus_1_calls do
+ yield
+ end
end
end
end
diff --git a/lib/gitlab/import_export/project/import_export.yml b/lib/gitlab/import_export/project/import_export.yml
index 3cbd0d144e6..8851b106ad5 100644
--- a/lib/gitlab/import_export/project/import_export.yml
+++ b/lib/gitlab/import_export/project/import_export.yml
@@ -29,6 +29,14 @@ tree:
- resource_label_events:
- label:
- :priorities
+ - designs:
+ - notes:
+ - :author
+ - events:
+ - :push_event_payload
+ - design_versions:
+ - actions:
+ - :design # Duplicate export of issues.designs in order to link the record to both Issue and Action
- :issue_assignees
- :zoom_meetings
- :sentry_issue
@@ -160,6 +168,7 @@ excluded_attributes:
- :marked_for_deletion_at
- :marked_for_deletion_by_user_id
- :compliance_framework_setting
+ - :show_default_award_emojis
namespaces:
- :runners_token
- :runners_token_encrypted
@@ -190,6 +199,7 @@ excluded_attributes:
- :merge_request_diff_id
issues:
- :milestone_id
+ - :sprint_id
- :moved_to_id
- :sent_notifications
- :state_id
@@ -197,6 +207,7 @@ excluded_attributes:
- :promoted_to_epic_id
merge_request:
- :milestone_id
+ - :sprint_id
- :ref_fetched
- :merge_jid
- :rebase_jid
@@ -205,6 +216,7 @@ excluded_attributes:
- :state_id
merge_requests:
- :milestone_id
+ - :sprint_id
- :ref_fetched
- :merge_jid
- :rebase_jid
@@ -250,8 +262,9 @@ excluded_attributes:
- :token
- :token_encrypted
services:
- - :template
+ - :inherit_from_id
- :instance
+ - :template
error_tracking_setting:
- :encrypted_token
- :encrypted_token_iv
@@ -284,6 +297,7 @@ excluded_attributes:
actions:
- :design_id
- :version_id
+ - image_v432x230
links:
- :release_id
project_members:
@@ -376,14 +390,6 @@ ee:
tree:
project:
- issues:
- - designs:
- - notes:
- - :author
- - events:
- - :push_event_payload
- - design_versions:
- - actions:
- - :design # Duplicate export of issues.designs in order to link the record to both Issue and Action
- epic_issue:
- :epic
- protected_branches:
@@ -391,6 +397,3 @@ ee:
- protected_environments:
- :deploy_access_levels
- :service_desk_setting
- excluded_attributes:
- actions:
- - image_v432x230
diff --git a/lib/gitlab/import_export/project/import_task.rb b/lib/gitlab/import_export/project/import_task.rb
index ae654ddbeaf..59bb8af750e 100644
--- a/lib/gitlab/import_export/project/import_task.rb
+++ b/lib/gitlab/import_export/project/import_task.rb
@@ -32,7 +32,7 @@ module Gitlab
# 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? ? measurement.with_measuring { yield } : yield
+ yield
end
end
@@ -56,11 +56,7 @@ module Gitlab
disable_upload_object_storage do
service = Projects::GitlabProjectsImportService.new(
current_user,
- {
- namespace_id: namespace.id,
- path: project_path,
- file: File.open(file_path)
- }
+ import_params
)
service.execute
@@ -71,24 +67,6 @@ module Gitlab
Sidekiq::Worker.drain_all
end
- def disable_upload_object_storage
- overwrite_uploads_setting('background_upload', false) do
- overwrite_uploads_setting('direct_upload', false) do
- yield
- end
- end
- end
-
- def overwrite_uploads_setting(key, value)
- old_value = Settings.uploads.object_store[key]
- Settings.uploads.object_store[key] = value
-
- yield
-
- ensure
- Settings.uploads.object_store[key] = old_value
- end
-
def full_path
"#{namespace.full_path}/#{project_path}"
end
@@ -99,6 +77,14 @@ module Gitlab
" as #{current_user.name}"
end
+ def import_params
+ {
+ namespace_id: namespace.id,
+ path: project_path,
+ file: File.open(file_path)
+ }
+ end
+
def show_import_failures_count
return unless project.import_failures.exists?
diff --git a/lib/gitlab/import_export/project/object_builder.rb b/lib/gitlab/import_export/project/object_builder.rb
index c3637b1c115..831e38f3034 100644
--- a/lib/gitlab/import_export/project/object_builder.rb
+++ b/lib/gitlab/import_export/project/object_builder.rb
@@ -57,6 +57,8 @@ module Gitlab
# Returns Arel clause for a particular model or `nil`.
def where_clause_for_klass
+ return attrs_to_arel(attributes.slice('filename')).and(table[:issue_id].eq(nil)) if design?
+
attrs_to_arel(attributes.slice('iid')) if merge_request?
end
@@ -95,6 +97,10 @@ module Gitlab
klass == Epic
end
+ def design?
+ klass == DesignManagement::Design
+ end
+
# If an existing group milestone used the IID
# claim the IID back and set the group milestone to use one available
# This is necessary to fix situations like the following:
@@ -115,5 +121,3 @@ module Gitlab
end
end
end
-
-Gitlab::ImportExport::Project::ObjectBuilder.prepend_if_ee('EE::Gitlab::ImportExport::Project::ObjectBuilder')
diff --git a/lib/gitlab/import_export/project/relation_factory.rb b/lib/gitlab/import_export/project/relation_factory.rb
index f7f1195f2f1..3ab9f2c4bfa 100644
--- a/lib/gitlab/import_export/project/relation_factory.rb
+++ b/lib/gitlab/import_export/project/relation_factory.rb
@@ -4,8 +4,6 @@ module Gitlab
module ImportExport
module Project
class RelationFactory < Base::RelationFactory
- prepend_if_ee('::EE::Gitlab::ImportExport::Project::RelationFactory') # rubocop: disable Cop/InjectEnterpriseEditionModule
-
OVERRIDES = { snippets: :project_snippets,
ci_pipelines: 'Ci::Pipeline',
pipelines: 'Ci::Pipeline',
@@ -19,6 +17,10 @@ module Gitlab
merge_access_levels: 'ProtectedBranch::MergeAccessLevel',
push_access_levels: 'ProtectedBranch::PushAccessLevel',
create_access_levels: 'ProtectedTag::CreateAccessLevel',
+ design: 'DesignManagement::Design',
+ designs: 'DesignManagement::Design',
+ design_versions: 'DesignManagement::Version',
+ actions: 'DesignManagement::Action',
labels: :project_labels,
priorities: :label_priorities,
auto_devops: :project_auto_devops,
@@ -53,6 +55,7 @@ module Gitlab
container_expiration_policy
external_pull_request
external_pull_requests
+ DesignManagement::Design
].freeze
def create
@@ -161,3 +164,5 @@ module Gitlab
end
end
end
+
+Gitlab::ImportExport::Project::RelationFactory.prepend_if_ee('::EE::Gitlab::ImportExport::Project::RelationFactory')
diff --git a/lib/gitlab/import_export/project/tree_restorer.rb b/lib/gitlab/import_export/project/tree_restorer.rb
index e9c89b803ba..a16ffe36054 100644
--- a/lib/gitlab/import_export/project/tree_restorer.rb
+++ b/lib/gitlab/import_export/project/tree_restorer.rb
@@ -54,7 +54,7 @@ module Gitlab
end
def ndjson_relation_reader
- return unless Feature.enabled?(:project_import_ndjson, project.namespace)
+ return unless Feature.enabled?(:project_import_ndjson, project.namespace, default_enabled: true)
ImportExport::JSON::NdjsonReader.new(
File.join(shared.export_path, 'tree')
diff --git a/lib/gitlab/import_export/project/tree_saver.rb b/lib/gitlab/import_export/project/tree_saver.rb
index 0017aa523c1..7cca3596da6 100644
--- a/lib/gitlab/import_export/project/tree_saver.rb
+++ b/lib/gitlab/import_export/project/tree_saver.rb
@@ -54,7 +54,7 @@ module Gitlab
def json_writer
@json_writer ||= begin
- if ::Feature.enabled?(:project_export_as_ndjson, @project.namespace)
+ if ::Feature.enabled?(:project_export_as_ndjson, @project.namespace, default_enabled: true)
full_path = File.join(@shared.export_path, 'tree')
Gitlab::ImportExport::JSON::NdjsonWriter.new(full_path)
else
diff --git a/lib/gitlab/import_export/relation_tree_restorer.rb b/lib/gitlab/import_export/relation_tree_restorer.rb
index 056945d0294..ea16d978127 100644
--- a/lib/gitlab/import_export/relation_tree_restorer.rb
+++ b/lib/gitlab/import_export/relation_tree_restorer.rb
@@ -37,9 +37,7 @@ module Gitlab
ActiveRecord::Base.no_touching do
update_params!
- bulk_inserts_enabled = @importable.class == ::Project &&
- Feature.enabled?(:import_bulk_inserts, @importable.group, default_enabled: true)
- BulkInsertableAssociations.with_bulk_insert(enabled: bulk_inserts_enabled) do
+ BulkInsertableAssociations.with_bulk_insert(enabled: @importable.class == ::Project) do
fix_ci_pipelines_not_sorted_on_legacy_project_json!
create_relations!
end
diff --git a/lib/gitlab/import_export/snippets_repo_restorer.rb b/lib/gitlab/import_export/snippets_repo_restorer.rb
index 8fe83225812..9ff3e74a6b1 100644
--- a/lib/gitlab/import_export/snippets_repo_restorer.rb
+++ b/lib/gitlab/import_export/snippets_repo_restorer.rb
@@ -10,7 +10,6 @@ module Gitlab
end
def restore
- return true unless Feature.enabled?(:version_snippets, @user)
return true unless Dir.exist?(snippets_repo_bundle_path)
@project.snippets.find_each.all? do |snippet|
diff --git a/lib/gitlab/import_export/snippets_repo_saver.rb b/lib/gitlab/import_export/snippets_repo_saver.rb
index 85e094c0d15..d3b0fe1c18c 100644
--- a/lib/gitlab/import_export/snippets_repo_saver.rb
+++ b/lib/gitlab/import_export/snippets_repo_saver.rb
@@ -12,8 +12,6 @@ module Gitlab
end
def save
- return true unless Feature.enabled?(:version_snippets, @current_user)
-
create_snippets_repo_directory
@project.snippets.find_each.all? do |snippet|
diff --git a/lib/gitlab/instrumentation/redis.rb b/lib/gitlab/instrumentation/redis.rb
index 6b066b800a5..cc99e828251 100644
--- a/lib/gitlab/instrumentation/redis.rb
+++ b/lib/gitlab/instrumentation/redis.rb
@@ -38,7 +38,8 @@ module Gitlab
end
def self.query_time
- ::RequestStore[REDIS_CALL_DURATION] || 0
+ query_time = ::RequestStore[REDIS_CALL_DURATION] || 0
+ query_time.round(::Gitlab::InstrumentationHelper::DURATION_PRECISION)
end
def self.add_duration(duration)
diff --git a/lib/gitlab/instrumentation_helper.rb b/lib/gitlab/instrumentation_helper.rb
index 308c3007720..7c5a601cd5b 100644
--- a/lib/gitlab/instrumentation_helper.rb
+++ b/lib/gitlab/instrumentation_helper.rb
@@ -5,27 +5,28 @@ module Gitlab
extend self
KEYS = %i(gitaly_calls gitaly_duration_s rugged_calls rugged_duration_s redis_calls redis_duration_s).freeze
+ DURATION_PRECISION = 6 # microseconds
def add_instrumentation_data(payload)
gitaly_calls = Gitlab::GitalyClient.get_request_count
if gitaly_calls > 0
payload[:gitaly_calls] = gitaly_calls
- payload[:gitaly_duration_s] = Gitlab::GitalyClient.query_time.round(2)
+ payload[:gitaly_duration_s] = Gitlab::GitalyClient.query_time
end
rugged_calls = Gitlab::RuggedInstrumentation.query_count
if rugged_calls > 0
payload[:rugged_calls] = rugged_calls
- payload[:rugged_duration_s] = Gitlab::RuggedInstrumentation.query_time.round(2)
+ payload[:rugged_duration_s] = Gitlab::RuggedInstrumentation.query_time
end
redis_calls = Gitlab::Instrumentation::Redis.get_request_count
if redis_calls > 0
payload[:redis_calls] = redis_calls
- payload[:redis_duration_s] = Gitlab::Instrumentation::Redis.query_time.round(2)
+ payload[:redis_duration_s] = Gitlab::Instrumentation::Redis.query_time
end
end
@@ -47,7 +48,7 @@ module Gitlab
# Its possible that if theres clock-skew between two nodes
# this value may be less than zero. In that event, we record the value
# as zero.
- [elapsed_by_absolute_time(enqueued_at_time), 0].max.round(2)
+ [elapsed_by_absolute_time(enqueued_at_time), 0].max.round(DURATION_PRECISION)
end
# Calculates the time in seconds, as a float, from
diff --git a/lib/gitlab/jira_import/base_importer.rb b/lib/gitlab/jira_import/base_importer.rb
index 5381812186d..306736df30f 100644
--- a/lib/gitlab/jira_import/base_importer.rb
+++ b/lib/gitlab/jira_import/base_importer.rb
@@ -3,12 +3,13 @@
module Gitlab
module JiraImport
class BaseImporter
- attr_reader :project, :client, :formatter, :jira_project_key
+ attr_reader :project, :client, :formatter, :jira_project_key, :running_import
def initialize(project)
project.validate_jira_import_settings!
- @jira_project_key = project.latest_jira_import&.jira_project_key
+ @running_import = project.latest_jira_import
+ @jira_project_key = running_import&.jira_project_key
raise Projects::ImportService::Error, _('Unable to find Jira project to import data from.') unless @jira_project_key
diff --git a/lib/gitlab/jira_import/handle_labels_service.rb b/lib/gitlab/jira_import/handle_labels_service.rb
new file mode 100644
index 00000000000..1b00515cced
--- /dev/null
+++ b/lib/gitlab/jira_import/handle_labels_service.rb
@@ -0,0 +1,39 @@
+# frozen_string_literal: true
+
+module Gitlab
+ module JiraImport
+ class HandleLabelsService
+ def initialize(project, jira_labels)
+ @project = project
+ @jira_labels = jira_labels
+ end
+
+ def execute
+ return if jira_labels.blank?
+
+ existing_labels = LabelsFinder.new(nil, project: project, title: jira_labels)
+ .execute(skip_authorization: true).select(:id, :name)
+ new_labels = create_missing_labels(existing_labels)
+
+ label_ids = existing_labels.map(&:id)
+ label_ids += new_labels if new_labels.present?
+ label_ids
+ end
+
+ private
+
+ attr_reader :project, :jira_labels
+
+ def create_missing_labels(existing_labels)
+ labels_to_create = jira_labels - existing_labels.map(&:name)
+ return if labels_to_create.empty?
+
+ new_labels_hash = labels_to_create.map do |title|
+ { project_id: project.id, title: title, type: 'ProjectLabel' }
+ end
+
+ Label.insert_all(new_labels_hash).rows.flatten
+ end
+ end
+ end
+end
diff --git a/lib/gitlab/jira_import/issue_serializer.rb b/lib/gitlab/jira_import/issue_serializer.rb
index cdcb62ac6e9..df57680073e 100644
--- a/lib/gitlab/jira_import/issue_serializer.rb
+++ b/lib/gitlab/jira_import/issue_serializer.rb
@@ -3,15 +3,14 @@
module Gitlab
module JiraImport
class IssueSerializer
- attr_reader :jira_issue, :project, :params, :formatter
- attr_accessor :metadata
+ attr_reader :jira_issue, :project, :import_owner_id, :params, :formatter
- def initialize(project, jira_issue, params = {})
+ def initialize(project, jira_issue, import_owner_id, params = {})
@jira_issue = jira_issue
@project = project
+ @import_owner_id = import_owner_id
@params = params
@formatter = Gitlab::ImportFormatter.new
- @metadata = []
end
def execute
@@ -23,7 +22,9 @@ module Gitlab
state_id: map_status(jira_issue.status.statusCategory),
updated_at: jira_issue.updated,
created_at: jira_issue.created,
- author_id: project.creator_id # TODO: map actual author: https://gitlab.com/gitlab-org/gitlab/-/issues/210580
+ author_id: reporter,
+ assignee_ids: assignees,
+ label_ids: label_ids
}
end
@@ -35,10 +36,8 @@ module Gitlab
def description
body = []
- body << formatter.author_line(jira_issue.reporter.displayName)
- body << formatter.assignee_line(jira_issue.assignee.displayName) if jira_issue.assignee
body << jira_issue.description
- body << add_metadata
+ body << MetadataCollector.new(jira_issue).execute
body.join
end
@@ -52,48 +51,33 @@ module Gitlab
end
end
- def add_metadata
- add_field(%w(issuetype name), 'Issue type')
- add_field(%w(priority name), 'Priority')
- add_labels
- add_field('environment', 'Environment')
- add_field('duedate', 'Due date')
- add_parent
- add_versions
-
- return if metadata.empty?
-
- metadata.join("\n").prepend("\n\n---\n\n**Issue metadata**\n\n")
+ def map_user_id(jira_user)
+ Gitlab::JiraImport::UserMapper.new(project, jira_user).execute&.id
end
- def add_field(keys, field_label)
- value = fields.dig(*keys)
- return if value.blank?
-
- metadata << "- #{field_label}: #{value}"
+ def reporter
+ map_user_id(jira_issue.reporter&.attrs) || import_owner_id
end
- def add_labels
- return if fields['labels'].blank?
-
- metadata << "- Labels: #{fields['labels'].join(', ')}"
- end
+ def assignees
+ found_user_id = map_user_id(jira_issue.assignee&.attrs)
- def add_parent
- parent_issue_key = fields.dig('parent', 'key')
- return if parent_issue_key.blank?
+ return unless found_user_id
- metadata << "- Parent issue: [#{parent_issue_key}] #{fields['parent']['fields']['summary']}"
+ [found_user_id]
end
- def add_versions
- return if fields['fixVersions'].blank?
+ # We already create labels in Gitlab::JiraImport::LabelsImporter stage but
+ # there is a possibility it may fail or
+ # new labels were created on the Jira in the meantime
+ def label_ids
+ return if jira_issue.fields['labels'].blank?
- metadata << "- Fix versions: #{fields['fixVersions'].map { |version| version['name'] }.join(', ')}"
+ Gitlab::JiraImport::HandleLabelsService.new(project, jira_issue.fields['labels']).execute
end
- def fields
- jira_issue.fields
+ def logger
+ @logger ||= Gitlab::Import::Logger.build
end
end
end
diff --git a/lib/gitlab/jira_import/issues_importer.rb b/lib/gitlab/jira_import/issues_importer.rb
index 6543b633ddf..8c18e58d9df 100644
--- a/lib/gitlab/jira_import/issues_importer.rb
+++ b/lib/gitlab/jira_import/issues_importer.rb
@@ -57,7 +57,7 @@ module Gitlab
# For such cases we exit early if issue was already imported.
next if already_imported?(jira_issue.id)
- issue_attrs = IssueSerializer.new(project, jira_issue, { iid: next_iid }).execute
+ issue_attrs = IssueSerializer.new(project, jira_issue, running_import.user_id, { iid: next_iid }).execute
Gitlab::JiraImport::ImportIssueWorker.perform_async(project.id, jira_issue.id, issue_attrs, job_waiter.key)
job_waiter.jobs_remaining += 1
diff --git a/lib/gitlab/jira_import/labels_importer.rb b/lib/gitlab/jira_import/labels_importer.rb
index 35c434e48a4..6e6842e06bf 100644
--- a/lib/gitlab/jira_import/labels_importer.rb
+++ b/lib/gitlab/jira_import/labels_importer.rb
@@ -5,6 +5,8 @@ module Gitlab
class LabelsImporter < BaseImporter
attr_reader :job_waiter
+ MAX_LABELS = 500
+
def initialize(project)
super
@job_waiter = JobWaiter.new
@@ -19,15 +21,35 @@ module Gitlab
def cache_import_label(project)
label = project.jira_imports.by_jira_project_key(jira_project_key).last.label
- raise Projects::ImportService::Error, _('Failed to find import label for jira import.') unless label
+ raise Projects::ImportService::Error, _('Failed to find import label for Jira import.') unless label
JiraImport.cache_import_label_id(project.id, label.id)
end
def import_jira_labels
- # todo: import jira labels, see https://gitlab.com/gitlab-org/gitlab/-/issues/212651
+ start_at = 0
+ loop do
+ break if process_jira_page(start_at)
+
+ start_at += MAX_LABELS
+ end
+
job_waiter
end
+
+ def process_jira_page(start_at)
+ request = "/rest/api/2/label?maxResults=#{MAX_LABELS}&startAt=#{start_at}"
+ response = client.get(request)
+
+ return true if response['values'].blank?
+ return true unless response.key?('isLast')
+
+ Gitlab::JiraImport::HandleLabelsService.new(project, response['values']).execute
+
+ response['isLast']
+ rescue => e
+ Gitlab::ErrorTracking.track_exception(e, project_id: project.id, request: request)
+ end
end
end
end
diff --git a/lib/gitlab/jira_import/metadata_collector.rb b/lib/gitlab/jira_import/metadata_collector.rb
new file mode 100644
index 00000000000..4551f38ba98
--- /dev/null
+++ b/lib/gitlab/jira_import/metadata_collector.rb
@@ -0,0 +1,57 @@
+# frozen_string_literal: true
+
+module Gitlab
+ module JiraImport
+ class MetadataCollector
+ attr_accessor :jira_issue, :metadata
+
+ def initialize(jira_issue)
+ @jira_issue = jira_issue
+ @metadata = []
+ end
+
+ def execute
+ add_field(%w(issuetype name), 'Issue type')
+ add_field(%w(priority name), 'Priority')
+ add_field('environment', 'Environment')
+ add_field('duedate', 'Due date')
+ add_parent
+ add_versions
+
+ return if metadata.empty?
+
+ metadata.join("\n").prepend("\n\n---\n\n**Issue metadata**\n\n")
+ end
+
+ private
+
+ def add_field(keys, field_label)
+ value = fields.dig(*keys)
+ return if value.blank?
+
+ metadata << "- #{field_label}: #{value}"
+ end
+
+ def add_parent
+ parent_issue_key = fields.dig('parent', 'key')
+
+ return if parent_issue_key.blank?
+
+ parent_summary_key = fields.dig('parent', 'fields', 'summary')
+
+ metadata << "- Parent issue: [#{parent_issue_key}] #{parent_summary_key}".strip
+ end
+
+ def add_versions
+ return if fields['fixVersions'].blank? || !fields['fixVersions'].is_a?(Array)
+
+ versions = fields['fixVersions'].map { |version| version['name'] }.compact.join(', ')
+ metadata << "- Fix versions: #{versions}"
+ end
+
+ def fields
+ jira_issue.fields
+ end
+ end
+ end
+end
diff --git a/lib/gitlab/jira_import/user_mapper.rb b/lib/gitlab/jira_import/user_mapper.rb
new file mode 100644
index 00000000000..208ee49b724
--- /dev/null
+++ b/lib/gitlab/jira_import/user_mapper.rb
@@ -0,0 +1,53 @@
+# frozen_string_literal: true
+
+module Gitlab
+ module JiraImport
+ class UserMapper
+ include ::Gitlab::Utils::StrongMemoize
+
+ def initialize(project, jira_user)
+ @project = project
+ @jira_user = jira_user
+ end
+
+ def execute
+ return unless jira_user
+
+ email = jira_user['emailAddress']
+
+ # We also include emails that are not yet confirmed
+ users = User.by_any_email(email).to_a
+
+ user = users.first
+
+ # this event should never happen but we should log it in case we have invalid data
+ log_user_mapping_message('Multiple users found for an email address', email) if users.count > 1
+
+ unless project.project_member(user) || project.group&.group_member(user)
+ log_user_mapping_message('Jira user not found', email)
+
+ return
+ end
+
+ user
+ end
+
+ private
+
+ attr_reader :project, :jira_user, :params
+
+ def log_user_mapping_message(message, email)
+ logger.info(
+ project_id: project.id,
+ project_path: project.full_path,
+ user_email: email,
+ message: message
+ )
+ end
+
+ def logger
+ @logger ||= Gitlab::Import::Logger.build
+ end
+ end
+ end
+end
diff --git a/lib/gitlab/json.rb b/lib/gitlab/json.rb
index 5ebda67e2ae..5b6689dbefe 100644
--- a/lib/gitlab/json.rb
+++ b/lib/gitlab/json.rb
@@ -2,13 +2,25 @@
module Gitlab
module Json
+ INVALID_LEGACY_TYPES = [String, TrueClass, FalseClass].freeze
+
class << self
- def parse(*args)
- adapter.parse(*args)
+ def parse(string, *args, **named_args)
+ legacy_mode = legacy_mode_enabled?(named_args.delete(:legacy_mode))
+ data = adapter.parse(string, *args, **named_args)
+
+ handle_legacy_mode!(data) if legacy_mode
+
+ data
end
- def parse!(*args)
- adapter.parse!(*args)
+ def parse!(string, *args, **named_args)
+ legacy_mode = legacy_mode_enabled?(named_args.delete(:legacy_mode))
+ data = adapter.parse!(string, *args, **named_args)
+
+ handle_legacy_mode!(data) if legacy_mode
+
+ data
end
def dump(*args)
@@ -28,6 +40,20 @@ module Gitlab
def adapter
::JSON
end
+
+ def parser_error
+ ::JSON::ParserError
+ end
+
+ def legacy_mode_enabled?(arg_value)
+ arg_value.nil? ? false : arg_value
+ end
+
+ def handle_legacy_mode!(data)
+ return data unless Feature.enabled?(:json_wrapper_legacy_mode, default_enabled: true)
+
+ raise parser_error if INVALID_LEGACY_TYPES.any? { |type| data.is_a?(type) }
+ end
end
end
end
diff --git a/lib/gitlab/kubernetes/helm.rb b/lib/gitlab/kubernetes/helm.rb
index 3e201d68297..00ab7109267 100644
--- a/lib/gitlab/kubernetes/helm.rb
+++ b/lib/gitlab/kubernetes/helm.rb
@@ -3,7 +3,7 @@
module Gitlab
module Kubernetes
module Helm
- HELM_VERSION = '2.16.3'
+ HELM_VERSION = '2.16.6'
KUBECTL_VERSION = '1.13.12'
NAMESPACE = 'gitlab-managed-apps'
NAMESPACE_LABELS = { 'app.gitlab.com/managed_by' => :gitlab }.freeze
diff --git a/lib/gitlab/kubernetes/helm/api.rb b/lib/gitlab/kubernetes/helm/api.rb
index 3b843799d66..ceda18442d6 100644
--- a/lib/gitlab/kubernetes/helm/api.rb
+++ b/lib/gitlab/kubernetes/helm/api.rb
@@ -99,11 +99,7 @@ module Gitlab
command.cluster_role_binding_resource.tap do |cluster_role_binding_resource|
break unless cluster_role_binding_resource
- if cluster_role_binding_exists?(cluster_role_binding_resource)
- kubeclient.update_cluster_role_binding(cluster_role_binding_resource)
- else
- kubeclient.create_cluster_role_binding(cluster_role_binding_resource)
- end
+ kubeclient.update_cluster_role_binding(cluster_role_binding_resource)
end
end
diff --git a/lib/gitlab/kubernetes/helm/delete_command.rb b/lib/gitlab/kubernetes/helm/delete_command.rb
index 9d0fd30ba8f..771444ee9ee 100644
--- a/lib/gitlab/kubernetes/helm/delete_command.rb
+++ b/lib/gitlab/kubernetes/helm/delete_command.rb
@@ -36,8 +36,6 @@ module Gitlab
@rbac
end
- private
-
def delete_command
command = ['helm', 'delete', '--purge', name] + tls_flags_if_remote_tiller
diff --git a/lib/gitlab/kubernetes/helm/parsers/list_v2.rb b/lib/gitlab/kubernetes/helm/parsers/list_v2.rb
new file mode 100644
index 00000000000..c5c5d198a6c
--- /dev/null
+++ b/lib/gitlab/kubernetes/helm/parsers/list_v2.rb
@@ -0,0 +1,37 @@
+# frozen_string_literal: true
+
+module Gitlab
+ module Kubernetes
+ module Helm
+ module Parsers
+ # Parses Helm v2 list (JSON) output
+ class ListV2
+ ParserError = Class.new(StandardError)
+
+ attr_reader :contents, :json
+
+ def initialize(contents)
+ @contents = contents
+ @json = Gitlab::Json.parse(contents)
+ rescue JSON::ParserError => e
+ raise ParserError, e.message
+ end
+
+ def releases
+ @releases = helm_releases
+ end
+
+ private
+
+ def helm_releases
+ helm_releases = json['Releases'] || []
+
+ raise ParserError, 'Invalid format for Releases' unless helm_releases.all? { |item| item.is_a?(Hash) }
+
+ helm_releases
+ end
+ end
+ end
+ end
+ end
+end
diff --git a/lib/gitlab/kubernetes/kube_client.rb b/lib/gitlab/kubernetes/kube_client.rb
index 7c5525b982c..2110d586d30 100644
--- a/lib/gitlab/kubernetes/kube_client.rb
+++ b/lib/gitlab/kubernetes/kube_client.rb
@@ -19,7 +19,9 @@ module Gitlab
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' }
+ knative: { group: 'apis/serving.knative.dev', version: 'v1alpha1' },
+ metrics: { group: 'apis/metrics.k8s.io', version: 'v1beta1' },
+ networking: { group: 'apis/networking.k8s.io', version: 'v1' }
}.freeze
SUPPORTED_API_GROUPS.each do |name, params|
@@ -33,7 +35,8 @@ module Gitlab
end
# Core API methods delegates to the core api group client
- delegate :get_pods,
+ delegate :get_nodes,
+ :get_pods,
:get_secrets,
:get_config_map,
:get_namespace,
@@ -56,9 +59,7 @@ module Gitlab
# RBAC methods delegates to the apis/rbac.authorization.k8s.io api
# group client
- delegate :create_cluster_role_binding,
- :get_cluster_role_binding,
- :update_cluster_role_binding,
+ delegate :update_cluster_role_binding,
to: :rbac_client
# RBAC methods delegates to the apis/rbac.authorization.k8s.io api
@@ -70,9 +71,7 @@ module Gitlab
# RBAC methods delegates to the apis/rbac.authorization.k8s.io api
# group client
- delegate :create_role_binding,
- :get_role_binding,
- :update_role_binding,
+ delegate :update_role_binding,
to: :rbac_client
# non-entity methods that can only work with the core client
@@ -88,6 +87,14 @@ module Gitlab
:update_gateway,
to: :istio_client
+ # NetworkPolicy methods delegate to the apis/networking.k8s.io api
+ # group client
+ delegate :create_network_policy,
+ :get_network_policies,
+ :update_network_policy,
+ :delete_network_policy,
+ to: :networking_client
+
attr_reader :api_prefix, :kubeclient_options
DEFAULT_KUBECLIENT_OPTIONS = {
@@ -97,6 +104,31 @@ module Gitlab
}
}.freeze
+ def self.graceful_request(cluster_id)
+ { status: :connected, response: yield }
+ rescue *Gitlab::Kubernetes::Errors::CONNECTION
+ { status: :unreachable }
+ rescue *Gitlab::Kubernetes::Errors::AUTHENTICATION
+ { status: :authentication_failure }
+ rescue Kubeclient::HttpError => e
+ { status: kubeclient_error_status(e.message) }
+ rescue => e
+ Gitlab::ErrorTracking.track_exception(e, cluster_id: cluster_id)
+
+ { status: :unknown_failure }
+ end
+
+ # KubeClient uses the same error class
+ # For connection errors (eg. timeout) and
+ # for Kubernetes errors.
+ def self.kubeclient_error_status(message)
+ if message&.match?(/timed out|timeout/i)
+ :unreachable
+ else
+ :authentication_failure
+ end
+ end
+
# We disable redirects through 'http_max_redirects: 0',
# so that KubeClient does not follow redirects and
# expose internal services.
@@ -125,19 +157,11 @@ module Gitlab
end
def create_or_update_cluster_role_binding(resource)
- if cluster_role_binding_exists?(resource)
- update_cluster_role_binding(resource)
- else
- create_cluster_role_binding(resource)
- end
+ update_cluster_role_binding(resource)
end
def create_or_update_role_binding(resource)
- if role_binding_exists?(resource)
- update_role_binding(resource)
- else
- create_role_binding(resource)
- end
+ update_role_binding(resource)
end
def create_or_update_service_account(resource)
@@ -164,18 +188,6 @@ module Gitlab
Gitlab::UrlBlocker.validate!(api_prefix, allow_local_network: false)
end
- def cluster_role_binding_exists?(resource)
- get_cluster_role_binding(resource.metadata.name)
- rescue ::Kubeclient::ResourceNotFoundError
- false
- end
-
- def role_binding_exists?(resource)
- get_role_binding(resource.metadata.name, resource.metadata.namespace)
- rescue ::Kubeclient::ResourceNotFoundError
- false
- end
-
def service_account_exists?(resource)
get_service_account(resource.metadata.name, resource.metadata.namespace)
rescue ::Kubeclient::ResourceNotFoundError
diff --git a/lib/gitlab/kubernetes/network_policy.rb b/lib/gitlab/kubernetes/network_policy.rb
new file mode 100644
index 00000000000..ea25d81cbd2
--- /dev/null
+++ b/lib/gitlab/kubernetes/network_policy.rb
@@ -0,0 +1,91 @@
+# frozen_string_literal: true
+
+module Gitlab
+ module Kubernetes
+ class NetworkPolicy
+ def initialize(name:, namespace:, pod_selector:, ingress:, creation_timestamp: nil, policy_types: ["Ingress"], egress: nil)
+ @name = name
+ @namespace = namespace
+ @creation_timestamp = creation_timestamp
+ @pod_selector = pod_selector
+ @policy_types = policy_types
+ @ingress = ingress
+ @egress = egress
+ end
+
+ def self.from_yaml(manifest)
+ return unless manifest
+
+ policy = YAML.safe_load(manifest, symbolize_names: true)
+ return if !policy[:metadata] || !policy[:spec]
+
+ metadata = policy[:metadata]
+ spec = policy[:spec]
+ self.new(
+ name: metadata[:name],
+ namespace: metadata[:namespace],
+ pod_selector: spec[:podSelector],
+ policy_types: spec[:policyTypes],
+ ingress: spec[:ingress],
+ egress: spec[:egress]
+ )
+ rescue Psych::SyntaxError, Psych::DisallowedClass
+ nil
+ end
+
+ def self.from_resource(resource)
+ return unless resource
+ return if !resource[:metadata] || !resource[:spec]
+
+ metadata = resource[:metadata]
+ spec = resource[:spec].to_h
+ self.new(
+ name: metadata[:name],
+ namespace: metadata[:namespace],
+ creation_timestamp: metadata[:creationTimestamp],
+ pod_selector: spec[:podSelector],
+ policy_types: spec[:policyTypes],
+ ingress: spec[:ingress],
+ egress: spec[:egress]
+ )
+ end
+
+ def generate
+ ::Kubeclient::Resource.new.tap do |resource|
+ resource.metadata = metadata
+ resource.spec = spec
+ end
+ end
+
+ def as_json(opts = nil)
+ {
+ name: name,
+ namespace: namespace,
+ creation_timestamp: creation_timestamp,
+ manifest: manifest
+ }
+ end
+
+ private
+
+ attr_reader :name, :namespace, :creation_timestamp, :pod_selector, :policy_types, :ingress, :egress
+
+ def metadata
+ { name: name, namespace: namespace }
+ end
+
+ def spec
+ {
+ podSelector: pod_selector,
+ policyTypes: policy_types,
+ ingress: ingress,
+ egress: egress
+ }
+ end
+
+ def manifest
+ YAML.dump({ metadata: metadata, spec: spec }.deep_stringify_keys)
+ end
+ end
+ end
+end
diff --git a/lib/gitlab/logging/cloudflare_helper.rb b/lib/gitlab/logging/cloudflare_helper.rb
new file mode 100644
index 00000000000..5cffe335bb5
--- /dev/null
+++ b/lib/gitlab/logging/cloudflare_helper.rb
@@ -0,0 +1,23 @@
+# frozen_string_literal: true
+
+module Gitlab
+ module Logging
+ module CloudflareHelper
+ CLOUDFLARE_CUSTOM_HEADERS = { 'Cf-Ray' => :cf_ray, 'Cf-Request-Id' => :cf_request_id }.freeze
+
+ def store_cloudflare_headers!(payload, request)
+ CLOUDFLARE_CUSTOM_HEADERS.each do |header, value|
+ payload[value] = request.headers[header] if valid_cloudflare_header?(request.headers[header])
+ end
+ end
+
+ def valid_cloudflare_header?(value)
+ return false unless value.present?
+ return false if value.length > 64
+ return false if value.index(/[^[A-Za-z0-9-]]/)
+
+ true
+ end
+ end
+ end
+end
diff --git a/lib/gitlab/lograge/custom_options.rb b/lib/gitlab/lograge/custom_options.rb
index 145d67d7101..55c46c365f6 100644
--- a/lib/gitlab/lograge/custom_options.rb
+++ b/lib/gitlab/lograge/custom_options.rb
@@ -3,6 +3,8 @@
module Gitlab
module Lograge
module CustomOptions
+ include ::Gitlab::Logging::CloudflareHelper
+
LIMITED_ARRAY_SENTINEL = { key: 'truncated', value: '...' }.freeze
IGNORE_PARAMS = Set.new(%w(controller action format)).freeze
@@ -21,6 +23,8 @@ module Gitlab
queue_duration_s: event.payload[:queue_duration_s]
}
+ payload.merge!(event.payload[:metadata]) if event.payload[:metadata]
+
::Gitlab::InstrumentationHelper.add_instrumentation_data(payload)
payload[:response] = event.payload[:response] if event.payload[:response]
@@ -31,6 +35,10 @@ module Gitlab
payload[:cpu_s] = cpu_s.round(2)
end
+ CLOUDFLARE_CUSTOM_HEADERS.each do |_, value|
+ payload[value] = event.payload[value] if event.payload[value]
+ end
+
# https://github.com/roidrage/lograge#logging-errors--exceptions
exception = event.payload[:exception_object]
diff --git a/lib/gitlab/mail_room.rb b/lib/gitlab/mail_room.rb
index bd69843adf1..0633efc6b0c 100644
--- a/lib/gitlab/mail_room.rb
+++ b/lib/gitlab/mail_room.rb
@@ -19,7 +19,8 @@ module Gitlab
start_tls: false,
mailbox: 'inbox',
idle_timeout: 60,
- log_path: RAILS_ROOT_DIR.join('log', 'mail_room_json.log')
+ log_path: RAILS_ROOT_DIR.join('log', 'mail_room_json.log'),
+ expunge_deleted: false
}.freeze
# Email specific configuration which is merged with configuration
diff --git a/lib/gitlab/markdown_cache.rb b/lib/gitlab/markdown_cache.rb
index d7a0a9b6518..489fc6fddac 100644
--- a/lib/gitlab/markdown_cache.rb
+++ b/lib/gitlab/markdown_cache.rb
@@ -3,7 +3,7 @@
module Gitlab
module MarkdownCache
# Increment this number every time the renderer changes its output
- CACHE_COMMONMARK_VERSION = 20
+ CACHE_COMMONMARK_VERSION = 21
CACHE_COMMONMARK_VERSION_START = 10
BaseError = Class.new(StandardError)
diff --git a/lib/gitlab/metrics.rb b/lib/gitlab/metrics.rb
index d759ae24051..5fed3d38d7c 100644
--- a/lib/gitlab/metrics.rb
+++ b/lib/gitlab/metrics.rb
@@ -2,17 +2,106 @@
module Gitlab
module Metrics
- include Gitlab::Metrics::InfluxDb
include Gitlab::Metrics::Prometheus
+ include Gitlab::Metrics::Methods
+
+ EXECUTION_MEASUREMENT_BUCKETS = [0.001, 0.01, 0.1, 1].freeze
@error = false
def self.enabled?
- influx_metrics_enabled? || prometheus_metrics_enabled?
+ prometheus_metrics_enabled?
end
def self.error?
@error
end
+
+ # Tracks an event.
+ #
+ # See `Gitlab::Metrics::Transaction#add_event` for more details.
+ def self.add_event(*args)
+ current_transaction&.add_event(*args)
+ end
+
+ # Allow access from other metrics related middlewares
+ def self.current_transaction
+ Transaction.current
+ end
+
+ # Returns the prefix to use for the name of a series.
+ def self.series_prefix
+ @series_prefix ||= Gitlab::Runtime.sidekiq? ? 'sidekiq_' : 'rails_'
+ end
+
+ def self.settings
+ @settings ||= begin
+ current_settings = Gitlab::CurrentSettings.current_application_settings
+
+ {
+
+ method_call_threshold: current_settings[:metrics_method_call_threshold]
+
+ }
+ end
+ end
+
+ def self.method_call_threshold
+ # This is memoized since this method is called for every instrumented
+ # method. Loading data from an external cache on every method call slows
+ # things down too much.
+ # in milliseconds
+ @method_call_threshold ||= settings[:method_call_threshold]
+ end
+
+ # Measures the execution time of a block.
+ #
+ # Example:
+ #
+ # Gitlab::Metrics.measure(:find_by_username_duration) do
+ # UserFinder.new(some_username).find_by_username
+ # end
+ #
+ # name - The name of the field to store the execution time in.
+ #
+ # Returns the value yielded by the supplied block.
+ def self.measure(name)
+ trans = current_transaction
+
+ return yield unless trans
+
+ real_start = System.monotonic_time
+ cpu_start = System.cpu_time
+
+ retval = yield
+
+ cpu_stop = System.cpu_time
+ real_stop = System.monotonic_time
+
+ real_time = (real_stop - real_start)
+ cpu_time = cpu_stop - cpu_start
+
+ real_duration_seconds = fetch_histogram("gitlab_#{name}_real_duration_seconds".to_sym) do
+ docstring "Measure #{name}"
+ base_labels Transaction::BASE_LABELS
+ buckets EXECUTION_MEASUREMENT_BUCKETS
+ end
+
+ real_duration_seconds.observe(trans.labels, real_time)
+
+ cpu_duration_seconds = fetch_histogram("gitlab_#{name}_cpu_duration_seconds".to_sym) do
+ docstring "Measure #{name}"
+ base_labels Transaction::BASE_LABELS
+ buckets EXECUTION_MEASUREMENT_BUCKETS
+ with_feature "prometheus_metrics_measure_#{name}_cpu_duration"
+ end
+ cpu_duration_seconds.observe(trans.labels, cpu_time)
+
+ trans.increment("#{name}_real_time", real_time.in_milliseconds, false)
+ trans.increment("#{name}_cpu_time", cpu_time.in_milliseconds, false)
+ trans.increment("#{name}_call_count", 1, false)
+
+ retval
+ end
end
end
diff --git a/lib/gitlab/metrics/dashboard/url.rb b/lib/gitlab/metrics/dashboard/url.rb
index 1d948883151..31670a3f533 100644
--- a/lib/gitlab/metrics/dashboard/url.rb
+++ b/lib/gitlab/metrics/dashboard/url.rb
@@ -23,7 +23,7 @@ module Gitlab
%r{
/environments
/(?<environment>\d+)
- /metrics
+ /(metrics_dashboard|metrics)
}x
)
end
diff --git a/lib/gitlab/metrics/exporter/sidekiq_exporter.rb b/lib/gitlab/metrics/exporter/sidekiq_exporter.rb
index 5ba7b29734b..054b4949dd6 100644
--- a/lib/gitlab/metrics/exporter/sidekiq_exporter.rb
+++ b/lib/gitlab/metrics/exporter/sidekiq_exporter.rb
@@ -32,7 +32,7 @@ module Gitlab
Sidekiq.logger.error(
class: self.class.to_s,
message: 'Cannot start sidekiq_exporter',
- exception: e.message
+ 'exception.message' => e.message
)
false
diff --git a/lib/gitlab/metrics/influx_db.rb b/lib/gitlab/metrics/influx_db.rb
deleted file mode 100644
index 1f252572461..00000000000
--- a/lib/gitlab/metrics/influx_db.rb
+++ /dev/null
@@ -1,183 +0,0 @@
-# frozen_string_literal: true
-
-module Gitlab
- module Metrics
- module InfluxDb
- extend ActiveSupport::Concern
- include Gitlab::Metrics::Methods
-
- EXECUTION_MEASUREMENT_BUCKETS = [0.001, 0.01, 0.1, 1].freeze
-
- MUTEX = Mutex.new
- private_constant :MUTEX
-
- class_methods do
- def influx_metrics_enabled?
- settings[:enabled] || false
- end
-
- # Prometheus histogram buckets used for arbitrary code measurements
-
- def settings
- @settings ||= begin
- current_settings = Gitlab::CurrentSettings.current_application_settings
-
- {
- enabled: current_settings[:metrics_enabled],
- pool_size: current_settings[:metrics_pool_size],
- timeout: current_settings[:metrics_timeout],
- method_call_threshold: current_settings[:metrics_method_call_threshold],
- host: current_settings[:metrics_host],
- port: current_settings[:metrics_port],
- sample_interval: current_settings[:metrics_sample_interval] || 15,
- packet_size: current_settings[:metrics_packet_size] || 1
- }
- end
- end
-
- def mri?
- RUBY_ENGINE == 'ruby'
- end
-
- def method_call_threshold
- # This is memoized since this method is called for every instrumented
- # method. Loading data from an external cache on every method call slows
- # things down too much.
- # in milliseconds
- @method_call_threshold ||= settings[:method_call_threshold]
- end
-
- def submit_metrics(metrics)
- prepared = prepare_metrics(metrics)
-
- pool&.with do |connection|
- prepared.each_slice(settings[:packet_size]) do |slice|
- connection.write_points(slice)
- rescue StandardError
- end
- end
- rescue Errno::EADDRNOTAVAIL, SocketError => ex
- Gitlab::EnvironmentLogger.error('Cannot resolve InfluxDB address. GitLab Performance Monitoring will not work.')
- Gitlab::EnvironmentLogger.error(ex)
- end
-
- def prepare_metrics(metrics)
- metrics.map do |hash|
- new_hash = hash.symbolize_keys
-
- new_hash[:tags].each do |key, value|
- if value.blank?
- new_hash[:tags].delete(key)
- else
- new_hash[:tags][key] = escape_value(value)
- end
- end
-
- new_hash
- end
- end
-
- def escape_value(value)
- value.to_s.gsub('=', '\\=')
- end
-
- # Measures the execution time of a block.
- #
- # Example:
- #
- # Gitlab::Metrics.measure(:find_by_username_duration) do
- # UserFinder.new(some_username).find_by_username
- # end
- #
- # name - The name of the field to store the execution time in.
- #
- # Returns the value yielded by the supplied block.
- def measure(name)
- trans = current_transaction
-
- return yield unless trans
-
- real_start = Time.now.to_f
- cpu_start = System.cpu_time
-
- retval = yield
-
- cpu_stop = System.cpu_time
- real_stop = Time.now.to_f
-
- real_time = (real_stop - real_start)
- cpu_time = cpu_stop - cpu_start
-
- real_duration_seconds = fetch_histogram("gitlab_#{name}_real_duration_seconds".to_sym) do
- docstring "Measure #{name}"
- base_labels Transaction::BASE_LABELS
- buckets EXECUTION_MEASUREMENT_BUCKETS
- end
-
- real_duration_seconds.observe(trans.labels, real_time)
-
- cpu_duration_seconds = fetch_histogram("gitlab_#{name}_cpu_duration_seconds".to_sym) do
- docstring "Measure #{name}"
- base_labels Transaction::BASE_LABELS
- buckets EXECUTION_MEASUREMENT_BUCKETS
- with_feature "prometheus_metrics_measure_#{name}_cpu_duration"
- end
- cpu_duration_seconds.observe(trans.labels, cpu_time)
-
- # InfluxDB stores the _real_time and _cpu_time time values as milliseconds
- trans.increment("#{name}_real_time", real_time.in_milliseconds, false)
- trans.increment("#{name}_cpu_time", cpu_time.in_milliseconds, false)
- trans.increment("#{name}_call_count", 1, false)
-
- retval
- end
-
- # Sets the action of the current transaction (if any)
- #
- # action - The name of the action.
- def action=(action)
- trans = current_transaction
-
- trans&.action = action
- end
-
- # Tracks an event.
- #
- # See `Gitlab::Metrics::Transaction#add_event` for more details.
- def add_event(*args)
- current_transaction&.add_event(*args)
- end
-
- # Returns the prefix to use for the name of a series.
- def series_prefix
- @series_prefix ||= Gitlab::Runtime.sidekiq? ? 'sidekiq_' : 'rails_'
- end
-
- # Allow access from other metrics related middlewares
- def current_transaction
- Transaction.current
- end
-
- # When enabled this should be set before being used as the usual pattern
- # "@foo ||= bar" is _not_ thread-safe.
- def pool
- if influx_metrics_enabled?
- if @pool.nil?
- MUTEX.synchronize do
- @pool ||= ConnectionPool.new(size: settings[:pool_size], timeout: settings[:timeout]) do
- host = settings[:host]
- port = settings[:port]
-
- InfluxDB::Client
- .new(udp: { host: host, port: port })
- end
- end
- end
-
- @pool
- end
- end
- end
- end
- end
-end
diff --git a/lib/gitlab/metrics/method_call.rb b/lib/gitlab/metrics/method_call.rb
index d0c63a862c2..fbeda3b75e0 100644
--- a/lib/gitlab/metrics/method_call.rb
+++ b/lib/gitlab/metrics/method_call.rb
@@ -49,19 +49,6 @@ module Gitlab
retval
end
- # Returns a Metric instance of the current method call.
- def to_metric
- Metric.new(
- Instrumentation.series,
- {
- duration: real_time.in_milliseconds.to_i,
- cpu_duration: cpu_time.in_milliseconds.to_i,
- call_count: call_count
- },
- method: @name
- )
- end
-
# Returns true if the total runtime of this method exceeds the method call
# threshold.
def above_threshold?
diff --git a/lib/gitlab/metrics/metric.rb b/lib/gitlab/metrics/metric.rb
deleted file mode 100644
index 30f181542be..00000000000
--- a/lib/gitlab/metrics/metric.rb
+++ /dev/null
@@ -1,54 +0,0 @@
-# frozen_string_literal: true
-
-module Gitlab
- module Metrics
- # Class for storing details of a single metric (label, value, etc).
- class Metric
- JITTER_RANGE = (0.000001..0.001).freeze
-
- attr_reader :series, :values, :tags, :type
-
- # series - The name of the series (as a String) to store the metric in.
- # values - A Hash containing the values to store.
- # tags - A Hash containing extra tags to add to the metrics.
- def initialize(series, values, tags = {}, type = :metric)
- @values = values
- @series = series
- @tags = tags
- @type = type
- end
-
- def event?
- type == :event
- end
-
- # Returns a Hash in a format that can be directly written to InfluxDB.
- def to_hash
- # InfluxDB overwrites an existing point if a new point has the same
- # series, tag set, and timestamp. In a highly concurrent environment
- # this means that using the number of seconds since the Unix epoch is
- # inevitably going to collide with another timestamp. For example, two
- # Rails requests processed by different processes may end up generating
- # metrics using the _exact_ same timestamp (in seconds).
- #
- # Due to the way InfluxDB is set up there's no solution to this problem,
- # all we can do is lower the amount of collisions. We do this by using
- # System.real_time which returns the nanoseconds as a Float providing
- # greater accuracy. We then add a small random value that is large
- # enough to distinguish most timestamps but small enough to not alter
- # the timestamp significantly.
- #
- # See https://gitlab.com/gitlab-com/operations/issues/175 for more
- # information.
- time = System.real_time(:nanosecond) + rand(JITTER_RANGE)
-
- {
- series: @series,
- tags: @tags,
- values: @values,
- timestamp: time.to_i
- }
- end
- end
- end
-end
diff --git a/lib/gitlab/metrics/rack_middleware.rb b/lib/gitlab/metrics/rack_middleware.rb
index 9aa97515961..c6a0457ffe5 100644
--- a/lib/gitlab/metrics/rack_middleware.rb
+++ b/lib/gitlab/metrics/rack_middleware.rb
@@ -20,10 +20,6 @@ module Gitlab
trans.add_event(:rails_exception)
raise error
- # Even in the event of an error we want to submit any metrics we
- # might've gathered up to this point.
- ensure
- trans.finish
end
retval
diff --git a/lib/gitlab/metrics/samplers/database_sampler.rb b/lib/gitlab/metrics/samplers/database_sampler.rb
new file mode 100644
index 00000000000..9ee4b0960c5
--- /dev/null
+++ b/lib/gitlab/metrics/samplers/database_sampler.rb
@@ -0,0 +1,58 @@
+# frozen_string_literal: true
+
+module Gitlab
+ module Metrics
+ module Samplers
+ class DatabaseSampler < BaseSampler
+ SAMPLING_INTERVAL_SECONDS = 5
+
+ METRIC_PREFIX = 'gitlab_database_connection_pool_'
+
+ METRIC_DESCRIPTIONS = {
+ size: 'Total connection pool capacity',
+ connections: 'Current connections in the pool',
+ busy: 'Connections in use where the owner is still alive',
+ dead: 'Connections in use where the owner is not alive',
+ idle: 'Connections not in use',
+ waiting: 'Threads currently waiting on this queue'
+ }.freeze
+
+ def metrics
+ @metrics ||= init_metrics
+ end
+
+ def sample
+ host_stats.each do |host_stat|
+ METRIC_DESCRIPTIONS.each_key do |metric|
+ metrics[metric].set(host_stat[:labels], host_stat[:stats][metric])
+ end
+ end
+ end
+
+ private
+
+ def init_metrics
+ METRIC_DESCRIPTIONS.map do |name, description|
+ [name, ::Gitlab::Metrics.gauge(:"#{METRIC_PREFIX}#{name}", description)]
+ end.to_h
+ end
+
+ def host_stats
+ return [] unless ActiveRecord::Base.connected?
+
+ [{ labels: labels_for_class(ActiveRecord::Base), stats: ActiveRecord::Base.connection_pool.stat }]
+ end
+
+ def labels_for_class(klass)
+ {
+ host: klass.connection_config[:host],
+ port: klass.connection_config[:port],
+ class: klass.to_s
+ }
+ end
+ end
+ end
+ end
+end
+
+Gitlab::Metrics::Samplers::DatabaseSampler.prepend_if_ee('EE::Gitlab::Metrics::Samplers::DatabaseSampler')
diff --git a/lib/gitlab/metrics/samplers/influx_sampler.rb b/lib/gitlab/metrics/samplers/influx_sampler.rb
deleted file mode 100644
index 4e16e335bee..00000000000
--- a/lib/gitlab/metrics/samplers/influx_sampler.rb
+++ /dev/null
@@ -1,49 +0,0 @@
-# frozen_string_literal: true
-
-module Gitlab
- module Metrics
- module Samplers
- # Class that sends certain metrics to InfluxDB at a specific interval.
- #
- # This class is used to gather statistics that can't be directly associated
- # with a transaction such as system memory usage, garbage collection
- # statistics, etc.
- class InfluxSampler < BaseSampler
- # interval - The sampling interval in seconds.
- def initialize(interval = ::Gitlab::Metrics.settings[:sample_interval])
- super(interval)
- @last_step = nil
-
- @metrics = []
- end
-
- def sample
- sample_memory_usage
- sample_file_descriptors
-
- flush
- ensure
- @metrics.clear
- end
-
- def flush
- ::Gitlab::Metrics.submit_metrics(@metrics.map(&:to_hash))
- end
-
- def sample_memory_usage
- add_metric('memory_usage', value: System.memory_usage)
- end
-
- def sample_file_descriptors
- add_metric('file_descriptors', value: System.file_descriptor_count)
- end
-
- def add_metric(series, values, tags = {})
- prefix = Gitlab::Runtime.sidekiq? ? 'sidekiq_' : 'rails_'
-
- @metrics << Metric.new("#{prefix}#{series}", values, tags)
- end
- end
- end
- end
-end
diff --git a/lib/gitlab/metrics/samplers/puma_sampler.rb b/lib/gitlab/metrics/samplers/puma_sampler.rb
index f788f51b1ce..98dd517ee3b 100644
--- a/lib/gitlab/metrics/samplers/puma_sampler.rb
+++ b/lib/gitlab/metrics/samplers/puma_sampler.rb
@@ -26,7 +26,7 @@ module Gitlab
json_stats = puma_stats
return unless json_stats
- stats = JSON.parse(json_stats)
+ stats = Gitlab::Json.parse(json_stats)
if cluster?(stats)
sample_cluster(stats)
diff --git a/lib/gitlab/metrics/samplers/ruby_sampler.rb b/lib/gitlab/metrics/samplers/ruby_sampler.rb
index c38769f39a9..df59c06911b 100644
--- a/lib/gitlab/metrics/samplers/ruby_sampler.rb
+++ b/lib/gitlab/metrics/samplers/ruby_sampler.rb
@@ -34,14 +34,15 @@ module Gitlab
def init_metrics
metrics = {
- file_descriptors: ::Gitlab::Metrics.gauge(with_prefix(:file, :descriptors), 'File descriptors used', labels),
- memory_bytes: ::Gitlab::Metrics.gauge(with_prefix(:memory, :bytes), 'Memory used', labels),
- process_cpu_seconds_total: ::Gitlab::Metrics.gauge(with_prefix(:process, :cpu_seconds_total), 'Process CPU seconds total'),
- process_max_fds: ::Gitlab::Metrics.gauge(with_prefix(:process, :max_fds), 'Process max fds'),
- process_resident_memory_bytes: ::Gitlab::Metrics.gauge(with_prefix(:process, :resident_memory_bytes), 'Memory used', labels),
- process_start_time_seconds: ::Gitlab::Metrics.gauge(with_prefix(:process, :start_time_seconds), 'Process start time seconds'),
- sampler_duration: ::Gitlab::Metrics.counter(with_prefix(:sampler, :duration_seconds_total), 'Sampler time', labels),
- gc_duration_seconds: ::Gitlab::Metrics.histogram(with_prefix(:gc, :duration_seconds), 'GC time', labels, GC_REPORT_BUCKETS)
+ file_descriptors: ::Gitlab::Metrics.gauge(with_prefix(:file, :descriptors), 'File descriptors used', labels),
+ process_cpu_seconds_total: ::Gitlab::Metrics.gauge(with_prefix(:process, :cpu_seconds_total), 'Process CPU seconds total'),
+ process_max_fds: ::Gitlab::Metrics.gauge(with_prefix(:process, :max_fds), 'Process max fds'),
+ process_resident_memory_bytes: ::Gitlab::Metrics.gauge(with_prefix(:process, :resident_memory_bytes), 'Memory used (RSS)', labels),
+ process_unique_memory_bytes: ::Gitlab::Metrics.gauge(with_prefix(:process, :unique_memory_bytes), 'Memory used (USS)', labels),
+ process_proportional_memory_bytes: ::Gitlab::Metrics.gauge(with_prefix(:process, :proportional_memory_bytes), 'Memory used (PSS)', labels),
+ process_start_time_seconds: ::Gitlab::Metrics.gauge(with_prefix(:process, :start_time_seconds), 'Process start time seconds'),
+ sampler_duration: ::Gitlab::Metrics.counter(with_prefix(:sampler, :duration_seconds_total), 'Sampler time', labels),
+ gc_duration_seconds: ::Gitlab::Metrics.histogram(with_prefix(:gc, :duration_seconds), 'GC time', labels, GC_REPORT_BUCKETS)
}
GC.stat.keys.each do |key|
@@ -85,10 +86,13 @@ module Gitlab
end
def set_memory_usage_metrics
- memory_usage = System.memory_usage
+ metrics[:process_resident_memory_bytes].set(labels, System.memory_usage_rss)
- metrics[:memory_bytes].set(labels, memory_usage)
- metrics[:process_resident_memory_bytes].set(labels, memory_usage)
+ if Gitlab::Utils.to_boolean(ENV['enable_memory_uss_pss'] || '1')
+ memory_uss_pss = System.memory_usage_uss_pss
+ metrics[:process_unique_memory_bytes].set(labels, memory_uss_pss[:uss])
+ metrics[:process_proportional_memory_bytes].set(labels, memory_uss_pss[:pss])
+ end
end
end
end
diff --git a/lib/gitlab/metrics/sidekiq_middleware.rb b/lib/gitlab/metrics/sidekiq_middleware.rb
index 0b4485feea9..8dfb61046c4 100644
--- a/lib/gitlab/metrics/sidekiq_middleware.rb
+++ b/lib/gitlab/metrics/sidekiq_middleware.rb
@@ -17,8 +17,6 @@ module Gitlab
trans.add_event(:sidekiq_exception)
raise error
- ensure
- trans.finish
end
end
end
diff --git a/lib/gitlab/metrics/subscribers/action_view.rb b/lib/gitlab/metrics/subscribers/action_view.rb
index 5bd21b8e5d1..24107e42aa9 100644
--- a/lib/gitlab/metrics/subscribers/action_view.rb
+++ b/lib/gitlab/metrics/subscribers/action_view.rb
@@ -26,23 +26,17 @@ module Gitlab
private
def track(event)
- values = values_for(event)
- tags = tags_for(event)
+ tags = tags_for(event)
self.class.gitlab_view_rendering_duration_seconds.observe(current_transaction.labels.merge(tags), event.duration)
current_transaction.increment(:view_duration, event.duration)
- current_transaction.add_metric(SERIES, values, tags)
end
def relative_path(path)
path.gsub(%r{^#{Rails.root}/?}, '')
end
- def values_for(event)
- { duration: event.duration }
- end
-
def tags_for(event)
path = relative_path(event.payload[:identifier])
diff --git a/lib/gitlab/metrics/system.rb b/lib/gitlab/metrics/system.rb
index 2a61b3de405..43005303dec 100644
--- a/lib/gitlab/metrics/system.rb
+++ b/lib/gitlab/metrics/system.rb
@@ -7,47 +7,37 @@ module Gitlab
# This module relies on the /proc filesystem being available. If /proc is
# not available the methods of this module will be stubbed.
module System
- if File.exist?('/proc')
- # Returns the current process' memory usage in bytes.
- def self.memory_usage
- mem = 0
- match = File.read('/proc/self/status').match(/VmRSS:\s+(\d+)/)
-
- if match && match[1]
- mem = match[1].to_f * 1024
- end
-
- mem
- end
-
- def self.file_descriptor_count
- Dir.glob('/proc/self/fd/*').length
- end
-
- def self.max_open_file_descriptors
- match = File.read('/proc/self/limits').match(/Max open files\s*(\d+)/)
-
- return unless match && match[1]
+ PROC_STATUS_PATH = '/proc/self/status'
+ PROC_SMAPS_ROLLUP_PATH = '/proc/self/smaps_rollup'
+ PROC_LIMITS_PATH = '/proc/self/limits'
+ PROC_FD_GLOB = '/proc/self/fd/*'
+
+ PRIVATE_PAGES_PATTERN = /^(Private_Clean|Private_Dirty|Private_Hugetlb):\s+(?<value>\d+)/.freeze
+ PSS_PATTERN = /^Pss:\s+(?<value>\d+)/.freeze
+ RSS_PATTERN = /VmRSS:\s+(?<value>\d+)/.freeze
+ MAX_OPEN_FILES_PATTERN = /Max open files\s*(?<value>\d+)/.freeze
+
+ # Returns the current process' RSS (resident set size) in bytes.
+ def self.memory_usage_rss
+ sum_matches(PROC_STATUS_PATH, rss: RSS_PATTERN)[:rss].kilobytes
+ end
- match[1].to_i
- end
- else
- def self.memory_usage
- 0.0
- end
+ # Returns the current process' USS/PSS (unique/proportional set size) in bytes.
+ def self.memory_usage_uss_pss
+ sum_matches(PROC_SMAPS_ROLLUP_PATH, uss: PRIVATE_PAGES_PATTERN, pss: PSS_PATTERN)
+ .transform_values(&:kilobytes)
+ end
- def self.file_descriptor_count
- 0
- end
+ def self.file_descriptor_count
+ Dir.glob(PROC_FD_GLOB).length
+ end
- def self.max_open_file_descriptors
- 0
- end
+ def self.max_open_file_descriptors
+ sum_matches(PROC_LIMITS_PATH, max_fds: MAX_OPEN_FILES_PATTERN)[:max_fds]
end
def self.cpu_time
- Process
- .clock_gettime(Process::CLOCK_PROCESS_CPUTIME_ID, :float_second)
+ Process.clock_gettime(Process::CLOCK_PROCESS_CPUTIME_ID, :float_second)
end
# Returns the current real time in a given precision.
@@ -78,6 +68,27 @@ module Gitlab
end_time - start_time
end
+
+ # Given a path to a file in /proc and a hash of (metric, pattern) pairs,
+ # sums up all values found for those patterns under the respective metric.
+ def self.sum_matches(proc_file, **patterns)
+ results = patterns.transform_values { 0 }
+
+ begin
+ File.foreach(proc_file) do |line|
+ patterns.each do |metric, pattern|
+ match = line.match(pattern)
+ value = match&.named_captures&.fetch('value', 0)
+ results[metric] += value.to_i
+ end
+ end
+ rescue Errno::ENOENT
+ # This means the procfile we're reading from did not exist;
+ # this is safe to ignore, since we initialize each metric to 0
+ end
+
+ results
+ end
end
end
end
diff --git a/lib/gitlab/metrics/transaction.rb b/lib/gitlab/metrics/transaction.rb
index 552eae639e6..b126efd2dd5 100644
--- a/lib/gitlab/metrics/transaction.rb
+++ b/lib/gitlab/metrics/transaction.rb
@@ -16,20 +16,18 @@ module Gitlab
# The series to store events (e.g. Git pushes) in.
EVENT_SERIES = 'events'
- attr_reader :tags, :values, :method, :metrics
+ attr_reader :tags, :method
def self.current
Thread.current[THREAD_KEY]
end
def initialize
- @metrics = []
@methods = {}
@started_at = nil
@finished_at = nil
- @values = Hash.new(0)
@tags = {}
@memory_before = 0
@@ -40,10 +38,6 @@ module Gitlab
@finished_at ? (@finished_at - @started_at) : 0.0
end
- def duration_milliseconds
- duration.in_milliseconds.to_i
- end
-
def thread_cpu_duration
System.thread_cpu_duration(@thread_cputime_start)
end
@@ -55,13 +49,13 @@ module Gitlab
def run
Thread.current[THREAD_KEY] = self
- @memory_before = System.memory_usage
+ @memory_before = System.memory_usage_rss
@started_at = System.monotonic_time
@thread_cputime_start = System.thread_cpu_time
yield
ensure
- @memory_after = System.memory_usage
+ @memory_after = System.memory_usage_rss
@finished_at = System.monotonic_time
self.class.gitlab_transaction_cputime_seconds.observe(labels, thread_cpu_duration)
@@ -71,10 +65,6 @@ module Gitlab
Thread.current[THREAD_KEY] = nil
end
- def add_metric(series, values, tags = {})
- @metrics << Metric.new("#{::Gitlab::Metrics.series_prefix}#{series}", values, filter_tags(tags))
- end
-
# Tracks a business level event
#
# Business level events including events such as Git pushes, Emails being
@@ -85,7 +75,6 @@ module Gitlab
def add_event(event_name, tags = {})
filtered_tags = filter_tags(tags)
self.class.transaction_metric(event_name, :counter, prefix: 'event_', tags: filtered_tags).increment(filtered_tags.merge(labels))
- @metrics << Metric.new(EVENT_SERIES, { count: 1 }, filtered_tags.merge(event: event_name), :event)
end
# Returns a MethodCall object for the given name.
@@ -99,55 +88,16 @@ module Gitlab
def increment(name, value, use_prometheus = true)
self.class.transaction_metric(name, :counter).increment(labels, value) if use_prometheus
- @values[name] += value
end
def set(name, value, use_prometheus = true)
self.class.transaction_metric(name, :gauge).set(labels, value) if use_prometheus
- @values[name] = value
- end
-
- def finish
- track_self
- submit
- end
-
- def track_self
- values = { duration: duration_milliseconds, allocated_memory: allocated_memory }
-
- @values.each do |name, value|
- values[name] = value
- end
-
- add_metric('transactions', values, @tags)
- end
-
- def submit
- submit = @metrics.dup
-
- @methods.each do |name, method|
- submit << method.to_metric if method.above_threshold?
- end
-
- submit_hashes = submit.map do |metric|
- hash = metric.to_hash
- hash[:tags][:action] ||= action if action && !metric.event?
-
- hash
- end
-
- ::Gitlab::Metrics.submit_metrics(submit_hashes)
end
def labels
BASE_LABELS
end
- # returns string describing the action performed, usually the class plus method name.
- def action
- "#{labels[:controller]}##{labels[:action]}" if labels && !labels.empty?
- end
-
define_histogram :gitlab_transaction_cputime_seconds do
docstring 'Transaction thread cputime'
base_labels BASE_LABELS
diff --git a/lib/gitlab/middleware/multipart.rb b/lib/gitlab/middleware/multipart.rb
index 7d0de3aee1c..3c45f841653 100644
--- a/lib/gitlab/middleware/multipart.rb
+++ b/lib/gitlab/middleware/multipart.rb
@@ -110,6 +110,7 @@ module Gitlab
::FileUploader.root,
Gitlab.config.uploads.storage_path,
JobArtifactUploader.workhorse_upload_path,
+ LfsObjectUploader.workhorse_upload_path,
File.join(Rails.root, 'public/uploads/tmp')
]
end
diff --git a/lib/gitlab/middleware/read_only/controller.rb b/lib/gitlab/middleware/read_only/controller.rb
index cdab86540f8..1c49379e8d2 100644
--- a/lib/gitlab/middleware/read_only/controller.rb
+++ b/lib/gitlab/middleware/read_only/controller.rb
@@ -4,8 +4,6 @@ module Gitlab
module Middleware
class ReadOnly
class Controller
- prepend_if_ee('EE::Gitlab::Middleware::ReadOnly::Controller') # rubocop: disable Cop/InjectEnterpriseEditionModule
-
DISALLOWED_METHODS = %w(POST PATCH PUT DELETE).freeze
APPLICATION_JSON = 'application/json'
APPLICATION_JSON_TYPES = %W{#{APPLICATION_JSON} application/vnd.git-lfs+json}.freeze
@@ -144,3 +142,5 @@ module Gitlab
end
end
end
+
+Gitlab::Middleware::ReadOnly::Controller.prepend_if_ee('EE::Gitlab::Middleware::ReadOnly::Controller')
diff --git a/lib/gitlab/omniauth_initializer.rb b/lib/gitlab/omniauth_initializer.rb
index c051a581837..b60ecb6631b 100644
--- a/lib/gitlab/omniauth_initializer.rb
+++ b/lib/gitlab/omniauth_initializer.rb
@@ -2,7 +2,7 @@
module Gitlab
class OmniauthInitializer
- prepend_if_ee('::EE::Gitlab::OmniauthInitializer') # rubocop: disable Cop/InjectEnterpriseEditionModule
+ OAUTH2_TIMEOUT_SECONDS = 10
def initialize(devise_config)
@devise_config = devise_config
@@ -17,6 +17,47 @@ module Gitlab
end
end
+ class << self
+ def default_arguments_for(provider_name)
+ case provider_name
+ when 'cas3'
+ { on_single_sign_out: cas3_signout_handler }
+ when 'authentiq'
+ { remote_sign_out_handler: authentiq_signout_handler }
+ when 'shibboleth'
+ { fail_with_empty_uid: true }
+ when 'google_oauth2'
+ { client_options: { connection_opts: { request: { timeout: OAUTH2_TIMEOUT_SECONDS } } } }
+ else
+ {}
+ end
+ end
+
+ private
+
+ def cas3_signout_handler
+ lambda do |request|
+ ticket = request.params[:session_index]
+ raise "Service Ticket not found." unless Gitlab::Auth::OAuth::Session.valid?(:cas3, ticket)
+
+ Gitlab::Auth::OAuth::Session.destroy(:cas3, ticket)
+ true
+ end
+ end
+
+ def authentiq_signout_handler
+ lambda do |request|
+ authentiq_session = request.params['sid']
+ if Gitlab::Auth::OAuth::Session.valid?(:authentiq, authentiq_session)
+ Gitlab::Auth::OAuth::Session.destroy(:authentiq, authentiq_session)
+ true
+ else
+ false
+ end
+ end
+ end
+ end
+
private
def add_provider_to_devise(*args)
@@ -35,7 +76,8 @@ module Gitlab
# An Array from the configuration will be expanded.
provider_arguments.concat provider['args']
when Hash
- hash_arguments = provider['args'].merge(provider_defaults(provider))
+ defaults = provider_defaults(provider)
+ hash_arguments = provider['args'].deep_symbolize_keys.deep_merge(defaults)
# A Hash from the configuration will be passed as is.
provider_arguments << normalize_hash_arguments(hash_arguments)
@@ -45,7 +87,7 @@ module Gitlab
end
def normalize_hash_arguments(args)
- args.symbolize_keys!
+ args.deep_symbolize_keys!
# Rails 5.1 deprecated the use of string names in the middleware
# (https://github.com/rails/rails/commit/83b767ce), so we need to
@@ -68,38 +110,7 @@ module Gitlab
end
def provider_defaults(provider)
- case provider['name']
- when 'cas3'
- { on_single_sign_out: cas3_signout_handler }
- when 'authentiq'
- { remote_sign_out_handler: authentiq_signout_handler }
- when 'shibboleth'
- { fail_with_empty_uid: true }
- else
- {}
- end
- end
-
- def cas3_signout_handler
- lambda do |request|
- ticket = request.params[:session_index]
- raise "Service Ticket not found." unless Gitlab::Auth::OAuth::Session.valid?(:cas3, ticket)
-
- Gitlab::Auth::OAuth::Session.destroy(:cas3, ticket)
- true
- end
- end
-
- def authentiq_signout_handler
- lambda do |request|
- authentiq_session = request.params['sid']
- if Gitlab::Auth::OAuth::Session.valid?(:authentiq, authentiq_session)
- Gitlab::Auth::OAuth::Session.destroy(:authentiq, authentiq_session)
- true
- else
- false
- end
- end
+ self.class.default_arguments_for(provider['name'])
end
def omniauth_customized_providers
@@ -121,3 +132,5 @@ module Gitlab
end
end
end
+
+Gitlab::OmniauthInitializer.prepend_if_ee('::EE::Gitlab::OmniauthInitializer')
diff --git a/lib/gitlab/pagination/keyset.rb b/lib/gitlab/pagination/keyset.rb
index 8692f30e165..67a5530d46c 100644
--- a/lib/gitlab/pagination/keyset.rb
+++ b/lib/gitlab/pagination/keyset.rb
@@ -3,11 +3,18 @@
module Gitlab
module Pagination
module Keyset
+ SUPPORTED_TYPES = [
+ Project
+ ].freeze
+
+ def self.available_for_type?(relation)
+ SUPPORTED_TYPES.include?(relation.klass)
+ end
+
def self.available?(request_context, relation)
order_by = request_context.page.order_by
- # This is only available for Project and order-by id (asc/desc)
- return false unless relation.klass == Project
+ return false unless available_for_type?(relation)
return false unless order_by.size == 1 && order_by[:id]
true
diff --git a/lib/gitlab/patch/draw_route.rb b/lib/gitlab/patch/draw_route.rb
index 4d1b57fbbbb..f5fcd5c6093 100644
--- a/lib/gitlab/patch/draw_route.rb
+++ b/lib/gitlab/patch/draw_route.rb
@@ -5,8 +5,6 @@
module Gitlab
module Patch
module DrawRoute
- prepend_if_ee('EE::Gitlab::Patch::DrawRoute') # rubocop: disable Cop/InjectEnterpriseEditionModule
-
RoutesNotFound = Class.new(StandardError)
def draw(routes_name)
@@ -38,3 +36,5 @@ module Gitlab
end
end
end
+
+Gitlab::Patch::DrawRoute.prepend_if_ee('EE::Gitlab::Patch::DrawRoute')
diff --git a/lib/gitlab/performance_bar.rb b/lib/gitlab/performance_bar.rb
index 4445c876e7a..e26309b5dfd 100644
--- a/lib/gitlab/performance_bar.rb
+++ b/lib/gitlab/performance_bar.rb
@@ -44,7 +44,7 @@ module Gitlab
end
def self.l1_cache_backend
- Gitlab::ThreadMemoryCache.cache_backend
+ Gitlab::ProcessMemoryCache.cache_backend
end
def self.l2_cache_backend
diff --git a/lib/gitlab/phabricator_import/conduit/response.rb b/lib/gitlab/phabricator_import/conduit/response.rb
index 6053ecfbd5e..1b03cfa05e6 100644
--- a/lib/gitlab/phabricator_import/conduit/response.rb
+++ b/lib/gitlab/phabricator_import/conduit/response.rb
@@ -9,7 +9,7 @@ module Gitlab
"Phabricator responded with #{http_response.status}"
end
- response = new(JSON.parse(http_response.body))
+ response = new(Gitlab::Json.parse(http_response.body))
unless response.success?
raise ResponseError,
diff --git a/lib/gitlab/project_search_results.rb b/lib/gitlab/project_search_results.rb
index eb7ca80dd60..fbdfe166645 100644
--- a/lib/gitlab/project_search_results.rb
+++ b/lib/gitlab/project_search_results.rb
@@ -2,30 +2,29 @@
module Gitlab
class ProjectSearchResults < SearchResults
- attr_reader :project, :repository_ref, :per_page
+ attr_reader :project, :repository_ref
- def initialize(current_user, project, query, repository_ref = nil, per_page: 20)
+ def initialize(current_user, project, query, repository_ref = nil)
@current_user = current_user
@project = project
@repository_ref = repository_ref.presence
@query = query
- @per_page = per_page
end
- def objects(scope, page = nil)
+ def objects(scope, page: nil, per_page: DEFAULT_PER_PAGE)
case scope
when 'notes'
notes.page(page).per(per_page)
when 'blobs'
- paginated_blobs(blobs(page), page)
+ paginated_blobs(blobs(limit: limit_up_to_page(page, per_page)), page, per_page)
when 'wiki_blobs'
- paginated_blobs(wiki_blobs, page)
+ paginated_wiki_blobs(wiki_blobs(limit: limit_up_to_page(page, per_page)), page, per_page)
when 'commits'
Kaminari.paginate_array(commits).page(page).per(per_page)
when 'users'
users.page(page).per(per_page)
else
- super(scope, page, false)
+ super(scope, page: page, per_page: per_page, without_count: false)
end
end
@@ -49,7 +48,7 @@ module Gitlab
end
def limited_blobs_count
- @limited_blobs_count ||= blobs.count
+ @limited_blobs_count ||= blobs(limit: count_limit).count
end
# rubocop: disable CodeReuse/ActiveRecord
@@ -69,7 +68,7 @@ module Gitlab
# rubocop: enable CodeReuse/ActiveRecord
def wiki_blobs_count
- @wiki_blobs_count ||= wiki_blobs.count
+ @wiki_blobs_count ||= wiki_blobs(limit: count_limit).count
end
def commits_count
@@ -87,7 +86,7 @@ module Gitlab
private
- def paginated_blobs(blobs, page)
+ def paginated_blobs(blobs, page, per_page)
results = Kaminari.paginate_array(blobs).page(page).per(per_page)
Gitlab::Search::FoundBlob.preload_blobs(results)
@@ -95,19 +94,26 @@ module Gitlab
results
end
- def limit_up_to_page(page)
+ def paginated_wiki_blobs(blobs, page, per_page)
+ blob_array = paginated_blobs(blobs, page, per_page)
+ blob_array.map! do |blob|
+ Gitlab::Search::FoundWikiPage.new(blob)
+ end
+ end
+
+ def limit_up_to_page(page, per_page)
current_page = page&.to_i || 1
offset = per_page * (current_page - 1)
count_limit + offset
end
- def blobs(page = 1)
+ def blobs(limit: count_limit)
return [] unless Ability.allowed?(@current_user, :download_code, @project)
- @blobs ||= Gitlab::FileFinder.new(project, repository_project_ref).find(query, content_match_cutoff: limit_up_to_page(page))
+ @blobs ||= Gitlab::FileFinder.new(project, repository_project_ref).find(query, content_match_cutoff: limit)
end
- def wiki_blobs
+ def wiki_blobs(limit: count_limit)
return [] unless Ability.allowed?(@current_user, :read_wiki, @project)
@wiki_blobs ||= begin
@@ -115,7 +121,7 @@ module Gitlab
if project.wiki.empty?
[]
else
- Gitlab::WikiFileFinder.new(project, repository_wiki_ref).find(query)
+ Gitlab::WikiFileFinder.new(project, repository_wiki_ref).find(query, content_match_cutoff: limit)
end
else
[]
diff --git a/lib/gitlab/prometheus/metric_group.rb b/lib/gitlab/prometheus/metric_group.rb
index 1b6f7282eb3..4a39260a340 100644
--- a/lib/gitlab/prometheus/metric_group.rb
+++ b/lib/gitlab/prometheus/metric_group.rb
@@ -3,7 +3,6 @@
module Gitlab
module Prometheus
class MetricGroup
- prepend_if_ee('EE::Gitlab::Prometheus::MetricGroup') # rubocop: disable Cop/InjectEnterpriseEditionModule
include ActiveModel::Model
attr_accessor :name, :priority, :metrics
@@ -31,3 +30,5 @@ module Gitlab
end
end
end
+
+Gitlab::Prometheus::MetricGroup.prepend_if_ee('EE::Gitlab::Prometheus::MetricGroup')
diff --git a/lib/gitlab/prometheus/queries/query_additional_metrics.rb b/lib/gitlab/prometheus/queries/query_additional_metrics.rb
index a5e7d0ac9d5..d24b98e790b 100644
--- a/lib/gitlab/prometheus/queries/query_additional_metrics.rb
+++ b/lib/gitlab/prometheus/queries/query_additional_metrics.rb
@@ -4,8 +4,6 @@ module Gitlab
module Prometheus
module Queries
module QueryAdditionalMetrics
- prepend_if_ee('EE::Gitlab::Prometheus::Queries::QueryAdditionalMetrics') # rubocop: disable Cop/InjectEnterpriseEditionModule
-
def query_metrics(project, environment, query_context)
matched_metrics(project).map(&query_group(query_context))
.select(&method(:group_with_any_metrics))
@@ -99,3 +97,5 @@ module Gitlab
end
end
end
+
+Gitlab::Prometheus::Queries::QueryAdditionalMetrics.prepend_if_ee('EE::Gitlab::Prometheus::Queries::QueryAdditionalMetrics')
diff --git a/lib/gitlab/prometheus_client.rb b/lib/gitlab/prometheus_client.rb
index 71a0d528bd7..b03d8a4d254 100644
--- a/lib/gitlab/prometheus_client.rb
+++ b/lib/gitlab/prometheus_client.rb
@@ -163,7 +163,7 @@ module Gitlab
end
def parse_json(response_body)
- JSON.parse(response_body)
+ Gitlab::Json.parse(response_body, legacy_mode: true)
rescue JSON::ParserError
raise PrometheusClient::Error, 'Parsing response failed'
end
diff --git a/lib/gitlab/quick_actions/issuable_actions.rb b/lib/gitlab/quick_actions/issuable_actions.rb
index 942f90e8040..6aa3f515ef0 100644
--- a/lib/gitlab/quick_actions/issuable_actions.rb
+++ b/lib/gitlab/quick_actions/issuable_actions.rb
@@ -109,7 +109,7 @@ module Gitlab
quick_action_target.labels.any? &&
current_user.can?(:"admin_#{quick_action_target.to_ability_name}", parent)
end
- command :unlabel do |labels_param = nil|
+ command :unlabel, :remove_label do |labels_param = nil|
if labels_param.present?
labels = find_labels(labels_param)
label_ids = labels.map(&:id)
diff --git a/lib/gitlab/regex.rb b/lib/gitlab/regex.rb
index db531f06f11..c8b04ce2a5c 100644
--- a/lib/gitlab/regex.rb
+++ b/lib/gitlab/regex.rb
@@ -79,6 +79,12 @@ module Gitlab
"Must start with a letter, and cannot end with '-'"
end
+ # Pod name adheres to DNS Subdomain Names(RFC 1123) naming convention
+ # https://kubernetes.io/docs/concepts/overview/working-with-objects/names/#dns-subdomain-names
+ def kubernetes_dns_subdomain_regex
+ /\A[a-z0-9]([a-z0-9\-\.]*[a-z0-9])?\z/
+ end
+
def environment_slug_regex
@environment_slug_regex ||= /\A[a-z]([a-z0-9-]*[a-z0-9])?\z/.freeze
end
@@ -121,7 +127,7 @@ module Gitlab
# Based on Jira's project key format
# https://confluence.atlassian.com/adminjiraserver073/changing-the-project-key-format-861253229.html
def jira_issue_key_regex
- @jira_issue_key_regex ||= /[A-Z][A-Z_0-9]+-\d+\b/
+ @jira_issue_key_regex ||= /[A-Z][A-Z_0-9]+-\d+/
end
def jira_transition_id_regex
diff --git a/lib/gitlab/request_context.rb b/lib/gitlab/request_context.rb
index 9da6732796a..952ae55d90a 100644
--- a/lib/gitlab/request_context.rb
+++ b/lib/gitlab/request_context.rb
@@ -24,11 +24,12 @@ module Gitlab
end
def ensure_deadline_not_exceeded!
+ return unless enabled?
return unless request_deadline
return if Gitlab::Metrics::System.real_time < request_deadline
raise RequestDeadlineExceeded,
- "Request takes longer than #{max_request_duration_seconds}"
+ "Request takes longer than #{max_request_duration_seconds} seconds"
end
private
@@ -36,5 +37,9 @@ module Gitlab
def max_request_duration_seconds
Settings.gitlab.max_request_duration_seconds
end
+
+ def enabled?
+ !Rails.env.test?
+ end
end
end
diff --git a/lib/gitlab/rugged_instrumentation.rb b/lib/gitlab/rugged_instrumentation.rb
index c2b55431547..9a5917ffba9 100644
--- a/lib/gitlab/rugged_instrumentation.rb
+++ b/lib/gitlab/rugged_instrumentation.rb
@@ -3,7 +3,8 @@
module Gitlab
module RuggedInstrumentation
def self.query_time
- SafeRequestStore[:rugged_query_time] ||= 0
+ query_time = SafeRequestStore[:rugged_query_time] ||= 0
+ query_time.round(Gitlab::InstrumentationHelper::DURATION_PRECISION)
end
def self.query_time=(duration)
diff --git a/lib/gitlab/runtime.rb b/lib/gitlab/runtime.rb
index bf579dd3b77..abf6ee07d53 100644
--- a/lib/gitlab/runtime.rb
+++ b/lib/gitlab/runtime.rb
@@ -37,7 +37,7 @@ module Gitlab
end
def puma?
- !!defined?(::Puma)
+ !!defined?(::Puma) && !defined?(ACTION_CABLE_SERVER)
end
# For unicorn, we need to check for actual server instances to avoid false positives.
@@ -70,25 +70,31 @@ module Gitlab
end
def web_server?
- puma? || unicorn?
+ puma? || unicorn? || action_cable?
+ end
+
+ def action_cable?
+ !!defined?(ACTION_CABLE_SERVER)
end
def multi_threaded?
- puma? || sidekiq?
+ puma? || sidekiq? || action_cable?
end
def max_threads
main_thread = 1
- if puma?
- Puma.cli_config.options[:max_threads] + main_thread
+ if action_cable?
+ Gitlab::Application.config.action_cable.worker_pool_size
+ elsif puma?
+ Puma.cli_config.options[:max_threads]
elsif sidekiq?
# An extra thread for the poller in Sidekiq Cron:
# https://github.com/ondrejbartas/sidekiq-cron#under-the-hood
- Sidekiq.options[:concurrency] + main_thread + 1
+ Sidekiq.options[:concurrency] + 1
else
- main_thread
- end
+ 0
+ end + main_thread
end
end
end
diff --git a/lib/gitlab/sanitizers/exif.rb b/lib/gitlab/sanitizers/exif.rb
index 5eeb8b00ff3..7e22bf4d7df 100644
--- a/lib/gitlab/sanitizers/exif.rb
+++ b/lib/gitlab/sanitizers/exif.rb
@@ -152,7 +152,7 @@ module Gitlab
raise "failed to get exif tags: #{output}" if status != 0
- JSON.parse(output).first
+ Gitlab::Json.parse(output).first
end
end
end
diff --git a/lib/gitlab/search/parsed_query.rb b/lib/gitlab/search/parsed_query.rb
index f3136fff294..1f6e0519b4c 100644
--- a/lib/gitlab/search/parsed_query.rb
+++ b/lib/gitlab/search/parsed_query.rb
@@ -3,8 +3,6 @@
module Gitlab
module Search
class ParsedQuery
- prepend_if_ee('EE::Gitlab::Search::ParsedQuery') # rubocop: disable Cop/InjectEnterpriseEditionModule
-
attr_reader :term, :filters
def initialize(term, filters)
@@ -25,3 +23,5 @@ module Gitlab
end
end
end
+
+Gitlab::Search::ParsedQuery.prepend_if_ee('EE::Gitlab::Search::ParsedQuery')
diff --git a/lib/gitlab/search_results.rb b/lib/gitlab/search_results.rb
index 0473fa89a0d..c35ee62163a 100644
--- a/lib/gitlab/search_results.rb
+++ b/lib/gitlab/search_results.rb
@@ -4,8 +4,10 @@ module Gitlab
class SearchResults
COUNT_LIMIT = 100
COUNT_LIMIT_MESSAGE = "#{COUNT_LIMIT - 1}+"
+ DEFAULT_PAGE = 1
+ DEFAULT_PER_PAGE = 20
- attr_reader :current_user, :query, :per_page
+ attr_reader :current_user, :query
# Limit search results by passed projects
# It allows us to search only for projects user has access to
@@ -17,15 +19,14 @@ module Gitlab
# query
attr_reader :default_project_filter
- def initialize(current_user, limit_projects, query, default_project_filter: false, per_page: 20)
+ def initialize(current_user, limit_projects, query, default_project_filter: false)
@current_user = current_user
@limit_projects = limit_projects || Project.all
@query = query
@default_project_filter = default_project_filter
- @per_page = per_page
end
- def objects(scope, page = nil, without_count = true)
+ def objects(scope, page: nil, per_page: DEFAULT_PER_PAGE, without_count: true)
collection = case scope
when 'projects'
projects
@@ -39,7 +40,9 @@ module Gitlab
users
else
Kaminari.paginate_array([])
- end.page(page).per(per_page)
+ end
+
+ collection = collection.page(page).per(per_page)
without_count ? collection.without_count : collection
end
diff --git a/lib/gitlab/services/logger.rb b/lib/gitlab/services/logger.rb
new file mode 100644
index 00000000000..4e7ef73922c
--- /dev/null
+++ b/lib/gitlab/services/logger.rb
@@ -0,0 +1,11 @@
+# frozen_string_literal: true
+
+module Gitlab
+ module Services
+ class Logger < ::Gitlab::JsonLogger
+ def self.file_name_noext
+ 'service_measurement'
+ end
+ end
+ end
+end
diff --git a/lib/gitlab/setup_helper.rb b/lib/gitlab/setup_helper.rb
index 196dc0e3447..3f36725cb66 100644
--- a/lib/gitlab/setup_helper.rb
+++ b/lib/gitlab/setup_helper.rb
@@ -96,7 +96,8 @@ module Gitlab
class << self
def configuration_toml(gitaly_dir, storage_paths)
nodes = [{ storage: 'default', address: "unix:#{gitaly_dir}/gitaly.socket", primary: true, token: 'secret' }]
- config = { socket_path: "#{gitaly_dir}/praefect.socket", virtual_storage_name: 'default', token: 'secret', node: nodes }
+ storages = [{ name: 'default', node: nodes }]
+ config = { socket_path: "#{gitaly_dir}/praefect.socket", virtual_storage: storages }
config[:token] = 'secret' if Rails.env.test?
TomlRB.dump(config)
diff --git a/lib/gitlab/sidekiq_config/cli_methods.rb b/lib/gitlab/sidekiq_config/cli_methods.rb
index c49432f0fc6..0d0efe8ffbd 100644
--- a/lib/gitlab/sidekiq_config/cli_methods.rb
+++ b/lib/gitlab/sidekiq_config/cli_methods.rb
@@ -48,7 +48,6 @@ module Gitlab
# 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
@@ -75,7 +74,7 @@ module Gitlab
private
def worker_names(workers)
- workers.map { |queue| queue.is_a?(Hash) ? queue[:name] : queue }
+ workers.map { |queue| queue[:name] }
end
def query_string_to_lambda(query_string)
diff --git a/lib/gitlab/sidekiq_daemon/monitor.rb b/lib/gitlab/sidekiq_daemon/monitor.rb
index 0723b514c90..1f1d63877b5 100644
--- a/lib/gitlab/sidekiq_daemon/monitor.rb
+++ b/lib/gitlab/sidekiq_daemon/monitor.rb
@@ -134,7 +134,7 @@ module Gitlab
end
def safe_parse(message)
- JSON.parse(message)
+ Gitlab::Json.parse(message)
rescue JSON::ParserError
end
diff --git a/lib/gitlab/sidekiq_logging/json_formatter.rb b/lib/gitlab/sidekiq_logging/json_formatter.rb
index 45c6842c59b..64782e1e1d1 100644
--- a/lib/gitlab/sidekiq_logging/json_formatter.rb
+++ b/lib/gitlab/sidekiq_logging/json_formatter.rb
@@ -19,6 +19,7 @@ module Gitlab
output[:message] = data
when Hash
convert_to_iso8601!(data)
+ convert_retry_to_integer!(data)
stringify_args!(data)
output.merge!(data)
end
@@ -41,6 +42,20 @@ module Gitlab
Time.at(timestamp).utc.iso8601(3)
end
+ def convert_retry_to_integer!(payload)
+ payload['retry'] =
+ case payload['retry']
+ when Integer
+ payload['retry']
+ when false, nil
+ 0
+ when true
+ Sidekiq::JobRetry::DEFAULT_MAX_RETRY_ATTEMPTS
+ else
+ -1
+ end
+ end
+
def stringify_args!(payload)
payload['args'] = Gitlab::Utils::LogLimitedArray.log_limited_array(payload['args'].map(&:to_s)) if payload['args']
end
diff --git a/lib/gitlab/sidekiq_logging/structured_logger.rb b/lib/gitlab/sidekiq_logging/structured_logger.rb
index ea60190353e..4e39120f8a7 100644
--- a/lib/gitlab/sidekiq_logging/structured_logger.rb
+++ b/lib/gitlab/sidekiq_logging/structured_logger.rb
@@ -30,6 +30,12 @@ module Gitlab
output_payload.merge!(job.slice(*::Gitlab::InstrumentationHelper::KEYS))
end
+ def add_logging_extras!(job, output_payload)
+ output_payload.merge!(
+ job.select { |key, _| key.to_s.start_with?("#{ApplicationWorker::LOGGING_EXTRA_KEY}.") }
+ )
+ end
+
def log_job_start(payload)
payload['message'] = "#{base_message(payload)}: start"
payload['job_status'] = 'start'
@@ -43,6 +49,7 @@ module Gitlab
def log_job_done(job, started_time, payload, job_exception = nil)
payload = payload.dup
add_instrumentation_keys!(job, payload)
+ add_logging_extras!(job, payload)
elapsed_time = elapsed(started_time)
add_time_keys!(elapsed_time, payload)
@@ -66,11 +73,11 @@ module Gitlab
end
def add_time_keys!(time, payload)
- payload['duration_s'] = time[:duration].round(2)
+ payload['duration_s'] = time[:duration].round(Gitlab::InstrumentationHelper::DURATION_PRECISION)
# ignore `cpu_s` if the platform does not support Process::CLOCK_THREAD_CPUTIME_ID (time[:cputime] == 0)
# supported OS version can be found at: https://www.rubydoc.info/stdlib/core/2.1.6/Process:clock_gettime
- payload['cpu_s'] = time[:cputime].round(2) if time[:cputime] > 0
+ payload['cpu_s'] = time[:cputime].round(Gitlab::InstrumentationHelper::DURATION_PRECISION) if time[:cputime] > 0
payload['completed_at'] = Time.now.utc.to_f
end
diff --git a/lib/gitlab/sidekiq_middleware.rb b/lib/gitlab/sidekiq_middleware.rb
index 1b155570f18..4eef3fbd12e 100644
--- a/lib/gitlab/sidekiq_middleware.rb
+++ b/lib/gitlab/sidekiq_middleware.rb
@@ -7,13 +7,14 @@ module Gitlab
# The result of this method should be passed to
# Sidekiq's `config.server_middleware` method
# eg: `config.server_middleware(&Gitlab::SidekiqMiddleware.server_configurator)`
- def self.server_configurator(metrics: true, arguments_logger: true, memory_killer: true, request_store: true)
+ def self.server_configurator(metrics: true, arguments_logger: true, memory_killer: true)
lambda do |chain|
chain.add ::Gitlab::SidekiqMiddleware::Monitor
chain.add ::Gitlab::SidekiqMiddleware::ServerMetrics if metrics
chain.add ::Gitlab::SidekiqMiddleware::ArgumentsLogger if arguments_logger
chain.add ::Gitlab::SidekiqMiddleware::MemoryKiller if memory_killer
- chain.add ::Gitlab::SidekiqMiddleware::RequestStoreMiddleware if request_store
+ chain.add ::Gitlab::SidekiqMiddleware::RequestStoreMiddleware
+ chain.add ::Gitlab::SidekiqMiddleware::ExtraDoneLogMetadata
chain.add ::Gitlab::SidekiqMiddleware::BatchLoader
chain.add ::Labkit::Middleware::Sidekiq::Server
chain.add ::Gitlab::SidekiqMiddleware::InstrumentationLogger
diff --git a/lib/gitlab/sidekiq_middleware/admin_mode/client.rb b/lib/gitlab/sidekiq_middleware/admin_mode/client.rb
index e227ee654ee..36204e1bee0 100644
--- a/lib/gitlab/sidekiq_middleware/admin_mode/client.rb
+++ b/lib/gitlab/sidekiq_middleware/admin_mode/client.rb
@@ -8,7 +8,7 @@ module Gitlab
# 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)
+ 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
@@ -16,7 +16,7 @@ module Gitlab
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}")
+ ::Gitlab::AppLogger.debug("AdminMode::Client injected admin mode for job: #{job.inspect}")
end
yield
@@ -25,8 +25,8 @@ module Gitlab
private
def find_admin_user_id
- Gitlab::Auth::CurrentUserMode.current_admin&.id ||
- Gitlab::Auth::CurrentUserMode.bypass_session_admin_id
+ ::Gitlab::Auth::CurrentUserMode.current_admin&.id ||
+ ::Gitlab::Auth::CurrentUserMode.bypass_session_admin_id
end
end
end
diff --git a/lib/gitlab/sidekiq_middleware/arguments_logger.rb b/lib/gitlab/sidekiq_middleware/arguments_logger.rb
index 2859aa5f4a6..fe5213fc5d7 100644
--- a/lib/gitlab/sidekiq_middleware/arguments_logger.rb
+++ b/lib/gitlab/sidekiq_middleware/arguments_logger.rb
@@ -4,7 +4,7 @@ module Gitlab
module SidekiqMiddleware
class ArgumentsLogger
def call(worker, job, queue)
- Sidekiq.logger.info "arguments: #{JSON.dump(job['args'])}"
+ Sidekiq.logger.info "arguments: #{Gitlab::Json.dump(job['args'])}"
yield
end
end
diff --git a/lib/gitlab/sidekiq_middleware/duplicate_jobs/duplicate_job.rb b/lib/gitlab/sidekiq_middleware/duplicate_jobs/duplicate_job.rb
index 79bbb99752e..fa742d07af2 100644
--- a/lib/gitlab/sidekiq_middleware/duplicate_jobs/duplicate_job.rb
+++ b/lib/gitlab/sidekiq_middleware/duplicate_jobs/duplicate_job.rb
@@ -67,7 +67,7 @@ module Gitlab
end
def droppable?
- idempotent? && duplicate?
+ idempotent? && duplicate? && ::Feature.disabled?("disable_#{queue_name}_deduplication")
end
private
diff --git a/lib/gitlab/sidekiq_middleware/extra_done_log_metadata.rb b/lib/gitlab/sidekiq_middleware/extra_done_log_metadata.rb
new file mode 100644
index 00000000000..93c3131d50e
--- /dev/null
+++ b/lib/gitlab/sidekiq_middleware/extra_done_log_metadata.rb
@@ -0,0 +1,25 @@
+# frozen_string_literal: true
+
+module Gitlab
+ module SidekiqMiddleware
+ class ExtraDoneLogMetadata
+ def call(worker, job, queue)
+ yield
+
+ # We needed a way to pass state from a worker in to the
+ # Gitlab::SidekiqLogging::StructuredLogger . Unfortunately the
+ # StructuredLogger itself is not a middleware so cannot access the
+ # worker object. We also tried to use SafeRequestStore but to pass the
+ # data up but that doesn't work either because this is reset in
+ # Gitlab::SidekiqMiddleware::RequestStoreMiddleware inside yield for
+ # the StructuredLogger so it's cleared before we get to logging the
+ # done statement. As such the only way to do this is to pass the data
+ # up in the `job` object. Since `job` is just a Hash we can add this
+ # extra metadata there.
+ if worker.respond_to?(:logging_extras)
+ job.merge!(worker.logging_extras)
+ end
+ end
+ end
+ end
+end
diff --git a/lib/gitlab/slash_commands/presenters/issue_base.rb b/lib/gitlab/slash_commands/presenters/issue_base.rb
index 4bc05d1f318..017fb8a62c4 100644
--- a/lib/gitlab/slash_commands/presenters/issue_base.rb
+++ b/lib/gitlab/slash_commands/presenters/issue_base.rb
@@ -4,8 +4,6 @@ module Gitlab
module SlashCommands
module Presenters
module IssueBase
- prepend_if_ee('EE::Gitlab::SlashCommands::Presenters::IssueBase') # rubocop: disable Cop/InjectEnterpriseEditionModule
-
def color(issuable)
issuable.open? ? '#38ae67' : '#d22852'
end
@@ -51,3 +49,5 @@ module Gitlab
end
end
end
+
+Gitlab::SlashCommands::Presenters::IssueBase.prepend_if_ee('EE::Gitlab::SlashCommands::Presenters::IssueBase')
diff --git a/lib/gitlab/snippet_search_results.rb b/lib/gitlab/snippet_search_results.rb
index e955ccd35da..9911f9e62a6 100644
--- a/lib/gitlab/snippet_search_results.rb
+++ b/lib/gitlab/snippet_search_results.rb
@@ -11,36 +11,18 @@ module Gitlab
@query = query
end
- def objects(scope, page = nil)
- case scope
- when 'snippet_titles'
- paginated_objects(snippet_titles, page)
- when 'snippet_blobs'
- paginated_objects(snippet_blobs, page)
- else
- super(scope, nil, false)
- end
+ def objects(scope, page: nil, per_page: DEFAULT_PER_PAGE)
+ paginated_objects(snippet_titles, page, per_page)
end
def formatted_count(scope)
- case scope
- when 'snippet_titles'
- formatted_limited_count(limited_snippet_titles_count)
- when 'snippet_blobs'
- formatted_limited_count(limited_snippet_blobs_count)
- else
- super
- end
+ formatted_limited_count(limited_snippet_titles_count)
end
def limited_snippet_titles_count
@limited_snippet_titles_count ||= limited_count(snippet_titles)
end
- def limited_snippet_blobs_count
- @limited_snippet_blobs_count ||= limited_count(snippet_blobs)
- end
-
private
# rubocop: disable CodeReuse/ActiveRecord
@@ -56,15 +38,7 @@ module Gitlab
snippets.search(query)
end
- def snippet_blobs
- snippets.search_code(query)
- end
-
- def default_scope
- 'snippet_blobs'
- end
-
- def paginated_objects(relation, page)
+ def paginated_objects(relation, page, per_page)
relation.page(page).per(per_page)
end
diff --git a/lib/gitlab/static_site_editor/config.rb b/lib/gitlab/static_site_editor/config.rb
index 41d54ee0a92..c931cdecbeb 100644
--- a/lib/gitlab/static_site_editor/config.rb
+++ b/lib/gitlab/static_site_editor/config.rb
@@ -22,7 +22,8 @@ module Gitlab
project: project.path,
namespace: project.namespace.path,
return_url: return_url,
- is_supported_content: supported_content?
+ is_supported_content: supported_content?.to_s,
+ base_url: Gitlab::Routing.url_helpers.project_show_sse_path(project, full_path)
}
end
@@ -47,6 +48,10 @@ module Gitlab
def file_exists?
commit_id.present? && repository.blob_at(commit_id, file_path).present?
end
+
+ def full_path
+ "#{ref}/#{file_path}"
+ end
end
end
end
diff --git a/lib/gitlab/task_helpers.rb b/lib/gitlab/task_helpers.rb
index ac02ec635e4..6ccb442b1e0 100644
--- a/lib/gitlab/task_helpers.rb
+++ b/lib/gitlab/task_helpers.rb
@@ -157,8 +157,8 @@ module Gitlab
Rails.env.test? ? Rails.root.join('tmp/tests') : Gitlab.config.gitlab.user_home
end
- def checkout_or_clone_version(version:, repo:, target_dir:)
- clone_repo(repo, target_dir) unless Dir.exist?(target_dir)
+ def checkout_or_clone_version(version:, repo:, target_dir:, clone_opts: [])
+ clone_repo(repo, target_dir, clone_opts: clone_opts) unless Dir.exist?(target_dir)
checkout_version(get_version(version), target_dir)
end
@@ -171,8 +171,8 @@ module Gitlab
"v#{component_version}"
end
- def clone_repo(repo, target_dir)
- run_command!(%W[#{Gitlab.config.git.bin_path} clone -- #{repo} #{target_dir}])
+ def clone_repo(repo, target_dir, clone_opts: [])
+ run_command!(%W[#{Gitlab.config.git.bin_path} clone] + clone_opts + %W[-- #{repo} #{target_dir}])
end
def checkout_version(version, target_dir)
diff --git a/lib/gitlab/testing/clear_process_memory_cache_middleware.rb b/lib/gitlab/testing/clear_process_memory_cache_middleware.rb
new file mode 100644
index 00000000000..1e69e5e142d
--- /dev/null
+++ b/lib/gitlab/testing/clear_process_memory_cache_middleware.rb
@@ -0,0 +1,17 @@
+# frozen_string_literal: true
+
+module Gitlab
+ module Testing
+ class ClearProcessMemoryCacheMiddleware
+ def initialize(app)
+ @app = app
+ end
+
+ def call(env)
+ Gitlab::ProcessMemoryCache.cache_backend.clear
+
+ @app.call(env)
+ end
+ end
+ end
+end
diff --git a/lib/gitlab/testing/clear_thread_memory_cache_middleware.rb b/lib/gitlab/testing/clear_thread_memory_cache_middleware.rb
deleted file mode 100644
index 6f54038ae22..00000000000
--- a/lib/gitlab/testing/clear_thread_memory_cache_middleware.rb
+++ /dev/null
@@ -1,17 +0,0 @@
-# frozen_string_literal: true
-
-module Gitlab
- module Testing
- class ClearThreadMemoryCacheMiddleware
- def initialize(app)
- @app = app
- end
-
- def call(env)
- Gitlab::ThreadMemoryCache.cache_backend.clear
-
- @app.call(env)
- end
- end
- end
-end
diff --git a/lib/gitlab/thread_memory_cache.rb b/lib/gitlab/thread_memory_cache.rb
deleted file mode 100644
index 7f363dc7feb..00000000000
--- a/lib/gitlab/thread_memory_cache.rb
+++ /dev/null
@@ -1,15 +0,0 @@
-# frozen_string_literal: true
-
-module Gitlab
- class ThreadMemoryCache
- THREAD_KEY = :thread_memory_cache
-
- def self.cache_backend
- # Note ActiveSupport::Cache::MemoryStore is thread-safe. Since
- # each backend is local per thread we probably don't need to worry
- # about synchronizing access, but this is a drop-in replacement
- # for ActiveSupport::Cache::RedisStore.
- Thread.current[THREAD_KEY] ||= ActiveSupport::Cache::MemoryStore.new
- end
- end
-end
diff --git a/lib/gitlab/tracking.rb b/lib/gitlab/tracking.rb
index 9a7b4cc65f6..37688d6e0e7 100644
--- a/lib/gitlab/tracking.rb
+++ b/lib/gitlab/tracking.rb
@@ -9,13 +9,6 @@ module Gitlab
module ControllerConcern
extend ActiveSupport::Concern
- included do
- # Tracking events from the template is not ideal and we are moving this to the client in https://gitlab.com/gitlab-org/gitlab/-/issues/213712
- # In the meantime, using this method from the view is frowned upon and this line will likely be removed
- # in the near future
- helper_method :track_event
- end
-
protected
def track_event(action = action_name, **args)
diff --git a/lib/gitlab/tree_summary.rb b/lib/gitlab/tree_summary.rb
index 5df53b5adde..4ec43e62c19 100644
--- a/lib/gitlab/tree_summary.rb
+++ b/lib/gitlab/tree_summary.rb
@@ -2,21 +2,24 @@
module Gitlab
class TreeSummary
- prepend_if_ee('::EE::Gitlab::TreeSummary') # rubocop: disable Cop/InjectEnterpriseEditionModule
-
include ::Gitlab::Utils::StrongMemoize
+ include ::MarkupHelper
+
+ CACHE_EXPIRE_IN = 1.hour
+ MAX_OFFSET = 2**31
- attr_reader :commit, :project, :path, :offset, :limit
+ attr_reader :commit, :project, :path, :offset, :limit, :user
attr_reader :resolved_commits
private :resolved_commits
- def initialize(commit, project, params = {})
+ def initialize(commit, project, user, params = {})
@commit = commit
@project = project
+ @user = user
@path = params.fetch(:path, nil).presence
- @offset = params.fetch(:offset, 0).to_i
+ @offset = [params.fetch(:offset, 0).to_i, MAX_OFFSET].min
@limit = (params.fetch(:limit, 25) || 25).to_i
# Ensure that if multiple tree entries share the same last commit, they share
@@ -43,6 +46,17 @@ module Gitlab
[summary, commits]
end
+ def fetch_logs
+ cache_key = ['projects', project.id, 'logs', commit.id, path, offset]
+ Rails.cache.fetch(cache_key, expires_in: CACHE_EXPIRE_IN) do
+ logs, _ = summarize
+
+ new_offset = next_offset if more?
+
+ [logs.as_json, new_offset]
+ end
+ end
+
# Does the tree contain more entries after the given offset + limit?
def more?
all_contents[next_offset].present?
@@ -84,6 +98,7 @@ module Gitlab
end
commits_hsh = repository.list_last_commits_for_tree(commit.id, ensured_path, offset: offset, limit: limit)
+ prerender_commit_full_titles!(commits_hsh.values)
entries.each do |entry|
path_key = entry_path(entry)
@@ -92,6 +107,7 @@ module Gitlab
if commit
entry[:commit] = commit
entry[:commit_path] = commit_path(commit)
+ entry[:commit_title_html] = markdown_field(commit, :full_title)
end
end
end
@@ -119,5 +135,15 @@ module Gitlab
def tree
strong_memoize(:tree) { repository.tree(commit.id, path) }
end
+
+ def prerender_commit_full_titles!(commits)
+ # Preload commit authors as they are used in rendering
+ commits.each(&:lazy_author)
+
+ renderer = Banzai::ObjectRenderer.new(user: user, default_project: project)
+ renderer.render(commits, :full_title)
+ end
end
end
+
+Gitlab::TreeSummary.prepend_if_ee('::EE::Gitlab::TreeSummary')
diff --git a/lib/gitlab/uploads/migration_helper.rb b/lib/gitlab/uploads/migration_helper.rb
index 96ee6f0e8e6..9377ccfec1e 100644
--- a/lib/gitlab/uploads/migration_helper.rb
+++ b/lib/gitlab/uploads/migration_helper.rb
@@ -15,6 +15,7 @@ module Gitlab
%w(FileUploader Project),
%w(PersonalFileUploader Snippet),
%w(NamespaceFileUploader Snippet),
+ %w(DesignManagement::DesignV432x230Uploader DesignManagement::Action :image_v432x230),
%w(FileUploader MergeRequest)].freeze
def initialize(args, logger)
@@ -74,5 +75,3 @@ module Gitlab
end
end
end
-
-Gitlab::Uploads::MigrationHelper.prepend_if_ee('EE::Gitlab::Uploads::MigrationHelper')
diff --git a/lib/gitlab/url_builder.rb b/lib/gitlab/url_builder.rb
index 4c56b9bb3c9..329f87d8be8 100644
--- a/lib/gitlab/url_builder.rb
+++ b/lib/gitlab/url_builder.rb
@@ -11,6 +11,10 @@ module Gitlab
class << self
include ActionView::RecordIdentifier
+ # Using a case statement here is preferable for readability and maintainability.
+ # See discussion in https://gitlab.com/gitlab-org/gitlab/-/issues/217397
+ #
+ # rubocop:disable Metrics/CyclomaticComplexity
def build(object, **options)
# Objects are sometimes wrapped in a BatchLoader instance
case object.itself
@@ -34,14 +38,17 @@ module Gitlab
snippet_url(object, **options)
when User
instance.user_url(object, **options)
- when ProjectWiki
- instance.project_wiki_url(object.project, :home, **options)
+ when Wiki
+ wiki_url(object, **options)
when WikiPage
instance.project_wiki_url(object.wiki.project, object.slug, **options)
+ when ::DesignManagement::Design
+ design_url(object, **options)
else
raise NotImplementedError.new("No URL builder defined for #{object.inspect}")
end
end
+ # rubocop:enable Metrics/CyclomaticComplexity
def commit_url(commit, **options)
return '' unless commit.project
@@ -70,6 +77,25 @@ module Gitlab
instance.gitlab_snippet_url(snippet, **options)
end
end
+
+ def wiki_url(object, **options)
+ if object.container.is_a?(Project)
+ instance.project_wiki_url(object.container, Wiki::HOMEPAGE, **options)
+ else
+ raise NotImplementedError.new("No URL builder defined for #{object.inspect}")
+ end
+ end
+
+ def design_url(design, **options)
+ size, ref = options.values_at(:size, :ref)
+ options.except!(:size, :ref)
+
+ if size
+ instance.project_design_management_designs_resized_image_url(design.project, design, ref, size, **options)
+ else
+ instance.project_design_management_designs_raw_image_url(design.project, design, ref, **options)
+ end
+ end
end
end
end
diff --git a/lib/gitlab/usage_data.rb b/lib/gitlab/usage_data.rb
index f8ee0ca6877..e60c786b52c 100644
--- a/lib/gitlab/usage_data.rb
+++ b/lib/gitlab/usage_data.rb
@@ -1,15 +1,25 @@
# frozen_string_literal: true
-# For hardening usage ping and make it easier to add measures there is in place alt_usage_data method
-# which handles StandardError and fallbacks into -1
-# this way not all measures fail if we encounter one exception
+# For hardening usage ping and make it easier to add measures there is in place
+# * alt_usage_data method
+# handles StandardError and fallbacks into -1 this way not all measures fail if we encounter one exception
#
-# Examples:
-# alt_usage_data { Gitlab::VERSION }
-# alt_usage_data { Gitlab::CurrentSettings.uuid }
+# Examples:
+# alt_usage_data { Gitlab::VERSION }
+# alt_usage_data { Gitlab::CurrentSettings.uuid }
+#
+# * redis_usage_data method
+# handles ::Redis::CommandError, Gitlab::UsageDataCounters::BaseCounter::UnknownEvent
+# returns -1 when a block is sent or hash with all values -1 when a counter is sent
+# different behaviour due to 2 different implementations of redis counter
+#
+# Examples:
+# redis_usage_data(Gitlab::UsageDataCounters::WikiPageCounter)
+# redis_usage_data { ::Gitlab::UsageCounters::PodLogs.usage_totals[:total] }
module Gitlab
class UsageData
BATCH_SIZE = 100
+ FALLBACK = -1
class << self
def data(force_refresh: false)
@@ -24,6 +34,8 @@ module Gitlab
.merge(features_usage_data)
.merge(components_usage_data)
.merge(cycle_analytics_usage_data)
+ .merge(object_store_usage_data)
+ .merge(recording_ce_finish_data)
end
def to_json(force_refresh: false)
@@ -32,19 +44,27 @@ module Gitlab
def license_usage_data
{
+ recorded_at: Time.now, # should be calculated very first
uuid: alt_usage_data { Gitlab::CurrentSettings.uuid },
hostname: alt_usage_data { Gitlab.config.gitlab.host },
version: alt_usage_data { Gitlab::VERSION },
installation_type: alt_usage_data { installation_type },
active_user_count: count(User.active),
- recorded_at: Time.now,
edition: 'CE'
}
end
+ def recording_ce_finish_data
+ {
+ recording_ce_finished_at: Time.now
+ }
+ end
+
# rubocop: disable Metrics/AbcSize
# rubocop: disable CodeReuse/ActiveRecord
def system_usage_data
+ alert_bot_incident_count = count(::Issue.authored(::User.alert_bot))
+
{
counts: {
assignee_lists: count(List.assignee),
@@ -94,7 +114,10 @@ module Gitlab
issues_with_associated_zoom_link: count(ZoomMeeting.added_to_issue),
issues_using_zoom_quick_actions: distinct_count(ZoomMeeting, :issue_id),
issues_with_embedded_grafana_charts_approx: grafana_embed_usage_data,
- incident_issues: count(::Issue.authored(::User.alert_bot)),
+ issues_created_gitlab_alerts: count(Issue.with_alert_management_alerts.not_authored_by(::User.alert_bot)),
+ incident_issues: alert_bot_incident_count,
+ alert_bot_incident_issues: alert_bot_incident_count,
+ incident_labeled_issues: count(::Issue.with_label_attributes(IncidentManagement::CreateIssueService::INCIDENT_LABEL)),
keys: count(Key),
label_lists: count(List.label),
lfs_objects: count(LfsObject),
@@ -123,7 +146,8 @@ module Gitlab
services_usage,
usage_counters,
user_preferences_usage,
- ingress_modsecurity_usage
+ ingress_modsecurity_usage,
+ container_expiration_policies_usage
)
}
end
@@ -154,7 +178,6 @@ module Gitlab
dependency_proxy_enabled: Gitlab.config.try(:dependency_proxy)&.enabled,
gitlab_shared_runners_enabled: alt_usage_data { Gitlab.config.gitlab_ci.shared_runners_enabled },
gravatar_enabled: alt_usage_data { Gitlab::CurrentSettings.gravatar_enabled? },
- influxdb_metrics_enabled: alt_usage_data { Gitlab::Metrics.influx_metrics_enabled? },
ldap_enabled: alt_usage_data { Gitlab.config.ldap.enabled },
mattermost_enabled: alt_usage_data { Gitlab.config.mattermost.enabled },
omniauth_enabled: alt_usage_data { Gitlab::Auth.omniauth_enabled? },
@@ -162,36 +185,14 @@ module Gitlab
reply_by_email_enabled: alt_usage_data { Gitlab::IncomingEmail.enabled? },
signup_enabled: alt_usage_data { Gitlab::CurrentSettings.allow_signup? },
web_ide_clientside_preview_enabled: alt_usage_data { Gitlab::CurrentSettings.web_ide_clientside_preview_enabled? },
- ingress_modsecurity_enabled: Feature.enabled?(:ingress_modsecurity)
- }.merge(features_usage_data_container_expiration_policies)
- end
-
- # rubocop: disable CodeReuse/ActiveRecord
- def features_usage_data_container_expiration_policies
- results = {}
- start = ::Project.minimum(:id)
- finish = ::Project.maximum(:id)
-
- results[:projects_with_expiration_policy_disabled] = distinct_count(::ContainerExpirationPolicy.where(enabled: false), :project_id, start: start, finish: finish)
- base = ::ContainerExpirationPolicy.active
- results[:projects_with_expiration_policy_enabled] = distinct_count(base, :project_id, start: start, finish: finish)
-
- %i[keep_n cadence older_than].each do |option|
- ::ContainerExpirationPolicy.public_send("#{option}_options").keys.each do |value| # rubocop: disable GitlabSecurity/PublicSend
- results["projects_with_expiration_policy_enabled_with_#{option}_set_to_#{value}".to_sym] = distinct_count(base.where(option => value), :project_id, start: start, finish: finish)
- end
- end
-
- results[:projects_with_expiration_policy_enabled_with_keep_n_unset] = distinct_count(base.where(keep_n: nil), :project_id, start: start, finish: finish)
- results[:projects_with_expiration_policy_enabled_with_older_than_unset] = distinct_count(base.where(older_than: nil), :project_id, start: start, finish: finish)
-
- results
+ ingress_modsecurity_enabled: Feature.enabled?(:ingress_modsecurity),
+ grafana_link_enabled: alt_usage_data { Gitlab::CurrentSettings.grafana_enabled? }
+ }
end
- # rubocop: enable CodeReuse/ActiveRecord
# @return [Hash<Symbol, Integer>]
def usage_counters
- usage_data_counters.map(&:totals).reduce({}) { |a, b| a.merge(b) }
+ usage_data_counters.map { |counter| redis_usage_data(counter) }.reduce({}, :merge)
end
# @return [Array<#totals>] An array of objects that respond to `#totals`
@@ -205,7 +206,8 @@ module Gitlab
Gitlab::UsageDataCounters::CycleAnalyticsCounter,
Gitlab::UsageDataCounters::ProductivityAnalyticsCounter,
Gitlab::UsageDataCounters::SourceCodeCounter,
- Gitlab::UsageDataCounters::MergeRequestCounter
+ Gitlab::UsageDataCounters::MergeRequestCounter,
+ Gitlab::UsageDataCounters::DesignsCounter
]
end
@@ -237,9 +239,83 @@ module Gitlab
'unknown_app_server_type'
end
+ def object_store_config(component)
+ config = alt_usage_data(fallback: nil) do
+ Settings[component]['object_store']
+ end
+
+ if config
+ {
+ enabled: alt_usage_data { Settings[component]['enabled'] },
+ object_store: {
+ enabled: alt_usage_data { config['enabled'] },
+ direct_upload: alt_usage_data { config['direct_upload'] },
+ background_upload: alt_usage_data { config['background_upload'] },
+ provider: alt_usage_data { config['connection']['provider'] }
+ }
+ }
+ else
+ {
+ enabled: alt_usage_data { Settings[component]['enabled'] }
+ }
+ end
+ end
+
+ def object_store_usage_data
+ {
+ object_store: {
+ artifacts: object_store_config('artifacts'),
+ external_diffs: object_store_config('external_diffs'),
+ lfs: object_store_config('lfs'),
+ uploads: object_store_config('uploads'),
+ packages: object_store_config('packages')
+ }
+ }
+ end
+
def ingress_modsecurity_usage
- ::Clusters::Applications::IngressModsecurityUsageService.new.execute
+ ##
+ # This method measures usage of the Modsecurity Web Application Firewall across the entire
+ # instance's deployed environments.
+ #
+ # NOTE: this service is an approximation as it does not yet take into account if environment
+ # is enabled and only measures applications installed using GitLab Managed Apps (disregards
+ # CI-based managed apps).
+ #
+ # More details: https://gitlab.com/gitlab-org/gitlab/-/merge_requests/28331#note_318621786
+ ##
+
+ column = ::Deployment.arel_table[:environment_id]
+ {
+ ingress_modsecurity_logging: distinct_count(successful_deployments_with_cluster(::Clusters::Applications::Ingress.modsecurity_enabled.logging), column),
+ ingress_modsecurity_blocking: distinct_count(successful_deployments_with_cluster(::Clusters::Applications::Ingress.modsecurity_enabled.blocking), column),
+ ingress_modsecurity_disabled: distinct_count(successful_deployments_with_cluster(::Clusters::Applications::Ingress.modsecurity_disabled), column),
+ ingress_modsecurity_not_installed: distinct_count(successful_deployments_with_cluster(::Clusters::Applications::Ingress.modsecurity_not_installed), column)
+ }
+ end
+
+ # rubocop: disable CodeReuse/ActiveRecord
+ def container_expiration_policies_usage
+ results = {}
+ start = ::Project.minimum(:id)
+ finish = ::Project.maximum(:id)
+
+ results[:projects_with_expiration_policy_disabled] = distinct_count(::ContainerExpirationPolicy.where(enabled: false), :project_id, start: start, finish: finish)
+ base = ::ContainerExpirationPolicy.active
+ results[:projects_with_expiration_policy_enabled] = distinct_count(base, :project_id, start: start, finish: finish)
+
+ %i[keep_n cadence older_than].each do |option|
+ ::ContainerExpirationPolicy.public_send("#{option}_options").keys.each do |value| # rubocop: disable GitlabSecurity/PublicSend
+ results["projects_with_expiration_policy_enabled_with_#{option}_set_to_#{value}".to_sym] = distinct_count(base.where(option => value), :project_id, start: start, finish: finish)
+ end
+ end
+
+ results[:projects_with_expiration_policy_enabled_with_keep_n_unset] = distinct_count(base.where(keep_n: nil), :project_id, start: start, finish: finish)
+ results[:projects_with_expiration_policy_enabled_with_older_than_unset] = distinct_count(base.where(older_than: nil), :project_id, start: start, finish: finish)
+
+ results
end
+ # rubocop: enable CodeReuse/ActiveRecord
# rubocop: disable CodeReuse/ActiveRecord
def services_usage
@@ -251,7 +327,7 @@ module Gitlab
results[:projects_slack_notifications_active] = results[:projects_slack_active]
results[:projects_slack_slash_active] = results[:projects_slack_slash_commands_active]
- results.merge(jira_usage)
+ results.merge(jira_usage).merge(jira_import_usage)
end
def jira_usage
@@ -281,35 +357,52 @@ module Gitlab
results
rescue ActiveRecord::StatementInvalid
- { projects_jira_server_active: -1, projects_jira_cloud_active: -1, projects_jira_active: -1 }
+ { projects_jira_server_active: FALLBACK, projects_jira_cloud_active: FALLBACK, projects_jira_active: FALLBACK }
+ end
+
+ def successful_deployments_with_cluster(scope)
+ scope
+ .joins(cluster: :deployments)
+ .merge(Clusters::Cluster.enabled)
+ .merge(Deployment.success)
end
# rubocop: enable CodeReuse/ActiveRecord
+ def jira_import_usage
+ finished_jira_imports = JiraImportState.finished
+
+ {
+ jira_imports_total_imported_count: count(finished_jira_imports),
+ jira_imports_projects_count: distinct_count(finished_jira_imports, :project_id),
+ jira_imports_total_imported_issues_count: alt_usage_data { JiraImportState.finished_imports_count }
+ }
+ end
+
def user_preferences_usage
{} # augmented in EE
end
- def count(relation, column = nil, fallback: -1, batch: true, start: nil, finish: nil)
+ def count(relation, column = nil, batch: true, start: nil, finish: nil)
if batch && Feature.enabled?(:usage_ping_batch_counter, default_enabled: true)
Gitlab::Database::BatchCount.batch_count(relation, column, start: start, finish: finish)
else
relation.count
end
rescue ActiveRecord::StatementInvalid
- fallback
+ FALLBACK
end
- def distinct_count(relation, column = nil, fallback: -1, batch: true, start: nil, finish: nil)
+ def distinct_count(relation, column = nil, batch: true, start: nil, finish: nil)
if batch && Feature.enabled?(:usage_ping_batch_counter, default_enabled: true)
Gitlab::Database::BatchCount.batch_distinct_count(relation, column, start: start, finish: finish)
else
relation.distinct_count_by(column)
end
rescue ActiveRecord::StatementInvalid
- fallback
+ FALLBACK
end
- def alt_usage_data(value = nil, fallback: -1, &block)
+ def alt_usage_data(value = nil, fallback: FALLBACK, &block)
if block_given?
yield
else
@@ -319,8 +412,28 @@ module Gitlab
fallback
end
+ def redis_usage_data(counter = nil, &block)
+ if block_given?
+ redis_usage_counter(&block)
+ elsif counter.present?
+ redis_usage_data_totals(counter)
+ end
+ end
+
private
+ def redis_usage_counter
+ yield
+ rescue ::Redis::CommandError, Gitlab::UsageDataCounters::BaseCounter::UnknownEvent
+ FALLBACK
+ end
+
+ def redis_usage_data_totals(counter)
+ counter.totals
+ rescue ::Redis::CommandError, Gitlab::UsageDataCounters::BaseCounter::UnknownEvent
+ counter.fallback_totals
+ end
+
def installation_type
if Rails.env.production?
Gitlab::INSTALLATION_TYPE
diff --git a/lib/gitlab/usage_data_counters/base_counter.rb b/lib/gitlab/usage_data_counters/base_counter.rb
index 33111b46381..96898e5189c 100644
--- a/lib/gitlab/usage_data_counters/base_counter.rb
+++ b/lib/gitlab/usage_data_counters/base_counter.rb
@@ -22,11 +22,19 @@ module Gitlab::UsageDataCounters
end
def totals
- known_events.map { |e| ["#{prefix}_#{e}".to_sym, read(e)] }.to_h
+ known_events.map { |event| [counter_key(event), read(event)] }.to_h
+ end
+
+ def fallback_totals
+ known_events.map { |event| [counter_key(event), -1] }.to_h
end
private
+ def counter_key(event)
+ "#{prefix}_#{event}".to_sym
+ end
+
def known_events
self::KNOWN_EVENTS
end
diff --git a/lib/gitlab/usage_data_counters/designs_counter.rb b/lib/gitlab/usage_data_counters/designs_counter.rb
new file mode 100644
index 00000000000..801fb8f3b3d
--- /dev/null
+++ b/lib/gitlab/usage_data_counters/designs_counter.rb
@@ -0,0 +1,42 @@
+# frozen_string_literal: true
+
+module Gitlab::UsageDataCounters
+ class DesignsCounter
+ extend Gitlab::UsageDataCounters::RedisCounter
+
+ KNOWN_EVENTS = %w[create update delete].map(&:freeze).freeze
+
+ UnknownEvent = Class.new(StandardError)
+
+ class << self
+ # Each event gets a unique Redis key
+ def redis_key(event)
+ raise UnknownEvent, event unless KNOWN_EVENTS.include?(event.to_s)
+
+ "USAGE_DESIGN_MANAGEMENT_DESIGNS_#{event}".upcase
+ end
+
+ def count(event)
+ increment(redis_key(event))
+ end
+
+ def read(event)
+ total_count(redis_key(event))
+ end
+
+ def totals
+ KNOWN_EVENTS.map { |event| [counter_key(event), read(event)] }.to_h
+ end
+
+ def fallback_totals
+ KNOWN_EVENTS.map { |event| [counter_key(event), -1] }.to_h
+ end
+
+ private
+
+ def counter_key(event)
+ "design_management_designs_#{event}".to_sym
+ end
+ end
+ end
+end
diff --git a/lib/gitlab/usage_data_counters/note_counter.rb b/lib/gitlab/usage_data_counters/note_counter.rb
index 672450ec82b..7a76180cb08 100644
--- a/lib/gitlab/usage_data_counters/note_counter.rb
+++ b/lib/gitlab/usage_data_counters/note_counter.rb
@@ -25,12 +25,20 @@ module Gitlab::UsageDataCounters
def totals
COUNTABLE_TYPES.map do |countable_type|
- [:"#{countable_type.underscore}_comment", read(:create, countable_type)]
+ [counter_key(countable_type), read(:create, countable_type)]
end.to_h
end
+ def fallback_totals
+ COUNTABLE_TYPES.map { |counter_key| [counter_key(counter_key), -1] }.to_h
+ end
+
private
+ def counter_key(countable_type)
+ "#{countable_type.underscore}_comment".to_sym
+ end
+
def countable?(noteable_type)
COUNTABLE_TYPES.include?(noteable_type.to_s)
end
diff --git a/lib/gitlab/usage_data_counters/search_counter.rb b/lib/gitlab/usage_data_counters/search_counter.rb
index 5f0735347e1..b9e3a5c0104 100644
--- a/lib/gitlab/usage_data_counters/search_counter.rb
+++ b/lib/gitlab/usage_data_counters/search_counter.rb
@@ -21,6 +21,10 @@ module Gitlab
navbar_searches: total_navbar_searches_count
}
end
+
+ def fallback_totals
+ { navbar_searches: -1 }
+ end
end
end
end
diff --git a/lib/gitlab/usage_data_counters/web_ide_counter.rb b/lib/gitlab/usage_data_counters/web_ide_counter.rb
index c012a6c96df..00fcd42a9af 100644
--- a/lib/gitlab/usage_data_counters/web_ide_counter.rb
+++ b/lib/gitlab/usage_data_counters/web_ide_counter.rb
@@ -4,54 +4,52 @@ module Gitlab
module UsageDataCounters
class WebIdeCounter
extend RedisCounter
-
- COMMITS_COUNT_KEY = 'WEB_IDE_COMMITS_COUNT'
- MERGE_REQUEST_COUNT_KEY = 'WEB_IDE_MERGE_REQUESTS_COUNT'
- VIEWS_COUNT_KEY = 'WEB_IDE_VIEWS_COUNT'
- PREVIEW_COUNT_KEY = 'WEB_IDE_PREVIEWS_COUNT'
+ KNOWN_EVENTS = %i[commits views merge_requests previews terminals pipelines].freeze
+ PREFIX = 'web_ide'
class << self
def increment_commits_count
- increment(COMMITS_COUNT_KEY)
- end
-
- def total_commits_count
- total_count(COMMITS_COUNT_KEY)
+ increment(redis_key('commits'))
end
def increment_merge_requests_count
- increment(MERGE_REQUEST_COUNT_KEY)
+ increment(redis_key('merge_requests'))
end
- def total_merge_requests_count
- total_count(MERGE_REQUEST_COUNT_KEY)
+ def increment_views_count
+ increment(redis_key('views'))
end
- def increment_views_count
- increment(VIEWS_COUNT_KEY)
+ def increment_terminals_count
+ increment(redis_key('terminals'))
end
- def total_views_count
- total_count(VIEWS_COUNT_KEY)
+ def increment_pipelines_count
+ increment(redis_key('pipelines'))
end
def increment_previews_count
return unless Gitlab::CurrentSettings.web_ide_clientside_preview_enabled?
- increment(PREVIEW_COUNT_KEY)
+ increment(redis_key('previews'))
+ end
+
+ def totals
+ KNOWN_EVENTS.map { |event| [counter_key(event), total_count(redis_key(event))] }.to_h
end
- def total_previews_count
- total_count(PREVIEW_COUNT_KEY)
+ def fallback_totals
+ KNOWN_EVENTS.map { |event| [counter_key(event), -1] }.to_h
end
- def totals
- {
- web_ide_commits: total_commits_count,
- web_ide_views: total_views_count,
- web_ide_merge_requests: total_merge_requests_count,
- web_ide_previews: total_previews_count
- }
+ private
+
+ def redis_key(event)
+ "#{PREFIX}_#{event}_count".upcase
+ end
+
+ def counter_key(event)
+ "#{PREFIX}_#{event}".to_sym
end
end
end
diff --git a/lib/gitlab/user_access_snippet.rb b/lib/gitlab/user_access_snippet.rb
index bfed86c4df4..dcd45f9350d 100644
--- a/lib/gitlab/user_access_snippet.rb
+++ b/lib/gitlab/user_access_snippet.rb
@@ -17,7 +17,14 @@ module Gitlab
@project = snippet&.project
end
+ def allowed?
+ return true if snippet_migration?
+
+ super
+ end
+
def can_do_action?(action)
+ return true if snippet_migration?
return false unless can_access_git?
permission_cache[action] =
@@ -35,7 +42,10 @@ module Gitlab
end
def can_push_to_branch?(ref)
+ return true if snippet_migration?
+
super
+
return false unless snippet
return false unless can_do_action?(:update_snippet)
@@ -45,5 +55,9 @@ module Gitlab
def can_merge_to_branch?(ref)
false
end
+
+ def snippet_migration?
+ user&.migration_bot? && snippet
+ end
end
end
diff --git a/lib/gitlab/utils.rb b/lib/gitlab/utils.rb
index 2e8a3ca4242..d46601fa2e8 100644
--- a/lib/gitlab/utils.rb
+++ b/lib/gitlab/utils.rb
@@ -75,12 +75,12 @@ module Gitlab
str.gsub(/\r?\n/, '')
end
- def to_boolean(value)
+ def to_boolean(value, default: nil)
return value if [true, false].include?(value)
return true if value =~ /^(true|t|yes|y|1|on)$/i
return false if value =~ /^(false|f|no|n|0|off)$/i
- nil
+ default
end
def boolean_to_yes_no(bool)
@@ -123,7 +123,7 @@ module Gitlab
end
def ms_to_round_sec(ms)
- (ms.to_f / 1000).round(2)
+ (ms.to_f / 1000).round(6)
end
# Used in EE
diff --git a/lib/gitlab/utils/measuring.rb b/lib/gitlab/utils/measuring.rb
index 0680cefd249..febe489f1f8 100644
--- a/lib/gitlab/utils/measuring.rb
+++ b/lib/gitlab/utils/measuring.rb
@@ -5,38 +5,51 @@ require 'prometheus/pid_provider'
module Gitlab
module Utils
class Measuring
- def initialize(logger: Logger.new($stdout))
- @logger = logger
+ class << self
+ attr_writer :logger
+
+ def logger
+ @logger ||= Logger.new(STDOUT)
+ end
+ end
+
+ def initialize(base_log_data = {})
+ @base_log_data = base_log_data
end
def with_measuring
- logger.info "Measuring enabled..."
+ result = nil
with_gc_stats do
with_count_queries do
with_measure_time do
- yield
+ result = yield
end
end
end
- logger.info "Memory usage: #{Gitlab::Metrics::System.memory_usage.to_f / 1024 / 1024} MiB"
- logger.info "Label: #{::Prometheus::PidProvider.worker_id}"
+ log_info(
+ gc_stats: gc_stats,
+ time_to_finish: time_to_finish,
+ number_of_sql_calls: sql_calls_count,
+ memory_usage: "#{Gitlab::Metrics::System.memory_usage_rss.to_f / 1024 / 1024} MiB",
+ label: ::Prometheus::PidProvider.worker_id
+ )
+
+ result
end
private
- attr_reader :logger
+ attr_reader :gc_stats, :time_to_finish, :sql_calls_count, :base_log_data
def with_count_queries(&block)
- count = 0
+ @sql_calls_count = 0
counter_f = ->(_name, _started, _finished, _unique_id, payload) {
- count += 1 unless payload[:name].in? %w[CACHE SCHEMA]
+ @sql_calls_count += 1 unless payload[:name].in? %w[CACHE SCHEMA]
}
ActiveSupport::Notifications.subscribed(counter_f, "sql.active_record", &block)
-
- logger.info "Number of sql calls: #{count}"
end
def with_gc_stats
@@ -44,33 +57,22 @@ module Gitlab
stats_before = GC.stat
yield
stats_after = GC.stat
- stats_diff = stats_after.map do |key, after_value|
+ @gc_stats = stats_after.map do |key, after_value|
before_value = stats_before[key]
[key, before: before_value, after: after_value, diff: after_value - before_value]
end.to_h
- logger.info "GC stats:"
- logger.info JSON.pretty_generate(stats_diff)
end
def with_measure_time
- timing = Benchmark.realtime do
+ @time_to_finish = Benchmark.realtime do
yield
end
-
- logger.info "Time to finish: #{duration_in_numbers(timing)}"
end
- def duration_in_numbers(duration_in_seconds)
- milliseconds = duration_in_seconds.in_milliseconds % 1.second.in_milliseconds
- seconds = duration_in_seconds % 1.minute
- minutes = (duration_in_seconds / 1.minute) % (1.hour / 1.minute)
- hours = duration_in_seconds / 1.hour
-
- if hours == 0
- "%02d:%02d:%03d" % [minutes, seconds, milliseconds]
- else
- "%02d:%02d:%02d:%03d" % [hours, minutes, seconds, milliseconds]
- end
+ def log_info(details)
+ details = base_log_data.merge(details)
+ details = details.to_yaml if ActiveSupport::Logger.logger_outputs_to?(Measuring.logger, STDOUT)
+ Measuring.logger.info(details)
end
end
end
diff --git a/lib/gitlab/wiki_pages.rb b/lib/gitlab/wiki_pages.rb
index 47f9aa1117f..dee885e74d1 100644
--- a/lib/gitlab/wiki_pages.rb
+++ b/lib/gitlab/wiki_pages.rb
@@ -11,5 +11,8 @@ module Gitlab
# through the GitLab web interface and API:
MAX_TITLE_BYTES = 245 # reserving 10 bytes for the file extension
MAX_DIRECTORY_BYTES = 255
+
+ # Limit the number of pages displayed in the wiki sidebar.
+ MAX_SIDEBAR_PAGES = 15
end
end
diff --git a/lib/gitlab/with_request_store.rb b/lib/gitlab/with_request_store.rb
index d6c05e1e256..d13cd9a72f7 100644
--- a/lib/gitlab/with_request_store.rb
+++ b/lib/gitlab/with_request_store.rb
@@ -2,12 +2,24 @@
module Gitlab
module WithRequestStore
- def with_request_store
+ def with_request_store(&block)
+ # Skip enabling the request store if it was already active. Whatever
+ # instantiated the request store first is responsible for clearing it
+ return yield if RequestStore.active?
+
+ enabling_request_store(&block)
+ end
+
+ private
+
+ def enabling_request_store
RequestStore.begin!
yield
ensure
RequestStore.end!
RequestStore.clear!
end
+
+ extend self
end
end
diff --git a/lib/gitlab/workhorse.rb b/lib/gitlab/workhorse.rb
index b375602a5fe..c91d1b05440 100644
--- a/lib/gitlab/workhorse.rb
+++ b/lib/gitlab/workhorse.rb
@@ -62,9 +62,6 @@ module Gitlab
end
def send_git_archive(repository, ref:, format:, append_sha:, path: nil)
- path_enabled = Feature.enabled?(:git_archive_path, default_enabled: true)
- path = nil unless path_enabled
-
format ||= 'tar.gz'
format = format.downcase
@@ -78,12 +75,7 @@ module Gitlab
raise "Repository or ref not found" if metadata.empty?
- params =
- if path_enabled
- send_git_archive_params(repository, metadata, path, archive_format(format))
- else
- metadata
- end
+ params = send_git_archive_params(repository, metadata, path, archive_format(format))
# If present, DisableCache must be a Boolean. Otherwise
# workhorse ignores it.
@@ -138,8 +130,7 @@ module Gitlab
]
end
- def send_artifacts_entry(build, entry)
- file = build.artifacts_file
+ def send_artifacts_entry(file, entry)
archive = file.file_storage? ? file.path : file.url
params = {
@@ -213,7 +204,7 @@ module Gitlab
# This is the outermost encoding of a senddata: header. It is safe for
# inclusion in HTTP response headers
def encode(hash)
- Base64.urlsafe_encode64(JSON.dump(hash))
+ Base64.urlsafe_encode64(Gitlab::Json.dump(hash))
end
# This is for encoding individual fields inside the senddata JSON that
diff --git a/lib/gitlab/x509/signature.rb b/lib/gitlab/x509/signature.rb
index ed248e29211..7d4d4d9d13a 100644
--- a/lib/gitlab/x509/signature.rb
+++ b/lib/gitlab/x509/signature.rb
@@ -22,6 +22,10 @@ module Gitlab
X509Certificate.safe_create!(certificate_attributes) unless verified_signature.nil?
end
+ def user
+ User.find_by_any_email(@email)
+ end
+
def verified_signature
strong_memoize(:verified_signature) { verified_signature? }
end
diff --git a/lib/gitlab/x509/tag.rb b/lib/gitlab/x509/tag.rb
new file mode 100644
index 00000000000..48582c17764
--- /dev/null
+++ b/lib/gitlab/x509/tag.rb
@@ -0,0 +1,41 @@
+# frozen_string_literal: true
+require 'openssl'
+require 'digest'
+
+module Gitlab
+ module X509
+ class Tag
+ include Gitlab::Utils::StrongMemoize
+
+ def initialize(raw_tag)
+ @raw_tag = raw_tag
+ end
+
+ def signature
+ signature = X509::Signature.new(signature_text, signed_text, @raw_tag.tagger.email, Time.at(@raw_tag.tagger.date.seconds))
+
+ return if signature.verified_signature.nil?
+
+ signature
+ end
+
+ private
+
+ def signature_text
+ @raw_tag.message.slice(@raw_tag.message.index("-----BEGIN SIGNED MESSAGE-----")..-1)
+ rescue
+ nil
+ end
+
+ def signed_text
+ # signed text is reconstructed as long as there is no specific gitaly function
+ %{object #{@raw_tag.target_commit.id}
+type commit
+tag #{@raw_tag.name}
+tagger #{@raw_tag.tagger.name} <#{@raw_tag.tagger.email}> #{@raw_tag.tagger.date.seconds} #{@raw_tag.tagger.timezone}
+
+#{@raw_tag.message.gsub(/-----BEGIN SIGNED MESSAGE-----(.*)-----END SIGNED MESSAGE-----/m, "")}}
+ end
+ end
+ end
+end
diff --git a/lib/gitlab_danger.rb b/lib/gitlab_danger.rb
index ee0951f18ca..1c1763454a5 100644
--- a/lib/gitlab_danger.rb
+++ b/lib/gitlab_danger.rb
@@ -12,6 +12,7 @@ class GitlabDanger
database
commit_messages
telemetry
+ utility_css
].freeze
CI_ONLY_RULES ||= %w[
@@ -19,7 +20,6 @@ class GitlabDanger
changelog
specs
roulette
- gitlab_ui_wg
ce_ee_vue_templates
].freeze
diff --git a/lib/google_api/auth.rb b/lib/google_api/auth.rb
index 56f056fd869..319e5d2063c 100644
--- a/lib/google_api/auth.rb
+++ b/lib/google_api/auth.rb
@@ -37,6 +37,10 @@ module GoogleApi
Gitlab::Auth::OAuth::Provider.config_for('google_oauth2')
end
+ def client_options
+ config.args.client_options.deep_symbolize_keys
+ end
+
def client
return @client if defined?(@client)
@@ -49,7 +53,8 @@ module GoogleApi
config.app_secret,
site: 'https://accounts.google.com',
token_url: '/o/oauth2/token',
- authorize_url: '/o/oauth2/auth'
+ authorize_url: '/o/oauth2/auth',
+ **client_options
)
end
end
diff --git a/lib/mattermost/client.rb b/lib/mattermost/client.rb
index 293d0c563c5..a9551ffbd30 100644
--- a/lib/mattermost/client.rb
+++ b/lib/mattermost/client.rb
@@ -49,7 +49,7 @@ module Mattermost
end
def json_response(response)
- json_response = JSON.parse(response.body)
+ json_response = Gitlab::Json.parse(response.body, legacy_mode: true)
unless response.success?
raise Mattermost::ClientError.new(json_response['message'] || 'Undefined error')
diff --git a/lib/quality/helm3_client.rb b/lib/quality/helm3_client.rb
index f5eb0834386..afea73cbc50 100644
--- a/lib/quality/helm3_client.rb
+++ b/lib/quality/helm3_client.rb
@@ -17,10 +17,6 @@ module Quality
@revision ||= self[:revision].to_i
end
- def status
- @status ||= self[:status].downcase
- end
-
def last_update
@last_update ||= Time.parse(self[:last_update])
end
@@ -29,7 +25,7 @@ module Quality
# A single page of data and the corresponding page number.
Page = Struct.new(:releases, :number)
- def initialize(namespace:, tiller_namespace: nil)
+ def initialize(namespace:)
@namespace = namespace
end
diff --git a/lib/quality/helm_client.rb b/lib/quality/helm_client.rb
deleted file mode 100644
index fc4e1ca2d18..00000000000
--- a/lib/quality/helm_client.rb
+++ /dev/null
@@ -1,114 +0,0 @@
-# frozen_string_literal: true
-
-require 'time'
-require_relative '../gitlab/popen' unless defined?(Gitlab::Popen)
-
-module Quality
- class HelmClient
- CommandFailedError = Class.new(StandardError)
-
- attr_reader :tiller_namespace, :namespace
-
- RELEASE_JSON_ATTRIBUTES = %w[Name Revision Updated Status Chart AppVersion Namespace].freeze
-
- Release = Struct.new(:name, :revision, :last_update, :status, :chart, :app_version, :namespace) do
- def revision
- @revision ||= self[:revision].to_i
- end
-
- def last_update
- @last_update ||= Time.parse(self[:last_update])
- end
- end
-
- # A single page of data and the corresponding page number.
- Page = Struct.new(:releases, :number)
-
- def initialize(tiller_namespace:, namespace:)
- @tiller_namespace = tiller_namespace
- @namespace = namespace
- end
-
- def releases(args: [])
- each_release(args)
- end
-
- def delete(release_name:)
- run_command([
- 'delete',
- %(--tiller-namespace "#{tiller_namespace}"),
- '--purge',
- release_name
- ])
- end
-
- private
-
- def run_command(command)
- final_command = ['helm', *command].join(' ')
- puts "Running command: `#{final_command}`" # rubocop:disable Rails/Output
-
- result = Gitlab::Popen.popen_with_detail([final_command])
-
- if result.status.success?
- result.stdout.chomp.freeze
- else
- raise CommandFailedError, "The `#{final_command}` command failed (status: #{result.status}) with the following error:\n#{result.stderr}"
- end
- end
-
- def raw_releases(args = [])
- command = [
- 'list',
- %(--namespace "#{namespace}"),
- %(--tiller-namespace "#{tiller_namespace}" --output json),
- *args
- ]
- json = JSON.parse(run_command(command))
-
- releases = json['Releases'].map do |json_release|
- Release.new(*json_release.values_at(*RELEASE_JSON_ATTRIBUTES))
- end
-
- [releases, json['Next']]
- rescue JSON::ParserError => ex
- puts "Ignoring this JSON parsing error: #{ex}" # rubocop:disable Rails/Output
- [[], nil]
- end
-
- # Fetches data from Helm and yields a Page object for every page
- # of data, without loading all of them into memory.
- #
- # method - The Octokit method to use for getting the data.
- # args - Arguments to pass to the `helm list` command.
- def each_releases_page(args, &block)
- return to_enum(__method__, args) unless block_given?
-
- page = 1
- offset = ''
-
- loop do
- final_args = args.dup
- final_args << "--offset #{offset}" unless offset.to_s.empty?
- collection, offset = raw_releases(final_args)
-
- yield Page.new(collection, page += 1)
-
- break if offset.to_s.empty?
- end
- end
-
- # Iterates over all of the releases.
- #
- # args - Any arguments to pass to the `helm list` command.
- def each_release(args, &block)
- return to_enum(__method__, args) unless block_given?
-
- each_releases_page(args) do |page|
- page.releases.each do |release|
- yield release
- end
- end
- end
- end
-end
diff --git a/lib/quality/test_level.rb b/lib/quality/test_level.rb
index bbd8b4dcc3f..97b86fa8c2e 100644
--- a/lib/quality/test_level.rb
+++ b/lib/quality/test_level.rb
@@ -14,6 +14,7 @@ module Quality
],
unit: %w[
bin
+ channels
config
db
dependencies
diff --git a/lib/rspec_flaky/listener.rb b/lib/rspec_flaky/listener.rb
index bf15130d17e..37e4e16e87e 100644
--- a/lib/rspec_flaky/listener.rb
+++ b/lib/rspec_flaky/listener.rb
@@ -40,7 +40,7 @@ module RspecFlaky
new_flaky_examples = flaky_examples - suite_flaky_examples
if new_flaky_examples.any?
Rails.logger.warn "\nNew flaky examples detected:\n"
- Rails.logger.warn JSON.pretty_generate(new_flaky_examples.to_h)
+ Rails.logger.warn Gitlab::Json.pretty_generate(new_flaky_examples.to_h)
RspecFlaky::Report.new(new_flaky_examples).write(RspecFlaky::Config.new_flaky_examples_report_path)
# write_report_file(new_flaky_examples, RspecFlaky::Config.new_flaky_examples_report_path)
diff --git a/lib/static_model.rb b/lib/static_model.rb
index 86bf8d62f9a..27805817f4d 100644
--- a/lib/static_model.rb
+++ b/lib/static_model.rb
@@ -40,10 +40,6 @@ module StaticModel
end
def ==(other)
- if other.is_a? ::StaticModel
- id == other.id
- else
- super
- end
+ other.present? && other.is_a?(self.class) && id == other.id
end
end
diff --git a/lib/support/init.d/gitlab b/lib/support/init.d/gitlab
index 5e8e2ab9c25..8e9f220ec85 100755
--- a/lib/support/init.d/gitlab
+++ b/lib/support/init.d/gitlab
@@ -23,11 +23,20 @@
# An example defaults file can be found in lib/support/init.d/gitlab.default.example
###
-
### Environment variables
-RAILS_ENV="production"
-USE_UNICORN=""
-SIDEKIQ_WORKERS=1
+RAILS_ENV=${RAILS_ENV:-'production'}
+SIDEKIQ_WORKERS=${SIDEKIQ_WORKERS:-1}
+USE_WEB_SERVER=${USE_WEB_SERVER:-'puma'}
+
+case "${USE_WEB_SERVER}" in
+ puma|unicorn)
+ use_web_server="$USE_WEB_SERVER"
+ ;;
+ *)
+ echo "Unsupported web server '${USE_WEB_SERVER}' (Allowed: 'puma', 'unicorn')" 1>&2
+ exit 1
+ ;;
+esac
# Script variable names should be lower-case not to conflict with
# internal /bin/sh variables such as PATH, EDITOR or SHELL.
@@ -36,7 +45,7 @@ app_root="/home/$app_user/gitlab"
pid_path="$app_root/tmp/pids"
socket_path="$app_root/tmp/sockets"
rails_socket="$socket_path/gitlab.socket"
-web_server_pid_path="$pid_path/unicorn.pid"
+web_server_pid_path="$pid_path/$use_web_server.pid"
mail_room_enabled=false
mail_room_pid_path="$pid_path/mail_room.pid"
gitlab_workhorse_dir=$(cd $app_root/../gitlab-workhorse 2> /dev/null && pwd)
@@ -67,13 +76,6 @@ if ! cd "$app_root" ; then
echo "Failed to cd into $app_root, exiting!"; exit 1
fi
-# Select the web server to use
-if [ -z "$USE_UNICORN" ]; then
- use_web_server="puma"
-else
- use_web_server="unicorn"
-fi
-
if [ -z "$SIDEKIQ_WORKERS" ]; then
sidekiq_pid_path="$pid_path/sidekiq.pid"
else
diff --git a/lib/support/init.d/gitlab.default.example b/lib/support/init.d/gitlab.default.example
index bb271b16836..1b499467ad6 100644
--- a/lib/support/init.d/gitlab.default.example
+++ b/lib/support/init.d/gitlab.default.example
@@ -6,7 +6,7 @@
RAILS_ENV="production"
# Uncomment the line below to enable the Unicorn web server instead of Puma.
-# USE_UNICORN=1
+# use_web_server="unicorn"
# app_user defines the user that GitLab is run as.
# The default is "git".
@@ -26,8 +26,8 @@ pid_path="$app_root/tmp/pids"
socket_path="$app_root/tmp/sockets"
# web_server_pid_path defines the path in which to create the pid file fo the web_server
-# The default is "$pid_path/unicorn.pid"
-web_server_pid_path="$pid_path/unicorn.pid"
+# The default is "$pid_path/puma.pid"
+web_server_pid_path="$pid_path/puma.pid"
# sidekiq_pid_path defines the path in which to create the pid file for sidekiq
# The default is "$pid_path/sidekiq.pid"
diff --git a/lib/system_check/app/hashed_storage_all_projects_check.rb b/lib/system_check/app/hashed_storage_all_projects_check.rb
new file mode 100644
index 00000000000..7539309fbf4
--- /dev/null
+++ b/lib/system_check/app/hashed_storage_all_projects_check.rb
@@ -0,0 +1,22 @@
+# frozen_string_literal: true
+
+module SystemCheck
+ module App
+ class HashedStorageAllProjectsCheck < SystemCheck::BaseCheck
+ set_name 'All projects are in hashed storage?'
+
+ def check?
+ !Project.with_unmigrated_storage.exists?
+ end
+
+ def show_error
+ try_fixing_it(
+ "Please migrate all projects to hashed storage#{' on the primary' if Gitlab.ee? && Gitlab::Geo.secondary?}",
+ "as legacy storage is deprecated in 13.0 and support will be removed in 14.0."
+ )
+
+ for_more_information('doc/administration/repository_storage_types.md')
+ end
+ end
+ end
+end
diff --git a/lib/system_check/app/hashed_storage_enabled_check.rb b/lib/system_check/app/hashed_storage_enabled_check.rb
new file mode 100644
index 00000000000..b7c1791b740
--- /dev/null
+++ b/lib/system_check/app/hashed_storage_enabled_check.rb
@@ -0,0 +1,23 @@
+# frozen_string_literal: true
+
+module SystemCheck
+ module App
+ class HashedStorageEnabledCheck < SystemCheck::BaseCheck
+ set_name 'GitLab configured to store new projects in hashed storage?'
+
+ def check?
+ Gitlab::CurrentSettings.current_application_settings.hashed_storage_enabled
+ end
+
+ def show_error
+ try_fixing_it(
+ "Please enable the setting",
+ "`Use hashed storage paths for newly created and renamed projects`",
+ "in GitLab's Admin panel to avoid security issues and ensure data integrity."
+ )
+
+ for_more_information('doc/administration/repository_storage_types.md')
+ end
+ end
+ end
+end
diff --git a/lib/system_check/rake_task/app_task.rb b/lib/system_check/rake_task/app_task.rb
index aec7e5f416e..99c93edd12d 100644
--- a/lib/system_check/rake_task/app_task.rb
+++ b/lib/system_check/rake_task/app_task.rb
@@ -31,7 +31,9 @@ module SystemCheck
SystemCheck::App::GitVersionCheck,
SystemCheck::App::GitUserDefaultSSHConfigCheck,
SystemCheck::App::ActiveUsersCheck,
- SystemCheck::App::AuthorizedKeysPermissionCheck
+ SystemCheck::App::AuthorizedKeysPermissionCheck,
+ SystemCheck::App::HashedStorageEnabledCheck,
+ SystemCheck::App::HashedStorageAllProjectsCheck
]
end
end
diff --git a/lib/tasks/file_hooks.rake b/lib/tasks/file_hooks.rake
index 66d382db612..f767d63fe0d 100644
--- a/lib/tasks/file_hooks.rake
+++ b/lib/tasks/file_hooks.rake
@@ -4,6 +4,11 @@ namespace :file_hooks do
puts 'Validating file hooks from /file_hooks and /plugins directories'
Gitlab::FileHook.files.each do |file|
+ if File.dirname(file).ends_with?('plugins')
+ puts 'DEPRECATED: /plugins directory is deprecated and will be removed in 14.0. ' \
+ 'Please move your files into /file_hooks directory.'
+ end
+
success, message = Gitlab::FileHook.execute(file, Gitlab::DataBuilder::Push::SAMPLE_DATA)
if success
diff --git a/lib/tasks/gemojione.rake b/lib/tasks/gemojione.rake
index 8cf7c9e89f0..3833689e07e 100644
--- a/lib/tasks/gemojione.rake
+++ b/lib/tasks/gemojione.rake
@@ -7,7 +7,7 @@ namespace :gemojione do
aliases = {}
index_file = File.join(Rails.root, 'fixtures', 'emojis', 'index.json')
- index = JSON.parse(File.read(index_file))
+ index = Gitlab::Json.parse(File.read(index_file))
index.each_pair do |key, data|
data['aliases'].each do |a|
@@ -19,7 +19,7 @@ namespace :gemojione do
out = File.join(Rails.root, 'fixtures', 'emojis', 'aliases.json')
File.open(out, 'w') do |handle|
- handle.write(JSON.pretty_generate(aliases, indent: ' ', space: '', space_before: ''))
+ handle.write(Gitlab::Json.pretty_generate(aliases, indent: ' ', space: '', space_before: ''))
end
end
@@ -58,7 +58,7 @@ namespace :gemojione do
out = File.join(Rails.root, 'fixtures', 'emojis', 'digests.json')
File.open(out, 'w') do |handle|
- handle.write(JSON.pretty_generate(resultant_emoji_map))
+ handle.write(Gitlab::Json.pretty_generate(resultant_emoji_map))
end
end
diff --git a/lib/tasks/gitlab/gitaly.rake b/lib/tasks/gitlab/gitaly.rake
index ee47f71af93..fc55d9704d1 100644
--- a/lib/tasks/gitlab/gitaly.rake
+++ b/lib/tasks/gitlab/gitaly.rake
@@ -13,10 +13,9 @@ Usage: rake "gitlab:gitaly:install[/installation/dir,/storage/path]")
version = Gitlab::GitalyClient.expected_server_version
- checkout_or_clone_version(version: version, repo: args.repo, target_dir: args.dir)
-
- command = %w[/usr/bin/env -u RUBYOPT -u BUNDLE_GEMFILE]
+ checkout_or_clone_version(version: version, repo: args.repo, target_dir: args.dir, clone_opts: %w[--depth 1])
+ command = []
_, status = Gitlab::Popen.popen(%w[which gmake])
command << (status.zero? ? 'gmake' : 'make')
@@ -31,7 +30,7 @@ Usage: rake "gitlab:gitaly:install[/installation/dir,/storage/path]")
Dir.chdir(args.dir) do
# In CI we run scripts/gitaly-test-build instead of this command
unless ENV['CI'].present?
- Bundler.with_original_env { run_command!(command) }
+ Bundler.with_original_env { Gitlab::Popen.popen(command, nil, { "RUBYOPT" => nil, "BUNDLE_GEMFILE" => nil }) }
end
end
end
diff --git a/lib/tasks/gitlab/import_export/export.rake b/lib/tasks/gitlab/import_export/export.rake
index c9c212fbe4d..4bdc62c9319 100644
--- a/lib/tasks/gitlab/import_export/export.rake
+++ b/lib/tasks/gitlab/import_export/export.rake
@@ -3,12 +3,12 @@
# Export project to archive
#
# @example
-# bundle exec rake "gitlab:import_export:export[root, root, project_to_export, /path/to/file.tar.gz, true]"
+# bundle exec rake "gitlab:import_export:export[root, root, project_to_export, /path/to/file.tar.gz]"
#
namespace :gitlab do
namespace :import_export do
desc 'GitLab | Import/Export | EXPERIMENTAL | Export large project archives'
- task :export, [:username, :namespace_path, :project_path, :archive_path, :measurement_enabled] => :gitlab_environment do |_t, args|
+ task :export, [:username, :namespace_path, :project_path, :archive_path] => :gitlab_environment do |_t, args|
# Load it here to avoid polluting Rake tasks with Sidekiq test warnings
require 'sidekiq/testing'
@@ -18,6 +18,7 @@ namespace :gitlab do
warn_user_is_not_gitlab
if ENV['EXPORT_DEBUG'].present?
+ Gitlab::Utils::Measuring.logger = logger
ActiveRecord::Base.logger = logger
logger.level = Logger::DEBUG
else
@@ -29,7 +30,6 @@ namespace :gitlab do
project_path: args.project_path,
username: args.username,
file_path: args.archive_path,
- measurement_enabled: Gitlab::Utils.to_boolean(args.measurement_enabled),
logger: logger
)
diff --git a/lib/tasks/gitlab/import_export/import.rake b/lib/tasks/gitlab/import_export/import.rake
index 7e2162a7774..2702b530334 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, true]"
+# bundle exec rake "gitlab:import_export:import[root, root, imported_project, /path/to/file.tar.gz]"
#
namespace :gitlab do
namespace :import_export do
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|
+ task :import, [:username, :namespace_path, :project_path, :archive_path] => :gitlab_environment do |_t, args|
# Load it here to avoid polluting Rake tasks with Sidekiq test warnings
require 'sidekiq/testing'
@@ -22,6 +22,7 @@ namespace :gitlab do
warn_user_is_not_gitlab
if ENV['IMPORT_DEBUG'].present?
+ Gitlab::Utils::Measuring.logger = logger
ActiveRecord::Base.logger = logger
logger.level = Logger::DEBUG
else
@@ -33,7 +34,6 @@ namespace :gitlab do
project_path: args.project_path,
username: args.username,
file_path: args.archive_path,
- measurement_enabled: Gitlab::Utils.to_boolean(args.measurement_enabled),
logger: logger
)
diff --git a/lib/tasks/gitlab/shell.rake b/lib/tasks/gitlab/shell.rake
index 6586699f8ba..d6e62a5c550 100644
--- a/lib/tasks/gitlab/shell.rake
+++ b/lib/tasks/gitlab/shell.rake
@@ -12,7 +12,7 @@ namespace :gitlab do
gitlab_url += '/' unless gitlab_url.end_with?('/')
target_dir = Gitlab.config.gitlab_shell.path
- checkout_or_clone_version(version: default_version, repo: args.repo, target_dir: target_dir)
+ checkout_or_clone_version(version: default_version, repo: args.repo, target_dir: target_dir, clone_opts: %w[--depth 1])
# Make sure we're on the right tag
Dir.chdir(target_dir) do
diff --git a/lib/tasks/gitlab/snippets.rake b/lib/tasks/gitlab/snippets.rake
new file mode 100644
index 00000000000..c391cecfdbc
--- /dev/null
+++ b/lib/tasks/gitlab/snippets.rake
@@ -0,0 +1,91 @@
+# frozen_string_literal: true
+
+namespace :gitlab do
+ namespace :snippets do
+ DEFAULT_LIMIT = 100
+
+ # @example
+ # bundle exec rake gitlab:snippets:migrate SNIPPET_IDS=1,2,3,4
+ # bundle exec rake gitlab:snippets:migrate SNIPPET_IDS=1,2,3,4 LIMIT=50
+ desc 'GitLab | Migrate specific snippets to git'
+ task :migrate, [:ids] => :environment do |_, args|
+ unless ENV['SNIPPET_IDS'].presence
+ raise "Please supply the list of ids through the SNIPPET_IDS env var"
+ end
+
+ raise "Invalid limit value" if limit.zero?
+
+ if migration_running?
+ raise "There are already snippet migrations running. Please wait until they are finished."
+ end
+
+ ids = parse_snippet_ids!
+
+ puts "Starting the migration..."
+ Gitlab::BackgroundMigration::BackfillSnippetRepositories.new.perform_by_ids(ids)
+
+ list_non_migrated = non_migrated_snippets.where(id: ids)
+
+ if list_non_migrated.exists?
+ puts "The following snippets couldn't be migrated:"
+ puts list_non_migrated.pluck(:id).join(',')
+ else
+ puts "All snippets were migrated successfully"
+ end
+ end
+
+ def parse_snippet_ids!
+ ids = ENV['SNIPPET_IDS'].delete(' ').split(',').map do |id|
+ id.to_i.tap do |value|
+ raise "Invalid id provided" if value.zero?
+ end
+ end
+
+ if ids.size > limit
+ raise "The number of ids provided is higher than #{limit}. You can update this limit by using the env var `LIMIT`"
+ end
+
+ ids
+ end
+
+ # @example
+ # bundle exec rake gitlab:snippets:migration_status
+ desc 'GitLab | Show whether there are snippet background migrations running'
+ task migration_status: :environment do
+ if migration_running?
+ puts "There are snippet migrations running"
+ else
+ puts "There are no snippet migrations running"
+ end
+ end
+
+ def migration_running?
+ Sidekiq::ScheduledSet.new.any? { |r| r.klass == 'BackgroundMigrationWorker' && r.args[0] == 'BackfillSnippetRepositories' }
+ end
+
+ # @example
+ # bundle exec rake gitlab:snippets:list_non_migrated
+ # bundle exec rake gitlab:snippets:list_non_migrated LIMIT=50
+ desc 'GitLab | Show non migrated snippets'
+ task list_non_migrated: :environment do
+ raise "Invalid limit value" if limit.zero?
+
+ non_migrated_count = non_migrated_snippets.count
+ if non_migrated_count.zero?
+ puts "All snippets have been successfully migrated"
+ else
+ puts "There are #{non_migrated_count} snippets that haven't been migrated. Showing a batch of ids of those snippets:\n"
+ puts non_migrated_snippets.limit(limit).pluck(:id).join(',')
+ end
+ end
+
+ def non_migrated_snippets
+ @non_migrated_snippets ||= Snippet.select(:id).where.not(id: SnippetRepository.select(:snippet_id))
+ end
+
+ # There are problems with the specs if we memoize this value
+ def limit
+ ENV['LIMIT'] ? ENV['LIMIT'].to_i : DEFAULT_LIMIT
+ end
+ end
+end
diff --git a/lib/tasks/gitlab/track_deployment.rake b/lib/tasks/gitlab/track_deployment.rake
deleted file mode 100644
index 6f101aea303..00000000000
--- a/lib/tasks/gitlab/track_deployment.rake
+++ /dev/null
@@ -1,9 +0,0 @@
-namespace :gitlab do
- desc 'GitLab | Tracks a deployment in GitLab Performance Monitoring'
- task track_deployment: :environment do
- metric = Gitlab::Metrics::Metric
- .new('deployments', version: Gitlab::VERSION)
-
- Gitlab::Metrics.submit_metrics([metric.to_hash])
- end
-end
diff --git a/lib/tasks/gitlab/workhorse.rake b/lib/tasks/gitlab/workhorse.rake
index bae3e4e8001..53343c8f8ff 100644
--- a/lib/tasks/gitlab/workhorse.rake
+++ b/lib/tasks/gitlab/workhorse.rake
@@ -12,7 +12,7 @@ namespace :gitlab do
version = Gitlab::Workhorse.version
- checkout_or_clone_version(version: version, repo: args.repo, target_dir: args.dir)
+ checkout_or_clone_version(version: version, repo: args.repo, target_dir: args.dir, clone_opts: %w[--depth 1])
_, status = Gitlab::Popen.popen(%w[which gmake])
command = status.zero? ? 'gmake' : 'make'
diff --git a/lib/tasks/sidekiq.rake b/lib/tasks/sidekiq.rake
deleted file mode 100644
index d74878835fd..00000000000
--- a/lib/tasks/sidekiq.rake
+++ /dev/null
@@ -1,38 +0,0 @@
-namespace :sidekiq do
- def deprecation_warning!
- warn <<~WARNING
- This task is deprecated and will be removed in 13.0 as it is thought to be unused.
-
- If you are using this task, please comment on the below issue:
- https://gitlab.com/gitlab-org/gitlab/issues/196731
- WARNING
- end
-
- desc '[DEPRECATED] GitLab | Sidekiq | Stop sidekiq'
- task :stop do
- deprecation_warning!
-
- system(*%w(bin/background_jobs stop))
- end
-
- desc '[DEPRECATED] GitLab | Sidekiq | Start sidekiq'
- task :start do
- deprecation_warning!
-
- system(*%w(bin/background_jobs start))
- end
-
- desc '[DEPRECATED] GitLab | Sidekiq | Restart sidekiq'
- task :restart do
- deprecation_warning!
-
- system(*%w(bin/background_jobs restart))
- end
-
- desc '[DEPRECATED] GitLab | Sidekiq | Start sidekiq with launchd on Mac OS X'
- task :launchd do
- deprecation_warning!
-
- system(*%w(bin/background_jobs start_silent))
- end
-end
diff --git a/locale/am_ET/gitlab.po b/locale/am_ET/gitlab.po
index b9ed15d2632..debbaaead07 100644
--- a/locale/am_ET/gitlab.po
+++ b/locale/am_ET/gitlab.po
@@ -12,7 +12,7 @@ msgstr ""
"X-Crowdin-Project: gitlab-ee\n"
"X-Crowdin-Language: am\n"
"X-Crowdin-File: /master/locale/gitlab.pot\n"
-"PO-Revision-Date: 2020-04-15 00:29\n"
+"PO-Revision-Date: 2020-05-05 21:37\n"
msgid " %{start} to %{end}"
msgstr ""
@@ -212,6 +212,11 @@ msgid_plural "%d tags"
msgstr[0] "%d መለያ"
msgstr[1] "%d መለያዎች"
+msgid "%d unresolved thread"
+msgid_plural "%d unresolved threads"
+msgstr[0] ""
+msgstr[1] ""
+
msgid "%d vulnerability dismissed"
msgid_plural "%d vulnerabilities dismissed"
msgstr[0] ""
@@ -371,6 +376,9 @@ msgstr "%{mergeLength}/%{usersLength} merge ማድረጠይቻላáˆ"
msgid "%{mrText}, this issue will be closed automatically."
msgstr "%{mrText}ᣠይህ እትሠበራስ-ሰር ይዘጋáˆá¢"
+msgid "%{namespace_name} is now read-only. You cannot: %{base_message}"
+msgstr ""
+
msgid "%{name} contained %{resultsString}"
msgstr "%{name} %{resultsString} á‹­á‹­á‹›áˆ"
@@ -383,6 +391,11 @@ msgstr ""
msgid "%{name}'s avatar"
msgstr "የ%{name} አáˆáˆ³á‹«"
+msgid "%{no_of_days} day"
+msgid_plural "%{no_of_days} days"
+msgstr[0] ""
+msgstr[1] ""
+
msgid "%{number_commits_behind} commits behind %{default_branch}, %{number_commits_ahead} commits ahead"
msgstr "%{number_commits_behind} commits ኅላ %{default_branch}, %{number_commits_ahead} commits በáŠá‰µ"
@@ -506,6 +519,9 @@ msgstr[1] ""
msgid "%{text} is available"
msgstr ""
+msgid "%{timebox_name} should belong either to a project or a group."
+msgstr ""
+
msgid "%{title} %{operator} %{threshold}"
msgstr ""
@@ -781,12 +797,18 @@ msgstr ""
msgid "<code>\"johnsmith@example.com\": \"johnsmith@example.com\"</code> will add \"By <a href=\"#\">johnsmith@example.com</a>\" to all issues and comments originally created by johnsmith@example.com. By default, the email address or username is masked to ensure the user's privacy. Use this option if you want to show the full email address."
msgstr ""
+msgid "<namespace / project>"
+msgstr ""
+
msgid "<no name set>"
msgstr ""
msgid "<no scopes selected>"
msgstr ""
+msgid "<project name>"
+msgstr ""
+
msgid "<strong>%{group_name}</strong> group members"
msgstr ""
@@ -820,6 +842,9 @@ msgstr ""
msgid "A basic page and serverless function that uses AWS Lambda, AWS API Gateway, and GitLab Pages"
msgstr ""
+msgid "A complete DevOps platform"
+msgstr ""
+
msgid "A default branch cannot be chosen for an empty project."
msgstr ""
@@ -1036,6 +1061,12 @@ msgstr ""
msgid "AccessTokens|reset it"
msgstr ""
+msgid "AccessibilityReport|Accessibility report artifact not found"
+msgstr ""
+
+msgid "AccessibilityReport|Failed to retrieve accessibility report"
+msgstr ""
+
msgid "AccessibilityReport|Learn More"
msgstr ""
@@ -1045,6 +1076,9 @@ msgstr ""
msgid "AccessibilityReport|New"
msgstr ""
+msgid "AccessibilityReport|The accessibility scanning found an error of the following type: %{code}"
+msgstr ""
+
msgid "Account"
msgstr ""
@@ -1287,6 +1321,9 @@ msgstr ""
msgid "Added at"
msgstr ""
+msgid "Added for this merge request"
+msgstr ""
+
msgid "Added in this version"
msgstr ""
@@ -1658,6 +1695,72 @@ msgid_plural "Alerts"
msgstr[0] ""
msgstr[1] ""
+msgid "Alert Details"
+msgstr ""
+
+msgid "AlertManagement|Acknowledged"
+msgstr ""
+
+msgid "AlertManagement|Alert"
+msgstr ""
+
+msgid "AlertManagement|Authorize external service"
+msgstr ""
+
+msgid "AlertManagement|Display alerts from all your monitoring tools directly within GitLab. Streamline the investigation of your alerts and the escalation of alerts to incidents."
+msgstr ""
+
+msgid "AlertManagement|End time"
+msgstr ""
+
+msgid "AlertManagement|End time:"
+msgstr ""
+
+msgid "AlertManagement|Events"
+msgstr ""
+
+msgid "AlertManagement|Events:"
+msgstr ""
+
+msgid "AlertManagement|Full Alert Details"
+msgstr ""
+
+msgid "AlertManagement|More information"
+msgstr ""
+
+msgid "AlertManagement|No alerts available to display. If you think you're seeing this message in error, refresh the page."
+msgstr ""
+
+msgid "AlertManagement|No alerts to display."
+msgstr ""
+
+msgid "AlertManagement|Overview"
+msgstr ""
+
+msgid "AlertManagement|Resolved"
+msgstr ""
+
+msgid "AlertManagement|Severity"
+msgstr ""
+
+msgid "AlertManagement|Start time"
+msgstr ""
+
+msgid "AlertManagement|Start time:"
+msgstr ""
+
+msgid "AlertManagement|Status"
+msgstr ""
+
+msgid "AlertManagement|Surface alerts in GitLab"
+msgstr ""
+
+msgid "AlertManagement|There was an error displaying the alerts. Confirm your endpoint's configuration details to ensure alerts appear."
+msgstr ""
+
+msgid "AlertManagement|Triggered"
+msgstr ""
+
msgid "AlertService|%{linkStart}Learn more%{linkEnd} about configuring this endpoint to receive alerts."
msgstr ""
@@ -1727,6 +1830,9 @@ msgstr ""
msgid "All security scans are enabled because %{linkStart}Auto DevOps%{linkEnd} is enabled on this project"
msgstr ""
+msgid "All threads resolved"
+msgstr ""
+
msgid "All users"
msgstr ""
@@ -1745,6 +1851,9 @@ msgstr ""
msgid "Allow only the selected protocols to be used for Git access."
msgstr ""
+msgid "Allow owners to manage default branch protection per group"
+msgstr ""
+
msgid "Allow owners to manually add users outside of LDAP"
msgstr ""
@@ -1853,6 +1962,9 @@ msgstr ""
msgid "An error occurred fetching the dropdown data."
msgstr ""
+msgid "An error occurred fetching the project authors."
+msgstr ""
+
msgid "An error occurred previewing the blob"
msgstr ""
@@ -1934,6 +2046,9 @@ msgstr ""
msgid "An error occurred while fetching sidebar data"
msgstr ""
+msgid "An error occurred while fetching terraform reports."
+msgstr ""
+
msgid "An error occurred while fetching the Service Desk address."
msgstr ""
@@ -2171,6 +2286,9 @@ msgstr ""
msgid "Any"
msgstr ""
+msgid "Any Author"
+msgstr ""
+
msgid "Any Label"
msgstr ""
@@ -2386,10 +2504,10 @@ msgstr ""
msgid "Are you sure that you want to unarchive this project?"
msgstr ""
-msgid "Are you sure you want to cancel creating this comment?"
+msgid "Are you sure you want to cancel editing this comment?"
msgstr ""
-msgid "Are you sure you want to cancel editing this comment?"
+msgid "Are you sure you want to close this blocked issue?"
msgstr ""
msgid "Are you sure you want to delete %{name}?"
@@ -2419,6 +2537,9 @@ msgstr ""
msgid "Are you sure you want to deploy this environment?"
msgstr ""
+msgid "Are you sure you want to discard this comment?"
+msgstr ""
+
msgid "Are you sure you want to erase this build?"
msgstr ""
@@ -2595,6 +2716,9 @@ msgstr ""
msgid "At least one approval from a code owner is required to change files matching the respective CODEOWNER rules."
msgstr ""
+msgid "At least one logging option is required to be enabled"
+msgstr ""
+
msgid "At least one of group_id or project_id must be specified"
msgstr ""
@@ -2633,6 +2757,27 @@ msgstr ""
msgid "AuditEvents|Target"
msgstr ""
+msgid "AuditLogs|(removed)"
+msgstr ""
+
+msgid "AuditLogs|Action"
+msgstr ""
+
+msgid "AuditLogs|Author"
+msgstr ""
+
+msgid "AuditLogs|Date"
+msgstr ""
+
+msgid "AuditLogs|IP Address"
+msgstr ""
+
+msgid "AuditLogs|Object"
+msgstr ""
+
+msgid "AuditLogs|Target"
+msgstr ""
+
msgid "Aug"
msgstr ""
@@ -2915,9 +3060,6 @@ msgstr ""
msgid "BambooService|You must set up automatic revision labeling and a repository trigger in Bamboo."
msgstr ""
-msgid "Batch operations"
-msgstr ""
-
msgid "BatchComments|Delete all pending comments"
msgstr ""
@@ -3047,6 +3189,9 @@ msgstr ""
msgid "Boards"
msgstr ""
+msgid "Boards and Board Lists"
+msgstr ""
+
msgid "Boards|Collapse"
msgstr ""
@@ -3266,6 +3411,9 @@ msgstr ""
msgid "Buy GitLab Enterprise Edition"
msgstr ""
+msgid "Buy more Pipeline minutes"
+msgstr ""
+
msgid "By %{user_name}"
msgstr ""
@@ -3386,6 +3534,9 @@ msgstr ""
msgid "Can't scan the code?"
msgstr ""
+msgid "Can't update snippet: %{err}"
+msgstr ""
+
msgid "Canary"
msgstr ""
@@ -3554,6 +3705,9 @@ msgstr ""
msgid "Changing group path can have unintended side effects."
msgstr ""
+msgid "Charts"
+msgstr ""
+
msgid "Charts can't be displayed as the request for data has timed out. %{documentationLink}"
msgstr ""
@@ -3590,9 +3744,6 @@ msgstr ""
msgid "ChatMessage|and [%{count} more](%{pipeline_failed_jobs_url})"
msgstr ""
-msgid "ChatMessage|failed"
-msgstr ""
-
msgid "ChatMessage|has failed"
msgstr ""
@@ -3608,9 +3759,6 @@ msgstr ""
msgid "ChatMessage|in %{project_link}"
msgstr ""
-msgid "ChatMessage|passed"
-msgstr ""
-
msgid "Check again"
msgstr ""
@@ -4061,9 +4209,6 @@ msgstr ""
msgid "Click any <strong>project name</strong> in the project list below to navigate to the project milestone."
msgstr ""
-msgid "Click here"
-msgstr ""
-
msgid "Click the <strong>Download</strong> button and wait for downloading to complete."
msgstr ""
@@ -4451,6 +4596,12 @@ msgstr ""
msgid "ClusterIntegration|Fetching zones"
msgstr ""
+msgid "ClusterIntegration|Fluentd"
+msgstr ""
+
+msgid "ClusterIntegration|Fluentd is an open source data collector, which lets you unify the data collection and consumption for a better use and understanding of data. It requires at least one of the following logs to be successfully installed."
+msgstr ""
+
msgid "ClusterIntegration|GitLab Integration"
msgstr ""
@@ -4757,6 +4908,15 @@ msgstr ""
msgid "ClusterIntegration|Request to begin uninstalling failed"
msgstr ""
+msgid "ClusterIntegration|SIEM Hostname"
+msgstr ""
+
+msgid "ClusterIntegration|SIEM Port"
+msgstr ""
+
+msgid "ClusterIntegration|SIEM Protocol"
+msgstr ""
+
msgid "ClusterIntegration|Save changes"
msgstr ""
@@ -4853,6 +5013,12 @@ msgstr ""
msgid "ClusterIntegration|Select zone to choose machine type"
msgstr ""
+msgid "ClusterIntegration|Send Cilium Logs"
+msgstr ""
+
+msgid "ClusterIntegration|Send ModSecurity Logs"
+msgstr ""
+
msgid "ClusterIntegration|Service Token"
msgstr ""
@@ -5281,18 +5447,33 @@ msgstr ""
msgid "Compliance framework (optional)"
msgstr ""
+msgid "ComplianceFramework|GDPR"
+msgstr ""
+
msgid "ComplianceFramework|GDPR - General Data Protection Regulation"
msgstr ""
+msgid "ComplianceFramework|HIPAA"
+msgstr ""
+
msgid "ComplianceFramework|HIPAA - Health Insurance Portability and Accountability Act"
msgstr ""
+msgid "ComplianceFramework|PCI-DSS"
+msgstr ""
+
msgid "ComplianceFramework|PCI-DSS - Payment Card Industry-Data Security Standard"
msgstr ""
+msgid "ComplianceFramework|SOC 2"
+msgstr ""
+
msgid "ComplianceFramework|SOC 2 - Service Organization Control 2"
msgstr ""
+msgid "ComplianceFramework|SOX"
+msgstr ""
+
msgid "ComplianceFramework|SOX - Sarbanes-Oxley"
msgstr ""
@@ -5440,9 +5621,15 @@ msgstr ""
msgid "Container repositories sync capacity"
msgstr ""
+msgid "ContainerRegistry| Please visit the %{linkStart}administration settings%{linkEnd} to enable this feature."
+msgstr ""
+
msgid "ContainerRegistry|%{imageName} tags"
msgstr ""
+msgid "ContainerRegistry|%{title} was successfully scheduled for deletion"
+msgstr ""
+
msgid "ContainerRegistry|Automatically remove extra images that aren't designed to be kept."
msgstr ""
@@ -5455,6 +5642,9 @@ msgstr ""
msgid "ContainerRegistry|Container Registry"
msgstr ""
+msgid "ContainerRegistry|Container Registry tag expiration and retention policy is disabled"
+msgstr ""
+
msgid "ContainerRegistry|Copy build command"
msgstr ""
@@ -5464,18 +5654,12 @@ msgstr ""
msgid "ContainerRegistry|Copy push command"
msgstr ""
-msgid "ContainerRegistry|Currently, the Container Registry tag expiration feature is not available for projects created before GitLab version 12.8. For updates and more information, visit Issue %{linkStart}#196124%{linkEnd}"
-msgstr ""
-
msgid "ContainerRegistry|Docker connection error"
msgstr ""
msgid "ContainerRegistry|Docker tag expiration policy is %{toggleStatus}"
msgstr ""
-msgid "ContainerRegistry|Docker tags with names matching this regex pattern will expire:"
-msgstr ""
-
msgid "ContainerRegistry|Edit Settings"
msgstr ""
@@ -5497,9 +5681,6 @@ msgstr ""
msgid "ContainerRegistry|Image ID"
msgstr ""
-msgid "ContainerRegistry|Image deleted successfully"
-msgstr ""
-
msgid "ContainerRegistry|Keep and protect the images that matter most."
msgstr ""
@@ -5515,12 +5696,24 @@ msgstr ""
msgid "ContainerRegistry|Number of tags to retain:"
msgstr ""
+msgid "ContainerRegistry|Please contact your administrator."
+msgstr ""
+
msgid "ContainerRegistry|Push an image"
msgstr ""
msgid "ContainerRegistry|Quick Start"
msgstr ""
+msgid "ContainerRegistry|Regular expressions such as %{codeStart}.*-test%{codeEnd} or %{codeStart}dev-.*%{codeEnd} are supported"
+msgstr ""
+
+msgid "ContainerRegistry|Regular expressions such as %{codeStart}.*-test%{codeEnd} or %{codeStart}dev-.*%{codeEnd} are supported. To select all tags, use %{codeStart}.*%{codeEnd}"
+msgstr ""
+
+msgid "ContainerRegistry|Remember to run %{docLinkStart}garbage collection%{docLinkEnd} to remove the stale data from storage."
+msgstr ""
+
msgid "ContainerRegistry|Remove repository"
msgstr ""
@@ -5535,22 +5728,22 @@ msgstr[1] ""
msgid "ContainerRegistry|Retention policy has been Enabled"
msgstr ""
-msgid "ContainerRegistry|Something went wrong while deleting the image."
+msgid "ContainerRegistry|Something went wrong while fetching the expiration policy."
msgstr ""
-msgid "ContainerRegistry|Something went wrong while deleting the tag."
+msgid "ContainerRegistry|Something went wrong while fetching the repository list."
msgstr ""
-msgid "ContainerRegistry|Something went wrong while deleting the tags."
+msgid "ContainerRegistry|Something went wrong while fetching the tags list."
msgstr ""
-msgid "ContainerRegistry|Something went wrong while fetching the expiration policy."
+msgid "ContainerRegistry|Something went wrong while marking the tag for deletion."
msgstr ""
-msgid "ContainerRegistry|Something went wrong while fetching the packages list."
+msgid "ContainerRegistry|Something went wrong while marking the tags for deletion."
msgstr ""
-msgid "ContainerRegistry|Something went wrong while fetching the tags list."
+msgid "ContainerRegistry|Something went wrong while scheduling %{title} for deletion. Please try again."
msgstr ""
msgid "ContainerRegistry|Something went wrong while updating the expiration policy."
@@ -5559,16 +5752,25 @@ msgstr ""
msgid "ContainerRegistry|Tag"
msgstr ""
-msgid "ContainerRegistry|Tag deleted successfully"
-msgstr ""
-
msgid "ContainerRegistry|Tag expiration policy"
msgstr ""
msgid "ContainerRegistry|Tag expiration policy is designed to:"
msgstr ""
-msgid "ContainerRegistry|Tags deleted successfully"
+msgid "ContainerRegistry|Tag successfully marked for deletion."
+msgstr ""
+
+msgid "ContainerRegistry|Tags successfully marked for deletion."
+msgstr ""
+
+msgid "ContainerRegistry|Tags with names matching this regex pattern will %{italicStart}be preserved:%{italicEnd}"
+msgstr ""
+
+msgid "ContainerRegistry|Tags with names matching this regex pattern will %{italicStart}expire:%{italicEnd}"
+msgstr ""
+
+msgid "ContainerRegistry|The Container Registry tag expiration and retention policies for this project have not been enabled."
msgstr ""
msgid "ContainerRegistry|The last tag related to this image was recently removed. This empty image and any associated data will be automatically removed as part of the regular Garbage Collection process. If you have any questions, contact your administrator."
@@ -5589,16 +5791,16 @@ msgstr ""
msgid "ContainerRegistry|There are no container images stored for this project"
msgstr ""
-msgid "ContainerRegistry|This Registry contains deleted image tag data. Remember to run %{docLinkStart}garbage collection%{docLinkEnd} to remove the stale data from storage."
+msgid "ContainerRegistry|There was an error during the deletion of this image repository, please try again."
msgstr ""
msgid "ContainerRegistry|This image has no active tags"
msgstr ""
-msgid "ContainerRegistry|We are having trouble connecting to Docker, which could be due to an issue with your project name or path. %{docLinkStart}More Information%{docLinkEnd}"
+msgid "ContainerRegistry|This image repository is scheduled for deletion"
msgstr ""
-msgid "ContainerRegistry|Wildcards such as %{codeStart}.*-stable%{codeEnd} or %{codeStart}production/.*%{codeEnd} are supported. To select all tags, use %{codeStart}.*%{codeEnd}"
+msgid "ContainerRegistry|We are having trouble connecting to Docker, which could be due to an issue with your project name or path. %{docLinkStart}More Information%{docLinkEnd}"
msgstr ""
msgid "ContainerRegistry|With the Container Registry, every project can have its own space to store its Docker images. %{docLinkStart}More Information%{docLinkEnd}"
@@ -5853,7 +6055,7 @@ msgstr ""
msgid "Could not delete chat nickname %{chat_name}."
msgstr ""
-msgid "Could not find design"
+msgid "Could not find design."
msgstr ""
msgid "Could not remove the trigger."
@@ -5868,6 +6070,9 @@ msgstr ""
msgid "Could not revoke personal access token %{personal_access_token_name}."
msgstr ""
+msgid "Could not revoke project access token %{project_access_token_name}."
+msgstr ""
+
msgid "Could not save group ID"
msgstr ""
@@ -5925,6 +6130,9 @@ msgstr ""
msgid "Create a new branch"
msgstr ""
+msgid "Create a new deploy key for this project"
+msgstr ""
+
msgid "Create a new file as there are no files yet. Afterwards, you'll be able to commit your changes."
msgstr ""
@@ -6030,6 +6238,9 @@ msgstr ""
msgid "Create requirement"
msgstr ""
+msgid "Create snippet"
+msgstr ""
+
msgid "Create wildcard: %{searchTerm}"
msgstr ""
@@ -6051,6 +6262,9 @@ msgstr ""
msgid "Created"
msgstr ""
+msgid "Created %{timestamp}"
+msgstr ""
+
msgid "Created At"
msgstr ""
@@ -6407,6 +6621,9 @@ msgstr ""
msgid "CycleAnalytics|stage dropdown"
msgstr ""
+msgid "DAG"
+msgstr ""
+
msgid "DNS"
msgstr ""
@@ -6773,6 +6990,9 @@ msgstr ""
msgid "Deploy key was successfully updated."
msgstr ""
+msgid "Deploy keys allow read-only or read-write (if enabled) access to your repository. Deploy keys can be used for CI, staging or production servers. You can create a deploy key or add an existing one."
+msgstr ""
+
msgid "Deploy progress not found. To see pods, ensure your environment matches %{linkStart}deploy board criteria%{linkEnd}."
msgstr ""
@@ -6926,6 +7146,9 @@ msgstr ""
msgid "Deploying to"
msgstr ""
+msgid "Deployment Frequency"
+msgstr ""
+
msgid "Deployment|API"
msgstr ""
@@ -7444,9 +7667,6 @@ msgstr ""
msgid "Edit environment"
msgstr ""
-msgid "Edit epic description"
-msgstr ""
-
msgid "Edit file"
msgstr ""
@@ -8068,9 +8288,6 @@ msgstr ""
msgid "Epics"
msgstr ""
-msgid "Epics (Ultimate / Gold license only)"
-msgstr ""
-
msgid "Epics Roadmap"
msgstr ""
@@ -8080,6 +8297,9 @@ msgstr ""
msgid "Epics let you manage your portfolio of projects more efficiently and with less effort"
msgstr ""
+msgid "Epics, Issues, and Merge Requests"
+msgstr ""
+
msgid "Epics|Add an epic"
msgstr ""
@@ -8131,6 +8351,9 @@ msgstr ""
msgid "Epics|Something went wrong while fetching group epics."
msgstr ""
+msgid "Epics|Something went wrong while moving item."
+msgstr ""
+
msgid "Epics|Something went wrong while ordering item."
msgstr ""
@@ -8185,6 +8408,9 @@ msgstr ""
msgid "Error fetching network graph."
msgstr ""
+msgid "Error fetching payload data."
+msgstr ""
+
msgid "Error fetching projects"
msgstr ""
@@ -8194,9 +8420,6 @@ msgstr ""
msgid "Error fetching the dependency list. Please check your network connection and try again."
msgstr ""
-msgid "Error fetching usage ping data."
-msgstr ""
-
msgid "Error loading branch data. Please try again."
msgstr ""
@@ -8338,6 +8561,9 @@ msgstr ""
msgid "Errors"
msgstr ""
+msgid "Errors:"
+msgstr ""
+
msgid "Estimated"
msgstr ""
@@ -8542,12 +8768,18 @@ msgstr ""
msgid "Export as CSV"
msgstr ""
+msgid "Export group"
+msgstr ""
+
msgid "Export issues"
msgstr ""
msgid "Export project"
msgstr ""
+msgid "Export this group with all related data to a new GitLab instance. Once complete, you can import the data file from the \"New Group\" page."
+msgstr ""
+
msgid "Export this project with all its related data in order to move your project to a new GitLab instance. Once the export is finished, you can import the file from the \"New Project\" page."
msgstr ""
@@ -8617,9 +8849,6 @@ msgstr ""
msgid "Failed Jobs"
msgstr ""
-msgid "Failed create wiki"
-msgstr ""
-
msgid "Failed to add a Zoom meeting"
msgstr ""
@@ -8659,6 +8888,9 @@ msgstr ""
msgid "Failed to create resources"
msgstr ""
+msgid "Failed to create wiki"
+msgstr ""
+
msgid "Failed to delete board. Please try again."
msgstr ""
@@ -8668,7 +8900,7 @@ msgstr ""
msgid "Failed to enqueue the rebase operation, possibly due to a long-lived transaction. Try again later."
msgstr ""
-msgid "Failed to find import label for jira import."
+msgid "Failed to find import label for Jira import."
msgstr ""
msgid "Failed to get ref."
@@ -8692,6 +8924,12 @@ msgstr ""
msgid "Failed to load groups & users."
msgstr ""
+msgid "Failed to load labels. Please try again."
+msgstr ""
+
+msgid "Failed to load milestones. Please try again."
+msgstr ""
+
msgid "Failed to load related branches"
msgstr ""
@@ -9067,7 +9305,10 @@ msgstr ""
msgid "Filter by milestone name"
msgstr ""
-msgid "Filter by name..."
+msgid "Filter by name"
+msgstr ""
+
+msgid "Filter by status"
msgstr ""
msgid "Filter by two-factor authentication"
@@ -9079,12 +9320,18 @@ msgstr ""
msgid "Filter projects"
msgstr ""
+msgid "Filter results"
+msgstr ""
+
msgid "Filter results by group"
msgstr ""
msgid "Filter results by project"
msgstr ""
+msgid "Filter results..."
+msgstr ""
+
msgid "Filter your projects by name"
msgstr ""
@@ -9373,10 +9620,10 @@ msgstr ""
msgid "GeoNodes|Checksummed"
msgstr ""
-msgid "GeoNodes|Container repositories"
+msgid "GeoNodes|Consult Geo troubleshooting information"
msgstr ""
-msgid "GeoNodes|Data is out of date from %{timeago}"
+msgid "GeoNodes|Container repositories"
msgstr ""
msgid "GeoNodes|Data replication lag"
@@ -9418,6 +9665,9 @@ msgstr ""
msgid "GeoNodes|Last event ID seen from primary"
msgstr ""
+msgid "GeoNodes|Learn more about Geo node statuses"
+msgstr ""
+
msgid "GeoNodes|Loading nodes"
msgstr ""
@@ -9433,6 +9683,9 @@ msgstr ""
msgid "GeoNodes|Node was successfully removed."
msgstr ""
+msgid "GeoNodes|Node's status was updated %{timeAgo}."
+msgstr ""
+
msgid "GeoNodes|Not checksummed"
msgstr ""
@@ -9463,7 +9716,10 @@ msgstr ""
msgid "GeoNodes|Repository verification progress"
msgstr ""
-msgid "GeoNodes|Selective"
+msgid "GeoNodes|Selective (%{syncLabel})"
+msgstr ""
+
+msgid "GeoNodes|Selective synchronization"
msgstr ""
msgid "GeoNodes|Something went wrong while changing node status"
@@ -9490,6 +9746,9 @@ msgstr ""
msgid "GeoNodes|Unverified"
msgstr ""
+msgid "GeoNodes|Updated %{timeAgo}"
+msgstr ""
+
msgid "GeoNodes|Used slots"
msgstr ""
@@ -9529,16 +9788,16 @@ msgstr ""
msgid "Geo|All"
msgstr ""
-msgid "Geo|All projects"
+msgid "Geo|All %{replicable_type}"
msgstr ""
-msgid "Geo|All projects are being scheduled for re-sync"
+msgid "Geo|All projects"
msgstr ""
-msgid "Geo|All projects are being scheduled for re-verify"
+msgid "Geo|All projects are being scheduled for resync"
msgstr ""
-msgid "Geo|Batch operations"
+msgid "Geo|All projects are being scheduled for reverify"
msgstr ""
msgid "Geo|Could not remove tracking entry for an existing project."
@@ -9550,9 +9809,15 @@ msgstr ""
msgid "Geo|Failed"
msgstr ""
+msgid "Geo|Filter by status"
+msgstr ""
+
msgid "Geo|Geo Status"
msgstr ""
+msgid "Geo|In progress"
+msgstr ""
+
msgid "Geo|In sync"
msgstr ""
@@ -9577,9 +9842,6 @@ msgstr ""
msgid "Geo|Not synced yet"
msgstr ""
-msgid "Geo|Pending"
-msgstr ""
-
msgid "Geo|Pending synchronization"
msgstr ""
@@ -9616,7 +9878,7 @@ msgstr ""
msgid "Geo|Resync"
msgstr ""
-msgid "Geo|Resync all projects"
+msgid "Geo|Resync all"
msgstr ""
msgid "Geo|Retry count"
@@ -9625,7 +9887,7 @@ msgstr ""
msgid "Geo|Reverify"
msgstr ""
-msgid "Geo|Reverify all projects"
+msgid "Geo|Reverify all"
msgstr ""
msgid "Geo|Status"
@@ -9967,9 +10229,6 @@ msgstr ""
msgid "Go to file"
msgstr ""
-msgid "Go to file (MRs only)"
-msgstr ""
-
msgid "Go to file permalink (while viewing a file)"
msgstr ""
@@ -10414,6 +10673,9 @@ msgstr ""
msgid "GroupSettings|Disable group mentions"
msgstr ""
+msgid "GroupSettings|Export group"
+msgstr ""
+
msgid "GroupSettings|If the parent group's visibility is lower than the group current visibility, visibility levels for subgroups and projects will be changed to match the new parent group's visibility."
msgstr ""
@@ -10492,6 +10754,9 @@ msgstr ""
msgid "Groups (%{count})"
msgstr ""
+msgid "Groups (%{groups})"
+msgstr ""
+
msgid "Groups can also be nested by creating %{subgroup_docs_link_start}subgroups%{subgroup_docs_link_end}."
msgstr ""
@@ -10757,15 +11022,12 @@ msgstr ""
msgid "ID:"
msgstr ""
-msgid "IDE|Allow live previews of JavaScript projects in the Web IDE using CodeSandbox client side evaluation."
+msgid "IDE|Allow live previews of JavaScript projects in the Web IDE using CodeSandbox Live Preview."
msgstr ""
msgid "IDE|Back"
msgstr ""
-msgid "IDE|Client side evaluation"
-msgstr ""
-
msgid "IDE|Commit"
msgstr ""
@@ -10829,6 +11091,9 @@ msgstr ""
msgid "If any job surpasses this timeout threshold, it will be marked as failed. Human readable time input language is accepted like \"1 hour\". Values without specification represent seconds."
msgstr ""
+msgid "If blank, set allowable lifetime to %{instance_level_policy_in_words}, as defined by the instance admin. Once set, existing tokens for users in this group may be revoked."
+msgstr ""
+
msgid "If checked, group owners can manage LDAP group links and LDAP member overrides"
msgstr ""
@@ -10868,6 +11133,9 @@ msgstr ""
msgid "If you lose your recovery codes you can generate new ones, invalidating all previous codes."
msgstr ""
+msgid "If you reach 100%% storage capacity, you will not be able to: %{base_message}"
+msgstr ""
+
msgid "If your HTTP repository is not publicly accessible, add your credentials."
msgstr ""
@@ -10979,6 +11247,9 @@ msgstr ""
msgid "Import repository"
msgstr ""
+msgid "Import started by: %{importInitiator}"
+msgstr ""
+
msgid "Import tasks"
msgstr ""
@@ -11045,6 +11316,9 @@ msgstr ""
msgid "In order to tailor your experience with GitLab we<br>would like to know a bit more about you."
msgstr ""
+msgid "In progress"
+msgstr ""
+
msgid "In the next step, you'll be able to select the projects you want to import."
msgstr ""
@@ -11392,15 +11666,15 @@ msgstr ""
msgid "Issues"
msgstr ""
-msgid "Issues / Merge Requests"
-msgstr ""
-
msgid "Issues Analytics"
msgstr ""
msgid "Issues Rate Limits"
msgstr ""
+msgid "Issues and Merge Requests"
+msgstr ""
+
msgid "Issues can be bugs, tasks or ideas to be discussed. Also, issues are searchable and filterable."
msgstr ""
@@ -11422,10 +11696,10 @@ msgstr ""
msgid "IssuesAnalytics|Avg/Month:"
msgstr ""
-msgid "IssuesAnalytics|Issues created"
+msgid "IssuesAnalytics|Issues opened"
msgstr ""
-msgid "IssuesAnalytics|Issues created per month"
+msgid "IssuesAnalytics|Issues opened per month"
msgstr ""
msgid "IssuesAnalytics|Last 12 months"
@@ -11479,6 +11753,9 @@ msgstr ""
msgid "Jira integration not configured."
msgstr ""
+msgid "Jira project: %{importProject}"
+msgstr ""
+
msgid "JiraService|Events for %{noteable_model_name} are disabled."
msgstr ""
@@ -11868,6 +12145,9 @@ msgstr ""
msgid "Lead"
msgstr ""
+msgid "Lead Time"
+msgstr ""
+
msgid "Learn GitLab"
msgstr ""
@@ -11880,6 +12160,9 @@ msgstr ""
msgid "Learn how to %{no_packages_link_start}publish and share your packages%{no_packages_link_end} with GitLab."
msgstr ""
+msgid "Learn how to enable synchronization"
+msgstr ""
+
msgid "Learn more"
msgstr ""
@@ -12211,9 +12494,6 @@ msgstr ""
msgid "Live preview"
msgstr ""
-msgid "Load more vulnerabilities"
-msgstr ""
-
msgid "Loading"
msgstr ""
@@ -12364,9 +12644,6 @@ msgstr ""
msgid "Manage"
msgstr ""
-msgid "Manage Git repositories with fine-grained access controls that keep your code secure. Perform code reviews and enhance collaboration with merge requests. Each project can also have an issue tracker and a wiki."
-msgstr ""
-
msgid "Manage Web IDE features"
msgstr ""
@@ -12397,6 +12674,9 @@ msgstr ""
msgid "Manage your license"
msgstr ""
+msgid "Managed Account"
+msgstr ""
+
msgid "Manifest"
msgstr ""
@@ -12661,6 +12941,9 @@ msgstr ""
msgid "Merge Requests in Review"
msgstr ""
+msgid "Merge automatically (%{strategy})"
+msgstr ""
+
msgid "Merge commit message"
msgstr ""
@@ -12814,6 +13097,12 @@ msgstr ""
msgid "Merged branches are being deleted. This can take some time depending on the number of branches. Please refresh the page to see changes."
msgstr ""
+msgid "Merged this merge request."
+msgstr ""
+
+msgid "Merges this merge request immediately."
+msgstr ""
+
msgid "Merges this merge request when the pipeline succeeds."
msgstr ""
@@ -12921,12 +13210,15 @@ msgid_plural "Metrics|Edit metrics"
msgstr[0] ""
msgstr[1] ""
-msgid "Metrics|Environment"
+msgid "Metrics|Expand panel"
msgstr ""
msgid "Metrics|For grouping similar metrics"
msgstr ""
+msgid "Metrics|Go back (Esc)"
+msgstr ""
+
msgid "Metrics|Invalid time range, please verify."
msgstr ""
@@ -12957,9 +13249,6 @@ msgstr ""
msgid "Metrics|Refresh dashboard"
msgstr ""
-msgid "Metrics|Show last"
-msgstr ""
-
msgid "Metrics|There was an error creating the dashboard."
msgstr ""
@@ -13038,6 +13327,9 @@ msgstr ""
msgid "Microsoft Azure"
msgstr ""
+msgid "Middleman project with Static Site Editor support"
+msgstr ""
+
msgid "Migrated %{success_count}/%{total_count} files."
msgstr ""
@@ -13483,10 +13775,10 @@ msgstr ""
msgid "Next"
msgstr ""
-msgid "Next file in diff (MRs only)"
+msgid "Next file in diff"
msgstr ""
-msgid "Next unresolved discussion (MRs only)"
+msgid "Next unresolved discussion"
msgstr ""
msgid "Nickname"
@@ -13546,6 +13838,9 @@ msgstr ""
msgid "No changes between %{ref_start}%{source_branch}%{ref_end} and %{ref_start}%{target_branch}%{ref_end}"
msgstr ""
+msgid "No child epics match applied filters"
+msgstr ""
+
msgid "No connection could be made to a Gitaly Server, please check your logs!"
msgstr ""
@@ -13684,15 +13979,6 @@ msgstr ""
msgid "No thanks, don't show this again"
msgstr ""
-msgid "No vulnerabilities found for this group"
-msgstr ""
-
-msgid "No vulnerabilities found for this pipeline"
-msgstr ""
-
-msgid "No vulnerabilities found for this project"
-msgstr ""
-
msgid "No vulnerabilities present"
msgstr ""
@@ -13816,6 +14102,9 @@ msgstr ""
msgid "Nothing to preview."
msgstr ""
+msgid "Nothing to synchronize"
+msgstr ""
+
msgid "Notification events"
msgstr ""
@@ -13987,6 +14276,9 @@ msgstr ""
msgid "Once removed, the fork relationship cannot be restored and you will no longer be able to send merge requests to the source."
msgstr ""
+msgid "Once the exported file is ready you can download it from this page."
+msgstr ""
+
msgid "Once the exported file is ready, you will receive a notification email with a download link, or you can download it from this page."
msgstr ""
@@ -14088,9 +14380,6 @@ msgstr ""
msgid "Open sidebar"
msgstr ""
-msgid "Open source software to collaborate on code"
-msgstr ""
-
msgid "Open: %{openIssuesCount}"
msgstr ""
@@ -14193,6 +14482,9 @@ msgstr ""
msgid "OutdatedBrowser|You can provide feedback %{feedback_link_start}on this issue%{feedback_link_end} or via your usual support channels."
msgstr ""
+msgid "Overridden"
+msgstr ""
+
msgid "Overview"
msgstr ""
@@ -14244,6 +14536,9 @@ msgstr ""
msgid "PackageRegistry|Conan Command"
msgstr ""
+msgid "PackageRegistry|Copy .pypirc content"
+msgstr ""
+
msgid "PackageRegistry|Copy Conan Command"
msgstr ""
@@ -14265,6 +14560,9 @@ msgstr ""
msgid "PackageRegistry|Copy NuGet Setup Command"
msgstr ""
+msgid "PackageRegistry|Copy Pip command"
+msgstr ""
+
msgid "PackageRegistry|Copy and paste this inside your %{codeStart}pom.xml%{codeEnd} %{codeStart}dependencies%{codeEnd} block."
msgstr ""
@@ -14298,6 +14596,12 @@ msgstr ""
msgid "PackageRegistry|For more information on the NuGet registry, %{linkStart}see the documentation%{linkEnd}."
msgstr ""
+msgid "PackageRegistry|For more information on the PyPi registry, %{linkStart}see the documentation%{linkEnd}."
+msgstr ""
+
+msgid "PackageRegistry|If you haven't already done so, you will need to add the below to your %{codeStart}.pypirc%{codeEnd} file."
+msgstr ""
+
msgid "PackageRegistry|If you haven't already done so, you will need to add the below to your %{codeStart}pom.xml%{codeEnd} file."
msgstr ""
@@ -14307,6 +14611,9 @@ msgstr ""
msgid "PackageRegistry|Learn how to %{noPackagesLinkStart}publish and share your packages%{noPackagesLinkEnd} with GitLab."
msgstr ""
+msgid "PackageRegistry|Manually Published"
+msgstr ""
+
msgid "PackageRegistry|Maven"
msgstr ""
@@ -14325,12 +14632,18 @@ msgstr ""
msgid "PackageRegistry|NuGet Command"
msgstr ""
+msgid "PackageRegistry|Pip Command"
+msgstr ""
+
msgid "PackageRegistry|Pipeline %{linkStart}%{linkEnd} triggered %{timestamp} by %{author}"
msgstr ""
msgid "PackageRegistry|Published to the repository at %{timestamp}"
msgstr ""
+msgid "PackageRegistry|PyPi"
+msgstr ""
+
msgid "PackageRegistry|Registry Setup"
msgstr ""
@@ -14367,6 +14680,9 @@ msgstr ""
msgid "PackageRegistry|npm"
msgstr ""
+msgid "PackageRegistry|published by %{author}"
+msgstr ""
+
msgid "PackageRegistry|yarn"
msgstr ""
@@ -14382,6 +14698,9 @@ msgstr ""
msgid "PackageType|NuGet"
msgstr ""
+msgid "PackageType|PyPi"
+msgstr ""
+
msgid "Packages"
msgstr ""
@@ -14475,6 +14794,9 @@ msgstr ""
msgid "Past due"
msgstr ""
+msgid "Paste a machine public key here. Read more about how to generate it"
+msgstr ""
+
msgid "Paste a machine public key here. Read more about how to generate it %{link_start}here%{link_end}"
msgstr ""
@@ -14490,9 +14812,6 @@ msgstr ""
msgid "Path"
msgstr ""
-msgid "Path, transfer, remove"
-msgstr ""
-
msgid "Path:"
msgstr ""
@@ -14523,7 +14842,7 @@ msgstr ""
msgid "Percentage"
msgstr ""
-msgid "Perform advanced options such as changing path, transferring, or removing the group."
+msgid "Perform advanced options such as changing path, transferring, exporting, or removing the group."
msgstr ""
msgid "Perform common operations on GitLab project"
@@ -14604,9 +14923,6 @@ msgstr ""
msgid "Pipeline minutes quota"
msgstr ""
-msgid "Pipeline quota"
-msgstr ""
-
msgid "Pipeline subscriptions"
msgstr ""
@@ -14616,6 +14932,9 @@ msgstr ""
msgid "Pipeline: %{status}"
msgstr ""
+msgid "PipelineCharts|CI / CD Analytics"
+msgstr ""
+
msgid "PipelineCharts|Failed:"
msgstr ""
@@ -14694,15 +15013,6 @@ msgstr ""
msgid "Pipelines settings for '%{project_name}' were successfully updated."
msgstr ""
-msgid "Pipelines| to purchase more minutes."
-msgstr ""
-
-msgid "Pipelines|%{namespace_name} has exceeded its pipeline minutes quota."
-msgstr ""
-
-msgid "Pipelines|%{namespace_name} has less than %{notification_level}%% of CI minutes available."
-msgstr ""
-
msgid "Pipelines|API"
msgstr ""
@@ -14724,10 +15034,13 @@ msgstr ""
msgid "Pipelines|Get started with Pipelines"
msgstr ""
-msgid "Pipelines|Loading Pipelines"
+msgid "Pipelines|Group %{namespace_name} has %{percentage}%% or less Shared Runner Pipeline minutes remaining. Once it runs out, no new jobs or pipelines in its projects will run."
msgstr ""
-msgid "Pipelines|Pipelines will not run anymore on shared Runners."
+msgid "Pipelines|Group %{namespace_name} has exceeded its pipeline minutes quota. Unless you buy additional pipeline minutes, no new jobs or pipelines in its projects will run."
+msgstr ""
+
+msgid "Pipelines|Loading Pipelines"
msgstr ""
msgid "Pipelines|Project cache successfully reset."
@@ -14958,6 +15271,9 @@ msgstr ""
msgid "Please select"
msgstr ""
+msgid "Please select a Jira project"
+msgstr ""
+
msgid "Please select a country"
msgstr ""
@@ -15003,6 +15319,9 @@ msgstr ""
msgid "Please wait while we import the repository for you. Refresh at will."
msgstr ""
+msgid "Plugins directory is deprecated and will be removed in 14.0. Please move this file into /file_hooks directory."
+msgstr ""
+
msgid "Pod does not exist"
msgstr ""
@@ -15120,7 +15439,7 @@ msgstr ""
msgid "Prevent users from changing their profile name"
msgstr ""
-msgid "Prevent users from modifing merge request approvers list"
+msgid "Prevent users from modifying merge request approvers list"
msgstr ""
msgid "Prevent users from performing write operations on GitLab while performing maintenance."
@@ -15141,10 +15460,10 @@ msgstr ""
msgid "Previous Artifacts"
msgstr ""
-msgid "Previous file in diff (MRs only)"
+msgid "Previous file in diff"
msgstr ""
-msgid "Previous unresolved discussion (MRs only)"
+msgid "Previous unresolved discussion"
msgstr ""
msgid "Primary"
@@ -15177,6 +15496,9 @@ msgstr ""
msgid "Private profile"
msgstr ""
+msgid "Private projects Minutes cost factor"
+msgstr ""
+
msgid "Private projects can be created in your personal namespace with:"
msgstr ""
@@ -16005,6 +16327,9 @@ msgstr ""
msgid "ProjectSettings|With GitLab Pages you can host your static websites on GitLab"
msgstr ""
+msgid "ProjectSettings|With Metrics Dashboard you can visualize this project performance metrics"
+msgstr ""
+
msgid "ProjectTemplates|.NET Core"
msgstr ""
@@ -16068,6 +16393,9 @@ msgstr ""
msgid "ProjectTemplates|Spring"
msgstr ""
+msgid "ProjectTemplates|Static Site Editor/Middleman"
+msgstr ""
+
msgid "ProjectTemplates|iOS (Swift)"
msgstr ""
@@ -16287,13 +16615,13 @@ msgstr ""
msgid "PrometheusService|Prometheus is being automatically managed on your clusters"
msgstr ""
-msgid "PrometheusService|These metrics will only be monitored after your first deployment to an environment"
+msgid "PrometheusService|Select the Active checkbox to override the Auto Configuration with custom settings. If unchecked, Auto Configuration settings are used."
msgstr ""
-msgid "PrometheusService|Time-series monitoring service"
+msgid "PrometheusService|These metrics will only be monitored after your first deployment to an environment"
msgstr ""
-msgid "PrometheusService|To enable manual configuration, uninstall Prometheus from your clusters"
+msgid "PrometheusService|Time-series monitoring service"
msgstr ""
msgid "PrometheusService|To enable the installation of Prometheus on your clusters, deactivate the manual configuration below"
@@ -16479,6 +16807,9 @@ msgstr ""
msgid "Protip:"
msgstr ""
+msgid "Protocol"
+msgstr ""
+
msgid "Provider"
msgstr ""
@@ -16503,6 +16834,9 @@ msgstr ""
msgid "Public pipelines"
msgstr ""
+msgid "Public projects Minutes cost factor"
+msgstr ""
+
msgid "Pull"
msgstr ""
@@ -16703,6 +17037,9 @@ msgid_plural "Refreshing in %d seconds to show the updated status..."
msgstr[0] ""
msgstr[1] ""
+msgid "Regenerate export"
+msgstr ""
+
msgid "Regenerate instance ID"
msgstr ""
@@ -17080,9 +17417,29 @@ msgstr ""
msgid "Reports|%{combinedString} and %{resolvedString}"
msgstr ""
+msgid "Reports|Accessibility scanning detected %d issue for the source branch only"
+msgid_plural "Reports|Accessibility scanning detected %d issues for the source branch only"
+msgstr[0] ""
+msgstr[1] ""
+
+msgid "Reports|Accessibility scanning detected no issues for the source branch only"
+msgstr ""
+
+msgid "Reports|Accessibility scanning failed loading results"
+msgstr ""
+
+msgid "Reports|Accessibility scanning results are being parsed"
+msgstr ""
+
msgid "Reports|Actions"
msgstr ""
+msgid "Reports|An error occured while loading report"
+msgstr ""
+
+msgid "Reports|An error occurred while loading %{name} results"
+msgstr ""
+
msgid "Reports|Class"
msgstr ""
@@ -17188,7 +17545,7 @@ msgstr ""
msgid "Requested %{time_ago}"
msgstr ""
-msgid "Requested design version does not exist"
+msgid "Requested design version does not exist."
msgstr ""
msgid "Requested states are invalid"
@@ -17372,6 +17729,9 @@ msgstr ""
msgid "Resync"
msgstr ""
+msgid "Resync all"
+msgstr ""
+
msgid "Resync all %{replicableType}"
msgstr ""
@@ -17440,6 +17800,9 @@ msgstr ""
msgid "Revoked personal access token %{personal_access_token_name}!"
msgstr ""
+msgid "Revoked project access token %{project_access_token_name}!"
+msgstr ""
+
msgid "RightSidebar|adding a"
msgstr ""
@@ -17488,6 +17851,9 @@ msgstr ""
msgid "Runner token"
msgstr ""
+msgid "Runner tokens"
+msgstr ""
+
msgid "Runner was not updated."
msgstr ""
@@ -17542,6 +17908,9 @@ msgstr ""
msgid "SAML SSO for %{group_name}"
msgstr ""
+msgid "SAML discovery tokens"
+msgstr ""
+
msgid "SAML for %{group_name}"
msgstr ""
@@ -17626,12 +17995,18 @@ msgstr ""
msgid "Scheduled"
msgstr ""
+msgid "Scheduled to merge this merge request (%{strategy})."
+msgstr ""
+
msgid "Scheduled to merge this merge request when the pipeline succeeds."
msgstr ""
msgid "Schedules"
msgstr ""
+msgid "Schedules to merge this merge request (%{strategy})."
+msgstr ""
+
msgid "Scheduling"
msgstr ""
@@ -17689,6 +18064,9 @@ msgstr ""
msgid "Search branches and tags"
msgstr ""
+msgid "Search by author"
+msgstr ""
+
msgid "Search files"
msgstr ""
@@ -17834,11 +18212,6 @@ msgid_plural "SearchResults|snippets"
msgstr[0] ""
msgstr[1] ""
-msgid "SearchResults|snippet result"
-msgid_plural "SearchResults|snippet results"
-msgstr[0] ""
-msgstr[1] ""
-
msgid "SearchResults|user"
msgid_plural "SearchResults|users"
msgstr[0] ""
@@ -17849,6 +18222,9 @@ msgid_plural "SearchResults|wiki results"
msgstr[0] ""
msgstr[1] ""
+msgid "Searching by both author and message is currently not supported."
+msgstr ""
+
msgid "Seat Link"
msgstr ""
@@ -17876,184 +18252,232 @@ msgstr ""
msgid "Security Dashboard"
msgstr ""
-msgid "Security Dashboard|Error fetching the vulnerability counts. Please check your network connection and try again."
+msgid "Security configuration help link"
+msgstr ""
+
+msgid "Security dashboard"
msgstr ""
-msgid "Security Dashboard|Error fetching the vulnerability list. Please check your network connection and try again."
+msgid "Security report is out of date. Please update your branch with the latest changes from the target branch (%{targetBranchName})"
msgstr ""
-msgid "Security Dashboard|Issue Created"
+msgid "Security report is out of date. Run %{newPipelineLinkStart}a new pipeline%{newPipelineLinkEnd} for the target branch (%{targetBranchName})"
msgstr ""
-msgid "Security Reports|Comment added to '%{vulnerabilityName}'"
+msgid "SecurityConfiguration|Configured"
msgstr ""
-msgid "Security Reports|Comment deleted on '%{vulnerabilityName}'"
+msgid "SecurityConfiguration|Feature"
msgstr ""
-msgid "Security Reports|Comment edited on '%{vulnerabilityName}'"
+msgid "SecurityConfiguration|Feature documentation for %{featureName}"
msgstr ""
-msgid "Security Reports|Create issue"
+msgid "SecurityConfiguration|Not yet configured"
msgstr ""
-msgid "Security Reports|Dismiss vulnerability"
+msgid "SecurityConfiguration|Secure features"
msgstr ""
-msgid "Security Reports|Dismissed '%{vulnerabilityName}'"
+msgid "SecurityConfiguration|Status"
msgstr ""
-msgid "Security Reports|Dismissed '%{vulnerabilityName}'. Turn off the hide dismissed toggle to view."
+msgid "SecurityReports|%{firstProject} and %{secondProject}"
msgstr ""
-msgid "Security Reports|Either you don't have permission to view this dashboard or the dashboard has not been setup. Please check your permission settings with your administrator or check your dashboard configurations to proceed."
+msgid "SecurityReports|%{firstProject}, %{secondProject}, and %{rest}"
msgstr ""
-msgid "Security Reports|Learn more about setting up your dashboard"
+msgid "SecurityReports|Add a project to your dashboard"
msgstr ""
-msgid "Security Reports|More info"
+msgid "SecurityReports|Add or remove projects from your dashboard"
msgstr ""
-msgid "Security Reports|Oops, something doesn't seem right."
+msgid "SecurityReports|Add projects"
msgstr ""
-msgid "Security Reports|Security reports can only be accessed by authorized users."
+msgid "SecurityReports|Comment added to '%{vulnerabilityName}'"
msgstr ""
-msgid "Security Reports|There was an error adding the comment."
+msgid "SecurityReports|Comment deleted on '%{vulnerabilityName}'"
msgstr ""
-msgid "Security Reports|There was an error creating the issue."
+msgid "SecurityReports|Comment edited on '%{vulnerabilityName}'"
msgstr ""
-msgid "Security Reports|There was an error creating the merge request."
+msgid "SecurityReports|Create issue"
msgstr ""
-msgid "Security Reports|There was an error deleting the comment."
+msgid "SecurityReports|Dismiss Selected"
msgstr ""
-msgid "Security Reports|There was an error dismissing the vulnerabilities."
+msgid "SecurityReports|Dismiss vulnerability"
msgstr ""
-msgid "Security Reports|There was an error dismissing the vulnerability."
+msgid "SecurityReports|Dismissed '%{vulnerabilityName}'"
msgstr ""
-msgid "Security Reports|There was an error reverting the dismissal."
+msgid "SecurityReports|Dismissed '%{vulnerabilityName}'. Turn off the hide dismissed toggle to view."
msgstr ""
-msgid "Security Reports|There was an error reverting this dismissal."
+msgid "SecurityReports|Each vulnerability now has a unique page that can be directly linked to, shared, referenced, and tracked as the single source of truth. Vulnerability occurrences also persist across scanner runs, which improves tracking and visibility and reduces duplicates between scans."
msgstr ""
-msgid "Security Reports|Undo dismiss"
+msgid "SecurityReports|Edit dashboard"
msgstr ""
-msgid "Security Reports|You do not have sufficient permissions to access this report"
+msgid "SecurityReports|Either you don't have permission to view this dashboard or the dashboard has not been setup. Please check your permission settings with your administrator or check your dashboard configurations to proceed."
msgstr ""
-msgid "Security Reports|You must sign in as an authorized user to see this report"
+msgid "SecurityReports|Error fetching the vulnerability counts. Please check your network connection and try again."
msgstr ""
-msgid "Security configuration help link"
+msgid "SecurityReports|Error fetching the vulnerability list. Please check your network connection and try again."
msgstr ""
-msgid "Security dashboard"
+msgid "SecurityReports|False positive"
msgstr ""
-msgid "Security report is out of date. Please update your branch with the latest changes from the target branch (%{targetBranchName})"
+msgid "SecurityReports|Hide dismissed"
msgstr ""
-msgid "Security report is out of date. Run %{newPipelineLinkStart}a new pipeline%{newPipelineLinkEnd} for the target branch (%{targetBranchName})"
+msgid "SecurityReports|Introducing standalone vulnerabilities"
msgstr ""
-msgid "SecurityConfiguration|Configured"
+msgid "SecurityReports|Issue Created"
msgstr ""
-msgid "SecurityConfiguration|Feature"
+msgid "SecurityReports|Learn More"
msgstr ""
-msgid "SecurityConfiguration|Feature documentation for %{featureName}"
+msgid "SecurityReports|Learn more about setting up your dashboard"
msgstr ""
-msgid "SecurityConfiguration|Not yet configured"
+msgid "SecurityReports|Load more vulnerabilities"
msgstr ""
-msgid "SecurityConfiguration|Secure features"
+msgid "SecurityReports|Monitor vulnerabilities in your code"
msgstr ""
-msgid "SecurityConfiguration|Status"
+msgid "SecurityReports|More info"
msgstr ""
-msgid "SecurityDashboard|%{firstProject} and %{secondProject}"
+msgid "SecurityReports|More information"
msgstr ""
-msgid "SecurityDashboard|%{firstProject}, %{secondProject}, and %{rest}"
+msgid "SecurityReports|No vulnerabilities found for dashboard"
msgstr ""
-msgid "SecurityDashboard|Add a project to your dashboard"
+msgid "SecurityReports|No vulnerabilities found for this group"
msgstr ""
-msgid "SecurityDashboard|Add or remove projects from your dashboard"
+msgid "SecurityReports|No vulnerabilities found for this pipeline"
msgstr ""
-msgid "SecurityDashboard|Add projects"
+msgid "SecurityReports|No vulnerabilities found for this project"
msgstr ""
-msgid "SecurityDashboard|Edit dashboard"
+msgid "SecurityReports|Oops, something doesn't seem right."
msgstr ""
-msgid "SecurityDashboard|Hide dismissed"
+msgid "SecurityReports|Pipeline %{pipelineLink} triggered %{timeago} by %{user}"
msgstr ""
-msgid "SecurityDashboard|Monitor vulnerabilities in your code"
+msgid "SecurityReports|Project"
msgstr ""
-msgid "SecurityDashboard|More information"
+msgid "SecurityReports|Projects added"
msgstr ""
-msgid "SecurityDashboard|No vulnerabilities found for dashboard"
+msgid "SecurityReports|Remove project from dashboard"
msgstr ""
-msgid "SecurityDashboard|Pipeline %{pipelineLink} triggered %{timeago} by %{user}"
+msgid "SecurityReports|Report type"
msgstr ""
-msgid "SecurityDashboard|Project"
+msgid "SecurityReports|Return to dashboard"
msgstr ""
-msgid "SecurityDashboard|Projects added"
+msgid "SecurityReports|Security Dashboard"
msgstr ""
-msgid "SecurityDashboard|Remove project from dashboard"
+msgid "SecurityReports|Security reports can only be accessed by authorized users."
msgstr ""
-msgid "SecurityDashboard|Report type"
+msgid "SecurityReports|Select a project to add by using the project search field above."
msgstr ""
-msgid "SecurityDashboard|Return to dashboard"
+msgid "SecurityReports|Select a reason"
msgstr ""
-msgid "SecurityDashboard|Security Dashboard"
+msgid "SecurityReports|Severity"
msgstr ""
-msgid "SecurityDashboard|Select a project to add by using the project search field above."
+msgid "SecurityReports|Status"
msgstr ""
-msgid "SecurityDashboard|Severity"
+msgid "SecurityReports|The security dashboard displays the latest security findings for projects you wish to monitor. Select \"Edit dashboard\" to add and remove projects."
msgstr ""
-msgid "SecurityDashboard|Status"
+msgid "SecurityReports|The security dashboard displays the latest security report. Use it to find and fix vulnerabilities."
msgstr ""
-msgid "SecurityDashboard|The security dashboard displays the latest security findings for projects you wish to monitor. Select \"Edit dashboard\" to add and remove projects."
+msgid "SecurityReports|There was an error adding the comment."
msgstr ""
-msgid "SecurityDashboard|The security dashboard displays the latest security report. Use it to find and fix vulnerabilities."
+msgid "SecurityReports|There was an error creating the issue."
msgstr ""
-msgid "SecurityDashboard|There was an error while generating the report."
+msgid "SecurityReports|There was an error creating the merge request."
msgstr ""
-msgid "SecurityDashboard|Unable to add %{invalidProjects}"
+msgid "SecurityReports|There was an error deleting the comment."
+msgstr ""
+
+msgid "SecurityReports|There was an error dismissing the vulnerabilities."
+msgstr ""
+
+msgid "SecurityReports|There was an error dismissing the vulnerability."
+msgstr ""
+
+msgid "SecurityReports|There was an error reverting the dismissal."
+msgstr ""
+
+msgid "SecurityReports|There was an error reverting this dismissal."
+msgstr ""
+
+msgid "SecurityReports|There was an error while generating the report."
+msgstr ""
+
+msgid "SecurityReports|Unable to add %{invalidProjects}"
+msgstr ""
+
+msgid "SecurityReports|Undo dismiss"
+msgstr ""
+
+msgid "SecurityReports|While it's rare to have no vulnerabilities for your group, it can happen. In any event, we ask that you double check your settings to make sure you've set up your dashboard correctly."
+msgstr ""
+
+msgid "SecurityReports|While it's rare to have no vulnerabilities for your pipeline, it can happen. In any event, we ask that you double check your settings to make sure all security scanning jobs have passed successfully."
+msgstr ""
+
+msgid "SecurityReports|While it's rare to have no vulnerabilities for your project, it can happen. In any event, we ask that you double check your settings to make sure you've set up your dashboard correctly."
+msgstr ""
+
+msgid "SecurityReports|While it's rare to have no vulnerabilities, it can happen. In any event, we ask that you please double check your settings to make sure you've set up your dashboard correctly."
+msgstr ""
+
+msgid "SecurityReports|Won't fix / Accept risk"
+msgstr ""
+
+msgid "SecurityReports|You do not have sufficient permissions to access this report"
+msgstr ""
+
+msgid "SecurityReports|You must sign in as an authorized user to see this report"
+msgstr ""
+
+msgid "SecurityReports|[No reason]"
msgstr ""
msgid "See GitLab's %{password_policy_guidelines}"
@@ -18065,6 +18489,9 @@ msgstr ""
msgid "See the affected projects in the GitLab admin panel"
msgstr ""
+msgid "See what's new at GitLab"
+msgstr ""
+
msgid "Select"
msgstr ""
@@ -18470,9 +18897,6 @@ msgstr ""
msgid "Set up Jira Integration"
msgstr ""
-msgid "Set up Jira Integration illustration"
-msgstr ""
-
msgid "Set up a %{type} Runner automatically"
msgstr ""
@@ -18557,6 +18981,9 @@ msgstr ""
msgid "Severity: %{severity}"
msgstr ""
+msgid "Shards (%{shards})"
+msgstr ""
+
msgid "Shards to synchronize"
msgstr ""
@@ -18871,6 +19298,9 @@ msgstr ""
msgid "Snowplow"
msgstr ""
+msgid "Some child epics may be hidden due to applied filters"
+msgstr ""
+
msgid "Some email servers do not support overriding the email sender name. Enable this option to include the name of the author of the issue, merge request or comment in the email body instead."
msgstr ""
@@ -19012,6 +19442,9 @@ msgstr ""
msgid "Something went wrong while updating your list settings"
msgstr ""
+msgid "Something went wrong with your automatic subscription renewal"
+msgstr ""
+
msgid "Something went wrong, unable to add %{project} to dashboard"
msgstr ""
@@ -19297,6 +19730,9 @@ msgstr ""
msgid "Stage removed"
msgstr ""
+msgid "Standard"
+msgstr ""
+
msgid "Star a label to make it a priority label. Order the prioritized labels to change their relative priority, by dragging."
msgstr ""
@@ -19420,6 +19856,9 @@ msgstr ""
msgid "Static Application Security Testing (SAST)"
msgstr ""
+msgid "StaticSiteEditor|An error occurred while submitting your changes."
+msgstr ""
+
msgid "StaticSiteEditor|Branch could not be created."
msgstr ""
@@ -19429,18 +19868,30 @@ msgstr ""
msgid "StaticSiteEditor|Could not create merge request."
msgstr ""
+msgid "StaticSiteEditor|Incompatible file content"
+msgstr ""
+
msgid "StaticSiteEditor|Return to site"
msgstr ""
+msgid "StaticSiteEditor|Static site editor"
+msgstr ""
+
msgid "StaticSiteEditor|Success!"
msgstr ""
msgid "StaticSiteEditor|Summary of changes"
msgstr ""
+msgid "StaticSiteEditor|The Static Site Editor is currently configured to only edit Markdown content on pages generated from Middleman. Visit the documentation to learn more about configuring your site to use the Static Site Editor."
+msgstr ""
+
msgid "StaticSiteEditor|Update %{sourcePath} file"
msgstr ""
+msgid "StaticSiteEditor|View documentation"
+msgstr ""
+
msgid "StaticSiteEditor|View merge request"
msgstr ""
@@ -19498,6 +19949,12 @@ msgstr ""
msgid "StatusPage|Status page"
msgstr ""
+msgid "StatusPage|Status page URL"
+msgstr ""
+
+msgid "StatusPage|Status page frontend documentation"
+msgstr ""
+
msgid "StatusPage|To publish incidents to an external status page, GitLab will store a JSON file in your Amazon S3 account in a location accessible to your external status page service. Make sure to also set up %{docsLink}"
msgstr ""
@@ -19846,6 +20303,12 @@ msgstr ""
msgid "Synced"
msgstr ""
+msgid "Synchronization disabled"
+msgstr ""
+
+msgid "Synchronization of container repositories is disabled."
+msgstr ""
+
msgid "System"
msgstr ""
@@ -19999,6 +20462,9 @@ msgstr ""
msgid "Telephone number"
msgstr ""
+msgid "Telephone number (Optional)"
+msgstr ""
+
msgid "Template"
msgstr ""
@@ -20305,6 +20771,9 @@ msgstr ""
msgid "The license was successfully uploaded and is now active. You can see the details below."
msgstr ""
+msgid "The license was successfully uploaded and will be active from %{starts_at}. You can see the details below."
+msgstr ""
+
msgid "The maximum file size allowed is %{size}."
msgstr ""
@@ -20626,6 +21095,9 @@ msgstr ""
msgid "There was an error getting the epic participants."
msgstr ""
+msgid "There was an error importing the Jira project."
+msgstr ""
+
msgid "There was an error loading users activity calendar."
msgstr ""
@@ -20641,7 +21113,7 @@ msgstr ""
msgid "There was an error resetting user pipeline minutes."
msgstr ""
-msgid "There was an error saving this Geo Node"
+msgid "There was an error saving this Geo Node."
msgstr ""
msgid "There was an error saving your changes."
@@ -20704,6 +21176,9 @@ msgstr ""
msgid "They can be managed using the %{link}."
msgstr ""
+msgid "Third Party Advisory Link"
+msgstr ""
+
msgid "Third party offers"
msgstr ""
@@ -20740,6 +21215,9 @@ msgstr ""
msgid "This application will be able to:"
msgstr ""
+msgid "This attachment has been truncated to avoid exceeding the maximum allowed attachment size of 15MB. %{written_count} of %{issues_count} issues have been included. Consider re-exporting with a narrower selection of issues."
+msgstr ""
+
msgid "This block is self-referential"
msgstr ""
@@ -20881,6 +21359,12 @@ msgstr ""
msgid "This issue is confidential"
msgstr ""
+msgid "This issue is currently blocked by the following issues: %{issues}."
+msgstr ""
+
+msgid "This issue is in a child epic of the filtered epic"
+msgstr ""
+
msgid "This issue is locked."
msgstr ""
@@ -21079,7 +21563,10 @@ msgstr ""
msgid "This user will be the author of all events in the activity feed that are the result of an update, like new branches being created or new commits being pushed to existing branches. Upon creation or when reassigning you can only assign yourself to be the mirror user."
msgstr ""
-msgid "This variable can not be masked"
+msgid "This variable can not be masked."
+msgstr ""
+
+msgid "This variable does not match the expected pattern."
msgstr ""
msgid "This will help us personalize your onboarding experience."
@@ -21109,10 +21596,10 @@ msgstr ""
msgid "ThreatMonitoring|Application firewall not detected"
msgstr ""
-msgid "ThreatMonitoring|Container Network Policy"
+msgid "ThreatMonitoring|Container Network Policies are not installed or have been disabled. To view this data, ensure your Network Policies are installed and enabled for your cluster."
msgstr ""
-msgid "ThreatMonitoring|Container NetworkPolicies are not installed or has been disabled. To view this data, ensure you NetworkPolicies are installed and enabled for your cluster."
+msgid "ThreatMonitoring|Container Network Policy"
msgstr ""
msgid "ThreatMonitoring|Container NetworkPolicies not detected"
@@ -21145,7 +21632,7 @@ msgstr ""
msgid "ThreatMonitoring|Something went wrong, unable to fetch statistics"
msgstr ""
-msgid "ThreatMonitoring|The firewall is not installed or has been disabled. To view this data, ensure you firewall is installed and enabled for your cluster."
+msgid "ThreatMonitoring|The firewall is not installed or has been disabled. To view this data, ensure the web application firewall is installed and enabled for your cluster."
msgstr ""
msgid "ThreatMonitoring|The graph below is an overview of traffic coming to your application as tracked by the Web Application Firewall (WAF). View the docs for instructions on how to access the WAF logs to see what type of malicious traffic is trying to access your app. The docs link is also accessible by clicking the \"?\" icon next to the title below."
@@ -21214,6 +21701,9 @@ msgstr ""
msgid "Time in seconds GitLab will wait for a response from the external service. When the service does not respond in time, access will be denied."
msgstr ""
+msgid "Time of import: %{importTime}"
+msgstr ""
+
msgid "Time remaining"
msgstr ""
@@ -21398,7 +21888,7 @@ msgstr ""
msgid "Title:"
msgstr ""
-msgid "Titles and Filenames"
+msgid "Titles and Descriptions"
msgstr ""
msgid "To"
@@ -21605,7 +22095,7 @@ msgstr ""
msgid "Too many projects enabled. You will need to manage them via the console or the API."
msgstr ""
-msgid "Topics"
+msgid "Topics (optional)"
msgstr ""
msgid "Total"
@@ -21767,6 +22257,9 @@ msgstr ""
msgid "Try all GitLab has to offer for 30 days."
msgstr ""
+msgid "Try changing or removing filters."
+msgstr ""
+
msgid "Try to fork again"
msgstr ""
@@ -21860,6 +22353,9 @@ msgstr ""
msgid "Unable to connect to server: %{error}"
msgstr ""
+msgid "Unable to connect to the Jira instance. Please check your Jira integration configuration."
+msgstr ""
+
msgid "Unable to convert Kubernetes logs encoding to UTF-8"
msgstr ""
@@ -22803,6 +23299,9 @@ msgstr ""
msgid "View issue"
msgstr ""
+msgid "View issues"
+msgstr ""
+
msgid "View it on GitLab"
msgstr ""
@@ -22980,6 +23479,15 @@ msgstr ""
msgid "VulnerabilityManagement|Resolved %{timeago} by %{user}"
msgstr ""
+msgid "VulnerabilityManagement|Something went wrong while trying to delete the comment. Please try again later."
+msgstr ""
+
+msgid "VulnerabilityManagement|Something went wrong while trying to retrieve the vulnerability history. Please try again later."
+msgstr ""
+
+msgid "VulnerabilityManagement|Something went wrong while trying to save the comment. Please try again later."
+msgstr ""
+
msgid "VulnerabilityManagement|Something went wrong, could not create an issue."
msgstr ""
@@ -23010,6 +23518,9 @@ msgstr ""
msgid "VulnerabilityStatusTypes|Resolved"
msgstr ""
+msgid "Vulnerability|%{scannerName} (version %{scannerVersion})"
+msgstr ""
+
msgid "Vulnerability|Class"
msgstr ""
@@ -23043,7 +23554,10 @@ msgstr ""
msgid "Vulnerability|Project"
msgstr ""
-msgid "Vulnerability|Report Type"
+msgid "Vulnerability|Scanner Provider"
+msgstr ""
+
+msgid "Vulnerability|Scanner Type"
msgstr ""
msgid "Vulnerability|Severity"
@@ -23097,9 +23611,15 @@ msgstr ""
msgid "We sent you an email with reset password instructions"
msgstr ""
+msgid "We tried to automatically renew your %{strong}%{plan_name}%{strong_close} subscription for %{strong}%{namespace_name}%{strong_close} on %{expires_on} but something went wrong so your subscription was downgraded to the free plan. Don't worry, your data is safe. We suggest you check your payment method and get in touch with our support team (%{support_link}). They'll gladly help with your subscription renewal."
+msgstr ""
+
msgid "We want to be sure it is you, please confirm you are not a robot."
msgstr ""
+msgid "We will automatically renew your %{strong}%{plan_name}%{strong_close} subscription for %{strong}%{namespace_name}%{strong_close} on %{strong}%{expires_on}%{strong_close}. There's nothing that you need to do, we'll let you know when the renewal is complete. Need more seats, a higher plan or just want to review your payment method?"
+msgstr ""
+
msgid "We've found no vulnerabilities"
msgstr ""
@@ -23204,15 +23724,6 @@ msgstr ""
msgid "When:"
msgstr ""
-msgid "While it's rare to have no vulnerabilities for your group, it can happen. In any event, we ask that you double check your settings to make sure you've set up your dashboard correctly."
-msgstr ""
-
-msgid "While it's rare to have no vulnerabilities for your pipeline, it can happen. In any event, we ask that you double check your settings to make sure all security scanning jobs have passed successfully."
-msgstr ""
-
-msgid "While it's rare to have no vulnerabilities for your project, it can happen. In any event, we ask that you double check your settings to make sure you've set up your dashboard correctly."
-msgstr ""
-
msgid "While it's rare to have no vulnerabilities, it can happen. In any event, we ask that you please double check your settings to make sure you've set up your dashboard correctly."
msgstr ""
@@ -23243,9 +23754,6 @@ msgstr ""
msgid "Wiki"
msgstr ""
-msgid "Wiki pages"
-msgstr ""
-
msgid "Wiki was successfully updated."
msgstr ""
@@ -23429,6 +23937,9 @@ msgstr ""
msgid "Yes, add it"
msgstr ""
+msgid "Yes, close issue"
+msgstr ""
+
msgid "Yes, let me map Google Code users to full names or GitLab users."
msgstr ""
@@ -23624,6 +24135,9 @@ msgstr ""
msgid "You could not create a new trigger."
msgstr ""
+msgid "You didn't renew your %{strong}%{plan_name}%{strong_close} subscription for %{strong}%{namespace_name}%{strong_close} so it was downgraded to the free plan."
+msgstr ""
+
msgid "You didn't renew your %{strong}%{plan_name}%{strong_close} subscription so it was downgraded to the GitLab Core Plan."
msgstr ""
@@ -23690,6 +24204,9 @@ msgstr ""
msgid "You have declined the invitation to join %{label}."
msgstr ""
+msgid "You have imported from this project %{numberOfPreviousImportsForProject} times before. Each new import will create duplicate issues."
+msgstr ""
+
msgid "You have no permissions"
msgstr ""
@@ -23771,6 +24288,9 @@ msgstr ""
msgid "You need to upload a Google Takeout archive."
msgstr ""
+msgid "You reached %{usage_in_percent} of %{namespace_name}'s capacity (%{used_storage} of %{storage_limit})"
+msgstr ""
+
msgid "You tried to fork %{link_to_the_project} but it failed for the following reason:"
msgstr ""
@@ -23855,9 +24375,21 @@ msgstr ""
msgid "YouTube"
msgstr ""
+msgid "Your %{strong}%{plan_name}%{strong_close} subscription for %{strong}%{namespace_name}%{strong_close} will expire on %{strong}%{expires_on}%{strong_close}. After that, you will not to be able to create issues or merge requests as well as many other features."
+msgstr ""
+
msgid "Your %{strong}%{plan_name}%{strong_close} subscription will expire on %{strong}%{expires_on}%{strong_close}. After that, you will not to be able to create issues or merge requests as well as many other features."
msgstr ""
+msgid "Your CSV export has started. It will be emailed to %{email} when complete."
+msgstr ""
+
+msgid "Your CSV export of %{issues_count} from project %{project_link} has been added to this email as an attachment."
+msgstr ""
+
+msgid "Your CSV export of %{written_count} from project %{project_name} (%{project_url}) has been added to this email as an attachment."
+msgstr ""
+
msgid "Your Commit Email will be used for web based operations, such as edits and merges."
msgstr ""
@@ -23954,6 +24486,9 @@ msgstr ""
msgid "Your comment could not be updated! Please check your network connection and try again."
msgstr ""
+msgid "Your comment will be discarded."
+msgstr ""
+
msgid "Your custom stage '%{title}' was created"
msgstr ""
@@ -23993,6 +24528,9 @@ msgstr ""
msgid "Your new personal access token has been created."
msgstr ""
+msgid "Your new project access token has been created."
+msgstr ""
+
msgid "Your password isn't required to view this page. If a password or any other personal details are requested, please contact your administrator to report abuse."
msgstr ""
@@ -24011,12 +24549,18 @@ msgstr ""
msgid "Your request for access has been queued for review."
msgstr ""
+msgid "Your search didn't match any commits."
+msgstr ""
+
msgid "Your subscription expired!"
msgstr ""
msgid "Your subscription has been downgraded"
msgstr ""
+msgid "Your subscription will automatically renew in %{remaining_days}"
+msgstr ""
+
msgid "Your subscription will expire in %{remaining_days}"
msgstr ""
@@ -24055,7 +24599,7 @@ msgstr ""
msgid "allowed to fail"
msgstr ""
-msgid "already being used for another group or project milestone."
+msgid "already being used for another group or project %{timebox_name}."
msgstr ""
msgid "already has a \"created\" issue link"
@@ -24133,6 +24677,9 @@ msgstr ""
msgid "ciReport|%{linkStartTag}Learn more about SAST %{linkEndTag}"
msgstr ""
+msgid "ciReport|%{linkStartTag}Learn more about Secret Scanning %{linkEndTag}"
+msgstr ""
+
msgid "ciReport|%{linkStartTag}Learn more about codequality reports %{linkEndTag}"
msgstr ""
@@ -24296,6 +24843,12 @@ msgstr ""
msgid "ciReport|SAST"
msgstr ""
+msgid "ciReport|Secret scanning"
+msgstr ""
+
+msgid "ciReport|Secret scanning detects secrets and credentials vulnerabilities in your source code."
+msgstr ""
+
msgid "ciReport|Security scanning"
msgstr ""
@@ -24493,6 +25046,9 @@ msgstr ""
msgid "group"
msgstr ""
+msgid "groups"
+msgstr ""
+
msgid "has already been linked to another vulnerability"
msgstr ""
@@ -24656,9 +25212,6 @@ msgstr[1] ""
msgid "merged %{time_ago}"
msgstr ""
-msgid "milestone should belong either to a project or a group."
-msgstr ""
-
msgid "missing"
msgstr ""
@@ -24995,6 +25548,9 @@ msgstr ""
msgid "no contributions"
msgstr ""
+msgid "no expiration"
+msgstr ""
+
msgid "no one can merge"
msgstr ""
@@ -25047,6 +25603,9 @@ msgstr ""
msgid "pending removal"
msgstr ""
+msgid "per day"
+msgstr ""
+
msgid "pipeline"
msgstr ""
@@ -25075,6 +25634,12 @@ msgstr[1] ""
msgid "project avatar"
msgstr ""
+msgid "projects"
+msgstr ""
+
+msgid "push to your repository, create pipelines, create issues or add comments. To reduce storage capacity, delete unused repositories, artifacts, wikis, issues, and pipelines."
+msgstr ""
+
msgid "quick actions"
msgstr ""
@@ -25221,11 +25786,6 @@ msgstr ""
msgid "this document"
msgstr ""
-msgid "thread resolved"
-msgid_plural "threads resolved"
-msgstr[0] ""
-msgstr[1] ""
-
msgid "to help your contributors communicate effectively!"
msgstr ""
@@ -25280,6 +25840,9 @@ msgstr ""
msgid "view the blob"
msgstr ""
+msgid "vulnerability|Add a comment"
+msgstr ""
+
msgid "vulnerability|Add a comment or reason for dismissal"
msgstr ""
diff --git a/locale/ar_SA/gitlab.po b/locale/ar_SA/gitlab.po
index d83fd808ec8..64c1936c4a2 100644
--- a/locale/ar_SA/gitlab.po
+++ b/locale/ar_SA/gitlab.po
@@ -12,7 +12,7 @@ msgstr ""
"X-Crowdin-Project: gitlab-ee\n"
"X-Crowdin-Language: ar\n"
"X-Crowdin-File: /master/locale/gitlab.pot\n"
-"PO-Revision-Date: 2020-04-15 00:29\n"
+"PO-Revision-Date: 2020-05-05 21:31\n"
msgid " %{start} to %{end}"
msgstr ""
@@ -332,6 +332,15 @@ msgstr[3] ""
msgstr[4] ""
msgstr[5] ""
+msgid "%d unresolved thread"
+msgid_plural "%d unresolved threads"
+msgstr[0] ""
+msgstr[1] ""
+msgstr[2] ""
+msgstr[3] ""
+msgstr[4] ""
+msgstr[5] ""
+
msgid "%d vulnerability dismissed"
msgid_plural "%d vulnerabilities dismissed"
msgstr[0] ""
@@ -515,6 +524,9 @@ msgstr ""
msgid "%{mrText}, this issue will be closed automatically."
msgstr ""
+msgid "%{namespace_name} is now read-only. You cannot: %{base_message}"
+msgstr ""
+
msgid "%{name} contained %{resultsString}"
msgstr ""
@@ -527,6 +539,15 @@ msgstr ""
msgid "%{name}'s avatar"
msgstr ""
+msgid "%{no_of_days} day"
+msgid_plural "%{no_of_days} days"
+msgstr[0] ""
+msgstr[1] ""
+msgstr[2] ""
+msgstr[3] ""
+msgstr[4] ""
+msgstr[5] ""
+
msgid "%{number_commits_behind} commits behind %{default_branch}, %{number_commits_ahead} commits ahead"
msgstr ""
@@ -674,6 +695,9 @@ msgstr[5] ""
msgid "%{text} is available"
msgstr ""
+msgid "%{timebox_name} should belong either to a project or a group."
+msgstr ""
+
msgid "%{title} %{operator} %{threshold}"
msgstr ""
@@ -1025,12 +1049,18 @@ msgstr ""
msgid "<code>\"johnsmith@example.com\": \"johnsmith@example.com\"</code> will add \"By <a href=\"#\">johnsmith@example.com</a>\" to all issues and comments originally created by johnsmith@example.com. By default, the email address or username is masked to ensure the user's privacy. Use this option if you want to show the full email address."
msgstr ""
+msgid "<namespace / project>"
+msgstr ""
+
msgid "<no name set>"
msgstr ""
msgid "<no scopes selected>"
msgstr ""
+msgid "<project name>"
+msgstr ""
+
msgid "<strong>%{group_name}</strong> group members"
msgstr ""
@@ -1064,6 +1094,9 @@ msgstr ""
msgid "A basic page and serverless function that uses AWS Lambda, AWS API Gateway, and GitLab Pages"
msgstr ""
+msgid "A complete DevOps platform"
+msgstr ""
+
msgid "A default branch cannot be chosen for an empty project."
msgstr ""
@@ -1280,6 +1313,12 @@ msgstr ""
msgid "AccessTokens|reset it"
msgstr ""
+msgid "AccessibilityReport|Accessibility report artifact not found"
+msgstr ""
+
+msgid "AccessibilityReport|Failed to retrieve accessibility report"
+msgstr ""
+
msgid "AccessibilityReport|Learn More"
msgstr ""
@@ -1289,6 +1328,9 @@ msgstr ""
msgid "AccessibilityReport|New"
msgstr ""
+msgid "AccessibilityReport|The accessibility scanning found an error of the following type: %{code}"
+msgstr ""
+
msgid "Account"
msgstr ""
@@ -1535,6 +1577,9 @@ msgstr ""
msgid "Added at"
msgstr ""
+msgid "Added for this merge request"
+msgstr ""
+
msgid "Added in this version"
msgstr ""
@@ -1910,6 +1955,72 @@ msgstr[3] ""
msgstr[4] ""
msgstr[5] ""
+msgid "Alert Details"
+msgstr ""
+
+msgid "AlertManagement|Acknowledged"
+msgstr ""
+
+msgid "AlertManagement|Alert"
+msgstr ""
+
+msgid "AlertManagement|Authorize external service"
+msgstr ""
+
+msgid "AlertManagement|Display alerts from all your monitoring tools directly within GitLab. Streamline the investigation of your alerts and the escalation of alerts to incidents."
+msgstr ""
+
+msgid "AlertManagement|End time"
+msgstr ""
+
+msgid "AlertManagement|End time:"
+msgstr ""
+
+msgid "AlertManagement|Events"
+msgstr ""
+
+msgid "AlertManagement|Events:"
+msgstr ""
+
+msgid "AlertManagement|Full Alert Details"
+msgstr ""
+
+msgid "AlertManagement|More information"
+msgstr ""
+
+msgid "AlertManagement|No alerts available to display. If you think you're seeing this message in error, refresh the page."
+msgstr ""
+
+msgid "AlertManagement|No alerts to display."
+msgstr ""
+
+msgid "AlertManagement|Overview"
+msgstr ""
+
+msgid "AlertManagement|Resolved"
+msgstr ""
+
+msgid "AlertManagement|Severity"
+msgstr ""
+
+msgid "AlertManagement|Start time"
+msgstr ""
+
+msgid "AlertManagement|Start time:"
+msgstr ""
+
+msgid "AlertManagement|Status"
+msgstr ""
+
+msgid "AlertManagement|Surface alerts in GitLab"
+msgstr ""
+
+msgid "AlertManagement|There was an error displaying the alerts. Confirm your endpoint's configuration details to ensure alerts appear."
+msgstr ""
+
+msgid "AlertManagement|Triggered"
+msgstr ""
+
msgid "AlertService|%{linkStart}Learn more%{linkEnd} about configuring this endpoint to receive alerts."
msgstr ""
@@ -1979,6 +2090,9 @@ msgstr ""
msgid "All security scans are enabled because %{linkStart}Auto DevOps%{linkEnd} is enabled on this project"
msgstr ""
+msgid "All threads resolved"
+msgstr ""
+
msgid "All users"
msgstr ""
@@ -1997,6 +2111,9 @@ msgstr ""
msgid "Allow only the selected protocols to be used for Git access."
msgstr ""
+msgid "Allow owners to manage default branch protection per group"
+msgstr ""
+
msgid "Allow owners to manually add users outside of LDAP"
msgstr ""
@@ -2105,6 +2222,9 @@ msgstr ""
msgid "An error occurred fetching the dropdown data."
msgstr ""
+msgid "An error occurred fetching the project authors."
+msgstr ""
+
msgid "An error occurred previewing the blob"
msgstr ""
@@ -2186,6 +2306,9 @@ msgstr ""
msgid "An error occurred while fetching sidebar data"
msgstr ""
+msgid "An error occurred while fetching terraform reports."
+msgstr ""
+
msgid "An error occurred while fetching the Service Desk address."
msgstr ""
@@ -2423,6 +2546,9 @@ msgstr ""
msgid "Any"
msgstr ""
+msgid "Any Author"
+msgstr ""
+
msgid "Any Label"
msgstr ""
@@ -2654,10 +2780,10 @@ msgstr ""
msgid "Are you sure that you want to unarchive this project?"
msgstr ""
-msgid "Are you sure you want to cancel creating this comment?"
+msgid "Are you sure you want to cancel editing this comment?"
msgstr ""
-msgid "Are you sure you want to cancel editing this comment?"
+msgid "Are you sure you want to close this blocked issue?"
msgstr ""
msgid "Are you sure you want to delete %{name}?"
@@ -2687,6 +2813,9 @@ msgstr ""
msgid "Are you sure you want to deploy this environment?"
msgstr ""
+msgid "Are you sure you want to discard this comment?"
+msgstr ""
+
msgid "Are you sure you want to erase this build?"
msgstr ""
@@ -2867,6 +2996,9 @@ msgstr ""
msgid "At least one approval from a code owner is required to change files matching the respective CODEOWNER rules."
msgstr ""
+msgid "At least one logging option is required to be enabled"
+msgstr ""
+
msgid "At least one of group_id or project_id must be specified"
msgstr ""
@@ -2909,6 +3041,27 @@ msgstr ""
msgid "AuditEvents|Target"
msgstr ""
+msgid "AuditLogs|(removed)"
+msgstr ""
+
+msgid "AuditLogs|Action"
+msgstr ""
+
+msgid "AuditLogs|Author"
+msgstr ""
+
+msgid "AuditLogs|Date"
+msgstr ""
+
+msgid "AuditLogs|IP Address"
+msgstr ""
+
+msgid "AuditLogs|Object"
+msgstr ""
+
+msgid "AuditLogs|Target"
+msgstr ""
+
msgid "Aug"
msgstr ""
@@ -3191,9 +3344,6 @@ msgstr ""
msgid "BambooService|You must set up automatic revision labeling and a repository trigger in Bamboo."
msgstr ""
-msgid "Batch operations"
-msgstr ""
-
msgid "BatchComments|Delete all pending comments"
msgstr ""
@@ -3323,6 +3473,9 @@ msgstr ""
msgid "Boards"
msgstr ""
+msgid "Boards and Board Lists"
+msgstr ""
+
msgid "Boards|Collapse"
msgstr ""
@@ -3542,6 +3695,9 @@ msgstr ""
msgid "Buy GitLab Enterprise Edition"
msgstr ""
+msgid "Buy more Pipeline minutes"
+msgstr ""
+
msgid "By %{user_name}"
msgstr ""
@@ -3662,6 +3818,9 @@ msgstr ""
msgid "Can't scan the code?"
msgstr ""
+msgid "Can't update snippet: %{err}"
+msgstr ""
+
msgid "Canary"
msgstr ""
@@ -3830,6 +3989,9 @@ msgstr ""
msgid "Changing group path can have unintended side effects."
msgstr ""
+msgid "Charts"
+msgstr ""
+
msgid "Charts can't be displayed as the request for data has timed out. %{documentationLink}"
msgstr ""
@@ -3866,9 +4028,6 @@ msgstr ""
msgid "ChatMessage|and [%{count} more](%{pipeline_failed_jobs_url})"
msgstr ""
-msgid "ChatMessage|failed"
-msgstr ""
-
msgid "ChatMessage|has failed"
msgstr ""
@@ -3884,9 +4043,6 @@ msgstr ""
msgid "ChatMessage|in %{project_link}"
msgstr ""
-msgid "ChatMessage|passed"
-msgstr ""
-
msgid "Check again"
msgstr ""
@@ -4337,9 +4493,6 @@ msgstr ""
msgid "Click any <strong>project name</strong> in the project list below to navigate to the project milestone."
msgstr ""
-msgid "Click here"
-msgstr ""
-
msgid "Click the <strong>Download</strong> button and wait for downloading to complete."
msgstr ""
@@ -4727,6 +4880,12 @@ msgstr ""
msgid "ClusterIntegration|Fetching zones"
msgstr ""
+msgid "ClusterIntegration|Fluentd"
+msgstr ""
+
+msgid "ClusterIntegration|Fluentd is an open source data collector, which lets you unify the data collection and consumption for a better use and understanding of data. It requires at least one of the following logs to be successfully installed."
+msgstr ""
+
msgid "ClusterIntegration|GitLab Integration"
msgstr ""
@@ -5033,6 +5192,15 @@ msgstr ""
msgid "ClusterIntegration|Request to begin uninstalling failed"
msgstr ""
+msgid "ClusterIntegration|SIEM Hostname"
+msgstr ""
+
+msgid "ClusterIntegration|SIEM Port"
+msgstr ""
+
+msgid "ClusterIntegration|SIEM Protocol"
+msgstr ""
+
msgid "ClusterIntegration|Save changes"
msgstr ""
@@ -5129,6 +5297,12 @@ msgstr ""
msgid "ClusterIntegration|Select zone to choose machine type"
msgstr ""
+msgid "ClusterIntegration|Send Cilium Logs"
+msgstr ""
+
+msgid "ClusterIntegration|Send ModSecurity Logs"
+msgstr ""
+
msgid "ClusterIntegration|Service Token"
msgstr ""
@@ -5561,18 +5735,33 @@ msgstr ""
msgid "Compliance framework (optional)"
msgstr ""
+msgid "ComplianceFramework|GDPR"
+msgstr ""
+
msgid "ComplianceFramework|GDPR - General Data Protection Regulation"
msgstr ""
+msgid "ComplianceFramework|HIPAA"
+msgstr ""
+
msgid "ComplianceFramework|HIPAA - Health Insurance Portability and Accountability Act"
msgstr ""
+msgid "ComplianceFramework|PCI-DSS"
+msgstr ""
+
msgid "ComplianceFramework|PCI-DSS - Payment Card Industry-Data Security Standard"
msgstr ""
+msgid "ComplianceFramework|SOC 2"
+msgstr ""
+
msgid "ComplianceFramework|SOC 2 - Service Organization Control 2"
msgstr ""
+msgid "ComplianceFramework|SOX"
+msgstr ""
+
msgid "ComplianceFramework|SOX - Sarbanes-Oxley"
msgstr ""
@@ -5720,9 +5909,15 @@ msgstr ""
msgid "Container repositories sync capacity"
msgstr ""
+msgid "ContainerRegistry| Please visit the %{linkStart}administration settings%{linkEnd} to enable this feature."
+msgstr ""
+
msgid "ContainerRegistry|%{imageName} tags"
msgstr ""
+msgid "ContainerRegistry|%{title} was successfully scheduled for deletion"
+msgstr ""
+
msgid "ContainerRegistry|Automatically remove extra images that aren't designed to be kept."
msgstr ""
@@ -5735,6 +5930,9 @@ msgstr ""
msgid "ContainerRegistry|Container Registry"
msgstr ""
+msgid "ContainerRegistry|Container Registry tag expiration and retention policy is disabled"
+msgstr ""
+
msgid "ContainerRegistry|Copy build command"
msgstr ""
@@ -5744,18 +5942,12 @@ msgstr ""
msgid "ContainerRegistry|Copy push command"
msgstr ""
-msgid "ContainerRegistry|Currently, the Container Registry tag expiration feature is not available for projects created before GitLab version 12.8. For updates and more information, visit Issue %{linkStart}#196124%{linkEnd}"
-msgstr ""
-
msgid "ContainerRegistry|Docker connection error"
msgstr ""
msgid "ContainerRegistry|Docker tag expiration policy is %{toggleStatus}"
msgstr ""
-msgid "ContainerRegistry|Docker tags with names matching this regex pattern will expire:"
-msgstr ""
-
msgid "ContainerRegistry|Edit Settings"
msgstr ""
@@ -5777,9 +5969,6 @@ msgstr ""
msgid "ContainerRegistry|Image ID"
msgstr ""
-msgid "ContainerRegistry|Image deleted successfully"
-msgstr ""
-
msgid "ContainerRegistry|Keep and protect the images that matter most."
msgstr ""
@@ -5795,12 +5984,24 @@ msgstr ""
msgid "ContainerRegistry|Number of tags to retain:"
msgstr ""
+msgid "ContainerRegistry|Please contact your administrator."
+msgstr ""
+
msgid "ContainerRegistry|Push an image"
msgstr ""
msgid "ContainerRegistry|Quick Start"
msgstr ""
+msgid "ContainerRegistry|Regular expressions such as %{codeStart}.*-test%{codeEnd} or %{codeStart}dev-.*%{codeEnd} are supported"
+msgstr ""
+
+msgid "ContainerRegistry|Regular expressions such as %{codeStart}.*-test%{codeEnd} or %{codeStart}dev-.*%{codeEnd} are supported. To select all tags, use %{codeStart}.*%{codeEnd}"
+msgstr ""
+
+msgid "ContainerRegistry|Remember to run %{docLinkStart}garbage collection%{docLinkEnd} to remove the stale data from storage."
+msgstr ""
+
msgid "ContainerRegistry|Remove repository"
msgstr ""
@@ -5819,22 +6020,22 @@ msgstr[5] ""
msgid "ContainerRegistry|Retention policy has been Enabled"
msgstr ""
-msgid "ContainerRegistry|Something went wrong while deleting the image."
+msgid "ContainerRegistry|Something went wrong while fetching the expiration policy."
msgstr ""
-msgid "ContainerRegistry|Something went wrong while deleting the tag."
+msgid "ContainerRegistry|Something went wrong while fetching the repository list."
msgstr ""
-msgid "ContainerRegistry|Something went wrong while deleting the tags."
+msgid "ContainerRegistry|Something went wrong while fetching the tags list."
msgstr ""
-msgid "ContainerRegistry|Something went wrong while fetching the expiration policy."
+msgid "ContainerRegistry|Something went wrong while marking the tag for deletion."
msgstr ""
-msgid "ContainerRegistry|Something went wrong while fetching the packages list."
+msgid "ContainerRegistry|Something went wrong while marking the tags for deletion."
msgstr ""
-msgid "ContainerRegistry|Something went wrong while fetching the tags list."
+msgid "ContainerRegistry|Something went wrong while scheduling %{title} for deletion. Please try again."
msgstr ""
msgid "ContainerRegistry|Something went wrong while updating the expiration policy."
@@ -5843,16 +6044,25 @@ msgstr ""
msgid "ContainerRegistry|Tag"
msgstr ""
-msgid "ContainerRegistry|Tag deleted successfully"
-msgstr ""
-
msgid "ContainerRegistry|Tag expiration policy"
msgstr ""
msgid "ContainerRegistry|Tag expiration policy is designed to:"
msgstr ""
-msgid "ContainerRegistry|Tags deleted successfully"
+msgid "ContainerRegistry|Tag successfully marked for deletion."
+msgstr ""
+
+msgid "ContainerRegistry|Tags successfully marked for deletion."
+msgstr ""
+
+msgid "ContainerRegistry|Tags with names matching this regex pattern will %{italicStart}be preserved:%{italicEnd}"
+msgstr ""
+
+msgid "ContainerRegistry|Tags with names matching this regex pattern will %{italicStart}expire:%{italicEnd}"
+msgstr ""
+
+msgid "ContainerRegistry|The Container Registry tag expiration and retention policies for this project have not been enabled."
msgstr ""
msgid "ContainerRegistry|The last tag related to this image was recently removed. This empty image and any associated data will be automatically removed as part of the regular Garbage Collection process. If you have any questions, contact your administrator."
@@ -5873,16 +6083,16 @@ msgstr ""
msgid "ContainerRegistry|There are no container images stored for this project"
msgstr ""
-msgid "ContainerRegistry|This Registry contains deleted image tag data. Remember to run %{docLinkStart}garbage collection%{docLinkEnd} to remove the stale data from storage."
+msgid "ContainerRegistry|There was an error during the deletion of this image repository, please try again."
msgstr ""
msgid "ContainerRegistry|This image has no active tags"
msgstr ""
-msgid "ContainerRegistry|We are having trouble connecting to Docker, which could be due to an issue with your project name or path. %{docLinkStart}More Information%{docLinkEnd}"
+msgid "ContainerRegistry|This image repository is scheduled for deletion"
msgstr ""
-msgid "ContainerRegistry|Wildcards such as %{codeStart}.*-stable%{codeEnd} or %{codeStart}production/.*%{codeEnd} are supported. To select all tags, use %{codeStart}.*%{codeEnd}"
+msgid "ContainerRegistry|We are having trouble connecting to Docker, which could be due to an issue with your project name or path. %{docLinkStart}More Information%{docLinkEnd}"
msgstr ""
msgid "ContainerRegistry|With the Container Registry, every project can have its own space to store its Docker images. %{docLinkStart}More Information%{docLinkEnd}"
@@ -6137,7 +6347,7 @@ msgstr ""
msgid "Could not delete chat nickname %{chat_name}."
msgstr ""
-msgid "Could not find design"
+msgid "Could not find design."
msgstr ""
msgid "Could not remove the trigger."
@@ -6152,6 +6362,9 @@ msgstr ""
msgid "Could not revoke personal access token %{personal_access_token_name}."
msgstr ""
+msgid "Could not revoke project access token %{project_access_token_name}."
+msgstr ""
+
msgid "Could not save group ID"
msgstr ""
@@ -6209,6 +6422,9 @@ msgstr ""
msgid "Create a new branch"
msgstr ""
+msgid "Create a new deploy key for this project"
+msgstr ""
+
msgid "Create a new file as there are no files yet. Afterwards, you'll be able to commit your changes."
msgstr ""
@@ -6314,6 +6530,9 @@ msgstr ""
msgid "Create requirement"
msgstr ""
+msgid "Create snippet"
+msgstr ""
+
msgid "Create wildcard: %{searchTerm}"
msgstr ""
@@ -6335,6 +6554,9 @@ msgstr ""
msgid "Created"
msgstr ""
+msgid "Created %{timestamp}"
+msgstr ""
+
msgid "Created At"
msgstr ""
@@ -6695,6 +6917,9 @@ msgstr ""
msgid "CycleAnalytics|stage dropdown"
msgstr ""
+msgid "DAG"
+msgstr ""
+
msgid "DNS"
msgstr ""
@@ -7085,6 +7310,9 @@ msgstr ""
msgid "Deploy key was successfully updated."
msgstr ""
+msgid "Deploy keys allow read-only or read-write (if enabled) access to your repository. Deploy keys can be used for CI, staging or production servers. You can create a deploy key or add an existing one."
+msgstr ""
+
msgid "Deploy progress not found. To see pods, ensure your environment matches %{linkStart}deploy board criteria%{linkEnd}."
msgstr ""
@@ -7238,6 +7466,9 @@ msgstr ""
msgid "Deploying to"
msgstr ""
+msgid "Deployment Frequency"
+msgstr ""
+
msgid "Deployment|API"
msgstr ""
@@ -7760,9 +7991,6 @@ msgstr ""
msgid "Edit environment"
msgstr ""
-msgid "Edit epic description"
-msgstr ""
-
msgid "Edit file"
msgstr ""
@@ -8384,9 +8612,6 @@ msgstr ""
msgid "Epics"
msgstr ""
-msgid "Epics (Ultimate / Gold license only)"
-msgstr ""
-
msgid "Epics Roadmap"
msgstr ""
@@ -8396,6 +8621,9 @@ msgstr ""
msgid "Epics let you manage your portfolio of projects more efficiently and with less effort"
msgstr ""
+msgid "Epics, Issues, and Merge Requests"
+msgstr ""
+
msgid "Epics|Add an epic"
msgstr ""
@@ -8447,6 +8675,9 @@ msgstr ""
msgid "Epics|Something went wrong while fetching group epics."
msgstr ""
+msgid "Epics|Something went wrong while moving item."
+msgstr ""
+
msgid "Epics|Something went wrong while ordering item."
msgstr ""
@@ -8501,6 +8732,9 @@ msgstr ""
msgid "Error fetching network graph."
msgstr ""
+msgid "Error fetching payload data."
+msgstr ""
+
msgid "Error fetching projects"
msgstr ""
@@ -8510,9 +8744,6 @@ msgstr ""
msgid "Error fetching the dependency list. Please check your network connection and try again."
msgstr ""
-msgid "Error fetching usage ping data."
-msgstr ""
-
msgid "Error loading branch data. Please try again."
msgstr ""
@@ -8654,6 +8885,9 @@ msgstr ""
msgid "Errors"
msgstr ""
+msgid "Errors:"
+msgstr ""
+
msgid "Estimated"
msgstr ""
@@ -8858,12 +9092,18 @@ msgstr ""
msgid "Export as CSV"
msgstr ""
+msgid "Export group"
+msgstr ""
+
msgid "Export issues"
msgstr ""
msgid "Export project"
msgstr ""
+msgid "Export this group with all related data to a new GitLab instance. Once complete, you can import the data file from the \"New Group\" page."
+msgstr ""
+
msgid "Export this project with all its related data in order to move your project to a new GitLab instance. Once the export is finished, you can import the file from the \"New Project\" page."
msgstr ""
@@ -8933,9 +9173,6 @@ msgstr ""
msgid "Failed Jobs"
msgstr ""
-msgid "Failed create wiki"
-msgstr ""
-
msgid "Failed to add a Zoom meeting"
msgstr ""
@@ -8975,6 +9212,9 @@ msgstr ""
msgid "Failed to create resources"
msgstr ""
+msgid "Failed to create wiki"
+msgstr ""
+
msgid "Failed to delete board. Please try again."
msgstr ""
@@ -8984,7 +9224,7 @@ msgstr ""
msgid "Failed to enqueue the rebase operation, possibly due to a long-lived transaction. Try again later."
msgstr ""
-msgid "Failed to find import label for jira import."
+msgid "Failed to find import label for Jira import."
msgstr ""
msgid "Failed to get ref."
@@ -9008,6 +9248,12 @@ msgstr ""
msgid "Failed to load groups & users."
msgstr ""
+msgid "Failed to load labels. Please try again."
+msgstr ""
+
+msgid "Failed to load milestones. Please try again."
+msgstr ""
+
msgid "Failed to load related branches"
msgstr ""
@@ -9383,7 +9629,10 @@ msgstr ""
msgid "Filter by milestone name"
msgstr ""
-msgid "Filter by name..."
+msgid "Filter by name"
+msgstr ""
+
+msgid "Filter by status"
msgstr ""
msgid "Filter by two-factor authentication"
@@ -9395,12 +9644,18 @@ msgstr ""
msgid "Filter projects"
msgstr ""
+msgid "Filter results"
+msgstr ""
+
msgid "Filter results by group"
msgstr ""
msgid "Filter results by project"
msgstr ""
+msgid "Filter results..."
+msgstr ""
+
msgid "Filter your projects by name"
msgstr ""
@@ -9689,10 +9944,10 @@ msgstr ""
msgid "GeoNodes|Checksummed"
msgstr ""
-msgid "GeoNodes|Container repositories"
+msgid "GeoNodes|Consult Geo troubleshooting information"
msgstr ""
-msgid "GeoNodes|Data is out of date from %{timeago}"
+msgid "GeoNodes|Container repositories"
msgstr ""
msgid "GeoNodes|Data replication lag"
@@ -9734,6 +9989,9 @@ msgstr ""
msgid "GeoNodes|Last event ID seen from primary"
msgstr ""
+msgid "GeoNodes|Learn more about Geo node statuses"
+msgstr ""
+
msgid "GeoNodes|Loading nodes"
msgstr ""
@@ -9749,6 +10007,9 @@ msgstr ""
msgid "GeoNodes|Node was successfully removed."
msgstr ""
+msgid "GeoNodes|Node's status was updated %{timeAgo}."
+msgstr ""
+
msgid "GeoNodes|Not checksummed"
msgstr ""
@@ -9779,7 +10040,10 @@ msgstr ""
msgid "GeoNodes|Repository verification progress"
msgstr ""
-msgid "GeoNodes|Selective"
+msgid "GeoNodes|Selective (%{syncLabel})"
+msgstr ""
+
+msgid "GeoNodes|Selective synchronization"
msgstr ""
msgid "GeoNodes|Something went wrong while changing node status"
@@ -9806,6 +10070,9 @@ msgstr ""
msgid "GeoNodes|Unverified"
msgstr ""
+msgid "GeoNodes|Updated %{timeAgo}"
+msgstr ""
+
msgid "GeoNodes|Used slots"
msgstr ""
@@ -9845,16 +10112,16 @@ msgstr ""
msgid "Geo|All"
msgstr ""
-msgid "Geo|All projects"
+msgid "Geo|All %{replicable_type}"
msgstr ""
-msgid "Geo|All projects are being scheduled for re-sync"
+msgid "Geo|All projects"
msgstr ""
-msgid "Geo|All projects are being scheduled for re-verify"
+msgid "Geo|All projects are being scheduled for resync"
msgstr ""
-msgid "Geo|Batch operations"
+msgid "Geo|All projects are being scheduled for reverify"
msgstr ""
msgid "Geo|Could not remove tracking entry for an existing project."
@@ -9866,9 +10133,15 @@ msgstr ""
msgid "Geo|Failed"
msgstr ""
+msgid "Geo|Filter by status"
+msgstr ""
+
msgid "Geo|Geo Status"
msgstr ""
+msgid "Geo|In progress"
+msgstr ""
+
msgid "Geo|In sync"
msgstr ""
@@ -9893,9 +10166,6 @@ msgstr ""
msgid "Geo|Not synced yet"
msgstr ""
-msgid "Geo|Pending"
-msgstr ""
-
msgid "Geo|Pending synchronization"
msgstr ""
@@ -9932,7 +10202,7 @@ msgstr ""
msgid "Geo|Resync"
msgstr ""
-msgid "Geo|Resync all projects"
+msgid "Geo|Resync all"
msgstr ""
msgid "Geo|Retry count"
@@ -9941,7 +10211,7 @@ msgstr ""
msgid "Geo|Reverify"
msgstr ""
-msgid "Geo|Reverify all projects"
+msgid "Geo|Reverify all"
msgstr ""
msgid "Geo|Status"
@@ -10283,9 +10553,6 @@ msgstr ""
msgid "Go to file"
msgstr ""
-msgid "Go to file (MRs only)"
-msgstr ""
-
msgid "Go to file permalink (while viewing a file)"
msgstr ""
@@ -10730,6 +10997,9 @@ msgstr ""
msgid "GroupSettings|Disable group mentions"
msgstr ""
+msgid "GroupSettings|Export group"
+msgstr ""
+
msgid "GroupSettings|If the parent group's visibility is lower than the group current visibility, visibility levels for subgroups and projects will be changed to match the new parent group's visibility."
msgstr ""
@@ -10808,6 +11078,9 @@ msgstr ""
msgid "Groups (%{count})"
msgstr ""
+msgid "Groups (%{groups})"
+msgstr ""
+
msgid "Groups can also be nested by creating %{subgroup_docs_link_start}subgroups%{subgroup_docs_link_end}."
msgstr ""
@@ -11081,15 +11354,12 @@ msgstr ""
msgid "ID:"
msgstr ""
-msgid "IDE|Allow live previews of JavaScript projects in the Web IDE using CodeSandbox client side evaluation."
+msgid "IDE|Allow live previews of JavaScript projects in the Web IDE using CodeSandbox Live Preview."
msgstr ""
msgid "IDE|Back"
msgstr ""
-msgid "IDE|Client side evaluation"
-msgstr ""
-
msgid "IDE|Commit"
msgstr ""
@@ -11153,6 +11423,9 @@ msgstr ""
msgid "If any job surpasses this timeout threshold, it will be marked as failed. Human readable time input language is accepted like \"1 hour\". Values without specification represent seconds."
msgstr ""
+msgid "If blank, set allowable lifetime to %{instance_level_policy_in_words}, as defined by the instance admin. Once set, existing tokens for users in this group may be revoked."
+msgstr ""
+
msgid "If checked, group owners can manage LDAP group links and LDAP member overrides"
msgstr ""
@@ -11192,6 +11465,9 @@ msgstr ""
msgid "If you lose your recovery codes you can generate new ones, invalidating all previous codes."
msgstr ""
+msgid "If you reach 100%% storage capacity, you will not be able to: %{base_message}"
+msgstr ""
+
msgid "If your HTTP repository is not publicly accessible, add your credentials."
msgstr ""
@@ -11303,6 +11579,9 @@ msgstr ""
msgid "Import repository"
msgstr ""
+msgid "Import started by: %{importInitiator}"
+msgstr ""
+
msgid "Import tasks"
msgstr ""
@@ -11369,6 +11648,9 @@ msgstr ""
msgid "In order to tailor your experience with GitLab we<br>would like to know a bit more about you."
msgstr ""
+msgid "In progress"
+msgstr ""
+
msgid "In the next step, you'll be able to select the projects you want to import."
msgstr ""
@@ -11720,15 +12002,15 @@ msgstr ""
msgid "Issues"
msgstr ""
-msgid "Issues / Merge Requests"
-msgstr ""
-
msgid "Issues Analytics"
msgstr ""
msgid "Issues Rate Limits"
msgstr ""
+msgid "Issues and Merge Requests"
+msgstr ""
+
msgid "Issues can be bugs, tasks or ideas to be discussed. Also, issues are searchable and filterable."
msgstr ""
@@ -11750,10 +12032,10 @@ msgstr ""
msgid "IssuesAnalytics|Avg/Month:"
msgstr ""
-msgid "IssuesAnalytics|Issues created"
+msgid "IssuesAnalytics|Issues opened"
msgstr ""
-msgid "IssuesAnalytics|Issues created per month"
+msgid "IssuesAnalytics|Issues opened per month"
msgstr ""
msgid "IssuesAnalytics|Last 12 months"
@@ -11807,6 +12089,9 @@ msgstr ""
msgid "Jira integration not configured."
msgstr ""
+msgid "Jira project: %{importProject}"
+msgstr ""
+
msgid "JiraService|Events for %{noteable_model_name} are disabled."
msgstr ""
@@ -12200,6 +12485,9 @@ msgstr ""
msgid "Lead"
msgstr ""
+msgid "Lead Time"
+msgstr ""
+
msgid "Learn GitLab"
msgstr ""
@@ -12212,6 +12500,9 @@ msgstr ""
msgid "Learn how to %{no_packages_link_start}publish and share your packages%{no_packages_link_end} with GitLab."
msgstr ""
+msgid "Learn how to enable synchronization"
+msgstr ""
+
msgid "Learn more"
msgstr ""
@@ -12563,9 +12854,6 @@ msgstr ""
msgid "Live preview"
msgstr ""
-msgid "Load more vulnerabilities"
-msgstr ""
-
msgid "Loading"
msgstr ""
@@ -12716,9 +13004,6 @@ msgstr ""
msgid "Manage"
msgstr ""
-msgid "Manage Git repositories with fine-grained access controls that keep your code secure. Perform code reviews and enhance collaboration with merge requests. Each project can also have an issue tracker and a wiki."
-msgstr ""
-
msgid "Manage Web IDE features"
msgstr ""
@@ -12749,6 +13034,9 @@ msgstr ""
msgid "Manage your license"
msgstr ""
+msgid "Managed Account"
+msgstr ""
+
msgid "Manifest"
msgstr ""
@@ -13013,6 +13301,9 @@ msgstr ""
msgid "Merge Requests in Review"
msgstr ""
+msgid "Merge automatically (%{strategy})"
+msgstr ""
+
msgid "Merge commit message"
msgstr ""
@@ -13166,6 +13457,12 @@ msgstr ""
msgid "Merged branches are being deleted. This can take some time depending on the number of branches. Please refresh the page to see changes."
msgstr ""
+msgid "Merged this merge request."
+msgstr ""
+
+msgid "Merges this merge request immediately."
+msgstr ""
+
msgid "Merges this merge request when the pipeline succeeds."
msgstr ""
@@ -13277,12 +13574,15 @@ msgstr[3] ""
msgstr[4] ""
msgstr[5] ""
-msgid "Metrics|Environment"
+msgid "Metrics|Expand panel"
msgstr ""
msgid "Metrics|For grouping similar metrics"
msgstr ""
+msgid "Metrics|Go back (Esc)"
+msgstr ""
+
msgid "Metrics|Invalid time range, please verify."
msgstr ""
@@ -13313,9 +13613,6 @@ msgstr ""
msgid "Metrics|Refresh dashboard"
msgstr ""
-msgid "Metrics|Show last"
-msgstr ""
-
msgid "Metrics|There was an error creating the dashboard."
msgstr ""
@@ -13394,6 +13691,9 @@ msgstr ""
msgid "Microsoft Azure"
msgstr ""
+msgid "Middleman project with Static Site Editor support"
+msgstr ""
+
msgid "Migrated %{success_count}/%{total_count} files."
msgstr ""
@@ -13847,10 +14147,10 @@ msgstr ""
msgid "Next"
msgstr ""
-msgid "Next file in diff (MRs only)"
+msgid "Next file in diff"
msgstr ""
-msgid "Next unresolved discussion (MRs only)"
+msgid "Next unresolved discussion"
msgstr ""
msgid "Nickname"
@@ -13910,6 +14210,9 @@ msgstr ""
msgid "No changes between %{ref_start}%{source_branch}%{ref_end} and %{ref_start}%{target_branch}%{ref_end}"
msgstr ""
+msgid "No child epics match applied filters"
+msgstr ""
+
msgid "No connection could be made to a Gitaly Server, please check your logs!"
msgstr ""
@@ -14048,15 +14351,6 @@ msgstr ""
msgid "No thanks, don't show this again"
msgstr ""
-msgid "No vulnerabilities found for this group"
-msgstr ""
-
-msgid "No vulnerabilities found for this pipeline"
-msgstr ""
-
-msgid "No vulnerabilities found for this project"
-msgstr ""
-
msgid "No vulnerabilities present"
msgstr ""
@@ -14180,6 +14474,9 @@ msgstr ""
msgid "Nothing to preview."
msgstr ""
+msgid "Nothing to synchronize"
+msgstr ""
+
msgid "Notification events"
msgstr ""
@@ -14351,6 +14648,9 @@ msgstr ""
msgid "Once removed, the fork relationship cannot be restored and you will no longer be able to send merge requests to the source."
msgstr ""
+msgid "Once the exported file is ready you can download it from this page."
+msgstr ""
+
msgid "Once the exported file is ready, you will receive a notification email with a download link, or you can download it from this page."
msgstr ""
@@ -14456,9 +14756,6 @@ msgstr ""
msgid "Open sidebar"
msgstr ""
-msgid "Open source software to collaborate on code"
-msgstr ""
-
msgid "Open: %{openIssuesCount}"
msgstr ""
@@ -14561,6 +14858,9 @@ msgstr ""
msgid "OutdatedBrowser|You can provide feedback %{feedback_link_start}on this issue%{feedback_link_end} or via your usual support channels."
msgstr ""
+msgid "Overridden"
+msgstr ""
+
msgid "Overview"
msgstr ""
@@ -14612,6 +14912,9 @@ msgstr ""
msgid "PackageRegistry|Conan Command"
msgstr ""
+msgid "PackageRegistry|Copy .pypirc content"
+msgstr ""
+
msgid "PackageRegistry|Copy Conan Command"
msgstr ""
@@ -14633,6 +14936,9 @@ msgstr ""
msgid "PackageRegistry|Copy NuGet Setup Command"
msgstr ""
+msgid "PackageRegistry|Copy Pip command"
+msgstr ""
+
msgid "PackageRegistry|Copy and paste this inside your %{codeStart}pom.xml%{codeEnd} %{codeStart}dependencies%{codeEnd} block."
msgstr ""
@@ -14666,6 +14972,12 @@ msgstr ""
msgid "PackageRegistry|For more information on the NuGet registry, %{linkStart}see the documentation%{linkEnd}."
msgstr ""
+msgid "PackageRegistry|For more information on the PyPi registry, %{linkStart}see the documentation%{linkEnd}."
+msgstr ""
+
+msgid "PackageRegistry|If you haven't already done so, you will need to add the below to your %{codeStart}.pypirc%{codeEnd} file."
+msgstr ""
+
msgid "PackageRegistry|If you haven't already done so, you will need to add the below to your %{codeStart}pom.xml%{codeEnd} file."
msgstr ""
@@ -14675,6 +14987,9 @@ msgstr ""
msgid "PackageRegistry|Learn how to %{noPackagesLinkStart}publish and share your packages%{noPackagesLinkEnd} with GitLab."
msgstr ""
+msgid "PackageRegistry|Manually Published"
+msgstr ""
+
msgid "PackageRegistry|Maven"
msgstr ""
@@ -14693,12 +15008,18 @@ msgstr ""
msgid "PackageRegistry|NuGet Command"
msgstr ""
+msgid "PackageRegistry|Pip Command"
+msgstr ""
+
msgid "PackageRegistry|Pipeline %{linkStart}%{linkEnd} triggered %{timestamp} by %{author}"
msgstr ""
msgid "PackageRegistry|Published to the repository at %{timestamp}"
msgstr ""
+msgid "PackageRegistry|PyPi"
+msgstr ""
+
msgid "PackageRegistry|Registry Setup"
msgstr ""
@@ -14735,6 +15056,9 @@ msgstr ""
msgid "PackageRegistry|npm"
msgstr ""
+msgid "PackageRegistry|published by %{author}"
+msgstr ""
+
msgid "PackageRegistry|yarn"
msgstr ""
@@ -14750,6 +15074,9 @@ msgstr ""
msgid "PackageType|NuGet"
msgstr ""
+msgid "PackageType|PyPi"
+msgstr ""
+
msgid "Packages"
msgstr ""
@@ -14843,6 +15170,9 @@ msgstr ""
msgid "Past due"
msgstr ""
+msgid "Paste a machine public key here. Read more about how to generate it"
+msgstr ""
+
msgid "Paste a machine public key here. Read more about how to generate it %{link_start}here%{link_end}"
msgstr ""
@@ -14858,9 +15188,6 @@ msgstr ""
msgid "Path"
msgstr ""
-msgid "Path, transfer, remove"
-msgstr ""
-
msgid "Path:"
msgstr ""
@@ -14891,7 +15218,7 @@ msgstr ""
msgid "Percentage"
msgstr ""
-msgid "Perform advanced options such as changing path, transferring, or removing the group."
+msgid "Perform advanced options such as changing path, transferring, exporting, or removing the group."
msgstr ""
msgid "Perform common operations on GitLab project"
@@ -14972,9 +15299,6 @@ msgstr ""
msgid "Pipeline minutes quota"
msgstr ""
-msgid "Pipeline quota"
-msgstr ""
-
msgid "Pipeline subscriptions"
msgstr ""
@@ -14984,6 +15308,9 @@ msgstr ""
msgid "Pipeline: %{status}"
msgstr ""
+msgid "PipelineCharts|CI / CD Analytics"
+msgstr ""
+
msgid "PipelineCharts|Failed:"
msgstr ""
@@ -15062,15 +15389,6 @@ msgstr ""
msgid "Pipelines settings for '%{project_name}' were successfully updated."
msgstr ""
-msgid "Pipelines| to purchase more minutes."
-msgstr ""
-
-msgid "Pipelines|%{namespace_name} has exceeded its pipeline minutes quota."
-msgstr ""
-
-msgid "Pipelines|%{namespace_name} has less than %{notification_level}%% of CI minutes available."
-msgstr ""
-
msgid "Pipelines|API"
msgstr ""
@@ -15092,10 +15410,13 @@ msgstr ""
msgid "Pipelines|Get started with Pipelines"
msgstr ""
-msgid "Pipelines|Loading Pipelines"
+msgid "Pipelines|Group %{namespace_name} has %{percentage}%% or less Shared Runner Pipeline minutes remaining. Once it runs out, no new jobs or pipelines in its projects will run."
msgstr ""
-msgid "Pipelines|Pipelines will not run anymore on shared Runners."
+msgid "Pipelines|Group %{namespace_name} has exceeded its pipeline minutes quota. Unless you buy additional pipeline minutes, no new jobs or pipelines in its projects will run."
+msgstr ""
+
+msgid "Pipelines|Loading Pipelines"
msgstr ""
msgid "Pipelines|Project cache successfully reset."
@@ -15326,6 +15647,9 @@ msgstr ""
msgid "Please select"
msgstr ""
+msgid "Please select a Jira project"
+msgstr ""
+
msgid "Please select a country"
msgstr ""
@@ -15371,6 +15695,9 @@ msgstr ""
msgid "Please wait while we import the repository for you. Refresh at will."
msgstr ""
+msgid "Plugins directory is deprecated and will be removed in 14.0. Please move this file into /file_hooks directory."
+msgstr ""
+
msgid "Pod does not exist"
msgstr ""
@@ -15488,7 +15815,7 @@ msgstr ""
msgid "Prevent users from changing their profile name"
msgstr ""
-msgid "Prevent users from modifing merge request approvers list"
+msgid "Prevent users from modifying merge request approvers list"
msgstr ""
msgid "Prevent users from performing write operations on GitLab while performing maintenance."
@@ -15509,10 +15836,10 @@ msgstr ""
msgid "Previous Artifacts"
msgstr ""
-msgid "Previous file in diff (MRs only)"
+msgid "Previous file in diff"
msgstr ""
-msgid "Previous unresolved discussion (MRs only)"
+msgid "Previous unresolved discussion"
msgstr ""
msgid "Primary"
@@ -15545,6 +15872,9 @@ msgstr ""
msgid "Private profile"
msgstr ""
+msgid "Private projects Minutes cost factor"
+msgstr ""
+
msgid "Private projects can be created in your personal namespace with:"
msgstr ""
@@ -16373,6 +16703,9 @@ msgstr ""
msgid "ProjectSettings|With GitLab Pages you can host your static websites on GitLab"
msgstr ""
+msgid "ProjectSettings|With Metrics Dashboard you can visualize this project performance metrics"
+msgstr ""
+
msgid "ProjectTemplates|.NET Core"
msgstr ""
@@ -16436,6 +16769,9 @@ msgstr ""
msgid "ProjectTemplates|Spring"
msgstr ""
+msgid "ProjectTemplates|Static Site Editor/Middleman"
+msgstr ""
+
msgid "ProjectTemplates|iOS (Swift)"
msgstr ""
@@ -16655,13 +16991,13 @@ msgstr ""
msgid "PrometheusService|Prometheus is being automatically managed on your clusters"
msgstr ""
-msgid "PrometheusService|These metrics will only be monitored after your first deployment to an environment"
+msgid "PrometheusService|Select the Active checkbox to override the Auto Configuration with custom settings. If unchecked, Auto Configuration settings are used."
msgstr ""
-msgid "PrometheusService|Time-series monitoring service"
+msgid "PrometheusService|These metrics will only be monitored after your first deployment to an environment"
msgstr ""
-msgid "PrometheusService|To enable manual configuration, uninstall Prometheus from your clusters"
+msgid "PrometheusService|Time-series monitoring service"
msgstr ""
msgid "PrometheusService|To enable the installation of Prometheus on your clusters, deactivate the manual configuration below"
@@ -16847,6 +17183,9 @@ msgstr ""
msgid "Protip:"
msgstr ""
+msgid "Protocol"
+msgstr ""
+
msgid "Provider"
msgstr ""
@@ -16871,6 +17210,9 @@ msgstr ""
msgid "Public pipelines"
msgstr ""
+msgid "Public projects Minutes cost factor"
+msgstr ""
+
msgid "Pull"
msgstr ""
@@ -17075,6 +17417,9 @@ msgstr[3] ""
msgstr[4] ""
msgstr[5] ""
+msgid "Regenerate export"
+msgstr ""
+
msgid "Regenerate instance ID"
msgstr ""
@@ -17456,9 +17801,33 @@ msgstr ""
msgid "Reports|%{combinedString} and %{resolvedString}"
msgstr ""
+msgid "Reports|Accessibility scanning detected %d issue for the source branch only"
+msgid_plural "Reports|Accessibility scanning detected %d issues for the source branch only"
+msgstr[0] ""
+msgstr[1] ""
+msgstr[2] ""
+msgstr[3] ""
+msgstr[4] ""
+msgstr[5] ""
+
+msgid "Reports|Accessibility scanning detected no issues for the source branch only"
+msgstr ""
+
+msgid "Reports|Accessibility scanning failed loading results"
+msgstr ""
+
+msgid "Reports|Accessibility scanning results are being parsed"
+msgstr ""
+
msgid "Reports|Actions"
msgstr ""
+msgid "Reports|An error occured while loading report"
+msgstr ""
+
+msgid "Reports|An error occurred while loading %{name} results"
+msgstr ""
+
msgid "Reports|Class"
msgstr ""
@@ -17564,7 +17933,7 @@ msgstr ""
msgid "Requested %{time_ago}"
msgstr ""
-msgid "Requested design version does not exist"
+msgid "Requested design version does not exist."
msgstr ""
msgid "Requested states are invalid"
@@ -17756,6 +18125,9 @@ msgstr ""
msgid "Resync"
msgstr ""
+msgid "Resync all"
+msgstr ""
+
msgid "Resync all %{replicableType}"
msgstr ""
@@ -17828,6 +18200,9 @@ msgstr ""
msgid "Revoked personal access token %{personal_access_token_name}!"
msgstr ""
+msgid "Revoked project access token %{project_access_token_name}!"
+msgstr ""
+
msgid "RightSidebar|adding a"
msgstr ""
@@ -17876,6 +18251,9 @@ msgstr ""
msgid "Runner token"
msgstr ""
+msgid "Runner tokens"
+msgstr ""
+
msgid "Runner was not updated."
msgstr ""
@@ -17930,6 +18308,9 @@ msgstr ""
msgid "SAML SSO for %{group_name}"
msgstr ""
+msgid "SAML discovery tokens"
+msgstr ""
+
msgid "SAML for %{group_name}"
msgstr ""
@@ -18014,12 +18395,18 @@ msgstr ""
msgid "Scheduled"
msgstr ""
+msgid "Scheduled to merge this merge request (%{strategy})."
+msgstr ""
+
msgid "Scheduled to merge this merge request when the pipeline succeeds."
msgstr ""
msgid "Schedules"
msgstr ""
+msgid "Schedules to merge this merge request (%{strategy})."
+msgstr ""
+
msgid "Scheduling"
msgstr ""
@@ -18077,6 +18464,9 @@ msgstr ""
msgid "Search branches and tags"
msgstr ""
+msgid "Search by author"
+msgstr ""
+
msgid "Search files"
msgstr ""
@@ -18254,15 +18644,6 @@ msgstr[3] ""
msgstr[4] ""
msgstr[5] ""
-msgid "SearchResults|snippet result"
-msgid_plural "SearchResults|snippet results"
-msgstr[0] ""
-msgstr[1] ""
-msgstr[2] ""
-msgstr[3] ""
-msgstr[4] ""
-msgstr[5] ""
-
msgid "SearchResults|user"
msgid_plural "SearchResults|users"
msgstr[0] ""
@@ -18281,6 +18662,9 @@ msgstr[3] ""
msgstr[4] ""
msgstr[5] ""
+msgid "Searching by both author and message is currently not supported."
+msgstr ""
+
msgid "Seat Link"
msgstr ""
@@ -18308,184 +18692,232 @@ msgstr ""
msgid "Security Dashboard"
msgstr ""
-msgid "Security Dashboard|Error fetching the vulnerability counts. Please check your network connection and try again."
+msgid "Security configuration help link"
msgstr ""
-msgid "Security Dashboard|Error fetching the vulnerability list. Please check your network connection and try again."
+msgid "Security dashboard"
msgstr ""
-msgid "Security Dashboard|Issue Created"
+msgid "Security report is out of date. Please update your branch with the latest changes from the target branch (%{targetBranchName})"
msgstr ""
-msgid "Security Reports|Comment added to '%{vulnerabilityName}'"
+msgid "Security report is out of date. Run %{newPipelineLinkStart}a new pipeline%{newPipelineLinkEnd} for the target branch (%{targetBranchName})"
msgstr ""
-msgid "Security Reports|Comment deleted on '%{vulnerabilityName}'"
+msgid "SecurityConfiguration|Configured"
msgstr ""
-msgid "Security Reports|Comment edited on '%{vulnerabilityName}'"
+msgid "SecurityConfiguration|Feature"
msgstr ""
-msgid "Security Reports|Create issue"
+msgid "SecurityConfiguration|Feature documentation for %{featureName}"
msgstr ""
-msgid "Security Reports|Dismiss vulnerability"
+msgid "SecurityConfiguration|Not yet configured"
msgstr ""
-msgid "Security Reports|Dismissed '%{vulnerabilityName}'"
+msgid "SecurityConfiguration|Secure features"
msgstr ""
-msgid "Security Reports|Dismissed '%{vulnerabilityName}'. Turn off the hide dismissed toggle to view."
+msgid "SecurityConfiguration|Status"
msgstr ""
-msgid "Security Reports|Either you don't have permission to view this dashboard or the dashboard has not been setup. Please check your permission settings with your administrator or check your dashboard configurations to proceed."
+msgid "SecurityReports|%{firstProject} and %{secondProject}"
msgstr ""
-msgid "Security Reports|Learn more about setting up your dashboard"
+msgid "SecurityReports|%{firstProject}, %{secondProject}, and %{rest}"
msgstr ""
-msgid "Security Reports|More info"
+msgid "SecurityReports|Add a project to your dashboard"
msgstr ""
-msgid "Security Reports|Oops, something doesn't seem right."
+msgid "SecurityReports|Add or remove projects from your dashboard"
msgstr ""
-msgid "Security Reports|Security reports can only be accessed by authorized users."
+msgid "SecurityReports|Add projects"
msgstr ""
-msgid "Security Reports|There was an error adding the comment."
+msgid "SecurityReports|Comment added to '%{vulnerabilityName}'"
msgstr ""
-msgid "Security Reports|There was an error creating the issue."
+msgid "SecurityReports|Comment deleted on '%{vulnerabilityName}'"
msgstr ""
-msgid "Security Reports|There was an error creating the merge request."
+msgid "SecurityReports|Comment edited on '%{vulnerabilityName}'"
msgstr ""
-msgid "Security Reports|There was an error deleting the comment."
+msgid "SecurityReports|Create issue"
msgstr ""
-msgid "Security Reports|There was an error dismissing the vulnerabilities."
+msgid "SecurityReports|Dismiss Selected"
msgstr ""
-msgid "Security Reports|There was an error dismissing the vulnerability."
+msgid "SecurityReports|Dismiss vulnerability"
msgstr ""
-msgid "Security Reports|There was an error reverting the dismissal."
+msgid "SecurityReports|Dismissed '%{vulnerabilityName}'"
msgstr ""
-msgid "Security Reports|There was an error reverting this dismissal."
+msgid "SecurityReports|Dismissed '%{vulnerabilityName}'. Turn off the hide dismissed toggle to view."
msgstr ""
-msgid "Security Reports|Undo dismiss"
+msgid "SecurityReports|Each vulnerability now has a unique page that can be directly linked to, shared, referenced, and tracked as the single source of truth. Vulnerability occurrences also persist across scanner runs, which improves tracking and visibility and reduces duplicates between scans."
msgstr ""
-msgid "Security Reports|You do not have sufficient permissions to access this report"
+msgid "SecurityReports|Edit dashboard"
msgstr ""
-msgid "Security Reports|You must sign in as an authorized user to see this report"
+msgid "SecurityReports|Either you don't have permission to view this dashboard or the dashboard has not been setup. Please check your permission settings with your administrator or check your dashboard configurations to proceed."
msgstr ""
-msgid "Security configuration help link"
+msgid "SecurityReports|Error fetching the vulnerability counts. Please check your network connection and try again."
msgstr ""
-msgid "Security dashboard"
+msgid "SecurityReports|Error fetching the vulnerability list. Please check your network connection and try again."
msgstr ""
-msgid "Security report is out of date. Please update your branch with the latest changes from the target branch (%{targetBranchName})"
+msgid "SecurityReports|False positive"
msgstr ""
-msgid "Security report is out of date. Run %{newPipelineLinkStart}a new pipeline%{newPipelineLinkEnd} for the target branch (%{targetBranchName})"
+msgid "SecurityReports|Hide dismissed"
msgstr ""
-msgid "SecurityConfiguration|Configured"
+msgid "SecurityReports|Introducing standalone vulnerabilities"
msgstr ""
-msgid "SecurityConfiguration|Feature"
+msgid "SecurityReports|Issue Created"
msgstr ""
-msgid "SecurityConfiguration|Feature documentation for %{featureName}"
+msgid "SecurityReports|Learn More"
msgstr ""
-msgid "SecurityConfiguration|Not yet configured"
+msgid "SecurityReports|Learn more about setting up your dashboard"
msgstr ""
-msgid "SecurityConfiguration|Secure features"
+msgid "SecurityReports|Load more vulnerabilities"
msgstr ""
-msgid "SecurityConfiguration|Status"
+msgid "SecurityReports|Monitor vulnerabilities in your code"
+msgstr ""
+
+msgid "SecurityReports|More info"
+msgstr ""
+
+msgid "SecurityReports|More information"
+msgstr ""
+
+msgid "SecurityReports|No vulnerabilities found for dashboard"
+msgstr ""
+
+msgid "SecurityReports|No vulnerabilities found for this group"
+msgstr ""
+
+msgid "SecurityReports|No vulnerabilities found for this pipeline"
+msgstr ""
+
+msgid "SecurityReports|No vulnerabilities found for this project"
+msgstr ""
+
+msgid "SecurityReports|Oops, something doesn't seem right."
+msgstr ""
+
+msgid "SecurityReports|Pipeline %{pipelineLink} triggered %{timeago} by %{user}"
msgstr ""
-msgid "SecurityDashboard|%{firstProject} and %{secondProject}"
+msgid "SecurityReports|Project"
msgstr ""
-msgid "SecurityDashboard|%{firstProject}, %{secondProject}, and %{rest}"
+msgid "SecurityReports|Projects added"
msgstr ""
-msgid "SecurityDashboard|Add a project to your dashboard"
+msgid "SecurityReports|Remove project from dashboard"
msgstr ""
-msgid "SecurityDashboard|Add or remove projects from your dashboard"
+msgid "SecurityReports|Report type"
msgstr ""
-msgid "SecurityDashboard|Add projects"
+msgid "SecurityReports|Return to dashboard"
msgstr ""
-msgid "SecurityDashboard|Edit dashboard"
+msgid "SecurityReports|Security Dashboard"
msgstr ""
-msgid "SecurityDashboard|Hide dismissed"
+msgid "SecurityReports|Security reports can only be accessed by authorized users."
msgstr ""
-msgid "SecurityDashboard|Monitor vulnerabilities in your code"
+msgid "SecurityReports|Select a project to add by using the project search field above."
msgstr ""
-msgid "SecurityDashboard|More information"
+msgid "SecurityReports|Select a reason"
msgstr ""
-msgid "SecurityDashboard|No vulnerabilities found for dashboard"
+msgid "SecurityReports|Severity"
msgstr ""
-msgid "SecurityDashboard|Pipeline %{pipelineLink} triggered %{timeago} by %{user}"
+msgid "SecurityReports|Status"
msgstr ""
-msgid "SecurityDashboard|Project"
+msgid "SecurityReports|The security dashboard displays the latest security findings for projects you wish to monitor. Select \"Edit dashboard\" to add and remove projects."
msgstr ""
-msgid "SecurityDashboard|Projects added"
+msgid "SecurityReports|The security dashboard displays the latest security report. Use it to find and fix vulnerabilities."
msgstr ""
-msgid "SecurityDashboard|Remove project from dashboard"
+msgid "SecurityReports|There was an error adding the comment."
msgstr ""
-msgid "SecurityDashboard|Report type"
+msgid "SecurityReports|There was an error creating the issue."
msgstr ""
-msgid "SecurityDashboard|Return to dashboard"
+msgid "SecurityReports|There was an error creating the merge request."
msgstr ""
-msgid "SecurityDashboard|Security Dashboard"
+msgid "SecurityReports|There was an error deleting the comment."
msgstr ""
-msgid "SecurityDashboard|Select a project to add by using the project search field above."
+msgid "SecurityReports|There was an error dismissing the vulnerabilities."
msgstr ""
-msgid "SecurityDashboard|Severity"
+msgid "SecurityReports|There was an error dismissing the vulnerability."
msgstr ""
-msgid "SecurityDashboard|Status"
+msgid "SecurityReports|There was an error reverting the dismissal."
msgstr ""
-msgid "SecurityDashboard|The security dashboard displays the latest security findings for projects you wish to monitor. Select \"Edit dashboard\" to add and remove projects."
+msgid "SecurityReports|There was an error reverting this dismissal."
msgstr ""
-msgid "SecurityDashboard|The security dashboard displays the latest security report. Use it to find and fix vulnerabilities."
+msgid "SecurityReports|There was an error while generating the report."
msgstr ""
-msgid "SecurityDashboard|There was an error while generating the report."
+msgid "SecurityReports|Unable to add %{invalidProjects}"
msgstr ""
-msgid "SecurityDashboard|Unable to add %{invalidProjects}"
+msgid "SecurityReports|Undo dismiss"
+msgstr ""
+
+msgid "SecurityReports|While it's rare to have no vulnerabilities for your group, it can happen. In any event, we ask that you double check your settings to make sure you've set up your dashboard correctly."
+msgstr ""
+
+msgid "SecurityReports|While it's rare to have no vulnerabilities for your pipeline, it can happen. In any event, we ask that you double check your settings to make sure all security scanning jobs have passed successfully."
+msgstr ""
+
+msgid "SecurityReports|While it's rare to have no vulnerabilities for your project, it can happen. In any event, we ask that you double check your settings to make sure you've set up your dashboard correctly."
+msgstr ""
+
+msgid "SecurityReports|While it's rare to have no vulnerabilities, it can happen. In any event, we ask that you please double check your settings to make sure you've set up your dashboard correctly."
+msgstr ""
+
+msgid "SecurityReports|Won't fix / Accept risk"
+msgstr ""
+
+msgid "SecurityReports|You do not have sufficient permissions to access this report"
+msgstr ""
+
+msgid "SecurityReports|You must sign in as an authorized user to see this report"
+msgstr ""
+
+msgid "SecurityReports|[No reason]"
msgstr ""
msgid "See GitLab's %{password_policy_guidelines}"
@@ -18497,6 +18929,9 @@ msgstr ""
msgid "See the affected projects in the GitLab admin panel"
msgstr ""
+msgid "See what's new at GitLab"
+msgstr ""
+
msgid "Select"
msgstr ""
@@ -18902,9 +19337,6 @@ msgstr ""
msgid "Set up Jira Integration"
msgstr ""
-msgid "Set up Jira Integration illustration"
-msgstr ""
-
msgid "Set up a %{type} Runner automatically"
msgstr ""
@@ -18989,6 +19421,9 @@ msgstr ""
msgid "Severity: %{severity}"
msgstr ""
+msgid "Shards (%{shards})"
+msgstr ""
+
msgid "Shards to synchronize"
msgstr ""
@@ -19307,6 +19742,9 @@ msgstr ""
msgid "Snowplow"
msgstr ""
+msgid "Some child epics may be hidden due to applied filters"
+msgstr ""
+
msgid "Some email servers do not support overriding the email sender name. Enable this option to include the name of the author of the issue, merge request or comment in the email body instead."
msgstr ""
@@ -19448,6 +19886,9 @@ msgstr ""
msgid "Something went wrong while updating your list settings"
msgstr ""
+msgid "Something went wrong with your automatic subscription renewal"
+msgstr ""
+
msgid "Something went wrong, unable to add %{project} to dashboard"
msgstr ""
@@ -19733,6 +20174,9 @@ msgstr ""
msgid "Stage removed"
msgstr ""
+msgid "Standard"
+msgstr ""
+
msgid "Star a label to make it a priority label. Order the prioritized labels to change their relative priority, by dragging."
msgstr ""
@@ -19856,6 +20300,9 @@ msgstr ""
msgid "Static Application Security Testing (SAST)"
msgstr ""
+msgid "StaticSiteEditor|An error occurred while submitting your changes."
+msgstr ""
+
msgid "StaticSiteEditor|Branch could not be created."
msgstr ""
@@ -19865,18 +20312,30 @@ msgstr ""
msgid "StaticSiteEditor|Could not create merge request."
msgstr ""
+msgid "StaticSiteEditor|Incompatible file content"
+msgstr ""
+
msgid "StaticSiteEditor|Return to site"
msgstr ""
+msgid "StaticSiteEditor|Static site editor"
+msgstr ""
+
msgid "StaticSiteEditor|Success!"
msgstr ""
msgid "StaticSiteEditor|Summary of changes"
msgstr ""
+msgid "StaticSiteEditor|The Static Site Editor is currently configured to only edit Markdown content on pages generated from Middleman. Visit the documentation to learn more about configuring your site to use the Static Site Editor."
+msgstr ""
+
msgid "StaticSiteEditor|Update %{sourcePath} file"
msgstr ""
+msgid "StaticSiteEditor|View documentation"
+msgstr ""
+
msgid "StaticSiteEditor|View merge request"
msgstr ""
@@ -19934,6 +20393,12 @@ msgstr ""
msgid "StatusPage|Status page"
msgstr ""
+msgid "StatusPage|Status page URL"
+msgstr ""
+
+msgid "StatusPage|Status page frontend documentation"
+msgstr ""
+
msgid "StatusPage|To publish incidents to an external status page, GitLab will store a JSON file in your Amazon S3 account in a location accessible to your external status page service. Make sure to also set up %{docsLink}"
msgstr ""
@@ -20282,6 +20747,12 @@ msgstr ""
msgid "Synced"
msgstr ""
+msgid "Synchronization disabled"
+msgstr ""
+
+msgid "Synchronization of container repositories is disabled."
+msgstr ""
+
msgid "System"
msgstr ""
@@ -20435,6 +20906,9 @@ msgstr ""
msgid "Telephone number"
msgstr ""
+msgid "Telephone number (Optional)"
+msgstr ""
+
msgid "Template"
msgstr ""
@@ -20753,6 +21227,9 @@ msgstr ""
msgid "The license was successfully uploaded and is now active. You can see the details below."
msgstr ""
+msgid "The license was successfully uploaded and will be active from %{starts_at}. You can see the details below."
+msgstr ""
+
msgid "The maximum file size allowed is %{size}."
msgstr ""
@@ -21074,6 +21551,9 @@ msgstr ""
msgid "There was an error getting the epic participants."
msgstr ""
+msgid "There was an error importing the Jira project."
+msgstr ""
+
msgid "There was an error loading users activity calendar."
msgstr ""
@@ -21089,7 +21569,7 @@ msgstr ""
msgid "There was an error resetting user pipeline minutes."
msgstr ""
-msgid "There was an error saving this Geo Node"
+msgid "There was an error saving this Geo Node."
msgstr ""
msgid "There was an error saving your changes."
@@ -21152,6 +21632,9 @@ msgstr ""
msgid "They can be managed using the %{link}."
msgstr ""
+msgid "Third Party Advisory Link"
+msgstr ""
+
msgid "Third party offers"
msgstr ""
@@ -21188,6 +21671,9 @@ msgstr ""
msgid "This application will be able to:"
msgstr ""
+msgid "This attachment has been truncated to avoid exceeding the maximum allowed attachment size of 15MB. %{written_count} of %{issues_count} issues have been included. Consider re-exporting with a narrower selection of issues."
+msgstr ""
+
msgid "This block is self-referential"
msgstr ""
@@ -21329,6 +21815,12 @@ msgstr ""
msgid "This issue is confidential"
msgstr ""
+msgid "This issue is currently blocked by the following issues: %{issues}."
+msgstr ""
+
+msgid "This issue is in a child epic of the filtered epic"
+msgstr ""
+
msgid "This issue is locked."
msgstr ""
@@ -21527,7 +22019,10 @@ msgstr ""
msgid "This user will be the author of all events in the activity feed that are the result of an update, like new branches being created or new commits being pushed to existing branches. Upon creation or when reassigning you can only assign yourself to be the mirror user."
msgstr ""
-msgid "This variable can not be masked"
+msgid "This variable can not be masked."
+msgstr ""
+
+msgid "This variable does not match the expected pattern."
msgstr ""
msgid "This will help us personalize your onboarding experience."
@@ -21557,10 +22052,10 @@ msgstr ""
msgid "ThreatMonitoring|Application firewall not detected"
msgstr ""
-msgid "ThreatMonitoring|Container Network Policy"
+msgid "ThreatMonitoring|Container Network Policies are not installed or have been disabled. To view this data, ensure your Network Policies are installed and enabled for your cluster."
msgstr ""
-msgid "ThreatMonitoring|Container NetworkPolicies are not installed or has been disabled. To view this data, ensure you NetworkPolicies are installed and enabled for your cluster."
+msgid "ThreatMonitoring|Container Network Policy"
msgstr ""
msgid "ThreatMonitoring|Container NetworkPolicies not detected"
@@ -21593,7 +22088,7 @@ msgstr ""
msgid "ThreatMonitoring|Something went wrong, unable to fetch statistics"
msgstr ""
-msgid "ThreatMonitoring|The firewall is not installed or has been disabled. To view this data, ensure you firewall is installed and enabled for your cluster."
+msgid "ThreatMonitoring|The firewall is not installed or has been disabled. To view this data, ensure the web application firewall is installed and enabled for your cluster."
msgstr ""
msgid "ThreatMonitoring|The graph below is an overview of traffic coming to your application as tracked by the Web Application Firewall (WAF). View the docs for instructions on how to access the WAF logs to see what type of malicious traffic is trying to access your app. The docs link is also accessible by clicking the \"?\" icon next to the title below."
@@ -21662,6 +22157,9 @@ msgstr ""
msgid "Time in seconds GitLab will wait for a response from the external service. When the service does not respond in time, access will be denied."
msgstr ""
+msgid "Time of import: %{importTime}"
+msgstr ""
+
msgid "Time remaining"
msgstr ""
@@ -21854,7 +22352,7 @@ msgstr ""
msgid "Title:"
msgstr ""
-msgid "Titles and Filenames"
+msgid "Titles and Descriptions"
msgstr ""
msgid "To"
@@ -22061,7 +22559,7 @@ msgstr ""
msgid "Too many projects enabled. You will need to manage them via the console or the API."
msgstr ""
-msgid "Topics"
+msgid "Topics (optional)"
msgstr ""
msgid "Total"
@@ -22223,6 +22721,9 @@ msgstr ""
msgid "Try all GitLab has to offer for 30 days."
msgstr ""
+msgid "Try changing or removing filters."
+msgstr ""
+
msgid "Try to fork again"
msgstr ""
@@ -22316,6 +22817,9 @@ msgstr ""
msgid "Unable to connect to server: %{error}"
msgstr ""
+msgid "Unable to connect to the Jira instance. Please check your Jira integration configuration."
+msgstr ""
+
msgid "Unable to convert Kubernetes logs encoding to UTF-8"
msgstr ""
@@ -23267,6 +23771,9 @@ msgstr ""
msgid "View issue"
msgstr ""
+msgid "View issues"
+msgstr ""
+
msgid "View it on GitLab"
msgstr ""
@@ -23444,6 +23951,15 @@ msgstr ""
msgid "VulnerabilityManagement|Resolved %{timeago} by %{user}"
msgstr ""
+msgid "VulnerabilityManagement|Something went wrong while trying to delete the comment. Please try again later."
+msgstr ""
+
+msgid "VulnerabilityManagement|Something went wrong while trying to retrieve the vulnerability history. Please try again later."
+msgstr ""
+
+msgid "VulnerabilityManagement|Something went wrong while trying to save the comment. Please try again later."
+msgstr ""
+
msgid "VulnerabilityManagement|Something went wrong, could not create an issue."
msgstr ""
@@ -23474,6 +23990,9 @@ msgstr ""
msgid "VulnerabilityStatusTypes|Resolved"
msgstr ""
+msgid "Vulnerability|%{scannerName} (version %{scannerVersion})"
+msgstr ""
+
msgid "Vulnerability|Class"
msgstr ""
@@ -23507,7 +24026,10 @@ msgstr ""
msgid "Vulnerability|Project"
msgstr ""
-msgid "Vulnerability|Report Type"
+msgid "Vulnerability|Scanner Provider"
+msgstr ""
+
+msgid "Vulnerability|Scanner Type"
msgstr ""
msgid "Vulnerability|Severity"
@@ -23561,9 +24083,15 @@ msgstr ""
msgid "We sent you an email with reset password instructions"
msgstr ""
+msgid "We tried to automatically renew your %{strong}%{plan_name}%{strong_close} subscription for %{strong}%{namespace_name}%{strong_close} on %{expires_on} but something went wrong so your subscription was downgraded to the free plan. Don't worry, your data is safe. We suggest you check your payment method and get in touch with our support team (%{support_link}). They'll gladly help with your subscription renewal."
+msgstr ""
+
msgid "We want to be sure it is you, please confirm you are not a robot."
msgstr ""
+msgid "We will automatically renew your %{strong}%{plan_name}%{strong_close} subscription for %{strong}%{namespace_name}%{strong_close} on %{strong}%{expires_on}%{strong_close}. There's nothing that you need to do, we'll let you know when the renewal is complete. Need more seats, a higher plan or just want to review your payment method?"
+msgstr ""
+
msgid "We've found no vulnerabilities"
msgstr ""
@@ -23672,15 +24200,6 @@ msgstr ""
msgid "When:"
msgstr ""
-msgid "While it's rare to have no vulnerabilities for your group, it can happen. In any event, we ask that you double check your settings to make sure you've set up your dashboard correctly."
-msgstr ""
-
-msgid "While it's rare to have no vulnerabilities for your pipeline, it can happen. In any event, we ask that you double check your settings to make sure all security scanning jobs have passed successfully."
-msgstr ""
-
-msgid "While it's rare to have no vulnerabilities for your project, it can happen. In any event, we ask that you double check your settings to make sure you've set up your dashboard correctly."
-msgstr ""
-
msgid "While it's rare to have no vulnerabilities, it can happen. In any event, we ask that you please double check your settings to make sure you've set up your dashboard correctly."
msgstr ""
@@ -23711,9 +24230,6 @@ msgstr ""
msgid "Wiki"
msgstr ""
-msgid "Wiki pages"
-msgstr ""
-
msgid "Wiki was successfully updated."
msgstr ""
@@ -23897,6 +24413,9 @@ msgstr ""
msgid "Yes, add it"
msgstr ""
+msgid "Yes, close issue"
+msgstr ""
+
msgid "Yes, let me map Google Code users to full names or GitLab users."
msgstr ""
@@ -24092,6 +24611,9 @@ msgstr ""
msgid "You could not create a new trigger."
msgstr ""
+msgid "You didn't renew your %{strong}%{plan_name}%{strong_close} subscription for %{strong}%{namespace_name}%{strong_close} so it was downgraded to the free plan."
+msgstr ""
+
msgid "You didn't renew your %{strong}%{plan_name}%{strong_close} subscription so it was downgraded to the GitLab Core Plan."
msgstr ""
@@ -24158,6 +24680,9 @@ msgstr ""
msgid "You have declined the invitation to join %{label}."
msgstr ""
+msgid "You have imported from this project %{numberOfPreviousImportsForProject} times before. Each new import will create duplicate issues."
+msgstr ""
+
msgid "You have no permissions"
msgstr ""
@@ -24239,6 +24764,9 @@ msgstr ""
msgid "You need to upload a Google Takeout archive."
msgstr ""
+msgid "You reached %{usage_in_percent} of %{namespace_name}'s capacity (%{used_storage} of %{storage_limit})"
+msgstr ""
+
msgid "You tried to fork %{link_to_the_project} but it failed for the following reason:"
msgstr ""
@@ -24323,9 +24851,21 @@ msgstr ""
msgid "YouTube"
msgstr ""
+msgid "Your %{strong}%{plan_name}%{strong_close} subscription for %{strong}%{namespace_name}%{strong_close} will expire on %{strong}%{expires_on}%{strong_close}. After that, you will not to be able to create issues or merge requests as well as many other features."
+msgstr ""
+
msgid "Your %{strong}%{plan_name}%{strong_close} subscription will expire on %{strong}%{expires_on}%{strong_close}. After that, you will not to be able to create issues or merge requests as well as many other features."
msgstr ""
+msgid "Your CSV export has started. It will be emailed to %{email} when complete."
+msgstr ""
+
+msgid "Your CSV export of %{issues_count} from project %{project_link} has been added to this email as an attachment."
+msgstr ""
+
+msgid "Your CSV export of %{written_count} from project %{project_name} (%{project_url}) has been added to this email as an attachment."
+msgstr ""
+
msgid "Your Commit Email will be used for web based operations, such as edits and merges."
msgstr ""
@@ -24422,6 +24962,9 @@ msgstr ""
msgid "Your comment could not be updated! Please check your network connection and try again."
msgstr ""
+msgid "Your comment will be discarded."
+msgstr ""
+
msgid "Your custom stage '%{title}' was created"
msgstr ""
@@ -24461,6 +25004,9 @@ msgstr ""
msgid "Your new personal access token has been created."
msgstr ""
+msgid "Your new project access token has been created."
+msgstr ""
+
msgid "Your password isn't required to view this page. If a password or any other personal details are requested, please contact your administrator to report abuse."
msgstr ""
@@ -24479,12 +25025,18 @@ msgstr ""
msgid "Your request for access has been queued for review."
msgstr ""
+msgid "Your search didn't match any commits."
+msgstr ""
+
msgid "Your subscription expired!"
msgstr ""
msgid "Your subscription has been downgraded"
msgstr ""
+msgid "Your subscription will automatically renew in %{remaining_days}"
+msgstr ""
+
msgid "Your subscription will expire in %{remaining_days}"
msgstr ""
@@ -24527,7 +25079,7 @@ msgstr ""
msgid "allowed to fail"
msgstr ""
-msgid "already being used for another group or project milestone."
+msgid "already being used for another group or project %{timebox_name}."
msgstr ""
msgid "already has a \"created\" issue link"
@@ -24605,6 +25157,9 @@ msgstr ""
msgid "ciReport|%{linkStartTag}Learn more about SAST %{linkEndTag}"
msgstr ""
+msgid "ciReport|%{linkStartTag}Learn more about Secret Scanning %{linkEndTag}"
+msgstr ""
+
msgid "ciReport|%{linkStartTag}Learn more about codequality reports %{linkEndTag}"
msgstr ""
@@ -24788,6 +25343,12 @@ msgstr ""
msgid "ciReport|SAST"
msgstr ""
+msgid "ciReport|Secret scanning"
+msgstr ""
+
+msgid "ciReport|Secret scanning detects secrets and credentials vulnerabilities in your source code."
+msgstr ""
+
msgid "ciReport|Security scanning"
msgstr ""
@@ -25001,6 +25562,9 @@ msgstr ""
msgid "group"
msgstr ""
+msgid "groups"
+msgstr ""
+
msgid "has already been linked to another vulnerability"
msgstr ""
@@ -25172,9 +25736,6 @@ msgstr[5] ""
msgid "merged %{time_ago}"
msgstr ""
-msgid "milestone should belong either to a project or a group."
-msgstr ""
-
msgid "missing"
msgstr ""
@@ -25511,6 +26072,9 @@ msgstr ""
msgid "no contributions"
msgstr ""
+msgid "no expiration"
+msgstr ""
+
msgid "no one can merge"
msgstr ""
@@ -25571,6 +26135,9 @@ msgstr ""
msgid "pending removal"
msgstr ""
+msgid "per day"
+msgstr ""
+
msgid "pipeline"
msgstr ""
@@ -25607,6 +26174,12 @@ msgstr[5] ""
msgid "project avatar"
msgstr ""
+msgid "projects"
+msgstr ""
+
+msgid "push to your repository, create pipelines, create issues or add comments. To reduce storage capacity, delete unused repositories, artifacts, wikis, issues, and pipelines."
+msgstr ""
+
msgid "quick actions"
msgstr ""
@@ -25757,15 +26330,6 @@ msgstr ""
msgid "this document"
msgstr ""
-msgid "thread resolved"
-msgid_plural "threads resolved"
-msgstr[0] ""
-msgstr[1] ""
-msgstr[2] ""
-msgstr[3] ""
-msgstr[4] ""
-msgstr[5] ""
-
msgid "to help your contributors communicate effectively!"
msgstr ""
@@ -25820,6 +26384,9 @@ msgstr ""
msgid "view the blob"
msgstr ""
+msgid "vulnerability|Add a comment"
+msgstr ""
+
msgid "vulnerability|Add a comment or reason for dismissal"
msgstr ""
diff --git a/locale/bg/gitlab.po b/locale/bg/gitlab.po
index e274e832a38..d9feb7039b4 100644
--- a/locale/bg/gitlab.po
+++ b/locale/bg/gitlab.po
@@ -12,7 +12,7 @@ msgstr ""
"X-Crowdin-Project: gitlab-ee\n"
"X-Crowdin-Language: bg\n"
"X-Crowdin-File: /master/locale/gitlab.pot\n"
-"PO-Revision-Date: 2020-04-15 00:30\n"
+"PO-Revision-Date: 2020-05-05 21:36\n"
msgid " %{start} to %{end}"
msgstr ""
@@ -212,6 +212,11 @@ msgid_plural "%d tags"
msgstr[0] ""
msgstr[1] ""
+msgid "%d unresolved thread"
+msgid_plural "%d unresolved threads"
+msgstr[0] ""
+msgstr[1] ""
+
msgid "%d vulnerability dismissed"
msgid_plural "%d vulnerabilities dismissed"
msgstr[0] ""
@@ -371,6 +376,9 @@ msgstr ""
msgid "%{mrText}, this issue will be closed automatically."
msgstr ""
+msgid "%{namespace_name} is now read-only. You cannot: %{base_message}"
+msgstr ""
+
msgid "%{name} contained %{resultsString}"
msgstr ""
@@ -383,6 +391,11 @@ msgstr ""
msgid "%{name}'s avatar"
msgstr ""
+msgid "%{no_of_days} day"
+msgid_plural "%{no_of_days} days"
+msgstr[0] ""
+msgstr[1] ""
+
msgid "%{number_commits_behind} commits behind %{default_branch}, %{number_commits_ahead} commits ahead"
msgstr ""
@@ -506,6 +519,9 @@ msgstr[1] ""
msgid "%{text} is available"
msgstr ""
+msgid "%{timebox_name} should belong either to a project or a group."
+msgstr ""
+
msgid "%{title} %{operator} %{threshold}"
msgstr ""
@@ -654,8 +670,8 @@ msgstr[1] ""
msgid "1 closed issue"
msgid_plural "%{issues} closed issues"
-msgstr[0] ""
-msgstr[1] ""
+msgstr[0] "1 затворен проблем"
+msgstr[1] "%{issues} затворени проблема"
msgid "1 closed merge request"
msgid_plural "%{merge_requests} closed merge requests"
@@ -781,12 +797,18 @@ msgstr ""
msgid "<code>\"johnsmith@example.com\": \"johnsmith@example.com\"</code> will add \"By <a href=\"#\">johnsmith@example.com</a>\" to all issues and comments originally created by johnsmith@example.com. By default, the email address or username is masked to ensure the user's privacy. Use this option if you want to show the full email address."
msgstr ""
+msgid "<namespace / project>"
+msgstr ""
+
msgid "<no name set>"
msgstr ""
msgid "<no scopes selected>"
msgstr ""
+msgid "<project name>"
+msgstr ""
+
msgid "<strong>%{group_name}</strong> group members"
msgstr ""
@@ -820,6 +842,9 @@ msgstr ""
msgid "A basic page and serverless function that uses AWS Lambda, AWS API Gateway, and GitLab Pages"
msgstr ""
+msgid "A complete DevOps platform"
+msgstr ""
+
msgid "A default branch cannot be chosen for an empty project."
msgstr ""
@@ -1036,6 +1061,12 @@ msgstr ""
msgid "AccessTokens|reset it"
msgstr ""
+msgid "AccessibilityReport|Accessibility report artifact not found"
+msgstr ""
+
+msgid "AccessibilityReport|Failed to retrieve accessibility report"
+msgstr ""
+
msgid "AccessibilityReport|Learn More"
msgstr ""
@@ -1045,6 +1076,9 @@ msgstr ""
msgid "AccessibilityReport|New"
msgstr ""
+msgid "AccessibilityReport|The accessibility scanning found an error of the following type: %{code}"
+msgstr ""
+
msgid "Account"
msgstr ""
@@ -1287,6 +1321,9 @@ msgstr ""
msgid "Added at"
msgstr ""
+msgid "Added for this merge request"
+msgstr ""
+
msgid "Added in this version"
msgstr ""
@@ -1658,6 +1695,72 @@ msgid_plural "Alerts"
msgstr[0] ""
msgstr[1] ""
+msgid "Alert Details"
+msgstr ""
+
+msgid "AlertManagement|Acknowledged"
+msgstr ""
+
+msgid "AlertManagement|Alert"
+msgstr ""
+
+msgid "AlertManagement|Authorize external service"
+msgstr ""
+
+msgid "AlertManagement|Display alerts from all your monitoring tools directly within GitLab. Streamline the investigation of your alerts and the escalation of alerts to incidents."
+msgstr ""
+
+msgid "AlertManagement|End time"
+msgstr ""
+
+msgid "AlertManagement|End time:"
+msgstr ""
+
+msgid "AlertManagement|Events"
+msgstr ""
+
+msgid "AlertManagement|Events:"
+msgstr ""
+
+msgid "AlertManagement|Full Alert Details"
+msgstr ""
+
+msgid "AlertManagement|More information"
+msgstr ""
+
+msgid "AlertManagement|No alerts available to display. If you think you're seeing this message in error, refresh the page."
+msgstr ""
+
+msgid "AlertManagement|No alerts to display."
+msgstr ""
+
+msgid "AlertManagement|Overview"
+msgstr ""
+
+msgid "AlertManagement|Resolved"
+msgstr ""
+
+msgid "AlertManagement|Severity"
+msgstr ""
+
+msgid "AlertManagement|Start time"
+msgstr ""
+
+msgid "AlertManagement|Start time:"
+msgstr ""
+
+msgid "AlertManagement|Status"
+msgstr ""
+
+msgid "AlertManagement|Surface alerts in GitLab"
+msgstr ""
+
+msgid "AlertManagement|There was an error displaying the alerts. Confirm your endpoint's configuration details to ensure alerts appear."
+msgstr ""
+
+msgid "AlertManagement|Triggered"
+msgstr ""
+
msgid "AlertService|%{linkStart}Learn more%{linkEnd} about configuring this endpoint to receive alerts."
msgstr ""
@@ -1727,6 +1830,9 @@ msgstr ""
msgid "All security scans are enabled because %{linkStart}Auto DevOps%{linkEnd} is enabled on this project"
msgstr ""
+msgid "All threads resolved"
+msgstr ""
+
msgid "All users"
msgstr ""
@@ -1745,6 +1851,9 @@ msgstr ""
msgid "Allow only the selected protocols to be used for Git access."
msgstr ""
+msgid "Allow owners to manage default branch protection per group"
+msgstr ""
+
msgid "Allow owners to manually add users outside of LDAP"
msgstr ""
@@ -1853,6 +1962,9 @@ msgstr ""
msgid "An error occurred fetching the dropdown data."
msgstr ""
+msgid "An error occurred fetching the project authors."
+msgstr ""
+
msgid "An error occurred previewing the blob"
msgstr ""
@@ -1934,6 +2046,9 @@ msgstr ""
msgid "An error occurred while fetching sidebar data"
msgstr ""
+msgid "An error occurred while fetching terraform reports."
+msgstr ""
+
msgid "An error occurred while fetching the Service Desk address."
msgstr ""
@@ -2171,6 +2286,9 @@ msgstr ""
msgid "Any"
msgstr ""
+msgid "Any Author"
+msgstr ""
+
msgid "Any Label"
msgstr ""
@@ -2386,10 +2504,10 @@ msgstr ""
msgid "Are you sure that you want to unarchive this project?"
msgstr ""
-msgid "Are you sure you want to cancel creating this comment?"
+msgid "Are you sure you want to cancel editing this comment?"
msgstr ""
-msgid "Are you sure you want to cancel editing this comment?"
+msgid "Are you sure you want to close this blocked issue?"
msgstr ""
msgid "Are you sure you want to delete %{name}?"
@@ -2419,6 +2537,9 @@ msgstr ""
msgid "Are you sure you want to deploy this environment?"
msgstr ""
+msgid "Are you sure you want to discard this comment?"
+msgstr ""
+
msgid "Are you sure you want to erase this build?"
msgstr ""
@@ -2595,6 +2716,9 @@ msgstr ""
msgid "At least one approval from a code owner is required to change files matching the respective CODEOWNER rules."
msgstr ""
+msgid "At least one logging option is required to be enabled"
+msgstr ""
+
msgid "At least one of group_id or project_id must be specified"
msgstr ""
@@ -2633,6 +2757,27 @@ msgstr ""
msgid "AuditEvents|Target"
msgstr ""
+msgid "AuditLogs|(removed)"
+msgstr ""
+
+msgid "AuditLogs|Action"
+msgstr ""
+
+msgid "AuditLogs|Author"
+msgstr ""
+
+msgid "AuditLogs|Date"
+msgstr ""
+
+msgid "AuditLogs|IP Address"
+msgstr ""
+
+msgid "AuditLogs|Object"
+msgstr ""
+
+msgid "AuditLogs|Target"
+msgstr ""
+
msgid "Aug"
msgstr ""
@@ -2915,9 +3060,6 @@ msgstr ""
msgid "BambooService|You must set up automatic revision labeling and a repository trigger in Bamboo."
msgstr ""
-msgid "Batch operations"
-msgstr ""
-
msgid "BatchComments|Delete all pending comments"
msgstr ""
@@ -3047,6 +3189,9 @@ msgstr ""
msgid "Boards"
msgstr ""
+msgid "Boards and Board Lists"
+msgstr ""
+
msgid "Boards|Collapse"
msgstr ""
@@ -3266,6 +3411,9 @@ msgstr ""
msgid "Buy GitLab Enterprise Edition"
msgstr ""
+msgid "Buy more Pipeline minutes"
+msgstr ""
+
msgid "By %{user_name}"
msgstr ""
@@ -3386,6 +3534,9 @@ msgstr ""
msgid "Can't scan the code?"
msgstr ""
+msgid "Can't update snippet: %{err}"
+msgstr ""
+
msgid "Canary"
msgstr ""
@@ -3554,6 +3705,9 @@ msgstr ""
msgid "Changing group path can have unintended side effects."
msgstr ""
+msgid "Charts"
+msgstr ""
+
msgid "Charts can't be displayed as the request for data has timed out. %{documentationLink}"
msgstr ""
@@ -3590,9 +3744,6 @@ msgstr ""
msgid "ChatMessage|and [%{count} more](%{pipeline_failed_jobs_url})"
msgstr ""
-msgid "ChatMessage|failed"
-msgstr ""
-
msgid "ChatMessage|has failed"
msgstr ""
@@ -3608,9 +3759,6 @@ msgstr ""
msgid "ChatMessage|in %{project_link}"
msgstr ""
-msgid "ChatMessage|passed"
-msgstr ""
-
msgid "Check again"
msgstr ""
@@ -4061,9 +4209,6 @@ msgstr ""
msgid "Click any <strong>project name</strong> in the project list below to navigate to the project milestone."
msgstr ""
-msgid "Click here"
-msgstr ""
-
msgid "Click the <strong>Download</strong> button and wait for downloading to complete."
msgstr ""
@@ -4451,6 +4596,12 @@ msgstr ""
msgid "ClusterIntegration|Fetching zones"
msgstr ""
+msgid "ClusterIntegration|Fluentd"
+msgstr ""
+
+msgid "ClusterIntegration|Fluentd is an open source data collector, which lets you unify the data collection and consumption for a better use and understanding of data. It requires at least one of the following logs to be successfully installed."
+msgstr ""
+
msgid "ClusterIntegration|GitLab Integration"
msgstr ""
@@ -4757,6 +4908,15 @@ msgstr ""
msgid "ClusterIntegration|Request to begin uninstalling failed"
msgstr ""
+msgid "ClusterIntegration|SIEM Hostname"
+msgstr ""
+
+msgid "ClusterIntegration|SIEM Port"
+msgstr ""
+
+msgid "ClusterIntegration|SIEM Protocol"
+msgstr ""
+
msgid "ClusterIntegration|Save changes"
msgstr ""
@@ -4853,6 +5013,12 @@ msgstr ""
msgid "ClusterIntegration|Select zone to choose machine type"
msgstr ""
+msgid "ClusterIntegration|Send Cilium Logs"
+msgstr ""
+
+msgid "ClusterIntegration|Send ModSecurity Logs"
+msgstr ""
+
msgid "ClusterIntegration|Service Token"
msgstr ""
@@ -5281,18 +5447,33 @@ msgstr ""
msgid "Compliance framework (optional)"
msgstr ""
+msgid "ComplianceFramework|GDPR"
+msgstr ""
+
msgid "ComplianceFramework|GDPR - General Data Protection Regulation"
msgstr ""
+msgid "ComplianceFramework|HIPAA"
+msgstr ""
+
msgid "ComplianceFramework|HIPAA - Health Insurance Portability and Accountability Act"
msgstr ""
+msgid "ComplianceFramework|PCI-DSS"
+msgstr ""
+
msgid "ComplianceFramework|PCI-DSS - Payment Card Industry-Data Security Standard"
msgstr ""
+msgid "ComplianceFramework|SOC 2"
+msgstr ""
+
msgid "ComplianceFramework|SOC 2 - Service Organization Control 2"
msgstr ""
+msgid "ComplianceFramework|SOX"
+msgstr ""
+
msgid "ComplianceFramework|SOX - Sarbanes-Oxley"
msgstr ""
@@ -5440,9 +5621,15 @@ msgstr ""
msgid "Container repositories sync capacity"
msgstr ""
+msgid "ContainerRegistry| Please visit the %{linkStart}administration settings%{linkEnd} to enable this feature."
+msgstr ""
+
msgid "ContainerRegistry|%{imageName} tags"
msgstr ""
+msgid "ContainerRegistry|%{title} was successfully scheduled for deletion"
+msgstr ""
+
msgid "ContainerRegistry|Automatically remove extra images that aren't designed to be kept."
msgstr ""
@@ -5455,6 +5642,9 @@ msgstr ""
msgid "ContainerRegistry|Container Registry"
msgstr ""
+msgid "ContainerRegistry|Container Registry tag expiration and retention policy is disabled"
+msgstr ""
+
msgid "ContainerRegistry|Copy build command"
msgstr ""
@@ -5464,18 +5654,12 @@ msgstr ""
msgid "ContainerRegistry|Copy push command"
msgstr ""
-msgid "ContainerRegistry|Currently, the Container Registry tag expiration feature is not available for projects created before GitLab version 12.8. For updates and more information, visit Issue %{linkStart}#196124%{linkEnd}"
-msgstr ""
-
msgid "ContainerRegistry|Docker connection error"
msgstr ""
msgid "ContainerRegistry|Docker tag expiration policy is %{toggleStatus}"
msgstr ""
-msgid "ContainerRegistry|Docker tags with names matching this regex pattern will expire:"
-msgstr ""
-
msgid "ContainerRegistry|Edit Settings"
msgstr ""
@@ -5497,9 +5681,6 @@ msgstr ""
msgid "ContainerRegistry|Image ID"
msgstr ""
-msgid "ContainerRegistry|Image deleted successfully"
-msgstr ""
-
msgid "ContainerRegistry|Keep and protect the images that matter most."
msgstr ""
@@ -5515,12 +5696,24 @@ msgstr ""
msgid "ContainerRegistry|Number of tags to retain:"
msgstr ""
+msgid "ContainerRegistry|Please contact your administrator."
+msgstr ""
+
msgid "ContainerRegistry|Push an image"
msgstr ""
msgid "ContainerRegistry|Quick Start"
msgstr ""
+msgid "ContainerRegistry|Regular expressions such as %{codeStart}.*-test%{codeEnd} or %{codeStart}dev-.*%{codeEnd} are supported"
+msgstr ""
+
+msgid "ContainerRegistry|Regular expressions such as %{codeStart}.*-test%{codeEnd} or %{codeStart}dev-.*%{codeEnd} are supported. To select all tags, use %{codeStart}.*%{codeEnd}"
+msgstr ""
+
+msgid "ContainerRegistry|Remember to run %{docLinkStart}garbage collection%{docLinkEnd} to remove the stale data from storage."
+msgstr ""
+
msgid "ContainerRegistry|Remove repository"
msgstr ""
@@ -5535,22 +5728,22 @@ msgstr[1] ""
msgid "ContainerRegistry|Retention policy has been Enabled"
msgstr ""
-msgid "ContainerRegistry|Something went wrong while deleting the image."
+msgid "ContainerRegistry|Something went wrong while fetching the expiration policy."
msgstr ""
-msgid "ContainerRegistry|Something went wrong while deleting the tag."
+msgid "ContainerRegistry|Something went wrong while fetching the repository list."
msgstr ""
-msgid "ContainerRegistry|Something went wrong while deleting the tags."
+msgid "ContainerRegistry|Something went wrong while fetching the tags list."
msgstr ""
-msgid "ContainerRegistry|Something went wrong while fetching the expiration policy."
+msgid "ContainerRegistry|Something went wrong while marking the tag for deletion."
msgstr ""
-msgid "ContainerRegistry|Something went wrong while fetching the packages list."
+msgid "ContainerRegistry|Something went wrong while marking the tags for deletion."
msgstr ""
-msgid "ContainerRegistry|Something went wrong while fetching the tags list."
+msgid "ContainerRegistry|Something went wrong while scheduling %{title} for deletion. Please try again."
msgstr ""
msgid "ContainerRegistry|Something went wrong while updating the expiration policy."
@@ -5559,16 +5752,25 @@ msgstr ""
msgid "ContainerRegistry|Tag"
msgstr ""
-msgid "ContainerRegistry|Tag deleted successfully"
-msgstr ""
-
msgid "ContainerRegistry|Tag expiration policy"
msgstr ""
msgid "ContainerRegistry|Tag expiration policy is designed to:"
msgstr ""
-msgid "ContainerRegistry|Tags deleted successfully"
+msgid "ContainerRegistry|Tag successfully marked for deletion."
+msgstr ""
+
+msgid "ContainerRegistry|Tags successfully marked for deletion."
+msgstr ""
+
+msgid "ContainerRegistry|Tags with names matching this regex pattern will %{italicStart}be preserved:%{italicEnd}"
+msgstr ""
+
+msgid "ContainerRegistry|Tags with names matching this regex pattern will %{italicStart}expire:%{italicEnd}"
+msgstr ""
+
+msgid "ContainerRegistry|The Container Registry tag expiration and retention policies for this project have not been enabled."
msgstr ""
msgid "ContainerRegistry|The last tag related to this image was recently removed. This empty image and any associated data will be automatically removed as part of the regular Garbage Collection process. If you have any questions, contact your administrator."
@@ -5589,16 +5791,16 @@ msgstr ""
msgid "ContainerRegistry|There are no container images stored for this project"
msgstr ""
-msgid "ContainerRegistry|This Registry contains deleted image tag data. Remember to run %{docLinkStart}garbage collection%{docLinkEnd} to remove the stale data from storage."
+msgid "ContainerRegistry|There was an error during the deletion of this image repository, please try again."
msgstr ""
msgid "ContainerRegistry|This image has no active tags"
msgstr ""
-msgid "ContainerRegistry|We are having trouble connecting to Docker, which could be due to an issue with your project name or path. %{docLinkStart}More Information%{docLinkEnd}"
+msgid "ContainerRegistry|This image repository is scheduled for deletion"
msgstr ""
-msgid "ContainerRegistry|Wildcards such as %{codeStart}.*-stable%{codeEnd} or %{codeStart}production/.*%{codeEnd} are supported. To select all tags, use %{codeStart}.*%{codeEnd}"
+msgid "ContainerRegistry|We are having trouble connecting to Docker, which could be due to an issue with your project name or path. %{docLinkStart}More Information%{docLinkEnd}"
msgstr ""
msgid "ContainerRegistry|With the Container Registry, every project can have its own space to store its Docker images. %{docLinkStart}More Information%{docLinkEnd}"
@@ -5853,7 +6055,7 @@ msgstr ""
msgid "Could not delete chat nickname %{chat_name}."
msgstr ""
-msgid "Could not find design"
+msgid "Could not find design."
msgstr ""
msgid "Could not remove the trigger."
@@ -5868,6 +6070,9 @@ msgstr ""
msgid "Could not revoke personal access token %{personal_access_token_name}."
msgstr ""
+msgid "Could not revoke project access token %{project_access_token_name}."
+msgstr ""
+
msgid "Could not save group ID"
msgstr ""
@@ -5925,6 +6130,9 @@ msgstr ""
msgid "Create a new branch"
msgstr ""
+msgid "Create a new deploy key for this project"
+msgstr ""
+
msgid "Create a new file as there are no files yet. Afterwards, you'll be able to commit your changes."
msgstr ""
@@ -6030,6 +6238,9 @@ msgstr ""
msgid "Create requirement"
msgstr ""
+msgid "Create snippet"
+msgstr ""
+
msgid "Create wildcard: %{searchTerm}"
msgstr ""
@@ -6051,6 +6262,9 @@ msgstr "Ñи Ñъздадете личен жетон за доÑтъп"
msgid "Created"
msgstr ""
+msgid "Created %{timestamp}"
+msgstr ""
+
msgid "Created At"
msgstr ""
@@ -6407,6 +6621,9 @@ msgstr ""
msgid "CycleAnalytics|stage dropdown"
msgstr ""
+msgid "DAG"
+msgstr ""
+
msgid "DNS"
msgstr ""
@@ -6773,6 +6990,9 @@ msgstr ""
msgid "Deploy key was successfully updated."
msgstr ""
+msgid "Deploy keys allow read-only or read-write (if enabled) access to your repository. Deploy keys can be used for CI, staging or production servers. You can create a deploy key or add an existing one."
+msgstr ""
+
msgid "Deploy progress not found. To see pods, ensure your environment matches %{linkStart}deploy board criteria%{linkEnd}."
msgstr ""
@@ -6926,6 +7146,9 @@ msgstr ""
msgid "Deploying to"
msgstr ""
+msgid "Deployment Frequency"
+msgstr ""
+
msgid "Deployment|API"
msgstr ""
@@ -7444,9 +7667,6 @@ msgstr ""
msgid "Edit environment"
msgstr ""
-msgid "Edit epic description"
-msgstr ""
-
msgid "Edit file"
msgstr ""
@@ -8068,9 +8288,6 @@ msgstr ""
msgid "Epics"
msgstr ""
-msgid "Epics (Ultimate / Gold license only)"
-msgstr ""
-
msgid "Epics Roadmap"
msgstr ""
@@ -8080,6 +8297,9 @@ msgstr ""
msgid "Epics let you manage your portfolio of projects more efficiently and with less effort"
msgstr ""
+msgid "Epics, Issues, and Merge Requests"
+msgstr ""
+
msgid "Epics|Add an epic"
msgstr ""
@@ -8131,6 +8351,9 @@ msgstr ""
msgid "Epics|Something went wrong while fetching group epics."
msgstr ""
+msgid "Epics|Something went wrong while moving item."
+msgstr ""
+
msgid "Epics|Something went wrong while ordering item."
msgstr ""
@@ -8185,6 +8408,9 @@ msgstr ""
msgid "Error fetching network graph."
msgstr ""
+msgid "Error fetching payload data."
+msgstr ""
+
msgid "Error fetching projects"
msgstr ""
@@ -8194,9 +8420,6 @@ msgstr ""
msgid "Error fetching the dependency list. Please check your network connection and try again."
msgstr ""
-msgid "Error fetching usage ping data."
-msgstr ""
-
msgid "Error loading branch data. Please try again."
msgstr ""
@@ -8338,6 +8561,9 @@ msgstr ""
msgid "Errors"
msgstr ""
+msgid "Errors:"
+msgstr ""
+
msgid "Estimated"
msgstr ""
@@ -8542,12 +8768,18 @@ msgstr ""
msgid "Export as CSV"
msgstr ""
+msgid "Export group"
+msgstr ""
+
msgid "Export issues"
msgstr ""
msgid "Export project"
msgstr ""
+msgid "Export this group with all related data to a new GitLab instance. Once complete, you can import the data file from the \"New Group\" page."
+msgstr ""
+
msgid "Export this project with all its related data in order to move your project to a new GitLab instance. Once the export is finished, you can import the file from the \"New Project\" page."
msgstr ""
@@ -8617,9 +8849,6 @@ msgstr ""
msgid "Failed Jobs"
msgstr ""
-msgid "Failed create wiki"
-msgstr ""
-
msgid "Failed to add a Zoom meeting"
msgstr ""
@@ -8659,6 +8888,9 @@ msgstr ""
msgid "Failed to create resources"
msgstr ""
+msgid "Failed to create wiki"
+msgstr ""
+
msgid "Failed to delete board. Please try again."
msgstr ""
@@ -8668,7 +8900,7 @@ msgstr ""
msgid "Failed to enqueue the rebase operation, possibly due to a long-lived transaction. Try again later."
msgstr ""
-msgid "Failed to find import label for jira import."
+msgid "Failed to find import label for Jira import."
msgstr ""
msgid "Failed to get ref."
@@ -8692,6 +8924,12 @@ msgstr ""
msgid "Failed to load groups & users."
msgstr ""
+msgid "Failed to load labels. Please try again."
+msgstr ""
+
+msgid "Failed to load milestones. Please try again."
+msgstr ""
+
msgid "Failed to load related branches"
msgstr ""
@@ -9067,7 +9305,10 @@ msgstr "Филтриране по Ñъобщение"
msgid "Filter by milestone name"
msgstr ""
-msgid "Filter by name..."
+msgid "Filter by name"
+msgstr ""
+
+msgid "Filter by status"
msgstr ""
msgid "Filter by two-factor authentication"
@@ -9079,12 +9320,18 @@ msgstr ""
msgid "Filter projects"
msgstr ""
+msgid "Filter results"
+msgstr ""
+
msgid "Filter results by group"
msgstr ""
msgid "Filter results by project"
msgstr ""
+msgid "Filter results..."
+msgstr ""
+
msgid "Filter your projects by name"
msgstr ""
@@ -9373,10 +9620,10 @@ msgstr ""
msgid "GeoNodes|Checksummed"
msgstr ""
-msgid "GeoNodes|Container repositories"
+msgid "GeoNodes|Consult Geo troubleshooting information"
msgstr ""
-msgid "GeoNodes|Data is out of date from %{timeago}"
+msgid "GeoNodes|Container repositories"
msgstr ""
msgid "GeoNodes|Data replication lag"
@@ -9418,6 +9665,9 @@ msgstr ""
msgid "GeoNodes|Last event ID seen from primary"
msgstr ""
+msgid "GeoNodes|Learn more about Geo node statuses"
+msgstr ""
+
msgid "GeoNodes|Loading nodes"
msgstr ""
@@ -9433,6 +9683,9 @@ msgstr ""
msgid "GeoNodes|Node was successfully removed."
msgstr ""
+msgid "GeoNodes|Node's status was updated %{timeAgo}."
+msgstr ""
+
msgid "GeoNodes|Not checksummed"
msgstr ""
@@ -9463,7 +9716,10 @@ msgstr ""
msgid "GeoNodes|Repository verification progress"
msgstr ""
-msgid "GeoNodes|Selective"
+msgid "GeoNodes|Selective (%{syncLabel})"
+msgstr ""
+
+msgid "GeoNodes|Selective synchronization"
msgstr ""
msgid "GeoNodes|Something went wrong while changing node status"
@@ -9490,6 +9746,9 @@ msgstr ""
msgid "GeoNodes|Unverified"
msgstr ""
+msgid "GeoNodes|Updated %{timeAgo}"
+msgstr ""
+
msgid "GeoNodes|Used slots"
msgstr ""
@@ -9529,16 +9788,16 @@ msgstr ""
msgid "Geo|All"
msgstr ""
-msgid "Geo|All projects"
+msgid "Geo|All %{replicable_type}"
msgstr ""
-msgid "Geo|All projects are being scheduled for re-sync"
+msgid "Geo|All projects"
msgstr ""
-msgid "Geo|All projects are being scheduled for re-verify"
+msgid "Geo|All projects are being scheduled for resync"
msgstr ""
-msgid "Geo|Batch operations"
+msgid "Geo|All projects are being scheduled for reverify"
msgstr ""
msgid "Geo|Could not remove tracking entry for an existing project."
@@ -9550,9 +9809,15 @@ msgstr ""
msgid "Geo|Failed"
msgstr ""
+msgid "Geo|Filter by status"
+msgstr ""
+
msgid "Geo|Geo Status"
msgstr ""
+msgid "Geo|In progress"
+msgstr ""
+
msgid "Geo|In sync"
msgstr ""
@@ -9577,9 +9842,6 @@ msgstr ""
msgid "Geo|Not synced yet"
msgstr ""
-msgid "Geo|Pending"
-msgstr ""
-
msgid "Geo|Pending synchronization"
msgstr ""
@@ -9616,7 +9878,7 @@ msgstr ""
msgid "Geo|Resync"
msgstr ""
-msgid "Geo|Resync all projects"
+msgid "Geo|Resync all"
msgstr ""
msgid "Geo|Retry count"
@@ -9625,7 +9887,7 @@ msgstr ""
msgid "Geo|Reverify"
msgstr ""
-msgid "Geo|Reverify all projects"
+msgid "Geo|Reverify all"
msgstr ""
msgid "Geo|Status"
@@ -9967,9 +10229,6 @@ msgstr ""
msgid "Go to file"
msgstr ""
-msgid "Go to file (MRs only)"
-msgstr ""
-
msgid "Go to file permalink (while viewing a file)"
msgstr ""
@@ -10414,6 +10673,9 @@ msgstr ""
msgid "GroupSettings|Disable group mentions"
msgstr ""
+msgid "GroupSettings|Export group"
+msgstr ""
+
msgid "GroupSettings|If the parent group's visibility is lower than the group current visibility, visibility levels for subgroups and projects will be changed to match the new parent group's visibility."
msgstr ""
@@ -10492,6 +10754,9 @@ msgstr ""
msgid "Groups (%{count})"
msgstr ""
+msgid "Groups (%{groups})"
+msgstr ""
+
msgid "Groups can also be nested by creating %{subgroup_docs_link_start}subgroups%{subgroup_docs_link_end}."
msgstr ""
@@ -10757,15 +11022,12 @@ msgstr ""
msgid "ID:"
msgstr ""
-msgid "IDE|Allow live previews of JavaScript projects in the Web IDE using CodeSandbox client side evaluation."
+msgid "IDE|Allow live previews of JavaScript projects in the Web IDE using CodeSandbox Live Preview."
msgstr ""
msgid "IDE|Back"
msgstr ""
-msgid "IDE|Client side evaluation"
-msgstr ""
-
msgid "IDE|Commit"
msgstr ""
@@ -10829,6 +11091,9 @@ msgstr ""
msgid "If any job surpasses this timeout threshold, it will be marked as failed. Human readable time input language is accepted like \"1 hour\". Values without specification represent seconds."
msgstr ""
+msgid "If blank, set allowable lifetime to %{instance_level_policy_in_words}, as defined by the instance admin. Once set, existing tokens for users in this group may be revoked."
+msgstr ""
+
msgid "If checked, group owners can manage LDAP group links and LDAP member overrides"
msgstr ""
@@ -10868,6 +11133,9 @@ msgstr ""
msgid "If you lose your recovery codes you can generate new ones, invalidating all previous codes."
msgstr ""
+msgid "If you reach 100%% storage capacity, you will not be able to: %{base_message}"
+msgstr ""
+
msgid "If your HTTP repository is not publicly accessible, add your credentials."
msgstr ""
@@ -10979,6 +11247,9 @@ msgstr ""
msgid "Import repository"
msgstr "ВнаÑÑне на хранилище"
+msgid "Import started by: %{importInitiator}"
+msgstr ""
+
msgid "Import tasks"
msgstr ""
@@ -11045,6 +11316,9 @@ msgstr ""
msgid "In order to tailor your experience with GitLab we<br>would like to know a bit more about you."
msgstr ""
+msgid "In progress"
+msgstr ""
+
msgid "In the next step, you'll be able to select the projects you want to import."
msgstr ""
@@ -11392,15 +11666,15 @@ msgstr ""
msgid "Issues"
msgstr ""
-msgid "Issues / Merge Requests"
-msgstr ""
-
msgid "Issues Analytics"
msgstr ""
msgid "Issues Rate Limits"
msgstr ""
+msgid "Issues and Merge Requests"
+msgstr ""
+
msgid "Issues can be bugs, tasks or ideas to be discussed. Also, issues are searchable and filterable."
msgstr ""
@@ -11422,10 +11696,10 @@ msgstr ""
msgid "IssuesAnalytics|Avg/Month:"
msgstr ""
-msgid "IssuesAnalytics|Issues created"
+msgid "IssuesAnalytics|Issues opened"
msgstr ""
-msgid "IssuesAnalytics|Issues created per month"
+msgid "IssuesAnalytics|Issues opened per month"
msgstr ""
msgid "IssuesAnalytics|Last 12 months"
@@ -11479,6 +11753,9 @@ msgstr ""
msgid "Jira integration not configured."
msgstr ""
+msgid "Jira project: %{importProject}"
+msgstr ""
+
msgid "JiraService|Events for %{noteable_model_name} are disabled."
msgstr ""
@@ -11868,6 +12145,9 @@ msgstr ""
msgid "Lead"
msgstr ""
+msgid "Lead Time"
+msgstr ""
+
msgid "Learn GitLab"
msgstr ""
@@ -11880,6 +12160,9 @@ msgstr ""
msgid "Learn how to %{no_packages_link_start}publish and share your packages%{no_packages_link_end} with GitLab."
msgstr ""
+msgid "Learn how to enable synchronization"
+msgstr ""
+
msgid "Learn more"
msgstr ""
@@ -12211,9 +12494,6 @@ msgstr ""
msgid "Live preview"
msgstr ""
-msgid "Load more vulnerabilities"
-msgstr ""
-
msgid "Loading"
msgstr ""
@@ -12364,9 +12644,6 @@ msgstr ""
msgid "Manage"
msgstr ""
-msgid "Manage Git repositories with fine-grained access controls that keep your code secure. Perform code reviews and enhance collaboration with merge requests. Each project can also have an issue tracker and a wiki."
-msgstr ""
-
msgid "Manage Web IDE features"
msgstr ""
@@ -12397,6 +12674,9 @@ msgstr ""
msgid "Manage your license"
msgstr ""
+msgid "Managed Account"
+msgstr ""
+
msgid "Manifest"
msgstr ""
@@ -12661,6 +12941,9 @@ msgstr ""
msgid "Merge Requests in Review"
msgstr ""
+msgid "Merge automatically (%{strategy})"
+msgstr ""
+
msgid "Merge commit message"
msgstr ""
@@ -12814,6 +13097,12 @@ msgstr ""
msgid "Merged branches are being deleted. This can take some time depending on the number of branches. Please refresh the page to see changes."
msgstr ""
+msgid "Merged this merge request."
+msgstr ""
+
+msgid "Merges this merge request immediately."
+msgstr ""
+
msgid "Merges this merge request when the pipeline succeeds."
msgstr ""
@@ -12921,12 +13210,15 @@ msgid_plural "Metrics|Edit metrics"
msgstr[0] ""
msgstr[1] ""
-msgid "Metrics|Environment"
+msgid "Metrics|Expand panel"
msgstr ""
msgid "Metrics|For grouping similar metrics"
msgstr ""
+msgid "Metrics|Go back (Esc)"
+msgstr ""
+
msgid "Metrics|Invalid time range, please verify."
msgstr ""
@@ -12957,9 +13249,6 @@ msgstr ""
msgid "Metrics|Refresh dashboard"
msgstr ""
-msgid "Metrics|Show last"
-msgstr ""
-
msgid "Metrics|There was an error creating the dashboard."
msgstr ""
@@ -13038,6 +13327,9 @@ msgstr ""
msgid "Microsoft Azure"
msgstr ""
+msgid "Middleman project with Static Site Editor support"
+msgstr ""
+
msgid "Migrated %{success_count}/%{total_count} files."
msgstr ""
@@ -13483,10 +13775,10 @@ msgstr ""
msgid "Next"
msgstr ""
-msgid "Next file in diff (MRs only)"
+msgid "Next file in diff"
msgstr ""
-msgid "Next unresolved discussion (MRs only)"
+msgid "Next unresolved discussion"
msgstr ""
msgid "Nickname"
@@ -13546,6 +13838,9 @@ msgstr ""
msgid "No changes between %{ref_start}%{source_branch}%{ref_end} and %{ref_start}%{target_branch}%{ref_end}"
msgstr ""
+msgid "No child epics match applied filters"
+msgstr ""
+
msgid "No connection could be made to a Gitaly Server, please check your logs!"
msgstr ""
@@ -13684,15 +13979,6 @@ msgstr ""
msgid "No thanks, don't show this again"
msgstr ""
-msgid "No vulnerabilities found for this group"
-msgstr ""
-
-msgid "No vulnerabilities found for this pipeline"
-msgstr ""
-
-msgid "No vulnerabilities found for this project"
-msgstr ""
-
msgid "No vulnerabilities present"
msgstr ""
@@ -13816,6 +14102,9 @@ msgstr ""
msgid "Nothing to preview."
msgstr ""
+msgid "Nothing to synchronize"
+msgstr ""
+
msgid "Notification events"
msgstr "Ð¡ÑŠÐ±Ð¸Ñ‚Ð¸Ñ Ð·Ð° извеÑÑ‚Ñване"
@@ -13987,6 +14276,9 @@ msgstr ""
msgid "Once removed, the fork relationship cannot be restored and you will no longer be able to send merge requests to the source."
msgstr ""
+msgid "Once the exported file is ready you can download it from this page."
+msgstr ""
+
msgid "Once the exported file is ready, you will receive a notification email with a download link, or you can download it from this page."
msgstr ""
@@ -14088,9 +14380,6 @@ msgstr ""
msgid "Open sidebar"
msgstr ""
-msgid "Open source software to collaborate on code"
-msgstr ""
-
msgid "Open: %{openIssuesCount}"
msgstr ""
@@ -14193,6 +14482,9 @@ msgstr ""
msgid "OutdatedBrowser|You can provide feedback %{feedback_link_start}on this issue%{feedback_link_end} or via your usual support channels."
msgstr ""
+msgid "Overridden"
+msgstr ""
+
msgid "Overview"
msgstr ""
@@ -14244,6 +14536,9 @@ msgstr ""
msgid "PackageRegistry|Conan Command"
msgstr ""
+msgid "PackageRegistry|Copy .pypirc content"
+msgstr ""
+
msgid "PackageRegistry|Copy Conan Command"
msgstr ""
@@ -14265,6 +14560,9 @@ msgstr ""
msgid "PackageRegistry|Copy NuGet Setup Command"
msgstr ""
+msgid "PackageRegistry|Copy Pip command"
+msgstr ""
+
msgid "PackageRegistry|Copy and paste this inside your %{codeStart}pom.xml%{codeEnd} %{codeStart}dependencies%{codeEnd} block."
msgstr ""
@@ -14298,6 +14596,12 @@ msgstr ""
msgid "PackageRegistry|For more information on the NuGet registry, %{linkStart}see the documentation%{linkEnd}."
msgstr ""
+msgid "PackageRegistry|For more information on the PyPi registry, %{linkStart}see the documentation%{linkEnd}."
+msgstr ""
+
+msgid "PackageRegistry|If you haven't already done so, you will need to add the below to your %{codeStart}.pypirc%{codeEnd} file."
+msgstr ""
+
msgid "PackageRegistry|If you haven't already done so, you will need to add the below to your %{codeStart}pom.xml%{codeEnd} file."
msgstr ""
@@ -14307,6 +14611,9 @@ msgstr ""
msgid "PackageRegistry|Learn how to %{noPackagesLinkStart}publish and share your packages%{noPackagesLinkEnd} with GitLab."
msgstr ""
+msgid "PackageRegistry|Manually Published"
+msgstr ""
+
msgid "PackageRegistry|Maven"
msgstr ""
@@ -14325,12 +14632,18 @@ msgstr ""
msgid "PackageRegistry|NuGet Command"
msgstr ""
+msgid "PackageRegistry|Pip Command"
+msgstr ""
+
msgid "PackageRegistry|Pipeline %{linkStart}%{linkEnd} triggered %{timestamp} by %{author}"
msgstr ""
msgid "PackageRegistry|Published to the repository at %{timestamp}"
msgstr ""
+msgid "PackageRegistry|PyPi"
+msgstr ""
+
msgid "PackageRegistry|Registry Setup"
msgstr ""
@@ -14367,6 +14680,9 @@ msgstr ""
msgid "PackageRegistry|npm"
msgstr ""
+msgid "PackageRegistry|published by %{author}"
+msgstr ""
+
msgid "PackageRegistry|yarn"
msgstr ""
@@ -14382,6 +14698,9 @@ msgstr ""
msgid "PackageType|NuGet"
msgstr ""
+msgid "PackageType|PyPi"
+msgstr ""
+
msgid "Packages"
msgstr ""
@@ -14475,6 +14794,9 @@ msgstr ""
msgid "Past due"
msgstr ""
+msgid "Paste a machine public key here. Read more about how to generate it"
+msgstr ""
+
msgid "Paste a machine public key here. Read more about how to generate it %{link_start}here%{link_end}"
msgstr ""
@@ -14490,9 +14812,6 @@ msgstr ""
msgid "Path"
msgstr ""
-msgid "Path, transfer, remove"
-msgstr ""
-
msgid "Path:"
msgstr ""
@@ -14523,7 +14842,7 @@ msgstr ""
msgid "Percentage"
msgstr ""
-msgid "Perform advanced options such as changing path, transferring, or removing the group."
+msgid "Perform advanced options such as changing path, transferring, exporting, or removing the group."
msgstr ""
msgid "Perform common operations on GitLab project"
@@ -14604,9 +14923,6 @@ msgstr "Планове за Ñхема"
msgid "Pipeline minutes quota"
msgstr ""
-msgid "Pipeline quota"
-msgstr ""
-
msgid "Pipeline subscriptions"
msgstr ""
@@ -14616,6 +14932,9 @@ msgstr ""
msgid "Pipeline: %{status}"
msgstr ""
+msgid "PipelineCharts|CI / CD Analytics"
+msgstr ""
+
msgid "PipelineCharts|Failed:"
msgstr "ÐеуÑпешни:"
@@ -14694,15 +15013,6 @@ msgstr ""
msgid "Pipelines settings for '%{project_name}' were successfully updated."
msgstr ""
-msgid "Pipelines| to purchase more minutes."
-msgstr ""
-
-msgid "Pipelines|%{namespace_name} has exceeded its pipeline minutes quota."
-msgstr ""
-
-msgid "Pipelines|%{namespace_name} has less than %{notification_level}%% of CI minutes available."
-msgstr ""
-
msgid "Pipelines|API"
msgstr ""
@@ -14724,10 +15034,13 @@ msgstr ""
msgid "Pipelines|Get started with Pipelines"
msgstr ""
-msgid "Pipelines|Loading Pipelines"
+msgid "Pipelines|Group %{namespace_name} has %{percentage}%% or less Shared Runner Pipeline minutes remaining. Once it runs out, no new jobs or pipelines in its projects will run."
msgstr ""
-msgid "Pipelines|Pipelines will not run anymore on shared Runners."
+msgid "Pipelines|Group %{namespace_name} has exceeded its pipeline minutes quota. Unless you buy additional pipeline minutes, no new jobs or pipelines in its projects will run."
+msgstr ""
+
+msgid "Pipelines|Loading Pipelines"
msgstr ""
msgid "Pipelines|Project cache successfully reset."
@@ -14958,6 +15271,9 @@ msgstr ""
msgid "Please select"
msgstr ""
+msgid "Please select a Jira project"
+msgstr ""
+
msgid "Please select a country"
msgstr ""
@@ -15003,6 +15319,9 @@ msgstr ""
msgid "Please wait while we import the repository for you. Refresh at will."
msgstr ""
+msgid "Plugins directory is deprecated and will be removed in 14.0. Please move this file into /file_hooks directory."
+msgstr ""
+
msgid "Pod does not exist"
msgstr ""
@@ -15120,7 +15439,7 @@ msgstr ""
msgid "Prevent users from changing their profile name"
msgstr ""
-msgid "Prevent users from modifing merge request approvers list"
+msgid "Prevent users from modifying merge request approvers list"
msgstr ""
msgid "Prevent users from performing write operations on GitLab while performing maintenance."
@@ -15141,10 +15460,10 @@ msgstr ""
msgid "Previous Artifacts"
msgstr ""
-msgid "Previous file in diff (MRs only)"
+msgid "Previous file in diff"
msgstr ""
-msgid "Previous unresolved discussion (MRs only)"
+msgid "Previous unresolved discussion"
msgstr ""
msgid "Primary"
@@ -15177,6 +15496,9 @@ msgstr ""
msgid "Private profile"
msgstr ""
+msgid "Private projects Minutes cost factor"
+msgstr ""
+
msgid "Private projects can be created in your personal namespace with:"
msgstr ""
@@ -16005,6 +16327,9 @@ msgstr ""
msgid "ProjectSettings|With GitLab Pages you can host your static websites on GitLab"
msgstr ""
+msgid "ProjectSettings|With Metrics Dashboard you can visualize this project performance metrics"
+msgstr ""
+
msgid "ProjectTemplates|.NET Core"
msgstr ""
@@ -16068,6 +16393,9 @@ msgstr ""
msgid "ProjectTemplates|Spring"
msgstr ""
+msgid "ProjectTemplates|Static Site Editor/Middleman"
+msgstr ""
+
msgid "ProjectTemplates|iOS (Swift)"
msgstr ""
@@ -16287,13 +16615,13 @@ msgstr ""
msgid "PrometheusService|Prometheus is being automatically managed on your clusters"
msgstr ""
-msgid "PrometheusService|These metrics will only be monitored after your first deployment to an environment"
+msgid "PrometheusService|Select the Active checkbox to override the Auto Configuration with custom settings. If unchecked, Auto Configuration settings are used."
msgstr ""
-msgid "PrometheusService|Time-series monitoring service"
+msgid "PrometheusService|These metrics will only be monitored after your first deployment to an environment"
msgstr ""
-msgid "PrometheusService|To enable manual configuration, uninstall Prometheus from your clusters"
+msgid "PrometheusService|Time-series monitoring service"
msgstr ""
msgid "PrometheusService|To enable the installation of Prometheus on your clusters, deactivate the manual configuration below"
@@ -16479,6 +16807,9 @@ msgstr ""
msgid "Protip:"
msgstr ""
+msgid "Protocol"
+msgstr ""
+
msgid "Provider"
msgstr ""
@@ -16503,6 +16834,9 @@ msgstr ""
msgid "Public pipelines"
msgstr ""
+msgid "Public projects Minutes cost factor"
+msgstr ""
+
msgid "Pull"
msgstr ""
@@ -16703,6 +17037,9 @@ msgid_plural "Refreshing in %d seconds to show the updated status..."
msgstr[0] ""
msgstr[1] ""
+msgid "Regenerate export"
+msgstr ""
+
msgid "Regenerate instance ID"
msgstr ""
@@ -17080,9 +17417,29 @@ msgstr ""
msgid "Reports|%{combinedString} and %{resolvedString}"
msgstr ""
+msgid "Reports|Accessibility scanning detected %d issue for the source branch only"
+msgid_plural "Reports|Accessibility scanning detected %d issues for the source branch only"
+msgstr[0] ""
+msgstr[1] ""
+
+msgid "Reports|Accessibility scanning detected no issues for the source branch only"
+msgstr ""
+
+msgid "Reports|Accessibility scanning failed loading results"
+msgstr ""
+
+msgid "Reports|Accessibility scanning results are being parsed"
+msgstr ""
+
msgid "Reports|Actions"
msgstr ""
+msgid "Reports|An error occured while loading report"
+msgstr ""
+
+msgid "Reports|An error occurred while loading %{name} results"
+msgstr ""
+
msgid "Reports|Class"
msgstr ""
@@ -17188,7 +17545,7 @@ msgstr ""
msgid "Requested %{time_ago}"
msgstr ""
-msgid "Requested design version does not exist"
+msgid "Requested design version does not exist."
msgstr ""
msgid "Requested states are invalid"
@@ -17372,6 +17729,9 @@ msgstr ""
msgid "Resync"
msgstr ""
+msgid "Resync all"
+msgstr ""
+
msgid "Resync all %{replicableType}"
msgstr ""
@@ -17440,6 +17800,9 @@ msgstr ""
msgid "Revoked personal access token %{personal_access_token_name}!"
msgstr ""
+msgid "Revoked project access token %{project_access_token_name}!"
+msgstr ""
+
msgid "RightSidebar|adding a"
msgstr ""
@@ -17488,6 +17851,9 @@ msgstr ""
msgid "Runner token"
msgstr ""
+msgid "Runner tokens"
+msgstr ""
+
msgid "Runner was not updated."
msgstr ""
@@ -17542,6 +17908,9 @@ msgstr ""
msgid "SAML SSO for %{group_name}"
msgstr ""
+msgid "SAML discovery tokens"
+msgstr ""
+
msgid "SAML for %{group_name}"
msgstr ""
@@ -17626,12 +17995,18 @@ msgstr "Създаване на нов план за Ñхема"
msgid "Scheduled"
msgstr ""
+msgid "Scheduled to merge this merge request (%{strategy})."
+msgstr ""
+
msgid "Scheduled to merge this merge request when the pipeline succeeds."
msgstr ""
msgid "Schedules"
msgstr ""
+msgid "Schedules to merge this merge request (%{strategy})."
+msgstr ""
+
msgid "Scheduling"
msgstr ""
@@ -17689,6 +18064,9 @@ msgstr ""
msgid "Search branches and tags"
msgstr "ТърÑете в клоните и етикетите"
+msgid "Search by author"
+msgstr ""
+
msgid "Search files"
msgstr ""
@@ -17834,11 +18212,6 @@ msgid_plural "SearchResults|snippets"
msgstr[0] ""
msgstr[1] ""
-msgid "SearchResults|snippet result"
-msgid_plural "SearchResults|snippet results"
-msgstr[0] ""
-msgstr[1] ""
-
msgid "SearchResults|user"
msgid_plural "SearchResults|users"
msgstr[0] ""
@@ -17849,6 +18222,9 @@ msgid_plural "SearchResults|wiki results"
msgstr[0] ""
msgstr[1] ""
+msgid "Searching by both author and message is currently not supported."
+msgstr ""
+
msgid "Seat Link"
msgstr ""
@@ -17876,184 +18252,232 @@ msgstr ""
msgid "Security Dashboard"
msgstr ""
-msgid "Security Dashboard|Error fetching the vulnerability counts. Please check your network connection and try again."
+msgid "Security configuration help link"
+msgstr ""
+
+msgid "Security dashboard"
msgstr ""
-msgid "Security Dashboard|Error fetching the vulnerability list. Please check your network connection and try again."
+msgid "Security report is out of date. Please update your branch with the latest changes from the target branch (%{targetBranchName})"
msgstr ""
-msgid "Security Dashboard|Issue Created"
+msgid "Security report is out of date. Run %{newPipelineLinkStart}a new pipeline%{newPipelineLinkEnd} for the target branch (%{targetBranchName})"
msgstr ""
-msgid "Security Reports|Comment added to '%{vulnerabilityName}'"
+msgid "SecurityConfiguration|Configured"
msgstr ""
-msgid "Security Reports|Comment deleted on '%{vulnerabilityName}'"
+msgid "SecurityConfiguration|Feature"
msgstr ""
-msgid "Security Reports|Comment edited on '%{vulnerabilityName}'"
+msgid "SecurityConfiguration|Feature documentation for %{featureName}"
msgstr ""
-msgid "Security Reports|Create issue"
+msgid "SecurityConfiguration|Not yet configured"
msgstr ""
-msgid "Security Reports|Dismiss vulnerability"
+msgid "SecurityConfiguration|Secure features"
msgstr ""
-msgid "Security Reports|Dismissed '%{vulnerabilityName}'"
+msgid "SecurityConfiguration|Status"
msgstr ""
-msgid "Security Reports|Dismissed '%{vulnerabilityName}'. Turn off the hide dismissed toggle to view."
+msgid "SecurityReports|%{firstProject} and %{secondProject}"
msgstr ""
-msgid "Security Reports|Either you don't have permission to view this dashboard or the dashboard has not been setup. Please check your permission settings with your administrator or check your dashboard configurations to proceed."
+msgid "SecurityReports|%{firstProject}, %{secondProject}, and %{rest}"
msgstr ""
-msgid "Security Reports|Learn more about setting up your dashboard"
+msgid "SecurityReports|Add a project to your dashboard"
msgstr ""
-msgid "Security Reports|More info"
+msgid "SecurityReports|Add or remove projects from your dashboard"
msgstr ""
-msgid "Security Reports|Oops, something doesn't seem right."
+msgid "SecurityReports|Add projects"
msgstr ""
-msgid "Security Reports|Security reports can only be accessed by authorized users."
+msgid "SecurityReports|Comment added to '%{vulnerabilityName}'"
msgstr ""
-msgid "Security Reports|There was an error adding the comment."
+msgid "SecurityReports|Comment deleted on '%{vulnerabilityName}'"
msgstr ""
-msgid "Security Reports|There was an error creating the issue."
+msgid "SecurityReports|Comment edited on '%{vulnerabilityName}'"
msgstr ""
-msgid "Security Reports|There was an error creating the merge request."
+msgid "SecurityReports|Create issue"
msgstr ""
-msgid "Security Reports|There was an error deleting the comment."
+msgid "SecurityReports|Dismiss Selected"
msgstr ""
-msgid "Security Reports|There was an error dismissing the vulnerabilities."
+msgid "SecurityReports|Dismiss vulnerability"
msgstr ""
-msgid "Security Reports|There was an error dismissing the vulnerability."
+msgid "SecurityReports|Dismissed '%{vulnerabilityName}'"
msgstr ""
-msgid "Security Reports|There was an error reverting the dismissal."
+msgid "SecurityReports|Dismissed '%{vulnerabilityName}'. Turn off the hide dismissed toggle to view."
msgstr ""
-msgid "Security Reports|There was an error reverting this dismissal."
+msgid "SecurityReports|Each vulnerability now has a unique page that can be directly linked to, shared, referenced, and tracked as the single source of truth. Vulnerability occurrences also persist across scanner runs, which improves tracking and visibility and reduces duplicates between scans."
msgstr ""
-msgid "Security Reports|Undo dismiss"
+msgid "SecurityReports|Edit dashboard"
msgstr ""
-msgid "Security Reports|You do not have sufficient permissions to access this report"
+msgid "SecurityReports|Either you don't have permission to view this dashboard or the dashboard has not been setup. Please check your permission settings with your administrator or check your dashboard configurations to proceed."
msgstr ""
-msgid "Security Reports|You must sign in as an authorized user to see this report"
+msgid "SecurityReports|Error fetching the vulnerability counts. Please check your network connection and try again."
msgstr ""
-msgid "Security configuration help link"
+msgid "SecurityReports|Error fetching the vulnerability list. Please check your network connection and try again."
msgstr ""
-msgid "Security dashboard"
+msgid "SecurityReports|False positive"
msgstr ""
-msgid "Security report is out of date. Please update your branch with the latest changes from the target branch (%{targetBranchName})"
+msgid "SecurityReports|Hide dismissed"
msgstr ""
-msgid "Security report is out of date. Run %{newPipelineLinkStart}a new pipeline%{newPipelineLinkEnd} for the target branch (%{targetBranchName})"
+msgid "SecurityReports|Introducing standalone vulnerabilities"
msgstr ""
-msgid "SecurityConfiguration|Configured"
+msgid "SecurityReports|Issue Created"
msgstr ""
-msgid "SecurityConfiguration|Feature"
+msgid "SecurityReports|Learn More"
msgstr ""
-msgid "SecurityConfiguration|Feature documentation for %{featureName}"
+msgid "SecurityReports|Learn more about setting up your dashboard"
msgstr ""
-msgid "SecurityConfiguration|Not yet configured"
+msgid "SecurityReports|Load more vulnerabilities"
msgstr ""
-msgid "SecurityConfiguration|Secure features"
+msgid "SecurityReports|Monitor vulnerabilities in your code"
msgstr ""
-msgid "SecurityConfiguration|Status"
+msgid "SecurityReports|More info"
+msgstr ""
+
+msgid "SecurityReports|More information"
+msgstr ""
+
+msgid "SecurityReports|No vulnerabilities found for dashboard"
+msgstr ""
+
+msgid "SecurityReports|No vulnerabilities found for this group"
+msgstr ""
+
+msgid "SecurityReports|No vulnerabilities found for this pipeline"
+msgstr ""
+
+msgid "SecurityReports|No vulnerabilities found for this project"
+msgstr ""
+
+msgid "SecurityReports|Oops, something doesn't seem right."
+msgstr ""
+
+msgid "SecurityReports|Pipeline %{pipelineLink} triggered %{timeago} by %{user}"
msgstr ""
-msgid "SecurityDashboard|%{firstProject} and %{secondProject}"
+msgid "SecurityReports|Project"
msgstr ""
-msgid "SecurityDashboard|%{firstProject}, %{secondProject}, and %{rest}"
+msgid "SecurityReports|Projects added"
msgstr ""
-msgid "SecurityDashboard|Add a project to your dashboard"
+msgid "SecurityReports|Remove project from dashboard"
msgstr ""
-msgid "SecurityDashboard|Add or remove projects from your dashboard"
+msgid "SecurityReports|Report type"
msgstr ""
-msgid "SecurityDashboard|Add projects"
+msgid "SecurityReports|Return to dashboard"
msgstr ""
-msgid "SecurityDashboard|Edit dashboard"
+msgid "SecurityReports|Security Dashboard"
msgstr ""
-msgid "SecurityDashboard|Hide dismissed"
+msgid "SecurityReports|Security reports can only be accessed by authorized users."
msgstr ""
-msgid "SecurityDashboard|Monitor vulnerabilities in your code"
+msgid "SecurityReports|Select a project to add by using the project search field above."
msgstr ""
-msgid "SecurityDashboard|More information"
+msgid "SecurityReports|Select a reason"
msgstr ""
-msgid "SecurityDashboard|No vulnerabilities found for dashboard"
+msgid "SecurityReports|Severity"
msgstr ""
-msgid "SecurityDashboard|Pipeline %{pipelineLink} triggered %{timeago} by %{user}"
+msgid "SecurityReports|Status"
msgstr ""
-msgid "SecurityDashboard|Project"
+msgid "SecurityReports|The security dashboard displays the latest security findings for projects you wish to monitor. Select \"Edit dashboard\" to add and remove projects."
msgstr ""
-msgid "SecurityDashboard|Projects added"
+msgid "SecurityReports|The security dashboard displays the latest security report. Use it to find and fix vulnerabilities."
msgstr ""
-msgid "SecurityDashboard|Remove project from dashboard"
+msgid "SecurityReports|There was an error adding the comment."
msgstr ""
-msgid "SecurityDashboard|Report type"
+msgid "SecurityReports|There was an error creating the issue."
msgstr ""
-msgid "SecurityDashboard|Return to dashboard"
+msgid "SecurityReports|There was an error creating the merge request."
msgstr ""
-msgid "SecurityDashboard|Security Dashboard"
+msgid "SecurityReports|There was an error deleting the comment."
msgstr ""
-msgid "SecurityDashboard|Select a project to add by using the project search field above."
+msgid "SecurityReports|There was an error dismissing the vulnerabilities."
msgstr ""
-msgid "SecurityDashboard|Severity"
+msgid "SecurityReports|There was an error dismissing the vulnerability."
msgstr ""
-msgid "SecurityDashboard|Status"
+msgid "SecurityReports|There was an error reverting the dismissal."
msgstr ""
-msgid "SecurityDashboard|The security dashboard displays the latest security findings for projects you wish to monitor. Select \"Edit dashboard\" to add and remove projects."
+msgid "SecurityReports|There was an error reverting this dismissal."
msgstr ""
-msgid "SecurityDashboard|The security dashboard displays the latest security report. Use it to find and fix vulnerabilities."
+msgid "SecurityReports|There was an error while generating the report."
msgstr ""
-msgid "SecurityDashboard|There was an error while generating the report."
+msgid "SecurityReports|Unable to add %{invalidProjects}"
msgstr ""
-msgid "SecurityDashboard|Unable to add %{invalidProjects}"
+msgid "SecurityReports|Undo dismiss"
+msgstr ""
+
+msgid "SecurityReports|While it's rare to have no vulnerabilities for your group, it can happen. In any event, we ask that you double check your settings to make sure you've set up your dashboard correctly."
+msgstr ""
+
+msgid "SecurityReports|While it's rare to have no vulnerabilities for your pipeline, it can happen. In any event, we ask that you double check your settings to make sure all security scanning jobs have passed successfully."
+msgstr ""
+
+msgid "SecurityReports|While it's rare to have no vulnerabilities for your project, it can happen. In any event, we ask that you double check your settings to make sure you've set up your dashboard correctly."
+msgstr ""
+
+msgid "SecurityReports|While it's rare to have no vulnerabilities, it can happen. In any event, we ask that you please double check your settings to make sure you've set up your dashboard correctly."
+msgstr ""
+
+msgid "SecurityReports|Won't fix / Accept risk"
+msgstr ""
+
+msgid "SecurityReports|You do not have sufficient permissions to access this report"
+msgstr ""
+
+msgid "SecurityReports|You must sign in as an authorized user to see this report"
+msgstr ""
+
+msgid "SecurityReports|[No reason]"
msgstr ""
msgid "See GitLab's %{password_policy_guidelines}"
@@ -18065,6 +18489,9 @@ msgstr ""
msgid "See the affected projects in the GitLab admin panel"
msgstr ""
+msgid "See what's new at GitLab"
+msgstr ""
+
msgid "Select"
msgstr ""
@@ -18470,9 +18897,6 @@ msgstr ""
msgid "Set up Jira Integration"
msgstr ""
-msgid "Set up Jira Integration illustration"
-msgstr ""
-
msgid "Set up a %{type} Runner automatically"
msgstr ""
@@ -18557,6 +18981,9 @@ msgstr ""
msgid "Severity: %{severity}"
msgstr ""
+msgid "Shards (%{shards})"
+msgstr ""
+
msgid "Shards to synchronize"
msgstr ""
@@ -18871,6 +19298,9 @@ msgstr ""
msgid "Snowplow"
msgstr ""
+msgid "Some child epics may be hidden due to applied filters"
+msgstr ""
+
msgid "Some email servers do not support overriding the email sender name. Enable this option to include the name of the author of the issue, merge request or comment in the email body instead."
msgstr ""
@@ -19012,6 +19442,9 @@ msgstr ""
msgid "Something went wrong while updating your list settings"
msgstr ""
+msgid "Something went wrong with your automatic subscription renewal"
+msgstr ""
+
msgid "Something went wrong, unable to add %{project} to dashboard"
msgstr ""
@@ -19297,6 +19730,9 @@ msgstr ""
msgid "Stage removed"
msgstr ""
+msgid "Standard"
+msgstr ""
+
msgid "Star a label to make it a priority label. Order the prioritized labels to change their relative priority, by dragging."
msgstr ""
@@ -19420,6 +19856,9 @@ msgstr ""
msgid "Static Application Security Testing (SAST)"
msgstr ""
+msgid "StaticSiteEditor|An error occurred while submitting your changes."
+msgstr ""
+
msgid "StaticSiteEditor|Branch could not be created."
msgstr ""
@@ -19429,18 +19868,30 @@ msgstr ""
msgid "StaticSiteEditor|Could not create merge request."
msgstr ""
+msgid "StaticSiteEditor|Incompatible file content"
+msgstr ""
+
msgid "StaticSiteEditor|Return to site"
msgstr ""
+msgid "StaticSiteEditor|Static site editor"
+msgstr ""
+
msgid "StaticSiteEditor|Success!"
msgstr ""
msgid "StaticSiteEditor|Summary of changes"
msgstr ""
+msgid "StaticSiteEditor|The Static Site Editor is currently configured to only edit Markdown content on pages generated from Middleman. Visit the documentation to learn more about configuring your site to use the Static Site Editor."
+msgstr ""
+
msgid "StaticSiteEditor|Update %{sourcePath} file"
msgstr ""
+msgid "StaticSiteEditor|View documentation"
+msgstr ""
+
msgid "StaticSiteEditor|View merge request"
msgstr ""
@@ -19498,6 +19949,12 @@ msgstr ""
msgid "StatusPage|Status page"
msgstr ""
+msgid "StatusPage|Status page URL"
+msgstr ""
+
+msgid "StatusPage|Status page frontend documentation"
+msgstr ""
+
msgid "StatusPage|To publish incidents to an external status page, GitLab will store a JSON file in your Amazon S3 account in a location accessible to your external status page service. Make sure to also set up %{docsLink}"
msgstr ""
@@ -19846,6 +20303,12 @@ msgstr ""
msgid "Synced"
msgstr ""
+msgid "Synchronization disabled"
+msgstr ""
+
+msgid "Synchronization of container repositories is disabled."
+msgstr ""
+
msgid "System"
msgstr ""
@@ -19999,6 +20462,9 @@ msgstr ""
msgid "Telephone number"
msgstr ""
+msgid "Telephone number (Optional)"
+msgstr ""
+
msgid "Template"
msgstr ""
@@ -20305,6 +20771,9 @@ msgstr ""
msgid "The license was successfully uploaded and is now active. You can see the details below."
msgstr ""
+msgid "The license was successfully uploaded and will be active from %{starts_at}. You can see the details below."
+msgstr ""
+
msgid "The maximum file size allowed is %{size}."
msgstr ""
@@ -20626,6 +21095,9 @@ msgstr ""
msgid "There was an error getting the epic participants."
msgstr ""
+msgid "There was an error importing the Jira project."
+msgstr ""
+
msgid "There was an error loading users activity calendar."
msgstr ""
@@ -20641,7 +21113,7 @@ msgstr ""
msgid "There was an error resetting user pipeline minutes."
msgstr ""
-msgid "There was an error saving this Geo Node"
+msgid "There was an error saving this Geo Node."
msgstr ""
msgid "There was an error saving your changes."
@@ -20704,6 +21176,9 @@ msgstr ""
msgid "They can be managed using the %{link}."
msgstr ""
+msgid "Third Party Advisory Link"
+msgstr ""
+
msgid "Third party offers"
msgstr ""
@@ -20740,6 +21215,9 @@ msgstr ""
msgid "This application will be able to:"
msgstr ""
+msgid "This attachment has been truncated to avoid exceeding the maximum allowed attachment size of 15MB. %{written_count} of %{issues_count} issues have been included. Consider re-exporting with a narrower selection of issues."
+msgstr ""
+
msgid "This block is self-referential"
msgstr ""
@@ -20881,6 +21359,12 @@ msgstr ""
msgid "This issue is confidential"
msgstr ""
+msgid "This issue is currently blocked by the following issues: %{issues}."
+msgstr ""
+
+msgid "This issue is in a child epic of the filtered epic"
+msgstr ""
+
msgid "This issue is locked."
msgstr ""
@@ -21079,7 +21563,10 @@ msgstr ""
msgid "This user will be the author of all events in the activity feed that are the result of an update, like new branches being created or new commits being pushed to existing branches. Upon creation or when reassigning you can only assign yourself to be the mirror user."
msgstr ""
-msgid "This variable can not be masked"
+msgid "This variable can not be masked."
+msgstr ""
+
+msgid "This variable does not match the expected pattern."
msgstr ""
msgid "This will help us personalize your onboarding experience."
@@ -21109,10 +21596,10 @@ msgstr ""
msgid "ThreatMonitoring|Application firewall not detected"
msgstr ""
-msgid "ThreatMonitoring|Container Network Policy"
+msgid "ThreatMonitoring|Container Network Policies are not installed or have been disabled. To view this data, ensure your Network Policies are installed and enabled for your cluster."
msgstr ""
-msgid "ThreatMonitoring|Container NetworkPolicies are not installed or has been disabled. To view this data, ensure you NetworkPolicies are installed and enabled for your cluster."
+msgid "ThreatMonitoring|Container Network Policy"
msgstr ""
msgid "ThreatMonitoring|Container NetworkPolicies not detected"
@@ -21145,7 +21632,7 @@ msgstr ""
msgid "ThreatMonitoring|Something went wrong, unable to fetch statistics"
msgstr ""
-msgid "ThreatMonitoring|The firewall is not installed or has been disabled. To view this data, ensure you firewall is installed and enabled for your cluster."
+msgid "ThreatMonitoring|The firewall is not installed or has been disabled. To view this data, ensure the web application firewall is installed and enabled for your cluster."
msgstr ""
msgid "ThreatMonitoring|The graph below is an overview of traffic coming to your application as tracked by the Web Application Firewall (WAF). View the docs for instructions on how to access the WAF logs to see what type of malicious traffic is trying to access your app. The docs link is also accessible by clicking the \"?\" icon next to the title below."
@@ -21214,6 +21701,9 @@ msgstr ""
msgid "Time in seconds GitLab will wait for a response from the external service. When the service does not respond in time, access will be denied."
msgstr ""
+msgid "Time of import: %{importTime}"
+msgstr ""
+
msgid "Time remaining"
msgstr ""
@@ -21398,7 +21888,7 @@ msgstr ""
msgid "Title:"
msgstr ""
-msgid "Titles and Filenames"
+msgid "Titles and Descriptions"
msgstr ""
msgid "To"
@@ -21605,7 +22095,7 @@ msgstr ""
msgid "Too many projects enabled. You will need to manage them via the console or the API."
msgstr ""
-msgid "Topics"
+msgid "Topics (optional)"
msgstr ""
msgid "Total"
@@ -21767,6 +22257,9 @@ msgstr ""
msgid "Try all GitLab has to offer for 30 days."
msgstr ""
+msgid "Try changing or removing filters."
+msgstr ""
+
msgid "Try to fork again"
msgstr ""
@@ -21860,6 +22353,9 @@ msgstr ""
msgid "Unable to connect to server: %{error}"
msgstr ""
+msgid "Unable to connect to the Jira instance. Please check your Jira integration configuration."
+msgstr ""
+
msgid "Unable to convert Kubernetes logs encoding to UTF-8"
msgstr ""
@@ -22803,6 +23299,9 @@ msgstr ""
msgid "View issue"
msgstr ""
+msgid "View issues"
+msgstr ""
+
msgid "View it on GitLab"
msgstr ""
@@ -22980,6 +23479,15 @@ msgstr ""
msgid "VulnerabilityManagement|Resolved %{timeago} by %{user}"
msgstr ""
+msgid "VulnerabilityManagement|Something went wrong while trying to delete the comment. Please try again later."
+msgstr ""
+
+msgid "VulnerabilityManagement|Something went wrong while trying to retrieve the vulnerability history. Please try again later."
+msgstr ""
+
+msgid "VulnerabilityManagement|Something went wrong while trying to save the comment. Please try again later."
+msgstr ""
+
msgid "VulnerabilityManagement|Something went wrong, could not create an issue."
msgstr ""
@@ -23010,6 +23518,9 @@ msgstr ""
msgid "VulnerabilityStatusTypes|Resolved"
msgstr ""
+msgid "Vulnerability|%{scannerName} (version %{scannerVersion})"
+msgstr ""
+
msgid "Vulnerability|Class"
msgstr ""
@@ -23043,7 +23554,10 @@ msgstr ""
msgid "Vulnerability|Project"
msgstr ""
-msgid "Vulnerability|Report Type"
+msgid "Vulnerability|Scanner Provider"
+msgstr ""
+
+msgid "Vulnerability|Scanner Type"
msgstr ""
msgid "Vulnerability|Severity"
@@ -23097,9 +23611,15 @@ msgstr ""
msgid "We sent you an email with reset password instructions"
msgstr ""
+msgid "We tried to automatically renew your %{strong}%{plan_name}%{strong_close} subscription for %{strong}%{namespace_name}%{strong_close} on %{expires_on} but something went wrong so your subscription was downgraded to the free plan. Don't worry, your data is safe. We suggest you check your payment method and get in touch with our support team (%{support_link}). They'll gladly help with your subscription renewal."
+msgstr ""
+
msgid "We want to be sure it is you, please confirm you are not a robot."
msgstr ""
+msgid "We will automatically renew your %{strong}%{plan_name}%{strong_close} subscription for %{strong}%{namespace_name}%{strong_close} on %{strong}%{expires_on}%{strong_close}. There's nothing that you need to do, we'll let you know when the renewal is complete. Need more seats, a higher plan or just want to review your payment method?"
+msgstr ""
+
msgid "We've found no vulnerabilities"
msgstr ""
@@ -23204,15 +23724,6 @@ msgstr ""
msgid "When:"
msgstr ""
-msgid "While it's rare to have no vulnerabilities for your group, it can happen. In any event, we ask that you double check your settings to make sure you've set up your dashboard correctly."
-msgstr ""
-
-msgid "While it's rare to have no vulnerabilities for your pipeline, it can happen. In any event, we ask that you double check your settings to make sure all security scanning jobs have passed successfully."
-msgstr ""
-
-msgid "While it's rare to have no vulnerabilities for your project, it can happen. In any event, we ask that you double check your settings to make sure you've set up your dashboard correctly."
-msgstr ""
-
msgid "While it's rare to have no vulnerabilities, it can happen. In any event, we ask that you please double check your settings to make sure you've set up your dashboard correctly."
msgstr ""
@@ -23243,9 +23754,6 @@ msgstr ""
msgid "Wiki"
msgstr ""
-msgid "Wiki pages"
-msgstr ""
-
msgid "Wiki was successfully updated."
msgstr ""
@@ -23429,6 +23937,9 @@ msgstr ""
msgid "Yes, add it"
msgstr ""
+msgid "Yes, close issue"
+msgstr ""
+
msgid "Yes, let me map Google Code users to full names or GitLab users."
msgstr ""
@@ -23624,6 +24135,9 @@ msgstr ""
msgid "You could not create a new trigger."
msgstr ""
+msgid "You didn't renew your %{strong}%{plan_name}%{strong_close} subscription for %{strong}%{namespace_name}%{strong_close} so it was downgraded to the free plan."
+msgstr ""
+
msgid "You didn't renew your %{strong}%{plan_name}%{strong_close} subscription so it was downgraded to the GitLab Core Plan."
msgstr ""
@@ -23690,6 +24204,9 @@ msgstr ""
msgid "You have declined the invitation to join %{label}."
msgstr ""
+msgid "You have imported from this project %{numberOfPreviousImportsForProject} times before. Each new import will create duplicate issues."
+msgstr ""
+
msgid "You have no permissions"
msgstr ""
@@ -23771,6 +24288,9 @@ msgstr ""
msgid "You need to upload a Google Takeout archive."
msgstr ""
+msgid "You reached %{usage_in_percent} of %{namespace_name}'s capacity (%{used_storage} of %{storage_limit})"
+msgstr ""
+
msgid "You tried to fork %{link_to_the_project} but it failed for the following reason:"
msgstr ""
@@ -23855,9 +24375,21 @@ msgstr ""
msgid "YouTube"
msgstr ""
+msgid "Your %{strong}%{plan_name}%{strong_close} subscription for %{strong}%{namespace_name}%{strong_close} will expire on %{strong}%{expires_on}%{strong_close}. After that, you will not to be able to create issues or merge requests as well as many other features."
+msgstr ""
+
msgid "Your %{strong}%{plan_name}%{strong_close} subscription will expire on %{strong}%{expires_on}%{strong_close}. After that, you will not to be able to create issues or merge requests as well as many other features."
msgstr ""
+msgid "Your CSV export has started. It will be emailed to %{email} when complete."
+msgstr ""
+
+msgid "Your CSV export of %{issues_count} from project %{project_link} has been added to this email as an attachment."
+msgstr ""
+
+msgid "Your CSV export of %{written_count} from project %{project_name} (%{project_url}) has been added to this email as an attachment."
+msgstr ""
+
msgid "Your Commit Email will be used for web based operations, such as edits and merges."
msgstr ""
@@ -23954,6 +24486,9 @@ msgstr ""
msgid "Your comment could not be updated! Please check your network connection and try again."
msgstr ""
+msgid "Your comment will be discarded."
+msgstr ""
+
msgid "Your custom stage '%{title}' was created"
msgstr ""
@@ -23993,6 +24528,9 @@ msgstr ""
msgid "Your new personal access token has been created."
msgstr ""
+msgid "Your new project access token has been created."
+msgstr ""
+
msgid "Your password isn't required to view this page. If a password or any other personal details are requested, please contact your administrator to report abuse."
msgstr ""
@@ -24011,12 +24549,18 @@ msgstr ""
msgid "Your request for access has been queued for review."
msgstr ""
+msgid "Your search didn't match any commits."
+msgstr ""
+
msgid "Your subscription expired!"
msgstr ""
msgid "Your subscription has been downgraded"
msgstr ""
+msgid "Your subscription will automatically renew in %{remaining_days}"
+msgstr ""
+
msgid "Your subscription will expire in %{remaining_days}"
msgstr ""
@@ -24055,7 +24599,7 @@ msgstr ""
msgid "allowed to fail"
msgstr ""
-msgid "already being used for another group or project milestone."
+msgid "already being used for another group or project %{timebox_name}."
msgstr ""
msgid "already has a \"created\" issue link"
@@ -24133,6 +24677,9 @@ msgstr ""
msgid "ciReport|%{linkStartTag}Learn more about SAST %{linkEndTag}"
msgstr ""
+msgid "ciReport|%{linkStartTag}Learn more about Secret Scanning %{linkEndTag}"
+msgstr ""
+
msgid "ciReport|%{linkStartTag}Learn more about codequality reports %{linkEndTag}"
msgstr ""
@@ -24296,6 +24843,12 @@ msgstr ""
msgid "ciReport|SAST"
msgstr ""
+msgid "ciReport|Secret scanning"
+msgstr ""
+
+msgid "ciReport|Secret scanning detects secrets and credentials vulnerabilities in your source code."
+msgstr ""
+
msgid "ciReport|Security scanning"
msgstr ""
@@ -24493,6 +25046,9 @@ msgstr ""
msgid "group"
msgstr ""
+msgid "groups"
+msgstr ""
+
msgid "has already been linked to another vulnerability"
msgstr ""
@@ -24656,9 +25212,6 @@ msgstr[1] ""
msgid "merged %{time_ago}"
msgstr ""
-msgid "milestone should belong either to a project or a group."
-msgstr ""
-
msgid "missing"
msgstr ""
@@ -24995,6 +25548,9 @@ msgstr "нова заÑвка за Ñливане"
msgid "no contributions"
msgstr ""
+msgid "no expiration"
+msgstr ""
+
msgid "no one can merge"
msgstr ""
@@ -25047,6 +25603,9 @@ msgstr ""
msgid "pending removal"
msgstr ""
+msgid "per day"
+msgstr ""
+
msgid "pipeline"
msgstr ""
@@ -25075,6 +25634,12 @@ msgstr[1] ""
msgid "project avatar"
msgstr ""
+msgid "projects"
+msgstr ""
+
+msgid "push to your repository, create pipelines, create issues or add comments. To reduce storage capacity, delete unused repositories, artifacts, wikis, issues, and pipelines."
+msgstr ""
+
msgid "quick actions"
msgstr ""
@@ -25221,11 +25786,6 @@ msgstr ""
msgid "this document"
msgstr ""
-msgid "thread resolved"
-msgid_plural "threads resolved"
-msgstr[0] ""
-msgstr[1] ""
-
msgid "to help your contributors communicate effectively!"
msgstr ""
@@ -25280,6 +25840,9 @@ msgstr ""
msgid "view the blob"
msgstr ""
+msgid "vulnerability|Add a comment"
+msgstr ""
+
msgid "vulnerability|Add a comment or reason for dismissal"
msgstr ""
diff --git a/locale/bn_BD/gitlab.po b/locale/bn_BD/gitlab.po
index 66869600500..26747c5d718 100644
--- a/locale/bn_BD/gitlab.po
+++ b/locale/bn_BD/gitlab.po
@@ -12,7 +12,7 @@ msgstr ""
"X-Crowdin-Project: gitlab-ee\n"
"X-Crowdin-Language: bn\n"
"X-Crowdin-File: /master/locale/gitlab.pot\n"
-"PO-Revision-Date: 2020-04-15 00:29\n"
+"PO-Revision-Date: 2020-05-05 21:31\n"
msgid " %{start} to %{end}"
msgstr ""
@@ -212,6 +212,11 @@ msgid_plural "%d tags"
msgstr[0] ""
msgstr[1] ""
+msgid "%d unresolved thread"
+msgid_plural "%d unresolved threads"
+msgstr[0] ""
+msgstr[1] ""
+
msgid "%d vulnerability dismissed"
msgid_plural "%d vulnerabilities dismissed"
msgstr[0] ""
@@ -371,6 +376,9 @@ msgstr ""
msgid "%{mrText}, this issue will be closed automatically."
msgstr ""
+msgid "%{namespace_name} is now read-only. You cannot: %{base_message}"
+msgstr ""
+
msgid "%{name} contained %{resultsString}"
msgstr ""
@@ -383,6 +391,11 @@ msgstr ""
msgid "%{name}'s avatar"
msgstr ""
+msgid "%{no_of_days} day"
+msgid_plural "%{no_of_days} days"
+msgstr[0] ""
+msgstr[1] ""
+
msgid "%{number_commits_behind} commits behind %{default_branch}, %{number_commits_ahead} commits ahead"
msgstr ""
@@ -506,6 +519,9 @@ msgstr[1] ""
msgid "%{text} is available"
msgstr ""
+msgid "%{timebox_name} should belong either to a project or a group."
+msgstr ""
+
msgid "%{title} %{operator} %{threshold}"
msgstr ""
@@ -781,12 +797,18 @@ msgstr ""
msgid "<code>\"johnsmith@example.com\": \"johnsmith@example.com\"</code> will add \"By <a href=\"#\">johnsmith@example.com</a>\" to all issues and comments originally created by johnsmith@example.com. By default, the email address or username is masked to ensure the user's privacy. Use this option if you want to show the full email address."
msgstr ""
+msgid "<namespace / project>"
+msgstr ""
+
msgid "<no name set>"
msgstr ""
msgid "<no scopes selected>"
msgstr ""
+msgid "<project name>"
+msgstr ""
+
msgid "<strong>%{group_name}</strong> group members"
msgstr ""
@@ -820,6 +842,9 @@ msgstr ""
msgid "A basic page and serverless function that uses AWS Lambda, AWS API Gateway, and GitLab Pages"
msgstr ""
+msgid "A complete DevOps platform"
+msgstr ""
+
msgid "A default branch cannot be chosen for an empty project."
msgstr ""
@@ -1036,6 +1061,12 @@ msgstr ""
msgid "AccessTokens|reset it"
msgstr ""
+msgid "AccessibilityReport|Accessibility report artifact not found"
+msgstr ""
+
+msgid "AccessibilityReport|Failed to retrieve accessibility report"
+msgstr ""
+
msgid "AccessibilityReport|Learn More"
msgstr ""
@@ -1045,6 +1076,9 @@ msgstr ""
msgid "AccessibilityReport|New"
msgstr ""
+msgid "AccessibilityReport|The accessibility scanning found an error of the following type: %{code}"
+msgstr ""
+
msgid "Account"
msgstr ""
@@ -1287,6 +1321,9 @@ msgstr ""
msgid "Added at"
msgstr ""
+msgid "Added for this merge request"
+msgstr ""
+
msgid "Added in this version"
msgstr ""
@@ -1658,6 +1695,72 @@ msgid_plural "Alerts"
msgstr[0] ""
msgstr[1] ""
+msgid "Alert Details"
+msgstr ""
+
+msgid "AlertManagement|Acknowledged"
+msgstr ""
+
+msgid "AlertManagement|Alert"
+msgstr ""
+
+msgid "AlertManagement|Authorize external service"
+msgstr ""
+
+msgid "AlertManagement|Display alerts from all your monitoring tools directly within GitLab. Streamline the investigation of your alerts and the escalation of alerts to incidents."
+msgstr ""
+
+msgid "AlertManagement|End time"
+msgstr ""
+
+msgid "AlertManagement|End time:"
+msgstr ""
+
+msgid "AlertManagement|Events"
+msgstr ""
+
+msgid "AlertManagement|Events:"
+msgstr ""
+
+msgid "AlertManagement|Full Alert Details"
+msgstr ""
+
+msgid "AlertManagement|More information"
+msgstr ""
+
+msgid "AlertManagement|No alerts available to display. If you think you're seeing this message in error, refresh the page."
+msgstr ""
+
+msgid "AlertManagement|No alerts to display."
+msgstr ""
+
+msgid "AlertManagement|Overview"
+msgstr ""
+
+msgid "AlertManagement|Resolved"
+msgstr ""
+
+msgid "AlertManagement|Severity"
+msgstr ""
+
+msgid "AlertManagement|Start time"
+msgstr ""
+
+msgid "AlertManagement|Start time:"
+msgstr ""
+
+msgid "AlertManagement|Status"
+msgstr ""
+
+msgid "AlertManagement|Surface alerts in GitLab"
+msgstr ""
+
+msgid "AlertManagement|There was an error displaying the alerts. Confirm your endpoint's configuration details to ensure alerts appear."
+msgstr ""
+
+msgid "AlertManagement|Triggered"
+msgstr ""
+
msgid "AlertService|%{linkStart}Learn more%{linkEnd} about configuring this endpoint to receive alerts."
msgstr ""
@@ -1727,6 +1830,9 @@ msgstr ""
msgid "All security scans are enabled because %{linkStart}Auto DevOps%{linkEnd} is enabled on this project"
msgstr ""
+msgid "All threads resolved"
+msgstr ""
+
msgid "All users"
msgstr ""
@@ -1745,6 +1851,9 @@ msgstr ""
msgid "Allow only the selected protocols to be used for Git access."
msgstr ""
+msgid "Allow owners to manage default branch protection per group"
+msgstr ""
+
msgid "Allow owners to manually add users outside of LDAP"
msgstr ""
@@ -1853,6 +1962,9 @@ msgstr ""
msgid "An error occurred fetching the dropdown data."
msgstr ""
+msgid "An error occurred fetching the project authors."
+msgstr ""
+
msgid "An error occurred previewing the blob"
msgstr ""
@@ -1934,6 +2046,9 @@ msgstr ""
msgid "An error occurred while fetching sidebar data"
msgstr ""
+msgid "An error occurred while fetching terraform reports."
+msgstr ""
+
msgid "An error occurred while fetching the Service Desk address."
msgstr ""
@@ -2171,6 +2286,9 @@ msgstr ""
msgid "Any"
msgstr ""
+msgid "Any Author"
+msgstr ""
+
msgid "Any Label"
msgstr ""
@@ -2386,10 +2504,10 @@ msgstr ""
msgid "Are you sure that you want to unarchive this project?"
msgstr ""
-msgid "Are you sure you want to cancel creating this comment?"
+msgid "Are you sure you want to cancel editing this comment?"
msgstr ""
-msgid "Are you sure you want to cancel editing this comment?"
+msgid "Are you sure you want to close this blocked issue?"
msgstr ""
msgid "Are you sure you want to delete %{name}?"
@@ -2419,6 +2537,9 @@ msgstr ""
msgid "Are you sure you want to deploy this environment?"
msgstr ""
+msgid "Are you sure you want to discard this comment?"
+msgstr ""
+
msgid "Are you sure you want to erase this build?"
msgstr ""
@@ -2595,6 +2716,9 @@ msgstr ""
msgid "At least one approval from a code owner is required to change files matching the respective CODEOWNER rules."
msgstr ""
+msgid "At least one logging option is required to be enabled"
+msgstr ""
+
msgid "At least one of group_id or project_id must be specified"
msgstr ""
@@ -2633,6 +2757,27 @@ msgstr ""
msgid "AuditEvents|Target"
msgstr ""
+msgid "AuditLogs|(removed)"
+msgstr ""
+
+msgid "AuditLogs|Action"
+msgstr ""
+
+msgid "AuditLogs|Author"
+msgstr ""
+
+msgid "AuditLogs|Date"
+msgstr ""
+
+msgid "AuditLogs|IP Address"
+msgstr ""
+
+msgid "AuditLogs|Object"
+msgstr ""
+
+msgid "AuditLogs|Target"
+msgstr ""
+
msgid "Aug"
msgstr ""
@@ -2915,9 +3060,6 @@ msgstr ""
msgid "BambooService|You must set up automatic revision labeling and a repository trigger in Bamboo."
msgstr ""
-msgid "Batch operations"
-msgstr ""
-
msgid "BatchComments|Delete all pending comments"
msgstr ""
@@ -3047,6 +3189,9 @@ msgstr ""
msgid "Boards"
msgstr ""
+msgid "Boards and Board Lists"
+msgstr ""
+
msgid "Boards|Collapse"
msgstr ""
@@ -3266,6 +3411,9 @@ msgstr ""
msgid "Buy GitLab Enterprise Edition"
msgstr ""
+msgid "Buy more Pipeline minutes"
+msgstr ""
+
msgid "By %{user_name}"
msgstr ""
@@ -3386,6 +3534,9 @@ msgstr ""
msgid "Can't scan the code?"
msgstr ""
+msgid "Can't update snippet: %{err}"
+msgstr ""
+
msgid "Canary"
msgstr ""
@@ -3554,6 +3705,9 @@ msgstr ""
msgid "Changing group path can have unintended side effects."
msgstr ""
+msgid "Charts"
+msgstr ""
+
msgid "Charts can't be displayed as the request for data has timed out. %{documentationLink}"
msgstr ""
@@ -3590,9 +3744,6 @@ msgstr ""
msgid "ChatMessage|and [%{count} more](%{pipeline_failed_jobs_url})"
msgstr ""
-msgid "ChatMessage|failed"
-msgstr ""
-
msgid "ChatMessage|has failed"
msgstr ""
@@ -3608,9 +3759,6 @@ msgstr ""
msgid "ChatMessage|in %{project_link}"
msgstr ""
-msgid "ChatMessage|passed"
-msgstr ""
-
msgid "Check again"
msgstr ""
@@ -4061,9 +4209,6 @@ msgstr ""
msgid "Click any <strong>project name</strong> in the project list below to navigate to the project milestone."
msgstr ""
-msgid "Click here"
-msgstr ""
-
msgid "Click the <strong>Download</strong> button and wait for downloading to complete."
msgstr ""
@@ -4451,6 +4596,12 @@ msgstr ""
msgid "ClusterIntegration|Fetching zones"
msgstr ""
+msgid "ClusterIntegration|Fluentd"
+msgstr ""
+
+msgid "ClusterIntegration|Fluentd is an open source data collector, which lets you unify the data collection and consumption for a better use and understanding of data. It requires at least one of the following logs to be successfully installed."
+msgstr ""
+
msgid "ClusterIntegration|GitLab Integration"
msgstr ""
@@ -4757,6 +4908,15 @@ msgstr ""
msgid "ClusterIntegration|Request to begin uninstalling failed"
msgstr ""
+msgid "ClusterIntegration|SIEM Hostname"
+msgstr ""
+
+msgid "ClusterIntegration|SIEM Port"
+msgstr ""
+
+msgid "ClusterIntegration|SIEM Protocol"
+msgstr ""
+
msgid "ClusterIntegration|Save changes"
msgstr ""
@@ -4853,6 +5013,12 @@ msgstr ""
msgid "ClusterIntegration|Select zone to choose machine type"
msgstr ""
+msgid "ClusterIntegration|Send Cilium Logs"
+msgstr ""
+
+msgid "ClusterIntegration|Send ModSecurity Logs"
+msgstr ""
+
msgid "ClusterIntegration|Service Token"
msgstr ""
@@ -5281,18 +5447,33 @@ msgstr ""
msgid "Compliance framework (optional)"
msgstr ""
+msgid "ComplianceFramework|GDPR"
+msgstr ""
+
msgid "ComplianceFramework|GDPR - General Data Protection Regulation"
msgstr ""
+msgid "ComplianceFramework|HIPAA"
+msgstr ""
+
msgid "ComplianceFramework|HIPAA - Health Insurance Portability and Accountability Act"
msgstr ""
+msgid "ComplianceFramework|PCI-DSS"
+msgstr ""
+
msgid "ComplianceFramework|PCI-DSS - Payment Card Industry-Data Security Standard"
msgstr ""
+msgid "ComplianceFramework|SOC 2"
+msgstr ""
+
msgid "ComplianceFramework|SOC 2 - Service Organization Control 2"
msgstr ""
+msgid "ComplianceFramework|SOX"
+msgstr ""
+
msgid "ComplianceFramework|SOX - Sarbanes-Oxley"
msgstr ""
@@ -5440,9 +5621,15 @@ msgstr ""
msgid "Container repositories sync capacity"
msgstr ""
+msgid "ContainerRegistry| Please visit the %{linkStart}administration settings%{linkEnd} to enable this feature."
+msgstr ""
+
msgid "ContainerRegistry|%{imageName} tags"
msgstr ""
+msgid "ContainerRegistry|%{title} was successfully scheduled for deletion"
+msgstr ""
+
msgid "ContainerRegistry|Automatically remove extra images that aren't designed to be kept."
msgstr ""
@@ -5455,6 +5642,9 @@ msgstr ""
msgid "ContainerRegistry|Container Registry"
msgstr ""
+msgid "ContainerRegistry|Container Registry tag expiration and retention policy is disabled"
+msgstr ""
+
msgid "ContainerRegistry|Copy build command"
msgstr ""
@@ -5464,18 +5654,12 @@ msgstr ""
msgid "ContainerRegistry|Copy push command"
msgstr ""
-msgid "ContainerRegistry|Currently, the Container Registry tag expiration feature is not available for projects created before GitLab version 12.8. For updates and more information, visit Issue %{linkStart}#196124%{linkEnd}"
-msgstr ""
-
msgid "ContainerRegistry|Docker connection error"
msgstr ""
msgid "ContainerRegistry|Docker tag expiration policy is %{toggleStatus}"
msgstr ""
-msgid "ContainerRegistry|Docker tags with names matching this regex pattern will expire:"
-msgstr ""
-
msgid "ContainerRegistry|Edit Settings"
msgstr ""
@@ -5497,9 +5681,6 @@ msgstr ""
msgid "ContainerRegistry|Image ID"
msgstr ""
-msgid "ContainerRegistry|Image deleted successfully"
-msgstr ""
-
msgid "ContainerRegistry|Keep and protect the images that matter most."
msgstr ""
@@ -5515,12 +5696,24 @@ msgstr ""
msgid "ContainerRegistry|Number of tags to retain:"
msgstr ""
+msgid "ContainerRegistry|Please contact your administrator."
+msgstr ""
+
msgid "ContainerRegistry|Push an image"
msgstr ""
msgid "ContainerRegistry|Quick Start"
msgstr ""
+msgid "ContainerRegistry|Regular expressions such as %{codeStart}.*-test%{codeEnd} or %{codeStart}dev-.*%{codeEnd} are supported"
+msgstr ""
+
+msgid "ContainerRegistry|Regular expressions such as %{codeStart}.*-test%{codeEnd} or %{codeStart}dev-.*%{codeEnd} are supported. To select all tags, use %{codeStart}.*%{codeEnd}"
+msgstr ""
+
+msgid "ContainerRegistry|Remember to run %{docLinkStart}garbage collection%{docLinkEnd} to remove the stale data from storage."
+msgstr ""
+
msgid "ContainerRegistry|Remove repository"
msgstr ""
@@ -5535,22 +5728,22 @@ msgstr[1] ""
msgid "ContainerRegistry|Retention policy has been Enabled"
msgstr ""
-msgid "ContainerRegistry|Something went wrong while deleting the image."
+msgid "ContainerRegistry|Something went wrong while fetching the expiration policy."
msgstr ""
-msgid "ContainerRegistry|Something went wrong while deleting the tag."
+msgid "ContainerRegistry|Something went wrong while fetching the repository list."
msgstr ""
-msgid "ContainerRegistry|Something went wrong while deleting the tags."
+msgid "ContainerRegistry|Something went wrong while fetching the tags list."
msgstr ""
-msgid "ContainerRegistry|Something went wrong while fetching the expiration policy."
+msgid "ContainerRegistry|Something went wrong while marking the tag for deletion."
msgstr ""
-msgid "ContainerRegistry|Something went wrong while fetching the packages list."
+msgid "ContainerRegistry|Something went wrong while marking the tags for deletion."
msgstr ""
-msgid "ContainerRegistry|Something went wrong while fetching the tags list."
+msgid "ContainerRegistry|Something went wrong while scheduling %{title} for deletion. Please try again."
msgstr ""
msgid "ContainerRegistry|Something went wrong while updating the expiration policy."
@@ -5559,16 +5752,25 @@ msgstr ""
msgid "ContainerRegistry|Tag"
msgstr ""
-msgid "ContainerRegistry|Tag deleted successfully"
-msgstr ""
-
msgid "ContainerRegistry|Tag expiration policy"
msgstr ""
msgid "ContainerRegistry|Tag expiration policy is designed to:"
msgstr ""
-msgid "ContainerRegistry|Tags deleted successfully"
+msgid "ContainerRegistry|Tag successfully marked for deletion."
+msgstr ""
+
+msgid "ContainerRegistry|Tags successfully marked for deletion."
+msgstr ""
+
+msgid "ContainerRegistry|Tags with names matching this regex pattern will %{italicStart}be preserved:%{italicEnd}"
+msgstr ""
+
+msgid "ContainerRegistry|Tags with names matching this regex pattern will %{italicStart}expire:%{italicEnd}"
+msgstr ""
+
+msgid "ContainerRegistry|The Container Registry tag expiration and retention policies for this project have not been enabled."
msgstr ""
msgid "ContainerRegistry|The last tag related to this image was recently removed. This empty image and any associated data will be automatically removed as part of the regular Garbage Collection process. If you have any questions, contact your administrator."
@@ -5589,16 +5791,16 @@ msgstr ""
msgid "ContainerRegistry|There are no container images stored for this project"
msgstr ""
-msgid "ContainerRegistry|This Registry contains deleted image tag data. Remember to run %{docLinkStart}garbage collection%{docLinkEnd} to remove the stale data from storage."
+msgid "ContainerRegistry|There was an error during the deletion of this image repository, please try again."
msgstr ""
msgid "ContainerRegistry|This image has no active tags"
msgstr ""
-msgid "ContainerRegistry|We are having trouble connecting to Docker, which could be due to an issue with your project name or path. %{docLinkStart}More Information%{docLinkEnd}"
+msgid "ContainerRegistry|This image repository is scheduled for deletion"
msgstr ""
-msgid "ContainerRegistry|Wildcards such as %{codeStart}.*-stable%{codeEnd} or %{codeStart}production/.*%{codeEnd} are supported. To select all tags, use %{codeStart}.*%{codeEnd}"
+msgid "ContainerRegistry|We are having trouble connecting to Docker, which could be due to an issue with your project name or path. %{docLinkStart}More Information%{docLinkEnd}"
msgstr ""
msgid "ContainerRegistry|With the Container Registry, every project can have its own space to store its Docker images. %{docLinkStart}More Information%{docLinkEnd}"
@@ -5853,7 +6055,7 @@ msgstr ""
msgid "Could not delete chat nickname %{chat_name}."
msgstr ""
-msgid "Could not find design"
+msgid "Could not find design."
msgstr ""
msgid "Could not remove the trigger."
@@ -5868,6 +6070,9 @@ msgstr ""
msgid "Could not revoke personal access token %{personal_access_token_name}."
msgstr ""
+msgid "Could not revoke project access token %{project_access_token_name}."
+msgstr ""
+
msgid "Could not save group ID"
msgstr ""
@@ -5925,6 +6130,9 @@ msgstr ""
msgid "Create a new branch"
msgstr ""
+msgid "Create a new deploy key for this project"
+msgstr ""
+
msgid "Create a new file as there are no files yet. Afterwards, you'll be able to commit your changes."
msgstr ""
@@ -6030,6 +6238,9 @@ msgstr ""
msgid "Create requirement"
msgstr ""
+msgid "Create snippet"
+msgstr ""
+
msgid "Create wildcard: %{searchTerm}"
msgstr ""
@@ -6051,6 +6262,9 @@ msgstr ""
msgid "Created"
msgstr ""
+msgid "Created %{timestamp}"
+msgstr ""
+
msgid "Created At"
msgstr ""
@@ -6407,6 +6621,9 @@ msgstr ""
msgid "CycleAnalytics|stage dropdown"
msgstr ""
+msgid "DAG"
+msgstr ""
+
msgid "DNS"
msgstr ""
@@ -6773,6 +6990,9 @@ msgstr ""
msgid "Deploy key was successfully updated."
msgstr ""
+msgid "Deploy keys allow read-only or read-write (if enabled) access to your repository. Deploy keys can be used for CI, staging or production servers. You can create a deploy key or add an existing one."
+msgstr ""
+
msgid "Deploy progress not found. To see pods, ensure your environment matches %{linkStart}deploy board criteria%{linkEnd}."
msgstr ""
@@ -6926,6 +7146,9 @@ msgstr ""
msgid "Deploying to"
msgstr ""
+msgid "Deployment Frequency"
+msgstr ""
+
msgid "Deployment|API"
msgstr ""
@@ -7444,9 +7667,6 @@ msgstr ""
msgid "Edit environment"
msgstr ""
-msgid "Edit epic description"
-msgstr ""
-
msgid "Edit file"
msgstr ""
@@ -8068,9 +8288,6 @@ msgstr ""
msgid "Epics"
msgstr ""
-msgid "Epics (Ultimate / Gold license only)"
-msgstr ""
-
msgid "Epics Roadmap"
msgstr ""
@@ -8080,6 +8297,9 @@ msgstr ""
msgid "Epics let you manage your portfolio of projects more efficiently and with less effort"
msgstr ""
+msgid "Epics, Issues, and Merge Requests"
+msgstr ""
+
msgid "Epics|Add an epic"
msgstr ""
@@ -8131,6 +8351,9 @@ msgstr ""
msgid "Epics|Something went wrong while fetching group epics."
msgstr ""
+msgid "Epics|Something went wrong while moving item."
+msgstr ""
+
msgid "Epics|Something went wrong while ordering item."
msgstr ""
@@ -8185,6 +8408,9 @@ msgstr ""
msgid "Error fetching network graph."
msgstr ""
+msgid "Error fetching payload data."
+msgstr ""
+
msgid "Error fetching projects"
msgstr ""
@@ -8194,9 +8420,6 @@ msgstr ""
msgid "Error fetching the dependency list. Please check your network connection and try again."
msgstr ""
-msgid "Error fetching usage ping data."
-msgstr ""
-
msgid "Error loading branch data. Please try again."
msgstr ""
@@ -8338,6 +8561,9 @@ msgstr ""
msgid "Errors"
msgstr ""
+msgid "Errors:"
+msgstr ""
+
msgid "Estimated"
msgstr ""
@@ -8542,12 +8768,18 @@ msgstr ""
msgid "Export as CSV"
msgstr ""
+msgid "Export group"
+msgstr ""
+
msgid "Export issues"
msgstr ""
msgid "Export project"
msgstr ""
+msgid "Export this group with all related data to a new GitLab instance. Once complete, you can import the data file from the \"New Group\" page."
+msgstr ""
+
msgid "Export this project with all its related data in order to move your project to a new GitLab instance. Once the export is finished, you can import the file from the \"New Project\" page."
msgstr ""
@@ -8617,9 +8849,6 @@ msgstr ""
msgid "Failed Jobs"
msgstr ""
-msgid "Failed create wiki"
-msgstr ""
-
msgid "Failed to add a Zoom meeting"
msgstr ""
@@ -8659,6 +8888,9 @@ msgstr ""
msgid "Failed to create resources"
msgstr ""
+msgid "Failed to create wiki"
+msgstr ""
+
msgid "Failed to delete board. Please try again."
msgstr ""
@@ -8668,7 +8900,7 @@ msgstr ""
msgid "Failed to enqueue the rebase operation, possibly due to a long-lived transaction. Try again later."
msgstr ""
-msgid "Failed to find import label for jira import."
+msgid "Failed to find import label for Jira import."
msgstr ""
msgid "Failed to get ref."
@@ -8692,6 +8924,12 @@ msgstr ""
msgid "Failed to load groups & users."
msgstr ""
+msgid "Failed to load labels. Please try again."
+msgstr ""
+
+msgid "Failed to load milestones. Please try again."
+msgstr ""
+
msgid "Failed to load related branches"
msgstr ""
@@ -9067,7 +9305,10 @@ msgstr ""
msgid "Filter by milestone name"
msgstr ""
-msgid "Filter by name..."
+msgid "Filter by name"
+msgstr ""
+
+msgid "Filter by status"
msgstr ""
msgid "Filter by two-factor authentication"
@@ -9079,12 +9320,18 @@ msgstr ""
msgid "Filter projects"
msgstr ""
+msgid "Filter results"
+msgstr ""
+
msgid "Filter results by group"
msgstr ""
msgid "Filter results by project"
msgstr ""
+msgid "Filter results..."
+msgstr ""
+
msgid "Filter your projects by name"
msgstr ""
@@ -9373,10 +9620,10 @@ msgstr ""
msgid "GeoNodes|Checksummed"
msgstr ""
-msgid "GeoNodes|Container repositories"
+msgid "GeoNodes|Consult Geo troubleshooting information"
msgstr ""
-msgid "GeoNodes|Data is out of date from %{timeago}"
+msgid "GeoNodes|Container repositories"
msgstr ""
msgid "GeoNodes|Data replication lag"
@@ -9418,6 +9665,9 @@ msgstr ""
msgid "GeoNodes|Last event ID seen from primary"
msgstr ""
+msgid "GeoNodes|Learn more about Geo node statuses"
+msgstr ""
+
msgid "GeoNodes|Loading nodes"
msgstr ""
@@ -9433,6 +9683,9 @@ msgstr ""
msgid "GeoNodes|Node was successfully removed."
msgstr ""
+msgid "GeoNodes|Node's status was updated %{timeAgo}."
+msgstr ""
+
msgid "GeoNodes|Not checksummed"
msgstr ""
@@ -9463,7 +9716,10 @@ msgstr ""
msgid "GeoNodes|Repository verification progress"
msgstr ""
-msgid "GeoNodes|Selective"
+msgid "GeoNodes|Selective (%{syncLabel})"
+msgstr ""
+
+msgid "GeoNodes|Selective synchronization"
msgstr ""
msgid "GeoNodes|Something went wrong while changing node status"
@@ -9490,6 +9746,9 @@ msgstr ""
msgid "GeoNodes|Unverified"
msgstr ""
+msgid "GeoNodes|Updated %{timeAgo}"
+msgstr ""
+
msgid "GeoNodes|Used slots"
msgstr ""
@@ -9529,16 +9788,16 @@ msgstr ""
msgid "Geo|All"
msgstr ""
-msgid "Geo|All projects"
+msgid "Geo|All %{replicable_type}"
msgstr ""
-msgid "Geo|All projects are being scheduled for re-sync"
+msgid "Geo|All projects"
msgstr ""
-msgid "Geo|All projects are being scheduled for re-verify"
+msgid "Geo|All projects are being scheduled for resync"
msgstr ""
-msgid "Geo|Batch operations"
+msgid "Geo|All projects are being scheduled for reverify"
msgstr ""
msgid "Geo|Could not remove tracking entry for an existing project."
@@ -9550,9 +9809,15 @@ msgstr ""
msgid "Geo|Failed"
msgstr ""
+msgid "Geo|Filter by status"
+msgstr ""
+
msgid "Geo|Geo Status"
msgstr ""
+msgid "Geo|In progress"
+msgstr ""
+
msgid "Geo|In sync"
msgstr ""
@@ -9577,9 +9842,6 @@ msgstr ""
msgid "Geo|Not synced yet"
msgstr ""
-msgid "Geo|Pending"
-msgstr ""
-
msgid "Geo|Pending synchronization"
msgstr ""
@@ -9616,7 +9878,7 @@ msgstr ""
msgid "Geo|Resync"
msgstr ""
-msgid "Geo|Resync all projects"
+msgid "Geo|Resync all"
msgstr ""
msgid "Geo|Retry count"
@@ -9625,7 +9887,7 @@ msgstr ""
msgid "Geo|Reverify"
msgstr ""
-msgid "Geo|Reverify all projects"
+msgid "Geo|Reverify all"
msgstr ""
msgid "Geo|Status"
@@ -9967,9 +10229,6 @@ msgstr ""
msgid "Go to file"
msgstr ""
-msgid "Go to file (MRs only)"
-msgstr ""
-
msgid "Go to file permalink (while viewing a file)"
msgstr ""
@@ -10414,6 +10673,9 @@ msgstr ""
msgid "GroupSettings|Disable group mentions"
msgstr ""
+msgid "GroupSettings|Export group"
+msgstr ""
+
msgid "GroupSettings|If the parent group's visibility is lower than the group current visibility, visibility levels for subgroups and projects will be changed to match the new parent group's visibility."
msgstr ""
@@ -10492,6 +10754,9 @@ msgstr ""
msgid "Groups (%{count})"
msgstr ""
+msgid "Groups (%{groups})"
+msgstr ""
+
msgid "Groups can also be nested by creating %{subgroup_docs_link_start}subgroups%{subgroup_docs_link_end}."
msgstr ""
@@ -10757,15 +11022,12 @@ msgstr ""
msgid "ID:"
msgstr ""
-msgid "IDE|Allow live previews of JavaScript projects in the Web IDE using CodeSandbox client side evaluation."
+msgid "IDE|Allow live previews of JavaScript projects in the Web IDE using CodeSandbox Live Preview."
msgstr ""
msgid "IDE|Back"
msgstr ""
-msgid "IDE|Client side evaluation"
-msgstr ""
-
msgid "IDE|Commit"
msgstr ""
@@ -10829,6 +11091,9 @@ msgstr ""
msgid "If any job surpasses this timeout threshold, it will be marked as failed. Human readable time input language is accepted like \"1 hour\". Values without specification represent seconds."
msgstr ""
+msgid "If blank, set allowable lifetime to %{instance_level_policy_in_words}, as defined by the instance admin. Once set, existing tokens for users in this group may be revoked."
+msgstr ""
+
msgid "If checked, group owners can manage LDAP group links and LDAP member overrides"
msgstr ""
@@ -10868,6 +11133,9 @@ msgstr ""
msgid "If you lose your recovery codes you can generate new ones, invalidating all previous codes."
msgstr ""
+msgid "If you reach 100%% storage capacity, you will not be able to: %{base_message}"
+msgstr ""
+
msgid "If your HTTP repository is not publicly accessible, add your credentials."
msgstr ""
@@ -10979,6 +11247,9 @@ msgstr ""
msgid "Import repository"
msgstr ""
+msgid "Import started by: %{importInitiator}"
+msgstr ""
+
msgid "Import tasks"
msgstr ""
@@ -11045,6 +11316,9 @@ msgstr ""
msgid "In order to tailor your experience with GitLab we<br>would like to know a bit more about you."
msgstr ""
+msgid "In progress"
+msgstr ""
+
msgid "In the next step, you'll be able to select the projects you want to import."
msgstr ""
@@ -11392,15 +11666,15 @@ msgstr ""
msgid "Issues"
msgstr ""
-msgid "Issues / Merge Requests"
-msgstr ""
-
msgid "Issues Analytics"
msgstr ""
msgid "Issues Rate Limits"
msgstr ""
+msgid "Issues and Merge Requests"
+msgstr ""
+
msgid "Issues can be bugs, tasks or ideas to be discussed. Also, issues are searchable and filterable."
msgstr ""
@@ -11422,10 +11696,10 @@ msgstr ""
msgid "IssuesAnalytics|Avg/Month:"
msgstr ""
-msgid "IssuesAnalytics|Issues created"
+msgid "IssuesAnalytics|Issues opened"
msgstr ""
-msgid "IssuesAnalytics|Issues created per month"
+msgid "IssuesAnalytics|Issues opened per month"
msgstr ""
msgid "IssuesAnalytics|Last 12 months"
@@ -11479,6 +11753,9 @@ msgstr ""
msgid "Jira integration not configured."
msgstr ""
+msgid "Jira project: %{importProject}"
+msgstr ""
+
msgid "JiraService|Events for %{noteable_model_name} are disabled."
msgstr ""
@@ -11868,6 +12145,9 @@ msgstr ""
msgid "Lead"
msgstr ""
+msgid "Lead Time"
+msgstr ""
+
msgid "Learn GitLab"
msgstr ""
@@ -11880,6 +12160,9 @@ msgstr ""
msgid "Learn how to %{no_packages_link_start}publish and share your packages%{no_packages_link_end} with GitLab."
msgstr ""
+msgid "Learn how to enable synchronization"
+msgstr ""
+
msgid "Learn more"
msgstr ""
@@ -12211,9 +12494,6 @@ msgstr ""
msgid "Live preview"
msgstr ""
-msgid "Load more vulnerabilities"
-msgstr ""
-
msgid "Loading"
msgstr ""
@@ -12364,9 +12644,6 @@ msgstr ""
msgid "Manage"
msgstr ""
-msgid "Manage Git repositories with fine-grained access controls that keep your code secure. Perform code reviews and enhance collaboration with merge requests. Each project can also have an issue tracker and a wiki."
-msgstr ""
-
msgid "Manage Web IDE features"
msgstr ""
@@ -12397,6 +12674,9 @@ msgstr ""
msgid "Manage your license"
msgstr ""
+msgid "Managed Account"
+msgstr ""
+
msgid "Manifest"
msgstr ""
@@ -12661,6 +12941,9 @@ msgstr ""
msgid "Merge Requests in Review"
msgstr ""
+msgid "Merge automatically (%{strategy})"
+msgstr ""
+
msgid "Merge commit message"
msgstr ""
@@ -12814,6 +13097,12 @@ msgstr ""
msgid "Merged branches are being deleted. This can take some time depending on the number of branches. Please refresh the page to see changes."
msgstr ""
+msgid "Merged this merge request."
+msgstr ""
+
+msgid "Merges this merge request immediately."
+msgstr ""
+
msgid "Merges this merge request when the pipeline succeeds."
msgstr ""
@@ -12921,12 +13210,15 @@ msgid_plural "Metrics|Edit metrics"
msgstr[0] ""
msgstr[1] ""
-msgid "Metrics|Environment"
+msgid "Metrics|Expand panel"
msgstr ""
msgid "Metrics|For grouping similar metrics"
msgstr ""
+msgid "Metrics|Go back (Esc)"
+msgstr ""
+
msgid "Metrics|Invalid time range, please verify."
msgstr ""
@@ -12957,9 +13249,6 @@ msgstr ""
msgid "Metrics|Refresh dashboard"
msgstr ""
-msgid "Metrics|Show last"
-msgstr ""
-
msgid "Metrics|There was an error creating the dashboard."
msgstr ""
@@ -13038,6 +13327,9 @@ msgstr ""
msgid "Microsoft Azure"
msgstr ""
+msgid "Middleman project with Static Site Editor support"
+msgstr ""
+
msgid "Migrated %{success_count}/%{total_count} files."
msgstr ""
@@ -13483,10 +13775,10 @@ msgstr ""
msgid "Next"
msgstr ""
-msgid "Next file in diff (MRs only)"
+msgid "Next file in diff"
msgstr ""
-msgid "Next unresolved discussion (MRs only)"
+msgid "Next unresolved discussion"
msgstr ""
msgid "Nickname"
@@ -13546,6 +13838,9 @@ msgstr ""
msgid "No changes between %{ref_start}%{source_branch}%{ref_end} and %{ref_start}%{target_branch}%{ref_end}"
msgstr ""
+msgid "No child epics match applied filters"
+msgstr ""
+
msgid "No connection could be made to a Gitaly Server, please check your logs!"
msgstr ""
@@ -13684,15 +13979,6 @@ msgstr ""
msgid "No thanks, don't show this again"
msgstr ""
-msgid "No vulnerabilities found for this group"
-msgstr ""
-
-msgid "No vulnerabilities found for this pipeline"
-msgstr ""
-
-msgid "No vulnerabilities found for this project"
-msgstr ""
-
msgid "No vulnerabilities present"
msgstr ""
@@ -13816,6 +14102,9 @@ msgstr ""
msgid "Nothing to preview."
msgstr ""
+msgid "Nothing to synchronize"
+msgstr ""
+
msgid "Notification events"
msgstr ""
@@ -13987,6 +14276,9 @@ msgstr ""
msgid "Once removed, the fork relationship cannot be restored and you will no longer be able to send merge requests to the source."
msgstr ""
+msgid "Once the exported file is ready you can download it from this page."
+msgstr ""
+
msgid "Once the exported file is ready, you will receive a notification email with a download link, or you can download it from this page."
msgstr ""
@@ -14088,9 +14380,6 @@ msgstr ""
msgid "Open sidebar"
msgstr ""
-msgid "Open source software to collaborate on code"
-msgstr ""
-
msgid "Open: %{openIssuesCount}"
msgstr ""
@@ -14193,6 +14482,9 @@ msgstr ""
msgid "OutdatedBrowser|You can provide feedback %{feedback_link_start}on this issue%{feedback_link_end} or via your usual support channels."
msgstr ""
+msgid "Overridden"
+msgstr ""
+
msgid "Overview"
msgstr ""
@@ -14244,6 +14536,9 @@ msgstr ""
msgid "PackageRegistry|Conan Command"
msgstr ""
+msgid "PackageRegistry|Copy .pypirc content"
+msgstr ""
+
msgid "PackageRegistry|Copy Conan Command"
msgstr ""
@@ -14265,6 +14560,9 @@ msgstr ""
msgid "PackageRegistry|Copy NuGet Setup Command"
msgstr ""
+msgid "PackageRegistry|Copy Pip command"
+msgstr ""
+
msgid "PackageRegistry|Copy and paste this inside your %{codeStart}pom.xml%{codeEnd} %{codeStart}dependencies%{codeEnd} block."
msgstr ""
@@ -14298,6 +14596,12 @@ msgstr ""
msgid "PackageRegistry|For more information on the NuGet registry, %{linkStart}see the documentation%{linkEnd}."
msgstr ""
+msgid "PackageRegistry|For more information on the PyPi registry, %{linkStart}see the documentation%{linkEnd}."
+msgstr ""
+
+msgid "PackageRegistry|If you haven't already done so, you will need to add the below to your %{codeStart}.pypirc%{codeEnd} file."
+msgstr ""
+
msgid "PackageRegistry|If you haven't already done so, you will need to add the below to your %{codeStart}pom.xml%{codeEnd} file."
msgstr ""
@@ -14307,6 +14611,9 @@ msgstr ""
msgid "PackageRegistry|Learn how to %{noPackagesLinkStart}publish and share your packages%{noPackagesLinkEnd} with GitLab."
msgstr ""
+msgid "PackageRegistry|Manually Published"
+msgstr ""
+
msgid "PackageRegistry|Maven"
msgstr ""
@@ -14325,12 +14632,18 @@ msgstr ""
msgid "PackageRegistry|NuGet Command"
msgstr ""
+msgid "PackageRegistry|Pip Command"
+msgstr ""
+
msgid "PackageRegistry|Pipeline %{linkStart}%{linkEnd} triggered %{timestamp} by %{author}"
msgstr ""
msgid "PackageRegistry|Published to the repository at %{timestamp}"
msgstr ""
+msgid "PackageRegistry|PyPi"
+msgstr ""
+
msgid "PackageRegistry|Registry Setup"
msgstr ""
@@ -14367,6 +14680,9 @@ msgstr ""
msgid "PackageRegistry|npm"
msgstr ""
+msgid "PackageRegistry|published by %{author}"
+msgstr ""
+
msgid "PackageRegistry|yarn"
msgstr ""
@@ -14382,6 +14698,9 @@ msgstr ""
msgid "PackageType|NuGet"
msgstr ""
+msgid "PackageType|PyPi"
+msgstr ""
+
msgid "Packages"
msgstr ""
@@ -14475,6 +14794,9 @@ msgstr ""
msgid "Past due"
msgstr ""
+msgid "Paste a machine public key here. Read more about how to generate it"
+msgstr ""
+
msgid "Paste a machine public key here. Read more about how to generate it %{link_start}here%{link_end}"
msgstr ""
@@ -14490,9 +14812,6 @@ msgstr ""
msgid "Path"
msgstr ""
-msgid "Path, transfer, remove"
-msgstr ""
-
msgid "Path:"
msgstr ""
@@ -14523,7 +14842,7 @@ msgstr ""
msgid "Percentage"
msgstr ""
-msgid "Perform advanced options such as changing path, transferring, or removing the group."
+msgid "Perform advanced options such as changing path, transferring, exporting, or removing the group."
msgstr ""
msgid "Perform common operations on GitLab project"
@@ -14604,9 +14923,6 @@ msgstr ""
msgid "Pipeline minutes quota"
msgstr ""
-msgid "Pipeline quota"
-msgstr ""
-
msgid "Pipeline subscriptions"
msgstr ""
@@ -14616,6 +14932,9 @@ msgstr ""
msgid "Pipeline: %{status}"
msgstr ""
+msgid "PipelineCharts|CI / CD Analytics"
+msgstr ""
+
msgid "PipelineCharts|Failed:"
msgstr ""
@@ -14694,15 +15013,6 @@ msgstr ""
msgid "Pipelines settings for '%{project_name}' were successfully updated."
msgstr ""
-msgid "Pipelines| to purchase more minutes."
-msgstr ""
-
-msgid "Pipelines|%{namespace_name} has exceeded its pipeline minutes quota."
-msgstr ""
-
-msgid "Pipelines|%{namespace_name} has less than %{notification_level}%% of CI minutes available."
-msgstr ""
-
msgid "Pipelines|API"
msgstr ""
@@ -14724,10 +15034,13 @@ msgstr ""
msgid "Pipelines|Get started with Pipelines"
msgstr ""
-msgid "Pipelines|Loading Pipelines"
+msgid "Pipelines|Group %{namespace_name} has %{percentage}%% or less Shared Runner Pipeline minutes remaining. Once it runs out, no new jobs or pipelines in its projects will run."
msgstr ""
-msgid "Pipelines|Pipelines will not run anymore on shared Runners."
+msgid "Pipelines|Group %{namespace_name} has exceeded its pipeline minutes quota. Unless you buy additional pipeline minutes, no new jobs or pipelines in its projects will run."
+msgstr ""
+
+msgid "Pipelines|Loading Pipelines"
msgstr ""
msgid "Pipelines|Project cache successfully reset."
@@ -14958,6 +15271,9 @@ msgstr ""
msgid "Please select"
msgstr ""
+msgid "Please select a Jira project"
+msgstr ""
+
msgid "Please select a country"
msgstr ""
@@ -15003,6 +15319,9 @@ msgstr ""
msgid "Please wait while we import the repository for you. Refresh at will."
msgstr ""
+msgid "Plugins directory is deprecated and will be removed in 14.0. Please move this file into /file_hooks directory."
+msgstr ""
+
msgid "Pod does not exist"
msgstr ""
@@ -15120,7 +15439,7 @@ msgstr ""
msgid "Prevent users from changing their profile name"
msgstr ""
-msgid "Prevent users from modifing merge request approvers list"
+msgid "Prevent users from modifying merge request approvers list"
msgstr ""
msgid "Prevent users from performing write operations on GitLab while performing maintenance."
@@ -15141,10 +15460,10 @@ msgstr ""
msgid "Previous Artifacts"
msgstr ""
-msgid "Previous file in diff (MRs only)"
+msgid "Previous file in diff"
msgstr ""
-msgid "Previous unresolved discussion (MRs only)"
+msgid "Previous unresolved discussion"
msgstr ""
msgid "Primary"
@@ -15177,6 +15496,9 @@ msgstr ""
msgid "Private profile"
msgstr ""
+msgid "Private projects Minutes cost factor"
+msgstr ""
+
msgid "Private projects can be created in your personal namespace with:"
msgstr ""
@@ -16005,6 +16327,9 @@ msgstr ""
msgid "ProjectSettings|With GitLab Pages you can host your static websites on GitLab"
msgstr ""
+msgid "ProjectSettings|With Metrics Dashboard you can visualize this project performance metrics"
+msgstr ""
+
msgid "ProjectTemplates|.NET Core"
msgstr ""
@@ -16068,6 +16393,9 @@ msgstr ""
msgid "ProjectTemplates|Spring"
msgstr ""
+msgid "ProjectTemplates|Static Site Editor/Middleman"
+msgstr ""
+
msgid "ProjectTemplates|iOS (Swift)"
msgstr ""
@@ -16287,13 +16615,13 @@ msgstr ""
msgid "PrometheusService|Prometheus is being automatically managed on your clusters"
msgstr ""
-msgid "PrometheusService|These metrics will only be monitored after your first deployment to an environment"
+msgid "PrometheusService|Select the Active checkbox to override the Auto Configuration with custom settings. If unchecked, Auto Configuration settings are used."
msgstr ""
-msgid "PrometheusService|Time-series monitoring service"
+msgid "PrometheusService|These metrics will only be monitored after your first deployment to an environment"
msgstr ""
-msgid "PrometheusService|To enable manual configuration, uninstall Prometheus from your clusters"
+msgid "PrometheusService|Time-series monitoring service"
msgstr ""
msgid "PrometheusService|To enable the installation of Prometheus on your clusters, deactivate the manual configuration below"
@@ -16479,6 +16807,9 @@ msgstr ""
msgid "Protip:"
msgstr ""
+msgid "Protocol"
+msgstr ""
+
msgid "Provider"
msgstr ""
@@ -16503,6 +16834,9 @@ msgstr ""
msgid "Public pipelines"
msgstr ""
+msgid "Public projects Minutes cost factor"
+msgstr ""
+
msgid "Pull"
msgstr ""
@@ -16703,6 +17037,9 @@ msgid_plural "Refreshing in %d seconds to show the updated status..."
msgstr[0] ""
msgstr[1] ""
+msgid "Regenerate export"
+msgstr ""
+
msgid "Regenerate instance ID"
msgstr ""
@@ -17080,9 +17417,29 @@ msgstr ""
msgid "Reports|%{combinedString} and %{resolvedString}"
msgstr ""
+msgid "Reports|Accessibility scanning detected %d issue for the source branch only"
+msgid_plural "Reports|Accessibility scanning detected %d issues for the source branch only"
+msgstr[0] ""
+msgstr[1] ""
+
+msgid "Reports|Accessibility scanning detected no issues for the source branch only"
+msgstr ""
+
+msgid "Reports|Accessibility scanning failed loading results"
+msgstr ""
+
+msgid "Reports|Accessibility scanning results are being parsed"
+msgstr ""
+
msgid "Reports|Actions"
msgstr ""
+msgid "Reports|An error occured while loading report"
+msgstr ""
+
+msgid "Reports|An error occurred while loading %{name} results"
+msgstr ""
+
msgid "Reports|Class"
msgstr ""
@@ -17188,7 +17545,7 @@ msgstr ""
msgid "Requested %{time_ago}"
msgstr ""
-msgid "Requested design version does not exist"
+msgid "Requested design version does not exist."
msgstr ""
msgid "Requested states are invalid"
@@ -17372,6 +17729,9 @@ msgstr ""
msgid "Resync"
msgstr ""
+msgid "Resync all"
+msgstr ""
+
msgid "Resync all %{replicableType}"
msgstr ""
@@ -17440,6 +17800,9 @@ msgstr ""
msgid "Revoked personal access token %{personal_access_token_name}!"
msgstr ""
+msgid "Revoked project access token %{project_access_token_name}!"
+msgstr ""
+
msgid "RightSidebar|adding a"
msgstr ""
@@ -17488,6 +17851,9 @@ msgstr ""
msgid "Runner token"
msgstr ""
+msgid "Runner tokens"
+msgstr ""
+
msgid "Runner was not updated."
msgstr ""
@@ -17542,6 +17908,9 @@ msgstr ""
msgid "SAML SSO for %{group_name}"
msgstr ""
+msgid "SAML discovery tokens"
+msgstr ""
+
msgid "SAML for %{group_name}"
msgstr ""
@@ -17626,12 +17995,18 @@ msgstr ""
msgid "Scheduled"
msgstr ""
+msgid "Scheduled to merge this merge request (%{strategy})."
+msgstr ""
+
msgid "Scheduled to merge this merge request when the pipeline succeeds."
msgstr ""
msgid "Schedules"
msgstr ""
+msgid "Schedules to merge this merge request (%{strategy})."
+msgstr ""
+
msgid "Scheduling"
msgstr ""
@@ -17689,6 +18064,9 @@ msgstr ""
msgid "Search branches and tags"
msgstr ""
+msgid "Search by author"
+msgstr ""
+
msgid "Search files"
msgstr ""
@@ -17834,11 +18212,6 @@ msgid_plural "SearchResults|snippets"
msgstr[0] ""
msgstr[1] ""
-msgid "SearchResults|snippet result"
-msgid_plural "SearchResults|snippet results"
-msgstr[0] ""
-msgstr[1] ""
-
msgid "SearchResults|user"
msgid_plural "SearchResults|users"
msgstr[0] ""
@@ -17849,6 +18222,9 @@ msgid_plural "SearchResults|wiki results"
msgstr[0] ""
msgstr[1] ""
+msgid "Searching by both author and message is currently not supported."
+msgstr ""
+
msgid "Seat Link"
msgstr ""
@@ -17876,184 +18252,232 @@ msgstr ""
msgid "Security Dashboard"
msgstr ""
-msgid "Security Dashboard|Error fetching the vulnerability counts. Please check your network connection and try again."
+msgid "Security configuration help link"
+msgstr ""
+
+msgid "Security dashboard"
msgstr ""
-msgid "Security Dashboard|Error fetching the vulnerability list. Please check your network connection and try again."
+msgid "Security report is out of date. Please update your branch with the latest changes from the target branch (%{targetBranchName})"
msgstr ""
-msgid "Security Dashboard|Issue Created"
+msgid "Security report is out of date. Run %{newPipelineLinkStart}a new pipeline%{newPipelineLinkEnd} for the target branch (%{targetBranchName})"
msgstr ""
-msgid "Security Reports|Comment added to '%{vulnerabilityName}'"
+msgid "SecurityConfiguration|Configured"
msgstr ""
-msgid "Security Reports|Comment deleted on '%{vulnerabilityName}'"
+msgid "SecurityConfiguration|Feature"
msgstr ""
-msgid "Security Reports|Comment edited on '%{vulnerabilityName}'"
+msgid "SecurityConfiguration|Feature documentation for %{featureName}"
msgstr ""
-msgid "Security Reports|Create issue"
+msgid "SecurityConfiguration|Not yet configured"
msgstr ""
-msgid "Security Reports|Dismiss vulnerability"
+msgid "SecurityConfiguration|Secure features"
msgstr ""
-msgid "Security Reports|Dismissed '%{vulnerabilityName}'"
+msgid "SecurityConfiguration|Status"
msgstr ""
-msgid "Security Reports|Dismissed '%{vulnerabilityName}'. Turn off the hide dismissed toggle to view."
+msgid "SecurityReports|%{firstProject} and %{secondProject}"
msgstr ""
-msgid "Security Reports|Either you don't have permission to view this dashboard or the dashboard has not been setup. Please check your permission settings with your administrator or check your dashboard configurations to proceed."
+msgid "SecurityReports|%{firstProject}, %{secondProject}, and %{rest}"
msgstr ""
-msgid "Security Reports|Learn more about setting up your dashboard"
+msgid "SecurityReports|Add a project to your dashboard"
msgstr ""
-msgid "Security Reports|More info"
+msgid "SecurityReports|Add or remove projects from your dashboard"
msgstr ""
-msgid "Security Reports|Oops, something doesn't seem right."
+msgid "SecurityReports|Add projects"
msgstr ""
-msgid "Security Reports|Security reports can only be accessed by authorized users."
+msgid "SecurityReports|Comment added to '%{vulnerabilityName}'"
msgstr ""
-msgid "Security Reports|There was an error adding the comment."
+msgid "SecurityReports|Comment deleted on '%{vulnerabilityName}'"
msgstr ""
-msgid "Security Reports|There was an error creating the issue."
+msgid "SecurityReports|Comment edited on '%{vulnerabilityName}'"
msgstr ""
-msgid "Security Reports|There was an error creating the merge request."
+msgid "SecurityReports|Create issue"
msgstr ""
-msgid "Security Reports|There was an error deleting the comment."
+msgid "SecurityReports|Dismiss Selected"
msgstr ""
-msgid "Security Reports|There was an error dismissing the vulnerabilities."
+msgid "SecurityReports|Dismiss vulnerability"
msgstr ""
-msgid "Security Reports|There was an error dismissing the vulnerability."
+msgid "SecurityReports|Dismissed '%{vulnerabilityName}'"
msgstr ""
-msgid "Security Reports|There was an error reverting the dismissal."
+msgid "SecurityReports|Dismissed '%{vulnerabilityName}'. Turn off the hide dismissed toggle to view."
msgstr ""
-msgid "Security Reports|There was an error reverting this dismissal."
+msgid "SecurityReports|Each vulnerability now has a unique page that can be directly linked to, shared, referenced, and tracked as the single source of truth. Vulnerability occurrences also persist across scanner runs, which improves tracking and visibility and reduces duplicates between scans."
msgstr ""
-msgid "Security Reports|Undo dismiss"
+msgid "SecurityReports|Edit dashboard"
msgstr ""
-msgid "Security Reports|You do not have sufficient permissions to access this report"
+msgid "SecurityReports|Either you don't have permission to view this dashboard or the dashboard has not been setup. Please check your permission settings with your administrator or check your dashboard configurations to proceed."
msgstr ""
-msgid "Security Reports|You must sign in as an authorized user to see this report"
+msgid "SecurityReports|Error fetching the vulnerability counts. Please check your network connection and try again."
msgstr ""
-msgid "Security configuration help link"
+msgid "SecurityReports|Error fetching the vulnerability list. Please check your network connection and try again."
msgstr ""
-msgid "Security dashboard"
+msgid "SecurityReports|False positive"
msgstr ""
-msgid "Security report is out of date. Please update your branch with the latest changes from the target branch (%{targetBranchName})"
+msgid "SecurityReports|Hide dismissed"
msgstr ""
-msgid "Security report is out of date. Run %{newPipelineLinkStart}a new pipeline%{newPipelineLinkEnd} for the target branch (%{targetBranchName})"
+msgid "SecurityReports|Introducing standalone vulnerabilities"
msgstr ""
-msgid "SecurityConfiguration|Configured"
+msgid "SecurityReports|Issue Created"
msgstr ""
-msgid "SecurityConfiguration|Feature"
+msgid "SecurityReports|Learn More"
msgstr ""
-msgid "SecurityConfiguration|Feature documentation for %{featureName}"
+msgid "SecurityReports|Learn more about setting up your dashboard"
msgstr ""
-msgid "SecurityConfiguration|Not yet configured"
+msgid "SecurityReports|Load more vulnerabilities"
msgstr ""
-msgid "SecurityConfiguration|Secure features"
+msgid "SecurityReports|Monitor vulnerabilities in your code"
msgstr ""
-msgid "SecurityConfiguration|Status"
+msgid "SecurityReports|More info"
msgstr ""
-msgid "SecurityDashboard|%{firstProject} and %{secondProject}"
+msgid "SecurityReports|More information"
msgstr ""
-msgid "SecurityDashboard|%{firstProject}, %{secondProject}, and %{rest}"
+msgid "SecurityReports|No vulnerabilities found for dashboard"
msgstr ""
-msgid "SecurityDashboard|Add a project to your dashboard"
+msgid "SecurityReports|No vulnerabilities found for this group"
msgstr ""
-msgid "SecurityDashboard|Add or remove projects from your dashboard"
+msgid "SecurityReports|No vulnerabilities found for this pipeline"
msgstr ""
-msgid "SecurityDashboard|Add projects"
+msgid "SecurityReports|No vulnerabilities found for this project"
msgstr ""
-msgid "SecurityDashboard|Edit dashboard"
+msgid "SecurityReports|Oops, something doesn't seem right."
msgstr ""
-msgid "SecurityDashboard|Hide dismissed"
+msgid "SecurityReports|Pipeline %{pipelineLink} triggered %{timeago} by %{user}"
msgstr ""
-msgid "SecurityDashboard|Monitor vulnerabilities in your code"
+msgid "SecurityReports|Project"
msgstr ""
-msgid "SecurityDashboard|More information"
+msgid "SecurityReports|Projects added"
msgstr ""
-msgid "SecurityDashboard|No vulnerabilities found for dashboard"
+msgid "SecurityReports|Remove project from dashboard"
msgstr ""
-msgid "SecurityDashboard|Pipeline %{pipelineLink} triggered %{timeago} by %{user}"
+msgid "SecurityReports|Report type"
msgstr ""
-msgid "SecurityDashboard|Project"
+msgid "SecurityReports|Return to dashboard"
msgstr ""
-msgid "SecurityDashboard|Projects added"
+msgid "SecurityReports|Security Dashboard"
msgstr ""
-msgid "SecurityDashboard|Remove project from dashboard"
+msgid "SecurityReports|Security reports can only be accessed by authorized users."
msgstr ""
-msgid "SecurityDashboard|Report type"
+msgid "SecurityReports|Select a project to add by using the project search field above."
msgstr ""
-msgid "SecurityDashboard|Return to dashboard"
+msgid "SecurityReports|Select a reason"
msgstr ""
-msgid "SecurityDashboard|Security Dashboard"
+msgid "SecurityReports|Severity"
msgstr ""
-msgid "SecurityDashboard|Select a project to add by using the project search field above."
+msgid "SecurityReports|Status"
msgstr ""
-msgid "SecurityDashboard|Severity"
+msgid "SecurityReports|The security dashboard displays the latest security findings for projects you wish to monitor. Select \"Edit dashboard\" to add and remove projects."
msgstr ""
-msgid "SecurityDashboard|Status"
+msgid "SecurityReports|The security dashboard displays the latest security report. Use it to find and fix vulnerabilities."
msgstr ""
-msgid "SecurityDashboard|The security dashboard displays the latest security findings for projects you wish to monitor. Select \"Edit dashboard\" to add and remove projects."
+msgid "SecurityReports|There was an error adding the comment."
msgstr ""
-msgid "SecurityDashboard|The security dashboard displays the latest security report. Use it to find and fix vulnerabilities."
+msgid "SecurityReports|There was an error creating the issue."
msgstr ""
-msgid "SecurityDashboard|There was an error while generating the report."
+msgid "SecurityReports|There was an error creating the merge request."
msgstr ""
-msgid "SecurityDashboard|Unable to add %{invalidProjects}"
+msgid "SecurityReports|There was an error deleting the comment."
+msgstr ""
+
+msgid "SecurityReports|There was an error dismissing the vulnerabilities."
+msgstr ""
+
+msgid "SecurityReports|There was an error dismissing the vulnerability."
+msgstr ""
+
+msgid "SecurityReports|There was an error reverting the dismissal."
+msgstr ""
+
+msgid "SecurityReports|There was an error reverting this dismissal."
+msgstr ""
+
+msgid "SecurityReports|There was an error while generating the report."
+msgstr ""
+
+msgid "SecurityReports|Unable to add %{invalidProjects}"
+msgstr ""
+
+msgid "SecurityReports|Undo dismiss"
+msgstr ""
+
+msgid "SecurityReports|While it's rare to have no vulnerabilities for your group, it can happen. In any event, we ask that you double check your settings to make sure you've set up your dashboard correctly."
+msgstr ""
+
+msgid "SecurityReports|While it's rare to have no vulnerabilities for your pipeline, it can happen. In any event, we ask that you double check your settings to make sure all security scanning jobs have passed successfully."
+msgstr ""
+
+msgid "SecurityReports|While it's rare to have no vulnerabilities for your project, it can happen. In any event, we ask that you double check your settings to make sure you've set up your dashboard correctly."
+msgstr ""
+
+msgid "SecurityReports|While it's rare to have no vulnerabilities, it can happen. In any event, we ask that you please double check your settings to make sure you've set up your dashboard correctly."
+msgstr ""
+
+msgid "SecurityReports|Won't fix / Accept risk"
+msgstr ""
+
+msgid "SecurityReports|You do not have sufficient permissions to access this report"
+msgstr ""
+
+msgid "SecurityReports|You must sign in as an authorized user to see this report"
+msgstr ""
+
+msgid "SecurityReports|[No reason]"
msgstr ""
msgid "See GitLab's %{password_policy_guidelines}"
@@ -18065,6 +18489,9 @@ msgstr ""
msgid "See the affected projects in the GitLab admin panel"
msgstr ""
+msgid "See what's new at GitLab"
+msgstr ""
+
msgid "Select"
msgstr ""
@@ -18470,9 +18897,6 @@ msgstr ""
msgid "Set up Jira Integration"
msgstr ""
-msgid "Set up Jira Integration illustration"
-msgstr ""
-
msgid "Set up a %{type} Runner automatically"
msgstr ""
@@ -18557,6 +18981,9 @@ msgstr ""
msgid "Severity: %{severity}"
msgstr ""
+msgid "Shards (%{shards})"
+msgstr ""
+
msgid "Shards to synchronize"
msgstr ""
@@ -18871,6 +19298,9 @@ msgstr ""
msgid "Snowplow"
msgstr ""
+msgid "Some child epics may be hidden due to applied filters"
+msgstr ""
+
msgid "Some email servers do not support overriding the email sender name. Enable this option to include the name of the author of the issue, merge request or comment in the email body instead."
msgstr ""
@@ -19012,6 +19442,9 @@ msgstr ""
msgid "Something went wrong while updating your list settings"
msgstr ""
+msgid "Something went wrong with your automatic subscription renewal"
+msgstr ""
+
msgid "Something went wrong, unable to add %{project} to dashboard"
msgstr ""
@@ -19297,6 +19730,9 @@ msgstr ""
msgid "Stage removed"
msgstr ""
+msgid "Standard"
+msgstr ""
+
msgid "Star a label to make it a priority label. Order the prioritized labels to change their relative priority, by dragging."
msgstr ""
@@ -19420,6 +19856,9 @@ msgstr ""
msgid "Static Application Security Testing (SAST)"
msgstr ""
+msgid "StaticSiteEditor|An error occurred while submitting your changes."
+msgstr ""
+
msgid "StaticSiteEditor|Branch could not be created."
msgstr ""
@@ -19429,18 +19868,30 @@ msgstr ""
msgid "StaticSiteEditor|Could not create merge request."
msgstr ""
+msgid "StaticSiteEditor|Incompatible file content"
+msgstr ""
+
msgid "StaticSiteEditor|Return to site"
msgstr ""
+msgid "StaticSiteEditor|Static site editor"
+msgstr ""
+
msgid "StaticSiteEditor|Success!"
msgstr ""
msgid "StaticSiteEditor|Summary of changes"
msgstr ""
+msgid "StaticSiteEditor|The Static Site Editor is currently configured to only edit Markdown content on pages generated from Middleman. Visit the documentation to learn more about configuring your site to use the Static Site Editor."
+msgstr ""
+
msgid "StaticSiteEditor|Update %{sourcePath} file"
msgstr ""
+msgid "StaticSiteEditor|View documentation"
+msgstr ""
+
msgid "StaticSiteEditor|View merge request"
msgstr ""
@@ -19498,6 +19949,12 @@ msgstr ""
msgid "StatusPage|Status page"
msgstr ""
+msgid "StatusPage|Status page URL"
+msgstr ""
+
+msgid "StatusPage|Status page frontend documentation"
+msgstr ""
+
msgid "StatusPage|To publish incidents to an external status page, GitLab will store a JSON file in your Amazon S3 account in a location accessible to your external status page service. Make sure to also set up %{docsLink}"
msgstr ""
@@ -19846,6 +20303,12 @@ msgstr ""
msgid "Synced"
msgstr ""
+msgid "Synchronization disabled"
+msgstr ""
+
+msgid "Synchronization of container repositories is disabled."
+msgstr ""
+
msgid "System"
msgstr ""
@@ -19999,6 +20462,9 @@ msgstr ""
msgid "Telephone number"
msgstr ""
+msgid "Telephone number (Optional)"
+msgstr ""
+
msgid "Template"
msgstr ""
@@ -20305,6 +20771,9 @@ msgstr ""
msgid "The license was successfully uploaded and is now active. You can see the details below."
msgstr ""
+msgid "The license was successfully uploaded and will be active from %{starts_at}. You can see the details below."
+msgstr ""
+
msgid "The maximum file size allowed is %{size}."
msgstr ""
@@ -20626,6 +21095,9 @@ msgstr ""
msgid "There was an error getting the epic participants."
msgstr ""
+msgid "There was an error importing the Jira project."
+msgstr ""
+
msgid "There was an error loading users activity calendar."
msgstr ""
@@ -20641,7 +21113,7 @@ msgstr ""
msgid "There was an error resetting user pipeline minutes."
msgstr ""
-msgid "There was an error saving this Geo Node"
+msgid "There was an error saving this Geo Node."
msgstr ""
msgid "There was an error saving your changes."
@@ -20704,6 +21176,9 @@ msgstr ""
msgid "They can be managed using the %{link}."
msgstr ""
+msgid "Third Party Advisory Link"
+msgstr ""
+
msgid "Third party offers"
msgstr ""
@@ -20740,6 +21215,9 @@ msgstr ""
msgid "This application will be able to:"
msgstr ""
+msgid "This attachment has been truncated to avoid exceeding the maximum allowed attachment size of 15MB. %{written_count} of %{issues_count} issues have been included. Consider re-exporting with a narrower selection of issues."
+msgstr ""
+
msgid "This block is self-referential"
msgstr ""
@@ -20881,6 +21359,12 @@ msgstr ""
msgid "This issue is confidential"
msgstr ""
+msgid "This issue is currently blocked by the following issues: %{issues}."
+msgstr ""
+
+msgid "This issue is in a child epic of the filtered epic"
+msgstr ""
+
msgid "This issue is locked."
msgstr ""
@@ -21079,7 +21563,10 @@ msgstr ""
msgid "This user will be the author of all events in the activity feed that are the result of an update, like new branches being created or new commits being pushed to existing branches. Upon creation or when reassigning you can only assign yourself to be the mirror user."
msgstr ""
-msgid "This variable can not be masked"
+msgid "This variable can not be masked."
+msgstr ""
+
+msgid "This variable does not match the expected pattern."
msgstr ""
msgid "This will help us personalize your onboarding experience."
@@ -21109,10 +21596,10 @@ msgstr ""
msgid "ThreatMonitoring|Application firewall not detected"
msgstr ""
-msgid "ThreatMonitoring|Container Network Policy"
+msgid "ThreatMonitoring|Container Network Policies are not installed or have been disabled. To view this data, ensure your Network Policies are installed and enabled for your cluster."
msgstr ""
-msgid "ThreatMonitoring|Container NetworkPolicies are not installed or has been disabled. To view this data, ensure you NetworkPolicies are installed and enabled for your cluster."
+msgid "ThreatMonitoring|Container Network Policy"
msgstr ""
msgid "ThreatMonitoring|Container NetworkPolicies not detected"
@@ -21145,7 +21632,7 @@ msgstr ""
msgid "ThreatMonitoring|Something went wrong, unable to fetch statistics"
msgstr ""
-msgid "ThreatMonitoring|The firewall is not installed or has been disabled. To view this data, ensure you firewall is installed and enabled for your cluster."
+msgid "ThreatMonitoring|The firewall is not installed or has been disabled. To view this data, ensure the web application firewall is installed and enabled for your cluster."
msgstr ""
msgid "ThreatMonitoring|The graph below is an overview of traffic coming to your application as tracked by the Web Application Firewall (WAF). View the docs for instructions on how to access the WAF logs to see what type of malicious traffic is trying to access your app. The docs link is also accessible by clicking the \"?\" icon next to the title below."
@@ -21214,6 +21701,9 @@ msgstr ""
msgid "Time in seconds GitLab will wait for a response from the external service. When the service does not respond in time, access will be denied."
msgstr ""
+msgid "Time of import: %{importTime}"
+msgstr ""
+
msgid "Time remaining"
msgstr ""
@@ -21398,7 +21888,7 @@ msgstr ""
msgid "Title:"
msgstr ""
-msgid "Titles and Filenames"
+msgid "Titles and Descriptions"
msgstr ""
msgid "To"
@@ -21605,7 +22095,7 @@ msgstr ""
msgid "Too many projects enabled. You will need to manage them via the console or the API."
msgstr ""
-msgid "Topics"
+msgid "Topics (optional)"
msgstr ""
msgid "Total"
@@ -21767,6 +22257,9 @@ msgstr ""
msgid "Try all GitLab has to offer for 30 days."
msgstr ""
+msgid "Try changing or removing filters."
+msgstr ""
+
msgid "Try to fork again"
msgstr ""
@@ -21860,6 +22353,9 @@ msgstr ""
msgid "Unable to connect to server: %{error}"
msgstr ""
+msgid "Unable to connect to the Jira instance. Please check your Jira integration configuration."
+msgstr ""
+
msgid "Unable to convert Kubernetes logs encoding to UTF-8"
msgstr ""
@@ -22803,6 +23299,9 @@ msgstr ""
msgid "View issue"
msgstr ""
+msgid "View issues"
+msgstr ""
+
msgid "View it on GitLab"
msgstr ""
@@ -22980,6 +23479,15 @@ msgstr ""
msgid "VulnerabilityManagement|Resolved %{timeago} by %{user}"
msgstr ""
+msgid "VulnerabilityManagement|Something went wrong while trying to delete the comment. Please try again later."
+msgstr ""
+
+msgid "VulnerabilityManagement|Something went wrong while trying to retrieve the vulnerability history. Please try again later."
+msgstr ""
+
+msgid "VulnerabilityManagement|Something went wrong while trying to save the comment. Please try again later."
+msgstr ""
+
msgid "VulnerabilityManagement|Something went wrong, could not create an issue."
msgstr ""
@@ -23010,6 +23518,9 @@ msgstr ""
msgid "VulnerabilityStatusTypes|Resolved"
msgstr ""
+msgid "Vulnerability|%{scannerName} (version %{scannerVersion})"
+msgstr ""
+
msgid "Vulnerability|Class"
msgstr ""
@@ -23043,7 +23554,10 @@ msgstr ""
msgid "Vulnerability|Project"
msgstr ""
-msgid "Vulnerability|Report Type"
+msgid "Vulnerability|Scanner Provider"
+msgstr ""
+
+msgid "Vulnerability|Scanner Type"
msgstr ""
msgid "Vulnerability|Severity"
@@ -23097,9 +23611,15 @@ msgstr ""
msgid "We sent you an email with reset password instructions"
msgstr ""
+msgid "We tried to automatically renew your %{strong}%{plan_name}%{strong_close} subscription for %{strong}%{namespace_name}%{strong_close} on %{expires_on} but something went wrong so your subscription was downgraded to the free plan. Don't worry, your data is safe. We suggest you check your payment method and get in touch with our support team (%{support_link}). They'll gladly help with your subscription renewal."
+msgstr ""
+
msgid "We want to be sure it is you, please confirm you are not a robot."
msgstr ""
+msgid "We will automatically renew your %{strong}%{plan_name}%{strong_close} subscription for %{strong}%{namespace_name}%{strong_close} on %{strong}%{expires_on}%{strong_close}. There's nothing that you need to do, we'll let you know when the renewal is complete. Need more seats, a higher plan or just want to review your payment method?"
+msgstr ""
+
msgid "We've found no vulnerabilities"
msgstr ""
@@ -23204,15 +23724,6 @@ msgstr ""
msgid "When:"
msgstr ""
-msgid "While it's rare to have no vulnerabilities for your group, it can happen. In any event, we ask that you double check your settings to make sure you've set up your dashboard correctly."
-msgstr ""
-
-msgid "While it's rare to have no vulnerabilities for your pipeline, it can happen. In any event, we ask that you double check your settings to make sure all security scanning jobs have passed successfully."
-msgstr ""
-
-msgid "While it's rare to have no vulnerabilities for your project, it can happen. In any event, we ask that you double check your settings to make sure you've set up your dashboard correctly."
-msgstr ""
-
msgid "While it's rare to have no vulnerabilities, it can happen. In any event, we ask that you please double check your settings to make sure you've set up your dashboard correctly."
msgstr ""
@@ -23243,9 +23754,6 @@ msgstr ""
msgid "Wiki"
msgstr ""
-msgid "Wiki pages"
-msgstr ""
-
msgid "Wiki was successfully updated."
msgstr ""
@@ -23429,6 +23937,9 @@ msgstr ""
msgid "Yes, add it"
msgstr ""
+msgid "Yes, close issue"
+msgstr ""
+
msgid "Yes, let me map Google Code users to full names or GitLab users."
msgstr ""
@@ -23624,6 +24135,9 @@ msgstr ""
msgid "You could not create a new trigger."
msgstr ""
+msgid "You didn't renew your %{strong}%{plan_name}%{strong_close} subscription for %{strong}%{namespace_name}%{strong_close} so it was downgraded to the free plan."
+msgstr ""
+
msgid "You didn't renew your %{strong}%{plan_name}%{strong_close} subscription so it was downgraded to the GitLab Core Plan."
msgstr ""
@@ -23690,6 +24204,9 @@ msgstr ""
msgid "You have declined the invitation to join %{label}."
msgstr ""
+msgid "You have imported from this project %{numberOfPreviousImportsForProject} times before. Each new import will create duplicate issues."
+msgstr ""
+
msgid "You have no permissions"
msgstr ""
@@ -23771,6 +24288,9 @@ msgstr ""
msgid "You need to upload a Google Takeout archive."
msgstr ""
+msgid "You reached %{usage_in_percent} of %{namespace_name}'s capacity (%{used_storage} of %{storage_limit})"
+msgstr ""
+
msgid "You tried to fork %{link_to_the_project} but it failed for the following reason:"
msgstr ""
@@ -23855,9 +24375,21 @@ msgstr ""
msgid "YouTube"
msgstr ""
+msgid "Your %{strong}%{plan_name}%{strong_close} subscription for %{strong}%{namespace_name}%{strong_close} will expire on %{strong}%{expires_on}%{strong_close}. After that, you will not to be able to create issues or merge requests as well as many other features."
+msgstr ""
+
msgid "Your %{strong}%{plan_name}%{strong_close} subscription will expire on %{strong}%{expires_on}%{strong_close}. After that, you will not to be able to create issues or merge requests as well as many other features."
msgstr ""
+msgid "Your CSV export has started. It will be emailed to %{email} when complete."
+msgstr ""
+
+msgid "Your CSV export of %{issues_count} from project %{project_link} has been added to this email as an attachment."
+msgstr ""
+
+msgid "Your CSV export of %{written_count} from project %{project_name} (%{project_url}) has been added to this email as an attachment."
+msgstr ""
+
msgid "Your Commit Email will be used for web based operations, such as edits and merges."
msgstr ""
@@ -23954,6 +24486,9 @@ msgstr ""
msgid "Your comment could not be updated! Please check your network connection and try again."
msgstr ""
+msgid "Your comment will be discarded."
+msgstr ""
+
msgid "Your custom stage '%{title}' was created"
msgstr ""
@@ -23993,6 +24528,9 @@ msgstr ""
msgid "Your new personal access token has been created."
msgstr ""
+msgid "Your new project access token has been created."
+msgstr ""
+
msgid "Your password isn't required to view this page. If a password or any other personal details are requested, please contact your administrator to report abuse."
msgstr ""
@@ -24011,12 +24549,18 @@ msgstr ""
msgid "Your request for access has been queued for review."
msgstr ""
+msgid "Your search didn't match any commits."
+msgstr ""
+
msgid "Your subscription expired!"
msgstr ""
msgid "Your subscription has been downgraded"
msgstr ""
+msgid "Your subscription will automatically renew in %{remaining_days}"
+msgstr ""
+
msgid "Your subscription will expire in %{remaining_days}"
msgstr ""
@@ -24055,7 +24599,7 @@ msgstr ""
msgid "allowed to fail"
msgstr ""
-msgid "already being used for another group or project milestone."
+msgid "already being used for another group or project %{timebox_name}."
msgstr ""
msgid "already has a \"created\" issue link"
@@ -24133,6 +24677,9 @@ msgstr ""
msgid "ciReport|%{linkStartTag}Learn more about SAST %{linkEndTag}"
msgstr ""
+msgid "ciReport|%{linkStartTag}Learn more about Secret Scanning %{linkEndTag}"
+msgstr ""
+
msgid "ciReport|%{linkStartTag}Learn more about codequality reports %{linkEndTag}"
msgstr ""
@@ -24296,6 +24843,12 @@ msgstr ""
msgid "ciReport|SAST"
msgstr ""
+msgid "ciReport|Secret scanning"
+msgstr ""
+
+msgid "ciReport|Secret scanning detects secrets and credentials vulnerabilities in your source code."
+msgstr ""
+
msgid "ciReport|Security scanning"
msgstr ""
@@ -24493,6 +25046,9 @@ msgstr ""
msgid "group"
msgstr ""
+msgid "groups"
+msgstr ""
+
msgid "has already been linked to another vulnerability"
msgstr ""
@@ -24656,9 +25212,6 @@ msgstr[1] ""
msgid "merged %{time_ago}"
msgstr ""
-msgid "milestone should belong either to a project or a group."
-msgstr ""
-
msgid "missing"
msgstr ""
@@ -24995,6 +25548,9 @@ msgstr ""
msgid "no contributions"
msgstr ""
+msgid "no expiration"
+msgstr ""
+
msgid "no one can merge"
msgstr ""
@@ -25047,6 +25603,9 @@ msgstr ""
msgid "pending removal"
msgstr ""
+msgid "per day"
+msgstr ""
+
msgid "pipeline"
msgstr ""
@@ -25075,6 +25634,12 @@ msgstr[1] ""
msgid "project avatar"
msgstr ""
+msgid "projects"
+msgstr ""
+
+msgid "push to your repository, create pipelines, create issues or add comments. To reduce storage capacity, delete unused repositories, artifacts, wikis, issues, and pipelines."
+msgstr ""
+
msgid "quick actions"
msgstr ""
@@ -25221,11 +25786,6 @@ msgstr ""
msgid "this document"
msgstr ""
-msgid "thread resolved"
-msgid_plural "threads resolved"
-msgstr[0] ""
-msgstr[1] ""
-
msgid "to help your contributors communicate effectively!"
msgstr ""
@@ -25280,6 +25840,9 @@ msgstr ""
msgid "view the blob"
msgstr ""
+msgid "vulnerability|Add a comment"
+msgstr ""
+
msgid "vulnerability|Add a comment or reason for dismissal"
msgstr ""
diff --git a/locale/bn_IN/gitlab.po b/locale/bn_IN/gitlab.po
index 9a0957d15be..8dd05d90fc5 100644
--- a/locale/bn_IN/gitlab.po
+++ b/locale/bn_IN/gitlab.po
@@ -12,7 +12,7 @@ msgstr ""
"X-Crowdin-Project: gitlab-ee\n"
"X-Crowdin-Language: bn-IN\n"
"X-Crowdin-File: /master/locale/gitlab.pot\n"
-"PO-Revision-Date: 2020-04-15 00:29\n"
+"PO-Revision-Date: 2020-05-05 21:34\n"
msgid " %{start} to %{end}"
msgstr ""
@@ -212,6 +212,11 @@ msgid_plural "%d tags"
msgstr[0] ""
msgstr[1] ""
+msgid "%d unresolved thread"
+msgid_plural "%d unresolved threads"
+msgstr[0] ""
+msgstr[1] ""
+
msgid "%d vulnerability dismissed"
msgid_plural "%d vulnerabilities dismissed"
msgstr[0] ""
@@ -371,6 +376,9 @@ msgstr ""
msgid "%{mrText}, this issue will be closed automatically."
msgstr ""
+msgid "%{namespace_name} is now read-only. You cannot: %{base_message}"
+msgstr ""
+
msgid "%{name} contained %{resultsString}"
msgstr ""
@@ -383,6 +391,11 @@ msgstr ""
msgid "%{name}'s avatar"
msgstr ""
+msgid "%{no_of_days} day"
+msgid_plural "%{no_of_days} days"
+msgstr[0] ""
+msgstr[1] ""
+
msgid "%{number_commits_behind} commits behind %{default_branch}, %{number_commits_ahead} commits ahead"
msgstr ""
@@ -506,6 +519,9 @@ msgstr[1] ""
msgid "%{text} is available"
msgstr ""
+msgid "%{timebox_name} should belong either to a project or a group."
+msgstr ""
+
msgid "%{title} %{operator} %{threshold}"
msgstr ""
@@ -781,12 +797,18 @@ msgstr ""
msgid "<code>\"johnsmith@example.com\": \"johnsmith@example.com\"</code> will add \"By <a href=\"#\">johnsmith@example.com</a>\" to all issues and comments originally created by johnsmith@example.com. By default, the email address or username is masked to ensure the user's privacy. Use this option if you want to show the full email address."
msgstr ""
+msgid "<namespace / project>"
+msgstr ""
+
msgid "<no name set>"
msgstr ""
msgid "<no scopes selected>"
msgstr ""
+msgid "<project name>"
+msgstr ""
+
msgid "<strong>%{group_name}</strong> group members"
msgstr ""
@@ -820,6 +842,9 @@ msgstr ""
msgid "A basic page and serverless function that uses AWS Lambda, AWS API Gateway, and GitLab Pages"
msgstr ""
+msgid "A complete DevOps platform"
+msgstr ""
+
msgid "A default branch cannot be chosen for an empty project."
msgstr ""
@@ -1036,6 +1061,12 @@ msgstr ""
msgid "AccessTokens|reset it"
msgstr ""
+msgid "AccessibilityReport|Accessibility report artifact not found"
+msgstr ""
+
+msgid "AccessibilityReport|Failed to retrieve accessibility report"
+msgstr ""
+
msgid "AccessibilityReport|Learn More"
msgstr ""
@@ -1045,6 +1076,9 @@ msgstr ""
msgid "AccessibilityReport|New"
msgstr ""
+msgid "AccessibilityReport|The accessibility scanning found an error of the following type: %{code}"
+msgstr ""
+
msgid "Account"
msgstr ""
@@ -1287,6 +1321,9 @@ msgstr ""
msgid "Added at"
msgstr ""
+msgid "Added for this merge request"
+msgstr ""
+
msgid "Added in this version"
msgstr ""
@@ -1658,6 +1695,72 @@ msgid_plural "Alerts"
msgstr[0] ""
msgstr[1] ""
+msgid "Alert Details"
+msgstr ""
+
+msgid "AlertManagement|Acknowledged"
+msgstr ""
+
+msgid "AlertManagement|Alert"
+msgstr ""
+
+msgid "AlertManagement|Authorize external service"
+msgstr ""
+
+msgid "AlertManagement|Display alerts from all your monitoring tools directly within GitLab. Streamline the investigation of your alerts and the escalation of alerts to incidents."
+msgstr ""
+
+msgid "AlertManagement|End time"
+msgstr ""
+
+msgid "AlertManagement|End time:"
+msgstr ""
+
+msgid "AlertManagement|Events"
+msgstr ""
+
+msgid "AlertManagement|Events:"
+msgstr ""
+
+msgid "AlertManagement|Full Alert Details"
+msgstr ""
+
+msgid "AlertManagement|More information"
+msgstr ""
+
+msgid "AlertManagement|No alerts available to display. If you think you're seeing this message in error, refresh the page."
+msgstr ""
+
+msgid "AlertManagement|No alerts to display."
+msgstr ""
+
+msgid "AlertManagement|Overview"
+msgstr ""
+
+msgid "AlertManagement|Resolved"
+msgstr ""
+
+msgid "AlertManagement|Severity"
+msgstr ""
+
+msgid "AlertManagement|Start time"
+msgstr ""
+
+msgid "AlertManagement|Start time:"
+msgstr ""
+
+msgid "AlertManagement|Status"
+msgstr ""
+
+msgid "AlertManagement|Surface alerts in GitLab"
+msgstr ""
+
+msgid "AlertManagement|There was an error displaying the alerts. Confirm your endpoint's configuration details to ensure alerts appear."
+msgstr ""
+
+msgid "AlertManagement|Triggered"
+msgstr ""
+
msgid "AlertService|%{linkStart}Learn more%{linkEnd} about configuring this endpoint to receive alerts."
msgstr ""
@@ -1727,6 +1830,9 @@ msgstr ""
msgid "All security scans are enabled because %{linkStart}Auto DevOps%{linkEnd} is enabled on this project"
msgstr ""
+msgid "All threads resolved"
+msgstr ""
+
msgid "All users"
msgstr ""
@@ -1745,6 +1851,9 @@ msgstr ""
msgid "Allow only the selected protocols to be used for Git access."
msgstr ""
+msgid "Allow owners to manage default branch protection per group"
+msgstr ""
+
msgid "Allow owners to manually add users outside of LDAP"
msgstr ""
@@ -1853,6 +1962,9 @@ msgstr ""
msgid "An error occurred fetching the dropdown data."
msgstr ""
+msgid "An error occurred fetching the project authors."
+msgstr ""
+
msgid "An error occurred previewing the blob"
msgstr ""
@@ -1934,6 +2046,9 @@ msgstr ""
msgid "An error occurred while fetching sidebar data"
msgstr ""
+msgid "An error occurred while fetching terraform reports."
+msgstr ""
+
msgid "An error occurred while fetching the Service Desk address."
msgstr ""
@@ -2171,6 +2286,9 @@ msgstr ""
msgid "Any"
msgstr ""
+msgid "Any Author"
+msgstr ""
+
msgid "Any Label"
msgstr ""
@@ -2386,10 +2504,10 @@ msgstr ""
msgid "Are you sure that you want to unarchive this project?"
msgstr ""
-msgid "Are you sure you want to cancel creating this comment?"
+msgid "Are you sure you want to cancel editing this comment?"
msgstr ""
-msgid "Are you sure you want to cancel editing this comment?"
+msgid "Are you sure you want to close this blocked issue?"
msgstr ""
msgid "Are you sure you want to delete %{name}?"
@@ -2419,6 +2537,9 @@ msgstr ""
msgid "Are you sure you want to deploy this environment?"
msgstr ""
+msgid "Are you sure you want to discard this comment?"
+msgstr ""
+
msgid "Are you sure you want to erase this build?"
msgstr ""
@@ -2595,6 +2716,9 @@ msgstr ""
msgid "At least one approval from a code owner is required to change files matching the respective CODEOWNER rules."
msgstr ""
+msgid "At least one logging option is required to be enabled"
+msgstr ""
+
msgid "At least one of group_id or project_id must be specified"
msgstr ""
@@ -2633,6 +2757,27 @@ msgstr ""
msgid "AuditEvents|Target"
msgstr ""
+msgid "AuditLogs|(removed)"
+msgstr ""
+
+msgid "AuditLogs|Action"
+msgstr ""
+
+msgid "AuditLogs|Author"
+msgstr ""
+
+msgid "AuditLogs|Date"
+msgstr ""
+
+msgid "AuditLogs|IP Address"
+msgstr ""
+
+msgid "AuditLogs|Object"
+msgstr ""
+
+msgid "AuditLogs|Target"
+msgstr ""
+
msgid "Aug"
msgstr ""
@@ -2915,9 +3060,6 @@ msgstr ""
msgid "BambooService|You must set up automatic revision labeling and a repository trigger in Bamboo."
msgstr ""
-msgid "Batch operations"
-msgstr ""
-
msgid "BatchComments|Delete all pending comments"
msgstr ""
@@ -3047,6 +3189,9 @@ msgstr ""
msgid "Boards"
msgstr ""
+msgid "Boards and Board Lists"
+msgstr ""
+
msgid "Boards|Collapse"
msgstr ""
@@ -3266,6 +3411,9 @@ msgstr ""
msgid "Buy GitLab Enterprise Edition"
msgstr ""
+msgid "Buy more Pipeline minutes"
+msgstr ""
+
msgid "By %{user_name}"
msgstr ""
@@ -3386,6 +3534,9 @@ msgstr ""
msgid "Can't scan the code?"
msgstr ""
+msgid "Can't update snippet: %{err}"
+msgstr ""
+
msgid "Canary"
msgstr ""
@@ -3554,6 +3705,9 @@ msgstr ""
msgid "Changing group path can have unintended side effects."
msgstr ""
+msgid "Charts"
+msgstr ""
+
msgid "Charts can't be displayed as the request for data has timed out. %{documentationLink}"
msgstr ""
@@ -3590,9 +3744,6 @@ msgstr ""
msgid "ChatMessage|and [%{count} more](%{pipeline_failed_jobs_url})"
msgstr ""
-msgid "ChatMessage|failed"
-msgstr ""
-
msgid "ChatMessage|has failed"
msgstr ""
@@ -3608,9 +3759,6 @@ msgstr ""
msgid "ChatMessage|in %{project_link}"
msgstr ""
-msgid "ChatMessage|passed"
-msgstr ""
-
msgid "Check again"
msgstr ""
@@ -4061,9 +4209,6 @@ msgstr ""
msgid "Click any <strong>project name</strong> in the project list below to navigate to the project milestone."
msgstr ""
-msgid "Click here"
-msgstr ""
-
msgid "Click the <strong>Download</strong> button and wait for downloading to complete."
msgstr ""
@@ -4451,6 +4596,12 @@ msgstr ""
msgid "ClusterIntegration|Fetching zones"
msgstr ""
+msgid "ClusterIntegration|Fluentd"
+msgstr ""
+
+msgid "ClusterIntegration|Fluentd is an open source data collector, which lets you unify the data collection and consumption for a better use and understanding of data. It requires at least one of the following logs to be successfully installed."
+msgstr ""
+
msgid "ClusterIntegration|GitLab Integration"
msgstr ""
@@ -4757,6 +4908,15 @@ msgstr ""
msgid "ClusterIntegration|Request to begin uninstalling failed"
msgstr ""
+msgid "ClusterIntegration|SIEM Hostname"
+msgstr ""
+
+msgid "ClusterIntegration|SIEM Port"
+msgstr ""
+
+msgid "ClusterIntegration|SIEM Protocol"
+msgstr ""
+
msgid "ClusterIntegration|Save changes"
msgstr ""
@@ -4853,6 +5013,12 @@ msgstr ""
msgid "ClusterIntegration|Select zone to choose machine type"
msgstr ""
+msgid "ClusterIntegration|Send Cilium Logs"
+msgstr ""
+
+msgid "ClusterIntegration|Send ModSecurity Logs"
+msgstr ""
+
msgid "ClusterIntegration|Service Token"
msgstr ""
@@ -5281,18 +5447,33 @@ msgstr ""
msgid "Compliance framework (optional)"
msgstr ""
+msgid "ComplianceFramework|GDPR"
+msgstr ""
+
msgid "ComplianceFramework|GDPR - General Data Protection Regulation"
msgstr ""
+msgid "ComplianceFramework|HIPAA"
+msgstr ""
+
msgid "ComplianceFramework|HIPAA - Health Insurance Portability and Accountability Act"
msgstr ""
+msgid "ComplianceFramework|PCI-DSS"
+msgstr ""
+
msgid "ComplianceFramework|PCI-DSS - Payment Card Industry-Data Security Standard"
msgstr ""
+msgid "ComplianceFramework|SOC 2"
+msgstr ""
+
msgid "ComplianceFramework|SOC 2 - Service Organization Control 2"
msgstr ""
+msgid "ComplianceFramework|SOX"
+msgstr ""
+
msgid "ComplianceFramework|SOX - Sarbanes-Oxley"
msgstr ""
@@ -5440,9 +5621,15 @@ msgstr ""
msgid "Container repositories sync capacity"
msgstr ""
+msgid "ContainerRegistry| Please visit the %{linkStart}administration settings%{linkEnd} to enable this feature."
+msgstr ""
+
msgid "ContainerRegistry|%{imageName} tags"
msgstr ""
+msgid "ContainerRegistry|%{title} was successfully scheduled for deletion"
+msgstr ""
+
msgid "ContainerRegistry|Automatically remove extra images that aren't designed to be kept."
msgstr ""
@@ -5455,6 +5642,9 @@ msgstr ""
msgid "ContainerRegistry|Container Registry"
msgstr ""
+msgid "ContainerRegistry|Container Registry tag expiration and retention policy is disabled"
+msgstr ""
+
msgid "ContainerRegistry|Copy build command"
msgstr ""
@@ -5464,18 +5654,12 @@ msgstr ""
msgid "ContainerRegistry|Copy push command"
msgstr ""
-msgid "ContainerRegistry|Currently, the Container Registry tag expiration feature is not available for projects created before GitLab version 12.8. For updates and more information, visit Issue %{linkStart}#196124%{linkEnd}"
-msgstr ""
-
msgid "ContainerRegistry|Docker connection error"
msgstr ""
msgid "ContainerRegistry|Docker tag expiration policy is %{toggleStatus}"
msgstr ""
-msgid "ContainerRegistry|Docker tags with names matching this regex pattern will expire:"
-msgstr ""
-
msgid "ContainerRegistry|Edit Settings"
msgstr ""
@@ -5497,9 +5681,6 @@ msgstr ""
msgid "ContainerRegistry|Image ID"
msgstr ""
-msgid "ContainerRegistry|Image deleted successfully"
-msgstr ""
-
msgid "ContainerRegistry|Keep and protect the images that matter most."
msgstr ""
@@ -5515,12 +5696,24 @@ msgstr ""
msgid "ContainerRegistry|Number of tags to retain:"
msgstr ""
+msgid "ContainerRegistry|Please contact your administrator."
+msgstr ""
+
msgid "ContainerRegistry|Push an image"
msgstr ""
msgid "ContainerRegistry|Quick Start"
msgstr ""
+msgid "ContainerRegistry|Regular expressions such as %{codeStart}.*-test%{codeEnd} or %{codeStart}dev-.*%{codeEnd} are supported"
+msgstr ""
+
+msgid "ContainerRegistry|Regular expressions such as %{codeStart}.*-test%{codeEnd} or %{codeStart}dev-.*%{codeEnd} are supported. To select all tags, use %{codeStart}.*%{codeEnd}"
+msgstr ""
+
+msgid "ContainerRegistry|Remember to run %{docLinkStart}garbage collection%{docLinkEnd} to remove the stale data from storage."
+msgstr ""
+
msgid "ContainerRegistry|Remove repository"
msgstr ""
@@ -5535,22 +5728,22 @@ msgstr[1] ""
msgid "ContainerRegistry|Retention policy has been Enabled"
msgstr ""
-msgid "ContainerRegistry|Something went wrong while deleting the image."
+msgid "ContainerRegistry|Something went wrong while fetching the expiration policy."
msgstr ""
-msgid "ContainerRegistry|Something went wrong while deleting the tag."
+msgid "ContainerRegistry|Something went wrong while fetching the repository list."
msgstr ""
-msgid "ContainerRegistry|Something went wrong while deleting the tags."
+msgid "ContainerRegistry|Something went wrong while fetching the tags list."
msgstr ""
-msgid "ContainerRegistry|Something went wrong while fetching the expiration policy."
+msgid "ContainerRegistry|Something went wrong while marking the tag for deletion."
msgstr ""
-msgid "ContainerRegistry|Something went wrong while fetching the packages list."
+msgid "ContainerRegistry|Something went wrong while marking the tags for deletion."
msgstr ""
-msgid "ContainerRegistry|Something went wrong while fetching the tags list."
+msgid "ContainerRegistry|Something went wrong while scheduling %{title} for deletion. Please try again."
msgstr ""
msgid "ContainerRegistry|Something went wrong while updating the expiration policy."
@@ -5559,16 +5752,25 @@ msgstr ""
msgid "ContainerRegistry|Tag"
msgstr ""
-msgid "ContainerRegistry|Tag deleted successfully"
-msgstr ""
-
msgid "ContainerRegistry|Tag expiration policy"
msgstr ""
msgid "ContainerRegistry|Tag expiration policy is designed to:"
msgstr ""
-msgid "ContainerRegistry|Tags deleted successfully"
+msgid "ContainerRegistry|Tag successfully marked for deletion."
+msgstr ""
+
+msgid "ContainerRegistry|Tags successfully marked for deletion."
+msgstr ""
+
+msgid "ContainerRegistry|Tags with names matching this regex pattern will %{italicStart}be preserved:%{italicEnd}"
+msgstr ""
+
+msgid "ContainerRegistry|Tags with names matching this regex pattern will %{italicStart}expire:%{italicEnd}"
+msgstr ""
+
+msgid "ContainerRegistry|The Container Registry tag expiration and retention policies for this project have not been enabled."
msgstr ""
msgid "ContainerRegistry|The last tag related to this image was recently removed. This empty image and any associated data will be automatically removed as part of the regular Garbage Collection process. If you have any questions, contact your administrator."
@@ -5589,16 +5791,16 @@ msgstr ""
msgid "ContainerRegistry|There are no container images stored for this project"
msgstr ""
-msgid "ContainerRegistry|This Registry contains deleted image tag data. Remember to run %{docLinkStart}garbage collection%{docLinkEnd} to remove the stale data from storage."
+msgid "ContainerRegistry|There was an error during the deletion of this image repository, please try again."
msgstr ""
msgid "ContainerRegistry|This image has no active tags"
msgstr ""
-msgid "ContainerRegistry|We are having trouble connecting to Docker, which could be due to an issue with your project name or path. %{docLinkStart}More Information%{docLinkEnd}"
+msgid "ContainerRegistry|This image repository is scheduled for deletion"
msgstr ""
-msgid "ContainerRegistry|Wildcards such as %{codeStart}.*-stable%{codeEnd} or %{codeStart}production/.*%{codeEnd} are supported. To select all tags, use %{codeStart}.*%{codeEnd}"
+msgid "ContainerRegistry|We are having trouble connecting to Docker, which could be due to an issue with your project name or path. %{docLinkStart}More Information%{docLinkEnd}"
msgstr ""
msgid "ContainerRegistry|With the Container Registry, every project can have its own space to store its Docker images. %{docLinkStart}More Information%{docLinkEnd}"
@@ -5853,7 +6055,7 @@ msgstr ""
msgid "Could not delete chat nickname %{chat_name}."
msgstr ""
-msgid "Could not find design"
+msgid "Could not find design."
msgstr ""
msgid "Could not remove the trigger."
@@ -5868,6 +6070,9 @@ msgstr ""
msgid "Could not revoke personal access token %{personal_access_token_name}."
msgstr ""
+msgid "Could not revoke project access token %{project_access_token_name}."
+msgstr ""
+
msgid "Could not save group ID"
msgstr ""
@@ -5925,6 +6130,9 @@ msgstr ""
msgid "Create a new branch"
msgstr ""
+msgid "Create a new deploy key for this project"
+msgstr ""
+
msgid "Create a new file as there are no files yet. Afterwards, you'll be able to commit your changes."
msgstr ""
@@ -6030,6 +6238,9 @@ msgstr ""
msgid "Create requirement"
msgstr ""
+msgid "Create snippet"
+msgstr ""
+
msgid "Create wildcard: %{searchTerm}"
msgstr ""
@@ -6051,6 +6262,9 @@ msgstr ""
msgid "Created"
msgstr ""
+msgid "Created %{timestamp}"
+msgstr ""
+
msgid "Created At"
msgstr ""
@@ -6407,6 +6621,9 @@ msgstr ""
msgid "CycleAnalytics|stage dropdown"
msgstr ""
+msgid "DAG"
+msgstr ""
+
msgid "DNS"
msgstr ""
@@ -6773,6 +6990,9 @@ msgstr ""
msgid "Deploy key was successfully updated."
msgstr ""
+msgid "Deploy keys allow read-only or read-write (if enabled) access to your repository. Deploy keys can be used for CI, staging or production servers. You can create a deploy key or add an existing one."
+msgstr ""
+
msgid "Deploy progress not found. To see pods, ensure your environment matches %{linkStart}deploy board criteria%{linkEnd}."
msgstr ""
@@ -6926,6 +7146,9 @@ msgstr ""
msgid "Deploying to"
msgstr ""
+msgid "Deployment Frequency"
+msgstr ""
+
msgid "Deployment|API"
msgstr ""
@@ -7444,9 +7667,6 @@ msgstr ""
msgid "Edit environment"
msgstr ""
-msgid "Edit epic description"
-msgstr ""
-
msgid "Edit file"
msgstr ""
@@ -8068,9 +8288,6 @@ msgstr ""
msgid "Epics"
msgstr ""
-msgid "Epics (Ultimate / Gold license only)"
-msgstr ""
-
msgid "Epics Roadmap"
msgstr ""
@@ -8080,6 +8297,9 @@ msgstr ""
msgid "Epics let you manage your portfolio of projects more efficiently and with less effort"
msgstr ""
+msgid "Epics, Issues, and Merge Requests"
+msgstr ""
+
msgid "Epics|Add an epic"
msgstr ""
@@ -8131,6 +8351,9 @@ msgstr ""
msgid "Epics|Something went wrong while fetching group epics."
msgstr ""
+msgid "Epics|Something went wrong while moving item."
+msgstr ""
+
msgid "Epics|Something went wrong while ordering item."
msgstr ""
@@ -8185,6 +8408,9 @@ msgstr ""
msgid "Error fetching network graph."
msgstr ""
+msgid "Error fetching payload data."
+msgstr ""
+
msgid "Error fetching projects"
msgstr ""
@@ -8194,9 +8420,6 @@ msgstr ""
msgid "Error fetching the dependency list. Please check your network connection and try again."
msgstr ""
-msgid "Error fetching usage ping data."
-msgstr ""
-
msgid "Error loading branch data. Please try again."
msgstr ""
@@ -8338,6 +8561,9 @@ msgstr ""
msgid "Errors"
msgstr ""
+msgid "Errors:"
+msgstr ""
+
msgid "Estimated"
msgstr ""
@@ -8542,12 +8768,18 @@ msgstr ""
msgid "Export as CSV"
msgstr ""
+msgid "Export group"
+msgstr ""
+
msgid "Export issues"
msgstr ""
msgid "Export project"
msgstr ""
+msgid "Export this group with all related data to a new GitLab instance. Once complete, you can import the data file from the \"New Group\" page."
+msgstr ""
+
msgid "Export this project with all its related data in order to move your project to a new GitLab instance. Once the export is finished, you can import the file from the \"New Project\" page."
msgstr ""
@@ -8617,9 +8849,6 @@ msgstr ""
msgid "Failed Jobs"
msgstr ""
-msgid "Failed create wiki"
-msgstr ""
-
msgid "Failed to add a Zoom meeting"
msgstr ""
@@ -8659,6 +8888,9 @@ msgstr ""
msgid "Failed to create resources"
msgstr ""
+msgid "Failed to create wiki"
+msgstr ""
+
msgid "Failed to delete board. Please try again."
msgstr ""
@@ -8668,7 +8900,7 @@ msgstr ""
msgid "Failed to enqueue the rebase operation, possibly due to a long-lived transaction. Try again later."
msgstr ""
-msgid "Failed to find import label for jira import."
+msgid "Failed to find import label for Jira import."
msgstr ""
msgid "Failed to get ref."
@@ -8692,6 +8924,12 @@ msgstr ""
msgid "Failed to load groups & users."
msgstr ""
+msgid "Failed to load labels. Please try again."
+msgstr ""
+
+msgid "Failed to load milestones. Please try again."
+msgstr ""
+
msgid "Failed to load related branches"
msgstr ""
@@ -9067,7 +9305,10 @@ msgstr ""
msgid "Filter by milestone name"
msgstr ""
-msgid "Filter by name..."
+msgid "Filter by name"
+msgstr ""
+
+msgid "Filter by status"
msgstr ""
msgid "Filter by two-factor authentication"
@@ -9079,12 +9320,18 @@ msgstr ""
msgid "Filter projects"
msgstr ""
+msgid "Filter results"
+msgstr ""
+
msgid "Filter results by group"
msgstr ""
msgid "Filter results by project"
msgstr ""
+msgid "Filter results..."
+msgstr ""
+
msgid "Filter your projects by name"
msgstr ""
@@ -9373,10 +9620,10 @@ msgstr ""
msgid "GeoNodes|Checksummed"
msgstr ""
-msgid "GeoNodes|Container repositories"
+msgid "GeoNodes|Consult Geo troubleshooting information"
msgstr ""
-msgid "GeoNodes|Data is out of date from %{timeago}"
+msgid "GeoNodes|Container repositories"
msgstr ""
msgid "GeoNodes|Data replication lag"
@@ -9418,6 +9665,9 @@ msgstr ""
msgid "GeoNodes|Last event ID seen from primary"
msgstr ""
+msgid "GeoNodes|Learn more about Geo node statuses"
+msgstr ""
+
msgid "GeoNodes|Loading nodes"
msgstr ""
@@ -9433,6 +9683,9 @@ msgstr ""
msgid "GeoNodes|Node was successfully removed."
msgstr ""
+msgid "GeoNodes|Node's status was updated %{timeAgo}."
+msgstr ""
+
msgid "GeoNodes|Not checksummed"
msgstr ""
@@ -9463,7 +9716,10 @@ msgstr ""
msgid "GeoNodes|Repository verification progress"
msgstr ""
-msgid "GeoNodes|Selective"
+msgid "GeoNodes|Selective (%{syncLabel})"
+msgstr ""
+
+msgid "GeoNodes|Selective synchronization"
msgstr ""
msgid "GeoNodes|Something went wrong while changing node status"
@@ -9490,6 +9746,9 @@ msgstr ""
msgid "GeoNodes|Unverified"
msgstr ""
+msgid "GeoNodes|Updated %{timeAgo}"
+msgstr ""
+
msgid "GeoNodes|Used slots"
msgstr ""
@@ -9529,16 +9788,16 @@ msgstr ""
msgid "Geo|All"
msgstr ""
-msgid "Geo|All projects"
+msgid "Geo|All %{replicable_type}"
msgstr ""
-msgid "Geo|All projects are being scheduled for re-sync"
+msgid "Geo|All projects"
msgstr ""
-msgid "Geo|All projects are being scheduled for re-verify"
+msgid "Geo|All projects are being scheduled for resync"
msgstr ""
-msgid "Geo|Batch operations"
+msgid "Geo|All projects are being scheduled for reverify"
msgstr ""
msgid "Geo|Could not remove tracking entry for an existing project."
@@ -9550,9 +9809,15 @@ msgstr ""
msgid "Geo|Failed"
msgstr ""
+msgid "Geo|Filter by status"
+msgstr ""
+
msgid "Geo|Geo Status"
msgstr ""
+msgid "Geo|In progress"
+msgstr ""
+
msgid "Geo|In sync"
msgstr ""
@@ -9577,9 +9842,6 @@ msgstr ""
msgid "Geo|Not synced yet"
msgstr ""
-msgid "Geo|Pending"
-msgstr ""
-
msgid "Geo|Pending synchronization"
msgstr ""
@@ -9616,7 +9878,7 @@ msgstr ""
msgid "Geo|Resync"
msgstr ""
-msgid "Geo|Resync all projects"
+msgid "Geo|Resync all"
msgstr ""
msgid "Geo|Retry count"
@@ -9625,7 +9887,7 @@ msgstr ""
msgid "Geo|Reverify"
msgstr ""
-msgid "Geo|Reverify all projects"
+msgid "Geo|Reverify all"
msgstr ""
msgid "Geo|Status"
@@ -9967,9 +10229,6 @@ msgstr ""
msgid "Go to file"
msgstr ""
-msgid "Go to file (MRs only)"
-msgstr ""
-
msgid "Go to file permalink (while viewing a file)"
msgstr ""
@@ -10414,6 +10673,9 @@ msgstr ""
msgid "GroupSettings|Disable group mentions"
msgstr ""
+msgid "GroupSettings|Export group"
+msgstr ""
+
msgid "GroupSettings|If the parent group's visibility is lower than the group current visibility, visibility levels for subgroups and projects will be changed to match the new parent group's visibility."
msgstr ""
@@ -10492,6 +10754,9 @@ msgstr ""
msgid "Groups (%{count})"
msgstr ""
+msgid "Groups (%{groups})"
+msgstr ""
+
msgid "Groups can also be nested by creating %{subgroup_docs_link_start}subgroups%{subgroup_docs_link_end}."
msgstr ""
@@ -10757,15 +11022,12 @@ msgstr ""
msgid "ID:"
msgstr ""
-msgid "IDE|Allow live previews of JavaScript projects in the Web IDE using CodeSandbox client side evaluation."
+msgid "IDE|Allow live previews of JavaScript projects in the Web IDE using CodeSandbox Live Preview."
msgstr ""
msgid "IDE|Back"
msgstr ""
-msgid "IDE|Client side evaluation"
-msgstr ""
-
msgid "IDE|Commit"
msgstr ""
@@ -10829,6 +11091,9 @@ msgstr ""
msgid "If any job surpasses this timeout threshold, it will be marked as failed. Human readable time input language is accepted like \"1 hour\". Values without specification represent seconds."
msgstr ""
+msgid "If blank, set allowable lifetime to %{instance_level_policy_in_words}, as defined by the instance admin. Once set, existing tokens for users in this group may be revoked."
+msgstr ""
+
msgid "If checked, group owners can manage LDAP group links and LDAP member overrides"
msgstr ""
@@ -10868,6 +11133,9 @@ msgstr ""
msgid "If you lose your recovery codes you can generate new ones, invalidating all previous codes."
msgstr ""
+msgid "If you reach 100%% storage capacity, you will not be able to: %{base_message}"
+msgstr ""
+
msgid "If your HTTP repository is not publicly accessible, add your credentials."
msgstr ""
@@ -10979,6 +11247,9 @@ msgstr ""
msgid "Import repository"
msgstr ""
+msgid "Import started by: %{importInitiator}"
+msgstr ""
+
msgid "Import tasks"
msgstr ""
@@ -11045,6 +11316,9 @@ msgstr ""
msgid "In order to tailor your experience with GitLab we<br>would like to know a bit more about you."
msgstr ""
+msgid "In progress"
+msgstr ""
+
msgid "In the next step, you'll be able to select the projects you want to import."
msgstr ""
@@ -11392,15 +11666,15 @@ msgstr ""
msgid "Issues"
msgstr ""
-msgid "Issues / Merge Requests"
-msgstr ""
-
msgid "Issues Analytics"
msgstr ""
msgid "Issues Rate Limits"
msgstr ""
+msgid "Issues and Merge Requests"
+msgstr ""
+
msgid "Issues can be bugs, tasks or ideas to be discussed. Also, issues are searchable and filterable."
msgstr ""
@@ -11422,10 +11696,10 @@ msgstr ""
msgid "IssuesAnalytics|Avg/Month:"
msgstr ""
-msgid "IssuesAnalytics|Issues created"
+msgid "IssuesAnalytics|Issues opened"
msgstr ""
-msgid "IssuesAnalytics|Issues created per month"
+msgid "IssuesAnalytics|Issues opened per month"
msgstr ""
msgid "IssuesAnalytics|Last 12 months"
@@ -11479,6 +11753,9 @@ msgstr ""
msgid "Jira integration not configured."
msgstr ""
+msgid "Jira project: %{importProject}"
+msgstr ""
+
msgid "JiraService|Events for %{noteable_model_name} are disabled."
msgstr ""
@@ -11868,6 +12145,9 @@ msgstr ""
msgid "Lead"
msgstr ""
+msgid "Lead Time"
+msgstr ""
+
msgid "Learn GitLab"
msgstr ""
@@ -11880,6 +12160,9 @@ msgstr ""
msgid "Learn how to %{no_packages_link_start}publish and share your packages%{no_packages_link_end} with GitLab."
msgstr ""
+msgid "Learn how to enable synchronization"
+msgstr ""
+
msgid "Learn more"
msgstr ""
@@ -12211,9 +12494,6 @@ msgstr ""
msgid "Live preview"
msgstr ""
-msgid "Load more vulnerabilities"
-msgstr ""
-
msgid "Loading"
msgstr ""
@@ -12364,9 +12644,6 @@ msgstr ""
msgid "Manage"
msgstr ""
-msgid "Manage Git repositories with fine-grained access controls that keep your code secure. Perform code reviews and enhance collaboration with merge requests. Each project can also have an issue tracker and a wiki."
-msgstr ""
-
msgid "Manage Web IDE features"
msgstr ""
@@ -12397,6 +12674,9 @@ msgstr ""
msgid "Manage your license"
msgstr ""
+msgid "Managed Account"
+msgstr ""
+
msgid "Manifest"
msgstr ""
@@ -12661,6 +12941,9 @@ msgstr ""
msgid "Merge Requests in Review"
msgstr ""
+msgid "Merge automatically (%{strategy})"
+msgstr ""
+
msgid "Merge commit message"
msgstr ""
@@ -12814,6 +13097,12 @@ msgstr ""
msgid "Merged branches are being deleted. This can take some time depending on the number of branches. Please refresh the page to see changes."
msgstr ""
+msgid "Merged this merge request."
+msgstr ""
+
+msgid "Merges this merge request immediately."
+msgstr ""
+
msgid "Merges this merge request when the pipeline succeeds."
msgstr ""
@@ -12921,12 +13210,15 @@ msgid_plural "Metrics|Edit metrics"
msgstr[0] ""
msgstr[1] ""
-msgid "Metrics|Environment"
+msgid "Metrics|Expand panel"
msgstr ""
msgid "Metrics|For grouping similar metrics"
msgstr ""
+msgid "Metrics|Go back (Esc)"
+msgstr ""
+
msgid "Metrics|Invalid time range, please verify."
msgstr ""
@@ -12957,9 +13249,6 @@ msgstr ""
msgid "Metrics|Refresh dashboard"
msgstr ""
-msgid "Metrics|Show last"
-msgstr ""
-
msgid "Metrics|There was an error creating the dashboard."
msgstr ""
@@ -13038,6 +13327,9 @@ msgstr ""
msgid "Microsoft Azure"
msgstr ""
+msgid "Middleman project with Static Site Editor support"
+msgstr ""
+
msgid "Migrated %{success_count}/%{total_count} files."
msgstr ""
@@ -13483,10 +13775,10 @@ msgstr ""
msgid "Next"
msgstr ""
-msgid "Next file in diff (MRs only)"
+msgid "Next file in diff"
msgstr ""
-msgid "Next unresolved discussion (MRs only)"
+msgid "Next unresolved discussion"
msgstr ""
msgid "Nickname"
@@ -13546,6 +13838,9 @@ msgstr ""
msgid "No changes between %{ref_start}%{source_branch}%{ref_end} and %{ref_start}%{target_branch}%{ref_end}"
msgstr ""
+msgid "No child epics match applied filters"
+msgstr ""
+
msgid "No connection could be made to a Gitaly Server, please check your logs!"
msgstr ""
@@ -13684,15 +13979,6 @@ msgstr ""
msgid "No thanks, don't show this again"
msgstr ""
-msgid "No vulnerabilities found for this group"
-msgstr ""
-
-msgid "No vulnerabilities found for this pipeline"
-msgstr ""
-
-msgid "No vulnerabilities found for this project"
-msgstr ""
-
msgid "No vulnerabilities present"
msgstr ""
@@ -13816,6 +14102,9 @@ msgstr ""
msgid "Nothing to preview."
msgstr ""
+msgid "Nothing to synchronize"
+msgstr ""
+
msgid "Notification events"
msgstr ""
@@ -13987,6 +14276,9 @@ msgstr ""
msgid "Once removed, the fork relationship cannot be restored and you will no longer be able to send merge requests to the source."
msgstr ""
+msgid "Once the exported file is ready you can download it from this page."
+msgstr ""
+
msgid "Once the exported file is ready, you will receive a notification email with a download link, or you can download it from this page."
msgstr ""
@@ -14088,9 +14380,6 @@ msgstr ""
msgid "Open sidebar"
msgstr ""
-msgid "Open source software to collaborate on code"
-msgstr ""
-
msgid "Open: %{openIssuesCount}"
msgstr ""
@@ -14193,6 +14482,9 @@ msgstr ""
msgid "OutdatedBrowser|You can provide feedback %{feedback_link_start}on this issue%{feedback_link_end} or via your usual support channels."
msgstr ""
+msgid "Overridden"
+msgstr ""
+
msgid "Overview"
msgstr ""
@@ -14244,6 +14536,9 @@ msgstr ""
msgid "PackageRegistry|Conan Command"
msgstr ""
+msgid "PackageRegistry|Copy .pypirc content"
+msgstr ""
+
msgid "PackageRegistry|Copy Conan Command"
msgstr ""
@@ -14265,6 +14560,9 @@ msgstr ""
msgid "PackageRegistry|Copy NuGet Setup Command"
msgstr ""
+msgid "PackageRegistry|Copy Pip command"
+msgstr ""
+
msgid "PackageRegistry|Copy and paste this inside your %{codeStart}pom.xml%{codeEnd} %{codeStart}dependencies%{codeEnd} block."
msgstr ""
@@ -14298,6 +14596,12 @@ msgstr ""
msgid "PackageRegistry|For more information on the NuGet registry, %{linkStart}see the documentation%{linkEnd}."
msgstr ""
+msgid "PackageRegistry|For more information on the PyPi registry, %{linkStart}see the documentation%{linkEnd}."
+msgstr ""
+
+msgid "PackageRegistry|If you haven't already done so, you will need to add the below to your %{codeStart}.pypirc%{codeEnd} file."
+msgstr ""
+
msgid "PackageRegistry|If you haven't already done so, you will need to add the below to your %{codeStart}pom.xml%{codeEnd} file."
msgstr ""
@@ -14307,6 +14611,9 @@ msgstr ""
msgid "PackageRegistry|Learn how to %{noPackagesLinkStart}publish and share your packages%{noPackagesLinkEnd} with GitLab."
msgstr ""
+msgid "PackageRegistry|Manually Published"
+msgstr ""
+
msgid "PackageRegistry|Maven"
msgstr ""
@@ -14325,12 +14632,18 @@ msgstr ""
msgid "PackageRegistry|NuGet Command"
msgstr ""
+msgid "PackageRegistry|Pip Command"
+msgstr ""
+
msgid "PackageRegistry|Pipeline %{linkStart}%{linkEnd} triggered %{timestamp} by %{author}"
msgstr ""
msgid "PackageRegistry|Published to the repository at %{timestamp}"
msgstr ""
+msgid "PackageRegistry|PyPi"
+msgstr ""
+
msgid "PackageRegistry|Registry Setup"
msgstr ""
@@ -14367,6 +14680,9 @@ msgstr ""
msgid "PackageRegistry|npm"
msgstr ""
+msgid "PackageRegistry|published by %{author}"
+msgstr ""
+
msgid "PackageRegistry|yarn"
msgstr ""
@@ -14382,6 +14698,9 @@ msgstr ""
msgid "PackageType|NuGet"
msgstr ""
+msgid "PackageType|PyPi"
+msgstr ""
+
msgid "Packages"
msgstr ""
@@ -14475,6 +14794,9 @@ msgstr ""
msgid "Past due"
msgstr ""
+msgid "Paste a machine public key here. Read more about how to generate it"
+msgstr ""
+
msgid "Paste a machine public key here. Read more about how to generate it %{link_start}here%{link_end}"
msgstr ""
@@ -14490,9 +14812,6 @@ msgstr ""
msgid "Path"
msgstr ""
-msgid "Path, transfer, remove"
-msgstr ""
-
msgid "Path:"
msgstr ""
@@ -14523,7 +14842,7 @@ msgstr ""
msgid "Percentage"
msgstr ""
-msgid "Perform advanced options such as changing path, transferring, or removing the group."
+msgid "Perform advanced options such as changing path, transferring, exporting, or removing the group."
msgstr ""
msgid "Perform common operations on GitLab project"
@@ -14604,9 +14923,6 @@ msgstr ""
msgid "Pipeline minutes quota"
msgstr ""
-msgid "Pipeline quota"
-msgstr ""
-
msgid "Pipeline subscriptions"
msgstr ""
@@ -14616,6 +14932,9 @@ msgstr ""
msgid "Pipeline: %{status}"
msgstr ""
+msgid "PipelineCharts|CI / CD Analytics"
+msgstr ""
+
msgid "PipelineCharts|Failed:"
msgstr ""
@@ -14694,15 +15013,6 @@ msgstr ""
msgid "Pipelines settings for '%{project_name}' were successfully updated."
msgstr ""
-msgid "Pipelines| to purchase more minutes."
-msgstr ""
-
-msgid "Pipelines|%{namespace_name} has exceeded its pipeline minutes quota."
-msgstr ""
-
-msgid "Pipelines|%{namespace_name} has less than %{notification_level}%% of CI minutes available."
-msgstr ""
-
msgid "Pipelines|API"
msgstr ""
@@ -14724,10 +15034,13 @@ msgstr ""
msgid "Pipelines|Get started with Pipelines"
msgstr ""
-msgid "Pipelines|Loading Pipelines"
+msgid "Pipelines|Group %{namespace_name} has %{percentage}%% or less Shared Runner Pipeline minutes remaining. Once it runs out, no new jobs or pipelines in its projects will run."
msgstr ""
-msgid "Pipelines|Pipelines will not run anymore on shared Runners."
+msgid "Pipelines|Group %{namespace_name} has exceeded its pipeline minutes quota. Unless you buy additional pipeline minutes, no new jobs or pipelines in its projects will run."
+msgstr ""
+
+msgid "Pipelines|Loading Pipelines"
msgstr ""
msgid "Pipelines|Project cache successfully reset."
@@ -14958,6 +15271,9 @@ msgstr ""
msgid "Please select"
msgstr ""
+msgid "Please select a Jira project"
+msgstr ""
+
msgid "Please select a country"
msgstr ""
@@ -15003,6 +15319,9 @@ msgstr ""
msgid "Please wait while we import the repository for you. Refresh at will."
msgstr ""
+msgid "Plugins directory is deprecated and will be removed in 14.0. Please move this file into /file_hooks directory."
+msgstr ""
+
msgid "Pod does not exist"
msgstr ""
@@ -15120,7 +15439,7 @@ msgstr ""
msgid "Prevent users from changing their profile name"
msgstr ""
-msgid "Prevent users from modifing merge request approvers list"
+msgid "Prevent users from modifying merge request approvers list"
msgstr ""
msgid "Prevent users from performing write operations on GitLab while performing maintenance."
@@ -15141,10 +15460,10 @@ msgstr ""
msgid "Previous Artifacts"
msgstr ""
-msgid "Previous file in diff (MRs only)"
+msgid "Previous file in diff"
msgstr ""
-msgid "Previous unresolved discussion (MRs only)"
+msgid "Previous unresolved discussion"
msgstr ""
msgid "Primary"
@@ -15177,6 +15496,9 @@ msgstr ""
msgid "Private profile"
msgstr ""
+msgid "Private projects Minutes cost factor"
+msgstr ""
+
msgid "Private projects can be created in your personal namespace with:"
msgstr ""
@@ -16005,6 +16327,9 @@ msgstr ""
msgid "ProjectSettings|With GitLab Pages you can host your static websites on GitLab"
msgstr ""
+msgid "ProjectSettings|With Metrics Dashboard you can visualize this project performance metrics"
+msgstr ""
+
msgid "ProjectTemplates|.NET Core"
msgstr ""
@@ -16068,6 +16393,9 @@ msgstr ""
msgid "ProjectTemplates|Spring"
msgstr ""
+msgid "ProjectTemplates|Static Site Editor/Middleman"
+msgstr ""
+
msgid "ProjectTemplates|iOS (Swift)"
msgstr ""
@@ -16287,13 +16615,13 @@ msgstr ""
msgid "PrometheusService|Prometheus is being automatically managed on your clusters"
msgstr ""
-msgid "PrometheusService|These metrics will only be monitored after your first deployment to an environment"
+msgid "PrometheusService|Select the Active checkbox to override the Auto Configuration with custom settings. If unchecked, Auto Configuration settings are used."
msgstr ""
-msgid "PrometheusService|Time-series monitoring service"
+msgid "PrometheusService|These metrics will only be monitored after your first deployment to an environment"
msgstr ""
-msgid "PrometheusService|To enable manual configuration, uninstall Prometheus from your clusters"
+msgid "PrometheusService|Time-series monitoring service"
msgstr ""
msgid "PrometheusService|To enable the installation of Prometheus on your clusters, deactivate the manual configuration below"
@@ -16479,6 +16807,9 @@ msgstr ""
msgid "Protip:"
msgstr ""
+msgid "Protocol"
+msgstr ""
+
msgid "Provider"
msgstr ""
@@ -16503,6 +16834,9 @@ msgstr ""
msgid "Public pipelines"
msgstr ""
+msgid "Public projects Minutes cost factor"
+msgstr ""
+
msgid "Pull"
msgstr ""
@@ -16703,6 +17037,9 @@ msgid_plural "Refreshing in %d seconds to show the updated status..."
msgstr[0] ""
msgstr[1] ""
+msgid "Regenerate export"
+msgstr ""
+
msgid "Regenerate instance ID"
msgstr ""
@@ -17080,9 +17417,29 @@ msgstr ""
msgid "Reports|%{combinedString} and %{resolvedString}"
msgstr ""
+msgid "Reports|Accessibility scanning detected %d issue for the source branch only"
+msgid_plural "Reports|Accessibility scanning detected %d issues for the source branch only"
+msgstr[0] ""
+msgstr[1] ""
+
+msgid "Reports|Accessibility scanning detected no issues for the source branch only"
+msgstr ""
+
+msgid "Reports|Accessibility scanning failed loading results"
+msgstr ""
+
+msgid "Reports|Accessibility scanning results are being parsed"
+msgstr ""
+
msgid "Reports|Actions"
msgstr ""
+msgid "Reports|An error occured while loading report"
+msgstr ""
+
+msgid "Reports|An error occurred while loading %{name} results"
+msgstr ""
+
msgid "Reports|Class"
msgstr ""
@@ -17188,7 +17545,7 @@ msgstr ""
msgid "Requested %{time_ago}"
msgstr ""
-msgid "Requested design version does not exist"
+msgid "Requested design version does not exist."
msgstr ""
msgid "Requested states are invalid"
@@ -17372,6 +17729,9 @@ msgstr ""
msgid "Resync"
msgstr ""
+msgid "Resync all"
+msgstr ""
+
msgid "Resync all %{replicableType}"
msgstr ""
@@ -17440,6 +17800,9 @@ msgstr ""
msgid "Revoked personal access token %{personal_access_token_name}!"
msgstr ""
+msgid "Revoked project access token %{project_access_token_name}!"
+msgstr ""
+
msgid "RightSidebar|adding a"
msgstr ""
@@ -17488,6 +17851,9 @@ msgstr ""
msgid "Runner token"
msgstr ""
+msgid "Runner tokens"
+msgstr ""
+
msgid "Runner was not updated."
msgstr ""
@@ -17542,6 +17908,9 @@ msgstr ""
msgid "SAML SSO for %{group_name}"
msgstr ""
+msgid "SAML discovery tokens"
+msgstr ""
+
msgid "SAML for %{group_name}"
msgstr ""
@@ -17626,12 +17995,18 @@ msgstr ""
msgid "Scheduled"
msgstr ""
+msgid "Scheduled to merge this merge request (%{strategy})."
+msgstr ""
+
msgid "Scheduled to merge this merge request when the pipeline succeeds."
msgstr ""
msgid "Schedules"
msgstr ""
+msgid "Schedules to merge this merge request (%{strategy})."
+msgstr ""
+
msgid "Scheduling"
msgstr ""
@@ -17689,6 +18064,9 @@ msgstr ""
msgid "Search branches and tags"
msgstr ""
+msgid "Search by author"
+msgstr ""
+
msgid "Search files"
msgstr ""
@@ -17834,11 +18212,6 @@ msgid_plural "SearchResults|snippets"
msgstr[0] ""
msgstr[1] ""
-msgid "SearchResults|snippet result"
-msgid_plural "SearchResults|snippet results"
-msgstr[0] ""
-msgstr[1] ""
-
msgid "SearchResults|user"
msgid_plural "SearchResults|users"
msgstr[0] ""
@@ -17849,6 +18222,9 @@ msgid_plural "SearchResults|wiki results"
msgstr[0] ""
msgstr[1] ""
+msgid "Searching by both author and message is currently not supported."
+msgstr ""
+
msgid "Seat Link"
msgstr ""
@@ -17876,184 +18252,232 @@ msgstr ""
msgid "Security Dashboard"
msgstr ""
-msgid "Security Dashboard|Error fetching the vulnerability counts. Please check your network connection and try again."
+msgid "Security configuration help link"
+msgstr ""
+
+msgid "Security dashboard"
msgstr ""
-msgid "Security Dashboard|Error fetching the vulnerability list. Please check your network connection and try again."
+msgid "Security report is out of date. Please update your branch with the latest changes from the target branch (%{targetBranchName})"
msgstr ""
-msgid "Security Dashboard|Issue Created"
+msgid "Security report is out of date. Run %{newPipelineLinkStart}a new pipeline%{newPipelineLinkEnd} for the target branch (%{targetBranchName})"
msgstr ""
-msgid "Security Reports|Comment added to '%{vulnerabilityName}'"
+msgid "SecurityConfiguration|Configured"
msgstr ""
-msgid "Security Reports|Comment deleted on '%{vulnerabilityName}'"
+msgid "SecurityConfiguration|Feature"
msgstr ""
-msgid "Security Reports|Comment edited on '%{vulnerabilityName}'"
+msgid "SecurityConfiguration|Feature documentation for %{featureName}"
msgstr ""
-msgid "Security Reports|Create issue"
+msgid "SecurityConfiguration|Not yet configured"
msgstr ""
-msgid "Security Reports|Dismiss vulnerability"
+msgid "SecurityConfiguration|Secure features"
msgstr ""
-msgid "Security Reports|Dismissed '%{vulnerabilityName}'"
+msgid "SecurityConfiguration|Status"
msgstr ""
-msgid "Security Reports|Dismissed '%{vulnerabilityName}'. Turn off the hide dismissed toggle to view."
+msgid "SecurityReports|%{firstProject} and %{secondProject}"
msgstr ""
-msgid "Security Reports|Either you don't have permission to view this dashboard or the dashboard has not been setup. Please check your permission settings with your administrator or check your dashboard configurations to proceed."
+msgid "SecurityReports|%{firstProject}, %{secondProject}, and %{rest}"
msgstr ""
-msgid "Security Reports|Learn more about setting up your dashboard"
+msgid "SecurityReports|Add a project to your dashboard"
msgstr ""
-msgid "Security Reports|More info"
+msgid "SecurityReports|Add or remove projects from your dashboard"
msgstr ""
-msgid "Security Reports|Oops, something doesn't seem right."
+msgid "SecurityReports|Add projects"
msgstr ""
-msgid "Security Reports|Security reports can only be accessed by authorized users."
+msgid "SecurityReports|Comment added to '%{vulnerabilityName}'"
msgstr ""
-msgid "Security Reports|There was an error adding the comment."
+msgid "SecurityReports|Comment deleted on '%{vulnerabilityName}'"
msgstr ""
-msgid "Security Reports|There was an error creating the issue."
+msgid "SecurityReports|Comment edited on '%{vulnerabilityName}'"
msgstr ""
-msgid "Security Reports|There was an error creating the merge request."
+msgid "SecurityReports|Create issue"
msgstr ""
-msgid "Security Reports|There was an error deleting the comment."
+msgid "SecurityReports|Dismiss Selected"
msgstr ""
-msgid "Security Reports|There was an error dismissing the vulnerabilities."
+msgid "SecurityReports|Dismiss vulnerability"
msgstr ""
-msgid "Security Reports|There was an error dismissing the vulnerability."
+msgid "SecurityReports|Dismissed '%{vulnerabilityName}'"
msgstr ""
-msgid "Security Reports|There was an error reverting the dismissal."
+msgid "SecurityReports|Dismissed '%{vulnerabilityName}'. Turn off the hide dismissed toggle to view."
msgstr ""
-msgid "Security Reports|There was an error reverting this dismissal."
+msgid "SecurityReports|Each vulnerability now has a unique page that can be directly linked to, shared, referenced, and tracked as the single source of truth. Vulnerability occurrences also persist across scanner runs, which improves tracking and visibility and reduces duplicates between scans."
msgstr ""
-msgid "Security Reports|Undo dismiss"
+msgid "SecurityReports|Edit dashboard"
msgstr ""
-msgid "Security Reports|You do not have sufficient permissions to access this report"
+msgid "SecurityReports|Either you don't have permission to view this dashboard or the dashboard has not been setup. Please check your permission settings with your administrator or check your dashboard configurations to proceed."
msgstr ""
-msgid "Security Reports|You must sign in as an authorized user to see this report"
+msgid "SecurityReports|Error fetching the vulnerability counts. Please check your network connection and try again."
msgstr ""
-msgid "Security configuration help link"
+msgid "SecurityReports|Error fetching the vulnerability list. Please check your network connection and try again."
msgstr ""
-msgid "Security dashboard"
+msgid "SecurityReports|False positive"
msgstr ""
-msgid "Security report is out of date. Please update your branch with the latest changes from the target branch (%{targetBranchName})"
+msgid "SecurityReports|Hide dismissed"
msgstr ""
-msgid "Security report is out of date. Run %{newPipelineLinkStart}a new pipeline%{newPipelineLinkEnd} for the target branch (%{targetBranchName})"
+msgid "SecurityReports|Introducing standalone vulnerabilities"
msgstr ""
-msgid "SecurityConfiguration|Configured"
+msgid "SecurityReports|Issue Created"
msgstr ""
-msgid "SecurityConfiguration|Feature"
+msgid "SecurityReports|Learn More"
msgstr ""
-msgid "SecurityConfiguration|Feature documentation for %{featureName}"
+msgid "SecurityReports|Learn more about setting up your dashboard"
msgstr ""
-msgid "SecurityConfiguration|Not yet configured"
+msgid "SecurityReports|Load more vulnerabilities"
msgstr ""
-msgid "SecurityConfiguration|Secure features"
+msgid "SecurityReports|Monitor vulnerabilities in your code"
msgstr ""
-msgid "SecurityConfiguration|Status"
+msgid "SecurityReports|More info"
msgstr ""
-msgid "SecurityDashboard|%{firstProject} and %{secondProject}"
+msgid "SecurityReports|More information"
msgstr ""
-msgid "SecurityDashboard|%{firstProject}, %{secondProject}, and %{rest}"
+msgid "SecurityReports|No vulnerabilities found for dashboard"
msgstr ""
-msgid "SecurityDashboard|Add a project to your dashboard"
+msgid "SecurityReports|No vulnerabilities found for this group"
msgstr ""
-msgid "SecurityDashboard|Add or remove projects from your dashboard"
+msgid "SecurityReports|No vulnerabilities found for this pipeline"
msgstr ""
-msgid "SecurityDashboard|Add projects"
+msgid "SecurityReports|No vulnerabilities found for this project"
msgstr ""
-msgid "SecurityDashboard|Edit dashboard"
+msgid "SecurityReports|Oops, something doesn't seem right."
msgstr ""
-msgid "SecurityDashboard|Hide dismissed"
+msgid "SecurityReports|Pipeline %{pipelineLink} triggered %{timeago} by %{user}"
msgstr ""
-msgid "SecurityDashboard|Monitor vulnerabilities in your code"
+msgid "SecurityReports|Project"
msgstr ""
-msgid "SecurityDashboard|More information"
+msgid "SecurityReports|Projects added"
msgstr ""
-msgid "SecurityDashboard|No vulnerabilities found for dashboard"
+msgid "SecurityReports|Remove project from dashboard"
msgstr ""
-msgid "SecurityDashboard|Pipeline %{pipelineLink} triggered %{timeago} by %{user}"
+msgid "SecurityReports|Report type"
msgstr ""
-msgid "SecurityDashboard|Project"
+msgid "SecurityReports|Return to dashboard"
msgstr ""
-msgid "SecurityDashboard|Projects added"
+msgid "SecurityReports|Security Dashboard"
msgstr ""
-msgid "SecurityDashboard|Remove project from dashboard"
+msgid "SecurityReports|Security reports can only be accessed by authorized users."
msgstr ""
-msgid "SecurityDashboard|Report type"
+msgid "SecurityReports|Select a project to add by using the project search field above."
msgstr ""
-msgid "SecurityDashboard|Return to dashboard"
+msgid "SecurityReports|Select a reason"
msgstr ""
-msgid "SecurityDashboard|Security Dashboard"
+msgid "SecurityReports|Severity"
msgstr ""
-msgid "SecurityDashboard|Select a project to add by using the project search field above."
+msgid "SecurityReports|Status"
msgstr ""
-msgid "SecurityDashboard|Severity"
+msgid "SecurityReports|The security dashboard displays the latest security findings for projects you wish to monitor. Select \"Edit dashboard\" to add and remove projects."
msgstr ""
-msgid "SecurityDashboard|Status"
+msgid "SecurityReports|The security dashboard displays the latest security report. Use it to find and fix vulnerabilities."
msgstr ""
-msgid "SecurityDashboard|The security dashboard displays the latest security findings for projects you wish to monitor. Select \"Edit dashboard\" to add and remove projects."
+msgid "SecurityReports|There was an error adding the comment."
msgstr ""
-msgid "SecurityDashboard|The security dashboard displays the latest security report. Use it to find and fix vulnerabilities."
+msgid "SecurityReports|There was an error creating the issue."
msgstr ""
-msgid "SecurityDashboard|There was an error while generating the report."
+msgid "SecurityReports|There was an error creating the merge request."
msgstr ""
-msgid "SecurityDashboard|Unable to add %{invalidProjects}"
+msgid "SecurityReports|There was an error deleting the comment."
+msgstr ""
+
+msgid "SecurityReports|There was an error dismissing the vulnerabilities."
+msgstr ""
+
+msgid "SecurityReports|There was an error dismissing the vulnerability."
+msgstr ""
+
+msgid "SecurityReports|There was an error reverting the dismissal."
+msgstr ""
+
+msgid "SecurityReports|There was an error reverting this dismissal."
+msgstr ""
+
+msgid "SecurityReports|There was an error while generating the report."
+msgstr ""
+
+msgid "SecurityReports|Unable to add %{invalidProjects}"
+msgstr ""
+
+msgid "SecurityReports|Undo dismiss"
+msgstr ""
+
+msgid "SecurityReports|While it's rare to have no vulnerabilities for your group, it can happen. In any event, we ask that you double check your settings to make sure you've set up your dashboard correctly."
+msgstr ""
+
+msgid "SecurityReports|While it's rare to have no vulnerabilities for your pipeline, it can happen. In any event, we ask that you double check your settings to make sure all security scanning jobs have passed successfully."
+msgstr ""
+
+msgid "SecurityReports|While it's rare to have no vulnerabilities for your project, it can happen. In any event, we ask that you double check your settings to make sure you've set up your dashboard correctly."
+msgstr ""
+
+msgid "SecurityReports|While it's rare to have no vulnerabilities, it can happen. In any event, we ask that you please double check your settings to make sure you've set up your dashboard correctly."
+msgstr ""
+
+msgid "SecurityReports|Won't fix / Accept risk"
+msgstr ""
+
+msgid "SecurityReports|You do not have sufficient permissions to access this report"
+msgstr ""
+
+msgid "SecurityReports|You must sign in as an authorized user to see this report"
+msgstr ""
+
+msgid "SecurityReports|[No reason]"
msgstr ""
msgid "See GitLab's %{password_policy_guidelines}"
@@ -18065,6 +18489,9 @@ msgstr ""
msgid "See the affected projects in the GitLab admin panel"
msgstr ""
+msgid "See what's new at GitLab"
+msgstr ""
+
msgid "Select"
msgstr ""
@@ -18470,9 +18897,6 @@ msgstr ""
msgid "Set up Jira Integration"
msgstr ""
-msgid "Set up Jira Integration illustration"
-msgstr ""
-
msgid "Set up a %{type} Runner automatically"
msgstr ""
@@ -18557,6 +18981,9 @@ msgstr ""
msgid "Severity: %{severity}"
msgstr ""
+msgid "Shards (%{shards})"
+msgstr ""
+
msgid "Shards to synchronize"
msgstr ""
@@ -18871,6 +19298,9 @@ msgstr ""
msgid "Snowplow"
msgstr ""
+msgid "Some child epics may be hidden due to applied filters"
+msgstr ""
+
msgid "Some email servers do not support overriding the email sender name. Enable this option to include the name of the author of the issue, merge request or comment in the email body instead."
msgstr ""
@@ -19012,6 +19442,9 @@ msgstr ""
msgid "Something went wrong while updating your list settings"
msgstr ""
+msgid "Something went wrong with your automatic subscription renewal"
+msgstr ""
+
msgid "Something went wrong, unable to add %{project} to dashboard"
msgstr ""
@@ -19297,6 +19730,9 @@ msgstr ""
msgid "Stage removed"
msgstr ""
+msgid "Standard"
+msgstr ""
+
msgid "Star a label to make it a priority label. Order the prioritized labels to change their relative priority, by dragging."
msgstr ""
@@ -19420,6 +19856,9 @@ msgstr ""
msgid "Static Application Security Testing (SAST)"
msgstr ""
+msgid "StaticSiteEditor|An error occurred while submitting your changes."
+msgstr ""
+
msgid "StaticSiteEditor|Branch could not be created."
msgstr ""
@@ -19429,18 +19868,30 @@ msgstr ""
msgid "StaticSiteEditor|Could not create merge request."
msgstr ""
+msgid "StaticSiteEditor|Incompatible file content"
+msgstr ""
+
msgid "StaticSiteEditor|Return to site"
msgstr ""
+msgid "StaticSiteEditor|Static site editor"
+msgstr ""
+
msgid "StaticSiteEditor|Success!"
msgstr ""
msgid "StaticSiteEditor|Summary of changes"
msgstr ""
+msgid "StaticSiteEditor|The Static Site Editor is currently configured to only edit Markdown content on pages generated from Middleman. Visit the documentation to learn more about configuring your site to use the Static Site Editor."
+msgstr ""
+
msgid "StaticSiteEditor|Update %{sourcePath} file"
msgstr ""
+msgid "StaticSiteEditor|View documentation"
+msgstr ""
+
msgid "StaticSiteEditor|View merge request"
msgstr ""
@@ -19498,6 +19949,12 @@ msgstr ""
msgid "StatusPage|Status page"
msgstr ""
+msgid "StatusPage|Status page URL"
+msgstr ""
+
+msgid "StatusPage|Status page frontend documentation"
+msgstr ""
+
msgid "StatusPage|To publish incidents to an external status page, GitLab will store a JSON file in your Amazon S3 account in a location accessible to your external status page service. Make sure to also set up %{docsLink}"
msgstr ""
@@ -19846,6 +20303,12 @@ msgstr ""
msgid "Synced"
msgstr ""
+msgid "Synchronization disabled"
+msgstr ""
+
+msgid "Synchronization of container repositories is disabled."
+msgstr ""
+
msgid "System"
msgstr ""
@@ -19999,6 +20462,9 @@ msgstr ""
msgid "Telephone number"
msgstr ""
+msgid "Telephone number (Optional)"
+msgstr ""
+
msgid "Template"
msgstr ""
@@ -20305,6 +20771,9 @@ msgstr ""
msgid "The license was successfully uploaded and is now active. You can see the details below."
msgstr ""
+msgid "The license was successfully uploaded and will be active from %{starts_at}. You can see the details below."
+msgstr ""
+
msgid "The maximum file size allowed is %{size}."
msgstr ""
@@ -20626,6 +21095,9 @@ msgstr ""
msgid "There was an error getting the epic participants."
msgstr ""
+msgid "There was an error importing the Jira project."
+msgstr ""
+
msgid "There was an error loading users activity calendar."
msgstr ""
@@ -20641,7 +21113,7 @@ msgstr ""
msgid "There was an error resetting user pipeline minutes."
msgstr ""
-msgid "There was an error saving this Geo Node"
+msgid "There was an error saving this Geo Node."
msgstr ""
msgid "There was an error saving your changes."
@@ -20704,6 +21176,9 @@ msgstr ""
msgid "They can be managed using the %{link}."
msgstr ""
+msgid "Third Party Advisory Link"
+msgstr ""
+
msgid "Third party offers"
msgstr ""
@@ -20740,6 +21215,9 @@ msgstr ""
msgid "This application will be able to:"
msgstr ""
+msgid "This attachment has been truncated to avoid exceeding the maximum allowed attachment size of 15MB. %{written_count} of %{issues_count} issues have been included. Consider re-exporting with a narrower selection of issues."
+msgstr ""
+
msgid "This block is self-referential"
msgstr ""
@@ -20881,6 +21359,12 @@ msgstr ""
msgid "This issue is confidential"
msgstr ""
+msgid "This issue is currently blocked by the following issues: %{issues}."
+msgstr ""
+
+msgid "This issue is in a child epic of the filtered epic"
+msgstr ""
+
msgid "This issue is locked."
msgstr ""
@@ -21079,7 +21563,10 @@ msgstr ""
msgid "This user will be the author of all events in the activity feed that are the result of an update, like new branches being created or new commits being pushed to existing branches. Upon creation or when reassigning you can only assign yourself to be the mirror user."
msgstr ""
-msgid "This variable can not be masked"
+msgid "This variable can not be masked."
+msgstr ""
+
+msgid "This variable does not match the expected pattern."
msgstr ""
msgid "This will help us personalize your onboarding experience."
@@ -21109,10 +21596,10 @@ msgstr ""
msgid "ThreatMonitoring|Application firewall not detected"
msgstr ""
-msgid "ThreatMonitoring|Container Network Policy"
+msgid "ThreatMonitoring|Container Network Policies are not installed or have been disabled. To view this data, ensure your Network Policies are installed and enabled for your cluster."
msgstr ""
-msgid "ThreatMonitoring|Container NetworkPolicies are not installed or has been disabled. To view this data, ensure you NetworkPolicies are installed and enabled for your cluster."
+msgid "ThreatMonitoring|Container Network Policy"
msgstr ""
msgid "ThreatMonitoring|Container NetworkPolicies not detected"
@@ -21145,7 +21632,7 @@ msgstr ""
msgid "ThreatMonitoring|Something went wrong, unable to fetch statistics"
msgstr ""
-msgid "ThreatMonitoring|The firewall is not installed or has been disabled. To view this data, ensure you firewall is installed and enabled for your cluster."
+msgid "ThreatMonitoring|The firewall is not installed or has been disabled. To view this data, ensure the web application firewall is installed and enabled for your cluster."
msgstr ""
msgid "ThreatMonitoring|The graph below is an overview of traffic coming to your application as tracked by the Web Application Firewall (WAF). View the docs for instructions on how to access the WAF logs to see what type of malicious traffic is trying to access your app. The docs link is also accessible by clicking the \"?\" icon next to the title below."
@@ -21214,6 +21701,9 @@ msgstr ""
msgid "Time in seconds GitLab will wait for a response from the external service. When the service does not respond in time, access will be denied."
msgstr ""
+msgid "Time of import: %{importTime}"
+msgstr ""
+
msgid "Time remaining"
msgstr ""
@@ -21398,7 +21888,7 @@ msgstr ""
msgid "Title:"
msgstr ""
-msgid "Titles and Filenames"
+msgid "Titles and Descriptions"
msgstr ""
msgid "To"
@@ -21605,7 +22095,7 @@ msgstr ""
msgid "Too many projects enabled. You will need to manage them via the console or the API."
msgstr ""
-msgid "Topics"
+msgid "Topics (optional)"
msgstr ""
msgid "Total"
@@ -21767,6 +22257,9 @@ msgstr ""
msgid "Try all GitLab has to offer for 30 days."
msgstr ""
+msgid "Try changing or removing filters."
+msgstr ""
+
msgid "Try to fork again"
msgstr ""
@@ -21860,6 +22353,9 @@ msgstr ""
msgid "Unable to connect to server: %{error}"
msgstr ""
+msgid "Unable to connect to the Jira instance. Please check your Jira integration configuration."
+msgstr ""
+
msgid "Unable to convert Kubernetes logs encoding to UTF-8"
msgstr ""
@@ -22803,6 +23299,9 @@ msgstr ""
msgid "View issue"
msgstr ""
+msgid "View issues"
+msgstr ""
+
msgid "View it on GitLab"
msgstr ""
@@ -22980,6 +23479,15 @@ msgstr ""
msgid "VulnerabilityManagement|Resolved %{timeago} by %{user}"
msgstr ""
+msgid "VulnerabilityManagement|Something went wrong while trying to delete the comment. Please try again later."
+msgstr ""
+
+msgid "VulnerabilityManagement|Something went wrong while trying to retrieve the vulnerability history. Please try again later."
+msgstr ""
+
+msgid "VulnerabilityManagement|Something went wrong while trying to save the comment. Please try again later."
+msgstr ""
+
msgid "VulnerabilityManagement|Something went wrong, could not create an issue."
msgstr ""
@@ -23010,6 +23518,9 @@ msgstr ""
msgid "VulnerabilityStatusTypes|Resolved"
msgstr ""
+msgid "Vulnerability|%{scannerName} (version %{scannerVersion})"
+msgstr ""
+
msgid "Vulnerability|Class"
msgstr ""
@@ -23043,7 +23554,10 @@ msgstr ""
msgid "Vulnerability|Project"
msgstr ""
-msgid "Vulnerability|Report Type"
+msgid "Vulnerability|Scanner Provider"
+msgstr ""
+
+msgid "Vulnerability|Scanner Type"
msgstr ""
msgid "Vulnerability|Severity"
@@ -23097,9 +23611,15 @@ msgstr ""
msgid "We sent you an email with reset password instructions"
msgstr ""
+msgid "We tried to automatically renew your %{strong}%{plan_name}%{strong_close} subscription for %{strong}%{namespace_name}%{strong_close} on %{expires_on} but something went wrong so your subscription was downgraded to the free plan. Don't worry, your data is safe. We suggest you check your payment method and get in touch with our support team (%{support_link}). They'll gladly help with your subscription renewal."
+msgstr ""
+
msgid "We want to be sure it is you, please confirm you are not a robot."
msgstr ""
+msgid "We will automatically renew your %{strong}%{plan_name}%{strong_close} subscription for %{strong}%{namespace_name}%{strong_close} on %{strong}%{expires_on}%{strong_close}. There's nothing that you need to do, we'll let you know when the renewal is complete. Need more seats, a higher plan or just want to review your payment method?"
+msgstr ""
+
msgid "We've found no vulnerabilities"
msgstr ""
@@ -23204,15 +23724,6 @@ msgstr ""
msgid "When:"
msgstr ""
-msgid "While it's rare to have no vulnerabilities for your group, it can happen. In any event, we ask that you double check your settings to make sure you've set up your dashboard correctly."
-msgstr ""
-
-msgid "While it's rare to have no vulnerabilities for your pipeline, it can happen. In any event, we ask that you double check your settings to make sure all security scanning jobs have passed successfully."
-msgstr ""
-
-msgid "While it's rare to have no vulnerabilities for your project, it can happen. In any event, we ask that you double check your settings to make sure you've set up your dashboard correctly."
-msgstr ""
-
msgid "While it's rare to have no vulnerabilities, it can happen. In any event, we ask that you please double check your settings to make sure you've set up your dashboard correctly."
msgstr ""
@@ -23243,9 +23754,6 @@ msgstr ""
msgid "Wiki"
msgstr ""
-msgid "Wiki pages"
-msgstr ""
-
msgid "Wiki was successfully updated."
msgstr ""
@@ -23429,6 +23937,9 @@ msgstr ""
msgid "Yes, add it"
msgstr ""
+msgid "Yes, close issue"
+msgstr ""
+
msgid "Yes, let me map Google Code users to full names or GitLab users."
msgstr ""
@@ -23624,6 +24135,9 @@ msgstr ""
msgid "You could not create a new trigger."
msgstr ""
+msgid "You didn't renew your %{strong}%{plan_name}%{strong_close} subscription for %{strong}%{namespace_name}%{strong_close} so it was downgraded to the free plan."
+msgstr ""
+
msgid "You didn't renew your %{strong}%{plan_name}%{strong_close} subscription so it was downgraded to the GitLab Core Plan."
msgstr ""
@@ -23690,6 +24204,9 @@ msgstr ""
msgid "You have declined the invitation to join %{label}."
msgstr ""
+msgid "You have imported from this project %{numberOfPreviousImportsForProject} times before. Each new import will create duplicate issues."
+msgstr ""
+
msgid "You have no permissions"
msgstr ""
@@ -23771,6 +24288,9 @@ msgstr ""
msgid "You need to upload a Google Takeout archive."
msgstr ""
+msgid "You reached %{usage_in_percent} of %{namespace_name}'s capacity (%{used_storage} of %{storage_limit})"
+msgstr ""
+
msgid "You tried to fork %{link_to_the_project} but it failed for the following reason:"
msgstr ""
@@ -23855,9 +24375,21 @@ msgstr ""
msgid "YouTube"
msgstr ""
+msgid "Your %{strong}%{plan_name}%{strong_close} subscription for %{strong}%{namespace_name}%{strong_close} will expire on %{strong}%{expires_on}%{strong_close}. After that, you will not to be able to create issues or merge requests as well as many other features."
+msgstr ""
+
msgid "Your %{strong}%{plan_name}%{strong_close} subscription will expire on %{strong}%{expires_on}%{strong_close}. After that, you will not to be able to create issues or merge requests as well as many other features."
msgstr ""
+msgid "Your CSV export has started. It will be emailed to %{email} when complete."
+msgstr ""
+
+msgid "Your CSV export of %{issues_count} from project %{project_link} has been added to this email as an attachment."
+msgstr ""
+
+msgid "Your CSV export of %{written_count} from project %{project_name} (%{project_url}) has been added to this email as an attachment."
+msgstr ""
+
msgid "Your Commit Email will be used for web based operations, such as edits and merges."
msgstr ""
@@ -23954,6 +24486,9 @@ msgstr ""
msgid "Your comment could not be updated! Please check your network connection and try again."
msgstr ""
+msgid "Your comment will be discarded."
+msgstr ""
+
msgid "Your custom stage '%{title}' was created"
msgstr ""
@@ -23993,6 +24528,9 @@ msgstr ""
msgid "Your new personal access token has been created."
msgstr ""
+msgid "Your new project access token has been created."
+msgstr ""
+
msgid "Your password isn't required to view this page. If a password or any other personal details are requested, please contact your administrator to report abuse."
msgstr ""
@@ -24011,12 +24549,18 @@ msgstr ""
msgid "Your request for access has been queued for review."
msgstr ""
+msgid "Your search didn't match any commits."
+msgstr ""
+
msgid "Your subscription expired!"
msgstr ""
msgid "Your subscription has been downgraded"
msgstr ""
+msgid "Your subscription will automatically renew in %{remaining_days}"
+msgstr ""
+
msgid "Your subscription will expire in %{remaining_days}"
msgstr ""
@@ -24055,7 +24599,7 @@ msgstr ""
msgid "allowed to fail"
msgstr ""
-msgid "already being used for another group or project milestone."
+msgid "already being used for another group or project %{timebox_name}."
msgstr ""
msgid "already has a \"created\" issue link"
@@ -24133,6 +24677,9 @@ msgstr ""
msgid "ciReport|%{linkStartTag}Learn more about SAST %{linkEndTag}"
msgstr ""
+msgid "ciReport|%{linkStartTag}Learn more about Secret Scanning %{linkEndTag}"
+msgstr ""
+
msgid "ciReport|%{linkStartTag}Learn more about codequality reports %{linkEndTag}"
msgstr ""
@@ -24296,6 +24843,12 @@ msgstr ""
msgid "ciReport|SAST"
msgstr ""
+msgid "ciReport|Secret scanning"
+msgstr ""
+
+msgid "ciReport|Secret scanning detects secrets and credentials vulnerabilities in your source code."
+msgstr ""
+
msgid "ciReport|Security scanning"
msgstr ""
@@ -24493,6 +25046,9 @@ msgstr ""
msgid "group"
msgstr ""
+msgid "groups"
+msgstr ""
+
msgid "has already been linked to another vulnerability"
msgstr ""
@@ -24656,9 +25212,6 @@ msgstr[1] ""
msgid "merged %{time_ago}"
msgstr ""
-msgid "milestone should belong either to a project or a group."
-msgstr ""
-
msgid "missing"
msgstr ""
@@ -24995,6 +25548,9 @@ msgstr ""
msgid "no contributions"
msgstr ""
+msgid "no expiration"
+msgstr ""
+
msgid "no one can merge"
msgstr ""
@@ -25047,6 +25603,9 @@ msgstr ""
msgid "pending removal"
msgstr ""
+msgid "per day"
+msgstr ""
+
msgid "pipeline"
msgstr ""
@@ -25075,6 +25634,12 @@ msgstr[1] ""
msgid "project avatar"
msgstr ""
+msgid "projects"
+msgstr ""
+
+msgid "push to your repository, create pipelines, create issues or add comments. To reduce storage capacity, delete unused repositories, artifacts, wikis, issues, and pipelines."
+msgstr ""
+
msgid "quick actions"
msgstr ""
@@ -25221,11 +25786,6 @@ msgstr ""
msgid "this document"
msgstr ""
-msgid "thread resolved"
-msgid_plural "threads resolved"
-msgstr[0] ""
-msgstr[1] ""
-
msgid "to help your contributors communicate effectively!"
msgstr ""
@@ -25280,6 +25840,9 @@ msgstr ""
msgid "view the blob"
msgstr ""
+msgid "vulnerability|Add a comment"
+msgstr ""
+
msgid "vulnerability|Add a comment or reason for dismissal"
msgstr ""
diff --git a/locale/bs_BA/gitlab.po b/locale/bs_BA/gitlab.po
index cd9940a7fab..69c02aa8f3f 100644
--- a/locale/bs_BA/gitlab.po
+++ b/locale/bs_BA/gitlab.po
@@ -12,7 +12,7 @@ msgstr ""
"X-Crowdin-Project: gitlab-ee\n"
"X-Crowdin-Language: bs\n"
"X-Crowdin-File: /master/locale/gitlab.pot\n"
-"PO-Revision-Date: 2020-04-15 00:29\n"
+"PO-Revision-Date: 2020-05-05 21:33\n"
msgid " %{start} to %{end}"
msgstr ""
@@ -202,9 +202,9 @@ msgstr[2] ""
msgid "%d minute"
msgid_plural "%d minutes"
-msgstr[0] ""
-msgstr[1] ""
-msgstr[2] ""
+msgstr[0] "%d minuta"
+msgstr[1] "%d minute"
+msgstr[2] "%d minuta"
msgid "%d more comment"
msgid_plural "%d more comments"
@@ -226,9 +226,9 @@ msgstr[2] ""
msgid "%d second"
msgid_plural "%d seconds"
-msgstr[0] ""
-msgstr[1] ""
-msgstr[2] ""
+msgstr[0] "%d sekunda"
+msgstr[1] "%d sekunde"
+msgstr[2] "%d sekundi"
msgid "%d shard selected"
msgid_plural "%d shards selected"
@@ -242,6 +242,12 @@ msgstr[0] ""
msgstr[1] ""
msgstr[2] ""
+msgid "%d unresolved thread"
+msgid_plural "%d unresolved threads"
+msgstr[0] ""
+msgstr[1] ""
+msgstr[2] ""
+
msgid "%d vulnerability dismissed"
msgid_plural "%d vulnerabilities dismissed"
msgstr[0] ""
@@ -407,6 +413,9 @@ msgstr ""
msgid "%{mrText}, this issue will be closed automatically."
msgstr ""
+msgid "%{namespace_name} is now read-only. You cannot: %{base_message}"
+msgstr ""
+
msgid "%{name} contained %{resultsString}"
msgstr ""
@@ -419,6 +428,12 @@ msgstr ""
msgid "%{name}'s avatar"
msgstr ""
+msgid "%{no_of_days} day"
+msgid_plural "%{no_of_days} days"
+msgstr[0] ""
+msgstr[1] ""
+msgstr[2] ""
+
msgid "%{number_commits_behind} commits behind %{default_branch}, %{number_commits_ahead} commits ahead"
msgstr ""
@@ -548,6 +563,9 @@ msgstr[2] ""
msgid "%{text} is available"
msgstr ""
+msgid "%{timebox_name} should belong either to a project or a group."
+msgstr ""
+
msgid "%{title} %{operator} %{threshold}"
msgstr ""
@@ -697,9 +715,9 @@ msgstr[2] ""
msgid "1 Day"
msgid_plural "%d Days"
-msgstr[0] ""
-msgstr[1] ""
-msgstr[2] ""
+msgstr[0] "1 dan"
+msgstr[1] "%d dana"
+msgstr[2] "%d dana"
msgid "1 closed issue"
msgid_plural "%{issues} closed issues"
@@ -715,9 +733,9 @@ msgstr[2] ""
msgid "1 day"
msgid_plural "%d days"
-msgstr[0] ""
-msgstr[1] ""
-msgstr[2] ""
+msgstr[0] "1 dan"
+msgstr[1] "%d dana"
+msgstr[2] "%d dana"
msgid "1 group"
msgid_plural "%d groups"
@@ -739,9 +757,9 @@ msgstr[2] ""
msgid "1 minute"
msgid_plural "%d minutes"
-msgstr[0] ""
-msgstr[1] ""
-msgstr[2] ""
+msgstr[0] "1 minuta"
+msgstr[1] "%d minute"
+msgstr[2] "%d minuta"
msgid "1 month"
msgstr ""
@@ -777,7 +795,7 @@ msgstr[1] ""
msgstr[2] ""
msgid "1 week"
-msgstr ""
+msgstr "1 sedmica"
msgid "1-9 contributions"
msgstr ""
@@ -798,13 +816,13 @@ msgid "2FADevice|Registered On"
msgstr ""
msgid "3 days"
-msgstr ""
+msgstr "3 dana"
msgid "3 hours"
msgstr ""
msgid "30 minutes"
-msgstr ""
+msgstr "30 minuta"
msgid "30+ contributions"
msgstr ""
@@ -842,12 +860,18 @@ msgstr ""
msgid "<code>\"johnsmith@example.com\": \"johnsmith@example.com\"</code> will add \"By <a href=\"#\">johnsmith@example.com</a>\" to all issues and comments originally created by johnsmith@example.com. By default, the email address or username is masked to ensure the user's privacy. Use this option if you want to show the full email address."
msgstr ""
+msgid "<namespace / project>"
+msgstr ""
+
msgid "<no name set>"
msgstr ""
msgid "<no scopes selected>"
msgstr ""
+msgid "<project name>"
+msgstr ""
+
msgid "<strong>%{group_name}</strong> group members"
msgstr ""
@@ -881,6 +905,9 @@ msgstr ""
msgid "A basic page and serverless function that uses AWS Lambda, AWS API Gateway, and GitLab Pages"
msgstr ""
+msgid "A complete DevOps platform"
+msgstr ""
+
msgid "A default branch cannot be chosen for an empty project."
msgstr ""
@@ -1097,6 +1124,12 @@ msgstr ""
msgid "AccessTokens|reset it"
msgstr ""
+msgid "AccessibilityReport|Accessibility report artifact not found"
+msgstr ""
+
+msgid "AccessibilityReport|Failed to retrieve accessibility report"
+msgstr ""
+
msgid "AccessibilityReport|Learn More"
msgstr ""
@@ -1106,6 +1139,9 @@ msgstr ""
msgid "AccessibilityReport|New"
msgstr ""
+msgid "AccessibilityReport|The accessibility scanning found an error of the following type: %{code}"
+msgstr ""
+
msgid "Account"
msgstr ""
@@ -1349,6 +1385,9 @@ msgstr ""
msgid "Added at"
msgstr ""
+msgid "Added for this merge request"
+msgstr ""
+
msgid "Added in this version"
msgstr ""
@@ -1356,7 +1395,7 @@ msgid "Adding new applications is disabled in your GitLab instance. Please conta
msgstr ""
msgid "Additional minutes"
-msgstr ""
+msgstr "Dodatne minute"
msgid "Additional text"
msgstr ""
@@ -1721,6 +1760,72 @@ msgstr[0] ""
msgstr[1] ""
msgstr[2] ""
+msgid "Alert Details"
+msgstr ""
+
+msgid "AlertManagement|Acknowledged"
+msgstr ""
+
+msgid "AlertManagement|Alert"
+msgstr ""
+
+msgid "AlertManagement|Authorize external service"
+msgstr ""
+
+msgid "AlertManagement|Display alerts from all your monitoring tools directly within GitLab. Streamline the investigation of your alerts and the escalation of alerts to incidents."
+msgstr ""
+
+msgid "AlertManagement|End time"
+msgstr ""
+
+msgid "AlertManagement|End time:"
+msgstr ""
+
+msgid "AlertManagement|Events"
+msgstr ""
+
+msgid "AlertManagement|Events:"
+msgstr ""
+
+msgid "AlertManagement|Full Alert Details"
+msgstr ""
+
+msgid "AlertManagement|More information"
+msgstr ""
+
+msgid "AlertManagement|No alerts available to display. If you think you're seeing this message in error, refresh the page."
+msgstr ""
+
+msgid "AlertManagement|No alerts to display."
+msgstr ""
+
+msgid "AlertManagement|Overview"
+msgstr ""
+
+msgid "AlertManagement|Resolved"
+msgstr ""
+
+msgid "AlertManagement|Severity"
+msgstr ""
+
+msgid "AlertManagement|Start time"
+msgstr ""
+
+msgid "AlertManagement|Start time:"
+msgstr ""
+
+msgid "AlertManagement|Status"
+msgstr ""
+
+msgid "AlertManagement|Surface alerts in GitLab"
+msgstr ""
+
+msgid "AlertManagement|There was an error displaying the alerts. Confirm your endpoint's configuration details to ensure alerts appear."
+msgstr ""
+
+msgid "AlertManagement|Triggered"
+msgstr ""
+
msgid "AlertService|%{linkStart}Learn more%{linkEnd} about configuring this endpoint to receive alerts."
msgstr ""
@@ -1790,6 +1895,9 @@ msgstr ""
msgid "All security scans are enabled because %{linkStart}Auto DevOps%{linkEnd} is enabled on this project"
msgstr ""
+msgid "All threads resolved"
+msgstr ""
+
msgid "All users"
msgstr ""
@@ -1808,6 +1916,9 @@ msgstr ""
msgid "Allow only the selected protocols to be used for Git access."
msgstr ""
+msgid "Allow owners to manage default branch protection per group"
+msgstr ""
+
msgid "Allow owners to manually add users outside of LDAP"
msgstr ""
@@ -1916,6 +2027,9 @@ msgstr ""
msgid "An error occurred fetching the dropdown data."
msgstr ""
+msgid "An error occurred fetching the project authors."
+msgstr ""
+
msgid "An error occurred previewing the blob"
msgstr ""
@@ -1997,6 +2111,9 @@ msgstr ""
msgid "An error occurred while fetching sidebar data"
msgstr ""
+msgid "An error occurred while fetching terraform reports."
+msgstr ""
+
msgid "An error occurred while fetching the Service Desk address."
msgstr ""
@@ -2234,6 +2351,9 @@ msgstr ""
msgid "Any"
msgstr ""
+msgid "Any Author"
+msgstr ""
+
msgid "Any Label"
msgstr ""
@@ -2453,10 +2573,10 @@ msgstr ""
msgid "Are you sure that you want to unarchive this project?"
msgstr ""
-msgid "Are you sure you want to cancel creating this comment?"
+msgid "Are you sure you want to cancel editing this comment?"
msgstr ""
-msgid "Are you sure you want to cancel editing this comment?"
+msgid "Are you sure you want to close this blocked issue?"
msgstr ""
msgid "Are you sure you want to delete %{name}?"
@@ -2486,6 +2606,9 @@ msgstr ""
msgid "Are you sure you want to deploy this environment?"
msgstr ""
+msgid "Are you sure you want to discard this comment?"
+msgstr ""
+
msgid "Are you sure you want to erase this build?"
msgstr ""
@@ -2625,10 +2748,10 @@ msgid "Assign to"
msgstr ""
msgid "Assign yourself to these issues"
-msgstr ""
+msgstr "Zaduži sebi ove zadatke"
msgid "Assign yourself to this issue"
-msgstr ""
+msgstr "Zaduži sebi ovaj zadatak"
msgid "Assigned %{assignee_users_sentence}."
msgstr ""
@@ -2663,6 +2786,9 @@ msgstr ""
msgid "At least one approval from a code owner is required to change files matching the respective CODEOWNER rules."
msgstr ""
+msgid "At least one logging option is required to be enabled"
+msgstr ""
+
msgid "At least one of group_id or project_id must be specified"
msgstr ""
@@ -2702,6 +2828,27 @@ msgstr ""
msgid "AuditEvents|Target"
msgstr ""
+msgid "AuditLogs|(removed)"
+msgstr ""
+
+msgid "AuditLogs|Action"
+msgstr ""
+
+msgid "AuditLogs|Author"
+msgstr ""
+
+msgid "AuditLogs|Date"
+msgstr ""
+
+msgid "AuditLogs|IP Address"
+msgstr ""
+
+msgid "AuditLogs|Object"
+msgstr ""
+
+msgid "AuditLogs|Target"
+msgstr ""
+
msgid "Aug"
msgstr ""
@@ -2865,7 +3012,7 @@ msgid "Avatar will be removed. Are you sure?"
msgstr ""
msgid "Average per day: %{average}"
-msgstr ""
+msgstr "Dnevni prosjek: %{average}"
msgid "Back to page %{number}"
msgstr ""
@@ -2984,9 +3131,6 @@ msgstr ""
msgid "BambooService|You must set up automatic revision labeling and a repository trigger in Bamboo."
msgstr ""
-msgid "Batch operations"
-msgstr ""
-
msgid "BatchComments|Delete all pending comments"
msgstr ""
@@ -3116,6 +3260,9 @@ msgstr ""
msgid "Boards"
msgstr ""
+msgid "Boards and Board Lists"
+msgstr ""
+
msgid "Boards|Collapse"
msgstr ""
@@ -3335,6 +3482,9 @@ msgstr ""
msgid "Buy GitLab Enterprise Edition"
msgstr ""
+msgid "Buy more Pipeline minutes"
+msgstr ""
+
msgid "By %{user_name}"
msgstr ""
@@ -3455,6 +3605,9 @@ msgstr ""
msgid "Can't scan the code?"
msgstr ""
+msgid "Can't update snippet: %{err}"
+msgstr ""
+
msgid "Canary"
msgstr ""
@@ -3623,6 +3776,9 @@ msgstr ""
msgid "Changing group path can have unintended side effects."
msgstr ""
+msgid "Charts"
+msgstr ""
+
msgid "Charts can't be displayed as the request for data has timed out. %{documentationLink}"
msgstr ""
@@ -3659,9 +3815,6 @@ msgstr ""
msgid "ChatMessage|and [%{count} more](%{pipeline_failed_jobs_url})"
msgstr ""
-msgid "ChatMessage|failed"
-msgstr ""
-
msgid "ChatMessage|has failed"
msgstr ""
@@ -3677,9 +3830,6 @@ msgstr ""
msgid "ChatMessage|in %{project_link}"
msgstr ""
-msgid "ChatMessage|passed"
-msgstr ""
-
msgid "Check again"
msgstr ""
@@ -4130,9 +4280,6 @@ msgstr ""
msgid "Click any <strong>project name</strong> in the project list below to navigate to the project milestone."
msgstr ""
-msgid "Click here"
-msgstr ""
-
msgid "Click the <strong>Download</strong> button and wait for downloading to complete."
msgstr ""
@@ -4520,6 +4667,12 @@ msgstr ""
msgid "ClusterIntegration|Fetching zones"
msgstr ""
+msgid "ClusterIntegration|Fluentd"
+msgstr ""
+
+msgid "ClusterIntegration|Fluentd is an open source data collector, which lets you unify the data collection and consumption for a better use and understanding of data. It requires at least one of the following logs to be successfully installed."
+msgstr ""
+
msgid "ClusterIntegration|GitLab Integration"
msgstr ""
@@ -4826,6 +4979,15 @@ msgstr ""
msgid "ClusterIntegration|Request to begin uninstalling failed"
msgstr ""
+msgid "ClusterIntegration|SIEM Hostname"
+msgstr ""
+
+msgid "ClusterIntegration|SIEM Port"
+msgstr ""
+
+msgid "ClusterIntegration|SIEM Protocol"
+msgstr ""
+
msgid "ClusterIntegration|Save changes"
msgstr ""
@@ -4922,6 +5084,12 @@ msgstr ""
msgid "ClusterIntegration|Select zone to choose machine type"
msgstr ""
+msgid "ClusterIntegration|Send Cilium Logs"
+msgstr ""
+
+msgid "ClusterIntegration|Send ModSecurity Logs"
+msgstr ""
+
msgid "ClusterIntegration|Service Token"
msgstr ""
@@ -5351,18 +5519,33 @@ msgstr ""
msgid "Compliance framework (optional)"
msgstr ""
+msgid "ComplianceFramework|GDPR"
+msgstr ""
+
msgid "ComplianceFramework|GDPR - General Data Protection Regulation"
msgstr ""
+msgid "ComplianceFramework|HIPAA"
+msgstr ""
+
msgid "ComplianceFramework|HIPAA - Health Insurance Portability and Accountability Act"
msgstr ""
+msgid "ComplianceFramework|PCI-DSS"
+msgstr ""
+
msgid "ComplianceFramework|PCI-DSS - Payment Card Industry-Data Security Standard"
msgstr ""
+msgid "ComplianceFramework|SOC 2"
+msgstr ""
+
msgid "ComplianceFramework|SOC 2 - Service Organization Control 2"
msgstr ""
+msgid "ComplianceFramework|SOX"
+msgstr ""
+
msgid "ComplianceFramework|SOX - Sarbanes-Oxley"
msgstr ""
@@ -5510,9 +5693,15 @@ msgstr ""
msgid "Container repositories sync capacity"
msgstr ""
+msgid "ContainerRegistry| Please visit the %{linkStart}administration settings%{linkEnd} to enable this feature."
+msgstr ""
+
msgid "ContainerRegistry|%{imageName} tags"
msgstr ""
+msgid "ContainerRegistry|%{title} was successfully scheduled for deletion"
+msgstr ""
+
msgid "ContainerRegistry|Automatically remove extra images that aren't designed to be kept."
msgstr ""
@@ -5525,6 +5714,9 @@ msgstr ""
msgid "ContainerRegistry|Container Registry"
msgstr ""
+msgid "ContainerRegistry|Container Registry tag expiration and retention policy is disabled"
+msgstr ""
+
msgid "ContainerRegistry|Copy build command"
msgstr ""
@@ -5534,18 +5726,12 @@ msgstr ""
msgid "ContainerRegistry|Copy push command"
msgstr ""
-msgid "ContainerRegistry|Currently, the Container Registry tag expiration feature is not available for projects created before GitLab version 12.8. For updates and more information, visit Issue %{linkStart}#196124%{linkEnd}"
-msgstr ""
-
msgid "ContainerRegistry|Docker connection error"
msgstr ""
msgid "ContainerRegistry|Docker tag expiration policy is %{toggleStatus}"
msgstr ""
-msgid "ContainerRegistry|Docker tags with names matching this regex pattern will expire:"
-msgstr ""
-
msgid "ContainerRegistry|Edit Settings"
msgstr ""
@@ -5567,9 +5753,6 @@ msgstr ""
msgid "ContainerRegistry|Image ID"
msgstr ""
-msgid "ContainerRegistry|Image deleted successfully"
-msgstr ""
-
msgid "ContainerRegistry|Keep and protect the images that matter most."
msgstr ""
@@ -5585,12 +5768,24 @@ msgstr ""
msgid "ContainerRegistry|Number of tags to retain:"
msgstr ""
+msgid "ContainerRegistry|Please contact your administrator."
+msgstr ""
+
msgid "ContainerRegistry|Push an image"
msgstr ""
msgid "ContainerRegistry|Quick Start"
msgstr ""
+msgid "ContainerRegistry|Regular expressions such as %{codeStart}.*-test%{codeEnd} or %{codeStart}dev-.*%{codeEnd} are supported"
+msgstr ""
+
+msgid "ContainerRegistry|Regular expressions such as %{codeStart}.*-test%{codeEnd} or %{codeStart}dev-.*%{codeEnd} are supported. To select all tags, use %{codeStart}.*%{codeEnd}"
+msgstr ""
+
+msgid "ContainerRegistry|Remember to run %{docLinkStart}garbage collection%{docLinkEnd} to remove the stale data from storage."
+msgstr ""
+
msgid "ContainerRegistry|Remove repository"
msgstr ""
@@ -5606,22 +5801,22 @@ msgstr[2] ""
msgid "ContainerRegistry|Retention policy has been Enabled"
msgstr ""
-msgid "ContainerRegistry|Something went wrong while deleting the image."
+msgid "ContainerRegistry|Something went wrong while fetching the expiration policy."
msgstr ""
-msgid "ContainerRegistry|Something went wrong while deleting the tag."
+msgid "ContainerRegistry|Something went wrong while fetching the repository list."
msgstr ""
-msgid "ContainerRegistry|Something went wrong while deleting the tags."
+msgid "ContainerRegistry|Something went wrong while fetching the tags list."
msgstr ""
-msgid "ContainerRegistry|Something went wrong while fetching the expiration policy."
+msgid "ContainerRegistry|Something went wrong while marking the tag for deletion."
msgstr ""
-msgid "ContainerRegistry|Something went wrong while fetching the packages list."
+msgid "ContainerRegistry|Something went wrong while marking the tags for deletion."
msgstr ""
-msgid "ContainerRegistry|Something went wrong while fetching the tags list."
+msgid "ContainerRegistry|Something went wrong while scheduling %{title} for deletion. Please try again."
msgstr ""
msgid "ContainerRegistry|Something went wrong while updating the expiration policy."
@@ -5630,16 +5825,25 @@ msgstr ""
msgid "ContainerRegistry|Tag"
msgstr ""
-msgid "ContainerRegistry|Tag deleted successfully"
-msgstr ""
-
msgid "ContainerRegistry|Tag expiration policy"
msgstr ""
msgid "ContainerRegistry|Tag expiration policy is designed to:"
msgstr ""
-msgid "ContainerRegistry|Tags deleted successfully"
+msgid "ContainerRegistry|Tag successfully marked for deletion."
+msgstr ""
+
+msgid "ContainerRegistry|Tags successfully marked for deletion."
+msgstr ""
+
+msgid "ContainerRegistry|Tags with names matching this regex pattern will %{italicStart}be preserved:%{italicEnd}"
+msgstr ""
+
+msgid "ContainerRegistry|Tags with names matching this regex pattern will %{italicStart}expire:%{italicEnd}"
+msgstr ""
+
+msgid "ContainerRegistry|The Container Registry tag expiration and retention policies for this project have not been enabled."
msgstr ""
msgid "ContainerRegistry|The last tag related to this image was recently removed. This empty image and any associated data will be automatically removed as part of the regular Garbage Collection process. If you have any questions, contact your administrator."
@@ -5660,16 +5864,16 @@ msgstr ""
msgid "ContainerRegistry|There are no container images stored for this project"
msgstr ""
-msgid "ContainerRegistry|This Registry contains deleted image tag data. Remember to run %{docLinkStart}garbage collection%{docLinkEnd} to remove the stale data from storage."
+msgid "ContainerRegistry|There was an error during the deletion of this image repository, please try again."
msgstr ""
msgid "ContainerRegistry|This image has no active tags"
msgstr ""
-msgid "ContainerRegistry|We are having trouble connecting to Docker, which could be due to an issue with your project name or path. %{docLinkStart}More Information%{docLinkEnd}"
+msgid "ContainerRegistry|This image repository is scheduled for deletion"
msgstr ""
-msgid "ContainerRegistry|Wildcards such as %{codeStart}.*-stable%{codeEnd} or %{codeStart}production/.*%{codeEnd} are supported. To select all tags, use %{codeStart}.*%{codeEnd}"
+msgid "ContainerRegistry|We are having trouble connecting to Docker, which could be due to an issue with your project name or path. %{docLinkStart}More Information%{docLinkEnd}"
msgstr ""
msgid "ContainerRegistry|With the Container Registry, every project can have its own space to store its Docker images. %{docLinkStart}More Information%{docLinkEnd}"
@@ -5924,7 +6128,7 @@ msgstr ""
msgid "Could not delete chat nickname %{chat_name}."
msgstr ""
-msgid "Could not find design"
+msgid "Could not find design."
msgstr ""
msgid "Could not remove the trigger."
@@ -5939,6 +6143,9 @@ msgstr ""
msgid "Could not revoke personal access token %{personal_access_token_name}."
msgstr ""
+msgid "Could not revoke project access token %{project_access_token_name}."
+msgstr ""
+
msgid "Could not save group ID"
msgstr ""
@@ -5996,6 +6203,9 @@ msgstr ""
msgid "Create a new branch"
msgstr ""
+msgid "Create a new deploy key for this project"
+msgstr ""
+
msgid "Create a new file as there are no files yet. Afterwards, you'll be able to commit your changes."
msgstr ""
@@ -6101,6 +6311,9 @@ msgstr ""
msgid "Create requirement"
msgstr ""
+msgid "Create snippet"
+msgstr ""
+
msgid "Create wildcard: %{searchTerm}"
msgstr ""
@@ -6122,6 +6335,9 @@ msgstr ""
msgid "Created"
msgstr ""
+msgid "Created %{timestamp}"
+msgstr ""
+
msgid "Created At"
msgstr ""
@@ -6479,6 +6695,9 @@ msgstr ""
msgid "CycleAnalytics|stage dropdown"
msgstr ""
+msgid "DAG"
+msgstr ""
+
msgid "DNS"
msgstr ""
@@ -6519,22 +6738,22 @@ msgid "Date range cannot exceed %{maxDateRange} days."
msgstr ""
msgid "Day of month"
-msgstr ""
+msgstr "Dan u mjesecu"
msgid "DayTitle|F"
-msgstr ""
+msgstr "P"
msgid "DayTitle|M"
-msgstr ""
+msgstr "P"
msgid "DayTitle|S"
-msgstr ""
+msgstr "N"
msgid "DayTitle|W"
-msgstr ""
+msgstr "S"
msgid "Days"
-msgstr ""
+msgstr "Dani"
msgid "Days to merge"
msgstr ""
@@ -6582,10 +6801,10 @@ msgid "Default description template for merge requests"
msgstr ""
msgid "Default first day of the week"
-msgstr ""
+msgstr "Standardni prvi dan sedmice"
msgid "Default first day of the week in calendars and date pickers."
-msgstr ""
+msgstr "Standardni prvi dan sedmice u kalendarima i izbornicima datuma."
msgid "Default issue template"
msgstr ""
@@ -6851,6 +7070,9 @@ msgstr ""
msgid "Deploy key was successfully updated."
msgstr ""
+msgid "Deploy keys allow read-only or read-write (if enabled) access to your repository. Deploy keys can be used for CI, staging or production servers. You can create a deploy key or add an existing one."
+msgstr ""
+
msgid "Deploy progress not found. To see pods, ensure your environment matches %{linkStart}deploy board criteria%{linkEnd}."
msgstr ""
@@ -7004,6 +7226,9 @@ msgstr ""
msgid "Deploying to"
msgstr ""
+msgid "Deployment Frequency"
+msgstr ""
+
msgid "Deployment|API"
msgstr ""
@@ -7523,9 +7748,6 @@ msgstr ""
msgid "Edit environment"
msgstr ""
-msgid "Edit epic description"
-msgstr ""
-
msgid "Edit file"
msgstr ""
@@ -8147,9 +8369,6 @@ msgstr ""
msgid "Epics"
msgstr ""
-msgid "Epics (Ultimate / Gold license only)"
-msgstr ""
-
msgid "Epics Roadmap"
msgstr ""
@@ -8159,6 +8378,9 @@ msgstr ""
msgid "Epics let you manage your portfolio of projects more efficiently and with less effort"
msgstr ""
+msgid "Epics, Issues, and Merge Requests"
+msgstr ""
+
msgid "Epics|Add an epic"
msgstr ""
@@ -8172,7 +8394,7 @@ msgid "Epics|An error occurred while updating labels."
msgstr ""
msgid "Epics|Are you sure you want to remove %{bStart}%{targetIssueTitle}%{bEnd} from %{bStart}%{parentEpicTitle}%{bEnd}?"
-msgstr ""
+msgstr "Jesi li siguran da želiš ukloniti %{bStart}%{targetIssueTitle}%{bEnd} iz %{bStart}%{parentEpicTitle}%{bEnd}?"
msgid "Epics|Create an epic within this group and add it as a child epic."
msgstr ""
@@ -8210,6 +8432,9 @@ msgstr ""
msgid "Epics|Something went wrong while fetching group epics."
msgstr ""
+msgid "Epics|Something went wrong while moving item."
+msgstr ""
+
msgid "Epics|Something went wrong while ordering item."
msgstr ""
@@ -8217,7 +8442,7 @@ msgid "Epics|Something went wrong while removing issue from epic."
msgstr ""
msgid "Epics|These dates affect how your epics appear in the roadmap. Dates from milestones come from the milestones assigned to issues in the epic. You can also set fixed dates or remove them entirely."
-msgstr ""
+msgstr "Ovi datumi utiÄu na to kako se epici pojavljuju u planu projekta. Datumi iz ciljeva se preuzimaju iz ciljeva koji su dodijeljeni zadacima u epiku. MožeÅ¡ postaviti i fiksne datume ili ih u potpunosti ukloniti."
msgid "Epics|This will also remove any descendents of %{bStart}%{targetEpicTitle}%{bEnd} from %{bStart}%{parentEpicTitle}%{bEnd}. Are you sure?"
msgstr ""
@@ -8264,6 +8489,9 @@ msgstr ""
msgid "Error fetching network graph."
msgstr ""
+msgid "Error fetching payload data."
+msgstr ""
+
msgid "Error fetching projects"
msgstr ""
@@ -8273,9 +8501,6 @@ msgstr ""
msgid "Error fetching the dependency list. Please check your network connection and try again."
msgstr ""
-msgid "Error fetching usage ping data."
-msgstr ""
-
msgid "Error loading branch data. Please try again."
msgstr ""
@@ -8417,6 +8642,9 @@ msgstr ""
msgid "Errors"
msgstr ""
+msgid "Errors:"
+msgstr ""
+
msgid "Estimated"
msgstr ""
@@ -8460,28 +8688,28 @@ msgid "Every %{action} attempt has failed: %{job_error_message}. Please try agai
msgstr ""
msgid "Every day"
-msgstr ""
+msgstr "Svaki dan"
msgid "Every day (at 4:00am)"
-msgstr ""
+msgstr "Svaki dan (u 4:00 h)"
msgid "Every month"
-msgstr ""
+msgstr "MjeseÄno"
msgid "Every month (on the 1st at 4:00am)"
-msgstr ""
+msgstr "MjeseÄno (1. u 4:00 h)"
msgid "Every three months"
-msgstr ""
+msgstr "TromjeseÄno"
msgid "Every two weeks"
-msgstr ""
+msgstr "DvosedmiÄno"
msgid "Every week"
-msgstr ""
+msgstr "SedmiÄno"
msgid "Every week (Sundays at 4:00am)"
-msgstr ""
+msgstr "Svake sedmice (nedjeljom u 4:00 h)"
msgid "Everyone"
msgstr ""
@@ -8621,12 +8849,18 @@ msgstr ""
msgid "Export as CSV"
msgstr ""
+msgid "Export group"
+msgstr ""
+
msgid "Export issues"
msgstr ""
msgid "Export project"
msgstr ""
+msgid "Export this group with all related data to a new GitLab instance. Once complete, you can import the data file from the \"New Group\" page."
+msgstr ""
+
msgid "Export this project with all its related data in order to move your project to a new GitLab instance. Once the export is finished, you can import the file from the \"New Project\" page."
msgstr ""
@@ -8696,9 +8930,6 @@ msgstr ""
msgid "Failed Jobs"
msgstr ""
-msgid "Failed create wiki"
-msgstr ""
-
msgid "Failed to add a Zoom meeting"
msgstr ""
@@ -8738,6 +8969,9 @@ msgstr ""
msgid "Failed to create resources"
msgstr ""
+msgid "Failed to create wiki"
+msgstr ""
+
msgid "Failed to delete board. Please try again."
msgstr ""
@@ -8747,7 +8981,7 @@ msgstr ""
msgid "Failed to enqueue the rebase operation, possibly due to a long-lived transaction. Try again later."
msgstr ""
-msgid "Failed to find import label for jira import."
+msgid "Failed to find import label for Jira import."
msgstr ""
msgid "Failed to get ref."
@@ -8771,6 +9005,12 @@ msgstr ""
msgid "Failed to load groups & users."
msgstr ""
+msgid "Failed to load labels. Please try again."
+msgstr ""
+
+msgid "Failed to load milestones. Please try again."
+msgstr ""
+
msgid "Failed to load related branches"
msgstr ""
@@ -9146,7 +9386,10 @@ msgstr ""
msgid "Filter by milestone name"
msgstr ""
-msgid "Filter by name..."
+msgid "Filter by name"
+msgstr ""
+
+msgid "Filter by status"
msgstr ""
msgid "Filter by two-factor authentication"
@@ -9158,12 +9401,18 @@ msgstr ""
msgid "Filter projects"
msgstr ""
+msgid "Filter results"
+msgstr ""
+
msgid "Filter results by group"
msgstr ""
msgid "Filter results by project"
msgstr ""
+msgid "Filter results..."
+msgstr ""
+
msgid "Filter your projects by name"
msgstr ""
@@ -9210,7 +9459,7 @@ msgid "First Seen"
msgstr ""
msgid "First day of the week"
-msgstr ""
+msgstr "Prvi dan sedmice"
msgid "First name"
msgstr ""
@@ -9348,7 +9597,7 @@ msgid "Frequency"
msgstr ""
msgid "Friday"
-msgstr ""
+msgstr "Petak"
msgid "From"
msgstr ""
@@ -9452,10 +9701,10 @@ msgstr ""
msgid "GeoNodes|Checksummed"
msgstr ""
-msgid "GeoNodes|Container repositories"
+msgid "GeoNodes|Consult Geo troubleshooting information"
msgstr ""
-msgid "GeoNodes|Data is out of date from %{timeago}"
+msgid "GeoNodes|Container repositories"
msgstr ""
msgid "GeoNodes|Data replication lag"
@@ -9497,6 +9746,9 @@ msgstr ""
msgid "GeoNodes|Last event ID seen from primary"
msgstr ""
+msgid "GeoNodes|Learn more about Geo node statuses"
+msgstr ""
+
msgid "GeoNodes|Loading nodes"
msgstr ""
@@ -9512,6 +9764,9 @@ msgstr ""
msgid "GeoNodes|Node was successfully removed."
msgstr ""
+msgid "GeoNodes|Node's status was updated %{timeAgo}."
+msgstr ""
+
msgid "GeoNodes|Not checksummed"
msgstr ""
@@ -9542,7 +9797,10 @@ msgstr ""
msgid "GeoNodes|Repository verification progress"
msgstr ""
-msgid "GeoNodes|Selective"
+msgid "GeoNodes|Selective (%{syncLabel})"
+msgstr ""
+
+msgid "GeoNodes|Selective synchronization"
msgstr ""
msgid "GeoNodes|Something went wrong while changing node status"
@@ -9569,6 +9827,9 @@ msgstr ""
msgid "GeoNodes|Unverified"
msgstr ""
+msgid "GeoNodes|Updated %{timeAgo}"
+msgstr ""
+
msgid "GeoNodes|Used slots"
msgstr ""
@@ -9608,16 +9869,16 @@ msgstr ""
msgid "Geo|All"
msgstr ""
-msgid "Geo|All projects"
+msgid "Geo|All %{replicable_type}"
msgstr ""
-msgid "Geo|All projects are being scheduled for re-sync"
+msgid "Geo|All projects"
msgstr ""
-msgid "Geo|All projects are being scheduled for re-verify"
+msgid "Geo|All projects are being scheduled for resync"
msgstr ""
-msgid "Geo|Batch operations"
+msgid "Geo|All projects are being scheduled for reverify"
msgstr ""
msgid "Geo|Could not remove tracking entry for an existing project."
@@ -9629,9 +9890,15 @@ msgstr ""
msgid "Geo|Failed"
msgstr ""
+msgid "Geo|Filter by status"
+msgstr ""
+
msgid "Geo|Geo Status"
msgstr ""
+msgid "Geo|In progress"
+msgstr ""
+
msgid "Geo|In sync"
msgstr ""
@@ -9656,9 +9923,6 @@ msgstr ""
msgid "Geo|Not synced yet"
msgstr ""
-msgid "Geo|Pending"
-msgstr ""
-
msgid "Geo|Pending synchronization"
msgstr ""
@@ -9695,7 +9959,7 @@ msgstr ""
msgid "Geo|Resync"
msgstr ""
-msgid "Geo|Resync all projects"
+msgid "Geo|Resync all"
msgstr ""
msgid "Geo|Retry count"
@@ -9704,7 +9968,7 @@ msgstr ""
msgid "Geo|Reverify"
msgstr ""
-msgid "Geo|Reverify all projects"
+msgid "Geo|Reverify all"
msgstr ""
msgid "Geo|Status"
@@ -10046,9 +10310,6 @@ msgstr ""
msgid "Go to file"
msgstr ""
-msgid "Go to file (MRs only)"
-msgstr ""
-
msgid "Go to file permalink (while viewing a file)"
msgstr ""
@@ -10119,7 +10380,7 @@ msgid "Go to your groups"
msgstr ""
msgid "Go to your issues"
-msgstr ""
+msgstr "Idi na tvoje zadatke"
msgid "Go to your merge requests"
msgstr ""
@@ -10493,6 +10754,9 @@ msgstr ""
msgid "GroupSettings|Disable group mentions"
msgstr ""
+msgid "GroupSettings|Export group"
+msgstr ""
+
msgid "GroupSettings|If the parent group's visibility is lower than the group current visibility, visibility levels for subgroups and projects will be changed to match the new parent group's visibility."
msgstr ""
@@ -10571,6 +10835,9 @@ msgstr ""
msgid "Groups (%{count})"
msgstr ""
+msgid "Groups (%{groups})"
+msgstr ""
+
msgid "Groups can also be nested by creating %{subgroup_docs_link_start}subgroups%{subgroup_docs_link_end}."
msgstr ""
@@ -10838,15 +11105,12 @@ msgstr ""
msgid "ID:"
msgstr ""
-msgid "IDE|Allow live previews of JavaScript projects in the Web IDE using CodeSandbox client side evaluation."
+msgid "IDE|Allow live previews of JavaScript projects in the Web IDE using CodeSandbox Live Preview."
msgstr ""
msgid "IDE|Back"
msgstr ""
-msgid "IDE|Client side evaluation"
-msgstr ""
-
msgid "IDE|Commit"
msgstr ""
@@ -10910,6 +11174,9 @@ msgstr ""
msgid "If any job surpasses this timeout threshold, it will be marked as failed. Human readable time input language is accepted like \"1 hour\". Values without specification represent seconds."
msgstr ""
+msgid "If blank, set allowable lifetime to %{instance_level_policy_in_words}, as defined by the instance admin. Once set, existing tokens for users in this group may be revoked."
+msgstr ""
+
msgid "If checked, group owners can manage LDAP group links and LDAP member overrides"
msgstr ""
@@ -10949,6 +11216,9 @@ msgstr ""
msgid "If you lose your recovery codes you can generate new ones, invalidating all previous codes."
msgstr ""
+msgid "If you reach 100%% storage capacity, you will not be able to: %{base_message}"
+msgstr ""
+
msgid "If your HTTP repository is not publicly accessible, add your credentials."
msgstr ""
@@ -11060,6 +11330,9 @@ msgstr ""
msgid "Import repository"
msgstr ""
+msgid "Import started by: %{importInitiator}"
+msgstr ""
+
msgid "Import tasks"
msgstr ""
@@ -11126,6 +11399,9 @@ msgstr ""
msgid "In order to tailor your experience with GitLab we<br>would like to know a bit more about you."
msgstr ""
+msgid "In progress"
+msgstr ""
+
msgid "In the next step, you'll be able to select the projects you want to import."
msgstr ""
@@ -11433,7 +11709,7 @@ msgid "Issue was closed by %{name} %{reason}"
msgstr ""
msgid "Issue weight"
-msgstr ""
+msgstr "Težina zadatka"
msgid "IssueBoards|Board"
msgstr ""
@@ -11451,7 +11727,7 @@ msgid "IssueBoards|No matching boards found"
msgstr ""
msgid "IssueBoards|Some of your boards are hidden, activate a license to see them again."
-msgstr ""
+msgstr "Neke tvoje table nisu vidljive, aktiviraj licencu za ponovni prikaz."
msgid "IssueBoards|Switch board"
msgstr ""
@@ -11474,15 +11750,15 @@ msgstr ""
msgid "Issues"
msgstr ""
-msgid "Issues / Merge Requests"
-msgstr ""
-
msgid "Issues Analytics"
msgstr ""
msgid "Issues Rate Limits"
msgstr ""
+msgid "Issues and Merge Requests"
+msgstr ""
+
msgid "Issues can be bugs, tasks or ideas to be discussed. Also, issues are searchable and filterable."
msgstr ""
@@ -11504,10 +11780,10 @@ msgstr ""
msgid "IssuesAnalytics|Avg/Month:"
msgstr ""
-msgid "IssuesAnalytics|Issues created"
+msgid "IssuesAnalytics|Issues opened"
msgstr ""
-msgid "IssuesAnalytics|Issues created per month"
+msgid "IssuesAnalytics|Issues opened per month"
msgstr ""
msgid "IssuesAnalytics|Last 12 months"
@@ -11561,6 +11837,9 @@ msgstr ""
msgid "Jira integration not configured."
msgstr ""
+msgid "Jira project: %{importProject}"
+msgstr ""
+
msgid "JiraService|Events for %{noteable_model_name} are disabled."
msgstr ""
@@ -11860,12 +12139,12 @@ msgstr ""
msgid "Last %d day"
msgid_plural "Last %d days"
-msgstr[0] ""
-msgstr[1] ""
-msgstr[2] ""
+msgstr[0] "Zadnjih %d dan"
+msgstr[1] "Zadnjih %d dana"
+msgstr[2] "Zadnjih %d dana"
msgid "Last %{days} days"
-msgstr ""
+msgstr "Zadnjih %{days} dana"
msgid "Last Accessed On"
msgstr ""
@@ -11951,6 +12230,9 @@ msgstr ""
msgid "Lead"
msgstr ""
+msgid "Lead Time"
+msgstr ""
+
msgid "Learn GitLab"
msgstr ""
@@ -11963,6 +12245,9 @@ msgstr ""
msgid "Learn how to %{no_packages_link_start}publish and share your packages%{no_packages_link_end} with GitLab."
msgstr ""
+msgid "Learn how to enable synchronization"
+msgstr ""
+
msgid "Learn more"
msgstr ""
@@ -12299,9 +12584,6 @@ msgstr ""
msgid "Live preview"
msgstr ""
-msgid "Load more vulnerabilities"
-msgstr ""
-
msgid "Loading"
msgstr ""
@@ -12452,9 +12734,6 @@ msgstr ""
msgid "Manage"
msgstr ""
-msgid "Manage Git repositories with fine-grained access controls that keep your code secure. Perform code reviews and enhance collaboration with merge requests. Each project can also have an issue tracker and a wiki."
-msgstr ""
-
msgid "Manage Web IDE features"
msgstr ""
@@ -12485,6 +12764,9 @@ msgstr ""
msgid "Manage your license"
msgstr ""
+msgid "Managed Account"
+msgstr ""
+
msgid "Manifest"
msgstr ""
@@ -12749,6 +13031,9 @@ msgstr ""
msgid "Merge Requests in Review"
msgstr ""
+msgid "Merge automatically (%{strategy})"
+msgstr ""
+
msgid "Merge commit message"
msgstr ""
@@ -12902,6 +13187,12 @@ msgstr ""
msgid "Merged branches are being deleted. This can take some time depending on the number of branches. Please refresh the page to see changes."
msgstr ""
+msgid "Merged this merge request."
+msgstr ""
+
+msgid "Merges this merge request immediately."
+msgstr ""
+
msgid "Merges this merge request when the pipeline succeeds."
msgstr ""
@@ -13010,12 +13301,15 @@ msgstr[0] ""
msgstr[1] ""
msgstr[2] ""
-msgid "Metrics|Environment"
+msgid "Metrics|Expand panel"
msgstr ""
msgid "Metrics|For grouping similar metrics"
msgstr ""
+msgid "Metrics|Go back (Esc)"
+msgstr ""
+
msgid "Metrics|Invalid time range, please verify."
msgstr ""
@@ -13046,9 +13340,6 @@ msgstr ""
msgid "Metrics|Refresh dashboard"
msgstr ""
-msgid "Metrics|Show last"
-msgstr ""
-
msgid "Metrics|There was an error creating the dashboard."
msgstr ""
@@ -13127,6 +13418,9 @@ msgstr ""
msgid "Microsoft Azure"
msgstr ""
+msgid "Middleman project with Static Site Editor support"
+msgstr ""
+
msgid "Migrated %{success_count}/%{total_count} files."
msgstr ""
@@ -13191,7 +13485,7 @@ msgid "Minimum password length (number of characters)"
msgstr ""
msgid "Minutes"
-msgstr ""
+msgstr "Minute"
msgid "Mirror direction"
msgstr ""
@@ -13260,7 +13554,7 @@ msgid "Modify merge commit"
msgstr ""
msgid "Monday"
-msgstr ""
+msgstr "Ponedjeljak"
msgid "Monitor your errors by integrating with Sentry."
msgstr ""
@@ -13269,7 +13563,7 @@ msgid "Monitoring"
msgstr ""
msgid "Months"
-msgstr ""
+msgstr "Mjeseci"
msgid "More"
msgstr ""
@@ -13574,10 +13868,10 @@ msgstr ""
msgid "Next"
msgstr ""
-msgid "Next file in diff (MRs only)"
+msgid "Next file in diff"
msgstr ""
-msgid "Next unresolved discussion (MRs only)"
+msgid "Next unresolved discussion"
msgstr ""
msgid "Nickname"
@@ -13637,6 +13931,9 @@ msgstr ""
msgid "No changes between %{ref_start}%{source_branch}%{ref_end} and %{ref_start}%{target_branch}%{ref_end}"
msgstr ""
+msgid "No child epics match applied filters"
+msgstr ""
+
msgid "No connection could be made to a Gitaly Server, please check your logs!"
msgstr ""
@@ -13775,15 +14072,6 @@ msgstr ""
msgid "No thanks, don't show this again"
msgstr ""
-msgid "No vulnerabilities found for this group"
-msgstr ""
-
-msgid "No vulnerabilities found for this pipeline"
-msgstr ""
-
-msgid "No vulnerabilities found for this project"
-msgstr ""
-
msgid "No vulnerabilities present"
msgstr ""
@@ -13907,6 +14195,9 @@ msgstr ""
msgid "Nothing to preview."
msgstr ""
+msgid "Nothing to synchronize"
+msgstr ""
+
msgid "Notification events"
msgstr ""
@@ -14078,6 +14369,9 @@ msgstr ""
msgid "Once removed, the fork relationship cannot be restored and you will no longer be able to send merge requests to the source."
msgstr ""
+msgid "Once the exported file is ready you can download it from this page."
+msgstr ""
+
msgid "Once the exported file is ready, you will receive a notification email with a download link, or you can download it from this page."
msgstr ""
@@ -14180,9 +14474,6 @@ msgstr ""
msgid "Open sidebar"
msgstr ""
-msgid "Open source software to collaborate on code"
-msgstr ""
-
msgid "Open: %{openIssuesCount}"
msgstr ""
@@ -14199,7 +14490,7 @@ msgid "Opened issues"
msgstr ""
msgid "OpenedNDaysAgo|Opened"
-msgstr ""
+msgstr "Otvoreno"
msgid "Opens in a new window"
msgstr ""
@@ -14285,6 +14576,9 @@ msgstr ""
msgid "OutdatedBrowser|You can provide feedback %{feedback_link_start}on this issue%{feedback_link_end} or via your usual support channels."
msgstr ""
+msgid "Overridden"
+msgstr ""
+
msgid "Overview"
msgstr ""
@@ -14336,6 +14630,9 @@ msgstr ""
msgid "PackageRegistry|Conan Command"
msgstr ""
+msgid "PackageRegistry|Copy .pypirc content"
+msgstr ""
+
msgid "PackageRegistry|Copy Conan Command"
msgstr ""
@@ -14357,6 +14654,9 @@ msgstr ""
msgid "PackageRegistry|Copy NuGet Setup Command"
msgstr ""
+msgid "PackageRegistry|Copy Pip command"
+msgstr ""
+
msgid "PackageRegistry|Copy and paste this inside your %{codeStart}pom.xml%{codeEnd} %{codeStart}dependencies%{codeEnd} block."
msgstr ""
@@ -14390,6 +14690,12 @@ msgstr ""
msgid "PackageRegistry|For more information on the NuGet registry, %{linkStart}see the documentation%{linkEnd}."
msgstr ""
+msgid "PackageRegistry|For more information on the PyPi registry, %{linkStart}see the documentation%{linkEnd}."
+msgstr ""
+
+msgid "PackageRegistry|If you haven't already done so, you will need to add the below to your %{codeStart}.pypirc%{codeEnd} file."
+msgstr ""
+
msgid "PackageRegistry|If you haven't already done so, you will need to add the below to your %{codeStart}pom.xml%{codeEnd} file."
msgstr ""
@@ -14399,6 +14705,9 @@ msgstr ""
msgid "PackageRegistry|Learn how to %{noPackagesLinkStart}publish and share your packages%{noPackagesLinkEnd} with GitLab."
msgstr ""
+msgid "PackageRegistry|Manually Published"
+msgstr ""
+
msgid "PackageRegistry|Maven"
msgstr ""
@@ -14417,12 +14726,18 @@ msgstr ""
msgid "PackageRegistry|NuGet Command"
msgstr ""
+msgid "PackageRegistry|Pip Command"
+msgstr ""
+
msgid "PackageRegistry|Pipeline %{linkStart}%{linkEnd} triggered %{timestamp} by %{author}"
msgstr ""
msgid "PackageRegistry|Published to the repository at %{timestamp}"
msgstr ""
+msgid "PackageRegistry|PyPi"
+msgstr ""
+
msgid "PackageRegistry|Registry Setup"
msgstr ""
@@ -14459,6 +14774,9 @@ msgstr ""
msgid "PackageRegistry|npm"
msgstr ""
+msgid "PackageRegistry|published by %{author}"
+msgstr ""
+
msgid "PackageRegistry|yarn"
msgstr ""
@@ -14474,6 +14792,9 @@ msgstr ""
msgid "PackageType|NuGet"
msgstr ""
+msgid "PackageType|PyPi"
+msgstr ""
+
msgid "Packages"
msgstr ""
@@ -14567,6 +14888,9 @@ msgstr ""
msgid "Past due"
msgstr ""
+msgid "Paste a machine public key here. Read more about how to generate it"
+msgstr ""
+
msgid "Paste a machine public key here. Read more about how to generate it %{link_start}here%{link_end}"
msgstr ""
@@ -14582,9 +14906,6 @@ msgstr ""
msgid "Path"
msgstr ""
-msgid "Path, transfer, remove"
-msgstr ""
-
msgid "Path:"
msgstr ""
@@ -14615,7 +14936,7 @@ msgstr ""
msgid "Percentage"
msgstr ""
-msgid "Perform advanced options such as changing path, transferring, or removing the group."
+msgid "Perform advanced options such as changing path, transferring, exporting, or removing the group."
msgstr ""
msgid "Perform common operations on GitLab project"
@@ -14696,9 +15017,6 @@ msgstr ""
msgid "Pipeline minutes quota"
msgstr ""
-msgid "Pipeline quota"
-msgstr ""
-
msgid "Pipeline subscriptions"
msgstr ""
@@ -14708,6 +15026,9 @@ msgstr ""
msgid "Pipeline: %{status}"
msgstr ""
+msgid "PipelineCharts|CI / CD Analytics"
+msgstr ""
+
msgid "PipelineCharts|Failed:"
msgstr ""
@@ -14786,15 +15107,6 @@ msgstr ""
msgid "Pipelines settings for '%{project_name}' were successfully updated."
msgstr ""
-msgid "Pipelines| to purchase more minutes."
-msgstr ""
-
-msgid "Pipelines|%{namespace_name} has exceeded its pipeline minutes quota."
-msgstr ""
-
-msgid "Pipelines|%{namespace_name} has less than %{notification_level}%% of CI minutes available."
-msgstr ""
-
msgid "Pipelines|API"
msgstr ""
@@ -14816,10 +15128,13 @@ msgstr ""
msgid "Pipelines|Get started with Pipelines"
msgstr ""
-msgid "Pipelines|Loading Pipelines"
+msgid "Pipelines|Group %{namespace_name} has %{percentage}%% or less Shared Runner Pipeline minutes remaining. Once it runs out, no new jobs or pipelines in its projects will run."
msgstr ""
-msgid "Pipelines|Pipelines will not run anymore on shared Runners."
+msgid "Pipelines|Group %{namespace_name} has exceeded its pipeline minutes quota. Unless you buy additional pipeline minutes, no new jobs or pipelines in its projects will run."
+msgstr ""
+
+msgid "Pipelines|Loading Pipelines"
msgstr ""
msgid "Pipelines|Project cache successfully reset."
@@ -15050,6 +15365,9 @@ msgstr ""
msgid "Please select"
msgstr ""
+msgid "Please select a Jira project"
+msgstr ""
+
msgid "Please select a country"
msgstr ""
@@ -15095,6 +15413,9 @@ msgstr ""
msgid "Please wait while we import the repository for you. Refresh at will."
msgstr ""
+msgid "Plugins directory is deprecated and will be removed in 14.0. Please move this file into /file_hooks directory."
+msgstr ""
+
msgid "Pod does not exist"
msgstr ""
@@ -15212,7 +15533,7 @@ msgstr ""
msgid "Prevent users from changing their profile name"
msgstr ""
-msgid "Prevent users from modifing merge request approvers list"
+msgid "Prevent users from modifying merge request approvers list"
msgstr ""
msgid "Prevent users from performing write operations on GitLab while performing maintenance."
@@ -15233,10 +15554,10 @@ msgstr ""
msgid "Previous Artifacts"
msgstr ""
-msgid "Previous file in diff (MRs only)"
+msgid "Previous file in diff"
msgstr ""
-msgid "Previous unresolved discussion (MRs only)"
+msgid "Previous unresolved discussion"
msgstr ""
msgid "Primary"
@@ -15269,6 +15590,9 @@ msgstr ""
msgid "Private profile"
msgstr ""
+msgid "Private projects Minutes cost factor"
+msgstr ""
+
msgid "Private projects can be created in your personal namespace with:"
msgstr ""
@@ -16097,6 +16421,9 @@ msgstr ""
msgid "ProjectSettings|With GitLab Pages you can host your static websites on GitLab"
msgstr ""
+msgid "ProjectSettings|With Metrics Dashboard you can visualize this project performance metrics"
+msgstr ""
+
msgid "ProjectTemplates|.NET Core"
msgstr ""
@@ -16160,6 +16487,9 @@ msgstr ""
msgid "ProjectTemplates|Spring"
msgstr ""
+msgid "ProjectTemplates|Static Site Editor/Middleman"
+msgstr ""
+
msgid "ProjectTemplates|iOS (Swift)"
msgstr ""
@@ -16379,13 +16709,13 @@ msgstr ""
msgid "PrometheusService|Prometheus is being automatically managed on your clusters"
msgstr ""
-msgid "PrometheusService|These metrics will only be monitored after your first deployment to an environment"
+msgid "PrometheusService|Select the Active checkbox to override the Auto Configuration with custom settings. If unchecked, Auto Configuration settings are used."
msgstr ""
-msgid "PrometheusService|Time-series monitoring service"
+msgid "PrometheusService|These metrics will only be monitored after your first deployment to an environment"
msgstr ""
-msgid "PrometheusService|To enable manual configuration, uninstall Prometheus from your clusters"
+msgid "PrometheusService|Time-series monitoring service"
msgstr ""
msgid "PrometheusService|To enable the installation of Prometheus on your clusters, deactivate the manual configuration below"
@@ -16571,6 +16901,9 @@ msgstr ""
msgid "Protip:"
msgstr ""
+msgid "Protocol"
+msgstr ""
+
msgid "Provider"
msgstr ""
@@ -16595,6 +16928,9 @@ msgstr ""
msgid "Public pipelines"
msgstr ""
+msgid "Public projects Minutes cost factor"
+msgstr ""
+
msgid "Pull"
msgstr ""
@@ -16796,6 +17132,9 @@ msgstr[0] ""
msgstr[1] ""
msgstr[2] ""
+msgid "Regenerate export"
+msgstr ""
+
msgid "Regenerate instance ID"
msgstr ""
@@ -17174,9 +17513,30 @@ msgstr ""
msgid "Reports|%{combinedString} and %{resolvedString}"
msgstr ""
+msgid "Reports|Accessibility scanning detected %d issue for the source branch only"
+msgid_plural "Reports|Accessibility scanning detected %d issues for the source branch only"
+msgstr[0] ""
+msgstr[1] ""
+msgstr[2] ""
+
+msgid "Reports|Accessibility scanning detected no issues for the source branch only"
+msgstr ""
+
+msgid "Reports|Accessibility scanning failed loading results"
+msgstr ""
+
+msgid "Reports|Accessibility scanning results are being parsed"
+msgstr ""
+
msgid "Reports|Actions"
msgstr ""
+msgid "Reports|An error occured while loading report"
+msgstr ""
+
+msgid "Reports|An error occurred while loading %{name} results"
+msgstr ""
+
msgid "Reports|Class"
msgstr ""
@@ -17282,7 +17642,7 @@ msgstr ""
msgid "Requested %{time_ago}"
msgstr ""
-msgid "Requested design version does not exist"
+msgid "Requested design version does not exist."
msgstr ""
msgid "Requested states are invalid"
@@ -17468,6 +17828,9 @@ msgstr ""
msgid "Resync"
msgstr ""
+msgid "Resync all"
+msgstr ""
+
msgid "Resync all %{replicableType}"
msgstr ""
@@ -17537,6 +17900,9 @@ msgstr ""
msgid "Revoked personal access token %{personal_access_token_name}!"
msgstr ""
+msgid "Revoked project access token %{project_access_token_name}!"
+msgstr ""
+
msgid "RightSidebar|adding a"
msgstr ""
@@ -17585,6 +17951,9 @@ msgstr ""
msgid "Runner token"
msgstr ""
+msgid "Runner tokens"
+msgstr ""
+
msgid "Runner was not updated."
msgstr ""
@@ -17639,6 +18008,9 @@ msgstr ""
msgid "SAML SSO for %{group_name}"
msgstr ""
+msgid "SAML discovery tokens"
+msgstr ""
+
msgid "SAML for %{group_name}"
msgstr ""
@@ -17673,7 +18045,7 @@ msgid "SSL Verification:"
msgstr ""
msgid "Saturday"
-msgstr ""
+msgstr "Subota"
msgid "Save"
msgstr ""
@@ -17723,12 +18095,18 @@ msgstr ""
msgid "Scheduled"
msgstr ""
+msgid "Scheduled to merge this merge request (%{strategy})."
+msgstr ""
+
msgid "Scheduled to merge this merge request when the pipeline succeeds."
msgstr ""
msgid "Schedules"
msgstr ""
+msgid "Schedules to merge this merge request (%{strategy})."
+msgstr ""
+
msgid "Scheduling"
msgstr ""
@@ -17786,6 +18164,9 @@ msgstr ""
msgid "Search branches and tags"
msgstr ""
+msgid "Search by author"
+msgstr ""
+
msgid "Search files"
msgstr ""
@@ -17939,12 +18320,6 @@ msgstr[0] ""
msgstr[1] ""
msgstr[2] ""
-msgid "SearchResults|snippet result"
-msgid_plural "SearchResults|snippet results"
-msgstr[0] ""
-msgstr[1] ""
-msgstr[2] ""
-
msgid "SearchResults|user"
msgid_plural "SearchResults|users"
msgstr[0] ""
@@ -17957,6 +18332,9 @@ msgstr[0] ""
msgstr[1] ""
msgstr[2] ""
+msgid "Searching by both author and message is currently not supported."
+msgstr ""
+
msgid "Seat Link"
msgstr ""
@@ -17984,184 +18362,232 @@ msgstr ""
msgid "Security Dashboard"
msgstr ""
-msgid "Security Dashboard|Error fetching the vulnerability counts. Please check your network connection and try again."
+msgid "Security configuration help link"
msgstr ""
-msgid "Security Dashboard|Error fetching the vulnerability list. Please check your network connection and try again."
+msgid "Security dashboard"
msgstr ""
-msgid "Security Dashboard|Issue Created"
+msgid "Security report is out of date. Please update your branch with the latest changes from the target branch (%{targetBranchName})"
msgstr ""
-msgid "Security Reports|Comment added to '%{vulnerabilityName}'"
+msgid "Security report is out of date. Run %{newPipelineLinkStart}a new pipeline%{newPipelineLinkEnd} for the target branch (%{targetBranchName})"
msgstr ""
-msgid "Security Reports|Comment deleted on '%{vulnerabilityName}'"
+msgid "SecurityConfiguration|Configured"
msgstr ""
-msgid "Security Reports|Comment edited on '%{vulnerabilityName}'"
+msgid "SecurityConfiguration|Feature"
msgstr ""
-msgid "Security Reports|Create issue"
+msgid "SecurityConfiguration|Feature documentation for %{featureName}"
msgstr ""
-msgid "Security Reports|Dismiss vulnerability"
+msgid "SecurityConfiguration|Not yet configured"
msgstr ""
-msgid "Security Reports|Dismissed '%{vulnerabilityName}'"
+msgid "SecurityConfiguration|Secure features"
msgstr ""
-msgid "Security Reports|Dismissed '%{vulnerabilityName}'. Turn off the hide dismissed toggle to view."
+msgid "SecurityConfiguration|Status"
msgstr ""
-msgid "Security Reports|Either you don't have permission to view this dashboard or the dashboard has not been setup. Please check your permission settings with your administrator or check your dashboard configurations to proceed."
+msgid "SecurityReports|%{firstProject} and %{secondProject}"
msgstr ""
-msgid "Security Reports|Learn more about setting up your dashboard"
+msgid "SecurityReports|%{firstProject}, %{secondProject}, and %{rest}"
msgstr ""
-msgid "Security Reports|More info"
+msgid "SecurityReports|Add a project to your dashboard"
msgstr ""
-msgid "Security Reports|Oops, something doesn't seem right."
+msgid "SecurityReports|Add or remove projects from your dashboard"
msgstr ""
-msgid "Security Reports|Security reports can only be accessed by authorized users."
+msgid "SecurityReports|Add projects"
msgstr ""
-msgid "Security Reports|There was an error adding the comment."
+msgid "SecurityReports|Comment added to '%{vulnerabilityName}'"
msgstr ""
-msgid "Security Reports|There was an error creating the issue."
+msgid "SecurityReports|Comment deleted on '%{vulnerabilityName}'"
msgstr ""
-msgid "Security Reports|There was an error creating the merge request."
+msgid "SecurityReports|Comment edited on '%{vulnerabilityName}'"
msgstr ""
-msgid "Security Reports|There was an error deleting the comment."
+msgid "SecurityReports|Create issue"
msgstr ""
-msgid "Security Reports|There was an error dismissing the vulnerabilities."
+msgid "SecurityReports|Dismiss Selected"
msgstr ""
-msgid "Security Reports|There was an error dismissing the vulnerability."
+msgid "SecurityReports|Dismiss vulnerability"
msgstr ""
-msgid "Security Reports|There was an error reverting the dismissal."
+msgid "SecurityReports|Dismissed '%{vulnerabilityName}'"
msgstr ""
-msgid "Security Reports|There was an error reverting this dismissal."
+msgid "SecurityReports|Dismissed '%{vulnerabilityName}'. Turn off the hide dismissed toggle to view."
msgstr ""
-msgid "Security Reports|Undo dismiss"
+msgid "SecurityReports|Each vulnerability now has a unique page that can be directly linked to, shared, referenced, and tracked as the single source of truth. Vulnerability occurrences also persist across scanner runs, which improves tracking and visibility and reduces duplicates between scans."
msgstr ""
-msgid "Security Reports|You do not have sufficient permissions to access this report"
+msgid "SecurityReports|Edit dashboard"
msgstr ""
-msgid "Security Reports|You must sign in as an authorized user to see this report"
+msgid "SecurityReports|Either you don't have permission to view this dashboard or the dashboard has not been setup. Please check your permission settings with your administrator or check your dashboard configurations to proceed."
msgstr ""
-msgid "Security configuration help link"
+msgid "SecurityReports|Error fetching the vulnerability counts. Please check your network connection and try again."
msgstr ""
-msgid "Security dashboard"
+msgid "SecurityReports|Error fetching the vulnerability list. Please check your network connection and try again."
msgstr ""
-msgid "Security report is out of date. Please update your branch with the latest changes from the target branch (%{targetBranchName})"
+msgid "SecurityReports|False positive"
msgstr ""
-msgid "Security report is out of date. Run %{newPipelineLinkStart}a new pipeline%{newPipelineLinkEnd} for the target branch (%{targetBranchName})"
+msgid "SecurityReports|Hide dismissed"
msgstr ""
-msgid "SecurityConfiguration|Configured"
+msgid "SecurityReports|Introducing standalone vulnerabilities"
msgstr ""
-msgid "SecurityConfiguration|Feature"
+msgid "SecurityReports|Issue Created"
msgstr ""
-msgid "SecurityConfiguration|Feature documentation for %{featureName}"
+msgid "SecurityReports|Learn More"
msgstr ""
-msgid "SecurityConfiguration|Not yet configured"
+msgid "SecurityReports|Learn more about setting up your dashboard"
msgstr ""
-msgid "SecurityConfiguration|Secure features"
+msgid "SecurityReports|Load more vulnerabilities"
msgstr ""
-msgid "SecurityConfiguration|Status"
+msgid "SecurityReports|Monitor vulnerabilities in your code"
+msgstr ""
+
+msgid "SecurityReports|More info"
+msgstr ""
+
+msgid "SecurityReports|More information"
+msgstr ""
+
+msgid "SecurityReports|No vulnerabilities found for dashboard"
+msgstr ""
+
+msgid "SecurityReports|No vulnerabilities found for this group"
+msgstr ""
+
+msgid "SecurityReports|No vulnerabilities found for this pipeline"
+msgstr ""
+
+msgid "SecurityReports|No vulnerabilities found for this project"
+msgstr ""
+
+msgid "SecurityReports|Oops, something doesn't seem right."
+msgstr ""
+
+msgid "SecurityReports|Pipeline %{pipelineLink} triggered %{timeago} by %{user}"
+msgstr ""
+
+msgid "SecurityReports|Project"
msgstr ""
-msgid "SecurityDashboard|%{firstProject} and %{secondProject}"
+msgid "SecurityReports|Projects added"
msgstr ""
-msgid "SecurityDashboard|%{firstProject}, %{secondProject}, and %{rest}"
+msgid "SecurityReports|Remove project from dashboard"
msgstr ""
-msgid "SecurityDashboard|Add a project to your dashboard"
+msgid "SecurityReports|Report type"
msgstr ""
-msgid "SecurityDashboard|Add or remove projects from your dashboard"
+msgid "SecurityReports|Return to dashboard"
msgstr ""
-msgid "SecurityDashboard|Add projects"
+msgid "SecurityReports|Security Dashboard"
msgstr ""
-msgid "SecurityDashboard|Edit dashboard"
+msgid "SecurityReports|Security reports can only be accessed by authorized users."
msgstr ""
-msgid "SecurityDashboard|Hide dismissed"
+msgid "SecurityReports|Select a project to add by using the project search field above."
msgstr ""
-msgid "SecurityDashboard|Monitor vulnerabilities in your code"
+msgid "SecurityReports|Select a reason"
msgstr ""
-msgid "SecurityDashboard|More information"
+msgid "SecurityReports|Severity"
msgstr ""
-msgid "SecurityDashboard|No vulnerabilities found for dashboard"
+msgid "SecurityReports|Status"
msgstr ""
-msgid "SecurityDashboard|Pipeline %{pipelineLink} triggered %{timeago} by %{user}"
+msgid "SecurityReports|The security dashboard displays the latest security findings for projects you wish to monitor. Select \"Edit dashboard\" to add and remove projects."
msgstr ""
-msgid "SecurityDashboard|Project"
+msgid "SecurityReports|The security dashboard displays the latest security report. Use it to find and fix vulnerabilities."
msgstr ""
-msgid "SecurityDashboard|Projects added"
+msgid "SecurityReports|There was an error adding the comment."
msgstr ""
-msgid "SecurityDashboard|Remove project from dashboard"
+msgid "SecurityReports|There was an error creating the issue."
msgstr ""
-msgid "SecurityDashboard|Report type"
+msgid "SecurityReports|There was an error creating the merge request."
msgstr ""
-msgid "SecurityDashboard|Return to dashboard"
+msgid "SecurityReports|There was an error deleting the comment."
msgstr ""
-msgid "SecurityDashboard|Security Dashboard"
+msgid "SecurityReports|There was an error dismissing the vulnerabilities."
msgstr ""
-msgid "SecurityDashboard|Select a project to add by using the project search field above."
+msgid "SecurityReports|There was an error dismissing the vulnerability."
msgstr ""
-msgid "SecurityDashboard|Severity"
+msgid "SecurityReports|There was an error reverting the dismissal."
msgstr ""
-msgid "SecurityDashboard|Status"
+msgid "SecurityReports|There was an error reverting this dismissal."
msgstr ""
-msgid "SecurityDashboard|The security dashboard displays the latest security findings for projects you wish to monitor. Select \"Edit dashboard\" to add and remove projects."
+msgid "SecurityReports|There was an error while generating the report."
msgstr ""
-msgid "SecurityDashboard|The security dashboard displays the latest security report. Use it to find and fix vulnerabilities."
+msgid "SecurityReports|Unable to add %{invalidProjects}"
msgstr ""
-msgid "SecurityDashboard|There was an error while generating the report."
+msgid "SecurityReports|Undo dismiss"
msgstr ""
-msgid "SecurityDashboard|Unable to add %{invalidProjects}"
+msgid "SecurityReports|While it's rare to have no vulnerabilities for your group, it can happen. In any event, we ask that you double check your settings to make sure you've set up your dashboard correctly."
+msgstr ""
+
+msgid "SecurityReports|While it's rare to have no vulnerabilities for your pipeline, it can happen. In any event, we ask that you double check your settings to make sure all security scanning jobs have passed successfully."
+msgstr ""
+
+msgid "SecurityReports|While it's rare to have no vulnerabilities for your project, it can happen. In any event, we ask that you double check your settings to make sure you've set up your dashboard correctly."
+msgstr ""
+
+msgid "SecurityReports|While it's rare to have no vulnerabilities, it can happen. In any event, we ask that you please double check your settings to make sure you've set up your dashboard correctly."
+msgstr ""
+
+msgid "SecurityReports|Won't fix / Accept risk"
+msgstr ""
+
+msgid "SecurityReports|You do not have sufficient permissions to access this report"
+msgstr ""
+
+msgid "SecurityReports|You must sign in as an authorized user to see this report"
+msgstr ""
+
+msgid "SecurityReports|[No reason]"
msgstr ""
msgid "See GitLab's %{password_policy_guidelines}"
@@ -18173,6 +18599,9 @@ msgstr ""
msgid "See the affected projects in the GitLab admin panel"
msgstr ""
+msgid "See what's new at GitLab"
+msgstr ""
+
msgid "Select"
msgstr ""
@@ -18578,9 +19007,6 @@ msgstr ""
msgid "Set up Jira Integration"
msgstr ""
-msgid "Set up Jira Integration illustration"
-msgstr ""
-
msgid "Set up a %{type} Runner automatically"
msgstr ""
@@ -18665,6 +19091,9 @@ msgstr ""
msgid "Severity: %{severity}"
msgstr ""
+msgid "Shards (%{shards})"
+msgstr ""
+
msgid "Shards to synchronize"
msgstr ""
@@ -18980,6 +19409,9 @@ msgstr ""
msgid "Snowplow"
msgstr ""
+msgid "Some child epics may be hidden due to applied filters"
+msgstr ""
+
msgid "Some email servers do not support overriding the email sender name. Enable this option to include the name of the author of the issue, merge request or comment in the email body instead."
msgstr ""
@@ -19121,6 +19553,9 @@ msgstr ""
msgid "Something went wrong while updating your list settings"
msgstr ""
+msgid "Something went wrong with your automatic subscription renewal"
+msgstr ""
+
msgid "Something went wrong, unable to add %{project} to dashboard"
msgstr ""
@@ -19406,6 +19841,9 @@ msgstr ""
msgid "Stage removed"
msgstr ""
+msgid "Standard"
+msgstr ""
+
msgid "Star a label to make it a priority label. Order the prioritized labels to change their relative priority, by dragging."
msgstr ""
@@ -19529,6 +19967,9 @@ msgstr ""
msgid "Static Application Security Testing (SAST)"
msgstr ""
+msgid "StaticSiteEditor|An error occurred while submitting your changes."
+msgstr ""
+
msgid "StaticSiteEditor|Branch could not be created."
msgstr ""
@@ -19538,18 +19979,30 @@ msgstr ""
msgid "StaticSiteEditor|Could not create merge request."
msgstr ""
+msgid "StaticSiteEditor|Incompatible file content"
+msgstr ""
+
msgid "StaticSiteEditor|Return to site"
msgstr ""
+msgid "StaticSiteEditor|Static site editor"
+msgstr ""
+
msgid "StaticSiteEditor|Success!"
msgstr ""
msgid "StaticSiteEditor|Summary of changes"
msgstr ""
+msgid "StaticSiteEditor|The Static Site Editor is currently configured to only edit Markdown content on pages generated from Middleman. Visit the documentation to learn more about configuring your site to use the Static Site Editor."
+msgstr ""
+
msgid "StaticSiteEditor|Update %{sourcePath} file"
msgstr ""
+msgid "StaticSiteEditor|View documentation"
+msgstr ""
+
msgid "StaticSiteEditor|View merge request"
msgstr ""
@@ -19607,6 +20060,12 @@ msgstr ""
msgid "StatusPage|Status page"
msgstr ""
+msgid "StatusPage|Status page URL"
+msgstr ""
+
+msgid "StatusPage|Status page frontend documentation"
+msgstr ""
+
msgid "StatusPage|To publish incidents to an external status page, GitLab will store a JSON file in your Amazon S3 account in a location accessible to your external status page service. Make sure to also set up %{docsLink}"
msgstr ""
@@ -19929,7 +20388,7 @@ msgid "Summary"
msgstr ""
msgid "Sunday"
-msgstr ""
+msgstr "Nedjelja"
msgid "Support"
msgstr ""
@@ -19955,6 +20414,12 @@ msgstr ""
msgid "Synced"
msgstr ""
+msgid "Synchronization disabled"
+msgstr ""
+
+msgid "Synchronization of container repositories is disabled."
+msgstr ""
+
msgid "System"
msgstr ""
@@ -20108,6 +20573,9 @@ msgstr ""
msgid "Telephone number"
msgstr ""
+msgid "Telephone number (Optional)"
+msgstr ""
+
msgid "Template"
msgstr ""
@@ -20417,6 +20885,9 @@ msgstr ""
msgid "The license was successfully uploaded and is now active. You can see the details below."
msgstr ""
+msgid "The license was successfully uploaded and will be active from %{starts_at}. You can see the details below."
+msgstr ""
+
msgid "The maximum file size allowed is %{size}."
msgstr ""
@@ -20738,6 +21209,9 @@ msgstr ""
msgid "There was an error getting the epic participants."
msgstr ""
+msgid "There was an error importing the Jira project."
+msgstr ""
+
msgid "There was an error loading users activity calendar."
msgstr ""
@@ -20753,7 +21227,7 @@ msgstr ""
msgid "There was an error resetting user pipeline minutes."
msgstr ""
-msgid "There was an error saving this Geo Node"
+msgid "There was an error saving this Geo Node."
msgstr ""
msgid "There was an error saving your changes."
@@ -20816,6 +21290,9 @@ msgstr ""
msgid "They can be managed using the %{link}."
msgstr ""
+msgid "Third Party Advisory Link"
+msgstr ""
+
msgid "Third party offers"
msgstr ""
@@ -20852,6 +21329,9 @@ msgstr ""
msgid "This application will be able to:"
msgstr ""
+msgid "This attachment has been truncated to avoid exceeding the maximum allowed attachment size of 15MB. %{written_count} of %{issues_count} issues have been included. Consider re-exporting with a narrower selection of issues."
+msgstr ""
+
msgid "This block is self-referential"
msgstr ""
@@ -20993,6 +21473,12 @@ msgstr ""
msgid "This issue is confidential"
msgstr ""
+msgid "This issue is currently blocked by the following issues: %{issues}."
+msgstr ""
+
+msgid "This issue is in a child epic of the filtered epic"
+msgstr ""
+
msgid "This issue is locked."
msgstr ""
@@ -21191,7 +21677,10 @@ msgstr ""
msgid "This user will be the author of all events in the activity feed that are the result of an update, like new branches being created or new commits being pushed to existing branches. Upon creation or when reassigning you can only assign yourself to be the mirror user."
msgstr ""
-msgid "This variable can not be masked"
+msgid "This variable can not be masked."
+msgstr ""
+
+msgid "This variable does not match the expected pattern."
msgstr ""
msgid "This will help us personalize your onboarding experience."
@@ -21221,10 +21710,10 @@ msgstr ""
msgid "ThreatMonitoring|Application firewall not detected"
msgstr ""
-msgid "ThreatMonitoring|Container Network Policy"
+msgid "ThreatMonitoring|Container Network Policies are not installed or have been disabled. To view this data, ensure your Network Policies are installed and enabled for your cluster."
msgstr ""
-msgid "ThreatMonitoring|Container NetworkPolicies are not installed or has been disabled. To view this data, ensure you NetworkPolicies are installed and enabled for your cluster."
+msgid "ThreatMonitoring|Container Network Policy"
msgstr ""
msgid "ThreatMonitoring|Container NetworkPolicies not detected"
@@ -21257,7 +21746,7 @@ msgstr ""
msgid "ThreatMonitoring|Something went wrong, unable to fetch statistics"
msgstr ""
-msgid "ThreatMonitoring|The firewall is not installed or has been disabled. To view this data, ensure you firewall is installed and enabled for your cluster."
+msgid "ThreatMonitoring|The firewall is not installed or has been disabled. To view this data, ensure the web application firewall is installed and enabled for your cluster."
msgstr ""
msgid "ThreatMonitoring|The graph below is an overview of traffic coming to your application as tracked by the Web Application Firewall (WAF). View the docs for instructions on how to access the WAF logs to see what type of malicious traffic is trying to access your app. The docs link is also accessible by clicking the \"?\" icon next to the title below."
@@ -21291,7 +21780,7 @@ msgid "ThreatMonitoring|While it's rare to have no traffic coming to your applic
msgstr ""
msgid "Thursday"
-msgstr ""
+msgstr "ÄŒetvrtak"
msgid "Time"
msgstr ""
@@ -21326,6 +21815,9 @@ msgstr ""
msgid "Time in seconds GitLab will wait for a response from the external service. When the service does not respond in time, access will be denied."
msgstr ""
+msgid "Time of import: %{importTime}"
+msgstr ""
+
msgid "Time remaining"
msgstr ""
@@ -21363,10 +21855,10 @@ msgid "TimeTracking|Time remaining: %{timeRemainingHumanReadable}"
msgstr ""
msgid "Timeago|%s days ago"
-msgstr ""
+msgstr "prije %s dana"
msgid "Timeago|%s days remaining"
-msgstr ""
+msgstr "preostalo %s dana"
msgid "Timeago|%s hours ago"
msgstr ""
@@ -21375,37 +21867,37 @@ msgid "Timeago|%s hours remaining"
msgstr ""
msgid "Timeago|%s minutes ago"
-msgstr ""
+msgstr "prije %s min"
msgid "Timeago|%s minutes remaining"
-msgstr ""
+msgstr "preostalo %s min"
msgid "Timeago|%s months ago"
-msgstr ""
+msgstr "prije %s mjeseci"
msgid "Timeago|%s months remaining"
-msgstr ""
+msgstr "preostalo %s mjeseci"
msgid "Timeago|%s seconds remaining"
-msgstr ""
+msgstr "preostalo %s s"
msgid "Timeago|%s weeks ago"
-msgstr ""
+msgstr "prije %s sedmica"
msgid "Timeago|%s weeks remaining"
-msgstr ""
+msgstr "preostalo %s sedmica"
msgid "Timeago|%s years ago"
-msgstr ""
+msgstr "prije %s godina"
msgid "Timeago|%s years remaining"
-msgstr ""
+msgstr "preostalo %s godina"
msgid "Timeago|1 day ago"
-msgstr ""
+msgstr "prije 1 dan"
msgid "Timeago|1 day remaining"
-msgstr ""
+msgstr "preostalo 1 dan"
msgid "Timeago|1 hour ago"
msgstr ""
@@ -21414,73 +21906,73 @@ msgid "Timeago|1 hour remaining"
msgstr ""
msgid "Timeago|1 minute ago"
-msgstr ""
+msgstr "prije 1 min"
msgid "Timeago|1 minute remaining"
-msgstr ""
+msgstr "preostalo 1 min"
msgid "Timeago|1 month ago"
-msgstr ""
+msgstr "prije 1 mjesec"
msgid "Timeago|1 month remaining"
-msgstr ""
+msgstr "preostalo 1 mjesec"
msgid "Timeago|1 week ago"
-msgstr ""
+msgstr "prije 1 sedmice"
msgid "Timeago|1 week remaining"
-msgstr ""
+msgstr "preostalo 1 sedmica"
msgid "Timeago|1 year ago"
-msgstr ""
+msgstr "prije 1 godine"
msgid "Timeago|1 year remaining"
-msgstr ""
+msgstr "preostalo 1 godina"
msgid "Timeago|Past due"
msgstr ""
msgid "Timeago|in %s days"
-msgstr ""
+msgstr "za %s dana"
msgid "Timeago|in %s hours"
msgstr ""
msgid "Timeago|in %s minutes"
-msgstr ""
+msgstr "za %s min"
msgid "Timeago|in %s months"
-msgstr ""
+msgstr "za %s mjeseci"
msgid "Timeago|in %s seconds"
-msgstr ""
+msgstr "za %s s"
msgid "Timeago|in %s weeks"
-msgstr ""
+msgstr "za %s sedmica"
msgid "Timeago|in %s years"
-msgstr ""
+msgstr "za %s godina"
msgid "Timeago|in 1 day"
-msgstr ""
+msgstr "za 1 dan"
msgid "Timeago|in 1 hour"
msgstr ""
msgid "Timeago|in 1 minute"
-msgstr ""
+msgstr "za 1 min"
msgid "Timeago|in 1 month"
-msgstr ""
+msgstr "za 1 mjesec"
msgid "Timeago|in 1 week"
-msgstr ""
+msgstr "za 1 sedmicu"
msgid "Timeago|in 1 year"
-msgstr ""
+msgstr "za 1 godinu"
msgid "Timeago|just now"
-msgstr ""
+msgstr "maloprije"
msgid "Timeago|right now"
msgstr ""
@@ -21512,7 +22004,7 @@ msgstr ""
msgid "Title:"
msgstr ""
-msgid "Titles and Filenames"
+msgid "Titles and Descriptions"
msgstr ""
msgid "To"
@@ -21648,7 +22140,7 @@ msgid "To-do item successfully marked as done."
msgstr ""
msgid "Today"
-msgstr ""
+msgstr "Danas"
msgid "Toggle Markdown preview"
msgstr ""
@@ -21719,7 +22211,7 @@ msgstr ""
msgid "Too many projects enabled. You will need to manage them via the console or the API."
msgstr ""
-msgid "Topics"
+msgid "Topics (optional)"
msgstr ""
msgid "Total"
@@ -21879,6 +22371,9 @@ msgid "Try again?"
msgstr ""
msgid "Try all GitLab has to offer for 30 days."
+msgstr "Isprobajte sve što GitLab može ponuditi za 30 dana."
+
+msgid "Try changing or removing filters."
msgstr ""
msgid "Try to fork again"
@@ -21891,7 +22386,7 @@ msgid "Trying to communicate with your device. Plug it in (if you haven't alread
msgstr ""
msgid "Tuesday"
-msgstr ""
+msgstr "Utorak"
msgid "Turn Off"
msgstr ""
@@ -21974,6 +22469,9 @@ msgstr ""
msgid "Unable to connect to server: %{error}"
msgstr ""
+msgid "Unable to connect to the Jira instance. Please check your Jira integration configuration."
+msgstr ""
+
msgid "Unable to convert Kubernetes logs encoding to UTF-8"
msgstr ""
@@ -22919,6 +23417,9 @@ msgstr ""
msgid "View issue"
msgstr ""
+msgid "View issues"
+msgstr ""
+
msgid "View it on GitLab"
msgstr ""
@@ -23096,6 +23597,15 @@ msgstr ""
msgid "VulnerabilityManagement|Resolved %{timeago} by %{user}"
msgstr ""
+msgid "VulnerabilityManagement|Something went wrong while trying to delete the comment. Please try again later."
+msgstr ""
+
+msgid "VulnerabilityManagement|Something went wrong while trying to retrieve the vulnerability history. Please try again later."
+msgstr ""
+
+msgid "VulnerabilityManagement|Something went wrong while trying to save the comment. Please try again later."
+msgstr ""
+
msgid "VulnerabilityManagement|Something went wrong, could not create an issue."
msgstr ""
@@ -23126,6 +23636,9 @@ msgstr ""
msgid "VulnerabilityStatusTypes|Resolved"
msgstr ""
+msgid "Vulnerability|%{scannerName} (version %{scannerVersion})"
+msgstr ""
+
msgid "Vulnerability|Class"
msgstr ""
@@ -23159,7 +23672,10 @@ msgstr ""
msgid "Vulnerability|Project"
msgstr ""
-msgid "Vulnerability|Report Type"
+msgid "Vulnerability|Scanner Provider"
+msgstr ""
+
+msgid "Vulnerability|Scanner Type"
msgstr ""
msgid "Vulnerability|Severity"
@@ -23213,9 +23729,15 @@ msgstr ""
msgid "We sent you an email with reset password instructions"
msgstr ""
+msgid "We tried to automatically renew your %{strong}%{plan_name}%{strong_close} subscription for %{strong}%{namespace_name}%{strong_close} on %{expires_on} but something went wrong so your subscription was downgraded to the free plan. Don't worry, your data is safe. We suggest you check your payment method and get in touch with our support team (%{support_link}). They'll gladly help with your subscription renewal."
+msgstr ""
+
msgid "We want to be sure it is you, please confirm you are not a robot."
msgstr ""
+msgid "We will automatically renew your %{strong}%{plan_name}%{strong_close} subscription for %{strong}%{namespace_name}%{strong_close} on %{strong}%{expires_on}%{strong_close}. There's nothing that you need to do, we'll let you know when the renewal is complete. Need more seats, a higher plan or just want to review your payment method?"
+msgstr ""
+
msgid "We've found no vulnerabilities"
msgstr ""
@@ -23253,19 +23775,19 @@ msgid "Webhooks have moved. They can now be found under the Settings menu."
msgstr ""
msgid "Wednesday"
-msgstr ""
+msgstr "Srijeda"
msgid "Weekday"
-msgstr ""
+msgstr "Radni dan"
msgid "Weeks"
-msgstr ""
+msgstr "Sedmice"
msgid "Weight"
-msgstr ""
+msgstr "Težina"
msgid "Weight %{weight}"
-msgstr ""
+msgstr "Težina %{weight}"
msgid "Welcome back! Your account had been deactivated due to inactivity but is now reactivated."
msgstr ""
@@ -23321,15 +23843,6 @@ msgstr ""
msgid "When:"
msgstr ""
-msgid "While it's rare to have no vulnerabilities for your group, it can happen. In any event, we ask that you double check your settings to make sure you've set up your dashboard correctly."
-msgstr ""
-
-msgid "While it's rare to have no vulnerabilities for your pipeline, it can happen. In any event, we ask that you double check your settings to make sure all security scanning jobs have passed successfully."
-msgstr ""
-
-msgid "While it's rare to have no vulnerabilities for your project, it can happen. In any event, we ask that you double check your settings to make sure you've set up your dashboard correctly."
-msgstr ""
-
msgid "While it's rare to have no vulnerabilities, it can happen. In any event, we ask that you please double check your settings to make sure you've set up your dashboard correctly."
msgstr ""
@@ -23360,9 +23873,6 @@ msgstr ""
msgid "Wiki"
msgstr ""
-msgid "Wiki pages"
-msgstr ""
-
msgid "Wiki was successfully updated."
msgstr ""
@@ -23546,11 +24056,14 @@ msgstr ""
msgid "Yes, add it"
msgstr ""
+msgid "Yes, close issue"
+msgstr ""
+
msgid "Yes, let me map Google Code users to full names or GitLab users."
msgstr ""
msgid "Yesterday"
-msgstr ""
+msgstr "JuÄer"
msgid "You"
msgstr ""
@@ -23741,6 +24254,9 @@ msgstr ""
msgid "You could not create a new trigger."
msgstr ""
+msgid "You didn't renew your %{strong}%{plan_name}%{strong_close} subscription for %{strong}%{namespace_name}%{strong_close} so it was downgraded to the free plan."
+msgstr ""
+
msgid "You didn't renew your %{strong}%{plan_name}%{strong_close} subscription so it was downgraded to the GitLab Core Plan."
msgstr ""
@@ -23807,6 +24323,9 @@ msgstr ""
msgid "You have declined the invitation to join %{label}."
msgstr ""
+msgid "You have imported from this project %{numberOfPreviousImportsForProject} times before. Each new import will create duplicate issues."
+msgstr ""
+
msgid "You have no permissions"
msgstr ""
@@ -23888,6 +24407,9 @@ msgstr ""
msgid "You need to upload a Google Takeout archive."
msgstr ""
+msgid "You reached %{usage_in_percent} of %{namespace_name}'s capacity (%{used_storage} of %{storage_limit})"
+msgstr ""
+
msgid "You tried to fork %{link_to_the_project} but it failed for the following reason:"
msgstr ""
@@ -23972,9 +24494,21 @@ msgstr ""
msgid "YouTube"
msgstr ""
+msgid "Your %{strong}%{plan_name}%{strong_close} subscription for %{strong}%{namespace_name}%{strong_close} will expire on %{strong}%{expires_on}%{strong_close}. After that, you will not to be able to create issues or merge requests as well as many other features."
+msgstr ""
+
msgid "Your %{strong}%{plan_name}%{strong_close} subscription will expire on %{strong}%{expires_on}%{strong_close}. After that, you will not to be able to create issues or merge requests as well as many other features."
msgstr ""
+msgid "Your CSV export has started. It will be emailed to %{email} when complete."
+msgstr ""
+
+msgid "Your CSV export of %{issues_count} from project %{project_link} has been added to this email as an attachment."
+msgstr ""
+
+msgid "Your CSV export of %{written_count} from project %{project_name} (%{project_url}) has been added to this email as an attachment."
+msgstr ""
+
msgid "Your Commit Email will be used for web based operations, such as edits and merges."
msgstr ""
@@ -24071,6 +24605,9 @@ msgstr ""
msgid "Your comment could not be updated! Please check your network connection and try again."
msgstr ""
+msgid "Your comment will be discarded."
+msgstr ""
+
msgid "Your custom stage '%{title}' was created"
msgstr ""
@@ -24110,6 +24647,9 @@ msgstr ""
msgid "Your new personal access token has been created."
msgstr ""
+msgid "Your new project access token has been created."
+msgstr ""
+
msgid "Your password isn't required to view this page. If a password or any other personal details are requested, please contact your administrator to report abuse."
msgstr ""
@@ -24128,12 +24668,18 @@ msgstr ""
msgid "Your request for access has been queued for review."
msgstr ""
+msgid "Your search didn't match any commits."
+msgstr ""
+
msgid "Your subscription expired!"
msgstr ""
msgid "Your subscription has been downgraded"
msgstr ""
+msgid "Your subscription will automatically renew in %{remaining_days}"
+msgstr ""
+
msgid "Your subscription will expire in %{remaining_days}"
msgstr ""
@@ -24173,7 +24719,7 @@ msgstr ""
msgid "allowed to fail"
msgstr ""
-msgid "already being used for another group or project milestone."
+msgid "already being used for another group or project %{timebox_name}."
msgstr ""
msgid "already has a \"created\" issue link"
@@ -24251,6 +24797,9 @@ msgstr ""
msgid "ciReport|%{linkStartTag}Learn more about SAST %{linkEndTag}"
msgstr ""
+msgid "ciReport|%{linkStartTag}Learn more about Secret Scanning %{linkEndTag}"
+msgstr ""
+
msgid "ciReport|%{linkStartTag}Learn more about codequality reports %{linkEndTag}"
msgstr ""
@@ -24419,6 +24968,12 @@ msgstr ""
msgid "ciReport|SAST"
msgstr ""
+msgid "ciReport|Secret scanning"
+msgstr ""
+
+msgid "ciReport|Secret scanning detects secrets and credentials vulnerabilities in your source code."
+msgstr ""
+
msgid "ciReport|Security scanning"
msgstr ""
@@ -24493,9 +25048,9 @@ msgstr ""
msgid "day"
msgid_plural "days"
-msgstr[0] ""
-msgstr[1] ""
-msgstr[2] ""
+msgstr[0] "dan"
+msgstr[1] "dana"
+msgstr[2] "dana"
msgid "default branch"
msgstr ""
@@ -24620,6 +25175,9 @@ msgstr ""
msgid "group"
msgstr ""
+msgid "groups"
+msgstr ""
+
msgid "has already been linked to another vulnerability"
msgstr ""
@@ -24753,7 +25311,7 @@ msgid "leave %{group_name}"
msgstr ""
msgid "less than a minute"
-msgstr ""
+msgstr "manje od minute"
msgid "level: %{level}"
msgstr ""
@@ -24785,9 +25343,6 @@ msgstr[2] ""
msgid "merged %{time_ago}"
msgstr ""
-msgid "milestone should belong either to a project or a group."
-msgstr ""
-
msgid "missing"
msgstr ""
@@ -25124,6 +25679,9 @@ msgstr ""
msgid "no contributions"
msgstr ""
+msgid "no expiration"
+msgstr ""
+
msgid "no one can merge"
msgstr ""
@@ -25178,6 +25736,9 @@ msgstr ""
msgid "pending removal"
msgstr ""
+msgid "per day"
+msgstr ""
+
msgid "pipeline"
msgstr ""
@@ -25208,6 +25769,12 @@ msgstr[2] ""
msgid "project avatar"
msgstr ""
+msgid "projects"
+msgstr ""
+
+msgid "push to your repository, create pipelines, create issues or add comments. To reduce storage capacity, delete unused repositories, artifacts, wikis, issues, and pipelines."
+msgstr ""
+
msgid "quick actions"
msgstr ""
@@ -25230,7 +25797,7 @@ msgid "remove due date"
msgstr ""
msgid "remove weight"
-msgstr ""
+msgstr "ukloni težinu"
msgid "removed a Zoom call from this issue"
msgstr ""
@@ -25355,12 +25922,6 @@ msgstr ""
msgid "this document"
msgstr ""
-msgid "thread resolved"
-msgid_plural "threads resolved"
-msgstr[0] ""
-msgstr[1] ""
-msgstr[2] ""
-
msgid "to help your contributors communicate effectively!"
msgstr ""
@@ -25415,6 +25976,9 @@ msgstr ""
msgid "view the blob"
msgstr ""
+msgid "vulnerability|Add a comment"
+msgstr ""
+
msgid "vulnerability|Add a comment or reason for dismissal"
msgstr ""
diff --git a/locale/ca_ES/gitlab.po b/locale/ca_ES/gitlab.po
index 0ce8ec901fb..6f3785e8e5a 100644
--- a/locale/ca_ES/gitlab.po
+++ b/locale/ca_ES/gitlab.po
@@ -12,7 +12,7 @@ msgstr ""
"X-Crowdin-Project: gitlab-ee\n"
"X-Crowdin-Language: ca\n"
"X-Crowdin-File: /master/locale/gitlab.pot\n"
-"PO-Revision-Date: 2020-04-15 00:30\n"
+"PO-Revision-Date: 2020-05-05 21:15\n"
msgid " %{start} to %{end}"
msgstr ""
@@ -212,6 +212,11 @@ msgid_plural "%d tags"
msgstr[0] ""
msgstr[1] ""
+msgid "%d unresolved thread"
+msgid_plural "%d unresolved threads"
+msgstr[0] ""
+msgstr[1] ""
+
msgid "%d vulnerability dismissed"
msgid_plural "%d vulnerabilities dismissed"
msgstr[0] ""
@@ -371,6 +376,9 @@ msgstr ""
msgid "%{mrText}, this issue will be closed automatically."
msgstr ""
+msgid "%{namespace_name} is now read-only. You cannot: %{base_message}"
+msgstr ""
+
msgid "%{name} contained %{resultsString}"
msgstr ""
@@ -383,6 +391,11 @@ msgstr ""
msgid "%{name}'s avatar"
msgstr ""
+msgid "%{no_of_days} day"
+msgid_plural "%{no_of_days} days"
+msgstr[0] ""
+msgstr[1] ""
+
msgid "%{number_commits_behind} commits behind %{default_branch}, %{number_commits_ahead} commits ahead"
msgstr ""
@@ -506,6 +519,9 @@ msgstr[1] "%{text} %{files} fitxers"
msgid "%{text} is available"
msgstr ""
+msgid "%{timebox_name} should belong either to a project or a group."
+msgstr ""
+
msgid "%{title} %{operator} %{threshold}"
msgstr ""
@@ -781,12 +797,18 @@ msgstr ""
msgid "<code>\"johnsmith@example.com\": \"johnsmith@example.com\"</code> will add \"By <a href=\"#\">johnsmith@example.com</a>\" to all issues and comments originally created by johnsmith@example.com. By default, the email address or username is masked to ensure the user's privacy. Use this option if you want to show the full email address."
msgstr ""
+msgid "<namespace / project>"
+msgstr ""
+
msgid "<no name set>"
msgstr ""
msgid "<no scopes selected>"
msgstr ""
+msgid "<project name>"
+msgstr ""
+
msgid "<strong>%{group_name}</strong> group members"
msgstr ""
@@ -820,6 +842,9 @@ msgstr ""
msgid "A basic page and serverless function that uses AWS Lambda, AWS API Gateway, and GitLab Pages"
msgstr ""
+msgid "A complete DevOps platform"
+msgstr ""
+
msgid "A default branch cannot be chosen for an empty project."
msgstr ""
@@ -1036,6 +1061,12 @@ msgstr ""
msgid "AccessTokens|reset it"
msgstr ""
+msgid "AccessibilityReport|Accessibility report artifact not found"
+msgstr ""
+
+msgid "AccessibilityReport|Failed to retrieve accessibility report"
+msgstr ""
+
msgid "AccessibilityReport|Learn More"
msgstr ""
@@ -1045,6 +1076,9 @@ msgstr ""
msgid "AccessibilityReport|New"
msgstr ""
+msgid "AccessibilityReport|The accessibility scanning found an error of the following type: %{code}"
+msgstr ""
+
msgid "Account"
msgstr "Compte"
@@ -1287,6 +1321,9 @@ msgstr ""
msgid "Added at"
msgstr ""
+msgid "Added for this merge request"
+msgstr ""
+
msgid "Added in this version"
msgstr ""
@@ -1658,6 +1695,72 @@ msgid_plural "Alerts"
msgstr[0] ""
msgstr[1] ""
+msgid "Alert Details"
+msgstr ""
+
+msgid "AlertManagement|Acknowledged"
+msgstr ""
+
+msgid "AlertManagement|Alert"
+msgstr ""
+
+msgid "AlertManagement|Authorize external service"
+msgstr ""
+
+msgid "AlertManagement|Display alerts from all your monitoring tools directly within GitLab. Streamline the investigation of your alerts and the escalation of alerts to incidents."
+msgstr ""
+
+msgid "AlertManagement|End time"
+msgstr ""
+
+msgid "AlertManagement|End time:"
+msgstr ""
+
+msgid "AlertManagement|Events"
+msgstr ""
+
+msgid "AlertManagement|Events:"
+msgstr ""
+
+msgid "AlertManagement|Full Alert Details"
+msgstr ""
+
+msgid "AlertManagement|More information"
+msgstr ""
+
+msgid "AlertManagement|No alerts available to display. If you think you're seeing this message in error, refresh the page."
+msgstr ""
+
+msgid "AlertManagement|No alerts to display."
+msgstr ""
+
+msgid "AlertManagement|Overview"
+msgstr ""
+
+msgid "AlertManagement|Resolved"
+msgstr ""
+
+msgid "AlertManagement|Severity"
+msgstr ""
+
+msgid "AlertManagement|Start time"
+msgstr ""
+
+msgid "AlertManagement|Start time:"
+msgstr ""
+
+msgid "AlertManagement|Status"
+msgstr ""
+
+msgid "AlertManagement|Surface alerts in GitLab"
+msgstr ""
+
+msgid "AlertManagement|There was an error displaying the alerts. Confirm your endpoint's configuration details to ensure alerts appear."
+msgstr ""
+
+msgid "AlertManagement|Triggered"
+msgstr ""
+
msgid "AlertService|%{linkStart}Learn more%{linkEnd} about configuring this endpoint to receive alerts."
msgstr ""
@@ -1727,6 +1830,9 @@ msgstr ""
msgid "All security scans are enabled because %{linkStart}Auto DevOps%{linkEnd} is enabled on this project"
msgstr ""
+msgid "All threads resolved"
+msgstr ""
+
msgid "All users"
msgstr "Tots els usuaris"
@@ -1745,6 +1851,9 @@ msgstr ""
msgid "Allow only the selected protocols to be used for Git access."
msgstr ""
+msgid "Allow owners to manage default branch protection per group"
+msgstr ""
+
msgid "Allow owners to manually add users outside of LDAP"
msgstr ""
@@ -1853,6 +1962,9 @@ msgstr ""
msgid "An error occurred fetching the dropdown data."
msgstr ""
+msgid "An error occurred fetching the project authors."
+msgstr ""
+
msgid "An error occurred previewing the blob"
msgstr ""
@@ -1934,6 +2046,9 @@ msgstr ""
msgid "An error occurred while fetching sidebar data"
msgstr ""
+msgid "An error occurred while fetching terraform reports."
+msgstr ""
+
msgid "An error occurred while fetching the Service Desk address."
msgstr ""
@@ -2171,6 +2286,9 @@ msgstr "Verificació contra el correu brossa"
msgid "Any"
msgstr "Qualsevol"
+msgid "Any Author"
+msgstr ""
+
msgid "Any Label"
msgstr "Qualsevol etiqueta"
@@ -2386,10 +2504,10 @@ msgstr ""
msgid "Are you sure that you want to unarchive this project?"
msgstr ""
-msgid "Are you sure you want to cancel creating this comment?"
+msgid "Are you sure you want to cancel editing this comment?"
msgstr ""
-msgid "Are you sure you want to cancel editing this comment?"
+msgid "Are you sure you want to close this blocked issue?"
msgstr ""
msgid "Are you sure you want to delete %{name}?"
@@ -2419,6 +2537,9 @@ msgstr ""
msgid "Are you sure you want to deploy this environment?"
msgstr ""
+msgid "Are you sure you want to discard this comment?"
+msgstr ""
+
msgid "Are you sure you want to erase this build?"
msgstr ""
@@ -2595,6 +2716,9 @@ msgstr ""
msgid "At least one approval from a code owner is required to change files matching the respective CODEOWNER rules."
msgstr ""
+msgid "At least one logging option is required to be enabled"
+msgstr ""
+
msgid "At least one of group_id or project_id must be specified"
msgstr ""
@@ -2633,6 +2757,27 @@ msgstr ""
msgid "AuditEvents|Target"
msgstr ""
+msgid "AuditLogs|(removed)"
+msgstr ""
+
+msgid "AuditLogs|Action"
+msgstr ""
+
+msgid "AuditLogs|Author"
+msgstr ""
+
+msgid "AuditLogs|Date"
+msgstr ""
+
+msgid "AuditLogs|IP Address"
+msgstr ""
+
+msgid "AuditLogs|Object"
+msgstr ""
+
+msgid "AuditLogs|Target"
+msgstr ""
+
msgid "Aug"
msgstr "ag."
@@ -2915,9 +3060,6 @@ msgstr ""
msgid "BambooService|You must set up automatic revision labeling and a repository trigger in Bamboo."
msgstr ""
-msgid "Batch operations"
-msgstr ""
-
msgid "BatchComments|Delete all pending comments"
msgstr ""
@@ -3047,6 +3189,9 @@ msgstr ""
msgid "Boards"
msgstr "Taulers"
+msgid "Boards and Board Lists"
+msgstr ""
+
msgid "Boards|Collapse"
msgstr ""
@@ -3266,6 +3411,9 @@ msgstr ""
msgid "Buy GitLab Enterprise Edition"
msgstr ""
+msgid "Buy more Pipeline minutes"
+msgstr ""
+
msgid "By %{user_name}"
msgstr ""
@@ -3386,6 +3534,9 @@ msgstr ""
msgid "Can't scan the code?"
msgstr ""
+msgid "Can't update snippet: %{err}"
+msgstr ""
+
msgid "Canary"
msgstr ""
@@ -3554,6 +3705,9 @@ msgstr ""
msgid "Changing group path can have unintended side effects."
msgstr ""
+msgid "Charts"
+msgstr ""
+
msgid "Charts can't be displayed as the request for data has timed out. %{documentationLink}"
msgstr ""
@@ -3590,9 +3744,6 @@ msgstr ""
msgid "ChatMessage|and [%{count} more](%{pipeline_failed_jobs_url})"
msgstr ""
-msgid "ChatMessage|failed"
-msgstr ""
-
msgid "ChatMessage|has failed"
msgstr ""
@@ -3608,9 +3759,6 @@ msgstr ""
msgid "ChatMessage|in %{project_link}"
msgstr ""
-msgid "ChatMessage|passed"
-msgstr ""
-
msgid "Check again"
msgstr ""
@@ -4061,9 +4209,6 @@ msgstr ""
msgid "Click any <strong>project name</strong> in the project list below to navigate to the project milestone."
msgstr ""
-msgid "Click here"
-msgstr ""
-
msgid "Click the <strong>Download</strong> button and wait for downloading to complete."
msgstr ""
@@ -4451,6 +4596,12 @@ msgstr ""
msgid "ClusterIntegration|Fetching zones"
msgstr ""
+msgid "ClusterIntegration|Fluentd"
+msgstr ""
+
+msgid "ClusterIntegration|Fluentd is an open source data collector, which lets you unify the data collection and consumption for a better use and understanding of data. It requires at least one of the following logs to be successfully installed."
+msgstr ""
+
msgid "ClusterIntegration|GitLab Integration"
msgstr ""
@@ -4757,6 +4908,15 @@ msgstr ""
msgid "ClusterIntegration|Request to begin uninstalling failed"
msgstr ""
+msgid "ClusterIntegration|SIEM Hostname"
+msgstr ""
+
+msgid "ClusterIntegration|SIEM Port"
+msgstr ""
+
+msgid "ClusterIntegration|SIEM Protocol"
+msgstr ""
+
msgid "ClusterIntegration|Save changes"
msgstr ""
@@ -4853,6 +5013,12 @@ msgstr ""
msgid "ClusterIntegration|Select zone to choose machine type"
msgstr ""
+msgid "ClusterIntegration|Send Cilium Logs"
+msgstr ""
+
+msgid "ClusterIntegration|Send ModSecurity Logs"
+msgstr ""
+
msgid "ClusterIntegration|Service Token"
msgstr ""
@@ -5281,18 +5447,33 @@ msgstr ""
msgid "Compliance framework (optional)"
msgstr ""
+msgid "ComplianceFramework|GDPR"
+msgstr ""
+
msgid "ComplianceFramework|GDPR - General Data Protection Regulation"
msgstr ""
+msgid "ComplianceFramework|HIPAA"
+msgstr ""
+
msgid "ComplianceFramework|HIPAA - Health Insurance Portability and Accountability Act"
msgstr ""
+msgid "ComplianceFramework|PCI-DSS"
+msgstr ""
+
msgid "ComplianceFramework|PCI-DSS - Payment Card Industry-Data Security Standard"
msgstr ""
+msgid "ComplianceFramework|SOC 2"
+msgstr ""
+
msgid "ComplianceFramework|SOC 2 - Service Organization Control 2"
msgstr ""
+msgid "ComplianceFramework|SOX"
+msgstr ""
+
msgid "ComplianceFramework|SOX - Sarbanes-Oxley"
msgstr ""
@@ -5440,9 +5621,15 @@ msgstr ""
msgid "Container repositories sync capacity"
msgstr ""
+msgid "ContainerRegistry| Please visit the %{linkStart}administration settings%{linkEnd} to enable this feature."
+msgstr ""
+
msgid "ContainerRegistry|%{imageName} tags"
msgstr ""
+msgid "ContainerRegistry|%{title} was successfully scheduled for deletion"
+msgstr ""
+
msgid "ContainerRegistry|Automatically remove extra images that aren't designed to be kept."
msgstr ""
@@ -5455,6 +5642,9 @@ msgstr ""
msgid "ContainerRegistry|Container Registry"
msgstr ""
+msgid "ContainerRegistry|Container Registry tag expiration and retention policy is disabled"
+msgstr ""
+
msgid "ContainerRegistry|Copy build command"
msgstr ""
@@ -5464,18 +5654,12 @@ msgstr ""
msgid "ContainerRegistry|Copy push command"
msgstr ""
-msgid "ContainerRegistry|Currently, the Container Registry tag expiration feature is not available for projects created before GitLab version 12.8. For updates and more information, visit Issue %{linkStart}#196124%{linkEnd}"
-msgstr ""
-
msgid "ContainerRegistry|Docker connection error"
msgstr ""
msgid "ContainerRegistry|Docker tag expiration policy is %{toggleStatus}"
msgstr ""
-msgid "ContainerRegistry|Docker tags with names matching this regex pattern will expire:"
-msgstr ""
-
msgid "ContainerRegistry|Edit Settings"
msgstr ""
@@ -5497,9 +5681,6 @@ msgstr ""
msgid "ContainerRegistry|Image ID"
msgstr ""
-msgid "ContainerRegistry|Image deleted successfully"
-msgstr ""
-
msgid "ContainerRegistry|Keep and protect the images that matter most."
msgstr ""
@@ -5515,12 +5696,24 @@ msgstr ""
msgid "ContainerRegistry|Number of tags to retain:"
msgstr ""
+msgid "ContainerRegistry|Please contact your administrator."
+msgstr ""
+
msgid "ContainerRegistry|Push an image"
msgstr ""
msgid "ContainerRegistry|Quick Start"
msgstr ""
+msgid "ContainerRegistry|Regular expressions such as %{codeStart}.*-test%{codeEnd} or %{codeStart}dev-.*%{codeEnd} are supported"
+msgstr ""
+
+msgid "ContainerRegistry|Regular expressions such as %{codeStart}.*-test%{codeEnd} or %{codeStart}dev-.*%{codeEnd} are supported. To select all tags, use %{codeStart}.*%{codeEnd}"
+msgstr ""
+
+msgid "ContainerRegistry|Remember to run %{docLinkStart}garbage collection%{docLinkEnd} to remove the stale data from storage."
+msgstr ""
+
msgid "ContainerRegistry|Remove repository"
msgstr ""
@@ -5535,22 +5728,22 @@ msgstr[1] ""
msgid "ContainerRegistry|Retention policy has been Enabled"
msgstr ""
-msgid "ContainerRegistry|Something went wrong while deleting the image."
+msgid "ContainerRegistry|Something went wrong while fetching the expiration policy."
msgstr ""
-msgid "ContainerRegistry|Something went wrong while deleting the tag."
+msgid "ContainerRegistry|Something went wrong while fetching the repository list."
msgstr ""
-msgid "ContainerRegistry|Something went wrong while deleting the tags."
+msgid "ContainerRegistry|Something went wrong while fetching the tags list."
msgstr ""
-msgid "ContainerRegistry|Something went wrong while fetching the expiration policy."
+msgid "ContainerRegistry|Something went wrong while marking the tag for deletion."
msgstr ""
-msgid "ContainerRegistry|Something went wrong while fetching the packages list."
+msgid "ContainerRegistry|Something went wrong while marking the tags for deletion."
msgstr ""
-msgid "ContainerRegistry|Something went wrong while fetching the tags list."
+msgid "ContainerRegistry|Something went wrong while scheduling %{title} for deletion. Please try again."
msgstr ""
msgid "ContainerRegistry|Something went wrong while updating the expiration policy."
@@ -5559,16 +5752,25 @@ msgstr ""
msgid "ContainerRegistry|Tag"
msgstr "Etiqueta"
-msgid "ContainerRegistry|Tag deleted successfully"
-msgstr ""
-
msgid "ContainerRegistry|Tag expiration policy"
msgstr ""
msgid "ContainerRegistry|Tag expiration policy is designed to:"
msgstr ""
-msgid "ContainerRegistry|Tags deleted successfully"
+msgid "ContainerRegistry|Tag successfully marked for deletion."
+msgstr ""
+
+msgid "ContainerRegistry|Tags successfully marked for deletion."
+msgstr ""
+
+msgid "ContainerRegistry|Tags with names matching this regex pattern will %{italicStart}be preserved:%{italicEnd}"
+msgstr ""
+
+msgid "ContainerRegistry|Tags with names matching this regex pattern will %{italicStart}expire:%{italicEnd}"
+msgstr ""
+
+msgid "ContainerRegistry|The Container Registry tag expiration and retention policies for this project have not been enabled."
msgstr ""
msgid "ContainerRegistry|The last tag related to this image was recently removed. This empty image and any associated data will be automatically removed as part of the regular Garbage Collection process. If you have any questions, contact your administrator."
@@ -5589,16 +5791,16 @@ msgstr ""
msgid "ContainerRegistry|There are no container images stored for this project"
msgstr ""
-msgid "ContainerRegistry|This Registry contains deleted image tag data. Remember to run %{docLinkStart}garbage collection%{docLinkEnd} to remove the stale data from storage."
+msgid "ContainerRegistry|There was an error during the deletion of this image repository, please try again."
msgstr ""
msgid "ContainerRegistry|This image has no active tags"
msgstr ""
-msgid "ContainerRegistry|We are having trouble connecting to Docker, which could be due to an issue with your project name or path. %{docLinkStart}More Information%{docLinkEnd}"
+msgid "ContainerRegistry|This image repository is scheduled for deletion"
msgstr ""
-msgid "ContainerRegistry|Wildcards such as %{codeStart}.*-stable%{codeEnd} or %{codeStart}production/.*%{codeEnd} are supported. To select all tags, use %{codeStart}.*%{codeEnd}"
+msgid "ContainerRegistry|We are having trouble connecting to Docker, which could be due to an issue with your project name or path. %{docLinkStart}More Information%{docLinkEnd}"
msgstr ""
msgid "ContainerRegistry|With the Container Registry, every project can have its own space to store its Docker images. %{docLinkStart}More Information%{docLinkEnd}"
@@ -5853,7 +6055,7 @@ msgstr ""
msgid "Could not delete chat nickname %{chat_name}."
msgstr ""
-msgid "Could not find design"
+msgid "Could not find design."
msgstr ""
msgid "Could not remove the trigger."
@@ -5868,6 +6070,9 @@ msgstr ""
msgid "Could not revoke personal access token %{personal_access_token_name}."
msgstr ""
+msgid "Could not revoke project access token %{project_access_token_name}."
+msgstr ""
+
msgid "Could not save group ID"
msgstr ""
@@ -5925,6 +6130,9 @@ msgstr ""
msgid "Create a new branch"
msgstr ""
+msgid "Create a new deploy key for this project"
+msgstr ""
+
msgid "Create a new file as there are no files yet. Afterwards, you'll be able to commit your changes."
msgstr ""
@@ -6030,6 +6238,9 @@ msgstr "Crea una etiqueta de projecte"
msgid "Create requirement"
msgstr ""
+msgid "Create snippet"
+msgstr ""
+
msgid "Create wildcard: %{searchTerm}"
msgstr ""
@@ -6051,6 +6262,9 @@ msgstr ""
msgid "Created"
msgstr "Creat"
+msgid "Created %{timestamp}"
+msgstr ""
+
msgid "Created At"
msgstr ""
@@ -6407,6 +6621,9 @@ msgstr ""
msgid "CycleAnalytics|stage dropdown"
msgstr ""
+msgid "DAG"
+msgstr ""
+
msgid "DNS"
msgstr ""
@@ -6773,6 +6990,9 @@ msgstr ""
msgid "Deploy key was successfully updated."
msgstr ""
+msgid "Deploy keys allow read-only or read-write (if enabled) access to your repository. Deploy keys can be used for CI, staging or production servers. You can create a deploy key or add an existing one."
+msgstr ""
+
msgid "Deploy progress not found. To see pods, ensure your environment matches %{linkStart}deploy board criteria%{linkEnd}."
msgstr ""
@@ -6926,6 +7146,9 @@ msgstr ""
msgid "Deploying to"
msgstr ""
+msgid "Deployment Frequency"
+msgstr ""
+
msgid "Deployment|API"
msgstr ""
@@ -7444,9 +7667,6 @@ msgstr ""
msgid "Edit environment"
msgstr ""
-msgid "Edit epic description"
-msgstr ""
-
msgid "Edit file"
msgstr ""
@@ -8068,9 +8288,6 @@ msgstr ""
msgid "Epics"
msgstr ""
-msgid "Epics (Ultimate / Gold license only)"
-msgstr ""
-
msgid "Epics Roadmap"
msgstr ""
@@ -8080,6 +8297,9 @@ msgstr ""
msgid "Epics let you manage your portfolio of projects more efficiently and with less effort"
msgstr ""
+msgid "Epics, Issues, and Merge Requests"
+msgstr ""
+
msgid "Epics|Add an epic"
msgstr ""
@@ -8131,6 +8351,9 @@ msgstr ""
msgid "Epics|Something went wrong while fetching group epics."
msgstr ""
+msgid "Epics|Something went wrong while moving item."
+msgstr ""
+
msgid "Epics|Something went wrong while ordering item."
msgstr ""
@@ -8185,6 +8408,9 @@ msgstr ""
msgid "Error fetching network graph."
msgstr ""
+msgid "Error fetching payload data."
+msgstr ""
+
msgid "Error fetching projects"
msgstr ""
@@ -8194,9 +8420,6 @@ msgstr ""
msgid "Error fetching the dependency list. Please check your network connection and try again."
msgstr ""
-msgid "Error fetching usage ping data."
-msgstr ""
-
msgid "Error loading branch data. Please try again."
msgstr ""
@@ -8338,6 +8561,9 @@ msgstr ""
msgid "Errors"
msgstr ""
+msgid "Errors:"
+msgstr ""
+
msgid "Estimated"
msgstr ""
@@ -8542,12 +8768,18 @@ msgstr ""
msgid "Export as CSV"
msgstr ""
+msgid "Export group"
+msgstr ""
+
msgid "Export issues"
msgstr ""
msgid "Export project"
msgstr ""
+msgid "Export this group with all related data to a new GitLab instance. Once complete, you can import the data file from the \"New Group\" page."
+msgstr ""
+
msgid "Export this project with all its related data in order to move your project to a new GitLab instance. Once the export is finished, you can import the file from the \"New Project\" page."
msgstr ""
@@ -8617,9 +8849,6 @@ msgstr ""
msgid "Failed Jobs"
msgstr ""
-msgid "Failed create wiki"
-msgstr ""
-
msgid "Failed to add a Zoom meeting"
msgstr ""
@@ -8659,6 +8888,9 @@ msgstr ""
msgid "Failed to create resources"
msgstr ""
+msgid "Failed to create wiki"
+msgstr ""
+
msgid "Failed to delete board. Please try again."
msgstr ""
@@ -8668,7 +8900,7 @@ msgstr ""
msgid "Failed to enqueue the rebase operation, possibly due to a long-lived transaction. Try again later."
msgstr ""
-msgid "Failed to find import label for jira import."
+msgid "Failed to find import label for Jira import."
msgstr ""
msgid "Failed to get ref."
@@ -8692,6 +8924,12 @@ msgstr ""
msgid "Failed to load groups & users."
msgstr ""
+msgid "Failed to load labels. Please try again."
+msgstr ""
+
+msgid "Failed to load milestones. Please try again."
+msgstr ""
+
msgid "Failed to load related branches"
msgstr ""
@@ -9067,7 +9305,10 @@ msgstr ""
msgid "Filter by milestone name"
msgstr ""
-msgid "Filter by name..."
+msgid "Filter by name"
+msgstr ""
+
+msgid "Filter by status"
msgstr ""
msgid "Filter by two-factor authentication"
@@ -9079,12 +9320,18 @@ msgstr ""
msgid "Filter projects"
msgstr ""
+msgid "Filter results"
+msgstr ""
+
msgid "Filter results by group"
msgstr ""
msgid "Filter results by project"
msgstr ""
+msgid "Filter results..."
+msgstr ""
+
msgid "Filter your projects by name"
msgstr ""
@@ -9373,10 +9620,10 @@ msgstr ""
msgid "GeoNodes|Checksummed"
msgstr ""
-msgid "GeoNodes|Container repositories"
+msgid "GeoNodes|Consult Geo troubleshooting information"
msgstr ""
-msgid "GeoNodes|Data is out of date from %{timeago}"
+msgid "GeoNodes|Container repositories"
msgstr ""
msgid "GeoNodes|Data replication lag"
@@ -9418,6 +9665,9 @@ msgstr ""
msgid "GeoNodes|Last event ID seen from primary"
msgstr ""
+msgid "GeoNodes|Learn more about Geo node statuses"
+msgstr ""
+
msgid "GeoNodes|Loading nodes"
msgstr ""
@@ -9433,6 +9683,9 @@ msgstr ""
msgid "GeoNodes|Node was successfully removed."
msgstr ""
+msgid "GeoNodes|Node's status was updated %{timeAgo}."
+msgstr ""
+
msgid "GeoNodes|Not checksummed"
msgstr ""
@@ -9463,7 +9716,10 @@ msgstr ""
msgid "GeoNodes|Repository verification progress"
msgstr ""
-msgid "GeoNodes|Selective"
+msgid "GeoNodes|Selective (%{syncLabel})"
+msgstr ""
+
+msgid "GeoNodes|Selective synchronization"
msgstr ""
msgid "GeoNodes|Something went wrong while changing node status"
@@ -9490,6 +9746,9 @@ msgstr ""
msgid "GeoNodes|Unverified"
msgstr ""
+msgid "GeoNodes|Updated %{timeAgo}"
+msgstr ""
+
msgid "GeoNodes|Used slots"
msgstr ""
@@ -9529,16 +9788,16 @@ msgstr ""
msgid "Geo|All"
msgstr ""
+msgid "Geo|All %{replicable_type}"
+msgstr ""
+
msgid "Geo|All projects"
msgstr "Tots els projectes"
-msgid "Geo|All projects are being scheduled for re-sync"
-msgstr ""
-
-msgid "Geo|All projects are being scheduled for re-verify"
+msgid "Geo|All projects are being scheduled for resync"
msgstr ""
-msgid "Geo|Batch operations"
+msgid "Geo|All projects are being scheduled for reverify"
msgstr ""
msgid "Geo|Could not remove tracking entry for an existing project."
@@ -9550,9 +9809,15 @@ msgstr ""
msgid "Geo|Failed"
msgstr "Fallit"
+msgid "Geo|Filter by status"
+msgstr ""
+
msgid "Geo|Geo Status"
msgstr ""
+msgid "Geo|In progress"
+msgstr ""
+
msgid "Geo|In sync"
msgstr ""
@@ -9577,9 +9842,6 @@ msgstr ""
msgid "Geo|Not synced yet"
msgstr ""
-msgid "Geo|Pending"
-msgstr ""
-
msgid "Geo|Pending synchronization"
msgstr ""
@@ -9616,7 +9878,7 @@ msgstr ""
msgid "Geo|Resync"
msgstr ""
-msgid "Geo|Resync all projects"
+msgid "Geo|Resync all"
msgstr ""
msgid "Geo|Retry count"
@@ -9625,7 +9887,7 @@ msgstr ""
msgid "Geo|Reverify"
msgstr ""
-msgid "Geo|Reverify all projects"
+msgid "Geo|Reverify all"
msgstr ""
msgid "Geo|Status"
@@ -9967,9 +10229,6 @@ msgstr ""
msgid "Go to file"
msgstr ""
-msgid "Go to file (MRs only)"
-msgstr ""
-
msgid "Go to file permalink (while viewing a file)"
msgstr ""
@@ -10414,6 +10673,9 @@ msgstr ""
msgid "GroupSettings|Disable group mentions"
msgstr ""
+msgid "GroupSettings|Export group"
+msgstr ""
+
msgid "GroupSettings|If the parent group's visibility is lower than the group current visibility, visibility levels for subgroups and projects will be changed to match the new parent group's visibility."
msgstr ""
@@ -10492,6 +10754,9 @@ msgstr "Grups"
msgid "Groups (%{count})"
msgstr ""
+msgid "Groups (%{groups})"
+msgstr ""
+
msgid "Groups can also be nested by creating %{subgroup_docs_link_start}subgroups%{subgroup_docs_link_end}."
msgstr ""
@@ -10757,15 +11022,12 @@ msgstr "ID"
msgid "ID:"
msgstr ""
-msgid "IDE|Allow live previews of JavaScript projects in the Web IDE using CodeSandbox client side evaluation."
+msgid "IDE|Allow live previews of JavaScript projects in the Web IDE using CodeSandbox Live Preview."
msgstr ""
msgid "IDE|Back"
msgstr "Enrere"
-msgid "IDE|Client side evaluation"
-msgstr ""
-
msgid "IDE|Commit"
msgstr ""
@@ -10829,6 +11091,9 @@ msgstr ""
msgid "If any job surpasses this timeout threshold, it will be marked as failed. Human readable time input language is accepted like \"1 hour\". Values without specification represent seconds."
msgstr ""
+msgid "If blank, set allowable lifetime to %{instance_level_policy_in_words}, as defined by the instance admin. Once set, existing tokens for users in this group may be revoked."
+msgstr ""
+
msgid "If checked, group owners can manage LDAP group links and LDAP member overrides"
msgstr ""
@@ -10868,6 +11133,9 @@ msgstr ""
msgid "If you lose your recovery codes you can generate new ones, invalidating all previous codes."
msgstr ""
+msgid "If you reach 100%% storage capacity, you will not be able to: %{base_message}"
+msgstr ""
+
msgid "If your HTTP repository is not publicly accessible, add your credentials."
msgstr ""
@@ -10979,6 +11247,9 @@ msgstr ""
msgid "Import repository"
msgstr ""
+msgid "Import started by: %{importInitiator}"
+msgstr ""
+
msgid "Import tasks"
msgstr ""
@@ -11045,6 +11316,9 @@ msgstr ""
msgid "In order to tailor your experience with GitLab we<br>would like to know a bit more about you."
msgstr ""
+msgid "In progress"
+msgstr ""
+
msgid "In the next step, you'll be able to select the projects you want to import."
msgstr ""
@@ -11392,15 +11666,15 @@ msgstr ""
msgid "Issues"
msgstr ""
-msgid "Issues / Merge Requests"
-msgstr ""
-
msgid "Issues Analytics"
msgstr ""
msgid "Issues Rate Limits"
msgstr ""
+msgid "Issues and Merge Requests"
+msgstr ""
+
msgid "Issues can be bugs, tasks or ideas to be discussed. Also, issues are searchable and filterable."
msgstr ""
@@ -11422,10 +11696,10 @@ msgstr ""
msgid "IssuesAnalytics|Avg/Month:"
msgstr ""
-msgid "IssuesAnalytics|Issues created"
+msgid "IssuesAnalytics|Issues opened"
msgstr ""
-msgid "IssuesAnalytics|Issues created per month"
+msgid "IssuesAnalytics|Issues opened per month"
msgstr ""
msgid "IssuesAnalytics|Last 12 months"
@@ -11479,6 +11753,9 @@ msgstr ""
msgid "Jira integration not configured."
msgstr ""
+msgid "Jira project: %{importProject}"
+msgstr ""
+
msgid "JiraService|Events for %{noteable_model_name} are disabled."
msgstr ""
@@ -11868,6 +12145,9 @@ msgstr ""
msgid "Lead"
msgstr ""
+msgid "Lead Time"
+msgstr ""
+
msgid "Learn GitLab"
msgstr ""
@@ -11880,6 +12160,9 @@ msgstr ""
msgid "Learn how to %{no_packages_link_start}publish and share your packages%{no_packages_link_end} with GitLab."
msgstr ""
+msgid "Learn how to enable synchronization"
+msgstr ""
+
msgid "Learn more"
msgstr ""
@@ -12211,9 +12494,6 @@ msgstr ""
msgid "Live preview"
msgstr ""
-msgid "Load more vulnerabilities"
-msgstr ""
-
msgid "Loading"
msgstr ""
@@ -12364,9 +12644,6 @@ msgstr ""
msgid "Manage"
msgstr ""
-msgid "Manage Git repositories with fine-grained access controls that keep your code secure. Perform code reviews and enhance collaboration with merge requests. Each project can also have an issue tracker and a wiki."
-msgstr ""
-
msgid "Manage Web IDE features"
msgstr ""
@@ -12397,6 +12674,9 @@ msgstr ""
msgid "Manage your license"
msgstr ""
+msgid "Managed Account"
+msgstr ""
+
msgid "Manifest"
msgstr ""
@@ -12661,6 +12941,9 @@ msgstr "S'han creat les peticions de fusió"
msgid "Merge Requests in Review"
msgstr ""
+msgid "Merge automatically (%{strategy})"
+msgstr ""
+
msgid "Merge commit message"
msgstr ""
@@ -12814,6 +13097,12 @@ msgstr ""
msgid "Merged branches are being deleted. This can take some time depending on the number of branches. Please refresh the page to see changes."
msgstr ""
+msgid "Merged this merge request."
+msgstr ""
+
+msgid "Merges this merge request immediately."
+msgstr ""
+
msgid "Merges this merge request when the pipeline succeeds."
msgstr ""
@@ -12921,12 +13210,15 @@ msgid_plural "Metrics|Edit metrics"
msgstr[0] ""
msgstr[1] ""
-msgid "Metrics|Environment"
-msgstr "Entorn"
+msgid "Metrics|Expand panel"
+msgstr ""
msgid "Metrics|For grouping similar metrics"
msgstr ""
+msgid "Metrics|Go back (Esc)"
+msgstr ""
+
msgid "Metrics|Invalid time range, please verify."
msgstr ""
@@ -12957,9 +13249,6 @@ msgstr ""
msgid "Metrics|Refresh dashboard"
msgstr ""
-msgid "Metrics|Show last"
-msgstr ""
-
msgid "Metrics|There was an error creating the dashboard."
msgstr ""
@@ -13038,6 +13327,9 @@ msgstr ""
msgid "Microsoft Azure"
msgstr ""
+msgid "Middleman project with Static Site Editor support"
+msgstr ""
+
msgid "Migrated %{success_count}/%{total_count} files."
msgstr ""
@@ -13483,10 +13775,10 @@ msgstr ""
msgid "Next"
msgstr ""
-msgid "Next file in diff (MRs only)"
+msgid "Next file in diff"
msgstr ""
-msgid "Next unresolved discussion (MRs only)"
+msgid "Next unresolved discussion"
msgstr ""
msgid "Nickname"
@@ -13546,6 +13838,9 @@ msgstr ""
msgid "No changes between %{ref_start}%{source_branch}%{ref_end} and %{ref_start}%{target_branch}%{ref_end}"
msgstr ""
+msgid "No child epics match applied filters"
+msgstr ""
+
msgid "No connection could be made to a Gitaly Server, please check your logs!"
msgstr ""
@@ -13684,15 +13979,6 @@ msgstr ""
msgid "No thanks, don't show this again"
msgstr ""
-msgid "No vulnerabilities found for this group"
-msgstr ""
-
-msgid "No vulnerabilities found for this pipeline"
-msgstr ""
-
-msgid "No vulnerabilities found for this project"
-msgstr ""
-
msgid "No vulnerabilities present"
msgstr ""
@@ -13816,6 +14102,9 @@ msgstr ""
msgid "Nothing to preview."
msgstr ""
+msgid "Nothing to synchronize"
+msgstr ""
+
msgid "Notification events"
msgstr ""
@@ -13987,6 +14276,9 @@ msgstr ""
msgid "Once removed, the fork relationship cannot be restored and you will no longer be able to send merge requests to the source."
msgstr ""
+msgid "Once the exported file is ready you can download it from this page."
+msgstr ""
+
msgid "Once the exported file is ready, you will receive a notification email with a download link, or you can download it from this page."
msgstr ""
@@ -14088,9 +14380,6 @@ msgstr ""
msgid "Open sidebar"
msgstr ""
-msgid "Open source software to collaborate on code"
-msgstr ""
-
msgid "Open: %{openIssuesCount}"
msgstr ""
@@ -14193,6 +14482,9 @@ msgstr ""
msgid "OutdatedBrowser|You can provide feedback %{feedback_link_start}on this issue%{feedback_link_end} or via your usual support channels."
msgstr ""
+msgid "Overridden"
+msgstr ""
+
msgid "Overview"
msgstr ""
@@ -14244,6 +14536,9 @@ msgstr ""
msgid "PackageRegistry|Conan Command"
msgstr ""
+msgid "PackageRegistry|Copy .pypirc content"
+msgstr ""
+
msgid "PackageRegistry|Copy Conan Command"
msgstr ""
@@ -14265,6 +14560,9 @@ msgstr ""
msgid "PackageRegistry|Copy NuGet Setup Command"
msgstr ""
+msgid "PackageRegistry|Copy Pip command"
+msgstr ""
+
msgid "PackageRegistry|Copy and paste this inside your %{codeStart}pom.xml%{codeEnd} %{codeStart}dependencies%{codeEnd} block."
msgstr ""
@@ -14298,6 +14596,12 @@ msgstr ""
msgid "PackageRegistry|For more information on the NuGet registry, %{linkStart}see the documentation%{linkEnd}."
msgstr ""
+msgid "PackageRegistry|For more information on the PyPi registry, %{linkStart}see the documentation%{linkEnd}."
+msgstr ""
+
+msgid "PackageRegistry|If you haven't already done so, you will need to add the below to your %{codeStart}.pypirc%{codeEnd} file."
+msgstr ""
+
msgid "PackageRegistry|If you haven't already done so, you will need to add the below to your %{codeStart}pom.xml%{codeEnd} file."
msgstr ""
@@ -14307,6 +14611,9 @@ msgstr ""
msgid "PackageRegistry|Learn how to %{noPackagesLinkStart}publish and share your packages%{noPackagesLinkEnd} with GitLab."
msgstr ""
+msgid "PackageRegistry|Manually Published"
+msgstr ""
+
msgid "PackageRegistry|Maven"
msgstr ""
@@ -14325,12 +14632,18 @@ msgstr ""
msgid "PackageRegistry|NuGet Command"
msgstr ""
+msgid "PackageRegistry|Pip Command"
+msgstr ""
+
msgid "PackageRegistry|Pipeline %{linkStart}%{linkEnd} triggered %{timestamp} by %{author}"
msgstr ""
msgid "PackageRegistry|Published to the repository at %{timestamp}"
msgstr ""
+msgid "PackageRegistry|PyPi"
+msgstr ""
+
msgid "PackageRegistry|Registry Setup"
msgstr ""
@@ -14367,6 +14680,9 @@ msgstr ""
msgid "PackageRegistry|npm"
msgstr ""
+msgid "PackageRegistry|published by %{author}"
+msgstr ""
+
msgid "PackageRegistry|yarn"
msgstr ""
@@ -14382,6 +14698,9 @@ msgstr ""
msgid "PackageType|NuGet"
msgstr ""
+msgid "PackageType|PyPi"
+msgstr ""
+
msgid "Packages"
msgstr ""
@@ -14475,6 +14794,9 @@ msgstr ""
msgid "Past due"
msgstr ""
+msgid "Paste a machine public key here. Read more about how to generate it"
+msgstr ""
+
msgid "Paste a machine public key here. Read more about how to generate it %{link_start}here%{link_end}"
msgstr ""
@@ -14490,9 +14812,6 @@ msgstr ""
msgid "Path"
msgstr ""
-msgid "Path, transfer, remove"
-msgstr ""
-
msgid "Path:"
msgstr ""
@@ -14523,7 +14842,7 @@ msgstr ""
msgid "Percentage"
msgstr ""
-msgid "Perform advanced options such as changing path, transferring, or removing the group."
+msgid "Perform advanced options such as changing path, transferring, exporting, or removing the group."
msgstr ""
msgid "Perform common operations on GitLab project"
@@ -14604,9 +14923,6 @@ msgstr ""
msgid "Pipeline minutes quota"
msgstr ""
-msgid "Pipeline quota"
-msgstr ""
-
msgid "Pipeline subscriptions"
msgstr ""
@@ -14616,6 +14932,9 @@ msgstr ""
msgid "Pipeline: %{status}"
msgstr ""
+msgid "PipelineCharts|CI / CD Analytics"
+msgstr ""
+
msgid "PipelineCharts|Failed:"
msgstr ""
@@ -14694,15 +15013,6 @@ msgstr ""
msgid "Pipelines settings for '%{project_name}' were successfully updated."
msgstr ""
-msgid "Pipelines| to purchase more minutes."
-msgstr ""
-
-msgid "Pipelines|%{namespace_name} has exceeded its pipeline minutes quota."
-msgstr ""
-
-msgid "Pipelines|%{namespace_name} has less than %{notification_level}%% of CI minutes available."
-msgstr ""
-
msgid "Pipelines|API"
msgstr ""
@@ -14724,10 +15034,13 @@ msgstr ""
msgid "Pipelines|Get started with Pipelines"
msgstr ""
-msgid "Pipelines|Loading Pipelines"
+msgid "Pipelines|Group %{namespace_name} has %{percentage}%% or less Shared Runner Pipeline minutes remaining. Once it runs out, no new jobs or pipelines in its projects will run."
+msgstr ""
+
+msgid "Pipelines|Group %{namespace_name} has exceeded its pipeline minutes quota. Unless you buy additional pipeline minutes, no new jobs or pipelines in its projects will run."
msgstr ""
-msgid "Pipelines|Pipelines will not run anymore on shared Runners."
+msgid "Pipelines|Loading Pipelines"
msgstr ""
msgid "Pipelines|Project cache successfully reset."
@@ -14958,6 +15271,9 @@ msgstr ""
msgid "Please select"
msgstr ""
+msgid "Please select a Jira project"
+msgstr ""
+
msgid "Please select a country"
msgstr ""
@@ -15003,6 +15319,9 @@ msgstr ""
msgid "Please wait while we import the repository for you. Refresh at will."
msgstr ""
+msgid "Plugins directory is deprecated and will be removed in 14.0. Please move this file into /file_hooks directory."
+msgstr ""
+
msgid "Pod does not exist"
msgstr ""
@@ -15120,7 +15439,7 @@ msgstr ""
msgid "Prevent users from changing their profile name"
msgstr ""
-msgid "Prevent users from modifing merge request approvers list"
+msgid "Prevent users from modifying merge request approvers list"
msgstr ""
msgid "Prevent users from performing write operations on GitLab while performing maintenance."
@@ -15141,10 +15460,10 @@ msgstr ""
msgid "Previous Artifacts"
msgstr ""
-msgid "Previous file in diff (MRs only)"
+msgid "Previous file in diff"
msgstr ""
-msgid "Previous unresolved discussion (MRs only)"
+msgid "Previous unresolved discussion"
msgstr ""
msgid "Primary"
@@ -15177,6 +15496,9 @@ msgstr ""
msgid "Private profile"
msgstr ""
+msgid "Private projects Minutes cost factor"
+msgstr ""
+
msgid "Private projects can be created in your personal namespace with:"
msgstr ""
@@ -16005,6 +16327,9 @@ msgstr ""
msgid "ProjectSettings|With GitLab Pages you can host your static websites on GitLab"
msgstr ""
+msgid "ProjectSettings|With Metrics Dashboard you can visualize this project performance metrics"
+msgstr ""
+
msgid "ProjectTemplates|.NET Core"
msgstr ""
@@ -16068,6 +16393,9 @@ msgstr ""
msgid "ProjectTemplates|Spring"
msgstr ""
+msgid "ProjectTemplates|Static Site Editor/Middleman"
+msgstr ""
+
msgid "ProjectTemplates|iOS (Swift)"
msgstr ""
@@ -16287,13 +16615,13 @@ msgstr ""
msgid "PrometheusService|Prometheus is being automatically managed on your clusters"
msgstr ""
-msgid "PrometheusService|These metrics will only be monitored after your first deployment to an environment"
+msgid "PrometheusService|Select the Active checkbox to override the Auto Configuration with custom settings. If unchecked, Auto Configuration settings are used."
msgstr ""
-msgid "PrometheusService|Time-series monitoring service"
+msgid "PrometheusService|These metrics will only be monitored after your first deployment to an environment"
msgstr ""
-msgid "PrometheusService|To enable manual configuration, uninstall Prometheus from your clusters"
+msgid "PrometheusService|Time-series monitoring service"
msgstr ""
msgid "PrometheusService|To enable the installation of Prometheus on your clusters, deactivate the manual configuration below"
@@ -16479,6 +16807,9 @@ msgstr ""
msgid "Protip:"
msgstr ""
+msgid "Protocol"
+msgstr ""
+
msgid "Provider"
msgstr ""
@@ -16503,6 +16834,9 @@ msgstr ""
msgid "Public pipelines"
msgstr ""
+msgid "Public projects Minutes cost factor"
+msgstr ""
+
msgid "Pull"
msgstr ""
@@ -16703,6 +17037,9 @@ msgid_plural "Refreshing in %d seconds to show the updated status..."
msgstr[0] ""
msgstr[1] ""
+msgid "Regenerate export"
+msgstr ""
+
msgid "Regenerate instance ID"
msgstr ""
@@ -17080,9 +17417,29 @@ msgstr ""
msgid "Reports|%{combinedString} and %{resolvedString}"
msgstr ""
+msgid "Reports|Accessibility scanning detected %d issue for the source branch only"
+msgid_plural "Reports|Accessibility scanning detected %d issues for the source branch only"
+msgstr[0] ""
+msgstr[1] ""
+
+msgid "Reports|Accessibility scanning detected no issues for the source branch only"
+msgstr ""
+
+msgid "Reports|Accessibility scanning failed loading results"
+msgstr ""
+
+msgid "Reports|Accessibility scanning results are being parsed"
+msgstr ""
+
msgid "Reports|Actions"
msgstr ""
+msgid "Reports|An error occured while loading report"
+msgstr ""
+
+msgid "Reports|An error occurred while loading %{name} results"
+msgstr ""
+
msgid "Reports|Class"
msgstr ""
@@ -17188,7 +17545,7 @@ msgstr ""
msgid "Requested %{time_ago}"
msgstr ""
-msgid "Requested design version does not exist"
+msgid "Requested design version does not exist."
msgstr ""
msgid "Requested states are invalid"
@@ -17372,6 +17729,9 @@ msgstr ""
msgid "Resync"
msgstr ""
+msgid "Resync all"
+msgstr ""
+
msgid "Resync all %{replicableType}"
msgstr ""
@@ -17440,6 +17800,9 @@ msgstr ""
msgid "Revoked personal access token %{personal_access_token_name}!"
msgstr ""
+msgid "Revoked project access token %{project_access_token_name}!"
+msgstr ""
+
msgid "RightSidebar|adding a"
msgstr ""
@@ -17488,6 +17851,9 @@ msgstr ""
msgid "Runner token"
msgstr ""
+msgid "Runner tokens"
+msgstr ""
+
msgid "Runner was not updated."
msgstr ""
@@ -17542,6 +17908,9 @@ msgstr ""
msgid "SAML SSO for %{group_name}"
msgstr ""
+msgid "SAML discovery tokens"
+msgstr ""
+
msgid "SAML for %{group_name}"
msgstr ""
@@ -17626,12 +17995,18 @@ msgstr ""
msgid "Scheduled"
msgstr ""
+msgid "Scheduled to merge this merge request (%{strategy})."
+msgstr ""
+
msgid "Scheduled to merge this merge request when the pipeline succeeds."
msgstr ""
msgid "Schedules"
msgstr ""
+msgid "Schedules to merge this merge request (%{strategy})."
+msgstr ""
+
msgid "Scheduling"
msgstr ""
@@ -17689,6 +18064,9 @@ msgstr ""
msgid "Search branches and tags"
msgstr ""
+msgid "Search by author"
+msgstr ""
+
msgid "Search files"
msgstr ""
@@ -17834,11 +18212,6 @@ msgid_plural "SearchResults|snippets"
msgstr[0] ""
msgstr[1] ""
-msgid "SearchResults|snippet result"
-msgid_plural "SearchResults|snippet results"
-msgstr[0] ""
-msgstr[1] ""
-
msgid "SearchResults|user"
msgid_plural "SearchResults|users"
msgstr[0] ""
@@ -17849,6 +18222,9 @@ msgid_plural "SearchResults|wiki results"
msgstr[0] ""
msgstr[1] ""
+msgid "Searching by both author and message is currently not supported."
+msgstr ""
+
msgid "Seat Link"
msgstr ""
@@ -17876,184 +18252,232 @@ msgstr ""
msgid "Security Dashboard"
msgstr ""
-msgid "Security Dashboard|Error fetching the vulnerability counts. Please check your network connection and try again."
+msgid "Security configuration help link"
msgstr ""
-msgid "Security Dashboard|Error fetching the vulnerability list. Please check your network connection and try again."
+msgid "Security dashboard"
msgstr ""
-msgid "Security Dashboard|Issue Created"
+msgid "Security report is out of date. Please update your branch with the latest changes from the target branch (%{targetBranchName})"
msgstr ""
-msgid "Security Reports|Comment added to '%{vulnerabilityName}'"
+msgid "Security report is out of date. Run %{newPipelineLinkStart}a new pipeline%{newPipelineLinkEnd} for the target branch (%{targetBranchName})"
msgstr ""
-msgid "Security Reports|Comment deleted on '%{vulnerabilityName}'"
+msgid "SecurityConfiguration|Configured"
msgstr ""
-msgid "Security Reports|Comment edited on '%{vulnerabilityName}'"
+msgid "SecurityConfiguration|Feature"
msgstr ""
-msgid "Security Reports|Create issue"
+msgid "SecurityConfiguration|Feature documentation for %{featureName}"
msgstr ""
-msgid "Security Reports|Dismiss vulnerability"
+msgid "SecurityConfiguration|Not yet configured"
msgstr ""
-msgid "Security Reports|Dismissed '%{vulnerabilityName}'"
+msgid "SecurityConfiguration|Secure features"
msgstr ""
-msgid "Security Reports|Dismissed '%{vulnerabilityName}'. Turn off the hide dismissed toggle to view."
+msgid "SecurityConfiguration|Status"
msgstr ""
-msgid "Security Reports|Either you don't have permission to view this dashboard or the dashboard has not been setup. Please check your permission settings with your administrator or check your dashboard configurations to proceed."
+msgid "SecurityReports|%{firstProject} and %{secondProject}"
msgstr ""
-msgid "Security Reports|Learn more about setting up your dashboard"
+msgid "SecurityReports|%{firstProject}, %{secondProject}, and %{rest}"
msgstr ""
-msgid "Security Reports|More info"
+msgid "SecurityReports|Add a project to your dashboard"
msgstr ""
-msgid "Security Reports|Oops, something doesn't seem right."
+msgid "SecurityReports|Add or remove projects from your dashboard"
msgstr ""
-msgid "Security Reports|Security reports can only be accessed by authorized users."
+msgid "SecurityReports|Add projects"
msgstr ""
-msgid "Security Reports|There was an error adding the comment."
+msgid "SecurityReports|Comment added to '%{vulnerabilityName}'"
msgstr ""
-msgid "Security Reports|There was an error creating the issue."
+msgid "SecurityReports|Comment deleted on '%{vulnerabilityName}'"
msgstr ""
-msgid "Security Reports|There was an error creating the merge request."
+msgid "SecurityReports|Comment edited on '%{vulnerabilityName}'"
msgstr ""
-msgid "Security Reports|There was an error deleting the comment."
+msgid "SecurityReports|Create issue"
msgstr ""
-msgid "Security Reports|There was an error dismissing the vulnerabilities."
+msgid "SecurityReports|Dismiss Selected"
msgstr ""
-msgid "Security Reports|There was an error dismissing the vulnerability."
+msgid "SecurityReports|Dismiss vulnerability"
msgstr ""
-msgid "Security Reports|There was an error reverting the dismissal."
+msgid "SecurityReports|Dismissed '%{vulnerabilityName}'"
msgstr ""
-msgid "Security Reports|There was an error reverting this dismissal."
+msgid "SecurityReports|Dismissed '%{vulnerabilityName}'. Turn off the hide dismissed toggle to view."
msgstr ""
-msgid "Security Reports|Undo dismiss"
+msgid "SecurityReports|Each vulnerability now has a unique page that can be directly linked to, shared, referenced, and tracked as the single source of truth. Vulnerability occurrences also persist across scanner runs, which improves tracking and visibility and reduces duplicates between scans."
msgstr ""
-msgid "Security Reports|You do not have sufficient permissions to access this report"
+msgid "SecurityReports|Edit dashboard"
msgstr ""
-msgid "Security Reports|You must sign in as an authorized user to see this report"
+msgid "SecurityReports|Either you don't have permission to view this dashboard or the dashboard has not been setup. Please check your permission settings with your administrator or check your dashboard configurations to proceed."
msgstr ""
-msgid "Security configuration help link"
+msgid "SecurityReports|Error fetching the vulnerability counts. Please check your network connection and try again."
msgstr ""
-msgid "Security dashboard"
+msgid "SecurityReports|Error fetching the vulnerability list. Please check your network connection and try again."
msgstr ""
-msgid "Security report is out of date. Please update your branch with the latest changes from the target branch (%{targetBranchName})"
+msgid "SecurityReports|False positive"
msgstr ""
-msgid "Security report is out of date. Run %{newPipelineLinkStart}a new pipeline%{newPipelineLinkEnd} for the target branch (%{targetBranchName})"
+msgid "SecurityReports|Hide dismissed"
msgstr ""
-msgid "SecurityConfiguration|Configured"
+msgid "SecurityReports|Introducing standalone vulnerabilities"
msgstr ""
-msgid "SecurityConfiguration|Feature"
+msgid "SecurityReports|Issue Created"
msgstr ""
-msgid "SecurityConfiguration|Feature documentation for %{featureName}"
+msgid "SecurityReports|Learn More"
msgstr ""
-msgid "SecurityConfiguration|Not yet configured"
+msgid "SecurityReports|Learn more about setting up your dashboard"
msgstr ""
-msgid "SecurityConfiguration|Secure features"
+msgid "SecurityReports|Load more vulnerabilities"
msgstr ""
-msgid "SecurityConfiguration|Status"
+msgid "SecurityReports|Monitor vulnerabilities in your code"
+msgstr ""
+
+msgid "SecurityReports|More info"
+msgstr ""
+
+msgid "SecurityReports|More information"
+msgstr ""
+
+msgid "SecurityReports|No vulnerabilities found for dashboard"
+msgstr ""
+
+msgid "SecurityReports|No vulnerabilities found for this group"
+msgstr ""
+
+msgid "SecurityReports|No vulnerabilities found for this pipeline"
+msgstr ""
+
+msgid "SecurityReports|No vulnerabilities found for this project"
+msgstr ""
+
+msgid "SecurityReports|Oops, something doesn't seem right."
+msgstr ""
+
+msgid "SecurityReports|Pipeline %{pipelineLink} triggered %{timeago} by %{user}"
msgstr ""
-msgid "SecurityDashboard|%{firstProject} and %{secondProject}"
+msgid "SecurityReports|Project"
msgstr ""
-msgid "SecurityDashboard|%{firstProject}, %{secondProject}, and %{rest}"
+msgid "SecurityReports|Projects added"
msgstr ""
-msgid "SecurityDashboard|Add a project to your dashboard"
+msgid "SecurityReports|Remove project from dashboard"
msgstr ""
-msgid "SecurityDashboard|Add or remove projects from your dashboard"
+msgid "SecurityReports|Report type"
msgstr ""
-msgid "SecurityDashboard|Add projects"
+msgid "SecurityReports|Return to dashboard"
msgstr ""
-msgid "SecurityDashboard|Edit dashboard"
+msgid "SecurityReports|Security Dashboard"
msgstr ""
-msgid "SecurityDashboard|Hide dismissed"
+msgid "SecurityReports|Security reports can only be accessed by authorized users."
msgstr ""
-msgid "SecurityDashboard|Monitor vulnerabilities in your code"
+msgid "SecurityReports|Select a project to add by using the project search field above."
msgstr ""
-msgid "SecurityDashboard|More information"
+msgid "SecurityReports|Select a reason"
msgstr ""
-msgid "SecurityDashboard|No vulnerabilities found for dashboard"
+msgid "SecurityReports|Severity"
msgstr ""
-msgid "SecurityDashboard|Pipeline %{pipelineLink} triggered %{timeago} by %{user}"
+msgid "SecurityReports|Status"
msgstr ""
-msgid "SecurityDashboard|Project"
+msgid "SecurityReports|The security dashboard displays the latest security findings for projects you wish to monitor. Select \"Edit dashboard\" to add and remove projects."
msgstr ""
-msgid "SecurityDashboard|Projects added"
+msgid "SecurityReports|The security dashboard displays the latest security report. Use it to find and fix vulnerabilities."
msgstr ""
-msgid "SecurityDashboard|Remove project from dashboard"
+msgid "SecurityReports|There was an error adding the comment."
msgstr ""
-msgid "SecurityDashboard|Report type"
+msgid "SecurityReports|There was an error creating the issue."
msgstr ""
-msgid "SecurityDashboard|Return to dashboard"
+msgid "SecurityReports|There was an error creating the merge request."
msgstr ""
-msgid "SecurityDashboard|Security Dashboard"
+msgid "SecurityReports|There was an error deleting the comment."
msgstr ""
-msgid "SecurityDashboard|Select a project to add by using the project search field above."
+msgid "SecurityReports|There was an error dismissing the vulnerabilities."
msgstr ""
-msgid "SecurityDashboard|Severity"
+msgid "SecurityReports|There was an error dismissing the vulnerability."
msgstr ""
-msgid "SecurityDashboard|Status"
+msgid "SecurityReports|There was an error reverting the dismissal."
msgstr ""
-msgid "SecurityDashboard|The security dashboard displays the latest security findings for projects you wish to monitor. Select \"Edit dashboard\" to add and remove projects."
+msgid "SecurityReports|There was an error reverting this dismissal."
msgstr ""
-msgid "SecurityDashboard|The security dashboard displays the latest security report. Use it to find and fix vulnerabilities."
+msgid "SecurityReports|There was an error while generating the report."
msgstr ""
-msgid "SecurityDashboard|There was an error while generating the report."
+msgid "SecurityReports|Unable to add %{invalidProjects}"
msgstr ""
-msgid "SecurityDashboard|Unable to add %{invalidProjects}"
+msgid "SecurityReports|Undo dismiss"
+msgstr ""
+
+msgid "SecurityReports|While it's rare to have no vulnerabilities for your group, it can happen. In any event, we ask that you double check your settings to make sure you've set up your dashboard correctly."
+msgstr ""
+
+msgid "SecurityReports|While it's rare to have no vulnerabilities for your pipeline, it can happen. In any event, we ask that you double check your settings to make sure all security scanning jobs have passed successfully."
+msgstr ""
+
+msgid "SecurityReports|While it's rare to have no vulnerabilities for your project, it can happen. In any event, we ask that you double check your settings to make sure you've set up your dashboard correctly."
+msgstr ""
+
+msgid "SecurityReports|While it's rare to have no vulnerabilities, it can happen. In any event, we ask that you please double check your settings to make sure you've set up your dashboard correctly."
+msgstr ""
+
+msgid "SecurityReports|Won't fix / Accept risk"
+msgstr ""
+
+msgid "SecurityReports|You do not have sufficient permissions to access this report"
+msgstr ""
+
+msgid "SecurityReports|You must sign in as an authorized user to see this report"
+msgstr ""
+
+msgid "SecurityReports|[No reason]"
msgstr ""
msgid "See GitLab's %{password_policy_guidelines}"
@@ -18065,6 +18489,9 @@ msgstr ""
msgid "See the affected projects in the GitLab admin panel"
msgstr ""
+msgid "See what's new at GitLab"
+msgstr ""
+
msgid "Select"
msgstr ""
@@ -18470,9 +18897,6 @@ msgstr ""
msgid "Set up Jira Integration"
msgstr ""
-msgid "Set up Jira Integration illustration"
-msgstr ""
-
msgid "Set up a %{type} Runner automatically"
msgstr ""
@@ -18557,6 +18981,9 @@ msgstr ""
msgid "Severity: %{severity}"
msgstr ""
+msgid "Shards (%{shards})"
+msgstr ""
+
msgid "Shards to synchronize"
msgstr ""
@@ -18871,6 +19298,9 @@ msgstr ""
msgid "Snowplow"
msgstr ""
+msgid "Some child epics may be hidden due to applied filters"
+msgstr ""
+
msgid "Some email servers do not support overriding the email sender name. Enable this option to include the name of the author of the issue, merge request or comment in the email body instead."
msgstr ""
@@ -19012,6 +19442,9 @@ msgstr ""
msgid "Something went wrong while updating your list settings"
msgstr ""
+msgid "Something went wrong with your automatic subscription renewal"
+msgstr ""
+
msgid "Something went wrong, unable to add %{project} to dashboard"
msgstr ""
@@ -19297,6 +19730,9 @@ msgstr ""
msgid "Stage removed"
msgstr ""
+msgid "Standard"
+msgstr ""
+
msgid "Star a label to make it a priority label. Order the prioritized labels to change their relative priority, by dragging."
msgstr ""
@@ -19420,6 +19856,9 @@ msgstr ""
msgid "Static Application Security Testing (SAST)"
msgstr ""
+msgid "StaticSiteEditor|An error occurred while submitting your changes."
+msgstr ""
+
msgid "StaticSiteEditor|Branch could not be created."
msgstr ""
@@ -19429,18 +19868,30 @@ msgstr ""
msgid "StaticSiteEditor|Could not create merge request."
msgstr ""
+msgid "StaticSiteEditor|Incompatible file content"
+msgstr ""
+
msgid "StaticSiteEditor|Return to site"
msgstr ""
+msgid "StaticSiteEditor|Static site editor"
+msgstr ""
+
msgid "StaticSiteEditor|Success!"
msgstr ""
msgid "StaticSiteEditor|Summary of changes"
msgstr ""
+msgid "StaticSiteEditor|The Static Site Editor is currently configured to only edit Markdown content on pages generated from Middleman. Visit the documentation to learn more about configuring your site to use the Static Site Editor."
+msgstr ""
+
msgid "StaticSiteEditor|Update %{sourcePath} file"
msgstr ""
+msgid "StaticSiteEditor|View documentation"
+msgstr ""
+
msgid "StaticSiteEditor|View merge request"
msgstr ""
@@ -19498,6 +19949,12 @@ msgstr ""
msgid "StatusPage|Status page"
msgstr ""
+msgid "StatusPage|Status page URL"
+msgstr ""
+
+msgid "StatusPage|Status page frontend documentation"
+msgstr ""
+
msgid "StatusPage|To publish incidents to an external status page, GitLab will store a JSON file in your Amazon S3 account in a location accessible to your external status page service. Make sure to also set up %{docsLink}"
msgstr ""
@@ -19846,6 +20303,12 @@ msgstr ""
msgid "Synced"
msgstr ""
+msgid "Synchronization disabled"
+msgstr ""
+
+msgid "Synchronization of container repositories is disabled."
+msgstr ""
+
msgid "System"
msgstr ""
@@ -19999,6 +20462,9 @@ msgstr ""
msgid "Telephone number"
msgstr ""
+msgid "Telephone number (Optional)"
+msgstr ""
+
msgid "Template"
msgstr ""
@@ -20305,6 +20771,9 @@ msgstr ""
msgid "The license was successfully uploaded and is now active. You can see the details below."
msgstr ""
+msgid "The license was successfully uploaded and will be active from %{starts_at}. You can see the details below."
+msgstr ""
+
msgid "The maximum file size allowed is %{size}."
msgstr ""
@@ -20626,6 +21095,9 @@ msgstr ""
msgid "There was an error getting the epic participants."
msgstr ""
+msgid "There was an error importing the Jira project."
+msgstr ""
+
msgid "There was an error loading users activity calendar."
msgstr ""
@@ -20641,7 +21113,7 @@ msgstr ""
msgid "There was an error resetting user pipeline minutes."
msgstr ""
-msgid "There was an error saving this Geo Node"
+msgid "There was an error saving this Geo Node."
msgstr ""
msgid "There was an error saving your changes."
@@ -20704,6 +21176,9 @@ msgstr ""
msgid "They can be managed using the %{link}."
msgstr ""
+msgid "Third Party Advisory Link"
+msgstr ""
+
msgid "Third party offers"
msgstr ""
@@ -20740,6 +21215,9 @@ msgstr ""
msgid "This application will be able to:"
msgstr ""
+msgid "This attachment has been truncated to avoid exceeding the maximum allowed attachment size of 15MB. %{written_count} of %{issues_count} issues have been included. Consider re-exporting with a narrower selection of issues."
+msgstr ""
+
msgid "This block is self-referential"
msgstr ""
@@ -20881,6 +21359,12 @@ msgstr ""
msgid "This issue is confidential"
msgstr ""
+msgid "This issue is currently blocked by the following issues: %{issues}."
+msgstr ""
+
+msgid "This issue is in a child epic of the filtered epic"
+msgstr ""
+
msgid "This issue is locked."
msgstr ""
@@ -21079,7 +21563,10 @@ msgstr ""
msgid "This user will be the author of all events in the activity feed that are the result of an update, like new branches being created or new commits being pushed to existing branches. Upon creation or when reassigning you can only assign yourself to be the mirror user."
msgstr ""
-msgid "This variable can not be masked"
+msgid "This variable can not be masked."
+msgstr ""
+
+msgid "This variable does not match the expected pattern."
msgstr ""
msgid "This will help us personalize your onboarding experience."
@@ -21109,10 +21596,10 @@ msgstr ""
msgid "ThreatMonitoring|Application firewall not detected"
msgstr ""
-msgid "ThreatMonitoring|Container Network Policy"
+msgid "ThreatMonitoring|Container Network Policies are not installed or have been disabled. To view this data, ensure your Network Policies are installed and enabled for your cluster."
msgstr ""
-msgid "ThreatMonitoring|Container NetworkPolicies are not installed or has been disabled. To view this data, ensure you NetworkPolicies are installed and enabled for your cluster."
+msgid "ThreatMonitoring|Container Network Policy"
msgstr ""
msgid "ThreatMonitoring|Container NetworkPolicies not detected"
@@ -21145,7 +21632,7 @@ msgstr ""
msgid "ThreatMonitoring|Something went wrong, unable to fetch statistics"
msgstr ""
-msgid "ThreatMonitoring|The firewall is not installed or has been disabled. To view this data, ensure you firewall is installed and enabled for your cluster."
+msgid "ThreatMonitoring|The firewall is not installed or has been disabled. To view this data, ensure the web application firewall is installed and enabled for your cluster."
msgstr ""
msgid "ThreatMonitoring|The graph below is an overview of traffic coming to your application as tracked by the Web Application Firewall (WAF). View the docs for instructions on how to access the WAF logs to see what type of malicious traffic is trying to access your app. The docs link is also accessible by clicking the \"?\" icon next to the title below."
@@ -21214,6 +21701,9 @@ msgstr ""
msgid "Time in seconds GitLab will wait for a response from the external service. When the service does not respond in time, access will be denied."
msgstr ""
+msgid "Time of import: %{importTime}"
+msgstr ""
+
msgid "Time remaining"
msgstr ""
@@ -21398,7 +21888,7 @@ msgstr ""
msgid "Title:"
msgstr ""
-msgid "Titles and Filenames"
+msgid "Titles and Descriptions"
msgstr ""
msgid "To"
@@ -21605,7 +22095,7 @@ msgstr ""
msgid "Too many projects enabled. You will need to manage them via the console or the API."
msgstr ""
-msgid "Topics"
+msgid "Topics (optional)"
msgstr ""
msgid "Total"
@@ -21767,6 +22257,9 @@ msgstr ""
msgid "Try all GitLab has to offer for 30 days."
msgstr ""
+msgid "Try changing or removing filters."
+msgstr ""
+
msgid "Try to fork again"
msgstr ""
@@ -21860,6 +22353,9 @@ msgstr ""
msgid "Unable to connect to server: %{error}"
msgstr ""
+msgid "Unable to connect to the Jira instance. Please check your Jira integration configuration."
+msgstr ""
+
msgid "Unable to convert Kubernetes logs encoding to UTF-8"
msgstr ""
@@ -22803,6 +23299,9 @@ msgstr ""
msgid "View issue"
msgstr ""
+msgid "View issues"
+msgstr ""
+
msgid "View it on GitLab"
msgstr ""
@@ -22980,6 +23479,15 @@ msgstr ""
msgid "VulnerabilityManagement|Resolved %{timeago} by %{user}"
msgstr ""
+msgid "VulnerabilityManagement|Something went wrong while trying to delete the comment. Please try again later."
+msgstr ""
+
+msgid "VulnerabilityManagement|Something went wrong while trying to retrieve the vulnerability history. Please try again later."
+msgstr ""
+
+msgid "VulnerabilityManagement|Something went wrong while trying to save the comment. Please try again later."
+msgstr ""
+
msgid "VulnerabilityManagement|Something went wrong, could not create an issue."
msgstr ""
@@ -23010,6 +23518,9 @@ msgstr ""
msgid "VulnerabilityStatusTypes|Resolved"
msgstr ""
+msgid "Vulnerability|%{scannerName} (version %{scannerVersion})"
+msgstr ""
+
msgid "Vulnerability|Class"
msgstr ""
@@ -23043,7 +23554,10 @@ msgstr ""
msgid "Vulnerability|Project"
msgstr ""
-msgid "Vulnerability|Report Type"
+msgid "Vulnerability|Scanner Provider"
+msgstr ""
+
+msgid "Vulnerability|Scanner Type"
msgstr ""
msgid "Vulnerability|Severity"
@@ -23097,9 +23611,15 @@ msgstr ""
msgid "We sent you an email with reset password instructions"
msgstr ""
+msgid "We tried to automatically renew your %{strong}%{plan_name}%{strong_close} subscription for %{strong}%{namespace_name}%{strong_close} on %{expires_on} but something went wrong so your subscription was downgraded to the free plan. Don't worry, your data is safe. We suggest you check your payment method and get in touch with our support team (%{support_link}). They'll gladly help with your subscription renewal."
+msgstr ""
+
msgid "We want to be sure it is you, please confirm you are not a robot."
msgstr ""
+msgid "We will automatically renew your %{strong}%{plan_name}%{strong_close} subscription for %{strong}%{namespace_name}%{strong_close} on %{strong}%{expires_on}%{strong_close}. There's nothing that you need to do, we'll let you know when the renewal is complete. Need more seats, a higher plan or just want to review your payment method?"
+msgstr ""
+
msgid "We've found no vulnerabilities"
msgstr ""
@@ -23204,15 +23724,6 @@ msgstr ""
msgid "When:"
msgstr ""
-msgid "While it's rare to have no vulnerabilities for your group, it can happen. In any event, we ask that you double check your settings to make sure you've set up your dashboard correctly."
-msgstr ""
-
-msgid "While it's rare to have no vulnerabilities for your pipeline, it can happen. In any event, we ask that you double check your settings to make sure all security scanning jobs have passed successfully."
-msgstr ""
-
-msgid "While it's rare to have no vulnerabilities for your project, it can happen. In any event, we ask that you double check your settings to make sure you've set up your dashboard correctly."
-msgstr ""
-
msgid "While it's rare to have no vulnerabilities, it can happen. In any event, we ask that you please double check your settings to make sure you've set up your dashboard correctly."
msgstr ""
@@ -23243,9 +23754,6 @@ msgstr ""
msgid "Wiki"
msgstr ""
-msgid "Wiki pages"
-msgstr ""
-
msgid "Wiki was successfully updated."
msgstr ""
@@ -23429,6 +23937,9 @@ msgstr ""
msgid "Yes, add it"
msgstr ""
+msgid "Yes, close issue"
+msgstr ""
+
msgid "Yes, let me map Google Code users to full names or GitLab users."
msgstr ""
@@ -23624,6 +24135,9 @@ msgstr ""
msgid "You could not create a new trigger."
msgstr ""
+msgid "You didn't renew your %{strong}%{plan_name}%{strong_close} subscription for %{strong}%{namespace_name}%{strong_close} so it was downgraded to the free plan."
+msgstr ""
+
msgid "You didn't renew your %{strong}%{plan_name}%{strong_close} subscription so it was downgraded to the GitLab Core Plan."
msgstr ""
@@ -23690,6 +24204,9 @@ msgstr ""
msgid "You have declined the invitation to join %{label}."
msgstr ""
+msgid "You have imported from this project %{numberOfPreviousImportsForProject} times before. Each new import will create duplicate issues."
+msgstr ""
+
msgid "You have no permissions"
msgstr ""
@@ -23771,6 +24288,9 @@ msgstr ""
msgid "You need to upload a Google Takeout archive."
msgstr ""
+msgid "You reached %{usage_in_percent} of %{namespace_name}'s capacity (%{used_storage} of %{storage_limit})"
+msgstr ""
+
msgid "You tried to fork %{link_to_the_project} but it failed for the following reason:"
msgstr ""
@@ -23855,9 +24375,21 @@ msgstr ""
msgid "YouTube"
msgstr ""
+msgid "Your %{strong}%{plan_name}%{strong_close} subscription for %{strong}%{namespace_name}%{strong_close} will expire on %{strong}%{expires_on}%{strong_close}. After that, you will not to be able to create issues or merge requests as well as many other features."
+msgstr ""
+
msgid "Your %{strong}%{plan_name}%{strong_close} subscription will expire on %{strong}%{expires_on}%{strong_close}. After that, you will not to be able to create issues or merge requests as well as many other features."
msgstr ""
+msgid "Your CSV export has started. It will be emailed to %{email} when complete."
+msgstr ""
+
+msgid "Your CSV export of %{issues_count} from project %{project_link} has been added to this email as an attachment."
+msgstr ""
+
+msgid "Your CSV export of %{written_count} from project %{project_name} (%{project_url}) has been added to this email as an attachment."
+msgstr ""
+
msgid "Your Commit Email will be used for web based operations, such as edits and merges."
msgstr ""
@@ -23954,6 +24486,9 @@ msgstr ""
msgid "Your comment could not be updated! Please check your network connection and try again."
msgstr ""
+msgid "Your comment will be discarded."
+msgstr ""
+
msgid "Your custom stage '%{title}' was created"
msgstr ""
@@ -23993,6 +24528,9 @@ msgstr ""
msgid "Your new personal access token has been created."
msgstr ""
+msgid "Your new project access token has been created."
+msgstr ""
+
msgid "Your password isn't required to view this page. If a password or any other personal details are requested, please contact your administrator to report abuse."
msgstr ""
@@ -24011,12 +24549,18 @@ msgstr ""
msgid "Your request for access has been queued for review."
msgstr ""
+msgid "Your search didn't match any commits."
+msgstr ""
+
msgid "Your subscription expired!"
msgstr ""
msgid "Your subscription has been downgraded"
msgstr ""
+msgid "Your subscription will automatically renew in %{remaining_days}"
+msgstr ""
+
msgid "Your subscription will expire in %{remaining_days}"
msgstr ""
@@ -24055,7 +24599,7 @@ msgstr ""
msgid "allowed to fail"
msgstr ""
-msgid "already being used for another group or project milestone."
+msgid "already being used for another group or project %{timebox_name}."
msgstr ""
msgid "already has a \"created\" issue link"
@@ -24133,6 +24677,9 @@ msgstr ""
msgid "ciReport|%{linkStartTag}Learn more about SAST %{linkEndTag}"
msgstr ""
+msgid "ciReport|%{linkStartTag}Learn more about Secret Scanning %{linkEndTag}"
+msgstr ""
+
msgid "ciReport|%{linkStartTag}Learn more about codequality reports %{linkEndTag}"
msgstr ""
@@ -24296,6 +24843,12 @@ msgstr ""
msgid "ciReport|SAST"
msgstr ""
+msgid "ciReport|Secret scanning"
+msgstr ""
+
+msgid "ciReport|Secret scanning detects secrets and credentials vulnerabilities in your source code."
+msgstr ""
+
msgid "ciReport|Security scanning"
msgstr ""
@@ -24493,6 +25046,9 @@ msgstr ""
msgid "group"
msgstr ""
+msgid "groups"
+msgstr ""
+
msgid "has already been linked to another vulnerability"
msgstr ""
@@ -24656,9 +25212,6 @@ msgstr[1] ""
msgid "merged %{time_ago}"
msgstr ""
-msgid "milestone should belong either to a project or a group."
-msgstr ""
-
msgid "missing"
msgstr ""
@@ -24995,6 +25548,9 @@ msgstr ""
msgid "no contributions"
msgstr ""
+msgid "no expiration"
+msgstr ""
+
msgid "no one can merge"
msgstr ""
@@ -25047,6 +25603,9 @@ msgstr ""
msgid "pending removal"
msgstr ""
+msgid "per day"
+msgstr ""
+
msgid "pipeline"
msgstr ""
@@ -25075,6 +25634,12 @@ msgstr[1] ""
msgid "project avatar"
msgstr ""
+msgid "projects"
+msgstr ""
+
+msgid "push to your repository, create pipelines, create issues or add comments. To reduce storage capacity, delete unused repositories, artifacts, wikis, issues, and pipelines."
+msgstr ""
+
msgid "quick actions"
msgstr ""
@@ -25221,11 +25786,6 @@ msgstr ""
msgid "this document"
msgstr ""
-msgid "thread resolved"
-msgid_plural "threads resolved"
-msgstr[0] ""
-msgstr[1] ""
-
msgid "to help your contributors communicate effectively!"
msgstr ""
@@ -25280,6 +25840,9 @@ msgstr ""
msgid "view the blob"
msgstr ""
+msgid "vulnerability|Add a comment"
+msgstr ""
+
msgid "vulnerability|Add a comment or reason for dismissal"
msgstr ""
diff --git a/locale/cs_CZ/gitlab.po b/locale/cs_CZ/gitlab.po
index b25da713cb4..55a1492cdad 100644
--- a/locale/cs_CZ/gitlab.po
+++ b/locale/cs_CZ/gitlab.po
@@ -12,7 +12,7 @@ msgstr ""
"X-Crowdin-Project: gitlab-ee\n"
"X-Crowdin-Language: cs\n"
"X-Crowdin-File: /master/locale/gitlab.pot\n"
-"PO-Revision-Date: 2020-04-15 01:24\n"
+"PO-Revision-Date: 2020-05-05 21:14\n"
msgid " %{start} to %{end}"
msgstr ""
@@ -272,6 +272,13 @@ msgstr[1] ""
msgstr[2] ""
msgstr[3] ""
+msgid "%d unresolved thread"
+msgid_plural "%d unresolved threads"
+msgstr[0] ""
+msgstr[1] ""
+msgstr[2] ""
+msgstr[3] ""
+
msgid "%d vulnerability dismissed"
msgid_plural "%d vulnerabilities dismissed"
msgstr[0] ""
@@ -443,6 +450,9 @@ msgstr ""
msgid "%{mrText}, this issue will be closed automatically."
msgstr ""
+msgid "%{namespace_name} is now read-only. You cannot: %{base_message}"
+msgstr ""
+
msgid "%{name} contained %{resultsString}"
msgstr ""
@@ -455,6 +465,13 @@ msgstr ""
msgid "%{name}'s avatar"
msgstr "Profilový obrázek %{name}"
+msgid "%{no_of_days} day"
+msgid_plural "%{no_of_days} days"
+msgstr[0] ""
+msgstr[1] ""
+msgstr[2] ""
+msgstr[3] ""
+
msgid "%{number_commits_behind} commits behind %{default_branch}, %{number_commits_ahead} commits ahead"
msgstr ""
@@ -590,6 +607,9 @@ msgstr[3] ""
msgid "%{text} is available"
msgstr "%{text} je k dispozici"
+msgid "%{timebox_name} should belong either to a project or a group."
+msgstr ""
+
msgid "%{title} %{operator} %{threshold}"
msgstr ""
@@ -903,12 +923,18 @@ msgstr ""
msgid "<code>\"johnsmith@example.com\": \"johnsmith@example.com\"</code> will add \"By <a href=\"#\">johnsmith@example.com</a>\" to all issues and comments originally created by johnsmith@example.com. By default, the email address or username is masked to ensure the user's privacy. Use this option if you want to show the full email address."
msgstr ""
+msgid "<namespace / project>"
+msgstr ""
+
msgid "<no name set>"
msgstr ""
msgid "<no scopes selected>"
msgstr ""
+msgid "<project name>"
+msgstr ""
+
msgid "<strong>%{group_name}</strong> group members"
msgstr ""
@@ -942,6 +968,9 @@ msgstr ""
msgid "A basic page and serverless function that uses AWS Lambda, AWS API Gateway, and GitLab Pages"
msgstr ""
+msgid "A complete DevOps platform"
+msgstr ""
+
msgid "A default branch cannot be chosen for an empty project."
msgstr ""
@@ -1158,6 +1187,12 @@ msgstr ""
msgid "AccessTokens|reset it"
msgstr ""
+msgid "AccessibilityReport|Accessibility report artifact not found"
+msgstr ""
+
+msgid "AccessibilityReport|Failed to retrieve accessibility report"
+msgstr ""
+
msgid "AccessibilityReport|Learn More"
msgstr ""
@@ -1167,6 +1202,9 @@ msgstr ""
msgid "AccessibilityReport|New"
msgstr ""
+msgid "AccessibilityReport|The accessibility scanning found an error of the following type: %{code}"
+msgstr ""
+
msgid "Account"
msgstr ""
@@ -1411,6 +1449,9 @@ msgstr ""
msgid "Added at"
msgstr ""
+msgid "Added for this merge request"
+msgstr ""
+
msgid "Added in this version"
msgstr ""
@@ -1784,6 +1825,72 @@ msgstr[1] ""
msgstr[2] ""
msgstr[3] ""
+msgid "Alert Details"
+msgstr ""
+
+msgid "AlertManagement|Acknowledged"
+msgstr ""
+
+msgid "AlertManagement|Alert"
+msgstr ""
+
+msgid "AlertManagement|Authorize external service"
+msgstr ""
+
+msgid "AlertManagement|Display alerts from all your monitoring tools directly within GitLab. Streamline the investigation of your alerts and the escalation of alerts to incidents."
+msgstr ""
+
+msgid "AlertManagement|End time"
+msgstr ""
+
+msgid "AlertManagement|End time:"
+msgstr ""
+
+msgid "AlertManagement|Events"
+msgstr ""
+
+msgid "AlertManagement|Events:"
+msgstr ""
+
+msgid "AlertManagement|Full Alert Details"
+msgstr ""
+
+msgid "AlertManagement|More information"
+msgstr ""
+
+msgid "AlertManagement|No alerts available to display. If you think you're seeing this message in error, refresh the page."
+msgstr ""
+
+msgid "AlertManagement|No alerts to display."
+msgstr ""
+
+msgid "AlertManagement|Overview"
+msgstr ""
+
+msgid "AlertManagement|Resolved"
+msgstr ""
+
+msgid "AlertManagement|Severity"
+msgstr ""
+
+msgid "AlertManagement|Start time"
+msgstr ""
+
+msgid "AlertManagement|Start time:"
+msgstr ""
+
+msgid "AlertManagement|Status"
+msgstr ""
+
+msgid "AlertManagement|Surface alerts in GitLab"
+msgstr ""
+
+msgid "AlertManagement|There was an error displaying the alerts. Confirm your endpoint's configuration details to ensure alerts appear."
+msgstr ""
+
+msgid "AlertManagement|Triggered"
+msgstr ""
+
msgid "AlertService|%{linkStart}Learn more%{linkEnd} about configuring this endpoint to receive alerts."
msgstr ""
@@ -1853,6 +1960,9 @@ msgstr ""
msgid "All security scans are enabled because %{linkStart}Auto DevOps%{linkEnd} is enabled on this project"
msgstr ""
+msgid "All threads resolved"
+msgstr ""
+
msgid "All users"
msgstr ""
@@ -1871,6 +1981,9 @@ msgstr ""
msgid "Allow only the selected protocols to be used for Git access."
msgstr ""
+msgid "Allow owners to manage default branch protection per group"
+msgstr ""
+
msgid "Allow owners to manually add users outside of LDAP"
msgstr ""
@@ -1979,6 +2092,9 @@ msgstr ""
msgid "An error occurred fetching the dropdown data."
msgstr ""
+msgid "An error occurred fetching the project authors."
+msgstr ""
+
msgid "An error occurred previewing the blob"
msgstr ""
@@ -2060,6 +2176,9 @@ msgstr ""
msgid "An error occurred while fetching sidebar data"
msgstr ""
+msgid "An error occurred while fetching terraform reports."
+msgstr ""
+
msgid "An error occurred while fetching the Service Desk address."
msgstr ""
@@ -2297,6 +2416,9 @@ msgstr ""
msgid "Any"
msgstr ""
+msgid "Any Author"
+msgstr ""
+
msgid "Any Label"
msgstr ""
@@ -2520,10 +2642,10 @@ msgstr ""
msgid "Are you sure that you want to unarchive this project?"
msgstr ""
-msgid "Are you sure you want to cancel creating this comment?"
+msgid "Are you sure you want to cancel editing this comment?"
msgstr ""
-msgid "Are you sure you want to cancel editing this comment?"
+msgid "Are you sure you want to close this blocked issue?"
msgstr ""
msgid "Are you sure you want to delete %{name}?"
@@ -2553,6 +2675,9 @@ msgstr ""
msgid "Are you sure you want to deploy this environment?"
msgstr ""
+msgid "Are you sure you want to discard this comment?"
+msgstr ""
+
msgid "Are you sure you want to erase this build?"
msgstr ""
@@ -2731,6 +2856,9 @@ msgstr ""
msgid "At least one approval from a code owner is required to change files matching the respective CODEOWNER rules."
msgstr ""
+msgid "At least one logging option is required to be enabled"
+msgstr ""
+
msgid "At least one of group_id or project_id must be specified"
msgstr ""
@@ -2771,6 +2899,27 @@ msgstr ""
msgid "AuditEvents|Target"
msgstr ""
+msgid "AuditLogs|(removed)"
+msgstr ""
+
+msgid "AuditLogs|Action"
+msgstr ""
+
+msgid "AuditLogs|Author"
+msgstr ""
+
+msgid "AuditLogs|Date"
+msgstr ""
+
+msgid "AuditLogs|IP Address"
+msgstr ""
+
+msgid "AuditLogs|Object"
+msgstr ""
+
+msgid "AuditLogs|Target"
+msgstr ""
+
msgid "Aug"
msgstr "Srp"
@@ -3053,9 +3202,6 @@ msgstr ""
msgid "BambooService|You must set up automatic revision labeling and a repository trigger in Bamboo."
msgstr ""
-msgid "Batch operations"
-msgstr ""
-
msgid "BatchComments|Delete all pending comments"
msgstr ""
@@ -3185,6 +3331,9 @@ msgstr ""
msgid "Boards"
msgstr ""
+msgid "Boards and Board Lists"
+msgstr ""
+
msgid "Boards|Collapse"
msgstr ""
@@ -3404,6 +3553,9 @@ msgstr ""
msgid "Buy GitLab Enterprise Edition"
msgstr ""
+msgid "Buy more Pipeline minutes"
+msgstr ""
+
msgid "By %{user_name}"
msgstr ""
@@ -3524,6 +3676,9 @@ msgstr ""
msgid "Can't scan the code?"
msgstr ""
+msgid "Can't update snippet: %{err}"
+msgstr ""
+
msgid "Canary"
msgstr ""
@@ -3692,6 +3847,9 @@ msgstr ""
msgid "Changing group path can have unintended side effects."
msgstr ""
+msgid "Charts"
+msgstr ""
+
msgid "Charts can't be displayed as the request for data has timed out. %{documentationLink}"
msgstr ""
@@ -3728,9 +3886,6 @@ msgstr ""
msgid "ChatMessage|and [%{count} more](%{pipeline_failed_jobs_url})"
msgstr ""
-msgid "ChatMessage|failed"
-msgstr ""
-
msgid "ChatMessage|has failed"
msgstr ""
@@ -3746,9 +3901,6 @@ msgstr ""
msgid "ChatMessage|in %{project_link}"
msgstr ""
-msgid "ChatMessage|passed"
-msgstr ""
-
msgid "Check again"
msgstr ""
@@ -4199,9 +4351,6 @@ msgstr ""
msgid "Click any <strong>project name</strong> in the project list below to navigate to the project milestone."
msgstr ""
-msgid "Click here"
-msgstr ""
-
msgid "Click the <strong>Download</strong> button and wait for downloading to complete."
msgstr ""
@@ -4589,6 +4738,12 @@ msgstr ""
msgid "ClusterIntegration|Fetching zones"
msgstr ""
+msgid "ClusterIntegration|Fluentd"
+msgstr ""
+
+msgid "ClusterIntegration|Fluentd is an open source data collector, which lets you unify the data collection and consumption for a better use and understanding of data. It requires at least one of the following logs to be successfully installed."
+msgstr ""
+
msgid "ClusterIntegration|GitLab Integration"
msgstr ""
@@ -4895,6 +5050,15 @@ msgstr ""
msgid "ClusterIntegration|Request to begin uninstalling failed"
msgstr ""
+msgid "ClusterIntegration|SIEM Hostname"
+msgstr ""
+
+msgid "ClusterIntegration|SIEM Port"
+msgstr ""
+
+msgid "ClusterIntegration|SIEM Protocol"
+msgstr ""
+
msgid "ClusterIntegration|Save changes"
msgstr ""
@@ -4991,6 +5155,12 @@ msgstr ""
msgid "ClusterIntegration|Select zone to choose machine type"
msgstr ""
+msgid "ClusterIntegration|Send Cilium Logs"
+msgstr ""
+
+msgid "ClusterIntegration|Send ModSecurity Logs"
+msgstr ""
+
msgid "ClusterIntegration|Service Token"
msgstr ""
@@ -5421,18 +5591,33 @@ msgstr ""
msgid "Compliance framework (optional)"
msgstr ""
+msgid "ComplianceFramework|GDPR"
+msgstr ""
+
msgid "ComplianceFramework|GDPR - General Data Protection Regulation"
msgstr ""
+msgid "ComplianceFramework|HIPAA"
+msgstr ""
+
msgid "ComplianceFramework|HIPAA - Health Insurance Portability and Accountability Act"
msgstr ""
+msgid "ComplianceFramework|PCI-DSS"
+msgstr ""
+
msgid "ComplianceFramework|PCI-DSS - Payment Card Industry-Data Security Standard"
msgstr ""
+msgid "ComplianceFramework|SOC 2"
+msgstr ""
+
msgid "ComplianceFramework|SOC 2 - Service Organization Control 2"
msgstr ""
+msgid "ComplianceFramework|SOX"
+msgstr ""
+
msgid "ComplianceFramework|SOX - Sarbanes-Oxley"
msgstr ""
@@ -5580,9 +5765,15 @@ msgstr ""
msgid "Container repositories sync capacity"
msgstr ""
+msgid "ContainerRegistry| Please visit the %{linkStart}administration settings%{linkEnd} to enable this feature."
+msgstr ""
+
msgid "ContainerRegistry|%{imageName} tags"
msgstr ""
+msgid "ContainerRegistry|%{title} was successfully scheduled for deletion"
+msgstr ""
+
msgid "ContainerRegistry|Automatically remove extra images that aren't designed to be kept."
msgstr ""
@@ -5595,6 +5786,9 @@ msgstr ""
msgid "ContainerRegistry|Container Registry"
msgstr ""
+msgid "ContainerRegistry|Container Registry tag expiration and retention policy is disabled"
+msgstr ""
+
msgid "ContainerRegistry|Copy build command"
msgstr ""
@@ -5604,18 +5798,12 @@ msgstr ""
msgid "ContainerRegistry|Copy push command"
msgstr ""
-msgid "ContainerRegistry|Currently, the Container Registry tag expiration feature is not available for projects created before GitLab version 12.8. For updates and more information, visit Issue %{linkStart}#196124%{linkEnd}"
-msgstr ""
-
msgid "ContainerRegistry|Docker connection error"
msgstr ""
msgid "ContainerRegistry|Docker tag expiration policy is %{toggleStatus}"
msgstr ""
-msgid "ContainerRegistry|Docker tags with names matching this regex pattern will expire:"
-msgstr ""
-
msgid "ContainerRegistry|Edit Settings"
msgstr ""
@@ -5637,9 +5825,6 @@ msgstr ""
msgid "ContainerRegistry|Image ID"
msgstr ""
-msgid "ContainerRegistry|Image deleted successfully"
-msgstr ""
-
msgid "ContainerRegistry|Keep and protect the images that matter most."
msgstr ""
@@ -5655,12 +5840,24 @@ msgstr ""
msgid "ContainerRegistry|Number of tags to retain:"
msgstr ""
+msgid "ContainerRegistry|Please contact your administrator."
+msgstr ""
+
msgid "ContainerRegistry|Push an image"
msgstr ""
msgid "ContainerRegistry|Quick Start"
msgstr ""
+msgid "ContainerRegistry|Regular expressions such as %{codeStart}.*-test%{codeEnd} or %{codeStart}dev-.*%{codeEnd} are supported"
+msgstr ""
+
+msgid "ContainerRegistry|Regular expressions such as %{codeStart}.*-test%{codeEnd} or %{codeStart}dev-.*%{codeEnd} are supported. To select all tags, use %{codeStart}.*%{codeEnd}"
+msgstr ""
+
+msgid "ContainerRegistry|Remember to run %{docLinkStart}garbage collection%{docLinkEnd} to remove the stale data from storage."
+msgstr ""
+
msgid "ContainerRegistry|Remove repository"
msgstr ""
@@ -5677,22 +5874,22 @@ msgstr[3] ""
msgid "ContainerRegistry|Retention policy has been Enabled"
msgstr ""
-msgid "ContainerRegistry|Something went wrong while deleting the image."
+msgid "ContainerRegistry|Something went wrong while fetching the expiration policy."
msgstr ""
-msgid "ContainerRegistry|Something went wrong while deleting the tag."
+msgid "ContainerRegistry|Something went wrong while fetching the repository list."
msgstr ""
-msgid "ContainerRegistry|Something went wrong while deleting the tags."
+msgid "ContainerRegistry|Something went wrong while fetching the tags list."
msgstr ""
-msgid "ContainerRegistry|Something went wrong while fetching the expiration policy."
+msgid "ContainerRegistry|Something went wrong while marking the tag for deletion."
msgstr ""
-msgid "ContainerRegistry|Something went wrong while fetching the packages list."
+msgid "ContainerRegistry|Something went wrong while marking the tags for deletion."
msgstr ""
-msgid "ContainerRegistry|Something went wrong while fetching the tags list."
+msgid "ContainerRegistry|Something went wrong while scheduling %{title} for deletion. Please try again."
msgstr ""
msgid "ContainerRegistry|Something went wrong while updating the expiration policy."
@@ -5701,16 +5898,25 @@ msgstr ""
msgid "ContainerRegistry|Tag"
msgstr ""
-msgid "ContainerRegistry|Tag deleted successfully"
-msgstr ""
-
msgid "ContainerRegistry|Tag expiration policy"
msgstr ""
msgid "ContainerRegistry|Tag expiration policy is designed to:"
msgstr ""
-msgid "ContainerRegistry|Tags deleted successfully"
+msgid "ContainerRegistry|Tag successfully marked for deletion."
+msgstr ""
+
+msgid "ContainerRegistry|Tags successfully marked for deletion."
+msgstr ""
+
+msgid "ContainerRegistry|Tags with names matching this regex pattern will %{italicStart}be preserved:%{italicEnd}"
+msgstr ""
+
+msgid "ContainerRegistry|Tags with names matching this regex pattern will %{italicStart}expire:%{italicEnd}"
+msgstr ""
+
+msgid "ContainerRegistry|The Container Registry tag expiration and retention policies for this project have not been enabled."
msgstr ""
msgid "ContainerRegistry|The last tag related to this image was recently removed. This empty image and any associated data will be automatically removed as part of the regular Garbage Collection process. If you have any questions, contact your administrator."
@@ -5731,16 +5937,16 @@ msgstr ""
msgid "ContainerRegistry|There are no container images stored for this project"
msgstr ""
-msgid "ContainerRegistry|This Registry contains deleted image tag data. Remember to run %{docLinkStart}garbage collection%{docLinkEnd} to remove the stale data from storage."
+msgid "ContainerRegistry|There was an error during the deletion of this image repository, please try again."
msgstr ""
msgid "ContainerRegistry|This image has no active tags"
msgstr ""
-msgid "ContainerRegistry|We are having trouble connecting to Docker, which could be due to an issue with your project name or path. %{docLinkStart}More Information%{docLinkEnd}"
+msgid "ContainerRegistry|This image repository is scheduled for deletion"
msgstr ""
-msgid "ContainerRegistry|Wildcards such as %{codeStart}.*-stable%{codeEnd} or %{codeStart}production/.*%{codeEnd} are supported. To select all tags, use %{codeStart}.*%{codeEnd}"
+msgid "ContainerRegistry|We are having trouble connecting to Docker, which could be due to an issue with your project name or path. %{docLinkStart}More Information%{docLinkEnd}"
msgstr ""
msgid "ContainerRegistry|With the Container Registry, every project can have its own space to store its Docker images. %{docLinkStart}More Information%{docLinkEnd}"
@@ -5995,7 +6201,7 @@ msgstr ""
msgid "Could not delete chat nickname %{chat_name}."
msgstr ""
-msgid "Could not find design"
+msgid "Could not find design."
msgstr ""
msgid "Could not remove the trigger."
@@ -6010,6 +6216,9 @@ msgstr ""
msgid "Could not revoke personal access token %{personal_access_token_name}."
msgstr ""
+msgid "Could not revoke project access token %{project_access_token_name}."
+msgstr ""
+
msgid "Could not save group ID"
msgstr ""
@@ -6067,6 +6276,9 @@ msgstr ""
msgid "Create a new branch"
msgstr ""
+msgid "Create a new deploy key for this project"
+msgstr ""
+
msgid "Create a new file as there are no files yet. Afterwards, you'll be able to commit your changes."
msgstr ""
@@ -6172,6 +6384,9 @@ msgstr ""
msgid "Create requirement"
msgstr ""
+msgid "Create snippet"
+msgstr ""
+
msgid "Create wildcard: %{searchTerm}"
msgstr ""
@@ -6193,6 +6408,9 @@ msgstr ""
msgid "Created"
msgstr ""
+msgid "Created %{timestamp}"
+msgstr ""
+
msgid "Created At"
msgstr ""
@@ -6551,6 +6769,9 @@ msgstr ""
msgid "CycleAnalytics|stage dropdown"
msgstr ""
+msgid "DAG"
+msgstr ""
+
msgid "DNS"
msgstr ""
@@ -6929,6 +7150,9 @@ msgstr ""
msgid "Deploy key was successfully updated."
msgstr ""
+msgid "Deploy keys allow read-only or read-write (if enabled) access to your repository. Deploy keys can be used for CI, staging or production servers. You can create a deploy key or add an existing one."
+msgstr ""
+
msgid "Deploy progress not found. To see pods, ensure your environment matches %{linkStart}deploy board criteria%{linkEnd}."
msgstr ""
@@ -7082,6 +7306,9 @@ msgstr ""
msgid "Deploying to"
msgstr ""
+msgid "Deployment Frequency"
+msgstr ""
+
msgid "Deployment|API"
msgstr ""
@@ -7602,9 +7829,6 @@ msgstr ""
msgid "Edit environment"
msgstr ""
-msgid "Edit epic description"
-msgstr ""
-
msgid "Edit file"
msgstr ""
@@ -8226,9 +8450,6 @@ msgstr ""
msgid "Epics"
msgstr ""
-msgid "Epics (Ultimate / Gold license only)"
-msgstr ""
-
msgid "Epics Roadmap"
msgstr ""
@@ -8238,6 +8459,9 @@ msgstr ""
msgid "Epics let you manage your portfolio of projects more efficiently and with less effort"
msgstr ""
+msgid "Epics, Issues, and Merge Requests"
+msgstr ""
+
msgid "Epics|Add an epic"
msgstr ""
@@ -8289,6 +8513,9 @@ msgstr ""
msgid "Epics|Something went wrong while fetching group epics."
msgstr ""
+msgid "Epics|Something went wrong while moving item."
+msgstr ""
+
msgid "Epics|Something went wrong while ordering item."
msgstr ""
@@ -8343,6 +8570,9 @@ msgstr ""
msgid "Error fetching network graph."
msgstr ""
+msgid "Error fetching payload data."
+msgstr ""
+
msgid "Error fetching projects"
msgstr ""
@@ -8352,9 +8582,6 @@ msgstr ""
msgid "Error fetching the dependency list. Please check your network connection and try again."
msgstr ""
-msgid "Error fetching usage ping data."
-msgstr ""
-
msgid "Error loading branch data. Please try again."
msgstr ""
@@ -8496,6 +8723,9 @@ msgstr ""
msgid "Errors"
msgstr ""
+msgid "Errors:"
+msgstr ""
+
msgid "Estimated"
msgstr ""
@@ -8700,12 +8930,18 @@ msgstr ""
msgid "Export as CSV"
msgstr ""
+msgid "Export group"
+msgstr ""
+
msgid "Export issues"
msgstr ""
msgid "Export project"
msgstr ""
+msgid "Export this group with all related data to a new GitLab instance. Once complete, you can import the data file from the \"New Group\" page."
+msgstr ""
+
msgid "Export this project with all its related data in order to move your project to a new GitLab instance. Once the export is finished, you can import the file from the \"New Project\" page."
msgstr ""
@@ -8775,9 +9011,6 @@ msgstr ""
msgid "Failed Jobs"
msgstr ""
-msgid "Failed create wiki"
-msgstr ""
-
msgid "Failed to add a Zoom meeting"
msgstr ""
@@ -8817,6 +9050,9 @@ msgstr ""
msgid "Failed to create resources"
msgstr ""
+msgid "Failed to create wiki"
+msgstr ""
+
msgid "Failed to delete board. Please try again."
msgstr ""
@@ -8826,7 +9062,7 @@ msgstr ""
msgid "Failed to enqueue the rebase operation, possibly due to a long-lived transaction. Try again later."
msgstr ""
-msgid "Failed to find import label for jira import."
+msgid "Failed to find import label for Jira import."
msgstr ""
msgid "Failed to get ref."
@@ -8850,6 +9086,12 @@ msgstr ""
msgid "Failed to load groups & users."
msgstr ""
+msgid "Failed to load labels. Please try again."
+msgstr ""
+
+msgid "Failed to load milestones. Please try again."
+msgstr ""
+
msgid "Failed to load related branches"
msgstr ""
@@ -9225,7 +9467,10 @@ msgstr ""
msgid "Filter by milestone name"
msgstr ""
-msgid "Filter by name..."
+msgid "Filter by name"
+msgstr ""
+
+msgid "Filter by status"
msgstr ""
msgid "Filter by two-factor authentication"
@@ -9237,12 +9482,18 @@ msgstr ""
msgid "Filter projects"
msgstr ""
+msgid "Filter results"
+msgstr ""
+
msgid "Filter results by group"
msgstr ""
msgid "Filter results by project"
msgstr ""
+msgid "Filter results..."
+msgstr ""
+
msgid "Filter your projects by name"
msgstr ""
@@ -9531,10 +9782,10 @@ msgstr ""
msgid "GeoNodes|Checksummed"
msgstr ""
-msgid "GeoNodes|Container repositories"
+msgid "GeoNodes|Consult Geo troubleshooting information"
msgstr ""
-msgid "GeoNodes|Data is out of date from %{timeago}"
+msgid "GeoNodes|Container repositories"
msgstr ""
msgid "GeoNodes|Data replication lag"
@@ -9576,6 +9827,9 @@ msgstr ""
msgid "GeoNodes|Last event ID seen from primary"
msgstr ""
+msgid "GeoNodes|Learn more about Geo node statuses"
+msgstr ""
+
msgid "GeoNodes|Loading nodes"
msgstr ""
@@ -9591,6 +9845,9 @@ msgstr ""
msgid "GeoNodes|Node was successfully removed."
msgstr ""
+msgid "GeoNodes|Node's status was updated %{timeAgo}."
+msgstr ""
+
msgid "GeoNodes|Not checksummed"
msgstr ""
@@ -9621,7 +9878,10 @@ msgstr ""
msgid "GeoNodes|Repository verification progress"
msgstr ""
-msgid "GeoNodes|Selective"
+msgid "GeoNodes|Selective (%{syncLabel})"
+msgstr ""
+
+msgid "GeoNodes|Selective synchronization"
msgstr ""
msgid "GeoNodes|Something went wrong while changing node status"
@@ -9648,6 +9908,9 @@ msgstr ""
msgid "GeoNodes|Unverified"
msgstr ""
+msgid "GeoNodes|Updated %{timeAgo}"
+msgstr ""
+
msgid "GeoNodes|Used slots"
msgstr ""
@@ -9687,16 +9950,16 @@ msgstr ""
msgid "Geo|All"
msgstr ""
-msgid "Geo|All projects"
+msgid "Geo|All %{replicable_type}"
msgstr ""
-msgid "Geo|All projects are being scheduled for re-sync"
+msgid "Geo|All projects"
msgstr ""
-msgid "Geo|All projects are being scheduled for re-verify"
+msgid "Geo|All projects are being scheduled for resync"
msgstr ""
-msgid "Geo|Batch operations"
+msgid "Geo|All projects are being scheduled for reverify"
msgstr ""
msgid "Geo|Could not remove tracking entry for an existing project."
@@ -9708,9 +9971,15 @@ msgstr ""
msgid "Geo|Failed"
msgstr ""
+msgid "Geo|Filter by status"
+msgstr ""
+
msgid "Geo|Geo Status"
msgstr ""
+msgid "Geo|In progress"
+msgstr ""
+
msgid "Geo|In sync"
msgstr ""
@@ -9735,9 +10004,6 @@ msgstr ""
msgid "Geo|Not synced yet"
msgstr ""
-msgid "Geo|Pending"
-msgstr ""
-
msgid "Geo|Pending synchronization"
msgstr ""
@@ -9774,7 +10040,7 @@ msgstr ""
msgid "Geo|Resync"
msgstr ""
-msgid "Geo|Resync all projects"
+msgid "Geo|Resync all"
msgstr ""
msgid "Geo|Retry count"
@@ -9783,7 +10049,7 @@ msgstr ""
msgid "Geo|Reverify"
msgstr ""
-msgid "Geo|Reverify all projects"
+msgid "Geo|Reverify all"
msgstr ""
msgid "Geo|Status"
@@ -10125,9 +10391,6 @@ msgstr ""
msgid "Go to file"
msgstr ""
-msgid "Go to file (MRs only)"
-msgstr ""
-
msgid "Go to file permalink (while viewing a file)"
msgstr ""
@@ -10572,6 +10835,9 @@ msgstr ""
msgid "GroupSettings|Disable group mentions"
msgstr ""
+msgid "GroupSettings|Export group"
+msgstr ""
+
msgid "GroupSettings|If the parent group's visibility is lower than the group current visibility, visibility levels for subgroups and projects will be changed to match the new parent group's visibility."
msgstr ""
@@ -10650,6 +10916,9 @@ msgstr ""
msgid "Groups (%{count})"
msgstr ""
+msgid "Groups (%{groups})"
+msgstr ""
+
msgid "Groups can also be nested by creating %{subgroup_docs_link_start}subgroups%{subgroup_docs_link_end}."
msgstr ""
@@ -10919,15 +11188,12 @@ msgstr ""
msgid "ID:"
msgstr ""
-msgid "IDE|Allow live previews of JavaScript projects in the Web IDE using CodeSandbox client side evaluation."
+msgid "IDE|Allow live previews of JavaScript projects in the Web IDE using CodeSandbox Live Preview."
msgstr ""
msgid "IDE|Back"
msgstr ""
-msgid "IDE|Client side evaluation"
-msgstr ""
-
msgid "IDE|Commit"
msgstr ""
@@ -10991,6 +11257,9 @@ msgstr ""
msgid "If any job surpasses this timeout threshold, it will be marked as failed. Human readable time input language is accepted like \"1 hour\". Values without specification represent seconds."
msgstr ""
+msgid "If blank, set allowable lifetime to %{instance_level_policy_in_words}, as defined by the instance admin. Once set, existing tokens for users in this group may be revoked."
+msgstr ""
+
msgid "If checked, group owners can manage LDAP group links and LDAP member overrides"
msgstr ""
@@ -11030,6 +11299,9 @@ msgstr ""
msgid "If you lose your recovery codes you can generate new ones, invalidating all previous codes."
msgstr ""
+msgid "If you reach 100%% storage capacity, you will not be able to: %{base_message}"
+msgstr ""
+
msgid "If your HTTP repository is not publicly accessible, add your credentials."
msgstr ""
@@ -11141,6 +11413,9 @@ msgstr ""
msgid "Import repository"
msgstr ""
+msgid "Import started by: %{importInitiator}"
+msgstr ""
+
msgid "Import tasks"
msgstr ""
@@ -11207,6 +11482,9 @@ msgstr ""
msgid "In order to tailor your experience with GitLab we<br>would like to know a bit more about you."
msgstr ""
+msgid "In progress"
+msgstr ""
+
msgid "In the next step, you'll be able to select the projects you want to import."
msgstr ""
@@ -11556,15 +11834,15 @@ msgstr ""
msgid "Issues"
msgstr ""
-msgid "Issues / Merge Requests"
-msgstr ""
-
msgid "Issues Analytics"
msgstr ""
msgid "Issues Rate Limits"
msgstr ""
+msgid "Issues and Merge Requests"
+msgstr ""
+
msgid "Issues can be bugs, tasks or ideas to be discussed. Also, issues are searchable and filterable."
msgstr ""
@@ -11586,10 +11864,10 @@ msgstr ""
msgid "IssuesAnalytics|Avg/Month:"
msgstr ""
-msgid "IssuesAnalytics|Issues created"
+msgid "IssuesAnalytics|Issues opened"
msgstr ""
-msgid "IssuesAnalytics|Issues created per month"
+msgid "IssuesAnalytics|Issues opened per month"
msgstr ""
msgid "IssuesAnalytics|Last 12 months"
@@ -11643,6 +11921,9 @@ msgstr ""
msgid "Jira integration not configured."
msgstr ""
+msgid "Jira project: %{importProject}"
+msgstr ""
+
msgid "JiraService|Events for %{noteable_model_name} are disabled."
msgstr ""
@@ -12034,6 +12315,9 @@ msgstr ""
msgid "Lead"
msgstr ""
+msgid "Lead Time"
+msgstr ""
+
msgid "Learn GitLab"
msgstr ""
@@ -12046,6 +12330,9 @@ msgstr ""
msgid "Learn how to %{no_packages_link_start}publish and share your packages%{no_packages_link_end} with GitLab."
msgstr ""
+msgid "Learn how to enable synchronization"
+msgstr ""
+
msgid "Learn more"
msgstr ""
@@ -12387,9 +12674,6 @@ msgstr ""
msgid "Live preview"
msgstr ""
-msgid "Load more vulnerabilities"
-msgstr ""
-
msgid "Loading"
msgstr ""
@@ -12540,9 +12824,6 @@ msgstr ""
msgid "Manage"
msgstr ""
-msgid "Manage Git repositories with fine-grained access controls that keep your code secure. Perform code reviews and enhance collaboration with merge requests. Each project can also have an issue tracker and a wiki."
-msgstr ""
-
msgid "Manage Web IDE features"
msgstr ""
@@ -12573,6 +12854,9 @@ msgstr ""
msgid "Manage your license"
msgstr ""
+msgid "Managed Account"
+msgstr ""
+
msgid "Manifest"
msgstr ""
@@ -12837,6 +13121,9 @@ msgstr ""
msgid "Merge Requests in Review"
msgstr ""
+msgid "Merge automatically (%{strategy})"
+msgstr ""
+
msgid "Merge commit message"
msgstr ""
@@ -12990,6 +13277,12 @@ msgstr ""
msgid "Merged branches are being deleted. This can take some time depending on the number of branches. Please refresh the page to see changes."
msgstr ""
+msgid "Merged this merge request."
+msgstr ""
+
+msgid "Merges this merge request immediately."
+msgstr ""
+
msgid "Merges this merge request when the pipeline succeeds."
msgstr ""
@@ -13099,12 +13392,15 @@ msgstr[1] ""
msgstr[2] ""
msgstr[3] ""
-msgid "Metrics|Environment"
+msgid "Metrics|Expand panel"
msgstr ""
msgid "Metrics|For grouping similar metrics"
msgstr ""
+msgid "Metrics|Go back (Esc)"
+msgstr ""
+
msgid "Metrics|Invalid time range, please verify."
msgstr ""
@@ -13135,9 +13431,6 @@ msgstr ""
msgid "Metrics|Refresh dashboard"
msgstr ""
-msgid "Metrics|Show last"
-msgstr ""
-
msgid "Metrics|There was an error creating the dashboard."
msgstr ""
@@ -13216,6 +13509,9 @@ msgstr ""
msgid "Microsoft Azure"
msgstr ""
+msgid "Middleman project with Static Site Editor support"
+msgstr ""
+
msgid "Migrated %{success_count}/%{total_count} files."
msgstr ""
@@ -13665,10 +13961,10 @@ msgstr ""
msgid "Next"
msgstr ""
-msgid "Next file in diff (MRs only)"
+msgid "Next file in diff"
msgstr ""
-msgid "Next unresolved discussion (MRs only)"
+msgid "Next unresolved discussion"
msgstr ""
msgid "Nickname"
@@ -13728,6 +14024,9 @@ msgstr ""
msgid "No changes between %{ref_start}%{source_branch}%{ref_end} and %{ref_start}%{target_branch}%{ref_end}"
msgstr ""
+msgid "No child epics match applied filters"
+msgstr ""
+
msgid "No connection could be made to a Gitaly Server, please check your logs!"
msgstr ""
@@ -13866,15 +14165,6 @@ msgstr ""
msgid "No thanks, don't show this again"
msgstr ""
-msgid "No vulnerabilities found for this group"
-msgstr ""
-
-msgid "No vulnerabilities found for this pipeline"
-msgstr ""
-
-msgid "No vulnerabilities found for this project"
-msgstr ""
-
msgid "No vulnerabilities present"
msgstr ""
@@ -13998,6 +14288,9 @@ msgstr ""
msgid "Nothing to preview."
msgstr ""
+msgid "Nothing to synchronize"
+msgstr ""
+
msgid "Notification events"
msgstr ""
@@ -14169,6 +14462,9 @@ msgstr ""
msgid "Once removed, the fork relationship cannot be restored and you will no longer be able to send merge requests to the source."
msgstr ""
+msgid "Once the exported file is ready you can download it from this page."
+msgstr ""
+
msgid "Once the exported file is ready, you will receive a notification email with a download link, or you can download it from this page."
msgstr ""
@@ -14272,9 +14568,6 @@ msgstr ""
msgid "Open sidebar"
msgstr ""
-msgid "Open source software to collaborate on code"
-msgstr ""
-
msgid "Open: %{openIssuesCount}"
msgstr ""
@@ -14377,6 +14670,9 @@ msgstr ""
msgid "OutdatedBrowser|You can provide feedback %{feedback_link_start}on this issue%{feedback_link_end} or via your usual support channels."
msgstr ""
+msgid "Overridden"
+msgstr ""
+
msgid "Overview"
msgstr ""
@@ -14428,6 +14724,9 @@ msgstr ""
msgid "PackageRegistry|Conan Command"
msgstr ""
+msgid "PackageRegistry|Copy .pypirc content"
+msgstr ""
+
msgid "PackageRegistry|Copy Conan Command"
msgstr ""
@@ -14449,6 +14748,9 @@ msgstr ""
msgid "PackageRegistry|Copy NuGet Setup Command"
msgstr ""
+msgid "PackageRegistry|Copy Pip command"
+msgstr ""
+
msgid "PackageRegistry|Copy and paste this inside your %{codeStart}pom.xml%{codeEnd} %{codeStart}dependencies%{codeEnd} block."
msgstr ""
@@ -14482,6 +14784,12 @@ msgstr ""
msgid "PackageRegistry|For more information on the NuGet registry, %{linkStart}see the documentation%{linkEnd}."
msgstr ""
+msgid "PackageRegistry|For more information on the PyPi registry, %{linkStart}see the documentation%{linkEnd}."
+msgstr ""
+
+msgid "PackageRegistry|If you haven't already done so, you will need to add the below to your %{codeStart}.pypirc%{codeEnd} file."
+msgstr ""
+
msgid "PackageRegistry|If you haven't already done so, you will need to add the below to your %{codeStart}pom.xml%{codeEnd} file."
msgstr ""
@@ -14491,6 +14799,9 @@ msgstr ""
msgid "PackageRegistry|Learn how to %{noPackagesLinkStart}publish and share your packages%{noPackagesLinkEnd} with GitLab."
msgstr ""
+msgid "PackageRegistry|Manually Published"
+msgstr ""
+
msgid "PackageRegistry|Maven"
msgstr ""
@@ -14509,12 +14820,18 @@ msgstr ""
msgid "PackageRegistry|NuGet Command"
msgstr ""
+msgid "PackageRegistry|Pip Command"
+msgstr ""
+
msgid "PackageRegistry|Pipeline %{linkStart}%{linkEnd} triggered %{timestamp} by %{author}"
msgstr ""
msgid "PackageRegistry|Published to the repository at %{timestamp}"
msgstr ""
+msgid "PackageRegistry|PyPi"
+msgstr ""
+
msgid "PackageRegistry|Registry Setup"
msgstr ""
@@ -14551,6 +14868,9 @@ msgstr ""
msgid "PackageRegistry|npm"
msgstr ""
+msgid "PackageRegistry|published by %{author}"
+msgstr ""
+
msgid "PackageRegistry|yarn"
msgstr ""
@@ -14566,6 +14886,9 @@ msgstr ""
msgid "PackageType|NuGet"
msgstr ""
+msgid "PackageType|PyPi"
+msgstr ""
+
msgid "Packages"
msgstr ""
@@ -14659,6 +14982,9 @@ msgstr ""
msgid "Past due"
msgstr ""
+msgid "Paste a machine public key here. Read more about how to generate it"
+msgstr ""
+
msgid "Paste a machine public key here. Read more about how to generate it %{link_start}here%{link_end}"
msgstr ""
@@ -14674,9 +15000,6 @@ msgstr ""
msgid "Path"
msgstr ""
-msgid "Path, transfer, remove"
-msgstr ""
-
msgid "Path:"
msgstr ""
@@ -14707,7 +15030,7 @@ msgstr ""
msgid "Percentage"
msgstr ""
-msgid "Perform advanced options such as changing path, transferring, or removing the group."
+msgid "Perform advanced options such as changing path, transferring, exporting, or removing the group."
msgstr ""
msgid "Perform common operations on GitLab project"
@@ -14788,9 +15111,6 @@ msgstr ""
msgid "Pipeline minutes quota"
msgstr ""
-msgid "Pipeline quota"
-msgstr ""
-
msgid "Pipeline subscriptions"
msgstr ""
@@ -14800,6 +15120,9 @@ msgstr ""
msgid "Pipeline: %{status}"
msgstr ""
+msgid "PipelineCharts|CI / CD Analytics"
+msgstr ""
+
msgid "PipelineCharts|Failed:"
msgstr ""
@@ -14878,15 +15201,6 @@ msgstr ""
msgid "Pipelines settings for '%{project_name}' were successfully updated."
msgstr ""
-msgid "Pipelines| to purchase more minutes."
-msgstr ""
-
-msgid "Pipelines|%{namespace_name} has exceeded its pipeline minutes quota."
-msgstr ""
-
-msgid "Pipelines|%{namespace_name} has less than %{notification_level}%% of CI minutes available."
-msgstr ""
-
msgid "Pipelines|API"
msgstr ""
@@ -14908,10 +15222,13 @@ msgstr ""
msgid "Pipelines|Get started with Pipelines"
msgstr ""
-msgid "Pipelines|Loading Pipelines"
+msgid "Pipelines|Group %{namespace_name} has %{percentage}%% or less Shared Runner Pipeline minutes remaining. Once it runs out, no new jobs or pipelines in its projects will run."
msgstr ""
-msgid "Pipelines|Pipelines will not run anymore on shared Runners."
+msgid "Pipelines|Group %{namespace_name} has exceeded its pipeline minutes quota. Unless you buy additional pipeline minutes, no new jobs or pipelines in its projects will run."
+msgstr ""
+
+msgid "Pipelines|Loading Pipelines"
msgstr ""
msgid "Pipelines|Project cache successfully reset."
@@ -15142,6 +15459,9 @@ msgstr ""
msgid "Please select"
msgstr ""
+msgid "Please select a Jira project"
+msgstr ""
+
msgid "Please select a country"
msgstr ""
@@ -15187,6 +15507,9 @@ msgstr ""
msgid "Please wait while we import the repository for you. Refresh at will."
msgstr ""
+msgid "Plugins directory is deprecated and will be removed in 14.0. Please move this file into /file_hooks directory."
+msgstr ""
+
msgid "Pod does not exist"
msgstr ""
@@ -15304,7 +15627,7 @@ msgstr ""
msgid "Prevent users from changing their profile name"
msgstr ""
-msgid "Prevent users from modifing merge request approvers list"
+msgid "Prevent users from modifying merge request approvers list"
msgstr ""
msgid "Prevent users from performing write operations on GitLab while performing maintenance."
@@ -15325,10 +15648,10 @@ msgstr ""
msgid "Previous Artifacts"
msgstr ""
-msgid "Previous file in diff (MRs only)"
+msgid "Previous file in diff"
msgstr ""
-msgid "Previous unresolved discussion (MRs only)"
+msgid "Previous unresolved discussion"
msgstr ""
msgid "Primary"
@@ -15361,6 +15684,9 @@ msgstr ""
msgid "Private profile"
msgstr ""
+msgid "Private projects Minutes cost factor"
+msgstr ""
+
msgid "Private projects can be created in your personal namespace with:"
msgstr ""
@@ -16189,6 +16515,9 @@ msgstr ""
msgid "ProjectSettings|With GitLab Pages you can host your static websites on GitLab"
msgstr ""
+msgid "ProjectSettings|With Metrics Dashboard you can visualize this project performance metrics"
+msgstr ""
+
msgid "ProjectTemplates|.NET Core"
msgstr ""
@@ -16252,6 +16581,9 @@ msgstr ""
msgid "ProjectTemplates|Spring"
msgstr ""
+msgid "ProjectTemplates|Static Site Editor/Middleman"
+msgstr ""
+
msgid "ProjectTemplates|iOS (Swift)"
msgstr ""
@@ -16471,13 +16803,13 @@ msgstr ""
msgid "PrometheusService|Prometheus is being automatically managed on your clusters"
msgstr ""
-msgid "PrometheusService|These metrics will only be monitored after your first deployment to an environment"
+msgid "PrometheusService|Select the Active checkbox to override the Auto Configuration with custom settings. If unchecked, Auto Configuration settings are used."
msgstr ""
-msgid "PrometheusService|Time-series monitoring service"
+msgid "PrometheusService|These metrics will only be monitored after your first deployment to an environment"
msgstr ""
-msgid "PrometheusService|To enable manual configuration, uninstall Prometheus from your clusters"
+msgid "PrometheusService|Time-series monitoring service"
msgstr ""
msgid "PrometheusService|To enable the installation of Prometheus on your clusters, deactivate the manual configuration below"
@@ -16663,6 +16995,9 @@ msgstr ""
msgid "Protip:"
msgstr ""
+msgid "Protocol"
+msgstr ""
+
msgid "Provider"
msgstr ""
@@ -16687,6 +17022,9 @@ msgstr ""
msgid "Public pipelines"
msgstr ""
+msgid "Public projects Minutes cost factor"
+msgstr ""
+
msgid "Pull"
msgstr ""
@@ -16889,6 +17227,9 @@ msgstr[1] ""
msgstr[2] ""
msgstr[3] ""
+msgid "Regenerate export"
+msgstr ""
+
msgid "Regenerate instance ID"
msgstr ""
@@ -17268,9 +17609,31 @@ msgstr ""
msgid "Reports|%{combinedString} and %{resolvedString}"
msgstr ""
+msgid "Reports|Accessibility scanning detected %d issue for the source branch only"
+msgid_plural "Reports|Accessibility scanning detected %d issues for the source branch only"
+msgstr[0] ""
+msgstr[1] ""
+msgstr[2] ""
+msgstr[3] ""
+
+msgid "Reports|Accessibility scanning detected no issues for the source branch only"
+msgstr ""
+
+msgid "Reports|Accessibility scanning failed loading results"
+msgstr ""
+
+msgid "Reports|Accessibility scanning results are being parsed"
+msgstr ""
+
msgid "Reports|Actions"
msgstr ""
+msgid "Reports|An error occured while loading report"
+msgstr ""
+
+msgid "Reports|An error occurred while loading %{name} results"
+msgstr ""
+
msgid "Reports|Class"
msgstr ""
@@ -17376,7 +17739,7 @@ msgstr ""
msgid "Requested %{time_ago}"
msgstr ""
-msgid "Requested design version does not exist"
+msgid "Requested design version does not exist."
msgstr ""
msgid "Requested states are invalid"
@@ -17564,6 +17927,9 @@ msgstr ""
msgid "Resync"
msgstr ""
+msgid "Resync all"
+msgstr ""
+
msgid "Resync all %{replicableType}"
msgstr ""
@@ -17634,6 +18000,9 @@ msgstr ""
msgid "Revoked personal access token %{personal_access_token_name}!"
msgstr ""
+msgid "Revoked project access token %{project_access_token_name}!"
+msgstr ""
+
msgid "RightSidebar|adding a"
msgstr ""
@@ -17682,6 +18051,9 @@ msgstr ""
msgid "Runner token"
msgstr ""
+msgid "Runner tokens"
+msgstr ""
+
msgid "Runner was not updated."
msgstr ""
@@ -17736,6 +18108,9 @@ msgstr ""
msgid "SAML SSO for %{group_name}"
msgstr ""
+msgid "SAML discovery tokens"
+msgstr ""
+
msgid "SAML for %{group_name}"
msgstr ""
@@ -17820,12 +18195,18 @@ msgstr ""
msgid "Scheduled"
msgstr ""
+msgid "Scheduled to merge this merge request (%{strategy})."
+msgstr ""
+
msgid "Scheduled to merge this merge request when the pipeline succeeds."
msgstr ""
msgid "Schedules"
msgstr ""
+msgid "Schedules to merge this merge request (%{strategy})."
+msgstr ""
+
msgid "Scheduling"
msgstr ""
@@ -17883,6 +18264,9 @@ msgstr ""
msgid "Search branches and tags"
msgstr ""
+msgid "Search by author"
+msgstr ""
+
msgid "Search files"
msgstr ""
@@ -18044,13 +18428,6 @@ msgstr[1] ""
msgstr[2] ""
msgstr[3] ""
-msgid "SearchResults|snippet result"
-msgid_plural "SearchResults|snippet results"
-msgstr[0] ""
-msgstr[1] ""
-msgstr[2] ""
-msgstr[3] ""
-
msgid "SearchResults|user"
msgid_plural "SearchResults|users"
msgstr[0] ""
@@ -18065,6 +18442,9 @@ msgstr[1] ""
msgstr[2] ""
msgstr[3] ""
+msgid "Searching by both author and message is currently not supported."
+msgstr ""
+
msgid "Seat Link"
msgstr ""
@@ -18092,184 +18472,232 @@ msgstr ""
msgid "Security Dashboard"
msgstr ""
-msgid "Security Dashboard|Error fetching the vulnerability counts. Please check your network connection and try again."
+msgid "Security configuration help link"
msgstr ""
-msgid "Security Dashboard|Error fetching the vulnerability list. Please check your network connection and try again."
+msgid "Security dashboard"
msgstr ""
-msgid "Security Dashboard|Issue Created"
+msgid "Security report is out of date. Please update your branch with the latest changes from the target branch (%{targetBranchName})"
msgstr ""
-msgid "Security Reports|Comment added to '%{vulnerabilityName}'"
+msgid "Security report is out of date. Run %{newPipelineLinkStart}a new pipeline%{newPipelineLinkEnd} for the target branch (%{targetBranchName})"
msgstr ""
-msgid "Security Reports|Comment deleted on '%{vulnerabilityName}'"
+msgid "SecurityConfiguration|Configured"
msgstr ""
-msgid "Security Reports|Comment edited on '%{vulnerabilityName}'"
+msgid "SecurityConfiguration|Feature"
msgstr ""
-msgid "Security Reports|Create issue"
+msgid "SecurityConfiguration|Feature documentation for %{featureName}"
msgstr ""
-msgid "Security Reports|Dismiss vulnerability"
+msgid "SecurityConfiguration|Not yet configured"
msgstr ""
-msgid "Security Reports|Dismissed '%{vulnerabilityName}'"
+msgid "SecurityConfiguration|Secure features"
msgstr ""
-msgid "Security Reports|Dismissed '%{vulnerabilityName}'. Turn off the hide dismissed toggle to view."
+msgid "SecurityConfiguration|Status"
msgstr ""
-msgid "Security Reports|Either you don't have permission to view this dashboard or the dashboard has not been setup. Please check your permission settings with your administrator or check your dashboard configurations to proceed."
+msgid "SecurityReports|%{firstProject} and %{secondProject}"
msgstr ""
-msgid "Security Reports|Learn more about setting up your dashboard"
+msgid "SecurityReports|%{firstProject}, %{secondProject}, and %{rest}"
msgstr ""
-msgid "Security Reports|More info"
+msgid "SecurityReports|Add a project to your dashboard"
msgstr ""
-msgid "Security Reports|Oops, something doesn't seem right."
+msgid "SecurityReports|Add or remove projects from your dashboard"
msgstr ""
-msgid "Security Reports|Security reports can only be accessed by authorized users."
+msgid "SecurityReports|Add projects"
msgstr ""
-msgid "Security Reports|There was an error adding the comment."
+msgid "SecurityReports|Comment added to '%{vulnerabilityName}'"
msgstr ""
-msgid "Security Reports|There was an error creating the issue."
+msgid "SecurityReports|Comment deleted on '%{vulnerabilityName}'"
msgstr ""
-msgid "Security Reports|There was an error creating the merge request."
+msgid "SecurityReports|Comment edited on '%{vulnerabilityName}'"
msgstr ""
-msgid "Security Reports|There was an error deleting the comment."
+msgid "SecurityReports|Create issue"
msgstr ""
-msgid "Security Reports|There was an error dismissing the vulnerabilities."
+msgid "SecurityReports|Dismiss Selected"
msgstr ""
-msgid "Security Reports|There was an error dismissing the vulnerability."
+msgid "SecurityReports|Dismiss vulnerability"
msgstr ""
-msgid "Security Reports|There was an error reverting the dismissal."
+msgid "SecurityReports|Dismissed '%{vulnerabilityName}'"
msgstr ""
-msgid "Security Reports|There was an error reverting this dismissal."
+msgid "SecurityReports|Dismissed '%{vulnerabilityName}'. Turn off the hide dismissed toggle to view."
msgstr ""
-msgid "Security Reports|Undo dismiss"
+msgid "SecurityReports|Each vulnerability now has a unique page that can be directly linked to, shared, referenced, and tracked as the single source of truth. Vulnerability occurrences also persist across scanner runs, which improves tracking and visibility and reduces duplicates between scans."
msgstr ""
-msgid "Security Reports|You do not have sufficient permissions to access this report"
+msgid "SecurityReports|Edit dashboard"
msgstr ""
-msgid "Security Reports|You must sign in as an authorized user to see this report"
+msgid "SecurityReports|Either you don't have permission to view this dashboard or the dashboard has not been setup. Please check your permission settings with your administrator or check your dashboard configurations to proceed."
msgstr ""
-msgid "Security configuration help link"
+msgid "SecurityReports|Error fetching the vulnerability counts. Please check your network connection and try again."
msgstr ""
-msgid "Security dashboard"
+msgid "SecurityReports|Error fetching the vulnerability list. Please check your network connection and try again."
msgstr ""
-msgid "Security report is out of date. Please update your branch with the latest changes from the target branch (%{targetBranchName})"
+msgid "SecurityReports|False positive"
msgstr ""
-msgid "Security report is out of date. Run %{newPipelineLinkStart}a new pipeline%{newPipelineLinkEnd} for the target branch (%{targetBranchName})"
+msgid "SecurityReports|Hide dismissed"
msgstr ""
-msgid "SecurityConfiguration|Configured"
+msgid "SecurityReports|Introducing standalone vulnerabilities"
msgstr ""
-msgid "SecurityConfiguration|Feature"
+msgid "SecurityReports|Issue Created"
msgstr ""
-msgid "SecurityConfiguration|Feature documentation for %{featureName}"
+msgid "SecurityReports|Learn More"
msgstr ""
-msgid "SecurityConfiguration|Not yet configured"
+msgid "SecurityReports|Learn more about setting up your dashboard"
msgstr ""
-msgid "SecurityConfiguration|Secure features"
+msgid "SecurityReports|Load more vulnerabilities"
msgstr ""
-msgid "SecurityConfiguration|Status"
+msgid "SecurityReports|Monitor vulnerabilities in your code"
+msgstr ""
+
+msgid "SecurityReports|More info"
+msgstr ""
+
+msgid "SecurityReports|More information"
+msgstr ""
+
+msgid "SecurityReports|No vulnerabilities found for dashboard"
+msgstr ""
+
+msgid "SecurityReports|No vulnerabilities found for this group"
+msgstr ""
+
+msgid "SecurityReports|No vulnerabilities found for this pipeline"
+msgstr ""
+
+msgid "SecurityReports|No vulnerabilities found for this project"
+msgstr ""
+
+msgid "SecurityReports|Oops, something doesn't seem right."
+msgstr ""
+
+msgid "SecurityReports|Pipeline %{pipelineLink} triggered %{timeago} by %{user}"
msgstr ""
-msgid "SecurityDashboard|%{firstProject} and %{secondProject}"
+msgid "SecurityReports|Project"
msgstr ""
-msgid "SecurityDashboard|%{firstProject}, %{secondProject}, and %{rest}"
+msgid "SecurityReports|Projects added"
msgstr ""
-msgid "SecurityDashboard|Add a project to your dashboard"
+msgid "SecurityReports|Remove project from dashboard"
msgstr ""
-msgid "SecurityDashboard|Add or remove projects from your dashboard"
+msgid "SecurityReports|Report type"
msgstr ""
-msgid "SecurityDashboard|Add projects"
+msgid "SecurityReports|Return to dashboard"
msgstr ""
-msgid "SecurityDashboard|Edit dashboard"
+msgid "SecurityReports|Security Dashboard"
msgstr ""
-msgid "SecurityDashboard|Hide dismissed"
+msgid "SecurityReports|Security reports can only be accessed by authorized users."
msgstr ""
-msgid "SecurityDashboard|Monitor vulnerabilities in your code"
+msgid "SecurityReports|Select a project to add by using the project search field above."
msgstr ""
-msgid "SecurityDashboard|More information"
+msgid "SecurityReports|Select a reason"
msgstr ""
-msgid "SecurityDashboard|No vulnerabilities found for dashboard"
+msgid "SecurityReports|Severity"
msgstr ""
-msgid "SecurityDashboard|Pipeline %{pipelineLink} triggered %{timeago} by %{user}"
+msgid "SecurityReports|Status"
msgstr ""
-msgid "SecurityDashboard|Project"
+msgid "SecurityReports|The security dashboard displays the latest security findings for projects you wish to monitor. Select \"Edit dashboard\" to add and remove projects."
msgstr ""
-msgid "SecurityDashboard|Projects added"
+msgid "SecurityReports|The security dashboard displays the latest security report. Use it to find and fix vulnerabilities."
msgstr ""
-msgid "SecurityDashboard|Remove project from dashboard"
+msgid "SecurityReports|There was an error adding the comment."
msgstr ""
-msgid "SecurityDashboard|Report type"
+msgid "SecurityReports|There was an error creating the issue."
msgstr ""
-msgid "SecurityDashboard|Return to dashboard"
+msgid "SecurityReports|There was an error creating the merge request."
msgstr ""
-msgid "SecurityDashboard|Security Dashboard"
+msgid "SecurityReports|There was an error deleting the comment."
msgstr ""
-msgid "SecurityDashboard|Select a project to add by using the project search field above."
+msgid "SecurityReports|There was an error dismissing the vulnerabilities."
msgstr ""
-msgid "SecurityDashboard|Severity"
+msgid "SecurityReports|There was an error dismissing the vulnerability."
msgstr ""
-msgid "SecurityDashboard|Status"
+msgid "SecurityReports|There was an error reverting the dismissal."
msgstr ""
-msgid "SecurityDashboard|The security dashboard displays the latest security findings for projects you wish to monitor. Select \"Edit dashboard\" to add and remove projects."
+msgid "SecurityReports|There was an error reverting this dismissal."
msgstr ""
-msgid "SecurityDashboard|The security dashboard displays the latest security report. Use it to find and fix vulnerabilities."
+msgid "SecurityReports|There was an error while generating the report."
msgstr ""
-msgid "SecurityDashboard|There was an error while generating the report."
+msgid "SecurityReports|Unable to add %{invalidProjects}"
msgstr ""
-msgid "SecurityDashboard|Unable to add %{invalidProjects}"
+msgid "SecurityReports|Undo dismiss"
+msgstr ""
+
+msgid "SecurityReports|While it's rare to have no vulnerabilities for your group, it can happen. In any event, we ask that you double check your settings to make sure you've set up your dashboard correctly."
+msgstr ""
+
+msgid "SecurityReports|While it's rare to have no vulnerabilities for your pipeline, it can happen. In any event, we ask that you double check your settings to make sure all security scanning jobs have passed successfully."
+msgstr ""
+
+msgid "SecurityReports|While it's rare to have no vulnerabilities for your project, it can happen. In any event, we ask that you double check your settings to make sure you've set up your dashboard correctly."
+msgstr ""
+
+msgid "SecurityReports|While it's rare to have no vulnerabilities, it can happen. In any event, we ask that you please double check your settings to make sure you've set up your dashboard correctly."
+msgstr ""
+
+msgid "SecurityReports|Won't fix / Accept risk"
+msgstr ""
+
+msgid "SecurityReports|You do not have sufficient permissions to access this report"
+msgstr ""
+
+msgid "SecurityReports|You must sign in as an authorized user to see this report"
+msgstr ""
+
+msgid "SecurityReports|[No reason]"
msgstr ""
msgid "See GitLab's %{password_policy_guidelines}"
@@ -18281,6 +18709,9 @@ msgstr ""
msgid "See the affected projects in the GitLab admin panel"
msgstr ""
+msgid "See what's new at GitLab"
+msgstr ""
+
msgid "Select"
msgstr ""
@@ -18686,9 +19117,6 @@ msgstr ""
msgid "Set up Jira Integration"
msgstr ""
-msgid "Set up Jira Integration illustration"
-msgstr ""
-
msgid "Set up a %{type} Runner automatically"
msgstr ""
@@ -18773,6 +19201,9 @@ msgstr ""
msgid "Severity: %{severity}"
msgstr ""
+msgid "Shards (%{shards})"
+msgstr ""
+
msgid "Shards to synchronize"
msgstr ""
@@ -19089,6 +19520,9 @@ msgstr ""
msgid "Snowplow"
msgstr ""
+msgid "Some child epics may be hidden due to applied filters"
+msgstr ""
+
msgid "Some email servers do not support overriding the email sender name. Enable this option to include the name of the author of the issue, merge request or comment in the email body instead."
msgstr ""
@@ -19230,6 +19664,9 @@ msgstr ""
msgid "Something went wrong while updating your list settings"
msgstr ""
+msgid "Something went wrong with your automatic subscription renewal"
+msgstr ""
+
msgid "Something went wrong, unable to add %{project} to dashboard"
msgstr ""
@@ -19515,6 +19952,9 @@ msgstr ""
msgid "Stage removed"
msgstr ""
+msgid "Standard"
+msgstr ""
+
msgid "Star a label to make it a priority label. Order the prioritized labels to change their relative priority, by dragging."
msgstr ""
@@ -19638,6 +20078,9 @@ msgstr ""
msgid "Static Application Security Testing (SAST)"
msgstr ""
+msgid "StaticSiteEditor|An error occurred while submitting your changes."
+msgstr ""
+
msgid "StaticSiteEditor|Branch could not be created."
msgstr ""
@@ -19647,18 +20090,30 @@ msgstr ""
msgid "StaticSiteEditor|Could not create merge request."
msgstr ""
+msgid "StaticSiteEditor|Incompatible file content"
+msgstr ""
+
msgid "StaticSiteEditor|Return to site"
msgstr ""
+msgid "StaticSiteEditor|Static site editor"
+msgstr ""
+
msgid "StaticSiteEditor|Success!"
msgstr ""
msgid "StaticSiteEditor|Summary of changes"
msgstr ""
+msgid "StaticSiteEditor|The Static Site Editor is currently configured to only edit Markdown content on pages generated from Middleman. Visit the documentation to learn more about configuring your site to use the Static Site Editor."
+msgstr ""
+
msgid "StaticSiteEditor|Update %{sourcePath} file"
msgstr ""
+msgid "StaticSiteEditor|View documentation"
+msgstr ""
+
msgid "StaticSiteEditor|View merge request"
msgstr ""
@@ -19716,6 +20171,12 @@ msgstr ""
msgid "StatusPage|Status page"
msgstr ""
+msgid "StatusPage|Status page URL"
+msgstr ""
+
+msgid "StatusPage|Status page frontend documentation"
+msgstr ""
+
msgid "StatusPage|To publish incidents to an external status page, GitLab will store a JSON file in your Amazon S3 account in a location accessible to your external status page service. Make sure to also set up %{docsLink}"
msgstr ""
@@ -20064,6 +20525,12 @@ msgstr ""
msgid "Synced"
msgstr ""
+msgid "Synchronization disabled"
+msgstr ""
+
+msgid "Synchronization of container repositories is disabled."
+msgstr ""
+
msgid "System"
msgstr ""
@@ -20217,6 +20684,9 @@ msgstr ""
msgid "Telephone number"
msgstr ""
+msgid "Telephone number (Optional)"
+msgstr ""
+
msgid "Template"
msgstr ""
@@ -20529,6 +20999,9 @@ msgstr ""
msgid "The license was successfully uploaded and is now active. You can see the details below."
msgstr ""
+msgid "The license was successfully uploaded and will be active from %{starts_at}. You can see the details below."
+msgstr ""
+
msgid "The maximum file size allowed is %{size}."
msgstr ""
@@ -20850,6 +21323,9 @@ msgstr ""
msgid "There was an error getting the epic participants."
msgstr ""
+msgid "There was an error importing the Jira project."
+msgstr ""
+
msgid "There was an error loading users activity calendar."
msgstr ""
@@ -20865,7 +21341,7 @@ msgstr ""
msgid "There was an error resetting user pipeline minutes."
msgstr ""
-msgid "There was an error saving this Geo Node"
+msgid "There was an error saving this Geo Node."
msgstr ""
msgid "There was an error saving your changes."
@@ -20928,6 +21404,9 @@ msgstr ""
msgid "They can be managed using the %{link}."
msgstr ""
+msgid "Third Party Advisory Link"
+msgstr ""
+
msgid "Third party offers"
msgstr ""
@@ -20964,6 +21443,9 @@ msgstr ""
msgid "This application will be able to:"
msgstr ""
+msgid "This attachment has been truncated to avoid exceeding the maximum allowed attachment size of 15MB. %{written_count} of %{issues_count} issues have been included. Consider re-exporting with a narrower selection of issues."
+msgstr ""
+
msgid "This block is self-referential"
msgstr ""
@@ -21105,6 +21587,12 @@ msgstr ""
msgid "This issue is confidential"
msgstr ""
+msgid "This issue is currently blocked by the following issues: %{issues}."
+msgstr ""
+
+msgid "This issue is in a child epic of the filtered epic"
+msgstr ""
+
msgid "This issue is locked."
msgstr ""
@@ -21303,7 +21791,10 @@ msgstr ""
msgid "This user will be the author of all events in the activity feed that are the result of an update, like new branches being created or new commits being pushed to existing branches. Upon creation or when reassigning you can only assign yourself to be the mirror user."
msgstr ""
-msgid "This variable can not be masked"
+msgid "This variable can not be masked."
+msgstr ""
+
+msgid "This variable does not match the expected pattern."
msgstr ""
msgid "This will help us personalize your onboarding experience."
@@ -21333,10 +21824,10 @@ msgstr ""
msgid "ThreatMonitoring|Application firewall not detected"
msgstr ""
-msgid "ThreatMonitoring|Container Network Policy"
+msgid "ThreatMonitoring|Container Network Policies are not installed or have been disabled. To view this data, ensure your Network Policies are installed and enabled for your cluster."
msgstr ""
-msgid "ThreatMonitoring|Container NetworkPolicies are not installed or has been disabled. To view this data, ensure you NetworkPolicies are installed and enabled for your cluster."
+msgid "ThreatMonitoring|Container Network Policy"
msgstr ""
msgid "ThreatMonitoring|Container NetworkPolicies not detected"
@@ -21369,7 +21860,7 @@ msgstr ""
msgid "ThreatMonitoring|Something went wrong, unable to fetch statistics"
msgstr ""
-msgid "ThreatMonitoring|The firewall is not installed or has been disabled. To view this data, ensure you firewall is installed and enabled for your cluster."
+msgid "ThreatMonitoring|The firewall is not installed or has been disabled. To view this data, ensure the web application firewall is installed and enabled for your cluster."
msgstr ""
msgid "ThreatMonitoring|The graph below is an overview of traffic coming to your application as tracked by the Web Application Firewall (WAF). View the docs for instructions on how to access the WAF logs to see what type of malicious traffic is trying to access your app. The docs link is also accessible by clicking the \"?\" icon next to the title below."
@@ -21438,6 +21929,9 @@ msgstr ""
msgid "Time in seconds GitLab will wait for a response from the external service. When the service does not respond in time, access will be denied."
msgstr ""
+msgid "Time of import: %{importTime}"
+msgstr ""
+
msgid "Time remaining"
msgstr ""
@@ -21626,7 +22120,7 @@ msgstr ""
msgid "Title:"
msgstr ""
-msgid "Titles and Filenames"
+msgid "Titles and Descriptions"
msgstr ""
msgid "To"
@@ -21833,7 +22327,7 @@ msgstr ""
msgid "Too many projects enabled. You will need to manage them via the console or the API."
msgstr ""
-msgid "Topics"
+msgid "Topics (optional)"
msgstr ""
msgid "Total"
@@ -21995,6 +22489,9 @@ msgstr ""
msgid "Try all GitLab has to offer for 30 days."
msgstr ""
+msgid "Try changing or removing filters."
+msgstr ""
+
msgid "Try to fork again"
msgstr ""
@@ -22088,6 +22585,9 @@ msgstr ""
msgid "Unable to connect to server: %{error}"
msgstr ""
+msgid "Unable to connect to the Jira instance. Please check your Jira integration configuration."
+msgstr ""
+
msgid "Unable to convert Kubernetes logs encoding to UTF-8"
msgstr ""
@@ -23035,6 +23535,9 @@ msgstr ""
msgid "View issue"
msgstr ""
+msgid "View issues"
+msgstr ""
+
msgid "View it on GitLab"
msgstr ""
@@ -23212,6 +23715,15 @@ msgstr ""
msgid "VulnerabilityManagement|Resolved %{timeago} by %{user}"
msgstr ""
+msgid "VulnerabilityManagement|Something went wrong while trying to delete the comment. Please try again later."
+msgstr ""
+
+msgid "VulnerabilityManagement|Something went wrong while trying to retrieve the vulnerability history. Please try again later."
+msgstr ""
+
+msgid "VulnerabilityManagement|Something went wrong while trying to save the comment. Please try again later."
+msgstr ""
+
msgid "VulnerabilityManagement|Something went wrong, could not create an issue."
msgstr ""
@@ -23242,6 +23754,9 @@ msgstr ""
msgid "VulnerabilityStatusTypes|Resolved"
msgstr ""
+msgid "Vulnerability|%{scannerName} (version %{scannerVersion})"
+msgstr ""
+
msgid "Vulnerability|Class"
msgstr ""
@@ -23275,7 +23790,10 @@ msgstr ""
msgid "Vulnerability|Project"
msgstr ""
-msgid "Vulnerability|Report Type"
+msgid "Vulnerability|Scanner Provider"
+msgstr ""
+
+msgid "Vulnerability|Scanner Type"
msgstr ""
msgid "Vulnerability|Severity"
@@ -23329,9 +23847,15 @@ msgstr ""
msgid "We sent you an email with reset password instructions"
msgstr ""
+msgid "We tried to automatically renew your %{strong}%{plan_name}%{strong_close} subscription for %{strong}%{namespace_name}%{strong_close} on %{expires_on} but something went wrong so your subscription was downgraded to the free plan. Don't worry, your data is safe. We suggest you check your payment method and get in touch with our support team (%{support_link}). They'll gladly help with your subscription renewal."
+msgstr ""
+
msgid "We want to be sure it is you, please confirm you are not a robot."
msgstr ""
+msgid "We will automatically renew your %{strong}%{plan_name}%{strong_close} subscription for %{strong}%{namespace_name}%{strong_close} on %{strong}%{expires_on}%{strong_close}. There's nothing that you need to do, we'll let you know when the renewal is complete. Need more seats, a higher plan or just want to review your payment method?"
+msgstr ""
+
msgid "We've found no vulnerabilities"
msgstr ""
@@ -23438,15 +23962,6 @@ msgstr ""
msgid "When:"
msgstr ""
-msgid "While it's rare to have no vulnerabilities for your group, it can happen. In any event, we ask that you double check your settings to make sure you've set up your dashboard correctly."
-msgstr ""
-
-msgid "While it's rare to have no vulnerabilities for your pipeline, it can happen. In any event, we ask that you double check your settings to make sure all security scanning jobs have passed successfully."
-msgstr ""
-
-msgid "While it's rare to have no vulnerabilities for your project, it can happen. In any event, we ask that you double check your settings to make sure you've set up your dashboard correctly."
-msgstr ""
-
msgid "While it's rare to have no vulnerabilities, it can happen. In any event, we ask that you please double check your settings to make sure you've set up your dashboard correctly."
msgstr ""
@@ -23477,9 +23992,6 @@ msgstr ""
msgid "Wiki"
msgstr ""
-msgid "Wiki pages"
-msgstr ""
-
msgid "Wiki was successfully updated."
msgstr ""
@@ -23663,6 +24175,9 @@ msgstr ""
msgid "Yes, add it"
msgstr ""
+msgid "Yes, close issue"
+msgstr ""
+
msgid "Yes, let me map Google Code users to full names or GitLab users."
msgstr ""
@@ -23858,6 +24373,9 @@ msgstr ""
msgid "You could not create a new trigger."
msgstr ""
+msgid "You didn't renew your %{strong}%{plan_name}%{strong_close} subscription for %{strong}%{namespace_name}%{strong_close} so it was downgraded to the free plan."
+msgstr ""
+
msgid "You didn't renew your %{strong}%{plan_name}%{strong_close} subscription so it was downgraded to the GitLab Core Plan."
msgstr ""
@@ -23924,6 +24442,9 @@ msgstr ""
msgid "You have declined the invitation to join %{label}."
msgstr ""
+msgid "You have imported from this project %{numberOfPreviousImportsForProject} times before. Each new import will create duplicate issues."
+msgstr ""
+
msgid "You have no permissions"
msgstr ""
@@ -24005,6 +24526,9 @@ msgstr ""
msgid "You need to upload a Google Takeout archive."
msgstr ""
+msgid "You reached %{usage_in_percent} of %{namespace_name}'s capacity (%{used_storage} of %{storage_limit})"
+msgstr ""
+
msgid "You tried to fork %{link_to_the_project} but it failed for the following reason:"
msgstr ""
@@ -24089,9 +24613,21 @@ msgstr ""
msgid "YouTube"
msgstr ""
+msgid "Your %{strong}%{plan_name}%{strong_close} subscription for %{strong}%{namespace_name}%{strong_close} will expire on %{strong}%{expires_on}%{strong_close}. After that, you will not to be able to create issues or merge requests as well as many other features."
+msgstr ""
+
msgid "Your %{strong}%{plan_name}%{strong_close} subscription will expire on %{strong}%{expires_on}%{strong_close}. After that, you will not to be able to create issues or merge requests as well as many other features."
msgstr ""
+msgid "Your CSV export has started. It will be emailed to %{email} when complete."
+msgstr ""
+
+msgid "Your CSV export of %{issues_count} from project %{project_link} has been added to this email as an attachment."
+msgstr ""
+
+msgid "Your CSV export of %{written_count} from project %{project_name} (%{project_url}) has been added to this email as an attachment."
+msgstr ""
+
msgid "Your Commit Email will be used for web based operations, such as edits and merges."
msgstr ""
@@ -24188,6 +24724,9 @@ msgstr ""
msgid "Your comment could not be updated! Please check your network connection and try again."
msgstr ""
+msgid "Your comment will be discarded."
+msgstr ""
+
msgid "Your custom stage '%{title}' was created"
msgstr ""
@@ -24227,6 +24766,9 @@ msgstr ""
msgid "Your new personal access token has been created."
msgstr ""
+msgid "Your new project access token has been created."
+msgstr ""
+
msgid "Your password isn't required to view this page. If a password or any other personal details are requested, please contact your administrator to report abuse."
msgstr ""
@@ -24245,12 +24787,18 @@ msgstr ""
msgid "Your request for access has been queued for review."
msgstr ""
+msgid "Your search didn't match any commits."
+msgstr ""
+
msgid "Your subscription expired!"
msgstr ""
msgid "Your subscription has been downgraded"
msgstr ""
+msgid "Your subscription will automatically renew in %{remaining_days}"
+msgstr ""
+
msgid "Your subscription will expire in %{remaining_days}"
msgstr ""
@@ -24291,7 +24839,7 @@ msgstr ""
msgid "allowed to fail"
msgstr ""
-msgid "already being used for another group or project milestone."
+msgid "already being used for another group or project %{timebox_name}."
msgstr ""
msgid "already has a \"created\" issue link"
@@ -24369,6 +24917,9 @@ msgstr ""
msgid "ciReport|%{linkStartTag}Learn more about SAST %{linkEndTag}"
msgstr ""
+msgid "ciReport|%{linkStartTag}Learn more about Secret Scanning %{linkEndTag}"
+msgstr ""
+
msgid "ciReport|%{linkStartTag}Learn more about codequality reports %{linkEndTag}"
msgstr ""
@@ -24542,6 +25093,12 @@ msgstr ""
msgid "ciReport|SAST"
msgstr ""
+msgid "ciReport|Secret scanning"
+msgstr ""
+
+msgid "ciReport|Secret scanning detects secrets and credentials vulnerabilities in your source code."
+msgstr ""
+
msgid "ciReport|Security scanning"
msgstr ""
@@ -24747,6 +25304,9 @@ msgstr ""
msgid "group"
msgstr ""
+msgid "groups"
+msgstr ""
+
msgid "has already been linked to another vulnerability"
msgstr ""
@@ -24914,9 +25474,6 @@ msgstr[3] ""
msgid "merged %{time_ago}"
msgstr ""
-msgid "milestone should belong either to a project or a group."
-msgstr ""
-
msgid "missing"
msgstr ""
@@ -25253,6 +25810,9 @@ msgstr ""
msgid "no contributions"
msgstr ""
+msgid "no expiration"
+msgstr ""
+
msgid "no one can merge"
msgstr ""
@@ -25309,6 +25869,9 @@ msgstr ""
msgid "pending removal"
msgstr ""
+msgid "per day"
+msgstr ""
+
msgid "pipeline"
msgstr ""
@@ -25341,6 +25904,12 @@ msgstr[3] ""
msgid "project avatar"
msgstr ""
+msgid "projects"
+msgstr ""
+
+msgid "push to your repository, create pipelines, create issues or add comments. To reduce storage capacity, delete unused repositories, artifacts, wikis, issues, and pipelines."
+msgstr ""
+
msgid "quick actions"
msgstr ""
@@ -25489,13 +26058,6 @@ msgstr ""
msgid "this document"
msgstr ""
-msgid "thread resolved"
-msgid_plural "threads resolved"
-msgstr[0] ""
-msgstr[1] ""
-msgstr[2] ""
-msgstr[3] ""
-
msgid "to help your contributors communicate effectively!"
msgstr ""
@@ -25550,6 +26112,9 @@ msgstr ""
msgid "view the blob"
msgstr ""
+msgid "vulnerability|Add a comment"
+msgstr ""
+
msgid "vulnerability|Add a comment or reason for dismissal"
msgstr ""
diff --git a/locale/cy_GB/gitlab.po b/locale/cy_GB/gitlab.po
index bb0b863f0d2..1b1664ca998 100644
--- a/locale/cy_GB/gitlab.po
+++ b/locale/cy_GB/gitlab.po
@@ -12,7 +12,7 @@ msgstr ""
"X-Crowdin-Project: gitlab-ee\n"
"X-Crowdin-Language: cy\n"
"X-Crowdin-File: /master/locale/gitlab.pot\n"
-"PO-Revision-Date: 2020-04-15 00:32\n"
+"PO-Revision-Date: 2020-05-05 21:32\n"
msgid " %{start} to %{end}"
msgstr ""
@@ -332,6 +332,15 @@ msgstr[3] ""
msgstr[4] ""
msgstr[5] ""
+msgid "%d unresolved thread"
+msgid_plural "%d unresolved threads"
+msgstr[0] ""
+msgstr[1] ""
+msgstr[2] ""
+msgstr[3] ""
+msgstr[4] ""
+msgstr[5] ""
+
msgid "%d vulnerability dismissed"
msgid_plural "%d vulnerabilities dismissed"
msgstr[0] ""
@@ -515,6 +524,9 @@ msgstr ""
msgid "%{mrText}, this issue will be closed automatically."
msgstr ""
+msgid "%{namespace_name} is now read-only. You cannot: %{base_message}"
+msgstr ""
+
msgid "%{name} contained %{resultsString}"
msgstr ""
@@ -527,6 +539,15 @@ msgstr ""
msgid "%{name}'s avatar"
msgstr ""
+msgid "%{no_of_days} day"
+msgid_plural "%{no_of_days} days"
+msgstr[0] ""
+msgstr[1] ""
+msgstr[2] ""
+msgstr[3] ""
+msgstr[4] ""
+msgstr[5] ""
+
msgid "%{number_commits_behind} commits behind %{default_branch}, %{number_commits_ahead} commits ahead"
msgstr ""
@@ -674,6 +695,9 @@ msgstr[5] ""
msgid "%{text} is available"
msgstr ""
+msgid "%{timebox_name} should belong either to a project or a group."
+msgstr ""
+
msgid "%{title} %{operator} %{threshold}"
msgstr ""
@@ -1025,12 +1049,18 @@ msgstr ""
msgid "<code>\"johnsmith@example.com\": \"johnsmith@example.com\"</code> will add \"By <a href=\"#\">johnsmith@example.com</a>\" to all issues and comments originally created by johnsmith@example.com. By default, the email address or username is masked to ensure the user's privacy. Use this option if you want to show the full email address."
msgstr ""
+msgid "<namespace / project>"
+msgstr ""
+
msgid "<no name set>"
msgstr ""
msgid "<no scopes selected>"
msgstr ""
+msgid "<project name>"
+msgstr ""
+
msgid "<strong>%{group_name}</strong> group members"
msgstr ""
@@ -1064,6 +1094,9 @@ msgstr ""
msgid "A basic page and serverless function that uses AWS Lambda, AWS API Gateway, and GitLab Pages"
msgstr ""
+msgid "A complete DevOps platform"
+msgstr ""
+
msgid "A default branch cannot be chosen for an empty project."
msgstr ""
@@ -1280,6 +1313,12 @@ msgstr ""
msgid "AccessTokens|reset it"
msgstr ""
+msgid "AccessibilityReport|Accessibility report artifact not found"
+msgstr ""
+
+msgid "AccessibilityReport|Failed to retrieve accessibility report"
+msgstr ""
+
msgid "AccessibilityReport|Learn More"
msgstr ""
@@ -1289,6 +1328,9 @@ msgstr ""
msgid "AccessibilityReport|New"
msgstr ""
+msgid "AccessibilityReport|The accessibility scanning found an error of the following type: %{code}"
+msgstr ""
+
msgid "Account"
msgstr ""
@@ -1535,6 +1577,9 @@ msgstr ""
msgid "Added at"
msgstr ""
+msgid "Added for this merge request"
+msgstr ""
+
msgid "Added in this version"
msgstr ""
@@ -1910,6 +1955,72 @@ msgstr[3] ""
msgstr[4] ""
msgstr[5] ""
+msgid "Alert Details"
+msgstr ""
+
+msgid "AlertManagement|Acknowledged"
+msgstr ""
+
+msgid "AlertManagement|Alert"
+msgstr ""
+
+msgid "AlertManagement|Authorize external service"
+msgstr ""
+
+msgid "AlertManagement|Display alerts from all your monitoring tools directly within GitLab. Streamline the investigation of your alerts and the escalation of alerts to incidents."
+msgstr ""
+
+msgid "AlertManagement|End time"
+msgstr ""
+
+msgid "AlertManagement|End time:"
+msgstr ""
+
+msgid "AlertManagement|Events"
+msgstr ""
+
+msgid "AlertManagement|Events:"
+msgstr ""
+
+msgid "AlertManagement|Full Alert Details"
+msgstr ""
+
+msgid "AlertManagement|More information"
+msgstr ""
+
+msgid "AlertManagement|No alerts available to display. If you think you're seeing this message in error, refresh the page."
+msgstr ""
+
+msgid "AlertManagement|No alerts to display."
+msgstr ""
+
+msgid "AlertManagement|Overview"
+msgstr ""
+
+msgid "AlertManagement|Resolved"
+msgstr ""
+
+msgid "AlertManagement|Severity"
+msgstr ""
+
+msgid "AlertManagement|Start time"
+msgstr ""
+
+msgid "AlertManagement|Start time:"
+msgstr ""
+
+msgid "AlertManagement|Status"
+msgstr ""
+
+msgid "AlertManagement|Surface alerts in GitLab"
+msgstr ""
+
+msgid "AlertManagement|There was an error displaying the alerts. Confirm your endpoint's configuration details to ensure alerts appear."
+msgstr ""
+
+msgid "AlertManagement|Triggered"
+msgstr ""
+
msgid "AlertService|%{linkStart}Learn more%{linkEnd} about configuring this endpoint to receive alerts."
msgstr ""
@@ -1979,6 +2090,9 @@ msgstr ""
msgid "All security scans are enabled because %{linkStart}Auto DevOps%{linkEnd} is enabled on this project"
msgstr ""
+msgid "All threads resolved"
+msgstr ""
+
msgid "All users"
msgstr ""
@@ -1997,6 +2111,9 @@ msgstr ""
msgid "Allow only the selected protocols to be used for Git access."
msgstr ""
+msgid "Allow owners to manage default branch protection per group"
+msgstr ""
+
msgid "Allow owners to manually add users outside of LDAP"
msgstr ""
@@ -2105,6 +2222,9 @@ msgstr ""
msgid "An error occurred fetching the dropdown data."
msgstr ""
+msgid "An error occurred fetching the project authors."
+msgstr ""
+
msgid "An error occurred previewing the blob"
msgstr ""
@@ -2186,6 +2306,9 @@ msgstr ""
msgid "An error occurred while fetching sidebar data"
msgstr ""
+msgid "An error occurred while fetching terraform reports."
+msgstr ""
+
msgid "An error occurred while fetching the Service Desk address."
msgstr ""
@@ -2423,6 +2546,9 @@ msgstr ""
msgid "Any"
msgstr ""
+msgid "Any Author"
+msgstr ""
+
msgid "Any Label"
msgstr ""
@@ -2654,10 +2780,10 @@ msgstr ""
msgid "Are you sure that you want to unarchive this project?"
msgstr ""
-msgid "Are you sure you want to cancel creating this comment?"
+msgid "Are you sure you want to cancel editing this comment?"
msgstr ""
-msgid "Are you sure you want to cancel editing this comment?"
+msgid "Are you sure you want to close this blocked issue?"
msgstr ""
msgid "Are you sure you want to delete %{name}?"
@@ -2687,6 +2813,9 @@ msgstr ""
msgid "Are you sure you want to deploy this environment?"
msgstr ""
+msgid "Are you sure you want to discard this comment?"
+msgstr ""
+
msgid "Are you sure you want to erase this build?"
msgstr ""
@@ -2867,6 +2996,9 @@ msgstr ""
msgid "At least one approval from a code owner is required to change files matching the respective CODEOWNER rules."
msgstr ""
+msgid "At least one logging option is required to be enabled"
+msgstr ""
+
msgid "At least one of group_id or project_id must be specified"
msgstr ""
@@ -2909,6 +3041,27 @@ msgstr ""
msgid "AuditEvents|Target"
msgstr ""
+msgid "AuditLogs|(removed)"
+msgstr ""
+
+msgid "AuditLogs|Action"
+msgstr ""
+
+msgid "AuditLogs|Author"
+msgstr ""
+
+msgid "AuditLogs|Date"
+msgstr ""
+
+msgid "AuditLogs|IP Address"
+msgstr ""
+
+msgid "AuditLogs|Object"
+msgstr ""
+
+msgid "AuditLogs|Target"
+msgstr ""
+
msgid "Aug"
msgstr ""
@@ -3191,9 +3344,6 @@ msgstr ""
msgid "BambooService|You must set up automatic revision labeling and a repository trigger in Bamboo."
msgstr ""
-msgid "Batch operations"
-msgstr ""
-
msgid "BatchComments|Delete all pending comments"
msgstr ""
@@ -3323,6 +3473,9 @@ msgstr ""
msgid "Boards"
msgstr ""
+msgid "Boards and Board Lists"
+msgstr ""
+
msgid "Boards|Collapse"
msgstr ""
@@ -3542,6 +3695,9 @@ msgstr ""
msgid "Buy GitLab Enterprise Edition"
msgstr ""
+msgid "Buy more Pipeline minutes"
+msgstr ""
+
msgid "By %{user_name}"
msgstr ""
@@ -3662,6 +3818,9 @@ msgstr ""
msgid "Can't scan the code?"
msgstr ""
+msgid "Can't update snippet: %{err}"
+msgstr ""
+
msgid "Canary"
msgstr ""
@@ -3830,6 +3989,9 @@ msgstr ""
msgid "Changing group path can have unintended side effects."
msgstr ""
+msgid "Charts"
+msgstr ""
+
msgid "Charts can't be displayed as the request for data has timed out. %{documentationLink}"
msgstr ""
@@ -3866,9 +4028,6 @@ msgstr ""
msgid "ChatMessage|and [%{count} more](%{pipeline_failed_jobs_url})"
msgstr ""
-msgid "ChatMessage|failed"
-msgstr ""
-
msgid "ChatMessage|has failed"
msgstr ""
@@ -3884,9 +4043,6 @@ msgstr ""
msgid "ChatMessage|in %{project_link}"
msgstr ""
-msgid "ChatMessage|passed"
-msgstr ""
-
msgid "Check again"
msgstr ""
@@ -4337,9 +4493,6 @@ msgstr ""
msgid "Click any <strong>project name</strong> in the project list below to navigate to the project milestone."
msgstr ""
-msgid "Click here"
-msgstr ""
-
msgid "Click the <strong>Download</strong> button and wait for downloading to complete."
msgstr ""
@@ -4727,6 +4880,12 @@ msgstr ""
msgid "ClusterIntegration|Fetching zones"
msgstr ""
+msgid "ClusterIntegration|Fluentd"
+msgstr ""
+
+msgid "ClusterIntegration|Fluentd is an open source data collector, which lets you unify the data collection and consumption for a better use and understanding of data. It requires at least one of the following logs to be successfully installed."
+msgstr ""
+
msgid "ClusterIntegration|GitLab Integration"
msgstr ""
@@ -5033,6 +5192,15 @@ msgstr ""
msgid "ClusterIntegration|Request to begin uninstalling failed"
msgstr ""
+msgid "ClusterIntegration|SIEM Hostname"
+msgstr ""
+
+msgid "ClusterIntegration|SIEM Port"
+msgstr ""
+
+msgid "ClusterIntegration|SIEM Protocol"
+msgstr ""
+
msgid "ClusterIntegration|Save changes"
msgstr ""
@@ -5129,6 +5297,12 @@ msgstr ""
msgid "ClusterIntegration|Select zone to choose machine type"
msgstr ""
+msgid "ClusterIntegration|Send Cilium Logs"
+msgstr ""
+
+msgid "ClusterIntegration|Send ModSecurity Logs"
+msgstr ""
+
msgid "ClusterIntegration|Service Token"
msgstr ""
@@ -5561,18 +5735,33 @@ msgstr ""
msgid "Compliance framework (optional)"
msgstr ""
+msgid "ComplianceFramework|GDPR"
+msgstr ""
+
msgid "ComplianceFramework|GDPR - General Data Protection Regulation"
msgstr ""
+msgid "ComplianceFramework|HIPAA"
+msgstr ""
+
msgid "ComplianceFramework|HIPAA - Health Insurance Portability and Accountability Act"
msgstr ""
+msgid "ComplianceFramework|PCI-DSS"
+msgstr ""
+
msgid "ComplianceFramework|PCI-DSS - Payment Card Industry-Data Security Standard"
msgstr ""
+msgid "ComplianceFramework|SOC 2"
+msgstr ""
+
msgid "ComplianceFramework|SOC 2 - Service Organization Control 2"
msgstr ""
+msgid "ComplianceFramework|SOX"
+msgstr ""
+
msgid "ComplianceFramework|SOX - Sarbanes-Oxley"
msgstr ""
@@ -5720,9 +5909,15 @@ msgstr ""
msgid "Container repositories sync capacity"
msgstr ""
+msgid "ContainerRegistry| Please visit the %{linkStart}administration settings%{linkEnd} to enable this feature."
+msgstr ""
+
msgid "ContainerRegistry|%{imageName} tags"
msgstr ""
+msgid "ContainerRegistry|%{title} was successfully scheduled for deletion"
+msgstr ""
+
msgid "ContainerRegistry|Automatically remove extra images that aren't designed to be kept."
msgstr ""
@@ -5735,6 +5930,9 @@ msgstr ""
msgid "ContainerRegistry|Container Registry"
msgstr ""
+msgid "ContainerRegistry|Container Registry tag expiration and retention policy is disabled"
+msgstr ""
+
msgid "ContainerRegistry|Copy build command"
msgstr ""
@@ -5744,18 +5942,12 @@ msgstr ""
msgid "ContainerRegistry|Copy push command"
msgstr ""
-msgid "ContainerRegistry|Currently, the Container Registry tag expiration feature is not available for projects created before GitLab version 12.8. For updates and more information, visit Issue %{linkStart}#196124%{linkEnd}"
-msgstr ""
-
msgid "ContainerRegistry|Docker connection error"
msgstr ""
msgid "ContainerRegistry|Docker tag expiration policy is %{toggleStatus}"
msgstr ""
-msgid "ContainerRegistry|Docker tags with names matching this regex pattern will expire:"
-msgstr ""
-
msgid "ContainerRegistry|Edit Settings"
msgstr ""
@@ -5777,9 +5969,6 @@ msgstr ""
msgid "ContainerRegistry|Image ID"
msgstr ""
-msgid "ContainerRegistry|Image deleted successfully"
-msgstr ""
-
msgid "ContainerRegistry|Keep and protect the images that matter most."
msgstr ""
@@ -5795,12 +5984,24 @@ msgstr ""
msgid "ContainerRegistry|Number of tags to retain:"
msgstr ""
+msgid "ContainerRegistry|Please contact your administrator."
+msgstr ""
+
msgid "ContainerRegistry|Push an image"
msgstr ""
msgid "ContainerRegistry|Quick Start"
msgstr ""
+msgid "ContainerRegistry|Regular expressions such as %{codeStart}.*-test%{codeEnd} or %{codeStart}dev-.*%{codeEnd} are supported"
+msgstr ""
+
+msgid "ContainerRegistry|Regular expressions such as %{codeStart}.*-test%{codeEnd} or %{codeStart}dev-.*%{codeEnd} are supported. To select all tags, use %{codeStart}.*%{codeEnd}"
+msgstr ""
+
+msgid "ContainerRegistry|Remember to run %{docLinkStart}garbage collection%{docLinkEnd} to remove the stale data from storage."
+msgstr ""
+
msgid "ContainerRegistry|Remove repository"
msgstr ""
@@ -5819,22 +6020,22 @@ msgstr[5] ""
msgid "ContainerRegistry|Retention policy has been Enabled"
msgstr ""
-msgid "ContainerRegistry|Something went wrong while deleting the image."
+msgid "ContainerRegistry|Something went wrong while fetching the expiration policy."
msgstr ""
-msgid "ContainerRegistry|Something went wrong while deleting the tag."
+msgid "ContainerRegistry|Something went wrong while fetching the repository list."
msgstr ""
-msgid "ContainerRegistry|Something went wrong while deleting the tags."
+msgid "ContainerRegistry|Something went wrong while fetching the tags list."
msgstr ""
-msgid "ContainerRegistry|Something went wrong while fetching the expiration policy."
+msgid "ContainerRegistry|Something went wrong while marking the tag for deletion."
msgstr ""
-msgid "ContainerRegistry|Something went wrong while fetching the packages list."
+msgid "ContainerRegistry|Something went wrong while marking the tags for deletion."
msgstr ""
-msgid "ContainerRegistry|Something went wrong while fetching the tags list."
+msgid "ContainerRegistry|Something went wrong while scheduling %{title} for deletion. Please try again."
msgstr ""
msgid "ContainerRegistry|Something went wrong while updating the expiration policy."
@@ -5843,16 +6044,25 @@ msgstr ""
msgid "ContainerRegistry|Tag"
msgstr ""
-msgid "ContainerRegistry|Tag deleted successfully"
-msgstr ""
-
msgid "ContainerRegistry|Tag expiration policy"
msgstr ""
msgid "ContainerRegistry|Tag expiration policy is designed to:"
msgstr ""
-msgid "ContainerRegistry|Tags deleted successfully"
+msgid "ContainerRegistry|Tag successfully marked for deletion."
+msgstr ""
+
+msgid "ContainerRegistry|Tags successfully marked for deletion."
+msgstr ""
+
+msgid "ContainerRegistry|Tags with names matching this regex pattern will %{italicStart}be preserved:%{italicEnd}"
+msgstr ""
+
+msgid "ContainerRegistry|Tags with names matching this regex pattern will %{italicStart}expire:%{italicEnd}"
+msgstr ""
+
+msgid "ContainerRegistry|The Container Registry tag expiration and retention policies for this project have not been enabled."
msgstr ""
msgid "ContainerRegistry|The last tag related to this image was recently removed. This empty image and any associated data will be automatically removed as part of the regular Garbage Collection process. If you have any questions, contact your administrator."
@@ -5873,16 +6083,16 @@ msgstr ""
msgid "ContainerRegistry|There are no container images stored for this project"
msgstr ""
-msgid "ContainerRegistry|This Registry contains deleted image tag data. Remember to run %{docLinkStart}garbage collection%{docLinkEnd} to remove the stale data from storage."
+msgid "ContainerRegistry|There was an error during the deletion of this image repository, please try again."
msgstr ""
msgid "ContainerRegistry|This image has no active tags"
msgstr ""
-msgid "ContainerRegistry|We are having trouble connecting to Docker, which could be due to an issue with your project name or path. %{docLinkStart}More Information%{docLinkEnd}"
+msgid "ContainerRegistry|This image repository is scheduled for deletion"
msgstr ""
-msgid "ContainerRegistry|Wildcards such as %{codeStart}.*-stable%{codeEnd} or %{codeStart}production/.*%{codeEnd} are supported. To select all tags, use %{codeStart}.*%{codeEnd}"
+msgid "ContainerRegistry|We are having trouble connecting to Docker, which could be due to an issue with your project name or path. %{docLinkStart}More Information%{docLinkEnd}"
msgstr ""
msgid "ContainerRegistry|With the Container Registry, every project can have its own space to store its Docker images. %{docLinkStart}More Information%{docLinkEnd}"
@@ -6137,7 +6347,7 @@ msgstr ""
msgid "Could not delete chat nickname %{chat_name}."
msgstr ""
-msgid "Could not find design"
+msgid "Could not find design."
msgstr ""
msgid "Could not remove the trigger."
@@ -6152,6 +6362,9 @@ msgstr ""
msgid "Could not revoke personal access token %{personal_access_token_name}."
msgstr ""
+msgid "Could not revoke project access token %{project_access_token_name}."
+msgstr ""
+
msgid "Could not save group ID"
msgstr ""
@@ -6209,6 +6422,9 @@ msgstr ""
msgid "Create a new branch"
msgstr ""
+msgid "Create a new deploy key for this project"
+msgstr ""
+
msgid "Create a new file as there are no files yet. Afterwards, you'll be able to commit your changes."
msgstr ""
@@ -6314,6 +6530,9 @@ msgstr ""
msgid "Create requirement"
msgstr ""
+msgid "Create snippet"
+msgstr ""
+
msgid "Create wildcard: %{searchTerm}"
msgstr ""
@@ -6335,6 +6554,9 @@ msgstr ""
msgid "Created"
msgstr ""
+msgid "Created %{timestamp}"
+msgstr ""
+
msgid "Created At"
msgstr ""
@@ -6695,6 +6917,9 @@ msgstr ""
msgid "CycleAnalytics|stage dropdown"
msgstr ""
+msgid "DAG"
+msgstr ""
+
msgid "DNS"
msgstr ""
@@ -7085,6 +7310,9 @@ msgstr ""
msgid "Deploy key was successfully updated."
msgstr ""
+msgid "Deploy keys allow read-only or read-write (if enabled) access to your repository. Deploy keys can be used for CI, staging or production servers. You can create a deploy key or add an existing one."
+msgstr ""
+
msgid "Deploy progress not found. To see pods, ensure your environment matches %{linkStart}deploy board criteria%{linkEnd}."
msgstr ""
@@ -7238,6 +7466,9 @@ msgstr ""
msgid "Deploying to"
msgstr ""
+msgid "Deployment Frequency"
+msgstr ""
+
msgid "Deployment|API"
msgstr ""
@@ -7760,9 +7991,6 @@ msgstr ""
msgid "Edit environment"
msgstr ""
-msgid "Edit epic description"
-msgstr ""
-
msgid "Edit file"
msgstr ""
@@ -8384,9 +8612,6 @@ msgstr ""
msgid "Epics"
msgstr ""
-msgid "Epics (Ultimate / Gold license only)"
-msgstr ""
-
msgid "Epics Roadmap"
msgstr ""
@@ -8396,6 +8621,9 @@ msgstr ""
msgid "Epics let you manage your portfolio of projects more efficiently and with less effort"
msgstr ""
+msgid "Epics, Issues, and Merge Requests"
+msgstr ""
+
msgid "Epics|Add an epic"
msgstr ""
@@ -8447,6 +8675,9 @@ msgstr ""
msgid "Epics|Something went wrong while fetching group epics."
msgstr ""
+msgid "Epics|Something went wrong while moving item."
+msgstr ""
+
msgid "Epics|Something went wrong while ordering item."
msgstr ""
@@ -8501,6 +8732,9 @@ msgstr ""
msgid "Error fetching network graph."
msgstr ""
+msgid "Error fetching payload data."
+msgstr ""
+
msgid "Error fetching projects"
msgstr ""
@@ -8510,9 +8744,6 @@ msgstr ""
msgid "Error fetching the dependency list. Please check your network connection and try again."
msgstr ""
-msgid "Error fetching usage ping data."
-msgstr ""
-
msgid "Error loading branch data. Please try again."
msgstr ""
@@ -8654,6 +8885,9 @@ msgstr ""
msgid "Errors"
msgstr ""
+msgid "Errors:"
+msgstr ""
+
msgid "Estimated"
msgstr ""
@@ -8858,12 +9092,18 @@ msgstr ""
msgid "Export as CSV"
msgstr ""
+msgid "Export group"
+msgstr ""
+
msgid "Export issues"
msgstr ""
msgid "Export project"
msgstr ""
+msgid "Export this group with all related data to a new GitLab instance. Once complete, you can import the data file from the \"New Group\" page."
+msgstr ""
+
msgid "Export this project with all its related data in order to move your project to a new GitLab instance. Once the export is finished, you can import the file from the \"New Project\" page."
msgstr ""
@@ -8933,9 +9173,6 @@ msgstr ""
msgid "Failed Jobs"
msgstr ""
-msgid "Failed create wiki"
-msgstr ""
-
msgid "Failed to add a Zoom meeting"
msgstr ""
@@ -8975,6 +9212,9 @@ msgstr ""
msgid "Failed to create resources"
msgstr ""
+msgid "Failed to create wiki"
+msgstr ""
+
msgid "Failed to delete board. Please try again."
msgstr ""
@@ -8984,7 +9224,7 @@ msgstr ""
msgid "Failed to enqueue the rebase operation, possibly due to a long-lived transaction. Try again later."
msgstr ""
-msgid "Failed to find import label for jira import."
+msgid "Failed to find import label for Jira import."
msgstr ""
msgid "Failed to get ref."
@@ -9008,6 +9248,12 @@ msgstr ""
msgid "Failed to load groups & users."
msgstr ""
+msgid "Failed to load labels. Please try again."
+msgstr ""
+
+msgid "Failed to load milestones. Please try again."
+msgstr ""
+
msgid "Failed to load related branches"
msgstr ""
@@ -9383,7 +9629,10 @@ msgstr ""
msgid "Filter by milestone name"
msgstr ""
-msgid "Filter by name..."
+msgid "Filter by name"
+msgstr ""
+
+msgid "Filter by status"
msgstr ""
msgid "Filter by two-factor authentication"
@@ -9395,12 +9644,18 @@ msgstr ""
msgid "Filter projects"
msgstr ""
+msgid "Filter results"
+msgstr ""
+
msgid "Filter results by group"
msgstr ""
msgid "Filter results by project"
msgstr ""
+msgid "Filter results..."
+msgstr ""
+
msgid "Filter your projects by name"
msgstr ""
@@ -9689,10 +9944,10 @@ msgstr ""
msgid "GeoNodes|Checksummed"
msgstr ""
-msgid "GeoNodes|Container repositories"
+msgid "GeoNodes|Consult Geo troubleshooting information"
msgstr ""
-msgid "GeoNodes|Data is out of date from %{timeago}"
+msgid "GeoNodes|Container repositories"
msgstr ""
msgid "GeoNodes|Data replication lag"
@@ -9734,6 +9989,9 @@ msgstr ""
msgid "GeoNodes|Last event ID seen from primary"
msgstr ""
+msgid "GeoNodes|Learn more about Geo node statuses"
+msgstr ""
+
msgid "GeoNodes|Loading nodes"
msgstr ""
@@ -9749,6 +10007,9 @@ msgstr ""
msgid "GeoNodes|Node was successfully removed."
msgstr ""
+msgid "GeoNodes|Node's status was updated %{timeAgo}."
+msgstr ""
+
msgid "GeoNodes|Not checksummed"
msgstr ""
@@ -9779,7 +10040,10 @@ msgstr ""
msgid "GeoNodes|Repository verification progress"
msgstr ""
-msgid "GeoNodes|Selective"
+msgid "GeoNodes|Selective (%{syncLabel})"
+msgstr ""
+
+msgid "GeoNodes|Selective synchronization"
msgstr ""
msgid "GeoNodes|Something went wrong while changing node status"
@@ -9806,6 +10070,9 @@ msgstr ""
msgid "GeoNodes|Unverified"
msgstr ""
+msgid "GeoNodes|Updated %{timeAgo}"
+msgstr ""
+
msgid "GeoNodes|Used slots"
msgstr ""
@@ -9845,16 +10112,16 @@ msgstr ""
msgid "Geo|All"
msgstr ""
-msgid "Geo|All projects"
+msgid "Geo|All %{replicable_type}"
msgstr ""
-msgid "Geo|All projects are being scheduled for re-sync"
+msgid "Geo|All projects"
msgstr ""
-msgid "Geo|All projects are being scheduled for re-verify"
+msgid "Geo|All projects are being scheduled for resync"
msgstr ""
-msgid "Geo|Batch operations"
+msgid "Geo|All projects are being scheduled for reverify"
msgstr ""
msgid "Geo|Could not remove tracking entry for an existing project."
@@ -9866,9 +10133,15 @@ msgstr ""
msgid "Geo|Failed"
msgstr ""
+msgid "Geo|Filter by status"
+msgstr ""
+
msgid "Geo|Geo Status"
msgstr ""
+msgid "Geo|In progress"
+msgstr ""
+
msgid "Geo|In sync"
msgstr ""
@@ -9893,9 +10166,6 @@ msgstr ""
msgid "Geo|Not synced yet"
msgstr ""
-msgid "Geo|Pending"
-msgstr ""
-
msgid "Geo|Pending synchronization"
msgstr ""
@@ -9932,7 +10202,7 @@ msgstr ""
msgid "Geo|Resync"
msgstr ""
-msgid "Geo|Resync all projects"
+msgid "Geo|Resync all"
msgstr ""
msgid "Geo|Retry count"
@@ -9941,7 +10211,7 @@ msgstr ""
msgid "Geo|Reverify"
msgstr ""
-msgid "Geo|Reverify all projects"
+msgid "Geo|Reverify all"
msgstr ""
msgid "Geo|Status"
@@ -10283,9 +10553,6 @@ msgstr ""
msgid "Go to file"
msgstr ""
-msgid "Go to file (MRs only)"
-msgstr ""
-
msgid "Go to file permalink (while viewing a file)"
msgstr ""
@@ -10730,6 +10997,9 @@ msgstr ""
msgid "GroupSettings|Disable group mentions"
msgstr ""
+msgid "GroupSettings|Export group"
+msgstr ""
+
msgid "GroupSettings|If the parent group's visibility is lower than the group current visibility, visibility levels for subgroups and projects will be changed to match the new parent group's visibility."
msgstr ""
@@ -10808,6 +11078,9 @@ msgstr ""
msgid "Groups (%{count})"
msgstr ""
+msgid "Groups (%{groups})"
+msgstr ""
+
msgid "Groups can also be nested by creating %{subgroup_docs_link_start}subgroups%{subgroup_docs_link_end}."
msgstr ""
@@ -11081,15 +11354,12 @@ msgstr ""
msgid "ID:"
msgstr ""
-msgid "IDE|Allow live previews of JavaScript projects in the Web IDE using CodeSandbox client side evaluation."
+msgid "IDE|Allow live previews of JavaScript projects in the Web IDE using CodeSandbox Live Preview."
msgstr ""
msgid "IDE|Back"
msgstr ""
-msgid "IDE|Client side evaluation"
-msgstr ""
-
msgid "IDE|Commit"
msgstr ""
@@ -11153,6 +11423,9 @@ msgstr ""
msgid "If any job surpasses this timeout threshold, it will be marked as failed. Human readable time input language is accepted like \"1 hour\". Values without specification represent seconds."
msgstr ""
+msgid "If blank, set allowable lifetime to %{instance_level_policy_in_words}, as defined by the instance admin. Once set, existing tokens for users in this group may be revoked."
+msgstr ""
+
msgid "If checked, group owners can manage LDAP group links and LDAP member overrides"
msgstr ""
@@ -11192,6 +11465,9 @@ msgstr ""
msgid "If you lose your recovery codes you can generate new ones, invalidating all previous codes."
msgstr ""
+msgid "If you reach 100%% storage capacity, you will not be able to: %{base_message}"
+msgstr ""
+
msgid "If your HTTP repository is not publicly accessible, add your credentials."
msgstr ""
@@ -11303,6 +11579,9 @@ msgstr ""
msgid "Import repository"
msgstr ""
+msgid "Import started by: %{importInitiator}"
+msgstr ""
+
msgid "Import tasks"
msgstr ""
@@ -11369,6 +11648,9 @@ msgstr ""
msgid "In order to tailor your experience with GitLab we<br>would like to know a bit more about you."
msgstr ""
+msgid "In progress"
+msgstr ""
+
msgid "In the next step, you'll be able to select the projects you want to import."
msgstr ""
@@ -11720,15 +12002,15 @@ msgstr ""
msgid "Issues"
msgstr ""
-msgid "Issues / Merge Requests"
-msgstr ""
-
msgid "Issues Analytics"
msgstr ""
msgid "Issues Rate Limits"
msgstr ""
+msgid "Issues and Merge Requests"
+msgstr ""
+
msgid "Issues can be bugs, tasks or ideas to be discussed. Also, issues are searchable and filterable."
msgstr ""
@@ -11750,10 +12032,10 @@ msgstr ""
msgid "IssuesAnalytics|Avg/Month:"
msgstr ""
-msgid "IssuesAnalytics|Issues created"
+msgid "IssuesAnalytics|Issues opened"
msgstr ""
-msgid "IssuesAnalytics|Issues created per month"
+msgid "IssuesAnalytics|Issues opened per month"
msgstr ""
msgid "IssuesAnalytics|Last 12 months"
@@ -11807,6 +12089,9 @@ msgstr ""
msgid "Jira integration not configured."
msgstr ""
+msgid "Jira project: %{importProject}"
+msgstr ""
+
msgid "JiraService|Events for %{noteable_model_name} are disabled."
msgstr ""
@@ -12200,6 +12485,9 @@ msgstr ""
msgid "Lead"
msgstr ""
+msgid "Lead Time"
+msgstr ""
+
msgid "Learn GitLab"
msgstr ""
@@ -12212,6 +12500,9 @@ msgstr ""
msgid "Learn how to %{no_packages_link_start}publish and share your packages%{no_packages_link_end} with GitLab."
msgstr ""
+msgid "Learn how to enable synchronization"
+msgstr ""
+
msgid "Learn more"
msgstr ""
@@ -12563,9 +12854,6 @@ msgstr ""
msgid "Live preview"
msgstr ""
-msgid "Load more vulnerabilities"
-msgstr ""
-
msgid "Loading"
msgstr ""
@@ -12716,9 +13004,6 @@ msgstr ""
msgid "Manage"
msgstr ""
-msgid "Manage Git repositories with fine-grained access controls that keep your code secure. Perform code reviews and enhance collaboration with merge requests. Each project can also have an issue tracker and a wiki."
-msgstr ""
-
msgid "Manage Web IDE features"
msgstr ""
@@ -12749,6 +13034,9 @@ msgstr ""
msgid "Manage your license"
msgstr ""
+msgid "Managed Account"
+msgstr ""
+
msgid "Manifest"
msgstr ""
@@ -13013,6 +13301,9 @@ msgstr ""
msgid "Merge Requests in Review"
msgstr ""
+msgid "Merge automatically (%{strategy})"
+msgstr ""
+
msgid "Merge commit message"
msgstr ""
@@ -13166,6 +13457,12 @@ msgstr ""
msgid "Merged branches are being deleted. This can take some time depending on the number of branches. Please refresh the page to see changes."
msgstr ""
+msgid "Merged this merge request."
+msgstr ""
+
+msgid "Merges this merge request immediately."
+msgstr ""
+
msgid "Merges this merge request when the pipeline succeeds."
msgstr ""
@@ -13277,12 +13574,15 @@ msgstr[3] ""
msgstr[4] ""
msgstr[5] ""
-msgid "Metrics|Environment"
+msgid "Metrics|Expand panel"
msgstr ""
msgid "Metrics|For grouping similar metrics"
msgstr ""
+msgid "Metrics|Go back (Esc)"
+msgstr ""
+
msgid "Metrics|Invalid time range, please verify."
msgstr ""
@@ -13313,9 +13613,6 @@ msgstr ""
msgid "Metrics|Refresh dashboard"
msgstr ""
-msgid "Metrics|Show last"
-msgstr ""
-
msgid "Metrics|There was an error creating the dashboard."
msgstr ""
@@ -13394,6 +13691,9 @@ msgstr ""
msgid "Microsoft Azure"
msgstr ""
+msgid "Middleman project with Static Site Editor support"
+msgstr ""
+
msgid "Migrated %{success_count}/%{total_count} files."
msgstr ""
@@ -13847,10 +14147,10 @@ msgstr ""
msgid "Next"
msgstr ""
-msgid "Next file in diff (MRs only)"
+msgid "Next file in diff"
msgstr ""
-msgid "Next unresolved discussion (MRs only)"
+msgid "Next unresolved discussion"
msgstr ""
msgid "Nickname"
@@ -13910,6 +14210,9 @@ msgstr ""
msgid "No changes between %{ref_start}%{source_branch}%{ref_end} and %{ref_start}%{target_branch}%{ref_end}"
msgstr ""
+msgid "No child epics match applied filters"
+msgstr ""
+
msgid "No connection could be made to a Gitaly Server, please check your logs!"
msgstr ""
@@ -14048,15 +14351,6 @@ msgstr ""
msgid "No thanks, don't show this again"
msgstr ""
-msgid "No vulnerabilities found for this group"
-msgstr ""
-
-msgid "No vulnerabilities found for this pipeline"
-msgstr ""
-
-msgid "No vulnerabilities found for this project"
-msgstr ""
-
msgid "No vulnerabilities present"
msgstr ""
@@ -14180,6 +14474,9 @@ msgstr ""
msgid "Nothing to preview."
msgstr ""
+msgid "Nothing to synchronize"
+msgstr ""
+
msgid "Notification events"
msgstr ""
@@ -14351,6 +14648,9 @@ msgstr ""
msgid "Once removed, the fork relationship cannot be restored and you will no longer be able to send merge requests to the source."
msgstr ""
+msgid "Once the exported file is ready you can download it from this page."
+msgstr ""
+
msgid "Once the exported file is ready, you will receive a notification email with a download link, or you can download it from this page."
msgstr ""
@@ -14456,9 +14756,6 @@ msgstr ""
msgid "Open sidebar"
msgstr ""
-msgid "Open source software to collaborate on code"
-msgstr ""
-
msgid "Open: %{openIssuesCount}"
msgstr ""
@@ -14561,6 +14858,9 @@ msgstr ""
msgid "OutdatedBrowser|You can provide feedback %{feedback_link_start}on this issue%{feedback_link_end} or via your usual support channels."
msgstr ""
+msgid "Overridden"
+msgstr ""
+
msgid "Overview"
msgstr ""
@@ -14612,6 +14912,9 @@ msgstr ""
msgid "PackageRegistry|Conan Command"
msgstr ""
+msgid "PackageRegistry|Copy .pypirc content"
+msgstr ""
+
msgid "PackageRegistry|Copy Conan Command"
msgstr ""
@@ -14633,6 +14936,9 @@ msgstr ""
msgid "PackageRegistry|Copy NuGet Setup Command"
msgstr ""
+msgid "PackageRegistry|Copy Pip command"
+msgstr ""
+
msgid "PackageRegistry|Copy and paste this inside your %{codeStart}pom.xml%{codeEnd} %{codeStart}dependencies%{codeEnd} block."
msgstr ""
@@ -14666,6 +14972,12 @@ msgstr ""
msgid "PackageRegistry|For more information on the NuGet registry, %{linkStart}see the documentation%{linkEnd}."
msgstr ""
+msgid "PackageRegistry|For more information on the PyPi registry, %{linkStart}see the documentation%{linkEnd}."
+msgstr ""
+
+msgid "PackageRegistry|If you haven't already done so, you will need to add the below to your %{codeStart}.pypirc%{codeEnd} file."
+msgstr ""
+
msgid "PackageRegistry|If you haven't already done so, you will need to add the below to your %{codeStart}pom.xml%{codeEnd} file."
msgstr ""
@@ -14675,6 +14987,9 @@ msgstr ""
msgid "PackageRegistry|Learn how to %{noPackagesLinkStart}publish and share your packages%{noPackagesLinkEnd} with GitLab."
msgstr ""
+msgid "PackageRegistry|Manually Published"
+msgstr ""
+
msgid "PackageRegistry|Maven"
msgstr ""
@@ -14693,12 +15008,18 @@ msgstr ""
msgid "PackageRegistry|NuGet Command"
msgstr ""
+msgid "PackageRegistry|Pip Command"
+msgstr ""
+
msgid "PackageRegistry|Pipeline %{linkStart}%{linkEnd} triggered %{timestamp} by %{author}"
msgstr ""
msgid "PackageRegistry|Published to the repository at %{timestamp}"
msgstr ""
+msgid "PackageRegistry|PyPi"
+msgstr ""
+
msgid "PackageRegistry|Registry Setup"
msgstr ""
@@ -14735,6 +15056,9 @@ msgstr ""
msgid "PackageRegistry|npm"
msgstr ""
+msgid "PackageRegistry|published by %{author}"
+msgstr ""
+
msgid "PackageRegistry|yarn"
msgstr ""
@@ -14750,6 +15074,9 @@ msgstr ""
msgid "PackageType|NuGet"
msgstr ""
+msgid "PackageType|PyPi"
+msgstr ""
+
msgid "Packages"
msgstr ""
@@ -14843,6 +15170,9 @@ msgstr ""
msgid "Past due"
msgstr ""
+msgid "Paste a machine public key here. Read more about how to generate it"
+msgstr ""
+
msgid "Paste a machine public key here. Read more about how to generate it %{link_start}here%{link_end}"
msgstr ""
@@ -14858,9 +15188,6 @@ msgstr ""
msgid "Path"
msgstr ""
-msgid "Path, transfer, remove"
-msgstr ""
-
msgid "Path:"
msgstr ""
@@ -14891,7 +15218,7 @@ msgstr ""
msgid "Percentage"
msgstr ""
-msgid "Perform advanced options such as changing path, transferring, or removing the group."
+msgid "Perform advanced options such as changing path, transferring, exporting, or removing the group."
msgstr ""
msgid "Perform common operations on GitLab project"
@@ -14972,9 +15299,6 @@ msgstr ""
msgid "Pipeline minutes quota"
msgstr ""
-msgid "Pipeline quota"
-msgstr ""
-
msgid "Pipeline subscriptions"
msgstr ""
@@ -14984,6 +15308,9 @@ msgstr ""
msgid "Pipeline: %{status}"
msgstr ""
+msgid "PipelineCharts|CI / CD Analytics"
+msgstr ""
+
msgid "PipelineCharts|Failed:"
msgstr ""
@@ -15062,15 +15389,6 @@ msgstr ""
msgid "Pipelines settings for '%{project_name}' were successfully updated."
msgstr ""
-msgid "Pipelines| to purchase more minutes."
-msgstr ""
-
-msgid "Pipelines|%{namespace_name} has exceeded its pipeline minutes quota."
-msgstr ""
-
-msgid "Pipelines|%{namespace_name} has less than %{notification_level}%% of CI minutes available."
-msgstr ""
-
msgid "Pipelines|API"
msgstr ""
@@ -15092,10 +15410,13 @@ msgstr ""
msgid "Pipelines|Get started with Pipelines"
msgstr ""
-msgid "Pipelines|Loading Pipelines"
+msgid "Pipelines|Group %{namespace_name} has %{percentage}%% or less Shared Runner Pipeline minutes remaining. Once it runs out, no new jobs or pipelines in its projects will run."
msgstr ""
-msgid "Pipelines|Pipelines will not run anymore on shared Runners."
+msgid "Pipelines|Group %{namespace_name} has exceeded its pipeline minutes quota. Unless you buy additional pipeline minutes, no new jobs or pipelines in its projects will run."
+msgstr ""
+
+msgid "Pipelines|Loading Pipelines"
msgstr ""
msgid "Pipelines|Project cache successfully reset."
@@ -15326,6 +15647,9 @@ msgstr ""
msgid "Please select"
msgstr ""
+msgid "Please select a Jira project"
+msgstr ""
+
msgid "Please select a country"
msgstr ""
@@ -15371,6 +15695,9 @@ msgstr ""
msgid "Please wait while we import the repository for you. Refresh at will."
msgstr ""
+msgid "Plugins directory is deprecated and will be removed in 14.0. Please move this file into /file_hooks directory."
+msgstr ""
+
msgid "Pod does not exist"
msgstr ""
@@ -15488,7 +15815,7 @@ msgstr ""
msgid "Prevent users from changing their profile name"
msgstr ""
-msgid "Prevent users from modifing merge request approvers list"
+msgid "Prevent users from modifying merge request approvers list"
msgstr ""
msgid "Prevent users from performing write operations on GitLab while performing maintenance."
@@ -15509,10 +15836,10 @@ msgstr ""
msgid "Previous Artifacts"
msgstr ""
-msgid "Previous file in diff (MRs only)"
+msgid "Previous file in diff"
msgstr ""
-msgid "Previous unresolved discussion (MRs only)"
+msgid "Previous unresolved discussion"
msgstr ""
msgid "Primary"
@@ -15545,6 +15872,9 @@ msgstr ""
msgid "Private profile"
msgstr ""
+msgid "Private projects Minutes cost factor"
+msgstr ""
+
msgid "Private projects can be created in your personal namespace with:"
msgstr ""
@@ -16373,6 +16703,9 @@ msgstr ""
msgid "ProjectSettings|With GitLab Pages you can host your static websites on GitLab"
msgstr ""
+msgid "ProjectSettings|With Metrics Dashboard you can visualize this project performance metrics"
+msgstr ""
+
msgid "ProjectTemplates|.NET Core"
msgstr ""
@@ -16436,6 +16769,9 @@ msgstr ""
msgid "ProjectTemplates|Spring"
msgstr ""
+msgid "ProjectTemplates|Static Site Editor/Middleman"
+msgstr ""
+
msgid "ProjectTemplates|iOS (Swift)"
msgstr ""
@@ -16655,13 +16991,13 @@ msgstr ""
msgid "PrometheusService|Prometheus is being automatically managed on your clusters"
msgstr ""
-msgid "PrometheusService|These metrics will only be monitored after your first deployment to an environment"
+msgid "PrometheusService|Select the Active checkbox to override the Auto Configuration with custom settings. If unchecked, Auto Configuration settings are used."
msgstr ""
-msgid "PrometheusService|Time-series monitoring service"
+msgid "PrometheusService|These metrics will only be monitored after your first deployment to an environment"
msgstr ""
-msgid "PrometheusService|To enable manual configuration, uninstall Prometheus from your clusters"
+msgid "PrometheusService|Time-series monitoring service"
msgstr ""
msgid "PrometheusService|To enable the installation of Prometheus on your clusters, deactivate the manual configuration below"
@@ -16847,6 +17183,9 @@ msgstr ""
msgid "Protip:"
msgstr ""
+msgid "Protocol"
+msgstr ""
+
msgid "Provider"
msgstr ""
@@ -16871,6 +17210,9 @@ msgstr ""
msgid "Public pipelines"
msgstr ""
+msgid "Public projects Minutes cost factor"
+msgstr ""
+
msgid "Pull"
msgstr ""
@@ -17075,6 +17417,9 @@ msgstr[3] ""
msgstr[4] ""
msgstr[5] ""
+msgid "Regenerate export"
+msgstr ""
+
msgid "Regenerate instance ID"
msgstr ""
@@ -17456,9 +17801,33 @@ msgstr ""
msgid "Reports|%{combinedString} and %{resolvedString}"
msgstr ""
+msgid "Reports|Accessibility scanning detected %d issue for the source branch only"
+msgid_plural "Reports|Accessibility scanning detected %d issues for the source branch only"
+msgstr[0] ""
+msgstr[1] ""
+msgstr[2] ""
+msgstr[3] ""
+msgstr[4] ""
+msgstr[5] ""
+
+msgid "Reports|Accessibility scanning detected no issues for the source branch only"
+msgstr ""
+
+msgid "Reports|Accessibility scanning failed loading results"
+msgstr ""
+
+msgid "Reports|Accessibility scanning results are being parsed"
+msgstr ""
+
msgid "Reports|Actions"
msgstr ""
+msgid "Reports|An error occured while loading report"
+msgstr ""
+
+msgid "Reports|An error occurred while loading %{name} results"
+msgstr ""
+
msgid "Reports|Class"
msgstr ""
@@ -17564,7 +17933,7 @@ msgstr ""
msgid "Requested %{time_ago}"
msgstr ""
-msgid "Requested design version does not exist"
+msgid "Requested design version does not exist."
msgstr ""
msgid "Requested states are invalid"
@@ -17756,6 +18125,9 @@ msgstr ""
msgid "Resync"
msgstr ""
+msgid "Resync all"
+msgstr ""
+
msgid "Resync all %{replicableType}"
msgstr ""
@@ -17828,6 +18200,9 @@ msgstr ""
msgid "Revoked personal access token %{personal_access_token_name}!"
msgstr ""
+msgid "Revoked project access token %{project_access_token_name}!"
+msgstr ""
+
msgid "RightSidebar|adding a"
msgstr ""
@@ -17876,6 +18251,9 @@ msgstr ""
msgid "Runner token"
msgstr ""
+msgid "Runner tokens"
+msgstr ""
+
msgid "Runner was not updated."
msgstr ""
@@ -17930,6 +18308,9 @@ msgstr ""
msgid "SAML SSO for %{group_name}"
msgstr ""
+msgid "SAML discovery tokens"
+msgstr ""
+
msgid "SAML for %{group_name}"
msgstr ""
@@ -18014,12 +18395,18 @@ msgstr ""
msgid "Scheduled"
msgstr ""
+msgid "Scheduled to merge this merge request (%{strategy})."
+msgstr ""
+
msgid "Scheduled to merge this merge request when the pipeline succeeds."
msgstr ""
msgid "Schedules"
msgstr ""
+msgid "Schedules to merge this merge request (%{strategy})."
+msgstr ""
+
msgid "Scheduling"
msgstr ""
@@ -18077,6 +18464,9 @@ msgstr ""
msgid "Search branches and tags"
msgstr ""
+msgid "Search by author"
+msgstr ""
+
msgid "Search files"
msgstr ""
@@ -18254,15 +18644,6 @@ msgstr[3] ""
msgstr[4] ""
msgstr[5] ""
-msgid "SearchResults|snippet result"
-msgid_plural "SearchResults|snippet results"
-msgstr[0] ""
-msgstr[1] ""
-msgstr[2] ""
-msgstr[3] ""
-msgstr[4] ""
-msgstr[5] ""
-
msgid "SearchResults|user"
msgid_plural "SearchResults|users"
msgstr[0] ""
@@ -18281,6 +18662,9 @@ msgstr[3] ""
msgstr[4] ""
msgstr[5] ""
+msgid "Searching by both author and message is currently not supported."
+msgstr ""
+
msgid "Seat Link"
msgstr ""
@@ -18308,184 +18692,232 @@ msgstr ""
msgid "Security Dashboard"
msgstr ""
-msgid "Security Dashboard|Error fetching the vulnerability counts. Please check your network connection and try again."
+msgid "Security configuration help link"
msgstr ""
-msgid "Security Dashboard|Error fetching the vulnerability list. Please check your network connection and try again."
+msgid "Security dashboard"
msgstr ""
-msgid "Security Dashboard|Issue Created"
+msgid "Security report is out of date. Please update your branch with the latest changes from the target branch (%{targetBranchName})"
msgstr ""
-msgid "Security Reports|Comment added to '%{vulnerabilityName}'"
+msgid "Security report is out of date. Run %{newPipelineLinkStart}a new pipeline%{newPipelineLinkEnd} for the target branch (%{targetBranchName})"
msgstr ""
-msgid "Security Reports|Comment deleted on '%{vulnerabilityName}'"
+msgid "SecurityConfiguration|Configured"
msgstr ""
-msgid "Security Reports|Comment edited on '%{vulnerabilityName}'"
+msgid "SecurityConfiguration|Feature"
msgstr ""
-msgid "Security Reports|Create issue"
+msgid "SecurityConfiguration|Feature documentation for %{featureName}"
msgstr ""
-msgid "Security Reports|Dismiss vulnerability"
+msgid "SecurityConfiguration|Not yet configured"
msgstr ""
-msgid "Security Reports|Dismissed '%{vulnerabilityName}'"
+msgid "SecurityConfiguration|Secure features"
msgstr ""
-msgid "Security Reports|Dismissed '%{vulnerabilityName}'. Turn off the hide dismissed toggle to view."
+msgid "SecurityConfiguration|Status"
msgstr ""
-msgid "Security Reports|Either you don't have permission to view this dashboard or the dashboard has not been setup. Please check your permission settings with your administrator or check your dashboard configurations to proceed."
+msgid "SecurityReports|%{firstProject} and %{secondProject}"
msgstr ""
-msgid "Security Reports|Learn more about setting up your dashboard"
+msgid "SecurityReports|%{firstProject}, %{secondProject}, and %{rest}"
msgstr ""
-msgid "Security Reports|More info"
+msgid "SecurityReports|Add a project to your dashboard"
msgstr ""
-msgid "Security Reports|Oops, something doesn't seem right."
+msgid "SecurityReports|Add or remove projects from your dashboard"
msgstr ""
-msgid "Security Reports|Security reports can only be accessed by authorized users."
+msgid "SecurityReports|Add projects"
msgstr ""
-msgid "Security Reports|There was an error adding the comment."
+msgid "SecurityReports|Comment added to '%{vulnerabilityName}'"
msgstr ""
-msgid "Security Reports|There was an error creating the issue."
+msgid "SecurityReports|Comment deleted on '%{vulnerabilityName}'"
msgstr ""
-msgid "Security Reports|There was an error creating the merge request."
+msgid "SecurityReports|Comment edited on '%{vulnerabilityName}'"
msgstr ""
-msgid "Security Reports|There was an error deleting the comment."
+msgid "SecurityReports|Create issue"
msgstr ""
-msgid "Security Reports|There was an error dismissing the vulnerabilities."
+msgid "SecurityReports|Dismiss Selected"
msgstr ""
-msgid "Security Reports|There was an error dismissing the vulnerability."
+msgid "SecurityReports|Dismiss vulnerability"
msgstr ""
-msgid "Security Reports|There was an error reverting the dismissal."
+msgid "SecurityReports|Dismissed '%{vulnerabilityName}'"
msgstr ""
-msgid "Security Reports|There was an error reverting this dismissal."
+msgid "SecurityReports|Dismissed '%{vulnerabilityName}'. Turn off the hide dismissed toggle to view."
msgstr ""
-msgid "Security Reports|Undo dismiss"
+msgid "SecurityReports|Each vulnerability now has a unique page that can be directly linked to, shared, referenced, and tracked as the single source of truth. Vulnerability occurrences also persist across scanner runs, which improves tracking and visibility and reduces duplicates between scans."
msgstr ""
-msgid "Security Reports|You do not have sufficient permissions to access this report"
+msgid "SecurityReports|Edit dashboard"
msgstr ""
-msgid "Security Reports|You must sign in as an authorized user to see this report"
+msgid "SecurityReports|Either you don't have permission to view this dashboard or the dashboard has not been setup. Please check your permission settings with your administrator or check your dashboard configurations to proceed."
msgstr ""
-msgid "Security configuration help link"
+msgid "SecurityReports|Error fetching the vulnerability counts. Please check your network connection and try again."
msgstr ""
-msgid "Security dashboard"
+msgid "SecurityReports|Error fetching the vulnerability list. Please check your network connection and try again."
msgstr ""
-msgid "Security report is out of date. Please update your branch with the latest changes from the target branch (%{targetBranchName})"
+msgid "SecurityReports|False positive"
msgstr ""
-msgid "Security report is out of date. Run %{newPipelineLinkStart}a new pipeline%{newPipelineLinkEnd} for the target branch (%{targetBranchName})"
+msgid "SecurityReports|Hide dismissed"
msgstr ""
-msgid "SecurityConfiguration|Configured"
+msgid "SecurityReports|Introducing standalone vulnerabilities"
msgstr ""
-msgid "SecurityConfiguration|Feature"
+msgid "SecurityReports|Issue Created"
msgstr ""
-msgid "SecurityConfiguration|Feature documentation for %{featureName}"
+msgid "SecurityReports|Learn More"
msgstr ""
-msgid "SecurityConfiguration|Not yet configured"
+msgid "SecurityReports|Learn more about setting up your dashboard"
msgstr ""
-msgid "SecurityConfiguration|Secure features"
+msgid "SecurityReports|Load more vulnerabilities"
msgstr ""
-msgid "SecurityConfiguration|Status"
+msgid "SecurityReports|Monitor vulnerabilities in your code"
+msgstr ""
+
+msgid "SecurityReports|More info"
+msgstr ""
+
+msgid "SecurityReports|More information"
+msgstr ""
+
+msgid "SecurityReports|No vulnerabilities found for dashboard"
+msgstr ""
+
+msgid "SecurityReports|No vulnerabilities found for this group"
+msgstr ""
+
+msgid "SecurityReports|No vulnerabilities found for this pipeline"
+msgstr ""
+
+msgid "SecurityReports|No vulnerabilities found for this project"
+msgstr ""
+
+msgid "SecurityReports|Oops, something doesn't seem right."
+msgstr ""
+
+msgid "SecurityReports|Pipeline %{pipelineLink} triggered %{timeago} by %{user}"
msgstr ""
-msgid "SecurityDashboard|%{firstProject} and %{secondProject}"
+msgid "SecurityReports|Project"
msgstr ""
-msgid "SecurityDashboard|%{firstProject}, %{secondProject}, and %{rest}"
+msgid "SecurityReports|Projects added"
msgstr ""
-msgid "SecurityDashboard|Add a project to your dashboard"
+msgid "SecurityReports|Remove project from dashboard"
msgstr ""
-msgid "SecurityDashboard|Add or remove projects from your dashboard"
+msgid "SecurityReports|Report type"
msgstr ""
-msgid "SecurityDashboard|Add projects"
+msgid "SecurityReports|Return to dashboard"
msgstr ""
-msgid "SecurityDashboard|Edit dashboard"
+msgid "SecurityReports|Security Dashboard"
msgstr ""
-msgid "SecurityDashboard|Hide dismissed"
+msgid "SecurityReports|Security reports can only be accessed by authorized users."
msgstr ""
-msgid "SecurityDashboard|Monitor vulnerabilities in your code"
+msgid "SecurityReports|Select a project to add by using the project search field above."
msgstr ""
-msgid "SecurityDashboard|More information"
+msgid "SecurityReports|Select a reason"
msgstr ""
-msgid "SecurityDashboard|No vulnerabilities found for dashboard"
+msgid "SecurityReports|Severity"
msgstr ""
-msgid "SecurityDashboard|Pipeline %{pipelineLink} triggered %{timeago} by %{user}"
+msgid "SecurityReports|Status"
msgstr ""
-msgid "SecurityDashboard|Project"
+msgid "SecurityReports|The security dashboard displays the latest security findings for projects you wish to monitor. Select \"Edit dashboard\" to add and remove projects."
msgstr ""
-msgid "SecurityDashboard|Projects added"
+msgid "SecurityReports|The security dashboard displays the latest security report. Use it to find and fix vulnerabilities."
msgstr ""
-msgid "SecurityDashboard|Remove project from dashboard"
+msgid "SecurityReports|There was an error adding the comment."
msgstr ""
-msgid "SecurityDashboard|Report type"
+msgid "SecurityReports|There was an error creating the issue."
msgstr ""
-msgid "SecurityDashboard|Return to dashboard"
+msgid "SecurityReports|There was an error creating the merge request."
msgstr ""
-msgid "SecurityDashboard|Security Dashboard"
+msgid "SecurityReports|There was an error deleting the comment."
msgstr ""
-msgid "SecurityDashboard|Select a project to add by using the project search field above."
+msgid "SecurityReports|There was an error dismissing the vulnerabilities."
msgstr ""
-msgid "SecurityDashboard|Severity"
+msgid "SecurityReports|There was an error dismissing the vulnerability."
msgstr ""
-msgid "SecurityDashboard|Status"
+msgid "SecurityReports|There was an error reverting the dismissal."
msgstr ""
-msgid "SecurityDashboard|The security dashboard displays the latest security findings for projects you wish to monitor. Select \"Edit dashboard\" to add and remove projects."
+msgid "SecurityReports|There was an error reverting this dismissal."
msgstr ""
-msgid "SecurityDashboard|The security dashboard displays the latest security report. Use it to find and fix vulnerabilities."
+msgid "SecurityReports|There was an error while generating the report."
msgstr ""
-msgid "SecurityDashboard|There was an error while generating the report."
+msgid "SecurityReports|Unable to add %{invalidProjects}"
msgstr ""
-msgid "SecurityDashboard|Unable to add %{invalidProjects}"
+msgid "SecurityReports|Undo dismiss"
+msgstr ""
+
+msgid "SecurityReports|While it's rare to have no vulnerabilities for your group, it can happen. In any event, we ask that you double check your settings to make sure you've set up your dashboard correctly."
+msgstr ""
+
+msgid "SecurityReports|While it's rare to have no vulnerabilities for your pipeline, it can happen. In any event, we ask that you double check your settings to make sure all security scanning jobs have passed successfully."
+msgstr ""
+
+msgid "SecurityReports|While it's rare to have no vulnerabilities for your project, it can happen. In any event, we ask that you double check your settings to make sure you've set up your dashboard correctly."
+msgstr ""
+
+msgid "SecurityReports|While it's rare to have no vulnerabilities, it can happen. In any event, we ask that you please double check your settings to make sure you've set up your dashboard correctly."
+msgstr ""
+
+msgid "SecurityReports|Won't fix / Accept risk"
+msgstr ""
+
+msgid "SecurityReports|You do not have sufficient permissions to access this report"
+msgstr ""
+
+msgid "SecurityReports|You must sign in as an authorized user to see this report"
+msgstr ""
+
+msgid "SecurityReports|[No reason]"
msgstr ""
msgid "See GitLab's %{password_policy_guidelines}"
@@ -18497,6 +18929,9 @@ msgstr ""
msgid "See the affected projects in the GitLab admin panel"
msgstr ""
+msgid "See what's new at GitLab"
+msgstr ""
+
msgid "Select"
msgstr ""
@@ -18902,9 +19337,6 @@ msgstr ""
msgid "Set up Jira Integration"
msgstr ""
-msgid "Set up Jira Integration illustration"
-msgstr ""
-
msgid "Set up a %{type} Runner automatically"
msgstr ""
@@ -18989,6 +19421,9 @@ msgstr ""
msgid "Severity: %{severity}"
msgstr ""
+msgid "Shards (%{shards})"
+msgstr ""
+
msgid "Shards to synchronize"
msgstr ""
@@ -19307,6 +19742,9 @@ msgstr ""
msgid "Snowplow"
msgstr ""
+msgid "Some child epics may be hidden due to applied filters"
+msgstr ""
+
msgid "Some email servers do not support overriding the email sender name. Enable this option to include the name of the author of the issue, merge request or comment in the email body instead."
msgstr ""
@@ -19448,6 +19886,9 @@ msgstr ""
msgid "Something went wrong while updating your list settings"
msgstr ""
+msgid "Something went wrong with your automatic subscription renewal"
+msgstr ""
+
msgid "Something went wrong, unable to add %{project} to dashboard"
msgstr ""
@@ -19733,6 +20174,9 @@ msgstr ""
msgid "Stage removed"
msgstr ""
+msgid "Standard"
+msgstr ""
+
msgid "Star a label to make it a priority label. Order the prioritized labels to change their relative priority, by dragging."
msgstr ""
@@ -19856,6 +20300,9 @@ msgstr ""
msgid "Static Application Security Testing (SAST)"
msgstr ""
+msgid "StaticSiteEditor|An error occurred while submitting your changes."
+msgstr ""
+
msgid "StaticSiteEditor|Branch could not be created."
msgstr ""
@@ -19865,18 +20312,30 @@ msgstr ""
msgid "StaticSiteEditor|Could not create merge request."
msgstr ""
+msgid "StaticSiteEditor|Incompatible file content"
+msgstr ""
+
msgid "StaticSiteEditor|Return to site"
msgstr ""
+msgid "StaticSiteEditor|Static site editor"
+msgstr ""
+
msgid "StaticSiteEditor|Success!"
msgstr ""
msgid "StaticSiteEditor|Summary of changes"
msgstr ""
+msgid "StaticSiteEditor|The Static Site Editor is currently configured to only edit Markdown content on pages generated from Middleman. Visit the documentation to learn more about configuring your site to use the Static Site Editor."
+msgstr ""
+
msgid "StaticSiteEditor|Update %{sourcePath} file"
msgstr ""
+msgid "StaticSiteEditor|View documentation"
+msgstr ""
+
msgid "StaticSiteEditor|View merge request"
msgstr ""
@@ -19934,6 +20393,12 @@ msgstr ""
msgid "StatusPage|Status page"
msgstr ""
+msgid "StatusPage|Status page URL"
+msgstr ""
+
+msgid "StatusPage|Status page frontend documentation"
+msgstr ""
+
msgid "StatusPage|To publish incidents to an external status page, GitLab will store a JSON file in your Amazon S3 account in a location accessible to your external status page service. Make sure to also set up %{docsLink}"
msgstr ""
@@ -20282,6 +20747,12 @@ msgstr ""
msgid "Synced"
msgstr ""
+msgid "Synchronization disabled"
+msgstr ""
+
+msgid "Synchronization of container repositories is disabled."
+msgstr ""
+
msgid "System"
msgstr ""
@@ -20435,6 +20906,9 @@ msgstr ""
msgid "Telephone number"
msgstr ""
+msgid "Telephone number (Optional)"
+msgstr ""
+
msgid "Template"
msgstr ""
@@ -20753,6 +21227,9 @@ msgstr ""
msgid "The license was successfully uploaded and is now active. You can see the details below."
msgstr ""
+msgid "The license was successfully uploaded and will be active from %{starts_at}. You can see the details below."
+msgstr ""
+
msgid "The maximum file size allowed is %{size}."
msgstr ""
@@ -21074,6 +21551,9 @@ msgstr ""
msgid "There was an error getting the epic participants."
msgstr ""
+msgid "There was an error importing the Jira project."
+msgstr ""
+
msgid "There was an error loading users activity calendar."
msgstr ""
@@ -21089,7 +21569,7 @@ msgstr ""
msgid "There was an error resetting user pipeline minutes."
msgstr ""
-msgid "There was an error saving this Geo Node"
+msgid "There was an error saving this Geo Node."
msgstr ""
msgid "There was an error saving your changes."
@@ -21152,6 +21632,9 @@ msgstr ""
msgid "They can be managed using the %{link}."
msgstr ""
+msgid "Third Party Advisory Link"
+msgstr ""
+
msgid "Third party offers"
msgstr ""
@@ -21188,6 +21671,9 @@ msgstr ""
msgid "This application will be able to:"
msgstr ""
+msgid "This attachment has been truncated to avoid exceeding the maximum allowed attachment size of 15MB. %{written_count} of %{issues_count} issues have been included. Consider re-exporting with a narrower selection of issues."
+msgstr ""
+
msgid "This block is self-referential"
msgstr ""
@@ -21329,6 +21815,12 @@ msgstr ""
msgid "This issue is confidential"
msgstr ""
+msgid "This issue is currently blocked by the following issues: %{issues}."
+msgstr ""
+
+msgid "This issue is in a child epic of the filtered epic"
+msgstr ""
+
msgid "This issue is locked."
msgstr ""
@@ -21527,7 +22019,10 @@ msgstr ""
msgid "This user will be the author of all events in the activity feed that are the result of an update, like new branches being created or new commits being pushed to existing branches. Upon creation or when reassigning you can only assign yourself to be the mirror user."
msgstr ""
-msgid "This variable can not be masked"
+msgid "This variable can not be masked."
+msgstr ""
+
+msgid "This variable does not match the expected pattern."
msgstr ""
msgid "This will help us personalize your onboarding experience."
@@ -21557,10 +22052,10 @@ msgstr ""
msgid "ThreatMonitoring|Application firewall not detected"
msgstr ""
-msgid "ThreatMonitoring|Container Network Policy"
+msgid "ThreatMonitoring|Container Network Policies are not installed or have been disabled. To view this data, ensure your Network Policies are installed and enabled for your cluster."
msgstr ""
-msgid "ThreatMonitoring|Container NetworkPolicies are not installed or has been disabled. To view this data, ensure you NetworkPolicies are installed and enabled for your cluster."
+msgid "ThreatMonitoring|Container Network Policy"
msgstr ""
msgid "ThreatMonitoring|Container NetworkPolicies not detected"
@@ -21593,7 +22088,7 @@ msgstr ""
msgid "ThreatMonitoring|Something went wrong, unable to fetch statistics"
msgstr ""
-msgid "ThreatMonitoring|The firewall is not installed or has been disabled. To view this data, ensure you firewall is installed and enabled for your cluster."
+msgid "ThreatMonitoring|The firewall is not installed or has been disabled. To view this data, ensure the web application firewall is installed and enabled for your cluster."
msgstr ""
msgid "ThreatMonitoring|The graph below is an overview of traffic coming to your application as tracked by the Web Application Firewall (WAF). View the docs for instructions on how to access the WAF logs to see what type of malicious traffic is trying to access your app. The docs link is also accessible by clicking the \"?\" icon next to the title below."
@@ -21662,6 +22157,9 @@ msgstr ""
msgid "Time in seconds GitLab will wait for a response from the external service. When the service does not respond in time, access will be denied."
msgstr ""
+msgid "Time of import: %{importTime}"
+msgstr ""
+
msgid "Time remaining"
msgstr ""
@@ -21854,7 +22352,7 @@ msgstr ""
msgid "Title:"
msgstr ""
-msgid "Titles and Filenames"
+msgid "Titles and Descriptions"
msgstr ""
msgid "To"
@@ -22061,7 +22559,7 @@ msgstr ""
msgid "Too many projects enabled. You will need to manage them via the console or the API."
msgstr ""
-msgid "Topics"
+msgid "Topics (optional)"
msgstr ""
msgid "Total"
@@ -22223,6 +22721,9 @@ msgstr ""
msgid "Try all GitLab has to offer for 30 days."
msgstr ""
+msgid "Try changing or removing filters."
+msgstr ""
+
msgid "Try to fork again"
msgstr ""
@@ -22316,6 +22817,9 @@ msgstr ""
msgid "Unable to connect to server: %{error}"
msgstr ""
+msgid "Unable to connect to the Jira instance. Please check your Jira integration configuration."
+msgstr ""
+
msgid "Unable to convert Kubernetes logs encoding to UTF-8"
msgstr ""
@@ -23267,6 +23771,9 @@ msgstr ""
msgid "View issue"
msgstr ""
+msgid "View issues"
+msgstr ""
+
msgid "View it on GitLab"
msgstr ""
@@ -23444,6 +23951,15 @@ msgstr ""
msgid "VulnerabilityManagement|Resolved %{timeago} by %{user}"
msgstr ""
+msgid "VulnerabilityManagement|Something went wrong while trying to delete the comment. Please try again later."
+msgstr ""
+
+msgid "VulnerabilityManagement|Something went wrong while trying to retrieve the vulnerability history. Please try again later."
+msgstr ""
+
+msgid "VulnerabilityManagement|Something went wrong while trying to save the comment. Please try again later."
+msgstr ""
+
msgid "VulnerabilityManagement|Something went wrong, could not create an issue."
msgstr ""
@@ -23474,6 +23990,9 @@ msgstr ""
msgid "VulnerabilityStatusTypes|Resolved"
msgstr ""
+msgid "Vulnerability|%{scannerName} (version %{scannerVersion})"
+msgstr ""
+
msgid "Vulnerability|Class"
msgstr ""
@@ -23507,7 +24026,10 @@ msgstr ""
msgid "Vulnerability|Project"
msgstr ""
-msgid "Vulnerability|Report Type"
+msgid "Vulnerability|Scanner Provider"
+msgstr ""
+
+msgid "Vulnerability|Scanner Type"
msgstr ""
msgid "Vulnerability|Severity"
@@ -23561,9 +24083,15 @@ msgstr ""
msgid "We sent you an email with reset password instructions"
msgstr ""
+msgid "We tried to automatically renew your %{strong}%{plan_name}%{strong_close} subscription for %{strong}%{namespace_name}%{strong_close} on %{expires_on} but something went wrong so your subscription was downgraded to the free plan. Don't worry, your data is safe. We suggest you check your payment method and get in touch with our support team (%{support_link}). They'll gladly help with your subscription renewal."
+msgstr ""
+
msgid "We want to be sure it is you, please confirm you are not a robot."
msgstr ""
+msgid "We will automatically renew your %{strong}%{plan_name}%{strong_close} subscription for %{strong}%{namespace_name}%{strong_close} on %{strong}%{expires_on}%{strong_close}. There's nothing that you need to do, we'll let you know when the renewal is complete. Need more seats, a higher plan or just want to review your payment method?"
+msgstr ""
+
msgid "We've found no vulnerabilities"
msgstr ""
@@ -23672,15 +24200,6 @@ msgstr ""
msgid "When:"
msgstr ""
-msgid "While it's rare to have no vulnerabilities for your group, it can happen. In any event, we ask that you double check your settings to make sure you've set up your dashboard correctly."
-msgstr ""
-
-msgid "While it's rare to have no vulnerabilities for your pipeline, it can happen. In any event, we ask that you double check your settings to make sure all security scanning jobs have passed successfully."
-msgstr ""
-
-msgid "While it's rare to have no vulnerabilities for your project, it can happen. In any event, we ask that you double check your settings to make sure you've set up your dashboard correctly."
-msgstr ""
-
msgid "While it's rare to have no vulnerabilities, it can happen. In any event, we ask that you please double check your settings to make sure you've set up your dashboard correctly."
msgstr ""
@@ -23711,9 +24230,6 @@ msgstr ""
msgid "Wiki"
msgstr ""
-msgid "Wiki pages"
-msgstr ""
-
msgid "Wiki was successfully updated."
msgstr ""
@@ -23897,6 +24413,9 @@ msgstr ""
msgid "Yes, add it"
msgstr ""
+msgid "Yes, close issue"
+msgstr ""
+
msgid "Yes, let me map Google Code users to full names or GitLab users."
msgstr ""
@@ -24092,6 +24611,9 @@ msgstr ""
msgid "You could not create a new trigger."
msgstr ""
+msgid "You didn't renew your %{strong}%{plan_name}%{strong_close} subscription for %{strong}%{namespace_name}%{strong_close} so it was downgraded to the free plan."
+msgstr ""
+
msgid "You didn't renew your %{strong}%{plan_name}%{strong_close} subscription so it was downgraded to the GitLab Core Plan."
msgstr ""
@@ -24158,6 +24680,9 @@ msgstr ""
msgid "You have declined the invitation to join %{label}."
msgstr ""
+msgid "You have imported from this project %{numberOfPreviousImportsForProject} times before. Each new import will create duplicate issues."
+msgstr ""
+
msgid "You have no permissions"
msgstr ""
@@ -24239,6 +24764,9 @@ msgstr ""
msgid "You need to upload a Google Takeout archive."
msgstr ""
+msgid "You reached %{usage_in_percent} of %{namespace_name}'s capacity (%{used_storage} of %{storage_limit})"
+msgstr ""
+
msgid "You tried to fork %{link_to_the_project} but it failed for the following reason:"
msgstr ""
@@ -24323,9 +24851,21 @@ msgstr ""
msgid "YouTube"
msgstr ""
+msgid "Your %{strong}%{plan_name}%{strong_close} subscription for %{strong}%{namespace_name}%{strong_close} will expire on %{strong}%{expires_on}%{strong_close}. After that, you will not to be able to create issues or merge requests as well as many other features."
+msgstr ""
+
msgid "Your %{strong}%{plan_name}%{strong_close} subscription will expire on %{strong}%{expires_on}%{strong_close}. After that, you will not to be able to create issues or merge requests as well as many other features."
msgstr ""
+msgid "Your CSV export has started. It will be emailed to %{email} when complete."
+msgstr ""
+
+msgid "Your CSV export of %{issues_count} from project %{project_link} has been added to this email as an attachment."
+msgstr ""
+
+msgid "Your CSV export of %{written_count} from project %{project_name} (%{project_url}) has been added to this email as an attachment."
+msgstr ""
+
msgid "Your Commit Email will be used for web based operations, such as edits and merges."
msgstr ""
@@ -24422,6 +24962,9 @@ msgstr ""
msgid "Your comment could not be updated! Please check your network connection and try again."
msgstr ""
+msgid "Your comment will be discarded."
+msgstr ""
+
msgid "Your custom stage '%{title}' was created"
msgstr ""
@@ -24461,6 +25004,9 @@ msgstr ""
msgid "Your new personal access token has been created."
msgstr ""
+msgid "Your new project access token has been created."
+msgstr ""
+
msgid "Your password isn't required to view this page. If a password or any other personal details are requested, please contact your administrator to report abuse."
msgstr ""
@@ -24479,12 +25025,18 @@ msgstr ""
msgid "Your request for access has been queued for review."
msgstr ""
+msgid "Your search didn't match any commits."
+msgstr ""
+
msgid "Your subscription expired!"
msgstr ""
msgid "Your subscription has been downgraded"
msgstr ""
+msgid "Your subscription will automatically renew in %{remaining_days}"
+msgstr ""
+
msgid "Your subscription will expire in %{remaining_days}"
msgstr ""
@@ -24527,7 +25079,7 @@ msgstr ""
msgid "allowed to fail"
msgstr ""
-msgid "already being used for another group or project milestone."
+msgid "already being used for another group or project %{timebox_name}."
msgstr ""
msgid "already has a \"created\" issue link"
@@ -24605,6 +25157,9 @@ msgstr ""
msgid "ciReport|%{linkStartTag}Learn more about SAST %{linkEndTag}"
msgstr ""
+msgid "ciReport|%{linkStartTag}Learn more about Secret Scanning %{linkEndTag}"
+msgstr ""
+
msgid "ciReport|%{linkStartTag}Learn more about codequality reports %{linkEndTag}"
msgstr ""
@@ -24788,6 +25343,12 @@ msgstr ""
msgid "ciReport|SAST"
msgstr ""
+msgid "ciReport|Secret scanning"
+msgstr ""
+
+msgid "ciReport|Secret scanning detects secrets and credentials vulnerabilities in your source code."
+msgstr ""
+
msgid "ciReport|Security scanning"
msgstr ""
@@ -25001,6 +25562,9 @@ msgstr ""
msgid "group"
msgstr ""
+msgid "groups"
+msgstr ""
+
msgid "has already been linked to another vulnerability"
msgstr ""
@@ -25172,9 +25736,6 @@ msgstr[5] ""
msgid "merged %{time_ago}"
msgstr ""
-msgid "milestone should belong either to a project or a group."
-msgstr ""
-
msgid "missing"
msgstr ""
@@ -25511,6 +26072,9 @@ msgstr ""
msgid "no contributions"
msgstr ""
+msgid "no expiration"
+msgstr ""
+
msgid "no one can merge"
msgstr ""
@@ -25571,6 +26135,9 @@ msgstr ""
msgid "pending removal"
msgstr ""
+msgid "per day"
+msgstr ""
+
msgid "pipeline"
msgstr ""
@@ -25607,6 +26174,12 @@ msgstr[5] ""
msgid "project avatar"
msgstr ""
+msgid "projects"
+msgstr ""
+
+msgid "push to your repository, create pipelines, create issues or add comments. To reduce storage capacity, delete unused repositories, artifacts, wikis, issues, and pipelines."
+msgstr ""
+
msgid "quick actions"
msgstr ""
@@ -25757,15 +26330,6 @@ msgstr ""
msgid "this document"
msgstr ""
-msgid "thread resolved"
-msgid_plural "threads resolved"
-msgstr[0] ""
-msgstr[1] ""
-msgstr[2] ""
-msgstr[3] ""
-msgstr[4] ""
-msgstr[5] ""
-
msgid "to help your contributors communicate effectively!"
msgstr ""
@@ -25820,6 +26384,9 @@ msgstr ""
msgid "view the blob"
msgstr ""
+msgid "vulnerability|Add a comment"
+msgstr ""
+
msgid "vulnerability|Add a comment or reason for dismissal"
msgstr ""
diff --git a/locale/da_DK/gitlab.po b/locale/da_DK/gitlab.po
index 792cff19df5..65964e7b148 100644
--- a/locale/da_DK/gitlab.po
+++ b/locale/da_DK/gitlab.po
@@ -12,7 +12,7 @@ msgstr ""
"X-Crowdin-Project: gitlab-ee\n"
"X-Crowdin-Language: da\n"
"X-Crowdin-File: /master/locale/gitlab.pot\n"
-"PO-Revision-Date: 2020-04-15 00:29\n"
+"PO-Revision-Date: 2020-05-05 21:14\n"
msgid " %{start} to %{end}"
msgstr ""
@@ -212,6 +212,11 @@ msgid_plural "%d tags"
msgstr[0] ""
msgstr[1] ""
+msgid "%d unresolved thread"
+msgid_plural "%d unresolved threads"
+msgstr[0] ""
+msgstr[1] ""
+
msgid "%d vulnerability dismissed"
msgid_plural "%d vulnerabilities dismissed"
msgstr[0] ""
@@ -371,6 +376,9 @@ msgstr ""
msgid "%{mrText}, this issue will be closed automatically."
msgstr ""
+msgid "%{namespace_name} is now read-only. You cannot: %{base_message}"
+msgstr ""
+
msgid "%{name} contained %{resultsString}"
msgstr ""
@@ -383,6 +391,11 @@ msgstr ""
msgid "%{name}'s avatar"
msgstr ""
+msgid "%{no_of_days} day"
+msgid_plural "%{no_of_days} days"
+msgstr[0] ""
+msgstr[1] ""
+
msgid "%{number_commits_behind} commits behind %{default_branch}, %{number_commits_ahead} commits ahead"
msgstr ""
@@ -506,6 +519,9 @@ msgstr[1] ""
msgid "%{text} is available"
msgstr ""
+msgid "%{timebox_name} should belong either to a project or a group."
+msgstr ""
+
msgid "%{title} %{operator} %{threshold}"
msgstr ""
@@ -781,12 +797,18 @@ msgstr ""
msgid "<code>\"johnsmith@example.com\": \"johnsmith@example.com\"</code> will add \"By <a href=\"#\">johnsmith@example.com</a>\" to all issues and comments originally created by johnsmith@example.com. By default, the email address or username is masked to ensure the user's privacy. Use this option if you want to show the full email address."
msgstr ""
+msgid "<namespace / project>"
+msgstr ""
+
msgid "<no name set>"
msgstr ""
msgid "<no scopes selected>"
msgstr ""
+msgid "<project name>"
+msgstr ""
+
msgid "<strong>%{group_name}</strong> group members"
msgstr ""
@@ -820,6 +842,9 @@ msgstr ""
msgid "A basic page and serverless function that uses AWS Lambda, AWS API Gateway, and GitLab Pages"
msgstr ""
+msgid "A complete DevOps platform"
+msgstr ""
+
msgid "A default branch cannot be chosen for an empty project."
msgstr ""
@@ -1036,6 +1061,12 @@ msgstr ""
msgid "AccessTokens|reset it"
msgstr ""
+msgid "AccessibilityReport|Accessibility report artifact not found"
+msgstr ""
+
+msgid "AccessibilityReport|Failed to retrieve accessibility report"
+msgstr ""
+
msgid "AccessibilityReport|Learn More"
msgstr ""
@@ -1045,6 +1076,9 @@ msgstr ""
msgid "AccessibilityReport|New"
msgstr ""
+msgid "AccessibilityReport|The accessibility scanning found an error of the following type: %{code}"
+msgstr ""
+
msgid "Account"
msgstr ""
@@ -1287,6 +1321,9 @@ msgstr ""
msgid "Added at"
msgstr ""
+msgid "Added for this merge request"
+msgstr ""
+
msgid "Added in this version"
msgstr ""
@@ -1658,6 +1695,72 @@ msgid_plural "Alerts"
msgstr[0] ""
msgstr[1] ""
+msgid "Alert Details"
+msgstr ""
+
+msgid "AlertManagement|Acknowledged"
+msgstr ""
+
+msgid "AlertManagement|Alert"
+msgstr ""
+
+msgid "AlertManagement|Authorize external service"
+msgstr ""
+
+msgid "AlertManagement|Display alerts from all your monitoring tools directly within GitLab. Streamline the investigation of your alerts and the escalation of alerts to incidents."
+msgstr ""
+
+msgid "AlertManagement|End time"
+msgstr ""
+
+msgid "AlertManagement|End time:"
+msgstr ""
+
+msgid "AlertManagement|Events"
+msgstr ""
+
+msgid "AlertManagement|Events:"
+msgstr ""
+
+msgid "AlertManagement|Full Alert Details"
+msgstr ""
+
+msgid "AlertManagement|More information"
+msgstr ""
+
+msgid "AlertManagement|No alerts available to display. If you think you're seeing this message in error, refresh the page."
+msgstr ""
+
+msgid "AlertManagement|No alerts to display."
+msgstr ""
+
+msgid "AlertManagement|Overview"
+msgstr ""
+
+msgid "AlertManagement|Resolved"
+msgstr ""
+
+msgid "AlertManagement|Severity"
+msgstr ""
+
+msgid "AlertManagement|Start time"
+msgstr ""
+
+msgid "AlertManagement|Start time:"
+msgstr ""
+
+msgid "AlertManagement|Status"
+msgstr ""
+
+msgid "AlertManagement|Surface alerts in GitLab"
+msgstr ""
+
+msgid "AlertManagement|There was an error displaying the alerts. Confirm your endpoint's configuration details to ensure alerts appear."
+msgstr ""
+
+msgid "AlertManagement|Triggered"
+msgstr ""
+
msgid "AlertService|%{linkStart}Learn more%{linkEnd} about configuring this endpoint to receive alerts."
msgstr ""
@@ -1727,6 +1830,9 @@ msgstr ""
msgid "All security scans are enabled because %{linkStart}Auto DevOps%{linkEnd} is enabled on this project"
msgstr ""
+msgid "All threads resolved"
+msgstr ""
+
msgid "All users"
msgstr ""
@@ -1745,6 +1851,9 @@ msgstr ""
msgid "Allow only the selected protocols to be used for Git access."
msgstr ""
+msgid "Allow owners to manage default branch protection per group"
+msgstr ""
+
msgid "Allow owners to manually add users outside of LDAP"
msgstr ""
@@ -1853,6 +1962,9 @@ msgstr ""
msgid "An error occurred fetching the dropdown data."
msgstr ""
+msgid "An error occurred fetching the project authors."
+msgstr ""
+
msgid "An error occurred previewing the blob"
msgstr ""
@@ -1934,6 +2046,9 @@ msgstr ""
msgid "An error occurred while fetching sidebar data"
msgstr ""
+msgid "An error occurred while fetching terraform reports."
+msgstr ""
+
msgid "An error occurred while fetching the Service Desk address."
msgstr ""
@@ -2171,6 +2286,9 @@ msgstr ""
msgid "Any"
msgstr ""
+msgid "Any Author"
+msgstr ""
+
msgid "Any Label"
msgstr ""
@@ -2386,10 +2504,10 @@ msgstr ""
msgid "Are you sure that you want to unarchive this project?"
msgstr ""
-msgid "Are you sure you want to cancel creating this comment?"
+msgid "Are you sure you want to cancel editing this comment?"
msgstr ""
-msgid "Are you sure you want to cancel editing this comment?"
+msgid "Are you sure you want to close this blocked issue?"
msgstr ""
msgid "Are you sure you want to delete %{name}?"
@@ -2419,6 +2537,9 @@ msgstr ""
msgid "Are you sure you want to deploy this environment?"
msgstr ""
+msgid "Are you sure you want to discard this comment?"
+msgstr ""
+
msgid "Are you sure you want to erase this build?"
msgstr ""
@@ -2595,6 +2716,9 @@ msgstr ""
msgid "At least one approval from a code owner is required to change files matching the respective CODEOWNER rules."
msgstr ""
+msgid "At least one logging option is required to be enabled"
+msgstr ""
+
msgid "At least one of group_id or project_id must be specified"
msgstr ""
@@ -2633,6 +2757,27 @@ msgstr ""
msgid "AuditEvents|Target"
msgstr ""
+msgid "AuditLogs|(removed)"
+msgstr ""
+
+msgid "AuditLogs|Action"
+msgstr ""
+
+msgid "AuditLogs|Author"
+msgstr ""
+
+msgid "AuditLogs|Date"
+msgstr ""
+
+msgid "AuditLogs|IP Address"
+msgstr ""
+
+msgid "AuditLogs|Object"
+msgstr ""
+
+msgid "AuditLogs|Target"
+msgstr ""
+
msgid "Aug"
msgstr ""
@@ -2915,9 +3060,6 @@ msgstr ""
msgid "BambooService|You must set up automatic revision labeling and a repository trigger in Bamboo."
msgstr ""
-msgid "Batch operations"
-msgstr ""
-
msgid "BatchComments|Delete all pending comments"
msgstr ""
@@ -3047,6 +3189,9 @@ msgstr ""
msgid "Boards"
msgstr ""
+msgid "Boards and Board Lists"
+msgstr ""
+
msgid "Boards|Collapse"
msgstr ""
@@ -3266,6 +3411,9 @@ msgstr ""
msgid "Buy GitLab Enterprise Edition"
msgstr ""
+msgid "Buy more Pipeline minutes"
+msgstr ""
+
msgid "By %{user_name}"
msgstr ""
@@ -3386,6 +3534,9 @@ msgstr ""
msgid "Can't scan the code?"
msgstr ""
+msgid "Can't update snippet: %{err}"
+msgstr ""
+
msgid "Canary"
msgstr ""
@@ -3554,6 +3705,9 @@ msgstr ""
msgid "Changing group path can have unintended side effects."
msgstr ""
+msgid "Charts"
+msgstr ""
+
msgid "Charts can't be displayed as the request for data has timed out. %{documentationLink}"
msgstr ""
@@ -3590,9 +3744,6 @@ msgstr ""
msgid "ChatMessage|and [%{count} more](%{pipeline_failed_jobs_url})"
msgstr ""
-msgid "ChatMessage|failed"
-msgstr ""
-
msgid "ChatMessage|has failed"
msgstr ""
@@ -3608,9 +3759,6 @@ msgstr ""
msgid "ChatMessage|in %{project_link}"
msgstr ""
-msgid "ChatMessage|passed"
-msgstr ""
-
msgid "Check again"
msgstr ""
@@ -4061,9 +4209,6 @@ msgstr ""
msgid "Click any <strong>project name</strong> in the project list below to navigate to the project milestone."
msgstr ""
-msgid "Click here"
-msgstr ""
-
msgid "Click the <strong>Download</strong> button and wait for downloading to complete."
msgstr ""
@@ -4451,6 +4596,12 @@ msgstr ""
msgid "ClusterIntegration|Fetching zones"
msgstr ""
+msgid "ClusterIntegration|Fluentd"
+msgstr ""
+
+msgid "ClusterIntegration|Fluentd is an open source data collector, which lets you unify the data collection and consumption for a better use and understanding of data. It requires at least one of the following logs to be successfully installed."
+msgstr ""
+
msgid "ClusterIntegration|GitLab Integration"
msgstr ""
@@ -4757,6 +4908,15 @@ msgstr ""
msgid "ClusterIntegration|Request to begin uninstalling failed"
msgstr ""
+msgid "ClusterIntegration|SIEM Hostname"
+msgstr ""
+
+msgid "ClusterIntegration|SIEM Port"
+msgstr ""
+
+msgid "ClusterIntegration|SIEM Protocol"
+msgstr ""
+
msgid "ClusterIntegration|Save changes"
msgstr ""
@@ -4853,6 +5013,12 @@ msgstr ""
msgid "ClusterIntegration|Select zone to choose machine type"
msgstr ""
+msgid "ClusterIntegration|Send Cilium Logs"
+msgstr ""
+
+msgid "ClusterIntegration|Send ModSecurity Logs"
+msgstr ""
+
msgid "ClusterIntegration|Service Token"
msgstr ""
@@ -5281,18 +5447,33 @@ msgstr ""
msgid "Compliance framework (optional)"
msgstr ""
+msgid "ComplianceFramework|GDPR"
+msgstr ""
+
msgid "ComplianceFramework|GDPR - General Data Protection Regulation"
msgstr ""
+msgid "ComplianceFramework|HIPAA"
+msgstr ""
+
msgid "ComplianceFramework|HIPAA - Health Insurance Portability and Accountability Act"
msgstr ""
+msgid "ComplianceFramework|PCI-DSS"
+msgstr ""
+
msgid "ComplianceFramework|PCI-DSS - Payment Card Industry-Data Security Standard"
msgstr ""
+msgid "ComplianceFramework|SOC 2"
+msgstr ""
+
msgid "ComplianceFramework|SOC 2 - Service Organization Control 2"
msgstr ""
+msgid "ComplianceFramework|SOX"
+msgstr ""
+
msgid "ComplianceFramework|SOX - Sarbanes-Oxley"
msgstr ""
@@ -5440,9 +5621,15 @@ msgstr ""
msgid "Container repositories sync capacity"
msgstr ""
+msgid "ContainerRegistry| Please visit the %{linkStart}administration settings%{linkEnd} to enable this feature."
+msgstr ""
+
msgid "ContainerRegistry|%{imageName} tags"
msgstr ""
+msgid "ContainerRegistry|%{title} was successfully scheduled for deletion"
+msgstr ""
+
msgid "ContainerRegistry|Automatically remove extra images that aren't designed to be kept."
msgstr ""
@@ -5455,6 +5642,9 @@ msgstr ""
msgid "ContainerRegistry|Container Registry"
msgstr ""
+msgid "ContainerRegistry|Container Registry tag expiration and retention policy is disabled"
+msgstr ""
+
msgid "ContainerRegistry|Copy build command"
msgstr ""
@@ -5464,18 +5654,12 @@ msgstr ""
msgid "ContainerRegistry|Copy push command"
msgstr ""
-msgid "ContainerRegistry|Currently, the Container Registry tag expiration feature is not available for projects created before GitLab version 12.8. For updates and more information, visit Issue %{linkStart}#196124%{linkEnd}"
-msgstr ""
-
msgid "ContainerRegistry|Docker connection error"
msgstr ""
msgid "ContainerRegistry|Docker tag expiration policy is %{toggleStatus}"
msgstr ""
-msgid "ContainerRegistry|Docker tags with names matching this regex pattern will expire:"
-msgstr ""
-
msgid "ContainerRegistry|Edit Settings"
msgstr ""
@@ -5497,9 +5681,6 @@ msgstr ""
msgid "ContainerRegistry|Image ID"
msgstr ""
-msgid "ContainerRegistry|Image deleted successfully"
-msgstr ""
-
msgid "ContainerRegistry|Keep and protect the images that matter most."
msgstr ""
@@ -5515,12 +5696,24 @@ msgstr ""
msgid "ContainerRegistry|Number of tags to retain:"
msgstr ""
+msgid "ContainerRegistry|Please contact your administrator."
+msgstr ""
+
msgid "ContainerRegistry|Push an image"
msgstr ""
msgid "ContainerRegistry|Quick Start"
msgstr ""
+msgid "ContainerRegistry|Regular expressions such as %{codeStart}.*-test%{codeEnd} or %{codeStart}dev-.*%{codeEnd} are supported"
+msgstr ""
+
+msgid "ContainerRegistry|Regular expressions such as %{codeStart}.*-test%{codeEnd} or %{codeStart}dev-.*%{codeEnd} are supported. To select all tags, use %{codeStart}.*%{codeEnd}"
+msgstr ""
+
+msgid "ContainerRegistry|Remember to run %{docLinkStart}garbage collection%{docLinkEnd} to remove the stale data from storage."
+msgstr ""
+
msgid "ContainerRegistry|Remove repository"
msgstr ""
@@ -5535,22 +5728,22 @@ msgstr[1] ""
msgid "ContainerRegistry|Retention policy has been Enabled"
msgstr ""
-msgid "ContainerRegistry|Something went wrong while deleting the image."
+msgid "ContainerRegistry|Something went wrong while fetching the expiration policy."
msgstr ""
-msgid "ContainerRegistry|Something went wrong while deleting the tag."
+msgid "ContainerRegistry|Something went wrong while fetching the repository list."
msgstr ""
-msgid "ContainerRegistry|Something went wrong while deleting the tags."
+msgid "ContainerRegistry|Something went wrong while fetching the tags list."
msgstr ""
-msgid "ContainerRegistry|Something went wrong while fetching the expiration policy."
+msgid "ContainerRegistry|Something went wrong while marking the tag for deletion."
msgstr ""
-msgid "ContainerRegistry|Something went wrong while fetching the packages list."
+msgid "ContainerRegistry|Something went wrong while marking the tags for deletion."
msgstr ""
-msgid "ContainerRegistry|Something went wrong while fetching the tags list."
+msgid "ContainerRegistry|Something went wrong while scheduling %{title} for deletion. Please try again."
msgstr ""
msgid "ContainerRegistry|Something went wrong while updating the expiration policy."
@@ -5559,16 +5752,25 @@ msgstr ""
msgid "ContainerRegistry|Tag"
msgstr ""
-msgid "ContainerRegistry|Tag deleted successfully"
-msgstr ""
-
msgid "ContainerRegistry|Tag expiration policy"
msgstr ""
msgid "ContainerRegistry|Tag expiration policy is designed to:"
msgstr ""
-msgid "ContainerRegistry|Tags deleted successfully"
+msgid "ContainerRegistry|Tag successfully marked for deletion."
+msgstr ""
+
+msgid "ContainerRegistry|Tags successfully marked for deletion."
+msgstr ""
+
+msgid "ContainerRegistry|Tags with names matching this regex pattern will %{italicStart}be preserved:%{italicEnd}"
+msgstr ""
+
+msgid "ContainerRegistry|Tags with names matching this regex pattern will %{italicStart}expire:%{italicEnd}"
+msgstr ""
+
+msgid "ContainerRegistry|The Container Registry tag expiration and retention policies for this project have not been enabled."
msgstr ""
msgid "ContainerRegistry|The last tag related to this image was recently removed. This empty image and any associated data will be automatically removed as part of the regular Garbage Collection process. If you have any questions, contact your administrator."
@@ -5589,16 +5791,16 @@ msgstr ""
msgid "ContainerRegistry|There are no container images stored for this project"
msgstr ""
-msgid "ContainerRegistry|This Registry contains deleted image tag data. Remember to run %{docLinkStart}garbage collection%{docLinkEnd} to remove the stale data from storage."
+msgid "ContainerRegistry|There was an error during the deletion of this image repository, please try again."
msgstr ""
msgid "ContainerRegistry|This image has no active tags"
msgstr ""
-msgid "ContainerRegistry|We are having trouble connecting to Docker, which could be due to an issue with your project name or path. %{docLinkStart}More Information%{docLinkEnd}"
+msgid "ContainerRegistry|This image repository is scheduled for deletion"
msgstr ""
-msgid "ContainerRegistry|Wildcards such as %{codeStart}.*-stable%{codeEnd} or %{codeStart}production/.*%{codeEnd} are supported. To select all tags, use %{codeStart}.*%{codeEnd}"
+msgid "ContainerRegistry|We are having trouble connecting to Docker, which could be due to an issue with your project name or path. %{docLinkStart}More Information%{docLinkEnd}"
msgstr ""
msgid "ContainerRegistry|With the Container Registry, every project can have its own space to store its Docker images. %{docLinkStart}More Information%{docLinkEnd}"
@@ -5853,7 +6055,7 @@ msgstr ""
msgid "Could not delete chat nickname %{chat_name}."
msgstr ""
-msgid "Could not find design"
+msgid "Could not find design."
msgstr ""
msgid "Could not remove the trigger."
@@ -5868,6 +6070,9 @@ msgstr ""
msgid "Could not revoke personal access token %{personal_access_token_name}."
msgstr ""
+msgid "Could not revoke project access token %{project_access_token_name}."
+msgstr ""
+
msgid "Could not save group ID"
msgstr ""
@@ -5925,6 +6130,9 @@ msgstr ""
msgid "Create a new branch"
msgstr ""
+msgid "Create a new deploy key for this project"
+msgstr ""
+
msgid "Create a new file as there are no files yet. Afterwards, you'll be able to commit your changes."
msgstr ""
@@ -6030,6 +6238,9 @@ msgstr ""
msgid "Create requirement"
msgstr ""
+msgid "Create snippet"
+msgstr ""
+
msgid "Create wildcard: %{searchTerm}"
msgstr ""
@@ -6051,6 +6262,9 @@ msgstr ""
msgid "Created"
msgstr ""
+msgid "Created %{timestamp}"
+msgstr ""
+
msgid "Created At"
msgstr ""
@@ -6407,6 +6621,9 @@ msgstr ""
msgid "CycleAnalytics|stage dropdown"
msgstr ""
+msgid "DAG"
+msgstr ""
+
msgid "DNS"
msgstr ""
@@ -6773,6 +6990,9 @@ msgstr ""
msgid "Deploy key was successfully updated."
msgstr ""
+msgid "Deploy keys allow read-only or read-write (if enabled) access to your repository. Deploy keys can be used for CI, staging or production servers. You can create a deploy key or add an existing one."
+msgstr ""
+
msgid "Deploy progress not found. To see pods, ensure your environment matches %{linkStart}deploy board criteria%{linkEnd}."
msgstr ""
@@ -6926,6 +7146,9 @@ msgstr ""
msgid "Deploying to"
msgstr ""
+msgid "Deployment Frequency"
+msgstr ""
+
msgid "Deployment|API"
msgstr ""
@@ -7444,9 +7667,6 @@ msgstr ""
msgid "Edit environment"
msgstr ""
-msgid "Edit epic description"
-msgstr ""
-
msgid "Edit file"
msgstr ""
@@ -8068,9 +8288,6 @@ msgstr ""
msgid "Epics"
msgstr ""
-msgid "Epics (Ultimate / Gold license only)"
-msgstr ""
-
msgid "Epics Roadmap"
msgstr ""
@@ -8080,6 +8297,9 @@ msgstr ""
msgid "Epics let you manage your portfolio of projects more efficiently and with less effort"
msgstr ""
+msgid "Epics, Issues, and Merge Requests"
+msgstr ""
+
msgid "Epics|Add an epic"
msgstr ""
@@ -8131,6 +8351,9 @@ msgstr ""
msgid "Epics|Something went wrong while fetching group epics."
msgstr ""
+msgid "Epics|Something went wrong while moving item."
+msgstr ""
+
msgid "Epics|Something went wrong while ordering item."
msgstr ""
@@ -8185,6 +8408,9 @@ msgstr ""
msgid "Error fetching network graph."
msgstr ""
+msgid "Error fetching payload data."
+msgstr ""
+
msgid "Error fetching projects"
msgstr ""
@@ -8194,9 +8420,6 @@ msgstr ""
msgid "Error fetching the dependency list. Please check your network connection and try again."
msgstr ""
-msgid "Error fetching usage ping data."
-msgstr ""
-
msgid "Error loading branch data. Please try again."
msgstr ""
@@ -8338,6 +8561,9 @@ msgstr ""
msgid "Errors"
msgstr ""
+msgid "Errors:"
+msgstr ""
+
msgid "Estimated"
msgstr ""
@@ -8542,12 +8768,18 @@ msgstr ""
msgid "Export as CSV"
msgstr ""
+msgid "Export group"
+msgstr ""
+
msgid "Export issues"
msgstr ""
msgid "Export project"
msgstr ""
+msgid "Export this group with all related data to a new GitLab instance. Once complete, you can import the data file from the \"New Group\" page."
+msgstr ""
+
msgid "Export this project with all its related data in order to move your project to a new GitLab instance. Once the export is finished, you can import the file from the \"New Project\" page."
msgstr ""
@@ -8617,9 +8849,6 @@ msgstr ""
msgid "Failed Jobs"
msgstr ""
-msgid "Failed create wiki"
-msgstr ""
-
msgid "Failed to add a Zoom meeting"
msgstr ""
@@ -8659,6 +8888,9 @@ msgstr ""
msgid "Failed to create resources"
msgstr ""
+msgid "Failed to create wiki"
+msgstr ""
+
msgid "Failed to delete board. Please try again."
msgstr ""
@@ -8668,7 +8900,7 @@ msgstr ""
msgid "Failed to enqueue the rebase operation, possibly due to a long-lived transaction. Try again later."
msgstr ""
-msgid "Failed to find import label for jira import."
+msgid "Failed to find import label for Jira import."
msgstr ""
msgid "Failed to get ref."
@@ -8692,6 +8924,12 @@ msgstr ""
msgid "Failed to load groups & users."
msgstr ""
+msgid "Failed to load labels. Please try again."
+msgstr ""
+
+msgid "Failed to load milestones. Please try again."
+msgstr ""
+
msgid "Failed to load related branches"
msgstr ""
@@ -9067,7 +9305,10 @@ msgstr ""
msgid "Filter by milestone name"
msgstr ""
-msgid "Filter by name..."
+msgid "Filter by name"
+msgstr ""
+
+msgid "Filter by status"
msgstr ""
msgid "Filter by two-factor authentication"
@@ -9079,12 +9320,18 @@ msgstr ""
msgid "Filter projects"
msgstr ""
+msgid "Filter results"
+msgstr ""
+
msgid "Filter results by group"
msgstr ""
msgid "Filter results by project"
msgstr ""
+msgid "Filter results..."
+msgstr ""
+
msgid "Filter your projects by name"
msgstr ""
@@ -9373,10 +9620,10 @@ msgstr ""
msgid "GeoNodes|Checksummed"
msgstr ""
-msgid "GeoNodes|Container repositories"
+msgid "GeoNodes|Consult Geo troubleshooting information"
msgstr ""
-msgid "GeoNodes|Data is out of date from %{timeago}"
+msgid "GeoNodes|Container repositories"
msgstr ""
msgid "GeoNodes|Data replication lag"
@@ -9418,6 +9665,9 @@ msgstr ""
msgid "GeoNodes|Last event ID seen from primary"
msgstr ""
+msgid "GeoNodes|Learn more about Geo node statuses"
+msgstr ""
+
msgid "GeoNodes|Loading nodes"
msgstr ""
@@ -9433,6 +9683,9 @@ msgstr ""
msgid "GeoNodes|Node was successfully removed."
msgstr ""
+msgid "GeoNodes|Node's status was updated %{timeAgo}."
+msgstr ""
+
msgid "GeoNodes|Not checksummed"
msgstr ""
@@ -9463,7 +9716,10 @@ msgstr ""
msgid "GeoNodes|Repository verification progress"
msgstr ""
-msgid "GeoNodes|Selective"
+msgid "GeoNodes|Selective (%{syncLabel})"
+msgstr ""
+
+msgid "GeoNodes|Selective synchronization"
msgstr ""
msgid "GeoNodes|Something went wrong while changing node status"
@@ -9490,6 +9746,9 @@ msgstr ""
msgid "GeoNodes|Unverified"
msgstr ""
+msgid "GeoNodes|Updated %{timeAgo}"
+msgstr ""
+
msgid "GeoNodes|Used slots"
msgstr ""
@@ -9529,16 +9788,16 @@ msgstr ""
msgid "Geo|All"
msgstr ""
-msgid "Geo|All projects"
+msgid "Geo|All %{replicable_type}"
msgstr ""
-msgid "Geo|All projects are being scheduled for re-sync"
+msgid "Geo|All projects"
msgstr ""
-msgid "Geo|All projects are being scheduled for re-verify"
+msgid "Geo|All projects are being scheduled for resync"
msgstr ""
-msgid "Geo|Batch operations"
+msgid "Geo|All projects are being scheduled for reverify"
msgstr ""
msgid "Geo|Could not remove tracking entry for an existing project."
@@ -9550,9 +9809,15 @@ msgstr ""
msgid "Geo|Failed"
msgstr ""
+msgid "Geo|Filter by status"
+msgstr ""
+
msgid "Geo|Geo Status"
msgstr ""
+msgid "Geo|In progress"
+msgstr ""
+
msgid "Geo|In sync"
msgstr ""
@@ -9577,9 +9842,6 @@ msgstr ""
msgid "Geo|Not synced yet"
msgstr ""
-msgid "Geo|Pending"
-msgstr ""
-
msgid "Geo|Pending synchronization"
msgstr ""
@@ -9616,7 +9878,7 @@ msgstr ""
msgid "Geo|Resync"
msgstr ""
-msgid "Geo|Resync all projects"
+msgid "Geo|Resync all"
msgstr ""
msgid "Geo|Retry count"
@@ -9625,7 +9887,7 @@ msgstr ""
msgid "Geo|Reverify"
msgstr ""
-msgid "Geo|Reverify all projects"
+msgid "Geo|Reverify all"
msgstr ""
msgid "Geo|Status"
@@ -9967,9 +10229,6 @@ msgstr ""
msgid "Go to file"
msgstr ""
-msgid "Go to file (MRs only)"
-msgstr ""
-
msgid "Go to file permalink (while viewing a file)"
msgstr ""
@@ -10414,6 +10673,9 @@ msgstr ""
msgid "GroupSettings|Disable group mentions"
msgstr ""
+msgid "GroupSettings|Export group"
+msgstr ""
+
msgid "GroupSettings|If the parent group's visibility is lower than the group current visibility, visibility levels for subgroups and projects will be changed to match the new parent group's visibility."
msgstr ""
@@ -10492,6 +10754,9 @@ msgstr ""
msgid "Groups (%{count})"
msgstr ""
+msgid "Groups (%{groups})"
+msgstr ""
+
msgid "Groups can also be nested by creating %{subgroup_docs_link_start}subgroups%{subgroup_docs_link_end}."
msgstr ""
@@ -10757,15 +11022,12 @@ msgstr ""
msgid "ID:"
msgstr ""
-msgid "IDE|Allow live previews of JavaScript projects in the Web IDE using CodeSandbox client side evaluation."
+msgid "IDE|Allow live previews of JavaScript projects in the Web IDE using CodeSandbox Live Preview."
msgstr ""
msgid "IDE|Back"
msgstr ""
-msgid "IDE|Client side evaluation"
-msgstr ""
-
msgid "IDE|Commit"
msgstr ""
@@ -10829,6 +11091,9 @@ msgstr ""
msgid "If any job surpasses this timeout threshold, it will be marked as failed. Human readable time input language is accepted like \"1 hour\". Values without specification represent seconds."
msgstr ""
+msgid "If blank, set allowable lifetime to %{instance_level_policy_in_words}, as defined by the instance admin. Once set, existing tokens for users in this group may be revoked."
+msgstr ""
+
msgid "If checked, group owners can manage LDAP group links and LDAP member overrides"
msgstr ""
@@ -10868,6 +11133,9 @@ msgstr ""
msgid "If you lose your recovery codes you can generate new ones, invalidating all previous codes."
msgstr ""
+msgid "If you reach 100%% storage capacity, you will not be able to: %{base_message}"
+msgstr ""
+
msgid "If your HTTP repository is not publicly accessible, add your credentials."
msgstr ""
@@ -10979,6 +11247,9 @@ msgstr ""
msgid "Import repository"
msgstr ""
+msgid "Import started by: %{importInitiator}"
+msgstr ""
+
msgid "Import tasks"
msgstr ""
@@ -11045,6 +11316,9 @@ msgstr ""
msgid "In order to tailor your experience with GitLab we<br>would like to know a bit more about you."
msgstr ""
+msgid "In progress"
+msgstr ""
+
msgid "In the next step, you'll be able to select the projects you want to import."
msgstr ""
@@ -11392,15 +11666,15 @@ msgstr ""
msgid "Issues"
msgstr ""
-msgid "Issues / Merge Requests"
-msgstr ""
-
msgid "Issues Analytics"
msgstr ""
msgid "Issues Rate Limits"
msgstr ""
+msgid "Issues and Merge Requests"
+msgstr ""
+
msgid "Issues can be bugs, tasks or ideas to be discussed. Also, issues are searchable and filterable."
msgstr ""
@@ -11422,10 +11696,10 @@ msgstr ""
msgid "IssuesAnalytics|Avg/Month:"
msgstr ""
-msgid "IssuesAnalytics|Issues created"
+msgid "IssuesAnalytics|Issues opened"
msgstr ""
-msgid "IssuesAnalytics|Issues created per month"
+msgid "IssuesAnalytics|Issues opened per month"
msgstr ""
msgid "IssuesAnalytics|Last 12 months"
@@ -11479,6 +11753,9 @@ msgstr ""
msgid "Jira integration not configured."
msgstr ""
+msgid "Jira project: %{importProject}"
+msgstr ""
+
msgid "JiraService|Events for %{noteable_model_name} are disabled."
msgstr ""
@@ -11868,6 +12145,9 @@ msgstr ""
msgid "Lead"
msgstr ""
+msgid "Lead Time"
+msgstr ""
+
msgid "Learn GitLab"
msgstr ""
@@ -11880,6 +12160,9 @@ msgstr ""
msgid "Learn how to %{no_packages_link_start}publish and share your packages%{no_packages_link_end} with GitLab."
msgstr ""
+msgid "Learn how to enable synchronization"
+msgstr ""
+
msgid "Learn more"
msgstr ""
@@ -12211,9 +12494,6 @@ msgstr ""
msgid "Live preview"
msgstr ""
-msgid "Load more vulnerabilities"
-msgstr ""
-
msgid "Loading"
msgstr ""
@@ -12364,9 +12644,6 @@ msgstr ""
msgid "Manage"
msgstr ""
-msgid "Manage Git repositories with fine-grained access controls that keep your code secure. Perform code reviews and enhance collaboration with merge requests. Each project can also have an issue tracker and a wiki."
-msgstr ""
-
msgid "Manage Web IDE features"
msgstr ""
@@ -12397,6 +12674,9 @@ msgstr ""
msgid "Manage your license"
msgstr ""
+msgid "Managed Account"
+msgstr ""
+
msgid "Manifest"
msgstr ""
@@ -12661,6 +12941,9 @@ msgstr ""
msgid "Merge Requests in Review"
msgstr ""
+msgid "Merge automatically (%{strategy})"
+msgstr ""
+
msgid "Merge commit message"
msgstr ""
@@ -12814,6 +13097,12 @@ msgstr ""
msgid "Merged branches are being deleted. This can take some time depending on the number of branches. Please refresh the page to see changes."
msgstr ""
+msgid "Merged this merge request."
+msgstr ""
+
+msgid "Merges this merge request immediately."
+msgstr ""
+
msgid "Merges this merge request when the pipeline succeeds."
msgstr ""
@@ -12921,12 +13210,15 @@ msgid_plural "Metrics|Edit metrics"
msgstr[0] ""
msgstr[1] ""
-msgid "Metrics|Environment"
+msgid "Metrics|Expand panel"
msgstr ""
msgid "Metrics|For grouping similar metrics"
msgstr ""
+msgid "Metrics|Go back (Esc)"
+msgstr ""
+
msgid "Metrics|Invalid time range, please verify."
msgstr ""
@@ -12957,9 +13249,6 @@ msgstr ""
msgid "Metrics|Refresh dashboard"
msgstr ""
-msgid "Metrics|Show last"
-msgstr ""
-
msgid "Metrics|There was an error creating the dashboard."
msgstr ""
@@ -13038,6 +13327,9 @@ msgstr ""
msgid "Microsoft Azure"
msgstr ""
+msgid "Middleman project with Static Site Editor support"
+msgstr ""
+
msgid "Migrated %{success_count}/%{total_count} files."
msgstr ""
@@ -13483,10 +13775,10 @@ msgstr ""
msgid "Next"
msgstr ""
-msgid "Next file in diff (MRs only)"
+msgid "Next file in diff"
msgstr ""
-msgid "Next unresolved discussion (MRs only)"
+msgid "Next unresolved discussion"
msgstr ""
msgid "Nickname"
@@ -13546,6 +13838,9 @@ msgstr ""
msgid "No changes between %{ref_start}%{source_branch}%{ref_end} and %{ref_start}%{target_branch}%{ref_end}"
msgstr ""
+msgid "No child epics match applied filters"
+msgstr ""
+
msgid "No connection could be made to a Gitaly Server, please check your logs!"
msgstr ""
@@ -13684,15 +13979,6 @@ msgstr ""
msgid "No thanks, don't show this again"
msgstr ""
-msgid "No vulnerabilities found for this group"
-msgstr ""
-
-msgid "No vulnerabilities found for this pipeline"
-msgstr ""
-
-msgid "No vulnerabilities found for this project"
-msgstr ""
-
msgid "No vulnerabilities present"
msgstr ""
@@ -13816,6 +14102,9 @@ msgstr ""
msgid "Nothing to preview."
msgstr ""
+msgid "Nothing to synchronize"
+msgstr ""
+
msgid "Notification events"
msgstr ""
@@ -13987,6 +14276,9 @@ msgstr ""
msgid "Once removed, the fork relationship cannot be restored and you will no longer be able to send merge requests to the source."
msgstr ""
+msgid "Once the exported file is ready you can download it from this page."
+msgstr ""
+
msgid "Once the exported file is ready, you will receive a notification email with a download link, or you can download it from this page."
msgstr ""
@@ -14088,9 +14380,6 @@ msgstr ""
msgid "Open sidebar"
msgstr ""
-msgid "Open source software to collaborate on code"
-msgstr ""
-
msgid "Open: %{openIssuesCount}"
msgstr ""
@@ -14193,6 +14482,9 @@ msgstr ""
msgid "OutdatedBrowser|You can provide feedback %{feedback_link_start}on this issue%{feedback_link_end} or via your usual support channels."
msgstr ""
+msgid "Overridden"
+msgstr ""
+
msgid "Overview"
msgstr ""
@@ -14244,6 +14536,9 @@ msgstr ""
msgid "PackageRegistry|Conan Command"
msgstr ""
+msgid "PackageRegistry|Copy .pypirc content"
+msgstr ""
+
msgid "PackageRegistry|Copy Conan Command"
msgstr ""
@@ -14265,6 +14560,9 @@ msgstr ""
msgid "PackageRegistry|Copy NuGet Setup Command"
msgstr ""
+msgid "PackageRegistry|Copy Pip command"
+msgstr ""
+
msgid "PackageRegistry|Copy and paste this inside your %{codeStart}pom.xml%{codeEnd} %{codeStart}dependencies%{codeEnd} block."
msgstr ""
@@ -14298,6 +14596,12 @@ msgstr ""
msgid "PackageRegistry|For more information on the NuGet registry, %{linkStart}see the documentation%{linkEnd}."
msgstr ""
+msgid "PackageRegistry|For more information on the PyPi registry, %{linkStart}see the documentation%{linkEnd}."
+msgstr ""
+
+msgid "PackageRegistry|If you haven't already done so, you will need to add the below to your %{codeStart}.pypirc%{codeEnd} file."
+msgstr ""
+
msgid "PackageRegistry|If you haven't already done so, you will need to add the below to your %{codeStart}pom.xml%{codeEnd} file."
msgstr ""
@@ -14307,6 +14611,9 @@ msgstr ""
msgid "PackageRegistry|Learn how to %{noPackagesLinkStart}publish and share your packages%{noPackagesLinkEnd} with GitLab."
msgstr ""
+msgid "PackageRegistry|Manually Published"
+msgstr ""
+
msgid "PackageRegistry|Maven"
msgstr ""
@@ -14325,12 +14632,18 @@ msgstr ""
msgid "PackageRegistry|NuGet Command"
msgstr ""
+msgid "PackageRegistry|Pip Command"
+msgstr ""
+
msgid "PackageRegistry|Pipeline %{linkStart}%{linkEnd} triggered %{timestamp} by %{author}"
msgstr ""
msgid "PackageRegistry|Published to the repository at %{timestamp}"
msgstr ""
+msgid "PackageRegistry|PyPi"
+msgstr ""
+
msgid "PackageRegistry|Registry Setup"
msgstr ""
@@ -14367,6 +14680,9 @@ msgstr ""
msgid "PackageRegistry|npm"
msgstr ""
+msgid "PackageRegistry|published by %{author}"
+msgstr ""
+
msgid "PackageRegistry|yarn"
msgstr ""
@@ -14382,6 +14698,9 @@ msgstr ""
msgid "PackageType|NuGet"
msgstr ""
+msgid "PackageType|PyPi"
+msgstr ""
+
msgid "Packages"
msgstr ""
@@ -14475,6 +14794,9 @@ msgstr ""
msgid "Past due"
msgstr ""
+msgid "Paste a machine public key here. Read more about how to generate it"
+msgstr ""
+
msgid "Paste a machine public key here. Read more about how to generate it %{link_start}here%{link_end}"
msgstr ""
@@ -14490,9 +14812,6 @@ msgstr ""
msgid "Path"
msgstr ""
-msgid "Path, transfer, remove"
-msgstr ""
-
msgid "Path:"
msgstr ""
@@ -14523,7 +14842,7 @@ msgstr ""
msgid "Percentage"
msgstr ""
-msgid "Perform advanced options such as changing path, transferring, or removing the group."
+msgid "Perform advanced options such as changing path, transferring, exporting, or removing the group."
msgstr ""
msgid "Perform common operations on GitLab project"
@@ -14604,9 +14923,6 @@ msgstr ""
msgid "Pipeline minutes quota"
msgstr ""
-msgid "Pipeline quota"
-msgstr ""
-
msgid "Pipeline subscriptions"
msgstr ""
@@ -14616,6 +14932,9 @@ msgstr ""
msgid "Pipeline: %{status}"
msgstr ""
+msgid "PipelineCharts|CI / CD Analytics"
+msgstr ""
+
msgid "PipelineCharts|Failed:"
msgstr ""
@@ -14694,15 +15013,6 @@ msgstr ""
msgid "Pipelines settings for '%{project_name}' were successfully updated."
msgstr ""
-msgid "Pipelines| to purchase more minutes."
-msgstr ""
-
-msgid "Pipelines|%{namespace_name} has exceeded its pipeline minutes quota."
-msgstr ""
-
-msgid "Pipelines|%{namespace_name} has less than %{notification_level}%% of CI minutes available."
-msgstr ""
-
msgid "Pipelines|API"
msgstr ""
@@ -14724,10 +15034,13 @@ msgstr ""
msgid "Pipelines|Get started with Pipelines"
msgstr ""
-msgid "Pipelines|Loading Pipelines"
+msgid "Pipelines|Group %{namespace_name} has %{percentage}%% or less Shared Runner Pipeline minutes remaining. Once it runs out, no new jobs or pipelines in its projects will run."
msgstr ""
-msgid "Pipelines|Pipelines will not run anymore on shared Runners."
+msgid "Pipelines|Group %{namespace_name} has exceeded its pipeline minutes quota. Unless you buy additional pipeline minutes, no new jobs or pipelines in its projects will run."
+msgstr ""
+
+msgid "Pipelines|Loading Pipelines"
msgstr ""
msgid "Pipelines|Project cache successfully reset."
@@ -14958,6 +15271,9 @@ msgstr ""
msgid "Please select"
msgstr ""
+msgid "Please select a Jira project"
+msgstr ""
+
msgid "Please select a country"
msgstr ""
@@ -15003,6 +15319,9 @@ msgstr ""
msgid "Please wait while we import the repository for you. Refresh at will."
msgstr ""
+msgid "Plugins directory is deprecated and will be removed in 14.0. Please move this file into /file_hooks directory."
+msgstr ""
+
msgid "Pod does not exist"
msgstr ""
@@ -15120,7 +15439,7 @@ msgstr ""
msgid "Prevent users from changing their profile name"
msgstr ""
-msgid "Prevent users from modifing merge request approvers list"
+msgid "Prevent users from modifying merge request approvers list"
msgstr ""
msgid "Prevent users from performing write operations on GitLab while performing maintenance."
@@ -15141,10 +15460,10 @@ msgstr ""
msgid "Previous Artifacts"
msgstr ""
-msgid "Previous file in diff (MRs only)"
+msgid "Previous file in diff"
msgstr ""
-msgid "Previous unresolved discussion (MRs only)"
+msgid "Previous unresolved discussion"
msgstr ""
msgid "Primary"
@@ -15177,6 +15496,9 @@ msgstr ""
msgid "Private profile"
msgstr ""
+msgid "Private projects Minutes cost factor"
+msgstr ""
+
msgid "Private projects can be created in your personal namespace with:"
msgstr ""
@@ -16005,6 +16327,9 @@ msgstr ""
msgid "ProjectSettings|With GitLab Pages you can host your static websites on GitLab"
msgstr ""
+msgid "ProjectSettings|With Metrics Dashboard you can visualize this project performance metrics"
+msgstr ""
+
msgid "ProjectTemplates|.NET Core"
msgstr ""
@@ -16068,6 +16393,9 @@ msgstr ""
msgid "ProjectTemplates|Spring"
msgstr ""
+msgid "ProjectTemplates|Static Site Editor/Middleman"
+msgstr ""
+
msgid "ProjectTemplates|iOS (Swift)"
msgstr ""
@@ -16287,13 +16615,13 @@ msgstr ""
msgid "PrometheusService|Prometheus is being automatically managed on your clusters"
msgstr ""
-msgid "PrometheusService|These metrics will only be monitored after your first deployment to an environment"
+msgid "PrometheusService|Select the Active checkbox to override the Auto Configuration with custom settings. If unchecked, Auto Configuration settings are used."
msgstr ""
-msgid "PrometheusService|Time-series monitoring service"
+msgid "PrometheusService|These metrics will only be monitored after your first deployment to an environment"
msgstr ""
-msgid "PrometheusService|To enable manual configuration, uninstall Prometheus from your clusters"
+msgid "PrometheusService|Time-series monitoring service"
msgstr ""
msgid "PrometheusService|To enable the installation of Prometheus on your clusters, deactivate the manual configuration below"
@@ -16479,6 +16807,9 @@ msgstr ""
msgid "Protip:"
msgstr ""
+msgid "Protocol"
+msgstr ""
+
msgid "Provider"
msgstr ""
@@ -16503,6 +16834,9 @@ msgstr ""
msgid "Public pipelines"
msgstr ""
+msgid "Public projects Minutes cost factor"
+msgstr ""
+
msgid "Pull"
msgstr ""
@@ -16703,6 +17037,9 @@ msgid_plural "Refreshing in %d seconds to show the updated status..."
msgstr[0] ""
msgstr[1] ""
+msgid "Regenerate export"
+msgstr ""
+
msgid "Regenerate instance ID"
msgstr ""
@@ -17080,9 +17417,29 @@ msgstr ""
msgid "Reports|%{combinedString} and %{resolvedString}"
msgstr ""
+msgid "Reports|Accessibility scanning detected %d issue for the source branch only"
+msgid_plural "Reports|Accessibility scanning detected %d issues for the source branch only"
+msgstr[0] ""
+msgstr[1] ""
+
+msgid "Reports|Accessibility scanning detected no issues for the source branch only"
+msgstr ""
+
+msgid "Reports|Accessibility scanning failed loading results"
+msgstr ""
+
+msgid "Reports|Accessibility scanning results are being parsed"
+msgstr ""
+
msgid "Reports|Actions"
msgstr ""
+msgid "Reports|An error occured while loading report"
+msgstr ""
+
+msgid "Reports|An error occurred while loading %{name} results"
+msgstr ""
+
msgid "Reports|Class"
msgstr ""
@@ -17188,7 +17545,7 @@ msgstr ""
msgid "Requested %{time_ago}"
msgstr ""
-msgid "Requested design version does not exist"
+msgid "Requested design version does not exist."
msgstr ""
msgid "Requested states are invalid"
@@ -17372,6 +17729,9 @@ msgstr ""
msgid "Resync"
msgstr ""
+msgid "Resync all"
+msgstr ""
+
msgid "Resync all %{replicableType}"
msgstr ""
@@ -17440,6 +17800,9 @@ msgstr ""
msgid "Revoked personal access token %{personal_access_token_name}!"
msgstr ""
+msgid "Revoked project access token %{project_access_token_name}!"
+msgstr ""
+
msgid "RightSidebar|adding a"
msgstr ""
@@ -17488,6 +17851,9 @@ msgstr ""
msgid "Runner token"
msgstr ""
+msgid "Runner tokens"
+msgstr ""
+
msgid "Runner was not updated."
msgstr ""
@@ -17542,6 +17908,9 @@ msgstr ""
msgid "SAML SSO for %{group_name}"
msgstr ""
+msgid "SAML discovery tokens"
+msgstr ""
+
msgid "SAML for %{group_name}"
msgstr ""
@@ -17626,12 +17995,18 @@ msgstr ""
msgid "Scheduled"
msgstr ""
+msgid "Scheduled to merge this merge request (%{strategy})."
+msgstr ""
+
msgid "Scheduled to merge this merge request when the pipeline succeeds."
msgstr ""
msgid "Schedules"
msgstr ""
+msgid "Schedules to merge this merge request (%{strategy})."
+msgstr ""
+
msgid "Scheduling"
msgstr ""
@@ -17689,6 +18064,9 @@ msgstr ""
msgid "Search branches and tags"
msgstr ""
+msgid "Search by author"
+msgstr ""
+
msgid "Search files"
msgstr ""
@@ -17834,11 +18212,6 @@ msgid_plural "SearchResults|snippets"
msgstr[0] ""
msgstr[1] ""
-msgid "SearchResults|snippet result"
-msgid_plural "SearchResults|snippet results"
-msgstr[0] ""
-msgstr[1] ""
-
msgid "SearchResults|user"
msgid_plural "SearchResults|users"
msgstr[0] ""
@@ -17849,6 +18222,9 @@ msgid_plural "SearchResults|wiki results"
msgstr[0] ""
msgstr[1] ""
+msgid "Searching by both author and message is currently not supported."
+msgstr ""
+
msgid "Seat Link"
msgstr ""
@@ -17876,184 +18252,232 @@ msgstr ""
msgid "Security Dashboard"
msgstr ""
-msgid "Security Dashboard|Error fetching the vulnerability counts. Please check your network connection and try again."
+msgid "Security configuration help link"
+msgstr ""
+
+msgid "Security dashboard"
msgstr ""
-msgid "Security Dashboard|Error fetching the vulnerability list. Please check your network connection and try again."
+msgid "Security report is out of date. Please update your branch with the latest changes from the target branch (%{targetBranchName})"
msgstr ""
-msgid "Security Dashboard|Issue Created"
+msgid "Security report is out of date. Run %{newPipelineLinkStart}a new pipeline%{newPipelineLinkEnd} for the target branch (%{targetBranchName})"
msgstr ""
-msgid "Security Reports|Comment added to '%{vulnerabilityName}'"
+msgid "SecurityConfiguration|Configured"
msgstr ""
-msgid "Security Reports|Comment deleted on '%{vulnerabilityName}'"
+msgid "SecurityConfiguration|Feature"
msgstr ""
-msgid "Security Reports|Comment edited on '%{vulnerabilityName}'"
+msgid "SecurityConfiguration|Feature documentation for %{featureName}"
msgstr ""
-msgid "Security Reports|Create issue"
+msgid "SecurityConfiguration|Not yet configured"
msgstr ""
-msgid "Security Reports|Dismiss vulnerability"
+msgid "SecurityConfiguration|Secure features"
msgstr ""
-msgid "Security Reports|Dismissed '%{vulnerabilityName}'"
+msgid "SecurityConfiguration|Status"
msgstr ""
-msgid "Security Reports|Dismissed '%{vulnerabilityName}'. Turn off the hide dismissed toggle to view."
+msgid "SecurityReports|%{firstProject} and %{secondProject}"
msgstr ""
-msgid "Security Reports|Either you don't have permission to view this dashboard or the dashboard has not been setup. Please check your permission settings with your administrator or check your dashboard configurations to proceed."
+msgid "SecurityReports|%{firstProject}, %{secondProject}, and %{rest}"
msgstr ""
-msgid "Security Reports|Learn more about setting up your dashboard"
+msgid "SecurityReports|Add a project to your dashboard"
msgstr ""
-msgid "Security Reports|More info"
+msgid "SecurityReports|Add or remove projects from your dashboard"
msgstr ""
-msgid "Security Reports|Oops, something doesn't seem right."
+msgid "SecurityReports|Add projects"
msgstr ""
-msgid "Security Reports|Security reports can only be accessed by authorized users."
+msgid "SecurityReports|Comment added to '%{vulnerabilityName}'"
msgstr ""
-msgid "Security Reports|There was an error adding the comment."
+msgid "SecurityReports|Comment deleted on '%{vulnerabilityName}'"
msgstr ""
-msgid "Security Reports|There was an error creating the issue."
+msgid "SecurityReports|Comment edited on '%{vulnerabilityName}'"
msgstr ""
-msgid "Security Reports|There was an error creating the merge request."
+msgid "SecurityReports|Create issue"
msgstr ""
-msgid "Security Reports|There was an error deleting the comment."
+msgid "SecurityReports|Dismiss Selected"
msgstr ""
-msgid "Security Reports|There was an error dismissing the vulnerabilities."
+msgid "SecurityReports|Dismiss vulnerability"
msgstr ""
-msgid "Security Reports|There was an error dismissing the vulnerability."
+msgid "SecurityReports|Dismissed '%{vulnerabilityName}'"
msgstr ""
-msgid "Security Reports|There was an error reverting the dismissal."
+msgid "SecurityReports|Dismissed '%{vulnerabilityName}'. Turn off the hide dismissed toggle to view."
msgstr ""
-msgid "Security Reports|There was an error reverting this dismissal."
+msgid "SecurityReports|Each vulnerability now has a unique page that can be directly linked to, shared, referenced, and tracked as the single source of truth. Vulnerability occurrences also persist across scanner runs, which improves tracking and visibility and reduces duplicates between scans."
msgstr ""
-msgid "Security Reports|Undo dismiss"
+msgid "SecurityReports|Edit dashboard"
msgstr ""
-msgid "Security Reports|You do not have sufficient permissions to access this report"
+msgid "SecurityReports|Either you don't have permission to view this dashboard or the dashboard has not been setup. Please check your permission settings with your administrator or check your dashboard configurations to proceed."
msgstr ""
-msgid "Security Reports|You must sign in as an authorized user to see this report"
+msgid "SecurityReports|Error fetching the vulnerability counts. Please check your network connection and try again."
msgstr ""
-msgid "Security configuration help link"
+msgid "SecurityReports|Error fetching the vulnerability list. Please check your network connection and try again."
msgstr ""
-msgid "Security dashboard"
+msgid "SecurityReports|False positive"
msgstr ""
-msgid "Security report is out of date. Please update your branch with the latest changes from the target branch (%{targetBranchName})"
+msgid "SecurityReports|Hide dismissed"
msgstr ""
-msgid "Security report is out of date. Run %{newPipelineLinkStart}a new pipeline%{newPipelineLinkEnd} for the target branch (%{targetBranchName})"
+msgid "SecurityReports|Introducing standalone vulnerabilities"
msgstr ""
-msgid "SecurityConfiguration|Configured"
+msgid "SecurityReports|Issue Created"
msgstr ""
-msgid "SecurityConfiguration|Feature"
+msgid "SecurityReports|Learn More"
msgstr ""
-msgid "SecurityConfiguration|Feature documentation for %{featureName}"
+msgid "SecurityReports|Learn more about setting up your dashboard"
msgstr ""
-msgid "SecurityConfiguration|Not yet configured"
+msgid "SecurityReports|Load more vulnerabilities"
msgstr ""
-msgid "SecurityConfiguration|Secure features"
+msgid "SecurityReports|Monitor vulnerabilities in your code"
msgstr ""
-msgid "SecurityConfiguration|Status"
+msgid "SecurityReports|More info"
msgstr ""
-msgid "SecurityDashboard|%{firstProject} and %{secondProject}"
+msgid "SecurityReports|More information"
msgstr ""
-msgid "SecurityDashboard|%{firstProject}, %{secondProject}, and %{rest}"
+msgid "SecurityReports|No vulnerabilities found for dashboard"
msgstr ""
-msgid "SecurityDashboard|Add a project to your dashboard"
+msgid "SecurityReports|No vulnerabilities found for this group"
msgstr ""
-msgid "SecurityDashboard|Add or remove projects from your dashboard"
+msgid "SecurityReports|No vulnerabilities found for this pipeline"
msgstr ""
-msgid "SecurityDashboard|Add projects"
+msgid "SecurityReports|No vulnerabilities found for this project"
msgstr ""
-msgid "SecurityDashboard|Edit dashboard"
+msgid "SecurityReports|Oops, something doesn't seem right."
msgstr ""
-msgid "SecurityDashboard|Hide dismissed"
+msgid "SecurityReports|Pipeline %{pipelineLink} triggered %{timeago} by %{user}"
msgstr ""
-msgid "SecurityDashboard|Monitor vulnerabilities in your code"
+msgid "SecurityReports|Project"
msgstr ""
-msgid "SecurityDashboard|More information"
+msgid "SecurityReports|Projects added"
msgstr ""
-msgid "SecurityDashboard|No vulnerabilities found for dashboard"
+msgid "SecurityReports|Remove project from dashboard"
msgstr ""
-msgid "SecurityDashboard|Pipeline %{pipelineLink} triggered %{timeago} by %{user}"
+msgid "SecurityReports|Report type"
msgstr ""
-msgid "SecurityDashboard|Project"
+msgid "SecurityReports|Return to dashboard"
msgstr ""
-msgid "SecurityDashboard|Projects added"
+msgid "SecurityReports|Security Dashboard"
msgstr ""
-msgid "SecurityDashboard|Remove project from dashboard"
+msgid "SecurityReports|Security reports can only be accessed by authorized users."
msgstr ""
-msgid "SecurityDashboard|Report type"
+msgid "SecurityReports|Select a project to add by using the project search field above."
msgstr ""
-msgid "SecurityDashboard|Return to dashboard"
+msgid "SecurityReports|Select a reason"
msgstr ""
-msgid "SecurityDashboard|Security Dashboard"
+msgid "SecurityReports|Severity"
msgstr ""
-msgid "SecurityDashboard|Select a project to add by using the project search field above."
+msgid "SecurityReports|Status"
msgstr ""
-msgid "SecurityDashboard|Severity"
+msgid "SecurityReports|The security dashboard displays the latest security findings for projects you wish to monitor. Select \"Edit dashboard\" to add and remove projects."
msgstr ""
-msgid "SecurityDashboard|Status"
+msgid "SecurityReports|The security dashboard displays the latest security report. Use it to find and fix vulnerabilities."
msgstr ""
-msgid "SecurityDashboard|The security dashboard displays the latest security findings for projects you wish to monitor. Select \"Edit dashboard\" to add and remove projects."
+msgid "SecurityReports|There was an error adding the comment."
msgstr ""
-msgid "SecurityDashboard|The security dashboard displays the latest security report. Use it to find and fix vulnerabilities."
+msgid "SecurityReports|There was an error creating the issue."
msgstr ""
-msgid "SecurityDashboard|There was an error while generating the report."
+msgid "SecurityReports|There was an error creating the merge request."
msgstr ""
-msgid "SecurityDashboard|Unable to add %{invalidProjects}"
+msgid "SecurityReports|There was an error deleting the comment."
+msgstr ""
+
+msgid "SecurityReports|There was an error dismissing the vulnerabilities."
+msgstr ""
+
+msgid "SecurityReports|There was an error dismissing the vulnerability."
+msgstr ""
+
+msgid "SecurityReports|There was an error reverting the dismissal."
+msgstr ""
+
+msgid "SecurityReports|There was an error reverting this dismissal."
+msgstr ""
+
+msgid "SecurityReports|There was an error while generating the report."
+msgstr ""
+
+msgid "SecurityReports|Unable to add %{invalidProjects}"
+msgstr ""
+
+msgid "SecurityReports|Undo dismiss"
+msgstr ""
+
+msgid "SecurityReports|While it's rare to have no vulnerabilities for your group, it can happen. In any event, we ask that you double check your settings to make sure you've set up your dashboard correctly."
+msgstr ""
+
+msgid "SecurityReports|While it's rare to have no vulnerabilities for your pipeline, it can happen. In any event, we ask that you double check your settings to make sure all security scanning jobs have passed successfully."
+msgstr ""
+
+msgid "SecurityReports|While it's rare to have no vulnerabilities for your project, it can happen. In any event, we ask that you double check your settings to make sure you've set up your dashboard correctly."
+msgstr ""
+
+msgid "SecurityReports|While it's rare to have no vulnerabilities, it can happen. In any event, we ask that you please double check your settings to make sure you've set up your dashboard correctly."
+msgstr ""
+
+msgid "SecurityReports|Won't fix / Accept risk"
+msgstr ""
+
+msgid "SecurityReports|You do not have sufficient permissions to access this report"
+msgstr ""
+
+msgid "SecurityReports|You must sign in as an authorized user to see this report"
+msgstr ""
+
+msgid "SecurityReports|[No reason]"
msgstr ""
msgid "See GitLab's %{password_policy_guidelines}"
@@ -18065,6 +18489,9 @@ msgstr ""
msgid "See the affected projects in the GitLab admin panel"
msgstr ""
+msgid "See what's new at GitLab"
+msgstr ""
+
msgid "Select"
msgstr ""
@@ -18470,9 +18897,6 @@ msgstr ""
msgid "Set up Jira Integration"
msgstr ""
-msgid "Set up Jira Integration illustration"
-msgstr ""
-
msgid "Set up a %{type} Runner automatically"
msgstr ""
@@ -18557,6 +18981,9 @@ msgstr ""
msgid "Severity: %{severity}"
msgstr ""
+msgid "Shards (%{shards})"
+msgstr ""
+
msgid "Shards to synchronize"
msgstr ""
@@ -18871,6 +19298,9 @@ msgstr ""
msgid "Snowplow"
msgstr ""
+msgid "Some child epics may be hidden due to applied filters"
+msgstr ""
+
msgid "Some email servers do not support overriding the email sender name. Enable this option to include the name of the author of the issue, merge request or comment in the email body instead."
msgstr ""
@@ -19012,6 +19442,9 @@ msgstr ""
msgid "Something went wrong while updating your list settings"
msgstr ""
+msgid "Something went wrong with your automatic subscription renewal"
+msgstr ""
+
msgid "Something went wrong, unable to add %{project} to dashboard"
msgstr ""
@@ -19297,6 +19730,9 @@ msgstr ""
msgid "Stage removed"
msgstr ""
+msgid "Standard"
+msgstr ""
+
msgid "Star a label to make it a priority label. Order the prioritized labels to change their relative priority, by dragging."
msgstr ""
@@ -19420,6 +19856,9 @@ msgstr ""
msgid "Static Application Security Testing (SAST)"
msgstr ""
+msgid "StaticSiteEditor|An error occurred while submitting your changes."
+msgstr ""
+
msgid "StaticSiteEditor|Branch could not be created."
msgstr ""
@@ -19429,18 +19868,30 @@ msgstr ""
msgid "StaticSiteEditor|Could not create merge request."
msgstr ""
+msgid "StaticSiteEditor|Incompatible file content"
+msgstr ""
+
msgid "StaticSiteEditor|Return to site"
msgstr ""
+msgid "StaticSiteEditor|Static site editor"
+msgstr ""
+
msgid "StaticSiteEditor|Success!"
msgstr ""
msgid "StaticSiteEditor|Summary of changes"
msgstr ""
+msgid "StaticSiteEditor|The Static Site Editor is currently configured to only edit Markdown content on pages generated from Middleman. Visit the documentation to learn more about configuring your site to use the Static Site Editor."
+msgstr ""
+
msgid "StaticSiteEditor|Update %{sourcePath} file"
msgstr ""
+msgid "StaticSiteEditor|View documentation"
+msgstr ""
+
msgid "StaticSiteEditor|View merge request"
msgstr ""
@@ -19498,6 +19949,12 @@ msgstr ""
msgid "StatusPage|Status page"
msgstr ""
+msgid "StatusPage|Status page URL"
+msgstr ""
+
+msgid "StatusPage|Status page frontend documentation"
+msgstr ""
+
msgid "StatusPage|To publish incidents to an external status page, GitLab will store a JSON file in your Amazon S3 account in a location accessible to your external status page service. Make sure to also set up %{docsLink}"
msgstr ""
@@ -19846,6 +20303,12 @@ msgstr ""
msgid "Synced"
msgstr ""
+msgid "Synchronization disabled"
+msgstr ""
+
+msgid "Synchronization of container repositories is disabled."
+msgstr ""
+
msgid "System"
msgstr ""
@@ -19999,6 +20462,9 @@ msgstr ""
msgid "Telephone number"
msgstr ""
+msgid "Telephone number (Optional)"
+msgstr ""
+
msgid "Template"
msgstr ""
@@ -20305,6 +20771,9 @@ msgstr ""
msgid "The license was successfully uploaded and is now active. You can see the details below."
msgstr ""
+msgid "The license was successfully uploaded and will be active from %{starts_at}. You can see the details below."
+msgstr ""
+
msgid "The maximum file size allowed is %{size}."
msgstr ""
@@ -20626,6 +21095,9 @@ msgstr ""
msgid "There was an error getting the epic participants."
msgstr ""
+msgid "There was an error importing the Jira project."
+msgstr ""
+
msgid "There was an error loading users activity calendar."
msgstr ""
@@ -20641,7 +21113,7 @@ msgstr ""
msgid "There was an error resetting user pipeline minutes."
msgstr ""
-msgid "There was an error saving this Geo Node"
+msgid "There was an error saving this Geo Node."
msgstr ""
msgid "There was an error saving your changes."
@@ -20704,6 +21176,9 @@ msgstr ""
msgid "They can be managed using the %{link}."
msgstr ""
+msgid "Third Party Advisory Link"
+msgstr ""
+
msgid "Third party offers"
msgstr ""
@@ -20740,6 +21215,9 @@ msgstr ""
msgid "This application will be able to:"
msgstr ""
+msgid "This attachment has been truncated to avoid exceeding the maximum allowed attachment size of 15MB. %{written_count} of %{issues_count} issues have been included. Consider re-exporting with a narrower selection of issues."
+msgstr ""
+
msgid "This block is self-referential"
msgstr ""
@@ -20881,6 +21359,12 @@ msgstr ""
msgid "This issue is confidential"
msgstr ""
+msgid "This issue is currently blocked by the following issues: %{issues}."
+msgstr ""
+
+msgid "This issue is in a child epic of the filtered epic"
+msgstr ""
+
msgid "This issue is locked."
msgstr ""
@@ -21079,7 +21563,10 @@ msgstr ""
msgid "This user will be the author of all events in the activity feed that are the result of an update, like new branches being created or new commits being pushed to existing branches. Upon creation or when reassigning you can only assign yourself to be the mirror user."
msgstr ""
-msgid "This variable can not be masked"
+msgid "This variable can not be masked."
+msgstr ""
+
+msgid "This variable does not match the expected pattern."
msgstr ""
msgid "This will help us personalize your onboarding experience."
@@ -21109,10 +21596,10 @@ msgstr ""
msgid "ThreatMonitoring|Application firewall not detected"
msgstr ""
-msgid "ThreatMonitoring|Container Network Policy"
+msgid "ThreatMonitoring|Container Network Policies are not installed or have been disabled. To view this data, ensure your Network Policies are installed and enabled for your cluster."
msgstr ""
-msgid "ThreatMonitoring|Container NetworkPolicies are not installed or has been disabled. To view this data, ensure you NetworkPolicies are installed and enabled for your cluster."
+msgid "ThreatMonitoring|Container Network Policy"
msgstr ""
msgid "ThreatMonitoring|Container NetworkPolicies not detected"
@@ -21145,7 +21632,7 @@ msgstr ""
msgid "ThreatMonitoring|Something went wrong, unable to fetch statistics"
msgstr ""
-msgid "ThreatMonitoring|The firewall is not installed or has been disabled. To view this data, ensure you firewall is installed and enabled for your cluster."
+msgid "ThreatMonitoring|The firewall is not installed or has been disabled. To view this data, ensure the web application firewall is installed and enabled for your cluster."
msgstr ""
msgid "ThreatMonitoring|The graph below is an overview of traffic coming to your application as tracked by the Web Application Firewall (WAF). View the docs for instructions on how to access the WAF logs to see what type of malicious traffic is trying to access your app. The docs link is also accessible by clicking the \"?\" icon next to the title below."
@@ -21214,6 +21701,9 @@ msgstr ""
msgid "Time in seconds GitLab will wait for a response from the external service. When the service does not respond in time, access will be denied."
msgstr ""
+msgid "Time of import: %{importTime}"
+msgstr ""
+
msgid "Time remaining"
msgstr ""
@@ -21398,7 +21888,7 @@ msgstr ""
msgid "Title:"
msgstr ""
-msgid "Titles and Filenames"
+msgid "Titles and Descriptions"
msgstr ""
msgid "To"
@@ -21605,7 +22095,7 @@ msgstr ""
msgid "Too many projects enabled. You will need to manage them via the console or the API."
msgstr ""
-msgid "Topics"
+msgid "Topics (optional)"
msgstr ""
msgid "Total"
@@ -21767,6 +22257,9 @@ msgstr ""
msgid "Try all GitLab has to offer for 30 days."
msgstr ""
+msgid "Try changing or removing filters."
+msgstr ""
+
msgid "Try to fork again"
msgstr ""
@@ -21860,6 +22353,9 @@ msgstr ""
msgid "Unable to connect to server: %{error}"
msgstr ""
+msgid "Unable to connect to the Jira instance. Please check your Jira integration configuration."
+msgstr ""
+
msgid "Unable to convert Kubernetes logs encoding to UTF-8"
msgstr ""
@@ -22803,6 +23299,9 @@ msgstr ""
msgid "View issue"
msgstr ""
+msgid "View issues"
+msgstr ""
+
msgid "View it on GitLab"
msgstr ""
@@ -22980,6 +23479,15 @@ msgstr ""
msgid "VulnerabilityManagement|Resolved %{timeago} by %{user}"
msgstr ""
+msgid "VulnerabilityManagement|Something went wrong while trying to delete the comment. Please try again later."
+msgstr ""
+
+msgid "VulnerabilityManagement|Something went wrong while trying to retrieve the vulnerability history. Please try again later."
+msgstr ""
+
+msgid "VulnerabilityManagement|Something went wrong while trying to save the comment. Please try again later."
+msgstr ""
+
msgid "VulnerabilityManagement|Something went wrong, could not create an issue."
msgstr ""
@@ -23010,6 +23518,9 @@ msgstr ""
msgid "VulnerabilityStatusTypes|Resolved"
msgstr ""
+msgid "Vulnerability|%{scannerName} (version %{scannerVersion})"
+msgstr ""
+
msgid "Vulnerability|Class"
msgstr ""
@@ -23043,7 +23554,10 @@ msgstr ""
msgid "Vulnerability|Project"
msgstr ""
-msgid "Vulnerability|Report Type"
+msgid "Vulnerability|Scanner Provider"
+msgstr ""
+
+msgid "Vulnerability|Scanner Type"
msgstr ""
msgid "Vulnerability|Severity"
@@ -23097,9 +23611,15 @@ msgstr ""
msgid "We sent you an email with reset password instructions"
msgstr ""
+msgid "We tried to automatically renew your %{strong}%{plan_name}%{strong_close} subscription for %{strong}%{namespace_name}%{strong_close} on %{expires_on} but something went wrong so your subscription was downgraded to the free plan. Don't worry, your data is safe. We suggest you check your payment method and get in touch with our support team (%{support_link}). They'll gladly help with your subscription renewal."
+msgstr ""
+
msgid "We want to be sure it is you, please confirm you are not a robot."
msgstr ""
+msgid "We will automatically renew your %{strong}%{plan_name}%{strong_close} subscription for %{strong}%{namespace_name}%{strong_close} on %{strong}%{expires_on}%{strong_close}. There's nothing that you need to do, we'll let you know when the renewal is complete. Need more seats, a higher plan or just want to review your payment method?"
+msgstr ""
+
msgid "We've found no vulnerabilities"
msgstr ""
@@ -23204,15 +23724,6 @@ msgstr ""
msgid "When:"
msgstr ""
-msgid "While it's rare to have no vulnerabilities for your group, it can happen. In any event, we ask that you double check your settings to make sure you've set up your dashboard correctly."
-msgstr ""
-
-msgid "While it's rare to have no vulnerabilities for your pipeline, it can happen. In any event, we ask that you double check your settings to make sure all security scanning jobs have passed successfully."
-msgstr ""
-
-msgid "While it's rare to have no vulnerabilities for your project, it can happen. In any event, we ask that you double check your settings to make sure you've set up your dashboard correctly."
-msgstr ""
-
msgid "While it's rare to have no vulnerabilities, it can happen. In any event, we ask that you please double check your settings to make sure you've set up your dashboard correctly."
msgstr ""
@@ -23243,9 +23754,6 @@ msgstr ""
msgid "Wiki"
msgstr ""
-msgid "Wiki pages"
-msgstr ""
-
msgid "Wiki was successfully updated."
msgstr ""
@@ -23429,6 +23937,9 @@ msgstr ""
msgid "Yes, add it"
msgstr ""
+msgid "Yes, close issue"
+msgstr ""
+
msgid "Yes, let me map Google Code users to full names or GitLab users."
msgstr ""
@@ -23624,6 +24135,9 @@ msgstr ""
msgid "You could not create a new trigger."
msgstr ""
+msgid "You didn't renew your %{strong}%{plan_name}%{strong_close} subscription for %{strong}%{namespace_name}%{strong_close} so it was downgraded to the free plan."
+msgstr ""
+
msgid "You didn't renew your %{strong}%{plan_name}%{strong_close} subscription so it was downgraded to the GitLab Core Plan."
msgstr ""
@@ -23690,6 +24204,9 @@ msgstr ""
msgid "You have declined the invitation to join %{label}."
msgstr ""
+msgid "You have imported from this project %{numberOfPreviousImportsForProject} times before. Each new import will create duplicate issues."
+msgstr ""
+
msgid "You have no permissions"
msgstr ""
@@ -23771,6 +24288,9 @@ msgstr ""
msgid "You need to upload a Google Takeout archive."
msgstr ""
+msgid "You reached %{usage_in_percent} of %{namespace_name}'s capacity (%{used_storage} of %{storage_limit})"
+msgstr ""
+
msgid "You tried to fork %{link_to_the_project} but it failed for the following reason:"
msgstr ""
@@ -23855,9 +24375,21 @@ msgstr ""
msgid "YouTube"
msgstr ""
+msgid "Your %{strong}%{plan_name}%{strong_close} subscription for %{strong}%{namespace_name}%{strong_close} will expire on %{strong}%{expires_on}%{strong_close}. After that, you will not to be able to create issues or merge requests as well as many other features."
+msgstr ""
+
msgid "Your %{strong}%{plan_name}%{strong_close} subscription will expire on %{strong}%{expires_on}%{strong_close}. After that, you will not to be able to create issues or merge requests as well as many other features."
msgstr ""
+msgid "Your CSV export has started. It will be emailed to %{email} when complete."
+msgstr ""
+
+msgid "Your CSV export of %{issues_count} from project %{project_link} has been added to this email as an attachment."
+msgstr ""
+
+msgid "Your CSV export of %{written_count} from project %{project_name} (%{project_url}) has been added to this email as an attachment."
+msgstr ""
+
msgid "Your Commit Email will be used for web based operations, such as edits and merges."
msgstr ""
@@ -23954,6 +24486,9 @@ msgstr ""
msgid "Your comment could not be updated! Please check your network connection and try again."
msgstr ""
+msgid "Your comment will be discarded."
+msgstr ""
+
msgid "Your custom stage '%{title}' was created"
msgstr ""
@@ -23993,6 +24528,9 @@ msgstr ""
msgid "Your new personal access token has been created."
msgstr ""
+msgid "Your new project access token has been created."
+msgstr ""
+
msgid "Your password isn't required to view this page. If a password or any other personal details are requested, please contact your administrator to report abuse."
msgstr ""
@@ -24011,12 +24549,18 @@ msgstr ""
msgid "Your request for access has been queued for review."
msgstr ""
+msgid "Your search didn't match any commits."
+msgstr ""
+
msgid "Your subscription expired!"
msgstr ""
msgid "Your subscription has been downgraded"
msgstr ""
+msgid "Your subscription will automatically renew in %{remaining_days}"
+msgstr ""
+
msgid "Your subscription will expire in %{remaining_days}"
msgstr ""
@@ -24055,7 +24599,7 @@ msgstr ""
msgid "allowed to fail"
msgstr ""
-msgid "already being used for another group or project milestone."
+msgid "already being used for another group or project %{timebox_name}."
msgstr ""
msgid "already has a \"created\" issue link"
@@ -24133,6 +24677,9 @@ msgstr ""
msgid "ciReport|%{linkStartTag}Learn more about SAST %{linkEndTag}"
msgstr ""
+msgid "ciReport|%{linkStartTag}Learn more about Secret Scanning %{linkEndTag}"
+msgstr ""
+
msgid "ciReport|%{linkStartTag}Learn more about codequality reports %{linkEndTag}"
msgstr ""
@@ -24296,6 +24843,12 @@ msgstr ""
msgid "ciReport|SAST"
msgstr ""
+msgid "ciReport|Secret scanning"
+msgstr ""
+
+msgid "ciReport|Secret scanning detects secrets and credentials vulnerabilities in your source code."
+msgstr ""
+
msgid "ciReport|Security scanning"
msgstr ""
@@ -24493,6 +25046,9 @@ msgstr ""
msgid "group"
msgstr ""
+msgid "groups"
+msgstr ""
+
msgid "has already been linked to another vulnerability"
msgstr ""
@@ -24656,9 +25212,6 @@ msgstr[1] ""
msgid "merged %{time_ago}"
msgstr ""
-msgid "milestone should belong either to a project or a group."
-msgstr ""
-
msgid "missing"
msgstr ""
@@ -24995,6 +25548,9 @@ msgstr ""
msgid "no contributions"
msgstr ""
+msgid "no expiration"
+msgstr ""
+
msgid "no one can merge"
msgstr ""
@@ -25047,6 +25603,9 @@ msgstr ""
msgid "pending removal"
msgstr ""
+msgid "per day"
+msgstr ""
+
msgid "pipeline"
msgstr ""
@@ -25075,6 +25634,12 @@ msgstr[1] ""
msgid "project avatar"
msgstr ""
+msgid "projects"
+msgstr ""
+
+msgid "push to your repository, create pipelines, create issues or add comments. To reduce storage capacity, delete unused repositories, artifacts, wikis, issues, and pipelines."
+msgstr ""
+
msgid "quick actions"
msgstr ""
@@ -25221,11 +25786,6 @@ msgstr ""
msgid "this document"
msgstr ""
-msgid "thread resolved"
-msgid_plural "threads resolved"
-msgstr[0] ""
-msgstr[1] ""
-
msgid "to help your contributors communicate effectively!"
msgstr ""
@@ -25280,6 +25840,9 @@ msgstr ""
msgid "view the blob"
msgstr ""
+msgid "vulnerability|Add a comment"
+msgstr ""
+
msgid "vulnerability|Add a comment or reason for dismissal"
msgstr ""
diff --git a/locale/de/gitlab.po b/locale/de/gitlab.po
index 02d7c210a2a..d0782b75010 100644
--- a/locale/de/gitlab.po
+++ b/locale/de/gitlab.po
@@ -12,7 +12,7 @@ msgstr ""
"X-Crowdin-Project: gitlab-ee\n"
"X-Crowdin-Language: de\n"
"X-Crowdin-File: /master/locale/gitlab.pot\n"
-"PO-Revision-Date: 2020-04-15 00:32\n"
+"PO-Revision-Date: 2020-05-05 21:14\n"
msgid " %{start} to %{end}"
msgstr ""
@@ -212,6 +212,11 @@ msgid_plural "%d tags"
msgstr[0] ""
msgstr[1] ""
+msgid "%d unresolved thread"
+msgid_plural "%d unresolved threads"
+msgstr[0] ""
+msgstr[1] ""
+
msgid "%d vulnerability dismissed"
msgid_plural "%d vulnerabilities dismissed"
msgstr[0] ""
@@ -371,6 +376,9 @@ msgstr "%{mergeLength}/%{usersLength} können zusammenführen"
msgid "%{mrText}, this issue will be closed automatically."
msgstr "%{mrText}, dieses Ticket wird automatisch geschlossen werden."
+msgid "%{namespace_name} is now read-only. You cannot: %{base_message}"
+msgstr ""
+
msgid "%{name} contained %{resultsString}"
msgstr "%{name} enthielt %{resultsString}"
@@ -383,6 +391,11 @@ msgstr ""
msgid "%{name}'s avatar"
msgstr "Avatar von %{name}"
+msgid "%{no_of_days} day"
+msgid_plural "%{no_of_days} days"
+msgstr[0] ""
+msgstr[1] ""
+
msgid "%{number_commits_behind} commits behind %{default_branch}, %{number_commits_ahead} commits ahead"
msgstr "%{number_commits_behind} Commits hinter %{default_branch}, %{number_commits_ahead} Commits voraus"
@@ -506,6 +519,9 @@ msgstr[1] "%{text} %{files} Dateien"
msgid "%{text} is available"
msgstr "%{text} ist verfügbar"
+msgid "%{timebox_name} should belong either to a project or a group."
+msgstr ""
+
msgid "%{title} %{operator} %{threshold}"
msgstr "%{title} %{operator} %{threshold}"
@@ -781,12 +797,18 @@ msgstr "<code>\"johnsmith@example.com\": \"johnsm...@example.com\"</code> fügt
msgid "<code>\"johnsmith@example.com\": \"johnsmith@example.com\"</code> will add \"By <a href=\"#\">johnsmith@example.com</a>\" to all issues and comments originally created by johnsmith@example.com. By default, the email address or username is masked to ensure the user's privacy. Use this option if you want to show the full email address."
msgstr "<code>\"johnsmith@example.com\": \"johnsmith@example.com\"</code> fügt \"Von <a href=\"#\">johnsmith@example.com</a>\" zu allen Tickets und Kommentaren hinzu, die ursprünglich von johnsmith@example.com erstellt wurden. Standardmäßig wird die E-Mail-Adresse maskiert, um den Datenschutz des Nutzers zu gewährleisten. Nutze diese Option, wenn du die volle E-Mail-Adresse anzeigen willst."
+msgid "<namespace / project>"
+msgstr ""
+
msgid "<no name set>"
msgstr "<no name set>"
msgid "<no scopes selected>"
msgstr "<keine Bereiche ausgewählt>"
+msgid "<project name>"
+msgstr ""
+
msgid "<strong>%{group_name}</strong> group members"
msgstr "<strong>%{group_name}</strong> Gruppenmitglieder"
@@ -820,6 +842,9 @@ msgstr "Ein Let's Encrypt-Konto wird für diese GitLab-Installation mit deiner E
msgid "A basic page and serverless function that uses AWS Lambda, AWS API Gateway, and GitLab Pages"
msgstr "Eine grundlegende Seite und serverlose Funktion, die AWS Lambda, AWS API Gateway und GitLab Pages verwendet"
+msgid "A complete DevOps platform"
+msgstr ""
+
msgid "A default branch cannot be chosen for an empty project."
msgstr "Ein Default-Branch kann nicht für ein leeres Projekt ausgewählt werden."
@@ -1036,6 +1061,12 @@ msgstr "Dein Token für statische Objekte wird verwendet, um dich zu authentifiz
msgid "AccessTokens|reset it"
msgstr "Zurücksetzen"
+msgid "AccessibilityReport|Accessibility report artifact not found"
+msgstr ""
+
+msgid "AccessibilityReport|Failed to retrieve accessibility report"
+msgstr ""
+
msgid "AccessibilityReport|Learn More"
msgstr ""
@@ -1045,6 +1076,9 @@ msgstr ""
msgid "AccessibilityReport|New"
msgstr ""
+msgid "AccessibilityReport|The accessibility scanning found an error of the following type: %{code}"
+msgstr ""
+
msgid "Account"
msgstr "Konto"
@@ -1287,6 +1321,9 @@ msgstr "Ein Ticket zu einem Epic hinzugefügt."
msgid "Added at"
msgstr "Hinzugefügt um"
+msgid "Added for this merge request"
+msgstr ""
+
msgid "Added in this version"
msgstr "In dieser Version hinzugefügt"
@@ -1658,6 +1695,72 @@ msgid_plural "Alerts"
msgstr[0] "Alarm"
msgstr[1] "Alarme"
+msgid "Alert Details"
+msgstr ""
+
+msgid "AlertManagement|Acknowledged"
+msgstr ""
+
+msgid "AlertManagement|Alert"
+msgstr ""
+
+msgid "AlertManagement|Authorize external service"
+msgstr ""
+
+msgid "AlertManagement|Display alerts from all your monitoring tools directly within GitLab. Streamline the investigation of your alerts and the escalation of alerts to incidents."
+msgstr ""
+
+msgid "AlertManagement|End time"
+msgstr ""
+
+msgid "AlertManagement|End time:"
+msgstr ""
+
+msgid "AlertManagement|Events"
+msgstr ""
+
+msgid "AlertManagement|Events:"
+msgstr ""
+
+msgid "AlertManagement|Full Alert Details"
+msgstr ""
+
+msgid "AlertManagement|More information"
+msgstr ""
+
+msgid "AlertManagement|No alerts available to display. If you think you're seeing this message in error, refresh the page."
+msgstr ""
+
+msgid "AlertManagement|No alerts to display."
+msgstr ""
+
+msgid "AlertManagement|Overview"
+msgstr ""
+
+msgid "AlertManagement|Resolved"
+msgstr ""
+
+msgid "AlertManagement|Severity"
+msgstr ""
+
+msgid "AlertManagement|Start time"
+msgstr ""
+
+msgid "AlertManagement|Start time:"
+msgstr ""
+
+msgid "AlertManagement|Status"
+msgstr ""
+
+msgid "AlertManagement|Surface alerts in GitLab"
+msgstr ""
+
+msgid "AlertManagement|There was an error displaying the alerts. Confirm your endpoint's configuration details to ensure alerts appear."
+msgstr ""
+
+msgid "AlertManagement|Triggered"
+msgstr ""
+
msgid "AlertService|%{linkStart}Learn more%{linkEnd} about configuring this endpoint to receive alerts."
msgstr ""
@@ -1727,6 +1830,9 @@ msgstr "Alle Projekte"
msgid "All security scans are enabled because %{linkStart}Auto DevOps%{linkEnd} is enabled on this project"
msgstr "Alle Sicherheits-Scans sind aktiviert, weil %{linkStart}Auto DevOps%{linkEnd} in diesem Projekt aktiviert ist"
+msgid "All threads resolved"
+msgstr ""
+
msgid "All users"
msgstr "Alle Benutzer(innen)"
@@ -1745,6 +1851,9 @@ msgstr "Gruppenbesitzer erlauben, LDAP-bezogene Einstellungen zu verwalten"
msgid "Allow only the selected protocols to be used for Git access."
msgstr "Nur die ausgewählten Protokolle für den Git-Zugriff zulassen."
+msgid "Allow owners to manage default branch protection per group"
+msgstr ""
+
msgid "Allow owners to manually add users outside of LDAP"
msgstr ""
@@ -1853,6 +1962,9 @@ msgstr "Beim Abrufen der Genehmigungsberechtigten für die neue Regel ist ein Fe
msgid "An error occurred fetching the dropdown data."
msgstr "Beim Abrufen der Dropdown-Daten ist ein Fehler aufgetreten."
+msgid "An error occurred fetching the project authors."
+msgstr ""
+
msgid "An error occurred previewing the blob"
msgstr "Bei der Vorschau des Blobs ist ein Fehler aufgetreten"
@@ -1934,6 +2046,9 @@ msgstr "Beim Abrufen der Projekt-Autovervollständigung ist ein Fehler aufgetret
msgid "An error occurred while fetching sidebar data"
msgstr "Während des Abrufens der Daten für die Seitenleiste ist ein Fehler aufgetreten"
+msgid "An error occurred while fetching terraform reports."
+msgstr ""
+
msgid "An error occurred while fetching the Service Desk address."
msgstr "Beim Abrufen der Service-Desk-Adresse ist ein Fehler aufgetreten."
@@ -2171,6 +2286,9 @@ msgstr "Anti-Spam-Überprüfung"
msgid "Any"
msgstr "Irgendein"
+msgid "Any Author"
+msgstr ""
+
msgid "Any Label"
msgstr "Beliebiges Label"
@@ -2386,12 +2504,12 @@ msgstr "Möchtest du wirklich dieses Projekt archivieren?"
msgid "Are you sure that you want to unarchive this project?"
msgstr "Möchtest du wirklich dieses Projekt aus dem Archiv zurückholen?"
-msgid "Are you sure you want to cancel creating this comment?"
-msgstr "Möchtest du wirklich die Erstellung dieses Kommentars abbrechen?"
-
msgid "Are you sure you want to cancel editing this comment?"
msgstr "Möchtest du wirklich die Bearbeitung dieses Kommentars abbrechen?"
+msgid "Are you sure you want to close this blocked issue?"
+msgstr ""
+
msgid "Are you sure you want to delete %{name}?"
msgstr ""
@@ -2419,6 +2537,9 @@ msgstr ""
msgid "Are you sure you want to deploy this environment?"
msgstr ""
+msgid "Are you sure you want to discard this comment?"
+msgstr ""
+
msgid "Are you sure you want to erase this build?"
msgstr "Bist du sicher, dass du diesen Build löschen möchtest?"
@@ -2595,6 +2716,9 @@ msgstr "Weise %{assignee_users_sentence} zu."
msgid "At least one approval from a code owner is required to change files matching the respective CODEOWNER rules."
msgstr "Mindestens eine Zustimmung der Code-Eigentümer(innen) ist erforderlich, um Dateien zu ändern, auf die die entsprechenden CODEOWNER-Regeln zutreffen."
+msgid "At least one logging option is required to be enabled"
+msgstr ""
+
msgid "At least one of group_id or project_id must be specified"
msgstr ""
@@ -2633,6 +2757,27 @@ msgstr ""
msgid "AuditEvents|Target"
msgstr ""
+msgid "AuditLogs|(removed)"
+msgstr ""
+
+msgid "AuditLogs|Action"
+msgstr ""
+
+msgid "AuditLogs|Author"
+msgstr ""
+
+msgid "AuditLogs|Date"
+msgstr ""
+
+msgid "AuditLogs|IP Address"
+msgstr ""
+
+msgid "AuditLogs|Object"
+msgstr ""
+
+msgid "AuditLogs|Target"
+msgstr ""
+
msgid "Aug"
msgstr "Aug"
@@ -2915,9 +3060,6 @@ msgstr ""
msgid "BambooService|You must set up automatic revision labeling and a repository trigger in Bamboo."
msgstr ""
-msgid "Batch operations"
-msgstr ""
-
msgid "BatchComments|Delete all pending comments"
msgstr "Alle ausstehenden Kommentare löschen"
@@ -3047,6 +3189,9 @@ msgstr ""
msgid "Boards"
msgstr "Boards"
+msgid "Boards and Board Lists"
+msgstr ""
+
msgid "Boards|Collapse"
msgstr "Einklappen"
@@ -3266,6 +3411,9 @@ msgstr "EE kaufen"
msgid "Buy GitLab Enterprise Edition"
msgstr "Kaufe die GitLab Enterprise Edition"
+msgid "Buy more Pipeline minutes"
+msgstr ""
+
msgid "By %{user_name}"
msgstr "Von %{user_name}"
@@ -3386,6 +3534,9 @@ msgstr ""
msgid "Can't scan the code?"
msgstr ""
+msgid "Can't update snippet: %{err}"
+msgstr ""
+
msgid "Canary"
msgstr ""
@@ -3554,6 +3705,9 @@ msgstr "Das Ändern eines Release-Tags wird nur über die Release-API unterstüt
msgid "Changing group path can have unintended side effects."
msgstr ""
+msgid "Charts"
+msgstr ""
+
msgid "Charts can't be displayed as the request for data has timed out. %{documentationLink}"
msgstr ""
@@ -3590,9 +3744,6 @@ msgstr "Tag"
msgid "ChatMessage|and [%{count} more](%{pipeline_failed_jobs_url})"
msgstr ""
-msgid "ChatMessage|failed"
-msgstr ""
-
msgid "ChatMessage|has failed"
msgstr ""
@@ -3608,9 +3759,6 @@ msgstr "in %{duration}"
msgid "ChatMessage|in %{project_link}"
msgstr ""
-msgid "ChatMessage|passed"
-msgstr ""
-
msgid "Check again"
msgstr "Erneut prüfen"
@@ -4061,9 +4209,6 @@ msgstr ""
msgid "Click any <strong>project name</strong> in the project list below to navigate to the project milestone."
msgstr "Klicke auf einen beliebigen <strong>-Projektnamen</strong> in der folgenden Projektliste, um zum Projektmeilenstein zu navigieren."
-msgid "Click here"
-msgstr ""
-
msgid "Click the <strong>Download</strong> button and wait for downloading to complete."
msgstr "Klicke auf den <strong>Download</strong>-Button und warte bis das Herunterladen abgeschlossen ist."
@@ -4451,6 +4596,12 @@ msgstr "Abrufen der Projekte"
msgid "ClusterIntegration|Fetching zones"
msgstr "Rufe Zonen ab"
+msgid "ClusterIntegration|Fluentd"
+msgstr ""
+
+msgid "ClusterIntegration|Fluentd is an open source data collector, which lets you unify the data collection and consumption for a better use and understanding of data. It requires at least one of the following logs to be successfully installed."
+msgstr ""
+
msgid "ClusterIntegration|GitLab Integration"
msgstr "GitLab-Integration"
@@ -4757,6 +4908,15 @@ msgstr "Anfrage zur Installation fehlgeschlagen"
msgid "ClusterIntegration|Request to begin uninstalling failed"
msgstr ""
+msgid "ClusterIntegration|SIEM Hostname"
+msgstr ""
+
+msgid "ClusterIntegration|SIEM Port"
+msgstr ""
+
+msgid "ClusterIntegration|SIEM Protocol"
+msgstr ""
+
msgid "ClusterIntegration|Save changes"
msgstr "Änderungen speichern"
@@ -4853,6 +5013,12 @@ msgstr "Wähle eine Zone aus"
msgid "ClusterIntegration|Select zone to choose machine type"
msgstr "Wähle eine Zone aus, um den Maschinentyp auszuwählen"
+msgid "ClusterIntegration|Send Cilium Logs"
+msgstr ""
+
+msgid "ClusterIntegration|Send ModSecurity Logs"
+msgstr ""
+
msgid "ClusterIntegration|Service Token"
msgstr ""
@@ -5281,18 +5447,33 @@ msgstr ""
msgid "Compliance framework (optional)"
msgstr ""
+msgid "ComplianceFramework|GDPR"
+msgstr ""
+
msgid "ComplianceFramework|GDPR - General Data Protection Regulation"
msgstr ""
+msgid "ComplianceFramework|HIPAA"
+msgstr ""
+
msgid "ComplianceFramework|HIPAA - Health Insurance Portability and Accountability Act"
msgstr ""
+msgid "ComplianceFramework|PCI-DSS"
+msgstr ""
+
msgid "ComplianceFramework|PCI-DSS - Payment Card Industry-Data Security Standard"
msgstr ""
+msgid "ComplianceFramework|SOC 2"
+msgstr ""
+
msgid "ComplianceFramework|SOC 2 - Service Organization Control 2"
msgstr ""
+msgid "ComplianceFramework|SOX"
+msgstr ""
+
msgid "ComplianceFramework|SOX - Sarbanes-Oxley"
msgstr ""
@@ -5440,9 +5621,15 @@ msgstr ""
msgid "Container repositories sync capacity"
msgstr ""
+msgid "ContainerRegistry| Please visit the %{linkStart}administration settings%{linkEnd} to enable this feature."
+msgstr ""
+
msgid "ContainerRegistry|%{imageName} tags"
msgstr ""
+msgid "ContainerRegistry|%{title} was successfully scheduled for deletion"
+msgstr ""
+
msgid "ContainerRegistry|Automatically remove extra images that aren't designed to be kept."
msgstr ""
@@ -5455,6 +5642,9 @@ msgstr ""
msgid "ContainerRegistry|Container Registry"
msgstr ""
+msgid "ContainerRegistry|Container Registry tag expiration and retention policy is disabled"
+msgstr ""
+
msgid "ContainerRegistry|Copy build command"
msgstr ""
@@ -5464,18 +5654,12 @@ msgstr ""
msgid "ContainerRegistry|Copy push command"
msgstr ""
-msgid "ContainerRegistry|Currently, the Container Registry tag expiration feature is not available for projects created before GitLab version 12.8. For updates and more information, visit Issue %{linkStart}#196124%{linkEnd}"
-msgstr ""
-
msgid "ContainerRegistry|Docker connection error"
msgstr ""
msgid "ContainerRegistry|Docker tag expiration policy is %{toggleStatus}"
msgstr ""
-msgid "ContainerRegistry|Docker tags with names matching this regex pattern will expire:"
-msgstr ""
-
msgid "ContainerRegistry|Edit Settings"
msgstr ""
@@ -5497,9 +5681,6 @@ msgstr ""
msgid "ContainerRegistry|Image ID"
msgstr ""
-msgid "ContainerRegistry|Image deleted successfully"
-msgstr ""
-
msgid "ContainerRegistry|Keep and protect the images that matter most."
msgstr ""
@@ -5515,12 +5696,24 @@ msgstr ""
msgid "ContainerRegistry|Number of tags to retain:"
msgstr ""
+msgid "ContainerRegistry|Please contact your administrator."
+msgstr ""
+
msgid "ContainerRegistry|Push an image"
msgstr ""
msgid "ContainerRegistry|Quick Start"
msgstr ""
+msgid "ContainerRegistry|Regular expressions such as %{codeStart}.*-test%{codeEnd} or %{codeStart}dev-.*%{codeEnd} are supported"
+msgstr ""
+
+msgid "ContainerRegistry|Regular expressions such as %{codeStart}.*-test%{codeEnd} or %{codeStart}dev-.*%{codeEnd} are supported. To select all tags, use %{codeStart}.*%{codeEnd}"
+msgstr ""
+
+msgid "ContainerRegistry|Remember to run %{docLinkStart}garbage collection%{docLinkEnd} to remove the stale data from storage."
+msgstr ""
+
msgid "ContainerRegistry|Remove repository"
msgstr "Repository entfernen"
@@ -5535,22 +5728,22 @@ msgstr[1] ""
msgid "ContainerRegistry|Retention policy has been Enabled"
msgstr ""
-msgid "ContainerRegistry|Something went wrong while deleting the image."
+msgid "ContainerRegistry|Something went wrong while fetching the expiration policy."
msgstr ""
-msgid "ContainerRegistry|Something went wrong while deleting the tag."
+msgid "ContainerRegistry|Something went wrong while fetching the repository list."
msgstr ""
-msgid "ContainerRegistry|Something went wrong while deleting the tags."
+msgid "ContainerRegistry|Something went wrong while fetching the tags list."
msgstr ""
-msgid "ContainerRegistry|Something went wrong while fetching the expiration policy."
+msgid "ContainerRegistry|Something went wrong while marking the tag for deletion."
msgstr ""
-msgid "ContainerRegistry|Something went wrong while fetching the packages list."
+msgid "ContainerRegistry|Something went wrong while marking the tags for deletion."
msgstr ""
-msgid "ContainerRegistry|Something went wrong while fetching the tags list."
+msgid "ContainerRegistry|Something went wrong while scheduling %{title} for deletion. Please try again."
msgstr ""
msgid "ContainerRegistry|Something went wrong while updating the expiration policy."
@@ -5559,16 +5752,25 @@ msgstr ""
msgid "ContainerRegistry|Tag"
msgstr "Tag"
-msgid "ContainerRegistry|Tag deleted successfully"
-msgstr ""
-
msgid "ContainerRegistry|Tag expiration policy"
msgstr ""
msgid "ContainerRegistry|Tag expiration policy is designed to:"
msgstr ""
-msgid "ContainerRegistry|Tags deleted successfully"
+msgid "ContainerRegistry|Tag successfully marked for deletion."
+msgstr ""
+
+msgid "ContainerRegistry|Tags successfully marked for deletion."
+msgstr ""
+
+msgid "ContainerRegistry|Tags with names matching this regex pattern will %{italicStart}be preserved:%{italicEnd}"
+msgstr ""
+
+msgid "ContainerRegistry|Tags with names matching this regex pattern will %{italicStart}expire:%{italicEnd}"
+msgstr ""
+
+msgid "ContainerRegistry|The Container Registry tag expiration and retention policies for this project have not been enabled."
msgstr ""
msgid "ContainerRegistry|The last tag related to this image was recently removed. This empty image and any associated data will be automatically removed as part of the regular Garbage Collection process. If you have any questions, contact your administrator."
@@ -5589,16 +5791,16 @@ msgstr ""
msgid "ContainerRegistry|There are no container images stored for this project"
msgstr ""
-msgid "ContainerRegistry|This Registry contains deleted image tag data. Remember to run %{docLinkStart}garbage collection%{docLinkEnd} to remove the stale data from storage."
+msgid "ContainerRegistry|There was an error during the deletion of this image repository, please try again."
msgstr ""
msgid "ContainerRegistry|This image has no active tags"
msgstr ""
-msgid "ContainerRegistry|We are having trouble connecting to Docker, which could be due to an issue with your project name or path. %{docLinkStart}More Information%{docLinkEnd}"
+msgid "ContainerRegistry|This image repository is scheduled for deletion"
msgstr ""
-msgid "ContainerRegistry|Wildcards such as %{codeStart}.*-stable%{codeEnd} or %{codeStart}production/.*%{codeEnd} are supported. To select all tags, use %{codeStart}.*%{codeEnd}"
+msgid "ContainerRegistry|We are having trouble connecting to Docker, which could be due to an issue with your project name or path. %{docLinkStart}More Information%{docLinkEnd}"
msgstr ""
msgid "ContainerRegistry|With the Container Registry, every project can have its own space to store its Docker images. %{docLinkStart}More Information%{docLinkEnd}"
@@ -5853,7 +6055,7 @@ msgstr ""
msgid "Could not delete chat nickname %{chat_name}."
msgstr ""
-msgid "Could not find design"
+msgid "Could not find design."
msgstr ""
msgid "Could not remove the trigger."
@@ -5868,6 +6070,9 @@ msgstr ""
msgid "Could not revoke personal access token %{personal_access_token_name}."
msgstr ""
+msgid "Could not revoke project access token %{project_access_token_name}."
+msgstr ""
+
msgid "Could not save group ID"
msgstr ""
@@ -5925,6 +6130,9 @@ msgstr ""
msgid "Create a new branch"
msgstr "Erstelle einen neuen Branch"
+msgid "Create a new deploy key for this project"
+msgstr ""
+
msgid "Create a new file as there are no files yet. Afterwards, you'll be able to commit your changes."
msgstr ""
@@ -6030,6 +6238,9 @@ msgstr "Projektlabel erstellen"
msgid "Create requirement"
msgstr ""
+msgid "Create snippet"
+msgstr ""
+
msgid "Create wildcard: %{searchTerm}"
msgstr ""
@@ -6051,6 +6262,9 @@ msgstr "Erstelle einen persönlichen Zugriffstoken"
msgid "Created"
msgstr "Erstellt"
+msgid "Created %{timestamp}"
+msgstr ""
+
msgid "Created At"
msgstr "Erstellt am"
@@ -6241,7 +6455,7 @@ msgid "Customize icon"
msgstr ""
msgid "Customize language and region related settings."
-msgstr ""
+msgstr "Sprach- und Region-bezogene Einstellungen anpassen."
msgid "Customize name"
msgstr ""
@@ -6407,6 +6621,9 @@ msgstr ""
msgid "CycleAnalytics|stage dropdown"
msgstr ""
+msgid "DAG"
+msgstr ""
+
msgid "DNS"
msgstr "DNS"
@@ -6510,10 +6727,10 @@ msgid "Default description template for merge requests"
msgstr ""
msgid "Default first day of the week"
-msgstr ""
+msgstr "Standardwert für den ersten Wochentag"
msgid "Default first day of the week in calendars and date pickers."
-msgstr ""
+msgstr "Standardwert für den ersten Wochentag im Kalender und in der Datumsauswahl."
msgid "Default issue template"
msgstr ""
@@ -6773,6 +6990,9 @@ msgstr "Bereitstellungsschlüssel"
msgid "Deploy key was successfully updated."
msgstr ""
+msgid "Deploy keys allow read-only or read-write (if enabled) access to your repository. Deploy keys can be used for CI, staging or production servers. You can create a deploy key or add an existing one."
+msgstr ""
+
msgid "Deploy progress not found. To see pods, ensure your environment matches %{linkStart}deploy board criteria%{linkEnd}."
msgstr ""
@@ -6926,6 +7146,9 @@ msgstr "Bereitgestellt für"
msgid "Deploying to"
msgstr "Bereitstellung für"
+msgid "Deployment Frequency"
+msgstr ""
+
msgid "Deployment|API"
msgstr ""
@@ -7444,9 +7667,6 @@ msgstr ""
msgid "Edit environment"
msgstr ""
-msgid "Edit epic description"
-msgstr ""
-
msgid "Edit file"
msgstr "Datei bearbeiten"
@@ -8068,9 +8288,6 @@ msgstr ""
msgid "Epics"
msgstr "Epics"
-msgid "Epics (Ultimate / Gold license only)"
-msgstr ""
-
msgid "Epics Roadmap"
msgstr "Epics Roadmap"
@@ -8080,6 +8297,9 @@ msgstr "Epics und Tickets"
msgid "Epics let you manage your portfolio of projects more efficiently and with less effort"
msgstr "Mit Epics kannst du deine Projekte effizienter und mit weniger Aufwand verwalten"
+msgid "Epics, Issues, and Merge Requests"
+msgstr ""
+
msgid "Epics|Add an epic"
msgstr ""
@@ -8131,6 +8351,9 @@ msgstr "Beim Abrufen untergeordneter Epics ist etwas schief gelaufen."
msgid "Epics|Something went wrong while fetching group epics."
msgstr ""
+msgid "Epics|Something went wrong while moving item."
+msgstr ""
+
msgid "Epics|Something went wrong while ordering item."
msgstr ""
@@ -8185,6 +8408,9 @@ msgstr "Fehler beim Abrufen der Label."
msgid "Error fetching network graph."
msgstr "Fehler beim Abrufen des Netzwerkgraphen."
+msgid "Error fetching payload data."
+msgstr ""
+
msgid "Error fetching projects"
msgstr ""
@@ -8194,9 +8420,6 @@ msgstr "Fehler beim Abrufen der Referenzen"
msgid "Error fetching the dependency list. Please check your network connection and try again."
msgstr ""
-msgid "Error fetching usage ping data."
-msgstr "Fehler beim Abrufen der Daten des Nutzungsberichts."
-
msgid "Error loading branch data. Please try again."
msgstr "Fehler beim Laden der Branch-Daten. Bitte versuche es erneut."
@@ -8338,6 +8561,9 @@ msgstr ""
msgid "Errors"
msgstr ""
+msgid "Errors:"
+msgstr ""
+
msgid "Estimated"
msgstr "Geschätzt"
@@ -8542,12 +8768,18 @@ msgstr "Erkunde öffentliche Gruppen"
msgid "Export as CSV"
msgstr ""
+msgid "Export group"
+msgstr ""
+
msgid "Export issues"
msgstr ""
msgid "Export project"
msgstr ""
+msgid "Export this group with all related data to a new GitLab instance. Once complete, you can import the data file from the \"New Group\" page."
+msgstr ""
+
msgid "Export this project with all its related data in order to move your project to a new GitLab instance. Once the export is finished, you can import the file from the \"New Project\" page."
msgstr ""
@@ -8617,9 +8849,6 @@ msgstr "Fehlgeschlagen"
msgid "Failed Jobs"
msgstr "Fehlgeschlagene Jobs"
-msgid "Failed create wiki"
-msgstr ""
-
msgid "Failed to add a Zoom meeting"
msgstr ""
@@ -8659,6 +8888,9 @@ msgstr ""
msgid "Failed to create resources"
msgstr ""
+msgid "Failed to create wiki"
+msgstr ""
+
msgid "Failed to delete board. Please try again."
msgstr ""
@@ -8668,7 +8900,7 @@ msgstr "Bereitstellung fehlgeschlagen für"
msgid "Failed to enqueue the rebase operation, possibly due to a long-lived transaction. Try again later."
msgstr ""
-msgid "Failed to find import label for jira import."
+msgid "Failed to find import label for Jira import."
msgstr ""
msgid "Failed to get ref."
@@ -8692,6 +8924,12 @@ msgstr ""
msgid "Failed to load groups & users."
msgstr ""
+msgid "Failed to load labels. Please try again."
+msgstr ""
+
+msgid "Failed to load milestones. Please try again."
+msgstr ""
+
msgid "Failed to load related branches"
msgstr ""
@@ -9067,8 +9305,11 @@ msgstr "Filter nach Commit-Nachricht"
msgid "Filter by milestone name"
msgstr ""
-msgid "Filter by name..."
-msgstr "Nach Namen filtern..."
+msgid "Filter by name"
+msgstr ""
+
+msgid "Filter by status"
+msgstr ""
msgid "Filter by two-factor authentication"
msgstr ""
@@ -9079,12 +9320,18 @@ msgstr ""
msgid "Filter projects"
msgstr ""
+msgid "Filter results"
+msgstr ""
+
msgid "Filter results by group"
msgstr ""
msgid "Filter results by project"
msgstr ""
+msgid "Filter results..."
+msgstr ""
+
msgid "Filter your projects by name"
msgstr ""
@@ -9131,7 +9378,7 @@ msgid "First Seen"
msgstr "Erstmals gesehen"
msgid "First day of the week"
-msgstr ""
+msgstr "Erster Wochentag"
msgid "First name"
msgstr ""
@@ -9373,11 +9620,11 @@ msgstr ""
msgid "GeoNodes|Checksummed"
msgstr "Prüfsumme verifiziert"
-msgid "GeoNodes|Container repositories"
+msgid "GeoNodes|Consult Geo troubleshooting information"
msgstr ""
-msgid "GeoNodes|Data is out of date from %{timeago}"
-msgstr "Daten sind veraltet von %{timeago}"
+msgid "GeoNodes|Container repositories"
+msgstr ""
msgid "GeoNodes|Data replication lag"
msgstr "Datenreplikationsverzögerung"
@@ -9418,6 +9665,9 @@ msgstr "Letzte Event-ID, die vom Cursor verarbeitet wurde"
msgid "GeoNodes|Last event ID seen from primary"
msgstr "Letzte Ereignis-ID vom Primärknoten aus gesehen"
+msgid "GeoNodes|Learn more about Geo node statuses"
+msgstr ""
+
msgid "GeoNodes|Loading nodes"
msgstr "Knoten werden geladen"
@@ -9433,6 +9683,9 @@ msgstr ""
msgid "GeoNodes|Node was successfully removed."
msgstr "Knoten wurde erfolgreich entfernt."
+msgid "GeoNodes|Node's status was updated %{timeAgo}."
+msgstr ""
+
msgid "GeoNodes|Not checksummed"
msgstr "Keine Prüfsumme"
@@ -9463,8 +9716,11 @@ msgstr "Repository-Prüfsummenfortschritt"
msgid "GeoNodes|Repository verification progress"
msgstr "Repository-Üerprüfungsfortschritt"
-msgid "GeoNodes|Selective"
-msgstr "Selektiv"
+msgid "GeoNodes|Selective (%{syncLabel})"
+msgstr ""
+
+msgid "GeoNodes|Selective synchronization"
+msgstr ""
msgid "GeoNodes|Something went wrong while changing node status"
msgstr "Etwas ist schiefgelaufen während der Änderung des Knotenstatuses"
@@ -9490,6 +9746,9 @@ msgstr "Unbenutzte Slots"
msgid "GeoNodes|Unverified"
msgstr "Nicht verifiziert"
+msgid "GeoNodes|Updated %{timeAgo}"
+msgstr ""
+
msgid "GeoNodes|Used slots"
msgstr "Benutzte Slots"
@@ -9529,17 +9788,17 @@ msgstr ""
msgid "Geo|All"
msgstr "Alle"
+msgid "Geo|All %{replicable_type}"
+msgstr ""
+
msgid "Geo|All projects"
msgstr "Alle Projekte"
-msgid "Geo|All projects are being scheduled for re-sync"
-msgstr "Alle Projekte sind für eine erneute Synchronisierung eingeplant"
-
-msgid "Geo|All projects are being scheduled for re-verify"
+msgid "Geo|All projects are being scheduled for resync"
msgstr ""
-msgid "Geo|Batch operations"
-msgstr "Stapelverarbeitung"
+msgid "Geo|All projects are being scheduled for reverify"
+msgstr ""
msgid "Geo|Could not remove tracking entry for an existing project."
msgstr "Tracking-Eintrag für ein vorhandenes Projekt konnte nicht entfernt werden."
@@ -9550,9 +9809,15 @@ msgstr ""
msgid "Geo|Failed"
msgstr "Fehlgeschlagen"
+msgid "Geo|Filter by status"
+msgstr ""
+
msgid "Geo|Geo Status"
msgstr "Geo-Status"
+msgid "Geo|In progress"
+msgstr ""
+
msgid "Geo|In sync"
msgstr "Wird synchronisiert"
@@ -9577,9 +9842,6 @@ msgstr "Nächste Synchronisierung geplant um"
msgid "Geo|Not synced yet"
msgstr "Noch nicht synchronisiert"
-msgid "Geo|Pending"
-msgstr "Ausstehend"
-
msgid "Geo|Pending synchronization"
msgstr "Ausstehende Synchronisation"
@@ -9616,8 +9878,8 @@ msgstr ""
msgid "Geo|Resync"
msgstr "Resynchronisation"
-msgid "Geo|Resync all projects"
-msgstr "Alle Projekte erneut synchronisieren"
+msgid "Geo|Resync all"
+msgstr ""
msgid "Geo|Retry count"
msgstr "Wiederholversuch"
@@ -9625,7 +9887,7 @@ msgstr "Wiederholversuch"
msgid "Geo|Reverify"
msgstr ""
-msgid "Geo|Reverify all projects"
+msgid "Geo|Reverify all"
msgstr ""
msgid "Geo|Status"
@@ -9967,9 +10229,6 @@ msgstr ""
msgid "Go to file"
msgstr ""
-msgid "Go to file (MRs only)"
-msgstr ""
-
msgid "Go to file permalink (while viewing a file)"
msgstr ""
@@ -10414,6 +10673,9 @@ msgstr ""
msgid "GroupSettings|Disable group mentions"
msgstr ""
+msgid "GroupSettings|Export group"
+msgstr ""
+
msgid "GroupSettings|If the parent group's visibility is lower than the group current visibility, visibility levels for subgroups and projects will be changed to match the new parent group's visibility."
msgstr ""
@@ -10492,6 +10754,9 @@ msgstr "Gruppen"
msgid "Groups (%{count})"
msgstr ""
+msgid "Groups (%{groups})"
+msgstr ""
+
msgid "Groups can also be nested by creating %{subgroup_docs_link_start}subgroups%{subgroup_docs_link_end}."
msgstr "Gruppen können durch %{subgroup_docs_link_start}Untergruppen%{subgroup_docs_link_end} verschachtelt werden."
@@ -10757,15 +11022,12 @@ msgstr "ID"
msgid "ID:"
msgstr ""
-msgid "IDE|Allow live previews of JavaScript projects in the Web IDE using CodeSandbox client side evaluation."
-msgstr "Ermöglicht Live-Vorschauen von JavaScript-Projekten in der Web-IDE mithilfe der clientseitigen CodeSandbox-Auswertung."
+msgid "IDE|Allow live previews of JavaScript projects in the Web IDE using CodeSandbox Live Preview."
+msgstr ""
msgid "IDE|Back"
msgstr "Zurück"
-msgid "IDE|Client side evaluation"
-msgstr "Clientseitige Auswertung"
-
msgid "IDE|Commit"
msgstr "Commit"
@@ -10829,6 +11091,9 @@ msgstr ""
msgid "If any job surpasses this timeout threshold, it will be marked as failed. Human readable time input language is accepted like \"1 hour\". Values without specification represent seconds."
msgstr ""
+msgid "If blank, set allowable lifetime to %{instance_level_policy_in_words}, as defined by the instance admin. Once set, existing tokens for users in this group may be revoked."
+msgstr ""
+
msgid "If checked, group owners can manage LDAP group links and LDAP member overrides"
msgstr ""
@@ -10868,6 +11133,9 @@ msgstr ""
msgid "If you lose your recovery codes you can generate new ones, invalidating all previous codes."
msgstr ""
+msgid "If you reach 100%% storage capacity, you will not be able to: %{base_message}"
+msgstr ""
+
msgid "If your HTTP repository is not publicly accessible, add your credentials."
msgstr ""
@@ -10979,6 +11247,9 @@ msgstr "Importiere Repositories von GitHub"
msgid "Import repository"
msgstr "Repository importieren"
+msgid "Import started by: %{importInitiator}"
+msgstr ""
+
msgid "Import tasks"
msgstr ""
@@ -11045,6 +11316,9 @@ msgstr ""
msgid "In order to tailor your experience with GitLab we<br>would like to know a bit more about you."
msgstr ""
+msgid "In progress"
+msgstr ""
+
msgid "In the next step, you'll be able to select the projects you want to import."
msgstr "Im nächsten Schritt kannst du die Projekte auswählen, die du importieren möchtest."
@@ -11392,15 +11666,15 @@ msgstr ""
msgid "Issues"
msgstr "Tickets"
-msgid "Issues / Merge Requests"
-msgstr ""
-
msgid "Issues Analytics"
msgstr ""
msgid "Issues Rate Limits"
msgstr ""
+msgid "Issues and Merge Requests"
+msgstr ""
+
msgid "Issues can be bugs, tasks or ideas to be discussed. Also, issues are searchable and filterable."
msgstr "Tickets können Bugs, Tasks oder zu diskutierende Ideen sein. Tickets sind darüber hinaus auch durchsuchbar und filterbar."
@@ -11422,11 +11696,11 @@ msgstr ""
msgid "IssuesAnalytics|Avg/Month:"
msgstr ""
-msgid "IssuesAnalytics|Issues created"
+msgid "IssuesAnalytics|Issues opened"
msgstr ""
-msgid "IssuesAnalytics|Issues created per month"
-msgstr "Erstellte Tickets pro Monat"
+msgid "IssuesAnalytics|Issues opened per month"
+msgstr ""
msgid "IssuesAnalytics|Last 12 months"
msgstr "Letzten 12 Monate"
@@ -11479,6 +11753,9 @@ msgstr ""
msgid "Jira integration not configured."
msgstr ""
+msgid "Jira project: %{importProject}"
+msgstr ""
+
msgid "JiraService|Events for %{noteable_model_name} are disabled."
msgstr ""
@@ -11771,7 +12048,7 @@ msgid "Labels|and %{count} more"
msgstr ""
msgid "Language"
-msgstr ""
+msgstr "Sprache"
msgid "Large File Storage"
msgstr "Large File Storage"
@@ -11868,6 +12145,9 @@ msgstr ""
msgid "Lead"
msgstr ""
+msgid "Lead Time"
+msgstr ""
+
msgid "Learn GitLab"
msgstr ""
@@ -11880,6 +12160,9 @@ msgstr ""
msgid "Learn how to %{no_packages_link_start}publish and share your packages%{no_packages_link_end} with GitLab."
msgstr ""
+msgid "Learn how to enable synchronization"
+msgstr ""
+
msgid "Learn more"
msgstr "Mehr Informationen"
@@ -12211,9 +12494,6 @@ msgstr "Liste deine Bitbucket-Server-Repositorys auf"
msgid "Live preview"
msgstr "Live-Vorschau"
-msgid "Load more vulnerabilities"
-msgstr ""
-
msgid "Loading"
msgstr ""
@@ -12242,7 +12522,7 @@ msgid "Loading…"
msgstr ""
msgid "Localization"
-msgstr ""
+msgstr "Lokalisierung"
msgid "Location"
msgstr ""
@@ -12364,9 +12644,6 @@ msgstr ""
msgid "Manage"
msgstr ""
-msgid "Manage Git repositories with fine-grained access controls that keep your code secure. Perform code reviews and enhance collaboration with merge requests. Each project can also have an issue tracker and a wiki."
-msgstr "Verwalte Git-Repositories mit detaillierten Zugriffskontrollen, die deinen Code schützen. Führe Codeüberprüfungen durch und verbessere die Zusammenarbeit mit Merge-Requests. Jedes Projekt kann auch ein Ticketsystem und ein Wiki haben."
-
msgid "Manage Web IDE features"
msgstr "Web-IDE Funktionen verwalten"
@@ -12397,6 +12674,9 @@ msgstr ""
msgid "Manage your license"
msgstr ""
+msgid "Managed Account"
+msgstr ""
+
msgid "Manifest"
msgstr "Manifest"
@@ -12661,6 +12941,9 @@ msgstr "Merge_Request erstellt"
msgid "Merge Requests in Review"
msgstr ""
+msgid "Merge automatically (%{strategy})"
+msgstr ""
+
msgid "Merge commit message"
msgstr ""
@@ -12814,6 +13097,12 @@ msgstr "Merged"
msgid "Merged branches are being deleted. This can take some time depending on the number of branches. Please refresh the page to see changes."
msgstr ""
+msgid "Merged this merge request."
+msgstr ""
+
+msgid "Merges this merge request immediately."
+msgstr ""
+
msgid "Merges this merge request when the pipeline succeeds."
msgstr ""
@@ -12921,12 +13210,15 @@ msgid_plural "Metrics|Edit metrics"
msgstr[0] ""
msgstr[1] ""
-msgid "Metrics|Environment"
-msgstr "Umgebung"
+msgid "Metrics|Expand panel"
+msgstr ""
msgid "Metrics|For grouping similar metrics"
msgstr "Zum Gruppieren ähnlicher Metriken"
+msgid "Metrics|Go back (Esc)"
+msgstr ""
+
msgid "Metrics|Invalid time range, please verify."
msgstr ""
@@ -12957,9 +13249,6 @@ msgstr "Prometheus Query-Dokumentation"
msgid "Metrics|Refresh dashboard"
msgstr ""
-msgid "Metrics|Show last"
-msgstr ""
-
msgid "Metrics|There was an error creating the dashboard."
msgstr ""
@@ -13038,6 +13327,9 @@ msgstr ""
msgid "Microsoft Azure"
msgstr ""
+msgid "Middleman project with Static Site Editor support"
+msgstr ""
+
msgid "Migrated %{success_count}/%{total_count} files."
msgstr ""
@@ -13170,7 +13462,7 @@ msgid "Modify merge commit"
msgstr ""
msgid "Monday"
-msgstr ""
+msgstr "Montag"
msgid "Monitor your errors by integrating with Sentry."
msgstr ""
@@ -13483,10 +13775,10 @@ msgstr ""
msgid "Next"
msgstr ""
-msgid "Next file in diff (MRs only)"
+msgid "Next file in diff"
msgstr ""
-msgid "Next unresolved discussion (MRs only)"
+msgid "Next unresolved discussion"
msgstr ""
msgid "Nickname"
@@ -13546,6 +13838,9 @@ msgstr "Keine Änderungen"
msgid "No changes between %{ref_start}%{source_branch}%{ref_end} and %{ref_start}%{target_branch}%{ref_end}"
msgstr ""
+msgid "No child epics match applied filters"
+msgstr ""
+
msgid "No connection could be made to a Gitaly Server, please check your logs!"
msgstr "Es konnte keine Verbindung zu einem Gitaly-Server hergestellt werden. Bitte überprüfe deine Protokolle!"
@@ -13684,15 +13979,6 @@ msgstr ""
msgid "No thanks, don't show this again"
msgstr ""
-msgid "No vulnerabilities found for this group"
-msgstr ""
-
-msgid "No vulnerabilities found for this pipeline"
-msgstr ""
-
-msgid "No vulnerabilities found for this project"
-msgstr ""
-
msgid "No vulnerabilities present"
msgstr ""
@@ -13816,6 +14102,9 @@ msgstr ""
msgid "Nothing to preview."
msgstr ""
+msgid "Nothing to synchronize"
+msgstr ""
+
msgid "Notification events"
msgstr "Benachrichtigungsereignisse"
@@ -13987,6 +14276,9 @@ msgstr ""
msgid "Once removed, the fork relationship cannot be restored and you will no longer be able to send merge requests to the source."
msgstr ""
+msgid "Once the exported file is ready you can download it from this page."
+msgstr ""
+
msgid "Once the exported file is ready, you will receive a notification email with a download link, or you can download it from this page."
msgstr ""
@@ -14088,9 +14380,6 @@ msgstr ""
msgid "Open sidebar"
msgstr "Seitenleiste öffnen"
-msgid "Open source software to collaborate on code"
-msgstr "Open-Source-Software für die Zusammenarbeit an Code"
-
msgid "Open: %{openIssuesCount}"
msgstr ""
@@ -14193,6 +14482,9 @@ msgstr ""
msgid "OutdatedBrowser|You can provide feedback %{feedback_link_start}on this issue%{feedback_link_end} or via your usual support channels."
msgstr ""
+msgid "Overridden"
+msgstr ""
+
msgid "Overview"
msgstr "Ãœbersicht"
@@ -14244,6 +14536,9 @@ msgstr ""
msgid "PackageRegistry|Conan Command"
msgstr "Conan-Befehl"
+msgid "PackageRegistry|Copy .pypirc content"
+msgstr ""
+
msgid "PackageRegistry|Copy Conan Command"
msgstr "Conan-Befehl kopieren"
@@ -14265,6 +14560,9 @@ msgstr ""
msgid "PackageRegistry|Copy NuGet Setup Command"
msgstr ""
+msgid "PackageRegistry|Copy Pip command"
+msgstr ""
+
msgid "PackageRegistry|Copy and paste this inside your %{codeStart}pom.xml%{codeEnd} %{codeStart}dependencies%{codeEnd} block."
msgstr ""
@@ -14298,6 +14596,12 @@ msgstr ""
msgid "PackageRegistry|For more information on the NuGet registry, %{linkStart}see the documentation%{linkEnd}."
msgstr ""
+msgid "PackageRegistry|For more information on the PyPi registry, %{linkStart}see the documentation%{linkEnd}."
+msgstr ""
+
+msgid "PackageRegistry|If you haven't already done so, you will need to add the below to your %{codeStart}.pypirc%{codeEnd} file."
+msgstr ""
+
msgid "PackageRegistry|If you haven't already done so, you will need to add the below to your %{codeStart}pom.xml%{codeEnd} file."
msgstr ""
@@ -14307,6 +14611,9 @@ msgstr ""
msgid "PackageRegistry|Learn how to %{noPackagesLinkStart}publish and share your packages%{noPackagesLinkEnd} with GitLab."
msgstr ""
+msgid "PackageRegistry|Manually Published"
+msgstr ""
+
msgid "PackageRegistry|Maven"
msgstr ""
@@ -14325,12 +14632,18 @@ msgstr ""
msgid "PackageRegistry|NuGet Command"
msgstr ""
+msgid "PackageRegistry|Pip Command"
+msgstr ""
+
msgid "PackageRegistry|Pipeline %{linkStart}%{linkEnd} triggered %{timestamp} by %{author}"
msgstr ""
msgid "PackageRegistry|Published to the repository at %{timestamp}"
msgstr ""
+msgid "PackageRegistry|PyPi"
+msgstr ""
+
msgid "PackageRegistry|Registry Setup"
msgstr ""
@@ -14367,6 +14680,9 @@ msgstr ""
msgid "PackageRegistry|npm"
msgstr ""
+msgid "PackageRegistry|published by %{author}"
+msgstr ""
+
msgid "PackageRegistry|yarn"
msgstr ""
@@ -14382,6 +14698,9 @@ msgstr ""
msgid "PackageType|NuGet"
msgstr ""
+msgid "PackageType|PyPi"
+msgstr ""
+
msgid "Packages"
msgstr "Pakete"
@@ -14475,6 +14794,9 @@ msgstr ""
msgid "Past due"
msgstr ""
+msgid "Paste a machine public key here. Read more about how to generate it"
+msgstr ""
+
msgid "Paste a machine public key here. Read more about how to generate it %{link_start}here%{link_end}"
msgstr ""
@@ -14490,9 +14812,6 @@ msgstr ""
msgid "Path"
msgstr ""
-msgid "Path, transfer, remove"
-msgstr "Pfad, übertragen, entfernen"
-
msgid "Path:"
msgstr "Pfad:"
@@ -14523,8 +14842,8 @@ msgstr ""
msgid "Percentage"
msgstr ""
-msgid "Perform advanced options such as changing path, transferring, or removing the group."
-msgstr "Führe erweiterte Optionen aus, wie z. B. das Ändern des Pfades, Übertragen oder Entfernen einer Gruppe."
+msgid "Perform advanced options such as changing path, transferring, exporting, or removing the group."
+msgstr ""
msgid "Perform common operations on GitLab project"
msgstr ""
@@ -14604,9 +14923,6 @@ msgstr "Zeitpläne der Pipeline"
msgid "Pipeline minutes quota"
msgstr ""
-msgid "Pipeline quota"
-msgstr "Pipeline-Kontingent"
-
msgid "Pipeline subscriptions"
msgstr ""
@@ -14616,6 +14932,9 @@ msgstr "Pipeline-Trigger"
msgid "Pipeline: %{status}"
msgstr ""
+msgid "PipelineCharts|CI / CD Analytics"
+msgstr ""
+
msgid "PipelineCharts|Failed:"
msgstr "Fehlgeschlagen:"
@@ -14694,15 +15013,6 @@ msgstr ""
msgid "Pipelines settings for '%{project_name}' were successfully updated."
msgstr ""
-msgid "Pipelines| to purchase more minutes."
-msgstr ""
-
-msgid "Pipelines|%{namespace_name} has exceeded its pipeline minutes quota."
-msgstr ""
-
-msgid "Pipelines|%{namespace_name} has less than %{notification_level}%% of CI minutes available."
-msgstr ""
-
msgid "Pipelines|API"
msgstr ""
@@ -14724,12 +15034,15 @@ msgstr "Kontinuierliche Integration (CI) kann dabei helfen, Fehler zu erkennen,
msgid "Pipelines|Get started with Pipelines"
msgstr "Erste Schritte mit Pipelines"
-msgid "Pipelines|Loading Pipelines"
-msgstr "Pipelines laden"
+msgid "Pipelines|Group %{namespace_name} has %{percentage}%% or less Shared Runner Pipeline minutes remaining. Once it runs out, no new jobs or pipelines in its projects will run."
+msgstr ""
-msgid "Pipelines|Pipelines will not run anymore on shared Runners."
+msgid "Pipelines|Group %{namespace_name} has exceeded its pipeline minutes quota. Unless you buy additional pipeline minutes, no new jobs or pipelines in its projects will run."
msgstr ""
+msgid "Pipelines|Loading Pipelines"
+msgstr "Pipelines laden"
+
msgid "Pipelines|Project cache successfully reset."
msgstr "Der Projekt-Cache wurde erfolgreich zurückgesetzt."
@@ -14958,6 +15271,9 @@ msgstr ""
msgid "Please select"
msgstr ""
+msgid "Please select a Jira project"
+msgstr ""
+
msgid "Please select a country"
msgstr ""
@@ -15003,6 +15319,9 @@ msgstr "Bitte warte, während wir uns mit deinem Repository verbinden. Aktualisi
msgid "Please wait while we import the repository for you. Refresh at will."
msgstr "Warte bitte, während wir das Repository für dich importieren. Aktualisiere nach Belieben."
+msgid "Plugins directory is deprecated and will be removed in 14.0. Please move this file into /file_hooks directory."
+msgstr ""
+
msgid "Pod does not exist"
msgstr ""
@@ -15082,7 +15401,7 @@ msgid "Preferences|These settings will update how dates and times are displayed
msgstr ""
msgid "Preferences|This feature is experimental and translations are not complete yet"
-msgstr ""
+msgstr "Dieses Feature ist experimentell und die Ãœbersetzungen sind noch nicht abgeschlossen"
msgid "Preferences|This setting allows you to customize the appearance of the syntax."
msgstr ""
@@ -15120,7 +15439,7 @@ msgstr ""
msgid "Prevent users from changing their profile name"
msgstr ""
-msgid "Prevent users from modifing merge request approvers list"
+msgid "Prevent users from modifying merge request approvers list"
msgstr ""
msgid "Prevent users from performing write operations on GitLab while performing maintenance."
@@ -15141,10 +15460,10 @@ msgstr "Vorschau der Nutzdaten"
msgid "Previous Artifacts"
msgstr ""
-msgid "Previous file in diff (MRs only)"
+msgid "Previous file in diff"
msgstr ""
-msgid "Previous unresolved discussion (MRs only)"
+msgid "Previous unresolved discussion"
msgstr ""
msgid "Primary"
@@ -15177,6 +15496,9 @@ msgstr ""
msgid "Private profile"
msgstr ""
+msgid "Private projects Minutes cost factor"
+msgstr ""
+
msgid "Private projects can be created in your personal namespace with:"
msgstr "Private Projekte können in deinem persönlichen Namensraum erstellt werden:"
@@ -16005,6 +16327,9 @@ msgstr ""
msgid "ProjectSettings|With GitLab Pages you can host your static websites on GitLab"
msgstr "Mit GitLab Pages kannst du deine statischen Webseiten auf GitLab hosten"
+msgid "ProjectSettings|With Metrics Dashboard you can visualize this project performance metrics"
+msgstr ""
+
msgid "ProjectTemplates|.NET Core"
msgstr ""
@@ -16068,6 +16393,9 @@ msgstr ""
msgid "ProjectTemplates|Spring"
msgstr ""
+msgid "ProjectTemplates|Static Site Editor/Middleman"
+msgstr ""
+
msgid "ProjectTemplates|iOS (Swift)"
msgstr ""
@@ -16287,15 +16615,15 @@ msgstr "Prometheus-API-Basis-URL, wie http://prometheus.example.com/"
msgid "PrometheusService|Prometheus is being automatically managed on your clusters"
msgstr "Prometheus wird automatisch auf deinen Clustern verwaltet"
+msgid "PrometheusService|Select the Active checkbox to override the Auto Configuration with custom settings. If unchecked, Auto Configuration settings are used."
+msgstr ""
+
msgid "PrometheusService|These metrics will only be monitored after your first deployment to an environment"
msgstr "Diese Metriken werden erst nach deiner ersten Bereitstellung in einer Umgebung überwacht"
msgid "PrometheusService|Time-series monitoring service"
msgstr "Zeitreihen-Monitoring-Service"
-msgid "PrometheusService|To enable manual configuration, uninstall Prometheus from your clusters"
-msgstr "Deinstalliere Prometheus aus deinen Clustern, um die manuelle Konfiguration zu aktivieren"
-
msgid "PrometheusService|To enable the installation of Prometheus on your clusters, deactivate the manual configuration below"
msgstr "Deaktiviere unten die manuelle Konfiguration, um die Installation von Prometheus auf deinen Clustern zu ermöglichen"
@@ -16479,6 +16807,9 @@ msgstr "Der Schutz deiner Umgebung wurde aufgehoben"
msgid "Protip:"
msgstr "Protipp:"
+msgid "Protocol"
+msgstr ""
+
msgid "Provider"
msgstr "Provider"
@@ -16503,6 +16834,9 @@ msgstr ""
msgid "Public pipelines"
msgstr "Öffentliche Pipelines"
+msgid "Public projects Minutes cost factor"
+msgstr ""
+
msgid "Pull"
msgstr "Pull"
@@ -16703,6 +17037,9 @@ msgid_plural "Refreshing in %d seconds to show the updated status..."
msgstr[0] "In einer Sekunde aktualisieren, um den aktualisierten Status anzuzeigen..."
msgstr[1] "In %d Sekunden aktualisieren, um den aktualisierten Status anzuzeigen..."
+msgid "Regenerate export"
+msgstr ""
+
msgid "Regenerate instance ID"
msgstr ""
@@ -17080,9 +17417,29 @@ msgstr "Statusbericht"
msgid "Reports|%{combinedString} and %{resolvedString}"
msgstr ""
+msgid "Reports|Accessibility scanning detected %d issue for the source branch only"
+msgid_plural "Reports|Accessibility scanning detected %d issues for the source branch only"
+msgstr[0] ""
+msgstr[1] ""
+
+msgid "Reports|Accessibility scanning detected no issues for the source branch only"
+msgstr ""
+
+msgid "Reports|Accessibility scanning failed loading results"
+msgstr ""
+
+msgid "Reports|Accessibility scanning results are being parsed"
+msgstr ""
+
msgid "Reports|Actions"
msgstr "Aktionen"
+msgid "Reports|An error occured while loading report"
+msgstr ""
+
+msgid "Reports|An error occurred while loading %{name} results"
+msgstr ""
+
msgid "Reports|Class"
msgstr "Klasse"
@@ -17188,7 +17545,7 @@ msgstr ""
msgid "Requested %{time_ago}"
msgstr ""
-msgid "Requested design version does not exist"
+msgid "Requested design version does not exist."
msgstr ""
msgid "Requested states are invalid"
@@ -17372,6 +17729,9 @@ msgstr "Replikation fortsetzen"
msgid "Resync"
msgstr ""
+msgid "Resync all"
+msgstr ""
+
msgid "Resync all %{replicableType}"
msgstr ""
@@ -17440,6 +17800,9 @@ msgstr ""
msgid "Revoked personal access token %{personal_access_token_name}!"
msgstr ""
+msgid "Revoked project access token %{project_access_token_name}!"
+msgstr ""
+
msgid "RightSidebar|adding a"
msgstr ""
@@ -17488,6 +17851,9 @@ msgstr "Runner führt Jobs von zugewiesenen Projekten aus"
msgid "Runner token"
msgstr "Runner-Token"
+msgid "Runner tokens"
+msgstr ""
+
msgid "Runner was not updated."
msgstr ""
@@ -17542,6 +17908,9 @@ msgstr "SAML SSO"
msgid "SAML SSO for %{group_name}"
msgstr "SAML SSO für %{group_name}"
+msgid "SAML discovery tokens"
+msgstr ""
+
msgid "SAML for %{group_name}"
msgstr ""
@@ -17576,7 +17945,7 @@ msgid "SSL Verification:"
msgstr ""
msgid "Saturday"
-msgstr ""
+msgstr "Samstag"
msgid "Save"
msgstr "Speichern"
@@ -17626,12 +17995,18 @@ msgstr "Plane eine neue Pipeline"
msgid "Scheduled"
msgstr "Geplant"
+msgid "Scheduled to merge this merge request (%{strategy})."
+msgstr ""
+
msgid "Scheduled to merge this merge request when the pipeline succeeds."
msgstr ""
msgid "Schedules"
msgstr "Zeitpläne"
+msgid "Schedules to merge this merge request (%{strategy})."
+msgstr ""
+
msgid "Scheduling"
msgstr ""
@@ -17689,6 +18064,9 @@ msgstr "Branches durchsuchen"
msgid "Search branches and tags"
msgstr "Suche nach Branches und Tags"
+msgid "Search by author"
+msgstr ""
+
msgid "Search files"
msgstr "Dateien durchsuchen"
@@ -17834,11 +18212,6 @@ msgid_plural "SearchResults|snippets"
msgstr[0] ""
msgstr[1] ""
-msgid "SearchResults|snippet result"
-msgid_plural "SearchResults|snippet results"
-msgstr[0] ""
-msgstr[1] ""
-
msgid "SearchResults|user"
msgid_plural "SearchResults|users"
msgstr[0] ""
@@ -17849,6 +18222,9 @@ msgid_plural "SearchResults|wiki results"
msgstr[0] ""
msgstr[1] ""
+msgid "Searching by both author and message is currently not supported."
+msgstr ""
+
msgid "Seat Link"
msgstr ""
@@ -17876,184 +18252,232 @@ msgstr ""
msgid "Security Dashboard"
msgstr "Sicherheits-Dashboard"
-msgid "Security Dashboard|Error fetching the vulnerability counts. Please check your network connection and try again."
+msgid "Security configuration help link"
msgstr ""
-msgid "Security Dashboard|Error fetching the vulnerability list. Please check your network connection and try again."
+msgid "Security dashboard"
msgstr ""
-msgid "Security Dashboard|Issue Created"
-msgstr "Ticket erstellt"
+msgid "Security report is out of date. Please update your branch with the latest changes from the target branch (%{targetBranchName})"
+msgstr ""
-msgid "Security Reports|Comment added to '%{vulnerabilityName}'"
+msgid "Security report is out of date. Run %{newPipelineLinkStart}a new pipeline%{newPipelineLinkEnd} for the target branch (%{targetBranchName})"
msgstr ""
-msgid "Security Reports|Comment deleted on '%{vulnerabilityName}'"
+msgid "SecurityConfiguration|Configured"
msgstr ""
-msgid "Security Reports|Comment edited on '%{vulnerabilityName}'"
+msgid "SecurityConfiguration|Feature"
msgstr ""
-msgid "Security Reports|Create issue"
-msgstr "Ticket erstellen"
+msgid "SecurityConfiguration|Feature documentation for %{featureName}"
+msgstr ""
-msgid "Security Reports|Dismiss vulnerability"
+msgid "SecurityConfiguration|Not yet configured"
msgstr ""
-msgid "Security Reports|Dismissed '%{vulnerabilityName}'"
+msgid "SecurityConfiguration|Secure features"
msgstr ""
-msgid "Security Reports|Dismissed '%{vulnerabilityName}'. Turn off the hide dismissed toggle to view."
+msgid "SecurityConfiguration|Status"
msgstr ""
-msgid "Security Reports|Either you don't have permission to view this dashboard or the dashboard has not been setup. Please check your permission settings with your administrator or check your dashboard configurations to proceed."
+msgid "SecurityReports|%{firstProject} and %{secondProject}"
msgstr ""
-msgid "Security Reports|Learn more about setting up your dashboard"
+msgid "SecurityReports|%{firstProject}, %{secondProject}, and %{rest}"
msgstr ""
-msgid "Security Reports|More info"
-msgstr "Mehr Informationen"
+msgid "SecurityReports|Add a project to your dashboard"
+msgstr ""
-msgid "Security Reports|Oops, something doesn't seem right."
+msgid "SecurityReports|Add or remove projects from your dashboard"
msgstr ""
-msgid "Security Reports|Security reports can only be accessed by authorized users."
+msgid "SecurityReports|Add projects"
msgstr ""
-msgid "Security Reports|There was an error adding the comment."
+msgid "SecurityReports|Comment added to '%{vulnerabilityName}'"
msgstr ""
-msgid "Security Reports|There was an error creating the issue."
-msgstr "Fehler beim Erstellen des Tickets."
+msgid "SecurityReports|Comment deleted on '%{vulnerabilityName}'"
+msgstr ""
-msgid "Security Reports|There was an error creating the merge request."
+msgid "SecurityReports|Comment edited on '%{vulnerabilityName}'"
msgstr ""
-msgid "Security Reports|There was an error deleting the comment."
+msgid "SecurityReports|Create issue"
msgstr ""
-msgid "Security Reports|There was an error dismissing the vulnerabilities."
+msgid "SecurityReports|Dismiss Selected"
msgstr ""
-msgid "Security Reports|There was an error dismissing the vulnerability."
+msgid "SecurityReports|Dismiss vulnerability"
msgstr ""
-msgid "Security Reports|There was an error reverting the dismissal."
+msgid "SecurityReports|Dismissed '%{vulnerabilityName}'"
msgstr ""
-msgid "Security Reports|There was an error reverting this dismissal."
+msgid "SecurityReports|Dismissed '%{vulnerabilityName}'. Turn off the hide dismissed toggle to view."
msgstr ""
-msgid "Security Reports|Undo dismiss"
+msgid "SecurityReports|Each vulnerability now has a unique page that can be directly linked to, shared, referenced, and tracked as the single source of truth. Vulnerability occurrences also persist across scanner runs, which improves tracking and visibility and reduces duplicates between scans."
msgstr ""
-msgid "Security Reports|You do not have sufficient permissions to access this report"
+msgid "SecurityReports|Edit dashboard"
msgstr ""
-msgid "Security Reports|You must sign in as an authorized user to see this report"
+msgid "SecurityReports|Either you don't have permission to view this dashboard or the dashboard has not been setup. Please check your permission settings with your administrator or check your dashboard configurations to proceed."
msgstr ""
-msgid "Security configuration help link"
+msgid "SecurityReports|Error fetching the vulnerability counts. Please check your network connection and try again."
msgstr ""
-msgid "Security dashboard"
+msgid "SecurityReports|Error fetching the vulnerability list. Please check your network connection and try again."
msgstr ""
-msgid "Security report is out of date. Please update your branch with the latest changes from the target branch (%{targetBranchName})"
+msgid "SecurityReports|False positive"
msgstr ""
-msgid "Security report is out of date. Run %{newPipelineLinkStart}a new pipeline%{newPipelineLinkEnd} for the target branch (%{targetBranchName})"
+msgid "SecurityReports|Hide dismissed"
msgstr ""
-msgid "SecurityConfiguration|Configured"
+msgid "SecurityReports|Introducing standalone vulnerabilities"
msgstr ""
-msgid "SecurityConfiguration|Feature"
+msgid "SecurityReports|Issue Created"
msgstr ""
-msgid "SecurityConfiguration|Feature documentation for %{featureName}"
+msgid "SecurityReports|Learn More"
msgstr ""
-msgid "SecurityConfiguration|Not yet configured"
+msgid "SecurityReports|Learn more about setting up your dashboard"
msgstr ""
-msgid "SecurityConfiguration|Secure features"
+msgid "SecurityReports|Load more vulnerabilities"
msgstr ""
-msgid "SecurityConfiguration|Status"
+msgid "SecurityReports|Monitor vulnerabilities in your code"
+msgstr ""
+
+msgid "SecurityReports|More info"
+msgstr ""
+
+msgid "SecurityReports|More information"
+msgstr ""
+
+msgid "SecurityReports|No vulnerabilities found for dashboard"
+msgstr ""
+
+msgid "SecurityReports|No vulnerabilities found for this group"
+msgstr ""
+
+msgid "SecurityReports|No vulnerabilities found for this pipeline"
+msgstr ""
+
+msgid "SecurityReports|No vulnerabilities found for this project"
+msgstr ""
+
+msgid "SecurityReports|Oops, something doesn't seem right."
+msgstr ""
+
+msgid "SecurityReports|Pipeline %{pipelineLink} triggered %{timeago} by %{user}"
msgstr ""
-msgid "SecurityDashboard|%{firstProject} and %{secondProject}"
+msgid "SecurityReports|Project"
msgstr ""
-msgid "SecurityDashboard|%{firstProject}, %{secondProject}, and %{rest}"
+msgid "SecurityReports|Projects added"
msgstr ""
-msgid "SecurityDashboard|Add a project to your dashboard"
+msgid "SecurityReports|Remove project from dashboard"
msgstr ""
-msgid "SecurityDashboard|Add or remove projects from your dashboard"
+msgid "SecurityReports|Report type"
msgstr ""
-msgid "SecurityDashboard|Add projects"
+msgid "SecurityReports|Return to dashboard"
msgstr ""
-msgid "SecurityDashboard|Edit dashboard"
+msgid "SecurityReports|Security Dashboard"
msgstr ""
-msgid "SecurityDashboard|Hide dismissed"
+msgid "SecurityReports|Security reports can only be accessed by authorized users."
msgstr ""
-msgid "SecurityDashboard|Monitor vulnerabilities in your code"
-msgstr "Überwache Sicherheitslücken in deinem Code"
+msgid "SecurityReports|Select a project to add by using the project search field above."
+msgstr ""
+
+msgid "SecurityReports|Select a reason"
+msgstr ""
+
+msgid "SecurityReports|Severity"
+msgstr ""
-msgid "SecurityDashboard|More information"
+msgid "SecurityReports|Status"
msgstr ""
-msgid "SecurityDashboard|No vulnerabilities found for dashboard"
+msgid "SecurityReports|The security dashboard displays the latest security findings for projects you wish to monitor. Select \"Edit dashboard\" to add and remove projects."
msgstr ""
-msgid "SecurityDashboard|Pipeline %{pipelineLink} triggered %{timeago} by %{user}"
+msgid "SecurityReports|The security dashboard displays the latest security report. Use it to find and fix vulnerabilities."
msgstr ""
-msgid "SecurityDashboard|Project"
+msgid "SecurityReports|There was an error adding the comment."
msgstr ""
-msgid "SecurityDashboard|Projects added"
+msgid "SecurityReports|There was an error creating the issue."
msgstr ""
-msgid "SecurityDashboard|Remove project from dashboard"
+msgid "SecurityReports|There was an error creating the merge request."
msgstr ""
-msgid "SecurityDashboard|Report type"
+msgid "SecurityReports|There was an error deleting the comment."
msgstr ""
-msgid "SecurityDashboard|Return to dashboard"
+msgid "SecurityReports|There was an error dismissing the vulnerabilities."
msgstr ""
-msgid "SecurityDashboard|Security Dashboard"
+msgid "SecurityReports|There was an error dismissing the vulnerability."
msgstr ""
-msgid "SecurityDashboard|Select a project to add by using the project search field above."
+msgid "SecurityReports|There was an error reverting the dismissal."
msgstr ""
-msgid "SecurityDashboard|Severity"
+msgid "SecurityReports|There was an error reverting this dismissal."
msgstr ""
-msgid "SecurityDashboard|Status"
+msgid "SecurityReports|There was an error while generating the report."
msgstr ""
-msgid "SecurityDashboard|The security dashboard displays the latest security findings for projects you wish to monitor. Select \"Edit dashboard\" to add and remove projects."
+msgid "SecurityReports|Unable to add %{invalidProjects}"
msgstr ""
-msgid "SecurityDashboard|The security dashboard displays the latest security report. Use it to find and fix vulnerabilities."
+msgid "SecurityReports|Undo dismiss"
msgstr ""
-msgid "SecurityDashboard|There was an error while generating the report."
+msgid "SecurityReports|While it's rare to have no vulnerabilities for your group, it can happen. In any event, we ask that you double check your settings to make sure you've set up your dashboard correctly."
msgstr ""
-msgid "SecurityDashboard|Unable to add %{invalidProjects}"
+msgid "SecurityReports|While it's rare to have no vulnerabilities for your pipeline, it can happen. In any event, we ask that you double check your settings to make sure all security scanning jobs have passed successfully."
+msgstr ""
+
+msgid "SecurityReports|While it's rare to have no vulnerabilities for your project, it can happen. In any event, we ask that you double check your settings to make sure you've set up your dashboard correctly."
+msgstr ""
+
+msgid "SecurityReports|While it's rare to have no vulnerabilities, it can happen. In any event, we ask that you please double check your settings to make sure you've set up your dashboard correctly."
+msgstr ""
+
+msgid "SecurityReports|Won't fix / Accept risk"
+msgstr ""
+
+msgid "SecurityReports|You do not have sufficient permissions to access this report"
+msgstr ""
+
+msgid "SecurityReports|You must sign in as an authorized user to see this report"
+msgstr ""
+
+msgid "SecurityReports|[No reason]"
msgstr ""
msgid "See GitLab's %{password_policy_guidelines}"
@@ -18065,6 +18489,9 @@ msgstr ""
msgid "See the affected projects in the GitLab admin panel"
msgstr ""
+msgid "See what's new at GitLab"
+msgstr ""
+
msgid "Select"
msgstr "Auswählen"
@@ -18470,9 +18897,6 @@ msgstr "CI/CD einrichten"
msgid "Set up Jira Integration"
msgstr ""
-msgid "Set up Jira Integration illustration"
-msgstr ""
-
msgid "Set up a %{type} Runner automatically"
msgstr ""
@@ -18557,6 +18981,9 @@ msgstr ""
msgid "Severity: %{severity}"
msgstr ""
+msgid "Shards (%{shards})"
+msgstr ""
+
msgid "Shards to synchronize"
msgstr ""
@@ -18871,6 +19298,9 @@ msgstr ""
msgid "Snowplow"
msgstr ""
+msgid "Some child epics may be hidden due to applied filters"
+msgstr ""
+
msgid "Some email servers do not support overriding the email sender name. Enable this option to include the name of the author of the issue, merge request or comment in the email body instead."
msgstr ""
@@ -19012,6 +19442,9 @@ msgstr ""
msgid "Something went wrong while updating your list settings"
msgstr ""
+msgid "Something went wrong with your automatic subscription renewal"
+msgstr ""
+
msgid "Something went wrong, unable to add %{project} to dashboard"
msgstr "Etwas lief schief, %{project} konnte nicht zum Dashboard hinzugefügt werden"
@@ -19297,6 +19730,9 @@ msgstr ""
msgid "Stage removed"
msgstr ""
+msgid "Standard"
+msgstr ""
+
msgid "Star a label to make it a priority label. Order the prioritized labels to change their relative priority, by dragging."
msgstr "Markiere ein Label, um es zu einem priorisierten Label zu machen. Ordne die priorisierten Labels durch Ziehen an, um ihre relative Priorität zu ändern."
@@ -19420,6 +19856,9 @@ msgstr "Gib deine Nachricht an, um sie zu aktivieren"
msgid "Static Application Security Testing (SAST)"
msgstr ""
+msgid "StaticSiteEditor|An error occurred while submitting your changes."
+msgstr ""
+
msgid "StaticSiteEditor|Branch could not be created."
msgstr ""
@@ -19429,18 +19868,30 @@ msgstr ""
msgid "StaticSiteEditor|Could not create merge request."
msgstr ""
+msgid "StaticSiteEditor|Incompatible file content"
+msgstr ""
+
msgid "StaticSiteEditor|Return to site"
msgstr ""
+msgid "StaticSiteEditor|Static site editor"
+msgstr ""
+
msgid "StaticSiteEditor|Success!"
msgstr ""
msgid "StaticSiteEditor|Summary of changes"
msgstr ""
+msgid "StaticSiteEditor|The Static Site Editor is currently configured to only edit Markdown content on pages generated from Middleman. Visit the documentation to learn more about configuring your site to use the Static Site Editor."
+msgstr ""
+
msgid "StaticSiteEditor|Update %{sourcePath} file"
msgstr ""
+msgid "StaticSiteEditor|View documentation"
+msgstr ""
+
msgid "StaticSiteEditor|View merge request"
msgstr ""
@@ -19498,6 +19949,12 @@ msgstr ""
msgid "StatusPage|Status page"
msgstr ""
+msgid "StatusPage|Status page URL"
+msgstr ""
+
+msgid "StatusPage|Status page frontend documentation"
+msgstr ""
+
msgid "StatusPage|To publish incidents to an external status page, GitLab will store a JSON file in your Amazon S3 account in a location accessible to your external status page service. Make sure to also set up %{docsLink}"
msgstr ""
@@ -19820,7 +20277,7 @@ msgid "Summary"
msgstr ""
msgid "Sunday"
-msgstr ""
+msgstr "Sonntag"
msgid "Support"
msgstr ""
@@ -19846,6 +20303,12 @@ msgstr "Informationen synchronisieren"
msgid "Synced"
msgstr ""
+msgid "Synchronization disabled"
+msgstr ""
+
+msgid "Synchronization of container repositories is disabled."
+msgstr ""
+
msgid "System"
msgstr ""
@@ -19859,7 +20322,7 @@ msgid "System Info"
msgstr "Systeminformationen"
msgid "System default (%{default})"
-msgstr ""
+msgstr "Systemstandard (%{default})"
msgid "System header and footer"
msgstr ""
@@ -19999,6 +20462,9 @@ msgstr ""
msgid "Telephone number"
msgstr ""
+msgid "Telephone number (Optional)"
+msgstr ""
+
msgid "Template"
msgstr "Vorlage"
@@ -20305,6 +20771,9 @@ msgstr ""
msgid "The license was successfully uploaded and is now active. You can see the details below."
msgstr ""
+msgid "The license was successfully uploaded and will be active from %{starts_at}. You can see the details below."
+msgstr ""
+
msgid "The maximum file size allowed is %{size}."
msgstr ""
@@ -20626,6 +21095,9 @@ msgstr ""
msgid "There was an error getting the epic participants."
msgstr ""
+msgid "There was an error importing the Jira project."
+msgstr ""
+
msgid "There was an error loading users activity calendar."
msgstr "Beim Laden des Benutzeraktivitäts-Kalenders ist ein Fehler aufgetreten."
@@ -20641,7 +21113,7 @@ msgstr ""
msgid "There was an error resetting user pipeline minutes."
msgstr ""
-msgid "There was an error saving this Geo Node"
+msgid "There was an error saving this Geo Node."
msgstr ""
msgid "There was an error saving your changes."
@@ -20704,6 +21176,9 @@ msgstr ""
msgid "They can be managed using the %{link}."
msgstr "Können unter %{link} verwaltet werden."
+msgid "Third Party Advisory Link"
+msgstr ""
+
msgid "Third party offers"
msgstr "Angebote von Drittanbietern"
@@ -20740,6 +21215,9 @@ msgstr "Diese Anwendung wurde erstellt von %{link_to_owner}."
msgid "This application will be able to:"
msgstr "Diese Anwendung wird in der Lage sein:"
+msgid "This attachment has been truncated to avoid exceeding the maximum allowed attachment size of 15MB. %{written_count} of %{issues_count} issues have been included. Consider re-exporting with a narrower selection of issues."
+msgstr ""
+
msgid "This block is self-referential"
msgstr ""
@@ -20881,6 +21359,12 @@ msgstr ""
msgid "This issue is confidential"
msgstr "Dieses Ticket ist vertraulich"
+msgid "This issue is currently blocked by the following issues: %{issues}."
+msgstr ""
+
+msgid "This issue is in a child epic of the filtered epic"
+msgstr ""
+
msgid "This issue is locked."
msgstr "Dieses Ticket ist gesperrt."
@@ -21079,7 +21563,10 @@ msgstr "Diese(r) Benutzer(in) wird der/die Autor(in) aller Ereignisse im Aktivit
msgid "This user will be the author of all events in the activity feed that are the result of an update, like new branches being created or new commits being pushed to existing branches. Upon creation or when reassigning you can only assign yourself to be the mirror user."
msgstr "Diese(r) Benutzer(in) wird der/die Autor(in) aller Ereignisse im Aktivitätsfeed sein, die die Folge eines Updates, wie die Erstellung neuer Branches oder das Pushen neuer Commits zu existierenden Branches, sind. Bei der Erstellung oder der Neuzuweisung kannst du nur dich selbst als Mirror-Benutzer(in) zuweisen."
-msgid "This variable can not be masked"
+msgid "This variable can not be masked."
+msgstr ""
+
+msgid "This variable does not match the expected pattern."
msgstr ""
msgid "This will help us personalize your onboarding experience."
@@ -21109,10 +21596,10 @@ msgstr ""
msgid "ThreatMonitoring|Application firewall not detected"
msgstr ""
-msgid "ThreatMonitoring|Container Network Policy"
+msgid "ThreatMonitoring|Container Network Policies are not installed or have been disabled. To view this data, ensure your Network Policies are installed and enabled for your cluster."
msgstr ""
-msgid "ThreatMonitoring|Container NetworkPolicies are not installed or has been disabled. To view this data, ensure you NetworkPolicies are installed and enabled for your cluster."
+msgid "ThreatMonitoring|Container Network Policy"
msgstr ""
msgid "ThreatMonitoring|Container NetworkPolicies not detected"
@@ -21145,7 +21632,7 @@ msgstr "Es ist ein Fehler aufgetreten. Umgebungen konnten nicht abgerufen werden
msgid "ThreatMonitoring|Something went wrong, unable to fetch statistics"
msgstr ""
-msgid "ThreatMonitoring|The firewall is not installed or has been disabled. To view this data, ensure you firewall is installed and enabled for your cluster."
+msgid "ThreatMonitoring|The firewall is not installed or has been disabled. To view this data, ensure the web application firewall is installed and enabled for your cluster."
msgstr ""
msgid "ThreatMonitoring|The graph below is an overview of traffic coming to your application as tracked by the Web Application Firewall (WAF). View the docs for instructions on how to access the WAF logs to see what type of malicious traffic is trying to access your app. The docs link is also accessible by clicking the \"?\" icon next to the title below."
@@ -21179,7 +21666,7 @@ msgid "ThreatMonitoring|While it's rare to have no traffic coming to your applic
msgstr ""
msgid "Thursday"
-msgstr ""
+msgstr "Donnerstag"
msgid "Time"
msgstr ""
@@ -21214,6 +21701,9 @@ msgstr ""
msgid "Time in seconds GitLab will wait for a response from the external service. When the service does not respond in time, access will be denied."
msgstr "Zeit in Sekunden die GitLab auf die Antwort des externen Dienstes warten wird. Wenn der Dienst nicht rechtzeitig antwortet, wird der Zugriff verweigert."
+msgid "Time of import: %{importTime}"
+msgstr ""
+
msgid "Time remaining"
msgstr "Verbleibende Zeit"
@@ -21398,7 +21888,7 @@ msgstr "Titel"
msgid "Title:"
msgstr ""
-msgid "Titles and Filenames"
+msgid "Titles and Descriptions"
msgstr ""
msgid "To"
@@ -21605,7 +22095,7 @@ msgstr "Zu viele Namespaces aktiviert. Sie müssen sie über die Konsole oder di
msgid "Too many projects enabled. You will need to manage them via the console or the API."
msgstr "Zu viele Projekte aktviert. Sie müssen sie über die Konsole oder die API verwalten."
-msgid "Topics"
+msgid "Topics (optional)"
msgstr ""
msgid "Total"
@@ -21767,6 +22257,9 @@ msgstr ""
msgid "Try all GitLab has to offer for 30 days."
msgstr "Teste für 30 Tage alles was GitLab zu bieten hat."
+msgid "Try changing or removing filters."
+msgstr ""
+
msgid "Try to fork again"
msgstr ""
@@ -21777,7 +22270,7 @@ msgid "Trying to communicate with your device. Plug it in (if you haven't alread
msgstr ""
msgid "Tuesday"
-msgstr ""
+msgstr "Dienstag"
msgid "Turn Off"
msgstr ""
@@ -21860,6 +22353,9 @@ msgstr ""
msgid "Unable to connect to server: %{error}"
msgstr ""
+msgid "Unable to connect to the Jira instance. Please check your Jira integration configuration."
+msgstr ""
+
msgid "Unable to convert Kubernetes logs encoding to UTF-8"
msgstr ""
@@ -22803,6 +23299,9 @@ msgstr ""
msgid "View issue"
msgstr ""
+msgid "View issues"
+msgstr ""
+
msgid "View it on GitLab"
msgstr "Sieh es auf GitLab an"
@@ -22980,6 +23479,15 @@ msgstr ""
msgid "VulnerabilityManagement|Resolved %{timeago} by %{user}"
msgstr ""
+msgid "VulnerabilityManagement|Something went wrong while trying to delete the comment. Please try again later."
+msgstr ""
+
+msgid "VulnerabilityManagement|Something went wrong while trying to retrieve the vulnerability history. Please try again later."
+msgstr ""
+
+msgid "VulnerabilityManagement|Something went wrong while trying to save the comment. Please try again later."
+msgstr ""
+
msgid "VulnerabilityManagement|Something went wrong, could not create an issue."
msgstr ""
@@ -23010,6 +23518,9 @@ msgstr ""
msgid "VulnerabilityStatusTypes|Resolved"
msgstr ""
+msgid "Vulnerability|%{scannerName} (version %{scannerVersion})"
+msgstr ""
+
msgid "Vulnerability|Class"
msgstr "Klasse"
@@ -23043,7 +23554,10 @@ msgstr ""
msgid "Vulnerability|Project"
msgstr "Projekt"
-msgid "Vulnerability|Report Type"
+msgid "Vulnerability|Scanner Provider"
+msgstr ""
+
+msgid "Vulnerability|Scanner Type"
msgstr ""
msgid "Vulnerability|Severity"
@@ -23097,9 +23611,15 @@ msgstr ""
msgid "We sent you an email with reset password instructions"
msgstr ""
+msgid "We tried to automatically renew your %{strong}%{plan_name}%{strong_close} subscription for %{strong}%{namespace_name}%{strong_close} on %{expires_on} but something went wrong so your subscription was downgraded to the free plan. Don't worry, your data is safe. We suggest you check your payment method and get in touch with our support team (%{support_link}). They'll gladly help with your subscription renewal."
+msgstr ""
+
msgid "We want to be sure it is you, please confirm you are not a robot."
msgstr "Wir wollen sicher gehen, dass du es bist. Bitte bestätige, dass du kein Roboter bist."
+msgid "We will automatically renew your %{strong}%{plan_name}%{strong_close} subscription for %{strong}%{namespace_name}%{strong_close} on %{strong}%{expires_on}%{strong_close}. There's nothing that you need to do, we'll let you know when the renewal is complete. Need more seats, a higher plan or just want to review your payment method?"
+msgstr ""
+
msgid "We've found no vulnerabilities"
msgstr ""
@@ -23137,7 +23657,7 @@ msgid "Webhooks have moved. They can now be found under the Settings menu."
msgstr ""
msgid "Wednesday"
-msgstr ""
+msgstr "Mittwoch"
msgid "Weekday"
msgstr ""
@@ -23204,15 +23724,6 @@ msgstr ""
msgid "When:"
msgstr ""
-msgid "While it's rare to have no vulnerabilities for your group, it can happen. In any event, we ask that you double check your settings to make sure you've set up your dashboard correctly."
-msgstr ""
-
-msgid "While it's rare to have no vulnerabilities for your pipeline, it can happen. In any event, we ask that you double check your settings to make sure all security scanning jobs have passed successfully."
-msgstr ""
-
-msgid "While it's rare to have no vulnerabilities for your project, it can happen. In any event, we ask that you double check your settings to make sure you've set up your dashboard correctly."
-msgstr ""
-
msgid "While it's rare to have no vulnerabilities, it can happen. In any event, we ask that you please double check your settings to make sure you've set up your dashboard correctly."
msgstr ""
@@ -23243,9 +23754,6 @@ msgstr ""
msgid "Wiki"
msgstr "Wiki"
-msgid "Wiki pages"
-msgstr ""
-
msgid "Wiki was successfully updated."
msgstr ""
@@ -23429,6 +23937,9 @@ msgstr ""
msgid "Yes, add it"
msgstr "Ja, füg' es hinzu"
+msgid "Yes, close issue"
+msgstr ""
+
msgid "Yes, let me map Google Code users to full names or GitLab users."
msgstr "Ja, lass mich Google Code-Benutzer(innen) vollständigen Namen oder GitLab-Benutzer(innen) zuordnen."
@@ -23624,6 +24135,9 @@ msgstr "Du kannst nicht auf dieser schreibgeschützte GitLab-Instanz schreiben."
msgid "You could not create a new trigger."
msgstr ""
+msgid "You didn't renew your %{strong}%{plan_name}%{strong_close} subscription for %{strong}%{namespace_name}%{strong_close} so it was downgraded to the free plan."
+msgstr ""
+
msgid "You didn't renew your %{strong}%{plan_name}%{strong_close} subscription so it was downgraded to the GitLab Core Plan."
msgstr ""
@@ -23690,6 +24204,9 @@ msgstr ""
msgid "You have declined the invitation to join %{label}."
msgstr ""
+msgid "You have imported from this project %{numberOfPreviousImportsForProject} times before. Each new import will create duplicate issues."
+msgstr ""
+
msgid "You have no permissions"
msgstr "Du hast keine Berechtigungen"
@@ -23771,6 +24288,9 @@ msgstr ""
msgid "You need to upload a Google Takeout archive."
msgstr ""
+msgid "You reached %{usage_in_percent} of %{namespace_name}'s capacity (%{used_storage} of %{storage_limit})"
+msgstr ""
+
msgid "You tried to fork %{link_to_the_project} but it failed for the following reason:"
msgstr ""
@@ -23855,9 +24375,21 @@ msgstr ""
msgid "YouTube"
msgstr "YouTube"
+msgid "Your %{strong}%{plan_name}%{strong_close} subscription for %{strong}%{namespace_name}%{strong_close} will expire on %{strong}%{expires_on}%{strong_close}. After that, you will not to be able to create issues or merge requests as well as many other features."
+msgstr ""
+
msgid "Your %{strong}%{plan_name}%{strong_close} subscription will expire on %{strong}%{expires_on}%{strong_close}. After that, you will not to be able to create issues or merge requests as well as many other features."
msgstr ""
+msgid "Your CSV export has started. It will be emailed to %{email} when complete."
+msgstr ""
+
+msgid "Your CSV export of %{issues_count} from project %{project_link} has been added to this email as an attachment."
+msgstr ""
+
+msgid "Your CSV export of %{written_count} from project %{project_name} (%{project_url}) has been added to this email as an attachment."
+msgstr ""
+
msgid "Your Commit Email will be used for web based operations, such as edits and merges."
msgstr ""
@@ -23954,6 +24486,9 @@ msgstr ""
msgid "Your comment could not be updated! Please check your network connection and try again."
msgstr ""
+msgid "Your comment will be discarded."
+msgstr ""
+
msgid "Your custom stage '%{title}' was created"
msgstr ""
@@ -23993,6 +24528,9 @@ msgstr ""
msgid "Your new personal access token has been created."
msgstr ""
+msgid "Your new project access token has been created."
+msgstr ""
+
msgid "Your password isn't required to view this page. If a password or any other personal details are requested, please contact your administrator to report abuse."
msgstr ""
@@ -24011,12 +24549,18 @@ msgstr "Deine Projekte"
msgid "Your request for access has been queued for review."
msgstr ""
+msgid "Your search didn't match any commits."
+msgstr ""
+
msgid "Your subscription expired!"
msgstr ""
msgid "Your subscription has been downgraded"
msgstr ""
+msgid "Your subscription will automatically renew in %{remaining_days}"
+msgstr ""
+
msgid "Your subscription will expire in %{remaining_days}"
msgstr ""
@@ -24055,7 +24599,7 @@ msgstr "vor"
msgid "allowed to fail"
msgstr ""
-msgid "already being used for another group or project milestone."
+msgid "already being used for another group or project %{timebox_name}."
msgstr ""
msgid "already has a \"created\" issue link"
@@ -24133,6 +24677,9 @@ msgstr "%{linkStartTag}Lerne mehr über Abhängigkeitsüberprüfung%{linkEndTag}
msgid "ciReport|%{linkStartTag}Learn more about SAST %{linkEndTag}"
msgstr "%{linkStartTag}Lerne mehr über SAST%{linkEndTag}"
+msgid "ciReport|%{linkStartTag}Learn more about Secret Scanning %{linkEndTag}"
+msgstr ""
+
msgid "ciReport|%{linkStartTag}Learn more about codequality reports %{linkEndTag}"
msgstr ""
@@ -24296,6 +24843,12 @@ msgstr ""
msgid "ciReport|SAST"
msgstr "SAST"
+msgid "ciReport|Secret scanning"
+msgstr ""
+
+msgid "ciReport|Secret scanning detects secrets and credentials vulnerabilities in your source code."
+msgstr ""
+
msgid "ciReport|Security scanning"
msgstr "Sicherheitsüberprüfung"
@@ -24493,6 +25046,9 @@ msgstr ""
msgid "group"
msgstr ""
+msgid "groups"
+msgstr ""
+
msgid "has already been linked to another vulnerability"
msgstr ""
@@ -24656,9 +25212,6 @@ msgstr[1] "Merge-Requests"
msgid "merged %{time_ago}"
msgstr ""
-msgid "milestone should belong either to a project or a group."
-msgstr ""
-
msgid "missing"
msgstr ""
@@ -24995,6 +25548,9 @@ msgstr "Neuer Merge-Request"
msgid "no contributions"
msgstr ""
+msgid "no expiration"
+msgstr ""
+
msgid "no one can merge"
msgstr ""
@@ -25047,6 +25603,9 @@ msgstr ""
msgid "pending removal"
msgstr ""
+msgid "per day"
+msgstr ""
+
msgid "pipeline"
msgstr ""
@@ -25075,6 +25634,12 @@ msgstr[1] "Projekte"
msgid "project avatar"
msgstr ""
+msgid "projects"
+msgstr ""
+
+msgid "push to your repository, create pipelines, create issues or add comments. To reduce storage capacity, delete unused repositories, artifacts, wikis, issues, and pipelines."
+msgstr ""
+
msgid "quick actions"
msgstr "Schnellzugriff"
@@ -25221,11 +25786,6 @@ msgstr ""
msgid "this document"
msgstr "dieses Dokument"
-msgid "thread resolved"
-msgid_plural "threads resolved"
-msgstr[0] ""
-msgstr[1] ""
-
msgid "to help your contributors communicate effectively!"
msgstr "um deinen Mitwirkenden zu helfen möglichst effizient zu kommunizieren!"
@@ -25280,6 +25840,9 @@ msgstr "sieh es auf GitLab an"
msgid "view the blob"
msgstr ""
+msgid "vulnerability|Add a comment"
+msgstr ""
+
msgid "vulnerability|Add a comment or reason for dismissal"
msgstr ""
diff --git a/locale/el_GR/gitlab.po b/locale/el_GR/gitlab.po
index 57539f9d394..86197dbbef9 100644
--- a/locale/el_GR/gitlab.po
+++ b/locale/el_GR/gitlab.po
@@ -12,7 +12,7 @@ msgstr ""
"X-Crowdin-Project: gitlab-ee\n"
"X-Crowdin-Language: el\n"
"X-Crowdin-File: /master/locale/gitlab.pot\n"
-"PO-Revision-Date: 2020-04-15 00:32\n"
+"PO-Revision-Date: 2020-05-05 21:35\n"
msgid " %{start} to %{end}"
msgstr ""
@@ -212,6 +212,11 @@ msgid_plural "%d tags"
msgstr[0] ""
msgstr[1] ""
+msgid "%d unresolved thread"
+msgid_plural "%d unresolved threads"
+msgstr[0] ""
+msgstr[1] ""
+
msgid "%d vulnerability dismissed"
msgid_plural "%d vulnerabilities dismissed"
msgstr[0] ""
@@ -371,6 +376,9 @@ msgstr ""
msgid "%{mrText}, this issue will be closed automatically."
msgstr ""
+msgid "%{namespace_name} is now read-only. You cannot: %{base_message}"
+msgstr ""
+
msgid "%{name} contained %{resultsString}"
msgstr ""
@@ -383,6 +391,11 @@ msgstr ""
msgid "%{name}'s avatar"
msgstr ""
+msgid "%{no_of_days} day"
+msgid_plural "%{no_of_days} days"
+msgstr[0] ""
+msgstr[1] ""
+
msgid "%{number_commits_behind} commits behind %{default_branch}, %{number_commits_ahead} commits ahead"
msgstr ""
@@ -506,6 +519,9 @@ msgstr[1] ""
msgid "%{text} is available"
msgstr ""
+msgid "%{timebox_name} should belong either to a project or a group."
+msgstr ""
+
msgid "%{title} %{operator} %{threshold}"
msgstr ""
@@ -781,12 +797,18 @@ msgstr ""
msgid "<code>\"johnsmith@example.com\": \"johnsmith@example.com\"</code> will add \"By <a href=\"#\">johnsmith@example.com</a>\" to all issues and comments originally created by johnsmith@example.com. By default, the email address or username is masked to ensure the user's privacy. Use this option if you want to show the full email address."
msgstr ""
+msgid "<namespace / project>"
+msgstr ""
+
msgid "<no name set>"
msgstr ""
msgid "<no scopes selected>"
msgstr ""
+msgid "<project name>"
+msgstr ""
+
msgid "<strong>%{group_name}</strong> group members"
msgstr ""
@@ -820,6 +842,9 @@ msgstr ""
msgid "A basic page and serverless function that uses AWS Lambda, AWS API Gateway, and GitLab Pages"
msgstr ""
+msgid "A complete DevOps platform"
+msgstr ""
+
msgid "A default branch cannot be chosen for an empty project."
msgstr ""
@@ -1036,6 +1061,12 @@ msgstr ""
msgid "AccessTokens|reset it"
msgstr ""
+msgid "AccessibilityReport|Accessibility report artifact not found"
+msgstr ""
+
+msgid "AccessibilityReport|Failed to retrieve accessibility report"
+msgstr ""
+
msgid "AccessibilityReport|Learn More"
msgstr ""
@@ -1045,6 +1076,9 @@ msgstr ""
msgid "AccessibilityReport|New"
msgstr ""
+msgid "AccessibilityReport|The accessibility scanning found an error of the following type: %{code}"
+msgstr ""
+
msgid "Account"
msgstr ""
@@ -1287,6 +1321,9 @@ msgstr ""
msgid "Added at"
msgstr ""
+msgid "Added for this merge request"
+msgstr ""
+
msgid "Added in this version"
msgstr ""
@@ -1658,6 +1695,72 @@ msgid_plural "Alerts"
msgstr[0] ""
msgstr[1] ""
+msgid "Alert Details"
+msgstr ""
+
+msgid "AlertManagement|Acknowledged"
+msgstr ""
+
+msgid "AlertManagement|Alert"
+msgstr ""
+
+msgid "AlertManagement|Authorize external service"
+msgstr ""
+
+msgid "AlertManagement|Display alerts from all your monitoring tools directly within GitLab. Streamline the investigation of your alerts and the escalation of alerts to incidents."
+msgstr ""
+
+msgid "AlertManagement|End time"
+msgstr ""
+
+msgid "AlertManagement|End time:"
+msgstr ""
+
+msgid "AlertManagement|Events"
+msgstr ""
+
+msgid "AlertManagement|Events:"
+msgstr ""
+
+msgid "AlertManagement|Full Alert Details"
+msgstr ""
+
+msgid "AlertManagement|More information"
+msgstr ""
+
+msgid "AlertManagement|No alerts available to display. If you think you're seeing this message in error, refresh the page."
+msgstr ""
+
+msgid "AlertManagement|No alerts to display."
+msgstr ""
+
+msgid "AlertManagement|Overview"
+msgstr ""
+
+msgid "AlertManagement|Resolved"
+msgstr ""
+
+msgid "AlertManagement|Severity"
+msgstr ""
+
+msgid "AlertManagement|Start time"
+msgstr ""
+
+msgid "AlertManagement|Start time:"
+msgstr ""
+
+msgid "AlertManagement|Status"
+msgstr ""
+
+msgid "AlertManagement|Surface alerts in GitLab"
+msgstr ""
+
+msgid "AlertManagement|There was an error displaying the alerts. Confirm your endpoint's configuration details to ensure alerts appear."
+msgstr ""
+
+msgid "AlertManagement|Triggered"
+msgstr ""
+
msgid "AlertService|%{linkStart}Learn more%{linkEnd} about configuring this endpoint to receive alerts."
msgstr ""
@@ -1727,6 +1830,9 @@ msgstr ""
msgid "All security scans are enabled because %{linkStart}Auto DevOps%{linkEnd} is enabled on this project"
msgstr ""
+msgid "All threads resolved"
+msgstr ""
+
msgid "All users"
msgstr ""
@@ -1745,6 +1851,9 @@ msgstr ""
msgid "Allow only the selected protocols to be used for Git access."
msgstr ""
+msgid "Allow owners to manage default branch protection per group"
+msgstr ""
+
msgid "Allow owners to manually add users outside of LDAP"
msgstr ""
@@ -1853,6 +1962,9 @@ msgstr ""
msgid "An error occurred fetching the dropdown data."
msgstr ""
+msgid "An error occurred fetching the project authors."
+msgstr ""
+
msgid "An error occurred previewing the blob"
msgstr ""
@@ -1934,6 +2046,9 @@ msgstr ""
msgid "An error occurred while fetching sidebar data"
msgstr ""
+msgid "An error occurred while fetching terraform reports."
+msgstr ""
+
msgid "An error occurred while fetching the Service Desk address."
msgstr ""
@@ -2171,6 +2286,9 @@ msgstr ""
msgid "Any"
msgstr ""
+msgid "Any Author"
+msgstr ""
+
msgid "Any Label"
msgstr ""
@@ -2386,10 +2504,10 @@ msgstr ""
msgid "Are you sure that you want to unarchive this project?"
msgstr ""
-msgid "Are you sure you want to cancel creating this comment?"
+msgid "Are you sure you want to cancel editing this comment?"
msgstr ""
-msgid "Are you sure you want to cancel editing this comment?"
+msgid "Are you sure you want to close this blocked issue?"
msgstr ""
msgid "Are you sure you want to delete %{name}?"
@@ -2419,6 +2537,9 @@ msgstr ""
msgid "Are you sure you want to deploy this environment?"
msgstr ""
+msgid "Are you sure you want to discard this comment?"
+msgstr ""
+
msgid "Are you sure you want to erase this build?"
msgstr ""
@@ -2595,6 +2716,9 @@ msgstr ""
msgid "At least one approval from a code owner is required to change files matching the respective CODEOWNER rules."
msgstr ""
+msgid "At least one logging option is required to be enabled"
+msgstr ""
+
msgid "At least one of group_id or project_id must be specified"
msgstr ""
@@ -2633,6 +2757,27 @@ msgstr ""
msgid "AuditEvents|Target"
msgstr ""
+msgid "AuditLogs|(removed)"
+msgstr ""
+
+msgid "AuditLogs|Action"
+msgstr ""
+
+msgid "AuditLogs|Author"
+msgstr ""
+
+msgid "AuditLogs|Date"
+msgstr ""
+
+msgid "AuditLogs|IP Address"
+msgstr ""
+
+msgid "AuditLogs|Object"
+msgstr ""
+
+msgid "AuditLogs|Target"
+msgstr ""
+
msgid "Aug"
msgstr ""
@@ -2915,9 +3060,6 @@ msgstr ""
msgid "BambooService|You must set up automatic revision labeling and a repository trigger in Bamboo."
msgstr ""
-msgid "Batch operations"
-msgstr ""
-
msgid "BatchComments|Delete all pending comments"
msgstr ""
@@ -3047,6 +3189,9 @@ msgstr ""
msgid "Boards"
msgstr ""
+msgid "Boards and Board Lists"
+msgstr ""
+
msgid "Boards|Collapse"
msgstr ""
@@ -3266,6 +3411,9 @@ msgstr ""
msgid "Buy GitLab Enterprise Edition"
msgstr ""
+msgid "Buy more Pipeline minutes"
+msgstr ""
+
msgid "By %{user_name}"
msgstr ""
@@ -3386,6 +3534,9 @@ msgstr ""
msgid "Can't scan the code?"
msgstr ""
+msgid "Can't update snippet: %{err}"
+msgstr ""
+
msgid "Canary"
msgstr ""
@@ -3554,6 +3705,9 @@ msgstr ""
msgid "Changing group path can have unintended side effects."
msgstr ""
+msgid "Charts"
+msgstr ""
+
msgid "Charts can't be displayed as the request for data has timed out. %{documentationLink}"
msgstr ""
@@ -3590,9 +3744,6 @@ msgstr ""
msgid "ChatMessage|and [%{count} more](%{pipeline_failed_jobs_url})"
msgstr ""
-msgid "ChatMessage|failed"
-msgstr ""
-
msgid "ChatMessage|has failed"
msgstr ""
@@ -3608,9 +3759,6 @@ msgstr ""
msgid "ChatMessage|in %{project_link}"
msgstr ""
-msgid "ChatMessage|passed"
-msgstr ""
-
msgid "Check again"
msgstr ""
@@ -4061,9 +4209,6 @@ msgstr ""
msgid "Click any <strong>project name</strong> in the project list below to navigate to the project milestone."
msgstr ""
-msgid "Click here"
-msgstr ""
-
msgid "Click the <strong>Download</strong> button and wait for downloading to complete."
msgstr ""
@@ -4451,6 +4596,12 @@ msgstr ""
msgid "ClusterIntegration|Fetching zones"
msgstr ""
+msgid "ClusterIntegration|Fluentd"
+msgstr ""
+
+msgid "ClusterIntegration|Fluentd is an open source data collector, which lets you unify the data collection and consumption for a better use and understanding of data. It requires at least one of the following logs to be successfully installed."
+msgstr ""
+
msgid "ClusterIntegration|GitLab Integration"
msgstr ""
@@ -4757,6 +4908,15 @@ msgstr ""
msgid "ClusterIntegration|Request to begin uninstalling failed"
msgstr ""
+msgid "ClusterIntegration|SIEM Hostname"
+msgstr ""
+
+msgid "ClusterIntegration|SIEM Port"
+msgstr ""
+
+msgid "ClusterIntegration|SIEM Protocol"
+msgstr ""
+
msgid "ClusterIntegration|Save changes"
msgstr ""
@@ -4853,6 +5013,12 @@ msgstr ""
msgid "ClusterIntegration|Select zone to choose machine type"
msgstr ""
+msgid "ClusterIntegration|Send Cilium Logs"
+msgstr ""
+
+msgid "ClusterIntegration|Send ModSecurity Logs"
+msgstr ""
+
msgid "ClusterIntegration|Service Token"
msgstr ""
@@ -5281,18 +5447,33 @@ msgstr ""
msgid "Compliance framework (optional)"
msgstr ""
+msgid "ComplianceFramework|GDPR"
+msgstr ""
+
msgid "ComplianceFramework|GDPR - General Data Protection Regulation"
msgstr ""
+msgid "ComplianceFramework|HIPAA"
+msgstr ""
+
msgid "ComplianceFramework|HIPAA - Health Insurance Portability and Accountability Act"
msgstr ""
+msgid "ComplianceFramework|PCI-DSS"
+msgstr ""
+
msgid "ComplianceFramework|PCI-DSS - Payment Card Industry-Data Security Standard"
msgstr ""
+msgid "ComplianceFramework|SOC 2"
+msgstr ""
+
msgid "ComplianceFramework|SOC 2 - Service Organization Control 2"
msgstr ""
+msgid "ComplianceFramework|SOX"
+msgstr ""
+
msgid "ComplianceFramework|SOX - Sarbanes-Oxley"
msgstr ""
@@ -5440,9 +5621,15 @@ msgstr ""
msgid "Container repositories sync capacity"
msgstr ""
+msgid "ContainerRegistry| Please visit the %{linkStart}administration settings%{linkEnd} to enable this feature."
+msgstr ""
+
msgid "ContainerRegistry|%{imageName} tags"
msgstr ""
+msgid "ContainerRegistry|%{title} was successfully scheduled for deletion"
+msgstr ""
+
msgid "ContainerRegistry|Automatically remove extra images that aren't designed to be kept."
msgstr ""
@@ -5455,6 +5642,9 @@ msgstr ""
msgid "ContainerRegistry|Container Registry"
msgstr ""
+msgid "ContainerRegistry|Container Registry tag expiration and retention policy is disabled"
+msgstr ""
+
msgid "ContainerRegistry|Copy build command"
msgstr ""
@@ -5464,18 +5654,12 @@ msgstr ""
msgid "ContainerRegistry|Copy push command"
msgstr ""
-msgid "ContainerRegistry|Currently, the Container Registry tag expiration feature is not available for projects created before GitLab version 12.8. For updates and more information, visit Issue %{linkStart}#196124%{linkEnd}"
-msgstr ""
-
msgid "ContainerRegistry|Docker connection error"
msgstr ""
msgid "ContainerRegistry|Docker tag expiration policy is %{toggleStatus}"
msgstr ""
-msgid "ContainerRegistry|Docker tags with names matching this regex pattern will expire:"
-msgstr ""
-
msgid "ContainerRegistry|Edit Settings"
msgstr ""
@@ -5497,9 +5681,6 @@ msgstr ""
msgid "ContainerRegistry|Image ID"
msgstr ""
-msgid "ContainerRegistry|Image deleted successfully"
-msgstr ""
-
msgid "ContainerRegistry|Keep and protect the images that matter most."
msgstr ""
@@ -5515,12 +5696,24 @@ msgstr ""
msgid "ContainerRegistry|Number of tags to retain:"
msgstr ""
+msgid "ContainerRegistry|Please contact your administrator."
+msgstr ""
+
msgid "ContainerRegistry|Push an image"
msgstr ""
msgid "ContainerRegistry|Quick Start"
msgstr ""
+msgid "ContainerRegistry|Regular expressions such as %{codeStart}.*-test%{codeEnd} or %{codeStart}dev-.*%{codeEnd} are supported"
+msgstr ""
+
+msgid "ContainerRegistry|Regular expressions such as %{codeStart}.*-test%{codeEnd} or %{codeStart}dev-.*%{codeEnd} are supported. To select all tags, use %{codeStart}.*%{codeEnd}"
+msgstr ""
+
+msgid "ContainerRegistry|Remember to run %{docLinkStart}garbage collection%{docLinkEnd} to remove the stale data from storage."
+msgstr ""
+
msgid "ContainerRegistry|Remove repository"
msgstr ""
@@ -5535,22 +5728,22 @@ msgstr[1] ""
msgid "ContainerRegistry|Retention policy has been Enabled"
msgstr ""
-msgid "ContainerRegistry|Something went wrong while deleting the image."
+msgid "ContainerRegistry|Something went wrong while fetching the expiration policy."
msgstr ""
-msgid "ContainerRegistry|Something went wrong while deleting the tag."
+msgid "ContainerRegistry|Something went wrong while fetching the repository list."
msgstr ""
-msgid "ContainerRegistry|Something went wrong while deleting the tags."
+msgid "ContainerRegistry|Something went wrong while fetching the tags list."
msgstr ""
-msgid "ContainerRegistry|Something went wrong while fetching the expiration policy."
+msgid "ContainerRegistry|Something went wrong while marking the tag for deletion."
msgstr ""
-msgid "ContainerRegistry|Something went wrong while fetching the packages list."
+msgid "ContainerRegistry|Something went wrong while marking the tags for deletion."
msgstr ""
-msgid "ContainerRegistry|Something went wrong while fetching the tags list."
+msgid "ContainerRegistry|Something went wrong while scheduling %{title} for deletion. Please try again."
msgstr ""
msgid "ContainerRegistry|Something went wrong while updating the expiration policy."
@@ -5559,16 +5752,25 @@ msgstr ""
msgid "ContainerRegistry|Tag"
msgstr ""
-msgid "ContainerRegistry|Tag deleted successfully"
-msgstr ""
-
msgid "ContainerRegistry|Tag expiration policy"
msgstr ""
msgid "ContainerRegistry|Tag expiration policy is designed to:"
msgstr ""
-msgid "ContainerRegistry|Tags deleted successfully"
+msgid "ContainerRegistry|Tag successfully marked for deletion."
+msgstr ""
+
+msgid "ContainerRegistry|Tags successfully marked for deletion."
+msgstr ""
+
+msgid "ContainerRegistry|Tags with names matching this regex pattern will %{italicStart}be preserved:%{italicEnd}"
+msgstr ""
+
+msgid "ContainerRegistry|Tags with names matching this regex pattern will %{italicStart}expire:%{italicEnd}"
+msgstr ""
+
+msgid "ContainerRegistry|The Container Registry tag expiration and retention policies for this project have not been enabled."
msgstr ""
msgid "ContainerRegistry|The last tag related to this image was recently removed. This empty image and any associated data will be automatically removed as part of the regular Garbage Collection process. If you have any questions, contact your administrator."
@@ -5589,16 +5791,16 @@ msgstr ""
msgid "ContainerRegistry|There are no container images stored for this project"
msgstr ""
-msgid "ContainerRegistry|This Registry contains deleted image tag data. Remember to run %{docLinkStart}garbage collection%{docLinkEnd} to remove the stale data from storage."
+msgid "ContainerRegistry|There was an error during the deletion of this image repository, please try again."
msgstr ""
msgid "ContainerRegistry|This image has no active tags"
msgstr ""
-msgid "ContainerRegistry|We are having trouble connecting to Docker, which could be due to an issue with your project name or path. %{docLinkStart}More Information%{docLinkEnd}"
+msgid "ContainerRegistry|This image repository is scheduled for deletion"
msgstr ""
-msgid "ContainerRegistry|Wildcards such as %{codeStart}.*-stable%{codeEnd} or %{codeStart}production/.*%{codeEnd} are supported. To select all tags, use %{codeStart}.*%{codeEnd}"
+msgid "ContainerRegistry|We are having trouble connecting to Docker, which could be due to an issue with your project name or path. %{docLinkStart}More Information%{docLinkEnd}"
msgstr ""
msgid "ContainerRegistry|With the Container Registry, every project can have its own space to store its Docker images. %{docLinkStart}More Information%{docLinkEnd}"
@@ -5853,7 +6055,7 @@ msgstr ""
msgid "Could not delete chat nickname %{chat_name}."
msgstr ""
-msgid "Could not find design"
+msgid "Could not find design."
msgstr ""
msgid "Could not remove the trigger."
@@ -5868,6 +6070,9 @@ msgstr ""
msgid "Could not revoke personal access token %{personal_access_token_name}."
msgstr ""
+msgid "Could not revoke project access token %{project_access_token_name}."
+msgstr ""
+
msgid "Could not save group ID"
msgstr ""
@@ -5925,6 +6130,9 @@ msgstr ""
msgid "Create a new branch"
msgstr ""
+msgid "Create a new deploy key for this project"
+msgstr ""
+
msgid "Create a new file as there are no files yet. Afterwards, you'll be able to commit your changes."
msgstr ""
@@ -6030,6 +6238,9 @@ msgstr ""
msgid "Create requirement"
msgstr ""
+msgid "Create snippet"
+msgstr ""
+
msgid "Create wildcard: %{searchTerm}"
msgstr ""
@@ -6051,6 +6262,9 @@ msgstr ""
msgid "Created"
msgstr ""
+msgid "Created %{timestamp}"
+msgstr ""
+
msgid "Created At"
msgstr ""
@@ -6407,6 +6621,9 @@ msgstr ""
msgid "CycleAnalytics|stage dropdown"
msgstr ""
+msgid "DAG"
+msgstr ""
+
msgid "DNS"
msgstr ""
@@ -6773,6 +6990,9 @@ msgstr ""
msgid "Deploy key was successfully updated."
msgstr ""
+msgid "Deploy keys allow read-only or read-write (if enabled) access to your repository. Deploy keys can be used for CI, staging or production servers. You can create a deploy key or add an existing one."
+msgstr ""
+
msgid "Deploy progress not found. To see pods, ensure your environment matches %{linkStart}deploy board criteria%{linkEnd}."
msgstr ""
@@ -6926,6 +7146,9 @@ msgstr ""
msgid "Deploying to"
msgstr ""
+msgid "Deployment Frequency"
+msgstr ""
+
msgid "Deployment|API"
msgstr ""
@@ -7444,9 +7667,6 @@ msgstr ""
msgid "Edit environment"
msgstr ""
-msgid "Edit epic description"
-msgstr ""
-
msgid "Edit file"
msgstr ""
@@ -8068,9 +8288,6 @@ msgstr ""
msgid "Epics"
msgstr ""
-msgid "Epics (Ultimate / Gold license only)"
-msgstr ""
-
msgid "Epics Roadmap"
msgstr ""
@@ -8080,6 +8297,9 @@ msgstr ""
msgid "Epics let you manage your portfolio of projects more efficiently and with less effort"
msgstr ""
+msgid "Epics, Issues, and Merge Requests"
+msgstr ""
+
msgid "Epics|Add an epic"
msgstr ""
@@ -8131,6 +8351,9 @@ msgstr ""
msgid "Epics|Something went wrong while fetching group epics."
msgstr ""
+msgid "Epics|Something went wrong while moving item."
+msgstr ""
+
msgid "Epics|Something went wrong while ordering item."
msgstr ""
@@ -8185,6 +8408,9 @@ msgstr ""
msgid "Error fetching network graph."
msgstr ""
+msgid "Error fetching payload data."
+msgstr ""
+
msgid "Error fetching projects"
msgstr ""
@@ -8194,9 +8420,6 @@ msgstr ""
msgid "Error fetching the dependency list. Please check your network connection and try again."
msgstr ""
-msgid "Error fetching usage ping data."
-msgstr ""
-
msgid "Error loading branch data. Please try again."
msgstr ""
@@ -8338,6 +8561,9 @@ msgstr ""
msgid "Errors"
msgstr ""
+msgid "Errors:"
+msgstr ""
+
msgid "Estimated"
msgstr ""
@@ -8542,12 +8768,18 @@ msgstr ""
msgid "Export as CSV"
msgstr ""
+msgid "Export group"
+msgstr ""
+
msgid "Export issues"
msgstr ""
msgid "Export project"
msgstr ""
+msgid "Export this group with all related data to a new GitLab instance. Once complete, you can import the data file from the \"New Group\" page."
+msgstr ""
+
msgid "Export this project with all its related data in order to move your project to a new GitLab instance. Once the export is finished, you can import the file from the \"New Project\" page."
msgstr ""
@@ -8617,9 +8849,6 @@ msgstr ""
msgid "Failed Jobs"
msgstr ""
-msgid "Failed create wiki"
-msgstr ""
-
msgid "Failed to add a Zoom meeting"
msgstr ""
@@ -8659,6 +8888,9 @@ msgstr ""
msgid "Failed to create resources"
msgstr ""
+msgid "Failed to create wiki"
+msgstr ""
+
msgid "Failed to delete board. Please try again."
msgstr ""
@@ -8668,7 +8900,7 @@ msgstr ""
msgid "Failed to enqueue the rebase operation, possibly due to a long-lived transaction. Try again later."
msgstr ""
-msgid "Failed to find import label for jira import."
+msgid "Failed to find import label for Jira import."
msgstr ""
msgid "Failed to get ref."
@@ -8692,6 +8924,12 @@ msgstr ""
msgid "Failed to load groups & users."
msgstr ""
+msgid "Failed to load labels. Please try again."
+msgstr ""
+
+msgid "Failed to load milestones. Please try again."
+msgstr ""
+
msgid "Failed to load related branches"
msgstr ""
@@ -9067,7 +9305,10 @@ msgstr ""
msgid "Filter by milestone name"
msgstr ""
-msgid "Filter by name..."
+msgid "Filter by name"
+msgstr ""
+
+msgid "Filter by status"
msgstr ""
msgid "Filter by two-factor authentication"
@@ -9079,12 +9320,18 @@ msgstr ""
msgid "Filter projects"
msgstr ""
+msgid "Filter results"
+msgstr ""
+
msgid "Filter results by group"
msgstr ""
msgid "Filter results by project"
msgstr ""
+msgid "Filter results..."
+msgstr ""
+
msgid "Filter your projects by name"
msgstr ""
@@ -9373,10 +9620,10 @@ msgstr ""
msgid "GeoNodes|Checksummed"
msgstr ""
-msgid "GeoNodes|Container repositories"
+msgid "GeoNodes|Consult Geo troubleshooting information"
msgstr ""
-msgid "GeoNodes|Data is out of date from %{timeago}"
+msgid "GeoNodes|Container repositories"
msgstr ""
msgid "GeoNodes|Data replication lag"
@@ -9418,6 +9665,9 @@ msgstr ""
msgid "GeoNodes|Last event ID seen from primary"
msgstr ""
+msgid "GeoNodes|Learn more about Geo node statuses"
+msgstr ""
+
msgid "GeoNodes|Loading nodes"
msgstr ""
@@ -9433,6 +9683,9 @@ msgstr ""
msgid "GeoNodes|Node was successfully removed."
msgstr ""
+msgid "GeoNodes|Node's status was updated %{timeAgo}."
+msgstr ""
+
msgid "GeoNodes|Not checksummed"
msgstr ""
@@ -9463,7 +9716,10 @@ msgstr ""
msgid "GeoNodes|Repository verification progress"
msgstr ""
-msgid "GeoNodes|Selective"
+msgid "GeoNodes|Selective (%{syncLabel})"
+msgstr ""
+
+msgid "GeoNodes|Selective synchronization"
msgstr ""
msgid "GeoNodes|Something went wrong while changing node status"
@@ -9490,6 +9746,9 @@ msgstr ""
msgid "GeoNodes|Unverified"
msgstr ""
+msgid "GeoNodes|Updated %{timeAgo}"
+msgstr ""
+
msgid "GeoNodes|Used slots"
msgstr ""
@@ -9529,16 +9788,16 @@ msgstr ""
msgid "Geo|All"
msgstr ""
-msgid "Geo|All projects"
+msgid "Geo|All %{replicable_type}"
msgstr ""
-msgid "Geo|All projects are being scheduled for re-sync"
+msgid "Geo|All projects"
msgstr ""
-msgid "Geo|All projects are being scheduled for re-verify"
+msgid "Geo|All projects are being scheduled for resync"
msgstr ""
-msgid "Geo|Batch operations"
+msgid "Geo|All projects are being scheduled for reverify"
msgstr ""
msgid "Geo|Could not remove tracking entry for an existing project."
@@ -9550,9 +9809,15 @@ msgstr ""
msgid "Geo|Failed"
msgstr ""
+msgid "Geo|Filter by status"
+msgstr ""
+
msgid "Geo|Geo Status"
msgstr ""
+msgid "Geo|In progress"
+msgstr ""
+
msgid "Geo|In sync"
msgstr ""
@@ -9577,9 +9842,6 @@ msgstr ""
msgid "Geo|Not synced yet"
msgstr ""
-msgid "Geo|Pending"
-msgstr ""
-
msgid "Geo|Pending synchronization"
msgstr ""
@@ -9616,7 +9878,7 @@ msgstr ""
msgid "Geo|Resync"
msgstr ""
-msgid "Geo|Resync all projects"
+msgid "Geo|Resync all"
msgstr ""
msgid "Geo|Retry count"
@@ -9625,7 +9887,7 @@ msgstr ""
msgid "Geo|Reverify"
msgstr ""
-msgid "Geo|Reverify all projects"
+msgid "Geo|Reverify all"
msgstr ""
msgid "Geo|Status"
@@ -9967,9 +10229,6 @@ msgstr ""
msgid "Go to file"
msgstr ""
-msgid "Go to file (MRs only)"
-msgstr ""
-
msgid "Go to file permalink (while viewing a file)"
msgstr ""
@@ -10414,6 +10673,9 @@ msgstr ""
msgid "GroupSettings|Disable group mentions"
msgstr ""
+msgid "GroupSettings|Export group"
+msgstr ""
+
msgid "GroupSettings|If the parent group's visibility is lower than the group current visibility, visibility levels for subgroups and projects will be changed to match the new parent group's visibility."
msgstr ""
@@ -10492,6 +10754,9 @@ msgstr ""
msgid "Groups (%{count})"
msgstr ""
+msgid "Groups (%{groups})"
+msgstr ""
+
msgid "Groups can also be nested by creating %{subgroup_docs_link_start}subgroups%{subgroup_docs_link_end}."
msgstr ""
@@ -10757,15 +11022,12 @@ msgstr ""
msgid "ID:"
msgstr ""
-msgid "IDE|Allow live previews of JavaScript projects in the Web IDE using CodeSandbox client side evaluation."
+msgid "IDE|Allow live previews of JavaScript projects in the Web IDE using CodeSandbox Live Preview."
msgstr ""
msgid "IDE|Back"
msgstr ""
-msgid "IDE|Client side evaluation"
-msgstr ""
-
msgid "IDE|Commit"
msgstr ""
@@ -10829,6 +11091,9 @@ msgstr ""
msgid "If any job surpasses this timeout threshold, it will be marked as failed. Human readable time input language is accepted like \"1 hour\". Values without specification represent seconds."
msgstr ""
+msgid "If blank, set allowable lifetime to %{instance_level_policy_in_words}, as defined by the instance admin. Once set, existing tokens for users in this group may be revoked."
+msgstr ""
+
msgid "If checked, group owners can manage LDAP group links and LDAP member overrides"
msgstr ""
@@ -10868,6 +11133,9 @@ msgstr ""
msgid "If you lose your recovery codes you can generate new ones, invalidating all previous codes."
msgstr ""
+msgid "If you reach 100%% storage capacity, you will not be able to: %{base_message}"
+msgstr ""
+
msgid "If your HTTP repository is not publicly accessible, add your credentials."
msgstr ""
@@ -10979,6 +11247,9 @@ msgstr ""
msgid "Import repository"
msgstr ""
+msgid "Import started by: %{importInitiator}"
+msgstr ""
+
msgid "Import tasks"
msgstr ""
@@ -11045,6 +11316,9 @@ msgstr ""
msgid "In order to tailor your experience with GitLab we<br>would like to know a bit more about you."
msgstr ""
+msgid "In progress"
+msgstr ""
+
msgid "In the next step, you'll be able to select the projects you want to import."
msgstr ""
@@ -11392,15 +11666,15 @@ msgstr ""
msgid "Issues"
msgstr ""
-msgid "Issues / Merge Requests"
-msgstr ""
-
msgid "Issues Analytics"
msgstr ""
msgid "Issues Rate Limits"
msgstr ""
+msgid "Issues and Merge Requests"
+msgstr ""
+
msgid "Issues can be bugs, tasks or ideas to be discussed. Also, issues are searchable and filterable."
msgstr ""
@@ -11422,10 +11696,10 @@ msgstr ""
msgid "IssuesAnalytics|Avg/Month:"
msgstr ""
-msgid "IssuesAnalytics|Issues created"
+msgid "IssuesAnalytics|Issues opened"
msgstr ""
-msgid "IssuesAnalytics|Issues created per month"
+msgid "IssuesAnalytics|Issues opened per month"
msgstr ""
msgid "IssuesAnalytics|Last 12 months"
@@ -11479,6 +11753,9 @@ msgstr ""
msgid "Jira integration not configured."
msgstr ""
+msgid "Jira project: %{importProject}"
+msgstr ""
+
msgid "JiraService|Events for %{noteable_model_name} are disabled."
msgstr ""
@@ -11868,6 +12145,9 @@ msgstr ""
msgid "Lead"
msgstr ""
+msgid "Lead Time"
+msgstr ""
+
msgid "Learn GitLab"
msgstr ""
@@ -11880,6 +12160,9 @@ msgstr ""
msgid "Learn how to %{no_packages_link_start}publish and share your packages%{no_packages_link_end} with GitLab."
msgstr ""
+msgid "Learn how to enable synchronization"
+msgstr ""
+
msgid "Learn more"
msgstr ""
@@ -12211,9 +12494,6 @@ msgstr ""
msgid "Live preview"
msgstr ""
-msgid "Load more vulnerabilities"
-msgstr ""
-
msgid "Loading"
msgstr ""
@@ -12364,9 +12644,6 @@ msgstr ""
msgid "Manage"
msgstr ""
-msgid "Manage Git repositories with fine-grained access controls that keep your code secure. Perform code reviews and enhance collaboration with merge requests. Each project can also have an issue tracker and a wiki."
-msgstr ""
-
msgid "Manage Web IDE features"
msgstr ""
@@ -12397,6 +12674,9 @@ msgstr ""
msgid "Manage your license"
msgstr ""
+msgid "Managed Account"
+msgstr ""
+
msgid "Manifest"
msgstr ""
@@ -12661,6 +12941,9 @@ msgstr ""
msgid "Merge Requests in Review"
msgstr ""
+msgid "Merge automatically (%{strategy})"
+msgstr ""
+
msgid "Merge commit message"
msgstr ""
@@ -12814,6 +13097,12 @@ msgstr ""
msgid "Merged branches are being deleted. This can take some time depending on the number of branches. Please refresh the page to see changes."
msgstr ""
+msgid "Merged this merge request."
+msgstr ""
+
+msgid "Merges this merge request immediately."
+msgstr ""
+
msgid "Merges this merge request when the pipeline succeeds."
msgstr ""
@@ -12921,12 +13210,15 @@ msgid_plural "Metrics|Edit metrics"
msgstr[0] ""
msgstr[1] ""
-msgid "Metrics|Environment"
+msgid "Metrics|Expand panel"
msgstr ""
msgid "Metrics|For grouping similar metrics"
msgstr ""
+msgid "Metrics|Go back (Esc)"
+msgstr ""
+
msgid "Metrics|Invalid time range, please verify."
msgstr ""
@@ -12957,9 +13249,6 @@ msgstr ""
msgid "Metrics|Refresh dashboard"
msgstr ""
-msgid "Metrics|Show last"
-msgstr ""
-
msgid "Metrics|There was an error creating the dashboard."
msgstr ""
@@ -13038,6 +13327,9 @@ msgstr ""
msgid "Microsoft Azure"
msgstr ""
+msgid "Middleman project with Static Site Editor support"
+msgstr ""
+
msgid "Migrated %{success_count}/%{total_count} files."
msgstr ""
@@ -13483,10 +13775,10 @@ msgstr ""
msgid "Next"
msgstr ""
-msgid "Next file in diff (MRs only)"
+msgid "Next file in diff"
msgstr ""
-msgid "Next unresolved discussion (MRs only)"
+msgid "Next unresolved discussion"
msgstr ""
msgid "Nickname"
@@ -13546,6 +13838,9 @@ msgstr ""
msgid "No changes between %{ref_start}%{source_branch}%{ref_end} and %{ref_start}%{target_branch}%{ref_end}"
msgstr ""
+msgid "No child epics match applied filters"
+msgstr ""
+
msgid "No connection could be made to a Gitaly Server, please check your logs!"
msgstr ""
@@ -13684,15 +13979,6 @@ msgstr ""
msgid "No thanks, don't show this again"
msgstr ""
-msgid "No vulnerabilities found for this group"
-msgstr ""
-
-msgid "No vulnerabilities found for this pipeline"
-msgstr ""
-
-msgid "No vulnerabilities found for this project"
-msgstr ""
-
msgid "No vulnerabilities present"
msgstr ""
@@ -13816,6 +14102,9 @@ msgstr ""
msgid "Nothing to preview."
msgstr ""
+msgid "Nothing to synchronize"
+msgstr ""
+
msgid "Notification events"
msgstr ""
@@ -13987,6 +14276,9 @@ msgstr ""
msgid "Once removed, the fork relationship cannot be restored and you will no longer be able to send merge requests to the source."
msgstr ""
+msgid "Once the exported file is ready you can download it from this page."
+msgstr ""
+
msgid "Once the exported file is ready, you will receive a notification email with a download link, or you can download it from this page."
msgstr ""
@@ -14088,9 +14380,6 @@ msgstr ""
msgid "Open sidebar"
msgstr ""
-msgid "Open source software to collaborate on code"
-msgstr ""
-
msgid "Open: %{openIssuesCount}"
msgstr ""
@@ -14193,6 +14482,9 @@ msgstr ""
msgid "OutdatedBrowser|You can provide feedback %{feedback_link_start}on this issue%{feedback_link_end} or via your usual support channels."
msgstr ""
+msgid "Overridden"
+msgstr ""
+
msgid "Overview"
msgstr ""
@@ -14244,6 +14536,9 @@ msgstr ""
msgid "PackageRegistry|Conan Command"
msgstr ""
+msgid "PackageRegistry|Copy .pypirc content"
+msgstr ""
+
msgid "PackageRegistry|Copy Conan Command"
msgstr ""
@@ -14265,6 +14560,9 @@ msgstr ""
msgid "PackageRegistry|Copy NuGet Setup Command"
msgstr ""
+msgid "PackageRegistry|Copy Pip command"
+msgstr ""
+
msgid "PackageRegistry|Copy and paste this inside your %{codeStart}pom.xml%{codeEnd} %{codeStart}dependencies%{codeEnd} block."
msgstr ""
@@ -14298,6 +14596,12 @@ msgstr ""
msgid "PackageRegistry|For more information on the NuGet registry, %{linkStart}see the documentation%{linkEnd}."
msgstr ""
+msgid "PackageRegistry|For more information on the PyPi registry, %{linkStart}see the documentation%{linkEnd}."
+msgstr ""
+
+msgid "PackageRegistry|If you haven't already done so, you will need to add the below to your %{codeStart}.pypirc%{codeEnd} file."
+msgstr ""
+
msgid "PackageRegistry|If you haven't already done so, you will need to add the below to your %{codeStart}pom.xml%{codeEnd} file."
msgstr ""
@@ -14307,6 +14611,9 @@ msgstr ""
msgid "PackageRegistry|Learn how to %{noPackagesLinkStart}publish and share your packages%{noPackagesLinkEnd} with GitLab."
msgstr ""
+msgid "PackageRegistry|Manually Published"
+msgstr ""
+
msgid "PackageRegistry|Maven"
msgstr ""
@@ -14325,12 +14632,18 @@ msgstr ""
msgid "PackageRegistry|NuGet Command"
msgstr ""
+msgid "PackageRegistry|Pip Command"
+msgstr ""
+
msgid "PackageRegistry|Pipeline %{linkStart}%{linkEnd} triggered %{timestamp} by %{author}"
msgstr ""
msgid "PackageRegistry|Published to the repository at %{timestamp}"
msgstr ""
+msgid "PackageRegistry|PyPi"
+msgstr ""
+
msgid "PackageRegistry|Registry Setup"
msgstr ""
@@ -14367,6 +14680,9 @@ msgstr ""
msgid "PackageRegistry|npm"
msgstr ""
+msgid "PackageRegistry|published by %{author}"
+msgstr ""
+
msgid "PackageRegistry|yarn"
msgstr ""
@@ -14382,6 +14698,9 @@ msgstr ""
msgid "PackageType|NuGet"
msgstr ""
+msgid "PackageType|PyPi"
+msgstr ""
+
msgid "Packages"
msgstr ""
@@ -14475,6 +14794,9 @@ msgstr ""
msgid "Past due"
msgstr ""
+msgid "Paste a machine public key here. Read more about how to generate it"
+msgstr ""
+
msgid "Paste a machine public key here. Read more about how to generate it %{link_start}here%{link_end}"
msgstr ""
@@ -14490,9 +14812,6 @@ msgstr ""
msgid "Path"
msgstr ""
-msgid "Path, transfer, remove"
-msgstr ""
-
msgid "Path:"
msgstr ""
@@ -14523,7 +14842,7 @@ msgstr ""
msgid "Percentage"
msgstr ""
-msgid "Perform advanced options such as changing path, transferring, or removing the group."
+msgid "Perform advanced options such as changing path, transferring, exporting, or removing the group."
msgstr ""
msgid "Perform common operations on GitLab project"
@@ -14604,9 +14923,6 @@ msgstr ""
msgid "Pipeline minutes quota"
msgstr ""
-msgid "Pipeline quota"
-msgstr ""
-
msgid "Pipeline subscriptions"
msgstr ""
@@ -14616,6 +14932,9 @@ msgstr ""
msgid "Pipeline: %{status}"
msgstr ""
+msgid "PipelineCharts|CI / CD Analytics"
+msgstr ""
+
msgid "PipelineCharts|Failed:"
msgstr ""
@@ -14694,15 +15013,6 @@ msgstr ""
msgid "Pipelines settings for '%{project_name}' were successfully updated."
msgstr ""
-msgid "Pipelines| to purchase more minutes."
-msgstr ""
-
-msgid "Pipelines|%{namespace_name} has exceeded its pipeline minutes quota."
-msgstr ""
-
-msgid "Pipelines|%{namespace_name} has less than %{notification_level}%% of CI minutes available."
-msgstr ""
-
msgid "Pipelines|API"
msgstr ""
@@ -14724,10 +15034,13 @@ msgstr ""
msgid "Pipelines|Get started with Pipelines"
msgstr ""
-msgid "Pipelines|Loading Pipelines"
+msgid "Pipelines|Group %{namespace_name} has %{percentage}%% or less Shared Runner Pipeline minutes remaining. Once it runs out, no new jobs or pipelines in its projects will run."
msgstr ""
-msgid "Pipelines|Pipelines will not run anymore on shared Runners."
+msgid "Pipelines|Group %{namespace_name} has exceeded its pipeline minutes quota. Unless you buy additional pipeline minutes, no new jobs or pipelines in its projects will run."
+msgstr ""
+
+msgid "Pipelines|Loading Pipelines"
msgstr ""
msgid "Pipelines|Project cache successfully reset."
@@ -14958,6 +15271,9 @@ msgstr ""
msgid "Please select"
msgstr ""
+msgid "Please select a Jira project"
+msgstr ""
+
msgid "Please select a country"
msgstr ""
@@ -15003,6 +15319,9 @@ msgstr ""
msgid "Please wait while we import the repository for you. Refresh at will."
msgstr ""
+msgid "Plugins directory is deprecated and will be removed in 14.0. Please move this file into /file_hooks directory."
+msgstr ""
+
msgid "Pod does not exist"
msgstr ""
@@ -15120,7 +15439,7 @@ msgstr ""
msgid "Prevent users from changing their profile name"
msgstr ""
-msgid "Prevent users from modifing merge request approvers list"
+msgid "Prevent users from modifying merge request approvers list"
msgstr ""
msgid "Prevent users from performing write operations on GitLab while performing maintenance."
@@ -15141,10 +15460,10 @@ msgstr ""
msgid "Previous Artifacts"
msgstr ""
-msgid "Previous file in diff (MRs only)"
+msgid "Previous file in diff"
msgstr ""
-msgid "Previous unresolved discussion (MRs only)"
+msgid "Previous unresolved discussion"
msgstr ""
msgid "Primary"
@@ -15177,6 +15496,9 @@ msgstr ""
msgid "Private profile"
msgstr ""
+msgid "Private projects Minutes cost factor"
+msgstr ""
+
msgid "Private projects can be created in your personal namespace with:"
msgstr ""
@@ -16005,6 +16327,9 @@ msgstr ""
msgid "ProjectSettings|With GitLab Pages you can host your static websites on GitLab"
msgstr ""
+msgid "ProjectSettings|With Metrics Dashboard you can visualize this project performance metrics"
+msgstr ""
+
msgid "ProjectTemplates|.NET Core"
msgstr ""
@@ -16068,6 +16393,9 @@ msgstr ""
msgid "ProjectTemplates|Spring"
msgstr ""
+msgid "ProjectTemplates|Static Site Editor/Middleman"
+msgstr ""
+
msgid "ProjectTemplates|iOS (Swift)"
msgstr ""
@@ -16287,13 +16615,13 @@ msgstr ""
msgid "PrometheusService|Prometheus is being automatically managed on your clusters"
msgstr ""
-msgid "PrometheusService|These metrics will only be monitored after your first deployment to an environment"
+msgid "PrometheusService|Select the Active checkbox to override the Auto Configuration with custom settings. If unchecked, Auto Configuration settings are used."
msgstr ""
-msgid "PrometheusService|Time-series monitoring service"
+msgid "PrometheusService|These metrics will only be monitored after your first deployment to an environment"
msgstr ""
-msgid "PrometheusService|To enable manual configuration, uninstall Prometheus from your clusters"
+msgid "PrometheusService|Time-series monitoring service"
msgstr ""
msgid "PrometheusService|To enable the installation of Prometheus on your clusters, deactivate the manual configuration below"
@@ -16479,6 +16807,9 @@ msgstr ""
msgid "Protip:"
msgstr ""
+msgid "Protocol"
+msgstr ""
+
msgid "Provider"
msgstr ""
@@ -16503,6 +16834,9 @@ msgstr ""
msgid "Public pipelines"
msgstr ""
+msgid "Public projects Minutes cost factor"
+msgstr ""
+
msgid "Pull"
msgstr ""
@@ -16703,6 +17037,9 @@ msgid_plural "Refreshing in %d seconds to show the updated status..."
msgstr[0] ""
msgstr[1] ""
+msgid "Regenerate export"
+msgstr ""
+
msgid "Regenerate instance ID"
msgstr ""
@@ -17080,9 +17417,29 @@ msgstr ""
msgid "Reports|%{combinedString} and %{resolvedString}"
msgstr ""
+msgid "Reports|Accessibility scanning detected %d issue for the source branch only"
+msgid_plural "Reports|Accessibility scanning detected %d issues for the source branch only"
+msgstr[0] ""
+msgstr[1] ""
+
+msgid "Reports|Accessibility scanning detected no issues for the source branch only"
+msgstr ""
+
+msgid "Reports|Accessibility scanning failed loading results"
+msgstr ""
+
+msgid "Reports|Accessibility scanning results are being parsed"
+msgstr ""
+
msgid "Reports|Actions"
msgstr ""
+msgid "Reports|An error occured while loading report"
+msgstr ""
+
+msgid "Reports|An error occurred while loading %{name} results"
+msgstr ""
+
msgid "Reports|Class"
msgstr ""
@@ -17188,7 +17545,7 @@ msgstr ""
msgid "Requested %{time_ago}"
msgstr ""
-msgid "Requested design version does not exist"
+msgid "Requested design version does not exist."
msgstr ""
msgid "Requested states are invalid"
@@ -17372,6 +17729,9 @@ msgstr ""
msgid "Resync"
msgstr ""
+msgid "Resync all"
+msgstr ""
+
msgid "Resync all %{replicableType}"
msgstr ""
@@ -17440,6 +17800,9 @@ msgstr ""
msgid "Revoked personal access token %{personal_access_token_name}!"
msgstr ""
+msgid "Revoked project access token %{project_access_token_name}!"
+msgstr ""
+
msgid "RightSidebar|adding a"
msgstr ""
@@ -17488,6 +17851,9 @@ msgstr ""
msgid "Runner token"
msgstr ""
+msgid "Runner tokens"
+msgstr ""
+
msgid "Runner was not updated."
msgstr ""
@@ -17542,6 +17908,9 @@ msgstr ""
msgid "SAML SSO for %{group_name}"
msgstr ""
+msgid "SAML discovery tokens"
+msgstr ""
+
msgid "SAML for %{group_name}"
msgstr ""
@@ -17626,12 +17995,18 @@ msgstr ""
msgid "Scheduled"
msgstr ""
+msgid "Scheduled to merge this merge request (%{strategy})."
+msgstr ""
+
msgid "Scheduled to merge this merge request when the pipeline succeeds."
msgstr ""
msgid "Schedules"
msgstr ""
+msgid "Schedules to merge this merge request (%{strategy})."
+msgstr ""
+
msgid "Scheduling"
msgstr ""
@@ -17689,6 +18064,9 @@ msgstr ""
msgid "Search branches and tags"
msgstr ""
+msgid "Search by author"
+msgstr ""
+
msgid "Search files"
msgstr ""
@@ -17834,11 +18212,6 @@ msgid_plural "SearchResults|snippets"
msgstr[0] ""
msgstr[1] ""
-msgid "SearchResults|snippet result"
-msgid_plural "SearchResults|snippet results"
-msgstr[0] ""
-msgstr[1] ""
-
msgid "SearchResults|user"
msgid_plural "SearchResults|users"
msgstr[0] ""
@@ -17849,6 +18222,9 @@ msgid_plural "SearchResults|wiki results"
msgstr[0] ""
msgstr[1] ""
+msgid "Searching by both author and message is currently not supported."
+msgstr ""
+
msgid "Seat Link"
msgstr ""
@@ -17876,184 +18252,232 @@ msgstr ""
msgid "Security Dashboard"
msgstr ""
-msgid "Security Dashboard|Error fetching the vulnerability counts. Please check your network connection and try again."
+msgid "Security configuration help link"
+msgstr ""
+
+msgid "Security dashboard"
msgstr ""
-msgid "Security Dashboard|Error fetching the vulnerability list. Please check your network connection and try again."
+msgid "Security report is out of date. Please update your branch with the latest changes from the target branch (%{targetBranchName})"
msgstr ""
-msgid "Security Dashboard|Issue Created"
+msgid "Security report is out of date. Run %{newPipelineLinkStart}a new pipeline%{newPipelineLinkEnd} for the target branch (%{targetBranchName})"
msgstr ""
-msgid "Security Reports|Comment added to '%{vulnerabilityName}'"
+msgid "SecurityConfiguration|Configured"
msgstr ""
-msgid "Security Reports|Comment deleted on '%{vulnerabilityName}'"
+msgid "SecurityConfiguration|Feature"
msgstr ""
-msgid "Security Reports|Comment edited on '%{vulnerabilityName}'"
+msgid "SecurityConfiguration|Feature documentation for %{featureName}"
msgstr ""
-msgid "Security Reports|Create issue"
+msgid "SecurityConfiguration|Not yet configured"
msgstr ""
-msgid "Security Reports|Dismiss vulnerability"
+msgid "SecurityConfiguration|Secure features"
msgstr ""
-msgid "Security Reports|Dismissed '%{vulnerabilityName}'"
+msgid "SecurityConfiguration|Status"
msgstr ""
-msgid "Security Reports|Dismissed '%{vulnerabilityName}'. Turn off the hide dismissed toggle to view."
+msgid "SecurityReports|%{firstProject} and %{secondProject}"
msgstr ""
-msgid "Security Reports|Either you don't have permission to view this dashboard or the dashboard has not been setup. Please check your permission settings with your administrator or check your dashboard configurations to proceed."
+msgid "SecurityReports|%{firstProject}, %{secondProject}, and %{rest}"
msgstr ""
-msgid "Security Reports|Learn more about setting up your dashboard"
+msgid "SecurityReports|Add a project to your dashboard"
msgstr ""
-msgid "Security Reports|More info"
+msgid "SecurityReports|Add or remove projects from your dashboard"
msgstr ""
-msgid "Security Reports|Oops, something doesn't seem right."
+msgid "SecurityReports|Add projects"
msgstr ""
-msgid "Security Reports|Security reports can only be accessed by authorized users."
+msgid "SecurityReports|Comment added to '%{vulnerabilityName}'"
msgstr ""
-msgid "Security Reports|There was an error adding the comment."
+msgid "SecurityReports|Comment deleted on '%{vulnerabilityName}'"
msgstr ""
-msgid "Security Reports|There was an error creating the issue."
+msgid "SecurityReports|Comment edited on '%{vulnerabilityName}'"
msgstr ""
-msgid "Security Reports|There was an error creating the merge request."
+msgid "SecurityReports|Create issue"
msgstr ""
-msgid "Security Reports|There was an error deleting the comment."
+msgid "SecurityReports|Dismiss Selected"
msgstr ""
-msgid "Security Reports|There was an error dismissing the vulnerabilities."
+msgid "SecurityReports|Dismiss vulnerability"
msgstr ""
-msgid "Security Reports|There was an error dismissing the vulnerability."
+msgid "SecurityReports|Dismissed '%{vulnerabilityName}'"
msgstr ""
-msgid "Security Reports|There was an error reverting the dismissal."
+msgid "SecurityReports|Dismissed '%{vulnerabilityName}'. Turn off the hide dismissed toggle to view."
msgstr ""
-msgid "Security Reports|There was an error reverting this dismissal."
+msgid "SecurityReports|Each vulnerability now has a unique page that can be directly linked to, shared, referenced, and tracked as the single source of truth. Vulnerability occurrences also persist across scanner runs, which improves tracking and visibility and reduces duplicates between scans."
msgstr ""
-msgid "Security Reports|Undo dismiss"
+msgid "SecurityReports|Edit dashboard"
msgstr ""
-msgid "Security Reports|You do not have sufficient permissions to access this report"
+msgid "SecurityReports|Either you don't have permission to view this dashboard or the dashboard has not been setup. Please check your permission settings with your administrator or check your dashboard configurations to proceed."
msgstr ""
-msgid "Security Reports|You must sign in as an authorized user to see this report"
+msgid "SecurityReports|Error fetching the vulnerability counts. Please check your network connection and try again."
msgstr ""
-msgid "Security configuration help link"
+msgid "SecurityReports|Error fetching the vulnerability list. Please check your network connection and try again."
msgstr ""
-msgid "Security dashboard"
+msgid "SecurityReports|False positive"
msgstr ""
-msgid "Security report is out of date. Please update your branch with the latest changes from the target branch (%{targetBranchName})"
+msgid "SecurityReports|Hide dismissed"
msgstr ""
-msgid "Security report is out of date. Run %{newPipelineLinkStart}a new pipeline%{newPipelineLinkEnd} for the target branch (%{targetBranchName})"
+msgid "SecurityReports|Introducing standalone vulnerabilities"
msgstr ""
-msgid "SecurityConfiguration|Configured"
+msgid "SecurityReports|Issue Created"
msgstr ""
-msgid "SecurityConfiguration|Feature"
+msgid "SecurityReports|Learn More"
msgstr ""
-msgid "SecurityConfiguration|Feature documentation for %{featureName}"
+msgid "SecurityReports|Learn more about setting up your dashboard"
msgstr ""
-msgid "SecurityConfiguration|Not yet configured"
+msgid "SecurityReports|Load more vulnerabilities"
msgstr ""
-msgid "SecurityConfiguration|Secure features"
+msgid "SecurityReports|Monitor vulnerabilities in your code"
msgstr ""
-msgid "SecurityConfiguration|Status"
+msgid "SecurityReports|More info"
msgstr ""
-msgid "SecurityDashboard|%{firstProject} and %{secondProject}"
+msgid "SecurityReports|More information"
msgstr ""
-msgid "SecurityDashboard|%{firstProject}, %{secondProject}, and %{rest}"
+msgid "SecurityReports|No vulnerabilities found for dashboard"
msgstr ""
-msgid "SecurityDashboard|Add a project to your dashboard"
+msgid "SecurityReports|No vulnerabilities found for this group"
msgstr ""
-msgid "SecurityDashboard|Add or remove projects from your dashboard"
+msgid "SecurityReports|No vulnerabilities found for this pipeline"
msgstr ""
-msgid "SecurityDashboard|Add projects"
+msgid "SecurityReports|No vulnerabilities found for this project"
msgstr ""
-msgid "SecurityDashboard|Edit dashboard"
+msgid "SecurityReports|Oops, something doesn't seem right."
msgstr ""
-msgid "SecurityDashboard|Hide dismissed"
+msgid "SecurityReports|Pipeline %{pipelineLink} triggered %{timeago} by %{user}"
msgstr ""
-msgid "SecurityDashboard|Monitor vulnerabilities in your code"
+msgid "SecurityReports|Project"
msgstr ""
-msgid "SecurityDashboard|More information"
+msgid "SecurityReports|Projects added"
msgstr ""
-msgid "SecurityDashboard|No vulnerabilities found for dashboard"
+msgid "SecurityReports|Remove project from dashboard"
msgstr ""
-msgid "SecurityDashboard|Pipeline %{pipelineLink} triggered %{timeago} by %{user}"
+msgid "SecurityReports|Report type"
msgstr ""
-msgid "SecurityDashboard|Project"
+msgid "SecurityReports|Return to dashboard"
msgstr ""
-msgid "SecurityDashboard|Projects added"
+msgid "SecurityReports|Security Dashboard"
msgstr ""
-msgid "SecurityDashboard|Remove project from dashboard"
+msgid "SecurityReports|Security reports can only be accessed by authorized users."
msgstr ""
-msgid "SecurityDashboard|Report type"
+msgid "SecurityReports|Select a project to add by using the project search field above."
msgstr ""
-msgid "SecurityDashboard|Return to dashboard"
+msgid "SecurityReports|Select a reason"
msgstr ""
-msgid "SecurityDashboard|Security Dashboard"
+msgid "SecurityReports|Severity"
msgstr ""
-msgid "SecurityDashboard|Select a project to add by using the project search field above."
+msgid "SecurityReports|Status"
msgstr ""
-msgid "SecurityDashboard|Severity"
+msgid "SecurityReports|The security dashboard displays the latest security findings for projects you wish to monitor. Select \"Edit dashboard\" to add and remove projects."
msgstr ""
-msgid "SecurityDashboard|Status"
+msgid "SecurityReports|The security dashboard displays the latest security report. Use it to find and fix vulnerabilities."
msgstr ""
-msgid "SecurityDashboard|The security dashboard displays the latest security findings for projects you wish to monitor. Select \"Edit dashboard\" to add and remove projects."
+msgid "SecurityReports|There was an error adding the comment."
msgstr ""
-msgid "SecurityDashboard|The security dashboard displays the latest security report. Use it to find and fix vulnerabilities."
+msgid "SecurityReports|There was an error creating the issue."
msgstr ""
-msgid "SecurityDashboard|There was an error while generating the report."
+msgid "SecurityReports|There was an error creating the merge request."
msgstr ""
-msgid "SecurityDashboard|Unable to add %{invalidProjects}"
+msgid "SecurityReports|There was an error deleting the comment."
+msgstr ""
+
+msgid "SecurityReports|There was an error dismissing the vulnerabilities."
+msgstr ""
+
+msgid "SecurityReports|There was an error dismissing the vulnerability."
+msgstr ""
+
+msgid "SecurityReports|There was an error reverting the dismissal."
+msgstr ""
+
+msgid "SecurityReports|There was an error reverting this dismissal."
+msgstr ""
+
+msgid "SecurityReports|There was an error while generating the report."
+msgstr ""
+
+msgid "SecurityReports|Unable to add %{invalidProjects}"
+msgstr ""
+
+msgid "SecurityReports|Undo dismiss"
+msgstr ""
+
+msgid "SecurityReports|While it's rare to have no vulnerabilities for your group, it can happen. In any event, we ask that you double check your settings to make sure you've set up your dashboard correctly."
+msgstr ""
+
+msgid "SecurityReports|While it's rare to have no vulnerabilities for your pipeline, it can happen. In any event, we ask that you double check your settings to make sure all security scanning jobs have passed successfully."
+msgstr ""
+
+msgid "SecurityReports|While it's rare to have no vulnerabilities for your project, it can happen. In any event, we ask that you double check your settings to make sure you've set up your dashboard correctly."
+msgstr ""
+
+msgid "SecurityReports|While it's rare to have no vulnerabilities, it can happen. In any event, we ask that you please double check your settings to make sure you've set up your dashboard correctly."
+msgstr ""
+
+msgid "SecurityReports|Won't fix / Accept risk"
+msgstr ""
+
+msgid "SecurityReports|You do not have sufficient permissions to access this report"
+msgstr ""
+
+msgid "SecurityReports|You must sign in as an authorized user to see this report"
+msgstr ""
+
+msgid "SecurityReports|[No reason]"
msgstr ""
msgid "See GitLab's %{password_policy_guidelines}"
@@ -18065,6 +18489,9 @@ msgstr ""
msgid "See the affected projects in the GitLab admin panel"
msgstr ""
+msgid "See what's new at GitLab"
+msgstr ""
+
msgid "Select"
msgstr ""
@@ -18470,9 +18897,6 @@ msgstr ""
msgid "Set up Jira Integration"
msgstr ""
-msgid "Set up Jira Integration illustration"
-msgstr ""
-
msgid "Set up a %{type} Runner automatically"
msgstr ""
@@ -18557,6 +18981,9 @@ msgstr ""
msgid "Severity: %{severity}"
msgstr ""
+msgid "Shards (%{shards})"
+msgstr ""
+
msgid "Shards to synchronize"
msgstr ""
@@ -18871,6 +19298,9 @@ msgstr ""
msgid "Snowplow"
msgstr ""
+msgid "Some child epics may be hidden due to applied filters"
+msgstr ""
+
msgid "Some email servers do not support overriding the email sender name. Enable this option to include the name of the author of the issue, merge request or comment in the email body instead."
msgstr ""
@@ -19012,6 +19442,9 @@ msgstr ""
msgid "Something went wrong while updating your list settings"
msgstr ""
+msgid "Something went wrong with your automatic subscription renewal"
+msgstr ""
+
msgid "Something went wrong, unable to add %{project} to dashboard"
msgstr ""
@@ -19297,6 +19730,9 @@ msgstr ""
msgid "Stage removed"
msgstr ""
+msgid "Standard"
+msgstr ""
+
msgid "Star a label to make it a priority label. Order the prioritized labels to change their relative priority, by dragging."
msgstr ""
@@ -19420,6 +19856,9 @@ msgstr ""
msgid "Static Application Security Testing (SAST)"
msgstr ""
+msgid "StaticSiteEditor|An error occurred while submitting your changes."
+msgstr ""
+
msgid "StaticSiteEditor|Branch could not be created."
msgstr ""
@@ -19429,18 +19868,30 @@ msgstr ""
msgid "StaticSiteEditor|Could not create merge request."
msgstr ""
+msgid "StaticSiteEditor|Incompatible file content"
+msgstr ""
+
msgid "StaticSiteEditor|Return to site"
msgstr ""
+msgid "StaticSiteEditor|Static site editor"
+msgstr ""
+
msgid "StaticSiteEditor|Success!"
msgstr ""
msgid "StaticSiteEditor|Summary of changes"
msgstr ""
+msgid "StaticSiteEditor|The Static Site Editor is currently configured to only edit Markdown content on pages generated from Middleman. Visit the documentation to learn more about configuring your site to use the Static Site Editor."
+msgstr ""
+
msgid "StaticSiteEditor|Update %{sourcePath} file"
msgstr ""
+msgid "StaticSiteEditor|View documentation"
+msgstr ""
+
msgid "StaticSiteEditor|View merge request"
msgstr ""
@@ -19498,6 +19949,12 @@ msgstr ""
msgid "StatusPage|Status page"
msgstr ""
+msgid "StatusPage|Status page URL"
+msgstr ""
+
+msgid "StatusPage|Status page frontend documentation"
+msgstr ""
+
msgid "StatusPage|To publish incidents to an external status page, GitLab will store a JSON file in your Amazon S3 account in a location accessible to your external status page service. Make sure to also set up %{docsLink}"
msgstr ""
@@ -19846,6 +20303,12 @@ msgstr ""
msgid "Synced"
msgstr ""
+msgid "Synchronization disabled"
+msgstr ""
+
+msgid "Synchronization of container repositories is disabled."
+msgstr ""
+
msgid "System"
msgstr ""
@@ -19999,6 +20462,9 @@ msgstr ""
msgid "Telephone number"
msgstr ""
+msgid "Telephone number (Optional)"
+msgstr ""
+
msgid "Template"
msgstr ""
@@ -20305,6 +20771,9 @@ msgstr ""
msgid "The license was successfully uploaded and is now active. You can see the details below."
msgstr ""
+msgid "The license was successfully uploaded and will be active from %{starts_at}. You can see the details below."
+msgstr ""
+
msgid "The maximum file size allowed is %{size}."
msgstr ""
@@ -20626,6 +21095,9 @@ msgstr ""
msgid "There was an error getting the epic participants."
msgstr ""
+msgid "There was an error importing the Jira project."
+msgstr ""
+
msgid "There was an error loading users activity calendar."
msgstr ""
@@ -20641,7 +21113,7 @@ msgstr ""
msgid "There was an error resetting user pipeline minutes."
msgstr ""
-msgid "There was an error saving this Geo Node"
+msgid "There was an error saving this Geo Node."
msgstr ""
msgid "There was an error saving your changes."
@@ -20704,6 +21176,9 @@ msgstr ""
msgid "They can be managed using the %{link}."
msgstr ""
+msgid "Third Party Advisory Link"
+msgstr ""
+
msgid "Third party offers"
msgstr ""
@@ -20740,6 +21215,9 @@ msgstr ""
msgid "This application will be able to:"
msgstr ""
+msgid "This attachment has been truncated to avoid exceeding the maximum allowed attachment size of 15MB. %{written_count} of %{issues_count} issues have been included. Consider re-exporting with a narrower selection of issues."
+msgstr ""
+
msgid "This block is self-referential"
msgstr ""
@@ -20881,6 +21359,12 @@ msgstr ""
msgid "This issue is confidential"
msgstr ""
+msgid "This issue is currently blocked by the following issues: %{issues}."
+msgstr ""
+
+msgid "This issue is in a child epic of the filtered epic"
+msgstr ""
+
msgid "This issue is locked."
msgstr ""
@@ -21079,7 +21563,10 @@ msgstr ""
msgid "This user will be the author of all events in the activity feed that are the result of an update, like new branches being created or new commits being pushed to existing branches. Upon creation or when reassigning you can only assign yourself to be the mirror user."
msgstr ""
-msgid "This variable can not be masked"
+msgid "This variable can not be masked."
+msgstr ""
+
+msgid "This variable does not match the expected pattern."
msgstr ""
msgid "This will help us personalize your onboarding experience."
@@ -21109,10 +21596,10 @@ msgstr ""
msgid "ThreatMonitoring|Application firewall not detected"
msgstr ""
-msgid "ThreatMonitoring|Container Network Policy"
+msgid "ThreatMonitoring|Container Network Policies are not installed or have been disabled. To view this data, ensure your Network Policies are installed and enabled for your cluster."
msgstr ""
-msgid "ThreatMonitoring|Container NetworkPolicies are not installed or has been disabled. To view this data, ensure you NetworkPolicies are installed and enabled for your cluster."
+msgid "ThreatMonitoring|Container Network Policy"
msgstr ""
msgid "ThreatMonitoring|Container NetworkPolicies not detected"
@@ -21145,7 +21632,7 @@ msgstr ""
msgid "ThreatMonitoring|Something went wrong, unable to fetch statistics"
msgstr ""
-msgid "ThreatMonitoring|The firewall is not installed or has been disabled. To view this data, ensure you firewall is installed and enabled for your cluster."
+msgid "ThreatMonitoring|The firewall is not installed or has been disabled. To view this data, ensure the web application firewall is installed and enabled for your cluster."
msgstr ""
msgid "ThreatMonitoring|The graph below is an overview of traffic coming to your application as tracked by the Web Application Firewall (WAF). View the docs for instructions on how to access the WAF logs to see what type of malicious traffic is trying to access your app. The docs link is also accessible by clicking the \"?\" icon next to the title below."
@@ -21214,6 +21701,9 @@ msgstr ""
msgid "Time in seconds GitLab will wait for a response from the external service. When the service does not respond in time, access will be denied."
msgstr ""
+msgid "Time of import: %{importTime}"
+msgstr ""
+
msgid "Time remaining"
msgstr ""
@@ -21398,7 +21888,7 @@ msgstr ""
msgid "Title:"
msgstr ""
-msgid "Titles and Filenames"
+msgid "Titles and Descriptions"
msgstr ""
msgid "To"
@@ -21605,7 +22095,7 @@ msgstr ""
msgid "Too many projects enabled. You will need to manage them via the console or the API."
msgstr ""
-msgid "Topics"
+msgid "Topics (optional)"
msgstr ""
msgid "Total"
@@ -21767,6 +22257,9 @@ msgstr ""
msgid "Try all GitLab has to offer for 30 days."
msgstr ""
+msgid "Try changing or removing filters."
+msgstr ""
+
msgid "Try to fork again"
msgstr ""
@@ -21860,6 +22353,9 @@ msgstr ""
msgid "Unable to connect to server: %{error}"
msgstr ""
+msgid "Unable to connect to the Jira instance. Please check your Jira integration configuration."
+msgstr ""
+
msgid "Unable to convert Kubernetes logs encoding to UTF-8"
msgstr ""
@@ -22803,6 +23299,9 @@ msgstr ""
msgid "View issue"
msgstr ""
+msgid "View issues"
+msgstr ""
+
msgid "View it on GitLab"
msgstr ""
@@ -22980,6 +23479,15 @@ msgstr ""
msgid "VulnerabilityManagement|Resolved %{timeago} by %{user}"
msgstr ""
+msgid "VulnerabilityManagement|Something went wrong while trying to delete the comment. Please try again later."
+msgstr ""
+
+msgid "VulnerabilityManagement|Something went wrong while trying to retrieve the vulnerability history. Please try again later."
+msgstr ""
+
+msgid "VulnerabilityManagement|Something went wrong while trying to save the comment. Please try again later."
+msgstr ""
+
msgid "VulnerabilityManagement|Something went wrong, could not create an issue."
msgstr ""
@@ -23010,6 +23518,9 @@ msgstr ""
msgid "VulnerabilityStatusTypes|Resolved"
msgstr ""
+msgid "Vulnerability|%{scannerName} (version %{scannerVersion})"
+msgstr ""
+
msgid "Vulnerability|Class"
msgstr ""
@@ -23043,7 +23554,10 @@ msgstr ""
msgid "Vulnerability|Project"
msgstr ""
-msgid "Vulnerability|Report Type"
+msgid "Vulnerability|Scanner Provider"
+msgstr ""
+
+msgid "Vulnerability|Scanner Type"
msgstr ""
msgid "Vulnerability|Severity"
@@ -23097,9 +23611,15 @@ msgstr ""
msgid "We sent you an email with reset password instructions"
msgstr ""
+msgid "We tried to automatically renew your %{strong}%{plan_name}%{strong_close} subscription for %{strong}%{namespace_name}%{strong_close} on %{expires_on} but something went wrong so your subscription was downgraded to the free plan. Don't worry, your data is safe. We suggest you check your payment method and get in touch with our support team (%{support_link}). They'll gladly help with your subscription renewal."
+msgstr ""
+
msgid "We want to be sure it is you, please confirm you are not a robot."
msgstr ""
+msgid "We will automatically renew your %{strong}%{plan_name}%{strong_close} subscription for %{strong}%{namespace_name}%{strong_close} on %{strong}%{expires_on}%{strong_close}. There's nothing that you need to do, we'll let you know when the renewal is complete. Need more seats, a higher plan or just want to review your payment method?"
+msgstr ""
+
msgid "We've found no vulnerabilities"
msgstr ""
@@ -23204,15 +23724,6 @@ msgstr ""
msgid "When:"
msgstr ""
-msgid "While it's rare to have no vulnerabilities for your group, it can happen. In any event, we ask that you double check your settings to make sure you've set up your dashboard correctly."
-msgstr ""
-
-msgid "While it's rare to have no vulnerabilities for your pipeline, it can happen. In any event, we ask that you double check your settings to make sure all security scanning jobs have passed successfully."
-msgstr ""
-
-msgid "While it's rare to have no vulnerabilities for your project, it can happen. In any event, we ask that you double check your settings to make sure you've set up your dashboard correctly."
-msgstr ""
-
msgid "While it's rare to have no vulnerabilities, it can happen. In any event, we ask that you please double check your settings to make sure you've set up your dashboard correctly."
msgstr ""
@@ -23243,9 +23754,6 @@ msgstr ""
msgid "Wiki"
msgstr ""
-msgid "Wiki pages"
-msgstr ""
-
msgid "Wiki was successfully updated."
msgstr ""
@@ -23429,6 +23937,9 @@ msgstr ""
msgid "Yes, add it"
msgstr ""
+msgid "Yes, close issue"
+msgstr ""
+
msgid "Yes, let me map Google Code users to full names or GitLab users."
msgstr ""
@@ -23624,6 +24135,9 @@ msgstr ""
msgid "You could not create a new trigger."
msgstr ""
+msgid "You didn't renew your %{strong}%{plan_name}%{strong_close} subscription for %{strong}%{namespace_name}%{strong_close} so it was downgraded to the free plan."
+msgstr ""
+
msgid "You didn't renew your %{strong}%{plan_name}%{strong_close} subscription so it was downgraded to the GitLab Core Plan."
msgstr ""
@@ -23690,6 +24204,9 @@ msgstr ""
msgid "You have declined the invitation to join %{label}."
msgstr ""
+msgid "You have imported from this project %{numberOfPreviousImportsForProject} times before. Each new import will create duplicate issues."
+msgstr ""
+
msgid "You have no permissions"
msgstr ""
@@ -23771,6 +24288,9 @@ msgstr ""
msgid "You need to upload a Google Takeout archive."
msgstr ""
+msgid "You reached %{usage_in_percent} of %{namespace_name}'s capacity (%{used_storage} of %{storage_limit})"
+msgstr ""
+
msgid "You tried to fork %{link_to_the_project} but it failed for the following reason:"
msgstr ""
@@ -23855,9 +24375,21 @@ msgstr ""
msgid "YouTube"
msgstr ""
+msgid "Your %{strong}%{plan_name}%{strong_close} subscription for %{strong}%{namespace_name}%{strong_close} will expire on %{strong}%{expires_on}%{strong_close}. After that, you will not to be able to create issues or merge requests as well as many other features."
+msgstr ""
+
msgid "Your %{strong}%{plan_name}%{strong_close} subscription will expire on %{strong}%{expires_on}%{strong_close}. After that, you will not to be able to create issues or merge requests as well as many other features."
msgstr ""
+msgid "Your CSV export has started. It will be emailed to %{email} when complete."
+msgstr ""
+
+msgid "Your CSV export of %{issues_count} from project %{project_link} has been added to this email as an attachment."
+msgstr ""
+
+msgid "Your CSV export of %{written_count} from project %{project_name} (%{project_url}) has been added to this email as an attachment."
+msgstr ""
+
msgid "Your Commit Email will be used for web based operations, such as edits and merges."
msgstr ""
@@ -23954,6 +24486,9 @@ msgstr ""
msgid "Your comment could not be updated! Please check your network connection and try again."
msgstr ""
+msgid "Your comment will be discarded."
+msgstr ""
+
msgid "Your custom stage '%{title}' was created"
msgstr ""
@@ -23993,6 +24528,9 @@ msgstr ""
msgid "Your new personal access token has been created."
msgstr ""
+msgid "Your new project access token has been created."
+msgstr ""
+
msgid "Your password isn't required to view this page. If a password or any other personal details are requested, please contact your administrator to report abuse."
msgstr ""
@@ -24011,12 +24549,18 @@ msgstr ""
msgid "Your request for access has been queued for review."
msgstr ""
+msgid "Your search didn't match any commits."
+msgstr ""
+
msgid "Your subscription expired!"
msgstr ""
msgid "Your subscription has been downgraded"
msgstr ""
+msgid "Your subscription will automatically renew in %{remaining_days}"
+msgstr ""
+
msgid "Your subscription will expire in %{remaining_days}"
msgstr ""
@@ -24055,7 +24599,7 @@ msgstr ""
msgid "allowed to fail"
msgstr ""
-msgid "already being used for another group or project milestone."
+msgid "already being used for another group or project %{timebox_name}."
msgstr ""
msgid "already has a \"created\" issue link"
@@ -24133,6 +24677,9 @@ msgstr ""
msgid "ciReport|%{linkStartTag}Learn more about SAST %{linkEndTag}"
msgstr ""
+msgid "ciReport|%{linkStartTag}Learn more about Secret Scanning %{linkEndTag}"
+msgstr ""
+
msgid "ciReport|%{linkStartTag}Learn more about codequality reports %{linkEndTag}"
msgstr ""
@@ -24296,6 +24843,12 @@ msgstr ""
msgid "ciReport|SAST"
msgstr ""
+msgid "ciReport|Secret scanning"
+msgstr ""
+
+msgid "ciReport|Secret scanning detects secrets and credentials vulnerabilities in your source code."
+msgstr ""
+
msgid "ciReport|Security scanning"
msgstr ""
@@ -24493,6 +25046,9 @@ msgstr ""
msgid "group"
msgstr ""
+msgid "groups"
+msgstr ""
+
msgid "has already been linked to another vulnerability"
msgstr ""
@@ -24656,9 +25212,6 @@ msgstr[1] ""
msgid "merged %{time_ago}"
msgstr ""
-msgid "milestone should belong either to a project or a group."
-msgstr ""
-
msgid "missing"
msgstr ""
@@ -24995,6 +25548,9 @@ msgstr ""
msgid "no contributions"
msgstr ""
+msgid "no expiration"
+msgstr ""
+
msgid "no one can merge"
msgstr ""
@@ -25047,6 +25603,9 @@ msgstr ""
msgid "pending removal"
msgstr ""
+msgid "per day"
+msgstr ""
+
msgid "pipeline"
msgstr ""
@@ -25075,6 +25634,12 @@ msgstr[1] ""
msgid "project avatar"
msgstr ""
+msgid "projects"
+msgstr ""
+
+msgid "push to your repository, create pipelines, create issues or add comments. To reduce storage capacity, delete unused repositories, artifacts, wikis, issues, and pipelines."
+msgstr ""
+
msgid "quick actions"
msgstr ""
@@ -25221,11 +25786,6 @@ msgstr ""
msgid "this document"
msgstr ""
-msgid "thread resolved"
-msgid_plural "threads resolved"
-msgstr[0] ""
-msgstr[1] ""
-
msgid "to help your contributors communicate effectively!"
msgstr ""
@@ -25280,6 +25840,9 @@ msgstr ""
msgid "view the blob"
msgstr ""
+msgid "vulnerability|Add a comment"
+msgstr ""
+
msgid "vulnerability|Add a comment or reason for dismissal"
msgstr ""
diff --git a/locale/eo/gitlab.po b/locale/eo/gitlab.po
index 2cd570848ef..447e4effc83 100644
--- a/locale/eo/gitlab.po
+++ b/locale/eo/gitlab.po
@@ -12,7 +12,7 @@ msgstr ""
"X-Crowdin-Project: gitlab-ee\n"
"X-Crowdin-Language: eo\n"
"X-Crowdin-File: /master/locale/gitlab.pot\n"
-"PO-Revision-Date: 2020-04-15 00:31\n"
+"PO-Revision-Date: 2020-05-05 21:31\n"
msgid " %{start} to %{end}"
msgstr ""
@@ -212,6 +212,11 @@ msgid_plural "%d tags"
msgstr[0] ""
msgstr[1] ""
+msgid "%d unresolved thread"
+msgid_plural "%d unresolved threads"
+msgstr[0] ""
+msgstr[1] ""
+
msgid "%d vulnerability dismissed"
msgid_plural "%d vulnerabilities dismissed"
msgstr[0] ""
@@ -371,6 +376,9 @@ msgstr ""
msgid "%{mrText}, this issue will be closed automatically."
msgstr ""
+msgid "%{namespace_name} is now read-only. You cannot: %{base_message}"
+msgstr ""
+
msgid "%{name} contained %{resultsString}"
msgstr ""
@@ -383,6 +391,11 @@ msgstr ""
msgid "%{name}'s avatar"
msgstr ""
+msgid "%{no_of_days} day"
+msgid_plural "%{no_of_days} days"
+msgstr[0] ""
+msgstr[1] ""
+
msgid "%{number_commits_behind} commits behind %{default_branch}, %{number_commits_ahead} commits ahead"
msgstr ""
@@ -506,6 +519,9 @@ msgstr[1] ""
msgid "%{text} is available"
msgstr ""
+msgid "%{timebox_name} should belong either to a project or a group."
+msgstr ""
+
msgid "%{title} %{operator} %{threshold}"
msgstr ""
@@ -781,12 +797,18 @@ msgstr ""
msgid "<code>\"johnsmith@example.com\": \"johnsmith@example.com\"</code> will add \"By <a href=\"#\">johnsmith@example.com</a>\" to all issues and comments originally created by johnsmith@example.com. By default, the email address or username is masked to ensure the user's privacy. Use this option if you want to show the full email address."
msgstr ""
+msgid "<namespace / project>"
+msgstr ""
+
msgid "<no name set>"
msgstr ""
msgid "<no scopes selected>"
msgstr ""
+msgid "<project name>"
+msgstr ""
+
msgid "<strong>%{group_name}</strong> group members"
msgstr ""
@@ -820,6 +842,9 @@ msgstr ""
msgid "A basic page and serverless function that uses AWS Lambda, AWS API Gateway, and GitLab Pages"
msgstr ""
+msgid "A complete DevOps platform"
+msgstr ""
+
msgid "A default branch cannot be chosen for an empty project."
msgstr ""
@@ -1036,6 +1061,12 @@ msgstr ""
msgid "AccessTokens|reset it"
msgstr ""
+msgid "AccessibilityReport|Accessibility report artifact not found"
+msgstr ""
+
+msgid "AccessibilityReport|Failed to retrieve accessibility report"
+msgstr ""
+
msgid "AccessibilityReport|Learn More"
msgstr ""
@@ -1045,6 +1076,9 @@ msgstr ""
msgid "AccessibilityReport|New"
msgstr ""
+msgid "AccessibilityReport|The accessibility scanning found an error of the following type: %{code}"
+msgstr ""
+
msgid "Account"
msgstr ""
@@ -1287,6 +1321,9 @@ msgstr ""
msgid "Added at"
msgstr ""
+msgid "Added for this merge request"
+msgstr ""
+
msgid "Added in this version"
msgstr ""
@@ -1658,6 +1695,72 @@ msgid_plural "Alerts"
msgstr[0] ""
msgstr[1] ""
+msgid "Alert Details"
+msgstr ""
+
+msgid "AlertManagement|Acknowledged"
+msgstr ""
+
+msgid "AlertManagement|Alert"
+msgstr ""
+
+msgid "AlertManagement|Authorize external service"
+msgstr ""
+
+msgid "AlertManagement|Display alerts from all your monitoring tools directly within GitLab. Streamline the investigation of your alerts and the escalation of alerts to incidents."
+msgstr ""
+
+msgid "AlertManagement|End time"
+msgstr ""
+
+msgid "AlertManagement|End time:"
+msgstr ""
+
+msgid "AlertManagement|Events"
+msgstr ""
+
+msgid "AlertManagement|Events:"
+msgstr ""
+
+msgid "AlertManagement|Full Alert Details"
+msgstr ""
+
+msgid "AlertManagement|More information"
+msgstr ""
+
+msgid "AlertManagement|No alerts available to display. If you think you're seeing this message in error, refresh the page."
+msgstr ""
+
+msgid "AlertManagement|No alerts to display."
+msgstr ""
+
+msgid "AlertManagement|Overview"
+msgstr ""
+
+msgid "AlertManagement|Resolved"
+msgstr ""
+
+msgid "AlertManagement|Severity"
+msgstr ""
+
+msgid "AlertManagement|Start time"
+msgstr ""
+
+msgid "AlertManagement|Start time:"
+msgstr ""
+
+msgid "AlertManagement|Status"
+msgstr ""
+
+msgid "AlertManagement|Surface alerts in GitLab"
+msgstr ""
+
+msgid "AlertManagement|There was an error displaying the alerts. Confirm your endpoint's configuration details to ensure alerts appear."
+msgstr ""
+
+msgid "AlertManagement|Triggered"
+msgstr ""
+
msgid "AlertService|%{linkStart}Learn more%{linkEnd} about configuring this endpoint to receive alerts."
msgstr ""
@@ -1727,6 +1830,9 @@ msgstr ""
msgid "All security scans are enabled because %{linkStart}Auto DevOps%{linkEnd} is enabled on this project"
msgstr ""
+msgid "All threads resolved"
+msgstr ""
+
msgid "All users"
msgstr ""
@@ -1745,6 +1851,9 @@ msgstr ""
msgid "Allow only the selected protocols to be used for Git access."
msgstr ""
+msgid "Allow owners to manage default branch protection per group"
+msgstr ""
+
msgid "Allow owners to manually add users outside of LDAP"
msgstr ""
@@ -1853,6 +1962,9 @@ msgstr ""
msgid "An error occurred fetching the dropdown data."
msgstr ""
+msgid "An error occurred fetching the project authors."
+msgstr ""
+
msgid "An error occurred previewing the blob"
msgstr ""
@@ -1934,6 +2046,9 @@ msgstr ""
msgid "An error occurred while fetching sidebar data"
msgstr ""
+msgid "An error occurred while fetching terraform reports."
+msgstr ""
+
msgid "An error occurred while fetching the Service Desk address."
msgstr ""
@@ -2171,6 +2286,9 @@ msgstr ""
msgid "Any"
msgstr ""
+msgid "Any Author"
+msgstr ""
+
msgid "Any Label"
msgstr ""
@@ -2386,10 +2504,10 @@ msgstr ""
msgid "Are you sure that you want to unarchive this project?"
msgstr ""
-msgid "Are you sure you want to cancel creating this comment?"
+msgid "Are you sure you want to cancel editing this comment?"
msgstr ""
-msgid "Are you sure you want to cancel editing this comment?"
+msgid "Are you sure you want to close this blocked issue?"
msgstr ""
msgid "Are you sure you want to delete %{name}?"
@@ -2419,6 +2537,9 @@ msgstr ""
msgid "Are you sure you want to deploy this environment?"
msgstr ""
+msgid "Are you sure you want to discard this comment?"
+msgstr ""
+
msgid "Are you sure you want to erase this build?"
msgstr ""
@@ -2595,6 +2716,9 @@ msgstr ""
msgid "At least one approval from a code owner is required to change files matching the respective CODEOWNER rules."
msgstr ""
+msgid "At least one logging option is required to be enabled"
+msgstr ""
+
msgid "At least one of group_id or project_id must be specified"
msgstr ""
@@ -2633,6 +2757,27 @@ msgstr ""
msgid "AuditEvents|Target"
msgstr ""
+msgid "AuditLogs|(removed)"
+msgstr ""
+
+msgid "AuditLogs|Action"
+msgstr ""
+
+msgid "AuditLogs|Author"
+msgstr ""
+
+msgid "AuditLogs|Date"
+msgstr ""
+
+msgid "AuditLogs|IP Address"
+msgstr ""
+
+msgid "AuditLogs|Object"
+msgstr ""
+
+msgid "AuditLogs|Target"
+msgstr ""
+
msgid "Aug"
msgstr ""
@@ -2915,9 +3060,6 @@ msgstr ""
msgid "BambooService|You must set up automatic revision labeling and a repository trigger in Bamboo."
msgstr ""
-msgid "Batch operations"
-msgstr ""
-
msgid "BatchComments|Delete all pending comments"
msgstr ""
@@ -3047,6 +3189,9 @@ msgstr ""
msgid "Boards"
msgstr ""
+msgid "Boards and Board Lists"
+msgstr ""
+
msgid "Boards|Collapse"
msgstr ""
@@ -3266,6 +3411,9 @@ msgstr ""
msgid "Buy GitLab Enterprise Edition"
msgstr ""
+msgid "Buy more Pipeline minutes"
+msgstr ""
+
msgid "By %{user_name}"
msgstr ""
@@ -3386,6 +3534,9 @@ msgstr ""
msgid "Can't scan the code?"
msgstr ""
+msgid "Can't update snippet: %{err}"
+msgstr ""
+
msgid "Canary"
msgstr ""
@@ -3554,6 +3705,9 @@ msgstr ""
msgid "Changing group path can have unintended side effects."
msgstr ""
+msgid "Charts"
+msgstr ""
+
msgid "Charts can't be displayed as the request for data has timed out. %{documentationLink}"
msgstr ""
@@ -3590,9 +3744,6 @@ msgstr ""
msgid "ChatMessage|and [%{count} more](%{pipeline_failed_jobs_url})"
msgstr ""
-msgid "ChatMessage|failed"
-msgstr ""
-
msgid "ChatMessage|has failed"
msgstr ""
@@ -3608,9 +3759,6 @@ msgstr ""
msgid "ChatMessage|in %{project_link}"
msgstr ""
-msgid "ChatMessage|passed"
-msgstr ""
-
msgid "Check again"
msgstr ""
@@ -4061,9 +4209,6 @@ msgstr ""
msgid "Click any <strong>project name</strong> in the project list below to navigate to the project milestone."
msgstr ""
-msgid "Click here"
-msgstr ""
-
msgid "Click the <strong>Download</strong> button and wait for downloading to complete."
msgstr ""
@@ -4451,6 +4596,12 @@ msgstr ""
msgid "ClusterIntegration|Fetching zones"
msgstr ""
+msgid "ClusterIntegration|Fluentd"
+msgstr ""
+
+msgid "ClusterIntegration|Fluentd is an open source data collector, which lets you unify the data collection and consumption for a better use and understanding of data. It requires at least one of the following logs to be successfully installed."
+msgstr ""
+
msgid "ClusterIntegration|GitLab Integration"
msgstr ""
@@ -4757,6 +4908,15 @@ msgstr ""
msgid "ClusterIntegration|Request to begin uninstalling failed"
msgstr ""
+msgid "ClusterIntegration|SIEM Hostname"
+msgstr ""
+
+msgid "ClusterIntegration|SIEM Port"
+msgstr ""
+
+msgid "ClusterIntegration|SIEM Protocol"
+msgstr ""
+
msgid "ClusterIntegration|Save changes"
msgstr ""
@@ -4853,6 +5013,12 @@ msgstr ""
msgid "ClusterIntegration|Select zone to choose machine type"
msgstr ""
+msgid "ClusterIntegration|Send Cilium Logs"
+msgstr ""
+
+msgid "ClusterIntegration|Send ModSecurity Logs"
+msgstr ""
+
msgid "ClusterIntegration|Service Token"
msgstr ""
@@ -5281,18 +5447,33 @@ msgstr ""
msgid "Compliance framework (optional)"
msgstr ""
+msgid "ComplianceFramework|GDPR"
+msgstr ""
+
msgid "ComplianceFramework|GDPR - General Data Protection Regulation"
msgstr ""
+msgid "ComplianceFramework|HIPAA"
+msgstr ""
+
msgid "ComplianceFramework|HIPAA - Health Insurance Portability and Accountability Act"
msgstr ""
+msgid "ComplianceFramework|PCI-DSS"
+msgstr ""
+
msgid "ComplianceFramework|PCI-DSS - Payment Card Industry-Data Security Standard"
msgstr ""
+msgid "ComplianceFramework|SOC 2"
+msgstr ""
+
msgid "ComplianceFramework|SOC 2 - Service Organization Control 2"
msgstr ""
+msgid "ComplianceFramework|SOX"
+msgstr ""
+
msgid "ComplianceFramework|SOX - Sarbanes-Oxley"
msgstr ""
@@ -5440,9 +5621,15 @@ msgstr ""
msgid "Container repositories sync capacity"
msgstr ""
+msgid "ContainerRegistry| Please visit the %{linkStart}administration settings%{linkEnd} to enable this feature."
+msgstr ""
+
msgid "ContainerRegistry|%{imageName} tags"
msgstr ""
+msgid "ContainerRegistry|%{title} was successfully scheduled for deletion"
+msgstr ""
+
msgid "ContainerRegistry|Automatically remove extra images that aren't designed to be kept."
msgstr ""
@@ -5455,6 +5642,9 @@ msgstr ""
msgid "ContainerRegistry|Container Registry"
msgstr ""
+msgid "ContainerRegistry|Container Registry tag expiration and retention policy is disabled"
+msgstr ""
+
msgid "ContainerRegistry|Copy build command"
msgstr ""
@@ -5464,18 +5654,12 @@ msgstr ""
msgid "ContainerRegistry|Copy push command"
msgstr ""
-msgid "ContainerRegistry|Currently, the Container Registry tag expiration feature is not available for projects created before GitLab version 12.8. For updates and more information, visit Issue %{linkStart}#196124%{linkEnd}"
-msgstr ""
-
msgid "ContainerRegistry|Docker connection error"
msgstr ""
msgid "ContainerRegistry|Docker tag expiration policy is %{toggleStatus}"
msgstr ""
-msgid "ContainerRegistry|Docker tags with names matching this regex pattern will expire:"
-msgstr ""
-
msgid "ContainerRegistry|Edit Settings"
msgstr ""
@@ -5497,9 +5681,6 @@ msgstr ""
msgid "ContainerRegistry|Image ID"
msgstr ""
-msgid "ContainerRegistry|Image deleted successfully"
-msgstr ""
-
msgid "ContainerRegistry|Keep and protect the images that matter most."
msgstr ""
@@ -5515,12 +5696,24 @@ msgstr ""
msgid "ContainerRegistry|Number of tags to retain:"
msgstr ""
+msgid "ContainerRegistry|Please contact your administrator."
+msgstr ""
+
msgid "ContainerRegistry|Push an image"
msgstr ""
msgid "ContainerRegistry|Quick Start"
msgstr ""
+msgid "ContainerRegistry|Regular expressions such as %{codeStart}.*-test%{codeEnd} or %{codeStart}dev-.*%{codeEnd} are supported"
+msgstr ""
+
+msgid "ContainerRegistry|Regular expressions such as %{codeStart}.*-test%{codeEnd} or %{codeStart}dev-.*%{codeEnd} are supported. To select all tags, use %{codeStart}.*%{codeEnd}"
+msgstr ""
+
+msgid "ContainerRegistry|Remember to run %{docLinkStart}garbage collection%{docLinkEnd} to remove the stale data from storage."
+msgstr ""
+
msgid "ContainerRegistry|Remove repository"
msgstr ""
@@ -5535,22 +5728,22 @@ msgstr[1] ""
msgid "ContainerRegistry|Retention policy has been Enabled"
msgstr ""
-msgid "ContainerRegistry|Something went wrong while deleting the image."
+msgid "ContainerRegistry|Something went wrong while fetching the expiration policy."
msgstr ""
-msgid "ContainerRegistry|Something went wrong while deleting the tag."
+msgid "ContainerRegistry|Something went wrong while fetching the repository list."
msgstr ""
-msgid "ContainerRegistry|Something went wrong while deleting the tags."
+msgid "ContainerRegistry|Something went wrong while fetching the tags list."
msgstr ""
-msgid "ContainerRegistry|Something went wrong while fetching the expiration policy."
+msgid "ContainerRegistry|Something went wrong while marking the tag for deletion."
msgstr ""
-msgid "ContainerRegistry|Something went wrong while fetching the packages list."
+msgid "ContainerRegistry|Something went wrong while marking the tags for deletion."
msgstr ""
-msgid "ContainerRegistry|Something went wrong while fetching the tags list."
+msgid "ContainerRegistry|Something went wrong while scheduling %{title} for deletion. Please try again."
msgstr ""
msgid "ContainerRegistry|Something went wrong while updating the expiration policy."
@@ -5559,16 +5752,25 @@ msgstr ""
msgid "ContainerRegistry|Tag"
msgstr ""
-msgid "ContainerRegistry|Tag deleted successfully"
-msgstr ""
-
msgid "ContainerRegistry|Tag expiration policy"
msgstr ""
msgid "ContainerRegistry|Tag expiration policy is designed to:"
msgstr ""
-msgid "ContainerRegistry|Tags deleted successfully"
+msgid "ContainerRegistry|Tag successfully marked for deletion."
+msgstr ""
+
+msgid "ContainerRegistry|Tags successfully marked for deletion."
+msgstr ""
+
+msgid "ContainerRegistry|Tags with names matching this regex pattern will %{italicStart}be preserved:%{italicEnd}"
+msgstr ""
+
+msgid "ContainerRegistry|Tags with names matching this regex pattern will %{italicStart}expire:%{italicEnd}"
+msgstr ""
+
+msgid "ContainerRegistry|The Container Registry tag expiration and retention policies for this project have not been enabled."
msgstr ""
msgid "ContainerRegistry|The last tag related to this image was recently removed. This empty image and any associated data will be automatically removed as part of the regular Garbage Collection process. If you have any questions, contact your administrator."
@@ -5589,16 +5791,16 @@ msgstr ""
msgid "ContainerRegistry|There are no container images stored for this project"
msgstr ""
-msgid "ContainerRegistry|This Registry contains deleted image tag data. Remember to run %{docLinkStart}garbage collection%{docLinkEnd} to remove the stale data from storage."
+msgid "ContainerRegistry|There was an error during the deletion of this image repository, please try again."
msgstr ""
msgid "ContainerRegistry|This image has no active tags"
msgstr ""
-msgid "ContainerRegistry|We are having trouble connecting to Docker, which could be due to an issue with your project name or path. %{docLinkStart}More Information%{docLinkEnd}"
+msgid "ContainerRegistry|This image repository is scheduled for deletion"
msgstr ""
-msgid "ContainerRegistry|Wildcards such as %{codeStart}.*-stable%{codeEnd} or %{codeStart}production/.*%{codeEnd} are supported. To select all tags, use %{codeStart}.*%{codeEnd}"
+msgid "ContainerRegistry|We are having trouble connecting to Docker, which could be due to an issue with your project name or path. %{docLinkStart}More Information%{docLinkEnd}"
msgstr ""
msgid "ContainerRegistry|With the Container Registry, every project can have its own space to store its Docker images. %{docLinkStart}More Information%{docLinkEnd}"
@@ -5853,7 +6055,7 @@ msgstr ""
msgid "Could not delete chat nickname %{chat_name}."
msgstr ""
-msgid "Could not find design"
+msgid "Could not find design."
msgstr ""
msgid "Could not remove the trigger."
@@ -5868,6 +6070,9 @@ msgstr ""
msgid "Could not revoke personal access token %{personal_access_token_name}."
msgstr ""
+msgid "Could not revoke project access token %{project_access_token_name}."
+msgstr ""
+
msgid "Could not save group ID"
msgstr ""
@@ -5925,6 +6130,9 @@ msgstr ""
msgid "Create a new branch"
msgstr ""
+msgid "Create a new deploy key for this project"
+msgstr ""
+
msgid "Create a new file as there are no files yet. Afterwards, you'll be able to commit your changes."
msgstr ""
@@ -6030,6 +6238,9 @@ msgstr ""
msgid "Create requirement"
msgstr ""
+msgid "Create snippet"
+msgstr ""
+
msgid "Create wildcard: %{searchTerm}"
msgstr ""
@@ -6051,6 +6262,9 @@ msgstr "kreos propran atingoĵetonon"
msgid "Created"
msgstr ""
+msgid "Created %{timestamp}"
+msgstr ""
+
msgid "Created At"
msgstr ""
@@ -6407,6 +6621,9 @@ msgstr ""
msgid "CycleAnalytics|stage dropdown"
msgstr ""
+msgid "DAG"
+msgstr ""
+
msgid "DNS"
msgstr ""
@@ -6773,6 +6990,9 @@ msgstr ""
msgid "Deploy key was successfully updated."
msgstr ""
+msgid "Deploy keys allow read-only or read-write (if enabled) access to your repository. Deploy keys can be used for CI, staging or production servers. You can create a deploy key or add an existing one."
+msgstr ""
+
msgid "Deploy progress not found. To see pods, ensure your environment matches %{linkStart}deploy board criteria%{linkEnd}."
msgstr ""
@@ -6926,6 +7146,9 @@ msgstr ""
msgid "Deploying to"
msgstr ""
+msgid "Deployment Frequency"
+msgstr ""
+
msgid "Deployment|API"
msgstr ""
@@ -7444,9 +7667,6 @@ msgstr ""
msgid "Edit environment"
msgstr ""
-msgid "Edit epic description"
-msgstr ""
-
msgid "Edit file"
msgstr ""
@@ -8068,9 +8288,6 @@ msgstr ""
msgid "Epics"
msgstr ""
-msgid "Epics (Ultimate / Gold license only)"
-msgstr ""
-
msgid "Epics Roadmap"
msgstr ""
@@ -8080,6 +8297,9 @@ msgstr ""
msgid "Epics let you manage your portfolio of projects more efficiently and with less effort"
msgstr ""
+msgid "Epics, Issues, and Merge Requests"
+msgstr ""
+
msgid "Epics|Add an epic"
msgstr ""
@@ -8131,6 +8351,9 @@ msgstr ""
msgid "Epics|Something went wrong while fetching group epics."
msgstr ""
+msgid "Epics|Something went wrong while moving item."
+msgstr ""
+
msgid "Epics|Something went wrong while ordering item."
msgstr ""
@@ -8185,6 +8408,9 @@ msgstr ""
msgid "Error fetching network graph."
msgstr ""
+msgid "Error fetching payload data."
+msgstr ""
+
msgid "Error fetching projects"
msgstr ""
@@ -8194,9 +8420,6 @@ msgstr ""
msgid "Error fetching the dependency list. Please check your network connection and try again."
msgstr ""
-msgid "Error fetching usage ping data."
-msgstr ""
-
msgid "Error loading branch data. Please try again."
msgstr ""
@@ -8338,6 +8561,9 @@ msgstr ""
msgid "Errors"
msgstr ""
+msgid "Errors:"
+msgstr ""
+
msgid "Estimated"
msgstr ""
@@ -8542,12 +8768,18 @@ msgstr ""
msgid "Export as CSV"
msgstr ""
+msgid "Export group"
+msgstr ""
+
msgid "Export issues"
msgstr ""
msgid "Export project"
msgstr ""
+msgid "Export this group with all related data to a new GitLab instance. Once complete, you can import the data file from the \"New Group\" page."
+msgstr ""
+
msgid "Export this project with all its related data in order to move your project to a new GitLab instance. Once the export is finished, you can import the file from the \"New Project\" page."
msgstr ""
@@ -8617,9 +8849,6 @@ msgstr ""
msgid "Failed Jobs"
msgstr ""
-msgid "Failed create wiki"
-msgstr ""
-
msgid "Failed to add a Zoom meeting"
msgstr ""
@@ -8659,6 +8888,9 @@ msgstr ""
msgid "Failed to create resources"
msgstr ""
+msgid "Failed to create wiki"
+msgstr ""
+
msgid "Failed to delete board. Please try again."
msgstr ""
@@ -8668,7 +8900,7 @@ msgstr ""
msgid "Failed to enqueue the rebase operation, possibly due to a long-lived transaction. Try again later."
msgstr ""
-msgid "Failed to find import label for jira import."
+msgid "Failed to find import label for Jira import."
msgstr ""
msgid "Failed to get ref."
@@ -8692,6 +8924,12 @@ msgstr ""
msgid "Failed to load groups & users."
msgstr ""
+msgid "Failed to load labels. Please try again."
+msgstr ""
+
+msgid "Failed to load milestones. Please try again."
+msgstr ""
+
msgid "Failed to load related branches"
msgstr ""
@@ -9067,7 +9305,10 @@ msgstr "Filtri per mesaÄo"
msgid "Filter by milestone name"
msgstr ""
-msgid "Filter by name..."
+msgid "Filter by name"
+msgstr ""
+
+msgid "Filter by status"
msgstr ""
msgid "Filter by two-factor authentication"
@@ -9079,12 +9320,18 @@ msgstr ""
msgid "Filter projects"
msgstr ""
+msgid "Filter results"
+msgstr ""
+
msgid "Filter results by group"
msgstr ""
msgid "Filter results by project"
msgstr ""
+msgid "Filter results..."
+msgstr ""
+
msgid "Filter your projects by name"
msgstr ""
@@ -9373,10 +9620,10 @@ msgstr ""
msgid "GeoNodes|Checksummed"
msgstr ""
-msgid "GeoNodes|Container repositories"
+msgid "GeoNodes|Consult Geo troubleshooting information"
msgstr ""
-msgid "GeoNodes|Data is out of date from %{timeago}"
+msgid "GeoNodes|Container repositories"
msgstr ""
msgid "GeoNodes|Data replication lag"
@@ -9418,6 +9665,9 @@ msgstr ""
msgid "GeoNodes|Last event ID seen from primary"
msgstr ""
+msgid "GeoNodes|Learn more about Geo node statuses"
+msgstr ""
+
msgid "GeoNodes|Loading nodes"
msgstr ""
@@ -9433,6 +9683,9 @@ msgstr ""
msgid "GeoNodes|Node was successfully removed."
msgstr ""
+msgid "GeoNodes|Node's status was updated %{timeAgo}."
+msgstr ""
+
msgid "GeoNodes|Not checksummed"
msgstr ""
@@ -9463,7 +9716,10 @@ msgstr ""
msgid "GeoNodes|Repository verification progress"
msgstr ""
-msgid "GeoNodes|Selective"
+msgid "GeoNodes|Selective (%{syncLabel})"
+msgstr ""
+
+msgid "GeoNodes|Selective synchronization"
msgstr ""
msgid "GeoNodes|Something went wrong while changing node status"
@@ -9490,6 +9746,9 @@ msgstr ""
msgid "GeoNodes|Unverified"
msgstr ""
+msgid "GeoNodes|Updated %{timeAgo}"
+msgstr ""
+
msgid "GeoNodes|Used slots"
msgstr ""
@@ -9529,16 +9788,16 @@ msgstr ""
msgid "Geo|All"
msgstr ""
-msgid "Geo|All projects"
+msgid "Geo|All %{replicable_type}"
msgstr ""
-msgid "Geo|All projects are being scheduled for re-sync"
+msgid "Geo|All projects"
msgstr ""
-msgid "Geo|All projects are being scheduled for re-verify"
+msgid "Geo|All projects are being scheduled for resync"
msgstr ""
-msgid "Geo|Batch operations"
+msgid "Geo|All projects are being scheduled for reverify"
msgstr ""
msgid "Geo|Could not remove tracking entry for an existing project."
@@ -9550,9 +9809,15 @@ msgstr ""
msgid "Geo|Failed"
msgstr ""
+msgid "Geo|Filter by status"
+msgstr ""
+
msgid "Geo|Geo Status"
msgstr ""
+msgid "Geo|In progress"
+msgstr ""
+
msgid "Geo|In sync"
msgstr ""
@@ -9577,9 +9842,6 @@ msgstr ""
msgid "Geo|Not synced yet"
msgstr ""
-msgid "Geo|Pending"
-msgstr ""
-
msgid "Geo|Pending synchronization"
msgstr ""
@@ -9616,7 +9878,7 @@ msgstr ""
msgid "Geo|Resync"
msgstr ""
-msgid "Geo|Resync all projects"
+msgid "Geo|Resync all"
msgstr ""
msgid "Geo|Retry count"
@@ -9625,7 +9887,7 @@ msgstr ""
msgid "Geo|Reverify"
msgstr ""
-msgid "Geo|Reverify all projects"
+msgid "Geo|Reverify all"
msgstr ""
msgid "Geo|Status"
@@ -9967,9 +10229,6 @@ msgstr ""
msgid "Go to file"
msgstr ""
-msgid "Go to file (MRs only)"
-msgstr ""
-
msgid "Go to file permalink (while viewing a file)"
msgstr ""
@@ -10414,6 +10673,9 @@ msgstr ""
msgid "GroupSettings|Disable group mentions"
msgstr ""
+msgid "GroupSettings|Export group"
+msgstr ""
+
msgid "GroupSettings|If the parent group's visibility is lower than the group current visibility, visibility levels for subgroups and projects will be changed to match the new parent group's visibility."
msgstr ""
@@ -10492,6 +10754,9 @@ msgstr ""
msgid "Groups (%{count})"
msgstr ""
+msgid "Groups (%{groups})"
+msgstr ""
+
msgid "Groups can also be nested by creating %{subgroup_docs_link_start}subgroups%{subgroup_docs_link_end}."
msgstr ""
@@ -10757,15 +11022,12 @@ msgstr ""
msgid "ID:"
msgstr ""
-msgid "IDE|Allow live previews of JavaScript projects in the Web IDE using CodeSandbox client side evaluation."
+msgid "IDE|Allow live previews of JavaScript projects in the Web IDE using CodeSandbox Live Preview."
msgstr ""
msgid "IDE|Back"
msgstr ""
-msgid "IDE|Client side evaluation"
-msgstr ""
-
msgid "IDE|Commit"
msgstr ""
@@ -10829,6 +11091,9 @@ msgstr ""
msgid "If any job surpasses this timeout threshold, it will be marked as failed. Human readable time input language is accepted like \"1 hour\". Values without specification represent seconds."
msgstr ""
+msgid "If blank, set allowable lifetime to %{instance_level_policy_in_words}, as defined by the instance admin. Once set, existing tokens for users in this group may be revoked."
+msgstr ""
+
msgid "If checked, group owners can manage LDAP group links and LDAP member overrides"
msgstr ""
@@ -10868,6 +11133,9 @@ msgstr ""
msgid "If you lose your recovery codes you can generate new ones, invalidating all previous codes."
msgstr ""
+msgid "If you reach 100%% storage capacity, you will not be able to: %{base_message}"
+msgstr ""
+
msgid "If your HTTP repository is not publicly accessible, add your credentials."
msgstr ""
@@ -10979,6 +11247,9 @@ msgstr ""
msgid "Import repository"
msgstr "Enporti deponejon"
+msgid "Import started by: %{importInitiator}"
+msgstr ""
+
msgid "Import tasks"
msgstr ""
@@ -11045,6 +11316,9 @@ msgstr ""
msgid "In order to tailor your experience with GitLab we<br>would like to know a bit more about you."
msgstr ""
+msgid "In progress"
+msgstr ""
+
msgid "In the next step, you'll be able to select the projects you want to import."
msgstr ""
@@ -11392,15 +11666,15 @@ msgstr ""
msgid "Issues"
msgstr ""
-msgid "Issues / Merge Requests"
-msgstr ""
-
msgid "Issues Analytics"
msgstr ""
msgid "Issues Rate Limits"
msgstr ""
+msgid "Issues and Merge Requests"
+msgstr ""
+
msgid "Issues can be bugs, tasks or ideas to be discussed. Also, issues are searchable and filterable."
msgstr ""
@@ -11422,10 +11696,10 @@ msgstr ""
msgid "IssuesAnalytics|Avg/Month:"
msgstr ""
-msgid "IssuesAnalytics|Issues created"
+msgid "IssuesAnalytics|Issues opened"
msgstr ""
-msgid "IssuesAnalytics|Issues created per month"
+msgid "IssuesAnalytics|Issues opened per month"
msgstr ""
msgid "IssuesAnalytics|Last 12 months"
@@ -11479,6 +11753,9 @@ msgstr ""
msgid "Jira integration not configured."
msgstr ""
+msgid "Jira project: %{importProject}"
+msgstr ""
+
msgid "JiraService|Events for %{noteable_model_name} are disabled."
msgstr ""
@@ -11868,6 +12145,9 @@ msgstr ""
msgid "Lead"
msgstr ""
+msgid "Lead Time"
+msgstr ""
+
msgid "Learn GitLab"
msgstr ""
@@ -11880,6 +12160,9 @@ msgstr ""
msgid "Learn how to %{no_packages_link_start}publish and share your packages%{no_packages_link_end} with GitLab."
msgstr ""
+msgid "Learn how to enable synchronization"
+msgstr ""
+
msgid "Learn more"
msgstr ""
@@ -12211,9 +12494,6 @@ msgstr ""
msgid "Live preview"
msgstr ""
-msgid "Load more vulnerabilities"
-msgstr ""
-
msgid "Loading"
msgstr ""
@@ -12364,9 +12644,6 @@ msgstr ""
msgid "Manage"
msgstr ""
-msgid "Manage Git repositories with fine-grained access controls that keep your code secure. Perform code reviews and enhance collaboration with merge requests. Each project can also have an issue tracker and a wiki."
-msgstr ""
-
msgid "Manage Web IDE features"
msgstr ""
@@ -12397,6 +12674,9 @@ msgstr ""
msgid "Manage your license"
msgstr ""
+msgid "Managed Account"
+msgstr ""
+
msgid "Manifest"
msgstr ""
@@ -12661,6 +12941,9 @@ msgstr ""
msgid "Merge Requests in Review"
msgstr ""
+msgid "Merge automatically (%{strategy})"
+msgstr ""
+
msgid "Merge commit message"
msgstr ""
@@ -12814,6 +13097,12 @@ msgstr ""
msgid "Merged branches are being deleted. This can take some time depending on the number of branches. Please refresh the page to see changes."
msgstr ""
+msgid "Merged this merge request."
+msgstr ""
+
+msgid "Merges this merge request immediately."
+msgstr ""
+
msgid "Merges this merge request when the pipeline succeeds."
msgstr ""
@@ -12921,12 +13210,15 @@ msgid_plural "Metrics|Edit metrics"
msgstr[0] ""
msgstr[1] ""
-msgid "Metrics|Environment"
+msgid "Metrics|Expand panel"
msgstr ""
msgid "Metrics|For grouping similar metrics"
msgstr ""
+msgid "Metrics|Go back (Esc)"
+msgstr ""
+
msgid "Metrics|Invalid time range, please verify."
msgstr ""
@@ -12957,9 +13249,6 @@ msgstr ""
msgid "Metrics|Refresh dashboard"
msgstr ""
-msgid "Metrics|Show last"
-msgstr ""
-
msgid "Metrics|There was an error creating the dashboard."
msgstr ""
@@ -13038,6 +13327,9 @@ msgstr ""
msgid "Microsoft Azure"
msgstr ""
+msgid "Middleman project with Static Site Editor support"
+msgstr ""
+
msgid "Migrated %{success_count}/%{total_count} files."
msgstr ""
@@ -13483,10 +13775,10 @@ msgstr ""
msgid "Next"
msgstr ""
-msgid "Next file in diff (MRs only)"
+msgid "Next file in diff"
msgstr ""
-msgid "Next unresolved discussion (MRs only)"
+msgid "Next unresolved discussion"
msgstr ""
msgid "Nickname"
@@ -13546,6 +13838,9 @@ msgstr ""
msgid "No changes between %{ref_start}%{source_branch}%{ref_end} and %{ref_start}%{target_branch}%{ref_end}"
msgstr ""
+msgid "No child epics match applied filters"
+msgstr ""
+
msgid "No connection could be made to a Gitaly Server, please check your logs!"
msgstr ""
@@ -13684,15 +13979,6 @@ msgstr ""
msgid "No thanks, don't show this again"
msgstr ""
-msgid "No vulnerabilities found for this group"
-msgstr ""
-
-msgid "No vulnerabilities found for this pipeline"
-msgstr ""
-
-msgid "No vulnerabilities found for this project"
-msgstr ""
-
msgid "No vulnerabilities present"
msgstr ""
@@ -13816,6 +14102,9 @@ msgstr ""
msgid "Nothing to preview."
msgstr ""
+msgid "Nothing to synchronize"
+msgstr ""
+
msgid "Notification events"
msgstr "Sciigaj eventoj"
@@ -13987,6 +14276,9 @@ msgstr ""
msgid "Once removed, the fork relationship cannot be restored and you will no longer be able to send merge requests to the source."
msgstr ""
+msgid "Once the exported file is ready you can download it from this page."
+msgstr ""
+
msgid "Once the exported file is ready, you will receive a notification email with a download link, or you can download it from this page."
msgstr ""
@@ -14088,9 +14380,6 @@ msgstr ""
msgid "Open sidebar"
msgstr ""
-msgid "Open source software to collaborate on code"
-msgstr ""
-
msgid "Open: %{openIssuesCount}"
msgstr ""
@@ -14193,6 +14482,9 @@ msgstr ""
msgid "OutdatedBrowser|You can provide feedback %{feedback_link_start}on this issue%{feedback_link_end} or via your usual support channels."
msgstr ""
+msgid "Overridden"
+msgstr ""
+
msgid "Overview"
msgstr ""
@@ -14244,6 +14536,9 @@ msgstr ""
msgid "PackageRegistry|Conan Command"
msgstr ""
+msgid "PackageRegistry|Copy .pypirc content"
+msgstr ""
+
msgid "PackageRegistry|Copy Conan Command"
msgstr ""
@@ -14265,6 +14560,9 @@ msgstr ""
msgid "PackageRegistry|Copy NuGet Setup Command"
msgstr ""
+msgid "PackageRegistry|Copy Pip command"
+msgstr ""
+
msgid "PackageRegistry|Copy and paste this inside your %{codeStart}pom.xml%{codeEnd} %{codeStart}dependencies%{codeEnd} block."
msgstr ""
@@ -14298,6 +14596,12 @@ msgstr ""
msgid "PackageRegistry|For more information on the NuGet registry, %{linkStart}see the documentation%{linkEnd}."
msgstr ""
+msgid "PackageRegistry|For more information on the PyPi registry, %{linkStart}see the documentation%{linkEnd}."
+msgstr ""
+
+msgid "PackageRegistry|If you haven't already done so, you will need to add the below to your %{codeStart}.pypirc%{codeEnd} file."
+msgstr ""
+
msgid "PackageRegistry|If you haven't already done so, you will need to add the below to your %{codeStart}pom.xml%{codeEnd} file."
msgstr ""
@@ -14307,6 +14611,9 @@ msgstr ""
msgid "PackageRegistry|Learn how to %{noPackagesLinkStart}publish and share your packages%{noPackagesLinkEnd} with GitLab."
msgstr ""
+msgid "PackageRegistry|Manually Published"
+msgstr ""
+
msgid "PackageRegistry|Maven"
msgstr ""
@@ -14325,12 +14632,18 @@ msgstr ""
msgid "PackageRegistry|NuGet Command"
msgstr ""
+msgid "PackageRegistry|Pip Command"
+msgstr ""
+
msgid "PackageRegistry|Pipeline %{linkStart}%{linkEnd} triggered %{timestamp} by %{author}"
msgstr ""
msgid "PackageRegistry|Published to the repository at %{timestamp}"
msgstr ""
+msgid "PackageRegistry|PyPi"
+msgstr ""
+
msgid "PackageRegistry|Registry Setup"
msgstr ""
@@ -14367,6 +14680,9 @@ msgstr ""
msgid "PackageRegistry|npm"
msgstr ""
+msgid "PackageRegistry|published by %{author}"
+msgstr ""
+
msgid "PackageRegistry|yarn"
msgstr ""
@@ -14382,6 +14698,9 @@ msgstr ""
msgid "PackageType|NuGet"
msgstr ""
+msgid "PackageType|PyPi"
+msgstr ""
+
msgid "Packages"
msgstr ""
@@ -14475,6 +14794,9 @@ msgstr ""
msgid "Past due"
msgstr ""
+msgid "Paste a machine public key here. Read more about how to generate it"
+msgstr ""
+
msgid "Paste a machine public key here. Read more about how to generate it %{link_start}here%{link_end}"
msgstr ""
@@ -14490,9 +14812,6 @@ msgstr ""
msgid "Path"
msgstr ""
-msgid "Path, transfer, remove"
-msgstr ""
-
msgid "Path:"
msgstr ""
@@ -14523,7 +14842,7 @@ msgstr ""
msgid "Percentage"
msgstr ""
-msgid "Perform advanced options such as changing path, transferring, or removing the group."
+msgid "Perform advanced options such as changing path, transferring, exporting, or removing the group."
msgstr ""
msgid "Perform common operations on GitLab project"
@@ -14604,9 +14923,6 @@ msgstr "Ĉenstablaj planoj"
msgid "Pipeline minutes quota"
msgstr ""
-msgid "Pipeline quota"
-msgstr ""
-
msgid "Pipeline subscriptions"
msgstr ""
@@ -14616,6 +14932,9 @@ msgstr ""
msgid "Pipeline: %{status}"
msgstr ""
+msgid "PipelineCharts|CI / CD Analytics"
+msgstr ""
+
msgid "PipelineCharts|Failed:"
msgstr "Malsukcesaj:"
@@ -14694,15 +15013,6 @@ msgstr ""
msgid "Pipelines settings for '%{project_name}' were successfully updated."
msgstr ""
-msgid "Pipelines| to purchase more minutes."
-msgstr ""
-
-msgid "Pipelines|%{namespace_name} has exceeded its pipeline minutes quota."
-msgstr ""
-
-msgid "Pipelines|%{namespace_name} has less than %{notification_level}%% of CI minutes available."
-msgstr ""
-
msgid "Pipelines|API"
msgstr ""
@@ -14724,10 +15034,13 @@ msgstr ""
msgid "Pipelines|Get started with Pipelines"
msgstr ""
-msgid "Pipelines|Loading Pipelines"
+msgid "Pipelines|Group %{namespace_name} has %{percentage}%% or less Shared Runner Pipeline minutes remaining. Once it runs out, no new jobs or pipelines in its projects will run."
msgstr ""
-msgid "Pipelines|Pipelines will not run anymore on shared Runners."
+msgid "Pipelines|Group %{namespace_name} has exceeded its pipeline minutes quota. Unless you buy additional pipeline minutes, no new jobs or pipelines in its projects will run."
+msgstr ""
+
+msgid "Pipelines|Loading Pipelines"
msgstr ""
msgid "Pipelines|Project cache successfully reset."
@@ -14958,6 +15271,9 @@ msgstr ""
msgid "Please select"
msgstr ""
+msgid "Please select a Jira project"
+msgstr ""
+
msgid "Please select a country"
msgstr ""
@@ -15003,6 +15319,9 @@ msgstr ""
msgid "Please wait while we import the repository for you. Refresh at will."
msgstr ""
+msgid "Plugins directory is deprecated and will be removed in 14.0. Please move this file into /file_hooks directory."
+msgstr ""
+
msgid "Pod does not exist"
msgstr ""
@@ -15120,7 +15439,7 @@ msgstr ""
msgid "Prevent users from changing their profile name"
msgstr ""
-msgid "Prevent users from modifing merge request approvers list"
+msgid "Prevent users from modifying merge request approvers list"
msgstr ""
msgid "Prevent users from performing write operations on GitLab while performing maintenance."
@@ -15141,10 +15460,10 @@ msgstr ""
msgid "Previous Artifacts"
msgstr ""
-msgid "Previous file in diff (MRs only)"
+msgid "Previous file in diff"
msgstr ""
-msgid "Previous unresolved discussion (MRs only)"
+msgid "Previous unresolved discussion"
msgstr ""
msgid "Primary"
@@ -15177,6 +15496,9 @@ msgstr ""
msgid "Private profile"
msgstr ""
+msgid "Private projects Minutes cost factor"
+msgstr ""
+
msgid "Private projects can be created in your personal namespace with:"
msgstr ""
@@ -16005,6 +16327,9 @@ msgstr ""
msgid "ProjectSettings|With GitLab Pages you can host your static websites on GitLab"
msgstr ""
+msgid "ProjectSettings|With Metrics Dashboard you can visualize this project performance metrics"
+msgstr ""
+
msgid "ProjectTemplates|.NET Core"
msgstr ""
@@ -16068,6 +16393,9 @@ msgstr ""
msgid "ProjectTemplates|Spring"
msgstr ""
+msgid "ProjectTemplates|Static Site Editor/Middleman"
+msgstr ""
+
msgid "ProjectTemplates|iOS (Swift)"
msgstr ""
@@ -16287,13 +16615,13 @@ msgstr ""
msgid "PrometheusService|Prometheus is being automatically managed on your clusters"
msgstr ""
-msgid "PrometheusService|These metrics will only be monitored after your first deployment to an environment"
+msgid "PrometheusService|Select the Active checkbox to override the Auto Configuration with custom settings. If unchecked, Auto Configuration settings are used."
msgstr ""
-msgid "PrometheusService|Time-series monitoring service"
+msgid "PrometheusService|These metrics will only be monitored after your first deployment to an environment"
msgstr ""
-msgid "PrometheusService|To enable manual configuration, uninstall Prometheus from your clusters"
+msgid "PrometheusService|Time-series monitoring service"
msgstr ""
msgid "PrometheusService|To enable the installation of Prometheus on your clusters, deactivate the manual configuration below"
@@ -16479,6 +16807,9 @@ msgstr ""
msgid "Protip:"
msgstr ""
+msgid "Protocol"
+msgstr ""
+
msgid "Provider"
msgstr ""
@@ -16503,6 +16834,9 @@ msgstr ""
msgid "Public pipelines"
msgstr ""
+msgid "Public projects Minutes cost factor"
+msgstr ""
+
msgid "Pull"
msgstr ""
@@ -16703,6 +17037,9 @@ msgid_plural "Refreshing in %d seconds to show the updated status..."
msgstr[0] ""
msgstr[1] ""
+msgid "Regenerate export"
+msgstr ""
+
msgid "Regenerate instance ID"
msgstr ""
@@ -17080,9 +17417,29 @@ msgstr ""
msgid "Reports|%{combinedString} and %{resolvedString}"
msgstr ""
+msgid "Reports|Accessibility scanning detected %d issue for the source branch only"
+msgid_plural "Reports|Accessibility scanning detected %d issues for the source branch only"
+msgstr[0] ""
+msgstr[1] ""
+
+msgid "Reports|Accessibility scanning detected no issues for the source branch only"
+msgstr ""
+
+msgid "Reports|Accessibility scanning failed loading results"
+msgstr ""
+
+msgid "Reports|Accessibility scanning results are being parsed"
+msgstr ""
+
msgid "Reports|Actions"
msgstr ""
+msgid "Reports|An error occured while loading report"
+msgstr ""
+
+msgid "Reports|An error occurred while loading %{name} results"
+msgstr ""
+
msgid "Reports|Class"
msgstr ""
@@ -17188,7 +17545,7 @@ msgstr ""
msgid "Requested %{time_ago}"
msgstr ""
-msgid "Requested design version does not exist"
+msgid "Requested design version does not exist."
msgstr ""
msgid "Requested states are invalid"
@@ -17372,6 +17729,9 @@ msgstr ""
msgid "Resync"
msgstr ""
+msgid "Resync all"
+msgstr ""
+
msgid "Resync all %{replicableType}"
msgstr ""
@@ -17440,6 +17800,9 @@ msgstr ""
msgid "Revoked personal access token %{personal_access_token_name}!"
msgstr ""
+msgid "Revoked project access token %{project_access_token_name}!"
+msgstr ""
+
msgid "RightSidebar|adding a"
msgstr ""
@@ -17488,6 +17851,9 @@ msgstr ""
msgid "Runner token"
msgstr ""
+msgid "Runner tokens"
+msgstr ""
+
msgid "Runner was not updated."
msgstr ""
@@ -17542,6 +17908,9 @@ msgstr ""
msgid "SAML SSO for %{group_name}"
msgstr ""
+msgid "SAML discovery tokens"
+msgstr ""
+
msgid "SAML for %{group_name}"
msgstr ""
@@ -17626,12 +17995,18 @@ msgstr "Plani novan ĉenstablon"
msgid "Scheduled"
msgstr ""
+msgid "Scheduled to merge this merge request (%{strategy})."
+msgstr ""
+
msgid "Scheduled to merge this merge request when the pipeline succeeds."
msgstr ""
msgid "Schedules"
msgstr ""
+msgid "Schedules to merge this merge request (%{strategy})."
+msgstr ""
+
msgid "Scheduling"
msgstr ""
@@ -17689,6 +18064,9 @@ msgstr ""
msgid "Search branches and tags"
msgstr "Serĉu branĉon aŭ etikedon"
+msgid "Search by author"
+msgstr ""
+
msgid "Search files"
msgstr ""
@@ -17834,11 +18212,6 @@ msgid_plural "SearchResults|snippets"
msgstr[0] ""
msgstr[1] ""
-msgid "SearchResults|snippet result"
-msgid_plural "SearchResults|snippet results"
-msgstr[0] ""
-msgstr[1] ""
-
msgid "SearchResults|user"
msgid_plural "SearchResults|users"
msgstr[0] ""
@@ -17849,6 +18222,9 @@ msgid_plural "SearchResults|wiki results"
msgstr[0] ""
msgstr[1] ""
+msgid "Searching by both author and message is currently not supported."
+msgstr ""
+
msgid "Seat Link"
msgstr ""
@@ -17876,184 +18252,232 @@ msgstr ""
msgid "Security Dashboard"
msgstr ""
-msgid "Security Dashboard|Error fetching the vulnerability counts. Please check your network connection and try again."
+msgid "Security configuration help link"
+msgstr ""
+
+msgid "Security dashboard"
msgstr ""
-msgid "Security Dashboard|Error fetching the vulnerability list. Please check your network connection and try again."
+msgid "Security report is out of date. Please update your branch with the latest changes from the target branch (%{targetBranchName})"
msgstr ""
-msgid "Security Dashboard|Issue Created"
+msgid "Security report is out of date. Run %{newPipelineLinkStart}a new pipeline%{newPipelineLinkEnd} for the target branch (%{targetBranchName})"
msgstr ""
-msgid "Security Reports|Comment added to '%{vulnerabilityName}'"
+msgid "SecurityConfiguration|Configured"
msgstr ""
-msgid "Security Reports|Comment deleted on '%{vulnerabilityName}'"
+msgid "SecurityConfiguration|Feature"
msgstr ""
-msgid "Security Reports|Comment edited on '%{vulnerabilityName}'"
+msgid "SecurityConfiguration|Feature documentation for %{featureName}"
msgstr ""
-msgid "Security Reports|Create issue"
+msgid "SecurityConfiguration|Not yet configured"
msgstr ""
-msgid "Security Reports|Dismiss vulnerability"
+msgid "SecurityConfiguration|Secure features"
msgstr ""
-msgid "Security Reports|Dismissed '%{vulnerabilityName}'"
+msgid "SecurityConfiguration|Status"
msgstr ""
-msgid "Security Reports|Dismissed '%{vulnerabilityName}'. Turn off the hide dismissed toggle to view."
+msgid "SecurityReports|%{firstProject} and %{secondProject}"
msgstr ""
-msgid "Security Reports|Either you don't have permission to view this dashboard or the dashboard has not been setup. Please check your permission settings with your administrator or check your dashboard configurations to proceed."
+msgid "SecurityReports|%{firstProject}, %{secondProject}, and %{rest}"
msgstr ""
-msgid "Security Reports|Learn more about setting up your dashboard"
+msgid "SecurityReports|Add a project to your dashboard"
msgstr ""
-msgid "Security Reports|More info"
+msgid "SecurityReports|Add or remove projects from your dashboard"
msgstr ""
-msgid "Security Reports|Oops, something doesn't seem right."
+msgid "SecurityReports|Add projects"
msgstr ""
-msgid "Security Reports|Security reports can only be accessed by authorized users."
+msgid "SecurityReports|Comment added to '%{vulnerabilityName}'"
msgstr ""
-msgid "Security Reports|There was an error adding the comment."
+msgid "SecurityReports|Comment deleted on '%{vulnerabilityName}'"
msgstr ""
-msgid "Security Reports|There was an error creating the issue."
+msgid "SecurityReports|Comment edited on '%{vulnerabilityName}'"
msgstr ""
-msgid "Security Reports|There was an error creating the merge request."
+msgid "SecurityReports|Create issue"
msgstr ""
-msgid "Security Reports|There was an error deleting the comment."
+msgid "SecurityReports|Dismiss Selected"
msgstr ""
-msgid "Security Reports|There was an error dismissing the vulnerabilities."
+msgid "SecurityReports|Dismiss vulnerability"
msgstr ""
-msgid "Security Reports|There was an error dismissing the vulnerability."
+msgid "SecurityReports|Dismissed '%{vulnerabilityName}'"
msgstr ""
-msgid "Security Reports|There was an error reverting the dismissal."
+msgid "SecurityReports|Dismissed '%{vulnerabilityName}'. Turn off the hide dismissed toggle to view."
msgstr ""
-msgid "Security Reports|There was an error reverting this dismissal."
+msgid "SecurityReports|Each vulnerability now has a unique page that can be directly linked to, shared, referenced, and tracked as the single source of truth. Vulnerability occurrences also persist across scanner runs, which improves tracking and visibility and reduces duplicates between scans."
msgstr ""
-msgid "Security Reports|Undo dismiss"
+msgid "SecurityReports|Edit dashboard"
msgstr ""
-msgid "Security Reports|You do not have sufficient permissions to access this report"
+msgid "SecurityReports|Either you don't have permission to view this dashboard or the dashboard has not been setup. Please check your permission settings with your administrator or check your dashboard configurations to proceed."
msgstr ""
-msgid "Security Reports|You must sign in as an authorized user to see this report"
+msgid "SecurityReports|Error fetching the vulnerability counts. Please check your network connection and try again."
msgstr ""
-msgid "Security configuration help link"
+msgid "SecurityReports|Error fetching the vulnerability list. Please check your network connection and try again."
msgstr ""
-msgid "Security dashboard"
+msgid "SecurityReports|False positive"
msgstr ""
-msgid "Security report is out of date. Please update your branch with the latest changes from the target branch (%{targetBranchName})"
+msgid "SecurityReports|Hide dismissed"
msgstr ""
-msgid "Security report is out of date. Run %{newPipelineLinkStart}a new pipeline%{newPipelineLinkEnd} for the target branch (%{targetBranchName})"
+msgid "SecurityReports|Introducing standalone vulnerabilities"
msgstr ""
-msgid "SecurityConfiguration|Configured"
+msgid "SecurityReports|Issue Created"
msgstr ""
-msgid "SecurityConfiguration|Feature"
+msgid "SecurityReports|Learn More"
msgstr ""
-msgid "SecurityConfiguration|Feature documentation for %{featureName}"
+msgid "SecurityReports|Learn more about setting up your dashboard"
msgstr ""
-msgid "SecurityConfiguration|Not yet configured"
+msgid "SecurityReports|Load more vulnerabilities"
msgstr ""
-msgid "SecurityConfiguration|Secure features"
+msgid "SecurityReports|Monitor vulnerabilities in your code"
msgstr ""
-msgid "SecurityConfiguration|Status"
+msgid "SecurityReports|More info"
msgstr ""
-msgid "SecurityDashboard|%{firstProject} and %{secondProject}"
+msgid "SecurityReports|More information"
msgstr ""
-msgid "SecurityDashboard|%{firstProject}, %{secondProject}, and %{rest}"
+msgid "SecurityReports|No vulnerabilities found for dashboard"
msgstr ""
-msgid "SecurityDashboard|Add a project to your dashboard"
+msgid "SecurityReports|No vulnerabilities found for this group"
msgstr ""
-msgid "SecurityDashboard|Add or remove projects from your dashboard"
+msgid "SecurityReports|No vulnerabilities found for this pipeline"
msgstr ""
-msgid "SecurityDashboard|Add projects"
+msgid "SecurityReports|No vulnerabilities found for this project"
msgstr ""
-msgid "SecurityDashboard|Edit dashboard"
+msgid "SecurityReports|Oops, something doesn't seem right."
msgstr ""
-msgid "SecurityDashboard|Hide dismissed"
+msgid "SecurityReports|Pipeline %{pipelineLink} triggered %{timeago} by %{user}"
msgstr ""
-msgid "SecurityDashboard|Monitor vulnerabilities in your code"
+msgid "SecurityReports|Project"
msgstr ""
-msgid "SecurityDashboard|More information"
+msgid "SecurityReports|Projects added"
msgstr ""
-msgid "SecurityDashboard|No vulnerabilities found for dashboard"
+msgid "SecurityReports|Remove project from dashboard"
msgstr ""
-msgid "SecurityDashboard|Pipeline %{pipelineLink} triggered %{timeago} by %{user}"
+msgid "SecurityReports|Report type"
msgstr ""
-msgid "SecurityDashboard|Project"
+msgid "SecurityReports|Return to dashboard"
msgstr ""
-msgid "SecurityDashboard|Projects added"
+msgid "SecurityReports|Security Dashboard"
msgstr ""
-msgid "SecurityDashboard|Remove project from dashboard"
+msgid "SecurityReports|Security reports can only be accessed by authorized users."
msgstr ""
-msgid "SecurityDashboard|Report type"
+msgid "SecurityReports|Select a project to add by using the project search field above."
msgstr ""
-msgid "SecurityDashboard|Return to dashboard"
+msgid "SecurityReports|Select a reason"
msgstr ""
-msgid "SecurityDashboard|Security Dashboard"
+msgid "SecurityReports|Severity"
msgstr ""
-msgid "SecurityDashboard|Select a project to add by using the project search field above."
+msgid "SecurityReports|Status"
msgstr ""
-msgid "SecurityDashboard|Severity"
+msgid "SecurityReports|The security dashboard displays the latest security findings for projects you wish to monitor. Select \"Edit dashboard\" to add and remove projects."
msgstr ""
-msgid "SecurityDashboard|Status"
+msgid "SecurityReports|The security dashboard displays the latest security report. Use it to find and fix vulnerabilities."
msgstr ""
-msgid "SecurityDashboard|The security dashboard displays the latest security findings for projects you wish to monitor. Select \"Edit dashboard\" to add and remove projects."
+msgid "SecurityReports|There was an error adding the comment."
msgstr ""
-msgid "SecurityDashboard|The security dashboard displays the latest security report. Use it to find and fix vulnerabilities."
+msgid "SecurityReports|There was an error creating the issue."
msgstr ""
-msgid "SecurityDashboard|There was an error while generating the report."
+msgid "SecurityReports|There was an error creating the merge request."
msgstr ""
-msgid "SecurityDashboard|Unable to add %{invalidProjects}"
+msgid "SecurityReports|There was an error deleting the comment."
+msgstr ""
+
+msgid "SecurityReports|There was an error dismissing the vulnerabilities."
+msgstr ""
+
+msgid "SecurityReports|There was an error dismissing the vulnerability."
+msgstr ""
+
+msgid "SecurityReports|There was an error reverting the dismissal."
+msgstr ""
+
+msgid "SecurityReports|There was an error reverting this dismissal."
+msgstr ""
+
+msgid "SecurityReports|There was an error while generating the report."
+msgstr ""
+
+msgid "SecurityReports|Unable to add %{invalidProjects}"
+msgstr ""
+
+msgid "SecurityReports|Undo dismiss"
+msgstr ""
+
+msgid "SecurityReports|While it's rare to have no vulnerabilities for your group, it can happen. In any event, we ask that you double check your settings to make sure you've set up your dashboard correctly."
+msgstr ""
+
+msgid "SecurityReports|While it's rare to have no vulnerabilities for your pipeline, it can happen. In any event, we ask that you double check your settings to make sure all security scanning jobs have passed successfully."
+msgstr ""
+
+msgid "SecurityReports|While it's rare to have no vulnerabilities for your project, it can happen. In any event, we ask that you double check your settings to make sure you've set up your dashboard correctly."
+msgstr ""
+
+msgid "SecurityReports|While it's rare to have no vulnerabilities, it can happen. In any event, we ask that you please double check your settings to make sure you've set up your dashboard correctly."
+msgstr ""
+
+msgid "SecurityReports|Won't fix / Accept risk"
+msgstr ""
+
+msgid "SecurityReports|You do not have sufficient permissions to access this report"
+msgstr ""
+
+msgid "SecurityReports|You must sign in as an authorized user to see this report"
+msgstr ""
+
+msgid "SecurityReports|[No reason]"
msgstr ""
msgid "See GitLab's %{password_policy_guidelines}"
@@ -18065,6 +18489,9 @@ msgstr ""
msgid "See the affected projects in the GitLab admin panel"
msgstr ""
+msgid "See what's new at GitLab"
+msgstr ""
+
msgid "Select"
msgstr ""
@@ -18470,9 +18897,6 @@ msgstr ""
msgid "Set up Jira Integration"
msgstr ""
-msgid "Set up Jira Integration illustration"
-msgstr ""
-
msgid "Set up a %{type} Runner automatically"
msgstr ""
@@ -18557,6 +18981,9 @@ msgstr ""
msgid "Severity: %{severity}"
msgstr ""
+msgid "Shards (%{shards})"
+msgstr ""
+
msgid "Shards to synchronize"
msgstr ""
@@ -18871,6 +19298,9 @@ msgstr ""
msgid "Snowplow"
msgstr ""
+msgid "Some child epics may be hidden due to applied filters"
+msgstr ""
+
msgid "Some email servers do not support overriding the email sender name. Enable this option to include the name of the author of the issue, merge request or comment in the email body instead."
msgstr ""
@@ -19012,6 +19442,9 @@ msgstr ""
msgid "Something went wrong while updating your list settings"
msgstr ""
+msgid "Something went wrong with your automatic subscription renewal"
+msgstr ""
+
msgid "Something went wrong, unable to add %{project} to dashboard"
msgstr ""
@@ -19297,6 +19730,9 @@ msgstr ""
msgid "Stage removed"
msgstr ""
+msgid "Standard"
+msgstr ""
+
msgid "Star a label to make it a priority label. Order the prioritized labels to change their relative priority, by dragging."
msgstr ""
@@ -19420,6 +19856,9 @@ msgstr ""
msgid "Static Application Security Testing (SAST)"
msgstr ""
+msgid "StaticSiteEditor|An error occurred while submitting your changes."
+msgstr ""
+
msgid "StaticSiteEditor|Branch could not be created."
msgstr ""
@@ -19429,18 +19868,30 @@ msgstr ""
msgid "StaticSiteEditor|Could not create merge request."
msgstr ""
+msgid "StaticSiteEditor|Incompatible file content"
+msgstr ""
+
msgid "StaticSiteEditor|Return to site"
msgstr ""
+msgid "StaticSiteEditor|Static site editor"
+msgstr ""
+
msgid "StaticSiteEditor|Success!"
msgstr ""
msgid "StaticSiteEditor|Summary of changes"
msgstr ""
+msgid "StaticSiteEditor|The Static Site Editor is currently configured to only edit Markdown content on pages generated from Middleman. Visit the documentation to learn more about configuring your site to use the Static Site Editor."
+msgstr ""
+
msgid "StaticSiteEditor|Update %{sourcePath} file"
msgstr ""
+msgid "StaticSiteEditor|View documentation"
+msgstr ""
+
msgid "StaticSiteEditor|View merge request"
msgstr ""
@@ -19498,6 +19949,12 @@ msgstr ""
msgid "StatusPage|Status page"
msgstr ""
+msgid "StatusPage|Status page URL"
+msgstr ""
+
+msgid "StatusPage|Status page frontend documentation"
+msgstr ""
+
msgid "StatusPage|To publish incidents to an external status page, GitLab will store a JSON file in your Amazon S3 account in a location accessible to your external status page service. Make sure to also set up %{docsLink}"
msgstr ""
@@ -19846,6 +20303,12 @@ msgstr ""
msgid "Synced"
msgstr ""
+msgid "Synchronization disabled"
+msgstr ""
+
+msgid "Synchronization of container repositories is disabled."
+msgstr ""
+
msgid "System"
msgstr ""
@@ -19999,6 +20462,9 @@ msgstr ""
msgid "Telephone number"
msgstr ""
+msgid "Telephone number (Optional)"
+msgstr ""
+
msgid "Template"
msgstr ""
@@ -20305,6 +20771,9 @@ msgstr ""
msgid "The license was successfully uploaded and is now active. You can see the details below."
msgstr ""
+msgid "The license was successfully uploaded and will be active from %{starts_at}. You can see the details below."
+msgstr ""
+
msgid "The maximum file size allowed is %{size}."
msgstr ""
@@ -20626,6 +21095,9 @@ msgstr ""
msgid "There was an error getting the epic participants."
msgstr ""
+msgid "There was an error importing the Jira project."
+msgstr ""
+
msgid "There was an error loading users activity calendar."
msgstr ""
@@ -20641,7 +21113,7 @@ msgstr ""
msgid "There was an error resetting user pipeline minutes."
msgstr ""
-msgid "There was an error saving this Geo Node"
+msgid "There was an error saving this Geo Node."
msgstr ""
msgid "There was an error saving your changes."
@@ -20704,6 +21176,9 @@ msgstr ""
msgid "They can be managed using the %{link}."
msgstr ""
+msgid "Third Party Advisory Link"
+msgstr ""
+
msgid "Third party offers"
msgstr ""
@@ -20740,6 +21215,9 @@ msgstr ""
msgid "This application will be able to:"
msgstr ""
+msgid "This attachment has been truncated to avoid exceeding the maximum allowed attachment size of 15MB. %{written_count} of %{issues_count} issues have been included. Consider re-exporting with a narrower selection of issues."
+msgstr ""
+
msgid "This block is self-referential"
msgstr ""
@@ -20881,6 +21359,12 @@ msgstr ""
msgid "This issue is confidential"
msgstr ""
+msgid "This issue is currently blocked by the following issues: %{issues}."
+msgstr ""
+
+msgid "This issue is in a child epic of the filtered epic"
+msgstr ""
+
msgid "This issue is locked."
msgstr ""
@@ -21079,7 +21563,10 @@ msgstr ""
msgid "This user will be the author of all events in the activity feed that are the result of an update, like new branches being created or new commits being pushed to existing branches. Upon creation or when reassigning you can only assign yourself to be the mirror user."
msgstr ""
-msgid "This variable can not be masked"
+msgid "This variable can not be masked."
+msgstr ""
+
+msgid "This variable does not match the expected pattern."
msgstr ""
msgid "This will help us personalize your onboarding experience."
@@ -21109,10 +21596,10 @@ msgstr ""
msgid "ThreatMonitoring|Application firewall not detected"
msgstr ""
-msgid "ThreatMonitoring|Container Network Policy"
+msgid "ThreatMonitoring|Container Network Policies are not installed or have been disabled. To view this data, ensure your Network Policies are installed and enabled for your cluster."
msgstr ""
-msgid "ThreatMonitoring|Container NetworkPolicies are not installed or has been disabled. To view this data, ensure you NetworkPolicies are installed and enabled for your cluster."
+msgid "ThreatMonitoring|Container Network Policy"
msgstr ""
msgid "ThreatMonitoring|Container NetworkPolicies not detected"
@@ -21145,7 +21632,7 @@ msgstr ""
msgid "ThreatMonitoring|Something went wrong, unable to fetch statistics"
msgstr ""
-msgid "ThreatMonitoring|The firewall is not installed or has been disabled. To view this data, ensure you firewall is installed and enabled for your cluster."
+msgid "ThreatMonitoring|The firewall is not installed or has been disabled. To view this data, ensure the web application firewall is installed and enabled for your cluster."
msgstr ""
msgid "ThreatMonitoring|The graph below is an overview of traffic coming to your application as tracked by the Web Application Firewall (WAF). View the docs for instructions on how to access the WAF logs to see what type of malicious traffic is trying to access your app. The docs link is also accessible by clicking the \"?\" icon next to the title below."
@@ -21214,6 +21701,9 @@ msgstr ""
msgid "Time in seconds GitLab will wait for a response from the external service. When the service does not respond in time, access will be denied."
msgstr ""
+msgid "Time of import: %{importTime}"
+msgstr ""
+
msgid "Time remaining"
msgstr ""
@@ -21398,7 +21888,7 @@ msgstr ""
msgid "Title:"
msgstr ""
-msgid "Titles and Filenames"
+msgid "Titles and Descriptions"
msgstr ""
msgid "To"
@@ -21605,7 +22095,7 @@ msgstr ""
msgid "Too many projects enabled. You will need to manage them via the console or the API."
msgstr ""
-msgid "Topics"
+msgid "Topics (optional)"
msgstr ""
msgid "Total"
@@ -21767,6 +22257,9 @@ msgstr ""
msgid "Try all GitLab has to offer for 30 days."
msgstr ""
+msgid "Try changing or removing filters."
+msgstr ""
+
msgid "Try to fork again"
msgstr ""
@@ -21860,6 +22353,9 @@ msgstr ""
msgid "Unable to connect to server: %{error}"
msgstr ""
+msgid "Unable to connect to the Jira instance. Please check your Jira integration configuration."
+msgstr ""
+
msgid "Unable to convert Kubernetes logs encoding to UTF-8"
msgstr ""
@@ -22803,6 +23299,9 @@ msgstr ""
msgid "View issue"
msgstr ""
+msgid "View issues"
+msgstr ""
+
msgid "View it on GitLab"
msgstr ""
@@ -22980,6 +23479,15 @@ msgstr ""
msgid "VulnerabilityManagement|Resolved %{timeago} by %{user}"
msgstr ""
+msgid "VulnerabilityManagement|Something went wrong while trying to delete the comment. Please try again later."
+msgstr ""
+
+msgid "VulnerabilityManagement|Something went wrong while trying to retrieve the vulnerability history. Please try again later."
+msgstr ""
+
+msgid "VulnerabilityManagement|Something went wrong while trying to save the comment. Please try again later."
+msgstr ""
+
msgid "VulnerabilityManagement|Something went wrong, could not create an issue."
msgstr ""
@@ -23010,6 +23518,9 @@ msgstr ""
msgid "VulnerabilityStatusTypes|Resolved"
msgstr ""
+msgid "Vulnerability|%{scannerName} (version %{scannerVersion})"
+msgstr ""
+
msgid "Vulnerability|Class"
msgstr ""
@@ -23043,7 +23554,10 @@ msgstr ""
msgid "Vulnerability|Project"
msgstr ""
-msgid "Vulnerability|Report Type"
+msgid "Vulnerability|Scanner Provider"
+msgstr ""
+
+msgid "Vulnerability|Scanner Type"
msgstr ""
msgid "Vulnerability|Severity"
@@ -23097,9 +23611,15 @@ msgstr ""
msgid "We sent you an email with reset password instructions"
msgstr ""
+msgid "We tried to automatically renew your %{strong}%{plan_name}%{strong_close} subscription for %{strong}%{namespace_name}%{strong_close} on %{expires_on} but something went wrong so your subscription was downgraded to the free plan. Don't worry, your data is safe. We suggest you check your payment method and get in touch with our support team (%{support_link}). They'll gladly help with your subscription renewal."
+msgstr ""
+
msgid "We want to be sure it is you, please confirm you are not a robot."
msgstr ""
+msgid "We will automatically renew your %{strong}%{plan_name}%{strong_close} subscription for %{strong}%{namespace_name}%{strong_close} on %{strong}%{expires_on}%{strong_close}. There's nothing that you need to do, we'll let you know when the renewal is complete. Need more seats, a higher plan or just want to review your payment method?"
+msgstr ""
+
msgid "We've found no vulnerabilities"
msgstr ""
@@ -23204,15 +23724,6 @@ msgstr ""
msgid "When:"
msgstr ""
-msgid "While it's rare to have no vulnerabilities for your group, it can happen. In any event, we ask that you double check your settings to make sure you've set up your dashboard correctly."
-msgstr ""
-
-msgid "While it's rare to have no vulnerabilities for your pipeline, it can happen. In any event, we ask that you double check your settings to make sure all security scanning jobs have passed successfully."
-msgstr ""
-
-msgid "While it's rare to have no vulnerabilities for your project, it can happen. In any event, we ask that you double check your settings to make sure you've set up your dashboard correctly."
-msgstr ""
-
msgid "While it's rare to have no vulnerabilities, it can happen. In any event, we ask that you please double check your settings to make sure you've set up your dashboard correctly."
msgstr ""
@@ -23243,9 +23754,6 @@ msgstr ""
msgid "Wiki"
msgstr ""
-msgid "Wiki pages"
-msgstr ""
-
msgid "Wiki was successfully updated."
msgstr ""
@@ -23429,6 +23937,9 @@ msgstr ""
msgid "Yes, add it"
msgstr ""
+msgid "Yes, close issue"
+msgstr ""
+
msgid "Yes, let me map Google Code users to full names or GitLab users."
msgstr ""
@@ -23624,6 +24135,9 @@ msgstr ""
msgid "You could not create a new trigger."
msgstr ""
+msgid "You didn't renew your %{strong}%{plan_name}%{strong_close} subscription for %{strong}%{namespace_name}%{strong_close} so it was downgraded to the free plan."
+msgstr ""
+
msgid "You didn't renew your %{strong}%{plan_name}%{strong_close} subscription so it was downgraded to the GitLab Core Plan."
msgstr ""
@@ -23690,6 +24204,9 @@ msgstr ""
msgid "You have declined the invitation to join %{label}."
msgstr ""
+msgid "You have imported from this project %{numberOfPreviousImportsForProject} times before. Each new import will create duplicate issues."
+msgstr ""
+
msgid "You have no permissions"
msgstr ""
@@ -23771,6 +24288,9 @@ msgstr ""
msgid "You need to upload a Google Takeout archive."
msgstr ""
+msgid "You reached %{usage_in_percent} of %{namespace_name}'s capacity (%{used_storage} of %{storage_limit})"
+msgstr ""
+
msgid "You tried to fork %{link_to_the_project} but it failed for the following reason:"
msgstr ""
@@ -23855,9 +24375,21 @@ msgstr ""
msgid "YouTube"
msgstr ""
+msgid "Your %{strong}%{plan_name}%{strong_close} subscription for %{strong}%{namespace_name}%{strong_close} will expire on %{strong}%{expires_on}%{strong_close}. After that, you will not to be able to create issues or merge requests as well as many other features."
+msgstr ""
+
msgid "Your %{strong}%{plan_name}%{strong_close} subscription will expire on %{strong}%{expires_on}%{strong_close}. After that, you will not to be able to create issues or merge requests as well as many other features."
msgstr ""
+msgid "Your CSV export has started. It will be emailed to %{email} when complete."
+msgstr ""
+
+msgid "Your CSV export of %{issues_count} from project %{project_link} has been added to this email as an attachment."
+msgstr ""
+
+msgid "Your CSV export of %{written_count} from project %{project_name} (%{project_url}) has been added to this email as an attachment."
+msgstr ""
+
msgid "Your Commit Email will be used for web based operations, such as edits and merges."
msgstr ""
@@ -23954,6 +24486,9 @@ msgstr ""
msgid "Your comment could not be updated! Please check your network connection and try again."
msgstr ""
+msgid "Your comment will be discarded."
+msgstr ""
+
msgid "Your custom stage '%{title}' was created"
msgstr ""
@@ -23993,6 +24528,9 @@ msgstr ""
msgid "Your new personal access token has been created."
msgstr ""
+msgid "Your new project access token has been created."
+msgstr ""
+
msgid "Your password isn't required to view this page. If a password or any other personal details are requested, please contact your administrator to report abuse."
msgstr ""
@@ -24011,12 +24549,18 @@ msgstr ""
msgid "Your request for access has been queued for review."
msgstr ""
+msgid "Your search didn't match any commits."
+msgstr ""
+
msgid "Your subscription expired!"
msgstr ""
msgid "Your subscription has been downgraded"
msgstr ""
+msgid "Your subscription will automatically renew in %{remaining_days}"
+msgstr ""
+
msgid "Your subscription will expire in %{remaining_days}"
msgstr ""
@@ -24055,7 +24599,7 @@ msgstr ""
msgid "allowed to fail"
msgstr ""
-msgid "already being used for another group or project milestone."
+msgid "already being used for another group or project %{timebox_name}."
msgstr ""
msgid "already has a \"created\" issue link"
@@ -24133,6 +24677,9 @@ msgstr ""
msgid "ciReport|%{linkStartTag}Learn more about SAST %{linkEndTag}"
msgstr ""
+msgid "ciReport|%{linkStartTag}Learn more about Secret Scanning %{linkEndTag}"
+msgstr ""
+
msgid "ciReport|%{linkStartTag}Learn more about codequality reports %{linkEndTag}"
msgstr ""
@@ -24296,6 +24843,12 @@ msgstr ""
msgid "ciReport|SAST"
msgstr ""
+msgid "ciReport|Secret scanning"
+msgstr ""
+
+msgid "ciReport|Secret scanning detects secrets and credentials vulnerabilities in your source code."
+msgstr ""
+
msgid "ciReport|Security scanning"
msgstr ""
@@ -24493,6 +25046,9 @@ msgstr ""
msgid "group"
msgstr ""
+msgid "groups"
+msgstr ""
+
msgid "has already been linked to another vulnerability"
msgstr ""
@@ -24656,9 +25212,6 @@ msgstr[1] ""
msgid "merged %{time_ago}"
msgstr ""
-msgid "milestone should belong either to a project or a group."
-msgstr ""
-
msgid "missing"
msgstr ""
@@ -24995,6 +25548,9 @@ msgstr "novan peton pri kunfando"
msgid "no contributions"
msgstr ""
+msgid "no expiration"
+msgstr ""
+
msgid "no one can merge"
msgstr ""
@@ -25047,6 +25603,9 @@ msgstr ""
msgid "pending removal"
msgstr ""
+msgid "per day"
+msgstr ""
+
msgid "pipeline"
msgstr ""
@@ -25075,6 +25634,12 @@ msgstr[1] ""
msgid "project avatar"
msgstr ""
+msgid "projects"
+msgstr ""
+
+msgid "push to your repository, create pipelines, create issues or add comments. To reduce storage capacity, delete unused repositories, artifacts, wikis, issues, and pipelines."
+msgstr ""
+
msgid "quick actions"
msgstr ""
@@ -25221,11 +25786,6 @@ msgstr ""
msgid "this document"
msgstr ""
-msgid "thread resolved"
-msgid_plural "threads resolved"
-msgstr[0] ""
-msgstr[1] ""
-
msgid "to help your contributors communicate effectively!"
msgstr ""
@@ -25280,6 +25840,9 @@ msgstr ""
msgid "view the blob"
msgstr ""
+msgid "vulnerability|Add a comment"
+msgstr ""
+
msgid "vulnerability|Add a comment or reason for dismissal"
msgstr ""
diff --git a/locale/es/gitlab.po b/locale/es/gitlab.po
index bba3b9a4487..d69bcadc687 100644
--- a/locale/es/gitlab.po
+++ b/locale/es/gitlab.po
@@ -12,7 +12,7 @@ msgstr ""
"X-Crowdin-Project: gitlab-ee\n"
"X-Crowdin-Language: es-ES\n"
"X-Crowdin-File: /master/locale/gitlab.pot\n"
-"PO-Revision-Date: 2020-04-15 00:27\n"
+"PO-Revision-Date: 2020-05-05 21:14\n"
msgid " %{start} to %{end}"
msgstr ""
@@ -212,6 +212,11 @@ msgid_plural "%d tags"
msgstr[0] ""
msgstr[1] ""
+msgid "%d unresolved thread"
+msgid_plural "%d unresolved threads"
+msgstr[0] ""
+msgstr[1] ""
+
msgid "%d vulnerability dismissed"
msgid_plural "%d vulnerabilities dismissed"
msgstr[0] ""
@@ -371,6 +376,9 @@ msgstr "%{mergeLength}/%{usersLength} puede fusionarse"
msgid "%{mrText}, this issue will be closed automatically."
msgstr "%{mrText}, esta incidencia se cerrará automáticamente."
+msgid "%{namespace_name} is now read-only. You cannot: %{base_message}"
+msgstr ""
+
msgid "%{name} contained %{resultsString}"
msgstr "%{name} contenido en %{resultsString}"
@@ -383,6 +391,11 @@ msgstr ""
msgid "%{name}'s avatar"
msgstr "Avatar de %{name}"
+msgid "%{no_of_days} day"
+msgid_plural "%{no_of_days} days"
+msgstr[0] ""
+msgstr[1] ""
+
msgid "%{number_commits_behind} commits behind %{default_branch}, %{number_commits_ahead} commits ahead"
msgstr "%{number_commits_behind} commits detrás de %{default_branch}, %{number_commits_ahead} commits por delante"
@@ -506,6 +519,9 @@ msgstr[1] "%{text}%{files} archivos"
msgid "%{text} is available"
msgstr "%{text} esta disponible"
+msgid "%{timebox_name} should belong either to a project or a group."
+msgstr ""
+
msgid "%{title} %{operator} %{threshold}"
msgstr "%{title} %{operator} %{threshold}"
@@ -781,12 +797,18 @@ msgstr "<code>\"johnsmith@example.com\": \"johnsm...@example.com\"</code> añadi
msgid "<code>\"johnsmith@example.com\": \"johnsmith@example.com\"</code> will add \"By <a href=\"#\">johnsmith@example.com</a>\" to all issues and comments originally created by johnsmith@example.com. By default, the email address or username is masked to ensure the user's privacy. Use this option if you want to show the full email address."
msgstr "<code>\"johnsmith@example.com\": \"johnsmith@example.com\"</code> añadirá \"Por <a href=\"#\">johnsmith@example.com</a>\" a todas las incidencias y comentarios originalmente creados por johnsmith@example.com. Por defecto, el correo electrónico o el nombre de usuario está oculto para asegurar la privacidad del usuario. Utilice esta opción si desea mostrar la dirección de correo electrónico completa."
+msgid "<namespace / project>"
+msgstr ""
+
msgid "<no name set>"
msgstr "<no tiene el nombre establecido>"
msgid "<no scopes selected>"
msgstr "<ningún alcance seleccionado>"
+msgid "<project name>"
+msgstr ""
+
msgid "<strong>%{group_name}</strong> group members"
msgstr "miembros del grupo <strong>%{group_name}</strong>"
@@ -820,6 +842,9 @@ msgstr "Se configurará una cuenta de Let's Encrypt para esta instalación de Gi
msgid "A basic page and serverless function that uses AWS Lambda, AWS API Gateway, and GitLab Pages"
msgstr ""
+msgid "A complete DevOps platform"
+msgstr ""
+
msgid "A default branch cannot be chosen for an empty project."
msgstr "No se puede elegir una rama por defecto para un proyecto vacío."
@@ -1036,6 +1061,12 @@ msgstr "Su token se utiliza para autenticarle cuando los objetos estáticos del
msgid "AccessTokens|reset it"
msgstr "Reiniciarlo"
+msgid "AccessibilityReport|Accessibility report artifact not found"
+msgstr ""
+
+msgid "AccessibilityReport|Failed to retrieve accessibility report"
+msgstr ""
+
msgid "AccessibilityReport|Learn More"
msgstr ""
@@ -1045,6 +1076,9 @@ msgstr ""
msgid "AccessibilityReport|New"
msgstr ""
+msgid "AccessibilityReport|The accessibility scanning found an error of the following type: %{code}"
+msgstr ""
+
msgid "Account"
msgstr "Cuenta"
@@ -1287,6 +1321,9 @@ msgstr "Agrada una incidencia a una tarea épica."
msgid "Added at"
msgstr "Añadido el"
+msgid "Added for this merge request"
+msgstr ""
+
msgid "Added in this version"
msgstr "Añadido en esta versión"
@@ -1658,6 +1695,72 @@ msgid_plural "Alerts"
msgstr[0] "Alerta"
msgstr[1] "Alertas"
+msgid "Alert Details"
+msgstr ""
+
+msgid "AlertManagement|Acknowledged"
+msgstr ""
+
+msgid "AlertManagement|Alert"
+msgstr ""
+
+msgid "AlertManagement|Authorize external service"
+msgstr ""
+
+msgid "AlertManagement|Display alerts from all your monitoring tools directly within GitLab. Streamline the investigation of your alerts and the escalation of alerts to incidents."
+msgstr ""
+
+msgid "AlertManagement|End time"
+msgstr ""
+
+msgid "AlertManagement|End time:"
+msgstr ""
+
+msgid "AlertManagement|Events"
+msgstr ""
+
+msgid "AlertManagement|Events:"
+msgstr ""
+
+msgid "AlertManagement|Full Alert Details"
+msgstr ""
+
+msgid "AlertManagement|More information"
+msgstr ""
+
+msgid "AlertManagement|No alerts available to display. If you think you're seeing this message in error, refresh the page."
+msgstr ""
+
+msgid "AlertManagement|No alerts to display."
+msgstr ""
+
+msgid "AlertManagement|Overview"
+msgstr ""
+
+msgid "AlertManagement|Resolved"
+msgstr ""
+
+msgid "AlertManagement|Severity"
+msgstr ""
+
+msgid "AlertManagement|Start time"
+msgstr ""
+
+msgid "AlertManagement|Start time:"
+msgstr ""
+
+msgid "AlertManagement|Status"
+msgstr ""
+
+msgid "AlertManagement|Surface alerts in GitLab"
+msgstr ""
+
+msgid "AlertManagement|There was an error displaying the alerts. Confirm your endpoint's configuration details to ensure alerts appear."
+msgstr ""
+
+msgid "AlertManagement|Triggered"
+msgstr ""
+
msgid "AlertService|%{linkStart}Learn more%{linkEnd} about configuring this endpoint to receive alerts."
msgstr "%{linkStart}Aprenda más%{linkEnd} sobre la configuración de este endpoint para recibir alertas."
@@ -1727,6 +1830,9 @@ msgstr "Todos los proyectos"
msgid "All security scans are enabled because %{linkStart}Auto DevOps%{linkEnd} is enabled on this project"
msgstr "Todas las exploraciones de seguridad están habilitadas porque %{linkStart}Auto DevOps%{linkEnd} está habilitado en este proyecto"
+msgid "All threads resolved"
+msgstr ""
+
msgid "All users"
msgstr "Todos los usuarios"
@@ -1745,6 +1851,9 @@ msgstr "Permitir a los propietarios de los grupos administrar configuraciones re
msgid "Allow only the selected protocols to be used for Git access."
msgstr "Permitir que sólo los protocolos seleccionados tengan acceso a Git."
+msgid "Allow owners to manage default branch protection per group"
+msgstr ""
+
msgid "Allow owners to manually add users outside of LDAP"
msgstr ""
@@ -1853,6 +1962,9 @@ msgstr "Se ha producido un error al obtener los aprobadores para la nueva regla.
msgid "An error occurred fetching the dropdown data."
msgstr "Se ha producido un error al recuperar los datos de la lista desplegable."
+msgid "An error occurred fetching the project authors."
+msgstr ""
+
msgid "An error occurred previewing the blob"
msgstr "Ha ocurrido un error visualizando el blob"
@@ -1934,6 +2046,9 @@ msgstr "Se ha producido un error al obtener la información de autocompletado de
msgid "An error occurred while fetching sidebar data"
msgstr "Se ha producido un error al obtener los datos de la barra lateral."
+msgid "An error occurred while fetching terraform reports."
+msgstr ""
+
msgid "An error occurred while fetching the Service Desk address."
msgstr "Se ha producido un error al obtener la dirección de Service Desk."
@@ -2171,6 +2286,9 @@ msgstr "Verificación de correo no deseado"
msgid "Any"
msgstr "Cualquiera"
+msgid "Any Author"
+msgstr ""
+
msgid "Any Label"
msgstr "Cualquier etiqueta"
@@ -2386,12 +2504,12 @@ msgstr "¿Está seguro de que desea archivar el proyecto?"
msgid "Are you sure that you want to unarchive this project?"
msgstr "¿Está seguro de que desea desarchivar este proyecto?"
-msgid "Are you sure you want to cancel creating this comment?"
-msgstr "¿Está seguro que deseas cancelar la creación de este comentario?"
-
msgid "Are you sure you want to cancel editing this comment?"
msgstr "¿Está seguro de que desea cancelar la edición de este comentario?"
+msgid "Are you sure you want to close this blocked issue?"
+msgstr ""
+
msgid "Are you sure you want to delete %{name}?"
msgstr ""
@@ -2419,6 +2537,9 @@ msgstr "¿Está seguro que desea eliminar este pipeline? Al hacerlo caducarán t
msgid "Are you sure you want to deploy this environment?"
msgstr ""
+msgid "Are you sure you want to discard this comment?"
+msgstr ""
+
msgid "Are you sure you want to erase this build?"
msgstr "¿Estás seguro de que quieres borrar esta versión?"
@@ -2595,6 +2716,9 @@ msgstr "Asigna %{assignee_users_sentence}."
msgid "At least one approval from a code owner is required to change files matching the respective CODEOWNER rules."
msgstr "Se requiere al menos la aprobación de un propietario del código para cambiar los archivos que coincidan con las reglas de CODEOWNER."
+msgid "At least one logging option is required to be enabled"
+msgstr ""
+
msgid "At least one of group_id or project_id must be specified"
msgstr "Debe especificar por lo menos un group_id o un project_id"
@@ -2633,6 +2757,27 @@ msgstr "En"
msgid "AuditEvents|Target"
msgstr "Objetivo"
+msgid "AuditLogs|(removed)"
+msgstr ""
+
+msgid "AuditLogs|Action"
+msgstr ""
+
+msgid "AuditLogs|Author"
+msgstr ""
+
+msgid "AuditLogs|Date"
+msgstr ""
+
+msgid "AuditLogs|IP Address"
+msgstr ""
+
+msgid "AuditLogs|Object"
+msgstr ""
+
+msgid "AuditLogs|Target"
+msgstr ""
+
msgid "Aug"
msgstr "Ago"
@@ -2915,9 +3060,6 @@ msgstr "La URL raíz de Bamboo, por ejemplo, https://bamboo.example.com"
msgid "BambooService|You must set up automatic revision labeling and a repository trigger in Bamboo."
msgstr "Debe configurar el etiquetado automático de revisión y un disparador del repositorio en Bamboo."
-msgid "Batch operations"
-msgstr "Operaciones por lotes"
-
msgid "BatchComments|Delete all pending comments"
msgstr "Borrar todos los comentarios pendientes"
@@ -3047,6 +3189,9 @@ msgstr "Comenzar con el conjunto de listas por defecto, lo pondrá en el camino
msgid "Boards"
msgstr "Tableros"
+msgid "Boards and Board Lists"
+msgstr ""
+
msgid "Boards|Collapse"
msgstr "Contraer"
@@ -3266,6 +3411,9 @@ msgstr "Comprar EE"
msgid "Buy GitLab Enterprise Edition"
msgstr "Comprar GitLab Enterprise Edition"
+msgid "Buy more Pipeline minutes"
+msgstr ""
+
msgid "By %{user_name}"
msgstr "Por %{user_name}"
@@ -3386,6 +3534,9 @@ msgstr "No se pueden eliminar miembros del grupo sin una cuenta administrada por
msgid "Can't scan the code?"
msgstr "¿No puede escanear el código?"
+msgid "Can't update snippet: %{err}"
+msgstr ""
+
msgid "Canary"
msgstr "Canary"
@@ -3554,6 +3705,9 @@ msgstr ""
msgid "Changing group path can have unintended side effects."
msgstr "Cambiar la ruta del grupo puede tener efectos secundarios no deseados."
+msgid "Charts"
+msgstr ""
+
msgid "Charts can't be displayed as the request for data has timed out. %{documentationLink}"
msgstr ""
@@ -3590,9 +3744,6 @@ msgstr "Tag"
msgid "ChatMessage|and [%{count} more](%{pipeline_failed_jobs_url})"
msgstr ""
-msgid "ChatMessage|failed"
-msgstr "falló"
-
msgid "ChatMessage|has failed"
msgstr "ha fallado"
@@ -3608,9 +3759,6 @@ msgstr "en %{duration}"
msgid "ChatMessage|in %{project_link}"
msgstr "en %{project_link}"
-msgid "ChatMessage|passed"
-msgstr "pasó"
-
msgid "Check again"
msgstr "Verficar de nuevo"
@@ -4061,9 +4209,6 @@ msgstr "Limpiar peso."
msgid "Click any <strong>project name</strong> in the project list below to navigate to the project milestone."
msgstr "Haga clic en cualquier <strong>nombre de proyecto</strong> en la lista de proyectos que se muestra a continuación para navegar hasta el hito de proyecto correspondiente."
-msgid "Click here"
-msgstr "Haga click aquí"
-
msgid "Click the <strong>Download</strong> button and wait for downloading to complete."
msgstr "Haga click en el botón <strong>Descargar</strong> y espere a que se complete la descarga."
@@ -4451,6 +4596,12 @@ msgstr "Obtener proyectos"
msgid "ClusterIntegration|Fetching zones"
msgstr "Obtener zonas"
+msgid "ClusterIntegration|Fluentd"
+msgstr ""
+
+msgid "ClusterIntegration|Fluentd is an open source data collector, which lets you unify the data collection and consumption for a better use and understanding of data. It requires at least one of the following logs to be successfully installed."
+msgstr ""
+
msgid "ClusterIntegration|GitLab Integration"
msgstr "Integración GitLab"
@@ -4757,6 +4908,15 @@ msgstr "Falló la solicitud para iniciar la instalación"
msgid "ClusterIntegration|Request to begin uninstalling failed"
msgstr "La solicitud para comenzar la desinstalación ha fallado"
+msgid "ClusterIntegration|SIEM Hostname"
+msgstr ""
+
+msgid "ClusterIntegration|SIEM Port"
+msgstr ""
+
+msgid "ClusterIntegration|SIEM Protocol"
+msgstr ""
+
msgid "ClusterIntegration|Save changes"
msgstr "Guardar cambios"
@@ -4853,6 +5013,12 @@ msgstr "Seleccione la zona"
msgid "ClusterIntegration|Select zone to choose machine type"
msgstr "Seleccione la zona para elegir el tipo de máquina"
+msgid "ClusterIntegration|Send Cilium Logs"
+msgstr ""
+
+msgid "ClusterIntegration|Send ModSecurity Logs"
+msgstr ""
+
msgid "ClusterIntegration|Service Token"
msgstr "Token de servicio"
@@ -5281,18 +5447,33 @@ msgstr ""
msgid "Compliance framework (optional)"
msgstr ""
+msgid "ComplianceFramework|GDPR"
+msgstr ""
+
msgid "ComplianceFramework|GDPR - General Data Protection Regulation"
msgstr ""
+msgid "ComplianceFramework|HIPAA"
+msgstr ""
+
msgid "ComplianceFramework|HIPAA - Health Insurance Portability and Accountability Act"
msgstr ""
+msgid "ComplianceFramework|PCI-DSS"
+msgstr ""
+
msgid "ComplianceFramework|PCI-DSS - Payment Card Industry-Data Security Standard"
msgstr ""
+msgid "ComplianceFramework|SOC 2"
+msgstr ""
+
msgid "ComplianceFramework|SOC 2 - Service Organization Control 2"
msgstr ""
+msgid "ComplianceFramework|SOX"
+msgstr ""
+
msgid "ComplianceFramework|SOX - Sarbanes-Oxley"
msgstr ""
@@ -5440,9 +5621,15 @@ msgstr "El registro de contenedores no está activado en esta instancia de GitLa
msgid "Container repositories sync capacity"
msgstr ""
+msgid "ContainerRegistry| Please visit the %{linkStart}administration settings%{linkEnd} to enable this feature."
+msgstr ""
+
msgid "ContainerRegistry|%{imageName} tags"
msgstr ""
+msgid "ContainerRegistry|%{title} was successfully scheduled for deletion"
+msgstr ""
+
msgid "ContainerRegistry|Automatically remove extra images that aren't designed to be kept."
msgstr "Eliminar automáticamente las imágenes adicionales que no están diseñados para ser conservadas."
@@ -5455,6 +5642,9 @@ msgstr ""
msgid "ContainerRegistry|Container Registry"
msgstr "Registro de contenedores"
+msgid "ContainerRegistry|Container Registry tag expiration and retention policy is disabled"
+msgstr ""
+
msgid "ContainerRegistry|Copy build command"
msgstr "Copiar el comando build"
@@ -5464,18 +5654,12 @@ msgstr "Copiar el comando de inicio de sesión"
msgid "ContainerRegistry|Copy push command"
msgstr "Copiar el comando push"
-msgid "ContainerRegistry|Currently, the Container Registry tag expiration feature is not available for projects created before GitLab version 12.8. For updates and more information, visit Issue %{linkStart}#196124%{linkEnd}"
-msgstr ""
-
msgid "ContainerRegistry|Docker connection error"
msgstr "Error de conexión de Docker"
msgid "ContainerRegistry|Docker tag expiration policy is %{toggleStatus}"
msgstr ""
-msgid "ContainerRegistry|Docker tags with names matching this regex pattern will expire:"
-msgstr ""
-
msgid "ContainerRegistry|Edit Settings"
msgstr ""
@@ -5497,9 +5681,6 @@ msgstr "Si no ha iniciado sesión, necesita autenticarse en el registro de conte
msgid "ContainerRegistry|Image ID"
msgstr "ID de la imagen"
-msgid "ContainerRegistry|Image deleted successfully"
-msgstr ""
-
msgid "ContainerRegistry|Keep and protect the images that matter most."
msgstr ""
@@ -5515,12 +5696,24 @@ msgstr ""
msgid "ContainerRegistry|Number of tags to retain:"
msgstr ""
+msgid "ContainerRegistry|Please contact your administrator."
+msgstr ""
+
msgid "ContainerRegistry|Push an image"
msgstr ""
msgid "ContainerRegistry|Quick Start"
msgstr "Inicio rápido"
+msgid "ContainerRegistry|Regular expressions such as %{codeStart}.*-test%{codeEnd} or %{codeStart}dev-.*%{codeEnd} are supported"
+msgstr ""
+
+msgid "ContainerRegistry|Regular expressions such as %{codeStart}.*-test%{codeEnd} or %{codeStart}dev-.*%{codeEnd} are supported. To select all tags, use %{codeStart}.*%{codeEnd}"
+msgstr ""
+
+msgid "ContainerRegistry|Remember to run %{docLinkStart}garbage collection%{docLinkEnd} to remove the stale data from storage."
+msgstr ""
+
msgid "ContainerRegistry|Remove repository"
msgstr "Borrar repositorio"
@@ -5535,22 +5728,22 @@ msgstr[1] "Eliminar etiquetas"
msgid "ContainerRegistry|Retention policy has been Enabled"
msgstr ""
-msgid "ContainerRegistry|Something went wrong while deleting the image."
+msgid "ContainerRegistry|Something went wrong while fetching the expiration policy."
msgstr ""
-msgid "ContainerRegistry|Something went wrong while deleting the tag."
+msgid "ContainerRegistry|Something went wrong while fetching the repository list."
msgstr ""
-msgid "ContainerRegistry|Something went wrong while deleting the tags."
+msgid "ContainerRegistry|Something went wrong while fetching the tags list."
msgstr ""
-msgid "ContainerRegistry|Something went wrong while fetching the expiration policy."
+msgid "ContainerRegistry|Something went wrong while marking the tag for deletion."
msgstr ""
-msgid "ContainerRegistry|Something went wrong while fetching the packages list."
+msgid "ContainerRegistry|Something went wrong while marking the tags for deletion."
msgstr ""
-msgid "ContainerRegistry|Something went wrong while fetching the tags list."
+msgid "ContainerRegistry|Something went wrong while scheduling %{title} for deletion. Please try again."
msgstr ""
msgid "ContainerRegistry|Something went wrong while updating the expiration policy."
@@ -5559,16 +5752,25 @@ msgstr "Se ha producido un error al actualizar la política de caducidad."
msgid "ContainerRegistry|Tag"
msgstr "Etiqueta"
-msgid "ContainerRegistry|Tag deleted successfully"
-msgstr ""
-
msgid "ContainerRegistry|Tag expiration policy"
msgstr "Política de caducidad de las etiquetas"
msgid "ContainerRegistry|Tag expiration policy is designed to:"
msgstr "La política de caducidad de las etiquetas está diseñada para:"
-msgid "ContainerRegistry|Tags deleted successfully"
+msgid "ContainerRegistry|Tag successfully marked for deletion."
+msgstr ""
+
+msgid "ContainerRegistry|Tags successfully marked for deletion."
+msgstr ""
+
+msgid "ContainerRegistry|Tags with names matching this regex pattern will %{italicStart}be preserved:%{italicEnd}"
+msgstr ""
+
+msgid "ContainerRegistry|Tags with names matching this regex pattern will %{italicStart}expire:%{italicEnd}"
+msgstr ""
+
+msgid "ContainerRegistry|The Container Registry tag expiration and retention policies for this project have not been enabled."
msgstr ""
msgid "ContainerRegistry|The last tag related to this image was recently removed. This empty image and any associated data will be automatically removed as part of the regular Garbage Collection process. If you have any questions, contact your administrator."
@@ -5589,18 +5791,18 @@ msgstr "No hay imágenes de contenedores disponibles en este grupo"
msgid "ContainerRegistry|There are no container images stored for this project"
msgstr "No hay imágenes de contenedores almacenadas para este proyecto"
-msgid "ContainerRegistry|This Registry contains deleted image tag data. Remember to run %{docLinkStart}garbage collection%{docLinkEnd} to remove the stale data from storage."
+msgid "ContainerRegistry|There was an error during the deletion of this image repository, please try again."
msgstr ""
msgid "ContainerRegistry|This image has no active tags"
msgstr "Esta imagen no tiene etiquetas activas"
+msgid "ContainerRegistry|This image repository is scheduled for deletion"
+msgstr ""
+
msgid "ContainerRegistry|We are having trouble connecting to Docker, which could be due to an issue with your project name or path. %{docLinkStart}More Information%{docLinkEnd}"
msgstr "Estamos teniendo problemas para conectar con Docker, lo que podría deberse a una incidencia con la ruta o el nombre de su proyecto. %{docLinkStart}Más Información%{docLinkEnd}"
-msgid "ContainerRegistry|Wildcards such as %{codeStart}.*-stable%{codeEnd} or %{codeStart}production/.*%{codeEnd} are supported. To select all tags, use %{codeStart}.*%{codeEnd}"
-msgstr ""
-
msgid "ContainerRegistry|With the Container Registry, every project can have its own space to store its Docker images. %{docLinkStart}More Information%{docLinkEnd}"
msgstr "Con el registro de contenedores, cada proyecto puede tener su propio espacio para almacenar sus imágenes de Docker. %{docLinkStart}Más Información%{docLinkEnd}"
@@ -5853,7 +6055,7 @@ msgstr ""
msgid "Could not delete chat nickname %{chat_name}."
msgstr "No es posible eliminar el apodo del chate %{chat_name}."
-msgid "Could not find design"
+msgid "Could not find design."
msgstr ""
msgid "Could not remove the trigger."
@@ -5868,6 +6070,9 @@ msgstr "No se puede revocar el token de suplantación %{token_name}."
msgid "Could not revoke personal access token %{personal_access_token_name}."
msgstr "No se pudo revocar el token de acceso personal %{personal_access_token_name}."
+msgid "Could not revoke project access token %{project_access_token_name}."
+msgstr ""
+
msgid "Could not save group ID"
msgstr ""
@@ -5925,6 +6130,9 @@ msgstr "Crear un merge request"
msgid "Create a new branch"
msgstr "Crear un nuevo 'branch'"
+msgid "Create a new deploy key for this project"
+msgstr ""
+
msgid "Create a new file as there are no files yet. Afterwards, you'll be able to commit your changes."
msgstr "Cree un nuevo archivo ya todavía no hay archivos. Posteriormente, podrá confirmar los cambios con sus commits."
@@ -6030,6 +6238,9 @@ msgstr "Crear etiqueta de proyecto"
msgid "Create requirement"
msgstr ""
+msgid "Create snippet"
+msgstr ""
+
msgid "Create wildcard: %{searchTerm}"
msgstr ""
@@ -6051,6 +6262,9 @@ msgstr "crear un token de acceso personal"
msgid "Created"
msgstr "Creado"
+msgid "Created %{timestamp}"
+msgstr ""
+
msgid "Created At"
msgstr "Creado en"
@@ -6407,6 +6621,9 @@ msgstr ""
msgid "CycleAnalytics|stage dropdown"
msgstr "Lista desplegable de las etapas"
+msgid "DAG"
+msgstr ""
+
msgid "DNS"
msgstr "DNS"
@@ -6773,6 +6990,9 @@ msgstr "Claves de despliegue"
msgid "Deploy key was successfully updated."
msgstr "La clave de despliegue se actualizó correctamente."
+msgid "Deploy keys allow read-only or read-write (if enabled) access to your repository. Deploy keys can be used for CI, staging or production servers. You can create a deploy key or add an existing one."
+msgstr ""
+
msgid "Deploy progress not found. To see pods, ensure your environment matches %{linkStart}deploy board criteria%{linkEnd}."
msgstr ""
@@ -6926,6 +7146,9 @@ msgstr "Desplegado en"
msgid "Deploying to"
msgstr "Desplegando en"
+msgid "Deployment Frequency"
+msgstr ""
+
msgid "Deployment|API"
msgstr "API"
@@ -7444,9 +7667,6 @@ msgstr "Editar descripción"
msgid "Edit environment"
msgstr "Editar el entorno"
-msgid "Edit epic description"
-msgstr "Editar la descripción de la tarea épica"
-
msgid "Edit file"
msgstr "Editar archivo"
@@ -8068,9 +8288,6 @@ msgstr "Eventos épicos"
msgid "Epics"
msgstr "Épicas"
-msgid "Epics (Ultimate / Gold license only)"
-msgstr "Tareas épicas (solo para las licencias Ultimate y Gold de Gitlab.com)"
-
msgid "Epics Roadmap"
msgstr "Hoja de ruta de tareas épicas"
@@ -8080,6 +8297,9 @@ msgstr "Tareas épicas e incidencias"
msgid "Epics let you manage your portfolio of projects more efficiently and with less effort"
msgstr "Las tareas épicas le permiten administrar sus proyectos de manera más eficiente y con menos esfuerzo"
+msgid "Epics, Issues, and Merge Requests"
+msgstr ""
+
msgid "Epics|Add an epic"
msgstr "Añadir una tarea épica"
@@ -8131,6 +8351,9 @@ msgstr "Se ha producido un error al obtener las tareas épicas hijas."
msgid "Epics|Something went wrong while fetching group epics."
msgstr "Se ha producido un error al obtener los grupos de tareas épicas."
+msgid "Epics|Something went wrong while moving item."
+msgstr ""
+
msgid "Epics|Something went wrong while ordering item."
msgstr "Se ha producido un error al ordenar el elemento."
@@ -8185,6 +8408,9 @@ msgstr "Se ha producido un error en la recuperación de las etiquetas."
msgid "Error fetching network graph."
msgstr "Se ha producido un error al obtener el gráfico de la red."
+msgid "Error fetching payload data."
+msgstr ""
+
msgid "Error fetching projects"
msgstr "Se ha producido un error al obtener los proyectos"
@@ -8194,9 +8420,6 @@ msgstr "Se ha producido un error al obtener los refs"
msgid "Error fetching the dependency list. Please check your network connection and try again."
msgstr "Se ha producido un error al obtener la lista de dependencias. Por favor revise su conexión a internet y vuelva a intentarlo."
-msgid "Error fetching usage ping data."
-msgstr "Se ha producido un error al obtener los datos de uso de ping."
-
msgid "Error loading branch data. Please try again."
msgstr "Se ha producido un error al cargar los datos de la rama. Por favor, vuelva a intentarlo."
@@ -8338,6 +8561,9 @@ msgstr "Para habilitar la selección del proyecto, ingrese un token de autentica
msgid "Errors"
msgstr "Errores"
+msgid "Errors:"
+msgstr ""
+
msgid "Estimated"
msgstr "Estimado"
@@ -8542,12 +8768,18 @@ msgstr "Explorar grupos públicos"
msgid "Export as CSV"
msgstr "Exportar como CSV"
+msgid "Export group"
+msgstr ""
+
msgid "Export issues"
msgstr "Exportar incidencias"
msgid "Export project"
msgstr "Exportar proyecto"
+msgid "Export this group with all related data to a new GitLab instance. Once complete, you can import the data file from the \"New Group\" page."
+msgstr ""
+
msgid "Export this project with all its related data in order to move your project to a new GitLab instance. Once the export is finished, you can import the file from the \"New Project\" page."
msgstr "Exporte este proyecto con todos los datos relacionados para mover su proyecto a una nueva instancia de GitLab. Una vez que finalice la exportación, puede importar el archivo desde la página \"Nuevo proyecto\"."
@@ -8617,9 +8849,6 @@ msgstr "Fallido"
msgid "Failed Jobs"
msgstr "Trabajos fallidos"
-msgid "Failed create wiki"
-msgstr "Se ha producido un error al crear el wiki"
-
msgid "Failed to add a Zoom meeting"
msgstr "Se ha producido un error al agregar una reunión de Zoom"
@@ -8659,6 +8888,9 @@ msgstr ""
msgid "Failed to create resources"
msgstr "Se ha producido un error al crear los recursos"
+msgid "Failed to create wiki"
+msgstr ""
+
msgid "Failed to delete board. Please try again."
msgstr "Se ha producido un error al eliminar el panel de control. Por favor, inténtelo de nuevo."
@@ -8668,7 +8900,7 @@ msgstr "Se ha producido un error al desplegar a"
msgid "Failed to enqueue the rebase operation, possibly due to a long-lived transaction. Try again later."
msgstr "Error al encolar la operación de rebase, posiblemente debido a una transacción de larga duración. Por favor, inténtelo de nuevo más tarde."
-msgid "Failed to find import label for jira import."
+msgid "Failed to find import label for Jira import."
msgstr ""
msgid "Failed to get ref."
@@ -8692,6 +8924,12 @@ msgstr ""
msgid "Failed to load groups & users."
msgstr "Se ha producido un error al cargar grupos y usuarios."
+msgid "Failed to load labels. Please try again."
+msgstr ""
+
+msgid "Failed to load milestones. Please try again."
+msgstr ""
+
msgid "Failed to load related branches"
msgstr "Se ha producido un error al cargar ramas relacionadas"
@@ -9067,8 +9305,11 @@ msgstr "Filtrar por mensaje del cambio"
msgid "Filter by milestone name"
msgstr "Filtrar por nombre del hito"
-msgid "Filter by name..."
-msgstr "Filtrar por nombre..."
+msgid "Filter by name"
+msgstr ""
+
+msgid "Filter by status"
+msgstr ""
msgid "Filter by two-factor authentication"
msgstr "Filtrar por autenticación de dos factores"
@@ -9079,12 +9320,18 @@ msgstr "Filtrar por usuario"
msgid "Filter projects"
msgstr "Filtrar proyectos"
+msgid "Filter results"
+msgstr ""
+
msgid "Filter results by group"
msgstr "Filtrar resultados por grupo"
msgid "Filter results by project"
msgstr "Filtrar resultados por proyecto"
+msgid "Filter results..."
+msgstr ""
+
msgid "Filter your projects by name"
msgstr "Filtra sus proyectos por nombre"
@@ -9373,12 +9620,12 @@ msgstr "Adjuntos"
msgid "GeoNodes|Checksummed"
msgstr "Comprobado"
+msgid "GeoNodes|Consult Geo troubleshooting information"
+msgstr ""
+
msgid "GeoNodes|Container repositories"
msgstr "Repositorios de contenedores"
-msgid "GeoNodes|Data is out of date from %{timeago}"
-msgstr "No se han actualizado los datos desde %{timeago}"
-
msgid "GeoNodes|Data replication lag"
msgstr "Retraso en replicación de datos"
@@ -9418,6 +9665,9 @@ msgstr "ID del último evento procesado por el cursor"
msgid "GeoNodes|Last event ID seen from primary"
msgstr "ID del último evento visto desde el principal"
+msgid "GeoNodes|Learn more about Geo node statuses"
+msgstr ""
+
msgid "GeoNodes|Loading nodes"
msgstr "Cargando nodos"
@@ -9433,6 +9683,9 @@ msgstr ""
msgid "GeoNodes|Node was successfully removed."
msgstr "El nodo se eliminó correctamente."
+msgid "GeoNodes|Node's status was updated %{timeAgo}."
+msgstr ""
+
msgid "GeoNodes|Not checksummed"
msgstr "Sin suma de verificación"
@@ -9463,8 +9716,11 @@ msgstr "Progreso de la suma de verificación del repositorio"
msgid "GeoNodes|Repository verification progress"
msgstr "GeoNodes|Progreso de la verificación del repositorio"
-msgid "GeoNodes|Selective"
-msgstr "Selectivo"
+msgid "GeoNodes|Selective (%{syncLabel})"
+msgstr ""
+
+msgid "GeoNodes|Selective synchronization"
+msgstr ""
msgid "GeoNodes|Something went wrong while changing node status"
msgstr "Se ha producido un error mientras se cambiaba el estado del nodo"
@@ -9490,6 +9746,9 @@ msgstr "Slots sin utilizar"
msgid "GeoNodes|Unverified"
msgstr " Sin verificar"
+msgid "GeoNodes|Updated %{timeAgo}"
+msgstr ""
+
msgid "GeoNodes|Used slots"
msgstr "Slots utilizados"
@@ -9529,17 +9788,17 @@ msgstr "Se ha programado %{name} para que se vuelva a comprobar"
msgid "Geo|All"
msgstr "Todos"
+msgid "Geo|All %{replicable_type}"
+msgstr ""
+
msgid "Geo|All projects"
msgstr "Todos los proyectos"
-msgid "Geo|All projects are being scheduled for re-sync"
-msgstr "Se ha programado la sincronización de los proyectos"
-
-msgid "Geo|All projects are being scheduled for re-verify"
-msgstr "Se ha programado la verificación de todos los proyectos"
+msgid "Geo|All projects are being scheduled for resync"
+msgstr ""
-msgid "Geo|Batch operations"
-msgstr "Operaciones por lotes"
+msgid "Geo|All projects are being scheduled for reverify"
+msgstr ""
msgid "Geo|Could not remove tracking entry for an existing project."
msgstr "No se puede eliminar la entrada de seguimiento para un proyecto existente."
@@ -9550,9 +9809,15 @@ msgstr "No se puede eliminar la entrada de seguimiento para un proyecto existent
msgid "Geo|Failed"
msgstr "Fallido"
+msgid "Geo|Filter by status"
+msgstr ""
+
msgid "Geo|Geo Status"
msgstr "Estado de Geo"
+msgid "Geo|In progress"
+msgstr ""
+
msgid "Geo|In sync"
msgstr "Sincronizado"
@@ -9577,9 +9842,6 @@ msgstr "Próxima sincronización programada en"
msgid "Geo|Not synced yet"
msgstr "Sin sincronizar aún"
-msgid "Geo|Pending"
-msgstr "Pendiente"
-
msgid "Geo|Pending synchronization"
msgstr "Sincronización pendiente"
@@ -9616,8 +9878,8 @@ msgstr ""
msgid "Geo|Resync"
msgstr "Resincronizar"
-msgid "Geo|Resync all projects"
-msgstr "Resincronizar todos los proyectos"
+msgid "Geo|Resync all"
+msgstr ""
msgid "Geo|Retry count"
msgstr "Contador de reintentos"
@@ -9625,8 +9887,8 @@ msgstr "Contador de reintentos"
msgid "Geo|Reverify"
msgstr "Volver a comprobar"
-msgid "Geo|Reverify all projects"
-msgstr "Volver a comprobar todos los proyectos"
+msgid "Geo|Reverify all"
+msgstr ""
msgid "Geo|Status"
msgstr "Estado"
@@ -9967,9 +10229,6 @@ msgstr "Ir a los entornos"
msgid "Go to file"
msgstr "Ir al archivo"
-msgid "Go to file (MRs only)"
-msgstr "Ir al archivo (sólo MRs)"
-
msgid "Go to file permalink (while viewing a file)"
msgstr "Ir al enlace permanente de archivo (mientras ve un archivo)"
@@ -10414,6 +10673,9 @@ msgstr "Desactivar las notificaciones por correo electrónico"
msgid "GroupSettings|Disable group mentions"
msgstr "Desactivar las menciones de grupo"
+msgid "GroupSettings|Export group"
+msgstr ""
+
msgid "GroupSettings|If the parent group's visibility is lower than the group current visibility, visibility levels for subgroups and projects will be changed to match the new parent group's visibility."
msgstr "Si la visibilidad del grupo principal es menor que la visibilidad actual del grupo, los niveles de visibilidad para subgrupos y proyectos se cambiarán para que coincidan con la visibilidad del nuevo grupo principal."
@@ -10492,6 +10754,9 @@ msgstr "Grupos"
msgid "Groups (%{count})"
msgstr "Grupos: (%{count})"
+msgid "Groups (%{groups})"
+msgstr ""
+
msgid "Groups can also be nested by creating %{subgroup_docs_link_start}subgroups%{subgroup_docs_link_end}."
msgstr "Los grupos también se pueden anidar creando %{subgroup_docs_link_start}subgrupos%{subgroup_docs_link_end}."
@@ -10757,15 +11022,12 @@ msgstr "ID"
msgid "ID:"
msgstr "ID:"
-msgid "IDE|Allow live previews of JavaScript projects in the Web IDE using CodeSandbox client side evaluation."
+msgid "IDE|Allow live previews of JavaScript projects in the Web IDE using CodeSandbox Live Preview."
msgstr ""
msgid "IDE|Back"
msgstr "Volver"
-msgid "IDE|Client side evaluation"
-msgstr "Evaluación del lado del cliente"
-
msgid "IDE|Commit"
msgstr "Commit"
@@ -10829,6 +11091,9 @@ msgstr ""
msgid "If any job surpasses this timeout threshold, it will be marked as failed. Human readable time input language is accepted like \"1 hour\". Values without specification represent seconds."
msgstr "Si algún trabajo supera este umbral de tiempo de espera, se marcará como fallido. Se permite la utilización del lenguaje de entrada de tiempo para humanos, como por ejemplo, \"1 hora\". Los valores que no contengan ninguna especificación de formato se representan como tiempo en segundos."
+msgid "If blank, set allowable lifetime to %{instance_level_policy_in_words}, as defined by the instance admin. Once set, existing tokens for users in this group may be revoked."
+msgstr ""
+
msgid "If checked, group owners can manage LDAP group links and LDAP member overrides"
msgstr "Si está marcado, los propietarios de grupos pueden administrar enlaces de grupo LDAP y anular los miembros LDAP"
@@ -10868,6 +11133,9 @@ msgstr "Si cree que esto puede ser un error, consulte la documentación para la
msgid "If you lose your recovery codes you can generate new ones, invalidating all previous codes."
msgstr "Si pierde los códigos de recuperación, puede generar otros nuevos, invalidando todos los códigos anteriores."
+msgid "If you reach 100%% storage capacity, you will not be able to: %{base_message}"
+msgstr ""
+
msgid "If your HTTP repository is not publicly accessible, add your credentials."
msgstr "Si su repositorio HTTP no es accesible públicamente, añada sus credenciales."
@@ -10979,6 +11247,9 @@ msgstr "Importar repositorios desde GitHub"
msgid "Import repository"
msgstr "Importar repositorio"
+msgid "Import started by: %{importInitiator}"
+msgstr ""
+
msgid "Import tasks"
msgstr "Importar tareas"
@@ -11045,6 +11316,9 @@ msgstr ""
msgid "In order to tailor your experience with GitLab we<br>would like to know a bit more about you."
msgstr "Para personalizar su experiencia con GitLab <br>nos gustaría conocer un poco más sobre usted."
+msgid "In progress"
+msgstr ""
+
msgid "In the next step, you'll be able to select the projects you want to import."
msgstr "En el siguiente paso, podrá seleccionar los proyectos que desea importar."
@@ -11392,15 +11666,15 @@ msgstr "Gestor de incidencias YouTrack"
msgid "Issues"
msgstr "Incidencias"
-msgid "Issues / Merge Requests"
-msgstr "Incidencias/Merge requests"
-
msgid "Issues Analytics"
msgstr ""
msgid "Issues Rate Limits"
msgstr ""
+msgid "Issues and Merge Requests"
+msgstr ""
+
msgid "Issues can be bugs, tasks or ideas to be discussed. Also, issues are searchable and filterable."
msgstr "Las incidencias pueden ser errores, tareas o ideas para ser discutidas. Además, las mismas, pueden ser buscadas y filtradas."
@@ -11422,11 +11696,11 @@ msgstr "Una vez haya comenzado a crear incidencias, podremos iniciar el seguimie
msgid "IssuesAnalytics|Avg/Month:"
msgstr "Promedio mensual:"
-msgid "IssuesAnalytics|Issues created"
-msgstr "Incidencias creadas"
+msgid "IssuesAnalytics|Issues opened"
+msgstr ""
-msgid "IssuesAnalytics|Issues created per month"
-msgstr "Incidencias creadas por mes"
+msgid "IssuesAnalytics|Issues opened per month"
+msgstr ""
msgid "IssuesAnalytics|Last 12 months"
msgstr "Últimos 12 meses"
@@ -11479,6 +11753,9 @@ msgstr ""
msgid "Jira integration not configured."
msgstr ""
+msgid "Jira project: %{importProject}"
+msgstr ""
+
msgid "JiraService|Events for %{noteable_model_name} are disabled."
msgstr "Los eventos para %{noteable_model_name} están deshabilitados."
@@ -11868,6 +12145,9 @@ msgstr ""
msgid "Lead"
msgstr ""
+msgid "Lead Time"
+msgstr ""
+
msgid "Learn GitLab"
msgstr "Aprenda GitLab"
@@ -11880,6 +12160,9 @@ msgstr "Aprenda como contribuir %{link_start}a las plantillas integradas%{link_e
msgid "Learn how to %{no_packages_link_start}publish and share your packages%{no_packages_link_end} with GitLab."
msgstr "Aprenda como %{no_packages_link_start}publicar y compartir sus paquetes%{no_packages_link_end} con GitLab."
+msgid "Learn how to enable synchronization"
+msgstr ""
+
msgid "Learn more"
msgstr "Conozca más"
@@ -12211,9 +12494,6 @@ msgstr "Listar sus repositorios de Bitbucket Server"
msgid "Live preview"
msgstr "Vista previa"
-msgid "Load more vulnerabilities"
-msgstr ""
-
msgid "Loading"
msgstr ""
@@ -12364,9 +12644,6 @@ msgstr "Cadena mal formada"
msgid "Manage"
msgstr "Administrar"
-msgid "Manage Git repositories with fine-grained access controls that keep your code secure. Perform code reviews and enhance collaboration with merge requests. Each project can also have an issue tracker and a wiki."
-msgstr "Administre los repositorios de Git con controles de acceso detallados que mantienen su código seguro. Realice revisiones de código y mejore la colaboración con merge request. Cada proyecto también puede tener una herramienta de gestión de incidencias y un wiki."
-
msgid "Manage Web IDE features"
msgstr "Administrar las características del Web IDE"
@@ -12397,6 +12674,9 @@ msgstr "Administrar autenticación de dos factores"
msgid "Manage your license"
msgstr ""
+msgid "Managed Account"
+msgstr ""
+
msgid "Manifest"
msgstr "Manifiesto"
@@ -12661,6 +12941,9 @@ msgstr "Solicitudes de merge creadas"
msgid "Merge Requests in Review"
msgstr ""
+msgid "Merge automatically (%{strategy})"
+msgstr ""
+
msgid "Merge commit message"
msgstr "Mensaje del merge commit"
@@ -12814,6 +13097,12 @@ msgstr "Fusionado"
msgid "Merged branches are being deleted. This can take some time depending on the number of branches. Please refresh the page to see changes."
msgstr ""
+msgid "Merged this merge request."
+msgstr ""
+
+msgid "Merges this merge request immediately."
+msgstr ""
+
msgid "Merges this merge request when the pipeline succeeds."
msgstr ""
@@ -12921,12 +13210,15 @@ msgid_plural "Metrics|Edit metrics"
msgstr[0] ""
msgstr[1] ""
-msgid "Metrics|Environment"
-msgstr "Entorno"
+msgid "Metrics|Expand panel"
+msgstr ""
msgid "Metrics|For grouping similar metrics"
msgstr "Para agrupar métricas similares"
+msgid "Metrics|Go back (Esc)"
+msgstr ""
+
msgid "Metrics|Invalid time range, please verify."
msgstr ""
@@ -12957,9 +13249,6 @@ msgstr "Documentción sobre las consultas de Prometheus"
msgid "Metrics|Refresh dashboard"
msgstr ""
-msgid "Metrics|Show last"
-msgstr "Mostrar último"
-
msgid "Metrics|There was an error creating the dashboard."
msgstr ""
@@ -13038,6 +13327,9 @@ msgstr "por ejemplo, req/seg."
msgid "Microsoft Azure"
msgstr "Microsoft Azure"
+msgid "Middleman project with Static Site Editor support"
+msgstr ""
+
msgid "Migrated %{success_count}/%{total_count} files."
msgstr "Migrados %{success_count}/%{total_count} archivos."
@@ -13483,11 +13775,11 @@ msgstr "Los usuarios recién registrados serán creados, por defecto, como exter
msgid "Next"
msgstr "Siguiente"
-msgid "Next file in diff (MRs only)"
-msgstr "Siguiente archivo en diff (sólo MRs)"
+msgid "Next file in diff"
+msgstr ""
-msgid "Next unresolved discussion (MRs only)"
-msgstr "Siguiente discusión sin resolver (solo MRs)"
+msgid "Next unresolved discussion"
+msgstr ""
msgid "Nickname"
msgstr "Seudónimo"
@@ -13546,6 +13838,9 @@ msgstr "Sin cambios"
msgid "No changes between %{ref_start}%{source_branch}%{ref_end} and %{ref_start}%{target_branch}%{ref_end}"
msgstr "No hay cambios entre %{ref_start}%{source_branch}%{ref_end} y %{ref_start}%{target_branch}%{ref_end}"
+msgid "No child epics match applied filters"
+msgstr ""
+
msgid "No connection could be made to a Gitaly Server, please check your logs!"
msgstr "¡No ha sido posible realizar la conexión con un servidor de Gitaly, por favor compruebe sus logs!"
@@ -13684,15 +13979,6 @@ msgstr ""
msgid "No thanks, don't show this again"
msgstr ""
-msgid "No vulnerabilities found for this group"
-msgstr "No se han encontrado vulnerabilidades para este grupo"
-
-msgid "No vulnerabilities found for this pipeline"
-msgstr "No se han encontrado vulnerabilidades para este pipeline"
-
-msgid "No vulnerabilities found for this project"
-msgstr "No se han encontrado vulnerabilidades para este proyecto"
-
msgid "No vulnerabilities present"
msgstr "No se han encontrado vulnerabilidades"
@@ -13816,6 +14102,9 @@ msgstr ""
msgid "Nothing to preview."
msgstr "Nada para previsualizar."
+msgid "Nothing to synchronize"
+msgstr ""
+
msgid "Notification events"
msgstr "Eventos de notificación"
@@ -13987,6 +14276,9 @@ msgstr "Una vez importados, los repositorios se pueden replicar vía de SSH. Par
msgid "Once removed, the fork relationship cannot be restored and you will no longer be able to send merge requests to the source."
msgstr ""
+msgid "Once the exported file is ready you can download it from this page."
+msgstr ""
+
msgid "Once the exported file is ready, you will receive a notification email with a download link, or you can download it from this page."
msgstr "Una vez que el archivo exportado esté preparado, recibirá una notificación por correo electrónico con un enlace para su descarga, también puede descargarlo desde esta página."
@@ -14088,9 +14380,6 @@ msgstr "Abrir raw"
msgid "Open sidebar"
msgstr "Abrir barra lateral"
-msgid "Open source software to collaborate on code"
-msgstr "Software de código abierto para colaborar en el código"
-
msgid "Open: %{openIssuesCount}"
msgstr ""
@@ -14193,6 +14482,9 @@ msgstr ""
msgid "OutdatedBrowser|You can provide feedback %{feedback_link_start}on this issue%{feedback_link_end} or via your usual support channels."
msgstr ""
+msgid "Overridden"
+msgstr ""
+
msgid "Overview"
msgstr "Resumen"
@@ -14244,6 +14536,9 @@ msgstr ""
msgid "PackageRegistry|Conan Command"
msgstr ""
+msgid "PackageRegistry|Copy .pypirc content"
+msgstr ""
+
msgid "PackageRegistry|Copy Conan Command"
msgstr ""
@@ -14265,6 +14560,9 @@ msgstr ""
msgid "PackageRegistry|Copy NuGet Setup Command"
msgstr ""
+msgid "PackageRegistry|Copy Pip command"
+msgstr ""
+
msgid "PackageRegistry|Copy and paste this inside your %{codeStart}pom.xml%{codeEnd} %{codeStart}dependencies%{codeEnd} block."
msgstr ""
@@ -14298,6 +14596,12 @@ msgstr "Para más información sobre el registro de Maven, %{linkStart}vea la do
msgid "PackageRegistry|For more information on the NuGet registry, %{linkStart}see the documentation%{linkEnd}."
msgstr ""
+msgid "PackageRegistry|For more information on the PyPi registry, %{linkStart}see the documentation%{linkEnd}."
+msgstr ""
+
+msgid "PackageRegistry|If you haven't already done so, you will need to add the below to your %{codeStart}.pypirc%{codeEnd} file."
+msgstr ""
+
msgid "PackageRegistry|If you haven't already done so, you will need to add the below to your %{codeStart}pom.xml%{codeEnd} file."
msgstr "Si aún no lo ha hecho, debe añadir lo siguiente a su archivo %{codeStart}pom.xml%{codeEnd}."
@@ -14307,6 +14611,9 @@ msgstr "Instalación"
msgid "PackageRegistry|Learn how to %{noPackagesLinkStart}publish and share your packages%{noPackagesLinkEnd} with GitLab."
msgstr "Aprenda a %{noPackagesLinkStart}publicar y compartir sus paquetes%{noPackagesLinkEnd} con GitLab."
+msgid "PackageRegistry|Manually Published"
+msgstr ""
+
msgid "PackageRegistry|Maven"
msgstr ""
@@ -14325,12 +14632,18 @@ msgstr ""
msgid "PackageRegistry|NuGet Command"
msgstr ""
+msgid "PackageRegistry|Pip Command"
+msgstr ""
+
msgid "PackageRegistry|Pipeline %{linkStart}%{linkEnd} triggered %{timestamp} by %{author}"
msgstr ""
msgid "PackageRegistry|Published to the repository at %{timestamp}"
msgstr ""
+msgid "PackageRegistry|PyPi"
+msgstr ""
+
msgid "PackageRegistry|Registry Setup"
msgstr "Configuración del registro"
@@ -14367,6 +14680,9 @@ msgstr ""
msgid "PackageRegistry|npm"
msgstr "npm"
+msgid "PackageRegistry|published by %{author}"
+msgstr ""
+
msgid "PackageRegistry|yarn"
msgstr "yarn"
@@ -14382,6 +14698,9 @@ msgstr ""
msgid "PackageType|NuGet"
msgstr ""
+msgid "PackageType|PyPi"
+msgstr ""
+
msgid "Packages"
msgstr "Paquetes"
@@ -14475,6 +14794,9 @@ msgstr "La contraseña se actualizó correctamente. Por favor, inicie la sesión
msgid "Past due"
msgstr "Vencido"
+msgid "Paste a machine public key here. Read more about how to generate it"
+msgstr ""
+
msgid "Paste a machine public key here. Read more about how to generate it %{link_start}here%{link_end}"
msgstr "Pegue aquí la clave pública de la máquina. Obtenga más información acerca de cómo generarla %{link_start}aquí%{link_end}"
@@ -14490,9 +14812,6 @@ msgstr "Pegue su clave pública SSH, que generalmente está contenida en el arch
msgid "Path"
msgstr "Ruta"
-msgid "Path, transfer, remove"
-msgstr "Ruta, transferir, eliminar"
-
msgid "Path:"
msgstr "Ruta:"
@@ -14523,8 +14842,8 @@ msgstr ""
msgid "Percentage"
msgstr ""
-msgid "Perform advanced options such as changing path, transferring, or removing the group."
-msgstr "Realice opciones avanzadas como cambiar la ruta, transferir o eliminar el grupo."
+msgid "Perform advanced options such as changing path, transferring, exporting, or removing the group."
+msgstr ""
msgid "Perform common operations on GitLab project"
msgstr "Realice operaciones comunes en el proyecto GitLab"
@@ -14604,9 +14923,6 @@ msgstr "Programaciones de los Pipelines"
msgid "Pipeline minutes quota"
msgstr "Cuota de minutos del pipeline"
-msgid "Pipeline quota"
-msgstr "Cuota de pipelines"
-
msgid "Pipeline subscriptions"
msgstr ""
@@ -14616,6 +14932,9 @@ msgstr "Disparadores de los triggers"
msgid "Pipeline: %{status}"
msgstr "Pipeline: %{status}"
+msgid "PipelineCharts|CI / CD Analytics"
+msgstr ""
+
msgid "PipelineCharts|Failed:"
msgstr "Fallidos:"
@@ -14694,15 +15013,6 @@ msgstr ""
msgid "Pipelines settings for '%{project_name}' were successfully updated."
msgstr "La configuración de los pipelines para '%{project_name}' se actualizó correctamente."
-msgid "Pipelines| to purchase more minutes."
-msgstr "para comprar más minutos."
-
-msgid "Pipelines|%{namespace_name} has exceeded its pipeline minutes quota."
-msgstr "%{namespace_name} ha excedido la cuota de minutos de su pipeline."
-
-msgid "Pipelines|%{namespace_name} has less than %{notification_level}%% of CI minutes available."
-msgstr "%{namespace_name} tiene menos de %{notification_level}%% minutos CI disponibles."
-
msgid "Pipelines|API"
msgstr "API"
@@ -14724,12 +15034,15 @@ msgstr "La integración continua le puede ayudar a capturar errores en su produc
msgid "Pipelines|Get started with Pipelines"
msgstr "Comenzar a utilizar los pipelines"
+msgid "Pipelines|Group %{namespace_name} has %{percentage}%% or less Shared Runner Pipeline minutes remaining. Once it runs out, no new jobs or pipelines in its projects will run."
+msgstr ""
+
+msgid "Pipelines|Group %{namespace_name} has exceeded its pipeline minutes quota. Unless you buy additional pipeline minutes, no new jobs or pipelines in its projects will run."
+msgstr ""
+
msgid "Pipelines|Loading Pipelines"
msgstr "Cargar pipelines"
-msgid "Pipelines|Pipelines will not run anymore on shared Runners."
-msgstr "Los pipelines no se ejecutarán en los ejecutores compartidos."
-
msgid "Pipelines|Project cache successfully reset."
msgstr "Caché del proyecto restablecida correctamente."
@@ -14958,6 +15271,9 @@ msgstr "Por favor, vuelva a introducir la dirección de correo electrónico."
msgid "Please select"
msgstr "Por favor, seleccione"
+msgid "Please select a Jira project"
+msgstr ""
+
msgid "Please select a country"
msgstr "Por favor, seleccione un país"
@@ -15003,6 +15319,9 @@ msgstr "Por favor espere mientras nos conectamos a su repositorio."
msgid "Please wait while we import the repository for you. Refresh at will."
msgstr "Por favor espere mientras importamos el repositorio por usted."
+msgid "Plugins directory is deprecated and will be removed in 14.0. Please move this file into /file_hooks directory."
+msgstr ""
+
msgid "Pod does not exist"
msgstr ""
@@ -15120,7 +15439,7 @@ msgstr ""
msgid "Prevent users from changing their profile name"
msgstr ""
-msgid "Prevent users from modifing merge request approvers list"
+msgid "Prevent users from modifying merge request approvers list"
msgstr ""
msgid "Prevent users from performing write operations on GitLab while performing maintenance."
@@ -15141,11 +15460,11 @@ msgstr "Vista previa del payload"
msgid "Previous Artifacts"
msgstr "Artefactos anteriores"
-msgid "Previous file in diff (MRs only)"
-msgstr "Archivo previo en diff (sólo MRs)"
+msgid "Previous file in diff"
+msgstr ""
-msgid "Previous unresolved discussion (MRs only)"
-msgstr "Discusión previa sin resolver (solo MRs)"
+msgid "Previous unresolved discussion"
+msgstr ""
msgid "Primary"
msgstr "Principal"
@@ -15177,6 +15496,9 @@ msgstr "Grupo(s) privado(s)"
msgid "Private profile"
msgstr "Perfil privado"
+msgid "Private projects Minutes cost factor"
+msgstr ""
+
msgid "Private projects can be created in your personal namespace with:"
msgstr "Los proyectos privados se pueden crear en su espacio de nombres personal con:"
@@ -16005,6 +16327,9 @@ msgstr "Wiki"
msgid "ProjectSettings|With GitLab Pages you can host your static websites on GitLab"
msgstr "Con GitLab Pages puede alojar sus sitios web estáticos en GitLab"
+msgid "ProjectSettings|With Metrics Dashboard you can visualize this project performance metrics"
+msgstr ""
+
msgid "ProjectTemplates|.NET Core"
msgstr ".NET Core"
@@ -16068,6 +16393,9 @@ msgstr "Serverless Framework/JS"
msgid "ProjectTemplates|Spring"
msgstr "Spring"
+msgid "ProjectTemplates|Static Site Editor/Middleman"
+msgstr ""
+
msgid "ProjectTemplates|iOS (Swift)"
msgstr "IOS (Swift)"
@@ -16287,15 +16615,15 @@ msgstr "URL Base de Prometheus, como http://prometheus.example.com/"
msgid "PrometheusService|Prometheus is being automatically managed on your clusters"
msgstr "Prometheus se administra automáticamente en sus clusters"
+msgid "PrometheusService|Select the Active checkbox to override the Auto Configuration with custom settings. If unchecked, Auto Configuration settings are used."
+msgstr ""
+
msgid "PrometheusService|These metrics will only be monitored after your first deployment to an environment"
msgstr "Estas métricas serán monitorizadas una vez se realice el primer despliegue a un entorno"
msgid "PrometheusService|Time-series monitoring service"
msgstr "Servicio de monitorización de series de tiempo"
-msgid "PrometheusService|To enable manual configuration, uninstall Prometheus from your clusters"
-msgstr "Para habilitar la configuración manual, por favor, desinstale Prometheus de sus clústeres"
-
msgid "PrometheusService|To enable the installation of Prometheus on your clusters, deactivate the manual configuration below"
msgstr "Para permitir la instalación de Prometheus en sus clústeres, por favor, desactive la configuración manual"
@@ -16479,6 +16807,9 @@ msgstr "Se ha desprotegido su entorno"
msgid "Protip:"
msgstr "Sugerencia:"
+msgid "Protocol"
+msgstr ""
+
msgid "Provider"
msgstr "Proveedor"
@@ -16503,6 +16834,9 @@ msgstr "Claves de despliegue público (%{deploy_keys_count})"
msgid "Public pipelines"
msgstr "Pipelines públicos"
+msgid "Public projects Minutes cost factor"
+msgstr ""
+
msgid "Pull"
msgstr "Pull"
@@ -16703,6 +17037,9 @@ msgid_plural "Refreshing in %d seconds to show the updated status..."
msgstr[0] "Actualizar en un segundo para mostrar el estado actualizado..."
msgstr[1] "Actualizar en %d segundos para mostrar el estado actualizado..."
+msgid "Regenerate export"
+msgstr ""
+
msgid "Regenerate instance ID"
msgstr "Regenerar el lD de la instancia"
@@ -17080,9 +17417,29 @@ msgstr "Informes"
msgid "Reports|%{combinedString} and %{resolvedString}"
msgstr ""
+msgid "Reports|Accessibility scanning detected %d issue for the source branch only"
+msgid_plural "Reports|Accessibility scanning detected %d issues for the source branch only"
+msgstr[0] ""
+msgstr[1] ""
+
+msgid "Reports|Accessibility scanning detected no issues for the source branch only"
+msgstr ""
+
+msgid "Reports|Accessibility scanning failed loading results"
+msgstr ""
+
+msgid "Reports|Accessibility scanning results are being parsed"
+msgstr ""
+
msgid "Reports|Actions"
msgstr "Acciones"
+msgid "Reports|An error occured while loading report"
+msgstr ""
+
+msgid "Reports|An error occurred while loading %{name} results"
+msgstr ""
+
msgid "Reports|Class"
msgstr "Clase"
@@ -17188,7 +17545,7 @@ msgstr "La solicitud para vincular la cuenta SAML debe ser autorizada"
msgid "Requested %{time_ago}"
msgstr "Solicitado %{time_ago}"
-msgid "Requested design version does not exist"
+msgid "Requested design version does not exist."
msgstr ""
msgid "Requested states are invalid"
@@ -17372,6 +17729,9 @@ msgstr "Reanudar la replicación"
msgid "Resync"
msgstr "Resincronizar"
+msgid "Resync all"
+msgstr ""
+
msgid "Resync all %{replicableType}"
msgstr ""
@@ -17440,6 +17800,9 @@ msgstr "¡Token de suplantación revocado %{token_name}!"
msgid "Revoked personal access token %{personal_access_token_name}!"
msgstr "Se ha revocado el token de acceso personal %{personal_access_token_name}!"
+msgid "Revoked project access token %{project_access_token_name}!"
+msgstr ""
+
msgid "RightSidebar|adding a"
msgstr "Agregar a"
@@ -17488,6 +17851,9 @@ msgstr "El ejecutor ejecuta los trabajos de todos los proyectos asignados"
msgid "Runner token"
msgstr "Token del ejecutor"
+msgid "Runner tokens"
+msgstr ""
+
msgid "Runner was not updated."
msgstr "No se actualizó el ejecutor."
@@ -17542,6 +17908,9 @@ msgstr "SAML SSO"
msgid "SAML SSO for %{group_name}"
msgstr "SAML SSO para %{group_name}"
+msgid "SAML discovery tokens"
+msgstr ""
+
msgid "SAML for %{group_name}"
msgstr "SAML para %{group_name}"
@@ -17626,12 +17995,18 @@ msgstr "Programar un nuevo pipeline"
msgid "Scheduled"
msgstr "Programado"
+msgid "Scheduled to merge this merge request (%{strategy})."
+msgstr ""
+
msgid "Scheduled to merge this merge request when the pipeline succeeds."
msgstr ""
msgid "Schedules"
msgstr "Programaciones"
+msgid "Schedules to merge this merge request (%{strategy})."
+msgstr ""
+
msgid "Scheduling"
msgstr "Programación"
@@ -17689,6 +18064,9 @@ msgstr "Buscar ramas"
msgid "Search branches and tags"
msgstr "Buscar ramas y etiquetas"
+msgid "Search by author"
+msgstr ""
+
msgid "Search files"
msgstr "Buscar archivos"
@@ -17834,11 +18212,6 @@ msgid_plural "SearchResults|snippets"
msgstr[0] "fragmento de código"
msgstr[1] "fragmentos de código"
-msgid "SearchResults|snippet result"
-msgid_plural "SearchResults|snippet results"
-msgstr[0] "Resultado del fragmento de código"
-msgstr[1] "Resultados del fragmento de código"
-
msgid "SearchResults|user"
msgid_plural "SearchResults|users"
msgstr[0] "usuario"
@@ -17849,6 +18222,9 @@ msgid_plural "SearchResults|wiki results"
msgstr[0] "Resultado de la wiki"
msgstr[1] "Resultados de la wiki"
+msgid "Searching by both author and message is currently not supported."
+msgstr ""
+
msgid "Seat Link"
msgstr ""
@@ -17876,185 +18252,233 @@ msgstr "Configuración de seguridad"
msgid "Security Dashboard"
msgstr "Panel de control de seguridad"
-msgid "Security Dashboard|Error fetching the vulnerability counts. Please check your network connection and try again."
-msgstr "Se ha producido un error al obtener los datos del panel de control. Por favor, compruebe su conexión de red y vuelva a intentarlo."
+msgid "Security configuration help link"
+msgstr "Enlace de ayuda de configuración de seguridad"
-msgid "Security Dashboard|Error fetching the vulnerability list. Please check your network connection and try again."
-msgstr "Se ha producido un error al obtener los datos del panel de control. Por favor, compruebe su conexión de red y vuelva a intentarlo."
+msgid "Security dashboard"
+msgstr "Panel de control de seguridad"
-msgid "Security Dashboard|Issue Created"
-msgstr "Incidencia creada"
+msgid "Security report is out of date. Please update your branch with the latest changes from the target branch (%{targetBranchName})"
+msgstr ""
-msgid "Security Reports|Comment added to '%{vulnerabilityName}'"
-msgstr "Comentario agregado a '%{vulnerabilityName}'"
+msgid "Security report is out of date. Run %{newPipelineLinkStart}a new pipeline%{newPipelineLinkEnd} for the target branch (%{targetBranchName})"
+msgstr ""
-msgid "Security Reports|Comment deleted on '%{vulnerabilityName}'"
-msgstr "Comentario eliminado en '%{vulnerabilityName}'"
+msgid "SecurityConfiguration|Configured"
+msgstr "Configurado"
-msgid "Security Reports|Comment edited on '%{vulnerabilityName}'"
-msgstr "Comentario editado en '%{vulnerabilityName}'"
+msgid "SecurityConfiguration|Feature"
+msgstr "Característica"
-msgid "Security Reports|Create issue"
-msgstr "Crear incidencia"
+msgid "SecurityConfiguration|Feature documentation for %{featureName}"
+msgstr ""
+
+msgid "SecurityConfiguration|Not yet configured"
+msgstr "Todavía no está configurado"
-msgid "Security Reports|Dismiss vulnerability"
-msgstr "Informes de seguridad | Descartar vulnerabilidad"
+msgid "SecurityConfiguration|Secure features"
+msgstr "Características seguras"
-msgid "Security Reports|Dismissed '%{vulnerabilityName}'"
-msgstr "Descartado '%{vulnerabilityName}'"
+msgid "SecurityConfiguration|Status"
+msgstr "Estado"
-msgid "Security Reports|Dismissed '%{vulnerabilityName}'. Turn off the hide dismissed toggle to view."
+msgid "SecurityReports|%{firstProject} and %{secondProject}"
msgstr ""
-msgid "Security Reports|Either you don't have permission to view this dashboard or the dashboard has not been setup. Please check your permission settings with your administrator or check your dashboard configurations to proceed."
+msgid "SecurityReports|%{firstProject}, %{secondProject}, and %{rest}"
msgstr ""
-msgid "Security Reports|Learn more about setting up your dashboard"
-msgstr "Obtenga más información sobre cómo configurar su panel de control"
+msgid "SecurityReports|Add a project to your dashboard"
+msgstr ""
-msgid "Security Reports|More info"
-msgstr "Más información"
+msgid "SecurityReports|Add or remove projects from your dashboard"
+msgstr ""
-msgid "Security Reports|Oops, something doesn't seem right."
-msgstr "Oops, algo no parece correcto."
+msgid "SecurityReports|Add projects"
+msgstr ""
-msgid "Security Reports|Security reports can only be accessed by authorized users."
+msgid "SecurityReports|Comment added to '%{vulnerabilityName}'"
msgstr ""
-msgid "Security Reports|There was an error adding the comment."
-msgstr "Se ha producido un error añadir el comentario."
+msgid "SecurityReports|Comment deleted on '%{vulnerabilityName}'"
+msgstr ""
-msgid "Security Reports|There was an error creating the issue."
-msgstr "Se ha producido un error al crear la incidencia."
+msgid "SecurityReports|Comment edited on '%{vulnerabilityName}'"
+msgstr ""
-msgid "Security Reports|There was an error creating the merge request."
-msgstr "Se ha producido un error al crear el merge request."
+msgid "SecurityReports|Create issue"
+msgstr ""
-msgid "Security Reports|There was an error deleting the comment."
-msgstr "Se ha producido un error al eliminar el comentario."
+msgid "SecurityReports|Dismiss Selected"
+msgstr ""
-msgid "Security Reports|There was an error dismissing the vulnerabilities."
+msgid "SecurityReports|Dismiss vulnerability"
msgstr ""
-msgid "Security Reports|There was an error dismissing the vulnerability."
-msgstr "Se ha producido un error al descartar la vulnerabilidad."
+msgid "SecurityReports|Dismissed '%{vulnerabilityName}'"
+msgstr ""
-msgid "Security Reports|There was an error reverting the dismissal."
+msgid "SecurityReports|Dismissed '%{vulnerabilityName}'. Turn off the hide dismissed toggle to view."
msgstr ""
-msgid "Security Reports|There was an error reverting this dismissal."
+msgid "SecurityReports|Each vulnerability now has a unique page that can be directly linked to, shared, referenced, and tracked as the single source of truth. Vulnerability occurrences also persist across scanner runs, which improves tracking and visibility and reduces duplicates between scans."
msgstr ""
-msgid "Security Reports|Undo dismiss"
+msgid "SecurityReports|Edit dashboard"
msgstr ""
-msgid "Security Reports|You do not have sufficient permissions to access this report"
+msgid "SecurityReports|Either you don't have permission to view this dashboard or the dashboard has not been setup. Please check your permission settings with your administrator or check your dashboard configurations to proceed."
msgstr ""
-msgid "Security Reports|You must sign in as an authorized user to see this report"
+msgid "SecurityReports|Error fetching the vulnerability counts. Please check your network connection and try again."
msgstr ""
-msgid "Security configuration help link"
-msgstr "Enlace de ayuda de configuración de seguridad"
+msgid "SecurityReports|Error fetching the vulnerability list. Please check your network connection and try again."
+msgstr ""
-msgid "Security dashboard"
-msgstr "Panel de control de seguridad"
+msgid "SecurityReports|False positive"
+msgstr ""
-msgid "Security report is out of date. Please update your branch with the latest changes from the target branch (%{targetBranchName})"
+msgid "SecurityReports|Hide dismissed"
msgstr ""
-msgid "Security report is out of date. Run %{newPipelineLinkStart}a new pipeline%{newPipelineLinkEnd} for the target branch (%{targetBranchName})"
+msgid "SecurityReports|Introducing standalone vulnerabilities"
msgstr ""
-msgid "SecurityConfiguration|Configured"
-msgstr "Configurado"
+msgid "SecurityReports|Issue Created"
+msgstr ""
-msgid "SecurityConfiguration|Feature"
-msgstr "Característica"
+msgid "SecurityReports|Learn More"
+msgstr ""
-msgid "SecurityConfiguration|Feature documentation for %{featureName}"
+msgid "SecurityReports|Learn more about setting up your dashboard"
msgstr ""
-msgid "SecurityConfiguration|Not yet configured"
-msgstr "Todavía no está configurado"
+msgid "SecurityReports|Load more vulnerabilities"
+msgstr ""
-msgid "SecurityConfiguration|Secure features"
-msgstr "Características seguras"
+msgid "SecurityReports|Monitor vulnerabilities in your code"
+msgstr ""
-msgid "SecurityConfiguration|Status"
-msgstr "Estado"
+msgid "SecurityReports|More info"
+msgstr ""
-msgid "SecurityDashboard|%{firstProject} and %{secondProject}"
-msgstr "%{firstProject} y %{secondProject}"
+msgid "SecurityReports|More information"
+msgstr ""
-msgid "SecurityDashboard|%{firstProject}, %{secondProject}, and %{rest}"
-msgstr "%{firstProject}, %{secondProject} y %{rest}"
+msgid "SecurityReports|No vulnerabilities found for dashboard"
+msgstr ""
-msgid "SecurityDashboard|Add a project to your dashboard"
-msgstr "Añadir un proyecto al panel de control"
+msgid "SecurityReports|No vulnerabilities found for this group"
+msgstr ""
+
+msgid "SecurityReports|No vulnerabilities found for this pipeline"
+msgstr ""
-msgid "SecurityDashboard|Add or remove projects from your dashboard"
-msgstr "Añadir o eliminar proyectos de su panel de control"
+msgid "SecurityReports|No vulnerabilities found for this project"
+msgstr ""
-msgid "SecurityDashboard|Add projects"
-msgstr "Añadir proyectos"
+msgid "SecurityReports|Oops, something doesn't seem right."
+msgstr ""
-msgid "SecurityDashboard|Edit dashboard"
-msgstr "Editar panel de control"
+msgid "SecurityReports|Pipeline %{pipelineLink} triggered %{timeago} by %{user}"
+msgstr ""
-msgid "SecurityDashboard|Hide dismissed"
-msgstr "Ocultar descartado"
+msgid "SecurityReports|Project"
+msgstr ""
-msgid "SecurityDashboard|Monitor vulnerabilities in your code"
-msgstr "Monitorizar las vulnerabilidades en su código"
+msgid "SecurityReports|Projects added"
+msgstr ""
-msgid "SecurityDashboard|More information"
-msgstr "Más información"
+msgid "SecurityReports|Remove project from dashboard"
+msgstr ""
-msgid "SecurityDashboard|No vulnerabilities found for dashboard"
+msgid "SecurityReports|Report type"
msgstr ""
-msgid "SecurityDashboard|Pipeline %{pipelineLink} triggered %{timeago} by %{user}"
+msgid "SecurityReports|Return to dashboard"
msgstr ""
-msgid "SecurityDashboard|Project"
-msgstr "Proyecto"
+msgid "SecurityReports|Security Dashboard"
+msgstr ""
-msgid "SecurityDashboard|Projects added"
-msgstr "Proyectos añadidos"
+msgid "SecurityReports|Security reports can only be accessed by authorized users."
+msgstr ""
-msgid "SecurityDashboard|Remove project from dashboard"
-msgstr "Eliminar proyecto del panel de control"
+msgid "SecurityReports|Select a project to add by using the project search field above."
+msgstr ""
-msgid "SecurityDashboard|Report type"
-msgstr "Tipo de informe"
+msgid "SecurityReports|Select a reason"
+msgstr ""
-msgid "SecurityDashboard|Return to dashboard"
-msgstr "Volver al panel de control"
+msgid "SecurityReports|Severity"
+msgstr ""
-msgid "SecurityDashboard|Security Dashboard"
-msgstr "Panel de seguridad"
+msgid "SecurityReports|Status"
+msgstr ""
-msgid "SecurityDashboard|Select a project to add by using the project search field above."
+msgid "SecurityReports|The security dashboard displays the latest security findings for projects you wish to monitor. Select \"Edit dashboard\" to add and remove projects."
msgstr ""
-msgid "SecurityDashboard|Severity"
-msgstr "Gravedad"
+msgid "SecurityReports|The security dashboard displays the latest security report. Use it to find and fix vulnerabilities."
+msgstr ""
-msgid "SecurityDashboard|Status"
+msgid "SecurityReports|There was an error adding the comment."
msgstr ""
-msgid "SecurityDashboard|The security dashboard displays the latest security findings for projects you wish to monitor. Select \"Edit dashboard\" to add and remove projects."
+msgid "SecurityReports|There was an error creating the issue."
msgstr ""
-msgid "SecurityDashboard|The security dashboard displays the latest security report. Use it to find and fix vulnerabilities."
+msgid "SecurityReports|There was an error creating the merge request."
msgstr ""
-msgid "SecurityDashboard|There was an error while generating the report."
+msgid "SecurityReports|There was an error deleting the comment."
msgstr ""
-msgid "SecurityDashboard|Unable to add %{invalidProjects}"
-msgstr "Se ha producido un error al añadir %{invalidProjects}"
+msgid "SecurityReports|There was an error dismissing the vulnerabilities."
+msgstr ""
+
+msgid "SecurityReports|There was an error dismissing the vulnerability."
+msgstr ""
+
+msgid "SecurityReports|There was an error reverting the dismissal."
+msgstr ""
+
+msgid "SecurityReports|There was an error reverting this dismissal."
+msgstr ""
+
+msgid "SecurityReports|There was an error while generating the report."
+msgstr ""
+
+msgid "SecurityReports|Unable to add %{invalidProjects}"
+msgstr ""
+
+msgid "SecurityReports|Undo dismiss"
+msgstr ""
+
+msgid "SecurityReports|While it's rare to have no vulnerabilities for your group, it can happen. In any event, we ask that you double check your settings to make sure you've set up your dashboard correctly."
+msgstr ""
+
+msgid "SecurityReports|While it's rare to have no vulnerabilities for your pipeline, it can happen. In any event, we ask that you double check your settings to make sure all security scanning jobs have passed successfully."
+msgstr ""
+
+msgid "SecurityReports|While it's rare to have no vulnerabilities for your project, it can happen. In any event, we ask that you double check your settings to make sure you've set up your dashboard correctly."
+msgstr ""
+
+msgid "SecurityReports|While it's rare to have no vulnerabilities, it can happen. In any event, we ask that you please double check your settings to make sure you've set up your dashboard correctly."
+msgstr ""
+
+msgid "SecurityReports|Won't fix / Accept risk"
+msgstr ""
+
+msgid "SecurityReports|You do not have sufficient permissions to access this report"
+msgstr ""
+
+msgid "SecurityReports|You must sign in as an authorized user to see this report"
+msgstr ""
+
+msgid "SecurityReports|[No reason]"
+msgstr ""
msgid "See GitLab's %{password_policy_guidelines}"
msgstr "Vea la %{password_policy_guidelines} de GitLab"
@@ -18065,6 +18489,9 @@ msgstr "Ver métricas"
msgid "See the affected projects in the GitLab admin panel"
msgstr "Ver los proyectos afectados en el panel de administración de GitLab"
+msgid "See what's new at GitLab"
+msgstr ""
+
msgid "Select"
msgstr "Seleccionar"
@@ -18470,9 +18897,6 @@ msgstr "Configurar CI/CD"
msgid "Set up Jira Integration"
msgstr ""
-msgid "Set up Jira Integration illustration"
-msgstr ""
-
msgid "Set up a %{type} Runner automatically"
msgstr "Configurar un %{type} ejecutor automáticamente"
@@ -18557,6 +18981,9 @@ msgstr ""
msgid "Severity: %{severity}"
msgstr "Gravedad: %{severity}"
+msgid "Shards (%{shards})"
+msgstr ""
+
msgid "Shards to synchronize"
msgstr ""
@@ -18871,6 +19298,9 @@ msgstr ""
msgid "Snowplow"
msgstr "Snowplow"
+msgid "Some child epics may be hidden due to applied filters"
+msgstr ""
+
msgid "Some email servers do not support overriding the email sender name. Enable this option to include the name of the author of the issue, merge request or comment in the email body instead."
msgstr "Algunos servidores de correo electrónico no permiten sobreescribir el nombre del remitente del correo electrónico. Habilite esta opción para incluir el nombre del autor de la incidencia, del merge request o del comentario en el cuerpo del correo electrónico."
@@ -19012,6 +19442,9 @@ msgstr ""
msgid "Something went wrong while updating your list settings"
msgstr ""
+msgid "Something went wrong with your automatic subscription renewal"
+msgstr ""
+
msgid "Something went wrong, unable to add %{project} to dashboard"
msgstr "Algo salió mal, no se puede agregar %{project} al panel de control"
@@ -19297,6 +19730,9 @@ msgstr "Datos de la etapa actualizados"
msgid "Stage removed"
msgstr "Etapa eliminada"
+msgid "Standard"
+msgstr ""
+
msgid "Star a label to make it a priority label. Order the prioritized labels to change their relative priority, by dragging."
msgstr "Resalte una etiqueta para convertirla en una etiqueta con prioridad. Ordene las etiquetas priorizadas para cambiar su prioridad relativa, arrastrando."
@@ -19420,6 +19856,9 @@ msgstr "Indique su mensaje para activar"
msgid "Static Application Security Testing (SAST)"
msgstr ""
+msgid "StaticSiteEditor|An error occurred while submitting your changes."
+msgstr ""
+
msgid "StaticSiteEditor|Branch could not be created."
msgstr ""
@@ -19429,18 +19868,30 @@ msgstr ""
msgid "StaticSiteEditor|Could not create merge request."
msgstr ""
+msgid "StaticSiteEditor|Incompatible file content"
+msgstr ""
+
msgid "StaticSiteEditor|Return to site"
msgstr ""
+msgid "StaticSiteEditor|Static site editor"
+msgstr ""
+
msgid "StaticSiteEditor|Success!"
msgstr ""
msgid "StaticSiteEditor|Summary of changes"
msgstr ""
+msgid "StaticSiteEditor|The Static Site Editor is currently configured to only edit Markdown content on pages generated from Middleman. Visit the documentation to learn more about configuring your site to use the Static Site Editor."
+msgstr ""
+
msgid "StaticSiteEditor|Update %{sourcePath} file"
msgstr ""
+msgid "StaticSiteEditor|View documentation"
+msgstr ""
+
msgid "StaticSiteEditor|View merge request"
msgstr ""
@@ -19498,6 +19949,12 @@ msgstr ""
msgid "StatusPage|Status page"
msgstr ""
+msgid "StatusPage|Status page URL"
+msgstr ""
+
+msgid "StatusPage|Status page frontend documentation"
+msgstr ""
+
msgid "StatusPage|To publish incidents to an external status page, GitLab will store a JSON file in your Amazon S3 account in a location accessible to your external status page service. Make sure to also set up %{docsLink}"
msgstr ""
@@ -19846,6 +20303,12 @@ msgstr "Sincronizar información"
msgid "Synced"
msgstr ""
+msgid "Synchronization disabled"
+msgstr ""
+
+msgid "Synchronization of container repositories is disabled."
+msgstr ""
+
msgid "System"
msgstr "Sistema"
@@ -19999,6 +20462,9 @@ msgstr "Dominio del equipo"
msgid "Telephone number"
msgstr "Número de teléfono"
+msgid "Telephone number (Optional)"
+msgstr ""
+
msgid "Template"
msgstr "Plantilla"
@@ -20305,6 +20771,9 @@ msgstr "La licencia se ha eliminado. GitLab ya no tiene una licencia válida."
msgid "The license was successfully uploaded and is now active. You can see the details below."
msgstr "La licencia se ha subido correctamente y está activada. Puede ver los detalles de a continuación."
+msgid "The license was successfully uploaded and will be active from %{starts_at}. You can see the details below."
+msgstr ""
+
msgid "The maximum file size allowed is %{size}."
msgstr "El tamaño máximo de archivo permitido es de %{size}."
@@ -20626,6 +21095,9 @@ msgstr "Se ha producido un error al recopilar los datos del gráfico"
msgid "There was an error getting the epic participants."
msgstr ""
+msgid "There was an error importing the Jira project."
+msgstr ""
+
msgid "There was an error loading users activity calendar."
msgstr "Se ha producido un error al cargar el calendario de actividades de los usuarios."
@@ -20641,7 +21113,7 @@ msgstr "Se ha producido un error al restablecer los minutos de ejecución de los
msgid "There was an error resetting user pipeline minutes."
msgstr "Se ha producido un error al restablecer los minutos de ejecución de los pipelines del usuario."
-msgid "There was an error saving this Geo Node"
+msgid "There was an error saving this Geo Node."
msgstr ""
msgid "There was an error saving your changes."
@@ -20704,6 +21176,9 @@ msgstr "Estas variables están configuradas en la configuración del grupo princ
msgid "They can be managed using the %{link}."
msgstr "Se pueden administrar usando el %{link}."
+msgid "Third Party Advisory Link"
+msgstr ""
+
msgid "Third party offers"
msgstr "Ofertas de terceros"
@@ -20740,6 +21215,9 @@ msgstr "Esta aplicación fue creada por %{link_to_owner}."
msgid "This application will be able to:"
msgstr "Está aplicación podrá:"
+msgid "This attachment has been truncated to avoid exceeding the maximum allowed attachment size of 15MB. %{written_count} of %{issues_count} issues have been included. Consider re-exporting with a narrower selection of issues."
+msgstr ""
+
msgid "This block is self-referential"
msgstr "Este bloque es auto-referencial"
@@ -20881,6 +21359,12 @@ msgstr ""
msgid "This issue is confidential"
msgstr "Esta incidencia es confidencial"
+msgid "This issue is currently blocked by the following issues: %{issues}."
+msgstr ""
+
+msgid "This issue is in a child epic of the filtered epic"
+msgstr ""
+
msgid "This issue is locked."
msgstr "Esta incidencia está bloqueada."
@@ -21079,7 +21563,10 @@ msgstr ""
msgid "This user will be the author of all events in the activity feed that are the result of an update, like new branches being created or new commits being pushed to existing branches. Upon creation or when reassigning you can only assign yourself to be the mirror user."
msgstr ""
-msgid "This variable can not be masked"
+msgid "This variable can not be masked."
+msgstr ""
+
+msgid "This variable does not match the expected pattern."
msgstr ""
msgid "This will help us personalize your onboarding experience."
@@ -21109,10 +21596,10 @@ msgstr ""
msgid "ThreatMonitoring|Application firewall not detected"
msgstr ""
-msgid "ThreatMonitoring|Container Network Policy"
+msgid "ThreatMonitoring|Container Network Policies are not installed or have been disabled. To view this data, ensure your Network Policies are installed and enabled for your cluster."
msgstr ""
-msgid "ThreatMonitoring|Container NetworkPolicies are not installed or has been disabled. To view this data, ensure you NetworkPolicies are installed and enabled for your cluster."
+msgid "ThreatMonitoring|Container Network Policy"
msgstr ""
msgid "ThreatMonitoring|Container NetworkPolicies not detected"
@@ -21145,7 +21632,7 @@ msgstr ""
msgid "ThreatMonitoring|Something went wrong, unable to fetch statistics"
msgstr ""
-msgid "ThreatMonitoring|The firewall is not installed or has been disabled. To view this data, ensure you firewall is installed and enabled for your cluster."
+msgid "ThreatMonitoring|The firewall is not installed or has been disabled. To view this data, ensure the web application firewall is installed and enabled for your cluster."
msgstr ""
msgid "ThreatMonitoring|The graph below is an overview of traffic coming to your application as tracked by the Web Application Firewall (WAF). View the docs for instructions on how to access the WAF logs to see what type of malicious traffic is trying to access your app. The docs link is also accessible by clicking the \"?\" icon next to the title below."
@@ -21214,6 +21701,9 @@ msgstr ""
msgid "Time in seconds GitLab will wait for a response from the external service. When the service does not respond in time, access will be denied."
msgstr "Tiempo en segundos que GitLab esperará hasta obtener una respuesta de un servicio externo. Cuando el servicio no responda a tiempo, se denegará el acceso."
+msgid "Time of import: %{importTime}"
+msgstr ""
+
msgid "Time remaining"
msgstr "Tiempo restante"
@@ -21398,8 +21888,8 @@ msgstr "Título"
msgid "Title:"
msgstr "Título:"
-msgid "Titles and Filenames"
-msgstr "Títulos y nombres de archivos"
+msgid "Titles and Descriptions"
+msgstr ""
msgid "To"
msgstr "Para"
@@ -21605,8 +22095,8 @@ msgstr "Demasiados espacios de nombres habilitados. Necesitará administrarlos a
msgid "Too many projects enabled. You will need to manage them via the console or the API."
msgstr "Demasiados proyectos habilitados. Necesitará administrarlos a través de la consola o mediante el API."
-msgid "Topics"
-msgstr "Temas"
+msgid "Topics (optional)"
+msgstr ""
msgid "Total"
msgstr "Total"
@@ -21767,6 +22257,9 @@ msgstr "¿Desea intentarlo de nuevo?"
msgid "Try all GitLab has to offer for 30 days."
msgstr "Pruebe todo lo que GitLab tiene para ofrecer durante 30 días."
+msgid "Try changing or removing filters."
+msgstr ""
+
msgid "Try to fork again"
msgstr "Intentar realizar el fork de nuevo"
@@ -21860,6 +22353,9 @@ msgstr "No se ha podido conectar con el servidor de Prometheus"
msgid "Unable to connect to server: %{error}"
msgstr "Imposible conectar con el servidor: %{error}"
+msgid "Unable to connect to the Jira instance. Please check your Jira integration configuration."
+msgstr ""
+
msgid "Unable to convert Kubernetes logs encoding to UTF-8"
msgstr ""
@@ -22803,6 +23299,9 @@ msgstr ""
msgid "View issue"
msgstr "Ver incidencia"
+msgid "View issues"
+msgstr ""
+
msgid "View it on GitLab"
msgstr "Ver en GitLab"
@@ -22980,6 +23479,15 @@ msgstr ""
msgid "VulnerabilityManagement|Resolved %{timeago} by %{user}"
msgstr ""
+msgid "VulnerabilityManagement|Something went wrong while trying to delete the comment. Please try again later."
+msgstr ""
+
+msgid "VulnerabilityManagement|Something went wrong while trying to retrieve the vulnerability history. Please try again later."
+msgstr ""
+
+msgid "VulnerabilityManagement|Something went wrong while trying to save the comment. Please try again later."
+msgstr ""
+
msgid "VulnerabilityManagement|Something went wrong, could not create an issue."
msgstr ""
@@ -23010,6 +23518,9 @@ msgstr ""
msgid "VulnerabilityStatusTypes|Resolved"
msgstr ""
+msgid "Vulnerability|%{scannerName} (version %{scannerVersion})"
+msgstr ""
+
msgid "Vulnerability|Class"
msgstr "Clase"
@@ -23043,8 +23554,11 @@ msgstr "Espacio de nombres"
msgid "Vulnerability|Project"
msgstr "Proyecto"
-msgid "Vulnerability|Report Type"
-msgstr "Tipo de informe"
+msgid "Vulnerability|Scanner Provider"
+msgstr ""
+
+msgid "Vulnerability|Scanner Type"
+msgstr ""
msgid "Vulnerability|Severity"
msgstr "Severidad"
@@ -23097,9 +23611,15 @@ msgstr "Hemos obtenido respuesta desde su dispositivo U2F. Ha sido autenticado c
msgid "We sent you an email with reset password instructions"
msgstr "Le enviamos un correo electrónico con las instrucciones para restablecer la contraseña"
+msgid "We tried to automatically renew your %{strong}%{plan_name}%{strong_close} subscription for %{strong}%{namespace_name}%{strong_close} on %{expires_on} but something went wrong so your subscription was downgraded to the free plan. Don't worry, your data is safe. We suggest you check your payment method and get in touch with our support team (%{support_link}). They'll gladly help with your subscription renewal."
+msgstr ""
+
msgid "We want to be sure it is you, please confirm you are not a robot."
msgstr "Queremos asegurarnos de que sea usted, por favor, ayudenos a confirmar que no es un robot."
+msgid "We will automatically renew your %{strong}%{plan_name}%{strong_close} subscription for %{strong}%{namespace_name}%{strong_close} on %{strong}%{expires_on}%{strong_close}. There's nothing that you need to do, we'll let you know when the renewal is complete. Need more seats, a higher plan or just want to review your payment method?"
+msgstr ""
+
msgid "We've found no vulnerabilities"
msgstr "No hemos encontrado vulnerabilidades"
@@ -23204,15 +23724,6 @@ msgstr "Al utilizar los protocolos <code>http://</code> o <code>https://</code>,
msgid "When:"
msgstr "Cuando:"
-msgid "While it's rare to have no vulnerabilities for your group, it can happen. In any event, we ask that you double check your settings to make sure you've set up your dashboard correctly."
-msgstr ""
-
-msgid "While it's rare to have no vulnerabilities for your pipeline, it can happen. In any event, we ask that you double check your settings to make sure all security scanning jobs have passed successfully."
-msgstr ""
-
-msgid "While it's rare to have no vulnerabilities for your project, it can happen. In any event, we ask that you double check your settings to make sure you've set up your dashboard correctly."
-msgstr ""
-
msgid "While it's rare to have no vulnerabilities, it can happen. In any event, we ask that you please double check your settings to make sure you've set up your dashboard correctly."
msgstr ""
@@ -23243,9 +23754,6 @@ msgstr ""
msgid "Wiki"
msgstr "Wiki"
-msgid "Wiki pages"
-msgstr "Páginas del Wiki"
-
msgid "Wiki was successfully updated."
msgstr "El wiki se actualizó correctamente."
@@ -23429,6 +23937,9 @@ msgstr "Sí o no"
msgid "Yes, add it"
msgstr "Si, hazlo"
+msgid "Yes, close issue"
+msgstr ""
+
msgid "Yes, let me map Google Code users to full names or GitLab users."
msgstr "Sí, déjenme asignar usuarios de Google Code a nombres completos o usuarios de GitLab."
@@ -23624,6 +24135,9 @@ msgstr "No puede escribir en esta instancia de sólo lectura de GitLab."
msgid "You could not create a new trigger."
msgstr "No es posible crear un nuevo disparador."
+msgid "You didn't renew your %{strong}%{plan_name}%{strong_close} subscription for %{strong}%{namespace_name}%{strong_close} so it was downgraded to the free plan."
+msgstr ""
+
msgid "You didn't renew your %{strong}%{plan_name}%{strong_close} subscription so it was downgraded to the GitLab Core Plan."
msgstr ""
@@ -23690,6 +24204,9 @@ msgstr "Se ha cancelado la suscripción a este hilo."
msgid "You have declined the invitation to join %{label}."
msgstr "Has rechazado la invitación para unirse a %{label}."
+msgid "You have imported from this project %{numberOfPreviousImportsForProject} times before. Each new import will create duplicate issues."
+msgstr ""
+
msgid "You have no permissions"
msgstr "No tiene permisos"
@@ -23771,6 +24288,9 @@ msgstr "Es necesario subir un archivo de exportación de proyecto de GitLab (en
msgid "You need to upload a Google Takeout archive."
msgstr "Necesita subir un archivo de Google Takeout."
+msgid "You reached %{usage_in_percent} of %{namespace_name}'s capacity (%{used_storage} of %{storage_limit})"
+msgstr ""
+
msgid "You tried to fork %{link_to_the_project} but it failed for the following reason:"
msgstr "Trató de hacer un fork de %{link_to_the_project} pero falló por la siguiente razón:"
@@ -23855,9 +24375,21 @@ msgstr "Ya ha habilitado la autenticación de dos pasos utilizando una contraseÃ
msgid "YouTube"
msgstr "YouTube"
+msgid "Your %{strong}%{plan_name}%{strong_close} subscription for %{strong}%{namespace_name}%{strong_close} will expire on %{strong}%{expires_on}%{strong_close}. After that, you will not to be able to create issues or merge requests as well as many other features."
+msgstr ""
+
msgid "Your %{strong}%{plan_name}%{strong_close} subscription will expire on %{strong}%{expires_on}%{strong_close}. After that, you will not to be able to create issues or merge requests as well as many other features."
msgstr ""
+msgid "Your CSV export has started. It will be emailed to %{email} when complete."
+msgstr ""
+
+msgid "Your CSV export of %{issues_count} from project %{project_link} has been added to this email as an attachment."
+msgstr ""
+
+msgid "Your CSV export of %{written_count} from project %{project_name} (%{project_url}) has been added to this email as an attachment."
+msgstr ""
+
msgid "Your Commit Email will be used for web based operations, such as edits and merges."
msgstr "Este correo electrónico se utilizará para operaciones basadas en la web, como por ejemplo, realizar ediciones y merges."
@@ -23954,6 +24486,9 @@ msgstr "Se ha producido un error al enviar su comentario. Por favor, compruebe s
msgid "Your comment could not be updated! Please check your network connection and try again."
msgstr "Se ha producido un error al editar su comentario. Por favor, compruebe su conexión de red y vuelva a intentarlo."
+msgid "Your comment will be discarded."
+msgstr ""
+
msgid "Your custom stage '%{title}' was created"
msgstr ""
@@ -23993,6 +24528,9 @@ msgstr "Su nuevo token SCIM"
msgid "Your new personal access token has been created."
msgstr "Se ha creado su nuevo token de acceso personal."
+msgid "Your new project access token has been created."
+msgstr ""
+
msgid "Your password isn't required to view this page. If a password or any other personal details are requested, please contact your administrator to report abuse."
msgstr "Su contraseña no es necesaria para ver esta página. Si se le solicita una contraseña o cualquier otro dato personal, póngase en contacto con su administrador para denunciar el abuso."
@@ -24011,12 +24549,18 @@ msgstr "Tus proyectos"
msgid "Your request for access has been queued for review."
msgstr "Su solicitud de acceso ha sido puesta en cola para su revisión."
+msgid "Your search didn't match any commits."
+msgstr ""
+
msgid "Your subscription expired!"
msgstr ""
msgid "Your subscription has been downgraded"
msgstr ""
+msgid "Your subscription will automatically renew in %{remaining_days}"
+msgstr ""
+
msgid "Your subscription will expire in %{remaining_days}"
msgstr ""
@@ -24055,8 +24599,8 @@ msgstr "hace"
msgid "allowed to fail"
msgstr "permitido fallar"
-msgid "already being used for another group or project milestone."
-msgstr "ya se está utilizando para otro grupo o hito del proyecto."
+msgid "already being used for another group or project %{timebox_name}."
+msgstr ""
msgid "already has a \"created\" issue link"
msgstr ""
@@ -24133,6 +24677,9 @@ msgstr "%{linkStartTag}Aprenda más sobre el análisis de dependencias %{linkEnd
msgid "ciReport|%{linkStartTag}Learn more about SAST %{linkEndTag}"
msgstr "%{linkStartTag}Aprenda más sobre SAST %{linkEndTag}"
+msgid "ciReport|%{linkStartTag}Learn more about Secret Scanning %{linkEndTag}"
+msgstr ""
+
msgid "ciReport|%{linkStartTag}Learn more about codequality reports %{linkEndTag}"
msgstr ""
@@ -24296,6 +24843,12 @@ msgstr "Resolver con un merge request"
msgid "ciReport|SAST"
msgstr "SAST"
+msgid "ciReport|Secret scanning"
+msgstr ""
+
+msgid "ciReport|Secret scanning detects secrets and credentials vulnerabilities in your source code."
+msgstr ""
+
msgid "ciReport|Security scanning"
msgstr "Análisis de seguridad"
@@ -24493,6 +25046,9 @@ msgstr ""
msgid "group"
msgstr "grupo"
+msgid "groups"
+msgstr ""
+
msgid "has already been linked to another vulnerability"
msgstr "ya ha sido vinculado a otra vulnerabilidad"
@@ -24656,9 +25212,6 @@ msgstr[1] "merge requests"
msgid "merged %{time_ago}"
msgstr ""
-msgid "milestone should belong either to a project or a group."
-msgstr "el hito debe pertenecer a un proyecto o un grupo."
-
msgid "missing"
msgstr "falta"
@@ -24995,6 +25548,9 @@ msgstr "nueva solicitud de fusión"
msgid "no contributions"
msgstr "sin contribuciones"
+msgid "no expiration"
+msgstr ""
+
msgid "no one can merge"
msgstr "nadie puede hacer merge"
@@ -25047,6 +25603,9 @@ msgstr "comentario pendiente"
msgid "pending removal"
msgstr "pendiente de eliminación"
+msgid "per day"
+msgstr ""
+
msgid "pipeline"
msgstr "pipeline"
@@ -25075,6 +25634,12 @@ msgstr[1] "proyectos"
msgid "project avatar"
msgstr "avatar del proyecto"
+msgid "projects"
+msgstr ""
+
+msgid "push to your repository, create pipelines, create issues or add comments. To reduce storage capacity, delete unused repositories, artifacts, wikis, issues, and pipelines."
+msgstr ""
+
msgid "quick actions"
msgstr "acciones rápidas"
@@ -25221,11 +25786,6 @@ msgstr "la siguiente incidencia(s)"
msgid "this document"
msgstr "este documento"
-msgid "thread resolved"
-msgid_plural "threads resolved"
-msgstr[0] "hilo resuelto"
-msgstr[1] "hilos resueltos"
-
msgid "to help your contributors communicate effectively!"
msgstr "¡Para ayudar a sus colaboradores a comunicarse de manera efectiva!"
@@ -25280,6 +25840,9 @@ msgstr "ver en GitLab"
msgid "view the blob"
msgstr "ver el blob"
+msgid "vulnerability|Add a comment"
+msgstr ""
+
msgid "vulnerability|Add a comment or reason for dismissal"
msgstr "Añadir un comentario o motivo para el descarte"
diff --git a/locale/et_EE/gitlab.po b/locale/et_EE/gitlab.po
index 220c08c9c3f..d1cb6cb1c03 100644
--- a/locale/et_EE/gitlab.po
+++ b/locale/et_EE/gitlab.po
@@ -12,7 +12,7 @@ msgstr ""
"X-Crowdin-Project: gitlab-ee\n"
"X-Crowdin-Language: et\n"
"X-Crowdin-File: /master/locale/gitlab.pot\n"
-"PO-Revision-Date: 2020-04-15 00:31\n"
+"PO-Revision-Date: 2020-05-05 21:32\n"
msgid " %{start} to %{end}"
msgstr ""
@@ -212,6 +212,11 @@ msgid_plural "%d tags"
msgstr[0] ""
msgstr[1] ""
+msgid "%d unresolved thread"
+msgid_plural "%d unresolved threads"
+msgstr[0] ""
+msgstr[1] ""
+
msgid "%d vulnerability dismissed"
msgid_plural "%d vulnerabilities dismissed"
msgstr[0] ""
@@ -371,6 +376,9 @@ msgstr ""
msgid "%{mrText}, this issue will be closed automatically."
msgstr ""
+msgid "%{namespace_name} is now read-only. You cannot: %{base_message}"
+msgstr ""
+
msgid "%{name} contained %{resultsString}"
msgstr ""
@@ -383,6 +391,11 @@ msgstr ""
msgid "%{name}'s avatar"
msgstr ""
+msgid "%{no_of_days} day"
+msgid_plural "%{no_of_days} days"
+msgstr[0] ""
+msgstr[1] ""
+
msgid "%{number_commits_behind} commits behind %{default_branch}, %{number_commits_ahead} commits ahead"
msgstr ""
@@ -506,6 +519,9 @@ msgstr[1] ""
msgid "%{text} is available"
msgstr ""
+msgid "%{timebox_name} should belong either to a project or a group."
+msgstr ""
+
msgid "%{title} %{operator} %{threshold}"
msgstr ""
@@ -781,12 +797,18 @@ msgstr ""
msgid "<code>\"johnsmith@example.com\": \"johnsmith@example.com\"</code> will add \"By <a href=\"#\">johnsmith@example.com</a>\" to all issues and comments originally created by johnsmith@example.com. By default, the email address or username is masked to ensure the user's privacy. Use this option if you want to show the full email address."
msgstr ""
+msgid "<namespace / project>"
+msgstr ""
+
msgid "<no name set>"
msgstr ""
msgid "<no scopes selected>"
msgstr ""
+msgid "<project name>"
+msgstr ""
+
msgid "<strong>%{group_name}</strong> group members"
msgstr ""
@@ -820,6 +842,9 @@ msgstr ""
msgid "A basic page and serverless function that uses AWS Lambda, AWS API Gateway, and GitLab Pages"
msgstr ""
+msgid "A complete DevOps platform"
+msgstr ""
+
msgid "A default branch cannot be chosen for an empty project."
msgstr ""
@@ -1036,6 +1061,12 @@ msgstr ""
msgid "AccessTokens|reset it"
msgstr ""
+msgid "AccessibilityReport|Accessibility report artifact not found"
+msgstr ""
+
+msgid "AccessibilityReport|Failed to retrieve accessibility report"
+msgstr ""
+
msgid "AccessibilityReport|Learn More"
msgstr ""
@@ -1045,6 +1076,9 @@ msgstr ""
msgid "AccessibilityReport|New"
msgstr ""
+msgid "AccessibilityReport|The accessibility scanning found an error of the following type: %{code}"
+msgstr ""
+
msgid "Account"
msgstr ""
@@ -1287,6 +1321,9 @@ msgstr ""
msgid "Added at"
msgstr ""
+msgid "Added for this merge request"
+msgstr ""
+
msgid "Added in this version"
msgstr ""
@@ -1658,6 +1695,72 @@ msgid_plural "Alerts"
msgstr[0] ""
msgstr[1] ""
+msgid "Alert Details"
+msgstr ""
+
+msgid "AlertManagement|Acknowledged"
+msgstr ""
+
+msgid "AlertManagement|Alert"
+msgstr ""
+
+msgid "AlertManagement|Authorize external service"
+msgstr ""
+
+msgid "AlertManagement|Display alerts from all your monitoring tools directly within GitLab. Streamline the investigation of your alerts and the escalation of alerts to incidents."
+msgstr ""
+
+msgid "AlertManagement|End time"
+msgstr ""
+
+msgid "AlertManagement|End time:"
+msgstr ""
+
+msgid "AlertManagement|Events"
+msgstr ""
+
+msgid "AlertManagement|Events:"
+msgstr ""
+
+msgid "AlertManagement|Full Alert Details"
+msgstr ""
+
+msgid "AlertManagement|More information"
+msgstr ""
+
+msgid "AlertManagement|No alerts available to display. If you think you're seeing this message in error, refresh the page."
+msgstr ""
+
+msgid "AlertManagement|No alerts to display."
+msgstr ""
+
+msgid "AlertManagement|Overview"
+msgstr ""
+
+msgid "AlertManagement|Resolved"
+msgstr ""
+
+msgid "AlertManagement|Severity"
+msgstr ""
+
+msgid "AlertManagement|Start time"
+msgstr ""
+
+msgid "AlertManagement|Start time:"
+msgstr ""
+
+msgid "AlertManagement|Status"
+msgstr ""
+
+msgid "AlertManagement|Surface alerts in GitLab"
+msgstr ""
+
+msgid "AlertManagement|There was an error displaying the alerts. Confirm your endpoint's configuration details to ensure alerts appear."
+msgstr ""
+
+msgid "AlertManagement|Triggered"
+msgstr ""
+
msgid "AlertService|%{linkStart}Learn more%{linkEnd} about configuring this endpoint to receive alerts."
msgstr ""
@@ -1727,6 +1830,9 @@ msgstr ""
msgid "All security scans are enabled because %{linkStart}Auto DevOps%{linkEnd} is enabled on this project"
msgstr ""
+msgid "All threads resolved"
+msgstr ""
+
msgid "All users"
msgstr ""
@@ -1745,6 +1851,9 @@ msgstr ""
msgid "Allow only the selected protocols to be used for Git access."
msgstr ""
+msgid "Allow owners to manage default branch protection per group"
+msgstr ""
+
msgid "Allow owners to manually add users outside of LDAP"
msgstr ""
@@ -1853,6 +1962,9 @@ msgstr ""
msgid "An error occurred fetching the dropdown data."
msgstr ""
+msgid "An error occurred fetching the project authors."
+msgstr ""
+
msgid "An error occurred previewing the blob"
msgstr ""
@@ -1934,6 +2046,9 @@ msgstr ""
msgid "An error occurred while fetching sidebar data"
msgstr ""
+msgid "An error occurred while fetching terraform reports."
+msgstr ""
+
msgid "An error occurred while fetching the Service Desk address."
msgstr ""
@@ -2171,6 +2286,9 @@ msgstr ""
msgid "Any"
msgstr ""
+msgid "Any Author"
+msgstr ""
+
msgid "Any Label"
msgstr ""
@@ -2386,10 +2504,10 @@ msgstr ""
msgid "Are you sure that you want to unarchive this project?"
msgstr ""
-msgid "Are you sure you want to cancel creating this comment?"
+msgid "Are you sure you want to cancel editing this comment?"
msgstr ""
-msgid "Are you sure you want to cancel editing this comment?"
+msgid "Are you sure you want to close this blocked issue?"
msgstr ""
msgid "Are you sure you want to delete %{name}?"
@@ -2419,6 +2537,9 @@ msgstr ""
msgid "Are you sure you want to deploy this environment?"
msgstr ""
+msgid "Are you sure you want to discard this comment?"
+msgstr ""
+
msgid "Are you sure you want to erase this build?"
msgstr ""
@@ -2595,6 +2716,9 @@ msgstr ""
msgid "At least one approval from a code owner is required to change files matching the respective CODEOWNER rules."
msgstr ""
+msgid "At least one logging option is required to be enabled"
+msgstr ""
+
msgid "At least one of group_id or project_id must be specified"
msgstr ""
@@ -2633,6 +2757,27 @@ msgstr ""
msgid "AuditEvents|Target"
msgstr ""
+msgid "AuditLogs|(removed)"
+msgstr ""
+
+msgid "AuditLogs|Action"
+msgstr ""
+
+msgid "AuditLogs|Author"
+msgstr ""
+
+msgid "AuditLogs|Date"
+msgstr ""
+
+msgid "AuditLogs|IP Address"
+msgstr ""
+
+msgid "AuditLogs|Object"
+msgstr ""
+
+msgid "AuditLogs|Target"
+msgstr ""
+
msgid "Aug"
msgstr ""
@@ -2915,9 +3060,6 @@ msgstr ""
msgid "BambooService|You must set up automatic revision labeling and a repository trigger in Bamboo."
msgstr ""
-msgid "Batch operations"
-msgstr ""
-
msgid "BatchComments|Delete all pending comments"
msgstr ""
@@ -3047,6 +3189,9 @@ msgstr ""
msgid "Boards"
msgstr ""
+msgid "Boards and Board Lists"
+msgstr ""
+
msgid "Boards|Collapse"
msgstr ""
@@ -3266,6 +3411,9 @@ msgstr ""
msgid "Buy GitLab Enterprise Edition"
msgstr ""
+msgid "Buy more Pipeline minutes"
+msgstr ""
+
msgid "By %{user_name}"
msgstr ""
@@ -3386,6 +3534,9 @@ msgstr ""
msgid "Can't scan the code?"
msgstr ""
+msgid "Can't update snippet: %{err}"
+msgstr ""
+
msgid "Canary"
msgstr ""
@@ -3554,6 +3705,9 @@ msgstr ""
msgid "Changing group path can have unintended side effects."
msgstr ""
+msgid "Charts"
+msgstr ""
+
msgid "Charts can't be displayed as the request for data has timed out. %{documentationLink}"
msgstr ""
@@ -3590,9 +3744,6 @@ msgstr ""
msgid "ChatMessage|and [%{count} more](%{pipeline_failed_jobs_url})"
msgstr ""
-msgid "ChatMessage|failed"
-msgstr ""
-
msgid "ChatMessage|has failed"
msgstr ""
@@ -3608,9 +3759,6 @@ msgstr ""
msgid "ChatMessage|in %{project_link}"
msgstr ""
-msgid "ChatMessage|passed"
-msgstr ""
-
msgid "Check again"
msgstr ""
@@ -4061,9 +4209,6 @@ msgstr ""
msgid "Click any <strong>project name</strong> in the project list below to navigate to the project milestone."
msgstr ""
-msgid "Click here"
-msgstr ""
-
msgid "Click the <strong>Download</strong> button and wait for downloading to complete."
msgstr ""
@@ -4451,6 +4596,12 @@ msgstr ""
msgid "ClusterIntegration|Fetching zones"
msgstr ""
+msgid "ClusterIntegration|Fluentd"
+msgstr ""
+
+msgid "ClusterIntegration|Fluentd is an open source data collector, which lets you unify the data collection and consumption for a better use and understanding of data. It requires at least one of the following logs to be successfully installed."
+msgstr ""
+
msgid "ClusterIntegration|GitLab Integration"
msgstr ""
@@ -4757,6 +4908,15 @@ msgstr ""
msgid "ClusterIntegration|Request to begin uninstalling failed"
msgstr ""
+msgid "ClusterIntegration|SIEM Hostname"
+msgstr ""
+
+msgid "ClusterIntegration|SIEM Port"
+msgstr ""
+
+msgid "ClusterIntegration|SIEM Protocol"
+msgstr ""
+
msgid "ClusterIntegration|Save changes"
msgstr ""
@@ -4853,6 +5013,12 @@ msgstr ""
msgid "ClusterIntegration|Select zone to choose machine type"
msgstr ""
+msgid "ClusterIntegration|Send Cilium Logs"
+msgstr ""
+
+msgid "ClusterIntegration|Send ModSecurity Logs"
+msgstr ""
+
msgid "ClusterIntegration|Service Token"
msgstr ""
@@ -5281,18 +5447,33 @@ msgstr ""
msgid "Compliance framework (optional)"
msgstr ""
+msgid "ComplianceFramework|GDPR"
+msgstr ""
+
msgid "ComplianceFramework|GDPR - General Data Protection Regulation"
msgstr ""
+msgid "ComplianceFramework|HIPAA"
+msgstr ""
+
msgid "ComplianceFramework|HIPAA - Health Insurance Portability and Accountability Act"
msgstr ""
+msgid "ComplianceFramework|PCI-DSS"
+msgstr ""
+
msgid "ComplianceFramework|PCI-DSS - Payment Card Industry-Data Security Standard"
msgstr ""
+msgid "ComplianceFramework|SOC 2"
+msgstr ""
+
msgid "ComplianceFramework|SOC 2 - Service Organization Control 2"
msgstr ""
+msgid "ComplianceFramework|SOX"
+msgstr ""
+
msgid "ComplianceFramework|SOX - Sarbanes-Oxley"
msgstr ""
@@ -5440,9 +5621,15 @@ msgstr ""
msgid "Container repositories sync capacity"
msgstr ""
+msgid "ContainerRegistry| Please visit the %{linkStart}administration settings%{linkEnd} to enable this feature."
+msgstr ""
+
msgid "ContainerRegistry|%{imageName} tags"
msgstr ""
+msgid "ContainerRegistry|%{title} was successfully scheduled for deletion"
+msgstr ""
+
msgid "ContainerRegistry|Automatically remove extra images that aren't designed to be kept."
msgstr ""
@@ -5455,6 +5642,9 @@ msgstr ""
msgid "ContainerRegistry|Container Registry"
msgstr ""
+msgid "ContainerRegistry|Container Registry tag expiration and retention policy is disabled"
+msgstr ""
+
msgid "ContainerRegistry|Copy build command"
msgstr ""
@@ -5464,18 +5654,12 @@ msgstr ""
msgid "ContainerRegistry|Copy push command"
msgstr ""
-msgid "ContainerRegistry|Currently, the Container Registry tag expiration feature is not available for projects created before GitLab version 12.8. For updates and more information, visit Issue %{linkStart}#196124%{linkEnd}"
-msgstr ""
-
msgid "ContainerRegistry|Docker connection error"
msgstr ""
msgid "ContainerRegistry|Docker tag expiration policy is %{toggleStatus}"
msgstr ""
-msgid "ContainerRegistry|Docker tags with names matching this regex pattern will expire:"
-msgstr ""
-
msgid "ContainerRegistry|Edit Settings"
msgstr ""
@@ -5497,9 +5681,6 @@ msgstr ""
msgid "ContainerRegistry|Image ID"
msgstr ""
-msgid "ContainerRegistry|Image deleted successfully"
-msgstr ""
-
msgid "ContainerRegistry|Keep and protect the images that matter most."
msgstr ""
@@ -5515,12 +5696,24 @@ msgstr ""
msgid "ContainerRegistry|Number of tags to retain:"
msgstr ""
+msgid "ContainerRegistry|Please contact your administrator."
+msgstr ""
+
msgid "ContainerRegistry|Push an image"
msgstr ""
msgid "ContainerRegistry|Quick Start"
msgstr ""
+msgid "ContainerRegistry|Regular expressions such as %{codeStart}.*-test%{codeEnd} or %{codeStart}dev-.*%{codeEnd} are supported"
+msgstr ""
+
+msgid "ContainerRegistry|Regular expressions such as %{codeStart}.*-test%{codeEnd} or %{codeStart}dev-.*%{codeEnd} are supported. To select all tags, use %{codeStart}.*%{codeEnd}"
+msgstr ""
+
+msgid "ContainerRegistry|Remember to run %{docLinkStart}garbage collection%{docLinkEnd} to remove the stale data from storage."
+msgstr ""
+
msgid "ContainerRegistry|Remove repository"
msgstr ""
@@ -5535,22 +5728,22 @@ msgstr[1] ""
msgid "ContainerRegistry|Retention policy has been Enabled"
msgstr ""
-msgid "ContainerRegistry|Something went wrong while deleting the image."
+msgid "ContainerRegistry|Something went wrong while fetching the expiration policy."
msgstr ""
-msgid "ContainerRegistry|Something went wrong while deleting the tag."
+msgid "ContainerRegistry|Something went wrong while fetching the repository list."
msgstr ""
-msgid "ContainerRegistry|Something went wrong while deleting the tags."
+msgid "ContainerRegistry|Something went wrong while fetching the tags list."
msgstr ""
-msgid "ContainerRegistry|Something went wrong while fetching the expiration policy."
+msgid "ContainerRegistry|Something went wrong while marking the tag for deletion."
msgstr ""
-msgid "ContainerRegistry|Something went wrong while fetching the packages list."
+msgid "ContainerRegistry|Something went wrong while marking the tags for deletion."
msgstr ""
-msgid "ContainerRegistry|Something went wrong while fetching the tags list."
+msgid "ContainerRegistry|Something went wrong while scheduling %{title} for deletion. Please try again."
msgstr ""
msgid "ContainerRegistry|Something went wrong while updating the expiration policy."
@@ -5559,16 +5752,25 @@ msgstr ""
msgid "ContainerRegistry|Tag"
msgstr ""
-msgid "ContainerRegistry|Tag deleted successfully"
-msgstr ""
-
msgid "ContainerRegistry|Tag expiration policy"
msgstr ""
msgid "ContainerRegistry|Tag expiration policy is designed to:"
msgstr ""
-msgid "ContainerRegistry|Tags deleted successfully"
+msgid "ContainerRegistry|Tag successfully marked for deletion."
+msgstr ""
+
+msgid "ContainerRegistry|Tags successfully marked for deletion."
+msgstr ""
+
+msgid "ContainerRegistry|Tags with names matching this regex pattern will %{italicStart}be preserved:%{italicEnd}"
+msgstr ""
+
+msgid "ContainerRegistry|Tags with names matching this regex pattern will %{italicStart}expire:%{italicEnd}"
+msgstr ""
+
+msgid "ContainerRegistry|The Container Registry tag expiration and retention policies for this project have not been enabled."
msgstr ""
msgid "ContainerRegistry|The last tag related to this image was recently removed. This empty image and any associated data will be automatically removed as part of the regular Garbage Collection process. If you have any questions, contact your administrator."
@@ -5589,16 +5791,16 @@ msgstr ""
msgid "ContainerRegistry|There are no container images stored for this project"
msgstr ""
-msgid "ContainerRegistry|This Registry contains deleted image tag data. Remember to run %{docLinkStart}garbage collection%{docLinkEnd} to remove the stale data from storage."
+msgid "ContainerRegistry|There was an error during the deletion of this image repository, please try again."
msgstr ""
msgid "ContainerRegistry|This image has no active tags"
msgstr ""
-msgid "ContainerRegistry|We are having trouble connecting to Docker, which could be due to an issue with your project name or path. %{docLinkStart}More Information%{docLinkEnd}"
+msgid "ContainerRegistry|This image repository is scheduled for deletion"
msgstr ""
-msgid "ContainerRegistry|Wildcards such as %{codeStart}.*-stable%{codeEnd} or %{codeStart}production/.*%{codeEnd} are supported. To select all tags, use %{codeStart}.*%{codeEnd}"
+msgid "ContainerRegistry|We are having trouble connecting to Docker, which could be due to an issue with your project name or path. %{docLinkStart}More Information%{docLinkEnd}"
msgstr ""
msgid "ContainerRegistry|With the Container Registry, every project can have its own space to store its Docker images. %{docLinkStart}More Information%{docLinkEnd}"
@@ -5853,7 +6055,7 @@ msgstr ""
msgid "Could not delete chat nickname %{chat_name}."
msgstr ""
-msgid "Could not find design"
+msgid "Could not find design."
msgstr ""
msgid "Could not remove the trigger."
@@ -5868,6 +6070,9 @@ msgstr ""
msgid "Could not revoke personal access token %{personal_access_token_name}."
msgstr ""
+msgid "Could not revoke project access token %{project_access_token_name}."
+msgstr ""
+
msgid "Could not save group ID"
msgstr ""
@@ -5925,6 +6130,9 @@ msgstr ""
msgid "Create a new branch"
msgstr ""
+msgid "Create a new deploy key for this project"
+msgstr ""
+
msgid "Create a new file as there are no files yet. Afterwards, you'll be able to commit your changes."
msgstr ""
@@ -6030,6 +6238,9 @@ msgstr ""
msgid "Create requirement"
msgstr ""
+msgid "Create snippet"
+msgstr ""
+
msgid "Create wildcard: %{searchTerm}"
msgstr ""
@@ -6051,6 +6262,9 @@ msgstr ""
msgid "Created"
msgstr ""
+msgid "Created %{timestamp}"
+msgstr ""
+
msgid "Created At"
msgstr ""
@@ -6407,6 +6621,9 @@ msgstr ""
msgid "CycleAnalytics|stage dropdown"
msgstr ""
+msgid "DAG"
+msgstr ""
+
msgid "DNS"
msgstr ""
@@ -6773,6 +6990,9 @@ msgstr ""
msgid "Deploy key was successfully updated."
msgstr ""
+msgid "Deploy keys allow read-only or read-write (if enabled) access to your repository. Deploy keys can be used for CI, staging or production servers. You can create a deploy key or add an existing one."
+msgstr ""
+
msgid "Deploy progress not found. To see pods, ensure your environment matches %{linkStart}deploy board criteria%{linkEnd}."
msgstr ""
@@ -6926,6 +7146,9 @@ msgstr ""
msgid "Deploying to"
msgstr ""
+msgid "Deployment Frequency"
+msgstr ""
+
msgid "Deployment|API"
msgstr ""
@@ -7444,9 +7667,6 @@ msgstr ""
msgid "Edit environment"
msgstr ""
-msgid "Edit epic description"
-msgstr ""
-
msgid "Edit file"
msgstr ""
@@ -8068,9 +8288,6 @@ msgstr ""
msgid "Epics"
msgstr ""
-msgid "Epics (Ultimate / Gold license only)"
-msgstr ""
-
msgid "Epics Roadmap"
msgstr ""
@@ -8080,6 +8297,9 @@ msgstr ""
msgid "Epics let you manage your portfolio of projects more efficiently and with less effort"
msgstr ""
+msgid "Epics, Issues, and Merge Requests"
+msgstr ""
+
msgid "Epics|Add an epic"
msgstr ""
@@ -8131,6 +8351,9 @@ msgstr ""
msgid "Epics|Something went wrong while fetching group epics."
msgstr ""
+msgid "Epics|Something went wrong while moving item."
+msgstr ""
+
msgid "Epics|Something went wrong while ordering item."
msgstr ""
@@ -8185,6 +8408,9 @@ msgstr ""
msgid "Error fetching network graph."
msgstr ""
+msgid "Error fetching payload data."
+msgstr ""
+
msgid "Error fetching projects"
msgstr ""
@@ -8194,9 +8420,6 @@ msgstr ""
msgid "Error fetching the dependency list. Please check your network connection and try again."
msgstr ""
-msgid "Error fetching usage ping data."
-msgstr ""
-
msgid "Error loading branch data. Please try again."
msgstr ""
@@ -8338,6 +8561,9 @@ msgstr ""
msgid "Errors"
msgstr ""
+msgid "Errors:"
+msgstr ""
+
msgid "Estimated"
msgstr ""
@@ -8542,12 +8768,18 @@ msgstr ""
msgid "Export as CSV"
msgstr ""
+msgid "Export group"
+msgstr ""
+
msgid "Export issues"
msgstr ""
msgid "Export project"
msgstr ""
+msgid "Export this group with all related data to a new GitLab instance. Once complete, you can import the data file from the \"New Group\" page."
+msgstr ""
+
msgid "Export this project with all its related data in order to move your project to a new GitLab instance. Once the export is finished, you can import the file from the \"New Project\" page."
msgstr ""
@@ -8617,9 +8849,6 @@ msgstr ""
msgid "Failed Jobs"
msgstr ""
-msgid "Failed create wiki"
-msgstr ""
-
msgid "Failed to add a Zoom meeting"
msgstr ""
@@ -8659,6 +8888,9 @@ msgstr ""
msgid "Failed to create resources"
msgstr ""
+msgid "Failed to create wiki"
+msgstr ""
+
msgid "Failed to delete board. Please try again."
msgstr ""
@@ -8668,7 +8900,7 @@ msgstr ""
msgid "Failed to enqueue the rebase operation, possibly due to a long-lived transaction. Try again later."
msgstr ""
-msgid "Failed to find import label for jira import."
+msgid "Failed to find import label for Jira import."
msgstr ""
msgid "Failed to get ref."
@@ -8692,6 +8924,12 @@ msgstr ""
msgid "Failed to load groups & users."
msgstr ""
+msgid "Failed to load labels. Please try again."
+msgstr ""
+
+msgid "Failed to load milestones. Please try again."
+msgstr ""
+
msgid "Failed to load related branches"
msgstr ""
@@ -9067,7 +9305,10 @@ msgstr ""
msgid "Filter by milestone name"
msgstr ""
-msgid "Filter by name..."
+msgid "Filter by name"
+msgstr ""
+
+msgid "Filter by status"
msgstr ""
msgid "Filter by two-factor authentication"
@@ -9079,12 +9320,18 @@ msgstr ""
msgid "Filter projects"
msgstr ""
+msgid "Filter results"
+msgstr ""
+
msgid "Filter results by group"
msgstr ""
msgid "Filter results by project"
msgstr ""
+msgid "Filter results..."
+msgstr ""
+
msgid "Filter your projects by name"
msgstr ""
@@ -9373,10 +9620,10 @@ msgstr ""
msgid "GeoNodes|Checksummed"
msgstr ""
-msgid "GeoNodes|Container repositories"
+msgid "GeoNodes|Consult Geo troubleshooting information"
msgstr ""
-msgid "GeoNodes|Data is out of date from %{timeago}"
+msgid "GeoNodes|Container repositories"
msgstr ""
msgid "GeoNodes|Data replication lag"
@@ -9418,6 +9665,9 @@ msgstr ""
msgid "GeoNodes|Last event ID seen from primary"
msgstr ""
+msgid "GeoNodes|Learn more about Geo node statuses"
+msgstr ""
+
msgid "GeoNodes|Loading nodes"
msgstr ""
@@ -9433,6 +9683,9 @@ msgstr ""
msgid "GeoNodes|Node was successfully removed."
msgstr ""
+msgid "GeoNodes|Node's status was updated %{timeAgo}."
+msgstr ""
+
msgid "GeoNodes|Not checksummed"
msgstr ""
@@ -9463,7 +9716,10 @@ msgstr ""
msgid "GeoNodes|Repository verification progress"
msgstr ""
-msgid "GeoNodes|Selective"
+msgid "GeoNodes|Selective (%{syncLabel})"
+msgstr ""
+
+msgid "GeoNodes|Selective synchronization"
msgstr ""
msgid "GeoNodes|Something went wrong while changing node status"
@@ -9490,6 +9746,9 @@ msgstr ""
msgid "GeoNodes|Unverified"
msgstr ""
+msgid "GeoNodes|Updated %{timeAgo}"
+msgstr ""
+
msgid "GeoNodes|Used slots"
msgstr ""
@@ -9529,16 +9788,16 @@ msgstr ""
msgid "Geo|All"
msgstr ""
-msgid "Geo|All projects"
+msgid "Geo|All %{replicable_type}"
msgstr ""
-msgid "Geo|All projects are being scheduled for re-sync"
+msgid "Geo|All projects"
msgstr ""
-msgid "Geo|All projects are being scheduled for re-verify"
+msgid "Geo|All projects are being scheduled for resync"
msgstr ""
-msgid "Geo|Batch operations"
+msgid "Geo|All projects are being scheduled for reverify"
msgstr ""
msgid "Geo|Could not remove tracking entry for an existing project."
@@ -9550,9 +9809,15 @@ msgstr ""
msgid "Geo|Failed"
msgstr ""
+msgid "Geo|Filter by status"
+msgstr ""
+
msgid "Geo|Geo Status"
msgstr ""
+msgid "Geo|In progress"
+msgstr ""
+
msgid "Geo|In sync"
msgstr ""
@@ -9577,9 +9842,6 @@ msgstr ""
msgid "Geo|Not synced yet"
msgstr ""
-msgid "Geo|Pending"
-msgstr ""
-
msgid "Geo|Pending synchronization"
msgstr ""
@@ -9616,7 +9878,7 @@ msgstr ""
msgid "Geo|Resync"
msgstr ""
-msgid "Geo|Resync all projects"
+msgid "Geo|Resync all"
msgstr ""
msgid "Geo|Retry count"
@@ -9625,7 +9887,7 @@ msgstr ""
msgid "Geo|Reverify"
msgstr ""
-msgid "Geo|Reverify all projects"
+msgid "Geo|Reverify all"
msgstr ""
msgid "Geo|Status"
@@ -9967,9 +10229,6 @@ msgstr ""
msgid "Go to file"
msgstr ""
-msgid "Go to file (MRs only)"
-msgstr ""
-
msgid "Go to file permalink (while viewing a file)"
msgstr ""
@@ -10414,6 +10673,9 @@ msgstr ""
msgid "GroupSettings|Disable group mentions"
msgstr ""
+msgid "GroupSettings|Export group"
+msgstr ""
+
msgid "GroupSettings|If the parent group's visibility is lower than the group current visibility, visibility levels for subgroups and projects will be changed to match the new parent group's visibility."
msgstr ""
@@ -10492,6 +10754,9 @@ msgstr ""
msgid "Groups (%{count})"
msgstr ""
+msgid "Groups (%{groups})"
+msgstr ""
+
msgid "Groups can also be nested by creating %{subgroup_docs_link_start}subgroups%{subgroup_docs_link_end}."
msgstr ""
@@ -10757,15 +11022,12 @@ msgstr ""
msgid "ID:"
msgstr ""
-msgid "IDE|Allow live previews of JavaScript projects in the Web IDE using CodeSandbox client side evaluation."
+msgid "IDE|Allow live previews of JavaScript projects in the Web IDE using CodeSandbox Live Preview."
msgstr ""
msgid "IDE|Back"
msgstr ""
-msgid "IDE|Client side evaluation"
-msgstr ""
-
msgid "IDE|Commit"
msgstr ""
@@ -10829,6 +11091,9 @@ msgstr ""
msgid "If any job surpasses this timeout threshold, it will be marked as failed. Human readable time input language is accepted like \"1 hour\". Values without specification represent seconds."
msgstr ""
+msgid "If blank, set allowable lifetime to %{instance_level_policy_in_words}, as defined by the instance admin. Once set, existing tokens for users in this group may be revoked."
+msgstr ""
+
msgid "If checked, group owners can manage LDAP group links and LDAP member overrides"
msgstr ""
@@ -10868,6 +11133,9 @@ msgstr ""
msgid "If you lose your recovery codes you can generate new ones, invalidating all previous codes."
msgstr ""
+msgid "If you reach 100%% storage capacity, you will not be able to: %{base_message}"
+msgstr ""
+
msgid "If your HTTP repository is not publicly accessible, add your credentials."
msgstr ""
@@ -10979,6 +11247,9 @@ msgstr ""
msgid "Import repository"
msgstr ""
+msgid "Import started by: %{importInitiator}"
+msgstr ""
+
msgid "Import tasks"
msgstr ""
@@ -11045,6 +11316,9 @@ msgstr ""
msgid "In order to tailor your experience with GitLab we<br>would like to know a bit more about you."
msgstr ""
+msgid "In progress"
+msgstr ""
+
msgid "In the next step, you'll be able to select the projects you want to import."
msgstr ""
@@ -11392,15 +11666,15 @@ msgstr ""
msgid "Issues"
msgstr ""
-msgid "Issues / Merge Requests"
-msgstr ""
-
msgid "Issues Analytics"
msgstr ""
msgid "Issues Rate Limits"
msgstr ""
+msgid "Issues and Merge Requests"
+msgstr ""
+
msgid "Issues can be bugs, tasks or ideas to be discussed. Also, issues are searchable and filterable."
msgstr ""
@@ -11422,10 +11696,10 @@ msgstr ""
msgid "IssuesAnalytics|Avg/Month:"
msgstr ""
-msgid "IssuesAnalytics|Issues created"
+msgid "IssuesAnalytics|Issues opened"
msgstr ""
-msgid "IssuesAnalytics|Issues created per month"
+msgid "IssuesAnalytics|Issues opened per month"
msgstr ""
msgid "IssuesAnalytics|Last 12 months"
@@ -11479,6 +11753,9 @@ msgstr ""
msgid "Jira integration not configured."
msgstr ""
+msgid "Jira project: %{importProject}"
+msgstr ""
+
msgid "JiraService|Events for %{noteable_model_name} are disabled."
msgstr ""
@@ -11868,6 +12145,9 @@ msgstr ""
msgid "Lead"
msgstr ""
+msgid "Lead Time"
+msgstr ""
+
msgid "Learn GitLab"
msgstr ""
@@ -11880,6 +12160,9 @@ msgstr ""
msgid "Learn how to %{no_packages_link_start}publish and share your packages%{no_packages_link_end} with GitLab."
msgstr ""
+msgid "Learn how to enable synchronization"
+msgstr ""
+
msgid "Learn more"
msgstr ""
@@ -12211,9 +12494,6 @@ msgstr ""
msgid "Live preview"
msgstr ""
-msgid "Load more vulnerabilities"
-msgstr ""
-
msgid "Loading"
msgstr ""
@@ -12364,9 +12644,6 @@ msgstr ""
msgid "Manage"
msgstr ""
-msgid "Manage Git repositories with fine-grained access controls that keep your code secure. Perform code reviews and enhance collaboration with merge requests. Each project can also have an issue tracker and a wiki."
-msgstr ""
-
msgid "Manage Web IDE features"
msgstr ""
@@ -12397,6 +12674,9 @@ msgstr ""
msgid "Manage your license"
msgstr ""
+msgid "Managed Account"
+msgstr ""
+
msgid "Manifest"
msgstr ""
@@ -12661,6 +12941,9 @@ msgstr ""
msgid "Merge Requests in Review"
msgstr ""
+msgid "Merge automatically (%{strategy})"
+msgstr ""
+
msgid "Merge commit message"
msgstr ""
@@ -12814,6 +13097,12 @@ msgstr ""
msgid "Merged branches are being deleted. This can take some time depending on the number of branches. Please refresh the page to see changes."
msgstr ""
+msgid "Merged this merge request."
+msgstr ""
+
+msgid "Merges this merge request immediately."
+msgstr ""
+
msgid "Merges this merge request when the pipeline succeeds."
msgstr ""
@@ -12921,12 +13210,15 @@ msgid_plural "Metrics|Edit metrics"
msgstr[0] ""
msgstr[1] ""
-msgid "Metrics|Environment"
+msgid "Metrics|Expand panel"
msgstr ""
msgid "Metrics|For grouping similar metrics"
msgstr ""
+msgid "Metrics|Go back (Esc)"
+msgstr ""
+
msgid "Metrics|Invalid time range, please verify."
msgstr ""
@@ -12957,9 +13249,6 @@ msgstr ""
msgid "Metrics|Refresh dashboard"
msgstr ""
-msgid "Metrics|Show last"
-msgstr ""
-
msgid "Metrics|There was an error creating the dashboard."
msgstr ""
@@ -13038,6 +13327,9 @@ msgstr ""
msgid "Microsoft Azure"
msgstr ""
+msgid "Middleman project with Static Site Editor support"
+msgstr ""
+
msgid "Migrated %{success_count}/%{total_count} files."
msgstr ""
@@ -13483,10 +13775,10 @@ msgstr ""
msgid "Next"
msgstr ""
-msgid "Next file in diff (MRs only)"
+msgid "Next file in diff"
msgstr ""
-msgid "Next unresolved discussion (MRs only)"
+msgid "Next unresolved discussion"
msgstr ""
msgid "Nickname"
@@ -13546,6 +13838,9 @@ msgstr ""
msgid "No changes between %{ref_start}%{source_branch}%{ref_end} and %{ref_start}%{target_branch}%{ref_end}"
msgstr ""
+msgid "No child epics match applied filters"
+msgstr ""
+
msgid "No connection could be made to a Gitaly Server, please check your logs!"
msgstr ""
@@ -13684,15 +13979,6 @@ msgstr ""
msgid "No thanks, don't show this again"
msgstr ""
-msgid "No vulnerabilities found for this group"
-msgstr ""
-
-msgid "No vulnerabilities found for this pipeline"
-msgstr ""
-
-msgid "No vulnerabilities found for this project"
-msgstr ""
-
msgid "No vulnerabilities present"
msgstr ""
@@ -13816,6 +14102,9 @@ msgstr ""
msgid "Nothing to preview."
msgstr ""
+msgid "Nothing to synchronize"
+msgstr ""
+
msgid "Notification events"
msgstr ""
@@ -13987,6 +14276,9 @@ msgstr ""
msgid "Once removed, the fork relationship cannot be restored and you will no longer be able to send merge requests to the source."
msgstr ""
+msgid "Once the exported file is ready you can download it from this page."
+msgstr ""
+
msgid "Once the exported file is ready, you will receive a notification email with a download link, or you can download it from this page."
msgstr ""
@@ -14088,9 +14380,6 @@ msgstr ""
msgid "Open sidebar"
msgstr ""
-msgid "Open source software to collaborate on code"
-msgstr ""
-
msgid "Open: %{openIssuesCount}"
msgstr ""
@@ -14193,6 +14482,9 @@ msgstr ""
msgid "OutdatedBrowser|You can provide feedback %{feedback_link_start}on this issue%{feedback_link_end} or via your usual support channels."
msgstr ""
+msgid "Overridden"
+msgstr ""
+
msgid "Overview"
msgstr ""
@@ -14244,6 +14536,9 @@ msgstr ""
msgid "PackageRegistry|Conan Command"
msgstr ""
+msgid "PackageRegistry|Copy .pypirc content"
+msgstr ""
+
msgid "PackageRegistry|Copy Conan Command"
msgstr ""
@@ -14265,6 +14560,9 @@ msgstr ""
msgid "PackageRegistry|Copy NuGet Setup Command"
msgstr ""
+msgid "PackageRegistry|Copy Pip command"
+msgstr ""
+
msgid "PackageRegistry|Copy and paste this inside your %{codeStart}pom.xml%{codeEnd} %{codeStart}dependencies%{codeEnd} block."
msgstr ""
@@ -14298,6 +14596,12 @@ msgstr ""
msgid "PackageRegistry|For more information on the NuGet registry, %{linkStart}see the documentation%{linkEnd}."
msgstr ""
+msgid "PackageRegistry|For more information on the PyPi registry, %{linkStart}see the documentation%{linkEnd}."
+msgstr ""
+
+msgid "PackageRegistry|If you haven't already done so, you will need to add the below to your %{codeStart}.pypirc%{codeEnd} file."
+msgstr ""
+
msgid "PackageRegistry|If you haven't already done so, you will need to add the below to your %{codeStart}pom.xml%{codeEnd} file."
msgstr ""
@@ -14307,6 +14611,9 @@ msgstr ""
msgid "PackageRegistry|Learn how to %{noPackagesLinkStart}publish and share your packages%{noPackagesLinkEnd} with GitLab."
msgstr ""
+msgid "PackageRegistry|Manually Published"
+msgstr ""
+
msgid "PackageRegistry|Maven"
msgstr ""
@@ -14325,12 +14632,18 @@ msgstr ""
msgid "PackageRegistry|NuGet Command"
msgstr ""
+msgid "PackageRegistry|Pip Command"
+msgstr ""
+
msgid "PackageRegistry|Pipeline %{linkStart}%{linkEnd} triggered %{timestamp} by %{author}"
msgstr ""
msgid "PackageRegistry|Published to the repository at %{timestamp}"
msgstr ""
+msgid "PackageRegistry|PyPi"
+msgstr ""
+
msgid "PackageRegistry|Registry Setup"
msgstr ""
@@ -14367,6 +14680,9 @@ msgstr ""
msgid "PackageRegistry|npm"
msgstr ""
+msgid "PackageRegistry|published by %{author}"
+msgstr ""
+
msgid "PackageRegistry|yarn"
msgstr ""
@@ -14382,6 +14698,9 @@ msgstr ""
msgid "PackageType|NuGet"
msgstr ""
+msgid "PackageType|PyPi"
+msgstr ""
+
msgid "Packages"
msgstr ""
@@ -14475,6 +14794,9 @@ msgstr ""
msgid "Past due"
msgstr ""
+msgid "Paste a machine public key here. Read more about how to generate it"
+msgstr ""
+
msgid "Paste a machine public key here. Read more about how to generate it %{link_start}here%{link_end}"
msgstr ""
@@ -14490,9 +14812,6 @@ msgstr ""
msgid "Path"
msgstr ""
-msgid "Path, transfer, remove"
-msgstr ""
-
msgid "Path:"
msgstr ""
@@ -14523,7 +14842,7 @@ msgstr ""
msgid "Percentage"
msgstr ""
-msgid "Perform advanced options such as changing path, transferring, or removing the group."
+msgid "Perform advanced options such as changing path, transferring, exporting, or removing the group."
msgstr ""
msgid "Perform common operations on GitLab project"
@@ -14604,9 +14923,6 @@ msgstr ""
msgid "Pipeline minutes quota"
msgstr ""
-msgid "Pipeline quota"
-msgstr ""
-
msgid "Pipeline subscriptions"
msgstr ""
@@ -14616,6 +14932,9 @@ msgstr ""
msgid "Pipeline: %{status}"
msgstr ""
+msgid "PipelineCharts|CI / CD Analytics"
+msgstr ""
+
msgid "PipelineCharts|Failed:"
msgstr ""
@@ -14694,15 +15013,6 @@ msgstr ""
msgid "Pipelines settings for '%{project_name}' were successfully updated."
msgstr ""
-msgid "Pipelines| to purchase more minutes."
-msgstr ""
-
-msgid "Pipelines|%{namespace_name} has exceeded its pipeline minutes quota."
-msgstr ""
-
-msgid "Pipelines|%{namespace_name} has less than %{notification_level}%% of CI minutes available."
-msgstr ""
-
msgid "Pipelines|API"
msgstr ""
@@ -14724,10 +15034,13 @@ msgstr ""
msgid "Pipelines|Get started with Pipelines"
msgstr ""
-msgid "Pipelines|Loading Pipelines"
+msgid "Pipelines|Group %{namespace_name} has %{percentage}%% or less Shared Runner Pipeline minutes remaining. Once it runs out, no new jobs or pipelines in its projects will run."
msgstr ""
-msgid "Pipelines|Pipelines will not run anymore on shared Runners."
+msgid "Pipelines|Group %{namespace_name} has exceeded its pipeline minutes quota. Unless you buy additional pipeline minutes, no new jobs or pipelines in its projects will run."
+msgstr ""
+
+msgid "Pipelines|Loading Pipelines"
msgstr ""
msgid "Pipelines|Project cache successfully reset."
@@ -14958,6 +15271,9 @@ msgstr ""
msgid "Please select"
msgstr ""
+msgid "Please select a Jira project"
+msgstr ""
+
msgid "Please select a country"
msgstr ""
@@ -15003,6 +15319,9 @@ msgstr ""
msgid "Please wait while we import the repository for you. Refresh at will."
msgstr ""
+msgid "Plugins directory is deprecated and will be removed in 14.0. Please move this file into /file_hooks directory."
+msgstr ""
+
msgid "Pod does not exist"
msgstr ""
@@ -15120,7 +15439,7 @@ msgstr ""
msgid "Prevent users from changing their profile name"
msgstr ""
-msgid "Prevent users from modifing merge request approvers list"
+msgid "Prevent users from modifying merge request approvers list"
msgstr ""
msgid "Prevent users from performing write operations on GitLab while performing maintenance."
@@ -15141,10 +15460,10 @@ msgstr ""
msgid "Previous Artifacts"
msgstr ""
-msgid "Previous file in diff (MRs only)"
+msgid "Previous file in diff"
msgstr ""
-msgid "Previous unresolved discussion (MRs only)"
+msgid "Previous unresolved discussion"
msgstr ""
msgid "Primary"
@@ -15177,6 +15496,9 @@ msgstr ""
msgid "Private profile"
msgstr ""
+msgid "Private projects Minutes cost factor"
+msgstr ""
+
msgid "Private projects can be created in your personal namespace with:"
msgstr ""
@@ -16005,6 +16327,9 @@ msgstr ""
msgid "ProjectSettings|With GitLab Pages you can host your static websites on GitLab"
msgstr ""
+msgid "ProjectSettings|With Metrics Dashboard you can visualize this project performance metrics"
+msgstr ""
+
msgid "ProjectTemplates|.NET Core"
msgstr ""
@@ -16068,6 +16393,9 @@ msgstr ""
msgid "ProjectTemplates|Spring"
msgstr ""
+msgid "ProjectTemplates|Static Site Editor/Middleman"
+msgstr ""
+
msgid "ProjectTemplates|iOS (Swift)"
msgstr ""
@@ -16287,13 +16615,13 @@ msgstr ""
msgid "PrometheusService|Prometheus is being automatically managed on your clusters"
msgstr ""
-msgid "PrometheusService|These metrics will only be monitored after your first deployment to an environment"
+msgid "PrometheusService|Select the Active checkbox to override the Auto Configuration with custom settings. If unchecked, Auto Configuration settings are used."
msgstr ""
-msgid "PrometheusService|Time-series monitoring service"
+msgid "PrometheusService|These metrics will only be monitored after your first deployment to an environment"
msgstr ""
-msgid "PrometheusService|To enable manual configuration, uninstall Prometheus from your clusters"
+msgid "PrometheusService|Time-series monitoring service"
msgstr ""
msgid "PrometheusService|To enable the installation of Prometheus on your clusters, deactivate the manual configuration below"
@@ -16479,6 +16807,9 @@ msgstr ""
msgid "Protip:"
msgstr ""
+msgid "Protocol"
+msgstr ""
+
msgid "Provider"
msgstr ""
@@ -16503,6 +16834,9 @@ msgstr ""
msgid "Public pipelines"
msgstr ""
+msgid "Public projects Minutes cost factor"
+msgstr ""
+
msgid "Pull"
msgstr ""
@@ -16703,6 +17037,9 @@ msgid_plural "Refreshing in %d seconds to show the updated status..."
msgstr[0] ""
msgstr[1] ""
+msgid "Regenerate export"
+msgstr ""
+
msgid "Regenerate instance ID"
msgstr ""
@@ -17080,9 +17417,29 @@ msgstr ""
msgid "Reports|%{combinedString} and %{resolvedString}"
msgstr ""
+msgid "Reports|Accessibility scanning detected %d issue for the source branch only"
+msgid_plural "Reports|Accessibility scanning detected %d issues for the source branch only"
+msgstr[0] ""
+msgstr[1] ""
+
+msgid "Reports|Accessibility scanning detected no issues for the source branch only"
+msgstr ""
+
+msgid "Reports|Accessibility scanning failed loading results"
+msgstr ""
+
+msgid "Reports|Accessibility scanning results are being parsed"
+msgstr ""
+
msgid "Reports|Actions"
msgstr ""
+msgid "Reports|An error occured while loading report"
+msgstr ""
+
+msgid "Reports|An error occurred while loading %{name} results"
+msgstr ""
+
msgid "Reports|Class"
msgstr ""
@@ -17188,7 +17545,7 @@ msgstr ""
msgid "Requested %{time_ago}"
msgstr ""
-msgid "Requested design version does not exist"
+msgid "Requested design version does not exist."
msgstr ""
msgid "Requested states are invalid"
@@ -17372,6 +17729,9 @@ msgstr ""
msgid "Resync"
msgstr ""
+msgid "Resync all"
+msgstr ""
+
msgid "Resync all %{replicableType}"
msgstr ""
@@ -17440,6 +17800,9 @@ msgstr ""
msgid "Revoked personal access token %{personal_access_token_name}!"
msgstr ""
+msgid "Revoked project access token %{project_access_token_name}!"
+msgstr ""
+
msgid "RightSidebar|adding a"
msgstr ""
@@ -17488,6 +17851,9 @@ msgstr ""
msgid "Runner token"
msgstr ""
+msgid "Runner tokens"
+msgstr ""
+
msgid "Runner was not updated."
msgstr ""
@@ -17542,6 +17908,9 @@ msgstr ""
msgid "SAML SSO for %{group_name}"
msgstr ""
+msgid "SAML discovery tokens"
+msgstr ""
+
msgid "SAML for %{group_name}"
msgstr ""
@@ -17626,12 +17995,18 @@ msgstr ""
msgid "Scheduled"
msgstr ""
+msgid "Scheduled to merge this merge request (%{strategy})."
+msgstr ""
+
msgid "Scheduled to merge this merge request when the pipeline succeeds."
msgstr ""
msgid "Schedules"
msgstr ""
+msgid "Schedules to merge this merge request (%{strategy})."
+msgstr ""
+
msgid "Scheduling"
msgstr ""
@@ -17689,6 +18064,9 @@ msgstr ""
msgid "Search branches and tags"
msgstr ""
+msgid "Search by author"
+msgstr ""
+
msgid "Search files"
msgstr ""
@@ -17834,11 +18212,6 @@ msgid_plural "SearchResults|snippets"
msgstr[0] ""
msgstr[1] ""
-msgid "SearchResults|snippet result"
-msgid_plural "SearchResults|snippet results"
-msgstr[0] ""
-msgstr[1] ""
-
msgid "SearchResults|user"
msgid_plural "SearchResults|users"
msgstr[0] ""
@@ -17849,6 +18222,9 @@ msgid_plural "SearchResults|wiki results"
msgstr[0] ""
msgstr[1] ""
+msgid "Searching by both author and message is currently not supported."
+msgstr ""
+
msgid "Seat Link"
msgstr ""
@@ -17876,184 +18252,232 @@ msgstr ""
msgid "Security Dashboard"
msgstr ""
-msgid "Security Dashboard|Error fetching the vulnerability counts. Please check your network connection and try again."
+msgid "Security configuration help link"
+msgstr ""
+
+msgid "Security dashboard"
msgstr ""
-msgid "Security Dashboard|Error fetching the vulnerability list. Please check your network connection and try again."
+msgid "Security report is out of date. Please update your branch with the latest changes from the target branch (%{targetBranchName})"
msgstr ""
-msgid "Security Dashboard|Issue Created"
+msgid "Security report is out of date. Run %{newPipelineLinkStart}a new pipeline%{newPipelineLinkEnd} for the target branch (%{targetBranchName})"
msgstr ""
-msgid "Security Reports|Comment added to '%{vulnerabilityName}'"
+msgid "SecurityConfiguration|Configured"
msgstr ""
-msgid "Security Reports|Comment deleted on '%{vulnerabilityName}'"
+msgid "SecurityConfiguration|Feature"
msgstr ""
-msgid "Security Reports|Comment edited on '%{vulnerabilityName}'"
+msgid "SecurityConfiguration|Feature documentation for %{featureName}"
msgstr ""
-msgid "Security Reports|Create issue"
+msgid "SecurityConfiguration|Not yet configured"
msgstr ""
-msgid "Security Reports|Dismiss vulnerability"
+msgid "SecurityConfiguration|Secure features"
msgstr ""
-msgid "Security Reports|Dismissed '%{vulnerabilityName}'"
+msgid "SecurityConfiguration|Status"
msgstr ""
-msgid "Security Reports|Dismissed '%{vulnerabilityName}'. Turn off the hide dismissed toggle to view."
+msgid "SecurityReports|%{firstProject} and %{secondProject}"
msgstr ""
-msgid "Security Reports|Either you don't have permission to view this dashboard or the dashboard has not been setup. Please check your permission settings with your administrator or check your dashboard configurations to proceed."
+msgid "SecurityReports|%{firstProject}, %{secondProject}, and %{rest}"
msgstr ""
-msgid "Security Reports|Learn more about setting up your dashboard"
+msgid "SecurityReports|Add a project to your dashboard"
msgstr ""
-msgid "Security Reports|More info"
+msgid "SecurityReports|Add or remove projects from your dashboard"
msgstr ""
-msgid "Security Reports|Oops, something doesn't seem right."
+msgid "SecurityReports|Add projects"
msgstr ""
-msgid "Security Reports|Security reports can only be accessed by authorized users."
+msgid "SecurityReports|Comment added to '%{vulnerabilityName}'"
msgstr ""
-msgid "Security Reports|There was an error adding the comment."
+msgid "SecurityReports|Comment deleted on '%{vulnerabilityName}'"
msgstr ""
-msgid "Security Reports|There was an error creating the issue."
+msgid "SecurityReports|Comment edited on '%{vulnerabilityName}'"
msgstr ""
-msgid "Security Reports|There was an error creating the merge request."
+msgid "SecurityReports|Create issue"
msgstr ""
-msgid "Security Reports|There was an error deleting the comment."
+msgid "SecurityReports|Dismiss Selected"
msgstr ""
-msgid "Security Reports|There was an error dismissing the vulnerabilities."
+msgid "SecurityReports|Dismiss vulnerability"
msgstr ""
-msgid "Security Reports|There was an error dismissing the vulnerability."
+msgid "SecurityReports|Dismissed '%{vulnerabilityName}'"
msgstr ""
-msgid "Security Reports|There was an error reverting the dismissal."
+msgid "SecurityReports|Dismissed '%{vulnerabilityName}'. Turn off the hide dismissed toggle to view."
msgstr ""
-msgid "Security Reports|There was an error reverting this dismissal."
+msgid "SecurityReports|Each vulnerability now has a unique page that can be directly linked to, shared, referenced, and tracked as the single source of truth. Vulnerability occurrences also persist across scanner runs, which improves tracking and visibility and reduces duplicates between scans."
msgstr ""
-msgid "Security Reports|Undo dismiss"
+msgid "SecurityReports|Edit dashboard"
msgstr ""
-msgid "Security Reports|You do not have sufficient permissions to access this report"
+msgid "SecurityReports|Either you don't have permission to view this dashboard or the dashboard has not been setup. Please check your permission settings with your administrator or check your dashboard configurations to proceed."
msgstr ""
-msgid "Security Reports|You must sign in as an authorized user to see this report"
+msgid "SecurityReports|Error fetching the vulnerability counts. Please check your network connection and try again."
msgstr ""
-msgid "Security configuration help link"
+msgid "SecurityReports|Error fetching the vulnerability list. Please check your network connection and try again."
msgstr ""
-msgid "Security dashboard"
+msgid "SecurityReports|False positive"
msgstr ""
-msgid "Security report is out of date. Please update your branch with the latest changes from the target branch (%{targetBranchName})"
+msgid "SecurityReports|Hide dismissed"
msgstr ""
-msgid "Security report is out of date. Run %{newPipelineLinkStart}a new pipeline%{newPipelineLinkEnd} for the target branch (%{targetBranchName})"
+msgid "SecurityReports|Introducing standalone vulnerabilities"
msgstr ""
-msgid "SecurityConfiguration|Configured"
+msgid "SecurityReports|Issue Created"
msgstr ""
-msgid "SecurityConfiguration|Feature"
+msgid "SecurityReports|Learn More"
msgstr ""
-msgid "SecurityConfiguration|Feature documentation for %{featureName}"
+msgid "SecurityReports|Learn more about setting up your dashboard"
msgstr ""
-msgid "SecurityConfiguration|Not yet configured"
+msgid "SecurityReports|Load more vulnerabilities"
msgstr ""
-msgid "SecurityConfiguration|Secure features"
+msgid "SecurityReports|Monitor vulnerabilities in your code"
msgstr ""
-msgid "SecurityConfiguration|Status"
+msgid "SecurityReports|More info"
msgstr ""
-msgid "SecurityDashboard|%{firstProject} and %{secondProject}"
+msgid "SecurityReports|More information"
msgstr ""
-msgid "SecurityDashboard|%{firstProject}, %{secondProject}, and %{rest}"
+msgid "SecurityReports|No vulnerabilities found for dashboard"
msgstr ""
-msgid "SecurityDashboard|Add a project to your dashboard"
+msgid "SecurityReports|No vulnerabilities found for this group"
msgstr ""
-msgid "SecurityDashboard|Add or remove projects from your dashboard"
+msgid "SecurityReports|No vulnerabilities found for this pipeline"
msgstr ""
-msgid "SecurityDashboard|Add projects"
+msgid "SecurityReports|No vulnerabilities found for this project"
msgstr ""
-msgid "SecurityDashboard|Edit dashboard"
+msgid "SecurityReports|Oops, something doesn't seem right."
msgstr ""
-msgid "SecurityDashboard|Hide dismissed"
+msgid "SecurityReports|Pipeline %{pipelineLink} triggered %{timeago} by %{user}"
msgstr ""
-msgid "SecurityDashboard|Monitor vulnerabilities in your code"
+msgid "SecurityReports|Project"
msgstr ""
-msgid "SecurityDashboard|More information"
+msgid "SecurityReports|Projects added"
msgstr ""
-msgid "SecurityDashboard|No vulnerabilities found for dashboard"
+msgid "SecurityReports|Remove project from dashboard"
msgstr ""
-msgid "SecurityDashboard|Pipeline %{pipelineLink} triggered %{timeago} by %{user}"
+msgid "SecurityReports|Report type"
msgstr ""
-msgid "SecurityDashboard|Project"
+msgid "SecurityReports|Return to dashboard"
msgstr ""
-msgid "SecurityDashboard|Projects added"
+msgid "SecurityReports|Security Dashboard"
msgstr ""
-msgid "SecurityDashboard|Remove project from dashboard"
+msgid "SecurityReports|Security reports can only be accessed by authorized users."
msgstr ""
-msgid "SecurityDashboard|Report type"
+msgid "SecurityReports|Select a project to add by using the project search field above."
msgstr ""
-msgid "SecurityDashboard|Return to dashboard"
+msgid "SecurityReports|Select a reason"
msgstr ""
-msgid "SecurityDashboard|Security Dashboard"
+msgid "SecurityReports|Severity"
msgstr ""
-msgid "SecurityDashboard|Select a project to add by using the project search field above."
+msgid "SecurityReports|Status"
msgstr ""
-msgid "SecurityDashboard|Severity"
+msgid "SecurityReports|The security dashboard displays the latest security findings for projects you wish to monitor. Select \"Edit dashboard\" to add and remove projects."
msgstr ""
-msgid "SecurityDashboard|Status"
+msgid "SecurityReports|The security dashboard displays the latest security report. Use it to find and fix vulnerabilities."
msgstr ""
-msgid "SecurityDashboard|The security dashboard displays the latest security findings for projects you wish to monitor. Select \"Edit dashboard\" to add and remove projects."
+msgid "SecurityReports|There was an error adding the comment."
msgstr ""
-msgid "SecurityDashboard|The security dashboard displays the latest security report. Use it to find and fix vulnerabilities."
+msgid "SecurityReports|There was an error creating the issue."
msgstr ""
-msgid "SecurityDashboard|There was an error while generating the report."
+msgid "SecurityReports|There was an error creating the merge request."
msgstr ""
-msgid "SecurityDashboard|Unable to add %{invalidProjects}"
+msgid "SecurityReports|There was an error deleting the comment."
+msgstr ""
+
+msgid "SecurityReports|There was an error dismissing the vulnerabilities."
+msgstr ""
+
+msgid "SecurityReports|There was an error dismissing the vulnerability."
+msgstr ""
+
+msgid "SecurityReports|There was an error reverting the dismissal."
+msgstr ""
+
+msgid "SecurityReports|There was an error reverting this dismissal."
+msgstr ""
+
+msgid "SecurityReports|There was an error while generating the report."
+msgstr ""
+
+msgid "SecurityReports|Unable to add %{invalidProjects}"
+msgstr ""
+
+msgid "SecurityReports|Undo dismiss"
+msgstr ""
+
+msgid "SecurityReports|While it's rare to have no vulnerabilities for your group, it can happen. In any event, we ask that you double check your settings to make sure you've set up your dashboard correctly."
+msgstr ""
+
+msgid "SecurityReports|While it's rare to have no vulnerabilities for your pipeline, it can happen. In any event, we ask that you double check your settings to make sure all security scanning jobs have passed successfully."
+msgstr ""
+
+msgid "SecurityReports|While it's rare to have no vulnerabilities for your project, it can happen. In any event, we ask that you double check your settings to make sure you've set up your dashboard correctly."
+msgstr ""
+
+msgid "SecurityReports|While it's rare to have no vulnerabilities, it can happen. In any event, we ask that you please double check your settings to make sure you've set up your dashboard correctly."
+msgstr ""
+
+msgid "SecurityReports|Won't fix / Accept risk"
+msgstr ""
+
+msgid "SecurityReports|You do not have sufficient permissions to access this report"
+msgstr ""
+
+msgid "SecurityReports|You must sign in as an authorized user to see this report"
+msgstr ""
+
+msgid "SecurityReports|[No reason]"
msgstr ""
msgid "See GitLab's %{password_policy_guidelines}"
@@ -18065,6 +18489,9 @@ msgstr ""
msgid "See the affected projects in the GitLab admin panel"
msgstr ""
+msgid "See what's new at GitLab"
+msgstr ""
+
msgid "Select"
msgstr ""
@@ -18470,9 +18897,6 @@ msgstr ""
msgid "Set up Jira Integration"
msgstr ""
-msgid "Set up Jira Integration illustration"
-msgstr ""
-
msgid "Set up a %{type} Runner automatically"
msgstr ""
@@ -18557,6 +18981,9 @@ msgstr ""
msgid "Severity: %{severity}"
msgstr ""
+msgid "Shards (%{shards})"
+msgstr ""
+
msgid "Shards to synchronize"
msgstr ""
@@ -18871,6 +19298,9 @@ msgstr ""
msgid "Snowplow"
msgstr ""
+msgid "Some child epics may be hidden due to applied filters"
+msgstr ""
+
msgid "Some email servers do not support overriding the email sender name. Enable this option to include the name of the author of the issue, merge request or comment in the email body instead."
msgstr ""
@@ -19012,6 +19442,9 @@ msgstr ""
msgid "Something went wrong while updating your list settings"
msgstr ""
+msgid "Something went wrong with your automatic subscription renewal"
+msgstr ""
+
msgid "Something went wrong, unable to add %{project} to dashboard"
msgstr ""
@@ -19297,6 +19730,9 @@ msgstr ""
msgid "Stage removed"
msgstr ""
+msgid "Standard"
+msgstr ""
+
msgid "Star a label to make it a priority label. Order the prioritized labels to change their relative priority, by dragging."
msgstr ""
@@ -19420,6 +19856,9 @@ msgstr ""
msgid "Static Application Security Testing (SAST)"
msgstr ""
+msgid "StaticSiteEditor|An error occurred while submitting your changes."
+msgstr ""
+
msgid "StaticSiteEditor|Branch could not be created."
msgstr ""
@@ -19429,18 +19868,30 @@ msgstr ""
msgid "StaticSiteEditor|Could not create merge request."
msgstr ""
+msgid "StaticSiteEditor|Incompatible file content"
+msgstr ""
+
msgid "StaticSiteEditor|Return to site"
msgstr ""
+msgid "StaticSiteEditor|Static site editor"
+msgstr ""
+
msgid "StaticSiteEditor|Success!"
msgstr ""
msgid "StaticSiteEditor|Summary of changes"
msgstr ""
+msgid "StaticSiteEditor|The Static Site Editor is currently configured to only edit Markdown content on pages generated from Middleman. Visit the documentation to learn more about configuring your site to use the Static Site Editor."
+msgstr ""
+
msgid "StaticSiteEditor|Update %{sourcePath} file"
msgstr ""
+msgid "StaticSiteEditor|View documentation"
+msgstr ""
+
msgid "StaticSiteEditor|View merge request"
msgstr ""
@@ -19498,6 +19949,12 @@ msgstr ""
msgid "StatusPage|Status page"
msgstr ""
+msgid "StatusPage|Status page URL"
+msgstr ""
+
+msgid "StatusPage|Status page frontend documentation"
+msgstr ""
+
msgid "StatusPage|To publish incidents to an external status page, GitLab will store a JSON file in your Amazon S3 account in a location accessible to your external status page service. Make sure to also set up %{docsLink}"
msgstr ""
@@ -19846,6 +20303,12 @@ msgstr ""
msgid "Synced"
msgstr ""
+msgid "Synchronization disabled"
+msgstr ""
+
+msgid "Synchronization of container repositories is disabled."
+msgstr ""
+
msgid "System"
msgstr ""
@@ -19999,6 +20462,9 @@ msgstr ""
msgid "Telephone number"
msgstr ""
+msgid "Telephone number (Optional)"
+msgstr ""
+
msgid "Template"
msgstr ""
@@ -20305,6 +20771,9 @@ msgstr ""
msgid "The license was successfully uploaded and is now active. You can see the details below."
msgstr ""
+msgid "The license was successfully uploaded and will be active from %{starts_at}. You can see the details below."
+msgstr ""
+
msgid "The maximum file size allowed is %{size}."
msgstr ""
@@ -20626,6 +21095,9 @@ msgstr ""
msgid "There was an error getting the epic participants."
msgstr ""
+msgid "There was an error importing the Jira project."
+msgstr ""
+
msgid "There was an error loading users activity calendar."
msgstr ""
@@ -20641,7 +21113,7 @@ msgstr ""
msgid "There was an error resetting user pipeline minutes."
msgstr ""
-msgid "There was an error saving this Geo Node"
+msgid "There was an error saving this Geo Node."
msgstr ""
msgid "There was an error saving your changes."
@@ -20704,6 +21176,9 @@ msgstr ""
msgid "They can be managed using the %{link}."
msgstr ""
+msgid "Third Party Advisory Link"
+msgstr ""
+
msgid "Third party offers"
msgstr ""
@@ -20740,6 +21215,9 @@ msgstr ""
msgid "This application will be able to:"
msgstr ""
+msgid "This attachment has been truncated to avoid exceeding the maximum allowed attachment size of 15MB. %{written_count} of %{issues_count} issues have been included. Consider re-exporting with a narrower selection of issues."
+msgstr ""
+
msgid "This block is self-referential"
msgstr ""
@@ -20881,6 +21359,12 @@ msgstr ""
msgid "This issue is confidential"
msgstr ""
+msgid "This issue is currently blocked by the following issues: %{issues}."
+msgstr ""
+
+msgid "This issue is in a child epic of the filtered epic"
+msgstr ""
+
msgid "This issue is locked."
msgstr ""
@@ -21079,7 +21563,10 @@ msgstr ""
msgid "This user will be the author of all events in the activity feed that are the result of an update, like new branches being created or new commits being pushed to existing branches. Upon creation or when reassigning you can only assign yourself to be the mirror user."
msgstr ""
-msgid "This variable can not be masked"
+msgid "This variable can not be masked."
+msgstr ""
+
+msgid "This variable does not match the expected pattern."
msgstr ""
msgid "This will help us personalize your onboarding experience."
@@ -21109,10 +21596,10 @@ msgstr ""
msgid "ThreatMonitoring|Application firewall not detected"
msgstr ""
-msgid "ThreatMonitoring|Container Network Policy"
+msgid "ThreatMonitoring|Container Network Policies are not installed or have been disabled. To view this data, ensure your Network Policies are installed and enabled for your cluster."
msgstr ""
-msgid "ThreatMonitoring|Container NetworkPolicies are not installed or has been disabled. To view this data, ensure you NetworkPolicies are installed and enabled for your cluster."
+msgid "ThreatMonitoring|Container Network Policy"
msgstr ""
msgid "ThreatMonitoring|Container NetworkPolicies not detected"
@@ -21145,7 +21632,7 @@ msgstr ""
msgid "ThreatMonitoring|Something went wrong, unable to fetch statistics"
msgstr ""
-msgid "ThreatMonitoring|The firewall is not installed or has been disabled. To view this data, ensure you firewall is installed and enabled for your cluster."
+msgid "ThreatMonitoring|The firewall is not installed or has been disabled. To view this data, ensure the web application firewall is installed and enabled for your cluster."
msgstr ""
msgid "ThreatMonitoring|The graph below is an overview of traffic coming to your application as tracked by the Web Application Firewall (WAF). View the docs for instructions on how to access the WAF logs to see what type of malicious traffic is trying to access your app. The docs link is also accessible by clicking the \"?\" icon next to the title below."
@@ -21214,6 +21701,9 @@ msgstr ""
msgid "Time in seconds GitLab will wait for a response from the external service. When the service does not respond in time, access will be denied."
msgstr ""
+msgid "Time of import: %{importTime}"
+msgstr ""
+
msgid "Time remaining"
msgstr ""
@@ -21398,7 +21888,7 @@ msgstr ""
msgid "Title:"
msgstr ""
-msgid "Titles and Filenames"
+msgid "Titles and Descriptions"
msgstr ""
msgid "To"
@@ -21605,7 +22095,7 @@ msgstr ""
msgid "Too many projects enabled. You will need to manage them via the console or the API."
msgstr ""
-msgid "Topics"
+msgid "Topics (optional)"
msgstr ""
msgid "Total"
@@ -21767,6 +22257,9 @@ msgstr ""
msgid "Try all GitLab has to offer for 30 days."
msgstr ""
+msgid "Try changing or removing filters."
+msgstr ""
+
msgid "Try to fork again"
msgstr ""
@@ -21860,6 +22353,9 @@ msgstr ""
msgid "Unable to connect to server: %{error}"
msgstr ""
+msgid "Unable to connect to the Jira instance. Please check your Jira integration configuration."
+msgstr ""
+
msgid "Unable to convert Kubernetes logs encoding to UTF-8"
msgstr ""
@@ -22803,6 +23299,9 @@ msgstr ""
msgid "View issue"
msgstr ""
+msgid "View issues"
+msgstr ""
+
msgid "View it on GitLab"
msgstr ""
@@ -22980,6 +23479,15 @@ msgstr ""
msgid "VulnerabilityManagement|Resolved %{timeago} by %{user}"
msgstr ""
+msgid "VulnerabilityManagement|Something went wrong while trying to delete the comment. Please try again later."
+msgstr ""
+
+msgid "VulnerabilityManagement|Something went wrong while trying to retrieve the vulnerability history. Please try again later."
+msgstr ""
+
+msgid "VulnerabilityManagement|Something went wrong while trying to save the comment. Please try again later."
+msgstr ""
+
msgid "VulnerabilityManagement|Something went wrong, could not create an issue."
msgstr ""
@@ -23010,6 +23518,9 @@ msgstr ""
msgid "VulnerabilityStatusTypes|Resolved"
msgstr ""
+msgid "Vulnerability|%{scannerName} (version %{scannerVersion})"
+msgstr ""
+
msgid "Vulnerability|Class"
msgstr ""
@@ -23043,7 +23554,10 @@ msgstr ""
msgid "Vulnerability|Project"
msgstr ""
-msgid "Vulnerability|Report Type"
+msgid "Vulnerability|Scanner Provider"
+msgstr ""
+
+msgid "Vulnerability|Scanner Type"
msgstr ""
msgid "Vulnerability|Severity"
@@ -23097,9 +23611,15 @@ msgstr ""
msgid "We sent you an email with reset password instructions"
msgstr ""
+msgid "We tried to automatically renew your %{strong}%{plan_name}%{strong_close} subscription for %{strong}%{namespace_name}%{strong_close} on %{expires_on} but something went wrong so your subscription was downgraded to the free plan. Don't worry, your data is safe. We suggest you check your payment method and get in touch with our support team (%{support_link}). They'll gladly help with your subscription renewal."
+msgstr ""
+
msgid "We want to be sure it is you, please confirm you are not a robot."
msgstr ""
+msgid "We will automatically renew your %{strong}%{plan_name}%{strong_close} subscription for %{strong}%{namespace_name}%{strong_close} on %{strong}%{expires_on}%{strong_close}. There's nothing that you need to do, we'll let you know when the renewal is complete. Need more seats, a higher plan or just want to review your payment method?"
+msgstr ""
+
msgid "We've found no vulnerabilities"
msgstr ""
@@ -23204,15 +23724,6 @@ msgstr ""
msgid "When:"
msgstr ""
-msgid "While it's rare to have no vulnerabilities for your group, it can happen. In any event, we ask that you double check your settings to make sure you've set up your dashboard correctly."
-msgstr ""
-
-msgid "While it's rare to have no vulnerabilities for your pipeline, it can happen. In any event, we ask that you double check your settings to make sure all security scanning jobs have passed successfully."
-msgstr ""
-
-msgid "While it's rare to have no vulnerabilities for your project, it can happen. In any event, we ask that you double check your settings to make sure you've set up your dashboard correctly."
-msgstr ""
-
msgid "While it's rare to have no vulnerabilities, it can happen. In any event, we ask that you please double check your settings to make sure you've set up your dashboard correctly."
msgstr ""
@@ -23243,9 +23754,6 @@ msgstr ""
msgid "Wiki"
msgstr ""
-msgid "Wiki pages"
-msgstr ""
-
msgid "Wiki was successfully updated."
msgstr ""
@@ -23429,6 +23937,9 @@ msgstr ""
msgid "Yes, add it"
msgstr ""
+msgid "Yes, close issue"
+msgstr ""
+
msgid "Yes, let me map Google Code users to full names or GitLab users."
msgstr ""
@@ -23624,6 +24135,9 @@ msgstr ""
msgid "You could not create a new trigger."
msgstr ""
+msgid "You didn't renew your %{strong}%{plan_name}%{strong_close} subscription for %{strong}%{namespace_name}%{strong_close} so it was downgraded to the free plan."
+msgstr ""
+
msgid "You didn't renew your %{strong}%{plan_name}%{strong_close} subscription so it was downgraded to the GitLab Core Plan."
msgstr ""
@@ -23690,6 +24204,9 @@ msgstr ""
msgid "You have declined the invitation to join %{label}."
msgstr ""
+msgid "You have imported from this project %{numberOfPreviousImportsForProject} times before. Each new import will create duplicate issues."
+msgstr ""
+
msgid "You have no permissions"
msgstr ""
@@ -23771,6 +24288,9 @@ msgstr ""
msgid "You need to upload a Google Takeout archive."
msgstr ""
+msgid "You reached %{usage_in_percent} of %{namespace_name}'s capacity (%{used_storage} of %{storage_limit})"
+msgstr ""
+
msgid "You tried to fork %{link_to_the_project} but it failed for the following reason:"
msgstr ""
@@ -23855,9 +24375,21 @@ msgstr ""
msgid "YouTube"
msgstr ""
+msgid "Your %{strong}%{plan_name}%{strong_close} subscription for %{strong}%{namespace_name}%{strong_close} will expire on %{strong}%{expires_on}%{strong_close}. After that, you will not to be able to create issues or merge requests as well as many other features."
+msgstr ""
+
msgid "Your %{strong}%{plan_name}%{strong_close} subscription will expire on %{strong}%{expires_on}%{strong_close}. After that, you will not to be able to create issues or merge requests as well as many other features."
msgstr ""
+msgid "Your CSV export has started. It will be emailed to %{email} when complete."
+msgstr ""
+
+msgid "Your CSV export of %{issues_count} from project %{project_link} has been added to this email as an attachment."
+msgstr ""
+
+msgid "Your CSV export of %{written_count} from project %{project_name} (%{project_url}) has been added to this email as an attachment."
+msgstr ""
+
msgid "Your Commit Email will be used for web based operations, such as edits and merges."
msgstr ""
@@ -23954,6 +24486,9 @@ msgstr ""
msgid "Your comment could not be updated! Please check your network connection and try again."
msgstr ""
+msgid "Your comment will be discarded."
+msgstr ""
+
msgid "Your custom stage '%{title}' was created"
msgstr ""
@@ -23993,6 +24528,9 @@ msgstr ""
msgid "Your new personal access token has been created."
msgstr ""
+msgid "Your new project access token has been created."
+msgstr ""
+
msgid "Your password isn't required to view this page. If a password or any other personal details are requested, please contact your administrator to report abuse."
msgstr ""
@@ -24011,12 +24549,18 @@ msgstr ""
msgid "Your request for access has been queued for review."
msgstr ""
+msgid "Your search didn't match any commits."
+msgstr ""
+
msgid "Your subscription expired!"
msgstr ""
msgid "Your subscription has been downgraded"
msgstr ""
+msgid "Your subscription will automatically renew in %{remaining_days}"
+msgstr ""
+
msgid "Your subscription will expire in %{remaining_days}"
msgstr ""
@@ -24055,7 +24599,7 @@ msgstr ""
msgid "allowed to fail"
msgstr ""
-msgid "already being used for another group or project milestone."
+msgid "already being used for another group or project %{timebox_name}."
msgstr ""
msgid "already has a \"created\" issue link"
@@ -24133,6 +24677,9 @@ msgstr ""
msgid "ciReport|%{linkStartTag}Learn more about SAST %{linkEndTag}"
msgstr ""
+msgid "ciReport|%{linkStartTag}Learn more about Secret Scanning %{linkEndTag}"
+msgstr ""
+
msgid "ciReport|%{linkStartTag}Learn more about codequality reports %{linkEndTag}"
msgstr ""
@@ -24296,6 +24843,12 @@ msgstr ""
msgid "ciReport|SAST"
msgstr ""
+msgid "ciReport|Secret scanning"
+msgstr ""
+
+msgid "ciReport|Secret scanning detects secrets and credentials vulnerabilities in your source code."
+msgstr ""
+
msgid "ciReport|Security scanning"
msgstr ""
@@ -24493,6 +25046,9 @@ msgstr ""
msgid "group"
msgstr ""
+msgid "groups"
+msgstr ""
+
msgid "has already been linked to another vulnerability"
msgstr ""
@@ -24656,9 +25212,6 @@ msgstr[1] ""
msgid "merged %{time_ago}"
msgstr ""
-msgid "milestone should belong either to a project or a group."
-msgstr ""
-
msgid "missing"
msgstr ""
@@ -24995,6 +25548,9 @@ msgstr ""
msgid "no contributions"
msgstr ""
+msgid "no expiration"
+msgstr ""
+
msgid "no one can merge"
msgstr ""
@@ -25047,6 +25603,9 @@ msgstr ""
msgid "pending removal"
msgstr ""
+msgid "per day"
+msgstr ""
+
msgid "pipeline"
msgstr ""
@@ -25075,6 +25634,12 @@ msgstr[1] ""
msgid "project avatar"
msgstr ""
+msgid "projects"
+msgstr ""
+
+msgid "push to your repository, create pipelines, create issues or add comments. To reduce storage capacity, delete unused repositories, artifacts, wikis, issues, and pipelines."
+msgstr ""
+
msgid "quick actions"
msgstr ""
@@ -25221,11 +25786,6 @@ msgstr ""
msgid "this document"
msgstr ""
-msgid "thread resolved"
-msgid_plural "threads resolved"
-msgstr[0] ""
-msgstr[1] ""
-
msgid "to help your contributors communicate effectively!"
msgstr ""
@@ -25280,6 +25840,9 @@ msgstr ""
msgid "view the blob"
msgstr ""
+msgid "vulnerability|Add a comment"
+msgstr ""
+
msgid "vulnerability|Add a comment or reason for dismissal"
msgstr ""
diff --git a/locale/fa_IR/gitlab.po b/locale/fa_IR/gitlab.po
index 36a4ffa0038..0fe3aefd655 100644
--- a/locale/fa_IR/gitlab.po
+++ b/locale/fa_IR/gitlab.po
@@ -12,7 +12,7 @@ msgstr ""
"X-Crowdin-Project: gitlab-ee\n"
"X-Crowdin-Language: fa\n"
"X-Crowdin-File: /master/locale/gitlab.pot\n"
-"PO-Revision-Date: 2020-04-15 00:26\n"
+"PO-Revision-Date: 2020-05-05 21:31\n"
msgid " %{start} to %{end}"
msgstr ""
@@ -212,6 +212,11 @@ msgid_plural "%d tags"
msgstr[0] ""
msgstr[1] ""
+msgid "%d unresolved thread"
+msgid_plural "%d unresolved threads"
+msgstr[0] ""
+msgstr[1] ""
+
msgid "%d vulnerability dismissed"
msgid_plural "%d vulnerabilities dismissed"
msgstr[0] ""
@@ -371,6 +376,9 @@ msgstr ""
msgid "%{mrText}, this issue will be closed automatically."
msgstr ""
+msgid "%{namespace_name} is now read-only. You cannot: %{base_message}"
+msgstr ""
+
msgid "%{name} contained %{resultsString}"
msgstr ""
@@ -383,6 +391,11 @@ msgstr ""
msgid "%{name}'s avatar"
msgstr ""
+msgid "%{no_of_days} day"
+msgid_plural "%{no_of_days} days"
+msgstr[0] ""
+msgstr[1] ""
+
msgid "%{number_commits_behind} commits behind %{default_branch}, %{number_commits_ahead} commits ahead"
msgstr ""
@@ -506,6 +519,9 @@ msgstr[1] ""
msgid "%{text} is available"
msgstr ""
+msgid "%{timebox_name} should belong either to a project or a group."
+msgstr ""
+
msgid "%{title} %{operator} %{threshold}"
msgstr ""
@@ -781,12 +797,18 @@ msgstr ""
msgid "<code>\"johnsmith@example.com\": \"johnsmith@example.com\"</code> will add \"By <a href=\"#\">johnsmith@example.com</a>\" to all issues and comments originally created by johnsmith@example.com. By default, the email address or username is masked to ensure the user's privacy. Use this option if you want to show the full email address."
msgstr ""
+msgid "<namespace / project>"
+msgstr ""
+
msgid "<no name set>"
msgstr ""
msgid "<no scopes selected>"
msgstr ""
+msgid "<project name>"
+msgstr ""
+
msgid "<strong>%{group_name}</strong> group members"
msgstr ""
@@ -820,6 +842,9 @@ msgstr ""
msgid "A basic page and serverless function that uses AWS Lambda, AWS API Gateway, and GitLab Pages"
msgstr ""
+msgid "A complete DevOps platform"
+msgstr ""
+
msgid "A default branch cannot be chosen for an empty project."
msgstr ""
@@ -1036,6 +1061,12 @@ msgstr ""
msgid "AccessTokens|reset it"
msgstr ""
+msgid "AccessibilityReport|Accessibility report artifact not found"
+msgstr ""
+
+msgid "AccessibilityReport|Failed to retrieve accessibility report"
+msgstr ""
+
msgid "AccessibilityReport|Learn More"
msgstr ""
@@ -1045,6 +1076,9 @@ msgstr ""
msgid "AccessibilityReport|New"
msgstr ""
+msgid "AccessibilityReport|The accessibility scanning found an error of the following type: %{code}"
+msgstr ""
+
msgid "Account"
msgstr ""
@@ -1287,6 +1321,9 @@ msgstr ""
msgid "Added at"
msgstr ""
+msgid "Added for this merge request"
+msgstr ""
+
msgid "Added in this version"
msgstr ""
@@ -1658,6 +1695,72 @@ msgid_plural "Alerts"
msgstr[0] ""
msgstr[1] ""
+msgid "Alert Details"
+msgstr ""
+
+msgid "AlertManagement|Acknowledged"
+msgstr ""
+
+msgid "AlertManagement|Alert"
+msgstr ""
+
+msgid "AlertManagement|Authorize external service"
+msgstr ""
+
+msgid "AlertManagement|Display alerts from all your monitoring tools directly within GitLab. Streamline the investigation of your alerts and the escalation of alerts to incidents."
+msgstr ""
+
+msgid "AlertManagement|End time"
+msgstr ""
+
+msgid "AlertManagement|End time:"
+msgstr ""
+
+msgid "AlertManagement|Events"
+msgstr ""
+
+msgid "AlertManagement|Events:"
+msgstr ""
+
+msgid "AlertManagement|Full Alert Details"
+msgstr ""
+
+msgid "AlertManagement|More information"
+msgstr ""
+
+msgid "AlertManagement|No alerts available to display. If you think you're seeing this message in error, refresh the page."
+msgstr ""
+
+msgid "AlertManagement|No alerts to display."
+msgstr ""
+
+msgid "AlertManagement|Overview"
+msgstr ""
+
+msgid "AlertManagement|Resolved"
+msgstr ""
+
+msgid "AlertManagement|Severity"
+msgstr ""
+
+msgid "AlertManagement|Start time"
+msgstr ""
+
+msgid "AlertManagement|Start time:"
+msgstr ""
+
+msgid "AlertManagement|Status"
+msgstr ""
+
+msgid "AlertManagement|Surface alerts in GitLab"
+msgstr ""
+
+msgid "AlertManagement|There was an error displaying the alerts. Confirm your endpoint's configuration details to ensure alerts appear."
+msgstr ""
+
+msgid "AlertManagement|Triggered"
+msgstr ""
+
msgid "AlertService|%{linkStart}Learn more%{linkEnd} about configuring this endpoint to receive alerts."
msgstr ""
@@ -1727,6 +1830,9 @@ msgstr ""
msgid "All security scans are enabled because %{linkStart}Auto DevOps%{linkEnd} is enabled on this project"
msgstr ""
+msgid "All threads resolved"
+msgstr ""
+
msgid "All users"
msgstr ""
@@ -1745,6 +1851,9 @@ msgstr ""
msgid "Allow only the selected protocols to be used for Git access."
msgstr ""
+msgid "Allow owners to manage default branch protection per group"
+msgstr ""
+
msgid "Allow owners to manually add users outside of LDAP"
msgstr ""
@@ -1853,6 +1962,9 @@ msgstr ""
msgid "An error occurred fetching the dropdown data."
msgstr ""
+msgid "An error occurred fetching the project authors."
+msgstr ""
+
msgid "An error occurred previewing the blob"
msgstr ""
@@ -1934,6 +2046,9 @@ msgstr ""
msgid "An error occurred while fetching sidebar data"
msgstr ""
+msgid "An error occurred while fetching terraform reports."
+msgstr ""
+
msgid "An error occurred while fetching the Service Desk address."
msgstr ""
@@ -2171,6 +2286,9 @@ msgstr ""
msgid "Any"
msgstr ""
+msgid "Any Author"
+msgstr ""
+
msgid "Any Label"
msgstr ""
@@ -2386,10 +2504,10 @@ msgstr ""
msgid "Are you sure that you want to unarchive this project?"
msgstr ""
-msgid "Are you sure you want to cancel creating this comment?"
+msgid "Are you sure you want to cancel editing this comment?"
msgstr ""
-msgid "Are you sure you want to cancel editing this comment?"
+msgid "Are you sure you want to close this blocked issue?"
msgstr ""
msgid "Are you sure you want to delete %{name}?"
@@ -2419,6 +2537,9 @@ msgstr ""
msgid "Are you sure you want to deploy this environment?"
msgstr ""
+msgid "Are you sure you want to discard this comment?"
+msgstr ""
+
msgid "Are you sure you want to erase this build?"
msgstr ""
@@ -2595,6 +2716,9 @@ msgstr ""
msgid "At least one approval from a code owner is required to change files matching the respective CODEOWNER rules."
msgstr ""
+msgid "At least one logging option is required to be enabled"
+msgstr ""
+
msgid "At least one of group_id or project_id must be specified"
msgstr ""
@@ -2633,6 +2757,27 @@ msgstr ""
msgid "AuditEvents|Target"
msgstr ""
+msgid "AuditLogs|(removed)"
+msgstr ""
+
+msgid "AuditLogs|Action"
+msgstr ""
+
+msgid "AuditLogs|Author"
+msgstr ""
+
+msgid "AuditLogs|Date"
+msgstr ""
+
+msgid "AuditLogs|IP Address"
+msgstr ""
+
+msgid "AuditLogs|Object"
+msgstr ""
+
+msgid "AuditLogs|Target"
+msgstr ""
+
msgid "Aug"
msgstr ""
@@ -2915,9 +3060,6 @@ msgstr ""
msgid "BambooService|You must set up automatic revision labeling and a repository trigger in Bamboo."
msgstr ""
-msgid "Batch operations"
-msgstr ""
-
msgid "BatchComments|Delete all pending comments"
msgstr ""
@@ -3047,6 +3189,9 @@ msgstr ""
msgid "Boards"
msgstr ""
+msgid "Boards and Board Lists"
+msgstr ""
+
msgid "Boards|Collapse"
msgstr ""
@@ -3266,6 +3411,9 @@ msgstr ""
msgid "Buy GitLab Enterprise Edition"
msgstr ""
+msgid "Buy more Pipeline minutes"
+msgstr ""
+
msgid "By %{user_name}"
msgstr ""
@@ -3386,6 +3534,9 @@ msgstr ""
msgid "Can't scan the code?"
msgstr ""
+msgid "Can't update snippet: %{err}"
+msgstr ""
+
msgid "Canary"
msgstr ""
@@ -3554,6 +3705,9 @@ msgstr ""
msgid "Changing group path can have unintended side effects."
msgstr ""
+msgid "Charts"
+msgstr ""
+
msgid "Charts can't be displayed as the request for data has timed out. %{documentationLink}"
msgstr ""
@@ -3590,9 +3744,6 @@ msgstr ""
msgid "ChatMessage|and [%{count} more](%{pipeline_failed_jobs_url})"
msgstr ""
-msgid "ChatMessage|failed"
-msgstr ""
-
msgid "ChatMessage|has failed"
msgstr ""
@@ -3608,9 +3759,6 @@ msgstr ""
msgid "ChatMessage|in %{project_link}"
msgstr ""
-msgid "ChatMessage|passed"
-msgstr ""
-
msgid "Check again"
msgstr ""
@@ -4061,9 +4209,6 @@ msgstr ""
msgid "Click any <strong>project name</strong> in the project list below to navigate to the project milestone."
msgstr ""
-msgid "Click here"
-msgstr ""
-
msgid "Click the <strong>Download</strong> button and wait for downloading to complete."
msgstr ""
@@ -4451,6 +4596,12 @@ msgstr ""
msgid "ClusterIntegration|Fetching zones"
msgstr ""
+msgid "ClusterIntegration|Fluentd"
+msgstr ""
+
+msgid "ClusterIntegration|Fluentd is an open source data collector, which lets you unify the data collection and consumption for a better use and understanding of data. It requires at least one of the following logs to be successfully installed."
+msgstr ""
+
msgid "ClusterIntegration|GitLab Integration"
msgstr ""
@@ -4757,6 +4908,15 @@ msgstr ""
msgid "ClusterIntegration|Request to begin uninstalling failed"
msgstr ""
+msgid "ClusterIntegration|SIEM Hostname"
+msgstr ""
+
+msgid "ClusterIntegration|SIEM Port"
+msgstr ""
+
+msgid "ClusterIntegration|SIEM Protocol"
+msgstr ""
+
msgid "ClusterIntegration|Save changes"
msgstr ""
@@ -4853,6 +5013,12 @@ msgstr ""
msgid "ClusterIntegration|Select zone to choose machine type"
msgstr ""
+msgid "ClusterIntegration|Send Cilium Logs"
+msgstr ""
+
+msgid "ClusterIntegration|Send ModSecurity Logs"
+msgstr ""
+
msgid "ClusterIntegration|Service Token"
msgstr ""
@@ -5281,18 +5447,33 @@ msgstr ""
msgid "Compliance framework (optional)"
msgstr ""
+msgid "ComplianceFramework|GDPR"
+msgstr ""
+
msgid "ComplianceFramework|GDPR - General Data Protection Regulation"
msgstr ""
+msgid "ComplianceFramework|HIPAA"
+msgstr ""
+
msgid "ComplianceFramework|HIPAA - Health Insurance Portability and Accountability Act"
msgstr ""
+msgid "ComplianceFramework|PCI-DSS"
+msgstr ""
+
msgid "ComplianceFramework|PCI-DSS - Payment Card Industry-Data Security Standard"
msgstr ""
+msgid "ComplianceFramework|SOC 2"
+msgstr ""
+
msgid "ComplianceFramework|SOC 2 - Service Organization Control 2"
msgstr ""
+msgid "ComplianceFramework|SOX"
+msgstr ""
+
msgid "ComplianceFramework|SOX - Sarbanes-Oxley"
msgstr ""
@@ -5440,9 +5621,15 @@ msgstr ""
msgid "Container repositories sync capacity"
msgstr ""
+msgid "ContainerRegistry| Please visit the %{linkStart}administration settings%{linkEnd} to enable this feature."
+msgstr ""
+
msgid "ContainerRegistry|%{imageName} tags"
msgstr ""
+msgid "ContainerRegistry|%{title} was successfully scheduled for deletion"
+msgstr ""
+
msgid "ContainerRegistry|Automatically remove extra images that aren't designed to be kept."
msgstr ""
@@ -5455,6 +5642,9 @@ msgstr ""
msgid "ContainerRegistry|Container Registry"
msgstr ""
+msgid "ContainerRegistry|Container Registry tag expiration and retention policy is disabled"
+msgstr ""
+
msgid "ContainerRegistry|Copy build command"
msgstr ""
@@ -5464,18 +5654,12 @@ msgstr ""
msgid "ContainerRegistry|Copy push command"
msgstr ""
-msgid "ContainerRegistry|Currently, the Container Registry tag expiration feature is not available for projects created before GitLab version 12.8. For updates and more information, visit Issue %{linkStart}#196124%{linkEnd}"
-msgstr ""
-
msgid "ContainerRegistry|Docker connection error"
msgstr ""
msgid "ContainerRegistry|Docker tag expiration policy is %{toggleStatus}"
msgstr ""
-msgid "ContainerRegistry|Docker tags with names matching this regex pattern will expire:"
-msgstr ""
-
msgid "ContainerRegistry|Edit Settings"
msgstr ""
@@ -5497,9 +5681,6 @@ msgstr ""
msgid "ContainerRegistry|Image ID"
msgstr ""
-msgid "ContainerRegistry|Image deleted successfully"
-msgstr ""
-
msgid "ContainerRegistry|Keep and protect the images that matter most."
msgstr ""
@@ -5515,12 +5696,24 @@ msgstr ""
msgid "ContainerRegistry|Number of tags to retain:"
msgstr ""
+msgid "ContainerRegistry|Please contact your administrator."
+msgstr ""
+
msgid "ContainerRegistry|Push an image"
msgstr ""
msgid "ContainerRegistry|Quick Start"
msgstr ""
+msgid "ContainerRegistry|Regular expressions such as %{codeStart}.*-test%{codeEnd} or %{codeStart}dev-.*%{codeEnd} are supported"
+msgstr ""
+
+msgid "ContainerRegistry|Regular expressions such as %{codeStart}.*-test%{codeEnd} or %{codeStart}dev-.*%{codeEnd} are supported. To select all tags, use %{codeStart}.*%{codeEnd}"
+msgstr ""
+
+msgid "ContainerRegistry|Remember to run %{docLinkStart}garbage collection%{docLinkEnd} to remove the stale data from storage."
+msgstr ""
+
msgid "ContainerRegistry|Remove repository"
msgstr ""
@@ -5535,22 +5728,22 @@ msgstr[1] ""
msgid "ContainerRegistry|Retention policy has been Enabled"
msgstr ""
-msgid "ContainerRegistry|Something went wrong while deleting the image."
+msgid "ContainerRegistry|Something went wrong while fetching the expiration policy."
msgstr ""
-msgid "ContainerRegistry|Something went wrong while deleting the tag."
+msgid "ContainerRegistry|Something went wrong while fetching the repository list."
msgstr ""
-msgid "ContainerRegistry|Something went wrong while deleting the tags."
+msgid "ContainerRegistry|Something went wrong while fetching the tags list."
msgstr ""
-msgid "ContainerRegistry|Something went wrong while fetching the expiration policy."
+msgid "ContainerRegistry|Something went wrong while marking the tag for deletion."
msgstr ""
-msgid "ContainerRegistry|Something went wrong while fetching the packages list."
+msgid "ContainerRegistry|Something went wrong while marking the tags for deletion."
msgstr ""
-msgid "ContainerRegistry|Something went wrong while fetching the tags list."
+msgid "ContainerRegistry|Something went wrong while scheduling %{title} for deletion. Please try again."
msgstr ""
msgid "ContainerRegistry|Something went wrong while updating the expiration policy."
@@ -5559,16 +5752,25 @@ msgstr ""
msgid "ContainerRegistry|Tag"
msgstr ""
-msgid "ContainerRegistry|Tag deleted successfully"
-msgstr ""
-
msgid "ContainerRegistry|Tag expiration policy"
msgstr ""
msgid "ContainerRegistry|Tag expiration policy is designed to:"
msgstr ""
-msgid "ContainerRegistry|Tags deleted successfully"
+msgid "ContainerRegistry|Tag successfully marked for deletion."
+msgstr ""
+
+msgid "ContainerRegistry|Tags successfully marked for deletion."
+msgstr ""
+
+msgid "ContainerRegistry|Tags with names matching this regex pattern will %{italicStart}be preserved:%{italicEnd}"
+msgstr ""
+
+msgid "ContainerRegistry|Tags with names matching this regex pattern will %{italicStart}expire:%{italicEnd}"
+msgstr ""
+
+msgid "ContainerRegistry|The Container Registry tag expiration and retention policies for this project have not been enabled."
msgstr ""
msgid "ContainerRegistry|The last tag related to this image was recently removed. This empty image and any associated data will be automatically removed as part of the regular Garbage Collection process. If you have any questions, contact your administrator."
@@ -5589,16 +5791,16 @@ msgstr ""
msgid "ContainerRegistry|There are no container images stored for this project"
msgstr ""
-msgid "ContainerRegistry|This Registry contains deleted image tag data. Remember to run %{docLinkStart}garbage collection%{docLinkEnd} to remove the stale data from storage."
+msgid "ContainerRegistry|There was an error during the deletion of this image repository, please try again."
msgstr ""
msgid "ContainerRegistry|This image has no active tags"
msgstr ""
-msgid "ContainerRegistry|We are having trouble connecting to Docker, which could be due to an issue with your project name or path. %{docLinkStart}More Information%{docLinkEnd}"
+msgid "ContainerRegistry|This image repository is scheduled for deletion"
msgstr ""
-msgid "ContainerRegistry|Wildcards such as %{codeStart}.*-stable%{codeEnd} or %{codeStart}production/.*%{codeEnd} are supported. To select all tags, use %{codeStart}.*%{codeEnd}"
+msgid "ContainerRegistry|We are having trouble connecting to Docker, which could be due to an issue with your project name or path. %{docLinkStart}More Information%{docLinkEnd}"
msgstr ""
msgid "ContainerRegistry|With the Container Registry, every project can have its own space to store its Docker images. %{docLinkStart}More Information%{docLinkEnd}"
@@ -5853,7 +6055,7 @@ msgstr ""
msgid "Could not delete chat nickname %{chat_name}."
msgstr ""
-msgid "Could not find design"
+msgid "Could not find design."
msgstr ""
msgid "Could not remove the trigger."
@@ -5868,6 +6070,9 @@ msgstr ""
msgid "Could not revoke personal access token %{personal_access_token_name}."
msgstr ""
+msgid "Could not revoke project access token %{project_access_token_name}."
+msgstr ""
+
msgid "Could not save group ID"
msgstr ""
@@ -5925,6 +6130,9 @@ msgstr ""
msgid "Create a new branch"
msgstr ""
+msgid "Create a new deploy key for this project"
+msgstr ""
+
msgid "Create a new file as there are no files yet. Afterwards, you'll be able to commit your changes."
msgstr ""
@@ -6030,6 +6238,9 @@ msgstr ""
msgid "Create requirement"
msgstr ""
+msgid "Create snippet"
+msgstr ""
+
msgid "Create wildcard: %{searchTerm}"
msgstr ""
@@ -6051,6 +6262,9 @@ msgstr ""
msgid "Created"
msgstr ""
+msgid "Created %{timestamp}"
+msgstr ""
+
msgid "Created At"
msgstr ""
@@ -6407,6 +6621,9 @@ msgstr ""
msgid "CycleAnalytics|stage dropdown"
msgstr ""
+msgid "DAG"
+msgstr ""
+
msgid "DNS"
msgstr ""
@@ -6773,6 +6990,9 @@ msgstr ""
msgid "Deploy key was successfully updated."
msgstr ""
+msgid "Deploy keys allow read-only or read-write (if enabled) access to your repository. Deploy keys can be used for CI, staging or production servers. You can create a deploy key or add an existing one."
+msgstr ""
+
msgid "Deploy progress not found. To see pods, ensure your environment matches %{linkStart}deploy board criteria%{linkEnd}."
msgstr ""
@@ -6926,6 +7146,9 @@ msgstr ""
msgid "Deploying to"
msgstr ""
+msgid "Deployment Frequency"
+msgstr ""
+
msgid "Deployment|API"
msgstr ""
@@ -7444,9 +7667,6 @@ msgstr ""
msgid "Edit environment"
msgstr ""
-msgid "Edit epic description"
-msgstr ""
-
msgid "Edit file"
msgstr ""
@@ -8068,9 +8288,6 @@ msgstr ""
msgid "Epics"
msgstr ""
-msgid "Epics (Ultimate / Gold license only)"
-msgstr ""
-
msgid "Epics Roadmap"
msgstr ""
@@ -8080,6 +8297,9 @@ msgstr ""
msgid "Epics let you manage your portfolio of projects more efficiently and with less effort"
msgstr ""
+msgid "Epics, Issues, and Merge Requests"
+msgstr ""
+
msgid "Epics|Add an epic"
msgstr ""
@@ -8131,6 +8351,9 @@ msgstr ""
msgid "Epics|Something went wrong while fetching group epics."
msgstr ""
+msgid "Epics|Something went wrong while moving item."
+msgstr ""
+
msgid "Epics|Something went wrong while ordering item."
msgstr ""
@@ -8185,6 +8408,9 @@ msgstr ""
msgid "Error fetching network graph."
msgstr ""
+msgid "Error fetching payload data."
+msgstr ""
+
msgid "Error fetching projects"
msgstr ""
@@ -8194,9 +8420,6 @@ msgstr ""
msgid "Error fetching the dependency list. Please check your network connection and try again."
msgstr ""
-msgid "Error fetching usage ping data."
-msgstr ""
-
msgid "Error loading branch data. Please try again."
msgstr ""
@@ -8338,6 +8561,9 @@ msgstr ""
msgid "Errors"
msgstr ""
+msgid "Errors:"
+msgstr ""
+
msgid "Estimated"
msgstr ""
@@ -8542,12 +8768,18 @@ msgstr ""
msgid "Export as CSV"
msgstr ""
+msgid "Export group"
+msgstr ""
+
msgid "Export issues"
msgstr ""
msgid "Export project"
msgstr ""
+msgid "Export this group with all related data to a new GitLab instance. Once complete, you can import the data file from the \"New Group\" page."
+msgstr ""
+
msgid "Export this project with all its related data in order to move your project to a new GitLab instance. Once the export is finished, you can import the file from the \"New Project\" page."
msgstr ""
@@ -8617,9 +8849,6 @@ msgstr ""
msgid "Failed Jobs"
msgstr ""
-msgid "Failed create wiki"
-msgstr ""
-
msgid "Failed to add a Zoom meeting"
msgstr ""
@@ -8659,6 +8888,9 @@ msgstr ""
msgid "Failed to create resources"
msgstr ""
+msgid "Failed to create wiki"
+msgstr ""
+
msgid "Failed to delete board. Please try again."
msgstr ""
@@ -8668,7 +8900,7 @@ msgstr ""
msgid "Failed to enqueue the rebase operation, possibly due to a long-lived transaction. Try again later."
msgstr ""
-msgid "Failed to find import label for jira import."
+msgid "Failed to find import label for Jira import."
msgstr ""
msgid "Failed to get ref."
@@ -8692,6 +8924,12 @@ msgstr ""
msgid "Failed to load groups & users."
msgstr ""
+msgid "Failed to load labels. Please try again."
+msgstr ""
+
+msgid "Failed to load milestones. Please try again."
+msgstr ""
+
msgid "Failed to load related branches"
msgstr ""
@@ -9067,7 +9305,10 @@ msgstr ""
msgid "Filter by milestone name"
msgstr ""
-msgid "Filter by name..."
+msgid "Filter by name"
+msgstr ""
+
+msgid "Filter by status"
msgstr ""
msgid "Filter by two-factor authentication"
@@ -9079,12 +9320,18 @@ msgstr ""
msgid "Filter projects"
msgstr ""
+msgid "Filter results"
+msgstr ""
+
msgid "Filter results by group"
msgstr ""
msgid "Filter results by project"
msgstr ""
+msgid "Filter results..."
+msgstr ""
+
msgid "Filter your projects by name"
msgstr ""
@@ -9373,10 +9620,10 @@ msgstr ""
msgid "GeoNodes|Checksummed"
msgstr ""
-msgid "GeoNodes|Container repositories"
+msgid "GeoNodes|Consult Geo troubleshooting information"
msgstr ""
-msgid "GeoNodes|Data is out of date from %{timeago}"
+msgid "GeoNodes|Container repositories"
msgstr ""
msgid "GeoNodes|Data replication lag"
@@ -9418,6 +9665,9 @@ msgstr ""
msgid "GeoNodes|Last event ID seen from primary"
msgstr ""
+msgid "GeoNodes|Learn more about Geo node statuses"
+msgstr ""
+
msgid "GeoNodes|Loading nodes"
msgstr ""
@@ -9433,6 +9683,9 @@ msgstr ""
msgid "GeoNodes|Node was successfully removed."
msgstr ""
+msgid "GeoNodes|Node's status was updated %{timeAgo}."
+msgstr ""
+
msgid "GeoNodes|Not checksummed"
msgstr ""
@@ -9463,7 +9716,10 @@ msgstr ""
msgid "GeoNodes|Repository verification progress"
msgstr ""
-msgid "GeoNodes|Selective"
+msgid "GeoNodes|Selective (%{syncLabel})"
+msgstr ""
+
+msgid "GeoNodes|Selective synchronization"
msgstr ""
msgid "GeoNodes|Something went wrong while changing node status"
@@ -9490,6 +9746,9 @@ msgstr ""
msgid "GeoNodes|Unverified"
msgstr ""
+msgid "GeoNodes|Updated %{timeAgo}"
+msgstr ""
+
msgid "GeoNodes|Used slots"
msgstr ""
@@ -9529,16 +9788,16 @@ msgstr ""
msgid "Geo|All"
msgstr ""
-msgid "Geo|All projects"
+msgid "Geo|All %{replicable_type}"
msgstr ""
-msgid "Geo|All projects are being scheduled for re-sync"
+msgid "Geo|All projects"
msgstr ""
-msgid "Geo|All projects are being scheduled for re-verify"
+msgid "Geo|All projects are being scheduled for resync"
msgstr ""
-msgid "Geo|Batch operations"
+msgid "Geo|All projects are being scheduled for reverify"
msgstr ""
msgid "Geo|Could not remove tracking entry for an existing project."
@@ -9550,9 +9809,15 @@ msgstr ""
msgid "Geo|Failed"
msgstr ""
+msgid "Geo|Filter by status"
+msgstr ""
+
msgid "Geo|Geo Status"
msgstr ""
+msgid "Geo|In progress"
+msgstr ""
+
msgid "Geo|In sync"
msgstr ""
@@ -9577,9 +9842,6 @@ msgstr ""
msgid "Geo|Not synced yet"
msgstr ""
-msgid "Geo|Pending"
-msgstr ""
-
msgid "Geo|Pending synchronization"
msgstr ""
@@ -9616,7 +9878,7 @@ msgstr ""
msgid "Geo|Resync"
msgstr ""
-msgid "Geo|Resync all projects"
+msgid "Geo|Resync all"
msgstr ""
msgid "Geo|Retry count"
@@ -9625,7 +9887,7 @@ msgstr ""
msgid "Geo|Reverify"
msgstr ""
-msgid "Geo|Reverify all projects"
+msgid "Geo|Reverify all"
msgstr ""
msgid "Geo|Status"
@@ -9967,9 +10229,6 @@ msgstr ""
msgid "Go to file"
msgstr ""
-msgid "Go to file (MRs only)"
-msgstr ""
-
msgid "Go to file permalink (while viewing a file)"
msgstr ""
@@ -10414,6 +10673,9 @@ msgstr ""
msgid "GroupSettings|Disable group mentions"
msgstr ""
+msgid "GroupSettings|Export group"
+msgstr ""
+
msgid "GroupSettings|If the parent group's visibility is lower than the group current visibility, visibility levels for subgroups and projects will be changed to match the new parent group's visibility."
msgstr ""
@@ -10492,6 +10754,9 @@ msgstr ""
msgid "Groups (%{count})"
msgstr ""
+msgid "Groups (%{groups})"
+msgstr ""
+
msgid "Groups can also be nested by creating %{subgroup_docs_link_start}subgroups%{subgroup_docs_link_end}."
msgstr ""
@@ -10757,15 +11022,12 @@ msgstr ""
msgid "ID:"
msgstr ""
-msgid "IDE|Allow live previews of JavaScript projects in the Web IDE using CodeSandbox client side evaluation."
+msgid "IDE|Allow live previews of JavaScript projects in the Web IDE using CodeSandbox Live Preview."
msgstr ""
msgid "IDE|Back"
msgstr ""
-msgid "IDE|Client side evaluation"
-msgstr ""
-
msgid "IDE|Commit"
msgstr ""
@@ -10829,6 +11091,9 @@ msgstr ""
msgid "If any job surpasses this timeout threshold, it will be marked as failed. Human readable time input language is accepted like \"1 hour\". Values without specification represent seconds."
msgstr ""
+msgid "If blank, set allowable lifetime to %{instance_level_policy_in_words}, as defined by the instance admin. Once set, existing tokens for users in this group may be revoked."
+msgstr ""
+
msgid "If checked, group owners can manage LDAP group links and LDAP member overrides"
msgstr ""
@@ -10868,6 +11133,9 @@ msgstr ""
msgid "If you lose your recovery codes you can generate new ones, invalidating all previous codes."
msgstr ""
+msgid "If you reach 100%% storage capacity, you will not be able to: %{base_message}"
+msgstr ""
+
msgid "If your HTTP repository is not publicly accessible, add your credentials."
msgstr ""
@@ -10979,6 +11247,9 @@ msgstr ""
msgid "Import repository"
msgstr ""
+msgid "Import started by: %{importInitiator}"
+msgstr ""
+
msgid "Import tasks"
msgstr ""
@@ -11045,6 +11316,9 @@ msgstr ""
msgid "In order to tailor your experience with GitLab we<br>would like to know a bit more about you."
msgstr ""
+msgid "In progress"
+msgstr ""
+
msgid "In the next step, you'll be able to select the projects you want to import."
msgstr ""
@@ -11392,15 +11666,15 @@ msgstr ""
msgid "Issues"
msgstr ""
-msgid "Issues / Merge Requests"
-msgstr ""
-
msgid "Issues Analytics"
msgstr ""
msgid "Issues Rate Limits"
msgstr ""
+msgid "Issues and Merge Requests"
+msgstr ""
+
msgid "Issues can be bugs, tasks or ideas to be discussed. Also, issues are searchable and filterable."
msgstr ""
@@ -11422,10 +11696,10 @@ msgstr ""
msgid "IssuesAnalytics|Avg/Month:"
msgstr ""
-msgid "IssuesAnalytics|Issues created"
+msgid "IssuesAnalytics|Issues opened"
msgstr ""
-msgid "IssuesAnalytics|Issues created per month"
+msgid "IssuesAnalytics|Issues opened per month"
msgstr ""
msgid "IssuesAnalytics|Last 12 months"
@@ -11479,6 +11753,9 @@ msgstr ""
msgid "Jira integration not configured."
msgstr ""
+msgid "Jira project: %{importProject}"
+msgstr ""
+
msgid "JiraService|Events for %{noteable_model_name} are disabled."
msgstr ""
@@ -11868,6 +12145,9 @@ msgstr ""
msgid "Lead"
msgstr ""
+msgid "Lead Time"
+msgstr ""
+
msgid "Learn GitLab"
msgstr ""
@@ -11880,6 +12160,9 @@ msgstr ""
msgid "Learn how to %{no_packages_link_start}publish and share your packages%{no_packages_link_end} with GitLab."
msgstr ""
+msgid "Learn how to enable synchronization"
+msgstr ""
+
msgid "Learn more"
msgstr ""
@@ -12211,9 +12494,6 @@ msgstr ""
msgid "Live preview"
msgstr ""
-msgid "Load more vulnerabilities"
-msgstr ""
-
msgid "Loading"
msgstr ""
@@ -12364,9 +12644,6 @@ msgstr ""
msgid "Manage"
msgstr ""
-msgid "Manage Git repositories with fine-grained access controls that keep your code secure. Perform code reviews and enhance collaboration with merge requests. Each project can also have an issue tracker and a wiki."
-msgstr ""
-
msgid "Manage Web IDE features"
msgstr ""
@@ -12397,6 +12674,9 @@ msgstr ""
msgid "Manage your license"
msgstr ""
+msgid "Managed Account"
+msgstr ""
+
msgid "Manifest"
msgstr ""
@@ -12661,6 +12941,9 @@ msgstr ""
msgid "Merge Requests in Review"
msgstr ""
+msgid "Merge automatically (%{strategy})"
+msgstr ""
+
msgid "Merge commit message"
msgstr ""
@@ -12814,6 +13097,12 @@ msgstr ""
msgid "Merged branches are being deleted. This can take some time depending on the number of branches. Please refresh the page to see changes."
msgstr ""
+msgid "Merged this merge request."
+msgstr ""
+
+msgid "Merges this merge request immediately."
+msgstr ""
+
msgid "Merges this merge request when the pipeline succeeds."
msgstr ""
@@ -12921,12 +13210,15 @@ msgid_plural "Metrics|Edit metrics"
msgstr[0] ""
msgstr[1] ""
-msgid "Metrics|Environment"
+msgid "Metrics|Expand panel"
msgstr ""
msgid "Metrics|For grouping similar metrics"
msgstr ""
+msgid "Metrics|Go back (Esc)"
+msgstr ""
+
msgid "Metrics|Invalid time range, please verify."
msgstr ""
@@ -12957,9 +13249,6 @@ msgstr ""
msgid "Metrics|Refresh dashboard"
msgstr ""
-msgid "Metrics|Show last"
-msgstr ""
-
msgid "Metrics|There was an error creating the dashboard."
msgstr ""
@@ -13038,6 +13327,9 @@ msgstr ""
msgid "Microsoft Azure"
msgstr ""
+msgid "Middleman project with Static Site Editor support"
+msgstr ""
+
msgid "Migrated %{success_count}/%{total_count} files."
msgstr ""
@@ -13483,10 +13775,10 @@ msgstr ""
msgid "Next"
msgstr ""
-msgid "Next file in diff (MRs only)"
+msgid "Next file in diff"
msgstr ""
-msgid "Next unresolved discussion (MRs only)"
+msgid "Next unresolved discussion"
msgstr ""
msgid "Nickname"
@@ -13546,6 +13838,9 @@ msgstr ""
msgid "No changes between %{ref_start}%{source_branch}%{ref_end} and %{ref_start}%{target_branch}%{ref_end}"
msgstr ""
+msgid "No child epics match applied filters"
+msgstr ""
+
msgid "No connection could be made to a Gitaly Server, please check your logs!"
msgstr ""
@@ -13684,15 +13979,6 @@ msgstr ""
msgid "No thanks, don't show this again"
msgstr ""
-msgid "No vulnerabilities found for this group"
-msgstr ""
-
-msgid "No vulnerabilities found for this pipeline"
-msgstr ""
-
-msgid "No vulnerabilities found for this project"
-msgstr ""
-
msgid "No vulnerabilities present"
msgstr ""
@@ -13816,6 +14102,9 @@ msgstr ""
msgid "Nothing to preview."
msgstr ""
+msgid "Nothing to synchronize"
+msgstr ""
+
msgid "Notification events"
msgstr ""
@@ -13987,6 +14276,9 @@ msgstr ""
msgid "Once removed, the fork relationship cannot be restored and you will no longer be able to send merge requests to the source."
msgstr ""
+msgid "Once the exported file is ready you can download it from this page."
+msgstr ""
+
msgid "Once the exported file is ready, you will receive a notification email with a download link, or you can download it from this page."
msgstr ""
@@ -14088,9 +14380,6 @@ msgstr ""
msgid "Open sidebar"
msgstr ""
-msgid "Open source software to collaborate on code"
-msgstr ""
-
msgid "Open: %{openIssuesCount}"
msgstr ""
@@ -14193,6 +14482,9 @@ msgstr ""
msgid "OutdatedBrowser|You can provide feedback %{feedback_link_start}on this issue%{feedback_link_end} or via your usual support channels."
msgstr ""
+msgid "Overridden"
+msgstr ""
+
msgid "Overview"
msgstr ""
@@ -14244,6 +14536,9 @@ msgstr ""
msgid "PackageRegistry|Conan Command"
msgstr ""
+msgid "PackageRegistry|Copy .pypirc content"
+msgstr ""
+
msgid "PackageRegistry|Copy Conan Command"
msgstr ""
@@ -14265,6 +14560,9 @@ msgstr ""
msgid "PackageRegistry|Copy NuGet Setup Command"
msgstr ""
+msgid "PackageRegistry|Copy Pip command"
+msgstr ""
+
msgid "PackageRegistry|Copy and paste this inside your %{codeStart}pom.xml%{codeEnd} %{codeStart}dependencies%{codeEnd} block."
msgstr ""
@@ -14298,6 +14596,12 @@ msgstr ""
msgid "PackageRegistry|For more information on the NuGet registry, %{linkStart}see the documentation%{linkEnd}."
msgstr ""
+msgid "PackageRegistry|For more information on the PyPi registry, %{linkStart}see the documentation%{linkEnd}."
+msgstr ""
+
+msgid "PackageRegistry|If you haven't already done so, you will need to add the below to your %{codeStart}.pypirc%{codeEnd} file."
+msgstr ""
+
msgid "PackageRegistry|If you haven't already done so, you will need to add the below to your %{codeStart}pom.xml%{codeEnd} file."
msgstr ""
@@ -14307,6 +14611,9 @@ msgstr ""
msgid "PackageRegistry|Learn how to %{noPackagesLinkStart}publish and share your packages%{noPackagesLinkEnd} with GitLab."
msgstr ""
+msgid "PackageRegistry|Manually Published"
+msgstr ""
+
msgid "PackageRegistry|Maven"
msgstr ""
@@ -14325,12 +14632,18 @@ msgstr ""
msgid "PackageRegistry|NuGet Command"
msgstr ""
+msgid "PackageRegistry|Pip Command"
+msgstr ""
+
msgid "PackageRegistry|Pipeline %{linkStart}%{linkEnd} triggered %{timestamp} by %{author}"
msgstr ""
msgid "PackageRegistry|Published to the repository at %{timestamp}"
msgstr ""
+msgid "PackageRegistry|PyPi"
+msgstr ""
+
msgid "PackageRegistry|Registry Setup"
msgstr ""
@@ -14367,6 +14680,9 @@ msgstr ""
msgid "PackageRegistry|npm"
msgstr ""
+msgid "PackageRegistry|published by %{author}"
+msgstr ""
+
msgid "PackageRegistry|yarn"
msgstr ""
@@ -14382,6 +14698,9 @@ msgstr ""
msgid "PackageType|NuGet"
msgstr ""
+msgid "PackageType|PyPi"
+msgstr ""
+
msgid "Packages"
msgstr ""
@@ -14475,6 +14794,9 @@ msgstr ""
msgid "Past due"
msgstr ""
+msgid "Paste a machine public key here. Read more about how to generate it"
+msgstr ""
+
msgid "Paste a machine public key here. Read more about how to generate it %{link_start}here%{link_end}"
msgstr ""
@@ -14490,9 +14812,6 @@ msgstr ""
msgid "Path"
msgstr ""
-msgid "Path, transfer, remove"
-msgstr ""
-
msgid "Path:"
msgstr ""
@@ -14523,7 +14842,7 @@ msgstr ""
msgid "Percentage"
msgstr ""
-msgid "Perform advanced options such as changing path, transferring, or removing the group."
+msgid "Perform advanced options such as changing path, transferring, exporting, or removing the group."
msgstr ""
msgid "Perform common operations on GitLab project"
@@ -14604,9 +14923,6 @@ msgstr ""
msgid "Pipeline minutes quota"
msgstr ""
-msgid "Pipeline quota"
-msgstr ""
-
msgid "Pipeline subscriptions"
msgstr ""
@@ -14616,6 +14932,9 @@ msgstr ""
msgid "Pipeline: %{status}"
msgstr ""
+msgid "PipelineCharts|CI / CD Analytics"
+msgstr ""
+
msgid "PipelineCharts|Failed:"
msgstr ""
@@ -14694,15 +15013,6 @@ msgstr ""
msgid "Pipelines settings for '%{project_name}' were successfully updated."
msgstr ""
-msgid "Pipelines| to purchase more minutes."
-msgstr ""
-
-msgid "Pipelines|%{namespace_name} has exceeded its pipeline minutes quota."
-msgstr ""
-
-msgid "Pipelines|%{namespace_name} has less than %{notification_level}%% of CI minutes available."
-msgstr ""
-
msgid "Pipelines|API"
msgstr ""
@@ -14724,10 +15034,13 @@ msgstr ""
msgid "Pipelines|Get started with Pipelines"
msgstr ""
-msgid "Pipelines|Loading Pipelines"
+msgid "Pipelines|Group %{namespace_name} has %{percentage}%% or less Shared Runner Pipeline minutes remaining. Once it runs out, no new jobs or pipelines in its projects will run."
msgstr ""
-msgid "Pipelines|Pipelines will not run anymore on shared Runners."
+msgid "Pipelines|Group %{namespace_name} has exceeded its pipeline minutes quota. Unless you buy additional pipeline minutes, no new jobs or pipelines in its projects will run."
+msgstr ""
+
+msgid "Pipelines|Loading Pipelines"
msgstr ""
msgid "Pipelines|Project cache successfully reset."
@@ -14958,6 +15271,9 @@ msgstr ""
msgid "Please select"
msgstr ""
+msgid "Please select a Jira project"
+msgstr ""
+
msgid "Please select a country"
msgstr ""
@@ -15003,6 +15319,9 @@ msgstr ""
msgid "Please wait while we import the repository for you. Refresh at will."
msgstr ""
+msgid "Plugins directory is deprecated and will be removed in 14.0. Please move this file into /file_hooks directory."
+msgstr ""
+
msgid "Pod does not exist"
msgstr ""
@@ -15120,7 +15439,7 @@ msgstr ""
msgid "Prevent users from changing their profile name"
msgstr ""
-msgid "Prevent users from modifing merge request approvers list"
+msgid "Prevent users from modifying merge request approvers list"
msgstr ""
msgid "Prevent users from performing write operations on GitLab while performing maintenance."
@@ -15141,10 +15460,10 @@ msgstr ""
msgid "Previous Artifacts"
msgstr ""
-msgid "Previous file in diff (MRs only)"
+msgid "Previous file in diff"
msgstr ""
-msgid "Previous unresolved discussion (MRs only)"
+msgid "Previous unresolved discussion"
msgstr ""
msgid "Primary"
@@ -15177,6 +15496,9 @@ msgstr ""
msgid "Private profile"
msgstr ""
+msgid "Private projects Minutes cost factor"
+msgstr ""
+
msgid "Private projects can be created in your personal namespace with:"
msgstr ""
@@ -16005,6 +16327,9 @@ msgstr ""
msgid "ProjectSettings|With GitLab Pages you can host your static websites on GitLab"
msgstr ""
+msgid "ProjectSettings|With Metrics Dashboard you can visualize this project performance metrics"
+msgstr ""
+
msgid "ProjectTemplates|.NET Core"
msgstr ""
@@ -16068,6 +16393,9 @@ msgstr ""
msgid "ProjectTemplates|Spring"
msgstr ""
+msgid "ProjectTemplates|Static Site Editor/Middleman"
+msgstr ""
+
msgid "ProjectTemplates|iOS (Swift)"
msgstr ""
@@ -16287,13 +16615,13 @@ msgstr ""
msgid "PrometheusService|Prometheus is being automatically managed on your clusters"
msgstr ""
-msgid "PrometheusService|These metrics will only be monitored after your first deployment to an environment"
+msgid "PrometheusService|Select the Active checkbox to override the Auto Configuration with custom settings. If unchecked, Auto Configuration settings are used."
msgstr ""
-msgid "PrometheusService|Time-series monitoring service"
+msgid "PrometheusService|These metrics will only be monitored after your first deployment to an environment"
msgstr ""
-msgid "PrometheusService|To enable manual configuration, uninstall Prometheus from your clusters"
+msgid "PrometheusService|Time-series monitoring service"
msgstr ""
msgid "PrometheusService|To enable the installation of Prometheus on your clusters, deactivate the manual configuration below"
@@ -16479,6 +16807,9 @@ msgstr ""
msgid "Protip:"
msgstr ""
+msgid "Protocol"
+msgstr ""
+
msgid "Provider"
msgstr ""
@@ -16503,6 +16834,9 @@ msgstr ""
msgid "Public pipelines"
msgstr ""
+msgid "Public projects Minutes cost factor"
+msgstr ""
+
msgid "Pull"
msgstr ""
@@ -16703,6 +17037,9 @@ msgid_plural "Refreshing in %d seconds to show the updated status..."
msgstr[0] ""
msgstr[1] ""
+msgid "Regenerate export"
+msgstr ""
+
msgid "Regenerate instance ID"
msgstr ""
@@ -17080,9 +17417,29 @@ msgstr ""
msgid "Reports|%{combinedString} and %{resolvedString}"
msgstr ""
+msgid "Reports|Accessibility scanning detected %d issue for the source branch only"
+msgid_plural "Reports|Accessibility scanning detected %d issues for the source branch only"
+msgstr[0] ""
+msgstr[1] ""
+
+msgid "Reports|Accessibility scanning detected no issues for the source branch only"
+msgstr ""
+
+msgid "Reports|Accessibility scanning failed loading results"
+msgstr ""
+
+msgid "Reports|Accessibility scanning results are being parsed"
+msgstr ""
+
msgid "Reports|Actions"
msgstr ""
+msgid "Reports|An error occured while loading report"
+msgstr ""
+
+msgid "Reports|An error occurred while loading %{name} results"
+msgstr ""
+
msgid "Reports|Class"
msgstr ""
@@ -17188,7 +17545,7 @@ msgstr ""
msgid "Requested %{time_ago}"
msgstr ""
-msgid "Requested design version does not exist"
+msgid "Requested design version does not exist."
msgstr ""
msgid "Requested states are invalid"
@@ -17372,6 +17729,9 @@ msgstr ""
msgid "Resync"
msgstr ""
+msgid "Resync all"
+msgstr ""
+
msgid "Resync all %{replicableType}"
msgstr ""
@@ -17440,6 +17800,9 @@ msgstr ""
msgid "Revoked personal access token %{personal_access_token_name}!"
msgstr ""
+msgid "Revoked project access token %{project_access_token_name}!"
+msgstr ""
+
msgid "RightSidebar|adding a"
msgstr ""
@@ -17488,6 +17851,9 @@ msgstr ""
msgid "Runner token"
msgstr ""
+msgid "Runner tokens"
+msgstr ""
+
msgid "Runner was not updated."
msgstr ""
@@ -17542,6 +17908,9 @@ msgstr ""
msgid "SAML SSO for %{group_name}"
msgstr ""
+msgid "SAML discovery tokens"
+msgstr ""
+
msgid "SAML for %{group_name}"
msgstr ""
@@ -17626,12 +17995,18 @@ msgstr ""
msgid "Scheduled"
msgstr ""
+msgid "Scheduled to merge this merge request (%{strategy})."
+msgstr ""
+
msgid "Scheduled to merge this merge request when the pipeline succeeds."
msgstr ""
msgid "Schedules"
msgstr ""
+msgid "Schedules to merge this merge request (%{strategy})."
+msgstr ""
+
msgid "Scheduling"
msgstr ""
@@ -17689,6 +18064,9 @@ msgstr ""
msgid "Search branches and tags"
msgstr ""
+msgid "Search by author"
+msgstr ""
+
msgid "Search files"
msgstr ""
@@ -17834,11 +18212,6 @@ msgid_plural "SearchResults|snippets"
msgstr[0] ""
msgstr[1] ""
-msgid "SearchResults|snippet result"
-msgid_plural "SearchResults|snippet results"
-msgstr[0] ""
-msgstr[1] ""
-
msgid "SearchResults|user"
msgid_plural "SearchResults|users"
msgstr[0] ""
@@ -17849,6 +18222,9 @@ msgid_plural "SearchResults|wiki results"
msgstr[0] ""
msgstr[1] ""
+msgid "Searching by both author and message is currently not supported."
+msgstr ""
+
msgid "Seat Link"
msgstr ""
@@ -17876,184 +18252,232 @@ msgstr ""
msgid "Security Dashboard"
msgstr ""
-msgid "Security Dashboard|Error fetching the vulnerability counts. Please check your network connection and try again."
+msgid "Security configuration help link"
+msgstr ""
+
+msgid "Security dashboard"
msgstr ""
-msgid "Security Dashboard|Error fetching the vulnerability list. Please check your network connection and try again."
+msgid "Security report is out of date. Please update your branch with the latest changes from the target branch (%{targetBranchName})"
msgstr ""
-msgid "Security Dashboard|Issue Created"
+msgid "Security report is out of date. Run %{newPipelineLinkStart}a new pipeline%{newPipelineLinkEnd} for the target branch (%{targetBranchName})"
msgstr ""
-msgid "Security Reports|Comment added to '%{vulnerabilityName}'"
+msgid "SecurityConfiguration|Configured"
msgstr ""
-msgid "Security Reports|Comment deleted on '%{vulnerabilityName}'"
+msgid "SecurityConfiguration|Feature"
msgstr ""
-msgid "Security Reports|Comment edited on '%{vulnerabilityName}'"
+msgid "SecurityConfiguration|Feature documentation for %{featureName}"
msgstr ""
-msgid "Security Reports|Create issue"
+msgid "SecurityConfiguration|Not yet configured"
msgstr ""
-msgid "Security Reports|Dismiss vulnerability"
+msgid "SecurityConfiguration|Secure features"
msgstr ""
-msgid "Security Reports|Dismissed '%{vulnerabilityName}'"
+msgid "SecurityConfiguration|Status"
msgstr ""
-msgid "Security Reports|Dismissed '%{vulnerabilityName}'. Turn off the hide dismissed toggle to view."
+msgid "SecurityReports|%{firstProject} and %{secondProject}"
msgstr ""
-msgid "Security Reports|Either you don't have permission to view this dashboard or the dashboard has not been setup. Please check your permission settings with your administrator or check your dashboard configurations to proceed."
+msgid "SecurityReports|%{firstProject}, %{secondProject}, and %{rest}"
msgstr ""
-msgid "Security Reports|Learn more about setting up your dashboard"
+msgid "SecurityReports|Add a project to your dashboard"
msgstr ""
-msgid "Security Reports|More info"
+msgid "SecurityReports|Add or remove projects from your dashboard"
msgstr ""
-msgid "Security Reports|Oops, something doesn't seem right."
+msgid "SecurityReports|Add projects"
msgstr ""
-msgid "Security Reports|Security reports can only be accessed by authorized users."
+msgid "SecurityReports|Comment added to '%{vulnerabilityName}'"
msgstr ""
-msgid "Security Reports|There was an error adding the comment."
+msgid "SecurityReports|Comment deleted on '%{vulnerabilityName}'"
msgstr ""
-msgid "Security Reports|There was an error creating the issue."
+msgid "SecurityReports|Comment edited on '%{vulnerabilityName}'"
msgstr ""
-msgid "Security Reports|There was an error creating the merge request."
+msgid "SecurityReports|Create issue"
msgstr ""
-msgid "Security Reports|There was an error deleting the comment."
+msgid "SecurityReports|Dismiss Selected"
msgstr ""
-msgid "Security Reports|There was an error dismissing the vulnerabilities."
+msgid "SecurityReports|Dismiss vulnerability"
msgstr ""
-msgid "Security Reports|There was an error dismissing the vulnerability."
+msgid "SecurityReports|Dismissed '%{vulnerabilityName}'"
msgstr ""
-msgid "Security Reports|There was an error reverting the dismissal."
+msgid "SecurityReports|Dismissed '%{vulnerabilityName}'. Turn off the hide dismissed toggle to view."
msgstr ""
-msgid "Security Reports|There was an error reverting this dismissal."
+msgid "SecurityReports|Each vulnerability now has a unique page that can be directly linked to, shared, referenced, and tracked as the single source of truth. Vulnerability occurrences also persist across scanner runs, which improves tracking and visibility and reduces duplicates between scans."
msgstr ""
-msgid "Security Reports|Undo dismiss"
+msgid "SecurityReports|Edit dashboard"
msgstr ""
-msgid "Security Reports|You do not have sufficient permissions to access this report"
+msgid "SecurityReports|Either you don't have permission to view this dashboard or the dashboard has not been setup. Please check your permission settings with your administrator or check your dashboard configurations to proceed."
msgstr ""
-msgid "Security Reports|You must sign in as an authorized user to see this report"
+msgid "SecurityReports|Error fetching the vulnerability counts. Please check your network connection and try again."
msgstr ""
-msgid "Security configuration help link"
+msgid "SecurityReports|Error fetching the vulnerability list. Please check your network connection and try again."
msgstr ""
-msgid "Security dashboard"
+msgid "SecurityReports|False positive"
msgstr ""
-msgid "Security report is out of date. Please update your branch with the latest changes from the target branch (%{targetBranchName})"
+msgid "SecurityReports|Hide dismissed"
msgstr ""
-msgid "Security report is out of date. Run %{newPipelineLinkStart}a new pipeline%{newPipelineLinkEnd} for the target branch (%{targetBranchName})"
+msgid "SecurityReports|Introducing standalone vulnerabilities"
msgstr ""
-msgid "SecurityConfiguration|Configured"
+msgid "SecurityReports|Issue Created"
msgstr ""
-msgid "SecurityConfiguration|Feature"
+msgid "SecurityReports|Learn More"
msgstr ""
-msgid "SecurityConfiguration|Feature documentation for %{featureName}"
+msgid "SecurityReports|Learn more about setting up your dashboard"
msgstr ""
-msgid "SecurityConfiguration|Not yet configured"
+msgid "SecurityReports|Load more vulnerabilities"
msgstr ""
-msgid "SecurityConfiguration|Secure features"
+msgid "SecurityReports|Monitor vulnerabilities in your code"
msgstr ""
-msgid "SecurityConfiguration|Status"
+msgid "SecurityReports|More info"
msgstr ""
-msgid "SecurityDashboard|%{firstProject} and %{secondProject}"
+msgid "SecurityReports|More information"
msgstr ""
-msgid "SecurityDashboard|%{firstProject}, %{secondProject}, and %{rest}"
+msgid "SecurityReports|No vulnerabilities found for dashboard"
msgstr ""
-msgid "SecurityDashboard|Add a project to your dashboard"
+msgid "SecurityReports|No vulnerabilities found for this group"
msgstr ""
-msgid "SecurityDashboard|Add or remove projects from your dashboard"
+msgid "SecurityReports|No vulnerabilities found for this pipeline"
msgstr ""
-msgid "SecurityDashboard|Add projects"
+msgid "SecurityReports|No vulnerabilities found for this project"
msgstr ""
-msgid "SecurityDashboard|Edit dashboard"
+msgid "SecurityReports|Oops, something doesn't seem right."
msgstr ""
-msgid "SecurityDashboard|Hide dismissed"
+msgid "SecurityReports|Pipeline %{pipelineLink} triggered %{timeago} by %{user}"
msgstr ""
-msgid "SecurityDashboard|Monitor vulnerabilities in your code"
+msgid "SecurityReports|Project"
msgstr ""
-msgid "SecurityDashboard|More information"
+msgid "SecurityReports|Projects added"
msgstr ""
-msgid "SecurityDashboard|No vulnerabilities found for dashboard"
+msgid "SecurityReports|Remove project from dashboard"
msgstr ""
-msgid "SecurityDashboard|Pipeline %{pipelineLink} triggered %{timeago} by %{user}"
+msgid "SecurityReports|Report type"
msgstr ""
-msgid "SecurityDashboard|Project"
+msgid "SecurityReports|Return to dashboard"
msgstr ""
-msgid "SecurityDashboard|Projects added"
+msgid "SecurityReports|Security Dashboard"
msgstr ""
-msgid "SecurityDashboard|Remove project from dashboard"
+msgid "SecurityReports|Security reports can only be accessed by authorized users."
msgstr ""
-msgid "SecurityDashboard|Report type"
+msgid "SecurityReports|Select a project to add by using the project search field above."
msgstr ""
-msgid "SecurityDashboard|Return to dashboard"
+msgid "SecurityReports|Select a reason"
msgstr ""
-msgid "SecurityDashboard|Security Dashboard"
+msgid "SecurityReports|Severity"
msgstr ""
-msgid "SecurityDashboard|Select a project to add by using the project search field above."
+msgid "SecurityReports|Status"
msgstr ""
-msgid "SecurityDashboard|Severity"
+msgid "SecurityReports|The security dashboard displays the latest security findings for projects you wish to monitor. Select \"Edit dashboard\" to add and remove projects."
msgstr ""
-msgid "SecurityDashboard|Status"
+msgid "SecurityReports|The security dashboard displays the latest security report. Use it to find and fix vulnerabilities."
msgstr ""
-msgid "SecurityDashboard|The security dashboard displays the latest security findings for projects you wish to monitor. Select \"Edit dashboard\" to add and remove projects."
+msgid "SecurityReports|There was an error adding the comment."
msgstr ""
-msgid "SecurityDashboard|The security dashboard displays the latest security report. Use it to find and fix vulnerabilities."
+msgid "SecurityReports|There was an error creating the issue."
msgstr ""
-msgid "SecurityDashboard|There was an error while generating the report."
+msgid "SecurityReports|There was an error creating the merge request."
msgstr ""
-msgid "SecurityDashboard|Unable to add %{invalidProjects}"
+msgid "SecurityReports|There was an error deleting the comment."
+msgstr ""
+
+msgid "SecurityReports|There was an error dismissing the vulnerabilities."
+msgstr ""
+
+msgid "SecurityReports|There was an error dismissing the vulnerability."
+msgstr ""
+
+msgid "SecurityReports|There was an error reverting the dismissal."
+msgstr ""
+
+msgid "SecurityReports|There was an error reverting this dismissal."
+msgstr ""
+
+msgid "SecurityReports|There was an error while generating the report."
+msgstr ""
+
+msgid "SecurityReports|Unable to add %{invalidProjects}"
+msgstr ""
+
+msgid "SecurityReports|Undo dismiss"
+msgstr ""
+
+msgid "SecurityReports|While it's rare to have no vulnerabilities for your group, it can happen. In any event, we ask that you double check your settings to make sure you've set up your dashboard correctly."
+msgstr ""
+
+msgid "SecurityReports|While it's rare to have no vulnerabilities for your pipeline, it can happen. In any event, we ask that you double check your settings to make sure all security scanning jobs have passed successfully."
+msgstr ""
+
+msgid "SecurityReports|While it's rare to have no vulnerabilities for your project, it can happen. In any event, we ask that you double check your settings to make sure you've set up your dashboard correctly."
+msgstr ""
+
+msgid "SecurityReports|While it's rare to have no vulnerabilities, it can happen. In any event, we ask that you please double check your settings to make sure you've set up your dashboard correctly."
+msgstr ""
+
+msgid "SecurityReports|Won't fix / Accept risk"
+msgstr ""
+
+msgid "SecurityReports|You do not have sufficient permissions to access this report"
+msgstr ""
+
+msgid "SecurityReports|You must sign in as an authorized user to see this report"
+msgstr ""
+
+msgid "SecurityReports|[No reason]"
msgstr ""
msgid "See GitLab's %{password_policy_guidelines}"
@@ -18065,6 +18489,9 @@ msgstr ""
msgid "See the affected projects in the GitLab admin panel"
msgstr ""
+msgid "See what's new at GitLab"
+msgstr ""
+
msgid "Select"
msgstr ""
@@ -18470,9 +18897,6 @@ msgstr ""
msgid "Set up Jira Integration"
msgstr ""
-msgid "Set up Jira Integration illustration"
-msgstr ""
-
msgid "Set up a %{type} Runner automatically"
msgstr ""
@@ -18557,6 +18981,9 @@ msgstr ""
msgid "Severity: %{severity}"
msgstr ""
+msgid "Shards (%{shards})"
+msgstr ""
+
msgid "Shards to synchronize"
msgstr ""
@@ -18871,6 +19298,9 @@ msgstr ""
msgid "Snowplow"
msgstr ""
+msgid "Some child epics may be hidden due to applied filters"
+msgstr ""
+
msgid "Some email servers do not support overriding the email sender name. Enable this option to include the name of the author of the issue, merge request or comment in the email body instead."
msgstr ""
@@ -19012,6 +19442,9 @@ msgstr ""
msgid "Something went wrong while updating your list settings"
msgstr ""
+msgid "Something went wrong with your automatic subscription renewal"
+msgstr ""
+
msgid "Something went wrong, unable to add %{project} to dashboard"
msgstr ""
@@ -19297,6 +19730,9 @@ msgstr ""
msgid "Stage removed"
msgstr ""
+msgid "Standard"
+msgstr ""
+
msgid "Star a label to make it a priority label. Order the prioritized labels to change their relative priority, by dragging."
msgstr ""
@@ -19420,6 +19856,9 @@ msgstr ""
msgid "Static Application Security Testing (SAST)"
msgstr ""
+msgid "StaticSiteEditor|An error occurred while submitting your changes."
+msgstr ""
+
msgid "StaticSiteEditor|Branch could not be created."
msgstr ""
@@ -19429,18 +19868,30 @@ msgstr ""
msgid "StaticSiteEditor|Could not create merge request."
msgstr ""
+msgid "StaticSiteEditor|Incompatible file content"
+msgstr ""
+
msgid "StaticSiteEditor|Return to site"
msgstr ""
+msgid "StaticSiteEditor|Static site editor"
+msgstr ""
+
msgid "StaticSiteEditor|Success!"
msgstr ""
msgid "StaticSiteEditor|Summary of changes"
msgstr ""
+msgid "StaticSiteEditor|The Static Site Editor is currently configured to only edit Markdown content on pages generated from Middleman. Visit the documentation to learn more about configuring your site to use the Static Site Editor."
+msgstr ""
+
msgid "StaticSiteEditor|Update %{sourcePath} file"
msgstr ""
+msgid "StaticSiteEditor|View documentation"
+msgstr ""
+
msgid "StaticSiteEditor|View merge request"
msgstr ""
@@ -19498,6 +19949,12 @@ msgstr ""
msgid "StatusPage|Status page"
msgstr ""
+msgid "StatusPage|Status page URL"
+msgstr ""
+
+msgid "StatusPage|Status page frontend documentation"
+msgstr ""
+
msgid "StatusPage|To publish incidents to an external status page, GitLab will store a JSON file in your Amazon S3 account in a location accessible to your external status page service. Make sure to also set up %{docsLink}"
msgstr ""
@@ -19846,6 +20303,12 @@ msgstr ""
msgid "Synced"
msgstr ""
+msgid "Synchronization disabled"
+msgstr ""
+
+msgid "Synchronization of container repositories is disabled."
+msgstr ""
+
msgid "System"
msgstr ""
@@ -19999,6 +20462,9 @@ msgstr ""
msgid "Telephone number"
msgstr ""
+msgid "Telephone number (Optional)"
+msgstr ""
+
msgid "Template"
msgstr ""
@@ -20305,6 +20771,9 @@ msgstr ""
msgid "The license was successfully uploaded and is now active. You can see the details below."
msgstr ""
+msgid "The license was successfully uploaded and will be active from %{starts_at}. You can see the details below."
+msgstr ""
+
msgid "The maximum file size allowed is %{size}."
msgstr ""
@@ -20626,6 +21095,9 @@ msgstr ""
msgid "There was an error getting the epic participants."
msgstr ""
+msgid "There was an error importing the Jira project."
+msgstr ""
+
msgid "There was an error loading users activity calendar."
msgstr ""
@@ -20641,7 +21113,7 @@ msgstr ""
msgid "There was an error resetting user pipeline minutes."
msgstr ""
-msgid "There was an error saving this Geo Node"
+msgid "There was an error saving this Geo Node."
msgstr ""
msgid "There was an error saving your changes."
@@ -20704,6 +21176,9 @@ msgstr ""
msgid "They can be managed using the %{link}."
msgstr ""
+msgid "Third Party Advisory Link"
+msgstr ""
+
msgid "Third party offers"
msgstr ""
@@ -20740,6 +21215,9 @@ msgstr ""
msgid "This application will be able to:"
msgstr ""
+msgid "This attachment has been truncated to avoid exceeding the maximum allowed attachment size of 15MB. %{written_count} of %{issues_count} issues have been included. Consider re-exporting with a narrower selection of issues."
+msgstr ""
+
msgid "This block is self-referential"
msgstr ""
@@ -20881,6 +21359,12 @@ msgstr ""
msgid "This issue is confidential"
msgstr ""
+msgid "This issue is currently blocked by the following issues: %{issues}."
+msgstr ""
+
+msgid "This issue is in a child epic of the filtered epic"
+msgstr ""
+
msgid "This issue is locked."
msgstr ""
@@ -21079,7 +21563,10 @@ msgstr ""
msgid "This user will be the author of all events in the activity feed that are the result of an update, like new branches being created or new commits being pushed to existing branches. Upon creation or when reassigning you can only assign yourself to be the mirror user."
msgstr ""
-msgid "This variable can not be masked"
+msgid "This variable can not be masked."
+msgstr ""
+
+msgid "This variable does not match the expected pattern."
msgstr ""
msgid "This will help us personalize your onboarding experience."
@@ -21109,10 +21596,10 @@ msgstr ""
msgid "ThreatMonitoring|Application firewall not detected"
msgstr ""
-msgid "ThreatMonitoring|Container Network Policy"
+msgid "ThreatMonitoring|Container Network Policies are not installed or have been disabled. To view this data, ensure your Network Policies are installed and enabled for your cluster."
msgstr ""
-msgid "ThreatMonitoring|Container NetworkPolicies are not installed or has been disabled. To view this data, ensure you NetworkPolicies are installed and enabled for your cluster."
+msgid "ThreatMonitoring|Container Network Policy"
msgstr ""
msgid "ThreatMonitoring|Container NetworkPolicies not detected"
@@ -21145,7 +21632,7 @@ msgstr ""
msgid "ThreatMonitoring|Something went wrong, unable to fetch statistics"
msgstr ""
-msgid "ThreatMonitoring|The firewall is not installed or has been disabled. To view this data, ensure you firewall is installed and enabled for your cluster."
+msgid "ThreatMonitoring|The firewall is not installed or has been disabled. To view this data, ensure the web application firewall is installed and enabled for your cluster."
msgstr ""
msgid "ThreatMonitoring|The graph below is an overview of traffic coming to your application as tracked by the Web Application Firewall (WAF). View the docs for instructions on how to access the WAF logs to see what type of malicious traffic is trying to access your app. The docs link is also accessible by clicking the \"?\" icon next to the title below."
@@ -21214,6 +21701,9 @@ msgstr ""
msgid "Time in seconds GitLab will wait for a response from the external service. When the service does not respond in time, access will be denied."
msgstr ""
+msgid "Time of import: %{importTime}"
+msgstr ""
+
msgid "Time remaining"
msgstr ""
@@ -21398,7 +21888,7 @@ msgstr ""
msgid "Title:"
msgstr ""
-msgid "Titles and Filenames"
+msgid "Titles and Descriptions"
msgstr ""
msgid "To"
@@ -21605,7 +22095,7 @@ msgstr ""
msgid "Too many projects enabled. You will need to manage them via the console or the API."
msgstr ""
-msgid "Topics"
+msgid "Topics (optional)"
msgstr ""
msgid "Total"
@@ -21767,6 +22257,9 @@ msgstr ""
msgid "Try all GitLab has to offer for 30 days."
msgstr ""
+msgid "Try changing or removing filters."
+msgstr ""
+
msgid "Try to fork again"
msgstr ""
@@ -21860,6 +22353,9 @@ msgstr ""
msgid "Unable to connect to server: %{error}"
msgstr ""
+msgid "Unable to connect to the Jira instance. Please check your Jira integration configuration."
+msgstr ""
+
msgid "Unable to convert Kubernetes logs encoding to UTF-8"
msgstr ""
@@ -22803,6 +23299,9 @@ msgstr ""
msgid "View issue"
msgstr ""
+msgid "View issues"
+msgstr ""
+
msgid "View it on GitLab"
msgstr ""
@@ -22980,6 +23479,15 @@ msgstr ""
msgid "VulnerabilityManagement|Resolved %{timeago} by %{user}"
msgstr ""
+msgid "VulnerabilityManagement|Something went wrong while trying to delete the comment. Please try again later."
+msgstr ""
+
+msgid "VulnerabilityManagement|Something went wrong while trying to retrieve the vulnerability history. Please try again later."
+msgstr ""
+
+msgid "VulnerabilityManagement|Something went wrong while trying to save the comment. Please try again later."
+msgstr ""
+
msgid "VulnerabilityManagement|Something went wrong, could not create an issue."
msgstr ""
@@ -23010,6 +23518,9 @@ msgstr ""
msgid "VulnerabilityStatusTypes|Resolved"
msgstr ""
+msgid "Vulnerability|%{scannerName} (version %{scannerVersion})"
+msgstr ""
+
msgid "Vulnerability|Class"
msgstr ""
@@ -23043,7 +23554,10 @@ msgstr ""
msgid "Vulnerability|Project"
msgstr ""
-msgid "Vulnerability|Report Type"
+msgid "Vulnerability|Scanner Provider"
+msgstr ""
+
+msgid "Vulnerability|Scanner Type"
msgstr ""
msgid "Vulnerability|Severity"
@@ -23097,9 +23611,15 @@ msgstr ""
msgid "We sent you an email with reset password instructions"
msgstr ""
+msgid "We tried to automatically renew your %{strong}%{plan_name}%{strong_close} subscription for %{strong}%{namespace_name}%{strong_close} on %{expires_on} but something went wrong so your subscription was downgraded to the free plan. Don't worry, your data is safe. We suggest you check your payment method and get in touch with our support team (%{support_link}). They'll gladly help with your subscription renewal."
+msgstr ""
+
msgid "We want to be sure it is you, please confirm you are not a robot."
msgstr ""
+msgid "We will automatically renew your %{strong}%{plan_name}%{strong_close} subscription for %{strong}%{namespace_name}%{strong_close} on %{strong}%{expires_on}%{strong_close}. There's nothing that you need to do, we'll let you know when the renewal is complete. Need more seats, a higher plan or just want to review your payment method?"
+msgstr ""
+
msgid "We've found no vulnerabilities"
msgstr ""
@@ -23204,15 +23724,6 @@ msgstr ""
msgid "When:"
msgstr ""
-msgid "While it's rare to have no vulnerabilities for your group, it can happen. In any event, we ask that you double check your settings to make sure you've set up your dashboard correctly."
-msgstr ""
-
-msgid "While it's rare to have no vulnerabilities for your pipeline, it can happen. In any event, we ask that you double check your settings to make sure all security scanning jobs have passed successfully."
-msgstr ""
-
-msgid "While it's rare to have no vulnerabilities for your project, it can happen. In any event, we ask that you double check your settings to make sure you've set up your dashboard correctly."
-msgstr ""
-
msgid "While it's rare to have no vulnerabilities, it can happen. In any event, we ask that you please double check your settings to make sure you've set up your dashboard correctly."
msgstr ""
@@ -23243,9 +23754,6 @@ msgstr ""
msgid "Wiki"
msgstr ""
-msgid "Wiki pages"
-msgstr ""
-
msgid "Wiki was successfully updated."
msgstr ""
@@ -23429,6 +23937,9 @@ msgstr ""
msgid "Yes, add it"
msgstr ""
+msgid "Yes, close issue"
+msgstr ""
+
msgid "Yes, let me map Google Code users to full names or GitLab users."
msgstr ""
@@ -23624,6 +24135,9 @@ msgstr ""
msgid "You could not create a new trigger."
msgstr ""
+msgid "You didn't renew your %{strong}%{plan_name}%{strong_close} subscription for %{strong}%{namespace_name}%{strong_close} so it was downgraded to the free plan."
+msgstr ""
+
msgid "You didn't renew your %{strong}%{plan_name}%{strong_close} subscription so it was downgraded to the GitLab Core Plan."
msgstr ""
@@ -23690,6 +24204,9 @@ msgstr ""
msgid "You have declined the invitation to join %{label}."
msgstr ""
+msgid "You have imported from this project %{numberOfPreviousImportsForProject} times before. Each new import will create duplicate issues."
+msgstr ""
+
msgid "You have no permissions"
msgstr ""
@@ -23771,6 +24288,9 @@ msgstr ""
msgid "You need to upload a Google Takeout archive."
msgstr ""
+msgid "You reached %{usage_in_percent} of %{namespace_name}'s capacity (%{used_storage} of %{storage_limit})"
+msgstr ""
+
msgid "You tried to fork %{link_to_the_project} but it failed for the following reason:"
msgstr ""
@@ -23855,9 +24375,21 @@ msgstr ""
msgid "YouTube"
msgstr ""
+msgid "Your %{strong}%{plan_name}%{strong_close} subscription for %{strong}%{namespace_name}%{strong_close} will expire on %{strong}%{expires_on}%{strong_close}. After that, you will not to be able to create issues or merge requests as well as many other features."
+msgstr ""
+
msgid "Your %{strong}%{plan_name}%{strong_close} subscription will expire on %{strong}%{expires_on}%{strong_close}. After that, you will not to be able to create issues or merge requests as well as many other features."
msgstr ""
+msgid "Your CSV export has started. It will be emailed to %{email} when complete."
+msgstr ""
+
+msgid "Your CSV export of %{issues_count} from project %{project_link} has been added to this email as an attachment."
+msgstr ""
+
+msgid "Your CSV export of %{written_count} from project %{project_name} (%{project_url}) has been added to this email as an attachment."
+msgstr ""
+
msgid "Your Commit Email will be used for web based operations, such as edits and merges."
msgstr ""
@@ -23954,6 +24486,9 @@ msgstr ""
msgid "Your comment could not be updated! Please check your network connection and try again."
msgstr ""
+msgid "Your comment will be discarded."
+msgstr ""
+
msgid "Your custom stage '%{title}' was created"
msgstr ""
@@ -23993,6 +24528,9 @@ msgstr ""
msgid "Your new personal access token has been created."
msgstr ""
+msgid "Your new project access token has been created."
+msgstr ""
+
msgid "Your password isn't required to view this page. If a password or any other personal details are requested, please contact your administrator to report abuse."
msgstr ""
@@ -24011,12 +24549,18 @@ msgstr ""
msgid "Your request for access has been queued for review."
msgstr ""
+msgid "Your search didn't match any commits."
+msgstr ""
+
msgid "Your subscription expired!"
msgstr ""
msgid "Your subscription has been downgraded"
msgstr ""
+msgid "Your subscription will automatically renew in %{remaining_days}"
+msgstr ""
+
msgid "Your subscription will expire in %{remaining_days}"
msgstr ""
@@ -24055,7 +24599,7 @@ msgstr ""
msgid "allowed to fail"
msgstr ""
-msgid "already being used for another group or project milestone."
+msgid "already being used for another group or project %{timebox_name}."
msgstr ""
msgid "already has a \"created\" issue link"
@@ -24133,6 +24677,9 @@ msgstr ""
msgid "ciReport|%{linkStartTag}Learn more about SAST %{linkEndTag}"
msgstr ""
+msgid "ciReport|%{linkStartTag}Learn more about Secret Scanning %{linkEndTag}"
+msgstr ""
+
msgid "ciReport|%{linkStartTag}Learn more about codequality reports %{linkEndTag}"
msgstr ""
@@ -24296,6 +24843,12 @@ msgstr ""
msgid "ciReport|SAST"
msgstr ""
+msgid "ciReport|Secret scanning"
+msgstr ""
+
+msgid "ciReport|Secret scanning detects secrets and credentials vulnerabilities in your source code."
+msgstr ""
+
msgid "ciReport|Security scanning"
msgstr ""
@@ -24493,6 +25046,9 @@ msgstr ""
msgid "group"
msgstr ""
+msgid "groups"
+msgstr ""
+
msgid "has already been linked to another vulnerability"
msgstr ""
@@ -24656,9 +25212,6 @@ msgstr[1] ""
msgid "merged %{time_ago}"
msgstr ""
-msgid "milestone should belong either to a project or a group."
-msgstr ""
-
msgid "missing"
msgstr ""
@@ -24995,6 +25548,9 @@ msgstr ""
msgid "no contributions"
msgstr ""
+msgid "no expiration"
+msgstr ""
+
msgid "no one can merge"
msgstr ""
@@ -25047,6 +25603,9 @@ msgstr ""
msgid "pending removal"
msgstr ""
+msgid "per day"
+msgstr ""
+
msgid "pipeline"
msgstr ""
@@ -25075,6 +25634,12 @@ msgstr[1] ""
msgid "project avatar"
msgstr ""
+msgid "projects"
+msgstr ""
+
+msgid "push to your repository, create pipelines, create issues or add comments. To reduce storage capacity, delete unused repositories, artifacts, wikis, issues, and pipelines."
+msgstr ""
+
msgid "quick actions"
msgstr ""
@@ -25221,11 +25786,6 @@ msgstr ""
msgid "this document"
msgstr ""
-msgid "thread resolved"
-msgid_plural "threads resolved"
-msgstr[0] ""
-msgstr[1] ""
-
msgid "to help your contributors communicate effectively!"
msgstr ""
@@ -25280,6 +25840,9 @@ msgstr ""
msgid "view the blob"
msgstr ""
+msgid "vulnerability|Add a comment"
+msgstr ""
+
msgid "vulnerability|Add a comment or reason for dismissal"
msgstr ""
diff --git a/locale/fi_FI/gitlab.po b/locale/fi_FI/gitlab.po
index 8841b855b52..4ec4ee77c25 100644
--- a/locale/fi_FI/gitlab.po
+++ b/locale/fi_FI/gitlab.po
@@ -12,7 +12,7 @@ msgstr ""
"X-Crowdin-Project: gitlab-ee\n"
"X-Crowdin-Language: fi\n"
"X-Crowdin-File: /master/locale/gitlab.pot\n"
-"PO-Revision-Date: 2020-04-15 00:31\n"
+"PO-Revision-Date: 2020-05-05 21:14\n"
msgid " %{start} to %{end}"
msgstr ""
@@ -212,6 +212,11 @@ msgid_plural "%d tags"
msgstr[0] ""
msgstr[1] ""
+msgid "%d unresolved thread"
+msgid_plural "%d unresolved threads"
+msgstr[0] ""
+msgstr[1] ""
+
msgid "%d vulnerability dismissed"
msgid_plural "%d vulnerabilities dismissed"
msgstr[0] ""
@@ -371,6 +376,9 @@ msgstr ""
msgid "%{mrText}, this issue will be closed automatically."
msgstr ""
+msgid "%{namespace_name} is now read-only. You cannot: %{base_message}"
+msgstr ""
+
msgid "%{name} contained %{resultsString}"
msgstr ""
@@ -383,6 +391,11 @@ msgstr ""
msgid "%{name}'s avatar"
msgstr ""
+msgid "%{no_of_days} day"
+msgid_plural "%{no_of_days} days"
+msgstr[0] ""
+msgstr[1] ""
+
msgid "%{number_commits_behind} commits behind %{default_branch}, %{number_commits_ahead} commits ahead"
msgstr ""
@@ -506,6 +519,9 @@ msgstr[1] ""
msgid "%{text} is available"
msgstr ""
+msgid "%{timebox_name} should belong either to a project or a group."
+msgstr ""
+
msgid "%{title} %{operator} %{threshold}"
msgstr ""
@@ -781,12 +797,18 @@ msgstr ""
msgid "<code>\"johnsmith@example.com\": \"johnsmith@example.com\"</code> will add \"By <a href=\"#\">johnsmith@example.com</a>\" to all issues and comments originally created by johnsmith@example.com. By default, the email address or username is masked to ensure the user's privacy. Use this option if you want to show the full email address."
msgstr ""
+msgid "<namespace / project>"
+msgstr ""
+
msgid "<no name set>"
msgstr ""
msgid "<no scopes selected>"
msgstr ""
+msgid "<project name>"
+msgstr ""
+
msgid "<strong>%{group_name}</strong> group members"
msgstr ""
@@ -820,6 +842,9 @@ msgstr ""
msgid "A basic page and serverless function that uses AWS Lambda, AWS API Gateway, and GitLab Pages"
msgstr ""
+msgid "A complete DevOps platform"
+msgstr ""
+
msgid "A default branch cannot be chosen for an empty project."
msgstr ""
@@ -1036,6 +1061,12 @@ msgstr ""
msgid "AccessTokens|reset it"
msgstr ""
+msgid "AccessibilityReport|Accessibility report artifact not found"
+msgstr ""
+
+msgid "AccessibilityReport|Failed to retrieve accessibility report"
+msgstr ""
+
msgid "AccessibilityReport|Learn More"
msgstr ""
@@ -1045,6 +1076,9 @@ msgstr ""
msgid "AccessibilityReport|New"
msgstr ""
+msgid "AccessibilityReport|The accessibility scanning found an error of the following type: %{code}"
+msgstr ""
+
msgid "Account"
msgstr ""
@@ -1287,6 +1321,9 @@ msgstr ""
msgid "Added at"
msgstr ""
+msgid "Added for this merge request"
+msgstr ""
+
msgid "Added in this version"
msgstr ""
@@ -1658,6 +1695,72 @@ msgid_plural "Alerts"
msgstr[0] ""
msgstr[1] ""
+msgid "Alert Details"
+msgstr ""
+
+msgid "AlertManagement|Acknowledged"
+msgstr ""
+
+msgid "AlertManagement|Alert"
+msgstr ""
+
+msgid "AlertManagement|Authorize external service"
+msgstr ""
+
+msgid "AlertManagement|Display alerts from all your monitoring tools directly within GitLab. Streamline the investigation of your alerts and the escalation of alerts to incidents."
+msgstr ""
+
+msgid "AlertManagement|End time"
+msgstr ""
+
+msgid "AlertManagement|End time:"
+msgstr ""
+
+msgid "AlertManagement|Events"
+msgstr ""
+
+msgid "AlertManagement|Events:"
+msgstr ""
+
+msgid "AlertManagement|Full Alert Details"
+msgstr ""
+
+msgid "AlertManagement|More information"
+msgstr ""
+
+msgid "AlertManagement|No alerts available to display. If you think you're seeing this message in error, refresh the page."
+msgstr ""
+
+msgid "AlertManagement|No alerts to display."
+msgstr ""
+
+msgid "AlertManagement|Overview"
+msgstr ""
+
+msgid "AlertManagement|Resolved"
+msgstr ""
+
+msgid "AlertManagement|Severity"
+msgstr ""
+
+msgid "AlertManagement|Start time"
+msgstr ""
+
+msgid "AlertManagement|Start time:"
+msgstr ""
+
+msgid "AlertManagement|Status"
+msgstr ""
+
+msgid "AlertManagement|Surface alerts in GitLab"
+msgstr ""
+
+msgid "AlertManagement|There was an error displaying the alerts. Confirm your endpoint's configuration details to ensure alerts appear."
+msgstr ""
+
+msgid "AlertManagement|Triggered"
+msgstr ""
+
msgid "AlertService|%{linkStart}Learn more%{linkEnd} about configuring this endpoint to receive alerts."
msgstr ""
@@ -1727,6 +1830,9 @@ msgstr ""
msgid "All security scans are enabled because %{linkStart}Auto DevOps%{linkEnd} is enabled on this project"
msgstr ""
+msgid "All threads resolved"
+msgstr ""
+
msgid "All users"
msgstr ""
@@ -1745,6 +1851,9 @@ msgstr ""
msgid "Allow only the selected protocols to be used for Git access."
msgstr ""
+msgid "Allow owners to manage default branch protection per group"
+msgstr ""
+
msgid "Allow owners to manually add users outside of LDAP"
msgstr ""
@@ -1853,6 +1962,9 @@ msgstr ""
msgid "An error occurred fetching the dropdown data."
msgstr ""
+msgid "An error occurred fetching the project authors."
+msgstr ""
+
msgid "An error occurred previewing the blob"
msgstr ""
@@ -1934,6 +2046,9 @@ msgstr ""
msgid "An error occurred while fetching sidebar data"
msgstr ""
+msgid "An error occurred while fetching terraform reports."
+msgstr ""
+
msgid "An error occurred while fetching the Service Desk address."
msgstr ""
@@ -2171,6 +2286,9 @@ msgstr ""
msgid "Any"
msgstr ""
+msgid "Any Author"
+msgstr ""
+
msgid "Any Label"
msgstr ""
@@ -2386,10 +2504,10 @@ msgstr ""
msgid "Are you sure that you want to unarchive this project?"
msgstr ""
-msgid "Are you sure you want to cancel creating this comment?"
+msgid "Are you sure you want to cancel editing this comment?"
msgstr ""
-msgid "Are you sure you want to cancel editing this comment?"
+msgid "Are you sure you want to close this blocked issue?"
msgstr ""
msgid "Are you sure you want to delete %{name}?"
@@ -2419,6 +2537,9 @@ msgstr ""
msgid "Are you sure you want to deploy this environment?"
msgstr ""
+msgid "Are you sure you want to discard this comment?"
+msgstr ""
+
msgid "Are you sure you want to erase this build?"
msgstr ""
@@ -2595,6 +2716,9 @@ msgstr ""
msgid "At least one approval from a code owner is required to change files matching the respective CODEOWNER rules."
msgstr ""
+msgid "At least one logging option is required to be enabled"
+msgstr ""
+
msgid "At least one of group_id or project_id must be specified"
msgstr ""
@@ -2633,6 +2757,27 @@ msgstr ""
msgid "AuditEvents|Target"
msgstr ""
+msgid "AuditLogs|(removed)"
+msgstr ""
+
+msgid "AuditLogs|Action"
+msgstr ""
+
+msgid "AuditLogs|Author"
+msgstr ""
+
+msgid "AuditLogs|Date"
+msgstr ""
+
+msgid "AuditLogs|IP Address"
+msgstr ""
+
+msgid "AuditLogs|Object"
+msgstr ""
+
+msgid "AuditLogs|Target"
+msgstr ""
+
msgid "Aug"
msgstr ""
@@ -2915,9 +3060,6 @@ msgstr ""
msgid "BambooService|You must set up automatic revision labeling and a repository trigger in Bamboo."
msgstr ""
-msgid "Batch operations"
-msgstr ""
-
msgid "BatchComments|Delete all pending comments"
msgstr ""
@@ -3047,6 +3189,9 @@ msgstr ""
msgid "Boards"
msgstr ""
+msgid "Boards and Board Lists"
+msgstr ""
+
msgid "Boards|Collapse"
msgstr ""
@@ -3266,6 +3411,9 @@ msgstr ""
msgid "Buy GitLab Enterprise Edition"
msgstr ""
+msgid "Buy more Pipeline minutes"
+msgstr ""
+
msgid "By %{user_name}"
msgstr ""
@@ -3386,6 +3534,9 @@ msgstr ""
msgid "Can't scan the code?"
msgstr ""
+msgid "Can't update snippet: %{err}"
+msgstr ""
+
msgid "Canary"
msgstr ""
@@ -3554,6 +3705,9 @@ msgstr ""
msgid "Changing group path can have unintended side effects."
msgstr ""
+msgid "Charts"
+msgstr ""
+
msgid "Charts can't be displayed as the request for data has timed out. %{documentationLink}"
msgstr ""
@@ -3590,9 +3744,6 @@ msgstr ""
msgid "ChatMessage|and [%{count} more](%{pipeline_failed_jobs_url})"
msgstr ""
-msgid "ChatMessage|failed"
-msgstr ""
-
msgid "ChatMessage|has failed"
msgstr ""
@@ -3608,9 +3759,6 @@ msgstr ""
msgid "ChatMessage|in %{project_link}"
msgstr ""
-msgid "ChatMessage|passed"
-msgstr ""
-
msgid "Check again"
msgstr ""
@@ -4061,9 +4209,6 @@ msgstr ""
msgid "Click any <strong>project name</strong> in the project list below to navigate to the project milestone."
msgstr ""
-msgid "Click here"
-msgstr ""
-
msgid "Click the <strong>Download</strong> button and wait for downloading to complete."
msgstr ""
@@ -4451,6 +4596,12 @@ msgstr ""
msgid "ClusterIntegration|Fetching zones"
msgstr ""
+msgid "ClusterIntegration|Fluentd"
+msgstr ""
+
+msgid "ClusterIntegration|Fluentd is an open source data collector, which lets you unify the data collection and consumption for a better use and understanding of data. It requires at least one of the following logs to be successfully installed."
+msgstr ""
+
msgid "ClusterIntegration|GitLab Integration"
msgstr ""
@@ -4757,6 +4908,15 @@ msgstr ""
msgid "ClusterIntegration|Request to begin uninstalling failed"
msgstr ""
+msgid "ClusterIntegration|SIEM Hostname"
+msgstr ""
+
+msgid "ClusterIntegration|SIEM Port"
+msgstr ""
+
+msgid "ClusterIntegration|SIEM Protocol"
+msgstr ""
+
msgid "ClusterIntegration|Save changes"
msgstr ""
@@ -4853,6 +5013,12 @@ msgstr ""
msgid "ClusterIntegration|Select zone to choose machine type"
msgstr ""
+msgid "ClusterIntegration|Send Cilium Logs"
+msgstr ""
+
+msgid "ClusterIntegration|Send ModSecurity Logs"
+msgstr ""
+
msgid "ClusterIntegration|Service Token"
msgstr ""
@@ -5281,18 +5447,33 @@ msgstr ""
msgid "Compliance framework (optional)"
msgstr ""
+msgid "ComplianceFramework|GDPR"
+msgstr ""
+
msgid "ComplianceFramework|GDPR - General Data Protection Regulation"
msgstr ""
+msgid "ComplianceFramework|HIPAA"
+msgstr ""
+
msgid "ComplianceFramework|HIPAA - Health Insurance Portability and Accountability Act"
msgstr ""
+msgid "ComplianceFramework|PCI-DSS"
+msgstr ""
+
msgid "ComplianceFramework|PCI-DSS - Payment Card Industry-Data Security Standard"
msgstr ""
+msgid "ComplianceFramework|SOC 2"
+msgstr ""
+
msgid "ComplianceFramework|SOC 2 - Service Organization Control 2"
msgstr ""
+msgid "ComplianceFramework|SOX"
+msgstr ""
+
msgid "ComplianceFramework|SOX - Sarbanes-Oxley"
msgstr ""
@@ -5440,9 +5621,15 @@ msgstr ""
msgid "Container repositories sync capacity"
msgstr ""
+msgid "ContainerRegistry| Please visit the %{linkStart}administration settings%{linkEnd} to enable this feature."
+msgstr ""
+
msgid "ContainerRegistry|%{imageName} tags"
msgstr ""
+msgid "ContainerRegistry|%{title} was successfully scheduled for deletion"
+msgstr ""
+
msgid "ContainerRegistry|Automatically remove extra images that aren't designed to be kept."
msgstr ""
@@ -5455,6 +5642,9 @@ msgstr ""
msgid "ContainerRegistry|Container Registry"
msgstr ""
+msgid "ContainerRegistry|Container Registry tag expiration and retention policy is disabled"
+msgstr ""
+
msgid "ContainerRegistry|Copy build command"
msgstr ""
@@ -5464,18 +5654,12 @@ msgstr ""
msgid "ContainerRegistry|Copy push command"
msgstr ""
-msgid "ContainerRegistry|Currently, the Container Registry tag expiration feature is not available for projects created before GitLab version 12.8. For updates and more information, visit Issue %{linkStart}#196124%{linkEnd}"
-msgstr ""
-
msgid "ContainerRegistry|Docker connection error"
msgstr ""
msgid "ContainerRegistry|Docker tag expiration policy is %{toggleStatus}"
msgstr ""
-msgid "ContainerRegistry|Docker tags with names matching this regex pattern will expire:"
-msgstr ""
-
msgid "ContainerRegistry|Edit Settings"
msgstr ""
@@ -5497,9 +5681,6 @@ msgstr ""
msgid "ContainerRegistry|Image ID"
msgstr ""
-msgid "ContainerRegistry|Image deleted successfully"
-msgstr ""
-
msgid "ContainerRegistry|Keep and protect the images that matter most."
msgstr ""
@@ -5515,12 +5696,24 @@ msgstr ""
msgid "ContainerRegistry|Number of tags to retain:"
msgstr ""
+msgid "ContainerRegistry|Please contact your administrator."
+msgstr ""
+
msgid "ContainerRegistry|Push an image"
msgstr ""
msgid "ContainerRegistry|Quick Start"
msgstr ""
+msgid "ContainerRegistry|Regular expressions such as %{codeStart}.*-test%{codeEnd} or %{codeStart}dev-.*%{codeEnd} are supported"
+msgstr ""
+
+msgid "ContainerRegistry|Regular expressions such as %{codeStart}.*-test%{codeEnd} or %{codeStart}dev-.*%{codeEnd} are supported. To select all tags, use %{codeStart}.*%{codeEnd}"
+msgstr ""
+
+msgid "ContainerRegistry|Remember to run %{docLinkStart}garbage collection%{docLinkEnd} to remove the stale data from storage."
+msgstr ""
+
msgid "ContainerRegistry|Remove repository"
msgstr ""
@@ -5535,22 +5728,22 @@ msgstr[1] ""
msgid "ContainerRegistry|Retention policy has been Enabled"
msgstr ""
-msgid "ContainerRegistry|Something went wrong while deleting the image."
+msgid "ContainerRegistry|Something went wrong while fetching the expiration policy."
msgstr ""
-msgid "ContainerRegistry|Something went wrong while deleting the tag."
+msgid "ContainerRegistry|Something went wrong while fetching the repository list."
msgstr ""
-msgid "ContainerRegistry|Something went wrong while deleting the tags."
+msgid "ContainerRegistry|Something went wrong while fetching the tags list."
msgstr ""
-msgid "ContainerRegistry|Something went wrong while fetching the expiration policy."
+msgid "ContainerRegistry|Something went wrong while marking the tag for deletion."
msgstr ""
-msgid "ContainerRegistry|Something went wrong while fetching the packages list."
+msgid "ContainerRegistry|Something went wrong while marking the tags for deletion."
msgstr ""
-msgid "ContainerRegistry|Something went wrong while fetching the tags list."
+msgid "ContainerRegistry|Something went wrong while scheduling %{title} for deletion. Please try again."
msgstr ""
msgid "ContainerRegistry|Something went wrong while updating the expiration policy."
@@ -5559,16 +5752,25 @@ msgstr ""
msgid "ContainerRegistry|Tag"
msgstr ""
-msgid "ContainerRegistry|Tag deleted successfully"
-msgstr ""
-
msgid "ContainerRegistry|Tag expiration policy"
msgstr ""
msgid "ContainerRegistry|Tag expiration policy is designed to:"
msgstr ""
-msgid "ContainerRegistry|Tags deleted successfully"
+msgid "ContainerRegistry|Tag successfully marked for deletion."
+msgstr ""
+
+msgid "ContainerRegistry|Tags successfully marked for deletion."
+msgstr ""
+
+msgid "ContainerRegistry|Tags with names matching this regex pattern will %{italicStart}be preserved:%{italicEnd}"
+msgstr ""
+
+msgid "ContainerRegistry|Tags with names matching this regex pattern will %{italicStart}expire:%{italicEnd}"
+msgstr ""
+
+msgid "ContainerRegistry|The Container Registry tag expiration and retention policies for this project have not been enabled."
msgstr ""
msgid "ContainerRegistry|The last tag related to this image was recently removed. This empty image and any associated data will be automatically removed as part of the regular Garbage Collection process. If you have any questions, contact your administrator."
@@ -5589,16 +5791,16 @@ msgstr ""
msgid "ContainerRegistry|There are no container images stored for this project"
msgstr ""
-msgid "ContainerRegistry|This Registry contains deleted image tag data. Remember to run %{docLinkStart}garbage collection%{docLinkEnd} to remove the stale data from storage."
+msgid "ContainerRegistry|There was an error during the deletion of this image repository, please try again."
msgstr ""
msgid "ContainerRegistry|This image has no active tags"
msgstr ""
-msgid "ContainerRegistry|We are having trouble connecting to Docker, which could be due to an issue with your project name or path. %{docLinkStart}More Information%{docLinkEnd}"
+msgid "ContainerRegistry|This image repository is scheduled for deletion"
msgstr ""
-msgid "ContainerRegistry|Wildcards such as %{codeStart}.*-stable%{codeEnd} or %{codeStart}production/.*%{codeEnd} are supported. To select all tags, use %{codeStart}.*%{codeEnd}"
+msgid "ContainerRegistry|We are having trouble connecting to Docker, which could be due to an issue with your project name or path. %{docLinkStart}More Information%{docLinkEnd}"
msgstr ""
msgid "ContainerRegistry|With the Container Registry, every project can have its own space to store its Docker images. %{docLinkStart}More Information%{docLinkEnd}"
@@ -5853,7 +6055,7 @@ msgstr ""
msgid "Could not delete chat nickname %{chat_name}."
msgstr ""
-msgid "Could not find design"
+msgid "Could not find design."
msgstr ""
msgid "Could not remove the trigger."
@@ -5868,6 +6070,9 @@ msgstr ""
msgid "Could not revoke personal access token %{personal_access_token_name}."
msgstr ""
+msgid "Could not revoke project access token %{project_access_token_name}."
+msgstr ""
+
msgid "Could not save group ID"
msgstr ""
@@ -5925,6 +6130,9 @@ msgstr ""
msgid "Create a new branch"
msgstr ""
+msgid "Create a new deploy key for this project"
+msgstr ""
+
msgid "Create a new file as there are no files yet. Afterwards, you'll be able to commit your changes."
msgstr ""
@@ -6030,6 +6238,9 @@ msgstr ""
msgid "Create requirement"
msgstr ""
+msgid "Create snippet"
+msgstr ""
+
msgid "Create wildcard: %{searchTerm}"
msgstr ""
@@ -6051,6 +6262,9 @@ msgstr ""
msgid "Created"
msgstr ""
+msgid "Created %{timestamp}"
+msgstr ""
+
msgid "Created At"
msgstr ""
@@ -6407,6 +6621,9 @@ msgstr ""
msgid "CycleAnalytics|stage dropdown"
msgstr ""
+msgid "DAG"
+msgstr ""
+
msgid "DNS"
msgstr ""
@@ -6773,6 +6990,9 @@ msgstr ""
msgid "Deploy key was successfully updated."
msgstr ""
+msgid "Deploy keys allow read-only or read-write (if enabled) access to your repository. Deploy keys can be used for CI, staging or production servers. You can create a deploy key or add an existing one."
+msgstr ""
+
msgid "Deploy progress not found. To see pods, ensure your environment matches %{linkStart}deploy board criteria%{linkEnd}."
msgstr ""
@@ -6926,6 +7146,9 @@ msgstr ""
msgid "Deploying to"
msgstr ""
+msgid "Deployment Frequency"
+msgstr ""
+
msgid "Deployment|API"
msgstr ""
@@ -7444,9 +7667,6 @@ msgstr ""
msgid "Edit environment"
msgstr ""
-msgid "Edit epic description"
-msgstr ""
-
msgid "Edit file"
msgstr ""
@@ -8068,9 +8288,6 @@ msgstr ""
msgid "Epics"
msgstr ""
-msgid "Epics (Ultimate / Gold license only)"
-msgstr ""
-
msgid "Epics Roadmap"
msgstr ""
@@ -8080,6 +8297,9 @@ msgstr ""
msgid "Epics let you manage your portfolio of projects more efficiently and with less effort"
msgstr ""
+msgid "Epics, Issues, and Merge Requests"
+msgstr ""
+
msgid "Epics|Add an epic"
msgstr ""
@@ -8131,6 +8351,9 @@ msgstr ""
msgid "Epics|Something went wrong while fetching group epics."
msgstr ""
+msgid "Epics|Something went wrong while moving item."
+msgstr ""
+
msgid "Epics|Something went wrong while ordering item."
msgstr ""
@@ -8185,6 +8408,9 @@ msgstr ""
msgid "Error fetching network graph."
msgstr ""
+msgid "Error fetching payload data."
+msgstr ""
+
msgid "Error fetching projects"
msgstr ""
@@ -8194,9 +8420,6 @@ msgstr ""
msgid "Error fetching the dependency list. Please check your network connection and try again."
msgstr ""
-msgid "Error fetching usage ping data."
-msgstr ""
-
msgid "Error loading branch data. Please try again."
msgstr ""
@@ -8338,6 +8561,9 @@ msgstr ""
msgid "Errors"
msgstr ""
+msgid "Errors:"
+msgstr ""
+
msgid "Estimated"
msgstr ""
@@ -8542,12 +8768,18 @@ msgstr ""
msgid "Export as CSV"
msgstr ""
+msgid "Export group"
+msgstr ""
+
msgid "Export issues"
msgstr ""
msgid "Export project"
msgstr ""
+msgid "Export this group with all related data to a new GitLab instance. Once complete, you can import the data file from the \"New Group\" page."
+msgstr ""
+
msgid "Export this project with all its related data in order to move your project to a new GitLab instance. Once the export is finished, you can import the file from the \"New Project\" page."
msgstr ""
@@ -8617,9 +8849,6 @@ msgstr ""
msgid "Failed Jobs"
msgstr ""
-msgid "Failed create wiki"
-msgstr ""
-
msgid "Failed to add a Zoom meeting"
msgstr ""
@@ -8659,6 +8888,9 @@ msgstr ""
msgid "Failed to create resources"
msgstr ""
+msgid "Failed to create wiki"
+msgstr ""
+
msgid "Failed to delete board. Please try again."
msgstr ""
@@ -8668,7 +8900,7 @@ msgstr ""
msgid "Failed to enqueue the rebase operation, possibly due to a long-lived transaction. Try again later."
msgstr ""
-msgid "Failed to find import label for jira import."
+msgid "Failed to find import label for Jira import."
msgstr ""
msgid "Failed to get ref."
@@ -8692,6 +8924,12 @@ msgstr ""
msgid "Failed to load groups & users."
msgstr ""
+msgid "Failed to load labels. Please try again."
+msgstr ""
+
+msgid "Failed to load milestones. Please try again."
+msgstr ""
+
msgid "Failed to load related branches"
msgstr ""
@@ -9067,7 +9305,10 @@ msgstr ""
msgid "Filter by milestone name"
msgstr ""
-msgid "Filter by name..."
+msgid "Filter by name"
+msgstr ""
+
+msgid "Filter by status"
msgstr ""
msgid "Filter by two-factor authentication"
@@ -9079,12 +9320,18 @@ msgstr ""
msgid "Filter projects"
msgstr ""
+msgid "Filter results"
+msgstr ""
+
msgid "Filter results by group"
msgstr ""
msgid "Filter results by project"
msgstr ""
+msgid "Filter results..."
+msgstr ""
+
msgid "Filter your projects by name"
msgstr ""
@@ -9373,10 +9620,10 @@ msgstr ""
msgid "GeoNodes|Checksummed"
msgstr ""
-msgid "GeoNodes|Container repositories"
+msgid "GeoNodes|Consult Geo troubleshooting information"
msgstr ""
-msgid "GeoNodes|Data is out of date from %{timeago}"
+msgid "GeoNodes|Container repositories"
msgstr ""
msgid "GeoNodes|Data replication lag"
@@ -9418,6 +9665,9 @@ msgstr ""
msgid "GeoNodes|Last event ID seen from primary"
msgstr ""
+msgid "GeoNodes|Learn more about Geo node statuses"
+msgstr ""
+
msgid "GeoNodes|Loading nodes"
msgstr ""
@@ -9433,6 +9683,9 @@ msgstr ""
msgid "GeoNodes|Node was successfully removed."
msgstr ""
+msgid "GeoNodes|Node's status was updated %{timeAgo}."
+msgstr ""
+
msgid "GeoNodes|Not checksummed"
msgstr ""
@@ -9463,7 +9716,10 @@ msgstr ""
msgid "GeoNodes|Repository verification progress"
msgstr ""
-msgid "GeoNodes|Selective"
+msgid "GeoNodes|Selective (%{syncLabel})"
+msgstr ""
+
+msgid "GeoNodes|Selective synchronization"
msgstr ""
msgid "GeoNodes|Something went wrong while changing node status"
@@ -9490,6 +9746,9 @@ msgstr ""
msgid "GeoNodes|Unverified"
msgstr ""
+msgid "GeoNodes|Updated %{timeAgo}"
+msgstr ""
+
msgid "GeoNodes|Used slots"
msgstr ""
@@ -9529,16 +9788,16 @@ msgstr ""
msgid "Geo|All"
msgstr ""
-msgid "Geo|All projects"
+msgid "Geo|All %{replicable_type}"
msgstr ""
-msgid "Geo|All projects are being scheduled for re-sync"
+msgid "Geo|All projects"
msgstr ""
-msgid "Geo|All projects are being scheduled for re-verify"
+msgid "Geo|All projects are being scheduled for resync"
msgstr ""
-msgid "Geo|Batch operations"
+msgid "Geo|All projects are being scheduled for reverify"
msgstr ""
msgid "Geo|Could not remove tracking entry for an existing project."
@@ -9550,9 +9809,15 @@ msgstr ""
msgid "Geo|Failed"
msgstr ""
+msgid "Geo|Filter by status"
+msgstr ""
+
msgid "Geo|Geo Status"
msgstr ""
+msgid "Geo|In progress"
+msgstr ""
+
msgid "Geo|In sync"
msgstr ""
@@ -9577,9 +9842,6 @@ msgstr ""
msgid "Geo|Not synced yet"
msgstr ""
-msgid "Geo|Pending"
-msgstr ""
-
msgid "Geo|Pending synchronization"
msgstr ""
@@ -9616,7 +9878,7 @@ msgstr ""
msgid "Geo|Resync"
msgstr ""
-msgid "Geo|Resync all projects"
+msgid "Geo|Resync all"
msgstr ""
msgid "Geo|Retry count"
@@ -9625,7 +9887,7 @@ msgstr ""
msgid "Geo|Reverify"
msgstr ""
-msgid "Geo|Reverify all projects"
+msgid "Geo|Reverify all"
msgstr ""
msgid "Geo|Status"
@@ -9967,9 +10229,6 @@ msgstr ""
msgid "Go to file"
msgstr ""
-msgid "Go to file (MRs only)"
-msgstr ""
-
msgid "Go to file permalink (while viewing a file)"
msgstr ""
@@ -10414,6 +10673,9 @@ msgstr ""
msgid "GroupSettings|Disable group mentions"
msgstr ""
+msgid "GroupSettings|Export group"
+msgstr ""
+
msgid "GroupSettings|If the parent group's visibility is lower than the group current visibility, visibility levels for subgroups and projects will be changed to match the new parent group's visibility."
msgstr ""
@@ -10492,6 +10754,9 @@ msgstr ""
msgid "Groups (%{count})"
msgstr ""
+msgid "Groups (%{groups})"
+msgstr ""
+
msgid "Groups can also be nested by creating %{subgroup_docs_link_start}subgroups%{subgroup_docs_link_end}."
msgstr ""
@@ -10757,15 +11022,12 @@ msgstr ""
msgid "ID:"
msgstr ""
-msgid "IDE|Allow live previews of JavaScript projects in the Web IDE using CodeSandbox client side evaluation."
+msgid "IDE|Allow live previews of JavaScript projects in the Web IDE using CodeSandbox Live Preview."
msgstr ""
msgid "IDE|Back"
msgstr ""
-msgid "IDE|Client side evaluation"
-msgstr ""
-
msgid "IDE|Commit"
msgstr ""
@@ -10829,6 +11091,9 @@ msgstr ""
msgid "If any job surpasses this timeout threshold, it will be marked as failed. Human readable time input language is accepted like \"1 hour\". Values without specification represent seconds."
msgstr ""
+msgid "If blank, set allowable lifetime to %{instance_level_policy_in_words}, as defined by the instance admin. Once set, existing tokens for users in this group may be revoked."
+msgstr ""
+
msgid "If checked, group owners can manage LDAP group links and LDAP member overrides"
msgstr ""
@@ -10868,6 +11133,9 @@ msgstr ""
msgid "If you lose your recovery codes you can generate new ones, invalidating all previous codes."
msgstr ""
+msgid "If you reach 100%% storage capacity, you will not be able to: %{base_message}"
+msgstr ""
+
msgid "If your HTTP repository is not publicly accessible, add your credentials."
msgstr ""
@@ -10979,6 +11247,9 @@ msgstr ""
msgid "Import repository"
msgstr ""
+msgid "Import started by: %{importInitiator}"
+msgstr ""
+
msgid "Import tasks"
msgstr ""
@@ -11045,6 +11316,9 @@ msgstr ""
msgid "In order to tailor your experience with GitLab we<br>would like to know a bit more about you."
msgstr ""
+msgid "In progress"
+msgstr ""
+
msgid "In the next step, you'll be able to select the projects you want to import."
msgstr ""
@@ -11392,15 +11666,15 @@ msgstr ""
msgid "Issues"
msgstr ""
-msgid "Issues / Merge Requests"
-msgstr ""
-
msgid "Issues Analytics"
msgstr ""
msgid "Issues Rate Limits"
msgstr ""
+msgid "Issues and Merge Requests"
+msgstr ""
+
msgid "Issues can be bugs, tasks or ideas to be discussed. Also, issues are searchable and filterable."
msgstr ""
@@ -11422,10 +11696,10 @@ msgstr ""
msgid "IssuesAnalytics|Avg/Month:"
msgstr ""
-msgid "IssuesAnalytics|Issues created"
+msgid "IssuesAnalytics|Issues opened"
msgstr ""
-msgid "IssuesAnalytics|Issues created per month"
+msgid "IssuesAnalytics|Issues opened per month"
msgstr ""
msgid "IssuesAnalytics|Last 12 months"
@@ -11479,6 +11753,9 @@ msgstr ""
msgid "Jira integration not configured."
msgstr ""
+msgid "Jira project: %{importProject}"
+msgstr ""
+
msgid "JiraService|Events for %{noteable_model_name} are disabled."
msgstr ""
@@ -11868,6 +12145,9 @@ msgstr ""
msgid "Lead"
msgstr ""
+msgid "Lead Time"
+msgstr ""
+
msgid "Learn GitLab"
msgstr ""
@@ -11880,6 +12160,9 @@ msgstr ""
msgid "Learn how to %{no_packages_link_start}publish and share your packages%{no_packages_link_end} with GitLab."
msgstr ""
+msgid "Learn how to enable synchronization"
+msgstr ""
+
msgid "Learn more"
msgstr ""
@@ -12211,9 +12494,6 @@ msgstr ""
msgid "Live preview"
msgstr ""
-msgid "Load more vulnerabilities"
-msgstr ""
-
msgid "Loading"
msgstr ""
@@ -12364,9 +12644,6 @@ msgstr ""
msgid "Manage"
msgstr ""
-msgid "Manage Git repositories with fine-grained access controls that keep your code secure. Perform code reviews and enhance collaboration with merge requests. Each project can also have an issue tracker and a wiki."
-msgstr ""
-
msgid "Manage Web IDE features"
msgstr ""
@@ -12397,6 +12674,9 @@ msgstr ""
msgid "Manage your license"
msgstr ""
+msgid "Managed Account"
+msgstr ""
+
msgid "Manifest"
msgstr ""
@@ -12661,6 +12941,9 @@ msgstr ""
msgid "Merge Requests in Review"
msgstr ""
+msgid "Merge automatically (%{strategy})"
+msgstr ""
+
msgid "Merge commit message"
msgstr ""
@@ -12814,6 +13097,12 @@ msgstr ""
msgid "Merged branches are being deleted. This can take some time depending on the number of branches. Please refresh the page to see changes."
msgstr ""
+msgid "Merged this merge request."
+msgstr ""
+
+msgid "Merges this merge request immediately."
+msgstr ""
+
msgid "Merges this merge request when the pipeline succeeds."
msgstr ""
@@ -12921,12 +13210,15 @@ msgid_plural "Metrics|Edit metrics"
msgstr[0] ""
msgstr[1] ""
-msgid "Metrics|Environment"
+msgid "Metrics|Expand panel"
msgstr ""
msgid "Metrics|For grouping similar metrics"
msgstr ""
+msgid "Metrics|Go back (Esc)"
+msgstr ""
+
msgid "Metrics|Invalid time range, please verify."
msgstr ""
@@ -12957,9 +13249,6 @@ msgstr ""
msgid "Metrics|Refresh dashboard"
msgstr ""
-msgid "Metrics|Show last"
-msgstr ""
-
msgid "Metrics|There was an error creating the dashboard."
msgstr ""
@@ -13038,6 +13327,9 @@ msgstr ""
msgid "Microsoft Azure"
msgstr ""
+msgid "Middleman project with Static Site Editor support"
+msgstr ""
+
msgid "Migrated %{success_count}/%{total_count} files."
msgstr ""
@@ -13483,10 +13775,10 @@ msgstr ""
msgid "Next"
msgstr ""
-msgid "Next file in diff (MRs only)"
+msgid "Next file in diff"
msgstr ""
-msgid "Next unresolved discussion (MRs only)"
+msgid "Next unresolved discussion"
msgstr ""
msgid "Nickname"
@@ -13546,6 +13838,9 @@ msgstr ""
msgid "No changes between %{ref_start}%{source_branch}%{ref_end} and %{ref_start}%{target_branch}%{ref_end}"
msgstr ""
+msgid "No child epics match applied filters"
+msgstr ""
+
msgid "No connection could be made to a Gitaly Server, please check your logs!"
msgstr ""
@@ -13684,15 +13979,6 @@ msgstr ""
msgid "No thanks, don't show this again"
msgstr ""
-msgid "No vulnerabilities found for this group"
-msgstr ""
-
-msgid "No vulnerabilities found for this pipeline"
-msgstr ""
-
-msgid "No vulnerabilities found for this project"
-msgstr ""
-
msgid "No vulnerabilities present"
msgstr ""
@@ -13816,6 +14102,9 @@ msgstr ""
msgid "Nothing to preview."
msgstr ""
+msgid "Nothing to synchronize"
+msgstr ""
+
msgid "Notification events"
msgstr ""
@@ -13987,6 +14276,9 @@ msgstr ""
msgid "Once removed, the fork relationship cannot be restored and you will no longer be able to send merge requests to the source."
msgstr ""
+msgid "Once the exported file is ready you can download it from this page."
+msgstr ""
+
msgid "Once the exported file is ready, you will receive a notification email with a download link, or you can download it from this page."
msgstr ""
@@ -14088,9 +14380,6 @@ msgstr ""
msgid "Open sidebar"
msgstr ""
-msgid "Open source software to collaborate on code"
-msgstr ""
-
msgid "Open: %{openIssuesCount}"
msgstr ""
@@ -14193,6 +14482,9 @@ msgstr ""
msgid "OutdatedBrowser|You can provide feedback %{feedback_link_start}on this issue%{feedback_link_end} or via your usual support channels."
msgstr ""
+msgid "Overridden"
+msgstr ""
+
msgid "Overview"
msgstr ""
@@ -14244,6 +14536,9 @@ msgstr ""
msgid "PackageRegistry|Conan Command"
msgstr ""
+msgid "PackageRegistry|Copy .pypirc content"
+msgstr ""
+
msgid "PackageRegistry|Copy Conan Command"
msgstr ""
@@ -14265,6 +14560,9 @@ msgstr ""
msgid "PackageRegistry|Copy NuGet Setup Command"
msgstr ""
+msgid "PackageRegistry|Copy Pip command"
+msgstr ""
+
msgid "PackageRegistry|Copy and paste this inside your %{codeStart}pom.xml%{codeEnd} %{codeStart}dependencies%{codeEnd} block."
msgstr ""
@@ -14298,6 +14596,12 @@ msgstr ""
msgid "PackageRegistry|For more information on the NuGet registry, %{linkStart}see the documentation%{linkEnd}."
msgstr ""
+msgid "PackageRegistry|For more information on the PyPi registry, %{linkStart}see the documentation%{linkEnd}."
+msgstr ""
+
+msgid "PackageRegistry|If you haven't already done so, you will need to add the below to your %{codeStart}.pypirc%{codeEnd} file."
+msgstr ""
+
msgid "PackageRegistry|If you haven't already done so, you will need to add the below to your %{codeStart}pom.xml%{codeEnd} file."
msgstr ""
@@ -14307,6 +14611,9 @@ msgstr ""
msgid "PackageRegistry|Learn how to %{noPackagesLinkStart}publish and share your packages%{noPackagesLinkEnd} with GitLab."
msgstr ""
+msgid "PackageRegistry|Manually Published"
+msgstr ""
+
msgid "PackageRegistry|Maven"
msgstr ""
@@ -14325,12 +14632,18 @@ msgstr ""
msgid "PackageRegistry|NuGet Command"
msgstr ""
+msgid "PackageRegistry|Pip Command"
+msgstr ""
+
msgid "PackageRegistry|Pipeline %{linkStart}%{linkEnd} triggered %{timestamp} by %{author}"
msgstr ""
msgid "PackageRegistry|Published to the repository at %{timestamp}"
msgstr ""
+msgid "PackageRegistry|PyPi"
+msgstr ""
+
msgid "PackageRegistry|Registry Setup"
msgstr ""
@@ -14367,6 +14680,9 @@ msgstr ""
msgid "PackageRegistry|npm"
msgstr ""
+msgid "PackageRegistry|published by %{author}"
+msgstr ""
+
msgid "PackageRegistry|yarn"
msgstr ""
@@ -14382,6 +14698,9 @@ msgstr ""
msgid "PackageType|NuGet"
msgstr ""
+msgid "PackageType|PyPi"
+msgstr ""
+
msgid "Packages"
msgstr ""
@@ -14475,6 +14794,9 @@ msgstr ""
msgid "Past due"
msgstr ""
+msgid "Paste a machine public key here. Read more about how to generate it"
+msgstr ""
+
msgid "Paste a machine public key here. Read more about how to generate it %{link_start}here%{link_end}"
msgstr ""
@@ -14490,9 +14812,6 @@ msgstr ""
msgid "Path"
msgstr ""
-msgid "Path, transfer, remove"
-msgstr ""
-
msgid "Path:"
msgstr ""
@@ -14523,7 +14842,7 @@ msgstr ""
msgid "Percentage"
msgstr ""
-msgid "Perform advanced options such as changing path, transferring, or removing the group."
+msgid "Perform advanced options such as changing path, transferring, exporting, or removing the group."
msgstr ""
msgid "Perform common operations on GitLab project"
@@ -14604,9 +14923,6 @@ msgstr ""
msgid "Pipeline minutes quota"
msgstr ""
-msgid "Pipeline quota"
-msgstr ""
-
msgid "Pipeline subscriptions"
msgstr ""
@@ -14616,6 +14932,9 @@ msgstr ""
msgid "Pipeline: %{status}"
msgstr ""
+msgid "PipelineCharts|CI / CD Analytics"
+msgstr ""
+
msgid "PipelineCharts|Failed:"
msgstr ""
@@ -14694,15 +15013,6 @@ msgstr ""
msgid "Pipelines settings for '%{project_name}' were successfully updated."
msgstr ""
-msgid "Pipelines| to purchase more minutes."
-msgstr ""
-
-msgid "Pipelines|%{namespace_name} has exceeded its pipeline minutes quota."
-msgstr ""
-
-msgid "Pipelines|%{namespace_name} has less than %{notification_level}%% of CI minutes available."
-msgstr ""
-
msgid "Pipelines|API"
msgstr ""
@@ -14724,10 +15034,13 @@ msgstr ""
msgid "Pipelines|Get started with Pipelines"
msgstr ""
-msgid "Pipelines|Loading Pipelines"
+msgid "Pipelines|Group %{namespace_name} has %{percentage}%% or less Shared Runner Pipeline minutes remaining. Once it runs out, no new jobs or pipelines in its projects will run."
msgstr ""
-msgid "Pipelines|Pipelines will not run anymore on shared Runners."
+msgid "Pipelines|Group %{namespace_name} has exceeded its pipeline minutes quota. Unless you buy additional pipeline minutes, no new jobs or pipelines in its projects will run."
+msgstr ""
+
+msgid "Pipelines|Loading Pipelines"
msgstr ""
msgid "Pipelines|Project cache successfully reset."
@@ -14958,6 +15271,9 @@ msgstr ""
msgid "Please select"
msgstr ""
+msgid "Please select a Jira project"
+msgstr ""
+
msgid "Please select a country"
msgstr ""
@@ -15003,6 +15319,9 @@ msgstr ""
msgid "Please wait while we import the repository for you. Refresh at will."
msgstr ""
+msgid "Plugins directory is deprecated and will be removed in 14.0. Please move this file into /file_hooks directory."
+msgstr ""
+
msgid "Pod does not exist"
msgstr ""
@@ -15120,7 +15439,7 @@ msgstr ""
msgid "Prevent users from changing their profile name"
msgstr ""
-msgid "Prevent users from modifing merge request approvers list"
+msgid "Prevent users from modifying merge request approvers list"
msgstr ""
msgid "Prevent users from performing write operations on GitLab while performing maintenance."
@@ -15141,10 +15460,10 @@ msgstr ""
msgid "Previous Artifacts"
msgstr ""
-msgid "Previous file in diff (MRs only)"
+msgid "Previous file in diff"
msgstr ""
-msgid "Previous unresolved discussion (MRs only)"
+msgid "Previous unresolved discussion"
msgstr ""
msgid "Primary"
@@ -15177,6 +15496,9 @@ msgstr ""
msgid "Private profile"
msgstr ""
+msgid "Private projects Minutes cost factor"
+msgstr ""
+
msgid "Private projects can be created in your personal namespace with:"
msgstr ""
@@ -16005,6 +16327,9 @@ msgstr ""
msgid "ProjectSettings|With GitLab Pages you can host your static websites on GitLab"
msgstr ""
+msgid "ProjectSettings|With Metrics Dashboard you can visualize this project performance metrics"
+msgstr ""
+
msgid "ProjectTemplates|.NET Core"
msgstr ""
@@ -16068,6 +16393,9 @@ msgstr ""
msgid "ProjectTemplates|Spring"
msgstr ""
+msgid "ProjectTemplates|Static Site Editor/Middleman"
+msgstr ""
+
msgid "ProjectTemplates|iOS (Swift)"
msgstr ""
@@ -16287,13 +16615,13 @@ msgstr ""
msgid "PrometheusService|Prometheus is being automatically managed on your clusters"
msgstr ""
-msgid "PrometheusService|These metrics will only be monitored after your first deployment to an environment"
+msgid "PrometheusService|Select the Active checkbox to override the Auto Configuration with custom settings. If unchecked, Auto Configuration settings are used."
msgstr ""
-msgid "PrometheusService|Time-series monitoring service"
+msgid "PrometheusService|These metrics will only be monitored after your first deployment to an environment"
msgstr ""
-msgid "PrometheusService|To enable manual configuration, uninstall Prometheus from your clusters"
+msgid "PrometheusService|Time-series monitoring service"
msgstr ""
msgid "PrometheusService|To enable the installation of Prometheus on your clusters, deactivate the manual configuration below"
@@ -16479,6 +16807,9 @@ msgstr ""
msgid "Protip:"
msgstr ""
+msgid "Protocol"
+msgstr ""
+
msgid "Provider"
msgstr ""
@@ -16503,6 +16834,9 @@ msgstr ""
msgid "Public pipelines"
msgstr ""
+msgid "Public projects Minutes cost factor"
+msgstr ""
+
msgid "Pull"
msgstr ""
@@ -16703,6 +17037,9 @@ msgid_plural "Refreshing in %d seconds to show the updated status..."
msgstr[0] ""
msgstr[1] ""
+msgid "Regenerate export"
+msgstr ""
+
msgid "Regenerate instance ID"
msgstr ""
@@ -17080,9 +17417,29 @@ msgstr ""
msgid "Reports|%{combinedString} and %{resolvedString}"
msgstr ""
+msgid "Reports|Accessibility scanning detected %d issue for the source branch only"
+msgid_plural "Reports|Accessibility scanning detected %d issues for the source branch only"
+msgstr[0] ""
+msgstr[1] ""
+
+msgid "Reports|Accessibility scanning detected no issues for the source branch only"
+msgstr ""
+
+msgid "Reports|Accessibility scanning failed loading results"
+msgstr ""
+
+msgid "Reports|Accessibility scanning results are being parsed"
+msgstr ""
+
msgid "Reports|Actions"
msgstr ""
+msgid "Reports|An error occured while loading report"
+msgstr ""
+
+msgid "Reports|An error occurred while loading %{name} results"
+msgstr ""
+
msgid "Reports|Class"
msgstr ""
@@ -17188,7 +17545,7 @@ msgstr ""
msgid "Requested %{time_ago}"
msgstr ""
-msgid "Requested design version does not exist"
+msgid "Requested design version does not exist."
msgstr ""
msgid "Requested states are invalid"
@@ -17372,6 +17729,9 @@ msgstr ""
msgid "Resync"
msgstr ""
+msgid "Resync all"
+msgstr ""
+
msgid "Resync all %{replicableType}"
msgstr ""
@@ -17440,6 +17800,9 @@ msgstr ""
msgid "Revoked personal access token %{personal_access_token_name}!"
msgstr ""
+msgid "Revoked project access token %{project_access_token_name}!"
+msgstr ""
+
msgid "RightSidebar|adding a"
msgstr ""
@@ -17488,6 +17851,9 @@ msgstr ""
msgid "Runner token"
msgstr ""
+msgid "Runner tokens"
+msgstr ""
+
msgid "Runner was not updated."
msgstr ""
@@ -17542,6 +17908,9 @@ msgstr ""
msgid "SAML SSO for %{group_name}"
msgstr ""
+msgid "SAML discovery tokens"
+msgstr ""
+
msgid "SAML for %{group_name}"
msgstr ""
@@ -17626,12 +17995,18 @@ msgstr ""
msgid "Scheduled"
msgstr ""
+msgid "Scheduled to merge this merge request (%{strategy})."
+msgstr ""
+
msgid "Scheduled to merge this merge request when the pipeline succeeds."
msgstr ""
msgid "Schedules"
msgstr ""
+msgid "Schedules to merge this merge request (%{strategy})."
+msgstr ""
+
msgid "Scheduling"
msgstr ""
@@ -17689,6 +18064,9 @@ msgstr ""
msgid "Search branches and tags"
msgstr ""
+msgid "Search by author"
+msgstr ""
+
msgid "Search files"
msgstr ""
@@ -17834,11 +18212,6 @@ msgid_plural "SearchResults|snippets"
msgstr[0] ""
msgstr[1] ""
-msgid "SearchResults|snippet result"
-msgid_plural "SearchResults|snippet results"
-msgstr[0] ""
-msgstr[1] ""
-
msgid "SearchResults|user"
msgid_plural "SearchResults|users"
msgstr[0] ""
@@ -17849,6 +18222,9 @@ msgid_plural "SearchResults|wiki results"
msgstr[0] ""
msgstr[1] ""
+msgid "Searching by both author and message is currently not supported."
+msgstr ""
+
msgid "Seat Link"
msgstr ""
@@ -17876,184 +18252,232 @@ msgstr ""
msgid "Security Dashboard"
msgstr ""
-msgid "Security Dashboard|Error fetching the vulnerability counts. Please check your network connection and try again."
+msgid "Security configuration help link"
+msgstr ""
+
+msgid "Security dashboard"
msgstr ""
-msgid "Security Dashboard|Error fetching the vulnerability list. Please check your network connection and try again."
+msgid "Security report is out of date. Please update your branch with the latest changes from the target branch (%{targetBranchName})"
msgstr ""
-msgid "Security Dashboard|Issue Created"
+msgid "Security report is out of date. Run %{newPipelineLinkStart}a new pipeline%{newPipelineLinkEnd} for the target branch (%{targetBranchName})"
msgstr ""
-msgid "Security Reports|Comment added to '%{vulnerabilityName}'"
+msgid "SecurityConfiguration|Configured"
msgstr ""
-msgid "Security Reports|Comment deleted on '%{vulnerabilityName}'"
+msgid "SecurityConfiguration|Feature"
msgstr ""
-msgid "Security Reports|Comment edited on '%{vulnerabilityName}'"
+msgid "SecurityConfiguration|Feature documentation for %{featureName}"
msgstr ""
-msgid "Security Reports|Create issue"
+msgid "SecurityConfiguration|Not yet configured"
msgstr ""
-msgid "Security Reports|Dismiss vulnerability"
+msgid "SecurityConfiguration|Secure features"
msgstr ""
-msgid "Security Reports|Dismissed '%{vulnerabilityName}'"
+msgid "SecurityConfiguration|Status"
msgstr ""
-msgid "Security Reports|Dismissed '%{vulnerabilityName}'. Turn off the hide dismissed toggle to view."
+msgid "SecurityReports|%{firstProject} and %{secondProject}"
msgstr ""
-msgid "Security Reports|Either you don't have permission to view this dashboard or the dashboard has not been setup. Please check your permission settings with your administrator or check your dashboard configurations to proceed."
+msgid "SecurityReports|%{firstProject}, %{secondProject}, and %{rest}"
msgstr ""
-msgid "Security Reports|Learn more about setting up your dashboard"
+msgid "SecurityReports|Add a project to your dashboard"
msgstr ""
-msgid "Security Reports|More info"
+msgid "SecurityReports|Add or remove projects from your dashboard"
msgstr ""
-msgid "Security Reports|Oops, something doesn't seem right."
+msgid "SecurityReports|Add projects"
msgstr ""
-msgid "Security Reports|Security reports can only be accessed by authorized users."
+msgid "SecurityReports|Comment added to '%{vulnerabilityName}'"
msgstr ""
-msgid "Security Reports|There was an error adding the comment."
+msgid "SecurityReports|Comment deleted on '%{vulnerabilityName}'"
msgstr ""
-msgid "Security Reports|There was an error creating the issue."
+msgid "SecurityReports|Comment edited on '%{vulnerabilityName}'"
msgstr ""
-msgid "Security Reports|There was an error creating the merge request."
+msgid "SecurityReports|Create issue"
msgstr ""
-msgid "Security Reports|There was an error deleting the comment."
+msgid "SecurityReports|Dismiss Selected"
msgstr ""
-msgid "Security Reports|There was an error dismissing the vulnerabilities."
+msgid "SecurityReports|Dismiss vulnerability"
msgstr ""
-msgid "Security Reports|There was an error dismissing the vulnerability."
+msgid "SecurityReports|Dismissed '%{vulnerabilityName}'"
msgstr ""
-msgid "Security Reports|There was an error reverting the dismissal."
+msgid "SecurityReports|Dismissed '%{vulnerabilityName}'. Turn off the hide dismissed toggle to view."
msgstr ""
-msgid "Security Reports|There was an error reverting this dismissal."
+msgid "SecurityReports|Each vulnerability now has a unique page that can be directly linked to, shared, referenced, and tracked as the single source of truth. Vulnerability occurrences also persist across scanner runs, which improves tracking and visibility and reduces duplicates between scans."
msgstr ""
-msgid "Security Reports|Undo dismiss"
+msgid "SecurityReports|Edit dashboard"
msgstr ""
-msgid "Security Reports|You do not have sufficient permissions to access this report"
+msgid "SecurityReports|Either you don't have permission to view this dashboard or the dashboard has not been setup. Please check your permission settings with your administrator or check your dashboard configurations to proceed."
msgstr ""
-msgid "Security Reports|You must sign in as an authorized user to see this report"
+msgid "SecurityReports|Error fetching the vulnerability counts. Please check your network connection and try again."
msgstr ""
-msgid "Security configuration help link"
+msgid "SecurityReports|Error fetching the vulnerability list. Please check your network connection and try again."
msgstr ""
-msgid "Security dashboard"
+msgid "SecurityReports|False positive"
msgstr ""
-msgid "Security report is out of date. Please update your branch with the latest changes from the target branch (%{targetBranchName})"
+msgid "SecurityReports|Hide dismissed"
msgstr ""
-msgid "Security report is out of date. Run %{newPipelineLinkStart}a new pipeline%{newPipelineLinkEnd} for the target branch (%{targetBranchName})"
+msgid "SecurityReports|Introducing standalone vulnerabilities"
msgstr ""
-msgid "SecurityConfiguration|Configured"
+msgid "SecurityReports|Issue Created"
msgstr ""
-msgid "SecurityConfiguration|Feature"
+msgid "SecurityReports|Learn More"
msgstr ""
-msgid "SecurityConfiguration|Feature documentation for %{featureName}"
+msgid "SecurityReports|Learn more about setting up your dashboard"
msgstr ""
-msgid "SecurityConfiguration|Not yet configured"
+msgid "SecurityReports|Load more vulnerabilities"
msgstr ""
-msgid "SecurityConfiguration|Secure features"
+msgid "SecurityReports|Monitor vulnerabilities in your code"
msgstr ""
-msgid "SecurityConfiguration|Status"
+msgid "SecurityReports|More info"
msgstr ""
-msgid "SecurityDashboard|%{firstProject} and %{secondProject}"
+msgid "SecurityReports|More information"
msgstr ""
-msgid "SecurityDashboard|%{firstProject}, %{secondProject}, and %{rest}"
+msgid "SecurityReports|No vulnerabilities found for dashboard"
msgstr ""
-msgid "SecurityDashboard|Add a project to your dashboard"
+msgid "SecurityReports|No vulnerabilities found for this group"
msgstr ""
-msgid "SecurityDashboard|Add or remove projects from your dashboard"
+msgid "SecurityReports|No vulnerabilities found for this pipeline"
msgstr ""
-msgid "SecurityDashboard|Add projects"
+msgid "SecurityReports|No vulnerabilities found for this project"
msgstr ""
-msgid "SecurityDashboard|Edit dashboard"
+msgid "SecurityReports|Oops, something doesn't seem right."
msgstr ""
-msgid "SecurityDashboard|Hide dismissed"
+msgid "SecurityReports|Pipeline %{pipelineLink} triggered %{timeago} by %{user}"
msgstr ""
-msgid "SecurityDashboard|Monitor vulnerabilities in your code"
+msgid "SecurityReports|Project"
msgstr ""
-msgid "SecurityDashboard|More information"
+msgid "SecurityReports|Projects added"
msgstr ""
-msgid "SecurityDashboard|No vulnerabilities found for dashboard"
+msgid "SecurityReports|Remove project from dashboard"
msgstr ""
-msgid "SecurityDashboard|Pipeline %{pipelineLink} triggered %{timeago} by %{user}"
+msgid "SecurityReports|Report type"
msgstr ""
-msgid "SecurityDashboard|Project"
+msgid "SecurityReports|Return to dashboard"
msgstr ""
-msgid "SecurityDashboard|Projects added"
+msgid "SecurityReports|Security Dashboard"
msgstr ""
-msgid "SecurityDashboard|Remove project from dashboard"
+msgid "SecurityReports|Security reports can only be accessed by authorized users."
msgstr ""
-msgid "SecurityDashboard|Report type"
+msgid "SecurityReports|Select a project to add by using the project search field above."
msgstr ""
-msgid "SecurityDashboard|Return to dashboard"
+msgid "SecurityReports|Select a reason"
msgstr ""
-msgid "SecurityDashboard|Security Dashboard"
+msgid "SecurityReports|Severity"
msgstr ""
-msgid "SecurityDashboard|Select a project to add by using the project search field above."
+msgid "SecurityReports|Status"
msgstr ""
-msgid "SecurityDashboard|Severity"
+msgid "SecurityReports|The security dashboard displays the latest security findings for projects you wish to monitor. Select \"Edit dashboard\" to add and remove projects."
msgstr ""
-msgid "SecurityDashboard|Status"
+msgid "SecurityReports|The security dashboard displays the latest security report. Use it to find and fix vulnerabilities."
msgstr ""
-msgid "SecurityDashboard|The security dashboard displays the latest security findings for projects you wish to monitor. Select \"Edit dashboard\" to add and remove projects."
+msgid "SecurityReports|There was an error adding the comment."
msgstr ""
-msgid "SecurityDashboard|The security dashboard displays the latest security report. Use it to find and fix vulnerabilities."
+msgid "SecurityReports|There was an error creating the issue."
msgstr ""
-msgid "SecurityDashboard|There was an error while generating the report."
+msgid "SecurityReports|There was an error creating the merge request."
msgstr ""
-msgid "SecurityDashboard|Unable to add %{invalidProjects}"
+msgid "SecurityReports|There was an error deleting the comment."
+msgstr ""
+
+msgid "SecurityReports|There was an error dismissing the vulnerabilities."
+msgstr ""
+
+msgid "SecurityReports|There was an error dismissing the vulnerability."
+msgstr ""
+
+msgid "SecurityReports|There was an error reverting the dismissal."
+msgstr ""
+
+msgid "SecurityReports|There was an error reverting this dismissal."
+msgstr ""
+
+msgid "SecurityReports|There was an error while generating the report."
+msgstr ""
+
+msgid "SecurityReports|Unable to add %{invalidProjects}"
+msgstr ""
+
+msgid "SecurityReports|Undo dismiss"
+msgstr ""
+
+msgid "SecurityReports|While it's rare to have no vulnerabilities for your group, it can happen. In any event, we ask that you double check your settings to make sure you've set up your dashboard correctly."
+msgstr ""
+
+msgid "SecurityReports|While it's rare to have no vulnerabilities for your pipeline, it can happen. In any event, we ask that you double check your settings to make sure all security scanning jobs have passed successfully."
+msgstr ""
+
+msgid "SecurityReports|While it's rare to have no vulnerabilities for your project, it can happen. In any event, we ask that you double check your settings to make sure you've set up your dashboard correctly."
+msgstr ""
+
+msgid "SecurityReports|While it's rare to have no vulnerabilities, it can happen. In any event, we ask that you please double check your settings to make sure you've set up your dashboard correctly."
+msgstr ""
+
+msgid "SecurityReports|Won't fix / Accept risk"
+msgstr ""
+
+msgid "SecurityReports|You do not have sufficient permissions to access this report"
+msgstr ""
+
+msgid "SecurityReports|You must sign in as an authorized user to see this report"
+msgstr ""
+
+msgid "SecurityReports|[No reason]"
msgstr ""
msgid "See GitLab's %{password_policy_guidelines}"
@@ -18065,6 +18489,9 @@ msgstr ""
msgid "See the affected projects in the GitLab admin panel"
msgstr ""
+msgid "See what's new at GitLab"
+msgstr ""
+
msgid "Select"
msgstr ""
@@ -18470,9 +18897,6 @@ msgstr ""
msgid "Set up Jira Integration"
msgstr ""
-msgid "Set up Jira Integration illustration"
-msgstr ""
-
msgid "Set up a %{type} Runner automatically"
msgstr ""
@@ -18557,6 +18981,9 @@ msgstr ""
msgid "Severity: %{severity}"
msgstr ""
+msgid "Shards (%{shards})"
+msgstr ""
+
msgid "Shards to synchronize"
msgstr ""
@@ -18871,6 +19298,9 @@ msgstr ""
msgid "Snowplow"
msgstr ""
+msgid "Some child epics may be hidden due to applied filters"
+msgstr ""
+
msgid "Some email servers do not support overriding the email sender name. Enable this option to include the name of the author of the issue, merge request or comment in the email body instead."
msgstr ""
@@ -19012,6 +19442,9 @@ msgstr ""
msgid "Something went wrong while updating your list settings"
msgstr ""
+msgid "Something went wrong with your automatic subscription renewal"
+msgstr ""
+
msgid "Something went wrong, unable to add %{project} to dashboard"
msgstr ""
@@ -19297,6 +19730,9 @@ msgstr ""
msgid "Stage removed"
msgstr ""
+msgid "Standard"
+msgstr ""
+
msgid "Star a label to make it a priority label. Order the prioritized labels to change their relative priority, by dragging."
msgstr ""
@@ -19420,6 +19856,9 @@ msgstr ""
msgid "Static Application Security Testing (SAST)"
msgstr ""
+msgid "StaticSiteEditor|An error occurred while submitting your changes."
+msgstr ""
+
msgid "StaticSiteEditor|Branch could not be created."
msgstr ""
@@ -19429,18 +19868,30 @@ msgstr ""
msgid "StaticSiteEditor|Could not create merge request."
msgstr ""
+msgid "StaticSiteEditor|Incompatible file content"
+msgstr ""
+
msgid "StaticSiteEditor|Return to site"
msgstr ""
+msgid "StaticSiteEditor|Static site editor"
+msgstr ""
+
msgid "StaticSiteEditor|Success!"
msgstr ""
msgid "StaticSiteEditor|Summary of changes"
msgstr ""
+msgid "StaticSiteEditor|The Static Site Editor is currently configured to only edit Markdown content on pages generated from Middleman. Visit the documentation to learn more about configuring your site to use the Static Site Editor."
+msgstr ""
+
msgid "StaticSiteEditor|Update %{sourcePath} file"
msgstr ""
+msgid "StaticSiteEditor|View documentation"
+msgstr ""
+
msgid "StaticSiteEditor|View merge request"
msgstr ""
@@ -19498,6 +19949,12 @@ msgstr ""
msgid "StatusPage|Status page"
msgstr ""
+msgid "StatusPage|Status page URL"
+msgstr ""
+
+msgid "StatusPage|Status page frontend documentation"
+msgstr ""
+
msgid "StatusPage|To publish incidents to an external status page, GitLab will store a JSON file in your Amazon S3 account in a location accessible to your external status page service. Make sure to also set up %{docsLink}"
msgstr ""
@@ -19846,6 +20303,12 @@ msgstr ""
msgid "Synced"
msgstr ""
+msgid "Synchronization disabled"
+msgstr ""
+
+msgid "Synchronization of container repositories is disabled."
+msgstr ""
+
msgid "System"
msgstr ""
@@ -19999,6 +20462,9 @@ msgstr ""
msgid "Telephone number"
msgstr ""
+msgid "Telephone number (Optional)"
+msgstr ""
+
msgid "Template"
msgstr ""
@@ -20305,6 +20771,9 @@ msgstr ""
msgid "The license was successfully uploaded and is now active. You can see the details below."
msgstr ""
+msgid "The license was successfully uploaded and will be active from %{starts_at}. You can see the details below."
+msgstr ""
+
msgid "The maximum file size allowed is %{size}."
msgstr ""
@@ -20626,6 +21095,9 @@ msgstr ""
msgid "There was an error getting the epic participants."
msgstr ""
+msgid "There was an error importing the Jira project."
+msgstr ""
+
msgid "There was an error loading users activity calendar."
msgstr ""
@@ -20641,7 +21113,7 @@ msgstr ""
msgid "There was an error resetting user pipeline minutes."
msgstr ""
-msgid "There was an error saving this Geo Node"
+msgid "There was an error saving this Geo Node."
msgstr ""
msgid "There was an error saving your changes."
@@ -20704,6 +21176,9 @@ msgstr ""
msgid "They can be managed using the %{link}."
msgstr ""
+msgid "Third Party Advisory Link"
+msgstr ""
+
msgid "Third party offers"
msgstr ""
@@ -20740,6 +21215,9 @@ msgstr ""
msgid "This application will be able to:"
msgstr ""
+msgid "This attachment has been truncated to avoid exceeding the maximum allowed attachment size of 15MB. %{written_count} of %{issues_count} issues have been included. Consider re-exporting with a narrower selection of issues."
+msgstr ""
+
msgid "This block is self-referential"
msgstr ""
@@ -20881,6 +21359,12 @@ msgstr ""
msgid "This issue is confidential"
msgstr ""
+msgid "This issue is currently blocked by the following issues: %{issues}."
+msgstr ""
+
+msgid "This issue is in a child epic of the filtered epic"
+msgstr ""
+
msgid "This issue is locked."
msgstr ""
@@ -21079,7 +21563,10 @@ msgstr ""
msgid "This user will be the author of all events in the activity feed that are the result of an update, like new branches being created or new commits being pushed to existing branches. Upon creation or when reassigning you can only assign yourself to be the mirror user."
msgstr ""
-msgid "This variable can not be masked"
+msgid "This variable can not be masked."
+msgstr ""
+
+msgid "This variable does not match the expected pattern."
msgstr ""
msgid "This will help us personalize your onboarding experience."
@@ -21109,10 +21596,10 @@ msgstr ""
msgid "ThreatMonitoring|Application firewall not detected"
msgstr ""
-msgid "ThreatMonitoring|Container Network Policy"
+msgid "ThreatMonitoring|Container Network Policies are not installed or have been disabled. To view this data, ensure your Network Policies are installed and enabled for your cluster."
msgstr ""
-msgid "ThreatMonitoring|Container NetworkPolicies are not installed or has been disabled. To view this data, ensure you NetworkPolicies are installed and enabled for your cluster."
+msgid "ThreatMonitoring|Container Network Policy"
msgstr ""
msgid "ThreatMonitoring|Container NetworkPolicies not detected"
@@ -21145,7 +21632,7 @@ msgstr ""
msgid "ThreatMonitoring|Something went wrong, unable to fetch statistics"
msgstr ""
-msgid "ThreatMonitoring|The firewall is not installed or has been disabled. To view this data, ensure you firewall is installed and enabled for your cluster."
+msgid "ThreatMonitoring|The firewall is not installed or has been disabled. To view this data, ensure the web application firewall is installed and enabled for your cluster."
msgstr ""
msgid "ThreatMonitoring|The graph below is an overview of traffic coming to your application as tracked by the Web Application Firewall (WAF). View the docs for instructions on how to access the WAF logs to see what type of malicious traffic is trying to access your app. The docs link is also accessible by clicking the \"?\" icon next to the title below."
@@ -21214,6 +21701,9 @@ msgstr ""
msgid "Time in seconds GitLab will wait for a response from the external service. When the service does not respond in time, access will be denied."
msgstr ""
+msgid "Time of import: %{importTime}"
+msgstr ""
+
msgid "Time remaining"
msgstr ""
@@ -21398,7 +21888,7 @@ msgstr ""
msgid "Title:"
msgstr ""
-msgid "Titles and Filenames"
+msgid "Titles and Descriptions"
msgstr ""
msgid "To"
@@ -21605,7 +22095,7 @@ msgstr ""
msgid "Too many projects enabled. You will need to manage them via the console or the API."
msgstr ""
-msgid "Topics"
+msgid "Topics (optional)"
msgstr ""
msgid "Total"
@@ -21767,6 +22257,9 @@ msgstr ""
msgid "Try all GitLab has to offer for 30 days."
msgstr ""
+msgid "Try changing or removing filters."
+msgstr ""
+
msgid "Try to fork again"
msgstr ""
@@ -21860,6 +22353,9 @@ msgstr ""
msgid "Unable to connect to server: %{error}"
msgstr ""
+msgid "Unable to connect to the Jira instance. Please check your Jira integration configuration."
+msgstr ""
+
msgid "Unable to convert Kubernetes logs encoding to UTF-8"
msgstr ""
@@ -22803,6 +23299,9 @@ msgstr ""
msgid "View issue"
msgstr ""
+msgid "View issues"
+msgstr ""
+
msgid "View it on GitLab"
msgstr ""
@@ -22980,6 +23479,15 @@ msgstr ""
msgid "VulnerabilityManagement|Resolved %{timeago} by %{user}"
msgstr ""
+msgid "VulnerabilityManagement|Something went wrong while trying to delete the comment. Please try again later."
+msgstr ""
+
+msgid "VulnerabilityManagement|Something went wrong while trying to retrieve the vulnerability history. Please try again later."
+msgstr ""
+
+msgid "VulnerabilityManagement|Something went wrong while trying to save the comment. Please try again later."
+msgstr ""
+
msgid "VulnerabilityManagement|Something went wrong, could not create an issue."
msgstr ""
@@ -23010,6 +23518,9 @@ msgstr ""
msgid "VulnerabilityStatusTypes|Resolved"
msgstr ""
+msgid "Vulnerability|%{scannerName} (version %{scannerVersion})"
+msgstr ""
+
msgid "Vulnerability|Class"
msgstr ""
@@ -23043,7 +23554,10 @@ msgstr ""
msgid "Vulnerability|Project"
msgstr ""
-msgid "Vulnerability|Report Type"
+msgid "Vulnerability|Scanner Provider"
+msgstr ""
+
+msgid "Vulnerability|Scanner Type"
msgstr ""
msgid "Vulnerability|Severity"
@@ -23097,9 +23611,15 @@ msgstr ""
msgid "We sent you an email with reset password instructions"
msgstr ""
+msgid "We tried to automatically renew your %{strong}%{plan_name}%{strong_close} subscription for %{strong}%{namespace_name}%{strong_close} on %{expires_on} but something went wrong so your subscription was downgraded to the free plan. Don't worry, your data is safe. We suggest you check your payment method and get in touch with our support team (%{support_link}). They'll gladly help with your subscription renewal."
+msgstr ""
+
msgid "We want to be sure it is you, please confirm you are not a robot."
msgstr ""
+msgid "We will automatically renew your %{strong}%{plan_name}%{strong_close} subscription for %{strong}%{namespace_name}%{strong_close} on %{strong}%{expires_on}%{strong_close}. There's nothing that you need to do, we'll let you know when the renewal is complete. Need more seats, a higher plan or just want to review your payment method?"
+msgstr ""
+
msgid "We've found no vulnerabilities"
msgstr ""
@@ -23204,15 +23724,6 @@ msgstr ""
msgid "When:"
msgstr ""
-msgid "While it's rare to have no vulnerabilities for your group, it can happen. In any event, we ask that you double check your settings to make sure you've set up your dashboard correctly."
-msgstr ""
-
-msgid "While it's rare to have no vulnerabilities for your pipeline, it can happen. In any event, we ask that you double check your settings to make sure all security scanning jobs have passed successfully."
-msgstr ""
-
-msgid "While it's rare to have no vulnerabilities for your project, it can happen. In any event, we ask that you double check your settings to make sure you've set up your dashboard correctly."
-msgstr ""
-
msgid "While it's rare to have no vulnerabilities, it can happen. In any event, we ask that you please double check your settings to make sure you've set up your dashboard correctly."
msgstr ""
@@ -23243,9 +23754,6 @@ msgstr ""
msgid "Wiki"
msgstr ""
-msgid "Wiki pages"
-msgstr ""
-
msgid "Wiki was successfully updated."
msgstr ""
@@ -23429,6 +23937,9 @@ msgstr ""
msgid "Yes, add it"
msgstr ""
+msgid "Yes, close issue"
+msgstr ""
+
msgid "Yes, let me map Google Code users to full names or GitLab users."
msgstr ""
@@ -23624,6 +24135,9 @@ msgstr ""
msgid "You could not create a new trigger."
msgstr ""
+msgid "You didn't renew your %{strong}%{plan_name}%{strong_close} subscription for %{strong}%{namespace_name}%{strong_close} so it was downgraded to the free plan."
+msgstr ""
+
msgid "You didn't renew your %{strong}%{plan_name}%{strong_close} subscription so it was downgraded to the GitLab Core Plan."
msgstr ""
@@ -23690,6 +24204,9 @@ msgstr ""
msgid "You have declined the invitation to join %{label}."
msgstr ""
+msgid "You have imported from this project %{numberOfPreviousImportsForProject} times before. Each new import will create duplicate issues."
+msgstr ""
+
msgid "You have no permissions"
msgstr ""
@@ -23771,6 +24288,9 @@ msgstr ""
msgid "You need to upload a Google Takeout archive."
msgstr ""
+msgid "You reached %{usage_in_percent} of %{namespace_name}'s capacity (%{used_storage} of %{storage_limit})"
+msgstr ""
+
msgid "You tried to fork %{link_to_the_project} but it failed for the following reason:"
msgstr ""
@@ -23855,9 +24375,21 @@ msgstr ""
msgid "YouTube"
msgstr ""
+msgid "Your %{strong}%{plan_name}%{strong_close} subscription for %{strong}%{namespace_name}%{strong_close} will expire on %{strong}%{expires_on}%{strong_close}. After that, you will not to be able to create issues or merge requests as well as many other features."
+msgstr ""
+
msgid "Your %{strong}%{plan_name}%{strong_close} subscription will expire on %{strong}%{expires_on}%{strong_close}. After that, you will not to be able to create issues or merge requests as well as many other features."
msgstr ""
+msgid "Your CSV export has started. It will be emailed to %{email} when complete."
+msgstr ""
+
+msgid "Your CSV export of %{issues_count} from project %{project_link} has been added to this email as an attachment."
+msgstr ""
+
+msgid "Your CSV export of %{written_count} from project %{project_name} (%{project_url}) has been added to this email as an attachment."
+msgstr ""
+
msgid "Your Commit Email will be used for web based operations, such as edits and merges."
msgstr ""
@@ -23954,6 +24486,9 @@ msgstr ""
msgid "Your comment could not be updated! Please check your network connection and try again."
msgstr ""
+msgid "Your comment will be discarded."
+msgstr ""
+
msgid "Your custom stage '%{title}' was created"
msgstr ""
@@ -23993,6 +24528,9 @@ msgstr ""
msgid "Your new personal access token has been created."
msgstr ""
+msgid "Your new project access token has been created."
+msgstr ""
+
msgid "Your password isn't required to view this page. If a password or any other personal details are requested, please contact your administrator to report abuse."
msgstr ""
@@ -24011,12 +24549,18 @@ msgstr ""
msgid "Your request for access has been queued for review."
msgstr ""
+msgid "Your search didn't match any commits."
+msgstr ""
+
msgid "Your subscription expired!"
msgstr ""
msgid "Your subscription has been downgraded"
msgstr ""
+msgid "Your subscription will automatically renew in %{remaining_days}"
+msgstr ""
+
msgid "Your subscription will expire in %{remaining_days}"
msgstr ""
@@ -24055,7 +24599,7 @@ msgstr ""
msgid "allowed to fail"
msgstr ""
-msgid "already being used for another group or project milestone."
+msgid "already being used for another group or project %{timebox_name}."
msgstr ""
msgid "already has a \"created\" issue link"
@@ -24133,6 +24677,9 @@ msgstr ""
msgid "ciReport|%{linkStartTag}Learn more about SAST %{linkEndTag}"
msgstr ""
+msgid "ciReport|%{linkStartTag}Learn more about Secret Scanning %{linkEndTag}"
+msgstr ""
+
msgid "ciReport|%{linkStartTag}Learn more about codequality reports %{linkEndTag}"
msgstr ""
@@ -24296,6 +24843,12 @@ msgstr ""
msgid "ciReport|SAST"
msgstr ""
+msgid "ciReport|Secret scanning"
+msgstr ""
+
+msgid "ciReport|Secret scanning detects secrets and credentials vulnerabilities in your source code."
+msgstr ""
+
msgid "ciReport|Security scanning"
msgstr ""
@@ -24493,6 +25046,9 @@ msgstr ""
msgid "group"
msgstr ""
+msgid "groups"
+msgstr ""
+
msgid "has already been linked to another vulnerability"
msgstr ""
@@ -24656,9 +25212,6 @@ msgstr[1] ""
msgid "merged %{time_ago}"
msgstr ""
-msgid "milestone should belong either to a project or a group."
-msgstr ""
-
msgid "missing"
msgstr ""
@@ -24995,6 +25548,9 @@ msgstr ""
msgid "no contributions"
msgstr ""
+msgid "no expiration"
+msgstr ""
+
msgid "no one can merge"
msgstr ""
@@ -25047,6 +25603,9 @@ msgstr ""
msgid "pending removal"
msgstr ""
+msgid "per day"
+msgstr ""
+
msgid "pipeline"
msgstr ""
@@ -25075,6 +25634,12 @@ msgstr[1] ""
msgid "project avatar"
msgstr ""
+msgid "projects"
+msgstr ""
+
+msgid "push to your repository, create pipelines, create issues or add comments. To reduce storage capacity, delete unused repositories, artifacts, wikis, issues, and pipelines."
+msgstr ""
+
msgid "quick actions"
msgstr ""
@@ -25221,11 +25786,6 @@ msgstr ""
msgid "this document"
msgstr ""
-msgid "thread resolved"
-msgid_plural "threads resolved"
-msgstr[0] ""
-msgstr[1] ""
-
msgid "to help your contributors communicate effectively!"
msgstr ""
@@ -25280,6 +25840,9 @@ msgstr ""
msgid "view the blob"
msgstr ""
+msgid "vulnerability|Add a comment"
+msgstr ""
+
msgid "vulnerability|Add a comment or reason for dismissal"
msgstr ""
diff --git a/locale/fil_PH/gitlab.po b/locale/fil_PH/gitlab.po
index f27cff91bb7..07f7779ecec 100644
--- a/locale/fil_PH/gitlab.po
+++ b/locale/fil_PH/gitlab.po
@@ -12,7 +12,7 @@ msgstr ""
"X-Crowdin-Project: gitlab-ee\n"
"X-Crowdin-Language: fil\n"
"X-Crowdin-File: /master/locale/gitlab.pot\n"
-"PO-Revision-Date: 2020-04-15 00:31\n"
+"PO-Revision-Date: 2020-05-05 21:33\n"
msgid " %{start} to %{end}"
msgstr ""
@@ -212,6 +212,11 @@ msgid_plural "%d tags"
msgstr[0] ""
msgstr[1] ""
+msgid "%d unresolved thread"
+msgid_plural "%d unresolved threads"
+msgstr[0] ""
+msgstr[1] ""
+
msgid "%d vulnerability dismissed"
msgid_plural "%d vulnerabilities dismissed"
msgstr[0] ""
@@ -371,6 +376,9 @@ msgstr ""
msgid "%{mrText}, this issue will be closed automatically."
msgstr ""
+msgid "%{namespace_name} is now read-only. You cannot: %{base_message}"
+msgstr ""
+
msgid "%{name} contained %{resultsString}"
msgstr ""
@@ -383,6 +391,11 @@ msgstr ""
msgid "%{name}'s avatar"
msgstr ""
+msgid "%{no_of_days} day"
+msgid_plural "%{no_of_days} days"
+msgstr[0] ""
+msgstr[1] ""
+
msgid "%{number_commits_behind} commits behind %{default_branch}, %{number_commits_ahead} commits ahead"
msgstr ""
@@ -506,6 +519,9 @@ msgstr[1] ""
msgid "%{text} is available"
msgstr ""
+msgid "%{timebox_name} should belong either to a project or a group."
+msgstr ""
+
msgid "%{title} %{operator} %{threshold}"
msgstr ""
@@ -781,12 +797,18 @@ msgstr ""
msgid "<code>\"johnsmith@example.com\": \"johnsmith@example.com\"</code> will add \"By <a href=\"#\">johnsmith@example.com</a>\" to all issues and comments originally created by johnsmith@example.com. By default, the email address or username is masked to ensure the user's privacy. Use this option if you want to show the full email address."
msgstr ""
+msgid "<namespace / project>"
+msgstr ""
+
msgid "<no name set>"
msgstr ""
msgid "<no scopes selected>"
msgstr ""
+msgid "<project name>"
+msgstr ""
+
msgid "<strong>%{group_name}</strong> group members"
msgstr ""
@@ -820,6 +842,9 @@ msgstr ""
msgid "A basic page and serverless function that uses AWS Lambda, AWS API Gateway, and GitLab Pages"
msgstr ""
+msgid "A complete DevOps platform"
+msgstr ""
+
msgid "A default branch cannot be chosen for an empty project."
msgstr ""
@@ -1036,6 +1061,12 @@ msgstr ""
msgid "AccessTokens|reset it"
msgstr ""
+msgid "AccessibilityReport|Accessibility report artifact not found"
+msgstr ""
+
+msgid "AccessibilityReport|Failed to retrieve accessibility report"
+msgstr ""
+
msgid "AccessibilityReport|Learn More"
msgstr ""
@@ -1045,6 +1076,9 @@ msgstr ""
msgid "AccessibilityReport|New"
msgstr ""
+msgid "AccessibilityReport|The accessibility scanning found an error of the following type: %{code}"
+msgstr ""
+
msgid "Account"
msgstr ""
@@ -1287,6 +1321,9 @@ msgstr ""
msgid "Added at"
msgstr ""
+msgid "Added for this merge request"
+msgstr ""
+
msgid "Added in this version"
msgstr ""
@@ -1658,6 +1695,72 @@ msgid_plural "Alerts"
msgstr[0] ""
msgstr[1] ""
+msgid "Alert Details"
+msgstr ""
+
+msgid "AlertManagement|Acknowledged"
+msgstr ""
+
+msgid "AlertManagement|Alert"
+msgstr ""
+
+msgid "AlertManagement|Authorize external service"
+msgstr ""
+
+msgid "AlertManagement|Display alerts from all your monitoring tools directly within GitLab. Streamline the investigation of your alerts and the escalation of alerts to incidents."
+msgstr ""
+
+msgid "AlertManagement|End time"
+msgstr ""
+
+msgid "AlertManagement|End time:"
+msgstr ""
+
+msgid "AlertManagement|Events"
+msgstr ""
+
+msgid "AlertManagement|Events:"
+msgstr ""
+
+msgid "AlertManagement|Full Alert Details"
+msgstr ""
+
+msgid "AlertManagement|More information"
+msgstr ""
+
+msgid "AlertManagement|No alerts available to display. If you think you're seeing this message in error, refresh the page."
+msgstr ""
+
+msgid "AlertManagement|No alerts to display."
+msgstr ""
+
+msgid "AlertManagement|Overview"
+msgstr ""
+
+msgid "AlertManagement|Resolved"
+msgstr ""
+
+msgid "AlertManagement|Severity"
+msgstr ""
+
+msgid "AlertManagement|Start time"
+msgstr ""
+
+msgid "AlertManagement|Start time:"
+msgstr ""
+
+msgid "AlertManagement|Status"
+msgstr ""
+
+msgid "AlertManagement|Surface alerts in GitLab"
+msgstr ""
+
+msgid "AlertManagement|There was an error displaying the alerts. Confirm your endpoint's configuration details to ensure alerts appear."
+msgstr ""
+
+msgid "AlertManagement|Triggered"
+msgstr ""
+
msgid "AlertService|%{linkStart}Learn more%{linkEnd} about configuring this endpoint to receive alerts."
msgstr ""
@@ -1727,6 +1830,9 @@ msgstr ""
msgid "All security scans are enabled because %{linkStart}Auto DevOps%{linkEnd} is enabled on this project"
msgstr ""
+msgid "All threads resolved"
+msgstr ""
+
msgid "All users"
msgstr ""
@@ -1745,6 +1851,9 @@ msgstr ""
msgid "Allow only the selected protocols to be used for Git access."
msgstr ""
+msgid "Allow owners to manage default branch protection per group"
+msgstr ""
+
msgid "Allow owners to manually add users outside of LDAP"
msgstr ""
@@ -1853,6 +1962,9 @@ msgstr ""
msgid "An error occurred fetching the dropdown data."
msgstr ""
+msgid "An error occurred fetching the project authors."
+msgstr ""
+
msgid "An error occurred previewing the blob"
msgstr ""
@@ -1934,6 +2046,9 @@ msgstr ""
msgid "An error occurred while fetching sidebar data"
msgstr ""
+msgid "An error occurred while fetching terraform reports."
+msgstr ""
+
msgid "An error occurred while fetching the Service Desk address."
msgstr ""
@@ -2171,6 +2286,9 @@ msgstr ""
msgid "Any"
msgstr ""
+msgid "Any Author"
+msgstr ""
+
msgid "Any Label"
msgstr ""
@@ -2386,10 +2504,10 @@ msgstr ""
msgid "Are you sure that you want to unarchive this project?"
msgstr ""
-msgid "Are you sure you want to cancel creating this comment?"
+msgid "Are you sure you want to cancel editing this comment?"
msgstr ""
-msgid "Are you sure you want to cancel editing this comment?"
+msgid "Are you sure you want to close this blocked issue?"
msgstr ""
msgid "Are you sure you want to delete %{name}?"
@@ -2419,6 +2537,9 @@ msgstr ""
msgid "Are you sure you want to deploy this environment?"
msgstr ""
+msgid "Are you sure you want to discard this comment?"
+msgstr ""
+
msgid "Are you sure you want to erase this build?"
msgstr ""
@@ -2595,6 +2716,9 @@ msgstr ""
msgid "At least one approval from a code owner is required to change files matching the respective CODEOWNER rules."
msgstr ""
+msgid "At least one logging option is required to be enabled"
+msgstr ""
+
msgid "At least one of group_id or project_id must be specified"
msgstr ""
@@ -2633,6 +2757,27 @@ msgstr ""
msgid "AuditEvents|Target"
msgstr ""
+msgid "AuditLogs|(removed)"
+msgstr ""
+
+msgid "AuditLogs|Action"
+msgstr ""
+
+msgid "AuditLogs|Author"
+msgstr ""
+
+msgid "AuditLogs|Date"
+msgstr ""
+
+msgid "AuditLogs|IP Address"
+msgstr ""
+
+msgid "AuditLogs|Object"
+msgstr ""
+
+msgid "AuditLogs|Target"
+msgstr ""
+
msgid "Aug"
msgstr ""
@@ -2915,9 +3060,6 @@ msgstr ""
msgid "BambooService|You must set up automatic revision labeling and a repository trigger in Bamboo."
msgstr ""
-msgid "Batch operations"
-msgstr ""
-
msgid "BatchComments|Delete all pending comments"
msgstr ""
@@ -3047,6 +3189,9 @@ msgstr ""
msgid "Boards"
msgstr ""
+msgid "Boards and Board Lists"
+msgstr ""
+
msgid "Boards|Collapse"
msgstr ""
@@ -3266,6 +3411,9 @@ msgstr ""
msgid "Buy GitLab Enterprise Edition"
msgstr ""
+msgid "Buy more Pipeline minutes"
+msgstr ""
+
msgid "By %{user_name}"
msgstr ""
@@ -3386,6 +3534,9 @@ msgstr ""
msgid "Can't scan the code?"
msgstr ""
+msgid "Can't update snippet: %{err}"
+msgstr ""
+
msgid "Canary"
msgstr ""
@@ -3554,6 +3705,9 @@ msgstr ""
msgid "Changing group path can have unintended side effects."
msgstr ""
+msgid "Charts"
+msgstr ""
+
msgid "Charts can't be displayed as the request for data has timed out. %{documentationLink}"
msgstr ""
@@ -3590,9 +3744,6 @@ msgstr ""
msgid "ChatMessage|and [%{count} more](%{pipeline_failed_jobs_url})"
msgstr ""
-msgid "ChatMessage|failed"
-msgstr ""
-
msgid "ChatMessage|has failed"
msgstr ""
@@ -3608,9 +3759,6 @@ msgstr ""
msgid "ChatMessage|in %{project_link}"
msgstr ""
-msgid "ChatMessage|passed"
-msgstr ""
-
msgid "Check again"
msgstr ""
@@ -4061,9 +4209,6 @@ msgstr ""
msgid "Click any <strong>project name</strong> in the project list below to navigate to the project milestone."
msgstr ""
-msgid "Click here"
-msgstr ""
-
msgid "Click the <strong>Download</strong> button and wait for downloading to complete."
msgstr ""
@@ -4451,6 +4596,12 @@ msgstr ""
msgid "ClusterIntegration|Fetching zones"
msgstr ""
+msgid "ClusterIntegration|Fluentd"
+msgstr ""
+
+msgid "ClusterIntegration|Fluentd is an open source data collector, which lets you unify the data collection and consumption for a better use and understanding of data. It requires at least one of the following logs to be successfully installed."
+msgstr ""
+
msgid "ClusterIntegration|GitLab Integration"
msgstr ""
@@ -4757,6 +4908,15 @@ msgstr ""
msgid "ClusterIntegration|Request to begin uninstalling failed"
msgstr ""
+msgid "ClusterIntegration|SIEM Hostname"
+msgstr ""
+
+msgid "ClusterIntegration|SIEM Port"
+msgstr ""
+
+msgid "ClusterIntegration|SIEM Protocol"
+msgstr ""
+
msgid "ClusterIntegration|Save changes"
msgstr ""
@@ -4853,6 +5013,12 @@ msgstr ""
msgid "ClusterIntegration|Select zone to choose machine type"
msgstr ""
+msgid "ClusterIntegration|Send Cilium Logs"
+msgstr ""
+
+msgid "ClusterIntegration|Send ModSecurity Logs"
+msgstr ""
+
msgid "ClusterIntegration|Service Token"
msgstr ""
@@ -5281,18 +5447,33 @@ msgstr ""
msgid "Compliance framework (optional)"
msgstr ""
+msgid "ComplianceFramework|GDPR"
+msgstr ""
+
msgid "ComplianceFramework|GDPR - General Data Protection Regulation"
msgstr ""
+msgid "ComplianceFramework|HIPAA"
+msgstr ""
+
msgid "ComplianceFramework|HIPAA - Health Insurance Portability and Accountability Act"
msgstr ""
+msgid "ComplianceFramework|PCI-DSS"
+msgstr ""
+
msgid "ComplianceFramework|PCI-DSS - Payment Card Industry-Data Security Standard"
msgstr ""
+msgid "ComplianceFramework|SOC 2"
+msgstr ""
+
msgid "ComplianceFramework|SOC 2 - Service Organization Control 2"
msgstr ""
+msgid "ComplianceFramework|SOX"
+msgstr ""
+
msgid "ComplianceFramework|SOX - Sarbanes-Oxley"
msgstr ""
@@ -5440,9 +5621,15 @@ msgstr ""
msgid "Container repositories sync capacity"
msgstr ""
+msgid "ContainerRegistry| Please visit the %{linkStart}administration settings%{linkEnd} to enable this feature."
+msgstr ""
+
msgid "ContainerRegistry|%{imageName} tags"
msgstr ""
+msgid "ContainerRegistry|%{title} was successfully scheduled for deletion"
+msgstr ""
+
msgid "ContainerRegistry|Automatically remove extra images that aren't designed to be kept."
msgstr ""
@@ -5455,6 +5642,9 @@ msgstr ""
msgid "ContainerRegistry|Container Registry"
msgstr ""
+msgid "ContainerRegistry|Container Registry tag expiration and retention policy is disabled"
+msgstr ""
+
msgid "ContainerRegistry|Copy build command"
msgstr ""
@@ -5464,18 +5654,12 @@ msgstr ""
msgid "ContainerRegistry|Copy push command"
msgstr ""
-msgid "ContainerRegistry|Currently, the Container Registry tag expiration feature is not available for projects created before GitLab version 12.8. For updates and more information, visit Issue %{linkStart}#196124%{linkEnd}"
-msgstr ""
-
msgid "ContainerRegistry|Docker connection error"
msgstr ""
msgid "ContainerRegistry|Docker tag expiration policy is %{toggleStatus}"
msgstr ""
-msgid "ContainerRegistry|Docker tags with names matching this regex pattern will expire:"
-msgstr ""
-
msgid "ContainerRegistry|Edit Settings"
msgstr ""
@@ -5497,9 +5681,6 @@ msgstr ""
msgid "ContainerRegistry|Image ID"
msgstr ""
-msgid "ContainerRegistry|Image deleted successfully"
-msgstr ""
-
msgid "ContainerRegistry|Keep and protect the images that matter most."
msgstr ""
@@ -5515,12 +5696,24 @@ msgstr ""
msgid "ContainerRegistry|Number of tags to retain:"
msgstr ""
+msgid "ContainerRegistry|Please contact your administrator."
+msgstr ""
+
msgid "ContainerRegistry|Push an image"
msgstr ""
msgid "ContainerRegistry|Quick Start"
msgstr ""
+msgid "ContainerRegistry|Regular expressions such as %{codeStart}.*-test%{codeEnd} or %{codeStart}dev-.*%{codeEnd} are supported"
+msgstr ""
+
+msgid "ContainerRegistry|Regular expressions such as %{codeStart}.*-test%{codeEnd} or %{codeStart}dev-.*%{codeEnd} are supported. To select all tags, use %{codeStart}.*%{codeEnd}"
+msgstr ""
+
+msgid "ContainerRegistry|Remember to run %{docLinkStart}garbage collection%{docLinkEnd} to remove the stale data from storage."
+msgstr ""
+
msgid "ContainerRegistry|Remove repository"
msgstr ""
@@ -5535,22 +5728,22 @@ msgstr[1] ""
msgid "ContainerRegistry|Retention policy has been Enabled"
msgstr ""
-msgid "ContainerRegistry|Something went wrong while deleting the image."
+msgid "ContainerRegistry|Something went wrong while fetching the expiration policy."
msgstr ""
-msgid "ContainerRegistry|Something went wrong while deleting the tag."
+msgid "ContainerRegistry|Something went wrong while fetching the repository list."
msgstr ""
-msgid "ContainerRegistry|Something went wrong while deleting the tags."
+msgid "ContainerRegistry|Something went wrong while fetching the tags list."
msgstr ""
-msgid "ContainerRegistry|Something went wrong while fetching the expiration policy."
+msgid "ContainerRegistry|Something went wrong while marking the tag for deletion."
msgstr ""
-msgid "ContainerRegistry|Something went wrong while fetching the packages list."
+msgid "ContainerRegistry|Something went wrong while marking the tags for deletion."
msgstr ""
-msgid "ContainerRegistry|Something went wrong while fetching the tags list."
+msgid "ContainerRegistry|Something went wrong while scheduling %{title} for deletion. Please try again."
msgstr ""
msgid "ContainerRegistry|Something went wrong while updating the expiration policy."
@@ -5559,16 +5752,25 @@ msgstr ""
msgid "ContainerRegistry|Tag"
msgstr ""
-msgid "ContainerRegistry|Tag deleted successfully"
-msgstr ""
-
msgid "ContainerRegistry|Tag expiration policy"
msgstr ""
msgid "ContainerRegistry|Tag expiration policy is designed to:"
msgstr ""
-msgid "ContainerRegistry|Tags deleted successfully"
+msgid "ContainerRegistry|Tag successfully marked for deletion."
+msgstr ""
+
+msgid "ContainerRegistry|Tags successfully marked for deletion."
+msgstr ""
+
+msgid "ContainerRegistry|Tags with names matching this regex pattern will %{italicStart}be preserved:%{italicEnd}"
+msgstr ""
+
+msgid "ContainerRegistry|Tags with names matching this regex pattern will %{italicStart}expire:%{italicEnd}"
+msgstr ""
+
+msgid "ContainerRegistry|The Container Registry tag expiration and retention policies for this project have not been enabled."
msgstr ""
msgid "ContainerRegistry|The last tag related to this image was recently removed. This empty image and any associated data will be automatically removed as part of the regular Garbage Collection process. If you have any questions, contact your administrator."
@@ -5589,16 +5791,16 @@ msgstr ""
msgid "ContainerRegistry|There are no container images stored for this project"
msgstr ""
-msgid "ContainerRegistry|This Registry contains deleted image tag data. Remember to run %{docLinkStart}garbage collection%{docLinkEnd} to remove the stale data from storage."
+msgid "ContainerRegistry|There was an error during the deletion of this image repository, please try again."
msgstr ""
msgid "ContainerRegistry|This image has no active tags"
msgstr ""
-msgid "ContainerRegistry|We are having trouble connecting to Docker, which could be due to an issue with your project name or path. %{docLinkStart}More Information%{docLinkEnd}"
+msgid "ContainerRegistry|This image repository is scheduled for deletion"
msgstr ""
-msgid "ContainerRegistry|Wildcards such as %{codeStart}.*-stable%{codeEnd} or %{codeStart}production/.*%{codeEnd} are supported. To select all tags, use %{codeStart}.*%{codeEnd}"
+msgid "ContainerRegistry|We are having trouble connecting to Docker, which could be due to an issue with your project name or path. %{docLinkStart}More Information%{docLinkEnd}"
msgstr ""
msgid "ContainerRegistry|With the Container Registry, every project can have its own space to store its Docker images. %{docLinkStart}More Information%{docLinkEnd}"
@@ -5853,7 +6055,7 @@ msgstr ""
msgid "Could not delete chat nickname %{chat_name}."
msgstr ""
-msgid "Could not find design"
+msgid "Could not find design."
msgstr ""
msgid "Could not remove the trigger."
@@ -5868,6 +6070,9 @@ msgstr ""
msgid "Could not revoke personal access token %{personal_access_token_name}."
msgstr ""
+msgid "Could not revoke project access token %{project_access_token_name}."
+msgstr ""
+
msgid "Could not save group ID"
msgstr ""
@@ -5925,6 +6130,9 @@ msgstr ""
msgid "Create a new branch"
msgstr ""
+msgid "Create a new deploy key for this project"
+msgstr ""
+
msgid "Create a new file as there are no files yet. Afterwards, you'll be able to commit your changes."
msgstr ""
@@ -6030,6 +6238,9 @@ msgstr ""
msgid "Create requirement"
msgstr ""
+msgid "Create snippet"
+msgstr ""
+
msgid "Create wildcard: %{searchTerm}"
msgstr ""
@@ -6051,6 +6262,9 @@ msgstr ""
msgid "Created"
msgstr ""
+msgid "Created %{timestamp}"
+msgstr ""
+
msgid "Created At"
msgstr ""
@@ -6407,6 +6621,9 @@ msgstr ""
msgid "CycleAnalytics|stage dropdown"
msgstr ""
+msgid "DAG"
+msgstr ""
+
msgid "DNS"
msgstr ""
@@ -6773,6 +6990,9 @@ msgstr ""
msgid "Deploy key was successfully updated."
msgstr ""
+msgid "Deploy keys allow read-only or read-write (if enabled) access to your repository. Deploy keys can be used for CI, staging or production servers. You can create a deploy key or add an existing one."
+msgstr ""
+
msgid "Deploy progress not found. To see pods, ensure your environment matches %{linkStart}deploy board criteria%{linkEnd}."
msgstr ""
@@ -6926,6 +7146,9 @@ msgstr ""
msgid "Deploying to"
msgstr ""
+msgid "Deployment Frequency"
+msgstr ""
+
msgid "Deployment|API"
msgstr ""
@@ -7444,9 +7667,6 @@ msgstr ""
msgid "Edit environment"
msgstr ""
-msgid "Edit epic description"
-msgstr ""
-
msgid "Edit file"
msgstr ""
@@ -8068,9 +8288,6 @@ msgstr ""
msgid "Epics"
msgstr ""
-msgid "Epics (Ultimate / Gold license only)"
-msgstr ""
-
msgid "Epics Roadmap"
msgstr ""
@@ -8080,6 +8297,9 @@ msgstr ""
msgid "Epics let you manage your portfolio of projects more efficiently and with less effort"
msgstr ""
+msgid "Epics, Issues, and Merge Requests"
+msgstr ""
+
msgid "Epics|Add an epic"
msgstr ""
@@ -8131,6 +8351,9 @@ msgstr ""
msgid "Epics|Something went wrong while fetching group epics."
msgstr ""
+msgid "Epics|Something went wrong while moving item."
+msgstr ""
+
msgid "Epics|Something went wrong while ordering item."
msgstr ""
@@ -8185,6 +8408,9 @@ msgstr ""
msgid "Error fetching network graph."
msgstr ""
+msgid "Error fetching payload data."
+msgstr ""
+
msgid "Error fetching projects"
msgstr ""
@@ -8194,9 +8420,6 @@ msgstr ""
msgid "Error fetching the dependency list. Please check your network connection and try again."
msgstr ""
-msgid "Error fetching usage ping data."
-msgstr ""
-
msgid "Error loading branch data. Please try again."
msgstr ""
@@ -8338,6 +8561,9 @@ msgstr ""
msgid "Errors"
msgstr ""
+msgid "Errors:"
+msgstr ""
+
msgid "Estimated"
msgstr ""
@@ -8542,12 +8768,18 @@ msgstr ""
msgid "Export as CSV"
msgstr ""
+msgid "Export group"
+msgstr ""
+
msgid "Export issues"
msgstr ""
msgid "Export project"
msgstr ""
+msgid "Export this group with all related data to a new GitLab instance. Once complete, you can import the data file from the \"New Group\" page."
+msgstr ""
+
msgid "Export this project with all its related data in order to move your project to a new GitLab instance. Once the export is finished, you can import the file from the \"New Project\" page."
msgstr ""
@@ -8617,9 +8849,6 @@ msgstr ""
msgid "Failed Jobs"
msgstr ""
-msgid "Failed create wiki"
-msgstr ""
-
msgid "Failed to add a Zoom meeting"
msgstr ""
@@ -8659,6 +8888,9 @@ msgstr ""
msgid "Failed to create resources"
msgstr ""
+msgid "Failed to create wiki"
+msgstr ""
+
msgid "Failed to delete board. Please try again."
msgstr ""
@@ -8668,7 +8900,7 @@ msgstr ""
msgid "Failed to enqueue the rebase operation, possibly due to a long-lived transaction. Try again later."
msgstr ""
-msgid "Failed to find import label for jira import."
+msgid "Failed to find import label for Jira import."
msgstr ""
msgid "Failed to get ref."
@@ -8692,6 +8924,12 @@ msgstr ""
msgid "Failed to load groups & users."
msgstr ""
+msgid "Failed to load labels. Please try again."
+msgstr ""
+
+msgid "Failed to load milestones. Please try again."
+msgstr ""
+
msgid "Failed to load related branches"
msgstr ""
@@ -9067,7 +9305,10 @@ msgstr ""
msgid "Filter by milestone name"
msgstr ""
-msgid "Filter by name..."
+msgid "Filter by name"
+msgstr ""
+
+msgid "Filter by status"
msgstr ""
msgid "Filter by two-factor authentication"
@@ -9079,12 +9320,18 @@ msgstr ""
msgid "Filter projects"
msgstr ""
+msgid "Filter results"
+msgstr ""
+
msgid "Filter results by group"
msgstr ""
msgid "Filter results by project"
msgstr ""
+msgid "Filter results..."
+msgstr ""
+
msgid "Filter your projects by name"
msgstr ""
@@ -9373,10 +9620,10 @@ msgstr ""
msgid "GeoNodes|Checksummed"
msgstr ""
-msgid "GeoNodes|Container repositories"
+msgid "GeoNodes|Consult Geo troubleshooting information"
msgstr ""
-msgid "GeoNodes|Data is out of date from %{timeago}"
+msgid "GeoNodes|Container repositories"
msgstr ""
msgid "GeoNodes|Data replication lag"
@@ -9418,6 +9665,9 @@ msgstr ""
msgid "GeoNodes|Last event ID seen from primary"
msgstr ""
+msgid "GeoNodes|Learn more about Geo node statuses"
+msgstr ""
+
msgid "GeoNodes|Loading nodes"
msgstr ""
@@ -9433,6 +9683,9 @@ msgstr ""
msgid "GeoNodes|Node was successfully removed."
msgstr ""
+msgid "GeoNodes|Node's status was updated %{timeAgo}."
+msgstr ""
+
msgid "GeoNodes|Not checksummed"
msgstr ""
@@ -9463,7 +9716,10 @@ msgstr ""
msgid "GeoNodes|Repository verification progress"
msgstr ""
-msgid "GeoNodes|Selective"
+msgid "GeoNodes|Selective (%{syncLabel})"
+msgstr ""
+
+msgid "GeoNodes|Selective synchronization"
msgstr ""
msgid "GeoNodes|Something went wrong while changing node status"
@@ -9490,6 +9746,9 @@ msgstr ""
msgid "GeoNodes|Unverified"
msgstr ""
+msgid "GeoNodes|Updated %{timeAgo}"
+msgstr ""
+
msgid "GeoNodes|Used slots"
msgstr ""
@@ -9529,16 +9788,16 @@ msgstr ""
msgid "Geo|All"
msgstr ""
-msgid "Geo|All projects"
+msgid "Geo|All %{replicable_type}"
msgstr ""
-msgid "Geo|All projects are being scheduled for re-sync"
+msgid "Geo|All projects"
msgstr ""
-msgid "Geo|All projects are being scheduled for re-verify"
+msgid "Geo|All projects are being scheduled for resync"
msgstr ""
-msgid "Geo|Batch operations"
+msgid "Geo|All projects are being scheduled for reverify"
msgstr ""
msgid "Geo|Could not remove tracking entry for an existing project."
@@ -9550,9 +9809,15 @@ msgstr ""
msgid "Geo|Failed"
msgstr ""
+msgid "Geo|Filter by status"
+msgstr ""
+
msgid "Geo|Geo Status"
msgstr ""
+msgid "Geo|In progress"
+msgstr ""
+
msgid "Geo|In sync"
msgstr ""
@@ -9577,9 +9842,6 @@ msgstr ""
msgid "Geo|Not synced yet"
msgstr ""
-msgid "Geo|Pending"
-msgstr ""
-
msgid "Geo|Pending synchronization"
msgstr ""
@@ -9616,7 +9878,7 @@ msgstr ""
msgid "Geo|Resync"
msgstr ""
-msgid "Geo|Resync all projects"
+msgid "Geo|Resync all"
msgstr ""
msgid "Geo|Retry count"
@@ -9625,7 +9887,7 @@ msgstr ""
msgid "Geo|Reverify"
msgstr ""
-msgid "Geo|Reverify all projects"
+msgid "Geo|Reverify all"
msgstr ""
msgid "Geo|Status"
@@ -9967,9 +10229,6 @@ msgstr ""
msgid "Go to file"
msgstr ""
-msgid "Go to file (MRs only)"
-msgstr ""
-
msgid "Go to file permalink (while viewing a file)"
msgstr ""
@@ -10414,6 +10673,9 @@ msgstr ""
msgid "GroupSettings|Disable group mentions"
msgstr ""
+msgid "GroupSettings|Export group"
+msgstr ""
+
msgid "GroupSettings|If the parent group's visibility is lower than the group current visibility, visibility levels for subgroups and projects will be changed to match the new parent group's visibility."
msgstr ""
@@ -10492,6 +10754,9 @@ msgstr ""
msgid "Groups (%{count})"
msgstr ""
+msgid "Groups (%{groups})"
+msgstr ""
+
msgid "Groups can also be nested by creating %{subgroup_docs_link_start}subgroups%{subgroup_docs_link_end}."
msgstr ""
@@ -10757,15 +11022,12 @@ msgstr ""
msgid "ID:"
msgstr ""
-msgid "IDE|Allow live previews of JavaScript projects in the Web IDE using CodeSandbox client side evaluation."
+msgid "IDE|Allow live previews of JavaScript projects in the Web IDE using CodeSandbox Live Preview."
msgstr ""
msgid "IDE|Back"
msgstr ""
-msgid "IDE|Client side evaluation"
-msgstr ""
-
msgid "IDE|Commit"
msgstr ""
@@ -10829,6 +11091,9 @@ msgstr ""
msgid "If any job surpasses this timeout threshold, it will be marked as failed. Human readable time input language is accepted like \"1 hour\". Values without specification represent seconds."
msgstr ""
+msgid "If blank, set allowable lifetime to %{instance_level_policy_in_words}, as defined by the instance admin. Once set, existing tokens for users in this group may be revoked."
+msgstr ""
+
msgid "If checked, group owners can manage LDAP group links and LDAP member overrides"
msgstr ""
@@ -10868,6 +11133,9 @@ msgstr ""
msgid "If you lose your recovery codes you can generate new ones, invalidating all previous codes."
msgstr ""
+msgid "If you reach 100%% storage capacity, you will not be able to: %{base_message}"
+msgstr ""
+
msgid "If your HTTP repository is not publicly accessible, add your credentials."
msgstr ""
@@ -10979,6 +11247,9 @@ msgstr ""
msgid "Import repository"
msgstr ""
+msgid "Import started by: %{importInitiator}"
+msgstr ""
+
msgid "Import tasks"
msgstr ""
@@ -11045,6 +11316,9 @@ msgstr ""
msgid "In order to tailor your experience with GitLab we<br>would like to know a bit more about you."
msgstr ""
+msgid "In progress"
+msgstr ""
+
msgid "In the next step, you'll be able to select the projects you want to import."
msgstr ""
@@ -11392,15 +11666,15 @@ msgstr ""
msgid "Issues"
msgstr ""
-msgid "Issues / Merge Requests"
-msgstr ""
-
msgid "Issues Analytics"
msgstr ""
msgid "Issues Rate Limits"
msgstr ""
+msgid "Issues and Merge Requests"
+msgstr ""
+
msgid "Issues can be bugs, tasks or ideas to be discussed. Also, issues are searchable and filterable."
msgstr ""
@@ -11422,10 +11696,10 @@ msgstr ""
msgid "IssuesAnalytics|Avg/Month:"
msgstr ""
-msgid "IssuesAnalytics|Issues created"
+msgid "IssuesAnalytics|Issues opened"
msgstr ""
-msgid "IssuesAnalytics|Issues created per month"
+msgid "IssuesAnalytics|Issues opened per month"
msgstr ""
msgid "IssuesAnalytics|Last 12 months"
@@ -11479,6 +11753,9 @@ msgstr ""
msgid "Jira integration not configured."
msgstr ""
+msgid "Jira project: %{importProject}"
+msgstr ""
+
msgid "JiraService|Events for %{noteable_model_name} are disabled."
msgstr ""
@@ -11868,6 +12145,9 @@ msgstr ""
msgid "Lead"
msgstr ""
+msgid "Lead Time"
+msgstr ""
+
msgid "Learn GitLab"
msgstr ""
@@ -11880,6 +12160,9 @@ msgstr ""
msgid "Learn how to %{no_packages_link_start}publish and share your packages%{no_packages_link_end} with GitLab."
msgstr ""
+msgid "Learn how to enable synchronization"
+msgstr ""
+
msgid "Learn more"
msgstr ""
@@ -12211,9 +12494,6 @@ msgstr ""
msgid "Live preview"
msgstr ""
-msgid "Load more vulnerabilities"
-msgstr ""
-
msgid "Loading"
msgstr ""
@@ -12364,9 +12644,6 @@ msgstr ""
msgid "Manage"
msgstr ""
-msgid "Manage Git repositories with fine-grained access controls that keep your code secure. Perform code reviews and enhance collaboration with merge requests. Each project can also have an issue tracker and a wiki."
-msgstr ""
-
msgid "Manage Web IDE features"
msgstr ""
@@ -12397,6 +12674,9 @@ msgstr ""
msgid "Manage your license"
msgstr ""
+msgid "Managed Account"
+msgstr ""
+
msgid "Manifest"
msgstr ""
@@ -12661,6 +12941,9 @@ msgstr ""
msgid "Merge Requests in Review"
msgstr ""
+msgid "Merge automatically (%{strategy})"
+msgstr ""
+
msgid "Merge commit message"
msgstr ""
@@ -12814,6 +13097,12 @@ msgstr ""
msgid "Merged branches are being deleted. This can take some time depending on the number of branches. Please refresh the page to see changes."
msgstr ""
+msgid "Merged this merge request."
+msgstr ""
+
+msgid "Merges this merge request immediately."
+msgstr ""
+
msgid "Merges this merge request when the pipeline succeeds."
msgstr ""
@@ -12921,12 +13210,15 @@ msgid_plural "Metrics|Edit metrics"
msgstr[0] ""
msgstr[1] ""
-msgid "Metrics|Environment"
+msgid "Metrics|Expand panel"
msgstr ""
msgid "Metrics|For grouping similar metrics"
msgstr ""
+msgid "Metrics|Go back (Esc)"
+msgstr ""
+
msgid "Metrics|Invalid time range, please verify."
msgstr ""
@@ -12957,9 +13249,6 @@ msgstr ""
msgid "Metrics|Refresh dashboard"
msgstr ""
-msgid "Metrics|Show last"
-msgstr ""
-
msgid "Metrics|There was an error creating the dashboard."
msgstr ""
@@ -13038,6 +13327,9 @@ msgstr ""
msgid "Microsoft Azure"
msgstr ""
+msgid "Middleman project with Static Site Editor support"
+msgstr ""
+
msgid "Migrated %{success_count}/%{total_count} files."
msgstr ""
@@ -13483,10 +13775,10 @@ msgstr ""
msgid "Next"
msgstr ""
-msgid "Next file in diff (MRs only)"
+msgid "Next file in diff"
msgstr ""
-msgid "Next unresolved discussion (MRs only)"
+msgid "Next unresolved discussion"
msgstr ""
msgid "Nickname"
@@ -13546,6 +13838,9 @@ msgstr ""
msgid "No changes between %{ref_start}%{source_branch}%{ref_end} and %{ref_start}%{target_branch}%{ref_end}"
msgstr ""
+msgid "No child epics match applied filters"
+msgstr ""
+
msgid "No connection could be made to a Gitaly Server, please check your logs!"
msgstr ""
@@ -13684,15 +13979,6 @@ msgstr ""
msgid "No thanks, don't show this again"
msgstr ""
-msgid "No vulnerabilities found for this group"
-msgstr ""
-
-msgid "No vulnerabilities found for this pipeline"
-msgstr ""
-
-msgid "No vulnerabilities found for this project"
-msgstr ""
-
msgid "No vulnerabilities present"
msgstr ""
@@ -13816,6 +14102,9 @@ msgstr ""
msgid "Nothing to preview."
msgstr ""
+msgid "Nothing to synchronize"
+msgstr ""
+
msgid "Notification events"
msgstr ""
@@ -13987,6 +14276,9 @@ msgstr ""
msgid "Once removed, the fork relationship cannot be restored and you will no longer be able to send merge requests to the source."
msgstr ""
+msgid "Once the exported file is ready you can download it from this page."
+msgstr ""
+
msgid "Once the exported file is ready, you will receive a notification email with a download link, or you can download it from this page."
msgstr ""
@@ -14088,9 +14380,6 @@ msgstr ""
msgid "Open sidebar"
msgstr ""
-msgid "Open source software to collaborate on code"
-msgstr ""
-
msgid "Open: %{openIssuesCount}"
msgstr ""
@@ -14193,6 +14482,9 @@ msgstr ""
msgid "OutdatedBrowser|You can provide feedback %{feedback_link_start}on this issue%{feedback_link_end} or via your usual support channels."
msgstr ""
+msgid "Overridden"
+msgstr ""
+
msgid "Overview"
msgstr ""
@@ -14244,6 +14536,9 @@ msgstr ""
msgid "PackageRegistry|Conan Command"
msgstr ""
+msgid "PackageRegistry|Copy .pypirc content"
+msgstr ""
+
msgid "PackageRegistry|Copy Conan Command"
msgstr ""
@@ -14265,6 +14560,9 @@ msgstr ""
msgid "PackageRegistry|Copy NuGet Setup Command"
msgstr ""
+msgid "PackageRegistry|Copy Pip command"
+msgstr ""
+
msgid "PackageRegistry|Copy and paste this inside your %{codeStart}pom.xml%{codeEnd} %{codeStart}dependencies%{codeEnd} block."
msgstr ""
@@ -14298,6 +14596,12 @@ msgstr ""
msgid "PackageRegistry|For more information on the NuGet registry, %{linkStart}see the documentation%{linkEnd}."
msgstr ""
+msgid "PackageRegistry|For more information on the PyPi registry, %{linkStart}see the documentation%{linkEnd}."
+msgstr ""
+
+msgid "PackageRegistry|If you haven't already done so, you will need to add the below to your %{codeStart}.pypirc%{codeEnd} file."
+msgstr ""
+
msgid "PackageRegistry|If you haven't already done so, you will need to add the below to your %{codeStart}pom.xml%{codeEnd} file."
msgstr ""
@@ -14307,6 +14611,9 @@ msgstr ""
msgid "PackageRegistry|Learn how to %{noPackagesLinkStart}publish and share your packages%{noPackagesLinkEnd} with GitLab."
msgstr ""
+msgid "PackageRegistry|Manually Published"
+msgstr ""
+
msgid "PackageRegistry|Maven"
msgstr ""
@@ -14325,12 +14632,18 @@ msgstr ""
msgid "PackageRegistry|NuGet Command"
msgstr ""
+msgid "PackageRegistry|Pip Command"
+msgstr ""
+
msgid "PackageRegistry|Pipeline %{linkStart}%{linkEnd} triggered %{timestamp} by %{author}"
msgstr ""
msgid "PackageRegistry|Published to the repository at %{timestamp}"
msgstr ""
+msgid "PackageRegistry|PyPi"
+msgstr ""
+
msgid "PackageRegistry|Registry Setup"
msgstr ""
@@ -14367,6 +14680,9 @@ msgstr ""
msgid "PackageRegistry|npm"
msgstr ""
+msgid "PackageRegistry|published by %{author}"
+msgstr ""
+
msgid "PackageRegistry|yarn"
msgstr ""
@@ -14382,6 +14698,9 @@ msgstr ""
msgid "PackageType|NuGet"
msgstr ""
+msgid "PackageType|PyPi"
+msgstr ""
+
msgid "Packages"
msgstr ""
@@ -14475,6 +14794,9 @@ msgstr ""
msgid "Past due"
msgstr ""
+msgid "Paste a machine public key here. Read more about how to generate it"
+msgstr ""
+
msgid "Paste a machine public key here. Read more about how to generate it %{link_start}here%{link_end}"
msgstr ""
@@ -14490,9 +14812,6 @@ msgstr ""
msgid "Path"
msgstr ""
-msgid "Path, transfer, remove"
-msgstr ""
-
msgid "Path:"
msgstr ""
@@ -14523,7 +14842,7 @@ msgstr ""
msgid "Percentage"
msgstr ""
-msgid "Perform advanced options such as changing path, transferring, or removing the group."
+msgid "Perform advanced options such as changing path, transferring, exporting, or removing the group."
msgstr ""
msgid "Perform common operations on GitLab project"
@@ -14604,9 +14923,6 @@ msgstr ""
msgid "Pipeline minutes quota"
msgstr ""
-msgid "Pipeline quota"
-msgstr ""
-
msgid "Pipeline subscriptions"
msgstr ""
@@ -14616,6 +14932,9 @@ msgstr ""
msgid "Pipeline: %{status}"
msgstr ""
+msgid "PipelineCharts|CI / CD Analytics"
+msgstr ""
+
msgid "PipelineCharts|Failed:"
msgstr ""
@@ -14694,15 +15013,6 @@ msgstr ""
msgid "Pipelines settings for '%{project_name}' were successfully updated."
msgstr ""
-msgid "Pipelines| to purchase more minutes."
-msgstr ""
-
-msgid "Pipelines|%{namespace_name} has exceeded its pipeline minutes quota."
-msgstr ""
-
-msgid "Pipelines|%{namespace_name} has less than %{notification_level}%% of CI minutes available."
-msgstr ""
-
msgid "Pipelines|API"
msgstr ""
@@ -14724,10 +15034,13 @@ msgstr ""
msgid "Pipelines|Get started with Pipelines"
msgstr ""
-msgid "Pipelines|Loading Pipelines"
+msgid "Pipelines|Group %{namespace_name} has %{percentage}%% or less Shared Runner Pipeline minutes remaining. Once it runs out, no new jobs or pipelines in its projects will run."
msgstr ""
-msgid "Pipelines|Pipelines will not run anymore on shared Runners."
+msgid "Pipelines|Group %{namespace_name} has exceeded its pipeline minutes quota. Unless you buy additional pipeline minutes, no new jobs or pipelines in its projects will run."
+msgstr ""
+
+msgid "Pipelines|Loading Pipelines"
msgstr ""
msgid "Pipelines|Project cache successfully reset."
@@ -14958,6 +15271,9 @@ msgstr ""
msgid "Please select"
msgstr ""
+msgid "Please select a Jira project"
+msgstr ""
+
msgid "Please select a country"
msgstr ""
@@ -15003,6 +15319,9 @@ msgstr ""
msgid "Please wait while we import the repository for you. Refresh at will."
msgstr ""
+msgid "Plugins directory is deprecated and will be removed in 14.0. Please move this file into /file_hooks directory."
+msgstr ""
+
msgid "Pod does not exist"
msgstr ""
@@ -15120,7 +15439,7 @@ msgstr ""
msgid "Prevent users from changing their profile name"
msgstr ""
-msgid "Prevent users from modifing merge request approvers list"
+msgid "Prevent users from modifying merge request approvers list"
msgstr ""
msgid "Prevent users from performing write operations on GitLab while performing maintenance."
@@ -15141,10 +15460,10 @@ msgstr ""
msgid "Previous Artifacts"
msgstr ""
-msgid "Previous file in diff (MRs only)"
+msgid "Previous file in diff"
msgstr ""
-msgid "Previous unresolved discussion (MRs only)"
+msgid "Previous unresolved discussion"
msgstr ""
msgid "Primary"
@@ -15177,6 +15496,9 @@ msgstr ""
msgid "Private profile"
msgstr ""
+msgid "Private projects Minutes cost factor"
+msgstr ""
+
msgid "Private projects can be created in your personal namespace with:"
msgstr ""
@@ -16005,6 +16327,9 @@ msgstr ""
msgid "ProjectSettings|With GitLab Pages you can host your static websites on GitLab"
msgstr ""
+msgid "ProjectSettings|With Metrics Dashboard you can visualize this project performance metrics"
+msgstr ""
+
msgid "ProjectTemplates|.NET Core"
msgstr ""
@@ -16068,6 +16393,9 @@ msgstr ""
msgid "ProjectTemplates|Spring"
msgstr ""
+msgid "ProjectTemplates|Static Site Editor/Middleman"
+msgstr ""
+
msgid "ProjectTemplates|iOS (Swift)"
msgstr ""
@@ -16287,13 +16615,13 @@ msgstr ""
msgid "PrometheusService|Prometheus is being automatically managed on your clusters"
msgstr ""
-msgid "PrometheusService|These metrics will only be monitored after your first deployment to an environment"
+msgid "PrometheusService|Select the Active checkbox to override the Auto Configuration with custom settings. If unchecked, Auto Configuration settings are used."
msgstr ""
-msgid "PrometheusService|Time-series monitoring service"
+msgid "PrometheusService|These metrics will only be monitored after your first deployment to an environment"
msgstr ""
-msgid "PrometheusService|To enable manual configuration, uninstall Prometheus from your clusters"
+msgid "PrometheusService|Time-series monitoring service"
msgstr ""
msgid "PrometheusService|To enable the installation of Prometheus on your clusters, deactivate the manual configuration below"
@@ -16479,6 +16807,9 @@ msgstr ""
msgid "Protip:"
msgstr ""
+msgid "Protocol"
+msgstr ""
+
msgid "Provider"
msgstr ""
@@ -16503,6 +16834,9 @@ msgstr ""
msgid "Public pipelines"
msgstr ""
+msgid "Public projects Minutes cost factor"
+msgstr ""
+
msgid "Pull"
msgstr ""
@@ -16703,6 +17037,9 @@ msgid_plural "Refreshing in %d seconds to show the updated status..."
msgstr[0] ""
msgstr[1] ""
+msgid "Regenerate export"
+msgstr ""
+
msgid "Regenerate instance ID"
msgstr ""
@@ -17080,9 +17417,29 @@ msgstr ""
msgid "Reports|%{combinedString} and %{resolvedString}"
msgstr ""
+msgid "Reports|Accessibility scanning detected %d issue for the source branch only"
+msgid_plural "Reports|Accessibility scanning detected %d issues for the source branch only"
+msgstr[0] ""
+msgstr[1] ""
+
+msgid "Reports|Accessibility scanning detected no issues for the source branch only"
+msgstr ""
+
+msgid "Reports|Accessibility scanning failed loading results"
+msgstr ""
+
+msgid "Reports|Accessibility scanning results are being parsed"
+msgstr ""
+
msgid "Reports|Actions"
msgstr ""
+msgid "Reports|An error occured while loading report"
+msgstr ""
+
+msgid "Reports|An error occurred while loading %{name} results"
+msgstr ""
+
msgid "Reports|Class"
msgstr ""
@@ -17188,7 +17545,7 @@ msgstr ""
msgid "Requested %{time_ago}"
msgstr ""
-msgid "Requested design version does not exist"
+msgid "Requested design version does not exist."
msgstr ""
msgid "Requested states are invalid"
@@ -17372,6 +17729,9 @@ msgstr ""
msgid "Resync"
msgstr ""
+msgid "Resync all"
+msgstr ""
+
msgid "Resync all %{replicableType}"
msgstr ""
@@ -17440,6 +17800,9 @@ msgstr ""
msgid "Revoked personal access token %{personal_access_token_name}!"
msgstr ""
+msgid "Revoked project access token %{project_access_token_name}!"
+msgstr ""
+
msgid "RightSidebar|adding a"
msgstr ""
@@ -17488,6 +17851,9 @@ msgstr ""
msgid "Runner token"
msgstr ""
+msgid "Runner tokens"
+msgstr ""
+
msgid "Runner was not updated."
msgstr ""
@@ -17542,6 +17908,9 @@ msgstr ""
msgid "SAML SSO for %{group_name}"
msgstr ""
+msgid "SAML discovery tokens"
+msgstr ""
+
msgid "SAML for %{group_name}"
msgstr ""
@@ -17626,12 +17995,18 @@ msgstr ""
msgid "Scheduled"
msgstr ""
+msgid "Scheduled to merge this merge request (%{strategy})."
+msgstr ""
+
msgid "Scheduled to merge this merge request when the pipeline succeeds."
msgstr ""
msgid "Schedules"
msgstr ""
+msgid "Schedules to merge this merge request (%{strategy})."
+msgstr ""
+
msgid "Scheduling"
msgstr ""
@@ -17689,6 +18064,9 @@ msgstr ""
msgid "Search branches and tags"
msgstr ""
+msgid "Search by author"
+msgstr ""
+
msgid "Search files"
msgstr ""
@@ -17834,11 +18212,6 @@ msgid_plural "SearchResults|snippets"
msgstr[0] ""
msgstr[1] ""
-msgid "SearchResults|snippet result"
-msgid_plural "SearchResults|snippet results"
-msgstr[0] ""
-msgstr[1] ""
-
msgid "SearchResults|user"
msgid_plural "SearchResults|users"
msgstr[0] ""
@@ -17849,6 +18222,9 @@ msgid_plural "SearchResults|wiki results"
msgstr[0] ""
msgstr[1] ""
+msgid "Searching by both author and message is currently not supported."
+msgstr ""
+
msgid "Seat Link"
msgstr ""
@@ -17876,184 +18252,232 @@ msgstr ""
msgid "Security Dashboard"
msgstr ""
-msgid "Security Dashboard|Error fetching the vulnerability counts. Please check your network connection and try again."
+msgid "Security configuration help link"
+msgstr ""
+
+msgid "Security dashboard"
msgstr ""
-msgid "Security Dashboard|Error fetching the vulnerability list. Please check your network connection and try again."
+msgid "Security report is out of date. Please update your branch with the latest changes from the target branch (%{targetBranchName})"
msgstr ""
-msgid "Security Dashboard|Issue Created"
+msgid "Security report is out of date. Run %{newPipelineLinkStart}a new pipeline%{newPipelineLinkEnd} for the target branch (%{targetBranchName})"
msgstr ""
-msgid "Security Reports|Comment added to '%{vulnerabilityName}'"
+msgid "SecurityConfiguration|Configured"
msgstr ""
-msgid "Security Reports|Comment deleted on '%{vulnerabilityName}'"
+msgid "SecurityConfiguration|Feature"
msgstr ""
-msgid "Security Reports|Comment edited on '%{vulnerabilityName}'"
+msgid "SecurityConfiguration|Feature documentation for %{featureName}"
msgstr ""
-msgid "Security Reports|Create issue"
+msgid "SecurityConfiguration|Not yet configured"
msgstr ""
-msgid "Security Reports|Dismiss vulnerability"
+msgid "SecurityConfiguration|Secure features"
msgstr ""
-msgid "Security Reports|Dismissed '%{vulnerabilityName}'"
+msgid "SecurityConfiguration|Status"
msgstr ""
-msgid "Security Reports|Dismissed '%{vulnerabilityName}'. Turn off the hide dismissed toggle to view."
+msgid "SecurityReports|%{firstProject} and %{secondProject}"
msgstr ""
-msgid "Security Reports|Either you don't have permission to view this dashboard or the dashboard has not been setup. Please check your permission settings with your administrator or check your dashboard configurations to proceed."
+msgid "SecurityReports|%{firstProject}, %{secondProject}, and %{rest}"
msgstr ""
-msgid "Security Reports|Learn more about setting up your dashboard"
+msgid "SecurityReports|Add a project to your dashboard"
msgstr ""
-msgid "Security Reports|More info"
+msgid "SecurityReports|Add or remove projects from your dashboard"
msgstr ""
-msgid "Security Reports|Oops, something doesn't seem right."
+msgid "SecurityReports|Add projects"
msgstr ""
-msgid "Security Reports|Security reports can only be accessed by authorized users."
+msgid "SecurityReports|Comment added to '%{vulnerabilityName}'"
msgstr ""
-msgid "Security Reports|There was an error adding the comment."
+msgid "SecurityReports|Comment deleted on '%{vulnerabilityName}'"
msgstr ""
-msgid "Security Reports|There was an error creating the issue."
+msgid "SecurityReports|Comment edited on '%{vulnerabilityName}'"
msgstr ""
-msgid "Security Reports|There was an error creating the merge request."
+msgid "SecurityReports|Create issue"
msgstr ""
-msgid "Security Reports|There was an error deleting the comment."
+msgid "SecurityReports|Dismiss Selected"
msgstr ""
-msgid "Security Reports|There was an error dismissing the vulnerabilities."
+msgid "SecurityReports|Dismiss vulnerability"
msgstr ""
-msgid "Security Reports|There was an error dismissing the vulnerability."
+msgid "SecurityReports|Dismissed '%{vulnerabilityName}'"
msgstr ""
-msgid "Security Reports|There was an error reverting the dismissal."
+msgid "SecurityReports|Dismissed '%{vulnerabilityName}'. Turn off the hide dismissed toggle to view."
msgstr ""
-msgid "Security Reports|There was an error reverting this dismissal."
+msgid "SecurityReports|Each vulnerability now has a unique page that can be directly linked to, shared, referenced, and tracked as the single source of truth. Vulnerability occurrences also persist across scanner runs, which improves tracking and visibility and reduces duplicates between scans."
msgstr ""
-msgid "Security Reports|Undo dismiss"
+msgid "SecurityReports|Edit dashboard"
msgstr ""
-msgid "Security Reports|You do not have sufficient permissions to access this report"
+msgid "SecurityReports|Either you don't have permission to view this dashboard or the dashboard has not been setup. Please check your permission settings with your administrator or check your dashboard configurations to proceed."
msgstr ""
-msgid "Security Reports|You must sign in as an authorized user to see this report"
+msgid "SecurityReports|Error fetching the vulnerability counts. Please check your network connection and try again."
msgstr ""
-msgid "Security configuration help link"
+msgid "SecurityReports|Error fetching the vulnerability list. Please check your network connection and try again."
msgstr ""
-msgid "Security dashboard"
+msgid "SecurityReports|False positive"
msgstr ""
-msgid "Security report is out of date. Please update your branch with the latest changes from the target branch (%{targetBranchName})"
+msgid "SecurityReports|Hide dismissed"
msgstr ""
-msgid "Security report is out of date. Run %{newPipelineLinkStart}a new pipeline%{newPipelineLinkEnd} for the target branch (%{targetBranchName})"
+msgid "SecurityReports|Introducing standalone vulnerabilities"
msgstr ""
-msgid "SecurityConfiguration|Configured"
+msgid "SecurityReports|Issue Created"
msgstr ""
-msgid "SecurityConfiguration|Feature"
+msgid "SecurityReports|Learn More"
msgstr ""
-msgid "SecurityConfiguration|Feature documentation for %{featureName}"
+msgid "SecurityReports|Learn more about setting up your dashboard"
msgstr ""
-msgid "SecurityConfiguration|Not yet configured"
+msgid "SecurityReports|Load more vulnerabilities"
msgstr ""
-msgid "SecurityConfiguration|Secure features"
+msgid "SecurityReports|Monitor vulnerabilities in your code"
msgstr ""
-msgid "SecurityConfiguration|Status"
+msgid "SecurityReports|More info"
msgstr ""
-msgid "SecurityDashboard|%{firstProject} and %{secondProject}"
+msgid "SecurityReports|More information"
msgstr ""
-msgid "SecurityDashboard|%{firstProject}, %{secondProject}, and %{rest}"
+msgid "SecurityReports|No vulnerabilities found for dashboard"
msgstr ""
-msgid "SecurityDashboard|Add a project to your dashboard"
+msgid "SecurityReports|No vulnerabilities found for this group"
msgstr ""
-msgid "SecurityDashboard|Add or remove projects from your dashboard"
+msgid "SecurityReports|No vulnerabilities found for this pipeline"
msgstr ""
-msgid "SecurityDashboard|Add projects"
+msgid "SecurityReports|No vulnerabilities found for this project"
msgstr ""
-msgid "SecurityDashboard|Edit dashboard"
+msgid "SecurityReports|Oops, something doesn't seem right."
msgstr ""
-msgid "SecurityDashboard|Hide dismissed"
+msgid "SecurityReports|Pipeline %{pipelineLink} triggered %{timeago} by %{user}"
msgstr ""
-msgid "SecurityDashboard|Monitor vulnerabilities in your code"
+msgid "SecurityReports|Project"
msgstr ""
-msgid "SecurityDashboard|More information"
+msgid "SecurityReports|Projects added"
msgstr ""
-msgid "SecurityDashboard|No vulnerabilities found for dashboard"
+msgid "SecurityReports|Remove project from dashboard"
msgstr ""
-msgid "SecurityDashboard|Pipeline %{pipelineLink} triggered %{timeago} by %{user}"
+msgid "SecurityReports|Report type"
msgstr ""
-msgid "SecurityDashboard|Project"
+msgid "SecurityReports|Return to dashboard"
msgstr ""
-msgid "SecurityDashboard|Projects added"
+msgid "SecurityReports|Security Dashboard"
msgstr ""
-msgid "SecurityDashboard|Remove project from dashboard"
+msgid "SecurityReports|Security reports can only be accessed by authorized users."
msgstr ""
-msgid "SecurityDashboard|Report type"
+msgid "SecurityReports|Select a project to add by using the project search field above."
msgstr ""
-msgid "SecurityDashboard|Return to dashboard"
+msgid "SecurityReports|Select a reason"
msgstr ""
-msgid "SecurityDashboard|Security Dashboard"
+msgid "SecurityReports|Severity"
msgstr ""
-msgid "SecurityDashboard|Select a project to add by using the project search field above."
+msgid "SecurityReports|Status"
msgstr ""
-msgid "SecurityDashboard|Severity"
+msgid "SecurityReports|The security dashboard displays the latest security findings for projects you wish to monitor. Select \"Edit dashboard\" to add and remove projects."
msgstr ""
-msgid "SecurityDashboard|Status"
+msgid "SecurityReports|The security dashboard displays the latest security report. Use it to find and fix vulnerabilities."
msgstr ""
-msgid "SecurityDashboard|The security dashboard displays the latest security findings for projects you wish to monitor. Select \"Edit dashboard\" to add and remove projects."
+msgid "SecurityReports|There was an error adding the comment."
msgstr ""
-msgid "SecurityDashboard|The security dashboard displays the latest security report. Use it to find and fix vulnerabilities."
+msgid "SecurityReports|There was an error creating the issue."
msgstr ""
-msgid "SecurityDashboard|There was an error while generating the report."
+msgid "SecurityReports|There was an error creating the merge request."
msgstr ""
-msgid "SecurityDashboard|Unable to add %{invalidProjects}"
+msgid "SecurityReports|There was an error deleting the comment."
+msgstr ""
+
+msgid "SecurityReports|There was an error dismissing the vulnerabilities."
+msgstr ""
+
+msgid "SecurityReports|There was an error dismissing the vulnerability."
+msgstr ""
+
+msgid "SecurityReports|There was an error reverting the dismissal."
+msgstr ""
+
+msgid "SecurityReports|There was an error reverting this dismissal."
+msgstr ""
+
+msgid "SecurityReports|There was an error while generating the report."
+msgstr ""
+
+msgid "SecurityReports|Unable to add %{invalidProjects}"
+msgstr ""
+
+msgid "SecurityReports|Undo dismiss"
+msgstr ""
+
+msgid "SecurityReports|While it's rare to have no vulnerabilities for your group, it can happen. In any event, we ask that you double check your settings to make sure you've set up your dashboard correctly."
+msgstr ""
+
+msgid "SecurityReports|While it's rare to have no vulnerabilities for your pipeline, it can happen. In any event, we ask that you double check your settings to make sure all security scanning jobs have passed successfully."
+msgstr ""
+
+msgid "SecurityReports|While it's rare to have no vulnerabilities for your project, it can happen. In any event, we ask that you double check your settings to make sure you've set up your dashboard correctly."
+msgstr ""
+
+msgid "SecurityReports|While it's rare to have no vulnerabilities, it can happen. In any event, we ask that you please double check your settings to make sure you've set up your dashboard correctly."
+msgstr ""
+
+msgid "SecurityReports|Won't fix / Accept risk"
+msgstr ""
+
+msgid "SecurityReports|You do not have sufficient permissions to access this report"
+msgstr ""
+
+msgid "SecurityReports|You must sign in as an authorized user to see this report"
+msgstr ""
+
+msgid "SecurityReports|[No reason]"
msgstr ""
msgid "See GitLab's %{password_policy_guidelines}"
@@ -18065,6 +18489,9 @@ msgstr ""
msgid "See the affected projects in the GitLab admin panel"
msgstr ""
+msgid "See what's new at GitLab"
+msgstr ""
+
msgid "Select"
msgstr ""
@@ -18470,9 +18897,6 @@ msgstr ""
msgid "Set up Jira Integration"
msgstr ""
-msgid "Set up Jira Integration illustration"
-msgstr ""
-
msgid "Set up a %{type} Runner automatically"
msgstr ""
@@ -18557,6 +18981,9 @@ msgstr ""
msgid "Severity: %{severity}"
msgstr ""
+msgid "Shards (%{shards})"
+msgstr ""
+
msgid "Shards to synchronize"
msgstr ""
@@ -18871,6 +19298,9 @@ msgstr ""
msgid "Snowplow"
msgstr ""
+msgid "Some child epics may be hidden due to applied filters"
+msgstr ""
+
msgid "Some email servers do not support overriding the email sender name. Enable this option to include the name of the author of the issue, merge request or comment in the email body instead."
msgstr ""
@@ -19012,6 +19442,9 @@ msgstr ""
msgid "Something went wrong while updating your list settings"
msgstr ""
+msgid "Something went wrong with your automatic subscription renewal"
+msgstr ""
+
msgid "Something went wrong, unable to add %{project} to dashboard"
msgstr ""
@@ -19297,6 +19730,9 @@ msgstr ""
msgid "Stage removed"
msgstr ""
+msgid "Standard"
+msgstr ""
+
msgid "Star a label to make it a priority label. Order the prioritized labels to change their relative priority, by dragging."
msgstr ""
@@ -19420,6 +19856,9 @@ msgstr ""
msgid "Static Application Security Testing (SAST)"
msgstr ""
+msgid "StaticSiteEditor|An error occurred while submitting your changes."
+msgstr ""
+
msgid "StaticSiteEditor|Branch could not be created."
msgstr ""
@@ -19429,18 +19868,30 @@ msgstr ""
msgid "StaticSiteEditor|Could not create merge request."
msgstr ""
+msgid "StaticSiteEditor|Incompatible file content"
+msgstr ""
+
msgid "StaticSiteEditor|Return to site"
msgstr ""
+msgid "StaticSiteEditor|Static site editor"
+msgstr ""
+
msgid "StaticSiteEditor|Success!"
msgstr ""
msgid "StaticSiteEditor|Summary of changes"
msgstr ""
+msgid "StaticSiteEditor|The Static Site Editor is currently configured to only edit Markdown content on pages generated from Middleman. Visit the documentation to learn more about configuring your site to use the Static Site Editor."
+msgstr ""
+
msgid "StaticSiteEditor|Update %{sourcePath} file"
msgstr ""
+msgid "StaticSiteEditor|View documentation"
+msgstr ""
+
msgid "StaticSiteEditor|View merge request"
msgstr ""
@@ -19498,6 +19949,12 @@ msgstr ""
msgid "StatusPage|Status page"
msgstr ""
+msgid "StatusPage|Status page URL"
+msgstr ""
+
+msgid "StatusPage|Status page frontend documentation"
+msgstr ""
+
msgid "StatusPage|To publish incidents to an external status page, GitLab will store a JSON file in your Amazon S3 account in a location accessible to your external status page service. Make sure to also set up %{docsLink}"
msgstr ""
@@ -19846,6 +20303,12 @@ msgstr ""
msgid "Synced"
msgstr ""
+msgid "Synchronization disabled"
+msgstr ""
+
+msgid "Synchronization of container repositories is disabled."
+msgstr ""
+
msgid "System"
msgstr ""
@@ -19999,6 +20462,9 @@ msgstr ""
msgid "Telephone number"
msgstr ""
+msgid "Telephone number (Optional)"
+msgstr ""
+
msgid "Template"
msgstr ""
@@ -20305,6 +20771,9 @@ msgstr ""
msgid "The license was successfully uploaded and is now active. You can see the details below."
msgstr ""
+msgid "The license was successfully uploaded and will be active from %{starts_at}. You can see the details below."
+msgstr ""
+
msgid "The maximum file size allowed is %{size}."
msgstr ""
@@ -20626,6 +21095,9 @@ msgstr ""
msgid "There was an error getting the epic participants."
msgstr ""
+msgid "There was an error importing the Jira project."
+msgstr ""
+
msgid "There was an error loading users activity calendar."
msgstr ""
@@ -20641,7 +21113,7 @@ msgstr ""
msgid "There was an error resetting user pipeline minutes."
msgstr ""
-msgid "There was an error saving this Geo Node"
+msgid "There was an error saving this Geo Node."
msgstr ""
msgid "There was an error saving your changes."
@@ -20704,6 +21176,9 @@ msgstr ""
msgid "They can be managed using the %{link}."
msgstr ""
+msgid "Third Party Advisory Link"
+msgstr ""
+
msgid "Third party offers"
msgstr ""
@@ -20740,6 +21215,9 @@ msgstr ""
msgid "This application will be able to:"
msgstr ""
+msgid "This attachment has been truncated to avoid exceeding the maximum allowed attachment size of 15MB. %{written_count} of %{issues_count} issues have been included. Consider re-exporting with a narrower selection of issues."
+msgstr ""
+
msgid "This block is self-referential"
msgstr ""
@@ -20881,6 +21359,12 @@ msgstr ""
msgid "This issue is confidential"
msgstr ""
+msgid "This issue is currently blocked by the following issues: %{issues}."
+msgstr ""
+
+msgid "This issue is in a child epic of the filtered epic"
+msgstr ""
+
msgid "This issue is locked."
msgstr ""
@@ -21079,7 +21563,10 @@ msgstr ""
msgid "This user will be the author of all events in the activity feed that are the result of an update, like new branches being created or new commits being pushed to existing branches. Upon creation or when reassigning you can only assign yourself to be the mirror user."
msgstr ""
-msgid "This variable can not be masked"
+msgid "This variable can not be masked."
+msgstr ""
+
+msgid "This variable does not match the expected pattern."
msgstr ""
msgid "This will help us personalize your onboarding experience."
@@ -21109,10 +21596,10 @@ msgstr ""
msgid "ThreatMonitoring|Application firewall not detected"
msgstr ""
-msgid "ThreatMonitoring|Container Network Policy"
+msgid "ThreatMonitoring|Container Network Policies are not installed or have been disabled. To view this data, ensure your Network Policies are installed and enabled for your cluster."
msgstr ""
-msgid "ThreatMonitoring|Container NetworkPolicies are not installed or has been disabled. To view this data, ensure you NetworkPolicies are installed and enabled for your cluster."
+msgid "ThreatMonitoring|Container Network Policy"
msgstr ""
msgid "ThreatMonitoring|Container NetworkPolicies not detected"
@@ -21145,7 +21632,7 @@ msgstr ""
msgid "ThreatMonitoring|Something went wrong, unable to fetch statistics"
msgstr ""
-msgid "ThreatMonitoring|The firewall is not installed or has been disabled. To view this data, ensure you firewall is installed and enabled for your cluster."
+msgid "ThreatMonitoring|The firewall is not installed or has been disabled. To view this data, ensure the web application firewall is installed and enabled for your cluster."
msgstr ""
msgid "ThreatMonitoring|The graph below is an overview of traffic coming to your application as tracked by the Web Application Firewall (WAF). View the docs for instructions on how to access the WAF logs to see what type of malicious traffic is trying to access your app. The docs link is also accessible by clicking the \"?\" icon next to the title below."
@@ -21214,6 +21701,9 @@ msgstr ""
msgid "Time in seconds GitLab will wait for a response from the external service. When the service does not respond in time, access will be denied."
msgstr ""
+msgid "Time of import: %{importTime}"
+msgstr ""
+
msgid "Time remaining"
msgstr ""
@@ -21398,7 +21888,7 @@ msgstr ""
msgid "Title:"
msgstr ""
-msgid "Titles and Filenames"
+msgid "Titles and Descriptions"
msgstr ""
msgid "To"
@@ -21605,7 +22095,7 @@ msgstr ""
msgid "Too many projects enabled. You will need to manage them via the console or the API."
msgstr ""
-msgid "Topics"
+msgid "Topics (optional)"
msgstr ""
msgid "Total"
@@ -21767,6 +22257,9 @@ msgstr ""
msgid "Try all GitLab has to offer for 30 days."
msgstr ""
+msgid "Try changing or removing filters."
+msgstr ""
+
msgid "Try to fork again"
msgstr ""
@@ -21860,6 +22353,9 @@ msgstr ""
msgid "Unable to connect to server: %{error}"
msgstr ""
+msgid "Unable to connect to the Jira instance. Please check your Jira integration configuration."
+msgstr ""
+
msgid "Unable to convert Kubernetes logs encoding to UTF-8"
msgstr ""
@@ -22803,6 +23299,9 @@ msgstr ""
msgid "View issue"
msgstr ""
+msgid "View issues"
+msgstr ""
+
msgid "View it on GitLab"
msgstr ""
@@ -22980,6 +23479,15 @@ msgstr ""
msgid "VulnerabilityManagement|Resolved %{timeago} by %{user}"
msgstr ""
+msgid "VulnerabilityManagement|Something went wrong while trying to delete the comment. Please try again later."
+msgstr ""
+
+msgid "VulnerabilityManagement|Something went wrong while trying to retrieve the vulnerability history. Please try again later."
+msgstr ""
+
+msgid "VulnerabilityManagement|Something went wrong while trying to save the comment. Please try again later."
+msgstr ""
+
msgid "VulnerabilityManagement|Something went wrong, could not create an issue."
msgstr ""
@@ -23010,6 +23518,9 @@ msgstr ""
msgid "VulnerabilityStatusTypes|Resolved"
msgstr ""
+msgid "Vulnerability|%{scannerName} (version %{scannerVersion})"
+msgstr ""
+
msgid "Vulnerability|Class"
msgstr ""
@@ -23043,7 +23554,10 @@ msgstr ""
msgid "Vulnerability|Project"
msgstr ""
-msgid "Vulnerability|Report Type"
+msgid "Vulnerability|Scanner Provider"
+msgstr ""
+
+msgid "Vulnerability|Scanner Type"
msgstr ""
msgid "Vulnerability|Severity"
@@ -23097,9 +23611,15 @@ msgstr ""
msgid "We sent you an email with reset password instructions"
msgstr ""
+msgid "We tried to automatically renew your %{strong}%{plan_name}%{strong_close} subscription for %{strong}%{namespace_name}%{strong_close} on %{expires_on} but something went wrong so your subscription was downgraded to the free plan. Don't worry, your data is safe. We suggest you check your payment method and get in touch with our support team (%{support_link}). They'll gladly help with your subscription renewal."
+msgstr ""
+
msgid "We want to be sure it is you, please confirm you are not a robot."
msgstr ""
+msgid "We will automatically renew your %{strong}%{plan_name}%{strong_close} subscription for %{strong}%{namespace_name}%{strong_close} on %{strong}%{expires_on}%{strong_close}. There's nothing that you need to do, we'll let you know when the renewal is complete. Need more seats, a higher plan or just want to review your payment method?"
+msgstr ""
+
msgid "We've found no vulnerabilities"
msgstr ""
@@ -23204,15 +23724,6 @@ msgstr ""
msgid "When:"
msgstr ""
-msgid "While it's rare to have no vulnerabilities for your group, it can happen. In any event, we ask that you double check your settings to make sure you've set up your dashboard correctly."
-msgstr ""
-
-msgid "While it's rare to have no vulnerabilities for your pipeline, it can happen. In any event, we ask that you double check your settings to make sure all security scanning jobs have passed successfully."
-msgstr ""
-
-msgid "While it's rare to have no vulnerabilities for your project, it can happen. In any event, we ask that you double check your settings to make sure you've set up your dashboard correctly."
-msgstr ""
-
msgid "While it's rare to have no vulnerabilities, it can happen. In any event, we ask that you please double check your settings to make sure you've set up your dashboard correctly."
msgstr ""
@@ -23243,9 +23754,6 @@ msgstr ""
msgid "Wiki"
msgstr ""
-msgid "Wiki pages"
-msgstr ""
-
msgid "Wiki was successfully updated."
msgstr ""
@@ -23429,6 +23937,9 @@ msgstr ""
msgid "Yes, add it"
msgstr ""
+msgid "Yes, close issue"
+msgstr ""
+
msgid "Yes, let me map Google Code users to full names or GitLab users."
msgstr ""
@@ -23624,6 +24135,9 @@ msgstr ""
msgid "You could not create a new trigger."
msgstr ""
+msgid "You didn't renew your %{strong}%{plan_name}%{strong_close} subscription for %{strong}%{namespace_name}%{strong_close} so it was downgraded to the free plan."
+msgstr ""
+
msgid "You didn't renew your %{strong}%{plan_name}%{strong_close} subscription so it was downgraded to the GitLab Core Plan."
msgstr ""
@@ -23690,6 +24204,9 @@ msgstr ""
msgid "You have declined the invitation to join %{label}."
msgstr ""
+msgid "You have imported from this project %{numberOfPreviousImportsForProject} times before. Each new import will create duplicate issues."
+msgstr ""
+
msgid "You have no permissions"
msgstr ""
@@ -23771,6 +24288,9 @@ msgstr ""
msgid "You need to upload a Google Takeout archive."
msgstr ""
+msgid "You reached %{usage_in_percent} of %{namespace_name}'s capacity (%{used_storage} of %{storage_limit})"
+msgstr ""
+
msgid "You tried to fork %{link_to_the_project} but it failed for the following reason:"
msgstr ""
@@ -23855,9 +24375,21 @@ msgstr ""
msgid "YouTube"
msgstr ""
+msgid "Your %{strong}%{plan_name}%{strong_close} subscription for %{strong}%{namespace_name}%{strong_close} will expire on %{strong}%{expires_on}%{strong_close}. After that, you will not to be able to create issues or merge requests as well as many other features."
+msgstr ""
+
msgid "Your %{strong}%{plan_name}%{strong_close} subscription will expire on %{strong}%{expires_on}%{strong_close}. After that, you will not to be able to create issues or merge requests as well as many other features."
msgstr ""
+msgid "Your CSV export has started. It will be emailed to %{email} when complete."
+msgstr ""
+
+msgid "Your CSV export of %{issues_count} from project %{project_link} has been added to this email as an attachment."
+msgstr ""
+
+msgid "Your CSV export of %{written_count} from project %{project_name} (%{project_url}) has been added to this email as an attachment."
+msgstr ""
+
msgid "Your Commit Email will be used for web based operations, such as edits and merges."
msgstr ""
@@ -23954,6 +24486,9 @@ msgstr ""
msgid "Your comment could not be updated! Please check your network connection and try again."
msgstr ""
+msgid "Your comment will be discarded."
+msgstr ""
+
msgid "Your custom stage '%{title}' was created"
msgstr ""
@@ -23993,6 +24528,9 @@ msgstr ""
msgid "Your new personal access token has been created."
msgstr ""
+msgid "Your new project access token has been created."
+msgstr ""
+
msgid "Your password isn't required to view this page. If a password or any other personal details are requested, please contact your administrator to report abuse."
msgstr ""
@@ -24011,12 +24549,18 @@ msgstr ""
msgid "Your request for access has been queued for review."
msgstr ""
+msgid "Your search didn't match any commits."
+msgstr ""
+
msgid "Your subscription expired!"
msgstr ""
msgid "Your subscription has been downgraded"
msgstr ""
+msgid "Your subscription will automatically renew in %{remaining_days}"
+msgstr ""
+
msgid "Your subscription will expire in %{remaining_days}"
msgstr ""
@@ -24055,7 +24599,7 @@ msgstr ""
msgid "allowed to fail"
msgstr ""
-msgid "already being used for another group or project milestone."
+msgid "already being used for another group or project %{timebox_name}."
msgstr ""
msgid "already has a \"created\" issue link"
@@ -24133,6 +24677,9 @@ msgstr ""
msgid "ciReport|%{linkStartTag}Learn more about SAST %{linkEndTag}"
msgstr ""
+msgid "ciReport|%{linkStartTag}Learn more about Secret Scanning %{linkEndTag}"
+msgstr ""
+
msgid "ciReport|%{linkStartTag}Learn more about codequality reports %{linkEndTag}"
msgstr ""
@@ -24296,6 +24843,12 @@ msgstr ""
msgid "ciReport|SAST"
msgstr ""
+msgid "ciReport|Secret scanning"
+msgstr ""
+
+msgid "ciReport|Secret scanning detects secrets and credentials vulnerabilities in your source code."
+msgstr ""
+
msgid "ciReport|Security scanning"
msgstr ""
@@ -24493,6 +25046,9 @@ msgstr ""
msgid "group"
msgstr ""
+msgid "groups"
+msgstr ""
+
msgid "has already been linked to another vulnerability"
msgstr ""
@@ -24656,9 +25212,6 @@ msgstr[1] ""
msgid "merged %{time_ago}"
msgstr ""
-msgid "milestone should belong either to a project or a group."
-msgstr ""
-
msgid "missing"
msgstr ""
@@ -24995,6 +25548,9 @@ msgstr ""
msgid "no contributions"
msgstr ""
+msgid "no expiration"
+msgstr ""
+
msgid "no one can merge"
msgstr ""
@@ -25047,6 +25603,9 @@ msgstr ""
msgid "pending removal"
msgstr ""
+msgid "per day"
+msgstr ""
+
msgid "pipeline"
msgstr ""
@@ -25075,6 +25634,12 @@ msgstr[1] ""
msgid "project avatar"
msgstr ""
+msgid "projects"
+msgstr ""
+
+msgid "push to your repository, create pipelines, create issues or add comments. To reduce storage capacity, delete unused repositories, artifacts, wikis, issues, and pipelines."
+msgstr ""
+
msgid "quick actions"
msgstr ""
@@ -25221,11 +25786,6 @@ msgstr ""
msgid "this document"
msgstr ""
-msgid "thread resolved"
-msgid_plural "threads resolved"
-msgstr[0] ""
-msgstr[1] ""
-
msgid "to help your contributors communicate effectively!"
msgstr ""
@@ -25280,6 +25840,9 @@ msgstr ""
msgid "view the blob"
msgstr ""
+msgid "vulnerability|Add a comment"
+msgstr ""
+
msgid "vulnerability|Add a comment or reason for dismissal"
msgstr ""
diff --git a/locale/fr/gitlab.po b/locale/fr/gitlab.po
index 58604450c93..fc31e7c2d23 100644
--- a/locale/fr/gitlab.po
+++ b/locale/fr/gitlab.po
@@ -12,7 +12,7 @@ msgstr ""
"X-Crowdin-Project: gitlab-ee\n"
"X-Crowdin-Language: fr\n"
"X-Crowdin-File: /master/locale/gitlab.pot\n"
-"PO-Revision-Date: 2020-04-15 00:32\n"
+"PO-Revision-Date: 2020-05-05 21:11\n"
msgid " %{start} to %{end}"
msgstr ""
@@ -212,6 +212,11 @@ msgid_plural "%d tags"
msgstr[0] ""
msgstr[1] ""
+msgid "%d unresolved thread"
+msgid_plural "%d unresolved threads"
+msgstr[0] ""
+msgstr[1] ""
+
msgid "%d vulnerability dismissed"
msgid_plural "%d vulnerabilities dismissed"
msgstr[0] ""
@@ -371,6 +376,9 @@ msgstr ""
msgid "%{mrText}, this issue will be closed automatically."
msgstr ""
+msgid "%{namespace_name} is now read-only. You cannot: %{base_message}"
+msgstr ""
+
msgid "%{name} contained %{resultsString}"
msgstr ""
@@ -383,6 +391,11 @@ msgstr ""
msgid "%{name}'s avatar"
msgstr "Avatar de %{name}"
+msgid "%{no_of_days} day"
+msgid_plural "%{no_of_days} days"
+msgstr[0] ""
+msgstr[1] ""
+
msgid "%{number_commits_behind} commits behind %{default_branch}, %{number_commits_ahead} commits ahead"
msgstr "%{number_commits_behind} commits de retard sur %{default_branch}, %{number_commits_ahead} commits d’avance"
@@ -506,6 +519,9 @@ msgstr[1] ""
msgid "%{text} is available"
msgstr "%{text} est disponible"
+msgid "%{timebox_name} should belong either to a project or a group."
+msgstr ""
+
msgid "%{title} %{operator} %{threshold}"
msgstr ""
@@ -781,12 +797,18 @@ msgstr "<code>\"johnsmith@example.com\": \"johnsm...@example.com\"</code> ajoute
msgid "<code>\"johnsmith@example.com\": \"johnsmith@example.com\"</code> will add \"By <a href=\"#\">johnsmith@example.com</a>\" to all issues and comments originally created by johnsmith@example.com. By default, the email address or username is masked to ensure the user's privacy. Use this option if you want to show the full email address."
msgstr "<code>\"johnsmith@example.com\": \"johnsmith@example.com\"</code> ajoutera « Par <a href=\"#\">johnsmith@example.com</a> » à tous les tickets et commentaires créés à l’origine par johnsmith@example.com. Par défaut, l’adresse de courriel ou le nom d’utilisateur est masqué pour garantir la confidentialité de l’utilisateur. Utilisez cette option si vous souhaitez afficher l’adresse de courriel complète."
+msgid "<namespace / project>"
+msgstr ""
+
msgid "<no name set>"
msgstr ""
msgid "<no scopes selected>"
msgstr ""
+msgid "<project name>"
+msgstr ""
+
msgid "<strong>%{group_name}</strong> group members"
msgstr "Membres du groupe <strong>%{group_name}</strong>"
@@ -820,6 +842,9 @@ msgstr ""
msgid "A basic page and serverless function that uses AWS Lambda, AWS API Gateway, and GitLab Pages"
msgstr ""
+msgid "A complete DevOps platform"
+msgstr ""
+
msgid "A default branch cannot be chosen for an empty project."
msgstr "Une branche par défaut ne peut pas être choisie pour un projet vide."
@@ -1036,6 +1061,12 @@ msgstr ""
msgid "AccessTokens|reset it"
msgstr ""
+msgid "AccessibilityReport|Accessibility report artifact not found"
+msgstr ""
+
+msgid "AccessibilityReport|Failed to retrieve accessibility report"
+msgstr ""
+
msgid "AccessibilityReport|Learn More"
msgstr ""
@@ -1045,6 +1076,9 @@ msgstr ""
msgid "AccessibilityReport|New"
msgstr ""
+msgid "AccessibilityReport|The accessibility scanning found an error of the following type: %{code}"
+msgstr ""
+
msgid "Account"
msgstr "Compte"
@@ -1287,6 +1321,9 @@ msgstr ""
msgid "Added at"
msgstr "Ajouté à"
+msgid "Added for this merge request"
+msgstr ""
+
msgid "Added in this version"
msgstr ""
@@ -1658,6 +1695,72 @@ msgid_plural "Alerts"
msgstr[0] "Alerte"
msgstr[1] "Alertes"
+msgid "Alert Details"
+msgstr ""
+
+msgid "AlertManagement|Acknowledged"
+msgstr ""
+
+msgid "AlertManagement|Alert"
+msgstr ""
+
+msgid "AlertManagement|Authorize external service"
+msgstr ""
+
+msgid "AlertManagement|Display alerts from all your monitoring tools directly within GitLab. Streamline the investigation of your alerts and the escalation of alerts to incidents."
+msgstr ""
+
+msgid "AlertManagement|End time"
+msgstr ""
+
+msgid "AlertManagement|End time:"
+msgstr ""
+
+msgid "AlertManagement|Events"
+msgstr ""
+
+msgid "AlertManagement|Events:"
+msgstr ""
+
+msgid "AlertManagement|Full Alert Details"
+msgstr ""
+
+msgid "AlertManagement|More information"
+msgstr ""
+
+msgid "AlertManagement|No alerts available to display. If you think you're seeing this message in error, refresh the page."
+msgstr ""
+
+msgid "AlertManagement|No alerts to display."
+msgstr ""
+
+msgid "AlertManagement|Overview"
+msgstr ""
+
+msgid "AlertManagement|Resolved"
+msgstr ""
+
+msgid "AlertManagement|Severity"
+msgstr ""
+
+msgid "AlertManagement|Start time"
+msgstr ""
+
+msgid "AlertManagement|Start time:"
+msgstr ""
+
+msgid "AlertManagement|Status"
+msgstr ""
+
+msgid "AlertManagement|Surface alerts in GitLab"
+msgstr ""
+
+msgid "AlertManagement|There was an error displaying the alerts. Confirm your endpoint's configuration details to ensure alerts appear."
+msgstr ""
+
+msgid "AlertManagement|Triggered"
+msgstr ""
+
msgid "AlertService|%{linkStart}Learn more%{linkEnd} about configuring this endpoint to receive alerts."
msgstr ""
@@ -1727,6 +1830,9 @@ msgstr ""
msgid "All security scans are enabled because %{linkStart}Auto DevOps%{linkEnd} is enabled on this project"
msgstr ""
+msgid "All threads resolved"
+msgstr ""
+
msgid "All users"
msgstr "Tous les utilisateurs"
@@ -1745,6 +1851,9 @@ msgstr ""
msgid "Allow only the selected protocols to be used for Git access."
msgstr ""
+msgid "Allow owners to manage default branch protection per group"
+msgstr ""
+
msgid "Allow owners to manually add users outside of LDAP"
msgstr ""
@@ -1853,6 +1962,9 @@ msgstr ""
msgid "An error occurred fetching the dropdown data."
msgstr ""
+msgid "An error occurred fetching the project authors."
+msgstr ""
+
msgid "An error occurred previewing the blob"
msgstr "Une erreur s’est produite lors de la prévisualisation du blob"
@@ -1934,6 +2046,9 @@ msgstr ""
msgid "An error occurred while fetching sidebar data"
msgstr "Une erreur s’est produite lors de la récupération des données de la barre latérale"
+msgid "An error occurred while fetching terraform reports."
+msgstr ""
+
msgid "An error occurred while fetching the Service Desk address."
msgstr ""
@@ -2171,6 +2286,9 @@ msgstr "Vérification antiâ€pourriel"
msgid "Any"
msgstr "Tout"
+msgid "Any Author"
+msgstr ""
+
msgid "Any Label"
msgstr "Toute étiquette"
@@ -2386,10 +2504,10 @@ msgstr ""
msgid "Are you sure that you want to unarchive this project?"
msgstr ""
-msgid "Are you sure you want to cancel creating this comment?"
+msgid "Are you sure you want to cancel editing this comment?"
msgstr ""
-msgid "Are you sure you want to cancel editing this comment?"
+msgid "Are you sure you want to close this blocked issue?"
msgstr ""
msgid "Are you sure you want to delete %{name}?"
@@ -2419,6 +2537,9 @@ msgstr ""
msgid "Are you sure you want to deploy this environment?"
msgstr ""
+msgid "Are you sure you want to discard this comment?"
+msgstr ""
+
msgid "Are you sure you want to erase this build?"
msgstr "Êtesâ€vous sûr(e) de vouloir supprimer cette construction ?"
@@ -2595,6 +2716,9 @@ msgstr ""
msgid "At least one approval from a code owner is required to change files matching the respective CODEOWNER rules."
msgstr "Au moins une approbation d’un propriétaire de code est requise pour modifier les fichiers correspondant aux règles de propriété du code (CODEOWNER)."
+msgid "At least one logging option is required to be enabled"
+msgstr ""
+
msgid "At least one of group_id or project_id must be specified"
msgstr ""
@@ -2633,6 +2757,27 @@ msgstr ""
msgid "AuditEvents|Target"
msgstr ""
+msgid "AuditLogs|(removed)"
+msgstr ""
+
+msgid "AuditLogs|Action"
+msgstr ""
+
+msgid "AuditLogs|Author"
+msgstr ""
+
+msgid "AuditLogs|Date"
+msgstr ""
+
+msgid "AuditLogs|IP Address"
+msgstr ""
+
+msgid "AuditLogs|Object"
+msgstr ""
+
+msgid "AuditLogs|Target"
+msgstr ""
+
msgid "Aug"
msgstr "août"
@@ -2915,9 +3060,6 @@ msgstr ""
msgid "BambooService|You must set up automatic revision labeling and a repository trigger in Bamboo."
msgstr ""
-msgid "Batch operations"
-msgstr ""
-
msgid "BatchComments|Delete all pending comments"
msgstr "Supprimer tous les commentaires en attente"
@@ -3047,6 +3189,9 @@ msgstr ""
msgid "Boards"
msgstr "Tableaux"
+msgid "Boards and Board Lists"
+msgstr ""
+
msgid "Boards|Collapse"
msgstr ""
@@ -3266,6 +3411,9 @@ msgstr ""
msgid "Buy GitLab Enterprise Edition"
msgstr ""
+msgid "Buy more Pipeline minutes"
+msgstr ""
+
msgid "By %{user_name}"
msgstr ""
@@ -3386,6 +3534,9 @@ msgstr ""
msgid "Can't scan the code?"
msgstr ""
+msgid "Can't update snippet: %{err}"
+msgstr ""
+
msgid "Canary"
msgstr ""
@@ -3554,6 +3705,9 @@ msgstr ""
msgid "Changing group path can have unintended side effects."
msgstr ""
+msgid "Charts"
+msgstr ""
+
msgid "Charts can't be displayed as the request for data has timed out. %{documentationLink}"
msgstr ""
@@ -3590,9 +3744,6 @@ msgstr ""
msgid "ChatMessage|and [%{count} more](%{pipeline_failed_jobs_url})"
msgstr ""
-msgid "ChatMessage|failed"
-msgstr ""
-
msgid "ChatMessage|has failed"
msgstr ""
@@ -3608,9 +3759,6 @@ msgstr ""
msgid "ChatMessage|in %{project_link}"
msgstr ""
-msgid "ChatMessage|passed"
-msgstr ""
-
msgid "Check again"
msgstr ""
@@ -4061,9 +4209,6 @@ msgstr ""
msgid "Click any <strong>project name</strong> in the project list below to navigate to the project milestone."
msgstr "Cliquez sur n’importe quel <strong>nom de projet</strong> dans la liste des projets ciâ€dessous pour naviguer jusqu’au jalon du projet."
-msgid "Click here"
-msgstr ""
-
msgid "Click the <strong>Download</strong> button and wait for downloading to complete."
msgstr "Cliquez sur le bouton <strong>Télécharger</strong> et attendez que le téléchargement soit terminé."
@@ -4451,6 +4596,12 @@ msgstr "Récupération des projets"
msgid "ClusterIntegration|Fetching zones"
msgstr "Récupération des zones"
+msgid "ClusterIntegration|Fluentd"
+msgstr ""
+
+msgid "ClusterIntegration|Fluentd is an open source data collector, which lets you unify the data collection and consumption for a better use and understanding of data. It requires at least one of the following logs to be successfully installed."
+msgstr ""
+
msgid "ClusterIntegration|GitLab Integration"
msgstr "Intégration GitLab"
@@ -4757,6 +4908,15 @@ msgstr "La demande de lancement de l’installation a échoué"
msgid "ClusterIntegration|Request to begin uninstalling failed"
msgstr ""
+msgid "ClusterIntegration|SIEM Hostname"
+msgstr ""
+
+msgid "ClusterIntegration|SIEM Port"
+msgstr ""
+
+msgid "ClusterIntegration|SIEM Protocol"
+msgstr ""
+
msgid "ClusterIntegration|Save changes"
msgstr "Enregistrer les modifications"
@@ -4853,6 +5013,12 @@ msgstr "Sélectionnez la zone"
msgid "ClusterIntegration|Select zone to choose machine type"
msgstr "Sélectionnez la zone afin de choisir le type de machine"
+msgid "ClusterIntegration|Send Cilium Logs"
+msgstr ""
+
+msgid "ClusterIntegration|Send ModSecurity Logs"
+msgstr ""
+
msgid "ClusterIntegration|Service Token"
msgstr ""
@@ -5281,18 +5447,33 @@ msgstr ""
msgid "Compliance framework (optional)"
msgstr ""
+msgid "ComplianceFramework|GDPR"
+msgstr ""
+
msgid "ComplianceFramework|GDPR - General Data Protection Regulation"
msgstr ""
+msgid "ComplianceFramework|HIPAA"
+msgstr ""
+
msgid "ComplianceFramework|HIPAA - Health Insurance Portability and Accountability Act"
msgstr ""
+msgid "ComplianceFramework|PCI-DSS"
+msgstr ""
+
msgid "ComplianceFramework|PCI-DSS - Payment Card Industry-Data Security Standard"
msgstr ""
+msgid "ComplianceFramework|SOC 2"
+msgstr ""
+
msgid "ComplianceFramework|SOC 2 - Service Organization Control 2"
msgstr ""
+msgid "ComplianceFramework|SOX"
+msgstr ""
+
msgid "ComplianceFramework|SOX - Sarbanes-Oxley"
msgstr ""
@@ -5440,9 +5621,15 @@ msgstr ""
msgid "Container repositories sync capacity"
msgstr ""
+msgid "ContainerRegistry| Please visit the %{linkStart}administration settings%{linkEnd} to enable this feature."
+msgstr ""
+
msgid "ContainerRegistry|%{imageName} tags"
msgstr ""
+msgid "ContainerRegistry|%{title} was successfully scheduled for deletion"
+msgstr ""
+
msgid "ContainerRegistry|Automatically remove extra images that aren't designed to be kept."
msgstr ""
@@ -5455,6 +5642,9 @@ msgstr ""
msgid "ContainerRegistry|Container Registry"
msgstr ""
+msgid "ContainerRegistry|Container Registry tag expiration and retention policy is disabled"
+msgstr ""
+
msgid "ContainerRegistry|Copy build command"
msgstr ""
@@ -5464,18 +5654,12 @@ msgstr ""
msgid "ContainerRegistry|Copy push command"
msgstr ""
-msgid "ContainerRegistry|Currently, the Container Registry tag expiration feature is not available for projects created before GitLab version 12.8. For updates and more information, visit Issue %{linkStart}#196124%{linkEnd}"
-msgstr ""
-
msgid "ContainerRegistry|Docker connection error"
msgstr ""
msgid "ContainerRegistry|Docker tag expiration policy is %{toggleStatus}"
msgstr ""
-msgid "ContainerRegistry|Docker tags with names matching this regex pattern will expire:"
-msgstr ""
-
msgid "ContainerRegistry|Edit Settings"
msgstr ""
@@ -5497,9 +5681,6 @@ msgstr ""
msgid "ContainerRegistry|Image ID"
msgstr ""
-msgid "ContainerRegistry|Image deleted successfully"
-msgstr ""
-
msgid "ContainerRegistry|Keep and protect the images that matter most."
msgstr ""
@@ -5515,12 +5696,24 @@ msgstr ""
msgid "ContainerRegistry|Number of tags to retain:"
msgstr ""
+msgid "ContainerRegistry|Please contact your administrator."
+msgstr ""
+
msgid "ContainerRegistry|Push an image"
msgstr ""
msgid "ContainerRegistry|Quick Start"
msgstr ""
+msgid "ContainerRegistry|Regular expressions such as %{codeStart}.*-test%{codeEnd} or %{codeStart}dev-.*%{codeEnd} are supported"
+msgstr ""
+
+msgid "ContainerRegistry|Regular expressions such as %{codeStart}.*-test%{codeEnd} or %{codeStart}dev-.*%{codeEnd} are supported. To select all tags, use %{codeStart}.*%{codeEnd}"
+msgstr ""
+
+msgid "ContainerRegistry|Remember to run %{docLinkStart}garbage collection%{docLinkEnd} to remove the stale data from storage."
+msgstr ""
+
msgid "ContainerRegistry|Remove repository"
msgstr "Supprimer le dépôt"
@@ -5535,22 +5728,22 @@ msgstr[1] ""
msgid "ContainerRegistry|Retention policy has been Enabled"
msgstr ""
-msgid "ContainerRegistry|Something went wrong while deleting the image."
+msgid "ContainerRegistry|Something went wrong while fetching the expiration policy."
msgstr ""
-msgid "ContainerRegistry|Something went wrong while deleting the tag."
+msgid "ContainerRegistry|Something went wrong while fetching the repository list."
msgstr ""
-msgid "ContainerRegistry|Something went wrong while deleting the tags."
+msgid "ContainerRegistry|Something went wrong while fetching the tags list."
msgstr ""
-msgid "ContainerRegistry|Something went wrong while fetching the expiration policy."
+msgid "ContainerRegistry|Something went wrong while marking the tag for deletion."
msgstr ""
-msgid "ContainerRegistry|Something went wrong while fetching the packages list."
+msgid "ContainerRegistry|Something went wrong while marking the tags for deletion."
msgstr ""
-msgid "ContainerRegistry|Something went wrong while fetching the tags list."
+msgid "ContainerRegistry|Something went wrong while scheduling %{title} for deletion. Please try again."
msgstr ""
msgid "ContainerRegistry|Something went wrong while updating the expiration policy."
@@ -5559,16 +5752,25 @@ msgstr ""
msgid "ContainerRegistry|Tag"
msgstr "Étiquette"
-msgid "ContainerRegistry|Tag deleted successfully"
-msgstr ""
-
msgid "ContainerRegistry|Tag expiration policy"
msgstr ""
msgid "ContainerRegistry|Tag expiration policy is designed to:"
msgstr ""
-msgid "ContainerRegistry|Tags deleted successfully"
+msgid "ContainerRegistry|Tag successfully marked for deletion."
+msgstr ""
+
+msgid "ContainerRegistry|Tags successfully marked for deletion."
+msgstr ""
+
+msgid "ContainerRegistry|Tags with names matching this regex pattern will %{italicStart}be preserved:%{italicEnd}"
+msgstr ""
+
+msgid "ContainerRegistry|Tags with names matching this regex pattern will %{italicStart}expire:%{italicEnd}"
+msgstr ""
+
+msgid "ContainerRegistry|The Container Registry tag expiration and retention policies for this project have not been enabled."
msgstr ""
msgid "ContainerRegistry|The last tag related to this image was recently removed. This empty image and any associated data will be automatically removed as part of the regular Garbage Collection process. If you have any questions, contact your administrator."
@@ -5589,16 +5791,16 @@ msgstr ""
msgid "ContainerRegistry|There are no container images stored for this project"
msgstr ""
-msgid "ContainerRegistry|This Registry contains deleted image tag data. Remember to run %{docLinkStart}garbage collection%{docLinkEnd} to remove the stale data from storage."
+msgid "ContainerRegistry|There was an error during the deletion of this image repository, please try again."
msgstr ""
msgid "ContainerRegistry|This image has no active tags"
msgstr ""
-msgid "ContainerRegistry|We are having trouble connecting to Docker, which could be due to an issue with your project name or path. %{docLinkStart}More Information%{docLinkEnd}"
+msgid "ContainerRegistry|This image repository is scheduled for deletion"
msgstr ""
-msgid "ContainerRegistry|Wildcards such as %{codeStart}.*-stable%{codeEnd} or %{codeStart}production/.*%{codeEnd} are supported. To select all tags, use %{codeStart}.*%{codeEnd}"
+msgid "ContainerRegistry|We are having trouble connecting to Docker, which could be due to an issue with your project name or path. %{docLinkStart}More Information%{docLinkEnd}"
msgstr ""
msgid "ContainerRegistry|With the Container Registry, every project can have its own space to store its Docker images. %{docLinkStart}More Information%{docLinkEnd}"
@@ -5853,7 +6055,7 @@ msgstr ""
msgid "Could not delete chat nickname %{chat_name}."
msgstr ""
-msgid "Could not find design"
+msgid "Could not find design."
msgstr ""
msgid "Could not remove the trigger."
@@ -5868,6 +6070,9 @@ msgstr ""
msgid "Could not revoke personal access token %{personal_access_token_name}."
msgstr ""
+msgid "Could not revoke project access token %{project_access_token_name}."
+msgstr ""
+
msgid "Could not save group ID"
msgstr ""
@@ -5925,6 +6130,9 @@ msgstr ""
msgid "Create a new branch"
msgstr "Créer une nouvelle branche"
+msgid "Create a new deploy key for this project"
+msgstr ""
+
msgid "Create a new file as there are no files yet. Afterwards, you'll be able to commit your changes."
msgstr ""
@@ -6030,6 +6238,9 @@ msgstr "Créer une étiquette de projet"
msgid "Create requirement"
msgstr ""
+msgid "Create snippet"
+msgstr ""
+
msgid "Create wildcard: %{searchTerm}"
msgstr ""
@@ -6051,6 +6262,9 @@ msgstr "Créer un jeton d’accès personnel"
msgid "Created"
msgstr "Créé"
+msgid "Created %{timestamp}"
+msgstr ""
+
msgid "Created At"
msgstr "Créé à"
@@ -6407,6 +6621,9 @@ msgstr ""
msgid "CycleAnalytics|stage dropdown"
msgstr ""
+msgid "DAG"
+msgstr ""
+
msgid "DNS"
msgstr ""
@@ -6773,6 +6990,9 @@ msgstr "Clefs de déploiement"
msgid "Deploy key was successfully updated."
msgstr ""
+msgid "Deploy keys allow read-only or read-write (if enabled) access to your repository. Deploy keys can be used for CI, staging or production servers. You can create a deploy key or add an existing one."
+msgstr ""
+
msgid "Deploy progress not found. To see pods, ensure your environment matches %{linkStart}deploy board criteria%{linkEnd}."
msgstr ""
@@ -6926,6 +7146,9 @@ msgstr "Déployé sur"
msgid "Deploying to"
msgstr "En cours de déploiement sur"
+msgid "Deployment Frequency"
+msgstr ""
+
msgid "Deployment|API"
msgstr ""
@@ -7444,9 +7667,6 @@ msgstr ""
msgid "Edit environment"
msgstr ""
-msgid "Edit epic description"
-msgstr ""
-
msgid "Edit file"
msgstr ""
@@ -8068,9 +8288,6 @@ msgstr ""
msgid "Epics"
msgstr "Épopées"
-msgid "Epics (Ultimate / Gold license only)"
-msgstr ""
-
msgid "Epics Roadmap"
msgstr "Feuille de route des épopées"
@@ -8080,6 +8297,9 @@ msgstr ""
msgid "Epics let you manage your portfolio of projects more efficiently and with less effort"
msgstr "Les épopées vous permettent de gérer votre portefeuille de projets plus efficacement et avec moins d’efforts"
+msgid "Epics, Issues, and Merge Requests"
+msgstr ""
+
msgid "Epics|Add an epic"
msgstr ""
@@ -8131,6 +8351,9 @@ msgstr ""
msgid "Epics|Something went wrong while fetching group epics."
msgstr ""
+msgid "Epics|Something went wrong while moving item."
+msgstr ""
+
msgid "Epics|Something went wrong while ordering item."
msgstr ""
@@ -8185,6 +8408,9 @@ msgstr "Erreur lors de la récupération des étiquettes."
msgid "Error fetching network graph."
msgstr "Erreur lors de la récupération du graphique du réseau."
+msgid "Error fetching payload data."
+msgstr ""
+
msgid "Error fetching projects"
msgstr ""
@@ -8194,9 +8420,6 @@ msgstr "Erreur lors de la récupération des refs"
msgid "Error fetching the dependency list. Please check your network connection and try again."
msgstr ""
-msgid "Error fetching usage ping data."
-msgstr "Erreur lors de la récupération des données d’utilisation."
-
msgid "Error loading branch data. Please try again."
msgstr "Erreur lors du chargement des données de branche. Veuillez réessayer."
@@ -8338,6 +8561,9 @@ msgstr ""
msgid "Errors"
msgstr ""
+msgid "Errors:"
+msgstr ""
+
msgid "Estimated"
msgstr "Estimé"
@@ -8542,12 +8768,18 @@ msgstr "Explorer les groupes publics"
msgid "Export as CSV"
msgstr ""
+msgid "Export group"
+msgstr ""
+
msgid "Export issues"
msgstr ""
msgid "Export project"
msgstr ""
+msgid "Export this group with all related data to a new GitLab instance. Once complete, you can import the data file from the \"New Group\" page."
+msgstr ""
+
msgid "Export this project with all its related data in order to move your project to a new GitLab instance. Once the export is finished, you can import the file from the \"New Project\" page."
msgstr ""
@@ -8617,9 +8849,6 @@ msgstr "Échec"
msgid "Failed Jobs"
msgstr "Tâches ayant échoué"
-msgid "Failed create wiki"
-msgstr ""
-
msgid "Failed to add a Zoom meeting"
msgstr ""
@@ -8659,6 +8888,9 @@ msgstr ""
msgid "Failed to create resources"
msgstr ""
+msgid "Failed to create wiki"
+msgstr ""
+
msgid "Failed to delete board. Please try again."
msgstr ""
@@ -8668,7 +8900,7 @@ msgstr "Échec du déploiement sur"
msgid "Failed to enqueue the rebase operation, possibly due to a long-lived transaction. Try again later."
msgstr ""
-msgid "Failed to find import label for jira import."
+msgid "Failed to find import label for Jira import."
msgstr ""
msgid "Failed to get ref."
@@ -8692,6 +8924,12 @@ msgstr ""
msgid "Failed to load groups & users."
msgstr ""
+msgid "Failed to load labels. Please try again."
+msgstr ""
+
+msgid "Failed to load milestones. Please try again."
+msgstr ""
+
msgid "Failed to load related branches"
msgstr ""
@@ -9067,7 +9305,10 @@ msgstr "Filtrer par message de commit"
msgid "Filter by milestone name"
msgstr ""
-msgid "Filter by name..."
+msgid "Filter by name"
+msgstr ""
+
+msgid "Filter by status"
msgstr ""
msgid "Filter by two-factor authentication"
@@ -9079,12 +9320,18 @@ msgstr ""
msgid "Filter projects"
msgstr ""
+msgid "Filter results"
+msgstr ""
+
msgid "Filter results by group"
msgstr ""
msgid "Filter results by project"
msgstr ""
+msgid "Filter results..."
+msgstr ""
+
msgid "Filter your projects by name"
msgstr ""
@@ -9373,11 +9620,11 @@ msgstr ""
msgid "GeoNodes|Checksummed"
msgstr "Vérifié par somme de contrôle"
-msgid "GeoNodes|Container repositories"
+msgid "GeoNodes|Consult Geo troubleshooting information"
msgstr ""
-msgid "GeoNodes|Data is out of date from %{timeago}"
-msgstr "Données périmées à partir de %{timeago}"
+msgid "GeoNodes|Container repositories"
+msgstr ""
msgid "GeoNodes|Data replication lag"
msgstr "Latence de la réplication des données"
@@ -9418,6 +9665,9 @@ msgstr "Dernier identifiant d’événement traité par le curseur"
msgid "GeoNodes|Last event ID seen from primary"
msgstr "Dernier identifiant d’événement vu par le nœud primaire"
+msgid "GeoNodes|Learn more about Geo node statuses"
+msgstr ""
+
msgid "GeoNodes|Loading nodes"
msgstr "Chargement des nœuds"
@@ -9433,6 +9683,9 @@ msgstr ""
msgid "GeoNodes|Node was successfully removed."
msgstr "Le nœud a été supprimé avec succès."
+msgid "GeoNodes|Node's status was updated %{timeAgo}."
+msgstr ""
+
msgid "GeoNodes|Not checksummed"
msgstr "Non vérifié par somme de contrôle"
@@ -9463,8 +9716,11 @@ msgstr "Progression du calcul de la somme de contrôle du dépôt"
msgid "GeoNodes|Repository verification progress"
msgstr "Progression de la vérification du dépôt"
-msgid "GeoNodes|Selective"
-msgstr "Sélectif"
+msgid "GeoNodes|Selective (%{syncLabel})"
+msgstr ""
+
+msgid "GeoNodes|Selective synchronization"
+msgstr ""
msgid "GeoNodes|Something went wrong while changing node status"
msgstr "Une erreur s’est produite lors du changement de statut du nœud"
@@ -9490,6 +9746,9 @@ msgstr "Emplacements non utilisés"
msgid "GeoNodes|Unverified"
msgstr "Non vérifié"
+msgid "GeoNodes|Updated %{timeAgo}"
+msgstr ""
+
msgid "GeoNodes|Used slots"
msgstr "Emplacements utilisés"
@@ -9529,17 +9788,17 @@ msgstr ""
msgid "Geo|All"
msgstr "Tous"
+msgid "Geo|All %{replicable_type}"
+msgstr ""
+
msgid "Geo|All projects"
msgstr "Tous les projets"
-msgid "Geo|All projects are being scheduled for re-sync"
-msgstr "Tous les projets ont été planifiés pour resynchronisation"
-
-msgid "Geo|All projects are being scheduled for re-verify"
+msgid "Geo|All projects are being scheduled for resync"
msgstr ""
-msgid "Geo|Batch operations"
-msgstr "Opérations par lot"
+msgid "Geo|All projects are being scheduled for reverify"
+msgstr ""
msgid "Geo|Could not remove tracking entry for an existing project."
msgstr "Impossible de supprimer l’entrée de suivi d’un projet existant."
@@ -9550,9 +9809,15 @@ msgstr ""
msgid "Geo|Failed"
msgstr "En échec"
+msgid "Geo|Filter by status"
+msgstr ""
+
msgid "Geo|Geo Status"
msgstr "État de Geo"
+msgid "Geo|In progress"
+msgstr ""
+
msgid "Geo|In sync"
msgstr "Synchronisé"
@@ -9577,9 +9842,6 @@ msgstr "Prochaine synchro programmée à"
msgid "Geo|Not synced yet"
msgstr "Pas encore synchronisé"
-msgid "Geo|Pending"
-msgstr "En attente"
-
msgid "Geo|Pending synchronization"
msgstr "En attente de synchronisation"
@@ -9616,8 +9878,8 @@ msgstr ""
msgid "Geo|Resync"
msgstr "Reâ€synchroniser"
-msgid "Geo|Resync all projects"
-msgstr "Resynchroniser tous les projets"
+msgid "Geo|Resync all"
+msgstr ""
msgid "Geo|Retry count"
msgstr "Nombre de tentatives"
@@ -9625,7 +9887,7 @@ msgstr "Nombre de tentatives"
msgid "Geo|Reverify"
msgstr ""
-msgid "Geo|Reverify all projects"
+msgid "Geo|Reverify all"
msgstr ""
msgid "Geo|Status"
@@ -9967,9 +10229,6 @@ msgstr ""
msgid "Go to file"
msgstr ""
-msgid "Go to file (MRs only)"
-msgstr ""
-
msgid "Go to file permalink (while viewing a file)"
msgstr ""
@@ -10414,6 +10673,9 @@ msgstr ""
msgid "GroupSettings|Disable group mentions"
msgstr ""
+msgid "GroupSettings|Export group"
+msgstr ""
+
msgid "GroupSettings|If the parent group's visibility is lower than the group current visibility, visibility levels for subgroups and projects will be changed to match the new parent group's visibility."
msgstr ""
@@ -10492,6 +10754,9 @@ msgstr "Groupes"
msgid "Groups (%{count})"
msgstr ""
+msgid "Groups (%{groups})"
+msgstr ""
+
msgid "Groups can also be nested by creating %{subgroup_docs_link_start}subgroups%{subgroup_docs_link_end}."
msgstr "Les groupes peuvent également être imbriqués en créant des %{subgroup_docs_link_start}sousâ€groupes%{subgroup_docs_link_end}."
@@ -10757,15 +11022,12 @@ msgstr "Identifiant"
msgid "ID:"
msgstr ""
-msgid "IDE|Allow live previews of JavaScript projects in the Web IDE using CodeSandbox client side evaluation."
-msgstr "Autoriser les aperçus en direct des projets JavaScript dans l’EDI Web à l’aide de l’évaluation côté client CodeSandbox."
+msgid "IDE|Allow live previews of JavaScript projects in the Web IDE using CodeSandbox Live Preview."
+msgstr ""
msgid "IDE|Back"
msgstr "Retour"
-msgid "IDE|Client side evaluation"
-msgstr "Évaluation côté client"
-
msgid "IDE|Commit"
msgstr "Valider"
@@ -10829,6 +11091,9 @@ msgstr ""
msgid "If any job surpasses this timeout threshold, it will be marked as failed. Human readable time input language is accepted like \"1 hour\". Values without specification represent seconds."
msgstr ""
+msgid "If blank, set allowable lifetime to %{instance_level_policy_in_words}, as defined by the instance admin. Once set, existing tokens for users in this group may be revoked."
+msgstr ""
+
msgid "If checked, group owners can manage LDAP group links and LDAP member overrides"
msgstr ""
@@ -10868,6 +11133,9 @@ msgstr ""
msgid "If you lose your recovery codes you can generate new ones, invalidating all previous codes."
msgstr ""
+msgid "If you reach 100%% storage capacity, you will not be able to: %{base_message}"
+msgstr ""
+
msgid "If your HTTP repository is not publicly accessible, add your credentials."
msgstr ""
@@ -10979,6 +11247,9 @@ msgstr "Importer des dépôts à partir de GitHub"
msgid "Import repository"
msgstr "Importer un dépôt"
+msgid "Import started by: %{importInitiator}"
+msgstr ""
+
msgid "Import tasks"
msgstr ""
@@ -11045,6 +11316,9 @@ msgstr ""
msgid "In order to tailor your experience with GitLab we<br>would like to know a bit more about you."
msgstr ""
+msgid "In progress"
+msgstr ""
+
msgid "In the next step, you'll be able to select the projects you want to import."
msgstr "À la prochaine étape, vous pourrez sélectionner les projets que vous souhaitez importer."
@@ -11392,15 +11666,15 @@ msgstr ""
msgid "Issues"
msgstr "Tickets"
-msgid "Issues / Merge Requests"
-msgstr ""
-
msgid "Issues Analytics"
msgstr ""
msgid "Issues Rate Limits"
msgstr ""
+msgid "Issues and Merge Requests"
+msgstr ""
+
msgid "Issues can be bugs, tasks or ideas to be discussed. Also, issues are searchable and filterable."
msgstr "Les tickets peuvent être des bogues, des tâches ou des sujets de discussion. De plus, les tickets sont consultables et filtrables."
@@ -11422,11 +11696,11 @@ msgstr "Une fois que vous aurez commencé à créer des tickets d’incident en
msgid "IssuesAnalytics|Avg/Month:"
msgstr ""
-msgid "IssuesAnalytics|Issues created"
+msgid "IssuesAnalytics|Issues opened"
msgstr ""
-msgid "IssuesAnalytics|Issues created per month"
-msgstr "Tickets créés par mois"
+msgid "IssuesAnalytics|Issues opened per month"
+msgstr ""
msgid "IssuesAnalytics|Last 12 months"
msgstr "Les 12 derniers mois"
@@ -11479,6 +11753,9 @@ msgstr ""
msgid "Jira integration not configured."
msgstr ""
+msgid "Jira project: %{importProject}"
+msgstr ""
+
msgid "JiraService|Events for %{noteable_model_name} are disabled."
msgstr ""
@@ -11868,6 +12145,9 @@ msgstr ""
msgid "Lead"
msgstr ""
+msgid "Lead Time"
+msgstr ""
+
msgid "Learn GitLab"
msgstr ""
@@ -11880,6 +12160,9 @@ msgstr ""
msgid "Learn how to %{no_packages_link_start}publish and share your packages%{no_packages_link_end} with GitLab."
msgstr ""
+msgid "Learn how to enable synchronization"
+msgstr ""
+
msgid "Learn more"
msgstr "En savoir plus"
@@ -12211,9 +12494,6 @@ msgstr "Lister vos dépôts BitBucket Server"
msgid "Live preview"
msgstr "Prévisualisation"
-msgid "Load more vulnerabilities"
-msgstr ""
-
msgid "Loading"
msgstr ""
@@ -12364,9 +12644,6 @@ msgstr ""
msgid "Manage"
msgstr ""
-msgid "Manage Git repositories with fine-grained access controls that keep your code secure. Perform code reviews and enhance collaboration with merge requests. Each project can also have an issue tracker and a wiki."
-msgstr "Gérez les dépôts Git avec des contrôles d’accès très précis permettant de sécuriser votre code. Effectuez des revues de code et renforcez la collaboration avec les demandes de fusion Git. Chaque projet peut également avoir un système de tickets de suivi et un wiki."
-
msgid "Manage Web IDE features"
msgstr "Gérer les fonctionnalités de l’EDI Web"
@@ -12397,6 +12674,9 @@ msgstr ""
msgid "Manage your license"
msgstr ""
+msgid "Managed Account"
+msgstr ""
+
msgid "Manifest"
msgstr "Manifeste"
@@ -12661,6 +12941,9 @@ msgstr "Demandes de fusion créées"
msgid "Merge Requests in Review"
msgstr ""
+msgid "Merge automatically (%{strategy})"
+msgstr ""
+
msgid "Merge commit message"
msgstr ""
@@ -12814,6 +13097,12 @@ msgstr "Fusionnée"
msgid "Merged branches are being deleted. This can take some time depending on the number of branches. Please refresh the page to see changes."
msgstr ""
+msgid "Merged this merge request."
+msgstr ""
+
+msgid "Merges this merge request immediately."
+msgstr ""
+
msgid "Merges this merge request when the pipeline succeeds."
msgstr ""
@@ -12921,12 +13210,15 @@ msgid_plural "Metrics|Edit metrics"
msgstr[0] ""
msgstr[1] ""
-msgid "Metrics|Environment"
-msgstr "Environnement"
+msgid "Metrics|Expand panel"
+msgstr ""
msgid "Metrics|For grouping similar metrics"
msgstr "Pour regrouper des métriques similaires"
+msgid "Metrics|Go back (Esc)"
+msgstr ""
+
msgid "Metrics|Invalid time range, please verify."
msgstr ""
@@ -12957,9 +13249,6 @@ msgstr "Documentation des requêtes Prometheus"
msgid "Metrics|Refresh dashboard"
msgstr ""
-msgid "Metrics|Show last"
-msgstr ""
-
msgid "Metrics|There was an error creating the dashboard."
msgstr ""
@@ -13038,6 +13327,9 @@ msgstr ""
msgid "Microsoft Azure"
msgstr ""
+msgid "Middleman project with Static Site Editor support"
+msgstr ""
+
msgid "Migrated %{success_count}/%{total_count} files."
msgstr ""
@@ -13483,10 +13775,10 @@ msgstr ""
msgid "Next"
msgstr ""
-msgid "Next file in diff (MRs only)"
+msgid "Next file in diff"
msgstr ""
-msgid "Next unresolved discussion (MRs only)"
+msgid "Next unresolved discussion"
msgstr ""
msgid "Nickname"
@@ -13546,6 +13838,9 @@ msgstr "Aucun changement"
msgid "No changes between %{ref_start}%{source_branch}%{ref_end} and %{ref_start}%{target_branch}%{ref_end}"
msgstr ""
+msgid "No child epics match applied filters"
+msgstr ""
+
msgid "No connection could be made to a Gitaly Server, please check your logs!"
msgstr "Aucune connexion n’a pu être établie avec un serveur Gitaly, veuillez vérifier votre journal !"
@@ -13684,15 +13979,6 @@ msgstr ""
msgid "No thanks, don't show this again"
msgstr ""
-msgid "No vulnerabilities found for this group"
-msgstr ""
-
-msgid "No vulnerabilities found for this pipeline"
-msgstr ""
-
-msgid "No vulnerabilities found for this project"
-msgstr ""
-
msgid "No vulnerabilities present"
msgstr ""
@@ -13816,6 +14102,9 @@ msgstr ""
msgid "Nothing to preview."
msgstr ""
+msgid "Nothing to synchronize"
+msgstr ""
+
msgid "Notification events"
msgstr "Événement de notifications"
@@ -13987,6 +14276,9 @@ msgstr ""
msgid "Once removed, the fork relationship cannot be restored and you will no longer be able to send merge requests to the source."
msgstr ""
+msgid "Once the exported file is ready you can download it from this page."
+msgstr ""
+
msgid "Once the exported file is ready, you will receive a notification email with a download link, or you can download it from this page."
msgstr ""
@@ -14088,9 +14380,6 @@ msgstr ""
msgid "Open sidebar"
msgstr "Ouvrir la barre latérale"
-msgid "Open source software to collaborate on code"
-msgstr "Logiciel libre permettant de collaborer sur du code source"
-
msgid "Open: %{openIssuesCount}"
msgstr ""
@@ -14193,6 +14482,9 @@ msgstr ""
msgid "OutdatedBrowser|You can provide feedback %{feedback_link_start}on this issue%{feedback_link_end} or via your usual support channels."
msgstr ""
+msgid "Overridden"
+msgstr ""
+
msgid "Overview"
msgstr "Vue d’ensemble"
@@ -14244,6 +14536,9 @@ msgstr ""
msgid "PackageRegistry|Conan Command"
msgstr ""
+msgid "PackageRegistry|Copy .pypirc content"
+msgstr ""
+
msgid "PackageRegistry|Copy Conan Command"
msgstr ""
@@ -14265,6 +14560,9 @@ msgstr ""
msgid "PackageRegistry|Copy NuGet Setup Command"
msgstr ""
+msgid "PackageRegistry|Copy Pip command"
+msgstr ""
+
msgid "PackageRegistry|Copy and paste this inside your %{codeStart}pom.xml%{codeEnd} %{codeStart}dependencies%{codeEnd} block."
msgstr ""
@@ -14298,6 +14596,12 @@ msgstr ""
msgid "PackageRegistry|For more information on the NuGet registry, %{linkStart}see the documentation%{linkEnd}."
msgstr ""
+msgid "PackageRegistry|For more information on the PyPi registry, %{linkStart}see the documentation%{linkEnd}."
+msgstr ""
+
+msgid "PackageRegistry|If you haven't already done so, you will need to add the below to your %{codeStart}.pypirc%{codeEnd} file."
+msgstr ""
+
msgid "PackageRegistry|If you haven't already done so, you will need to add the below to your %{codeStart}pom.xml%{codeEnd} file."
msgstr ""
@@ -14307,6 +14611,9 @@ msgstr ""
msgid "PackageRegistry|Learn how to %{noPackagesLinkStart}publish and share your packages%{noPackagesLinkEnd} with GitLab."
msgstr ""
+msgid "PackageRegistry|Manually Published"
+msgstr ""
+
msgid "PackageRegistry|Maven"
msgstr ""
@@ -14325,12 +14632,18 @@ msgstr ""
msgid "PackageRegistry|NuGet Command"
msgstr ""
+msgid "PackageRegistry|Pip Command"
+msgstr ""
+
msgid "PackageRegistry|Pipeline %{linkStart}%{linkEnd} triggered %{timestamp} by %{author}"
msgstr ""
msgid "PackageRegistry|Published to the repository at %{timestamp}"
msgstr ""
+msgid "PackageRegistry|PyPi"
+msgstr ""
+
msgid "PackageRegistry|Registry Setup"
msgstr ""
@@ -14367,6 +14680,9 @@ msgstr ""
msgid "PackageRegistry|npm"
msgstr ""
+msgid "PackageRegistry|published by %{author}"
+msgstr ""
+
msgid "PackageRegistry|yarn"
msgstr ""
@@ -14382,6 +14698,9 @@ msgstr ""
msgid "PackageType|NuGet"
msgstr ""
+msgid "PackageType|PyPi"
+msgstr ""
+
msgid "Packages"
msgstr "Paquets"
@@ -14475,6 +14794,9 @@ msgstr ""
msgid "Past due"
msgstr ""
+msgid "Paste a machine public key here. Read more about how to generate it"
+msgstr ""
+
msgid "Paste a machine public key here. Read more about how to generate it %{link_start}here%{link_end}"
msgstr ""
@@ -14490,9 +14812,6 @@ msgstr ""
msgid "Path"
msgstr ""
-msgid "Path, transfer, remove"
-msgstr "Chemin d’accès, transfert et suppression"
-
msgid "Path:"
msgstr "Chemin d’accès :"
@@ -14523,8 +14842,8 @@ msgstr ""
msgid "Percentage"
msgstr ""
-msgid "Perform advanced options such as changing path, transferring, or removing the group."
-msgstr "Effectuer des actions avancées telles que la modification du chemin d’accès, le transfert ou la suppression du groupe."
+msgid "Perform advanced options such as changing path, transferring, exporting, or removing the group."
+msgstr ""
msgid "Perform common operations on GitLab project"
msgstr ""
@@ -14604,9 +14923,6 @@ msgstr "Planifications de pipelines"
msgid "Pipeline minutes quota"
msgstr ""
-msgid "Pipeline quota"
-msgstr "Quota du pipeline"
-
msgid "Pipeline subscriptions"
msgstr ""
@@ -14616,6 +14932,9 @@ msgstr "Déclencheurs de pipeline"
msgid "Pipeline: %{status}"
msgstr ""
+msgid "PipelineCharts|CI / CD Analytics"
+msgstr ""
+
msgid "PipelineCharts|Failed:"
msgstr "Échecs :"
@@ -14694,15 +15013,6 @@ msgstr ""
msgid "Pipelines settings for '%{project_name}' were successfully updated."
msgstr ""
-msgid "Pipelines| to purchase more minutes."
-msgstr ""
-
-msgid "Pipelines|%{namespace_name} has exceeded its pipeline minutes quota."
-msgstr ""
-
-msgid "Pipelines|%{namespace_name} has less than %{notification_level}%% of CI minutes available."
-msgstr ""
-
msgid "Pipelines|API"
msgstr ""
@@ -14724,12 +15034,15 @@ msgstr "L’intégration continue peut aider à détecter les bogues en exécuta
msgid "Pipelines|Get started with Pipelines"
msgstr "Premiers pas avec les pipelines"
-msgid "Pipelines|Loading Pipelines"
-msgstr "Chargement des pipelines"
+msgid "Pipelines|Group %{namespace_name} has %{percentage}%% or less Shared Runner Pipeline minutes remaining. Once it runs out, no new jobs or pipelines in its projects will run."
+msgstr ""
-msgid "Pipelines|Pipelines will not run anymore on shared Runners."
+msgid "Pipelines|Group %{namespace_name} has exceeded its pipeline minutes quota. Unless you buy additional pipeline minutes, no new jobs or pipelines in its projects will run."
msgstr ""
+msgid "Pipelines|Loading Pipelines"
+msgstr "Chargement des pipelines"
+
msgid "Pipelines|Project cache successfully reset."
msgstr "Réinitialisation du cache de projet réussie."
@@ -14958,6 +15271,9 @@ msgstr ""
msgid "Please select"
msgstr ""
+msgid "Please select a Jira project"
+msgstr ""
+
msgid "Please select a country"
msgstr ""
@@ -15003,6 +15319,9 @@ msgstr "Veuillez patienter pendant la connexion à votre dépôt. Actualisez à
msgid "Please wait while we import the repository for you. Refresh at will."
msgstr "Veuillez patienter pendant l’importation de votre dépôt. Actualisez à votre guise."
+msgid "Plugins directory is deprecated and will be removed in 14.0. Please move this file into /file_hooks directory."
+msgstr ""
+
msgid "Pod does not exist"
msgstr ""
@@ -15120,7 +15439,7 @@ msgstr ""
msgid "Prevent users from changing their profile name"
msgstr ""
-msgid "Prevent users from modifing merge request approvers list"
+msgid "Prevent users from modifying merge request approvers list"
msgstr ""
msgid "Prevent users from performing write operations on GitLab while performing maintenance."
@@ -15141,10 +15460,10 @@ msgstr "Aperçu de la charge utile"
msgid "Previous Artifacts"
msgstr ""
-msgid "Previous file in diff (MRs only)"
+msgid "Previous file in diff"
msgstr ""
-msgid "Previous unresolved discussion (MRs only)"
+msgid "Previous unresolved discussion"
msgstr ""
msgid "Primary"
@@ -15177,6 +15496,9 @@ msgstr ""
msgid "Private profile"
msgstr ""
+msgid "Private projects Minutes cost factor"
+msgstr ""
+
msgid "Private projects can be created in your personal namespace with:"
msgstr "Des projets privés peuvent être créés dans votre espace de noms personnel avec :"
@@ -16005,6 +16327,9 @@ msgstr ""
msgid "ProjectSettings|With GitLab Pages you can host your static websites on GitLab"
msgstr ""
+msgid "ProjectSettings|With Metrics Dashboard you can visualize this project performance metrics"
+msgstr ""
+
msgid "ProjectTemplates|.NET Core"
msgstr ""
@@ -16068,6 +16393,9 @@ msgstr ""
msgid "ProjectTemplates|Spring"
msgstr ""
+msgid "ProjectTemplates|Static Site Editor/Middleman"
+msgstr ""
+
msgid "ProjectTemplates|iOS (Swift)"
msgstr ""
@@ -16287,15 +16615,15 @@ msgstr "URL de base de l’API Prometheus, telle que http://prometheus.example.c
msgid "PrometheusService|Prometheus is being automatically managed on your clusters"
msgstr "Prometheus est géré automatiquement sur vos grappes de serveurs"
+msgid "PrometheusService|Select the Active checkbox to override the Auto Configuration with custom settings. If unchecked, Auto Configuration settings are used."
+msgstr ""
+
msgid "PrometheusService|These metrics will only be monitored after your first deployment to an environment"
msgstr "Ces métriques ne seront supervisées qu’après votre premier déploiement dans un environnement"
msgid "PrometheusService|Time-series monitoring service"
msgstr "Service de supervision de séries temporelles"
-msgid "PrometheusService|To enable manual configuration, uninstall Prometheus from your clusters"
-msgstr "Pour activer la configuration manuelle, désinstallez Prometheus de vos grappes de serveurs"
-
msgid "PrometheusService|To enable the installation of Prometheus on your clusters, deactivate the manual configuration below"
msgstr "Pour activer l’installation de Prometheus sur vos grappes de serveurs, désactivez la configuration manuelle ciâ€dessous"
@@ -16479,6 +16807,9 @@ msgstr "Votre environnement a été déprotégé"
msgid "Protip:"
msgstr "Astuce :"
+msgid "Protocol"
+msgstr ""
+
msgid "Provider"
msgstr "Fournisseur"
@@ -16503,6 +16834,9 @@ msgstr ""
msgid "Public pipelines"
msgstr "Pipelines publics"
+msgid "Public projects Minutes cost factor"
+msgstr ""
+
msgid "Pull"
msgstr "Récupérer"
@@ -16703,6 +17037,9 @@ msgid_plural "Refreshing in %d seconds to show the updated status..."
msgstr[0] "L’affichage sera réactualisé dans une seconde avec le statut mis à jour..."
msgstr[1] "L’affichage sera réactualisé dans %d secondes avec le statut mis à jour…"
+msgid "Regenerate export"
+msgstr ""
+
msgid "Regenerate instance ID"
msgstr ""
@@ -17080,9 +17417,29 @@ msgstr "Rapports"
msgid "Reports|%{combinedString} and %{resolvedString}"
msgstr ""
+msgid "Reports|Accessibility scanning detected %d issue for the source branch only"
+msgid_plural "Reports|Accessibility scanning detected %d issues for the source branch only"
+msgstr[0] ""
+msgstr[1] ""
+
+msgid "Reports|Accessibility scanning detected no issues for the source branch only"
+msgstr ""
+
+msgid "Reports|Accessibility scanning failed loading results"
+msgstr ""
+
+msgid "Reports|Accessibility scanning results are being parsed"
+msgstr ""
+
msgid "Reports|Actions"
msgstr "Actions"
+msgid "Reports|An error occured while loading report"
+msgstr ""
+
+msgid "Reports|An error occurred while loading %{name} results"
+msgstr ""
+
msgid "Reports|Class"
msgstr "Classe"
@@ -17188,7 +17545,7 @@ msgstr ""
msgid "Requested %{time_ago}"
msgstr ""
-msgid "Requested design version does not exist"
+msgid "Requested design version does not exist."
msgstr ""
msgid "Requested states are invalid"
@@ -17372,6 +17729,9 @@ msgstr ""
msgid "Resync"
msgstr ""
+msgid "Resync all"
+msgstr ""
+
msgid "Resync all %{replicableType}"
msgstr ""
@@ -17440,6 +17800,9 @@ msgstr ""
msgid "Revoked personal access token %{personal_access_token_name}!"
msgstr ""
+msgid "Revoked project access token %{project_access_token_name}!"
+msgstr ""
+
msgid "RightSidebar|adding a"
msgstr ""
@@ -17488,6 +17851,9 @@ msgstr "L’exécuteur exécute des tâches de projets attribués"
msgid "Runner token"
msgstr "Jeton de l’exécuteur"
+msgid "Runner tokens"
+msgstr ""
+
msgid "Runner was not updated."
msgstr ""
@@ -17542,6 +17908,9 @@ msgstr "Authentification unique SAML"
msgid "SAML SSO for %{group_name}"
msgstr "Authentification unique SAML pour %{group_name}"
+msgid "SAML discovery tokens"
+msgstr ""
+
msgid "SAML for %{group_name}"
msgstr ""
@@ -17626,12 +17995,18 @@ msgstr "Planifier un nouveau pipeline"
msgid "Scheduled"
msgstr "Planifié"
+msgid "Scheduled to merge this merge request (%{strategy})."
+msgstr ""
+
msgid "Scheduled to merge this merge request when the pipeline succeeds."
msgstr ""
msgid "Schedules"
msgstr "Planifications"
+msgid "Schedules to merge this merge request (%{strategy})."
+msgstr ""
+
msgid "Scheduling"
msgstr ""
@@ -17689,6 +18064,9 @@ msgstr "Rechercher des branches"
msgid "Search branches and tags"
msgstr "Rechercher dans les branches et les étiquettes"
+msgid "Search by author"
+msgstr ""
+
msgid "Search files"
msgstr "Rechercher des fichiers"
@@ -17834,11 +18212,6 @@ msgid_plural "SearchResults|snippets"
msgstr[0] ""
msgstr[1] ""
-msgid "SearchResults|snippet result"
-msgid_plural "SearchResults|snippet results"
-msgstr[0] ""
-msgstr[1] ""
-
msgid "SearchResults|user"
msgid_plural "SearchResults|users"
msgstr[0] ""
@@ -17849,6 +18222,9 @@ msgid_plural "SearchResults|wiki results"
msgstr[0] ""
msgstr[1] ""
+msgid "Searching by both author and message is currently not supported."
+msgstr ""
+
msgid "Seat Link"
msgstr ""
@@ -17876,184 +18252,232 @@ msgstr ""
msgid "Security Dashboard"
msgstr "Tableau de bord de sécurité"
-msgid "Security Dashboard|Error fetching the vulnerability counts. Please check your network connection and try again."
-msgstr "Erreur lors de la récupération du nombre de vulnérabilités. Veuillez vérifier votre connexion réseau puis réessayer."
+msgid "Security configuration help link"
+msgstr ""
+
+msgid "Security dashboard"
+msgstr ""
+
+msgid "Security report is out of date. Please update your branch with the latest changes from the target branch (%{targetBranchName})"
+msgstr ""
+
+msgid "Security report is out of date. Run %{newPipelineLinkStart}a new pipeline%{newPipelineLinkEnd} for the target branch (%{targetBranchName})"
+msgstr ""
+
+msgid "SecurityConfiguration|Configured"
+msgstr ""
+
+msgid "SecurityConfiguration|Feature"
+msgstr ""
-msgid "Security Dashboard|Error fetching the vulnerability list. Please check your network connection and try again."
-msgstr "Erreur lors de la récupération de la liste des vulnérabilités. Veuillez vérifier votre connexion réseau puis réessayer."
+msgid "SecurityConfiguration|Feature documentation for %{featureName}"
+msgstr ""
-msgid "Security Dashboard|Issue Created"
-msgstr "Ticket créé"
+msgid "SecurityConfiguration|Not yet configured"
+msgstr ""
-msgid "Security Reports|Comment added to '%{vulnerabilityName}'"
+msgid "SecurityConfiguration|Secure features"
msgstr ""
-msgid "Security Reports|Comment deleted on '%{vulnerabilityName}'"
+msgid "SecurityConfiguration|Status"
msgstr ""
-msgid "Security Reports|Comment edited on '%{vulnerabilityName}'"
+msgid "SecurityReports|%{firstProject} and %{secondProject}"
msgstr ""
-msgid "Security Reports|Create issue"
-msgstr "Créer un ticket"
+msgid "SecurityReports|%{firstProject}, %{secondProject}, and %{rest}"
+msgstr ""
-msgid "Security Reports|Dismiss vulnerability"
-msgstr "Rejeter la vulnérabilité"
+msgid "SecurityReports|Add a project to your dashboard"
+msgstr ""
-msgid "Security Reports|Dismissed '%{vulnerabilityName}'"
+msgid "SecurityReports|Add or remove projects from your dashboard"
msgstr ""
-msgid "Security Reports|Dismissed '%{vulnerabilityName}'. Turn off the hide dismissed toggle to view."
+msgid "SecurityReports|Add projects"
msgstr ""
-msgid "Security Reports|Either you don't have permission to view this dashboard or the dashboard has not been setup. Please check your permission settings with your administrator or check your dashboard configurations to proceed."
+msgid "SecurityReports|Comment added to '%{vulnerabilityName}'"
msgstr ""
-msgid "Security Reports|Learn more about setting up your dashboard"
+msgid "SecurityReports|Comment deleted on '%{vulnerabilityName}'"
msgstr ""
-msgid "Security Reports|More info"
-msgstr "Plus d’informations"
+msgid "SecurityReports|Comment edited on '%{vulnerabilityName}'"
+msgstr ""
-msgid "Security Reports|Oops, something doesn't seem right."
+msgid "SecurityReports|Create issue"
msgstr ""
-msgid "Security Reports|Security reports can only be accessed by authorized users."
+msgid "SecurityReports|Dismiss Selected"
msgstr ""
-msgid "Security Reports|There was an error adding the comment."
+msgid "SecurityReports|Dismiss vulnerability"
msgstr ""
-msgid "Security Reports|There was an error creating the issue."
-msgstr "Une erreur est survenue lors de la création du ticket."
+msgid "SecurityReports|Dismissed '%{vulnerabilityName}'"
+msgstr ""
-msgid "Security Reports|There was an error creating the merge request."
+msgid "SecurityReports|Dismissed '%{vulnerabilityName}'. Turn off the hide dismissed toggle to view."
msgstr ""
-msgid "Security Reports|There was an error deleting the comment."
+msgid "SecurityReports|Each vulnerability now has a unique page that can be directly linked to, shared, referenced, and tracked as the single source of truth. Vulnerability occurrences also persist across scanner runs, which improves tracking and visibility and reduces duplicates between scans."
msgstr ""
-msgid "Security Reports|There was an error dismissing the vulnerabilities."
+msgid "SecurityReports|Edit dashboard"
msgstr ""
-msgid "Security Reports|There was an error dismissing the vulnerability."
-msgstr "Une erreur est survenue lors du rejet de la vulnérabilité."
+msgid "SecurityReports|Either you don't have permission to view this dashboard or the dashboard has not been setup. Please check your permission settings with your administrator or check your dashboard configurations to proceed."
+msgstr ""
-msgid "Security Reports|There was an error reverting the dismissal."
-msgstr "Une erreur est survenue lors de l’annulation du rejet."
+msgid "SecurityReports|Error fetching the vulnerability counts. Please check your network connection and try again."
+msgstr ""
-msgid "Security Reports|There was an error reverting this dismissal."
-msgstr "Une erreur est survenue lors de l’annulation de ce rejet."
+msgid "SecurityReports|Error fetching the vulnerability list. Please check your network connection and try again."
+msgstr ""
-msgid "Security Reports|Undo dismiss"
+msgid "SecurityReports|False positive"
msgstr ""
-msgid "Security Reports|You do not have sufficient permissions to access this report"
+msgid "SecurityReports|Hide dismissed"
msgstr ""
-msgid "Security Reports|You must sign in as an authorized user to see this report"
+msgid "SecurityReports|Introducing standalone vulnerabilities"
msgstr ""
-msgid "Security configuration help link"
+msgid "SecurityReports|Issue Created"
msgstr ""
-msgid "Security dashboard"
+msgid "SecurityReports|Learn More"
msgstr ""
-msgid "Security report is out of date. Please update your branch with the latest changes from the target branch (%{targetBranchName})"
+msgid "SecurityReports|Learn more about setting up your dashboard"
msgstr ""
-msgid "Security report is out of date. Run %{newPipelineLinkStart}a new pipeline%{newPipelineLinkEnd} for the target branch (%{targetBranchName})"
+msgid "SecurityReports|Load more vulnerabilities"
msgstr ""
-msgid "SecurityConfiguration|Configured"
+msgid "SecurityReports|Monitor vulnerabilities in your code"
msgstr ""
-msgid "SecurityConfiguration|Feature"
+msgid "SecurityReports|More info"
msgstr ""
-msgid "SecurityConfiguration|Feature documentation for %{featureName}"
+msgid "SecurityReports|More information"
msgstr ""
-msgid "SecurityConfiguration|Not yet configured"
+msgid "SecurityReports|No vulnerabilities found for dashboard"
msgstr ""
-msgid "SecurityConfiguration|Secure features"
+msgid "SecurityReports|No vulnerabilities found for this group"
msgstr ""
-msgid "SecurityConfiguration|Status"
+msgid "SecurityReports|No vulnerabilities found for this pipeline"
+msgstr ""
+
+msgid "SecurityReports|No vulnerabilities found for this project"
+msgstr ""
+
+msgid "SecurityReports|Oops, something doesn't seem right."
+msgstr ""
+
+msgid "SecurityReports|Pipeline %{pipelineLink} triggered %{timeago} by %{user}"
+msgstr ""
+
+msgid "SecurityReports|Project"
msgstr ""
-msgid "SecurityDashboard|%{firstProject} and %{secondProject}"
+msgid "SecurityReports|Projects added"
msgstr ""
-msgid "SecurityDashboard|%{firstProject}, %{secondProject}, and %{rest}"
+msgid "SecurityReports|Remove project from dashboard"
msgstr ""
-msgid "SecurityDashboard|Add a project to your dashboard"
+msgid "SecurityReports|Report type"
msgstr ""
-msgid "SecurityDashboard|Add or remove projects from your dashboard"
+msgid "SecurityReports|Return to dashboard"
msgstr ""
-msgid "SecurityDashboard|Add projects"
+msgid "SecurityReports|Security Dashboard"
msgstr ""
-msgid "SecurityDashboard|Edit dashboard"
+msgid "SecurityReports|Security reports can only be accessed by authorized users."
msgstr ""
-msgid "SecurityDashboard|Hide dismissed"
+msgid "SecurityReports|Select a project to add by using the project search field above."
msgstr ""
-msgid "SecurityDashboard|Monitor vulnerabilities in your code"
-msgstr "Surveiller les vulnérabilités dans votre code"
+msgid "SecurityReports|Select a reason"
+msgstr ""
+
+msgid "SecurityReports|Severity"
+msgstr ""
+
+msgid "SecurityReports|Status"
+msgstr ""
-msgid "SecurityDashboard|More information"
+msgid "SecurityReports|The security dashboard displays the latest security findings for projects you wish to monitor. Select \"Edit dashboard\" to add and remove projects."
msgstr ""
-msgid "SecurityDashboard|No vulnerabilities found for dashboard"
+msgid "SecurityReports|The security dashboard displays the latest security report. Use it to find and fix vulnerabilities."
msgstr ""
-msgid "SecurityDashboard|Pipeline %{pipelineLink} triggered %{timeago} by %{user}"
+msgid "SecurityReports|There was an error adding the comment."
msgstr ""
-msgid "SecurityDashboard|Project"
+msgid "SecurityReports|There was an error creating the issue."
msgstr ""
-msgid "SecurityDashboard|Projects added"
+msgid "SecurityReports|There was an error creating the merge request."
msgstr ""
-msgid "SecurityDashboard|Remove project from dashboard"
+msgid "SecurityReports|There was an error deleting the comment."
msgstr ""
-msgid "SecurityDashboard|Report type"
+msgid "SecurityReports|There was an error dismissing the vulnerabilities."
msgstr ""
-msgid "SecurityDashboard|Return to dashboard"
+msgid "SecurityReports|There was an error dismissing the vulnerability."
msgstr ""
-msgid "SecurityDashboard|Security Dashboard"
+msgid "SecurityReports|There was an error reverting the dismissal."
msgstr ""
-msgid "SecurityDashboard|Select a project to add by using the project search field above."
+msgid "SecurityReports|There was an error reverting this dismissal."
msgstr ""
-msgid "SecurityDashboard|Severity"
+msgid "SecurityReports|There was an error while generating the report."
msgstr ""
-msgid "SecurityDashboard|Status"
+msgid "SecurityReports|Unable to add %{invalidProjects}"
msgstr ""
-msgid "SecurityDashboard|The security dashboard displays the latest security findings for projects you wish to monitor. Select \"Edit dashboard\" to add and remove projects."
+msgid "SecurityReports|Undo dismiss"
msgstr ""
-msgid "SecurityDashboard|The security dashboard displays the latest security report. Use it to find and fix vulnerabilities."
+msgid "SecurityReports|While it's rare to have no vulnerabilities for your group, it can happen. In any event, we ask that you double check your settings to make sure you've set up your dashboard correctly."
msgstr ""
-msgid "SecurityDashboard|There was an error while generating the report."
+msgid "SecurityReports|While it's rare to have no vulnerabilities for your pipeline, it can happen. In any event, we ask that you double check your settings to make sure all security scanning jobs have passed successfully."
msgstr ""
-msgid "SecurityDashboard|Unable to add %{invalidProjects}"
+msgid "SecurityReports|While it's rare to have no vulnerabilities for your project, it can happen. In any event, we ask that you double check your settings to make sure you've set up your dashboard correctly."
+msgstr ""
+
+msgid "SecurityReports|While it's rare to have no vulnerabilities, it can happen. In any event, we ask that you please double check your settings to make sure you've set up your dashboard correctly."
+msgstr ""
+
+msgid "SecurityReports|Won't fix / Accept risk"
+msgstr ""
+
+msgid "SecurityReports|You do not have sufficient permissions to access this report"
+msgstr ""
+
+msgid "SecurityReports|You must sign in as an authorized user to see this report"
+msgstr ""
+
+msgid "SecurityReports|[No reason]"
msgstr ""
msgid "See GitLab's %{password_policy_guidelines}"
@@ -18065,6 +18489,9 @@ msgstr ""
msgid "See the affected projects in the GitLab admin panel"
msgstr ""
+msgid "See what's new at GitLab"
+msgstr ""
+
msgid "Select"
msgstr "Sélectionner"
@@ -18470,9 +18897,6 @@ msgstr "Configuration CI/CD"
msgid "Set up Jira Integration"
msgstr ""
-msgid "Set up Jira Integration illustration"
-msgstr ""
-
msgid "Set up a %{type} Runner automatically"
msgstr ""
@@ -18557,6 +18981,9 @@ msgstr ""
msgid "Severity: %{severity}"
msgstr ""
+msgid "Shards (%{shards})"
+msgstr ""
+
msgid "Shards to synchronize"
msgstr ""
@@ -18871,6 +19298,9 @@ msgstr ""
msgid "Snowplow"
msgstr ""
+msgid "Some child epics may be hidden due to applied filters"
+msgstr ""
+
msgid "Some email servers do not support overriding the email sender name. Enable this option to include the name of the author of the issue, merge request or comment in the email body instead."
msgstr ""
@@ -19012,6 +19442,9 @@ msgstr ""
msgid "Something went wrong while updating your list settings"
msgstr ""
+msgid "Something went wrong with your automatic subscription renewal"
+msgstr ""
+
msgid "Something went wrong, unable to add %{project} to dashboard"
msgstr "Une erreur est survenue, impossible d’ajouter %{project} au tableau de bord"
@@ -19297,6 +19730,9 @@ msgstr ""
msgid "Stage removed"
msgstr ""
+msgid "Standard"
+msgstr ""
+
msgid "Star a label to make it a priority label. Order the prioritized labels to change their relative priority, by dragging."
msgstr "Mettez une étoile sur une étiquette pour en faire une étiquette prioritaire. Ordonnez les étiquettes prioritaires pour changer leurs priorités relatives en les faisant glisser."
@@ -19420,6 +19856,9 @@ msgstr "Énoncez votre message à activer"
msgid "Static Application Security Testing (SAST)"
msgstr ""
+msgid "StaticSiteEditor|An error occurred while submitting your changes."
+msgstr ""
+
msgid "StaticSiteEditor|Branch could not be created."
msgstr ""
@@ -19429,18 +19868,30 @@ msgstr ""
msgid "StaticSiteEditor|Could not create merge request."
msgstr ""
+msgid "StaticSiteEditor|Incompatible file content"
+msgstr ""
+
msgid "StaticSiteEditor|Return to site"
msgstr ""
+msgid "StaticSiteEditor|Static site editor"
+msgstr ""
+
msgid "StaticSiteEditor|Success!"
msgstr ""
msgid "StaticSiteEditor|Summary of changes"
msgstr ""
+msgid "StaticSiteEditor|The Static Site Editor is currently configured to only edit Markdown content on pages generated from Middleman. Visit the documentation to learn more about configuring your site to use the Static Site Editor."
+msgstr ""
+
msgid "StaticSiteEditor|Update %{sourcePath} file"
msgstr ""
+msgid "StaticSiteEditor|View documentation"
+msgstr ""
+
msgid "StaticSiteEditor|View merge request"
msgstr ""
@@ -19498,6 +19949,12 @@ msgstr ""
msgid "StatusPage|Status page"
msgstr ""
+msgid "StatusPage|Status page URL"
+msgstr ""
+
+msgid "StatusPage|Status page frontend documentation"
+msgstr ""
+
msgid "StatusPage|To publish incidents to an external status page, GitLab will store a JSON file in your Amazon S3 account in a location accessible to your external status page service. Make sure to also set up %{docsLink}"
msgstr ""
@@ -19846,6 +20303,12 @@ msgstr "Synchroniser les informations"
msgid "Synced"
msgstr ""
+msgid "Synchronization disabled"
+msgstr ""
+
+msgid "Synchronization of container repositories is disabled."
+msgstr ""
+
msgid "System"
msgstr ""
@@ -19999,6 +20462,9 @@ msgstr ""
msgid "Telephone number"
msgstr ""
+msgid "Telephone number (Optional)"
+msgstr ""
+
msgid "Template"
msgstr "Modèle"
@@ -20305,6 +20771,9 @@ msgstr ""
msgid "The license was successfully uploaded and is now active. You can see the details below."
msgstr ""
+msgid "The license was successfully uploaded and will be active from %{starts_at}. You can see the details below."
+msgstr ""
+
msgid "The maximum file size allowed is %{size}."
msgstr ""
@@ -20626,6 +21095,9 @@ msgstr ""
msgid "There was an error getting the epic participants."
msgstr ""
+msgid "There was an error importing the Jira project."
+msgstr ""
+
msgid "There was an error loading users activity calendar."
msgstr "Une erreur s’est produite lors du chargement du calendrier d’activité des utilisateurs."
@@ -20641,7 +21113,7 @@ msgstr ""
msgid "There was an error resetting user pipeline minutes."
msgstr ""
-msgid "There was an error saving this Geo Node"
+msgid "There was an error saving this Geo Node."
msgstr ""
msgid "There was an error saving your changes."
@@ -20704,6 +21176,9 @@ msgstr ""
msgid "They can be managed using the %{link}."
msgstr "Ils peuvent être gérés en utilisant %{link}."
+msgid "Third Party Advisory Link"
+msgstr ""
+
msgid "Third party offers"
msgstr "Offres tierces"
@@ -20740,6 +21215,9 @@ msgstr "Cette application a été créée par %{link_to_owner}."
msgid "This application will be able to:"
msgstr "Cette application sera en mesure de :"
+msgid "This attachment has been truncated to avoid exceeding the maximum allowed attachment size of 15MB. %{written_count} of %{issues_count} issues have been included. Consider re-exporting with a narrower selection of issues."
+msgstr ""
+
msgid "This block is self-referential"
msgstr ""
@@ -20881,6 +21359,12 @@ msgstr ""
msgid "This issue is confidential"
msgstr "Ce ticket est confidentiel"
+msgid "This issue is currently blocked by the following issues: %{issues}."
+msgstr ""
+
+msgid "This issue is in a child epic of the filtered epic"
+msgstr ""
+
msgid "This issue is locked."
msgstr "Ce ticket est verrouillé."
@@ -21079,7 +21563,10 @@ msgstr "Cet utilisateur sera l’auteur de tous les événements du flux d’act
msgid "This user will be the author of all events in the activity feed that are the result of an update, like new branches being created or new commits being pushed to existing branches. Upon creation or when reassigning you can only assign yourself to be the mirror user."
msgstr "Cet utilisateur sera l’auteur de tous les événements du flux d’activité résultant d’une mise à jour, comme la création de nouvelles branches ou les nouveaux commits poussés vers des branches existantes. Lors de la création ou lors de la réaffectation, vous ne pouvez assigner que vousâ€même comme utilisateur du miroir."
-msgid "This variable can not be masked"
+msgid "This variable can not be masked."
+msgstr ""
+
+msgid "This variable does not match the expected pattern."
msgstr ""
msgid "This will help us personalize your onboarding experience."
@@ -21109,10 +21596,10 @@ msgstr ""
msgid "ThreatMonitoring|Application firewall not detected"
msgstr ""
-msgid "ThreatMonitoring|Container Network Policy"
+msgid "ThreatMonitoring|Container Network Policies are not installed or have been disabled. To view this data, ensure your Network Policies are installed and enabled for your cluster."
msgstr ""
-msgid "ThreatMonitoring|Container NetworkPolicies are not installed or has been disabled. To view this data, ensure you NetworkPolicies are installed and enabled for your cluster."
+msgid "ThreatMonitoring|Container Network Policy"
msgstr ""
msgid "ThreatMonitoring|Container NetworkPolicies not detected"
@@ -21145,7 +21632,7 @@ msgstr ""
msgid "ThreatMonitoring|Something went wrong, unable to fetch statistics"
msgstr ""
-msgid "ThreatMonitoring|The firewall is not installed or has been disabled. To view this data, ensure you firewall is installed and enabled for your cluster."
+msgid "ThreatMonitoring|The firewall is not installed or has been disabled. To view this data, ensure the web application firewall is installed and enabled for your cluster."
msgstr ""
msgid "ThreatMonitoring|The graph below is an overview of traffic coming to your application as tracked by the Web Application Firewall (WAF). View the docs for instructions on how to access the WAF logs to see what type of malicious traffic is trying to access your app. The docs link is also accessible by clicking the \"?\" icon next to the title below."
@@ -21214,6 +21701,9 @@ msgstr ""
msgid "Time in seconds GitLab will wait for a response from the external service. When the service does not respond in time, access will be denied."
msgstr "Délai d’attente, en secondes, d’une réponse du service externe. Lorsque le service ne répond pas à temps, l’accès sera refusé."
+msgid "Time of import: %{importTime}"
+msgstr ""
+
msgid "Time remaining"
msgstr "Temps restant"
@@ -21398,7 +21888,7 @@ msgstr "Titre"
msgid "Title:"
msgstr ""
-msgid "Titles and Filenames"
+msgid "Titles and Descriptions"
msgstr ""
msgid "To"
@@ -21605,7 +22095,7 @@ msgstr ""
msgid "Too many projects enabled. You will need to manage them via the console or the API."
msgstr ""
-msgid "Topics"
+msgid "Topics (optional)"
msgstr ""
msgid "Total"
@@ -21767,6 +22257,9 @@ msgstr ""
msgid "Try all GitLab has to offer for 30 days."
msgstr "Essayez tout ce que GitLab peut vous offrir pendant 30 jours."
+msgid "Try changing or removing filters."
+msgstr ""
+
msgid "Try to fork again"
msgstr ""
@@ -21860,6 +22353,9 @@ msgstr ""
msgid "Unable to connect to server: %{error}"
msgstr ""
+msgid "Unable to connect to the Jira instance. Please check your Jira integration configuration."
+msgstr ""
+
msgid "Unable to convert Kubernetes logs encoding to UTF-8"
msgstr ""
@@ -22803,6 +23299,9 @@ msgstr ""
msgid "View issue"
msgstr ""
+msgid "View issues"
+msgstr ""
+
msgid "View it on GitLab"
msgstr "Voir sur GitLab"
@@ -22980,6 +23479,15 @@ msgstr ""
msgid "VulnerabilityManagement|Resolved %{timeago} by %{user}"
msgstr ""
+msgid "VulnerabilityManagement|Something went wrong while trying to delete the comment. Please try again later."
+msgstr ""
+
+msgid "VulnerabilityManagement|Something went wrong while trying to retrieve the vulnerability history. Please try again later."
+msgstr ""
+
+msgid "VulnerabilityManagement|Something went wrong while trying to save the comment. Please try again later."
+msgstr ""
+
msgid "VulnerabilityManagement|Something went wrong, could not create an issue."
msgstr ""
@@ -23010,6 +23518,9 @@ msgstr ""
msgid "VulnerabilityStatusTypes|Resolved"
msgstr ""
+msgid "Vulnerability|%{scannerName} (version %{scannerVersion})"
+msgstr ""
+
msgid "Vulnerability|Class"
msgstr "Classe"
@@ -23043,7 +23554,10 @@ msgstr ""
msgid "Vulnerability|Project"
msgstr "Projet"
-msgid "Vulnerability|Report Type"
+msgid "Vulnerability|Scanner Provider"
+msgstr ""
+
+msgid "Vulnerability|Scanner Type"
msgstr ""
msgid "Vulnerability|Severity"
@@ -23097,9 +23611,15 @@ msgstr ""
msgid "We sent you an email with reset password instructions"
msgstr ""
+msgid "We tried to automatically renew your %{strong}%{plan_name}%{strong_close} subscription for %{strong}%{namespace_name}%{strong_close} on %{expires_on} but something went wrong so your subscription was downgraded to the free plan. Don't worry, your data is safe. We suggest you check your payment method and get in touch with our support team (%{support_link}). They'll gladly help with your subscription renewal."
+msgstr ""
+
msgid "We want to be sure it is you, please confirm you are not a robot."
msgstr "Nous voulons nous assurer qu’il s’agit bien de vous, merci de confirmer que vous n’êtes pas un robot."
+msgid "We will automatically renew your %{strong}%{plan_name}%{strong_close} subscription for %{strong}%{namespace_name}%{strong_close} on %{strong}%{expires_on}%{strong_close}. There's nothing that you need to do, we'll let you know when the renewal is complete. Need more seats, a higher plan or just want to review your payment method?"
+msgstr ""
+
msgid "We've found no vulnerabilities"
msgstr ""
@@ -23204,15 +23724,6 @@ msgstr ""
msgid "When:"
msgstr ""
-msgid "While it's rare to have no vulnerabilities for your group, it can happen. In any event, we ask that you double check your settings to make sure you've set up your dashboard correctly."
-msgstr ""
-
-msgid "While it's rare to have no vulnerabilities for your pipeline, it can happen. In any event, we ask that you double check your settings to make sure all security scanning jobs have passed successfully."
-msgstr ""
-
-msgid "While it's rare to have no vulnerabilities for your project, it can happen. In any event, we ask that you double check your settings to make sure you've set up your dashboard correctly."
-msgstr ""
-
msgid "While it's rare to have no vulnerabilities, it can happen. In any event, we ask that you please double check your settings to make sure you've set up your dashboard correctly."
msgstr ""
@@ -23243,9 +23754,6 @@ msgstr ""
msgid "Wiki"
msgstr "Wiki"
-msgid "Wiki pages"
-msgstr ""
-
msgid "Wiki was successfully updated."
msgstr ""
@@ -23429,6 +23937,9 @@ msgstr ""
msgid "Yes, add it"
msgstr "Oui, l’ajouter"
+msgid "Yes, close issue"
+msgstr ""
+
msgid "Yes, let me map Google Code users to full names or GitLab users."
msgstr "Oui, permettezâ€moi d’associer les utilisateurs de Google Code aux noms complets ou aux nom d’utilisateurs de GitLab."
@@ -23624,6 +24135,9 @@ msgstr "Vous ne pouvez pas écrire sur cette instance GitLab en lecture seule."
msgid "You could not create a new trigger."
msgstr ""
+msgid "You didn't renew your %{strong}%{plan_name}%{strong_close} subscription for %{strong}%{namespace_name}%{strong_close} so it was downgraded to the free plan."
+msgstr ""
+
msgid "You didn't renew your %{strong}%{plan_name}%{strong_close} subscription so it was downgraded to the GitLab Core Plan."
msgstr ""
@@ -23690,6 +24204,9 @@ msgstr ""
msgid "You have declined the invitation to join %{label}."
msgstr ""
+msgid "You have imported from this project %{numberOfPreviousImportsForProject} times before. Each new import will create duplicate issues."
+msgstr ""
+
msgid "You have no permissions"
msgstr "Vous n’avez pas les autorisations"
@@ -23771,6 +24288,9 @@ msgstr ""
msgid "You need to upload a Google Takeout archive."
msgstr ""
+msgid "You reached %{usage_in_percent} of %{namespace_name}'s capacity (%{used_storage} of %{storage_limit})"
+msgstr ""
+
msgid "You tried to fork %{link_to_the_project} but it failed for the following reason:"
msgstr ""
@@ -23855,9 +24375,21 @@ msgstr ""
msgid "YouTube"
msgstr "YouTube"
+msgid "Your %{strong}%{plan_name}%{strong_close} subscription for %{strong}%{namespace_name}%{strong_close} will expire on %{strong}%{expires_on}%{strong_close}. After that, you will not to be able to create issues or merge requests as well as many other features."
+msgstr ""
+
msgid "Your %{strong}%{plan_name}%{strong_close} subscription will expire on %{strong}%{expires_on}%{strong_close}. After that, you will not to be able to create issues or merge requests as well as many other features."
msgstr ""
+msgid "Your CSV export has started. It will be emailed to %{email} when complete."
+msgstr ""
+
+msgid "Your CSV export of %{issues_count} from project %{project_link} has been added to this email as an attachment."
+msgstr ""
+
+msgid "Your CSV export of %{written_count} from project %{project_name} (%{project_url}) has been added to this email as an attachment."
+msgstr ""
+
msgid "Your Commit Email will be used for web based operations, such as edits and merges."
msgstr ""
@@ -23954,6 +24486,9 @@ msgstr ""
msgid "Your comment could not be updated! Please check your network connection and try again."
msgstr ""
+msgid "Your comment will be discarded."
+msgstr ""
+
msgid "Your custom stage '%{title}' was created"
msgstr ""
@@ -23993,6 +24528,9 @@ msgstr ""
msgid "Your new personal access token has been created."
msgstr ""
+msgid "Your new project access token has been created."
+msgstr ""
+
msgid "Your password isn't required to view this page. If a password or any other personal details are requested, please contact your administrator to report abuse."
msgstr ""
@@ -24011,12 +24549,18 @@ msgstr "Vos projets"
msgid "Your request for access has been queued for review."
msgstr ""
+msgid "Your search didn't match any commits."
+msgstr ""
+
msgid "Your subscription expired!"
msgstr ""
msgid "Your subscription has been downgraded"
msgstr ""
+msgid "Your subscription will automatically renew in %{remaining_days}"
+msgstr ""
+
msgid "Your subscription will expire in %{remaining_days}"
msgstr ""
@@ -24055,7 +24599,7 @@ msgstr "auparavant "
msgid "allowed to fail"
msgstr ""
-msgid "already being used for another group or project milestone."
+msgid "already being used for another group or project %{timebox_name}."
msgstr ""
msgid "already has a \"created\" issue link"
@@ -24133,6 +24677,9 @@ msgstr "%{linkStartTag}En savoir plus sur l’analyse des dépendances%{linkEndT
msgid "ciReport|%{linkStartTag}Learn more about SAST %{linkEndTag}"
msgstr "%{linkStartTag}En savoir plus sur SAST %{linkEndTag}"
+msgid "ciReport|%{linkStartTag}Learn more about Secret Scanning %{linkEndTag}"
+msgstr ""
+
msgid "ciReport|%{linkStartTag}Learn more about codequality reports %{linkEndTag}"
msgstr ""
@@ -24296,6 +24843,12 @@ msgstr ""
msgid "ciReport|SAST"
msgstr "SAST"
+msgid "ciReport|Secret scanning"
+msgstr ""
+
+msgid "ciReport|Secret scanning detects secrets and credentials vulnerabilities in your source code."
+msgstr ""
+
msgid "ciReport|Security scanning"
msgstr "Analyse de sécurité"
@@ -24493,6 +25046,9 @@ msgstr ""
msgid "group"
msgstr ""
+msgid "groups"
+msgstr ""
+
msgid "has already been linked to another vulnerability"
msgstr ""
@@ -24656,9 +25212,6 @@ msgstr[1] "demandes de fusion"
msgid "merged %{time_ago}"
msgstr ""
-msgid "milestone should belong either to a project or a group."
-msgstr ""
-
msgid "missing"
msgstr ""
@@ -24995,6 +25548,9 @@ msgstr "nouvelle demande de fusion"
msgid "no contributions"
msgstr ""
+msgid "no expiration"
+msgstr ""
+
msgid "no one can merge"
msgstr ""
@@ -25047,6 +25603,9 @@ msgstr ""
msgid "pending removal"
msgstr ""
+msgid "per day"
+msgstr ""
+
msgid "pipeline"
msgstr ""
@@ -25075,6 +25634,12 @@ msgstr[1] "projets"
msgid "project avatar"
msgstr ""
+msgid "projects"
+msgstr ""
+
+msgid "push to your repository, create pipelines, create issues or add comments. To reduce storage capacity, delete unused repositories, artifacts, wikis, issues, and pipelines."
+msgstr ""
+
msgid "quick actions"
msgstr ""
@@ -25221,11 +25786,6 @@ msgstr ""
msgid "this document"
msgstr "ce document"
-msgid "thread resolved"
-msgid_plural "threads resolved"
-msgstr[0] ""
-msgstr[1] ""
-
msgid "to help your contributors communicate effectively!"
msgstr "pour aider vos contributeurs à communiquer efficacement !"
@@ -25280,6 +25840,9 @@ msgstr "voir sur GitLab"
msgid "view the blob"
msgstr ""
+msgid "vulnerability|Add a comment"
+msgstr ""
+
msgid "vulnerability|Add a comment or reason for dismissal"
msgstr ""
diff --git a/locale/gitlab.pot b/locale/gitlab.pot
index 24e4073d3af..14afe67bbfd 100644
--- a/locale/gitlab.pot
+++ b/locale/gitlab.pot
@@ -214,6 +214,11 @@ msgid_plural "%d tags"
msgstr[0] ""
msgstr[1] ""
+msgid "%d unresolved thread"
+msgid_plural "%d unresolved threads"
+msgstr[0] ""
+msgstr[1] ""
+
msgid "%d vulnerability dismissed"
msgid_plural "%d vulnerabilities dismissed"
msgstr[0] ""
@@ -236,6 +241,9 @@ msgstr ""
msgid "%{completedWeight} of %{totalWeight} weight completed"
msgstr ""
+msgid "%{containerScanningLinkStart}Container Scanning%{containerScanningLinkEnd} and/or %{dependencyScanningLinkStart}Dependency Scanning%{dependencyScanningLinkEnd} must be enabled. %{securityBotLinkStart}GitLab-Security-Bot%{securityBotLinkEnd} will be the author of the auto-created merge request. %{moreInfoLinkStart}More information%{moreInfoLinkEnd}."
+msgstr ""
+
msgid "%{cores} cores"
msgstr ""
@@ -313,6 +321,9 @@ msgstr ""
msgid "%{firstLabel} +%{labelCount} more"
msgstr ""
+msgid "%{firstMilestoneName} + %{numberOfOtherMilestones} more"
+msgstr ""
+
msgid "%{global_id} is not a valid id for %{expected_type}."
msgstr ""
@@ -373,12 +384,21 @@ msgstr ""
msgid "%{mrText}, this issue will be closed automatically."
msgstr ""
+msgid "%{namespace_name} is now read-only. You cannot: %{base_message}"
+msgstr ""
+
msgid "%{name} contained %{resultsString}"
msgstr ""
msgid "%{name} found %{resultsString}"
msgstr ""
+msgid "%{name} has %{percent} or less Shared Runner Pipeline minutes remaining. Once it runs out, no new jobs or pipelines in its projects will run."
+msgstr ""
+
+msgid "%{name} has run out of Shared Runner Pipeline minutes so no new jobs or pipelines in its projects will run."
+msgstr ""
+
msgid "%{name} is scheduled for %{action}"
msgstr ""
@@ -513,6 +533,9 @@ msgstr[1] ""
msgid "%{text} is available"
msgstr ""
+msgid "%{timebox_name} should belong either to a project or a group."
+msgstr ""
+
msgid "%{title} %{operator} %{threshold}"
msgstr ""
@@ -569,6 +592,9 @@ msgid_plural "(%d closed)"
msgstr[0] ""
msgstr[1] ""
+msgid "(%{linkStart}Cron syntax%{linkEnd})"
+msgstr ""
+
msgid "(%{mrCount} merged)"
msgstr ""
@@ -803,6 +829,9 @@ msgstr ""
msgid "<strong>%{group_name}</strong> group members"
msgstr ""
+msgid "<strong>%{label_name}</strong> <span>will be permanently deleted from %{subject_name}. This cannot be undone.</span>"
+msgstr ""
+
msgid "<strong>Deletes</strong> source branch"
msgstr ""
@@ -833,6 +862,9 @@ msgstr ""
msgid "A basic page and serverless function that uses AWS Lambda, AWS API Gateway, and GitLab Pages"
msgstr ""
+msgid "A complete DevOps platform"
+msgstr ""
+
msgid "A default branch cannot be chosen for an empty project."
msgstr ""
@@ -845,7 +877,10 @@ msgstr ""
msgid "A fork is a copy of a project.<br />Forking a repository allows you to make changes without affecting the original project."
msgstr ""
-msgid "A group represents your organization in GitLab."
+msgid "A group is a collection of several projects"
+msgstr ""
+
+msgid "A group represents your organization in GitLab. Groups allow you to manage users and collaborate across multiple projects."
msgstr ""
msgid "A member of the abuse team will review your report as soon as possible."
@@ -896,6 +931,12 @@ msgstr ""
msgid "A secure token that identifies an external storage request."
msgstr ""
+msgid "A sign-in to your account has been made from the following IP address: %{ip}"
+msgstr ""
+
+msgid "A sign-in to your account has been made from the following IP address: %{ip}."
+msgstr ""
+
msgid "A subscription will trigger a new pipeline on the default branch of this project when a pipeline successfully completes for a new tag on the %{default_branch_docs} of the subscribed project."
msgstr ""
@@ -1058,6 +1099,9 @@ msgstr ""
msgid "AccessibilityReport|New"
msgstr ""
+msgid "AccessibilityReport|The accessibility scanning found an error of the following type: %{code}"
+msgstr ""
+
msgid "Account"
msgstr ""
@@ -1082,7 +1126,7 @@ msgstr ""
msgid "Active"
msgstr ""
-msgid "Active %{type} Tokens (%{token_length})"
+msgid "Active %{type} (%{token_length})"
msgstr ""
msgid "Active Sessions"
@@ -1135,7 +1179,7 @@ msgstr ""
msgid "Add Zoom meeting"
msgstr ""
-msgid "Add a %{type} token"
+msgid "Add a %{type}"
msgstr ""
msgid "Add a GPG key"
@@ -1159,9 +1203,15 @@ msgstr ""
msgid "Add a homepage to your wiki that contains information about your project and GitLab will display it here instead of this message."
msgstr ""
+msgid "Add a line"
+msgstr ""
+
msgid "Add a link"
msgstr ""
+msgid "Add a new issue"
+msgstr ""
+
msgid "Add a numbered list"
msgstr ""
@@ -1177,7 +1227,10 @@ msgstr ""
msgid "Add an SSH key"
msgstr ""
-msgid "Add an existing issue to the epic."
+msgid "Add an existing issue"
+msgstr ""
+
+msgid "Add an impersonation token"
msgstr ""
msgid "Add an issue"
@@ -1243,6 +1296,9 @@ msgstr ""
msgid "Add request manually"
msgstr ""
+msgid "Add strikethrough text"
+msgstr ""
+
msgid "Add system hook"
msgstr ""
@@ -1333,7 +1389,7 @@ msgstr ""
msgid "Adds an issue to an epic."
msgstr ""
-msgid "Adjust your filters/search criteria above."
+msgid "Adjust your filters/search criteria above. If you believe this may be an error, please refer to the %{linkStart}Geo Troubleshooting%{linkEnd} documentation for more information."
msgstr ""
msgid "Admin Area"
@@ -1669,11 +1725,128 @@ msgstr ""
msgid "After a successful password update, you will be redirected to the login page where you can log in with your new password."
msgstr ""
+msgid "After that, you will not to be able to use merge approvals or code quality as well as many other features."
+msgstr ""
+
+msgid "After that, you will not to be able to use merge approvals or epics as well as many other features."
+msgstr ""
+
+msgid "After that, you will not to be able to use merge approvals or epics as well as many security features."
+msgstr ""
+
msgid "Alert"
msgid_plural "Alerts"
msgstr[0] ""
msgstr[1] ""
+msgid "AlertManagement|Acknowledged"
+msgstr ""
+
+msgid "AlertManagement|Alert"
+msgstr ""
+
+msgid "AlertManagement|Alert detail"
+msgstr ""
+
+msgid "AlertManagement|Alert details"
+msgstr ""
+
+msgid "AlertManagement|Alerts"
+msgstr ""
+
+msgid "AlertManagement|All alerts"
+msgstr ""
+
+msgid "AlertManagement|Authorize external service"
+msgstr ""
+
+msgid "AlertManagement|Create issue"
+msgstr ""
+
+msgid "AlertManagement|Critical"
+msgstr ""
+
+msgid "AlertManagement|Display alerts from all your monitoring tools directly within GitLab. Streamline the investigation of your alerts and the escalation of alerts to incidents."
+msgstr ""
+
+msgid "AlertManagement|End time"
+msgstr ""
+
+msgid "AlertManagement|Events"
+msgstr ""
+
+msgid "AlertManagement|High"
+msgstr ""
+
+msgid "AlertManagement|Info"
+msgstr ""
+
+msgid "AlertManagement|Low"
+msgstr ""
+
+msgid "AlertManagement|Medium"
+msgstr ""
+
+msgid "AlertManagement|More information"
+msgstr ""
+
+msgid "AlertManagement|No alert data to display."
+msgstr ""
+
+msgid "AlertManagement|No alerts available to display. If you think you're seeing this message in error, refresh the page."
+msgstr ""
+
+msgid "AlertManagement|No alerts to display."
+msgstr ""
+
+msgid "AlertManagement|Open"
+msgstr ""
+
+msgid "AlertManagement|Overview"
+msgstr ""
+
+msgid "AlertManagement|Reported %{when}"
+msgstr ""
+
+msgid "AlertManagement|Reported %{when} by %{tool}"
+msgstr ""
+
+msgid "AlertManagement|Resolved"
+msgstr ""
+
+msgid "AlertManagement|Service"
+msgstr ""
+
+msgid "AlertManagement|Severity"
+msgstr ""
+
+msgid "AlertManagement|Start time"
+msgstr ""
+
+msgid "AlertManagement|Status"
+msgstr ""
+
+msgid "AlertManagement|Surface alerts in GitLab"
+msgstr ""
+
+msgid "AlertManagement|There was an error displaying the alert. Please refresh the page to try again."
+msgstr ""
+
+msgid "AlertManagement|There was an error displaying the alerts. Confirm your endpoint's configuration details to ensure alerts appear."
+msgstr ""
+
+msgid "AlertManagement|There was an error while updating the status of the alert. Please try again."
+msgstr ""
+
+msgid "AlertManagement|Tool"
+msgstr ""
+
+msgid "AlertManagement|Triggered"
+msgstr ""
+
+msgid "AlertManagement|Unknown"
+msgstr ""
+
msgid "AlertService|%{linkStart}Learn more%{linkEnd} about configuring this endpoint to receive alerts."
msgstr ""
@@ -1743,6 +1916,9 @@ msgstr ""
msgid "All security scans are enabled because %{linkStart}Auto DevOps%{linkEnd} is enabled on this project"
msgstr ""
+msgid "All threads resolved"
+msgstr ""
+
msgid "All users"
msgstr ""
@@ -1761,6 +1937,9 @@ msgstr ""
msgid "Allow only the selected protocols to be used for Git access."
msgstr ""
+msgid "Allow owners to manage default branch protection per group"
+msgstr ""
+
msgid "Allow owners to manually add users outside of LDAP"
msgstr ""
@@ -1845,6 +2024,9 @@ msgstr ""
msgid "An application called %{link_to_client} is requesting access to your GitLab account."
msgstr ""
+msgid "An email notification was recently sent from the admin panel. Please wait %{wait_time_in_words} before attempting to send another message."
+msgstr ""
+
msgid "An empty GitLab User field will add the FogBugz user's full name (e.g. \"By John Smith\") in the description of all issues and comments. It will also associate and/or assign these issues and comments with the project creator."
msgstr ""
@@ -1953,6 +2135,9 @@ msgstr ""
msgid "An error occurred while fetching sidebar data"
msgstr ""
+msgid "An error occurred while fetching terraform reports."
+msgstr ""
+
msgid "An error occurred while fetching the Service Desk address."
msgstr ""
@@ -2031,6 +2216,9 @@ msgstr ""
msgid "An error occurred while loading merge requests."
msgstr ""
+msgid "An error occurred while loading milestones"
+msgstr ""
+
msgid "An error occurred while loading terraform report"
msgstr ""
@@ -2106,6 +2294,9 @@ msgstr ""
msgid "An error occurred while saving the template. Please check if the template exists."
msgstr ""
+msgid "An error occurred while searching for milestones"
+msgstr ""
+
msgid "An error occurred while subscribing to notifications."
msgstr ""
@@ -2142,9 +2333,15 @@ msgstr ""
msgid "An instance-level serverless domain already exists."
msgstr ""
+msgid "An issue already exists"
+msgstr ""
+
msgid "An issue can be a bug, a todo or a feature request that needs to be discussed in a project. Besides, issues are searchable and filterable."
msgstr ""
+msgid "An unauthenticated user"
+msgstr ""
+
msgid "An unexpected error occurred while checking the project environment."
msgstr ""
@@ -2193,12 +2390,6 @@ msgstr ""
msgid "Any Author"
msgstr ""
-msgid "Any Label"
-msgstr ""
-
-msgid "Any Milestone"
-msgstr ""
-
msgid "Any branch"
msgstr ""
@@ -2208,9 +2399,15 @@ msgstr ""
msgid "Any encrypted tokens"
msgstr ""
+msgid "Any label"
+msgstr ""
+
msgid "Any member with Developer or higher permissions to the project."
msgstr ""
+msgid "Any milestone"
+msgstr ""
+
msgid "Any namespace"
msgstr ""
@@ -2363,6 +2560,9 @@ msgstr ""
msgid "Approved the current merge request."
msgstr ""
+msgid "Approved-By"
+msgstr ""
+
msgid "Approver"
msgstr ""
@@ -2408,10 +2608,10 @@ msgstr ""
msgid "Are you sure that you want to unarchive this project?"
msgstr ""
-msgid "Are you sure you want to cancel creating this comment?"
+msgid "Are you sure you want to cancel editing this comment?"
msgstr ""
-msgid "Are you sure you want to cancel editing this comment?"
+msgid "Are you sure you want to close this blocked issue?"
msgstr ""
msgid "Are you sure you want to delete %{name}?"
@@ -2441,6 +2641,9 @@ msgstr ""
msgid "Are you sure you want to deploy this environment?"
msgstr ""
+msgid "Are you sure you want to discard this comment?"
+msgstr ""
+
msgid "Are you sure you want to erase this build?"
msgstr ""
@@ -2480,7 +2683,7 @@ msgstr ""
msgid "Are you sure you want to reset the health check token?"
msgstr ""
-msgid "Are you sure you want to revoke this %{type} Token? This action cannot be undone."
+msgid "Are you sure you want to revoke this %{type}? This action cannot be undone."
msgstr ""
msgid "Are you sure you want to revoke this nickname?"
@@ -2594,6 +2797,9 @@ msgstr ""
msgid "Assigned Merge Requests"
msgstr ""
+msgid "Assigned to %{assignee_name}"
+msgstr ""
+
msgid "Assigned to me"
msgstr ""
@@ -2617,6 +2823,9 @@ msgstr ""
msgid "At least one approval from a code owner is required to change files matching the respective CODEOWNER rules."
msgstr ""
+msgid "At least one logging option is required to be enabled"
+msgstr ""
+
msgid "At least one of group_id or project_id must be specified"
msgstr ""
@@ -2655,6 +2864,45 @@ msgstr ""
msgid "AuditEvents|Target"
msgstr ""
+msgid "AuditLogs|(removed)"
+msgstr ""
+
+msgid "AuditLogs|Action"
+msgstr ""
+
+msgid "AuditLogs|Author"
+msgstr ""
+
+msgid "AuditLogs|Date"
+msgstr ""
+
+msgid "AuditLogs|Failed to find %{type}. Please search for another %{type}."
+msgstr ""
+
+msgid "AuditLogs|Failed to find %{type}. Please try again."
+msgstr ""
+
+msgid "AuditLogs|Group Events"
+msgstr ""
+
+msgid "AuditLogs|IP Address"
+msgstr ""
+
+msgid "AuditLogs|No matching %{type} found."
+msgstr ""
+
+msgid "AuditLogs|Object"
+msgstr ""
+
+msgid "AuditLogs|Project Events"
+msgstr ""
+
+msgid "AuditLogs|Target"
+msgstr ""
+
+msgid "AuditLogs|User Events"
+msgstr ""
+
msgid "Aug"
msgstr ""
@@ -2754,6 +3002,9 @@ msgstr ""
msgid "AutoDevOps|Auto DevOps documentation"
msgstr ""
+msgid "AutoDevOps|Dismiss Auto DevOps box"
+msgstr ""
+
msgid "AutoDevOps|Enable in settings"
msgstr ""
@@ -2784,6 +3035,9 @@ msgstr ""
msgid "Automatic certificate management using Let's Encrypt"
msgstr ""
+msgid "Automatically create merge requests for vulnerabilities that have fixes available."
+msgstr ""
+
msgid "Automatically marked as default internal user"
msgstr ""
@@ -2799,6 +3053,9 @@ msgstr ""
msgid "Available"
msgstr ""
+msgid "Available for dependency and container scanning"
+msgstr ""
+
msgid "Available group Runners: %{runners}"
msgstr ""
@@ -2937,9 +3194,6 @@ msgstr ""
msgid "BambooService|You must set up automatic revision labeling and a repository trigger in Bamboo."
msgstr ""
-msgid "Batch operations"
-msgstr ""
-
msgid "BatchComments|Delete all pending comments"
msgstr ""
@@ -3069,6 +3323,9 @@ msgstr ""
msgid "Boards"
msgstr ""
+msgid "Boards and Board Lists"
+msgstr ""
+
msgid "Boards|Collapse"
msgstr ""
@@ -3276,16 +3533,16 @@ msgstr ""
msgid "BurndownChartLabel|Open issues"
msgstr ""
-msgid "Business"
+msgid "Burnup chart"
msgstr ""
-msgid "Business metrics (Custom)"
+msgid "Business"
msgstr ""
-msgid "Buy EE"
+msgid "Business metrics (Custom)"
msgstr ""
-msgid "Buy GitLab Enterprise Edition"
+msgid "Buy more Pipeline minutes"
msgstr ""
msgid "By %{user_name}"
@@ -3333,6 +3590,9 @@ msgstr ""
msgid "CI/CD settings"
msgstr ""
+msgid "CICD|Add a %{kubernetes_cluster_link_start}Kubernetes cluster integration%{link_end} with a domain or create an AUTO_DEVOPS_PLATFORM_TARGET CI variable."
+msgstr ""
+
msgid "CICD|Auto DevOps"
msgstr ""
@@ -3363,9 +3623,6 @@ msgstr ""
msgid "CICD|You must add a %{base_domain_link_start}base domain%{link_end} to your %{kubernetes_cluster_link_start}Kubernetes cluster%{link_end} in order for your deployment strategy to work."
msgstr ""
-msgid "CICD|You must add a %{kubernetes_cluster_link_start}Kubernetes cluster integration%{link_end} to this project with a domain in order for your deployment strategy to work correctly."
-msgstr ""
-
msgid "CICD|group enabled"
msgstr ""
@@ -3393,6 +3650,9 @@ msgstr ""
msgid "Can override approvers and approvals required per merge request"
msgstr ""
+msgid "Can't edit as source branch was deleted"
+msgstr ""
+
msgid "Can't find HEAD commit for this branch"
msgstr ""
@@ -3483,6 +3743,9 @@ msgstr ""
msgid "Capacity threshold"
msgstr ""
+msgid "Certain user content will be moved to a system-wide \"Ghost User\" in order to maintain content for posterity. For further information, please refer to the %{link_start}user account deletion documentation.%{link_end}"
+msgstr ""
+
msgid "Certificate"
msgstr ""
@@ -3519,6 +3782,12 @@ msgstr ""
msgid "Change permissions"
msgstr ""
+msgid "Change status"
+msgstr ""
+
+msgid "Change subscription"
+msgstr ""
+
msgid "Change template"
msgstr ""
@@ -3579,6 +3848,9 @@ msgstr ""
msgid "Changing group path can have unintended side effects."
msgstr ""
+msgid "Channel handle (e.g. town-square)"
+msgstr ""
+
msgid "Charts"
msgstr ""
@@ -3618,9 +3890,6 @@ msgstr ""
msgid "ChatMessage|and [%{count} more](%{pipeline_failed_jobs_url})"
msgstr ""
-msgid "ChatMessage|failed"
-msgstr ""
-
msgid "ChatMessage|has failed"
msgstr ""
@@ -3636,9 +3905,6 @@ msgstr ""
msgid "ChatMessage|in %{project_link}"
msgstr ""
-msgid "ChatMessage|passed"
-msgstr ""
-
msgid "Check again"
msgstr ""
@@ -3690,15 +3956,6 @@ msgstr ""
msgid "Checkout|(x%{numberOfUsers})"
msgstr ""
-msgid "Checkout|1. Your profile"
-msgstr ""
-
-msgid "Checkout|2. Checkout"
-msgstr ""
-
-msgid "Checkout|3. Your GitLab group"
-msgstr ""
-
msgid "Checkout|Billing address"
msgstr ""
@@ -4089,9 +4346,6 @@ msgstr ""
msgid "Click any <strong>project name</strong> in the project list below to navigate to the project milestone."
msgstr ""
-msgid "Click here"
-msgstr ""
-
msgid "Click the <strong>Download</strong> button and wait for downloading to complete."
msgstr ""
@@ -4146,6 +4400,9 @@ msgstr ""
msgid "Close"
msgstr ""
+msgid "Close %{display_issuable_type}"
+msgstr ""
+
msgid "Close %{tabname}"
msgstr ""
@@ -4341,6 +4598,9 @@ msgstr ""
msgid "ClusterIntegration|Cluster name is required."
msgstr ""
+msgid "ClusterIntegration|Cluster_applications artifact too big. Maximum allowable size: %{human_size}"
+msgstr ""
+
msgid "ClusterIntegration|Clusters are utilized by selecting the nearest ancestor with a matching environment scope. For example, project clusters will override group clusters."
msgstr ""
@@ -4479,6 +4739,12 @@ msgstr ""
msgid "ClusterIntegration|Fetching zones"
msgstr ""
+msgid "ClusterIntegration|Fluentd"
+msgstr ""
+
+msgid "ClusterIntegration|Fluentd is an open source data collector, which lets you unify the data collection and consumption for a better use and understanding of data. It requires at least one of the following logs to be successfully installed."
+msgstr ""
+
msgid "ClusterIntegration|GitLab Integration"
msgstr ""
@@ -4515,6 +4781,9 @@ msgstr ""
msgid "ClusterIntegration|Helm Tiller"
msgstr ""
+msgid "ClusterIntegration|Helm release failed to install"
+msgstr ""
+
msgid "ClusterIntegration|Helm streamlines installing and managing Kubernetes applications. Tiller runs inside of your Kubernetes Cluster, and manages releases of your charts."
msgstr ""
@@ -4677,6 +4946,9 @@ msgstr ""
msgid "ClusterIntegration|No VPCs found"
msgstr ""
+msgid "ClusterIntegration|No deployment cluster found for this job"
+msgstr ""
+
msgid "ClusterIntegration|No instance type found"
msgstr ""
@@ -4785,6 +5057,15 @@ msgstr ""
msgid "ClusterIntegration|Request to begin uninstalling failed"
msgstr ""
+msgid "ClusterIntegration|SIEM Hostname"
+msgstr ""
+
+msgid "ClusterIntegration|SIEM Port"
+msgstr ""
+
+msgid "ClusterIntegration|SIEM Protocol"
+msgstr ""
+
msgid "ClusterIntegration|Save changes"
msgstr ""
@@ -4881,6 +5162,12 @@ msgstr ""
msgid "ClusterIntegration|Select zone to choose machine type"
msgstr ""
+msgid "ClusterIntegration|Send Cilium Logs"
+msgstr ""
+
+msgid "ClusterIntegration|Send ModSecurity Logs"
+msgstr ""
+
msgid "ClusterIntegration|Service Token"
msgstr ""
@@ -5094,6 +5381,9 @@ msgstr ""
msgid "Code Review Analytics displays a table of open merge requests considered to be in code review. There are currently no merge requests in review for this project and/or filters."
msgstr ""
+msgid "Code coverage statistics for master %{start_date} - %{end_date}"
+msgstr ""
+
msgid "Code owner approval is required"
msgstr ""
@@ -5142,6 +5432,9 @@ msgstr ""
msgid "ComboSearch is not defined"
msgstr ""
+msgid "Coming soon"
+msgstr ""
+
msgid "Command"
msgstr ""
@@ -5339,6 +5632,9 @@ msgstr ""
msgid "ComplianceFramework|SOX - Sarbanes-Oxley"
msgstr ""
+msgid "ComplianceFramework|This project is regulated by %{framework}."
+msgstr ""
+
msgid "Confidence: %{confidence}"
msgstr ""
@@ -5450,16 +5746,10 @@ msgstr ""
msgid "Connection timed out"
msgstr ""
-msgid "Contact an owner of group %{namespace_name} to upgrade the plan."
-msgstr ""
-
-msgid "Contact owner %{link_start}%{owner_name}%{link_end} to upgrade the plan."
-msgstr ""
-
msgid "Contact sales to upgrade"
msgstr ""
-msgid "Contact your Administrator to upgrade your license."
+msgid "Contact support"
msgstr ""
msgid "Container Registry"
@@ -5522,9 +5812,6 @@ msgstr ""
msgid "ContainerRegistry|Docker tag expiration policy is %{toggleStatus}"
msgstr ""
-msgid "ContainerRegistry|Docker tags with names matching this regex pattern will expire:"
-msgstr ""
-
msgid "ContainerRegistry|Edit Settings"
msgstr ""
@@ -5540,12 +5827,18 @@ msgstr ""
msgid "ContainerRegistry|Expiration schedule:"
msgstr ""
+msgid "ContainerRegistry|Filter by name"
+msgstr ""
+
msgid "ContainerRegistry|If you are not already logged in, you need to authenticate to the Container Registry by using your GitLab username and password. If you have %{twofaDocLinkStart}Two-Factor Authentication%{twofaDocLinkEnd} enabled, use a %{personalAccessTokensDocLinkStart}Personal Access Token%{personalAccessTokensDocLinkEnd} instead of a password."
msgstr ""
msgid "ContainerRegistry|Image ID"
msgstr ""
+msgid "ContainerRegistry|Image Repositories"
+msgstr ""
+
msgid "ContainerRegistry|Keep and protect the images that matter most."
msgstr ""
@@ -5570,6 +5863,15 @@ msgstr ""
msgid "ContainerRegistry|Quick Start"
msgstr ""
+msgid "ContainerRegistry|Regular expressions such as %{codeStart}.*-master%{codeEnd} or %{codeStart}release-.*%{codeEnd} are supported"
+msgstr ""
+
+msgid "ContainerRegistry|Regular expressions such as %{codeStart}.*-test%{codeEnd} or %{codeStart}dev-.*%{codeEnd} are supported. To select all tags, use %{codeStart}.*%{codeEnd}"
+msgstr ""
+
+msgid "ContainerRegistry|Remember to run %{docLinkStart}garbage collection%{docLinkEnd} to remove the stale data from storage."
+msgstr ""
+
msgid "ContainerRegistry|Remove repository"
msgstr ""
@@ -5584,19 +5886,19 @@ msgstr[1] ""
msgid "ContainerRegistry|Retention policy has been Enabled"
msgstr ""
-msgid "ContainerRegistry|Something went wrong while deleting the tag."
+msgid "ContainerRegistry|Something went wrong while fetching the expiration policy."
msgstr ""
-msgid "ContainerRegistry|Something went wrong while deleting the tags."
+msgid "ContainerRegistry|Something went wrong while fetching the repository list."
msgstr ""
-msgid "ContainerRegistry|Something went wrong while fetching the expiration policy."
+msgid "ContainerRegistry|Something went wrong while fetching the tags list."
msgstr ""
-msgid "ContainerRegistry|Something went wrong while fetching the repository list."
+msgid "ContainerRegistry|Something went wrong while marking the tag for deletion."
msgstr ""
-msgid "ContainerRegistry|Something went wrong while fetching the tags list."
+msgid "ContainerRegistry|Something went wrong while marking the tags for deletion."
msgstr ""
msgid "ContainerRegistry|Something went wrong while scheduling %{title} for deletion. Please try again."
@@ -5605,10 +5907,10 @@ msgstr ""
msgid "ContainerRegistry|Something went wrong while updating the expiration policy."
msgstr ""
-msgid "ContainerRegistry|Tag"
+msgid "ContainerRegistry|Sorry, your filter produced no results."
msgstr ""
-msgid "ContainerRegistry|Tag deleted successfully"
+msgid "ContainerRegistry|Tag"
msgstr ""
msgid "ContainerRegistry|Tag expiration policy"
@@ -5617,7 +5919,16 @@ msgstr ""
msgid "ContainerRegistry|Tag expiration policy is designed to:"
msgstr ""
-msgid "ContainerRegistry|Tags deleted successfully"
+msgid "ContainerRegistry|Tag successfully marked for deletion."
+msgstr ""
+
+msgid "ContainerRegistry|Tags successfully marked for deletion."
+msgstr ""
+
+msgid "ContainerRegistry|Tags with names matching this regex pattern will %{italicStart}be preserved:%{italicEnd}"
+msgstr ""
+
+msgid "ContainerRegistry|Tags with names matching this regex pattern will %{italicStart}expire:%{italicEnd}"
msgstr ""
msgid "ContainerRegistry|The Container Registry tag expiration and retention policies for this project have not been enabled."
@@ -5644,19 +5955,16 @@ msgstr ""
msgid "ContainerRegistry|There was an error during the deletion of this image repository, please try again."
msgstr ""
-msgid "ContainerRegistry|This Registry contains deleted image tag data. Remember to run %{docLinkStart}garbage collection%{docLinkEnd} to remove the stale data from storage."
-msgstr ""
-
msgid "ContainerRegistry|This image has no active tags"
msgstr ""
msgid "ContainerRegistry|This image repository is scheduled for deletion"
msgstr ""
-msgid "ContainerRegistry|We are having trouble connecting to Docker, which could be due to an issue with your project name or path. %{docLinkStart}More Information%{docLinkEnd}"
+msgid "ContainerRegistry|To widen your search, change or remove the filters above."
msgstr ""
-msgid "ContainerRegistry|Wildcards such as %{codeStart}.*-stable%{codeEnd} or %{codeStart}production/.*%{codeEnd} are supported. To select all tags, use %{codeStart}.*%{codeEnd}"
+msgid "ContainerRegistry|We are having trouble connecting to Docker, which could be due to an issue with your project name or path. %{docLinkStart}More Information%{docLinkEnd}"
msgstr ""
msgid "ContainerRegistry|With the Container Registry, every project can have its own space to store its Docker images. %{docLinkStart}More Information%{docLinkEnd}"
@@ -5794,6 +6102,9 @@ msgstr ""
msgid "Copy %{proxy_url}"
msgstr ""
+msgid "Copy %{type}"
+msgstr ""
+
msgid "Copy Account ID to clipboard"
msgstr ""
@@ -5839,9 +6150,6 @@ msgstr ""
msgid "Copy file path"
msgstr ""
-msgid "Copy impersonation token"
-msgstr ""
-
msgid "Copy key"
msgstr ""
@@ -5857,9 +6165,6 @@ msgstr ""
msgid "Copy link to chart"
msgstr ""
-msgid "Copy personal access token"
-msgstr ""
-
msgid "Copy reference"
msgstr ""
@@ -5911,7 +6216,7 @@ msgstr ""
msgid "Could not delete chat nickname %{chat_name}."
msgstr ""
-msgid "Could not find design"
+msgid "Could not find design."
msgstr ""
msgid "Could not remove the trigger."
@@ -5926,6 +6231,9 @@ msgstr ""
msgid "Could not revoke personal access token %{personal_access_token_name}."
msgstr ""
+msgid "Could not revoke project access token %{project_access_token_name}."
+msgstr ""
+
msgid "Could not save group ID"
msgstr ""
@@ -5953,7 +6261,7 @@ msgstr ""
msgid "Create %{environment}"
msgstr ""
-msgid "Create %{type} token"
+msgid "Create %{type}"
msgstr ""
msgid "Create New Directory"
@@ -5971,9 +6279,6 @@ msgstr ""
msgid "Create a Mattermost team for this group"
msgstr ""
-msgid "Create a group for your organization"
-msgstr ""
-
msgid "Create a local proxy for storing frequently used upstream images. %{link_start}Learn more%{link_end} about dependency proxies."
msgstr ""
@@ -5983,13 +6288,13 @@ msgstr ""
msgid "Create a new branch"
msgstr ""
-msgid "Create a new file as there are no files yet. Afterwards, you'll be able to commit your changes."
+msgid "Create a new deploy key for this project"
msgstr ""
-msgid "Create a new issue"
+msgid "Create a new file as there are no files yet. Afterwards, you'll be able to commit your changes."
msgstr ""
-msgid "Create a new issue and add it to the epic."
+msgid "Create a new issue"
msgstr ""
msgid "Create a new repository"
@@ -6001,9 +6306,6 @@ msgstr ""
msgid "Create an account using:"
msgstr ""
-msgid "Create an issue"
-msgstr ""
-
msgid "Create an issue. Issues are created for each alert triggered."
msgstr ""
@@ -6058,6 +6360,9 @@ msgstr ""
msgid "Create milestone"
msgstr ""
+msgid "Create new"
+msgstr ""
+
msgid "Create new board"
msgstr ""
@@ -6097,6 +6402,9 @@ msgstr ""
msgid "Create your first page"
msgstr ""
+msgid "Create your group"
+msgstr ""
+
msgid "CreateGroup|You don’t have permission to create a subgroup in this group."
msgstr ""
@@ -6181,9 +6489,6 @@ msgstr ""
msgid "Cron Timezone"
msgstr ""
-msgid "Cron syntax"
-msgstr ""
-
msgid "Crossplane"
msgstr ""
@@ -6220,6 +6525,9 @@ msgstr ""
msgid "CurrentUser|Start a Gold trial"
msgstr ""
+msgid "CurrentUser|Upgrade"
+msgstr ""
+
msgid "Custom CI configuration path"
msgstr ""
@@ -6316,6 +6624,9 @@ msgstr ""
msgid "Customize your pipeline configuration."
msgstr ""
+msgid "Cycle Time"
+msgstr ""
+
msgid "CycleAnalyticsEvent|Issue closed"
msgstr ""
@@ -6471,6 +6782,9 @@ msgstr ""
msgid "CycleAnalytics|stage dropdown"
msgstr ""
+msgid "DAG"
+msgstr ""
+
msgid "DNS"
msgstr ""
@@ -6642,6 +6956,12 @@ msgstr ""
msgid "Delete domain"
msgstr ""
+msgid "Delete label"
+msgstr ""
+
+msgid "Delete label: %{label_name} ?"
+msgstr ""
+
msgid "Delete license"
msgstr ""
@@ -6731,11 +7051,6 @@ msgid_plural "Dependencies|%d additional vulnerabilities not shown"
msgstr[0] ""
msgstr[1] ""
-msgid "Dependencies|%d vulnerability"
-msgid_plural "Dependencies|%d vulnerabilities"
-msgstr[0] ""
-msgstr[1] ""
-
msgid "Dependencies|%d vulnerability detected"
msgid_plural "Dependencies|%d vulnerabilities detected"
msgstr[0] ""
@@ -6771,12 +7086,6 @@ msgstr ""
msgid "Dependencies|Packager"
msgstr ""
-msgid "Dependencies|Safe"
-msgstr ""
-
-msgid "Dependencies|Status"
-msgstr ""
-
msgid "Dependencies|The %{codeStartTag}dependency_scanning%{codeEndTag} job has failed and cannot generate the list. Please ensure the job is running properly and run the pipeline again."
msgstr ""
@@ -6786,9 +7095,6 @@ msgstr ""
msgid "Dependencies|Unsupported file(s) detected"
msgstr ""
-msgid "Dependencies|Version"
-msgstr ""
-
msgid "Dependencies|Vulnerable components"
msgstr ""
@@ -6837,6 +7143,9 @@ msgstr ""
msgid "Deploy key was successfully updated."
msgstr ""
+msgid "Deploy keys allow read-only or read-write (if enabled) access to your repository. Deploy keys can be used for CI, staging or production servers. You can create a deploy key or add an existing one."
+msgstr ""
+
msgid "Deploy progress not found. To see pods, ensure your environment matches %{linkStart}deploy board criteria%{linkEnd}."
msgstr ""
@@ -6900,12 +7209,18 @@ msgstr ""
msgid "DeployTokens|Add a deploy token"
msgstr ""
+msgid "DeployTokens|Allows read access to the package registry"
+msgstr ""
+
msgid "DeployTokens|Allows read-only access to the registry images"
msgstr ""
msgid "DeployTokens|Allows read-only access to the repository"
msgstr ""
+msgid "DeployTokens|Allows write access to the package registry"
+msgstr ""
+
msgid "DeployTokens|Allows write access to the registry images"
msgstr ""
@@ -6927,13 +7242,13 @@ msgstr ""
msgid "DeployTokens|Deploy Tokens"
msgstr ""
-msgid "DeployTokens|Deploy tokens allow access to your repository and registry images."
+msgid "DeployTokens|Deploy tokens allow access to packages, your repository, and registry images."
msgstr ""
msgid "DeployTokens|Expires"
msgstr ""
-msgid "DeployTokens|Group deploy tokens allow read-only access to the repositories and registry images within the group."
+msgid "DeployTokens|Group deploy tokens allow access to the packages, repositories, and registry images within the group."
msgstr ""
msgid "DeployTokens|Name"
@@ -7059,15 +7374,27 @@ msgstr ""
msgid "DesignManagement|Adding a design with the same filename replaces the file in a new version."
msgstr ""
+msgid "DesignManagement|Are you sure you want to cancel changes to this comment?"
+msgstr ""
+
msgid "DesignManagement|Are you sure you want to cancel creating this comment?"
msgstr ""
msgid "DesignManagement|Are you sure you want to delete the selected designs?"
msgstr ""
+msgid "DesignManagement|Cancel changes"
+msgstr ""
+
msgid "DesignManagement|Cancel comment confirmation"
msgstr ""
+msgid "DesignManagement|Cancel comment update confirmation"
+msgstr ""
+
+msgid "DesignManagement|Comment"
+msgstr ""
+
msgid "DesignManagement|Could not add a new comment. Please try again."
msgstr ""
@@ -7077,6 +7404,9 @@ msgstr ""
msgid "DesignManagement|Could not update discussion. Please try again."
msgstr ""
+msgid "DesignManagement|Could not update note. Please try again."
+msgstr ""
+
msgid "DesignManagement|Delete"
msgstr ""
@@ -7104,12 +7434,18 @@ msgstr ""
msgid "DesignManagement|Go to previous design"
msgstr ""
+msgid "DesignManagement|Keep changes"
+msgstr ""
+
msgid "DesignManagement|Keep comment"
msgstr ""
msgid "DesignManagement|Requested design version does not exist. Showing latest version instead"
msgstr ""
+msgid "DesignManagement|Save comment"
+msgstr ""
+
msgid "DesignManagement|Select all"
msgstr ""
@@ -7131,6 +7467,9 @@ msgstr ""
msgid "Destroy"
msgstr ""
+msgid "Detail"
+msgstr ""
+
msgid "Details"
msgstr ""
@@ -7358,6 +7697,9 @@ msgstr ""
msgid "Don't have an account yet?"
msgstr ""
+msgid "Don't include description in commit message"
+msgstr ""
+
msgid "Don't paste the private part of the GPG key. Paste the public part which begins with '-----BEGIN PGP PUBLIC KEY BLOCK-----'."
msgstr ""
@@ -7406,6 +7748,9 @@ msgstr ""
msgid "Download license"
msgstr ""
+msgid "Download raw data (.csv)"
+msgstr ""
+
msgid "Download source code"
msgstr ""
@@ -7511,9 +7856,6 @@ msgstr ""
msgid "Edit environment"
msgstr ""
-msgid "Edit epic description"
-msgstr ""
-
msgid "Edit file"
msgstr ""
@@ -7580,6 +7922,9 @@ msgstr ""
msgid "Email address"
msgstr ""
+msgid "Email could not be sent"
+msgstr ""
+
msgid "Email display name"
msgstr ""
@@ -7595,6 +7940,9 @@ msgstr ""
msgid "Email restrictions for sign-ups"
msgstr ""
+msgid "Email sent"
+msgstr ""
+
msgid "Email the pipelines status to a list of recipients."
msgstr ""
@@ -7697,9 +8045,6 @@ msgstr ""
msgid "Enable and configure Grafana."
msgstr ""
-msgid "Enable and configure InfluxDB metrics."
-msgstr ""
-
msgid "Enable and configure Prometheus metrics."
msgstr ""
@@ -7850,6 +8195,9 @@ msgstr ""
msgid "Enter merge request URLs"
msgstr ""
+msgid "Enter new %{field_title}"
+msgstr ""
+
msgid "Enter new AWS Secret Access Key"
msgstr ""
@@ -7874,6 +8222,9 @@ msgstr ""
msgid "Enter the merge request title"
msgstr ""
+msgid "Enter the name of your application, and we'll return a unique %{type}."
+msgstr ""
+
msgid "Enter your password to approve"
msgstr ""
@@ -8135,9 +8486,6 @@ msgstr ""
msgid "Epics"
msgstr ""
-msgid "Epics (Ultimate / Gold license only)"
-msgstr ""
-
msgid "Epics Roadmap"
msgstr ""
@@ -8147,10 +8495,13 @@ msgstr ""
msgid "Epics let you manage your portfolio of projects more efficiently and with less effort"
msgstr ""
-msgid "Epics|Add an epic"
+msgid "Epics, Issues, and Merge Requests"
+msgstr ""
+
+msgid "Epics|Add a new epic"
msgstr ""
-msgid "Epics|Add an existing epic as a child epic."
+msgid "Epics|Add an existing epic"
msgstr ""
msgid "Epics|An error occurred while saving the %{epicDateType} date"
@@ -8162,12 +8513,6 @@ msgstr ""
msgid "Epics|Are you sure you want to remove %{bStart}%{targetIssueTitle}%{bEnd} from %{bStart}%{parentEpicTitle}%{bEnd}?"
msgstr ""
-msgid "Epics|Create an epic within this group and add it as a child epic."
-msgstr ""
-
-msgid "Epics|Create new epic"
-msgstr ""
-
msgid "Epics|How can I solve this?"
msgstr ""
@@ -8198,6 +8543,9 @@ msgstr ""
msgid "Epics|Something went wrong while fetching group epics."
msgstr ""
+msgid "Epics|Something went wrong while moving item."
+msgstr ""
+
msgid "Epics|Something went wrong while ordering item."
msgstr ""
@@ -8234,7 +8582,13 @@ msgstr ""
msgid "Error creating label."
msgstr ""
-msgid "Error deleting %{issuableType}"
+msgid "Error creating new iteration"
+msgstr ""
+
+msgid "Error creating the snippet"
+msgstr ""
+
+msgid "Error deleting %{issuableType}"
msgstr ""
msgid "Error deleting project. Check logs for error details."
@@ -8339,9 +8693,6 @@ msgstr ""
msgid "Error rendering markdown preview"
msgstr ""
-msgid "Error rendering query"
-msgstr ""
-
msgid "Error saving label update."
msgstr ""
@@ -8357,6 +8708,9 @@ msgstr ""
msgid "Error updating status of to-do item."
msgstr ""
+msgid "Error updating the snippet"
+msgstr ""
+
msgid "Error uploading file"
msgstr ""
@@ -8411,9 +8765,6 @@ msgstr ""
msgid "Estimated"
msgstr ""
-msgid "Event Actions"
-msgstr ""
-
msgid "EventFilterBy|Filter by all"
msgstr ""
@@ -8453,13 +8804,13 @@ msgstr ""
msgid "Every day"
msgstr ""
-msgid "Every day (at 4:00am)"
+msgid "Every day (at %{time})"
msgstr ""
msgid "Every month"
msgstr ""
-msgid "Every month (on the 1st at 4:00am)"
+msgid "Every month (Day %{day} at %{time})"
msgstr ""
msgid "Every three months"
@@ -8471,7 +8822,7 @@ msgstr ""
msgid "Every week"
msgstr ""
-msgid "Every week (Sundays at 4:00am)"
+msgid "Every week (%{weekday} at %{time})"
msgstr ""
msgid "Everyone"
@@ -8528,6 +8879,9 @@ msgstr ""
msgid "Existing members and groups"
msgstr ""
+msgid "Existing projects may be moved into a group"
+msgstr ""
+
msgid "Existing projects will be able to use expiration policies. Avoid enabling this if an external Container Registry is being used, as there is a performance risk if many images exist on one project."
msgstr ""
@@ -8612,12 +8966,18 @@ msgstr ""
msgid "Export as CSV"
msgstr ""
+msgid "Export group"
+msgstr ""
+
msgid "Export issues"
msgstr ""
msgid "Export project"
msgstr ""
+msgid "Export this group with all related data to a new GitLab instance. Once complete, you can import the data file from the \"New Group\" page."
+msgstr ""
+
msgid "Export this project with all its related data in order to move your project to a new GitLab instance. Once the export is finished, you can import the file from the \"New Project\" page."
msgstr ""
@@ -8687,9 +9047,6 @@ msgstr ""
msgid "Failed Jobs"
msgstr ""
-msgid "Failed create wiki"
-msgstr ""
-
msgid "Failed to add a Zoom meeting"
msgstr ""
@@ -8729,6 +9086,9 @@ msgstr ""
msgid "Failed to create resources"
msgstr ""
+msgid "Failed to create wiki"
+msgstr ""
+
msgid "Failed to delete board. Please try again."
msgstr ""
@@ -8738,7 +9098,7 @@ msgstr ""
msgid "Failed to enqueue the rebase operation, possibly due to a long-lived transaction. Try again later."
msgstr ""
-msgid "Failed to find import label for jira import."
+msgid "Failed to find import label for Jira import."
msgstr ""
msgid "Failed to get ref."
@@ -8762,6 +9122,12 @@ msgstr ""
msgid "Failed to load groups & users."
msgstr ""
+msgid "Failed to load labels. Please try again."
+msgstr ""
+
+msgid "Failed to load milestones. Please try again."
+msgstr ""
+
msgid "Failed to load related branches"
msgstr ""
@@ -8927,6 +9293,12 @@ msgstr ""
msgid "FeatureFlags|Edit Feature Flag"
msgstr ""
+msgid "FeatureFlags|Edit Feature Flag User List"
+msgstr ""
+
+msgid "FeatureFlags|Edit list"
+msgstr ""
+
msgid "FeatureFlags|Enable features for specific users and specific environments by defining feature flag strategies. By default, features are available to all users in all environments."
msgstr ""
@@ -8939,6 +9311,9 @@ msgstr ""
msgid "FeatureFlags|Feature Flag"
msgstr ""
+msgid "FeatureFlags|Feature Flag User List Details"
+msgstr ""
+
msgid "FeatureFlags|Feature Flag behavior is built up by creating a set of rules to define the status of target environments. A default wildcard rule %{codeStart}*%{codeEnd} for %{boldStart}All Environments%{boldEnd} is set, and you are able to add as many rules as you need by choosing environment specs below. You can toggle the behavior for each of your rules to set them %{boldStart}Active%{boldEnd} or %{boldStart}Inactive%{boldEnd}."
msgstr ""
@@ -8948,15 +9323,24 @@ msgstr ""
msgid "FeatureFlags|Feature Flags"
msgstr ""
+msgid "FeatureFlags|Feature Flags will look different in the next milestone. No action is needed, but you may notice the functionality was changed to improve the workflow."
+msgstr ""
+
msgid "FeatureFlags|Feature flag %{name} will be removed. Are you sure?"
msgstr ""
msgid "FeatureFlags|Feature flags allow you to configure your code into different flavors by dynamically toggling certain functionality."
msgstr ""
+msgid "FeatureFlags|Flag becomes read only soon"
+msgstr ""
+
msgid "FeatureFlags|Get started with feature flags"
msgstr ""
+msgid "FeatureFlags|GitLab is moving to a new way of managing feature flags, and in 13.4, this feature flag will become read-only. Please create a new feature flag."
+msgstr ""
+
msgid "FeatureFlags|ID"
msgstr ""
@@ -8975,6 +9359,9 @@ msgstr ""
msgid "FeatureFlags|Instance ID"
msgstr ""
+msgid "FeatureFlags|List details"
+msgstr ""
+
msgid "FeatureFlags|Loading feature flags"
msgstr ""
@@ -8990,9 +9377,15 @@ msgstr ""
msgid "FeatureFlags|New Feature Flag"
msgstr ""
+msgid "FeatureFlags|New Feature Flag User List"
+msgstr ""
+
msgid "FeatureFlags|New feature flag"
msgstr ""
+msgid "FeatureFlags|New list"
+msgstr ""
+
msgid "FeatureFlags|Percent rollout (logged in users)"
msgstr ""
@@ -9083,6 +9476,9 @@ msgstr ""
msgid "File deleted"
msgstr ""
+msgid "File format is no longer supported"
+msgstr ""
+
msgid "File hooks are similar to system hooks but are executed as files instead of sending data to a URL."
msgstr ""
@@ -9122,22 +9518,46 @@ msgstr ""
msgid "Filter"
msgstr ""
-msgid "Filter by %{issuable_type} that are currently archived."
-msgstr ""
-
msgid "Filter by %{issuable_type} that are currently closed."
msgstr ""
msgid "Filter by %{issuable_type} that are currently opened."
msgstr ""
+msgid "Filter by %{page_context_word} that are currently opened."
+msgstr ""
+
+msgid "Filter by Git revision"
+msgstr ""
+
msgid "Filter by commit message"
msgstr ""
+msgid "Filter by issues that are currently closed."
+msgstr ""
+
+msgid "Filter by label"
+msgstr ""
+
+msgid "Filter by merge requests that are currently closed and unmerged."
+msgstr ""
+
+msgid "Filter by merge requests that are currently merged."
+msgstr ""
+
msgid "Filter by milestone name"
msgstr ""
-msgid "Filter by name..."
+msgid "Filter by name"
+msgstr ""
+
+msgid "Filter by requirements that are currently archived."
+msgstr ""
+
+msgid "Filter by requirements that are currently opened."
+msgstr ""
+
+msgid "Filter by status"
msgstr ""
msgid "Filter by two-factor authentication"
@@ -9146,15 +9566,24 @@ msgstr ""
msgid "Filter by user"
msgstr ""
+msgid "Filter pipelines"
+msgstr ""
+
msgid "Filter projects"
msgstr ""
+msgid "Filter results"
+msgstr ""
+
msgid "Filter results by group"
msgstr ""
msgid "Filter results by project"
msgstr ""
+msgid "Filter results..."
+msgstr ""
+
msgid "Filter your projects by name"
msgstr ""
@@ -9413,15 +9842,15 @@ msgstr ""
msgid "Geo"
msgstr ""
-msgid "Geo Designs"
-msgstr ""
-
msgid "Geo Nodes"
msgstr ""
msgid "Geo Nodes|Cannot remove a primary node if there is a secondary node"
msgstr ""
+msgid "Geo Replication"
+msgstr ""
+
msgid "Geo Settings"
msgstr ""
@@ -9443,10 +9872,10 @@ msgstr ""
msgid "GeoNodes|Checksummed"
msgstr ""
-msgid "GeoNodes|Container repositories"
+msgid "GeoNodes|Consult Geo troubleshooting information"
msgstr ""
-msgid "GeoNodes|Data is out of date from %{timeago}"
+msgid "GeoNodes|Container repositories"
msgstr ""
msgid "GeoNodes|Data replication lag"
@@ -9488,6 +9917,9 @@ msgstr ""
msgid "GeoNodes|Last event ID seen from primary"
msgstr ""
+msgid "GeoNodes|Learn more about Geo node statuses"
+msgstr ""
+
msgid "GeoNodes|Loading nodes"
msgstr ""
@@ -9503,6 +9935,9 @@ msgstr ""
msgid "GeoNodes|Node was successfully removed."
msgstr ""
+msgid "GeoNodes|Node's status was updated %{timeAgo}."
+msgstr ""
+
msgid "GeoNodes|Not checksummed"
msgstr ""
@@ -9533,7 +9968,10 @@ msgstr ""
msgid "GeoNodes|Repository verification progress"
msgstr ""
-msgid "GeoNodes|Selective"
+msgid "GeoNodes|Selective (%{syncLabel})"
+msgstr ""
+
+msgid "GeoNodes|Selective synchronization"
msgstr ""
msgid "GeoNodes|Something went wrong while changing node status"
@@ -9560,6 +9998,9 @@ msgstr ""
msgid "GeoNodes|Unverified"
msgstr ""
+msgid "GeoNodes|Updated %{timeAgo}"
+msgstr ""
+
msgid "GeoNodes|Used slots"
msgstr ""
@@ -9596,19 +10037,19 @@ msgstr ""
msgid "Geo|%{name} is scheduled for re-verify"
msgstr ""
-msgid "Geo|All"
+msgid "Geo|Adjust your filters/search criteria above. If you believe this may be an error, please refer to the %{linkStart}Geo Troubleshooting%{linkEnd} documentation for more information."
msgstr ""
-msgid "Geo|All projects"
+msgid "Geo|All %{replicable_name}"
msgstr ""
-msgid "Geo|All projects are being scheduled for re-sync"
+msgid "Geo|All projects"
msgstr ""
-msgid "Geo|All projects are being scheduled for re-verify"
+msgid "Geo|All projects are being scheduled for resync"
msgstr ""
-msgid "Geo|Batch operations"
+msgid "Geo|All projects are being scheduled for reverify"
msgstr ""
msgid "Geo|Could not remove tracking entry for an existing project."
@@ -9620,9 +10061,15 @@ msgstr ""
msgid "Geo|Failed"
msgstr ""
+msgid "Geo|Filter by status"
+msgstr ""
+
msgid "Geo|Geo Status"
msgstr ""
+msgid "Geo|In progress"
+msgstr ""
+
msgid "Geo|In sync"
msgstr ""
@@ -9647,9 +10094,6 @@ msgstr ""
msgid "Geo|Not synced yet"
msgstr ""
-msgid "Geo|Pending"
-msgstr ""
-
msgid "Geo|Pending synchronization"
msgstr ""
@@ -9686,7 +10130,7 @@ msgstr ""
msgid "Geo|Resync"
msgstr ""
-msgid "Geo|Resync all projects"
+msgid "Geo|Resync all"
msgstr ""
msgid "Geo|Retry count"
@@ -9695,7 +10139,10 @@ msgstr ""
msgid "Geo|Reverify"
msgstr ""
-msgid "Geo|Reverify all projects"
+msgid "Geo|Reverify all"
+msgstr ""
+
+msgid "Geo|Review replication status, and resynchronize and reverify items with the primary node."
msgstr ""
msgid "Geo|Status"
@@ -9716,6 +10163,9 @@ msgstr ""
msgid "Geo|The node is currently %{minutes_behind} behind the primary node."
msgstr ""
+msgid "Geo|There are no %{replicable_type} to show"
+msgstr ""
+
msgid "Geo|Tracking database entry will be removed. Are you sure?"
msgstr ""
@@ -10037,9 +10487,6 @@ msgstr ""
msgid "Go to file"
msgstr ""
-msgid "Go to file (MRs only)"
-msgstr ""
-
msgid "Go to file permalink (while viewing a file)"
msgstr ""
@@ -10181,6 +10628,12 @@ msgstr ""
msgid "Group"
msgstr ""
+msgid "Group %{group_name} couldn't be exported."
+msgstr ""
+
+msgid "Group %{group_name} was exported successfully."
+msgstr ""
+
msgid "Group %{group_name} was scheduled for deletion."
msgstr ""
@@ -10232,10 +10685,13 @@ msgstr ""
msgid "Group export could not be started."
msgstr ""
+msgid "Group export error"
+msgstr ""
+
msgid "Group export link has expired. Please generate a new export from your group settings."
msgstr ""
-msgid "Group export started."
+msgid "Group export started. A download link will be sent by email and made available on this page."
msgstr ""
msgid "Group has been already marked for deletion"
@@ -10256,7 +10712,7 @@ msgstr ""
msgid "Group name"
msgstr ""
-msgid "Group name (Your organization)"
+msgid "Group name (your organization)"
msgstr ""
msgid "Group overview"
@@ -10274,12 +10730,18 @@ msgstr ""
msgid "Group pipeline minutes were successfully reset."
msgstr ""
+msgid "Group project URLs are prefixed with the group namespace"
+msgstr ""
+
msgid "Group requires separate account"
msgstr ""
msgid "Group variables (inherited)"
msgstr ""
+msgid "Group was exported"
+msgstr ""
+
msgid "Group was successfully updated."
msgstr ""
@@ -10346,9 +10808,6 @@ msgstr ""
msgid "GroupSAML|Enforce SSO-only authentication for this group."
msgstr ""
-msgid "GroupSAML|Enforce SSO-only membership for this group."
-msgstr ""
-
msgid "GroupSAML|Enforce users to have dedicated group managed accounts for this group."
msgstr ""
@@ -10484,6 +10943,9 @@ msgstr ""
msgid "GroupSettings|Disable group mentions"
msgstr ""
+msgid "GroupSettings|Export group"
+msgstr ""
+
msgid "GroupSettings|If the parent group's visibility is lower than the group current visibility, visibility levels for subgroups and projects will be changed to match the new parent group's visibility."
msgstr ""
@@ -10562,6 +11024,9 @@ msgstr ""
msgid "Groups (%{count})"
msgstr ""
+msgid "Groups (%{groups})"
+msgstr ""
+
msgid "Groups can also be nested by creating %{subgroup_docs_link_start}subgroups%{subgroup_docs_link_end}."
msgstr ""
@@ -10649,6 +11114,9 @@ msgstr ""
msgid "Hashed repository storage paths"
msgstr ""
+msgid "Hashed storage can't be disabled anymore for new projects"
+msgstr ""
+
msgid "Have your users email"
msgstr ""
@@ -10658,6 +11126,9 @@ msgstr ""
msgid "Header message"
msgstr ""
+msgid "Headings"
+msgstr ""
+
msgid "Health"
msgstr ""
@@ -10827,15 +11298,12 @@ msgstr ""
msgid "ID:"
msgstr ""
-msgid "IDE|Allow live previews of JavaScript projects in the Web IDE using CodeSandbox client side evaluation."
+msgid "IDE|Allow live previews of JavaScript projects in the Web IDE using CodeSandbox Live Preview."
msgstr ""
msgid "IDE|Back"
msgstr ""
-msgid "IDE|Client side evaluation"
-msgstr ""
-
msgid "IDE|Commit"
msgstr ""
@@ -10935,12 +11403,21 @@ msgstr ""
msgid "If using GitHub, you’ll see pipeline statuses on GitHub for your commits and pull requests. %{more_info_link}"
msgstr ""
-msgid "If you believe this may be an error, please refer to the %{linkStart}Geo Troubleshooting%{linkEnd} documentation for more information."
+msgid "If you did not recently sign in, you should immediately %{password_link_start}change your password%{password_link_end}."
+msgstr ""
+
+msgid "If you did not recently sign in, you should immediately change your password: %{password_link}."
msgstr ""
msgid "If you lose your recovery codes you can generate new ones, invalidating all previous codes."
msgstr ""
+msgid "If you reach 100%% storage capacity, you will not be able to: %{base_message}"
+msgstr ""
+
+msgid "If you recently signed in and recognize the IP address, you may disregard this email."
+msgstr ""
+
msgid "If your HTTP repository is not publicly accessible, add your credentials."
msgstr ""
@@ -10971,6 +11448,9 @@ msgstr ""
msgid "ImageViewerDimensions|W"
msgstr ""
+msgid "Impersonation Tokens"
+msgstr ""
+
msgid "Impersonation has been disabled"
msgstr ""
@@ -11007,6 +11487,9 @@ msgstr ""
msgid "Import in progress"
msgstr ""
+msgid "Import in progress. Refresh page to see newly added issues."
+msgstr ""
+
msgid "Import issues"
msgstr ""
@@ -11103,10 +11586,10 @@ msgstr ""
msgid "Improve Merge Requests and customer support with GitLab Enterprise Edition."
msgstr ""
-msgid "Improve issues management with Issue weight and GitLab Enterprise Edition."
+msgid "Improve search with Advanced Global Search and GitLab Enterprise Edition."
msgstr ""
-msgid "Improve search with Advanced Global Search and GitLab Enterprise Edition."
+msgid "In %{time_to_now}"
msgstr ""
msgid "In order to enable instance-level analytics, please ask an admin to enable %{usage_ping_link_start}usage ping%{usage_ping_link_end}."
@@ -11121,6 +11604,9 @@ msgstr ""
msgid "In order to tailor your experience with GitLab we<br>would like to know a bit more about you."
msgstr ""
+msgid "In progress"
+msgstr ""
+
msgid "In the next step, you'll be able to select the projects you want to import."
msgstr ""
@@ -11136,6 +11622,9 @@ msgstr ""
msgid "Include author name in notification email body"
msgstr ""
+msgid "Include description in commit message"
+msgstr ""
+
msgid "Include merge request description"
msgstr ""
@@ -11169,6 +11658,9 @@ msgstr ""
msgid "Incompatible options set!"
msgstr ""
+msgid "Indent"
+msgstr ""
+
msgid "Index all projects"
msgstr ""
@@ -11193,18 +11685,30 @@ msgstr ""
msgid "Input your repository URL"
msgstr ""
+msgid "Insert a code block"
+msgstr ""
+
msgid "Insert a quote"
msgstr ""
msgid "Insert code"
msgstr ""
+msgid "Insert inline code"
+msgstr ""
+
msgid "Insert suggestion"
msgstr ""
msgid "Insights"
msgstr ""
+msgid "Insights|Some items are not visible beacuse the project was filtered out in the insights.yml file (see the projects.only config for more information)."
+msgstr ""
+
+msgid "Insights|This project is filtered out in the insights.yml file (see the projects.only config for more information)."
+msgstr ""
+
msgid "Install"
msgstr ""
@@ -11255,6 +11759,30 @@ msgstr ""
msgid "Integrations allow you to integrate GitLab with other applications"
msgstr ""
+msgid "Integrations|All details"
+msgstr ""
+
+msgid "Integrations|Comment detail:"
+msgstr ""
+
+msgid "Integrations|Comment settings:"
+msgstr ""
+
+msgid "Integrations|Enable comments"
+msgstr ""
+
+msgid "Integrations|Includes Standard plus entire commit message, commit hash, and issue IDs"
+msgstr ""
+
+msgid "Integrations|Includes commit title and branch"
+msgstr ""
+
+msgid "Integrations|Standard"
+msgstr ""
+
+msgid "Integrations|When a Jira issue is mentioned in a commit or merge request a remote link and comment (if enabled) will be created."
+msgstr ""
+
msgid "Interested parties can even contribute by pushing commits if they want to."
msgstr ""
@@ -11294,6 +11822,12 @@ msgstr ""
msgid "Invalid URL"
msgstr ""
+msgid "Invalid container_name"
+msgstr ""
+
+msgid "Invalid cursor parameter"
+msgstr ""
+
msgid "Invalid cursor value provided"
msgstr ""
@@ -11330,18 +11864,27 @@ msgstr ""
msgid "Invalid pin code"
msgstr ""
+msgid "Invalid pod_name"
+msgstr ""
+
msgid "Invalid query"
msgstr ""
msgid "Invalid repository path"
msgstr ""
+msgid "Invalid search parameter"
+msgstr ""
+
msgid "Invalid server response"
msgstr ""
msgid "Invalid start or end time format"
msgstr ""
+msgid "Invalid status"
+msgstr ""
+
msgid "Invalid two-factor code."
msgstr ""
@@ -11468,15 +12011,15 @@ msgstr ""
msgid "Issues"
msgstr ""
-msgid "Issues / Merge Requests"
-msgstr ""
-
msgid "Issues Analytics"
msgstr ""
msgid "Issues Rate Limits"
msgstr ""
+msgid "Issues and Merge Requests"
+msgstr ""
+
msgid "Issues can be bugs, tasks or ideas to be discussed. Also, issues are searchable and filterable."
msgstr ""
@@ -11486,6 +12029,9 @@ msgstr ""
msgid "Issues referenced by merge requests and commits within the default branch will be closed automatically"
msgstr ""
+msgid "Issues successfully imported with the label"
+msgstr ""
+
msgid "Issues with comments, merge requests with diffs and comments, labels, milestones, snippets, and other project entities"
msgstr ""
@@ -11498,10 +12044,10 @@ msgstr ""
msgid "IssuesAnalytics|Avg/Month:"
msgstr ""
-msgid "IssuesAnalytics|Issues created"
+msgid "IssuesAnalytics|Issues opened"
msgstr ""
-msgid "IssuesAnalytics|Issues created per month"
+msgid "IssuesAnalytics|Issues opened per month"
msgstr ""
msgid "IssuesAnalytics|Last 12 months"
@@ -11531,6 +12077,18 @@ msgstr ""
msgid "It's you"
msgstr ""
+msgid "Iterations"
+msgstr ""
+
+msgid "Iteration|Dates cannot overlap with other existing Iterations"
+msgstr ""
+
+msgid "Iteration|cannot be in the past"
+msgstr ""
+
+msgid "Iteration|cannot be more than 500 years in the future"
+msgstr ""
+
msgid "Jaeger URL"
msgstr ""
@@ -11558,6 +12116,12 @@ msgstr ""
msgid "Jira project: %{importProject}"
msgstr ""
+msgid "JiraService| on branch %{branch_link}"
+msgstr ""
+
+msgid "JiraService|%{user_link} mentioned this issue in %{entity_link} of %{project_link}%{branch}:{quote}%{entity_message}{quote}"
+msgstr ""
+
msgid "JiraService|Events for %{noteable_model_name} are disabled."
msgstr ""
@@ -11720,6 +12284,9 @@ msgstr ""
msgid "Just me"
msgstr ""
+msgid "Keep divergent refs"
+msgstr ""
+
msgid "Key"
msgstr ""
@@ -11899,6 +12466,9 @@ msgstr ""
msgid "Last reply by"
msgstr ""
+msgid "Last repository check (%{last_check_timestamp}) failed. See the 'repocheck.log' file for error messages."
+msgstr ""
+
msgid "Last repository check run"
msgstr ""
@@ -11947,6 +12517,9 @@ msgstr ""
msgid "Lead"
msgstr ""
+msgid "Lead Time"
+msgstr ""
+
msgid "Learn GitLab"
msgstr ""
@@ -11980,6 +12553,9 @@ msgstr ""
msgid "Learn more about Web Terminal"
msgstr ""
+msgid "Learn more about X.509 signed commits"
+msgstr ""
+
msgid "Learn more about adding certificates to your project by following the %{docs_link_start}documentation on GitLab Pages%{docs_link_end}."
msgstr ""
@@ -12001,9 +12577,6 @@ msgstr ""
msgid "Learn more about the dependency list"
msgstr ""
-msgid "Learn more about x509 signed commits"
-msgstr ""
-
msgid "Learn more in the"
msgstr ""
@@ -12210,6 +12783,9 @@ msgstr ""
msgid "License|License"
msgstr ""
+msgid "License|Licensed user count exceeded"
+msgstr ""
+
msgid "License|You can restore access to the Gold features at any time by upgrading."
msgstr ""
@@ -12225,6 +12801,9 @@ msgstr ""
msgid "License|Your free trial of GitLab Ultimate expired on %{trial_ends_on}."
msgstr ""
+msgid "License|Your instance has exceeded your subscription's number of licensed users by %{extra_users_count}. You can continue to add more users and we'll include the overage in your next bill."
+msgstr ""
+
msgid "Limit display of time tracking units to hours."
msgstr ""
@@ -12293,9 +12872,6 @@ msgstr ""
msgid "Live preview"
msgstr ""
-msgid "Load more vulnerabilities"
-msgstr ""
-
msgid "Loading"
msgstr ""
@@ -12437,18 +13013,15 @@ msgstr ""
msgid "Make sure you're logged into the account that owns the projects you'd like to import."
msgstr ""
-msgid "Makes this issue confidential."
+msgid "Make this epic confidential"
msgstr ""
-msgid "Malformed string"
+msgid "Makes this issue confidential."
msgstr ""
msgid "Manage"
msgstr ""
-msgid "Manage Git repositories with fine-grained access controls that keep your code secure. Perform code reviews and enhance collaboration with merge requests. Each project can also have an issue tracker and a wiki."
-msgstr ""
-
msgid "Manage Web IDE features"
msgstr ""
@@ -12470,6 +13043,9 @@ msgstr ""
msgid "Manage labels"
msgstr ""
+msgid "Manage milestones"
+msgstr ""
+
msgid "Manage project labels"
msgstr ""
@@ -12479,6 +13055,9 @@ msgstr ""
msgid "Manage your license"
msgstr ""
+msgid "Managed Account"
+msgstr ""
+
msgid "Manifest"
msgstr ""
@@ -12710,6 +13289,9 @@ msgstr ""
msgid "Members of <strong>%{project_name}</strong>"
msgstr ""
+msgid "Members of a group may only view projects they have permission to access"
+msgstr ""
+
msgid "Members with access to %{strong_start}%{group_name}%{strong_end}"
msgstr ""
@@ -12719,9 +13301,6 @@ msgstr ""
msgid "Memory Usage"
msgstr ""
-msgid "Memory limit exceeded while rendering template"
-msgstr ""
-
msgid "Merge"
msgstr ""
@@ -12734,6 +13313,9 @@ msgstr ""
msgid "Merge Request Approvals"
msgstr ""
+msgid "Merge Request Commits"
+msgstr ""
+
msgid "Merge Requests"
msgstr ""
@@ -12818,6 +13400,9 @@ msgstr ""
msgid "MergeRequests|Add a reply"
msgstr ""
+msgid "MergeRequests|An error occurred while checking whether another squash is in progress."
+msgstr ""
+
msgid "MergeRequests|An error occurred while saving the draft comment."
msgstr ""
@@ -12941,9 +13526,6 @@ msgstr ""
msgid "Metrics - Grafana"
msgstr ""
-msgid "Metrics - Influx"
-msgstr ""
-
msgid "Metrics - Prometheus"
msgstr ""
@@ -12977,6 +13559,12 @@ msgstr ""
msgid "Metrics::Dashboard::Annotation|You are not authorized to delete this annotation"
msgstr ""
+msgid "Metrics::UsersStarredDashboards|Dashboard with requested path can not be found"
+msgstr ""
+
+msgid "Metrics::UsersStarredDashboards|You are not authorized to add star to this dashboard"
+msgstr ""
+
msgid "Metrics|Add metric"
msgstr ""
@@ -13012,9 +13600,15 @@ msgid_plural "Metrics|Edit metrics"
msgstr[0] ""
msgstr[1] ""
+msgid "Metrics|Expand panel"
+msgstr ""
+
msgid "Metrics|For grouping similar metrics"
msgstr ""
+msgid "Metrics|Go back (Esc)"
+msgstr ""
+
msgid "Metrics|Invalid time range, please verify."
msgstr ""
@@ -13027,6 +13621,9 @@ msgstr ""
msgid "Metrics|Link contains an invalid time window, please verify the link to see the requested time range."
msgstr ""
+msgid "Metrics|Link contains invalid chart information, please verify the link to see the expanded panel."
+msgstr ""
+
msgid "Metrics|Max"
msgstr ""
@@ -13045,6 +13642,9 @@ msgstr ""
msgid "Metrics|Refresh dashboard"
msgstr ""
+msgid "Metrics|Star dashboard"
+msgstr ""
+
msgid "Metrics|There was an error creating the dashboard."
msgstr ""
@@ -13081,6 +13681,9 @@ msgstr ""
msgid "Metrics|Unit label"
msgstr ""
+msgid "Metrics|Unstar dashboard"
+msgstr ""
+
msgid "Metrics|Used as a title for the chart"
msgstr ""
@@ -13152,6 +13755,12 @@ msgstr ""
msgid "Milestones| You’re about to permanently delete the milestone %{milestoneTitle}. This milestone is not currently used in any issues or merge requests."
msgstr ""
+msgid "Milestones|Close Milestone"
+msgstr ""
+
+msgid "Milestones|Completed Issues (closed)"
+msgstr ""
+
msgid "Milestones|Delete milestone"
msgstr ""
@@ -13161,21 +13770,39 @@ msgstr ""
msgid "Milestones|Failed to delete milestone %{milestoneTitle}"
msgstr ""
+msgid "Milestones|Group Milestone"
+msgstr ""
+
msgid "Milestones|Milestone %{milestoneTitle} was not found"
msgstr ""
+msgid "Milestones|Ongoing Issues (open and assigned)"
+msgstr ""
+
+msgid "Milestones|Project Milestone"
+msgstr ""
+
msgid "Milestones|Promote %{milestoneTitle} to group milestone?"
msgstr ""
msgid "Milestones|Promote Milestone"
msgstr ""
+msgid "Milestones|Promote to Group Milestone"
+msgstr ""
+
msgid "Milestones|Promoting %{milestoneTitle} will make it available for all projects inside %{groupName}. Existing project milestones with the same title will be merged."
msgstr ""
+msgid "Milestones|Reopen Milestone"
+msgstr ""
+
msgid "Milestones|This action cannot be reversed."
msgstr ""
+msgid "Milestones|Unstarted Issues (open and unassigned)"
+msgstr ""
+
msgid "Minimum capacity to be available before we schedule more mirrors preemptively."
msgstr ""
@@ -13359,6 +13986,9 @@ msgstr ""
msgid "Multiple uploaders found: %{uploader_types}"
msgstr ""
+msgid "My Awesome Group"
+msgstr ""
+
msgid "My company or team"
msgstr ""
@@ -13419,6 +14049,18 @@ msgstr ""
msgid "Network"
msgstr ""
+msgid "NetworkPolicies|Environment does not have deployment platform"
+msgstr ""
+
+msgid "NetworkPolicies|Invalid or empty policy"
+msgstr ""
+
+msgid "NetworkPolicies|Kubernetes error: %{error}"
+msgstr ""
+
+msgid "NetworkPolicies|Something went wrong, unable to fetch policies"
+msgstr ""
+
msgid "Never"
msgstr ""
@@ -13448,6 +14090,9 @@ msgid_plural "New Issues"
msgstr[0] ""
msgstr[1] ""
+msgid "New Iteration"
+msgstr ""
+
msgid "New Jira import"
msgstr ""
@@ -13514,6 +14159,9 @@ msgstr ""
msgid "New issue title"
msgstr ""
+msgid "New iteration created"
+msgstr ""
+
msgid "New label"
msgstr ""
@@ -13571,10 +14219,13 @@ msgstr ""
msgid "Next"
msgstr ""
-msgid "Next file in diff (MRs only)"
+msgid "Next commit"
+msgstr ""
+
+msgid "Next file in diff"
msgstr ""
-msgid "Next unresolved discussion (MRs only)"
+msgid "Next unresolved discussion"
msgstr ""
msgid "Nickname"
@@ -13589,18 +14240,9 @@ msgstr ""
msgid "No %{providerTitle} repositories found"
msgstr ""
-msgid "No %{replicableType} match this filter"
-msgstr ""
-
msgid "No Epic"
msgstr ""
-msgid "No Label"
-msgstr ""
-
-msgid "No Milestone"
-msgstr ""
-
msgid "No Scopes"
msgstr ""
@@ -13634,6 +14276,9 @@ msgstr ""
msgid "No changes between %{ref_start}%{source_branch}%{ref_end} and %{ref_start}%{target_branch}%{ref_end}"
msgstr ""
+msgid "No child epics match applied filters"
+msgstr ""
+
msgid "No connection could be made to a Gitaly Server, please check your logs!"
msgstr ""
@@ -13694,6 +14339,9 @@ msgstr ""
msgid "No jobs to show"
msgstr ""
+msgid "No label"
+msgstr ""
+
msgid "No labels with such name or description"
msgstr ""
@@ -13718,6 +14366,9 @@ msgstr ""
msgid "No messages were logged"
msgstr ""
+msgid "No milestone"
+msgstr ""
+
msgid "No milestones to show"
msgstr ""
@@ -13772,15 +14423,6 @@ msgstr ""
msgid "No thanks, don't show this again"
msgstr ""
-msgid "No vulnerabilities found for this group"
-msgstr ""
-
-msgid "No vulnerabilities found for this pipeline"
-msgstr ""
-
-msgid "No vulnerabilities found for this project"
-msgstr ""
-
msgid "No vulnerabilities present"
msgstr ""
@@ -13862,6 +14504,9 @@ msgstr ""
msgid "Note parameters are invalid: %{errors}"
msgstr ""
+msgid "Note that PostgreSQL 11 will become the minimum required PostgreSQL version in GitLab 13.0 (May 2020). PostgreSQL 9.6 and PostgreSQL 10 will no longer be supported in GitLab 13.0. Please consider upgrading your PostgreSQL version (%{db_version}) soon."
+msgstr ""
+
msgid "Note that this invitation was sent to %{mail_to_invite_email}, but you are signed in as %{link_to_current_user} with email %{mail_to_current_user}."
msgstr ""
@@ -13886,6 +14531,9 @@ msgstr ""
msgid "Notes|Collapse replies"
msgstr ""
+msgid "Notes|Private comments are accessible by internal staff only"
+msgstr ""
+
msgid "Notes|Show all activity"
msgstr ""
@@ -14003,6 +14651,9 @@ msgstr ""
msgid "Now you can access the merge request navigation tabs at the top, where they’re easier to find."
msgstr ""
+msgid "Nuget metadatum must have at least license_url, project_url or icon_url set"
+msgstr ""
+
msgid "Number of %{itemTitle}"
msgstr ""
@@ -14179,9 +14830,6 @@ msgstr ""
msgid "Open sidebar"
msgstr ""
-msgid "Open source software to collaborate on code"
-msgstr ""
-
msgid "Open: %{openIssuesCount}"
msgstr ""
@@ -14206,6 +14854,9 @@ msgstr ""
msgid "Operation failed. Check pod logs for %{pod_name} for more details."
msgstr ""
+msgid "Operation not allowed"
+msgstr ""
+
msgid "Operation timed out. Check pod logs for %{pod_name} for more details."
msgstr ""
@@ -14284,6 +14935,9 @@ msgstr ""
msgid "OutdatedBrowser|You can provide feedback %{feedback_link_start}on this issue%{feedback_link_end} or via your usual support channels."
msgstr ""
+msgid "Outdent"
+msgstr ""
+
msgid "Overridden"
msgstr ""
@@ -14323,6 +14977,12 @@ msgstr ""
msgid "Package type must be Maven"
msgstr ""
+msgid "Package type must be NuGet"
+msgstr ""
+
+msgid "Package type must be PyPi"
+msgstr ""
+
msgid "Package was removed"
msgstr ""
@@ -14338,6 +14998,9 @@ msgstr ""
msgid "PackageRegistry|Conan Command"
msgstr ""
+msgid "PackageRegistry|Copy .pypirc content"
+msgstr ""
+
msgid "PackageRegistry|Copy Conan Command"
msgstr ""
@@ -14359,6 +15022,9 @@ msgstr ""
msgid "PackageRegistry|Copy NuGet Setup Command"
msgstr ""
+msgid "PackageRegistry|Copy Pip command"
+msgstr ""
+
msgid "PackageRegistry|Copy and paste this inside your %{codeStart}pom.xml%{codeEnd} %{codeStart}dependencies%{codeEnd} block."
msgstr ""
@@ -14392,12 +15058,21 @@ msgstr ""
msgid "PackageRegistry|For more information on the NuGet registry, %{linkStart}see the documentation%{linkEnd}."
msgstr ""
+msgid "PackageRegistry|For more information on the PyPi registry, %{linkStart}see the documentation%{linkEnd}."
+msgstr ""
+
+msgid "PackageRegistry|If you haven't already done so, you will need to add the below to your %{codeStart}.pypirc%{codeEnd} file."
+msgstr ""
+
msgid "PackageRegistry|If you haven't already done so, you will need to add the below to your %{codeStart}pom.xml%{codeEnd} file."
msgstr ""
msgid "PackageRegistry|Installation"
msgstr ""
+msgid "PackageRegistry|Is your favorite package manager missing? We'd love your help in building first-class support for it into GitLab! %{contributionLinkStart}Visit the contribution documentation%{contributionLinkEnd} to learn more about how to build support for new package managers into GitLab. Below is a list of package managers that are on our radar."
+msgstr ""
+
msgid "PackageRegistry|Learn how to %{noPackagesLinkStart}publish and share your packages%{noPackagesLinkEnd} with GitLab."
msgstr ""
@@ -14416,18 +15091,27 @@ msgstr ""
msgid "PackageRegistry|NPM"
msgstr ""
+msgid "PackageRegistry|No upcoming issues"
+msgstr ""
+
msgid "PackageRegistry|NuGet"
msgstr ""
msgid "PackageRegistry|NuGet Command"
msgstr ""
+msgid "PackageRegistry|Pip Command"
+msgstr ""
+
msgid "PackageRegistry|Pipeline %{linkStart}%{linkEnd} triggered %{timestamp} by %{author}"
msgstr ""
msgid "PackageRegistry|Published to the repository at %{timestamp}"
msgstr ""
+msgid "PackageRegistry|PyPi"
+msgstr ""
+
msgid "PackageRegistry|Registry Setup"
msgstr ""
@@ -14440,18 +15124,30 @@ msgstr ""
msgid "PackageRegistry|There are no %{packageType} packages yet"
msgstr ""
+msgid "PackageRegistry|There are no other versions of this package."
+msgstr ""
+
msgid "PackageRegistry|There are no packages yet"
msgstr ""
+msgid "PackageRegistry|There are no upcoming issues to display."
+msgstr ""
+
msgid "PackageRegistry|There was a problem fetching the details for this package."
msgstr ""
msgid "PackageRegistry|To widen your search, change or remove the filters above."
msgstr ""
+msgid "PackageRegistry|Unable to fetch package version information."
+msgstr ""
+
msgid "PackageRegistry|Unable to load package"
msgstr ""
+msgid "PackageRegistry|Upcoming package managers"
+msgstr ""
+
msgid "PackageRegistry|You are about to delete <b>%{packageName}</b>, this operation is irreversible, are you sure?"
msgstr ""
@@ -14482,9 +15178,15 @@ msgstr ""
msgid "PackageType|NuGet"
msgstr ""
+msgid "PackageType|PyPi"
+msgstr ""
+
msgid "Packages"
msgstr ""
+msgid "Packages & Registries"
+msgstr ""
+
msgid "Page not found"
msgstr ""
@@ -14572,9 +15274,15 @@ msgstr ""
msgid "Password was successfully updated. Please login with it"
msgstr ""
+msgid "Passwords should be unique and not used for any other sites or services."
+msgstr ""
+
msgid "Past due"
msgstr ""
+msgid "Paste a machine public key here. Read more about how to generate it"
+msgstr ""
+
msgid "Paste a machine public key here. Read more about how to generate it %{link_start}here%{link_end}"
msgstr ""
@@ -14590,9 +15298,6 @@ msgstr ""
msgid "Path"
msgstr ""
-msgid "Path, transfer, remove"
-msgstr ""
-
msgid "Path:"
msgstr ""
@@ -14623,7 +15328,7 @@ msgstr ""
msgid "Percentage"
msgstr ""
-msgid "Perform advanced options such as changing path, transferring, or removing the group."
+msgid "Perform advanced options such as changing path, transferring, exporting, or removing the group."
msgstr ""
msgid "Perform common operations on GitLab project"
@@ -14680,9 +15385,6 @@ msgstr ""
msgid "Pick a name"
msgstr ""
-msgid "Pick a name for the application, and we'll give you a unique %{type} token."
-msgstr ""
-
msgid "Pin code"
msgstr ""
@@ -14704,9 +15406,6 @@ msgstr ""
msgid "Pipeline minutes quota"
msgstr ""
-msgid "Pipeline quota"
-msgstr ""
-
msgid "Pipeline subscriptions"
msgstr ""
@@ -14716,6 +15415,9 @@ msgstr ""
msgid "Pipeline: %{status}"
msgstr ""
+msgid "PipelineCharts|CI / CD Analytics"
+msgstr ""
+
msgid "PipelineCharts|Failed:"
msgstr ""
@@ -14791,16 +15493,10 @@ msgstr ""
msgid "Pipelines for merge requests are configured. A detached pipeline runs in the context of the merge request, and not against the merged result. Learn more in the documentation for Pipelines for Merged Results."
msgstr ""
-msgid "Pipelines settings for '%{project_name}' were successfully updated."
+msgid "Pipelines must succeed for merge requests to be eligible to merge. Please enable pipelines for this project to continue. For more information, see the %{linkStart}documentation.%{linkEnd}"
msgstr ""
-msgid "Pipelines| to purchase more minutes."
-msgstr ""
-
-msgid "Pipelines|%{namespace_name} has exceeded its pipeline minutes quota."
-msgstr ""
-
-msgid "Pipelines|%{namespace_name} has less than %{notification_level}%% of CI minutes available."
+msgid "Pipelines settings for '%{project_name}' were successfully updated."
msgstr ""
msgid "Pipelines|API"
@@ -14824,10 +15520,13 @@ msgstr ""
msgid "Pipelines|Get started with Pipelines"
msgstr ""
-msgid "Pipelines|Loading Pipelines"
+msgid "Pipelines|Group %{namespace_name} has %{percentage}%% or less Shared Runner Pipeline minutes remaining. Once it runs out, no new jobs or pipelines in its projects will run."
msgstr ""
-msgid "Pipelines|Pipelines will not run anymore on shared Runners."
+msgid "Pipelines|Group %{namespace_name} has exceeded its pipeline minutes quota. Unless you buy additional pipeline minutes, no new jobs or pipelines in its projects will run."
+msgstr ""
+
+msgid "Pipelines|Loading Pipelines"
msgstr ""
msgid "Pipelines|Project cache successfully reset."
@@ -14857,10 +15556,13 @@ msgstr ""
msgid "Pipelines|parent"
msgstr ""
+msgid "Pipeline|Branch name"
+msgstr ""
+
msgid "Pipeline|Commit"
msgstr ""
-msgid "Pipeline|Could not retrieve the pipeline status. For troubleshooting steps, read the %{linkStart}documentation.%{linkEnd}"
+msgid "Pipeline|Could not retrieve the pipeline status. For troubleshooting steps, read the %{linkStart}documentation%{linkEnd}."
msgstr ""
msgid "Pipeline|Coverage"
@@ -14887,12 +15589,18 @@ msgstr ""
msgid "Pipeline|Merged result pipeline"
msgstr ""
+msgid "Pipeline|No pipeline has been run for this commit."
+msgstr ""
+
msgid "Pipeline|Pipeline"
msgstr ""
msgid "Pipeline|Pipelines"
msgstr ""
+msgid "Pipeline|Raw text search is not currently supported. Please use the available search tokens."
+msgstr ""
+
msgid "Pipeline|Run Pipeline"
msgstr ""
@@ -14917,6 +15625,9 @@ msgstr ""
msgid "Pipeline|Stop pipeline #%{pipelineId}?"
msgstr ""
+msgid "Pipeline|Trigger author"
+msgstr ""
+
msgid "Pipeline|Triggerer"
msgstr ""
@@ -15106,6 +15817,9 @@ msgstr ""
msgid "Please wait while we import the repository for you. Refresh at will."
msgstr ""
+msgid "Plugins directory is deprecated and will be removed in 14.0. Please move this file into /file_hooks directory."
+msgstr ""
+
msgid "Pod does not exist"
msgstr ""
@@ -15118,6 +15832,9 @@ msgstr ""
msgid "Point to any links you like: documentation, built binaries, or other related materials. These can be internal or external links from your GitLab instance. Duplicate URLs are not allowed."
msgstr ""
+msgid "Pre-defined push rules."
+msgstr ""
+
msgid "Preferences"
msgstr ""
@@ -15208,6 +15925,9 @@ msgstr ""
msgid "Press %{key}-C to copy"
msgstr ""
+msgid "Prev"
+msgstr ""
+
msgid "Prevent adding new members to project membership within this group"
msgstr ""
@@ -15223,7 +15943,7 @@ msgstr ""
msgid "Prevent users from changing their profile name"
msgstr ""
-msgid "Prevent users from modifing merge request approvers list"
+msgid "Prevent users from modifying merge request approvers list"
msgstr ""
msgid "Prevent users from performing write operations on GitLab while performing maintenance."
@@ -15244,10 +15964,13 @@ msgstr ""
msgid "Previous Artifacts"
msgstr ""
-msgid "Previous file in diff (MRs only)"
+msgid "Previous commit"
+msgstr ""
+
+msgid "Previous file in diff"
msgstr ""
-msgid "Previous unresolved discussion (MRs only)"
+msgid "Previous unresolved discussion"
msgstr ""
msgid "Primary"
@@ -15463,9 +16186,6 @@ msgstr ""
msgid "Profiles|Give your individual key a title"
msgstr ""
-msgid "Profiles|Impersonation"
-msgstr ""
-
msgid "Profiles|Include private contributions on my profile"
msgstr ""
@@ -15511,9 +16231,6 @@ msgstr ""
msgid "Profiles|Path"
msgstr ""
-msgid "Profiles|Personal Access"
-msgstr ""
-
msgid "Profiles|Position and size your new avatar"
msgstr ""
@@ -15658,12 +16375,6 @@ msgstr ""
msgid "Profiles|e.g. My MacBook key"
msgstr ""
-msgid "Profiles|impersonation"
-msgstr ""
-
-msgid "Profiles|personal access"
-msgstr ""
-
msgid "Profiles|username"
msgstr ""
@@ -15694,6 +16405,9 @@ msgstr ""
msgid "Project %{project_repo} could not be found"
msgstr ""
+msgid "Project & Group can not be assigned at the same time"
+msgstr ""
+
msgid "Project '%{project_name}' is being imported."
msgstr ""
@@ -15715,6 +16429,9 @@ msgstr ""
msgid "Project '%{project_name}' will be deleted on %{date}"
msgstr ""
+msgid "Project Access Tokens"
+msgstr ""
+
msgid "Project Audit Events"
msgstr ""
@@ -15745,6 +16462,9 @@ msgstr ""
msgid "Project cannot be shared with the group it is in or one of its ancestors."
msgstr ""
+msgid "Project clone URL"
+msgstr ""
+
msgid "Project configuration, including services"
msgstr ""
@@ -15769,7 +16489,7 @@ msgstr ""
msgid "Project export link has expired. Please generate a new export from your project settings."
msgstr ""
-msgid "Project export started. A download link will be sent by email."
+msgid "Project export started. A download link will be sent by email and made available on this page."
msgstr ""
msgid "Project has too many %{label_for_message} to search"
@@ -15892,9 +16612,6 @@ msgstr ""
msgid "ProjectService|Comment will be posted on each event"
msgstr ""
-msgid "ProjectService|Last edit"
-msgstr ""
-
msgid "ProjectService|Perform common operations on GitLab project: %{project_name}"
msgstr ""
@@ -16060,6 +16777,9 @@ msgstr ""
msgid "ProjectSettings|Share code pastes with others out of Git repository"
msgstr ""
+msgid "ProjectSettings|Show default award emojis"
+msgstr ""
+
msgid "ProjectSettings|Show link to create/view merge request when pushing from the command line"
msgstr ""
@@ -16105,6 +16825,9 @@ msgstr ""
msgid "ProjectSettings|When conflicts arise the user is given the option to rebase"
msgstr ""
+msgid "ProjectSettings|When enabled, issues, merge requests, and snippets will always show thumbs-up and thumbs-down award emoji buttons."
+msgstr ""
+
msgid "ProjectSettings|Wiki"
msgstr ""
@@ -16429,9 +17152,6 @@ msgstr ""
msgid "Promote these project milestones into a group milestone."
msgstr ""
-msgid "Promote to Group Milestone"
-msgstr ""
-
msgid "Promote to group label"
msgstr ""
@@ -16450,30 +17170,78 @@ msgstr ""
msgid "Promoted issue to an epic."
msgstr ""
+msgid "Promotions|Burndown Charts are visual representations of the progress of completing a milestone. At a glance, you see the current state for the completion a given milestone. Without them, you would have to organize the data from the milestone and plot it yourself to have the same sense of progress."
+msgstr ""
+
+msgid "Promotions|Buy EE"
+msgstr ""
+
+msgid "Promotions|Buy GitLab Enterprise Edition"
+msgstr ""
+
+msgid "Promotions|Contact an owner of group %{namespace_name} to upgrade the plan."
+msgstr ""
+
+msgid "Promotions|Contact owner %{link_start}%{owner_name}%{link_end} to upgrade the plan."
+msgstr ""
+
+msgid "Promotions|Contact your Administrator to upgrade your license."
+msgstr ""
+
+msgid "Promotions|Dismiss burndown charts promotion"
+msgstr ""
+
msgid "Promotions|Don't show me this again"
msgstr ""
msgid "Promotions|Epics let you manage your portfolio of projects more efficiently and with less effort by tracking groups of issues that share a theme, across projects and milestones."
msgstr ""
+msgid "Promotions|Improve issues management with Issue weight and GitLab Enterprise Edition."
+msgstr ""
+
+msgid "Promotions|Improve milestones with Burndown Charts."
+msgstr ""
+
msgid "Promotions|Learn more"
msgstr ""
msgid "Promotions|See the other features in the %{subscription_link_start}bronze plan%{subscription_link_end}"
msgstr ""
+msgid "Promotions|Start GitLab Ultimate trial"
+msgstr ""
+
msgid "Promotions|This feature is locked."
msgstr ""
+msgid "Promotions|Track activity with Contribution Analytics."
+msgstr ""
+
msgid "Promotions|Upgrade plan"
msgstr ""
+msgid "Promotions|Upgrade your plan"
+msgstr ""
+
+msgid "Promotions|Upgrade your plan to activate Contribution Analytics."
+msgstr ""
+
+msgid "Promotions|Upgrade your plan to improve milestones with Burndown Charts."
+msgstr ""
+
+msgid "Promotions|Weight"
+msgstr ""
+
msgid "Promotions|Weighting your issue"
msgstr ""
msgid "Promotions|When you have a lot of issues, it can be hard to get an overview. By adding a weight to your issues, you can get a better idea of the effort, cost, required time, or value of each, and so better manage them."
msgstr ""
+msgid "Promotions|With Contribution Analytics you can have an overview for the activity of issues, merge requests, and push events of your organization and its members."
+msgstr ""
+
msgid "Prompt users to upload SSH keys"
msgstr ""
@@ -16591,6 +17359,9 @@ msgstr ""
msgid "Protip:"
msgstr ""
+msgid "Protocol"
+msgstr ""
+
msgid "Provider"
msgstr ""
@@ -16818,6 +17589,9 @@ msgid_plural "Refreshing in %d seconds to show the updated status..."
msgstr[0] ""
msgstr[1] ""
+msgid "Regenerate export"
+msgstr ""
+
msgid "Regenerate instance ID"
msgstr ""
@@ -16863,6 +17637,21 @@ msgstr ""
msgid "Registration"
msgstr ""
+msgid "Registration|Checkout"
+msgstr ""
+
+msgid "Registration|Your GitLab group"
+msgstr ""
+
+msgid "Registration|Your first project"
+msgstr ""
+
+msgid "Registration|Your profile"
+msgstr ""
+
+msgid "Rejected (closed)"
+msgstr ""
+
msgid "Related Deployed Jobs"
msgstr ""
@@ -17129,6 +17918,9 @@ msgstr ""
msgid "Reopen"
msgstr ""
+msgid "Reopen %{display_issuable_type}"
+msgstr ""
+
msgid "Reopen epic"
msgstr ""
@@ -17159,6 +17951,9 @@ msgstr ""
msgid "Replaces the clone URL root."
msgstr ""
+msgid "Replication"
+msgstr ""
+
msgid "Reply by email"
msgstr ""
@@ -17174,9 +17969,15 @@ msgstr ""
msgid "Repo by URL"
msgstr ""
+msgid "Report %{display_issuable_type} that are abusive, inappropriate or spam."
+msgstr ""
+
msgid "Report Type: %{report_type}"
msgstr ""
+msgid "Report abuse"
+msgstr ""
+
msgid "Report abuse to admin"
msgstr ""
@@ -17195,9 +17996,29 @@ msgstr ""
msgid "Reports|%{combinedString} and %{resolvedString}"
msgstr ""
+msgid "Reports|Accessibility scanning detected %d issue for the source branch only"
+msgid_plural "Reports|Accessibility scanning detected %d issues for the source branch only"
+msgstr[0] ""
+msgstr[1] ""
+
+msgid "Reports|Accessibility scanning detected no issues for the source branch only"
+msgstr ""
+
+msgid "Reports|Accessibility scanning failed loading results"
+msgstr ""
+
+msgid "Reports|Accessibility scanning results are being parsed"
+msgstr ""
+
msgid "Reports|Actions"
msgstr ""
+msgid "Reports|An error occured while loading report"
+msgstr ""
+
+msgid "Reports|An error occurred while loading %{name} results"
+msgstr ""
+
msgid "Reports|Class"
msgstr ""
@@ -17258,6 +18079,9 @@ msgstr ""
msgid "Repository URL"
msgstr ""
+msgid "Repository check"
+msgstr ""
+
msgid "Repository check was triggered."
msgstr ""
@@ -17294,6 +18118,9 @@ msgstr ""
msgid "Request Access"
msgstr ""
+msgid "Request Headers"
+msgstr ""
+
msgid "Request parameter %{param} is missing."
msgstr ""
@@ -17303,7 +18130,7 @@ msgstr ""
msgid "Requested %{time_ago}"
msgstr ""
-msgid "Requested design version does not exist"
+msgid "Requested design version does not exist."
msgstr ""
msgid "Requested states are invalid"
@@ -17433,6 +18260,12 @@ msgstr ""
msgid "Response"
msgstr ""
+msgid "Response Headers"
+msgstr ""
+
+msgid "Response Status"
+msgstr ""
+
msgid "Response didn't include `service_desk_address`"
msgstr ""
@@ -17487,6 +18320,9 @@ msgstr ""
msgid "Resync"
msgstr ""
+msgid "Resync all"
+msgstr ""
+
msgid "Resync all %{replicableType}"
msgstr ""
@@ -17555,6 +18391,9 @@ msgstr ""
msgid "Revoked personal access token %{personal_access_token_name}!"
msgstr ""
+msgid "Revoked project access token %{project_access_token_name}!"
+msgstr ""
+
msgid "RightSidebar|adding a"
msgstr ""
@@ -17573,6 +18412,9 @@ msgstr ""
msgid "Rook"
msgstr ""
+msgid "Rules that define what git pushes are accepted for a project in this group. All newly created projects in this group will use these settings."
+msgstr ""
+
msgid "Rules that define what git pushes are accepted for a project. All newly created projects will use these settings."
msgstr ""
@@ -17603,6 +18445,9 @@ msgstr ""
msgid "Runner token"
msgstr ""
+msgid "Runner tokens"
+msgstr ""
+
msgid "Runner was not updated."
msgstr ""
@@ -17657,6 +18502,9 @@ msgstr ""
msgid "SAML SSO for %{group_name}"
msgstr ""
+msgid "SAML discovery tokens"
+msgstr ""
+
msgid "SAML for %{group_name}"
msgstr ""
@@ -17801,9 +18649,15 @@ msgstr ""
msgid "Search Button"
msgstr ""
+msgid "Search Milestones"
+msgstr ""
+
msgid "Search an environment spec"
msgstr ""
+msgid "Search authors"
+msgstr ""
+
msgid "Search branches"
msgstr ""
@@ -17958,11 +18812,6 @@ msgid_plural "SearchResults|snippets"
msgstr[0] ""
msgstr[1] ""
-msgid "SearchResults|snippet result"
-msgid_plural "SearchResults|snippet results"
-msgstr[0] ""
-msgstr[1] ""
-
msgid "SearchResults|user"
msgid_plural "SearchResults|users"
msgstr[0] ""
@@ -17988,6 +18837,9 @@ msgstr ""
msgid "Seats in license"
msgstr ""
+msgid "Secondary"
+msgstr ""
+
msgid "Secret"
msgstr ""
@@ -18003,208 +18855,232 @@ msgstr ""
msgid "Security Dashboard"
msgstr ""
-msgid "Security Dashboard|Error fetching the vulnerability counts. Please check your network connection and try again."
+msgid "Security configuration help link"
msgstr ""
-msgid "Security Dashboard|Error fetching the vulnerability list. Please check your network connection and try again."
+msgid "Security dashboard"
msgstr ""
-msgid "Security Dashboard|Issue Created"
+msgid "Security report is out of date. Please update your branch with the latest changes from the target branch (%{targetBranchName})"
msgstr ""
-msgid "Security Reports|Comment added to '%{vulnerabilityName}'"
+msgid "Security report is out of date. Run %{newPipelineLinkStart}a new pipeline%{newPipelineLinkEnd} for the target branch (%{targetBranchName})"
msgstr ""
-msgid "Security Reports|Comment deleted on '%{vulnerabilityName}'"
+msgid "SecurityConfiguration|Enabled"
msgstr ""
-msgid "Security Reports|Comment edited on '%{vulnerabilityName}'"
+msgid "SecurityConfiguration|Feature"
msgstr ""
-msgid "Security Reports|Create issue"
+msgid "SecurityConfiguration|Feature documentation for %{featureName}"
msgstr ""
-msgid "Security Reports|Dismiss Selected"
+msgid "SecurityConfiguration|Not yet enabled"
msgstr ""
-msgid "Security Reports|Dismiss vulnerability"
+msgid "SecurityConfiguration|Secure features"
msgstr ""
-msgid "Security Reports|Dismissed '%{vulnerabilityName}'"
+msgid "SecurityConfiguration|Status"
msgstr ""
-msgid "Security Reports|Dismissed '%{vulnerabilityName}'. Turn off the hide dismissed toggle to view."
+msgid "SecurityReports|%{firstProject} and %{secondProject}"
msgstr ""
-msgid "Security Reports|Either you don't have permission to view this dashboard or the dashboard has not been setup. Please check your permission settings with your administrator or check your dashboard configurations to proceed."
+msgid "SecurityReports|%{firstProject}, %{secondProject}, and %{rest}"
msgstr ""
-msgid "Security Reports|False positive"
+msgid "SecurityReports|Add a project to your dashboard"
msgstr ""
-msgid "Security Reports|Learn more about setting up your dashboard"
+msgid "SecurityReports|Add or remove projects from your dashboard"
msgstr ""
-msgid "Security Reports|More info"
+msgid "SecurityReports|Add projects"
msgstr ""
-msgid "Security Reports|Oops, something doesn't seem right."
+msgid "SecurityReports|Comment added to '%{vulnerabilityName}'"
msgstr ""
-msgid "Security Reports|Security reports can only be accessed by authorized users."
+msgid "SecurityReports|Comment deleted on '%{vulnerabilityName}'"
msgstr ""
-msgid "Security Reports|Select a reason"
+msgid "SecurityReports|Comment edited on '%{vulnerabilityName}'"
msgstr ""
-msgid "Security Reports|There was an error adding the comment."
+msgid "SecurityReports|Create issue"
msgstr ""
-msgid "Security Reports|There was an error creating the issue."
+msgid "SecurityReports|Dismiss Selected"
msgstr ""
-msgid "Security Reports|There was an error creating the merge request."
+msgid "SecurityReports|Dismiss vulnerability"
msgstr ""
-msgid "Security Reports|There was an error deleting the comment."
+msgid "SecurityReports|Dismissed '%{vulnerabilityName}'"
msgstr ""
-msgid "Security Reports|There was an error dismissing the vulnerabilities."
+msgid "SecurityReports|Dismissed '%{vulnerabilityName}'. Turn off the hide dismissed toggle to view."
msgstr ""
-msgid "Security Reports|There was an error dismissing the vulnerability."
+msgid "SecurityReports|Each vulnerability now has a unique page that can be directly linked to, shared, referenced, and tracked as the single source of truth. Vulnerability occurrences also persist across scanner runs, which improves tracking and visibility and reduces duplicates between scans."
msgstr ""
-msgid "Security Reports|There was an error reverting the dismissal."
+msgid "SecurityReports|Edit dashboard"
msgstr ""
-msgid "Security Reports|There was an error reverting this dismissal."
+msgid "SecurityReports|Either you don't have permission to view this dashboard or the dashboard has not been setup. Please check your permission settings with your administrator or check your dashboard configurations to proceed."
msgstr ""
-msgid "Security Reports|Undo dismiss"
+msgid "SecurityReports|Error fetching the vulnerability counts. Please check your network connection and try again."
msgstr ""
-msgid "Security Reports|Won't fix / Accept risk"
+msgid "SecurityReports|Error fetching the vulnerability list. Please check your network connection and try again."
msgstr ""
-msgid "Security Reports|You do not have sufficient permissions to access this report"
+msgid "SecurityReports|False positive"
msgstr ""
-msgid "Security Reports|You must sign in as an authorized user to see this report"
+msgid "SecurityReports|Hide dismissed"
msgstr ""
-msgid "Security Reports|[No reason]"
+msgid "SecurityReports|Introducing standalone vulnerabilities"
msgstr ""
-msgid "Security configuration help link"
+msgid "SecurityReports|Issue Created"
msgstr ""
-msgid "Security dashboard"
+msgid "SecurityReports|Learn More"
msgstr ""
-msgid "Security report is out of date. Please update your branch with the latest changes from the target branch (%{targetBranchName})"
+msgid "SecurityReports|Learn more about setting up your dashboard"
msgstr ""
-msgid "Security report is out of date. Run %{newPipelineLinkStart}a new pipeline%{newPipelineLinkEnd} for the target branch (%{targetBranchName})"
+msgid "SecurityReports|Load more vulnerabilities"
msgstr ""
-msgid "SecurityConfiguration|Configured"
+msgid "SecurityReports|Monitor vulnerabilities in your code"
msgstr ""
-msgid "SecurityConfiguration|Feature"
+msgid "SecurityReports|More info"
msgstr ""
-msgid "SecurityConfiguration|Feature documentation for %{featureName}"
+msgid "SecurityReports|More information"
msgstr ""
-msgid "SecurityConfiguration|Not yet configured"
+msgid "SecurityReports|No vulnerabilities found for dashboard"
msgstr ""
-msgid "SecurityConfiguration|Secure features"
+msgid "SecurityReports|No vulnerabilities found for this group"
msgstr ""
-msgid "SecurityConfiguration|Status"
+msgid "SecurityReports|No vulnerabilities found for this pipeline"
+msgstr ""
+
+msgid "SecurityReports|No vulnerabilities found for this project"
+msgstr ""
+
+msgid "SecurityReports|Oops, something doesn't seem right."
+msgstr ""
+
+msgid "SecurityReports|Pipeline %{pipelineLink} triggered %{timeago} by %{user}"
+msgstr ""
+
+msgid "SecurityReports|Project"
+msgstr ""
+
+msgid "SecurityReports|Projects added"
+msgstr ""
+
+msgid "SecurityReports|Remove project from dashboard"
+msgstr ""
+
+msgid "SecurityReports|Report type"
+msgstr ""
+
+msgid "SecurityReports|Return to dashboard"
msgstr ""
-msgid "SecurityDashboard|%{firstProject} and %{secondProject}"
+msgid "SecurityReports|Security Dashboard"
msgstr ""
-msgid "SecurityDashboard|%{firstProject}, %{secondProject}, and %{rest}"
+msgid "SecurityReports|Security reports can only be accessed by authorized users."
msgstr ""
-msgid "SecurityDashboard|Add a project to your dashboard"
+msgid "SecurityReports|Select a project to add by using the project search field above."
msgstr ""
-msgid "SecurityDashboard|Add or remove projects from your dashboard"
+msgid "SecurityReports|Select a reason"
msgstr ""
-msgid "SecurityDashboard|Add projects"
+msgid "SecurityReports|Severity"
msgstr ""
-msgid "SecurityDashboard|Each vulnerability now has a unique page that can be directly linked to, shared, referenced, and tracked as the single source of truth. Vulnerability occurrences also persist across scanner runs, which improves tracking and visibility and reduces duplicates between scans."
+msgid "SecurityReports|Status"
msgstr ""
-msgid "SecurityDashboard|Edit dashboard"
+msgid "SecurityReports|The security dashboard displays the latest security findings for projects you wish to monitor. Select \"Edit dashboard\" to add and remove projects."
msgstr ""
-msgid "SecurityDashboard|Hide dismissed"
+msgid "SecurityReports|The security dashboard displays the latest security report. Use it to find and fix vulnerabilities."
msgstr ""
-msgid "SecurityDashboard|Introducing standalone vulnerabilities"
+msgid "SecurityReports|There was an error adding the comment."
msgstr ""
-msgid "SecurityDashboard|Learn More"
+msgid "SecurityReports|There was an error creating the issue."
msgstr ""
-msgid "SecurityDashboard|Monitor vulnerabilities in your code"
+msgid "SecurityReports|There was an error creating the merge request."
msgstr ""
-msgid "SecurityDashboard|More information"
+msgid "SecurityReports|There was an error deleting the comment."
msgstr ""
-msgid "SecurityDashboard|No vulnerabilities found for dashboard"
+msgid "SecurityReports|There was an error dismissing the vulnerabilities."
msgstr ""
-msgid "SecurityDashboard|Pipeline %{pipelineLink} triggered %{timeago} by %{user}"
+msgid "SecurityReports|There was an error dismissing the vulnerability."
msgstr ""
-msgid "SecurityDashboard|Project"
+msgid "SecurityReports|There was an error reverting the dismissal."
msgstr ""
-msgid "SecurityDashboard|Projects added"
+msgid "SecurityReports|There was an error reverting this dismissal."
msgstr ""
-msgid "SecurityDashboard|Remove project from dashboard"
+msgid "SecurityReports|There was an error while generating the report."
msgstr ""
-msgid "SecurityDashboard|Report type"
+msgid "SecurityReports|Unable to add %{invalidProjects}"
msgstr ""
-msgid "SecurityDashboard|Return to dashboard"
+msgid "SecurityReports|Undo dismiss"
msgstr ""
-msgid "SecurityDashboard|Security Dashboard"
+msgid "SecurityReports|While it's rare to have no vulnerabilities for your group, it can happen. In any event, we ask that you double check your settings to make sure you've set up your dashboard correctly."
msgstr ""
-msgid "SecurityDashboard|Select a project to add by using the project search field above."
+msgid "SecurityReports|While it's rare to have no vulnerabilities for your pipeline, it can happen. In any event, we ask that you double check your settings to make sure all security scanning jobs have passed successfully."
msgstr ""
-msgid "SecurityDashboard|Severity"
+msgid "SecurityReports|While it's rare to have no vulnerabilities for your project, it can happen. In any event, we ask that you double check your settings to make sure you've set up your dashboard correctly."
msgstr ""
-msgid "SecurityDashboard|Status"
+msgid "SecurityReports|While it's rare to have no vulnerabilities, it can happen. In any event, we ask that you please double check your settings to make sure you've set up your dashboard correctly."
msgstr ""
-msgid "SecurityDashboard|The security dashboard displays the latest security findings for projects you wish to monitor. Select \"Edit dashboard\" to add and remove projects."
+msgid "SecurityReports|Won't fix / Accept risk"
msgstr ""
-msgid "SecurityDashboard|The security dashboard displays the latest security report. Use it to find and fix vulnerabilities."
+msgid "SecurityReports|You do not have sufficient permissions to access this report"
msgstr ""
-msgid "SecurityDashboard|There was an error while generating the report."
+msgid "SecurityReports|You must sign in as an authorized user to see this report"
msgstr ""
-msgid "SecurityDashboard|Unable to add %{invalidProjects}"
+msgid "SecurityReports|[No reason]"
msgstr ""
msgid "See GitLab's %{password_policy_guidelines}"
@@ -18216,12 +19092,18 @@ msgstr ""
msgid "See the affected projects in the GitLab admin panel"
msgstr ""
+msgid "See what's new at GitLab"
+msgstr ""
+
msgid "Select"
msgstr ""
msgid "Select Archive Format"
msgstr ""
+msgid "Select Git revision"
+msgstr ""
+
msgid "Select GitLab project to link with your Slack team"
msgstr ""
@@ -18273,6 +19155,9 @@ msgstr ""
msgid "Select an existing Kubernetes cluster or create a new one"
msgstr ""
+msgid "Select assignee"
+msgstr ""
+
msgid "Select branch"
msgstr ""
@@ -18324,9 +19209,15 @@ msgstr ""
msgid "Select source branch"
msgstr ""
+msgid "Select status"
+msgstr ""
+
msgid "Select strategy activation method"
msgstr ""
+msgid "Select subscription"
+msgstr ""
+
msgid "Select target branch"
msgstr ""
@@ -18399,6 +19290,12 @@ msgstr ""
msgid "Send email"
msgstr ""
+msgid "Send email notification"
+msgstr ""
+
+msgid "Send message"
+msgstr ""
+
msgid "Send report"
msgstr ""
@@ -18705,6 +19602,9 @@ msgstr ""
msgid "Severity: %{severity}"
msgstr ""
+msgid "Shards (%{shards})"
+msgstr ""
+
msgid "Shards to synchronize"
msgstr ""
@@ -18738,15 +19638,15 @@ msgstr ""
msgid "Should you ever lose your phone or access to your one time password secret, each of these recovery codes can be used one time each to regain access to your account. Please save them in a safe place, or you %{b_start}will%{b_end} lose access to your account."
msgstr ""
-msgid "Show all %{issuable_type}."
-msgstr ""
-
msgid "Show all activity"
msgstr ""
msgid "Show all members"
msgstr ""
+msgid "Show all requirements."
+msgstr ""
+
msgid "Show archived projects"
msgstr ""
@@ -18983,6 +19883,9 @@ msgstr ""
msgid "Snippets"
msgstr ""
+msgid "Snippets with non-text files can only be edited via Git."
+msgstr ""
+
msgid "SnippetsEmptyState|Code snippets"
msgstr ""
@@ -19019,6 +19922,9 @@ msgstr ""
msgid "Snowplow"
msgstr ""
+msgid "Some child epics may be hidden due to applied filters"
+msgstr ""
+
msgid "Some email servers do not support overriding the email sender name. Enable this option to include the name of the author of the issue, merge request or comment in the email body instead."
msgstr ""
@@ -19115,6 +20021,9 @@ msgstr ""
msgid "Something went wrong while fetching related merge requests."
msgstr ""
+msgid "Something went wrong while fetching requirements count."
+msgstr ""
+
msgid "Something went wrong while fetching requirements list."
msgstr ""
@@ -19154,12 +20063,18 @@ msgstr ""
msgid "Something went wrong while stopping this environment. Please try again."
msgstr ""
+msgid "Something went wrong while toggling auto-fix settings, please try again later."
+msgstr ""
+
msgid "Something went wrong while updating a requirement."
msgstr ""
msgid "Something went wrong while updating your list settings"
msgstr ""
+msgid "Something went wrong with your automatic subscription renewal"
+msgstr ""
+
msgid "Something went wrong, unable to add %{project} to dashboard"
msgstr ""
@@ -19445,6 +20360,9 @@ msgstr ""
msgid "Stage removed"
msgstr ""
+msgid "Standard"
+msgstr ""
+
msgid "Star a label to make it a priority label. Order the prioritized labels to change their relative priority, by dragging."
msgstr ""
@@ -19478,9 +20396,6 @@ msgstr ""
msgid "Stars"
msgstr ""
-msgid "Start GitLab Ultimate trial"
-msgstr ""
-
msgid "Start Web Terminal"
msgstr ""
@@ -19562,6 +20477,9 @@ msgstr ""
msgid "Starts at (UTC)"
msgstr ""
+msgid "State"
+msgstr ""
+
msgid "State your message to activate"
msgstr ""
@@ -19661,6 +20579,12 @@ msgstr ""
msgid "StatusPage|Status page"
msgstr ""
+msgid "StatusPage|Status page URL"
+msgstr ""
+
+msgid "StatusPage|Status page frontend documentation"
+msgstr ""
+
msgid "StatusPage|To publish incidents to an external status page, GitLab will store a JSON file in your Amazon S3 account in a location accessible to your external status page service. Make sure to also set up %{docsLink}"
msgstr ""
@@ -19727,6 +20651,9 @@ msgstr ""
msgid "Subkeys"
msgstr ""
+msgid "Submit %{humanized_resource_name}"
+msgstr ""
+
msgid "Submit Changes"
msgstr ""
@@ -19907,9 +20834,15 @@ msgstr ""
msgid "Suggest code changes which can be immediately applied in one click. Try it out!"
msgstr ""
+msgid "Suggested Solutions"
+msgstr ""
+
msgid "Suggested change"
msgstr ""
+msgid "Suggested solutions help link"
+msgstr ""
+
msgid "SuggestedColors|Bright green"
msgstr ""
@@ -19994,6 +20927,9 @@ msgstr ""
msgid "Support page URL"
msgstr ""
+msgid "Survey Response"
+msgstr ""
+
msgid "Switch branch/tag"
msgstr ""
@@ -20168,6 +21104,9 @@ msgstr ""
msgid "Telephone number"
msgstr ""
+msgid "Telephone number (Optional)"
+msgstr ""
+
msgid "Template"
msgstr ""
@@ -20272,6 +21211,9 @@ msgstr ""
msgid "Thank you for signing up for your free trial! You will get additional instructions in your inbox shortly."
msgstr ""
+msgid "Thank you for your feedback!"
+msgstr ""
+
msgid "Thank you for your report. A GitLab administrator will look into it shortly."
msgstr ""
@@ -20355,9 +21297,6 @@ msgstr ""
msgid "The commit does not exist"
msgstr ""
-msgid "The configuration status of the table below only applies to the default branch and is based on the %{linkStart}latest pipeline%{linkEnd}. Once you've configured a scan for the default branch, any subsequent feature branch you create will include the scan."
-msgstr ""
-
msgid "The connection will time out after %{timeout}. For repositories that take longer, use a clone/push combination."
msgstr ""
@@ -20394,9 +21333,15 @@ msgstr ""
msgid "The domain you entered is not allowed."
msgstr ""
+msgid "The download link will expire in 24 hours."
+msgstr ""
+
msgid "The entered user map is not a valid JSON user map."
msgstr ""
+msgid "The errors we encountered were:"
+msgstr ""
+
msgid "The file has been successfully created."
msgstr ""
@@ -20435,6 +21380,9 @@ msgstr ""
msgid "The group can be fully restored"
msgstr ""
+msgid "The group export can be downloaded from:"
+msgstr ""
+
msgid "The group has already been shared with this group"
msgstr ""
@@ -20474,6 +21422,9 @@ msgstr ""
msgid "The license was successfully uploaded and is now active. You can see the details below."
msgstr ""
+msgid "The license was successfully uploaded and will be active from %{starts_at}. You can see the details below."
+msgstr ""
+
msgid "The maximum file size allowed is %{size}."
msgstr ""
@@ -20606,6 +21557,9 @@ msgstr ""
msgid "The staging stage shows the time between merging the MR and deploying code to the production environment. The data will be automatically added once you deploy to production for the first time."
msgstr ""
+msgid "The status of the table below only applies to the default branch and is based on the %{linkStart}latest pipeline%{linkEnd}. Once you've enabled a scan for the default branch, any subsequent feature branch you create will include the scan."
+msgstr ""
+
msgid "The testing stage shows the time GitLab CI takes to run every pipeline for the related merge request. The data will automatically be added after your first pipeline finishes running."
msgstr ""
@@ -20654,6 +21608,9 @@ msgstr ""
msgid "The vulnerability is no longer detected. Verify the vulnerability has been remediated before changing its status."
msgstr ""
+msgid "There are no %{replicableType} to show"
+msgstr ""
+
msgid "There are no GPG keys associated with this account."
msgstr ""
@@ -20729,6 +21686,12 @@ msgstr ""
msgid "There was a problem communicating with your device."
msgstr ""
+msgid "There was a problem fetching project branches."
+msgstr ""
+
+msgid "There was a problem fetching project users."
+msgstr ""
+
msgid "There was a problem refreshing the data, please try again"
msgstr ""
@@ -20864,6 +21827,9 @@ msgstr ""
msgid "There was an error while fetching value stream analytics recent activity data."
msgstr ""
+msgid "There was an error while fetching value stream analytics time summary data."
+msgstr ""
+
msgid "There was an error with the reCAPTCHA. Please solve the reCAPTCHA again."
msgstr ""
@@ -20876,6 +21842,9 @@ msgstr ""
msgid "They can be managed using the %{link}."
msgstr ""
+msgid "Third Party Advisory Link"
+msgstr ""
+
msgid "Third party offers"
msgstr ""
@@ -20912,6 +21881,9 @@ msgstr ""
msgid "This application will be able to:"
msgstr ""
+msgid "This attachment has been truncated to avoid exceeding the maximum allowed attachment size of 15MB. %{written_count} of %{issues_count} issues have been included. Consider re-exporting with a narrower selection of issues."
+msgstr ""
+
msgid "This block is self-referential"
msgstr ""
@@ -20942,6 +21914,9 @@ msgstr ""
msgid "This commit was signed with an <strong>unverified</strong> signature."
msgstr ""
+msgid "This content could not be displayed because %{reason}. You can %{options} instead."
+msgstr ""
+
msgid "This date is after the due date, so this epic won't appear in the roadmap."
msgstr ""
@@ -20981,6 +21956,9 @@ msgstr ""
msgid "This epic already has the maximum number of child epics."
msgstr ""
+msgid "This epic and its child elements will only be visible to team members with at minimum Reporter access."
+msgstr ""
+
msgid "This epic does not exist or you don't have sufficient permission."
msgstr ""
@@ -21026,9 +22004,6 @@ msgstr ""
msgid "This is a list of devices that have logged into your account. Revoke any sessions that you do not recognize."
msgstr ""
-msgid "This is a primary node"
-msgstr ""
-
msgid "This is a security log of important events involving your account."
msgstr ""
@@ -21053,6 +22028,12 @@ msgstr ""
msgid "This issue is confidential"
msgstr ""
+msgid "This issue is currently blocked by the following issues: %{issues}."
+msgstr ""
+
+msgid "This issue is in a child epic of the filtered epic"
+msgstr ""
+
msgid "This issue is locked."
msgstr ""
@@ -21161,6 +22142,9 @@ msgstr ""
msgid "This means you can not push code until you create an empty repository or import existing one."
msgstr ""
+msgid "This merge request does not have accessibility reports"
+msgstr ""
+
msgid "This merge request is locked."
msgstr ""
@@ -21203,6 +22187,9 @@ msgstr ""
msgid "This project does not have billing enabled. To create a cluster, <a href=%{linkToBilling} target=\"_blank\" rel=\"noopener noreferrer\">enable billing <i class=\"fa fa-external-link\" aria-hidden=\"true\"></i></a> and try again."
msgstr ""
+msgid "This project has no active access tokens."
+msgstr ""
+
msgid "This project is archived and cannot be commented on."
msgstr ""
@@ -21218,9 +22205,18 @@ msgstr ""
msgid "This repository"
msgstr ""
+msgid "This repository has never been checked."
+msgstr ""
+
msgid "This repository is currently empty. A new Auto DevOps pipeline will be created after a new file has been pushed to a branch."
msgstr ""
+msgid "This repository was last checked %{last_check_timestamp}. The check %{strong_start}failed.%{strong_end} See the 'repocheck.log' file for error messages."
+msgstr ""
+
+msgid "This repository was last checked %{last_check_timestamp}. The check passed."
+msgstr ""
+
msgid "This runner will only run on pipelines triggered on protected branches"
msgstr ""
@@ -21239,7 +22235,7 @@ msgstr ""
msgid "This user cannot be unlocked manually from GitLab"
msgstr ""
-msgid "This user has no active %{type} Tokens."
+msgid "This user has no active %{type}."
msgstr ""
msgid "This user has no identities"
@@ -21284,10 +22280,10 @@ msgstr ""
msgid "ThreatMonitoring|Application firewall not detected"
msgstr ""
-msgid "ThreatMonitoring|Container Network Policy"
+msgid "ThreatMonitoring|Container Network Policies are not installed or have been disabled. To view this data, ensure your Network Policies are installed and enabled for your cluster."
msgstr ""
-msgid "ThreatMonitoring|Container NetworkPolicies are not installed or has been disabled. To view this data, ensure you NetworkPolicies are installed and enabled for your cluster."
+msgid "ThreatMonitoring|Container Network Policy"
msgstr ""
msgid "ThreatMonitoring|Container NetworkPolicies not detected"
@@ -21320,7 +22316,7 @@ msgstr ""
msgid "ThreatMonitoring|Something went wrong, unable to fetch statistics"
msgstr ""
-msgid "ThreatMonitoring|The firewall is not installed or has been disabled. To view this data, ensure you firewall is installed and enabled for your cluster."
+msgid "ThreatMonitoring|The firewall is not installed or has been disabled. To view this data, ensure the web application firewall is installed and enabled for your cluster."
msgstr ""
msgid "ThreatMonitoring|The graph below is an overview of traffic coming to your application as tracked by the Web Application Firewall (WAF). View the docs for instructions on how to access the WAF logs to see what type of malicious traffic is trying to access your app. The docs link is also accessible by clicking the \"?\" icon next to the title below."
@@ -21554,6 +22550,9 @@ msgstr ""
msgid "Timeout"
msgstr ""
+msgid "Timeout connecting to the Google API. Please try again."
+msgstr ""
+
msgid "Time|hr"
msgid_plural "Time|hrs"
msgstr[0] ""
@@ -21576,7 +22575,7 @@ msgstr ""
msgid "Title:"
msgstr ""
-msgid "Titles and Filenames"
+msgid "Titles and Descriptions"
msgstr ""
msgid "To"
@@ -21618,6 +22617,12 @@ msgstr ""
msgid "To enable it and see User Cohorts, visit %{application_settings_link_start}application settings%{application_settings_link_end}."
msgstr ""
+msgid "To further protect your account, consider configuring a %{mfa_link_start}two-factor authentication%{mfa_link_end} method."
+msgstr ""
+
+msgid "To further protect your account, consider configuring a two-factor authentication method: %{mfa_link}."
+msgstr ""
+
msgid "To get started you enter your FogBugz URL and login information below. In the next steps, you'll be able to map users and select the projects you want to import."
msgstr ""
@@ -21738,6 +22743,9 @@ msgstr ""
msgid "Toggle commit list"
msgstr ""
+msgid "Toggle dropdown"
+msgstr ""
+
msgid "Toggle emoji award"
msgstr ""
@@ -21783,7 +22791,7 @@ msgstr ""
msgid "Too many projects enabled. You will need to manage them via the console or the API."
msgstr ""
-msgid "Topics"
+msgid "Topics (optional)"
msgstr ""
msgid "Total"
@@ -21795,15 +22803,9 @@ msgstr ""
msgid "Total artifacts size: %{total_size}"
msgstr ""
-msgid "Total cores (vCPUs)"
-msgstr ""
-
msgid "Total issues"
msgstr ""
-msgid "Total memory (GB)"
-msgstr ""
-
msgid "Total test time for all commits/merges"
msgstr ""
@@ -21819,9 +22821,6 @@ msgstr ""
msgid "Tracing"
msgstr ""
-msgid "Track activity with Contribution Analytics."
-msgstr ""
-
msgid "Track groups of issues that share a theme, across projects and milestones"
msgstr ""
@@ -21951,6 +22950,9 @@ msgstr ""
msgid "Try to fork again"
msgstr ""
+msgid "Try to keep the first line under 52 characters and the others under 72."
+msgstr ""
+
msgid "Try using a different search term to find the file you are looking for."
msgstr ""
@@ -22041,6 +23043,9 @@ msgstr ""
msgid "Unable to connect to server: %{error}"
msgstr ""
+msgid "Unable to connect to the Jira instance. Please check your Jira integration configuration."
+msgstr ""
+
msgid "Unable to convert Kubernetes logs encoding to UTF-8"
msgstr ""
@@ -22137,6 +23142,9 @@ msgstr ""
msgid "Unknown response text"
msgstr ""
+msgid "Unknown sign-in from new location"
+msgstr ""
+
msgid "Unlimited"
msgstr ""
@@ -22317,18 +23325,12 @@ msgstr ""
msgid "Upgrade plan to unlock Canary Deployments feature"
msgstr ""
-msgid "Upgrade your plan"
-msgstr ""
-
msgid "Upgrade your plan to activate Advanced Global Search."
msgstr ""
msgid "Upgrade your plan to activate Audit Events."
msgstr ""
-msgid "Upgrade your plan to activate Contribution Analytics."
-msgstr ""
-
msgid "Upgrade your plan to activate Group Webhooks."
msgstr ""
@@ -22470,7 +23472,7 @@ msgstr ""
msgid "Use hashed storage"
msgstr ""
-msgid "Use hashed storage paths for newly created and renamed repositories. Enable immutable, hash-based paths and repository names to store repositories on disk. This prevents repositories from having to be moved or renamed when the Repository URL changes and may improve disk I/O performance."
+msgid "Use hashed storage paths for newly created and renamed repositories. Enable immutable, hash-based paths and repository names to store repositories on disk. This prevents repositories from having to be moved or renamed when the Repository URL changes and may improve disk I/O performance. (Always enabled since 13.0)"
msgstr ""
msgid "Use one line per URI"
@@ -22794,6 +23796,12 @@ msgstr ""
msgid "UserProfile|Your projects can be available publicly, internally, or privately, at your choice."
msgstr ""
+msgid "UserProfile|at"
+msgstr ""
+
+msgid "UserProfile|made a private contribution"
+msgstr ""
+
msgid "Username (optional)"
msgstr ""
@@ -22881,7 +23889,10 @@ msgstr ""
msgid "Value Stream Analytics gives an overview of how much time it takes to go from idea to production in your project."
msgstr ""
-msgid "Var"
+msgid "ValueStreamAnalytics|%{days}d"
+msgstr ""
+
+msgid "Variable"
msgstr ""
msgid "Variable will be masked in job logs."
@@ -22923,6 +23934,9 @@ msgstr ""
msgid "Version"
msgstr ""
+msgid "Versions"
+msgstr ""
+
msgid "Very helpful"
msgstr ""
@@ -23113,9 +24127,6 @@ msgstr ""
msgid "Vulnerabilities over time"
msgstr ""
-msgid "Vulnerability List"
-msgstr ""
-
msgid "Vulnerability remediated. Review before resolving."
msgstr ""
@@ -23146,9 +24157,6 @@ msgstr ""
msgid "VulnerabilityManagement|Confirmed %{timeago} by %{user}"
msgstr ""
-msgid "VulnerabilityManagement|Create issue"
-msgstr ""
-
msgid "VulnerabilityManagement|Detected %{timeago} in pipeline %{pipelineLink}"
msgstr ""
@@ -23203,6 +24211,9 @@ msgstr ""
msgid "VulnerabilityStatusTypes|Resolved"
msgstr ""
+msgid "Vulnerability|%{scannerName} (version %{scannerVersion})"
+msgstr ""
+
msgid "Vulnerability|Class"
msgstr ""
@@ -23236,7 +24247,10 @@ msgstr ""
msgid "Vulnerability|Project"
msgstr ""
-msgid "Vulnerability|Report Type"
+msgid "Vulnerability|Scanner Provider"
+msgstr ""
+
+msgid "Vulnerability|Scanner Type"
msgstr ""
msgid "Vulnerability|Severity"
@@ -23251,6 +24265,9 @@ msgstr ""
msgid "Wait for the file to load to copy its contents"
msgstr ""
+msgid "Waiting for merge (open and assigned)"
+msgstr ""
+
msgid "Waiting for performance data"
msgstr ""
@@ -23287,12 +24304,24 @@ msgstr ""
msgid "We heard back from your U2F device. You have been authenticated."
msgstr ""
+msgid "We recommend that you buy more Pipeline minutes to avoid any interruption of service."
+msgstr ""
+
+msgid "We recommend that you buy more Pipeline minutes to resume normal service."
+msgstr ""
+
msgid "We sent you an email with reset password instructions"
msgstr ""
+msgid "We tried to automatically renew your %{strong}%{plan_name}%{strong_close} subscription for %{strong}%{namespace_name}%{strong_close} on %{expires_on} but something went wrong so your subscription was downgraded to the free plan. Don't worry, your data is safe. We suggest you check your payment method and get in touch with our support team (%{support_link}). They'll gladly help with your subscription renewal."
+msgstr ""
+
msgid "We want to be sure it is you, please confirm you are not a robot."
msgstr ""
+msgid "We will automatically renew your %{strong}%{plan_name}%{strong_close} subscription for %{strong}%{namespace_name}%{strong_close} on %{strong}%{expires_on}%{strong_close}. There's nothing that you need to do, we'll let you know when the renewal is complete. Need more seats, a higher plan or just want to review your payment method?"
+msgstr ""
+
msgid "We've found no vulnerabilities"
msgstr ""
@@ -23397,15 +24426,6 @@ msgstr ""
msgid "When:"
msgstr ""
-msgid "While it's rare to have no vulnerabilities for your group, it can happen. In any event, we ask that you double check your settings to make sure you've set up your dashboard correctly."
-msgstr ""
-
-msgid "While it's rare to have no vulnerabilities for your pipeline, it can happen. In any event, we ask that you double check your settings to make sure all security scanning jobs have passed successfully."
-msgstr ""
-
-msgid "While it's rare to have no vulnerabilities for your project, it can happen. In any event, we ask that you double check your settings to make sure you've set up your dashboard correctly."
-msgstr ""
-
msgid "While it's rare to have no vulnerabilities, it can happen. In any event, we ask that you please double check your settings to make sure you've set up your dashboard correctly."
msgstr ""
@@ -23436,9 +24456,6 @@ msgstr ""
msgid "Wiki"
msgstr ""
-msgid "Wiki pages"
-msgstr ""
-
msgid "Wiki was successfully updated."
msgstr ""
@@ -23451,9 +24468,6 @@ msgstr ""
msgid "WikiClone|Install Gollum"
msgstr ""
-msgid "WikiClone|It is recommended to install %{markdown} so that GFM features render locally:"
-msgstr ""
-
msgid "WikiClone|Start Gollum and edit locally"
msgstr ""
@@ -23574,10 +24588,10 @@ msgstr ""
msgid "Wiki|Wiki Pages"
msgstr ""
-msgid "Will deploy to"
+msgid "Will be created"
msgstr ""
-msgid "With contribution analytics you can have an overview for the activity of issues, merge requests and push events of your organization and its members."
+msgid "Will deploy to"
msgstr ""
msgid "Withdraw Access Request"
@@ -23586,6 +24600,9 @@ msgstr ""
msgid "Won't fix / Accept risk"
msgstr ""
+msgid "Work in progress (open and unassigned)"
+msgstr ""
+
msgid "Work in progress Limit"
msgstr ""
@@ -23622,6 +24639,9 @@ msgstr ""
msgid "Yes, add it"
msgstr ""
+msgid "Yes, close issue"
+msgstr ""
+
msgid "Yes, let me map Google Code users to full names or GitLab users."
msgstr ""
@@ -23703,6 +24723,9 @@ msgstr ""
msgid "You can also upload existing files from your computer using the instructions below."
msgstr ""
+msgid "You can also use project access tokens to authenticate against Git over HTTP."
+msgstr ""
+
msgid "You can always edit this later"
msgstr ""
@@ -23730,6 +24753,9 @@ msgstr ""
msgid "You can filter by 'days to merge' by clicking on the columns in the chart."
msgstr ""
+msgid "You can generate an access token scoped to this project for each application to use the GitLab API."
+msgstr ""
+
msgid "You can get started by cloning the repository or start adding files to it with one of the following options."
msgstr ""
@@ -23745,6 +24771,9 @@ msgstr ""
msgid "You can move around the graph by using the arrow keys."
msgstr ""
+msgid "You can notify the app / group or a project by sending them an email notification"
+msgstr ""
+
msgid "You can now export your security dashboard to a CSV report."
msgstr ""
@@ -23823,6 +24852,9 @@ msgstr ""
msgid "You didn't renew your %{strong}%{plan_name}%{strong_close} subscription so it was downgraded to the GitLab Core Plan."
msgstr ""
+msgid "You do not have an active license"
+msgstr ""
+
msgid "You do not have any subscriptions yet"
msgstr ""
@@ -23871,6 +24903,9 @@ msgstr ""
msgid "You don’t have access to Value Stream Analytics for this group"
msgstr ""
+msgid "You have a license(s) that activates at a future date. Please see the License History table below."
+msgstr ""
+
msgid "You have been granted %{access_level} access to the %{source_link} %{source_type}."
msgstr ""
@@ -23886,6 +24921,9 @@ msgstr ""
msgid "You have declined the invitation to join %{label}."
msgstr ""
+msgid "You have imported from this project %{numberOfPreviousImportsForProject} times before. Each new import will create duplicate issues."
+msgstr ""
+
msgid "You have no permissions"
msgstr ""
@@ -23967,6 +25005,9 @@ msgstr ""
msgid "You need to upload a Google Takeout archive."
msgstr ""
+msgid "You reached %{usage_in_percent} of %{namespace_name}'s capacity (%{used_storage} of %{storage_limit})"
+msgstr ""
+
msgid "You tried to fork %{link_to_the_project} but it failed for the following reason:"
msgstr ""
@@ -24000,6 +25041,9 @@ msgstr ""
msgid "You will receive notifications only for comments in which you were @mentioned"
msgstr ""
+msgid "You won't be able to create new projects because you have reached your project limit."
+msgstr ""
+
msgid "You won't be able to pull or push project code via %{protocol} until you %{set_password_link} on your account"
msgstr ""
@@ -24018,6 +25062,12 @@ msgstr ""
msgid "You're about to reduce the visibility of the project %{strong_start}%{project_name}%{strong_end}."
msgstr ""
+msgid "You're at the first commit"
+msgstr ""
+
+msgid "You're at the last commit"
+msgstr ""
+
msgid "You're not allowed to %{tag_start}edit%{tag_end} files in this project directly. Please fork this project, make your changes there, and submit a merge request."
msgstr ""
@@ -24051,12 +25101,21 @@ msgstr ""
msgid "YouTube"
msgstr ""
-msgid "Your %{strong}%{plan_name}%{strong_close} subscription for %{strong}%{namespace_name}%{strong_close} will expire on %{strong}%{expires_on}%{strong_close}. After that, you will not to be able to create issues or merge requests as well as many other features."
+msgid "Your %{strong}%{plan_name}%{strong_close} subscription for %{strong}%{namespace_name}%{strong_close} will expire on %{strong}%{expires_on}%{strong_close}."
msgstr ""
msgid "Your %{strong}%{plan_name}%{strong_close} subscription will expire on %{strong}%{expires_on}%{strong_close}. After that, you will not to be able to create issues or merge requests as well as many other features."
msgstr ""
+msgid "Your CSV export has started. It will be emailed to %{email} when complete."
+msgstr ""
+
+msgid "Your CSV export of %{issues_count} from project %{project_link} has been added to this email as an attachment."
+msgstr ""
+
+msgid "Your CSV export of %{written_count} from project %{project_name} (%{project_url}) has been added to this email as an attachment."
+msgstr ""
+
msgid "Your Commit Email will be used for web based operations, such as edits and merges."
msgstr ""
@@ -24078,9 +25137,6 @@ msgstr ""
msgid "Your Groups"
msgstr ""
-msgid "Your New Personal Access Token"
-msgstr ""
-
msgid "Your Personal Access Tokens will expire in %{days_to_expire} days or less"
msgstr ""
@@ -24153,6 +25209,9 @@ msgstr ""
msgid "Your comment could not be updated! Please check your network connection and try again."
msgstr ""
+msgid "Your comment will be discarded."
+msgstr ""
+
msgid "Your custom stage '%{title}' was created"
msgstr ""
@@ -24186,12 +25245,18 @@ msgstr ""
msgid "Your name"
msgstr ""
+msgid "Your new %{type}"
+msgstr ""
+
msgid "Your new SCIM token"
msgstr ""
msgid "Your new personal access token has been created."
msgstr ""
+msgid "Your new project access token has been created."
+msgstr ""
+
msgid "Your password isn't required to view this page. If a password or any other personal details are requested, please contact your administrator to report abuse."
msgstr ""
@@ -24210,6 +25275,9 @@ msgstr ""
msgid "Your request for access has been queued for review."
msgstr ""
+msgid "Your response has been recorded."
+msgstr ""
+
msgid "Your search didn't match any commits."
msgstr ""
@@ -24219,6 +25287,9 @@ msgstr ""
msgid "Your subscription has been downgraded"
msgstr ""
+msgid "Your subscription will automatically renew in %{remaining_days}"
+msgstr ""
+
msgid "Your subscription will expire in %{remaining_days}"
msgstr ""
@@ -24257,7 +25328,7 @@ msgstr ""
msgid "allowed to fail"
msgstr ""
-msgid "already being used for another group or project milestone."
+msgid "already being used for another group or project %{timebox_name}."
msgstr ""
msgid "already has a \"created\" issue link"
@@ -24465,6 +25536,9 @@ msgstr ""
msgid "ciReport|Failed to load %{reportName} report"
msgstr ""
+msgid "ciReport|Fixed"
+msgstr ""
+
msgid "ciReport|Fixed:"
msgstr ""
@@ -24483,6 +25557,9 @@ msgstr ""
msgid "ciReport|Manage licenses"
msgstr ""
+msgid "ciReport|New"
+msgstr ""
+
msgid "ciReport|No changes to code quality"
msgstr ""
@@ -24542,6 +25619,9 @@ msgstr[1] ""
msgid "ciReport|View full report"
msgstr ""
+msgid "closed issue"
+msgstr ""
+
msgid "comment"
msgstr ""
@@ -24560,6 +25640,9 @@ msgstr ""
msgid "connecting"
msgstr ""
+msgid "container_name can contain only lowercase letters, digits, '-', and '.' and must start and end with an alphanumeric character"
+msgstr ""
+
msgid "container_name cannot be larger than %{max_length} chars"
msgstr ""
@@ -24610,6 +25693,9 @@ msgstr ""
msgid "done"
msgstr ""
+msgid "download it"
+msgstr ""
+
msgid "draft"
msgid_plural "drafts"
msgstr[0] ""
@@ -24704,6 +25790,9 @@ msgstr ""
msgid "group"
msgstr ""
+msgid "groups"
+msgstr ""
+
msgid "has already been linked to another vulnerability"
msgstr ""
@@ -24722,6 +25811,12 @@ msgstr ""
msgid "image diff"
msgstr ""
+msgid "impersonation token"
+msgstr ""
+
+msgid "impersonation tokens"
+msgstr ""
+
msgid "import flow"
msgstr ""
@@ -24805,6 +25900,12 @@ msgstr ""
msgid "issues on track"
msgstr ""
+msgid "it is larger than %{limit}"
+msgstr ""
+
+msgid "it is stored as a job artifact"
+msgstr ""
+
msgid "it is stored externally"
msgstr ""
@@ -24844,6 +25945,9 @@ msgstr ""
msgid "limit of %{project_limit} reached"
msgstr ""
+msgid "load it anyway"
+msgstr ""
+
msgid "locked by %{path_lock_user_name} %{created_at}"
msgstr ""
@@ -24867,9 +25971,6 @@ msgstr[1] ""
msgid "merged %{time_ago}"
msgstr ""
-msgid "milestone should belong either to a project or a group."
-msgstr ""
-
msgid "missing"
msgstr ""
@@ -24975,7 +26076,7 @@ msgstr ""
msgid "mrWidget|Deployment statistics are not available currently"
msgstr ""
-msgid "mrWidget|Detect issues before deployment with a CI pipeline"
+msgid "mrWidget|Detect issues before deployment with a CI pipeline that continuously tests your code. We created a quick guide that will show you how to create one. Make your code more secure and more robust in just a minute."
msgstr ""
msgid "mrWidget|Did not close"
@@ -25155,9 +26256,6 @@ msgstr ""
msgid "mrWidget|Your password"
msgstr ""
-msgid "mrWidget|a quick guide that'll show you how to create"
-msgstr ""
-
msgid "mrWidget|branch does not exist."
msgstr ""
@@ -25167,15 +26265,6 @@ msgstr ""
msgid "mrWidget|into"
msgstr ""
-msgid "mrWidget|one. Make your code more secure and more"
-msgstr ""
-
-msgid "mrWidget|robust in just a minute."
-msgstr ""
-
-msgid "mrWidget|that continuously tests your code. We created"
-msgstr ""
-
msgid "mrWidget|to be added to the merge train when the pipeline succeeds"
msgstr ""
@@ -25188,6 +26277,9 @@ msgstr ""
msgid "must be greater than start date"
msgstr ""
+msgid "my-awesome-group"
+msgstr ""
+
msgid "n/a"
msgstr ""
@@ -25233,6 +26325,9 @@ msgstr ""
msgid "on track"
msgstr ""
+msgid "open issue"
+msgstr ""
+
msgid "opened %{timeAgoString} by %{user}"
msgstr ""
@@ -25264,9 +26359,23 @@ msgstr ""
msgid "per day"
msgstr ""
+msgid "personal access token"
+msgstr ""
+
+msgid "personal access tokens"
+msgstr ""
+
+msgid "personal project will be removed and cannot be restored"
+msgid_plural "%d personal projects will be removed and cannot be restored"
+msgstr[0] ""
+msgstr[1] ""
+
msgid "pipeline"
msgstr ""
+msgid "pod_name can contain only lowercase letters, digits, '-', and '.' and must start and end with an alphanumeric character"
+msgstr ""
+
msgid "pod_name cannot be larger than %{max_length} chars"
msgstr ""
@@ -25289,9 +26398,21 @@ msgid_plural "projects"
msgstr[0] ""
msgstr[1] ""
+msgid "project access token"
+msgstr ""
+
+msgid "project access tokens"
+msgstr ""
+
msgid "project avatar"
msgstr ""
+msgid "projects"
+msgstr ""
+
+msgid "push to your repository, create pipelines, create issues or add comments. To reduce storage capacity, delete unused repositories, artifacts, wikis, issues, and pipelines."
+msgstr ""
+
msgid "quick actions"
msgstr ""
@@ -25438,11 +26559,6 @@ msgstr ""
msgid "this document"
msgstr ""
-msgid "thread resolved"
-msgid_plural "threads resolved"
-msgstr[0] ""
-msgstr[1] ""
-
msgid "to help your contributors communicate effectively!"
msgstr ""
@@ -25470,6 +26586,9 @@ msgstr ""
msgid "updated %{time_ago}"
msgstr ""
+msgid "uploads"
+msgstr ""
+
msgid "user avatar"
msgstr ""
@@ -25497,6 +26616,9 @@ msgstr ""
msgid "view the blob"
msgstr ""
+msgid "view the source"
+msgstr ""
+
msgid "vulnerability|Add a comment"
msgstr ""
@@ -25524,6 +26646,9 @@ msgstr ""
msgid "wiki page"
msgstr ""
+msgid "will be released %{time}"
+msgstr ""
+
msgid "with %{additions} additions, %{deletions} deletions."
msgstr ""
diff --git a/locale/gl_ES/gitlab.po b/locale/gl_ES/gitlab.po
index 085f3b76a51..14066fd10dc 100644
--- a/locale/gl_ES/gitlab.po
+++ b/locale/gl_ES/gitlab.po
@@ -12,7 +12,7 @@ msgstr ""
"X-Crowdin-Project: gitlab-ee\n"
"X-Crowdin-Language: gl\n"
"X-Crowdin-File: /master/locale/gitlab.pot\n"
-"PO-Revision-Date: 2020-04-15 00:32\n"
+"PO-Revision-Date: 2020-05-05 21:34\n"
msgid " %{start} to %{end}"
msgstr ""
@@ -212,6 +212,11 @@ msgid_plural "%d tags"
msgstr[0] ""
msgstr[1] ""
+msgid "%d unresolved thread"
+msgid_plural "%d unresolved threads"
+msgstr[0] ""
+msgstr[1] ""
+
msgid "%d vulnerability dismissed"
msgid_plural "%d vulnerabilities dismissed"
msgstr[0] ""
@@ -371,6 +376,9 @@ msgstr ""
msgid "%{mrText}, this issue will be closed automatically."
msgstr ""
+msgid "%{namespace_name} is now read-only. You cannot: %{base_message}"
+msgstr ""
+
msgid "%{name} contained %{resultsString}"
msgstr ""
@@ -383,6 +391,11 @@ msgstr ""
msgid "%{name}'s avatar"
msgstr ""
+msgid "%{no_of_days} day"
+msgid_plural "%{no_of_days} days"
+msgstr[0] ""
+msgstr[1] ""
+
msgid "%{number_commits_behind} commits behind %{default_branch}, %{number_commits_ahead} commits ahead"
msgstr ""
@@ -506,6 +519,9 @@ msgstr[1] ""
msgid "%{text} is available"
msgstr ""
+msgid "%{timebox_name} should belong either to a project or a group."
+msgstr ""
+
msgid "%{title} %{operator} %{threshold}"
msgstr ""
@@ -781,12 +797,18 @@ msgstr ""
msgid "<code>\"johnsmith@example.com\": \"johnsmith@example.com\"</code> will add \"By <a href=\"#\">johnsmith@example.com</a>\" to all issues and comments originally created by johnsmith@example.com. By default, the email address or username is masked to ensure the user's privacy. Use this option if you want to show the full email address."
msgstr ""
+msgid "<namespace / project>"
+msgstr ""
+
msgid "<no name set>"
msgstr ""
msgid "<no scopes selected>"
msgstr ""
+msgid "<project name>"
+msgstr ""
+
msgid "<strong>%{group_name}</strong> group members"
msgstr ""
@@ -820,6 +842,9 @@ msgstr ""
msgid "A basic page and serverless function that uses AWS Lambda, AWS API Gateway, and GitLab Pages"
msgstr ""
+msgid "A complete DevOps platform"
+msgstr ""
+
msgid "A default branch cannot be chosen for an empty project."
msgstr ""
@@ -1036,6 +1061,12 @@ msgstr ""
msgid "AccessTokens|reset it"
msgstr ""
+msgid "AccessibilityReport|Accessibility report artifact not found"
+msgstr ""
+
+msgid "AccessibilityReport|Failed to retrieve accessibility report"
+msgstr ""
+
msgid "AccessibilityReport|Learn More"
msgstr ""
@@ -1045,6 +1076,9 @@ msgstr ""
msgid "AccessibilityReport|New"
msgstr ""
+msgid "AccessibilityReport|The accessibility scanning found an error of the following type: %{code}"
+msgstr ""
+
msgid "Account"
msgstr ""
@@ -1287,6 +1321,9 @@ msgstr ""
msgid "Added at"
msgstr ""
+msgid "Added for this merge request"
+msgstr ""
+
msgid "Added in this version"
msgstr ""
@@ -1658,6 +1695,72 @@ msgid_plural "Alerts"
msgstr[0] ""
msgstr[1] ""
+msgid "Alert Details"
+msgstr ""
+
+msgid "AlertManagement|Acknowledged"
+msgstr ""
+
+msgid "AlertManagement|Alert"
+msgstr ""
+
+msgid "AlertManagement|Authorize external service"
+msgstr ""
+
+msgid "AlertManagement|Display alerts from all your monitoring tools directly within GitLab. Streamline the investigation of your alerts and the escalation of alerts to incidents."
+msgstr ""
+
+msgid "AlertManagement|End time"
+msgstr ""
+
+msgid "AlertManagement|End time:"
+msgstr ""
+
+msgid "AlertManagement|Events"
+msgstr ""
+
+msgid "AlertManagement|Events:"
+msgstr ""
+
+msgid "AlertManagement|Full Alert Details"
+msgstr ""
+
+msgid "AlertManagement|More information"
+msgstr ""
+
+msgid "AlertManagement|No alerts available to display. If you think you're seeing this message in error, refresh the page."
+msgstr ""
+
+msgid "AlertManagement|No alerts to display."
+msgstr ""
+
+msgid "AlertManagement|Overview"
+msgstr ""
+
+msgid "AlertManagement|Resolved"
+msgstr ""
+
+msgid "AlertManagement|Severity"
+msgstr ""
+
+msgid "AlertManagement|Start time"
+msgstr ""
+
+msgid "AlertManagement|Start time:"
+msgstr ""
+
+msgid "AlertManagement|Status"
+msgstr ""
+
+msgid "AlertManagement|Surface alerts in GitLab"
+msgstr ""
+
+msgid "AlertManagement|There was an error displaying the alerts. Confirm your endpoint's configuration details to ensure alerts appear."
+msgstr ""
+
+msgid "AlertManagement|Triggered"
+msgstr ""
+
msgid "AlertService|%{linkStart}Learn more%{linkEnd} about configuring this endpoint to receive alerts."
msgstr ""
@@ -1727,6 +1830,9 @@ msgstr ""
msgid "All security scans are enabled because %{linkStart}Auto DevOps%{linkEnd} is enabled on this project"
msgstr ""
+msgid "All threads resolved"
+msgstr ""
+
msgid "All users"
msgstr ""
@@ -1745,6 +1851,9 @@ msgstr ""
msgid "Allow only the selected protocols to be used for Git access."
msgstr ""
+msgid "Allow owners to manage default branch protection per group"
+msgstr ""
+
msgid "Allow owners to manually add users outside of LDAP"
msgstr ""
@@ -1853,6 +1962,9 @@ msgstr ""
msgid "An error occurred fetching the dropdown data."
msgstr ""
+msgid "An error occurred fetching the project authors."
+msgstr ""
+
msgid "An error occurred previewing the blob"
msgstr ""
@@ -1934,6 +2046,9 @@ msgstr ""
msgid "An error occurred while fetching sidebar data"
msgstr ""
+msgid "An error occurred while fetching terraform reports."
+msgstr ""
+
msgid "An error occurred while fetching the Service Desk address."
msgstr ""
@@ -2171,6 +2286,9 @@ msgstr ""
msgid "Any"
msgstr ""
+msgid "Any Author"
+msgstr ""
+
msgid "Any Label"
msgstr ""
@@ -2386,10 +2504,10 @@ msgstr ""
msgid "Are you sure that you want to unarchive this project?"
msgstr ""
-msgid "Are you sure you want to cancel creating this comment?"
+msgid "Are you sure you want to cancel editing this comment?"
msgstr ""
-msgid "Are you sure you want to cancel editing this comment?"
+msgid "Are you sure you want to close this blocked issue?"
msgstr ""
msgid "Are you sure you want to delete %{name}?"
@@ -2419,6 +2537,9 @@ msgstr ""
msgid "Are you sure you want to deploy this environment?"
msgstr ""
+msgid "Are you sure you want to discard this comment?"
+msgstr ""
+
msgid "Are you sure you want to erase this build?"
msgstr ""
@@ -2595,6 +2716,9 @@ msgstr ""
msgid "At least one approval from a code owner is required to change files matching the respective CODEOWNER rules."
msgstr ""
+msgid "At least one logging option is required to be enabled"
+msgstr ""
+
msgid "At least one of group_id or project_id must be specified"
msgstr ""
@@ -2633,6 +2757,27 @@ msgstr ""
msgid "AuditEvents|Target"
msgstr ""
+msgid "AuditLogs|(removed)"
+msgstr ""
+
+msgid "AuditLogs|Action"
+msgstr ""
+
+msgid "AuditLogs|Author"
+msgstr ""
+
+msgid "AuditLogs|Date"
+msgstr ""
+
+msgid "AuditLogs|IP Address"
+msgstr ""
+
+msgid "AuditLogs|Object"
+msgstr ""
+
+msgid "AuditLogs|Target"
+msgstr ""
+
msgid "Aug"
msgstr ""
@@ -2915,9 +3060,6 @@ msgstr ""
msgid "BambooService|You must set up automatic revision labeling and a repository trigger in Bamboo."
msgstr ""
-msgid "Batch operations"
-msgstr ""
-
msgid "BatchComments|Delete all pending comments"
msgstr ""
@@ -3047,6 +3189,9 @@ msgstr ""
msgid "Boards"
msgstr ""
+msgid "Boards and Board Lists"
+msgstr ""
+
msgid "Boards|Collapse"
msgstr ""
@@ -3266,6 +3411,9 @@ msgstr ""
msgid "Buy GitLab Enterprise Edition"
msgstr ""
+msgid "Buy more Pipeline minutes"
+msgstr ""
+
msgid "By %{user_name}"
msgstr ""
@@ -3386,6 +3534,9 @@ msgstr ""
msgid "Can't scan the code?"
msgstr ""
+msgid "Can't update snippet: %{err}"
+msgstr ""
+
msgid "Canary"
msgstr ""
@@ -3554,6 +3705,9 @@ msgstr ""
msgid "Changing group path can have unintended side effects."
msgstr ""
+msgid "Charts"
+msgstr ""
+
msgid "Charts can't be displayed as the request for data has timed out. %{documentationLink}"
msgstr ""
@@ -3590,9 +3744,6 @@ msgstr ""
msgid "ChatMessage|and [%{count} more](%{pipeline_failed_jobs_url})"
msgstr ""
-msgid "ChatMessage|failed"
-msgstr ""
-
msgid "ChatMessage|has failed"
msgstr ""
@@ -3608,9 +3759,6 @@ msgstr ""
msgid "ChatMessage|in %{project_link}"
msgstr ""
-msgid "ChatMessage|passed"
-msgstr ""
-
msgid "Check again"
msgstr ""
@@ -4061,9 +4209,6 @@ msgstr ""
msgid "Click any <strong>project name</strong> in the project list below to navigate to the project milestone."
msgstr ""
-msgid "Click here"
-msgstr ""
-
msgid "Click the <strong>Download</strong> button and wait for downloading to complete."
msgstr ""
@@ -4451,6 +4596,12 @@ msgstr ""
msgid "ClusterIntegration|Fetching zones"
msgstr ""
+msgid "ClusterIntegration|Fluentd"
+msgstr ""
+
+msgid "ClusterIntegration|Fluentd is an open source data collector, which lets you unify the data collection and consumption for a better use and understanding of data. It requires at least one of the following logs to be successfully installed."
+msgstr ""
+
msgid "ClusterIntegration|GitLab Integration"
msgstr ""
@@ -4757,6 +4908,15 @@ msgstr ""
msgid "ClusterIntegration|Request to begin uninstalling failed"
msgstr ""
+msgid "ClusterIntegration|SIEM Hostname"
+msgstr ""
+
+msgid "ClusterIntegration|SIEM Port"
+msgstr ""
+
+msgid "ClusterIntegration|SIEM Protocol"
+msgstr ""
+
msgid "ClusterIntegration|Save changes"
msgstr ""
@@ -4853,6 +5013,12 @@ msgstr ""
msgid "ClusterIntegration|Select zone to choose machine type"
msgstr ""
+msgid "ClusterIntegration|Send Cilium Logs"
+msgstr ""
+
+msgid "ClusterIntegration|Send ModSecurity Logs"
+msgstr ""
+
msgid "ClusterIntegration|Service Token"
msgstr ""
@@ -5281,18 +5447,33 @@ msgstr ""
msgid "Compliance framework (optional)"
msgstr ""
+msgid "ComplianceFramework|GDPR"
+msgstr ""
+
msgid "ComplianceFramework|GDPR - General Data Protection Regulation"
msgstr ""
+msgid "ComplianceFramework|HIPAA"
+msgstr ""
+
msgid "ComplianceFramework|HIPAA - Health Insurance Portability and Accountability Act"
msgstr ""
+msgid "ComplianceFramework|PCI-DSS"
+msgstr ""
+
msgid "ComplianceFramework|PCI-DSS - Payment Card Industry-Data Security Standard"
msgstr ""
+msgid "ComplianceFramework|SOC 2"
+msgstr ""
+
msgid "ComplianceFramework|SOC 2 - Service Organization Control 2"
msgstr ""
+msgid "ComplianceFramework|SOX"
+msgstr ""
+
msgid "ComplianceFramework|SOX - Sarbanes-Oxley"
msgstr ""
@@ -5440,9 +5621,15 @@ msgstr ""
msgid "Container repositories sync capacity"
msgstr ""
+msgid "ContainerRegistry| Please visit the %{linkStart}administration settings%{linkEnd} to enable this feature."
+msgstr ""
+
msgid "ContainerRegistry|%{imageName} tags"
msgstr ""
+msgid "ContainerRegistry|%{title} was successfully scheduled for deletion"
+msgstr ""
+
msgid "ContainerRegistry|Automatically remove extra images that aren't designed to be kept."
msgstr ""
@@ -5455,6 +5642,9 @@ msgstr ""
msgid "ContainerRegistry|Container Registry"
msgstr ""
+msgid "ContainerRegistry|Container Registry tag expiration and retention policy is disabled"
+msgstr ""
+
msgid "ContainerRegistry|Copy build command"
msgstr ""
@@ -5464,18 +5654,12 @@ msgstr ""
msgid "ContainerRegistry|Copy push command"
msgstr ""
-msgid "ContainerRegistry|Currently, the Container Registry tag expiration feature is not available for projects created before GitLab version 12.8. For updates and more information, visit Issue %{linkStart}#196124%{linkEnd}"
-msgstr ""
-
msgid "ContainerRegistry|Docker connection error"
msgstr ""
msgid "ContainerRegistry|Docker tag expiration policy is %{toggleStatus}"
msgstr ""
-msgid "ContainerRegistry|Docker tags with names matching this regex pattern will expire:"
-msgstr ""
-
msgid "ContainerRegistry|Edit Settings"
msgstr ""
@@ -5497,9 +5681,6 @@ msgstr ""
msgid "ContainerRegistry|Image ID"
msgstr ""
-msgid "ContainerRegistry|Image deleted successfully"
-msgstr ""
-
msgid "ContainerRegistry|Keep and protect the images that matter most."
msgstr ""
@@ -5515,12 +5696,24 @@ msgstr ""
msgid "ContainerRegistry|Number of tags to retain:"
msgstr ""
+msgid "ContainerRegistry|Please contact your administrator."
+msgstr ""
+
msgid "ContainerRegistry|Push an image"
msgstr ""
msgid "ContainerRegistry|Quick Start"
msgstr ""
+msgid "ContainerRegistry|Regular expressions such as %{codeStart}.*-test%{codeEnd} or %{codeStart}dev-.*%{codeEnd} are supported"
+msgstr ""
+
+msgid "ContainerRegistry|Regular expressions such as %{codeStart}.*-test%{codeEnd} or %{codeStart}dev-.*%{codeEnd} are supported. To select all tags, use %{codeStart}.*%{codeEnd}"
+msgstr ""
+
+msgid "ContainerRegistry|Remember to run %{docLinkStart}garbage collection%{docLinkEnd} to remove the stale data from storage."
+msgstr ""
+
msgid "ContainerRegistry|Remove repository"
msgstr ""
@@ -5535,22 +5728,22 @@ msgstr[1] ""
msgid "ContainerRegistry|Retention policy has been Enabled"
msgstr ""
-msgid "ContainerRegistry|Something went wrong while deleting the image."
+msgid "ContainerRegistry|Something went wrong while fetching the expiration policy."
msgstr ""
-msgid "ContainerRegistry|Something went wrong while deleting the tag."
+msgid "ContainerRegistry|Something went wrong while fetching the repository list."
msgstr ""
-msgid "ContainerRegistry|Something went wrong while deleting the tags."
+msgid "ContainerRegistry|Something went wrong while fetching the tags list."
msgstr ""
-msgid "ContainerRegistry|Something went wrong while fetching the expiration policy."
+msgid "ContainerRegistry|Something went wrong while marking the tag for deletion."
msgstr ""
-msgid "ContainerRegistry|Something went wrong while fetching the packages list."
+msgid "ContainerRegistry|Something went wrong while marking the tags for deletion."
msgstr ""
-msgid "ContainerRegistry|Something went wrong while fetching the tags list."
+msgid "ContainerRegistry|Something went wrong while scheduling %{title} for deletion. Please try again."
msgstr ""
msgid "ContainerRegistry|Something went wrong while updating the expiration policy."
@@ -5559,16 +5752,25 @@ msgstr ""
msgid "ContainerRegistry|Tag"
msgstr ""
-msgid "ContainerRegistry|Tag deleted successfully"
-msgstr ""
-
msgid "ContainerRegistry|Tag expiration policy"
msgstr ""
msgid "ContainerRegistry|Tag expiration policy is designed to:"
msgstr ""
-msgid "ContainerRegistry|Tags deleted successfully"
+msgid "ContainerRegistry|Tag successfully marked for deletion."
+msgstr ""
+
+msgid "ContainerRegistry|Tags successfully marked for deletion."
+msgstr ""
+
+msgid "ContainerRegistry|Tags with names matching this regex pattern will %{italicStart}be preserved:%{italicEnd}"
+msgstr ""
+
+msgid "ContainerRegistry|Tags with names matching this regex pattern will %{italicStart}expire:%{italicEnd}"
+msgstr ""
+
+msgid "ContainerRegistry|The Container Registry tag expiration and retention policies for this project have not been enabled."
msgstr ""
msgid "ContainerRegistry|The last tag related to this image was recently removed. This empty image and any associated data will be automatically removed as part of the regular Garbage Collection process. If you have any questions, contact your administrator."
@@ -5589,16 +5791,16 @@ msgstr ""
msgid "ContainerRegistry|There are no container images stored for this project"
msgstr ""
-msgid "ContainerRegistry|This Registry contains deleted image tag data. Remember to run %{docLinkStart}garbage collection%{docLinkEnd} to remove the stale data from storage."
+msgid "ContainerRegistry|There was an error during the deletion of this image repository, please try again."
msgstr ""
msgid "ContainerRegistry|This image has no active tags"
msgstr ""
-msgid "ContainerRegistry|We are having trouble connecting to Docker, which could be due to an issue with your project name or path. %{docLinkStart}More Information%{docLinkEnd}"
+msgid "ContainerRegistry|This image repository is scheduled for deletion"
msgstr ""
-msgid "ContainerRegistry|Wildcards such as %{codeStart}.*-stable%{codeEnd} or %{codeStart}production/.*%{codeEnd} are supported. To select all tags, use %{codeStart}.*%{codeEnd}"
+msgid "ContainerRegistry|We are having trouble connecting to Docker, which could be due to an issue with your project name or path. %{docLinkStart}More Information%{docLinkEnd}"
msgstr ""
msgid "ContainerRegistry|With the Container Registry, every project can have its own space to store its Docker images. %{docLinkStart}More Information%{docLinkEnd}"
@@ -5853,7 +6055,7 @@ msgstr ""
msgid "Could not delete chat nickname %{chat_name}."
msgstr ""
-msgid "Could not find design"
+msgid "Could not find design."
msgstr ""
msgid "Could not remove the trigger."
@@ -5868,6 +6070,9 @@ msgstr ""
msgid "Could not revoke personal access token %{personal_access_token_name}."
msgstr ""
+msgid "Could not revoke project access token %{project_access_token_name}."
+msgstr ""
+
msgid "Could not save group ID"
msgstr ""
@@ -5925,6 +6130,9 @@ msgstr ""
msgid "Create a new branch"
msgstr ""
+msgid "Create a new deploy key for this project"
+msgstr ""
+
msgid "Create a new file as there are no files yet. Afterwards, you'll be able to commit your changes."
msgstr ""
@@ -6030,6 +6238,9 @@ msgstr ""
msgid "Create requirement"
msgstr ""
+msgid "Create snippet"
+msgstr ""
+
msgid "Create wildcard: %{searchTerm}"
msgstr ""
@@ -6051,6 +6262,9 @@ msgstr ""
msgid "Created"
msgstr ""
+msgid "Created %{timestamp}"
+msgstr ""
+
msgid "Created At"
msgstr ""
@@ -6407,6 +6621,9 @@ msgstr ""
msgid "CycleAnalytics|stage dropdown"
msgstr ""
+msgid "DAG"
+msgstr ""
+
msgid "DNS"
msgstr ""
@@ -6773,6 +6990,9 @@ msgstr ""
msgid "Deploy key was successfully updated."
msgstr ""
+msgid "Deploy keys allow read-only or read-write (if enabled) access to your repository. Deploy keys can be used for CI, staging or production servers. You can create a deploy key or add an existing one."
+msgstr ""
+
msgid "Deploy progress not found. To see pods, ensure your environment matches %{linkStart}deploy board criteria%{linkEnd}."
msgstr ""
@@ -6926,6 +7146,9 @@ msgstr ""
msgid "Deploying to"
msgstr ""
+msgid "Deployment Frequency"
+msgstr ""
+
msgid "Deployment|API"
msgstr ""
@@ -7444,9 +7667,6 @@ msgstr ""
msgid "Edit environment"
msgstr ""
-msgid "Edit epic description"
-msgstr ""
-
msgid "Edit file"
msgstr ""
@@ -8068,9 +8288,6 @@ msgstr ""
msgid "Epics"
msgstr ""
-msgid "Epics (Ultimate / Gold license only)"
-msgstr ""
-
msgid "Epics Roadmap"
msgstr ""
@@ -8080,6 +8297,9 @@ msgstr ""
msgid "Epics let you manage your portfolio of projects more efficiently and with less effort"
msgstr ""
+msgid "Epics, Issues, and Merge Requests"
+msgstr ""
+
msgid "Epics|Add an epic"
msgstr ""
@@ -8131,6 +8351,9 @@ msgstr ""
msgid "Epics|Something went wrong while fetching group epics."
msgstr ""
+msgid "Epics|Something went wrong while moving item."
+msgstr ""
+
msgid "Epics|Something went wrong while ordering item."
msgstr ""
@@ -8185,6 +8408,9 @@ msgstr ""
msgid "Error fetching network graph."
msgstr ""
+msgid "Error fetching payload data."
+msgstr ""
+
msgid "Error fetching projects"
msgstr ""
@@ -8194,9 +8420,6 @@ msgstr ""
msgid "Error fetching the dependency list. Please check your network connection and try again."
msgstr ""
-msgid "Error fetching usage ping data."
-msgstr ""
-
msgid "Error loading branch data. Please try again."
msgstr ""
@@ -8338,6 +8561,9 @@ msgstr ""
msgid "Errors"
msgstr ""
+msgid "Errors:"
+msgstr ""
+
msgid "Estimated"
msgstr ""
@@ -8542,12 +8768,18 @@ msgstr ""
msgid "Export as CSV"
msgstr ""
+msgid "Export group"
+msgstr ""
+
msgid "Export issues"
msgstr ""
msgid "Export project"
msgstr ""
+msgid "Export this group with all related data to a new GitLab instance. Once complete, you can import the data file from the \"New Group\" page."
+msgstr ""
+
msgid "Export this project with all its related data in order to move your project to a new GitLab instance. Once the export is finished, you can import the file from the \"New Project\" page."
msgstr ""
@@ -8617,9 +8849,6 @@ msgstr ""
msgid "Failed Jobs"
msgstr ""
-msgid "Failed create wiki"
-msgstr ""
-
msgid "Failed to add a Zoom meeting"
msgstr ""
@@ -8659,6 +8888,9 @@ msgstr ""
msgid "Failed to create resources"
msgstr ""
+msgid "Failed to create wiki"
+msgstr ""
+
msgid "Failed to delete board. Please try again."
msgstr ""
@@ -8668,7 +8900,7 @@ msgstr ""
msgid "Failed to enqueue the rebase operation, possibly due to a long-lived transaction. Try again later."
msgstr ""
-msgid "Failed to find import label for jira import."
+msgid "Failed to find import label for Jira import."
msgstr ""
msgid "Failed to get ref."
@@ -8692,6 +8924,12 @@ msgstr ""
msgid "Failed to load groups & users."
msgstr ""
+msgid "Failed to load labels. Please try again."
+msgstr ""
+
+msgid "Failed to load milestones. Please try again."
+msgstr ""
+
msgid "Failed to load related branches"
msgstr ""
@@ -9067,7 +9305,10 @@ msgstr ""
msgid "Filter by milestone name"
msgstr ""
-msgid "Filter by name..."
+msgid "Filter by name"
+msgstr ""
+
+msgid "Filter by status"
msgstr ""
msgid "Filter by two-factor authentication"
@@ -9079,12 +9320,18 @@ msgstr ""
msgid "Filter projects"
msgstr ""
+msgid "Filter results"
+msgstr ""
+
msgid "Filter results by group"
msgstr ""
msgid "Filter results by project"
msgstr ""
+msgid "Filter results..."
+msgstr ""
+
msgid "Filter your projects by name"
msgstr ""
@@ -9373,10 +9620,10 @@ msgstr ""
msgid "GeoNodes|Checksummed"
msgstr ""
-msgid "GeoNodes|Container repositories"
+msgid "GeoNodes|Consult Geo troubleshooting information"
msgstr ""
-msgid "GeoNodes|Data is out of date from %{timeago}"
+msgid "GeoNodes|Container repositories"
msgstr ""
msgid "GeoNodes|Data replication lag"
@@ -9418,6 +9665,9 @@ msgstr ""
msgid "GeoNodes|Last event ID seen from primary"
msgstr ""
+msgid "GeoNodes|Learn more about Geo node statuses"
+msgstr ""
+
msgid "GeoNodes|Loading nodes"
msgstr ""
@@ -9433,6 +9683,9 @@ msgstr ""
msgid "GeoNodes|Node was successfully removed."
msgstr ""
+msgid "GeoNodes|Node's status was updated %{timeAgo}."
+msgstr ""
+
msgid "GeoNodes|Not checksummed"
msgstr ""
@@ -9463,7 +9716,10 @@ msgstr ""
msgid "GeoNodes|Repository verification progress"
msgstr ""
-msgid "GeoNodes|Selective"
+msgid "GeoNodes|Selective (%{syncLabel})"
+msgstr ""
+
+msgid "GeoNodes|Selective synchronization"
msgstr ""
msgid "GeoNodes|Something went wrong while changing node status"
@@ -9490,6 +9746,9 @@ msgstr ""
msgid "GeoNodes|Unverified"
msgstr ""
+msgid "GeoNodes|Updated %{timeAgo}"
+msgstr ""
+
msgid "GeoNodes|Used slots"
msgstr ""
@@ -9529,16 +9788,16 @@ msgstr ""
msgid "Geo|All"
msgstr ""
-msgid "Geo|All projects"
+msgid "Geo|All %{replicable_type}"
msgstr ""
-msgid "Geo|All projects are being scheduled for re-sync"
+msgid "Geo|All projects"
msgstr ""
-msgid "Geo|All projects are being scheduled for re-verify"
+msgid "Geo|All projects are being scheduled for resync"
msgstr ""
-msgid "Geo|Batch operations"
+msgid "Geo|All projects are being scheduled for reverify"
msgstr ""
msgid "Geo|Could not remove tracking entry for an existing project."
@@ -9550,9 +9809,15 @@ msgstr ""
msgid "Geo|Failed"
msgstr ""
+msgid "Geo|Filter by status"
+msgstr ""
+
msgid "Geo|Geo Status"
msgstr ""
+msgid "Geo|In progress"
+msgstr ""
+
msgid "Geo|In sync"
msgstr ""
@@ -9577,9 +9842,6 @@ msgstr ""
msgid "Geo|Not synced yet"
msgstr ""
-msgid "Geo|Pending"
-msgstr ""
-
msgid "Geo|Pending synchronization"
msgstr ""
@@ -9616,7 +9878,7 @@ msgstr ""
msgid "Geo|Resync"
msgstr ""
-msgid "Geo|Resync all projects"
+msgid "Geo|Resync all"
msgstr ""
msgid "Geo|Retry count"
@@ -9625,7 +9887,7 @@ msgstr ""
msgid "Geo|Reverify"
msgstr ""
-msgid "Geo|Reverify all projects"
+msgid "Geo|Reverify all"
msgstr ""
msgid "Geo|Status"
@@ -9967,9 +10229,6 @@ msgstr ""
msgid "Go to file"
msgstr ""
-msgid "Go to file (MRs only)"
-msgstr ""
-
msgid "Go to file permalink (while viewing a file)"
msgstr ""
@@ -10414,6 +10673,9 @@ msgstr ""
msgid "GroupSettings|Disable group mentions"
msgstr ""
+msgid "GroupSettings|Export group"
+msgstr ""
+
msgid "GroupSettings|If the parent group's visibility is lower than the group current visibility, visibility levels for subgroups and projects will be changed to match the new parent group's visibility."
msgstr ""
@@ -10492,6 +10754,9 @@ msgstr ""
msgid "Groups (%{count})"
msgstr ""
+msgid "Groups (%{groups})"
+msgstr ""
+
msgid "Groups can also be nested by creating %{subgroup_docs_link_start}subgroups%{subgroup_docs_link_end}."
msgstr ""
@@ -10757,15 +11022,12 @@ msgstr ""
msgid "ID:"
msgstr ""
-msgid "IDE|Allow live previews of JavaScript projects in the Web IDE using CodeSandbox client side evaluation."
+msgid "IDE|Allow live previews of JavaScript projects in the Web IDE using CodeSandbox Live Preview."
msgstr ""
msgid "IDE|Back"
msgstr ""
-msgid "IDE|Client side evaluation"
-msgstr ""
-
msgid "IDE|Commit"
msgstr ""
@@ -10829,6 +11091,9 @@ msgstr ""
msgid "If any job surpasses this timeout threshold, it will be marked as failed. Human readable time input language is accepted like \"1 hour\". Values without specification represent seconds."
msgstr ""
+msgid "If blank, set allowable lifetime to %{instance_level_policy_in_words}, as defined by the instance admin. Once set, existing tokens for users in this group may be revoked."
+msgstr ""
+
msgid "If checked, group owners can manage LDAP group links and LDAP member overrides"
msgstr ""
@@ -10868,6 +11133,9 @@ msgstr ""
msgid "If you lose your recovery codes you can generate new ones, invalidating all previous codes."
msgstr ""
+msgid "If you reach 100%% storage capacity, you will not be able to: %{base_message}"
+msgstr ""
+
msgid "If your HTTP repository is not publicly accessible, add your credentials."
msgstr ""
@@ -10979,6 +11247,9 @@ msgstr ""
msgid "Import repository"
msgstr ""
+msgid "Import started by: %{importInitiator}"
+msgstr ""
+
msgid "Import tasks"
msgstr ""
@@ -11045,6 +11316,9 @@ msgstr ""
msgid "In order to tailor your experience with GitLab we<br>would like to know a bit more about you."
msgstr ""
+msgid "In progress"
+msgstr ""
+
msgid "In the next step, you'll be able to select the projects you want to import."
msgstr ""
@@ -11392,15 +11666,15 @@ msgstr ""
msgid "Issues"
msgstr ""
-msgid "Issues / Merge Requests"
-msgstr ""
-
msgid "Issues Analytics"
msgstr ""
msgid "Issues Rate Limits"
msgstr ""
+msgid "Issues and Merge Requests"
+msgstr ""
+
msgid "Issues can be bugs, tasks or ideas to be discussed. Also, issues are searchable and filterable."
msgstr ""
@@ -11422,10 +11696,10 @@ msgstr ""
msgid "IssuesAnalytics|Avg/Month:"
msgstr ""
-msgid "IssuesAnalytics|Issues created"
+msgid "IssuesAnalytics|Issues opened"
msgstr ""
-msgid "IssuesAnalytics|Issues created per month"
+msgid "IssuesAnalytics|Issues opened per month"
msgstr ""
msgid "IssuesAnalytics|Last 12 months"
@@ -11479,6 +11753,9 @@ msgstr ""
msgid "Jira integration not configured."
msgstr ""
+msgid "Jira project: %{importProject}"
+msgstr ""
+
msgid "JiraService|Events for %{noteable_model_name} are disabled."
msgstr ""
@@ -11868,6 +12145,9 @@ msgstr ""
msgid "Lead"
msgstr ""
+msgid "Lead Time"
+msgstr ""
+
msgid "Learn GitLab"
msgstr ""
@@ -11880,6 +12160,9 @@ msgstr ""
msgid "Learn how to %{no_packages_link_start}publish and share your packages%{no_packages_link_end} with GitLab."
msgstr ""
+msgid "Learn how to enable synchronization"
+msgstr ""
+
msgid "Learn more"
msgstr ""
@@ -12211,9 +12494,6 @@ msgstr ""
msgid "Live preview"
msgstr ""
-msgid "Load more vulnerabilities"
-msgstr ""
-
msgid "Loading"
msgstr ""
@@ -12364,9 +12644,6 @@ msgstr ""
msgid "Manage"
msgstr ""
-msgid "Manage Git repositories with fine-grained access controls that keep your code secure. Perform code reviews and enhance collaboration with merge requests. Each project can also have an issue tracker and a wiki."
-msgstr ""
-
msgid "Manage Web IDE features"
msgstr ""
@@ -12397,6 +12674,9 @@ msgstr ""
msgid "Manage your license"
msgstr ""
+msgid "Managed Account"
+msgstr ""
+
msgid "Manifest"
msgstr ""
@@ -12661,6 +12941,9 @@ msgstr ""
msgid "Merge Requests in Review"
msgstr ""
+msgid "Merge automatically (%{strategy})"
+msgstr ""
+
msgid "Merge commit message"
msgstr ""
@@ -12814,6 +13097,12 @@ msgstr ""
msgid "Merged branches are being deleted. This can take some time depending on the number of branches. Please refresh the page to see changes."
msgstr ""
+msgid "Merged this merge request."
+msgstr ""
+
+msgid "Merges this merge request immediately."
+msgstr ""
+
msgid "Merges this merge request when the pipeline succeeds."
msgstr ""
@@ -12921,12 +13210,15 @@ msgid_plural "Metrics|Edit metrics"
msgstr[0] ""
msgstr[1] ""
-msgid "Metrics|Environment"
+msgid "Metrics|Expand panel"
msgstr ""
msgid "Metrics|For grouping similar metrics"
msgstr ""
+msgid "Metrics|Go back (Esc)"
+msgstr ""
+
msgid "Metrics|Invalid time range, please verify."
msgstr ""
@@ -12957,9 +13249,6 @@ msgstr ""
msgid "Metrics|Refresh dashboard"
msgstr ""
-msgid "Metrics|Show last"
-msgstr ""
-
msgid "Metrics|There was an error creating the dashboard."
msgstr ""
@@ -13038,6 +13327,9 @@ msgstr ""
msgid "Microsoft Azure"
msgstr ""
+msgid "Middleman project with Static Site Editor support"
+msgstr ""
+
msgid "Migrated %{success_count}/%{total_count} files."
msgstr ""
@@ -13483,10 +13775,10 @@ msgstr ""
msgid "Next"
msgstr ""
-msgid "Next file in diff (MRs only)"
+msgid "Next file in diff"
msgstr ""
-msgid "Next unresolved discussion (MRs only)"
+msgid "Next unresolved discussion"
msgstr ""
msgid "Nickname"
@@ -13546,6 +13838,9 @@ msgstr ""
msgid "No changes between %{ref_start}%{source_branch}%{ref_end} and %{ref_start}%{target_branch}%{ref_end}"
msgstr ""
+msgid "No child epics match applied filters"
+msgstr ""
+
msgid "No connection could be made to a Gitaly Server, please check your logs!"
msgstr ""
@@ -13684,15 +13979,6 @@ msgstr ""
msgid "No thanks, don't show this again"
msgstr ""
-msgid "No vulnerabilities found for this group"
-msgstr ""
-
-msgid "No vulnerabilities found for this pipeline"
-msgstr ""
-
-msgid "No vulnerabilities found for this project"
-msgstr ""
-
msgid "No vulnerabilities present"
msgstr ""
@@ -13816,6 +14102,9 @@ msgstr ""
msgid "Nothing to preview."
msgstr ""
+msgid "Nothing to synchronize"
+msgstr ""
+
msgid "Notification events"
msgstr ""
@@ -13987,6 +14276,9 @@ msgstr ""
msgid "Once removed, the fork relationship cannot be restored and you will no longer be able to send merge requests to the source."
msgstr ""
+msgid "Once the exported file is ready you can download it from this page."
+msgstr ""
+
msgid "Once the exported file is ready, you will receive a notification email with a download link, or you can download it from this page."
msgstr ""
@@ -14088,9 +14380,6 @@ msgstr ""
msgid "Open sidebar"
msgstr ""
-msgid "Open source software to collaborate on code"
-msgstr ""
-
msgid "Open: %{openIssuesCount}"
msgstr ""
@@ -14193,6 +14482,9 @@ msgstr ""
msgid "OutdatedBrowser|You can provide feedback %{feedback_link_start}on this issue%{feedback_link_end} or via your usual support channels."
msgstr ""
+msgid "Overridden"
+msgstr ""
+
msgid "Overview"
msgstr ""
@@ -14244,6 +14536,9 @@ msgstr ""
msgid "PackageRegistry|Conan Command"
msgstr ""
+msgid "PackageRegistry|Copy .pypirc content"
+msgstr ""
+
msgid "PackageRegistry|Copy Conan Command"
msgstr ""
@@ -14265,6 +14560,9 @@ msgstr ""
msgid "PackageRegistry|Copy NuGet Setup Command"
msgstr ""
+msgid "PackageRegistry|Copy Pip command"
+msgstr ""
+
msgid "PackageRegistry|Copy and paste this inside your %{codeStart}pom.xml%{codeEnd} %{codeStart}dependencies%{codeEnd} block."
msgstr ""
@@ -14298,6 +14596,12 @@ msgstr ""
msgid "PackageRegistry|For more information on the NuGet registry, %{linkStart}see the documentation%{linkEnd}."
msgstr ""
+msgid "PackageRegistry|For more information on the PyPi registry, %{linkStart}see the documentation%{linkEnd}."
+msgstr ""
+
+msgid "PackageRegistry|If you haven't already done so, you will need to add the below to your %{codeStart}.pypirc%{codeEnd} file."
+msgstr ""
+
msgid "PackageRegistry|If you haven't already done so, you will need to add the below to your %{codeStart}pom.xml%{codeEnd} file."
msgstr ""
@@ -14307,6 +14611,9 @@ msgstr ""
msgid "PackageRegistry|Learn how to %{noPackagesLinkStart}publish and share your packages%{noPackagesLinkEnd} with GitLab."
msgstr ""
+msgid "PackageRegistry|Manually Published"
+msgstr ""
+
msgid "PackageRegistry|Maven"
msgstr ""
@@ -14325,12 +14632,18 @@ msgstr ""
msgid "PackageRegistry|NuGet Command"
msgstr ""
+msgid "PackageRegistry|Pip Command"
+msgstr ""
+
msgid "PackageRegistry|Pipeline %{linkStart}%{linkEnd} triggered %{timestamp} by %{author}"
msgstr ""
msgid "PackageRegistry|Published to the repository at %{timestamp}"
msgstr ""
+msgid "PackageRegistry|PyPi"
+msgstr ""
+
msgid "PackageRegistry|Registry Setup"
msgstr ""
@@ -14367,6 +14680,9 @@ msgstr ""
msgid "PackageRegistry|npm"
msgstr ""
+msgid "PackageRegistry|published by %{author}"
+msgstr ""
+
msgid "PackageRegistry|yarn"
msgstr ""
@@ -14382,6 +14698,9 @@ msgstr ""
msgid "PackageType|NuGet"
msgstr ""
+msgid "PackageType|PyPi"
+msgstr ""
+
msgid "Packages"
msgstr ""
@@ -14475,6 +14794,9 @@ msgstr ""
msgid "Past due"
msgstr ""
+msgid "Paste a machine public key here. Read more about how to generate it"
+msgstr ""
+
msgid "Paste a machine public key here. Read more about how to generate it %{link_start}here%{link_end}"
msgstr ""
@@ -14490,9 +14812,6 @@ msgstr ""
msgid "Path"
msgstr ""
-msgid "Path, transfer, remove"
-msgstr ""
-
msgid "Path:"
msgstr ""
@@ -14523,7 +14842,7 @@ msgstr ""
msgid "Percentage"
msgstr ""
-msgid "Perform advanced options such as changing path, transferring, or removing the group."
+msgid "Perform advanced options such as changing path, transferring, exporting, or removing the group."
msgstr ""
msgid "Perform common operations on GitLab project"
@@ -14604,9 +14923,6 @@ msgstr ""
msgid "Pipeline minutes quota"
msgstr ""
-msgid "Pipeline quota"
-msgstr ""
-
msgid "Pipeline subscriptions"
msgstr ""
@@ -14616,6 +14932,9 @@ msgstr ""
msgid "Pipeline: %{status}"
msgstr ""
+msgid "PipelineCharts|CI / CD Analytics"
+msgstr ""
+
msgid "PipelineCharts|Failed:"
msgstr ""
@@ -14694,15 +15013,6 @@ msgstr ""
msgid "Pipelines settings for '%{project_name}' were successfully updated."
msgstr ""
-msgid "Pipelines| to purchase more minutes."
-msgstr ""
-
-msgid "Pipelines|%{namespace_name} has exceeded its pipeline minutes quota."
-msgstr ""
-
-msgid "Pipelines|%{namespace_name} has less than %{notification_level}%% of CI minutes available."
-msgstr ""
-
msgid "Pipelines|API"
msgstr ""
@@ -14724,10 +15034,13 @@ msgstr ""
msgid "Pipelines|Get started with Pipelines"
msgstr ""
-msgid "Pipelines|Loading Pipelines"
+msgid "Pipelines|Group %{namespace_name} has %{percentage}%% or less Shared Runner Pipeline minutes remaining. Once it runs out, no new jobs or pipelines in its projects will run."
msgstr ""
-msgid "Pipelines|Pipelines will not run anymore on shared Runners."
+msgid "Pipelines|Group %{namespace_name} has exceeded its pipeline minutes quota. Unless you buy additional pipeline minutes, no new jobs or pipelines in its projects will run."
+msgstr ""
+
+msgid "Pipelines|Loading Pipelines"
msgstr ""
msgid "Pipelines|Project cache successfully reset."
@@ -14958,6 +15271,9 @@ msgstr ""
msgid "Please select"
msgstr ""
+msgid "Please select a Jira project"
+msgstr ""
+
msgid "Please select a country"
msgstr ""
@@ -15003,6 +15319,9 @@ msgstr ""
msgid "Please wait while we import the repository for you. Refresh at will."
msgstr ""
+msgid "Plugins directory is deprecated and will be removed in 14.0. Please move this file into /file_hooks directory."
+msgstr ""
+
msgid "Pod does not exist"
msgstr ""
@@ -15120,7 +15439,7 @@ msgstr ""
msgid "Prevent users from changing their profile name"
msgstr ""
-msgid "Prevent users from modifing merge request approvers list"
+msgid "Prevent users from modifying merge request approvers list"
msgstr ""
msgid "Prevent users from performing write operations on GitLab while performing maintenance."
@@ -15141,10 +15460,10 @@ msgstr ""
msgid "Previous Artifacts"
msgstr ""
-msgid "Previous file in diff (MRs only)"
+msgid "Previous file in diff"
msgstr ""
-msgid "Previous unresolved discussion (MRs only)"
+msgid "Previous unresolved discussion"
msgstr ""
msgid "Primary"
@@ -15177,6 +15496,9 @@ msgstr ""
msgid "Private profile"
msgstr ""
+msgid "Private projects Minutes cost factor"
+msgstr ""
+
msgid "Private projects can be created in your personal namespace with:"
msgstr ""
@@ -16005,6 +16327,9 @@ msgstr ""
msgid "ProjectSettings|With GitLab Pages you can host your static websites on GitLab"
msgstr ""
+msgid "ProjectSettings|With Metrics Dashboard you can visualize this project performance metrics"
+msgstr ""
+
msgid "ProjectTemplates|.NET Core"
msgstr ""
@@ -16068,6 +16393,9 @@ msgstr ""
msgid "ProjectTemplates|Spring"
msgstr ""
+msgid "ProjectTemplates|Static Site Editor/Middleman"
+msgstr ""
+
msgid "ProjectTemplates|iOS (Swift)"
msgstr ""
@@ -16287,13 +16615,13 @@ msgstr ""
msgid "PrometheusService|Prometheus is being automatically managed on your clusters"
msgstr ""
-msgid "PrometheusService|These metrics will only be monitored after your first deployment to an environment"
+msgid "PrometheusService|Select the Active checkbox to override the Auto Configuration with custom settings. If unchecked, Auto Configuration settings are used."
msgstr ""
-msgid "PrometheusService|Time-series monitoring service"
+msgid "PrometheusService|These metrics will only be monitored after your first deployment to an environment"
msgstr ""
-msgid "PrometheusService|To enable manual configuration, uninstall Prometheus from your clusters"
+msgid "PrometheusService|Time-series monitoring service"
msgstr ""
msgid "PrometheusService|To enable the installation of Prometheus on your clusters, deactivate the manual configuration below"
@@ -16479,6 +16807,9 @@ msgstr ""
msgid "Protip:"
msgstr ""
+msgid "Protocol"
+msgstr ""
+
msgid "Provider"
msgstr ""
@@ -16503,6 +16834,9 @@ msgstr ""
msgid "Public pipelines"
msgstr ""
+msgid "Public projects Minutes cost factor"
+msgstr ""
+
msgid "Pull"
msgstr ""
@@ -16703,6 +17037,9 @@ msgid_plural "Refreshing in %d seconds to show the updated status..."
msgstr[0] ""
msgstr[1] ""
+msgid "Regenerate export"
+msgstr ""
+
msgid "Regenerate instance ID"
msgstr ""
@@ -17080,9 +17417,29 @@ msgstr ""
msgid "Reports|%{combinedString} and %{resolvedString}"
msgstr ""
+msgid "Reports|Accessibility scanning detected %d issue for the source branch only"
+msgid_plural "Reports|Accessibility scanning detected %d issues for the source branch only"
+msgstr[0] ""
+msgstr[1] ""
+
+msgid "Reports|Accessibility scanning detected no issues for the source branch only"
+msgstr ""
+
+msgid "Reports|Accessibility scanning failed loading results"
+msgstr ""
+
+msgid "Reports|Accessibility scanning results are being parsed"
+msgstr ""
+
msgid "Reports|Actions"
msgstr ""
+msgid "Reports|An error occured while loading report"
+msgstr ""
+
+msgid "Reports|An error occurred while loading %{name} results"
+msgstr ""
+
msgid "Reports|Class"
msgstr ""
@@ -17188,7 +17545,7 @@ msgstr ""
msgid "Requested %{time_ago}"
msgstr ""
-msgid "Requested design version does not exist"
+msgid "Requested design version does not exist."
msgstr ""
msgid "Requested states are invalid"
@@ -17372,6 +17729,9 @@ msgstr ""
msgid "Resync"
msgstr ""
+msgid "Resync all"
+msgstr ""
+
msgid "Resync all %{replicableType}"
msgstr ""
@@ -17440,6 +17800,9 @@ msgstr ""
msgid "Revoked personal access token %{personal_access_token_name}!"
msgstr ""
+msgid "Revoked project access token %{project_access_token_name}!"
+msgstr ""
+
msgid "RightSidebar|adding a"
msgstr ""
@@ -17488,6 +17851,9 @@ msgstr ""
msgid "Runner token"
msgstr ""
+msgid "Runner tokens"
+msgstr ""
+
msgid "Runner was not updated."
msgstr ""
@@ -17542,6 +17908,9 @@ msgstr ""
msgid "SAML SSO for %{group_name}"
msgstr ""
+msgid "SAML discovery tokens"
+msgstr ""
+
msgid "SAML for %{group_name}"
msgstr ""
@@ -17626,12 +17995,18 @@ msgstr ""
msgid "Scheduled"
msgstr ""
+msgid "Scheduled to merge this merge request (%{strategy})."
+msgstr ""
+
msgid "Scheduled to merge this merge request when the pipeline succeeds."
msgstr ""
msgid "Schedules"
msgstr ""
+msgid "Schedules to merge this merge request (%{strategy})."
+msgstr ""
+
msgid "Scheduling"
msgstr ""
@@ -17689,6 +18064,9 @@ msgstr ""
msgid "Search branches and tags"
msgstr ""
+msgid "Search by author"
+msgstr ""
+
msgid "Search files"
msgstr ""
@@ -17834,11 +18212,6 @@ msgid_plural "SearchResults|snippets"
msgstr[0] ""
msgstr[1] ""
-msgid "SearchResults|snippet result"
-msgid_plural "SearchResults|snippet results"
-msgstr[0] ""
-msgstr[1] ""
-
msgid "SearchResults|user"
msgid_plural "SearchResults|users"
msgstr[0] ""
@@ -17849,6 +18222,9 @@ msgid_plural "SearchResults|wiki results"
msgstr[0] ""
msgstr[1] ""
+msgid "Searching by both author and message is currently not supported."
+msgstr ""
+
msgid "Seat Link"
msgstr ""
@@ -17876,184 +18252,232 @@ msgstr ""
msgid "Security Dashboard"
msgstr ""
-msgid "Security Dashboard|Error fetching the vulnerability counts. Please check your network connection and try again."
+msgid "Security configuration help link"
+msgstr ""
+
+msgid "Security dashboard"
msgstr ""
-msgid "Security Dashboard|Error fetching the vulnerability list. Please check your network connection and try again."
+msgid "Security report is out of date. Please update your branch with the latest changes from the target branch (%{targetBranchName})"
msgstr ""
-msgid "Security Dashboard|Issue Created"
+msgid "Security report is out of date. Run %{newPipelineLinkStart}a new pipeline%{newPipelineLinkEnd} for the target branch (%{targetBranchName})"
msgstr ""
-msgid "Security Reports|Comment added to '%{vulnerabilityName}'"
+msgid "SecurityConfiguration|Configured"
msgstr ""
-msgid "Security Reports|Comment deleted on '%{vulnerabilityName}'"
+msgid "SecurityConfiguration|Feature"
msgstr ""
-msgid "Security Reports|Comment edited on '%{vulnerabilityName}'"
+msgid "SecurityConfiguration|Feature documentation for %{featureName}"
msgstr ""
-msgid "Security Reports|Create issue"
+msgid "SecurityConfiguration|Not yet configured"
msgstr ""
-msgid "Security Reports|Dismiss vulnerability"
+msgid "SecurityConfiguration|Secure features"
msgstr ""
-msgid "Security Reports|Dismissed '%{vulnerabilityName}'"
+msgid "SecurityConfiguration|Status"
msgstr ""
-msgid "Security Reports|Dismissed '%{vulnerabilityName}'. Turn off the hide dismissed toggle to view."
+msgid "SecurityReports|%{firstProject} and %{secondProject}"
msgstr ""
-msgid "Security Reports|Either you don't have permission to view this dashboard or the dashboard has not been setup. Please check your permission settings with your administrator or check your dashboard configurations to proceed."
+msgid "SecurityReports|%{firstProject}, %{secondProject}, and %{rest}"
msgstr ""
-msgid "Security Reports|Learn more about setting up your dashboard"
+msgid "SecurityReports|Add a project to your dashboard"
msgstr ""
-msgid "Security Reports|More info"
+msgid "SecurityReports|Add or remove projects from your dashboard"
msgstr ""
-msgid "Security Reports|Oops, something doesn't seem right."
+msgid "SecurityReports|Add projects"
msgstr ""
-msgid "Security Reports|Security reports can only be accessed by authorized users."
+msgid "SecurityReports|Comment added to '%{vulnerabilityName}'"
msgstr ""
-msgid "Security Reports|There was an error adding the comment."
+msgid "SecurityReports|Comment deleted on '%{vulnerabilityName}'"
msgstr ""
-msgid "Security Reports|There was an error creating the issue."
+msgid "SecurityReports|Comment edited on '%{vulnerabilityName}'"
msgstr ""
-msgid "Security Reports|There was an error creating the merge request."
+msgid "SecurityReports|Create issue"
msgstr ""
-msgid "Security Reports|There was an error deleting the comment."
+msgid "SecurityReports|Dismiss Selected"
msgstr ""
-msgid "Security Reports|There was an error dismissing the vulnerabilities."
+msgid "SecurityReports|Dismiss vulnerability"
msgstr ""
-msgid "Security Reports|There was an error dismissing the vulnerability."
+msgid "SecurityReports|Dismissed '%{vulnerabilityName}'"
msgstr ""
-msgid "Security Reports|There was an error reverting the dismissal."
+msgid "SecurityReports|Dismissed '%{vulnerabilityName}'. Turn off the hide dismissed toggle to view."
msgstr ""
-msgid "Security Reports|There was an error reverting this dismissal."
+msgid "SecurityReports|Each vulnerability now has a unique page that can be directly linked to, shared, referenced, and tracked as the single source of truth. Vulnerability occurrences also persist across scanner runs, which improves tracking and visibility and reduces duplicates between scans."
msgstr ""
-msgid "Security Reports|Undo dismiss"
+msgid "SecurityReports|Edit dashboard"
msgstr ""
-msgid "Security Reports|You do not have sufficient permissions to access this report"
+msgid "SecurityReports|Either you don't have permission to view this dashboard or the dashboard has not been setup. Please check your permission settings with your administrator or check your dashboard configurations to proceed."
msgstr ""
-msgid "Security Reports|You must sign in as an authorized user to see this report"
+msgid "SecurityReports|Error fetching the vulnerability counts. Please check your network connection and try again."
msgstr ""
-msgid "Security configuration help link"
+msgid "SecurityReports|Error fetching the vulnerability list. Please check your network connection and try again."
msgstr ""
-msgid "Security dashboard"
+msgid "SecurityReports|False positive"
msgstr ""
-msgid "Security report is out of date. Please update your branch with the latest changes from the target branch (%{targetBranchName})"
+msgid "SecurityReports|Hide dismissed"
msgstr ""
-msgid "Security report is out of date. Run %{newPipelineLinkStart}a new pipeline%{newPipelineLinkEnd} for the target branch (%{targetBranchName})"
+msgid "SecurityReports|Introducing standalone vulnerabilities"
msgstr ""
-msgid "SecurityConfiguration|Configured"
+msgid "SecurityReports|Issue Created"
msgstr ""
-msgid "SecurityConfiguration|Feature"
+msgid "SecurityReports|Learn More"
msgstr ""
-msgid "SecurityConfiguration|Feature documentation for %{featureName}"
+msgid "SecurityReports|Learn more about setting up your dashboard"
msgstr ""
-msgid "SecurityConfiguration|Not yet configured"
+msgid "SecurityReports|Load more vulnerabilities"
msgstr ""
-msgid "SecurityConfiguration|Secure features"
+msgid "SecurityReports|Monitor vulnerabilities in your code"
msgstr ""
-msgid "SecurityConfiguration|Status"
+msgid "SecurityReports|More info"
msgstr ""
-msgid "SecurityDashboard|%{firstProject} and %{secondProject}"
+msgid "SecurityReports|More information"
msgstr ""
-msgid "SecurityDashboard|%{firstProject}, %{secondProject}, and %{rest}"
+msgid "SecurityReports|No vulnerabilities found for dashboard"
msgstr ""
-msgid "SecurityDashboard|Add a project to your dashboard"
+msgid "SecurityReports|No vulnerabilities found for this group"
msgstr ""
-msgid "SecurityDashboard|Add or remove projects from your dashboard"
+msgid "SecurityReports|No vulnerabilities found for this pipeline"
msgstr ""
-msgid "SecurityDashboard|Add projects"
+msgid "SecurityReports|No vulnerabilities found for this project"
msgstr ""
-msgid "SecurityDashboard|Edit dashboard"
+msgid "SecurityReports|Oops, something doesn't seem right."
msgstr ""
-msgid "SecurityDashboard|Hide dismissed"
+msgid "SecurityReports|Pipeline %{pipelineLink} triggered %{timeago} by %{user}"
msgstr ""
-msgid "SecurityDashboard|Monitor vulnerabilities in your code"
+msgid "SecurityReports|Project"
msgstr ""
-msgid "SecurityDashboard|More information"
+msgid "SecurityReports|Projects added"
msgstr ""
-msgid "SecurityDashboard|No vulnerabilities found for dashboard"
+msgid "SecurityReports|Remove project from dashboard"
msgstr ""
-msgid "SecurityDashboard|Pipeline %{pipelineLink} triggered %{timeago} by %{user}"
+msgid "SecurityReports|Report type"
msgstr ""
-msgid "SecurityDashboard|Project"
+msgid "SecurityReports|Return to dashboard"
msgstr ""
-msgid "SecurityDashboard|Projects added"
+msgid "SecurityReports|Security Dashboard"
msgstr ""
-msgid "SecurityDashboard|Remove project from dashboard"
+msgid "SecurityReports|Security reports can only be accessed by authorized users."
msgstr ""
-msgid "SecurityDashboard|Report type"
+msgid "SecurityReports|Select a project to add by using the project search field above."
msgstr ""
-msgid "SecurityDashboard|Return to dashboard"
+msgid "SecurityReports|Select a reason"
msgstr ""
-msgid "SecurityDashboard|Security Dashboard"
+msgid "SecurityReports|Severity"
msgstr ""
-msgid "SecurityDashboard|Select a project to add by using the project search field above."
+msgid "SecurityReports|Status"
msgstr ""
-msgid "SecurityDashboard|Severity"
+msgid "SecurityReports|The security dashboard displays the latest security findings for projects you wish to monitor. Select \"Edit dashboard\" to add and remove projects."
msgstr ""
-msgid "SecurityDashboard|Status"
+msgid "SecurityReports|The security dashboard displays the latest security report. Use it to find and fix vulnerabilities."
msgstr ""
-msgid "SecurityDashboard|The security dashboard displays the latest security findings for projects you wish to monitor. Select \"Edit dashboard\" to add and remove projects."
+msgid "SecurityReports|There was an error adding the comment."
msgstr ""
-msgid "SecurityDashboard|The security dashboard displays the latest security report. Use it to find and fix vulnerabilities."
+msgid "SecurityReports|There was an error creating the issue."
msgstr ""
-msgid "SecurityDashboard|There was an error while generating the report."
+msgid "SecurityReports|There was an error creating the merge request."
msgstr ""
-msgid "SecurityDashboard|Unable to add %{invalidProjects}"
+msgid "SecurityReports|There was an error deleting the comment."
+msgstr ""
+
+msgid "SecurityReports|There was an error dismissing the vulnerabilities."
+msgstr ""
+
+msgid "SecurityReports|There was an error dismissing the vulnerability."
+msgstr ""
+
+msgid "SecurityReports|There was an error reverting the dismissal."
+msgstr ""
+
+msgid "SecurityReports|There was an error reverting this dismissal."
+msgstr ""
+
+msgid "SecurityReports|There was an error while generating the report."
+msgstr ""
+
+msgid "SecurityReports|Unable to add %{invalidProjects}"
+msgstr ""
+
+msgid "SecurityReports|Undo dismiss"
+msgstr ""
+
+msgid "SecurityReports|While it's rare to have no vulnerabilities for your group, it can happen. In any event, we ask that you double check your settings to make sure you've set up your dashboard correctly."
+msgstr ""
+
+msgid "SecurityReports|While it's rare to have no vulnerabilities for your pipeline, it can happen. In any event, we ask that you double check your settings to make sure all security scanning jobs have passed successfully."
+msgstr ""
+
+msgid "SecurityReports|While it's rare to have no vulnerabilities for your project, it can happen. In any event, we ask that you double check your settings to make sure you've set up your dashboard correctly."
+msgstr ""
+
+msgid "SecurityReports|While it's rare to have no vulnerabilities, it can happen. In any event, we ask that you please double check your settings to make sure you've set up your dashboard correctly."
+msgstr ""
+
+msgid "SecurityReports|Won't fix / Accept risk"
+msgstr ""
+
+msgid "SecurityReports|You do not have sufficient permissions to access this report"
+msgstr ""
+
+msgid "SecurityReports|You must sign in as an authorized user to see this report"
+msgstr ""
+
+msgid "SecurityReports|[No reason]"
msgstr ""
msgid "See GitLab's %{password_policy_guidelines}"
@@ -18065,6 +18489,9 @@ msgstr ""
msgid "See the affected projects in the GitLab admin panel"
msgstr ""
+msgid "See what's new at GitLab"
+msgstr ""
+
msgid "Select"
msgstr ""
@@ -18470,9 +18897,6 @@ msgstr ""
msgid "Set up Jira Integration"
msgstr ""
-msgid "Set up Jira Integration illustration"
-msgstr ""
-
msgid "Set up a %{type} Runner automatically"
msgstr ""
@@ -18557,6 +18981,9 @@ msgstr ""
msgid "Severity: %{severity}"
msgstr ""
+msgid "Shards (%{shards})"
+msgstr ""
+
msgid "Shards to synchronize"
msgstr ""
@@ -18871,6 +19298,9 @@ msgstr ""
msgid "Snowplow"
msgstr ""
+msgid "Some child epics may be hidden due to applied filters"
+msgstr ""
+
msgid "Some email servers do not support overriding the email sender name. Enable this option to include the name of the author of the issue, merge request or comment in the email body instead."
msgstr ""
@@ -19012,6 +19442,9 @@ msgstr ""
msgid "Something went wrong while updating your list settings"
msgstr ""
+msgid "Something went wrong with your automatic subscription renewal"
+msgstr ""
+
msgid "Something went wrong, unable to add %{project} to dashboard"
msgstr ""
@@ -19297,6 +19730,9 @@ msgstr ""
msgid "Stage removed"
msgstr ""
+msgid "Standard"
+msgstr ""
+
msgid "Star a label to make it a priority label. Order the prioritized labels to change their relative priority, by dragging."
msgstr ""
@@ -19420,6 +19856,9 @@ msgstr ""
msgid "Static Application Security Testing (SAST)"
msgstr ""
+msgid "StaticSiteEditor|An error occurred while submitting your changes."
+msgstr ""
+
msgid "StaticSiteEditor|Branch could not be created."
msgstr ""
@@ -19429,18 +19868,30 @@ msgstr ""
msgid "StaticSiteEditor|Could not create merge request."
msgstr ""
+msgid "StaticSiteEditor|Incompatible file content"
+msgstr ""
+
msgid "StaticSiteEditor|Return to site"
msgstr ""
+msgid "StaticSiteEditor|Static site editor"
+msgstr ""
+
msgid "StaticSiteEditor|Success!"
msgstr ""
msgid "StaticSiteEditor|Summary of changes"
msgstr ""
+msgid "StaticSiteEditor|The Static Site Editor is currently configured to only edit Markdown content on pages generated from Middleman. Visit the documentation to learn more about configuring your site to use the Static Site Editor."
+msgstr ""
+
msgid "StaticSiteEditor|Update %{sourcePath} file"
msgstr ""
+msgid "StaticSiteEditor|View documentation"
+msgstr ""
+
msgid "StaticSiteEditor|View merge request"
msgstr ""
@@ -19498,6 +19949,12 @@ msgstr ""
msgid "StatusPage|Status page"
msgstr ""
+msgid "StatusPage|Status page URL"
+msgstr ""
+
+msgid "StatusPage|Status page frontend documentation"
+msgstr ""
+
msgid "StatusPage|To publish incidents to an external status page, GitLab will store a JSON file in your Amazon S3 account in a location accessible to your external status page service. Make sure to also set up %{docsLink}"
msgstr ""
@@ -19846,6 +20303,12 @@ msgstr ""
msgid "Synced"
msgstr ""
+msgid "Synchronization disabled"
+msgstr ""
+
+msgid "Synchronization of container repositories is disabled."
+msgstr ""
+
msgid "System"
msgstr ""
@@ -19999,6 +20462,9 @@ msgstr ""
msgid "Telephone number"
msgstr ""
+msgid "Telephone number (Optional)"
+msgstr ""
+
msgid "Template"
msgstr ""
@@ -20305,6 +20771,9 @@ msgstr ""
msgid "The license was successfully uploaded and is now active. You can see the details below."
msgstr ""
+msgid "The license was successfully uploaded and will be active from %{starts_at}. You can see the details below."
+msgstr ""
+
msgid "The maximum file size allowed is %{size}."
msgstr ""
@@ -20626,6 +21095,9 @@ msgstr ""
msgid "There was an error getting the epic participants."
msgstr ""
+msgid "There was an error importing the Jira project."
+msgstr ""
+
msgid "There was an error loading users activity calendar."
msgstr ""
@@ -20641,7 +21113,7 @@ msgstr ""
msgid "There was an error resetting user pipeline minutes."
msgstr ""
-msgid "There was an error saving this Geo Node"
+msgid "There was an error saving this Geo Node."
msgstr ""
msgid "There was an error saving your changes."
@@ -20704,6 +21176,9 @@ msgstr ""
msgid "They can be managed using the %{link}."
msgstr ""
+msgid "Third Party Advisory Link"
+msgstr ""
+
msgid "Third party offers"
msgstr ""
@@ -20740,6 +21215,9 @@ msgstr ""
msgid "This application will be able to:"
msgstr ""
+msgid "This attachment has been truncated to avoid exceeding the maximum allowed attachment size of 15MB. %{written_count} of %{issues_count} issues have been included. Consider re-exporting with a narrower selection of issues."
+msgstr ""
+
msgid "This block is self-referential"
msgstr ""
@@ -20881,6 +21359,12 @@ msgstr ""
msgid "This issue is confidential"
msgstr ""
+msgid "This issue is currently blocked by the following issues: %{issues}."
+msgstr ""
+
+msgid "This issue is in a child epic of the filtered epic"
+msgstr ""
+
msgid "This issue is locked."
msgstr ""
@@ -21079,7 +21563,10 @@ msgstr ""
msgid "This user will be the author of all events in the activity feed that are the result of an update, like new branches being created or new commits being pushed to existing branches. Upon creation or when reassigning you can only assign yourself to be the mirror user."
msgstr ""
-msgid "This variable can not be masked"
+msgid "This variable can not be masked."
+msgstr ""
+
+msgid "This variable does not match the expected pattern."
msgstr ""
msgid "This will help us personalize your onboarding experience."
@@ -21109,10 +21596,10 @@ msgstr ""
msgid "ThreatMonitoring|Application firewall not detected"
msgstr ""
-msgid "ThreatMonitoring|Container Network Policy"
+msgid "ThreatMonitoring|Container Network Policies are not installed or have been disabled. To view this data, ensure your Network Policies are installed and enabled for your cluster."
msgstr ""
-msgid "ThreatMonitoring|Container NetworkPolicies are not installed or has been disabled. To view this data, ensure you NetworkPolicies are installed and enabled for your cluster."
+msgid "ThreatMonitoring|Container Network Policy"
msgstr ""
msgid "ThreatMonitoring|Container NetworkPolicies not detected"
@@ -21145,7 +21632,7 @@ msgstr ""
msgid "ThreatMonitoring|Something went wrong, unable to fetch statistics"
msgstr ""
-msgid "ThreatMonitoring|The firewall is not installed or has been disabled. To view this data, ensure you firewall is installed and enabled for your cluster."
+msgid "ThreatMonitoring|The firewall is not installed or has been disabled. To view this data, ensure the web application firewall is installed and enabled for your cluster."
msgstr ""
msgid "ThreatMonitoring|The graph below is an overview of traffic coming to your application as tracked by the Web Application Firewall (WAF). View the docs for instructions on how to access the WAF logs to see what type of malicious traffic is trying to access your app. The docs link is also accessible by clicking the \"?\" icon next to the title below."
@@ -21214,6 +21701,9 @@ msgstr ""
msgid "Time in seconds GitLab will wait for a response from the external service. When the service does not respond in time, access will be denied."
msgstr ""
+msgid "Time of import: %{importTime}"
+msgstr ""
+
msgid "Time remaining"
msgstr ""
@@ -21398,7 +21888,7 @@ msgstr ""
msgid "Title:"
msgstr ""
-msgid "Titles and Filenames"
+msgid "Titles and Descriptions"
msgstr ""
msgid "To"
@@ -21605,7 +22095,7 @@ msgstr ""
msgid "Too many projects enabled. You will need to manage them via the console or the API."
msgstr ""
-msgid "Topics"
+msgid "Topics (optional)"
msgstr ""
msgid "Total"
@@ -21767,6 +22257,9 @@ msgstr ""
msgid "Try all GitLab has to offer for 30 days."
msgstr ""
+msgid "Try changing or removing filters."
+msgstr ""
+
msgid "Try to fork again"
msgstr ""
@@ -21860,6 +22353,9 @@ msgstr ""
msgid "Unable to connect to server: %{error}"
msgstr ""
+msgid "Unable to connect to the Jira instance. Please check your Jira integration configuration."
+msgstr ""
+
msgid "Unable to convert Kubernetes logs encoding to UTF-8"
msgstr ""
@@ -22803,6 +23299,9 @@ msgstr ""
msgid "View issue"
msgstr ""
+msgid "View issues"
+msgstr ""
+
msgid "View it on GitLab"
msgstr ""
@@ -22980,6 +23479,15 @@ msgstr ""
msgid "VulnerabilityManagement|Resolved %{timeago} by %{user}"
msgstr ""
+msgid "VulnerabilityManagement|Something went wrong while trying to delete the comment. Please try again later."
+msgstr ""
+
+msgid "VulnerabilityManagement|Something went wrong while trying to retrieve the vulnerability history. Please try again later."
+msgstr ""
+
+msgid "VulnerabilityManagement|Something went wrong while trying to save the comment. Please try again later."
+msgstr ""
+
msgid "VulnerabilityManagement|Something went wrong, could not create an issue."
msgstr ""
@@ -23010,6 +23518,9 @@ msgstr ""
msgid "VulnerabilityStatusTypes|Resolved"
msgstr ""
+msgid "Vulnerability|%{scannerName} (version %{scannerVersion})"
+msgstr ""
+
msgid "Vulnerability|Class"
msgstr ""
@@ -23043,7 +23554,10 @@ msgstr ""
msgid "Vulnerability|Project"
msgstr ""
-msgid "Vulnerability|Report Type"
+msgid "Vulnerability|Scanner Provider"
+msgstr ""
+
+msgid "Vulnerability|Scanner Type"
msgstr ""
msgid "Vulnerability|Severity"
@@ -23097,9 +23611,15 @@ msgstr ""
msgid "We sent you an email with reset password instructions"
msgstr ""
+msgid "We tried to automatically renew your %{strong}%{plan_name}%{strong_close} subscription for %{strong}%{namespace_name}%{strong_close} on %{expires_on} but something went wrong so your subscription was downgraded to the free plan. Don't worry, your data is safe. We suggest you check your payment method and get in touch with our support team (%{support_link}). They'll gladly help with your subscription renewal."
+msgstr ""
+
msgid "We want to be sure it is you, please confirm you are not a robot."
msgstr ""
+msgid "We will automatically renew your %{strong}%{plan_name}%{strong_close} subscription for %{strong}%{namespace_name}%{strong_close} on %{strong}%{expires_on}%{strong_close}. There's nothing that you need to do, we'll let you know when the renewal is complete. Need more seats, a higher plan or just want to review your payment method?"
+msgstr ""
+
msgid "We've found no vulnerabilities"
msgstr ""
@@ -23204,15 +23724,6 @@ msgstr ""
msgid "When:"
msgstr ""
-msgid "While it's rare to have no vulnerabilities for your group, it can happen. In any event, we ask that you double check your settings to make sure you've set up your dashboard correctly."
-msgstr ""
-
-msgid "While it's rare to have no vulnerabilities for your pipeline, it can happen. In any event, we ask that you double check your settings to make sure all security scanning jobs have passed successfully."
-msgstr ""
-
-msgid "While it's rare to have no vulnerabilities for your project, it can happen. In any event, we ask that you double check your settings to make sure you've set up your dashboard correctly."
-msgstr ""
-
msgid "While it's rare to have no vulnerabilities, it can happen. In any event, we ask that you please double check your settings to make sure you've set up your dashboard correctly."
msgstr ""
@@ -23243,9 +23754,6 @@ msgstr ""
msgid "Wiki"
msgstr ""
-msgid "Wiki pages"
-msgstr ""
-
msgid "Wiki was successfully updated."
msgstr ""
@@ -23429,6 +23937,9 @@ msgstr ""
msgid "Yes, add it"
msgstr ""
+msgid "Yes, close issue"
+msgstr ""
+
msgid "Yes, let me map Google Code users to full names or GitLab users."
msgstr ""
@@ -23624,6 +24135,9 @@ msgstr ""
msgid "You could not create a new trigger."
msgstr ""
+msgid "You didn't renew your %{strong}%{plan_name}%{strong_close} subscription for %{strong}%{namespace_name}%{strong_close} so it was downgraded to the free plan."
+msgstr ""
+
msgid "You didn't renew your %{strong}%{plan_name}%{strong_close} subscription so it was downgraded to the GitLab Core Plan."
msgstr ""
@@ -23690,6 +24204,9 @@ msgstr ""
msgid "You have declined the invitation to join %{label}."
msgstr ""
+msgid "You have imported from this project %{numberOfPreviousImportsForProject} times before. Each new import will create duplicate issues."
+msgstr ""
+
msgid "You have no permissions"
msgstr ""
@@ -23771,6 +24288,9 @@ msgstr ""
msgid "You need to upload a Google Takeout archive."
msgstr ""
+msgid "You reached %{usage_in_percent} of %{namespace_name}'s capacity (%{used_storage} of %{storage_limit})"
+msgstr ""
+
msgid "You tried to fork %{link_to_the_project} but it failed for the following reason:"
msgstr ""
@@ -23855,9 +24375,21 @@ msgstr ""
msgid "YouTube"
msgstr ""
+msgid "Your %{strong}%{plan_name}%{strong_close} subscription for %{strong}%{namespace_name}%{strong_close} will expire on %{strong}%{expires_on}%{strong_close}. After that, you will not to be able to create issues or merge requests as well as many other features."
+msgstr ""
+
msgid "Your %{strong}%{plan_name}%{strong_close} subscription will expire on %{strong}%{expires_on}%{strong_close}. After that, you will not to be able to create issues or merge requests as well as many other features."
msgstr ""
+msgid "Your CSV export has started. It will be emailed to %{email} when complete."
+msgstr ""
+
+msgid "Your CSV export of %{issues_count} from project %{project_link} has been added to this email as an attachment."
+msgstr ""
+
+msgid "Your CSV export of %{written_count} from project %{project_name} (%{project_url}) has been added to this email as an attachment."
+msgstr ""
+
msgid "Your Commit Email will be used for web based operations, such as edits and merges."
msgstr ""
@@ -23954,6 +24486,9 @@ msgstr ""
msgid "Your comment could not be updated! Please check your network connection and try again."
msgstr ""
+msgid "Your comment will be discarded."
+msgstr ""
+
msgid "Your custom stage '%{title}' was created"
msgstr ""
@@ -23993,6 +24528,9 @@ msgstr ""
msgid "Your new personal access token has been created."
msgstr ""
+msgid "Your new project access token has been created."
+msgstr ""
+
msgid "Your password isn't required to view this page. If a password or any other personal details are requested, please contact your administrator to report abuse."
msgstr ""
@@ -24011,12 +24549,18 @@ msgstr ""
msgid "Your request for access has been queued for review."
msgstr ""
+msgid "Your search didn't match any commits."
+msgstr ""
+
msgid "Your subscription expired!"
msgstr ""
msgid "Your subscription has been downgraded"
msgstr ""
+msgid "Your subscription will automatically renew in %{remaining_days}"
+msgstr ""
+
msgid "Your subscription will expire in %{remaining_days}"
msgstr ""
@@ -24055,7 +24599,7 @@ msgstr ""
msgid "allowed to fail"
msgstr ""
-msgid "already being used for another group or project milestone."
+msgid "already being used for another group or project %{timebox_name}."
msgstr ""
msgid "already has a \"created\" issue link"
@@ -24133,6 +24677,9 @@ msgstr ""
msgid "ciReport|%{linkStartTag}Learn more about SAST %{linkEndTag}"
msgstr ""
+msgid "ciReport|%{linkStartTag}Learn more about Secret Scanning %{linkEndTag}"
+msgstr ""
+
msgid "ciReport|%{linkStartTag}Learn more about codequality reports %{linkEndTag}"
msgstr ""
@@ -24296,6 +24843,12 @@ msgstr ""
msgid "ciReport|SAST"
msgstr ""
+msgid "ciReport|Secret scanning"
+msgstr ""
+
+msgid "ciReport|Secret scanning detects secrets and credentials vulnerabilities in your source code."
+msgstr ""
+
msgid "ciReport|Security scanning"
msgstr ""
@@ -24493,6 +25046,9 @@ msgstr ""
msgid "group"
msgstr ""
+msgid "groups"
+msgstr ""
+
msgid "has already been linked to another vulnerability"
msgstr ""
@@ -24656,9 +25212,6 @@ msgstr[1] ""
msgid "merged %{time_ago}"
msgstr ""
-msgid "milestone should belong either to a project or a group."
-msgstr ""
-
msgid "missing"
msgstr ""
@@ -24995,6 +25548,9 @@ msgstr ""
msgid "no contributions"
msgstr ""
+msgid "no expiration"
+msgstr ""
+
msgid "no one can merge"
msgstr ""
@@ -25047,6 +25603,9 @@ msgstr ""
msgid "pending removal"
msgstr ""
+msgid "per day"
+msgstr ""
+
msgid "pipeline"
msgstr ""
@@ -25075,6 +25634,12 @@ msgstr[1] ""
msgid "project avatar"
msgstr ""
+msgid "projects"
+msgstr ""
+
+msgid "push to your repository, create pipelines, create issues or add comments. To reduce storage capacity, delete unused repositories, artifacts, wikis, issues, and pipelines."
+msgstr ""
+
msgid "quick actions"
msgstr ""
@@ -25221,11 +25786,6 @@ msgstr ""
msgid "this document"
msgstr ""
-msgid "thread resolved"
-msgid_plural "threads resolved"
-msgstr[0] ""
-msgstr[1] ""
-
msgid "to help your contributors communicate effectively!"
msgstr ""
@@ -25280,6 +25840,9 @@ msgstr ""
msgid "view the blob"
msgstr ""
+msgid "vulnerability|Add a comment"
+msgstr ""
+
msgid "vulnerability|Add a comment or reason for dismissal"
msgstr ""
diff --git a/locale/he_IL/gitlab.po b/locale/he_IL/gitlab.po
index b48785a99c6..b25581c3683 100644
--- a/locale/he_IL/gitlab.po
+++ b/locale/he_IL/gitlab.po
@@ -12,7 +12,7 @@ msgstr ""
"X-Crowdin-Project: gitlab-ee\n"
"X-Crowdin-Language: he\n"
"X-Crowdin-File: /master/locale/gitlab.pot\n"
-"PO-Revision-Date: 2020-04-15 00:32\n"
+"PO-Revision-Date: 2020-05-05 21:11\n"
msgid " %{start} to %{end}"
msgstr ""
@@ -272,6 +272,13 @@ msgstr[1] ""
msgstr[2] ""
msgstr[3] ""
+msgid "%d unresolved thread"
+msgid_plural "%d unresolved threads"
+msgstr[0] ""
+msgstr[1] ""
+msgstr[2] ""
+msgstr[3] ""
+
msgid "%d vulnerability dismissed"
msgid_plural "%d vulnerabilities dismissed"
msgstr[0] ""
@@ -443,6 +450,9 @@ msgstr ""
msgid "%{mrText}, this issue will be closed automatically."
msgstr ""
+msgid "%{namespace_name} is now read-only. You cannot: %{base_message}"
+msgstr ""
+
msgid "%{name} contained %{resultsString}"
msgstr ""
@@ -455,6 +465,13 @@ msgstr ""
msgid "%{name}'s avatar"
msgstr ""
+msgid "%{no_of_days} day"
+msgid_plural "%{no_of_days} days"
+msgstr[0] ""
+msgstr[1] ""
+msgstr[2] ""
+msgstr[3] ""
+
msgid "%{number_commits_behind} commits behind %{default_branch}, %{number_commits_ahead} commits ahead"
msgstr ""
@@ -590,6 +607,9 @@ msgstr[3] ""
msgid "%{text} is available"
msgstr ""
+msgid "%{timebox_name} should belong either to a project or a group."
+msgstr ""
+
msgid "%{title} %{operator} %{threshold}"
msgstr ""
@@ -903,12 +923,18 @@ msgstr ""
msgid "<code>\"johnsmith@example.com\": \"johnsmith@example.com\"</code> will add \"By <a href=\"#\">johnsmith@example.com</a>\" to all issues and comments originally created by johnsmith@example.com. By default, the email address or username is masked to ensure the user's privacy. Use this option if you want to show the full email address."
msgstr ""
+msgid "<namespace / project>"
+msgstr ""
+
msgid "<no name set>"
msgstr ""
msgid "<no scopes selected>"
msgstr ""
+msgid "<project name>"
+msgstr ""
+
msgid "<strong>%{group_name}</strong> group members"
msgstr ""
@@ -942,6 +968,9 @@ msgstr ""
msgid "A basic page and serverless function that uses AWS Lambda, AWS API Gateway, and GitLab Pages"
msgstr ""
+msgid "A complete DevOps platform"
+msgstr ""
+
msgid "A default branch cannot be chosen for an empty project."
msgstr ""
@@ -1158,6 +1187,12 @@ msgstr ""
msgid "AccessTokens|reset it"
msgstr ""
+msgid "AccessibilityReport|Accessibility report artifact not found"
+msgstr ""
+
+msgid "AccessibilityReport|Failed to retrieve accessibility report"
+msgstr ""
+
msgid "AccessibilityReport|Learn More"
msgstr ""
@@ -1167,6 +1202,9 @@ msgstr ""
msgid "AccessibilityReport|New"
msgstr ""
+msgid "AccessibilityReport|The accessibility scanning found an error of the following type: %{code}"
+msgstr ""
+
msgid "Account"
msgstr ""
@@ -1411,6 +1449,9 @@ msgstr ""
msgid "Added at"
msgstr ""
+msgid "Added for this merge request"
+msgstr ""
+
msgid "Added in this version"
msgstr ""
@@ -1784,6 +1825,72 @@ msgstr[1] ""
msgstr[2] ""
msgstr[3] ""
+msgid "Alert Details"
+msgstr ""
+
+msgid "AlertManagement|Acknowledged"
+msgstr ""
+
+msgid "AlertManagement|Alert"
+msgstr ""
+
+msgid "AlertManagement|Authorize external service"
+msgstr ""
+
+msgid "AlertManagement|Display alerts from all your monitoring tools directly within GitLab. Streamline the investigation of your alerts and the escalation of alerts to incidents."
+msgstr ""
+
+msgid "AlertManagement|End time"
+msgstr ""
+
+msgid "AlertManagement|End time:"
+msgstr ""
+
+msgid "AlertManagement|Events"
+msgstr ""
+
+msgid "AlertManagement|Events:"
+msgstr ""
+
+msgid "AlertManagement|Full Alert Details"
+msgstr ""
+
+msgid "AlertManagement|More information"
+msgstr ""
+
+msgid "AlertManagement|No alerts available to display. If you think you're seeing this message in error, refresh the page."
+msgstr ""
+
+msgid "AlertManagement|No alerts to display."
+msgstr ""
+
+msgid "AlertManagement|Overview"
+msgstr ""
+
+msgid "AlertManagement|Resolved"
+msgstr ""
+
+msgid "AlertManagement|Severity"
+msgstr ""
+
+msgid "AlertManagement|Start time"
+msgstr ""
+
+msgid "AlertManagement|Start time:"
+msgstr ""
+
+msgid "AlertManagement|Status"
+msgstr ""
+
+msgid "AlertManagement|Surface alerts in GitLab"
+msgstr ""
+
+msgid "AlertManagement|There was an error displaying the alerts. Confirm your endpoint's configuration details to ensure alerts appear."
+msgstr ""
+
+msgid "AlertManagement|Triggered"
+msgstr ""
+
msgid "AlertService|%{linkStart}Learn more%{linkEnd} about configuring this endpoint to receive alerts."
msgstr ""
@@ -1853,6 +1960,9 @@ msgstr ""
msgid "All security scans are enabled because %{linkStart}Auto DevOps%{linkEnd} is enabled on this project"
msgstr ""
+msgid "All threads resolved"
+msgstr ""
+
msgid "All users"
msgstr ""
@@ -1871,6 +1981,9 @@ msgstr ""
msgid "Allow only the selected protocols to be used for Git access."
msgstr ""
+msgid "Allow owners to manage default branch protection per group"
+msgstr ""
+
msgid "Allow owners to manually add users outside of LDAP"
msgstr ""
@@ -1979,6 +2092,9 @@ msgstr ""
msgid "An error occurred fetching the dropdown data."
msgstr ""
+msgid "An error occurred fetching the project authors."
+msgstr ""
+
msgid "An error occurred previewing the blob"
msgstr ""
@@ -2060,6 +2176,9 @@ msgstr ""
msgid "An error occurred while fetching sidebar data"
msgstr ""
+msgid "An error occurred while fetching terraform reports."
+msgstr ""
+
msgid "An error occurred while fetching the Service Desk address."
msgstr ""
@@ -2297,6 +2416,9 @@ msgstr ""
msgid "Any"
msgstr ""
+msgid "Any Author"
+msgstr ""
+
msgid "Any Label"
msgstr ""
@@ -2520,10 +2642,10 @@ msgstr ""
msgid "Are you sure that you want to unarchive this project?"
msgstr ""
-msgid "Are you sure you want to cancel creating this comment?"
+msgid "Are you sure you want to cancel editing this comment?"
msgstr ""
-msgid "Are you sure you want to cancel editing this comment?"
+msgid "Are you sure you want to close this blocked issue?"
msgstr ""
msgid "Are you sure you want to delete %{name}?"
@@ -2553,6 +2675,9 @@ msgstr ""
msgid "Are you sure you want to deploy this environment?"
msgstr ""
+msgid "Are you sure you want to discard this comment?"
+msgstr ""
+
msgid "Are you sure you want to erase this build?"
msgstr ""
@@ -2731,6 +2856,9 @@ msgstr ""
msgid "At least one approval from a code owner is required to change files matching the respective CODEOWNER rules."
msgstr ""
+msgid "At least one logging option is required to be enabled"
+msgstr ""
+
msgid "At least one of group_id or project_id must be specified"
msgstr ""
@@ -2771,6 +2899,27 @@ msgstr ""
msgid "AuditEvents|Target"
msgstr ""
+msgid "AuditLogs|(removed)"
+msgstr ""
+
+msgid "AuditLogs|Action"
+msgstr ""
+
+msgid "AuditLogs|Author"
+msgstr ""
+
+msgid "AuditLogs|Date"
+msgstr ""
+
+msgid "AuditLogs|IP Address"
+msgstr ""
+
+msgid "AuditLogs|Object"
+msgstr ""
+
+msgid "AuditLogs|Target"
+msgstr ""
+
msgid "Aug"
msgstr ""
@@ -3053,9 +3202,6 @@ msgstr ""
msgid "BambooService|You must set up automatic revision labeling and a repository trigger in Bamboo."
msgstr ""
-msgid "Batch operations"
-msgstr ""
-
msgid "BatchComments|Delete all pending comments"
msgstr ""
@@ -3185,6 +3331,9 @@ msgstr ""
msgid "Boards"
msgstr ""
+msgid "Boards and Board Lists"
+msgstr ""
+
msgid "Boards|Collapse"
msgstr ""
@@ -3404,6 +3553,9 @@ msgstr ""
msgid "Buy GitLab Enterprise Edition"
msgstr ""
+msgid "Buy more Pipeline minutes"
+msgstr ""
+
msgid "By %{user_name}"
msgstr ""
@@ -3524,6 +3676,9 @@ msgstr ""
msgid "Can't scan the code?"
msgstr ""
+msgid "Can't update snippet: %{err}"
+msgstr ""
+
msgid "Canary"
msgstr ""
@@ -3692,6 +3847,9 @@ msgstr ""
msgid "Changing group path can have unintended side effects."
msgstr ""
+msgid "Charts"
+msgstr ""
+
msgid "Charts can't be displayed as the request for data has timed out. %{documentationLink}"
msgstr ""
@@ -3728,9 +3886,6 @@ msgstr ""
msgid "ChatMessage|and [%{count} more](%{pipeline_failed_jobs_url})"
msgstr ""
-msgid "ChatMessage|failed"
-msgstr ""
-
msgid "ChatMessage|has failed"
msgstr ""
@@ -3746,9 +3901,6 @@ msgstr ""
msgid "ChatMessage|in %{project_link}"
msgstr ""
-msgid "ChatMessage|passed"
-msgstr ""
-
msgid "Check again"
msgstr ""
@@ -4199,9 +4351,6 @@ msgstr ""
msgid "Click any <strong>project name</strong> in the project list below to navigate to the project milestone."
msgstr ""
-msgid "Click here"
-msgstr ""
-
msgid "Click the <strong>Download</strong> button and wait for downloading to complete."
msgstr ""
@@ -4589,6 +4738,12 @@ msgstr ""
msgid "ClusterIntegration|Fetching zones"
msgstr ""
+msgid "ClusterIntegration|Fluentd"
+msgstr ""
+
+msgid "ClusterIntegration|Fluentd is an open source data collector, which lets you unify the data collection and consumption for a better use and understanding of data. It requires at least one of the following logs to be successfully installed."
+msgstr ""
+
msgid "ClusterIntegration|GitLab Integration"
msgstr ""
@@ -4895,6 +5050,15 @@ msgstr ""
msgid "ClusterIntegration|Request to begin uninstalling failed"
msgstr ""
+msgid "ClusterIntegration|SIEM Hostname"
+msgstr ""
+
+msgid "ClusterIntegration|SIEM Port"
+msgstr ""
+
+msgid "ClusterIntegration|SIEM Protocol"
+msgstr ""
+
msgid "ClusterIntegration|Save changes"
msgstr ""
@@ -4991,6 +5155,12 @@ msgstr ""
msgid "ClusterIntegration|Select zone to choose machine type"
msgstr ""
+msgid "ClusterIntegration|Send Cilium Logs"
+msgstr ""
+
+msgid "ClusterIntegration|Send ModSecurity Logs"
+msgstr ""
+
msgid "ClusterIntegration|Service Token"
msgstr ""
@@ -5421,18 +5591,33 @@ msgstr ""
msgid "Compliance framework (optional)"
msgstr ""
+msgid "ComplianceFramework|GDPR"
+msgstr ""
+
msgid "ComplianceFramework|GDPR - General Data Protection Regulation"
msgstr ""
+msgid "ComplianceFramework|HIPAA"
+msgstr ""
+
msgid "ComplianceFramework|HIPAA - Health Insurance Portability and Accountability Act"
msgstr ""
+msgid "ComplianceFramework|PCI-DSS"
+msgstr ""
+
msgid "ComplianceFramework|PCI-DSS - Payment Card Industry-Data Security Standard"
msgstr ""
+msgid "ComplianceFramework|SOC 2"
+msgstr ""
+
msgid "ComplianceFramework|SOC 2 - Service Organization Control 2"
msgstr ""
+msgid "ComplianceFramework|SOX"
+msgstr ""
+
msgid "ComplianceFramework|SOX - Sarbanes-Oxley"
msgstr ""
@@ -5580,9 +5765,15 @@ msgstr ""
msgid "Container repositories sync capacity"
msgstr ""
+msgid "ContainerRegistry| Please visit the %{linkStart}administration settings%{linkEnd} to enable this feature."
+msgstr ""
+
msgid "ContainerRegistry|%{imageName} tags"
msgstr ""
+msgid "ContainerRegistry|%{title} was successfully scheduled for deletion"
+msgstr ""
+
msgid "ContainerRegistry|Automatically remove extra images that aren't designed to be kept."
msgstr ""
@@ -5595,6 +5786,9 @@ msgstr ""
msgid "ContainerRegistry|Container Registry"
msgstr ""
+msgid "ContainerRegistry|Container Registry tag expiration and retention policy is disabled"
+msgstr ""
+
msgid "ContainerRegistry|Copy build command"
msgstr ""
@@ -5604,18 +5798,12 @@ msgstr ""
msgid "ContainerRegistry|Copy push command"
msgstr ""
-msgid "ContainerRegistry|Currently, the Container Registry tag expiration feature is not available for projects created before GitLab version 12.8. For updates and more information, visit Issue %{linkStart}#196124%{linkEnd}"
-msgstr ""
-
msgid "ContainerRegistry|Docker connection error"
msgstr ""
msgid "ContainerRegistry|Docker tag expiration policy is %{toggleStatus}"
msgstr ""
-msgid "ContainerRegistry|Docker tags with names matching this regex pattern will expire:"
-msgstr ""
-
msgid "ContainerRegistry|Edit Settings"
msgstr ""
@@ -5637,9 +5825,6 @@ msgstr ""
msgid "ContainerRegistry|Image ID"
msgstr ""
-msgid "ContainerRegistry|Image deleted successfully"
-msgstr ""
-
msgid "ContainerRegistry|Keep and protect the images that matter most."
msgstr ""
@@ -5655,12 +5840,24 @@ msgstr ""
msgid "ContainerRegistry|Number of tags to retain:"
msgstr ""
+msgid "ContainerRegistry|Please contact your administrator."
+msgstr ""
+
msgid "ContainerRegistry|Push an image"
msgstr ""
msgid "ContainerRegistry|Quick Start"
msgstr ""
+msgid "ContainerRegistry|Regular expressions such as %{codeStart}.*-test%{codeEnd} or %{codeStart}dev-.*%{codeEnd} are supported"
+msgstr ""
+
+msgid "ContainerRegistry|Regular expressions such as %{codeStart}.*-test%{codeEnd} or %{codeStart}dev-.*%{codeEnd} are supported. To select all tags, use %{codeStart}.*%{codeEnd}"
+msgstr ""
+
+msgid "ContainerRegistry|Remember to run %{docLinkStart}garbage collection%{docLinkEnd} to remove the stale data from storage."
+msgstr ""
+
msgid "ContainerRegistry|Remove repository"
msgstr ""
@@ -5677,22 +5874,22 @@ msgstr[3] ""
msgid "ContainerRegistry|Retention policy has been Enabled"
msgstr ""
-msgid "ContainerRegistry|Something went wrong while deleting the image."
+msgid "ContainerRegistry|Something went wrong while fetching the expiration policy."
msgstr ""
-msgid "ContainerRegistry|Something went wrong while deleting the tag."
+msgid "ContainerRegistry|Something went wrong while fetching the repository list."
msgstr ""
-msgid "ContainerRegistry|Something went wrong while deleting the tags."
+msgid "ContainerRegistry|Something went wrong while fetching the tags list."
msgstr ""
-msgid "ContainerRegistry|Something went wrong while fetching the expiration policy."
+msgid "ContainerRegistry|Something went wrong while marking the tag for deletion."
msgstr ""
-msgid "ContainerRegistry|Something went wrong while fetching the packages list."
+msgid "ContainerRegistry|Something went wrong while marking the tags for deletion."
msgstr ""
-msgid "ContainerRegistry|Something went wrong while fetching the tags list."
+msgid "ContainerRegistry|Something went wrong while scheduling %{title} for deletion. Please try again."
msgstr ""
msgid "ContainerRegistry|Something went wrong while updating the expiration policy."
@@ -5701,16 +5898,25 @@ msgstr ""
msgid "ContainerRegistry|Tag"
msgstr ""
-msgid "ContainerRegistry|Tag deleted successfully"
-msgstr ""
-
msgid "ContainerRegistry|Tag expiration policy"
msgstr ""
msgid "ContainerRegistry|Tag expiration policy is designed to:"
msgstr ""
-msgid "ContainerRegistry|Tags deleted successfully"
+msgid "ContainerRegistry|Tag successfully marked for deletion."
+msgstr ""
+
+msgid "ContainerRegistry|Tags successfully marked for deletion."
+msgstr ""
+
+msgid "ContainerRegistry|Tags with names matching this regex pattern will %{italicStart}be preserved:%{italicEnd}"
+msgstr ""
+
+msgid "ContainerRegistry|Tags with names matching this regex pattern will %{italicStart}expire:%{italicEnd}"
+msgstr ""
+
+msgid "ContainerRegistry|The Container Registry tag expiration and retention policies for this project have not been enabled."
msgstr ""
msgid "ContainerRegistry|The last tag related to this image was recently removed. This empty image and any associated data will be automatically removed as part of the regular Garbage Collection process. If you have any questions, contact your administrator."
@@ -5731,16 +5937,16 @@ msgstr ""
msgid "ContainerRegistry|There are no container images stored for this project"
msgstr ""
-msgid "ContainerRegistry|This Registry contains deleted image tag data. Remember to run %{docLinkStart}garbage collection%{docLinkEnd} to remove the stale data from storage."
+msgid "ContainerRegistry|There was an error during the deletion of this image repository, please try again."
msgstr ""
msgid "ContainerRegistry|This image has no active tags"
msgstr ""
-msgid "ContainerRegistry|We are having trouble connecting to Docker, which could be due to an issue with your project name or path. %{docLinkStart}More Information%{docLinkEnd}"
+msgid "ContainerRegistry|This image repository is scheduled for deletion"
msgstr ""
-msgid "ContainerRegistry|Wildcards such as %{codeStart}.*-stable%{codeEnd} or %{codeStart}production/.*%{codeEnd} are supported. To select all tags, use %{codeStart}.*%{codeEnd}"
+msgid "ContainerRegistry|We are having trouble connecting to Docker, which could be due to an issue with your project name or path. %{docLinkStart}More Information%{docLinkEnd}"
msgstr ""
msgid "ContainerRegistry|With the Container Registry, every project can have its own space to store its Docker images. %{docLinkStart}More Information%{docLinkEnd}"
@@ -5995,7 +6201,7 @@ msgstr ""
msgid "Could not delete chat nickname %{chat_name}."
msgstr ""
-msgid "Could not find design"
+msgid "Could not find design."
msgstr ""
msgid "Could not remove the trigger."
@@ -6010,6 +6216,9 @@ msgstr ""
msgid "Could not revoke personal access token %{personal_access_token_name}."
msgstr ""
+msgid "Could not revoke project access token %{project_access_token_name}."
+msgstr ""
+
msgid "Could not save group ID"
msgstr ""
@@ -6067,6 +6276,9 @@ msgstr ""
msgid "Create a new branch"
msgstr ""
+msgid "Create a new deploy key for this project"
+msgstr ""
+
msgid "Create a new file as there are no files yet. Afterwards, you'll be able to commit your changes."
msgstr ""
@@ -6172,6 +6384,9 @@ msgstr ""
msgid "Create requirement"
msgstr ""
+msgid "Create snippet"
+msgstr ""
+
msgid "Create wildcard: %{searchTerm}"
msgstr ""
@@ -6193,6 +6408,9 @@ msgstr ""
msgid "Created"
msgstr ""
+msgid "Created %{timestamp}"
+msgstr ""
+
msgid "Created At"
msgstr ""
@@ -6551,6 +6769,9 @@ msgstr ""
msgid "CycleAnalytics|stage dropdown"
msgstr ""
+msgid "DAG"
+msgstr ""
+
msgid "DNS"
msgstr ""
@@ -6929,6 +7150,9 @@ msgstr ""
msgid "Deploy key was successfully updated."
msgstr ""
+msgid "Deploy keys allow read-only or read-write (if enabled) access to your repository. Deploy keys can be used for CI, staging or production servers. You can create a deploy key or add an existing one."
+msgstr ""
+
msgid "Deploy progress not found. To see pods, ensure your environment matches %{linkStart}deploy board criteria%{linkEnd}."
msgstr ""
@@ -7082,6 +7306,9 @@ msgstr ""
msgid "Deploying to"
msgstr ""
+msgid "Deployment Frequency"
+msgstr ""
+
msgid "Deployment|API"
msgstr ""
@@ -7602,9 +7829,6 @@ msgstr ""
msgid "Edit environment"
msgstr ""
-msgid "Edit epic description"
-msgstr ""
-
msgid "Edit file"
msgstr ""
@@ -8226,9 +8450,6 @@ msgstr ""
msgid "Epics"
msgstr ""
-msgid "Epics (Ultimate / Gold license only)"
-msgstr ""
-
msgid "Epics Roadmap"
msgstr ""
@@ -8238,6 +8459,9 @@ msgstr ""
msgid "Epics let you manage your portfolio of projects more efficiently and with less effort"
msgstr ""
+msgid "Epics, Issues, and Merge Requests"
+msgstr ""
+
msgid "Epics|Add an epic"
msgstr ""
@@ -8289,6 +8513,9 @@ msgstr ""
msgid "Epics|Something went wrong while fetching group epics."
msgstr ""
+msgid "Epics|Something went wrong while moving item."
+msgstr ""
+
msgid "Epics|Something went wrong while ordering item."
msgstr ""
@@ -8343,6 +8570,9 @@ msgstr ""
msgid "Error fetching network graph."
msgstr ""
+msgid "Error fetching payload data."
+msgstr ""
+
msgid "Error fetching projects"
msgstr ""
@@ -8352,9 +8582,6 @@ msgstr ""
msgid "Error fetching the dependency list. Please check your network connection and try again."
msgstr ""
-msgid "Error fetching usage ping data."
-msgstr ""
-
msgid "Error loading branch data. Please try again."
msgstr ""
@@ -8496,6 +8723,9 @@ msgstr ""
msgid "Errors"
msgstr ""
+msgid "Errors:"
+msgstr ""
+
msgid "Estimated"
msgstr ""
@@ -8700,12 +8930,18 @@ msgstr ""
msgid "Export as CSV"
msgstr ""
+msgid "Export group"
+msgstr ""
+
msgid "Export issues"
msgstr ""
msgid "Export project"
msgstr ""
+msgid "Export this group with all related data to a new GitLab instance. Once complete, you can import the data file from the \"New Group\" page."
+msgstr ""
+
msgid "Export this project with all its related data in order to move your project to a new GitLab instance. Once the export is finished, you can import the file from the \"New Project\" page."
msgstr ""
@@ -8775,9 +9011,6 @@ msgstr ""
msgid "Failed Jobs"
msgstr ""
-msgid "Failed create wiki"
-msgstr ""
-
msgid "Failed to add a Zoom meeting"
msgstr ""
@@ -8817,6 +9050,9 @@ msgstr ""
msgid "Failed to create resources"
msgstr ""
+msgid "Failed to create wiki"
+msgstr ""
+
msgid "Failed to delete board. Please try again."
msgstr ""
@@ -8826,7 +9062,7 @@ msgstr ""
msgid "Failed to enqueue the rebase operation, possibly due to a long-lived transaction. Try again later."
msgstr ""
-msgid "Failed to find import label for jira import."
+msgid "Failed to find import label for Jira import."
msgstr ""
msgid "Failed to get ref."
@@ -8850,6 +9086,12 @@ msgstr ""
msgid "Failed to load groups & users."
msgstr ""
+msgid "Failed to load labels. Please try again."
+msgstr ""
+
+msgid "Failed to load milestones. Please try again."
+msgstr ""
+
msgid "Failed to load related branches"
msgstr ""
@@ -9225,7 +9467,10 @@ msgstr ""
msgid "Filter by milestone name"
msgstr ""
-msgid "Filter by name..."
+msgid "Filter by name"
+msgstr ""
+
+msgid "Filter by status"
msgstr ""
msgid "Filter by two-factor authentication"
@@ -9237,12 +9482,18 @@ msgstr ""
msgid "Filter projects"
msgstr ""
+msgid "Filter results"
+msgstr ""
+
msgid "Filter results by group"
msgstr ""
msgid "Filter results by project"
msgstr ""
+msgid "Filter results..."
+msgstr ""
+
msgid "Filter your projects by name"
msgstr ""
@@ -9531,10 +9782,10 @@ msgstr ""
msgid "GeoNodes|Checksummed"
msgstr ""
-msgid "GeoNodes|Container repositories"
+msgid "GeoNodes|Consult Geo troubleshooting information"
msgstr ""
-msgid "GeoNodes|Data is out of date from %{timeago}"
+msgid "GeoNodes|Container repositories"
msgstr ""
msgid "GeoNodes|Data replication lag"
@@ -9576,6 +9827,9 @@ msgstr ""
msgid "GeoNodes|Last event ID seen from primary"
msgstr ""
+msgid "GeoNodes|Learn more about Geo node statuses"
+msgstr ""
+
msgid "GeoNodes|Loading nodes"
msgstr ""
@@ -9591,6 +9845,9 @@ msgstr ""
msgid "GeoNodes|Node was successfully removed."
msgstr ""
+msgid "GeoNodes|Node's status was updated %{timeAgo}."
+msgstr ""
+
msgid "GeoNodes|Not checksummed"
msgstr ""
@@ -9621,7 +9878,10 @@ msgstr ""
msgid "GeoNodes|Repository verification progress"
msgstr ""
-msgid "GeoNodes|Selective"
+msgid "GeoNodes|Selective (%{syncLabel})"
+msgstr ""
+
+msgid "GeoNodes|Selective synchronization"
msgstr ""
msgid "GeoNodes|Something went wrong while changing node status"
@@ -9648,6 +9908,9 @@ msgstr ""
msgid "GeoNodes|Unverified"
msgstr ""
+msgid "GeoNodes|Updated %{timeAgo}"
+msgstr ""
+
msgid "GeoNodes|Used slots"
msgstr ""
@@ -9687,16 +9950,16 @@ msgstr ""
msgid "Geo|All"
msgstr ""
-msgid "Geo|All projects"
+msgid "Geo|All %{replicable_type}"
msgstr ""
-msgid "Geo|All projects are being scheduled for re-sync"
+msgid "Geo|All projects"
msgstr ""
-msgid "Geo|All projects are being scheduled for re-verify"
+msgid "Geo|All projects are being scheduled for resync"
msgstr ""
-msgid "Geo|Batch operations"
+msgid "Geo|All projects are being scheduled for reverify"
msgstr ""
msgid "Geo|Could not remove tracking entry for an existing project."
@@ -9708,9 +9971,15 @@ msgstr ""
msgid "Geo|Failed"
msgstr ""
+msgid "Geo|Filter by status"
+msgstr ""
+
msgid "Geo|Geo Status"
msgstr ""
+msgid "Geo|In progress"
+msgstr ""
+
msgid "Geo|In sync"
msgstr ""
@@ -9735,9 +10004,6 @@ msgstr ""
msgid "Geo|Not synced yet"
msgstr ""
-msgid "Geo|Pending"
-msgstr ""
-
msgid "Geo|Pending synchronization"
msgstr ""
@@ -9774,7 +10040,7 @@ msgstr ""
msgid "Geo|Resync"
msgstr ""
-msgid "Geo|Resync all projects"
+msgid "Geo|Resync all"
msgstr ""
msgid "Geo|Retry count"
@@ -9783,7 +10049,7 @@ msgstr ""
msgid "Geo|Reverify"
msgstr ""
-msgid "Geo|Reverify all projects"
+msgid "Geo|Reverify all"
msgstr ""
msgid "Geo|Status"
@@ -10125,9 +10391,6 @@ msgstr ""
msgid "Go to file"
msgstr ""
-msgid "Go to file (MRs only)"
-msgstr ""
-
msgid "Go to file permalink (while viewing a file)"
msgstr ""
@@ -10572,6 +10835,9 @@ msgstr ""
msgid "GroupSettings|Disable group mentions"
msgstr ""
+msgid "GroupSettings|Export group"
+msgstr ""
+
msgid "GroupSettings|If the parent group's visibility is lower than the group current visibility, visibility levels for subgroups and projects will be changed to match the new parent group's visibility."
msgstr ""
@@ -10650,6 +10916,9 @@ msgstr ""
msgid "Groups (%{count})"
msgstr ""
+msgid "Groups (%{groups})"
+msgstr ""
+
msgid "Groups can also be nested by creating %{subgroup_docs_link_start}subgroups%{subgroup_docs_link_end}."
msgstr ""
@@ -10919,15 +11188,12 @@ msgstr ""
msgid "ID:"
msgstr ""
-msgid "IDE|Allow live previews of JavaScript projects in the Web IDE using CodeSandbox client side evaluation."
+msgid "IDE|Allow live previews of JavaScript projects in the Web IDE using CodeSandbox Live Preview."
msgstr ""
msgid "IDE|Back"
msgstr ""
-msgid "IDE|Client side evaluation"
-msgstr ""
-
msgid "IDE|Commit"
msgstr ""
@@ -10991,6 +11257,9 @@ msgstr ""
msgid "If any job surpasses this timeout threshold, it will be marked as failed. Human readable time input language is accepted like \"1 hour\". Values without specification represent seconds."
msgstr ""
+msgid "If blank, set allowable lifetime to %{instance_level_policy_in_words}, as defined by the instance admin. Once set, existing tokens for users in this group may be revoked."
+msgstr ""
+
msgid "If checked, group owners can manage LDAP group links and LDAP member overrides"
msgstr ""
@@ -11030,6 +11299,9 @@ msgstr ""
msgid "If you lose your recovery codes you can generate new ones, invalidating all previous codes."
msgstr ""
+msgid "If you reach 100%% storage capacity, you will not be able to: %{base_message}"
+msgstr ""
+
msgid "If your HTTP repository is not publicly accessible, add your credentials."
msgstr ""
@@ -11141,6 +11413,9 @@ msgstr ""
msgid "Import repository"
msgstr ""
+msgid "Import started by: %{importInitiator}"
+msgstr ""
+
msgid "Import tasks"
msgstr ""
@@ -11207,6 +11482,9 @@ msgstr ""
msgid "In order to tailor your experience with GitLab we<br>would like to know a bit more about you."
msgstr ""
+msgid "In progress"
+msgstr ""
+
msgid "In the next step, you'll be able to select the projects you want to import."
msgstr ""
@@ -11556,15 +11834,15 @@ msgstr ""
msgid "Issues"
msgstr ""
-msgid "Issues / Merge Requests"
-msgstr ""
-
msgid "Issues Analytics"
msgstr ""
msgid "Issues Rate Limits"
msgstr ""
+msgid "Issues and Merge Requests"
+msgstr ""
+
msgid "Issues can be bugs, tasks or ideas to be discussed. Also, issues are searchable and filterable."
msgstr ""
@@ -11586,10 +11864,10 @@ msgstr ""
msgid "IssuesAnalytics|Avg/Month:"
msgstr ""
-msgid "IssuesAnalytics|Issues created"
+msgid "IssuesAnalytics|Issues opened"
msgstr ""
-msgid "IssuesAnalytics|Issues created per month"
+msgid "IssuesAnalytics|Issues opened per month"
msgstr ""
msgid "IssuesAnalytics|Last 12 months"
@@ -11643,6 +11921,9 @@ msgstr ""
msgid "Jira integration not configured."
msgstr ""
+msgid "Jira project: %{importProject}"
+msgstr ""
+
msgid "JiraService|Events for %{noteable_model_name} are disabled."
msgstr ""
@@ -12034,6 +12315,9 @@ msgstr ""
msgid "Lead"
msgstr ""
+msgid "Lead Time"
+msgstr ""
+
msgid "Learn GitLab"
msgstr ""
@@ -12046,6 +12330,9 @@ msgstr ""
msgid "Learn how to %{no_packages_link_start}publish and share your packages%{no_packages_link_end} with GitLab."
msgstr ""
+msgid "Learn how to enable synchronization"
+msgstr ""
+
msgid "Learn more"
msgstr ""
@@ -12387,9 +12674,6 @@ msgstr ""
msgid "Live preview"
msgstr ""
-msgid "Load more vulnerabilities"
-msgstr ""
-
msgid "Loading"
msgstr ""
@@ -12540,9 +12824,6 @@ msgstr ""
msgid "Manage"
msgstr ""
-msgid "Manage Git repositories with fine-grained access controls that keep your code secure. Perform code reviews and enhance collaboration with merge requests. Each project can also have an issue tracker and a wiki."
-msgstr ""
-
msgid "Manage Web IDE features"
msgstr ""
@@ -12573,6 +12854,9 @@ msgstr ""
msgid "Manage your license"
msgstr ""
+msgid "Managed Account"
+msgstr ""
+
msgid "Manifest"
msgstr ""
@@ -12837,6 +13121,9 @@ msgstr ""
msgid "Merge Requests in Review"
msgstr ""
+msgid "Merge automatically (%{strategy})"
+msgstr ""
+
msgid "Merge commit message"
msgstr ""
@@ -12990,6 +13277,12 @@ msgstr ""
msgid "Merged branches are being deleted. This can take some time depending on the number of branches. Please refresh the page to see changes."
msgstr ""
+msgid "Merged this merge request."
+msgstr ""
+
+msgid "Merges this merge request immediately."
+msgstr ""
+
msgid "Merges this merge request when the pipeline succeeds."
msgstr ""
@@ -13099,12 +13392,15 @@ msgstr[1] ""
msgstr[2] ""
msgstr[3] ""
-msgid "Metrics|Environment"
+msgid "Metrics|Expand panel"
msgstr ""
msgid "Metrics|For grouping similar metrics"
msgstr ""
+msgid "Metrics|Go back (Esc)"
+msgstr ""
+
msgid "Metrics|Invalid time range, please verify."
msgstr ""
@@ -13135,9 +13431,6 @@ msgstr ""
msgid "Metrics|Refresh dashboard"
msgstr ""
-msgid "Metrics|Show last"
-msgstr ""
-
msgid "Metrics|There was an error creating the dashboard."
msgstr ""
@@ -13216,6 +13509,9 @@ msgstr ""
msgid "Microsoft Azure"
msgstr ""
+msgid "Middleman project with Static Site Editor support"
+msgstr ""
+
msgid "Migrated %{success_count}/%{total_count} files."
msgstr ""
@@ -13665,10 +13961,10 @@ msgstr ""
msgid "Next"
msgstr ""
-msgid "Next file in diff (MRs only)"
+msgid "Next file in diff"
msgstr ""
-msgid "Next unresolved discussion (MRs only)"
+msgid "Next unresolved discussion"
msgstr ""
msgid "Nickname"
@@ -13728,6 +14024,9 @@ msgstr ""
msgid "No changes between %{ref_start}%{source_branch}%{ref_end} and %{ref_start}%{target_branch}%{ref_end}"
msgstr ""
+msgid "No child epics match applied filters"
+msgstr ""
+
msgid "No connection could be made to a Gitaly Server, please check your logs!"
msgstr ""
@@ -13866,15 +14165,6 @@ msgstr ""
msgid "No thanks, don't show this again"
msgstr ""
-msgid "No vulnerabilities found for this group"
-msgstr ""
-
-msgid "No vulnerabilities found for this pipeline"
-msgstr ""
-
-msgid "No vulnerabilities found for this project"
-msgstr ""
-
msgid "No vulnerabilities present"
msgstr ""
@@ -13998,6 +14288,9 @@ msgstr ""
msgid "Nothing to preview."
msgstr ""
+msgid "Nothing to synchronize"
+msgstr ""
+
msgid "Notification events"
msgstr ""
@@ -14169,6 +14462,9 @@ msgstr ""
msgid "Once removed, the fork relationship cannot be restored and you will no longer be able to send merge requests to the source."
msgstr ""
+msgid "Once the exported file is ready you can download it from this page."
+msgstr ""
+
msgid "Once the exported file is ready, you will receive a notification email with a download link, or you can download it from this page."
msgstr ""
@@ -14272,9 +14568,6 @@ msgstr ""
msgid "Open sidebar"
msgstr ""
-msgid "Open source software to collaborate on code"
-msgstr ""
-
msgid "Open: %{openIssuesCount}"
msgstr ""
@@ -14377,6 +14670,9 @@ msgstr ""
msgid "OutdatedBrowser|You can provide feedback %{feedback_link_start}on this issue%{feedback_link_end} or via your usual support channels."
msgstr ""
+msgid "Overridden"
+msgstr ""
+
msgid "Overview"
msgstr ""
@@ -14428,6 +14724,9 @@ msgstr ""
msgid "PackageRegistry|Conan Command"
msgstr ""
+msgid "PackageRegistry|Copy .pypirc content"
+msgstr ""
+
msgid "PackageRegistry|Copy Conan Command"
msgstr ""
@@ -14449,6 +14748,9 @@ msgstr ""
msgid "PackageRegistry|Copy NuGet Setup Command"
msgstr ""
+msgid "PackageRegistry|Copy Pip command"
+msgstr ""
+
msgid "PackageRegistry|Copy and paste this inside your %{codeStart}pom.xml%{codeEnd} %{codeStart}dependencies%{codeEnd} block."
msgstr ""
@@ -14482,6 +14784,12 @@ msgstr ""
msgid "PackageRegistry|For more information on the NuGet registry, %{linkStart}see the documentation%{linkEnd}."
msgstr ""
+msgid "PackageRegistry|For more information on the PyPi registry, %{linkStart}see the documentation%{linkEnd}."
+msgstr ""
+
+msgid "PackageRegistry|If you haven't already done so, you will need to add the below to your %{codeStart}.pypirc%{codeEnd} file."
+msgstr ""
+
msgid "PackageRegistry|If you haven't already done so, you will need to add the below to your %{codeStart}pom.xml%{codeEnd} file."
msgstr ""
@@ -14491,6 +14799,9 @@ msgstr ""
msgid "PackageRegistry|Learn how to %{noPackagesLinkStart}publish and share your packages%{noPackagesLinkEnd} with GitLab."
msgstr ""
+msgid "PackageRegistry|Manually Published"
+msgstr ""
+
msgid "PackageRegistry|Maven"
msgstr ""
@@ -14509,12 +14820,18 @@ msgstr ""
msgid "PackageRegistry|NuGet Command"
msgstr ""
+msgid "PackageRegistry|Pip Command"
+msgstr ""
+
msgid "PackageRegistry|Pipeline %{linkStart}%{linkEnd} triggered %{timestamp} by %{author}"
msgstr ""
msgid "PackageRegistry|Published to the repository at %{timestamp}"
msgstr ""
+msgid "PackageRegistry|PyPi"
+msgstr ""
+
msgid "PackageRegistry|Registry Setup"
msgstr ""
@@ -14551,6 +14868,9 @@ msgstr ""
msgid "PackageRegistry|npm"
msgstr ""
+msgid "PackageRegistry|published by %{author}"
+msgstr ""
+
msgid "PackageRegistry|yarn"
msgstr ""
@@ -14566,6 +14886,9 @@ msgstr ""
msgid "PackageType|NuGet"
msgstr ""
+msgid "PackageType|PyPi"
+msgstr ""
+
msgid "Packages"
msgstr ""
@@ -14659,6 +14982,9 @@ msgstr ""
msgid "Past due"
msgstr ""
+msgid "Paste a machine public key here. Read more about how to generate it"
+msgstr ""
+
msgid "Paste a machine public key here. Read more about how to generate it %{link_start}here%{link_end}"
msgstr ""
@@ -14674,9 +15000,6 @@ msgstr ""
msgid "Path"
msgstr ""
-msgid "Path, transfer, remove"
-msgstr ""
-
msgid "Path:"
msgstr ""
@@ -14707,7 +15030,7 @@ msgstr ""
msgid "Percentage"
msgstr ""
-msgid "Perform advanced options such as changing path, transferring, or removing the group."
+msgid "Perform advanced options such as changing path, transferring, exporting, or removing the group."
msgstr ""
msgid "Perform common operations on GitLab project"
@@ -14788,9 +15111,6 @@ msgstr ""
msgid "Pipeline minutes quota"
msgstr ""
-msgid "Pipeline quota"
-msgstr ""
-
msgid "Pipeline subscriptions"
msgstr ""
@@ -14800,6 +15120,9 @@ msgstr ""
msgid "Pipeline: %{status}"
msgstr ""
+msgid "PipelineCharts|CI / CD Analytics"
+msgstr ""
+
msgid "PipelineCharts|Failed:"
msgstr ""
@@ -14878,15 +15201,6 @@ msgstr ""
msgid "Pipelines settings for '%{project_name}' were successfully updated."
msgstr ""
-msgid "Pipelines| to purchase more minutes."
-msgstr ""
-
-msgid "Pipelines|%{namespace_name} has exceeded its pipeline minutes quota."
-msgstr ""
-
-msgid "Pipelines|%{namespace_name} has less than %{notification_level}%% of CI minutes available."
-msgstr ""
-
msgid "Pipelines|API"
msgstr ""
@@ -14908,10 +15222,13 @@ msgstr ""
msgid "Pipelines|Get started with Pipelines"
msgstr ""
-msgid "Pipelines|Loading Pipelines"
+msgid "Pipelines|Group %{namespace_name} has %{percentage}%% or less Shared Runner Pipeline minutes remaining. Once it runs out, no new jobs or pipelines in its projects will run."
msgstr ""
-msgid "Pipelines|Pipelines will not run anymore on shared Runners."
+msgid "Pipelines|Group %{namespace_name} has exceeded its pipeline minutes quota. Unless you buy additional pipeline minutes, no new jobs or pipelines in its projects will run."
+msgstr ""
+
+msgid "Pipelines|Loading Pipelines"
msgstr ""
msgid "Pipelines|Project cache successfully reset."
@@ -15142,6 +15459,9 @@ msgstr ""
msgid "Please select"
msgstr ""
+msgid "Please select a Jira project"
+msgstr ""
+
msgid "Please select a country"
msgstr ""
@@ -15187,6 +15507,9 @@ msgstr ""
msgid "Please wait while we import the repository for you. Refresh at will."
msgstr ""
+msgid "Plugins directory is deprecated and will be removed in 14.0. Please move this file into /file_hooks directory."
+msgstr ""
+
msgid "Pod does not exist"
msgstr ""
@@ -15304,7 +15627,7 @@ msgstr ""
msgid "Prevent users from changing their profile name"
msgstr ""
-msgid "Prevent users from modifing merge request approvers list"
+msgid "Prevent users from modifying merge request approvers list"
msgstr ""
msgid "Prevent users from performing write operations on GitLab while performing maintenance."
@@ -15325,10 +15648,10 @@ msgstr ""
msgid "Previous Artifacts"
msgstr ""
-msgid "Previous file in diff (MRs only)"
+msgid "Previous file in diff"
msgstr ""
-msgid "Previous unresolved discussion (MRs only)"
+msgid "Previous unresolved discussion"
msgstr ""
msgid "Primary"
@@ -15361,6 +15684,9 @@ msgstr ""
msgid "Private profile"
msgstr ""
+msgid "Private projects Minutes cost factor"
+msgstr ""
+
msgid "Private projects can be created in your personal namespace with:"
msgstr ""
@@ -16189,6 +16515,9 @@ msgstr ""
msgid "ProjectSettings|With GitLab Pages you can host your static websites on GitLab"
msgstr ""
+msgid "ProjectSettings|With Metrics Dashboard you can visualize this project performance metrics"
+msgstr ""
+
msgid "ProjectTemplates|.NET Core"
msgstr ""
@@ -16252,6 +16581,9 @@ msgstr ""
msgid "ProjectTemplates|Spring"
msgstr ""
+msgid "ProjectTemplates|Static Site Editor/Middleman"
+msgstr ""
+
msgid "ProjectTemplates|iOS (Swift)"
msgstr ""
@@ -16471,13 +16803,13 @@ msgstr ""
msgid "PrometheusService|Prometheus is being automatically managed on your clusters"
msgstr ""
-msgid "PrometheusService|These metrics will only be monitored after your first deployment to an environment"
+msgid "PrometheusService|Select the Active checkbox to override the Auto Configuration with custom settings. If unchecked, Auto Configuration settings are used."
msgstr ""
-msgid "PrometheusService|Time-series monitoring service"
+msgid "PrometheusService|These metrics will only be monitored after your first deployment to an environment"
msgstr ""
-msgid "PrometheusService|To enable manual configuration, uninstall Prometheus from your clusters"
+msgid "PrometheusService|Time-series monitoring service"
msgstr ""
msgid "PrometheusService|To enable the installation of Prometheus on your clusters, deactivate the manual configuration below"
@@ -16663,6 +16995,9 @@ msgstr ""
msgid "Protip:"
msgstr ""
+msgid "Protocol"
+msgstr ""
+
msgid "Provider"
msgstr ""
@@ -16687,6 +17022,9 @@ msgstr ""
msgid "Public pipelines"
msgstr ""
+msgid "Public projects Minutes cost factor"
+msgstr ""
+
msgid "Pull"
msgstr ""
@@ -16889,6 +17227,9 @@ msgstr[1] ""
msgstr[2] ""
msgstr[3] ""
+msgid "Regenerate export"
+msgstr ""
+
msgid "Regenerate instance ID"
msgstr ""
@@ -17268,9 +17609,31 @@ msgstr ""
msgid "Reports|%{combinedString} and %{resolvedString}"
msgstr ""
+msgid "Reports|Accessibility scanning detected %d issue for the source branch only"
+msgid_plural "Reports|Accessibility scanning detected %d issues for the source branch only"
+msgstr[0] ""
+msgstr[1] ""
+msgstr[2] ""
+msgstr[3] ""
+
+msgid "Reports|Accessibility scanning detected no issues for the source branch only"
+msgstr ""
+
+msgid "Reports|Accessibility scanning failed loading results"
+msgstr ""
+
+msgid "Reports|Accessibility scanning results are being parsed"
+msgstr ""
+
msgid "Reports|Actions"
msgstr ""
+msgid "Reports|An error occured while loading report"
+msgstr ""
+
+msgid "Reports|An error occurred while loading %{name} results"
+msgstr ""
+
msgid "Reports|Class"
msgstr ""
@@ -17376,7 +17739,7 @@ msgstr ""
msgid "Requested %{time_ago}"
msgstr ""
-msgid "Requested design version does not exist"
+msgid "Requested design version does not exist."
msgstr ""
msgid "Requested states are invalid"
@@ -17564,6 +17927,9 @@ msgstr ""
msgid "Resync"
msgstr ""
+msgid "Resync all"
+msgstr ""
+
msgid "Resync all %{replicableType}"
msgstr ""
@@ -17634,6 +18000,9 @@ msgstr ""
msgid "Revoked personal access token %{personal_access_token_name}!"
msgstr ""
+msgid "Revoked project access token %{project_access_token_name}!"
+msgstr ""
+
msgid "RightSidebar|adding a"
msgstr ""
@@ -17682,6 +18051,9 @@ msgstr ""
msgid "Runner token"
msgstr ""
+msgid "Runner tokens"
+msgstr ""
+
msgid "Runner was not updated."
msgstr ""
@@ -17736,6 +18108,9 @@ msgstr ""
msgid "SAML SSO for %{group_name}"
msgstr ""
+msgid "SAML discovery tokens"
+msgstr ""
+
msgid "SAML for %{group_name}"
msgstr ""
@@ -17820,12 +18195,18 @@ msgstr ""
msgid "Scheduled"
msgstr ""
+msgid "Scheduled to merge this merge request (%{strategy})."
+msgstr ""
+
msgid "Scheduled to merge this merge request when the pipeline succeeds."
msgstr ""
msgid "Schedules"
msgstr ""
+msgid "Schedules to merge this merge request (%{strategy})."
+msgstr ""
+
msgid "Scheduling"
msgstr ""
@@ -17883,6 +18264,9 @@ msgstr ""
msgid "Search branches and tags"
msgstr ""
+msgid "Search by author"
+msgstr ""
+
msgid "Search files"
msgstr ""
@@ -18044,13 +18428,6 @@ msgstr[1] ""
msgstr[2] ""
msgstr[3] ""
-msgid "SearchResults|snippet result"
-msgid_plural "SearchResults|snippet results"
-msgstr[0] ""
-msgstr[1] ""
-msgstr[2] ""
-msgstr[3] ""
-
msgid "SearchResults|user"
msgid_plural "SearchResults|users"
msgstr[0] ""
@@ -18065,6 +18442,9 @@ msgstr[1] ""
msgstr[2] ""
msgstr[3] ""
+msgid "Searching by both author and message is currently not supported."
+msgstr ""
+
msgid "Seat Link"
msgstr ""
@@ -18092,184 +18472,232 @@ msgstr ""
msgid "Security Dashboard"
msgstr ""
-msgid "Security Dashboard|Error fetching the vulnerability counts. Please check your network connection and try again."
+msgid "Security configuration help link"
msgstr ""
-msgid "Security Dashboard|Error fetching the vulnerability list. Please check your network connection and try again."
+msgid "Security dashboard"
msgstr ""
-msgid "Security Dashboard|Issue Created"
+msgid "Security report is out of date. Please update your branch with the latest changes from the target branch (%{targetBranchName})"
msgstr ""
-msgid "Security Reports|Comment added to '%{vulnerabilityName}'"
+msgid "Security report is out of date. Run %{newPipelineLinkStart}a new pipeline%{newPipelineLinkEnd} for the target branch (%{targetBranchName})"
msgstr ""
-msgid "Security Reports|Comment deleted on '%{vulnerabilityName}'"
+msgid "SecurityConfiguration|Configured"
msgstr ""
-msgid "Security Reports|Comment edited on '%{vulnerabilityName}'"
+msgid "SecurityConfiguration|Feature"
msgstr ""
-msgid "Security Reports|Create issue"
+msgid "SecurityConfiguration|Feature documentation for %{featureName}"
msgstr ""
-msgid "Security Reports|Dismiss vulnerability"
+msgid "SecurityConfiguration|Not yet configured"
msgstr ""
-msgid "Security Reports|Dismissed '%{vulnerabilityName}'"
+msgid "SecurityConfiguration|Secure features"
msgstr ""
-msgid "Security Reports|Dismissed '%{vulnerabilityName}'. Turn off the hide dismissed toggle to view."
+msgid "SecurityConfiguration|Status"
msgstr ""
-msgid "Security Reports|Either you don't have permission to view this dashboard or the dashboard has not been setup. Please check your permission settings with your administrator or check your dashboard configurations to proceed."
+msgid "SecurityReports|%{firstProject} and %{secondProject}"
msgstr ""
-msgid "Security Reports|Learn more about setting up your dashboard"
+msgid "SecurityReports|%{firstProject}, %{secondProject}, and %{rest}"
msgstr ""
-msgid "Security Reports|More info"
+msgid "SecurityReports|Add a project to your dashboard"
msgstr ""
-msgid "Security Reports|Oops, something doesn't seem right."
+msgid "SecurityReports|Add or remove projects from your dashboard"
msgstr ""
-msgid "Security Reports|Security reports can only be accessed by authorized users."
+msgid "SecurityReports|Add projects"
msgstr ""
-msgid "Security Reports|There was an error adding the comment."
+msgid "SecurityReports|Comment added to '%{vulnerabilityName}'"
msgstr ""
-msgid "Security Reports|There was an error creating the issue."
+msgid "SecurityReports|Comment deleted on '%{vulnerabilityName}'"
msgstr ""
-msgid "Security Reports|There was an error creating the merge request."
+msgid "SecurityReports|Comment edited on '%{vulnerabilityName}'"
msgstr ""
-msgid "Security Reports|There was an error deleting the comment."
+msgid "SecurityReports|Create issue"
msgstr ""
-msgid "Security Reports|There was an error dismissing the vulnerabilities."
+msgid "SecurityReports|Dismiss Selected"
msgstr ""
-msgid "Security Reports|There was an error dismissing the vulnerability."
+msgid "SecurityReports|Dismiss vulnerability"
msgstr ""
-msgid "Security Reports|There was an error reverting the dismissal."
+msgid "SecurityReports|Dismissed '%{vulnerabilityName}'"
msgstr ""
-msgid "Security Reports|There was an error reverting this dismissal."
+msgid "SecurityReports|Dismissed '%{vulnerabilityName}'. Turn off the hide dismissed toggle to view."
msgstr ""
-msgid "Security Reports|Undo dismiss"
+msgid "SecurityReports|Each vulnerability now has a unique page that can be directly linked to, shared, referenced, and tracked as the single source of truth. Vulnerability occurrences also persist across scanner runs, which improves tracking and visibility and reduces duplicates between scans."
msgstr ""
-msgid "Security Reports|You do not have sufficient permissions to access this report"
+msgid "SecurityReports|Edit dashboard"
msgstr ""
-msgid "Security Reports|You must sign in as an authorized user to see this report"
+msgid "SecurityReports|Either you don't have permission to view this dashboard or the dashboard has not been setup. Please check your permission settings with your administrator or check your dashboard configurations to proceed."
msgstr ""
-msgid "Security configuration help link"
+msgid "SecurityReports|Error fetching the vulnerability counts. Please check your network connection and try again."
msgstr ""
-msgid "Security dashboard"
+msgid "SecurityReports|Error fetching the vulnerability list. Please check your network connection and try again."
msgstr ""
-msgid "Security report is out of date. Please update your branch with the latest changes from the target branch (%{targetBranchName})"
+msgid "SecurityReports|False positive"
msgstr ""
-msgid "Security report is out of date. Run %{newPipelineLinkStart}a new pipeline%{newPipelineLinkEnd} for the target branch (%{targetBranchName})"
+msgid "SecurityReports|Hide dismissed"
msgstr ""
-msgid "SecurityConfiguration|Configured"
+msgid "SecurityReports|Introducing standalone vulnerabilities"
msgstr ""
-msgid "SecurityConfiguration|Feature"
+msgid "SecurityReports|Issue Created"
msgstr ""
-msgid "SecurityConfiguration|Feature documentation for %{featureName}"
+msgid "SecurityReports|Learn More"
msgstr ""
-msgid "SecurityConfiguration|Not yet configured"
+msgid "SecurityReports|Learn more about setting up your dashboard"
msgstr ""
-msgid "SecurityConfiguration|Secure features"
+msgid "SecurityReports|Load more vulnerabilities"
msgstr ""
-msgid "SecurityConfiguration|Status"
+msgid "SecurityReports|Monitor vulnerabilities in your code"
+msgstr ""
+
+msgid "SecurityReports|More info"
+msgstr ""
+
+msgid "SecurityReports|More information"
+msgstr ""
+
+msgid "SecurityReports|No vulnerabilities found for dashboard"
+msgstr ""
+
+msgid "SecurityReports|No vulnerabilities found for this group"
+msgstr ""
+
+msgid "SecurityReports|No vulnerabilities found for this pipeline"
+msgstr ""
+
+msgid "SecurityReports|No vulnerabilities found for this project"
+msgstr ""
+
+msgid "SecurityReports|Oops, something doesn't seem right."
+msgstr ""
+
+msgid "SecurityReports|Pipeline %{pipelineLink} triggered %{timeago} by %{user}"
msgstr ""
-msgid "SecurityDashboard|%{firstProject} and %{secondProject}"
+msgid "SecurityReports|Project"
msgstr ""
-msgid "SecurityDashboard|%{firstProject}, %{secondProject}, and %{rest}"
+msgid "SecurityReports|Projects added"
msgstr ""
-msgid "SecurityDashboard|Add a project to your dashboard"
+msgid "SecurityReports|Remove project from dashboard"
msgstr ""
-msgid "SecurityDashboard|Add or remove projects from your dashboard"
+msgid "SecurityReports|Report type"
msgstr ""
-msgid "SecurityDashboard|Add projects"
+msgid "SecurityReports|Return to dashboard"
msgstr ""
-msgid "SecurityDashboard|Edit dashboard"
+msgid "SecurityReports|Security Dashboard"
msgstr ""
-msgid "SecurityDashboard|Hide dismissed"
+msgid "SecurityReports|Security reports can only be accessed by authorized users."
msgstr ""
-msgid "SecurityDashboard|Monitor vulnerabilities in your code"
+msgid "SecurityReports|Select a project to add by using the project search field above."
msgstr ""
-msgid "SecurityDashboard|More information"
+msgid "SecurityReports|Select a reason"
msgstr ""
-msgid "SecurityDashboard|No vulnerabilities found for dashboard"
+msgid "SecurityReports|Severity"
msgstr ""
-msgid "SecurityDashboard|Pipeline %{pipelineLink} triggered %{timeago} by %{user}"
+msgid "SecurityReports|Status"
msgstr ""
-msgid "SecurityDashboard|Project"
+msgid "SecurityReports|The security dashboard displays the latest security findings for projects you wish to monitor. Select \"Edit dashboard\" to add and remove projects."
msgstr ""
-msgid "SecurityDashboard|Projects added"
+msgid "SecurityReports|The security dashboard displays the latest security report. Use it to find and fix vulnerabilities."
msgstr ""
-msgid "SecurityDashboard|Remove project from dashboard"
+msgid "SecurityReports|There was an error adding the comment."
msgstr ""
-msgid "SecurityDashboard|Report type"
+msgid "SecurityReports|There was an error creating the issue."
msgstr ""
-msgid "SecurityDashboard|Return to dashboard"
+msgid "SecurityReports|There was an error creating the merge request."
msgstr ""
-msgid "SecurityDashboard|Security Dashboard"
+msgid "SecurityReports|There was an error deleting the comment."
msgstr ""
-msgid "SecurityDashboard|Select a project to add by using the project search field above."
+msgid "SecurityReports|There was an error dismissing the vulnerabilities."
msgstr ""
-msgid "SecurityDashboard|Severity"
+msgid "SecurityReports|There was an error dismissing the vulnerability."
msgstr ""
-msgid "SecurityDashboard|Status"
+msgid "SecurityReports|There was an error reverting the dismissal."
msgstr ""
-msgid "SecurityDashboard|The security dashboard displays the latest security findings for projects you wish to monitor. Select \"Edit dashboard\" to add and remove projects."
+msgid "SecurityReports|There was an error reverting this dismissal."
msgstr ""
-msgid "SecurityDashboard|The security dashboard displays the latest security report. Use it to find and fix vulnerabilities."
+msgid "SecurityReports|There was an error while generating the report."
msgstr ""
-msgid "SecurityDashboard|There was an error while generating the report."
+msgid "SecurityReports|Unable to add %{invalidProjects}"
msgstr ""
-msgid "SecurityDashboard|Unable to add %{invalidProjects}"
+msgid "SecurityReports|Undo dismiss"
+msgstr ""
+
+msgid "SecurityReports|While it's rare to have no vulnerabilities for your group, it can happen. In any event, we ask that you double check your settings to make sure you've set up your dashboard correctly."
+msgstr ""
+
+msgid "SecurityReports|While it's rare to have no vulnerabilities for your pipeline, it can happen. In any event, we ask that you double check your settings to make sure all security scanning jobs have passed successfully."
+msgstr ""
+
+msgid "SecurityReports|While it's rare to have no vulnerabilities for your project, it can happen. In any event, we ask that you double check your settings to make sure you've set up your dashboard correctly."
+msgstr ""
+
+msgid "SecurityReports|While it's rare to have no vulnerabilities, it can happen. In any event, we ask that you please double check your settings to make sure you've set up your dashboard correctly."
+msgstr ""
+
+msgid "SecurityReports|Won't fix / Accept risk"
+msgstr ""
+
+msgid "SecurityReports|You do not have sufficient permissions to access this report"
+msgstr ""
+
+msgid "SecurityReports|You must sign in as an authorized user to see this report"
+msgstr ""
+
+msgid "SecurityReports|[No reason]"
msgstr ""
msgid "See GitLab's %{password_policy_guidelines}"
@@ -18281,6 +18709,9 @@ msgstr ""
msgid "See the affected projects in the GitLab admin panel"
msgstr ""
+msgid "See what's new at GitLab"
+msgstr ""
+
msgid "Select"
msgstr ""
@@ -18686,9 +19117,6 @@ msgstr ""
msgid "Set up Jira Integration"
msgstr ""
-msgid "Set up Jira Integration illustration"
-msgstr ""
-
msgid "Set up a %{type} Runner automatically"
msgstr ""
@@ -18773,6 +19201,9 @@ msgstr ""
msgid "Severity: %{severity}"
msgstr ""
+msgid "Shards (%{shards})"
+msgstr ""
+
msgid "Shards to synchronize"
msgstr ""
@@ -19089,6 +19520,9 @@ msgstr ""
msgid "Snowplow"
msgstr ""
+msgid "Some child epics may be hidden due to applied filters"
+msgstr ""
+
msgid "Some email servers do not support overriding the email sender name. Enable this option to include the name of the author of the issue, merge request or comment in the email body instead."
msgstr ""
@@ -19230,6 +19664,9 @@ msgstr ""
msgid "Something went wrong while updating your list settings"
msgstr ""
+msgid "Something went wrong with your automatic subscription renewal"
+msgstr ""
+
msgid "Something went wrong, unable to add %{project} to dashboard"
msgstr ""
@@ -19515,6 +19952,9 @@ msgstr ""
msgid "Stage removed"
msgstr ""
+msgid "Standard"
+msgstr ""
+
msgid "Star a label to make it a priority label. Order the prioritized labels to change their relative priority, by dragging."
msgstr ""
@@ -19638,6 +20078,9 @@ msgstr ""
msgid "Static Application Security Testing (SAST)"
msgstr ""
+msgid "StaticSiteEditor|An error occurred while submitting your changes."
+msgstr ""
+
msgid "StaticSiteEditor|Branch could not be created."
msgstr ""
@@ -19647,18 +20090,30 @@ msgstr ""
msgid "StaticSiteEditor|Could not create merge request."
msgstr ""
+msgid "StaticSiteEditor|Incompatible file content"
+msgstr ""
+
msgid "StaticSiteEditor|Return to site"
msgstr ""
+msgid "StaticSiteEditor|Static site editor"
+msgstr ""
+
msgid "StaticSiteEditor|Success!"
msgstr ""
msgid "StaticSiteEditor|Summary of changes"
msgstr ""
+msgid "StaticSiteEditor|The Static Site Editor is currently configured to only edit Markdown content on pages generated from Middleman. Visit the documentation to learn more about configuring your site to use the Static Site Editor."
+msgstr ""
+
msgid "StaticSiteEditor|Update %{sourcePath} file"
msgstr ""
+msgid "StaticSiteEditor|View documentation"
+msgstr ""
+
msgid "StaticSiteEditor|View merge request"
msgstr ""
@@ -19716,6 +20171,12 @@ msgstr ""
msgid "StatusPage|Status page"
msgstr ""
+msgid "StatusPage|Status page URL"
+msgstr ""
+
+msgid "StatusPage|Status page frontend documentation"
+msgstr ""
+
msgid "StatusPage|To publish incidents to an external status page, GitLab will store a JSON file in your Amazon S3 account in a location accessible to your external status page service. Make sure to also set up %{docsLink}"
msgstr ""
@@ -20064,6 +20525,12 @@ msgstr ""
msgid "Synced"
msgstr ""
+msgid "Synchronization disabled"
+msgstr ""
+
+msgid "Synchronization of container repositories is disabled."
+msgstr ""
+
msgid "System"
msgstr ""
@@ -20217,6 +20684,9 @@ msgstr ""
msgid "Telephone number"
msgstr ""
+msgid "Telephone number (Optional)"
+msgstr ""
+
msgid "Template"
msgstr ""
@@ -20529,6 +20999,9 @@ msgstr ""
msgid "The license was successfully uploaded and is now active. You can see the details below."
msgstr ""
+msgid "The license was successfully uploaded and will be active from %{starts_at}. You can see the details below."
+msgstr ""
+
msgid "The maximum file size allowed is %{size}."
msgstr ""
@@ -20850,6 +21323,9 @@ msgstr ""
msgid "There was an error getting the epic participants."
msgstr ""
+msgid "There was an error importing the Jira project."
+msgstr ""
+
msgid "There was an error loading users activity calendar."
msgstr ""
@@ -20865,7 +21341,7 @@ msgstr ""
msgid "There was an error resetting user pipeline minutes."
msgstr ""
-msgid "There was an error saving this Geo Node"
+msgid "There was an error saving this Geo Node."
msgstr ""
msgid "There was an error saving your changes."
@@ -20928,6 +21404,9 @@ msgstr ""
msgid "They can be managed using the %{link}."
msgstr ""
+msgid "Third Party Advisory Link"
+msgstr ""
+
msgid "Third party offers"
msgstr ""
@@ -20964,6 +21443,9 @@ msgstr ""
msgid "This application will be able to:"
msgstr ""
+msgid "This attachment has been truncated to avoid exceeding the maximum allowed attachment size of 15MB. %{written_count} of %{issues_count} issues have been included. Consider re-exporting with a narrower selection of issues."
+msgstr ""
+
msgid "This block is self-referential"
msgstr ""
@@ -21105,6 +21587,12 @@ msgstr ""
msgid "This issue is confidential"
msgstr ""
+msgid "This issue is currently blocked by the following issues: %{issues}."
+msgstr ""
+
+msgid "This issue is in a child epic of the filtered epic"
+msgstr ""
+
msgid "This issue is locked."
msgstr ""
@@ -21303,7 +21791,10 @@ msgstr ""
msgid "This user will be the author of all events in the activity feed that are the result of an update, like new branches being created or new commits being pushed to existing branches. Upon creation or when reassigning you can only assign yourself to be the mirror user."
msgstr ""
-msgid "This variable can not be masked"
+msgid "This variable can not be masked."
+msgstr ""
+
+msgid "This variable does not match the expected pattern."
msgstr ""
msgid "This will help us personalize your onboarding experience."
@@ -21333,10 +21824,10 @@ msgstr ""
msgid "ThreatMonitoring|Application firewall not detected"
msgstr ""
-msgid "ThreatMonitoring|Container Network Policy"
+msgid "ThreatMonitoring|Container Network Policies are not installed or have been disabled. To view this data, ensure your Network Policies are installed and enabled for your cluster."
msgstr ""
-msgid "ThreatMonitoring|Container NetworkPolicies are not installed or has been disabled. To view this data, ensure you NetworkPolicies are installed and enabled for your cluster."
+msgid "ThreatMonitoring|Container Network Policy"
msgstr ""
msgid "ThreatMonitoring|Container NetworkPolicies not detected"
@@ -21369,7 +21860,7 @@ msgstr ""
msgid "ThreatMonitoring|Something went wrong, unable to fetch statistics"
msgstr ""
-msgid "ThreatMonitoring|The firewall is not installed or has been disabled. To view this data, ensure you firewall is installed and enabled for your cluster."
+msgid "ThreatMonitoring|The firewall is not installed or has been disabled. To view this data, ensure the web application firewall is installed and enabled for your cluster."
msgstr ""
msgid "ThreatMonitoring|The graph below is an overview of traffic coming to your application as tracked by the Web Application Firewall (WAF). View the docs for instructions on how to access the WAF logs to see what type of malicious traffic is trying to access your app. The docs link is also accessible by clicking the \"?\" icon next to the title below."
@@ -21438,6 +21929,9 @@ msgstr ""
msgid "Time in seconds GitLab will wait for a response from the external service. When the service does not respond in time, access will be denied."
msgstr ""
+msgid "Time of import: %{importTime}"
+msgstr ""
+
msgid "Time remaining"
msgstr ""
@@ -21626,7 +22120,7 @@ msgstr ""
msgid "Title:"
msgstr ""
-msgid "Titles and Filenames"
+msgid "Titles and Descriptions"
msgstr ""
msgid "To"
@@ -21833,7 +22327,7 @@ msgstr ""
msgid "Too many projects enabled. You will need to manage them via the console or the API."
msgstr ""
-msgid "Topics"
+msgid "Topics (optional)"
msgstr ""
msgid "Total"
@@ -21995,6 +22489,9 @@ msgstr ""
msgid "Try all GitLab has to offer for 30 days."
msgstr ""
+msgid "Try changing or removing filters."
+msgstr ""
+
msgid "Try to fork again"
msgstr ""
@@ -22088,6 +22585,9 @@ msgstr ""
msgid "Unable to connect to server: %{error}"
msgstr ""
+msgid "Unable to connect to the Jira instance. Please check your Jira integration configuration."
+msgstr ""
+
msgid "Unable to convert Kubernetes logs encoding to UTF-8"
msgstr ""
@@ -23035,6 +23535,9 @@ msgstr ""
msgid "View issue"
msgstr ""
+msgid "View issues"
+msgstr ""
+
msgid "View it on GitLab"
msgstr ""
@@ -23212,6 +23715,15 @@ msgstr ""
msgid "VulnerabilityManagement|Resolved %{timeago} by %{user}"
msgstr ""
+msgid "VulnerabilityManagement|Something went wrong while trying to delete the comment. Please try again later."
+msgstr ""
+
+msgid "VulnerabilityManagement|Something went wrong while trying to retrieve the vulnerability history. Please try again later."
+msgstr ""
+
+msgid "VulnerabilityManagement|Something went wrong while trying to save the comment. Please try again later."
+msgstr ""
+
msgid "VulnerabilityManagement|Something went wrong, could not create an issue."
msgstr ""
@@ -23242,6 +23754,9 @@ msgstr ""
msgid "VulnerabilityStatusTypes|Resolved"
msgstr ""
+msgid "Vulnerability|%{scannerName} (version %{scannerVersion})"
+msgstr ""
+
msgid "Vulnerability|Class"
msgstr ""
@@ -23275,7 +23790,10 @@ msgstr ""
msgid "Vulnerability|Project"
msgstr ""
-msgid "Vulnerability|Report Type"
+msgid "Vulnerability|Scanner Provider"
+msgstr ""
+
+msgid "Vulnerability|Scanner Type"
msgstr ""
msgid "Vulnerability|Severity"
@@ -23329,9 +23847,15 @@ msgstr ""
msgid "We sent you an email with reset password instructions"
msgstr ""
+msgid "We tried to automatically renew your %{strong}%{plan_name}%{strong_close} subscription for %{strong}%{namespace_name}%{strong_close} on %{expires_on} but something went wrong so your subscription was downgraded to the free plan. Don't worry, your data is safe. We suggest you check your payment method and get in touch with our support team (%{support_link}). They'll gladly help with your subscription renewal."
+msgstr ""
+
msgid "We want to be sure it is you, please confirm you are not a robot."
msgstr ""
+msgid "We will automatically renew your %{strong}%{plan_name}%{strong_close} subscription for %{strong}%{namespace_name}%{strong_close} on %{strong}%{expires_on}%{strong_close}. There's nothing that you need to do, we'll let you know when the renewal is complete. Need more seats, a higher plan or just want to review your payment method?"
+msgstr ""
+
msgid "We've found no vulnerabilities"
msgstr ""
@@ -23438,15 +23962,6 @@ msgstr ""
msgid "When:"
msgstr ""
-msgid "While it's rare to have no vulnerabilities for your group, it can happen. In any event, we ask that you double check your settings to make sure you've set up your dashboard correctly."
-msgstr ""
-
-msgid "While it's rare to have no vulnerabilities for your pipeline, it can happen. In any event, we ask that you double check your settings to make sure all security scanning jobs have passed successfully."
-msgstr ""
-
-msgid "While it's rare to have no vulnerabilities for your project, it can happen. In any event, we ask that you double check your settings to make sure you've set up your dashboard correctly."
-msgstr ""
-
msgid "While it's rare to have no vulnerabilities, it can happen. In any event, we ask that you please double check your settings to make sure you've set up your dashboard correctly."
msgstr ""
@@ -23477,9 +23992,6 @@ msgstr ""
msgid "Wiki"
msgstr ""
-msgid "Wiki pages"
-msgstr ""
-
msgid "Wiki was successfully updated."
msgstr ""
@@ -23663,6 +24175,9 @@ msgstr ""
msgid "Yes, add it"
msgstr ""
+msgid "Yes, close issue"
+msgstr ""
+
msgid "Yes, let me map Google Code users to full names or GitLab users."
msgstr ""
@@ -23858,6 +24373,9 @@ msgstr ""
msgid "You could not create a new trigger."
msgstr ""
+msgid "You didn't renew your %{strong}%{plan_name}%{strong_close} subscription for %{strong}%{namespace_name}%{strong_close} so it was downgraded to the free plan."
+msgstr ""
+
msgid "You didn't renew your %{strong}%{plan_name}%{strong_close} subscription so it was downgraded to the GitLab Core Plan."
msgstr ""
@@ -23924,6 +24442,9 @@ msgstr ""
msgid "You have declined the invitation to join %{label}."
msgstr ""
+msgid "You have imported from this project %{numberOfPreviousImportsForProject} times before. Each new import will create duplicate issues."
+msgstr ""
+
msgid "You have no permissions"
msgstr ""
@@ -24005,6 +24526,9 @@ msgstr ""
msgid "You need to upload a Google Takeout archive."
msgstr ""
+msgid "You reached %{usage_in_percent} of %{namespace_name}'s capacity (%{used_storage} of %{storage_limit})"
+msgstr ""
+
msgid "You tried to fork %{link_to_the_project} but it failed for the following reason:"
msgstr ""
@@ -24089,9 +24613,21 @@ msgstr ""
msgid "YouTube"
msgstr ""
+msgid "Your %{strong}%{plan_name}%{strong_close} subscription for %{strong}%{namespace_name}%{strong_close} will expire on %{strong}%{expires_on}%{strong_close}. After that, you will not to be able to create issues or merge requests as well as many other features."
+msgstr ""
+
msgid "Your %{strong}%{plan_name}%{strong_close} subscription will expire on %{strong}%{expires_on}%{strong_close}. After that, you will not to be able to create issues or merge requests as well as many other features."
msgstr ""
+msgid "Your CSV export has started. It will be emailed to %{email} when complete."
+msgstr ""
+
+msgid "Your CSV export of %{issues_count} from project %{project_link} has been added to this email as an attachment."
+msgstr ""
+
+msgid "Your CSV export of %{written_count} from project %{project_name} (%{project_url}) has been added to this email as an attachment."
+msgstr ""
+
msgid "Your Commit Email will be used for web based operations, such as edits and merges."
msgstr ""
@@ -24188,6 +24724,9 @@ msgstr ""
msgid "Your comment could not be updated! Please check your network connection and try again."
msgstr ""
+msgid "Your comment will be discarded."
+msgstr ""
+
msgid "Your custom stage '%{title}' was created"
msgstr ""
@@ -24227,6 +24766,9 @@ msgstr ""
msgid "Your new personal access token has been created."
msgstr ""
+msgid "Your new project access token has been created."
+msgstr ""
+
msgid "Your password isn't required to view this page. If a password or any other personal details are requested, please contact your administrator to report abuse."
msgstr ""
@@ -24245,12 +24787,18 @@ msgstr ""
msgid "Your request for access has been queued for review."
msgstr ""
+msgid "Your search didn't match any commits."
+msgstr ""
+
msgid "Your subscription expired!"
msgstr ""
msgid "Your subscription has been downgraded"
msgstr ""
+msgid "Your subscription will automatically renew in %{remaining_days}"
+msgstr ""
+
msgid "Your subscription will expire in %{remaining_days}"
msgstr ""
@@ -24291,7 +24839,7 @@ msgstr ""
msgid "allowed to fail"
msgstr ""
-msgid "already being used for another group or project milestone."
+msgid "already being used for another group or project %{timebox_name}."
msgstr ""
msgid "already has a \"created\" issue link"
@@ -24369,6 +24917,9 @@ msgstr ""
msgid "ciReport|%{linkStartTag}Learn more about SAST %{linkEndTag}"
msgstr ""
+msgid "ciReport|%{linkStartTag}Learn more about Secret Scanning %{linkEndTag}"
+msgstr ""
+
msgid "ciReport|%{linkStartTag}Learn more about codequality reports %{linkEndTag}"
msgstr ""
@@ -24542,6 +25093,12 @@ msgstr ""
msgid "ciReport|SAST"
msgstr ""
+msgid "ciReport|Secret scanning"
+msgstr ""
+
+msgid "ciReport|Secret scanning detects secrets and credentials vulnerabilities in your source code."
+msgstr ""
+
msgid "ciReport|Security scanning"
msgstr ""
@@ -24747,6 +25304,9 @@ msgstr ""
msgid "group"
msgstr ""
+msgid "groups"
+msgstr ""
+
msgid "has already been linked to another vulnerability"
msgstr ""
@@ -24914,9 +25474,6 @@ msgstr[3] ""
msgid "merged %{time_ago}"
msgstr ""
-msgid "milestone should belong either to a project or a group."
-msgstr ""
-
msgid "missing"
msgstr ""
@@ -25253,6 +25810,9 @@ msgstr ""
msgid "no contributions"
msgstr ""
+msgid "no expiration"
+msgstr ""
+
msgid "no one can merge"
msgstr ""
@@ -25309,6 +25869,9 @@ msgstr ""
msgid "pending removal"
msgstr ""
+msgid "per day"
+msgstr ""
+
msgid "pipeline"
msgstr ""
@@ -25341,6 +25904,12 @@ msgstr[3] ""
msgid "project avatar"
msgstr ""
+msgid "projects"
+msgstr ""
+
+msgid "push to your repository, create pipelines, create issues or add comments. To reduce storage capacity, delete unused repositories, artifacts, wikis, issues, and pipelines."
+msgstr ""
+
msgid "quick actions"
msgstr ""
@@ -25489,13 +26058,6 @@ msgstr ""
msgid "this document"
msgstr ""
-msgid "thread resolved"
-msgid_plural "threads resolved"
-msgstr[0] ""
-msgstr[1] ""
-msgstr[2] ""
-msgstr[3] ""
-
msgid "to help your contributors communicate effectively!"
msgstr ""
@@ -25550,6 +26112,9 @@ msgstr ""
msgid "view the blob"
msgstr ""
+msgid "vulnerability|Add a comment"
+msgstr ""
+
msgid "vulnerability|Add a comment or reason for dismissal"
msgstr ""
diff --git a/locale/hi_IN/gitlab.po b/locale/hi_IN/gitlab.po
index 0e2e1f8d644..220339b757d 100644
--- a/locale/hi_IN/gitlab.po
+++ b/locale/hi_IN/gitlab.po
@@ -12,7 +12,7 @@ msgstr ""
"X-Crowdin-Project: gitlab-ee\n"
"X-Crowdin-Language: hi\n"
"X-Crowdin-File: /master/locale/gitlab.pot\n"
-"PO-Revision-Date: 2020-04-15 00:32\n"
+"PO-Revision-Date: 2020-05-05 21:32\n"
msgid " %{start} to %{end}"
msgstr ""
@@ -212,6 +212,11 @@ msgid_plural "%d tags"
msgstr[0] ""
msgstr[1] ""
+msgid "%d unresolved thread"
+msgid_plural "%d unresolved threads"
+msgstr[0] ""
+msgstr[1] ""
+
msgid "%d vulnerability dismissed"
msgid_plural "%d vulnerabilities dismissed"
msgstr[0] ""
@@ -371,6 +376,9 @@ msgstr ""
msgid "%{mrText}, this issue will be closed automatically."
msgstr ""
+msgid "%{namespace_name} is now read-only. You cannot: %{base_message}"
+msgstr ""
+
msgid "%{name} contained %{resultsString}"
msgstr ""
@@ -383,6 +391,11 @@ msgstr ""
msgid "%{name}'s avatar"
msgstr ""
+msgid "%{no_of_days} day"
+msgid_plural "%{no_of_days} days"
+msgstr[0] ""
+msgstr[1] ""
+
msgid "%{number_commits_behind} commits behind %{default_branch}, %{number_commits_ahead} commits ahead"
msgstr ""
@@ -506,6 +519,9 @@ msgstr[1] ""
msgid "%{text} is available"
msgstr ""
+msgid "%{timebox_name} should belong either to a project or a group."
+msgstr ""
+
msgid "%{title} %{operator} %{threshold}"
msgstr ""
@@ -781,12 +797,18 @@ msgstr ""
msgid "<code>\"johnsmith@example.com\": \"johnsmith@example.com\"</code> will add \"By <a href=\"#\">johnsmith@example.com</a>\" to all issues and comments originally created by johnsmith@example.com. By default, the email address or username is masked to ensure the user's privacy. Use this option if you want to show the full email address."
msgstr ""
+msgid "<namespace / project>"
+msgstr ""
+
msgid "<no name set>"
msgstr ""
msgid "<no scopes selected>"
msgstr ""
+msgid "<project name>"
+msgstr ""
+
msgid "<strong>%{group_name}</strong> group members"
msgstr ""
@@ -820,6 +842,9 @@ msgstr ""
msgid "A basic page and serverless function that uses AWS Lambda, AWS API Gateway, and GitLab Pages"
msgstr ""
+msgid "A complete DevOps platform"
+msgstr ""
+
msgid "A default branch cannot be chosen for an empty project."
msgstr ""
@@ -1036,6 +1061,12 @@ msgstr ""
msgid "AccessTokens|reset it"
msgstr ""
+msgid "AccessibilityReport|Accessibility report artifact not found"
+msgstr ""
+
+msgid "AccessibilityReport|Failed to retrieve accessibility report"
+msgstr ""
+
msgid "AccessibilityReport|Learn More"
msgstr ""
@@ -1045,6 +1076,9 @@ msgstr ""
msgid "AccessibilityReport|New"
msgstr ""
+msgid "AccessibilityReport|The accessibility scanning found an error of the following type: %{code}"
+msgstr ""
+
msgid "Account"
msgstr ""
@@ -1287,6 +1321,9 @@ msgstr ""
msgid "Added at"
msgstr ""
+msgid "Added for this merge request"
+msgstr ""
+
msgid "Added in this version"
msgstr ""
@@ -1658,6 +1695,72 @@ msgid_plural "Alerts"
msgstr[0] ""
msgstr[1] ""
+msgid "Alert Details"
+msgstr ""
+
+msgid "AlertManagement|Acknowledged"
+msgstr ""
+
+msgid "AlertManagement|Alert"
+msgstr ""
+
+msgid "AlertManagement|Authorize external service"
+msgstr ""
+
+msgid "AlertManagement|Display alerts from all your monitoring tools directly within GitLab. Streamline the investigation of your alerts and the escalation of alerts to incidents."
+msgstr ""
+
+msgid "AlertManagement|End time"
+msgstr ""
+
+msgid "AlertManagement|End time:"
+msgstr ""
+
+msgid "AlertManagement|Events"
+msgstr ""
+
+msgid "AlertManagement|Events:"
+msgstr ""
+
+msgid "AlertManagement|Full Alert Details"
+msgstr ""
+
+msgid "AlertManagement|More information"
+msgstr ""
+
+msgid "AlertManagement|No alerts available to display. If you think you're seeing this message in error, refresh the page."
+msgstr ""
+
+msgid "AlertManagement|No alerts to display."
+msgstr ""
+
+msgid "AlertManagement|Overview"
+msgstr ""
+
+msgid "AlertManagement|Resolved"
+msgstr ""
+
+msgid "AlertManagement|Severity"
+msgstr ""
+
+msgid "AlertManagement|Start time"
+msgstr ""
+
+msgid "AlertManagement|Start time:"
+msgstr ""
+
+msgid "AlertManagement|Status"
+msgstr ""
+
+msgid "AlertManagement|Surface alerts in GitLab"
+msgstr ""
+
+msgid "AlertManagement|There was an error displaying the alerts. Confirm your endpoint's configuration details to ensure alerts appear."
+msgstr ""
+
+msgid "AlertManagement|Triggered"
+msgstr ""
+
msgid "AlertService|%{linkStart}Learn more%{linkEnd} about configuring this endpoint to receive alerts."
msgstr ""
@@ -1727,6 +1830,9 @@ msgstr ""
msgid "All security scans are enabled because %{linkStart}Auto DevOps%{linkEnd} is enabled on this project"
msgstr ""
+msgid "All threads resolved"
+msgstr ""
+
msgid "All users"
msgstr ""
@@ -1745,6 +1851,9 @@ msgstr ""
msgid "Allow only the selected protocols to be used for Git access."
msgstr ""
+msgid "Allow owners to manage default branch protection per group"
+msgstr ""
+
msgid "Allow owners to manually add users outside of LDAP"
msgstr ""
@@ -1853,6 +1962,9 @@ msgstr ""
msgid "An error occurred fetching the dropdown data."
msgstr ""
+msgid "An error occurred fetching the project authors."
+msgstr ""
+
msgid "An error occurred previewing the blob"
msgstr ""
@@ -1934,6 +2046,9 @@ msgstr ""
msgid "An error occurred while fetching sidebar data"
msgstr ""
+msgid "An error occurred while fetching terraform reports."
+msgstr ""
+
msgid "An error occurred while fetching the Service Desk address."
msgstr ""
@@ -2171,6 +2286,9 @@ msgstr ""
msgid "Any"
msgstr ""
+msgid "Any Author"
+msgstr ""
+
msgid "Any Label"
msgstr ""
@@ -2386,10 +2504,10 @@ msgstr ""
msgid "Are you sure that you want to unarchive this project?"
msgstr ""
-msgid "Are you sure you want to cancel creating this comment?"
+msgid "Are you sure you want to cancel editing this comment?"
msgstr ""
-msgid "Are you sure you want to cancel editing this comment?"
+msgid "Are you sure you want to close this blocked issue?"
msgstr ""
msgid "Are you sure you want to delete %{name}?"
@@ -2419,6 +2537,9 @@ msgstr ""
msgid "Are you sure you want to deploy this environment?"
msgstr ""
+msgid "Are you sure you want to discard this comment?"
+msgstr ""
+
msgid "Are you sure you want to erase this build?"
msgstr ""
@@ -2595,6 +2716,9 @@ msgstr ""
msgid "At least one approval from a code owner is required to change files matching the respective CODEOWNER rules."
msgstr ""
+msgid "At least one logging option is required to be enabled"
+msgstr ""
+
msgid "At least one of group_id or project_id must be specified"
msgstr ""
@@ -2633,6 +2757,27 @@ msgstr ""
msgid "AuditEvents|Target"
msgstr ""
+msgid "AuditLogs|(removed)"
+msgstr ""
+
+msgid "AuditLogs|Action"
+msgstr ""
+
+msgid "AuditLogs|Author"
+msgstr ""
+
+msgid "AuditLogs|Date"
+msgstr ""
+
+msgid "AuditLogs|IP Address"
+msgstr ""
+
+msgid "AuditLogs|Object"
+msgstr ""
+
+msgid "AuditLogs|Target"
+msgstr ""
+
msgid "Aug"
msgstr ""
@@ -2915,9 +3060,6 @@ msgstr ""
msgid "BambooService|You must set up automatic revision labeling and a repository trigger in Bamboo."
msgstr ""
-msgid "Batch operations"
-msgstr ""
-
msgid "BatchComments|Delete all pending comments"
msgstr ""
@@ -3047,6 +3189,9 @@ msgstr ""
msgid "Boards"
msgstr ""
+msgid "Boards and Board Lists"
+msgstr ""
+
msgid "Boards|Collapse"
msgstr ""
@@ -3266,6 +3411,9 @@ msgstr ""
msgid "Buy GitLab Enterprise Edition"
msgstr ""
+msgid "Buy more Pipeline minutes"
+msgstr ""
+
msgid "By %{user_name}"
msgstr ""
@@ -3386,6 +3534,9 @@ msgstr ""
msgid "Can't scan the code?"
msgstr ""
+msgid "Can't update snippet: %{err}"
+msgstr ""
+
msgid "Canary"
msgstr ""
@@ -3554,6 +3705,9 @@ msgstr ""
msgid "Changing group path can have unintended side effects."
msgstr ""
+msgid "Charts"
+msgstr ""
+
msgid "Charts can't be displayed as the request for data has timed out. %{documentationLink}"
msgstr ""
@@ -3590,9 +3744,6 @@ msgstr ""
msgid "ChatMessage|and [%{count} more](%{pipeline_failed_jobs_url})"
msgstr ""
-msgid "ChatMessage|failed"
-msgstr ""
-
msgid "ChatMessage|has failed"
msgstr ""
@@ -3608,9 +3759,6 @@ msgstr ""
msgid "ChatMessage|in %{project_link}"
msgstr ""
-msgid "ChatMessage|passed"
-msgstr ""
-
msgid "Check again"
msgstr ""
@@ -4061,9 +4209,6 @@ msgstr ""
msgid "Click any <strong>project name</strong> in the project list below to navigate to the project milestone."
msgstr ""
-msgid "Click here"
-msgstr ""
-
msgid "Click the <strong>Download</strong> button and wait for downloading to complete."
msgstr ""
@@ -4451,6 +4596,12 @@ msgstr ""
msgid "ClusterIntegration|Fetching zones"
msgstr ""
+msgid "ClusterIntegration|Fluentd"
+msgstr ""
+
+msgid "ClusterIntegration|Fluentd is an open source data collector, which lets you unify the data collection and consumption for a better use and understanding of data. It requires at least one of the following logs to be successfully installed."
+msgstr ""
+
msgid "ClusterIntegration|GitLab Integration"
msgstr ""
@@ -4757,6 +4908,15 @@ msgstr ""
msgid "ClusterIntegration|Request to begin uninstalling failed"
msgstr ""
+msgid "ClusterIntegration|SIEM Hostname"
+msgstr ""
+
+msgid "ClusterIntegration|SIEM Port"
+msgstr ""
+
+msgid "ClusterIntegration|SIEM Protocol"
+msgstr ""
+
msgid "ClusterIntegration|Save changes"
msgstr ""
@@ -4853,6 +5013,12 @@ msgstr ""
msgid "ClusterIntegration|Select zone to choose machine type"
msgstr ""
+msgid "ClusterIntegration|Send Cilium Logs"
+msgstr ""
+
+msgid "ClusterIntegration|Send ModSecurity Logs"
+msgstr ""
+
msgid "ClusterIntegration|Service Token"
msgstr ""
@@ -5281,18 +5447,33 @@ msgstr ""
msgid "Compliance framework (optional)"
msgstr ""
+msgid "ComplianceFramework|GDPR"
+msgstr ""
+
msgid "ComplianceFramework|GDPR - General Data Protection Regulation"
msgstr ""
+msgid "ComplianceFramework|HIPAA"
+msgstr ""
+
msgid "ComplianceFramework|HIPAA - Health Insurance Portability and Accountability Act"
msgstr ""
+msgid "ComplianceFramework|PCI-DSS"
+msgstr ""
+
msgid "ComplianceFramework|PCI-DSS - Payment Card Industry-Data Security Standard"
msgstr ""
+msgid "ComplianceFramework|SOC 2"
+msgstr ""
+
msgid "ComplianceFramework|SOC 2 - Service Organization Control 2"
msgstr ""
+msgid "ComplianceFramework|SOX"
+msgstr ""
+
msgid "ComplianceFramework|SOX - Sarbanes-Oxley"
msgstr ""
@@ -5440,9 +5621,15 @@ msgstr ""
msgid "Container repositories sync capacity"
msgstr ""
+msgid "ContainerRegistry| Please visit the %{linkStart}administration settings%{linkEnd} to enable this feature."
+msgstr ""
+
msgid "ContainerRegistry|%{imageName} tags"
msgstr ""
+msgid "ContainerRegistry|%{title} was successfully scheduled for deletion"
+msgstr ""
+
msgid "ContainerRegistry|Automatically remove extra images that aren't designed to be kept."
msgstr ""
@@ -5455,6 +5642,9 @@ msgstr ""
msgid "ContainerRegistry|Container Registry"
msgstr ""
+msgid "ContainerRegistry|Container Registry tag expiration and retention policy is disabled"
+msgstr ""
+
msgid "ContainerRegistry|Copy build command"
msgstr ""
@@ -5464,18 +5654,12 @@ msgstr ""
msgid "ContainerRegistry|Copy push command"
msgstr ""
-msgid "ContainerRegistry|Currently, the Container Registry tag expiration feature is not available for projects created before GitLab version 12.8. For updates and more information, visit Issue %{linkStart}#196124%{linkEnd}"
-msgstr ""
-
msgid "ContainerRegistry|Docker connection error"
msgstr ""
msgid "ContainerRegistry|Docker tag expiration policy is %{toggleStatus}"
msgstr ""
-msgid "ContainerRegistry|Docker tags with names matching this regex pattern will expire:"
-msgstr ""
-
msgid "ContainerRegistry|Edit Settings"
msgstr ""
@@ -5497,9 +5681,6 @@ msgstr ""
msgid "ContainerRegistry|Image ID"
msgstr ""
-msgid "ContainerRegistry|Image deleted successfully"
-msgstr ""
-
msgid "ContainerRegistry|Keep and protect the images that matter most."
msgstr ""
@@ -5515,12 +5696,24 @@ msgstr ""
msgid "ContainerRegistry|Number of tags to retain:"
msgstr ""
+msgid "ContainerRegistry|Please contact your administrator."
+msgstr ""
+
msgid "ContainerRegistry|Push an image"
msgstr ""
msgid "ContainerRegistry|Quick Start"
msgstr ""
+msgid "ContainerRegistry|Regular expressions such as %{codeStart}.*-test%{codeEnd} or %{codeStart}dev-.*%{codeEnd} are supported"
+msgstr ""
+
+msgid "ContainerRegistry|Regular expressions such as %{codeStart}.*-test%{codeEnd} or %{codeStart}dev-.*%{codeEnd} are supported. To select all tags, use %{codeStart}.*%{codeEnd}"
+msgstr ""
+
+msgid "ContainerRegistry|Remember to run %{docLinkStart}garbage collection%{docLinkEnd} to remove the stale data from storage."
+msgstr ""
+
msgid "ContainerRegistry|Remove repository"
msgstr ""
@@ -5535,22 +5728,22 @@ msgstr[1] ""
msgid "ContainerRegistry|Retention policy has been Enabled"
msgstr ""
-msgid "ContainerRegistry|Something went wrong while deleting the image."
+msgid "ContainerRegistry|Something went wrong while fetching the expiration policy."
msgstr ""
-msgid "ContainerRegistry|Something went wrong while deleting the tag."
+msgid "ContainerRegistry|Something went wrong while fetching the repository list."
msgstr ""
-msgid "ContainerRegistry|Something went wrong while deleting the tags."
+msgid "ContainerRegistry|Something went wrong while fetching the tags list."
msgstr ""
-msgid "ContainerRegistry|Something went wrong while fetching the expiration policy."
+msgid "ContainerRegistry|Something went wrong while marking the tag for deletion."
msgstr ""
-msgid "ContainerRegistry|Something went wrong while fetching the packages list."
+msgid "ContainerRegistry|Something went wrong while marking the tags for deletion."
msgstr ""
-msgid "ContainerRegistry|Something went wrong while fetching the tags list."
+msgid "ContainerRegistry|Something went wrong while scheduling %{title} for deletion. Please try again."
msgstr ""
msgid "ContainerRegistry|Something went wrong while updating the expiration policy."
@@ -5559,16 +5752,25 @@ msgstr ""
msgid "ContainerRegistry|Tag"
msgstr ""
-msgid "ContainerRegistry|Tag deleted successfully"
-msgstr ""
-
msgid "ContainerRegistry|Tag expiration policy"
msgstr ""
msgid "ContainerRegistry|Tag expiration policy is designed to:"
msgstr ""
-msgid "ContainerRegistry|Tags deleted successfully"
+msgid "ContainerRegistry|Tag successfully marked for deletion."
+msgstr ""
+
+msgid "ContainerRegistry|Tags successfully marked for deletion."
+msgstr ""
+
+msgid "ContainerRegistry|Tags with names matching this regex pattern will %{italicStart}be preserved:%{italicEnd}"
+msgstr ""
+
+msgid "ContainerRegistry|Tags with names matching this regex pattern will %{italicStart}expire:%{italicEnd}"
+msgstr ""
+
+msgid "ContainerRegistry|The Container Registry tag expiration and retention policies for this project have not been enabled."
msgstr ""
msgid "ContainerRegistry|The last tag related to this image was recently removed. This empty image and any associated data will be automatically removed as part of the regular Garbage Collection process. If you have any questions, contact your administrator."
@@ -5589,16 +5791,16 @@ msgstr ""
msgid "ContainerRegistry|There are no container images stored for this project"
msgstr ""
-msgid "ContainerRegistry|This Registry contains deleted image tag data. Remember to run %{docLinkStart}garbage collection%{docLinkEnd} to remove the stale data from storage."
+msgid "ContainerRegistry|There was an error during the deletion of this image repository, please try again."
msgstr ""
msgid "ContainerRegistry|This image has no active tags"
msgstr ""
-msgid "ContainerRegistry|We are having trouble connecting to Docker, which could be due to an issue with your project name or path. %{docLinkStart}More Information%{docLinkEnd}"
+msgid "ContainerRegistry|This image repository is scheduled for deletion"
msgstr ""
-msgid "ContainerRegistry|Wildcards such as %{codeStart}.*-stable%{codeEnd} or %{codeStart}production/.*%{codeEnd} are supported. To select all tags, use %{codeStart}.*%{codeEnd}"
+msgid "ContainerRegistry|We are having trouble connecting to Docker, which could be due to an issue with your project name or path. %{docLinkStart}More Information%{docLinkEnd}"
msgstr ""
msgid "ContainerRegistry|With the Container Registry, every project can have its own space to store its Docker images. %{docLinkStart}More Information%{docLinkEnd}"
@@ -5853,7 +6055,7 @@ msgstr ""
msgid "Could not delete chat nickname %{chat_name}."
msgstr ""
-msgid "Could not find design"
+msgid "Could not find design."
msgstr ""
msgid "Could not remove the trigger."
@@ -5868,6 +6070,9 @@ msgstr ""
msgid "Could not revoke personal access token %{personal_access_token_name}."
msgstr ""
+msgid "Could not revoke project access token %{project_access_token_name}."
+msgstr ""
+
msgid "Could not save group ID"
msgstr ""
@@ -5925,6 +6130,9 @@ msgstr ""
msgid "Create a new branch"
msgstr ""
+msgid "Create a new deploy key for this project"
+msgstr ""
+
msgid "Create a new file as there are no files yet. Afterwards, you'll be able to commit your changes."
msgstr ""
@@ -6030,6 +6238,9 @@ msgstr ""
msgid "Create requirement"
msgstr ""
+msgid "Create snippet"
+msgstr ""
+
msgid "Create wildcard: %{searchTerm}"
msgstr ""
@@ -6051,6 +6262,9 @@ msgstr ""
msgid "Created"
msgstr ""
+msgid "Created %{timestamp}"
+msgstr ""
+
msgid "Created At"
msgstr ""
@@ -6407,6 +6621,9 @@ msgstr ""
msgid "CycleAnalytics|stage dropdown"
msgstr ""
+msgid "DAG"
+msgstr ""
+
msgid "DNS"
msgstr ""
@@ -6773,6 +6990,9 @@ msgstr ""
msgid "Deploy key was successfully updated."
msgstr ""
+msgid "Deploy keys allow read-only or read-write (if enabled) access to your repository. Deploy keys can be used for CI, staging or production servers. You can create a deploy key or add an existing one."
+msgstr ""
+
msgid "Deploy progress not found. To see pods, ensure your environment matches %{linkStart}deploy board criteria%{linkEnd}."
msgstr ""
@@ -6926,6 +7146,9 @@ msgstr ""
msgid "Deploying to"
msgstr ""
+msgid "Deployment Frequency"
+msgstr ""
+
msgid "Deployment|API"
msgstr ""
@@ -7444,9 +7667,6 @@ msgstr ""
msgid "Edit environment"
msgstr ""
-msgid "Edit epic description"
-msgstr ""
-
msgid "Edit file"
msgstr ""
@@ -8068,9 +8288,6 @@ msgstr ""
msgid "Epics"
msgstr ""
-msgid "Epics (Ultimate / Gold license only)"
-msgstr ""
-
msgid "Epics Roadmap"
msgstr ""
@@ -8080,6 +8297,9 @@ msgstr ""
msgid "Epics let you manage your portfolio of projects more efficiently and with less effort"
msgstr ""
+msgid "Epics, Issues, and Merge Requests"
+msgstr ""
+
msgid "Epics|Add an epic"
msgstr ""
@@ -8131,6 +8351,9 @@ msgstr ""
msgid "Epics|Something went wrong while fetching group epics."
msgstr ""
+msgid "Epics|Something went wrong while moving item."
+msgstr ""
+
msgid "Epics|Something went wrong while ordering item."
msgstr ""
@@ -8185,6 +8408,9 @@ msgstr ""
msgid "Error fetching network graph."
msgstr ""
+msgid "Error fetching payload data."
+msgstr ""
+
msgid "Error fetching projects"
msgstr ""
@@ -8194,9 +8420,6 @@ msgstr ""
msgid "Error fetching the dependency list. Please check your network connection and try again."
msgstr ""
-msgid "Error fetching usage ping data."
-msgstr ""
-
msgid "Error loading branch data. Please try again."
msgstr ""
@@ -8338,6 +8561,9 @@ msgstr ""
msgid "Errors"
msgstr ""
+msgid "Errors:"
+msgstr ""
+
msgid "Estimated"
msgstr ""
@@ -8542,12 +8768,18 @@ msgstr ""
msgid "Export as CSV"
msgstr ""
+msgid "Export group"
+msgstr ""
+
msgid "Export issues"
msgstr ""
msgid "Export project"
msgstr ""
+msgid "Export this group with all related data to a new GitLab instance. Once complete, you can import the data file from the \"New Group\" page."
+msgstr ""
+
msgid "Export this project with all its related data in order to move your project to a new GitLab instance. Once the export is finished, you can import the file from the \"New Project\" page."
msgstr ""
@@ -8617,9 +8849,6 @@ msgstr ""
msgid "Failed Jobs"
msgstr ""
-msgid "Failed create wiki"
-msgstr ""
-
msgid "Failed to add a Zoom meeting"
msgstr ""
@@ -8659,6 +8888,9 @@ msgstr ""
msgid "Failed to create resources"
msgstr ""
+msgid "Failed to create wiki"
+msgstr ""
+
msgid "Failed to delete board. Please try again."
msgstr ""
@@ -8668,7 +8900,7 @@ msgstr ""
msgid "Failed to enqueue the rebase operation, possibly due to a long-lived transaction. Try again later."
msgstr ""
-msgid "Failed to find import label for jira import."
+msgid "Failed to find import label for Jira import."
msgstr ""
msgid "Failed to get ref."
@@ -8692,6 +8924,12 @@ msgstr ""
msgid "Failed to load groups & users."
msgstr ""
+msgid "Failed to load labels. Please try again."
+msgstr ""
+
+msgid "Failed to load milestones. Please try again."
+msgstr ""
+
msgid "Failed to load related branches"
msgstr ""
@@ -9067,7 +9305,10 @@ msgstr ""
msgid "Filter by milestone name"
msgstr ""
-msgid "Filter by name..."
+msgid "Filter by name"
+msgstr ""
+
+msgid "Filter by status"
msgstr ""
msgid "Filter by two-factor authentication"
@@ -9079,12 +9320,18 @@ msgstr ""
msgid "Filter projects"
msgstr ""
+msgid "Filter results"
+msgstr ""
+
msgid "Filter results by group"
msgstr ""
msgid "Filter results by project"
msgstr ""
+msgid "Filter results..."
+msgstr ""
+
msgid "Filter your projects by name"
msgstr ""
@@ -9373,10 +9620,10 @@ msgstr ""
msgid "GeoNodes|Checksummed"
msgstr ""
-msgid "GeoNodes|Container repositories"
+msgid "GeoNodes|Consult Geo troubleshooting information"
msgstr ""
-msgid "GeoNodes|Data is out of date from %{timeago}"
+msgid "GeoNodes|Container repositories"
msgstr ""
msgid "GeoNodes|Data replication lag"
@@ -9418,6 +9665,9 @@ msgstr ""
msgid "GeoNodes|Last event ID seen from primary"
msgstr ""
+msgid "GeoNodes|Learn more about Geo node statuses"
+msgstr ""
+
msgid "GeoNodes|Loading nodes"
msgstr ""
@@ -9433,6 +9683,9 @@ msgstr ""
msgid "GeoNodes|Node was successfully removed."
msgstr ""
+msgid "GeoNodes|Node's status was updated %{timeAgo}."
+msgstr ""
+
msgid "GeoNodes|Not checksummed"
msgstr ""
@@ -9463,7 +9716,10 @@ msgstr ""
msgid "GeoNodes|Repository verification progress"
msgstr ""
-msgid "GeoNodes|Selective"
+msgid "GeoNodes|Selective (%{syncLabel})"
+msgstr ""
+
+msgid "GeoNodes|Selective synchronization"
msgstr ""
msgid "GeoNodes|Something went wrong while changing node status"
@@ -9490,6 +9746,9 @@ msgstr ""
msgid "GeoNodes|Unverified"
msgstr ""
+msgid "GeoNodes|Updated %{timeAgo}"
+msgstr ""
+
msgid "GeoNodes|Used slots"
msgstr ""
@@ -9529,16 +9788,16 @@ msgstr ""
msgid "Geo|All"
msgstr ""
-msgid "Geo|All projects"
+msgid "Geo|All %{replicable_type}"
msgstr ""
-msgid "Geo|All projects are being scheduled for re-sync"
+msgid "Geo|All projects"
msgstr ""
-msgid "Geo|All projects are being scheduled for re-verify"
+msgid "Geo|All projects are being scheduled for resync"
msgstr ""
-msgid "Geo|Batch operations"
+msgid "Geo|All projects are being scheduled for reverify"
msgstr ""
msgid "Geo|Could not remove tracking entry for an existing project."
@@ -9550,9 +9809,15 @@ msgstr ""
msgid "Geo|Failed"
msgstr ""
+msgid "Geo|Filter by status"
+msgstr ""
+
msgid "Geo|Geo Status"
msgstr ""
+msgid "Geo|In progress"
+msgstr ""
+
msgid "Geo|In sync"
msgstr ""
@@ -9577,9 +9842,6 @@ msgstr ""
msgid "Geo|Not synced yet"
msgstr ""
-msgid "Geo|Pending"
-msgstr ""
-
msgid "Geo|Pending synchronization"
msgstr ""
@@ -9616,7 +9878,7 @@ msgstr ""
msgid "Geo|Resync"
msgstr ""
-msgid "Geo|Resync all projects"
+msgid "Geo|Resync all"
msgstr ""
msgid "Geo|Retry count"
@@ -9625,7 +9887,7 @@ msgstr ""
msgid "Geo|Reverify"
msgstr ""
-msgid "Geo|Reverify all projects"
+msgid "Geo|Reverify all"
msgstr ""
msgid "Geo|Status"
@@ -9967,9 +10229,6 @@ msgstr ""
msgid "Go to file"
msgstr ""
-msgid "Go to file (MRs only)"
-msgstr ""
-
msgid "Go to file permalink (while viewing a file)"
msgstr ""
@@ -10414,6 +10673,9 @@ msgstr ""
msgid "GroupSettings|Disable group mentions"
msgstr ""
+msgid "GroupSettings|Export group"
+msgstr ""
+
msgid "GroupSettings|If the parent group's visibility is lower than the group current visibility, visibility levels for subgroups and projects will be changed to match the new parent group's visibility."
msgstr ""
@@ -10492,6 +10754,9 @@ msgstr ""
msgid "Groups (%{count})"
msgstr ""
+msgid "Groups (%{groups})"
+msgstr ""
+
msgid "Groups can also be nested by creating %{subgroup_docs_link_start}subgroups%{subgroup_docs_link_end}."
msgstr ""
@@ -10757,15 +11022,12 @@ msgstr ""
msgid "ID:"
msgstr ""
-msgid "IDE|Allow live previews of JavaScript projects in the Web IDE using CodeSandbox client side evaluation."
+msgid "IDE|Allow live previews of JavaScript projects in the Web IDE using CodeSandbox Live Preview."
msgstr ""
msgid "IDE|Back"
msgstr ""
-msgid "IDE|Client side evaluation"
-msgstr ""
-
msgid "IDE|Commit"
msgstr ""
@@ -10829,6 +11091,9 @@ msgstr ""
msgid "If any job surpasses this timeout threshold, it will be marked as failed. Human readable time input language is accepted like \"1 hour\". Values without specification represent seconds."
msgstr ""
+msgid "If blank, set allowable lifetime to %{instance_level_policy_in_words}, as defined by the instance admin. Once set, existing tokens for users in this group may be revoked."
+msgstr ""
+
msgid "If checked, group owners can manage LDAP group links and LDAP member overrides"
msgstr ""
@@ -10868,6 +11133,9 @@ msgstr ""
msgid "If you lose your recovery codes you can generate new ones, invalidating all previous codes."
msgstr ""
+msgid "If you reach 100%% storage capacity, you will not be able to: %{base_message}"
+msgstr ""
+
msgid "If your HTTP repository is not publicly accessible, add your credentials."
msgstr ""
@@ -10979,6 +11247,9 @@ msgstr ""
msgid "Import repository"
msgstr ""
+msgid "Import started by: %{importInitiator}"
+msgstr ""
+
msgid "Import tasks"
msgstr ""
@@ -11045,6 +11316,9 @@ msgstr ""
msgid "In order to tailor your experience with GitLab we<br>would like to know a bit more about you."
msgstr ""
+msgid "In progress"
+msgstr ""
+
msgid "In the next step, you'll be able to select the projects you want to import."
msgstr ""
@@ -11392,15 +11666,15 @@ msgstr ""
msgid "Issues"
msgstr ""
-msgid "Issues / Merge Requests"
-msgstr ""
-
msgid "Issues Analytics"
msgstr ""
msgid "Issues Rate Limits"
msgstr ""
+msgid "Issues and Merge Requests"
+msgstr ""
+
msgid "Issues can be bugs, tasks or ideas to be discussed. Also, issues are searchable and filterable."
msgstr ""
@@ -11422,10 +11696,10 @@ msgstr ""
msgid "IssuesAnalytics|Avg/Month:"
msgstr ""
-msgid "IssuesAnalytics|Issues created"
+msgid "IssuesAnalytics|Issues opened"
msgstr ""
-msgid "IssuesAnalytics|Issues created per month"
+msgid "IssuesAnalytics|Issues opened per month"
msgstr ""
msgid "IssuesAnalytics|Last 12 months"
@@ -11479,6 +11753,9 @@ msgstr ""
msgid "Jira integration not configured."
msgstr ""
+msgid "Jira project: %{importProject}"
+msgstr ""
+
msgid "JiraService|Events for %{noteable_model_name} are disabled."
msgstr ""
@@ -11868,6 +12145,9 @@ msgstr ""
msgid "Lead"
msgstr ""
+msgid "Lead Time"
+msgstr ""
+
msgid "Learn GitLab"
msgstr ""
@@ -11880,6 +12160,9 @@ msgstr ""
msgid "Learn how to %{no_packages_link_start}publish and share your packages%{no_packages_link_end} with GitLab."
msgstr ""
+msgid "Learn how to enable synchronization"
+msgstr ""
+
msgid "Learn more"
msgstr ""
@@ -12211,9 +12494,6 @@ msgstr ""
msgid "Live preview"
msgstr ""
-msgid "Load more vulnerabilities"
-msgstr ""
-
msgid "Loading"
msgstr ""
@@ -12364,9 +12644,6 @@ msgstr ""
msgid "Manage"
msgstr ""
-msgid "Manage Git repositories with fine-grained access controls that keep your code secure. Perform code reviews and enhance collaboration with merge requests. Each project can also have an issue tracker and a wiki."
-msgstr ""
-
msgid "Manage Web IDE features"
msgstr ""
@@ -12397,6 +12674,9 @@ msgstr ""
msgid "Manage your license"
msgstr ""
+msgid "Managed Account"
+msgstr ""
+
msgid "Manifest"
msgstr ""
@@ -12661,6 +12941,9 @@ msgstr ""
msgid "Merge Requests in Review"
msgstr ""
+msgid "Merge automatically (%{strategy})"
+msgstr ""
+
msgid "Merge commit message"
msgstr ""
@@ -12814,6 +13097,12 @@ msgstr ""
msgid "Merged branches are being deleted. This can take some time depending on the number of branches. Please refresh the page to see changes."
msgstr ""
+msgid "Merged this merge request."
+msgstr ""
+
+msgid "Merges this merge request immediately."
+msgstr ""
+
msgid "Merges this merge request when the pipeline succeeds."
msgstr ""
@@ -12921,12 +13210,15 @@ msgid_plural "Metrics|Edit metrics"
msgstr[0] ""
msgstr[1] ""
-msgid "Metrics|Environment"
+msgid "Metrics|Expand panel"
msgstr ""
msgid "Metrics|For grouping similar metrics"
msgstr ""
+msgid "Metrics|Go back (Esc)"
+msgstr ""
+
msgid "Metrics|Invalid time range, please verify."
msgstr ""
@@ -12957,9 +13249,6 @@ msgstr ""
msgid "Metrics|Refresh dashboard"
msgstr ""
-msgid "Metrics|Show last"
-msgstr ""
-
msgid "Metrics|There was an error creating the dashboard."
msgstr ""
@@ -13038,6 +13327,9 @@ msgstr ""
msgid "Microsoft Azure"
msgstr ""
+msgid "Middleman project with Static Site Editor support"
+msgstr ""
+
msgid "Migrated %{success_count}/%{total_count} files."
msgstr ""
@@ -13483,10 +13775,10 @@ msgstr ""
msgid "Next"
msgstr ""
-msgid "Next file in diff (MRs only)"
+msgid "Next file in diff"
msgstr ""
-msgid "Next unresolved discussion (MRs only)"
+msgid "Next unresolved discussion"
msgstr ""
msgid "Nickname"
@@ -13546,6 +13838,9 @@ msgstr ""
msgid "No changes between %{ref_start}%{source_branch}%{ref_end} and %{ref_start}%{target_branch}%{ref_end}"
msgstr ""
+msgid "No child epics match applied filters"
+msgstr ""
+
msgid "No connection could be made to a Gitaly Server, please check your logs!"
msgstr ""
@@ -13684,15 +13979,6 @@ msgstr ""
msgid "No thanks, don't show this again"
msgstr ""
-msgid "No vulnerabilities found for this group"
-msgstr ""
-
-msgid "No vulnerabilities found for this pipeline"
-msgstr ""
-
-msgid "No vulnerabilities found for this project"
-msgstr ""
-
msgid "No vulnerabilities present"
msgstr ""
@@ -13816,6 +14102,9 @@ msgstr ""
msgid "Nothing to preview."
msgstr ""
+msgid "Nothing to synchronize"
+msgstr ""
+
msgid "Notification events"
msgstr ""
@@ -13987,6 +14276,9 @@ msgstr ""
msgid "Once removed, the fork relationship cannot be restored and you will no longer be able to send merge requests to the source."
msgstr ""
+msgid "Once the exported file is ready you can download it from this page."
+msgstr ""
+
msgid "Once the exported file is ready, you will receive a notification email with a download link, or you can download it from this page."
msgstr ""
@@ -14088,9 +14380,6 @@ msgstr ""
msgid "Open sidebar"
msgstr ""
-msgid "Open source software to collaborate on code"
-msgstr ""
-
msgid "Open: %{openIssuesCount}"
msgstr ""
@@ -14193,6 +14482,9 @@ msgstr ""
msgid "OutdatedBrowser|You can provide feedback %{feedback_link_start}on this issue%{feedback_link_end} or via your usual support channels."
msgstr ""
+msgid "Overridden"
+msgstr ""
+
msgid "Overview"
msgstr ""
@@ -14244,6 +14536,9 @@ msgstr ""
msgid "PackageRegistry|Conan Command"
msgstr ""
+msgid "PackageRegistry|Copy .pypirc content"
+msgstr ""
+
msgid "PackageRegistry|Copy Conan Command"
msgstr ""
@@ -14265,6 +14560,9 @@ msgstr ""
msgid "PackageRegistry|Copy NuGet Setup Command"
msgstr ""
+msgid "PackageRegistry|Copy Pip command"
+msgstr ""
+
msgid "PackageRegistry|Copy and paste this inside your %{codeStart}pom.xml%{codeEnd} %{codeStart}dependencies%{codeEnd} block."
msgstr ""
@@ -14298,6 +14596,12 @@ msgstr ""
msgid "PackageRegistry|For more information on the NuGet registry, %{linkStart}see the documentation%{linkEnd}."
msgstr ""
+msgid "PackageRegistry|For more information on the PyPi registry, %{linkStart}see the documentation%{linkEnd}."
+msgstr ""
+
+msgid "PackageRegistry|If you haven't already done so, you will need to add the below to your %{codeStart}.pypirc%{codeEnd} file."
+msgstr ""
+
msgid "PackageRegistry|If you haven't already done so, you will need to add the below to your %{codeStart}pom.xml%{codeEnd} file."
msgstr ""
@@ -14307,6 +14611,9 @@ msgstr ""
msgid "PackageRegistry|Learn how to %{noPackagesLinkStart}publish and share your packages%{noPackagesLinkEnd} with GitLab."
msgstr ""
+msgid "PackageRegistry|Manually Published"
+msgstr ""
+
msgid "PackageRegistry|Maven"
msgstr ""
@@ -14325,12 +14632,18 @@ msgstr ""
msgid "PackageRegistry|NuGet Command"
msgstr ""
+msgid "PackageRegistry|Pip Command"
+msgstr ""
+
msgid "PackageRegistry|Pipeline %{linkStart}%{linkEnd} triggered %{timestamp} by %{author}"
msgstr ""
msgid "PackageRegistry|Published to the repository at %{timestamp}"
msgstr ""
+msgid "PackageRegistry|PyPi"
+msgstr ""
+
msgid "PackageRegistry|Registry Setup"
msgstr ""
@@ -14367,6 +14680,9 @@ msgstr ""
msgid "PackageRegistry|npm"
msgstr ""
+msgid "PackageRegistry|published by %{author}"
+msgstr ""
+
msgid "PackageRegistry|yarn"
msgstr ""
@@ -14382,6 +14698,9 @@ msgstr ""
msgid "PackageType|NuGet"
msgstr ""
+msgid "PackageType|PyPi"
+msgstr ""
+
msgid "Packages"
msgstr ""
@@ -14475,6 +14794,9 @@ msgstr ""
msgid "Past due"
msgstr ""
+msgid "Paste a machine public key here. Read more about how to generate it"
+msgstr ""
+
msgid "Paste a machine public key here. Read more about how to generate it %{link_start}here%{link_end}"
msgstr ""
@@ -14490,9 +14812,6 @@ msgstr ""
msgid "Path"
msgstr ""
-msgid "Path, transfer, remove"
-msgstr ""
-
msgid "Path:"
msgstr ""
@@ -14523,7 +14842,7 @@ msgstr ""
msgid "Percentage"
msgstr ""
-msgid "Perform advanced options such as changing path, transferring, or removing the group."
+msgid "Perform advanced options such as changing path, transferring, exporting, or removing the group."
msgstr ""
msgid "Perform common operations on GitLab project"
@@ -14604,9 +14923,6 @@ msgstr ""
msgid "Pipeline minutes quota"
msgstr ""
-msgid "Pipeline quota"
-msgstr ""
-
msgid "Pipeline subscriptions"
msgstr ""
@@ -14616,6 +14932,9 @@ msgstr ""
msgid "Pipeline: %{status}"
msgstr ""
+msgid "PipelineCharts|CI / CD Analytics"
+msgstr ""
+
msgid "PipelineCharts|Failed:"
msgstr ""
@@ -14694,15 +15013,6 @@ msgstr ""
msgid "Pipelines settings for '%{project_name}' were successfully updated."
msgstr ""
-msgid "Pipelines| to purchase more minutes."
-msgstr ""
-
-msgid "Pipelines|%{namespace_name} has exceeded its pipeline minutes quota."
-msgstr ""
-
-msgid "Pipelines|%{namespace_name} has less than %{notification_level}%% of CI minutes available."
-msgstr ""
-
msgid "Pipelines|API"
msgstr ""
@@ -14724,10 +15034,13 @@ msgstr ""
msgid "Pipelines|Get started with Pipelines"
msgstr ""
-msgid "Pipelines|Loading Pipelines"
+msgid "Pipelines|Group %{namespace_name} has %{percentage}%% or less Shared Runner Pipeline minutes remaining. Once it runs out, no new jobs or pipelines in its projects will run."
msgstr ""
-msgid "Pipelines|Pipelines will not run anymore on shared Runners."
+msgid "Pipelines|Group %{namespace_name} has exceeded its pipeline minutes quota. Unless you buy additional pipeline minutes, no new jobs or pipelines in its projects will run."
+msgstr ""
+
+msgid "Pipelines|Loading Pipelines"
msgstr ""
msgid "Pipelines|Project cache successfully reset."
@@ -14958,6 +15271,9 @@ msgstr ""
msgid "Please select"
msgstr ""
+msgid "Please select a Jira project"
+msgstr ""
+
msgid "Please select a country"
msgstr ""
@@ -15003,6 +15319,9 @@ msgstr ""
msgid "Please wait while we import the repository for you. Refresh at will."
msgstr ""
+msgid "Plugins directory is deprecated and will be removed in 14.0. Please move this file into /file_hooks directory."
+msgstr ""
+
msgid "Pod does not exist"
msgstr ""
@@ -15120,7 +15439,7 @@ msgstr ""
msgid "Prevent users from changing their profile name"
msgstr ""
-msgid "Prevent users from modifing merge request approvers list"
+msgid "Prevent users from modifying merge request approvers list"
msgstr ""
msgid "Prevent users from performing write operations on GitLab while performing maintenance."
@@ -15141,10 +15460,10 @@ msgstr ""
msgid "Previous Artifacts"
msgstr ""
-msgid "Previous file in diff (MRs only)"
+msgid "Previous file in diff"
msgstr ""
-msgid "Previous unresolved discussion (MRs only)"
+msgid "Previous unresolved discussion"
msgstr ""
msgid "Primary"
@@ -15177,6 +15496,9 @@ msgstr ""
msgid "Private profile"
msgstr ""
+msgid "Private projects Minutes cost factor"
+msgstr ""
+
msgid "Private projects can be created in your personal namespace with:"
msgstr ""
@@ -16005,6 +16327,9 @@ msgstr ""
msgid "ProjectSettings|With GitLab Pages you can host your static websites on GitLab"
msgstr ""
+msgid "ProjectSettings|With Metrics Dashboard you can visualize this project performance metrics"
+msgstr ""
+
msgid "ProjectTemplates|.NET Core"
msgstr ""
@@ -16068,6 +16393,9 @@ msgstr ""
msgid "ProjectTemplates|Spring"
msgstr ""
+msgid "ProjectTemplates|Static Site Editor/Middleman"
+msgstr ""
+
msgid "ProjectTemplates|iOS (Swift)"
msgstr ""
@@ -16287,13 +16615,13 @@ msgstr ""
msgid "PrometheusService|Prometheus is being automatically managed on your clusters"
msgstr ""
-msgid "PrometheusService|These metrics will only be monitored after your first deployment to an environment"
+msgid "PrometheusService|Select the Active checkbox to override the Auto Configuration with custom settings. If unchecked, Auto Configuration settings are used."
msgstr ""
-msgid "PrometheusService|Time-series monitoring service"
+msgid "PrometheusService|These metrics will only be monitored after your first deployment to an environment"
msgstr ""
-msgid "PrometheusService|To enable manual configuration, uninstall Prometheus from your clusters"
+msgid "PrometheusService|Time-series monitoring service"
msgstr ""
msgid "PrometheusService|To enable the installation of Prometheus on your clusters, deactivate the manual configuration below"
@@ -16479,6 +16807,9 @@ msgstr ""
msgid "Protip:"
msgstr ""
+msgid "Protocol"
+msgstr ""
+
msgid "Provider"
msgstr ""
@@ -16503,6 +16834,9 @@ msgstr ""
msgid "Public pipelines"
msgstr ""
+msgid "Public projects Minutes cost factor"
+msgstr ""
+
msgid "Pull"
msgstr ""
@@ -16703,6 +17037,9 @@ msgid_plural "Refreshing in %d seconds to show the updated status..."
msgstr[0] ""
msgstr[1] ""
+msgid "Regenerate export"
+msgstr ""
+
msgid "Regenerate instance ID"
msgstr ""
@@ -17080,9 +17417,29 @@ msgstr ""
msgid "Reports|%{combinedString} and %{resolvedString}"
msgstr ""
+msgid "Reports|Accessibility scanning detected %d issue for the source branch only"
+msgid_plural "Reports|Accessibility scanning detected %d issues for the source branch only"
+msgstr[0] ""
+msgstr[1] ""
+
+msgid "Reports|Accessibility scanning detected no issues for the source branch only"
+msgstr ""
+
+msgid "Reports|Accessibility scanning failed loading results"
+msgstr ""
+
+msgid "Reports|Accessibility scanning results are being parsed"
+msgstr ""
+
msgid "Reports|Actions"
msgstr ""
+msgid "Reports|An error occured while loading report"
+msgstr ""
+
+msgid "Reports|An error occurred while loading %{name} results"
+msgstr ""
+
msgid "Reports|Class"
msgstr ""
@@ -17188,7 +17545,7 @@ msgstr ""
msgid "Requested %{time_ago}"
msgstr ""
-msgid "Requested design version does not exist"
+msgid "Requested design version does not exist."
msgstr ""
msgid "Requested states are invalid"
@@ -17372,6 +17729,9 @@ msgstr ""
msgid "Resync"
msgstr ""
+msgid "Resync all"
+msgstr ""
+
msgid "Resync all %{replicableType}"
msgstr ""
@@ -17440,6 +17800,9 @@ msgstr ""
msgid "Revoked personal access token %{personal_access_token_name}!"
msgstr ""
+msgid "Revoked project access token %{project_access_token_name}!"
+msgstr ""
+
msgid "RightSidebar|adding a"
msgstr ""
@@ -17488,6 +17851,9 @@ msgstr ""
msgid "Runner token"
msgstr ""
+msgid "Runner tokens"
+msgstr ""
+
msgid "Runner was not updated."
msgstr ""
@@ -17542,6 +17908,9 @@ msgstr ""
msgid "SAML SSO for %{group_name}"
msgstr ""
+msgid "SAML discovery tokens"
+msgstr ""
+
msgid "SAML for %{group_name}"
msgstr ""
@@ -17626,12 +17995,18 @@ msgstr ""
msgid "Scheduled"
msgstr ""
+msgid "Scheduled to merge this merge request (%{strategy})."
+msgstr ""
+
msgid "Scheduled to merge this merge request when the pipeline succeeds."
msgstr ""
msgid "Schedules"
msgstr ""
+msgid "Schedules to merge this merge request (%{strategy})."
+msgstr ""
+
msgid "Scheduling"
msgstr ""
@@ -17689,6 +18064,9 @@ msgstr ""
msgid "Search branches and tags"
msgstr ""
+msgid "Search by author"
+msgstr ""
+
msgid "Search files"
msgstr ""
@@ -17834,11 +18212,6 @@ msgid_plural "SearchResults|snippets"
msgstr[0] ""
msgstr[1] ""
-msgid "SearchResults|snippet result"
-msgid_plural "SearchResults|snippet results"
-msgstr[0] ""
-msgstr[1] ""
-
msgid "SearchResults|user"
msgid_plural "SearchResults|users"
msgstr[0] ""
@@ -17849,6 +18222,9 @@ msgid_plural "SearchResults|wiki results"
msgstr[0] ""
msgstr[1] ""
+msgid "Searching by both author and message is currently not supported."
+msgstr ""
+
msgid "Seat Link"
msgstr ""
@@ -17876,184 +18252,232 @@ msgstr ""
msgid "Security Dashboard"
msgstr ""
-msgid "Security Dashboard|Error fetching the vulnerability counts. Please check your network connection and try again."
+msgid "Security configuration help link"
+msgstr ""
+
+msgid "Security dashboard"
msgstr ""
-msgid "Security Dashboard|Error fetching the vulnerability list. Please check your network connection and try again."
+msgid "Security report is out of date. Please update your branch with the latest changes from the target branch (%{targetBranchName})"
msgstr ""
-msgid "Security Dashboard|Issue Created"
+msgid "Security report is out of date. Run %{newPipelineLinkStart}a new pipeline%{newPipelineLinkEnd} for the target branch (%{targetBranchName})"
msgstr ""
-msgid "Security Reports|Comment added to '%{vulnerabilityName}'"
+msgid "SecurityConfiguration|Configured"
msgstr ""
-msgid "Security Reports|Comment deleted on '%{vulnerabilityName}'"
+msgid "SecurityConfiguration|Feature"
msgstr ""
-msgid "Security Reports|Comment edited on '%{vulnerabilityName}'"
+msgid "SecurityConfiguration|Feature documentation for %{featureName}"
msgstr ""
-msgid "Security Reports|Create issue"
+msgid "SecurityConfiguration|Not yet configured"
msgstr ""
-msgid "Security Reports|Dismiss vulnerability"
+msgid "SecurityConfiguration|Secure features"
msgstr ""
-msgid "Security Reports|Dismissed '%{vulnerabilityName}'"
+msgid "SecurityConfiguration|Status"
msgstr ""
-msgid "Security Reports|Dismissed '%{vulnerabilityName}'. Turn off the hide dismissed toggle to view."
+msgid "SecurityReports|%{firstProject} and %{secondProject}"
msgstr ""
-msgid "Security Reports|Either you don't have permission to view this dashboard or the dashboard has not been setup. Please check your permission settings with your administrator or check your dashboard configurations to proceed."
+msgid "SecurityReports|%{firstProject}, %{secondProject}, and %{rest}"
msgstr ""
-msgid "Security Reports|Learn more about setting up your dashboard"
+msgid "SecurityReports|Add a project to your dashboard"
msgstr ""
-msgid "Security Reports|More info"
+msgid "SecurityReports|Add or remove projects from your dashboard"
msgstr ""
-msgid "Security Reports|Oops, something doesn't seem right."
+msgid "SecurityReports|Add projects"
msgstr ""
-msgid "Security Reports|Security reports can only be accessed by authorized users."
+msgid "SecurityReports|Comment added to '%{vulnerabilityName}'"
msgstr ""
-msgid "Security Reports|There was an error adding the comment."
+msgid "SecurityReports|Comment deleted on '%{vulnerabilityName}'"
msgstr ""
-msgid "Security Reports|There was an error creating the issue."
+msgid "SecurityReports|Comment edited on '%{vulnerabilityName}'"
msgstr ""
-msgid "Security Reports|There was an error creating the merge request."
+msgid "SecurityReports|Create issue"
msgstr ""
-msgid "Security Reports|There was an error deleting the comment."
+msgid "SecurityReports|Dismiss Selected"
msgstr ""
-msgid "Security Reports|There was an error dismissing the vulnerabilities."
+msgid "SecurityReports|Dismiss vulnerability"
msgstr ""
-msgid "Security Reports|There was an error dismissing the vulnerability."
+msgid "SecurityReports|Dismissed '%{vulnerabilityName}'"
msgstr ""
-msgid "Security Reports|There was an error reverting the dismissal."
+msgid "SecurityReports|Dismissed '%{vulnerabilityName}'. Turn off the hide dismissed toggle to view."
msgstr ""
-msgid "Security Reports|There was an error reverting this dismissal."
+msgid "SecurityReports|Each vulnerability now has a unique page that can be directly linked to, shared, referenced, and tracked as the single source of truth. Vulnerability occurrences also persist across scanner runs, which improves tracking and visibility and reduces duplicates between scans."
msgstr ""
-msgid "Security Reports|Undo dismiss"
+msgid "SecurityReports|Edit dashboard"
msgstr ""
-msgid "Security Reports|You do not have sufficient permissions to access this report"
+msgid "SecurityReports|Either you don't have permission to view this dashboard or the dashboard has not been setup. Please check your permission settings with your administrator or check your dashboard configurations to proceed."
msgstr ""
-msgid "Security Reports|You must sign in as an authorized user to see this report"
+msgid "SecurityReports|Error fetching the vulnerability counts. Please check your network connection and try again."
msgstr ""
-msgid "Security configuration help link"
+msgid "SecurityReports|Error fetching the vulnerability list. Please check your network connection and try again."
msgstr ""
-msgid "Security dashboard"
+msgid "SecurityReports|False positive"
msgstr ""
-msgid "Security report is out of date. Please update your branch with the latest changes from the target branch (%{targetBranchName})"
+msgid "SecurityReports|Hide dismissed"
msgstr ""
-msgid "Security report is out of date. Run %{newPipelineLinkStart}a new pipeline%{newPipelineLinkEnd} for the target branch (%{targetBranchName})"
+msgid "SecurityReports|Introducing standalone vulnerabilities"
msgstr ""
-msgid "SecurityConfiguration|Configured"
+msgid "SecurityReports|Issue Created"
msgstr ""
-msgid "SecurityConfiguration|Feature"
+msgid "SecurityReports|Learn More"
msgstr ""
-msgid "SecurityConfiguration|Feature documentation for %{featureName}"
+msgid "SecurityReports|Learn more about setting up your dashboard"
msgstr ""
-msgid "SecurityConfiguration|Not yet configured"
+msgid "SecurityReports|Load more vulnerabilities"
msgstr ""
-msgid "SecurityConfiguration|Secure features"
+msgid "SecurityReports|Monitor vulnerabilities in your code"
msgstr ""
-msgid "SecurityConfiguration|Status"
+msgid "SecurityReports|More info"
msgstr ""
-msgid "SecurityDashboard|%{firstProject} and %{secondProject}"
+msgid "SecurityReports|More information"
msgstr ""
-msgid "SecurityDashboard|%{firstProject}, %{secondProject}, and %{rest}"
+msgid "SecurityReports|No vulnerabilities found for dashboard"
msgstr ""
-msgid "SecurityDashboard|Add a project to your dashboard"
+msgid "SecurityReports|No vulnerabilities found for this group"
msgstr ""
-msgid "SecurityDashboard|Add or remove projects from your dashboard"
+msgid "SecurityReports|No vulnerabilities found for this pipeline"
msgstr ""
-msgid "SecurityDashboard|Add projects"
+msgid "SecurityReports|No vulnerabilities found for this project"
msgstr ""
-msgid "SecurityDashboard|Edit dashboard"
+msgid "SecurityReports|Oops, something doesn't seem right."
msgstr ""
-msgid "SecurityDashboard|Hide dismissed"
+msgid "SecurityReports|Pipeline %{pipelineLink} triggered %{timeago} by %{user}"
msgstr ""
-msgid "SecurityDashboard|Monitor vulnerabilities in your code"
+msgid "SecurityReports|Project"
msgstr ""
-msgid "SecurityDashboard|More information"
+msgid "SecurityReports|Projects added"
msgstr ""
-msgid "SecurityDashboard|No vulnerabilities found for dashboard"
+msgid "SecurityReports|Remove project from dashboard"
msgstr ""
-msgid "SecurityDashboard|Pipeline %{pipelineLink} triggered %{timeago} by %{user}"
+msgid "SecurityReports|Report type"
msgstr ""
-msgid "SecurityDashboard|Project"
+msgid "SecurityReports|Return to dashboard"
msgstr ""
-msgid "SecurityDashboard|Projects added"
+msgid "SecurityReports|Security Dashboard"
msgstr ""
-msgid "SecurityDashboard|Remove project from dashboard"
+msgid "SecurityReports|Security reports can only be accessed by authorized users."
msgstr ""
-msgid "SecurityDashboard|Report type"
+msgid "SecurityReports|Select a project to add by using the project search field above."
msgstr ""
-msgid "SecurityDashboard|Return to dashboard"
+msgid "SecurityReports|Select a reason"
msgstr ""
-msgid "SecurityDashboard|Security Dashboard"
+msgid "SecurityReports|Severity"
msgstr ""
-msgid "SecurityDashboard|Select a project to add by using the project search field above."
+msgid "SecurityReports|Status"
msgstr ""
-msgid "SecurityDashboard|Severity"
+msgid "SecurityReports|The security dashboard displays the latest security findings for projects you wish to monitor. Select \"Edit dashboard\" to add and remove projects."
msgstr ""
-msgid "SecurityDashboard|Status"
+msgid "SecurityReports|The security dashboard displays the latest security report. Use it to find and fix vulnerabilities."
msgstr ""
-msgid "SecurityDashboard|The security dashboard displays the latest security findings for projects you wish to monitor. Select \"Edit dashboard\" to add and remove projects."
+msgid "SecurityReports|There was an error adding the comment."
msgstr ""
-msgid "SecurityDashboard|The security dashboard displays the latest security report. Use it to find and fix vulnerabilities."
+msgid "SecurityReports|There was an error creating the issue."
msgstr ""
-msgid "SecurityDashboard|There was an error while generating the report."
+msgid "SecurityReports|There was an error creating the merge request."
msgstr ""
-msgid "SecurityDashboard|Unable to add %{invalidProjects}"
+msgid "SecurityReports|There was an error deleting the comment."
+msgstr ""
+
+msgid "SecurityReports|There was an error dismissing the vulnerabilities."
+msgstr ""
+
+msgid "SecurityReports|There was an error dismissing the vulnerability."
+msgstr ""
+
+msgid "SecurityReports|There was an error reverting the dismissal."
+msgstr ""
+
+msgid "SecurityReports|There was an error reverting this dismissal."
+msgstr ""
+
+msgid "SecurityReports|There was an error while generating the report."
+msgstr ""
+
+msgid "SecurityReports|Unable to add %{invalidProjects}"
+msgstr ""
+
+msgid "SecurityReports|Undo dismiss"
+msgstr ""
+
+msgid "SecurityReports|While it's rare to have no vulnerabilities for your group, it can happen. In any event, we ask that you double check your settings to make sure you've set up your dashboard correctly."
+msgstr ""
+
+msgid "SecurityReports|While it's rare to have no vulnerabilities for your pipeline, it can happen. In any event, we ask that you double check your settings to make sure all security scanning jobs have passed successfully."
+msgstr ""
+
+msgid "SecurityReports|While it's rare to have no vulnerabilities for your project, it can happen. In any event, we ask that you double check your settings to make sure you've set up your dashboard correctly."
+msgstr ""
+
+msgid "SecurityReports|While it's rare to have no vulnerabilities, it can happen. In any event, we ask that you please double check your settings to make sure you've set up your dashboard correctly."
+msgstr ""
+
+msgid "SecurityReports|Won't fix / Accept risk"
+msgstr ""
+
+msgid "SecurityReports|You do not have sufficient permissions to access this report"
+msgstr ""
+
+msgid "SecurityReports|You must sign in as an authorized user to see this report"
+msgstr ""
+
+msgid "SecurityReports|[No reason]"
msgstr ""
msgid "See GitLab's %{password_policy_guidelines}"
@@ -18065,6 +18489,9 @@ msgstr ""
msgid "See the affected projects in the GitLab admin panel"
msgstr ""
+msgid "See what's new at GitLab"
+msgstr ""
+
msgid "Select"
msgstr ""
@@ -18470,9 +18897,6 @@ msgstr ""
msgid "Set up Jira Integration"
msgstr ""
-msgid "Set up Jira Integration illustration"
-msgstr ""
-
msgid "Set up a %{type} Runner automatically"
msgstr ""
@@ -18557,6 +18981,9 @@ msgstr ""
msgid "Severity: %{severity}"
msgstr ""
+msgid "Shards (%{shards})"
+msgstr ""
+
msgid "Shards to synchronize"
msgstr ""
@@ -18871,6 +19298,9 @@ msgstr ""
msgid "Snowplow"
msgstr ""
+msgid "Some child epics may be hidden due to applied filters"
+msgstr ""
+
msgid "Some email servers do not support overriding the email sender name. Enable this option to include the name of the author of the issue, merge request or comment in the email body instead."
msgstr ""
@@ -19012,6 +19442,9 @@ msgstr ""
msgid "Something went wrong while updating your list settings"
msgstr ""
+msgid "Something went wrong with your automatic subscription renewal"
+msgstr ""
+
msgid "Something went wrong, unable to add %{project} to dashboard"
msgstr ""
@@ -19297,6 +19730,9 @@ msgstr ""
msgid "Stage removed"
msgstr ""
+msgid "Standard"
+msgstr ""
+
msgid "Star a label to make it a priority label. Order the prioritized labels to change their relative priority, by dragging."
msgstr ""
@@ -19420,6 +19856,9 @@ msgstr ""
msgid "Static Application Security Testing (SAST)"
msgstr ""
+msgid "StaticSiteEditor|An error occurred while submitting your changes."
+msgstr ""
+
msgid "StaticSiteEditor|Branch could not be created."
msgstr ""
@@ -19429,18 +19868,30 @@ msgstr ""
msgid "StaticSiteEditor|Could not create merge request."
msgstr ""
+msgid "StaticSiteEditor|Incompatible file content"
+msgstr ""
+
msgid "StaticSiteEditor|Return to site"
msgstr ""
+msgid "StaticSiteEditor|Static site editor"
+msgstr ""
+
msgid "StaticSiteEditor|Success!"
msgstr ""
msgid "StaticSiteEditor|Summary of changes"
msgstr ""
+msgid "StaticSiteEditor|The Static Site Editor is currently configured to only edit Markdown content on pages generated from Middleman. Visit the documentation to learn more about configuring your site to use the Static Site Editor."
+msgstr ""
+
msgid "StaticSiteEditor|Update %{sourcePath} file"
msgstr ""
+msgid "StaticSiteEditor|View documentation"
+msgstr ""
+
msgid "StaticSiteEditor|View merge request"
msgstr ""
@@ -19498,6 +19949,12 @@ msgstr ""
msgid "StatusPage|Status page"
msgstr ""
+msgid "StatusPage|Status page URL"
+msgstr ""
+
+msgid "StatusPage|Status page frontend documentation"
+msgstr ""
+
msgid "StatusPage|To publish incidents to an external status page, GitLab will store a JSON file in your Amazon S3 account in a location accessible to your external status page service. Make sure to also set up %{docsLink}"
msgstr ""
@@ -19846,6 +20303,12 @@ msgstr ""
msgid "Synced"
msgstr ""
+msgid "Synchronization disabled"
+msgstr ""
+
+msgid "Synchronization of container repositories is disabled."
+msgstr ""
+
msgid "System"
msgstr ""
@@ -19999,6 +20462,9 @@ msgstr ""
msgid "Telephone number"
msgstr ""
+msgid "Telephone number (Optional)"
+msgstr ""
+
msgid "Template"
msgstr ""
@@ -20305,6 +20771,9 @@ msgstr ""
msgid "The license was successfully uploaded and is now active. You can see the details below."
msgstr ""
+msgid "The license was successfully uploaded and will be active from %{starts_at}. You can see the details below."
+msgstr ""
+
msgid "The maximum file size allowed is %{size}."
msgstr ""
@@ -20626,6 +21095,9 @@ msgstr ""
msgid "There was an error getting the epic participants."
msgstr ""
+msgid "There was an error importing the Jira project."
+msgstr ""
+
msgid "There was an error loading users activity calendar."
msgstr ""
@@ -20641,7 +21113,7 @@ msgstr ""
msgid "There was an error resetting user pipeline minutes."
msgstr ""
-msgid "There was an error saving this Geo Node"
+msgid "There was an error saving this Geo Node."
msgstr ""
msgid "There was an error saving your changes."
@@ -20704,6 +21176,9 @@ msgstr ""
msgid "They can be managed using the %{link}."
msgstr ""
+msgid "Third Party Advisory Link"
+msgstr ""
+
msgid "Third party offers"
msgstr ""
@@ -20740,6 +21215,9 @@ msgstr ""
msgid "This application will be able to:"
msgstr ""
+msgid "This attachment has been truncated to avoid exceeding the maximum allowed attachment size of 15MB. %{written_count} of %{issues_count} issues have been included. Consider re-exporting with a narrower selection of issues."
+msgstr ""
+
msgid "This block is self-referential"
msgstr ""
@@ -20881,6 +21359,12 @@ msgstr ""
msgid "This issue is confidential"
msgstr ""
+msgid "This issue is currently blocked by the following issues: %{issues}."
+msgstr ""
+
+msgid "This issue is in a child epic of the filtered epic"
+msgstr ""
+
msgid "This issue is locked."
msgstr ""
@@ -21079,7 +21563,10 @@ msgstr ""
msgid "This user will be the author of all events in the activity feed that are the result of an update, like new branches being created or new commits being pushed to existing branches. Upon creation or when reassigning you can only assign yourself to be the mirror user."
msgstr ""
-msgid "This variable can not be masked"
+msgid "This variable can not be masked."
+msgstr ""
+
+msgid "This variable does not match the expected pattern."
msgstr ""
msgid "This will help us personalize your onboarding experience."
@@ -21109,10 +21596,10 @@ msgstr ""
msgid "ThreatMonitoring|Application firewall not detected"
msgstr ""
-msgid "ThreatMonitoring|Container Network Policy"
+msgid "ThreatMonitoring|Container Network Policies are not installed or have been disabled. To view this data, ensure your Network Policies are installed and enabled for your cluster."
msgstr ""
-msgid "ThreatMonitoring|Container NetworkPolicies are not installed or has been disabled. To view this data, ensure you NetworkPolicies are installed and enabled for your cluster."
+msgid "ThreatMonitoring|Container Network Policy"
msgstr ""
msgid "ThreatMonitoring|Container NetworkPolicies not detected"
@@ -21145,7 +21632,7 @@ msgstr ""
msgid "ThreatMonitoring|Something went wrong, unable to fetch statistics"
msgstr ""
-msgid "ThreatMonitoring|The firewall is not installed or has been disabled. To view this data, ensure you firewall is installed and enabled for your cluster."
+msgid "ThreatMonitoring|The firewall is not installed or has been disabled. To view this data, ensure the web application firewall is installed and enabled for your cluster."
msgstr ""
msgid "ThreatMonitoring|The graph below is an overview of traffic coming to your application as tracked by the Web Application Firewall (WAF). View the docs for instructions on how to access the WAF logs to see what type of malicious traffic is trying to access your app. The docs link is also accessible by clicking the \"?\" icon next to the title below."
@@ -21214,6 +21701,9 @@ msgstr ""
msgid "Time in seconds GitLab will wait for a response from the external service. When the service does not respond in time, access will be denied."
msgstr ""
+msgid "Time of import: %{importTime}"
+msgstr ""
+
msgid "Time remaining"
msgstr ""
@@ -21398,7 +21888,7 @@ msgstr ""
msgid "Title:"
msgstr ""
-msgid "Titles and Filenames"
+msgid "Titles and Descriptions"
msgstr ""
msgid "To"
@@ -21605,7 +22095,7 @@ msgstr ""
msgid "Too many projects enabled. You will need to manage them via the console or the API."
msgstr ""
-msgid "Topics"
+msgid "Topics (optional)"
msgstr ""
msgid "Total"
@@ -21767,6 +22257,9 @@ msgstr ""
msgid "Try all GitLab has to offer for 30 days."
msgstr ""
+msgid "Try changing or removing filters."
+msgstr ""
+
msgid "Try to fork again"
msgstr ""
@@ -21860,6 +22353,9 @@ msgstr ""
msgid "Unable to connect to server: %{error}"
msgstr ""
+msgid "Unable to connect to the Jira instance. Please check your Jira integration configuration."
+msgstr ""
+
msgid "Unable to convert Kubernetes logs encoding to UTF-8"
msgstr ""
@@ -22803,6 +23299,9 @@ msgstr ""
msgid "View issue"
msgstr ""
+msgid "View issues"
+msgstr ""
+
msgid "View it on GitLab"
msgstr ""
@@ -22980,6 +23479,15 @@ msgstr ""
msgid "VulnerabilityManagement|Resolved %{timeago} by %{user}"
msgstr ""
+msgid "VulnerabilityManagement|Something went wrong while trying to delete the comment. Please try again later."
+msgstr ""
+
+msgid "VulnerabilityManagement|Something went wrong while trying to retrieve the vulnerability history. Please try again later."
+msgstr ""
+
+msgid "VulnerabilityManagement|Something went wrong while trying to save the comment. Please try again later."
+msgstr ""
+
msgid "VulnerabilityManagement|Something went wrong, could not create an issue."
msgstr ""
@@ -23010,6 +23518,9 @@ msgstr ""
msgid "VulnerabilityStatusTypes|Resolved"
msgstr ""
+msgid "Vulnerability|%{scannerName} (version %{scannerVersion})"
+msgstr ""
+
msgid "Vulnerability|Class"
msgstr ""
@@ -23043,7 +23554,10 @@ msgstr ""
msgid "Vulnerability|Project"
msgstr ""
-msgid "Vulnerability|Report Type"
+msgid "Vulnerability|Scanner Provider"
+msgstr ""
+
+msgid "Vulnerability|Scanner Type"
msgstr ""
msgid "Vulnerability|Severity"
@@ -23097,9 +23611,15 @@ msgstr ""
msgid "We sent you an email with reset password instructions"
msgstr ""
+msgid "We tried to automatically renew your %{strong}%{plan_name}%{strong_close} subscription for %{strong}%{namespace_name}%{strong_close} on %{expires_on} but something went wrong so your subscription was downgraded to the free plan. Don't worry, your data is safe. We suggest you check your payment method and get in touch with our support team (%{support_link}). They'll gladly help with your subscription renewal."
+msgstr ""
+
msgid "We want to be sure it is you, please confirm you are not a robot."
msgstr ""
+msgid "We will automatically renew your %{strong}%{plan_name}%{strong_close} subscription for %{strong}%{namespace_name}%{strong_close} on %{strong}%{expires_on}%{strong_close}. There's nothing that you need to do, we'll let you know when the renewal is complete. Need more seats, a higher plan or just want to review your payment method?"
+msgstr ""
+
msgid "We've found no vulnerabilities"
msgstr ""
@@ -23204,15 +23724,6 @@ msgstr ""
msgid "When:"
msgstr ""
-msgid "While it's rare to have no vulnerabilities for your group, it can happen. In any event, we ask that you double check your settings to make sure you've set up your dashboard correctly."
-msgstr ""
-
-msgid "While it's rare to have no vulnerabilities for your pipeline, it can happen. In any event, we ask that you double check your settings to make sure all security scanning jobs have passed successfully."
-msgstr ""
-
-msgid "While it's rare to have no vulnerabilities for your project, it can happen. In any event, we ask that you double check your settings to make sure you've set up your dashboard correctly."
-msgstr ""
-
msgid "While it's rare to have no vulnerabilities, it can happen. In any event, we ask that you please double check your settings to make sure you've set up your dashboard correctly."
msgstr ""
@@ -23243,9 +23754,6 @@ msgstr ""
msgid "Wiki"
msgstr ""
-msgid "Wiki pages"
-msgstr ""
-
msgid "Wiki was successfully updated."
msgstr ""
@@ -23429,6 +23937,9 @@ msgstr ""
msgid "Yes, add it"
msgstr ""
+msgid "Yes, close issue"
+msgstr ""
+
msgid "Yes, let me map Google Code users to full names or GitLab users."
msgstr ""
@@ -23624,6 +24135,9 @@ msgstr ""
msgid "You could not create a new trigger."
msgstr ""
+msgid "You didn't renew your %{strong}%{plan_name}%{strong_close} subscription for %{strong}%{namespace_name}%{strong_close} so it was downgraded to the free plan."
+msgstr ""
+
msgid "You didn't renew your %{strong}%{plan_name}%{strong_close} subscription so it was downgraded to the GitLab Core Plan."
msgstr ""
@@ -23690,6 +24204,9 @@ msgstr ""
msgid "You have declined the invitation to join %{label}."
msgstr ""
+msgid "You have imported from this project %{numberOfPreviousImportsForProject} times before. Each new import will create duplicate issues."
+msgstr ""
+
msgid "You have no permissions"
msgstr ""
@@ -23771,6 +24288,9 @@ msgstr ""
msgid "You need to upload a Google Takeout archive."
msgstr ""
+msgid "You reached %{usage_in_percent} of %{namespace_name}'s capacity (%{used_storage} of %{storage_limit})"
+msgstr ""
+
msgid "You tried to fork %{link_to_the_project} but it failed for the following reason:"
msgstr ""
@@ -23855,9 +24375,21 @@ msgstr ""
msgid "YouTube"
msgstr ""
+msgid "Your %{strong}%{plan_name}%{strong_close} subscription for %{strong}%{namespace_name}%{strong_close} will expire on %{strong}%{expires_on}%{strong_close}. After that, you will not to be able to create issues or merge requests as well as many other features."
+msgstr ""
+
msgid "Your %{strong}%{plan_name}%{strong_close} subscription will expire on %{strong}%{expires_on}%{strong_close}. After that, you will not to be able to create issues or merge requests as well as many other features."
msgstr ""
+msgid "Your CSV export has started. It will be emailed to %{email} when complete."
+msgstr ""
+
+msgid "Your CSV export of %{issues_count} from project %{project_link} has been added to this email as an attachment."
+msgstr ""
+
+msgid "Your CSV export of %{written_count} from project %{project_name} (%{project_url}) has been added to this email as an attachment."
+msgstr ""
+
msgid "Your Commit Email will be used for web based operations, such as edits and merges."
msgstr ""
@@ -23954,6 +24486,9 @@ msgstr ""
msgid "Your comment could not be updated! Please check your network connection and try again."
msgstr ""
+msgid "Your comment will be discarded."
+msgstr ""
+
msgid "Your custom stage '%{title}' was created"
msgstr ""
@@ -23993,6 +24528,9 @@ msgstr ""
msgid "Your new personal access token has been created."
msgstr ""
+msgid "Your new project access token has been created."
+msgstr ""
+
msgid "Your password isn't required to view this page. If a password or any other personal details are requested, please contact your administrator to report abuse."
msgstr ""
@@ -24011,12 +24549,18 @@ msgstr ""
msgid "Your request for access has been queued for review."
msgstr ""
+msgid "Your search didn't match any commits."
+msgstr ""
+
msgid "Your subscription expired!"
msgstr ""
msgid "Your subscription has been downgraded"
msgstr ""
+msgid "Your subscription will automatically renew in %{remaining_days}"
+msgstr ""
+
msgid "Your subscription will expire in %{remaining_days}"
msgstr ""
@@ -24055,7 +24599,7 @@ msgstr ""
msgid "allowed to fail"
msgstr ""
-msgid "already being used for another group or project milestone."
+msgid "already being used for another group or project %{timebox_name}."
msgstr ""
msgid "already has a \"created\" issue link"
@@ -24133,6 +24677,9 @@ msgstr ""
msgid "ciReport|%{linkStartTag}Learn more about SAST %{linkEndTag}"
msgstr ""
+msgid "ciReport|%{linkStartTag}Learn more about Secret Scanning %{linkEndTag}"
+msgstr ""
+
msgid "ciReport|%{linkStartTag}Learn more about codequality reports %{linkEndTag}"
msgstr ""
@@ -24296,6 +24843,12 @@ msgstr ""
msgid "ciReport|SAST"
msgstr ""
+msgid "ciReport|Secret scanning"
+msgstr ""
+
+msgid "ciReport|Secret scanning detects secrets and credentials vulnerabilities in your source code."
+msgstr ""
+
msgid "ciReport|Security scanning"
msgstr ""
@@ -24493,6 +25046,9 @@ msgstr ""
msgid "group"
msgstr ""
+msgid "groups"
+msgstr ""
+
msgid "has already been linked to another vulnerability"
msgstr ""
@@ -24656,9 +25212,6 @@ msgstr[1] ""
msgid "merged %{time_ago}"
msgstr ""
-msgid "milestone should belong either to a project or a group."
-msgstr ""
-
msgid "missing"
msgstr ""
@@ -24995,6 +25548,9 @@ msgstr ""
msgid "no contributions"
msgstr ""
+msgid "no expiration"
+msgstr ""
+
msgid "no one can merge"
msgstr ""
@@ -25047,6 +25603,9 @@ msgstr ""
msgid "pending removal"
msgstr ""
+msgid "per day"
+msgstr ""
+
msgid "pipeline"
msgstr ""
@@ -25075,6 +25634,12 @@ msgstr[1] ""
msgid "project avatar"
msgstr ""
+msgid "projects"
+msgstr ""
+
+msgid "push to your repository, create pipelines, create issues or add comments. To reduce storage capacity, delete unused repositories, artifacts, wikis, issues, and pipelines."
+msgstr ""
+
msgid "quick actions"
msgstr ""
@@ -25221,11 +25786,6 @@ msgstr ""
msgid "this document"
msgstr ""
-msgid "thread resolved"
-msgid_plural "threads resolved"
-msgstr[0] ""
-msgstr[1] ""
-
msgid "to help your contributors communicate effectively!"
msgstr ""
@@ -25280,6 +25840,9 @@ msgstr ""
msgid "view the blob"
msgstr ""
+msgid "vulnerability|Add a comment"
+msgstr ""
+
msgid "vulnerability|Add a comment or reason for dismissal"
msgstr ""
diff --git a/locale/hr_HR/gitlab.po b/locale/hr_HR/gitlab.po
index 164774ef390..31ef7dcfe58 100644
--- a/locale/hr_HR/gitlab.po
+++ b/locale/hr_HR/gitlab.po
@@ -12,7 +12,7 @@ msgstr ""
"X-Crowdin-Project: gitlab-ee\n"
"X-Crowdin-Language: hr\n"
"X-Crowdin-File: /master/locale/gitlab.pot\n"
-"PO-Revision-Date: 2020-04-15 00:30\n"
+"PO-Revision-Date: 2020-05-05 21:32\n"
msgid " %{start} to %{end}"
msgstr ""
@@ -242,6 +242,12 @@ msgstr[0] ""
msgstr[1] ""
msgstr[2] ""
+msgid "%d unresolved thread"
+msgid_plural "%d unresolved threads"
+msgstr[0] ""
+msgstr[1] ""
+msgstr[2] ""
+
msgid "%d vulnerability dismissed"
msgid_plural "%d vulnerabilities dismissed"
msgstr[0] ""
@@ -407,6 +413,9 @@ msgstr ""
msgid "%{mrText}, this issue will be closed automatically."
msgstr ""
+msgid "%{namespace_name} is now read-only. You cannot: %{base_message}"
+msgstr ""
+
msgid "%{name} contained %{resultsString}"
msgstr ""
@@ -419,6 +428,12 @@ msgstr ""
msgid "%{name}'s avatar"
msgstr ""
+msgid "%{no_of_days} day"
+msgid_plural "%{no_of_days} days"
+msgstr[0] ""
+msgstr[1] ""
+msgstr[2] ""
+
msgid "%{number_commits_behind} commits behind %{default_branch}, %{number_commits_ahead} commits ahead"
msgstr ""
@@ -548,6 +563,9 @@ msgstr[2] ""
msgid "%{text} is available"
msgstr ""
+msgid "%{timebox_name} should belong either to a project or a group."
+msgstr ""
+
msgid "%{title} %{operator} %{threshold}"
msgstr ""
@@ -842,12 +860,18 @@ msgstr ""
msgid "<code>\"johnsmith@example.com\": \"johnsmith@example.com\"</code> will add \"By <a href=\"#\">johnsmith@example.com</a>\" to all issues and comments originally created by johnsmith@example.com. By default, the email address or username is masked to ensure the user's privacy. Use this option if you want to show the full email address."
msgstr ""
+msgid "<namespace / project>"
+msgstr ""
+
msgid "<no name set>"
msgstr ""
msgid "<no scopes selected>"
msgstr ""
+msgid "<project name>"
+msgstr ""
+
msgid "<strong>%{group_name}</strong> group members"
msgstr ""
@@ -881,6 +905,9 @@ msgstr ""
msgid "A basic page and serverless function that uses AWS Lambda, AWS API Gateway, and GitLab Pages"
msgstr ""
+msgid "A complete DevOps platform"
+msgstr ""
+
msgid "A default branch cannot be chosen for an empty project."
msgstr ""
@@ -1097,6 +1124,12 @@ msgstr ""
msgid "AccessTokens|reset it"
msgstr ""
+msgid "AccessibilityReport|Accessibility report artifact not found"
+msgstr ""
+
+msgid "AccessibilityReport|Failed to retrieve accessibility report"
+msgstr ""
+
msgid "AccessibilityReport|Learn More"
msgstr ""
@@ -1106,6 +1139,9 @@ msgstr ""
msgid "AccessibilityReport|New"
msgstr ""
+msgid "AccessibilityReport|The accessibility scanning found an error of the following type: %{code}"
+msgstr ""
+
msgid "Account"
msgstr ""
@@ -1349,6 +1385,9 @@ msgstr ""
msgid "Added at"
msgstr ""
+msgid "Added for this merge request"
+msgstr ""
+
msgid "Added in this version"
msgstr ""
@@ -1721,6 +1760,72 @@ msgstr[0] ""
msgstr[1] ""
msgstr[2] ""
+msgid "Alert Details"
+msgstr ""
+
+msgid "AlertManagement|Acknowledged"
+msgstr ""
+
+msgid "AlertManagement|Alert"
+msgstr ""
+
+msgid "AlertManagement|Authorize external service"
+msgstr ""
+
+msgid "AlertManagement|Display alerts from all your monitoring tools directly within GitLab. Streamline the investigation of your alerts and the escalation of alerts to incidents."
+msgstr ""
+
+msgid "AlertManagement|End time"
+msgstr ""
+
+msgid "AlertManagement|End time:"
+msgstr ""
+
+msgid "AlertManagement|Events"
+msgstr ""
+
+msgid "AlertManagement|Events:"
+msgstr ""
+
+msgid "AlertManagement|Full Alert Details"
+msgstr ""
+
+msgid "AlertManagement|More information"
+msgstr ""
+
+msgid "AlertManagement|No alerts available to display. If you think you're seeing this message in error, refresh the page."
+msgstr ""
+
+msgid "AlertManagement|No alerts to display."
+msgstr ""
+
+msgid "AlertManagement|Overview"
+msgstr ""
+
+msgid "AlertManagement|Resolved"
+msgstr ""
+
+msgid "AlertManagement|Severity"
+msgstr ""
+
+msgid "AlertManagement|Start time"
+msgstr ""
+
+msgid "AlertManagement|Start time:"
+msgstr ""
+
+msgid "AlertManagement|Status"
+msgstr ""
+
+msgid "AlertManagement|Surface alerts in GitLab"
+msgstr ""
+
+msgid "AlertManagement|There was an error displaying the alerts. Confirm your endpoint's configuration details to ensure alerts appear."
+msgstr ""
+
+msgid "AlertManagement|Triggered"
+msgstr ""
+
msgid "AlertService|%{linkStart}Learn more%{linkEnd} about configuring this endpoint to receive alerts."
msgstr ""
@@ -1790,6 +1895,9 @@ msgstr ""
msgid "All security scans are enabled because %{linkStart}Auto DevOps%{linkEnd} is enabled on this project"
msgstr ""
+msgid "All threads resolved"
+msgstr ""
+
msgid "All users"
msgstr ""
@@ -1808,6 +1916,9 @@ msgstr ""
msgid "Allow only the selected protocols to be used for Git access."
msgstr ""
+msgid "Allow owners to manage default branch protection per group"
+msgstr ""
+
msgid "Allow owners to manually add users outside of LDAP"
msgstr ""
@@ -1916,6 +2027,9 @@ msgstr ""
msgid "An error occurred fetching the dropdown data."
msgstr ""
+msgid "An error occurred fetching the project authors."
+msgstr ""
+
msgid "An error occurred previewing the blob"
msgstr ""
@@ -1997,6 +2111,9 @@ msgstr ""
msgid "An error occurred while fetching sidebar data"
msgstr ""
+msgid "An error occurred while fetching terraform reports."
+msgstr ""
+
msgid "An error occurred while fetching the Service Desk address."
msgstr ""
@@ -2234,6 +2351,9 @@ msgstr ""
msgid "Any"
msgstr ""
+msgid "Any Author"
+msgstr ""
+
msgid "Any Label"
msgstr ""
@@ -2453,10 +2573,10 @@ msgstr ""
msgid "Are you sure that you want to unarchive this project?"
msgstr ""
-msgid "Are you sure you want to cancel creating this comment?"
+msgid "Are you sure you want to cancel editing this comment?"
msgstr ""
-msgid "Are you sure you want to cancel editing this comment?"
+msgid "Are you sure you want to close this blocked issue?"
msgstr ""
msgid "Are you sure you want to delete %{name}?"
@@ -2486,6 +2606,9 @@ msgstr ""
msgid "Are you sure you want to deploy this environment?"
msgstr ""
+msgid "Are you sure you want to discard this comment?"
+msgstr ""
+
msgid "Are you sure you want to erase this build?"
msgstr ""
@@ -2663,6 +2786,9 @@ msgstr ""
msgid "At least one approval from a code owner is required to change files matching the respective CODEOWNER rules."
msgstr ""
+msgid "At least one logging option is required to be enabled"
+msgstr ""
+
msgid "At least one of group_id or project_id must be specified"
msgstr ""
@@ -2702,6 +2828,27 @@ msgstr ""
msgid "AuditEvents|Target"
msgstr ""
+msgid "AuditLogs|(removed)"
+msgstr ""
+
+msgid "AuditLogs|Action"
+msgstr ""
+
+msgid "AuditLogs|Author"
+msgstr ""
+
+msgid "AuditLogs|Date"
+msgstr ""
+
+msgid "AuditLogs|IP Address"
+msgstr ""
+
+msgid "AuditLogs|Object"
+msgstr ""
+
+msgid "AuditLogs|Target"
+msgstr ""
+
msgid "Aug"
msgstr ""
@@ -2984,9 +3131,6 @@ msgstr ""
msgid "BambooService|You must set up automatic revision labeling and a repository trigger in Bamboo."
msgstr ""
-msgid "Batch operations"
-msgstr ""
-
msgid "BatchComments|Delete all pending comments"
msgstr ""
@@ -3116,6 +3260,9 @@ msgstr ""
msgid "Boards"
msgstr ""
+msgid "Boards and Board Lists"
+msgstr ""
+
msgid "Boards|Collapse"
msgstr ""
@@ -3335,6 +3482,9 @@ msgstr ""
msgid "Buy GitLab Enterprise Edition"
msgstr ""
+msgid "Buy more Pipeline minutes"
+msgstr ""
+
msgid "By %{user_name}"
msgstr ""
@@ -3455,6 +3605,9 @@ msgstr ""
msgid "Can't scan the code?"
msgstr ""
+msgid "Can't update snippet: %{err}"
+msgstr ""
+
msgid "Canary"
msgstr ""
@@ -3623,6 +3776,9 @@ msgstr ""
msgid "Changing group path can have unintended side effects."
msgstr ""
+msgid "Charts"
+msgstr ""
+
msgid "Charts can't be displayed as the request for data has timed out. %{documentationLink}"
msgstr ""
@@ -3659,9 +3815,6 @@ msgstr ""
msgid "ChatMessage|and [%{count} more](%{pipeline_failed_jobs_url})"
msgstr ""
-msgid "ChatMessage|failed"
-msgstr ""
-
msgid "ChatMessage|has failed"
msgstr ""
@@ -3677,9 +3830,6 @@ msgstr ""
msgid "ChatMessage|in %{project_link}"
msgstr ""
-msgid "ChatMessage|passed"
-msgstr ""
-
msgid "Check again"
msgstr ""
@@ -4130,9 +4280,6 @@ msgstr ""
msgid "Click any <strong>project name</strong> in the project list below to navigate to the project milestone."
msgstr ""
-msgid "Click here"
-msgstr ""
-
msgid "Click the <strong>Download</strong> button and wait for downloading to complete."
msgstr ""
@@ -4520,6 +4667,12 @@ msgstr ""
msgid "ClusterIntegration|Fetching zones"
msgstr ""
+msgid "ClusterIntegration|Fluentd"
+msgstr ""
+
+msgid "ClusterIntegration|Fluentd is an open source data collector, which lets you unify the data collection and consumption for a better use and understanding of data. It requires at least one of the following logs to be successfully installed."
+msgstr ""
+
msgid "ClusterIntegration|GitLab Integration"
msgstr ""
@@ -4826,6 +4979,15 @@ msgstr ""
msgid "ClusterIntegration|Request to begin uninstalling failed"
msgstr ""
+msgid "ClusterIntegration|SIEM Hostname"
+msgstr ""
+
+msgid "ClusterIntegration|SIEM Port"
+msgstr ""
+
+msgid "ClusterIntegration|SIEM Protocol"
+msgstr ""
+
msgid "ClusterIntegration|Save changes"
msgstr ""
@@ -4922,6 +5084,12 @@ msgstr ""
msgid "ClusterIntegration|Select zone to choose machine type"
msgstr ""
+msgid "ClusterIntegration|Send Cilium Logs"
+msgstr ""
+
+msgid "ClusterIntegration|Send ModSecurity Logs"
+msgstr ""
+
msgid "ClusterIntegration|Service Token"
msgstr ""
@@ -5351,18 +5519,33 @@ msgstr ""
msgid "Compliance framework (optional)"
msgstr ""
+msgid "ComplianceFramework|GDPR"
+msgstr ""
+
msgid "ComplianceFramework|GDPR - General Data Protection Regulation"
msgstr ""
+msgid "ComplianceFramework|HIPAA"
+msgstr ""
+
msgid "ComplianceFramework|HIPAA - Health Insurance Portability and Accountability Act"
msgstr ""
+msgid "ComplianceFramework|PCI-DSS"
+msgstr ""
+
msgid "ComplianceFramework|PCI-DSS - Payment Card Industry-Data Security Standard"
msgstr ""
+msgid "ComplianceFramework|SOC 2"
+msgstr ""
+
msgid "ComplianceFramework|SOC 2 - Service Organization Control 2"
msgstr ""
+msgid "ComplianceFramework|SOX"
+msgstr ""
+
msgid "ComplianceFramework|SOX - Sarbanes-Oxley"
msgstr ""
@@ -5510,9 +5693,15 @@ msgstr ""
msgid "Container repositories sync capacity"
msgstr ""
+msgid "ContainerRegistry| Please visit the %{linkStart}administration settings%{linkEnd} to enable this feature."
+msgstr ""
+
msgid "ContainerRegistry|%{imageName} tags"
msgstr ""
+msgid "ContainerRegistry|%{title} was successfully scheduled for deletion"
+msgstr ""
+
msgid "ContainerRegistry|Automatically remove extra images that aren't designed to be kept."
msgstr ""
@@ -5525,6 +5714,9 @@ msgstr ""
msgid "ContainerRegistry|Container Registry"
msgstr ""
+msgid "ContainerRegistry|Container Registry tag expiration and retention policy is disabled"
+msgstr ""
+
msgid "ContainerRegistry|Copy build command"
msgstr ""
@@ -5534,18 +5726,12 @@ msgstr ""
msgid "ContainerRegistry|Copy push command"
msgstr ""
-msgid "ContainerRegistry|Currently, the Container Registry tag expiration feature is not available for projects created before GitLab version 12.8. For updates and more information, visit Issue %{linkStart}#196124%{linkEnd}"
-msgstr ""
-
msgid "ContainerRegistry|Docker connection error"
msgstr ""
msgid "ContainerRegistry|Docker tag expiration policy is %{toggleStatus}"
msgstr ""
-msgid "ContainerRegistry|Docker tags with names matching this regex pattern will expire:"
-msgstr ""
-
msgid "ContainerRegistry|Edit Settings"
msgstr ""
@@ -5567,9 +5753,6 @@ msgstr ""
msgid "ContainerRegistry|Image ID"
msgstr ""
-msgid "ContainerRegistry|Image deleted successfully"
-msgstr ""
-
msgid "ContainerRegistry|Keep and protect the images that matter most."
msgstr ""
@@ -5585,12 +5768,24 @@ msgstr ""
msgid "ContainerRegistry|Number of tags to retain:"
msgstr ""
+msgid "ContainerRegistry|Please contact your administrator."
+msgstr ""
+
msgid "ContainerRegistry|Push an image"
msgstr ""
msgid "ContainerRegistry|Quick Start"
msgstr ""
+msgid "ContainerRegistry|Regular expressions such as %{codeStart}.*-test%{codeEnd} or %{codeStart}dev-.*%{codeEnd} are supported"
+msgstr ""
+
+msgid "ContainerRegistry|Regular expressions such as %{codeStart}.*-test%{codeEnd} or %{codeStart}dev-.*%{codeEnd} are supported. To select all tags, use %{codeStart}.*%{codeEnd}"
+msgstr ""
+
+msgid "ContainerRegistry|Remember to run %{docLinkStart}garbage collection%{docLinkEnd} to remove the stale data from storage."
+msgstr ""
+
msgid "ContainerRegistry|Remove repository"
msgstr ""
@@ -5606,22 +5801,22 @@ msgstr[2] ""
msgid "ContainerRegistry|Retention policy has been Enabled"
msgstr ""
-msgid "ContainerRegistry|Something went wrong while deleting the image."
+msgid "ContainerRegistry|Something went wrong while fetching the expiration policy."
msgstr ""
-msgid "ContainerRegistry|Something went wrong while deleting the tag."
+msgid "ContainerRegistry|Something went wrong while fetching the repository list."
msgstr ""
-msgid "ContainerRegistry|Something went wrong while deleting the tags."
+msgid "ContainerRegistry|Something went wrong while fetching the tags list."
msgstr ""
-msgid "ContainerRegistry|Something went wrong while fetching the expiration policy."
+msgid "ContainerRegistry|Something went wrong while marking the tag for deletion."
msgstr ""
-msgid "ContainerRegistry|Something went wrong while fetching the packages list."
+msgid "ContainerRegistry|Something went wrong while marking the tags for deletion."
msgstr ""
-msgid "ContainerRegistry|Something went wrong while fetching the tags list."
+msgid "ContainerRegistry|Something went wrong while scheduling %{title} for deletion. Please try again."
msgstr ""
msgid "ContainerRegistry|Something went wrong while updating the expiration policy."
@@ -5630,16 +5825,25 @@ msgstr ""
msgid "ContainerRegistry|Tag"
msgstr ""
-msgid "ContainerRegistry|Tag deleted successfully"
-msgstr ""
-
msgid "ContainerRegistry|Tag expiration policy"
msgstr ""
msgid "ContainerRegistry|Tag expiration policy is designed to:"
msgstr ""
-msgid "ContainerRegistry|Tags deleted successfully"
+msgid "ContainerRegistry|Tag successfully marked for deletion."
+msgstr ""
+
+msgid "ContainerRegistry|Tags successfully marked for deletion."
+msgstr ""
+
+msgid "ContainerRegistry|Tags with names matching this regex pattern will %{italicStart}be preserved:%{italicEnd}"
+msgstr ""
+
+msgid "ContainerRegistry|Tags with names matching this regex pattern will %{italicStart}expire:%{italicEnd}"
+msgstr ""
+
+msgid "ContainerRegistry|The Container Registry tag expiration and retention policies for this project have not been enabled."
msgstr ""
msgid "ContainerRegistry|The last tag related to this image was recently removed. This empty image and any associated data will be automatically removed as part of the regular Garbage Collection process. If you have any questions, contact your administrator."
@@ -5660,16 +5864,16 @@ msgstr ""
msgid "ContainerRegistry|There are no container images stored for this project"
msgstr ""
-msgid "ContainerRegistry|This Registry contains deleted image tag data. Remember to run %{docLinkStart}garbage collection%{docLinkEnd} to remove the stale data from storage."
+msgid "ContainerRegistry|There was an error during the deletion of this image repository, please try again."
msgstr ""
msgid "ContainerRegistry|This image has no active tags"
msgstr ""
-msgid "ContainerRegistry|We are having trouble connecting to Docker, which could be due to an issue with your project name or path. %{docLinkStart}More Information%{docLinkEnd}"
+msgid "ContainerRegistry|This image repository is scheduled for deletion"
msgstr ""
-msgid "ContainerRegistry|Wildcards such as %{codeStart}.*-stable%{codeEnd} or %{codeStart}production/.*%{codeEnd} are supported. To select all tags, use %{codeStart}.*%{codeEnd}"
+msgid "ContainerRegistry|We are having trouble connecting to Docker, which could be due to an issue with your project name or path. %{docLinkStart}More Information%{docLinkEnd}"
msgstr ""
msgid "ContainerRegistry|With the Container Registry, every project can have its own space to store its Docker images. %{docLinkStart}More Information%{docLinkEnd}"
@@ -5924,7 +6128,7 @@ msgstr ""
msgid "Could not delete chat nickname %{chat_name}."
msgstr ""
-msgid "Could not find design"
+msgid "Could not find design."
msgstr ""
msgid "Could not remove the trigger."
@@ -5939,6 +6143,9 @@ msgstr ""
msgid "Could not revoke personal access token %{personal_access_token_name}."
msgstr ""
+msgid "Could not revoke project access token %{project_access_token_name}."
+msgstr ""
+
msgid "Could not save group ID"
msgstr ""
@@ -5996,6 +6203,9 @@ msgstr ""
msgid "Create a new branch"
msgstr ""
+msgid "Create a new deploy key for this project"
+msgstr ""
+
msgid "Create a new file as there are no files yet. Afterwards, you'll be able to commit your changes."
msgstr ""
@@ -6101,6 +6311,9 @@ msgstr ""
msgid "Create requirement"
msgstr ""
+msgid "Create snippet"
+msgstr ""
+
msgid "Create wildcard: %{searchTerm}"
msgstr ""
@@ -6122,6 +6335,9 @@ msgstr ""
msgid "Created"
msgstr ""
+msgid "Created %{timestamp}"
+msgstr ""
+
msgid "Created At"
msgstr ""
@@ -6479,6 +6695,9 @@ msgstr ""
msgid "CycleAnalytics|stage dropdown"
msgstr ""
+msgid "DAG"
+msgstr ""
+
msgid "DNS"
msgstr ""
@@ -6851,6 +7070,9 @@ msgstr ""
msgid "Deploy key was successfully updated."
msgstr ""
+msgid "Deploy keys allow read-only or read-write (if enabled) access to your repository. Deploy keys can be used for CI, staging or production servers. You can create a deploy key or add an existing one."
+msgstr ""
+
msgid "Deploy progress not found. To see pods, ensure your environment matches %{linkStart}deploy board criteria%{linkEnd}."
msgstr ""
@@ -7004,6 +7226,9 @@ msgstr ""
msgid "Deploying to"
msgstr ""
+msgid "Deployment Frequency"
+msgstr ""
+
msgid "Deployment|API"
msgstr ""
@@ -7523,9 +7748,6 @@ msgstr ""
msgid "Edit environment"
msgstr ""
-msgid "Edit epic description"
-msgstr ""
-
msgid "Edit file"
msgstr ""
@@ -8147,9 +8369,6 @@ msgstr ""
msgid "Epics"
msgstr ""
-msgid "Epics (Ultimate / Gold license only)"
-msgstr ""
-
msgid "Epics Roadmap"
msgstr ""
@@ -8159,6 +8378,9 @@ msgstr ""
msgid "Epics let you manage your portfolio of projects more efficiently and with less effort"
msgstr ""
+msgid "Epics, Issues, and Merge Requests"
+msgstr ""
+
msgid "Epics|Add an epic"
msgstr ""
@@ -8210,6 +8432,9 @@ msgstr ""
msgid "Epics|Something went wrong while fetching group epics."
msgstr ""
+msgid "Epics|Something went wrong while moving item."
+msgstr ""
+
msgid "Epics|Something went wrong while ordering item."
msgstr ""
@@ -8264,6 +8489,9 @@ msgstr ""
msgid "Error fetching network graph."
msgstr ""
+msgid "Error fetching payload data."
+msgstr ""
+
msgid "Error fetching projects"
msgstr ""
@@ -8273,9 +8501,6 @@ msgstr ""
msgid "Error fetching the dependency list. Please check your network connection and try again."
msgstr ""
-msgid "Error fetching usage ping data."
-msgstr ""
-
msgid "Error loading branch data. Please try again."
msgstr ""
@@ -8417,6 +8642,9 @@ msgstr ""
msgid "Errors"
msgstr ""
+msgid "Errors:"
+msgstr ""
+
msgid "Estimated"
msgstr ""
@@ -8621,12 +8849,18 @@ msgstr ""
msgid "Export as CSV"
msgstr ""
+msgid "Export group"
+msgstr ""
+
msgid "Export issues"
msgstr ""
msgid "Export project"
msgstr ""
+msgid "Export this group with all related data to a new GitLab instance. Once complete, you can import the data file from the \"New Group\" page."
+msgstr ""
+
msgid "Export this project with all its related data in order to move your project to a new GitLab instance. Once the export is finished, you can import the file from the \"New Project\" page."
msgstr ""
@@ -8696,9 +8930,6 @@ msgstr ""
msgid "Failed Jobs"
msgstr ""
-msgid "Failed create wiki"
-msgstr ""
-
msgid "Failed to add a Zoom meeting"
msgstr ""
@@ -8738,6 +8969,9 @@ msgstr ""
msgid "Failed to create resources"
msgstr ""
+msgid "Failed to create wiki"
+msgstr ""
+
msgid "Failed to delete board. Please try again."
msgstr ""
@@ -8747,7 +8981,7 @@ msgstr ""
msgid "Failed to enqueue the rebase operation, possibly due to a long-lived transaction. Try again later."
msgstr ""
-msgid "Failed to find import label for jira import."
+msgid "Failed to find import label for Jira import."
msgstr ""
msgid "Failed to get ref."
@@ -8771,6 +9005,12 @@ msgstr ""
msgid "Failed to load groups & users."
msgstr ""
+msgid "Failed to load labels. Please try again."
+msgstr ""
+
+msgid "Failed to load milestones. Please try again."
+msgstr ""
+
msgid "Failed to load related branches"
msgstr ""
@@ -9146,7 +9386,10 @@ msgstr ""
msgid "Filter by milestone name"
msgstr ""
-msgid "Filter by name..."
+msgid "Filter by name"
+msgstr ""
+
+msgid "Filter by status"
msgstr ""
msgid "Filter by two-factor authentication"
@@ -9158,12 +9401,18 @@ msgstr ""
msgid "Filter projects"
msgstr ""
+msgid "Filter results"
+msgstr ""
+
msgid "Filter results by group"
msgstr ""
msgid "Filter results by project"
msgstr ""
+msgid "Filter results..."
+msgstr ""
+
msgid "Filter your projects by name"
msgstr ""
@@ -9452,10 +9701,10 @@ msgstr ""
msgid "GeoNodes|Checksummed"
msgstr ""
-msgid "GeoNodes|Container repositories"
+msgid "GeoNodes|Consult Geo troubleshooting information"
msgstr ""
-msgid "GeoNodes|Data is out of date from %{timeago}"
+msgid "GeoNodes|Container repositories"
msgstr ""
msgid "GeoNodes|Data replication lag"
@@ -9497,6 +9746,9 @@ msgstr ""
msgid "GeoNodes|Last event ID seen from primary"
msgstr ""
+msgid "GeoNodes|Learn more about Geo node statuses"
+msgstr ""
+
msgid "GeoNodes|Loading nodes"
msgstr ""
@@ -9512,6 +9764,9 @@ msgstr ""
msgid "GeoNodes|Node was successfully removed."
msgstr ""
+msgid "GeoNodes|Node's status was updated %{timeAgo}."
+msgstr ""
+
msgid "GeoNodes|Not checksummed"
msgstr ""
@@ -9542,7 +9797,10 @@ msgstr ""
msgid "GeoNodes|Repository verification progress"
msgstr ""
-msgid "GeoNodes|Selective"
+msgid "GeoNodes|Selective (%{syncLabel})"
+msgstr ""
+
+msgid "GeoNodes|Selective synchronization"
msgstr ""
msgid "GeoNodes|Something went wrong while changing node status"
@@ -9569,6 +9827,9 @@ msgstr ""
msgid "GeoNodes|Unverified"
msgstr ""
+msgid "GeoNodes|Updated %{timeAgo}"
+msgstr ""
+
msgid "GeoNodes|Used slots"
msgstr ""
@@ -9608,16 +9869,16 @@ msgstr ""
msgid "Geo|All"
msgstr ""
-msgid "Geo|All projects"
+msgid "Geo|All %{replicable_type}"
msgstr ""
-msgid "Geo|All projects are being scheduled for re-sync"
+msgid "Geo|All projects"
msgstr ""
-msgid "Geo|All projects are being scheduled for re-verify"
+msgid "Geo|All projects are being scheduled for resync"
msgstr ""
-msgid "Geo|Batch operations"
+msgid "Geo|All projects are being scheduled for reverify"
msgstr ""
msgid "Geo|Could not remove tracking entry for an existing project."
@@ -9629,9 +9890,15 @@ msgstr ""
msgid "Geo|Failed"
msgstr ""
+msgid "Geo|Filter by status"
+msgstr ""
+
msgid "Geo|Geo Status"
msgstr ""
+msgid "Geo|In progress"
+msgstr ""
+
msgid "Geo|In sync"
msgstr ""
@@ -9656,9 +9923,6 @@ msgstr ""
msgid "Geo|Not synced yet"
msgstr ""
-msgid "Geo|Pending"
-msgstr ""
-
msgid "Geo|Pending synchronization"
msgstr ""
@@ -9695,7 +9959,7 @@ msgstr ""
msgid "Geo|Resync"
msgstr ""
-msgid "Geo|Resync all projects"
+msgid "Geo|Resync all"
msgstr ""
msgid "Geo|Retry count"
@@ -9704,7 +9968,7 @@ msgstr ""
msgid "Geo|Reverify"
msgstr ""
-msgid "Geo|Reverify all projects"
+msgid "Geo|Reverify all"
msgstr ""
msgid "Geo|Status"
@@ -10046,9 +10310,6 @@ msgstr ""
msgid "Go to file"
msgstr ""
-msgid "Go to file (MRs only)"
-msgstr ""
-
msgid "Go to file permalink (while viewing a file)"
msgstr ""
@@ -10493,6 +10754,9 @@ msgstr ""
msgid "GroupSettings|Disable group mentions"
msgstr ""
+msgid "GroupSettings|Export group"
+msgstr ""
+
msgid "GroupSettings|If the parent group's visibility is lower than the group current visibility, visibility levels for subgroups and projects will be changed to match the new parent group's visibility."
msgstr ""
@@ -10571,6 +10835,9 @@ msgstr ""
msgid "Groups (%{count})"
msgstr ""
+msgid "Groups (%{groups})"
+msgstr ""
+
msgid "Groups can also be nested by creating %{subgroup_docs_link_start}subgroups%{subgroup_docs_link_end}."
msgstr ""
@@ -10838,15 +11105,12 @@ msgstr ""
msgid "ID:"
msgstr ""
-msgid "IDE|Allow live previews of JavaScript projects in the Web IDE using CodeSandbox client side evaluation."
+msgid "IDE|Allow live previews of JavaScript projects in the Web IDE using CodeSandbox Live Preview."
msgstr ""
msgid "IDE|Back"
msgstr ""
-msgid "IDE|Client side evaluation"
-msgstr ""
-
msgid "IDE|Commit"
msgstr ""
@@ -10910,6 +11174,9 @@ msgstr ""
msgid "If any job surpasses this timeout threshold, it will be marked as failed. Human readable time input language is accepted like \"1 hour\". Values without specification represent seconds."
msgstr ""
+msgid "If blank, set allowable lifetime to %{instance_level_policy_in_words}, as defined by the instance admin. Once set, existing tokens for users in this group may be revoked."
+msgstr ""
+
msgid "If checked, group owners can manage LDAP group links and LDAP member overrides"
msgstr ""
@@ -10949,6 +11216,9 @@ msgstr ""
msgid "If you lose your recovery codes you can generate new ones, invalidating all previous codes."
msgstr ""
+msgid "If you reach 100%% storage capacity, you will not be able to: %{base_message}"
+msgstr ""
+
msgid "If your HTTP repository is not publicly accessible, add your credentials."
msgstr ""
@@ -11060,6 +11330,9 @@ msgstr ""
msgid "Import repository"
msgstr ""
+msgid "Import started by: %{importInitiator}"
+msgstr ""
+
msgid "Import tasks"
msgstr ""
@@ -11126,6 +11399,9 @@ msgstr ""
msgid "In order to tailor your experience with GitLab we<br>would like to know a bit more about you."
msgstr ""
+msgid "In progress"
+msgstr ""
+
msgid "In the next step, you'll be able to select the projects you want to import."
msgstr ""
@@ -11474,15 +11750,15 @@ msgstr ""
msgid "Issues"
msgstr ""
-msgid "Issues / Merge Requests"
-msgstr ""
-
msgid "Issues Analytics"
msgstr ""
msgid "Issues Rate Limits"
msgstr ""
+msgid "Issues and Merge Requests"
+msgstr ""
+
msgid "Issues can be bugs, tasks or ideas to be discussed. Also, issues are searchable and filterable."
msgstr ""
@@ -11504,10 +11780,10 @@ msgstr ""
msgid "IssuesAnalytics|Avg/Month:"
msgstr ""
-msgid "IssuesAnalytics|Issues created"
+msgid "IssuesAnalytics|Issues opened"
msgstr ""
-msgid "IssuesAnalytics|Issues created per month"
+msgid "IssuesAnalytics|Issues opened per month"
msgstr ""
msgid "IssuesAnalytics|Last 12 months"
@@ -11561,6 +11837,9 @@ msgstr ""
msgid "Jira integration not configured."
msgstr ""
+msgid "Jira project: %{importProject}"
+msgstr ""
+
msgid "JiraService|Events for %{noteable_model_name} are disabled."
msgstr ""
@@ -11951,6 +12230,9 @@ msgstr ""
msgid "Lead"
msgstr ""
+msgid "Lead Time"
+msgstr ""
+
msgid "Learn GitLab"
msgstr ""
@@ -11963,6 +12245,9 @@ msgstr ""
msgid "Learn how to %{no_packages_link_start}publish and share your packages%{no_packages_link_end} with GitLab."
msgstr ""
+msgid "Learn how to enable synchronization"
+msgstr ""
+
msgid "Learn more"
msgstr ""
@@ -12299,9 +12584,6 @@ msgstr ""
msgid "Live preview"
msgstr ""
-msgid "Load more vulnerabilities"
-msgstr ""
-
msgid "Loading"
msgstr ""
@@ -12452,9 +12734,6 @@ msgstr ""
msgid "Manage"
msgstr ""
-msgid "Manage Git repositories with fine-grained access controls that keep your code secure. Perform code reviews and enhance collaboration with merge requests. Each project can also have an issue tracker and a wiki."
-msgstr ""
-
msgid "Manage Web IDE features"
msgstr ""
@@ -12485,6 +12764,9 @@ msgstr ""
msgid "Manage your license"
msgstr ""
+msgid "Managed Account"
+msgstr ""
+
msgid "Manifest"
msgstr ""
@@ -12749,6 +13031,9 @@ msgstr ""
msgid "Merge Requests in Review"
msgstr ""
+msgid "Merge automatically (%{strategy})"
+msgstr ""
+
msgid "Merge commit message"
msgstr ""
@@ -12902,6 +13187,12 @@ msgstr ""
msgid "Merged branches are being deleted. This can take some time depending on the number of branches. Please refresh the page to see changes."
msgstr ""
+msgid "Merged this merge request."
+msgstr ""
+
+msgid "Merges this merge request immediately."
+msgstr ""
+
msgid "Merges this merge request when the pipeline succeeds."
msgstr ""
@@ -13010,12 +13301,15 @@ msgstr[0] ""
msgstr[1] ""
msgstr[2] ""
-msgid "Metrics|Environment"
+msgid "Metrics|Expand panel"
msgstr ""
msgid "Metrics|For grouping similar metrics"
msgstr ""
+msgid "Metrics|Go back (Esc)"
+msgstr ""
+
msgid "Metrics|Invalid time range, please verify."
msgstr ""
@@ -13046,9 +13340,6 @@ msgstr ""
msgid "Metrics|Refresh dashboard"
msgstr ""
-msgid "Metrics|Show last"
-msgstr ""
-
msgid "Metrics|There was an error creating the dashboard."
msgstr ""
@@ -13127,6 +13418,9 @@ msgstr ""
msgid "Microsoft Azure"
msgstr ""
+msgid "Middleman project with Static Site Editor support"
+msgstr ""
+
msgid "Migrated %{success_count}/%{total_count} files."
msgstr ""
@@ -13574,10 +13868,10 @@ msgstr ""
msgid "Next"
msgstr ""
-msgid "Next file in diff (MRs only)"
+msgid "Next file in diff"
msgstr ""
-msgid "Next unresolved discussion (MRs only)"
+msgid "Next unresolved discussion"
msgstr ""
msgid "Nickname"
@@ -13637,6 +13931,9 @@ msgstr ""
msgid "No changes between %{ref_start}%{source_branch}%{ref_end} and %{ref_start}%{target_branch}%{ref_end}"
msgstr ""
+msgid "No child epics match applied filters"
+msgstr ""
+
msgid "No connection could be made to a Gitaly Server, please check your logs!"
msgstr ""
@@ -13775,15 +14072,6 @@ msgstr ""
msgid "No thanks, don't show this again"
msgstr ""
-msgid "No vulnerabilities found for this group"
-msgstr ""
-
-msgid "No vulnerabilities found for this pipeline"
-msgstr ""
-
-msgid "No vulnerabilities found for this project"
-msgstr ""
-
msgid "No vulnerabilities present"
msgstr ""
@@ -13907,6 +14195,9 @@ msgstr ""
msgid "Nothing to preview."
msgstr ""
+msgid "Nothing to synchronize"
+msgstr ""
+
msgid "Notification events"
msgstr ""
@@ -14078,6 +14369,9 @@ msgstr ""
msgid "Once removed, the fork relationship cannot be restored and you will no longer be able to send merge requests to the source."
msgstr ""
+msgid "Once the exported file is ready you can download it from this page."
+msgstr ""
+
msgid "Once the exported file is ready, you will receive a notification email with a download link, or you can download it from this page."
msgstr ""
@@ -14180,9 +14474,6 @@ msgstr ""
msgid "Open sidebar"
msgstr ""
-msgid "Open source software to collaborate on code"
-msgstr ""
-
msgid "Open: %{openIssuesCount}"
msgstr ""
@@ -14285,6 +14576,9 @@ msgstr ""
msgid "OutdatedBrowser|You can provide feedback %{feedback_link_start}on this issue%{feedback_link_end} or via your usual support channels."
msgstr ""
+msgid "Overridden"
+msgstr ""
+
msgid "Overview"
msgstr ""
@@ -14336,6 +14630,9 @@ msgstr ""
msgid "PackageRegistry|Conan Command"
msgstr ""
+msgid "PackageRegistry|Copy .pypirc content"
+msgstr ""
+
msgid "PackageRegistry|Copy Conan Command"
msgstr ""
@@ -14357,6 +14654,9 @@ msgstr ""
msgid "PackageRegistry|Copy NuGet Setup Command"
msgstr ""
+msgid "PackageRegistry|Copy Pip command"
+msgstr ""
+
msgid "PackageRegistry|Copy and paste this inside your %{codeStart}pom.xml%{codeEnd} %{codeStart}dependencies%{codeEnd} block."
msgstr ""
@@ -14390,6 +14690,12 @@ msgstr ""
msgid "PackageRegistry|For more information on the NuGet registry, %{linkStart}see the documentation%{linkEnd}."
msgstr ""
+msgid "PackageRegistry|For more information on the PyPi registry, %{linkStart}see the documentation%{linkEnd}."
+msgstr ""
+
+msgid "PackageRegistry|If you haven't already done so, you will need to add the below to your %{codeStart}.pypirc%{codeEnd} file."
+msgstr ""
+
msgid "PackageRegistry|If you haven't already done so, you will need to add the below to your %{codeStart}pom.xml%{codeEnd} file."
msgstr ""
@@ -14399,6 +14705,9 @@ msgstr ""
msgid "PackageRegistry|Learn how to %{noPackagesLinkStart}publish and share your packages%{noPackagesLinkEnd} with GitLab."
msgstr ""
+msgid "PackageRegistry|Manually Published"
+msgstr ""
+
msgid "PackageRegistry|Maven"
msgstr ""
@@ -14417,12 +14726,18 @@ msgstr ""
msgid "PackageRegistry|NuGet Command"
msgstr ""
+msgid "PackageRegistry|Pip Command"
+msgstr ""
+
msgid "PackageRegistry|Pipeline %{linkStart}%{linkEnd} triggered %{timestamp} by %{author}"
msgstr ""
msgid "PackageRegistry|Published to the repository at %{timestamp}"
msgstr ""
+msgid "PackageRegistry|PyPi"
+msgstr ""
+
msgid "PackageRegistry|Registry Setup"
msgstr ""
@@ -14459,6 +14774,9 @@ msgstr ""
msgid "PackageRegistry|npm"
msgstr ""
+msgid "PackageRegistry|published by %{author}"
+msgstr ""
+
msgid "PackageRegistry|yarn"
msgstr ""
@@ -14474,6 +14792,9 @@ msgstr ""
msgid "PackageType|NuGet"
msgstr ""
+msgid "PackageType|PyPi"
+msgstr ""
+
msgid "Packages"
msgstr ""
@@ -14567,6 +14888,9 @@ msgstr ""
msgid "Past due"
msgstr ""
+msgid "Paste a machine public key here. Read more about how to generate it"
+msgstr ""
+
msgid "Paste a machine public key here. Read more about how to generate it %{link_start}here%{link_end}"
msgstr ""
@@ -14582,9 +14906,6 @@ msgstr ""
msgid "Path"
msgstr ""
-msgid "Path, transfer, remove"
-msgstr ""
-
msgid "Path:"
msgstr ""
@@ -14615,7 +14936,7 @@ msgstr ""
msgid "Percentage"
msgstr ""
-msgid "Perform advanced options such as changing path, transferring, or removing the group."
+msgid "Perform advanced options such as changing path, transferring, exporting, or removing the group."
msgstr ""
msgid "Perform common operations on GitLab project"
@@ -14696,9 +15017,6 @@ msgstr ""
msgid "Pipeline minutes quota"
msgstr ""
-msgid "Pipeline quota"
-msgstr ""
-
msgid "Pipeline subscriptions"
msgstr ""
@@ -14708,6 +15026,9 @@ msgstr ""
msgid "Pipeline: %{status}"
msgstr ""
+msgid "PipelineCharts|CI / CD Analytics"
+msgstr ""
+
msgid "PipelineCharts|Failed:"
msgstr ""
@@ -14786,15 +15107,6 @@ msgstr ""
msgid "Pipelines settings for '%{project_name}' were successfully updated."
msgstr ""
-msgid "Pipelines| to purchase more minutes."
-msgstr ""
-
-msgid "Pipelines|%{namespace_name} has exceeded its pipeline minutes quota."
-msgstr ""
-
-msgid "Pipelines|%{namespace_name} has less than %{notification_level}%% of CI minutes available."
-msgstr ""
-
msgid "Pipelines|API"
msgstr ""
@@ -14816,10 +15128,13 @@ msgstr ""
msgid "Pipelines|Get started with Pipelines"
msgstr ""
-msgid "Pipelines|Loading Pipelines"
+msgid "Pipelines|Group %{namespace_name} has %{percentage}%% or less Shared Runner Pipeline minutes remaining. Once it runs out, no new jobs or pipelines in its projects will run."
msgstr ""
-msgid "Pipelines|Pipelines will not run anymore on shared Runners."
+msgid "Pipelines|Group %{namespace_name} has exceeded its pipeline minutes quota. Unless you buy additional pipeline minutes, no new jobs or pipelines in its projects will run."
+msgstr ""
+
+msgid "Pipelines|Loading Pipelines"
msgstr ""
msgid "Pipelines|Project cache successfully reset."
@@ -15050,6 +15365,9 @@ msgstr ""
msgid "Please select"
msgstr ""
+msgid "Please select a Jira project"
+msgstr ""
+
msgid "Please select a country"
msgstr ""
@@ -15095,6 +15413,9 @@ msgstr ""
msgid "Please wait while we import the repository for you. Refresh at will."
msgstr ""
+msgid "Plugins directory is deprecated and will be removed in 14.0. Please move this file into /file_hooks directory."
+msgstr ""
+
msgid "Pod does not exist"
msgstr ""
@@ -15212,7 +15533,7 @@ msgstr ""
msgid "Prevent users from changing their profile name"
msgstr ""
-msgid "Prevent users from modifing merge request approvers list"
+msgid "Prevent users from modifying merge request approvers list"
msgstr ""
msgid "Prevent users from performing write operations on GitLab while performing maintenance."
@@ -15233,10 +15554,10 @@ msgstr ""
msgid "Previous Artifacts"
msgstr ""
-msgid "Previous file in diff (MRs only)"
+msgid "Previous file in diff"
msgstr ""
-msgid "Previous unresolved discussion (MRs only)"
+msgid "Previous unresolved discussion"
msgstr ""
msgid "Primary"
@@ -15269,6 +15590,9 @@ msgstr ""
msgid "Private profile"
msgstr ""
+msgid "Private projects Minutes cost factor"
+msgstr ""
+
msgid "Private projects can be created in your personal namespace with:"
msgstr ""
@@ -16097,6 +16421,9 @@ msgstr ""
msgid "ProjectSettings|With GitLab Pages you can host your static websites on GitLab"
msgstr ""
+msgid "ProjectSettings|With Metrics Dashboard you can visualize this project performance metrics"
+msgstr ""
+
msgid "ProjectTemplates|.NET Core"
msgstr ""
@@ -16160,6 +16487,9 @@ msgstr ""
msgid "ProjectTemplates|Spring"
msgstr ""
+msgid "ProjectTemplates|Static Site Editor/Middleman"
+msgstr ""
+
msgid "ProjectTemplates|iOS (Swift)"
msgstr ""
@@ -16379,13 +16709,13 @@ msgstr ""
msgid "PrometheusService|Prometheus is being automatically managed on your clusters"
msgstr ""
-msgid "PrometheusService|These metrics will only be monitored after your first deployment to an environment"
+msgid "PrometheusService|Select the Active checkbox to override the Auto Configuration with custom settings. If unchecked, Auto Configuration settings are used."
msgstr ""
-msgid "PrometheusService|Time-series monitoring service"
+msgid "PrometheusService|These metrics will only be monitored after your first deployment to an environment"
msgstr ""
-msgid "PrometheusService|To enable manual configuration, uninstall Prometheus from your clusters"
+msgid "PrometheusService|Time-series monitoring service"
msgstr ""
msgid "PrometheusService|To enable the installation of Prometheus on your clusters, deactivate the manual configuration below"
@@ -16571,6 +16901,9 @@ msgstr ""
msgid "Protip:"
msgstr ""
+msgid "Protocol"
+msgstr ""
+
msgid "Provider"
msgstr ""
@@ -16595,6 +16928,9 @@ msgstr ""
msgid "Public pipelines"
msgstr ""
+msgid "Public projects Minutes cost factor"
+msgstr ""
+
msgid "Pull"
msgstr ""
@@ -16796,6 +17132,9 @@ msgstr[0] ""
msgstr[1] ""
msgstr[2] ""
+msgid "Regenerate export"
+msgstr ""
+
msgid "Regenerate instance ID"
msgstr ""
@@ -17174,9 +17513,30 @@ msgstr ""
msgid "Reports|%{combinedString} and %{resolvedString}"
msgstr ""
+msgid "Reports|Accessibility scanning detected %d issue for the source branch only"
+msgid_plural "Reports|Accessibility scanning detected %d issues for the source branch only"
+msgstr[0] ""
+msgstr[1] ""
+msgstr[2] ""
+
+msgid "Reports|Accessibility scanning detected no issues for the source branch only"
+msgstr ""
+
+msgid "Reports|Accessibility scanning failed loading results"
+msgstr ""
+
+msgid "Reports|Accessibility scanning results are being parsed"
+msgstr ""
+
msgid "Reports|Actions"
msgstr ""
+msgid "Reports|An error occured while loading report"
+msgstr ""
+
+msgid "Reports|An error occurred while loading %{name} results"
+msgstr ""
+
msgid "Reports|Class"
msgstr ""
@@ -17282,7 +17642,7 @@ msgstr ""
msgid "Requested %{time_ago}"
msgstr ""
-msgid "Requested design version does not exist"
+msgid "Requested design version does not exist."
msgstr ""
msgid "Requested states are invalid"
@@ -17468,6 +17828,9 @@ msgstr ""
msgid "Resync"
msgstr ""
+msgid "Resync all"
+msgstr ""
+
msgid "Resync all %{replicableType}"
msgstr ""
@@ -17537,6 +17900,9 @@ msgstr ""
msgid "Revoked personal access token %{personal_access_token_name}!"
msgstr ""
+msgid "Revoked project access token %{project_access_token_name}!"
+msgstr ""
+
msgid "RightSidebar|adding a"
msgstr ""
@@ -17585,6 +17951,9 @@ msgstr ""
msgid "Runner token"
msgstr ""
+msgid "Runner tokens"
+msgstr ""
+
msgid "Runner was not updated."
msgstr ""
@@ -17639,6 +18008,9 @@ msgstr ""
msgid "SAML SSO for %{group_name}"
msgstr ""
+msgid "SAML discovery tokens"
+msgstr ""
+
msgid "SAML for %{group_name}"
msgstr ""
@@ -17723,12 +18095,18 @@ msgstr ""
msgid "Scheduled"
msgstr ""
+msgid "Scheduled to merge this merge request (%{strategy})."
+msgstr ""
+
msgid "Scheduled to merge this merge request when the pipeline succeeds."
msgstr ""
msgid "Schedules"
msgstr ""
+msgid "Schedules to merge this merge request (%{strategy})."
+msgstr ""
+
msgid "Scheduling"
msgstr ""
@@ -17786,6 +18164,9 @@ msgstr ""
msgid "Search branches and tags"
msgstr ""
+msgid "Search by author"
+msgstr ""
+
msgid "Search files"
msgstr ""
@@ -17939,12 +18320,6 @@ msgstr[0] ""
msgstr[1] ""
msgstr[2] ""
-msgid "SearchResults|snippet result"
-msgid_plural "SearchResults|snippet results"
-msgstr[0] ""
-msgstr[1] ""
-msgstr[2] ""
-
msgid "SearchResults|user"
msgid_plural "SearchResults|users"
msgstr[0] ""
@@ -17957,6 +18332,9 @@ msgstr[0] ""
msgstr[1] ""
msgstr[2] ""
+msgid "Searching by both author and message is currently not supported."
+msgstr ""
+
msgid "Seat Link"
msgstr ""
@@ -17984,184 +18362,232 @@ msgstr ""
msgid "Security Dashboard"
msgstr ""
-msgid "Security Dashboard|Error fetching the vulnerability counts. Please check your network connection and try again."
+msgid "Security configuration help link"
msgstr ""
-msgid "Security Dashboard|Error fetching the vulnerability list. Please check your network connection and try again."
+msgid "Security dashboard"
msgstr ""
-msgid "Security Dashboard|Issue Created"
+msgid "Security report is out of date. Please update your branch with the latest changes from the target branch (%{targetBranchName})"
msgstr ""
-msgid "Security Reports|Comment added to '%{vulnerabilityName}'"
+msgid "Security report is out of date. Run %{newPipelineLinkStart}a new pipeline%{newPipelineLinkEnd} for the target branch (%{targetBranchName})"
msgstr ""
-msgid "Security Reports|Comment deleted on '%{vulnerabilityName}'"
+msgid "SecurityConfiguration|Configured"
msgstr ""
-msgid "Security Reports|Comment edited on '%{vulnerabilityName}'"
+msgid "SecurityConfiguration|Feature"
msgstr ""
-msgid "Security Reports|Create issue"
+msgid "SecurityConfiguration|Feature documentation for %{featureName}"
msgstr ""
-msgid "Security Reports|Dismiss vulnerability"
+msgid "SecurityConfiguration|Not yet configured"
msgstr ""
-msgid "Security Reports|Dismissed '%{vulnerabilityName}'"
+msgid "SecurityConfiguration|Secure features"
msgstr ""
-msgid "Security Reports|Dismissed '%{vulnerabilityName}'. Turn off the hide dismissed toggle to view."
+msgid "SecurityConfiguration|Status"
msgstr ""
-msgid "Security Reports|Either you don't have permission to view this dashboard or the dashboard has not been setup. Please check your permission settings with your administrator or check your dashboard configurations to proceed."
+msgid "SecurityReports|%{firstProject} and %{secondProject}"
msgstr ""
-msgid "Security Reports|Learn more about setting up your dashboard"
+msgid "SecurityReports|%{firstProject}, %{secondProject}, and %{rest}"
msgstr ""
-msgid "Security Reports|More info"
+msgid "SecurityReports|Add a project to your dashboard"
msgstr ""
-msgid "Security Reports|Oops, something doesn't seem right."
+msgid "SecurityReports|Add or remove projects from your dashboard"
msgstr ""
-msgid "Security Reports|Security reports can only be accessed by authorized users."
+msgid "SecurityReports|Add projects"
msgstr ""
-msgid "Security Reports|There was an error adding the comment."
+msgid "SecurityReports|Comment added to '%{vulnerabilityName}'"
msgstr ""
-msgid "Security Reports|There was an error creating the issue."
+msgid "SecurityReports|Comment deleted on '%{vulnerabilityName}'"
msgstr ""
-msgid "Security Reports|There was an error creating the merge request."
+msgid "SecurityReports|Comment edited on '%{vulnerabilityName}'"
msgstr ""
-msgid "Security Reports|There was an error deleting the comment."
+msgid "SecurityReports|Create issue"
msgstr ""
-msgid "Security Reports|There was an error dismissing the vulnerabilities."
+msgid "SecurityReports|Dismiss Selected"
msgstr ""
-msgid "Security Reports|There was an error dismissing the vulnerability."
+msgid "SecurityReports|Dismiss vulnerability"
msgstr ""
-msgid "Security Reports|There was an error reverting the dismissal."
+msgid "SecurityReports|Dismissed '%{vulnerabilityName}'"
msgstr ""
-msgid "Security Reports|There was an error reverting this dismissal."
+msgid "SecurityReports|Dismissed '%{vulnerabilityName}'. Turn off the hide dismissed toggle to view."
msgstr ""
-msgid "Security Reports|Undo dismiss"
+msgid "SecurityReports|Each vulnerability now has a unique page that can be directly linked to, shared, referenced, and tracked as the single source of truth. Vulnerability occurrences also persist across scanner runs, which improves tracking and visibility and reduces duplicates between scans."
msgstr ""
-msgid "Security Reports|You do not have sufficient permissions to access this report"
+msgid "SecurityReports|Edit dashboard"
msgstr ""
-msgid "Security Reports|You must sign in as an authorized user to see this report"
+msgid "SecurityReports|Either you don't have permission to view this dashboard or the dashboard has not been setup. Please check your permission settings with your administrator or check your dashboard configurations to proceed."
msgstr ""
-msgid "Security configuration help link"
+msgid "SecurityReports|Error fetching the vulnerability counts. Please check your network connection and try again."
msgstr ""
-msgid "Security dashboard"
+msgid "SecurityReports|Error fetching the vulnerability list. Please check your network connection and try again."
msgstr ""
-msgid "Security report is out of date. Please update your branch with the latest changes from the target branch (%{targetBranchName})"
+msgid "SecurityReports|False positive"
msgstr ""
-msgid "Security report is out of date. Run %{newPipelineLinkStart}a new pipeline%{newPipelineLinkEnd} for the target branch (%{targetBranchName})"
+msgid "SecurityReports|Hide dismissed"
msgstr ""
-msgid "SecurityConfiguration|Configured"
+msgid "SecurityReports|Introducing standalone vulnerabilities"
msgstr ""
-msgid "SecurityConfiguration|Feature"
+msgid "SecurityReports|Issue Created"
msgstr ""
-msgid "SecurityConfiguration|Feature documentation for %{featureName}"
+msgid "SecurityReports|Learn More"
msgstr ""
-msgid "SecurityConfiguration|Not yet configured"
+msgid "SecurityReports|Learn more about setting up your dashboard"
msgstr ""
-msgid "SecurityConfiguration|Secure features"
+msgid "SecurityReports|Load more vulnerabilities"
msgstr ""
-msgid "SecurityConfiguration|Status"
+msgid "SecurityReports|Monitor vulnerabilities in your code"
+msgstr ""
+
+msgid "SecurityReports|More info"
+msgstr ""
+
+msgid "SecurityReports|More information"
+msgstr ""
+
+msgid "SecurityReports|No vulnerabilities found for dashboard"
+msgstr ""
+
+msgid "SecurityReports|No vulnerabilities found for this group"
+msgstr ""
+
+msgid "SecurityReports|No vulnerabilities found for this pipeline"
+msgstr ""
+
+msgid "SecurityReports|No vulnerabilities found for this project"
+msgstr ""
+
+msgid "SecurityReports|Oops, something doesn't seem right."
+msgstr ""
+
+msgid "SecurityReports|Pipeline %{pipelineLink} triggered %{timeago} by %{user}"
msgstr ""
-msgid "SecurityDashboard|%{firstProject} and %{secondProject}"
+msgid "SecurityReports|Project"
msgstr ""
-msgid "SecurityDashboard|%{firstProject}, %{secondProject}, and %{rest}"
+msgid "SecurityReports|Projects added"
msgstr ""
-msgid "SecurityDashboard|Add a project to your dashboard"
+msgid "SecurityReports|Remove project from dashboard"
msgstr ""
-msgid "SecurityDashboard|Add or remove projects from your dashboard"
+msgid "SecurityReports|Report type"
msgstr ""
-msgid "SecurityDashboard|Add projects"
+msgid "SecurityReports|Return to dashboard"
msgstr ""
-msgid "SecurityDashboard|Edit dashboard"
+msgid "SecurityReports|Security Dashboard"
msgstr ""
-msgid "SecurityDashboard|Hide dismissed"
+msgid "SecurityReports|Security reports can only be accessed by authorized users."
msgstr ""
-msgid "SecurityDashboard|Monitor vulnerabilities in your code"
+msgid "SecurityReports|Select a project to add by using the project search field above."
msgstr ""
-msgid "SecurityDashboard|More information"
+msgid "SecurityReports|Select a reason"
msgstr ""
-msgid "SecurityDashboard|No vulnerabilities found for dashboard"
+msgid "SecurityReports|Severity"
msgstr ""
-msgid "SecurityDashboard|Pipeline %{pipelineLink} triggered %{timeago} by %{user}"
+msgid "SecurityReports|Status"
msgstr ""
-msgid "SecurityDashboard|Project"
+msgid "SecurityReports|The security dashboard displays the latest security findings for projects you wish to monitor. Select \"Edit dashboard\" to add and remove projects."
msgstr ""
-msgid "SecurityDashboard|Projects added"
+msgid "SecurityReports|The security dashboard displays the latest security report. Use it to find and fix vulnerabilities."
msgstr ""
-msgid "SecurityDashboard|Remove project from dashboard"
+msgid "SecurityReports|There was an error adding the comment."
msgstr ""
-msgid "SecurityDashboard|Report type"
+msgid "SecurityReports|There was an error creating the issue."
msgstr ""
-msgid "SecurityDashboard|Return to dashboard"
+msgid "SecurityReports|There was an error creating the merge request."
msgstr ""
-msgid "SecurityDashboard|Security Dashboard"
+msgid "SecurityReports|There was an error deleting the comment."
msgstr ""
-msgid "SecurityDashboard|Select a project to add by using the project search field above."
+msgid "SecurityReports|There was an error dismissing the vulnerabilities."
msgstr ""
-msgid "SecurityDashboard|Severity"
+msgid "SecurityReports|There was an error dismissing the vulnerability."
msgstr ""
-msgid "SecurityDashboard|Status"
+msgid "SecurityReports|There was an error reverting the dismissal."
msgstr ""
-msgid "SecurityDashboard|The security dashboard displays the latest security findings for projects you wish to monitor. Select \"Edit dashboard\" to add and remove projects."
+msgid "SecurityReports|There was an error reverting this dismissal."
msgstr ""
-msgid "SecurityDashboard|The security dashboard displays the latest security report. Use it to find and fix vulnerabilities."
+msgid "SecurityReports|There was an error while generating the report."
msgstr ""
-msgid "SecurityDashboard|There was an error while generating the report."
+msgid "SecurityReports|Unable to add %{invalidProjects}"
msgstr ""
-msgid "SecurityDashboard|Unable to add %{invalidProjects}"
+msgid "SecurityReports|Undo dismiss"
+msgstr ""
+
+msgid "SecurityReports|While it's rare to have no vulnerabilities for your group, it can happen. In any event, we ask that you double check your settings to make sure you've set up your dashboard correctly."
+msgstr ""
+
+msgid "SecurityReports|While it's rare to have no vulnerabilities for your pipeline, it can happen. In any event, we ask that you double check your settings to make sure all security scanning jobs have passed successfully."
+msgstr ""
+
+msgid "SecurityReports|While it's rare to have no vulnerabilities for your project, it can happen. In any event, we ask that you double check your settings to make sure you've set up your dashboard correctly."
+msgstr ""
+
+msgid "SecurityReports|While it's rare to have no vulnerabilities, it can happen. In any event, we ask that you please double check your settings to make sure you've set up your dashboard correctly."
+msgstr ""
+
+msgid "SecurityReports|Won't fix / Accept risk"
+msgstr ""
+
+msgid "SecurityReports|You do not have sufficient permissions to access this report"
+msgstr ""
+
+msgid "SecurityReports|You must sign in as an authorized user to see this report"
+msgstr ""
+
+msgid "SecurityReports|[No reason]"
msgstr ""
msgid "See GitLab's %{password_policy_guidelines}"
@@ -18173,6 +18599,9 @@ msgstr ""
msgid "See the affected projects in the GitLab admin panel"
msgstr ""
+msgid "See what's new at GitLab"
+msgstr ""
+
msgid "Select"
msgstr ""
@@ -18578,9 +19007,6 @@ msgstr ""
msgid "Set up Jira Integration"
msgstr ""
-msgid "Set up Jira Integration illustration"
-msgstr ""
-
msgid "Set up a %{type} Runner automatically"
msgstr ""
@@ -18665,6 +19091,9 @@ msgstr ""
msgid "Severity: %{severity}"
msgstr ""
+msgid "Shards (%{shards})"
+msgstr ""
+
msgid "Shards to synchronize"
msgstr ""
@@ -18980,6 +19409,9 @@ msgstr ""
msgid "Snowplow"
msgstr ""
+msgid "Some child epics may be hidden due to applied filters"
+msgstr ""
+
msgid "Some email servers do not support overriding the email sender name. Enable this option to include the name of the author of the issue, merge request or comment in the email body instead."
msgstr ""
@@ -19121,6 +19553,9 @@ msgstr ""
msgid "Something went wrong while updating your list settings"
msgstr ""
+msgid "Something went wrong with your automatic subscription renewal"
+msgstr ""
+
msgid "Something went wrong, unable to add %{project} to dashboard"
msgstr ""
@@ -19406,6 +19841,9 @@ msgstr ""
msgid "Stage removed"
msgstr ""
+msgid "Standard"
+msgstr ""
+
msgid "Star a label to make it a priority label. Order the prioritized labels to change their relative priority, by dragging."
msgstr ""
@@ -19529,6 +19967,9 @@ msgstr ""
msgid "Static Application Security Testing (SAST)"
msgstr ""
+msgid "StaticSiteEditor|An error occurred while submitting your changes."
+msgstr ""
+
msgid "StaticSiteEditor|Branch could not be created."
msgstr ""
@@ -19538,18 +19979,30 @@ msgstr ""
msgid "StaticSiteEditor|Could not create merge request."
msgstr ""
+msgid "StaticSiteEditor|Incompatible file content"
+msgstr ""
+
msgid "StaticSiteEditor|Return to site"
msgstr ""
+msgid "StaticSiteEditor|Static site editor"
+msgstr ""
+
msgid "StaticSiteEditor|Success!"
msgstr ""
msgid "StaticSiteEditor|Summary of changes"
msgstr ""
+msgid "StaticSiteEditor|The Static Site Editor is currently configured to only edit Markdown content on pages generated from Middleman. Visit the documentation to learn more about configuring your site to use the Static Site Editor."
+msgstr ""
+
msgid "StaticSiteEditor|Update %{sourcePath} file"
msgstr ""
+msgid "StaticSiteEditor|View documentation"
+msgstr ""
+
msgid "StaticSiteEditor|View merge request"
msgstr ""
@@ -19607,6 +20060,12 @@ msgstr ""
msgid "StatusPage|Status page"
msgstr ""
+msgid "StatusPage|Status page URL"
+msgstr ""
+
+msgid "StatusPage|Status page frontend documentation"
+msgstr ""
+
msgid "StatusPage|To publish incidents to an external status page, GitLab will store a JSON file in your Amazon S3 account in a location accessible to your external status page service. Make sure to also set up %{docsLink}"
msgstr ""
@@ -19955,6 +20414,12 @@ msgstr ""
msgid "Synced"
msgstr ""
+msgid "Synchronization disabled"
+msgstr ""
+
+msgid "Synchronization of container repositories is disabled."
+msgstr ""
+
msgid "System"
msgstr ""
@@ -20108,6 +20573,9 @@ msgstr ""
msgid "Telephone number"
msgstr ""
+msgid "Telephone number (Optional)"
+msgstr ""
+
msgid "Template"
msgstr ""
@@ -20417,6 +20885,9 @@ msgstr ""
msgid "The license was successfully uploaded and is now active. You can see the details below."
msgstr ""
+msgid "The license was successfully uploaded and will be active from %{starts_at}. You can see the details below."
+msgstr ""
+
msgid "The maximum file size allowed is %{size}."
msgstr ""
@@ -20738,6 +21209,9 @@ msgstr ""
msgid "There was an error getting the epic participants."
msgstr ""
+msgid "There was an error importing the Jira project."
+msgstr ""
+
msgid "There was an error loading users activity calendar."
msgstr ""
@@ -20753,7 +21227,7 @@ msgstr ""
msgid "There was an error resetting user pipeline minutes."
msgstr ""
-msgid "There was an error saving this Geo Node"
+msgid "There was an error saving this Geo Node."
msgstr ""
msgid "There was an error saving your changes."
@@ -20816,6 +21290,9 @@ msgstr ""
msgid "They can be managed using the %{link}."
msgstr ""
+msgid "Third Party Advisory Link"
+msgstr ""
+
msgid "Third party offers"
msgstr ""
@@ -20852,6 +21329,9 @@ msgstr ""
msgid "This application will be able to:"
msgstr ""
+msgid "This attachment has been truncated to avoid exceeding the maximum allowed attachment size of 15MB. %{written_count} of %{issues_count} issues have been included. Consider re-exporting with a narrower selection of issues."
+msgstr ""
+
msgid "This block is self-referential"
msgstr ""
@@ -20993,6 +21473,12 @@ msgstr ""
msgid "This issue is confidential"
msgstr ""
+msgid "This issue is currently blocked by the following issues: %{issues}."
+msgstr ""
+
+msgid "This issue is in a child epic of the filtered epic"
+msgstr ""
+
msgid "This issue is locked."
msgstr ""
@@ -21191,7 +21677,10 @@ msgstr ""
msgid "This user will be the author of all events in the activity feed that are the result of an update, like new branches being created or new commits being pushed to existing branches. Upon creation or when reassigning you can only assign yourself to be the mirror user."
msgstr ""
-msgid "This variable can not be masked"
+msgid "This variable can not be masked."
+msgstr ""
+
+msgid "This variable does not match the expected pattern."
msgstr ""
msgid "This will help us personalize your onboarding experience."
@@ -21221,10 +21710,10 @@ msgstr ""
msgid "ThreatMonitoring|Application firewall not detected"
msgstr ""
-msgid "ThreatMonitoring|Container Network Policy"
+msgid "ThreatMonitoring|Container Network Policies are not installed or have been disabled. To view this data, ensure your Network Policies are installed and enabled for your cluster."
msgstr ""
-msgid "ThreatMonitoring|Container NetworkPolicies are not installed or has been disabled. To view this data, ensure you NetworkPolicies are installed and enabled for your cluster."
+msgid "ThreatMonitoring|Container Network Policy"
msgstr ""
msgid "ThreatMonitoring|Container NetworkPolicies not detected"
@@ -21257,7 +21746,7 @@ msgstr ""
msgid "ThreatMonitoring|Something went wrong, unable to fetch statistics"
msgstr ""
-msgid "ThreatMonitoring|The firewall is not installed or has been disabled. To view this data, ensure you firewall is installed and enabled for your cluster."
+msgid "ThreatMonitoring|The firewall is not installed or has been disabled. To view this data, ensure the web application firewall is installed and enabled for your cluster."
msgstr ""
msgid "ThreatMonitoring|The graph below is an overview of traffic coming to your application as tracked by the Web Application Firewall (WAF). View the docs for instructions on how to access the WAF logs to see what type of malicious traffic is trying to access your app. The docs link is also accessible by clicking the \"?\" icon next to the title below."
@@ -21326,6 +21815,9 @@ msgstr ""
msgid "Time in seconds GitLab will wait for a response from the external service. When the service does not respond in time, access will be denied."
msgstr ""
+msgid "Time of import: %{importTime}"
+msgstr ""
+
msgid "Time remaining"
msgstr ""
@@ -21512,7 +22004,7 @@ msgstr ""
msgid "Title:"
msgstr ""
-msgid "Titles and Filenames"
+msgid "Titles and Descriptions"
msgstr ""
msgid "To"
@@ -21719,7 +22211,7 @@ msgstr ""
msgid "Too many projects enabled. You will need to manage them via the console or the API."
msgstr ""
-msgid "Topics"
+msgid "Topics (optional)"
msgstr ""
msgid "Total"
@@ -21881,6 +22373,9 @@ msgstr ""
msgid "Try all GitLab has to offer for 30 days."
msgstr ""
+msgid "Try changing or removing filters."
+msgstr ""
+
msgid "Try to fork again"
msgstr ""
@@ -21974,6 +22469,9 @@ msgstr ""
msgid "Unable to connect to server: %{error}"
msgstr ""
+msgid "Unable to connect to the Jira instance. Please check your Jira integration configuration."
+msgstr ""
+
msgid "Unable to convert Kubernetes logs encoding to UTF-8"
msgstr ""
@@ -22919,6 +23417,9 @@ msgstr ""
msgid "View issue"
msgstr ""
+msgid "View issues"
+msgstr ""
+
msgid "View it on GitLab"
msgstr ""
@@ -23096,6 +23597,15 @@ msgstr ""
msgid "VulnerabilityManagement|Resolved %{timeago} by %{user}"
msgstr ""
+msgid "VulnerabilityManagement|Something went wrong while trying to delete the comment. Please try again later."
+msgstr ""
+
+msgid "VulnerabilityManagement|Something went wrong while trying to retrieve the vulnerability history. Please try again later."
+msgstr ""
+
+msgid "VulnerabilityManagement|Something went wrong while trying to save the comment. Please try again later."
+msgstr ""
+
msgid "VulnerabilityManagement|Something went wrong, could not create an issue."
msgstr ""
@@ -23126,6 +23636,9 @@ msgstr ""
msgid "VulnerabilityStatusTypes|Resolved"
msgstr ""
+msgid "Vulnerability|%{scannerName} (version %{scannerVersion})"
+msgstr ""
+
msgid "Vulnerability|Class"
msgstr ""
@@ -23159,7 +23672,10 @@ msgstr ""
msgid "Vulnerability|Project"
msgstr ""
-msgid "Vulnerability|Report Type"
+msgid "Vulnerability|Scanner Provider"
+msgstr ""
+
+msgid "Vulnerability|Scanner Type"
msgstr ""
msgid "Vulnerability|Severity"
@@ -23213,9 +23729,15 @@ msgstr ""
msgid "We sent you an email with reset password instructions"
msgstr ""
+msgid "We tried to automatically renew your %{strong}%{plan_name}%{strong_close} subscription for %{strong}%{namespace_name}%{strong_close} on %{expires_on} but something went wrong so your subscription was downgraded to the free plan. Don't worry, your data is safe. We suggest you check your payment method and get in touch with our support team (%{support_link}). They'll gladly help with your subscription renewal."
+msgstr ""
+
msgid "We want to be sure it is you, please confirm you are not a robot."
msgstr ""
+msgid "We will automatically renew your %{strong}%{plan_name}%{strong_close} subscription for %{strong}%{namespace_name}%{strong_close} on %{strong}%{expires_on}%{strong_close}. There's nothing that you need to do, we'll let you know when the renewal is complete. Need more seats, a higher plan or just want to review your payment method?"
+msgstr ""
+
msgid "We've found no vulnerabilities"
msgstr ""
@@ -23321,15 +23843,6 @@ msgstr ""
msgid "When:"
msgstr ""
-msgid "While it's rare to have no vulnerabilities for your group, it can happen. In any event, we ask that you double check your settings to make sure you've set up your dashboard correctly."
-msgstr ""
-
-msgid "While it's rare to have no vulnerabilities for your pipeline, it can happen. In any event, we ask that you double check your settings to make sure all security scanning jobs have passed successfully."
-msgstr ""
-
-msgid "While it's rare to have no vulnerabilities for your project, it can happen. In any event, we ask that you double check your settings to make sure you've set up your dashboard correctly."
-msgstr ""
-
msgid "While it's rare to have no vulnerabilities, it can happen. In any event, we ask that you please double check your settings to make sure you've set up your dashboard correctly."
msgstr ""
@@ -23360,9 +23873,6 @@ msgstr ""
msgid "Wiki"
msgstr ""
-msgid "Wiki pages"
-msgstr ""
-
msgid "Wiki was successfully updated."
msgstr ""
@@ -23546,6 +24056,9 @@ msgstr ""
msgid "Yes, add it"
msgstr ""
+msgid "Yes, close issue"
+msgstr ""
+
msgid "Yes, let me map Google Code users to full names or GitLab users."
msgstr ""
@@ -23741,6 +24254,9 @@ msgstr ""
msgid "You could not create a new trigger."
msgstr ""
+msgid "You didn't renew your %{strong}%{plan_name}%{strong_close} subscription for %{strong}%{namespace_name}%{strong_close} so it was downgraded to the free plan."
+msgstr ""
+
msgid "You didn't renew your %{strong}%{plan_name}%{strong_close} subscription so it was downgraded to the GitLab Core Plan."
msgstr ""
@@ -23807,6 +24323,9 @@ msgstr ""
msgid "You have declined the invitation to join %{label}."
msgstr ""
+msgid "You have imported from this project %{numberOfPreviousImportsForProject} times before. Each new import will create duplicate issues."
+msgstr ""
+
msgid "You have no permissions"
msgstr ""
@@ -23888,6 +24407,9 @@ msgstr ""
msgid "You need to upload a Google Takeout archive."
msgstr ""
+msgid "You reached %{usage_in_percent} of %{namespace_name}'s capacity (%{used_storage} of %{storage_limit})"
+msgstr ""
+
msgid "You tried to fork %{link_to_the_project} but it failed for the following reason:"
msgstr ""
@@ -23972,9 +24494,21 @@ msgstr ""
msgid "YouTube"
msgstr ""
+msgid "Your %{strong}%{plan_name}%{strong_close} subscription for %{strong}%{namespace_name}%{strong_close} will expire on %{strong}%{expires_on}%{strong_close}. After that, you will not to be able to create issues or merge requests as well as many other features."
+msgstr ""
+
msgid "Your %{strong}%{plan_name}%{strong_close} subscription will expire on %{strong}%{expires_on}%{strong_close}. After that, you will not to be able to create issues or merge requests as well as many other features."
msgstr ""
+msgid "Your CSV export has started. It will be emailed to %{email} when complete."
+msgstr ""
+
+msgid "Your CSV export of %{issues_count} from project %{project_link} has been added to this email as an attachment."
+msgstr ""
+
+msgid "Your CSV export of %{written_count} from project %{project_name} (%{project_url}) has been added to this email as an attachment."
+msgstr ""
+
msgid "Your Commit Email will be used for web based operations, such as edits and merges."
msgstr ""
@@ -24071,6 +24605,9 @@ msgstr ""
msgid "Your comment could not be updated! Please check your network connection and try again."
msgstr ""
+msgid "Your comment will be discarded."
+msgstr ""
+
msgid "Your custom stage '%{title}' was created"
msgstr ""
@@ -24110,6 +24647,9 @@ msgstr ""
msgid "Your new personal access token has been created."
msgstr ""
+msgid "Your new project access token has been created."
+msgstr ""
+
msgid "Your password isn't required to view this page. If a password or any other personal details are requested, please contact your administrator to report abuse."
msgstr ""
@@ -24128,12 +24668,18 @@ msgstr ""
msgid "Your request for access has been queued for review."
msgstr ""
+msgid "Your search didn't match any commits."
+msgstr ""
+
msgid "Your subscription expired!"
msgstr ""
msgid "Your subscription has been downgraded"
msgstr ""
+msgid "Your subscription will automatically renew in %{remaining_days}"
+msgstr ""
+
msgid "Your subscription will expire in %{remaining_days}"
msgstr ""
@@ -24173,7 +24719,7 @@ msgstr ""
msgid "allowed to fail"
msgstr ""
-msgid "already being used for another group or project milestone."
+msgid "already being used for another group or project %{timebox_name}."
msgstr ""
msgid "already has a \"created\" issue link"
@@ -24251,6 +24797,9 @@ msgstr ""
msgid "ciReport|%{linkStartTag}Learn more about SAST %{linkEndTag}"
msgstr ""
+msgid "ciReport|%{linkStartTag}Learn more about Secret Scanning %{linkEndTag}"
+msgstr ""
+
msgid "ciReport|%{linkStartTag}Learn more about codequality reports %{linkEndTag}"
msgstr ""
@@ -24419,6 +24968,12 @@ msgstr ""
msgid "ciReport|SAST"
msgstr ""
+msgid "ciReport|Secret scanning"
+msgstr ""
+
+msgid "ciReport|Secret scanning detects secrets and credentials vulnerabilities in your source code."
+msgstr ""
+
msgid "ciReport|Security scanning"
msgstr ""
@@ -24620,6 +25175,9 @@ msgstr ""
msgid "group"
msgstr ""
+msgid "groups"
+msgstr ""
+
msgid "has already been linked to another vulnerability"
msgstr ""
@@ -24785,9 +25343,6 @@ msgstr[2] ""
msgid "merged %{time_ago}"
msgstr ""
-msgid "milestone should belong either to a project or a group."
-msgstr ""
-
msgid "missing"
msgstr ""
@@ -25124,6 +25679,9 @@ msgstr ""
msgid "no contributions"
msgstr ""
+msgid "no expiration"
+msgstr ""
+
msgid "no one can merge"
msgstr ""
@@ -25178,6 +25736,9 @@ msgstr ""
msgid "pending removal"
msgstr ""
+msgid "per day"
+msgstr ""
+
msgid "pipeline"
msgstr ""
@@ -25208,6 +25769,12 @@ msgstr[2] ""
msgid "project avatar"
msgstr ""
+msgid "projects"
+msgstr ""
+
+msgid "push to your repository, create pipelines, create issues or add comments. To reduce storage capacity, delete unused repositories, artifacts, wikis, issues, and pipelines."
+msgstr ""
+
msgid "quick actions"
msgstr ""
@@ -25355,12 +25922,6 @@ msgstr ""
msgid "this document"
msgstr ""
-msgid "thread resolved"
-msgid_plural "threads resolved"
-msgstr[0] ""
-msgstr[1] ""
-msgstr[2] ""
-
msgid "to help your contributors communicate effectively!"
msgstr ""
@@ -25415,6 +25976,9 @@ msgstr ""
msgid "view the blob"
msgstr ""
+msgid "vulnerability|Add a comment"
+msgstr ""
+
msgid "vulnerability|Add a comment or reason for dismissal"
msgstr ""
diff --git a/locale/hu_HU/gitlab.po b/locale/hu_HU/gitlab.po
index 7b057304d32..340f3e57d82 100644
--- a/locale/hu_HU/gitlab.po
+++ b/locale/hu_HU/gitlab.po
@@ -12,7 +12,7 @@ msgstr ""
"X-Crowdin-Project: gitlab-ee\n"
"X-Crowdin-Language: hu\n"
"X-Crowdin-File: /master/locale/gitlab.pot\n"
-"PO-Revision-Date: 2020-04-15 00:31\n"
+"PO-Revision-Date: 2020-05-05 21:13\n"
msgid " %{start} to %{end}"
msgstr ""
@@ -212,6 +212,11 @@ msgid_plural "%d tags"
msgstr[0] ""
msgstr[1] ""
+msgid "%d unresolved thread"
+msgid_plural "%d unresolved threads"
+msgstr[0] ""
+msgstr[1] ""
+
msgid "%d vulnerability dismissed"
msgid_plural "%d vulnerabilities dismissed"
msgstr[0] ""
@@ -371,6 +376,9 @@ msgstr ""
msgid "%{mrText}, this issue will be closed automatically."
msgstr ""
+msgid "%{namespace_name} is now read-only. You cannot: %{base_message}"
+msgstr ""
+
msgid "%{name} contained %{resultsString}"
msgstr ""
@@ -383,6 +391,11 @@ msgstr ""
msgid "%{name}'s avatar"
msgstr ""
+msgid "%{no_of_days} day"
+msgid_plural "%{no_of_days} days"
+msgstr[0] ""
+msgstr[1] ""
+
msgid "%{number_commits_behind} commits behind %{default_branch}, %{number_commits_ahead} commits ahead"
msgstr ""
@@ -506,6 +519,9 @@ msgstr[1] ""
msgid "%{text} is available"
msgstr ""
+msgid "%{timebox_name} should belong either to a project or a group."
+msgstr ""
+
msgid "%{title} %{operator} %{threshold}"
msgstr ""
@@ -781,12 +797,18 @@ msgstr ""
msgid "<code>\"johnsmith@example.com\": \"johnsmith@example.com\"</code> will add \"By <a href=\"#\">johnsmith@example.com</a>\" to all issues and comments originally created by johnsmith@example.com. By default, the email address or username is masked to ensure the user's privacy. Use this option if you want to show the full email address."
msgstr ""
+msgid "<namespace / project>"
+msgstr ""
+
msgid "<no name set>"
msgstr ""
msgid "<no scopes selected>"
msgstr ""
+msgid "<project name>"
+msgstr ""
+
msgid "<strong>%{group_name}</strong> group members"
msgstr ""
@@ -820,6 +842,9 @@ msgstr ""
msgid "A basic page and serverless function that uses AWS Lambda, AWS API Gateway, and GitLab Pages"
msgstr ""
+msgid "A complete DevOps platform"
+msgstr ""
+
msgid "A default branch cannot be chosen for an empty project."
msgstr ""
@@ -1036,6 +1061,12 @@ msgstr ""
msgid "AccessTokens|reset it"
msgstr ""
+msgid "AccessibilityReport|Accessibility report artifact not found"
+msgstr ""
+
+msgid "AccessibilityReport|Failed to retrieve accessibility report"
+msgstr ""
+
msgid "AccessibilityReport|Learn More"
msgstr ""
@@ -1045,6 +1076,9 @@ msgstr ""
msgid "AccessibilityReport|New"
msgstr ""
+msgid "AccessibilityReport|The accessibility scanning found an error of the following type: %{code}"
+msgstr ""
+
msgid "Account"
msgstr ""
@@ -1287,6 +1321,9 @@ msgstr ""
msgid "Added at"
msgstr ""
+msgid "Added for this merge request"
+msgstr ""
+
msgid "Added in this version"
msgstr ""
@@ -1658,6 +1695,72 @@ msgid_plural "Alerts"
msgstr[0] ""
msgstr[1] ""
+msgid "Alert Details"
+msgstr ""
+
+msgid "AlertManagement|Acknowledged"
+msgstr ""
+
+msgid "AlertManagement|Alert"
+msgstr ""
+
+msgid "AlertManagement|Authorize external service"
+msgstr ""
+
+msgid "AlertManagement|Display alerts from all your monitoring tools directly within GitLab. Streamline the investigation of your alerts and the escalation of alerts to incidents."
+msgstr ""
+
+msgid "AlertManagement|End time"
+msgstr ""
+
+msgid "AlertManagement|End time:"
+msgstr ""
+
+msgid "AlertManagement|Events"
+msgstr ""
+
+msgid "AlertManagement|Events:"
+msgstr ""
+
+msgid "AlertManagement|Full Alert Details"
+msgstr ""
+
+msgid "AlertManagement|More information"
+msgstr ""
+
+msgid "AlertManagement|No alerts available to display. If you think you're seeing this message in error, refresh the page."
+msgstr ""
+
+msgid "AlertManagement|No alerts to display."
+msgstr ""
+
+msgid "AlertManagement|Overview"
+msgstr ""
+
+msgid "AlertManagement|Resolved"
+msgstr ""
+
+msgid "AlertManagement|Severity"
+msgstr ""
+
+msgid "AlertManagement|Start time"
+msgstr ""
+
+msgid "AlertManagement|Start time:"
+msgstr ""
+
+msgid "AlertManagement|Status"
+msgstr ""
+
+msgid "AlertManagement|Surface alerts in GitLab"
+msgstr ""
+
+msgid "AlertManagement|There was an error displaying the alerts. Confirm your endpoint's configuration details to ensure alerts appear."
+msgstr ""
+
+msgid "AlertManagement|Triggered"
+msgstr ""
+
msgid "AlertService|%{linkStart}Learn more%{linkEnd} about configuring this endpoint to receive alerts."
msgstr ""
@@ -1727,6 +1830,9 @@ msgstr ""
msgid "All security scans are enabled because %{linkStart}Auto DevOps%{linkEnd} is enabled on this project"
msgstr ""
+msgid "All threads resolved"
+msgstr ""
+
msgid "All users"
msgstr ""
@@ -1745,6 +1851,9 @@ msgstr ""
msgid "Allow only the selected protocols to be used for Git access."
msgstr ""
+msgid "Allow owners to manage default branch protection per group"
+msgstr ""
+
msgid "Allow owners to manually add users outside of LDAP"
msgstr ""
@@ -1853,6 +1962,9 @@ msgstr ""
msgid "An error occurred fetching the dropdown data."
msgstr ""
+msgid "An error occurred fetching the project authors."
+msgstr ""
+
msgid "An error occurred previewing the blob"
msgstr ""
@@ -1934,6 +2046,9 @@ msgstr ""
msgid "An error occurred while fetching sidebar data"
msgstr ""
+msgid "An error occurred while fetching terraform reports."
+msgstr ""
+
msgid "An error occurred while fetching the Service Desk address."
msgstr ""
@@ -2171,6 +2286,9 @@ msgstr ""
msgid "Any"
msgstr ""
+msgid "Any Author"
+msgstr ""
+
msgid "Any Label"
msgstr ""
@@ -2386,10 +2504,10 @@ msgstr ""
msgid "Are you sure that you want to unarchive this project?"
msgstr ""
-msgid "Are you sure you want to cancel creating this comment?"
+msgid "Are you sure you want to cancel editing this comment?"
msgstr ""
-msgid "Are you sure you want to cancel editing this comment?"
+msgid "Are you sure you want to close this blocked issue?"
msgstr ""
msgid "Are you sure you want to delete %{name}?"
@@ -2419,6 +2537,9 @@ msgstr ""
msgid "Are you sure you want to deploy this environment?"
msgstr ""
+msgid "Are you sure you want to discard this comment?"
+msgstr ""
+
msgid "Are you sure you want to erase this build?"
msgstr ""
@@ -2595,6 +2716,9 @@ msgstr ""
msgid "At least one approval from a code owner is required to change files matching the respective CODEOWNER rules."
msgstr ""
+msgid "At least one logging option is required to be enabled"
+msgstr ""
+
msgid "At least one of group_id or project_id must be specified"
msgstr ""
@@ -2633,6 +2757,27 @@ msgstr ""
msgid "AuditEvents|Target"
msgstr ""
+msgid "AuditLogs|(removed)"
+msgstr ""
+
+msgid "AuditLogs|Action"
+msgstr ""
+
+msgid "AuditLogs|Author"
+msgstr ""
+
+msgid "AuditLogs|Date"
+msgstr ""
+
+msgid "AuditLogs|IP Address"
+msgstr ""
+
+msgid "AuditLogs|Object"
+msgstr ""
+
+msgid "AuditLogs|Target"
+msgstr ""
+
msgid "Aug"
msgstr ""
@@ -2915,9 +3060,6 @@ msgstr ""
msgid "BambooService|You must set up automatic revision labeling and a repository trigger in Bamboo."
msgstr ""
-msgid "Batch operations"
-msgstr ""
-
msgid "BatchComments|Delete all pending comments"
msgstr ""
@@ -3047,6 +3189,9 @@ msgstr ""
msgid "Boards"
msgstr ""
+msgid "Boards and Board Lists"
+msgstr ""
+
msgid "Boards|Collapse"
msgstr ""
@@ -3266,6 +3411,9 @@ msgstr ""
msgid "Buy GitLab Enterprise Edition"
msgstr ""
+msgid "Buy more Pipeline minutes"
+msgstr ""
+
msgid "By %{user_name}"
msgstr ""
@@ -3386,6 +3534,9 @@ msgstr ""
msgid "Can't scan the code?"
msgstr ""
+msgid "Can't update snippet: %{err}"
+msgstr ""
+
msgid "Canary"
msgstr ""
@@ -3554,6 +3705,9 @@ msgstr ""
msgid "Changing group path can have unintended side effects."
msgstr ""
+msgid "Charts"
+msgstr ""
+
msgid "Charts can't be displayed as the request for data has timed out. %{documentationLink}"
msgstr ""
@@ -3590,9 +3744,6 @@ msgstr ""
msgid "ChatMessage|and [%{count} more](%{pipeline_failed_jobs_url})"
msgstr ""
-msgid "ChatMessage|failed"
-msgstr ""
-
msgid "ChatMessage|has failed"
msgstr ""
@@ -3608,9 +3759,6 @@ msgstr ""
msgid "ChatMessage|in %{project_link}"
msgstr ""
-msgid "ChatMessage|passed"
-msgstr ""
-
msgid "Check again"
msgstr ""
@@ -4061,9 +4209,6 @@ msgstr ""
msgid "Click any <strong>project name</strong> in the project list below to navigate to the project milestone."
msgstr ""
-msgid "Click here"
-msgstr ""
-
msgid "Click the <strong>Download</strong> button and wait for downloading to complete."
msgstr ""
@@ -4451,6 +4596,12 @@ msgstr ""
msgid "ClusterIntegration|Fetching zones"
msgstr ""
+msgid "ClusterIntegration|Fluentd"
+msgstr ""
+
+msgid "ClusterIntegration|Fluentd is an open source data collector, which lets you unify the data collection and consumption for a better use and understanding of data. It requires at least one of the following logs to be successfully installed."
+msgstr ""
+
msgid "ClusterIntegration|GitLab Integration"
msgstr ""
@@ -4757,6 +4908,15 @@ msgstr ""
msgid "ClusterIntegration|Request to begin uninstalling failed"
msgstr ""
+msgid "ClusterIntegration|SIEM Hostname"
+msgstr ""
+
+msgid "ClusterIntegration|SIEM Port"
+msgstr ""
+
+msgid "ClusterIntegration|SIEM Protocol"
+msgstr ""
+
msgid "ClusterIntegration|Save changes"
msgstr ""
@@ -4853,6 +5013,12 @@ msgstr ""
msgid "ClusterIntegration|Select zone to choose machine type"
msgstr ""
+msgid "ClusterIntegration|Send Cilium Logs"
+msgstr ""
+
+msgid "ClusterIntegration|Send ModSecurity Logs"
+msgstr ""
+
msgid "ClusterIntegration|Service Token"
msgstr ""
@@ -5281,18 +5447,33 @@ msgstr ""
msgid "Compliance framework (optional)"
msgstr ""
+msgid "ComplianceFramework|GDPR"
+msgstr ""
+
msgid "ComplianceFramework|GDPR - General Data Protection Regulation"
msgstr ""
+msgid "ComplianceFramework|HIPAA"
+msgstr ""
+
msgid "ComplianceFramework|HIPAA - Health Insurance Portability and Accountability Act"
msgstr ""
+msgid "ComplianceFramework|PCI-DSS"
+msgstr ""
+
msgid "ComplianceFramework|PCI-DSS - Payment Card Industry-Data Security Standard"
msgstr ""
+msgid "ComplianceFramework|SOC 2"
+msgstr ""
+
msgid "ComplianceFramework|SOC 2 - Service Organization Control 2"
msgstr ""
+msgid "ComplianceFramework|SOX"
+msgstr ""
+
msgid "ComplianceFramework|SOX - Sarbanes-Oxley"
msgstr ""
@@ -5440,9 +5621,15 @@ msgstr ""
msgid "Container repositories sync capacity"
msgstr ""
+msgid "ContainerRegistry| Please visit the %{linkStart}administration settings%{linkEnd} to enable this feature."
+msgstr ""
+
msgid "ContainerRegistry|%{imageName} tags"
msgstr ""
+msgid "ContainerRegistry|%{title} was successfully scheduled for deletion"
+msgstr ""
+
msgid "ContainerRegistry|Automatically remove extra images that aren't designed to be kept."
msgstr ""
@@ -5455,6 +5642,9 @@ msgstr ""
msgid "ContainerRegistry|Container Registry"
msgstr ""
+msgid "ContainerRegistry|Container Registry tag expiration and retention policy is disabled"
+msgstr ""
+
msgid "ContainerRegistry|Copy build command"
msgstr ""
@@ -5464,18 +5654,12 @@ msgstr ""
msgid "ContainerRegistry|Copy push command"
msgstr ""
-msgid "ContainerRegistry|Currently, the Container Registry tag expiration feature is not available for projects created before GitLab version 12.8. For updates and more information, visit Issue %{linkStart}#196124%{linkEnd}"
-msgstr ""
-
msgid "ContainerRegistry|Docker connection error"
msgstr ""
msgid "ContainerRegistry|Docker tag expiration policy is %{toggleStatus}"
msgstr ""
-msgid "ContainerRegistry|Docker tags with names matching this regex pattern will expire:"
-msgstr ""
-
msgid "ContainerRegistry|Edit Settings"
msgstr ""
@@ -5497,9 +5681,6 @@ msgstr ""
msgid "ContainerRegistry|Image ID"
msgstr ""
-msgid "ContainerRegistry|Image deleted successfully"
-msgstr ""
-
msgid "ContainerRegistry|Keep and protect the images that matter most."
msgstr ""
@@ -5515,12 +5696,24 @@ msgstr ""
msgid "ContainerRegistry|Number of tags to retain:"
msgstr ""
+msgid "ContainerRegistry|Please contact your administrator."
+msgstr ""
+
msgid "ContainerRegistry|Push an image"
msgstr ""
msgid "ContainerRegistry|Quick Start"
msgstr ""
+msgid "ContainerRegistry|Regular expressions such as %{codeStart}.*-test%{codeEnd} or %{codeStart}dev-.*%{codeEnd} are supported"
+msgstr ""
+
+msgid "ContainerRegistry|Regular expressions such as %{codeStart}.*-test%{codeEnd} or %{codeStart}dev-.*%{codeEnd} are supported. To select all tags, use %{codeStart}.*%{codeEnd}"
+msgstr ""
+
+msgid "ContainerRegistry|Remember to run %{docLinkStart}garbage collection%{docLinkEnd} to remove the stale data from storage."
+msgstr ""
+
msgid "ContainerRegistry|Remove repository"
msgstr ""
@@ -5535,22 +5728,22 @@ msgstr[1] ""
msgid "ContainerRegistry|Retention policy has been Enabled"
msgstr ""
-msgid "ContainerRegistry|Something went wrong while deleting the image."
+msgid "ContainerRegistry|Something went wrong while fetching the expiration policy."
msgstr ""
-msgid "ContainerRegistry|Something went wrong while deleting the tag."
+msgid "ContainerRegistry|Something went wrong while fetching the repository list."
msgstr ""
-msgid "ContainerRegistry|Something went wrong while deleting the tags."
+msgid "ContainerRegistry|Something went wrong while fetching the tags list."
msgstr ""
-msgid "ContainerRegistry|Something went wrong while fetching the expiration policy."
+msgid "ContainerRegistry|Something went wrong while marking the tag for deletion."
msgstr ""
-msgid "ContainerRegistry|Something went wrong while fetching the packages list."
+msgid "ContainerRegistry|Something went wrong while marking the tags for deletion."
msgstr ""
-msgid "ContainerRegistry|Something went wrong while fetching the tags list."
+msgid "ContainerRegistry|Something went wrong while scheduling %{title} for deletion. Please try again."
msgstr ""
msgid "ContainerRegistry|Something went wrong while updating the expiration policy."
@@ -5559,16 +5752,25 @@ msgstr ""
msgid "ContainerRegistry|Tag"
msgstr ""
-msgid "ContainerRegistry|Tag deleted successfully"
-msgstr ""
-
msgid "ContainerRegistry|Tag expiration policy"
msgstr ""
msgid "ContainerRegistry|Tag expiration policy is designed to:"
msgstr ""
-msgid "ContainerRegistry|Tags deleted successfully"
+msgid "ContainerRegistry|Tag successfully marked for deletion."
+msgstr ""
+
+msgid "ContainerRegistry|Tags successfully marked for deletion."
+msgstr ""
+
+msgid "ContainerRegistry|Tags with names matching this regex pattern will %{italicStart}be preserved:%{italicEnd}"
+msgstr ""
+
+msgid "ContainerRegistry|Tags with names matching this regex pattern will %{italicStart}expire:%{italicEnd}"
+msgstr ""
+
+msgid "ContainerRegistry|The Container Registry tag expiration and retention policies for this project have not been enabled."
msgstr ""
msgid "ContainerRegistry|The last tag related to this image was recently removed. This empty image and any associated data will be automatically removed as part of the regular Garbage Collection process. If you have any questions, contact your administrator."
@@ -5589,16 +5791,16 @@ msgstr ""
msgid "ContainerRegistry|There are no container images stored for this project"
msgstr ""
-msgid "ContainerRegistry|This Registry contains deleted image tag data. Remember to run %{docLinkStart}garbage collection%{docLinkEnd} to remove the stale data from storage."
+msgid "ContainerRegistry|There was an error during the deletion of this image repository, please try again."
msgstr ""
msgid "ContainerRegistry|This image has no active tags"
msgstr ""
-msgid "ContainerRegistry|We are having trouble connecting to Docker, which could be due to an issue with your project name or path. %{docLinkStart}More Information%{docLinkEnd}"
+msgid "ContainerRegistry|This image repository is scheduled for deletion"
msgstr ""
-msgid "ContainerRegistry|Wildcards such as %{codeStart}.*-stable%{codeEnd} or %{codeStart}production/.*%{codeEnd} are supported. To select all tags, use %{codeStart}.*%{codeEnd}"
+msgid "ContainerRegistry|We are having trouble connecting to Docker, which could be due to an issue with your project name or path. %{docLinkStart}More Information%{docLinkEnd}"
msgstr ""
msgid "ContainerRegistry|With the Container Registry, every project can have its own space to store its Docker images. %{docLinkStart}More Information%{docLinkEnd}"
@@ -5853,7 +6055,7 @@ msgstr ""
msgid "Could not delete chat nickname %{chat_name}."
msgstr ""
-msgid "Could not find design"
+msgid "Could not find design."
msgstr ""
msgid "Could not remove the trigger."
@@ -5868,6 +6070,9 @@ msgstr ""
msgid "Could not revoke personal access token %{personal_access_token_name}."
msgstr ""
+msgid "Could not revoke project access token %{project_access_token_name}."
+msgstr ""
+
msgid "Could not save group ID"
msgstr ""
@@ -5925,6 +6130,9 @@ msgstr ""
msgid "Create a new branch"
msgstr ""
+msgid "Create a new deploy key for this project"
+msgstr ""
+
msgid "Create a new file as there are no files yet. Afterwards, you'll be able to commit your changes."
msgstr ""
@@ -6030,6 +6238,9 @@ msgstr ""
msgid "Create requirement"
msgstr ""
+msgid "Create snippet"
+msgstr ""
+
msgid "Create wildcard: %{searchTerm}"
msgstr ""
@@ -6051,6 +6262,9 @@ msgstr ""
msgid "Created"
msgstr ""
+msgid "Created %{timestamp}"
+msgstr ""
+
msgid "Created At"
msgstr ""
@@ -6407,6 +6621,9 @@ msgstr ""
msgid "CycleAnalytics|stage dropdown"
msgstr ""
+msgid "DAG"
+msgstr ""
+
msgid "DNS"
msgstr ""
@@ -6773,6 +6990,9 @@ msgstr ""
msgid "Deploy key was successfully updated."
msgstr ""
+msgid "Deploy keys allow read-only or read-write (if enabled) access to your repository. Deploy keys can be used for CI, staging or production servers. You can create a deploy key or add an existing one."
+msgstr ""
+
msgid "Deploy progress not found. To see pods, ensure your environment matches %{linkStart}deploy board criteria%{linkEnd}."
msgstr ""
@@ -6926,6 +7146,9 @@ msgstr ""
msgid "Deploying to"
msgstr ""
+msgid "Deployment Frequency"
+msgstr ""
+
msgid "Deployment|API"
msgstr ""
@@ -7444,9 +7667,6 @@ msgstr ""
msgid "Edit environment"
msgstr ""
-msgid "Edit epic description"
-msgstr ""
-
msgid "Edit file"
msgstr ""
@@ -8068,9 +8288,6 @@ msgstr ""
msgid "Epics"
msgstr ""
-msgid "Epics (Ultimate / Gold license only)"
-msgstr ""
-
msgid "Epics Roadmap"
msgstr ""
@@ -8080,6 +8297,9 @@ msgstr ""
msgid "Epics let you manage your portfolio of projects more efficiently and with less effort"
msgstr ""
+msgid "Epics, Issues, and Merge Requests"
+msgstr ""
+
msgid "Epics|Add an epic"
msgstr ""
@@ -8131,6 +8351,9 @@ msgstr ""
msgid "Epics|Something went wrong while fetching group epics."
msgstr ""
+msgid "Epics|Something went wrong while moving item."
+msgstr ""
+
msgid "Epics|Something went wrong while ordering item."
msgstr ""
@@ -8185,6 +8408,9 @@ msgstr ""
msgid "Error fetching network graph."
msgstr ""
+msgid "Error fetching payload data."
+msgstr ""
+
msgid "Error fetching projects"
msgstr ""
@@ -8194,9 +8420,6 @@ msgstr ""
msgid "Error fetching the dependency list. Please check your network connection and try again."
msgstr ""
-msgid "Error fetching usage ping data."
-msgstr ""
-
msgid "Error loading branch data. Please try again."
msgstr ""
@@ -8338,6 +8561,9 @@ msgstr ""
msgid "Errors"
msgstr ""
+msgid "Errors:"
+msgstr ""
+
msgid "Estimated"
msgstr ""
@@ -8542,12 +8768,18 @@ msgstr ""
msgid "Export as CSV"
msgstr ""
+msgid "Export group"
+msgstr ""
+
msgid "Export issues"
msgstr ""
msgid "Export project"
msgstr ""
+msgid "Export this group with all related data to a new GitLab instance. Once complete, you can import the data file from the \"New Group\" page."
+msgstr ""
+
msgid "Export this project with all its related data in order to move your project to a new GitLab instance. Once the export is finished, you can import the file from the \"New Project\" page."
msgstr ""
@@ -8617,9 +8849,6 @@ msgstr ""
msgid "Failed Jobs"
msgstr ""
-msgid "Failed create wiki"
-msgstr ""
-
msgid "Failed to add a Zoom meeting"
msgstr ""
@@ -8659,6 +8888,9 @@ msgstr ""
msgid "Failed to create resources"
msgstr ""
+msgid "Failed to create wiki"
+msgstr ""
+
msgid "Failed to delete board. Please try again."
msgstr ""
@@ -8668,7 +8900,7 @@ msgstr ""
msgid "Failed to enqueue the rebase operation, possibly due to a long-lived transaction. Try again later."
msgstr ""
-msgid "Failed to find import label for jira import."
+msgid "Failed to find import label for Jira import."
msgstr ""
msgid "Failed to get ref."
@@ -8692,6 +8924,12 @@ msgstr ""
msgid "Failed to load groups & users."
msgstr ""
+msgid "Failed to load labels. Please try again."
+msgstr ""
+
+msgid "Failed to load milestones. Please try again."
+msgstr ""
+
msgid "Failed to load related branches"
msgstr ""
@@ -9067,7 +9305,10 @@ msgstr ""
msgid "Filter by milestone name"
msgstr ""
-msgid "Filter by name..."
+msgid "Filter by name"
+msgstr ""
+
+msgid "Filter by status"
msgstr ""
msgid "Filter by two-factor authentication"
@@ -9079,12 +9320,18 @@ msgstr ""
msgid "Filter projects"
msgstr ""
+msgid "Filter results"
+msgstr ""
+
msgid "Filter results by group"
msgstr ""
msgid "Filter results by project"
msgstr ""
+msgid "Filter results..."
+msgstr ""
+
msgid "Filter your projects by name"
msgstr ""
@@ -9373,10 +9620,10 @@ msgstr ""
msgid "GeoNodes|Checksummed"
msgstr ""
-msgid "GeoNodes|Container repositories"
+msgid "GeoNodes|Consult Geo troubleshooting information"
msgstr ""
-msgid "GeoNodes|Data is out of date from %{timeago}"
+msgid "GeoNodes|Container repositories"
msgstr ""
msgid "GeoNodes|Data replication lag"
@@ -9418,6 +9665,9 @@ msgstr ""
msgid "GeoNodes|Last event ID seen from primary"
msgstr ""
+msgid "GeoNodes|Learn more about Geo node statuses"
+msgstr ""
+
msgid "GeoNodes|Loading nodes"
msgstr ""
@@ -9433,6 +9683,9 @@ msgstr ""
msgid "GeoNodes|Node was successfully removed."
msgstr ""
+msgid "GeoNodes|Node's status was updated %{timeAgo}."
+msgstr ""
+
msgid "GeoNodes|Not checksummed"
msgstr ""
@@ -9463,7 +9716,10 @@ msgstr ""
msgid "GeoNodes|Repository verification progress"
msgstr ""
-msgid "GeoNodes|Selective"
+msgid "GeoNodes|Selective (%{syncLabel})"
+msgstr ""
+
+msgid "GeoNodes|Selective synchronization"
msgstr ""
msgid "GeoNodes|Something went wrong while changing node status"
@@ -9490,6 +9746,9 @@ msgstr ""
msgid "GeoNodes|Unverified"
msgstr ""
+msgid "GeoNodes|Updated %{timeAgo}"
+msgstr ""
+
msgid "GeoNodes|Used slots"
msgstr ""
@@ -9529,16 +9788,16 @@ msgstr ""
msgid "Geo|All"
msgstr ""
-msgid "Geo|All projects"
+msgid "Geo|All %{replicable_type}"
msgstr ""
-msgid "Geo|All projects are being scheduled for re-sync"
+msgid "Geo|All projects"
msgstr ""
-msgid "Geo|All projects are being scheduled for re-verify"
+msgid "Geo|All projects are being scheduled for resync"
msgstr ""
-msgid "Geo|Batch operations"
+msgid "Geo|All projects are being scheduled for reverify"
msgstr ""
msgid "Geo|Could not remove tracking entry for an existing project."
@@ -9550,9 +9809,15 @@ msgstr ""
msgid "Geo|Failed"
msgstr ""
+msgid "Geo|Filter by status"
+msgstr ""
+
msgid "Geo|Geo Status"
msgstr ""
+msgid "Geo|In progress"
+msgstr ""
+
msgid "Geo|In sync"
msgstr ""
@@ -9577,9 +9842,6 @@ msgstr ""
msgid "Geo|Not synced yet"
msgstr ""
-msgid "Geo|Pending"
-msgstr ""
-
msgid "Geo|Pending synchronization"
msgstr ""
@@ -9616,7 +9878,7 @@ msgstr ""
msgid "Geo|Resync"
msgstr ""
-msgid "Geo|Resync all projects"
+msgid "Geo|Resync all"
msgstr ""
msgid "Geo|Retry count"
@@ -9625,7 +9887,7 @@ msgstr ""
msgid "Geo|Reverify"
msgstr ""
-msgid "Geo|Reverify all projects"
+msgid "Geo|Reverify all"
msgstr ""
msgid "Geo|Status"
@@ -9967,9 +10229,6 @@ msgstr ""
msgid "Go to file"
msgstr ""
-msgid "Go to file (MRs only)"
-msgstr ""
-
msgid "Go to file permalink (while viewing a file)"
msgstr ""
@@ -10414,6 +10673,9 @@ msgstr ""
msgid "GroupSettings|Disable group mentions"
msgstr ""
+msgid "GroupSettings|Export group"
+msgstr ""
+
msgid "GroupSettings|If the parent group's visibility is lower than the group current visibility, visibility levels for subgroups and projects will be changed to match the new parent group's visibility."
msgstr ""
@@ -10492,6 +10754,9 @@ msgstr ""
msgid "Groups (%{count})"
msgstr ""
+msgid "Groups (%{groups})"
+msgstr ""
+
msgid "Groups can also be nested by creating %{subgroup_docs_link_start}subgroups%{subgroup_docs_link_end}."
msgstr ""
@@ -10757,15 +11022,12 @@ msgstr ""
msgid "ID:"
msgstr ""
-msgid "IDE|Allow live previews of JavaScript projects in the Web IDE using CodeSandbox client side evaluation."
+msgid "IDE|Allow live previews of JavaScript projects in the Web IDE using CodeSandbox Live Preview."
msgstr ""
msgid "IDE|Back"
msgstr ""
-msgid "IDE|Client side evaluation"
-msgstr ""
-
msgid "IDE|Commit"
msgstr ""
@@ -10829,6 +11091,9 @@ msgstr ""
msgid "If any job surpasses this timeout threshold, it will be marked as failed. Human readable time input language is accepted like \"1 hour\". Values without specification represent seconds."
msgstr ""
+msgid "If blank, set allowable lifetime to %{instance_level_policy_in_words}, as defined by the instance admin. Once set, existing tokens for users in this group may be revoked."
+msgstr ""
+
msgid "If checked, group owners can manage LDAP group links and LDAP member overrides"
msgstr ""
@@ -10868,6 +11133,9 @@ msgstr ""
msgid "If you lose your recovery codes you can generate new ones, invalidating all previous codes."
msgstr ""
+msgid "If you reach 100%% storage capacity, you will not be able to: %{base_message}"
+msgstr ""
+
msgid "If your HTTP repository is not publicly accessible, add your credentials."
msgstr ""
@@ -10979,6 +11247,9 @@ msgstr ""
msgid "Import repository"
msgstr ""
+msgid "Import started by: %{importInitiator}"
+msgstr ""
+
msgid "Import tasks"
msgstr ""
@@ -11045,6 +11316,9 @@ msgstr ""
msgid "In order to tailor your experience with GitLab we<br>would like to know a bit more about you."
msgstr ""
+msgid "In progress"
+msgstr ""
+
msgid "In the next step, you'll be able to select the projects you want to import."
msgstr ""
@@ -11392,15 +11666,15 @@ msgstr ""
msgid "Issues"
msgstr ""
-msgid "Issues / Merge Requests"
-msgstr ""
-
msgid "Issues Analytics"
msgstr ""
msgid "Issues Rate Limits"
msgstr ""
+msgid "Issues and Merge Requests"
+msgstr ""
+
msgid "Issues can be bugs, tasks or ideas to be discussed. Also, issues are searchable and filterable."
msgstr ""
@@ -11422,10 +11696,10 @@ msgstr ""
msgid "IssuesAnalytics|Avg/Month:"
msgstr ""
-msgid "IssuesAnalytics|Issues created"
+msgid "IssuesAnalytics|Issues opened"
msgstr ""
-msgid "IssuesAnalytics|Issues created per month"
+msgid "IssuesAnalytics|Issues opened per month"
msgstr ""
msgid "IssuesAnalytics|Last 12 months"
@@ -11479,6 +11753,9 @@ msgstr ""
msgid "Jira integration not configured."
msgstr ""
+msgid "Jira project: %{importProject}"
+msgstr ""
+
msgid "JiraService|Events for %{noteable_model_name} are disabled."
msgstr ""
@@ -11868,6 +12145,9 @@ msgstr ""
msgid "Lead"
msgstr ""
+msgid "Lead Time"
+msgstr ""
+
msgid "Learn GitLab"
msgstr ""
@@ -11880,6 +12160,9 @@ msgstr ""
msgid "Learn how to %{no_packages_link_start}publish and share your packages%{no_packages_link_end} with GitLab."
msgstr ""
+msgid "Learn how to enable synchronization"
+msgstr ""
+
msgid "Learn more"
msgstr ""
@@ -12211,9 +12494,6 @@ msgstr ""
msgid "Live preview"
msgstr ""
-msgid "Load more vulnerabilities"
-msgstr ""
-
msgid "Loading"
msgstr ""
@@ -12364,9 +12644,6 @@ msgstr ""
msgid "Manage"
msgstr ""
-msgid "Manage Git repositories with fine-grained access controls that keep your code secure. Perform code reviews and enhance collaboration with merge requests. Each project can also have an issue tracker and a wiki."
-msgstr ""
-
msgid "Manage Web IDE features"
msgstr ""
@@ -12397,6 +12674,9 @@ msgstr ""
msgid "Manage your license"
msgstr ""
+msgid "Managed Account"
+msgstr ""
+
msgid "Manifest"
msgstr ""
@@ -12661,6 +12941,9 @@ msgstr ""
msgid "Merge Requests in Review"
msgstr ""
+msgid "Merge automatically (%{strategy})"
+msgstr ""
+
msgid "Merge commit message"
msgstr ""
@@ -12814,6 +13097,12 @@ msgstr ""
msgid "Merged branches are being deleted. This can take some time depending on the number of branches. Please refresh the page to see changes."
msgstr ""
+msgid "Merged this merge request."
+msgstr ""
+
+msgid "Merges this merge request immediately."
+msgstr ""
+
msgid "Merges this merge request when the pipeline succeeds."
msgstr ""
@@ -12921,12 +13210,15 @@ msgid_plural "Metrics|Edit metrics"
msgstr[0] ""
msgstr[1] ""
-msgid "Metrics|Environment"
+msgid "Metrics|Expand panel"
msgstr ""
msgid "Metrics|For grouping similar metrics"
msgstr ""
+msgid "Metrics|Go back (Esc)"
+msgstr ""
+
msgid "Metrics|Invalid time range, please verify."
msgstr ""
@@ -12957,9 +13249,6 @@ msgstr ""
msgid "Metrics|Refresh dashboard"
msgstr ""
-msgid "Metrics|Show last"
-msgstr ""
-
msgid "Metrics|There was an error creating the dashboard."
msgstr ""
@@ -13038,6 +13327,9 @@ msgstr ""
msgid "Microsoft Azure"
msgstr ""
+msgid "Middleman project with Static Site Editor support"
+msgstr ""
+
msgid "Migrated %{success_count}/%{total_count} files."
msgstr ""
@@ -13483,10 +13775,10 @@ msgstr ""
msgid "Next"
msgstr ""
-msgid "Next file in diff (MRs only)"
+msgid "Next file in diff"
msgstr ""
-msgid "Next unresolved discussion (MRs only)"
+msgid "Next unresolved discussion"
msgstr ""
msgid "Nickname"
@@ -13546,6 +13838,9 @@ msgstr ""
msgid "No changes between %{ref_start}%{source_branch}%{ref_end} and %{ref_start}%{target_branch}%{ref_end}"
msgstr ""
+msgid "No child epics match applied filters"
+msgstr ""
+
msgid "No connection could be made to a Gitaly Server, please check your logs!"
msgstr ""
@@ -13684,15 +13979,6 @@ msgstr ""
msgid "No thanks, don't show this again"
msgstr ""
-msgid "No vulnerabilities found for this group"
-msgstr ""
-
-msgid "No vulnerabilities found for this pipeline"
-msgstr ""
-
-msgid "No vulnerabilities found for this project"
-msgstr ""
-
msgid "No vulnerabilities present"
msgstr ""
@@ -13816,6 +14102,9 @@ msgstr ""
msgid "Nothing to preview."
msgstr ""
+msgid "Nothing to synchronize"
+msgstr ""
+
msgid "Notification events"
msgstr ""
@@ -13987,6 +14276,9 @@ msgstr ""
msgid "Once removed, the fork relationship cannot be restored and you will no longer be able to send merge requests to the source."
msgstr ""
+msgid "Once the exported file is ready you can download it from this page."
+msgstr ""
+
msgid "Once the exported file is ready, you will receive a notification email with a download link, or you can download it from this page."
msgstr ""
@@ -14088,9 +14380,6 @@ msgstr ""
msgid "Open sidebar"
msgstr ""
-msgid "Open source software to collaborate on code"
-msgstr ""
-
msgid "Open: %{openIssuesCount}"
msgstr ""
@@ -14193,6 +14482,9 @@ msgstr ""
msgid "OutdatedBrowser|You can provide feedback %{feedback_link_start}on this issue%{feedback_link_end} or via your usual support channels."
msgstr ""
+msgid "Overridden"
+msgstr ""
+
msgid "Overview"
msgstr ""
@@ -14244,6 +14536,9 @@ msgstr ""
msgid "PackageRegistry|Conan Command"
msgstr ""
+msgid "PackageRegistry|Copy .pypirc content"
+msgstr ""
+
msgid "PackageRegistry|Copy Conan Command"
msgstr ""
@@ -14265,6 +14560,9 @@ msgstr ""
msgid "PackageRegistry|Copy NuGet Setup Command"
msgstr ""
+msgid "PackageRegistry|Copy Pip command"
+msgstr ""
+
msgid "PackageRegistry|Copy and paste this inside your %{codeStart}pom.xml%{codeEnd} %{codeStart}dependencies%{codeEnd} block."
msgstr ""
@@ -14298,6 +14596,12 @@ msgstr ""
msgid "PackageRegistry|For more information on the NuGet registry, %{linkStart}see the documentation%{linkEnd}."
msgstr ""
+msgid "PackageRegistry|For more information on the PyPi registry, %{linkStart}see the documentation%{linkEnd}."
+msgstr ""
+
+msgid "PackageRegistry|If you haven't already done so, you will need to add the below to your %{codeStart}.pypirc%{codeEnd} file."
+msgstr ""
+
msgid "PackageRegistry|If you haven't already done so, you will need to add the below to your %{codeStart}pom.xml%{codeEnd} file."
msgstr ""
@@ -14307,6 +14611,9 @@ msgstr ""
msgid "PackageRegistry|Learn how to %{noPackagesLinkStart}publish and share your packages%{noPackagesLinkEnd} with GitLab."
msgstr ""
+msgid "PackageRegistry|Manually Published"
+msgstr ""
+
msgid "PackageRegistry|Maven"
msgstr ""
@@ -14325,12 +14632,18 @@ msgstr ""
msgid "PackageRegistry|NuGet Command"
msgstr ""
+msgid "PackageRegistry|Pip Command"
+msgstr ""
+
msgid "PackageRegistry|Pipeline %{linkStart}%{linkEnd} triggered %{timestamp} by %{author}"
msgstr ""
msgid "PackageRegistry|Published to the repository at %{timestamp}"
msgstr ""
+msgid "PackageRegistry|PyPi"
+msgstr ""
+
msgid "PackageRegistry|Registry Setup"
msgstr ""
@@ -14367,6 +14680,9 @@ msgstr ""
msgid "PackageRegistry|npm"
msgstr ""
+msgid "PackageRegistry|published by %{author}"
+msgstr ""
+
msgid "PackageRegistry|yarn"
msgstr ""
@@ -14382,6 +14698,9 @@ msgstr ""
msgid "PackageType|NuGet"
msgstr ""
+msgid "PackageType|PyPi"
+msgstr ""
+
msgid "Packages"
msgstr ""
@@ -14475,6 +14794,9 @@ msgstr ""
msgid "Past due"
msgstr ""
+msgid "Paste a machine public key here. Read more about how to generate it"
+msgstr ""
+
msgid "Paste a machine public key here. Read more about how to generate it %{link_start}here%{link_end}"
msgstr ""
@@ -14490,9 +14812,6 @@ msgstr ""
msgid "Path"
msgstr ""
-msgid "Path, transfer, remove"
-msgstr ""
-
msgid "Path:"
msgstr ""
@@ -14523,7 +14842,7 @@ msgstr ""
msgid "Percentage"
msgstr ""
-msgid "Perform advanced options such as changing path, transferring, or removing the group."
+msgid "Perform advanced options such as changing path, transferring, exporting, or removing the group."
msgstr ""
msgid "Perform common operations on GitLab project"
@@ -14604,9 +14923,6 @@ msgstr ""
msgid "Pipeline minutes quota"
msgstr ""
-msgid "Pipeline quota"
-msgstr ""
-
msgid "Pipeline subscriptions"
msgstr ""
@@ -14616,6 +14932,9 @@ msgstr ""
msgid "Pipeline: %{status}"
msgstr ""
+msgid "PipelineCharts|CI / CD Analytics"
+msgstr ""
+
msgid "PipelineCharts|Failed:"
msgstr ""
@@ -14694,15 +15013,6 @@ msgstr ""
msgid "Pipelines settings for '%{project_name}' were successfully updated."
msgstr ""
-msgid "Pipelines| to purchase more minutes."
-msgstr ""
-
-msgid "Pipelines|%{namespace_name} has exceeded its pipeline minutes quota."
-msgstr ""
-
-msgid "Pipelines|%{namespace_name} has less than %{notification_level}%% of CI minutes available."
-msgstr ""
-
msgid "Pipelines|API"
msgstr ""
@@ -14724,10 +15034,13 @@ msgstr ""
msgid "Pipelines|Get started with Pipelines"
msgstr ""
-msgid "Pipelines|Loading Pipelines"
+msgid "Pipelines|Group %{namespace_name} has %{percentage}%% or less Shared Runner Pipeline minutes remaining. Once it runs out, no new jobs or pipelines in its projects will run."
msgstr ""
-msgid "Pipelines|Pipelines will not run anymore on shared Runners."
+msgid "Pipelines|Group %{namespace_name} has exceeded its pipeline minutes quota. Unless you buy additional pipeline minutes, no new jobs or pipelines in its projects will run."
+msgstr ""
+
+msgid "Pipelines|Loading Pipelines"
msgstr ""
msgid "Pipelines|Project cache successfully reset."
@@ -14958,6 +15271,9 @@ msgstr ""
msgid "Please select"
msgstr ""
+msgid "Please select a Jira project"
+msgstr ""
+
msgid "Please select a country"
msgstr ""
@@ -15003,6 +15319,9 @@ msgstr ""
msgid "Please wait while we import the repository for you. Refresh at will."
msgstr ""
+msgid "Plugins directory is deprecated and will be removed in 14.0. Please move this file into /file_hooks directory."
+msgstr ""
+
msgid "Pod does not exist"
msgstr ""
@@ -15120,7 +15439,7 @@ msgstr ""
msgid "Prevent users from changing their profile name"
msgstr ""
-msgid "Prevent users from modifing merge request approvers list"
+msgid "Prevent users from modifying merge request approvers list"
msgstr ""
msgid "Prevent users from performing write operations on GitLab while performing maintenance."
@@ -15141,10 +15460,10 @@ msgstr ""
msgid "Previous Artifacts"
msgstr ""
-msgid "Previous file in diff (MRs only)"
+msgid "Previous file in diff"
msgstr ""
-msgid "Previous unresolved discussion (MRs only)"
+msgid "Previous unresolved discussion"
msgstr ""
msgid "Primary"
@@ -15177,6 +15496,9 @@ msgstr ""
msgid "Private profile"
msgstr ""
+msgid "Private projects Minutes cost factor"
+msgstr ""
+
msgid "Private projects can be created in your personal namespace with:"
msgstr ""
@@ -16005,6 +16327,9 @@ msgstr ""
msgid "ProjectSettings|With GitLab Pages you can host your static websites on GitLab"
msgstr ""
+msgid "ProjectSettings|With Metrics Dashboard you can visualize this project performance metrics"
+msgstr ""
+
msgid "ProjectTemplates|.NET Core"
msgstr ""
@@ -16068,6 +16393,9 @@ msgstr ""
msgid "ProjectTemplates|Spring"
msgstr ""
+msgid "ProjectTemplates|Static Site Editor/Middleman"
+msgstr ""
+
msgid "ProjectTemplates|iOS (Swift)"
msgstr ""
@@ -16287,13 +16615,13 @@ msgstr ""
msgid "PrometheusService|Prometheus is being automatically managed on your clusters"
msgstr ""
-msgid "PrometheusService|These metrics will only be monitored after your first deployment to an environment"
+msgid "PrometheusService|Select the Active checkbox to override the Auto Configuration with custom settings. If unchecked, Auto Configuration settings are used."
msgstr ""
-msgid "PrometheusService|Time-series monitoring service"
+msgid "PrometheusService|These metrics will only be monitored after your first deployment to an environment"
msgstr ""
-msgid "PrometheusService|To enable manual configuration, uninstall Prometheus from your clusters"
+msgid "PrometheusService|Time-series monitoring service"
msgstr ""
msgid "PrometheusService|To enable the installation of Prometheus on your clusters, deactivate the manual configuration below"
@@ -16479,6 +16807,9 @@ msgstr ""
msgid "Protip:"
msgstr ""
+msgid "Protocol"
+msgstr ""
+
msgid "Provider"
msgstr ""
@@ -16503,6 +16834,9 @@ msgstr ""
msgid "Public pipelines"
msgstr ""
+msgid "Public projects Minutes cost factor"
+msgstr ""
+
msgid "Pull"
msgstr ""
@@ -16703,6 +17037,9 @@ msgid_plural "Refreshing in %d seconds to show the updated status..."
msgstr[0] ""
msgstr[1] ""
+msgid "Regenerate export"
+msgstr ""
+
msgid "Regenerate instance ID"
msgstr ""
@@ -17080,9 +17417,29 @@ msgstr ""
msgid "Reports|%{combinedString} and %{resolvedString}"
msgstr ""
+msgid "Reports|Accessibility scanning detected %d issue for the source branch only"
+msgid_plural "Reports|Accessibility scanning detected %d issues for the source branch only"
+msgstr[0] ""
+msgstr[1] ""
+
+msgid "Reports|Accessibility scanning detected no issues for the source branch only"
+msgstr ""
+
+msgid "Reports|Accessibility scanning failed loading results"
+msgstr ""
+
+msgid "Reports|Accessibility scanning results are being parsed"
+msgstr ""
+
msgid "Reports|Actions"
msgstr ""
+msgid "Reports|An error occured while loading report"
+msgstr ""
+
+msgid "Reports|An error occurred while loading %{name} results"
+msgstr ""
+
msgid "Reports|Class"
msgstr ""
@@ -17188,7 +17545,7 @@ msgstr ""
msgid "Requested %{time_ago}"
msgstr ""
-msgid "Requested design version does not exist"
+msgid "Requested design version does not exist."
msgstr ""
msgid "Requested states are invalid"
@@ -17372,6 +17729,9 @@ msgstr ""
msgid "Resync"
msgstr ""
+msgid "Resync all"
+msgstr ""
+
msgid "Resync all %{replicableType}"
msgstr ""
@@ -17440,6 +17800,9 @@ msgstr ""
msgid "Revoked personal access token %{personal_access_token_name}!"
msgstr ""
+msgid "Revoked project access token %{project_access_token_name}!"
+msgstr ""
+
msgid "RightSidebar|adding a"
msgstr ""
@@ -17488,6 +17851,9 @@ msgstr ""
msgid "Runner token"
msgstr ""
+msgid "Runner tokens"
+msgstr ""
+
msgid "Runner was not updated."
msgstr ""
@@ -17542,6 +17908,9 @@ msgstr ""
msgid "SAML SSO for %{group_name}"
msgstr ""
+msgid "SAML discovery tokens"
+msgstr ""
+
msgid "SAML for %{group_name}"
msgstr ""
@@ -17626,12 +17995,18 @@ msgstr ""
msgid "Scheduled"
msgstr ""
+msgid "Scheduled to merge this merge request (%{strategy})."
+msgstr ""
+
msgid "Scheduled to merge this merge request when the pipeline succeeds."
msgstr ""
msgid "Schedules"
msgstr ""
+msgid "Schedules to merge this merge request (%{strategy})."
+msgstr ""
+
msgid "Scheduling"
msgstr ""
@@ -17689,6 +18064,9 @@ msgstr ""
msgid "Search branches and tags"
msgstr ""
+msgid "Search by author"
+msgstr ""
+
msgid "Search files"
msgstr ""
@@ -17834,11 +18212,6 @@ msgid_plural "SearchResults|snippets"
msgstr[0] ""
msgstr[1] ""
-msgid "SearchResults|snippet result"
-msgid_plural "SearchResults|snippet results"
-msgstr[0] ""
-msgstr[1] ""
-
msgid "SearchResults|user"
msgid_plural "SearchResults|users"
msgstr[0] ""
@@ -17849,6 +18222,9 @@ msgid_plural "SearchResults|wiki results"
msgstr[0] ""
msgstr[1] ""
+msgid "Searching by both author and message is currently not supported."
+msgstr ""
+
msgid "Seat Link"
msgstr ""
@@ -17876,184 +18252,232 @@ msgstr ""
msgid "Security Dashboard"
msgstr ""
-msgid "Security Dashboard|Error fetching the vulnerability counts. Please check your network connection and try again."
+msgid "Security configuration help link"
+msgstr ""
+
+msgid "Security dashboard"
msgstr ""
-msgid "Security Dashboard|Error fetching the vulnerability list. Please check your network connection and try again."
+msgid "Security report is out of date. Please update your branch with the latest changes from the target branch (%{targetBranchName})"
msgstr ""
-msgid "Security Dashboard|Issue Created"
+msgid "Security report is out of date. Run %{newPipelineLinkStart}a new pipeline%{newPipelineLinkEnd} for the target branch (%{targetBranchName})"
msgstr ""
-msgid "Security Reports|Comment added to '%{vulnerabilityName}'"
+msgid "SecurityConfiguration|Configured"
msgstr ""
-msgid "Security Reports|Comment deleted on '%{vulnerabilityName}'"
+msgid "SecurityConfiguration|Feature"
msgstr ""
-msgid "Security Reports|Comment edited on '%{vulnerabilityName}'"
+msgid "SecurityConfiguration|Feature documentation for %{featureName}"
msgstr ""
-msgid "Security Reports|Create issue"
+msgid "SecurityConfiguration|Not yet configured"
msgstr ""
-msgid "Security Reports|Dismiss vulnerability"
+msgid "SecurityConfiguration|Secure features"
msgstr ""
-msgid "Security Reports|Dismissed '%{vulnerabilityName}'"
+msgid "SecurityConfiguration|Status"
msgstr ""
-msgid "Security Reports|Dismissed '%{vulnerabilityName}'. Turn off the hide dismissed toggle to view."
+msgid "SecurityReports|%{firstProject} and %{secondProject}"
msgstr ""
-msgid "Security Reports|Either you don't have permission to view this dashboard or the dashboard has not been setup. Please check your permission settings with your administrator or check your dashboard configurations to proceed."
+msgid "SecurityReports|%{firstProject}, %{secondProject}, and %{rest}"
msgstr ""
-msgid "Security Reports|Learn more about setting up your dashboard"
+msgid "SecurityReports|Add a project to your dashboard"
msgstr ""
-msgid "Security Reports|More info"
+msgid "SecurityReports|Add or remove projects from your dashboard"
msgstr ""
-msgid "Security Reports|Oops, something doesn't seem right."
+msgid "SecurityReports|Add projects"
msgstr ""
-msgid "Security Reports|Security reports can only be accessed by authorized users."
+msgid "SecurityReports|Comment added to '%{vulnerabilityName}'"
msgstr ""
-msgid "Security Reports|There was an error adding the comment."
+msgid "SecurityReports|Comment deleted on '%{vulnerabilityName}'"
msgstr ""
-msgid "Security Reports|There was an error creating the issue."
+msgid "SecurityReports|Comment edited on '%{vulnerabilityName}'"
msgstr ""
-msgid "Security Reports|There was an error creating the merge request."
+msgid "SecurityReports|Create issue"
msgstr ""
-msgid "Security Reports|There was an error deleting the comment."
+msgid "SecurityReports|Dismiss Selected"
msgstr ""
-msgid "Security Reports|There was an error dismissing the vulnerabilities."
+msgid "SecurityReports|Dismiss vulnerability"
msgstr ""
-msgid "Security Reports|There was an error dismissing the vulnerability."
+msgid "SecurityReports|Dismissed '%{vulnerabilityName}'"
msgstr ""
-msgid "Security Reports|There was an error reverting the dismissal."
+msgid "SecurityReports|Dismissed '%{vulnerabilityName}'. Turn off the hide dismissed toggle to view."
msgstr ""
-msgid "Security Reports|There was an error reverting this dismissal."
+msgid "SecurityReports|Each vulnerability now has a unique page that can be directly linked to, shared, referenced, and tracked as the single source of truth. Vulnerability occurrences also persist across scanner runs, which improves tracking and visibility and reduces duplicates between scans."
msgstr ""
-msgid "Security Reports|Undo dismiss"
+msgid "SecurityReports|Edit dashboard"
msgstr ""
-msgid "Security Reports|You do not have sufficient permissions to access this report"
+msgid "SecurityReports|Either you don't have permission to view this dashboard or the dashboard has not been setup. Please check your permission settings with your administrator or check your dashboard configurations to proceed."
msgstr ""
-msgid "Security Reports|You must sign in as an authorized user to see this report"
+msgid "SecurityReports|Error fetching the vulnerability counts. Please check your network connection and try again."
msgstr ""
-msgid "Security configuration help link"
+msgid "SecurityReports|Error fetching the vulnerability list. Please check your network connection and try again."
msgstr ""
-msgid "Security dashboard"
+msgid "SecurityReports|False positive"
msgstr ""
-msgid "Security report is out of date. Please update your branch with the latest changes from the target branch (%{targetBranchName})"
+msgid "SecurityReports|Hide dismissed"
msgstr ""
-msgid "Security report is out of date. Run %{newPipelineLinkStart}a new pipeline%{newPipelineLinkEnd} for the target branch (%{targetBranchName})"
+msgid "SecurityReports|Introducing standalone vulnerabilities"
msgstr ""
-msgid "SecurityConfiguration|Configured"
+msgid "SecurityReports|Issue Created"
msgstr ""
-msgid "SecurityConfiguration|Feature"
+msgid "SecurityReports|Learn More"
msgstr ""
-msgid "SecurityConfiguration|Feature documentation for %{featureName}"
+msgid "SecurityReports|Learn more about setting up your dashboard"
msgstr ""
-msgid "SecurityConfiguration|Not yet configured"
+msgid "SecurityReports|Load more vulnerabilities"
msgstr ""
-msgid "SecurityConfiguration|Secure features"
+msgid "SecurityReports|Monitor vulnerabilities in your code"
msgstr ""
-msgid "SecurityConfiguration|Status"
+msgid "SecurityReports|More info"
msgstr ""
-msgid "SecurityDashboard|%{firstProject} and %{secondProject}"
+msgid "SecurityReports|More information"
msgstr ""
-msgid "SecurityDashboard|%{firstProject}, %{secondProject}, and %{rest}"
+msgid "SecurityReports|No vulnerabilities found for dashboard"
msgstr ""
-msgid "SecurityDashboard|Add a project to your dashboard"
+msgid "SecurityReports|No vulnerabilities found for this group"
msgstr ""
-msgid "SecurityDashboard|Add or remove projects from your dashboard"
+msgid "SecurityReports|No vulnerabilities found for this pipeline"
msgstr ""
-msgid "SecurityDashboard|Add projects"
+msgid "SecurityReports|No vulnerabilities found for this project"
msgstr ""
-msgid "SecurityDashboard|Edit dashboard"
+msgid "SecurityReports|Oops, something doesn't seem right."
msgstr ""
-msgid "SecurityDashboard|Hide dismissed"
+msgid "SecurityReports|Pipeline %{pipelineLink} triggered %{timeago} by %{user}"
msgstr ""
-msgid "SecurityDashboard|Monitor vulnerabilities in your code"
+msgid "SecurityReports|Project"
msgstr ""
-msgid "SecurityDashboard|More information"
+msgid "SecurityReports|Projects added"
msgstr ""
-msgid "SecurityDashboard|No vulnerabilities found for dashboard"
+msgid "SecurityReports|Remove project from dashboard"
msgstr ""
-msgid "SecurityDashboard|Pipeline %{pipelineLink} triggered %{timeago} by %{user}"
+msgid "SecurityReports|Report type"
msgstr ""
-msgid "SecurityDashboard|Project"
+msgid "SecurityReports|Return to dashboard"
msgstr ""
-msgid "SecurityDashboard|Projects added"
+msgid "SecurityReports|Security Dashboard"
msgstr ""
-msgid "SecurityDashboard|Remove project from dashboard"
+msgid "SecurityReports|Security reports can only be accessed by authorized users."
msgstr ""
-msgid "SecurityDashboard|Report type"
+msgid "SecurityReports|Select a project to add by using the project search field above."
msgstr ""
-msgid "SecurityDashboard|Return to dashboard"
+msgid "SecurityReports|Select a reason"
msgstr ""
-msgid "SecurityDashboard|Security Dashboard"
+msgid "SecurityReports|Severity"
msgstr ""
-msgid "SecurityDashboard|Select a project to add by using the project search field above."
+msgid "SecurityReports|Status"
msgstr ""
-msgid "SecurityDashboard|Severity"
+msgid "SecurityReports|The security dashboard displays the latest security findings for projects you wish to monitor. Select \"Edit dashboard\" to add and remove projects."
msgstr ""
-msgid "SecurityDashboard|Status"
+msgid "SecurityReports|The security dashboard displays the latest security report. Use it to find and fix vulnerabilities."
msgstr ""
-msgid "SecurityDashboard|The security dashboard displays the latest security findings for projects you wish to monitor. Select \"Edit dashboard\" to add and remove projects."
+msgid "SecurityReports|There was an error adding the comment."
msgstr ""
-msgid "SecurityDashboard|The security dashboard displays the latest security report. Use it to find and fix vulnerabilities."
+msgid "SecurityReports|There was an error creating the issue."
msgstr ""
-msgid "SecurityDashboard|There was an error while generating the report."
+msgid "SecurityReports|There was an error creating the merge request."
msgstr ""
-msgid "SecurityDashboard|Unable to add %{invalidProjects}"
+msgid "SecurityReports|There was an error deleting the comment."
+msgstr ""
+
+msgid "SecurityReports|There was an error dismissing the vulnerabilities."
+msgstr ""
+
+msgid "SecurityReports|There was an error dismissing the vulnerability."
+msgstr ""
+
+msgid "SecurityReports|There was an error reverting the dismissal."
+msgstr ""
+
+msgid "SecurityReports|There was an error reverting this dismissal."
+msgstr ""
+
+msgid "SecurityReports|There was an error while generating the report."
+msgstr ""
+
+msgid "SecurityReports|Unable to add %{invalidProjects}"
+msgstr ""
+
+msgid "SecurityReports|Undo dismiss"
+msgstr ""
+
+msgid "SecurityReports|While it's rare to have no vulnerabilities for your group, it can happen. In any event, we ask that you double check your settings to make sure you've set up your dashboard correctly."
+msgstr ""
+
+msgid "SecurityReports|While it's rare to have no vulnerabilities for your pipeline, it can happen. In any event, we ask that you double check your settings to make sure all security scanning jobs have passed successfully."
+msgstr ""
+
+msgid "SecurityReports|While it's rare to have no vulnerabilities for your project, it can happen. In any event, we ask that you double check your settings to make sure you've set up your dashboard correctly."
+msgstr ""
+
+msgid "SecurityReports|While it's rare to have no vulnerabilities, it can happen. In any event, we ask that you please double check your settings to make sure you've set up your dashboard correctly."
+msgstr ""
+
+msgid "SecurityReports|Won't fix / Accept risk"
+msgstr ""
+
+msgid "SecurityReports|You do not have sufficient permissions to access this report"
+msgstr ""
+
+msgid "SecurityReports|You must sign in as an authorized user to see this report"
+msgstr ""
+
+msgid "SecurityReports|[No reason]"
msgstr ""
msgid "See GitLab's %{password_policy_guidelines}"
@@ -18065,6 +18489,9 @@ msgstr ""
msgid "See the affected projects in the GitLab admin panel"
msgstr ""
+msgid "See what's new at GitLab"
+msgstr ""
+
msgid "Select"
msgstr ""
@@ -18470,9 +18897,6 @@ msgstr ""
msgid "Set up Jira Integration"
msgstr ""
-msgid "Set up Jira Integration illustration"
-msgstr ""
-
msgid "Set up a %{type} Runner automatically"
msgstr ""
@@ -18557,6 +18981,9 @@ msgstr ""
msgid "Severity: %{severity}"
msgstr ""
+msgid "Shards (%{shards})"
+msgstr ""
+
msgid "Shards to synchronize"
msgstr ""
@@ -18871,6 +19298,9 @@ msgstr ""
msgid "Snowplow"
msgstr ""
+msgid "Some child epics may be hidden due to applied filters"
+msgstr ""
+
msgid "Some email servers do not support overriding the email sender name. Enable this option to include the name of the author of the issue, merge request or comment in the email body instead."
msgstr ""
@@ -19012,6 +19442,9 @@ msgstr ""
msgid "Something went wrong while updating your list settings"
msgstr ""
+msgid "Something went wrong with your automatic subscription renewal"
+msgstr ""
+
msgid "Something went wrong, unable to add %{project} to dashboard"
msgstr ""
@@ -19297,6 +19730,9 @@ msgstr ""
msgid "Stage removed"
msgstr ""
+msgid "Standard"
+msgstr ""
+
msgid "Star a label to make it a priority label. Order the prioritized labels to change their relative priority, by dragging."
msgstr ""
@@ -19420,6 +19856,9 @@ msgstr ""
msgid "Static Application Security Testing (SAST)"
msgstr ""
+msgid "StaticSiteEditor|An error occurred while submitting your changes."
+msgstr ""
+
msgid "StaticSiteEditor|Branch could not be created."
msgstr ""
@@ -19429,18 +19868,30 @@ msgstr ""
msgid "StaticSiteEditor|Could not create merge request."
msgstr ""
+msgid "StaticSiteEditor|Incompatible file content"
+msgstr ""
+
msgid "StaticSiteEditor|Return to site"
msgstr ""
+msgid "StaticSiteEditor|Static site editor"
+msgstr ""
+
msgid "StaticSiteEditor|Success!"
msgstr ""
msgid "StaticSiteEditor|Summary of changes"
msgstr ""
+msgid "StaticSiteEditor|The Static Site Editor is currently configured to only edit Markdown content on pages generated from Middleman. Visit the documentation to learn more about configuring your site to use the Static Site Editor."
+msgstr ""
+
msgid "StaticSiteEditor|Update %{sourcePath} file"
msgstr ""
+msgid "StaticSiteEditor|View documentation"
+msgstr ""
+
msgid "StaticSiteEditor|View merge request"
msgstr ""
@@ -19498,6 +19949,12 @@ msgstr ""
msgid "StatusPage|Status page"
msgstr ""
+msgid "StatusPage|Status page URL"
+msgstr ""
+
+msgid "StatusPage|Status page frontend documentation"
+msgstr ""
+
msgid "StatusPage|To publish incidents to an external status page, GitLab will store a JSON file in your Amazon S3 account in a location accessible to your external status page service. Make sure to also set up %{docsLink}"
msgstr ""
@@ -19846,6 +20303,12 @@ msgstr ""
msgid "Synced"
msgstr ""
+msgid "Synchronization disabled"
+msgstr ""
+
+msgid "Synchronization of container repositories is disabled."
+msgstr ""
+
msgid "System"
msgstr ""
@@ -19999,6 +20462,9 @@ msgstr ""
msgid "Telephone number"
msgstr ""
+msgid "Telephone number (Optional)"
+msgstr ""
+
msgid "Template"
msgstr ""
@@ -20305,6 +20771,9 @@ msgstr ""
msgid "The license was successfully uploaded and is now active. You can see the details below."
msgstr ""
+msgid "The license was successfully uploaded and will be active from %{starts_at}. You can see the details below."
+msgstr ""
+
msgid "The maximum file size allowed is %{size}."
msgstr ""
@@ -20626,6 +21095,9 @@ msgstr ""
msgid "There was an error getting the epic participants."
msgstr ""
+msgid "There was an error importing the Jira project."
+msgstr ""
+
msgid "There was an error loading users activity calendar."
msgstr ""
@@ -20641,7 +21113,7 @@ msgstr ""
msgid "There was an error resetting user pipeline minutes."
msgstr ""
-msgid "There was an error saving this Geo Node"
+msgid "There was an error saving this Geo Node."
msgstr ""
msgid "There was an error saving your changes."
@@ -20704,6 +21176,9 @@ msgstr ""
msgid "They can be managed using the %{link}."
msgstr ""
+msgid "Third Party Advisory Link"
+msgstr ""
+
msgid "Third party offers"
msgstr ""
@@ -20740,6 +21215,9 @@ msgstr ""
msgid "This application will be able to:"
msgstr ""
+msgid "This attachment has been truncated to avoid exceeding the maximum allowed attachment size of 15MB. %{written_count} of %{issues_count} issues have been included. Consider re-exporting with a narrower selection of issues."
+msgstr ""
+
msgid "This block is self-referential"
msgstr ""
@@ -20881,6 +21359,12 @@ msgstr ""
msgid "This issue is confidential"
msgstr ""
+msgid "This issue is currently blocked by the following issues: %{issues}."
+msgstr ""
+
+msgid "This issue is in a child epic of the filtered epic"
+msgstr ""
+
msgid "This issue is locked."
msgstr ""
@@ -21079,7 +21563,10 @@ msgstr ""
msgid "This user will be the author of all events in the activity feed that are the result of an update, like new branches being created or new commits being pushed to existing branches. Upon creation or when reassigning you can only assign yourself to be the mirror user."
msgstr ""
-msgid "This variable can not be masked"
+msgid "This variable can not be masked."
+msgstr ""
+
+msgid "This variable does not match the expected pattern."
msgstr ""
msgid "This will help us personalize your onboarding experience."
@@ -21109,10 +21596,10 @@ msgstr ""
msgid "ThreatMonitoring|Application firewall not detected"
msgstr ""
-msgid "ThreatMonitoring|Container Network Policy"
+msgid "ThreatMonitoring|Container Network Policies are not installed or have been disabled. To view this data, ensure your Network Policies are installed and enabled for your cluster."
msgstr ""
-msgid "ThreatMonitoring|Container NetworkPolicies are not installed or has been disabled. To view this data, ensure you NetworkPolicies are installed and enabled for your cluster."
+msgid "ThreatMonitoring|Container Network Policy"
msgstr ""
msgid "ThreatMonitoring|Container NetworkPolicies not detected"
@@ -21145,7 +21632,7 @@ msgstr ""
msgid "ThreatMonitoring|Something went wrong, unable to fetch statistics"
msgstr ""
-msgid "ThreatMonitoring|The firewall is not installed or has been disabled. To view this data, ensure you firewall is installed and enabled for your cluster."
+msgid "ThreatMonitoring|The firewall is not installed or has been disabled. To view this data, ensure the web application firewall is installed and enabled for your cluster."
msgstr ""
msgid "ThreatMonitoring|The graph below is an overview of traffic coming to your application as tracked by the Web Application Firewall (WAF). View the docs for instructions on how to access the WAF logs to see what type of malicious traffic is trying to access your app. The docs link is also accessible by clicking the \"?\" icon next to the title below."
@@ -21214,6 +21701,9 @@ msgstr ""
msgid "Time in seconds GitLab will wait for a response from the external service. When the service does not respond in time, access will be denied."
msgstr ""
+msgid "Time of import: %{importTime}"
+msgstr ""
+
msgid "Time remaining"
msgstr ""
@@ -21398,7 +21888,7 @@ msgstr ""
msgid "Title:"
msgstr ""
-msgid "Titles and Filenames"
+msgid "Titles and Descriptions"
msgstr ""
msgid "To"
@@ -21605,7 +22095,7 @@ msgstr ""
msgid "Too many projects enabled. You will need to manage them via the console or the API."
msgstr ""
-msgid "Topics"
+msgid "Topics (optional)"
msgstr ""
msgid "Total"
@@ -21767,6 +22257,9 @@ msgstr ""
msgid "Try all GitLab has to offer for 30 days."
msgstr ""
+msgid "Try changing or removing filters."
+msgstr ""
+
msgid "Try to fork again"
msgstr ""
@@ -21860,6 +22353,9 @@ msgstr ""
msgid "Unable to connect to server: %{error}"
msgstr ""
+msgid "Unable to connect to the Jira instance. Please check your Jira integration configuration."
+msgstr ""
+
msgid "Unable to convert Kubernetes logs encoding to UTF-8"
msgstr ""
@@ -22803,6 +23299,9 @@ msgstr ""
msgid "View issue"
msgstr ""
+msgid "View issues"
+msgstr ""
+
msgid "View it on GitLab"
msgstr ""
@@ -22980,6 +23479,15 @@ msgstr ""
msgid "VulnerabilityManagement|Resolved %{timeago} by %{user}"
msgstr ""
+msgid "VulnerabilityManagement|Something went wrong while trying to delete the comment. Please try again later."
+msgstr ""
+
+msgid "VulnerabilityManagement|Something went wrong while trying to retrieve the vulnerability history. Please try again later."
+msgstr ""
+
+msgid "VulnerabilityManagement|Something went wrong while trying to save the comment. Please try again later."
+msgstr ""
+
msgid "VulnerabilityManagement|Something went wrong, could not create an issue."
msgstr ""
@@ -23010,6 +23518,9 @@ msgstr ""
msgid "VulnerabilityStatusTypes|Resolved"
msgstr ""
+msgid "Vulnerability|%{scannerName} (version %{scannerVersion})"
+msgstr ""
+
msgid "Vulnerability|Class"
msgstr ""
@@ -23043,7 +23554,10 @@ msgstr ""
msgid "Vulnerability|Project"
msgstr ""
-msgid "Vulnerability|Report Type"
+msgid "Vulnerability|Scanner Provider"
+msgstr ""
+
+msgid "Vulnerability|Scanner Type"
msgstr ""
msgid "Vulnerability|Severity"
@@ -23097,9 +23611,15 @@ msgstr ""
msgid "We sent you an email with reset password instructions"
msgstr ""
+msgid "We tried to automatically renew your %{strong}%{plan_name}%{strong_close} subscription for %{strong}%{namespace_name}%{strong_close} on %{expires_on} but something went wrong so your subscription was downgraded to the free plan. Don't worry, your data is safe. We suggest you check your payment method and get in touch with our support team (%{support_link}). They'll gladly help with your subscription renewal."
+msgstr ""
+
msgid "We want to be sure it is you, please confirm you are not a robot."
msgstr ""
+msgid "We will automatically renew your %{strong}%{plan_name}%{strong_close} subscription for %{strong}%{namespace_name}%{strong_close} on %{strong}%{expires_on}%{strong_close}. There's nothing that you need to do, we'll let you know when the renewal is complete. Need more seats, a higher plan or just want to review your payment method?"
+msgstr ""
+
msgid "We've found no vulnerabilities"
msgstr ""
@@ -23204,15 +23724,6 @@ msgstr ""
msgid "When:"
msgstr ""
-msgid "While it's rare to have no vulnerabilities for your group, it can happen. In any event, we ask that you double check your settings to make sure you've set up your dashboard correctly."
-msgstr ""
-
-msgid "While it's rare to have no vulnerabilities for your pipeline, it can happen. In any event, we ask that you double check your settings to make sure all security scanning jobs have passed successfully."
-msgstr ""
-
-msgid "While it's rare to have no vulnerabilities for your project, it can happen. In any event, we ask that you double check your settings to make sure you've set up your dashboard correctly."
-msgstr ""
-
msgid "While it's rare to have no vulnerabilities, it can happen. In any event, we ask that you please double check your settings to make sure you've set up your dashboard correctly."
msgstr ""
@@ -23243,9 +23754,6 @@ msgstr ""
msgid "Wiki"
msgstr ""
-msgid "Wiki pages"
-msgstr ""
-
msgid "Wiki was successfully updated."
msgstr ""
@@ -23429,6 +23937,9 @@ msgstr ""
msgid "Yes, add it"
msgstr ""
+msgid "Yes, close issue"
+msgstr ""
+
msgid "Yes, let me map Google Code users to full names or GitLab users."
msgstr ""
@@ -23624,6 +24135,9 @@ msgstr ""
msgid "You could not create a new trigger."
msgstr ""
+msgid "You didn't renew your %{strong}%{plan_name}%{strong_close} subscription for %{strong}%{namespace_name}%{strong_close} so it was downgraded to the free plan."
+msgstr ""
+
msgid "You didn't renew your %{strong}%{plan_name}%{strong_close} subscription so it was downgraded to the GitLab Core Plan."
msgstr ""
@@ -23690,6 +24204,9 @@ msgstr ""
msgid "You have declined the invitation to join %{label}."
msgstr ""
+msgid "You have imported from this project %{numberOfPreviousImportsForProject} times before. Each new import will create duplicate issues."
+msgstr ""
+
msgid "You have no permissions"
msgstr ""
@@ -23771,6 +24288,9 @@ msgstr ""
msgid "You need to upload a Google Takeout archive."
msgstr ""
+msgid "You reached %{usage_in_percent} of %{namespace_name}'s capacity (%{used_storage} of %{storage_limit})"
+msgstr ""
+
msgid "You tried to fork %{link_to_the_project} but it failed for the following reason:"
msgstr ""
@@ -23855,9 +24375,21 @@ msgstr ""
msgid "YouTube"
msgstr ""
+msgid "Your %{strong}%{plan_name}%{strong_close} subscription for %{strong}%{namespace_name}%{strong_close} will expire on %{strong}%{expires_on}%{strong_close}. After that, you will not to be able to create issues or merge requests as well as many other features."
+msgstr ""
+
msgid "Your %{strong}%{plan_name}%{strong_close} subscription will expire on %{strong}%{expires_on}%{strong_close}. After that, you will not to be able to create issues or merge requests as well as many other features."
msgstr ""
+msgid "Your CSV export has started. It will be emailed to %{email} when complete."
+msgstr ""
+
+msgid "Your CSV export of %{issues_count} from project %{project_link} has been added to this email as an attachment."
+msgstr ""
+
+msgid "Your CSV export of %{written_count} from project %{project_name} (%{project_url}) has been added to this email as an attachment."
+msgstr ""
+
msgid "Your Commit Email will be used for web based operations, such as edits and merges."
msgstr ""
@@ -23954,6 +24486,9 @@ msgstr ""
msgid "Your comment could not be updated! Please check your network connection and try again."
msgstr ""
+msgid "Your comment will be discarded."
+msgstr ""
+
msgid "Your custom stage '%{title}' was created"
msgstr ""
@@ -23993,6 +24528,9 @@ msgstr ""
msgid "Your new personal access token has been created."
msgstr ""
+msgid "Your new project access token has been created."
+msgstr ""
+
msgid "Your password isn't required to view this page. If a password or any other personal details are requested, please contact your administrator to report abuse."
msgstr ""
@@ -24011,12 +24549,18 @@ msgstr ""
msgid "Your request for access has been queued for review."
msgstr ""
+msgid "Your search didn't match any commits."
+msgstr ""
+
msgid "Your subscription expired!"
msgstr ""
msgid "Your subscription has been downgraded"
msgstr ""
+msgid "Your subscription will automatically renew in %{remaining_days}"
+msgstr ""
+
msgid "Your subscription will expire in %{remaining_days}"
msgstr ""
@@ -24055,7 +24599,7 @@ msgstr ""
msgid "allowed to fail"
msgstr ""
-msgid "already being used for another group or project milestone."
+msgid "already being used for another group or project %{timebox_name}."
msgstr ""
msgid "already has a \"created\" issue link"
@@ -24133,6 +24677,9 @@ msgstr ""
msgid "ciReport|%{linkStartTag}Learn more about SAST %{linkEndTag}"
msgstr ""
+msgid "ciReport|%{linkStartTag}Learn more about Secret Scanning %{linkEndTag}"
+msgstr ""
+
msgid "ciReport|%{linkStartTag}Learn more about codequality reports %{linkEndTag}"
msgstr ""
@@ -24296,6 +24843,12 @@ msgstr ""
msgid "ciReport|SAST"
msgstr ""
+msgid "ciReport|Secret scanning"
+msgstr ""
+
+msgid "ciReport|Secret scanning detects secrets and credentials vulnerabilities in your source code."
+msgstr ""
+
msgid "ciReport|Security scanning"
msgstr ""
@@ -24493,6 +25046,9 @@ msgstr ""
msgid "group"
msgstr ""
+msgid "groups"
+msgstr ""
+
msgid "has already been linked to another vulnerability"
msgstr ""
@@ -24656,9 +25212,6 @@ msgstr[1] ""
msgid "merged %{time_ago}"
msgstr ""
-msgid "milestone should belong either to a project or a group."
-msgstr ""
-
msgid "missing"
msgstr ""
@@ -24995,6 +25548,9 @@ msgstr ""
msgid "no contributions"
msgstr ""
+msgid "no expiration"
+msgstr ""
+
msgid "no one can merge"
msgstr ""
@@ -25047,6 +25603,9 @@ msgstr ""
msgid "pending removal"
msgstr ""
+msgid "per day"
+msgstr ""
+
msgid "pipeline"
msgstr ""
@@ -25075,6 +25634,12 @@ msgstr[1] ""
msgid "project avatar"
msgstr ""
+msgid "projects"
+msgstr ""
+
+msgid "push to your repository, create pipelines, create issues or add comments. To reduce storage capacity, delete unused repositories, artifacts, wikis, issues, and pipelines."
+msgstr ""
+
msgid "quick actions"
msgstr ""
@@ -25221,11 +25786,6 @@ msgstr ""
msgid "this document"
msgstr ""
-msgid "thread resolved"
-msgid_plural "threads resolved"
-msgstr[0] ""
-msgstr[1] ""
-
msgid "to help your contributors communicate effectively!"
msgstr ""
@@ -25280,6 +25840,9 @@ msgstr ""
msgid "view the blob"
msgstr ""
+msgid "vulnerability|Add a comment"
+msgstr ""
+
msgid "vulnerability|Add a comment or reason for dismissal"
msgstr ""
diff --git a/locale/id_ID/gitlab.po b/locale/id_ID/gitlab.po
index c0d87b0b2f7..0c3e1a9e5ed 100644
--- a/locale/id_ID/gitlab.po
+++ b/locale/id_ID/gitlab.po
@@ -12,7 +12,7 @@ msgstr ""
"X-Crowdin-Project: gitlab-ee\n"
"X-Crowdin-Language: id\n"
"X-Crowdin-File: /master/locale/gitlab.pot\n"
-"PO-Revision-Date: 2020-04-15 00:29\n"
+"PO-Revision-Date: 2020-05-05 21:34\n"
msgid " %{start} to %{end}"
msgstr ""
@@ -182,6 +182,10 @@ msgid "%d tag"
msgid_plural "%d tags"
msgstr[0] ""
+msgid "%d unresolved thread"
+msgid_plural "%d unresolved threads"
+msgstr[0] ""
+
msgid "%d vulnerability dismissed"
msgid_plural "%d vulnerabilities dismissed"
msgstr[0] ""
@@ -335,6 +339,9 @@ msgstr ""
msgid "%{mrText}, this issue will be closed automatically."
msgstr ""
+msgid "%{namespace_name} is now read-only. You cannot: %{base_message}"
+msgstr ""
+
msgid "%{name} contained %{resultsString}"
msgstr ""
@@ -347,6 +354,10 @@ msgstr ""
msgid "%{name}'s avatar"
msgstr ""
+msgid "%{no_of_days} day"
+msgid_plural "%{no_of_days} days"
+msgstr[0] ""
+
msgid "%{number_commits_behind} commits behind %{default_branch}, %{number_commits_ahead} commits ahead"
msgstr ""
@@ -464,6 +475,9 @@ msgstr[0] ""
msgid "%{text} is available"
msgstr ""
+msgid "%{timebox_name} should belong either to a project or a group."
+msgstr ""
+
msgid "%{title} %{operator} %{threshold}"
msgstr ""
@@ -720,12 +734,18 @@ msgstr ""
msgid "<code>\"johnsmith@example.com\": \"johnsmith@example.com\"</code> will add \"By <a href=\"#\">johnsmith@example.com</a>\" to all issues and comments originally created by johnsmith@example.com. By default, the email address or username is masked to ensure the user's privacy. Use this option if you want to show the full email address."
msgstr ""
+msgid "<namespace / project>"
+msgstr ""
+
msgid "<no name set>"
msgstr ""
msgid "<no scopes selected>"
msgstr ""
+msgid "<project name>"
+msgstr ""
+
msgid "<strong>%{group_name}</strong> group members"
msgstr ""
@@ -759,6 +779,9 @@ msgstr ""
msgid "A basic page and serverless function that uses AWS Lambda, AWS API Gateway, and GitLab Pages"
msgstr ""
+msgid "A complete DevOps platform"
+msgstr ""
+
msgid "A default branch cannot be chosen for an empty project."
msgstr ""
@@ -975,6 +998,12 @@ msgstr ""
msgid "AccessTokens|reset it"
msgstr ""
+msgid "AccessibilityReport|Accessibility report artifact not found"
+msgstr ""
+
+msgid "AccessibilityReport|Failed to retrieve accessibility report"
+msgstr ""
+
msgid "AccessibilityReport|Learn More"
msgstr ""
@@ -984,6 +1013,9 @@ msgstr ""
msgid "AccessibilityReport|New"
msgstr ""
+msgid "AccessibilityReport|The accessibility scanning found an error of the following type: %{code}"
+msgstr ""
+
msgid "Account"
msgstr ""
@@ -1225,6 +1257,9 @@ msgstr ""
msgid "Added at"
msgstr ""
+msgid "Added for this merge request"
+msgstr ""
+
msgid "Added in this version"
msgstr ""
@@ -1595,6 +1630,72 @@ msgid "Alert"
msgid_plural "Alerts"
msgstr[0] ""
+msgid "Alert Details"
+msgstr ""
+
+msgid "AlertManagement|Acknowledged"
+msgstr ""
+
+msgid "AlertManagement|Alert"
+msgstr ""
+
+msgid "AlertManagement|Authorize external service"
+msgstr ""
+
+msgid "AlertManagement|Display alerts from all your monitoring tools directly within GitLab. Streamline the investigation of your alerts and the escalation of alerts to incidents."
+msgstr ""
+
+msgid "AlertManagement|End time"
+msgstr ""
+
+msgid "AlertManagement|End time:"
+msgstr ""
+
+msgid "AlertManagement|Events"
+msgstr ""
+
+msgid "AlertManagement|Events:"
+msgstr ""
+
+msgid "AlertManagement|Full Alert Details"
+msgstr ""
+
+msgid "AlertManagement|More information"
+msgstr ""
+
+msgid "AlertManagement|No alerts available to display. If you think you're seeing this message in error, refresh the page."
+msgstr ""
+
+msgid "AlertManagement|No alerts to display."
+msgstr ""
+
+msgid "AlertManagement|Overview"
+msgstr ""
+
+msgid "AlertManagement|Resolved"
+msgstr ""
+
+msgid "AlertManagement|Severity"
+msgstr ""
+
+msgid "AlertManagement|Start time"
+msgstr ""
+
+msgid "AlertManagement|Start time:"
+msgstr ""
+
+msgid "AlertManagement|Status"
+msgstr ""
+
+msgid "AlertManagement|Surface alerts in GitLab"
+msgstr ""
+
+msgid "AlertManagement|There was an error displaying the alerts. Confirm your endpoint's configuration details to ensure alerts appear."
+msgstr ""
+
+msgid "AlertManagement|Triggered"
+msgstr ""
+
msgid "AlertService|%{linkStart}Learn more%{linkEnd} about configuring this endpoint to receive alerts."
msgstr ""
@@ -1664,6 +1765,9 @@ msgstr ""
msgid "All security scans are enabled because %{linkStart}Auto DevOps%{linkEnd} is enabled on this project"
msgstr ""
+msgid "All threads resolved"
+msgstr ""
+
msgid "All users"
msgstr ""
@@ -1682,6 +1786,9 @@ msgstr ""
msgid "Allow only the selected protocols to be used for Git access."
msgstr ""
+msgid "Allow owners to manage default branch protection per group"
+msgstr ""
+
msgid "Allow owners to manually add users outside of LDAP"
msgstr ""
@@ -1790,6 +1897,9 @@ msgstr ""
msgid "An error occurred fetching the dropdown data."
msgstr ""
+msgid "An error occurred fetching the project authors."
+msgstr ""
+
msgid "An error occurred previewing the blob"
msgstr ""
@@ -1871,6 +1981,9 @@ msgstr ""
msgid "An error occurred while fetching sidebar data"
msgstr ""
+msgid "An error occurred while fetching terraform reports."
+msgstr ""
+
msgid "An error occurred while fetching the Service Desk address."
msgstr ""
@@ -2108,6 +2221,9 @@ msgstr ""
msgid "Any"
msgstr ""
+msgid "Any Author"
+msgstr ""
+
msgid "Any Label"
msgstr ""
@@ -2319,10 +2435,10 @@ msgstr ""
msgid "Are you sure that you want to unarchive this project?"
msgstr ""
-msgid "Are you sure you want to cancel creating this comment?"
+msgid "Are you sure you want to cancel editing this comment?"
msgstr ""
-msgid "Are you sure you want to cancel editing this comment?"
+msgid "Are you sure you want to close this blocked issue?"
msgstr ""
msgid "Are you sure you want to delete %{name}?"
@@ -2352,6 +2468,9 @@ msgstr ""
msgid "Are you sure you want to deploy this environment?"
msgstr ""
+msgid "Are you sure you want to discard this comment?"
+msgstr ""
+
msgid "Are you sure you want to erase this build?"
msgstr ""
@@ -2527,6 +2646,9 @@ msgstr ""
msgid "At least one approval from a code owner is required to change files matching the respective CODEOWNER rules."
msgstr ""
+msgid "At least one logging option is required to be enabled"
+msgstr ""
+
msgid "At least one of group_id or project_id must be specified"
msgstr ""
@@ -2564,6 +2686,27 @@ msgstr ""
msgid "AuditEvents|Target"
msgstr ""
+msgid "AuditLogs|(removed)"
+msgstr ""
+
+msgid "AuditLogs|Action"
+msgstr ""
+
+msgid "AuditLogs|Author"
+msgstr ""
+
+msgid "AuditLogs|Date"
+msgstr ""
+
+msgid "AuditLogs|IP Address"
+msgstr ""
+
+msgid "AuditLogs|Object"
+msgstr ""
+
+msgid "AuditLogs|Target"
+msgstr ""
+
msgid "Aug"
msgstr ""
@@ -2846,9 +2989,6 @@ msgstr ""
msgid "BambooService|You must set up automatic revision labeling and a repository trigger in Bamboo."
msgstr ""
-msgid "Batch operations"
-msgstr ""
-
msgid "BatchComments|Delete all pending comments"
msgstr ""
@@ -2978,6 +3118,9 @@ msgstr ""
msgid "Boards"
msgstr ""
+msgid "Boards and Board Lists"
+msgstr ""
+
msgid "Boards|Collapse"
msgstr ""
@@ -3197,6 +3340,9 @@ msgstr ""
msgid "Buy GitLab Enterprise Edition"
msgstr ""
+msgid "Buy more Pipeline minutes"
+msgstr ""
+
msgid "By %{user_name}"
msgstr ""
@@ -3317,6 +3463,9 @@ msgstr ""
msgid "Can't scan the code?"
msgstr ""
+msgid "Can't update snippet: %{err}"
+msgstr ""
+
msgid "Canary"
msgstr ""
@@ -3485,6 +3634,9 @@ msgstr ""
msgid "Changing group path can have unintended side effects."
msgstr ""
+msgid "Charts"
+msgstr ""
+
msgid "Charts can't be displayed as the request for data has timed out. %{documentationLink}"
msgstr ""
@@ -3521,9 +3673,6 @@ msgstr ""
msgid "ChatMessage|and [%{count} more](%{pipeline_failed_jobs_url})"
msgstr ""
-msgid "ChatMessage|failed"
-msgstr ""
-
msgid "ChatMessage|has failed"
msgstr ""
@@ -3539,9 +3688,6 @@ msgstr ""
msgid "ChatMessage|in %{project_link}"
msgstr ""
-msgid "ChatMessage|passed"
-msgstr ""
-
msgid "Check again"
msgstr ""
@@ -3992,9 +4138,6 @@ msgstr ""
msgid "Click any <strong>project name</strong> in the project list below to navigate to the project milestone."
msgstr ""
-msgid "Click here"
-msgstr ""
-
msgid "Click the <strong>Download</strong> button and wait for downloading to complete."
msgstr ""
@@ -4382,6 +4525,12 @@ msgstr ""
msgid "ClusterIntegration|Fetching zones"
msgstr ""
+msgid "ClusterIntegration|Fluentd"
+msgstr ""
+
+msgid "ClusterIntegration|Fluentd is an open source data collector, which lets you unify the data collection and consumption for a better use and understanding of data. It requires at least one of the following logs to be successfully installed."
+msgstr ""
+
msgid "ClusterIntegration|GitLab Integration"
msgstr ""
@@ -4688,6 +4837,15 @@ msgstr ""
msgid "ClusterIntegration|Request to begin uninstalling failed"
msgstr ""
+msgid "ClusterIntegration|SIEM Hostname"
+msgstr ""
+
+msgid "ClusterIntegration|SIEM Port"
+msgstr ""
+
+msgid "ClusterIntegration|SIEM Protocol"
+msgstr ""
+
msgid "ClusterIntegration|Save changes"
msgstr ""
@@ -4784,6 +4942,12 @@ msgstr ""
msgid "ClusterIntegration|Select zone to choose machine type"
msgstr ""
+msgid "ClusterIntegration|Send Cilium Logs"
+msgstr ""
+
+msgid "ClusterIntegration|Send ModSecurity Logs"
+msgstr ""
+
msgid "ClusterIntegration|Service Token"
msgstr ""
@@ -5211,18 +5375,33 @@ msgstr ""
msgid "Compliance framework (optional)"
msgstr ""
+msgid "ComplianceFramework|GDPR"
+msgstr ""
+
msgid "ComplianceFramework|GDPR - General Data Protection Regulation"
msgstr ""
+msgid "ComplianceFramework|HIPAA"
+msgstr ""
+
msgid "ComplianceFramework|HIPAA - Health Insurance Portability and Accountability Act"
msgstr ""
+msgid "ComplianceFramework|PCI-DSS"
+msgstr ""
+
msgid "ComplianceFramework|PCI-DSS - Payment Card Industry-Data Security Standard"
msgstr ""
+msgid "ComplianceFramework|SOC 2"
+msgstr ""
+
msgid "ComplianceFramework|SOC 2 - Service Organization Control 2"
msgstr ""
+msgid "ComplianceFramework|SOX"
+msgstr ""
+
msgid "ComplianceFramework|SOX - Sarbanes-Oxley"
msgstr ""
@@ -5370,9 +5549,15 @@ msgstr ""
msgid "Container repositories sync capacity"
msgstr ""
+msgid "ContainerRegistry| Please visit the %{linkStart}administration settings%{linkEnd} to enable this feature."
+msgstr ""
+
msgid "ContainerRegistry|%{imageName} tags"
msgstr ""
+msgid "ContainerRegistry|%{title} was successfully scheduled for deletion"
+msgstr ""
+
msgid "ContainerRegistry|Automatically remove extra images that aren't designed to be kept."
msgstr ""
@@ -5385,6 +5570,9 @@ msgstr ""
msgid "ContainerRegistry|Container Registry"
msgstr ""
+msgid "ContainerRegistry|Container Registry tag expiration and retention policy is disabled"
+msgstr ""
+
msgid "ContainerRegistry|Copy build command"
msgstr ""
@@ -5394,18 +5582,12 @@ msgstr ""
msgid "ContainerRegistry|Copy push command"
msgstr ""
-msgid "ContainerRegistry|Currently, the Container Registry tag expiration feature is not available for projects created before GitLab version 12.8. For updates and more information, visit Issue %{linkStart}#196124%{linkEnd}"
-msgstr ""
-
msgid "ContainerRegistry|Docker connection error"
msgstr ""
msgid "ContainerRegistry|Docker tag expiration policy is %{toggleStatus}"
msgstr ""
-msgid "ContainerRegistry|Docker tags with names matching this regex pattern will expire:"
-msgstr ""
-
msgid "ContainerRegistry|Edit Settings"
msgstr ""
@@ -5427,9 +5609,6 @@ msgstr ""
msgid "ContainerRegistry|Image ID"
msgstr ""
-msgid "ContainerRegistry|Image deleted successfully"
-msgstr ""
-
msgid "ContainerRegistry|Keep and protect the images that matter most."
msgstr ""
@@ -5445,12 +5624,24 @@ msgstr ""
msgid "ContainerRegistry|Number of tags to retain:"
msgstr ""
+msgid "ContainerRegistry|Please contact your administrator."
+msgstr ""
+
msgid "ContainerRegistry|Push an image"
msgstr ""
msgid "ContainerRegistry|Quick Start"
msgstr ""
+msgid "ContainerRegistry|Regular expressions such as %{codeStart}.*-test%{codeEnd} or %{codeStart}dev-.*%{codeEnd} are supported"
+msgstr ""
+
+msgid "ContainerRegistry|Regular expressions such as %{codeStart}.*-test%{codeEnd} or %{codeStart}dev-.*%{codeEnd} are supported. To select all tags, use %{codeStart}.*%{codeEnd}"
+msgstr ""
+
+msgid "ContainerRegistry|Remember to run %{docLinkStart}garbage collection%{docLinkEnd} to remove the stale data from storage."
+msgstr ""
+
msgid "ContainerRegistry|Remove repository"
msgstr ""
@@ -5464,22 +5655,22 @@ msgstr[0] ""
msgid "ContainerRegistry|Retention policy has been Enabled"
msgstr ""
-msgid "ContainerRegistry|Something went wrong while deleting the image."
+msgid "ContainerRegistry|Something went wrong while fetching the expiration policy."
msgstr ""
-msgid "ContainerRegistry|Something went wrong while deleting the tag."
+msgid "ContainerRegistry|Something went wrong while fetching the repository list."
msgstr ""
-msgid "ContainerRegistry|Something went wrong while deleting the tags."
+msgid "ContainerRegistry|Something went wrong while fetching the tags list."
msgstr ""
-msgid "ContainerRegistry|Something went wrong while fetching the expiration policy."
+msgid "ContainerRegistry|Something went wrong while marking the tag for deletion."
msgstr ""
-msgid "ContainerRegistry|Something went wrong while fetching the packages list."
+msgid "ContainerRegistry|Something went wrong while marking the tags for deletion."
msgstr ""
-msgid "ContainerRegistry|Something went wrong while fetching the tags list."
+msgid "ContainerRegistry|Something went wrong while scheduling %{title} for deletion. Please try again."
msgstr ""
msgid "ContainerRegistry|Something went wrong while updating the expiration policy."
@@ -5488,16 +5679,25 @@ msgstr ""
msgid "ContainerRegistry|Tag"
msgstr ""
-msgid "ContainerRegistry|Tag deleted successfully"
-msgstr ""
-
msgid "ContainerRegistry|Tag expiration policy"
msgstr ""
msgid "ContainerRegistry|Tag expiration policy is designed to:"
msgstr ""
-msgid "ContainerRegistry|Tags deleted successfully"
+msgid "ContainerRegistry|Tag successfully marked for deletion."
+msgstr ""
+
+msgid "ContainerRegistry|Tags successfully marked for deletion."
+msgstr ""
+
+msgid "ContainerRegistry|Tags with names matching this regex pattern will %{italicStart}be preserved:%{italicEnd}"
+msgstr ""
+
+msgid "ContainerRegistry|Tags with names matching this regex pattern will %{italicStart}expire:%{italicEnd}"
+msgstr ""
+
+msgid "ContainerRegistry|The Container Registry tag expiration and retention policies for this project have not been enabled."
msgstr ""
msgid "ContainerRegistry|The last tag related to this image was recently removed. This empty image and any associated data will be automatically removed as part of the regular Garbage Collection process. If you have any questions, contact your administrator."
@@ -5518,16 +5718,16 @@ msgstr ""
msgid "ContainerRegistry|There are no container images stored for this project"
msgstr ""
-msgid "ContainerRegistry|This Registry contains deleted image tag data. Remember to run %{docLinkStart}garbage collection%{docLinkEnd} to remove the stale data from storage."
+msgid "ContainerRegistry|There was an error during the deletion of this image repository, please try again."
msgstr ""
msgid "ContainerRegistry|This image has no active tags"
msgstr ""
-msgid "ContainerRegistry|We are having trouble connecting to Docker, which could be due to an issue with your project name or path. %{docLinkStart}More Information%{docLinkEnd}"
+msgid "ContainerRegistry|This image repository is scheduled for deletion"
msgstr ""
-msgid "ContainerRegistry|Wildcards such as %{codeStart}.*-stable%{codeEnd} or %{codeStart}production/.*%{codeEnd} are supported. To select all tags, use %{codeStart}.*%{codeEnd}"
+msgid "ContainerRegistry|We are having trouble connecting to Docker, which could be due to an issue with your project name or path. %{docLinkStart}More Information%{docLinkEnd}"
msgstr ""
msgid "ContainerRegistry|With the Container Registry, every project can have its own space to store its Docker images. %{docLinkStart}More Information%{docLinkEnd}"
@@ -5782,7 +5982,7 @@ msgstr ""
msgid "Could not delete chat nickname %{chat_name}."
msgstr ""
-msgid "Could not find design"
+msgid "Could not find design."
msgstr ""
msgid "Could not remove the trigger."
@@ -5797,6 +5997,9 @@ msgstr ""
msgid "Could not revoke personal access token %{personal_access_token_name}."
msgstr ""
+msgid "Could not revoke project access token %{project_access_token_name}."
+msgstr ""
+
msgid "Could not save group ID"
msgstr ""
@@ -5854,6 +6057,9 @@ msgstr ""
msgid "Create a new branch"
msgstr ""
+msgid "Create a new deploy key for this project"
+msgstr ""
+
msgid "Create a new file as there are no files yet. Afterwards, you'll be able to commit your changes."
msgstr ""
@@ -5959,6 +6165,9 @@ msgstr ""
msgid "Create requirement"
msgstr ""
+msgid "Create snippet"
+msgstr ""
+
msgid "Create wildcard: %{searchTerm}"
msgstr ""
@@ -5980,6 +6189,9 @@ msgstr ""
msgid "Created"
msgstr ""
+msgid "Created %{timestamp}"
+msgstr ""
+
msgid "Created At"
msgstr ""
@@ -6335,6 +6547,9 @@ msgstr ""
msgid "CycleAnalytics|stage dropdown"
msgstr ""
+msgid "DAG"
+msgstr ""
+
msgid "DNS"
msgstr ""
@@ -6695,6 +6910,9 @@ msgstr ""
msgid "Deploy key was successfully updated."
msgstr ""
+msgid "Deploy keys allow read-only or read-write (if enabled) access to your repository. Deploy keys can be used for CI, staging or production servers. You can create a deploy key or add an existing one."
+msgstr ""
+
msgid "Deploy progress not found. To see pods, ensure your environment matches %{linkStart}deploy board criteria%{linkEnd}."
msgstr ""
@@ -6848,6 +7066,9 @@ msgstr ""
msgid "Deploying to"
msgstr ""
+msgid "Deployment Frequency"
+msgstr ""
+
msgid "Deployment|API"
msgstr ""
@@ -7365,9 +7586,6 @@ msgstr ""
msgid "Edit environment"
msgstr ""
-msgid "Edit epic description"
-msgstr ""
-
msgid "Edit file"
msgstr ""
@@ -7989,9 +8207,6 @@ msgstr ""
msgid "Epics"
msgstr ""
-msgid "Epics (Ultimate / Gold license only)"
-msgstr ""
-
msgid "Epics Roadmap"
msgstr ""
@@ -8001,6 +8216,9 @@ msgstr ""
msgid "Epics let you manage your portfolio of projects more efficiently and with less effort"
msgstr ""
+msgid "Epics, Issues, and Merge Requests"
+msgstr ""
+
msgid "Epics|Add an epic"
msgstr ""
@@ -8052,6 +8270,9 @@ msgstr ""
msgid "Epics|Something went wrong while fetching group epics."
msgstr ""
+msgid "Epics|Something went wrong while moving item."
+msgstr ""
+
msgid "Epics|Something went wrong while ordering item."
msgstr ""
@@ -8106,6 +8327,9 @@ msgstr ""
msgid "Error fetching network graph."
msgstr ""
+msgid "Error fetching payload data."
+msgstr ""
+
msgid "Error fetching projects"
msgstr ""
@@ -8115,9 +8339,6 @@ msgstr ""
msgid "Error fetching the dependency list. Please check your network connection and try again."
msgstr ""
-msgid "Error fetching usage ping data."
-msgstr ""
-
msgid "Error loading branch data. Please try again."
msgstr ""
@@ -8259,6 +8480,9 @@ msgstr ""
msgid "Errors"
msgstr ""
+msgid "Errors:"
+msgstr ""
+
msgid "Estimated"
msgstr ""
@@ -8463,12 +8687,18 @@ msgstr ""
msgid "Export as CSV"
msgstr ""
+msgid "Export group"
+msgstr ""
+
msgid "Export issues"
msgstr ""
msgid "Export project"
msgstr ""
+msgid "Export this group with all related data to a new GitLab instance. Once complete, you can import the data file from the \"New Group\" page."
+msgstr ""
+
msgid "Export this project with all its related data in order to move your project to a new GitLab instance. Once the export is finished, you can import the file from the \"New Project\" page."
msgstr ""
@@ -8538,9 +8768,6 @@ msgstr ""
msgid "Failed Jobs"
msgstr ""
-msgid "Failed create wiki"
-msgstr ""
-
msgid "Failed to add a Zoom meeting"
msgstr ""
@@ -8580,6 +8807,9 @@ msgstr ""
msgid "Failed to create resources"
msgstr ""
+msgid "Failed to create wiki"
+msgstr ""
+
msgid "Failed to delete board. Please try again."
msgstr ""
@@ -8589,7 +8819,7 @@ msgstr ""
msgid "Failed to enqueue the rebase operation, possibly due to a long-lived transaction. Try again later."
msgstr ""
-msgid "Failed to find import label for jira import."
+msgid "Failed to find import label for Jira import."
msgstr ""
msgid "Failed to get ref."
@@ -8613,6 +8843,12 @@ msgstr ""
msgid "Failed to load groups & users."
msgstr ""
+msgid "Failed to load labels. Please try again."
+msgstr ""
+
+msgid "Failed to load milestones. Please try again."
+msgstr ""
+
msgid "Failed to load related branches"
msgstr ""
@@ -8988,7 +9224,10 @@ msgstr ""
msgid "Filter by milestone name"
msgstr ""
-msgid "Filter by name..."
+msgid "Filter by name"
+msgstr ""
+
+msgid "Filter by status"
msgstr ""
msgid "Filter by two-factor authentication"
@@ -9000,12 +9239,18 @@ msgstr ""
msgid "Filter projects"
msgstr ""
+msgid "Filter results"
+msgstr ""
+
msgid "Filter results by group"
msgstr ""
msgid "Filter results by project"
msgstr ""
+msgid "Filter results..."
+msgstr ""
+
msgid "Filter your projects by name"
msgstr ""
@@ -9294,10 +9539,10 @@ msgstr ""
msgid "GeoNodes|Checksummed"
msgstr ""
-msgid "GeoNodes|Container repositories"
+msgid "GeoNodes|Consult Geo troubleshooting information"
msgstr ""
-msgid "GeoNodes|Data is out of date from %{timeago}"
+msgid "GeoNodes|Container repositories"
msgstr ""
msgid "GeoNodes|Data replication lag"
@@ -9339,6 +9584,9 @@ msgstr ""
msgid "GeoNodes|Last event ID seen from primary"
msgstr ""
+msgid "GeoNodes|Learn more about Geo node statuses"
+msgstr ""
+
msgid "GeoNodes|Loading nodes"
msgstr ""
@@ -9354,6 +9602,9 @@ msgstr ""
msgid "GeoNodes|Node was successfully removed."
msgstr ""
+msgid "GeoNodes|Node's status was updated %{timeAgo}."
+msgstr ""
+
msgid "GeoNodes|Not checksummed"
msgstr ""
@@ -9384,7 +9635,10 @@ msgstr ""
msgid "GeoNodes|Repository verification progress"
msgstr ""
-msgid "GeoNodes|Selective"
+msgid "GeoNodes|Selective (%{syncLabel})"
+msgstr ""
+
+msgid "GeoNodes|Selective synchronization"
msgstr ""
msgid "GeoNodes|Something went wrong while changing node status"
@@ -9411,6 +9665,9 @@ msgstr ""
msgid "GeoNodes|Unverified"
msgstr ""
+msgid "GeoNodes|Updated %{timeAgo}"
+msgstr ""
+
msgid "GeoNodes|Used slots"
msgstr ""
@@ -9450,16 +9707,16 @@ msgstr ""
msgid "Geo|All"
msgstr ""
-msgid "Geo|All projects"
+msgid "Geo|All %{replicable_type}"
msgstr ""
-msgid "Geo|All projects are being scheduled for re-sync"
+msgid "Geo|All projects"
msgstr ""
-msgid "Geo|All projects are being scheduled for re-verify"
+msgid "Geo|All projects are being scheduled for resync"
msgstr ""
-msgid "Geo|Batch operations"
+msgid "Geo|All projects are being scheduled for reverify"
msgstr ""
msgid "Geo|Could not remove tracking entry for an existing project."
@@ -9471,9 +9728,15 @@ msgstr ""
msgid "Geo|Failed"
msgstr ""
+msgid "Geo|Filter by status"
+msgstr ""
+
msgid "Geo|Geo Status"
msgstr ""
+msgid "Geo|In progress"
+msgstr ""
+
msgid "Geo|In sync"
msgstr ""
@@ -9498,9 +9761,6 @@ msgstr ""
msgid "Geo|Not synced yet"
msgstr ""
-msgid "Geo|Pending"
-msgstr ""
-
msgid "Geo|Pending synchronization"
msgstr ""
@@ -9537,7 +9797,7 @@ msgstr ""
msgid "Geo|Resync"
msgstr ""
-msgid "Geo|Resync all projects"
+msgid "Geo|Resync all"
msgstr ""
msgid "Geo|Retry count"
@@ -9546,7 +9806,7 @@ msgstr ""
msgid "Geo|Reverify"
msgstr ""
-msgid "Geo|Reverify all projects"
+msgid "Geo|Reverify all"
msgstr ""
msgid "Geo|Status"
@@ -9888,9 +10148,6 @@ msgstr ""
msgid "Go to file"
msgstr ""
-msgid "Go to file (MRs only)"
-msgstr ""
-
msgid "Go to file permalink (while viewing a file)"
msgstr ""
@@ -10335,6 +10592,9 @@ msgstr ""
msgid "GroupSettings|Disable group mentions"
msgstr ""
+msgid "GroupSettings|Export group"
+msgstr ""
+
msgid "GroupSettings|If the parent group's visibility is lower than the group current visibility, visibility levels for subgroups and projects will be changed to match the new parent group's visibility."
msgstr ""
@@ -10413,6 +10673,9 @@ msgstr ""
msgid "Groups (%{count})"
msgstr ""
+msgid "Groups (%{groups})"
+msgstr ""
+
msgid "Groups can also be nested by creating %{subgroup_docs_link_start}subgroups%{subgroup_docs_link_end}."
msgstr ""
@@ -10676,15 +10939,12 @@ msgstr ""
msgid "ID:"
msgstr ""
-msgid "IDE|Allow live previews of JavaScript projects in the Web IDE using CodeSandbox client side evaluation."
+msgid "IDE|Allow live previews of JavaScript projects in the Web IDE using CodeSandbox Live Preview."
msgstr ""
msgid "IDE|Back"
msgstr ""
-msgid "IDE|Client side evaluation"
-msgstr ""
-
msgid "IDE|Commit"
msgstr ""
@@ -10748,6 +11008,9 @@ msgstr ""
msgid "If any job surpasses this timeout threshold, it will be marked as failed. Human readable time input language is accepted like \"1 hour\". Values without specification represent seconds."
msgstr ""
+msgid "If blank, set allowable lifetime to %{instance_level_policy_in_words}, as defined by the instance admin. Once set, existing tokens for users in this group may be revoked."
+msgstr ""
+
msgid "If checked, group owners can manage LDAP group links and LDAP member overrides"
msgstr ""
@@ -10787,6 +11050,9 @@ msgstr ""
msgid "If you lose your recovery codes you can generate new ones, invalidating all previous codes."
msgstr ""
+msgid "If you reach 100%% storage capacity, you will not be able to: %{base_message}"
+msgstr ""
+
msgid "If your HTTP repository is not publicly accessible, add your credentials."
msgstr ""
@@ -10898,6 +11164,9 @@ msgstr ""
msgid "Import repository"
msgstr ""
+msgid "Import started by: %{importInitiator}"
+msgstr ""
+
msgid "Import tasks"
msgstr ""
@@ -10964,6 +11233,9 @@ msgstr ""
msgid "In order to tailor your experience with GitLab we<br>would like to know a bit more about you."
msgstr ""
+msgid "In progress"
+msgstr ""
+
msgid "In the next step, you'll be able to select the projects you want to import."
msgstr ""
@@ -11310,15 +11582,15 @@ msgstr ""
msgid "Issues"
msgstr ""
-msgid "Issues / Merge Requests"
-msgstr ""
-
msgid "Issues Analytics"
msgstr ""
msgid "Issues Rate Limits"
msgstr ""
+msgid "Issues and Merge Requests"
+msgstr ""
+
msgid "Issues can be bugs, tasks or ideas to be discussed. Also, issues are searchable and filterable."
msgstr ""
@@ -11340,10 +11612,10 @@ msgstr ""
msgid "IssuesAnalytics|Avg/Month:"
msgstr ""
-msgid "IssuesAnalytics|Issues created"
+msgid "IssuesAnalytics|Issues opened"
msgstr ""
-msgid "IssuesAnalytics|Issues created per month"
+msgid "IssuesAnalytics|Issues opened per month"
msgstr ""
msgid "IssuesAnalytics|Last 12 months"
@@ -11397,6 +11669,9 @@ msgstr ""
msgid "Jira integration not configured."
msgstr ""
+msgid "Jira project: %{importProject}"
+msgstr ""
+
msgid "JiraService|Events for %{noteable_model_name} are disabled."
msgstr ""
@@ -11785,6 +12060,9 @@ msgstr ""
msgid "Lead"
msgstr ""
+msgid "Lead Time"
+msgstr ""
+
msgid "Learn GitLab"
msgstr ""
@@ -11797,6 +12075,9 @@ msgstr ""
msgid "Learn how to %{no_packages_link_start}publish and share your packages%{no_packages_link_end} with GitLab."
msgstr ""
+msgid "Learn how to enable synchronization"
+msgstr ""
+
msgid "Learn more"
msgstr ""
@@ -12123,9 +12404,6 @@ msgstr ""
msgid "Live preview"
msgstr ""
-msgid "Load more vulnerabilities"
-msgstr ""
-
msgid "Loading"
msgstr ""
@@ -12276,9 +12554,6 @@ msgstr ""
msgid "Manage"
msgstr ""
-msgid "Manage Git repositories with fine-grained access controls that keep your code secure. Perform code reviews and enhance collaboration with merge requests. Each project can also have an issue tracker and a wiki."
-msgstr ""
-
msgid "Manage Web IDE features"
msgstr ""
@@ -12309,6 +12584,9 @@ msgstr ""
msgid "Manage your license"
msgstr ""
+msgid "Managed Account"
+msgstr ""
+
msgid "Manifest"
msgstr ""
@@ -12573,6 +12851,9 @@ msgstr ""
msgid "Merge Requests in Review"
msgstr ""
+msgid "Merge automatically (%{strategy})"
+msgstr ""
+
msgid "Merge commit message"
msgstr ""
@@ -12726,6 +13007,12 @@ msgstr ""
msgid "Merged branches are being deleted. This can take some time depending on the number of branches. Please refresh the page to see changes."
msgstr ""
+msgid "Merged this merge request."
+msgstr ""
+
+msgid "Merges this merge request immediately."
+msgstr ""
+
msgid "Merges this merge request when the pipeline succeeds."
msgstr ""
@@ -12832,12 +13119,15 @@ msgid "Metrics|Edit metric"
msgid_plural "Metrics|Edit metrics"
msgstr[0] ""
-msgid "Metrics|Environment"
+msgid "Metrics|Expand panel"
msgstr ""
msgid "Metrics|For grouping similar metrics"
msgstr ""
+msgid "Metrics|Go back (Esc)"
+msgstr ""
+
msgid "Metrics|Invalid time range, please verify."
msgstr ""
@@ -12868,9 +13158,6 @@ msgstr ""
msgid "Metrics|Refresh dashboard"
msgstr ""
-msgid "Metrics|Show last"
-msgstr ""
-
msgid "Metrics|There was an error creating the dashboard."
msgstr ""
@@ -12949,6 +13236,9 @@ msgstr ""
msgid "Microsoft Azure"
msgstr ""
+msgid "Middleman project with Static Site Editor support"
+msgstr ""
+
msgid "Migrated %{success_count}/%{total_count} files."
msgstr ""
@@ -13392,10 +13682,10 @@ msgstr ""
msgid "Next"
msgstr ""
-msgid "Next file in diff (MRs only)"
+msgid "Next file in diff"
msgstr ""
-msgid "Next unresolved discussion (MRs only)"
+msgid "Next unresolved discussion"
msgstr ""
msgid "Nickname"
@@ -13455,6 +13745,9 @@ msgstr ""
msgid "No changes between %{ref_start}%{source_branch}%{ref_end} and %{ref_start}%{target_branch}%{ref_end}"
msgstr ""
+msgid "No child epics match applied filters"
+msgstr ""
+
msgid "No connection could be made to a Gitaly Server, please check your logs!"
msgstr ""
@@ -13593,15 +13886,6 @@ msgstr ""
msgid "No thanks, don't show this again"
msgstr ""
-msgid "No vulnerabilities found for this group"
-msgstr ""
-
-msgid "No vulnerabilities found for this pipeline"
-msgstr ""
-
-msgid "No vulnerabilities found for this project"
-msgstr ""
-
msgid "No vulnerabilities present"
msgstr ""
@@ -13725,6 +14009,9 @@ msgstr ""
msgid "Nothing to preview."
msgstr ""
+msgid "Nothing to synchronize"
+msgstr ""
+
msgid "Notification events"
msgstr ""
@@ -13896,6 +14183,9 @@ msgstr ""
msgid "Once removed, the fork relationship cannot be restored and you will no longer be able to send merge requests to the source."
msgstr ""
+msgid "Once the exported file is ready you can download it from this page."
+msgstr ""
+
msgid "Once the exported file is ready, you will receive a notification email with a download link, or you can download it from this page."
msgstr ""
@@ -13996,9 +14286,6 @@ msgstr ""
msgid "Open sidebar"
msgstr ""
-msgid "Open source software to collaborate on code"
-msgstr ""
-
msgid "Open: %{openIssuesCount}"
msgstr ""
@@ -14101,6 +14388,9 @@ msgstr ""
msgid "OutdatedBrowser|You can provide feedback %{feedback_link_start}on this issue%{feedback_link_end} or via your usual support channels."
msgstr ""
+msgid "Overridden"
+msgstr ""
+
msgid "Overview"
msgstr ""
@@ -14152,6 +14442,9 @@ msgstr ""
msgid "PackageRegistry|Conan Command"
msgstr ""
+msgid "PackageRegistry|Copy .pypirc content"
+msgstr ""
+
msgid "PackageRegistry|Copy Conan Command"
msgstr ""
@@ -14173,6 +14466,9 @@ msgstr ""
msgid "PackageRegistry|Copy NuGet Setup Command"
msgstr ""
+msgid "PackageRegistry|Copy Pip command"
+msgstr ""
+
msgid "PackageRegistry|Copy and paste this inside your %{codeStart}pom.xml%{codeEnd} %{codeStart}dependencies%{codeEnd} block."
msgstr ""
@@ -14206,6 +14502,12 @@ msgstr ""
msgid "PackageRegistry|For more information on the NuGet registry, %{linkStart}see the documentation%{linkEnd}."
msgstr ""
+msgid "PackageRegistry|For more information on the PyPi registry, %{linkStart}see the documentation%{linkEnd}."
+msgstr ""
+
+msgid "PackageRegistry|If you haven't already done so, you will need to add the below to your %{codeStart}.pypirc%{codeEnd} file."
+msgstr ""
+
msgid "PackageRegistry|If you haven't already done so, you will need to add the below to your %{codeStart}pom.xml%{codeEnd} file."
msgstr ""
@@ -14215,6 +14517,9 @@ msgstr ""
msgid "PackageRegistry|Learn how to %{noPackagesLinkStart}publish and share your packages%{noPackagesLinkEnd} with GitLab."
msgstr ""
+msgid "PackageRegistry|Manually Published"
+msgstr ""
+
msgid "PackageRegistry|Maven"
msgstr ""
@@ -14233,12 +14538,18 @@ msgstr ""
msgid "PackageRegistry|NuGet Command"
msgstr ""
+msgid "PackageRegistry|Pip Command"
+msgstr ""
+
msgid "PackageRegistry|Pipeline %{linkStart}%{linkEnd} triggered %{timestamp} by %{author}"
msgstr ""
msgid "PackageRegistry|Published to the repository at %{timestamp}"
msgstr ""
+msgid "PackageRegistry|PyPi"
+msgstr ""
+
msgid "PackageRegistry|Registry Setup"
msgstr ""
@@ -14275,6 +14586,9 @@ msgstr ""
msgid "PackageRegistry|npm"
msgstr ""
+msgid "PackageRegistry|published by %{author}"
+msgstr ""
+
msgid "PackageRegistry|yarn"
msgstr ""
@@ -14290,6 +14604,9 @@ msgstr ""
msgid "PackageType|NuGet"
msgstr ""
+msgid "PackageType|PyPi"
+msgstr ""
+
msgid "Packages"
msgstr ""
@@ -14383,6 +14700,9 @@ msgstr ""
msgid "Past due"
msgstr ""
+msgid "Paste a machine public key here. Read more about how to generate it"
+msgstr ""
+
msgid "Paste a machine public key here. Read more about how to generate it %{link_start}here%{link_end}"
msgstr ""
@@ -14398,9 +14718,6 @@ msgstr ""
msgid "Path"
msgstr ""
-msgid "Path, transfer, remove"
-msgstr ""
-
msgid "Path:"
msgstr ""
@@ -14431,7 +14748,7 @@ msgstr ""
msgid "Percentage"
msgstr ""
-msgid "Perform advanced options such as changing path, transferring, or removing the group."
+msgid "Perform advanced options such as changing path, transferring, exporting, or removing the group."
msgstr ""
msgid "Perform common operations on GitLab project"
@@ -14512,9 +14829,6 @@ msgstr ""
msgid "Pipeline minutes quota"
msgstr ""
-msgid "Pipeline quota"
-msgstr ""
-
msgid "Pipeline subscriptions"
msgstr ""
@@ -14524,6 +14838,9 @@ msgstr ""
msgid "Pipeline: %{status}"
msgstr ""
+msgid "PipelineCharts|CI / CD Analytics"
+msgstr ""
+
msgid "PipelineCharts|Failed:"
msgstr ""
@@ -14602,15 +14919,6 @@ msgstr ""
msgid "Pipelines settings for '%{project_name}' were successfully updated."
msgstr ""
-msgid "Pipelines| to purchase more minutes."
-msgstr ""
-
-msgid "Pipelines|%{namespace_name} has exceeded its pipeline minutes quota."
-msgstr ""
-
-msgid "Pipelines|%{namespace_name} has less than %{notification_level}%% of CI minutes available."
-msgstr ""
-
msgid "Pipelines|API"
msgstr ""
@@ -14632,10 +14940,13 @@ msgstr ""
msgid "Pipelines|Get started with Pipelines"
msgstr ""
-msgid "Pipelines|Loading Pipelines"
+msgid "Pipelines|Group %{namespace_name} has %{percentage}%% or less Shared Runner Pipeline minutes remaining. Once it runs out, no new jobs or pipelines in its projects will run."
+msgstr ""
+
+msgid "Pipelines|Group %{namespace_name} has exceeded its pipeline minutes quota. Unless you buy additional pipeline minutes, no new jobs or pipelines in its projects will run."
msgstr ""
-msgid "Pipelines|Pipelines will not run anymore on shared Runners."
+msgid "Pipelines|Loading Pipelines"
msgstr ""
msgid "Pipelines|Project cache successfully reset."
@@ -14866,6 +15177,9 @@ msgstr ""
msgid "Please select"
msgstr ""
+msgid "Please select a Jira project"
+msgstr ""
+
msgid "Please select a country"
msgstr ""
@@ -14911,6 +15225,9 @@ msgstr ""
msgid "Please wait while we import the repository for you. Refresh at will."
msgstr ""
+msgid "Plugins directory is deprecated and will be removed in 14.0. Please move this file into /file_hooks directory."
+msgstr ""
+
msgid "Pod does not exist"
msgstr ""
@@ -15028,7 +15345,7 @@ msgstr ""
msgid "Prevent users from changing their profile name"
msgstr ""
-msgid "Prevent users from modifing merge request approvers list"
+msgid "Prevent users from modifying merge request approvers list"
msgstr ""
msgid "Prevent users from performing write operations on GitLab while performing maintenance."
@@ -15049,10 +15366,10 @@ msgstr ""
msgid "Previous Artifacts"
msgstr ""
-msgid "Previous file in diff (MRs only)"
+msgid "Previous file in diff"
msgstr ""
-msgid "Previous unresolved discussion (MRs only)"
+msgid "Previous unresolved discussion"
msgstr ""
msgid "Primary"
@@ -15085,6 +15402,9 @@ msgstr ""
msgid "Private profile"
msgstr ""
+msgid "Private projects Minutes cost factor"
+msgstr ""
+
msgid "Private projects can be created in your personal namespace with:"
msgstr ""
@@ -15913,6 +16233,9 @@ msgstr ""
msgid "ProjectSettings|With GitLab Pages you can host your static websites on GitLab"
msgstr ""
+msgid "ProjectSettings|With Metrics Dashboard you can visualize this project performance metrics"
+msgstr ""
+
msgid "ProjectTemplates|.NET Core"
msgstr ""
@@ -15976,6 +16299,9 @@ msgstr ""
msgid "ProjectTemplates|Spring"
msgstr ""
+msgid "ProjectTemplates|Static Site Editor/Middleman"
+msgstr ""
+
msgid "ProjectTemplates|iOS (Swift)"
msgstr ""
@@ -16195,13 +16521,13 @@ msgstr ""
msgid "PrometheusService|Prometheus is being automatically managed on your clusters"
msgstr ""
-msgid "PrometheusService|These metrics will only be monitored after your first deployment to an environment"
+msgid "PrometheusService|Select the Active checkbox to override the Auto Configuration with custom settings. If unchecked, Auto Configuration settings are used."
msgstr ""
-msgid "PrometheusService|Time-series monitoring service"
+msgid "PrometheusService|These metrics will only be monitored after your first deployment to an environment"
msgstr ""
-msgid "PrometheusService|To enable manual configuration, uninstall Prometheus from your clusters"
+msgid "PrometheusService|Time-series monitoring service"
msgstr ""
msgid "PrometheusService|To enable the installation of Prometheus on your clusters, deactivate the manual configuration below"
@@ -16387,6 +16713,9 @@ msgstr ""
msgid "Protip:"
msgstr ""
+msgid "Protocol"
+msgstr ""
+
msgid "Provider"
msgstr ""
@@ -16411,6 +16740,9 @@ msgstr ""
msgid "Public pipelines"
msgstr ""
+msgid "Public projects Minutes cost factor"
+msgstr ""
+
msgid "Pull"
msgstr ""
@@ -16610,6 +16942,9 @@ msgid "Refreshing in a second to show the updated status..."
msgid_plural "Refreshing in %d seconds to show the updated status..."
msgstr[0] ""
+msgid "Regenerate export"
+msgstr ""
+
msgid "Regenerate instance ID"
msgstr ""
@@ -16986,9 +17321,28 @@ msgstr ""
msgid "Reports|%{combinedString} and %{resolvedString}"
msgstr ""
+msgid "Reports|Accessibility scanning detected %d issue for the source branch only"
+msgid_plural "Reports|Accessibility scanning detected %d issues for the source branch only"
+msgstr[0] ""
+
+msgid "Reports|Accessibility scanning detected no issues for the source branch only"
+msgstr ""
+
+msgid "Reports|Accessibility scanning failed loading results"
+msgstr ""
+
+msgid "Reports|Accessibility scanning results are being parsed"
+msgstr ""
+
msgid "Reports|Actions"
msgstr ""
+msgid "Reports|An error occured while loading report"
+msgstr ""
+
+msgid "Reports|An error occurred while loading %{name} results"
+msgstr ""
+
msgid "Reports|Class"
msgstr ""
@@ -17094,7 +17448,7 @@ msgstr ""
msgid "Requested %{time_ago}"
msgstr ""
-msgid "Requested design version does not exist"
+msgid "Requested design version does not exist."
msgstr ""
msgid "Requested states are invalid"
@@ -17276,6 +17630,9 @@ msgstr ""
msgid "Resync"
msgstr ""
+msgid "Resync all"
+msgstr ""
+
msgid "Resync all %{replicableType}"
msgstr ""
@@ -17343,6 +17700,9 @@ msgstr ""
msgid "Revoked personal access token %{personal_access_token_name}!"
msgstr ""
+msgid "Revoked project access token %{project_access_token_name}!"
+msgstr ""
+
msgid "RightSidebar|adding a"
msgstr ""
@@ -17391,6 +17751,9 @@ msgstr ""
msgid "Runner token"
msgstr ""
+msgid "Runner tokens"
+msgstr ""
+
msgid "Runner was not updated."
msgstr ""
@@ -17445,6 +17808,9 @@ msgstr ""
msgid "SAML SSO for %{group_name}"
msgstr ""
+msgid "SAML discovery tokens"
+msgstr ""
+
msgid "SAML for %{group_name}"
msgstr ""
@@ -17529,12 +17895,18 @@ msgstr ""
msgid "Scheduled"
msgstr ""
+msgid "Scheduled to merge this merge request (%{strategy})."
+msgstr ""
+
msgid "Scheduled to merge this merge request when the pipeline succeeds."
msgstr ""
msgid "Schedules"
msgstr ""
+msgid "Schedules to merge this merge request (%{strategy})."
+msgstr ""
+
msgid "Scheduling"
msgstr ""
@@ -17592,6 +17964,9 @@ msgstr ""
msgid "Search branches and tags"
msgstr ""
+msgid "Search by author"
+msgstr ""
+
msgid "Search files"
msgstr ""
@@ -17729,10 +18104,6 @@ msgid "SearchResults|snippet"
msgid_plural "SearchResults|snippets"
msgstr[0] ""
-msgid "SearchResults|snippet result"
-msgid_plural "SearchResults|snippet results"
-msgstr[0] ""
-
msgid "SearchResults|user"
msgid_plural "SearchResults|users"
msgstr[0] ""
@@ -17741,6 +18112,9 @@ msgid "SearchResults|wiki result"
msgid_plural "SearchResults|wiki results"
msgstr[0] ""
+msgid "Searching by both author and message is currently not supported."
+msgstr ""
+
msgid "Seat Link"
msgstr ""
@@ -17768,184 +18142,232 @@ msgstr ""
msgid "Security Dashboard"
msgstr ""
-msgid "Security Dashboard|Error fetching the vulnerability counts. Please check your network connection and try again."
+msgid "Security configuration help link"
+msgstr ""
+
+msgid "Security dashboard"
msgstr ""
-msgid "Security Dashboard|Error fetching the vulnerability list. Please check your network connection and try again."
+msgid "Security report is out of date. Please update your branch with the latest changes from the target branch (%{targetBranchName})"
msgstr ""
-msgid "Security Dashboard|Issue Created"
+msgid "Security report is out of date. Run %{newPipelineLinkStart}a new pipeline%{newPipelineLinkEnd} for the target branch (%{targetBranchName})"
msgstr ""
-msgid "Security Reports|Comment added to '%{vulnerabilityName}'"
+msgid "SecurityConfiguration|Configured"
msgstr ""
-msgid "Security Reports|Comment deleted on '%{vulnerabilityName}'"
+msgid "SecurityConfiguration|Feature"
msgstr ""
-msgid "Security Reports|Comment edited on '%{vulnerabilityName}'"
+msgid "SecurityConfiguration|Feature documentation for %{featureName}"
msgstr ""
-msgid "Security Reports|Create issue"
+msgid "SecurityConfiguration|Not yet configured"
msgstr ""
-msgid "Security Reports|Dismiss vulnerability"
+msgid "SecurityConfiguration|Secure features"
msgstr ""
-msgid "Security Reports|Dismissed '%{vulnerabilityName}'"
+msgid "SecurityConfiguration|Status"
msgstr ""
-msgid "Security Reports|Dismissed '%{vulnerabilityName}'. Turn off the hide dismissed toggle to view."
+msgid "SecurityReports|%{firstProject} and %{secondProject}"
msgstr ""
-msgid "Security Reports|Either you don't have permission to view this dashboard or the dashboard has not been setup. Please check your permission settings with your administrator or check your dashboard configurations to proceed."
+msgid "SecurityReports|%{firstProject}, %{secondProject}, and %{rest}"
msgstr ""
-msgid "Security Reports|Learn more about setting up your dashboard"
+msgid "SecurityReports|Add a project to your dashboard"
msgstr ""
-msgid "Security Reports|More info"
+msgid "SecurityReports|Add or remove projects from your dashboard"
msgstr ""
-msgid "Security Reports|Oops, something doesn't seem right."
+msgid "SecurityReports|Add projects"
msgstr ""
-msgid "Security Reports|Security reports can only be accessed by authorized users."
+msgid "SecurityReports|Comment added to '%{vulnerabilityName}'"
msgstr ""
-msgid "Security Reports|There was an error adding the comment."
+msgid "SecurityReports|Comment deleted on '%{vulnerabilityName}'"
msgstr ""
-msgid "Security Reports|There was an error creating the issue."
+msgid "SecurityReports|Comment edited on '%{vulnerabilityName}'"
msgstr ""
-msgid "Security Reports|There was an error creating the merge request."
+msgid "SecurityReports|Create issue"
msgstr ""
-msgid "Security Reports|There was an error deleting the comment."
+msgid "SecurityReports|Dismiss Selected"
msgstr ""
-msgid "Security Reports|There was an error dismissing the vulnerabilities."
+msgid "SecurityReports|Dismiss vulnerability"
msgstr ""
-msgid "Security Reports|There was an error dismissing the vulnerability."
+msgid "SecurityReports|Dismissed '%{vulnerabilityName}'"
msgstr ""
-msgid "Security Reports|There was an error reverting the dismissal."
+msgid "SecurityReports|Dismissed '%{vulnerabilityName}'. Turn off the hide dismissed toggle to view."
msgstr ""
-msgid "Security Reports|There was an error reverting this dismissal."
+msgid "SecurityReports|Each vulnerability now has a unique page that can be directly linked to, shared, referenced, and tracked as the single source of truth. Vulnerability occurrences also persist across scanner runs, which improves tracking and visibility and reduces duplicates between scans."
msgstr ""
-msgid "Security Reports|Undo dismiss"
+msgid "SecurityReports|Edit dashboard"
msgstr ""
-msgid "Security Reports|You do not have sufficient permissions to access this report"
+msgid "SecurityReports|Either you don't have permission to view this dashboard or the dashboard has not been setup. Please check your permission settings with your administrator or check your dashboard configurations to proceed."
msgstr ""
-msgid "Security Reports|You must sign in as an authorized user to see this report"
+msgid "SecurityReports|Error fetching the vulnerability counts. Please check your network connection and try again."
msgstr ""
-msgid "Security configuration help link"
+msgid "SecurityReports|Error fetching the vulnerability list. Please check your network connection and try again."
msgstr ""
-msgid "Security dashboard"
+msgid "SecurityReports|False positive"
msgstr ""
-msgid "Security report is out of date. Please update your branch with the latest changes from the target branch (%{targetBranchName})"
+msgid "SecurityReports|Hide dismissed"
msgstr ""
-msgid "Security report is out of date. Run %{newPipelineLinkStart}a new pipeline%{newPipelineLinkEnd} for the target branch (%{targetBranchName})"
+msgid "SecurityReports|Introducing standalone vulnerabilities"
msgstr ""
-msgid "SecurityConfiguration|Configured"
+msgid "SecurityReports|Issue Created"
msgstr ""
-msgid "SecurityConfiguration|Feature"
+msgid "SecurityReports|Learn More"
msgstr ""
-msgid "SecurityConfiguration|Feature documentation for %{featureName}"
+msgid "SecurityReports|Learn more about setting up your dashboard"
msgstr ""
-msgid "SecurityConfiguration|Not yet configured"
+msgid "SecurityReports|Load more vulnerabilities"
msgstr ""
-msgid "SecurityConfiguration|Secure features"
+msgid "SecurityReports|Monitor vulnerabilities in your code"
msgstr ""
-msgid "SecurityConfiguration|Status"
+msgid "SecurityReports|More info"
msgstr ""
-msgid "SecurityDashboard|%{firstProject} and %{secondProject}"
+msgid "SecurityReports|More information"
msgstr ""
-msgid "SecurityDashboard|%{firstProject}, %{secondProject}, and %{rest}"
+msgid "SecurityReports|No vulnerabilities found for dashboard"
msgstr ""
-msgid "SecurityDashboard|Add a project to your dashboard"
+msgid "SecurityReports|No vulnerabilities found for this group"
msgstr ""
-msgid "SecurityDashboard|Add or remove projects from your dashboard"
+msgid "SecurityReports|No vulnerabilities found for this pipeline"
msgstr ""
-msgid "SecurityDashboard|Add projects"
+msgid "SecurityReports|No vulnerabilities found for this project"
msgstr ""
-msgid "SecurityDashboard|Edit dashboard"
+msgid "SecurityReports|Oops, something doesn't seem right."
msgstr ""
-msgid "SecurityDashboard|Hide dismissed"
+msgid "SecurityReports|Pipeline %{pipelineLink} triggered %{timeago} by %{user}"
msgstr ""
-msgid "SecurityDashboard|Monitor vulnerabilities in your code"
+msgid "SecurityReports|Project"
msgstr ""
-msgid "SecurityDashboard|More information"
+msgid "SecurityReports|Projects added"
msgstr ""
-msgid "SecurityDashboard|No vulnerabilities found for dashboard"
+msgid "SecurityReports|Remove project from dashboard"
msgstr ""
-msgid "SecurityDashboard|Pipeline %{pipelineLink} triggered %{timeago} by %{user}"
+msgid "SecurityReports|Report type"
msgstr ""
-msgid "SecurityDashboard|Project"
+msgid "SecurityReports|Return to dashboard"
msgstr ""
-msgid "SecurityDashboard|Projects added"
+msgid "SecurityReports|Security Dashboard"
msgstr ""
-msgid "SecurityDashboard|Remove project from dashboard"
+msgid "SecurityReports|Security reports can only be accessed by authorized users."
msgstr ""
-msgid "SecurityDashboard|Report type"
+msgid "SecurityReports|Select a project to add by using the project search field above."
msgstr ""
-msgid "SecurityDashboard|Return to dashboard"
+msgid "SecurityReports|Select a reason"
msgstr ""
-msgid "SecurityDashboard|Security Dashboard"
+msgid "SecurityReports|Severity"
msgstr ""
-msgid "SecurityDashboard|Select a project to add by using the project search field above."
+msgid "SecurityReports|Status"
msgstr ""
-msgid "SecurityDashboard|Severity"
+msgid "SecurityReports|The security dashboard displays the latest security findings for projects you wish to monitor. Select \"Edit dashboard\" to add and remove projects."
msgstr ""
-msgid "SecurityDashboard|Status"
+msgid "SecurityReports|The security dashboard displays the latest security report. Use it to find and fix vulnerabilities."
msgstr ""
-msgid "SecurityDashboard|The security dashboard displays the latest security findings for projects you wish to monitor. Select \"Edit dashboard\" to add and remove projects."
+msgid "SecurityReports|There was an error adding the comment."
msgstr ""
-msgid "SecurityDashboard|The security dashboard displays the latest security report. Use it to find and fix vulnerabilities."
+msgid "SecurityReports|There was an error creating the issue."
msgstr ""
-msgid "SecurityDashboard|There was an error while generating the report."
+msgid "SecurityReports|There was an error creating the merge request."
msgstr ""
-msgid "SecurityDashboard|Unable to add %{invalidProjects}"
+msgid "SecurityReports|There was an error deleting the comment."
+msgstr ""
+
+msgid "SecurityReports|There was an error dismissing the vulnerabilities."
+msgstr ""
+
+msgid "SecurityReports|There was an error dismissing the vulnerability."
+msgstr ""
+
+msgid "SecurityReports|There was an error reverting the dismissal."
+msgstr ""
+
+msgid "SecurityReports|There was an error reverting this dismissal."
+msgstr ""
+
+msgid "SecurityReports|There was an error while generating the report."
+msgstr ""
+
+msgid "SecurityReports|Unable to add %{invalidProjects}"
+msgstr ""
+
+msgid "SecurityReports|Undo dismiss"
+msgstr ""
+
+msgid "SecurityReports|While it's rare to have no vulnerabilities for your group, it can happen. In any event, we ask that you double check your settings to make sure you've set up your dashboard correctly."
+msgstr ""
+
+msgid "SecurityReports|While it's rare to have no vulnerabilities for your pipeline, it can happen. In any event, we ask that you double check your settings to make sure all security scanning jobs have passed successfully."
+msgstr ""
+
+msgid "SecurityReports|While it's rare to have no vulnerabilities for your project, it can happen. In any event, we ask that you double check your settings to make sure you've set up your dashboard correctly."
+msgstr ""
+
+msgid "SecurityReports|While it's rare to have no vulnerabilities, it can happen. In any event, we ask that you please double check your settings to make sure you've set up your dashboard correctly."
+msgstr ""
+
+msgid "SecurityReports|Won't fix / Accept risk"
+msgstr ""
+
+msgid "SecurityReports|You do not have sufficient permissions to access this report"
+msgstr ""
+
+msgid "SecurityReports|You must sign in as an authorized user to see this report"
+msgstr ""
+
+msgid "SecurityReports|[No reason]"
msgstr ""
msgid "See GitLab's %{password_policy_guidelines}"
@@ -17957,6 +18379,9 @@ msgstr ""
msgid "See the affected projects in the GitLab admin panel"
msgstr ""
+msgid "See what's new at GitLab"
+msgstr ""
+
msgid "Select"
msgstr ""
@@ -18362,9 +18787,6 @@ msgstr ""
msgid "Set up Jira Integration"
msgstr ""
-msgid "Set up Jira Integration illustration"
-msgstr ""
-
msgid "Set up a %{type} Runner automatically"
msgstr ""
@@ -18449,6 +18871,9 @@ msgstr ""
msgid "Severity: %{severity}"
msgstr ""
+msgid "Shards (%{shards})"
+msgstr ""
+
msgid "Shards to synchronize"
msgstr ""
@@ -18762,6 +19187,9 @@ msgstr ""
msgid "Snowplow"
msgstr ""
+msgid "Some child epics may be hidden due to applied filters"
+msgstr ""
+
msgid "Some email servers do not support overriding the email sender name. Enable this option to include the name of the author of the issue, merge request or comment in the email body instead."
msgstr ""
@@ -18903,6 +19331,9 @@ msgstr ""
msgid "Something went wrong while updating your list settings"
msgstr ""
+msgid "Something went wrong with your automatic subscription renewal"
+msgstr ""
+
msgid "Something went wrong, unable to add %{project} to dashboard"
msgstr ""
@@ -19188,6 +19619,9 @@ msgstr ""
msgid "Stage removed"
msgstr ""
+msgid "Standard"
+msgstr ""
+
msgid "Star a label to make it a priority label. Order the prioritized labels to change their relative priority, by dragging."
msgstr ""
@@ -19311,6 +19745,9 @@ msgstr ""
msgid "Static Application Security Testing (SAST)"
msgstr ""
+msgid "StaticSiteEditor|An error occurred while submitting your changes."
+msgstr ""
+
msgid "StaticSiteEditor|Branch could not be created."
msgstr ""
@@ -19320,18 +19757,30 @@ msgstr ""
msgid "StaticSiteEditor|Could not create merge request."
msgstr ""
+msgid "StaticSiteEditor|Incompatible file content"
+msgstr ""
+
msgid "StaticSiteEditor|Return to site"
msgstr ""
+msgid "StaticSiteEditor|Static site editor"
+msgstr ""
+
msgid "StaticSiteEditor|Success!"
msgstr ""
msgid "StaticSiteEditor|Summary of changes"
msgstr ""
+msgid "StaticSiteEditor|The Static Site Editor is currently configured to only edit Markdown content on pages generated from Middleman. Visit the documentation to learn more about configuring your site to use the Static Site Editor."
+msgstr ""
+
msgid "StaticSiteEditor|Update %{sourcePath} file"
msgstr ""
+msgid "StaticSiteEditor|View documentation"
+msgstr ""
+
msgid "StaticSiteEditor|View merge request"
msgstr ""
@@ -19389,6 +19838,12 @@ msgstr ""
msgid "StatusPage|Status page"
msgstr ""
+msgid "StatusPage|Status page URL"
+msgstr ""
+
+msgid "StatusPage|Status page frontend documentation"
+msgstr ""
+
msgid "StatusPage|To publish incidents to an external status page, GitLab will store a JSON file in your Amazon S3 account in a location accessible to your external status page service. Make sure to also set up %{docsLink}"
msgstr ""
@@ -19737,6 +20192,12 @@ msgstr ""
msgid "Synced"
msgstr ""
+msgid "Synchronization disabled"
+msgstr ""
+
+msgid "Synchronization of container repositories is disabled."
+msgstr ""
+
msgid "System"
msgstr ""
@@ -19890,6 +20351,9 @@ msgstr ""
msgid "Telephone number"
msgstr ""
+msgid "Telephone number (Optional)"
+msgstr ""
+
msgid "Template"
msgstr ""
@@ -20193,6 +20657,9 @@ msgstr ""
msgid "The license was successfully uploaded and is now active. You can see the details below."
msgstr ""
+msgid "The license was successfully uploaded and will be active from %{starts_at}. You can see the details below."
+msgstr ""
+
msgid "The maximum file size allowed is %{size}."
msgstr ""
@@ -20514,6 +20981,9 @@ msgstr ""
msgid "There was an error getting the epic participants."
msgstr ""
+msgid "There was an error importing the Jira project."
+msgstr ""
+
msgid "There was an error loading users activity calendar."
msgstr ""
@@ -20529,7 +20999,7 @@ msgstr ""
msgid "There was an error resetting user pipeline minutes."
msgstr ""
-msgid "There was an error saving this Geo Node"
+msgid "There was an error saving this Geo Node."
msgstr ""
msgid "There was an error saving your changes."
@@ -20592,6 +21062,9 @@ msgstr ""
msgid "They can be managed using the %{link}."
msgstr ""
+msgid "Third Party Advisory Link"
+msgstr ""
+
msgid "Third party offers"
msgstr ""
@@ -20628,6 +21101,9 @@ msgstr ""
msgid "This application will be able to:"
msgstr ""
+msgid "This attachment has been truncated to avoid exceeding the maximum allowed attachment size of 15MB. %{written_count} of %{issues_count} issues have been included. Consider re-exporting with a narrower selection of issues."
+msgstr ""
+
msgid "This block is self-referential"
msgstr ""
@@ -20769,6 +21245,12 @@ msgstr ""
msgid "This issue is confidential"
msgstr ""
+msgid "This issue is currently blocked by the following issues: %{issues}."
+msgstr ""
+
+msgid "This issue is in a child epic of the filtered epic"
+msgstr ""
+
msgid "This issue is locked."
msgstr ""
@@ -20967,7 +21449,10 @@ msgstr ""
msgid "This user will be the author of all events in the activity feed that are the result of an update, like new branches being created or new commits being pushed to existing branches. Upon creation or when reassigning you can only assign yourself to be the mirror user."
msgstr ""
-msgid "This variable can not be masked"
+msgid "This variable can not be masked."
+msgstr ""
+
+msgid "This variable does not match the expected pattern."
msgstr ""
msgid "This will help us personalize your onboarding experience."
@@ -20997,10 +21482,10 @@ msgstr ""
msgid "ThreatMonitoring|Application firewall not detected"
msgstr ""
-msgid "ThreatMonitoring|Container Network Policy"
+msgid "ThreatMonitoring|Container Network Policies are not installed or have been disabled. To view this data, ensure your Network Policies are installed and enabled for your cluster."
msgstr ""
-msgid "ThreatMonitoring|Container NetworkPolicies are not installed or has been disabled. To view this data, ensure you NetworkPolicies are installed and enabled for your cluster."
+msgid "ThreatMonitoring|Container Network Policy"
msgstr ""
msgid "ThreatMonitoring|Container NetworkPolicies not detected"
@@ -21033,7 +21518,7 @@ msgstr ""
msgid "ThreatMonitoring|Something went wrong, unable to fetch statistics"
msgstr ""
-msgid "ThreatMonitoring|The firewall is not installed or has been disabled. To view this data, ensure you firewall is installed and enabled for your cluster."
+msgid "ThreatMonitoring|The firewall is not installed or has been disabled. To view this data, ensure the web application firewall is installed and enabled for your cluster."
msgstr ""
msgid "ThreatMonitoring|The graph below is an overview of traffic coming to your application as tracked by the Web Application Firewall (WAF). View the docs for instructions on how to access the WAF logs to see what type of malicious traffic is trying to access your app. The docs link is also accessible by clicking the \"?\" icon next to the title below."
@@ -21102,6 +21587,9 @@ msgstr ""
msgid "Time in seconds GitLab will wait for a response from the external service. When the service does not respond in time, access will be denied."
msgstr ""
+msgid "Time of import: %{importTime}"
+msgstr ""
+
msgid "Time remaining"
msgstr ""
@@ -21284,7 +21772,7 @@ msgstr ""
msgid "Title:"
msgstr ""
-msgid "Titles and Filenames"
+msgid "Titles and Descriptions"
msgstr ""
msgid "To"
@@ -21491,7 +21979,7 @@ msgstr ""
msgid "Too many projects enabled. You will need to manage them via the console or the API."
msgstr ""
-msgid "Topics"
+msgid "Topics (optional)"
msgstr ""
msgid "Total"
@@ -21653,6 +22141,9 @@ msgstr ""
msgid "Try all GitLab has to offer for 30 days."
msgstr ""
+msgid "Try changing or removing filters."
+msgstr ""
+
msgid "Try to fork again"
msgstr ""
@@ -21746,6 +22237,9 @@ msgstr ""
msgid "Unable to connect to server: %{error}"
msgstr ""
+msgid "Unable to connect to the Jira instance. Please check your Jira integration configuration."
+msgstr ""
+
msgid "Unable to convert Kubernetes logs encoding to UTF-8"
msgstr ""
@@ -22687,6 +23181,9 @@ msgstr ""
msgid "View issue"
msgstr ""
+msgid "View issues"
+msgstr ""
+
msgid "View it on GitLab"
msgstr ""
@@ -22864,6 +23361,15 @@ msgstr ""
msgid "VulnerabilityManagement|Resolved %{timeago} by %{user}"
msgstr ""
+msgid "VulnerabilityManagement|Something went wrong while trying to delete the comment. Please try again later."
+msgstr ""
+
+msgid "VulnerabilityManagement|Something went wrong while trying to retrieve the vulnerability history. Please try again later."
+msgstr ""
+
+msgid "VulnerabilityManagement|Something went wrong while trying to save the comment. Please try again later."
+msgstr ""
+
msgid "VulnerabilityManagement|Something went wrong, could not create an issue."
msgstr ""
@@ -22894,6 +23400,9 @@ msgstr ""
msgid "VulnerabilityStatusTypes|Resolved"
msgstr ""
+msgid "Vulnerability|%{scannerName} (version %{scannerVersion})"
+msgstr ""
+
msgid "Vulnerability|Class"
msgstr ""
@@ -22927,7 +23436,10 @@ msgstr ""
msgid "Vulnerability|Project"
msgstr ""
-msgid "Vulnerability|Report Type"
+msgid "Vulnerability|Scanner Provider"
+msgstr ""
+
+msgid "Vulnerability|Scanner Type"
msgstr ""
msgid "Vulnerability|Severity"
@@ -22981,9 +23493,15 @@ msgstr ""
msgid "We sent you an email with reset password instructions"
msgstr ""
+msgid "We tried to automatically renew your %{strong}%{plan_name}%{strong_close} subscription for %{strong}%{namespace_name}%{strong_close} on %{expires_on} but something went wrong so your subscription was downgraded to the free plan. Don't worry, your data is safe. We suggest you check your payment method and get in touch with our support team (%{support_link}). They'll gladly help with your subscription renewal."
+msgstr ""
+
msgid "We want to be sure it is you, please confirm you are not a robot."
msgstr ""
+msgid "We will automatically renew your %{strong}%{plan_name}%{strong_close} subscription for %{strong}%{namespace_name}%{strong_close} on %{strong}%{expires_on}%{strong_close}. There's nothing that you need to do, we'll let you know when the renewal is complete. Need more seats, a higher plan or just want to review your payment method?"
+msgstr ""
+
msgid "We've found no vulnerabilities"
msgstr ""
@@ -23087,15 +23605,6 @@ msgstr ""
msgid "When:"
msgstr ""
-msgid "While it's rare to have no vulnerabilities for your group, it can happen. In any event, we ask that you double check your settings to make sure you've set up your dashboard correctly."
-msgstr ""
-
-msgid "While it's rare to have no vulnerabilities for your pipeline, it can happen. In any event, we ask that you double check your settings to make sure all security scanning jobs have passed successfully."
-msgstr ""
-
-msgid "While it's rare to have no vulnerabilities for your project, it can happen. In any event, we ask that you double check your settings to make sure you've set up your dashboard correctly."
-msgstr ""
-
msgid "While it's rare to have no vulnerabilities, it can happen. In any event, we ask that you please double check your settings to make sure you've set up your dashboard correctly."
msgstr ""
@@ -23126,9 +23635,6 @@ msgstr ""
msgid "Wiki"
msgstr ""
-msgid "Wiki pages"
-msgstr ""
-
msgid "Wiki was successfully updated."
msgstr ""
@@ -23312,6 +23818,9 @@ msgstr ""
msgid "Yes, add it"
msgstr ""
+msgid "Yes, close issue"
+msgstr ""
+
msgid "Yes, let me map Google Code users to full names or GitLab users."
msgstr ""
@@ -23507,6 +24016,9 @@ msgstr ""
msgid "You could not create a new trigger."
msgstr ""
+msgid "You didn't renew your %{strong}%{plan_name}%{strong_close} subscription for %{strong}%{namespace_name}%{strong_close} so it was downgraded to the free plan."
+msgstr ""
+
msgid "You didn't renew your %{strong}%{plan_name}%{strong_close} subscription so it was downgraded to the GitLab Core Plan."
msgstr ""
@@ -23573,6 +24085,9 @@ msgstr ""
msgid "You have declined the invitation to join %{label}."
msgstr ""
+msgid "You have imported from this project %{numberOfPreviousImportsForProject} times before. Each new import will create duplicate issues."
+msgstr ""
+
msgid "You have no permissions"
msgstr ""
@@ -23654,6 +24169,9 @@ msgstr ""
msgid "You need to upload a Google Takeout archive."
msgstr ""
+msgid "You reached %{usage_in_percent} of %{namespace_name}'s capacity (%{used_storage} of %{storage_limit})"
+msgstr ""
+
msgid "You tried to fork %{link_to_the_project} but it failed for the following reason:"
msgstr ""
@@ -23738,9 +24256,21 @@ msgstr ""
msgid "YouTube"
msgstr ""
+msgid "Your %{strong}%{plan_name}%{strong_close} subscription for %{strong}%{namespace_name}%{strong_close} will expire on %{strong}%{expires_on}%{strong_close}. After that, you will not to be able to create issues or merge requests as well as many other features."
+msgstr ""
+
msgid "Your %{strong}%{plan_name}%{strong_close} subscription will expire on %{strong}%{expires_on}%{strong_close}. After that, you will not to be able to create issues or merge requests as well as many other features."
msgstr ""
+msgid "Your CSV export has started. It will be emailed to %{email} when complete."
+msgstr ""
+
+msgid "Your CSV export of %{issues_count} from project %{project_link} has been added to this email as an attachment."
+msgstr ""
+
+msgid "Your CSV export of %{written_count} from project %{project_name} (%{project_url}) has been added to this email as an attachment."
+msgstr ""
+
msgid "Your Commit Email will be used for web based operations, such as edits and merges."
msgstr ""
@@ -23837,6 +24367,9 @@ msgstr ""
msgid "Your comment could not be updated! Please check your network connection and try again."
msgstr ""
+msgid "Your comment will be discarded."
+msgstr ""
+
msgid "Your custom stage '%{title}' was created"
msgstr ""
@@ -23876,6 +24409,9 @@ msgstr ""
msgid "Your new personal access token has been created."
msgstr ""
+msgid "Your new project access token has been created."
+msgstr ""
+
msgid "Your password isn't required to view this page. If a password or any other personal details are requested, please contact your administrator to report abuse."
msgstr ""
@@ -23894,12 +24430,18 @@ msgstr ""
msgid "Your request for access has been queued for review."
msgstr ""
+msgid "Your search didn't match any commits."
+msgstr ""
+
msgid "Your subscription expired!"
msgstr ""
msgid "Your subscription has been downgraded"
msgstr ""
+msgid "Your subscription will automatically renew in %{remaining_days}"
+msgstr ""
+
msgid "Your subscription will expire in %{remaining_days}"
msgstr ""
@@ -23937,7 +24479,7 @@ msgstr ""
msgid "allowed to fail"
msgstr ""
-msgid "already being used for another group or project milestone."
+msgid "already being used for another group or project %{timebox_name}."
msgstr ""
msgid "already has a \"created\" issue link"
@@ -24015,6 +24557,9 @@ msgstr ""
msgid "ciReport|%{linkStartTag}Learn more about SAST %{linkEndTag}"
msgstr ""
+msgid "ciReport|%{linkStartTag}Learn more about Secret Scanning %{linkEndTag}"
+msgstr ""
+
msgid "ciReport|%{linkStartTag}Learn more about codequality reports %{linkEndTag}"
msgstr ""
@@ -24173,6 +24718,12 @@ msgstr ""
msgid "ciReport|SAST"
msgstr ""
+msgid "ciReport|Secret scanning"
+msgstr ""
+
+msgid "ciReport|Secret scanning detects secrets and credentials vulnerabilities in your source code."
+msgstr ""
+
msgid "ciReport|Security scanning"
msgstr ""
@@ -24366,6 +24917,9 @@ msgstr ""
msgid "group"
msgstr ""
+msgid "groups"
+msgstr ""
+
msgid "has already been linked to another vulnerability"
msgstr ""
@@ -24527,9 +25081,6 @@ msgstr[0] ""
msgid "merged %{time_ago}"
msgstr ""
-msgid "milestone should belong either to a project or a group."
-msgstr ""
-
msgid "missing"
msgstr ""
@@ -24866,6 +25417,9 @@ msgstr ""
msgid "no contributions"
msgstr ""
+msgid "no expiration"
+msgstr ""
+
msgid "no one can merge"
msgstr ""
@@ -24916,6 +25470,9 @@ msgstr ""
msgid "pending removal"
msgstr ""
+msgid "per day"
+msgstr ""
+
msgid "pipeline"
msgstr ""
@@ -24942,6 +25499,12 @@ msgstr[0] ""
msgid "project avatar"
msgstr ""
+msgid "projects"
+msgstr ""
+
+msgid "push to your repository, create pipelines, create issues or add comments. To reduce storage capacity, delete unused repositories, artifacts, wikis, issues, and pipelines."
+msgstr ""
+
msgid "quick actions"
msgstr ""
@@ -25087,10 +25650,6 @@ msgstr ""
msgid "this document"
msgstr ""
-msgid "thread resolved"
-msgid_plural "threads resolved"
-msgstr[0] ""
-
msgid "to help your contributors communicate effectively!"
msgstr ""
@@ -25145,6 +25704,9 @@ msgstr ""
msgid "view the blob"
msgstr ""
+msgid "vulnerability|Add a comment"
+msgstr ""
+
msgid "vulnerability|Add a comment or reason for dismissal"
msgstr ""
diff --git a/locale/it/gitlab.po b/locale/it/gitlab.po
index 36aa6ef29f7..d7bd6b7ed4d 100644
--- a/locale/it/gitlab.po
+++ b/locale/it/gitlab.po
@@ -12,7 +12,7 @@ msgstr ""
"X-Crowdin-Project: gitlab-ee\n"
"X-Crowdin-Language: it\n"
"X-Crowdin-File: /master/locale/gitlab.pot\n"
-"PO-Revision-Date: 2020-04-15 00:27\n"
+"PO-Revision-Date: 2020-05-05 21:13\n"
msgid " %{start} to %{end}"
msgstr ""
@@ -212,6 +212,11 @@ msgid_plural "%d tags"
msgstr[0] ""
msgstr[1] ""
+msgid "%d unresolved thread"
+msgid_plural "%d unresolved threads"
+msgstr[0] ""
+msgstr[1] ""
+
msgid "%d vulnerability dismissed"
msgid_plural "%d vulnerabilities dismissed"
msgstr[0] ""
@@ -371,6 +376,9 @@ msgstr ""
msgid "%{mrText}, this issue will be closed automatically."
msgstr "%{mrText}, questo issue verrà chiuso automaticamente."
+msgid "%{namespace_name} is now read-only. You cannot: %{base_message}"
+msgstr ""
+
msgid "%{name} contained %{resultsString}"
msgstr ""
@@ -383,6 +391,11 @@ msgstr ""
msgid "%{name}'s avatar"
msgstr "Avatar di %{name}"
+msgid "%{no_of_days} day"
+msgid_plural "%{no_of_days} days"
+msgstr[0] ""
+msgstr[1] ""
+
msgid "%{number_commits_behind} commits behind %{default_branch}, %{number_commits_ahead} commits ahead"
msgstr "%{number_commits_behind} commits precedenti %{default_branch}, %{number_commits_ahead} commits avanti"
@@ -506,6 +519,9 @@ msgstr[1] "%{text}%{files} file"
msgid "%{text} is available"
msgstr "%{text} è disponibile"
+msgid "%{timebox_name} should belong either to a project or a group."
+msgstr ""
+
msgid "%{title} %{operator} %{threshold}"
msgstr ""
@@ -781,12 +797,18 @@ msgstr "<code>\"johnsmith@example.com\": \"johnsm...@example.com\"</code> Aggiun
msgid "<code>\"johnsmith@example.com\": \"johnsmith@example.com\"</code> will add \"By <a href=\"#\">johnsmith@example.com</a>\" to all issues and comments originally created by johnsmith@example.com. By default, the email address or username is masked to ensure the user's privacy. Use this option if you want to show the full email address."
msgstr ""
+msgid "<namespace / project>"
+msgstr ""
+
msgid "<no name set>"
msgstr ""
msgid "<no scopes selected>"
msgstr ""
+msgid "<project name>"
+msgstr ""
+
msgid "<strong>%{group_name}</strong> group members"
msgstr ""
@@ -820,6 +842,9 @@ msgstr ""
msgid "A basic page and serverless function that uses AWS Lambda, AWS API Gateway, and GitLab Pages"
msgstr ""
+msgid "A complete DevOps platform"
+msgstr ""
+
msgid "A default branch cannot be chosen for an empty project."
msgstr "Un ramo predefinito non può essere scelto per un progetto vuoto."
@@ -1036,6 +1061,12 @@ msgstr ""
msgid "AccessTokens|reset it"
msgstr ""
+msgid "AccessibilityReport|Accessibility report artifact not found"
+msgstr ""
+
+msgid "AccessibilityReport|Failed to retrieve accessibility report"
+msgstr ""
+
msgid "AccessibilityReport|Learn More"
msgstr ""
@@ -1045,6 +1076,9 @@ msgstr ""
msgid "AccessibilityReport|New"
msgstr ""
+msgid "AccessibilityReport|The accessibility scanning found an error of the following type: %{code}"
+msgstr ""
+
msgid "Account"
msgstr "Account"
@@ -1287,6 +1321,9 @@ msgstr ""
msgid "Added at"
msgstr ""
+msgid "Added for this merge request"
+msgstr ""
+
msgid "Added in this version"
msgstr ""
@@ -1658,6 +1695,72 @@ msgid_plural "Alerts"
msgstr[0] ""
msgstr[1] ""
+msgid "Alert Details"
+msgstr ""
+
+msgid "AlertManagement|Acknowledged"
+msgstr ""
+
+msgid "AlertManagement|Alert"
+msgstr ""
+
+msgid "AlertManagement|Authorize external service"
+msgstr ""
+
+msgid "AlertManagement|Display alerts from all your monitoring tools directly within GitLab. Streamline the investigation of your alerts and the escalation of alerts to incidents."
+msgstr ""
+
+msgid "AlertManagement|End time"
+msgstr ""
+
+msgid "AlertManagement|End time:"
+msgstr ""
+
+msgid "AlertManagement|Events"
+msgstr ""
+
+msgid "AlertManagement|Events:"
+msgstr ""
+
+msgid "AlertManagement|Full Alert Details"
+msgstr ""
+
+msgid "AlertManagement|More information"
+msgstr ""
+
+msgid "AlertManagement|No alerts available to display. If you think you're seeing this message in error, refresh the page."
+msgstr ""
+
+msgid "AlertManagement|No alerts to display."
+msgstr ""
+
+msgid "AlertManagement|Overview"
+msgstr ""
+
+msgid "AlertManagement|Resolved"
+msgstr ""
+
+msgid "AlertManagement|Severity"
+msgstr ""
+
+msgid "AlertManagement|Start time"
+msgstr ""
+
+msgid "AlertManagement|Start time:"
+msgstr ""
+
+msgid "AlertManagement|Status"
+msgstr ""
+
+msgid "AlertManagement|Surface alerts in GitLab"
+msgstr ""
+
+msgid "AlertManagement|There was an error displaying the alerts. Confirm your endpoint's configuration details to ensure alerts appear."
+msgstr ""
+
+msgid "AlertManagement|Triggered"
+msgstr ""
+
msgid "AlertService|%{linkStart}Learn more%{linkEnd} about configuring this endpoint to receive alerts."
msgstr ""
@@ -1727,6 +1830,9 @@ msgstr ""
msgid "All security scans are enabled because %{linkStart}Auto DevOps%{linkEnd} is enabled on this project"
msgstr ""
+msgid "All threads resolved"
+msgstr ""
+
msgid "All users"
msgstr ""
@@ -1745,6 +1851,9 @@ msgstr ""
msgid "Allow only the selected protocols to be used for Git access."
msgstr ""
+msgid "Allow owners to manage default branch protection per group"
+msgstr ""
+
msgid "Allow owners to manually add users outside of LDAP"
msgstr ""
@@ -1853,6 +1962,9 @@ msgstr ""
msgid "An error occurred fetching the dropdown data."
msgstr ""
+msgid "An error occurred fetching the project authors."
+msgstr ""
+
msgid "An error occurred previewing the blob"
msgstr ""
@@ -1934,6 +2046,9 @@ msgstr ""
msgid "An error occurred while fetching sidebar data"
msgstr "Errore durante il recupero dei dati della barra laterale"
+msgid "An error occurred while fetching terraform reports."
+msgstr ""
+
msgid "An error occurred while fetching the Service Desk address."
msgstr ""
@@ -2171,6 +2286,9 @@ msgstr ""
msgid "Any"
msgstr ""
+msgid "Any Author"
+msgstr ""
+
msgid "Any Label"
msgstr ""
@@ -2386,10 +2504,10 @@ msgstr ""
msgid "Are you sure that you want to unarchive this project?"
msgstr ""
-msgid "Are you sure you want to cancel creating this comment?"
+msgid "Are you sure you want to cancel editing this comment?"
msgstr ""
-msgid "Are you sure you want to cancel editing this comment?"
+msgid "Are you sure you want to close this blocked issue?"
msgstr ""
msgid "Are you sure you want to delete %{name}?"
@@ -2419,6 +2537,9 @@ msgstr ""
msgid "Are you sure you want to deploy this environment?"
msgstr ""
+msgid "Are you sure you want to discard this comment?"
+msgstr ""
+
msgid "Are you sure you want to erase this build?"
msgstr ""
@@ -2595,6 +2716,9 @@ msgstr ""
msgid "At least one approval from a code owner is required to change files matching the respective CODEOWNER rules."
msgstr ""
+msgid "At least one logging option is required to be enabled"
+msgstr ""
+
msgid "At least one of group_id or project_id must be specified"
msgstr ""
@@ -2633,6 +2757,27 @@ msgstr ""
msgid "AuditEvents|Target"
msgstr ""
+msgid "AuditLogs|(removed)"
+msgstr ""
+
+msgid "AuditLogs|Action"
+msgstr ""
+
+msgid "AuditLogs|Author"
+msgstr ""
+
+msgid "AuditLogs|Date"
+msgstr ""
+
+msgid "AuditLogs|IP Address"
+msgstr ""
+
+msgid "AuditLogs|Object"
+msgstr ""
+
+msgid "AuditLogs|Target"
+msgstr ""
+
msgid "Aug"
msgstr "Ago"
@@ -2915,9 +3060,6 @@ msgstr ""
msgid "BambooService|You must set up automatic revision labeling and a repository trigger in Bamboo."
msgstr ""
-msgid "Batch operations"
-msgstr ""
-
msgid "BatchComments|Delete all pending comments"
msgstr ""
@@ -3047,6 +3189,9 @@ msgstr ""
msgid "Boards"
msgstr ""
+msgid "Boards and Board Lists"
+msgstr ""
+
msgid "Boards|Collapse"
msgstr ""
@@ -3266,6 +3411,9 @@ msgstr ""
msgid "Buy GitLab Enterprise Edition"
msgstr ""
+msgid "Buy more Pipeline minutes"
+msgstr ""
+
msgid "By %{user_name}"
msgstr ""
@@ -3386,6 +3534,9 @@ msgstr ""
msgid "Can't scan the code?"
msgstr ""
+msgid "Can't update snippet: %{err}"
+msgstr ""
+
msgid "Canary"
msgstr ""
@@ -3554,6 +3705,9 @@ msgstr ""
msgid "Changing group path can have unintended side effects."
msgstr ""
+msgid "Charts"
+msgstr ""
+
msgid "Charts can't be displayed as the request for data has timed out. %{documentationLink}"
msgstr ""
@@ -3590,9 +3744,6 @@ msgstr ""
msgid "ChatMessage|and [%{count} more](%{pipeline_failed_jobs_url})"
msgstr ""
-msgid "ChatMessage|failed"
-msgstr ""
-
msgid "ChatMessage|has failed"
msgstr ""
@@ -3608,9 +3759,6 @@ msgstr ""
msgid "ChatMessage|in %{project_link}"
msgstr ""
-msgid "ChatMessage|passed"
-msgstr ""
-
msgid "Check again"
msgstr ""
@@ -4061,9 +4209,6 @@ msgstr ""
msgid "Click any <strong>project name</strong> in the project list below to navigate to the project milestone."
msgstr ""
-msgid "Click here"
-msgstr ""
-
msgid "Click the <strong>Download</strong> button and wait for downloading to complete."
msgstr ""
@@ -4451,6 +4596,12 @@ msgstr ""
msgid "ClusterIntegration|Fetching zones"
msgstr ""
+msgid "ClusterIntegration|Fluentd"
+msgstr ""
+
+msgid "ClusterIntegration|Fluentd is an open source data collector, which lets you unify the data collection and consumption for a better use and understanding of data. It requires at least one of the following logs to be successfully installed."
+msgstr ""
+
msgid "ClusterIntegration|GitLab Integration"
msgstr ""
@@ -4757,6 +4908,15 @@ msgstr ""
msgid "ClusterIntegration|Request to begin uninstalling failed"
msgstr ""
+msgid "ClusterIntegration|SIEM Hostname"
+msgstr ""
+
+msgid "ClusterIntegration|SIEM Port"
+msgstr ""
+
+msgid "ClusterIntegration|SIEM Protocol"
+msgstr ""
+
msgid "ClusterIntegration|Save changes"
msgstr ""
@@ -4853,6 +5013,12 @@ msgstr ""
msgid "ClusterIntegration|Select zone to choose machine type"
msgstr ""
+msgid "ClusterIntegration|Send Cilium Logs"
+msgstr ""
+
+msgid "ClusterIntegration|Send ModSecurity Logs"
+msgstr ""
+
msgid "ClusterIntegration|Service Token"
msgstr ""
@@ -5281,18 +5447,33 @@ msgstr ""
msgid "Compliance framework (optional)"
msgstr ""
+msgid "ComplianceFramework|GDPR"
+msgstr ""
+
msgid "ComplianceFramework|GDPR - General Data Protection Regulation"
msgstr ""
+msgid "ComplianceFramework|HIPAA"
+msgstr ""
+
msgid "ComplianceFramework|HIPAA - Health Insurance Portability and Accountability Act"
msgstr ""
+msgid "ComplianceFramework|PCI-DSS"
+msgstr ""
+
msgid "ComplianceFramework|PCI-DSS - Payment Card Industry-Data Security Standard"
msgstr ""
+msgid "ComplianceFramework|SOC 2"
+msgstr ""
+
msgid "ComplianceFramework|SOC 2 - Service Organization Control 2"
msgstr ""
+msgid "ComplianceFramework|SOX"
+msgstr ""
+
msgid "ComplianceFramework|SOX - Sarbanes-Oxley"
msgstr ""
@@ -5440,9 +5621,15 @@ msgstr ""
msgid "Container repositories sync capacity"
msgstr ""
+msgid "ContainerRegistry| Please visit the %{linkStart}administration settings%{linkEnd} to enable this feature."
+msgstr ""
+
msgid "ContainerRegistry|%{imageName} tags"
msgstr ""
+msgid "ContainerRegistry|%{title} was successfully scheduled for deletion"
+msgstr ""
+
msgid "ContainerRegistry|Automatically remove extra images that aren't designed to be kept."
msgstr ""
@@ -5455,6 +5642,9 @@ msgstr ""
msgid "ContainerRegistry|Container Registry"
msgstr ""
+msgid "ContainerRegistry|Container Registry tag expiration and retention policy is disabled"
+msgstr ""
+
msgid "ContainerRegistry|Copy build command"
msgstr ""
@@ -5464,18 +5654,12 @@ msgstr ""
msgid "ContainerRegistry|Copy push command"
msgstr ""
-msgid "ContainerRegistry|Currently, the Container Registry tag expiration feature is not available for projects created before GitLab version 12.8. For updates and more information, visit Issue %{linkStart}#196124%{linkEnd}"
-msgstr ""
-
msgid "ContainerRegistry|Docker connection error"
msgstr ""
msgid "ContainerRegistry|Docker tag expiration policy is %{toggleStatus}"
msgstr ""
-msgid "ContainerRegistry|Docker tags with names matching this regex pattern will expire:"
-msgstr ""
-
msgid "ContainerRegistry|Edit Settings"
msgstr ""
@@ -5497,9 +5681,6 @@ msgstr ""
msgid "ContainerRegistry|Image ID"
msgstr ""
-msgid "ContainerRegistry|Image deleted successfully"
-msgstr ""
-
msgid "ContainerRegistry|Keep and protect the images that matter most."
msgstr ""
@@ -5515,12 +5696,24 @@ msgstr ""
msgid "ContainerRegistry|Number of tags to retain:"
msgstr ""
+msgid "ContainerRegistry|Please contact your administrator."
+msgstr ""
+
msgid "ContainerRegistry|Push an image"
msgstr ""
msgid "ContainerRegistry|Quick Start"
msgstr ""
+msgid "ContainerRegistry|Regular expressions such as %{codeStart}.*-test%{codeEnd} or %{codeStart}dev-.*%{codeEnd} are supported"
+msgstr ""
+
+msgid "ContainerRegistry|Regular expressions such as %{codeStart}.*-test%{codeEnd} or %{codeStart}dev-.*%{codeEnd} are supported. To select all tags, use %{codeStart}.*%{codeEnd}"
+msgstr ""
+
+msgid "ContainerRegistry|Remember to run %{docLinkStart}garbage collection%{docLinkEnd} to remove the stale data from storage."
+msgstr ""
+
msgid "ContainerRegistry|Remove repository"
msgstr ""
@@ -5535,22 +5728,22 @@ msgstr[1] ""
msgid "ContainerRegistry|Retention policy has been Enabled"
msgstr ""
-msgid "ContainerRegistry|Something went wrong while deleting the image."
+msgid "ContainerRegistry|Something went wrong while fetching the expiration policy."
msgstr ""
-msgid "ContainerRegistry|Something went wrong while deleting the tag."
+msgid "ContainerRegistry|Something went wrong while fetching the repository list."
msgstr ""
-msgid "ContainerRegistry|Something went wrong while deleting the tags."
+msgid "ContainerRegistry|Something went wrong while fetching the tags list."
msgstr ""
-msgid "ContainerRegistry|Something went wrong while fetching the expiration policy."
+msgid "ContainerRegistry|Something went wrong while marking the tag for deletion."
msgstr ""
-msgid "ContainerRegistry|Something went wrong while fetching the packages list."
+msgid "ContainerRegistry|Something went wrong while marking the tags for deletion."
msgstr ""
-msgid "ContainerRegistry|Something went wrong while fetching the tags list."
+msgid "ContainerRegistry|Something went wrong while scheduling %{title} for deletion. Please try again."
msgstr ""
msgid "ContainerRegistry|Something went wrong while updating the expiration policy."
@@ -5559,16 +5752,25 @@ msgstr ""
msgid "ContainerRegistry|Tag"
msgstr "Tag"
-msgid "ContainerRegistry|Tag deleted successfully"
-msgstr ""
-
msgid "ContainerRegistry|Tag expiration policy"
msgstr ""
msgid "ContainerRegistry|Tag expiration policy is designed to:"
msgstr ""
-msgid "ContainerRegistry|Tags deleted successfully"
+msgid "ContainerRegistry|Tag successfully marked for deletion."
+msgstr ""
+
+msgid "ContainerRegistry|Tags successfully marked for deletion."
+msgstr ""
+
+msgid "ContainerRegistry|Tags with names matching this regex pattern will %{italicStart}be preserved:%{italicEnd}"
+msgstr ""
+
+msgid "ContainerRegistry|Tags with names matching this regex pattern will %{italicStart}expire:%{italicEnd}"
+msgstr ""
+
+msgid "ContainerRegistry|The Container Registry tag expiration and retention policies for this project have not been enabled."
msgstr ""
msgid "ContainerRegistry|The last tag related to this image was recently removed. This empty image and any associated data will be automatically removed as part of the regular Garbage Collection process. If you have any questions, contact your administrator."
@@ -5589,16 +5791,16 @@ msgstr ""
msgid "ContainerRegistry|There are no container images stored for this project"
msgstr ""
-msgid "ContainerRegistry|This Registry contains deleted image tag data. Remember to run %{docLinkStart}garbage collection%{docLinkEnd} to remove the stale data from storage."
+msgid "ContainerRegistry|There was an error during the deletion of this image repository, please try again."
msgstr ""
msgid "ContainerRegistry|This image has no active tags"
msgstr ""
-msgid "ContainerRegistry|We are having trouble connecting to Docker, which could be due to an issue with your project name or path. %{docLinkStart}More Information%{docLinkEnd}"
+msgid "ContainerRegistry|This image repository is scheduled for deletion"
msgstr ""
-msgid "ContainerRegistry|Wildcards such as %{codeStart}.*-stable%{codeEnd} or %{codeStart}production/.*%{codeEnd} are supported. To select all tags, use %{codeStart}.*%{codeEnd}"
+msgid "ContainerRegistry|We are having trouble connecting to Docker, which could be due to an issue with your project name or path. %{docLinkStart}More Information%{docLinkEnd}"
msgstr ""
msgid "ContainerRegistry|With the Container Registry, every project can have its own space to store its Docker images. %{docLinkStart}More Information%{docLinkEnd}"
@@ -5853,7 +6055,7 @@ msgstr ""
msgid "Could not delete chat nickname %{chat_name}."
msgstr ""
-msgid "Could not find design"
+msgid "Could not find design."
msgstr ""
msgid "Could not remove the trigger."
@@ -5868,6 +6070,9 @@ msgstr ""
msgid "Could not revoke personal access token %{personal_access_token_name}."
msgstr ""
+msgid "Could not revoke project access token %{project_access_token_name}."
+msgstr ""
+
msgid "Could not save group ID"
msgstr ""
@@ -5925,6 +6130,9 @@ msgstr ""
msgid "Create a new branch"
msgstr ""
+msgid "Create a new deploy key for this project"
+msgstr ""
+
msgid "Create a new file as there are no files yet. Afterwards, you'll be able to commit your changes."
msgstr ""
@@ -6030,6 +6238,9 @@ msgstr ""
msgid "Create requirement"
msgstr ""
+msgid "Create snippet"
+msgstr ""
+
msgid "Create wildcard: %{searchTerm}"
msgstr ""
@@ -6051,6 +6262,9 @@ msgstr "Crea token d'accesso personale"
msgid "Created"
msgstr ""
+msgid "Created %{timestamp}"
+msgstr ""
+
msgid "Created At"
msgstr ""
@@ -6407,6 +6621,9 @@ msgstr ""
msgid "CycleAnalytics|stage dropdown"
msgstr ""
+msgid "DAG"
+msgstr ""
+
msgid "DNS"
msgstr ""
@@ -6773,6 +6990,9 @@ msgstr "Chiavi di Deploy (rilascio)"
msgid "Deploy key was successfully updated."
msgstr ""
+msgid "Deploy keys allow read-only or read-write (if enabled) access to your repository. Deploy keys can be used for CI, staging or production servers. You can create a deploy key or add an existing one."
+msgstr ""
+
msgid "Deploy progress not found. To see pods, ensure your environment matches %{linkStart}deploy board criteria%{linkEnd}."
msgstr ""
@@ -6926,6 +7146,9 @@ msgstr ""
msgid "Deploying to"
msgstr ""
+msgid "Deployment Frequency"
+msgstr ""
+
msgid "Deployment|API"
msgstr ""
@@ -7444,9 +7667,6 @@ msgstr ""
msgid "Edit environment"
msgstr ""
-msgid "Edit epic description"
-msgstr ""
-
msgid "Edit file"
msgstr ""
@@ -8068,9 +8288,6 @@ msgstr ""
msgid "Epics"
msgstr ""
-msgid "Epics (Ultimate / Gold license only)"
-msgstr ""
-
msgid "Epics Roadmap"
msgstr ""
@@ -8080,6 +8297,9 @@ msgstr ""
msgid "Epics let you manage your portfolio of projects more efficiently and with less effort"
msgstr ""
+msgid "Epics, Issues, and Merge Requests"
+msgstr ""
+
msgid "Epics|Add an epic"
msgstr ""
@@ -8131,6 +8351,9 @@ msgstr ""
msgid "Epics|Something went wrong while fetching group epics."
msgstr ""
+msgid "Epics|Something went wrong while moving item."
+msgstr ""
+
msgid "Epics|Something went wrong while ordering item."
msgstr ""
@@ -8185,6 +8408,9 @@ msgstr ""
msgid "Error fetching network graph."
msgstr ""
+msgid "Error fetching payload data."
+msgstr ""
+
msgid "Error fetching projects"
msgstr ""
@@ -8194,9 +8420,6 @@ msgstr ""
msgid "Error fetching the dependency list. Please check your network connection and try again."
msgstr ""
-msgid "Error fetching usage ping data."
-msgstr ""
-
msgid "Error loading branch data. Please try again."
msgstr ""
@@ -8338,6 +8561,9 @@ msgstr ""
msgid "Errors"
msgstr ""
+msgid "Errors:"
+msgstr ""
+
msgid "Estimated"
msgstr ""
@@ -8542,12 +8768,18 @@ msgstr "Esplora gruppi pubblici"
msgid "Export as CSV"
msgstr ""
+msgid "Export group"
+msgstr ""
+
msgid "Export issues"
msgstr ""
msgid "Export project"
msgstr ""
+msgid "Export this group with all related data to a new GitLab instance. Once complete, you can import the data file from the \"New Group\" page."
+msgstr ""
+
msgid "Export this project with all its related data in order to move your project to a new GitLab instance. Once the export is finished, you can import the file from the \"New Project\" page."
msgstr ""
@@ -8617,9 +8849,6 @@ msgstr ""
msgid "Failed Jobs"
msgstr ""
-msgid "Failed create wiki"
-msgstr ""
-
msgid "Failed to add a Zoom meeting"
msgstr ""
@@ -8659,6 +8888,9 @@ msgstr ""
msgid "Failed to create resources"
msgstr ""
+msgid "Failed to create wiki"
+msgstr ""
+
msgid "Failed to delete board. Please try again."
msgstr ""
@@ -8668,7 +8900,7 @@ msgstr ""
msgid "Failed to enqueue the rebase operation, possibly due to a long-lived transaction. Try again later."
msgstr ""
-msgid "Failed to find import label for jira import."
+msgid "Failed to find import label for Jira import."
msgstr ""
msgid "Failed to get ref."
@@ -8692,6 +8924,12 @@ msgstr ""
msgid "Failed to load groups & users."
msgstr ""
+msgid "Failed to load labels. Please try again."
+msgstr ""
+
+msgid "Failed to load milestones. Please try again."
+msgstr ""
+
msgid "Failed to load related branches"
msgstr ""
@@ -9067,7 +9305,10 @@ msgstr "Filtra per messaggio di commit"
msgid "Filter by milestone name"
msgstr ""
-msgid "Filter by name..."
+msgid "Filter by name"
+msgstr ""
+
+msgid "Filter by status"
msgstr ""
msgid "Filter by two-factor authentication"
@@ -9079,12 +9320,18 @@ msgstr ""
msgid "Filter projects"
msgstr ""
+msgid "Filter results"
+msgstr ""
+
msgid "Filter results by group"
msgstr ""
msgid "Filter results by project"
msgstr ""
+msgid "Filter results..."
+msgstr ""
+
msgid "Filter your projects by name"
msgstr ""
@@ -9373,10 +9620,10 @@ msgstr ""
msgid "GeoNodes|Checksummed"
msgstr ""
-msgid "GeoNodes|Container repositories"
+msgid "GeoNodes|Consult Geo troubleshooting information"
msgstr ""
-msgid "GeoNodes|Data is out of date from %{timeago}"
+msgid "GeoNodes|Container repositories"
msgstr ""
msgid "GeoNodes|Data replication lag"
@@ -9418,6 +9665,9 @@ msgstr ""
msgid "GeoNodes|Last event ID seen from primary"
msgstr ""
+msgid "GeoNodes|Learn more about Geo node statuses"
+msgstr ""
+
msgid "GeoNodes|Loading nodes"
msgstr ""
@@ -9433,6 +9683,9 @@ msgstr ""
msgid "GeoNodes|Node was successfully removed."
msgstr ""
+msgid "GeoNodes|Node's status was updated %{timeAgo}."
+msgstr ""
+
msgid "GeoNodes|Not checksummed"
msgstr ""
@@ -9463,7 +9716,10 @@ msgstr ""
msgid "GeoNodes|Repository verification progress"
msgstr ""
-msgid "GeoNodes|Selective"
+msgid "GeoNodes|Selective (%{syncLabel})"
+msgstr ""
+
+msgid "GeoNodes|Selective synchronization"
msgstr ""
msgid "GeoNodes|Something went wrong while changing node status"
@@ -9490,6 +9746,9 @@ msgstr ""
msgid "GeoNodes|Unverified"
msgstr ""
+msgid "GeoNodes|Updated %{timeAgo}"
+msgstr ""
+
msgid "GeoNodes|Used slots"
msgstr ""
@@ -9529,16 +9788,16 @@ msgstr ""
msgid "Geo|All"
msgstr ""
-msgid "Geo|All projects"
+msgid "Geo|All %{replicable_type}"
msgstr ""
-msgid "Geo|All projects are being scheduled for re-sync"
+msgid "Geo|All projects"
msgstr ""
-msgid "Geo|All projects are being scheduled for re-verify"
+msgid "Geo|All projects are being scheduled for resync"
msgstr ""
-msgid "Geo|Batch operations"
+msgid "Geo|All projects are being scheduled for reverify"
msgstr ""
msgid "Geo|Could not remove tracking entry for an existing project."
@@ -9550,9 +9809,15 @@ msgstr ""
msgid "Geo|Failed"
msgstr ""
+msgid "Geo|Filter by status"
+msgstr ""
+
msgid "Geo|Geo Status"
msgstr ""
+msgid "Geo|In progress"
+msgstr ""
+
msgid "Geo|In sync"
msgstr ""
@@ -9577,9 +9842,6 @@ msgstr ""
msgid "Geo|Not synced yet"
msgstr ""
-msgid "Geo|Pending"
-msgstr ""
-
msgid "Geo|Pending synchronization"
msgstr ""
@@ -9616,7 +9878,7 @@ msgstr ""
msgid "Geo|Resync"
msgstr ""
-msgid "Geo|Resync all projects"
+msgid "Geo|Resync all"
msgstr ""
msgid "Geo|Retry count"
@@ -9625,7 +9887,7 @@ msgstr ""
msgid "Geo|Reverify"
msgstr ""
-msgid "Geo|Reverify all projects"
+msgid "Geo|Reverify all"
msgstr ""
msgid "Geo|Status"
@@ -9967,9 +10229,6 @@ msgstr ""
msgid "Go to file"
msgstr ""
-msgid "Go to file (MRs only)"
-msgstr ""
-
msgid "Go to file permalink (while viewing a file)"
msgstr ""
@@ -10414,6 +10673,9 @@ msgstr ""
msgid "GroupSettings|Disable group mentions"
msgstr ""
+msgid "GroupSettings|Export group"
+msgstr ""
+
msgid "GroupSettings|If the parent group's visibility is lower than the group current visibility, visibility levels for subgroups and projects will be changed to match the new parent group's visibility."
msgstr ""
@@ -10492,6 +10754,9 @@ msgstr ""
msgid "Groups (%{count})"
msgstr ""
+msgid "Groups (%{groups})"
+msgstr ""
+
msgid "Groups can also be nested by creating %{subgroup_docs_link_start}subgroups%{subgroup_docs_link_end}."
msgstr ""
@@ -10757,15 +11022,12 @@ msgstr ""
msgid "ID:"
msgstr ""
-msgid "IDE|Allow live previews of JavaScript projects in the Web IDE using CodeSandbox client side evaluation."
+msgid "IDE|Allow live previews of JavaScript projects in the Web IDE using CodeSandbox Live Preview."
msgstr ""
msgid "IDE|Back"
msgstr ""
-msgid "IDE|Client side evaluation"
-msgstr ""
-
msgid "IDE|Commit"
msgstr ""
@@ -10829,6 +11091,9 @@ msgstr ""
msgid "If any job surpasses this timeout threshold, it will be marked as failed. Human readable time input language is accepted like \"1 hour\". Values without specification represent seconds."
msgstr ""
+msgid "If blank, set allowable lifetime to %{instance_level_policy_in_words}, as defined by the instance admin. Once set, existing tokens for users in this group may be revoked."
+msgstr ""
+
msgid "If checked, group owners can manage LDAP group links and LDAP member overrides"
msgstr ""
@@ -10868,6 +11133,9 @@ msgstr ""
msgid "If you lose your recovery codes you can generate new ones, invalidating all previous codes."
msgstr ""
+msgid "If you reach 100%% storage capacity, you will not be able to: %{base_message}"
+msgstr ""
+
msgid "If your HTTP repository is not publicly accessible, add your credentials."
msgstr ""
@@ -10979,6 +11247,9 @@ msgstr ""
msgid "Import repository"
msgstr "Importa repository"
+msgid "Import started by: %{importInitiator}"
+msgstr ""
+
msgid "Import tasks"
msgstr ""
@@ -11045,6 +11316,9 @@ msgstr ""
msgid "In order to tailor your experience with GitLab we<br>would like to know a bit more about you."
msgstr ""
+msgid "In progress"
+msgstr ""
+
msgid "In the next step, you'll be able to select the projects you want to import."
msgstr ""
@@ -11392,15 +11666,15 @@ msgstr ""
msgid "Issues"
msgstr ""
-msgid "Issues / Merge Requests"
-msgstr ""
-
msgid "Issues Analytics"
msgstr ""
msgid "Issues Rate Limits"
msgstr ""
+msgid "Issues and Merge Requests"
+msgstr ""
+
msgid "Issues can be bugs, tasks or ideas to be discussed. Also, issues are searchable and filterable."
msgstr ""
@@ -11422,10 +11696,10 @@ msgstr ""
msgid "IssuesAnalytics|Avg/Month:"
msgstr ""
-msgid "IssuesAnalytics|Issues created"
+msgid "IssuesAnalytics|Issues opened"
msgstr ""
-msgid "IssuesAnalytics|Issues created per month"
+msgid "IssuesAnalytics|Issues opened per month"
msgstr ""
msgid "IssuesAnalytics|Last 12 months"
@@ -11479,6 +11753,9 @@ msgstr ""
msgid "Jira integration not configured."
msgstr ""
+msgid "Jira project: %{importProject}"
+msgstr ""
+
msgid "JiraService|Events for %{noteable_model_name} are disabled."
msgstr ""
@@ -11868,6 +12145,9 @@ msgstr ""
msgid "Lead"
msgstr ""
+msgid "Lead Time"
+msgstr ""
+
msgid "Learn GitLab"
msgstr ""
@@ -11880,6 +12160,9 @@ msgstr ""
msgid "Learn how to %{no_packages_link_start}publish and share your packages%{no_packages_link_end} with GitLab."
msgstr ""
+msgid "Learn how to enable synchronization"
+msgstr ""
+
msgid "Learn more"
msgstr ""
@@ -12211,9 +12494,6 @@ msgstr ""
msgid "Live preview"
msgstr ""
-msgid "Load more vulnerabilities"
-msgstr ""
-
msgid "Loading"
msgstr ""
@@ -12364,9 +12644,6 @@ msgstr ""
msgid "Manage"
msgstr ""
-msgid "Manage Git repositories with fine-grained access controls that keep your code secure. Perform code reviews and enhance collaboration with merge requests. Each project can also have an issue tracker and a wiki."
-msgstr ""
-
msgid "Manage Web IDE features"
msgstr ""
@@ -12397,6 +12674,9 @@ msgstr ""
msgid "Manage your license"
msgstr ""
+msgid "Managed Account"
+msgstr ""
+
msgid "Manifest"
msgstr ""
@@ -12661,6 +12941,9 @@ msgstr ""
msgid "Merge Requests in Review"
msgstr ""
+msgid "Merge automatically (%{strategy})"
+msgstr ""
+
msgid "Merge commit message"
msgstr ""
@@ -12814,6 +13097,12 @@ msgstr ""
msgid "Merged branches are being deleted. This can take some time depending on the number of branches. Please refresh the page to see changes."
msgstr ""
+msgid "Merged this merge request."
+msgstr ""
+
+msgid "Merges this merge request immediately."
+msgstr ""
+
msgid "Merges this merge request when the pipeline succeeds."
msgstr ""
@@ -12921,12 +13210,15 @@ msgid_plural "Metrics|Edit metrics"
msgstr[0] ""
msgstr[1] ""
-msgid "Metrics|Environment"
+msgid "Metrics|Expand panel"
msgstr ""
msgid "Metrics|For grouping similar metrics"
msgstr ""
+msgid "Metrics|Go back (Esc)"
+msgstr ""
+
msgid "Metrics|Invalid time range, please verify."
msgstr ""
@@ -12957,9 +13249,6 @@ msgstr ""
msgid "Metrics|Refresh dashboard"
msgstr ""
-msgid "Metrics|Show last"
-msgstr ""
-
msgid "Metrics|There was an error creating the dashboard."
msgstr ""
@@ -13038,6 +13327,9 @@ msgstr ""
msgid "Microsoft Azure"
msgstr ""
+msgid "Middleman project with Static Site Editor support"
+msgstr ""
+
msgid "Migrated %{success_count}/%{total_count} files."
msgstr ""
@@ -13483,10 +13775,10 @@ msgstr ""
msgid "Next"
msgstr ""
-msgid "Next file in diff (MRs only)"
+msgid "Next file in diff"
msgstr ""
-msgid "Next unresolved discussion (MRs only)"
+msgid "Next unresolved discussion"
msgstr ""
msgid "Nickname"
@@ -13546,6 +13838,9 @@ msgstr ""
msgid "No changes between %{ref_start}%{source_branch}%{ref_end} and %{ref_start}%{target_branch}%{ref_end}"
msgstr ""
+msgid "No child epics match applied filters"
+msgstr ""
+
msgid "No connection could be made to a Gitaly Server, please check your logs!"
msgstr ""
@@ -13684,15 +13979,6 @@ msgstr ""
msgid "No thanks, don't show this again"
msgstr ""
-msgid "No vulnerabilities found for this group"
-msgstr ""
-
-msgid "No vulnerabilities found for this pipeline"
-msgstr ""
-
-msgid "No vulnerabilities found for this project"
-msgstr ""
-
msgid "No vulnerabilities present"
msgstr ""
@@ -13816,6 +14102,9 @@ msgstr ""
msgid "Nothing to preview."
msgstr ""
+msgid "Nothing to synchronize"
+msgstr ""
+
msgid "Notification events"
msgstr "Notifica eventi"
@@ -13987,6 +14276,9 @@ msgstr ""
msgid "Once removed, the fork relationship cannot be restored and you will no longer be able to send merge requests to the source."
msgstr ""
+msgid "Once the exported file is ready you can download it from this page."
+msgstr ""
+
msgid "Once the exported file is ready, you will receive a notification email with a download link, or you can download it from this page."
msgstr ""
@@ -14088,9 +14380,6 @@ msgstr ""
msgid "Open sidebar"
msgstr ""
-msgid "Open source software to collaborate on code"
-msgstr ""
-
msgid "Open: %{openIssuesCount}"
msgstr ""
@@ -14193,6 +14482,9 @@ msgstr ""
msgid "OutdatedBrowser|You can provide feedback %{feedback_link_start}on this issue%{feedback_link_end} or via your usual support channels."
msgstr ""
+msgid "Overridden"
+msgstr ""
+
msgid "Overview"
msgstr "Panoramica"
@@ -14244,6 +14536,9 @@ msgstr ""
msgid "PackageRegistry|Conan Command"
msgstr ""
+msgid "PackageRegistry|Copy .pypirc content"
+msgstr ""
+
msgid "PackageRegistry|Copy Conan Command"
msgstr ""
@@ -14265,6 +14560,9 @@ msgstr ""
msgid "PackageRegistry|Copy NuGet Setup Command"
msgstr ""
+msgid "PackageRegistry|Copy Pip command"
+msgstr ""
+
msgid "PackageRegistry|Copy and paste this inside your %{codeStart}pom.xml%{codeEnd} %{codeStart}dependencies%{codeEnd} block."
msgstr ""
@@ -14298,6 +14596,12 @@ msgstr ""
msgid "PackageRegistry|For more information on the NuGet registry, %{linkStart}see the documentation%{linkEnd}."
msgstr ""
+msgid "PackageRegistry|For more information on the PyPi registry, %{linkStart}see the documentation%{linkEnd}."
+msgstr ""
+
+msgid "PackageRegistry|If you haven't already done so, you will need to add the below to your %{codeStart}.pypirc%{codeEnd} file."
+msgstr ""
+
msgid "PackageRegistry|If you haven't already done so, you will need to add the below to your %{codeStart}pom.xml%{codeEnd} file."
msgstr ""
@@ -14307,6 +14611,9 @@ msgstr ""
msgid "PackageRegistry|Learn how to %{noPackagesLinkStart}publish and share your packages%{noPackagesLinkEnd} with GitLab."
msgstr ""
+msgid "PackageRegistry|Manually Published"
+msgstr ""
+
msgid "PackageRegistry|Maven"
msgstr ""
@@ -14325,12 +14632,18 @@ msgstr ""
msgid "PackageRegistry|NuGet Command"
msgstr ""
+msgid "PackageRegistry|Pip Command"
+msgstr ""
+
msgid "PackageRegistry|Pipeline %{linkStart}%{linkEnd} triggered %{timestamp} by %{author}"
msgstr ""
msgid "PackageRegistry|Published to the repository at %{timestamp}"
msgstr ""
+msgid "PackageRegistry|PyPi"
+msgstr ""
+
msgid "PackageRegistry|Registry Setup"
msgstr ""
@@ -14367,6 +14680,9 @@ msgstr ""
msgid "PackageRegistry|npm"
msgstr ""
+msgid "PackageRegistry|published by %{author}"
+msgstr ""
+
msgid "PackageRegistry|yarn"
msgstr ""
@@ -14382,6 +14698,9 @@ msgstr ""
msgid "PackageType|NuGet"
msgstr ""
+msgid "PackageType|PyPi"
+msgstr ""
+
msgid "Packages"
msgstr ""
@@ -14475,6 +14794,9 @@ msgstr ""
msgid "Past due"
msgstr ""
+msgid "Paste a machine public key here. Read more about how to generate it"
+msgstr ""
+
msgid "Paste a machine public key here. Read more about how to generate it %{link_start}here%{link_end}"
msgstr ""
@@ -14490,9 +14812,6 @@ msgstr ""
msgid "Path"
msgstr ""
-msgid "Path, transfer, remove"
-msgstr ""
-
msgid "Path:"
msgstr ""
@@ -14523,7 +14842,7 @@ msgstr ""
msgid "Percentage"
msgstr ""
-msgid "Perform advanced options such as changing path, transferring, or removing the group."
+msgid "Perform advanced options such as changing path, transferring, exporting, or removing the group."
msgstr ""
msgid "Perform common operations on GitLab project"
@@ -14604,9 +14923,6 @@ msgstr "Pianificazione multipla Pipeline"
msgid "Pipeline minutes quota"
msgstr ""
-msgid "Pipeline quota"
-msgstr ""
-
msgid "Pipeline subscriptions"
msgstr ""
@@ -14616,6 +14932,9 @@ msgstr ""
msgid "Pipeline: %{status}"
msgstr ""
+msgid "PipelineCharts|CI / CD Analytics"
+msgstr ""
+
msgid "PipelineCharts|Failed:"
msgstr "Fallita:"
@@ -14694,15 +15013,6 @@ msgstr ""
msgid "Pipelines settings for '%{project_name}' were successfully updated."
msgstr ""
-msgid "Pipelines| to purchase more minutes."
-msgstr ""
-
-msgid "Pipelines|%{namespace_name} has exceeded its pipeline minutes quota."
-msgstr ""
-
-msgid "Pipelines|%{namespace_name} has less than %{notification_level}%% of CI minutes available."
-msgstr ""
-
msgid "Pipelines|API"
msgstr ""
@@ -14724,10 +15034,13 @@ msgstr ""
msgid "Pipelines|Get started with Pipelines"
msgstr ""
-msgid "Pipelines|Loading Pipelines"
+msgid "Pipelines|Group %{namespace_name} has %{percentage}%% or less Shared Runner Pipeline minutes remaining. Once it runs out, no new jobs or pipelines in its projects will run."
msgstr ""
-msgid "Pipelines|Pipelines will not run anymore on shared Runners."
+msgid "Pipelines|Group %{namespace_name} has exceeded its pipeline minutes quota. Unless you buy additional pipeline minutes, no new jobs or pipelines in its projects will run."
+msgstr ""
+
+msgid "Pipelines|Loading Pipelines"
msgstr ""
msgid "Pipelines|Project cache successfully reset."
@@ -14958,6 +15271,9 @@ msgstr ""
msgid "Please select"
msgstr ""
+msgid "Please select a Jira project"
+msgstr ""
+
msgid "Please select a country"
msgstr ""
@@ -15003,6 +15319,9 @@ msgstr ""
msgid "Please wait while we import the repository for you. Refresh at will."
msgstr ""
+msgid "Plugins directory is deprecated and will be removed in 14.0. Please move this file into /file_hooks directory."
+msgstr ""
+
msgid "Pod does not exist"
msgstr ""
@@ -15120,7 +15439,7 @@ msgstr ""
msgid "Prevent users from changing their profile name"
msgstr ""
-msgid "Prevent users from modifing merge request approvers list"
+msgid "Prevent users from modifying merge request approvers list"
msgstr ""
msgid "Prevent users from performing write operations on GitLab while performing maintenance."
@@ -15141,10 +15460,10 @@ msgstr ""
msgid "Previous Artifacts"
msgstr ""
-msgid "Previous file in diff (MRs only)"
+msgid "Previous file in diff"
msgstr ""
-msgid "Previous unresolved discussion (MRs only)"
+msgid "Previous unresolved discussion"
msgstr ""
msgid "Primary"
@@ -15177,6 +15496,9 @@ msgstr ""
msgid "Private profile"
msgstr ""
+msgid "Private projects Minutes cost factor"
+msgstr ""
+
msgid "Private projects can be created in your personal namespace with:"
msgstr ""
@@ -16005,6 +16327,9 @@ msgstr ""
msgid "ProjectSettings|With GitLab Pages you can host your static websites on GitLab"
msgstr ""
+msgid "ProjectSettings|With Metrics Dashboard you can visualize this project performance metrics"
+msgstr ""
+
msgid "ProjectTemplates|.NET Core"
msgstr ""
@@ -16068,6 +16393,9 @@ msgstr ""
msgid "ProjectTemplates|Spring"
msgstr ""
+msgid "ProjectTemplates|Static Site Editor/Middleman"
+msgstr ""
+
msgid "ProjectTemplates|iOS (Swift)"
msgstr ""
@@ -16287,13 +16615,13 @@ msgstr ""
msgid "PrometheusService|Prometheus is being automatically managed on your clusters"
msgstr ""
-msgid "PrometheusService|These metrics will only be monitored after your first deployment to an environment"
+msgid "PrometheusService|Select the Active checkbox to override the Auto Configuration with custom settings. If unchecked, Auto Configuration settings are used."
msgstr ""
-msgid "PrometheusService|Time-series monitoring service"
+msgid "PrometheusService|These metrics will only be monitored after your first deployment to an environment"
msgstr ""
-msgid "PrometheusService|To enable manual configuration, uninstall Prometheus from your clusters"
+msgid "PrometheusService|Time-series monitoring service"
msgstr ""
msgid "PrometheusService|To enable the installation of Prometheus on your clusters, deactivate the manual configuration below"
@@ -16479,6 +16807,9 @@ msgstr ""
msgid "Protip:"
msgstr ""
+msgid "Protocol"
+msgstr ""
+
msgid "Provider"
msgstr ""
@@ -16503,6 +16834,9 @@ msgstr ""
msgid "Public pipelines"
msgstr ""
+msgid "Public projects Minutes cost factor"
+msgstr ""
+
msgid "Pull"
msgstr ""
@@ -16703,6 +17037,9 @@ msgid_plural "Refreshing in %d seconds to show the updated status..."
msgstr[0] ""
msgstr[1] ""
+msgid "Regenerate export"
+msgstr ""
+
msgid "Regenerate instance ID"
msgstr ""
@@ -17080,9 +17417,29 @@ msgstr ""
msgid "Reports|%{combinedString} and %{resolvedString}"
msgstr ""
+msgid "Reports|Accessibility scanning detected %d issue for the source branch only"
+msgid_plural "Reports|Accessibility scanning detected %d issues for the source branch only"
+msgstr[0] ""
+msgstr[1] ""
+
+msgid "Reports|Accessibility scanning detected no issues for the source branch only"
+msgstr ""
+
+msgid "Reports|Accessibility scanning failed loading results"
+msgstr ""
+
+msgid "Reports|Accessibility scanning results are being parsed"
+msgstr ""
+
msgid "Reports|Actions"
msgstr ""
+msgid "Reports|An error occured while loading report"
+msgstr ""
+
+msgid "Reports|An error occurred while loading %{name} results"
+msgstr ""
+
msgid "Reports|Class"
msgstr ""
@@ -17188,7 +17545,7 @@ msgstr ""
msgid "Requested %{time_ago}"
msgstr ""
-msgid "Requested design version does not exist"
+msgid "Requested design version does not exist."
msgstr ""
msgid "Requested states are invalid"
@@ -17372,6 +17729,9 @@ msgstr ""
msgid "Resync"
msgstr ""
+msgid "Resync all"
+msgstr ""
+
msgid "Resync all %{replicableType}"
msgstr ""
@@ -17440,6 +17800,9 @@ msgstr ""
msgid "Revoked personal access token %{personal_access_token_name}!"
msgstr ""
+msgid "Revoked project access token %{project_access_token_name}!"
+msgstr ""
+
msgid "RightSidebar|adding a"
msgstr ""
@@ -17488,6 +17851,9 @@ msgstr ""
msgid "Runner token"
msgstr ""
+msgid "Runner tokens"
+msgstr ""
+
msgid "Runner was not updated."
msgstr ""
@@ -17542,6 +17908,9 @@ msgstr ""
msgid "SAML SSO for %{group_name}"
msgstr ""
+msgid "SAML discovery tokens"
+msgstr ""
+
msgid "SAML for %{group_name}"
msgstr ""
@@ -17626,12 +17995,18 @@ msgstr "Pianifica una nuova Pipeline"
msgid "Scheduled"
msgstr ""
+msgid "Scheduled to merge this merge request (%{strategy})."
+msgstr ""
+
msgid "Scheduled to merge this merge request when the pipeline succeeds."
msgstr ""
msgid "Schedules"
msgstr ""
+msgid "Schedules to merge this merge request (%{strategy})."
+msgstr ""
+
msgid "Scheduling"
msgstr ""
@@ -17689,6 +18064,9 @@ msgstr ""
msgid "Search branches and tags"
msgstr "Ricerca branches e tags"
+msgid "Search by author"
+msgstr ""
+
msgid "Search files"
msgstr ""
@@ -17834,11 +18212,6 @@ msgid_plural "SearchResults|snippets"
msgstr[0] ""
msgstr[1] ""
-msgid "SearchResults|snippet result"
-msgid_plural "SearchResults|snippet results"
-msgstr[0] ""
-msgstr[1] ""
-
msgid "SearchResults|user"
msgid_plural "SearchResults|users"
msgstr[0] ""
@@ -17849,6 +18222,9 @@ msgid_plural "SearchResults|wiki results"
msgstr[0] ""
msgstr[1] ""
+msgid "Searching by both author and message is currently not supported."
+msgstr ""
+
msgid "Seat Link"
msgstr ""
@@ -17876,184 +18252,232 @@ msgstr ""
msgid "Security Dashboard"
msgstr ""
-msgid "Security Dashboard|Error fetching the vulnerability counts. Please check your network connection and try again."
+msgid "Security configuration help link"
+msgstr ""
+
+msgid "Security dashboard"
msgstr ""
-msgid "Security Dashboard|Error fetching the vulnerability list. Please check your network connection and try again."
+msgid "Security report is out of date. Please update your branch with the latest changes from the target branch (%{targetBranchName})"
msgstr ""
-msgid "Security Dashboard|Issue Created"
+msgid "Security report is out of date. Run %{newPipelineLinkStart}a new pipeline%{newPipelineLinkEnd} for the target branch (%{targetBranchName})"
msgstr ""
-msgid "Security Reports|Comment added to '%{vulnerabilityName}'"
+msgid "SecurityConfiguration|Configured"
msgstr ""
-msgid "Security Reports|Comment deleted on '%{vulnerabilityName}'"
+msgid "SecurityConfiguration|Feature"
msgstr ""
-msgid "Security Reports|Comment edited on '%{vulnerabilityName}'"
+msgid "SecurityConfiguration|Feature documentation for %{featureName}"
msgstr ""
-msgid "Security Reports|Create issue"
+msgid "SecurityConfiguration|Not yet configured"
msgstr ""
-msgid "Security Reports|Dismiss vulnerability"
+msgid "SecurityConfiguration|Secure features"
msgstr ""
-msgid "Security Reports|Dismissed '%{vulnerabilityName}'"
+msgid "SecurityConfiguration|Status"
msgstr ""
-msgid "Security Reports|Dismissed '%{vulnerabilityName}'. Turn off the hide dismissed toggle to view."
+msgid "SecurityReports|%{firstProject} and %{secondProject}"
msgstr ""
-msgid "Security Reports|Either you don't have permission to view this dashboard or the dashboard has not been setup. Please check your permission settings with your administrator or check your dashboard configurations to proceed."
+msgid "SecurityReports|%{firstProject}, %{secondProject}, and %{rest}"
msgstr ""
-msgid "Security Reports|Learn more about setting up your dashboard"
+msgid "SecurityReports|Add a project to your dashboard"
msgstr ""
-msgid "Security Reports|More info"
+msgid "SecurityReports|Add or remove projects from your dashboard"
msgstr ""
-msgid "Security Reports|Oops, something doesn't seem right."
+msgid "SecurityReports|Add projects"
msgstr ""
-msgid "Security Reports|Security reports can only be accessed by authorized users."
+msgid "SecurityReports|Comment added to '%{vulnerabilityName}'"
msgstr ""
-msgid "Security Reports|There was an error adding the comment."
+msgid "SecurityReports|Comment deleted on '%{vulnerabilityName}'"
msgstr ""
-msgid "Security Reports|There was an error creating the issue."
+msgid "SecurityReports|Comment edited on '%{vulnerabilityName}'"
msgstr ""
-msgid "Security Reports|There was an error creating the merge request."
+msgid "SecurityReports|Create issue"
msgstr ""
-msgid "Security Reports|There was an error deleting the comment."
+msgid "SecurityReports|Dismiss Selected"
msgstr ""
-msgid "Security Reports|There was an error dismissing the vulnerabilities."
+msgid "SecurityReports|Dismiss vulnerability"
msgstr ""
-msgid "Security Reports|There was an error dismissing the vulnerability."
+msgid "SecurityReports|Dismissed '%{vulnerabilityName}'"
msgstr ""
-msgid "Security Reports|There was an error reverting the dismissal."
+msgid "SecurityReports|Dismissed '%{vulnerabilityName}'. Turn off the hide dismissed toggle to view."
msgstr ""
-msgid "Security Reports|There was an error reverting this dismissal."
+msgid "SecurityReports|Each vulnerability now has a unique page that can be directly linked to, shared, referenced, and tracked as the single source of truth. Vulnerability occurrences also persist across scanner runs, which improves tracking and visibility and reduces duplicates between scans."
msgstr ""
-msgid "Security Reports|Undo dismiss"
+msgid "SecurityReports|Edit dashboard"
msgstr ""
-msgid "Security Reports|You do not have sufficient permissions to access this report"
+msgid "SecurityReports|Either you don't have permission to view this dashboard or the dashboard has not been setup. Please check your permission settings with your administrator or check your dashboard configurations to proceed."
msgstr ""
-msgid "Security Reports|You must sign in as an authorized user to see this report"
+msgid "SecurityReports|Error fetching the vulnerability counts. Please check your network connection and try again."
msgstr ""
-msgid "Security configuration help link"
+msgid "SecurityReports|Error fetching the vulnerability list. Please check your network connection and try again."
msgstr ""
-msgid "Security dashboard"
+msgid "SecurityReports|False positive"
msgstr ""
-msgid "Security report is out of date. Please update your branch with the latest changes from the target branch (%{targetBranchName})"
+msgid "SecurityReports|Hide dismissed"
msgstr ""
-msgid "Security report is out of date. Run %{newPipelineLinkStart}a new pipeline%{newPipelineLinkEnd} for the target branch (%{targetBranchName})"
+msgid "SecurityReports|Introducing standalone vulnerabilities"
msgstr ""
-msgid "SecurityConfiguration|Configured"
+msgid "SecurityReports|Issue Created"
msgstr ""
-msgid "SecurityConfiguration|Feature"
+msgid "SecurityReports|Learn More"
msgstr ""
-msgid "SecurityConfiguration|Feature documentation for %{featureName}"
+msgid "SecurityReports|Learn more about setting up your dashboard"
msgstr ""
-msgid "SecurityConfiguration|Not yet configured"
+msgid "SecurityReports|Load more vulnerabilities"
msgstr ""
-msgid "SecurityConfiguration|Secure features"
+msgid "SecurityReports|Monitor vulnerabilities in your code"
msgstr ""
-msgid "SecurityConfiguration|Status"
+msgid "SecurityReports|More info"
msgstr ""
-msgid "SecurityDashboard|%{firstProject} and %{secondProject}"
+msgid "SecurityReports|More information"
msgstr ""
-msgid "SecurityDashboard|%{firstProject}, %{secondProject}, and %{rest}"
+msgid "SecurityReports|No vulnerabilities found for dashboard"
msgstr ""
-msgid "SecurityDashboard|Add a project to your dashboard"
+msgid "SecurityReports|No vulnerabilities found for this group"
msgstr ""
-msgid "SecurityDashboard|Add or remove projects from your dashboard"
+msgid "SecurityReports|No vulnerabilities found for this pipeline"
msgstr ""
-msgid "SecurityDashboard|Add projects"
+msgid "SecurityReports|No vulnerabilities found for this project"
msgstr ""
-msgid "SecurityDashboard|Edit dashboard"
+msgid "SecurityReports|Oops, something doesn't seem right."
msgstr ""
-msgid "SecurityDashboard|Hide dismissed"
+msgid "SecurityReports|Pipeline %{pipelineLink} triggered %{timeago} by %{user}"
msgstr ""
-msgid "SecurityDashboard|Monitor vulnerabilities in your code"
+msgid "SecurityReports|Project"
msgstr ""
-msgid "SecurityDashboard|More information"
+msgid "SecurityReports|Projects added"
msgstr ""
-msgid "SecurityDashboard|No vulnerabilities found for dashboard"
+msgid "SecurityReports|Remove project from dashboard"
msgstr ""
-msgid "SecurityDashboard|Pipeline %{pipelineLink} triggered %{timeago} by %{user}"
+msgid "SecurityReports|Report type"
msgstr ""
-msgid "SecurityDashboard|Project"
+msgid "SecurityReports|Return to dashboard"
msgstr ""
-msgid "SecurityDashboard|Projects added"
+msgid "SecurityReports|Security Dashboard"
msgstr ""
-msgid "SecurityDashboard|Remove project from dashboard"
+msgid "SecurityReports|Security reports can only be accessed by authorized users."
msgstr ""
-msgid "SecurityDashboard|Report type"
+msgid "SecurityReports|Select a project to add by using the project search field above."
msgstr ""
-msgid "SecurityDashboard|Return to dashboard"
+msgid "SecurityReports|Select a reason"
msgstr ""
-msgid "SecurityDashboard|Security Dashboard"
+msgid "SecurityReports|Severity"
msgstr ""
-msgid "SecurityDashboard|Select a project to add by using the project search field above."
+msgid "SecurityReports|Status"
msgstr ""
-msgid "SecurityDashboard|Severity"
+msgid "SecurityReports|The security dashboard displays the latest security findings for projects you wish to monitor. Select \"Edit dashboard\" to add and remove projects."
msgstr ""
-msgid "SecurityDashboard|Status"
+msgid "SecurityReports|The security dashboard displays the latest security report. Use it to find and fix vulnerabilities."
msgstr ""
-msgid "SecurityDashboard|The security dashboard displays the latest security findings for projects you wish to monitor. Select \"Edit dashboard\" to add and remove projects."
+msgid "SecurityReports|There was an error adding the comment."
msgstr ""
-msgid "SecurityDashboard|The security dashboard displays the latest security report. Use it to find and fix vulnerabilities."
+msgid "SecurityReports|There was an error creating the issue."
msgstr ""
-msgid "SecurityDashboard|There was an error while generating the report."
+msgid "SecurityReports|There was an error creating the merge request."
msgstr ""
-msgid "SecurityDashboard|Unable to add %{invalidProjects}"
+msgid "SecurityReports|There was an error deleting the comment."
+msgstr ""
+
+msgid "SecurityReports|There was an error dismissing the vulnerabilities."
+msgstr ""
+
+msgid "SecurityReports|There was an error dismissing the vulnerability."
+msgstr ""
+
+msgid "SecurityReports|There was an error reverting the dismissal."
+msgstr ""
+
+msgid "SecurityReports|There was an error reverting this dismissal."
+msgstr ""
+
+msgid "SecurityReports|There was an error while generating the report."
+msgstr ""
+
+msgid "SecurityReports|Unable to add %{invalidProjects}"
+msgstr ""
+
+msgid "SecurityReports|Undo dismiss"
+msgstr ""
+
+msgid "SecurityReports|While it's rare to have no vulnerabilities for your group, it can happen. In any event, we ask that you double check your settings to make sure you've set up your dashboard correctly."
+msgstr ""
+
+msgid "SecurityReports|While it's rare to have no vulnerabilities for your pipeline, it can happen. In any event, we ask that you double check your settings to make sure all security scanning jobs have passed successfully."
+msgstr ""
+
+msgid "SecurityReports|While it's rare to have no vulnerabilities for your project, it can happen. In any event, we ask that you double check your settings to make sure you've set up your dashboard correctly."
+msgstr ""
+
+msgid "SecurityReports|While it's rare to have no vulnerabilities, it can happen. In any event, we ask that you please double check your settings to make sure you've set up your dashboard correctly."
+msgstr ""
+
+msgid "SecurityReports|Won't fix / Accept risk"
+msgstr ""
+
+msgid "SecurityReports|You do not have sufficient permissions to access this report"
+msgstr ""
+
+msgid "SecurityReports|You must sign in as an authorized user to see this report"
+msgstr ""
+
+msgid "SecurityReports|[No reason]"
msgstr ""
msgid "See GitLab's %{password_policy_guidelines}"
@@ -18065,6 +18489,9 @@ msgstr ""
msgid "See the affected projects in the GitLab admin panel"
msgstr ""
+msgid "See what's new at GitLab"
+msgstr ""
+
msgid "Select"
msgstr ""
@@ -18470,9 +18897,6 @@ msgstr ""
msgid "Set up Jira Integration"
msgstr ""
-msgid "Set up Jira Integration illustration"
-msgstr ""
-
msgid "Set up a %{type} Runner automatically"
msgstr ""
@@ -18557,6 +18981,9 @@ msgstr ""
msgid "Severity: %{severity}"
msgstr ""
+msgid "Shards (%{shards})"
+msgstr ""
+
msgid "Shards to synchronize"
msgstr ""
@@ -18871,6 +19298,9 @@ msgstr ""
msgid "Snowplow"
msgstr ""
+msgid "Some child epics may be hidden due to applied filters"
+msgstr ""
+
msgid "Some email servers do not support overriding the email sender name. Enable this option to include the name of the author of the issue, merge request or comment in the email body instead."
msgstr ""
@@ -19012,6 +19442,9 @@ msgstr ""
msgid "Something went wrong while updating your list settings"
msgstr ""
+msgid "Something went wrong with your automatic subscription renewal"
+msgstr ""
+
msgid "Something went wrong, unable to add %{project} to dashboard"
msgstr ""
@@ -19297,6 +19730,9 @@ msgstr ""
msgid "Stage removed"
msgstr ""
+msgid "Standard"
+msgstr ""
+
msgid "Star a label to make it a priority label. Order the prioritized labels to change their relative priority, by dragging."
msgstr ""
@@ -19420,6 +19856,9 @@ msgstr ""
msgid "Static Application Security Testing (SAST)"
msgstr ""
+msgid "StaticSiteEditor|An error occurred while submitting your changes."
+msgstr ""
+
msgid "StaticSiteEditor|Branch could not be created."
msgstr ""
@@ -19429,18 +19868,30 @@ msgstr ""
msgid "StaticSiteEditor|Could not create merge request."
msgstr ""
+msgid "StaticSiteEditor|Incompatible file content"
+msgstr ""
+
msgid "StaticSiteEditor|Return to site"
msgstr ""
+msgid "StaticSiteEditor|Static site editor"
+msgstr ""
+
msgid "StaticSiteEditor|Success!"
msgstr ""
msgid "StaticSiteEditor|Summary of changes"
msgstr ""
+msgid "StaticSiteEditor|The Static Site Editor is currently configured to only edit Markdown content on pages generated from Middleman. Visit the documentation to learn more about configuring your site to use the Static Site Editor."
+msgstr ""
+
msgid "StaticSiteEditor|Update %{sourcePath} file"
msgstr ""
+msgid "StaticSiteEditor|View documentation"
+msgstr ""
+
msgid "StaticSiteEditor|View merge request"
msgstr ""
@@ -19498,6 +19949,12 @@ msgstr ""
msgid "StatusPage|Status page"
msgstr ""
+msgid "StatusPage|Status page URL"
+msgstr ""
+
+msgid "StatusPage|Status page frontend documentation"
+msgstr ""
+
msgid "StatusPage|To publish incidents to an external status page, GitLab will store a JSON file in your Amazon S3 account in a location accessible to your external status page service. Make sure to also set up %{docsLink}"
msgstr ""
@@ -19846,6 +20303,12 @@ msgstr ""
msgid "Synced"
msgstr ""
+msgid "Synchronization disabled"
+msgstr ""
+
+msgid "Synchronization of container repositories is disabled."
+msgstr ""
+
msgid "System"
msgstr ""
@@ -19999,6 +20462,9 @@ msgstr ""
msgid "Telephone number"
msgstr ""
+msgid "Telephone number (Optional)"
+msgstr ""
+
msgid "Template"
msgstr ""
@@ -20305,6 +20771,9 @@ msgstr ""
msgid "The license was successfully uploaded and is now active. You can see the details below."
msgstr ""
+msgid "The license was successfully uploaded and will be active from %{starts_at}. You can see the details below."
+msgstr ""
+
msgid "The maximum file size allowed is %{size}."
msgstr ""
@@ -20626,6 +21095,9 @@ msgstr ""
msgid "There was an error getting the epic participants."
msgstr ""
+msgid "There was an error importing the Jira project."
+msgstr ""
+
msgid "There was an error loading users activity calendar."
msgstr ""
@@ -20641,7 +21113,7 @@ msgstr ""
msgid "There was an error resetting user pipeline minutes."
msgstr ""
-msgid "There was an error saving this Geo Node"
+msgid "There was an error saving this Geo Node."
msgstr ""
msgid "There was an error saving your changes."
@@ -20704,6 +21176,9 @@ msgstr ""
msgid "They can be managed using the %{link}."
msgstr ""
+msgid "Third Party Advisory Link"
+msgstr ""
+
msgid "Third party offers"
msgstr ""
@@ -20740,6 +21215,9 @@ msgstr ""
msgid "This application will be able to:"
msgstr ""
+msgid "This attachment has been truncated to avoid exceeding the maximum allowed attachment size of 15MB. %{written_count} of %{issues_count} issues have been included. Consider re-exporting with a narrower selection of issues."
+msgstr ""
+
msgid "This block is self-referential"
msgstr ""
@@ -20881,6 +21359,12 @@ msgstr ""
msgid "This issue is confidential"
msgstr ""
+msgid "This issue is currently blocked by the following issues: %{issues}."
+msgstr ""
+
+msgid "This issue is in a child epic of the filtered epic"
+msgstr ""
+
msgid "This issue is locked."
msgstr ""
@@ -21079,7 +21563,10 @@ msgstr ""
msgid "This user will be the author of all events in the activity feed that are the result of an update, like new branches being created or new commits being pushed to existing branches. Upon creation or when reassigning you can only assign yourself to be the mirror user."
msgstr ""
-msgid "This variable can not be masked"
+msgid "This variable can not be masked."
+msgstr ""
+
+msgid "This variable does not match the expected pattern."
msgstr ""
msgid "This will help us personalize your onboarding experience."
@@ -21109,10 +21596,10 @@ msgstr ""
msgid "ThreatMonitoring|Application firewall not detected"
msgstr ""
-msgid "ThreatMonitoring|Container Network Policy"
+msgid "ThreatMonitoring|Container Network Policies are not installed or have been disabled. To view this data, ensure your Network Policies are installed and enabled for your cluster."
msgstr ""
-msgid "ThreatMonitoring|Container NetworkPolicies are not installed or has been disabled. To view this data, ensure you NetworkPolicies are installed and enabled for your cluster."
+msgid "ThreatMonitoring|Container Network Policy"
msgstr ""
msgid "ThreatMonitoring|Container NetworkPolicies not detected"
@@ -21145,7 +21632,7 @@ msgstr ""
msgid "ThreatMonitoring|Something went wrong, unable to fetch statistics"
msgstr ""
-msgid "ThreatMonitoring|The firewall is not installed or has been disabled. To view this data, ensure you firewall is installed and enabled for your cluster."
+msgid "ThreatMonitoring|The firewall is not installed or has been disabled. To view this data, ensure the web application firewall is installed and enabled for your cluster."
msgstr ""
msgid "ThreatMonitoring|The graph below is an overview of traffic coming to your application as tracked by the Web Application Firewall (WAF). View the docs for instructions on how to access the WAF logs to see what type of malicious traffic is trying to access your app. The docs link is also accessible by clicking the \"?\" icon next to the title below."
@@ -21214,6 +21701,9 @@ msgstr ""
msgid "Time in seconds GitLab will wait for a response from the external service. When the service does not respond in time, access will be denied."
msgstr ""
+msgid "Time of import: %{importTime}"
+msgstr ""
+
msgid "Time remaining"
msgstr ""
@@ -21398,7 +21888,7 @@ msgstr ""
msgid "Title:"
msgstr ""
-msgid "Titles and Filenames"
+msgid "Titles and Descriptions"
msgstr ""
msgid "To"
@@ -21605,7 +22095,7 @@ msgstr ""
msgid "Too many projects enabled. You will need to manage them via the console or the API."
msgstr ""
-msgid "Topics"
+msgid "Topics (optional)"
msgstr ""
msgid "Total"
@@ -21767,6 +22257,9 @@ msgstr ""
msgid "Try all GitLab has to offer for 30 days."
msgstr ""
+msgid "Try changing or removing filters."
+msgstr ""
+
msgid "Try to fork again"
msgstr ""
@@ -21860,6 +22353,9 @@ msgstr ""
msgid "Unable to connect to server: %{error}"
msgstr ""
+msgid "Unable to connect to the Jira instance. Please check your Jira integration configuration."
+msgstr ""
+
msgid "Unable to convert Kubernetes logs encoding to UTF-8"
msgstr ""
@@ -22803,6 +23299,9 @@ msgstr ""
msgid "View issue"
msgstr ""
+msgid "View issues"
+msgstr ""
+
msgid "View it on GitLab"
msgstr ""
@@ -22980,6 +23479,15 @@ msgstr ""
msgid "VulnerabilityManagement|Resolved %{timeago} by %{user}"
msgstr ""
+msgid "VulnerabilityManagement|Something went wrong while trying to delete the comment. Please try again later."
+msgstr ""
+
+msgid "VulnerabilityManagement|Something went wrong while trying to retrieve the vulnerability history. Please try again later."
+msgstr ""
+
+msgid "VulnerabilityManagement|Something went wrong while trying to save the comment. Please try again later."
+msgstr ""
+
msgid "VulnerabilityManagement|Something went wrong, could not create an issue."
msgstr ""
@@ -23010,6 +23518,9 @@ msgstr ""
msgid "VulnerabilityStatusTypes|Resolved"
msgstr ""
+msgid "Vulnerability|%{scannerName} (version %{scannerVersion})"
+msgstr ""
+
msgid "Vulnerability|Class"
msgstr ""
@@ -23043,7 +23554,10 @@ msgstr ""
msgid "Vulnerability|Project"
msgstr ""
-msgid "Vulnerability|Report Type"
+msgid "Vulnerability|Scanner Provider"
+msgstr ""
+
+msgid "Vulnerability|Scanner Type"
msgstr ""
msgid "Vulnerability|Severity"
@@ -23097,9 +23611,15 @@ msgstr ""
msgid "We sent you an email with reset password instructions"
msgstr ""
+msgid "We tried to automatically renew your %{strong}%{plan_name}%{strong_close} subscription for %{strong}%{namespace_name}%{strong_close} on %{expires_on} but something went wrong so your subscription was downgraded to the free plan. Don't worry, your data is safe. We suggest you check your payment method and get in touch with our support team (%{support_link}). They'll gladly help with your subscription renewal."
+msgstr ""
+
msgid "We want to be sure it is you, please confirm you are not a robot."
msgstr ""
+msgid "We will automatically renew your %{strong}%{plan_name}%{strong_close} subscription for %{strong}%{namespace_name}%{strong_close} on %{strong}%{expires_on}%{strong_close}. There's nothing that you need to do, we'll let you know when the renewal is complete. Need more seats, a higher plan or just want to review your payment method?"
+msgstr ""
+
msgid "We've found no vulnerabilities"
msgstr ""
@@ -23204,15 +23724,6 @@ msgstr ""
msgid "When:"
msgstr ""
-msgid "While it's rare to have no vulnerabilities for your group, it can happen. In any event, we ask that you double check your settings to make sure you've set up your dashboard correctly."
-msgstr ""
-
-msgid "While it's rare to have no vulnerabilities for your pipeline, it can happen. In any event, we ask that you double check your settings to make sure all security scanning jobs have passed successfully."
-msgstr ""
-
-msgid "While it's rare to have no vulnerabilities for your project, it can happen. In any event, we ask that you double check your settings to make sure you've set up your dashboard correctly."
-msgstr ""
-
msgid "While it's rare to have no vulnerabilities, it can happen. In any event, we ask that you please double check your settings to make sure you've set up your dashboard correctly."
msgstr ""
@@ -23243,9 +23754,6 @@ msgstr ""
msgid "Wiki"
msgstr ""
-msgid "Wiki pages"
-msgstr ""
-
msgid "Wiki was successfully updated."
msgstr ""
@@ -23429,6 +23937,9 @@ msgstr ""
msgid "Yes, add it"
msgstr ""
+msgid "Yes, close issue"
+msgstr ""
+
msgid "Yes, let me map Google Code users to full names or GitLab users."
msgstr ""
@@ -23624,6 +24135,9 @@ msgstr ""
msgid "You could not create a new trigger."
msgstr ""
+msgid "You didn't renew your %{strong}%{plan_name}%{strong_close} subscription for %{strong}%{namespace_name}%{strong_close} so it was downgraded to the free plan."
+msgstr ""
+
msgid "You didn't renew your %{strong}%{plan_name}%{strong_close} subscription so it was downgraded to the GitLab Core Plan."
msgstr ""
@@ -23690,6 +24204,9 @@ msgstr ""
msgid "You have declined the invitation to join %{label}."
msgstr ""
+msgid "You have imported from this project %{numberOfPreviousImportsForProject} times before. Each new import will create duplicate issues."
+msgstr ""
+
msgid "You have no permissions"
msgstr ""
@@ -23771,6 +24288,9 @@ msgstr ""
msgid "You need to upload a Google Takeout archive."
msgstr ""
+msgid "You reached %{usage_in_percent} of %{namespace_name}'s capacity (%{used_storage} of %{storage_limit})"
+msgstr ""
+
msgid "You tried to fork %{link_to_the_project} but it failed for the following reason:"
msgstr ""
@@ -23855,9 +24375,21 @@ msgstr ""
msgid "YouTube"
msgstr ""
+msgid "Your %{strong}%{plan_name}%{strong_close} subscription for %{strong}%{namespace_name}%{strong_close} will expire on %{strong}%{expires_on}%{strong_close}. After that, you will not to be able to create issues or merge requests as well as many other features."
+msgstr ""
+
msgid "Your %{strong}%{plan_name}%{strong_close} subscription will expire on %{strong}%{expires_on}%{strong_close}. After that, you will not to be able to create issues or merge requests as well as many other features."
msgstr ""
+msgid "Your CSV export has started. It will be emailed to %{email} when complete."
+msgstr ""
+
+msgid "Your CSV export of %{issues_count} from project %{project_link} has been added to this email as an attachment."
+msgstr ""
+
+msgid "Your CSV export of %{written_count} from project %{project_name} (%{project_url}) has been added to this email as an attachment."
+msgstr ""
+
msgid "Your Commit Email will be used for web based operations, such as edits and merges."
msgstr ""
@@ -23954,6 +24486,9 @@ msgstr ""
msgid "Your comment could not be updated! Please check your network connection and try again."
msgstr ""
+msgid "Your comment will be discarded."
+msgstr ""
+
msgid "Your custom stage '%{title}' was created"
msgstr ""
@@ -23993,6 +24528,9 @@ msgstr ""
msgid "Your new personal access token has been created."
msgstr ""
+msgid "Your new project access token has been created."
+msgstr ""
+
msgid "Your password isn't required to view this page. If a password or any other personal details are requested, please contact your administrator to report abuse."
msgstr ""
@@ -24011,12 +24549,18 @@ msgstr ""
msgid "Your request for access has been queued for review."
msgstr ""
+msgid "Your search didn't match any commits."
+msgstr ""
+
msgid "Your subscription expired!"
msgstr ""
msgid "Your subscription has been downgraded"
msgstr ""
+msgid "Your subscription will automatically renew in %{remaining_days}"
+msgstr ""
+
msgid "Your subscription will expire in %{remaining_days}"
msgstr ""
@@ -24055,7 +24599,7 @@ msgstr "fa"
msgid "allowed to fail"
msgstr ""
-msgid "already being used for another group or project milestone."
+msgid "already being used for another group or project %{timebox_name}."
msgstr ""
msgid "already has a \"created\" issue link"
@@ -24133,6 +24677,9 @@ msgstr ""
msgid "ciReport|%{linkStartTag}Learn more about SAST %{linkEndTag}"
msgstr ""
+msgid "ciReport|%{linkStartTag}Learn more about Secret Scanning %{linkEndTag}"
+msgstr ""
+
msgid "ciReport|%{linkStartTag}Learn more about codequality reports %{linkEndTag}"
msgstr ""
@@ -24296,6 +24843,12 @@ msgstr ""
msgid "ciReport|SAST"
msgstr ""
+msgid "ciReport|Secret scanning"
+msgstr ""
+
+msgid "ciReport|Secret scanning detects secrets and credentials vulnerabilities in your source code."
+msgstr ""
+
msgid "ciReport|Security scanning"
msgstr ""
@@ -24493,6 +25046,9 @@ msgstr ""
msgid "group"
msgstr ""
+msgid "groups"
+msgstr ""
+
msgid "has already been linked to another vulnerability"
msgstr ""
@@ -24656,9 +25212,6 @@ msgstr[1] ""
msgid "merged %{time_ago}"
msgstr ""
-msgid "milestone should belong either to a project or a group."
-msgstr ""
-
msgid "missing"
msgstr ""
@@ -24995,6 +25548,9 @@ msgstr "Nuova richiesta di merge"
msgid "no contributions"
msgstr ""
+msgid "no expiration"
+msgstr ""
+
msgid "no one can merge"
msgstr ""
@@ -25047,6 +25603,9 @@ msgstr ""
msgid "pending removal"
msgstr ""
+msgid "per day"
+msgstr ""
+
msgid "pipeline"
msgstr ""
@@ -25075,6 +25634,12 @@ msgstr[1] ""
msgid "project avatar"
msgstr ""
+msgid "projects"
+msgstr ""
+
+msgid "push to your repository, create pipelines, create issues or add comments. To reduce storage capacity, delete unused repositories, artifacts, wikis, issues, and pipelines."
+msgstr ""
+
msgid "quick actions"
msgstr ""
@@ -25221,11 +25786,6 @@ msgstr ""
msgid "this document"
msgstr ""
-msgid "thread resolved"
-msgid_plural "threads resolved"
-msgstr[0] ""
-msgstr[1] ""
-
msgid "to help your contributors communicate effectively!"
msgstr ""
@@ -25280,6 +25840,9 @@ msgstr ""
msgid "view the blob"
msgstr ""
+msgid "vulnerability|Add a comment"
+msgstr ""
+
msgid "vulnerability|Add a comment or reason for dismissal"
msgstr ""
diff --git a/locale/ja/gitlab.po b/locale/ja/gitlab.po
index 92cf1a5fcb4..defe692b907 100644
--- a/locale/ja/gitlab.po
+++ b/locale/ja/gitlab.po
@@ -12,10 +12,10 @@ msgstr ""
"X-Crowdin-Project: gitlab-ee\n"
"X-Crowdin-Language: ja\n"
"X-Crowdin-File: /master/locale/gitlab.pot\n"
-"PO-Revision-Date: 2020-04-15 00:28\n"
+"PO-Revision-Date: 2020-05-05 22:46\n"
msgid " %{start} to %{end}"
-msgstr ""
+msgstr " %{start} ã‹ã‚‰ %{end} "
msgid " (from %{timeoutSource})"
msgstr " (%{timeoutSource} ã‹ã‚‰)"
@@ -73,7 +73,7 @@ msgstr[0] ""
msgid "%d changed file"
msgid_plural "%d changed files"
-msgstr[0] ""
+msgstr[0] "%d 個ã®å¤‰æ›´ã•ã‚ŒãŸãƒ•ã‚¡ã‚¤ãƒ«"
msgid "%d child epic"
msgid_plural "%d child epics"
@@ -180,6 +180,10 @@ msgstr[0] ""
msgid "%d tag"
msgid_plural "%d tags"
+msgstr[0] "%d ã‚¿ã‚°"
+
+msgid "%d unresolved thread"
+msgid_plural "%d unresolved threads"
msgstr[0] ""
msgid "%d vulnerability dismissed"
@@ -276,7 +280,7 @@ msgid "%{firstLabel} +%{labelCount} more"
msgstr "%{firstLabel} +%{labelCount} ã®è©³ç´°"
msgid "%{global_id} is not a valid id for %{expected_type}."
-msgstr ""
+msgstr "%{global_id} 㯠%{expected_type} ã«ã¨ã£ã¦æœ‰åŠ¹ãªIDã§ã¯ã‚ã‚Šã¾ã›ã‚“。"
msgid "%{group_docs_link_start}Groups%{group_docs_link_end} allow you to manage and collaborate across multiple projects. Members of a group have access to all of its projects."
msgstr "%{group_docs_link_start}グループ%{group_docs_link_end}を使用ã™ã‚‹ã¨ã€è¤‡æ•°ã®ãƒ—ロジェクトを管ç†ã—ã¦å…±åŒä½œæ¥­ã‚’è¡Œã†ã“ã¨ãŒã§ãã¾ã™ã€‚グループã®ãƒ¡ãƒ³ãƒãƒ¼ã¯ã€æ‰€å±žã™ã‚‹ãƒ—ロジェクトã®ã™ã¹ã¦ã«ã‚¢ã‚¯ã‚»ã‚¹ã§ãã¾ã™ã€‚"
@@ -291,7 +295,7 @@ msgid "%{issuableType} will be removed! Are you sure?"
msgstr "%{issuableType}を削除ã—ã¾ã™ï¼ã‚ˆã‚ã—ã„ã§ã™ã‹ï¼Ÿ"
msgid "%{issuesSize} issues"
-msgstr ""
+msgstr "%{issuesSize} 件ã®èª²é¡Œ"
msgid "%{issuesSize} issues with a limit of %{maxIssueCount}"
msgstr ""
@@ -335,6 +339,9 @@ msgstr "%{mergeLength}/%{usersLength} 人ãŒãƒžãƒ¼ã‚¸ã§ãã¾ã™"
msgid "%{mrText}, this issue will be closed automatically."
msgstr "%{mrText} ã€ã“ã®èª²é¡Œã¯è‡ªå‹•çš„ã«ã‚¯ãƒ­ãƒ¼ã‚ºã—ã¾ã™ã€‚"
+msgid "%{namespace_name} is now read-only. You cannot: %{base_message}"
+msgstr ""
+
msgid "%{name} contained %{resultsString}"
msgstr "%{name} ã«ã¯ %{resultsString} ãŒå«ã¾ã‚Œã¦ã„ã¾ã™"
@@ -347,6 +354,10 @@ msgstr ""
msgid "%{name}'s avatar"
msgstr "%{name}ã®ã‚¢ãƒã‚¿ãƒ¼"
+msgid "%{no_of_days} day"
+msgid_plural "%{no_of_days} days"
+msgstr[0] ""
+
msgid "%{number_commits_behind} commits behind %{default_branch}, %{number_commits_ahead} commits ahead"
msgstr "%{default_branch} ã‹ã‚‰ %{number_commits_behind} コミットé…ã‚Œã¦ã„ã¦ã€ %{number_commits_ahead} コミット進んã§ã„ã¾ã™ã€‚"
@@ -366,7 +377,7 @@ msgid "%{percent}%% complete"
msgstr "%{percent}%% 完了"
msgid "%{percent}%{percentSymbol} complete"
-msgstr ""
+msgstr "%{percent}%{percentSymbol} 完了"
msgid "%{placeholder} is not a valid color scheme"
msgstr ""
@@ -412,7 +423,7 @@ msgid "%{spanStart}in%{spanEnd} %{errorFn}"
msgstr ""
msgid "%{start} to %{end}"
-msgstr ""
+msgstr "%{start} ã‹ã‚‰ %{end} ã¾ã§"
msgid "%{state} epics"
msgstr "%{state}エピック"
@@ -446,13 +457,13 @@ msgid "%{tabname} changed"
msgstr "%{tabname} 変更ã•ã‚Œã¾ã—ãŸ"
msgid "%{tags} tag per image name"
-msgstr ""
+msgstr "ç”»åƒåã”ã¨ã«%{tags} tag"
msgid "%{tags} tags per image name"
-msgstr ""
+msgstr "ç”»åƒåã”ã¨ã«%{tags} tag"
msgid "%{tag}-%{evidence}-%{filename}"
-msgstr ""
+msgstr "%{tag}-%{evidence}-%{filename}"
msgid "%{template_project_id} is unknown or invalid"
msgstr "%{template_project_id} ã¯ä¸æ˜Žã€ã¾ãŸã¯ç„¡åŠ¹ã§ã™"
@@ -464,6 +475,9 @@ msgstr[0] "%{text} %{files} ファイル"
msgid "%{text} is available"
msgstr "%{text} ãŒåˆ©ç”¨ã§ãã¾ã™ã€‚"
+msgid "%{timebox_name} should belong either to a project or a group."
+msgstr ""
+
msgid "%{title} %{operator} %{threshold}"
msgstr "%{title} %{operator} %{threshold}"
@@ -541,7 +555,7 @@ msgid "(revoked)"
msgstr ""
msgid "*"
-msgstr ""
+msgstr "*"
msgid "+ %{amount} more"
msgstr "+ %{amount} 件以上"
@@ -632,7 +646,7 @@ msgid_plural "%d minutes"
msgstr[0] "%d 分"
msgid "1 month"
-msgstr ""
+msgstr "1ヶ月"
msgid "1 open issue"
msgid_plural "%{issues} open issues"
@@ -720,12 +734,18 @@ msgstr "<code>\"johnsmith@example.com\": \"johnsm...@example.com\"</code> ã¯ã€
msgid "<code>\"johnsmith@example.com\": \"johnsmith@example.com\"</code> will add \"By <a href=\"#\">johnsmith@example.com</a>\" to all issues and comments originally created by johnsmith@example.com. By default, the email address or username is masked to ensure the user's privacy. Use this option if you want to show the full email address."
msgstr "<code>\"johnsmith@example.com\": \"johnsmith@example.com\"</code> ã¯ã€johnsmith@example.com ãŒä½œæˆã—ãŸå…¨ã¦ã®èª²é¡Œã¨ã‚³ãƒ¡ãƒ³ãƒˆã« \"By <a href=\"#\">johnsmith@example.com</a>\" を追加ã—ã¾ã™ã€‚デフォルトã§ã€ãƒ¡ãƒ¼ãƒ«ã‚¢ãƒ‰ãƒ¬ã‚¹ã‚„ユーザーåã‚’éš ã—ã¦ãƒ¦ãƒ¼ã‚¶ãƒ¼ã®ãƒ—ライãƒã‚·ãƒ¼ã‚’ä¿è­·ã•ã‚Œã¾ã™ã€‚メールアドレスを全ã¦è¡¨ç¤ºã—ãŸã„å ´åˆã€ã“ã®æ–¹æ³•ã‚’指定ã—ã¦ãã ã•ã„。"
+msgid "<namespace / project>"
+msgstr ""
+
msgid "<no name set>"
msgstr "<no name set>"
msgid "<no scopes selected>"
msgstr "<スコープãŒé¸æŠžã•ã‚Œã¦ã„ã¾ã›ã‚“>"
+msgid "<project name>"
+msgstr ""
+
msgid "<strong>%{group_name}</strong> group members"
msgstr "<strong>%{group_name}</strong> グループã®ãƒ¡ãƒ³ãƒãƒ¼"
@@ -757,6 +777,9 @@ msgid "A Let's Encrypt account will be configured for this GitLab installation u
msgstr "Let's Encryptアカウントをã“ã®GitLabインストール用ã«æ§‹æˆã™ã‚‹ãŸã‚ã«ã‚ãªãŸã®ãƒ¡ãƒ¼ãƒ«ã‚¢ãƒ‰ãƒ¬ã‚¹ã‚’使用ã—ã¾ã™ã€‚証明書ã®æœŸé™ãŒåˆ‡ã‚ŒãŸéš›ã€è­¦å‘Šãƒ¡ãƒ¼ãƒ«ãŒå±Šãã¾ã™ã€‚"
msgid "A basic page and serverless function that uses AWS Lambda, AWS API Gateway, and GitLab Pages"
+msgstr "AWS Lambdaã€AWS API Gatewayã€ãŠã‚ˆã³GitLab Pagesを使用ã™ã‚‹åŸºæœ¬çš„ãªãƒšãƒ¼ã‚¸ã¨ã‚µãƒ¼ãƒãƒ¼ãƒ¬ã‚¹é–¢æ•°"
+
+msgid "A complete DevOps platform"
msgstr ""
msgid "A default branch cannot be chosen for an empty project."
@@ -772,7 +795,7 @@ msgid "A fork is a copy of a project.<br />Forking a repository allows you to ma
msgstr "フォークã¯ãƒ—ロジェクトã®ã‚³ãƒ”ーã§ã™ã€‚<br />リãƒã‚¸ãƒˆãƒªã‚’フォークã™ã‚‹ã¨ã€å…ƒã®ãƒ—ロジェクトã«å½±éŸ¿ã‚’与ãˆãšã«å¤‰æ›´ã™ã‚‹ã“ã¨ãŒã§ãã¾ã™ã€‚"
msgid "A group represents your organization in GitLab."
-msgstr ""
+msgstr "グループã¯GitLabã§ã®çµ„織を表ã—ã¾ã™ã€‚"
msgid "A member of the abuse team will review your report as soon as possible."
msgstr "ãŸã ã¡ã«ä¸æ­£åˆ©ç”¨å¯¾å¿œãƒãƒ¼ãƒ ãƒ¡ãƒ³ãƒãƒ¼ã§ã„ãŸã ã„ãŸãƒ¬ãƒãƒ¼ãƒˆã‚’æ‹èª­ã—å‚考ã«ã•ã›ã¦ã„ãŸã ãã¾ã™ã€‚"
@@ -790,7 +813,7 @@ msgid "A new Release %{tag} for %{name} was published. Visit the %{release_link_
msgstr "%{name} ã®æ–°ã—ã„リリース %{tag} ãŒå…¬é–‹ã•ã‚Œã¾ã—ãŸã€‚ 詳細ã«ã¤ã„ã¦ã¯ã€ %{release_link_start} リリースページ %{release_link_end} ã‚’ã”覧ãã ã•ã„。"
msgid "A new Release %{tag} for %{name} was published. Visit the Releases page to read more about it:"
-msgstr ""
+msgstr "%{name} ã®æ–°ã—ã„リリース %{tag} ãŒå…¬é–‹ã•ã‚Œã¾ã—ãŸã€‚ リリースページã«ã‚¢ã‚¯ã‚»ã‚¹ã—ã¦è©³ç´°ã‚’確èªã—ã¦ãã ã•ã„:"
msgid "A new branch will be created in your fork and a new merge request will be started."
msgstr "フォークã«æ–°ã—ã„ブランãƒãŒä½œæˆã•ã‚Œã€æ–°ã—ã„マージリクエストãŒé–‹å§‹ã—ã¾ã™ã€‚"
@@ -975,6 +998,12 @@ msgstr "é™çš„オブジェクトトークンã¯ã€ (アーカイブã€ãƒ–ロブã
msgid "AccessTokens|reset it"
msgstr "リセット"
+msgid "AccessibilityReport|Accessibility report artifact not found"
+msgstr ""
+
+msgid "AccessibilityReport|Failed to retrieve accessibility report"
+msgstr ""
+
msgid "AccessibilityReport|Learn More"
msgstr ""
@@ -984,6 +1013,9 @@ msgstr ""
msgid "AccessibilityReport|New"
msgstr ""
+msgid "AccessibilityReport|The accessibility scanning found an error of the following type: %{code}"
+msgstr ""
+
msgid "Account"
msgstr "アカウント"
@@ -1015,7 +1047,7 @@ msgid "Active Sessions"
msgstr "アクティブ セッション"
msgid "Active Users:"
-msgstr ""
+msgstr "アクティブユーザー"
msgid "Activity"
msgstr "アクティビティー"
@@ -1169,7 +1201,7 @@ msgid "Add request manually"
msgstr "リクエストを手動ã§è¿½åŠ ã—ã¾ã™"
msgid "Add system hook"
-msgstr ""
+msgstr "システムフックã®è¿½åŠ "
msgid "Add to Slack"
msgstr "Slackã«è¿½åŠ "
@@ -1199,7 +1231,7 @@ msgid "Add variable"
msgstr ""
msgid "Add webhook"
-msgstr ""
+msgstr "Webhook ã®è¿½åŠ "
msgid "AddMember|No users specified."
msgstr "ユーザーãŒæŒ‡å®šã•ã‚Œã¦ã„ã¾ã›ã‚“。"
@@ -1225,6 +1257,9 @@ msgstr "課題をエピックã«è¿½åŠ ã—ã¾ã—ãŸã€‚"
msgid "Added at"
msgstr "追加日時: "
+msgid "Added for this merge request"
+msgstr ""
+
msgid "Added in this version"
msgstr "ã“ã®ãƒãƒ¼ã‚¸ãƒ§ãƒ³ã®æ–°æ©Ÿèƒ½"
@@ -1274,7 +1309,7 @@ msgid "Admin mode disabled"
msgstr "管ç†ãƒ¢ãƒ¼ãƒ‰ç„¡åŠ¹"
msgid "Admin mode enabled"
-msgstr ""
+msgstr "管ç†ãƒ¢ãƒ¼ãƒ‰ã¯æœ‰åŠ¹ã§ã™"
msgid "Admin notes"
msgstr "管ç†è€…メモ"
@@ -1463,7 +1498,7 @@ msgid "AdminUsers|Cannot unblock LDAP blocked users"
msgstr "LDAP ã§ãƒ–ロックã•ã‚ŒãŸãƒ¦ãƒ¼ã‚¶ãƒ¼ã‚’ブロック解除ã§ãã¾ã›ã‚“"
msgid "AdminUsers|Deactivate"
-msgstr ""
+msgstr "無効ã«ã™ã‚‹"
msgid "AdminUsers|Deactivate User %{username}?"
msgstr "ユーザー %{username} ã‚’éžã‚¢ã‚¯ãƒ†ã‚£ãƒ–ã«ã—ã¾ã™ã‹?"
@@ -1472,7 +1507,7 @@ msgid "AdminUsers|Deactivate user"
msgstr ""
msgid "AdminUsers|Deactivated"
-msgstr ""
+msgstr "無効ã«ã—ã¾ã—ãŸ"
msgid "AdminUsers|Deactivating a user has the following effects:"
msgstr "ユーザーã®éžã‚¢ã‚¯ãƒ†ã‚£ãƒ–化ã«ã¯æ¬¡ã®åŠ¹æžœãŒã‚ã‚Šã¾ã™:"
@@ -1559,7 +1594,7 @@ msgid "AdminUsers|User will not be able to login"
msgstr "ユーザーã¯ãƒ­ã‚°ã‚¤ãƒ³ã§ããªããªã‚Šã¾ã™"
msgid "AdminUsers|When the user logs back in, their account will reactivate as a fully active account"
-msgstr ""
+msgstr "ã“ã®ãƒ¦ãƒ¼ã‚¶ãƒ¼ãŒå†ã³ãƒ­ã‚°ã‚¤ãƒ³ã™ã‚‹ã¨ã€ã‚¢ã‚«ã‚¦ãƒ³ãƒˆã¯æœ‰åŠ¹ã«ãªã‚Šã¾ã™"
msgid "AdminUsers|Without projects"
msgstr "プロジェクトãªã—"
@@ -1595,6 +1630,72 @@ msgid "Alert"
msgid_plural "Alerts"
msgstr[0] "アラート"
+msgid "Alert Details"
+msgstr ""
+
+msgid "AlertManagement|Acknowledged"
+msgstr ""
+
+msgid "AlertManagement|Alert"
+msgstr ""
+
+msgid "AlertManagement|Authorize external service"
+msgstr ""
+
+msgid "AlertManagement|Display alerts from all your monitoring tools directly within GitLab. Streamline the investigation of your alerts and the escalation of alerts to incidents."
+msgstr ""
+
+msgid "AlertManagement|End time"
+msgstr ""
+
+msgid "AlertManagement|End time:"
+msgstr ""
+
+msgid "AlertManagement|Events"
+msgstr ""
+
+msgid "AlertManagement|Events:"
+msgstr ""
+
+msgid "AlertManagement|Full Alert Details"
+msgstr ""
+
+msgid "AlertManagement|More information"
+msgstr ""
+
+msgid "AlertManagement|No alerts available to display. If you think you're seeing this message in error, refresh the page."
+msgstr ""
+
+msgid "AlertManagement|No alerts to display."
+msgstr ""
+
+msgid "AlertManagement|Overview"
+msgstr ""
+
+msgid "AlertManagement|Resolved"
+msgstr ""
+
+msgid "AlertManagement|Severity"
+msgstr ""
+
+msgid "AlertManagement|Start time"
+msgstr ""
+
+msgid "AlertManagement|Start time:"
+msgstr ""
+
+msgid "AlertManagement|Status"
+msgstr ""
+
+msgid "AlertManagement|Surface alerts in GitLab"
+msgstr ""
+
+msgid "AlertManagement|There was an error displaying the alerts. Confirm your endpoint's configuration details to ensure alerts appear."
+msgstr ""
+
+msgid "AlertManagement|Triggered"
+msgstr ""
+
msgid "AlertService|%{linkStart}Learn more%{linkEnd} about configuring this endpoint to receive alerts."
msgstr "アラートをå—ä¿¡ã™ã‚‹ãŸã‚ã®ã€ã“ã®ã‚¨ãƒ³ãƒ‰ãƒã‚¤ãƒ³ãƒˆã®è¨­å®šã® %{linkStart} 詳細をã¿ã‚‹ %{linkEnd}"
@@ -1656,7 +1757,7 @@ msgid "All merge request dependencies have been merged"
msgstr "ä¾å­˜ã™ã‚‹ã™ã¹ã¦ã®ãƒžãƒ¼ã‚¸ãƒªã‚¯ã‚¨ã‚¹ãƒˆãŒãƒžãƒ¼ã‚¸ã•ã‚Œã¾ã—ãŸ"
msgid "All paths are relative to the GitLab URL. Do not include %{relative_url_link_start}relative URL%{relative_url_link_end}."
-msgstr ""
+msgstr "ã™ã¹ã¦ã®ãƒ‘スã¯GitLab URL㮠相対パスã§ã™ã€‚ %{relative_url_link_start}相対URL%{relative_url_link_end}ã‚’å«ã‚ãªã„ã§ãã ã•ã„。"
msgid "All projects"
msgstr "ã™ã¹ã¦ã®ãƒ—ロジェクト"
@@ -1664,6 +1765,9 @@ msgstr "ã™ã¹ã¦ã®ãƒ—ロジェクト"
msgid "All security scans are enabled because %{linkStart}Auto DevOps%{linkEnd} is enabled on this project"
msgstr "ã“ã®ãƒ—ロジェクト㧠%{linkStart} Auto DevOps %{linkEnd} ãŒæœ‰åŠ¹ã«ãªã£ã¦ã„ã‚‹ãŸã‚ã€ã™ã¹ã¦ã®ã‚»ã‚­ãƒ¥ãƒªãƒ†ã‚£ã‚¹ã‚­ãƒ£ãƒ³ãŒæœ‰åŠ¹ã«ãªã£ã¦ã„ã¾ã™"
+msgid "All threads resolved"
+msgstr ""
+
msgid "All users"
msgstr "ã™ã¹ã¦ã®ãƒ¦ãƒ¼ã‚¶ãƒ¼"
@@ -1682,6 +1786,9 @@ msgstr "グループオーナー㌠LDAP 関連ã®è¨­å®šã‚’管ç†ã§ãるよã†
msgid "Allow only the selected protocols to be used for Git access."
msgstr "é¸æŠžã—ãŸãƒ—ロトコルã®ã¿ã‚’ Git アクセスã«ä½¿ç”¨ã§ãるよã†ã«ã—ã¾ã™ã€‚"
+msgid "Allow owners to manage default branch protection per group"
+msgstr ""
+
msgid "Allow owners to manually add users outside of LDAP"
msgstr ""
@@ -1790,6 +1897,9 @@ msgstr "æ–°ã—ã„ルールã®æ‰¿èªè€…ã®å–得中ã«ã‚¨ãƒ©ãƒ¼ãŒç™ºç”Ÿã—ã¾ã—
msgid "An error occurred fetching the dropdown data."
msgstr "ドロップダウンデータã®å–得中ã«ã‚¨ãƒ©ãƒ¼ãŒç™ºç”Ÿã—ã¾ã—ãŸã€‚"
+msgid "An error occurred fetching the project authors."
+msgstr ""
+
msgid "An error occurred previewing the blob"
msgstr "Blobã®ãƒ—レビュー中ã«ã‚¨ãƒ©ãƒ¼ãŒç™ºç”Ÿã—ã¾ã—ãŸ"
@@ -1812,7 +1922,7 @@ msgid "An error occurred while checking group path"
msgstr "グループã®ãƒ‘スã®ãƒã‚§ãƒƒã‚¯ä¸­ã«ã‚¨ãƒ©ãƒ¼ãŒç™ºç”Ÿã—ã¾ã—ãŸ"
msgid "An error occurred while committing your changes."
-msgstr ""
+msgstr "変更ã®ã‚³ãƒŸãƒƒãƒˆä¸­ã«ã‚¨ãƒ©ãƒ¼ãŒç™ºç”Ÿã—ã¾ã—ãŸã€‚"
msgid "An error occurred while decoding the file."
msgstr ""
@@ -1871,6 +1981,9 @@ msgstr "プロジェクトã®ã‚ªãƒ¼ãƒˆã‚³ãƒ³ãƒ—リートå–得中ã«ã‚¨ãƒ©ãƒ¼ãŒ
msgid "An error occurred while fetching sidebar data"
msgstr "サイドãƒãƒ¼ã®ãƒ‡ãƒ¼ã‚¿å–得中ã«ã‚¨ãƒ©ãƒ¼ãŒç™ºç”Ÿã—ã¾ã—ãŸã€‚"
+msgid "An error occurred while fetching terraform reports."
+msgstr ""
+
msgid "An error occurred while fetching the Service Desk address."
msgstr "サービスデスクアドレスã®å–得中ã«ã‚¨ãƒ©ãƒ¼ãŒç™ºç”Ÿã—ã¾ã—ãŸã€‚"
@@ -1953,7 +2066,7 @@ msgid "An error occurred while loading terraform report"
msgstr ""
msgid "An error occurred while loading the data. Please try again."
-msgstr ""
+msgstr "データ読ã¿è¾¼ã¿ä¸­ã«ã‚¨ãƒ©ãƒ¼ãŒç™ºç”Ÿã—ã¾ã—ãŸã€‚ã‚‚ã†ä¸€åº¦ã‚„ã‚Šç›´ã—ã¦ãã ã•ã„。"
msgid "An error occurred while loading the file"
msgstr "ファイルã®èª­ã¿è¾¼ã¿ä¸­ã«ã‚¨ãƒ©ãƒ¼ãŒç™ºç”Ÿã—ã¾ã—ãŸ"
@@ -2028,7 +2141,7 @@ msgid "An error occurred while subscribing to notifications."
msgstr "通知ã®è³¼èª­ä¸­ã«ã‚¨ãƒ©ãƒ¼ãŒç™ºç”Ÿã—ã¾ã—ãŸã€‚"
msgid "An error occurred while triggering the job."
-msgstr ""
+msgstr "ジョブã®ãƒˆãƒªã‚¬ãƒ¼ä¸­ã«ã‚¨ãƒ©ãƒ¼ãŒç™ºç”Ÿã—ã¾ã—ãŸã€‚"
msgid "An error occurred while trying to run a new pipeline for this Merge Request."
msgstr "ã“ã®ãƒžãƒ¼ã‚¸ãƒªã‚¯ã‚¨ã‚¹ãƒˆã®ãŸã‚ã«æ–°ã—ã„パイプラインを実行ã—よã†ã¨ã—ã¦ã„ã‚‹é–“ã«ã‚¨ãƒ©ãƒ¼ãŒç™ºç”Ÿã—ã¾ã—ãŸã€‚"
@@ -2108,6 +2221,9 @@ msgstr "スパム対策ã®æ¤œè¨¼"
msgid "Any"
msgstr "ä»»æ„ã®"
+msgid "Any Author"
+msgstr ""
+
msgid "Any Label"
msgstr "ä»»æ„ã®ãƒ©ãƒ™ãƒ«"
@@ -2124,7 +2240,7 @@ msgid "Any encrypted tokens"
msgstr "æš—å·åŒ–済ã¿ãƒˆãƒ¼ã‚¯ãƒ³"
msgid "Any member with Developer or higher permissions to the project."
-msgstr ""
+msgstr "ã“ã®ãƒ—ロジェクトã®é–‹ç™ºè€…ã¾ãŸã¯ãれ以上ã®æ¨©é™ã‚’æŒã¤ãƒ¡ãƒ³ãƒãƒ¼ã€‚"
msgid "Any namespace"
msgstr "ä»»æ„ã®ãƒãƒ¼ãƒ ã‚¹ãƒšãƒ¼ã‚¹"
@@ -2148,7 +2264,7 @@ msgid "Append the comment with %{shrug}"
msgstr "コメントを %{shrug} ã«è¿½åŠ "
msgid "Append the comment with %{tableflip}"
-msgstr ""
+msgstr "コメントを%{tableflip} ã«è¿½åŠ ã—ã¾ã—ãŸ"
msgid "Application"
msgstr "アプリケーション"
@@ -2187,19 +2303,19 @@ msgid "Apply a label"
msgstr "ラベルをé©ç”¨"
msgid "Apply a template"
-msgstr ""
+msgstr "テンプレートをé©ç”¨"
msgid "Apply suggestion"
msgstr "æ案をé©ç”¨"
msgid "Apply template"
-msgstr ""
+msgstr "テンプレートをé©ç”¨"
msgid "Apply this approval rule to any branch or a specific protected branch."
msgstr ""
msgid "Applying a template will replace the existing issue description. Any changes you have made will be lost."
-msgstr ""
+msgstr "テンプレートをé©ç”¨ã™ã‚‹ã¨ã€æ—¢å­˜ã®èª²é¡Œã®èª¬æ˜ŽãŒç½®ãæ›ãˆã‚‰ã‚Œã¾ã™ã€‚ è¡Œã£ãŸå¤‰æ›´ã¯ã™ã¹ã¦å¤±ã‚ã‚Œã¾ã™ã€‚"
msgid "Applying command"
msgstr "コマンドをé©ç”¨"
@@ -2266,13 +2382,13 @@ msgid "Approve a merge request"
msgstr "マージリクエストを承èªã™ã‚‹"
msgid "Approve the current merge request."
-msgstr ""
+msgstr "ç¾åœ¨ã®ãƒžãƒ¼ã‚¸ãƒªã‚¯ã‚¨ã‚¹ãƒˆã‚’承èªã™ã‚‹ã€‚"
msgid "Approved by: "
msgstr ""
msgid "Approved the current merge request."
-msgstr ""
+msgstr "ç¾åœ¨ã®ãƒžãƒ¼ã‚¸ãƒªã‚¯ã‚¨ã‚¹ãƒˆã¯æ‰¿èªæ¸ˆã§ã™ã€‚"
msgid "Approver"
msgstr ""
@@ -2299,7 +2415,7 @@ msgid "Archived"
msgstr ""
msgid "Archived project! Repository and other project resources are read only"
-msgstr ""
+msgstr "アーカイブã•ã‚ŒãŸãƒ—ロジェクトã§ã™ã€‚リãƒã‚¸ãƒ‰ãƒªãŠã‚ˆã³ãã®ä»–ã®ãƒ—ロジェクトリソースã¯èª­ã¿å–り専用ã§ã™ã€‚"
msgid "Archived project! Repository and other project resources are read-only"
msgstr "ã“ã®ãƒ—ロジェクトã¯ã‚¢ãƒ¼ã‚«ã‚¤ãƒ–ã•ã‚Œã¦ã„ã¾ã™ã€‚リãƒã‚¸ãƒˆãƒªãŠã‚ˆã³ãã®ä»–ã®ãƒ—ロジェクトリソースã¯èª­ã¿å–り専用ã§ã™"
@@ -2311,7 +2427,7 @@ msgid "Archiving the project will make it entirely read only. It is hidden from
msgstr ""
msgid "Are you setting up GitLab for a company?"
-msgstr ""
+msgstr "ä¼æ¥­å‘ã‘ã« GitLab をセットアップã—ã¦ã„ã¾ã™ã‹ï¼Ÿ"
msgid "Are you sure that you want to archive this project?"
msgstr "本当ã«ã“ã®ãƒ—ロジェクトをアーカイブã—ã¾ã™ã‹ï¼Ÿ"
@@ -2319,12 +2435,12 @@ msgstr "本当ã«ã“ã®ãƒ—ロジェクトをアーカイブã—ã¾ã™ã‹ï¼Ÿ"
msgid "Are you sure that you want to unarchive this project?"
msgstr "ã“ã®ãƒ—ロジェクトã®ã‚¢ãƒ¼ã‚«ã‚¤ãƒ–を解除ã—ã¦ã‚ˆã‚ã—ã„ã§ã™ã‹?"
-msgid "Are you sure you want to cancel creating this comment?"
-msgstr "ã“ã®ã‚³ãƒ¡ãƒ³ãƒˆã®ä½œæˆã‚’キャンセルã—ã¦ã‚‚よã‚ã—ã„ã§ã™ã‹ï¼Ÿ"
-
msgid "Are you sure you want to cancel editing this comment?"
msgstr "ã“ã®ã‚³ãƒ¡ãƒ³ãƒˆã®ç·¨é›†ã‚’キャンセルã—ã¦ã‚‚よã‚ã—ã„ã§ã™ã‹ï¼Ÿ"
+msgid "Are you sure you want to close this blocked issue?"
+msgstr ""
+
msgid "Are you sure you want to delete %{name}?"
msgstr ""
@@ -2352,6 +2468,9 @@ msgstr ""
msgid "Are you sure you want to deploy this environment?"
msgstr ""
+msgid "Are you sure you want to discard this comment?"
+msgstr ""
+
msgid "Are you sure you want to erase this build?"
msgstr "ã“ã®ãƒ“ルドを削除ã—ã¦ã‚‚よã‚ã—ã„ã§ã™ã‹ï¼Ÿ"
@@ -2467,7 +2586,7 @@ msgid "Assets"
msgstr "アセット"
msgid "Assets:"
-msgstr ""
+msgstr "アセット:"
msgid "Assign"
msgstr "割り当ã¦"
@@ -2527,9 +2646,12 @@ msgstr ""
msgid "At least one approval from a code owner is required to change files matching the respective CODEOWNER rules."
msgstr "ãã‚Œãžã‚Œã®ãƒ•ã‚¡ã‚¤ãƒ«ã‚’変更ã™ã‚‹ã«ã¯ã€CODEOWNERã®ãƒ«ãƒ¼ãƒ«ã«ä¸€è‡´ã™ã‚‹ã€å°‘ãªãã¨ã‚‚1åã®ã‚³ãƒ¼ãƒ‰æ‰€æœ‰è€…ã‹ã‚‰ã®æ‰¿èªãŒå¿…è¦ã§ã™ã€‚"
-msgid "At least one of group_id or project_id must be specified"
+msgid "At least one logging option is required to be enabled"
msgstr ""
+msgid "At least one of group_id or project_id must be specified"
+msgstr "group_id ã¾ãŸã¯ project_id ã‚’1ã¤ä»¥ä¸ŠæŒ‡å®šã™ã‚‹å¿…è¦ãŒã‚ã‚Šã¾ã™"
+
msgid "At risk"
msgstr ""
@@ -2553,15 +2675,36 @@ msgid "Audit Events is a way to keep track of important events that happened in
msgstr "監査イベントã¯ã€GitLab ã§ç™ºç”Ÿã—ãŸé‡è¦ãªã‚¤ãƒ™ãƒ³ãƒˆã‚’追跡ã—続ã‘る方法ã§ã™ã€‚"
msgid "AuditEvents|(removed)"
-msgstr ""
+msgstr "(削除済ã¿)"
msgid "AuditEvents|Action"
-msgstr ""
+msgstr "アクション"
msgid "AuditEvents|At"
msgstr ""
msgid "AuditEvents|Target"
+msgstr "ターゲット"
+
+msgid "AuditLogs|(removed)"
+msgstr ""
+
+msgid "AuditLogs|Action"
+msgstr ""
+
+msgid "AuditLogs|Author"
+msgstr ""
+
+msgid "AuditLogs|Date"
+msgstr ""
+
+msgid "AuditLogs|IP Address"
+msgstr ""
+
+msgid "AuditLogs|Object"
+msgstr ""
+
+msgid "AuditLogs|Target"
msgstr ""
msgid "Aug"
@@ -2646,7 +2789,7 @@ msgid "Auto License Compliance"
msgstr "自動ライセンスコンプライアンス"
msgid "Auto stop successfully canceled."
-msgstr ""
+msgstr "自動åœæ­¢æ©Ÿèƒ½ã‚’正常ã«ã‚­ãƒ£ãƒ³ã‚»ãƒ«ã—ã¾ã—ãŸã€‚"
msgid "Auto-cancel redundant, pending pipelines"
msgstr "冗長・ä¿ç•™ä¸­ã®ãƒ‘イプラインを自動キャンセル"
@@ -2775,7 +2918,7 @@ msgid "Badges|Link"
msgstr "リンク"
msgid "Badges|Name"
-msgstr ""
+msgstr "åå‰"
msgid "Badges|No badge image"
msgstr "ãƒãƒƒã‚¸ç”»åƒãªã—"
@@ -2846,9 +2989,6 @@ msgstr "Bamboo ã®ãƒ«ãƒ¼ãƒˆURL 例: https://bamboo.example.com"
msgid "BambooService|You must set up automatic revision labeling and a repository trigger in Bamboo."
msgstr "Bambooã§è‡ªå‹•ãƒªãƒ“ジョンラベリングã¨ãƒªãƒã‚¸ãƒˆãƒªãƒˆãƒªã‚¬ãƒ¼ã‚’設定ã—ãªã‘ã‚Œã°ãªã‚Šã¾ã›ã‚“。"
-msgid "Batch operations"
-msgstr "ãƒãƒƒãƒå‡¦ç†"
-
msgid "BatchComments|Delete all pending comments"
msgstr "ä¿ç•™ä¸­ã®ã‚³ãƒ¡ãƒ³ãƒˆã‚’ã™ã¹ã¦å‰Šé™¤"
@@ -2880,10 +3020,10 @@ msgid "Billing"
msgstr "請求"
msgid "BillingPlans|%{group_name} is currently using the %{plan_name} plan."
-msgstr ""
+msgstr "%{group_name} ã®ç¾åœ¨ã®ãƒ—ラン㯠%{plan_name} ã§ã™ã€‚"
msgid "BillingPlans|@%{user_name} you are currently using the %{plan_name} plan."
-msgstr ""
+msgstr "@%{user_name} ã‚ãªãŸã®ç¾åœ¨ã®ãƒ—ランã¯%{plan_name} ã§ã™ã€‚"
msgid "BillingPlans|Congratulations, your new trial is activated"
msgstr "ãŠã‚ã§ã¨ã†ã”ã–ã„ã¾ã™ã€æ–°ã—ã„試用版ãŒæœ‰åŠ¹ã«ãªã‚Šã¾ã—ãŸ"
@@ -2913,10 +3053,10 @@ msgid "BillingPlans|To manage the plan for this group, visit the billing section
msgstr "ã“ã®ã‚°ãƒ«ãƒ¼ãƒ—ã®ãƒ—ランã®ç®¡ç†ã¯ã€%{parent_billing_page_link} ã®è«‹æ±‚ã®ã‚»ã‚¯ã‚·ãƒ§ãƒ³ã‚’ã”覧ãã ã•ã„。"
msgid "BillingPlans|Your GitLab.com %{plan} trial will <strong>expire after %{expiration_date}</strong>. You can retain access to the %{plan} features by upgrading below."
-msgstr ""
+msgstr "GitLab.com ã® %{plan} ã®è©¦ç”¨ç‰ˆã¯ã€<strong>%{expiration_date}後ã«æœ‰åŠ¹æœŸé™ãŒåˆ‡ã‚Œã¾ã™</strong>。 以下をアップグレードã—ã¦ã€%{plan} ã®æ©Ÿèƒ½ã¸ã®ã‚¢ã‚¯ã‚»ã‚¹ã‚’ãã®ã¾ã¾ã«ã§ãã¾ã™ã€‚"
msgid "BillingPlans|Your GitLab.com trial expired on %{expiration_date}. You can restore access to the features at any time by upgrading below."
-msgstr ""
+msgstr "GitLab.com ã®è©¦ç”¨ç‰ˆã¯ %{expiration_date} ã«æœŸé™åˆ‡ã‚Œã«ãªã‚Šã¾ã—ãŸã€‚ 以下ã®ã‚¢ãƒƒãƒ—グレードã«ã‚ˆã‚Šã€ã„ã¤ã§ã‚‚ゴールド機能ã¸ã®ã‚¢ã‚¯ã‚»ã‚¹ã‚’復元ã§ãã¾ã™ã€‚"
msgid "BillingPlans|billed annually at %{price_per_year}"
msgstr ""
@@ -2955,10 +3095,10 @@ msgid "Blue helpers indicate an action to be taken."
msgstr "é’ã„ヘルパーã¯å–ã‚‹ã¹ã行動を示ã—ã¾ã™ã€‚"
msgid "Board name"
-msgstr ""
+msgstr "ボードå"
msgid "Board scope"
-msgstr ""
+msgstr "ボードスコープ"
msgid "Board scope affects which issues are displayed for anyone who visits this board"
msgstr ""
@@ -2978,11 +3118,14 @@ msgstr "デフォルトã®ãƒªã‚¹ãƒˆã‚»ãƒƒãƒˆã‹ã‚‰å§‹ã‚ã‚‹ã¨ã€ãƒœãƒ¼ãƒ‰ã‚’最
msgid "Boards"
msgstr "ボード"
+msgid "Boards and Board Lists"
+msgstr ""
+
msgid "Boards|Collapse"
msgstr ""
msgid "Boards|Edit board"
-msgstr ""
+msgstr "ボードを編集"
msgid "Boards|Expand"
msgstr ""
@@ -3012,7 +3155,7 @@ msgid "Branch not loaded - %{branchId}"
msgstr "%{branchId} ブランãƒã¯ãƒ­ãƒ¼ãƒ‰ã•ã‚Œã¦ã„ã¾ã›ã‚“。"
msgid "Branch prefix"
-msgstr ""
+msgstr "ブランãƒãƒ—レフィックス"
msgid "BranchSwitcherPlaceholder|Search branches"
msgstr "ブランãƒã‚’検索"
@@ -3180,7 +3323,7 @@ msgid "Burndown chart"
msgstr "ãƒãƒ¼ãƒ³ãƒ€ã‚¦ãƒ³ãƒãƒ£ãƒ¼ãƒˆ"
msgid "BurndownChartLabel|Open issue weight"
-msgstr ""
+msgstr "課題ã®é‡ã¿ã‚’é–‹ã"
msgid "BurndownChartLabel|Open issues"
msgstr "未解決ã®èª²é¡Œ"
@@ -3197,6 +3340,9 @@ msgstr "EE版を購入"
msgid "Buy GitLab Enterprise Edition"
msgstr "GitLabエンタープライズエディションを購入"
+msgid "Buy more Pipeline minutes"
+msgstr ""
+
msgid "By %{user_name}"
msgstr ""
@@ -3204,7 +3350,7 @@ msgid "By default GitLab sends emails in HTML and plain text formats so mail cli
msgstr "デフォルトã§ã¯ã€GitLabã¯HTMLå½¢å¼ã¨ãƒ—レーンテキスト形å¼ã®ä¸¡ãƒ•ã‚©ãƒ¼ãƒžãƒƒãƒˆã®ãƒ¡ãƒ¼ãƒ«ã‚’é€ä¿¡ã—ã¾ã™ã€‚ãã®ãŸã‚ã€ãƒ¡ãƒ¼ãƒ«ã‚¯ãƒ©ã‚¤ã‚¢ãƒ³ãƒˆã¯ã©ã¡ã‚‰ã®ãƒ•ã‚©ãƒ¼ãƒžãƒƒãƒˆã‚’表示ã™ã‚‹ã‹ã‚’é¸ã¹ã¾ã™ã€‚ã‚‚ã—プレーンテキスト形å¼ã ã‘ã§ãƒ¡ãƒ¼ãƒ«ã‚’é€ä¿¡ã—ãŸã„å ´åˆã¯ã€ã“ã®ã‚ªãƒ—ションを無効ã«ã—ã¾ã™"
msgid "By default, all projects and groups will use the global notifications setting."
-msgstr ""
+msgstr "デフォルトã§ã¯ã€ã™ã¹ã¦ã®ãƒ—ロジェクトã¨ã‚°ãƒ«ãƒ¼ãƒ—ã¯ã‚°ãƒ­ãƒ¼ãƒãƒ«é€šçŸ¥è¨­å®šã‚’使用ã—ã¾ã™ã€‚"
msgid "ByAuthor|by"
msgstr "作者"
@@ -3276,7 +3422,7 @@ msgid "CICD|You must add a %{kubernetes_cluster_link_start}Kubernetes cluster in
msgstr ""
msgid "CICD|group enabled"
-msgstr ""
+msgstr "グループã¯æœ‰åŠ¹ã§ã™"
msgid "CICD|instance enabled"
msgstr "インスタンスãŒæœ‰åŠ¹"
@@ -3317,6 +3463,9 @@ msgstr "グループ管ç†ã‚¢ã‚«ã‚¦ãƒ³ãƒˆãŒãªã„ã¨ã‚°ãƒ«ãƒ¼ãƒ—メンãƒãƒ¼ã¯
msgid "Can't scan the code?"
msgstr ""
+msgid "Can't update snippet: %{err}"
+msgstr ""
+
msgid "Canary"
msgstr "カナリア"
@@ -3327,7 +3476,7 @@ msgid "Cancel"
msgstr "キャンセル"
msgid "Cancel running"
-msgstr ""
+msgstr "実行ã®ã‚­ãƒ£ãƒ³ã‚»ãƒ«"
msgid "Cancel this job"
msgstr "ã“ã®ã‚¸ãƒ§ãƒ–をキャンセルã™ã‚‹"
@@ -3438,7 +3587,7 @@ msgid "Change your password"
msgstr "パスワードを変更ã—ã¦ãã ã•ã„"
msgid "Change your password or recover your current one"
-msgstr ""
+msgstr "パスワードを変更ã™ã‚‹ã‹ã€ç¾åœ¨ã®ãƒ‘スワードを入力ã—ã¦ãã ã•ã„"
msgid "ChangeTypeActionLabel|Pick into branch"
msgstr "ピック先ブランãƒ:"
@@ -3483,6 +3632,9 @@ msgid "Changing a Release tag is only supported via Releases API. %{linkStart}Mo
msgstr ""
msgid "Changing group path can have unintended side effects."
+msgstr "グループã®ãƒ‘スを変更ã™ã‚‹ã¨ã€æ„図ã—ãªã„副作用ãŒç™ºç”Ÿã™ã‚‹å¯èƒ½æ€§ãŒã‚ã‚Šã¾ã™ã€‚"
+
+msgid "Charts"
msgstr ""
msgid "Charts can't be displayed as the request for data has timed out. %{documentationLink}"
@@ -3521,9 +3673,6 @@ msgstr "ã‚¿ã‚°"
msgid "ChatMessage|and [%{count} more](%{pipeline_failed_jobs_url})"
msgstr ""
-msgid "ChatMessage|failed"
-msgstr "失敗"
-
msgid "ChatMessage|has failed"
msgstr "ã¯å¤±æ•—ã—ã¾ã—ãŸ"
@@ -3534,13 +3683,10 @@ msgid "ChatMessage|has passed with warnings"
msgstr ""
msgid "ChatMessage|in %{duration}"
-msgstr ""
+msgstr "%{duration} 中"
msgid "ChatMessage|in %{project_link}"
-msgstr ""
-
-msgid "ChatMessage|passed"
-msgstr ""
+msgstr "%{project_link} 中"
msgid "Check again"
msgstr "å†ãƒã‚§ãƒƒã‚¯"
@@ -3567,7 +3713,7 @@ msgid "Checking branch availability..."
msgstr "ブランãƒãŒåˆ©ç”¨å¯èƒ½ã‹ç¢ºèªã—ã¦ã„ã¾ã™â€¦"
msgid "Checking group path availability..."
-msgstr ""
+msgstr "グループパスãŒåˆ©ç”¨å¯èƒ½ã‹ç¢ºèªã—ã¦ã„ã¾ã™..."
msgid "Checking username availability..."
msgstr "ユーザーåãŒåˆ©ç”¨å¯èƒ½ã‹ç¢ºèªã—ã¦ã„ã¾ã™..."
@@ -3594,10 +3740,10 @@ msgid "Checkout|(x%{numberOfUsers})"
msgstr ""
msgid "Checkout|1. Your profile"
-msgstr ""
+msgstr "1. ã‚ãªãŸã®ãƒ—ロフィール"
msgid "Checkout|2. Checkout"
-msgstr ""
+msgstr "2. ãƒã‚§ãƒƒã‚¯ã‚¢ã‚¦ãƒˆ"
msgid "Checkout|3. Your GitLab group"
msgstr ""
@@ -3606,7 +3752,7 @@ msgid "Checkout|Billing address"
msgstr ""
msgid "Checkout|Checkout"
-msgstr ""
+msgstr "ãƒã‚§ãƒƒã‚¯ã‚¢ã‚¦ãƒˆ"
msgid "Checkout|City"
msgstr ""
@@ -3735,7 +3881,7 @@ msgid "Child"
msgstr ""
msgid "Child epic does not exist."
-msgstr ""
+msgstr "å­ã‚¨ãƒ”ックã¯å­˜åœ¨ã—ã¾ã›ã‚“。"
msgid "Child epic doesn't exist."
msgstr ""
@@ -3756,7 +3902,7 @@ msgid "Choose a group"
msgstr "グループをé¸æŠž"
msgid "Choose a role permission"
-msgstr ""
+msgstr "役割(権é™)ã‚’é¸æŠžã—ã¦ãã ã•ã„"
msgid "Choose a template"
msgstr "テンプレートをé¸æŠžã—ã¦ãã ã•ã„"
@@ -3963,7 +4109,7 @@ msgid "Clear"
msgstr "クリア"
msgid "Clear chart filters"
-msgstr ""
+msgstr "ãƒãƒ£ãƒ¼ãƒˆãƒ•ã‚£ãƒ«ã‚¿ãƒ¼ã‚’解除"
msgid "Clear input"
msgstr "入力をクリア"
@@ -3992,9 +4138,6 @@ msgstr "ウェイトをクリアã—ã¾ã™ã€‚"
msgid "Click any <strong>project name</strong> in the project list below to navigate to the project milestone."
msgstr "プロジェクトリストã§<strong>プロジェクトå</strong>をクリックã™ã‚‹ã¨ã€ãƒ—ロジェクトã®ãƒžã‚¤ãƒ«ã‚¹ãƒˆãƒ¼ãƒ³ã«ç§»å‹•ã—ã¾ã™ã€‚"
-msgid "Click here"
-msgstr "ã“ã“をクリックã—ã¦ä¸‹ã•ã„"
-
msgid "Click the <strong>Download</strong> button and wait for downloading to complete."
msgstr "<strong>ダウンロード</strong> ボタンをクリックã—ã€ãƒ€ã‚¦ãƒ³ãƒ­ãƒ¼ãƒ‰ã®å®Œäº†ã‚’ãŠå¾…ã¡ãã ã•ã„。"
@@ -4329,7 +4472,7 @@ msgid "ClusterIntegration|Did you know?"
msgstr "ã”存知ã§ã™ã‹ï¼Ÿ"
msgid "ClusterIntegration|Elastic Kubernetes Service"
-msgstr ""
+msgstr "Elastic Kubernetes サービス"
msgid "ClusterIntegration|Elastic Stack"
msgstr ""
@@ -4347,7 +4490,7 @@ msgid "ClusterIntegration|Enabled stack"
msgstr ""
msgid "ClusterIntegration|Enter the details for your Amazon EKS Kubernetes cluster"
-msgstr ""
+msgstr "Amazon EKS Kubernetes クラスタã®è©³ç´°ã‚’入力ã—ã¦ãã ã•ã„"
msgid "ClusterIntegration|Enter the details for your Kubernetes cluster"
msgstr "Kubernetes クラスターã®è©³ç´°ã‚’入力ã—ã¦ãã ã•ã„"
@@ -4382,6 +4525,12 @@ msgstr "プロジェクトをå–得中"
msgid "ClusterIntegration|Fetching zones"
msgstr "ゾーンをå–得中"
+msgid "ClusterIntegration|Fluentd"
+msgstr ""
+
+msgid "ClusterIntegration|Fluentd is an open source data collector, which lets you unify the data collection and consumption for a better use and understanding of data. It requires at least one of the following logs to be successfully installed."
+msgstr ""
+
msgid "ClusterIntegration|GitLab Integration"
msgstr "GitLabã‚’çµ±åˆ"
@@ -4467,10 +4616,10 @@ msgid "ClusterIntegration|JupyterHub"
msgstr "JupyterHub"
msgid "ClusterIntegration|JupyterHub, a multi-user Hub, spawns, manages, and proxies multiple instances of the single-user Jupyter notebook server. JupyterHub can be used to serve notebooks to a class of students, a corporate data science group, or a scientific research group."
-msgstr ""
+msgstr "マルãƒãƒ¦ãƒ¼ã‚¶ãƒ¼ãƒãƒ–JupyterHubã¯ã€ã‚·ãƒ³ã‚°ãƒ«ãƒ¦ãƒ¼ã‚¶ãƒ¼Jupyterノートブックサーãƒãƒ¼ã®è¤‡æ•°ã®ã‚¤ãƒ³ã‚¹ã‚¿ãƒ³ã‚¹ã‚’生æˆã€ç®¡ç†ã€ãŠã‚ˆã³ãƒ—ロキシã—ã¾ã™ã€‚ JupyterHubã¯ã€å­¦ç”Ÿã®ã‚¯ãƒ©ã‚¹ã€ä¼æ¥­ãƒ‡ãƒ¼ã‚¿ç§‘学グループã€ã¾ãŸã¯ç§‘学研究グループã«ãƒŽãƒ¼ãƒˆãƒ–ックをæä¾›ã™ã‚‹ãŸã‚ã«ä½¿ç”¨ã§ãã¾ã™ã€‚"
msgid "ClusterIntegration|Key pair name"
-msgstr ""
+msgstr "キーペアå"
msgid "ClusterIntegration|Knative"
msgstr "Knative"
@@ -4485,7 +4634,7 @@ msgid "ClusterIntegration|Knative domain name was updated successfully."
msgstr "Knativeドメインåを正常ã«æ›´æ–°ã—ã¾ã—ãŸã€‚"
msgid "ClusterIntegration|Knative extends Kubernetes to provide a set of middleware components that are essential to build modern, source-centric, and container-based applications that can run anywhere: on premises, in the cloud, or even in a third-party data center."
-msgstr ""
+msgstr "Knativeã¯Kubernetesã‚’æ‹¡å¼µã—ã¦ã€ã‚ªãƒ³ãƒ—レミスã€ã‚¯ãƒ©ã‚¦ãƒ‰ã€ã•ã‚‰ã«ã¯ã‚µãƒ¼ãƒ‰ãƒ‘ーティã®ãƒ‡ãƒ¼ã‚¿ã‚»ãƒ³ã‚¿ãƒ¼ãªã©ã€ã©ã“ã§ã‚‚実行å¯èƒ½ãªæœ€æ–°ã®ã‚½ãƒ¼ã‚¹ä¸­å¿ƒã®ã‚³ãƒ³ãƒ†ãƒŠãƒ™ãƒ¼ã‚¹ã®ã‚¢ãƒ—リケーションを構築ã™ã‚‹ãŸã‚ã«ä¸å¯æ¬ ãªãƒŸãƒ‰ãƒ«ã‚¦ã‚§ã‚¢ã‚³ãƒ³ãƒãƒ¼ãƒãƒ³ãƒˆã®ã‚»ãƒƒãƒˆã‚’æä¾›ã—ã¾ã™ã€‚"
msgid "ClusterIntegration|Kubernetes cluster"
msgstr "Kubernetes クラスター"
@@ -4509,7 +4658,7 @@ msgid "ClusterIntegration|Kubernetes version"
msgstr "Kubernetesãƒãƒ¼ã‚¸ãƒ§ãƒ³"
msgid "ClusterIntegration|Kubernetes version not found"
-msgstr ""
+msgstr "Kubernetes ãƒãƒ¼ã‚¸ãƒ§ãƒ³ãŒè¦‹ã¤ã‹ã‚Šã¾ã›ã‚“"
msgid "ClusterIntegration|Learn more about %{help_link_start_machine_type}machine types%{help_link_end} and %{help_link_start_pricing}pricing%{help_link_end}."
msgstr "%{help_link_start_machine_type}マシンタイプ%{help_link_end}ã¨%{help_link_start_pricing}価格%{help_link_end}ã®è©³ç´°ã€‚"
@@ -4533,7 +4682,7 @@ msgid "ClusterIntegration|Let's Encrypt"
msgstr "Let's Encrypt"
msgid "ClusterIntegration|Loading IAM Roles"
-msgstr ""
+msgstr "IAMロールã®ãƒ­ãƒ¼ãƒ‰"
msgid "ClusterIntegration|Loading Key Pairs"
msgstr "キーペアをロード"
@@ -4623,7 +4772,7 @@ msgid "ClusterIntegration|Please make sure that your Google account meets the fo
msgstr "Google アカウントãŒæ¬¡ã®è¦ä»¶ã‚’満ãŸã—ã¦ã„ã‚‹ã“ã¨ã‚’確èªã—ã¦ãã ã•ã„。"
msgid "ClusterIntegration|Point a wildcard DNS to this generated endpoint in order to access your application after it has been deployed."
-msgstr ""
+msgstr "デプロイ完了後ã«ã‚ãªãŸã®ã‚¢ãƒ—リケーションã«ã‚¢ã‚¯ã‚»ã‚¹ã™ã‚‹ç‚ºã«ã€ç”Ÿæˆã•ã‚ŒãŸã‚¨ãƒ³ãƒ‰ãƒã‚¤ãƒ³ãƒˆã‚’ワイルドカードDNSã«æŒ‡å®šã—ã¾ã™ã€‚"
msgid "ClusterIntegration|Project cluster"
msgstr "プロジェクトクラスター"
@@ -4650,7 +4799,7 @@ msgid "ClusterIntegration|RBAC-enabled cluster"
msgstr "RBAC 有効クラスター"
msgid "ClusterIntegration|Read our %{link_start}help page%{link_end} on Kubernetes cluster integration."
-msgstr ""
+msgstr "çµ±åˆKubernetesクラスターã«ã¤ã„ã¦ã¯ã€%{link_start}ヘルプページ%{link_end}ã‚’ã”覧ãã ã•ã„。"
msgid "ClusterIntegration|Real-time web application monitoring, logging and access control. %{linkStart}More information%{linkEnd}"
msgstr ""
@@ -4688,6 +4837,15 @@ msgstr "インストール開始ã«å¤±æ•—ã—ã¾ã—ãŸ"
msgid "ClusterIntegration|Request to begin uninstalling failed"
msgstr "アンインストールã®é–‹å§‹è¦æ±‚ã«å¤±æ•—ã—ã¾ã—ãŸ"
+msgid "ClusterIntegration|SIEM Hostname"
+msgstr ""
+
+msgid "ClusterIntegration|SIEM Port"
+msgstr ""
+
+msgid "ClusterIntegration|SIEM Protocol"
+msgstr ""
+
msgid "ClusterIntegration|Save changes"
msgstr "変更をä¿å­˜"
@@ -4784,6 +4942,12 @@ msgstr "ゾーンをé¸æŠž"
msgid "ClusterIntegration|Select zone to choose machine type"
msgstr "ゾーンをé¸æŠžã—ã¦ãƒžã‚·ãƒ³ã‚¿ã‚¤ãƒ—ã‚’é¸æŠž"
+msgid "ClusterIntegration|Send Cilium Logs"
+msgstr ""
+
+msgid "ClusterIntegration|Send ModSecurity Logs"
+msgstr ""
+
msgid "ClusterIntegration|Service Token"
msgstr "サービストークン"
@@ -4839,10 +5003,10 @@ msgid "ClusterIntegration|The associated IP and all deployed services will be de
msgstr ""
msgid "ClusterIntegration|The associated Tiller pod, the %{gitlabManagedAppsNamespace} namespace, and all of its resources will be deleted and cannot be restored."
-msgstr ""
+msgstr "関連付ã‘られã¦ã„ã‚‹Tillerãƒãƒƒãƒ‰ã€ %{gitlabManagedAppsNamespace} åå‰ç©ºé–“ã€ãŠã‚ˆã³ãã®ã™ã¹ã¦ã®ãƒªã‚½ãƒ¼ã‚¹ã¯å‰Šé™¤ã•ã‚Œã¾ã™ã€‚復元ã¯ã§ãã¾ã›ã‚“。"
msgid "ClusterIntegration|The associated load balancer and IP will be deleted and cannot be restored."
-msgstr ""
+msgstr "関連ã™ã‚‹ãƒ­ãƒ¼ãƒ‰ãƒãƒ©ãƒ³ã‚µãƒ¼ã¨IPã¯å‰Šé™¤ã•ã‚Œã€å¾©å…ƒã§ãã¾ã›ã‚“。"
msgid "ClusterIntegration|The associated private key will be deleted and cannot be restored."
msgstr ""
@@ -4851,13 +5015,13 @@ msgid "ClusterIntegration|The elastic stack collects logs from all pods in your
msgstr ""
msgid "ClusterIntegration|The endpoint is in the process of being assigned. Please check your Kubernetes cluster or Quotas on Google Kubernetes Engine if it takes a long time."
-msgstr ""
+msgstr "ã“ã®ã‚¨ãƒ³ãƒ‰ãƒã‚¤ãƒ³ãƒˆã¯å‰²ã‚Šå½“ã¦ãƒ—ロセス実施中ã§ã™ã€‚ã‚‚ã—処ç†æ™‚é–“ãŒé•·ã„å ´åˆã€ã‚ãªãŸã®Kubernetesクラスタã¾ãŸã¯Google Kubernetes Engineã®ã‚¯ã‚©ãƒ¼ã‚¿ã‚’確èªã—ã¦ãã ã•ã„。"
msgid "ClusterIntegration|The namespace associated with your project. This will be used for deploy boards, logs, and Web terminals."
msgstr ""
msgid "ClusterIntegration|There was a problem authenticating with your cluster. Please ensure your CA Certificate and Token are valid."
-msgstr ""
+msgstr "クラスターã¨ã®èªè¨¼ã«å•é¡ŒãŒã‚ã‚Šã¾ã—ãŸã€‚ CA証明書ã¨ãƒˆãƒ¼ã‚¯ãƒ³ãŒæœ‰åŠ¹ã§ã‚ã‚‹ã“ã¨ã‚’確èªã—ã¦ãã ã•ã„。"
msgid "ClusterIntegration|This account must have permissions to create a Kubernetes cluster in the %{link_to_container_project} specified below"
msgstr "ã“ã®ã‚¢ã‚«ã‚¦ãƒ³ãƒˆã¯ %{link_to_container_project} 㧠Kubernetes クラスターを作æˆã™ã‚‹ã®ã«ä»¥ä¸‹ã®æ¨©é™ãŒå¿…è¦ã§ã™"
@@ -4866,7 +5030,7 @@ msgid "ClusterIntegration|This option will allow you to install applications on
msgstr "ã“ã®ã‚ªãƒ—ションを使用ã™ã‚‹ã¨ã€ã‚ãªãŸã¯ã‚¢ãƒ—リケーションを RBAC クラスター上ã«ã‚¤ãƒ³ã‚¹ãƒˆãƒ¼ãƒ«ã§ãã¾ã™ã€‚"
msgid "ClusterIntegration|To access your application after deployment, point a wildcard DNS to the Knative Endpoint."
-msgstr ""
+msgstr "デプロイ後ã«ã‚¢ãƒ—リケーションã«ã‚¢ã‚¯ã‚»ã‚¹ã™ã‚‹ã«ã¯ã€ãƒ¯ã‚¤ãƒ«ãƒ‰ã‚«ãƒ¼ãƒ‰ DNS ã‚’ Knative エンドãƒã‚¤ãƒ³ãƒˆã«æŒ‡å®šã—ã¾ã™ã€‚"
msgid "ClusterIntegration|To remove your integration and resources, type %{clusterName} to confirm:"
msgstr ""
@@ -4878,7 +5042,7 @@ msgid "ClusterIntegration|Toggle Kubernetes cluster"
msgstr "Kubernetes クラスターを切り替ãˆ"
msgid "ClusterIntegration|Uninstall %{appTitle}"
-msgstr ""
+msgstr "%{appTitle} をアンインストール"
msgid "ClusterIntegration|Update failed. Please check the logs and try again."
msgstr "æ›´æ–°ã«å¤±æ•—ã—ã¾ã—ãŸã€‚ログを確èªã—ã¦ã‚‚ã†ä¸€åº¦è©¦ã—ã¦ãã ã•ã„。"
@@ -4971,7 +5135,7 @@ msgid "ClusterIntergation|Select a subnetwork"
msgstr ""
msgid "ClusterIntergation|Select an instance type"
-msgstr ""
+msgstr "インスタンスタイプをé¸æŠž"
msgid "ClusterIntergation|Select key pair"
msgstr "キーペアをé¸æŠž"
@@ -5010,19 +5174,19 @@ msgid "Cohorts"
msgstr "コホート"
msgid "Cohorts|Inactive users"
-msgstr ""
+msgstr "アクティブã§ã¯ãªã„ユーザー"
msgid "Cohorts|Month %{month_index}"
-msgstr ""
+msgstr "%{month_index} 月 "
msgid "Cohorts|New users"
-msgstr ""
+msgstr "æ–°è¦ãƒ¦ãƒ¼ã‚¶ãƒ¼"
msgid "Cohorts|Registration month"
-msgstr ""
+msgstr "登録月"
msgid "Cohorts|Returning users"
-msgstr ""
+msgstr "継続利用ユーザー"
msgid "Cohorts|User cohorts are shown for the last %{months_included} months. Only users with activity are counted in the 'New users' column; inactive users are counted separately."
msgstr ""
@@ -5067,10 +5231,10 @@ msgid "Comment & reopen %{noteable_name}"
msgstr "コメントã—ã¦å†é–‹ %{noteable_name}"
msgid "Comment & resolve thread"
-msgstr ""
+msgstr "コメントã—ã¦ã€ã‚¹ãƒ¬ãƒƒãƒ‰ã‚’解決ã™ã‚‹"
msgid "Comment & unresolve thread"
-msgstr ""
+msgstr "コメントã—ã¦ã€ã‚¹ãƒ¬ãƒƒãƒ‰ã‚’未解決ã«ã™ã‚‹"
msgid "Comment '%{label}' position"
msgstr ""
@@ -5082,7 +5246,7 @@ msgid "Comment is being updated"
msgstr "コメントãŒæ›´æ–°ã•ã‚Œã¦ã„ã¾ã™"
msgid "Comment/Reply (quoting selected text)"
-msgstr ""
+msgstr "コメント/返信 (é¸æŠžã—ãŸãƒ†ã‚­ã‚¹ãƒˆã‚’引用ã—ã¾ã™)"
msgid "Comments"
msgstr "コメント"
@@ -5095,7 +5259,7 @@ msgid "Commit %{commit_id}"
msgstr "コミット %{commit_id}"
msgid "Commit (when editing commit message)"
-msgstr ""
+msgstr "コミット(コミットメッセージã®ç·¨é›†æ™‚)"
msgid "Commit Message"
msgstr "コミットメッセージ"
@@ -5161,7 +5325,7 @@ msgid "Company"
msgstr "会社"
msgid "Company name"
-msgstr ""
+msgstr "会社å"
msgid "Compare"
msgstr "比較"
@@ -5182,7 +5346,7 @@ msgid "Compare changes with the merge request target branch"
msgstr "マージリクエストã®ã‚¿ãƒ¼ã‚²ãƒƒãƒˆãƒ–ランãƒã¨ã®å¤‰æ›´ã‚’比較ã™ã‚‹"
msgid "Compare with previous version"
-msgstr ""
+msgstr "以å‰ã®ãƒãƒ¼ã‚¸ãƒ§ãƒ³ã¨æ¯”較"
msgid "CompareBranches|%{source_branch} and %{target_branch} are the same."
msgstr "%{source_branch} 㨠%{target_branch} ã¯ä¸€è‡´ã—ã¾ã™ã€‚"
@@ -5211,18 +5375,33 @@ msgstr ""
msgid "Compliance framework (optional)"
msgstr ""
+msgid "ComplianceFramework|GDPR"
+msgstr ""
+
msgid "ComplianceFramework|GDPR - General Data Protection Regulation"
msgstr ""
+msgid "ComplianceFramework|HIPAA"
+msgstr ""
+
msgid "ComplianceFramework|HIPAA - Health Insurance Portability and Accountability Act"
msgstr ""
+msgid "ComplianceFramework|PCI-DSS"
+msgstr ""
+
msgid "ComplianceFramework|PCI-DSS - Payment Card Industry-Data Security Standard"
msgstr ""
+msgid "ComplianceFramework|SOC 2"
+msgstr ""
+
msgid "ComplianceFramework|SOC 2 - Service Organization Control 2"
msgstr ""
+msgid "ComplianceFramework|SOX"
+msgstr ""
+
msgid "ComplianceFramework|SOX - Sarbanes-Oxley"
msgstr ""
@@ -5239,7 +5418,7 @@ msgid "Configuration"
msgstr "設定"
msgid "Configure GitLab runners to start using the Web Terminal. %{helpStart}Learn more.%{helpEnd}"
-msgstr ""
+msgstr "GitLab Runner を設定ã—ã¦Web端末を使ã„始ã‚ã¾ã™ã€‚ 詳細ã¯%{helpStart} ã“ã¡ã‚‰%{helpEnd}ã‚’å‚照。"
msgid "Configure Gitaly timeouts."
msgstr "Gitaly ã®ã‚¿ã‚¤ãƒ ã‚¢ã‚¦ãƒˆã‚’設定ã—ã¾ã™ã€‚"
@@ -5263,7 +5442,7 @@ msgid "Configure automatic git checks and housekeeping on repositories."
msgstr "リãƒã‚¸ãƒˆãƒªã«å¯¾ã—ã¦è‡ªå‹•å®Ÿè¡Œã™ã‚‹ Git ãƒã‚§ãƒƒã‚¯ã¨ãƒã‚¦ã‚¹ã‚­ãƒ¼ãƒ”ングを設定ã—ã¾ã™ã€‚"
msgid "Configure existing installation"
-msgstr ""
+msgstr "既存ã®ã‚¤ãƒ³ã‚¹ãƒˆãƒ¼ãƒ«ã‚’設定ã™ã‚‹"
msgid "Configure limit for issues created per minute by web and API requests."
msgstr ""
@@ -5272,7 +5451,7 @@ msgid "Configure limits for web and API requests."
msgstr "ウェブãŠã‚ˆã³APIリクエストã®åˆ¶é™ã‚’設定ã™ã‚‹ã€‚"
msgid "Configure limits on the number of inbound alerts able to be sent to a project."
-msgstr ""
+msgstr "プロジェクトã«é€ä¿¡ã§ãるインãƒã‚¦ãƒ³ãƒ‰ã‚¢ãƒ©ãƒ¼ãƒˆã®æ•°ã«åˆ¶é™ã‚’設定ã—ã¾ã™ã€‚"
msgid "Configure paths to be protected by Rack Attack."
msgstr ""
@@ -5302,7 +5481,7 @@ msgid "Confirmation required"
msgstr "確èªãŒå¿…è¦ã§ã™"
msgid "Congratulations! You have enabled Two-factor Authentication!"
-msgstr ""
+msgstr "ãŠã‚ã§ã¨ã†ã”ã–ã„ã¾ã™ã€‚2è¦ç´ èªè¨¼ãŒæœ‰åŠ¹ã«ãªã‚Šã¾ã—ãŸã€‚"
msgid "Connect"
msgstr "接続"
@@ -5323,7 +5502,7 @@ msgid "Connecting"
msgstr "接続中"
msgid "Connecting to terminal sync service"
-msgstr ""
+msgstr "ターミナルåŒæœŸã‚µãƒ¼ãƒ“スã«æŽ¥ç¶š"
msgid "Connecting..."
msgstr "接続ã—ã¦ã„ã¾ã™..."
@@ -5338,10 +5517,10 @@ msgid "Connection timed out"
msgstr ""
msgid "Contact an owner of group %{namespace_name} to upgrade the plan."
-msgstr ""
+msgstr "%{namespace_name} グループã®ã‚ªãƒ¼ãƒŠãƒ¼ã«é€£çµ¡ã—ã¦ã€ãƒ—ランをアップグレードã—ã¦ãã ã•ã„。"
msgid "Contact owner %{link_start}%{owner_name}%{link_end} to upgrade the plan."
-msgstr ""
+msgstr "オーナー㮠%{link_start}%{owner_name}%{link_end} ã«é€£çµ¡ã—ã¦ã€è¨ˆç”»ã‚’æ›´æ–°ã—ã¦ãã ã•ã„。"
msgid "Contact sales to upgrade"
msgstr "アップグレードã®å•ã„åˆã‚ã›"
@@ -5356,7 +5535,7 @@ msgid "Container Registry tag expiration policy"
msgstr ""
msgid "Container Scanning"
-msgstr ""
+msgstr "コンテナã®ã‚¹ã‚­ãƒ£ãƒ³"
msgid "Container does not exist"
msgstr ""
@@ -5370,9 +5549,15 @@ msgstr ""
msgid "Container repositories sync capacity"
msgstr ""
+msgid "ContainerRegistry| Please visit the %{linkStart}administration settings%{linkEnd} to enable this feature."
+msgstr ""
+
msgid "ContainerRegistry|%{imageName} tags"
msgstr ""
+msgid "ContainerRegistry|%{title} was successfully scheduled for deletion"
+msgstr ""
+
msgid "ContainerRegistry|Automatically remove extra images that aren't designed to be kept."
msgstr ""
@@ -5385,17 +5570,17 @@ msgstr ""
msgid "ContainerRegistry|Container Registry"
msgstr "コンテナレジストリ"
-msgid "ContainerRegistry|Copy build command"
+msgid "ContainerRegistry|Container Registry tag expiration and retention policy is disabled"
msgstr ""
+msgid "ContainerRegistry|Copy build command"
+msgstr "ビルドコマンドをコピー"
+
msgid "ContainerRegistry|Copy login command"
-msgstr ""
+msgstr "ログインコマンドã®ã‚³ãƒ”ー"
msgid "ContainerRegistry|Copy push command"
-msgstr ""
-
-msgid "ContainerRegistry|Currently, the Container Registry tag expiration feature is not available for projects created before GitLab version 12.8. For updates and more information, visit Issue %{linkStart}#196124%{linkEnd}"
-msgstr ""
+msgstr "プッシュコマンドをコピー"
msgid "ContainerRegistry|Docker connection error"
msgstr "Docker接続エラー"
@@ -5403,9 +5588,6 @@ msgstr "Docker接続エラー"
msgid "ContainerRegistry|Docker tag expiration policy is %{toggleStatus}"
msgstr ""
-msgid "ContainerRegistry|Docker tags with names matching this regex pattern will expire:"
-msgstr ""
-
msgid "ContainerRegistry|Edit Settings"
msgstr ""
@@ -5425,10 +5607,7 @@ msgid "ContainerRegistry|If you are not already logged in, you need to authentic
msgstr ""
msgid "ContainerRegistry|Image ID"
-msgstr ""
-
-msgid "ContainerRegistry|Image deleted successfully"
-msgstr ""
+msgstr "イメージID"
msgid "ContainerRegistry|Keep and protect the images that matter most."
msgstr ""
@@ -5445,41 +5624,53 @@ msgstr ""
msgid "ContainerRegistry|Number of tags to retain:"
msgstr ""
+msgid "ContainerRegistry|Please contact your administrator."
+msgstr ""
+
msgid "ContainerRegistry|Push an image"
msgstr ""
msgid "ContainerRegistry|Quick Start"
msgstr "クイックスタート"
+msgid "ContainerRegistry|Regular expressions such as %{codeStart}.*-test%{codeEnd} or %{codeStart}dev-.*%{codeEnd} are supported"
+msgstr ""
+
+msgid "ContainerRegistry|Regular expressions such as %{codeStart}.*-test%{codeEnd} or %{codeStart}dev-.*%{codeEnd} are supported. To select all tags, use %{codeStart}.*%{codeEnd}"
+msgstr ""
+
+msgid "ContainerRegistry|Remember to run %{docLinkStart}garbage collection%{docLinkEnd} to remove the stale data from storage."
+msgstr ""
+
msgid "ContainerRegistry|Remove repository"
msgstr "リãƒã‚¸ãƒˆãƒªã®å‰Šé™¤"
msgid "ContainerRegistry|Remove selected tags"
-msgstr ""
+msgstr "é¸æŠžã—ãŸã‚¿ã‚°ã®å‰Šé™¤"
msgid "ContainerRegistry|Remove tag"
msgid_plural "ContainerRegistry|Remove tags"
-msgstr[0] ""
+msgstr[0] "ã‚¿ã‚°ã®å‰Šé™¤"
msgid "ContainerRegistry|Retention policy has been Enabled"
msgstr ""
-msgid "ContainerRegistry|Something went wrong while deleting the image."
+msgid "ContainerRegistry|Something went wrong while fetching the expiration policy."
msgstr ""
-msgid "ContainerRegistry|Something went wrong while deleting the tag."
+msgid "ContainerRegistry|Something went wrong while fetching the repository list."
msgstr ""
-msgid "ContainerRegistry|Something went wrong while deleting the tags."
+msgid "ContainerRegistry|Something went wrong while fetching the tags list."
msgstr ""
-msgid "ContainerRegistry|Something went wrong while fetching the expiration policy."
+msgid "ContainerRegistry|Something went wrong while marking the tag for deletion."
msgstr ""
-msgid "ContainerRegistry|Something went wrong while fetching the packages list."
+msgid "ContainerRegistry|Something went wrong while marking the tags for deletion."
msgstr ""
-msgid "ContainerRegistry|Something went wrong while fetching the tags list."
+msgid "ContainerRegistry|Something went wrong while scheduling %{title} for deletion. Please try again."
msgstr ""
msgid "ContainerRegistry|Something went wrong while updating the expiration policy."
@@ -5488,16 +5679,25 @@ msgstr ""
msgid "ContainerRegistry|Tag"
msgstr "ã‚¿ã‚°"
-msgid "ContainerRegistry|Tag deleted successfully"
-msgstr ""
-
msgid "ContainerRegistry|Tag expiration policy"
msgstr ""
msgid "ContainerRegistry|Tag expiration policy is designed to:"
msgstr ""
-msgid "ContainerRegistry|Tags deleted successfully"
+msgid "ContainerRegistry|Tag successfully marked for deletion."
+msgstr ""
+
+msgid "ContainerRegistry|Tags successfully marked for deletion."
+msgstr ""
+
+msgid "ContainerRegistry|Tags with names matching this regex pattern will %{italicStart}be preserved:%{italicEnd}"
+msgstr ""
+
+msgid "ContainerRegistry|Tags with names matching this regex pattern will %{italicStart}expire:%{italicEnd}"
+msgstr ""
+
+msgid "ContainerRegistry|The Container Registry tag expiration and retention policies for this project have not been enabled."
msgstr ""
msgid "ContainerRegistry|The last tag related to this image was recently removed. This empty image and any associated data will be automatically removed as part of the regular Garbage Collection process. If you have any questions, contact your administrator."
@@ -5518,16 +5718,16 @@ msgstr "ã“ã®ã‚°ãƒ«ãƒ¼ãƒ—ã«ã¯åˆ©ç”¨ã§ãるコンテナイメージãŒã‚ã‚Š
msgid "ContainerRegistry|There are no container images stored for this project"
msgstr "ã“ã®ãƒ—ロジェクトã«ã¯ã‚³ãƒ³ãƒ†ãƒŠã‚¤ãƒ¡ãƒ¼ã‚¸ãŒä¿å­˜ã•ã‚Œã¦ã„ã¾ã›ã‚“"
-msgid "ContainerRegistry|This Registry contains deleted image tag data. Remember to run %{docLinkStart}garbage collection%{docLinkEnd} to remove the stale data from storage."
+msgid "ContainerRegistry|There was an error during the deletion of this image repository, please try again."
msgstr ""
msgid "ContainerRegistry|This image has no active tags"
-msgstr ""
+msgstr "ã“ã®ã‚¤ãƒ¡ãƒ¼ã‚¸ã«ã¯æœ‰åŠ¹ãªã‚¿ã‚°ãŒã‚ã‚Šã¾ã›ã‚“"
-msgid "ContainerRegistry|We are having trouble connecting to Docker, which could be due to an issue with your project name or path. %{docLinkStart}More Information%{docLinkEnd}"
+msgid "ContainerRegistry|This image repository is scheduled for deletion"
msgstr ""
-msgid "ContainerRegistry|Wildcards such as %{codeStart}.*-stable%{codeEnd} or %{codeStart}production/.*%{codeEnd} are supported. To select all tags, use %{codeStart}.*%{codeEnd}"
+msgid "ContainerRegistry|We are having trouble connecting to Docker, which could be due to an issue with your project name or path. %{docLinkStart}More Information%{docLinkEnd}"
msgstr ""
msgid "ContainerRegistry|With the Container Registry, every project can have its own space to store its Docker images. %{docLinkStart}More Information%{docLinkEnd}"
@@ -5591,10 +5791,10 @@ msgid "ContributionAnalytics|Issues"
msgstr ""
msgid "ContributionAnalytics|Last 3 months"
-msgstr ""
+msgstr "éŽåŽ»3ã‹æœˆ"
msgid "ContributionAnalytics|Last month"
-msgstr ""
+msgstr "先月"
msgid "ContributionAnalytics|Last week"
msgstr ""
@@ -5666,16 +5866,16 @@ msgid "Copy %{proxy_url}"
msgstr "%{proxy_url} をコピー"
msgid "Copy Account ID to clipboard"
-msgstr ""
+msgstr "クリップボードã«ã‚¢ã‚«ã‚¦ãƒ³ãƒˆ ID をコピー"
msgid "Copy External ID to clipboard"
-msgstr ""
+msgstr "クリップボードã«å¤–部 ID をコピー"
msgid "Copy ID"
msgstr "IDをコピー"
msgid "Copy KRB5 clone URL"
-msgstr ""
+msgstr "KRB5用クローンURLをコピー"
msgid "Copy SSH clone URL"
msgstr "SSH 用クローン URL をコピー"
@@ -5708,7 +5908,7 @@ msgid "Copy file contents"
msgstr "ファイルã®å†…容をコピー"
msgid "Copy file path"
-msgstr ""
+msgstr "ファイルã®ãƒ‘スをコピー"
msgid "Copy impersonation token"
msgstr ""
@@ -5729,19 +5929,19 @@ msgid "Copy link to chart"
msgstr ""
msgid "Copy personal access token"
-msgstr ""
+msgstr "個人ã®ã‚¢ã‚¯ã‚»ã‚¹ãƒˆãƒ¼ã‚¯ãƒ³ã‚’コピー"
msgid "Copy reference"
msgstr "å‚ç…§ã®ã‚³ãƒ”ー"
msgid "Copy secret"
-msgstr ""
+msgstr "秘密情報をコピー"
msgid "Copy token"
msgstr ""
msgid "Copy trigger token"
-msgstr ""
+msgstr "トリガートークンをコピー"
msgid "Copy value"
msgstr ""
@@ -5756,7 +5956,7 @@ msgid "Could not change HEAD: branch '%{branch}' does not exist"
msgstr "HEADを変更ã§ãã¾ã›ã‚“ã§ã—ãŸï¼š'%{branch}'ブランムãŒå­˜åœ¨ã—ã¾ã›ã‚“ã§ã—ãŸã€‚"
msgid "Could not connect to FogBugz, check your URL"
-msgstr ""
+msgstr "FogBugz ã«æŽ¥ç¶šã§ãã¾ã›ã‚“ã€URL を確èªã—ã¦ãã ã•ã„"
msgid "Could not connect to Sentry. Refresh the page to try again."
msgstr ""
@@ -5765,7 +5965,7 @@ msgid "Could not connect to Web IDE file mirror service."
msgstr ""
msgid "Could not create Wiki Repository at this time. Please try again later."
-msgstr ""
+msgstr "Wikiリãƒã‚¸ãƒˆãƒªã‚’作æˆã§ãã¾ã›ã‚“ã§ã—ãŸã€‚後ã§ã‚‚ã†ä¸€åº¦ã‚„ã‚Šç›´ã—ã¦ãã ã•ã„。"
msgid "Could not create environment"
msgstr ""
@@ -5782,19 +5982,22 @@ msgstr ""
msgid "Could not delete chat nickname %{chat_name}."
msgstr "ãƒãƒ£ãƒƒãƒˆã®ãƒ‹ãƒƒã‚¯ãƒãƒ¼ãƒ  %{chat_name} を削除ã§ãã¾ã›ã‚“ã§ã—ãŸã€‚"
-msgid "Could not find design"
+msgid "Could not find design."
msgstr ""
msgid "Could not remove the trigger."
msgstr "トリガーを除去ã§ãã¾ã›ã‚“ã§ã—ãŸã€‚"
msgid "Could not restore the group"
-msgstr ""
+msgstr "グループをリストアã§ãã¾ã›ã‚“ã§ã—ãŸ"
msgid "Could not revoke impersonation token %{token_name}."
msgstr ""
msgid "Could not revoke personal access token %{personal_access_token_name}."
+msgstr "個人ã®ã‚¢ã‚¯ã‚»ã‚¹ãƒˆãƒ¼ã‚¯ãƒ³ %{personal_access_token_name} を無効ã«ã§ãã¾ã›ã‚“ã§ã—ãŸã€‚"
+
+msgid "Could not revoke project access token %{project_access_token_name}."
msgstr ""
msgid "Could not save group ID"
@@ -5804,7 +6007,7 @@ msgid "Could not save project ID"
msgstr "プロジェクトIDã‚’ä¿å­˜ã§ãã¾ã›ã‚“ã§ã—ãŸ"
msgid "Could not save prometheus manual configuration"
-msgstr ""
+msgstr "プロメテウスã®ãƒžãƒ‹ãƒ¥ã‚¢ãƒ«è¨­å®šã‚’ä¿å­˜ã§ãã¾ã›ã‚“ã§ã—ãŸ"
msgid "Could not update the LDAP settings"
msgstr ""
@@ -5840,13 +6043,13 @@ msgid "Create a GitLab account first, and then connect it to your %{label} accou
msgstr "ã¯ã˜ã‚ã«GitLabアカウントを作æˆã—ã€ãã®å¾Œ %{label} ã®ã‚¢ã‚«ã‚¦ãƒ³ãƒˆã«æŽ¥ç¶šã—ã¾ã™ã€‚"
msgid "Create a Mattermost team for this group"
-msgstr ""
+msgstr "ã“ã®ã‚°ãƒ«ãƒ¼ãƒ—ã® Mattermost ãƒãƒ¼ãƒ ã‚’作æˆã—ã¾ã™"
msgid "Create a group for your organization"
msgstr ""
msgid "Create a local proxy for storing frequently used upstream images. %{link_start}Learn more%{link_end} about dependency proxies."
-msgstr ""
+msgstr "é »ç¹ã«ä½¿ç”¨ã™ã‚‹ã‚¢ãƒƒãƒ—ストリームイメージをä¿å­˜ã™ã‚‹ãŸã‚ã€ãƒ­ãƒ¼ã‚«ãƒ«ãƒ—ロキシを作æˆã—ã¾ã™ã€‚ %{link_start}ä¾å­˜é–¢ä¿‚プロキシã«ã¤ã„ã¦ã¯ã“ã¡ã‚‰ã‚’å‚ç…§ã—ã¦ãã ã•ã„%{link_end}。"
msgid "Create a merge request"
msgstr "マージリクエストを作æˆ"
@@ -5854,9 +6057,12 @@ msgstr "マージリクエストを作æˆ"
msgid "Create a new branch"
msgstr "æ–°ã—ã„ブランãƒã‚’作æˆ"
-msgid "Create a new file as there are no files yet. Afterwards, you'll be able to commit your changes."
+msgid "Create a new deploy key for this project"
msgstr ""
+msgid "Create a new file as there are no files yet. Afterwards, you'll be able to commit your changes."
+msgstr "ã¾ã ãƒ•ã‚¡ã‚¤ãƒ«ãŒãªã„ãŸã‚ã€æ–°ã—ã„ファイルを作æˆã—ã¾ã™ã€‚ ãã®å¾Œã€ã‚ãªãŸã¯å¤‰æ›´ã‚’コミットã§ãã¾ã™ã€‚"
+
msgid "Create a new issue"
msgstr "課題ã®æ–°è¦ä½œæˆ"
@@ -5873,7 +6079,7 @@ msgid "Create an account using:"
msgstr ""
msgid "Create an issue"
-msgstr ""
+msgstr "課題を作æˆ"
msgid "Create an issue. Issues are created for each alert triggered."
msgstr "課題を作æˆã™ã‚‹ã€‚課題ã¯ç™ºç”Ÿã—ãŸå„アラート毎ã«ä½œæˆã•ã‚Œã¾ã™ã€‚"
@@ -5915,7 +6121,7 @@ msgid "Create group label"
msgstr "グループラベルを作æˆ"
msgid "Create issue"
-msgstr ""
+msgstr "課題ã®ä½œæˆ"
msgid "Create lists from labels. Issues with that label appear in that list."
msgstr "ラベルã‹ã‚‰ãƒªã‚¹ãƒˆã‚’作æˆã€‚ãã®ãƒ©ãƒ™ãƒ«ã®èª²é¡ŒãŒãƒªã‚¹ãƒˆã«è¡¨ç¤ºã•ã‚Œã¾ã™ã€‚"
@@ -5959,6 +6165,9 @@ msgstr "プロジェクトラベルを作æˆ"
msgid "Create requirement"
msgstr ""
+msgid "Create snippet"
+msgstr ""
+
msgid "Create wildcard: %{searchTerm}"
msgstr ""
@@ -5980,6 +6189,9 @@ msgstr "個人用アクセストークンを作æˆ"
msgid "Created"
msgstr "作æˆæ¸ˆã¿"
+msgid "Created %{timestamp}"
+msgstr ""
+
msgid "Created At"
msgstr "作æˆæ—¥"
@@ -6029,7 +6241,7 @@ msgid "Creation date"
msgstr "作æˆæ—¥æ™‚"
msgid "Credentials"
-msgstr ""
+msgstr "èªè¨¼æƒ…å ±"
msgid "CredentialsInventory|No credentials found"
msgstr ""
@@ -6041,7 +6253,7 @@ msgid "CredentialsInventory|SSH Keys"
msgstr ""
msgid "Critical vulnerabilities present"
-msgstr ""
+msgstr "é‡å¤§ãªè„†å¼±æ€§ã®å­˜åœ¨"
msgid "Cron Timezone"
msgstr "Cron ã®ã‚¿ã‚¤ãƒ ã‚¾ãƒ¼ãƒ³"
@@ -6071,7 +6283,7 @@ msgid "Current password"
msgstr "ç¾åœ¨ã®ãƒ‘スワード"
msgid "Current vulnerabilities count"
-msgstr ""
+msgstr "ç¾åœ¨ã®è„†å¼±æ€§ã‚«ã‚¦ãƒ³ãƒˆ"
msgid "CurrentUser|Buy CI minutes"
msgstr ""
@@ -6083,13 +6295,13 @@ msgid "CurrentUser|Settings"
msgstr "設定"
msgid "CurrentUser|Start a Gold trial"
-msgstr ""
+msgstr "Gold試用版を開始"
msgid "Custom CI configuration path"
-msgstr ""
+msgstr "カスタムCIã®è¨­å®šãƒ‘ス"
msgid "Custom Git clone URL for HTTP(S)"
-msgstr ""
+msgstr "HTTP(S) 用ã®ã‚«ã‚¹ã‚¿ãƒ  Git クローン URL"
msgid "Custom hostname (for private commit emails)"
msgstr "カスタムホストå (プライベートコミット用メールアドレス)"
@@ -6116,13 +6328,13 @@ msgid "CustomCycleAnalytics|Add stage"
msgstr "ステージを追加"
msgid "CustomCycleAnalytics|Editing stage"
-msgstr ""
+msgstr "ステージã®ç·¨é›†"
msgid "CustomCycleAnalytics|Enter a name for the stage"
msgstr ""
msgid "CustomCycleAnalytics|Name"
-msgstr ""
+msgstr "åå‰"
msgid "CustomCycleAnalytics|New stage"
msgstr ""
@@ -6140,40 +6352,40 @@ msgid "CustomCycleAnalytics|Stage name already exists"
msgstr ""
msgid "CustomCycleAnalytics|Start event"
-msgstr ""
+msgstr "開始イベント"
msgid "CustomCycleAnalytics|Start event changed, please select a valid stop event"
msgstr ""
msgid "CustomCycleAnalytics|Start event label"
-msgstr ""
+msgstr "開始イベントã®ãƒ©ãƒ™ãƒ«"
msgid "CustomCycleAnalytics|Stop event"
-msgstr ""
+msgstr "åœæ­¢ã‚¤ãƒ™ãƒ³ãƒˆ"
msgid "CustomCycleAnalytics|Stop event label"
-msgstr ""
+msgstr "åœæ­¢ã‚¤ãƒ™ãƒ³ãƒˆã®ãƒ©ãƒ™ãƒ«"
msgid "CustomCycleAnalytics|Update stage"
-msgstr ""
+msgstr "ステージã®æ›´æ–°"
msgid "Customize colors"
msgstr "カスタムカラー"
msgid "Customize how FogBugz email addresses and usernames are imported into GitLab. In the next step, you'll be able to select the projects you want to import."
-msgstr ""
+msgstr "FogBugz ã®ãƒ¡ãƒ¼ãƒ«ã‚¢ãƒ‰ãƒ¬ã‚¹ã¨ãƒ¦ãƒ¼ã‚¶åã‚’ GitLab ã«ã‚¤ãƒ³ãƒãƒ¼ãƒˆã™ã‚‹æ–¹æ³•ã‚’カスタマイズã—ã¾ã™ã€‚次ã®ã‚¹ãƒ†ãƒƒãƒ—ã§ã¯ã€ã‚¤ãƒ³ãƒãƒ¼ãƒˆã™ã‚‹ãƒ—ロジェクトをé¸æŠžã—ã¾ã™ã€‚"
msgid "Customize how Google Code email addresses and usernames are imported into GitLab. In the next step, you'll be able to select the projects you want to import."
msgstr "Google Code ã®ãƒ¡ãƒ¼ãƒ«ã‚¢ãƒ‰ãƒ¬ã‚¹ã¨ãƒ¦ãƒ¼ã‚¶åã‚’ GitLab ã«ã‚¤ãƒ³ãƒãƒ¼ãƒˆã™ã‚‹æ–¹æ³•ã‚’カスタマイズã—ã¾ã™ã€‚次ã®ã‚¹ãƒ†ãƒƒãƒ—ã§ã¯ã€ã‚¤ãƒ³ãƒãƒ¼ãƒˆã™ã‚‹ãƒ—ロジェクトをé¸æŠžã—ã¾ã™ã€‚"
msgid "Customize icon"
-msgstr ""
+msgstr "アイコンをカスタマイズã™ã‚‹"
msgid "Customize language and region related settings."
-msgstr ""
+msgstr "言語ã¨åœ°åŸŸã®è¨­å®šã‚’カスタマイズã—ã¾ã™ã€‚"
msgid "Customize name"
-msgstr ""
+msgstr "åå‰ã‚’カスタマイズ"
msgid "Customize your pipeline configuration, view your pipeline status and coverage report."
msgstr "Runner トークンã«ã‚¢ã‚¯ã‚»ã‚¹ã—ã€ãƒ‘イプラインã®è¨­å®šã‚’カスタマイズã€ãã—ã¦ãƒ‘イプラインã®çŠ¶æ…‹ã¨ã‚«ãƒãƒ¬ãƒƒã‚¸ãƒ¬ãƒãƒ¼ãƒˆã‚’閲覧ã—ã¾ã™ã€‚"
@@ -6182,40 +6394,40 @@ msgid "Customize your pipeline configuration."
msgstr ""
msgid "CycleAnalyticsEvent|Issue closed"
-msgstr ""
+msgstr "完了ã—ãŸèª²é¡Œ"
msgid "CycleAnalyticsEvent|Issue created"
-msgstr ""
+msgstr "作æˆã•ã‚ŒãŸèª²é¡Œ"
msgid "CycleAnalyticsEvent|Issue first added to a board"
-msgstr ""
+msgstr "ボードã«æœ€åˆã«åŠ ãˆã‚‰ã‚ŒãŸèª²é¡Œ"
msgid "CycleAnalyticsEvent|Issue first associated with a milestone"
-msgstr ""
+msgstr "マイルストーンã«æœ€åˆã«é–¢é€£ä»˜ã‘られãŸèª²é¡Œ"
msgid "CycleAnalyticsEvent|Issue first associated with a milestone or issue first added to a board"
-msgstr ""
+msgstr "最åˆã«ãƒžã‚¤ãƒ«ã‚¹ãƒˆãƒ¼ãƒ³ã«é–¢é€£ã¥ã‘られãŸèª²é¡Œã€ã¾ãŸã¯æœ€åˆã«ãƒœãƒ¼ãƒ‰ã«åŠ ãˆã‚‰ã‚ŒãŸèª²é¡Œ"
msgid "CycleAnalyticsEvent|Issue first mentioned in a commit"
msgstr ""
msgid "CycleAnalyticsEvent|Issue label was added"
-msgstr ""
+msgstr "追加ã•ã‚ŒãŸèª²é¡Œãƒ©ãƒ™ãƒ«"
msgid "CycleAnalyticsEvent|Issue label was removed"
-msgstr ""
+msgstr "削除ã•ã‚ŒãŸèª²é¡Œãƒ©ãƒ™ãƒ«"
msgid "CycleAnalyticsEvent|Issue last edited"
-msgstr ""
+msgstr "最後ã«ç·¨é›†ã•ã‚ŒãŸèª²é¡Œ"
msgid "CycleAnalyticsEvent|Merge request closed"
-msgstr ""
+msgstr "クローズã—ãŸãƒžãƒ¼ã‚¸ãƒªã‚¯ã‚¨ã‚¹ãƒˆ"
msgid "CycleAnalyticsEvent|Merge request created"
-msgstr ""
+msgstr "作æˆã•ã‚ŒãŸãƒžãƒ¼ã‚¸ãƒªã‚¯ã‚¨ã‚¹ãƒˆ"
msgid "CycleAnalyticsEvent|Merge request first deployed to production"
-msgstr ""
+msgstr "最åˆã«è£½å“ã«ãƒ‡ãƒ—ロイã—ãŸãƒžãƒ¼ã‚¸ãƒªã‚¯ã‚¨ã‚¹ãƒˆ"
msgid "CycleAnalyticsEvent|Merge request label was added"
msgstr ""
@@ -6335,6 +6547,9 @@ msgstr ""
msgid "CycleAnalytics|stage dropdown"
msgstr ""
+msgid "DAG"
+msgstr ""
+
msgid "DNS"
msgstr "DNS"
@@ -6348,13 +6563,13 @@ msgid "DashboardProjects|Personal"
msgstr "個人"
msgid "DashboardProjects|Trending"
-msgstr ""
+msgstr "トレンド"
msgid "Dashboards"
msgstr "ダッシュボード"
msgid "Dashboard|%{firstProject} and %{secondProject}"
-msgstr ""
+msgstr "%{firstProject} 㨠%{secondProject}"
msgid "Dashboard|%{firstProject}, %{rest}, and %{secondProject}"
msgstr ""
@@ -6381,7 +6596,7 @@ msgid "DayTitle|F"
msgstr "金"
msgid "DayTitle|M"
-msgstr ""
+msgstr "月"
msgid "DayTitle|S"
msgstr ""
@@ -6420,7 +6635,7 @@ msgid "Default artifacts expiration"
msgstr "デフォルトã®ã‚¢ãƒ¼ãƒ†ã‚£ãƒ•ã‚¡ã‚¯ãƒˆã®æœ‰åŠ¹æœŸé™"
msgid "Default branch"
-msgstr ""
+msgstr "デフォルトブランãƒ"
msgid "Default branch and protected branches"
msgstr ""
@@ -6558,7 +6773,7 @@ msgid "Deleted"
msgstr "削除完了"
msgid "Deleted chat nickname: %{chat_name}!"
-msgstr ""
+msgstr "ãƒãƒ£ãƒƒãƒˆãƒ‹ãƒƒã‚¯ãƒãƒ¼ãƒ ã‚’削除ã—ã¾ã—ãŸï¼š %{chat_name}ï¼"
msgid "Deleted in this version"
msgstr ""
@@ -6579,7 +6794,7 @@ msgid "Deletion pending. This project will be removed on %{date}. Repository and
msgstr ""
msgid "Denied authorization of chat nickname %{user_name}."
-msgstr ""
+msgstr "ãƒãƒ£ãƒƒãƒˆãƒ‹ãƒƒã‚¯ãƒãƒ¼ãƒ  %{user_name} ã®æ‰¿èªã‚’æ‹’å¦ã—ã¾ã—ãŸã€‚"
msgid "Deny"
msgstr "æ‹’å¦"
@@ -6618,7 +6833,7 @@ msgid "Dependencies|Component name"
msgstr "コンãƒãƒ¼ãƒãƒ³ãƒˆå"
msgid "Dependencies|Export as JSON"
-msgstr ""
+msgstr "JSONエクスãƒãƒ¼ãƒˆ"
msgid "Dependencies|Job failed to generate the dependency list"
msgstr ""
@@ -6627,7 +6842,7 @@ msgid "Dependencies|License"
msgstr ""
msgid "Dependencies|Location"
-msgstr ""
+msgstr "ロケーション"
msgid "Dependencies|Packager"
msgstr "パッケージャー"
@@ -6660,22 +6875,22 @@ msgid "Dependency List has no entries"
msgstr ""
msgid "Dependency Proxy"
-msgstr ""
+msgstr "ä¾å­˜é–¢ä¿‚プロキシ"
msgid "Dependency Scanning"
msgstr ""
msgid "Dependency proxy"
-msgstr ""
+msgstr "ä¾å­˜é–¢ä¿‚プロキシ"
msgid "Dependency proxy URL"
-msgstr ""
+msgstr "ä¾å­˜é–¢ä¿‚プロキシURL"
msgid "Dependency proxy feature is limited to public groups for now."
msgstr ""
msgid "DependencyProxy|Toggle Dependency Proxy"
-msgstr ""
+msgstr "ä¾å­˜é–¢ä¿‚プロキシã®åˆ‡ã‚Šæ›¿ãˆ"
msgid "Depends on %d merge request being merged"
msgid_plural "Depends on %d merge requests being merged"
@@ -6695,6 +6910,9 @@ msgstr "デプロイキー"
msgid "Deploy key was successfully updated."
msgstr "デプロイキーã¯æ­£å¸¸ã«æ›´æ–°ã•ã‚Œã¾ã—ãŸã€‚"
+msgid "Deploy keys allow read-only or read-write (if enabled) access to your repository. Deploy keys can be used for CI, staging or production servers. You can create a deploy key or add an existing one."
+msgstr ""
+
msgid "Deploy progress not found. To see pods, ensure your environment matches %{linkStart}deploy board criteria%{linkEnd}."
msgstr ""
@@ -6848,6 +7066,9 @@ msgstr "デプロイ先"
msgid "Deploying to"
msgstr "デプロイ先"
+msgid "Deployment Frequency"
+msgstr ""
+
msgid "Deployment|API"
msgstr ""
@@ -6903,7 +7124,7 @@ msgid "Design Management files and data"
msgstr ""
msgid "DesignManagement|%{current_design} of %{designs_count}"
-msgstr ""
+msgstr "%{designs_count} / %{current_design}"
msgid "DesignManagement|%{filename} did not change."
msgstr ""
@@ -6951,7 +7172,7 @@ msgid "DesignManagement|Error uploading a new design. Please try again."
msgstr ""
msgid "DesignManagement|Go back to designs"
-msgstr ""
+msgstr "å…ƒã®ãƒ‡ã‚¶ã‚¤ãƒ³ã¸"
msgid "DesignManagement|Go to next design"
msgstr "次ã®ãƒ‡ã‚¶ã‚¤ãƒ³ã¸"
@@ -7044,13 +7265,13 @@ msgid "Disable shared Runners"
msgstr "共有 Runner を無効化"
msgid "Disable two-factor authentication"
-msgstr ""
+msgstr "2è¦ç´ èªè¨¼ã‚’無効ã«ã™ã‚‹"
msgid "Disabled"
msgstr "無効"
msgid "Disabled mirrors can only be enabled by instance owners. It is recommended that you delete them."
-msgstr ""
+msgstr "無効化ã•ã‚ŒãŸãƒŸãƒ©ãƒ¼ã¯ã€ã‚¤ãƒ³ã‚¹ã‚¿ãƒ³ã‚¹æ‰€æœ‰è€…ã ã‘ãŒæœ‰åŠ¹ã«ã§ãã¾ã™ã€‚削除ã™ã‚‹ã“ã¨ã‚’ãŠå‹§ã‚ã—ã¾ã™ã€‚"
msgid "Discard all changes"
msgstr "ã™ã¹ã¦ã®å¤‰æ›´ã‚’破棄"
@@ -7365,9 +7586,6 @@ msgstr ""
msgid "Edit environment"
msgstr "環境を編集"
-msgid "Edit epic description"
-msgstr ""
-
msgid "Edit file"
msgstr "ファイルã®ç·¨é›†"
@@ -7396,7 +7614,7 @@ msgid "Edit wiki page"
msgstr ""
msgid "Edit your most recent comment in a thread (from an empty textarea)"
-msgstr ""
+msgstr "スレッド内ã®æœ€æ–°ã®ã‚ãªãŸã®ã‚³ãƒ¡ãƒ³ãƒˆã‚’編集 (空ã®ãƒ†ã‚­ã‚¹ãƒˆé ˜åŸŸã‹ã‚‰)"
msgid "Edited %{timeago}"
msgstr ""
@@ -7408,7 +7626,7 @@ msgid "Elasticsearch"
msgstr "Elasticsearch"
msgid "Elasticsearch AWS IAM credentials"
-msgstr ""
+msgstr "Elasticsearch AWS IAMèªè¨¼æƒ…å ±"
msgid "Elasticsearch indexing restrictions"
msgstr "Elasticsearchã®ã‚¤ãƒ³ãƒ‡ãƒƒã‚¯ã‚¹ä½œæˆã®åˆ¶é™"
@@ -7438,7 +7656,7 @@ msgid "Email display name"
msgstr ""
msgid "Email not verified. Please verify your email in Salesforce."
-msgstr ""
+msgstr "メールãŒç¢ºèªã§ãã¦ã„ã¾ã›ã‚“。 Salesforce ã§ãƒ¡ãƒ¼ãƒ«ã‚’確èªã—ã¦ãã ã•ã„。"
msgid "Email patch"
msgstr "パッãƒã‚’メールã™ã‚‹"
@@ -7459,13 +7677,13 @@ msgid "EmailError|The thread you are replying to no longer exists, perhaps it wa
msgstr "返信ã•ã‚ŒãŸã‚¹ãƒ¬ãƒƒãƒ‰ã¯æ—¢ã«å­˜åœ¨ã—ã¾ã›ã‚“。ãŠãらããã‚Œã¯å‰Šé™¤ã•ã‚ŒãŸã‚‚ã®ã¨æ€ã‚ã‚Œã¾ã™ã€‚ã“ã‚ŒãŒèª¤ã£ã¦ã„ã‚‹ã¨æ€ã‚れる場åˆã¯ã€ã‚¹ã‚¿ãƒƒãƒ•ãƒ¡ãƒ³ãƒãƒ¼ã«å•ã„åˆã‚ã›ã¦ãã ã•ã„。"
msgid "EmailError|We couldn't figure out what the email is for. Please create your issue or comment through the web interface."
-msgstr ""
+msgstr "é€ä¿¡ã•ã‚ŒãŸãƒ¡ãƒ¼ãƒ«ã‚’処ç†ã™ã‚‹ã“ã¨ãŒã§ãã¾ã›ã‚“ã§ã—ãŸã€‚Webインターフェース上ã‹ã‚‰èª²é¡Œã‚’作æˆã™ã‚‹ã‹ã€ã‚³ãƒ¡ãƒ³ãƒˆã‚’ã—ã¦ãã ã•ã„。"
msgid "EmailError|We couldn't figure out what the email is in reply to. Please create your comment through the web interface."
msgstr "メールãŒã©ã“ã¸ã®è¿”ä¿¡ã‹åˆ¤ã‚Šã¾ã›ã‚“ã§ã—ãŸã€‚ Webインターフェイス上ã‹ã‚‰ã‚³ãƒ¡ãƒ³ãƒˆã‚’作æˆã—ã¦ãã ã•ã„。"
msgid "EmailError|We couldn't figure out what user corresponds to the email. Please create your comment through the web interface."
-msgstr ""
+msgstr "ã©ã®ãƒ¦ãƒ¼ã‚¶ãƒ¼ãŒãƒ¡ãƒ¼ãƒ«ã«å¯¾å¿œã—ã¦ã„ã‚‹ã®ã‹æŠŠæ¡ã§ãã¾ã›ã‚“ã§ã—ãŸã€‚ Webインターフェイス上ã‹ã‚‰ã‚³ãƒ¡ãƒ³ãƒˆã‚’作æˆã—ã¦ãã ã•ã„。"
msgid "EmailError|We couldn't find the project. Please check if there's any typo."
msgstr "プロジェクトãŒè¦‹ã¤ã‹ã‚Šã¾ã›ã‚“ã§ã—ãŸã€‚入力ミスãŒãªã„ã‹ç¢ºèªã—ã¦ãã ã•ã„。"
@@ -7492,19 +7710,19 @@ msgid "Emails separated by comma"
msgstr "Eメール (コンマ区切り)"
msgid "EmailsOnPushService|Disable code diffs"
-msgstr ""
+msgstr "コード比較を無効ã«ã™ã‚‹"
msgid "EmailsOnPushService|Don't include possibly sensitive code diffs in notification body."
-msgstr ""
+msgstr "公開ã—ã¦ã¯ã„ã‘ãªã„コードã§ã‚ã‚‹å¯èƒ½æ€§ã‚’è¸ã¾ãˆã€ã‚³ãƒ¼ãƒ‰å·®åˆ†ã‚’通知ã®æœ¬æ–‡ã«å«ã‚ãªã„ã§ãã ã•ã„。"
msgid "EmailsOnPushService|Email the commits and diff of each push to a list of recipients."
msgstr ""
msgid "EmailsOnPushService|Emails on push"
-msgstr ""
+msgstr "プッシュ時ã®Eメール"
msgid "EmailsOnPushService|Emails separated by whitespace"
-msgstr ""
+msgstr "空白区切りã®ãƒ¡ãƒ¼ãƒ«ã‚¢ãƒ‰ãƒ¬ã‚¹(複数)"
msgid "EmailsOnPushService|Send from committer"
msgstr "コミッターã‹ã‚‰é€ä¿¡ã™ã‚‹"
@@ -7531,7 +7749,7 @@ msgid "Enable Incident Management inbound alert limit"
msgstr ""
msgid "Enable PlantUML"
-msgstr ""
+msgstr "PlantUML を有効化"
msgid "Enable Pseudonymizer data collection"
msgstr "匿å化データã®åŽé›†ã‚’有効ã«ã—ã¾ã™"
@@ -7549,7 +7767,7 @@ msgid "Enable access to the Performance Bar for a given group."
msgstr "指定グループã®ãƒ‘フォーマンスãƒãƒ¼ã¸ã®ã‚¢ã‚¯ã‚»ã‚¹ã‚’有効ã«ã™ã‚‹ã€‚"
msgid "Enable and configure Grafana."
-msgstr ""
+msgstr "Grafana を有効ã«ã—ã¦æ§‹æˆã™ã‚‹ã€‚"
msgid "Enable and configure InfluxDB metrics."
msgstr "InfluxDB ã®ãƒ¡ãƒˆãƒªã‚¯ã‚¹ã‚’有効ã«ã—ã¦è¨­å®šã™ã‚‹ã€‚"
@@ -7624,7 +7842,7 @@ msgid "Enable usage ping to get an overview of how you are using GitLab from a f
msgstr "Ping ã®ä½¿ç”¨ã‚’有効ã«ã™ã‚‹ã¨ã€æ©Ÿèƒ½ãƒ‘ースペクティブã‹ã‚‰ GitLab ã‚’ã©ã®ã‚ˆã†ã«ä½¿ç”¨ã—ã¦ã„ã‚‹ã‹ã‚’知るã“ã¨ãŒã§ãã¾ã™ã€‚"
msgid "Enable/disable your service desk. %{link_start}Learn more about service desk%{link_end}."
-msgstr ""
+msgstr "サービスデスクを有効/無効ã«ã—ã¾ã™ã€‚ %{link_start}サービスデスクã®è©³ç´°ã«ã¤ã„ã¦%{link_end}"
msgid "EnableReviewApp|%{stepStart}Step 1%{stepEnd}. Ensure you have Kubernetes set up and have a base domain for your %{linkStart}cluster%{linkEnd}."
msgstr ""
@@ -7648,10 +7866,10 @@ msgid "Enabled Git access protocols"
msgstr ""
msgid "Enabled sources for code import during project creation. OmniAuth must be configured for GitHub"
-msgstr ""
+msgstr "プロジェクト作æˆä¸­ã®ã‚³ãƒ¼ãƒ‰ã‚¤ãƒ³ãƒãƒ¼ãƒˆã«æœ‰åŠ¹ãªã‚½ãƒ¼ã‚¹ã€‚ OmniAuth 㯠GitHub 用ã«è¨­å®šã™ã‚‹å¿…è¦ãŒã‚ã‚Šã¾ã™"
msgid "Enabling this will only make licensed EE features available to projects if the project namespace's plan includes the feature or if the project is public."
-msgstr ""
+msgstr "ã“れを有効ã«ã™ã‚‹ã¨ã€ãƒ—ロジェクトåå‰ç©ºé–“ã®è¨ˆç”»ã«ãã®æ©Ÿèƒ½ãŒå«ã¾ã‚Œã¦ã„ã‚‹å ´åˆã€ã¾ãŸã¯ãƒ—ロジェクトãŒå…¬é–‹ã•ã‚Œã¦ã„ã‚‹å ´åˆã«ã®ã¿ã€ãƒ©ã‚¤ã‚»ãƒ³ã‚¹ã•ã‚ŒãŸEE機能ãŒãã®ãƒ—ロジェクトã§ä½¿ç”¨å¯èƒ½ã«ãªã‚Šã¾ã™ã€‚"
msgid "Encountered an error while rendering: %{err}"
msgstr ""
@@ -7747,7 +7965,7 @@ msgid "Environment variables are applied to environments via the runner. They ca
msgstr ""
msgid "Environment variables are configured by your administrator to be %{link_start}protected%{link_end} by default"
-msgstr ""
+msgstr "環境変数ã¯ã€ç®¡ç†è€…ã«ã‚ˆã£ã¦ãƒ‡ãƒ•ã‚©ãƒ«ãƒˆãŒ %{link_start}ä¿è­·%{link_end} ã«è¨­å®šã•ã‚Œã¦ã„ã¾ã™ã€‚"
msgid "Environment:"
msgstr "環境:"
@@ -7786,16 +8004,16 @@ msgid "EnvironmentsDashboard|Job: %{job}"
msgstr "ジョブ: %{job}"
msgid "EnvironmentsDashboard|More actions"
-msgstr ""
+msgstr "追加ã®ã‚¢ã‚¯ã‚·ãƒ§ãƒ³"
msgid "EnvironmentsDashboard|Read more."
msgstr ""
msgid "EnvironmentsDashboard|Remove"
-msgstr ""
+msgstr "削除"
msgid "EnvironmentsDashboard|The environments dashboard provides a summary of each project's environments' status, including pipeline and alert statuses."
-msgstr ""
+msgstr "環境情報ダッシュボードã¯å„プロジェクトã®ç’°å¢ƒæƒ…å ±(パイプラインやアラート状態å«ã‚€) を表示ã—ã¾ã™ã€‚"
msgid "EnvironmentsDashboard|This dashboard displays a maximum of 7 projects and 3 environments per project. %{readMoreLink}"
msgstr ""
@@ -7912,10 +8130,10 @@ msgid "Environments|Re-deploy"
msgstr "å†ãƒ‡ãƒ—ロイ"
msgid "Environments|Re-deploy environment %{environment_name}?"
-msgstr ""
+msgstr "環境 %{environment_name} å†ãƒ‡ãƒ—ロイã—ã¾ã™ã‹?"
msgid "Environments|Re-deploy environment %{name}?"
-msgstr ""
+msgstr "環境 %{name} å†ãƒ‡ãƒ—ロイã—ã¾ã™ã‹?"
msgid "Environments|Re-deploy to environment"
msgstr "環境ã«å†ãƒ‡ãƒ—ロイ"
@@ -7930,10 +8148,10 @@ msgid "Environments|Rollback environment"
msgstr "環境ã®ãƒ­ãƒ¼ãƒ«ãƒãƒƒã‚¯"
msgid "Environments|Rollback environment %{environment_name}?"
-msgstr ""
+msgstr "環境 %{environment_name} をロールãƒãƒƒã‚¯ã—ã¾ã™ã‹?"
msgid "Environments|Rollback environment %{name}?"
-msgstr ""
+msgstr "環境 %{name} をロールãƒãƒƒã‚¯ã—ã¾ã™ã‹?"
msgid "Environments|Select environment"
msgstr ""
@@ -7957,16 +8175,16 @@ msgid "Environments|There was an error fetching the logs. Please try again."
msgstr ""
msgid "Environments|This action will relaunch the job for commit %{commit_id}, putting the environment in a previous version. Are you sure you want to continue?"
-msgstr ""
+msgstr "ã“ã®ã‚¢ã‚¯ã‚·ãƒ§ãƒ³ã¯ã‚³ãƒŸãƒƒãƒˆ %{commit_id} ã®ã‚¸ãƒ§ãƒ–ã‚’å†èµ·å‹•ã—ã€ç’°å¢ƒã‚’以å‰ã®ãƒãƒ¼ã‚¸ãƒ§ãƒ³ã«ã—ã¾ã™ã€‚本当ã«ç¶šã‘ã¾ã™ã‹ï¼Ÿ"
msgid "Environments|This action will relaunch the job for commit %{linkStart}%{commitId}%{linkEnd}, putting the environment in a previous version. Are you sure you want to continue?"
-msgstr ""
+msgstr "ã“ã®ã‚¢ã‚¯ã‚·ãƒ§ãƒ³ã¯ã‚³ãƒŸãƒƒãƒˆ %{linkStart}%{commitId}%{linkEnd} ã®ã‚¸ãƒ§ãƒ–ã‚’å†èµ·å‹•ã—ã€ç’°å¢ƒã‚’以å‰ã®ãƒãƒ¼ã‚¸ãƒ§ãƒ³ã«ã—ã¾ã™ã€‚本当ã«ç¶šã‘ã¾ã™ã‹ï¼Ÿ"
msgid "Environments|This action will run the job defined by %{environment_name} for commit %{commit_id}, putting the environment in a previous version. You can revert it by re-deploying the latest version of your application. Are you sure you want to continue?"
msgstr ""
msgid "Environments|This action will run the job defined by %{name} for commit %{linkStart}%{commitId}%{linkEnd} putting the environment in a previous version. You can revert it by re-deploying the latest version of your application. Are you sure you want to continue?"
-msgstr ""
+msgstr "ã“ã®ã‚¢ã‚¯ã‚·ãƒ§ãƒ³ã¯ã€ã‚³ãƒŸãƒƒãƒˆ %{linkStart}%{commitId}%{linkEnd} ã«å¯¾ã—㦠%{name} ã§å®šç¾©ã•ã‚ŒãŸã‚¸ãƒ§ãƒ–を実行ã—ã¦ã€ç’°å¢ƒã‚’å‰ã®ãƒãƒ¼ã‚¸ãƒ§ãƒ³ã«ã—ã¾ã™ã€‚アプリケーションã®æœ€æ–°ãƒãƒ¼ã‚¸ãƒ§ãƒ³ã‚’å†ãƒ‡ãƒ—ロイã™ã‚‹ã“ã¨ã§ revert ã§ãã¾ã™ã€‚続ã‘ã¾ã™ã‹ï¼Ÿ"
msgid "Environments|Updated"
msgstr "更新済ã¿"
@@ -7989,9 +8207,6 @@ msgstr ""
msgid "Epics"
msgstr "エピック"
-msgid "Epics (Ultimate / Gold license only)"
-msgstr ""
-
msgid "Epics Roadmap"
msgstr "エピック ロードマップ"
@@ -8001,6 +8216,9 @@ msgstr ""
msgid "Epics let you manage your portfolio of projects more efficiently and with less effort"
msgstr "エピックを使用ã™ã‚‹ã¨ã€ãƒ—ロジェクトã®ãƒãƒ¼ãƒˆãƒ•ã‚©ãƒªã‚ªã‚’より効率的ã‹ã¤å°‘ãªã„労力ã§ç®¡ç†ã§ãã¾ã™"
+msgid "Epics, Issues, and Merge Requests"
+msgstr ""
+
msgid "Epics|Add an epic"
msgstr "エピックを追加"
@@ -8052,6 +8270,9 @@ msgstr ""
msgid "Epics|Something went wrong while fetching group epics."
msgstr ""
+msgid "Epics|Something went wrong while moving item."
+msgstr ""
+
msgid "Epics|Something went wrong while ordering item."
msgstr ""
@@ -8062,7 +8283,7 @@ msgid "Epics|These dates affect how your epics appear in the roadmap. Dates from
msgstr "ã“れらã®æ—¥ä»˜ã¯ã€ã‚ãªãŸã®ã‚¨ãƒ”ックãŒãƒ­ãƒ¼ãƒ‰ãƒžãƒƒãƒ—ã«ã©ã®ã‚ˆã†ã«è¡¨ç¤ºã•ã‚Œã‚‹ã‹ã«å½±éŸ¿ã—ã¾ã™ã€‚マイルストーンã®æ—¥ä»˜ã¯ã€ã‚¨ãƒ”ックã®èª²é¡Œã«å‰²ã‚Šå½“ã¦ã‚‰ã‚ŒãŸãƒžã‚¤ãƒ«ã‚¹ãƒˆãƒ¼ãƒ³ã«ç”±æ¥ã—ã¾ã™ã€‚日付を修正ã™ã‚‹ã“ã¨ã‚‚ã€å®Œå…¨ã«å‰Šé™¤ã™ã‚‹ã“ã¨ã‚‚ã§ãã¾ã™ã€‚"
msgid "Epics|This will also remove any descendents of %{bStart}%{targetEpicTitle}%{bEnd} from %{bStart}%{parentEpicTitle}%{bEnd}. Are you sure?"
-msgstr ""
+msgstr "%{bStart}%{parentEpicTitle}%{bEnd} ã‹ã‚‰ %{bStart}%{targetEpicTitle}%{bEnd} 以下ã®å…¨ã¦ã®èª²é¡Œã‚’削除ã—ã¾ã™ã€‚よã‚ã—ã„ã§ã™ã‹ï¼Ÿ"
msgid "Epics|To schedule your epic's %{epicDateType} date based on milestones, assign a milestone with a %{epicDateType} date to any issue in the epic."
msgstr "マイルストーンã«åŸºã¥ã„ãŸã‚¨ãƒ”ック㮠%{epicDateType} をスケジューリングã™ã‚‹ã«ã¯ã€ã‚¨ãƒ”ックã®èª²é¡Œã« %{epicDateType} ã®ãƒžã‚¤ãƒ«ã‚¹ãƒˆãƒ¼ãƒ³ã‚’割り当ã¦ã¾ã™ã€‚"
@@ -8089,7 +8310,7 @@ msgid "Error creating label."
msgstr ""
msgid "Error deleting %{issuableType}"
-msgstr ""
+msgstr "%{issuableType} ã®å‰Šé™¤ã‚¨ãƒ©ãƒ¼"
msgid "Error deleting project. Check logs for error details."
msgstr ""
@@ -8106,6 +8327,9 @@ msgstr "ラベルã®å–得中ã«ã‚¨ãƒ©ãƒ¼ãŒç™ºç”Ÿã—ã¾ã—ãŸã€‚"
msgid "Error fetching network graph."
msgstr "ãƒãƒƒãƒˆãƒ¯ãƒ¼ã‚¯ã‚°ãƒ©ãƒ•ã®å–得中ã«ã‚¨ãƒ©ãƒ¼ãŒç™ºç”Ÿã—ã¾ã—ãŸã€‚"
+msgid "Error fetching payload data."
+msgstr ""
+
msgid "Error fetching projects"
msgstr ""
@@ -8115,9 +8339,6 @@ msgstr "å‚ç…§ã®å–得中ã«ã‚¨ãƒ©ãƒ¼ãŒç™ºç”Ÿ"
msgid "Error fetching the dependency list. Please check your network connection and try again."
msgstr ""
-msgid "Error fetching usage ping data."
-msgstr "Ping データå–得中ã«ã‚¨ãƒ©ãƒ¼ãŒç™ºç”Ÿã—ã¾ã—ãŸã€‚"
-
msgid "Error loading branch data. Please try again."
msgstr "ブランムデータã®èª­ã¿è¾¼ã¿ä¸­ã«ã‚¨ãƒ©ãƒ¼ãŒç™ºç”Ÿã—ã¾ã—ãŸã€‚ã‚‚ã†ä¸€åº¦ã‚„ã‚Šç›´ã—ã¦ãã ã•ã„。"
@@ -8137,7 +8358,7 @@ msgid "Error loading last commit."
msgstr "ç›´è¿‘ã®ã‚³ãƒŸãƒƒãƒˆã®èª­ã¿è¾¼ã¿ä¸­ã«ã‚¨ãƒ©ãƒ¼ãŒç™ºç”Ÿã—ã¾ã—ãŸã€‚"
msgid "Error loading markdown preview"
-msgstr ""
+msgstr "Markdown プレビューã®èª­ã¿è¾¼ã¿ã‚¨ãƒ©ãƒ¼"
msgid "Error loading merge requests."
msgstr "マージリクエストをロード中ã«ã‚¨ãƒ©ãƒ¼ãŒç™ºç”Ÿã—ã¾ã—ãŸã€‚"
@@ -8203,7 +8424,7 @@ msgid "Error setting up editor. Please try again."
msgstr ""
msgid "Error updating %{issuableType}"
-msgstr ""
+msgstr "%{issuableType} ã®æ›´æ–°ä¸­ã‚¨ãƒ©ãƒ¼"
msgid "Error updating status for all to-do items."
msgstr ""
@@ -8212,7 +8433,7 @@ msgid "Error updating status of to-do item."
msgstr ""
msgid "Error uploading file"
-msgstr ""
+msgstr "ファイルアップロードエラー"
msgid "Error uploading file: %{stripped}"
msgstr ""
@@ -8227,7 +8448,7 @@ msgid "Error while migrating %{upload_id}: %{error_message}"
msgstr ""
msgid "Error with Akismet. Please check the logs for more info."
-msgstr ""
+msgstr "Akismet ã§ã‚¨ãƒ©ãƒ¼ãŒç™ºç”Ÿã—ã¾ã—ãŸã€‚詳細ã«ã¤ã„ã¦ã¯ãƒ­ã‚°ã‚’確èªã—ã¦ãã ã•ã„。"
msgid "ErrorTracking|Active"
msgstr ""
@@ -8259,6 +8480,9 @@ msgstr "プロジェクトã®é¸æŠžã‚’有効ã«ã™ã‚‹ã«ã¯ã€æœ‰åŠ¹ãªèªè¨¼ãƒˆ
msgid "Errors"
msgstr "エラー"
+msgid "Errors:"
+msgstr ""
+
msgid "Estimated"
msgstr "見ç©"
@@ -8299,7 +8523,7 @@ msgid "Events in %{project_path}"
msgstr ""
msgid "Every %{action} attempt has failed: %{job_error_message}. Please try again."
-msgstr ""
+msgstr "ã™ã¹ã¦ã® %{action} ãŒå¤±æ•—ã—ã¾ã—ãŸï¼š %{job_error_message}。もã†ä¸€åº¦ã‚„ã‚Šç›´ã—ã¦ãã ã•ã„。"
msgid "Every day"
msgstr ""
@@ -8422,7 +8646,7 @@ msgid "Expiration policy for the Container Registry is a perfect solution for ke
msgstr ""
msgid "Expired"
-msgstr ""
+msgstr "有効期é™åˆ‡ã‚Œ"
msgid "Expired %{expiredOn}"
msgstr "%{expiredOn} ã«æœŸé™åˆ‡ã‚Œ"
@@ -8463,15 +8687,21 @@ msgstr "公開グループを検索"
msgid "Export as CSV"
msgstr "CSV å½¢å¼ã§ã‚¨ã‚¯ã‚¹ãƒãƒ¼ãƒˆ"
+msgid "Export group"
+msgstr ""
+
msgid "Export issues"
msgstr "課題をエクスãƒãƒ¼ãƒˆ"
msgid "Export project"
msgstr "プロジェクトã®ã‚¨ã‚¯ã‚¹ãƒãƒ¼ãƒˆ"
-msgid "Export this project with all its related data in order to move your project to a new GitLab instance. Once the export is finished, you can import the file from the \"New Project\" page."
+msgid "Export this group with all related data to a new GitLab instance. Once complete, you can import the data file from the \"New Group\" page."
msgstr ""
+msgid "Export this project with all its related data in order to move your project to a new GitLab instance. Once the export is finished, you can import the file from the \"New Project\" page."
+msgstr "ã‚ãªãŸã®ãƒ—ロジェクトを新ã—ã„GitLabインスタンスã«ç§»å‹•ã™ã‚‹ãŸã‚ã«ã€ã“ã®ãƒ—ロジェクトã¨ãã®é–¢é€£ãƒ‡ãƒ¼ã‚¿ã™ã¹ã¦ã‚’ã¨å…±ã«ã‚¨ã‚¯ã‚¹ãƒãƒ¼ãƒˆã—ã¾ã™ã€‚エクスãƒãƒ¼ãƒˆãŒå®Œäº†ã—ãŸã‚‰ã€\"æ–°ã—ã„プロジェクト\"ページã‹ã‚‰ãƒ•ã‚¡ã‚¤ãƒ«ã‚’インãƒãƒ¼ãƒˆã§ãã¾ã™ã€‚"
+
msgid "Export variable to pipelines running on protected branches and tags only."
msgstr ""
@@ -8509,25 +8739,25 @@ msgid "ExternalAuthorizationService|When no classification label is set the defa
msgstr "ラベルãŒåˆ†é¡žã•ã‚Œã¦ã„ãªã„ã¨ãã¯ã€`%{default_label}` ãŒæ—¢å®šã®ãƒ©ãƒ™ãƒ«ã¨ã—ã¦ä½¿ç”¨ã•ã‚Œã¾ã™ã€‚"
msgid "ExternalMetrics|Add a button to the metrics dashboard linking directly to your existing external dashboards."
-msgstr ""
+msgstr "既存ã®å¤–部ダッシュボードã«ç›´æŽ¥ãƒªãƒ³ã‚¯ã—ã¦ã„ã‚‹ã€ãƒ¡ãƒˆãƒªã‚¯ã‚¹ãƒ€ãƒƒã‚·ãƒ¥ãƒœãƒ¼ãƒ‰ã«ãƒœã‚¿ãƒ³ã‚’追加ã™ã‚‹ã€‚"
msgid "ExternalMetrics|Enter the URL of the dashboard you want to link to"
-msgstr ""
+msgstr "ã‚ãªãŸãŒãƒªãƒ³ã‚¯ã—ãŸã„ã€ãƒ€ãƒƒã‚·ãƒ¥ãƒœãƒ¼ãƒ‰ã®URLを入力ã—ã¦ãã ã•ã„"
msgid "ExternalMetrics|External Dashboard"
-msgstr ""
+msgstr "外部ã®ãƒ€ãƒƒã‚·ãƒ¥ãƒœãƒ¼ãƒ‰"
msgid "ExternalMetrics|Full dashboard URL"
-msgstr ""
+msgstr "完全ãªã€ãƒ€ãƒƒã‚·ãƒ¥ãƒœãƒ¼ãƒ‰URL"
msgid "ExternalWikiService|External Wiki"
-msgstr ""
+msgstr "外部Wiki"
msgid "ExternalWikiService|Replaces the link to the internal wiki with a link to an external wiki."
-msgstr ""
+msgstr "内部ã®wikiã¸ã®ãƒªãƒ³ã‚¯ã‚’外部ã®wikiã¸ã®ãƒªãƒ³ã‚¯ã«ç½®ãæ›ãˆã¾ã™ã€‚"
msgid "ExternalWikiService|The URL of the external Wiki"
-msgstr ""
+msgstr "外部Wikiã®URL"
msgid "Facebook"
msgstr "Facebook"
@@ -8538,9 +8768,6 @@ msgstr "失敗"
msgid "Failed Jobs"
msgstr "失敗ã—ãŸã‚¸ãƒ§ãƒ–"
-msgid "Failed create wiki"
-msgstr "wiki を作æˆã§ãã¾ã›ã‚“ã§ã—ãŸ"
-
msgid "Failed to add a Zoom meeting"
msgstr ""
@@ -8566,10 +8793,10 @@ msgid "Failed to check related branches."
msgstr "関連ã™ã‚‹ãƒ–ランãƒã®ç¢ºèªã«å¤±æ•—ã—ã¾ã—ãŸã€‚"
msgid "Failed to create Merge Request. Please try again."
-msgstr ""
+msgstr "マージリクエストã®ä½œæˆã«å¤±æ•—ã—ã¾ã—ãŸã€‚ã‚‚ã†ä¸€åº¦ãŠè©¦ã—ãã ã•ã„。"
msgid "Failed to create a branch for this issue. Please try again."
-msgstr ""
+msgstr "ã“ã®èª²é¡Œã®ãŸã‚ã®ãƒ–ランãƒã®ä½œæˆã«å¤±æ•—ã—ã¾ã—ãŸã€‚ã‚‚ã†ä¸€åº¦ã‚„ã‚Šç›´ã—ã¦ãã ã•ã„。"
msgid "Failed to create import label for jira import."
msgstr ""
@@ -8580,20 +8807,23 @@ msgstr ""
msgid "Failed to create resources"
msgstr "リソースã®ä½œæˆã«å¤±æ•—ã—ã¾ã—ãŸ"
+msgid "Failed to create wiki"
+msgstr ""
+
msgid "Failed to delete board. Please try again."
msgstr ""
msgid "Failed to deploy to"
-msgstr ""
+msgstr "デプロイã«å¤±æ•—ã—ã¾ã—ãŸ"
msgid "Failed to enqueue the rebase operation, possibly due to a long-lived transaction. Try again later."
msgstr ""
-msgid "Failed to find import label for jira import."
+msgid "Failed to find import label for Jira import."
msgstr ""
msgid "Failed to get ref."
-msgstr ""
+msgstr "ref ã‚’å–å¾—ã§ãã¾ã›ã‚“ã§ã—ãŸ"
msgid "Failed to install."
msgstr "インストールã«å¤±æ•—ã—ã¾ã—ãŸã€‚"
@@ -8613,6 +8843,12 @@ msgstr ""
msgid "Failed to load groups & users."
msgstr ""
+msgid "Failed to load labels. Please try again."
+msgstr ""
+
+msgid "Failed to load milestones. Please try again."
+msgstr ""
+
msgid "Failed to load related branches"
msgstr ""
@@ -8662,7 +8898,7 @@ msgid "Failed to reset key. Please try again."
msgstr "キーをリセットã§ãã¾ã›ã‚“ã§ã—ãŸã€‚ã‚‚ã†ä¸€åº¦ãŠè©¦ã—下ã•ã„。"
msgid "Failed to save merge conflicts resolutions. Please try again!"
-msgstr ""
+msgstr "マージã®ç«¶åˆã®è§£æ±ºã‚’ä¿å­˜ã§ãã¾ã›ã‚“ã§ã—ãŸã€‚ã‚‚ã†ä¸€åº¦ã‚„ã‚Šç›´ã—ã¦ãã ã•ã„。"
msgid "Failed to save new settings"
msgstr "æ–°ã—ã„設定をä¿å­˜ã§ãã¾ã›ã‚“ã§ã—ãŸ"
@@ -8698,7 +8934,7 @@ msgid "Failed to update."
msgstr "æ›´æ–°ã«å¤±æ•—ã—ã¾ã—ãŸã€‚"
msgid "Failed to upgrade."
-msgstr ""
+msgstr "アップグレードã«å¤±æ•—ã—ã¾ã—ãŸã€‚"
msgid "Failed to upload object map file"
msgstr "オブジェクトマップファイルをアップロードã§ãã¾ã›ã‚“ã§ã—ãŸã€‚"
@@ -8722,7 +8958,7 @@ msgid "Fast-forward merge without a merge commit"
msgstr ""
msgid "Faster as it re-uses the project workspace (falling back to clone if it doesn't exist)"
-msgstr ""
+msgstr "プロジェクトワークスペースをå†åˆ©ç”¨ã™ã‚‹ã‚ˆã†ã«é«˜é€ŸåŒ–(存在ã—ãªã„å ´åˆã¯ã€ã‚¯ãƒ­ãƒ¼ãƒ³ã‚’è¡Œã†)"
msgid "Faster releases. Better code. Less pain."
msgstr ""
@@ -8734,10 +8970,10 @@ msgid "Feature Flags"
msgstr "機能フラグ"
msgid "Feature flag was not removed."
-msgstr ""
+msgstr "機能フラグを削除ã—ã¾ã›ã‚“ã§ã—ãŸã€‚"
msgid "Feature flag was successfully removed."
-msgstr ""
+msgstr "機能フラグを正常ã«å‰Šé™¤ã—ã¾ã—ãŸã€‚"
msgid "FeatureFlags|* (All Environments)"
msgstr "* (ã™ã¹ã¦ã®ç’°å¢ƒ)"
@@ -8782,10 +9018,10 @@ msgid "FeatureFlags|Enable features for specific users and specific environments
msgstr ""
msgid "FeatureFlags|Environment Spec"
-msgstr ""
+msgstr "環境スペック"
msgid "FeatureFlags|Environment Specs"
-msgstr ""
+msgstr "環境スペック"
msgid "FeatureFlags|Feature Flag"
msgstr "機能フラグ"
@@ -8851,16 +9087,16 @@ msgid "FeatureFlags|Percent rollout must be a whole number between 0 and 100"
msgstr ""
msgid "FeatureFlags|Protected"
-msgstr ""
+msgstr "ä¿è­·ã•ã‚Œã¦ã„ã¾ã™"
msgid "FeatureFlags|Remove"
-msgstr ""
+msgstr "削除"
msgid "FeatureFlags|Rollout Percentage"
msgstr ""
msgid "FeatureFlags|Rollout Strategy"
-msgstr ""
+msgstr "ロールアウト戦略"
msgid "FeatureFlags|Status"
msgstr "ステータス"
@@ -8878,13 +9114,13 @@ msgid "FeatureFlags|There are no inactive feature flags"
msgstr ""
msgid "FeatureFlags|There was an error fetching the feature flags."
-msgstr ""
+msgstr "機能フラグã®å–得中ã«ã‚¨ãƒ©ãƒ¼ãŒç™ºç”Ÿã—ã¾ã—ãŸã€‚"
msgid "FeatureFlags|Try again in a few moments or contact your support team."
msgstr "ã—ã°ã‚‰ãã—ã¦ã‹ã‚‰ã‚‚ã†ä¸€åº¦è©¦ã™ã‹ã€ã‚µãƒãƒ¼ãƒˆãƒãƒ¼ãƒ ã«é€£çµ¡ã—ã¦ãã ã•ã„。"
msgid "FeatureFlags|User IDs"
-msgstr ""
+msgstr "ユーザーID"
msgid "FeatureFlag|Delete strategy"
msgstr ""
@@ -8908,13 +9144,13 @@ msgid "Fetching incoming email"
msgstr ""
msgid "Fetching licenses failed."
-msgstr ""
+msgstr "ライセンスã®å–得を失敗ã—ã¾ã—ãŸã€‚"
msgid "Fetching licenses failed. The request endpoint was not found."
-msgstr ""
+msgstr "ライセンスã®å–å¾—ã«å¤±æ•—ã—ã¾ã—ãŸã€‚è¦æ±‚エンドãƒã‚¤ãƒ³ãƒˆãŒè¦‹ã¤ã‹ã‚Šã¾ã›ã‚“。"
msgid "Fetching licenses failed. You are not permitted to perform this action."
-msgstr ""
+msgstr "ライセンスã®å–å¾—ã«å¤±æ•—ã—ã¾ã—ãŸã€‚ã‚ãªãŸã¯ã“ã‚Œã®å®Ÿè¡Œã‚’許å¯ã•ã‚Œã¦ã„ã¾ã›ã‚“。"
msgid "File"
msgstr ""
@@ -8938,7 +9174,7 @@ msgid "File hooks are similar to system hooks but are executed as files instead
msgstr ""
msgid "File mode changed from %{a_mode} to %{b_mode}"
-msgstr ""
+msgstr "ファイルã®ãƒ¢ãƒ¼ãƒ‰ã‚’ %{a_mode} ã‹ã‚‰ %{b_mode} ã«å¤‰æ›´"
msgid "File moved"
msgstr "ファイルを移動ã—ã¾ã—ãŸã€‚"
@@ -8988,7 +9224,10 @@ msgstr "コミットメッセージã§çµžã‚Šè¾¼ã¿"
msgid "Filter by milestone name"
msgstr "マイルストーンåã§çµžã‚Šè¾¼ã‚€"
-msgid "Filter by name..."
+msgid "Filter by name"
+msgstr ""
+
+msgid "Filter by status"
msgstr ""
msgid "Filter by two-factor authentication"
@@ -9000,12 +9239,18 @@ msgstr ""
msgid "Filter projects"
msgstr ""
+msgid "Filter results"
+msgstr ""
+
msgid "Filter results by group"
msgstr "グループã§çµžã‚Šè¾¼ã¿"
msgid "Filter results by project"
msgstr "プロジェクトã§çµžã‚Šè¾¼ã¿"
+msgid "Filter results..."
+msgstr ""
+
msgid "Filter your projects by name"
msgstr ""
@@ -9040,7 +9285,7 @@ msgid "Finish review"
msgstr "レビューを完了ã™ã‚‹"
msgid "Finish setting up your dedicated account for <strong>%{group_name}</strong>."
-msgstr ""
+msgstr "<strong>%{group_name} </strong>ã®å°‚用アカウントã®è¨­å®šã‚’完了ã—ã¦ãã ã•ã„。"
msgid "Finished"
msgstr "完了"
@@ -9061,13 +9306,13 @@ msgid "First seen"
msgstr ""
msgid "Fixed date"
-msgstr ""
+msgstr "日付を修正"
msgid "Fixed due date"
-msgstr ""
+msgstr "期日を修正"
msgid "Fixed start date"
-msgstr ""
+msgstr "修正開始日"
msgid "Fixed:"
msgstr "修正:"
@@ -9076,10 +9321,10 @@ msgid "Flags"
msgstr ""
msgid "FlowdockService|Flowdock Git source token"
-msgstr ""
+msgstr "Flowdock Gitソーストークン"
msgid "FlowdockService|Flowdock is a collaboration web app for technical teams."
-msgstr ""
+msgstr "Flowdockã¯ã€æŠ€è¡“ãƒãƒ¼ãƒ ç”¨ã®ã‚³ãƒ©ãƒœãƒ¬ãƒ¼ã‚·ãƒ§ãƒ³Webアプリã§ã™ã€‚"
msgid "FogBugz Email"
msgstr "FogBugz メール"
@@ -9112,7 +9357,7 @@ msgid "For each Jira issue successfully imported, we'll create a new GitLab issu
msgstr ""
msgid "For internal projects, any logged in user can view pipelines and access job details (output logs and artifacts)"
-msgstr ""
+msgstr "内部プロジェクトã®å ´åˆã€ãƒ­ã‚°ã‚¤ãƒ³ã—ã¦ã„ã‚‹ã™ã¹ã¦ã®ãƒ¦ãƒ¼ã‚¶ãƒ¼ã¯ãƒ‘イプラインを表示ã§ãã€ã‚¸ãƒ§ãƒ–ã®è©³ç´°(出力ログãŠã‚ˆã³æˆæžœç‰©) ã«ã‚¢ã‚¯ã‚»ã‚¹ã§ãã¾ã™ã€‚"
msgid "For more info, read the documentation."
msgstr "詳細ã«ã¤ã„ã¦ã¯ã€ãƒ‰ã‚­ãƒ¥ãƒ¡ãƒ³ãƒˆã‚’読んã§ãã ã•ã„。"
@@ -9121,7 +9366,7 @@ msgid "For more information, go to the "
msgstr ""
msgid "For more information, please review %{link_start_tag}Jaeger's configuration doc%{link_end_tag}"
-msgstr ""
+msgstr "詳細ã«ã¤ã„ã¦ã¯ã€ %{link_start_tag}aeger'sã®æ§‹æˆãƒ‰ã‚­ãƒ¥ãƒ¡ãƒ³ãƒˆ%{link_end_tag} ã‚’å‚ç…§ã—ã¦ãã ã•ã„。"
msgid "For more information, see the File Hooks documentation."
msgstr ""
@@ -9133,10 +9378,10 @@ msgid "For more information, see the documentation on %{link_start}disabling Sea
msgstr ""
msgid "For private projects, any member (guest or higher) can view pipelines and access job details (output logs and artifacts)"
-msgstr ""
+msgstr "プライベートãªãƒ—ロジェクトã®å ´åˆã€(ゲストã¾ãŸã¯ãれ以上ã®æ¨©é™ã®) ã™ã¹ã¦ã®ãƒ¡ãƒ³ãƒãƒ¼ãŒãƒ‘イプラインを表示ã—ã€ã‚¸ãƒ§ãƒ–ã®è©³ç´°(出力ログãŠã‚ˆã³æˆæžœç‰©) ã«ã‚¢ã‚¯ã‚»ã‚¹ã§ãã¾ã™ã€‚"
msgid "For public projects, anyone can view pipelines and access job details (output logs and artifacts)"
-msgstr ""
+msgstr "パブリックプロジェクトã®å ´åˆã€èª°ã§ã‚‚パイプラインを表示ã—ã€ã‚¸ãƒ§ãƒ–ã®è©³ç´° (出力ログãŠã‚ˆã³æˆæžœç‰©) ã«ã‚¢ã‚¯ã‚»ã‚¹ã§ãã¾ã™ã€‚"
msgid "Forgot your password?"
msgstr "パスワードを忘れã¾ã—ãŸã‹ï¼Ÿ"
@@ -9229,7 +9474,7 @@ msgid "From the Kubernetes cluster details view, install Runner from the applica
msgstr "Kubernetes クラスターã®è©³ç´°ç”»é¢ã‚’介ã—ã¦ã€ã‚¢ãƒ—リケーションリストã‹ã‚‰ Runner をインストールã—ã¾ã™ã€‚"
msgid "Full name"
-msgstr ""
+msgstr "æ°å"
msgid "GPG Key ID:"
msgstr "GPG キー ID:"
@@ -9259,7 +9504,7 @@ msgid "Generate key"
msgstr "éµã‚’生æˆ"
msgid "Generate new export"
-msgstr ""
+msgstr "æ–°ã—ㄠエクスãƒãƒ¼ãƒˆã‚’生æˆ"
msgid "Geo"
msgstr "Geo"
@@ -9294,11 +9539,11 @@ msgstr ""
msgid "GeoNodes|Checksummed"
msgstr "ãƒã‚§ãƒƒã‚¯ã‚µãƒ "
-msgid "GeoNodes|Container repositories"
+msgid "GeoNodes|Consult Geo troubleshooting information"
msgstr ""
-msgid "GeoNodes|Data is out of date from %{timeago}"
-msgstr "データ㯠%{timeago} ã‹ã‚‰å¤ããªã£ã¦ã„ã¾ã™"
+msgid "GeoNodes|Container repositories"
+msgstr ""
msgid "GeoNodes|Data replication lag"
msgstr "データ レプリケーションã®é…延"
@@ -9339,6 +9584,9 @@ msgstr "カーソルã§å‡¦ç†ã•ã‚ŒãŸæœ€çµ‚イベント ID"
msgid "GeoNodes|Last event ID seen from primary"
msgstr "プライマリãŒå–å¾—ã—ãŸæœ€çµ‚イベント ID"
+msgid "GeoNodes|Learn more about Geo node statuses"
+msgstr ""
+
msgid "GeoNodes|Loading nodes"
msgstr "ノードã®èª­ã¿è¾¼ã¿ä¸­"
@@ -9354,6 +9602,9 @@ msgstr ""
msgid "GeoNodes|Node was successfully removed."
msgstr "ノードã®å‰Šé™¤ã«æˆåŠŸã—ã¾ã—ãŸã€‚"
+msgid "GeoNodes|Node's status was updated %{timeAgo}."
+msgstr ""
+
msgid "GeoNodes|Not checksummed"
msgstr "ãƒã‚§ãƒƒã‚¯ã‚µãƒ ãªã—"
@@ -9384,8 +9635,11 @@ msgstr "リãƒã‚¸ãƒˆãƒªãƒã‚§ãƒƒã‚¯ã‚µãƒ ã®é€²è¡ŒçŠ¶æ³"
msgid "GeoNodes|Repository verification progress"
msgstr "リãƒã‚¸ãƒˆãƒªæ¤œè¨¼ã®é€²æ—状æ³"
-msgid "GeoNodes|Selective"
-msgstr "é¸æŠžçš„"
+msgid "GeoNodes|Selective (%{syncLabel})"
+msgstr ""
+
+msgid "GeoNodes|Selective synchronization"
+msgstr ""
msgid "GeoNodes|Something went wrong while changing node status"
msgstr "ノードã®çŠ¶æ…‹ã‚’変更中ã«å•é¡ŒãŒç™ºç”Ÿã—ã¾ã—ãŸ"
@@ -9411,6 +9665,9 @@ msgstr "未使用ã®ã‚¹ãƒ­ãƒƒãƒˆ"
msgid "GeoNodes|Unverified"
msgstr "未検証"
+msgid "GeoNodes|Updated %{timeAgo}"
+msgstr ""
+
msgid "GeoNodes|Used slots"
msgstr "使用済ã¿ã®ã‚¹ãƒ­ãƒƒãƒˆ"
@@ -9427,7 +9684,7 @@ msgid "GeoNodes|Wikis"
msgstr "Wiki"
msgid "GeoNodes|With %{geo} you can install a special read-only and replicated instance anywhere. Before you add nodes, follow the %{instructions} in the exact order they appear."
-msgstr ""
+msgstr "%{geo} を指定ã™ã‚‹ã¨ã€ç‰¹åˆ¥ãªèª­ã¿å–り専用ã®è¤‡è£½ã‚¤ãƒ³ã‚¹ã‚¿ãƒ³ã‚¹ã‚’ã©ã“ã«ã§ã‚‚インストールã§ãã¾ã™ã€‚ノードを追加ã™ã‚‹å‰ã«ã€ãれらãŒç¾ã‚Œã‚‹æ­£ç¢ºãªé †åºã§ %{instructions} ã«å¾“ã£ã¦ãã ã•ã„。"
msgid "GeoNodes|You have configured Geo nodes using an insecure HTTP connection. We recommend the use of HTTPS."
msgstr "安全ã§ãªã„ HTTP 接続を使用ã—㦠Geo ノードを設定ã—ã¾ã—ãŸã€‚HTTPS 通信ã®ä½¿ç”¨ã‚’推奨ã—ã¾ã™ã€‚"
@@ -9450,35 +9707,41 @@ msgstr ""
msgid "Geo|All"
msgstr "ã™ã¹ã¦"
+msgid "Geo|All %{replicable_type}"
+msgstr ""
+
msgid "Geo|All projects"
msgstr "ã™ã¹ã¦ã®ãƒ—ロジェクト"
-msgid "Geo|All projects are being scheduled for re-sync"
-msgstr "ã™ã¹ã¦ã®ãƒ—ロジェクトã®å†åŒæœŸãŒäºˆå®šã•ã‚Œã¦ã„ã¾ã™"
-
-msgid "Geo|All projects are being scheduled for re-verify"
+msgid "Geo|All projects are being scheduled for resync"
msgstr ""
-msgid "Geo|Batch operations"
-msgstr "ãƒãƒƒãƒå‡¦ç†"
+msgid "Geo|All projects are being scheduled for reverify"
+msgstr ""
msgid "Geo|Could not remove tracking entry for an existing project."
msgstr "既存ã®ãƒ—ロジェクトã®ãƒˆãƒ©ãƒƒã‚­ãƒ³ã‚°ã‚¨ãƒ³ãƒˆãƒªã‚’削除ã§ãã¾ã›ã‚“ã§ã—ãŸã€‚"
msgid "Geo|Could not remove tracking entry for an existing upload."
-msgstr ""
+msgstr "既存ã®ã‚¢ãƒƒãƒ—ロードã®ãƒˆãƒ©ãƒƒã‚­ãƒ³ã‚°ã‚¨ãƒ³ãƒˆãƒªã‚’削除ã§ãã¾ã›ã‚“ã§ã—ãŸã€‚"
msgid "Geo|Failed"
msgstr "失敗"
+msgid "Geo|Filter by status"
+msgstr ""
+
msgid "Geo|Geo Status"
msgstr "ジオステータス"
+msgid "Geo|In progress"
+msgstr ""
+
msgid "Geo|In sync"
msgstr "åŒæœŸ"
msgid "Geo|Last repository check run"
-msgstr ""
+msgstr "最後ã®ãƒªãƒã‚¸ãƒˆãƒªãƒã‚§ãƒƒã‚¯ã‚’実行"
msgid "Geo|Last successful sync"
msgstr "最後ã«æˆåŠŸã—ãŸåŒæœŸ"
@@ -9490,7 +9753,7 @@ msgid "Geo|Last time verified"
msgstr "å‰å›žã®ç¢ºèªæ—¥æ™‚"
msgid "Geo|Never"
-msgstr ""
+msgstr "決ã—ã¦"
msgid "Geo|Next sync scheduled at"
msgstr ""
@@ -9498,9 +9761,6 @@ msgstr ""
msgid "Geo|Not synced yet"
msgstr "ã¾ã åŒæœŸã—ã¦ã„ã¾ã›ã‚“。"
-msgid "Geo|Pending"
-msgstr ""
-
msgid "Geo|Pending synchronization"
msgstr ""
@@ -9537,8 +9797,8 @@ msgstr ""
msgid "Geo|Resync"
msgstr "å†åŒæœŸ"
-msgid "Geo|Resync all projects"
-msgstr "ã™ã¹ã¦ã®ãƒ—ロジェクトをå†åŒæœŸ"
+msgid "Geo|Resync all"
+msgstr ""
msgid "Geo|Retry count"
msgstr "リトライ回数"
@@ -9546,7 +9806,7 @@ msgstr "リトライ回数"
msgid "Geo|Reverify"
msgstr ""
-msgid "Geo|Reverify all projects"
+msgid "Geo|Reverify all"
msgstr ""
msgid "Geo|Status"
@@ -9556,7 +9816,7 @@ msgid "Geo|Synced"
msgstr "åŒæœŸæ¸ˆã¿"
msgid "Geo|Synced at"
-msgstr ""
+msgstr "åŒæœŸå…ˆ"
msgid "Geo|Synchronization failed - %{error}"
msgstr "åŒæœŸã«å¤±æ•—ã—ã¾ã—㟠- %{error}"
@@ -9583,13 +9843,13 @@ msgid "Geo|Verification failed - %{error}"
msgstr "åŒæœŸã«å¤±æ•—ã—ã¾ã—㟠- %{error}"
msgid "Geo|Waiting for scheduler"
-msgstr ""
+msgstr "スケジューラ待ã¡"
msgid "Geo|You are on a secondary, <b>read-only</b> Geo node. If you want to make changes, you must visit this page on the %{primary_node}."
-msgstr ""
+msgstr "ã‚ãªãŸã¯<b>読ã¿å–り専用ã®</b>セカンダリGeoノードã«ã„ã¾ã™ã€‚変更を加ãˆãŸã„å ´åˆã¯ã€%{primary_node} ã®ã“ã®ãƒšãƒ¼ã‚¸ã«ã‚¢ã‚¯ã‚»ã‚¹ã™ã‚‹å¿…è¦ãŒã‚ã‚Šã¾ã™ã€‚"
msgid "Geo|You are on a secondary, <b>read-only</b> Geo node. You may be able to make a limited amount of changes or perform a limited amount of actions on this page."
-msgstr ""
+msgstr "ã‚ãªãŸã¯<b>読ã¿å–り専用ã®</b>セカンダリGeoノードã«ã„ã¾ã™ã€‚ã“ã®ãƒšãƒ¼ã‚¸ã§ã¯é™å®šçš„ãªå¤‰æ›´ãƒ»æ“作をã™ã‚‹ã“ã¨ãŒã§ãã¾ã™ã€‚"
msgid "Geo|misconfigured"
msgstr "設定ãŒé–“é•ã£ã¦ã„ã¾ã™"
@@ -9625,7 +9885,7 @@ msgid "Git LFS is not enabled on this GitLab server, contact your admin."
msgstr "ã“ã®GitLabサーãƒãƒ¼ã¯Git LFSãŒç„¡åŠ¹ã«ãªã£ã¦ã„ã¾ã™ã€‚管ç†è€…ã«é€£çµ¡ã—ã¦ãã ã•ã„。"
msgid "Git LFS objects will be synced in pull mirrors if LFS is %{docs_link_start}enabled for the project%{docs_link_end}. They will <strong>not</strong> be synced in push mirrors."
-msgstr ""
+msgstr "LFS㌠%{docs_link_start} ã“ã®ãƒ—ロジェクトã§æœ‰åŠ¹ã«ãªã£ã¦ã„ã‚‹ %{docs_link_end} å ´åˆã«Git LFS オブジェクト プルミラーã«åŒæœŸã•ã‚Œã¾ã™ã€‚ã“れらã¯ãƒ—ッシュミラーã«åŒæœŸ <strong>ã—ã¾ã›ã‚“</strong> 。"
msgid "Git global setup"
msgstr "Git ã®ã‚°ãƒ­ãƒ¼ãƒãƒ«è¨­å®š"
@@ -9640,7 +9900,7 @@ msgid "Git shallow clone"
msgstr ""
msgid "Git strategy for pipelines"
-msgstr ""
+msgstr "パイプラインã®Git戦略"
msgid "Git version"
msgstr "Git ãƒãƒ¼ã‚¸ãƒ§ãƒ³"
@@ -9667,7 +9927,7 @@ msgid "GitLab Issue"
msgstr ""
msgid "GitLab Shared Runners execute code of different projects on the same Runner unless you configure GitLab Runner Autoscale with MaxBuilds 1 (which it is on GitLab.com)."
-msgstr ""
+msgstr "GitLab Shared Runnerã¯ã€GitLab Runnerã®ã‚ªãƒ¼ãƒˆã‚¹ã‚±ãƒ¼ãƒ«è¨­å®šã§MaxBuildsã‚’1 (GitLab.com上ã§ã¯ã“ã®è¨­å®š)ã—ãªã„é™ã‚Šã€åŒã˜Runnerã§åˆ¥ã®ãƒ—ロジェクトã®ã‚³ãƒ¼ãƒ‰ã‚’実行ã—ã¾ã™ã€‚"
msgid "GitLab Support Bot"
msgstr ""
@@ -9679,7 +9939,7 @@ msgid "GitLab User"
msgstr "GitLab ユーザー"
msgid "GitLab allows you to continue using your license even if you exceed the number of seats you purchased. You will be required to pay for these seats when you renew your license."
-msgstr ""
+msgstr "GitLabã§ã¯ã€è³¼å…¥ã—ãŸã‚·ãƒ¼ãƒˆæ•°ã‚’超ãˆã¦ã‚‚ライセンスを使用ã—続ã‘ã‚‹ã“ã¨ãŒã§ãã¾ã™ã€‚ライセンス更新時ã«ã€ã“れらã®ã‚·ãƒ¼ãƒˆã®æ–™é‡‘を支払ã†å¿…è¦ãŒã‚ã‚Šã¾ã™ã€‚"
msgid "GitLab commit"
msgstr ""
@@ -9715,7 +9975,7 @@ msgid "GitLab uses %{jaeger_link} to monitor distributed systems."
msgstr ""
msgid "GitLab will run a background job that will produce pseudonymized CSVs of the GitLab database that will be uploaded to your configured object storage directory."
-msgstr ""
+msgstr "Gitlabã¯ã€ã‚ãªãŸãŒè¨­å®šã—ãŸã‚ªãƒ–ジェクトストレージディレクトリã«ã‚¢ãƒƒãƒ—ロードã™ã‚‹ Gitlabデータベースã®åŒ¿å化ã—ãŸCSVを生æˆã™ã‚‹ã‚¸ãƒ§ãƒ–ã‚’ãƒãƒƒã‚¯ã‚°ãƒ©ã‚¦ãƒ³ãƒ‰ã§å®Ÿè¡Œã—ã¾ã™ã€‚"
msgid "GitLab.com import"
msgstr "GitLab.com インãƒãƒ¼ãƒˆ"
@@ -9844,13 +10104,13 @@ msgid "Global Shortcuts"
msgstr ""
msgid "Global notification settings"
-msgstr ""
+msgstr "グローãƒãƒ«é€šçŸ¥è¨­å®š"
msgid "Go Back"
msgstr "戻る"
msgid "Go Micro is a framework for micro service development."
-msgstr ""
+msgstr "Go Microã¯ã€ãƒžã‚¤ã‚¯ãƒ­ã‚µãƒ¼ãƒ“ス開発ã®ãŸã‚ã®ãƒ•ãƒ¬ãƒ¼ãƒ ãƒ¯ãƒ¼ã‚¯ã§ã™ã€‚"
msgid "Go back"
msgstr "å‰ã«æˆ»ã‚‹"
@@ -9868,7 +10128,7 @@ msgid "Go to"
msgstr ""
msgid "Go to %{link_to_google_takeout}."
-msgstr ""
+msgstr "%{link_to_google_takeout} ã«ç§»å‹•ã—ã¾ã™ã€‚"
msgid "Go to Pipelines"
msgstr ""
@@ -9888,9 +10148,6 @@ msgstr ""
msgid "Go to file"
msgstr ""
-msgid "Go to file (MRs only)"
-msgstr ""
-
msgid "Go to file permalink (while viewing a file)"
msgstr ""
@@ -9997,7 +10254,7 @@ msgid "Grafana URL"
msgstr ""
msgid "GrafanaIntegration|API Token"
-msgstr ""
+msgstr "APIトークン"
msgid "GrafanaIntegration|Active"
msgstr ""
@@ -10006,16 +10263,16 @@ msgid "GrafanaIntegration|Embed Grafana charts in GitLab issues."
msgstr ""
msgid "GrafanaIntegration|Enter the Grafana API Token."
-msgstr ""
+msgstr "Grafana API トークンを入力。"
msgid "GrafanaIntegration|Enter the base URL of the Grafana instance."
-msgstr ""
+msgstr "Grafana インスタンスã®ãƒ™ãƒ¼ã‚¹ URL を入力ã—ã¾ã™ã€‚"
msgid "GrafanaIntegration|Grafana Authentication"
-msgstr ""
+msgstr "Grafana èªè¨¼"
msgid "GrafanaIntegration|Grafana URL"
-msgstr ""
+msgstr "Grafana URL"
msgid "Grant access"
msgstr "アクセス許å¯"
@@ -10033,10 +10290,10 @@ msgid "Group"
msgstr "グループ"
msgid "Group %{group_name} was scheduled for deletion."
-msgstr ""
+msgstr "グループ %{group_name} ã¯å‰Šé™¤äºˆå®šã—ã¾ã—ãŸã€‚"
msgid "Group %{group_name} was successfully created."
-msgstr ""
+msgstr "'%{group_name}' グループã¯æ­£å¸¸ã«ä½œæˆã•ã‚Œã¾ã—ãŸã€‚"
msgid "Group Audit Events"
msgstr ""
@@ -10132,7 +10389,7 @@ msgid "Group variables (inherited)"
msgstr ""
msgid "Group was successfully updated."
-msgstr ""
+msgstr "グループã¯æ­£å¸¸ã«æ›´æ–°ã•ã‚Œã¾ã—ãŸã€‚"
msgid "Group: %{group_name}"
msgstr "グループ:%{group_name}"
@@ -10174,16 +10431,16 @@ msgid "GroupRoadmap|The roadmap shows the progress of your epics along a timelin
msgstr "ロードマップã¯æ™‚é–“ã®çµŒéŽã¨ã¨ã‚‚ã«ã‚¨ãƒ”ックã®é€²è¡Œã‚’表示ã—ã¾ã™"
msgid "GroupRoadmap|To view the roadmap, add a start or due date to one of the %{linkStart}child epics%{linkEnd}."
-msgstr ""
+msgstr "ロードマップを表示ã™ã‚‹ã«ã¯ã€é–‹å§‹æ—¥ã¾ãŸã¯ã„ãšã‚Œã‹ã® %{linkStart}å­ã‚¨ãƒ”ック%{linkEnd} ã®æœŸæ—¥ã‚’追加ã—ã¾ã™ã€‚"
msgid "GroupRoadmap|To view the roadmap, add a start or due date to one of your epics in this group or its subgroups; from %{startDate} to %{endDate}."
-msgstr ""
+msgstr "ロードマップを表示ã™ã‚‹ã«ã¯ã€ã“ã®ã‚°ãƒ«ãƒ¼ãƒ—ã¾ãŸã¯ãã®ã‚µãƒ–グループã®ã‚¨ãƒ”ックã®ã„ãšã‚Œã‹ã«é–‹å§‹æ—¥ã¾ãŸã¯æœŸæ—¥ã‚’追加ã—ã¾ã™ã€‚ %{startDate} ã‹ã‚‰ %{endDate} ã¾ã§"
msgid "GroupRoadmap|To widen your search, change or remove filters; from %{startDate} to %{endDate}."
-msgstr ""
+msgstr "検索範囲を広ã’ã‚‹ã«ã¯ã€ãƒ•ã‚£ãƒ«ã‚¿ã‚’変更ã¾ãŸã¯å‰Šé™¤ã—ã¾ã™ã€‚ %{startDate} ã‹ã‚‰ %{endDate} ã¾ã§"
msgid "GroupSAML|Certificate fingerprint"
-msgstr ""
+msgstr "証明書ã®ãƒ•ã‚£ãƒ³ã‚¬ãƒ¼ãƒ—リント"
msgid "GroupSAML|Configuration"
msgstr ""
@@ -10192,25 +10449,25 @@ msgid "GroupSAML|Copy SAML Response XML"
msgstr ""
msgid "GroupSAML|Enable SAML authentication for this group."
-msgstr ""
+msgstr "ã“ã®ã‚°ãƒ«ãƒ¼ãƒ—ã® SAML èªè¨¼ã‚’有効ã«ã™ã‚‹."
msgid "GroupSAML|Enforce SSO-only authentication for this group."
-msgstr ""
+msgstr "ã“ã®ã‚°ãƒ«ãƒ¼ãƒ—ã«SSOã®ã¿ã®èªè¨¼ã‚’強制ã™ã‚‹."
msgid "GroupSAML|Enforce SSO-only membership for this group."
msgstr ""
msgid "GroupSAML|Enforce users to have dedicated group managed accounts for this group."
-msgstr ""
+msgstr "ã“ã®ã‚°ãƒ«ãƒ¼ãƒ—専用ã®ã‚°ãƒ«ãƒ¼ãƒ—管ç†ã‚¢ã‚«ã‚¦ãƒ³ãƒˆã‚’ユーザーã«è¨­å®šã™ã‚‹ã€‚"
msgid "GroupSAML|Enforced SSO"
-msgstr ""
+msgstr "強制SSO"
msgid "GroupSAML|Generate a SCIM token"
-msgstr ""
+msgstr "SCIMトークンを生æˆ"
msgid "GroupSAML|Generate a SCIM token to set up your System for Cross-Domain Identity Management."
-msgstr ""
+msgstr "クロスドメインã®ID管ç†ç”¨ã«ã‚·ã‚¹ãƒ†ãƒ ã‚’設定ã™ã‚‹ãŸã‚ã®SCIMトークンを生æˆã—ã¾ã™ã€‚"
msgid "GroupSAML|Identity"
msgstr ""
@@ -10219,16 +10476,16 @@ msgid "GroupSAML|Identity provider single sign on URL"
msgstr "アイデンティティプロãƒã‚¤ãƒ€ã®ã‚·ãƒ³ã‚°ãƒ«ã‚µã‚¤ãƒ³ã‚ªãƒ³ URL"
msgid "GroupSAML|Make sure you save this token — you won't be able to access it again."
-msgstr ""
+msgstr "ã“ã®ãƒˆãƒ¼ã‚¯ãƒ³ã¯å¿…ãšä¿å­˜ã—ã¦ãã ã•ã„。二度ã¨ã‚¢ã‚¯ã‚»ã‚¹ã§ãã¾ã›ã‚“。"
msgid "GroupSAML|Manage your group’s membership while adding another level of security with SAML."
-msgstr ""
+msgstr "グループã®ãƒ¡ãƒ³ãƒãƒ¼ã‚·ãƒƒãƒ—を管ç†ã—ãªãŒã‚‰ã€SAML ã§åˆ¥ã®ã‚»ã‚­ãƒ¥ãƒªãƒ†ã‚£ãƒ¬ãƒ™ãƒ«ã‚’追加ã—ã¾ã™ã€‚"
msgid "GroupSAML|Members"
msgstr ""
msgid "GroupSAML|Members will be forwarded here when signing in to your group. Get this from your identity provider, where it can also be called \"SSO Service Location\", \"SAML Token Issuance Endpoint\", or \"SAML 2.0/W-Federation URL\"."
-msgstr ""
+msgstr "メンãƒãƒ¼ãŒã‚°ãƒ«ãƒ¼ãƒ—ã«ã‚µã‚¤ãƒ³ã‚¤ãƒ³ã™ã‚‹ã¨ã€ã“ã“ã«ãƒ•ã‚©ãƒ¯ãƒ¼ãƒ‰ã•ã‚Œã¾ã™ã€‚ã“ã‚Œã¯IDプロãƒã‚¤ãƒ€ãƒ¼ã‹ã‚‰å–å¾—ã§ãã¾ã™ã€‚IDプロãƒã‚¤ãƒ€ãƒ¼ã¯\"SSO Service Location\"ã€\"SAML Token Issuance Endpoint\"ã€\"SAML 2.0/W-Federation URL\"ãªã©ã¨ã‚‚呼ã°ã‚Œã¾ã™ã€‚"
msgid "GroupSAML|NameID"
msgstr ""
@@ -10255,7 +10512,7 @@ msgid "GroupSAML|SAML Single Sign On Settings"
msgstr "SAML シングル サインオンã®è¨­å®š"
msgid "GroupSAML|SCIM API endpoint URL"
-msgstr ""
+msgstr "SCIM APIエンドãƒã‚¤ãƒ³ãƒˆã®URL"
msgid "GroupSAML|SCIM Token"
msgstr "SCIMトークン"
@@ -10264,19 +10521,19 @@ msgid "GroupSAML|SHA1 fingerprint of the SAML token signing certificate. Get thi
msgstr "SAML トークン㮠SHA1 フィンガープリントã§è¨¼æ˜Žæ›¸ã«ç½²åã—ã¾ã™ã€‚ã“ã‚Œã¯ã‚µãƒ ãƒ—リントã¨å‘¼ã°ã‚Œã‚¢ã‚¤ãƒ‡ãƒ³ãƒ†ã‚£ãƒ†ã‚£ãƒ—ロãƒã‚¤ãƒ€ãƒ¼ã‹ã‚‰å–å¾—ã§ãã¾ã™ã€‚"
msgid "GroupSAML|The SCIM token is now hidden. To see the value of the token again, you need to "
-msgstr ""
+msgstr "SCIMトークンã¯éžè¡¨ç¤ºã«ãªã£ã¦ã„ã¾ã™ã€‚トークンã®å€¤ã‚’ã‚‚ã†ä¸€åº¦ç¢ºèªã™ã‚‹ã«ã¯ã€æ¬¡ã®æ‰‹é †ã‚’実行ã™ã‚‹å¿…è¦ãŒã‚ã‚Šã¾ã™ã€‚ "
msgid "GroupSAML|To be able to enable enforced SSO, you first need to enable SAML authentication."
-msgstr ""
+msgstr "強制ã•ã‚ŒãŸSSOを有効化ã«ã™ã‚‹ãŸã‚ã«ã¯ã€ã¾ãšSAMLèªè¨¼ã‚’有効ã«ã—ã¾ã™ã€‚"
msgid "GroupSAML|To be able to enable group managed accounts, you first need to enable enforced SSO."
-msgstr ""
+msgstr "グループ管ç†ã‚¢ã‚«ã‚¦ãƒ³ãƒˆã‚’有効ã«ã™ã‚‹ãŸã‚ã«ã¯ã€ã¾ãšå¼·åˆ¶SSOを有効化ã—ã¾ã™ã€‚"
msgid "GroupSAML|To be able to prohibit outer forks, you first need to enforce dedicate group managed accounts."
msgstr ""
msgid "GroupSAML|Toggle SAML authentication"
-msgstr ""
+msgstr "SAMLèªè¨¼ã‚’切り替ãˆ"
msgid "GroupSAML|Valid SAML Response"
msgstr ""
@@ -10335,6 +10592,9 @@ msgstr ""
msgid "GroupSettings|Disable group mentions"
msgstr ""
+msgid "GroupSettings|Export group"
+msgstr ""
+
msgid "GroupSettings|If the parent group's visibility is lower than the group current visibility, visibility levels for subgroups and projects will be changed to match the new parent group's visibility."
msgstr ""
@@ -10363,10 +10623,10 @@ msgid "GroupSettings|Select a sub-group as the custom project template source fo
msgstr "ã“ã®ã‚°ãƒ«ãƒ¼ãƒ—ã®ã‚«ã‚¹ã‚¿ãƒ ãƒ—ロジェクトテンプレートソースã¨ã—ã¦ã‚µãƒ–グループをé¸æŠž"
msgid "GroupSettings|The Auto DevOps pipeline will run if no alternative CI configuration file is found."
-msgstr ""
+msgstr "代替㮠CI 設定ファイルãŒè¦‹ã¤ã‹ã‚‰ãªã„å ´åˆã¯ã€Auto DevOps パイプラインãŒå®Ÿè¡Œã•ã‚Œã¾ã™ã€‚"
msgid "GroupSettings|There was a problem updating Auto DevOps pipeline: %{error_messages}."
-msgstr ""
+msgstr "Auto DevOps パイプラインã®æ›´æ–°ä¸­ã«å•é¡ŒãŒç™ºç”Ÿã—ã¾ã—ãŸï¼š %{error_messages} 。"
msgid "GroupSettings|There was a problem updating the pipeline settings: %{error_messages}."
msgstr ""
@@ -10411,6 +10671,9 @@ msgid "Groups"
msgstr "グループ"
msgid "Groups (%{count})"
+msgstr "グループ (%{count})"
+
+msgid "Groups (%{groups})"
msgstr ""
msgid "Groups can also be nested by creating %{subgroup_docs_link_start}subgroups%{subgroup_docs_link_end}."
@@ -10438,10 +10701,10 @@ msgid "GroupsDropdown|Search your groups"
msgstr "ã‚ãªãŸã®ã‚°ãƒ«ãƒ¼ãƒ—を検索"
msgid "GroupsDropdown|Something went wrong on our end."
-msgstr ""
+msgstr "エラーãŒç™ºç”Ÿã—ã¾ã—ãŸ"
msgid "GroupsDropdown|Sorry, no groups matched your search"
-msgstr ""
+msgstr "ã™ã¿ã¾ã›ã‚“。検索ã«é©åˆã™ã‚‹ã‚°ãƒ«ãƒ¼ãƒ—ãŒå­˜åœ¨ã—ã¾ã›ã‚“。"
msgid "GroupsDropdown|This feature requires browser localStorage support"
msgstr "ã“ã®æ©Ÿèƒ½ã«ã¯ãƒ–ラウザー㫠localStorage サãƒãƒ¼ãƒˆãŒå¿…è¦ã§ã™"
@@ -10480,10 +10743,10 @@ msgid "GroupsTree|Loading groups"
msgstr "グループã®èª­ã¿è¾¼ã¿ä¸­"
msgid "GroupsTree|No groups matched your search"
-msgstr ""
+msgstr "検索ã«é©åˆã™ã‚‹ã‚°ãƒ«ãƒ¼ãƒ—ã¯ã‚ã‚Šã¾ã›ã‚“"
msgid "GroupsTree|No groups or projects matched your search"
-msgstr ""
+msgstr "検索ã«é©åˆã™ã‚‹ãƒ—ロジェクトã€ã‚°ãƒ«ãƒ¼ãƒ—ãŒã‚ã‚Šã¾ã›ã‚“"
msgid "GroupsTree|Search by name"
msgstr "åå‰ã§æ¤œç´¢"
@@ -10492,7 +10755,7 @@ msgid "Guideline"
msgstr ""
msgid "HTTP Basic: Access denied\\nYou must use a personal access token with 'api' scope for Git over HTTP.\\nYou can generate one at %{profile_personal_access_tokens_url}"
-msgstr ""
+msgstr "HTTP Basic: ã‚¢ã‚¯ã‚»ã‚¹æ‹’å¦ \\n Git over HTTPã«ã¯ 'api' スコープã®ãƒ‘ーソナルアクセストークンを使用ã™ã‚‹å¿…è¦ãŒã‚ã‚Šã¾ã™ã€‚\\n %{profile_personal_access_tokens_url} ã§ç”Ÿæˆã§ãã¾ã™"
msgid "Hashed Storage must be enabled to use Geo"
msgstr ""
@@ -10504,7 +10767,7 @@ msgid "Have your users email"
msgstr "ユーザーã«ãƒ¡ãƒ¼ãƒ«ã‚’é€ä¿¡"
msgid "Header logo was successfully removed."
-msgstr ""
+msgstr "ヘッダーロゴã¯æ­£å¸¸ã«å‰Šé™¤ã—ã¾ã—ãŸã€‚"
msgid "Header message"
msgstr "ヘッダーメッセージ"
@@ -10583,7 +10846,7 @@ msgid "Hide payload"
msgstr "ペイロードを隠ã™"
msgid "Hide shared projects"
-msgstr ""
+msgstr "共有プロジェクトをéžè¡¨ç¤ºã«"
msgid "Hide stage"
msgstr ""
@@ -10605,7 +10868,7 @@ msgid "Highest number of requests per minute for each raw path, default to 300.
msgstr ""
msgid "Highest role:"
-msgstr ""
+msgstr "最高ä½:"
msgid "History"
msgstr "履歴"
@@ -10620,7 +10883,7 @@ msgid "Hook was successfully created."
msgstr "フックã¯æ­£å¸¸ã«ä½œæˆã•ã‚Œã¾ã—ãŸã€‚"
msgid "Hook was successfully updated."
-msgstr ""
+msgstr "フックã¯æ­£å¸¸ã«æ›´æ–°ã—ã¾ã—ãŸã€‚"
msgid "Hour (UTC)"
msgstr ""
@@ -10641,10 +10904,10 @@ msgid "How many days need to pass between marking entity for deletion and actual
msgstr ""
msgid "How many replicas each Elasticsearch shard has."
-msgstr ""
+msgstr "å„Elasticsearchシャードã®ãƒ¬ãƒ—リカ数"
msgid "How many shards to split the Elasticsearch index over."
-msgstr ""
+msgstr "Elasticseachã®ã‚¤ãƒ³ãƒ‡ãƒƒã‚¯ã‚¹ã‚’分割ã™ã‚‹ã‚·ãƒ£ãƒ¼ãƒ‰æ•°"
msgid "How many users will be evaluating the trial?"
msgstr ""
@@ -10676,15 +10939,12 @@ msgstr "ID"
msgid "ID:"
msgstr ""
-msgid "IDE|Allow live previews of JavaScript projects in the Web IDE using CodeSandbox client side evaluation."
-msgstr "CodeSandbox クライアントサイド評価を使用ã—ã¦ã€Web IDE 㧠JavaScript プロジェクトã®ãƒ©ã‚¤ãƒ–プレビューを許å¯ã—ã¾ã™ã€‚"
+msgid "IDE|Allow live previews of JavaScript projects in the Web IDE using CodeSandbox Live Preview."
+msgstr ""
msgid "IDE|Back"
msgstr "戻る"
-msgid "IDE|Client side evaluation"
-msgstr "クライアントサイド評価"
-
msgid "IDE|Commit"
msgstr "コミット"
@@ -10748,6 +11008,9 @@ msgstr ""
msgid "If any job surpasses this timeout threshold, it will be marked as failed. Human readable time input language is accepted like \"1 hour\". Values without specification represent seconds."
msgstr ""
+msgid "If blank, set allowable lifetime to %{instance_level_policy_in_words}, as defined by the instance admin. Once set, existing tokens for users in this group may be revoked."
+msgstr ""
+
msgid "If checked, group owners can manage LDAP group links and LDAP member overrides"
msgstr ""
@@ -10761,10 +11024,10 @@ msgid "If disabled, only admins will be able to configure repository mirroring."
msgstr ""
msgid "If disabled, the access level will depend on the user's permissions in the project."
-msgstr ""
+msgstr "無効ã«ã™ã‚‹ã¨ã€ã‚¢ã‚¯ã‚»ã‚¹ãƒ¬ãƒ™ãƒ«ã¯ãã®ãƒ—ロジェクトã®ãƒ¦ãƒ¼ã‚¶ãƒ¼ã®æ¨©é™ã«ä¾å­˜ã—ã¾ã™ã€‚"
msgid "If enabled"
-msgstr ""
+msgstr "有効ã«ã—ãŸå ´åˆ"
msgid "If enabled, access to projects will be validated on an external service using their classification label."
msgstr "有効ã«è¨­å®šã—ãŸå ´åˆã€å¤–部サービスã‹ã‚‰ãƒ—ロジェクトã¸ã®ã‚¢ã‚¯ã‚»ã‚¹ãŒåˆ†é¡žãƒ©ãƒ™ãƒ«ã‚’使用ã—ã¦åˆ¶å¾¡ã•ã‚Œã¾ã™ã€‚"
@@ -10776,7 +11039,7 @@ msgid "If this was a mistake you can %{leave_link_start}leave the %{source_type}
msgstr ""
msgid "If this was a mistake you can leave the %{source_type}."
-msgstr ""
+msgstr "ã“ã‚ŒãŒé–“é•ã„ã ã£ãŸå ´åˆã¯ %{source_type} ã‚’ãã®ã¾ã¾ã«ã§ãã¾ã™ã€‚"
msgid "If using GitHub, you’ll see pipeline statuses on GitHub for your commits and pull requests. %{more_info_link}"
msgstr "GitHub を使用ã—ã¦ã„ã‚‹å ´åˆã€GitHub 上ã®ã‚³ãƒŸãƒƒãƒˆã‚„プルリクエストã‹ã‚‰ãƒ‘イプラインã®çŠ¶æ…‹ã‚’確èªã™ã‚‹ã“ã¨ãŒã§ãã¾ã™ã€‚%{more_info_link}"
@@ -10785,6 +11048,9 @@ msgid "If you believe this may be an error, please refer to the %{linkStart}Geo
msgstr ""
msgid "If you lose your recovery codes you can generate new ones, invalidating all previous codes."
+msgstr "リカãƒãƒªãƒ¼ã‚³ãƒ¼ãƒ‰ã‚’紛失ã—ãŸå ´åˆã¯ã€æ–°ã—ã„リカãƒãƒªãƒ¼ã‚³ãƒ¼ãƒ‰ã‚’生æˆã—ã¦ã€ä»¥å‰ã®ãƒªã‚«ãƒãƒªãƒ¼ã‚³ãƒ¼ãƒ‰ã‚’ã™ã¹ã¦ç„¡åŠ¹ã«ã™ã‚‹ã“ã¨ãŒã§ãã¾ã™ã€‚"
+
+msgid "If you reach 100%% storage capacity, you will not be able to: %{base_message}"
msgstr ""
msgid "If your HTTP repository is not publicly accessible, add your credentials."
@@ -10833,7 +11099,7 @@ msgid "Import Projects from Gitea"
msgstr "Gitea ã‹ã‚‰ãƒ—ロジェクトã®ã‚¤ãƒ³ãƒãƒ¼ãƒˆ"
msgid "Import all compatible projects"
-msgstr ""
+msgstr "å…¨ã¦ã®äº’æ›æ€§ã®ã‚るプロジェクトã®ã‚¤ãƒ³ãƒãƒ¼ãƒˆ"
msgid "Import all projects"
msgstr "全プロジェクトã®ã‚¤ãƒ³ãƒãƒ¼ãƒˆ"
@@ -10898,35 +11164,38 @@ msgstr "リãƒã‚¸ãƒˆãƒªã‚’ GitHub ã‹ã‚‰ã‚¤ãƒ³ãƒãƒ¼ãƒˆã™ã‚‹"
msgid "Import repository"
msgstr "リãƒã‚¸ãƒˆãƒªã‚’インãƒãƒ¼ãƒˆ"
-msgid "Import tasks"
+msgid "Import started by: %{importInitiator}"
msgstr ""
+msgid "Import tasks"
+msgstr "タスクã®ã‚¤ãƒ³ãƒãƒ¼ãƒˆ"
+
msgid "Import tasks from Phabricator into issues"
-msgstr ""
+msgstr "Phabricator ã‹ã‚‰èª²é¡Œã¸ã‚¿ã‚¹ã‚¯ã‚’インãƒãƒ¼ãƒˆã™ã‚‹"
msgid "Import timed out. Import took longer than %{import_jobs_expiration} seconds"
msgstr "インãƒãƒ¼ãƒˆãŒã‚¿ã‚¤ãƒ ã‚¢ã‚¦ãƒˆã—ã¾ã—ãŸã€‚インãƒãƒ¼ãƒˆã« %{import_jobs_expiration} 秒以上ã‹ã‹ã‚Šã¾ã—ãŸ"
msgid "Import/Export illustration"
-msgstr ""
+msgstr "インãƒãƒ¼ãƒˆ/エクスãƒãƒ¼ãƒˆã®èª¬æ˜Ž"
msgid "ImportButtons|Connect repositories from"
msgstr "リãƒã‚¸ãƒˆãƒªã¸æŽ¥ç¶š"
msgid "ImportProjects|Blocked import URL: %{message}"
-msgstr ""
+msgstr "ブロックã•ã‚ŒãŸã‚¤ãƒ³ãƒãƒ¼ãƒˆURL: %{message}"
msgid "ImportProjects|Error importing repository %{project_safe_import_url} into %{project_full_path} - %{message}"
-msgstr ""
+msgstr "%{project_safe_import_url} リãƒã‚¸ãƒˆãƒªã‹ã‚‰ %{project_full_path} ã¸ã®ã‚¤ãƒ³ãƒãƒ¼ãƒˆã‚¨ãƒ©ãƒ¼- %{message}"
msgid "ImportProjects|Importing the project failed"
msgstr "プロジェクトã®ã‚¤ãƒ³ãƒãƒ¼ãƒˆã«å¤±æ•—ã—ã¾ã—ãŸ"
msgid "ImportProjects|Requesting your %{provider} repositories failed"
-msgstr ""
+msgstr "%{provider} リãƒã‚¸ãƒˆãƒªã®è¦æ±‚ã«å¤±æ•—ã—ã¾ã—ãŸ"
msgid "ImportProjects|Select the projects you want to import"
-msgstr ""
+msgstr "ã‚ãªãŸãŒã‚¤ãƒ³ãƒãƒ¼ãƒˆã—ãŸã„プロジェクトをé¸æŠžã—ã¾ã™"
msgid "ImportProjects|The remote data could not be imported."
msgstr "リモートデータをインãƒãƒ¼ãƒˆã§ãã¾ã›ã‚“ã§ã—ãŸã€‚"
@@ -10944,7 +11213,7 @@ msgid "Improve Issue boards with GitLab Enterprise Edition."
msgstr "GitLab エンタープライズエディションを使用ã™ã‚‹ã¨ã€èª²é¡Œãƒœãƒ¼ãƒ‰ã®æ©Ÿèƒ½ãŒå¼·åŒ–ã•ã‚Œã¾ã™ã€‚"
msgid "Improve Merge Requests and customer support with GitLab Enterprise Edition."
-msgstr ""
+msgstr "GitLab エンタープライズエディションã§ã¯ã€ãƒžãƒ¼ã‚¸ãƒªã‚¯ã‚¨ã‚¹ãƒˆã¨ã‚«ã‚¹ã‚¿ãƒžãƒ¼ã‚µãƒãƒ¼ãƒˆã‚’改善"
msgid "Improve issues management with Issue weight and GitLab Enterprise Edition."
msgstr "GitLab エンタープライズエディションを使用ã™ã‚‹ã¨ã€èª²é¡Œç®¡ç†æ©Ÿèƒ½ãŒå¼·åŒ–ã•ã‚Œã€èª²é¡Œã«ã‚¦ã‚§ã‚¤ãƒˆã‚’設定ã§ãã¾ã™ã€‚"
@@ -10953,10 +11222,10 @@ msgid "Improve search with Advanced Global Search and GitLab Enterprise Edition.
msgstr "GitLab エンタープライズエディションを使用ã™ã‚‹ã¨ã€æ¤œç´¢æ©Ÿèƒ½ãŒå¼·åŒ–ã•ã‚Œã€é«˜åº¦ãªã‚°ãƒ­ãƒ¼ãƒãƒ«æ¤œç´¢ãŒå¯èƒ½ã«ãªã‚Šã¾ã™ã€‚"
msgid "In order to enable instance-level analytics, please ask an admin to enable %{usage_ping_link_start}usage ping%{usage_ping_link_end}."
-msgstr ""
+msgstr "インスタンスレベルã®åˆ†æžã‚’å¯èƒ½ã«ã™ã‚‹ãŸã‚ã«ã€æœ‰åŠ¹ã«ã™ã‚‹ãŸã‚ã«ç®¡ç†è€…ã«ä¾é ¼ã—ã¦ãã ã•ã„ %{usage_ping_link_start}ping ã®ä½¿ç”¨%{usage_ping_link_end}。"
msgid "In order to gather accurate feature usage data, it can take 1 to 2 weeks to see your index."
-msgstr ""
+msgstr "正確ãªæ©Ÿèƒ½ã®ä½¿ç”¨çŠ¶æ³ã‚’åŽé›†ã™ã‚‹ãŸã‚ã«ã€ã‚¤ãƒ³ãƒ‡ãƒƒã‚¯ã‚¹ã‚’確èªã™ã‚‹ã®ã«1〜2週間ã‹ã‹ã‚‹ã“ã¨ãŒã‚ã‚Šã¾ã™ã€‚"
msgid "In order to personalize your experience with GitLab<br>we would like to know a bit more about you."
msgstr ""
@@ -10964,9 +11233,12 @@ msgstr ""
msgid "In order to tailor your experience with GitLab we<br>would like to know a bit more about you."
msgstr ""
-msgid "In the next step, you'll be able to select the projects you want to import."
+msgid "In progress"
msgstr ""
+msgid "In the next step, you'll be able to select the projects you want to import."
+msgstr "次ã®ã‚¹ãƒ†ãƒƒãƒ—ã§ã¯ã€ã‚¤ãƒ³ãƒãƒ¼ãƒˆã™ã‚‹ãƒ—ロジェクトをé¸æŠžã—ã¾ã™ã€‚"
+
msgid "Incident Management Limits"
msgstr ""
@@ -10986,16 +11258,16 @@ msgid "Include the username in the URL if required: <code>https://username@gitla
msgstr "å¿…è¦ãªå ´åˆã€URL ã«ãƒ¦ãƒ¼ã‚¶ãƒ¼åã‚’å«ã‚ã¾ã™ <code>https://username@gitlab.company.com/group/project.git</code>。"
msgid "Includes LFS objects. It can be overridden per group, or per project. 0 for unlimited."
-msgstr ""
+msgstr "LFSオブジェクトをå«ã¿ã¾ã™ã€‚グループã”ã¨ã€ã¾ãŸã¯ãƒ—ロジェクトã”ã¨ã«ã‚ªãƒ¼ãƒãƒ¼ãƒ©ã‚¤ãƒ‰ã§ãã¾ã™ã€‚無制é™ã‚’指定ã™ã‚‹å ´åˆã¯0ã«ã—ã¾ã™ã€‚"
msgid "Includes an MVC structure to help you get started."
-msgstr ""
+msgstr "MVC構造をå«ã‚ã¦ã€é–‹å§‹ã—ã¾ã™ã€‚"
msgid "Includes an MVC structure, Gemfile, Rakefile, along with many others, to help you get started."
-msgstr ""
+msgstr "MVC構造体ã€Gemfileã€Rakefile等をå«ã‚ã¦é–‹å§‹ã—ã¾ã™ã€‚"
msgid "Includes an MVC structure, mvnw and pom.xml to help you get started."
-msgstr ""
+msgstr "MVC構造体ã€mvnwã€pom.xmlã‚’å«ã‚ã¦é–‹å§‹ã—ã¾ã™ã€‚"
msgid "Includes repository storage, wiki storage, LFS objects, build artifacts and packages. 0 for unlimited."
msgstr ""
@@ -11010,22 +11282,22 @@ msgid "Incompatible Project"
msgstr "互æ›æ€§ã®ãªã„プロジェクト"
msgid "Incompatible options set!"
-msgstr ""
+msgstr "互æ›æ€§ã®ãªã„オプションãŒã‚»ãƒƒãƒˆã•ã‚Œã¦ã„ã¾ã™ã€‚"
msgid "Index all projects"
msgstr ""
msgid "Indicates whether this runner can pick jobs without tags"
-msgstr ""
+msgstr "ã“ã®ãƒ©ãƒ³ãƒŠãƒ¼ãŒã‚¿ã‚°ã®ãªã„ジョブをé¸æŠžã§ãã‚‹ã‹ã©ã†ã‹ã‚’示ã—ã¾ã™ã€‚"
msgid "Inform users without uploaded SSH keys that they can't push over SSH until one is added"
msgstr "SSH éµã‚’アップロードã—ã¦ã„ãªã„ユーザーã«ã€è¿½åŠ ã•ã‚Œã‚‹ã¾ã§ SSH 経由ã§ãƒ—ッシュã§ããªã„ã“ã¨ã‚’通知"
msgid "Information about additional Pages templates and how to install them can be found in our %{pages_getting_started_guide}."
-msgstr ""
+msgstr "追加ã®Pagesテンプレートã«é–¢ã™ã‚‹æƒ…å ±ã¨ãれらをインストールã™ã‚‹æ–¹æ³•ã«ã¤ã„ã¦ã¯ã€ç§ãŸã¡ã® %{pages_getting_started_guide} ã‚’å‚ç…§ã—ã¦ãã ã•ã„。"
msgid "Inherited:"
-msgstr ""
+msgstr "継承ã•ã‚Œã¾ã—ãŸï¼š"
msgid "Inline"
msgstr "インライン"
@@ -11128,7 +11400,7 @@ msgid "Invalid Git ref"
msgstr ""
msgid "Invalid Insights config file detected"
-msgstr ""
+msgstr "無効㪠Insights 設定ファイルãŒã¿ã¤ã‹ã‚Šã¾ã—ãŸ"
msgid "Invalid Login or password"
msgstr "ログインIDã¾ãŸã¯ãƒ‘スワードãŒç„¡åŠ¹ã§ã™ã€‚"
@@ -11143,7 +11415,7 @@ msgid "Invalid date"
msgstr "無効ãªæ—¥ä»˜"
msgid "Invalid date format. Please use UTC format as YYYY-MM-DD"
-msgstr ""
+msgstr "無効ãªæ—¥ä»˜å½¢å¼ã§ã™ã€‚YYYY-MM-DDã®UTCフォーマットを使用ã—ã¦ãã ã•ã„"
msgid "Invalid date range"
msgstr ""
@@ -11161,13 +11433,13 @@ msgid "Invalid file."
msgstr "無効ãªãƒ•ã‚¡ã‚¤ãƒ«ã§ã™ã€‚"
msgid "Invalid import params"
-msgstr ""
+msgstr "無効ãªã‚¤ãƒ³ãƒãƒ¼ãƒˆãƒ‘ラメーターã§ã™"
msgid "Invalid input, please avoid emojis"
msgstr "入力ãŒç„¡åŠ¹ã§ã™ã€‚絵文字をå«ã‚ãªã„ã§ãã ã•ã„"
msgid "Invalid login or password"
-msgstr ""
+msgstr "ログインIDã¾ãŸã¯ãƒ‘スワードãŒç„¡åŠ¹ã§ã™ã€‚"
msgid "Invalid pin code"
msgstr ""
@@ -11176,7 +11448,7 @@ msgid "Invalid query"
msgstr "無効ãªã‚¯ã‚¨ãƒªãƒ¼"
msgid "Invalid repository path"
-msgstr ""
+msgstr "リãƒã‚¸ãƒˆãƒªãƒ‘スãŒç„¡åŠ¹ã§ã™"
msgid "Invalid server response"
msgstr "無効ãªã‚µãƒ¼ãƒãƒ¼å¿œç­”"
@@ -11212,7 +11484,7 @@ msgid "Is this GitLab trial for your company?"
msgstr ""
msgid "Is using license seat:"
-msgstr ""
+msgstr "ライセンスシートã®ä½¿ç”¨:"
msgid "Is using seat"
msgstr ""
@@ -11221,13 +11493,13 @@ msgid "IssuableStatus|Closed"
msgstr "完了"
msgid "IssuableStatus|Closed (%{link})"
-msgstr ""
+msgstr "完了(%{link})"
msgid "IssuableStatus|duplicated"
-msgstr ""
+msgstr "é‡è¤‡"
msgid "IssuableStatus|moved"
-msgstr ""
+msgstr "移動済ã¿"
msgid "IssuableStatus|promoted"
msgstr ""
@@ -11245,7 +11517,7 @@ msgid "Issue already promoted to epic."
msgstr ""
msgid "Issue cannot be found."
-msgstr ""
+msgstr "課題を見ã¤ã‘ã‚‹ã“ã¨ãŒã§ãã¾ã›ã‚“"
msgid "Issue events"
msgstr "課題イベント"
@@ -11310,15 +11582,15 @@ msgstr ""
msgid "Issues"
msgstr "課題"
-msgid "Issues / Merge Requests"
-msgstr ""
-
msgid "Issues Analytics"
msgstr ""
msgid "Issues Rate Limits"
msgstr ""
+msgid "Issues and Merge Requests"
+msgstr ""
+
msgid "Issues can be bugs, tasks or ideas to be discussed. Also, issues are searchable and filterable."
msgstr "課題ã¨ã¯ãƒã‚°ã€ã‚¿ã‚¹ã‚¯ã€ã¾ãŸã¯è­°è«–ã®å¿…è¦ãªã‚¢ã‚¤ãƒ‡ã‚¢ã§ã™ã€‚ã¾ãŸã€èª²é¡Œã¯æ¤œç´¢ã‚„フィルター処ç†ãŒå¯èƒ½ã§ã™ã€‚"
@@ -11329,7 +11601,7 @@ msgid "Issues referenced by merge requests and commits within the default branch
msgstr ""
msgid "Issues with comments, merge requests with diffs and comments, labels, milestones, snippets, and other project entities"
-msgstr ""
+msgstr "コメントã®ã‚る課題ã€å·®åˆ†ã¨ã‚³ãƒ¡ãƒ³ãƒˆã€ãƒ©ãƒ™ãƒ«ã€ãƒžã‚¤ãƒ«ã‚¹ãƒˆãƒ¼ãƒ³ã€ã‚¹ãƒ‹ãƒšãƒƒãƒˆã€ãã®ä»–ã®ãƒ—ロジェクトエンティティã¨é–¢é€£ã—ãŸãƒžãƒ¼ã‚¸ãƒªã‚¯ã‚¨ã‚¹ãƒˆ"
msgid "Issues, merge requests, pushes, and comments."
msgstr "課題ã€ãƒžãƒ¼ã‚¸ãƒªã‚¯ã‚¨ã‚¹ãƒˆã€ãƒ—ッシュã€ã‚³ãƒ¡ãƒ³ãƒˆ"
@@ -11340,11 +11612,11 @@ msgstr "プロジェクトã®èª²é¡Œã®ä½œæˆã‚’開始ã—ãŸã‚‰ã€ãƒ—ロジェク
msgid "IssuesAnalytics|Avg/Month:"
msgstr ""
-msgid "IssuesAnalytics|Issues created"
+msgid "IssuesAnalytics|Issues opened"
msgstr ""
-msgid "IssuesAnalytics|Issues created per month"
-msgstr "課題ã®ä½œæˆï¼æœˆ"
+msgid "IssuesAnalytics|Issues opened per month"
+msgstr ""
msgid "IssuesAnalytics|Last 12 months"
msgstr "éŽåŽ»12ヶ月間"
@@ -11397,14 +11669,17 @@ msgstr ""
msgid "Jira integration not configured."
msgstr ""
-msgid "JiraService|Events for %{noteable_model_name} are disabled."
+msgid "Jira project: %{importProject}"
msgstr ""
+msgid "JiraService|Events for %{noteable_model_name} are disabled."
+msgstr "%{noteable_model_name} イベントã¯ç„¡åŠ¹ã§ã™ã€‚"
+
msgid "JiraService|If different from Web URL"
-msgstr ""
+msgstr "Web URLã¨ç•°ãªã‚‹å ´åˆ"
msgid "JiraService|Jira API URL"
-msgstr ""
+msgstr "JIRA APIã®URL"
msgid "JiraService|Jira comments will be created when an issue gets referenced in a commit."
msgstr ""
@@ -11413,37 +11688,37 @@ msgid "JiraService|Jira comments will be created when an issue gets referenced i
msgstr ""
msgid "JiraService|Jira issue tracker"
-msgstr ""
+msgstr "Jira 課題トラッカー"
msgid "JiraService|Password or API token"
msgstr "パスワードã¾ãŸã¯APIトークン"
msgid "JiraService|Transition ID(s)"
-msgstr ""
+msgstr "é·ç§»ID"
msgid "JiraService|Use , or ; to separate multiple transition IDs"
-msgstr ""
+msgstr "複数ã®é·ç§»IDを記載ã™ã‚‹å ´åˆã€ã‚³ãƒ³ãƒž, ã¾ãŸã¯ã‚»ãƒŸã‚³ãƒ­ãƒ³; を使ã£ã¦åˆ†ã‘ã‚‹"
msgid "JiraService|Use a password for server version and an API token for cloud version"
-msgstr ""
+msgstr "サーãƒãƒ¼ãƒãƒ¼ã‚¸ãƒ§ãƒ³ã«ã¯ãƒ‘スワードを使用ã—ã¦ãã ã•ã„。クラウドãƒãƒ¼ã‚¸ãƒ§ãƒ³ã«ã¯APIトークンを使用ã—ã¦ãã ã•ã„。"
msgid "JiraService|Use a username for server version and an email for cloud version"
-msgstr ""
+msgstr "サーãƒãƒ¼ãƒãƒ¼ã‚¸ãƒ§ãƒ³ã«ã¯ãƒ¦ãƒ¼ã‚¶ãƒ¼åを使用ã—ã¦ãã ã•ã„。クラウドãƒãƒ¼ã‚¸ãƒ§ãƒ³ã«ã¯Eメールを使用ã—ã¦ãã ã•ã„。"
msgid "JiraService|Username or Email"
-msgstr ""
+msgstr "ユーザーåã¾ãŸã¯ãƒ¡ãƒ¼ãƒ«"
msgid "JiraService|Web URL"
msgstr "Web URL"
msgid "JiraService|transition ids can have only numbers which can be split with , or ;"
-msgstr ""
+msgstr "é·ç§»IDã¯ã€ã‚³ãƒ³ãƒž, やセミコロン; ã§åˆ†å‰²ã§ãる複数ã¾ãŸã¯ä¸€ã¤ã®æ•°å€¤ã§ãªã‘ã‚Œã°ãªã‚Šã¾ã›ã‚“。"
msgid "Job"
msgstr "ジョブ"
msgid "Job Failed #%{build_id}"
-msgstr ""
+msgstr "#%{build_id} ã®ã‚¸ãƒ§ãƒ–ãŒå¤±æ•—"
msgid "Job ID"
msgstr "ジョブ ID"
@@ -11452,13 +11727,13 @@ msgid "Job has been erased"
msgstr "ジョブãŒæ¶ˆåŽ»ã•ã‚Œã¾ã—ãŸ"
msgid "Job has been successfully erased!"
-msgstr ""
+msgstr "ジョブã®å‰Šé™¤ã«æˆåŠŸã—ã¾ã—ãŸã€‚"
msgid "Job has wrong arguments format."
-msgstr ""
+msgstr "ジョブã®å¼•æ•°ãƒ•ã‚©ãƒ¼ãƒžãƒƒãƒˆé–“é•ã£ã¦ã„ã¾ã™"
msgid "Job is missing the `model_type` argument."
-msgstr ""
+msgstr "ジョブ㫠`model_type`引数ãŒã‚ã‚Šã¾ã›ã‚“。"
msgid "Job is stuck. Check runners."
msgstr "ジョブãŒæºœã¾ã£ã¦ã„ã¾ã™ã€‚Runner を確èªã—ã¦ãã ã•ã„。"
@@ -11515,13 +11790,13 @@ msgid "Job|Show complete raw"
msgstr "完全㪠Raw を表示ã™ã‚‹"
msgid "Job|The artifacts were removed"
-msgstr ""
+msgstr "アーティファクトã¯å‰Šé™¤ã•ã‚Œã¾ã—ãŸ"
msgid "Job|The artifacts will be removed"
msgstr "æˆæžœç‰©ã¯å‰Šé™¤ã•ã‚Œã¾ã™"
msgid "Job|This job failed because the necessary resources were not successfully created."
-msgstr ""
+msgstr "ã“ã®ã‚¸ãƒ§ãƒ–ã¯å¤±æ•—ã—ã¾ã—ãŸã€‚å¿…è¦ãªãƒªã‚½ãƒ¼ã‚¹ãŒæ­£å¸¸ã«ä½œæˆã§ããªã‹ã£ãŸãŸã‚ã§ã™ã€‚"
msgid "Job|This job is stuck because the project doesn't have any runners online assigned to it."
msgstr "プロジェクトã«ã‚ªãƒ³ãƒ©ã‚¤ãƒ³ã® Runner ãŒå‰²ã‚Šå½“ã¦ã‚‰ã‚Œã¦ã„ãªã„ãŸã‚ã€ã‚¸ãƒ§ãƒ–ã¯åœæ­¢ã—ã¦ã„ã¾ã™ã€‚"
@@ -11723,7 +11998,7 @@ msgid "Last commit"
msgstr "最新コミット"
msgid "Last contact"
-msgstr ""
+msgstr "最終連絡日時"
msgid "Last edited %{date}"
msgstr "最終編集日 %{date}"
@@ -11762,10 +12037,10 @@ msgid "Last updated"
msgstr "最終更新"
msgid "Last used"
-msgstr ""
+msgstr "å‰å›žä½¿ç”¨"
msgid "Last used on:"
-msgstr ""
+msgstr "最終使用日: "
msgid "LastCommit|authored"
msgstr ""
@@ -11785,6 +12060,9 @@ msgstr ""
msgid "Lead"
msgstr "リード"
+msgid "Lead Time"
+msgstr ""
+
msgid "Learn GitLab"
msgstr ""
@@ -11792,9 +12070,12 @@ msgid "Learn More"
msgstr ""
msgid "Learn how to %{link_start}contribute to the built-in templates%{link_end}"
-msgstr ""
+msgstr "%{link_start} ビルトインã®ãƒ†ãƒ³ãƒ—レートã«è²¢çŒ® %{link_end}ã™ã‚‹æ–¹æ³•ã‚’å­¦ã¶"
msgid "Learn how to %{no_packages_link_start}publish and share your packages%{no_packages_link_end} with GitLab."
+msgstr "GitLabã§ã®%{no_packages_link_start}パッケージã®å…±æœ‰ã¨å…¬é–‹%{no_packages_link_end}ã«ã¤ã„ã¦"
+
+msgid "Learn how to enable synchronization"
msgstr ""
msgid "Learn more"
@@ -11864,7 +12145,7 @@ msgid "Leave project"
msgstr "プロジェクトを離脱"
msgid "Leave the \"File type\" and \"Delivery method\" options on their default values."
-msgstr ""
+msgstr "\"ファイルタイプ\"ã¨\"デリãƒãƒªãƒ¼æ–¹æ³•\"オプションをデフォルト値ã®ã¾ã¾ã«ã—ã¾ã™ã€‚"
msgid "Let's Encrypt does not accept emails on example.com"
msgstr "Let's Encryptã¯example.comã®ãƒ¡ãƒ¼ãƒ«ã‚¢ãƒ‰ãƒ¬ã‚¹ã‚’å—ã‘付ã‘ã¾ã›ã‚“"
@@ -12106,7 +12387,7 @@ msgid "List available repositories"
msgstr "利用å¯èƒ½ãªãƒªãƒã‚¸ãƒˆãƒªã®ä¸€è¦§"
msgid "List of IPs and CIDRs of allowed secondary nodes. Comma-separated, e.g. \"1.1.1.1, 2.2.2.0/24\""
-msgstr ""
+msgstr "許å¯ã•ã‚ŒãŸã‚»ã‚«ãƒ³ãƒ€ãƒªãƒŽãƒ¼ãƒ‰ã®IPã¾ãŸã¯CIDRã®ãƒªã‚¹ãƒˆ(コンマ区切り)。例ãˆã° \"1.1.1.1,2.2.2.0/24\""
msgid "List settings"
msgstr ""
@@ -12123,9 +12404,6 @@ msgstr "ã‚ãªãŸã®ã€Bitbucket Server ã®ãƒªãƒã‚¸ãƒˆãƒªã‚’一覧表示ã™ã‚‹"
msgid "Live preview"
msgstr "ライブプレビュー"
-msgid "Load more vulnerabilities"
-msgstr ""
-
msgid "Loading"
msgstr ""
@@ -12172,7 +12450,7 @@ msgid "Lock not found"
msgstr "ロックãŒè¦‹ã¤ã‹ã‚Šã¾ã›ã‚“"
msgid "Lock the discussion"
-msgstr ""
+msgstr "ディスカッションをロックã™ã‚‹"
msgid "Lock this %{issuableDisplayName}? Only <strong>project members</strong> will be able to comment."
msgstr "%{issuableDisplayName} をロックã—ã¾ã™ã‹ï¼Ÿ<strong>プロジェクトメンãƒãƒ¼</strong> ã®ã¿ã‚³ãƒ¡ãƒ³ãƒˆã§ãã¾ã™ã€‚"
@@ -12187,10 +12465,10 @@ msgid "Locked Files"
msgstr "ロックã•ã‚ŒãŸãƒ•ã‚¡ã‚¤ãƒ«"
msgid "Locked by %{fileLockUserName}"
-msgstr ""
+msgstr "%{fileLockUserName} ã«ã‚ˆã£ã¦ ã«ãƒ­ãƒƒã‚¯ã•ã‚Œã¦ã„ã¾ã™ã€‚"
msgid "Locked the discussion."
-msgstr ""
+msgstr "ロックã—ãŸè­°è«–"
msgid "Locked to current projects"
msgstr "ç¾åœ¨ã®ãƒ—ロジェクトã¯ãƒ­ãƒƒã‚¯ã•ã‚Œã¦ã„ã¾ã™"
@@ -12214,10 +12492,10 @@ msgid "Logs|To see the logs, deploy your code to an environment."
msgstr ""
msgid "Low vulnerabilities present"
-msgstr ""
+msgstr "å¼±ã„脆弱性ã®å­˜åœ¨"
msgid "MB"
-msgstr ""
+msgstr "MB"
msgid "MD5"
msgstr "MD5"
@@ -12271,14 +12549,11 @@ msgid "Makes this issue confidential."
msgstr "課題をéžå…¬é–‹ã«è¨­å®šã™ã‚‹ã€‚"
msgid "Malformed string"
-msgstr ""
+msgstr "ä¸æ­£ãªãƒ•ã‚©ãƒ¼ãƒžãƒƒãƒˆã®æ–‡å­—列"
msgid "Manage"
msgstr "管ç†"
-msgid "Manage Git repositories with fine-grained access controls that keep your code secure. Perform code reviews and enhance collaboration with merge requests. Each project can also have an issue tracker and a wiki."
-msgstr "ã‚ãªãŸã®ã‚³ãƒ¼ãƒ‰ã‚’安全ã«ä¿ã¤ãŸã‚ã«ã€ã‚¢ã‚¯ã‚»ã‚¹ã‚³ãƒ³ãƒˆãƒ­ãƒ¼ãƒ«ã§ Git リãƒã‚¸ãƒˆãƒªã‚’管ç†ã—ã¾ã™ã€‚コードレビューを実行ã—ã€ãƒžãƒ¼ã‚¸ãƒªã‚¯ã‚¨ã‚¹ãƒˆã¨ã®ã‚³ãƒ©ãƒœãƒ¬ãƒ¼ã‚·ãƒ§ãƒ³ã‚’強化ã—ã¾ã™ã€‚å„プロジェクトã«ã¯ã€èª²é¡Œãƒˆãƒ©ãƒƒã‚«ãƒ¼ã¨ã‚¦ã‚£ã‚­ã‚‚ã‚ã‚Šã¾ã™ã€‚"
-
msgid "Manage Web IDE features"
msgstr "Web IDE 機能ã®ç®¡ç†"
@@ -12309,6 +12584,9 @@ msgstr "2è¦ç´ èªè¨¼ã®ç®¡ç†"
msgid "Manage your license"
msgstr ""
+msgid "Managed Account"
+msgstr ""
+
msgid "Manifest"
msgstr "マニフェスト"
@@ -12343,7 +12621,7 @@ msgid "Mark To Do as done"
msgstr "Todo を完了ã«ã™ã‚‹"
msgid "Mark as done"
-msgstr ""
+msgstr "完了ã«ã™ã‚‹"
msgid "Mark as resolved"
msgstr "解決済ã«ã™ã‚‹"
@@ -12355,7 +12633,7 @@ msgid "Mark this issue as a duplicate of another issue"
msgstr "ã“ã®èª²é¡Œã‚’別ã®èª²é¡Œã¨é‡è¤‡ã—ã¦ã„ã‚‹ã¨ãƒžãƒ¼ã‚¯ã™ã‚‹"
msgid "Mark this issue as related to another issue"
-msgstr ""
+msgstr "ã“ã®èª²é¡Œã‚’別ã®èª²é¡Œã«é–¢é€£ã—ã¦ã„ã‚‹ã¨ãƒžãƒ¼ã‚¯ã™ã‚‹"
msgid "Markdown"
msgstr "Markdown"
@@ -12367,22 +12645,22 @@ msgid "Markdown enabled"
msgstr "マークダウンを使用ã§ãã¾ã™"
msgid "Markdown is supported"
-msgstr ""
+msgstr "マークダウンをサãƒãƒ¼ãƒˆã—ã¦ã„ã¾ã™ã€‚"
msgid "Marked To Do as done."
-msgstr ""
+msgstr "Todo を完了ã«ã—ãŸã€‚"
msgid "Marked this %{noun} as Work In Progress."
-msgstr ""
+msgstr "ã“ã® %{noun} を作業中ã¨ãƒžãƒ¼ã‚¯ã—ã¾ã—ãŸã€‚"
msgid "Marked this issue as a duplicate of %{duplicate_param}."
-msgstr ""
+msgstr "ã“ã®èª²é¡Œã¯ %{duplicate_param} ã¨é‡è¤‡ã¨ã—ã¦ã„ã‚‹ã¨ãƒžãƒ¼ã‚¯ã—ã¾ã—ãŸã€‚"
msgid "Marked this issue as related to %{issue_ref}."
-msgstr ""
+msgstr "ã“ã®èª²é¡Œã‚’ %{issue_ref} ã«é–¢é€£ã—ã¦ã„ã‚‹ã¨ãƒžãƒ¼ã‚¯ã—ã¾ã—ãŸã€‚"
msgid "Marks To Do as done."
-msgstr ""
+msgstr "Todo を完了ã«ã™ã‚‹."
msgid "Marks this %{noun} as Work In Progress."
msgstr ""
@@ -12400,28 +12678,28 @@ msgid "Match not found; try refining your search query."
msgstr "マッãƒã—ã¾ã›ã‚“ã§ã—ãŸã€‚検索クエリを変ãˆã¦è©¦ã—ã¦ãã ã•ã„。"
msgid "MattermostService|Add to Mattermost"
-msgstr ""
+msgstr "Mattermostã«è¿½åŠ "
msgid "MattermostService|Command trigger word"
-msgstr ""
+msgstr "コマンドã®ãƒˆãƒªã‚¬ãƒ¼ãƒ¯ãƒ¼ãƒ‰"
msgid "MattermostService|Fill in the word that works best for your team."
-msgstr ""
+msgstr "ãƒãƒ¼ãƒ ã«æœ€é©ãªãƒ¯ãƒ¼ãƒ‰ã‚’入力ã—ã¦ãã ã•ã„。"
msgid "MattermostService|Request URL"
-msgstr ""
+msgstr "リクエストURL"
msgid "MattermostService|Request method"
-msgstr ""
+msgstr "リクエストメソッド"
msgid "MattermostService|Response icon"
-msgstr ""
+msgstr "応答アイコン"
msgid "MattermostService|Response username"
msgstr "応答ユーザーå"
msgid "MattermostService|See list of available commands in Mattermost after setting up this service, by entering"
-msgstr ""
+msgstr "ã“ã®ã‚µãƒ¼ãƒ“スを設定ã—ãŸå¾Œã«Mattermost上ã§ä½¿ç”¨ã§ãるコマンドã®ãƒªã‚¹ãƒˆã‚’表示ã™ã‚‹ã«ã¯ã€æ¬¡ã‚’入力ã—ã¾ã™ã€‚"
msgid "MattermostService|Suggestions:"
msgstr ""
@@ -12472,7 +12750,7 @@ msgid "Maximum job timeout"
msgstr "ジョブタイムアウトã®æœ€å¤§å€¤"
msgid "Maximum job timeout has a value which could not be accepted"
-msgstr ""
+msgstr "最大ジョブタイムアウトã«ã¯å—ã‘入れられãªã„値ãŒæŒ‡å®šã•ã‚Œã¦ã„ã¾ã™"
msgid "Maximum lifetime allowable for Personal Access Tokens is active, your expire date must be set before %{maximum_allowable_date}."
msgstr ""
@@ -12481,7 +12759,7 @@ msgid "Maximum namespace storage (MB)"
msgstr ""
msgid "Maximum number of %{name} (%{count}) exceeded"
-msgstr ""
+msgstr "%{name} ã®ä¸Šé™(%{count})を超ãˆã¾ã—ãŸ"
msgid "Maximum number of comments exceeded"
msgstr "コメント数ãŒä¸Šé™ã«é”ã—ã¾ã—ãŸã€‚"
@@ -12511,7 +12789,7 @@ msgid "Maximum size of individual attachments in comments."
msgstr ""
msgid "Maximum time between updates that a mirror can have when scheduled to synchronize."
-msgstr ""
+msgstr "åŒæœŸãŒã‚¹ã‚±ã‚¸ãƒ¥ãƒ¼ãƒ«ã•ã‚Œã¦ã„ã‚‹ã¨ãã«ãƒŸãƒ©ãƒ¼ãƒªãƒ³ã‚°ã®æ›´æ–°é–“ã®æœ€å¤§æ™‚間。"
msgid "May"
msgstr "5月"
@@ -12523,13 +12801,13 @@ msgid "Median"
msgstr "中央値"
msgid "Medium vulnerabilities present"
-msgstr ""
+msgstr "中程度ã®è„†å¼±æ€§ã®å­˜åœ¨"
msgid "Member lock"
-msgstr ""
+msgstr "メンãƒãƒ¼ã®ãƒ­ãƒƒã‚¯"
msgid "Member since %{date}"
-msgstr ""
+msgstr "%{date} ã«ãƒ¡ãƒ³ãƒãƒ¼ç™»éŒ²"
msgid "Members"
msgstr "メンãƒãƒ¼"
@@ -12547,7 +12825,7 @@ msgid "Members with pending access to %{strong_start}%{group_name}%{strong_end}"
msgstr ""
msgid "Memory Usage"
-msgstr ""
+msgstr "メモリ使用é‡"
msgid "Memory limit exceeded while rendering template"
msgstr ""
@@ -12573,6 +12851,9 @@ msgstr "作æˆã•ã‚ŒãŸãƒžãƒ¼ã‚¸ãƒªã‚¯ã‚¨ã‚¹ãƒˆ"
msgid "Merge Requests in Review"
msgstr ""
+msgid "Merge automatically (%{strategy})"
+msgstr ""
+
msgid "Merge commit message"
msgstr "マージコミットメッセージ"
@@ -12664,7 +12945,7 @@ msgid "MergeRequests|Saving the comment failed"
msgstr "コメントã®ä¿å­˜ã«å¤±æ•—ã—ã¾ã—ãŸ"
msgid "MergeRequests|Squash task canceled: another squash is already in progress."
-msgstr ""
+msgstr "スカッシュタスクãŒå–り消ã•ã‚Œã¾ã—ãŸã€‚別ã®ã‚¹ã‚«ãƒƒã‚·ãƒ¥ãŒæ—¢ã«é€²è¡Œä¸­ã§ã™ã€‚"
msgid "MergeRequests|Thread stays resolved"
msgstr ""
@@ -12682,7 +12963,7 @@ msgid "MergeRequests|Toggle comments for this file"
msgstr "ã“ã®ãƒ•ã‚¡ã‚¤ãƒ«ã®ã‚³ãƒ¡ãƒ³ãƒˆã‚’切り替ãˆã¾ã™"
msgid "MergeRequests|View file @ %{commitId}"
-msgstr ""
+msgstr "%{commitId} ã®ãƒ•ã‚¡ã‚¤ãƒ«ã‚’表示"
msgid "MergeRequests|View replaced file @ %{commitId}"
msgstr ""
@@ -12709,10 +12990,10 @@ msgid "MergeRequest|Compare %{source} and %{target}"
msgstr ""
msgid "MergeRequest|Error dismissing suggestion popover. Please try again."
-msgstr ""
+msgstr "サジェストãƒãƒƒãƒ—オーãƒãƒ¼å´ä¸‹ä¸­ã«ã‚¨ãƒ©ãƒ¼ãŒç™ºç”Ÿã—ã¾ã—ãŸã€‚ã‚‚ã†ä¸€åº¦ã‚„ã‚Šç›´ã—ã¦ãã ã•ã„。"
msgid "MergeRequest|Error loading full diff. Please try again."
-msgstr ""
+msgstr "全体差分を読ã¿è¾¼ã¿ä¸­ã«ã‚¨ãƒ©ãƒ¼ãŒç™ºç”Ÿã—ã¾ã—ãŸã€‚ã‚‚ã†ä¸€åº¦ã‚„ã‚Šç›´ã—ã¦ä¸‹ã•ã„。"
msgid "MergeRequest|No files found"
msgstr "ファイルãŒè¦‹ã¤ã‹ã‚Šã¾ã›ã‚“"
@@ -12724,6 +13005,12 @@ msgid "Merged"
msgstr "マージ済ã¿"
msgid "Merged branches are being deleted. This can take some time depending on the number of branches. Please refresh the page to see changes."
+msgstr "マージã—ãŸãƒ–ランãƒã¯å‰Šé™¤ä¸­ã§ã™ã€‚ã“ã‚Œã¯ãƒ–ランãƒã®æ•°ã«ã‚ˆã£ã¦ã¯æ™‚é–“ãŒã‹ã‹ã‚‹ã“ã¨ãŒã‚ã‚Šã¾ã™ã€‚進æ—を確èªã™ã‚‹ã«ã¯ã€ãƒšãƒ¼ã‚¸ã‚’æ›´æ–°ã—ã¦ãã ã•ã„。"
+
+msgid "Merged this merge request."
+msgstr ""
+
+msgid "Merges this merge request immediately."
msgstr ""
msgid "Merges this merge request when the pipeline succeeds."
@@ -12775,7 +13062,7 @@ msgid "Metrics and profiling"
msgstr "メトリクスã¨ãƒ—ロファイリング"
msgid "Metrics for environment"
-msgstr ""
+msgstr "環境ã®ãƒ¡ãƒˆãƒªã‚¯ã‚¹"
msgid "Metrics::Dashboard::Annotation|Annotation can't belong to both a cluster and an environment at the same time"
msgstr ""
@@ -12832,12 +13119,15 @@ msgid "Metrics|Edit metric"
msgid_plural "Metrics|Edit metrics"
msgstr[0] ""
-msgid "Metrics|Environment"
-msgstr "環境"
+msgid "Metrics|Expand panel"
+msgstr ""
msgid "Metrics|For grouping similar metrics"
msgstr "é¡žä¼¼ã®ãƒ¡ãƒˆãƒªã‚¯ã‚¹ã‚’グループ化"
+msgid "Metrics|Go back (Esc)"
+msgstr ""
+
msgid "Metrics|Invalid time range, please verify."
msgstr ""
@@ -12868,9 +13158,6 @@ msgstr "Prometheus クエリã®ãƒ‰ã‚­ãƒ¥ãƒ¡ãƒ³ãƒˆ"
msgid "Metrics|Refresh dashboard"
msgstr ""
-msgid "Metrics|Show last"
-msgstr ""
-
msgid "Metrics|There was an error creating the dashboard."
msgstr ""
@@ -12881,7 +13168,7 @@ msgid "Metrics|There was an error fetching annotations. Please try again."
msgstr ""
msgid "Metrics|There was an error fetching the environments data, please try again"
-msgstr ""
+msgstr "環境データã®å–得中ã«ã‚¨ãƒ©ãƒ¼ãŒç™ºç”Ÿã—ã¾ã—ãŸã€‚ã‚‚ã†ä¸€åº¦ãŠè©¦ã—ãã ã•ã„"
msgid "Metrics|There was an error getting annotations information."
msgstr ""
@@ -12890,13 +13177,13 @@ msgid "Metrics|There was an error getting deployment information."
msgstr "デプロイ情報ã®å–得中ã«ã‚¨ãƒ©ãƒ¼ãŒç™ºç”Ÿã—ã¾ã—ãŸã€‚"
msgid "Metrics|There was an error getting environments information."
-msgstr ""
+msgstr "環境情報ã®å–得中ã«ã‚¨ãƒ©ãƒ¼ãŒç™ºç”Ÿã—ã¾ã—ãŸã€‚"
msgid "Metrics|There was an error trying to validate your query"
-msgstr ""
+msgstr "クエリã®æ¤œè¨¼ä¸­ã«ã‚¨ãƒ©ãƒ¼ãŒç™ºç”Ÿã—ã¾ã—ãŸ"
msgid "Metrics|There was an error while retrieving metrics"
-msgstr ""
+msgstr "メトリクスã®å–得中ã«ã‚¨ãƒ©ãƒ¼ãŒç™ºç”Ÿã—ã¾ã—ãŸ"
msgid "Metrics|There was an error while retrieving metrics. %{message}"
msgstr ""
@@ -12911,7 +13198,7 @@ msgid "Metrics|Used as a title for the chart"
msgstr "ãƒãƒ£ãƒ¼ãƒˆã®ã‚¿ã‚¤ãƒˆãƒ«ã¨ã—ã¦ä½¿ç”¨ã•ã‚Œã¾ã™"
msgid "Metrics|Used if the query returns a single series. If it returns multiple series, their legend labels will be picked up from the response."
-msgstr ""
+msgstr "クエリãŒå˜ä¸€ã®ç³»åˆ—ã‚’è¿”ã™ã¨ãã«ä½¿ç”¨ã•ã‚Œã¾ã™ã€‚複数ã®ç³»åˆ—ãŒè¿”ã•ã‚ŒãŸå ´åˆã€æ±Žç”¨ãƒ©ãƒ™ãƒ«ã¯ãã®å¿œç­”ã‹ã‚‰å–å¾—ã—ã¾ã™ã€‚"
msgid "Metrics|Validating query"
msgstr ""
@@ -12929,13 +13216,13 @@ msgid "Metrics|You can save a copy of this dashboard to your repository so it ca
msgstr ""
msgid "Metrics|You're about to permanently delete this metric. This cannot be undone."
-msgstr ""
+msgstr "ã“ã®ãƒ¡ãƒˆãƒªã‚¯ã‚¹ã‚’完全ã«å‰Šé™¤ã—よã†ã¨ã—ã¦ã„ã¾ã™ã€‚ã“ã®æ“作ã¯ã€å…ƒã«æˆ»ã›ã¾ã›ã‚“。"
msgid "Metrics|e.g. HTTP requests"
msgstr "例: HTTP リクエスト"
msgid "Metrics|e.g. Requests/second"
-msgstr ""
+msgstr "例:リクエスト毎秒"
msgid "Metrics|e.g. Throughput"
msgstr "例:スループット"
@@ -12944,16 +13231,19 @@ msgid "Metrics|e.g. rate(http_requests_total[5m])"
msgstr "例:レート (http_requests_total[5m])"
msgid "Metrics|e.g. req/sec"
-msgstr ""
+msgstr "例:リクエスト毎秒"
msgid "Microsoft Azure"
msgstr ""
-msgid "Migrated %{success_count}/%{total_count} files."
+msgid "Middleman project with Static Site Editor support"
msgstr ""
+msgid "Migrated %{success_count}/%{total_count} files."
+msgstr "%{success_count}/%{total_count} 個ã®ãƒ•ã‚¡ã‚¤ãƒ«ãŒç§»è¡Œã•ã‚Œã¾ã—ãŸã€‚"
+
msgid "Migration successful."
-msgstr ""
+msgstr "移行æˆåŠŸ"
msgid "Milestone"
msgid_plural "Milestones"
@@ -12963,16 +13253,16 @@ msgid "Milestone lists not available with your current license"
msgstr "ç¾åœ¨ã®ãƒ©ã‚¤ã‚»ãƒ³ã‚¹ã§ã¯ マイルストーンリストを利用ã§ãã¾ã›ã‚“"
msgid "Milestone lists show all issues from the selected milestone."
-msgstr ""
+msgstr "マイルストーンリストã«ã¯ã€é¸æŠžã—ãŸãƒžã‚¤ãƒ«ã‚¹ãƒˆãƒ¼ãƒ³ã®ã™ã¹ã¦ã®èª²é¡Œã‚’表示ã—ã¾ã™ã€‚"
msgid "Milestones"
msgstr "マイルストーン"
msgid "Milestones| You’re about to permanently delete the milestone %{milestoneTitle} and remove it from %{issuesWithCount} and %{mergeRequestsWithCount}. Once deleted, it cannot be undone or recovered."
-msgstr ""
+msgstr "ã‚ãªãŸã¯ã€ãƒžã‚¤ãƒ«ã‚¹ãƒˆãƒ¼ãƒ³ %{milestoneTitle} を永久ã«å‰Šé™¤ã—〠%{issuesWithCount} 㨠%{mergeRequestsWithCount} ã‹ã‚‰å‰Šé™¤ã—よã†ã¨ã—ã¦ã„ã¾ã™ã€‚一度削除ã™ã‚‹ã¨ã€å…ƒã«æˆ»ã™ã“ã¨ã¯ã§ãã¾ã›ã‚“。"
msgid "Milestones| You’re about to permanently delete the milestone %{milestoneTitle}. This milestone is not currently used in any issues or merge requests."
-msgstr ""
+msgstr "ã‚ãªãŸã¯ã€ãƒžã‚¤ãƒ«ã‚¹ãƒˆãƒ¼ãƒ³ %{milestoneTitle} を永久ã«å‰Šé™¤ã—よã†ã¨ã—ã¦ã„ã¾ã™ã€‚ã“ã®ãƒžã‚¤ãƒ«ã‚¹ãƒˆãƒ¼ãƒ³ã¯ç¾åœ¨ã€èª²é¡Œã¾ãŸã¯ãƒžãƒ¼ã‚¸ãƒªã‚¯ã‚¨ã‚¹ãƒˆã§ä½¿ç”¨ã•ã‚Œã¦ã„ã¾ã›ã‚“。"
msgid "Milestones|Delete milestone"
msgstr "マイルストーンã®å‰Šé™¤"
@@ -12993,22 +13283,22 @@ msgid "Milestones|Promote Milestone"
msgstr "マイルストーンã®æ˜‡æ ¼"
msgid "Milestones|Promoting %{milestoneTitle} will make it available for all projects inside %{groupName}. Existing project milestones with the same title will be merged."
-msgstr ""
+msgstr "%{milestoneTitle} を昇格ã™ã‚‹ã¨ã€ %{groupName} 内ã®ã™ã¹ã¦ã®ãƒ—ロジェクトã§ä½¿ç”¨ã§ãるよã†ã«ãªã‚Šã¾ã™ã€‚åŒã˜ã‚¿ã‚¤ãƒˆãƒ«ã‚’æŒã¤æ—¢å­˜ã®ãƒ—ロジェクトマイルストーンãŒãƒžãƒ¼ã‚¸ã•ã‚Œã¾ã™ã€‚"
msgid "Milestones|This action cannot be reversed."
msgstr "ã“ã®æ“作ã¯å…ƒã«æˆ»ã›ã¾ã›ã‚“。"
msgid "Minimum capacity to be available before we schedule more mirrors preemptively."
-msgstr ""
+msgstr "より多ãã®ãƒŸãƒ©ãƒ¼ã‚’優先的ã«ã‚¹ã‚±ã‚¸ãƒ¥ãƒ¼ãƒ«ã™ã‚‹å‰ã«ä½¿ç”¨å¯èƒ½ã«ãªã‚‹ãŸã‚ã®æœ€å°å®¹é‡ã€‚"
msgid "Minimum length is %{minimum_password_length} characters"
-msgstr ""
+msgstr "パスワードã¯æœ€ä½Ž %{minimum_password_length} 文字ã§ã™ã€‚"
msgid "Minimum length is %{minimum_password_length} characters."
-msgstr ""
+msgstr "パスワードã¯æœ€ä½Ž %{minimum_password_length} 文字ã§ã™ã€‚"
msgid "Minimum password length (number of characters)"
-msgstr ""
+msgstr "パスワードã®æœ€å°æ–‡å­—æ•°"
msgid "Minutes"
msgstr "分"
@@ -13044,7 +13334,7 @@ msgid "Mirroring was successfully disabled."
msgstr "ミラーリングを正ã—ã無効化ã—ã¾ã—ãŸ"
msgid "Mirroring will only be available if the feature is included in the plan of the selected group or user."
-msgstr ""
+msgstr "ミラーリングã¯ã€é¸æŠžã—ãŸã‚°ãƒ«ãƒ¼ãƒ—ã¾ãŸã¯ãƒ¦ãƒ¼ã‚¶ãƒ¼ãŒãƒ—ランã«å«ã¾ã‚Œã¦ã„ã‚‹å ´åˆã«ã®ã¿åˆ©ç”¨å¯èƒ½ã§ã™ã€‚"
msgid "Missing commit signatures endpoint!"
msgstr "コミット署åã®ã‚¨ãƒ³ãƒ‰ãƒã‚¤ãƒ³ãƒˆãŒã¿ã¤ã‹ã‚Šã¾ã›ã‚“。"
@@ -13158,7 +13448,7 @@ msgid "Moved this issue to %{path_to_project}."
msgstr ""
msgid "Moves issue to %{label} column in the board."
-msgstr ""
+msgstr "課題をボード㮠%{label} 列ã«ç§»å‹•ã—ã¾ã™ã€‚"
msgid "Moves this issue to %{path_to_project}."
msgstr "課題を %{path_to_project} ã«ç§»å‹•ã™ã‚‹ã€‚"
@@ -13209,7 +13499,7 @@ msgid "Namespace: %{namespace}"
msgstr ""
msgid "Namespaces to index"
-msgstr ""
+msgstr "インデックス作æˆã™ã‚‹åå‰ç©ºé–“"
msgid "Naming, topics, avatar"
msgstr "命åã€ãƒˆãƒ”ックã€ã‚¢ãƒã‚¿ãƒ¼"
@@ -13242,7 +13532,7 @@ msgid "Network"
msgstr "ãƒãƒƒãƒˆãƒ¯ãƒ¼ã‚¯"
msgid "Never"
-msgstr ""
+msgstr "ã—ãªã„"
msgid "New"
msgstr "æ–°ã—ã„"
@@ -13333,7 +13623,7 @@ msgid "New issue"
msgstr "æ–°è¦èª²é¡Œ"
msgid "New issue title"
-msgstr ""
+msgstr "æ–°ã—ã„課題ã®ã‚¿ã‚¤ãƒˆãƒ«"
msgid "New label"
msgstr "æ–°ã—ã„ラベル"
@@ -13375,7 +13665,7 @@ msgid "New tag"
msgstr "æ–°è¦ã‚¿ã‚°"
msgid "New users set to external"
-msgstr ""
+msgstr "æ–°è¦ãƒ¦ãƒ¼ã‚¶ãƒ¼ã‚’外部ユーザーã«è¨­å®š"
msgid "New! Suggest changes directly"
msgstr ""
@@ -13387,15 +13677,15 @@ msgid "Newest first"
msgstr ""
msgid "Newly registered users will by default be external"
-msgstr ""
+msgstr "æ–°ã—ã登録ã•ã‚ŒãŸãƒ¦ãƒ¼ã‚¶ãƒ¼ã¯ãƒ‡ãƒ•ã‚©ãƒ«ãƒˆã§å¤–部ユーザーã«ãªã‚Šã¾ã™"
msgid "Next"
msgstr "Next"
-msgid "Next file in diff (MRs only)"
+msgid "Next file in diff"
msgstr ""
-msgid "Next unresolved discussion (MRs only)"
+msgid "Next unresolved discussion"
msgstr ""
msgid "Nickname"
@@ -13455,6 +13745,9 @@ msgstr "変更ãªã—"
msgid "No changes between %{ref_start}%{source_branch}%{ref_end} and %{ref_start}%{target_branch}%{ref_end}"
msgstr "%{ref_start}%{source_branch}%{ref_end} 㨠%{ref_start}%{target_branch}%{ref_end} ã®é–“ã«å¤‰æ›´ãŒã‚ã‚Šã¾ã›ã‚“。"
+msgid "No child epics match applied filters"
+msgstr ""
+
msgid "No connection could be made to a Gitaly Server, please check your logs!"
msgstr "Gitaly サーãƒãƒ¼ã«æŽ¥ç¶šã§ãã¾ã›ã‚“ã§ã—ãŸã€‚ログを確èªã—ã¦ãã ã•ã„ï¼"
@@ -13486,7 +13779,7 @@ msgid "No endpoint provided"
msgstr ""
msgid "No errors to display."
-msgstr ""
+msgstr "表示ã§ãるエラーã¯ã‚ã‚Šã¾ã›ã‚“。"
msgid "No estimate or time spent"
msgstr "見ç©ã¨å®Ÿç¸¾ã¯æœªå…¥åŠ›"
@@ -13519,7 +13812,7 @@ msgid "No labels with such name or description"
msgstr "ãã®åå‰ã¾ãŸã¯èª¬æ˜Žã®ãƒ©ãƒ™ãƒ«ãŒã‚ã‚Šã¾ã›ã‚“"
msgid "No license. All rights reserved"
-msgstr ""
+msgstr "ライセンス表記ãŒã‚ã‚Šã¾ã›ã‚“。全ã¦ã®æ¨©åˆ©ã‚’ä¿æœ‰ã—ã¦ã„ã¾ã™ã€‚"
msgid "No licenses found."
msgstr "ライセンスãŒè¦‹ã¤ã‹ã‚Šã¾ã›ã‚“。"
@@ -13555,7 +13848,7 @@ msgid "No policy matches this license"
msgstr ""
msgid "No preview for this file type"
-msgstr ""
+msgstr "ã“ã®ãƒ•ã‚¡ã‚¤ãƒ«ã‚¿ã‚¤ãƒ—ã¯ãƒ—レビューã—ã¾ã›ã‚“"
msgid "No prioritized labels with such name or description"
msgstr ""
@@ -13593,15 +13886,6 @@ msgstr ""
msgid "No thanks, don't show this again"
msgstr ""
-msgid "No vulnerabilities found for this group"
-msgstr ""
-
-msgid "No vulnerabilities found for this pipeline"
-msgstr ""
-
-msgid "No vulnerabilities found for this project"
-msgstr ""
-
msgid "No vulnerabilities present"
msgstr ""
@@ -13612,7 +13896,7 @@ msgid "No worries, you can still use all the %{strong}%{plan_name}%{strong_close
msgstr ""
msgid "No, directly import the existing email addresses and usernames."
-msgstr ""
+msgstr "ã„ã„ãˆã€æ—¢å­˜ã®ãƒ¡ãƒ¼ãƒ«ã‚¢ãƒ‰ãƒ¬ã‚¹ã¨ãƒ¦ãƒ¼ã‚¶ãƒ¼åを直接インãƒãƒ¼ãƒˆã—ã¾ã™ã€‚"
msgid "No, not interested right now"
msgstr ""
@@ -13624,10 +13908,10 @@ msgid "Nobody has starred this repository yet"
msgstr ""
msgid "Node was successfully created."
-msgstr ""
+msgstr "ノードを正常ã«ä½œæˆã—ã¾ã—ãŸã€‚"
msgid "Node was successfully updated."
-msgstr ""
+msgstr "ノードã¯æ­£å¸¸ã«æ›´æ–°ã—ã¾ã—ãŸã€‚"
msgid "Nodes"
msgstr "ノード"
@@ -13669,7 +13953,7 @@ msgid "Not now"
msgstr "後ã§"
msgid "Not ready yet. Try again later."
-msgstr ""
+msgstr "準備ãŒã¾ã ã§ãã¦ã„ã¾ã›ã‚“。ã‚ã¨ã§ã‚‚ã†ä¸€åº¦è©¦ã—ã¦ãã ã•ã„。"
msgid "Not started"
msgstr "開始ã•ã‚Œã¦ã„ã¾ã›ã‚“"
@@ -13684,7 +13968,7 @@ msgid "Note parameters are invalid: %{errors}"
msgstr ""
msgid "Note that this invitation was sent to %{mail_to_invite_email}, but you are signed in as %{link_to_current_user} with email %{mail_to_current_user}."
-msgstr ""
+msgstr "ã“ã®æ‹›å¾…メール㯠%{mail_to_invite_email} å®›ã«é€ä¿¡ã—ã¾ã—ãŸãŒã€ã‚ãªãŸã¯ %{link_to_current_user} ã¨ã—ã¦ãŠã‚Šã€ãã®Eメールアドレス㯠%{mail_to_current_user} ã§ã™ã€‚"
msgid "Note: As an administrator you may like to configure %{github_integration_link}, which will allow login via GitHub and allow connecting repositories without generating a Personal Access Token."
msgstr "注: 管ç†è€…ã¨ã—ã¦%{github_integration_link} を設定ã™ã‚‹ã¨ã€GitHub 経由ã®ãƒ­ã‚°ã‚¤ãƒ³ã‚’許å¯ã—ã€å€‹äººç”¨ã®ã‚¢ã‚¯ã‚»ã‚¹ãƒˆãƒ¼ã‚¯ãƒ³ã‚’生æˆã›ãšã«ãƒªãƒã‚¸ãƒˆãƒªã«æŽ¥ç¶šã™ã‚‹ã“ã¨ãŒã§ãã¾ã™ã€‚"
@@ -13699,13 +13983,13 @@ msgid "Note: Consider asking your GitLab administrator to configure %{github_int
msgstr "注: GitLab ã®ç®¡ç†è€…ã«%{github_integration_link} を設定ã—ã¦ã€GitHub 経由ã®ãƒ­ã‚°ã‚¤ãƒ³ãŒè¨±å¯ã—ã€å€‹äººç”¨ã®ã‚¢ã‚¯ã‚»ã‚¹ãƒˆãƒ¼ã‚¯ãƒ³ã‚’生æˆã›ãšã«ãƒªãƒã‚¸ãƒˆãƒªã‚’インãƒãƒ¼ãƒˆã§ããªã„ã‹å•ã„åˆã‚ã›ãã ã•ã„。"
msgid "NoteForm|Note"
-msgstr ""
+msgstr "メモ"
msgid "Notes|Are you sure you want to cancel creating this comment?"
msgstr "本当ã«ã“ã®ã‚³ãƒ¡ãƒ³ãƒˆã®ä½œæˆã‚’キャンセルã—ã¾ã™ã‹ï¼Ÿ"
msgid "Notes|Collapse replies"
-msgstr ""
+msgstr "返信を折りãŸãŸã‚€"
msgid "Notes|Show all activity"
msgstr "ã™ã¹ã¦ã®ã‚¢ã‚¯ãƒ†ã‚£ãƒ“ティを表示"
@@ -13717,7 +14001,7 @@ msgid "Notes|Show history only"
msgstr "履歴ã®ã¿è¡¨ç¤º"
msgid "Notes|This comment has changed since you started editing, please review the %{open_link}updated comment%{close_link} to ensure information is not lost"
-msgstr ""
+msgstr "ã“ã®ã‚³ãƒ¡ãƒ³ãƒˆã¯ç·¨é›†ã‚’始ã‚ã¦ã‹ã‚‰å¤‰æ›´ã•ã‚Œã¦ã„ã¾ã™ã€‚情報ãŒå¤±ã‚ã‚Œãªã„よã†ã«ã€%{open_link}æ›´æ–°ã•ã‚ŒãŸã‚³ãƒ¡ãƒ³ãƒˆ%{close_link}をレビューã—ã¦ãã ã•ã„。"
msgid "Nothing found…"
msgstr ""
@@ -13725,6 +14009,9 @@ msgstr ""
msgid "Nothing to preview."
msgstr "プレビューã§ãã‚‹ã‚‚ã®ã¯ä½•ã‚‚ã‚ã‚Šã¾ã›ã‚“。"
+msgid "Nothing to synchronize"
+msgstr ""
+
msgid "Notification events"
msgstr "イベント通知"
@@ -13735,7 +14022,7 @@ msgid "Notification setting - %{notification_title}"
msgstr "通知設定 - %{notification_title}"
msgid "Notification settings saved"
-msgstr ""
+msgstr "通知設定ãŒä¿å­˜ã•ã‚Œã¾ã—ãŸ"
msgid "NotificationEvent|Close issue"
msgstr "課題をクローズ"
@@ -13798,7 +14085,7 @@ msgid "NotificationLevel|Watch"
msgstr "ã™ã¹ã¦é€šçŸ¥"
msgid "NotificationSetting|Custom"
-msgstr ""
+msgstr "カスタム"
msgid "Notifications"
msgstr "通知"
@@ -13825,10 +14112,10 @@ msgid "Number of %{itemTitle}"
msgstr ""
msgid "Number of Elasticsearch replicas"
-msgstr ""
+msgstr "Elasticsearchã®ãƒ¬ãƒ—リカã®æ•°"
msgid "Number of Elasticsearch shards"
-msgstr ""
+msgstr "Elasticsearchã®ã‚·ãƒ£ãƒ¼ãƒ‰ã®æ•°"
msgid "Number of LOCs per commit"
msgstr ""
@@ -13858,7 +14145,7 @@ msgid "Object Storage replication"
msgstr ""
msgid "Object does not exist on the server or you don't have permissions to access it"
-msgstr ""
+msgstr "オブジェクトãŒã‚µãƒ¼ãƒãƒ¼ã«å­˜åœ¨ã—ã¾ã›ã‚“ã€ã¾ãŸã¯ã€ãã‚Œã«ã‚¢ã‚¯ã‚»ã‚¹ã™ã‚‹æ¨©é™ãŒã‚ã‚Šã¾ã›ã‚“"
msgid "Oct"
msgstr "10月"
@@ -13888,23 +14175,26 @@ msgid "On track"
msgstr ""
msgid "Onboarding"
-msgstr ""
+msgstr "オンボーディング"
msgid "Once imported, repositories can be mirrored over SSH. Read more %{link_start}here%{link_end}."
msgstr ""
msgid "Once removed, the fork relationship cannot be restored and you will no longer be able to send merge requests to the source."
+msgstr "一度削除ã™ã‚‹ã¨ã€forkã®é–¢ä¿‚ã‚’å…ƒã«æˆ»ã™ã“ã¨ã¯ã§ããªããªã‚Šã¾ã™ã€‚ã¾ãŸã€ã‚ãªãŸã¯ãã®ã‚½ãƒ¼ã‚¹ã¸ãƒžãƒ¼ã‚¸ãƒªã‚¯ã‚¨ã‚¹ãƒˆã‚’é€ä¿¡ã§ããªããªã‚Šã¾ã™ã€‚"
+
+msgid "Once the exported file is ready you can download it from this page."
msgstr ""
msgid "Once the exported file is ready, you will receive a notification email with a download link, or you can download it from this page."
-msgstr ""
+msgstr "エクスãƒãƒ¼ãƒˆã•ã‚ŒãŸãƒ•ã‚¡ã‚¤ãƒ«ã®æº–å‚™ãŒã§ããŸã‚‰ã€ã‚ãªãŸã¯ãƒ€ã‚¦ãƒ³ãƒ­ãƒ¼ãƒ‰ãƒªãƒ³ã‚¯ä»˜ãã®é€šçŸ¥ãƒ¡ãƒ¼ãƒ«ã‚’å—ã‘å–ã‚Œã¾ã™ã€‚ã¾ãŸã¯ã“ã®ãƒšãƒ¼ã‚¸ã‹ã‚‰ãれをダウンロードã§ãã¾ã™ã€‚"
msgid "Once you confirm and press \"Reduce project visibility\":"
msgstr ""
msgid "One more item"
msgid_plural "%d more items"
-msgstr[0] ""
+msgstr[0] "ãã®ä»–ã®ã‚¢ã‚¤ãƒ†ãƒ ã®ä»¶æ•° %d"
msgid "One or more groups that you don't have access to."
msgstr ""
@@ -13952,7 +14242,7 @@ msgid "Only policy:"
msgstr "ãƒãƒªã‚·ãƒ¼ã®ã¿:"
msgid "Only proceed if you trust %{idp_url} to control your GitLab account sign in."
-msgstr ""
+msgstr "GitLab アカウントã®ã‚µã‚¤ãƒ³ã‚¤ãƒ³ã‚’制御ã™ã‚‹ãŸã‚ã« %{idp_url} ã‚’ä¿¡é ¼ã™ã‚‹å ´åˆã«ã®ã¿ç¶šè¡Œã—ã¦ãã ã•ã„。"
msgid "Only project members can comment."
msgstr "プロジェクトメンãƒãƒ¼ã®ã¿ã‚³ãƒ¡ãƒ³ãƒˆã§ãã¾ã™"
@@ -13967,7 +14257,7 @@ msgid "Only ‘Reporter’ roles and above on tiers Premium / Silver and above c
msgstr ""
msgid "Oops, are you sure?"
-msgstr ""
+msgstr "ãã‚Œã¯ã€æœ¬å½“ã§ã™ã‹ï¼Ÿ"
msgid "Open"
msgstr "é–‹ã"
@@ -13996,9 +14286,6 @@ msgstr ""
msgid "Open sidebar"
msgstr "サイドãƒãƒ¼ã‚’é–‹ã"
-msgid "Open source software to collaborate on code"
-msgstr ""
-
msgid "Open: %{openIssuesCount}"
msgstr ""
@@ -14012,7 +14299,7 @@ msgid "Opened MR"
msgstr "オープン中ã®ãƒžãƒ¼ã‚¸ãƒªã‚¯ã‚¨ã‚¹ãƒˆ"
msgid "Opened issues"
-msgstr ""
+msgstr "オープン中ã®èª²é¡Œ"
msgid "OpenedNDaysAgo|Opened"
msgstr "オープンã•ã‚ŒãŸã®ã¯"
@@ -14033,7 +14320,7 @@ msgid "Operations Dashboard"
msgstr ""
msgid "Operations Settings"
-msgstr ""
+msgstr "æ“作設定"
msgid "OperationsDashboard|Add a project to the dashboard"
msgstr "ダッシュボードã«ãƒ—ロジェクトを追加"
@@ -14042,13 +14329,13 @@ msgid "OperationsDashboard|Add projects"
msgstr "プロジェクトを追加"
msgid "OperationsDashboard|More information"
-msgstr ""
+msgstr "詳細情報"
msgid "OperationsDashboard|Operations Dashboard"
-msgstr ""
+msgstr "オペレーションダッシュボード"
msgid "OperationsDashboard|The operations dashboard provides a summary of each project's operational health, including pipeline and alert statuses."
-msgstr ""
+msgstr "オペレーションダッシュボードã¯å„プロジェクトé‹ç”¨çŠ¶æ…‹ï¼ˆãƒ‘イプライン&アラート状態ãªã©ï¼‰ã‚’表示ã•ã‚Œã¾ã™ã€‚"
msgid "Optional"
msgstr "オプション"
@@ -14078,7 +14365,7 @@ msgid "Other information"
msgstr "ãã®ä»–ã®æƒ…å ±"
msgid "Other merge requests block this MR"
-msgstr ""
+msgstr "ä»–ã®ãƒžãƒ¼ã‚¸ãƒªã‚¯ã‚¨ã‚¹ãƒˆã¯ã“ã®ãƒžãƒ¼ã‚¸ãƒªã‚¯ã‚¨ã‚¹ãƒˆã‚’ブロックã—ã¾ã™"
msgid "Other visibility settings have been disabled by the administrator."
msgstr ""
@@ -14087,7 +14374,7 @@ msgid "Out-of-compliance with this project's policies and should be removed"
msgstr ""
msgid "Outbound requests"
-msgstr ""
+msgstr "アウトãƒã‚¦ãƒ³ãƒ‰ãƒªã‚¯ã‚¨ã‚¹ãƒˆ"
msgid "OutdatedBrowser|From May 2020 GitLab no longer supports Internet Explorer 11."
msgstr ""
@@ -14101,23 +14388,26 @@ msgstr ""
msgid "OutdatedBrowser|You can provide feedback %{feedback_link_start}on this issue%{feedback_link_end} or via your usual support channels."
msgstr ""
+msgid "Overridden"
+msgstr ""
+
msgid "Overview"
msgstr "概è¦"
msgid "Overwrite diverged branches"
-msgstr ""
+msgstr "分å²ã—ãŸãƒ–ランãƒã®ä¸Šæ›¸ã"
msgid "Owned by anyone"
msgstr ""
msgid "Owned by me"
-msgstr ""
+msgstr "ç§ãŒæ‰€æœ‰"
msgid "Owner"
msgstr "オーナー"
msgid "Package Registry"
-msgstr ""
+msgstr "パッケージレジストリ"
msgid "Package already exists"
msgstr ""
@@ -14152,6 +14442,9 @@ msgstr ""
msgid "PackageRegistry|Conan Command"
msgstr ""
+msgid "PackageRegistry|Copy .pypirc content"
+msgstr ""
+
msgid "PackageRegistry|Copy Conan Command"
msgstr ""
@@ -14173,6 +14466,9 @@ msgstr ""
msgid "PackageRegistry|Copy NuGet Setup Command"
msgstr ""
+msgid "PackageRegistry|Copy Pip command"
+msgstr ""
+
msgid "PackageRegistry|Copy and paste this inside your %{codeStart}pom.xml%{codeEnd} %{codeStart}dependencies%{codeEnd} block."
msgstr ""
@@ -14206,6 +14502,12 @@ msgstr ""
msgid "PackageRegistry|For more information on the NuGet registry, %{linkStart}see the documentation%{linkEnd}."
msgstr ""
+msgid "PackageRegistry|For more information on the PyPi registry, %{linkStart}see the documentation%{linkEnd}."
+msgstr ""
+
+msgid "PackageRegistry|If you haven't already done so, you will need to add the below to your %{codeStart}.pypirc%{codeEnd} file."
+msgstr ""
+
msgid "PackageRegistry|If you haven't already done so, you will need to add the below to your %{codeStart}pom.xml%{codeEnd} file."
msgstr ""
@@ -14215,6 +14517,9 @@ msgstr ""
msgid "PackageRegistry|Learn how to %{noPackagesLinkStart}publish and share your packages%{noPackagesLinkEnd} with GitLab."
msgstr ""
+msgid "PackageRegistry|Manually Published"
+msgstr ""
+
msgid "PackageRegistry|Maven"
msgstr ""
@@ -14233,12 +14538,18 @@ msgstr ""
msgid "PackageRegistry|NuGet Command"
msgstr ""
+msgid "PackageRegistry|Pip Command"
+msgstr ""
+
msgid "PackageRegistry|Pipeline %{linkStart}%{linkEnd} triggered %{timestamp} by %{author}"
msgstr ""
msgid "PackageRegistry|Published to the repository at %{timestamp}"
msgstr ""
+msgid "PackageRegistry|PyPi"
+msgstr ""
+
msgid "PackageRegistry|Registry Setup"
msgstr ""
@@ -14275,6 +14586,9 @@ msgstr ""
msgid "PackageRegistry|npm"
msgstr ""
+msgid "PackageRegistry|published by %{author}"
+msgstr ""
+
msgid "PackageRegistry|yarn"
msgstr ""
@@ -14290,23 +14604,26 @@ msgstr ""
msgid "PackageType|NuGet"
msgstr ""
+msgid "PackageType|PyPi"
+msgstr ""
+
msgid "Packages"
msgstr "パッケージ"
msgid "Page not found"
-msgstr ""
+msgstr "ページãŒè¦‹ã¤ã‹ã‚Šã¾ã›ã‚“"
msgid "Page was successfully deleted"
-msgstr ""
+msgstr "ページã¯æ­£å¸¸ã«å‰Šé™¤ã•ã‚Œã¾ã—ãŸ"
msgid "Pages"
msgstr "Pages"
msgid "Pages Domain"
-msgstr ""
+msgstr "ページドメイン"
msgid "Pages getting started guide"
-msgstr ""
+msgstr "Pages 入門ガイド"
msgid "Pagination|Go to first page"
msgstr ""
@@ -14369,28 +14686,31 @@ msgid "Password Policy Guidelines"
msgstr ""
msgid "Password authentication is unavailable."
-msgstr ""
+msgstr "パスワードèªè¨¼ã¯ã”利用ã§ãã¾ã›ã‚“。"
msgid "Password confirmation"
msgstr "パスワードã®ç¢ºèª"
msgid "Password successfully changed"
-msgstr ""
+msgstr "パスワードãŒæ­£å¸¸ã«å¤‰æ›´ã•ã‚Œã¾ã—ãŸ"
msgid "Password was successfully updated. Please login with it"
-msgstr ""
+msgstr "パスワードã¯æ­£å¸¸ã«æ›´æ–°ã•ã‚Œã¾ã—ãŸã€‚æ–°ã—ã„パスワードã§ãƒ­ã‚°ã‚¤ãƒ³ã—ãªãŠã—ã¦ãã ã•ã„。"
msgid "Past due"
msgstr "期é™åˆ‡ã‚Œ"
-msgid "Paste a machine public key here. Read more about how to generate it %{link_start}here%{link_end}"
+msgid "Paste a machine public key here. Read more about how to generate it"
msgstr ""
+msgid "Paste a machine public key here. Read more about how to generate it %{link_start}here%{link_end}"
+msgstr "マシンã®å…¬é–‹éµã‚’ã“ã“ã«è²¼ã‚Šä»˜ã‘ã¾ã™ã€‚生æˆæ–¹æ³•ã«ã¤ã„ã¦ã¯%{link_start}ã“ã¡ã‚‰%{link_end}を確èªã—ã¦ä¸‹ã•ã„。"
+
msgid "Paste epic link"
-msgstr ""
+msgstr "エピックリンクã®è²¼ã‚Šä»˜ã‘"
msgid "Paste issue link"
-msgstr ""
+msgstr "課題リンクã®è²¼ã‚Šä»˜ã‘"
msgid "Paste your public SSH key, which is usually contained in the file '~/.ssh/id_ed25519.pub' or '~/.ssh/id_rsa.pub' and begins with 'ssh-ed25519' or 'ssh-rsa'. Don't use your private SSH key."
msgstr ""
@@ -14398,9 +14718,6 @@ msgstr ""
msgid "Path"
msgstr "パス"
-msgid "Path, transfer, remove"
-msgstr ""
-
msgid "Path:"
msgstr "パス:"
@@ -14411,10 +14728,10 @@ msgid "Pause"
msgstr "åœæ­¢"
msgid "Pause replication"
-msgstr ""
+msgstr "レプリケーションã®ä¸€æ™‚åœæ­¢"
msgid "Paused Runners don't accept new jobs"
-msgstr ""
+msgstr "一時åœæ­¢ã—ãŸãƒ©ãƒ³ãƒŠãƒ¼ã¯æ–°ã—ã„ジョブをå—ã‘入れãªã„"
msgid "Pending"
msgstr "ä¿ç•™ä¸­"
@@ -14431,8 +14748,8 @@ msgstr ""
msgid "Percentage"
msgstr ""
-msgid "Perform advanced options such as changing path, transferring, or removing the group."
-msgstr "パスã®å¤‰æ›´ã€è»¢é€ã€ã‚°ãƒ«ãƒ¼ãƒ—ã®å‰Šé™¤ãªã©ã®é«˜åº¦ãªã‚ªãƒ—ションを実行ã—ã¾ã™ã€‚"
+msgid "Perform advanced options such as changing path, transferring, exporting, or removing the group."
+msgstr ""
msgid "Perform common operations on GitLab project"
msgstr ""
@@ -14447,7 +14764,7 @@ msgid "PerformanceBar|Frontend resources"
msgstr ""
msgid "PerformanceBar|Gitaly calls"
-msgstr ""
+msgstr "Gitalyリクエスト"
msgid "PerformanceBar|Redis calls"
msgstr ""
@@ -14512,9 +14829,6 @@ msgstr "パイプラインスケジュール"
msgid "Pipeline minutes quota"
msgstr ""
-msgid "Pipeline quota"
-msgstr "パイプラインã®ã‚¯ã‚©ãƒ¼ã‚¿"
-
msgid "Pipeline subscriptions"
msgstr ""
@@ -14524,6 +14838,9 @@ msgstr "パイプラインã®ãƒˆãƒªã‚¬ãƒ¼"
msgid "Pipeline: %{status}"
msgstr ""
+msgid "PipelineCharts|CI / CD Analytics"
+msgstr ""
+
msgid "PipelineCharts|Failed:"
msgstr "失敗:"
@@ -14602,20 +14919,11 @@ msgstr ""
msgid "Pipelines settings for '%{project_name}' were successfully updated."
msgstr "%{project_name} ã®ãƒ‘イプライン設定を正常ã«æ›´æ–°ã—ã¾ã—ãŸã€‚"
-msgid "Pipelines| to purchase more minutes."
-msgstr ""
-
-msgid "Pipelines|%{namespace_name} has exceeded its pipeline minutes quota."
-msgstr ""
-
-msgid "Pipelines|%{namespace_name} has less than %{notification_level}%% of CI minutes available."
-msgstr ""
-
msgid "Pipelines|API"
msgstr "API"
msgid "Pipelines|Build with confidence"
-msgstr ""
+msgstr "ä¿¡é ¼ã®ã‚るビルド"
msgid "Pipelines|CI Lint"
msgstr "CI Lint"
@@ -14627,17 +14935,20 @@ msgid "Pipelines|Clear Runner Caches"
msgstr "Runner ã®ã‚­ãƒ£ãƒƒã‚·ãƒ¥ã‚’削除"
msgid "Pipelines|Continuous Integration can help catch bugs by running your tests automatically, while Continuous Deployment can help you deliver code to your product environment."
-msgstr ""
+msgstr "継続的インテグレーションã¯ã€ãƒ†ã‚¹ãƒˆã‚’自動的ã«å®Ÿè¡Œã™ã‚‹ã“ã¨ã§ãƒã‚°ã‚’検出ã—ã€ç¶™ç¶šçš„デプロイã¯æœ¬ç•ªç’°å¢ƒã«ã‚³ãƒ¼ãƒ‰ã‚’é…ç½®ã™ã‚‹ã®ã«å½¹ç«‹ã¡ã¾ã™ã€‚"
msgid "Pipelines|Get started with Pipelines"
msgstr "パイプラインã®åˆ©ç”¨ã‚’開始ã™ã‚‹"
-msgid "Pipelines|Loading Pipelines"
-msgstr "パイプラインを読ã¿è¾¼ã¿ä¸­"
+msgid "Pipelines|Group %{namespace_name} has %{percentage}%% or less Shared Runner Pipeline minutes remaining. Once it runs out, no new jobs or pipelines in its projects will run."
+msgstr ""
-msgid "Pipelines|Pipelines will not run anymore on shared Runners."
+msgid "Pipelines|Group %{namespace_name} has exceeded its pipeline minutes quota. Unless you buy additional pipeline minutes, no new jobs or pipelines in its projects will run."
msgstr ""
+msgid "Pipelines|Loading Pipelines"
+msgstr "パイプラインを読ã¿è¾¼ã¿ä¸­"
+
msgid "Pipelines|Project cache successfully reset."
msgstr "プロジェクトã®ã‚­ãƒ£ãƒƒã‚·ãƒ¥ã‚’正常ã«ãƒªã‚»ãƒƒãƒˆã—ã¾ã—ãŸã€‚"
@@ -14684,7 +14995,7 @@ msgid "Pipeline|Duration"
msgstr "期間"
msgid "Pipeline|Existing branch name or tag"
-msgstr ""
+msgstr "既存ã®ãƒ–ランãƒåã¾ãŸã¯tag"
msgid "Pipeline|Key"
msgstr ""
@@ -14705,7 +15016,7 @@ msgid "Pipeline|Run Pipeline"
msgstr "パイプラインを実行"
msgid "Pipeline|Run for"
-msgstr ""
+msgstr "実行対象"
msgid "Pipeline|Search branches"
msgstr "ブランãƒã®æ¤œç´¢"
@@ -14756,10 +15067,10 @@ msgid "PivotalTrackerService|Pivotal Tracker API token."
msgstr "Pivotal Tracker APIトークン。"
msgid "PivotalTrackerService|Project Management Software (Source Commits Endpoint)"
-msgstr ""
+msgstr "プロジェクト管ç†ã‚½ãƒ•ãƒˆã‚¦ã‚§ã‚¢(ソースコミットエンドãƒã‚¤ãƒ³ãƒˆ)"
msgid "Plain diff"
-msgstr ""
+msgstr "テキスト差分"
msgid "PlantUML"
msgstr "PlantUML"
@@ -14783,7 +15094,7 @@ msgid "Please add a comment in the text area above"
msgstr "上ã®ãƒ†ã‚­ã‚¹ãƒˆã‚¨ãƒªã‚¢ã«ã‚³ãƒ¡ãƒ³ãƒˆã‚’加ãˆã¦ãã ã•ã„。"
msgid "Please add a list to your board first"
-msgstr ""
+msgstr "ã¾ãšã‚ãªãŸã®ãƒœãƒ¼ãƒ‰ã«ãƒªã‚¹ãƒˆã‚’追加ã—ã¦ãã ã•ã„"
msgid "Please check the configuration file for this chart"
msgstr "ãƒãƒ£ãƒ¼ãƒˆã®è¨­å®šãƒ•ã‚¡ã‚¤ãƒ«ã‚’確èªã—ã¦ãã ã•ã„"
@@ -14804,7 +15115,7 @@ msgid "Please complete your profile with email address"
msgstr ""
msgid "Please convert them to %{link_to_git}, and go through the %{link_to_import_flow} again."
-msgstr ""
+msgstr "ãれらを %{link_to_git} ã«å¤‰æ›ã—ã¦ã€å†ã³ %{link_to_import_flow} ã‚’è¡Œã£ã¦ãã ã•ã„。"
msgid "Please convert them to Git on Google Code, and go through the %{link_to_import_flow} again."
msgstr "ãれらを Google コード上㧠Git ã«å¤‰æ›ã—ã¦ã€å†ã³ %{link_to_import_flow} ã‚’è¡Œã£ã¦ãã ã•ã„。"
@@ -14819,7 +15130,7 @@ msgid "Please create an index before enabling indexing"
msgstr ""
msgid "Please enable and migrate to hashed storage to avoid security issues and ensure data integrity. %{migrate_link}"
-msgstr ""
+msgstr "セキュリティ上ã®å•é¡Œå›žé¿ã¨ãƒ‡ãƒ¼ã‚¿æ•´åˆæ€§ã®ç¢ºä¿ã®ãŸã‚ã€ãƒãƒƒã‚·ãƒ¥åŒ–ã•ã‚ŒãŸã‚¹ãƒˆãƒ¬ãƒ¼ã‚¸ã‚’有効化ã—ã€ç§»è¡Œã—ã¦ãã ã•ã„。 %{migrate_link}"
msgid "Please ensure your account's %{account_link_start}recovery settings%{account_link_end} are up to date."
msgstr ""
@@ -14828,7 +15139,7 @@ msgid "Please enter a non-negative number"
msgstr "有効ãªã‚¼ãƒ­ä»¥ä¸Šã®æ•°å­—を入力ã—ã¦ãã ã•ã„"
msgid "Please enter a number greater than %{number} (from the project settings)"
-msgstr ""
+msgstr "%{number} より大ãã„数字を入力ã—ã¦ãã ã•ã„。(プロジェクト設定ã®ãŸã‚)"
msgid "Please enter a valid number"
msgstr "有効ãªæ•°å€¤ã‚’入力ã—ã¦ãã ã•ã„"
@@ -14846,7 +15157,7 @@ msgid "Please follow the Let's Encrypt troubleshooting instructions to re-obtain
msgstr ""
msgid "Please migrate all existing projects to hashed storage to avoid security issues and ensure data integrity. %{migrate_link}"
-msgstr ""
+msgstr "ã™ã¹ã¦ã®æ—¢å­˜ã®ãƒ—ロジェクトをãƒãƒƒã‚·ãƒ¥åŒ–ã—ãŸã‚¹ãƒˆãƒ¬ãƒ¼ã‚¸ã«ç§»è¡Œã—ã¦ã€ã‚»ã‚­ãƒ¥ãƒªãƒ†ã‚£ä¸Šã®å•é¡Œã‚’回é¿ã—ã¦ã€ãƒ‡ãƒ¼ã‚¿æ•´åˆæ€§ã®ç¢ºä¿ã—ã¦ãã ã•ã„。%{migrate_link}"
msgid "Please note that this application is not provided by GitLab and you should verify its authenticity before allowing access."
msgstr "ã“ã®ã‚¢ãƒ—リケーション㯠GitLab ãŒæä¾›ã—ã¦ã„ã¾ã›ã‚“。ãã®ãŸã‚ã‚ãªãŸãŒã‚¢ã‚¯ã‚»ã‚¹ã‚’許å¯ã™ã‚‹å‰ã«ãã®èªè¨¼ã‚’確èªã™ã‚‹å¿…è¦ãŒã‚ã‚Šã¾ã™ã€‚"
@@ -14866,6 +15177,9 @@ msgstr "メールアドレスをå†åº¦å…¥åŠ›ã—ã¦ãã ã•ã„。"
msgid "Please select"
msgstr "é¸æŠžã—ã¦ãã ã•ã„"
+msgid "Please select a Jira project"
+msgstr ""
+
msgid "Please select a country"
msgstr "国をé¸æŠžã—ã¦ãã ã•ã„"
@@ -14903,7 +15217,7 @@ msgid "Please use this form to report to the admin users who create spam issues,
msgstr ""
msgid "Please wait a moment, this page will automatically refresh when ready."
-msgstr ""
+msgstr "少々ãŠå¾…ã¡ãã ã•ã„。準備ãŒæ•´ã„次第ã“ã®ãƒšãƒ¼ã‚¸ã¯è‡ªå‹•çš„ã«æ›´æ–°ã•ã‚Œã¾ã™ã€‚"
msgid "Please wait while we connect to your repository. Refresh at will."
msgstr "リãƒã‚¸ãƒˆãƒªã«æŽ¥ç¶šã§ãã‚‹ã¾ã§ã€ã—ã°ã‚‰ããŠå¾…ã¡ãã ã•ã„。完了ã™ã‚‹ã¨ã€ç”»é¢ãŒè‡ªå‹•çš„ã«æ›´æ–°ã•ã‚Œã¾ã™ã€‚"
@@ -14911,6 +15225,9 @@ msgstr "リãƒã‚¸ãƒˆãƒªã«æŽ¥ç¶šã§ãã‚‹ã¾ã§ã€ã—ã°ã‚‰ããŠå¾…ã¡ãã ã•
msgid "Please wait while we import the repository for you. Refresh at will."
msgstr "リãƒã‚¸ãƒˆãƒªã‚’インãƒãƒ¼ãƒˆã—ã¦ã„ã‚‹ã®ã§ã€ã—ã°ã‚‰ããŠå¾…ã¡ãã ã•ã„。完了ã™ã‚‹ã¨ã€ç”»é¢ãŒè‡ªå‹•çš„ã«æ›´æ–°ã•ã‚Œã¾ã™ã€‚"
+msgid "Plugins directory is deprecated and will be removed in 14.0. Please move this file into /file_hooks directory."
+msgstr ""
+
msgid "Pod does not exist"
msgstr ""
@@ -14927,7 +15244,7 @@ msgid "Preferences"
msgstr "基本設定"
msgid "Preferences saved."
-msgstr ""
+msgstr "設定ãŒä¿å­˜ã•ã‚Œã¾ã—ãŸã€‚"
msgid "Preferences|Behavior"
msgstr "挙動"
@@ -14948,13 +15265,13 @@ msgid "Preferences|Default dashboard"
msgstr ""
msgid "Preferences|Display time in 24-hour format"
-msgstr ""
+msgstr "時間を24時間形å¼ã§è¡¨ç¤ºã™ã‚‹"
msgid "Preferences|Enable integrated code intelligence on code views"
msgstr ""
msgid "Preferences|For example: 30 mins ago."
-msgstr ""
+msgstr "例: 30分å‰"
msgid "Preferences|Integrations"
msgstr ""
@@ -14987,7 +15304,7 @@ msgid "Preferences|Tab width"
msgstr ""
msgid "Preferences|These settings will update how dates and times are displayed for you."
-msgstr ""
+msgstr "ã“れらã®è¨­å®šã¯æ—¥ä»˜ã¨æ™‚刻ã®è¡¨ç¤ºå½¢å¼ã‚’æ›´æ–°ã—ã¾ã™ã€‚"
msgid "Preferences|This feature is experimental and translations are not complete yet"
msgstr "ã“ã®æ©Ÿèƒ½ã¯å®Ÿé¨“çš„ãªã‚‚ã®ã§ã™ã€‚ã¾ãŸç¿»è¨³ã¯ã¾ã å®Œäº†ã—ã¦ã„ã¾ã›ã‚“"
@@ -14999,28 +15316,28 @@ msgid "Preferences|This setting allows you to customize the behavior of the syst
msgstr ""
msgid "Preferences|Time display"
-msgstr ""
+msgstr "時間表示"
msgid "Preferences|Time format"
msgstr "時間フォーマット"
msgid "Preferences|Time preferences"
-msgstr ""
+msgstr "時間設定"
msgid "Preferences|Use relative times"
-msgstr ""
+msgstr "相対時間を使用"
msgid "Press %{key}-C to copy"
-msgstr ""
+msgstr "%{key} + C を押ã—ã¦ã‚³ãƒ”ーã™ã‚‹"
msgid "Prevent adding new members to project membership within this group"
msgstr ""
msgid "Prevent approval of merge requests by merge request author"
-msgstr ""
+msgstr "マージリクエストã®ä½œæˆè€…ã«ã‚ˆã‚‹ãƒžãƒ¼ã‚¸ãƒªã‚¯ã‚¨ã‚¹ãƒˆã®æ‰¿èªã‚’ç¦æ­¢ã™ã‚‹"
msgid "Prevent approval of merge requests by merge request committers"
-msgstr ""
+msgstr "マージリクエストã®ã‚³ãƒŸãƒƒã‚¿ãƒ¼ã«ã‚ˆã‚‹ãƒžãƒ¼ã‚¸ãƒªã‚¯ã‚¨ã‚¹ãƒˆã®æ‰¿èªã‚’ç¦æ­¢ã™ã‚‹"
msgid "Prevent environment from auto-stopping"
msgstr ""
@@ -15028,7 +15345,7 @@ msgstr ""
msgid "Prevent users from changing their profile name"
msgstr ""
-msgid "Prevent users from modifing merge request approvers list"
+msgid "Prevent users from modifying merge request approvers list"
msgstr ""
msgid "Prevent users from performing write operations on GitLab while performing maintenance."
@@ -15044,15 +15361,15 @@ msgid "Preview changes"
msgstr "変更をプレビュー"
msgid "Preview payload"
-msgstr ""
+msgstr "データ部ã®ãƒ—レビュー"
msgid "Previous Artifacts"
-msgstr ""
+msgstr "å‰å›žã®æˆæžœç‰©"
-msgid "Previous file in diff (MRs only)"
+msgid "Previous file in diff"
msgstr ""
-msgid "Previous unresolved discussion (MRs only)"
+msgid "Previous unresolved discussion"
msgstr ""
msgid "Primary"
@@ -15080,16 +15397,19 @@ msgid "Private - The group and its projects can only be viewed by members."
msgstr "プライベート - グループã¨ãƒ—ロジェクトã¯ãƒ¡ãƒ³ãƒãƒ¼ã®ã¿ãŒé–²è¦§ã§ãã¾ã™ã€‚"
msgid "Private group(s)"
-msgstr ""
+msgstr "éžå…¬é–‹ã‚°ãƒ«ãƒ¼ãƒ—"
msgid "Private profile"
msgstr ""
+msgid "Private projects Minutes cost factor"
+msgstr ""
+
msgid "Private projects can be created in your personal namespace with:"
msgstr "ã‚ãªãŸå€‹äººã®åå‰ç©ºé–“ã«ãƒ—ライベートプロジェクトを作æˆã§ãã¾ã™:"
msgid "Proceed"
-msgstr ""
+msgstr "続行"
msgid "Productivity"
msgstr ""
@@ -15149,13 +15469,13 @@ msgid "Profile Settings"
msgstr "プロファイルã®è¨­å®š"
msgid "ProfileSession|on"
-msgstr ""
+msgstr "オン"
msgid "Profiles| You are about to permanently delete %{yourAccount}, and all of the issues, merge requests, and groups linked to your account. Once you confirm %{deleteAccount}, it cannot be undone or recovered."
-msgstr ""
+msgstr "ã‚ãªãŸã¯ %{yourAccount} ã¨ã€ã‚ãªãŸã®ã‚¢ã‚«ã‚¦ãƒ³ãƒˆã«ãƒªãƒ³ã‚¯ã•ã‚Œã¦ã„ã‚‹ã™ã¹ã¦ã®èª²é¡Œã€ãƒžãƒ¼ã‚¸ãƒªã‚¯ã‚¨ã‚¹ãƒˆã€ã‚°ãƒ«ãƒ¼ãƒ—を削除ã—よã†ã¨ã—ã¦ã„ã¾ã™ã€‚ %{deleteAccount} を確èªã™ã‚‹ã¨ã€å…ƒã«æˆ»ã™ã“ã¨ã¯ã§ãã¾ã›ã‚“。"
msgid "Profiles| You are going to change the username %{currentUsernameBold} to %{newUsernameBold}. Profile and projects will be redirected to the %{newUsername} namespace but this redirect will expire once the %{currentUsername} namespace is registered by another user or group. Please update your Git repository remotes as soon as possible."
-msgstr ""
+msgstr "ユーザåã‚’ %{currentUsernameBold} ã‹ã‚‰ %{newUsernameBold} ã«å¤‰æ›´ã—よã†ã¨ã—ã¦ã„ã¾ã™ã€‚プロファイルã¨ãƒ—ロジェクト㯠%{newUsername} åå‰ç©ºé–“ã«ãƒªãƒ€ã‚¤ãƒ¬ã‚¯ãƒˆã•ã‚Œã¾ã™ã€‚ã—ã‹ã—〠%{currentUsername} åå‰ç©ºé–“ãŒåˆ¥ã®ãƒ¦ãƒ¼ã‚¶ãƒ¼ã¾ãŸã¯ã‚°ãƒ«ãƒ¼ãƒ—ã«ã‚ˆã£ã¦ç™»éŒ²ã•ã‚Œã‚‹ã¨ã€ã“ã®ãƒªãƒ€ã‚¤ãƒ¬ã‚¯ãƒˆã¯æœŸé™åˆ‡ã‚Œã«ãªã‚Šã¾ã™ã€‚ Gitリãƒã‚¸ãƒˆãƒªã®ãƒªãƒ¢ãƒ¼ãƒˆã‚’ã§ãã‚‹ã ã‘æ—©ãæ›´æ–°ã—ã¦ãã ã•ã„。"
msgid "Profiles|@username"
msgstr "@username"
@@ -15164,7 +15484,7 @@ msgid "Profiles|Account scheduled for removal."
msgstr "削除予定ã®ã‚¢ã‚«ã‚¦ãƒ³ãƒˆã§ã™ã€‚"
msgid "Profiles|Activate signin with one of the following services"
-msgstr ""
+msgstr "次ã®ã‚µãƒ¼ãƒ“スã®ä¸­ã‹ã‚‰ä¸€ã¤ã‚’é¸ã‚“ã§ã‚µã‚¤ãƒ³ã‚¤ãƒ³ã‚’有効ã«ã—ã¦ãã ã•ã„"
msgid "Profiles|Active"
msgstr "アクティブ"
@@ -15176,19 +15496,19 @@ msgid "Profiles|Add status emoji"
msgstr "ステータス絵文字を追加"
msgid "Profiles|Avatar cropper"
-msgstr ""
+msgstr "ã‚¢ãƒã‚¿ãƒ¼ã‚¯ãƒ­ãƒƒãƒ‘ー"
msgid "Profiles|Avatar will be removed. Are you sure?"
msgstr "ã‚¢ãƒã‚¿ãƒ¼ã‚’削除ã—ã¾ã™ã€‚よã‚ã—ã„ã§ã™ã‹ï¼Ÿ"
msgid "Profiles|Bio"
-msgstr ""
+msgstr "BIO"
msgid "Profiles|Change username"
msgstr "ユーザーåã®å¤‰æ›´"
msgid "Profiles|Changing your username can have unintended side effects."
-msgstr ""
+msgstr "ユーザーåを変更ã™ã‚‹ã¨ã€æ„図ã—ãªã„副作用ãŒç™ºç”Ÿã™ã‚‹å¯èƒ½æ€§ãŒã‚ã‚Šã¾ã™ã€‚"
msgid "Profiles|Choose file..."
msgstr "ファイルをé¸æŠž..."
@@ -15206,13 +15526,13 @@ msgid "Profiles|Click on icon to activate signin with one of the following servi
msgstr ""
msgid "Profiles|Commit email"
-msgstr ""
+msgstr "コミットメール"
msgid "Profiles|Connect"
-msgstr ""
+msgstr "接続"
msgid "Profiles|Connected Accounts"
-msgstr ""
+msgstr "接続ã—ãŸã‚¢ã‚«ã‚¦ãƒ³ãƒˆ"
msgid "Profiles|Current path: %{path}"
msgstr "ç¾åœ¨ã®ãƒ‘ス: %{path}"
@@ -15221,7 +15541,7 @@ msgid "Profiles|Current status"
msgstr "ç¾åœ¨ã®çŠ¶æ…‹"
msgid "Profiles|Default notification email"
-msgstr ""
+msgstr "デフォルト通知メール"
msgid "Profiles|Delete Account"
msgstr "アカウント削除"
@@ -15236,19 +15556,19 @@ msgid "Profiles|Deleting an account has the following effects:"
msgstr "アカウントを削除ã™ã‚‹ã¨æ¬¡ã®ã‚ˆã†ãªå½±éŸ¿ãŒã‚ã‚Šã¾ã™:"
msgid "Profiles|Disconnect"
-msgstr ""
+msgstr "接続断"
msgid "Profiles|Do not show on profile"
msgstr "プロフィールã«è¡¨ç¤ºã—ãªã„"
msgid "Profiles|Don't display activity-related personal information on your profiles"
-msgstr ""
+msgstr "プロファイルã«æ“作ã«é–¢é€£ã—ãŸå€‹äººæƒ…報を表示ã—ãªã„"
msgid "Profiles|Edit Profile"
msgstr "プロフィールを編集"
msgid "Profiles|Enter your name, so people you know can recognize you"
-msgstr ""
+msgstr "ä»–ã®äººãŒã‚ãªãŸã ã¨ã‚ã‹ã‚‹åå‰ã‚’入力ã—ã¦ãã ã•ã„。"
msgid "Profiles|Expires at"
msgstr ""
@@ -15257,7 +15577,7 @@ msgid "Profiles|Expires:"
msgstr ""
msgid "Profiles|Feed token was successfully reset"
-msgstr ""
+msgstr "フィードトークンを正常ã«ãƒªã‚»ãƒƒãƒˆã§ãã¾ã—ãŸ"
msgid "Profiles|Full name"
msgstr "フルãƒãƒ¼ãƒ "
@@ -15269,10 +15589,10 @@ msgid "Profiles|Impersonation"
msgstr ""
msgid "Profiles|Include private contributions on my profile"
-msgstr ""
+msgstr "プロフィールã«å€‹äººçš„ãªã‚³ãƒ³ãƒˆãƒªãƒ“ューションをå«ã‚ã‚‹"
msgid "Profiles|Incoming email token was successfully reset"
-msgstr ""
+msgstr "å—信用ã®ãƒ¡ãƒ¼ãƒ«ãƒˆãƒ¼ã‚¯ãƒ³ã‚’正常ã«ãƒªã‚»ãƒƒãƒˆã§ãã¾ã—ãŸ"
msgid "Profiles|Increase your account's security by enabling Two-Factor Authentication (2FA)"
msgstr "2è¦ç´ èªè¨¼ã‚’有効ã«ã™ã‚‹ã“ã¨ã§ã€ã‚ãªãŸã®ã‚¢ã‚«ã‚¦ãƒ³ãƒˆã®ã‚»ã‚­ãƒ¥ãƒªãƒ†ã‚£ãŒå‘上ã—ã¾ã™ã€‚"
@@ -15290,13 +15610,13 @@ msgid "Profiles|Last used:"
msgstr ""
msgid "Profiles|Learn more"
-msgstr ""
+msgstr "詳細情報"
msgid "Profiles|Location"
-msgstr ""
+msgstr "場所"
msgid "Profiles|Made a private contribution"
-msgstr ""
+msgstr "éžå…¬é–‹ã§è²¢çŒ®ã™ã‚‹"
msgid "Profiles|Main settings"
msgstr "メインã®è¨­å®š"
@@ -15305,10 +15625,10 @@ msgid "Profiles|No file chosen"
msgstr "ファイルãŒé¸æŠžã•ã‚Œã¦ã„ã¾ã›ã‚“"
msgid "Profiles|Notification email"
-msgstr ""
+msgstr "通知メール"
msgid "Profiles|Organization"
-msgstr ""
+msgstr "組織"
msgid "Profiles|Path"
msgstr "パス"
@@ -15317,22 +15637,22 @@ msgid "Profiles|Personal Access"
msgstr ""
msgid "Profiles|Position and size your new avatar"
-msgstr ""
+msgstr "ã‚ãªãŸã®æ–°ã—ã„ã‚¢ãƒã‚¿ãƒ¼ã®ä½ç½®ã¨ã‚µã‚¤ã‚º"
msgid "Profiles|Primary email"
msgstr ""
msgid "Profiles|Private contributions"
-msgstr ""
+msgstr "éžå…¬é–‹ã®è²¢çŒ®"
msgid "Profiles|Profile was successfully updated"
-msgstr ""
+msgstr "ã‚ãªãŸã®ãƒ—ロファイルを正常ã«æ›´æ–°ã§ãã¾ã—ãŸã€‚"
msgid "Profiles|Public Avatar"
msgstr "公開アãƒã‚¿ãƒ¼"
msgid "Profiles|Public email"
-msgstr ""
+msgstr "公開メール"
msgid "Profiles|Remove avatar"
msgstr "ã‚¢ãƒã‚¿ãƒ¼ã‚’削除"
@@ -15344,7 +15664,7 @@ msgid "Profiles|Social sign-in"
msgstr "ソーシャルサインイン"
msgid "Profiles|Some options are unavailable for LDAP accounts"
-msgstr ""
+msgstr "ã„ãã¤ã‹ã®ã‚ªãƒ—ションã¯LDAPアカウントã§ã¯ä½¿ç”¨ã§ãã¾ã›ã‚“。"
msgid "Profiles|Static object token was successfully reset"
msgstr ""
@@ -15356,7 +15676,7 @@ msgid "Profiles|The ability to update your name has been disabled by your admini
msgstr ""
msgid "Profiles|The maximum file size allowed is 200KB."
-msgstr ""
+msgstr "許å¯ã•ã‚Œã‚‹æœ€å¤§ãƒ•ã‚¡ã‚¤ãƒ«ã‚µã‚¤ã‚ºã¯200KBã§ã™ã€‚"
msgid "Profiles|This doesn't look like a public SSH key, are you sure you want to add it?"
msgstr "ã“れ㯠SSH ã®å…¬é–‹éµã«ã¯è¦‹ãˆã¾ã›ã‚“。本当ã«è¿½åŠ ã—ã¦ã‚‚よã‚ã—ã„ã§ã™ã‹ï¼Ÿ"
@@ -15386,13 +15706,13 @@ msgid "Profiles|Typically starts with \"ssh-ed25519 …\" or \"ssh-rsa …\""
msgstr ""
msgid "Profiles|Update profile settings"
-msgstr ""
+msgstr "プロファイル設定を更新"
msgid "Profiles|Update username"
msgstr "ユーザーåã‚’æ›´æ–°"
msgid "Profiles|Upload new avatar"
-msgstr ""
+msgstr "æ–°ã—ã„ã‚¢ãƒã‚¿ãƒ¼ã‚’アップロード"
msgid "Profiles|Use a private email - %{email}"
msgstr ""
@@ -15611,10 +15931,10 @@ msgid "Project slug"
msgstr ""
msgid "Project uploads"
-msgstr ""
+msgstr "プロジェクトアップロード"
msgid "Project visibility level will be changed to match namespace rules when transferring to a group."
-msgstr ""
+msgstr "プロジェクトをグループã«è»¢é€ã—ãŸå ´åˆã€ãƒ—ロジェクトã®å¯è¦–性レベルãŒåå‰ç©ºé–“ã®ãƒ«ãƒ¼ãƒ«ã«åˆã‚ã›ã¦å¤‰æ›´ã•ã‚Œã¾ã™ã€‚"
msgid "Project: %{name}"
msgstr ""
@@ -15626,7 +15946,7 @@ msgid "ProjectCreationLevel|Allowed to create projects"
msgstr "プロジェクトã®ä½œæˆã‚’許å¯ã™ã‚‹"
msgid "ProjectCreationLevel|Default project creation protection"
-msgstr ""
+msgstr "デフォルトã®ãƒ—ロジェクト作æˆä¿è­·"
msgid "ProjectCreationLevel|Developers + Maintainers"
msgstr "Developers + Maintainers"
@@ -15635,7 +15955,7 @@ msgid "ProjectCreationLevel|Maintainers"
msgstr "Maintainers"
msgid "ProjectCreationLevel|No one"
-msgstr ""
+msgstr "誰ã«ã‚‚許å¯ã—ãªã„"
msgid "ProjectFileTree|Name"
msgstr "åå‰"
@@ -15668,19 +15988,19 @@ msgid "ProjectOverview|Unstar"
msgstr ""
msgid "ProjectOverview|You have reached your project limit"
-msgstr ""
+msgstr "プロジェクトã®ä½œæˆåˆ¶é™ã«é”ã—ã¾ã—ãŸ"
msgid "ProjectOverview|You must sign in to star a project"
-msgstr ""
+msgstr "プロジェクトã«ã‚¹ã‚¿ãƒ¼ã‚’付ã‘ã‚‹ã«ã¯ã€ã‚µã‚¤ãƒ³ã‚¤ãƒ³ã—ã¦ã„ã‚‹å¿…è¦ãŒã‚ã‚Šã¾ã™"
msgid "ProjectPage|Project ID: %{project_id}"
-msgstr ""
+msgstr "プロジェクトID: %{project_id}"
msgid "ProjectSelect| or group"
-msgstr ""
+msgstr "ã¾ãŸã¯ã‚°ãƒ«ãƒ¼ãƒ—"
msgid "ProjectSelect|Search for project"
-msgstr ""
+msgstr "プロジェクトã®æ¤œç´¢"
msgid "ProjectService|%{service_title}: status off"
msgstr ""
@@ -15704,7 +16024,7 @@ msgid "ProjectService|To set up this service:"
msgstr ""
msgid "ProjectSettings|Additional merge request capabilities that influence how and when merges will be performed"
-msgstr ""
+msgstr "マージをã„ã¤ã©ã®ã‚ˆã†ã«è¡Œã†ã‹ã«å½±éŸ¿ã‚’与ãˆã‚‹ã€è¿½åŠ ã®ãƒžãƒ¼ã‚¸ãƒªã‚¯ã‚¨ã‚¹ãƒˆæ©Ÿèƒ½"
msgid "ProjectSettings|All discussions must be resolved"
msgstr ""
@@ -15737,7 +16057,7 @@ msgid "ProjectSettings|Container registry"
msgstr ""
msgid "ProjectSettings|Customize your project badges."
-msgstr ""
+msgstr "ã‚ãªãŸã®ãƒ—ロジェクトãƒãƒƒã‚¸ã®ã‚«ã‚¹ã‚¿ãƒžã‚¤ã‚º"
msgid "ProjectSettings|Disable email notifications"
msgstr ""
@@ -15746,7 +16066,7 @@ msgid "ProjectSettings|Enable 'Delete source branch' option by default"
msgstr ""
msgid "ProjectSettings|Every merge creates a merge commit"
-msgstr ""
+msgstr "マージã”ã¨ã«ãƒžãƒ¼ã‚¸ã‚³ãƒŸãƒƒãƒˆãŒä½œæˆã•ã‚Œã¾ã™"
msgid "ProjectSettings|Every project can have its own space to store its Docker images"
msgstr ""
@@ -15761,16 +16081,16 @@ msgid "ProjectSettings|Existing merge requests and protected branches are not af
msgstr ""
msgid "ProjectSettings|Failed to protect the tag"
-msgstr ""
+msgstr "tagã®ä¿è­·ã«å¤±æ•—ã—ã¾ã—ãŸ"
msgid "ProjectSettings|Failed to update tag!"
-msgstr ""
+msgstr "Tag ã®æ›´æ–°ã«å¤±æ•—ã—ã¾ã—ãŸï¼"
msgid "ProjectSettings|Fast-forward merge"
-msgstr ""
+msgstr "æ—©é€ã‚Šãƒžãƒ¼ã‚¸"
msgid "ProjectSettings|Fast-forward merges only"
-msgstr ""
+msgstr "æ—©é€ã‚Šãƒžãƒ¼ã‚¸ã®ã¿"
msgid "ProjectSettings|Forks"
msgstr ""
@@ -15842,7 +16162,7 @@ msgid "ProjectSettings|Pipelines"
msgstr ""
msgid "ProjectSettings|Pipelines must succeed"
-msgstr ""
+msgstr "パイプラインã¯æˆåŠŸã—ãªã‘ã‚Œã°ãªã‚‰ãªã„"
msgid "ProjectSettings|Pipelines need to be configured to enable this feature."
msgstr ""
@@ -15863,7 +16183,7 @@ msgid "ProjectSettings|Share code pastes with others out of Git repository"
msgstr ""
msgid "ProjectSettings|Show link to create/view merge request when pushing from the command line"
-msgstr ""
+msgstr "コマンドラインã‹ã‚‰ãƒ—ッシュã™ã‚‹ã¨ãã€ãƒžãƒ¼ã‚¸ãƒªã‚¯ã‚¨ã‚¹ãƒˆã‚’作æˆã¾ãŸã¯è¡¨ç¤ºã™ã‚‹ãŸã‚ã®ãƒªãƒ³ã‚¯ã‚’表示"
msgid "ProjectSettings|Snippets"
msgstr ""
@@ -15881,22 +16201,22 @@ msgid "ProjectSettings|These checks must pass before merge requests can be merge
msgstr ""
msgid "ProjectSettings|This setting is applied on the server level and can be overridden by an admin."
-msgstr ""
+msgstr "ã“ã®è¨­å®šã¯ã‚µãƒ¼ãƒãƒ¼ãƒ¬ãƒ™ãƒ«ã§é©ç”¨ã•ã‚Œã¦ãŠã‚Šã€ç®¡ç†è€…ãŒä¸Šæ›¸ãã§ãã¾ã™ã€‚"
msgid "ProjectSettings|This setting is applied on the server level but has been overridden for this project."
-msgstr ""
+msgstr "ã“ã®è¨­å®šã¯ã‚µãƒ¼ãƒãƒ¼ãƒ¬ãƒ™ãƒ«ã§é©ç”¨ã•ã‚Œã¦ã„ã¾ã™ãŒã€ã“ã®ãƒ—ロジェクトå‘ã‘ã«ä¸Šæ›¸ãã•ã‚Œã¦ã„ã¾ã™ã€‚"
msgid "ProjectSettings|This setting will be applied to all projects unless overridden by an admin."
-msgstr ""
+msgstr "ã“ã®è¨­å®šã¯ã€ç®¡ç†è€…ãŒä¸Šæ›¸ãã—ãªã„å ´åˆã€ã™ã¹ã¦ã®ãƒ—ロジェクトã«é©ç”¨ã•ã‚Œã¾ã™ã€‚"
msgid "ProjectSettings|This setting will override user notification preferences for all project members."
msgstr ""
msgid "ProjectSettings|This will dictate the commit history when you merge a merge request"
-msgstr ""
+msgstr "マージリクエストをマージã™ã‚‹ã¨ãã«ã‚³ãƒŸãƒƒãƒˆå±¥æ­´ã‚’記録ã—ã¾ã™"
msgid "ProjectSettings|Users can only push commits to this repository that were committed with one of their own verified emails."
-msgstr ""
+msgstr "ユーザã¯æ¤œè¨¼æ¸ˆã¿ãƒ¡ãƒ¼ãƒ«ã®ã„ãšã‚Œã‹ã«ã‚ˆã‚‹ã‚³ãƒŸãƒƒãƒˆã—ãŸã€ã“ã®ãƒªãƒã‚¸ãƒˆãƒªã«ã‚³ãƒŸãƒƒãƒˆã‚’プッシュã§ãã¾ã™ã€‚"
msgid "ProjectSettings|View and edit files in this project"
msgstr ""
@@ -15905,7 +16225,7 @@ msgid "ProjectSettings|View and edit files in this project. Non-project members
msgstr ""
msgid "ProjectSettings|When conflicts arise the user is given the option to rebase"
-msgstr ""
+msgstr "競åˆãŒç™ºç”Ÿã—ãŸã¨ãã€ãƒ¦ãƒ¼ã‚¶ãƒ¼ã«ã¯ãƒªãƒ™ãƒ¼ã‚¹ã™ã‚‹ã‚ªãƒ—ションãŒä¸Žãˆã‚‰ã‚Œã¾ã™"
msgid "ProjectSettings|Wiki"
msgstr "Wiki"
@@ -15913,6 +16233,9 @@ msgstr "Wiki"
msgid "ProjectSettings|With GitLab Pages you can host your static websites on GitLab"
msgstr ""
+msgid "ProjectSettings|With Metrics Dashboard you can visualize this project performance metrics"
+msgstr ""
+
msgid "ProjectTemplates|.NET Core"
msgstr ".Net Core"
@@ -15923,7 +16246,7 @@ msgid "ProjectTemplates|GitLab Cluster Management"
msgstr ""
msgid "ProjectTemplates|Go Micro"
-msgstr ""
+msgstr "Go Micro"
msgid "ProjectTemplates|HIPAA Audit Protocol"
msgstr ""
@@ -15976,6 +16299,9 @@ msgstr ""
msgid "ProjectTemplates|Spring"
msgstr "Spring"
+msgid "ProjectTemplates|Static Site Editor/Middleman"
+msgstr ""
+
msgid "ProjectTemplates|iOS (Swift)"
msgstr "iOS (Swift)"
@@ -15986,16 +16312,16 @@ msgid "Projects (%{count})"
msgstr ""
msgid "Projects Successfully Retrieved"
-msgstr ""
+msgstr "正常ã«å–å¾—ã—ãŸãƒ—ロジェクト"
msgid "Projects are graded based on the highest severity vulnerability present"
msgstr ""
msgid "Projects shared with %{group_name}"
-msgstr ""
+msgstr "%{group_name} グループã§å…±æœ‰ã•ã‚ŒãŸãƒ—ロジェクト"
msgid "Projects that belong to a group are prefixed with the group namespace. Existing projects may be moved into a group."
-msgstr ""
+msgstr "グループã«å±žã™ã‚‹ãƒ—ロジェクトã®å…ˆé ­ã«ã¯ã€ã‚°ãƒ«ãƒ¼ãƒ—ã®åå‰ç©ºé–“ãŒä»˜ã„ã¦ã„ã¾ã™ã€‚既存ã®ãƒ—ロジェクトをグループã«ç§»å‹•ã™ã‚‹ã“ã¨ãŒã§ãã¾ã™ã€‚"
msgid "Projects to index"
msgstr ""
@@ -16016,7 +16342,7 @@ msgid "Projects with no vulnerabilities and security scanning enabled"
msgstr ""
msgid "Projects with write access"
-msgstr ""
+msgstr "書ãè¾¼ã¿æ¨©é™ã®ã‚るプロジェクト"
msgid "ProjectsDropdown|Frequently visited"
msgstr "よã使ã†ãƒ—ロジェクト"
@@ -16097,22 +16423,22 @@ msgid "PrometheusAlerts|%{firingCount} firing"
msgstr ""
msgid "PrometheusAlerts|Add alert"
-msgstr ""
+msgstr "アラートを追加"
msgid "PrometheusAlerts|Edit alert"
-msgstr ""
+msgstr "アラートã®ç·¨é›†"
msgid "PrometheusAlerts|Error creating alert"
-msgstr ""
+msgstr "アラート作æˆã®ã‚¨ãƒ©ãƒ¼"
msgid "PrometheusAlerts|Error deleting alert"
-msgstr ""
+msgstr "アラートã®å‰Šé™¤ã®ã‚¨ãƒ©ãƒ¼"
msgid "PrometheusAlerts|Error fetching alert"
-msgstr ""
+msgstr "アラートå–å¾—ã®ã‚¨ãƒ©ãƒ¼"
msgid "PrometheusAlerts|Error saving alert"
-msgstr ""
+msgstr "アラートä¿å­˜ã®ã‚¨ãƒ©ãƒ¼"
msgid "PrometheusAlerts|Firing: %{alerts}"
msgstr ""
@@ -16121,13 +16447,13 @@ msgid "PrometheusAlerts|Firing: %{alert}"
msgstr ""
msgid "PrometheusAlerts|Operator"
-msgstr ""
+msgstr "オペレータ"
msgid "PrometheusAlerts|Select query"
-msgstr ""
+msgstr "クエリをé¸æŠž"
msgid "PrometheusAlerts|Threshold"
-msgstr ""
+msgstr "ã—ãã„値"
msgid "PrometheusService|%{exporters} with %{metrics} were found"
msgstr "%{metrics} ã® %{exporters} ãŒè¦‹ã¤ã‹ã‚Šã¾ã—ãŸ"
@@ -16148,7 +16474,7 @@ msgid "PrometheusService|Common metrics"
msgstr "共通メトリクス"
msgid "PrometheusService|Common metrics are automatically monitored based on a library of metrics from popular exporters."
-msgstr ""
+msgstr "共通メトリクスã¯ã€ä¸€èˆ¬çš„ãªã‚¨ã‚¯ã‚¹ãƒãƒ¼ã‚¿ã‹ã‚‰ã®ãƒ¡ãƒˆãƒªã‚¯ã‚¹ã®ãƒ©ã‚¤ãƒ–ラリã«åŸºã¥ã„ã¦ã€è‡ªå‹•çš„ã«ç›£è¦–ã•ã‚Œã¾ã™ã€‚"
msgid "PrometheusService|Custom metrics"
msgstr "カスタムメトリクス"
@@ -16157,7 +16483,7 @@ msgid "PrometheusService|Custom metrics require Prometheus installed on a cluste
msgstr ""
msgid "PrometheusService|Enable Prometheus to define custom metrics, using either option above"
-msgstr ""
+msgstr "上記ã®ã„ãšã‚Œã‹ã®ã‚ªãƒ—ションを使用ã—ã¦ã€ã‚«ã‚¹ã‚¿ãƒ ãƒ¡ãƒˆãƒªã‚¯ã‚¹ã‚’定義ã§ãるよã†ã«Prometheusを有効ã«ã™ã‚‹"
msgid "PrometheusService|Finding and configuring metrics..."
msgstr "メトリクスã®æ¤œç´¢ã¨è¨­å®š..."
@@ -16195,20 +16521,20 @@ msgstr "Prometheus API ã®ãƒ™ãƒ¼ã‚¹ URLã€ä¾‹:http://prometheus.example.com/"
msgid "PrometheusService|Prometheus is being automatically managed on your clusters"
msgstr "Prometheus ã¯ã‚¯ãƒ©ã‚¹ã‚¿ãƒ¼ä¸Šã§è‡ªå‹•çš„ã«ç®¡ç†ã•ã‚Œã¦ã„ã¾ã™"
-msgid "PrometheusService|These metrics will only be monitored after your first deployment to an environment"
+msgid "PrometheusService|Select the Active checkbox to override the Auto Configuration with custom settings. If unchecked, Auto Configuration settings are used."
msgstr ""
+msgid "PrometheusService|These metrics will only be monitored after your first deployment to an environment"
+msgstr "環境ã«æœ€åˆã®ãƒ‡ãƒ—ロイãŒã•ã‚Œã¦ã‹ã‚‰ã€ã“れらã®ãƒ¡ãƒˆãƒªã‚¯ã‚¹ã§ç›£è¦–ã—ã¾ã™"
+
msgid "PrometheusService|Time-series monitoring service"
msgstr "時系列監視サービス"
-msgid "PrometheusService|To enable manual configuration, uninstall Prometheus from your clusters"
-msgstr "手動設定を有効ã«ã™ã‚‹ãŸã‚ã«ã¯ã€ã‚ãªãŸã®ã‚¯ãƒ©ã‚¹ã‚¿ãƒ¼ã‹ã‚‰ Prometheus をアンインストールã—ã¾ã™"
-
msgid "PrometheusService|To enable the installation of Prometheus on your clusters, deactivate the manual configuration below"
msgstr "クラスター㫠Prometheus をインストールã™ã‚‹ã«ã¯ã€ä»¥ä¸‹ã®æ‰‹å‹•è¨­å®šã‚’無効ã«ã—ã¦ãã ã•ã„"
msgid "PrometheusService|Waiting for your first deployment to an environment to find common metrics"
-msgstr ""
+msgstr "共通メトリックãŒæ§‹ç¯‰ç’°å¢ƒã¸å±•é–‹ã•ã‚Œã‚‹ã®ã‚’å¾…ã£ã¦ã„ã¾ã™"
msgid "PrometheusService|You can now manage your Prometheus settings on the %{operations_link_start}Operations%{operations_link_end} page. Fields on this page has been deprecated."
msgstr ""
@@ -16232,19 +16558,19 @@ msgid "Promote to group label"
msgstr "グループラベルã¸æ˜‡æ ¼"
msgid "PromoteMilestone|Only project milestones can be promoted."
-msgstr ""
+msgstr "プロジェクトã®ãƒžã‚¤ãƒ«ã‚¹ トーンã ã‘ãŒãƒ—ロモートã§ãã¾ã™ã€‚"
msgid "PromoteMilestone|Project does not belong to a group."
-msgstr ""
+msgstr "プロジェクトãŒã‚°ãƒ«ãƒ¼ãƒ—ã«å±žã—ã¦ã„ã¾ã›ã‚“。"
msgid "PromoteMilestone|Promotion failed - %{message}"
-msgstr ""
+msgstr "プロモートã«å¤±æ•—ã—ã¾ã—㟠- %{message}"
msgid "Promoted confidential issue to a non-confidential epic. Information in this issue is no longer confidential as epics are public to group members."
msgstr ""
msgid "Promoted issue to an epic."
-msgstr ""
+msgstr "課題をエピックã«æ˜‡æ ¼æ¸ˆã¿"
msgid "Promotions|Don't show me this again"
msgstr "次回ã‹ã‚‰ã¯ã“ã®ãƒ¡ãƒƒã‚»ãƒ¼ã‚¸ã‚’表示ã—ãªã„"
@@ -16253,7 +16579,7 @@ msgid "Promotions|Epics let you manage your portfolio of projects more efficient
msgstr "エピックを使用ã™ã‚‹ã¨ã€ãƒ—ロジェクトやマイルストーンをã¾ãŸãŒã‚‹å…±é€šãƒ†ãƒ¼ãƒžã®èª²é¡Œã‚°ãƒ«ãƒ¼ãƒ—を追跡ã§ãã€ãƒ—ロジェクトã®ãƒãƒ¼ãƒˆãƒ•ã‚©ãƒªã‚ªã‚’より効率的ã«ç°¡å˜ã«ç®¡ç†ã§ãã¾ã™ã€‚"
msgid "Promotions|Learn more"
-msgstr ""
+msgstr "詳ã—ã見る"
msgid "Promotions|See the other features in the %{subscription_link_start}bronze plan%{subscription_link_end}"
msgstr ""
@@ -16271,7 +16597,7 @@ msgid "Promotions|When you have a lot of issues, it can be hard to get an overvi
msgstr ""
msgid "Prompt users to upload SSH keys"
-msgstr ""
+msgstr "SSH éµã‚’アップロードã™ã‚‹ã‚ˆã†ã«ãƒ¦ãƒ¼ã‚¶ãƒ¼ã«ä¿ƒã™"
msgid "Protect variable"
msgstr ""
@@ -16283,10 +16609,10 @@ msgid "Protected Branch"
msgstr "ä¿è­·ã•ã‚ŒãŸãƒ–ランãƒ"
msgid "Protected Environment"
-msgstr ""
+msgstr "ä¿è­·ã•ã‚ŒãŸç’°å¢ƒ"
msgid "Protected Environments"
-msgstr ""
+msgstr "ä¿è­·ã•ã‚ŒãŸç’°å¢ƒ"
msgid "Protected Paths"
msgstr ""
@@ -16358,7 +16684,7 @@ msgid "ProtectedEnvironment|Protected Environment (%{protected_environments_coun
msgstr "ä¿è­·ã•ã‚ŒãŸç’°å¢ƒ (%{protected_environments_count})"
msgid "ProtectedEnvironment|Protecting an environment restricts the users who can execute deployments."
-msgstr ""
+msgstr "環境をä¿è­·ã™ã‚‹ã¨ã€ãƒ‡ãƒ—ロイメントを実行ã§ãるユーザーãŒåˆ¶é™ã•ã‚Œã¾ã™ã€‚"
msgid "ProtectedEnvironment|Select an environment"
msgstr "環境をé¸æŠž"
@@ -16382,16 +16708,19 @@ msgid "ProtectedEnvironment|Your environment has been protected."
msgstr "ã‚ãªãŸã®ç’°å¢ƒã¯ä¿è­·ã•ã‚Œã¦ã„ã¾ã™ã€‚"
msgid "ProtectedEnvironment|Your environment has been unprotected"
-msgstr ""
+msgstr "ã‚ãªãŸã®ç’°å¢ƒã¯ä¿è­·ã•ã‚Œã¦ã„ã¾ã›ã‚“。ã¾ãŸã¯ä¿è­·ãŒè§£é™¤ã•ã‚Œã¦ã„ã¾ã™ã€‚"
msgid "Protip:"
+msgstr "Protip:"
+
+msgid "Protocol"
msgstr ""
msgid "Provider"
msgstr "プロãƒã‚¤ãƒ€ãƒ¼"
msgid "Pseudonymizer data collection"
-msgstr ""
+msgstr "匿å化データã®åŽé›†"
msgid "Public"
msgstr "公開"
@@ -16403,7 +16732,7 @@ msgid "Public - The project can be accessed without any authentication."
msgstr "公開 - プロジェクトã¯èªè¨¼ç„¡ã—ã«ã‚¢ã‚¯ã‚»ã‚¹ã§ãã¾ã™"
msgid "Public Access Help"
-msgstr ""
+msgstr "公開アクセスã®ãƒ˜ãƒ«ãƒ—"
msgid "Public deploy keys (%{deploy_keys_count})"
msgstr ""
@@ -16411,6 +16740,9 @@ msgstr ""
msgid "Public pipelines"
msgstr "公開パイプライン"
+msgid "Public projects Minutes cost factor"
+msgstr ""
+
msgid "Pull"
msgstr "プル"
@@ -16523,7 +16855,7 @@ msgid "README"
msgstr "README"
msgid "Rake Tasks Help"
-msgstr ""
+msgstr "Rakeタスクã®ãƒ˜ãƒ«ãƒ—"
msgid "Raw blob request rate limit per minute"
msgstr ""
@@ -16541,7 +16873,7 @@ msgid "Read more"
msgstr "続ãを読む"
msgid "Read more about environments"
-msgstr ""
+msgstr "環境ã®è©³ç´°"
msgid "Read more about project permissions <strong>%{link_to_help}</strong>"
msgstr "プロジェクトã®æ¨©é™ã«ã¤ã„ã¦ã¯ <strong>%{link_to_help}</strong> ã‚’å‚ç…§"
@@ -16559,25 +16891,25 @@ msgid "Rebase in progress"
msgstr ""
msgid "Receive alerts from manually configured Prometheus servers."
-msgstr ""
+msgstr "手動ã§è¨­å®šã—ãŸPrometheusサーãƒãƒ¼ã‹ã‚‰ã‚¢ãƒ©ãƒ¼ãƒˆã‚’å—ã‘å–る。"
msgid "Receive alerts on GitLab from any source"
msgstr ""
msgid "Receive notifications about your own activity"
-msgstr ""
+msgstr "自身ã®ã‚¢ã‚¯ãƒ†ã‚£ãƒ“ティーã«é–¢ã™ã‚‹é€šçŸ¥ã‚’å—ä¿¡ã™ã‚‹"
msgid "Recent"
-msgstr ""
+msgstr "最近"
msgid "Recent Activity"
-msgstr ""
+msgstr "最新アクティビティー"
msgid "Recent Project Activity"
-msgstr ""
+msgstr "最近ã®ãƒ—ロジェクトアクティビティ"
msgid "Recent Searches Service is unavailable"
-msgstr ""
+msgstr "最近ã®æ¤œç´¢ã‚µãƒ¼ãƒ“スã¯åˆ©ç”¨ã§ãã¾ã›ã‚“"
msgid "Recent searches"
msgstr "最近ã®æ¤œç´¢"
@@ -16589,10 +16921,10 @@ msgid "Recover hidden stage"
msgstr ""
msgid "Recovery Codes"
-msgstr ""
+msgstr "リカãƒãƒªãƒ¼ã‚³ãƒ¼ãƒ‰"
msgid "Redirect to SAML provider to test configuration"
-msgstr ""
+msgstr "設定ã®ãƒ†ã‚¹ãƒˆã‚’実施ã™ã‚‹SAMLプロãƒã‚¤ãƒ€ã¸ãƒªãƒ€ã‚¤ãƒ¬ã‚¯ãƒˆ"
msgid "Reduce project visibility"
msgstr ""
@@ -16608,16 +16940,19 @@ msgstr "æ›´æ–°"
msgid "Refreshing in a second to show the updated status..."
msgid_plural "Refreshing in %d seconds to show the updated status..."
-msgstr[0] ""
+msgstr[0] "æ›´æ–°ã•ã‚ŒãŸã‚¹ãƒ†ãƒ¼ã‚¿ã‚¹ã‚’表示ã™ã‚‹ãŸã‚ã«ã€ %d 秒間リフレッシュã—ã¦ã„ã¾ã™..."
+
+msgid "Regenerate export"
+msgstr ""
msgid "Regenerate instance ID"
msgstr ""
msgid "Regenerate key"
-msgstr ""
+msgstr "éµã‚’å†ç”Ÿæˆã™ã‚‹"
msgid "Regenerate recovery codes"
-msgstr ""
+msgstr "リカãƒãƒªãƒ¼ã‚³ãƒ¼ãƒ‰ã®å†ç™ºè¡Œ"
msgid "Regenerating the instance ID can break integration depending on the client you are using."
msgstr ""
@@ -16629,19 +16964,19 @@ msgid "Region that Elasticsearch is configured"
msgstr ""
msgid "Register"
-msgstr ""
+msgstr "登録"
msgid "Register / Sign In"
msgstr "登録 / サインイン"
msgid "Register Two-Factor Authenticator"
-msgstr ""
+msgstr "2è¦ç´ èªè¨¼ã®ç™»éŒ²"
msgid "Register U2F device"
msgstr "U2F デãƒã‚¤ã‚¹ã‚’登録ã™ã‚‹"
msgid "Register Universal Two-Factor (U2F) Device"
-msgstr ""
+msgstr "ユニãƒãƒ¼ã‚µãƒ«ãª2è¦ç´ èªè¨¼ãƒ‡ãƒã‚¤ã‚¹ã‚’登録ã™ã‚‹"
msgid "Register for GitLab"
msgstr ""
@@ -16650,10 +16985,10 @@ msgid "Register now"
msgstr ""
msgid "Register with two-factor app"
-msgstr ""
+msgstr "2è¦ç´ èªè¨¼ã‚¢ãƒ—リã§ç™»éŒ²"
msgid "Registration"
-msgstr ""
+msgstr "登録"
msgid "Related Deployed Jobs"
msgstr "関連ã™ã‚‹ãƒ‡ãƒ—ロイ済ジョブ"
@@ -16786,13 +17121,13 @@ msgid "Remove from epic"
msgstr ""
msgid "Remove group"
-msgstr ""
+msgstr "グループを削除"
msgid "Remove limit"
msgstr ""
msgid "Remove milestone"
-msgstr ""
+msgstr "マイルストーンを削除"
msgid "Remove node"
msgstr ""
@@ -16813,16 +17148,16 @@ msgid "Remove secondary node"
msgstr ""
msgid "Remove spent time"
-msgstr ""
+msgstr "作業時間ã®å‰Šé™¤"
msgid "Remove stage"
msgstr ""
msgid "Remove time estimate"
-msgstr ""
+msgstr "見ç©æ™‚間を削除"
msgid "Removed"
-msgstr ""
+msgstr "除去ã—ã¾ã—ãŸ"
msgid "Removed %{assignee_text} %{assignee_references}."
msgstr ""
@@ -16837,7 +17172,7 @@ msgid "Removed %{milestone_reference} milestone."
msgstr ""
msgid "Removed %{type} with id %{id}"
-msgstr ""
+msgstr "Id %{id} 㮠%{type} を削除"
msgid "Removed all labels."
msgstr ""
@@ -16846,19 +17181,19 @@ msgid "Removed an issue from an epic."
msgstr ""
msgid "Removed group can not be restored!"
-msgstr ""
+msgstr "削除ã•ã‚ŒãŸã‚°ãƒ«ãƒ¼ãƒ—ã¯ãƒ¬ã‚¹ãƒˆã‚¢ã§ãã¾ã›ã‚“ï¼"
msgid "Removed parent epic %{epic_ref}."
msgstr ""
msgid "Removed projects cannot be restored!"
-msgstr ""
+msgstr "削除ã•ã‚ŒãŸãƒ—ロジェクトã¯å¾©å…ƒã§ãã¾ã›ã‚“ï¼"
msgid "Removed spent time."
-msgstr ""
+msgstr "作業時間を削除ã—ã¾ã—ãŸã€‚"
msgid "Removed the due date."
-msgstr ""
+msgstr "期日を削除ã—ã¾ã—ãŸã€‚"
msgid "Removed time estimate."
msgstr ""
@@ -16873,10 +17208,10 @@ msgid "Removes %{label_references} %{label_text}."
msgstr ""
msgid "Removes %{milestone_reference} milestone."
-msgstr ""
+msgstr "%{milestone_reference} マイルストーンを削除"
msgid "Removes all labels."
-msgstr ""
+msgstr "ã™ã¹ã¦ã®ãƒ©ãƒ™ãƒ«ã‚’削除"
msgid "Removes an issue from an epic."
msgstr ""
@@ -16885,13 +17220,13 @@ msgid "Removes parent epic %{epic_ref}."
msgstr ""
msgid "Removes spent time."
-msgstr ""
+msgstr "作業時間を削除ã—ã¾ã™ã€‚"
msgid "Removes the due date."
-msgstr ""
+msgstr "期日を削除."
msgid "Removes time estimate."
-msgstr ""
+msgstr "見ç©æ™‚間を削除."
msgid "Removing a project places it into a read-only state until %{date}, at which point the project will be permanantly removed. Are you ABSOLUTELY sure?"
msgstr ""
@@ -16903,7 +17238,7 @@ msgid "Removing license…"
msgstr ""
msgid "Removing the project will delete its repository and all related resources including issues, merge requests etc."
-msgstr ""
+msgstr "プロジェクトを除去ã™ã‚‹ã¨ã€ãã®ãƒªãƒã‚¸ãƒˆãƒªã¨ã€èª²é¡Œã€ãƒžãƒ¼ã‚¸ãƒªã‚¯ã‚¨ã‚¹ãƒˆãªã©ã‚’å«ã‚€ã™ã¹ã¦ã®é–¢é€£ãƒªã‚½ãƒ¼ã‚¹ãŒå‰Šé™¤ã•ã‚Œã¾ã™"
msgid "Removing this group also removes all child projects, including archived projects, and their resources."
msgstr ""
@@ -16939,10 +17274,10 @@ msgid "Repair authentication"
msgstr "èªè¨¼ã®ä¿®å¾©"
msgid "Replace"
-msgstr ""
+msgstr "ç½®ãæ›ãˆ"
msgid "Replace all label(s)"
-msgstr ""
+msgstr "ã™ã¹ã¦ã®ãƒ©ãƒ™ãƒ«ã‚’ç½®æ›"
msgid "Replaced all labels with %{label_references} %{label_text}."
msgstr ""
@@ -16957,7 +17292,7 @@ msgid "Reply to comment"
msgstr "コメントã«è¿”ä¿¡"
msgid "Reply to this email directly or %{view_it_on_gitlab}."
-msgstr ""
+msgstr "ã“ã®ãƒ¡ãƒ¼ãƒ«ã«ç›´æŽ¥è¿”ä¿¡ã™ã‚‹ã‹ã€ %{view_it_on_gitlab} ã‚’å‚ç…§ã—ã¦ãã ã•ã„。"
msgid "Reply..."
msgstr ""
@@ -16986,9 +17321,28 @@ msgstr "レãƒãƒ¼ãƒˆ"
msgid "Reports|%{combinedString} and %{resolvedString}"
msgstr ""
+msgid "Reports|Accessibility scanning detected %d issue for the source branch only"
+msgid_plural "Reports|Accessibility scanning detected %d issues for the source branch only"
+msgstr[0] ""
+
+msgid "Reports|Accessibility scanning detected no issues for the source branch only"
+msgstr ""
+
+msgid "Reports|Accessibility scanning failed loading results"
+msgstr ""
+
+msgid "Reports|Accessibility scanning results are being parsed"
+msgstr ""
+
msgid "Reports|Actions"
msgstr "アクション"
+msgid "Reports|An error occured while loading report"
+msgstr ""
+
+msgid "Reports|An error occurred while loading %{name} results"
+msgstr ""
+
msgid "Reports|Class"
msgstr "クラス"
@@ -17005,10 +17359,10 @@ msgid "Reports|Metrics reports are loading"
msgstr ""
msgid "Reports|Metrics reports changed on %{numberOfChanges} %{pointsString}"
-msgstr ""
+msgstr "メトリクスレãƒãƒ¼ãƒˆã¯ %{numberOfChanges}%{pointsString} 変更ã•ã‚Œã¾ã—ãŸ"
msgid "Reports|Metrics reports did not change"
-msgstr ""
+msgstr "メトリクスレãƒãƒ¼ãƒˆã¯å¤‰æ›´ã•ã‚Œã¾ã›ã‚“ã§ã—ãŸã€‚"
msgid "Reports|Metrics reports failed loading results"
msgstr ""
@@ -17044,7 +17398,7 @@ msgid "Repository Graph"
msgstr ""
msgid "Repository Settings"
-msgstr ""
+msgstr "リãƒã‚¸ãƒˆãƒªã®è¨­å®š"
msgid "Repository URL"
msgstr "リãƒã‚¸ãƒˆãƒª URL"
@@ -17092,28 +17446,28 @@ msgid "Request to link SAML account must be authorized"
msgstr ""
msgid "Requested %{time_ago}"
-msgstr ""
+msgstr "%{time_ago} ã«ãƒªã‚¯ã‚¨ã‚¹ãƒˆ"
-msgid "Requested design version does not exist"
+msgid "Requested design version does not exist."
msgstr ""
msgid "Requested states are invalid"
msgstr ""
msgid "Requests Profiles"
-msgstr ""
+msgstr "プロファイルã®è¦æ±‚"
msgid "Requests to these domain(s)/address(es) on the local network will be allowed when local requests from hooks and services are not allowed. IP ranges such as 1:0:0:0:0:0:0:0/124 or 127.0.0.0/28 are supported. Domain wildcards are not supported currently. Use comma, semicolon, or newline to separate multiple entries. The whitelist can hold a maximum of 1000 entries. Domains should use IDNA encoding. Ex: example.com, 192.168.1.1, 127.0.0.0/28, xn--itlab-j1a.com."
msgstr ""
msgid "Require all users in this group to setup Two-factor authentication"
-msgstr ""
+msgstr "ã“ã®ã‚°ãƒ«ãƒ¼ãƒ—内ã®ã™ã¹ã¦ã®ãƒ¦ãƒ¼ã‚¶ãƒ¼ã«2è¦ç´ èªè¨¼ã®ã‚»ãƒƒãƒˆã‚¢ãƒƒãƒ—ã‚’è¦æ±‚ã™ã‚‹"
msgid "Require all users to accept Terms of Service and Privacy Policy when they access GitLab."
msgstr "GitLab ã«ã‚¢ã‚¯ã‚»ã‚¹ã™ã‚‹éš›ã«ã€ã™ã¹ã¦ã®ãƒ¦ãƒ¼ã‚¶ãƒ¼ãŒåˆ©ç”¨è¦ç´„ã«åŒæ„ã™ã‚‹ã“ã¨ã‚’è¦æ±‚ã—ã¾ã™ã€‚"
msgid "Require user password to approve"
-msgstr ""
+msgstr "承èªã®ãŸã‚ã«ãƒ¦ãƒ¼ã‚¶ãƒ¼ãƒ‘スワードをè¦æ±‚"
msgid "Require users to prove ownership of custom domains"
msgstr ""
@@ -17175,7 +17529,7 @@ msgid "Reset health check access token"
msgstr "正常性ãƒã‚§ãƒƒã‚¯ã‚¢ã‚¯ã‚»ã‚¹ãƒˆãƒ¼ã‚¯ãƒ³ã‚’リセット"
msgid "Reset key"
-msgstr ""
+msgstr "éµã‚’リセット"
msgid "Reset runners registration token"
msgstr "Runner 登録トークンをリセット"
@@ -17205,16 +17559,16 @@ msgid "Resolved"
msgstr "解決済ã¿"
msgid "Resolved 1 discussion."
-msgstr ""
+msgstr "1件ã®æ¤œè¨Žã‚’解決ã—ã¾ã—ãŸã€‚"
msgid "Resolved all discussions."
-msgstr ""
+msgstr "ã™ã¹ã¦ã®æ¤œè¨Žã‚’解決ã—ã¾ã—ãŸã€‚"
msgid "Resolved by %{name}"
-msgstr ""
+msgstr "%{name} ã«ã‚ˆã£ã¦è§£æ±º"
msgid "Resolved by %{resolvedByName}"
-msgstr ""
+msgstr "%{resolvedByName} ã«ã‚ˆã£ã¦è§£æ±º"
msgid "Resolves IP addresses once and uses them to submit requests"
msgstr ""
@@ -17223,10 +17577,10 @@ msgid "Response"
msgstr "レスãƒãƒ³ã‚¹"
msgid "Response didn't include `service_desk_address`"
-msgstr ""
+msgstr "応答㫠`service_desk_address`ã¯å«ã¾ã‚Œã¦ã„ã¾ã›ã‚“ã§ã—ãŸ"
msgid "Response metrics (AWS ELB)"
-msgstr ""
+msgstr "レスãƒãƒ³ã‚¹ メトリクス (AWS ELB)"
msgid "Response metrics (Custom)"
msgstr "レスãƒãƒ³ã‚¹ メトリクス(カスタム)"
@@ -17238,7 +17592,7 @@ msgid "Response metrics (NGINX Ingress VTS)"
msgstr "レスãƒãƒ³ã‚¹ãƒ¡ãƒˆãƒªã‚¯ã‚¹ (NGINX Ingress VTS)"
msgid "Response metrics (NGINX Ingress)"
-msgstr ""
+msgstr "レスãƒãƒ³ã‚¹ãƒ¡ãƒˆãƒªãƒƒã‚¯ (NGINX Ingress)"
msgid "Response metrics (NGINX)"
msgstr "レスãƒãƒ³ã‚¹ãƒ¡ãƒˆãƒªã‚¯ã‚¹ (NGINX)"
@@ -17271,11 +17625,14 @@ msgid "Resume"
msgstr "å†é–‹"
msgid "Resume replication"
-msgstr ""
+msgstr "レプリケーションã®å†é–‹"
msgid "Resync"
msgstr ""
+msgid "Resync all"
+msgstr ""
+
msgid "Resync all %{replicableType}"
msgstr ""
@@ -17286,7 +17643,7 @@ msgid "Retry this job"
msgstr "ジョブをå†è©¦è¡Œã—ã¦ãã ã•ã„"
msgid "Retry this job in order to create the necessary resources."
-msgstr ""
+msgstr "å¿…è¦ãªãƒªã‚½ãƒ¼ã‚¹ã‚’作æˆã™ã‚‹ãŸã‚ã«ã“ã®ã‚¸ãƒ§ãƒ–ã‚’å†è©¦è¡Œã—ã¦ãã ã•ã„。"
msgid "Retry update"
msgstr ""
@@ -17343,6 +17700,9 @@ msgstr ""
msgid "Revoked personal access token %{personal_access_token_name}!"
msgstr ""
+msgid "Revoked project access token %{project_access_token_name}!"
+msgstr ""
+
msgid "RightSidebar|adding a"
msgstr ""
@@ -17377,28 +17737,31 @@ msgid "Run untagged jobs"
msgstr ""
msgid "Runner cannot be assigned to other projects"
-msgstr ""
+msgstr "Runnerã‚’ä»–ã®ãƒ—ロジェクトã«å‰²ã‚Šå½“ã¦ã‚‰ã‚Œã¾ã›ã‚“"
msgid "Runner runs jobs from all unassigned projects"
-msgstr ""
+msgstr "Runnerã¯æœªå‰²ã‚Šå½“ã¦ã®ã™ã¹ã¦ã®ãƒ—ロジェクトã‹ã‚‰ã‚¸ãƒ§ãƒ–を実行ã—ã¾ã™"
msgid "Runner runs jobs from all unassigned projects in its group"
-msgstr ""
+msgstr "Runnerã¯ãã®ã‚°ãƒ«ãƒ¼ãƒ—ã®æœªå‰²ã‚Šå½“ã¦ã®ã™ã¹ã¦ã®ãƒ—ロジェクトã‹ã‚‰ã‚¸ãƒ§ãƒ–を実行ã—ã¾ã™"
msgid "Runner runs jobs from assigned projects"
-msgstr ""
+msgstr "Runnerã¯å‰²ã‚Šå½“ã¦ã‚‰ã‚ŒãŸãƒ—ロジェクトã‹ã‚‰ã‚¸ãƒ§ãƒ–を実行ã—ã¾ã™"
msgid "Runner token"
msgstr "Runner トークン"
-msgid "Runner was not updated."
+msgid "Runner tokens"
msgstr ""
+msgid "Runner was not updated."
+msgstr "Runner ã¯æ›´æ–°ã•ã‚Œã¦ã„ã¾ã›ã‚“。"
+
msgid "Runner was successfully updated."
-msgstr ""
+msgstr "Runner ã¯æ­£å¸¸ã«æ›´æ–°ã•ã‚Œã¾ã—ãŸã€‚"
msgid "Runner will not receive any new jobs"
-msgstr ""
+msgstr "Runnerã¯æ–°ã—ã„ジョブを処ç†ã—ã¾ã›ã‚“"
msgid "Runners"
msgstr "Runner"
@@ -17407,7 +17770,7 @@ msgid "Runners API"
msgstr "Runner API"
msgid "Runners activated for this project"
-msgstr ""
+msgstr "ã“ã®ãƒ—ロジェクト用ã«Runnerをアクティブ化"
msgid "Runners are processes that pick up and execute jobs for GitLab. Here you can register and see your Runners for this project."
msgstr ""
@@ -17416,7 +17779,7 @@ msgid "Runners can be placed on separate users, servers, and even on your local
msgstr "Runner ã¯åˆ¥ã€…ã®ãƒ¦ãƒ¼ã‚¶ãƒ¼ã€ã‚µãƒ¼ãƒãƒ¼ã€ã•ã‚‰ã«ã¯ã‚ãªãŸã®ãƒ­ãƒ¼ã‚«ãƒ«ãƒžã‚·ãƒ¼ãƒ³ã«ã‚‚é…ç½®ã§ãã¾ã™ã€‚"
msgid "Runners can be placed on separate users, servers, even on your local machine."
-msgstr ""
+msgstr "Runnerã¯åˆ¥ã€…ã®ãƒ¦ãƒ¼ã‚¶ãƒ¼ã€ã‚µãƒ¼ãƒãƒ¼ã€ã•ã‚‰ã«ã¯ã‚ãªãŸã®ãƒ­ãƒ¼ã‚«ãƒ«ãƒžã‚·ãƒ¼ãƒ³ã«ã‚‚é…ç½®ã§ãã¾ã™ã€‚"
msgid "Runners currently online: %{active_runners_count}"
msgstr "ç¾åœ¨ã‚ªãƒ³ãƒ©ã‚¤ãƒ³ã®Runner: %{active_runners_count}"
@@ -17437,7 +17800,7 @@ msgid "Running…"
msgstr "実行中…"
msgid "Runs a number of housekeeping tasks within the current repository, such as compressing file revisions and removing unreachable objects."
-msgstr ""
+msgstr "ç¾åœ¨ã®ãƒªãƒã‚¸ãƒˆãƒªå†…ã§ã€ãƒ•ã‚¡ã‚¤ãƒ«ãƒªãƒ“ジョンã®åœ§ç¸®ã‚„アクセスä¸å¯èƒ½ãªã‚ªãƒ–ジェクトã®é™¤åŽ»ã¨ã„ã£ãŸã€ã„ãã¤ã‹ã®ãƒã‚¦ã‚¹ã‚­ãƒ¼ãƒ”ングを実行ã—ã¾ã™ã€‚"
msgid "SAML SSO"
msgstr "SAML SSO"
@@ -17445,20 +17808,23 @@ msgstr "SAML SSO"
msgid "SAML SSO for %{group_name}"
msgstr "%{group_name} 用㮠SAML SSO"
-msgid "SAML for %{group_name}"
+msgid "SAML discovery tokens"
msgstr ""
+msgid "SAML for %{group_name}"
+msgstr "%{group_name} 用ã®SAML"
+
msgid "SHA256"
msgstr ""
msgid "SSH Key"
-msgstr ""
+msgstr "SSH éµ"
msgid "SSH Keys"
msgstr "SSH éµ"
msgid "SSH Keys Help"
-msgstr ""
+msgstr "SSHéµã®ãƒ˜ãƒ«ãƒ—"
msgid "SSH host key fingerprints"
msgstr ""
@@ -17488,7 +17854,7 @@ msgid "Save Changes"
msgstr "変更をä¿å­˜"
msgid "Save anyway"
-msgstr ""
+msgstr "強制ä¿å­˜"
msgid "Save application"
msgstr "アプリケーションã®ä¿å­˜"
@@ -17497,7 +17863,7 @@ msgid "Save changes"
msgstr "変更をä¿å­˜"
msgid "Save changes before testing"
-msgstr ""
+msgstr "テストをã™ã‚‹å‰ã«å¤‰æ›´ã‚’é©ç”¨ã—ã¾ã™ã‹ï¼Ÿ"
msgid "Save comment"
msgstr "コメントをä¿å­˜"
@@ -17521,7 +17887,7 @@ msgid "Saving"
msgstr ""
msgid "Saving project."
-msgstr ""
+msgstr "プロジェクトをä¿å­˜"
msgid "Schedule a new pipeline"
msgstr "æ–°ã—ã„パイプラインã®ã‚¹ã‚±ã‚¸ãƒ¥ãƒ¼ãƒ«ã‚’作æˆ"
@@ -17529,15 +17895,21 @@ msgstr "æ–°ã—ã„パイプラインã®ã‚¹ã‚±ã‚¸ãƒ¥ãƒ¼ãƒ«ã‚’作æˆ"
msgid "Scheduled"
msgstr "スケジュール済"
+msgid "Scheduled to merge this merge request (%{strategy})."
+msgstr ""
+
msgid "Scheduled to merge this merge request when the pipeline succeeds."
msgstr ""
msgid "Schedules"
msgstr "スケジュール"
-msgid "Scheduling"
+msgid "Schedules to merge this merge request (%{strategy})."
msgstr ""
+msgid "Scheduling"
+msgstr "スケジュール"
+
msgid "Scheduling Pipelines"
msgstr "パイプラインスケジューリング"
@@ -17545,7 +17917,7 @@ msgid "Scope"
msgstr "スコープ"
msgid "Scope not supported with disabled 'users_search' feature!"
-msgstr ""
+msgstr "スコープã¯ã€ç„¡åŠ¹ãª 'users_search' 機能ã§ã‚µãƒãƒ¼ãƒˆã•ã‚Œã¦ã„ã¾ã›ã‚“。"
msgid "Scoped issue boards"
msgstr "スコープ付課題ボード"
@@ -17560,7 +17932,7 @@ msgid "Scroll down"
msgstr ""
msgid "Scroll down to <strong>Google Code Project Hosting</strong> and enable the switch on the right."
-msgstr ""
+msgstr "<strong>Google Code Project Hosting</strong> ã«ã‚¹ã‚¯ãƒ­ãƒ¼ãƒ«ã—ã€å³å´ã®ã‚¹ã‚¤ãƒƒãƒã‚’有効ã«ã—ã¾ã™ã€‚"
msgid "Scroll left"
msgstr ""
@@ -17592,6 +17964,9 @@ msgstr "ブランãƒã‚’検索"
msgid "Search branches and tags"
msgstr "ブランãƒã¾ãŸã¯ã‚¿ã‚°ã‚’検索"
+msgid "Search by author"
+msgstr ""
+
msgid "Search files"
msgstr "ファイルã®æ¤œç´¢"
@@ -17611,7 +17986,7 @@ msgid "Search for this text"
msgstr ""
msgid "Search forks"
-msgstr ""
+msgstr "フォークを検索ã™ã‚‹"
msgid "Search groups"
msgstr "グループを検索"
@@ -17623,7 +17998,7 @@ msgid "Search milestones"
msgstr "マイルストーンを検索"
msgid "Search or filter results..."
-msgstr ""
+msgstr "çµæžœã‚’検索ã¾ãŸã¯ãƒ•ã‚£ãƒ«ã‚¿ã™ã‚‹..."
msgid "Search or filter results…"
msgstr ""
@@ -17638,7 +18013,7 @@ msgid "Search projects"
msgstr "プロジェクトを検索"
msgid "Search projects..."
-msgstr ""
+msgstr "プロジェクトを検索..."
msgid "Search users"
msgstr "ユーザーを検索"
@@ -17653,13 +18028,13 @@ msgid "Search your projects"
msgstr "プロジェクトを検索"
msgid "SearchAutocomplete|All GitLab"
-msgstr ""
+msgstr "å…¨ã¦ã® GitLab"
msgid "SearchAutocomplete|Issues I've created"
-msgstr ""
+msgstr "自身ãŒä½œæˆã—ãŸèª²é¡Œ"
msgid "SearchAutocomplete|Issues assigned to me"
-msgstr ""
+msgstr "自身ã«å‰²ã‚Šå½“ã¦ã‚‰ã‚ŒãŸèª²é¡Œ"
msgid "SearchAutocomplete|Merge requests I've created"
msgstr "自身ãŒä½œæˆã—ãŸãƒžãƒ¼ã‚¸ãƒªã‚¯ã‚¨ã‚¹ãƒˆ"
@@ -17671,10 +18046,10 @@ msgid "SearchAutocomplete|in all GitLab"
msgstr "å…¨ã¦ã® GitLab 中ã§"
msgid "SearchAutocomplete|in this group"
-msgstr ""
+msgstr "グループ内ã§"
msgid "SearchAutocomplete|in this project"
-msgstr ""
+msgstr "ã“ã®ãƒ—ロジェクト内ã§"
msgid "SearchCodeResults|in"
msgstr ""
@@ -17729,10 +18104,6 @@ msgid "SearchResults|snippet"
msgid_plural "SearchResults|snippets"
msgstr[0] ""
-msgid "SearchResults|snippet result"
-msgid_plural "SearchResults|snippet results"
-msgstr[0] ""
-
msgid "SearchResults|user"
msgid_plural "SearchResults|users"
msgstr[0] ""
@@ -17741,6 +18112,9 @@ msgid "SearchResults|wiki result"
msgid_plural "SearchResults|wiki results"
msgstr[0] ""
+msgid "Searching by both author and message is currently not supported."
+msgstr ""
+
msgid "Seat Link"
msgstr ""
@@ -17768,184 +18142,232 @@ msgstr ""
msgid "Security Dashboard"
msgstr "セキュリティダッシュボード"
-msgid "Security Dashboard|Error fetching the vulnerability counts. Please check your network connection and try again."
+msgid "Security configuration help link"
msgstr ""
-msgid "Security Dashboard|Error fetching the vulnerability list. Please check your network connection and try again."
+msgid "Security dashboard"
+msgstr "セキュリティダッシュボード"
+
+msgid "Security report is out of date. Please update your branch with the latest changes from the target branch (%{targetBranchName})"
msgstr ""
-msgid "Security Dashboard|Issue Created"
+msgid "Security report is out of date. Run %{newPipelineLinkStart}a new pipeline%{newPipelineLinkEnd} for the target branch (%{targetBranchName})"
msgstr ""
-msgid "Security Reports|Comment added to '%{vulnerabilityName}'"
+msgid "SecurityConfiguration|Configured"
msgstr ""
-msgid "Security Reports|Comment deleted on '%{vulnerabilityName}'"
+msgid "SecurityConfiguration|Feature"
msgstr ""
-msgid "Security Reports|Comment edited on '%{vulnerabilityName}'"
+msgid "SecurityConfiguration|Feature documentation for %{featureName}"
msgstr ""
-msgid "Security Reports|Create issue"
+msgid "SecurityConfiguration|Not yet configured"
msgstr ""
-msgid "Security Reports|Dismiss vulnerability"
-msgstr "脆弱性を無視ã™ã‚‹"
+msgid "SecurityConfiguration|Secure features"
+msgstr ""
-msgid "Security Reports|Dismissed '%{vulnerabilityName}'"
+msgid "SecurityConfiguration|Status"
msgstr ""
-msgid "Security Reports|Dismissed '%{vulnerabilityName}'. Turn off the hide dismissed toggle to view."
+msgid "SecurityReports|%{firstProject} and %{secondProject}"
msgstr ""
-msgid "Security Reports|Either you don't have permission to view this dashboard or the dashboard has not been setup. Please check your permission settings with your administrator or check your dashboard configurations to proceed."
+msgid "SecurityReports|%{firstProject}, %{secondProject}, and %{rest}"
msgstr ""
-msgid "Security Reports|Learn more about setting up your dashboard"
+msgid "SecurityReports|Add a project to your dashboard"
msgstr ""
-msgid "Security Reports|More info"
+msgid "SecurityReports|Add or remove projects from your dashboard"
msgstr ""
-msgid "Security Reports|Oops, something doesn't seem right."
+msgid "SecurityReports|Add projects"
msgstr ""
-msgid "Security Reports|Security reports can only be accessed by authorized users."
+msgid "SecurityReports|Comment added to '%{vulnerabilityName}'"
msgstr ""
-msgid "Security Reports|There was an error adding the comment."
+msgid "SecurityReports|Comment deleted on '%{vulnerabilityName}'"
msgstr ""
-msgid "Security Reports|There was an error creating the issue."
+msgid "SecurityReports|Comment edited on '%{vulnerabilityName}'"
msgstr ""
-msgid "Security Reports|There was an error creating the merge request."
-msgstr "マージリクエストã®ä½œæˆä¸­ã«ã‚¨ãƒ©ãƒ¼ãŒç™ºç”Ÿã—ã¾ã—ãŸã€‚"
+msgid "SecurityReports|Create issue"
+msgstr ""
-msgid "Security Reports|There was an error deleting the comment."
+msgid "SecurityReports|Dismiss Selected"
msgstr ""
-msgid "Security Reports|There was an error dismissing the vulnerabilities."
+msgid "SecurityReports|Dismiss vulnerability"
msgstr ""
-msgid "Security Reports|There was an error dismissing the vulnerability."
+msgid "SecurityReports|Dismissed '%{vulnerabilityName}'"
msgstr ""
-msgid "Security Reports|There was an error reverting the dismissal."
-msgstr "無視ã®å–り消ã—中ã«ã‚¨ãƒ©ãƒ¼ãŒç™ºç”Ÿã—ã¾ã—ãŸã€‚"
+msgid "SecurityReports|Dismissed '%{vulnerabilityName}'. Turn off the hide dismissed toggle to view."
+msgstr ""
-msgid "Security Reports|There was an error reverting this dismissal."
+msgid "SecurityReports|Each vulnerability now has a unique page that can be directly linked to, shared, referenced, and tracked as the single source of truth. Vulnerability occurrences also persist across scanner runs, which improves tracking and visibility and reduces duplicates between scans."
msgstr ""
-msgid "Security Reports|Undo dismiss"
-msgstr "å…ƒã«æˆ»ã™"
+msgid "SecurityReports|Edit dashboard"
+msgstr ""
-msgid "Security Reports|You do not have sufficient permissions to access this report"
+msgid "SecurityReports|Either you don't have permission to view this dashboard or the dashboard has not been setup. Please check your permission settings with your administrator or check your dashboard configurations to proceed."
msgstr ""
-msgid "Security Reports|You must sign in as an authorized user to see this report"
+msgid "SecurityReports|Error fetching the vulnerability counts. Please check your network connection and try again."
msgstr ""
-msgid "Security configuration help link"
+msgid "SecurityReports|Error fetching the vulnerability list. Please check your network connection and try again."
msgstr ""
-msgid "Security dashboard"
-msgstr "セキュリティダッシュボード"
+msgid "SecurityReports|False positive"
+msgstr ""
-msgid "Security report is out of date. Please update your branch with the latest changes from the target branch (%{targetBranchName})"
+msgid "SecurityReports|Hide dismissed"
msgstr ""
-msgid "Security report is out of date. Run %{newPipelineLinkStart}a new pipeline%{newPipelineLinkEnd} for the target branch (%{targetBranchName})"
+msgid "SecurityReports|Introducing standalone vulnerabilities"
msgstr ""
-msgid "SecurityConfiguration|Configured"
+msgid "SecurityReports|Issue Created"
msgstr ""
-msgid "SecurityConfiguration|Feature"
+msgid "SecurityReports|Learn More"
msgstr ""
-msgid "SecurityConfiguration|Feature documentation for %{featureName}"
+msgid "SecurityReports|Learn more about setting up your dashboard"
msgstr ""
-msgid "SecurityConfiguration|Not yet configured"
+msgid "SecurityReports|Load more vulnerabilities"
msgstr ""
-msgid "SecurityConfiguration|Secure features"
+msgid "SecurityReports|Monitor vulnerabilities in your code"
msgstr ""
-msgid "SecurityConfiguration|Status"
+msgid "SecurityReports|More info"
+msgstr ""
+
+msgid "SecurityReports|More information"
+msgstr ""
+
+msgid "SecurityReports|No vulnerabilities found for dashboard"
+msgstr ""
+
+msgid "SecurityReports|No vulnerabilities found for this group"
+msgstr ""
+
+msgid "SecurityReports|No vulnerabilities found for this pipeline"
+msgstr ""
+
+msgid "SecurityReports|No vulnerabilities found for this project"
+msgstr ""
+
+msgid "SecurityReports|Oops, something doesn't seem right."
+msgstr ""
+
+msgid "SecurityReports|Pipeline %{pipelineLink} triggered %{timeago} by %{user}"
+msgstr ""
+
+msgid "SecurityReports|Project"
+msgstr ""
+
+msgid "SecurityReports|Projects added"
+msgstr ""
+
+msgid "SecurityReports|Remove project from dashboard"
+msgstr ""
+
+msgid "SecurityReports|Report type"
msgstr ""
-msgid "SecurityDashboard|%{firstProject} and %{secondProject}"
+msgid "SecurityReports|Return to dashboard"
msgstr ""
-msgid "SecurityDashboard|%{firstProject}, %{secondProject}, and %{rest}"
+msgid "SecurityReports|Security Dashboard"
msgstr ""
-msgid "SecurityDashboard|Add a project to your dashboard"
+msgid "SecurityReports|Security reports can only be accessed by authorized users."
msgstr ""
-msgid "SecurityDashboard|Add or remove projects from your dashboard"
+msgid "SecurityReports|Select a project to add by using the project search field above."
msgstr ""
-msgid "SecurityDashboard|Add projects"
+msgid "SecurityReports|Select a reason"
msgstr ""
-msgid "SecurityDashboard|Edit dashboard"
+msgid "SecurityReports|Severity"
msgstr ""
-msgid "SecurityDashboard|Hide dismissed"
+msgid "SecurityReports|Status"
msgstr ""
-msgid "SecurityDashboard|Monitor vulnerabilities in your code"
+msgid "SecurityReports|The security dashboard displays the latest security findings for projects you wish to monitor. Select \"Edit dashboard\" to add and remove projects."
msgstr ""
-msgid "SecurityDashboard|More information"
+msgid "SecurityReports|The security dashboard displays the latest security report. Use it to find and fix vulnerabilities."
msgstr ""
-msgid "SecurityDashboard|No vulnerabilities found for dashboard"
+msgid "SecurityReports|There was an error adding the comment."
msgstr ""
-msgid "SecurityDashboard|Pipeline %{pipelineLink} triggered %{timeago} by %{user}"
+msgid "SecurityReports|There was an error creating the issue."
msgstr ""
-msgid "SecurityDashboard|Project"
+msgid "SecurityReports|There was an error creating the merge request."
msgstr ""
-msgid "SecurityDashboard|Projects added"
+msgid "SecurityReports|There was an error deleting the comment."
msgstr ""
-msgid "SecurityDashboard|Remove project from dashboard"
+msgid "SecurityReports|There was an error dismissing the vulnerabilities."
msgstr ""
-msgid "SecurityDashboard|Report type"
+msgid "SecurityReports|There was an error dismissing the vulnerability."
msgstr ""
-msgid "SecurityDashboard|Return to dashboard"
+msgid "SecurityReports|There was an error reverting the dismissal."
msgstr ""
-msgid "SecurityDashboard|Security Dashboard"
+msgid "SecurityReports|There was an error reverting this dismissal."
msgstr ""
-msgid "SecurityDashboard|Select a project to add by using the project search field above."
+msgid "SecurityReports|There was an error while generating the report."
msgstr ""
-msgid "SecurityDashboard|Severity"
+msgid "SecurityReports|Unable to add %{invalidProjects}"
msgstr ""
-msgid "SecurityDashboard|Status"
+msgid "SecurityReports|Undo dismiss"
msgstr ""
-msgid "SecurityDashboard|The security dashboard displays the latest security findings for projects you wish to monitor. Select \"Edit dashboard\" to add and remove projects."
+msgid "SecurityReports|While it's rare to have no vulnerabilities for your group, it can happen. In any event, we ask that you double check your settings to make sure you've set up your dashboard correctly."
msgstr ""
-msgid "SecurityDashboard|The security dashboard displays the latest security report. Use it to find and fix vulnerabilities."
+msgid "SecurityReports|While it's rare to have no vulnerabilities for your pipeline, it can happen. In any event, we ask that you double check your settings to make sure all security scanning jobs have passed successfully."
msgstr ""
-msgid "SecurityDashboard|There was an error while generating the report."
+msgid "SecurityReports|While it's rare to have no vulnerabilities for your project, it can happen. In any event, we ask that you double check your settings to make sure you've set up your dashboard correctly."
msgstr ""
-msgid "SecurityDashboard|Unable to add %{invalidProjects}"
+msgid "SecurityReports|While it's rare to have no vulnerabilities, it can happen. In any event, we ask that you please double check your settings to make sure you've set up your dashboard correctly."
+msgstr ""
+
+msgid "SecurityReports|Won't fix / Accept risk"
+msgstr ""
+
+msgid "SecurityReports|You do not have sufficient permissions to access this report"
+msgstr ""
+
+msgid "SecurityReports|You must sign in as an authorized user to see this report"
+msgstr ""
+
+msgid "SecurityReports|[No reason]"
msgstr ""
msgid "See GitLab's %{password_policy_guidelines}"
@@ -17955,6 +18377,9 @@ msgid "See metrics"
msgstr "メトリクスをå‚ç…§"
msgid "See the affected projects in the GitLab admin panel"
+msgstr "GitLab管ç†ç”»é¢ã§å½±éŸ¿ã‚’å—ã‘るプロジェクトをå‚ç…§"
+
+msgid "See what's new at GitLab"
msgstr ""
msgid "Select"
@@ -17964,19 +18389,19 @@ msgid "Select Archive Format"
msgstr "アーカイブã®ãƒ•ã‚©ãƒ¼ãƒžãƒƒãƒˆã‚’é¸æŠž"
msgid "Select GitLab project to link with your Slack team"
-msgstr ""
+msgstr "Slackãƒãƒ¼ãƒ ã¨ãƒªãƒ³ã‚¯ã™ã‚‹ GitLab ã®ãƒ—ロジェクトをé¸æŠžã—ã¾ã™"
msgid "Select Page"
-msgstr ""
+msgstr "ページを変更"
msgid "Select Stack"
-msgstr ""
+msgstr "スタックã®é¸æŠž"
msgid "Select a file from the left sidebar to begin editing. Afterwards, you'll be able to commit your changes."
msgstr ""
msgid "Select a group to invite"
-msgstr ""
+msgstr "招待ã™ã‚‹ãŸã‚ã®ã‚°ãƒ«ãƒ¼ãƒ—ã‚’é¸æŠžã™ã‚‹"
msgid "Select a label"
msgstr ""
@@ -17985,13 +18410,13 @@ msgid "Select a namespace to fork the project"
msgstr "プロジェクトをフォークã™ã‚‹åå‰ç©ºé–“ã‚’é¸æŠžã—ã¦ãã ã•ã„"
msgid "Select a new namespace"
-msgstr ""
+msgstr "æ–°ã—ã„åå‰ç©ºé–“ã‚’é¸æŠž"
msgid "Select a project"
msgstr ""
msgid "Select a project to read Insights configuration file"
-msgstr ""
+msgstr "Insights構æˆãƒ•ã‚¡ã‚¤ãƒ«ã‚’読むプロジェクトをé¸æŠžã—ã¦ãã ã•ã„"
msgid "Select a reason"
msgstr ""
@@ -18048,7 +18473,7 @@ msgid "Select project and zone to choose machine type"
msgstr "マシンタイプをé¸æŠžã™ã‚‹ãŸã‚ã«ã€ãƒ—ロジェクトã¨ã‚¾ãƒ¼ãƒ³ã‚’é¸æŠž"
msgid "Select project to choose zone"
-msgstr ""
+msgstr "プロジェクトをé¸æŠžã—ã¦ã‚¾ãƒ¼ãƒ³ã‚’é¸æŠž"
msgid "Select projects"
msgstr ""
@@ -18072,13 +18497,13 @@ msgid "Select target branch"
msgstr "ターゲットブランãƒã‚’é¸æŠž"
msgid "Select the branch you want to set as the default for this project. All merge requests and commits will automatically be made against this branch unless you specify a different one."
-msgstr ""
+msgstr "ã“ã®ãƒ—ロジェクトã®ãƒ‡ãƒ•ã‚©ãƒ«ãƒˆã¨ã—ã¦è¨­å®šã™ã‚‹ãƒ–ランãƒã‚’é¸æŠžã—ã¾ã™ã€‚ã‚ãªãŸãŒä»–ã®ãƒ–ランãƒã‚’指定ã—ãªã„é™ã‚Šã€ã™ã¹ã¦ã®ãƒžãƒ¼ã‚¸ãƒªã‚¯ã‚¨ã‚¹ãƒˆã¨ã‚³ãƒŸãƒƒãƒˆãŒã“ã®ãƒ–ランãƒã«å¯¾ã—ã¦è‡ªå‹•çš„ã«è¡Œã‚ã‚Œã¾ã™ã€‚"
msgid "Select the configured storage available for new repositories to be placed on."
msgstr ""
msgid "Select the custom project template source group."
-msgstr ""
+msgstr "カスタムã®ãƒ—ロジェクトテンプレートã®ã‚°ãƒ«ãƒ¼ãƒ—ã‚’é¸æŠžã—ã¾ã™ã€‚"
msgid "Select timeframe"
msgstr ""
@@ -18087,7 +18512,7 @@ msgid "Select user"
msgstr ""
msgid "Selected levels cannot be used by non-admin users for groups, projects or snippets. If the public level is restricted, user profiles are only visible to logged in users."
-msgstr ""
+msgstr "é¸æŠžã—ãŸãƒ¬ãƒ™ãƒ«ã¯ã€ç®¡ç†è€…以外ã®ãƒ¦ãƒ¼ã‚¶ãƒ¼ãŒã‚°ãƒ«ãƒ¼ãƒ—ã€ãƒ—ロジェクトã€ã¾ãŸã¯ã‚¹ãƒ‹ãƒšãƒƒãƒˆã«ä½¿ç”¨ã™ã‚‹ã“ã¨ã¯ã§ãã¾ã›ã‚“。公開レベルãŒåˆ¶é™ã•ã‚Œã¦ã„ã‚‹å ´åˆã€ãƒ¦ãƒ¼ã‚¶ãƒ¼ãƒ—ロファイルã¯ãƒ­ã‚°ã‚¤ãƒ³ãƒ¦ãƒ¼ã‚¶ãƒ¼ã«ã®ã¿è¡¨ç¤ºã•ã‚Œã¾ã™ã€‚"
msgid "Selecting a GitLab user will add a link to the GitLab user in the descriptions of issues and comments (e.g. \"By <a href=\"#\">@johnsmith</a>\"). It will also associate and/or assign these issues and comments with the selected user."
msgstr "GitLab ユーザーをé¸æŠžã™ã‚‹ã¨ã€GitLab ユーザーã¸ã®ãƒªãƒ³ã‚¯ãŒèª²é¡Œã‚„コメントã®èª¬æ˜Žã«è¿½åŠ ã•ã‚Œã¾ã™ (例:\"By <a href=\"#\">@johnsmith</a>\")。ã¾ãŸã€ã“れらã®èª²é¡Œã‚„コメントをé¸æŠžã—ãŸãƒ¦ãƒ¼ã‚¶ãƒ¼ã«é–¢é€£ä»˜ã‘や割り当ã¦ãŒã§ãã¾ã™ã€‚"
@@ -18156,7 +18581,7 @@ msgid "Sep"
msgstr "9月"
msgid "Separate topics with commas."
-msgstr ""
+msgstr "トピックã¯ã‚³ãƒ³ãƒžã§åŒºåˆ‡ã‚Šã¾ã™ã€‚"
msgid "September"
msgstr "9月"
@@ -18168,55 +18593,55 @@ msgid "Serve repository static objects (e.g. archives, blobs, ...) from an exter
msgstr ""
msgid "Server supports batch API only, please update your Git LFS client to version 1.0.1 and up."
-msgstr ""
+msgstr "サーãƒãƒ¼ã¯ãƒãƒƒãƒ API ã®ã¿ã‚’サãƒãƒ¼ãƒˆã—ã¦ã„ã¾ã™ã€‚ Git LFS クライアントをãƒãƒ¼ã‚¸ãƒ§ãƒ³1.0.1以上ã«æ›´æ–°ã—ã¦ãã ã•ã„。"
msgid "Server version"
msgstr "サーãƒãƒ¼ã®ãƒãƒ¼ã‚¸ãƒ§ãƒ³"
msgid "Serverless"
-msgstr ""
+msgstr "Serverless"
msgid "Serverless domain"
-msgstr ""
+msgstr "Severlessドメイン"
msgid "ServerlessDetails|Function invocation metrics require Prometheus to be installed first."
-msgstr ""
+msgstr "関数呼ã³å‡ºã—メトリクスã§ã¯ã€ Prometheus を最åˆã«ã‚¤ãƒ³ã‚¹ãƒˆãƒ¼ãƒ«ã™ã‚‹å¿…è¦ãŒã‚ã‚Šã¾ã™ã€‚"
msgid "ServerlessDetails|Install Prometheus"
-msgstr ""
+msgstr "Prometheus ã®ã‚¤ãƒ³ã‚¹ãƒˆãƒ¼ãƒ«"
msgid "ServerlessDetails|Invocation metrics loading or not available at this time."
-msgstr ""
+msgstr "呼ã³å‡ºã—メトリクスã®ãƒ­ãƒ¼ãƒ‰ä¸­ã€ã¾ãŸã¯ç¾åœ¨åˆ©ç”¨ã§ãã¾ã›ã‚“。"
msgid "ServerlessDetails|Invocations"
-msgstr ""
+msgstr "呼ã³å‡ºã—"
msgid "ServerlessDetails|Kubernetes Pods"
-msgstr ""
+msgstr "Kubernetesãƒãƒƒãƒ‰"
msgid "ServerlessDetails|More information"
-msgstr ""
+msgstr "詳細情報"
msgid "ServerlessDetails|No pods loaded at this time."
msgstr ""
msgid "ServerlessDetails|Number of Kubernetes pods in use over time based on necessity."
-msgstr ""
+msgstr "å¿…è¦ã«å¿œã˜ã¦é•·æ™‚間使用中ã¨ãªã‚‹Kubernetesãƒãƒƒãƒ‰æ•°ã€‚"
msgid "ServerlessDetails|pod in use"
-msgstr ""
+msgstr "使用中ã®ãƒãƒƒãƒ‰"
msgid "ServerlessDetails|pods in use"
-msgstr ""
+msgstr "使用中ã®ãƒãƒƒãƒ‰"
msgid "ServerlessURL|Copy URL"
msgstr ""
msgid "Serverless| In order to start using functions as a service, you must first install Knative on your Kubernetes cluster."
-msgstr ""
+msgstr "サービスã¨ã—ã¦functionã®ä½¿ç”¨ã‚’開始ã™ã‚‹ã«ã¯ã€ã¾ãšKubernetesクラスターã«Knativeをインストールã™ã‚‹å¿…è¦ãŒã‚ã‚Šã¾ã™ã€‚"
msgid "Serverless|Getting started with serverless"
-msgstr ""
+msgstr "Serverless を始ã‚よã†"
msgid "Serverless|Help shape the future of Serverless at GitLab"
msgstr ""
@@ -18225,10 +18650,10 @@ msgid "Serverless|If you believe none of these apply, please check back later as
msgstr ""
msgid "Serverless|Install Knative"
-msgstr ""
+msgstr "Knativeã®ã‚¤ãƒ³ã‚¹ãƒˆãƒ¼ãƒ«"
msgid "Serverless|Learn more about Serverless"
-msgstr ""
+msgstr "Serverless ã«ã¤ã„ã¦"
msgid "Serverless|No functions available"
msgstr "利用å¯èƒ½ãªæ©Ÿèƒ½ã¯ã‚ã‚Šã¾ã›ã‚“"
@@ -18342,16 +18767,16 @@ msgid "Set the duration for which the jobs will be considered as old and expired
msgstr ""
msgid "Set the maximum file size for each job's artifacts"
-msgstr ""
+msgstr "å„ジョブã®æˆæžœç‰©ã®æœ€å¤§ãƒ•ã‚¡ã‚¤ãƒ«ã‚µã‚¤ã‚ºã‚’設定"
msgid "Set the maximum number of pipeline minutes that a group can use on shared Runners per month. 0 for unlimited."
-msgstr ""
+msgstr "グループãŒ1ヶ月間ã«å…±æœ‰ãƒ©ãƒ³ãƒŠãƒ¼ã§ä½¿ç”¨ã§ãã‚‹ã®ãƒ‘イプラインã®æœ€å¤§æ™‚間(分)を設定ã—ã¾ã™ã€‚無制é™ã«ã™ã‚‹å ´åˆã¯0を指定ã—ã¾ã™ã€‚"
msgid "Set the milestone to %{milestone_reference}."
msgstr ""
msgid "Set time estimate"
-msgstr ""
+msgstr "見ç©æ™‚é–“ã®è¨­å®š"
msgid "Set time estimate to %{time_estimate}."
msgstr ""
@@ -18362,9 +18787,6 @@ msgstr "CI/CD を設定"
msgid "Set up Jira Integration"
msgstr ""
-msgid "Set up Jira Integration illustration"
-msgstr ""
-
msgid "Set up a %{type} Runner automatically"
msgstr ""
@@ -18375,7 +18797,7 @@ msgid "Set up assertions/attributes/claims (email, first_name, last_name) and Na
msgstr ""
msgid "Set up new U2F device"
-msgstr ""
+msgstr "æ–°ã—ã„U2Fデãƒã‚¤ã‚¹ã‚’セットアップ"
msgid "Set up new password"
msgstr "æ–°ã—ã„パスワードを設定"
@@ -18384,7 +18806,7 @@ msgid "Set up pipeline subscriptions for this project."
msgstr ""
msgid "Set up your project to automatically push and/or pull changes to/from another repository. Branches, tags, and commits will be synced automatically."
-msgstr ""
+msgstr "プロジェクトを設定ã—ã¦ã€åˆ¥ã®ãƒªãƒã‚¸ãƒˆãƒªã¨ã®é–“ã§å¤‰æ›´ã‚’自動的ã«ãƒ—ッシュ/プルã™ã‚‹ã€‚ブランãƒã€ã‚¿ã‚°ã€ãŠã‚ˆã³ã‚³ãƒŸãƒƒãƒˆã¯è‡ªå‹•çš„ã«åŒæœŸã—ã¾ã™ã€‚"
msgid "Set weight"
msgstr "ウェイトを設定"
@@ -18414,10 +18836,10 @@ msgid "SetStatusModal|Set status"
msgstr "ステータスを設定"
msgid "SetStatusModal|Sorry, we weren't able to set your status. Please try again later."
-msgstr ""
+msgstr "ã‚ãªãŸã®ã‚¹ãƒ†ãƒ¼ã‚¿ã‚¹ã‚’設定ã§ãã¾ã›ã‚“ã§ã—ãŸã€‚後ã§ã‚‚ã†ä¸€åº¦ãŠè©¦ã—ãã ã•ã„。"
msgid "SetStatusModal|What's your status?"
-msgstr ""
+msgstr "ã‚ãªãŸã®ã‚¹ãƒ†ãƒ¼ã‚¿ã‚¹ã¯ï¼Ÿ"
msgid "Sets %{epic_ref} as parent epic."
msgstr ""
@@ -18426,16 +18848,16 @@ msgid "Sets target branch to %{branch_name}."
msgstr ""
msgid "Sets the due date to %{due_date}."
-msgstr ""
+msgstr "期日を %{due_date} ã«è¨­å®š 。"
msgid "Sets the milestone to %{milestone_reference}."
-msgstr ""
+msgstr "マイルストーンを %{milestone_reference} ã«è¨­å®šã€‚"
msgid "Sets time estimate to %{time_estimate}."
-msgstr ""
+msgstr "見ç©ã‚‚り時間を %{time_estimate} ã«è¨­å®šã€‚"
msgid "Sets weight to %{weight}."
-msgstr ""
+msgstr "ウェイトを %{weight} ã«è¨­å®š"
msgid "Settings"
msgstr "設定"
@@ -18449,6 +18871,9 @@ msgstr ""
msgid "Severity: %{severity}"
msgstr ""
+msgid "Shards (%{shards})"
+msgstr ""
+
msgid "Shards to synchronize"
msgstr ""
@@ -18468,13 +18893,13 @@ msgid "Shared runners help link"
msgstr "共有Runnerã®ãƒ˜ãƒ«ãƒ—リンク"
msgid "SharedRunnersMinutesSettings|By resetting the pipeline minutes for this namespace, the currently used minutes will be set to zero."
-msgstr ""
+msgstr "ã“ã®åå‰ç©ºé–“ã®ãƒ‘イプライン実行時間をリセットã™ã‚‹ã¨ã€ç¾åœ¨ä½¿ç”¨ã—ã¦ã„るパイプライン実行時間ãŒã‚¼ãƒ­ã«è¨­å®šã•ã‚Œã¾ã™ã€‚"
msgid "SharedRunnersMinutesSettings|Reset pipeline minutes"
-msgstr ""
+msgstr "パイプライン実行時間をリセット"
msgid "SharedRunnersMinutesSettings|Reset used pipeline minutes"
-msgstr ""
+msgstr "使用済ã¿ã®ãƒ‘イプライン実行時間をリセットã™ã‚‹"
msgid "Sherlock Transactions"
msgstr "シャーロックトランザクション"
@@ -18598,7 +19023,7 @@ msgid "Sign in using smart card"
msgstr "スマートカードを使ã£ã¦ã‚µã‚¤ãƒ³ã‚¤ãƒ³"
msgid "Sign in via 2FA code"
-msgstr ""
+msgstr "2FAコードã§ã‚µã‚¤ãƒ³ã‚¤ãƒ³"
msgid "Sign in with Single Sign-On"
msgstr "シングルサインオンã§ã‚µã‚¤ãƒ³ã‚¤ãƒ³ã™ã‚‹"
@@ -18610,13 +19035,13 @@ msgid "Sign out"
msgstr "サインアウト"
msgid "Sign out & Register"
-msgstr ""
+msgstr "ログアウト&登録"
msgid "Sign up"
msgstr ""
msgid "Sign up was successful! Please confirm your email to sign in."
-msgstr ""
+msgstr "登録ã¯æˆåŠŸã—ã¾ã—ãŸï¼ メールアドレスを確èªã—ã¦ã‚µã‚¤ãƒ³ã‚¤ãƒ³ã—ã¦ãã ã•ã„。"
msgid "Sign-in restrictions"
msgstr "サインインã®åˆ¶é™"
@@ -18631,16 +19056,16 @@ msgid "SignUp|Last Name is too long (maximum is %{max_length} characters)."
msgstr ""
msgid "SignUp|Name is too long (maximum is %{max_length} characters)."
-msgstr ""
+msgstr "åå‰ãŒé•·ã™ãŽã¾ã™(最大 %{max_length} 文字)"
msgid "SignUp|Username is too long (maximum is %{max_length} characters)."
-msgstr ""
+msgstr "ユーザーåãŒé•·ã™ãŽã¾ã™(最大 %{max_length} 文字)"
msgid "Signed in"
msgstr ""
msgid "Signed in with %{authentication} authentication"
-msgstr ""
+msgstr "%{authentication} èªè¨¼ã§ã‚µã‚¤ãƒ³ã‚¤ãƒ³"
msgid "Signing in using %{label} has been disabled"
msgstr ""
@@ -18715,7 +19140,7 @@ msgid "SlackService|This service allows users to perform common operations on th
msgstr ""
msgid "Slower but makes sure the project workspace is pristine as it clones the repository from scratch for every job"
-msgstr ""
+msgstr "é…ããªã‚Šã¾ã™ãŒã€ãƒ—ロジェクトã®ä½œæ¥­é ˜åŸŸã¯ã€ã™ã¹ã¦ã®ã‚¸ãƒ§ãƒ–ã®ãŸã‚ã«ã‚¼ãƒ­ã‹ã‚‰ãƒªãƒã‚¸ãƒˆãƒªã‚’クローンã™ã‚‹ã®ã§ã€ã‚¯ãƒªã‚¢ãªçŠ¶æ…‹ã«ãªã‚Šã¾ã™"
msgid "Smartcard"
msgstr "スマートカード"
@@ -18762,6 +19187,9 @@ msgstr ""
msgid "Snowplow"
msgstr ""
+msgid "Some child epics may be hidden due to applied filters"
+msgstr ""
+
msgid "Some email servers do not support overriding the email sender name. Enable this option to include the name of the author of the issue, merge request or comment in the email body instead."
msgstr "一部ã®ãƒ¡ãƒ¼ãƒ«ã‚µãƒ¼ãƒãƒ¼ã¯ã€ãƒ¡ãƒ¼ãƒ«é€ä¿¡è€…åã®ä¸Šæ›¸ãをサãƒãƒ¼ãƒˆã—ã¦ã„ã¾ã›ã‚“。ã“ã®ã‚ªãƒ—ションを有効ã«ã™ã‚‹ã¨ã€ä»£ã‚ã‚Šã«ãƒ¡ãƒ¼ãƒ«æœ¬æ–‡ã«èª²é¡Œã€ãƒžãƒ¼ã‚¸ãƒªã‚¯ã‚¨ã‚¹ãƒˆã€ã¾ãŸã¯ã‚³ãƒ¡ãƒ³ãƒˆã®ä½œæˆè€…ã®åå‰ã‚’å«ã‚ã‚‹ã“ã¨ãŒã§ãã¾ã™ã€‚"
@@ -18802,10 +19230,10 @@ msgid "Something went wrong when toggling the button"
msgstr "ボタンã®åˆ‡ã‚Šæ›¿ãˆä¸­ã«å•é¡ŒãŒç™ºç”Ÿã—ã¾ã—ãŸ"
msgid "Something went wrong while adding your award. Please try again."
-msgstr ""
+msgstr "å—賞中ã«å•é¡ŒãŒç™ºç”Ÿã—ã¾ã—ãŸã€‚ã‚‚ã†ä¸€åº¦ã‚„ã‚Šç›´ã—ã¦ãã ã•ã„。"
msgid "Something went wrong while applying the suggestion. Please try again."
-msgstr ""
+msgstr "æ案ã—ã¦ã„ã‚‹é–“ã«å•é¡ŒãŒç™ºç”Ÿã—ã¾ã—ãŸã€‚ã‚‚ã†ä¸€åº¦ã‚„ã‚Šç›´ã—ã¦ãã ã•ã„。"
msgid "Something went wrong while archiving a requirement."
msgstr ""
@@ -18823,7 +19251,7 @@ msgid "Something went wrong while deleting the package."
msgstr ""
msgid "Something went wrong while deleting the source branch. Please try again."
-msgstr ""
+msgstr "ソースブランãƒã®å‰Šé™¤ä¸­ã«å•é¡ŒãŒç™ºç”Ÿã—ã¾ã—ãŸã€‚ã‚‚ã†ä¸€åº¦ã‚„ã‚Šç›´ã—ã¦ãã ã•ã„。"
msgid "Something went wrong while deleting your note. Please try again."
msgstr ""
@@ -18847,7 +19275,7 @@ msgid "Something went wrong while fetching group member contributions"
msgstr "グループメンãƒãƒ¼ã®è²¢çŒ®åº¦ã‚’å–得中ã«å•é¡ŒãŒç™ºç”Ÿã—ã¾ã—ãŸ"
msgid "Something went wrong while fetching latest comments."
-msgstr ""
+msgstr "最新コメントã®å–得中ã«å•é¡ŒãŒç™ºç”Ÿã—ã¾ã—ãŸã€‚"
msgid "Something went wrong while fetching projects"
msgstr ""
@@ -18856,13 +19284,13 @@ msgid "Something went wrong while fetching projects."
msgstr ""
msgid "Something went wrong while fetching related merge requests."
-msgstr ""
+msgstr "マージリクエストã«é–¢é€£ã™ã‚‹ãƒ•ã‚§ãƒƒãƒä¸­ã«å¤±æ•—ã—ã¾ã—ãŸã€‚"
msgid "Something went wrong while fetching requirements list."
msgstr ""
msgid "Something went wrong while fetching the environments for this merge request. Please try again."
-msgstr ""
+msgstr "ã“ã®ãƒžãƒ¼ã‚¸ãƒªã‚¯ã‚¨ã‚¹ãƒˆã®ç’°å¢ƒã‚’å–得中ã«å•é¡ŒãŒç™ºç”Ÿã—ã¾ã—ãŸã€‚ã‚‚ã†ä¸€åº¦ãŠè©¦ã—ãã ã•ã„。"
msgid "Something went wrong while fetching the package."
msgstr ""
@@ -18874,7 +19302,7 @@ msgid "Something went wrong while initializing the OpenAPI viewer"
msgstr ""
msgid "Something went wrong while merging this merge request. Please try again."
-msgstr ""
+msgstr "ã“ã®ãƒžãƒ¼ã‚¸ãƒªã‚¯ã‚¨ã‚¹ãƒˆã®ãƒžãƒ¼ã‚¸ä¸­ã«å•é¡ŒãŒç™ºç”Ÿã—ã¾ã—ãŸã€‚ã‚‚ã†ä¸€åº¦ã‚„ã‚Šç›´ã—ã¦ãã ã•ã„。"
msgid "Something went wrong while moving issues."
msgstr ""
@@ -18903,9 +19331,12 @@ msgstr ""
msgid "Something went wrong while updating your list settings"
msgstr ""
-msgid "Something went wrong, unable to add %{project} to dashboard"
+msgid "Something went wrong with your automatic subscription renewal"
msgstr ""
+msgid "Something went wrong, unable to add %{project} to dashboard"
+msgstr "å•é¡ŒãŒç™ºç”Ÿã—ã¦ã€ãƒ€ãƒƒã‚·ãƒ¥ãƒœãƒ¼ãƒ‰ã« %{project} を追加ã§ãã¾ã›ã‚“"
+
msgid "Something went wrong, unable to add projects to dashboard"
msgstr ""
@@ -18991,10 +19422,10 @@ msgid "SortOptions|Less weight"
msgstr "ウェイトãŒå°ã•ã„é †"
msgid "SortOptions|Manual"
-msgstr ""
+msgstr "マニュアル"
msgid "SortOptions|Milestone due date"
-msgstr ""
+msgstr "マイルストーン期日"
msgid "SortOptions|Milestone due later"
msgstr "マイルストーン期é™ã®é…ã„é †"
@@ -19009,7 +19440,7 @@ msgid "SortOptions|Most popular"
msgstr "人気順"
msgid "SortOptions|Most stars"
-msgstr ""
+msgstr "星ã®å¤šã„é †"
msgid "SortOptions|Name"
msgstr "åå‰"
@@ -19033,7 +19464,7 @@ msgid "SortOptions|Oldest sign in"
msgstr "サインイン順"
msgid "SortOptions|Oldest starred"
-msgstr ""
+msgstr "ãŠæ°—ã«ã„ã‚Šã§å¤ã„é †"
msgid "SortOptions|Oldest updated"
msgstr "å¤ã„æ›´æ–°é †"
@@ -19048,13 +19479,13 @@ msgid "SortOptions|Project"
msgstr ""
msgid "SortOptions|Recent last activity"
-msgstr ""
+msgstr "最近ã®æœ€æ–°ã‚¢ã‚¯ãƒ†ã‚£ãƒ“ティ"
msgid "SortOptions|Recent sign in"
msgstr "最近ã®ã‚µã‚¤ãƒ³ã‚¤ãƒ³é †"
msgid "SortOptions|Recently starred"
-msgstr ""
+msgstr "ãŠæ°—ã«ã„ã‚Šã§æ–°ã—ã„é †"
msgid "SortOptions|Size"
msgstr ""
@@ -19066,7 +19497,7 @@ msgid "SortOptions|Stars"
msgstr ""
msgid "SortOptions|Start date"
-msgstr ""
+msgstr "開始日ã®é †"
msgid "SortOptions|Start later"
msgstr "å¤ã„é †"
@@ -19150,10 +19581,10 @@ msgid "Spam and Anti-bot Protection"
msgstr "スパムã¨ã‚¢ãƒ³ãƒãƒœãƒƒãƒˆä¿è­·"
msgid "Spam log successfully submitted as ham."
-msgstr ""
+msgstr "スパムログã¯ãƒãƒ ã¨ã—ã¦é€ä¿¡ã•ã‚Œã¾ã—ãŸã€‚"
msgid "Specific Runners"
-msgstr ""
+msgstr "特定㮠Runner"
msgid "Specified URL cannot be used: \"%{reason}\""
msgstr ""
@@ -19188,6 +19619,9 @@ msgstr ""
msgid "Stage removed"
msgstr ""
+msgid "Standard"
+msgstr ""
+
msgid "Star a label to make it a priority label. Order the prioritized labels to change their relative priority, by dragging."
msgstr "優先ラベルã«ã™ã‚‹ã«ã¯ã€ãƒ©ãƒ™ãƒ«ã«ã‚¹ã‚¿ãƒ¼ã‚’付ã‘ã¾ã™ã€‚ドラッグã—ã¦ã€å„ªå…ˆé †ä½ã‚’並ã¹æ›¿ãˆã‚‹ã“ã¨ãŒã§ãã¾ã™ã€‚"
@@ -19210,22 +19644,22 @@ msgid "Starred projects"
msgstr "スター付ãプロジェクト"
msgid "StarredProjectsEmptyState|Visit a project page and press on a star icon. Then, you can find the project on this page."
-msgstr ""
+msgstr "プロジェクトページã«ç§»å‹•ã—ã¦ã‚¹ã‚¿ãƒ¼ã‚¢ã‚¤ã‚³ãƒ³ã‚’押ã—ã¾ã™ã€‚ãã®å¾Œã€ã“ã®ãƒšãƒ¼ã‚¸ã§ã“ã®ãƒ—ロジェクトを見ã¤ã‘られるよã†ã«ãªã‚Šã¾ã™ã€‚"
msgid "StarredProjectsEmptyState|You don't have starred projects yet."
-msgstr ""
+msgstr "ã‚ãªãŸã¯ã¾ã ãƒ—ロジェクトã«ã‚¹ã‚¿ãƒ¼ã‚’ã¤ã‘ã¦ã„ã¾ã›ã‚“。"
msgid "Starrers"
-msgstr ""
+msgstr "ãŠæ°—ã«ã„ã‚Š"
msgid "Stars"
msgstr "スター"
msgid "Start GitLab Ultimate trial"
-msgstr ""
+msgstr "GitLab Ultimateトライアルを始ã‚ã¾ã—ょã†"
msgid "Start Web Terminal"
-msgstr ""
+msgstr "Webターミナルã®èµ·å‹•"
msgid "Start a %{new_merge_request} with these changes"
msgstr "ã“ã®å¤‰æ›´ã§ %{new_merge_request} を作æˆã™ã‚‹"
@@ -19237,10 +19671,10 @@ msgid "Start a new discussion..."
msgstr ""
msgid "Start a new merge request"
-msgstr ""
+msgstr "æ–°ã—ã„マージリクエストを開始ã™ã‚‹"
msgid "Start a review"
-msgstr ""
+msgstr "レビューã®é–‹å§‹"
msgid "Start and due date"
msgstr "開始日ã¨çµ‚了日"
@@ -19311,6 +19745,9 @@ msgstr "メッセージを有効ã«ã™ã‚‹"
msgid "Static Application Security Testing (SAST)"
msgstr ""
+msgid "StaticSiteEditor|An error occurred while submitting your changes."
+msgstr ""
+
msgid "StaticSiteEditor|Branch could not be created."
msgstr ""
@@ -19320,18 +19757,30 @@ msgstr ""
msgid "StaticSiteEditor|Could not create merge request."
msgstr ""
+msgid "StaticSiteEditor|Incompatible file content"
+msgstr ""
+
msgid "StaticSiteEditor|Return to site"
msgstr ""
+msgid "StaticSiteEditor|Static site editor"
+msgstr ""
+
msgid "StaticSiteEditor|Success!"
msgstr ""
msgid "StaticSiteEditor|Summary of changes"
msgstr ""
+msgid "StaticSiteEditor|The Static Site Editor is currently configured to only edit Markdown content on pages generated from Middleman. Visit the documentation to learn more about configuring your site to use the Static Site Editor."
+msgstr ""
+
msgid "StaticSiteEditor|Update %{sourcePath} file"
msgstr ""
+msgid "StaticSiteEditor|View documentation"
+msgstr ""
+
msgid "StaticSiteEditor|View merge request"
msgstr ""
@@ -19389,6 +19838,12 @@ msgstr ""
msgid "StatusPage|Status page"
msgstr ""
+msgid "StatusPage|Status page URL"
+msgstr ""
+
+msgid "StatusPage|Status page frontend documentation"
+msgstr ""
+
msgid "StatusPage|To publish incidents to an external status page, GitLab will store a JSON file in your Amazon S3 account in a location accessible to your external status page service. Make sure to also set up %{docsLink}"
msgstr ""
@@ -19405,7 +19860,7 @@ msgid "Stop Terminal"
msgstr "ターミナルをåœæ­¢"
msgid "Stop impersonation"
-msgstr ""
+msgstr "代ç†ã‚’åœæ­¢"
msgid "Stop this environment"
msgstr "ã“ã®ç’°å¢ƒã‚’åœæ­¢ã™ã‚‹"
@@ -19426,7 +19881,7 @@ msgid "Storage:"
msgstr "ストレージ:"
msgid "StorageSize|Unknown"
-msgstr ""
+msgstr "ä¸æ˜Ž"
msgid "Subgroup overview"
msgstr ""
@@ -19447,13 +19902,13 @@ msgid "Subgroups"
msgstr "サブグループ"
msgid "Subgroups and projects"
-msgstr ""
+msgstr "サブグループã¨ãƒ—ロジェクト"
msgid "Subject Key Identifier:"
msgstr ""
msgid "Subkeys"
-msgstr ""
+msgstr "サブキー"
msgid "Submit Changes"
msgstr ""
@@ -19471,10 +19926,10 @@ msgid "Submit issue"
msgstr ""
msgid "Submit review"
-msgstr ""
+msgstr "レビューをé€ä¿¡"
msgid "Submit search"
-msgstr ""
+msgstr "検索ã®æ出"
msgid "Submit the current review."
msgstr ""
@@ -19495,7 +19950,7 @@ msgid "Subscribe to RSS feed"
msgstr "RSS フィードを購読"
msgid "Subscribe to calendar"
-msgstr ""
+msgstr "カレンダーã«ç™»éŒ²"
msgid "Subscribed"
msgstr ""
@@ -19507,7 +19962,7 @@ msgid "Subscribes to this %{quick_action_target}."
msgstr ""
msgid "Subscription"
-msgstr ""
+msgstr "サブスクリプション"
msgid "Subscription deletion failed."
msgstr ""
@@ -19546,46 +20001,46 @@ msgid "SubscriptionTable|Next invoice"
msgstr ""
msgid "SubscriptionTable|Seats currently in use"
-msgstr ""
+msgstr "ç¾åœ¨ä½¿ç”¨ä¸­ã®ã‚·ãƒ¼ãƒˆæ•°"
msgid "SubscriptionTable|Seats in subscription"
-msgstr ""
+msgstr "購読中ã®ã‚·ãƒ¼ãƒˆæ•°"
msgid "SubscriptionTable|Seats owed"
-msgstr ""
+msgstr "未払ã„ã®ã‚·ãƒ¼ãƒˆæ•°"
msgid "SubscriptionTable|Subscription end date"
-msgstr ""
+msgstr "サブスクリプション終了日"
msgid "SubscriptionTable|Subscription start date"
-msgstr ""
+msgstr "サブスクリプション開始日"
msgid "SubscriptionTable|This is the last time the GitLab.com team was in contact with you to settle any outstanding balances."
-msgstr ""
+msgstr "ã“ã‚Œã¯æœ€å¾Œã«ã€æœªæ‰•ã„ã®æ®‹é«˜ã«ã¤ã„ã¦GitLab.comãƒãƒ¼ãƒ ãŒãŠå®¢æ§˜ã«é€£çµ¡ã—ãŸæ—¥æ™‚ã§ã™ã€‚"
msgid "SubscriptionTable|This is the maximum number of users that have existed at the same time since this subscription started."
-msgstr ""
+msgstr "ã“ã‚Œã¯ã€ã“ã®ã‚µãƒ–スクリプション開始以é™ã«åŒæ™‚ã«å­˜åœ¨ã—ã¦ã„ãŸæœ€å¤§ãƒ¦ãƒ¼ã‚¶ãƒ¼æ•°ã§ã™ã€‚"
msgid "SubscriptionTable|This is the next date when the GitLab.com team is scheduled to get in contact with you to settle any outstanding balances."
-msgstr ""
+msgstr "ã“ã‚Œã¯æ¬¡å›žã€æœªæ‰•ã„ã®æ®‹é«˜ã«ã¤ã„ã¦GitLab.comãƒãƒ¼ãƒ ãŒãŠå®¢æ§˜ã«é€£çµ¡ã™ã‚‹äºˆå®šã®æ—¥ç¨‹ã§ã™ã€‚"
msgid "SubscriptionTable|This is the number of seats you will be required to purchase if you update to a paid plan."
-msgstr ""
+msgstr "ã“ã¡ã‚‰ã¯ã€æœ‰æ–™ãƒ—ランã«æ›´æ–°ã—ãŸå ´åˆã«è³¼å…¥ãŒå¿…è¦ã«ãªã‚‹ã‚·ãƒ¼ãƒˆæ•°ã§ã™ã€‚"
msgid "SubscriptionTable|Trial"
-msgstr ""
+msgstr "試用版"
msgid "SubscriptionTable|Trial end date"
-msgstr ""
+msgstr "試用終了日"
msgid "SubscriptionTable|Trial start date"
-msgstr ""
+msgstr "試用開始日"
msgid "SubscriptionTable|Upgrade"
msgstr "アップグレード"
msgid "SubscriptionTable|Usage"
-msgstr ""
+msgstr "使用方法"
msgid "SubscriptionTable|Usage count is performed once a day at 12:00 PM."
msgstr ""
@@ -19606,10 +20061,10 @@ msgid "Successfully activated"
msgstr ""
msgid "Successfully blocked"
-msgstr ""
+msgstr "正常ã«ãƒ–ロックã•ã‚Œã¾ã—ãŸ"
msgid "Successfully confirmed"
-msgstr ""
+msgstr "確èªã«æˆåŠŸã—ã¾ã—ãŸ"
msgid "Successfully deactivated"
msgstr ""
@@ -19618,16 +20073,16 @@ msgid "Successfully deleted U2F device."
msgstr ""
msgid "Successfully removed email."
-msgstr ""
+msgstr "メールを削除ã—ã¾ã—ãŸã€‚"
msgid "Successfully scheduled a pipeline to run. Go to the %{pipelines_link_start}Pipelines page%{pipelines_link_end} for details."
-msgstr ""
+msgstr "実行ã™ã‚‹ãƒ‘イプラインをスケジュールã—ã¾ã—ãŸã€‚ 詳ã—ãã¯%{pipelines_link_start}パイプラインページ%{pipelines_link_end}ã‚’ã”覧ãã ã•ã„。"
msgid "Successfully unblocked"
-msgstr ""
+msgstr "正常ã«ãƒ–ロックãŒè§£é™¤ã•ã‚Œã¾ã—ãŸ"
msgid "Successfully unlocked"
-msgstr ""
+msgstr "正常ã«ãƒ­ãƒƒã‚¯ãŒè§£é™¤ã•ã‚Œã¾ã—ãŸ"
msgid "Successfully verified domain ownership"
msgstr ""
@@ -19639,7 +20094,7 @@ msgid "Suggested change"
msgstr ""
msgid "SuggestedColors|Bright green"
-msgstr ""
+msgstr "ブライトグリーン"
msgid "SuggestedColors|Dark grayish cyan"
msgstr ""
@@ -19660,7 +20115,7 @@ msgid "SuggestedColors|Feijoa"
msgstr ""
msgid "SuggestedColors|Lime green"
-msgstr ""
+msgstr "ライムグリーン"
msgid "SuggestedColors|Moderate blue"
msgstr ""
@@ -19717,10 +20172,10 @@ msgid "Support"
msgstr "サãƒãƒ¼ãƒˆ"
msgid "Support for custom certificates is disabled. Ask your system's administrator to enable it."
-msgstr ""
+msgstr "カスタム証明書ã®ã‚µãƒãƒ¼ãƒˆã¯ç„¡åŠ¹ã§ã™ã€‚有効ã«ã™ã‚‹ã«ã¯ã‚·ã‚¹ãƒ†ãƒ ç®¡ç†è€…ã«ãŠå•ã„åˆã‚ã›ãã ã•ã„。"
msgid "Support page URL"
-msgstr ""
+msgstr "サãƒãƒ¼ãƒˆãƒšãƒ¼ã‚¸URL"
msgid "Switch branch/tag"
msgstr "ブランãƒãƒ»ã‚¿ã‚°åˆ‡ã‚Šæ›¿ãˆ"
@@ -19737,6 +20192,12 @@ msgstr "åŒæœŸæƒ…å ±"
msgid "Synced"
msgstr ""
+msgid "Synchronization disabled"
+msgstr ""
+
+msgid "Synchronization of container repositories is disabled."
+msgstr ""
+
msgid "System"
msgstr "システム"
@@ -19890,6 +20351,9 @@ msgstr "ãƒãƒ¼ãƒ ãƒ‰ãƒ¡ã‚¤ãƒ³"
msgid "Telephone number"
msgstr ""
+msgid "Telephone number (Optional)"
+msgstr ""
+
msgid "Template"
msgstr "テンプレート"
@@ -19906,7 +20370,7 @@ msgid "Terminal"
msgstr "ターミナル"
msgid "Terminal for environment"
-msgstr ""
+msgstr "環境ターミナル"
msgid "Terminal sync service is running"
msgstr ""
@@ -19928,34 +20392,34 @@ msgid_plural "Test coverage: %d hits"
msgstr[0] ""
msgid "Test failed."
-msgstr ""
+msgstr "テストãŒå¤±æ•—ã—ã¾ã—ãŸã€‚"
msgid "Test settings and save changes"
-msgstr ""
+msgstr "設定をテストã—ã¦ã€å¤‰æ›´ã‚’ä¿å­˜ã™ã‚‹"
msgid "TestHooks|Ensure one of your projects has merge requests."
-msgstr ""
+msgstr "å°‘ãã¨ã‚‚一ã¤ã®ãƒ—ロジェクトã«ãƒžãƒ¼ã‚¸ãƒªã‚¯ã‚¨ã‚¹ãƒˆãŒã‚ã‚‹ã“ã¨ã‚’確èªã—ã¦ãã ã•ã„。"
msgid "TestHooks|Ensure the project has CI jobs."
-msgstr ""
+msgstr "プロジェクトã«CIジョブãŒã‚ã‚‹ã“ã¨ã‚’確èªã—ã¦ãã ã•ã„。"
msgid "TestHooks|Ensure the project has CI pipelines."
-msgstr ""
+msgstr "プロジェクトã«CIパイプラインãŒã‚ã‚‹ã“ã¨ã‚’確èªã—ã¦ãã ã•ã„。"
msgid "TestHooks|Ensure the project has at least one commit."
-msgstr ""
+msgstr "プロジェクトã«å°‘ãªãã¨ã‚‚1ã¤ã®ã‚³ãƒŸãƒƒãƒˆãŒã‚ã‚‹ã“ã¨ã‚’確èªã—ã¦ãã ã•ã„。"
msgid "TestHooks|Ensure the project has issues."
-msgstr ""
+msgstr "プロジェクトã«èª²é¡ŒãŒã‚ã‚‹ã“ã¨ã‚’確èªã—ã¦ãã ã•ã„。"
msgid "TestHooks|Ensure the project has merge requests."
-msgstr ""
+msgstr "ã“ã®ãƒ—ロジェクトã«ãƒžãƒ¼ã‚¸ãƒªã‚¯ã‚¨ã‚¹ãƒˆãŒã‚ã‚‹ã“ã¨ã‚’確èªã—ã¦ãã ã•ã„。"
msgid "TestHooks|Ensure the project has notes."
-msgstr ""
+msgstr "プロジェクトã«ãƒŽãƒ¼ãƒˆãŒã‚ã‚‹ã“ã¨ã‚’確èªã—ã¦ãã ã•ã„。"
msgid "TestHooks|Ensure the wiki is enabled and has pages."
-msgstr ""
+msgstr "ã“ã®wikiãŒæœ‰åŠ¹ã§ã€ãƒšãƒ¼ã‚¸ãŒã‚ã‚‹ã“ã¨ã‚’確èªã—ã¦ãã ã•ã„。"
msgid "TestReports|%{count} errors"
msgstr ""
@@ -19994,7 +20458,7 @@ msgid "Thank you for signing up for your free trial! You will get additional ins
msgstr ""
msgid "Thank you for your report. A GitLab administrator will look into it shortly."
-msgstr ""
+msgstr "ã”報告ã‚ã‚ŠãŒã¨ã†ã”ã–ã„ã¾ã™ã€‚ GitLabã®ç®¡ç†è€…ãŒã¾ã‚‚ãªããれを調ã¹ã¾ã™ã€‚"
msgid "Thanks for your purchase!"
msgstr ""
@@ -20006,7 +20470,7 @@ msgid "That's it, well done!%{celebrate}"
msgstr ""
msgid "The \"%{group_path}\" group allows you to sign in with your Single Sign-On Account"
-msgstr ""
+msgstr "ã“ã® %{group_path} グループã§ã€ã‚ãªãŸã¯ã‚·ãƒ³ã‚°ãƒ«ã‚µã‚¤ãƒ³ã‚ªãƒ³ã‚¢ã‚«ã‚¦ãƒ³ãƒˆã§ã‚µã‚¤ãƒ³ã‚¤ãƒ³ã§ãã¾ã™ã€‚"
msgid "The \"Require approval from CODEOWNERS\" setting was moved to %{banner_link_start}Protected Branches%{banner_link_end}"
msgstr ""
@@ -20025,7 +20489,7 @@ msgid "The Advanced Global Search in GitLab is a powerful search service that sa
msgstr "GitLab ã®é«˜åº¦ãªã‚°ãƒ­ãƒ¼ãƒãƒ«æ¤œç´¢ã¯ã€æ™‚間を節約ã§ãる強力ãªæ¤œç´¢ã‚µãƒ¼ãƒ“スã§ã™ã€‚é‡è¤‡ã—ãŸã‚³ãƒ¼ãƒ‰ã®ä½œæˆã‚’ã—ã¦æ™‚間を無駄ã«ã™ã‚‹ã“ã¨ãªãã€è‡ªèº«ã®ãƒ—ロジェクトã®åŠ©ã‘ã¨ãªã‚‹å¤–ã®ãƒãƒ¼ãƒ ã®ã‚³ãƒ¼ãƒ‰ã‚’検索ã™ã‚‹ã“ã¨ãŒã§ãるよã†ã«ãªã‚Šã¾ã™ã€‚"
msgid "The CSV export will be created in the background. Once finished, it will be sent to <strong>%{email}</strong> in an attachment."
-msgstr ""
+msgstr "CSVエクスãƒãƒ¼ãƒˆã‚’ãƒãƒƒã‚¯ã‚°ãƒ©ã‚¦ãƒ³ãƒ‰ã§ä½œæˆã—ã¾ã™ã€‚終了後ã«ã€<strong>%{email}</strong> ã«æ·»ä»˜ã—ã¦ãƒ¡ãƒ¼ãƒ«ã§é€ä¿¡ã•ã‚Œã¾ã™ã€‚"
msgid "The Git LFS objects will <strong>not</strong> be synced."
msgstr "Git ã® LFS オブジェクトã¯<strong>åŒæœŸã—ã¾ã›ã‚“</strong>。"
@@ -20049,7 +20513,7 @@ msgid "The X509 Certificate to use when mutual TLS is required to communicate wi
msgstr "外部èªè¨¼ã‚µãƒ¼ãƒ“スã¨ã®é€šä¿¡ã«ç›¸äº’ TLS ãŒå¿…è¦ãªå ´åˆã«ä½¿ç”¨ã™ã‚‹ X509 証明書。空白ã®ã¾ã¾ã«ã™ã‚‹ã¨ã€HTTPS 経由ã§ã‚¢ã‚¯ã‚»ã‚¹ã™ã‚‹ã¨ãã«ã‚µãƒ¼ãƒè¨¼æ˜Žæ›¸ã®æ¤œè¨¼ãŒè¡Œã‚ã‚Œã¾ã™ã€‚"
msgid "The amount of seconds after which a request to get a secondary node status will time out."
-msgstr ""
+msgstr "セカンダリノードã®ã‚¹ãƒ†ãƒ¼ã‚¿ã‚¹ã‚’å–å¾—ã™ã‚‹è¦æ±‚ãŒã‚¿ã‚¤ãƒ ã‚¢ã‚¦ãƒˆã™ã‚‹ã¾ã§ã®ç§’数。"
msgid "The application will be used where the client secret can be kept confidential. Native mobile apps and Single Page Apps are considered non-confidential."
msgstr ""
@@ -20058,13 +20522,13 @@ msgid "The associated issue #%{issueId} has been closed as the error is now reso
msgstr ""
msgid "The branch for this project has no active pipeline configuration."
-msgstr ""
+msgstr "ã“ã®ãƒ—ロジェクトã®ã“ã®ãƒ–ランãƒã«ã‚¢ã‚¯ãƒ†ã‚£ãƒ–ãªãƒ‘イプライン設定ãŒã‚ã‚Šã¾ã›ã‚“。"
msgid "The branch or tag does not exist"
msgstr ""
msgid "The character highlighter helps you keep the subject line to %{titleLength} characters and wrap the body at %{bodyLength} so they are readable in git."
-msgstr ""
+msgstr "キャラクターãƒã‚¤ãƒ©ã‚¤ã‚¿ãƒ¼ã¯ã€ä»¶åã‚’ %{titleLength} 文字ã«ã€æœ¬æ–‡ã‚’ %{bodyLength} 文字ã«ã¾ã¨ã‚ã€gitã§èª­ã‚るよã†ã«ã—ã¾ã™ã€‚"
msgid "The coding stage shows the time from the first commit to creating the merge request. The data will automatically be added here once you create your first merge request."
msgstr "コーディングステージã§ã¯ã€æœ€åˆã®ã‚³ãƒŸãƒƒãƒˆã‹ã‚‰ãƒžãƒ¼ã‚¸ãƒªã‚¯ã‚¨ã‚¹ãƒˆãŒä½œæˆã•ã‚Œã‚‹ã¾ã§ã®æ™‚é–“ãŒè¡¨ç¤ºã•ã‚Œã¾ã™ã€‚ã“ã®ãƒ‡ãƒ¼ã‚¿ã¯æœ€åˆã®ãƒžãƒ¼ã‚¸ãƒªã‚¯ã‚¨ã‚¹ãƒˆãŒä½œæˆã•ã‚ŒãŸã¨ãã«è‡ªå‹•çš„ã«è¿½åŠ ã•ã‚Œã¾ã™ã€‚"
@@ -20082,7 +20546,7 @@ msgid "The connection will time out after %{timeout}. For repositories that take
msgstr "コãƒã‚¯ã‚·ãƒ§ãƒ³ã¯ %{timeout} ã§ã‚¿ã‚¤ãƒ ã‚¢ã‚¦ãƒˆã—ã¾ã™ã€‚タイムアウトã™ã‚‹ãƒªãƒã‚¸ãƒˆãƒªã§ã¯ã€clone/push を組ã¿åˆã‚ã›ã¦ä½¿ç”¨ã—ã¦ãã ã•ã„。"
msgid "The content of this page is not encoded in UTF-8. Edits can only be made via the Git repository."
-msgstr ""
+msgstr "ã“ã®ãƒšãƒ¼ã‚¸ã¯UTF-8ã§ã‚¨ãƒ³ã‚³ãƒ¼ãƒ‰ã•ã‚Œã¦ã„ã¾ã›ã‚“。編集ã¯Gitリãƒã‚¸ãƒˆãƒªã‚’介ã—ã¦è¡Œã£ã¦ãã ã•ã„。"
msgid "The contents of this group, its subgroups and projects will be permanently removed after %{deletion_adjourned_period} days on %{date}. After this point, your data cannot be recovered."
msgstr ""
@@ -20100,13 +20564,13 @@ msgid "The dependency list details information about the components used within
msgstr ""
msgid "The deployment of this job to %{environmentLink} did not succeed."
-msgstr ""
+msgstr "ã“ã®ã‚¸ãƒ§ãƒ–ã® %{environmentLink} ã¸ã®ãƒ‡ãƒ—ロイã¯æˆåŠŸã—ã¾ã›ã‚“ã§ã—ãŸã€‚"
msgid "The designs you tried uploading did not change."
msgstr ""
msgid "The directory has been successfully created."
-msgstr ""
+msgstr "ディレクトリã®ä½œæˆã«æˆåŠŸã—ã¾ã—ãŸ"
msgid "The domain you entered is misformatted."
msgstr ""
@@ -20115,7 +20579,7 @@ msgid "The domain you entered is not allowed."
msgstr ""
msgid "The entered user map is not a valid JSON user map."
-msgstr ""
+msgstr "入力ã•ã‚ŒãŸãƒ¦ãƒ¼ã‚¶ãƒ¼ãƒžãƒƒãƒ—ã¯ã€æœ‰åŠ¹ãªJSONユーザーマップã§ã¯ã‚ã‚Šã¾ã›ã‚“。"
msgid "The file has been successfully created."
msgstr ""
@@ -20191,6 +20655,9 @@ msgid "The license was removed. GitLab now no longer has a valid license."
msgstr ""
msgid "The license was successfully uploaded and is now active. You can see the details below."
+msgstr "ライセンスã¯æ­£å¸¸ã«ã‚¢ãƒƒãƒ—ロードã•ã‚Œã€ã‚¢ã‚¯ãƒ†ã‚£ãƒ–ã«ãªã‚Šã¾ã—ãŸã€‚ã‚ãªãŸã¯ã€è©³ç´°ãŒè¦‹ã‚Œã¾ã™ã€‚"
+
+msgid "The license was successfully uploaded and will be active from %{starts_at}. You can see the details below."
msgstr ""
msgid "The maximum file size allowed is %{size}."
@@ -20200,13 +20667,13 @@ msgid "The maximum file size allowed is 200KB."
msgstr "許å¯ã•ã‚Œã‚‹æœ€å¤§ãƒ•ã‚¡ã‚¤ãƒ«ã‚µã‚¤ã‚ºã¯ 200KB ã§ã™ã€‚"
msgid "The merge conflicts for this merge request cannot be resolved through GitLab. Please try to resolve them locally."
-msgstr ""
+msgstr "ã“ã®ãƒžãƒ¼ã‚¸ãƒªã‚¯ã‚¨ã‚¹ãƒˆã«å¯¾ã™ã‚‹ãƒžãƒ¼ã‚¸ã®ç«¶åˆã¯ GitLab ã§ã¯è§£æ±ºã§ãã¾ã›ã‚“。ローカルã§è§£æ±ºã—ã¦ãã ã•ã„。"
msgid "The merge conflicts for this merge request have already been resolved."
-msgstr ""
+msgstr "ã“ã®ãƒžãƒ¼ã‚¸ãƒªã‚¯ã‚¨ã‚¹ãƒˆã«å¯¾ã™ã‚‹ãƒžãƒ¼ã‚¸ã®ç«¶åˆã¯ã™ã§ã«è§£æ±ºã•ã‚Œã¦ã„ã¾ã™ã€‚"
msgid "The merge conflicts for this merge request have already been resolved. Please return to the merge request."
-msgstr ""
+msgstr "ã“ã®ãƒžãƒ¼ã‚¸ãƒªã‚¯ã‚¨ã‚¹ãƒˆã«å¯¾ã™ã‚‹ãƒžãƒ¼ã‚¸ã®ç«¶åˆã¯ã™ã§ã«è§£æ±ºã•ã‚Œã¦ã„ã¾ã™ã€‚マージリクエストã«æˆ»ã£ã¦ãã ã•ã„。"
msgid "The merge request can now be merged."
msgstr ""
@@ -20218,7 +20685,7 @@ msgid "The number of changes to be fetched from GitLab when cloning a repository
msgstr ""
msgid "The number of times an upload record could not find its file"
-msgstr ""
+msgstr "アップロードレコードãŒãƒ•ã‚¡ã‚¤ãƒ«ã‚’見ã¤ã‘られãªã‹ã£ãŸå›žæ•°"
msgid "The one place for your designs"
msgstr ""
@@ -20254,19 +20721,19 @@ msgid "The project can be accessed by any user who is logged in."
msgstr "ã“ã®ãƒ—ロジェクトã¯ã€ãƒ­ã‚°ã‚¤ãƒ³ãƒ¦ãƒ¼ã‚¶ãƒ¼ã§ã‚ã‚Œã°èª°ã§ã‚‚アクセスã§ãã¾ã™ã€‚"
msgid "The project can be accessed by anyone, regardless of authentication."
-msgstr ""
+msgstr "プロジェクトã¯ã€èªè¨¼ã«ã‚ˆã‚‰ãšã€èª°ã§ã‚‚アクセスã§ãã¾ã™ã€‚"
msgid "The project can be accessed without any authentication."
msgstr "プロジェクトã¯ã€ãƒ­ã‚°ã‚¤ãƒ³ãªã—ã«èª°ã§ã‚‚アクセスã§ãã¾ã™ã€‚"
msgid "The project is accessible only by members of the project. Access must be granted explicitly to each user."
-msgstr ""
+msgstr "プロジェクトã¯ã€ãã®ãƒ—ロジェクトã®ãƒ¡ãƒ³ãƒãƒ¼ã®ã¿ã‚¢ã‚¯ã‚»ã‚¹ã§ãã¾ã™ã€‚å„ユーザーã«ã‚¢ã‚¯ã‚»ã‚¹æ¨©ã‚’明示的ã«æŒ‡å®šã—ãªã‘ã‚Œã°ãªã‚Šã¾ã›ã‚“。"
msgid "The project is still being deleted. Please try again later."
-msgstr ""
+msgstr "ã“ã®ãƒ—ロジェクトã¯å‰Šé™¤ä¸­ã§ã™ã€‚ã—ã°ã‚‰ã後ã§è©¦ã—ã¦ãã ã•ã„。"
msgid "The project was successfully forked."
-msgstr ""
+msgstr "プロジェクトã¯æ­£å¸¸ã«ãƒ•ã‚©ãƒ¼ã‚¯ã•ã‚Œã¾ã—ãŸã€‚"
msgid "The project was successfully imported."
msgstr ""
@@ -20275,10 +20742,10 @@ msgid "The pseudonymizer data collection is disabled. When enabled, GitLab will
msgstr "匿å化データコレクションã¯ç„¡åŠ¹ã§ã™ã€‚有効ã«ã—ãŸã¨ãã€GitLab ã¯ã€ã‚ãªãŸãŒè¨­å®šã—ãŸã‚ªãƒ–ジェクトストレージディレクトリã«ã‚¢ãƒƒãƒ—ロードã™ã‚‹ GitLab データベースã®åŒ¿å化ã—㟠CSV を生æˆã™ã‚‹ã‚¸ãƒ§ãƒ–ã‚’ãƒãƒƒã‚¯ã‚°ãƒ©ã‚¦ãƒ³ãƒ‰ã§å®Ÿè¡Œã—ã¾ã™ã€‚"
msgid "The remote mirror took to long to complete."
-msgstr ""
+msgstr "リモートミラーãŒå®Œäº†ã™ã‚‹ã¾ã§ã«æ™‚é–“ãŒã‹ã‹ã‚Šã¾ã™ã€‚"
msgid "The remote repository is being updated..."
-msgstr ""
+msgstr "リモートリãƒã‚¸ãƒˆãƒªã¯ç¾åœ¨ã€æ›´æ–°ä¸­ã§ã™ã€‚"
msgid "The repository can be commited to, and issues, comments and other entities can be created."
msgstr ""
@@ -20290,7 +20757,7 @@ msgid "The repository for this project is empty"
msgstr "ã“ã®ãƒ—ロジェクトã«ãƒªãƒã‚¸ãƒˆãƒªã¯ã‚ã‚Šã¾ã›ã‚“"
msgid "The repository is being updated..."
-msgstr ""
+msgstr "リãƒã‚¸ãƒˆãƒªã¯ç¾åœ¨ã€æ›´æ–°ä¸­ã§ã™ã€‚"
msgid "The repository must be accessible over <code>http://</code>, <code>https://</code> or <code>git://</code>."
msgstr "リãƒã‚¸ãƒˆãƒªã«ã¯ã€<code>http://</code>〠<code>https://</code> ã¾ãŸã¯ <code>git://</code>ã§æŽ¥ç¶šã§ããªã‘ã‚Œã°ãªã‚Šã¾ã›ã‚“。"
@@ -20305,19 +20772,19 @@ msgid "The roadmap shows the progress of your epics along a timeline"
msgstr "ロードマップã«ã¯ã€ã‚¿ã‚¤ãƒ ãƒ©ã‚¤ãƒ³ã«æ²¿ã£ãŸã‚¨ãƒ”ックã®é€²æ—ãŒè¡¨ç¤ºã•ã‚Œã¾ã™"
msgid "The schedule time must be in the future!"
-msgstr ""
+msgstr "スケジュールã®æ™‚é–“ã¯ç¾åœ¨ä»¥é™ã®æ—¥æ™‚ã§ãªã‘ã‚Œã°ãªã‚Šã¾ã›ã‚“"
msgid "The snippet can be accessed without any authentication."
-msgstr ""
+msgstr "ã“ã®ã‚¹ãƒ‹ãƒšãƒƒãƒˆã¯èªè¨¼ç„¡ã—ã«ã‚¢ã‚¯ã‚»ã‚¹ã§ãã¾ã™"
msgid "The snippet is visible only to me."
msgstr ""
msgid "The snippet is visible only to project members."
-msgstr ""
+msgstr "スニペットã¯ãƒ—ロジェクトメンãƒãƒ¼ã ã‘ãŒè¦‹ã‚Œã¾ã™ã€‚"
msgid "The snippet is visible to any logged in user."
-msgstr ""
+msgstr "スニペットã¯ã€ãƒ­ã‚°ã‚¤ãƒ³ã—ã¦ã„ã‚‹ã™ã¹ã¦ã®ãƒ¦ãƒ¼ã‚¶ãƒ¼ãŒè¦‹ã‚Œã¾ã™ã€‚"
msgid "The specified tab is invalid, please select another"
msgstr ""
@@ -20338,16 +20805,16 @@ msgid "The unique identifier for the Geo node. Must match %{geoNodeName} if it i
msgstr ""
msgid "The update action will time out after %{number_of_minutes} minutes. For big repositories, use a clone/push combination."
-msgstr ""
+msgstr "アップデート㯠%{number_of_minutes} 分後ã«ã‚¿ã‚¤ãƒ ã‚¢ã‚¦ãƒˆã—ã¾ã™ã€‚大ããªãƒªãƒã‚¸ãƒˆãƒªã«ã¯ã€clone 㨠push を組ã¿åˆã‚ã›ã¦ä½¿ã£ã¦ãã ã•ã„。"
msgid "The uploaded file is not a valid Google Takeout archive."
-msgstr ""
+msgstr "アップロードã•ã‚ŒãŸãƒ•ã‚¡ã‚¤ãƒ«ã¯æœ‰åŠ¹ãª Google テイクアウトアーカイブã§ã¯ã‚ã‚Šã¾ã›ã‚“。"
msgid "The usage ping is disabled, and cannot be configured through this form."
-msgstr ""
+msgstr "ping ã®ä½¿ç”¨ã¯ç„¡åŠ¹ã«ãªã£ã¦ã„ã‚‹ãŸã‚ã€ã“ã®ãƒ•ã‚©ãƒ¼ãƒ ã§ã¯è¨­å®šã§ãã¾ã›ã‚“。"
msgid "The user is being deleted."
-msgstr ""
+msgstr "ユーザーを削除中ã§ã™ã€‚"
msgid "The user map has been saved. Continue by selecting the projects you want to import."
msgstr ""
@@ -20380,10 +20847,10 @@ msgid "There are no GPG keys with access to your account."
msgstr ""
msgid "There are no SSH keys associated with this account."
-msgstr ""
+msgstr "ã“ã®ã‚¢ã‚«ã‚¦ãƒ³ãƒˆã«ç´ä»˜ã„ã¦ã„ã‚‹SSH éµã¯ã‚ã‚Šã¾ã›ã‚“"
msgid "There are no SSH keys with access to your account."
-msgstr ""
+msgstr "ã‚ãªãŸã®ã‚¢ã‚«ã‚¦ãƒ³ãƒˆã«ã‚¢ã‚¯ã‚»ã‚¹ã™ã‚‹ãŸã‚ã®SSH éµãŒã‚ã‚Šã¾ã›ã‚“"
msgid "There are no archived projects yet"
msgstr "アーカイブã•ã‚ŒãŸãƒ—ロジェクトã¯ã‚ã‚Šã¾ã›ã‚“"
@@ -20404,7 +20871,7 @@ msgid "There are no closed merge requests"
msgstr "クローズã•ã‚ŒãŸãƒžãƒ¼ã‚¸ãƒªã‚¯ã‚¨ã‚¹ãƒˆã¯ã‚ã‚Šã¾ã›ã‚“"
msgid "There are no custom project templates set up for this GitLab instance. They are enabled from GitLab's Admin Area. Contact your GitLab instance administrator to setup custom project templates."
-msgstr ""
+msgstr "ã“ã® GitLab インスタンス用ã«è¨­å®šã•ã‚ŒãŸã‚«ã‚¹ã‚¿ãƒ ãƒ—ロジェクトテンプレートã¯ã‚ã‚Šã¾ã›ã‚“。カスタムプロジェクトテンプレート㯠GitLab ã®ç®¡ç†è€…エリアã§æœ‰åŠ¹ã«ã§ãã¾ã™ã€‚設定ã™ã‚‹ã«ã¯ã€ GitLab インスタンス管ç†è€…ã«é€£çµ¡ã—ã¦ãã ã•ã„。"
msgid "There are no issues to show"
msgstr "表示ã™ã‚‹èª²é¡ŒãŒã‚ã‚Šã¾ã›ã‚“"
@@ -20428,10 +20895,10 @@ msgid "There are no open requirements"
msgstr ""
msgid "There are no packages yet"
-msgstr ""
+msgstr "ã¾ã ãƒ‘ッケージãŒã‚ã‚Šã¾ã›ã‚“。"
msgid "There are no projects shared with this group yet"
-msgstr ""
+msgstr "ã“ã®ã‚°ãƒ«ãƒ¼ãƒ—ã¨å…±æœ‰ã—ã¦ã„るプロジェクトã¯ã‚ã‚Šã¾ã›ã‚“"
msgid "There are no variables yet."
msgstr ""
@@ -20514,11 +20981,14 @@ msgstr ""
msgid "There was an error getting the epic participants."
msgstr ""
+msgid "There was an error importing the Jira project."
+msgstr ""
+
msgid "There was an error loading users activity calendar."
msgstr "ユーザーã®ã‚¢ã‚¯ãƒ†ã‚£ãƒ“ティカレンダーã®èª­ã¿è¾¼ã¿ä¸­ã«ã‚¨ãƒ©ãƒ¼ãŒç™ºç”Ÿã—ã¾ã—ãŸã€‚"
msgid "There was an error removing the e-mail."
-msgstr ""
+msgstr "メールã®å‰Šé™¤ä¸­ã«ã‚¨ãƒ©ãƒ¼ãŒç™ºç”Ÿã—ã¾ã—ãŸã€‚"
msgid "There was an error removing your custom stage, please try again"
msgstr ""
@@ -20529,11 +20999,11 @@ msgstr ""
msgid "There was an error resetting user pipeline minutes."
msgstr ""
-msgid "There was an error saving this Geo Node"
+msgid "There was an error saving this Geo Node."
msgstr ""
msgid "There was an error saving your changes."
-msgstr ""
+msgstr "ã‚ãªãŸã®å¤‰æ›´ã®ä¿å­˜ä¸­ã«ã‚¨ãƒ©ãƒ¼ãŒç™ºç”Ÿã—ã¾ã—ãŸã€‚"
msgid "There was an error saving your notification settings."
msgstr "通知設定をä¿å­˜ä¸­ã«ã‚¨ãƒ©ãƒ¼ãŒç™ºç”Ÿã—ã¾ã—ãŸã€‚"
@@ -20581,10 +21051,10 @@ msgid "There was an error while fetching value stream analytics recent activity
msgstr ""
msgid "There was an error with the reCAPTCHA. Please solve the reCAPTCHA again."
-msgstr ""
+msgstr "reCAPTCHA ã«ã‚¨ãƒ©ãƒ¼ãŒã‚ã‚Šã¾ã—ãŸã€‚ reCAPTCHA ã‚’ã‚‚ã†ä¸€åº¦å®Ÿæ–½ã—ã¦ãã ã•ã„。"
msgid "These existing issues have a similar title. It might be better to comment there instead of creating another similar issue."
-msgstr ""
+msgstr "既存ã®èª²é¡Œã«åŒæ§˜ã®ã‚¿ã‚¤ãƒˆãƒ«ãŒã‚ã‚Šã¾ã™ã€‚別ã®ä¼¼ãŸã‚ˆã†ãªèª²é¡Œã‚’作æˆã™ã‚‹ã‚ˆã‚Šã€ãã“ã«ã‚³ãƒ¡ãƒ³ãƒˆã™ã‚‹æ–¹ãŒè‰¯ã„ã§ã™ã€‚"
msgid "These variables are configured in the parent group settings, and will be active in the current project in addition to the project variables."
msgstr ""
@@ -20592,17 +21062,20 @@ msgstr ""
msgid "They can be managed using the %{link}."
msgstr "%{link}を使用ã—ã¦ãれらを管ç†ã§ãã¾ã™ã€‚"
-msgid "Third party offers"
+msgid "Third Party Advisory Link"
msgstr ""
+msgid "Third party offers"
+msgstr "サードパーティã®ã‚¢ãƒ—リã®ç”³ã—出"
+
msgid "This %{issuableDisplayName} is locked. Only project members can comment."
msgstr ""
msgid "This %{issuable} is locked. Only <strong>project members</strong> can comment."
-msgstr ""
+msgstr "ã“ã® %{issuable} ã¯ãƒ­ãƒƒã‚¯ã•ã‚Œã¦ã„ã¾ã™ã€‚<strong>プロジェクトメンãƒãƒ¼</strong> ã ã‘ãŒã‚³ãƒ¡ãƒ³ãƒˆã§ãã¾ã™ã€‚"
msgid "This %{viewer} could not be displayed because %{reason}. You can %{options} instead."
-msgstr ""
+msgstr "%{reason} ã®ãŸã‚ã€ã“ã® %{viewer} ã¯è¡¨ç¤ºã§ãã¾ã›ã‚“ã§ã—ãŸã€‚代ã‚ã‚Šã« %{options} ãŒä½¿ç”¨ã§ãã¾ã™ã€‚"
msgid "This GitLab instance does not provide any shared Runners yet. Instance administrators can register shared Runners in the admin area."
msgstr "ã“ã® GitLab インスタンスã¯ã¾ã å…±æœ‰ Runner ã‚’æä¾›ã—ã¦ã„ã¾ã›ã‚“。インスタンス管ç†è€…ã¯ç®¡ç†è€…エリアã§å…±æœ‰ Runner を登録ã§ãã¾ã™ã€‚"
@@ -20617,10 +21090,10 @@ msgid "This URL is already used for another link; duplicate URLs are not allowed
msgstr ""
msgid "This action can lead to data loss. To prevent accidental actions we ask you to confirm your intention."
-msgstr ""
+msgstr "ã“ã®å‹•ä½œã«ã‚ˆã£ã¦ãƒ‡ãƒ¼ã‚¿ãŒå¤±ã‚れるå¯èƒ½æ€§ãŒã‚ã‚Šã¾ã™ã€‚çªç™ºçš„ãªäº‹æ…‹ã‚’防ããŸã‚ã«ã€ä¸€åº¦æ“作ã®æ„図を確èªã—ã¦ãã ã•ã„。ãŠé¡˜ã„ã—ã¾ã™ã€‚"
msgid "This also resolves the discussion"
-msgstr ""
+msgstr "ã“れもディスカッションを解決ã—ã¾ã™"
msgid "This application was created by %{link_to_owner}."
msgstr "ã“ã®ã‚¢ãƒ—リケーション㯠%{link_to_owner} ã«ã‚ˆã£ã¦ä½œæˆã•ã‚Œã¾ã—ãŸã€‚"
@@ -20628,9 +21101,12 @@ msgstr "ã“ã®ã‚¢ãƒ—リケーション㯠%{link_to_owner} ã«ã‚ˆã£ã¦ä½œæˆã•
msgid "This application will be able to:"
msgstr ""
-msgid "This block is self-referential"
+msgid "This attachment has been truncated to avoid exceeding the maximum allowed attachment size of 15MB. %{written_count} of %{issues_count} issues have been included. Consider re-exporting with a narrower selection of issues."
msgstr ""
+msgid "This block is self-referential"
+msgstr "ã“ã®ãƒ–ロックã¯è‡ªå·±å‚ç…§ã—ã¦ã„ã¾ã™ã€‚"
+
msgid "This board's scope is reduced"
msgstr ""
@@ -20638,55 +21114,55 @@ msgid "This branch has changed since you started editing. Would you like to crea
msgstr "編集を開始ã—ã¦ã‹ã‚‰ãƒ–ランãƒãŒæ›´æ–°ã•ã‚Œã¦ã„ã¾ã™ã€‚æ–°ã—ã„ブランãƒã‚’作æˆã—ã¾ã™ã‹ï¼Ÿ"
msgid "This chart could not be displayed"
-msgstr ""
+msgstr "ã“ã®ãƒãƒ£ãƒ¼ãƒˆã¯è¡¨ç¤ºã§ãã¾ã›ã‚“"
msgid "This comment has changed since you started editing, please review the %{startTag}updated comment%{endTag} to ensure information is not lost."
msgstr ""
msgid "This commit is part of merge request %{link_to_merge_request}. Comments created here will be created in the context of that merge request."
-msgstr ""
+msgstr "ã“ã®ã‚³ãƒŸãƒƒãƒˆã¯ãƒžãƒ¼ã‚¸ãƒªã‚¯ã‚¨ã‚¹ãƒˆ %{link_to_merge_request} ã®ä¸€éƒ¨ã§ã™ã€‚ã“ã®ã‚³ãƒ¡ãƒ³ãƒˆã¯ã€ãã®ãƒžãƒ¼ã‚¸ãƒªã‚¯ã‚¨ã‚¹ãƒˆã®ã‚³ãƒ³ãƒ†ã‚­ã‚¹ãƒˆã§ä½œæˆã—ã¾ã™ã€‚"
msgid "This commit was signed with a <strong>verified</strong> signature and the committer email is verified to belong to the same user."
-msgstr ""
+msgstr "ã“ã®ã‚³ãƒŸãƒƒãƒˆã¯ <strong>検証済ã¿</strong> ã®ç½²åã§ã‚µã‚¤ãƒ³ã•ã‚Œã¦ãŠã‚Šã€ã“ã®ã‚³ãƒŸãƒƒã‚¿ãƒ¼ã®ãƒ¡ãƒ¼ãƒ«ã¯åŒã˜ãƒ¦ãƒ¼ã‚¶ãƒ¼ã®ã‚‚ã®ã§ã‚ã‚‹ã“ã¨ãŒæ¤œè¨¼ã•ã‚Œã¦ã„ã¾ã™ã€‚"
msgid "This commit was signed with a different user's verified signature."
-msgstr ""
+msgstr "ã“ã®ã‚³ãƒŸãƒƒãƒˆã¯åˆ¥ã®ãƒ¦ãƒ¼ã‚¶ãƒ¼ã®æ¤œè¨¼æ¸ˆã¿ç½²åã§ã‚µã‚¤ãƒ³ã•ã‚Œã¦ã„ã¾ã™ã€‚"
msgid "This commit was signed with a verified signature, but the committer email is <strong>not verified</strong> to belong to the same user."
msgstr "ã“ã®ã‚³ãƒŸãƒƒãƒˆã¯æ¤œè¨¼æ¸ˆã¿ã®ç½²åã§ã‚µã‚¤ãƒ³ã•ã‚Œã¦ã„ã¾ã™ã€‚ã—ã‹ã—コミッターã®ãƒ¡ãƒ¼ãƒ«ã¯ã€ åŒã˜ãƒ¦ãƒ¼ã‚¶ãƒ¼ã®ã‚‚ã®ã¨<strong>検証ã•ã‚Œã¦ã„ã¾ã›ã‚“</strong>。"
msgid "This commit was signed with an <strong>unverified</strong> signature."
-msgstr ""
+msgstr "ã“ã®ã‚³ãƒŸãƒƒãƒˆã¯<strong>検証ã•ã‚Œã¦ã„ãªã„</strong> ç½²åã§ã‚µã‚¤ãƒ³ã•ã‚Œã¦ã„ã¾ã™ã€‚"
msgid "This date is after the due date, so this epic won't appear in the roadmap."
-msgstr ""
+msgstr "ã“ã®æ—¥ä»˜ã¯æœŸæ—¥ã‚ˆã‚Šå¾Œã§ã™ã€‚ãã®ãŸã‚ã“ã®ã‚¨ãƒ”ックをロードマップã«è¡¨ç¤ºã—ã¾ã›ã‚“。"
msgid "This date is before the start date, so this epic won't appear in the roadmap."
-msgstr ""
+msgstr "ã“ã®æ—¥ä»˜ã¯é–‹å§‹æ—¥ã‚ˆã‚Šå‰ã§ã™ã€‚ãã®ãŸã‚ã“ã®ã‚¨ãƒ”ックをロードマップã«è¡¨ç¤ºã—ã¾ã›ã‚“。"
msgid "This device has already been registered with us."
msgstr "ã“ã®ãƒ‡ãƒã‚¤ã‚¹ã¯æ—¢ã«ç™»éŒ²ã•ã‚Œã¦ã„ã¾ã™ã€‚"
msgid "This device has not been registered with us."
-msgstr ""
+msgstr "ã“ã®ãƒ‡ãƒã‚¤ã‚¹ã¯æœªã ç™»éŒ²ã•ã‚Œã¦ã„ã¾ã›ã‚“。"
msgid "This diff is collapsed."
msgstr "ã“ã®å·®åˆ†ã¯æŠ˜ã‚ŠãŸãŸã¾ã‚Œã¦ã„ã¾ã™ã€‚"
msgid "This diff was suppressed by a .gitattributes entry."
-msgstr ""
+msgstr "ã“ã®å·®åˆ†ã¯ .gitattributesã®ã‚¨ãƒ³ãƒˆãƒªã«ã‚ˆã£ã¦æŠ‘制ã•ã‚Œã¾ã—ãŸã€‚"
msgid "This directory"
msgstr "ディレクトリ"
msgid "This domain is not verified. You will need to verify ownership before access is enabled."
-msgstr ""
+msgstr "ã“ã®ãƒ‰ãƒ¡ã‚¤ãƒ³ã¯æ¤œè¨¼ã•ã‚Œã¦ã„ã¾ã›ã‚“。ã‚ãªãŸã¯ã‚¢ã‚¯ã‚»ã‚¹ã‚’有効ã«ã™ã‚‹å‰ã«æ‰€æœ‰æ¨©ã‚’確èªã™ã‚‹å¿…è¦ãŒã‚ã‚Šã¾ã™ã€‚"
msgid "This endpoint has been requested too many times. Try again later."
msgstr ""
msgid "This environment has no deployments yet."
-msgstr ""
+msgstr "ã“ã®ç’°å¢ƒã¯ã¾ã ãƒ‡ãƒ—ロイã—ã¦ã„ã¾ã›ã‚“。"
msgid "This environment is being deployed"
msgstr ""
@@ -20704,7 +21180,7 @@ msgid "This feature requires local storage to be enabled"
msgstr ""
msgid "This field is required."
-msgstr ""
+msgstr "ã“ã®ãƒ•ã‚£ãƒ¼ãƒ«ãƒ‰ã¯å¿…é ˆã§ã™ã€‚"
msgid "This group"
msgstr "ã“ã®ã‚°ãƒ«ãƒ¼ãƒ—"
@@ -20746,7 +21222,7 @@ msgid "This is a primary node"
msgstr ""
msgid "This is a security log of important events involving your account."
-msgstr ""
+msgstr "ã“ã‚Œã¯ã‚ãªãŸã®ã‚¢ã‚«ã‚¦ãƒ³ãƒˆã«é–¢ã‚ã‚‹é‡è¦ãªã‚¤ãƒ™ãƒ³ãƒˆã®ã‚»ã‚­ãƒ¥ãƒªãƒ†ã‚£ãƒ­ã‚°ã§ã™ã€‚"
msgid "This is the author's first Merge Request to this project."
msgstr "ã“ã®ãƒ—ロジェクトã«å¯¾ã™ã‚‹ã€ä½œæˆè€…ã®æœ€åˆã®ãƒžãƒ¼ã‚¸ãƒªã‚¯ã‚¨ã‚¹ãƒˆã§ã™ã€‚"
@@ -20769,6 +21245,12 @@ msgstr ""
msgid "This issue is confidential"
msgstr "ã“ã®èª²é¡Œã¯éžå…¬é–‹è¨­å®šã§ã™"
+msgid "This issue is currently blocked by the following issues: %{issues}."
+msgstr ""
+
+msgid "This issue is in a child epic of the filtered epic"
+msgstr ""
+
msgid "This issue is locked."
msgstr "ã“ã®èª²é¡Œã¯ãƒ­ãƒƒã‚¯ã•ã‚Œã¦ã„ã¾ã™ã€‚"
@@ -20845,13 +21327,13 @@ msgid "This job is in pending state and is waiting to be picked by a runner"
msgstr "ã“ã®ã‚¸ãƒ§ãƒ–ã¯ä¿ç•™ä¸­ã§ Runner ãŒå‹•ä½œã™ã‚‹ã®ã‚’å¾…ã£ã¦ã„ã¾ã™ã€‚"
msgid "This job is performing tasks that must complete before it can start"
-msgstr ""
+msgstr "ã“ã®ã‚¸ãƒ§ãƒ–ã¯ã€é–‹å§‹ã™ã‚‹å‰ã«å®Œäº†ã—ãªã‘ã‚Œã°ãªã‚‰ãªã„タスクを実行ã—ã¦ã„ã¾ã™"
msgid "This job is preparing to start"
-msgstr ""
+msgstr "ã“ã®ã‚¸ãƒ§ãƒ–ã¯å§‹ã‚る準備をã—ã¦ã„ã¾ã™"
msgid "This job is stuck because you don't have any active runners online with any of these tags assigned to them:"
-msgstr ""
+msgstr "ã“れらã®ã‚¿ã‚°ãŒå‰²ã‚Šå½“ã¦ã‚‰ã‚Œã¦ã„るアクティブ㪠Runner ãŒã‚ªãƒ³ãƒ©ã‚¤ãƒ³ã«ãªã£ã¦ã„ãªã„ãŸã‚ã€ã“ã®ã‚¸ãƒ§ãƒ–ã¯åœæ­¢ã—ã¦ã„ã¾ã™ã€‚"
msgid "This job is stuck because you don't have any active runners that can run this job."
msgstr "ã“ã®ã‚¸ãƒ§ãƒ–を実行ã§ãるアクティブ㪠Runner ãŒãªã„ãŸã‚ã€ã“ã®ã‚¸ãƒ§ãƒ–ã¯åœæ­¢ã—ã¦ã„ã¾ã™ã€‚"
@@ -20863,7 +21345,7 @@ msgid "This job requires a manual action"
msgstr "ã“ã®ã‚¸ãƒ§ãƒ–ã¯æ‰‹å‹•ã«ã‚ˆã‚‹å®Ÿè¡Œã‚’求ã‚ã¦ã„ã¾ã™"
msgid "This job requires manual intervention to start. Before starting this job, you can add variables below for last-minute configuration changes."
-msgstr ""
+msgstr "ã“ã®ã‚¸ãƒ§ãƒ–を開始ã™ã‚‹ã«ã¯ã€æ‰‹å‹•ã«ã‚ˆã‚‹ä»‹å…¥ãŒå¿…è¦ã§ã™ã€‚ ã“ã®ã‚¸ãƒ§ãƒ–を開始ã™ã‚‹å‰ã«ã€ç›´å‰ã®æ§‹æˆå¤‰æ›´ã®ãŸã‚ã«ä»¥ä¸‹ã®å¤‰æ•°ã‚’追加ã§ãã¾ã™ã€‚"
msgid "This job will automatically run after its timer finishes. Often they are used for incremental roll-out deploys to production environments. When unscheduled it converts into a manual action."
msgstr "ã“ã®ã‚¸ãƒ§ãƒ–ã¯ã‚¿ã‚¤ãƒžãƒ¼çµ‚了後ã«è‡ªå‹•çš„ã«å®Ÿè¡Œã•ã‚Œã¾ã™ã€‚多ãã®å ´åˆæœ¬ç•ªç’°å¢ƒã¸ã®æ®µéšŽçš„ãªãƒ­ãƒ¼ãƒ«ã‚¢ã‚¦ãƒˆãƒ‡ãƒ—ロイã«ä½¿ç”¨ã•ã‚Œã¾ã™ã€‚スケジュールã•ã‚Œã¦ã„ãªã„ã¨ãã¯æ‰‹å‹•ã‚¢ã‚¯ã‚·ãƒ§ãƒ³ã«å¤‰æ›ã•ã‚Œã¾ã™ã€‚"
@@ -20881,7 +21363,7 @@ msgid "This merge request is locked."
msgstr "ã“ã®ãƒžãƒ¼ã‚¸ãƒªã‚¯ã‚¨ã‚¹ãƒˆã¯ãƒ­ãƒƒã‚¯ã•ã‚Œã¦ã„ã¾ã™ã€‚"
msgid "This namespace has already been taken! Please choose another one."
-msgstr ""
+msgstr "ã“ã®åå‰ç©ºé–“ã¯æ—¢ã«ä½¿ç”¨ã•ã‚Œã¦ã„ã¾ã™ã€‚別ã®åå‰ã«ã—ã¦ãã ã•ã„。"
msgid "This only applies to repository indexing operations."
msgstr ""
@@ -20896,10 +21378,10 @@ msgid "This page will be removed in a future release."
msgstr "ã“ã®ãƒšãƒ¼ã‚¸ã¯ã€å°†æ¥ã®ãƒªãƒªãƒ¼ã‚¹ã§å‰Šé™¤ã•ã‚Œã‚‹äºˆå®šã§ã™ã€‚"
msgid "This pipeline makes use of a predefined CI/CD configuration enabled by %{strongStart}Auto DevOps.%{strongEnd}"
-msgstr ""
+msgstr "ã“ã®ãƒ‘イプラインã¯ã€ %{strongStart}Auto DevOps%{strongEnd}ã«ã‚ˆã£ã¦æœ‰åŠ¹åŒ–ã•ã‚ŒãŸå®šç¾©æ¸ˆã¿ã®CI/CD構æˆã‚’利用ã—ã¾ã™ã€‚"
msgid "This pipeline makes use of a predefined CI/CD configuration enabled by <b>Auto DevOps.</b>"
-msgstr ""
+msgstr "ã“ã®ãƒ‘イプラインã¯ã€<b>Auto DevOps</b>ã«ã‚ˆã£ã¦æœ‰åŠ¹åŒ–ã•ã‚ŒãŸå®šç¾©æ¸ˆã¿ã®CI/CD構æˆã‚’利用ã—ã¾ã™ã€‚"
msgid "This pipeline triggered a child pipeline"
msgstr ""
@@ -20914,13 +21396,13 @@ msgid "This project does not belong to a group and can therefore not make use of
msgstr "ã“ã®ãƒ—ロジェクトã¯ã‚°ãƒ«ãƒ¼ãƒ—ã«æ‰€å±žã—ã¦ã„ãªã„ãŸã‚ã€ã‚°ãƒ«ãƒ¼ãƒ— Runner を利用ã§ãã¾ã›ã‚“。"
msgid "This project does not have a wiki homepage yet"
-msgstr ""
+msgstr "ã“ã®ãƒ—ロジェクトã¯ã¾ã Wikiã®ãƒ›ãƒ¼ãƒ ãƒšãƒ¼ã‚¸ãŒã‚ã‚Šã¾ã›ã‚“。"
msgid "This project does not have billing enabled. To create a cluster, <a href=%{linkToBilling} target=\"_blank\" rel=\"noopener noreferrer\">enable billing <i class=\"fa fa-external-link\" aria-hidden=\"true\"></i></a> and try again."
-msgstr ""
+msgstr "ã“ã®ãƒ—ロジェクトã§ã¯èª²é‡‘ãŒæœ‰åŠ¹ã«ãªã£ã¦ã„ã¾ã›ã‚“。クラスターを作æˆã™ã‚‹ã«ã¯ã€<a href=%{linkToBilling} target=\"_blank\" rel=\"noopener noreferrer\"> 課金を有効<i class=\"fa fa-external-link\" aria-hidden=\"true\"></i></a> ã«ã—ã¦å†åº¦ãŠè©¦ã—ãã ã•ã„。"
msgid "This project is archived and cannot be commented on."
-msgstr ""
+msgstr "ã“ã®ãƒ—ロジェクトã¯ã‚¢ãƒ¼ã‚«ã‚¤ãƒ–ã•ã‚Œã¦ãŠã‚Šã€ã‚³ãƒ¡ãƒ³ãƒˆã™ã‚‹ã“ã¨ã¯ã§ãã¾ã›ã‚“。"
msgid "This project path either does not exist or you do not have access."
msgstr ""
@@ -20938,22 +21420,22 @@ msgid "This repository is currently empty. A new Auto DevOps pipeline will be cr
msgstr ""
msgid "This runner will only run on pipelines triggered on protected branches"
-msgstr ""
+msgstr "ã“ã® Runner ã¯ä¿è­·ã•ã‚ŒãŸãƒ–ランãƒä¸Šã§å¼•ãèµ·ã•ã‚ŒãŸ パイプラインã§ã®ã¿å®Ÿè¡Œã§ãã¾ã™ã€‚"
msgid "This setting can be overridden in each project."
msgstr "ã“ã®è¨­å®šã¯ã€å„プロジェクトã§ä¸Šæ›¸ãã§ãã¾ã™ã€‚"
msgid "This setting will update the hostname that is used to generate private commit emails. %{learn_more}"
-msgstr ""
+msgstr "ã“ã®è¨­å®šã¯ãƒ—ライベートã®ã‚³ãƒŸãƒƒãƒˆãƒ¡ãƒ¼ãƒ«ã‚’生æˆã™ã‚‹éš›ã«ä½¿ç”¨ã™ã‚‹ãƒ›ã‚¹ãƒˆåã‚’æ›´æ–°ã—ã¾ã™ã€‚ %{learn_more}"
msgid "This subscription is for"
msgstr ""
msgid "This timeout will take precedence when lower than project-defined timeout and accepts a human readable time input language like \"1 hour\". Values without specification represent seconds."
-msgstr ""
+msgstr "ã“ã®ã‚¿ã‚¤ãƒ ã‚¢ã‚¦ãƒˆã¯ã€ãƒ—ロジェクトã§å®šç¾©ã•ã‚ŒãŸã‚¿ã‚¤ãƒ ã‚¢ã‚¦ãƒˆã‚ˆã‚Šã‚‚å°ã•ã„å ´åˆã«å„ªå…ˆã•ã‚Œã€ã€Œ1時間ã€ãªã©ã®å…¥åŠ›è¨€èªžã«ã‚ˆã‚‹äººãŒèª­ã¿ã‚„ã™ã„相対時間形å¼ã‚’å—ã‘入れã¾ã™ã€‚値ã®æŒ‡å®šãŒç„¡ã„å ´åˆã¯ç§’を表ã—ã¾ã™ã€‚"
msgid "This user cannot be unlocked manually from GitLab"
-msgstr ""
+msgstr "ã“ã®ãƒ¦ãƒ¼ã‚¶ãƒ¼ã¯ GitLab ã‹ã‚‰ãƒžãƒ‹ãƒ¥ã‚¢ãƒ«æ“作ã§ãƒ­ãƒƒã‚¯ã‚’解除ã§ãã¾ã›ã‚“"
msgid "This user has no active %{type} Tokens."
msgstr ""
@@ -20962,12 +21444,15 @@ msgid "This user has no identities"
msgstr "ã“ã®ãƒ¦ãƒ¼ã‚¶ãƒ¼ã«ã¯ ID ãŒã‚ã‚Šã¾ã›ã‚“"
msgid "This user will be the author of all events in the activity feed that are the result of an update, like new branches being created or new commits being pushed to existing branches."
-msgstr ""
+msgstr "ã“ã®ãƒ¦ãƒ¼ã‚¶ãƒ¼ã¯ã€æ–°ã—ã作æˆã•ã‚ŒãŸãƒ–ランãƒã‚„既存ã®ãƒ–ランãƒã¸ã®æ–°è¦ã‚³ãƒŸãƒƒãƒˆãªã©ã®ã‚ˆã†ã«ã€æ›´æ–°ã®çµæžœã§ã‚るアクティビティーフィード内ã®ã™ã¹ã¦ã®ã‚¤ãƒ™ãƒ³ãƒˆã®ä½œæˆè€…ã«ãªã‚Šã¾ã™ã€‚"
msgid "This user will be the author of all events in the activity feed that are the result of an update, like new branches being created or new commits being pushed to existing branches. Upon creation or when reassigning you can only assign yourself to be the mirror user."
+msgstr "ã“ã®ãƒ¦ãƒ¼ã‚¶ãƒ¼ã¯ã€æ–°ã—ã作æˆã•ã‚ŒãŸãƒ–ランãƒã‚„既存ã®ãƒ–ランãƒã¸ã®æ–°è¦ã‚³ãƒŸãƒƒãƒˆãªã©ã®ã‚ˆã†ã«ã€æ›´æ–°ã®çµæžœã§ã‚るアクティビティーフィード内ã®ã™ã¹ã¦ã®ã‚¤ãƒ™ãƒ³ãƒˆã®ä½œæˆè€…ã«ãªã‚Šã¾ã™ã€‚作æˆæ™‚ã¾ãŸã¯å†ã³æ‹…当を割り当ã¦ãŸéš›ã«ã€ãã®ãƒŸãƒ©ãƒ¼ãƒ¦ãƒ¼ã‚¶ãƒ¼ã‚’ã‚ãªãŸã«å‰²ã‚Šå½“ã¦ã‚‹ã“ã¨ãŒã§ãã¾ã™ã€‚"
+
+msgid "This variable can not be masked."
msgstr ""
-msgid "This variable can not be masked"
+msgid "This variable does not match the expected pattern."
msgstr ""
msgid "This will help us personalize your onboarding experience."
@@ -20997,10 +21482,10 @@ msgstr ""
msgid "ThreatMonitoring|Application firewall not detected"
msgstr ""
-msgid "ThreatMonitoring|Container Network Policy"
+msgid "ThreatMonitoring|Container Network Policies are not installed or have been disabled. To view this data, ensure your Network Policies are installed and enabled for your cluster."
msgstr ""
-msgid "ThreatMonitoring|Container NetworkPolicies are not installed or has been disabled. To view this data, ensure you NetworkPolicies are installed and enabled for your cluster."
+msgid "ThreatMonitoring|Container Network Policy"
msgstr ""
msgid "ThreatMonitoring|Container NetworkPolicies not detected"
@@ -21033,14 +21518,14 @@ msgstr ""
msgid "ThreatMonitoring|Something went wrong, unable to fetch statistics"
msgstr ""
-msgid "ThreatMonitoring|The firewall is not installed or has been disabled. To view this data, ensure you firewall is installed and enabled for your cluster."
+msgid "ThreatMonitoring|The firewall is not installed or has been disabled. To view this data, ensure the web application firewall is installed and enabled for your cluster."
msgstr ""
msgid "ThreatMonitoring|The graph below is an overview of traffic coming to your application as tracked by the Web Application Firewall (WAF). View the docs for instructions on how to access the WAF logs to see what type of malicious traffic is trying to access your app. The docs link is also accessible by clicking the \"?\" icon next to the title below."
msgstr ""
msgid "ThreatMonitoring|Threat Monitoring"
-msgstr ""
+msgstr "è„…å¨ç›£è¦–"
msgid "ThreatMonitoring|Threat Monitoring help page link"
msgstr ""
@@ -21088,7 +21573,7 @@ msgid "Time between merge request creation and merge/close"
msgstr "マージリクエストãŒä½œæˆã•ã‚Œã¦ã‹ã‚‰ãƒžãƒ¼ã‚¸ã¾ãŸã¯ã‚¯ãƒ­ãƒ¼ã‚ºã•ã‚Œã‚‹ã¾ã§ã®æ™‚é–“"
msgid "Time estimate"
-msgstr ""
+msgstr "見ç©æ™‚é–“"
msgid "Time from first comment to last commit"
msgstr ""
@@ -21102,6 +21587,9 @@ msgstr ""
msgid "Time in seconds GitLab will wait for a response from the external service. When the service does not respond in time, access will be denied."
msgstr "外部サービスã‹ã‚‰ã®å¿œç­”時間(秒å˜ä½ï¼‰ã‚’設定ã—ã¾ã™ã€‚設定時間内ã«å¿œç­”ãŒç„¡ã„å ´åˆã€ã‚¢ã‚¯ã‚»ã‚¹ãŒæ‹’å¦ã•ã‚Œã¾ã™ã€‚"
+msgid "Time of import: %{importTime}"
+msgstr ""
+
msgid "Time remaining"
msgstr "残り時間"
@@ -21127,13 +21615,13 @@ msgid "TimeTracking|%{startTag}Spent: %{endTag}%{timeSpentHumanReadable}"
msgstr ""
msgid "TimeTracking|Estimated:"
-msgstr ""
+msgstr "見ç©ã‚‚り時間:"
msgid "TimeTracking|Over by %{timeRemainingHumanReadable}"
msgstr ""
msgid "TimeTracking|Spent"
-msgstr ""
+msgstr "経éŽæ™‚é–“"
msgid "TimeTracking|Time remaining: %{timeRemainingHumanReadable}"
msgstr ""
@@ -21284,8 +21772,8 @@ msgstr "タイトル"
msgid "Title:"
msgstr "タイトル:"
-msgid "Titles and Filenames"
-msgstr "タイトルã¨ãƒ•ã‚¡ã‚¤ãƒ«å"
+msgid "Titles and Descriptions"
+msgstr ""
msgid "To"
msgstr ""
@@ -21351,7 +21839,7 @@ msgid "To keep this project going, create a new merge request"
msgstr "ã“ã®ãƒ—ロジェクトを継続ã™ã‚‹ã«ã¯ã€æ–°ã—ã„マージリクエストを作æˆã—ã¦ãã ã•ã„"
msgid "To link Sentry to GitLab, enter your Sentry URL and Auth Token."
-msgstr ""
+msgstr "Sentryã‚’GitLabã«ãƒªãƒ³ã‚¯ã™ã‚‹ãŸã‚ã€Sentryã®URLã¨èªè¨¼ãƒˆãƒ¼ã‚¯ãƒ³ã‚’入力ã—ã¾ã™ã€‚"
msgid "To move or copy an entire GitLab project from another GitLab installation to this one, navigate to the original project's settings page, generate an export file, and upload it here."
msgstr "GitLab プロジェクト全体を別㮠GitLab インストールã‹ã‚‰ã“ã®ãƒ—ロジェクトã«ç§»å‹•ã¾ãŸã¯ã‚³ãƒ”ーã™ã‚‹ã«ã¯ã€å…ƒã®ãƒ—ロジェクトã®è¨­å®šãƒšãƒ¼ã‚¸ã«ç§»å‹•ã—ã€ã‚¨ã‚¯ã‚¹ãƒãƒ¼ãƒˆãƒ•ã‚¡ã‚¤ãƒ«ã‚’生æˆã—ã¦ã“ã“ã«ã‚¢ãƒƒãƒ—ロードã—ã¾ã™ã€‚"
@@ -21399,22 +21887,22 @@ msgid "To start serving your jobs you can add Runners to your group"
msgstr "ジョブを開始ã™ã‚‹ãŸã‚ã€ã‚°ãƒ«ãƒ¼ãƒ—ã« Runner を追加ã§ãã¾ã™"
msgid "To start serving your jobs you can either add specific Runners to your project or use shared Runners"
-msgstr ""
+msgstr "ジョブを開始ã™ã‚‹ãŸã‚ã€ãƒ—ロジェクトã«ç‰¹å®šã® Runner を追加ã—ã¦ãã ã•ã„。ã¾ãŸã¯å…±æœ‰ Runner を利用ã—ã¦ãã ã•ã„。"
msgid "To this GitLab instance"
msgstr "ã“ã® GitLab インスタンスã«"
msgid "To view the roadmap, add a start or due date to one of your epics in this group or its subgroups. In the months view, only epics in the past month, current month, and next 5 months are shown."
-msgstr ""
+msgstr "ロードマップを表示ã™ã‚‹ã«ã¯ã€äºˆå®šé–‹å§‹æ—¥ã‹äºˆå®šçµ‚了日をã“ã®ã‚°ãƒ«ãƒ¼ãƒ—ã‹å­ã‚°ãƒ«ãƒ¼ãƒ—ã®ã‚¨ãƒ”ックã®1ã¤ã«è¿½åŠ ã—ã¦ãã ã•ã„。月表示ã§ã¯ã€å‰æœˆã€å½“月ã€ã‚‚ã—ãã¯5ヶ月先ã®ã‚¨ãƒ”ックã®ã¿ãŒè¡¨ç¤ºã•ã‚Œã¾ã™ã€‚"
msgid "To widen your search, change or remove filters above"
-msgstr ""
+msgstr "検索範囲を広ã’ã‚‹ã«ã¯ã€ä¸Šã®ãƒ•ã‚£ãƒ«ã‚¿ãƒ¼ã‚’変更ã¾ãŸã¯å‰Šé™¤ã—ã¾ã™"
msgid "To widen your search, change or remove filters."
msgstr "検索範囲を広ã’ã‚‹ã«ã¯ã€ãƒ•ã‚£ãƒ«ã‚¿ãƒ¼ã‚’変更ã¾ãŸã¯å‰Šé™¤ã—ã¾ã™ã€‚"
msgid "To-Do List"
-msgstr ""
+msgstr "To-Do リスト"
msgid "To-do item successfully marked as done."
msgstr ""
@@ -21432,7 +21920,7 @@ msgid "Toggle all threads"
msgstr ""
msgid "Toggle backtrace"
-msgstr ""
+msgstr "ãƒãƒƒã‚¯ãƒˆãƒ¬ãƒ¼ã‚¹ã‚’切り替ãˆ"
msgid "Toggle collapse"
msgstr ""
@@ -21450,16 +21938,16 @@ msgid "Toggle emoji award"
msgstr "絵文字賞をå—賞"
msgid "Toggle navigation"
-msgstr ""
+msgstr "案内ã®åˆ‡ã‚Šæ›¿ãˆ"
msgid "Toggle project"
-msgstr ""
+msgstr "プロジェクト切り替ãˆ"
msgid "Toggle sidebar"
msgstr "サイドãƒãƒ¼ã‚’切り替ãˆ"
msgid "Toggle the Performance Bar"
-msgstr ""
+msgstr "パフォーマンスãƒãƒ¼ã®åˆ‡ã‚Šæ›¿ãˆ"
msgid "Toggle this dialog"
msgstr ""
@@ -21483,7 +21971,7 @@ msgid "Tomorrow"
msgstr "明日"
msgid "Too many changes to show."
-msgstr ""
+msgstr "変更ãŒå¤šã„ãŸã‚表示ã§ãã¾ã›ã‚“。"
msgid "Too many namespaces enabled. You will need to manage them via the console or the API."
msgstr ""
@@ -21491,11 +21979,11 @@ msgstr ""
msgid "Too many projects enabled. You will need to manage them via the console or the API."
msgstr ""
-msgid "Topics"
-msgstr "トピック"
+msgid "Topics (optional)"
+msgstr ""
msgid "Total"
-msgstr ""
+msgstr "åˆè¨ˆ"
msgid "Total Contributions"
msgstr "ç·è²¢çŒ®åº¦"
@@ -21525,7 +22013,7 @@ msgid "Trace"
msgstr ""
msgid "Tracing"
-msgstr ""
+msgstr "追跡"
msgid "Track activity with Contribution Analytics."
msgstr ""
@@ -21546,7 +22034,7 @@ msgid "Transfer ownership"
msgstr ""
msgid "Transfer project"
-msgstr ""
+msgstr "プロジェクトを転é€"
msgid "TransferGroup|Cannot update the path because there are projects under this group that contain Docker images in their Container Registry. Please remove the images from your projects first and try again."
msgstr ""
@@ -21561,16 +22049,16 @@ msgid "TransferGroup|Group is already a root group."
msgstr ""
msgid "TransferGroup|Group is already associated to the parent group."
-msgstr ""
+msgstr "Groupã¯ã™ã§ã«è¦ªã‚°ãƒ«ãƒ¼ãƒ—ã«é–¢é€£ä»˜ã‘ã§ãã¦ã„ã¾ã™ã€‚"
msgid "TransferGroup|The parent group already has a subgroup with the same path."
-msgstr ""
+msgstr "親グループã«ã¯ã€ã™ã§ã«åŒã˜ãƒ‘スをæŒã¤ã‚µãƒ–グループãŒã‚ã‚Šã¾ã™ã€‚"
msgid "TransferGroup|Transfer failed: %{error_message}"
-msgstr ""
+msgstr "転é€ã«å¤±æ•—: %{error_message}"
msgid "TransferGroup|You don't have enough permissions."
-msgstr ""
+msgstr "ã‚ãªãŸã«ã¯ã€å分ãªæ¨©é™ãŒã‚ã‚Šã¾ã›ã‚“。"
msgid "TransferProject|Cannot move project"
msgstr "プロジェクトを移動ã§ãã¾ã›ã‚“"
@@ -21588,10 +22076,10 @@ msgid "TransferProject|Root namespace can't be updated if project has NPM packag
msgstr ""
msgid "TransferProject|Transfer failed, please contact an admin."
-msgstr ""
+msgstr "転é€ã«å¤±æ•—ã—ã¾ã—ãŸã€ç®¡ç†è€…ã«é€£çµ¡ã—ã¦ãã ã•ã„。"
msgid "Tree view"
-msgstr ""
+msgstr "ツリービュー"
msgid "Trending"
msgstr "トレンド分æž"
@@ -21618,7 +22106,7 @@ msgid "Trigger pipelines when branches or tags are updated from the upstream rep
msgstr ""
msgid "Trigger removed."
-msgstr ""
+msgstr "トリガーを除去ã—ã¾ã—ãŸã€‚"
msgid "Trigger this manual action"
msgstr "ã“ã®æ‰‹å‹•ã‚¢ã‚¯ã‚·ãƒ§ãƒ³ã‚’トリガーã™ã‚‹"
@@ -21642,7 +22130,7 @@ msgid "Triggers can force a specific branch or tag to get rebuilt with an API ca
msgstr ""
msgid "Troubleshoot and monitor your application with tracing"
-msgstr ""
+msgstr "追跡ã§ã‚¢ãƒ—リケーションをトラブルシューティングãŠã‚ˆã³ç›£è¦–ã™ã‚‹"
msgid "Try again"
msgstr "å†è©¦è¡Œ"
@@ -21653,9 +22141,12 @@ msgstr "å†è©¦è¡Œã—ã¾ã™ã‹ï¼Ÿ"
msgid "Try all GitLab has to offer for 30 days."
msgstr "30日間ã§GitLabãŒæä¾›ã™ã‚‹ã™ã¹ã¦ã®æ©Ÿèƒ½ã‚’試ã—ã¦ã¿ã¦ãã ã•ã„。"
-msgid "Try to fork again"
+msgid "Try changing or removing filters."
msgstr ""
+msgid "Try to fork again"
+msgstr "ã‚‚ã†ä¸€åº¦ãƒ•ã‚©ãƒ¼ã‚¯ã™ã‚‹"
+
msgid "Try using a different search term to find the file you are looking for."
msgstr ""
@@ -21678,7 +22169,7 @@ msgid "Twitter"
msgstr "Twitter"
msgid "Two-Factor Authentication"
-msgstr ""
+msgstr "2è¦ç´ èªè¨¼"
msgid "Two-Factor Authentication code"
msgstr ""
@@ -21746,6 +22237,9 @@ msgstr ""
msgid "Unable to connect to server: %{error}"
msgstr ""
+msgid "Unable to connect to the Jira instance. Please check your Jira integration configuration."
+msgstr ""
+
msgid "Unable to convert Kubernetes logs encoding to UTF-8"
msgstr ""
@@ -21771,28 +22265,28 @@ msgid "Unable to load the merge request widget. Try reloading the page."
msgstr ""
msgid "Unable to resolve"
-msgstr ""
+msgstr "解決ä¸å¯"
msgid "Unable to save your changes. Please try again."
msgstr ""
msgid "Unable to schedule a pipeline to run immediately"
-msgstr ""
+msgstr "パイプラインを今ã™ã実行ã™ã‚‹ã‚ˆã†ã«ã‚¹ã‚±ã‚¸ãƒ¥ãƒ¼ãƒ«ã§ãã¾ã›ã‚“"
msgid "Unable to sign you in to the group with SAML due to \"%{reason}\""
msgstr "\"%{reason}\"ã®ãŸã‚ SAML ã§ã‚°ãƒ«ãƒ¼ãƒ—ã«ã‚µã‚¤ãƒ³ã‚¤ãƒ³ã§ãã¾ã›ã‚“"
msgid "Unable to update label prioritization at this time"
-msgstr ""
+msgstr "ç¾æ™‚点ã§ã¯ãƒ©ãƒ™ãƒ«ã®å„ªå…ˆé †ä½ä»˜ã‘ã‚’æ›´æ–°ã§ãã¾ã›ã‚“"
msgid "Unable to update this epic at this time."
-msgstr ""
+msgstr "ç¾æ™‚点ã§ã¯ã“ã®ã‚¨ãƒ”ックを更新ã§ãã¾ã›ã‚“。"
msgid "Unable to update this issue at this time."
-msgstr ""
+msgstr "ç¾æ™‚点ã§ã¯ã“ã®å•é¡Œã‚’æ›´æ–°ã™ã‚‹ã“ã¨ãŒã§ãã¾ã›ã‚“。"
msgid "Unarchive project"
-msgstr ""
+msgstr "プロジェクトã®ã‚¢ãƒ¼ã‚«ã‚¤ãƒ–解除"
msgid "Unarchiving the project will restore people's ability to make changes to it. The repository can be committed to, and issues, comments, and other entities can be created. %{strong_start}Once active, this project shows up in the search and on the dashboard.%{strong_end}"
msgstr ""
@@ -21801,7 +22295,7 @@ msgid "Unblock"
msgstr "ブロック解除"
msgid "Undo"
-msgstr ""
+msgstr "å…ƒã«æˆ»ã™"
msgid "Undo Ignore"
msgstr ""
@@ -21834,13 +22328,13 @@ msgid "Unknown cache key"
msgstr ""
msgid "Unknown encryption strategy: %{encrypted_strategy}!"
-msgstr ""
+msgstr "æš—å·åŒ–戦略ãŒä¸æ˜Ž: %{encrypted_strategy}ï¼"
msgid "Unknown format"
-msgstr ""
+msgstr "ä¸æ˜Žãªå½¢å¼ã§ã™"
msgid "Unknown response text"
-msgstr ""
+msgstr "応答テキストãŒä¸æ˜Žã§ã™ã€‚"
msgid "Unlimited"
msgstr "無制é™"
@@ -21924,7 +22418,7 @@ msgid "Unsubscribe at project level"
msgstr "プロジェクトレベルã§è³¼èª­ã‚’解除"
msgid "Unsubscribe from %{type}"
-msgstr ""
+msgstr "%{type} ã‹ã‚‰è³¼èª­ã‚’解除"
msgid "Unsubscribed from this %{quick_action_target}."
msgstr ""
@@ -21945,7 +22439,7 @@ msgid "Up to date"
msgstr "最新"
msgid "Upcoming"
-msgstr ""
+msgstr "今後ã®äºˆå®š"
msgid "Upcoming Release"
msgstr ""
@@ -21975,13 +22469,13 @@ msgid "Update variable"
msgstr ""
msgid "Update your bookmarked URLs as filtered/sorted branches URL has been changed."
-msgstr ""
+msgstr "ã‚ãªãŸã®ãƒ–ックマークã•ã‚ŒãŸURLã‚’æ›´æ–°ã—ã¦ãã ã•ã„。フィルタã¾ãŸã¯ã‚½ãƒ¼ãƒˆã•ã‚ŒãŸãƒ–ランãƒã® URL ã¯å¤‰æ›´ã•ã‚Œã¦ã„ã¾ã™ã€‚"
msgid "Update your group name, description, avatar, and visibility."
-msgstr ""
+msgstr "グループåã€èª¬æ˜Žã€ã‚¢ãƒã‚¿ãƒ¼ã€ãŠã‚ˆã³å¯è¦–性を更新ã—ã¾ã™ã€‚"
msgid "Update your project name, topics, description and avatar."
-msgstr ""
+msgstr "プロジェクトåã€ãƒˆãƒ”ックã€èª¬æ˜Žã€ãã‚Œã«ã‚¢ãƒã‚¿ãƒ¼ã‚’æ›´æ–°ã—ã¦ãã ã•ã„。"
msgid "UpdateProject|Cannot rename project because it contains container registry tags!"
msgstr ""
@@ -22005,10 +22499,10 @@ msgid "UpdateRepositoryStorage|Timeout waiting for %{type} repository pushes"
msgstr ""
msgid "Updated"
-msgstr ""
+msgstr "æ›´æ–°ã—ã¾ã—ãŸ"
msgid "Updated %{updated_at} by %{updated_by}"
-msgstr ""
+msgstr "%{updated_by} ã«ã‚ˆã£ã¦%{updated_at} ã«æ›´æ–°"
msgid "Updated at"
msgstr ""
@@ -22023,7 +22517,7 @@ msgid "Upgrade plan to unlock Canary Deployments feature"
msgstr ""
msgid "Upgrade your plan"
-msgstr ""
+msgstr "プランをアップグレードã™ã‚‹"
msgid "Upgrade your plan to activate Advanced Global Search."
msgstr "高度ãªã‚°ãƒ­ãƒ¼ãƒãƒ«æ¤œç´¢ã‚’有効ã«ã™ã‚‹ãŸã‚ã«ãƒ—ランをアップグレードã—ã¾ã™ã€‚"
@@ -22092,7 +22586,7 @@ msgid "Usage"
msgstr "使ã„æ–¹"
msgid "Usage ping is not enabled"
-msgstr ""
+msgstr "ping ã®ä½¿ç”¨ãŒæœ‰åŠ¹ã«ãªã£ã¦ã„ã¾ã›ã‚“"
msgid "Usage quotas help link"
msgstr ""
@@ -22104,31 +22598,31 @@ msgid "UsageQuota|%{help_link_start}Shared runners%{help_link_end} are disabled,
msgstr ""
msgid "UsageQuota|Artifacts"
-msgstr ""
+msgstr "æˆæžœç‰©"
msgid "UsageQuota|Buy additional minutes"
-msgstr ""
+msgstr "追加分ã®è³¼å…¥"
msgid "UsageQuota|Current period usage"
-msgstr ""
+msgstr "ç¾åœ¨ã®ä½¿ç”¨çŠ¶æ³"
msgid "UsageQuota|LFS Storage"
-msgstr ""
+msgstr "LFSストレージ"
msgid "UsageQuota|Packages"
-msgstr ""
+msgstr "パッケージ"
msgid "UsageQuota|Pipelines"
msgstr "パイプライン"
msgid "UsageQuota|Repository"
-msgstr ""
+msgstr "リãƒã‚¸ãƒˆãƒª"
msgid "UsageQuota|Storage"
-msgstr ""
+msgstr "ストレージ"
msgid "UsageQuota|This namespace has no projects which use shared runners"
-msgstr ""
+msgstr "ã“ã®ãƒãƒ¼ãƒ ã‚¹ãƒšãƒ¼ã‚¹ã«ã¯ã€å…±æœ‰ãƒ©ãƒ³ãƒŠãƒ¼ã‚’使用ã™ã‚‹ãƒ—ロジェクトã¯ã‚ã‚Šã¾ã›ã‚“。"
msgid "UsageQuota|Unlimited"
msgstr "無制é™"
@@ -22149,10 +22643,10 @@ msgid "UsageQuota|Usage since"
msgstr ""
msgid "UsageQuota|Wiki"
-msgstr ""
+msgstr "Wiki"
msgid "Use %{code_start}::%{code_end} to create a %{link_start}scoped label set%{link_end} (eg. %{code_start}priority::1%{code_end})"
-msgstr ""
+msgstr "%{link_start} スコープã®ãƒ©ãƒ™ãƒ«ã‚»ãƒƒãƒˆ%{link_end} を作æˆã™ã‚‹ã«ã¯ã€ %{code_start}::%{code_end} を使用ã—ã¾ã™ï¼ˆä¾‹ï¼š %{code_start}priority::1%{code_end})。"
msgid "Use <code>%{native_redirect_uri}</code> for local tests"
msgstr "ローカルテスト㫠<code>%{native_redirect_uri}</code> を使用"
@@ -22164,7 +22658,7 @@ msgid "Use a hardware device to add the second factor of authentication."
msgstr ""
msgid "Use an one time password authenticator on your mobile device or computer to enable two-factor authentication (2FA)."
-msgstr ""
+msgstr "モãƒã‚¤ãƒ«ãƒ‡ãƒã‚¤ã‚¹ã¾ãŸã¯ã‚³ãƒ³ãƒ”ュータã§ãƒ¯ãƒ³ã‚¿ã‚¤ãƒ ãƒ‘スワードèªè¨¼ã‚’使用ã—ã¦ã€2è¦ç´ èªè¨¼ã‚’有効ã«ã—ã¾ã™ã€‚"
msgid "Use custom color #FF0000"
msgstr ""
@@ -22191,7 +22685,7 @@ msgid "Use your global notification setting"
msgstr "全体通知設定を利用"
msgid "Use your smart card to authenticate with the LDAP server."
-msgstr ""
+msgstr "スマートカードを使ã£ã¦ã€LDAPサーãƒãƒ¼ã«ã‚ˆã‚Šèªè¨¼ã—ã¾ã™ã€‚"
msgid "Used by members to sign in to your group in GitLab"
msgstr "メンãƒãƒ¼ãŒ GitLab ã®ã‚°ãƒ«ãƒ¼ãƒ—ã«ã‚µã‚¤ãƒ³ã‚¤ãƒ³ã™ã‚‹ãŸã‚ã«ä½¿ç”¨ã•ã‚Œã¾ã™"
@@ -22200,7 +22694,7 @@ msgid "Used programming language"
msgstr ""
msgid "Used to help configure your identity provider"
-msgstr ""
+msgstr "アイデンティティ プロãƒã‚¤ãƒ€ãƒ¼ã‚’設定ã™ã‚‹ã®ã«ä½¿ç”¨ã—ã¾ã™"
msgid "User"
msgstr ""
@@ -22212,13 +22706,13 @@ msgid "User %{username} was successfully removed."
msgstr ""
msgid "User Cohorts are only shown when the %{usage_ping_link_start}usage ping%{usage_ping_link_end} is enabled."
-msgstr ""
+msgstr "ユーザー世代㯠%{usage_ping_link_start} ping ã®ä½¿ç”¨ %{usage_ping_link_end} ãŒæœ‰åŠ¹ãªå ´åˆã«ã®ã¿è¡¨ç¤ºã•ã‚Œã¾ã™ã€‚"
msgid "User IDs"
msgstr ""
msgid "User OAuth applications"
-msgstr ""
+msgstr "ユーザー OAuth アプリケーション"
msgid "User Settings"
msgstr "ユーザー設定"
@@ -22227,13 +22721,13 @@ msgid "User and IP Rate Limits"
msgstr "ユーザーã¨IPレートã®åˆ¶é™"
msgid "User identity was successfully created."
-msgstr ""
+msgstr "ユーザーã®è­˜åˆ¥å­ãŒæ­£å¸¸ã«ä½œæˆã§ãã¾ã—ãŸã€‚"
msgid "User identity was successfully removed."
-msgstr ""
+msgstr "ユーザーã®è­˜åˆ¥å­ã‚’正常ã«å‰Šé™¤ã§ãã¾ã—ãŸã€‚"
msgid "User identity was successfully updated."
-msgstr ""
+msgstr "ユーザーã®è­˜åˆ¥å­ã‚’正常ã«æ›´æ–°ã§ãã¾ã—ãŸã€‚"
msgid "User is not allowed to resolve thread"
msgstr ""
@@ -22245,7 +22739,7 @@ msgid "User map"
msgstr "ユーザーマップ"
msgid "User pipeline minutes were successfully reset."
-msgstr ""
+msgstr "ユーザーã®ãƒ‘イプライン時間ãŒæ­£å¸¸ã«ãƒªã‚»ãƒƒãƒˆã•ã‚Œã¾ã—ãŸã€‚"
msgid "User restrictions"
msgstr ""
@@ -22254,7 +22748,7 @@ msgid "User settings"
msgstr "ユーザー設定"
msgid "User was successfully created."
-msgstr ""
+msgstr "ユーザーã¯æ­£å¸¸ã«ä½œæˆã•ã‚Œã¾ã—ãŸã€‚"
msgid "User was successfully removed from group and any subresources."
msgstr ""
@@ -22266,7 +22760,7 @@ msgid "User was successfully updated."
msgstr ""
msgid "UserOnboardingTour|%{activeTour}/%{totalTours}"
-msgstr ""
+msgstr "%{activeTour}/%{totalTours}"
msgid "UserOnboardingTour|%{completed}/%{total} steps completed"
msgstr ""
@@ -22302,7 +22796,7 @@ msgid "UserOnboardingTour|Create a project"
msgstr ""
msgid "UserOnboardingTour|Exit 'Learn GitLab'"
-msgstr ""
+msgstr "'GitLabã«ã¤ã„ã¦å­¦ã¶' を終了"
msgid "UserOnboardingTour|Got it"
msgstr ""
@@ -22332,13 +22826,13 @@ msgid "UserOnboardingTour|Here's an overview of branches in the %{emphasisStart}
msgstr ""
msgid "UserOnboardingTour|Invite colleagues"
-msgstr ""
+msgstr "招待ã™ã‚‹"
msgid "UserOnboardingTour|Issues are great for communicating and keeping track of progress in GitLab. These are all issues that are open in the %{emphasisStart}%{projectName}%{emphasisEnd}.%{lineBreak}%{lineBreak}You can help us improve GitLab by contributing work to issues that are labeled <span class=\"badge color-label accept-mr-label\">Accepting merge requests</span>.%{lineBreak}%{lineBreak}This list can be filtered by labels, milestones, assignees, authors... We'll show you how it looks when the list is filtered by a label."
msgstr ""
msgid "UserOnboardingTour|Learn GitLab"
-msgstr ""
+msgstr "GitLabã«ã¤ã„ã¦å­¦ã¶"
msgid "UserOnboardingTour|Let's take a closer look at a merge request. Click on the title of one."
msgstr ""
@@ -22362,16 +22856,16 @@ msgid "UserOnboardingTour|Open one of the issues by clicking on its title."
msgstr ""
msgid "UserOnboardingTour|Restart this step"
-msgstr ""
+msgstr "ã“ã®ã‚¹ãƒ†ãƒƒãƒ—ã‚’å†é–‹"
msgid "UserOnboardingTour|Skip this step"
-msgstr ""
+msgstr "ã“ã®ã‚¹ãƒ†ãƒƒãƒ—をスキップ"
msgid "UserOnboardingTour|Sweet! Your project was created and is ready to be used.%{lineBreak}%{lineBreak}You can start adding files to the repository or clone it. One last thing we want to show you is how to invite your colleagues to your new project."
msgstr ""
msgid "UserOnboardingTour|Take a look. Here's a nifty menu for quickly creating issues, merge requests, snippets, projects and groups. Click on it and select \"New project\" from the \"GitLab\" section to get started."
-msgstr ""
+msgstr "ã“れを見ã¦ãã ã•ã„。課題ã€ãƒžãƒ¼ã‚¸ãƒªã‚¯ã‚¨ã‚¹ãƒˆã€ã‚¹ãƒ‹ãƒšãƒƒãƒˆã€ãƒ—ロジェクトã€ãŠã‚ˆã³ã‚°ãƒ«ãƒ¼ãƒ—ã‚’ã™ã°ã‚„ã作æˆã™ã‚‹ãŸã‚ã®ä¾¿åˆ©ãªãƒ¡ãƒ‹ãƒ¥ãƒ¼ãŒã‚ã‚Šã¾ã™ã€‚ãれをクリックã—ã¦ã€ã€ŒGitLabã€ã‚»ã‚¯ã‚·ãƒ§ãƒ³ã‹ã‚‰ã€ŒNew projectã€ã‚’é¸æŠžã—ã¦é–‹å§‹ã—ã¾ã™ã€‚"
msgid "UserOnboardingTour|Thanks for taking the guided tour. Remember, if you want to go through it again, you can start %{emphasisStart}Learn GitLab%{emphasisEnd} in the help menu on the top right."
msgstr ""
@@ -22413,7 +22907,7 @@ msgid "UserProfile|Activity"
msgstr "アクティビティ"
msgid "UserProfile|Already reported for abuse"
-msgstr ""
+msgstr "æ—¢ã«å ±å‘Šã•ã‚ŒãŸä¸æ­£è¡Œç‚º"
msgid "UserProfile|Blocked user"
msgstr ""
@@ -22425,16 +22919,16 @@ msgid "UserProfile|Edit profile"
msgstr "プロフィールを編集"
msgid "UserProfile|Explore public groups to find projects to contribute to."
-msgstr ""
+msgstr "公開グループを調ã¹ã¦ã€è²¢çŒ®ã™ã‚‹ãƒ—ロジェクトを見ã¤ã‘ã¦ãã ã•ã„。"
msgid "UserProfile|Groups"
msgstr "グループ"
msgid "UserProfile|Groups are the best way to manage projects and members."
-msgstr ""
+msgstr "グループã¯ã€ãƒ—ロジェクトã¨ãƒ¡ãƒ³ãƒãƒ¼ã‚’管ç†ã™ã‚‹ãŸã‚ã®æœ€è‰¯ã®æ–¹æ³•ã§ã™ã€‚"
msgid "UserProfile|Join or create a group to start contributing by commenting on issues or submitting merge requests!"
-msgstr ""
+msgstr "課題ã«ã‚³ãƒ¡ãƒ³ãƒˆã—ãŸã‚Šã€ãƒžãƒ¼ã‚¸ãƒªã‚¯ã‚¨ã‚¹ãƒˆã‚’é€ä¿¡ã—ãŸã‚Šã—ã¦è²¢çŒ®ã™ã‚‹ãŸã‚ã«ã€ã‚°ãƒ«ãƒ¼ãƒ—ã«å‚加ã¾ãŸã¯ä½œæˆã—ã¾ã—ょã†ã€‚"
msgid "UserProfile|Most Recent Activity"
msgstr "最新ã®ã‚¢ã‚¯ãƒ†ã‚£ãƒ“ティ"
@@ -22446,16 +22940,16 @@ msgid "UserProfile|Overview"
msgstr "概è¦"
msgid "UserProfile|Personal projects"
-msgstr ""
+msgstr "個人的ãªãƒ—ロジェクト"
msgid "UserProfile|Report abuse"
-msgstr ""
+msgstr "ä¸æ­£è¡Œç‚ºã‚’報告ã™ã‚‹"
msgid "UserProfile|Snippets"
msgstr "スニペット"
msgid "UserProfile|Snippets in GitLab can either be private, internal, or public."
-msgstr ""
+msgstr "GitLab ã®ã‚¹ãƒ‹ãƒšãƒƒãƒˆã¯ã€éžå…¬é–‹ã€å†…部ã€ã¾ãŸã¯å…¬é–‹ã«ã™ã‚‹ã“ã¨ãŒã§ãã¾ã™ã€‚"
msgid "UserProfile|Star projects to track their progress and show your appreciation."
msgstr ""
@@ -22464,19 +22958,19 @@ msgid "UserProfile|Starred projects"
msgstr ""
msgid "UserProfile|Subscribe"
-msgstr ""
+msgstr "購読"
msgid "UserProfile|This user doesn't have any personal projects"
-msgstr ""
+msgstr "ã“ã®ãƒ¦ãƒ¼ã‚¶ãƒ¼ã¯å€‹äººãƒ—ロジェクトをæŒã£ã¦ã„ã¾ã›ã‚“。"
msgid "UserProfile|This user has a private profile"
-msgstr ""
+msgstr "ã“ã®ãƒ¦ãƒ¼ã‚¶ãƒ¼ã¯éžå…¬é–‹ã®ãƒ—ロファイルをæŒã£ã¦ã„ã¾ã™"
msgid "UserProfile|This user hasn't contributed to any projects"
-msgstr ""
+msgstr "ã“ã®ãƒ¦ãƒ¼ã‚¶ãƒ¼ã¯ã©ã®ãƒ—ロジェクトã«ã‚‚貢献ã—ã¦ã„ã¾ã›ã‚“。"
msgid "UserProfile|This user hasn't starred any projects"
-msgstr ""
+msgstr "ã“ã®ãƒ¦ãƒ¼ã‚¶ãƒ¼ã¯ã©ã®ãƒ—ロジェクトもãŠæ°—ã«ã„ã‚Šã«ã—ã¦ã„ã¾ã›ã‚“。"
msgid "UserProfile|This user is blocked"
msgstr ""
@@ -22485,28 +22979,28 @@ msgid "UserProfile|View all"
msgstr "ã™ã¹ã¦è¡¨ç¤º"
msgid "UserProfile|View user in admin area"
-msgstr ""
+msgstr "管ç†è€…エリアã§ã®ãƒ¦ãƒ¼ã‚¶ãƒ¼ã®è¡¨ç¤º"
msgid "UserProfile|You can create a group for several dependent projects."
-msgstr ""
+msgstr "複数ã®ä¾å­˜ãƒ—ロジェクトã«å¯¾ã—ã¦ã‚°ãƒ«ãƒ¼ãƒ—を作æˆã§ãã¾ã™ã€‚"
msgid "UserProfile|You haven't created any personal projects."
-msgstr ""
+msgstr "個人的ãªãƒ—ロジェクトを作æˆã—ã¦ã„ã¾ã›ã‚“。"
msgid "UserProfile|You haven't created any snippets."
-msgstr ""
+msgstr "スニペットを作æˆã—ã¦ã„ã¾ã›ã‚“。"
msgid "UserProfile|Your projects can be available publicly, internally, or privately, at your choice."
-msgstr ""
+msgstr "プロジェクトã¯å…¬é–‹ã€å†…部公開ã€ã‚ã‚‹ã„ã¯éžå…¬é–‹ã‚’é¸æŠžã§ãã¾ã™ã€‚"
msgid "Username (optional)"
msgstr "ユーザå (オプション)"
msgid "Username is already taken."
-msgstr ""
+msgstr "ãã®ãƒ¦ãƒ¼ã‚¶ãƒ¼åã¯æ—¢ã«ä½¿ã‚ã‚Œã¦ã„ã¾ã™ã€‚"
msgid "Username is available."
-msgstr ""
+msgstr "ãã®ãƒ¦ãƒ¼ã‚¶ãƒ¼åã¯ä½¿ç”¨å¯èƒ½ã§ã™ã€‚"
msgid "Username is too long (maximum is %{max_length} characters)."
msgstr ""
@@ -22530,49 +23024,49 @@ msgid "Users over License:"
msgstr ""
msgid "Users requesting access to"
-msgstr ""
+msgstr "次ã®å ´æ‰€ã¸ã®ã‚¢ã‚¯ã‚»ã‚¹ã‚’è¦æ±‚ã—ã¦ã„るユーザー"
msgid "Users were successfully added."
-msgstr ""
+msgstr "ユーザーを正常ã«è¿½åŠ ã§ãã¾ã—ãŸã€‚"
msgid "Users with a Guest role or those who don't belong to any projects or groups don't count towards seats in use."
-msgstr ""
+msgstr "ゲストロールをæŒã¤ãƒ¦ãƒ¼ã‚¶ã€ã¾ãŸã¯ãƒ—ロジェクトやグループã«å±žã—ã¦ã„ãªã„ユーザã¯ã€ä½¿ç”¨ä¸­ã®ã‚·ãƒ¼ãƒˆã«ã‚«ã‚¦ãƒ³ãƒˆã•ã‚Œã¾ã›ã‚“。"
msgid "UsersSelect|%{name} + %{length} more"
msgstr ""
msgid "UsersSelect|Any User"
-msgstr ""
+msgstr "ä»»æ„ã®ãƒ¦ãƒ¼ã‚¶ãƒ¼"
msgid "UsersSelect|Assignee"
-msgstr ""
+msgstr "è­²å—人"
msgid "UsersSelect|No assignee - %{openingTag} assign yourself %{closingTag}"
-msgstr ""
+msgstr "è­²å—人ãªã— - %{openingTag} 自分自身を割り当ã¦ã‚‹ %{closingTag}"
msgid "UsersSelect|Unassigned"
-msgstr ""
+msgstr "未割り当ã¦"
msgid "Using %{code_start}::%{code_end} denotes a %{link_start}scoped label set%{link_end}"
-msgstr ""
+msgstr "%{code_start}::%{code_end} を使ã£ã¦ã€ %{link_start} ラベルセットã®ç¯„囲%{link_end} を示ã™"
msgid "Using required encryption strategy when encrypted field is missing!"
-msgstr ""
+msgstr "æš—å·åŒ–フィールドãŒãªã„å ´åˆã€å¿…è¦ãªæš—å·åŒ–戦略を使用ã™ã‚‹!"
msgid "Validate"
-msgstr ""
+msgstr "検証"
msgid "Validate your GitLab CI configuration file"
-msgstr ""
+msgstr "GitLab CI構æˆãƒ•ã‚¡ã‚¤ãƒ«ã‚’検証"
msgid "Validations failed."
-msgstr ""
+msgstr "検証ã«å¤±æ•—ã—ã¾ã—ãŸã€‚"
msgid "Validity"
-msgstr ""
+msgstr "有効"
msgid "Value"
-msgstr ""
+msgstr "値"
msgid "Value Stream"
msgstr ""
@@ -22687,6 +23181,9 @@ msgstr ""
msgid "View issue"
msgstr ""
+msgid "View issues"
+msgstr ""
+
msgid "View it on GitLab"
msgstr "GitLab ã§è¡¨ç¤º"
@@ -22724,7 +23221,7 @@ msgid "View supported languages and frameworks"
msgstr ""
msgid "View the documentation"
-msgstr ""
+msgstr "ドキュメントã®è¡¨ç¤º"
msgid "View the latest successful deployment to this environment"
msgstr ""
@@ -22736,7 +23233,7 @@ msgid "Viewing commit"
msgstr ""
msgid "Visibility"
-msgstr ""
+msgstr "公開範囲"
msgid "Visibility and access controls"
msgstr "å¯è¦–性ã¨ã‚¢ã‚¯ã‚»ã‚¹åˆ¶å¾¡"
@@ -22754,7 +23251,7 @@ msgid "Visibility, project features, permissions"
msgstr ""
msgid "Visibility:"
-msgstr ""
+msgstr "公開範囲:"
msgid "VisibilityLevel|Internal"
msgstr "内部"
@@ -22769,7 +23266,7 @@ msgid "VisibilityLevel|Unknown"
msgstr "ä¸æ˜Ž"
msgid "VisualReviewApp|%{stepStart}Step 1%{stepEnd}. Copy the following script:"
-msgstr ""
+msgstr "%{stepStart}ステップ1%{stepEnd} 次ã®ã‚¹ã‚¯ãƒªãƒ—トをコピーã—ã¾ã™:"
msgid "VisualReviewApp|%{stepStart}Step 2%{stepEnd}. Add it to the %{headTags} tags of every page of your application, ensuring the merge request ID is set or not set as required. "
msgstr ""
@@ -22864,6 +23361,15 @@ msgstr ""
msgid "VulnerabilityManagement|Resolved %{timeago} by %{user}"
msgstr ""
+msgid "VulnerabilityManagement|Something went wrong while trying to delete the comment. Please try again later."
+msgstr ""
+
+msgid "VulnerabilityManagement|Something went wrong while trying to retrieve the vulnerability history. Please try again later."
+msgstr ""
+
+msgid "VulnerabilityManagement|Something went wrong while trying to save the comment. Please try again later."
+msgstr ""
+
msgid "VulnerabilityManagement|Something went wrong, could not create an issue."
msgstr ""
@@ -22894,6 +23400,9 @@ msgstr ""
msgid "VulnerabilityStatusTypes|Resolved"
msgstr ""
+msgid "Vulnerability|%{scannerName} (version %{scannerVersion})"
+msgstr ""
+
msgid "Vulnerability|Class"
msgstr "クラス"
@@ -22910,7 +23419,7 @@ msgid "Vulnerability|Identifiers"
msgstr "識別å­"
msgid "Vulnerability|Image"
-msgstr ""
+msgstr "イメージ"
msgid "Vulnerability|Instances"
msgstr "インスタンス"
@@ -22927,12 +23436,15 @@ msgstr "åå‰ç©ºé–“"
msgid "Vulnerability|Project"
msgstr "プロジェクト"
-msgid "Vulnerability|Report Type"
+msgid "Vulnerability|Scanner Provider"
msgstr ""
-msgid "Vulnerability|Severity"
+msgid "Vulnerability|Scanner Type"
msgstr ""
+msgid "Vulnerability|Severity"
+msgstr "é‡è¦åº¦"
+
msgid "Vulnerability|Status"
msgstr ""
@@ -22955,10 +23467,10 @@ msgid "Warning: Displaying this diagram might cause performance issues on this p
msgstr ""
msgid "We could not determine the path to remove the epic"
-msgstr ""
+msgstr "エピックを削除ã™ã‚‹ãƒ‘スを特定ã§ãã¾ã›ã‚“ã§ã—ãŸ"
msgid "We could not determine the path to remove the issue"
-msgstr ""
+msgstr "課題を削除ã™ã‚‹ãŸã‚ã®ãƒ‘スを特定ã§ãã¾ã›ã‚“ã§ã—ãŸ"
msgid "We couldn't reach the Prometheus server. Either the server no longer exists or the configuration details need updating."
msgstr ""
@@ -22979,11 +23491,17 @@ msgid "We heard back from your U2F device. You have been authenticated."
msgstr ""
msgid "We sent you an email with reset password instructions"
+msgstr "パスワードリセット方法ã«ã¤ã„ã¦ãƒ¡ãƒ¼ãƒ«ã‚’é€ä¿¡ã—ã¾ã—ãŸ"
+
+msgid "We tried to automatically renew your %{strong}%{plan_name}%{strong_close} subscription for %{strong}%{namespace_name}%{strong_close} on %{expires_on} but something went wrong so your subscription was downgraded to the free plan. Don't worry, your data is safe. We suggest you check your payment method and get in touch with our support team (%{support_link}). They'll gladly help with your subscription renewal."
msgstr ""
msgid "We want to be sure it is you, please confirm you are not a robot."
msgstr "本人確èªã®ãŸã‚ã€ã‚ãªãŸãŒãƒ­ãƒœãƒƒãƒˆã§ãªã„ã“ã¨ã‚’確èªã—ã¦ãã ã•ã„。"
+msgid "We will automatically renew your %{strong}%{plan_name}%{strong_close} subscription for %{strong}%{namespace_name}%{strong_close} on %{strong}%{expires_on}%{strong_close}. There's nothing that you need to do, we'll let you know when the renewal is complete. Need more seats, a higher plan or just want to review your payment method?"
+msgstr ""
+
msgid "We've found no vulnerabilities"
msgstr ""
@@ -22991,7 +23509,7 @@ msgid "Web IDE"
msgstr "Web IDE"
msgid "Web Terminal"
-msgstr ""
+msgstr "Web端末"
msgid "Web terminal"
msgstr "ウェブターミナル"
@@ -23009,10 +23527,10 @@ msgid "Webhook Settings"
msgstr ""
msgid "Webhooks"
-msgstr ""
+msgstr "Webhooks"
msgid "Webhooks Help"
-msgstr ""
+msgstr "Webhooks ã®ãƒ˜ãƒ«ãƒ—"
msgid "Webhooks allow you to trigger a URL if, for example, new code is pushed or a new issue is created. You can configure webhooks to listen for specific events like pushes, issues or merge requests. Group webhooks will apply to all projects in a group, allowing you to standardize webhook functionality across your entire group."
msgstr ""
@@ -23087,15 +23605,6 @@ msgstr ""
msgid "When:"
msgstr ""
-msgid "While it's rare to have no vulnerabilities for your group, it can happen. In any event, we ask that you double check your settings to make sure you've set up your dashboard correctly."
-msgstr ""
-
-msgid "While it's rare to have no vulnerabilities for your pipeline, it can happen. In any event, we ask that you double check your settings to make sure all security scanning jobs have passed successfully."
-msgstr ""
-
-msgid "While it's rare to have no vulnerabilities for your project, it can happen. In any event, we ask that you double check your settings to make sure you've set up your dashboard correctly."
-msgstr ""
-
msgid "While it's rare to have no vulnerabilities, it can happen. In any event, we ask that you please double check your settings to make sure you've set up your dashboard correctly."
msgstr ""
@@ -23112,7 +23621,7 @@ msgid "Who can see this group?"
msgstr "誰ãŒã“ã®ã‚°ãƒ«ãƒ¼ãƒ—を見れã¾ã™ã‹ï¼Ÿ"
msgid "Who will be able to see this group?"
-msgstr ""
+msgstr "誰ãŒã“ã®ã‚°ãƒ«ãƒ¼ãƒ—を見れã¾ã™ã‹ï¼Ÿ"
msgid "Who will be using GitLab?"
msgstr ""
@@ -23126,11 +23635,8 @@ msgstr ""
msgid "Wiki"
msgstr "Wiki"
-msgid "Wiki pages"
-msgstr ""
-
msgid "Wiki was successfully updated."
-msgstr ""
+msgstr "Wikiã¯æ­£å¸¸ã«æ›´æ–°ã•ã‚Œã¾ã—ãŸ"
msgid "WikiClone|Clone your wiki"
msgstr "Wiki をクローン"
@@ -23184,7 +23690,7 @@ msgid "WikiHistoricalPage|This is an old version of this page."
msgstr "ã“ã®ãƒšãƒ¼ã‚¸ã®å¤ã„ãƒãƒ¼ã‚¸ãƒ§ãƒ³ãŒè¡¨ç¤ºã•ã‚Œã¦ã„ã¾ã™ã€‚"
msgid "WikiHistoricalPage|You can view the %{most_recent_link} or browse the %{history_link}."
-msgstr ""
+msgstr "%{most_recent_link} を表示ã™ã‚‹ã‹ã€%{history_link} ã‚’å‚ç…§ã§ãã¾ã™ã€‚"
msgid "WikiHistoricalPage|history"
msgstr "履歴"
@@ -23235,7 +23741,7 @@ msgid "Wiki|Create page"
msgstr "ページを作æˆ"
msgid "Wiki|Created date"
-msgstr ""
+msgstr "作æˆæ—¥æ™‚"
msgid "Wiki|Edit Page"
msgstr "ページを編集"
@@ -23256,7 +23762,7 @@ msgid "Wiki|Pages"
msgstr "Pages"
msgid "Wiki|Title"
-msgstr ""
+msgstr "タイトル"
msgid "Wiki|View All Pages"
msgstr ""
@@ -23280,7 +23786,7 @@ msgid "Work in progress Limit"
msgstr ""
msgid "Workflow Help"
-msgstr ""
+msgstr "ワークフローã®ãƒ˜ãƒ«ãƒ—"
msgid "Write"
msgstr ""
@@ -23292,7 +23798,7 @@ msgid "Write a comment…"
msgstr ""
msgid "Write access allowed"
-msgstr ""
+msgstr "書ãè¾¼ã¿ã‚¢ã‚¯ã‚»ã‚¹ã‚’許å¯"
msgid "Write milestone description..."
msgstr "マイルストーンã®èª¬æ˜Žã‚’書ã..."
@@ -23301,20 +23807,23 @@ msgid "Write your release notes or drag your files here…"
msgstr ""
msgid "Wrong extern UID provided. Make sure Auth0 is configured correctly."
-msgstr ""
+msgstr "é–“é•ã£ãŸå¤–部UIDãŒæŒ‡å®šã•ã‚Œã¾ã—ãŸã€‚ Auth0ãŒæ­£ã—ã設定ã•ã‚Œã¦ã„ã‚‹ã“ã¨ã‚’確èªã—ã¦ãã ã•ã„。"
msgid "Yes"
msgstr "ã¯ã„"
msgid "Yes or No"
-msgstr ""
+msgstr "ã¯ã„ / ã„ã„ãˆ"
msgid "Yes, add it"
msgstr "ã¯ã„ã€è¿½åŠ ã—ã¾ã™"
-msgid "Yes, let me map Google Code users to full names or GitLab users."
+msgid "Yes, close issue"
msgstr ""
+msgid "Yes, let me map Google Code users to full names or GitLab users."
+msgstr "ã¯ã„ã€Googleコードã®ãƒ¦ãƒ¼ã‚¶ãƒ¼ã‚’フルãƒãƒ¼ãƒ ã¾ãŸã¯GitLab ユーザーã«å¯¾å¿œã•ã›ã¦ãã ã•ã„。"
+
msgid "Yesterday"
msgstr "昨日"
@@ -23328,19 +23837,19 @@ msgid "You are about to transfer the control of your account to %{group_name} gr
msgstr ""
msgid "You are an admin, which means granting access to <strong>%{client_name}</strong> will allow them to interact with GitLab as an admin as well. Proceed with caution."
-msgstr ""
+msgstr "ã‚ãªãŸã¯ç®¡ç†è€…ã§ã™ã€‚ã¤ã¾ã‚Šã€ <strong>%{client_name}</strong> ã¸ã®ã‚¢ã‚¯ã‚»ã‚¹ã‚’許å¯ã™ã‚‹ã¨ã€ãã‚Œã¯ç®¡ç†è€…ã¨ã—ã¦GitLabã¨ã‚„ã‚Šã¨ã‚Šã§ãã¾ã™ã€‚注æ„ã—ã¦ãã ã•ã„。"
msgid "You are attempting to delete a file that has been previously updated."
-msgstr ""
+msgstr "ã‚ãªãŸã¯ã€ä»¥å‰ã«æ›´æ–°ã—ãŸãƒ•ã‚¡ã‚¤ãƒ«ã‚’削除ã—よã†ã¨ã—ã¦ã„ã¾ã™ã€‚"
msgid "You are attempting to update a file that has changed since you started editing it."
-msgstr ""
+msgstr "編集ã—ãŸãƒ•ã‚¡ã‚¤ãƒ«ã‚’æ›´æ–°ã—よã†ã¨ã—ã¦ã„ã¾ã™ã€‚"
msgid "You are connected to the Prometheus server, but there is currently no data to display."
msgstr ""
msgid "You are going to remove %{group_name}, this will also remove all of its subgroups and projects. Removed groups CANNOT be restored! Are you ABSOLUTELY sure?"
-msgstr ""
+msgstr "%{group_name} を削除ã—よã†ã¨ã—ã¦ã„ã¾ã™ã€‚ã“ã‚Œã¯ã€ã“ã®ã‚°ãƒ«ãƒ¼ãƒ—é…下ã®ã‚µãƒ–グループã¨ãƒ—ロジェクトもã™ã¹ã¦å‰Šé™¤ã•ã‚Œã¾ã™ã€‚一度削除ã•ã‚ŒãŸã‚°ãƒ«ãƒ¼ãƒ—を復元ã™ã‚‹ã“ã¨ã¯ã§ãã¾ã›ã‚“。本当ã«ã‚ˆã‚ã—ã„ã§ã™ã‹ï¼Ÿ"
msgid "You are going to remove %{project_full_name}. Removed project CANNOT be restored! Are you ABSOLUTELY sure?"
msgstr "%{project_full_name} プロジェクトを削除ã—よã†ã¨ã—ã¦ã„ã¾ã™ã€‚削除ã•ã‚ŒãŸãƒ—ロジェクトã¯çµ¶å¯¾ã«å…ƒã«ã¯æˆ»ã›ã¾ã›ã‚“ï¼æœ¬å½“ã«ã‚ˆã‚ã—ã„ã§ã™ã‹ï¼Ÿ"
@@ -23367,31 +23876,31 @@ msgid "You are on a read-only GitLab instance."
msgstr "読ã¿å–り専用 GitLab インスタンスをå‚照中ã§ã™ã€‚"
msgid "You are receiving this message because you are a GitLab administrator for %{url}."
-msgstr ""
+msgstr "ã‚ãªãŸã¯ %{url} ã®GitLab管ç†è€…ã§ã‚ã‚‹ãŸã‚ã€ã“ã®ãƒ¡ãƒƒã‚»ãƒ¼ã‚¸ã‚’å—ã‘å–ã£ã¦ã„ã¾ã™ã€‚"
msgid "You are trying to upload something other than an image. Please upload a .png, .jpg, .jpeg, .gif, .bmp, .tiff or .ico."
msgstr ""
msgid "You can %{linkStart}view the blob%{linkEnd} instead."
-msgstr ""
+msgstr "代ã‚ã‚Šã«ã€%{linkStart} blob を見る%{linkEnd} ã“ã¨ãŒã§ãã¾ã™ã€‚"
msgid "You can also create a project from the command line."
msgstr "コマンドラインã‹ã‚‰ãƒ—ロジェクトを作æˆã™ã‚‹ã“ã¨ã‚‚ã§ãã¾ã™ã€‚"
msgid "You can also press &#8984;-Enter"
-msgstr ""
+msgstr "&#8984; ã¨Enter を押ã™ã“ã¨ã‚‚ã§ãã¾ã™ã€‚"
msgid "You can also press Ctrl-Enter"
-msgstr ""
+msgstr "Ctrl ã¨Enter を押ã™ã“ã¨ã‚‚ã§ãã¾ã™ã€‚"
msgid "You can also star a label to make it a priority label."
msgstr "ラベルã«ã‚¹ã‚¿ãƒ¼ã‚’付ã‘ã¦å„ªå…ˆãƒ©ãƒ™ãƒ«ã«ã™ã‚‹ã“ã¨ã‚‚ã§ãã¾ã™ã€‚"
msgid "You can also test your %{gitlab_ci_yml} in %{lint_link_start}CI Lint%{lint_link_end}"
-msgstr ""
+msgstr "%{lint_link_start}CI Lint%{lint_link_end}㧠%{gitlab_ci_yml} をテストã™ã‚‹ã“ã¨ãŒã§ãã¾ã™"
msgid "You can also upload existing files from your computer using the instructions below."
-msgstr ""
+msgstr "以下ã®æ‰‹é †ã«ãã£ã¦ã€ã‚ãªãŸã®ã‚³ãƒ³ãƒ”ューター上ã®æ—¢å­˜ã®ãƒ•ã‚¡ã‚¤ãƒ«ã‚’アップロードã§ãã¾ã™ã€‚"
msgid "You can always edit this later"
msgstr ""
@@ -23424,13 +23933,13 @@ msgid "You can get started by cloning the repository or start adding files to it
msgstr ""
msgid "You can invite a new member to <strong>%{project_name}</strong> or invite another group."
-msgstr ""
+msgstr "æ–°ã—ã„メンãƒãƒ¼ã‚’<strong>%{project_name} </strong>ã«æ‹›å¾…ã™ã‚‹ã‹ã€åˆ¥ã®ã‚°ãƒ«ãƒ¼ãƒ—を招待ã™ã‚‹ã“ã¨ãŒã§ãã¾ã™ã€‚"
msgid "You can invite a new member to <strong>%{project_name}</strong>."
-msgstr ""
+msgstr "æ–°ã—ã„メンãƒãƒ¼ã‚’<strong>%{project_name} </strong>ã«æ‹›å¾…ã§ãã¾ã™ã€‚"
msgid "You can invite another group to <strong>%{project_name}</strong>."
-msgstr ""
+msgstr "ä»–ã®ã‚°ãƒ«ãƒ¼ãƒ—ã‚’<strong>%{project_name} </strong>ã«æ‹›å¾…ã§ãã¾ã™ã€‚"
msgid "You can move around the graph by using the arrow keys."
msgstr "矢å°ã‚­ãƒ¼ã‚’使用ã—ã¦ã‚°ãƒ©ãƒ•ã‚’移動ã™ã‚‹ã“ã¨ãŒã§ãã¾ã™ã€‚"
@@ -23507,6 +24016,9 @@ msgstr "ã“ã®èª­ã¿å–り専用 GitLab インスタンスã«ã¯æ›¸ã込むã“ã
msgid "You could not create a new trigger."
msgstr "æ–°ã—ã„トリガーを作æˆã§ãã¾ã›ã‚“ã§ã—ãŸã€‚"
+msgid "You didn't renew your %{strong}%{plan_name}%{strong_close} subscription for %{strong}%{namespace_name}%{strong_close} so it was downgraded to the free plan."
+msgstr ""
+
msgid "You didn't renew your %{strong}%{plan_name}%{strong_close} subscription so it was downgraded to the GitLab Core Plan."
msgstr ""
@@ -23538,7 +24050,7 @@ msgid "You don't have any authorized applications"
msgstr "承èªã•ã‚ŒãŸã‚¢ãƒ—リケーションãŒã‚ã‚Šã¾ã›ã‚“"
msgid "You don't have any deployments right now."
-msgstr ""
+msgstr "ç¾åœ¨ãƒ‡ãƒ—ロイãŒã‚ã‚Šã¾ã›ã‚“。"
msgid "You don't have any open merge requests"
msgstr ""
@@ -23559,25 +24071,28 @@ msgid "You don’t have access to Value Stream Analytics for this group"
msgstr ""
msgid "You have been granted %{access_level} access to the %{source_link} %{source_type}."
-msgstr ""
+msgstr "ã‚ãªãŸã¯ %{source_link} %{source_type} ã¸ã®ã‚¢ã‚¯ã‚»ã‚¹æ¨©é™ %{access_level} を与ãˆã‚‰ã‚Œã¦ã„ã¾ã™ã€‚"
msgid "You have been granted %{access_level} access to the %{source_name} %{source_type}."
-msgstr ""
+msgstr "ã‚ãªãŸã¯ %{source_name} %{source_type} ã¸ã®ã‚¢ã‚¯ã‚»ã‚¹æ¨©é™ %{access_level} を与ãˆã‚‰ã‚Œã¦ã„ã¾ã™ã€‚"
msgid "You have been granted %{member_human_access} access to %{label}."
-msgstr ""
+msgstr "ã‚ãªãŸã¯ %{label} ã¸ã® %{member_human_access} アクセス権é™ã‚’与ãˆã‚‰ã‚Œã¦ã„ã¾ã™ã€‚"
msgid "You have been unsubscribed from this thread."
-msgstr ""
+msgstr "ã‚ãªãŸã¯ã“ã®ã‚¹ãƒ¬ãƒƒãƒ‰ã‹ã‚‰è„±é€€ã—ã¾ã—ãŸã€‚"
msgid "You have declined the invitation to join %{label}."
+msgstr "%{label} ã¸ã®å‚加ã®æ‹›å¾…を辞退ã—ã¾ã—ãŸã€‚"
+
+msgid "You have imported from this project %{numberOfPreviousImportsForProject} times before. Each new import will create duplicate issues."
msgstr ""
msgid "You have no permissions"
msgstr "権é™ãŒã‚ã‚Šã¾ã›ã‚“"
msgid "You have not added any approvers. Start by adding users or groups."
-msgstr ""
+msgstr "ã‚ãªãŸã¯æ‰¿èªè€…を追加ã—ã¦ã„ã¾ã›ã‚“。ユーザーã¾ãŸã¯ã‚°ãƒ«ãƒ¼ãƒ—ã‹ã‚‰è¿½åŠ ã—ã¦ãã ã•ã„。"
msgid "You have reached your project limit"
msgstr "プロジェクト数ã®ä¸Šé™ã«é”ã—ã¦ã„ã¾ã™"
@@ -23595,7 +24110,7 @@ msgid "You left the \"%{membershipable_human_name}\" %{source_type}."
msgstr ""
msgid "You may also add variables that are made available to the running application by prepending the variable key with <code>K8S_SECRET_</code>."
-msgstr ""
+msgstr "変数åã®å‰ã« <code>K8S_SECRET_</code>を指定ã™ã‚‹ã“ã¨ã§ã€å®Ÿè¡Œä¸­ã®ã‚¢ãƒ—リケーションã§ä½¿ç”¨ã™ã‚‹å¤‰æ•°ã‚’追加ã™ã‚‹ã“ã¨ã‚‚ã§ãã¾ã™ã€‚"
msgid "You may close the milestone now."
msgstr ""
@@ -23613,7 +24128,7 @@ msgid "You must have permission to create a project in a namespace before forkin
msgstr "フォークã™ã‚‹å‰ã«ã€åå‰ç©ºé–“ã«ãƒ—ロジェクトを作æˆã™ã‚‹æ¨©é™ãŒå¿…è¦ã§ã™ã€‚"
msgid "You must provide a valid current password"
-msgstr ""
+msgstr "ç¾åœ¨ã®æ­£ã—ã„パスワードを入力ã—ã¦ãã ã•ã„"
msgid "You must provide your current password in order to change it."
msgstr "パスワードを変更ã™ã‚‹ã«ã¯ã€ç¾åœ¨ã®ãƒ‘スワードを入力ã™ã‚‹å¿…è¦ãŒã‚ã‚Šã¾ã™ã€‚"
@@ -23631,31 +24146,34 @@ msgid "You need a different license to enable FileLocks feature"
msgstr "ファイルロック機能を有効ã«ã™ã‚‹ã«ã¯åˆ¥ã®ãƒ©ã‚¤ã‚»ãƒ³ã‚¹ãŒå¿…è¦ã§ã™"
msgid "You need git-lfs version %{min_git_lfs_version} (or greater) to continue. Please visit https://git-lfs.github.com"
-msgstr ""
+msgstr "続行ã™ã‚‹ã«ã¯ã€ãƒãƒ¼ã‚¸ãƒ§ãƒ³ %{min_git_lfs_version} (ã¾ãŸã¯ãれ以é™ã®ï¼‰ã® git-lfs ãŒå¿…è¦ã§ã™ã€‚ https://git-lfs.github.com ã«ã‚¢ã‚¯ã‚»ã‚¹ã—ã¦ãã ã•ã„。"
msgid "You need permission."
msgstr "権é™ãŒå¿…è¦ã§ã™"
msgid "You need to be logged in."
-msgstr ""
+msgstr "ログインã™ã‚‹å¿…è¦ãŒã‚ã‚Šã¾ã™"
msgid "You need to register a two-factor authentication app before you can set up a U2F device."
-msgstr ""
+msgstr "U2Fデãƒã‚¤ã‚¹ã‚’設定ã™ã‚‹å‰ã«ã€ã‚ãªãŸã¯2è¦ç´ èªè¨¼ã‚¢ãƒ—リを登録ã™ã‚‹å¿…è¦ãŒã‚ã‚Šã¾ã™ã€‚"
msgid "You need to set terms to be enforced"
msgstr ""
msgid "You need to specify both an Access Token and a Host URL."
-msgstr ""
+msgstr "アクセストークンã¨ãƒ›ã‚¹ãƒˆURLã®ä¸¡æ–¹ã‚’指定ã—ãªã‘ã‚Œã°ãªã‚Šã¾ã›ã‚“。"
msgid "You need to upload a GitLab project export archive (ending in .gz)."
-msgstr ""
+msgstr "GitLab プロジェクトã®ã‚¨ã‚¯ã‚¹ãƒãƒ¼ãƒˆã‚¢ãƒ¼ã‚«ã‚¤ãƒ–(.gz ã§çµ‚ã‚る)をアップロードã™ã‚‹å¿…è¦ãŒã‚ã‚Šã¾ã™ã€‚"
msgid "You need to upload a Google Takeout archive."
+msgstr "Google テイクアウトアーカイブをアップロードã™ã‚‹å¿…è¦ãŒã‚ã‚Šã¾ã™ã€‚"
+
+msgid "You reached %{usage_in_percent} of %{namespace_name}'s capacity (%{used_storage} of %{storage_limit})"
msgstr ""
msgid "You tried to fork %{link_to_the_project} but it failed for the following reason:"
-msgstr ""
+msgstr "%{link_to_the_project} ã®ãƒ•ã‚©ãƒ¼ã‚¯ã¯ã€æ¬¡ã®ç†ç”±ã§å¤±æ•—ã—ã¾ã—ãŸã€‚:"
msgid "You will be removed from existing projects/groups"
msgstr ""
@@ -23664,10 +24182,10 @@ msgid "You will first need to set up Jira Integration to use this feature."
msgstr ""
msgid "You will lose all changes you've made to this file. This action cannot be undone."
-msgstr ""
+msgstr "ã‚ãªãŸã¯ã€ã“ã®ãƒ•ã‚¡ã‚¤ãƒ«ã«è¡Œã£ãŸå¤‰æ›´ã‚’ã™ã¹ã¦å¤±ã£ã¦ã—ã¾ã„ã¾ã™ã€‚ã“ã®æ“作ã¯å…ƒã«æˆ»ã™ã“ã¨ã¯ã§ãã¾ã›ã‚“。"
msgid "You will lose all uncommitted changes you've made in this project. This action cannot be undone."
-msgstr ""
+msgstr "ã‚ãªãŸã¯ã€ã“ã®ãƒ—ロジェクトã§è¡Œã£ãŸã€ã‚³ãƒŸãƒƒãƒˆã—ã¦ã„ãªã„変更をã™ã¹ã¦å¤±ã£ã¦ã—ã¾ã„ã¾ã™ã€‚ã“ã®æ“作ã¯å…ƒã«æˆ»ã™ã“ã¨ã¯ã§ãã¾ã›ã‚“。"
msgid "You will need to update your local repositories to point to the new location."
msgstr "ローカルリãƒã‚¸ãƒˆãƒªãŒæ–°ã—ã„場所を示ã™ã‚ˆã†ã«æ›´æ–°ã™ã‚‹å¿…è¦ãŒã‚ã‚Šã¾ã™ã€‚"
@@ -23700,7 +24218,7 @@ msgid "You'll need to use different branch names to get a valid comparison."
msgstr "有効ãªæ¯”較を行ã†ãŸã‚ã«ã¯ã€ç•°ãªã‚‹ãƒ–ランãƒåを使用ã™ã‚‹å¿…è¦ãŒã‚ã‚Šã¾ã™ã€‚"
msgid "You're about to reduce the visibility of the project %{strong_start}%{project_name}%{strong_end} in %{strong_start}%{group_name}%{strong_end}."
-msgstr ""
+msgstr "%{strong_start}%{group_name}%{strong_end} 内ã®%{strong_start}%{project_name}%{strong_end} プロジェクトã®å…¬é–‹ç¯„囲を低下ã•ã›ã‚ˆã†ã¨ã—ã¦ã„ã¾ã™ã€‚"
msgid "You're about to reduce the visibility of the project %{strong_start}%{project_name}%{strong_end}."
msgstr ""
@@ -23709,13 +24227,13 @@ msgid "You're not allowed to %{tag_start}edit%{tag_end} files in this project di
msgstr ""
msgid "You're not allowed to make changes to this project directly. A fork of this project has been created that you can make changes in, so you can submit a merge request."
-msgstr ""
+msgstr "ã“ã®ãƒ—ロジェクトを直接変更ã™ã‚‹ã“ã¨ã¯ã§ãã¾ã›ã‚“。変更を加ãˆã‚‹ã“ã¨ãŒã§ãるプロジェクトã®ãƒ•ã‚©ãƒ¼ã‚¯ãŒä½œæˆã•ã‚ŒãŸã®ã§ã€ãƒžãƒ¼ã‚¸ãƒªã‚¯ã‚¨ã‚¹ãƒˆã‚’é€ä¿¡ã™ã‚‹ã“ã¨ãŒã§ãã¾ã™ã€‚"
msgid "You're not allowed to make changes to this project directly. A fork of this project is being created that you can make changes in, so you can submit a merge request."
-msgstr ""
+msgstr "ã“ã®ãƒ—ロジェクトを直接変更ã™ã‚‹ã“ã¨ã¯ã§ãã¾ã›ã‚“。変更を加ãˆã‚‹ã“ã¨ãŒã§ãるプロジェクトã®ãƒ•ã‚©ãƒ¼ã‚¯ãŒä½œæˆã•ã‚Œã¦ã„ã‚‹ã®ã§ã€ãƒžãƒ¼ã‚¸ãƒªã‚¯ã‚¨ã‚¹ãƒˆã‚’é€ä¿¡ã™ã‚‹ã“ã¨ãŒã§ãã¾ã™ã€‚"
msgid "You're only seeing %{startTag}other activity%{endTag} in the feed. To add a comment, switch to one of the following options."
-msgstr ""
+msgstr "フィードã«ã¯%{startTag}ã»ã‹ã®ã‚¢ã‚¯ãƒ†ã‚£ãƒ“ティ%{endTag}ã®ã¿ãŒè¡¨ç¤ºã•ã‚Œã¦ã„ã¾ã™ã€‚コメントを追加ã™ã‚‹ã«ã¯æ¬¡ã®ã„ãšã‚Œã‹ã®ã‚ªãƒ—ションã«åˆ‡ã‚Šæ›¿ãˆã¾ã™ã€‚"
msgid "You're receiving this email because of your account on %{host}."
msgstr "ã“ã®ãƒ¡ãƒ¼ãƒ«ã¯ %{host} ã®ã‚¢ã‚«ã‚¦ãƒ³ãƒˆå®›ã«é€ä¿¡ã•ã‚Œã¾ã—ãŸã€‚"
@@ -23738,9 +24256,21 @@ msgstr ""
msgid "YouTube"
msgstr "YouTube"
+msgid "Your %{strong}%{plan_name}%{strong_close} subscription for %{strong}%{namespace_name}%{strong_close} will expire on %{strong}%{expires_on}%{strong_close}. After that, you will not to be able to create issues or merge requests as well as many other features."
+msgstr ""
+
msgid "Your %{strong}%{plan_name}%{strong_close} subscription will expire on %{strong}%{expires_on}%{strong_close}. After that, you will not to be able to create issues or merge requests as well as many other features."
msgstr ""
+msgid "Your CSV export has started. It will be emailed to %{email} when complete."
+msgstr ""
+
+msgid "Your CSV export of %{issues_count} from project %{project_link} has been added to this email as an attachment."
+msgstr ""
+
+msgid "Your CSV export of %{written_count} from project %{project_name} (%{project_url}) has been added to this email as an attachment."
+msgstr ""
+
msgid "Your Commit Email will be used for web based operations, such as edits and merges."
msgstr ""
@@ -23751,10 +24281,10 @@ msgid "Your DevOps Score gives an overview of how you are using GitLab from a fe
msgstr ""
msgid "Your GPG keys (%{count})"
-msgstr ""
+msgstr "GPG éµ (%{count})"
msgid "Your GitLab group"
-msgstr ""
+msgstr "ã‚ãªãŸã® GitLab ã®ã‚°ãƒ«ãƒ¼ãƒ—"
msgid "Your Gitlab Gold trial will last 30 days after which point you can keep your free Gitlab account forever. We just need some additional information to activate your trial."
msgstr ""
@@ -23781,22 +24311,22 @@ msgid "Your Public Email will be displayed on your public profile."
msgstr ""
msgid "Your SSH keys (%{count})"
-msgstr ""
+msgstr "SSH éµ (%{count})"
msgid "Your To-Do List"
msgstr ""
msgid "Your U2F device did not send a valid JSON response."
-msgstr ""
+msgstr "ã‚ãªãŸã®U2Fデãƒã‚¤ã‚¹ã¯ã€æœ‰åŠ¹ãªJSON応答をé€ä¿¡ã—ã¾ã›ã‚“ã§ã—ãŸã€‚"
msgid "Your U2F device needs to be set up. Plug it in (if not already) and click the button on the left."
-msgstr ""
+msgstr "U2Fデãƒã‚¤ã‚¹ã‚’セットアップã™ã‚‹å¿…è¦ãŒã‚ã‚Šã¾ã™ã€‚(ã¾ã æŽ¥ç¶šã—ã¦ã„ãªã„å ´åˆã¯) å·®ã—込んã§æŽ¥ç¶šã—ã€å·¦ã®ãƒœã‚¿ãƒ³ã‚’クリックã—ã¾ã™ã€‚"
msgid "Your U2F device was registered!"
-msgstr ""
+msgstr "ã‚ãªãŸã® U2F デãƒã‚¤ã‚¹ãŒç™»éŒ²ã•ã‚Œã¾ã—ãŸã€‚"
msgid "Your access request to the %{source_type} has been withdrawn."
-msgstr ""
+msgstr "%{source_type} ã¸ã®ã‚¢ã‚¯ã‚»ã‚¹è¦æ±‚ã¯å–り消ã•ã‚Œã¾ã—ãŸã€‚"
msgid "Your account has been deactivated by your administrator. Please log back in to reactivate your account."
msgstr ""
@@ -23805,7 +24335,7 @@ msgid "Your account is locked."
msgstr ""
msgid "Your account uses dedicated credentials for the \"%{group_name}\" group and can only be updated through SSO."
-msgstr ""
+msgstr "ã‚ãªãŸã®ã‚¢ã‚«ã‚¦ãƒ³ãƒˆã¯ \"%{group_name}\"グループã«å°‚用ã®èªè¨¼æƒ…報を使用ã—ã¦ãŠã‚Šã€SSOを通ã˜ã¦ã®ã¿æ›´æ–°ã™ã‚‹ã“ã¨ãŒã§ãã¾ã™ã€‚"
msgid "Your applications (%{size})"
msgstr "アプリケーション(%{size})"
@@ -23837,6 +24367,9 @@ msgstr ""
msgid "Your comment could not be updated! Please check your network connection and try again."
msgstr ""
+msgid "Your comment will be discarded."
+msgstr ""
+
msgid "Your custom stage '%{title}' was created"
msgstr ""
@@ -23874,40 +24407,49 @@ msgid "Your new SCIM token"
msgstr ""
msgid "Your new personal access token has been created."
+msgstr "æ–°ã—ã„パーソナルアクセストークンãŒä½œæˆã•ã‚Œã¾ã—ãŸã€‚"
+
+msgid "Your new project access token has been created."
msgstr ""
msgid "Your password isn't required to view this page. If a password or any other personal details are requested, please contact your administrator to report abuse."
msgstr ""
msgid "Your password reset token has expired."
-msgstr ""
+msgstr "パスワードリセットトークンã¯æœ‰åŠ¹æœŸé™åˆ‡ã‚Œã§ã™ã€‚"
msgid "Your profile"
msgstr ""
msgid "Your project limit is %{limit} projects! Please contact your administrator to increase it"
-msgstr ""
+msgstr "ã‚ãªãŸã®ãƒ—ロジェクト数㯠%{limit} 以下ã«åˆ¶é™ã•ã‚Œã¦ã„ã¾ã™ã€‚ã“ã®ä¸Šé™ã‚’増やã™ã«ã¯ã‚ãªãŸã®ç®¡ç†è€…ã«é€£çµ¡ã—ã¦ãã ã•ã„"
msgid "Your projects"
msgstr "ã‚ãªãŸã®ãƒ—ロジェクト"
msgid "Your request for access has been queued for review."
+msgstr "アクセスã®ãƒªã‚¯ã‚¨ã‚¹ãƒˆã¯å¯©æŸ»å¾…ã¡ã§ã™ã€‚"
+
+msgid "Your search didn't match any commits."
msgstr ""
msgid "Your subscription expired!"
msgstr ""
msgid "Your subscription has been downgraded"
+msgstr "サブスクリプションã®ãƒ€ã‚¦ãƒ³ãƒ­ãƒ¼ãƒ‰ã‚’完了ã—ã¾ã—ãŸã€‚"
+
+msgid "Your subscription will automatically renew in %{remaining_days}"
msgstr ""
msgid "Your subscription will expire in %{remaining_days}"
msgstr ""
msgid "Zoom meeting added"
-msgstr ""
+msgstr "Zoom ミーティングを追加ã—ã¾ã—ãŸ"
msgid "Zoom meeting removed"
-msgstr ""
+msgstr "Zoom ミーティングを削除ã—ã¾ã—ãŸ"
msgid "[No reason]"
msgstr ""
@@ -23935,31 +24477,31 @@ msgid "ago"
msgstr "å‰"
msgid "allowed to fail"
-msgstr ""
+msgstr "失敗å¯èƒ½"
-msgid "already being used for another group or project milestone."
+msgid "already being used for another group or project %{timebox_name}."
msgstr ""
msgid "already has a \"created\" issue link"
msgstr ""
msgid "already shared with this group"
-msgstr ""
+msgstr "ã™ã§ã«ã“ã®ã‚°ãƒ«ãƒ¼ãƒ—ã¨å…±æœ‰ã—ã¦ã„ã¾ã™"
msgid "among other things"
msgstr "ãã®ä»–ã®ã‚‚ã®"
msgid "any-approver for the merge request already exists"
-msgstr ""
+msgstr "マージリクエストã®æ‰¿èªè€…ã¯ã™ã§ã«å­˜åœ¨ã—ã¾ã™"
msgid "any-approver for the project already exists"
-msgstr ""
+msgstr "プロジェクトã®æ‰¿èªè€…ã¯ã™ã§ã«å­˜åœ¨ã—ã¾ã™"
msgid "archived"
-msgstr ""
+msgstr "アーカイブ済ã¿"
msgid "assign yourself"
-msgstr ""
+msgstr "自分ã«å‰²ã‚Šå½“ã¦"
msgid "at risk"
msgstr ""
@@ -23983,25 +24525,25 @@ msgid "by %{user}"
msgstr ""
msgid "cannot be changed if a personal project has container registry tags."
-msgstr ""
+msgstr "個人用プロジェクトã«ã‚³ãƒ³ãƒ†ãƒŠãƒ¬ã‚¸ã‚¹ãƒˆãƒªã‚¿ã‚°ãŒã‚ã‚‹å ´åˆã€å¤‰æ›´ã§ãã¾ã›ã‚“。"
msgid "cannot be enabled unless all domains have TLS certificates"
-msgstr ""
+msgstr "ã™ã¹ã¦ã®ãƒ‰ãƒ¡ã‚¤ãƒ³ã« TLS 証明書ãŒãªã„ã¨æœ‰åŠ¹ã«ã§ãã¾ã›ã‚“"
msgid "cannot be modified"
-msgstr ""
+msgstr "変更ã§ãã¾ã›ã‚“"
msgid "cannot block others"
-msgstr ""
+msgstr "他をブロックã§ãã¾ã›ã‚“"
msgid "cannot include leading slash or directory traversal."
-msgstr ""
+msgstr "先頭ã®ã‚¹ãƒ©ãƒƒã‚·ãƒ¥ã‚’å«ã‚られã¾ã›ã‚“。ã¾ãŸãƒ‡ã‚£ãƒ¬ã‚¯ãƒˆãƒªãƒˆãƒ©ãƒãƒ¼ã‚µãƒ«ã¯ã§ãã¾ã›ã‚“。"
msgid "cannot itself be blocked"
-msgstr ""
+msgstr "ãれ自体をブロックã§ãã¾ã›ã‚“"
msgid "cannot merge"
-msgstr ""
+msgstr "マージã§ãã¾ã›ã‚“"
msgid "ciReport|%{linkStartTag}Learn more about Container Scanning %{linkEndTag}"
msgstr "%{linkStartTag}コンテナスキャンã®è©³ç´°%{linkEndTag}"
@@ -24015,6 +24557,9 @@ msgstr "%{linkStartTag}ä¾å­˜æ€§ã‚¹ã‚­ãƒ£ãƒ³ã®è©³ç´°%{linkEndTag}"
msgid "ciReport|%{linkStartTag}Learn more about SAST %{linkEndTag}"
msgstr "%{linkStartTag}SAST ã®è©³ç´°%{linkEndTag}"
+msgid "ciReport|%{linkStartTag}Learn more about Secret Scanning %{linkEndTag}"
+msgstr ""
+
msgid "ciReport|%{linkStartTag}Learn more about codequality reports %{linkEndTag}"
msgstr ""
@@ -24069,7 +24614,7 @@ msgid "ciReport|%{reportType} is loading"
msgstr ""
msgid "ciReport|%{reportType}: Loading resulted in an error"
-msgstr ""
+msgstr "%{reportType}:読ã¿è¾¼ã¿ä¸­ã«ã‚¨ãƒ©ãƒ¼ãŒç™ºç”Ÿã—ã¾ã—ãŸ"
msgid "ciReport|(errors when loading results)"
msgstr ""
@@ -24081,16 +24626,16 @@ msgid "ciReport|(is loading, errors when loading results)"
msgstr ""
msgid "ciReport|All projects"
-msgstr ""
+msgstr "ã™ã¹ã¦ã®ãƒ—ロジェクト"
msgid "ciReport|All report types"
-msgstr ""
+msgstr "ã™ã¹ã¦ã®ãƒ¬ãƒãƒ¼ãƒˆã‚¿ã‚¤ãƒ—"
msgid "ciReport|All severities"
-msgstr ""
+msgstr "ã™ã¹ã¦ã®é‡è¦åº¦"
msgid "ciReport|Automatically apply the patch in a new branch"
-msgstr ""
+msgstr "自動的ã«æ–°ã—ã„ブランãƒã«ã“ã®ãƒ‘ッãƒã‚’é©ç”¨ã™ã‚‹"
msgid "ciReport|Base pipeline codequality artifact not found"
msgstr ""
@@ -24099,25 +24644,25 @@ msgid "ciReport|Code quality"
msgstr "コードã®å“質"
msgid "ciReport|Container Scanning"
-msgstr ""
+msgstr "コンテナースキャン"
msgid "ciReport|Container scanning"
-msgstr ""
+msgstr "コンテナースキャン"
msgid "ciReport|Container scanning detects known vulnerabilities in your docker images."
msgstr "コンテナスキャンã¯ã€Docker イメージã«å­˜åœ¨ã™ã‚‹æ—¢çŸ¥ã®è„†å¼±æ€§ã‚’検出ã—ã¾ã™ã€‚"
msgid "ciReport|Create a merge request to implement this solution, or download and apply the patch manually."
-msgstr ""
+msgstr "マージリクエストを作æˆã—ã¦ã“ã®ã‚½ãƒªãƒ¥ãƒ¼ã‚·ãƒ§ãƒ³ã‚’実装ã™ã‚‹ã€ã¾ãŸã¯æ‰‹å‹•ã§ãƒ‘ッãƒã‚’ダウンロードã—ã¦é©ç”¨ã—ã¾ã™ã€‚"
msgid "ciReport|Create issue"
-msgstr ""
+msgstr "課題を作æˆã™ã‚‹"
msgid "ciReport|DAST"
msgstr "DAST"
msgid "ciReport|Dependency Scanning"
-msgstr ""
+msgstr "ä¾å­˜é–¢ä¿‚ã®ã‚¹ã‚­ãƒ£ãƒ³"
msgid "ciReport|Dependency Scanning detects known vulnerabilities in your source code's dependencies."
msgstr "ä¾å­˜é–¢ä¿‚スキャンã¯ã€ã‚½ãƒ¼ã‚¹ã‚³ãƒ¼ãƒ‰ã®ä¾å­˜é–¢ä¿‚ã®ä¸­ã®ã€æ—¢çŸ¥ã®è„†å¼±æ€§ã‚’検出ã—ã¾ã™ã€‚"
@@ -24126,10 +24671,10 @@ msgid "ciReport|Dependency scanning"
msgstr "ä¾å­˜é–¢ä¿‚スキャン"
msgid "ciReport|Download patch to resolve"
-msgstr ""
+msgstr "解決ã™ã‚‹ãŸã‚ã®ãƒ‘ッãƒã®ãƒ€ã‚¦ãƒ³ãƒ­ãƒ¼ãƒ‰"
msgid "ciReport|Download the patch to apply it manually"
-msgstr ""
+msgstr "パッãƒã‚’ダウンロードã—ã¦æ‰‹å‹•ã§é©ç”¨ã™ã‚‹"
msgid "ciReport|Dynamic Application Security Testing (DAST) detects known vulnerabilities in your web application."
msgstr "動的アプリケーションセキュリティテスト (DAST) ã¯ã€ã‚¦ã‚§ãƒ–アプリケーションã®æ—¢çŸ¥ã®è„†å¼±æ€§ã‚’検出ã—ã¾ã™ã€‚"
@@ -24144,16 +24689,16 @@ msgid "ciReport|Found %{issuesWithCount}"
msgstr ""
msgid "ciReport|Investigate this vulnerability by creating an issue"
-msgstr ""
+msgstr "課題を作æˆã—ã¦ã€ã“ã®è„†å¼±æ€§ã‚’調査・検証ã—ã¦ãã ã•ã„"
msgid "ciReport|Learn more about interacting with security reports"
-msgstr ""
+msgstr "セキュリティレãƒãƒ¼ãƒˆã¨ã®ç›¸äº’作用ã®è©³ç´°ã‚’ã”覧ãã ã•ã„。"
msgid "ciReport|Loading %{reportName} report"
msgstr "%{reportName} レãƒãƒ¼ãƒˆã‚’読ã¿è¾¼ã‚“ã§ã„ã¾ã™"
msgid "ciReport|Manage licenses"
-msgstr ""
+msgstr "ライセンスã®ç®¡ç†"
msgid "ciReport|No changes to code quality"
msgstr "コードå“質ã«å¤‰æ›´ã¯ã‚ã‚Šã¾ã›ã‚“"
@@ -24162,17 +24707,23 @@ msgid "ciReport|No changes to performance metrics"
msgstr "パーフォーマンスメトリクスã«å¤‰æ›´ã¯ã‚ã‚Šã¾ã›ã‚“"
msgid "ciReport|No code quality issues found"
-msgstr ""
+msgstr "コードå“質ã®èª²é¡Œã¯è¦‹ã¤ã‹ã‚Šã¾ã›ã‚“ã§ã—ãŸ"
msgid "ciReport|Performance metrics"
msgstr "パフォーマンスメトリクス"
msgid "ciReport|Resolve with merge request"
-msgstr ""
+msgstr "マージリクエストã§è§£æ±º"
msgid "ciReport|SAST"
msgstr "SAST"
+msgid "ciReport|Secret scanning"
+msgstr ""
+
+msgid "ciReport|Secret scanning detects secrets and credentials vulnerabilities in your source code."
+msgstr ""
+
msgid "ciReport|Security scanning"
msgstr "セキュリティスキャン"
@@ -24180,7 +24731,7 @@ msgid "ciReport|Security scanning failed loading any results"
msgstr "セキュリティスキャンã¯çµæžœã®èª­ã¿è¾¼ã¿ã«å¤±æ•—ã—ã¾ã—ãŸ"
msgid "ciReport|Solution"
-msgstr ""
+msgstr "ソリューション"
msgid "ciReport|Static Application Security Testing (SAST) detects known vulnerabilities in your source code."
msgstr "é™çš„アプリケーションセキュリティテスト(SAST)ã¯ã€ã‚½ãƒ¼ã‚¹ã‚³ãƒ¼ãƒ‰ä¸­ã®æ—¢çŸ¥ã®è„†å¼±æ€§ã‚’検出ã—ã¾ã™ã€‚"
@@ -24189,7 +24740,7 @@ msgid "ciReport|There was an error creating the issue. Please try again."
msgstr "課題を作æˆä¸­ã«ã‚¨ãƒ©ãƒ¼ãŒç™ºç”Ÿã—ã¾ã—ãŸã€‚ã‚‚ã†ä¸€åº¦ã‚„ã‚Šç›´ã—ã¦ãã ã•ã„。"
msgid "ciReport|There was an error creating the merge request. Please try again."
-msgstr ""
+msgstr "マージリクエストã®ä½œæˆä¸­ã«ã‚¨ãƒ©ãƒ¼ãŒç™ºç”Ÿã—ã¾ã—ãŸã€‚ã‚‚ã†ä¸€åº¦ã‚„ã‚Šç›´ã—ã¦ãã ã•ã„。"
msgid "ciReport|There was an error dismissing the vulnerability. Please try again."
msgstr "脆弱性を無視ã™ã‚‹éš›ã«ã‚¨ãƒ©ãƒ¼ãŒç™ºç”Ÿã—ã¾ã—ãŸã€‚ã‚‚ã†ä¸€åº¦ã‚„ã‚Šç›´ã—ã¦ãã ã•ã„。"
@@ -24208,13 +24759,13 @@ msgid "ciReport|View full report"
msgstr "レãƒãƒ¼ãƒˆå…¨ä½“を見る"
msgid "comment"
-msgstr ""
+msgstr "コメント"
msgid "commented on %{link_to_project}"
-msgstr ""
+msgstr "%{link_to_project} ã«ã¤ã„ã¦ã‚³ãƒ¡ãƒ³ãƒˆã—ã¾ã—ãŸ"
msgid "commit %{commit_id}"
-msgstr ""
+msgstr "コミット %{commit_id}"
msgid "confidentiality|You are going to turn off the confidentiality. This means <strong>everyone</strong> will be able to see and leave a comment on this issue."
msgstr "ã‚ãªãŸã¯å…¬é–‹è¨­å®šã«å¤‰æ›´ã—よã†ã¨ã—ã¦ã„ã¾ã™ã€‚ã“ã‚Œã¯<strong>ã™ã¹ã¦ã®äºº</strong> ãŒé–²è¦§å¯èƒ½ã«ãªã‚Šã€èª²é¡Œã«å¯¾ã—ã¦ã‚³ãƒ¡ãƒ³ãƒˆã‚’残ã™ã“ã¨ãŒã§ãるよã†ã«ãªã‚‹ã“ã¨ã‚’æ„味ã—ã¾ã™ã€‚"
@@ -24226,13 +24777,13 @@ msgid "connecting"
msgstr "接続中"
msgid "container_name cannot be larger than %{max_length} chars"
-msgstr ""
+msgstr "container_name ã« %{max_length} 以上ã®æ–‡å­—列を付与ã™ã‚‹ã“ã¨ã¯ã§ãã¾ã›ã‚“。"
msgid "could not read private key, is the passphrase correct?"
msgstr "秘密éµã‚’読ã¿å–ã‚‹ã“ã¨ãŒã§ãã¾ã›ã‚“ã§ã—ãŸã€‚パスフレーズã¯æ­£ã—ã„ã§ã™ã‹ï¼Ÿ"
msgid "created"
-msgstr ""
+msgstr "作æˆæ¸ˆã¿"
msgid "created %{timeAgo}"
msgstr ""
@@ -24241,20 +24792,20 @@ msgid "customize"
msgstr "カスタマイズ"
msgid "date must not be after 9999-12-31"
-msgstr ""
+msgstr "日付㯠9999-12-31 よりå‰ã‚’指定ã—ã¦ä¸‹ã•ã„"
msgid "day"
msgid_plural "days"
msgstr[0] "æ—¥"
msgid "default branch"
-msgstr ""
+msgstr "デフォルトブランãƒ"
msgid "deleted"
-msgstr ""
+msgstr "削除完了"
msgid "deploy"
-msgstr ""
+msgstr "デプロイ"
msgid "design"
msgstr ""
@@ -24276,16 +24827,16 @@ msgstr "完了"
msgid "draft"
msgid_plural "drafts"
-msgstr[0] ""
+msgstr[0] "ドラフト"
msgid "e.g. %{token}"
msgstr ""
msgid "element is not a hierarchy"
-msgstr ""
+msgstr "è¦ç´ ã¯éšŽå±¤ã§ã¯ã‚ã‚Šã¾ã›ã‚“"
msgid "email '%{email}' does not match the allowed domain of '%{email_domain}'"
-msgstr ""
+msgstr "メールアドレス %{email} 㯠許å¯ãƒ‰ãƒ¡ã‚¤ãƒ³ã® %{email_domain} ã¨ä¸€è‡´ã—ã¾ã›ã‚“"
msgid "enabled"
msgstr "有効"
@@ -24294,13 +24845,13 @@ msgid "encrypted: needs to be a :required, :optional or :migrating!"
msgstr ""
msgid "entries cannot be larger than 255 characters"
-msgstr ""
+msgstr "エントリーã¯255文字を超ãˆã‚‰ã‚Œã¾ã›ã‚“。"
msgid "entries cannot be nil"
-msgstr ""
+msgstr "エントリをnilã«ã™ã‚‹ã“ã¨ã¯ã§ãã¾ã›ã‚“"
msgid "entries cannot contain HTML tags"
-msgstr ""
+msgstr "エントリã«HTMLã‚¿ã‚°ã‚’å«ã‚られã¾ã›ã‚“。"
msgid "error"
msgstr "エラー"
@@ -24318,16 +24869,16 @@ msgid "exceeds the limit of %{bytes} bytes for directory name \"%{dirname}\""
msgstr ""
msgid "expired on %{milestone_due_date}"
-msgstr ""
+msgstr "%{milestone_due_date} ã«æœŸé™åˆ‡ã‚Œ"
msgid "expires on %{milestone_due_date}"
-msgstr ""
+msgstr "%{milestone_due_date} ã«æœŸé™åˆ‡ã‚Œ"
msgid "external_url"
msgstr ""
msgid "failed"
-msgstr ""
+msgstr "失敗ã—ã¾ã—ãŸ"
msgid "failed to dismiss associated finding(id=%{finding_id}): %{message}"
msgstr ""
@@ -24340,7 +24891,7 @@ msgid "finding is not found or is already attached to a vulnerability"
msgstr ""
msgid "for %{link_to_merge_request} with %{link_to_merge_request_source_branch}"
-msgstr ""
+msgstr "%{link_to_merge_request} ã¨%{link_to_merge_request_source_branch} ã®å ´åˆ"
msgid "for %{link_to_merge_request} with %{link_to_merge_request_source_branch} into %{link_to_merge_request_target_branch}"
msgstr ""
@@ -24355,17 +24906,20 @@ msgid "for this project"
msgstr "ã“ã®ãƒ—ロジェクトã§ã¯"
msgid "fork this project"
-msgstr ""
+msgstr "ã“ã®ãƒ—ロジェクトをフォーク"
msgid "from"
msgstr ""
msgid "geo_node_name"
-msgstr ""
+msgstr "geo_node_name"
msgid "group"
msgstr "グループ"
+msgid "groups"
+msgstr ""
+
msgid "has already been linked to another vulnerability"
msgstr ""
@@ -24373,7 +24927,7 @@ msgid "has already been taken"
msgstr ""
msgid "help"
-msgstr ""
+msgstr "ヘルプ"
msgid "here"
msgstr "ã“ã“"
@@ -24394,7 +24948,7 @@ msgid "in group %{link_to_group}"
msgstr ""
msgid "in project %{link_to_project}"
-msgstr ""
+msgstr "プロジェクト %{link_to_project}"
msgid "index"
msgstr "インデックス"
@@ -24404,7 +24958,7 @@ msgid_plural "instances completed"
msgstr[0] "インスタンスãŒå®Œäº†ã—ã¾ã—ãŸ"
msgid "invalid milestone state `%{state}`"
-msgstr ""
+msgstr "無効ãªãƒžã‚¤ãƒ«ã‚¹ãƒˆãƒ¼ãƒ³çŠ¶æ…‹ `%{state}`"
msgid "is"
msgstr ""
@@ -24419,19 +24973,19 @@ msgid "is blocked by"
msgstr ""
msgid "is enabled."
-msgstr ""
+msgstr "有効ã«ãªã‚Šã¾ã—ãŸ"
msgid "is invalid because there is downstream lock"
-msgstr ""
+msgstr "ã¯ãƒ€ã‚¦ãƒ³ãƒ­ãƒ¼ãƒ‰ã‚¹ãƒˆãƒªãƒ¼ãƒ ãŒãƒ­ãƒƒã‚¯ã•ã‚Œã¦ã„ã‚‹ãŸã‚無効ã§ã™"
msgid "is invalid because there is upstream lock"
-msgstr ""
+msgstr "ã¯ã‚¢ãƒƒãƒ—ストリームãŒãƒ­ãƒƒã‚¯ã•ã‚Œã¦ã„ã‚‹ãŸã‚無効ã§ã™"
msgid "is not"
msgstr ""
msgid "is not a descendant of the Group owning the template"
-msgstr ""
+msgstr "ã¯ãƒ†ãƒ³ãƒ—レートを利用ã§ãるグループã«æ‰€å±žã—ã¦ã„ã¾ã›ã‚“。"
msgid "is not a valid X509 certificate."
msgstr "ã¯æœ‰åŠ¹ãª X509 証明書ã§ã¯ã‚ã‚Šã¾ã›ã‚“。"
@@ -24440,19 +24994,19 @@ msgid "is not allowed. Try again with a different email address, or contact your
msgstr ""
msgid "is not an email you own"
-msgstr ""
+msgstr "ã‚ãªãŸãŒæ‰€æœ‰ã™ã‚‹ãƒ¡ãƒ¼ãƒ«ã‚¢ãƒ‰ãƒ¬ã‚¹ã§ã¯ã‚ã‚Šã¾ã›ã‚“"
msgid "is not in the group enforcing Group Managed Account"
msgstr ""
msgid "is too long (%{current_value}). The maximum size is %{max_size}."
-msgstr ""
+msgstr "é•·ã™ãŽã¾ã™(%{current_value})。最大サイズ㯠%{max_size} ã§ã™ã€‚"
msgid "is too long (maximum is 100 entries)"
-msgstr ""
+msgstr "ãŒé•·ã™ãŽã¾ã™ (最大100エントリ)"
msgid "is too long (maximum is 1000 entries)"
-msgstr ""
+msgstr "ãŒé•·ã™ãŽã¾ã™ï¼ˆæœ€å¤§1000エントリ)"
msgid "issue"
msgstr "課題"
@@ -24467,16 +25021,16 @@ msgid "issues on track"
msgstr ""
msgid "it is stored externally"
-msgstr ""
+msgstr "外部ã«ä¿å­˜ã•ã‚Œã¦ã„ã¾ã™"
msgid "it is stored in LFS"
-msgstr ""
+msgstr "LFS ã«ä¿ç®¡ã—ã¦ã„ã‚‹"
msgid "it is too large"
-msgstr ""
+msgstr "大ãã™ãŽã§ã™"
msgid "jigsaw is not defined"
-msgstr ""
+msgstr "jigsaw ãŒæœªå®šç¾©ã§ã™"
msgid "jira.issue.description.content"
msgstr ""
@@ -24494,7 +25048,7 @@ msgid "latest version"
msgstr "最新ãƒãƒ¼ã‚¸ãƒ§ãƒ³"
msgid "leave %{group_name}"
-msgstr ""
+msgstr "%{group_name} を離れる"
msgid "less than a minute"
msgstr ""
@@ -24503,22 +25057,22 @@ msgid "level: %{level}"
msgstr ""
msgid "limit of %{project_limit} reached"
-msgstr ""
+msgstr "%{project_limit} ã®åˆ¶é™ã«é”ã—ã¾ã—ãŸ"
msgid "locked by %{path_lock_user_name} %{created_at}"
msgstr "%{path_lock_user_name} ã«ã‚ˆã£ã¦ %{created_at} ã«ãƒ­ãƒƒã‚¯ã•ã‚Œã¦ã„ã¾ã™ã€‚"
msgid "log in"
-msgstr ""
+msgstr "ログイン"
msgid "manual"
msgstr "マニュアル"
msgid "math|The math in this entry is taking too long to render and may not be displayed as expected. For performance reasons, math blocks are also limited to %{maxChars} characters. Consider splitting up large formulae, splitting math blocks among multiple entries, or using an image instead."
-msgstr ""
+msgstr "ã“ã®ã‚¨ãƒ³ãƒˆãƒªã® math ã¯ãƒ¬ãƒ³ãƒ€ãƒªãƒ³ã‚°ã«æ™‚é–“ãŒã‹ã‹ã‚Šã™ãŽã¦ã€æœŸå¾…ã©ãŠã‚Šã«è¡¨ç¤ºã•ã‚Œãªã„å ´åˆãŒã‚ã‚Šã¾ã™ã€‚ パフォーマンス上ã®ç†ç”±ã‹ã‚‰ã€math ブロックも%{maxChars} 文字ã«åˆ¶é™ã•ã‚Œã¦ã„ã¾ã™ã€‚ 大ããªæ•°å¼ã‚’分割ã™ã‚‹ã€è¤‡æ•°ã®ã‚¨ãƒ³ãƒˆãƒªé–“ã§æ•°å­¦ãƒ–ロックを分割ã™ã‚‹ã€ã¾ãŸã¯ä»£ã‚ã‚Šã«ç”»åƒã‚’使用ã™ã‚‹ã“ã¨ã‚’検討ã—ã¦ãã ã•ã„。"
msgid "math|There was an error rendering this math block"
-msgstr ""
+msgstr "ã“ã® math ブロックã®ãƒ¬ãƒ³ãƒ€ãƒªãƒ³ã‚°ä¸­ã«ã‚¨ãƒ©ãƒ¼ãŒç™ºç”Ÿã—ã¾ã—ãŸ"
msgid "merge request"
msgid_plural "merge requests"
@@ -24527,38 +25081,35 @@ msgstr[0] "マージリクエスト"
msgid "merged %{time_ago}"
msgstr ""
-msgid "milestone should belong either to a project or a group."
-msgstr ""
-
msgid "missing"
-msgstr ""
+msgstr "見ã¤ã‹ã‚Šã¾ã›ã‚“"
msgid "most recent deployment"
msgstr ""
msgid "mrWidgetCommitsAdded|%{commitCount} and %{mergeCommitCount} will be added to %{targetBranch}."
-msgstr ""
+msgstr "%{commitCount} 㨠%{mergeCommitCount} ã‚’ %{targetBranch} ã«è¿½åŠ ã™ã‚‹ã€‚"
msgid "mrWidgetCommitsAdded|%{commitCount} will be added to %{targetBranch}."
-msgstr ""
+msgstr "%{commitCount} ã‚’ %{targetBranch} ã«è¿½åŠ ã™ã‚‹ã€‚"
msgid "mrWidgetCommitsAdded|1 merge commit"
-msgstr ""
+msgstr "1マージコミット"
msgid "mrWidgetNothingToMerge|Currently there are no changes in this merge request's source branch. Please push new commits or use a different branch."
-msgstr ""
+msgstr "ç¾åœ¨ã€ã“ã®ãƒžãƒ¼ã‚¸ãƒªã‚¯ã‚¨ã‚¹ãƒˆã®ã‚½ãƒ¼ã‚¹ãƒ–ランãƒã«å¤‰æ›´ã¯ã‚ã‚Šã¾ã›ã‚“。新ã—ãコミットをプッシュã™ã‚‹ã‹ã€åˆ¥ã®ãƒ–ランãƒã‚’使用ã—ã¦ãã ã•ã„。"
msgid "mrWidgetNothingToMerge|Interested parties can even contribute by pushing commits if they want to."
-msgstr ""
+msgstr "貢献をã—ãŸã„関係者ã¯ã€ã‚³ãƒŸãƒƒãƒˆã‚’プッシュã—ã¦è²¢çŒ®ã§ãã¾ã™ã€‚"
msgid "mrWidgetNothingToMerge|Merge requests are a place to propose changes you have made to a project and discuss those changes with others."
-msgstr ""
+msgstr "マージリクエストã¯ã€ãƒ—ロジェクトã«åŠ ãˆãŸã‚ãªãŸã®å¤‰æ›´ã‚’æ案ã—ã€ã¾ãŸãã®å¤‰æ›´ã‚’ä»–ã®ãƒ¡ãƒ³ãƒãƒ¼ã¨è©±ã—åˆã†ãŸã‚ã®å ´æ‰€ã§ã™ã€‚"
msgid "mrWidget| Please restore it or use a different %{missingBranchName} branch"
msgstr "ブランãƒã‚’復元ã™ã‚‹ã‹ã€åˆ¥ã® %{missingBranchName} ブランãƒã‚’使用ã—ã¦ãã ã•ã„"
msgid "mrWidget|%{link_start}Learn more about resolving conflicts%{link_end}"
-msgstr ""
+msgstr "%{link_start}競åˆã®è§£æ±ºã®è©³ç´°%{link_end}"
msgid "mrWidget|%{metricsLinkStart} Memory %{metricsLinkEnd} usage %{emphasisStart} decreased %{emphasisEnd} from %{memoryFrom}MB to %{memoryTo}MB"
msgstr "%{metricsLinkStart} メモリ %{metricsLinkEnd} 使用率㌠%{memoryFrom} MBã‹ã‚‰ %{memoryTo} MB㸠%{emphasisStart} 減少 %{emphasisEnd}"
@@ -24573,7 +25124,7 @@ msgid "mrWidget|%{prefixToLinkStart}No pipeline%{prefixToLinkEnd} %{addPipelineL
msgstr ""
msgid "mrWidget|Added to the merge train by"
-msgstr ""
+msgstr "次ã®ãƒ¦ãƒ¼ã‚¶ã«ã‚ˆã£ã¦ãƒžãƒ¼ã‚¸ãƒˆãƒ¬ã‚¤ãƒ³ã«è¿½åŠ ã•ã‚Œã¾ã—ãŸï¼š"
msgid "mrWidget|Allows commits from members who can merge to the target branch"
msgstr "ターゲットブランãƒã«ãƒžãƒ¼ã‚¸ã§ãるメンãƒãƒ¼ã‹ã‚‰ã®ã‚³ãƒŸãƒƒãƒˆã‚’許å¯ã™ã‚‹"
@@ -24594,7 +25145,7 @@ msgid "mrWidget|Approve"
msgstr "承èª"
msgid "mrWidget|Approve additionally"
-msgstr ""
+msgstr "追加ã§æ‰¿èªã™ã‚‹"
msgid "mrWidget|Approved by"
msgstr "承èªè€…"
@@ -24651,7 +25202,7 @@ msgid "mrWidget|Fast-forward merge is not possible. To merge this request, first
msgstr "æ—©é€ã‚Šãƒžãƒ¼ã‚¸ã¯ã§ãã¾ã›ã‚“。ã“ã®è¦æ±‚をマージã™ã‚‹ã«ã¯ã€ã¾ãšãƒ­ãƒ¼ã‚«ãƒ«ã‚’ rebase ã—ã¾ã™ã€‚"
msgid "mrWidget|Fork merge requests do not create merge request pipelines which validate a post merge result"
-msgstr ""
+msgstr "フォークマージリクエストã¯ãƒã‚¹ãƒˆãƒžãƒ¼ã‚¸çµæžœã‚’検証ã™ã‚‹ãƒžãƒ¼ã‚¸ãƒªã‚¯ã‚¨ã‚¹ãƒˆãƒ‘イプラインを作æˆã—ã¾ã›ã‚“"
msgid "mrWidget|If the %{branch} branch exists in your local repository, you can merge this merge request manually using the"
msgstr "%{branch} ブランãƒãŒãƒ­ãƒ¼ã‚«ãƒ«ãƒªãƒã‚¸ãƒˆãƒªã«å­˜åœ¨ã™ã‚‹å ´åˆã¯ã€ã“ã®ãƒžãƒ¼ã‚¸ãƒªã‚¯ã‚¨ã‚¹ãƒˆã‚’手動ã§ãƒžãƒ¼ã‚¸ã™ã‚‹ã“ã¨ãŒã§ãã¾ã™ã€‚"
@@ -24675,22 +25226,22 @@ msgid "mrWidget|Merge failed."
msgstr "マージã«å¤±æ•—ã—ã¾ã—ãŸã€‚"
msgid "mrWidget|Merge failed: %{mergeError}. Please try again."
-msgstr ""
+msgstr "マージã«å¤±æ•—ã—ã¾ã—ãŸ: %{mergeError}ã€ã‚‚ã†ä¸€åº¦ã‚„ã‚Šç›´ã—ã¦ãã ã•ã„。"
msgid "mrWidget|Merge locally"
msgstr "ローカルã§ãƒžãƒ¼ã‚¸"
msgid "mrWidget|Merge request approved."
-msgstr ""
+msgstr "マージリクエストãŒæ‰¿èªã•ã‚Œã¾ã—ãŸ."
msgid "mrWidget|Merged by"
msgstr "マージ作業者"
msgid "mrWidget|More information"
-msgstr ""
+msgstr "詳ã—ã„情報"
msgid "mrWidget|No approval required"
-msgstr ""
+msgstr "承èªã¯ä¸è¦"
msgid "mrWidget|No approval required; you can still approve"
msgstr "承èªã¯ä¸è¦ã§ã™ãŒã€æ‰¿èªã™ã‚‹ã“ã¨ã‚‚ã§ãã¾ã™"
@@ -24717,13 +25268,13 @@ msgid "mrWidget|Refreshing now"
msgstr "更新中"
msgid "mrWidget|Remove from merge train"
-msgstr ""
+msgstr "マージトレインã‹ã‚‰å‰Šé™¤"
msgid "mrWidget|Request to merge"
msgstr "マージをリクエスト"
msgid "mrWidget|Resolve WIP status"
-msgstr ""
+msgstr "作業中状態を解決ã™ã‚‹"
msgid "mrWidget|Resolve conflicts"
msgstr "競åˆã‚’解決ã™ã‚‹"
@@ -24762,7 +25313,7 @@ msgid "mrWidget|The source branch has been deleted"
msgstr "ã“ã®ã‚½ãƒ¼ã‚¹ãƒ–ランãƒã¯å‰Šé™¤ã•ã‚Œã¾ã—ãŸ"
msgid "mrWidget|The source branch is %{commitsBehindLinkStart}%{commitsBehind}%{commitsBehindLinkEnd} the target branch"
-msgstr ""
+msgstr "ソースブランãƒã¯ %{commitsBehindLinkStart}%{commitsBehind}%{commitsBehindLinkEnd} ターゲットブランãƒã§ã™"
msgid "mrWidget|The source branch is being deleted"
msgstr "ã“ã®ã‚½ãƒ¼ã‚¹ãƒ–ランãƒã¯å‰Šé™¤ã•ã‚Œã‚ˆã†ã¨ã—ã¦ã„ã¾ã™"
@@ -24777,10 +25328,10 @@ msgid "mrWidget|There are merge conflicts"
msgstr "マージã®ç«¶åˆãŒã‚ã‚Šã¾ã™"
msgid "mrWidget|There are unresolved threads. Please resolve these threads"
-msgstr ""
+msgstr "未解決ã®ã‚¹ãƒ¬ãƒƒãƒ‰ãŒã‚ã‚Šã¾ã™ã€‚ã“れらã®ã‚¹ãƒ¬ãƒƒãƒ‰ã‚’解決ã—ã¦ãã ã•ã„。"
msgid "mrWidget|This feature merges changes from the target branch to the source branch. You cannot use this feature since the source branch is protected."
-msgstr ""
+msgstr "ã“ã®æ©Ÿèƒ½ã¯ã€ã‚¿ãƒ¼ã‚²ãƒƒãƒˆãƒ–ランãƒã‹ã‚‰ã‚½ãƒ¼ã‚¹ãƒ–ランãƒã¸ã®å¤‰æ›´ã‚’マージã—ã¾ã™ã€‚ã“ã®ã‚½ãƒ¼ã‚¹ãƒ–ランãƒã¯ä¿è­·ã•ã‚Œã¦ã„ã‚‹ãŸã‚ã€ã‚ãªãŸã¯ã“ã®æ©Ÿèƒ½ã‚’使用ã§ãã¾ã›ã‚“。"
msgid "mrWidget|This merge request failed to be merged automatically"
msgstr "ã“ã®ãƒžãƒ¼ã‚¸ãƒªã‚¯ã‚¨ã‚¹ãƒˆã¯è‡ªå‹•çš„ã«ãƒžãƒ¼ã‚¸ã•ã‚Œã¾ã›ã‚“ã§ã—ãŸ"
@@ -24789,19 +25340,19 @@ msgid "mrWidget|This merge request is in the process of being merged"
msgstr "ã“ã®ãƒžãƒ¼ã‚¸ãƒªã‚¯ã‚¨ã‚¹ãƒˆã¯ãƒžãƒ¼ã‚¸å®Ÿè¡Œä¸­ã§ã™"
msgid "mrWidget|This merge request will be added to the merge train when pipeline %{linkStart}#%{pipelineId}%{linkEnd} succeeds."
-msgstr ""
+msgstr "ã“ã®ãƒžãƒ¼ã‚¸ãƒªã‚¯ã‚¨ã‚¹ãƒˆã‚’ã€ãƒ‘イプライン %{linkStart}#%{pipelineId}%{linkEnd} ãŒæˆåŠŸã—ãŸã¨ãã«ãƒžãƒ¼ã‚¸ãƒˆãƒ¬ã‚¤ãƒ³ã¸è¿½åŠ ã—ã¾ã™ã€‚"
msgid "mrWidget|This merge request will start a merge train when pipeline %{linkStart}#%{pipelineId}%{linkEnd} succeeds."
-msgstr ""
+msgstr "ã“ã®ãƒžãƒ¼ã‚¸ãƒªã‚¯ã‚¨ã‚¹ãƒˆã¯ã€ãƒ‘イプライン %{linkStart}#%{pipelineId}%{linkEnd} ãŒæˆåŠŸã—ãŸã¨ãã«ãƒžãƒ¼ã‚¸ãƒˆãƒ¬ã‚¤ãƒ³ã‚’開始ã—ã¾ã™ã€‚"
msgid "mrWidget|This project is archived, write access has been disabled"
msgstr "ã“ã®ãƒ—ロジェクトã¯ã‚¢ãƒ¼ã‚«ã‚¤ãƒ–ã•ã‚Œã¦ã„ã‚‹ãŸã‚ã€æ›¸ãè¾¼ã¿ã¯ç„¡åŠ¹ã§ã™ã€‚"
msgid "mrWidget|To approve this merge request, please enter your password. This project requires all approvals to be authenticated."
-msgstr ""
+msgstr "ã“ã®ãƒžãƒ¼ã‚¸ãƒªã‚¯ã‚¨ã‚¹ãƒˆã‚’承èªã™ã‚‹ã«ã¯ã€ãƒ‘スワードを入力ã—ã¦ãã ã•ã„。ã“ã®ãƒ—ロジェクトã§ã¯ã€ã™ã¹ã¦ã®æ‰¿èªãŒèªè¨¼ã•ã‚Œã¦ã„ãªã‘ã‚Œã°ãªã‚Šã¾ã›ã‚“。"
msgid "mrWidget|When this merge request is ready, remove the WIP: prefix from the title to allow it to be merged"
-msgstr ""
+msgstr "ã“ã®ãƒžãƒ¼ã‚¸ãƒªã‚¯ã‚¨ã‚¹ãƒˆã«æº–å‚™ãŒã§ããŸã‚‰ã€ã‚¿ã‚¤ãƒˆãƒ«ã®å…ˆé ­ã‹ã‚‰ WIP: を削除ã—ã¦ãƒžãƒ¼ã‚¸ã§ãるよã†ã«ã—ã¾ã™"
msgid "mrWidget|You are not allowed to edit this project directly. Please fork to make changes."
msgstr "ã“ã®ãƒ—ロジェクトを直接編集ã™ã‚‹ã“ã¨ã¯ã§ãã¾ã›ã‚“。変更ã™ã‚‹ã«ã¯ãƒ•ã‚©ãƒ¼ã‚¯ã—ã¦ãã ã•ã„。"
@@ -24813,10 +25364,10 @@ msgid "mrWidget|You can merge this merge request manually using the"
msgstr "ã“ã®ãƒžãƒ¼ã‚¸ãƒªã‚¯ã‚¨ã‚¹ãƒˆã‚’手動ã§ãƒžãƒ¼ã‚¸ã§ãã¾ã™"
msgid "mrWidget|Your password"
-msgstr ""
+msgstr "パスワード"
msgid "mrWidget|a quick guide that'll show you how to create"
-msgstr ""
+msgstr "作æˆã™ã‚‹æ–¹æ³•ã®ã‚¯ã‚¤ãƒƒã‚¯ã‚¬ã‚¤ãƒ‰"
msgid "mrWidget|branch does not exist."
msgstr "ブランãƒãŒå­˜åœ¨ã—ã¾ã›ã‚“。"
@@ -24831,25 +25382,25 @@ msgid "mrWidget|one. Make your code more secure and more"
msgstr ""
msgid "mrWidget|robust in just a minute."
-msgstr ""
+msgstr "ãŸã£ãŸ1分ã§å …牢ã«ã€‚"
msgid "mrWidget|that continuously tests your code. We created"
msgstr ""
msgid "mrWidget|to be added to the merge train when the pipeline succeeds"
-msgstr ""
+msgstr "パイプラインãŒæˆåŠŸã—ãŸã¨ãã«ãƒžãƒ¼ã‚¸ãƒˆãƒ¬ã‚¤ãƒ³ã«è¿½åŠ ã•ã‚Œã‚‹"
msgid "mrWidget|to be merged automatically when the pipeline succeeds"
msgstr "パイプラインãŒæˆåŠŸã—ãŸã¨ãã¯è‡ªå‹•çš„ã«ãƒžãƒ¼ã‚¸ã•ã‚Œã¾ã™"
msgid "mrWidget|to start a merge train when the pipeline succeeds"
-msgstr ""
+msgstr "パイプラインãŒæˆåŠŸã—ãŸã¨ãã«ãƒžãƒ¼ã‚¸ãƒˆãƒ¬ã‚¤ãƒ³ã‚’開始ã™ã‚‹"
msgid "must be greater than start date"
-msgstr ""
+msgstr "開始日より後ã«ã—ã¦ãã ã•ã„。"
msgid "n/a"
-msgstr ""
+msgstr "利用ä¸å¯"
msgid "need attention"
msgstr ""
@@ -24866,14 +25417,17 @@ msgstr "æ–°è¦ãƒžãƒ¼ã‚¸ãƒªã‚¯ã‚¨ã‚¹ãƒˆ"
msgid "no contributions"
msgstr "貢献ãªã—"
-msgid "no one can merge"
+msgid "no expiration"
msgstr ""
+msgid "no one can merge"
+msgstr "誰もマージã§ãã¾ã›ã‚“"
+
msgid "none"
msgstr "ãªã—"
msgid "not found"
-msgstr ""
+msgstr "見ã¤ã‹ã‚Šã¾ã›ã‚“ã§ã—ãŸ"
msgid "notification emails"
msgstr "メール通知"
@@ -24891,7 +25445,7 @@ msgid "on track"
msgstr ""
msgid "opened %{timeAgoString} by %{user}"
-msgstr ""
+msgstr "%{user} ㌠%{timeAgoString} を開始ã—ã¾ã—ãŸã€‚"
msgid "opened %{timeAgo}"
msgstr ""
@@ -24901,7 +25455,7 @@ msgstr ""
msgid "out of %d total test"
msgid_plural "out of %d total tests"
-msgstr[0] ""
+msgstr[0] "テスト全件数 %d ã®ã†ã¡"
msgid "parent"
msgid_plural "parents"
@@ -24911,20 +25465,23 @@ msgid "password"
msgstr "パスワード"
msgid "pending comment"
-msgstr ""
+msgstr "ä¿ç•™ä¸­ã®ã‚³ãƒ¡ãƒ³ãƒˆ 件"
msgid "pending removal"
msgstr ""
-msgid "pipeline"
+msgid "per day"
msgstr ""
+msgid "pipeline"
+msgstr "パイプライン"
+
msgid "pod_name cannot be larger than %{max_length} chars"
-msgstr ""
+msgstr "pod_name ã« %{max_length} 以上ã®æ–‡å­—列を付与ã™ã‚‹ã“ã¨ã¯ã§ãã¾ã›ã‚“。"
msgid "point"
msgid_plural "points"
-msgstr[0] ""
+msgstr[0] "ãƒã‚¤ãƒ³ãƒˆ"
msgid "private"
msgstr "プライベート"
@@ -24933,13 +25490,19 @@ msgid "private key does not match certificate."
msgstr "秘密éµãŒè¨¼æ˜Žæ›¸ã¨ä¸€è‡´ã—ã¾ã›ã‚“。"
msgid "processing"
-msgstr ""
+msgstr "処ç†ä¸­"
msgid "project"
msgid_plural "projects"
msgstr[0] "プロジェクト"
msgid "project avatar"
+msgstr "プロジェクトアãƒã‚¿ãƒ¼"
+
+msgid "projects"
+msgstr ""
+
+msgid "push to your repository, create pipelines, create issues or add comments. To reduce storage capacity, delete unused repositories, artifacts, wikis, issues, and pipelines."
msgstr ""
msgid "quick actions"
@@ -24952,7 +25515,7 @@ msgid "relates to"
msgstr ""
msgid "released %{time}"
-msgstr ""
+msgstr "%{time} ã«ãƒªãƒªãƒ¼ã‚¹"
msgid "remaining"
msgstr "残り"
@@ -24967,7 +25530,7 @@ msgid "remove weight"
msgstr "ウェイトを削除"
msgid "removed a Zoom call from this issue"
-msgstr ""
+msgstr "ã“ã® issue ã‹ã‚‰ã‚ºãƒ¼ãƒ ã‚³ãƒ¼ãƒ«ã‚’削除ã—ã¾ã—ãŸã€‚"
msgid "rendered diff"
msgstr "差分を表示"
@@ -24977,7 +25540,7 @@ msgid_plural "replies"
msgstr[0] "返信"
msgid "reset it."
-msgstr ""
+msgstr "リセットã—ã¾ã™ã€‚"
msgid "resolved the corresponding error and closed the issue."
msgstr ""
@@ -24992,28 +25555,28 @@ msgid "settings saved, but not activated"
msgstr ""
msgid "severity|Critical"
-msgstr ""
+msgstr "致命的"
msgid "severity|High"
-msgstr ""
+msgstr "高"
msgid "severity|Info"
-msgstr ""
+msgstr "情報"
msgid "severity|Low"
-msgstr ""
+msgstr "低"
msgid "severity|Medium"
-msgstr ""
+msgstr "中"
msgid "severity|None"
msgstr ""
msgid "severity|Unknown"
-msgstr ""
+msgstr "ä¸æ˜Ž"
msgid "should be greater than or equal to %{access} inherited membership from group %{group_name}"
-msgstr ""
+msgstr "%{group_name} グループã‹ã‚‰ç¶™æ‰¿ã•ã‚ŒãŸãƒ¡ãƒ³ãƒãƒ¼ã‚·ãƒƒãƒ—㯠%{access} ã¨åŒç­‰ã¾ãŸã¯ãれ以上ã§ã‚ã‚‹å¿…è¦ãŒã‚ã‚Šã¾ã™"
msgid "show %{count} more"
msgstr ""
@@ -25028,7 +25591,7 @@ msgid "sign in"
msgstr "サインイン"
msgid "sort:"
-msgstr ""
+msgstr "並ã¹æ›¿ãˆ:"
msgid "source"
msgstr "ソース"
@@ -25037,7 +25600,7 @@ msgid "source diff"
msgstr "ソース差分"
msgid "specified top is not part of the tree"
-msgstr ""
+msgstr "指定ã•ã‚ŒãŸãƒˆãƒƒãƒ—ã¯ã€ãƒ„リーã®ä¸€éƒ¨ã§ã¯ã‚ã‚Šã¾ã›ã‚“"
msgid "spendCommand|%{slash_command} will update the sum of the time spent."
msgstr "%{slash_command} ã¯ç´¯è¨ˆçµŒéŽæ™‚é–“ã‚’æ›´æ–°ã—ã¾ã™"
@@ -25046,19 +25609,19 @@ msgid "started"
msgstr "開始"
msgid "started a discussion on %{design_link}"
-msgstr ""
+msgstr "%{design_link} ã§ãƒ‡ã‚£ã‚¹ã‚«ãƒƒã‚·ãƒ§ãƒ³ã‚’始ã‚ã¾ã—ãŸ"
msgid "started on %{milestone_start_date}"
-msgstr ""
+msgstr "%{milestone_start_date} ã«é–‹å§‹ã—ã¾ã—ãŸ"
msgid "starts on %{milestone_start_date}"
-msgstr ""
+msgstr "%{milestone_start_date} ã«é–‹å§‹"
msgid "stuck"
msgstr "スタック"
msgid "success"
-msgstr ""
+msgstr "æˆåŠŸ"
msgid "suggestPipeline|1/2: Choose a template"
msgstr ""
@@ -25073,10 +25636,10 @@ msgid "suggestPipeline|We recommend the %{boldStart}Code Quality%{boldEnd} templ
msgstr ""
msgid "syntax is correct"
-msgstr ""
+msgstr "構文ã¯æ­£ã—ã„ã§ã™"
msgid "syntax is incorrect"
-msgstr ""
+msgstr "構文ãŒé–“é•ã£ã¦ã„ã¾ã™ã€‚"
msgid "tag name"
msgstr ""
@@ -25087,10 +25650,6 @@ msgstr ""
msgid "this document"
msgstr "ã“ã®ãƒ‰ã‚­ãƒ¥ãƒ¡ãƒ³ãƒˆ"
-msgid "thread resolved"
-msgid_plural "threads resolved"
-msgstr[0] ""
-
msgid "to help your contributors communicate effectively!"
msgstr "コントリビュータã®ã‚³ãƒŸãƒ¥ãƒ‹ã‚±ãƒ¼ã‚·ãƒ§ãƒ³ã‚’効率化ã™ã‚‹ï¼"
@@ -25110,13 +25669,13 @@ msgid "unicode domains should use IDNA encoding"
msgstr ""
msgid "updated"
-msgstr ""
+msgstr "æ›´æ–°ã•ã‚ŒãŸ"
msgid "updated %{timeAgo}"
msgstr ""
msgid "updated %{time_ago}"
-msgstr ""
+msgstr "%{time_ago} ã«æ›´æ–°"
msgid "user avatar"
msgstr ""
@@ -25134,37 +25693,40 @@ msgid "version %{versionIndex}"
msgstr ""
msgid "via %{closed_via}"
-msgstr ""
+msgstr "%{closed_via} ã«ã‚ˆã‚Š"
msgid "via merge request %{link}"
-msgstr ""
+msgstr "マージリクエスト %{link} ã«ã‚ˆã‚Š"
msgid "view it on GitLab"
msgstr "GitLab ã§è¦‹ã‚‹"
msgid "view the blob"
+msgstr "blobを表示"
+
+msgid "vulnerability|Add a comment"
msgstr ""
msgid "vulnerability|Add a comment or reason for dismissal"
-msgstr ""
+msgstr "å´ä¸‹ã®ã‚³ãƒ¡ãƒ³ãƒˆã¾ãŸã¯ç†ç”±ã‚’記入"
msgid "vulnerability|Add comment"
msgstr ""
msgid "vulnerability|Add comment & dismiss"
-msgstr ""
+msgstr "コメントを追加ã—ã¦å´ä¸‹"
msgid "vulnerability|Dismiss vulnerability"
-msgstr ""
+msgstr "脆弱性報告をå´ä¸‹ã™ã‚‹"
msgid "vulnerability|Save comment"
-msgstr ""
+msgstr "コメントをä¿å­˜"
msgid "vulnerability|Undo dismiss"
-msgstr ""
+msgstr "å´ä¸‹ã‚’å–り消ã™"
msgid "vulnerability|dismissed"
-msgstr ""
+msgstr "å´ä¸‹ã•ã‚Œã¾ã—ãŸ"
msgid "wiki page"
msgstr ""
diff --git a/locale/ka_GE/gitlab.po b/locale/ka_GE/gitlab.po
index 6f095f70bca..09b11bfb9e9 100644
--- a/locale/ka_GE/gitlab.po
+++ b/locale/ka_GE/gitlab.po
@@ -12,7 +12,7 @@ msgstr ""
"X-Crowdin-Project: gitlab-ee\n"
"X-Crowdin-Language: ka\n"
"X-Crowdin-File: /master/locale/gitlab.pot\n"
-"PO-Revision-Date: 2020-04-15 00:32\n"
+"PO-Revision-Date: 2020-05-05 21:12\n"
msgid " %{start} to %{end}"
msgstr ""
@@ -212,6 +212,11 @@ msgid_plural "%d tags"
msgstr[0] ""
msgstr[1] ""
+msgid "%d unresolved thread"
+msgid_plural "%d unresolved threads"
+msgstr[0] ""
+msgstr[1] ""
+
msgid "%d vulnerability dismissed"
msgid_plural "%d vulnerabilities dismissed"
msgstr[0] ""
@@ -371,6 +376,9 @@ msgstr ""
msgid "%{mrText}, this issue will be closed automatically."
msgstr ""
+msgid "%{namespace_name} is now read-only. You cannot: %{base_message}"
+msgstr ""
+
msgid "%{name} contained %{resultsString}"
msgstr ""
@@ -383,6 +391,11 @@ msgstr ""
msgid "%{name}'s avatar"
msgstr ""
+msgid "%{no_of_days} day"
+msgid_plural "%{no_of_days} days"
+msgstr[0] ""
+msgstr[1] ""
+
msgid "%{number_commits_behind} commits behind %{default_branch}, %{number_commits_ahead} commits ahead"
msgstr ""
@@ -506,6 +519,9 @@ msgstr[1] ""
msgid "%{text} is available"
msgstr ""
+msgid "%{timebox_name} should belong either to a project or a group."
+msgstr ""
+
msgid "%{title} %{operator} %{threshold}"
msgstr ""
@@ -781,12 +797,18 @@ msgstr ""
msgid "<code>\"johnsmith@example.com\": \"johnsmith@example.com\"</code> will add \"By <a href=\"#\">johnsmith@example.com</a>\" to all issues and comments originally created by johnsmith@example.com. By default, the email address or username is masked to ensure the user's privacy. Use this option if you want to show the full email address."
msgstr ""
+msgid "<namespace / project>"
+msgstr ""
+
msgid "<no name set>"
msgstr ""
msgid "<no scopes selected>"
msgstr ""
+msgid "<project name>"
+msgstr ""
+
msgid "<strong>%{group_name}</strong> group members"
msgstr ""
@@ -820,6 +842,9 @@ msgstr ""
msgid "A basic page and serverless function that uses AWS Lambda, AWS API Gateway, and GitLab Pages"
msgstr ""
+msgid "A complete DevOps platform"
+msgstr ""
+
msgid "A default branch cannot be chosen for an empty project."
msgstr ""
@@ -1036,6 +1061,12 @@ msgstr ""
msgid "AccessTokens|reset it"
msgstr ""
+msgid "AccessibilityReport|Accessibility report artifact not found"
+msgstr ""
+
+msgid "AccessibilityReport|Failed to retrieve accessibility report"
+msgstr ""
+
msgid "AccessibilityReport|Learn More"
msgstr ""
@@ -1045,6 +1076,9 @@ msgstr ""
msgid "AccessibilityReport|New"
msgstr ""
+msgid "AccessibilityReport|The accessibility scanning found an error of the following type: %{code}"
+msgstr ""
+
msgid "Account"
msgstr ""
@@ -1287,6 +1321,9 @@ msgstr ""
msgid "Added at"
msgstr ""
+msgid "Added for this merge request"
+msgstr ""
+
msgid "Added in this version"
msgstr ""
@@ -1658,6 +1695,72 @@ msgid_plural "Alerts"
msgstr[0] ""
msgstr[1] ""
+msgid "Alert Details"
+msgstr ""
+
+msgid "AlertManagement|Acknowledged"
+msgstr ""
+
+msgid "AlertManagement|Alert"
+msgstr ""
+
+msgid "AlertManagement|Authorize external service"
+msgstr ""
+
+msgid "AlertManagement|Display alerts from all your monitoring tools directly within GitLab. Streamline the investigation of your alerts and the escalation of alerts to incidents."
+msgstr ""
+
+msgid "AlertManagement|End time"
+msgstr ""
+
+msgid "AlertManagement|End time:"
+msgstr ""
+
+msgid "AlertManagement|Events"
+msgstr ""
+
+msgid "AlertManagement|Events:"
+msgstr ""
+
+msgid "AlertManagement|Full Alert Details"
+msgstr ""
+
+msgid "AlertManagement|More information"
+msgstr ""
+
+msgid "AlertManagement|No alerts available to display. If you think you're seeing this message in error, refresh the page."
+msgstr ""
+
+msgid "AlertManagement|No alerts to display."
+msgstr ""
+
+msgid "AlertManagement|Overview"
+msgstr ""
+
+msgid "AlertManagement|Resolved"
+msgstr ""
+
+msgid "AlertManagement|Severity"
+msgstr ""
+
+msgid "AlertManagement|Start time"
+msgstr ""
+
+msgid "AlertManagement|Start time:"
+msgstr ""
+
+msgid "AlertManagement|Status"
+msgstr ""
+
+msgid "AlertManagement|Surface alerts in GitLab"
+msgstr ""
+
+msgid "AlertManagement|There was an error displaying the alerts. Confirm your endpoint's configuration details to ensure alerts appear."
+msgstr ""
+
+msgid "AlertManagement|Triggered"
+msgstr ""
+
msgid "AlertService|%{linkStart}Learn more%{linkEnd} about configuring this endpoint to receive alerts."
msgstr ""
@@ -1727,6 +1830,9 @@ msgstr ""
msgid "All security scans are enabled because %{linkStart}Auto DevOps%{linkEnd} is enabled on this project"
msgstr ""
+msgid "All threads resolved"
+msgstr ""
+
msgid "All users"
msgstr ""
@@ -1745,6 +1851,9 @@ msgstr ""
msgid "Allow only the selected protocols to be used for Git access."
msgstr ""
+msgid "Allow owners to manage default branch protection per group"
+msgstr ""
+
msgid "Allow owners to manually add users outside of LDAP"
msgstr ""
@@ -1853,6 +1962,9 @@ msgstr ""
msgid "An error occurred fetching the dropdown data."
msgstr ""
+msgid "An error occurred fetching the project authors."
+msgstr ""
+
msgid "An error occurred previewing the blob"
msgstr ""
@@ -1934,6 +2046,9 @@ msgstr ""
msgid "An error occurred while fetching sidebar data"
msgstr ""
+msgid "An error occurred while fetching terraform reports."
+msgstr ""
+
msgid "An error occurred while fetching the Service Desk address."
msgstr ""
@@ -2171,6 +2286,9 @@ msgstr ""
msgid "Any"
msgstr ""
+msgid "Any Author"
+msgstr ""
+
msgid "Any Label"
msgstr ""
@@ -2386,10 +2504,10 @@ msgstr ""
msgid "Are you sure that you want to unarchive this project?"
msgstr ""
-msgid "Are you sure you want to cancel creating this comment?"
+msgid "Are you sure you want to cancel editing this comment?"
msgstr ""
-msgid "Are you sure you want to cancel editing this comment?"
+msgid "Are you sure you want to close this blocked issue?"
msgstr ""
msgid "Are you sure you want to delete %{name}?"
@@ -2419,6 +2537,9 @@ msgstr ""
msgid "Are you sure you want to deploy this environment?"
msgstr ""
+msgid "Are you sure you want to discard this comment?"
+msgstr ""
+
msgid "Are you sure you want to erase this build?"
msgstr ""
@@ -2595,6 +2716,9 @@ msgstr ""
msgid "At least one approval from a code owner is required to change files matching the respective CODEOWNER rules."
msgstr ""
+msgid "At least one logging option is required to be enabled"
+msgstr ""
+
msgid "At least one of group_id or project_id must be specified"
msgstr ""
@@ -2633,6 +2757,27 @@ msgstr ""
msgid "AuditEvents|Target"
msgstr ""
+msgid "AuditLogs|(removed)"
+msgstr ""
+
+msgid "AuditLogs|Action"
+msgstr ""
+
+msgid "AuditLogs|Author"
+msgstr ""
+
+msgid "AuditLogs|Date"
+msgstr ""
+
+msgid "AuditLogs|IP Address"
+msgstr ""
+
+msgid "AuditLogs|Object"
+msgstr ""
+
+msgid "AuditLogs|Target"
+msgstr ""
+
msgid "Aug"
msgstr ""
@@ -2915,9 +3060,6 @@ msgstr ""
msgid "BambooService|You must set up automatic revision labeling and a repository trigger in Bamboo."
msgstr ""
-msgid "Batch operations"
-msgstr ""
-
msgid "BatchComments|Delete all pending comments"
msgstr ""
@@ -3047,6 +3189,9 @@ msgstr ""
msgid "Boards"
msgstr ""
+msgid "Boards and Board Lists"
+msgstr ""
+
msgid "Boards|Collapse"
msgstr ""
@@ -3266,6 +3411,9 @@ msgstr ""
msgid "Buy GitLab Enterprise Edition"
msgstr ""
+msgid "Buy more Pipeline minutes"
+msgstr ""
+
msgid "By %{user_name}"
msgstr ""
@@ -3386,6 +3534,9 @@ msgstr ""
msgid "Can't scan the code?"
msgstr ""
+msgid "Can't update snippet: %{err}"
+msgstr ""
+
msgid "Canary"
msgstr ""
@@ -3554,6 +3705,9 @@ msgstr ""
msgid "Changing group path can have unintended side effects."
msgstr ""
+msgid "Charts"
+msgstr ""
+
msgid "Charts can't be displayed as the request for data has timed out. %{documentationLink}"
msgstr ""
@@ -3590,9 +3744,6 @@ msgstr ""
msgid "ChatMessage|and [%{count} more](%{pipeline_failed_jobs_url})"
msgstr ""
-msgid "ChatMessage|failed"
-msgstr ""
-
msgid "ChatMessage|has failed"
msgstr ""
@@ -3608,9 +3759,6 @@ msgstr ""
msgid "ChatMessage|in %{project_link}"
msgstr ""
-msgid "ChatMessage|passed"
-msgstr ""
-
msgid "Check again"
msgstr ""
@@ -4061,9 +4209,6 @@ msgstr ""
msgid "Click any <strong>project name</strong> in the project list below to navigate to the project milestone."
msgstr ""
-msgid "Click here"
-msgstr ""
-
msgid "Click the <strong>Download</strong> button and wait for downloading to complete."
msgstr ""
@@ -4451,6 +4596,12 @@ msgstr ""
msgid "ClusterIntegration|Fetching zones"
msgstr ""
+msgid "ClusterIntegration|Fluentd"
+msgstr ""
+
+msgid "ClusterIntegration|Fluentd is an open source data collector, which lets you unify the data collection and consumption for a better use and understanding of data. It requires at least one of the following logs to be successfully installed."
+msgstr ""
+
msgid "ClusterIntegration|GitLab Integration"
msgstr ""
@@ -4757,6 +4908,15 @@ msgstr ""
msgid "ClusterIntegration|Request to begin uninstalling failed"
msgstr ""
+msgid "ClusterIntegration|SIEM Hostname"
+msgstr ""
+
+msgid "ClusterIntegration|SIEM Port"
+msgstr ""
+
+msgid "ClusterIntegration|SIEM Protocol"
+msgstr ""
+
msgid "ClusterIntegration|Save changes"
msgstr ""
@@ -4853,6 +5013,12 @@ msgstr ""
msgid "ClusterIntegration|Select zone to choose machine type"
msgstr ""
+msgid "ClusterIntegration|Send Cilium Logs"
+msgstr ""
+
+msgid "ClusterIntegration|Send ModSecurity Logs"
+msgstr ""
+
msgid "ClusterIntegration|Service Token"
msgstr ""
@@ -5281,18 +5447,33 @@ msgstr ""
msgid "Compliance framework (optional)"
msgstr ""
+msgid "ComplianceFramework|GDPR"
+msgstr ""
+
msgid "ComplianceFramework|GDPR - General Data Protection Regulation"
msgstr ""
+msgid "ComplianceFramework|HIPAA"
+msgstr ""
+
msgid "ComplianceFramework|HIPAA - Health Insurance Portability and Accountability Act"
msgstr ""
+msgid "ComplianceFramework|PCI-DSS"
+msgstr ""
+
msgid "ComplianceFramework|PCI-DSS - Payment Card Industry-Data Security Standard"
msgstr ""
+msgid "ComplianceFramework|SOC 2"
+msgstr ""
+
msgid "ComplianceFramework|SOC 2 - Service Organization Control 2"
msgstr ""
+msgid "ComplianceFramework|SOX"
+msgstr ""
+
msgid "ComplianceFramework|SOX - Sarbanes-Oxley"
msgstr ""
@@ -5440,9 +5621,15 @@ msgstr ""
msgid "Container repositories sync capacity"
msgstr ""
+msgid "ContainerRegistry| Please visit the %{linkStart}administration settings%{linkEnd} to enable this feature."
+msgstr ""
+
msgid "ContainerRegistry|%{imageName} tags"
msgstr ""
+msgid "ContainerRegistry|%{title} was successfully scheduled for deletion"
+msgstr ""
+
msgid "ContainerRegistry|Automatically remove extra images that aren't designed to be kept."
msgstr ""
@@ -5455,6 +5642,9 @@ msgstr ""
msgid "ContainerRegistry|Container Registry"
msgstr ""
+msgid "ContainerRegistry|Container Registry tag expiration and retention policy is disabled"
+msgstr ""
+
msgid "ContainerRegistry|Copy build command"
msgstr ""
@@ -5464,18 +5654,12 @@ msgstr ""
msgid "ContainerRegistry|Copy push command"
msgstr ""
-msgid "ContainerRegistry|Currently, the Container Registry tag expiration feature is not available for projects created before GitLab version 12.8. For updates and more information, visit Issue %{linkStart}#196124%{linkEnd}"
-msgstr ""
-
msgid "ContainerRegistry|Docker connection error"
msgstr ""
msgid "ContainerRegistry|Docker tag expiration policy is %{toggleStatus}"
msgstr ""
-msgid "ContainerRegistry|Docker tags with names matching this regex pattern will expire:"
-msgstr ""
-
msgid "ContainerRegistry|Edit Settings"
msgstr ""
@@ -5497,9 +5681,6 @@ msgstr ""
msgid "ContainerRegistry|Image ID"
msgstr ""
-msgid "ContainerRegistry|Image deleted successfully"
-msgstr ""
-
msgid "ContainerRegistry|Keep and protect the images that matter most."
msgstr ""
@@ -5515,12 +5696,24 @@ msgstr ""
msgid "ContainerRegistry|Number of tags to retain:"
msgstr ""
+msgid "ContainerRegistry|Please contact your administrator."
+msgstr ""
+
msgid "ContainerRegistry|Push an image"
msgstr ""
msgid "ContainerRegistry|Quick Start"
msgstr ""
+msgid "ContainerRegistry|Regular expressions such as %{codeStart}.*-test%{codeEnd} or %{codeStart}dev-.*%{codeEnd} are supported"
+msgstr ""
+
+msgid "ContainerRegistry|Regular expressions such as %{codeStart}.*-test%{codeEnd} or %{codeStart}dev-.*%{codeEnd} are supported. To select all tags, use %{codeStart}.*%{codeEnd}"
+msgstr ""
+
+msgid "ContainerRegistry|Remember to run %{docLinkStart}garbage collection%{docLinkEnd} to remove the stale data from storage."
+msgstr ""
+
msgid "ContainerRegistry|Remove repository"
msgstr ""
@@ -5535,22 +5728,22 @@ msgstr[1] ""
msgid "ContainerRegistry|Retention policy has been Enabled"
msgstr ""
-msgid "ContainerRegistry|Something went wrong while deleting the image."
+msgid "ContainerRegistry|Something went wrong while fetching the expiration policy."
msgstr ""
-msgid "ContainerRegistry|Something went wrong while deleting the tag."
+msgid "ContainerRegistry|Something went wrong while fetching the repository list."
msgstr ""
-msgid "ContainerRegistry|Something went wrong while deleting the tags."
+msgid "ContainerRegistry|Something went wrong while fetching the tags list."
msgstr ""
-msgid "ContainerRegistry|Something went wrong while fetching the expiration policy."
+msgid "ContainerRegistry|Something went wrong while marking the tag for deletion."
msgstr ""
-msgid "ContainerRegistry|Something went wrong while fetching the packages list."
+msgid "ContainerRegistry|Something went wrong while marking the tags for deletion."
msgstr ""
-msgid "ContainerRegistry|Something went wrong while fetching the tags list."
+msgid "ContainerRegistry|Something went wrong while scheduling %{title} for deletion. Please try again."
msgstr ""
msgid "ContainerRegistry|Something went wrong while updating the expiration policy."
@@ -5559,16 +5752,25 @@ msgstr ""
msgid "ContainerRegistry|Tag"
msgstr ""
-msgid "ContainerRegistry|Tag deleted successfully"
-msgstr ""
-
msgid "ContainerRegistry|Tag expiration policy"
msgstr ""
msgid "ContainerRegistry|Tag expiration policy is designed to:"
msgstr ""
-msgid "ContainerRegistry|Tags deleted successfully"
+msgid "ContainerRegistry|Tag successfully marked for deletion."
+msgstr ""
+
+msgid "ContainerRegistry|Tags successfully marked for deletion."
+msgstr ""
+
+msgid "ContainerRegistry|Tags with names matching this regex pattern will %{italicStart}be preserved:%{italicEnd}"
+msgstr ""
+
+msgid "ContainerRegistry|Tags with names matching this regex pattern will %{italicStart}expire:%{italicEnd}"
+msgstr ""
+
+msgid "ContainerRegistry|The Container Registry tag expiration and retention policies for this project have not been enabled."
msgstr ""
msgid "ContainerRegistry|The last tag related to this image was recently removed. This empty image and any associated data will be automatically removed as part of the regular Garbage Collection process. If you have any questions, contact your administrator."
@@ -5589,16 +5791,16 @@ msgstr ""
msgid "ContainerRegistry|There are no container images stored for this project"
msgstr ""
-msgid "ContainerRegistry|This Registry contains deleted image tag data. Remember to run %{docLinkStart}garbage collection%{docLinkEnd} to remove the stale data from storage."
+msgid "ContainerRegistry|There was an error during the deletion of this image repository, please try again."
msgstr ""
msgid "ContainerRegistry|This image has no active tags"
msgstr ""
-msgid "ContainerRegistry|We are having trouble connecting to Docker, which could be due to an issue with your project name or path. %{docLinkStart}More Information%{docLinkEnd}"
+msgid "ContainerRegistry|This image repository is scheduled for deletion"
msgstr ""
-msgid "ContainerRegistry|Wildcards such as %{codeStart}.*-stable%{codeEnd} or %{codeStart}production/.*%{codeEnd} are supported. To select all tags, use %{codeStart}.*%{codeEnd}"
+msgid "ContainerRegistry|We are having trouble connecting to Docker, which could be due to an issue with your project name or path. %{docLinkStart}More Information%{docLinkEnd}"
msgstr ""
msgid "ContainerRegistry|With the Container Registry, every project can have its own space to store its Docker images. %{docLinkStart}More Information%{docLinkEnd}"
@@ -5853,7 +6055,7 @@ msgstr ""
msgid "Could not delete chat nickname %{chat_name}."
msgstr ""
-msgid "Could not find design"
+msgid "Could not find design."
msgstr ""
msgid "Could not remove the trigger."
@@ -5868,6 +6070,9 @@ msgstr ""
msgid "Could not revoke personal access token %{personal_access_token_name}."
msgstr ""
+msgid "Could not revoke project access token %{project_access_token_name}."
+msgstr ""
+
msgid "Could not save group ID"
msgstr ""
@@ -5925,6 +6130,9 @@ msgstr ""
msgid "Create a new branch"
msgstr ""
+msgid "Create a new deploy key for this project"
+msgstr ""
+
msgid "Create a new file as there are no files yet. Afterwards, you'll be able to commit your changes."
msgstr ""
@@ -6030,6 +6238,9 @@ msgstr ""
msgid "Create requirement"
msgstr ""
+msgid "Create snippet"
+msgstr ""
+
msgid "Create wildcard: %{searchTerm}"
msgstr ""
@@ -6051,6 +6262,9 @@ msgstr ""
msgid "Created"
msgstr ""
+msgid "Created %{timestamp}"
+msgstr ""
+
msgid "Created At"
msgstr ""
@@ -6407,6 +6621,9 @@ msgstr ""
msgid "CycleAnalytics|stage dropdown"
msgstr ""
+msgid "DAG"
+msgstr ""
+
msgid "DNS"
msgstr ""
@@ -6773,6 +6990,9 @@ msgstr ""
msgid "Deploy key was successfully updated."
msgstr ""
+msgid "Deploy keys allow read-only or read-write (if enabled) access to your repository. Deploy keys can be used for CI, staging or production servers. You can create a deploy key or add an existing one."
+msgstr ""
+
msgid "Deploy progress not found. To see pods, ensure your environment matches %{linkStart}deploy board criteria%{linkEnd}."
msgstr ""
@@ -6926,6 +7146,9 @@ msgstr ""
msgid "Deploying to"
msgstr ""
+msgid "Deployment Frequency"
+msgstr ""
+
msgid "Deployment|API"
msgstr ""
@@ -7444,9 +7667,6 @@ msgstr ""
msgid "Edit environment"
msgstr ""
-msgid "Edit epic description"
-msgstr ""
-
msgid "Edit file"
msgstr ""
@@ -8068,9 +8288,6 @@ msgstr ""
msgid "Epics"
msgstr ""
-msgid "Epics (Ultimate / Gold license only)"
-msgstr ""
-
msgid "Epics Roadmap"
msgstr ""
@@ -8080,6 +8297,9 @@ msgstr ""
msgid "Epics let you manage your portfolio of projects more efficiently and with less effort"
msgstr ""
+msgid "Epics, Issues, and Merge Requests"
+msgstr ""
+
msgid "Epics|Add an epic"
msgstr ""
@@ -8131,6 +8351,9 @@ msgstr ""
msgid "Epics|Something went wrong while fetching group epics."
msgstr ""
+msgid "Epics|Something went wrong while moving item."
+msgstr ""
+
msgid "Epics|Something went wrong while ordering item."
msgstr ""
@@ -8185,6 +8408,9 @@ msgstr ""
msgid "Error fetching network graph."
msgstr ""
+msgid "Error fetching payload data."
+msgstr ""
+
msgid "Error fetching projects"
msgstr ""
@@ -8194,9 +8420,6 @@ msgstr ""
msgid "Error fetching the dependency list. Please check your network connection and try again."
msgstr ""
-msgid "Error fetching usage ping data."
-msgstr ""
-
msgid "Error loading branch data. Please try again."
msgstr ""
@@ -8338,6 +8561,9 @@ msgstr ""
msgid "Errors"
msgstr ""
+msgid "Errors:"
+msgstr ""
+
msgid "Estimated"
msgstr ""
@@ -8542,12 +8768,18 @@ msgstr ""
msgid "Export as CSV"
msgstr ""
+msgid "Export group"
+msgstr ""
+
msgid "Export issues"
msgstr ""
msgid "Export project"
msgstr ""
+msgid "Export this group with all related data to a new GitLab instance. Once complete, you can import the data file from the \"New Group\" page."
+msgstr ""
+
msgid "Export this project with all its related data in order to move your project to a new GitLab instance. Once the export is finished, you can import the file from the \"New Project\" page."
msgstr ""
@@ -8617,9 +8849,6 @@ msgstr ""
msgid "Failed Jobs"
msgstr ""
-msgid "Failed create wiki"
-msgstr ""
-
msgid "Failed to add a Zoom meeting"
msgstr ""
@@ -8659,6 +8888,9 @@ msgstr ""
msgid "Failed to create resources"
msgstr ""
+msgid "Failed to create wiki"
+msgstr ""
+
msgid "Failed to delete board. Please try again."
msgstr ""
@@ -8668,7 +8900,7 @@ msgstr ""
msgid "Failed to enqueue the rebase operation, possibly due to a long-lived transaction. Try again later."
msgstr ""
-msgid "Failed to find import label for jira import."
+msgid "Failed to find import label for Jira import."
msgstr ""
msgid "Failed to get ref."
@@ -8692,6 +8924,12 @@ msgstr ""
msgid "Failed to load groups & users."
msgstr ""
+msgid "Failed to load labels. Please try again."
+msgstr ""
+
+msgid "Failed to load milestones. Please try again."
+msgstr ""
+
msgid "Failed to load related branches"
msgstr ""
@@ -9067,7 +9305,10 @@ msgstr ""
msgid "Filter by milestone name"
msgstr ""
-msgid "Filter by name..."
+msgid "Filter by name"
+msgstr ""
+
+msgid "Filter by status"
msgstr ""
msgid "Filter by two-factor authentication"
@@ -9079,12 +9320,18 @@ msgstr ""
msgid "Filter projects"
msgstr ""
+msgid "Filter results"
+msgstr ""
+
msgid "Filter results by group"
msgstr ""
msgid "Filter results by project"
msgstr ""
+msgid "Filter results..."
+msgstr ""
+
msgid "Filter your projects by name"
msgstr ""
@@ -9373,10 +9620,10 @@ msgstr ""
msgid "GeoNodes|Checksummed"
msgstr ""
-msgid "GeoNodes|Container repositories"
+msgid "GeoNodes|Consult Geo troubleshooting information"
msgstr ""
-msgid "GeoNodes|Data is out of date from %{timeago}"
+msgid "GeoNodes|Container repositories"
msgstr ""
msgid "GeoNodes|Data replication lag"
@@ -9418,6 +9665,9 @@ msgstr ""
msgid "GeoNodes|Last event ID seen from primary"
msgstr ""
+msgid "GeoNodes|Learn more about Geo node statuses"
+msgstr ""
+
msgid "GeoNodes|Loading nodes"
msgstr ""
@@ -9433,6 +9683,9 @@ msgstr ""
msgid "GeoNodes|Node was successfully removed."
msgstr ""
+msgid "GeoNodes|Node's status was updated %{timeAgo}."
+msgstr ""
+
msgid "GeoNodes|Not checksummed"
msgstr ""
@@ -9463,7 +9716,10 @@ msgstr ""
msgid "GeoNodes|Repository verification progress"
msgstr ""
-msgid "GeoNodes|Selective"
+msgid "GeoNodes|Selective (%{syncLabel})"
+msgstr ""
+
+msgid "GeoNodes|Selective synchronization"
msgstr ""
msgid "GeoNodes|Something went wrong while changing node status"
@@ -9490,6 +9746,9 @@ msgstr ""
msgid "GeoNodes|Unverified"
msgstr ""
+msgid "GeoNodes|Updated %{timeAgo}"
+msgstr ""
+
msgid "GeoNodes|Used slots"
msgstr ""
@@ -9529,16 +9788,16 @@ msgstr ""
msgid "Geo|All"
msgstr ""
-msgid "Geo|All projects"
+msgid "Geo|All %{replicable_type}"
msgstr ""
-msgid "Geo|All projects are being scheduled for re-sync"
+msgid "Geo|All projects"
msgstr ""
-msgid "Geo|All projects are being scheduled for re-verify"
+msgid "Geo|All projects are being scheduled for resync"
msgstr ""
-msgid "Geo|Batch operations"
+msgid "Geo|All projects are being scheduled for reverify"
msgstr ""
msgid "Geo|Could not remove tracking entry for an existing project."
@@ -9550,9 +9809,15 @@ msgstr ""
msgid "Geo|Failed"
msgstr ""
+msgid "Geo|Filter by status"
+msgstr ""
+
msgid "Geo|Geo Status"
msgstr ""
+msgid "Geo|In progress"
+msgstr ""
+
msgid "Geo|In sync"
msgstr ""
@@ -9577,9 +9842,6 @@ msgstr ""
msgid "Geo|Not synced yet"
msgstr ""
-msgid "Geo|Pending"
-msgstr ""
-
msgid "Geo|Pending synchronization"
msgstr ""
@@ -9616,7 +9878,7 @@ msgstr ""
msgid "Geo|Resync"
msgstr ""
-msgid "Geo|Resync all projects"
+msgid "Geo|Resync all"
msgstr ""
msgid "Geo|Retry count"
@@ -9625,7 +9887,7 @@ msgstr ""
msgid "Geo|Reverify"
msgstr ""
-msgid "Geo|Reverify all projects"
+msgid "Geo|Reverify all"
msgstr ""
msgid "Geo|Status"
@@ -9967,9 +10229,6 @@ msgstr ""
msgid "Go to file"
msgstr ""
-msgid "Go to file (MRs only)"
-msgstr ""
-
msgid "Go to file permalink (while viewing a file)"
msgstr ""
@@ -10414,6 +10673,9 @@ msgstr ""
msgid "GroupSettings|Disable group mentions"
msgstr ""
+msgid "GroupSettings|Export group"
+msgstr ""
+
msgid "GroupSettings|If the parent group's visibility is lower than the group current visibility, visibility levels for subgroups and projects will be changed to match the new parent group's visibility."
msgstr ""
@@ -10492,6 +10754,9 @@ msgstr ""
msgid "Groups (%{count})"
msgstr ""
+msgid "Groups (%{groups})"
+msgstr ""
+
msgid "Groups can also be nested by creating %{subgroup_docs_link_start}subgroups%{subgroup_docs_link_end}."
msgstr ""
@@ -10757,15 +11022,12 @@ msgstr ""
msgid "ID:"
msgstr ""
-msgid "IDE|Allow live previews of JavaScript projects in the Web IDE using CodeSandbox client side evaluation."
+msgid "IDE|Allow live previews of JavaScript projects in the Web IDE using CodeSandbox Live Preview."
msgstr ""
msgid "IDE|Back"
msgstr ""
-msgid "IDE|Client side evaluation"
-msgstr ""
-
msgid "IDE|Commit"
msgstr ""
@@ -10829,6 +11091,9 @@ msgstr ""
msgid "If any job surpasses this timeout threshold, it will be marked as failed. Human readable time input language is accepted like \"1 hour\". Values without specification represent seconds."
msgstr ""
+msgid "If blank, set allowable lifetime to %{instance_level_policy_in_words}, as defined by the instance admin. Once set, existing tokens for users in this group may be revoked."
+msgstr ""
+
msgid "If checked, group owners can manage LDAP group links and LDAP member overrides"
msgstr ""
@@ -10868,6 +11133,9 @@ msgstr ""
msgid "If you lose your recovery codes you can generate new ones, invalidating all previous codes."
msgstr ""
+msgid "If you reach 100%% storage capacity, you will not be able to: %{base_message}"
+msgstr ""
+
msgid "If your HTTP repository is not publicly accessible, add your credentials."
msgstr ""
@@ -10979,6 +11247,9 @@ msgstr ""
msgid "Import repository"
msgstr ""
+msgid "Import started by: %{importInitiator}"
+msgstr ""
+
msgid "Import tasks"
msgstr ""
@@ -11045,6 +11316,9 @@ msgstr ""
msgid "In order to tailor your experience with GitLab we<br>would like to know a bit more about you."
msgstr ""
+msgid "In progress"
+msgstr ""
+
msgid "In the next step, you'll be able to select the projects you want to import."
msgstr ""
@@ -11392,15 +11666,15 @@ msgstr ""
msgid "Issues"
msgstr ""
-msgid "Issues / Merge Requests"
-msgstr ""
-
msgid "Issues Analytics"
msgstr ""
msgid "Issues Rate Limits"
msgstr ""
+msgid "Issues and Merge Requests"
+msgstr ""
+
msgid "Issues can be bugs, tasks or ideas to be discussed. Also, issues are searchable and filterable."
msgstr ""
@@ -11422,10 +11696,10 @@ msgstr ""
msgid "IssuesAnalytics|Avg/Month:"
msgstr ""
-msgid "IssuesAnalytics|Issues created"
+msgid "IssuesAnalytics|Issues opened"
msgstr ""
-msgid "IssuesAnalytics|Issues created per month"
+msgid "IssuesAnalytics|Issues opened per month"
msgstr ""
msgid "IssuesAnalytics|Last 12 months"
@@ -11479,6 +11753,9 @@ msgstr ""
msgid "Jira integration not configured."
msgstr ""
+msgid "Jira project: %{importProject}"
+msgstr ""
+
msgid "JiraService|Events for %{noteable_model_name} are disabled."
msgstr ""
@@ -11868,6 +12145,9 @@ msgstr ""
msgid "Lead"
msgstr ""
+msgid "Lead Time"
+msgstr ""
+
msgid "Learn GitLab"
msgstr ""
@@ -11880,6 +12160,9 @@ msgstr ""
msgid "Learn how to %{no_packages_link_start}publish and share your packages%{no_packages_link_end} with GitLab."
msgstr ""
+msgid "Learn how to enable synchronization"
+msgstr ""
+
msgid "Learn more"
msgstr ""
@@ -12211,9 +12494,6 @@ msgstr ""
msgid "Live preview"
msgstr ""
-msgid "Load more vulnerabilities"
-msgstr ""
-
msgid "Loading"
msgstr ""
@@ -12364,9 +12644,6 @@ msgstr ""
msgid "Manage"
msgstr ""
-msgid "Manage Git repositories with fine-grained access controls that keep your code secure. Perform code reviews and enhance collaboration with merge requests. Each project can also have an issue tracker and a wiki."
-msgstr ""
-
msgid "Manage Web IDE features"
msgstr ""
@@ -12397,6 +12674,9 @@ msgstr ""
msgid "Manage your license"
msgstr ""
+msgid "Managed Account"
+msgstr ""
+
msgid "Manifest"
msgstr ""
@@ -12661,6 +12941,9 @@ msgstr ""
msgid "Merge Requests in Review"
msgstr ""
+msgid "Merge automatically (%{strategy})"
+msgstr ""
+
msgid "Merge commit message"
msgstr ""
@@ -12814,6 +13097,12 @@ msgstr ""
msgid "Merged branches are being deleted. This can take some time depending on the number of branches. Please refresh the page to see changes."
msgstr ""
+msgid "Merged this merge request."
+msgstr ""
+
+msgid "Merges this merge request immediately."
+msgstr ""
+
msgid "Merges this merge request when the pipeline succeeds."
msgstr ""
@@ -12921,12 +13210,15 @@ msgid_plural "Metrics|Edit metrics"
msgstr[0] ""
msgstr[1] ""
-msgid "Metrics|Environment"
+msgid "Metrics|Expand panel"
msgstr ""
msgid "Metrics|For grouping similar metrics"
msgstr ""
+msgid "Metrics|Go back (Esc)"
+msgstr ""
+
msgid "Metrics|Invalid time range, please verify."
msgstr ""
@@ -12957,9 +13249,6 @@ msgstr ""
msgid "Metrics|Refresh dashboard"
msgstr ""
-msgid "Metrics|Show last"
-msgstr ""
-
msgid "Metrics|There was an error creating the dashboard."
msgstr ""
@@ -13038,6 +13327,9 @@ msgstr ""
msgid "Microsoft Azure"
msgstr ""
+msgid "Middleman project with Static Site Editor support"
+msgstr ""
+
msgid "Migrated %{success_count}/%{total_count} files."
msgstr ""
@@ -13483,10 +13775,10 @@ msgstr ""
msgid "Next"
msgstr ""
-msgid "Next file in diff (MRs only)"
+msgid "Next file in diff"
msgstr ""
-msgid "Next unresolved discussion (MRs only)"
+msgid "Next unresolved discussion"
msgstr ""
msgid "Nickname"
@@ -13546,6 +13838,9 @@ msgstr ""
msgid "No changes between %{ref_start}%{source_branch}%{ref_end} and %{ref_start}%{target_branch}%{ref_end}"
msgstr ""
+msgid "No child epics match applied filters"
+msgstr ""
+
msgid "No connection could be made to a Gitaly Server, please check your logs!"
msgstr ""
@@ -13684,15 +13979,6 @@ msgstr ""
msgid "No thanks, don't show this again"
msgstr ""
-msgid "No vulnerabilities found for this group"
-msgstr ""
-
-msgid "No vulnerabilities found for this pipeline"
-msgstr ""
-
-msgid "No vulnerabilities found for this project"
-msgstr ""
-
msgid "No vulnerabilities present"
msgstr ""
@@ -13816,6 +14102,9 @@ msgstr ""
msgid "Nothing to preview."
msgstr ""
+msgid "Nothing to synchronize"
+msgstr ""
+
msgid "Notification events"
msgstr ""
@@ -13987,6 +14276,9 @@ msgstr ""
msgid "Once removed, the fork relationship cannot be restored and you will no longer be able to send merge requests to the source."
msgstr ""
+msgid "Once the exported file is ready you can download it from this page."
+msgstr ""
+
msgid "Once the exported file is ready, you will receive a notification email with a download link, or you can download it from this page."
msgstr ""
@@ -14088,9 +14380,6 @@ msgstr ""
msgid "Open sidebar"
msgstr ""
-msgid "Open source software to collaborate on code"
-msgstr ""
-
msgid "Open: %{openIssuesCount}"
msgstr ""
@@ -14193,6 +14482,9 @@ msgstr ""
msgid "OutdatedBrowser|You can provide feedback %{feedback_link_start}on this issue%{feedback_link_end} or via your usual support channels."
msgstr ""
+msgid "Overridden"
+msgstr ""
+
msgid "Overview"
msgstr ""
@@ -14244,6 +14536,9 @@ msgstr ""
msgid "PackageRegistry|Conan Command"
msgstr ""
+msgid "PackageRegistry|Copy .pypirc content"
+msgstr ""
+
msgid "PackageRegistry|Copy Conan Command"
msgstr ""
@@ -14265,6 +14560,9 @@ msgstr ""
msgid "PackageRegistry|Copy NuGet Setup Command"
msgstr ""
+msgid "PackageRegistry|Copy Pip command"
+msgstr ""
+
msgid "PackageRegistry|Copy and paste this inside your %{codeStart}pom.xml%{codeEnd} %{codeStart}dependencies%{codeEnd} block."
msgstr ""
@@ -14298,6 +14596,12 @@ msgstr ""
msgid "PackageRegistry|For more information on the NuGet registry, %{linkStart}see the documentation%{linkEnd}."
msgstr ""
+msgid "PackageRegistry|For more information on the PyPi registry, %{linkStart}see the documentation%{linkEnd}."
+msgstr ""
+
+msgid "PackageRegistry|If you haven't already done so, you will need to add the below to your %{codeStart}.pypirc%{codeEnd} file."
+msgstr ""
+
msgid "PackageRegistry|If you haven't already done so, you will need to add the below to your %{codeStart}pom.xml%{codeEnd} file."
msgstr ""
@@ -14307,6 +14611,9 @@ msgstr ""
msgid "PackageRegistry|Learn how to %{noPackagesLinkStart}publish and share your packages%{noPackagesLinkEnd} with GitLab."
msgstr ""
+msgid "PackageRegistry|Manually Published"
+msgstr ""
+
msgid "PackageRegistry|Maven"
msgstr ""
@@ -14325,12 +14632,18 @@ msgstr ""
msgid "PackageRegistry|NuGet Command"
msgstr ""
+msgid "PackageRegistry|Pip Command"
+msgstr ""
+
msgid "PackageRegistry|Pipeline %{linkStart}%{linkEnd} triggered %{timestamp} by %{author}"
msgstr ""
msgid "PackageRegistry|Published to the repository at %{timestamp}"
msgstr ""
+msgid "PackageRegistry|PyPi"
+msgstr ""
+
msgid "PackageRegistry|Registry Setup"
msgstr ""
@@ -14367,6 +14680,9 @@ msgstr ""
msgid "PackageRegistry|npm"
msgstr ""
+msgid "PackageRegistry|published by %{author}"
+msgstr ""
+
msgid "PackageRegistry|yarn"
msgstr ""
@@ -14382,6 +14698,9 @@ msgstr ""
msgid "PackageType|NuGet"
msgstr ""
+msgid "PackageType|PyPi"
+msgstr ""
+
msgid "Packages"
msgstr ""
@@ -14475,6 +14794,9 @@ msgstr ""
msgid "Past due"
msgstr ""
+msgid "Paste a machine public key here. Read more about how to generate it"
+msgstr ""
+
msgid "Paste a machine public key here. Read more about how to generate it %{link_start}here%{link_end}"
msgstr ""
@@ -14490,9 +14812,6 @@ msgstr ""
msgid "Path"
msgstr ""
-msgid "Path, transfer, remove"
-msgstr ""
-
msgid "Path:"
msgstr ""
@@ -14523,7 +14842,7 @@ msgstr ""
msgid "Percentage"
msgstr ""
-msgid "Perform advanced options such as changing path, transferring, or removing the group."
+msgid "Perform advanced options such as changing path, transferring, exporting, or removing the group."
msgstr ""
msgid "Perform common operations on GitLab project"
@@ -14604,9 +14923,6 @@ msgstr ""
msgid "Pipeline minutes quota"
msgstr ""
-msgid "Pipeline quota"
-msgstr ""
-
msgid "Pipeline subscriptions"
msgstr ""
@@ -14616,6 +14932,9 @@ msgstr ""
msgid "Pipeline: %{status}"
msgstr ""
+msgid "PipelineCharts|CI / CD Analytics"
+msgstr ""
+
msgid "PipelineCharts|Failed:"
msgstr ""
@@ -14694,15 +15013,6 @@ msgstr ""
msgid "Pipelines settings for '%{project_name}' were successfully updated."
msgstr ""
-msgid "Pipelines| to purchase more minutes."
-msgstr ""
-
-msgid "Pipelines|%{namespace_name} has exceeded its pipeline minutes quota."
-msgstr ""
-
-msgid "Pipelines|%{namespace_name} has less than %{notification_level}%% of CI minutes available."
-msgstr ""
-
msgid "Pipelines|API"
msgstr ""
@@ -14724,10 +15034,13 @@ msgstr ""
msgid "Pipelines|Get started with Pipelines"
msgstr ""
-msgid "Pipelines|Loading Pipelines"
+msgid "Pipelines|Group %{namespace_name} has %{percentage}%% or less Shared Runner Pipeline minutes remaining. Once it runs out, no new jobs or pipelines in its projects will run."
msgstr ""
-msgid "Pipelines|Pipelines will not run anymore on shared Runners."
+msgid "Pipelines|Group %{namespace_name} has exceeded its pipeline minutes quota. Unless you buy additional pipeline minutes, no new jobs or pipelines in its projects will run."
+msgstr ""
+
+msgid "Pipelines|Loading Pipelines"
msgstr ""
msgid "Pipelines|Project cache successfully reset."
@@ -14958,6 +15271,9 @@ msgstr ""
msgid "Please select"
msgstr ""
+msgid "Please select a Jira project"
+msgstr ""
+
msgid "Please select a country"
msgstr ""
@@ -15003,6 +15319,9 @@ msgstr ""
msgid "Please wait while we import the repository for you. Refresh at will."
msgstr ""
+msgid "Plugins directory is deprecated and will be removed in 14.0. Please move this file into /file_hooks directory."
+msgstr ""
+
msgid "Pod does not exist"
msgstr ""
@@ -15120,7 +15439,7 @@ msgstr ""
msgid "Prevent users from changing their profile name"
msgstr ""
-msgid "Prevent users from modifing merge request approvers list"
+msgid "Prevent users from modifying merge request approvers list"
msgstr ""
msgid "Prevent users from performing write operations on GitLab while performing maintenance."
@@ -15141,10 +15460,10 @@ msgstr ""
msgid "Previous Artifacts"
msgstr ""
-msgid "Previous file in diff (MRs only)"
+msgid "Previous file in diff"
msgstr ""
-msgid "Previous unresolved discussion (MRs only)"
+msgid "Previous unresolved discussion"
msgstr ""
msgid "Primary"
@@ -15177,6 +15496,9 @@ msgstr ""
msgid "Private profile"
msgstr ""
+msgid "Private projects Minutes cost factor"
+msgstr ""
+
msgid "Private projects can be created in your personal namespace with:"
msgstr ""
@@ -16005,6 +16327,9 @@ msgstr ""
msgid "ProjectSettings|With GitLab Pages you can host your static websites on GitLab"
msgstr ""
+msgid "ProjectSettings|With Metrics Dashboard you can visualize this project performance metrics"
+msgstr ""
+
msgid "ProjectTemplates|.NET Core"
msgstr ""
@@ -16068,6 +16393,9 @@ msgstr ""
msgid "ProjectTemplates|Spring"
msgstr ""
+msgid "ProjectTemplates|Static Site Editor/Middleman"
+msgstr ""
+
msgid "ProjectTemplates|iOS (Swift)"
msgstr ""
@@ -16287,13 +16615,13 @@ msgstr ""
msgid "PrometheusService|Prometheus is being automatically managed on your clusters"
msgstr ""
-msgid "PrometheusService|These metrics will only be monitored after your first deployment to an environment"
+msgid "PrometheusService|Select the Active checkbox to override the Auto Configuration with custom settings. If unchecked, Auto Configuration settings are used."
msgstr ""
-msgid "PrometheusService|Time-series monitoring service"
+msgid "PrometheusService|These metrics will only be monitored after your first deployment to an environment"
msgstr ""
-msgid "PrometheusService|To enable manual configuration, uninstall Prometheus from your clusters"
+msgid "PrometheusService|Time-series monitoring service"
msgstr ""
msgid "PrometheusService|To enable the installation of Prometheus on your clusters, deactivate the manual configuration below"
@@ -16479,6 +16807,9 @@ msgstr ""
msgid "Protip:"
msgstr ""
+msgid "Protocol"
+msgstr ""
+
msgid "Provider"
msgstr ""
@@ -16503,6 +16834,9 @@ msgstr ""
msgid "Public pipelines"
msgstr ""
+msgid "Public projects Minutes cost factor"
+msgstr ""
+
msgid "Pull"
msgstr ""
@@ -16703,6 +17037,9 @@ msgid_plural "Refreshing in %d seconds to show the updated status..."
msgstr[0] ""
msgstr[1] ""
+msgid "Regenerate export"
+msgstr ""
+
msgid "Regenerate instance ID"
msgstr ""
@@ -17080,9 +17417,29 @@ msgstr ""
msgid "Reports|%{combinedString} and %{resolvedString}"
msgstr ""
+msgid "Reports|Accessibility scanning detected %d issue for the source branch only"
+msgid_plural "Reports|Accessibility scanning detected %d issues for the source branch only"
+msgstr[0] ""
+msgstr[1] ""
+
+msgid "Reports|Accessibility scanning detected no issues for the source branch only"
+msgstr ""
+
+msgid "Reports|Accessibility scanning failed loading results"
+msgstr ""
+
+msgid "Reports|Accessibility scanning results are being parsed"
+msgstr ""
+
msgid "Reports|Actions"
msgstr ""
+msgid "Reports|An error occured while loading report"
+msgstr ""
+
+msgid "Reports|An error occurred while loading %{name} results"
+msgstr ""
+
msgid "Reports|Class"
msgstr ""
@@ -17188,7 +17545,7 @@ msgstr ""
msgid "Requested %{time_ago}"
msgstr ""
-msgid "Requested design version does not exist"
+msgid "Requested design version does not exist."
msgstr ""
msgid "Requested states are invalid"
@@ -17372,6 +17729,9 @@ msgstr ""
msgid "Resync"
msgstr ""
+msgid "Resync all"
+msgstr ""
+
msgid "Resync all %{replicableType}"
msgstr ""
@@ -17440,6 +17800,9 @@ msgstr ""
msgid "Revoked personal access token %{personal_access_token_name}!"
msgstr ""
+msgid "Revoked project access token %{project_access_token_name}!"
+msgstr ""
+
msgid "RightSidebar|adding a"
msgstr ""
@@ -17488,6 +17851,9 @@ msgstr ""
msgid "Runner token"
msgstr ""
+msgid "Runner tokens"
+msgstr ""
+
msgid "Runner was not updated."
msgstr ""
@@ -17542,6 +17908,9 @@ msgstr ""
msgid "SAML SSO for %{group_name}"
msgstr ""
+msgid "SAML discovery tokens"
+msgstr ""
+
msgid "SAML for %{group_name}"
msgstr ""
@@ -17626,12 +17995,18 @@ msgstr ""
msgid "Scheduled"
msgstr ""
+msgid "Scheduled to merge this merge request (%{strategy})."
+msgstr ""
+
msgid "Scheduled to merge this merge request when the pipeline succeeds."
msgstr ""
msgid "Schedules"
msgstr ""
+msgid "Schedules to merge this merge request (%{strategy})."
+msgstr ""
+
msgid "Scheduling"
msgstr ""
@@ -17689,6 +18064,9 @@ msgstr ""
msgid "Search branches and tags"
msgstr ""
+msgid "Search by author"
+msgstr ""
+
msgid "Search files"
msgstr ""
@@ -17834,11 +18212,6 @@ msgid_plural "SearchResults|snippets"
msgstr[0] ""
msgstr[1] ""
-msgid "SearchResults|snippet result"
-msgid_plural "SearchResults|snippet results"
-msgstr[0] ""
-msgstr[1] ""
-
msgid "SearchResults|user"
msgid_plural "SearchResults|users"
msgstr[0] ""
@@ -17849,6 +18222,9 @@ msgid_plural "SearchResults|wiki results"
msgstr[0] ""
msgstr[1] ""
+msgid "Searching by both author and message is currently not supported."
+msgstr ""
+
msgid "Seat Link"
msgstr ""
@@ -17876,184 +18252,232 @@ msgstr ""
msgid "Security Dashboard"
msgstr ""
-msgid "Security Dashboard|Error fetching the vulnerability counts. Please check your network connection and try again."
+msgid "Security configuration help link"
+msgstr ""
+
+msgid "Security dashboard"
msgstr ""
-msgid "Security Dashboard|Error fetching the vulnerability list. Please check your network connection and try again."
+msgid "Security report is out of date. Please update your branch with the latest changes from the target branch (%{targetBranchName})"
msgstr ""
-msgid "Security Dashboard|Issue Created"
+msgid "Security report is out of date. Run %{newPipelineLinkStart}a new pipeline%{newPipelineLinkEnd} for the target branch (%{targetBranchName})"
msgstr ""
-msgid "Security Reports|Comment added to '%{vulnerabilityName}'"
+msgid "SecurityConfiguration|Configured"
msgstr ""
-msgid "Security Reports|Comment deleted on '%{vulnerabilityName}'"
+msgid "SecurityConfiguration|Feature"
msgstr ""
-msgid "Security Reports|Comment edited on '%{vulnerabilityName}'"
+msgid "SecurityConfiguration|Feature documentation for %{featureName}"
msgstr ""
-msgid "Security Reports|Create issue"
+msgid "SecurityConfiguration|Not yet configured"
msgstr ""
-msgid "Security Reports|Dismiss vulnerability"
+msgid "SecurityConfiguration|Secure features"
msgstr ""
-msgid "Security Reports|Dismissed '%{vulnerabilityName}'"
+msgid "SecurityConfiguration|Status"
msgstr ""
-msgid "Security Reports|Dismissed '%{vulnerabilityName}'. Turn off the hide dismissed toggle to view."
+msgid "SecurityReports|%{firstProject} and %{secondProject}"
msgstr ""
-msgid "Security Reports|Either you don't have permission to view this dashboard or the dashboard has not been setup. Please check your permission settings with your administrator or check your dashboard configurations to proceed."
+msgid "SecurityReports|%{firstProject}, %{secondProject}, and %{rest}"
msgstr ""
-msgid "Security Reports|Learn more about setting up your dashboard"
+msgid "SecurityReports|Add a project to your dashboard"
msgstr ""
-msgid "Security Reports|More info"
+msgid "SecurityReports|Add or remove projects from your dashboard"
msgstr ""
-msgid "Security Reports|Oops, something doesn't seem right."
+msgid "SecurityReports|Add projects"
msgstr ""
-msgid "Security Reports|Security reports can only be accessed by authorized users."
+msgid "SecurityReports|Comment added to '%{vulnerabilityName}'"
msgstr ""
-msgid "Security Reports|There was an error adding the comment."
+msgid "SecurityReports|Comment deleted on '%{vulnerabilityName}'"
msgstr ""
-msgid "Security Reports|There was an error creating the issue."
+msgid "SecurityReports|Comment edited on '%{vulnerabilityName}'"
msgstr ""
-msgid "Security Reports|There was an error creating the merge request."
+msgid "SecurityReports|Create issue"
msgstr ""
-msgid "Security Reports|There was an error deleting the comment."
+msgid "SecurityReports|Dismiss Selected"
msgstr ""
-msgid "Security Reports|There was an error dismissing the vulnerabilities."
+msgid "SecurityReports|Dismiss vulnerability"
msgstr ""
-msgid "Security Reports|There was an error dismissing the vulnerability."
+msgid "SecurityReports|Dismissed '%{vulnerabilityName}'"
msgstr ""
-msgid "Security Reports|There was an error reverting the dismissal."
+msgid "SecurityReports|Dismissed '%{vulnerabilityName}'. Turn off the hide dismissed toggle to view."
msgstr ""
-msgid "Security Reports|There was an error reverting this dismissal."
+msgid "SecurityReports|Each vulnerability now has a unique page that can be directly linked to, shared, referenced, and tracked as the single source of truth. Vulnerability occurrences also persist across scanner runs, which improves tracking and visibility and reduces duplicates between scans."
msgstr ""
-msgid "Security Reports|Undo dismiss"
+msgid "SecurityReports|Edit dashboard"
msgstr ""
-msgid "Security Reports|You do not have sufficient permissions to access this report"
+msgid "SecurityReports|Either you don't have permission to view this dashboard or the dashboard has not been setup. Please check your permission settings with your administrator or check your dashboard configurations to proceed."
msgstr ""
-msgid "Security Reports|You must sign in as an authorized user to see this report"
+msgid "SecurityReports|Error fetching the vulnerability counts. Please check your network connection and try again."
msgstr ""
-msgid "Security configuration help link"
+msgid "SecurityReports|Error fetching the vulnerability list. Please check your network connection and try again."
msgstr ""
-msgid "Security dashboard"
+msgid "SecurityReports|False positive"
msgstr ""
-msgid "Security report is out of date. Please update your branch with the latest changes from the target branch (%{targetBranchName})"
+msgid "SecurityReports|Hide dismissed"
msgstr ""
-msgid "Security report is out of date. Run %{newPipelineLinkStart}a new pipeline%{newPipelineLinkEnd} for the target branch (%{targetBranchName})"
+msgid "SecurityReports|Introducing standalone vulnerabilities"
msgstr ""
-msgid "SecurityConfiguration|Configured"
+msgid "SecurityReports|Issue Created"
msgstr ""
-msgid "SecurityConfiguration|Feature"
+msgid "SecurityReports|Learn More"
msgstr ""
-msgid "SecurityConfiguration|Feature documentation for %{featureName}"
+msgid "SecurityReports|Learn more about setting up your dashboard"
msgstr ""
-msgid "SecurityConfiguration|Not yet configured"
+msgid "SecurityReports|Load more vulnerabilities"
msgstr ""
-msgid "SecurityConfiguration|Secure features"
+msgid "SecurityReports|Monitor vulnerabilities in your code"
msgstr ""
-msgid "SecurityConfiguration|Status"
+msgid "SecurityReports|More info"
msgstr ""
-msgid "SecurityDashboard|%{firstProject} and %{secondProject}"
+msgid "SecurityReports|More information"
msgstr ""
-msgid "SecurityDashboard|%{firstProject}, %{secondProject}, and %{rest}"
+msgid "SecurityReports|No vulnerabilities found for dashboard"
msgstr ""
-msgid "SecurityDashboard|Add a project to your dashboard"
+msgid "SecurityReports|No vulnerabilities found for this group"
msgstr ""
-msgid "SecurityDashboard|Add or remove projects from your dashboard"
+msgid "SecurityReports|No vulnerabilities found for this pipeline"
msgstr ""
-msgid "SecurityDashboard|Add projects"
+msgid "SecurityReports|No vulnerabilities found for this project"
msgstr ""
-msgid "SecurityDashboard|Edit dashboard"
+msgid "SecurityReports|Oops, something doesn't seem right."
msgstr ""
-msgid "SecurityDashboard|Hide dismissed"
+msgid "SecurityReports|Pipeline %{pipelineLink} triggered %{timeago} by %{user}"
msgstr ""
-msgid "SecurityDashboard|Monitor vulnerabilities in your code"
+msgid "SecurityReports|Project"
msgstr ""
-msgid "SecurityDashboard|More information"
+msgid "SecurityReports|Projects added"
msgstr ""
-msgid "SecurityDashboard|No vulnerabilities found for dashboard"
+msgid "SecurityReports|Remove project from dashboard"
msgstr ""
-msgid "SecurityDashboard|Pipeline %{pipelineLink} triggered %{timeago} by %{user}"
+msgid "SecurityReports|Report type"
msgstr ""
-msgid "SecurityDashboard|Project"
+msgid "SecurityReports|Return to dashboard"
msgstr ""
-msgid "SecurityDashboard|Projects added"
+msgid "SecurityReports|Security Dashboard"
msgstr ""
-msgid "SecurityDashboard|Remove project from dashboard"
+msgid "SecurityReports|Security reports can only be accessed by authorized users."
msgstr ""
-msgid "SecurityDashboard|Report type"
+msgid "SecurityReports|Select a project to add by using the project search field above."
msgstr ""
-msgid "SecurityDashboard|Return to dashboard"
+msgid "SecurityReports|Select a reason"
msgstr ""
-msgid "SecurityDashboard|Security Dashboard"
+msgid "SecurityReports|Severity"
msgstr ""
-msgid "SecurityDashboard|Select a project to add by using the project search field above."
+msgid "SecurityReports|Status"
msgstr ""
-msgid "SecurityDashboard|Severity"
+msgid "SecurityReports|The security dashboard displays the latest security findings for projects you wish to monitor. Select \"Edit dashboard\" to add and remove projects."
msgstr ""
-msgid "SecurityDashboard|Status"
+msgid "SecurityReports|The security dashboard displays the latest security report. Use it to find and fix vulnerabilities."
msgstr ""
-msgid "SecurityDashboard|The security dashboard displays the latest security findings for projects you wish to monitor. Select \"Edit dashboard\" to add and remove projects."
+msgid "SecurityReports|There was an error adding the comment."
msgstr ""
-msgid "SecurityDashboard|The security dashboard displays the latest security report. Use it to find and fix vulnerabilities."
+msgid "SecurityReports|There was an error creating the issue."
msgstr ""
-msgid "SecurityDashboard|There was an error while generating the report."
+msgid "SecurityReports|There was an error creating the merge request."
msgstr ""
-msgid "SecurityDashboard|Unable to add %{invalidProjects}"
+msgid "SecurityReports|There was an error deleting the comment."
+msgstr ""
+
+msgid "SecurityReports|There was an error dismissing the vulnerabilities."
+msgstr ""
+
+msgid "SecurityReports|There was an error dismissing the vulnerability."
+msgstr ""
+
+msgid "SecurityReports|There was an error reverting the dismissal."
+msgstr ""
+
+msgid "SecurityReports|There was an error reverting this dismissal."
+msgstr ""
+
+msgid "SecurityReports|There was an error while generating the report."
+msgstr ""
+
+msgid "SecurityReports|Unable to add %{invalidProjects}"
+msgstr ""
+
+msgid "SecurityReports|Undo dismiss"
+msgstr ""
+
+msgid "SecurityReports|While it's rare to have no vulnerabilities for your group, it can happen. In any event, we ask that you double check your settings to make sure you've set up your dashboard correctly."
+msgstr ""
+
+msgid "SecurityReports|While it's rare to have no vulnerabilities for your pipeline, it can happen. In any event, we ask that you double check your settings to make sure all security scanning jobs have passed successfully."
+msgstr ""
+
+msgid "SecurityReports|While it's rare to have no vulnerabilities for your project, it can happen. In any event, we ask that you double check your settings to make sure you've set up your dashboard correctly."
+msgstr ""
+
+msgid "SecurityReports|While it's rare to have no vulnerabilities, it can happen. In any event, we ask that you please double check your settings to make sure you've set up your dashboard correctly."
+msgstr ""
+
+msgid "SecurityReports|Won't fix / Accept risk"
+msgstr ""
+
+msgid "SecurityReports|You do not have sufficient permissions to access this report"
+msgstr ""
+
+msgid "SecurityReports|You must sign in as an authorized user to see this report"
+msgstr ""
+
+msgid "SecurityReports|[No reason]"
msgstr ""
msgid "See GitLab's %{password_policy_guidelines}"
@@ -18065,6 +18489,9 @@ msgstr ""
msgid "See the affected projects in the GitLab admin panel"
msgstr ""
+msgid "See what's new at GitLab"
+msgstr ""
+
msgid "Select"
msgstr ""
@@ -18470,9 +18897,6 @@ msgstr ""
msgid "Set up Jira Integration"
msgstr ""
-msgid "Set up Jira Integration illustration"
-msgstr ""
-
msgid "Set up a %{type} Runner automatically"
msgstr ""
@@ -18557,6 +18981,9 @@ msgstr ""
msgid "Severity: %{severity}"
msgstr ""
+msgid "Shards (%{shards})"
+msgstr ""
+
msgid "Shards to synchronize"
msgstr ""
@@ -18871,6 +19298,9 @@ msgstr ""
msgid "Snowplow"
msgstr ""
+msgid "Some child epics may be hidden due to applied filters"
+msgstr ""
+
msgid "Some email servers do not support overriding the email sender name. Enable this option to include the name of the author of the issue, merge request or comment in the email body instead."
msgstr ""
@@ -19012,6 +19442,9 @@ msgstr ""
msgid "Something went wrong while updating your list settings"
msgstr ""
+msgid "Something went wrong with your automatic subscription renewal"
+msgstr ""
+
msgid "Something went wrong, unable to add %{project} to dashboard"
msgstr ""
@@ -19297,6 +19730,9 @@ msgstr ""
msgid "Stage removed"
msgstr ""
+msgid "Standard"
+msgstr ""
+
msgid "Star a label to make it a priority label. Order the prioritized labels to change their relative priority, by dragging."
msgstr ""
@@ -19420,6 +19856,9 @@ msgstr ""
msgid "Static Application Security Testing (SAST)"
msgstr ""
+msgid "StaticSiteEditor|An error occurred while submitting your changes."
+msgstr ""
+
msgid "StaticSiteEditor|Branch could not be created."
msgstr ""
@@ -19429,18 +19868,30 @@ msgstr ""
msgid "StaticSiteEditor|Could not create merge request."
msgstr ""
+msgid "StaticSiteEditor|Incompatible file content"
+msgstr ""
+
msgid "StaticSiteEditor|Return to site"
msgstr ""
+msgid "StaticSiteEditor|Static site editor"
+msgstr ""
+
msgid "StaticSiteEditor|Success!"
msgstr ""
msgid "StaticSiteEditor|Summary of changes"
msgstr ""
+msgid "StaticSiteEditor|The Static Site Editor is currently configured to only edit Markdown content on pages generated from Middleman. Visit the documentation to learn more about configuring your site to use the Static Site Editor."
+msgstr ""
+
msgid "StaticSiteEditor|Update %{sourcePath} file"
msgstr ""
+msgid "StaticSiteEditor|View documentation"
+msgstr ""
+
msgid "StaticSiteEditor|View merge request"
msgstr ""
@@ -19498,6 +19949,12 @@ msgstr ""
msgid "StatusPage|Status page"
msgstr ""
+msgid "StatusPage|Status page URL"
+msgstr ""
+
+msgid "StatusPage|Status page frontend documentation"
+msgstr ""
+
msgid "StatusPage|To publish incidents to an external status page, GitLab will store a JSON file in your Amazon S3 account in a location accessible to your external status page service. Make sure to also set up %{docsLink}"
msgstr ""
@@ -19846,6 +20303,12 @@ msgstr ""
msgid "Synced"
msgstr ""
+msgid "Synchronization disabled"
+msgstr ""
+
+msgid "Synchronization of container repositories is disabled."
+msgstr ""
+
msgid "System"
msgstr ""
@@ -19999,6 +20462,9 @@ msgstr ""
msgid "Telephone number"
msgstr ""
+msgid "Telephone number (Optional)"
+msgstr ""
+
msgid "Template"
msgstr ""
@@ -20305,6 +20771,9 @@ msgstr ""
msgid "The license was successfully uploaded and is now active. You can see the details below."
msgstr ""
+msgid "The license was successfully uploaded and will be active from %{starts_at}. You can see the details below."
+msgstr ""
+
msgid "The maximum file size allowed is %{size}."
msgstr ""
@@ -20626,6 +21095,9 @@ msgstr ""
msgid "There was an error getting the epic participants."
msgstr ""
+msgid "There was an error importing the Jira project."
+msgstr ""
+
msgid "There was an error loading users activity calendar."
msgstr ""
@@ -20641,7 +21113,7 @@ msgstr ""
msgid "There was an error resetting user pipeline minutes."
msgstr ""
-msgid "There was an error saving this Geo Node"
+msgid "There was an error saving this Geo Node."
msgstr ""
msgid "There was an error saving your changes."
@@ -20704,6 +21176,9 @@ msgstr ""
msgid "They can be managed using the %{link}."
msgstr ""
+msgid "Third Party Advisory Link"
+msgstr ""
+
msgid "Third party offers"
msgstr ""
@@ -20740,6 +21215,9 @@ msgstr ""
msgid "This application will be able to:"
msgstr ""
+msgid "This attachment has been truncated to avoid exceeding the maximum allowed attachment size of 15MB. %{written_count} of %{issues_count} issues have been included. Consider re-exporting with a narrower selection of issues."
+msgstr ""
+
msgid "This block is self-referential"
msgstr ""
@@ -20881,6 +21359,12 @@ msgstr ""
msgid "This issue is confidential"
msgstr ""
+msgid "This issue is currently blocked by the following issues: %{issues}."
+msgstr ""
+
+msgid "This issue is in a child epic of the filtered epic"
+msgstr ""
+
msgid "This issue is locked."
msgstr ""
@@ -21079,7 +21563,10 @@ msgstr ""
msgid "This user will be the author of all events in the activity feed that are the result of an update, like new branches being created or new commits being pushed to existing branches. Upon creation or when reassigning you can only assign yourself to be the mirror user."
msgstr ""
-msgid "This variable can not be masked"
+msgid "This variable can not be masked."
+msgstr ""
+
+msgid "This variable does not match the expected pattern."
msgstr ""
msgid "This will help us personalize your onboarding experience."
@@ -21109,10 +21596,10 @@ msgstr ""
msgid "ThreatMonitoring|Application firewall not detected"
msgstr ""
-msgid "ThreatMonitoring|Container Network Policy"
+msgid "ThreatMonitoring|Container Network Policies are not installed or have been disabled. To view this data, ensure your Network Policies are installed and enabled for your cluster."
msgstr ""
-msgid "ThreatMonitoring|Container NetworkPolicies are not installed or has been disabled. To view this data, ensure you NetworkPolicies are installed and enabled for your cluster."
+msgid "ThreatMonitoring|Container Network Policy"
msgstr ""
msgid "ThreatMonitoring|Container NetworkPolicies not detected"
@@ -21145,7 +21632,7 @@ msgstr ""
msgid "ThreatMonitoring|Something went wrong, unable to fetch statistics"
msgstr ""
-msgid "ThreatMonitoring|The firewall is not installed or has been disabled. To view this data, ensure you firewall is installed and enabled for your cluster."
+msgid "ThreatMonitoring|The firewall is not installed or has been disabled. To view this data, ensure the web application firewall is installed and enabled for your cluster."
msgstr ""
msgid "ThreatMonitoring|The graph below is an overview of traffic coming to your application as tracked by the Web Application Firewall (WAF). View the docs for instructions on how to access the WAF logs to see what type of malicious traffic is trying to access your app. The docs link is also accessible by clicking the \"?\" icon next to the title below."
@@ -21214,6 +21701,9 @@ msgstr ""
msgid "Time in seconds GitLab will wait for a response from the external service. When the service does not respond in time, access will be denied."
msgstr ""
+msgid "Time of import: %{importTime}"
+msgstr ""
+
msgid "Time remaining"
msgstr ""
@@ -21398,7 +21888,7 @@ msgstr ""
msgid "Title:"
msgstr ""
-msgid "Titles and Filenames"
+msgid "Titles and Descriptions"
msgstr ""
msgid "To"
@@ -21605,7 +22095,7 @@ msgstr ""
msgid "Too many projects enabled. You will need to manage them via the console or the API."
msgstr ""
-msgid "Topics"
+msgid "Topics (optional)"
msgstr ""
msgid "Total"
@@ -21767,6 +22257,9 @@ msgstr ""
msgid "Try all GitLab has to offer for 30 days."
msgstr ""
+msgid "Try changing or removing filters."
+msgstr ""
+
msgid "Try to fork again"
msgstr ""
@@ -21860,6 +22353,9 @@ msgstr ""
msgid "Unable to connect to server: %{error}"
msgstr ""
+msgid "Unable to connect to the Jira instance. Please check your Jira integration configuration."
+msgstr ""
+
msgid "Unable to convert Kubernetes logs encoding to UTF-8"
msgstr ""
@@ -22803,6 +23299,9 @@ msgstr ""
msgid "View issue"
msgstr ""
+msgid "View issues"
+msgstr ""
+
msgid "View it on GitLab"
msgstr ""
@@ -22980,6 +23479,15 @@ msgstr ""
msgid "VulnerabilityManagement|Resolved %{timeago} by %{user}"
msgstr ""
+msgid "VulnerabilityManagement|Something went wrong while trying to delete the comment. Please try again later."
+msgstr ""
+
+msgid "VulnerabilityManagement|Something went wrong while trying to retrieve the vulnerability history. Please try again later."
+msgstr ""
+
+msgid "VulnerabilityManagement|Something went wrong while trying to save the comment. Please try again later."
+msgstr ""
+
msgid "VulnerabilityManagement|Something went wrong, could not create an issue."
msgstr ""
@@ -23010,6 +23518,9 @@ msgstr ""
msgid "VulnerabilityStatusTypes|Resolved"
msgstr ""
+msgid "Vulnerability|%{scannerName} (version %{scannerVersion})"
+msgstr ""
+
msgid "Vulnerability|Class"
msgstr ""
@@ -23043,7 +23554,10 @@ msgstr ""
msgid "Vulnerability|Project"
msgstr ""
-msgid "Vulnerability|Report Type"
+msgid "Vulnerability|Scanner Provider"
+msgstr ""
+
+msgid "Vulnerability|Scanner Type"
msgstr ""
msgid "Vulnerability|Severity"
@@ -23097,9 +23611,15 @@ msgstr ""
msgid "We sent you an email with reset password instructions"
msgstr ""
+msgid "We tried to automatically renew your %{strong}%{plan_name}%{strong_close} subscription for %{strong}%{namespace_name}%{strong_close} on %{expires_on} but something went wrong so your subscription was downgraded to the free plan. Don't worry, your data is safe. We suggest you check your payment method and get in touch with our support team (%{support_link}). They'll gladly help with your subscription renewal."
+msgstr ""
+
msgid "We want to be sure it is you, please confirm you are not a robot."
msgstr ""
+msgid "We will automatically renew your %{strong}%{plan_name}%{strong_close} subscription for %{strong}%{namespace_name}%{strong_close} on %{strong}%{expires_on}%{strong_close}. There's nothing that you need to do, we'll let you know when the renewal is complete. Need more seats, a higher plan or just want to review your payment method?"
+msgstr ""
+
msgid "We've found no vulnerabilities"
msgstr ""
@@ -23204,15 +23724,6 @@ msgstr ""
msgid "When:"
msgstr ""
-msgid "While it's rare to have no vulnerabilities for your group, it can happen. In any event, we ask that you double check your settings to make sure you've set up your dashboard correctly."
-msgstr ""
-
-msgid "While it's rare to have no vulnerabilities for your pipeline, it can happen. In any event, we ask that you double check your settings to make sure all security scanning jobs have passed successfully."
-msgstr ""
-
-msgid "While it's rare to have no vulnerabilities for your project, it can happen. In any event, we ask that you double check your settings to make sure you've set up your dashboard correctly."
-msgstr ""
-
msgid "While it's rare to have no vulnerabilities, it can happen. In any event, we ask that you please double check your settings to make sure you've set up your dashboard correctly."
msgstr ""
@@ -23243,9 +23754,6 @@ msgstr ""
msgid "Wiki"
msgstr ""
-msgid "Wiki pages"
-msgstr ""
-
msgid "Wiki was successfully updated."
msgstr ""
@@ -23429,6 +23937,9 @@ msgstr ""
msgid "Yes, add it"
msgstr ""
+msgid "Yes, close issue"
+msgstr ""
+
msgid "Yes, let me map Google Code users to full names or GitLab users."
msgstr ""
@@ -23624,6 +24135,9 @@ msgstr ""
msgid "You could not create a new trigger."
msgstr ""
+msgid "You didn't renew your %{strong}%{plan_name}%{strong_close} subscription for %{strong}%{namespace_name}%{strong_close} so it was downgraded to the free plan."
+msgstr ""
+
msgid "You didn't renew your %{strong}%{plan_name}%{strong_close} subscription so it was downgraded to the GitLab Core Plan."
msgstr ""
@@ -23690,6 +24204,9 @@ msgstr ""
msgid "You have declined the invitation to join %{label}."
msgstr ""
+msgid "You have imported from this project %{numberOfPreviousImportsForProject} times before. Each new import will create duplicate issues."
+msgstr ""
+
msgid "You have no permissions"
msgstr ""
@@ -23771,6 +24288,9 @@ msgstr ""
msgid "You need to upload a Google Takeout archive."
msgstr ""
+msgid "You reached %{usage_in_percent} of %{namespace_name}'s capacity (%{used_storage} of %{storage_limit})"
+msgstr ""
+
msgid "You tried to fork %{link_to_the_project} but it failed for the following reason:"
msgstr ""
@@ -23855,9 +24375,21 @@ msgstr ""
msgid "YouTube"
msgstr ""
+msgid "Your %{strong}%{plan_name}%{strong_close} subscription for %{strong}%{namespace_name}%{strong_close} will expire on %{strong}%{expires_on}%{strong_close}. After that, you will not to be able to create issues or merge requests as well as many other features."
+msgstr ""
+
msgid "Your %{strong}%{plan_name}%{strong_close} subscription will expire on %{strong}%{expires_on}%{strong_close}. After that, you will not to be able to create issues or merge requests as well as many other features."
msgstr ""
+msgid "Your CSV export has started. It will be emailed to %{email} when complete."
+msgstr ""
+
+msgid "Your CSV export of %{issues_count} from project %{project_link} has been added to this email as an attachment."
+msgstr ""
+
+msgid "Your CSV export of %{written_count} from project %{project_name} (%{project_url}) has been added to this email as an attachment."
+msgstr ""
+
msgid "Your Commit Email will be used for web based operations, such as edits and merges."
msgstr ""
@@ -23954,6 +24486,9 @@ msgstr ""
msgid "Your comment could not be updated! Please check your network connection and try again."
msgstr ""
+msgid "Your comment will be discarded."
+msgstr ""
+
msgid "Your custom stage '%{title}' was created"
msgstr ""
@@ -23993,6 +24528,9 @@ msgstr ""
msgid "Your new personal access token has been created."
msgstr ""
+msgid "Your new project access token has been created."
+msgstr ""
+
msgid "Your password isn't required to view this page. If a password or any other personal details are requested, please contact your administrator to report abuse."
msgstr ""
@@ -24011,12 +24549,18 @@ msgstr ""
msgid "Your request for access has been queued for review."
msgstr ""
+msgid "Your search didn't match any commits."
+msgstr ""
+
msgid "Your subscription expired!"
msgstr ""
msgid "Your subscription has been downgraded"
msgstr ""
+msgid "Your subscription will automatically renew in %{remaining_days}"
+msgstr ""
+
msgid "Your subscription will expire in %{remaining_days}"
msgstr ""
@@ -24055,7 +24599,7 @@ msgstr ""
msgid "allowed to fail"
msgstr ""
-msgid "already being used for another group or project milestone."
+msgid "already being used for another group or project %{timebox_name}."
msgstr ""
msgid "already has a \"created\" issue link"
@@ -24133,6 +24677,9 @@ msgstr ""
msgid "ciReport|%{linkStartTag}Learn more about SAST %{linkEndTag}"
msgstr ""
+msgid "ciReport|%{linkStartTag}Learn more about Secret Scanning %{linkEndTag}"
+msgstr ""
+
msgid "ciReport|%{linkStartTag}Learn more about codequality reports %{linkEndTag}"
msgstr ""
@@ -24296,6 +24843,12 @@ msgstr ""
msgid "ciReport|SAST"
msgstr ""
+msgid "ciReport|Secret scanning"
+msgstr ""
+
+msgid "ciReport|Secret scanning detects secrets and credentials vulnerabilities in your source code."
+msgstr ""
+
msgid "ciReport|Security scanning"
msgstr ""
@@ -24493,6 +25046,9 @@ msgstr ""
msgid "group"
msgstr ""
+msgid "groups"
+msgstr ""
+
msgid "has already been linked to another vulnerability"
msgstr ""
@@ -24656,9 +25212,6 @@ msgstr[1] ""
msgid "merged %{time_ago}"
msgstr ""
-msgid "milestone should belong either to a project or a group."
-msgstr ""
-
msgid "missing"
msgstr ""
@@ -24995,6 +25548,9 @@ msgstr ""
msgid "no contributions"
msgstr ""
+msgid "no expiration"
+msgstr ""
+
msgid "no one can merge"
msgstr ""
@@ -25047,6 +25603,9 @@ msgstr ""
msgid "pending removal"
msgstr ""
+msgid "per day"
+msgstr ""
+
msgid "pipeline"
msgstr ""
@@ -25075,6 +25634,12 @@ msgstr[1] ""
msgid "project avatar"
msgstr ""
+msgid "projects"
+msgstr ""
+
+msgid "push to your repository, create pipelines, create issues or add comments. To reduce storage capacity, delete unused repositories, artifacts, wikis, issues, and pipelines."
+msgstr ""
+
msgid "quick actions"
msgstr ""
@@ -25221,11 +25786,6 @@ msgstr ""
msgid "this document"
msgstr ""
-msgid "thread resolved"
-msgid_plural "threads resolved"
-msgstr[0] ""
-msgstr[1] ""
-
msgid "to help your contributors communicate effectively!"
msgstr ""
@@ -25280,6 +25840,9 @@ msgstr ""
msgid "view the blob"
msgstr ""
+msgid "vulnerability|Add a comment"
+msgstr ""
+
msgid "vulnerability|Add a comment or reason for dismissal"
msgstr ""
diff --git a/locale/ko/gitlab.po b/locale/ko/gitlab.po
index 04eb87189d0..1bc537934f5 100644
--- a/locale/ko/gitlab.po
+++ b/locale/ko/gitlab.po
@@ -12,7 +12,7 @@ msgstr ""
"X-Crowdin-Project: gitlab-ee\n"
"X-Crowdin-Language: ko\n"
"X-Crowdin-File: /master/locale/gitlab.pot\n"
-"PO-Revision-Date: 2020-04-15 00:25\n"
+"PO-Revision-Date: 2020-05-05 21:11\n"
msgid " %{start} to %{end}"
msgstr ""
@@ -182,6 +182,10 @@ msgid "%d tag"
msgid_plural "%d tags"
msgstr[0] ""
+msgid "%d unresolved thread"
+msgid_plural "%d unresolved threads"
+msgstr[0] ""
+
msgid "%d vulnerability dismissed"
msgid_plural "%d vulnerabilities dismissed"
msgstr[0] ""
@@ -335,6 +339,9 @@ msgstr "%{mergeLength}/%{usersLength} 머지 가능"
msgid "%{mrText}, this issue will be closed automatically."
msgstr "%{mrText}, ì´ ì´ìŠˆëŠ” ìžë™ìœ¼ë¡œ í´ë¡œì¦ˆ ë©ë‹ˆë‹¤."
+msgid "%{namespace_name} is now read-only. You cannot: %{base_message}"
+msgstr ""
+
msgid "%{name} contained %{resultsString}"
msgstr "%{name} í¬í•¨ë¨ %{resultsString}"
@@ -347,6 +354,10 @@ msgstr ""
msgid "%{name}'s avatar"
msgstr "%{name} ì˜ ì•„ë°”íƒ€"
+msgid "%{no_of_days} day"
+msgid_plural "%{no_of_days} days"
+msgstr[0] ""
+
msgid "%{number_commits_behind} commits behind %{default_branch}, %{number_commits_ahead} commits ahead"
msgstr "%{default_branch} 로부터 %{number_commits_behind} commits behind, %{number_commits_ahead} commits ahead"
@@ -464,6 +475,9 @@ msgstr[0] "%{text} %{files} 파ì¼"
msgid "%{text} is available"
msgstr "%{text} 사용 가능"
+msgid "%{timebox_name} should belong either to a project or a group."
+msgstr ""
+
msgid "%{title} %{operator} %{threshold}"
msgstr "%{title} %{operator} %{threshold}"
@@ -720,12 +734,18 @@ msgstr "<code>\"johnsmith@example.com\": \"johnsm... @ example.com\"</code> ì€
msgid "<code>\"johnsmith@example.com\": \"johnsmith@example.com\"</code> will add \"By <a href=\"#\">johnsmith@example.com</a>\" to all issues and comments originally created by johnsmith@example.com. By default, the email address or username is masked to ensure the user's privacy. Use this option if you want to show the full email address."
msgstr "<code>\"johnsmith@example.com\": \"johnsmith@example.com\"</code> ì€ ì›ëž˜ johnsmith@example.comì´ ìƒì„±í•œ 모든 ì´ìŠˆì™€ 주ì„ì— \"By <a href=\"#\">johnsmith@example.com</a>\"ì„ ì¶”ê°€í•©ë‹ˆë‹¤. 기본ì ìœ¼ë¡œ ì´ë©”ì¼ ì£¼ì†Œ ë˜ëŠ” ì‚¬ìš©ìž ì´ë¦„ì€ ê°€ë ¤ì ¸ 있어서 사용ìžì˜ ê°œì¸ì •ë³´ë¥¼ 보호합니다. ì „ì²´ ì „ìž ë©”ì¼ ì£¼ì†Œë¥¼ 표시하려면 ì´ ì˜µì…˜ì„ ì‚¬ìš©í•˜ì‹­ì‹œì˜¤."
+msgid "<namespace / project>"
+msgstr ""
+
msgid "<no name set>"
msgstr ""
msgid "<no scopes selected>"
msgstr ""
+msgid "<project name>"
+msgstr ""
+
msgid "<strong>%{group_name}</strong> group members"
msgstr "<strong>%{group_name}</strong> 그룹 멤버"
@@ -759,6 +779,9 @@ msgstr "ì´ë©”ì¼ ì£¼ì†Œë¥¼ ì‚¬ìš©í•˜ì—¬ì´ GitLab ì„¤ì¹˜ì— ëŒ€í•´ Let 's Encry
msgid "A basic page and serverless function that uses AWS Lambda, AWS API Gateway, and GitLab Pages"
msgstr ""
+msgid "A complete DevOps platform"
+msgstr ""
+
msgid "A default branch cannot be chosen for an empty project."
msgstr "빈 프로ì íŠ¸ì—서는 기본 브랜치를 ì„ íƒí•  수 없습니다."
@@ -975,6 +998,12 @@ msgstr ""
msgid "AccessTokens|reset it"
msgstr "재설정"
+msgid "AccessibilityReport|Accessibility report artifact not found"
+msgstr ""
+
+msgid "AccessibilityReport|Failed to retrieve accessibility report"
+msgstr ""
+
msgid "AccessibilityReport|Learn More"
msgstr ""
@@ -984,6 +1013,9 @@ msgstr ""
msgid "AccessibilityReport|New"
msgstr ""
+msgid "AccessibilityReport|The accessibility scanning found an error of the following type: %{code}"
+msgstr ""
+
msgid "Account"
msgstr "계정"
@@ -1225,6 +1257,9 @@ msgstr ""
msgid "Added at"
msgstr "ì— ì¶”ê°€ë¨"
+msgid "Added for this merge request"
+msgstr ""
+
msgid "Added in this version"
msgstr ""
@@ -1595,6 +1630,72 @@ msgid "Alert"
msgid_plural "Alerts"
msgstr[0] "알림"
+msgid "Alert Details"
+msgstr ""
+
+msgid "AlertManagement|Acknowledged"
+msgstr ""
+
+msgid "AlertManagement|Alert"
+msgstr ""
+
+msgid "AlertManagement|Authorize external service"
+msgstr ""
+
+msgid "AlertManagement|Display alerts from all your monitoring tools directly within GitLab. Streamline the investigation of your alerts and the escalation of alerts to incidents."
+msgstr ""
+
+msgid "AlertManagement|End time"
+msgstr ""
+
+msgid "AlertManagement|End time:"
+msgstr ""
+
+msgid "AlertManagement|Events"
+msgstr ""
+
+msgid "AlertManagement|Events:"
+msgstr ""
+
+msgid "AlertManagement|Full Alert Details"
+msgstr ""
+
+msgid "AlertManagement|More information"
+msgstr ""
+
+msgid "AlertManagement|No alerts available to display. If you think you're seeing this message in error, refresh the page."
+msgstr ""
+
+msgid "AlertManagement|No alerts to display."
+msgstr ""
+
+msgid "AlertManagement|Overview"
+msgstr ""
+
+msgid "AlertManagement|Resolved"
+msgstr ""
+
+msgid "AlertManagement|Severity"
+msgstr ""
+
+msgid "AlertManagement|Start time"
+msgstr ""
+
+msgid "AlertManagement|Start time:"
+msgstr ""
+
+msgid "AlertManagement|Status"
+msgstr ""
+
+msgid "AlertManagement|Surface alerts in GitLab"
+msgstr ""
+
+msgid "AlertManagement|There was an error displaying the alerts. Confirm your endpoint's configuration details to ensure alerts appear."
+msgstr ""
+
+msgid "AlertManagement|Triggered"
+msgstr ""
+
msgid "AlertService|%{linkStart}Learn more%{linkEnd} about configuring this endpoint to receive alerts."
msgstr ""
@@ -1664,6 +1765,9 @@ msgstr "모든 프로ì íŠ¸"
msgid "All security scans are enabled because %{linkStart}Auto DevOps%{linkEnd} is enabled on this project"
msgstr ""
+msgid "All threads resolved"
+msgstr ""
+
msgid "All users"
msgstr "모든 사용ìž"
@@ -1682,6 +1786,9 @@ msgstr ""
msgid "Allow only the selected protocols to be used for Git access."
msgstr ""
+msgid "Allow owners to manage default branch protection per group"
+msgstr ""
+
msgid "Allow owners to manually add users outside of LDAP"
msgstr ""
@@ -1790,6 +1897,9 @@ msgstr "새로운 ê·œì¹™ì— ëŒ€í•œ 승ì¸ìž 목ë¡ì„ 가져오는 ë„중 오ë¥
msgid "An error occurred fetching the dropdown data."
msgstr "드롭다운 ë°ì´í„°ë¥¼ 가져오는 ë„중 오류가 ë°œìƒí–ˆìŠµë‹ˆë‹¤."
+msgid "An error occurred fetching the project authors."
+msgstr ""
+
msgid "An error occurred previewing the blob"
msgstr "BLOB 미리보기 ì¤‘ì— ì˜¤ë¥˜ê°€ ë°œìƒí–ˆìŠµë‹ˆë‹¤."
@@ -1871,6 +1981,9 @@ msgstr ""
msgid "An error occurred while fetching sidebar data"
msgstr "사ì´ë“œë°” ë°ì´í„°ë¥¼ 가져오는 중 문제가 ë°œìƒí–ˆìŠµë‹ˆë‹¤."
+msgid "An error occurred while fetching terraform reports."
+msgstr ""
+
msgid "An error occurred while fetching the Service Desk address."
msgstr ""
@@ -2108,6 +2221,9 @@ msgstr "안티 스팸 ê²€ì¦"
msgid "Any"
msgstr "Any"
+msgid "Any Author"
+msgstr ""
+
msgid "Any Label"
msgstr "모든 ë¼ë²¨"
@@ -2319,10 +2435,10 @@ msgstr ""
msgid "Are you sure that you want to unarchive this project?"
msgstr ""
-msgid "Are you sure you want to cancel creating this comment?"
+msgid "Are you sure you want to cancel editing this comment?"
msgstr ""
-msgid "Are you sure you want to cancel editing this comment?"
+msgid "Are you sure you want to close this blocked issue?"
msgstr ""
msgid "Are you sure you want to delete %{name}?"
@@ -2352,6 +2468,9 @@ msgstr ""
msgid "Are you sure you want to deploy this environment?"
msgstr ""
+msgid "Are you sure you want to discard this comment?"
+msgstr ""
+
msgid "Are you sure you want to erase this build?"
msgstr "ì •ë§ë¡œ ì´ ë¹Œë“œë¥¼ 지우시겠습니까?"
@@ -2527,6 +2646,9 @@ msgstr ""
msgid "At least one approval from a code owner is required to change files matching the respective CODEOWNER rules."
msgstr ""
+msgid "At least one logging option is required to be enabled"
+msgstr ""
+
msgid "At least one of group_id or project_id must be specified"
msgstr ""
@@ -2564,6 +2686,27 @@ msgstr ""
msgid "AuditEvents|Target"
msgstr ""
+msgid "AuditLogs|(removed)"
+msgstr ""
+
+msgid "AuditLogs|Action"
+msgstr ""
+
+msgid "AuditLogs|Author"
+msgstr ""
+
+msgid "AuditLogs|Date"
+msgstr ""
+
+msgid "AuditLogs|IP Address"
+msgstr ""
+
+msgid "AuditLogs|Object"
+msgstr ""
+
+msgid "AuditLogs|Target"
+msgstr ""
+
msgid "Aug"
msgstr "8ì›”"
@@ -2846,9 +2989,6 @@ msgstr ""
msgid "BambooService|You must set up automatic revision labeling and a repository trigger in Bamboo."
msgstr ""
-msgid "Batch operations"
-msgstr ""
-
msgid "BatchComments|Delete all pending comments"
msgstr "모든 보류 ì¤‘ì— ìžˆëŠ” 커맨트 ì‚­ì œ"
@@ -2978,6 +3118,9 @@ msgstr ""
msgid "Boards"
msgstr "보드"
+msgid "Boards and Board Lists"
+msgstr ""
+
msgid "Boards|Collapse"
msgstr ""
@@ -3197,6 +3340,9 @@ msgstr "EE 구입"
msgid "Buy GitLab Enterprise Edition"
msgstr "GitLab Enterprise Edition 구입"
+msgid "Buy more Pipeline minutes"
+msgstr ""
+
msgid "By %{user_name}"
msgstr "%{user_name} ì˜í•´ì„œ"
@@ -3317,6 +3463,9 @@ msgstr ""
msgid "Can't scan the code?"
msgstr ""
+msgid "Can't update snippet: %{err}"
+msgstr ""
+
msgid "Canary"
msgstr ""
@@ -3485,6 +3634,9 @@ msgstr ""
msgid "Changing group path can have unintended side effects."
msgstr ""
+msgid "Charts"
+msgstr ""
+
msgid "Charts can't be displayed as the request for data has timed out. %{documentationLink}"
msgstr ""
@@ -3521,9 +3673,6 @@ msgstr ""
msgid "ChatMessage|and [%{count} more](%{pipeline_failed_jobs_url})"
msgstr ""
-msgid "ChatMessage|failed"
-msgstr ""
-
msgid "ChatMessage|has failed"
msgstr ""
@@ -3539,9 +3688,6 @@ msgstr ""
msgid "ChatMessage|in %{project_link}"
msgstr ""
-msgid "ChatMessage|passed"
-msgstr ""
-
msgid "Check again"
msgstr "다시 확ì¸"
@@ -3992,9 +4138,6 @@ msgstr ""
msgid "Click any <strong>project name</strong> in the project list below to navigate to the project milestone."
msgstr "아래 프로ì íŠ¸ 목ë¡ì—ì„œ <strong>프로ì íŠ¸ ì´ë¦„</strong>ì„ ëˆŒëŸ¬ 프로ì íŠ¸ 마ì¼ìŠ¤í†¤ì„ 봅니다."
-msgid "Click here"
-msgstr ""
-
msgid "Click the <strong>Download</strong> button and wait for downloading to complete."
msgstr "<strong>다운로드</strong> ë²„íŠ¼ì„ í´ë¦­í•˜ê³  다운로드가 완료 ë  ë•Œê¹Œì§€ 기다려 주세요."
@@ -4382,6 +4525,12 @@ msgstr "프로ì íŠ¸ 가져오기"
msgid "ClusterIntegration|Fetching zones"
msgstr "ì˜ì—­ 가져오기"
+msgid "ClusterIntegration|Fluentd"
+msgstr ""
+
+msgid "ClusterIntegration|Fluentd is an open source data collector, which lets you unify the data collection and consumption for a better use and understanding of data. It requires at least one of the following logs to be successfully installed."
+msgstr ""
+
msgid "ClusterIntegration|GitLab Integration"
msgstr "GitLab 통합"
@@ -4688,6 +4837,15 @@ msgstr "설치 시작 요청 실패"
msgid "ClusterIntegration|Request to begin uninstalling failed"
msgstr ""
+msgid "ClusterIntegration|SIEM Hostname"
+msgstr ""
+
+msgid "ClusterIntegration|SIEM Port"
+msgstr ""
+
+msgid "ClusterIntegration|SIEM Protocol"
+msgstr ""
+
msgid "ClusterIntegration|Save changes"
msgstr "변경 ì‚¬í•­ì„ ì €ìž¥"
@@ -4784,6 +4942,12 @@ msgstr "ClusterIntegration|지역 ì„ íƒ"
msgid "ClusterIntegration|Select zone to choose machine type"
msgstr ""
+msgid "ClusterIntegration|Send Cilium Logs"
+msgstr ""
+
+msgid "ClusterIntegration|Send ModSecurity Logs"
+msgstr ""
+
msgid "ClusterIntegration|Service Token"
msgstr ""
@@ -5211,18 +5375,33 @@ msgstr ""
msgid "Compliance framework (optional)"
msgstr ""
+msgid "ComplianceFramework|GDPR"
+msgstr ""
+
msgid "ComplianceFramework|GDPR - General Data Protection Regulation"
msgstr ""
+msgid "ComplianceFramework|HIPAA"
+msgstr ""
+
msgid "ComplianceFramework|HIPAA - Health Insurance Portability and Accountability Act"
msgstr ""
+msgid "ComplianceFramework|PCI-DSS"
+msgstr ""
+
msgid "ComplianceFramework|PCI-DSS - Payment Card Industry-Data Security Standard"
msgstr ""
+msgid "ComplianceFramework|SOC 2"
+msgstr ""
+
msgid "ComplianceFramework|SOC 2 - Service Organization Control 2"
msgstr ""
+msgid "ComplianceFramework|SOX"
+msgstr ""
+
msgid "ComplianceFramework|SOX - Sarbanes-Oxley"
msgstr ""
@@ -5370,9 +5549,15 @@ msgstr ""
msgid "Container repositories sync capacity"
msgstr ""
+msgid "ContainerRegistry| Please visit the %{linkStart}administration settings%{linkEnd} to enable this feature."
+msgstr ""
+
msgid "ContainerRegistry|%{imageName} tags"
msgstr ""
+msgid "ContainerRegistry|%{title} was successfully scheduled for deletion"
+msgstr ""
+
msgid "ContainerRegistry|Automatically remove extra images that aren't designed to be kept."
msgstr ""
@@ -5385,6 +5570,9 @@ msgstr ""
msgid "ContainerRegistry|Container Registry"
msgstr ""
+msgid "ContainerRegistry|Container Registry tag expiration and retention policy is disabled"
+msgstr ""
+
msgid "ContainerRegistry|Copy build command"
msgstr ""
@@ -5394,18 +5582,12 @@ msgstr ""
msgid "ContainerRegistry|Copy push command"
msgstr ""
-msgid "ContainerRegistry|Currently, the Container Registry tag expiration feature is not available for projects created before GitLab version 12.8. For updates and more information, visit Issue %{linkStart}#196124%{linkEnd}"
-msgstr ""
-
msgid "ContainerRegistry|Docker connection error"
msgstr ""
msgid "ContainerRegistry|Docker tag expiration policy is %{toggleStatus}"
msgstr ""
-msgid "ContainerRegistry|Docker tags with names matching this regex pattern will expire:"
-msgstr ""
-
msgid "ContainerRegistry|Edit Settings"
msgstr ""
@@ -5427,9 +5609,6 @@ msgstr ""
msgid "ContainerRegistry|Image ID"
msgstr ""
-msgid "ContainerRegistry|Image deleted successfully"
-msgstr ""
-
msgid "ContainerRegistry|Keep and protect the images that matter most."
msgstr ""
@@ -5445,12 +5624,24 @@ msgstr ""
msgid "ContainerRegistry|Number of tags to retain:"
msgstr ""
+msgid "ContainerRegistry|Please contact your administrator."
+msgstr ""
+
msgid "ContainerRegistry|Push an image"
msgstr ""
msgid "ContainerRegistry|Quick Start"
msgstr ""
+msgid "ContainerRegistry|Regular expressions such as %{codeStart}.*-test%{codeEnd} or %{codeStart}dev-.*%{codeEnd} are supported"
+msgstr ""
+
+msgid "ContainerRegistry|Regular expressions such as %{codeStart}.*-test%{codeEnd} or %{codeStart}dev-.*%{codeEnd} are supported. To select all tags, use %{codeStart}.*%{codeEnd}"
+msgstr ""
+
+msgid "ContainerRegistry|Remember to run %{docLinkStart}garbage collection%{docLinkEnd} to remove the stale data from storage."
+msgstr ""
+
msgid "ContainerRegistry|Remove repository"
msgstr "저장소 제거"
@@ -5464,22 +5655,22 @@ msgstr[0] ""
msgid "ContainerRegistry|Retention policy has been Enabled"
msgstr ""
-msgid "ContainerRegistry|Something went wrong while deleting the image."
+msgid "ContainerRegistry|Something went wrong while fetching the expiration policy."
msgstr ""
-msgid "ContainerRegistry|Something went wrong while deleting the tag."
+msgid "ContainerRegistry|Something went wrong while fetching the repository list."
msgstr ""
-msgid "ContainerRegistry|Something went wrong while deleting the tags."
+msgid "ContainerRegistry|Something went wrong while fetching the tags list."
msgstr ""
-msgid "ContainerRegistry|Something went wrong while fetching the expiration policy."
+msgid "ContainerRegistry|Something went wrong while marking the tag for deletion."
msgstr ""
-msgid "ContainerRegistry|Something went wrong while fetching the packages list."
+msgid "ContainerRegistry|Something went wrong while marking the tags for deletion."
msgstr ""
-msgid "ContainerRegistry|Something went wrong while fetching the tags list."
+msgid "ContainerRegistry|Something went wrong while scheduling %{title} for deletion. Please try again."
msgstr ""
msgid "ContainerRegistry|Something went wrong while updating the expiration policy."
@@ -5488,16 +5679,25 @@ msgstr ""
msgid "ContainerRegistry|Tag"
msgstr "태그"
-msgid "ContainerRegistry|Tag deleted successfully"
-msgstr ""
-
msgid "ContainerRegistry|Tag expiration policy"
msgstr ""
msgid "ContainerRegistry|Tag expiration policy is designed to:"
msgstr ""
-msgid "ContainerRegistry|Tags deleted successfully"
+msgid "ContainerRegistry|Tag successfully marked for deletion."
+msgstr ""
+
+msgid "ContainerRegistry|Tags successfully marked for deletion."
+msgstr ""
+
+msgid "ContainerRegistry|Tags with names matching this regex pattern will %{italicStart}be preserved:%{italicEnd}"
+msgstr ""
+
+msgid "ContainerRegistry|Tags with names matching this regex pattern will %{italicStart}expire:%{italicEnd}"
+msgstr ""
+
+msgid "ContainerRegistry|The Container Registry tag expiration and retention policies for this project have not been enabled."
msgstr ""
msgid "ContainerRegistry|The last tag related to this image was recently removed. This empty image and any associated data will be automatically removed as part of the regular Garbage Collection process. If you have any questions, contact your administrator."
@@ -5518,16 +5718,16 @@ msgstr ""
msgid "ContainerRegistry|There are no container images stored for this project"
msgstr ""
-msgid "ContainerRegistry|This Registry contains deleted image tag data. Remember to run %{docLinkStart}garbage collection%{docLinkEnd} to remove the stale data from storage."
+msgid "ContainerRegistry|There was an error during the deletion of this image repository, please try again."
msgstr ""
msgid "ContainerRegistry|This image has no active tags"
msgstr ""
-msgid "ContainerRegistry|We are having trouble connecting to Docker, which could be due to an issue with your project name or path. %{docLinkStart}More Information%{docLinkEnd}"
+msgid "ContainerRegistry|This image repository is scheduled for deletion"
msgstr ""
-msgid "ContainerRegistry|Wildcards such as %{codeStart}.*-stable%{codeEnd} or %{codeStart}production/.*%{codeEnd} are supported. To select all tags, use %{codeStart}.*%{codeEnd}"
+msgid "ContainerRegistry|We are having trouble connecting to Docker, which could be due to an issue with your project name or path. %{docLinkStart}More Information%{docLinkEnd}"
msgstr ""
msgid "ContainerRegistry|With the Container Registry, every project can have its own space to store its Docker images. %{docLinkStart}More Information%{docLinkEnd}"
@@ -5782,7 +5982,7 @@ msgstr ""
msgid "Could not delete chat nickname %{chat_name}."
msgstr "채팅 닉네임 %{chat_name}(ì„)를 삭제할 수 없습니다."
-msgid "Could not find design"
+msgid "Could not find design."
msgstr ""
msgid "Could not remove the trigger."
@@ -5797,6 +5997,9 @@ msgstr ""
msgid "Could not revoke personal access token %{personal_access_token_name}."
msgstr ""
+msgid "Could not revoke project access token %{project_access_token_name}."
+msgstr ""
+
msgid "Could not save group ID"
msgstr ""
@@ -5854,6 +6057,9 @@ msgstr ""
msgid "Create a new branch"
msgstr "새 브랜치 ìƒì„±"
+msgid "Create a new deploy key for this project"
+msgstr ""
+
msgid "Create a new file as there are no files yet. Afterwards, you'll be able to commit your changes."
msgstr ""
@@ -5959,6 +6165,9 @@ msgstr "새로운 프로ì íŠ¸ ë¼ë²¨"
msgid "Create requirement"
msgstr ""
+msgid "Create snippet"
+msgstr ""
+
msgid "Create wildcard: %{searchTerm}"
msgstr ""
@@ -5980,6 +6189,9 @@ msgstr "ê°œì¸ ì•¡ì„¸ìŠ¤ í† í° ë§Œë“¤ê¸°"
msgid "Created"
msgstr "ìƒì„±ë¨"
+msgid "Created %{timestamp}"
+msgstr ""
+
msgid "Created At"
msgstr "ìƒì„± 위치"
@@ -6335,6 +6547,9 @@ msgstr ""
msgid "CycleAnalytics|stage dropdown"
msgstr ""
+msgid "DAG"
+msgstr ""
+
msgid "DNS"
msgstr "DNS"
@@ -6695,6 +6910,9 @@ msgstr "ë°°í¬ í‚¤"
msgid "Deploy key was successfully updated."
msgstr ""
+msgid "Deploy keys allow read-only or read-write (if enabled) access to your repository. Deploy keys can be used for CI, staging or production servers. You can create a deploy key or add an existing one."
+msgstr ""
+
msgid "Deploy progress not found. To see pods, ensure your environment matches %{linkStart}deploy board criteria%{linkEnd}."
msgstr ""
@@ -6848,6 +7066,9 @@ msgstr "ë°°í¬ë¨"
msgid "Deploying to"
msgstr "다ìŒì— ë°°í¬ì¤‘: "
+msgid "Deployment Frequency"
+msgstr ""
+
msgid "Deployment|API"
msgstr ""
@@ -7365,9 +7586,6 @@ msgstr ""
msgid "Edit environment"
msgstr "환경 편집"
-msgid "Edit epic description"
-msgstr ""
-
msgid "Edit file"
msgstr "íŒŒì¼ ìˆ˜ì •"
@@ -7989,9 +8207,6 @@ msgstr ""
msgid "Epics"
msgstr "ì—픽"
-msgid "Epics (Ultimate / Gold license only)"
-msgstr ""
-
msgid "Epics Roadmap"
msgstr "ì—픽 로드맵"
@@ -8001,6 +8216,9 @@ msgstr ""
msgid "Epics let you manage your portfolio of projects more efficiently and with less effort"
msgstr ""
+msgid "Epics, Issues, and Merge Requests"
+msgstr ""
+
msgid "Epics|Add an epic"
msgstr ""
@@ -8052,6 +8270,9 @@ msgstr ""
msgid "Epics|Something went wrong while fetching group epics."
msgstr ""
+msgid "Epics|Something went wrong while moving item."
+msgstr ""
+
msgid "Epics|Something went wrong while ordering item."
msgstr ""
@@ -8106,6 +8327,9 @@ msgstr "ë¼ë²¨ì„ 가져 오는 중 오류가 ë°œìƒí–ˆìŠµë‹ˆë‹¤."
msgid "Error fetching network graph."
msgstr "ë„¤íŠ¸ì›Œí¬ ê·¸ëž˜í”„ ë°˜ì˜ ì¤‘ 오류"
+msgid "Error fetching payload data."
+msgstr ""
+
msgid "Error fetching projects"
msgstr ""
@@ -8115,9 +8339,6 @@ msgstr "refs ë°˜ì˜ ì¤‘ 오류"
msgid "Error fetching the dependency list. Please check your network connection and try again."
msgstr ""
-msgid "Error fetching usage ping data."
-msgstr "사용 í•‘ ë°ì´í„° ë°˜ì˜ ì¤‘ 오류"
-
msgid "Error loading branch data. Please try again."
msgstr "브랜치 ë°ì´í„°ë¥¼ 로드하는 중 오류가 ë°œìƒí–ˆìŠµë‹ˆë‹¤. 다시 ì‹œë„하십시오."
@@ -8259,6 +8480,9 @@ msgstr ""
msgid "Errors"
msgstr "오류"
+msgid "Errors:"
+msgstr ""
+
msgid "Estimated"
msgstr "예ìƒ"
@@ -8463,12 +8687,18 @@ msgstr "공개 그룹 íƒìƒ‰"
msgid "Export as CSV"
msgstr "CSV로 내보내기"
+msgid "Export group"
+msgstr ""
+
msgid "Export issues"
msgstr "ì´ìŠˆ 내보내기"
msgid "Export project"
msgstr ""
+msgid "Export this group with all related data to a new GitLab instance. Once complete, you can import the data file from the \"New Group\" page."
+msgstr ""
+
msgid "Export this project with all its related data in order to move your project to a new GitLab instance. Once the export is finished, you can import the file from the \"New Project\" page."
msgstr ""
@@ -8538,9 +8768,6 @@ msgstr "실패"
msgid "Failed Jobs"
msgstr "작업 실패"
-msgid "Failed create wiki"
-msgstr "wiki를 만들지 못했습니다."
-
msgid "Failed to add a Zoom meeting"
msgstr ""
@@ -8580,6 +8807,9 @@ msgstr ""
msgid "Failed to create resources"
msgstr ""
+msgid "Failed to create wiki"
+msgstr ""
+
msgid "Failed to delete board. Please try again."
msgstr ""
@@ -8589,7 +8819,7 @@ msgstr "ì— ë°°í¬í•˜ì§€ 못했습니다."
msgid "Failed to enqueue the rebase operation, possibly due to a long-lived transaction. Try again later."
msgstr ""
-msgid "Failed to find import label for jira import."
+msgid "Failed to find import label for Jira import."
msgstr ""
msgid "Failed to get ref."
@@ -8613,6 +8843,12 @@ msgstr ""
msgid "Failed to load groups & users."
msgstr ""
+msgid "Failed to load labels. Please try again."
+msgstr ""
+
+msgid "Failed to load milestones. Please try again."
+msgstr ""
+
msgid "Failed to load related branches"
msgstr ""
@@ -8988,7 +9224,10 @@ msgstr "커밋 메시지로 필터"
msgid "Filter by milestone name"
msgstr ""
-msgid "Filter by name..."
+msgid "Filter by name"
+msgstr ""
+
+msgid "Filter by status"
msgstr ""
msgid "Filter by two-factor authentication"
@@ -9000,12 +9239,18 @@ msgstr ""
msgid "Filter projects"
msgstr ""
+msgid "Filter results"
+msgstr ""
+
msgid "Filter results by group"
msgstr ""
msgid "Filter results by project"
msgstr ""
+msgid "Filter results..."
+msgstr ""
+
msgid "Filter your projects by name"
msgstr ""
@@ -9294,10 +9539,10 @@ msgstr ""
msgid "GeoNodes|Checksummed"
msgstr "ì²´í¬ì„¬ë¨"
-msgid "GeoNodes|Container repositories"
+msgid "GeoNodes|Consult Geo troubleshooting information"
msgstr ""
-msgid "GeoNodes|Data is out of date from %{timeago}"
+msgid "GeoNodes|Container repositories"
msgstr ""
msgid "GeoNodes|Data replication lag"
@@ -9339,6 +9584,9 @@ msgstr "커서로 ì²˜ë¦¬ëœ ë§ˆì§€ë§‰ ì´ë²¤íŠ¸ ID"
msgid "GeoNodes|Last event ID seen from primary"
msgstr ""
+msgid "GeoNodes|Learn more about Geo node statuses"
+msgstr ""
+
msgid "GeoNodes|Loading nodes"
msgstr "노드 불러오는 중"
@@ -9354,6 +9602,9 @@ msgstr ""
msgid "GeoNodes|Node was successfully removed."
msgstr "노드가 성공ì ìœ¼ë¡œ ì‚­ì œë˜ì—ˆìŠµë‹ˆë‹¤."
+msgid "GeoNodes|Node's status was updated %{timeAgo}."
+msgstr ""
+
msgid "GeoNodes|Not checksummed"
msgstr "ì²´í¬ì„¬ ë˜ì§€ ì•ŠìŒ"
@@ -9384,7 +9635,10 @@ msgstr ""
msgid "GeoNodes|Repository verification progress"
msgstr ""
-msgid "GeoNodes|Selective"
+msgid "GeoNodes|Selective (%{syncLabel})"
+msgstr ""
+
+msgid "GeoNodes|Selective synchronization"
msgstr ""
msgid "GeoNodes|Something went wrong while changing node status"
@@ -9411,6 +9665,9 @@ msgstr ""
msgid "GeoNodes|Unverified"
msgstr "미확ì¸"
+msgid "GeoNodes|Updated %{timeAgo}"
+msgstr ""
+
msgid "GeoNodes|Used slots"
msgstr ""
@@ -9450,16 +9707,16 @@ msgstr ""
msgid "Geo|All"
msgstr "Geo| ì „ì²´"
+msgid "Geo|All %{replicable_type}"
+msgstr ""
+
msgid "Geo|All projects"
msgstr "모든 프로ì íŠ¸"
-msgid "Geo|All projects are being scheduled for re-sync"
-msgstr ""
-
-msgid "Geo|All projects are being scheduled for re-verify"
+msgid "Geo|All projects are being scheduled for resync"
msgstr ""
-msgid "Geo|Batch operations"
+msgid "Geo|All projects are being scheduled for reverify"
msgstr ""
msgid "Geo|Could not remove tracking entry for an existing project."
@@ -9471,9 +9728,15 @@ msgstr ""
msgid "Geo|Failed"
msgstr "실패"
+msgid "Geo|Filter by status"
+msgstr ""
+
msgid "Geo|Geo Status"
msgstr ""
+msgid "Geo|In progress"
+msgstr ""
+
msgid "Geo|In sync"
msgstr ""
@@ -9498,9 +9761,6 @@ msgstr ""
msgid "Geo|Not synced yet"
msgstr ""
-msgid "Geo|Pending"
-msgstr ""
-
msgid "Geo|Pending synchronization"
msgstr ""
@@ -9537,7 +9797,7 @@ msgstr ""
msgid "Geo|Resync"
msgstr ""
-msgid "Geo|Resync all projects"
+msgid "Geo|Resync all"
msgstr ""
msgid "Geo|Retry count"
@@ -9546,7 +9806,7 @@ msgstr "ìž¬ì‹œë„ íšŸìˆ˜"
msgid "Geo|Reverify"
msgstr ""
-msgid "Geo|Reverify all projects"
+msgid "Geo|Reverify all"
msgstr ""
msgid "Geo|Status"
@@ -9888,9 +10148,6 @@ msgstr ""
msgid "Go to file"
msgstr ""
-msgid "Go to file (MRs only)"
-msgstr ""
-
msgid "Go to file permalink (while viewing a file)"
msgstr ""
@@ -10335,6 +10592,9 @@ msgstr ""
msgid "GroupSettings|Disable group mentions"
msgstr ""
+msgid "GroupSettings|Export group"
+msgstr ""
+
msgid "GroupSettings|If the parent group's visibility is lower than the group current visibility, visibility levels for subgroups and projects will be changed to match the new parent group's visibility."
msgstr ""
@@ -10413,6 +10673,9 @@ msgstr "그룹들"
msgid "Groups (%{count})"
msgstr ""
+msgid "Groups (%{groups})"
+msgstr ""
+
msgid "Groups can also be nested by creating %{subgroup_docs_link_start}subgroups%{subgroup_docs_link_end}."
msgstr ""
@@ -10676,15 +10939,12 @@ msgstr "ID"
msgid "ID:"
msgstr ""
-msgid "IDE|Allow live previews of JavaScript projects in the Web IDE using CodeSandbox client side evaluation."
-msgstr "CodeSandbox client side evaluation를 사용하여 Web IDEì—ì„œ JavaScript 프로ì íŠ¸ì˜ 실시간 미리보기를 허용합니다."
+msgid "IDE|Allow live previews of JavaScript projects in the Web IDE using CodeSandbox Live Preview."
+msgstr ""
msgid "IDE|Back"
msgstr "뒤로"
-msgid "IDE|Client side evaluation"
-msgstr ""
-
msgid "IDE|Commit"
msgstr "커밋"
@@ -10748,6 +11008,9 @@ msgstr ""
msgid "If any job surpasses this timeout threshold, it will be marked as failed. Human readable time input language is accepted like \"1 hour\". Values without specification represent seconds."
msgstr ""
+msgid "If blank, set allowable lifetime to %{instance_level_policy_in_words}, as defined by the instance admin. Once set, existing tokens for users in this group may be revoked."
+msgstr ""
+
msgid "If checked, group owners can manage LDAP group links and LDAP member overrides"
msgstr ""
@@ -10787,6 +11050,9 @@ msgstr ""
msgid "If you lose your recovery codes you can generate new ones, invalidating all previous codes."
msgstr ""
+msgid "If you reach 100%% storage capacity, you will not be able to: %{base_message}"
+msgstr ""
+
msgid "If your HTTP repository is not publicly accessible, add your credentials."
msgstr ""
@@ -10898,6 +11164,9 @@ msgstr "GitHub으로 부터 저장소 가져오기"
msgid "Import repository"
msgstr "저장소 가져 오기"
+msgid "Import started by: %{importInitiator}"
+msgstr ""
+
msgid "Import tasks"
msgstr ""
@@ -10964,6 +11233,9 @@ msgstr ""
msgid "In order to tailor your experience with GitLab we<br>would like to know a bit more about you."
msgstr ""
+msgid "In progress"
+msgstr ""
+
msgid "In the next step, you'll be able to select the projects you want to import."
msgstr ""
@@ -11310,15 +11582,15 @@ msgstr ""
msgid "Issues"
msgstr "ì´ìŠˆ"
-msgid "Issues / Merge Requests"
-msgstr ""
-
msgid "Issues Analytics"
msgstr ""
msgid "Issues Rate Limits"
msgstr ""
+msgid "Issues and Merge Requests"
+msgstr ""
+
msgid "Issues can be bugs, tasks or ideas to be discussed. Also, issues are searchable and filterable."
msgstr "ì´ìŠˆëŠ” 버그, ìž‘ì—… í˜¹ì€ ë…¼ì˜í•  ì•„ì´ë””ì–´ì¼ ìˆ˜ 있습니다. 그리고, ì´ìŠˆëŠ” 검색 ë° í•„í„°ë§ ê°€ëŠ¥í•©ë‹ˆë‹¤."
@@ -11340,10 +11612,10 @@ msgstr ""
msgid "IssuesAnalytics|Avg/Month:"
msgstr ""
-msgid "IssuesAnalytics|Issues created"
+msgid "IssuesAnalytics|Issues opened"
msgstr ""
-msgid "IssuesAnalytics|Issues created per month"
+msgid "IssuesAnalytics|Issues opened per month"
msgstr ""
msgid "IssuesAnalytics|Last 12 months"
@@ -11397,6 +11669,9 @@ msgstr ""
msgid "Jira integration not configured."
msgstr ""
+msgid "Jira project: %{importProject}"
+msgstr ""
+
msgid "JiraService|Events for %{noteable_model_name} are disabled."
msgstr ""
@@ -11785,6 +12060,9 @@ msgstr ""
msgid "Lead"
msgstr ""
+msgid "Lead Time"
+msgstr ""
+
msgid "Learn GitLab"
msgstr ""
@@ -11797,6 +12075,9 @@ msgstr ""
msgid "Learn how to %{no_packages_link_start}publish and share your packages%{no_packages_link_end} with GitLab."
msgstr ""
+msgid "Learn how to enable synchronization"
+msgstr ""
+
msgid "Learn more"
msgstr "ë” ì•Œì•„ë³´ê¸°"
@@ -12123,9 +12404,6 @@ msgstr "Bitbucket Server 저장소 목ë¡"
msgid "Live preview"
msgstr "실시간 미리보기"
-msgid "Load more vulnerabilities"
-msgstr ""
-
msgid "Loading"
msgstr ""
@@ -12276,9 +12554,6 @@ msgstr ""
msgid "Manage"
msgstr ""
-msgid "Manage Git repositories with fine-grained access controls that keep your code secure. Perform code reviews and enhance collaboration with merge requests. Each project can also have an issue tracker and a wiki."
-msgstr ""
-
msgid "Manage Web IDE features"
msgstr "웹 IDE 기능 관리"
@@ -12309,6 +12584,9 @@ msgstr ""
msgid "Manage your license"
msgstr ""
+msgid "Managed Account"
+msgstr ""
+
msgid "Manifest"
msgstr "Manifest"
@@ -12573,6 +12851,9 @@ msgstr "머지 리퀘스트(MR) ìƒì„±ë¨"
msgid "Merge Requests in Review"
msgstr ""
+msgid "Merge automatically (%{strategy})"
+msgstr ""
+
msgid "Merge commit message"
msgstr ""
@@ -12726,6 +13007,12 @@ msgstr "머지ë¨"
msgid "Merged branches are being deleted. This can take some time depending on the number of branches. Please refresh the page to see changes."
msgstr ""
+msgid "Merged this merge request."
+msgstr ""
+
+msgid "Merges this merge request immediately."
+msgstr ""
+
msgid "Merges this merge request when the pipeline succeeds."
msgstr ""
@@ -12832,12 +13119,15 @@ msgid "Metrics|Edit metric"
msgid_plural "Metrics|Edit metrics"
msgstr[0] ""
-msgid "Metrics|Environment"
-msgstr "환경"
+msgid "Metrics|Expand panel"
+msgstr ""
msgid "Metrics|For grouping similar metrics"
msgstr ""
+msgid "Metrics|Go back (Esc)"
+msgstr ""
+
msgid "Metrics|Invalid time range, please verify."
msgstr ""
@@ -12868,9 +13158,6 @@ msgstr ""
msgid "Metrics|Refresh dashboard"
msgstr ""
-msgid "Metrics|Show last"
-msgstr ""
-
msgid "Metrics|There was an error creating the dashboard."
msgstr ""
@@ -12949,6 +13236,9 @@ msgstr ""
msgid "Microsoft Azure"
msgstr ""
+msgid "Middleman project with Static Site Editor support"
+msgstr ""
+
msgid "Migrated %{success_count}/%{total_count} files."
msgstr ""
@@ -13392,10 +13682,10 @@ msgstr ""
msgid "Next"
msgstr "다ìŒ"
-msgid "Next file in diff (MRs only)"
+msgid "Next file in diff"
msgstr ""
-msgid "Next unresolved discussion (MRs only)"
+msgid "Next unresolved discussion"
msgstr ""
msgid "Nickname"
@@ -13455,6 +13745,9 @@ msgstr "변경 사항 ì—†ìŒ"
msgid "No changes between %{ref_start}%{source_branch}%{ref_end} and %{ref_start}%{target_branch}%{ref_end}"
msgstr ""
+msgid "No child epics match applied filters"
+msgstr ""
+
msgid "No connection could be made to a Gitaly Server, please check your logs!"
msgstr "Gitaly Serverì— ì—°ê²°í•  수 없습니다. 로그를 확ì¸í•˜ì‹­ì‹œì˜¤!"
@@ -13593,15 +13886,6 @@ msgstr ""
msgid "No thanks, don't show this again"
msgstr ""
-msgid "No vulnerabilities found for this group"
-msgstr ""
-
-msgid "No vulnerabilities found for this pipeline"
-msgstr ""
-
-msgid "No vulnerabilities found for this project"
-msgstr ""
-
msgid "No vulnerabilities present"
msgstr ""
@@ -13725,6 +14009,9 @@ msgstr ""
msgid "Nothing to preview."
msgstr ""
+msgid "Nothing to synchronize"
+msgstr ""
+
msgid "Notification events"
msgstr "알림 ì´ë²¤íŠ¸"
@@ -13896,6 +14183,9 @@ msgstr ""
msgid "Once removed, the fork relationship cannot be restored and you will no longer be able to send merge requests to the source."
msgstr ""
+msgid "Once the exported file is ready you can download it from this page."
+msgstr ""
+
msgid "Once the exported file is ready, you will receive a notification email with a download link, or you can download it from this page."
msgstr ""
@@ -13996,9 +14286,6 @@ msgstr ""
msgid "Open sidebar"
msgstr "사ì´ë“œë°” 열기"
-msgid "Open source software to collaborate on code"
-msgstr ""
-
msgid "Open: %{openIssuesCount}"
msgstr ""
@@ -14101,6 +14388,9 @@ msgstr ""
msgid "OutdatedBrowser|You can provide feedback %{feedback_link_start}on this issue%{feedback_link_end} or via your usual support channels."
msgstr ""
+msgid "Overridden"
+msgstr ""
+
msgid "Overview"
msgstr "개요"
@@ -14152,6 +14442,9 @@ msgstr ""
msgid "PackageRegistry|Conan Command"
msgstr ""
+msgid "PackageRegistry|Copy .pypirc content"
+msgstr ""
+
msgid "PackageRegistry|Copy Conan Command"
msgstr ""
@@ -14173,6 +14466,9 @@ msgstr ""
msgid "PackageRegistry|Copy NuGet Setup Command"
msgstr ""
+msgid "PackageRegistry|Copy Pip command"
+msgstr ""
+
msgid "PackageRegistry|Copy and paste this inside your %{codeStart}pom.xml%{codeEnd} %{codeStart}dependencies%{codeEnd} block."
msgstr ""
@@ -14206,6 +14502,12 @@ msgstr ""
msgid "PackageRegistry|For more information on the NuGet registry, %{linkStart}see the documentation%{linkEnd}."
msgstr ""
+msgid "PackageRegistry|For more information on the PyPi registry, %{linkStart}see the documentation%{linkEnd}."
+msgstr ""
+
+msgid "PackageRegistry|If you haven't already done so, you will need to add the below to your %{codeStart}.pypirc%{codeEnd} file."
+msgstr ""
+
msgid "PackageRegistry|If you haven't already done so, you will need to add the below to your %{codeStart}pom.xml%{codeEnd} file."
msgstr ""
@@ -14215,6 +14517,9 @@ msgstr ""
msgid "PackageRegistry|Learn how to %{noPackagesLinkStart}publish and share your packages%{noPackagesLinkEnd} with GitLab."
msgstr ""
+msgid "PackageRegistry|Manually Published"
+msgstr ""
+
msgid "PackageRegistry|Maven"
msgstr ""
@@ -14233,12 +14538,18 @@ msgstr ""
msgid "PackageRegistry|NuGet Command"
msgstr ""
+msgid "PackageRegistry|Pip Command"
+msgstr ""
+
msgid "PackageRegistry|Pipeline %{linkStart}%{linkEnd} triggered %{timestamp} by %{author}"
msgstr ""
msgid "PackageRegistry|Published to the repository at %{timestamp}"
msgstr ""
+msgid "PackageRegistry|PyPi"
+msgstr ""
+
msgid "PackageRegistry|Registry Setup"
msgstr ""
@@ -14275,6 +14586,9 @@ msgstr ""
msgid "PackageRegistry|npm"
msgstr ""
+msgid "PackageRegistry|published by %{author}"
+msgstr ""
+
msgid "PackageRegistry|yarn"
msgstr ""
@@ -14290,6 +14604,9 @@ msgstr ""
msgid "PackageType|NuGet"
msgstr ""
+msgid "PackageType|PyPi"
+msgstr ""
+
msgid "Packages"
msgstr "패키지"
@@ -14383,6 +14700,9 @@ msgstr "비밀번호가 성공ì ìœ¼ë¡œ ì—…ë°ì´íŠ¸ë˜ì—ˆìŠµë‹ˆë‹¤. 새 비밀ë
msgid "Past due"
msgstr ""
+msgid "Paste a machine public key here. Read more about how to generate it"
+msgstr ""
+
msgid "Paste a machine public key here. Read more about how to generate it %{link_start}here%{link_end}"
msgstr ""
@@ -14398,9 +14718,6 @@ msgstr ""
msgid "Path"
msgstr ""
-msgid "Path, transfer, remove"
-msgstr ""
-
msgid "Path:"
msgstr "경로:"
@@ -14431,8 +14748,8 @@ msgstr ""
msgid "Percentage"
msgstr ""
-msgid "Perform advanced options such as changing path, transferring, or removing the group."
-msgstr "경로 변경, 그룹 전송 ë˜ëŠ” 제거와 ê°™ì€ ê³ ê¸‰ ì˜µì…˜ì„ ìˆ˜í–‰í•©ë‹ˆë‹¤."
+msgid "Perform advanced options such as changing path, transferring, exporting, or removing the group."
+msgstr ""
msgid "Perform common operations on GitLab project"
msgstr ""
@@ -14512,9 +14829,6 @@ msgstr "파ì´í”„ë¼ì¸ 스케쥴"
msgid "Pipeline minutes quota"
msgstr ""
-msgid "Pipeline quota"
-msgstr "파ì´í”„ë¼ì¸ 할당량"
-
msgid "Pipeline subscriptions"
msgstr ""
@@ -14524,6 +14838,9 @@ msgstr "파ì´í”„ë¼ì¸ 트리거"
msgid "Pipeline: %{status}"
msgstr ""
+msgid "PipelineCharts|CI / CD Analytics"
+msgstr ""
+
msgid "PipelineCharts|Failed:"
msgstr "실패 :"
@@ -14602,15 +14919,6 @@ msgstr ""
msgid "Pipelines settings for '%{project_name}' were successfully updated."
msgstr ""
-msgid "Pipelines| to purchase more minutes."
-msgstr ""
-
-msgid "Pipelines|%{namespace_name} has exceeded its pipeline minutes quota."
-msgstr ""
-
-msgid "Pipelines|%{namespace_name} has less than %{notification_level}%% of CI minutes available."
-msgstr ""
-
msgid "Pipelines|API"
msgstr ""
@@ -14632,12 +14940,15 @@ msgstr ""
msgid "Pipelines|Get started with Pipelines"
msgstr "파ì´í”„ ë¼ì¸ìœ¼ë¡œ 시작하기"
-msgid "Pipelines|Loading Pipelines"
-msgstr "파ì´í”„ë¼ì¸ 로딩중"
+msgid "Pipelines|Group %{namespace_name} has %{percentage}%% or less Shared Runner Pipeline minutes remaining. Once it runs out, no new jobs or pipelines in its projects will run."
+msgstr ""
-msgid "Pipelines|Pipelines will not run anymore on shared Runners."
+msgid "Pipelines|Group %{namespace_name} has exceeded its pipeline minutes quota. Unless you buy additional pipeline minutes, no new jobs or pipelines in its projects will run."
msgstr ""
+msgid "Pipelines|Loading Pipelines"
+msgstr "파ì´í”„ë¼ì¸ 로딩중"
+
msgid "Pipelines|Project cache successfully reset."
msgstr "프로ì íŠ¸ ìºì‹œê°€ 성공ì ìœ¼ë¡œ 재설정ë˜ì—ˆìŠµë‹ˆë‹¤."
@@ -14866,6 +15177,9 @@ msgstr ""
msgid "Please select"
msgstr ""
+msgid "Please select a Jira project"
+msgstr ""
+
msgid "Please select a country"
msgstr ""
@@ -14911,6 +15225,9 @@ msgstr ""
msgid "Please wait while we import the repository for you. Refresh at will."
msgstr "저장소를 가져오는 ë™ì•ˆ 기다려주십시오. 완료ë˜ë©´ ìžë™ìœ¼ë¡œ 페ì´ì§€ë¥¼ 새로고침 합니다."
+msgid "Plugins directory is deprecated and will be removed in 14.0. Please move this file into /file_hooks directory."
+msgstr ""
+
msgid "Pod does not exist"
msgstr ""
@@ -15028,7 +15345,7 @@ msgstr ""
msgid "Prevent users from changing their profile name"
msgstr ""
-msgid "Prevent users from modifing merge request approvers list"
+msgid "Prevent users from modifying merge request approvers list"
msgstr ""
msgid "Prevent users from performing write operations on GitLab while performing maintenance."
@@ -15049,10 +15366,10 @@ msgstr "페ì´ë¡œë“œ 미리보기"
msgid "Previous Artifacts"
msgstr ""
-msgid "Previous file in diff (MRs only)"
+msgid "Previous file in diff"
msgstr ""
-msgid "Previous unresolved discussion (MRs only)"
+msgid "Previous unresolved discussion"
msgstr ""
msgid "Primary"
@@ -15085,6 +15402,9 @@ msgstr ""
msgid "Private profile"
msgstr ""
+msgid "Private projects Minutes cost factor"
+msgstr ""
+
msgid "Private projects can be created in your personal namespace with:"
msgstr "비공개 프로ì íŠ¸ëŠ” ê°œì¸ ë„¤ìž„ìŠ¤íŽ˜ì´ìŠ¤ì—ì„œ 만들어집니다:"
@@ -15913,6 +16233,9 @@ msgstr ""
msgid "ProjectSettings|With GitLab Pages you can host your static websites on GitLab"
msgstr ""
+msgid "ProjectSettings|With Metrics Dashboard you can visualize this project performance metrics"
+msgstr ""
+
msgid "ProjectTemplates|.NET Core"
msgstr ""
@@ -15976,6 +16299,9 @@ msgstr ""
msgid "ProjectTemplates|Spring"
msgstr ""
+msgid "ProjectTemplates|Static Site Editor/Middleman"
+msgstr ""
+
msgid "ProjectTemplates|iOS (Swift)"
msgstr ""
@@ -16195,15 +16521,15 @@ msgstr "Prometheus API 기반 URL, 예를 들면 http://prometheus.example.com/"
msgid "PrometheusService|Prometheus is being automatically managed on your clusters"
msgstr "ë‹¹ì‹ ì˜ í´ëŸ¬ìŠ¤í„°ì—ì„œ Prometheusê°€ ìžë™ìœ¼ë¡œ 관리ë˜ê³  있습니다."
+msgid "PrometheusService|Select the Active checkbox to override the Auto Configuration with custom settings. If unchecked, Auto Configuration settings are used."
+msgstr ""
+
msgid "PrometheusService|These metrics will only be monitored after your first deployment to an environment"
msgstr ""
msgid "PrometheusService|Time-series monitoring service"
msgstr "시간 계열 ëª¨ë‹ˆí„°ë§ ì„œë¹„ìŠ¤"
-msgid "PrometheusService|To enable manual configuration, uninstall Prometheus from your clusters"
-msgstr "ìˆ˜ë™ ì„¤ì •ì„ í™œì„±í™” 하려면, í´ëŸ¬ìŠ¤í„°ì—ì„œ Prometheus를 제거하십시오."
-
msgid "PrometheusService|To enable the installation of Prometheus on your clusters, deactivate the manual configuration below"
msgstr "í´ëŸ¬ìŠ¤í„°ì— Prometheus를 설치하려면, 아래 ìˆ˜ë™ ì„¤ì •ì„ ë¹„í™œì„±í™” 하십시오."
@@ -16387,6 +16713,9 @@ msgstr ""
msgid "Protip:"
msgstr "Protip:"
+msgid "Protocol"
+msgstr ""
+
msgid "Provider"
msgstr "공급ìž"
@@ -16411,6 +16740,9 @@ msgstr ""
msgid "Public pipelines"
msgstr "공용 파ì´í”„ ë¼ì¸"
+msgid "Public projects Minutes cost factor"
+msgstr ""
+
msgid "Pull"
msgstr "Pull"
@@ -16610,6 +16942,9 @@ msgid "Refreshing in a second to show the updated status..."
msgid_plural "Refreshing in %d seconds to show the updated status..."
msgstr[0] ""
+msgid "Regenerate export"
+msgstr ""
+
msgid "Regenerate instance ID"
msgstr ""
@@ -16986,9 +17321,28 @@ msgstr "ë³´ê³ "
msgid "Reports|%{combinedString} and %{resolvedString}"
msgstr ""
+msgid "Reports|Accessibility scanning detected %d issue for the source branch only"
+msgid_plural "Reports|Accessibility scanning detected %d issues for the source branch only"
+msgstr[0] ""
+
+msgid "Reports|Accessibility scanning detected no issues for the source branch only"
+msgstr ""
+
+msgid "Reports|Accessibility scanning failed loading results"
+msgstr ""
+
+msgid "Reports|Accessibility scanning results are being parsed"
+msgstr ""
+
msgid "Reports|Actions"
msgstr "ìž‘ì—…"
+msgid "Reports|An error occured while loading report"
+msgstr ""
+
+msgid "Reports|An error occurred while loading %{name} results"
+msgstr ""
+
msgid "Reports|Class"
msgstr "í´ëž˜ìŠ¤"
@@ -17094,7 +17448,7 @@ msgstr ""
msgid "Requested %{time_ago}"
msgstr ""
-msgid "Requested design version does not exist"
+msgid "Requested design version does not exist."
msgstr ""
msgid "Requested states are invalid"
@@ -17276,6 +17630,9 @@ msgstr ""
msgid "Resync"
msgstr ""
+msgid "Resync all"
+msgstr ""
+
msgid "Resync all %{replicableType}"
msgstr ""
@@ -17343,6 +17700,9 @@ msgstr ""
msgid "Revoked personal access token %{personal_access_token_name}!"
msgstr ""
+msgid "Revoked project access token %{project_access_token_name}!"
+msgstr ""
+
msgid "RightSidebar|adding a"
msgstr ""
@@ -17391,6 +17751,9 @@ msgstr ""
msgid "Runner token"
msgstr "Runner 토í°"
+msgid "Runner tokens"
+msgstr ""
+
msgid "Runner was not updated."
msgstr ""
@@ -17445,6 +17808,9 @@ msgstr "SAML SSO"
msgid "SAML SSO for %{group_name}"
msgstr "%{group_name}ì˜ SAML SSO"
+msgid "SAML discovery tokens"
+msgstr ""
+
msgid "SAML for %{group_name}"
msgstr ""
@@ -17529,12 +17895,18 @@ msgstr "새로운 파ì´í”„ë¼ì¸ 스케줄 잡기"
msgid "Scheduled"
msgstr "예정ë¨"
+msgid "Scheduled to merge this merge request (%{strategy})."
+msgstr ""
+
msgid "Scheduled to merge this merge request when the pipeline succeeds."
msgstr ""
msgid "Schedules"
msgstr "ì¼ì •"
+msgid "Schedules to merge this merge request (%{strategy})."
+msgstr ""
+
msgid "Scheduling"
msgstr ""
@@ -17592,6 +17964,9 @@ msgstr "브랜치 검색"
msgid "Search branches and tags"
msgstr "브랜치 ë° íƒœê·¸ 검색"
+msgid "Search by author"
+msgstr ""
+
msgid "Search files"
msgstr "íŒŒì¼ ê²€ìƒ‰"
@@ -17729,10 +18104,6 @@ msgid "SearchResults|snippet"
msgid_plural "SearchResults|snippets"
msgstr[0] ""
-msgid "SearchResults|snippet result"
-msgid_plural "SearchResults|snippet results"
-msgstr[0] ""
-
msgid "SearchResults|user"
msgid_plural "SearchResults|users"
msgstr[0] ""
@@ -17741,6 +18112,9 @@ msgid "SearchResults|wiki result"
msgid_plural "SearchResults|wiki results"
msgstr[0] ""
+msgid "Searching by both author and message is currently not supported."
+msgstr ""
+
msgid "Seat Link"
msgstr ""
@@ -17768,184 +18142,232 @@ msgstr ""
msgid "Security Dashboard"
msgstr "보안 대시보드"
-msgid "Security Dashboard|Error fetching the vulnerability counts. Please check your network connection and try again."
+msgid "Security configuration help link"
msgstr ""
-msgid "Security Dashboard|Error fetching the vulnerability list. Please check your network connection and try again."
+msgid "Security dashboard"
msgstr ""
-msgid "Security Dashboard|Issue Created"
+msgid "Security report is out of date. Please update your branch with the latest changes from the target branch (%{targetBranchName})"
msgstr ""
-msgid "Security Reports|Comment added to '%{vulnerabilityName}'"
+msgid "Security report is out of date. Run %{newPipelineLinkStart}a new pipeline%{newPipelineLinkEnd} for the target branch (%{targetBranchName})"
msgstr ""
-msgid "Security Reports|Comment deleted on '%{vulnerabilityName}'"
+msgid "SecurityConfiguration|Configured"
msgstr ""
-msgid "Security Reports|Comment edited on '%{vulnerabilityName}'"
+msgid "SecurityConfiguration|Feature"
msgstr ""
-msgid "Security Reports|Create issue"
-msgstr "ì´ìŠˆ ìƒì„±"
+msgid "SecurityConfiguration|Feature documentation for %{featureName}"
+msgstr ""
-msgid "Security Reports|Dismiss vulnerability"
-msgstr "ì·¨ì•½ì  ë¬´ì‹œ"
+msgid "SecurityConfiguration|Not yet configured"
+msgstr ""
-msgid "Security Reports|Dismissed '%{vulnerabilityName}'"
+msgid "SecurityConfiguration|Secure features"
msgstr ""
-msgid "Security Reports|Dismissed '%{vulnerabilityName}'. Turn off the hide dismissed toggle to view."
+msgid "SecurityConfiguration|Status"
msgstr ""
-msgid "Security Reports|Either you don't have permission to view this dashboard or the dashboard has not been setup. Please check your permission settings with your administrator or check your dashboard configurations to proceed."
+msgid "SecurityReports|%{firstProject} and %{secondProject}"
msgstr ""
-msgid "Security Reports|Learn more about setting up your dashboard"
+msgid "SecurityReports|%{firstProject}, %{secondProject}, and %{rest}"
msgstr ""
-msgid "Security Reports|More info"
-msgstr "ë” ë§Žì€ ì •ë³´"
+msgid "SecurityReports|Add a project to your dashboard"
+msgstr ""
-msgid "Security Reports|Oops, something doesn't seem right."
+msgid "SecurityReports|Add or remove projects from your dashboard"
msgstr ""
-msgid "Security Reports|Security reports can only be accessed by authorized users."
+msgid "SecurityReports|Add projects"
msgstr ""
-msgid "Security Reports|There was an error adding the comment."
+msgid "SecurityReports|Comment added to '%{vulnerabilityName}'"
msgstr ""
-msgid "Security Reports|There was an error creating the issue."
-msgstr "ì´ìŠˆë¥¼ ìƒì„± í•˜ë˜ ì¤‘ 오류가 ë°œìƒí–ˆìŠµë‹ˆë‹¤."
+msgid "SecurityReports|Comment deleted on '%{vulnerabilityName}'"
+msgstr ""
-msgid "Security Reports|There was an error creating the merge request."
+msgid "SecurityReports|Comment edited on '%{vulnerabilityName}'"
msgstr ""
-msgid "Security Reports|There was an error deleting the comment."
+msgid "SecurityReports|Create issue"
msgstr ""
-msgid "Security Reports|There was an error dismissing the vulnerabilities."
+msgid "SecurityReports|Dismiss Selected"
msgstr ""
-msgid "Security Reports|There was an error dismissing the vulnerability."
+msgid "SecurityReports|Dismiss vulnerability"
msgstr ""
-msgid "Security Reports|There was an error reverting the dismissal."
+msgid "SecurityReports|Dismissed '%{vulnerabilityName}'"
msgstr ""
-msgid "Security Reports|There was an error reverting this dismissal."
+msgid "SecurityReports|Dismissed '%{vulnerabilityName}'. Turn off the hide dismissed toggle to view."
msgstr ""
-msgid "Security Reports|Undo dismiss"
+msgid "SecurityReports|Each vulnerability now has a unique page that can be directly linked to, shared, referenced, and tracked as the single source of truth. Vulnerability occurrences also persist across scanner runs, which improves tracking and visibility and reduces duplicates between scans."
msgstr ""
-msgid "Security Reports|You do not have sufficient permissions to access this report"
+msgid "SecurityReports|Edit dashboard"
msgstr ""
-msgid "Security Reports|You must sign in as an authorized user to see this report"
+msgid "SecurityReports|Either you don't have permission to view this dashboard or the dashboard has not been setup. Please check your permission settings with your administrator or check your dashboard configurations to proceed."
msgstr ""
-msgid "Security configuration help link"
+msgid "SecurityReports|Error fetching the vulnerability counts. Please check your network connection and try again."
msgstr ""
-msgid "Security dashboard"
+msgid "SecurityReports|Error fetching the vulnerability list. Please check your network connection and try again."
msgstr ""
-msgid "Security report is out of date. Please update your branch with the latest changes from the target branch (%{targetBranchName})"
+msgid "SecurityReports|False positive"
msgstr ""
-msgid "Security report is out of date. Run %{newPipelineLinkStart}a new pipeline%{newPipelineLinkEnd} for the target branch (%{targetBranchName})"
+msgid "SecurityReports|Hide dismissed"
msgstr ""
-msgid "SecurityConfiguration|Configured"
+msgid "SecurityReports|Introducing standalone vulnerabilities"
msgstr ""
-msgid "SecurityConfiguration|Feature"
+msgid "SecurityReports|Issue Created"
msgstr ""
-msgid "SecurityConfiguration|Feature documentation for %{featureName}"
+msgid "SecurityReports|Learn More"
msgstr ""
-msgid "SecurityConfiguration|Not yet configured"
+msgid "SecurityReports|Learn more about setting up your dashboard"
msgstr ""
-msgid "SecurityConfiguration|Secure features"
+msgid "SecurityReports|Load more vulnerabilities"
msgstr ""
-msgid "SecurityConfiguration|Status"
+msgid "SecurityReports|Monitor vulnerabilities in your code"
+msgstr ""
+
+msgid "SecurityReports|More info"
+msgstr ""
+
+msgid "SecurityReports|More information"
+msgstr ""
+
+msgid "SecurityReports|No vulnerabilities found for dashboard"
+msgstr ""
+
+msgid "SecurityReports|No vulnerabilities found for this group"
+msgstr ""
+
+msgid "SecurityReports|No vulnerabilities found for this pipeline"
+msgstr ""
+
+msgid "SecurityReports|No vulnerabilities found for this project"
+msgstr ""
+
+msgid "SecurityReports|Oops, something doesn't seem right."
+msgstr ""
+
+msgid "SecurityReports|Pipeline %{pipelineLink} triggered %{timeago} by %{user}"
+msgstr ""
+
+msgid "SecurityReports|Project"
+msgstr ""
+
+msgid "SecurityReports|Projects added"
+msgstr ""
+
+msgid "SecurityReports|Remove project from dashboard"
+msgstr ""
+
+msgid "SecurityReports|Report type"
msgstr ""
-msgid "SecurityDashboard|%{firstProject} and %{secondProject}"
+msgid "SecurityReports|Return to dashboard"
msgstr ""
-msgid "SecurityDashboard|%{firstProject}, %{secondProject}, and %{rest}"
+msgid "SecurityReports|Security Dashboard"
msgstr ""
-msgid "SecurityDashboard|Add a project to your dashboard"
+msgid "SecurityReports|Security reports can only be accessed by authorized users."
msgstr ""
-msgid "SecurityDashboard|Add or remove projects from your dashboard"
+msgid "SecurityReports|Select a project to add by using the project search field above."
msgstr ""
-msgid "SecurityDashboard|Add projects"
+msgid "SecurityReports|Select a reason"
msgstr ""
-msgid "SecurityDashboard|Edit dashboard"
+msgid "SecurityReports|Severity"
msgstr ""
-msgid "SecurityDashboard|Hide dismissed"
+msgid "SecurityReports|Status"
msgstr ""
-msgid "SecurityDashboard|Monitor vulnerabilities in your code"
-msgstr "코드 ì·¨ì•½ì  ëª¨ë‹ˆí„°ë§"
+msgid "SecurityReports|The security dashboard displays the latest security findings for projects you wish to monitor. Select \"Edit dashboard\" to add and remove projects."
+msgstr ""
+
+msgid "SecurityReports|The security dashboard displays the latest security report. Use it to find and fix vulnerabilities."
+msgstr ""
+
+msgid "SecurityReports|There was an error adding the comment."
+msgstr ""
+
+msgid "SecurityReports|There was an error creating the issue."
+msgstr ""
+
+msgid "SecurityReports|There was an error creating the merge request."
+msgstr ""
-msgid "SecurityDashboard|More information"
+msgid "SecurityReports|There was an error deleting the comment."
msgstr ""
-msgid "SecurityDashboard|No vulnerabilities found for dashboard"
+msgid "SecurityReports|There was an error dismissing the vulnerabilities."
msgstr ""
-msgid "SecurityDashboard|Pipeline %{pipelineLink} triggered %{timeago} by %{user}"
+msgid "SecurityReports|There was an error dismissing the vulnerability."
msgstr ""
-msgid "SecurityDashboard|Project"
+msgid "SecurityReports|There was an error reverting the dismissal."
msgstr ""
-msgid "SecurityDashboard|Projects added"
+msgid "SecurityReports|There was an error reverting this dismissal."
msgstr ""
-msgid "SecurityDashboard|Remove project from dashboard"
+msgid "SecurityReports|There was an error while generating the report."
msgstr ""
-msgid "SecurityDashboard|Report type"
+msgid "SecurityReports|Unable to add %{invalidProjects}"
msgstr ""
-msgid "SecurityDashboard|Return to dashboard"
+msgid "SecurityReports|Undo dismiss"
msgstr ""
-msgid "SecurityDashboard|Security Dashboard"
+msgid "SecurityReports|While it's rare to have no vulnerabilities for your group, it can happen. In any event, we ask that you double check your settings to make sure you've set up your dashboard correctly."
msgstr ""
-msgid "SecurityDashboard|Select a project to add by using the project search field above."
+msgid "SecurityReports|While it's rare to have no vulnerabilities for your pipeline, it can happen. In any event, we ask that you double check your settings to make sure all security scanning jobs have passed successfully."
msgstr ""
-msgid "SecurityDashboard|Severity"
+msgid "SecurityReports|While it's rare to have no vulnerabilities for your project, it can happen. In any event, we ask that you double check your settings to make sure you've set up your dashboard correctly."
msgstr ""
-msgid "SecurityDashboard|Status"
+msgid "SecurityReports|While it's rare to have no vulnerabilities, it can happen. In any event, we ask that you please double check your settings to make sure you've set up your dashboard correctly."
msgstr ""
-msgid "SecurityDashboard|The security dashboard displays the latest security findings for projects you wish to monitor. Select \"Edit dashboard\" to add and remove projects."
+msgid "SecurityReports|Won't fix / Accept risk"
msgstr ""
-msgid "SecurityDashboard|The security dashboard displays the latest security report. Use it to find and fix vulnerabilities."
+msgid "SecurityReports|You do not have sufficient permissions to access this report"
msgstr ""
-msgid "SecurityDashboard|There was an error while generating the report."
+msgid "SecurityReports|You must sign in as an authorized user to see this report"
msgstr ""
-msgid "SecurityDashboard|Unable to add %{invalidProjects}"
+msgid "SecurityReports|[No reason]"
msgstr ""
msgid "See GitLab's %{password_policy_guidelines}"
@@ -17957,6 +18379,9 @@ msgstr ""
msgid "See the affected projects in the GitLab admin panel"
msgstr ""
+msgid "See what's new at GitLab"
+msgstr ""
+
msgid "Select"
msgstr "ì„ íƒ"
@@ -18362,9 +18787,6 @@ msgstr "CI/CD 설정"
msgid "Set up Jira Integration"
msgstr ""
-msgid "Set up Jira Integration illustration"
-msgstr ""
-
msgid "Set up a %{type} Runner automatically"
msgstr ""
@@ -18449,6 +18871,9 @@ msgstr ""
msgid "Severity: %{severity}"
msgstr ""
+msgid "Shards (%{shards})"
+msgstr ""
+
msgid "Shards to synchronize"
msgstr ""
@@ -18762,6 +19187,9 @@ msgstr ""
msgid "Snowplow"
msgstr ""
+msgid "Some child epics may be hidden due to applied filters"
+msgstr ""
+
msgid "Some email servers do not support overriding the email sender name. Enable this option to include the name of the author of the issue, merge request or comment in the email body instead."
msgstr ""
@@ -18903,6 +19331,9 @@ msgstr ""
msgid "Something went wrong while updating your list settings"
msgstr ""
+msgid "Something went wrong with your automatic subscription renewal"
+msgstr ""
+
msgid "Something went wrong, unable to add %{project} to dashboard"
msgstr ""
@@ -19188,6 +19619,9 @@ msgstr ""
msgid "Stage removed"
msgstr ""
+msgid "Standard"
+msgstr ""
+
msgid "Star a label to make it a priority label. Order the prioritized labels to change their relative priority, by dragging."
msgstr ""
@@ -19311,6 +19745,9 @@ msgstr ""
msgid "Static Application Security Testing (SAST)"
msgstr ""
+msgid "StaticSiteEditor|An error occurred while submitting your changes."
+msgstr ""
+
msgid "StaticSiteEditor|Branch could not be created."
msgstr ""
@@ -19320,18 +19757,30 @@ msgstr ""
msgid "StaticSiteEditor|Could not create merge request."
msgstr ""
+msgid "StaticSiteEditor|Incompatible file content"
+msgstr ""
+
msgid "StaticSiteEditor|Return to site"
msgstr ""
+msgid "StaticSiteEditor|Static site editor"
+msgstr ""
+
msgid "StaticSiteEditor|Success!"
msgstr ""
msgid "StaticSiteEditor|Summary of changes"
msgstr ""
+msgid "StaticSiteEditor|The Static Site Editor is currently configured to only edit Markdown content on pages generated from Middleman. Visit the documentation to learn more about configuring your site to use the Static Site Editor."
+msgstr ""
+
msgid "StaticSiteEditor|Update %{sourcePath} file"
msgstr ""
+msgid "StaticSiteEditor|View documentation"
+msgstr ""
+
msgid "StaticSiteEditor|View merge request"
msgstr ""
@@ -19389,6 +19838,12 @@ msgstr ""
msgid "StatusPage|Status page"
msgstr ""
+msgid "StatusPage|Status page URL"
+msgstr ""
+
+msgid "StatusPage|Status page frontend documentation"
+msgstr ""
+
msgid "StatusPage|To publish incidents to an external status page, GitLab will store a JSON file in your Amazon S3 account in a location accessible to your external status page service. Make sure to also set up %{docsLink}"
msgstr ""
@@ -19737,6 +20192,12 @@ msgstr "ì •ë³´ ë™ê¸°í™”"
msgid "Synced"
msgstr ""
+msgid "Synchronization disabled"
+msgstr ""
+
+msgid "Synchronization of container repositories is disabled."
+msgstr ""
+
msgid "System"
msgstr ""
@@ -19890,6 +20351,9 @@ msgstr ""
msgid "Telephone number"
msgstr ""
+msgid "Telephone number (Optional)"
+msgstr ""
+
msgid "Template"
msgstr "템플릿"
@@ -20193,6 +20657,9 @@ msgstr ""
msgid "The license was successfully uploaded and is now active. You can see the details below."
msgstr ""
+msgid "The license was successfully uploaded and will be active from %{starts_at}. You can see the details below."
+msgstr ""
+
msgid "The maximum file size allowed is %{size}."
msgstr ""
@@ -20514,6 +20981,9 @@ msgstr ""
msgid "There was an error getting the epic participants."
msgstr ""
+msgid "There was an error importing the Jira project."
+msgstr ""
+
msgid "There was an error loading users activity calendar."
msgstr "ì‚¬ìš©ìž í™œë™ ìº˜ë¦°ë”를 로딩하는 ì¤‘ì— ì˜¤ë¥˜ê°€ ë°œìƒí•˜ì˜€ìŠµë‹ˆë‹¤."
@@ -20529,7 +20999,7 @@ msgstr ""
msgid "There was an error resetting user pipeline minutes."
msgstr ""
-msgid "There was an error saving this Geo Node"
+msgid "There was an error saving this Geo Node."
msgstr ""
msgid "There was an error saving your changes."
@@ -20592,6 +21062,9 @@ msgstr ""
msgid "They can be managed using the %{link}."
msgstr "%{link}를 사용하여 관리 할 수 ​​있습니다."
+msgid "Third Party Advisory Link"
+msgstr ""
+
msgid "Third party offers"
msgstr "타사 제공"
@@ -20628,6 +21101,9 @@ msgstr ""
msgid "This application will be able to:"
msgstr "ì´ ì‘ìš© í”„ë¡œê·¸ëž¨ì€ ë‹¤ìŒì„ 수행 í•  수 있습니다."
+msgid "This attachment has been truncated to avoid exceeding the maximum allowed attachment size of 15MB. %{written_count} of %{issues_count} issues have been included. Consider re-exporting with a narrower selection of issues."
+msgstr ""
+
msgid "This block is self-referential"
msgstr ""
@@ -20769,6 +21245,12 @@ msgstr ""
msgid "This issue is confidential"
msgstr "ì´ ì´ìŠˆëŠ” 비밀입니다."
+msgid "This issue is currently blocked by the following issues: %{issues}."
+msgstr ""
+
+msgid "This issue is in a child epic of the filtered epic"
+msgstr ""
+
msgid "This issue is locked."
msgstr "ì´ ì´ìŠˆëŠ” lock ë˜ì—ˆìŠµë‹ˆë‹¤."
@@ -20967,7 +21449,10 @@ msgstr ""
msgid "This user will be the author of all events in the activity feed that are the result of an update, like new branches being created or new commits being pushed to existing branches. Upon creation or when reassigning you can only assign yourself to be the mirror user."
msgstr ""
-msgid "This variable can not be masked"
+msgid "This variable can not be masked."
+msgstr ""
+
+msgid "This variable does not match the expected pattern."
msgstr ""
msgid "This will help us personalize your onboarding experience."
@@ -20997,10 +21482,10 @@ msgstr ""
msgid "ThreatMonitoring|Application firewall not detected"
msgstr ""
-msgid "ThreatMonitoring|Container Network Policy"
+msgid "ThreatMonitoring|Container Network Policies are not installed or have been disabled. To view this data, ensure your Network Policies are installed and enabled for your cluster."
msgstr ""
-msgid "ThreatMonitoring|Container NetworkPolicies are not installed or has been disabled. To view this data, ensure you NetworkPolicies are installed and enabled for your cluster."
+msgid "ThreatMonitoring|Container Network Policy"
msgstr ""
msgid "ThreatMonitoring|Container NetworkPolicies not detected"
@@ -21033,7 +21518,7 @@ msgstr ""
msgid "ThreatMonitoring|Something went wrong, unable to fetch statistics"
msgstr ""
-msgid "ThreatMonitoring|The firewall is not installed or has been disabled. To view this data, ensure you firewall is installed and enabled for your cluster."
+msgid "ThreatMonitoring|The firewall is not installed or has been disabled. To view this data, ensure the web application firewall is installed and enabled for your cluster."
msgstr ""
msgid "ThreatMonitoring|The graph below is an overview of traffic coming to your application as tracked by the Web Application Firewall (WAF). View the docs for instructions on how to access the WAF logs to see what type of malicious traffic is trying to access your app. The docs link is also accessible by clicking the \"?\" icon next to the title below."
@@ -21102,6 +21587,9 @@ msgstr ""
msgid "Time in seconds GitLab will wait for a response from the external service. When the service does not respond in time, access will be denied."
msgstr ""
+msgid "Time of import: %{importTime}"
+msgstr ""
+
msgid "Time remaining"
msgstr "ë‚¨ì€ ì‹œê°„"
@@ -21284,7 +21772,7 @@ msgstr "제목"
msgid "Title:"
msgstr "제목:"
-msgid "Titles and Filenames"
+msgid "Titles and Descriptions"
msgstr ""
msgid "To"
@@ -21491,7 +21979,7 @@ msgstr ""
msgid "Too many projects enabled. You will need to manage them via the console or the API."
msgstr ""
-msgid "Topics"
+msgid "Topics (optional)"
msgstr ""
msgid "Total"
@@ -21653,6 +22141,9 @@ msgstr ""
msgid "Try all GitLab has to offer for 30 days."
msgstr "GitLabì´ ì œê³µí•˜ëŠ” 모든 ê¸°ëŠ¥ì„ 30ë™ì•ˆ 사용해 보세요."
+msgid "Try changing or removing filters."
+msgstr ""
+
msgid "Try to fork again"
msgstr ""
@@ -21746,6 +22237,9 @@ msgstr ""
msgid "Unable to connect to server: %{error}"
msgstr ""
+msgid "Unable to connect to the Jira instance. Please check your Jira integration configuration."
+msgstr ""
+
msgid "Unable to convert Kubernetes logs encoding to UTF-8"
msgstr ""
@@ -22687,6 +23181,9 @@ msgstr ""
msgid "View issue"
msgstr ""
+msgid "View issues"
+msgstr ""
+
msgid "View it on GitLab"
msgstr "GitLabì—ì„œ 보기"
@@ -22864,6 +23361,15 @@ msgstr ""
msgid "VulnerabilityManagement|Resolved %{timeago} by %{user}"
msgstr ""
+msgid "VulnerabilityManagement|Something went wrong while trying to delete the comment. Please try again later."
+msgstr ""
+
+msgid "VulnerabilityManagement|Something went wrong while trying to retrieve the vulnerability history. Please try again later."
+msgstr ""
+
+msgid "VulnerabilityManagement|Something went wrong while trying to save the comment. Please try again later."
+msgstr ""
+
msgid "VulnerabilityManagement|Something went wrong, could not create an issue."
msgstr ""
@@ -22894,6 +23400,9 @@ msgstr ""
msgid "VulnerabilityStatusTypes|Resolved"
msgstr ""
+msgid "Vulnerability|%{scannerName} (version %{scannerVersion})"
+msgstr ""
+
msgid "Vulnerability|Class"
msgstr ""
@@ -22927,7 +23436,10 @@ msgstr ""
msgid "Vulnerability|Project"
msgstr ""
-msgid "Vulnerability|Report Type"
+msgid "Vulnerability|Scanner Provider"
+msgstr ""
+
+msgid "Vulnerability|Scanner Type"
msgstr ""
msgid "Vulnerability|Severity"
@@ -22981,9 +23493,15 @@ msgstr ""
msgid "We sent you an email with reset password instructions"
msgstr ""
+msgid "We tried to automatically renew your %{strong}%{plan_name}%{strong_close} subscription for %{strong}%{namespace_name}%{strong_close} on %{expires_on} but something went wrong so your subscription was downgraded to the free plan. Don't worry, your data is safe. We suggest you check your payment method and get in touch with our support team (%{support_link}). They'll gladly help with your subscription renewal."
+msgstr ""
+
msgid "We want to be sure it is you, please confirm you are not a robot."
msgstr "ì •ë§ ë‹¹ì‹ ì´ ë§žëŠ”ì§€ 확ì¸í•˜ê³  싶습니다, ë‹¹ì‹ ì´ ë¡œë´‡ì´ ì•„ë‹ˆë¼ëŠ” ê²ƒì„ í™•ì¸í•´ì£¼ì‹­ì‹œì˜¤."
+msgid "We will automatically renew your %{strong}%{plan_name}%{strong_close} subscription for %{strong}%{namespace_name}%{strong_close} on %{strong}%{expires_on}%{strong_close}. There's nothing that you need to do, we'll let you know when the renewal is complete. Need more seats, a higher plan or just want to review your payment method?"
+msgstr ""
+
msgid "We've found no vulnerabilities"
msgstr ""
@@ -23087,15 +23605,6 @@ msgstr ""
msgid "When:"
msgstr ""
-msgid "While it's rare to have no vulnerabilities for your group, it can happen. In any event, we ask that you double check your settings to make sure you've set up your dashboard correctly."
-msgstr ""
-
-msgid "While it's rare to have no vulnerabilities for your pipeline, it can happen. In any event, we ask that you double check your settings to make sure all security scanning jobs have passed successfully."
-msgstr ""
-
-msgid "While it's rare to have no vulnerabilities for your project, it can happen. In any event, we ask that you double check your settings to make sure you've set up your dashboard correctly."
-msgstr ""
-
msgid "While it's rare to have no vulnerabilities, it can happen. In any event, we ask that you please double check your settings to make sure you've set up your dashboard correctly."
msgstr ""
@@ -23126,9 +23635,6 @@ msgstr ""
msgid "Wiki"
msgstr "위키"
-msgid "Wiki pages"
-msgstr "위키 페ì´ì§€"
-
msgid "Wiki was successfully updated."
msgstr ""
@@ -23312,6 +23818,9 @@ msgstr ""
msgid "Yes, add it"
msgstr "네, 추가합니다."
+msgid "Yes, close issue"
+msgstr ""
+
msgid "Yes, let me map Google Code users to full names or GitLab users."
msgstr "네, Google code 사용ìžë¥¼ ì´ë¦„ì´ë‚˜ GitLab 사용ìžì— 연결하겠습니다."
@@ -23507,6 +24016,9 @@ msgstr "ì½ê¸° ì „ìš© GitLab ì¸ìŠ¤í„´ìŠ¤ì—는 쓰기가 불가능합니다."
msgid "You could not create a new trigger."
msgstr ""
+msgid "You didn't renew your %{strong}%{plan_name}%{strong_close} subscription for %{strong}%{namespace_name}%{strong_close} so it was downgraded to the free plan."
+msgstr ""
+
msgid "You didn't renew your %{strong}%{plan_name}%{strong_close} subscription so it was downgraded to the GitLab Core Plan."
msgstr ""
@@ -23573,6 +24085,9 @@ msgstr ""
msgid "You have declined the invitation to join %{label}."
msgstr ""
+msgid "You have imported from this project %{numberOfPreviousImportsForProject} times before. Each new import will create duplicate issues."
+msgstr ""
+
msgid "You have no permissions"
msgstr "ê¶Œí•œì´ ì—†ìŠµë‹ˆë‹¤."
@@ -23654,6 +24169,9 @@ msgstr ""
msgid "You need to upload a Google Takeout archive."
msgstr ""
+msgid "You reached %{usage_in_percent} of %{namespace_name}'s capacity (%{used_storage} of %{storage_limit})"
+msgstr ""
+
msgid "You tried to fork %{link_to_the_project} but it failed for the following reason:"
msgstr ""
@@ -23738,9 +24256,21 @@ msgstr ""
msgid "YouTube"
msgstr "YouTube"
+msgid "Your %{strong}%{plan_name}%{strong_close} subscription for %{strong}%{namespace_name}%{strong_close} will expire on %{strong}%{expires_on}%{strong_close}. After that, you will not to be able to create issues or merge requests as well as many other features."
+msgstr ""
+
msgid "Your %{strong}%{plan_name}%{strong_close} subscription will expire on %{strong}%{expires_on}%{strong_close}. After that, you will not to be able to create issues or merge requests as well as many other features."
msgstr ""
+msgid "Your CSV export has started. It will be emailed to %{email} when complete."
+msgstr ""
+
+msgid "Your CSV export of %{issues_count} from project %{project_link} has been added to this email as an attachment."
+msgstr ""
+
+msgid "Your CSV export of %{written_count} from project %{project_name} (%{project_url}) has been added to this email as an attachment."
+msgstr ""
+
msgid "Your Commit Email will be used for web based operations, such as edits and merges."
msgstr ""
@@ -23837,6 +24367,9 @@ msgstr ""
msgid "Your comment could not be updated! Please check your network connection and try again."
msgstr ""
+msgid "Your comment will be discarded."
+msgstr ""
+
msgid "Your custom stage '%{title}' was created"
msgstr ""
@@ -23876,6 +24409,9 @@ msgstr ""
msgid "Your new personal access token has been created."
msgstr ""
+msgid "Your new project access token has been created."
+msgstr ""
+
msgid "Your password isn't required to view this page. If a password or any other personal details are requested, please contact your administrator to report abuse."
msgstr ""
@@ -23894,12 +24430,18 @@ msgstr "ë‚´ 프로ì íŠ¸"
msgid "Your request for access has been queued for review."
msgstr ""
+msgid "Your search didn't match any commits."
+msgstr ""
+
msgid "Your subscription expired!"
msgstr ""
msgid "Your subscription has been downgraded"
msgstr ""
+msgid "Your subscription will automatically renew in %{remaining_days}"
+msgstr ""
+
msgid "Your subscription will expire in %{remaining_days}"
msgstr ""
@@ -23937,7 +24479,7 @@ msgstr "ì „"
msgid "allowed to fail"
msgstr ""
-msgid "already being used for another group or project milestone."
+msgid "already being used for another group or project %{timebox_name}."
msgstr ""
msgid "already has a \"created\" issue link"
@@ -24015,6 +24557,9 @@ msgstr ""
msgid "ciReport|%{linkStartTag}Learn more about SAST %{linkEndTag}"
msgstr ""
+msgid "ciReport|%{linkStartTag}Learn more about Secret Scanning %{linkEndTag}"
+msgstr ""
+
msgid "ciReport|%{linkStartTag}Learn more about codequality reports %{linkEndTag}"
msgstr ""
@@ -24173,6 +24718,12 @@ msgstr ""
msgid "ciReport|SAST"
msgstr "SAST"
+msgid "ciReport|Secret scanning"
+msgstr ""
+
+msgid "ciReport|Secret scanning detects secrets and credentials vulnerabilities in your source code."
+msgstr ""
+
msgid "ciReport|Security scanning"
msgstr ""
@@ -24366,6 +24917,9 @@ msgstr ""
msgid "group"
msgstr ""
+msgid "groups"
+msgstr ""
+
msgid "has already been linked to another vulnerability"
msgstr ""
@@ -24527,9 +25081,6 @@ msgstr[0] "머지 리퀘스트(MR)"
msgid "merged %{time_ago}"
msgstr ""
-msgid "milestone should belong either to a project or a group."
-msgstr ""
-
msgid "missing"
msgstr ""
@@ -24866,6 +25417,9 @@ msgstr "새 머지 리퀘스트(MR)"
msgid "no contributions"
msgstr ""
+msgid "no expiration"
+msgstr ""
+
msgid "no one can merge"
msgstr ""
@@ -24916,6 +25470,9 @@ msgstr ""
msgid "pending removal"
msgstr ""
+msgid "per day"
+msgstr ""
+
msgid "pipeline"
msgstr ""
@@ -24942,6 +25499,12 @@ msgstr[0] "프로ì íŠ¸"
msgid "project avatar"
msgstr ""
+msgid "projects"
+msgstr ""
+
+msgid "push to your repository, create pipelines, create issues or add comments. To reduce storage capacity, delete unused repositories, artifacts, wikis, issues, and pipelines."
+msgstr ""
+
msgid "quick actions"
msgstr ""
@@ -25087,10 +25650,6 @@ msgstr ""
msgid "this document"
msgstr "ì´ ë¬¸ì„œ"
-msgid "thread resolved"
-msgid_plural "threads resolved"
-msgstr[0] ""
-
msgid "to help your contributors communicate effectively!"
msgstr ""
@@ -25145,6 +25704,9 @@ msgstr "GitLabì—ì„œ 보기"
msgid "view the blob"
msgstr ""
+msgid "vulnerability|Add a comment"
+msgstr ""
+
msgid "vulnerability|Add a comment or reason for dismissal"
msgstr ""
diff --git a/locale/ku_TR/gitlab.po b/locale/ku_TR/gitlab.po
index 291c0499c73..22c00156333 100644
--- a/locale/ku_TR/gitlab.po
+++ b/locale/ku_TR/gitlab.po
@@ -12,7 +12,7 @@ msgstr ""
"X-Crowdin-Project: gitlab-ee\n"
"X-Crowdin-Language: ku\n"
"X-Crowdin-File: /master/locale/gitlab.pot\n"
-"PO-Revision-Date: 2020-04-15 00:25\n"
+"PO-Revision-Date: 2020-05-05 21:13\n"
msgid " %{start} to %{end}"
msgstr ""
@@ -212,6 +212,11 @@ msgid_plural "%d tags"
msgstr[0] ""
msgstr[1] ""
+msgid "%d unresolved thread"
+msgid_plural "%d unresolved threads"
+msgstr[0] ""
+msgstr[1] ""
+
msgid "%d vulnerability dismissed"
msgid_plural "%d vulnerabilities dismissed"
msgstr[0] ""
@@ -371,6 +376,9 @@ msgstr ""
msgid "%{mrText}, this issue will be closed automatically."
msgstr ""
+msgid "%{namespace_name} is now read-only. You cannot: %{base_message}"
+msgstr ""
+
msgid "%{name} contained %{resultsString}"
msgstr ""
@@ -383,6 +391,11 @@ msgstr ""
msgid "%{name}'s avatar"
msgstr ""
+msgid "%{no_of_days} day"
+msgid_plural "%{no_of_days} days"
+msgstr[0] ""
+msgstr[1] ""
+
msgid "%{number_commits_behind} commits behind %{default_branch}, %{number_commits_ahead} commits ahead"
msgstr ""
@@ -506,6 +519,9 @@ msgstr[1] ""
msgid "%{text} is available"
msgstr ""
+msgid "%{timebox_name} should belong either to a project or a group."
+msgstr ""
+
msgid "%{title} %{operator} %{threshold}"
msgstr ""
@@ -781,12 +797,18 @@ msgstr ""
msgid "<code>\"johnsmith@example.com\": \"johnsmith@example.com\"</code> will add \"By <a href=\"#\">johnsmith@example.com</a>\" to all issues and comments originally created by johnsmith@example.com. By default, the email address or username is masked to ensure the user's privacy. Use this option if you want to show the full email address."
msgstr ""
+msgid "<namespace / project>"
+msgstr ""
+
msgid "<no name set>"
msgstr ""
msgid "<no scopes selected>"
msgstr ""
+msgid "<project name>"
+msgstr ""
+
msgid "<strong>%{group_name}</strong> group members"
msgstr ""
@@ -820,6 +842,9 @@ msgstr ""
msgid "A basic page and serverless function that uses AWS Lambda, AWS API Gateway, and GitLab Pages"
msgstr ""
+msgid "A complete DevOps platform"
+msgstr ""
+
msgid "A default branch cannot be chosen for an empty project."
msgstr ""
@@ -1036,6 +1061,12 @@ msgstr ""
msgid "AccessTokens|reset it"
msgstr ""
+msgid "AccessibilityReport|Accessibility report artifact not found"
+msgstr ""
+
+msgid "AccessibilityReport|Failed to retrieve accessibility report"
+msgstr ""
+
msgid "AccessibilityReport|Learn More"
msgstr ""
@@ -1045,6 +1076,9 @@ msgstr ""
msgid "AccessibilityReport|New"
msgstr ""
+msgid "AccessibilityReport|The accessibility scanning found an error of the following type: %{code}"
+msgstr ""
+
msgid "Account"
msgstr ""
@@ -1287,6 +1321,9 @@ msgstr ""
msgid "Added at"
msgstr ""
+msgid "Added for this merge request"
+msgstr ""
+
msgid "Added in this version"
msgstr ""
@@ -1658,6 +1695,72 @@ msgid_plural "Alerts"
msgstr[0] ""
msgstr[1] ""
+msgid "Alert Details"
+msgstr ""
+
+msgid "AlertManagement|Acknowledged"
+msgstr ""
+
+msgid "AlertManagement|Alert"
+msgstr ""
+
+msgid "AlertManagement|Authorize external service"
+msgstr ""
+
+msgid "AlertManagement|Display alerts from all your monitoring tools directly within GitLab. Streamline the investigation of your alerts and the escalation of alerts to incidents."
+msgstr ""
+
+msgid "AlertManagement|End time"
+msgstr ""
+
+msgid "AlertManagement|End time:"
+msgstr ""
+
+msgid "AlertManagement|Events"
+msgstr ""
+
+msgid "AlertManagement|Events:"
+msgstr ""
+
+msgid "AlertManagement|Full Alert Details"
+msgstr ""
+
+msgid "AlertManagement|More information"
+msgstr ""
+
+msgid "AlertManagement|No alerts available to display. If you think you're seeing this message in error, refresh the page."
+msgstr ""
+
+msgid "AlertManagement|No alerts to display."
+msgstr ""
+
+msgid "AlertManagement|Overview"
+msgstr ""
+
+msgid "AlertManagement|Resolved"
+msgstr ""
+
+msgid "AlertManagement|Severity"
+msgstr ""
+
+msgid "AlertManagement|Start time"
+msgstr ""
+
+msgid "AlertManagement|Start time:"
+msgstr ""
+
+msgid "AlertManagement|Status"
+msgstr ""
+
+msgid "AlertManagement|Surface alerts in GitLab"
+msgstr ""
+
+msgid "AlertManagement|There was an error displaying the alerts. Confirm your endpoint's configuration details to ensure alerts appear."
+msgstr ""
+
+msgid "AlertManagement|Triggered"
+msgstr ""
+
msgid "AlertService|%{linkStart}Learn more%{linkEnd} about configuring this endpoint to receive alerts."
msgstr ""
@@ -1727,6 +1830,9 @@ msgstr ""
msgid "All security scans are enabled because %{linkStart}Auto DevOps%{linkEnd} is enabled on this project"
msgstr ""
+msgid "All threads resolved"
+msgstr ""
+
msgid "All users"
msgstr ""
@@ -1745,6 +1851,9 @@ msgstr ""
msgid "Allow only the selected protocols to be used for Git access."
msgstr ""
+msgid "Allow owners to manage default branch protection per group"
+msgstr ""
+
msgid "Allow owners to manually add users outside of LDAP"
msgstr ""
@@ -1853,6 +1962,9 @@ msgstr ""
msgid "An error occurred fetching the dropdown data."
msgstr ""
+msgid "An error occurred fetching the project authors."
+msgstr ""
+
msgid "An error occurred previewing the blob"
msgstr ""
@@ -1934,6 +2046,9 @@ msgstr ""
msgid "An error occurred while fetching sidebar data"
msgstr ""
+msgid "An error occurred while fetching terraform reports."
+msgstr ""
+
msgid "An error occurred while fetching the Service Desk address."
msgstr ""
@@ -2171,6 +2286,9 @@ msgstr ""
msgid "Any"
msgstr ""
+msgid "Any Author"
+msgstr ""
+
msgid "Any Label"
msgstr ""
@@ -2386,10 +2504,10 @@ msgstr ""
msgid "Are you sure that you want to unarchive this project?"
msgstr ""
-msgid "Are you sure you want to cancel creating this comment?"
+msgid "Are you sure you want to cancel editing this comment?"
msgstr ""
-msgid "Are you sure you want to cancel editing this comment?"
+msgid "Are you sure you want to close this blocked issue?"
msgstr ""
msgid "Are you sure you want to delete %{name}?"
@@ -2419,6 +2537,9 @@ msgstr ""
msgid "Are you sure you want to deploy this environment?"
msgstr ""
+msgid "Are you sure you want to discard this comment?"
+msgstr ""
+
msgid "Are you sure you want to erase this build?"
msgstr ""
@@ -2595,6 +2716,9 @@ msgstr ""
msgid "At least one approval from a code owner is required to change files matching the respective CODEOWNER rules."
msgstr ""
+msgid "At least one logging option is required to be enabled"
+msgstr ""
+
msgid "At least one of group_id or project_id must be specified"
msgstr ""
@@ -2633,6 +2757,27 @@ msgstr ""
msgid "AuditEvents|Target"
msgstr ""
+msgid "AuditLogs|(removed)"
+msgstr ""
+
+msgid "AuditLogs|Action"
+msgstr ""
+
+msgid "AuditLogs|Author"
+msgstr ""
+
+msgid "AuditLogs|Date"
+msgstr ""
+
+msgid "AuditLogs|IP Address"
+msgstr ""
+
+msgid "AuditLogs|Object"
+msgstr ""
+
+msgid "AuditLogs|Target"
+msgstr ""
+
msgid "Aug"
msgstr ""
@@ -2915,9 +3060,6 @@ msgstr ""
msgid "BambooService|You must set up automatic revision labeling and a repository trigger in Bamboo."
msgstr ""
-msgid "Batch operations"
-msgstr ""
-
msgid "BatchComments|Delete all pending comments"
msgstr ""
@@ -3047,6 +3189,9 @@ msgstr ""
msgid "Boards"
msgstr ""
+msgid "Boards and Board Lists"
+msgstr ""
+
msgid "Boards|Collapse"
msgstr ""
@@ -3266,6 +3411,9 @@ msgstr ""
msgid "Buy GitLab Enterprise Edition"
msgstr ""
+msgid "Buy more Pipeline minutes"
+msgstr ""
+
msgid "By %{user_name}"
msgstr ""
@@ -3386,6 +3534,9 @@ msgstr ""
msgid "Can't scan the code?"
msgstr ""
+msgid "Can't update snippet: %{err}"
+msgstr ""
+
msgid "Canary"
msgstr ""
@@ -3554,6 +3705,9 @@ msgstr ""
msgid "Changing group path can have unintended side effects."
msgstr ""
+msgid "Charts"
+msgstr ""
+
msgid "Charts can't be displayed as the request for data has timed out. %{documentationLink}"
msgstr ""
@@ -3590,9 +3744,6 @@ msgstr ""
msgid "ChatMessage|and [%{count} more](%{pipeline_failed_jobs_url})"
msgstr ""
-msgid "ChatMessage|failed"
-msgstr ""
-
msgid "ChatMessage|has failed"
msgstr ""
@@ -3608,9 +3759,6 @@ msgstr ""
msgid "ChatMessage|in %{project_link}"
msgstr ""
-msgid "ChatMessage|passed"
-msgstr ""
-
msgid "Check again"
msgstr ""
@@ -4061,9 +4209,6 @@ msgstr ""
msgid "Click any <strong>project name</strong> in the project list below to navigate to the project milestone."
msgstr ""
-msgid "Click here"
-msgstr ""
-
msgid "Click the <strong>Download</strong> button and wait for downloading to complete."
msgstr ""
@@ -4451,6 +4596,12 @@ msgstr ""
msgid "ClusterIntegration|Fetching zones"
msgstr ""
+msgid "ClusterIntegration|Fluentd"
+msgstr ""
+
+msgid "ClusterIntegration|Fluentd is an open source data collector, which lets you unify the data collection and consumption for a better use and understanding of data. It requires at least one of the following logs to be successfully installed."
+msgstr ""
+
msgid "ClusterIntegration|GitLab Integration"
msgstr ""
@@ -4757,6 +4908,15 @@ msgstr ""
msgid "ClusterIntegration|Request to begin uninstalling failed"
msgstr ""
+msgid "ClusterIntegration|SIEM Hostname"
+msgstr ""
+
+msgid "ClusterIntegration|SIEM Port"
+msgstr ""
+
+msgid "ClusterIntegration|SIEM Protocol"
+msgstr ""
+
msgid "ClusterIntegration|Save changes"
msgstr ""
@@ -4853,6 +5013,12 @@ msgstr ""
msgid "ClusterIntegration|Select zone to choose machine type"
msgstr ""
+msgid "ClusterIntegration|Send Cilium Logs"
+msgstr ""
+
+msgid "ClusterIntegration|Send ModSecurity Logs"
+msgstr ""
+
msgid "ClusterIntegration|Service Token"
msgstr ""
@@ -5281,18 +5447,33 @@ msgstr ""
msgid "Compliance framework (optional)"
msgstr ""
+msgid "ComplianceFramework|GDPR"
+msgstr ""
+
msgid "ComplianceFramework|GDPR - General Data Protection Regulation"
msgstr ""
+msgid "ComplianceFramework|HIPAA"
+msgstr ""
+
msgid "ComplianceFramework|HIPAA - Health Insurance Portability and Accountability Act"
msgstr ""
+msgid "ComplianceFramework|PCI-DSS"
+msgstr ""
+
msgid "ComplianceFramework|PCI-DSS - Payment Card Industry-Data Security Standard"
msgstr ""
+msgid "ComplianceFramework|SOC 2"
+msgstr ""
+
msgid "ComplianceFramework|SOC 2 - Service Organization Control 2"
msgstr ""
+msgid "ComplianceFramework|SOX"
+msgstr ""
+
msgid "ComplianceFramework|SOX - Sarbanes-Oxley"
msgstr ""
@@ -5440,9 +5621,15 @@ msgstr ""
msgid "Container repositories sync capacity"
msgstr ""
+msgid "ContainerRegistry| Please visit the %{linkStart}administration settings%{linkEnd} to enable this feature."
+msgstr ""
+
msgid "ContainerRegistry|%{imageName} tags"
msgstr ""
+msgid "ContainerRegistry|%{title} was successfully scheduled for deletion"
+msgstr ""
+
msgid "ContainerRegistry|Automatically remove extra images that aren't designed to be kept."
msgstr ""
@@ -5455,6 +5642,9 @@ msgstr ""
msgid "ContainerRegistry|Container Registry"
msgstr ""
+msgid "ContainerRegistry|Container Registry tag expiration and retention policy is disabled"
+msgstr ""
+
msgid "ContainerRegistry|Copy build command"
msgstr ""
@@ -5464,18 +5654,12 @@ msgstr ""
msgid "ContainerRegistry|Copy push command"
msgstr ""
-msgid "ContainerRegistry|Currently, the Container Registry tag expiration feature is not available for projects created before GitLab version 12.8. For updates and more information, visit Issue %{linkStart}#196124%{linkEnd}"
-msgstr ""
-
msgid "ContainerRegistry|Docker connection error"
msgstr ""
msgid "ContainerRegistry|Docker tag expiration policy is %{toggleStatus}"
msgstr ""
-msgid "ContainerRegistry|Docker tags with names matching this regex pattern will expire:"
-msgstr ""
-
msgid "ContainerRegistry|Edit Settings"
msgstr ""
@@ -5497,9 +5681,6 @@ msgstr ""
msgid "ContainerRegistry|Image ID"
msgstr ""
-msgid "ContainerRegistry|Image deleted successfully"
-msgstr ""
-
msgid "ContainerRegistry|Keep and protect the images that matter most."
msgstr ""
@@ -5515,12 +5696,24 @@ msgstr ""
msgid "ContainerRegistry|Number of tags to retain:"
msgstr ""
+msgid "ContainerRegistry|Please contact your administrator."
+msgstr ""
+
msgid "ContainerRegistry|Push an image"
msgstr ""
msgid "ContainerRegistry|Quick Start"
msgstr ""
+msgid "ContainerRegistry|Regular expressions such as %{codeStart}.*-test%{codeEnd} or %{codeStart}dev-.*%{codeEnd} are supported"
+msgstr ""
+
+msgid "ContainerRegistry|Regular expressions such as %{codeStart}.*-test%{codeEnd} or %{codeStart}dev-.*%{codeEnd} are supported. To select all tags, use %{codeStart}.*%{codeEnd}"
+msgstr ""
+
+msgid "ContainerRegistry|Remember to run %{docLinkStart}garbage collection%{docLinkEnd} to remove the stale data from storage."
+msgstr ""
+
msgid "ContainerRegistry|Remove repository"
msgstr ""
@@ -5535,22 +5728,22 @@ msgstr[1] ""
msgid "ContainerRegistry|Retention policy has been Enabled"
msgstr ""
-msgid "ContainerRegistry|Something went wrong while deleting the image."
+msgid "ContainerRegistry|Something went wrong while fetching the expiration policy."
msgstr ""
-msgid "ContainerRegistry|Something went wrong while deleting the tag."
+msgid "ContainerRegistry|Something went wrong while fetching the repository list."
msgstr ""
-msgid "ContainerRegistry|Something went wrong while deleting the tags."
+msgid "ContainerRegistry|Something went wrong while fetching the tags list."
msgstr ""
-msgid "ContainerRegistry|Something went wrong while fetching the expiration policy."
+msgid "ContainerRegistry|Something went wrong while marking the tag for deletion."
msgstr ""
-msgid "ContainerRegistry|Something went wrong while fetching the packages list."
+msgid "ContainerRegistry|Something went wrong while marking the tags for deletion."
msgstr ""
-msgid "ContainerRegistry|Something went wrong while fetching the tags list."
+msgid "ContainerRegistry|Something went wrong while scheduling %{title} for deletion. Please try again."
msgstr ""
msgid "ContainerRegistry|Something went wrong while updating the expiration policy."
@@ -5559,16 +5752,25 @@ msgstr ""
msgid "ContainerRegistry|Tag"
msgstr ""
-msgid "ContainerRegistry|Tag deleted successfully"
-msgstr ""
-
msgid "ContainerRegistry|Tag expiration policy"
msgstr ""
msgid "ContainerRegistry|Tag expiration policy is designed to:"
msgstr ""
-msgid "ContainerRegistry|Tags deleted successfully"
+msgid "ContainerRegistry|Tag successfully marked for deletion."
+msgstr ""
+
+msgid "ContainerRegistry|Tags successfully marked for deletion."
+msgstr ""
+
+msgid "ContainerRegistry|Tags with names matching this regex pattern will %{italicStart}be preserved:%{italicEnd}"
+msgstr ""
+
+msgid "ContainerRegistry|Tags with names matching this regex pattern will %{italicStart}expire:%{italicEnd}"
+msgstr ""
+
+msgid "ContainerRegistry|The Container Registry tag expiration and retention policies for this project have not been enabled."
msgstr ""
msgid "ContainerRegistry|The last tag related to this image was recently removed. This empty image and any associated data will be automatically removed as part of the regular Garbage Collection process. If you have any questions, contact your administrator."
@@ -5589,16 +5791,16 @@ msgstr ""
msgid "ContainerRegistry|There are no container images stored for this project"
msgstr ""
-msgid "ContainerRegistry|This Registry contains deleted image tag data. Remember to run %{docLinkStart}garbage collection%{docLinkEnd} to remove the stale data from storage."
+msgid "ContainerRegistry|There was an error during the deletion of this image repository, please try again."
msgstr ""
msgid "ContainerRegistry|This image has no active tags"
msgstr ""
-msgid "ContainerRegistry|We are having trouble connecting to Docker, which could be due to an issue with your project name or path. %{docLinkStart}More Information%{docLinkEnd}"
+msgid "ContainerRegistry|This image repository is scheduled for deletion"
msgstr ""
-msgid "ContainerRegistry|Wildcards such as %{codeStart}.*-stable%{codeEnd} or %{codeStart}production/.*%{codeEnd} are supported. To select all tags, use %{codeStart}.*%{codeEnd}"
+msgid "ContainerRegistry|We are having trouble connecting to Docker, which could be due to an issue with your project name or path. %{docLinkStart}More Information%{docLinkEnd}"
msgstr ""
msgid "ContainerRegistry|With the Container Registry, every project can have its own space to store its Docker images. %{docLinkStart}More Information%{docLinkEnd}"
@@ -5853,7 +6055,7 @@ msgstr ""
msgid "Could not delete chat nickname %{chat_name}."
msgstr ""
-msgid "Could not find design"
+msgid "Could not find design."
msgstr ""
msgid "Could not remove the trigger."
@@ -5868,6 +6070,9 @@ msgstr ""
msgid "Could not revoke personal access token %{personal_access_token_name}."
msgstr ""
+msgid "Could not revoke project access token %{project_access_token_name}."
+msgstr ""
+
msgid "Could not save group ID"
msgstr ""
@@ -5925,6 +6130,9 @@ msgstr ""
msgid "Create a new branch"
msgstr ""
+msgid "Create a new deploy key for this project"
+msgstr ""
+
msgid "Create a new file as there are no files yet. Afterwards, you'll be able to commit your changes."
msgstr ""
@@ -6030,6 +6238,9 @@ msgstr ""
msgid "Create requirement"
msgstr ""
+msgid "Create snippet"
+msgstr ""
+
msgid "Create wildcard: %{searchTerm}"
msgstr ""
@@ -6051,6 +6262,9 @@ msgstr ""
msgid "Created"
msgstr ""
+msgid "Created %{timestamp}"
+msgstr ""
+
msgid "Created At"
msgstr ""
@@ -6407,6 +6621,9 @@ msgstr ""
msgid "CycleAnalytics|stage dropdown"
msgstr ""
+msgid "DAG"
+msgstr ""
+
msgid "DNS"
msgstr ""
@@ -6773,6 +6990,9 @@ msgstr ""
msgid "Deploy key was successfully updated."
msgstr ""
+msgid "Deploy keys allow read-only or read-write (if enabled) access to your repository. Deploy keys can be used for CI, staging or production servers. You can create a deploy key or add an existing one."
+msgstr ""
+
msgid "Deploy progress not found. To see pods, ensure your environment matches %{linkStart}deploy board criteria%{linkEnd}."
msgstr ""
@@ -6926,6 +7146,9 @@ msgstr ""
msgid "Deploying to"
msgstr ""
+msgid "Deployment Frequency"
+msgstr ""
+
msgid "Deployment|API"
msgstr ""
@@ -7444,9 +7667,6 @@ msgstr ""
msgid "Edit environment"
msgstr ""
-msgid "Edit epic description"
-msgstr ""
-
msgid "Edit file"
msgstr ""
@@ -8068,9 +8288,6 @@ msgstr ""
msgid "Epics"
msgstr ""
-msgid "Epics (Ultimate / Gold license only)"
-msgstr ""
-
msgid "Epics Roadmap"
msgstr ""
@@ -8080,6 +8297,9 @@ msgstr ""
msgid "Epics let you manage your portfolio of projects more efficiently and with less effort"
msgstr ""
+msgid "Epics, Issues, and Merge Requests"
+msgstr ""
+
msgid "Epics|Add an epic"
msgstr ""
@@ -8131,6 +8351,9 @@ msgstr ""
msgid "Epics|Something went wrong while fetching group epics."
msgstr ""
+msgid "Epics|Something went wrong while moving item."
+msgstr ""
+
msgid "Epics|Something went wrong while ordering item."
msgstr ""
@@ -8185,6 +8408,9 @@ msgstr ""
msgid "Error fetching network graph."
msgstr ""
+msgid "Error fetching payload data."
+msgstr ""
+
msgid "Error fetching projects"
msgstr ""
@@ -8194,9 +8420,6 @@ msgstr ""
msgid "Error fetching the dependency list. Please check your network connection and try again."
msgstr ""
-msgid "Error fetching usage ping data."
-msgstr ""
-
msgid "Error loading branch data. Please try again."
msgstr ""
@@ -8338,6 +8561,9 @@ msgstr ""
msgid "Errors"
msgstr ""
+msgid "Errors:"
+msgstr ""
+
msgid "Estimated"
msgstr ""
@@ -8542,12 +8768,18 @@ msgstr ""
msgid "Export as CSV"
msgstr ""
+msgid "Export group"
+msgstr ""
+
msgid "Export issues"
msgstr ""
msgid "Export project"
msgstr ""
+msgid "Export this group with all related data to a new GitLab instance. Once complete, you can import the data file from the \"New Group\" page."
+msgstr ""
+
msgid "Export this project with all its related data in order to move your project to a new GitLab instance. Once the export is finished, you can import the file from the \"New Project\" page."
msgstr ""
@@ -8617,9 +8849,6 @@ msgstr ""
msgid "Failed Jobs"
msgstr ""
-msgid "Failed create wiki"
-msgstr ""
-
msgid "Failed to add a Zoom meeting"
msgstr ""
@@ -8659,6 +8888,9 @@ msgstr ""
msgid "Failed to create resources"
msgstr ""
+msgid "Failed to create wiki"
+msgstr ""
+
msgid "Failed to delete board. Please try again."
msgstr ""
@@ -8668,7 +8900,7 @@ msgstr ""
msgid "Failed to enqueue the rebase operation, possibly due to a long-lived transaction. Try again later."
msgstr ""
-msgid "Failed to find import label for jira import."
+msgid "Failed to find import label for Jira import."
msgstr ""
msgid "Failed to get ref."
@@ -8692,6 +8924,12 @@ msgstr ""
msgid "Failed to load groups & users."
msgstr ""
+msgid "Failed to load labels. Please try again."
+msgstr ""
+
+msgid "Failed to load milestones. Please try again."
+msgstr ""
+
msgid "Failed to load related branches"
msgstr ""
@@ -9067,7 +9305,10 @@ msgstr ""
msgid "Filter by milestone name"
msgstr ""
-msgid "Filter by name..."
+msgid "Filter by name"
+msgstr ""
+
+msgid "Filter by status"
msgstr ""
msgid "Filter by two-factor authentication"
@@ -9079,12 +9320,18 @@ msgstr ""
msgid "Filter projects"
msgstr ""
+msgid "Filter results"
+msgstr ""
+
msgid "Filter results by group"
msgstr ""
msgid "Filter results by project"
msgstr ""
+msgid "Filter results..."
+msgstr ""
+
msgid "Filter your projects by name"
msgstr ""
@@ -9373,10 +9620,10 @@ msgstr ""
msgid "GeoNodes|Checksummed"
msgstr ""
-msgid "GeoNodes|Container repositories"
+msgid "GeoNodes|Consult Geo troubleshooting information"
msgstr ""
-msgid "GeoNodes|Data is out of date from %{timeago}"
+msgid "GeoNodes|Container repositories"
msgstr ""
msgid "GeoNodes|Data replication lag"
@@ -9418,6 +9665,9 @@ msgstr ""
msgid "GeoNodes|Last event ID seen from primary"
msgstr ""
+msgid "GeoNodes|Learn more about Geo node statuses"
+msgstr ""
+
msgid "GeoNodes|Loading nodes"
msgstr ""
@@ -9433,6 +9683,9 @@ msgstr ""
msgid "GeoNodes|Node was successfully removed."
msgstr ""
+msgid "GeoNodes|Node's status was updated %{timeAgo}."
+msgstr ""
+
msgid "GeoNodes|Not checksummed"
msgstr ""
@@ -9463,7 +9716,10 @@ msgstr ""
msgid "GeoNodes|Repository verification progress"
msgstr ""
-msgid "GeoNodes|Selective"
+msgid "GeoNodes|Selective (%{syncLabel})"
+msgstr ""
+
+msgid "GeoNodes|Selective synchronization"
msgstr ""
msgid "GeoNodes|Something went wrong while changing node status"
@@ -9490,6 +9746,9 @@ msgstr ""
msgid "GeoNodes|Unverified"
msgstr ""
+msgid "GeoNodes|Updated %{timeAgo}"
+msgstr ""
+
msgid "GeoNodes|Used slots"
msgstr ""
@@ -9529,16 +9788,16 @@ msgstr ""
msgid "Geo|All"
msgstr ""
-msgid "Geo|All projects"
+msgid "Geo|All %{replicable_type}"
msgstr ""
-msgid "Geo|All projects are being scheduled for re-sync"
+msgid "Geo|All projects"
msgstr ""
-msgid "Geo|All projects are being scheduled for re-verify"
+msgid "Geo|All projects are being scheduled for resync"
msgstr ""
-msgid "Geo|Batch operations"
+msgid "Geo|All projects are being scheduled for reverify"
msgstr ""
msgid "Geo|Could not remove tracking entry for an existing project."
@@ -9550,9 +9809,15 @@ msgstr ""
msgid "Geo|Failed"
msgstr ""
+msgid "Geo|Filter by status"
+msgstr ""
+
msgid "Geo|Geo Status"
msgstr ""
+msgid "Geo|In progress"
+msgstr ""
+
msgid "Geo|In sync"
msgstr ""
@@ -9577,9 +9842,6 @@ msgstr ""
msgid "Geo|Not synced yet"
msgstr ""
-msgid "Geo|Pending"
-msgstr ""
-
msgid "Geo|Pending synchronization"
msgstr ""
@@ -9616,7 +9878,7 @@ msgstr ""
msgid "Geo|Resync"
msgstr ""
-msgid "Geo|Resync all projects"
+msgid "Geo|Resync all"
msgstr ""
msgid "Geo|Retry count"
@@ -9625,7 +9887,7 @@ msgstr ""
msgid "Geo|Reverify"
msgstr ""
-msgid "Geo|Reverify all projects"
+msgid "Geo|Reverify all"
msgstr ""
msgid "Geo|Status"
@@ -9967,9 +10229,6 @@ msgstr ""
msgid "Go to file"
msgstr ""
-msgid "Go to file (MRs only)"
-msgstr ""
-
msgid "Go to file permalink (while viewing a file)"
msgstr ""
@@ -10414,6 +10673,9 @@ msgstr ""
msgid "GroupSettings|Disable group mentions"
msgstr ""
+msgid "GroupSettings|Export group"
+msgstr ""
+
msgid "GroupSettings|If the parent group's visibility is lower than the group current visibility, visibility levels for subgroups and projects will be changed to match the new parent group's visibility."
msgstr ""
@@ -10492,6 +10754,9 @@ msgstr ""
msgid "Groups (%{count})"
msgstr ""
+msgid "Groups (%{groups})"
+msgstr ""
+
msgid "Groups can also be nested by creating %{subgroup_docs_link_start}subgroups%{subgroup_docs_link_end}."
msgstr ""
@@ -10757,15 +11022,12 @@ msgstr ""
msgid "ID:"
msgstr ""
-msgid "IDE|Allow live previews of JavaScript projects in the Web IDE using CodeSandbox client side evaluation."
+msgid "IDE|Allow live previews of JavaScript projects in the Web IDE using CodeSandbox Live Preview."
msgstr ""
msgid "IDE|Back"
msgstr ""
-msgid "IDE|Client side evaluation"
-msgstr ""
-
msgid "IDE|Commit"
msgstr ""
@@ -10829,6 +11091,9 @@ msgstr ""
msgid "If any job surpasses this timeout threshold, it will be marked as failed. Human readable time input language is accepted like \"1 hour\". Values without specification represent seconds."
msgstr ""
+msgid "If blank, set allowable lifetime to %{instance_level_policy_in_words}, as defined by the instance admin. Once set, existing tokens for users in this group may be revoked."
+msgstr ""
+
msgid "If checked, group owners can manage LDAP group links and LDAP member overrides"
msgstr ""
@@ -10868,6 +11133,9 @@ msgstr ""
msgid "If you lose your recovery codes you can generate new ones, invalidating all previous codes."
msgstr ""
+msgid "If you reach 100%% storage capacity, you will not be able to: %{base_message}"
+msgstr ""
+
msgid "If your HTTP repository is not publicly accessible, add your credentials."
msgstr ""
@@ -10979,6 +11247,9 @@ msgstr ""
msgid "Import repository"
msgstr ""
+msgid "Import started by: %{importInitiator}"
+msgstr ""
+
msgid "Import tasks"
msgstr ""
@@ -11045,6 +11316,9 @@ msgstr ""
msgid "In order to tailor your experience with GitLab we<br>would like to know a bit more about you."
msgstr ""
+msgid "In progress"
+msgstr ""
+
msgid "In the next step, you'll be able to select the projects you want to import."
msgstr ""
@@ -11392,15 +11666,15 @@ msgstr ""
msgid "Issues"
msgstr ""
-msgid "Issues / Merge Requests"
-msgstr ""
-
msgid "Issues Analytics"
msgstr ""
msgid "Issues Rate Limits"
msgstr ""
+msgid "Issues and Merge Requests"
+msgstr ""
+
msgid "Issues can be bugs, tasks or ideas to be discussed. Also, issues are searchable and filterable."
msgstr ""
@@ -11422,10 +11696,10 @@ msgstr ""
msgid "IssuesAnalytics|Avg/Month:"
msgstr ""
-msgid "IssuesAnalytics|Issues created"
+msgid "IssuesAnalytics|Issues opened"
msgstr ""
-msgid "IssuesAnalytics|Issues created per month"
+msgid "IssuesAnalytics|Issues opened per month"
msgstr ""
msgid "IssuesAnalytics|Last 12 months"
@@ -11479,6 +11753,9 @@ msgstr ""
msgid "Jira integration not configured."
msgstr ""
+msgid "Jira project: %{importProject}"
+msgstr ""
+
msgid "JiraService|Events for %{noteable_model_name} are disabled."
msgstr ""
@@ -11868,6 +12145,9 @@ msgstr ""
msgid "Lead"
msgstr ""
+msgid "Lead Time"
+msgstr ""
+
msgid "Learn GitLab"
msgstr ""
@@ -11880,6 +12160,9 @@ msgstr ""
msgid "Learn how to %{no_packages_link_start}publish and share your packages%{no_packages_link_end} with GitLab."
msgstr ""
+msgid "Learn how to enable synchronization"
+msgstr ""
+
msgid "Learn more"
msgstr ""
@@ -12211,9 +12494,6 @@ msgstr ""
msgid "Live preview"
msgstr ""
-msgid "Load more vulnerabilities"
-msgstr ""
-
msgid "Loading"
msgstr ""
@@ -12364,9 +12644,6 @@ msgstr ""
msgid "Manage"
msgstr ""
-msgid "Manage Git repositories with fine-grained access controls that keep your code secure. Perform code reviews and enhance collaboration with merge requests. Each project can also have an issue tracker and a wiki."
-msgstr ""
-
msgid "Manage Web IDE features"
msgstr ""
@@ -12397,6 +12674,9 @@ msgstr ""
msgid "Manage your license"
msgstr ""
+msgid "Managed Account"
+msgstr ""
+
msgid "Manifest"
msgstr ""
@@ -12661,6 +12941,9 @@ msgstr ""
msgid "Merge Requests in Review"
msgstr ""
+msgid "Merge automatically (%{strategy})"
+msgstr ""
+
msgid "Merge commit message"
msgstr ""
@@ -12814,6 +13097,12 @@ msgstr ""
msgid "Merged branches are being deleted. This can take some time depending on the number of branches. Please refresh the page to see changes."
msgstr ""
+msgid "Merged this merge request."
+msgstr ""
+
+msgid "Merges this merge request immediately."
+msgstr ""
+
msgid "Merges this merge request when the pipeline succeeds."
msgstr ""
@@ -12921,12 +13210,15 @@ msgid_plural "Metrics|Edit metrics"
msgstr[0] ""
msgstr[1] ""
-msgid "Metrics|Environment"
+msgid "Metrics|Expand panel"
msgstr ""
msgid "Metrics|For grouping similar metrics"
msgstr ""
+msgid "Metrics|Go back (Esc)"
+msgstr ""
+
msgid "Metrics|Invalid time range, please verify."
msgstr ""
@@ -12957,9 +13249,6 @@ msgstr ""
msgid "Metrics|Refresh dashboard"
msgstr ""
-msgid "Metrics|Show last"
-msgstr ""
-
msgid "Metrics|There was an error creating the dashboard."
msgstr ""
@@ -13038,6 +13327,9 @@ msgstr ""
msgid "Microsoft Azure"
msgstr ""
+msgid "Middleman project with Static Site Editor support"
+msgstr ""
+
msgid "Migrated %{success_count}/%{total_count} files."
msgstr ""
@@ -13483,10 +13775,10 @@ msgstr ""
msgid "Next"
msgstr ""
-msgid "Next file in diff (MRs only)"
+msgid "Next file in diff"
msgstr ""
-msgid "Next unresolved discussion (MRs only)"
+msgid "Next unresolved discussion"
msgstr ""
msgid "Nickname"
@@ -13546,6 +13838,9 @@ msgstr ""
msgid "No changes between %{ref_start}%{source_branch}%{ref_end} and %{ref_start}%{target_branch}%{ref_end}"
msgstr ""
+msgid "No child epics match applied filters"
+msgstr ""
+
msgid "No connection could be made to a Gitaly Server, please check your logs!"
msgstr ""
@@ -13684,15 +13979,6 @@ msgstr ""
msgid "No thanks, don't show this again"
msgstr ""
-msgid "No vulnerabilities found for this group"
-msgstr ""
-
-msgid "No vulnerabilities found for this pipeline"
-msgstr ""
-
-msgid "No vulnerabilities found for this project"
-msgstr ""
-
msgid "No vulnerabilities present"
msgstr ""
@@ -13816,6 +14102,9 @@ msgstr ""
msgid "Nothing to preview."
msgstr ""
+msgid "Nothing to synchronize"
+msgstr ""
+
msgid "Notification events"
msgstr ""
@@ -13987,6 +14276,9 @@ msgstr ""
msgid "Once removed, the fork relationship cannot be restored and you will no longer be able to send merge requests to the source."
msgstr ""
+msgid "Once the exported file is ready you can download it from this page."
+msgstr ""
+
msgid "Once the exported file is ready, you will receive a notification email with a download link, or you can download it from this page."
msgstr ""
@@ -14088,9 +14380,6 @@ msgstr ""
msgid "Open sidebar"
msgstr ""
-msgid "Open source software to collaborate on code"
-msgstr ""
-
msgid "Open: %{openIssuesCount}"
msgstr ""
@@ -14193,6 +14482,9 @@ msgstr ""
msgid "OutdatedBrowser|You can provide feedback %{feedback_link_start}on this issue%{feedback_link_end} or via your usual support channels."
msgstr ""
+msgid "Overridden"
+msgstr ""
+
msgid "Overview"
msgstr ""
@@ -14244,6 +14536,9 @@ msgstr ""
msgid "PackageRegistry|Conan Command"
msgstr ""
+msgid "PackageRegistry|Copy .pypirc content"
+msgstr ""
+
msgid "PackageRegistry|Copy Conan Command"
msgstr ""
@@ -14265,6 +14560,9 @@ msgstr ""
msgid "PackageRegistry|Copy NuGet Setup Command"
msgstr ""
+msgid "PackageRegistry|Copy Pip command"
+msgstr ""
+
msgid "PackageRegistry|Copy and paste this inside your %{codeStart}pom.xml%{codeEnd} %{codeStart}dependencies%{codeEnd} block."
msgstr ""
@@ -14298,6 +14596,12 @@ msgstr ""
msgid "PackageRegistry|For more information on the NuGet registry, %{linkStart}see the documentation%{linkEnd}."
msgstr ""
+msgid "PackageRegistry|For more information on the PyPi registry, %{linkStart}see the documentation%{linkEnd}."
+msgstr ""
+
+msgid "PackageRegistry|If you haven't already done so, you will need to add the below to your %{codeStart}.pypirc%{codeEnd} file."
+msgstr ""
+
msgid "PackageRegistry|If you haven't already done so, you will need to add the below to your %{codeStart}pom.xml%{codeEnd} file."
msgstr ""
@@ -14307,6 +14611,9 @@ msgstr ""
msgid "PackageRegistry|Learn how to %{noPackagesLinkStart}publish and share your packages%{noPackagesLinkEnd} with GitLab."
msgstr ""
+msgid "PackageRegistry|Manually Published"
+msgstr ""
+
msgid "PackageRegistry|Maven"
msgstr ""
@@ -14325,12 +14632,18 @@ msgstr ""
msgid "PackageRegistry|NuGet Command"
msgstr ""
+msgid "PackageRegistry|Pip Command"
+msgstr ""
+
msgid "PackageRegistry|Pipeline %{linkStart}%{linkEnd} triggered %{timestamp} by %{author}"
msgstr ""
msgid "PackageRegistry|Published to the repository at %{timestamp}"
msgstr ""
+msgid "PackageRegistry|PyPi"
+msgstr ""
+
msgid "PackageRegistry|Registry Setup"
msgstr ""
@@ -14367,6 +14680,9 @@ msgstr ""
msgid "PackageRegistry|npm"
msgstr ""
+msgid "PackageRegistry|published by %{author}"
+msgstr ""
+
msgid "PackageRegistry|yarn"
msgstr ""
@@ -14382,6 +14698,9 @@ msgstr ""
msgid "PackageType|NuGet"
msgstr ""
+msgid "PackageType|PyPi"
+msgstr ""
+
msgid "Packages"
msgstr ""
@@ -14475,6 +14794,9 @@ msgstr ""
msgid "Past due"
msgstr ""
+msgid "Paste a machine public key here. Read more about how to generate it"
+msgstr ""
+
msgid "Paste a machine public key here. Read more about how to generate it %{link_start}here%{link_end}"
msgstr ""
@@ -14490,9 +14812,6 @@ msgstr ""
msgid "Path"
msgstr ""
-msgid "Path, transfer, remove"
-msgstr ""
-
msgid "Path:"
msgstr ""
@@ -14523,7 +14842,7 @@ msgstr ""
msgid "Percentage"
msgstr ""
-msgid "Perform advanced options such as changing path, transferring, or removing the group."
+msgid "Perform advanced options such as changing path, transferring, exporting, or removing the group."
msgstr ""
msgid "Perform common operations on GitLab project"
@@ -14604,9 +14923,6 @@ msgstr ""
msgid "Pipeline minutes quota"
msgstr ""
-msgid "Pipeline quota"
-msgstr ""
-
msgid "Pipeline subscriptions"
msgstr ""
@@ -14616,6 +14932,9 @@ msgstr ""
msgid "Pipeline: %{status}"
msgstr ""
+msgid "PipelineCharts|CI / CD Analytics"
+msgstr ""
+
msgid "PipelineCharts|Failed:"
msgstr ""
@@ -14694,15 +15013,6 @@ msgstr ""
msgid "Pipelines settings for '%{project_name}' were successfully updated."
msgstr ""
-msgid "Pipelines| to purchase more minutes."
-msgstr ""
-
-msgid "Pipelines|%{namespace_name} has exceeded its pipeline minutes quota."
-msgstr ""
-
-msgid "Pipelines|%{namespace_name} has less than %{notification_level}%% of CI minutes available."
-msgstr ""
-
msgid "Pipelines|API"
msgstr ""
@@ -14724,10 +15034,13 @@ msgstr ""
msgid "Pipelines|Get started with Pipelines"
msgstr ""
-msgid "Pipelines|Loading Pipelines"
+msgid "Pipelines|Group %{namespace_name} has %{percentage}%% or less Shared Runner Pipeline minutes remaining. Once it runs out, no new jobs or pipelines in its projects will run."
msgstr ""
-msgid "Pipelines|Pipelines will not run anymore on shared Runners."
+msgid "Pipelines|Group %{namespace_name} has exceeded its pipeline minutes quota. Unless you buy additional pipeline minutes, no new jobs or pipelines in its projects will run."
+msgstr ""
+
+msgid "Pipelines|Loading Pipelines"
msgstr ""
msgid "Pipelines|Project cache successfully reset."
@@ -14958,6 +15271,9 @@ msgstr ""
msgid "Please select"
msgstr ""
+msgid "Please select a Jira project"
+msgstr ""
+
msgid "Please select a country"
msgstr ""
@@ -15003,6 +15319,9 @@ msgstr ""
msgid "Please wait while we import the repository for you. Refresh at will."
msgstr ""
+msgid "Plugins directory is deprecated and will be removed in 14.0. Please move this file into /file_hooks directory."
+msgstr ""
+
msgid "Pod does not exist"
msgstr ""
@@ -15120,7 +15439,7 @@ msgstr ""
msgid "Prevent users from changing their profile name"
msgstr ""
-msgid "Prevent users from modifing merge request approvers list"
+msgid "Prevent users from modifying merge request approvers list"
msgstr ""
msgid "Prevent users from performing write operations on GitLab while performing maintenance."
@@ -15141,10 +15460,10 @@ msgstr ""
msgid "Previous Artifacts"
msgstr ""
-msgid "Previous file in diff (MRs only)"
+msgid "Previous file in diff"
msgstr ""
-msgid "Previous unresolved discussion (MRs only)"
+msgid "Previous unresolved discussion"
msgstr ""
msgid "Primary"
@@ -15177,6 +15496,9 @@ msgstr ""
msgid "Private profile"
msgstr ""
+msgid "Private projects Minutes cost factor"
+msgstr ""
+
msgid "Private projects can be created in your personal namespace with:"
msgstr ""
@@ -16005,6 +16327,9 @@ msgstr ""
msgid "ProjectSettings|With GitLab Pages you can host your static websites on GitLab"
msgstr ""
+msgid "ProjectSettings|With Metrics Dashboard you can visualize this project performance metrics"
+msgstr ""
+
msgid "ProjectTemplates|.NET Core"
msgstr ""
@@ -16068,6 +16393,9 @@ msgstr ""
msgid "ProjectTemplates|Spring"
msgstr ""
+msgid "ProjectTemplates|Static Site Editor/Middleman"
+msgstr ""
+
msgid "ProjectTemplates|iOS (Swift)"
msgstr ""
@@ -16287,13 +16615,13 @@ msgstr ""
msgid "PrometheusService|Prometheus is being automatically managed on your clusters"
msgstr ""
-msgid "PrometheusService|These metrics will only be monitored after your first deployment to an environment"
+msgid "PrometheusService|Select the Active checkbox to override the Auto Configuration with custom settings. If unchecked, Auto Configuration settings are used."
msgstr ""
-msgid "PrometheusService|Time-series monitoring service"
+msgid "PrometheusService|These metrics will only be monitored after your first deployment to an environment"
msgstr ""
-msgid "PrometheusService|To enable manual configuration, uninstall Prometheus from your clusters"
+msgid "PrometheusService|Time-series monitoring service"
msgstr ""
msgid "PrometheusService|To enable the installation of Prometheus on your clusters, deactivate the manual configuration below"
@@ -16479,6 +16807,9 @@ msgstr ""
msgid "Protip:"
msgstr ""
+msgid "Protocol"
+msgstr ""
+
msgid "Provider"
msgstr ""
@@ -16503,6 +16834,9 @@ msgstr ""
msgid "Public pipelines"
msgstr ""
+msgid "Public projects Minutes cost factor"
+msgstr ""
+
msgid "Pull"
msgstr ""
@@ -16703,6 +17037,9 @@ msgid_plural "Refreshing in %d seconds to show the updated status..."
msgstr[0] ""
msgstr[1] ""
+msgid "Regenerate export"
+msgstr ""
+
msgid "Regenerate instance ID"
msgstr ""
@@ -17080,9 +17417,29 @@ msgstr ""
msgid "Reports|%{combinedString} and %{resolvedString}"
msgstr ""
+msgid "Reports|Accessibility scanning detected %d issue for the source branch only"
+msgid_plural "Reports|Accessibility scanning detected %d issues for the source branch only"
+msgstr[0] ""
+msgstr[1] ""
+
+msgid "Reports|Accessibility scanning detected no issues for the source branch only"
+msgstr ""
+
+msgid "Reports|Accessibility scanning failed loading results"
+msgstr ""
+
+msgid "Reports|Accessibility scanning results are being parsed"
+msgstr ""
+
msgid "Reports|Actions"
msgstr ""
+msgid "Reports|An error occured while loading report"
+msgstr ""
+
+msgid "Reports|An error occurred while loading %{name} results"
+msgstr ""
+
msgid "Reports|Class"
msgstr ""
@@ -17188,7 +17545,7 @@ msgstr ""
msgid "Requested %{time_ago}"
msgstr ""
-msgid "Requested design version does not exist"
+msgid "Requested design version does not exist."
msgstr ""
msgid "Requested states are invalid"
@@ -17372,6 +17729,9 @@ msgstr ""
msgid "Resync"
msgstr ""
+msgid "Resync all"
+msgstr ""
+
msgid "Resync all %{replicableType}"
msgstr ""
@@ -17440,6 +17800,9 @@ msgstr ""
msgid "Revoked personal access token %{personal_access_token_name}!"
msgstr ""
+msgid "Revoked project access token %{project_access_token_name}!"
+msgstr ""
+
msgid "RightSidebar|adding a"
msgstr ""
@@ -17488,6 +17851,9 @@ msgstr ""
msgid "Runner token"
msgstr ""
+msgid "Runner tokens"
+msgstr ""
+
msgid "Runner was not updated."
msgstr ""
@@ -17542,6 +17908,9 @@ msgstr ""
msgid "SAML SSO for %{group_name}"
msgstr ""
+msgid "SAML discovery tokens"
+msgstr ""
+
msgid "SAML for %{group_name}"
msgstr ""
@@ -17626,12 +17995,18 @@ msgstr ""
msgid "Scheduled"
msgstr ""
+msgid "Scheduled to merge this merge request (%{strategy})."
+msgstr ""
+
msgid "Scheduled to merge this merge request when the pipeline succeeds."
msgstr ""
msgid "Schedules"
msgstr ""
+msgid "Schedules to merge this merge request (%{strategy})."
+msgstr ""
+
msgid "Scheduling"
msgstr ""
@@ -17689,6 +18064,9 @@ msgstr ""
msgid "Search branches and tags"
msgstr ""
+msgid "Search by author"
+msgstr ""
+
msgid "Search files"
msgstr ""
@@ -17834,11 +18212,6 @@ msgid_plural "SearchResults|snippets"
msgstr[0] ""
msgstr[1] ""
-msgid "SearchResults|snippet result"
-msgid_plural "SearchResults|snippet results"
-msgstr[0] ""
-msgstr[1] ""
-
msgid "SearchResults|user"
msgid_plural "SearchResults|users"
msgstr[0] ""
@@ -17849,6 +18222,9 @@ msgid_plural "SearchResults|wiki results"
msgstr[0] ""
msgstr[1] ""
+msgid "Searching by both author and message is currently not supported."
+msgstr ""
+
msgid "Seat Link"
msgstr ""
@@ -17876,184 +18252,232 @@ msgstr ""
msgid "Security Dashboard"
msgstr ""
-msgid "Security Dashboard|Error fetching the vulnerability counts. Please check your network connection and try again."
+msgid "Security configuration help link"
+msgstr ""
+
+msgid "Security dashboard"
msgstr ""
-msgid "Security Dashboard|Error fetching the vulnerability list. Please check your network connection and try again."
+msgid "Security report is out of date. Please update your branch with the latest changes from the target branch (%{targetBranchName})"
msgstr ""
-msgid "Security Dashboard|Issue Created"
+msgid "Security report is out of date. Run %{newPipelineLinkStart}a new pipeline%{newPipelineLinkEnd} for the target branch (%{targetBranchName})"
msgstr ""
-msgid "Security Reports|Comment added to '%{vulnerabilityName}'"
+msgid "SecurityConfiguration|Configured"
msgstr ""
-msgid "Security Reports|Comment deleted on '%{vulnerabilityName}'"
+msgid "SecurityConfiguration|Feature"
msgstr ""
-msgid "Security Reports|Comment edited on '%{vulnerabilityName}'"
+msgid "SecurityConfiguration|Feature documentation for %{featureName}"
msgstr ""
-msgid "Security Reports|Create issue"
+msgid "SecurityConfiguration|Not yet configured"
msgstr ""
-msgid "Security Reports|Dismiss vulnerability"
+msgid "SecurityConfiguration|Secure features"
msgstr ""
-msgid "Security Reports|Dismissed '%{vulnerabilityName}'"
+msgid "SecurityConfiguration|Status"
msgstr ""
-msgid "Security Reports|Dismissed '%{vulnerabilityName}'. Turn off the hide dismissed toggle to view."
+msgid "SecurityReports|%{firstProject} and %{secondProject}"
msgstr ""
-msgid "Security Reports|Either you don't have permission to view this dashboard or the dashboard has not been setup. Please check your permission settings with your administrator or check your dashboard configurations to proceed."
+msgid "SecurityReports|%{firstProject}, %{secondProject}, and %{rest}"
msgstr ""
-msgid "Security Reports|Learn more about setting up your dashboard"
+msgid "SecurityReports|Add a project to your dashboard"
msgstr ""
-msgid "Security Reports|More info"
+msgid "SecurityReports|Add or remove projects from your dashboard"
msgstr ""
-msgid "Security Reports|Oops, something doesn't seem right."
+msgid "SecurityReports|Add projects"
msgstr ""
-msgid "Security Reports|Security reports can only be accessed by authorized users."
+msgid "SecurityReports|Comment added to '%{vulnerabilityName}'"
msgstr ""
-msgid "Security Reports|There was an error adding the comment."
+msgid "SecurityReports|Comment deleted on '%{vulnerabilityName}'"
msgstr ""
-msgid "Security Reports|There was an error creating the issue."
+msgid "SecurityReports|Comment edited on '%{vulnerabilityName}'"
msgstr ""
-msgid "Security Reports|There was an error creating the merge request."
+msgid "SecurityReports|Create issue"
msgstr ""
-msgid "Security Reports|There was an error deleting the comment."
+msgid "SecurityReports|Dismiss Selected"
msgstr ""
-msgid "Security Reports|There was an error dismissing the vulnerabilities."
+msgid "SecurityReports|Dismiss vulnerability"
msgstr ""
-msgid "Security Reports|There was an error dismissing the vulnerability."
+msgid "SecurityReports|Dismissed '%{vulnerabilityName}'"
msgstr ""
-msgid "Security Reports|There was an error reverting the dismissal."
+msgid "SecurityReports|Dismissed '%{vulnerabilityName}'. Turn off the hide dismissed toggle to view."
msgstr ""
-msgid "Security Reports|There was an error reverting this dismissal."
+msgid "SecurityReports|Each vulnerability now has a unique page that can be directly linked to, shared, referenced, and tracked as the single source of truth. Vulnerability occurrences also persist across scanner runs, which improves tracking and visibility and reduces duplicates between scans."
msgstr ""
-msgid "Security Reports|Undo dismiss"
+msgid "SecurityReports|Edit dashboard"
msgstr ""
-msgid "Security Reports|You do not have sufficient permissions to access this report"
+msgid "SecurityReports|Either you don't have permission to view this dashboard or the dashboard has not been setup. Please check your permission settings with your administrator or check your dashboard configurations to proceed."
msgstr ""
-msgid "Security Reports|You must sign in as an authorized user to see this report"
+msgid "SecurityReports|Error fetching the vulnerability counts. Please check your network connection and try again."
msgstr ""
-msgid "Security configuration help link"
+msgid "SecurityReports|Error fetching the vulnerability list. Please check your network connection and try again."
msgstr ""
-msgid "Security dashboard"
+msgid "SecurityReports|False positive"
msgstr ""
-msgid "Security report is out of date. Please update your branch with the latest changes from the target branch (%{targetBranchName})"
+msgid "SecurityReports|Hide dismissed"
msgstr ""
-msgid "Security report is out of date. Run %{newPipelineLinkStart}a new pipeline%{newPipelineLinkEnd} for the target branch (%{targetBranchName})"
+msgid "SecurityReports|Introducing standalone vulnerabilities"
msgstr ""
-msgid "SecurityConfiguration|Configured"
+msgid "SecurityReports|Issue Created"
msgstr ""
-msgid "SecurityConfiguration|Feature"
+msgid "SecurityReports|Learn More"
msgstr ""
-msgid "SecurityConfiguration|Feature documentation for %{featureName}"
+msgid "SecurityReports|Learn more about setting up your dashboard"
msgstr ""
-msgid "SecurityConfiguration|Not yet configured"
+msgid "SecurityReports|Load more vulnerabilities"
msgstr ""
-msgid "SecurityConfiguration|Secure features"
+msgid "SecurityReports|Monitor vulnerabilities in your code"
msgstr ""
-msgid "SecurityConfiguration|Status"
+msgid "SecurityReports|More info"
msgstr ""
-msgid "SecurityDashboard|%{firstProject} and %{secondProject}"
+msgid "SecurityReports|More information"
msgstr ""
-msgid "SecurityDashboard|%{firstProject}, %{secondProject}, and %{rest}"
+msgid "SecurityReports|No vulnerabilities found for dashboard"
msgstr ""
-msgid "SecurityDashboard|Add a project to your dashboard"
+msgid "SecurityReports|No vulnerabilities found for this group"
msgstr ""
-msgid "SecurityDashboard|Add or remove projects from your dashboard"
+msgid "SecurityReports|No vulnerabilities found for this pipeline"
msgstr ""
-msgid "SecurityDashboard|Add projects"
+msgid "SecurityReports|No vulnerabilities found for this project"
msgstr ""
-msgid "SecurityDashboard|Edit dashboard"
+msgid "SecurityReports|Oops, something doesn't seem right."
msgstr ""
-msgid "SecurityDashboard|Hide dismissed"
+msgid "SecurityReports|Pipeline %{pipelineLink} triggered %{timeago} by %{user}"
msgstr ""
-msgid "SecurityDashboard|Monitor vulnerabilities in your code"
+msgid "SecurityReports|Project"
msgstr ""
-msgid "SecurityDashboard|More information"
+msgid "SecurityReports|Projects added"
msgstr ""
-msgid "SecurityDashboard|No vulnerabilities found for dashboard"
+msgid "SecurityReports|Remove project from dashboard"
msgstr ""
-msgid "SecurityDashboard|Pipeline %{pipelineLink} triggered %{timeago} by %{user}"
+msgid "SecurityReports|Report type"
msgstr ""
-msgid "SecurityDashboard|Project"
+msgid "SecurityReports|Return to dashboard"
msgstr ""
-msgid "SecurityDashboard|Projects added"
+msgid "SecurityReports|Security Dashboard"
msgstr ""
-msgid "SecurityDashboard|Remove project from dashboard"
+msgid "SecurityReports|Security reports can only be accessed by authorized users."
msgstr ""
-msgid "SecurityDashboard|Report type"
+msgid "SecurityReports|Select a project to add by using the project search field above."
msgstr ""
-msgid "SecurityDashboard|Return to dashboard"
+msgid "SecurityReports|Select a reason"
msgstr ""
-msgid "SecurityDashboard|Security Dashboard"
+msgid "SecurityReports|Severity"
msgstr ""
-msgid "SecurityDashboard|Select a project to add by using the project search field above."
+msgid "SecurityReports|Status"
msgstr ""
-msgid "SecurityDashboard|Severity"
+msgid "SecurityReports|The security dashboard displays the latest security findings for projects you wish to monitor. Select \"Edit dashboard\" to add and remove projects."
msgstr ""
-msgid "SecurityDashboard|Status"
+msgid "SecurityReports|The security dashboard displays the latest security report. Use it to find and fix vulnerabilities."
msgstr ""
-msgid "SecurityDashboard|The security dashboard displays the latest security findings for projects you wish to monitor. Select \"Edit dashboard\" to add and remove projects."
+msgid "SecurityReports|There was an error adding the comment."
msgstr ""
-msgid "SecurityDashboard|The security dashboard displays the latest security report. Use it to find and fix vulnerabilities."
+msgid "SecurityReports|There was an error creating the issue."
msgstr ""
-msgid "SecurityDashboard|There was an error while generating the report."
+msgid "SecurityReports|There was an error creating the merge request."
msgstr ""
-msgid "SecurityDashboard|Unable to add %{invalidProjects}"
+msgid "SecurityReports|There was an error deleting the comment."
+msgstr ""
+
+msgid "SecurityReports|There was an error dismissing the vulnerabilities."
+msgstr ""
+
+msgid "SecurityReports|There was an error dismissing the vulnerability."
+msgstr ""
+
+msgid "SecurityReports|There was an error reverting the dismissal."
+msgstr ""
+
+msgid "SecurityReports|There was an error reverting this dismissal."
+msgstr ""
+
+msgid "SecurityReports|There was an error while generating the report."
+msgstr ""
+
+msgid "SecurityReports|Unable to add %{invalidProjects}"
+msgstr ""
+
+msgid "SecurityReports|Undo dismiss"
+msgstr ""
+
+msgid "SecurityReports|While it's rare to have no vulnerabilities for your group, it can happen. In any event, we ask that you double check your settings to make sure you've set up your dashboard correctly."
+msgstr ""
+
+msgid "SecurityReports|While it's rare to have no vulnerabilities for your pipeline, it can happen. In any event, we ask that you double check your settings to make sure all security scanning jobs have passed successfully."
+msgstr ""
+
+msgid "SecurityReports|While it's rare to have no vulnerabilities for your project, it can happen. In any event, we ask that you double check your settings to make sure you've set up your dashboard correctly."
+msgstr ""
+
+msgid "SecurityReports|While it's rare to have no vulnerabilities, it can happen. In any event, we ask that you please double check your settings to make sure you've set up your dashboard correctly."
+msgstr ""
+
+msgid "SecurityReports|Won't fix / Accept risk"
+msgstr ""
+
+msgid "SecurityReports|You do not have sufficient permissions to access this report"
+msgstr ""
+
+msgid "SecurityReports|You must sign in as an authorized user to see this report"
+msgstr ""
+
+msgid "SecurityReports|[No reason]"
msgstr ""
msgid "See GitLab's %{password_policy_guidelines}"
@@ -18065,6 +18489,9 @@ msgstr ""
msgid "See the affected projects in the GitLab admin panel"
msgstr ""
+msgid "See what's new at GitLab"
+msgstr ""
+
msgid "Select"
msgstr ""
@@ -18470,9 +18897,6 @@ msgstr ""
msgid "Set up Jira Integration"
msgstr ""
-msgid "Set up Jira Integration illustration"
-msgstr ""
-
msgid "Set up a %{type} Runner automatically"
msgstr ""
@@ -18557,6 +18981,9 @@ msgstr ""
msgid "Severity: %{severity}"
msgstr ""
+msgid "Shards (%{shards})"
+msgstr ""
+
msgid "Shards to synchronize"
msgstr ""
@@ -18871,6 +19298,9 @@ msgstr ""
msgid "Snowplow"
msgstr ""
+msgid "Some child epics may be hidden due to applied filters"
+msgstr ""
+
msgid "Some email servers do not support overriding the email sender name. Enable this option to include the name of the author of the issue, merge request or comment in the email body instead."
msgstr ""
@@ -19012,6 +19442,9 @@ msgstr ""
msgid "Something went wrong while updating your list settings"
msgstr ""
+msgid "Something went wrong with your automatic subscription renewal"
+msgstr ""
+
msgid "Something went wrong, unable to add %{project} to dashboard"
msgstr ""
@@ -19297,6 +19730,9 @@ msgstr ""
msgid "Stage removed"
msgstr ""
+msgid "Standard"
+msgstr ""
+
msgid "Star a label to make it a priority label. Order the prioritized labels to change their relative priority, by dragging."
msgstr ""
@@ -19420,6 +19856,9 @@ msgstr ""
msgid "Static Application Security Testing (SAST)"
msgstr ""
+msgid "StaticSiteEditor|An error occurred while submitting your changes."
+msgstr ""
+
msgid "StaticSiteEditor|Branch could not be created."
msgstr ""
@@ -19429,18 +19868,30 @@ msgstr ""
msgid "StaticSiteEditor|Could not create merge request."
msgstr ""
+msgid "StaticSiteEditor|Incompatible file content"
+msgstr ""
+
msgid "StaticSiteEditor|Return to site"
msgstr ""
+msgid "StaticSiteEditor|Static site editor"
+msgstr ""
+
msgid "StaticSiteEditor|Success!"
msgstr ""
msgid "StaticSiteEditor|Summary of changes"
msgstr ""
+msgid "StaticSiteEditor|The Static Site Editor is currently configured to only edit Markdown content on pages generated from Middleman. Visit the documentation to learn more about configuring your site to use the Static Site Editor."
+msgstr ""
+
msgid "StaticSiteEditor|Update %{sourcePath} file"
msgstr ""
+msgid "StaticSiteEditor|View documentation"
+msgstr ""
+
msgid "StaticSiteEditor|View merge request"
msgstr ""
@@ -19498,6 +19949,12 @@ msgstr ""
msgid "StatusPage|Status page"
msgstr ""
+msgid "StatusPage|Status page URL"
+msgstr ""
+
+msgid "StatusPage|Status page frontend documentation"
+msgstr ""
+
msgid "StatusPage|To publish incidents to an external status page, GitLab will store a JSON file in your Amazon S3 account in a location accessible to your external status page service. Make sure to also set up %{docsLink}"
msgstr ""
@@ -19846,6 +20303,12 @@ msgstr ""
msgid "Synced"
msgstr ""
+msgid "Synchronization disabled"
+msgstr ""
+
+msgid "Synchronization of container repositories is disabled."
+msgstr ""
+
msgid "System"
msgstr ""
@@ -19999,6 +20462,9 @@ msgstr ""
msgid "Telephone number"
msgstr ""
+msgid "Telephone number (Optional)"
+msgstr ""
+
msgid "Template"
msgstr ""
@@ -20305,6 +20771,9 @@ msgstr ""
msgid "The license was successfully uploaded and is now active. You can see the details below."
msgstr ""
+msgid "The license was successfully uploaded and will be active from %{starts_at}. You can see the details below."
+msgstr ""
+
msgid "The maximum file size allowed is %{size}."
msgstr ""
@@ -20626,6 +21095,9 @@ msgstr ""
msgid "There was an error getting the epic participants."
msgstr ""
+msgid "There was an error importing the Jira project."
+msgstr ""
+
msgid "There was an error loading users activity calendar."
msgstr ""
@@ -20641,7 +21113,7 @@ msgstr ""
msgid "There was an error resetting user pipeline minutes."
msgstr ""
-msgid "There was an error saving this Geo Node"
+msgid "There was an error saving this Geo Node."
msgstr ""
msgid "There was an error saving your changes."
@@ -20704,6 +21176,9 @@ msgstr ""
msgid "They can be managed using the %{link}."
msgstr ""
+msgid "Third Party Advisory Link"
+msgstr ""
+
msgid "Third party offers"
msgstr ""
@@ -20740,6 +21215,9 @@ msgstr ""
msgid "This application will be able to:"
msgstr ""
+msgid "This attachment has been truncated to avoid exceeding the maximum allowed attachment size of 15MB. %{written_count} of %{issues_count} issues have been included. Consider re-exporting with a narrower selection of issues."
+msgstr ""
+
msgid "This block is self-referential"
msgstr ""
@@ -20881,6 +21359,12 @@ msgstr ""
msgid "This issue is confidential"
msgstr ""
+msgid "This issue is currently blocked by the following issues: %{issues}."
+msgstr ""
+
+msgid "This issue is in a child epic of the filtered epic"
+msgstr ""
+
msgid "This issue is locked."
msgstr ""
@@ -21079,7 +21563,10 @@ msgstr ""
msgid "This user will be the author of all events in the activity feed that are the result of an update, like new branches being created or new commits being pushed to existing branches. Upon creation or when reassigning you can only assign yourself to be the mirror user."
msgstr ""
-msgid "This variable can not be masked"
+msgid "This variable can not be masked."
+msgstr ""
+
+msgid "This variable does not match the expected pattern."
msgstr ""
msgid "This will help us personalize your onboarding experience."
@@ -21109,10 +21596,10 @@ msgstr ""
msgid "ThreatMonitoring|Application firewall not detected"
msgstr ""
-msgid "ThreatMonitoring|Container Network Policy"
+msgid "ThreatMonitoring|Container Network Policies are not installed or have been disabled. To view this data, ensure your Network Policies are installed and enabled for your cluster."
msgstr ""
-msgid "ThreatMonitoring|Container NetworkPolicies are not installed or has been disabled. To view this data, ensure you NetworkPolicies are installed and enabled for your cluster."
+msgid "ThreatMonitoring|Container Network Policy"
msgstr ""
msgid "ThreatMonitoring|Container NetworkPolicies not detected"
@@ -21145,7 +21632,7 @@ msgstr ""
msgid "ThreatMonitoring|Something went wrong, unable to fetch statistics"
msgstr ""
-msgid "ThreatMonitoring|The firewall is not installed or has been disabled. To view this data, ensure you firewall is installed and enabled for your cluster."
+msgid "ThreatMonitoring|The firewall is not installed or has been disabled. To view this data, ensure the web application firewall is installed and enabled for your cluster."
msgstr ""
msgid "ThreatMonitoring|The graph below is an overview of traffic coming to your application as tracked by the Web Application Firewall (WAF). View the docs for instructions on how to access the WAF logs to see what type of malicious traffic is trying to access your app. The docs link is also accessible by clicking the \"?\" icon next to the title below."
@@ -21214,6 +21701,9 @@ msgstr ""
msgid "Time in seconds GitLab will wait for a response from the external service. When the service does not respond in time, access will be denied."
msgstr ""
+msgid "Time of import: %{importTime}"
+msgstr ""
+
msgid "Time remaining"
msgstr ""
@@ -21398,7 +21888,7 @@ msgstr ""
msgid "Title:"
msgstr ""
-msgid "Titles and Filenames"
+msgid "Titles and Descriptions"
msgstr ""
msgid "To"
@@ -21605,7 +22095,7 @@ msgstr ""
msgid "Too many projects enabled. You will need to manage them via the console or the API."
msgstr ""
-msgid "Topics"
+msgid "Topics (optional)"
msgstr ""
msgid "Total"
@@ -21767,6 +22257,9 @@ msgstr ""
msgid "Try all GitLab has to offer for 30 days."
msgstr ""
+msgid "Try changing or removing filters."
+msgstr ""
+
msgid "Try to fork again"
msgstr ""
@@ -21860,6 +22353,9 @@ msgstr ""
msgid "Unable to connect to server: %{error}"
msgstr ""
+msgid "Unable to connect to the Jira instance. Please check your Jira integration configuration."
+msgstr ""
+
msgid "Unable to convert Kubernetes logs encoding to UTF-8"
msgstr ""
@@ -22803,6 +23299,9 @@ msgstr ""
msgid "View issue"
msgstr ""
+msgid "View issues"
+msgstr ""
+
msgid "View it on GitLab"
msgstr ""
@@ -22980,6 +23479,15 @@ msgstr ""
msgid "VulnerabilityManagement|Resolved %{timeago} by %{user}"
msgstr ""
+msgid "VulnerabilityManagement|Something went wrong while trying to delete the comment. Please try again later."
+msgstr ""
+
+msgid "VulnerabilityManagement|Something went wrong while trying to retrieve the vulnerability history. Please try again later."
+msgstr ""
+
+msgid "VulnerabilityManagement|Something went wrong while trying to save the comment. Please try again later."
+msgstr ""
+
msgid "VulnerabilityManagement|Something went wrong, could not create an issue."
msgstr ""
@@ -23010,6 +23518,9 @@ msgstr ""
msgid "VulnerabilityStatusTypes|Resolved"
msgstr ""
+msgid "Vulnerability|%{scannerName} (version %{scannerVersion})"
+msgstr ""
+
msgid "Vulnerability|Class"
msgstr ""
@@ -23043,7 +23554,10 @@ msgstr ""
msgid "Vulnerability|Project"
msgstr ""
-msgid "Vulnerability|Report Type"
+msgid "Vulnerability|Scanner Provider"
+msgstr ""
+
+msgid "Vulnerability|Scanner Type"
msgstr ""
msgid "Vulnerability|Severity"
@@ -23097,9 +23611,15 @@ msgstr ""
msgid "We sent you an email with reset password instructions"
msgstr ""
+msgid "We tried to automatically renew your %{strong}%{plan_name}%{strong_close} subscription for %{strong}%{namespace_name}%{strong_close} on %{expires_on} but something went wrong so your subscription was downgraded to the free plan. Don't worry, your data is safe. We suggest you check your payment method and get in touch with our support team (%{support_link}). They'll gladly help with your subscription renewal."
+msgstr ""
+
msgid "We want to be sure it is you, please confirm you are not a robot."
msgstr ""
+msgid "We will automatically renew your %{strong}%{plan_name}%{strong_close} subscription for %{strong}%{namespace_name}%{strong_close} on %{strong}%{expires_on}%{strong_close}. There's nothing that you need to do, we'll let you know when the renewal is complete. Need more seats, a higher plan or just want to review your payment method?"
+msgstr ""
+
msgid "We've found no vulnerabilities"
msgstr ""
@@ -23204,15 +23724,6 @@ msgstr ""
msgid "When:"
msgstr ""
-msgid "While it's rare to have no vulnerabilities for your group, it can happen. In any event, we ask that you double check your settings to make sure you've set up your dashboard correctly."
-msgstr ""
-
-msgid "While it's rare to have no vulnerabilities for your pipeline, it can happen. In any event, we ask that you double check your settings to make sure all security scanning jobs have passed successfully."
-msgstr ""
-
-msgid "While it's rare to have no vulnerabilities for your project, it can happen. In any event, we ask that you double check your settings to make sure you've set up your dashboard correctly."
-msgstr ""
-
msgid "While it's rare to have no vulnerabilities, it can happen. In any event, we ask that you please double check your settings to make sure you've set up your dashboard correctly."
msgstr ""
@@ -23243,9 +23754,6 @@ msgstr ""
msgid "Wiki"
msgstr ""
-msgid "Wiki pages"
-msgstr ""
-
msgid "Wiki was successfully updated."
msgstr ""
@@ -23429,6 +23937,9 @@ msgstr ""
msgid "Yes, add it"
msgstr ""
+msgid "Yes, close issue"
+msgstr ""
+
msgid "Yes, let me map Google Code users to full names or GitLab users."
msgstr ""
@@ -23624,6 +24135,9 @@ msgstr ""
msgid "You could not create a new trigger."
msgstr ""
+msgid "You didn't renew your %{strong}%{plan_name}%{strong_close} subscription for %{strong}%{namespace_name}%{strong_close} so it was downgraded to the free plan."
+msgstr ""
+
msgid "You didn't renew your %{strong}%{plan_name}%{strong_close} subscription so it was downgraded to the GitLab Core Plan."
msgstr ""
@@ -23690,6 +24204,9 @@ msgstr ""
msgid "You have declined the invitation to join %{label}."
msgstr ""
+msgid "You have imported from this project %{numberOfPreviousImportsForProject} times before. Each new import will create duplicate issues."
+msgstr ""
+
msgid "You have no permissions"
msgstr ""
@@ -23771,6 +24288,9 @@ msgstr ""
msgid "You need to upload a Google Takeout archive."
msgstr ""
+msgid "You reached %{usage_in_percent} of %{namespace_name}'s capacity (%{used_storage} of %{storage_limit})"
+msgstr ""
+
msgid "You tried to fork %{link_to_the_project} but it failed for the following reason:"
msgstr ""
@@ -23855,9 +24375,21 @@ msgstr ""
msgid "YouTube"
msgstr ""
+msgid "Your %{strong}%{plan_name}%{strong_close} subscription for %{strong}%{namespace_name}%{strong_close} will expire on %{strong}%{expires_on}%{strong_close}. After that, you will not to be able to create issues or merge requests as well as many other features."
+msgstr ""
+
msgid "Your %{strong}%{plan_name}%{strong_close} subscription will expire on %{strong}%{expires_on}%{strong_close}. After that, you will not to be able to create issues or merge requests as well as many other features."
msgstr ""
+msgid "Your CSV export has started. It will be emailed to %{email} when complete."
+msgstr ""
+
+msgid "Your CSV export of %{issues_count} from project %{project_link} has been added to this email as an attachment."
+msgstr ""
+
+msgid "Your CSV export of %{written_count} from project %{project_name} (%{project_url}) has been added to this email as an attachment."
+msgstr ""
+
msgid "Your Commit Email will be used for web based operations, such as edits and merges."
msgstr ""
@@ -23954,6 +24486,9 @@ msgstr ""
msgid "Your comment could not be updated! Please check your network connection and try again."
msgstr ""
+msgid "Your comment will be discarded."
+msgstr ""
+
msgid "Your custom stage '%{title}' was created"
msgstr ""
@@ -23993,6 +24528,9 @@ msgstr ""
msgid "Your new personal access token has been created."
msgstr ""
+msgid "Your new project access token has been created."
+msgstr ""
+
msgid "Your password isn't required to view this page. If a password or any other personal details are requested, please contact your administrator to report abuse."
msgstr ""
@@ -24011,12 +24549,18 @@ msgstr ""
msgid "Your request for access has been queued for review."
msgstr ""
+msgid "Your search didn't match any commits."
+msgstr ""
+
msgid "Your subscription expired!"
msgstr ""
msgid "Your subscription has been downgraded"
msgstr ""
+msgid "Your subscription will automatically renew in %{remaining_days}"
+msgstr ""
+
msgid "Your subscription will expire in %{remaining_days}"
msgstr ""
@@ -24055,7 +24599,7 @@ msgstr ""
msgid "allowed to fail"
msgstr ""
-msgid "already being used for another group or project milestone."
+msgid "already being used for another group or project %{timebox_name}."
msgstr ""
msgid "already has a \"created\" issue link"
@@ -24133,6 +24677,9 @@ msgstr ""
msgid "ciReport|%{linkStartTag}Learn more about SAST %{linkEndTag}"
msgstr ""
+msgid "ciReport|%{linkStartTag}Learn more about Secret Scanning %{linkEndTag}"
+msgstr ""
+
msgid "ciReport|%{linkStartTag}Learn more about codequality reports %{linkEndTag}"
msgstr ""
@@ -24296,6 +24843,12 @@ msgstr ""
msgid "ciReport|SAST"
msgstr ""
+msgid "ciReport|Secret scanning"
+msgstr ""
+
+msgid "ciReport|Secret scanning detects secrets and credentials vulnerabilities in your source code."
+msgstr ""
+
msgid "ciReport|Security scanning"
msgstr ""
@@ -24493,6 +25046,9 @@ msgstr ""
msgid "group"
msgstr ""
+msgid "groups"
+msgstr ""
+
msgid "has already been linked to another vulnerability"
msgstr ""
@@ -24656,9 +25212,6 @@ msgstr[1] ""
msgid "merged %{time_ago}"
msgstr ""
-msgid "milestone should belong either to a project or a group."
-msgstr ""
-
msgid "missing"
msgstr ""
@@ -24995,6 +25548,9 @@ msgstr ""
msgid "no contributions"
msgstr ""
+msgid "no expiration"
+msgstr ""
+
msgid "no one can merge"
msgstr ""
@@ -25047,6 +25603,9 @@ msgstr ""
msgid "pending removal"
msgstr ""
+msgid "per day"
+msgstr ""
+
msgid "pipeline"
msgstr ""
@@ -25075,6 +25634,12 @@ msgstr[1] ""
msgid "project avatar"
msgstr ""
+msgid "projects"
+msgstr ""
+
+msgid "push to your repository, create pipelines, create issues or add comments. To reduce storage capacity, delete unused repositories, artifacts, wikis, issues, and pipelines."
+msgstr ""
+
msgid "quick actions"
msgstr ""
@@ -25221,11 +25786,6 @@ msgstr ""
msgid "this document"
msgstr ""
-msgid "thread resolved"
-msgid_plural "threads resolved"
-msgstr[0] ""
-msgstr[1] ""
-
msgid "to help your contributors communicate effectively!"
msgstr ""
@@ -25280,6 +25840,9 @@ msgstr ""
msgid "view the blob"
msgstr ""
+msgid "vulnerability|Add a comment"
+msgstr ""
+
msgid "vulnerability|Add a comment or reason for dismissal"
msgstr ""
diff --git a/locale/mn_MN/gitlab.po b/locale/mn_MN/gitlab.po
index d79ba180c2e..3a745563dcd 100644
--- a/locale/mn_MN/gitlab.po
+++ b/locale/mn_MN/gitlab.po
@@ -12,7 +12,7 @@ msgstr ""
"X-Crowdin-Project: gitlab-ee\n"
"X-Crowdin-Language: mn\n"
"X-Crowdin-File: /master/locale/gitlab.pot\n"
-"PO-Revision-Date: 2020-04-15 00:25\n"
+"PO-Revision-Date: 2020-05-05 21:11\n"
msgid " %{start} to %{end}"
msgstr ""
@@ -212,6 +212,11 @@ msgid_plural "%d tags"
msgstr[0] ""
msgstr[1] ""
+msgid "%d unresolved thread"
+msgid_plural "%d unresolved threads"
+msgstr[0] ""
+msgstr[1] ""
+
msgid "%d vulnerability dismissed"
msgid_plural "%d vulnerabilities dismissed"
msgstr[0] ""
@@ -371,6 +376,9 @@ msgstr ""
msgid "%{mrText}, this issue will be closed automatically."
msgstr ""
+msgid "%{namespace_name} is now read-only. You cannot: %{base_message}"
+msgstr ""
+
msgid "%{name} contained %{resultsString}"
msgstr ""
@@ -383,6 +391,11 @@ msgstr ""
msgid "%{name}'s avatar"
msgstr ""
+msgid "%{no_of_days} day"
+msgid_plural "%{no_of_days} days"
+msgstr[0] ""
+msgstr[1] ""
+
msgid "%{number_commits_behind} commits behind %{default_branch}, %{number_commits_ahead} commits ahead"
msgstr ""
@@ -506,6 +519,9 @@ msgstr[1] ""
msgid "%{text} is available"
msgstr ""
+msgid "%{timebox_name} should belong either to a project or a group."
+msgstr ""
+
msgid "%{title} %{operator} %{threshold}"
msgstr ""
@@ -781,12 +797,18 @@ msgstr ""
msgid "<code>\"johnsmith@example.com\": \"johnsmith@example.com\"</code> will add \"By <a href=\"#\">johnsmith@example.com</a>\" to all issues and comments originally created by johnsmith@example.com. By default, the email address or username is masked to ensure the user's privacy. Use this option if you want to show the full email address."
msgstr ""
+msgid "<namespace / project>"
+msgstr ""
+
msgid "<no name set>"
msgstr ""
msgid "<no scopes selected>"
msgstr ""
+msgid "<project name>"
+msgstr ""
+
msgid "<strong>%{group_name}</strong> group members"
msgstr ""
@@ -820,6 +842,9 @@ msgstr ""
msgid "A basic page and serverless function that uses AWS Lambda, AWS API Gateway, and GitLab Pages"
msgstr ""
+msgid "A complete DevOps platform"
+msgstr ""
+
msgid "A default branch cannot be chosen for an empty project."
msgstr ""
@@ -1036,6 +1061,12 @@ msgstr ""
msgid "AccessTokens|reset it"
msgstr ""
+msgid "AccessibilityReport|Accessibility report artifact not found"
+msgstr ""
+
+msgid "AccessibilityReport|Failed to retrieve accessibility report"
+msgstr ""
+
msgid "AccessibilityReport|Learn More"
msgstr ""
@@ -1045,6 +1076,9 @@ msgstr ""
msgid "AccessibilityReport|New"
msgstr ""
+msgid "AccessibilityReport|The accessibility scanning found an error of the following type: %{code}"
+msgstr ""
+
msgid "Account"
msgstr ""
@@ -1287,6 +1321,9 @@ msgstr ""
msgid "Added at"
msgstr ""
+msgid "Added for this merge request"
+msgstr ""
+
msgid "Added in this version"
msgstr ""
@@ -1658,6 +1695,72 @@ msgid_plural "Alerts"
msgstr[0] ""
msgstr[1] ""
+msgid "Alert Details"
+msgstr ""
+
+msgid "AlertManagement|Acknowledged"
+msgstr ""
+
+msgid "AlertManagement|Alert"
+msgstr ""
+
+msgid "AlertManagement|Authorize external service"
+msgstr ""
+
+msgid "AlertManagement|Display alerts from all your monitoring tools directly within GitLab. Streamline the investigation of your alerts and the escalation of alerts to incidents."
+msgstr ""
+
+msgid "AlertManagement|End time"
+msgstr ""
+
+msgid "AlertManagement|End time:"
+msgstr ""
+
+msgid "AlertManagement|Events"
+msgstr ""
+
+msgid "AlertManagement|Events:"
+msgstr ""
+
+msgid "AlertManagement|Full Alert Details"
+msgstr ""
+
+msgid "AlertManagement|More information"
+msgstr ""
+
+msgid "AlertManagement|No alerts available to display. If you think you're seeing this message in error, refresh the page."
+msgstr ""
+
+msgid "AlertManagement|No alerts to display."
+msgstr ""
+
+msgid "AlertManagement|Overview"
+msgstr ""
+
+msgid "AlertManagement|Resolved"
+msgstr ""
+
+msgid "AlertManagement|Severity"
+msgstr ""
+
+msgid "AlertManagement|Start time"
+msgstr ""
+
+msgid "AlertManagement|Start time:"
+msgstr ""
+
+msgid "AlertManagement|Status"
+msgstr ""
+
+msgid "AlertManagement|Surface alerts in GitLab"
+msgstr ""
+
+msgid "AlertManagement|There was an error displaying the alerts. Confirm your endpoint's configuration details to ensure alerts appear."
+msgstr ""
+
+msgid "AlertManagement|Triggered"
+msgstr ""
+
msgid "AlertService|%{linkStart}Learn more%{linkEnd} about configuring this endpoint to receive alerts."
msgstr ""
@@ -1727,6 +1830,9 @@ msgstr ""
msgid "All security scans are enabled because %{linkStart}Auto DevOps%{linkEnd} is enabled on this project"
msgstr ""
+msgid "All threads resolved"
+msgstr ""
+
msgid "All users"
msgstr ""
@@ -1745,6 +1851,9 @@ msgstr ""
msgid "Allow only the selected protocols to be used for Git access."
msgstr ""
+msgid "Allow owners to manage default branch protection per group"
+msgstr ""
+
msgid "Allow owners to manually add users outside of LDAP"
msgstr ""
@@ -1853,6 +1962,9 @@ msgstr ""
msgid "An error occurred fetching the dropdown data."
msgstr ""
+msgid "An error occurred fetching the project authors."
+msgstr ""
+
msgid "An error occurred previewing the blob"
msgstr ""
@@ -1934,6 +2046,9 @@ msgstr ""
msgid "An error occurred while fetching sidebar data"
msgstr ""
+msgid "An error occurred while fetching terraform reports."
+msgstr ""
+
msgid "An error occurred while fetching the Service Desk address."
msgstr ""
@@ -2171,6 +2286,9 @@ msgstr ""
msgid "Any"
msgstr ""
+msgid "Any Author"
+msgstr ""
+
msgid "Any Label"
msgstr ""
@@ -2386,10 +2504,10 @@ msgstr ""
msgid "Are you sure that you want to unarchive this project?"
msgstr ""
-msgid "Are you sure you want to cancel creating this comment?"
+msgid "Are you sure you want to cancel editing this comment?"
msgstr ""
-msgid "Are you sure you want to cancel editing this comment?"
+msgid "Are you sure you want to close this blocked issue?"
msgstr ""
msgid "Are you sure you want to delete %{name}?"
@@ -2419,6 +2537,9 @@ msgstr ""
msgid "Are you sure you want to deploy this environment?"
msgstr ""
+msgid "Are you sure you want to discard this comment?"
+msgstr ""
+
msgid "Are you sure you want to erase this build?"
msgstr ""
@@ -2595,6 +2716,9 @@ msgstr ""
msgid "At least one approval from a code owner is required to change files matching the respective CODEOWNER rules."
msgstr ""
+msgid "At least one logging option is required to be enabled"
+msgstr ""
+
msgid "At least one of group_id or project_id must be specified"
msgstr ""
@@ -2633,6 +2757,27 @@ msgstr ""
msgid "AuditEvents|Target"
msgstr ""
+msgid "AuditLogs|(removed)"
+msgstr ""
+
+msgid "AuditLogs|Action"
+msgstr ""
+
+msgid "AuditLogs|Author"
+msgstr ""
+
+msgid "AuditLogs|Date"
+msgstr ""
+
+msgid "AuditLogs|IP Address"
+msgstr ""
+
+msgid "AuditLogs|Object"
+msgstr ""
+
+msgid "AuditLogs|Target"
+msgstr ""
+
msgid "Aug"
msgstr ""
@@ -2915,9 +3060,6 @@ msgstr ""
msgid "BambooService|You must set up automatic revision labeling and a repository trigger in Bamboo."
msgstr ""
-msgid "Batch operations"
-msgstr ""
-
msgid "BatchComments|Delete all pending comments"
msgstr ""
@@ -3047,6 +3189,9 @@ msgstr ""
msgid "Boards"
msgstr ""
+msgid "Boards and Board Lists"
+msgstr ""
+
msgid "Boards|Collapse"
msgstr ""
@@ -3266,6 +3411,9 @@ msgstr ""
msgid "Buy GitLab Enterprise Edition"
msgstr ""
+msgid "Buy more Pipeline minutes"
+msgstr ""
+
msgid "By %{user_name}"
msgstr ""
@@ -3386,6 +3534,9 @@ msgstr ""
msgid "Can't scan the code?"
msgstr ""
+msgid "Can't update snippet: %{err}"
+msgstr ""
+
msgid "Canary"
msgstr ""
@@ -3554,6 +3705,9 @@ msgstr ""
msgid "Changing group path can have unintended side effects."
msgstr ""
+msgid "Charts"
+msgstr ""
+
msgid "Charts can't be displayed as the request for data has timed out. %{documentationLink}"
msgstr ""
@@ -3590,9 +3744,6 @@ msgstr ""
msgid "ChatMessage|and [%{count} more](%{pipeline_failed_jobs_url})"
msgstr ""
-msgid "ChatMessage|failed"
-msgstr ""
-
msgid "ChatMessage|has failed"
msgstr ""
@@ -3608,9 +3759,6 @@ msgstr ""
msgid "ChatMessage|in %{project_link}"
msgstr ""
-msgid "ChatMessage|passed"
-msgstr ""
-
msgid "Check again"
msgstr ""
@@ -4061,9 +4209,6 @@ msgstr ""
msgid "Click any <strong>project name</strong> in the project list below to navigate to the project milestone."
msgstr ""
-msgid "Click here"
-msgstr ""
-
msgid "Click the <strong>Download</strong> button and wait for downloading to complete."
msgstr ""
@@ -4451,6 +4596,12 @@ msgstr ""
msgid "ClusterIntegration|Fetching zones"
msgstr ""
+msgid "ClusterIntegration|Fluentd"
+msgstr ""
+
+msgid "ClusterIntegration|Fluentd is an open source data collector, which lets you unify the data collection and consumption for a better use and understanding of data. It requires at least one of the following logs to be successfully installed."
+msgstr ""
+
msgid "ClusterIntegration|GitLab Integration"
msgstr ""
@@ -4757,6 +4908,15 @@ msgstr ""
msgid "ClusterIntegration|Request to begin uninstalling failed"
msgstr ""
+msgid "ClusterIntegration|SIEM Hostname"
+msgstr ""
+
+msgid "ClusterIntegration|SIEM Port"
+msgstr ""
+
+msgid "ClusterIntegration|SIEM Protocol"
+msgstr ""
+
msgid "ClusterIntegration|Save changes"
msgstr ""
@@ -4853,6 +5013,12 @@ msgstr ""
msgid "ClusterIntegration|Select zone to choose machine type"
msgstr ""
+msgid "ClusterIntegration|Send Cilium Logs"
+msgstr ""
+
+msgid "ClusterIntegration|Send ModSecurity Logs"
+msgstr ""
+
msgid "ClusterIntegration|Service Token"
msgstr ""
@@ -5281,18 +5447,33 @@ msgstr ""
msgid "Compliance framework (optional)"
msgstr ""
+msgid "ComplianceFramework|GDPR"
+msgstr ""
+
msgid "ComplianceFramework|GDPR - General Data Protection Regulation"
msgstr ""
+msgid "ComplianceFramework|HIPAA"
+msgstr ""
+
msgid "ComplianceFramework|HIPAA - Health Insurance Portability and Accountability Act"
msgstr ""
+msgid "ComplianceFramework|PCI-DSS"
+msgstr ""
+
msgid "ComplianceFramework|PCI-DSS - Payment Card Industry-Data Security Standard"
msgstr ""
+msgid "ComplianceFramework|SOC 2"
+msgstr ""
+
msgid "ComplianceFramework|SOC 2 - Service Organization Control 2"
msgstr ""
+msgid "ComplianceFramework|SOX"
+msgstr ""
+
msgid "ComplianceFramework|SOX - Sarbanes-Oxley"
msgstr ""
@@ -5440,9 +5621,15 @@ msgstr ""
msgid "Container repositories sync capacity"
msgstr ""
+msgid "ContainerRegistry| Please visit the %{linkStart}administration settings%{linkEnd} to enable this feature."
+msgstr ""
+
msgid "ContainerRegistry|%{imageName} tags"
msgstr ""
+msgid "ContainerRegistry|%{title} was successfully scheduled for deletion"
+msgstr ""
+
msgid "ContainerRegistry|Automatically remove extra images that aren't designed to be kept."
msgstr ""
@@ -5455,6 +5642,9 @@ msgstr ""
msgid "ContainerRegistry|Container Registry"
msgstr ""
+msgid "ContainerRegistry|Container Registry tag expiration and retention policy is disabled"
+msgstr ""
+
msgid "ContainerRegistry|Copy build command"
msgstr ""
@@ -5464,18 +5654,12 @@ msgstr ""
msgid "ContainerRegistry|Copy push command"
msgstr ""
-msgid "ContainerRegistry|Currently, the Container Registry tag expiration feature is not available for projects created before GitLab version 12.8. For updates and more information, visit Issue %{linkStart}#196124%{linkEnd}"
-msgstr ""
-
msgid "ContainerRegistry|Docker connection error"
msgstr ""
msgid "ContainerRegistry|Docker tag expiration policy is %{toggleStatus}"
msgstr ""
-msgid "ContainerRegistry|Docker tags with names matching this regex pattern will expire:"
-msgstr ""
-
msgid "ContainerRegistry|Edit Settings"
msgstr ""
@@ -5497,9 +5681,6 @@ msgstr ""
msgid "ContainerRegistry|Image ID"
msgstr ""
-msgid "ContainerRegistry|Image deleted successfully"
-msgstr ""
-
msgid "ContainerRegistry|Keep and protect the images that matter most."
msgstr ""
@@ -5515,12 +5696,24 @@ msgstr ""
msgid "ContainerRegistry|Number of tags to retain:"
msgstr ""
+msgid "ContainerRegistry|Please contact your administrator."
+msgstr ""
+
msgid "ContainerRegistry|Push an image"
msgstr ""
msgid "ContainerRegistry|Quick Start"
msgstr ""
+msgid "ContainerRegistry|Regular expressions such as %{codeStart}.*-test%{codeEnd} or %{codeStart}dev-.*%{codeEnd} are supported"
+msgstr ""
+
+msgid "ContainerRegistry|Regular expressions such as %{codeStart}.*-test%{codeEnd} or %{codeStart}dev-.*%{codeEnd} are supported. To select all tags, use %{codeStart}.*%{codeEnd}"
+msgstr ""
+
+msgid "ContainerRegistry|Remember to run %{docLinkStart}garbage collection%{docLinkEnd} to remove the stale data from storage."
+msgstr ""
+
msgid "ContainerRegistry|Remove repository"
msgstr ""
@@ -5535,22 +5728,22 @@ msgstr[1] ""
msgid "ContainerRegistry|Retention policy has been Enabled"
msgstr ""
-msgid "ContainerRegistry|Something went wrong while deleting the image."
+msgid "ContainerRegistry|Something went wrong while fetching the expiration policy."
msgstr ""
-msgid "ContainerRegistry|Something went wrong while deleting the tag."
+msgid "ContainerRegistry|Something went wrong while fetching the repository list."
msgstr ""
-msgid "ContainerRegistry|Something went wrong while deleting the tags."
+msgid "ContainerRegistry|Something went wrong while fetching the tags list."
msgstr ""
-msgid "ContainerRegistry|Something went wrong while fetching the expiration policy."
+msgid "ContainerRegistry|Something went wrong while marking the tag for deletion."
msgstr ""
-msgid "ContainerRegistry|Something went wrong while fetching the packages list."
+msgid "ContainerRegistry|Something went wrong while marking the tags for deletion."
msgstr ""
-msgid "ContainerRegistry|Something went wrong while fetching the tags list."
+msgid "ContainerRegistry|Something went wrong while scheduling %{title} for deletion. Please try again."
msgstr ""
msgid "ContainerRegistry|Something went wrong while updating the expiration policy."
@@ -5559,16 +5752,25 @@ msgstr ""
msgid "ContainerRegistry|Tag"
msgstr ""
-msgid "ContainerRegistry|Tag deleted successfully"
-msgstr ""
-
msgid "ContainerRegistry|Tag expiration policy"
msgstr ""
msgid "ContainerRegistry|Tag expiration policy is designed to:"
msgstr ""
-msgid "ContainerRegistry|Tags deleted successfully"
+msgid "ContainerRegistry|Tag successfully marked for deletion."
+msgstr ""
+
+msgid "ContainerRegistry|Tags successfully marked for deletion."
+msgstr ""
+
+msgid "ContainerRegistry|Tags with names matching this regex pattern will %{italicStart}be preserved:%{italicEnd}"
+msgstr ""
+
+msgid "ContainerRegistry|Tags with names matching this regex pattern will %{italicStart}expire:%{italicEnd}"
+msgstr ""
+
+msgid "ContainerRegistry|The Container Registry tag expiration and retention policies for this project have not been enabled."
msgstr ""
msgid "ContainerRegistry|The last tag related to this image was recently removed. This empty image and any associated data will be automatically removed as part of the regular Garbage Collection process. If you have any questions, contact your administrator."
@@ -5589,16 +5791,16 @@ msgstr ""
msgid "ContainerRegistry|There are no container images stored for this project"
msgstr ""
-msgid "ContainerRegistry|This Registry contains deleted image tag data. Remember to run %{docLinkStart}garbage collection%{docLinkEnd} to remove the stale data from storage."
+msgid "ContainerRegistry|There was an error during the deletion of this image repository, please try again."
msgstr ""
msgid "ContainerRegistry|This image has no active tags"
msgstr ""
-msgid "ContainerRegistry|We are having trouble connecting to Docker, which could be due to an issue with your project name or path. %{docLinkStart}More Information%{docLinkEnd}"
+msgid "ContainerRegistry|This image repository is scheduled for deletion"
msgstr ""
-msgid "ContainerRegistry|Wildcards such as %{codeStart}.*-stable%{codeEnd} or %{codeStart}production/.*%{codeEnd} are supported. To select all tags, use %{codeStart}.*%{codeEnd}"
+msgid "ContainerRegistry|We are having trouble connecting to Docker, which could be due to an issue with your project name or path. %{docLinkStart}More Information%{docLinkEnd}"
msgstr ""
msgid "ContainerRegistry|With the Container Registry, every project can have its own space to store its Docker images. %{docLinkStart}More Information%{docLinkEnd}"
@@ -5853,7 +6055,7 @@ msgstr ""
msgid "Could not delete chat nickname %{chat_name}."
msgstr ""
-msgid "Could not find design"
+msgid "Could not find design."
msgstr ""
msgid "Could not remove the trigger."
@@ -5868,6 +6070,9 @@ msgstr ""
msgid "Could not revoke personal access token %{personal_access_token_name}."
msgstr ""
+msgid "Could not revoke project access token %{project_access_token_name}."
+msgstr ""
+
msgid "Could not save group ID"
msgstr ""
@@ -5925,6 +6130,9 @@ msgstr ""
msgid "Create a new branch"
msgstr ""
+msgid "Create a new deploy key for this project"
+msgstr ""
+
msgid "Create a new file as there are no files yet. Afterwards, you'll be able to commit your changes."
msgstr ""
@@ -6030,6 +6238,9 @@ msgstr ""
msgid "Create requirement"
msgstr ""
+msgid "Create snippet"
+msgstr ""
+
msgid "Create wildcard: %{searchTerm}"
msgstr ""
@@ -6051,6 +6262,9 @@ msgstr ""
msgid "Created"
msgstr ""
+msgid "Created %{timestamp}"
+msgstr ""
+
msgid "Created At"
msgstr ""
@@ -6407,6 +6621,9 @@ msgstr ""
msgid "CycleAnalytics|stage dropdown"
msgstr ""
+msgid "DAG"
+msgstr ""
+
msgid "DNS"
msgstr ""
@@ -6773,6 +6990,9 @@ msgstr ""
msgid "Deploy key was successfully updated."
msgstr ""
+msgid "Deploy keys allow read-only or read-write (if enabled) access to your repository. Deploy keys can be used for CI, staging or production servers. You can create a deploy key or add an existing one."
+msgstr ""
+
msgid "Deploy progress not found. To see pods, ensure your environment matches %{linkStart}deploy board criteria%{linkEnd}."
msgstr ""
@@ -6926,6 +7146,9 @@ msgstr ""
msgid "Deploying to"
msgstr ""
+msgid "Deployment Frequency"
+msgstr ""
+
msgid "Deployment|API"
msgstr ""
@@ -7444,9 +7667,6 @@ msgstr ""
msgid "Edit environment"
msgstr ""
-msgid "Edit epic description"
-msgstr ""
-
msgid "Edit file"
msgstr ""
@@ -8068,9 +8288,6 @@ msgstr ""
msgid "Epics"
msgstr ""
-msgid "Epics (Ultimate / Gold license only)"
-msgstr ""
-
msgid "Epics Roadmap"
msgstr ""
@@ -8080,6 +8297,9 @@ msgstr ""
msgid "Epics let you manage your portfolio of projects more efficiently and with less effort"
msgstr ""
+msgid "Epics, Issues, and Merge Requests"
+msgstr ""
+
msgid "Epics|Add an epic"
msgstr ""
@@ -8131,6 +8351,9 @@ msgstr ""
msgid "Epics|Something went wrong while fetching group epics."
msgstr ""
+msgid "Epics|Something went wrong while moving item."
+msgstr ""
+
msgid "Epics|Something went wrong while ordering item."
msgstr ""
@@ -8185,6 +8408,9 @@ msgstr ""
msgid "Error fetching network graph."
msgstr ""
+msgid "Error fetching payload data."
+msgstr ""
+
msgid "Error fetching projects"
msgstr ""
@@ -8194,9 +8420,6 @@ msgstr ""
msgid "Error fetching the dependency list. Please check your network connection and try again."
msgstr ""
-msgid "Error fetching usage ping data."
-msgstr ""
-
msgid "Error loading branch data. Please try again."
msgstr ""
@@ -8338,6 +8561,9 @@ msgstr ""
msgid "Errors"
msgstr ""
+msgid "Errors:"
+msgstr ""
+
msgid "Estimated"
msgstr ""
@@ -8542,12 +8768,18 @@ msgstr ""
msgid "Export as CSV"
msgstr ""
+msgid "Export group"
+msgstr ""
+
msgid "Export issues"
msgstr ""
msgid "Export project"
msgstr ""
+msgid "Export this group with all related data to a new GitLab instance. Once complete, you can import the data file from the \"New Group\" page."
+msgstr ""
+
msgid "Export this project with all its related data in order to move your project to a new GitLab instance. Once the export is finished, you can import the file from the \"New Project\" page."
msgstr ""
@@ -8617,9 +8849,6 @@ msgstr ""
msgid "Failed Jobs"
msgstr ""
-msgid "Failed create wiki"
-msgstr ""
-
msgid "Failed to add a Zoom meeting"
msgstr ""
@@ -8659,6 +8888,9 @@ msgstr ""
msgid "Failed to create resources"
msgstr ""
+msgid "Failed to create wiki"
+msgstr ""
+
msgid "Failed to delete board. Please try again."
msgstr ""
@@ -8668,7 +8900,7 @@ msgstr ""
msgid "Failed to enqueue the rebase operation, possibly due to a long-lived transaction. Try again later."
msgstr ""
-msgid "Failed to find import label for jira import."
+msgid "Failed to find import label for Jira import."
msgstr ""
msgid "Failed to get ref."
@@ -8692,6 +8924,12 @@ msgstr ""
msgid "Failed to load groups & users."
msgstr ""
+msgid "Failed to load labels. Please try again."
+msgstr ""
+
+msgid "Failed to load milestones. Please try again."
+msgstr ""
+
msgid "Failed to load related branches"
msgstr ""
@@ -9067,7 +9305,10 @@ msgstr ""
msgid "Filter by milestone name"
msgstr ""
-msgid "Filter by name..."
+msgid "Filter by name"
+msgstr ""
+
+msgid "Filter by status"
msgstr ""
msgid "Filter by two-factor authentication"
@@ -9079,12 +9320,18 @@ msgstr ""
msgid "Filter projects"
msgstr ""
+msgid "Filter results"
+msgstr ""
+
msgid "Filter results by group"
msgstr ""
msgid "Filter results by project"
msgstr ""
+msgid "Filter results..."
+msgstr ""
+
msgid "Filter your projects by name"
msgstr ""
@@ -9373,10 +9620,10 @@ msgstr ""
msgid "GeoNodes|Checksummed"
msgstr ""
-msgid "GeoNodes|Container repositories"
+msgid "GeoNodes|Consult Geo troubleshooting information"
msgstr ""
-msgid "GeoNodes|Data is out of date from %{timeago}"
+msgid "GeoNodes|Container repositories"
msgstr ""
msgid "GeoNodes|Data replication lag"
@@ -9418,6 +9665,9 @@ msgstr ""
msgid "GeoNodes|Last event ID seen from primary"
msgstr ""
+msgid "GeoNodes|Learn more about Geo node statuses"
+msgstr ""
+
msgid "GeoNodes|Loading nodes"
msgstr ""
@@ -9433,6 +9683,9 @@ msgstr ""
msgid "GeoNodes|Node was successfully removed."
msgstr ""
+msgid "GeoNodes|Node's status was updated %{timeAgo}."
+msgstr ""
+
msgid "GeoNodes|Not checksummed"
msgstr ""
@@ -9463,7 +9716,10 @@ msgstr ""
msgid "GeoNodes|Repository verification progress"
msgstr ""
-msgid "GeoNodes|Selective"
+msgid "GeoNodes|Selective (%{syncLabel})"
+msgstr ""
+
+msgid "GeoNodes|Selective synchronization"
msgstr ""
msgid "GeoNodes|Something went wrong while changing node status"
@@ -9490,6 +9746,9 @@ msgstr ""
msgid "GeoNodes|Unverified"
msgstr ""
+msgid "GeoNodes|Updated %{timeAgo}"
+msgstr ""
+
msgid "GeoNodes|Used slots"
msgstr ""
@@ -9529,16 +9788,16 @@ msgstr ""
msgid "Geo|All"
msgstr ""
-msgid "Geo|All projects"
+msgid "Geo|All %{replicable_type}"
msgstr ""
-msgid "Geo|All projects are being scheduled for re-sync"
+msgid "Geo|All projects"
msgstr ""
-msgid "Geo|All projects are being scheduled for re-verify"
+msgid "Geo|All projects are being scheduled for resync"
msgstr ""
-msgid "Geo|Batch operations"
+msgid "Geo|All projects are being scheduled for reverify"
msgstr ""
msgid "Geo|Could not remove tracking entry for an existing project."
@@ -9550,9 +9809,15 @@ msgstr ""
msgid "Geo|Failed"
msgstr ""
+msgid "Geo|Filter by status"
+msgstr ""
+
msgid "Geo|Geo Status"
msgstr ""
+msgid "Geo|In progress"
+msgstr ""
+
msgid "Geo|In sync"
msgstr ""
@@ -9577,9 +9842,6 @@ msgstr ""
msgid "Geo|Not synced yet"
msgstr ""
-msgid "Geo|Pending"
-msgstr ""
-
msgid "Geo|Pending synchronization"
msgstr ""
@@ -9616,7 +9878,7 @@ msgstr ""
msgid "Geo|Resync"
msgstr ""
-msgid "Geo|Resync all projects"
+msgid "Geo|Resync all"
msgstr ""
msgid "Geo|Retry count"
@@ -9625,7 +9887,7 @@ msgstr ""
msgid "Geo|Reverify"
msgstr ""
-msgid "Geo|Reverify all projects"
+msgid "Geo|Reverify all"
msgstr ""
msgid "Geo|Status"
@@ -9967,9 +10229,6 @@ msgstr ""
msgid "Go to file"
msgstr ""
-msgid "Go to file (MRs only)"
-msgstr ""
-
msgid "Go to file permalink (while viewing a file)"
msgstr ""
@@ -10414,6 +10673,9 @@ msgstr ""
msgid "GroupSettings|Disable group mentions"
msgstr ""
+msgid "GroupSettings|Export group"
+msgstr ""
+
msgid "GroupSettings|If the parent group's visibility is lower than the group current visibility, visibility levels for subgroups and projects will be changed to match the new parent group's visibility."
msgstr ""
@@ -10492,6 +10754,9 @@ msgstr ""
msgid "Groups (%{count})"
msgstr ""
+msgid "Groups (%{groups})"
+msgstr ""
+
msgid "Groups can also be nested by creating %{subgroup_docs_link_start}subgroups%{subgroup_docs_link_end}."
msgstr ""
@@ -10757,15 +11022,12 @@ msgstr ""
msgid "ID:"
msgstr ""
-msgid "IDE|Allow live previews of JavaScript projects in the Web IDE using CodeSandbox client side evaluation."
+msgid "IDE|Allow live previews of JavaScript projects in the Web IDE using CodeSandbox Live Preview."
msgstr ""
msgid "IDE|Back"
msgstr ""
-msgid "IDE|Client side evaluation"
-msgstr ""
-
msgid "IDE|Commit"
msgstr ""
@@ -10829,6 +11091,9 @@ msgstr ""
msgid "If any job surpasses this timeout threshold, it will be marked as failed. Human readable time input language is accepted like \"1 hour\". Values without specification represent seconds."
msgstr ""
+msgid "If blank, set allowable lifetime to %{instance_level_policy_in_words}, as defined by the instance admin. Once set, existing tokens for users in this group may be revoked."
+msgstr ""
+
msgid "If checked, group owners can manage LDAP group links and LDAP member overrides"
msgstr ""
@@ -10868,6 +11133,9 @@ msgstr ""
msgid "If you lose your recovery codes you can generate new ones, invalidating all previous codes."
msgstr ""
+msgid "If you reach 100%% storage capacity, you will not be able to: %{base_message}"
+msgstr ""
+
msgid "If your HTTP repository is not publicly accessible, add your credentials."
msgstr ""
@@ -10979,6 +11247,9 @@ msgstr ""
msgid "Import repository"
msgstr ""
+msgid "Import started by: %{importInitiator}"
+msgstr ""
+
msgid "Import tasks"
msgstr ""
@@ -11045,6 +11316,9 @@ msgstr ""
msgid "In order to tailor your experience with GitLab we<br>would like to know a bit more about you."
msgstr ""
+msgid "In progress"
+msgstr ""
+
msgid "In the next step, you'll be able to select the projects you want to import."
msgstr ""
@@ -11392,15 +11666,15 @@ msgstr ""
msgid "Issues"
msgstr ""
-msgid "Issues / Merge Requests"
-msgstr ""
-
msgid "Issues Analytics"
msgstr ""
msgid "Issues Rate Limits"
msgstr ""
+msgid "Issues and Merge Requests"
+msgstr ""
+
msgid "Issues can be bugs, tasks or ideas to be discussed. Also, issues are searchable and filterable."
msgstr ""
@@ -11422,10 +11696,10 @@ msgstr ""
msgid "IssuesAnalytics|Avg/Month:"
msgstr ""
-msgid "IssuesAnalytics|Issues created"
+msgid "IssuesAnalytics|Issues opened"
msgstr ""
-msgid "IssuesAnalytics|Issues created per month"
+msgid "IssuesAnalytics|Issues opened per month"
msgstr ""
msgid "IssuesAnalytics|Last 12 months"
@@ -11479,6 +11753,9 @@ msgstr ""
msgid "Jira integration not configured."
msgstr ""
+msgid "Jira project: %{importProject}"
+msgstr ""
+
msgid "JiraService|Events for %{noteable_model_name} are disabled."
msgstr ""
@@ -11868,6 +12145,9 @@ msgstr ""
msgid "Lead"
msgstr ""
+msgid "Lead Time"
+msgstr ""
+
msgid "Learn GitLab"
msgstr ""
@@ -11880,6 +12160,9 @@ msgstr ""
msgid "Learn how to %{no_packages_link_start}publish and share your packages%{no_packages_link_end} with GitLab."
msgstr ""
+msgid "Learn how to enable synchronization"
+msgstr ""
+
msgid "Learn more"
msgstr ""
@@ -12211,9 +12494,6 @@ msgstr ""
msgid "Live preview"
msgstr ""
-msgid "Load more vulnerabilities"
-msgstr ""
-
msgid "Loading"
msgstr ""
@@ -12364,9 +12644,6 @@ msgstr ""
msgid "Manage"
msgstr ""
-msgid "Manage Git repositories with fine-grained access controls that keep your code secure. Perform code reviews and enhance collaboration with merge requests. Each project can also have an issue tracker and a wiki."
-msgstr ""
-
msgid "Manage Web IDE features"
msgstr ""
@@ -12397,6 +12674,9 @@ msgstr ""
msgid "Manage your license"
msgstr ""
+msgid "Managed Account"
+msgstr ""
+
msgid "Manifest"
msgstr ""
@@ -12661,6 +12941,9 @@ msgstr ""
msgid "Merge Requests in Review"
msgstr ""
+msgid "Merge automatically (%{strategy})"
+msgstr ""
+
msgid "Merge commit message"
msgstr ""
@@ -12814,6 +13097,12 @@ msgstr ""
msgid "Merged branches are being deleted. This can take some time depending on the number of branches. Please refresh the page to see changes."
msgstr ""
+msgid "Merged this merge request."
+msgstr ""
+
+msgid "Merges this merge request immediately."
+msgstr ""
+
msgid "Merges this merge request when the pipeline succeeds."
msgstr ""
@@ -12921,12 +13210,15 @@ msgid_plural "Metrics|Edit metrics"
msgstr[0] ""
msgstr[1] ""
-msgid "Metrics|Environment"
+msgid "Metrics|Expand panel"
msgstr ""
msgid "Metrics|For grouping similar metrics"
msgstr ""
+msgid "Metrics|Go back (Esc)"
+msgstr ""
+
msgid "Metrics|Invalid time range, please verify."
msgstr ""
@@ -12957,9 +13249,6 @@ msgstr ""
msgid "Metrics|Refresh dashboard"
msgstr ""
-msgid "Metrics|Show last"
-msgstr ""
-
msgid "Metrics|There was an error creating the dashboard."
msgstr ""
@@ -13038,6 +13327,9 @@ msgstr ""
msgid "Microsoft Azure"
msgstr ""
+msgid "Middleman project with Static Site Editor support"
+msgstr ""
+
msgid "Migrated %{success_count}/%{total_count} files."
msgstr ""
@@ -13483,10 +13775,10 @@ msgstr ""
msgid "Next"
msgstr ""
-msgid "Next file in diff (MRs only)"
+msgid "Next file in diff"
msgstr ""
-msgid "Next unresolved discussion (MRs only)"
+msgid "Next unresolved discussion"
msgstr ""
msgid "Nickname"
@@ -13546,6 +13838,9 @@ msgstr ""
msgid "No changes between %{ref_start}%{source_branch}%{ref_end} and %{ref_start}%{target_branch}%{ref_end}"
msgstr ""
+msgid "No child epics match applied filters"
+msgstr ""
+
msgid "No connection could be made to a Gitaly Server, please check your logs!"
msgstr ""
@@ -13684,15 +13979,6 @@ msgstr ""
msgid "No thanks, don't show this again"
msgstr ""
-msgid "No vulnerabilities found for this group"
-msgstr ""
-
-msgid "No vulnerabilities found for this pipeline"
-msgstr ""
-
-msgid "No vulnerabilities found for this project"
-msgstr ""
-
msgid "No vulnerabilities present"
msgstr ""
@@ -13816,6 +14102,9 @@ msgstr ""
msgid "Nothing to preview."
msgstr ""
+msgid "Nothing to synchronize"
+msgstr ""
+
msgid "Notification events"
msgstr ""
@@ -13987,6 +14276,9 @@ msgstr ""
msgid "Once removed, the fork relationship cannot be restored and you will no longer be able to send merge requests to the source."
msgstr ""
+msgid "Once the exported file is ready you can download it from this page."
+msgstr ""
+
msgid "Once the exported file is ready, you will receive a notification email with a download link, or you can download it from this page."
msgstr ""
@@ -14088,9 +14380,6 @@ msgstr ""
msgid "Open sidebar"
msgstr ""
-msgid "Open source software to collaborate on code"
-msgstr ""
-
msgid "Open: %{openIssuesCount}"
msgstr ""
@@ -14193,6 +14482,9 @@ msgstr ""
msgid "OutdatedBrowser|You can provide feedback %{feedback_link_start}on this issue%{feedback_link_end} or via your usual support channels."
msgstr ""
+msgid "Overridden"
+msgstr ""
+
msgid "Overview"
msgstr ""
@@ -14244,6 +14536,9 @@ msgstr ""
msgid "PackageRegistry|Conan Command"
msgstr ""
+msgid "PackageRegistry|Copy .pypirc content"
+msgstr ""
+
msgid "PackageRegistry|Copy Conan Command"
msgstr ""
@@ -14265,6 +14560,9 @@ msgstr ""
msgid "PackageRegistry|Copy NuGet Setup Command"
msgstr ""
+msgid "PackageRegistry|Copy Pip command"
+msgstr ""
+
msgid "PackageRegistry|Copy and paste this inside your %{codeStart}pom.xml%{codeEnd} %{codeStart}dependencies%{codeEnd} block."
msgstr ""
@@ -14298,6 +14596,12 @@ msgstr ""
msgid "PackageRegistry|For more information on the NuGet registry, %{linkStart}see the documentation%{linkEnd}."
msgstr ""
+msgid "PackageRegistry|For more information on the PyPi registry, %{linkStart}see the documentation%{linkEnd}."
+msgstr ""
+
+msgid "PackageRegistry|If you haven't already done so, you will need to add the below to your %{codeStart}.pypirc%{codeEnd} file."
+msgstr ""
+
msgid "PackageRegistry|If you haven't already done so, you will need to add the below to your %{codeStart}pom.xml%{codeEnd} file."
msgstr ""
@@ -14307,6 +14611,9 @@ msgstr ""
msgid "PackageRegistry|Learn how to %{noPackagesLinkStart}publish and share your packages%{noPackagesLinkEnd} with GitLab."
msgstr ""
+msgid "PackageRegistry|Manually Published"
+msgstr ""
+
msgid "PackageRegistry|Maven"
msgstr ""
@@ -14325,12 +14632,18 @@ msgstr ""
msgid "PackageRegistry|NuGet Command"
msgstr ""
+msgid "PackageRegistry|Pip Command"
+msgstr ""
+
msgid "PackageRegistry|Pipeline %{linkStart}%{linkEnd} triggered %{timestamp} by %{author}"
msgstr ""
msgid "PackageRegistry|Published to the repository at %{timestamp}"
msgstr ""
+msgid "PackageRegistry|PyPi"
+msgstr ""
+
msgid "PackageRegistry|Registry Setup"
msgstr ""
@@ -14367,6 +14680,9 @@ msgstr ""
msgid "PackageRegistry|npm"
msgstr ""
+msgid "PackageRegistry|published by %{author}"
+msgstr ""
+
msgid "PackageRegistry|yarn"
msgstr ""
@@ -14382,6 +14698,9 @@ msgstr ""
msgid "PackageType|NuGet"
msgstr ""
+msgid "PackageType|PyPi"
+msgstr ""
+
msgid "Packages"
msgstr ""
@@ -14475,6 +14794,9 @@ msgstr ""
msgid "Past due"
msgstr ""
+msgid "Paste a machine public key here. Read more about how to generate it"
+msgstr ""
+
msgid "Paste a machine public key here. Read more about how to generate it %{link_start}here%{link_end}"
msgstr ""
@@ -14490,9 +14812,6 @@ msgstr ""
msgid "Path"
msgstr ""
-msgid "Path, transfer, remove"
-msgstr ""
-
msgid "Path:"
msgstr ""
@@ -14523,7 +14842,7 @@ msgstr ""
msgid "Percentage"
msgstr ""
-msgid "Perform advanced options such as changing path, transferring, or removing the group."
+msgid "Perform advanced options such as changing path, transferring, exporting, or removing the group."
msgstr ""
msgid "Perform common operations on GitLab project"
@@ -14604,9 +14923,6 @@ msgstr ""
msgid "Pipeline minutes quota"
msgstr ""
-msgid "Pipeline quota"
-msgstr ""
-
msgid "Pipeline subscriptions"
msgstr ""
@@ -14616,6 +14932,9 @@ msgstr ""
msgid "Pipeline: %{status}"
msgstr ""
+msgid "PipelineCharts|CI / CD Analytics"
+msgstr ""
+
msgid "PipelineCharts|Failed:"
msgstr ""
@@ -14694,15 +15013,6 @@ msgstr ""
msgid "Pipelines settings for '%{project_name}' were successfully updated."
msgstr ""
-msgid "Pipelines| to purchase more minutes."
-msgstr ""
-
-msgid "Pipelines|%{namespace_name} has exceeded its pipeline minutes quota."
-msgstr ""
-
-msgid "Pipelines|%{namespace_name} has less than %{notification_level}%% of CI minutes available."
-msgstr ""
-
msgid "Pipelines|API"
msgstr ""
@@ -14724,10 +15034,13 @@ msgstr ""
msgid "Pipelines|Get started with Pipelines"
msgstr ""
-msgid "Pipelines|Loading Pipelines"
+msgid "Pipelines|Group %{namespace_name} has %{percentage}%% or less Shared Runner Pipeline minutes remaining. Once it runs out, no new jobs or pipelines in its projects will run."
msgstr ""
-msgid "Pipelines|Pipelines will not run anymore on shared Runners."
+msgid "Pipelines|Group %{namespace_name} has exceeded its pipeline minutes quota. Unless you buy additional pipeline minutes, no new jobs or pipelines in its projects will run."
+msgstr ""
+
+msgid "Pipelines|Loading Pipelines"
msgstr ""
msgid "Pipelines|Project cache successfully reset."
@@ -14958,6 +15271,9 @@ msgstr ""
msgid "Please select"
msgstr ""
+msgid "Please select a Jira project"
+msgstr ""
+
msgid "Please select a country"
msgstr ""
@@ -15003,6 +15319,9 @@ msgstr ""
msgid "Please wait while we import the repository for you. Refresh at will."
msgstr ""
+msgid "Plugins directory is deprecated and will be removed in 14.0. Please move this file into /file_hooks directory."
+msgstr ""
+
msgid "Pod does not exist"
msgstr ""
@@ -15120,7 +15439,7 @@ msgstr ""
msgid "Prevent users from changing their profile name"
msgstr ""
-msgid "Prevent users from modifing merge request approvers list"
+msgid "Prevent users from modifying merge request approvers list"
msgstr ""
msgid "Prevent users from performing write operations on GitLab while performing maintenance."
@@ -15141,10 +15460,10 @@ msgstr ""
msgid "Previous Artifacts"
msgstr ""
-msgid "Previous file in diff (MRs only)"
+msgid "Previous file in diff"
msgstr ""
-msgid "Previous unresolved discussion (MRs only)"
+msgid "Previous unresolved discussion"
msgstr ""
msgid "Primary"
@@ -15177,6 +15496,9 @@ msgstr ""
msgid "Private profile"
msgstr ""
+msgid "Private projects Minutes cost factor"
+msgstr ""
+
msgid "Private projects can be created in your personal namespace with:"
msgstr ""
@@ -16005,6 +16327,9 @@ msgstr ""
msgid "ProjectSettings|With GitLab Pages you can host your static websites on GitLab"
msgstr ""
+msgid "ProjectSettings|With Metrics Dashboard you can visualize this project performance metrics"
+msgstr ""
+
msgid "ProjectTemplates|.NET Core"
msgstr ""
@@ -16068,6 +16393,9 @@ msgstr ""
msgid "ProjectTemplates|Spring"
msgstr ""
+msgid "ProjectTemplates|Static Site Editor/Middleman"
+msgstr ""
+
msgid "ProjectTemplates|iOS (Swift)"
msgstr ""
@@ -16287,13 +16615,13 @@ msgstr ""
msgid "PrometheusService|Prometheus is being automatically managed on your clusters"
msgstr ""
-msgid "PrometheusService|These metrics will only be monitored after your first deployment to an environment"
+msgid "PrometheusService|Select the Active checkbox to override the Auto Configuration with custom settings. If unchecked, Auto Configuration settings are used."
msgstr ""
-msgid "PrometheusService|Time-series monitoring service"
+msgid "PrometheusService|These metrics will only be monitored after your first deployment to an environment"
msgstr ""
-msgid "PrometheusService|To enable manual configuration, uninstall Prometheus from your clusters"
+msgid "PrometheusService|Time-series monitoring service"
msgstr ""
msgid "PrometheusService|To enable the installation of Prometheus on your clusters, deactivate the manual configuration below"
@@ -16479,6 +16807,9 @@ msgstr ""
msgid "Protip:"
msgstr ""
+msgid "Protocol"
+msgstr ""
+
msgid "Provider"
msgstr ""
@@ -16503,6 +16834,9 @@ msgstr ""
msgid "Public pipelines"
msgstr ""
+msgid "Public projects Minutes cost factor"
+msgstr ""
+
msgid "Pull"
msgstr ""
@@ -16703,6 +17037,9 @@ msgid_plural "Refreshing in %d seconds to show the updated status..."
msgstr[0] ""
msgstr[1] ""
+msgid "Regenerate export"
+msgstr ""
+
msgid "Regenerate instance ID"
msgstr ""
@@ -17080,9 +17417,29 @@ msgstr ""
msgid "Reports|%{combinedString} and %{resolvedString}"
msgstr ""
+msgid "Reports|Accessibility scanning detected %d issue for the source branch only"
+msgid_plural "Reports|Accessibility scanning detected %d issues for the source branch only"
+msgstr[0] ""
+msgstr[1] ""
+
+msgid "Reports|Accessibility scanning detected no issues for the source branch only"
+msgstr ""
+
+msgid "Reports|Accessibility scanning failed loading results"
+msgstr ""
+
+msgid "Reports|Accessibility scanning results are being parsed"
+msgstr ""
+
msgid "Reports|Actions"
msgstr ""
+msgid "Reports|An error occured while loading report"
+msgstr ""
+
+msgid "Reports|An error occurred while loading %{name} results"
+msgstr ""
+
msgid "Reports|Class"
msgstr ""
@@ -17188,7 +17545,7 @@ msgstr ""
msgid "Requested %{time_ago}"
msgstr ""
-msgid "Requested design version does not exist"
+msgid "Requested design version does not exist."
msgstr ""
msgid "Requested states are invalid"
@@ -17372,6 +17729,9 @@ msgstr ""
msgid "Resync"
msgstr ""
+msgid "Resync all"
+msgstr ""
+
msgid "Resync all %{replicableType}"
msgstr ""
@@ -17440,6 +17800,9 @@ msgstr ""
msgid "Revoked personal access token %{personal_access_token_name}!"
msgstr ""
+msgid "Revoked project access token %{project_access_token_name}!"
+msgstr ""
+
msgid "RightSidebar|adding a"
msgstr ""
@@ -17488,6 +17851,9 @@ msgstr ""
msgid "Runner token"
msgstr ""
+msgid "Runner tokens"
+msgstr ""
+
msgid "Runner was not updated."
msgstr ""
@@ -17542,6 +17908,9 @@ msgstr ""
msgid "SAML SSO for %{group_name}"
msgstr ""
+msgid "SAML discovery tokens"
+msgstr ""
+
msgid "SAML for %{group_name}"
msgstr ""
@@ -17626,12 +17995,18 @@ msgstr ""
msgid "Scheduled"
msgstr ""
+msgid "Scheduled to merge this merge request (%{strategy})."
+msgstr ""
+
msgid "Scheduled to merge this merge request when the pipeline succeeds."
msgstr ""
msgid "Schedules"
msgstr ""
+msgid "Schedules to merge this merge request (%{strategy})."
+msgstr ""
+
msgid "Scheduling"
msgstr ""
@@ -17689,6 +18064,9 @@ msgstr ""
msgid "Search branches and tags"
msgstr ""
+msgid "Search by author"
+msgstr ""
+
msgid "Search files"
msgstr ""
@@ -17834,11 +18212,6 @@ msgid_plural "SearchResults|snippets"
msgstr[0] ""
msgstr[1] ""
-msgid "SearchResults|snippet result"
-msgid_plural "SearchResults|snippet results"
-msgstr[0] ""
-msgstr[1] ""
-
msgid "SearchResults|user"
msgid_plural "SearchResults|users"
msgstr[0] ""
@@ -17849,6 +18222,9 @@ msgid_plural "SearchResults|wiki results"
msgstr[0] ""
msgstr[1] ""
+msgid "Searching by both author and message is currently not supported."
+msgstr ""
+
msgid "Seat Link"
msgstr ""
@@ -17876,184 +18252,232 @@ msgstr ""
msgid "Security Dashboard"
msgstr ""
-msgid "Security Dashboard|Error fetching the vulnerability counts. Please check your network connection and try again."
+msgid "Security configuration help link"
+msgstr ""
+
+msgid "Security dashboard"
msgstr ""
-msgid "Security Dashboard|Error fetching the vulnerability list. Please check your network connection and try again."
+msgid "Security report is out of date. Please update your branch with the latest changes from the target branch (%{targetBranchName})"
msgstr ""
-msgid "Security Dashboard|Issue Created"
+msgid "Security report is out of date. Run %{newPipelineLinkStart}a new pipeline%{newPipelineLinkEnd} for the target branch (%{targetBranchName})"
msgstr ""
-msgid "Security Reports|Comment added to '%{vulnerabilityName}'"
+msgid "SecurityConfiguration|Configured"
msgstr ""
-msgid "Security Reports|Comment deleted on '%{vulnerabilityName}'"
+msgid "SecurityConfiguration|Feature"
msgstr ""
-msgid "Security Reports|Comment edited on '%{vulnerabilityName}'"
+msgid "SecurityConfiguration|Feature documentation for %{featureName}"
msgstr ""
-msgid "Security Reports|Create issue"
+msgid "SecurityConfiguration|Not yet configured"
msgstr ""
-msgid "Security Reports|Dismiss vulnerability"
+msgid "SecurityConfiguration|Secure features"
msgstr ""
-msgid "Security Reports|Dismissed '%{vulnerabilityName}'"
+msgid "SecurityConfiguration|Status"
msgstr ""
-msgid "Security Reports|Dismissed '%{vulnerabilityName}'. Turn off the hide dismissed toggle to view."
+msgid "SecurityReports|%{firstProject} and %{secondProject}"
msgstr ""
-msgid "Security Reports|Either you don't have permission to view this dashboard or the dashboard has not been setup. Please check your permission settings with your administrator or check your dashboard configurations to proceed."
+msgid "SecurityReports|%{firstProject}, %{secondProject}, and %{rest}"
msgstr ""
-msgid "Security Reports|Learn more about setting up your dashboard"
+msgid "SecurityReports|Add a project to your dashboard"
msgstr ""
-msgid "Security Reports|More info"
+msgid "SecurityReports|Add or remove projects from your dashboard"
msgstr ""
-msgid "Security Reports|Oops, something doesn't seem right."
+msgid "SecurityReports|Add projects"
msgstr ""
-msgid "Security Reports|Security reports can only be accessed by authorized users."
+msgid "SecurityReports|Comment added to '%{vulnerabilityName}'"
msgstr ""
-msgid "Security Reports|There was an error adding the comment."
+msgid "SecurityReports|Comment deleted on '%{vulnerabilityName}'"
msgstr ""
-msgid "Security Reports|There was an error creating the issue."
+msgid "SecurityReports|Comment edited on '%{vulnerabilityName}'"
msgstr ""
-msgid "Security Reports|There was an error creating the merge request."
+msgid "SecurityReports|Create issue"
msgstr ""
-msgid "Security Reports|There was an error deleting the comment."
+msgid "SecurityReports|Dismiss Selected"
msgstr ""
-msgid "Security Reports|There was an error dismissing the vulnerabilities."
+msgid "SecurityReports|Dismiss vulnerability"
msgstr ""
-msgid "Security Reports|There was an error dismissing the vulnerability."
+msgid "SecurityReports|Dismissed '%{vulnerabilityName}'"
msgstr ""
-msgid "Security Reports|There was an error reverting the dismissal."
+msgid "SecurityReports|Dismissed '%{vulnerabilityName}'. Turn off the hide dismissed toggle to view."
msgstr ""
-msgid "Security Reports|There was an error reverting this dismissal."
+msgid "SecurityReports|Each vulnerability now has a unique page that can be directly linked to, shared, referenced, and tracked as the single source of truth. Vulnerability occurrences also persist across scanner runs, which improves tracking and visibility and reduces duplicates between scans."
msgstr ""
-msgid "Security Reports|Undo dismiss"
+msgid "SecurityReports|Edit dashboard"
msgstr ""
-msgid "Security Reports|You do not have sufficient permissions to access this report"
+msgid "SecurityReports|Either you don't have permission to view this dashboard or the dashboard has not been setup. Please check your permission settings with your administrator or check your dashboard configurations to proceed."
msgstr ""
-msgid "Security Reports|You must sign in as an authorized user to see this report"
+msgid "SecurityReports|Error fetching the vulnerability counts. Please check your network connection and try again."
msgstr ""
-msgid "Security configuration help link"
+msgid "SecurityReports|Error fetching the vulnerability list. Please check your network connection and try again."
msgstr ""
-msgid "Security dashboard"
+msgid "SecurityReports|False positive"
msgstr ""
-msgid "Security report is out of date. Please update your branch with the latest changes from the target branch (%{targetBranchName})"
+msgid "SecurityReports|Hide dismissed"
msgstr ""
-msgid "Security report is out of date. Run %{newPipelineLinkStart}a new pipeline%{newPipelineLinkEnd} for the target branch (%{targetBranchName})"
+msgid "SecurityReports|Introducing standalone vulnerabilities"
msgstr ""
-msgid "SecurityConfiguration|Configured"
+msgid "SecurityReports|Issue Created"
msgstr ""
-msgid "SecurityConfiguration|Feature"
+msgid "SecurityReports|Learn More"
msgstr ""
-msgid "SecurityConfiguration|Feature documentation for %{featureName}"
+msgid "SecurityReports|Learn more about setting up your dashboard"
msgstr ""
-msgid "SecurityConfiguration|Not yet configured"
+msgid "SecurityReports|Load more vulnerabilities"
msgstr ""
-msgid "SecurityConfiguration|Secure features"
+msgid "SecurityReports|Monitor vulnerabilities in your code"
msgstr ""
-msgid "SecurityConfiguration|Status"
+msgid "SecurityReports|More info"
msgstr ""
-msgid "SecurityDashboard|%{firstProject} and %{secondProject}"
+msgid "SecurityReports|More information"
msgstr ""
-msgid "SecurityDashboard|%{firstProject}, %{secondProject}, and %{rest}"
+msgid "SecurityReports|No vulnerabilities found for dashboard"
msgstr ""
-msgid "SecurityDashboard|Add a project to your dashboard"
+msgid "SecurityReports|No vulnerabilities found for this group"
msgstr ""
-msgid "SecurityDashboard|Add or remove projects from your dashboard"
+msgid "SecurityReports|No vulnerabilities found for this pipeline"
msgstr ""
-msgid "SecurityDashboard|Add projects"
+msgid "SecurityReports|No vulnerabilities found for this project"
msgstr ""
-msgid "SecurityDashboard|Edit dashboard"
+msgid "SecurityReports|Oops, something doesn't seem right."
msgstr ""
-msgid "SecurityDashboard|Hide dismissed"
+msgid "SecurityReports|Pipeline %{pipelineLink} triggered %{timeago} by %{user}"
msgstr ""
-msgid "SecurityDashboard|Monitor vulnerabilities in your code"
+msgid "SecurityReports|Project"
msgstr ""
-msgid "SecurityDashboard|More information"
+msgid "SecurityReports|Projects added"
msgstr ""
-msgid "SecurityDashboard|No vulnerabilities found for dashboard"
+msgid "SecurityReports|Remove project from dashboard"
msgstr ""
-msgid "SecurityDashboard|Pipeline %{pipelineLink} triggered %{timeago} by %{user}"
+msgid "SecurityReports|Report type"
msgstr ""
-msgid "SecurityDashboard|Project"
+msgid "SecurityReports|Return to dashboard"
msgstr ""
-msgid "SecurityDashboard|Projects added"
+msgid "SecurityReports|Security Dashboard"
msgstr ""
-msgid "SecurityDashboard|Remove project from dashboard"
+msgid "SecurityReports|Security reports can only be accessed by authorized users."
msgstr ""
-msgid "SecurityDashboard|Report type"
+msgid "SecurityReports|Select a project to add by using the project search field above."
msgstr ""
-msgid "SecurityDashboard|Return to dashboard"
+msgid "SecurityReports|Select a reason"
msgstr ""
-msgid "SecurityDashboard|Security Dashboard"
+msgid "SecurityReports|Severity"
msgstr ""
-msgid "SecurityDashboard|Select a project to add by using the project search field above."
+msgid "SecurityReports|Status"
msgstr ""
-msgid "SecurityDashboard|Severity"
+msgid "SecurityReports|The security dashboard displays the latest security findings for projects you wish to monitor. Select \"Edit dashboard\" to add and remove projects."
msgstr ""
-msgid "SecurityDashboard|Status"
+msgid "SecurityReports|The security dashboard displays the latest security report. Use it to find and fix vulnerabilities."
msgstr ""
-msgid "SecurityDashboard|The security dashboard displays the latest security findings for projects you wish to monitor. Select \"Edit dashboard\" to add and remove projects."
+msgid "SecurityReports|There was an error adding the comment."
msgstr ""
-msgid "SecurityDashboard|The security dashboard displays the latest security report. Use it to find and fix vulnerabilities."
+msgid "SecurityReports|There was an error creating the issue."
msgstr ""
-msgid "SecurityDashboard|There was an error while generating the report."
+msgid "SecurityReports|There was an error creating the merge request."
msgstr ""
-msgid "SecurityDashboard|Unable to add %{invalidProjects}"
+msgid "SecurityReports|There was an error deleting the comment."
+msgstr ""
+
+msgid "SecurityReports|There was an error dismissing the vulnerabilities."
+msgstr ""
+
+msgid "SecurityReports|There was an error dismissing the vulnerability."
+msgstr ""
+
+msgid "SecurityReports|There was an error reverting the dismissal."
+msgstr ""
+
+msgid "SecurityReports|There was an error reverting this dismissal."
+msgstr ""
+
+msgid "SecurityReports|There was an error while generating the report."
+msgstr ""
+
+msgid "SecurityReports|Unable to add %{invalidProjects}"
+msgstr ""
+
+msgid "SecurityReports|Undo dismiss"
+msgstr ""
+
+msgid "SecurityReports|While it's rare to have no vulnerabilities for your group, it can happen. In any event, we ask that you double check your settings to make sure you've set up your dashboard correctly."
+msgstr ""
+
+msgid "SecurityReports|While it's rare to have no vulnerabilities for your pipeline, it can happen. In any event, we ask that you double check your settings to make sure all security scanning jobs have passed successfully."
+msgstr ""
+
+msgid "SecurityReports|While it's rare to have no vulnerabilities for your project, it can happen. In any event, we ask that you double check your settings to make sure you've set up your dashboard correctly."
+msgstr ""
+
+msgid "SecurityReports|While it's rare to have no vulnerabilities, it can happen. In any event, we ask that you please double check your settings to make sure you've set up your dashboard correctly."
+msgstr ""
+
+msgid "SecurityReports|Won't fix / Accept risk"
+msgstr ""
+
+msgid "SecurityReports|You do not have sufficient permissions to access this report"
+msgstr ""
+
+msgid "SecurityReports|You must sign in as an authorized user to see this report"
+msgstr ""
+
+msgid "SecurityReports|[No reason]"
msgstr ""
msgid "See GitLab's %{password_policy_guidelines}"
@@ -18065,6 +18489,9 @@ msgstr ""
msgid "See the affected projects in the GitLab admin panel"
msgstr ""
+msgid "See what's new at GitLab"
+msgstr ""
+
msgid "Select"
msgstr ""
@@ -18470,9 +18897,6 @@ msgstr ""
msgid "Set up Jira Integration"
msgstr ""
-msgid "Set up Jira Integration illustration"
-msgstr ""
-
msgid "Set up a %{type} Runner automatically"
msgstr ""
@@ -18557,6 +18981,9 @@ msgstr ""
msgid "Severity: %{severity}"
msgstr ""
+msgid "Shards (%{shards})"
+msgstr ""
+
msgid "Shards to synchronize"
msgstr ""
@@ -18871,6 +19298,9 @@ msgstr ""
msgid "Snowplow"
msgstr ""
+msgid "Some child epics may be hidden due to applied filters"
+msgstr ""
+
msgid "Some email servers do not support overriding the email sender name. Enable this option to include the name of the author of the issue, merge request or comment in the email body instead."
msgstr ""
@@ -19012,6 +19442,9 @@ msgstr ""
msgid "Something went wrong while updating your list settings"
msgstr ""
+msgid "Something went wrong with your automatic subscription renewal"
+msgstr ""
+
msgid "Something went wrong, unable to add %{project} to dashboard"
msgstr ""
@@ -19297,6 +19730,9 @@ msgstr ""
msgid "Stage removed"
msgstr ""
+msgid "Standard"
+msgstr ""
+
msgid "Star a label to make it a priority label. Order the prioritized labels to change their relative priority, by dragging."
msgstr ""
@@ -19420,6 +19856,9 @@ msgstr ""
msgid "Static Application Security Testing (SAST)"
msgstr ""
+msgid "StaticSiteEditor|An error occurred while submitting your changes."
+msgstr ""
+
msgid "StaticSiteEditor|Branch could not be created."
msgstr ""
@@ -19429,18 +19868,30 @@ msgstr ""
msgid "StaticSiteEditor|Could not create merge request."
msgstr ""
+msgid "StaticSiteEditor|Incompatible file content"
+msgstr ""
+
msgid "StaticSiteEditor|Return to site"
msgstr ""
+msgid "StaticSiteEditor|Static site editor"
+msgstr ""
+
msgid "StaticSiteEditor|Success!"
msgstr ""
msgid "StaticSiteEditor|Summary of changes"
msgstr ""
+msgid "StaticSiteEditor|The Static Site Editor is currently configured to only edit Markdown content on pages generated from Middleman. Visit the documentation to learn more about configuring your site to use the Static Site Editor."
+msgstr ""
+
msgid "StaticSiteEditor|Update %{sourcePath} file"
msgstr ""
+msgid "StaticSiteEditor|View documentation"
+msgstr ""
+
msgid "StaticSiteEditor|View merge request"
msgstr ""
@@ -19498,6 +19949,12 @@ msgstr ""
msgid "StatusPage|Status page"
msgstr ""
+msgid "StatusPage|Status page URL"
+msgstr ""
+
+msgid "StatusPage|Status page frontend documentation"
+msgstr ""
+
msgid "StatusPage|To publish incidents to an external status page, GitLab will store a JSON file in your Amazon S3 account in a location accessible to your external status page service. Make sure to also set up %{docsLink}"
msgstr ""
@@ -19846,6 +20303,12 @@ msgstr ""
msgid "Synced"
msgstr ""
+msgid "Synchronization disabled"
+msgstr ""
+
+msgid "Synchronization of container repositories is disabled."
+msgstr ""
+
msgid "System"
msgstr ""
@@ -19999,6 +20462,9 @@ msgstr ""
msgid "Telephone number"
msgstr ""
+msgid "Telephone number (Optional)"
+msgstr ""
+
msgid "Template"
msgstr ""
@@ -20305,6 +20771,9 @@ msgstr ""
msgid "The license was successfully uploaded and is now active. You can see the details below."
msgstr ""
+msgid "The license was successfully uploaded and will be active from %{starts_at}. You can see the details below."
+msgstr ""
+
msgid "The maximum file size allowed is %{size}."
msgstr ""
@@ -20626,6 +21095,9 @@ msgstr ""
msgid "There was an error getting the epic participants."
msgstr ""
+msgid "There was an error importing the Jira project."
+msgstr ""
+
msgid "There was an error loading users activity calendar."
msgstr ""
@@ -20641,7 +21113,7 @@ msgstr ""
msgid "There was an error resetting user pipeline minutes."
msgstr ""
-msgid "There was an error saving this Geo Node"
+msgid "There was an error saving this Geo Node."
msgstr ""
msgid "There was an error saving your changes."
@@ -20704,6 +21176,9 @@ msgstr ""
msgid "They can be managed using the %{link}."
msgstr ""
+msgid "Third Party Advisory Link"
+msgstr ""
+
msgid "Third party offers"
msgstr ""
@@ -20740,6 +21215,9 @@ msgstr ""
msgid "This application will be able to:"
msgstr ""
+msgid "This attachment has been truncated to avoid exceeding the maximum allowed attachment size of 15MB. %{written_count} of %{issues_count} issues have been included. Consider re-exporting with a narrower selection of issues."
+msgstr ""
+
msgid "This block is self-referential"
msgstr ""
@@ -20881,6 +21359,12 @@ msgstr ""
msgid "This issue is confidential"
msgstr ""
+msgid "This issue is currently blocked by the following issues: %{issues}."
+msgstr ""
+
+msgid "This issue is in a child epic of the filtered epic"
+msgstr ""
+
msgid "This issue is locked."
msgstr ""
@@ -21079,7 +21563,10 @@ msgstr ""
msgid "This user will be the author of all events in the activity feed that are the result of an update, like new branches being created or new commits being pushed to existing branches. Upon creation or when reassigning you can only assign yourself to be the mirror user."
msgstr ""
-msgid "This variable can not be masked"
+msgid "This variable can not be masked."
+msgstr ""
+
+msgid "This variable does not match the expected pattern."
msgstr ""
msgid "This will help us personalize your onboarding experience."
@@ -21109,10 +21596,10 @@ msgstr ""
msgid "ThreatMonitoring|Application firewall not detected"
msgstr ""
-msgid "ThreatMonitoring|Container Network Policy"
+msgid "ThreatMonitoring|Container Network Policies are not installed or have been disabled. To view this data, ensure your Network Policies are installed and enabled for your cluster."
msgstr ""
-msgid "ThreatMonitoring|Container NetworkPolicies are not installed or has been disabled. To view this data, ensure you NetworkPolicies are installed and enabled for your cluster."
+msgid "ThreatMonitoring|Container Network Policy"
msgstr ""
msgid "ThreatMonitoring|Container NetworkPolicies not detected"
@@ -21145,7 +21632,7 @@ msgstr ""
msgid "ThreatMonitoring|Something went wrong, unable to fetch statistics"
msgstr ""
-msgid "ThreatMonitoring|The firewall is not installed or has been disabled. To view this data, ensure you firewall is installed and enabled for your cluster."
+msgid "ThreatMonitoring|The firewall is not installed or has been disabled. To view this data, ensure the web application firewall is installed and enabled for your cluster."
msgstr ""
msgid "ThreatMonitoring|The graph below is an overview of traffic coming to your application as tracked by the Web Application Firewall (WAF). View the docs for instructions on how to access the WAF logs to see what type of malicious traffic is trying to access your app. The docs link is also accessible by clicking the \"?\" icon next to the title below."
@@ -21214,6 +21701,9 @@ msgstr ""
msgid "Time in seconds GitLab will wait for a response from the external service. When the service does not respond in time, access will be denied."
msgstr ""
+msgid "Time of import: %{importTime}"
+msgstr ""
+
msgid "Time remaining"
msgstr ""
@@ -21398,7 +21888,7 @@ msgstr ""
msgid "Title:"
msgstr ""
-msgid "Titles and Filenames"
+msgid "Titles and Descriptions"
msgstr ""
msgid "To"
@@ -21605,7 +22095,7 @@ msgstr ""
msgid "Too many projects enabled. You will need to manage them via the console or the API."
msgstr ""
-msgid "Topics"
+msgid "Topics (optional)"
msgstr ""
msgid "Total"
@@ -21767,6 +22257,9 @@ msgstr ""
msgid "Try all GitLab has to offer for 30 days."
msgstr ""
+msgid "Try changing or removing filters."
+msgstr ""
+
msgid "Try to fork again"
msgstr ""
@@ -21860,6 +22353,9 @@ msgstr ""
msgid "Unable to connect to server: %{error}"
msgstr ""
+msgid "Unable to connect to the Jira instance. Please check your Jira integration configuration."
+msgstr ""
+
msgid "Unable to convert Kubernetes logs encoding to UTF-8"
msgstr ""
@@ -22803,6 +23299,9 @@ msgstr ""
msgid "View issue"
msgstr ""
+msgid "View issues"
+msgstr ""
+
msgid "View it on GitLab"
msgstr ""
@@ -22980,6 +23479,15 @@ msgstr ""
msgid "VulnerabilityManagement|Resolved %{timeago} by %{user}"
msgstr ""
+msgid "VulnerabilityManagement|Something went wrong while trying to delete the comment. Please try again later."
+msgstr ""
+
+msgid "VulnerabilityManagement|Something went wrong while trying to retrieve the vulnerability history. Please try again later."
+msgstr ""
+
+msgid "VulnerabilityManagement|Something went wrong while trying to save the comment. Please try again later."
+msgstr ""
+
msgid "VulnerabilityManagement|Something went wrong, could not create an issue."
msgstr ""
@@ -23010,6 +23518,9 @@ msgstr ""
msgid "VulnerabilityStatusTypes|Resolved"
msgstr ""
+msgid "Vulnerability|%{scannerName} (version %{scannerVersion})"
+msgstr ""
+
msgid "Vulnerability|Class"
msgstr ""
@@ -23043,7 +23554,10 @@ msgstr ""
msgid "Vulnerability|Project"
msgstr ""
-msgid "Vulnerability|Report Type"
+msgid "Vulnerability|Scanner Provider"
+msgstr ""
+
+msgid "Vulnerability|Scanner Type"
msgstr ""
msgid "Vulnerability|Severity"
@@ -23097,9 +23611,15 @@ msgstr ""
msgid "We sent you an email with reset password instructions"
msgstr ""
+msgid "We tried to automatically renew your %{strong}%{plan_name}%{strong_close} subscription for %{strong}%{namespace_name}%{strong_close} on %{expires_on} but something went wrong so your subscription was downgraded to the free plan. Don't worry, your data is safe. We suggest you check your payment method and get in touch with our support team (%{support_link}). They'll gladly help with your subscription renewal."
+msgstr ""
+
msgid "We want to be sure it is you, please confirm you are not a robot."
msgstr ""
+msgid "We will automatically renew your %{strong}%{plan_name}%{strong_close} subscription for %{strong}%{namespace_name}%{strong_close} on %{strong}%{expires_on}%{strong_close}. There's nothing that you need to do, we'll let you know when the renewal is complete. Need more seats, a higher plan or just want to review your payment method?"
+msgstr ""
+
msgid "We've found no vulnerabilities"
msgstr ""
@@ -23204,15 +23724,6 @@ msgstr ""
msgid "When:"
msgstr ""
-msgid "While it's rare to have no vulnerabilities for your group, it can happen. In any event, we ask that you double check your settings to make sure you've set up your dashboard correctly."
-msgstr ""
-
-msgid "While it's rare to have no vulnerabilities for your pipeline, it can happen. In any event, we ask that you double check your settings to make sure all security scanning jobs have passed successfully."
-msgstr ""
-
-msgid "While it's rare to have no vulnerabilities for your project, it can happen. In any event, we ask that you double check your settings to make sure you've set up your dashboard correctly."
-msgstr ""
-
msgid "While it's rare to have no vulnerabilities, it can happen. In any event, we ask that you please double check your settings to make sure you've set up your dashboard correctly."
msgstr ""
@@ -23243,9 +23754,6 @@ msgstr ""
msgid "Wiki"
msgstr ""
-msgid "Wiki pages"
-msgstr ""
-
msgid "Wiki was successfully updated."
msgstr ""
@@ -23429,6 +23937,9 @@ msgstr ""
msgid "Yes, add it"
msgstr ""
+msgid "Yes, close issue"
+msgstr ""
+
msgid "Yes, let me map Google Code users to full names or GitLab users."
msgstr ""
@@ -23624,6 +24135,9 @@ msgstr ""
msgid "You could not create a new trigger."
msgstr ""
+msgid "You didn't renew your %{strong}%{plan_name}%{strong_close} subscription for %{strong}%{namespace_name}%{strong_close} so it was downgraded to the free plan."
+msgstr ""
+
msgid "You didn't renew your %{strong}%{plan_name}%{strong_close} subscription so it was downgraded to the GitLab Core Plan."
msgstr ""
@@ -23690,6 +24204,9 @@ msgstr ""
msgid "You have declined the invitation to join %{label}."
msgstr ""
+msgid "You have imported from this project %{numberOfPreviousImportsForProject} times before. Each new import will create duplicate issues."
+msgstr ""
+
msgid "You have no permissions"
msgstr ""
@@ -23771,6 +24288,9 @@ msgstr ""
msgid "You need to upload a Google Takeout archive."
msgstr ""
+msgid "You reached %{usage_in_percent} of %{namespace_name}'s capacity (%{used_storage} of %{storage_limit})"
+msgstr ""
+
msgid "You tried to fork %{link_to_the_project} but it failed for the following reason:"
msgstr ""
@@ -23855,9 +24375,21 @@ msgstr ""
msgid "YouTube"
msgstr ""
+msgid "Your %{strong}%{plan_name}%{strong_close} subscription for %{strong}%{namespace_name}%{strong_close} will expire on %{strong}%{expires_on}%{strong_close}. After that, you will not to be able to create issues or merge requests as well as many other features."
+msgstr ""
+
msgid "Your %{strong}%{plan_name}%{strong_close} subscription will expire on %{strong}%{expires_on}%{strong_close}. After that, you will not to be able to create issues or merge requests as well as many other features."
msgstr ""
+msgid "Your CSV export has started. It will be emailed to %{email} when complete."
+msgstr ""
+
+msgid "Your CSV export of %{issues_count} from project %{project_link} has been added to this email as an attachment."
+msgstr ""
+
+msgid "Your CSV export of %{written_count} from project %{project_name} (%{project_url}) has been added to this email as an attachment."
+msgstr ""
+
msgid "Your Commit Email will be used for web based operations, such as edits and merges."
msgstr ""
@@ -23954,6 +24486,9 @@ msgstr ""
msgid "Your comment could not be updated! Please check your network connection and try again."
msgstr ""
+msgid "Your comment will be discarded."
+msgstr ""
+
msgid "Your custom stage '%{title}' was created"
msgstr ""
@@ -23993,6 +24528,9 @@ msgstr ""
msgid "Your new personal access token has been created."
msgstr ""
+msgid "Your new project access token has been created."
+msgstr ""
+
msgid "Your password isn't required to view this page. If a password or any other personal details are requested, please contact your administrator to report abuse."
msgstr ""
@@ -24011,12 +24549,18 @@ msgstr ""
msgid "Your request for access has been queued for review."
msgstr ""
+msgid "Your search didn't match any commits."
+msgstr ""
+
msgid "Your subscription expired!"
msgstr ""
msgid "Your subscription has been downgraded"
msgstr ""
+msgid "Your subscription will automatically renew in %{remaining_days}"
+msgstr ""
+
msgid "Your subscription will expire in %{remaining_days}"
msgstr ""
@@ -24055,7 +24599,7 @@ msgstr ""
msgid "allowed to fail"
msgstr ""
-msgid "already being used for another group or project milestone."
+msgid "already being used for another group or project %{timebox_name}."
msgstr ""
msgid "already has a \"created\" issue link"
@@ -24133,6 +24677,9 @@ msgstr ""
msgid "ciReport|%{linkStartTag}Learn more about SAST %{linkEndTag}"
msgstr ""
+msgid "ciReport|%{linkStartTag}Learn more about Secret Scanning %{linkEndTag}"
+msgstr ""
+
msgid "ciReport|%{linkStartTag}Learn more about codequality reports %{linkEndTag}"
msgstr ""
@@ -24296,6 +24843,12 @@ msgstr ""
msgid "ciReport|SAST"
msgstr ""
+msgid "ciReport|Secret scanning"
+msgstr ""
+
+msgid "ciReport|Secret scanning detects secrets and credentials vulnerabilities in your source code."
+msgstr ""
+
msgid "ciReport|Security scanning"
msgstr ""
@@ -24493,6 +25046,9 @@ msgstr ""
msgid "group"
msgstr ""
+msgid "groups"
+msgstr ""
+
msgid "has already been linked to another vulnerability"
msgstr ""
@@ -24656,9 +25212,6 @@ msgstr[1] ""
msgid "merged %{time_ago}"
msgstr ""
-msgid "milestone should belong either to a project or a group."
-msgstr ""
-
msgid "missing"
msgstr ""
@@ -24995,6 +25548,9 @@ msgstr ""
msgid "no contributions"
msgstr ""
+msgid "no expiration"
+msgstr ""
+
msgid "no one can merge"
msgstr ""
@@ -25047,6 +25603,9 @@ msgstr ""
msgid "pending removal"
msgstr ""
+msgid "per day"
+msgstr ""
+
msgid "pipeline"
msgstr ""
@@ -25075,6 +25634,12 @@ msgstr[1] ""
msgid "project avatar"
msgstr ""
+msgid "projects"
+msgstr ""
+
+msgid "push to your repository, create pipelines, create issues or add comments. To reduce storage capacity, delete unused repositories, artifacts, wikis, issues, and pipelines."
+msgstr ""
+
msgid "quick actions"
msgstr ""
@@ -25221,11 +25786,6 @@ msgstr ""
msgid "this document"
msgstr ""
-msgid "thread resolved"
-msgid_plural "threads resolved"
-msgstr[0] ""
-msgstr[1] ""
-
msgid "to help your contributors communicate effectively!"
msgstr ""
@@ -25280,6 +25840,9 @@ msgstr ""
msgid "view the blob"
msgstr ""
+msgid "vulnerability|Add a comment"
+msgstr ""
+
msgid "vulnerability|Add a comment or reason for dismissal"
msgstr ""
diff --git a/locale/nb_NO/gitlab.po b/locale/nb_NO/gitlab.po
index dbb95441d9f..ca5cac0c94e 100644
--- a/locale/nb_NO/gitlab.po
+++ b/locale/nb_NO/gitlab.po
@@ -12,7 +12,7 @@ msgstr ""
"X-Crowdin-Project: gitlab-ee\n"
"X-Crowdin-Language: nb\n"
"X-Crowdin-File: /master/locale/gitlab.pot\n"
-"PO-Revision-Date: 2020-04-15 00:26\n"
+"PO-Revision-Date: 2020-05-05 21:33\n"
msgid " %{start} to %{end}"
msgstr ""
@@ -212,6 +212,11 @@ msgid_plural "%d tags"
msgstr[0] ""
msgstr[1] ""
+msgid "%d unresolved thread"
+msgid_plural "%d unresolved threads"
+msgstr[0] ""
+msgstr[1] ""
+
msgid "%d vulnerability dismissed"
msgid_plural "%d vulnerabilities dismissed"
msgstr[0] ""
@@ -371,6 +376,9 @@ msgstr ""
msgid "%{mrText}, this issue will be closed automatically."
msgstr ""
+msgid "%{namespace_name} is now read-only. You cannot: %{base_message}"
+msgstr ""
+
msgid "%{name} contained %{resultsString}"
msgstr ""
@@ -383,6 +391,11 @@ msgstr ""
msgid "%{name}'s avatar"
msgstr ""
+msgid "%{no_of_days} day"
+msgid_plural "%{no_of_days} days"
+msgstr[0] ""
+msgstr[1] ""
+
msgid "%{number_commits_behind} commits behind %{default_branch}, %{number_commits_ahead} commits ahead"
msgstr ""
@@ -506,6 +519,9 @@ msgstr[1] ""
msgid "%{text} is available"
msgstr ""
+msgid "%{timebox_name} should belong either to a project or a group."
+msgstr ""
+
msgid "%{title} %{operator} %{threshold}"
msgstr ""
@@ -781,12 +797,18 @@ msgstr ""
msgid "<code>\"johnsmith@example.com\": \"johnsmith@example.com\"</code> will add \"By <a href=\"#\">johnsmith@example.com</a>\" to all issues and comments originally created by johnsmith@example.com. By default, the email address or username is masked to ensure the user's privacy. Use this option if you want to show the full email address."
msgstr ""
+msgid "<namespace / project>"
+msgstr ""
+
msgid "<no name set>"
msgstr ""
msgid "<no scopes selected>"
msgstr ""
+msgid "<project name>"
+msgstr ""
+
msgid "<strong>%{group_name}</strong> group members"
msgstr ""
@@ -820,6 +842,9 @@ msgstr ""
msgid "A basic page and serverless function that uses AWS Lambda, AWS API Gateway, and GitLab Pages"
msgstr ""
+msgid "A complete DevOps platform"
+msgstr ""
+
msgid "A default branch cannot be chosen for an empty project."
msgstr ""
@@ -1036,6 +1061,12 @@ msgstr ""
msgid "AccessTokens|reset it"
msgstr ""
+msgid "AccessibilityReport|Accessibility report artifact not found"
+msgstr ""
+
+msgid "AccessibilityReport|Failed to retrieve accessibility report"
+msgstr ""
+
msgid "AccessibilityReport|Learn More"
msgstr ""
@@ -1045,6 +1076,9 @@ msgstr ""
msgid "AccessibilityReport|New"
msgstr ""
+msgid "AccessibilityReport|The accessibility scanning found an error of the following type: %{code}"
+msgstr ""
+
msgid "Account"
msgstr ""
@@ -1287,6 +1321,9 @@ msgstr ""
msgid "Added at"
msgstr ""
+msgid "Added for this merge request"
+msgstr ""
+
msgid "Added in this version"
msgstr ""
@@ -1658,6 +1695,72 @@ msgid_plural "Alerts"
msgstr[0] ""
msgstr[1] ""
+msgid "Alert Details"
+msgstr ""
+
+msgid "AlertManagement|Acknowledged"
+msgstr ""
+
+msgid "AlertManagement|Alert"
+msgstr ""
+
+msgid "AlertManagement|Authorize external service"
+msgstr ""
+
+msgid "AlertManagement|Display alerts from all your monitoring tools directly within GitLab. Streamline the investigation of your alerts and the escalation of alerts to incidents."
+msgstr ""
+
+msgid "AlertManagement|End time"
+msgstr ""
+
+msgid "AlertManagement|End time:"
+msgstr ""
+
+msgid "AlertManagement|Events"
+msgstr ""
+
+msgid "AlertManagement|Events:"
+msgstr ""
+
+msgid "AlertManagement|Full Alert Details"
+msgstr ""
+
+msgid "AlertManagement|More information"
+msgstr ""
+
+msgid "AlertManagement|No alerts available to display. If you think you're seeing this message in error, refresh the page."
+msgstr ""
+
+msgid "AlertManagement|No alerts to display."
+msgstr ""
+
+msgid "AlertManagement|Overview"
+msgstr ""
+
+msgid "AlertManagement|Resolved"
+msgstr ""
+
+msgid "AlertManagement|Severity"
+msgstr ""
+
+msgid "AlertManagement|Start time"
+msgstr ""
+
+msgid "AlertManagement|Start time:"
+msgstr ""
+
+msgid "AlertManagement|Status"
+msgstr ""
+
+msgid "AlertManagement|Surface alerts in GitLab"
+msgstr ""
+
+msgid "AlertManagement|There was an error displaying the alerts. Confirm your endpoint's configuration details to ensure alerts appear."
+msgstr ""
+
+msgid "AlertManagement|Triggered"
+msgstr ""
+
msgid "AlertService|%{linkStart}Learn more%{linkEnd} about configuring this endpoint to receive alerts."
msgstr ""
@@ -1727,6 +1830,9 @@ msgstr ""
msgid "All security scans are enabled because %{linkStart}Auto DevOps%{linkEnd} is enabled on this project"
msgstr ""
+msgid "All threads resolved"
+msgstr ""
+
msgid "All users"
msgstr ""
@@ -1745,6 +1851,9 @@ msgstr ""
msgid "Allow only the selected protocols to be used for Git access."
msgstr ""
+msgid "Allow owners to manage default branch protection per group"
+msgstr ""
+
msgid "Allow owners to manually add users outside of LDAP"
msgstr ""
@@ -1853,6 +1962,9 @@ msgstr ""
msgid "An error occurred fetching the dropdown data."
msgstr ""
+msgid "An error occurred fetching the project authors."
+msgstr ""
+
msgid "An error occurred previewing the blob"
msgstr ""
@@ -1934,6 +2046,9 @@ msgstr ""
msgid "An error occurred while fetching sidebar data"
msgstr ""
+msgid "An error occurred while fetching terraform reports."
+msgstr ""
+
msgid "An error occurred while fetching the Service Desk address."
msgstr ""
@@ -2171,6 +2286,9 @@ msgstr ""
msgid "Any"
msgstr ""
+msgid "Any Author"
+msgstr ""
+
msgid "Any Label"
msgstr ""
@@ -2386,10 +2504,10 @@ msgstr ""
msgid "Are you sure that you want to unarchive this project?"
msgstr ""
-msgid "Are you sure you want to cancel creating this comment?"
+msgid "Are you sure you want to cancel editing this comment?"
msgstr ""
-msgid "Are you sure you want to cancel editing this comment?"
+msgid "Are you sure you want to close this blocked issue?"
msgstr ""
msgid "Are you sure you want to delete %{name}?"
@@ -2419,6 +2537,9 @@ msgstr ""
msgid "Are you sure you want to deploy this environment?"
msgstr ""
+msgid "Are you sure you want to discard this comment?"
+msgstr ""
+
msgid "Are you sure you want to erase this build?"
msgstr ""
@@ -2595,6 +2716,9 @@ msgstr ""
msgid "At least one approval from a code owner is required to change files matching the respective CODEOWNER rules."
msgstr ""
+msgid "At least one logging option is required to be enabled"
+msgstr ""
+
msgid "At least one of group_id or project_id must be specified"
msgstr ""
@@ -2633,6 +2757,27 @@ msgstr ""
msgid "AuditEvents|Target"
msgstr ""
+msgid "AuditLogs|(removed)"
+msgstr ""
+
+msgid "AuditLogs|Action"
+msgstr ""
+
+msgid "AuditLogs|Author"
+msgstr ""
+
+msgid "AuditLogs|Date"
+msgstr ""
+
+msgid "AuditLogs|IP Address"
+msgstr ""
+
+msgid "AuditLogs|Object"
+msgstr ""
+
+msgid "AuditLogs|Target"
+msgstr ""
+
msgid "Aug"
msgstr ""
@@ -2915,9 +3060,6 @@ msgstr ""
msgid "BambooService|You must set up automatic revision labeling and a repository trigger in Bamboo."
msgstr ""
-msgid "Batch operations"
-msgstr ""
-
msgid "BatchComments|Delete all pending comments"
msgstr ""
@@ -3047,6 +3189,9 @@ msgstr ""
msgid "Boards"
msgstr ""
+msgid "Boards and Board Lists"
+msgstr ""
+
msgid "Boards|Collapse"
msgstr ""
@@ -3266,6 +3411,9 @@ msgstr ""
msgid "Buy GitLab Enterprise Edition"
msgstr ""
+msgid "Buy more Pipeline minutes"
+msgstr ""
+
msgid "By %{user_name}"
msgstr ""
@@ -3386,6 +3534,9 @@ msgstr ""
msgid "Can't scan the code?"
msgstr ""
+msgid "Can't update snippet: %{err}"
+msgstr ""
+
msgid "Canary"
msgstr ""
@@ -3554,6 +3705,9 @@ msgstr ""
msgid "Changing group path can have unintended side effects."
msgstr ""
+msgid "Charts"
+msgstr ""
+
msgid "Charts can't be displayed as the request for data has timed out. %{documentationLink}"
msgstr ""
@@ -3590,9 +3744,6 @@ msgstr ""
msgid "ChatMessage|and [%{count} more](%{pipeline_failed_jobs_url})"
msgstr ""
-msgid "ChatMessage|failed"
-msgstr ""
-
msgid "ChatMessage|has failed"
msgstr ""
@@ -3608,9 +3759,6 @@ msgstr ""
msgid "ChatMessage|in %{project_link}"
msgstr ""
-msgid "ChatMessage|passed"
-msgstr ""
-
msgid "Check again"
msgstr ""
@@ -4061,9 +4209,6 @@ msgstr ""
msgid "Click any <strong>project name</strong> in the project list below to navigate to the project milestone."
msgstr ""
-msgid "Click here"
-msgstr ""
-
msgid "Click the <strong>Download</strong> button and wait for downloading to complete."
msgstr ""
@@ -4451,6 +4596,12 @@ msgstr ""
msgid "ClusterIntegration|Fetching zones"
msgstr ""
+msgid "ClusterIntegration|Fluentd"
+msgstr ""
+
+msgid "ClusterIntegration|Fluentd is an open source data collector, which lets you unify the data collection and consumption for a better use and understanding of data. It requires at least one of the following logs to be successfully installed."
+msgstr ""
+
msgid "ClusterIntegration|GitLab Integration"
msgstr ""
@@ -4757,6 +4908,15 @@ msgstr ""
msgid "ClusterIntegration|Request to begin uninstalling failed"
msgstr ""
+msgid "ClusterIntegration|SIEM Hostname"
+msgstr ""
+
+msgid "ClusterIntegration|SIEM Port"
+msgstr ""
+
+msgid "ClusterIntegration|SIEM Protocol"
+msgstr ""
+
msgid "ClusterIntegration|Save changes"
msgstr ""
@@ -4853,6 +5013,12 @@ msgstr ""
msgid "ClusterIntegration|Select zone to choose machine type"
msgstr ""
+msgid "ClusterIntegration|Send Cilium Logs"
+msgstr ""
+
+msgid "ClusterIntegration|Send ModSecurity Logs"
+msgstr ""
+
msgid "ClusterIntegration|Service Token"
msgstr ""
@@ -5281,18 +5447,33 @@ msgstr ""
msgid "Compliance framework (optional)"
msgstr ""
+msgid "ComplianceFramework|GDPR"
+msgstr ""
+
msgid "ComplianceFramework|GDPR - General Data Protection Regulation"
msgstr ""
+msgid "ComplianceFramework|HIPAA"
+msgstr ""
+
msgid "ComplianceFramework|HIPAA - Health Insurance Portability and Accountability Act"
msgstr ""
+msgid "ComplianceFramework|PCI-DSS"
+msgstr ""
+
msgid "ComplianceFramework|PCI-DSS - Payment Card Industry-Data Security Standard"
msgstr ""
+msgid "ComplianceFramework|SOC 2"
+msgstr ""
+
msgid "ComplianceFramework|SOC 2 - Service Organization Control 2"
msgstr ""
+msgid "ComplianceFramework|SOX"
+msgstr ""
+
msgid "ComplianceFramework|SOX - Sarbanes-Oxley"
msgstr ""
@@ -5440,9 +5621,15 @@ msgstr ""
msgid "Container repositories sync capacity"
msgstr ""
+msgid "ContainerRegistry| Please visit the %{linkStart}administration settings%{linkEnd} to enable this feature."
+msgstr ""
+
msgid "ContainerRegistry|%{imageName} tags"
msgstr ""
+msgid "ContainerRegistry|%{title} was successfully scheduled for deletion"
+msgstr ""
+
msgid "ContainerRegistry|Automatically remove extra images that aren't designed to be kept."
msgstr ""
@@ -5455,6 +5642,9 @@ msgstr ""
msgid "ContainerRegistry|Container Registry"
msgstr ""
+msgid "ContainerRegistry|Container Registry tag expiration and retention policy is disabled"
+msgstr ""
+
msgid "ContainerRegistry|Copy build command"
msgstr ""
@@ -5464,18 +5654,12 @@ msgstr ""
msgid "ContainerRegistry|Copy push command"
msgstr ""
-msgid "ContainerRegistry|Currently, the Container Registry tag expiration feature is not available for projects created before GitLab version 12.8. For updates and more information, visit Issue %{linkStart}#196124%{linkEnd}"
-msgstr ""
-
msgid "ContainerRegistry|Docker connection error"
msgstr ""
msgid "ContainerRegistry|Docker tag expiration policy is %{toggleStatus}"
msgstr ""
-msgid "ContainerRegistry|Docker tags with names matching this regex pattern will expire:"
-msgstr ""
-
msgid "ContainerRegistry|Edit Settings"
msgstr ""
@@ -5497,9 +5681,6 @@ msgstr ""
msgid "ContainerRegistry|Image ID"
msgstr ""
-msgid "ContainerRegistry|Image deleted successfully"
-msgstr ""
-
msgid "ContainerRegistry|Keep and protect the images that matter most."
msgstr ""
@@ -5515,12 +5696,24 @@ msgstr ""
msgid "ContainerRegistry|Number of tags to retain:"
msgstr ""
+msgid "ContainerRegistry|Please contact your administrator."
+msgstr ""
+
msgid "ContainerRegistry|Push an image"
msgstr ""
msgid "ContainerRegistry|Quick Start"
msgstr ""
+msgid "ContainerRegistry|Regular expressions such as %{codeStart}.*-test%{codeEnd} or %{codeStart}dev-.*%{codeEnd} are supported"
+msgstr ""
+
+msgid "ContainerRegistry|Regular expressions such as %{codeStart}.*-test%{codeEnd} or %{codeStart}dev-.*%{codeEnd} are supported. To select all tags, use %{codeStart}.*%{codeEnd}"
+msgstr ""
+
+msgid "ContainerRegistry|Remember to run %{docLinkStart}garbage collection%{docLinkEnd} to remove the stale data from storage."
+msgstr ""
+
msgid "ContainerRegistry|Remove repository"
msgstr ""
@@ -5535,22 +5728,22 @@ msgstr[1] ""
msgid "ContainerRegistry|Retention policy has been Enabled"
msgstr ""
-msgid "ContainerRegistry|Something went wrong while deleting the image."
+msgid "ContainerRegistry|Something went wrong while fetching the expiration policy."
msgstr ""
-msgid "ContainerRegistry|Something went wrong while deleting the tag."
+msgid "ContainerRegistry|Something went wrong while fetching the repository list."
msgstr ""
-msgid "ContainerRegistry|Something went wrong while deleting the tags."
+msgid "ContainerRegistry|Something went wrong while fetching the tags list."
msgstr ""
-msgid "ContainerRegistry|Something went wrong while fetching the expiration policy."
+msgid "ContainerRegistry|Something went wrong while marking the tag for deletion."
msgstr ""
-msgid "ContainerRegistry|Something went wrong while fetching the packages list."
+msgid "ContainerRegistry|Something went wrong while marking the tags for deletion."
msgstr ""
-msgid "ContainerRegistry|Something went wrong while fetching the tags list."
+msgid "ContainerRegistry|Something went wrong while scheduling %{title} for deletion. Please try again."
msgstr ""
msgid "ContainerRegistry|Something went wrong while updating the expiration policy."
@@ -5559,16 +5752,25 @@ msgstr ""
msgid "ContainerRegistry|Tag"
msgstr ""
-msgid "ContainerRegistry|Tag deleted successfully"
-msgstr ""
-
msgid "ContainerRegistry|Tag expiration policy"
msgstr ""
msgid "ContainerRegistry|Tag expiration policy is designed to:"
msgstr ""
-msgid "ContainerRegistry|Tags deleted successfully"
+msgid "ContainerRegistry|Tag successfully marked for deletion."
+msgstr ""
+
+msgid "ContainerRegistry|Tags successfully marked for deletion."
+msgstr ""
+
+msgid "ContainerRegistry|Tags with names matching this regex pattern will %{italicStart}be preserved:%{italicEnd}"
+msgstr ""
+
+msgid "ContainerRegistry|Tags with names matching this regex pattern will %{italicStart}expire:%{italicEnd}"
+msgstr ""
+
+msgid "ContainerRegistry|The Container Registry tag expiration and retention policies for this project have not been enabled."
msgstr ""
msgid "ContainerRegistry|The last tag related to this image was recently removed. This empty image and any associated data will be automatically removed as part of the regular Garbage Collection process. If you have any questions, contact your administrator."
@@ -5589,16 +5791,16 @@ msgstr ""
msgid "ContainerRegistry|There are no container images stored for this project"
msgstr ""
-msgid "ContainerRegistry|This Registry contains deleted image tag data. Remember to run %{docLinkStart}garbage collection%{docLinkEnd} to remove the stale data from storage."
+msgid "ContainerRegistry|There was an error during the deletion of this image repository, please try again."
msgstr ""
msgid "ContainerRegistry|This image has no active tags"
msgstr ""
-msgid "ContainerRegistry|We are having trouble connecting to Docker, which could be due to an issue with your project name or path. %{docLinkStart}More Information%{docLinkEnd}"
+msgid "ContainerRegistry|This image repository is scheduled for deletion"
msgstr ""
-msgid "ContainerRegistry|Wildcards such as %{codeStart}.*-stable%{codeEnd} or %{codeStart}production/.*%{codeEnd} are supported. To select all tags, use %{codeStart}.*%{codeEnd}"
+msgid "ContainerRegistry|We are having trouble connecting to Docker, which could be due to an issue with your project name or path. %{docLinkStart}More Information%{docLinkEnd}"
msgstr ""
msgid "ContainerRegistry|With the Container Registry, every project can have its own space to store its Docker images. %{docLinkStart}More Information%{docLinkEnd}"
@@ -5853,7 +6055,7 @@ msgstr ""
msgid "Could not delete chat nickname %{chat_name}."
msgstr ""
-msgid "Could not find design"
+msgid "Could not find design."
msgstr ""
msgid "Could not remove the trigger."
@@ -5868,6 +6070,9 @@ msgstr ""
msgid "Could not revoke personal access token %{personal_access_token_name}."
msgstr ""
+msgid "Could not revoke project access token %{project_access_token_name}."
+msgstr ""
+
msgid "Could not save group ID"
msgstr ""
@@ -5925,6 +6130,9 @@ msgstr ""
msgid "Create a new branch"
msgstr ""
+msgid "Create a new deploy key for this project"
+msgstr ""
+
msgid "Create a new file as there are no files yet. Afterwards, you'll be able to commit your changes."
msgstr ""
@@ -6030,6 +6238,9 @@ msgstr ""
msgid "Create requirement"
msgstr ""
+msgid "Create snippet"
+msgstr ""
+
msgid "Create wildcard: %{searchTerm}"
msgstr ""
@@ -6051,6 +6262,9 @@ msgstr ""
msgid "Created"
msgstr ""
+msgid "Created %{timestamp}"
+msgstr ""
+
msgid "Created At"
msgstr ""
@@ -6407,6 +6621,9 @@ msgstr ""
msgid "CycleAnalytics|stage dropdown"
msgstr ""
+msgid "DAG"
+msgstr ""
+
msgid "DNS"
msgstr ""
@@ -6773,6 +6990,9 @@ msgstr ""
msgid "Deploy key was successfully updated."
msgstr ""
+msgid "Deploy keys allow read-only or read-write (if enabled) access to your repository. Deploy keys can be used for CI, staging or production servers. You can create a deploy key or add an existing one."
+msgstr ""
+
msgid "Deploy progress not found. To see pods, ensure your environment matches %{linkStart}deploy board criteria%{linkEnd}."
msgstr ""
@@ -6926,6 +7146,9 @@ msgstr ""
msgid "Deploying to"
msgstr ""
+msgid "Deployment Frequency"
+msgstr ""
+
msgid "Deployment|API"
msgstr ""
@@ -7444,9 +7667,6 @@ msgstr ""
msgid "Edit environment"
msgstr ""
-msgid "Edit epic description"
-msgstr ""
-
msgid "Edit file"
msgstr ""
@@ -8068,9 +8288,6 @@ msgstr ""
msgid "Epics"
msgstr ""
-msgid "Epics (Ultimate / Gold license only)"
-msgstr ""
-
msgid "Epics Roadmap"
msgstr ""
@@ -8080,6 +8297,9 @@ msgstr ""
msgid "Epics let you manage your portfolio of projects more efficiently and with less effort"
msgstr ""
+msgid "Epics, Issues, and Merge Requests"
+msgstr ""
+
msgid "Epics|Add an epic"
msgstr ""
@@ -8131,6 +8351,9 @@ msgstr ""
msgid "Epics|Something went wrong while fetching group epics."
msgstr ""
+msgid "Epics|Something went wrong while moving item."
+msgstr ""
+
msgid "Epics|Something went wrong while ordering item."
msgstr ""
@@ -8185,6 +8408,9 @@ msgstr ""
msgid "Error fetching network graph."
msgstr ""
+msgid "Error fetching payload data."
+msgstr ""
+
msgid "Error fetching projects"
msgstr ""
@@ -8194,9 +8420,6 @@ msgstr ""
msgid "Error fetching the dependency list. Please check your network connection and try again."
msgstr ""
-msgid "Error fetching usage ping data."
-msgstr ""
-
msgid "Error loading branch data. Please try again."
msgstr ""
@@ -8338,6 +8561,9 @@ msgstr ""
msgid "Errors"
msgstr ""
+msgid "Errors:"
+msgstr ""
+
msgid "Estimated"
msgstr ""
@@ -8542,12 +8768,18 @@ msgstr ""
msgid "Export as CSV"
msgstr ""
+msgid "Export group"
+msgstr ""
+
msgid "Export issues"
msgstr ""
msgid "Export project"
msgstr ""
+msgid "Export this group with all related data to a new GitLab instance. Once complete, you can import the data file from the \"New Group\" page."
+msgstr ""
+
msgid "Export this project with all its related data in order to move your project to a new GitLab instance. Once the export is finished, you can import the file from the \"New Project\" page."
msgstr ""
@@ -8617,9 +8849,6 @@ msgstr ""
msgid "Failed Jobs"
msgstr ""
-msgid "Failed create wiki"
-msgstr ""
-
msgid "Failed to add a Zoom meeting"
msgstr ""
@@ -8659,6 +8888,9 @@ msgstr ""
msgid "Failed to create resources"
msgstr ""
+msgid "Failed to create wiki"
+msgstr ""
+
msgid "Failed to delete board. Please try again."
msgstr ""
@@ -8668,7 +8900,7 @@ msgstr ""
msgid "Failed to enqueue the rebase operation, possibly due to a long-lived transaction. Try again later."
msgstr ""
-msgid "Failed to find import label for jira import."
+msgid "Failed to find import label for Jira import."
msgstr ""
msgid "Failed to get ref."
@@ -8692,6 +8924,12 @@ msgstr ""
msgid "Failed to load groups & users."
msgstr ""
+msgid "Failed to load labels. Please try again."
+msgstr ""
+
+msgid "Failed to load milestones. Please try again."
+msgstr ""
+
msgid "Failed to load related branches"
msgstr ""
@@ -9067,7 +9305,10 @@ msgstr ""
msgid "Filter by milestone name"
msgstr ""
-msgid "Filter by name..."
+msgid "Filter by name"
+msgstr ""
+
+msgid "Filter by status"
msgstr ""
msgid "Filter by two-factor authentication"
@@ -9079,12 +9320,18 @@ msgstr ""
msgid "Filter projects"
msgstr ""
+msgid "Filter results"
+msgstr ""
+
msgid "Filter results by group"
msgstr ""
msgid "Filter results by project"
msgstr ""
+msgid "Filter results..."
+msgstr ""
+
msgid "Filter your projects by name"
msgstr ""
@@ -9373,10 +9620,10 @@ msgstr ""
msgid "GeoNodes|Checksummed"
msgstr ""
-msgid "GeoNodes|Container repositories"
+msgid "GeoNodes|Consult Geo troubleshooting information"
msgstr ""
-msgid "GeoNodes|Data is out of date from %{timeago}"
+msgid "GeoNodes|Container repositories"
msgstr ""
msgid "GeoNodes|Data replication lag"
@@ -9418,6 +9665,9 @@ msgstr ""
msgid "GeoNodes|Last event ID seen from primary"
msgstr ""
+msgid "GeoNodes|Learn more about Geo node statuses"
+msgstr ""
+
msgid "GeoNodes|Loading nodes"
msgstr ""
@@ -9433,6 +9683,9 @@ msgstr ""
msgid "GeoNodes|Node was successfully removed."
msgstr ""
+msgid "GeoNodes|Node's status was updated %{timeAgo}."
+msgstr ""
+
msgid "GeoNodes|Not checksummed"
msgstr ""
@@ -9463,7 +9716,10 @@ msgstr ""
msgid "GeoNodes|Repository verification progress"
msgstr ""
-msgid "GeoNodes|Selective"
+msgid "GeoNodes|Selective (%{syncLabel})"
+msgstr ""
+
+msgid "GeoNodes|Selective synchronization"
msgstr ""
msgid "GeoNodes|Something went wrong while changing node status"
@@ -9490,6 +9746,9 @@ msgstr ""
msgid "GeoNodes|Unverified"
msgstr ""
+msgid "GeoNodes|Updated %{timeAgo}"
+msgstr ""
+
msgid "GeoNodes|Used slots"
msgstr ""
@@ -9529,16 +9788,16 @@ msgstr ""
msgid "Geo|All"
msgstr ""
-msgid "Geo|All projects"
+msgid "Geo|All %{replicable_type}"
msgstr ""
-msgid "Geo|All projects are being scheduled for re-sync"
+msgid "Geo|All projects"
msgstr ""
-msgid "Geo|All projects are being scheduled for re-verify"
+msgid "Geo|All projects are being scheduled for resync"
msgstr ""
-msgid "Geo|Batch operations"
+msgid "Geo|All projects are being scheduled for reverify"
msgstr ""
msgid "Geo|Could not remove tracking entry for an existing project."
@@ -9550,9 +9809,15 @@ msgstr ""
msgid "Geo|Failed"
msgstr ""
+msgid "Geo|Filter by status"
+msgstr ""
+
msgid "Geo|Geo Status"
msgstr ""
+msgid "Geo|In progress"
+msgstr ""
+
msgid "Geo|In sync"
msgstr ""
@@ -9577,9 +9842,6 @@ msgstr ""
msgid "Geo|Not synced yet"
msgstr ""
-msgid "Geo|Pending"
-msgstr ""
-
msgid "Geo|Pending synchronization"
msgstr ""
@@ -9616,7 +9878,7 @@ msgstr ""
msgid "Geo|Resync"
msgstr ""
-msgid "Geo|Resync all projects"
+msgid "Geo|Resync all"
msgstr ""
msgid "Geo|Retry count"
@@ -9625,7 +9887,7 @@ msgstr ""
msgid "Geo|Reverify"
msgstr ""
-msgid "Geo|Reverify all projects"
+msgid "Geo|Reverify all"
msgstr ""
msgid "Geo|Status"
@@ -9967,9 +10229,6 @@ msgstr ""
msgid "Go to file"
msgstr ""
-msgid "Go to file (MRs only)"
-msgstr ""
-
msgid "Go to file permalink (while viewing a file)"
msgstr ""
@@ -10414,6 +10673,9 @@ msgstr ""
msgid "GroupSettings|Disable group mentions"
msgstr ""
+msgid "GroupSettings|Export group"
+msgstr ""
+
msgid "GroupSettings|If the parent group's visibility is lower than the group current visibility, visibility levels for subgroups and projects will be changed to match the new parent group's visibility."
msgstr ""
@@ -10492,6 +10754,9 @@ msgstr ""
msgid "Groups (%{count})"
msgstr ""
+msgid "Groups (%{groups})"
+msgstr ""
+
msgid "Groups can also be nested by creating %{subgroup_docs_link_start}subgroups%{subgroup_docs_link_end}."
msgstr ""
@@ -10757,15 +11022,12 @@ msgstr ""
msgid "ID:"
msgstr ""
-msgid "IDE|Allow live previews of JavaScript projects in the Web IDE using CodeSandbox client side evaluation."
+msgid "IDE|Allow live previews of JavaScript projects in the Web IDE using CodeSandbox Live Preview."
msgstr ""
msgid "IDE|Back"
msgstr ""
-msgid "IDE|Client side evaluation"
-msgstr ""
-
msgid "IDE|Commit"
msgstr ""
@@ -10829,6 +11091,9 @@ msgstr ""
msgid "If any job surpasses this timeout threshold, it will be marked as failed. Human readable time input language is accepted like \"1 hour\". Values without specification represent seconds."
msgstr ""
+msgid "If blank, set allowable lifetime to %{instance_level_policy_in_words}, as defined by the instance admin. Once set, existing tokens for users in this group may be revoked."
+msgstr ""
+
msgid "If checked, group owners can manage LDAP group links and LDAP member overrides"
msgstr ""
@@ -10868,6 +11133,9 @@ msgstr ""
msgid "If you lose your recovery codes you can generate new ones, invalidating all previous codes."
msgstr ""
+msgid "If you reach 100%% storage capacity, you will not be able to: %{base_message}"
+msgstr ""
+
msgid "If your HTTP repository is not publicly accessible, add your credentials."
msgstr ""
@@ -10979,6 +11247,9 @@ msgstr ""
msgid "Import repository"
msgstr ""
+msgid "Import started by: %{importInitiator}"
+msgstr ""
+
msgid "Import tasks"
msgstr ""
@@ -11045,6 +11316,9 @@ msgstr ""
msgid "In order to tailor your experience with GitLab we<br>would like to know a bit more about you."
msgstr ""
+msgid "In progress"
+msgstr ""
+
msgid "In the next step, you'll be able to select the projects you want to import."
msgstr ""
@@ -11392,15 +11666,15 @@ msgstr ""
msgid "Issues"
msgstr ""
-msgid "Issues / Merge Requests"
-msgstr ""
-
msgid "Issues Analytics"
msgstr ""
msgid "Issues Rate Limits"
msgstr ""
+msgid "Issues and Merge Requests"
+msgstr ""
+
msgid "Issues can be bugs, tasks or ideas to be discussed. Also, issues are searchable and filterable."
msgstr ""
@@ -11422,10 +11696,10 @@ msgstr ""
msgid "IssuesAnalytics|Avg/Month:"
msgstr ""
-msgid "IssuesAnalytics|Issues created"
+msgid "IssuesAnalytics|Issues opened"
msgstr ""
-msgid "IssuesAnalytics|Issues created per month"
+msgid "IssuesAnalytics|Issues opened per month"
msgstr ""
msgid "IssuesAnalytics|Last 12 months"
@@ -11479,6 +11753,9 @@ msgstr ""
msgid "Jira integration not configured."
msgstr ""
+msgid "Jira project: %{importProject}"
+msgstr ""
+
msgid "JiraService|Events for %{noteable_model_name} are disabled."
msgstr ""
@@ -11868,6 +12145,9 @@ msgstr ""
msgid "Lead"
msgstr ""
+msgid "Lead Time"
+msgstr ""
+
msgid "Learn GitLab"
msgstr ""
@@ -11880,6 +12160,9 @@ msgstr ""
msgid "Learn how to %{no_packages_link_start}publish and share your packages%{no_packages_link_end} with GitLab."
msgstr ""
+msgid "Learn how to enable synchronization"
+msgstr ""
+
msgid "Learn more"
msgstr ""
@@ -12211,9 +12494,6 @@ msgstr ""
msgid "Live preview"
msgstr ""
-msgid "Load more vulnerabilities"
-msgstr ""
-
msgid "Loading"
msgstr ""
@@ -12364,9 +12644,6 @@ msgstr ""
msgid "Manage"
msgstr ""
-msgid "Manage Git repositories with fine-grained access controls that keep your code secure. Perform code reviews and enhance collaboration with merge requests. Each project can also have an issue tracker and a wiki."
-msgstr ""
-
msgid "Manage Web IDE features"
msgstr ""
@@ -12397,6 +12674,9 @@ msgstr ""
msgid "Manage your license"
msgstr ""
+msgid "Managed Account"
+msgstr ""
+
msgid "Manifest"
msgstr ""
@@ -12661,6 +12941,9 @@ msgstr ""
msgid "Merge Requests in Review"
msgstr ""
+msgid "Merge automatically (%{strategy})"
+msgstr ""
+
msgid "Merge commit message"
msgstr ""
@@ -12814,6 +13097,12 @@ msgstr ""
msgid "Merged branches are being deleted. This can take some time depending on the number of branches. Please refresh the page to see changes."
msgstr ""
+msgid "Merged this merge request."
+msgstr ""
+
+msgid "Merges this merge request immediately."
+msgstr ""
+
msgid "Merges this merge request when the pipeline succeeds."
msgstr ""
@@ -12921,12 +13210,15 @@ msgid_plural "Metrics|Edit metrics"
msgstr[0] ""
msgstr[1] ""
-msgid "Metrics|Environment"
+msgid "Metrics|Expand panel"
msgstr ""
msgid "Metrics|For grouping similar metrics"
msgstr ""
+msgid "Metrics|Go back (Esc)"
+msgstr ""
+
msgid "Metrics|Invalid time range, please verify."
msgstr ""
@@ -12957,9 +13249,6 @@ msgstr ""
msgid "Metrics|Refresh dashboard"
msgstr ""
-msgid "Metrics|Show last"
-msgstr ""
-
msgid "Metrics|There was an error creating the dashboard."
msgstr ""
@@ -13038,6 +13327,9 @@ msgstr ""
msgid "Microsoft Azure"
msgstr ""
+msgid "Middleman project with Static Site Editor support"
+msgstr ""
+
msgid "Migrated %{success_count}/%{total_count} files."
msgstr ""
@@ -13483,10 +13775,10 @@ msgstr ""
msgid "Next"
msgstr ""
-msgid "Next file in diff (MRs only)"
+msgid "Next file in diff"
msgstr ""
-msgid "Next unresolved discussion (MRs only)"
+msgid "Next unresolved discussion"
msgstr ""
msgid "Nickname"
@@ -13546,6 +13838,9 @@ msgstr ""
msgid "No changes between %{ref_start}%{source_branch}%{ref_end} and %{ref_start}%{target_branch}%{ref_end}"
msgstr ""
+msgid "No child epics match applied filters"
+msgstr ""
+
msgid "No connection could be made to a Gitaly Server, please check your logs!"
msgstr ""
@@ -13684,15 +13979,6 @@ msgstr ""
msgid "No thanks, don't show this again"
msgstr ""
-msgid "No vulnerabilities found for this group"
-msgstr ""
-
-msgid "No vulnerabilities found for this pipeline"
-msgstr ""
-
-msgid "No vulnerabilities found for this project"
-msgstr ""
-
msgid "No vulnerabilities present"
msgstr ""
@@ -13816,6 +14102,9 @@ msgstr ""
msgid "Nothing to preview."
msgstr ""
+msgid "Nothing to synchronize"
+msgstr ""
+
msgid "Notification events"
msgstr ""
@@ -13987,6 +14276,9 @@ msgstr ""
msgid "Once removed, the fork relationship cannot be restored and you will no longer be able to send merge requests to the source."
msgstr ""
+msgid "Once the exported file is ready you can download it from this page."
+msgstr ""
+
msgid "Once the exported file is ready, you will receive a notification email with a download link, or you can download it from this page."
msgstr ""
@@ -14088,9 +14380,6 @@ msgstr ""
msgid "Open sidebar"
msgstr ""
-msgid "Open source software to collaborate on code"
-msgstr ""
-
msgid "Open: %{openIssuesCount}"
msgstr ""
@@ -14193,6 +14482,9 @@ msgstr ""
msgid "OutdatedBrowser|You can provide feedback %{feedback_link_start}on this issue%{feedback_link_end} or via your usual support channels."
msgstr ""
+msgid "Overridden"
+msgstr ""
+
msgid "Overview"
msgstr ""
@@ -14244,6 +14536,9 @@ msgstr ""
msgid "PackageRegistry|Conan Command"
msgstr ""
+msgid "PackageRegistry|Copy .pypirc content"
+msgstr ""
+
msgid "PackageRegistry|Copy Conan Command"
msgstr ""
@@ -14265,6 +14560,9 @@ msgstr ""
msgid "PackageRegistry|Copy NuGet Setup Command"
msgstr ""
+msgid "PackageRegistry|Copy Pip command"
+msgstr ""
+
msgid "PackageRegistry|Copy and paste this inside your %{codeStart}pom.xml%{codeEnd} %{codeStart}dependencies%{codeEnd} block."
msgstr ""
@@ -14298,6 +14596,12 @@ msgstr ""
msgid "PackageRegistry|For more information on the NuGet registry, %{linkStart}see the documentation%{linkEnd}."
msgstr ""
+msgid "PackageRegistry|For more information on the PyPi registry, %{linkStart}see the documentation%{linkEnd}."
+msgstr ""
+
+msgid "PackageRegistry|If you haven't already done so, you will need to add the below to your %{codeStart}.pypirc%{codeEnd} file."
+msgstr ""
+
msgid "PackageRegistry|If you haven't already done so, you will need to add the below to your %{codeStart}pom.xml%{codeEnd} file."
msgstr ""
@@ -14307,6 +14611,9 @@ msgstr ""
msgid "PackageRegistry|Learn how to %{noPackagesLinkStart}publish and share your packages%{noPackagesLinkEnd} with GitLab."
msgstr ""
+msgid "PackageRegistry|Manually Published"
+msgstr ""
+
msgid "PackageRegistry|Maven"
msgstr ""
@@ -14325,12 +14632,18 @@ msgstr ""
msgid "PackageRegistry|NuGet Command"
msgstr ""
+msgid "PackageRegistry|Pip Command"
+msgstr ""
+
msgid "PackageRegistry|Pipeline %{linkStart}%{linkEnd} triggered %{timestamp} by %{author}"
msgstr ""
msgid "PackageRegistry|Published to the repository at %{timestamp}"
msgstr ""
+msgid "PackageRegistry|PyPi"
+msgstr ""
+
msgid "PackageRegistry|Registry Setup"
msgstr ""
@@ -14367,6 +14680,9 @@ msgstr ""
msgid "PackageRegistry|npm"
msgstr ""
+msgid "PackageRegistry|published by %{author}"
+msgstr ""
+
msgid "PackageRegistry|yarn"
msgstr ""
@@ -14382,6 +14698,9 @@ msgstr ""
msgid "PackageType|NuGet"
msgstr ""
+msgid "PackageType|PyPi"
+msgstr ""
+
msgid "Packages"
msgstr ""
@@ -14475,6 +14794,9 @@ msgstr ""
msgid "Past due"
msgstr ""
+msgid "Paste a machine public key here. Read more about how to generate it"
+msgstr ""
+
msgid "Paste a machine public key here. Read more about how to generate it %{link_start}here%{link_end}"
msgstr ""
@@ -14490,9 +14812,6 @@ msgstr ""
msgid "Path"
msgstr ""
-msgid "Path, transfer, remove"
-msgstr ""
-
msgid "Path:"
msgstr ""
@@ -14523,7 +14842,7 @@ msgstr ""
msgid "Percentage"
msgstr ""
-msgid "Perform advanced options such as changing path, transferring, or removing the group."
+msgid "Perform advanced options such as changing path, transferring, exporting, or removing the group."
msgstr ""
msgid "Perform common operations on GitLab project"
@@ -14604,9 +14923,6 @@ msgstr ""
msgid "Pipeline minutes quota"
msgstr ""
-msgid "Pipeline quota"
-msgstr ""
-
msgid "Pipeline subscriptions"
msgstr ""
@@ -14616,6 +14932,9 @@ msgstr ""
msgid "Pipeline: %{status}"
msgstr ""
+msgid "PipelineCharts|CI / CD Analytics"
+msgstr ""
+
msgid "PipelineCharts|Failed:"
msgstr ""
@@ -14694,15 +15013,6 @@ msgstr ""
msgid "Pipelines settings for '%{project_name}' were successfully updated."
msgstr ""
-msgid "Pipelines| to purchase more minutes."
-msgstr ""
-
-msgid "Pipelines|%{namespace_name} has exceeded its pipeline minutes quota."
-msgstr ""
-
-msgid "Pipelines|%{namespace_name} has less than %{notification_level}%% of CI minutes available."
-msgstr ""
-
msgid "Pipelines|API"
msgstr ""
@@ -14724,10 +15034,13 @@ msgstr ""
msgid "Pipelines|Get started with Pipelines"
msgstr ""
-msgid "Pipelines|Loading Pipelines"
+msgid "Pipelines|Group %{namespace_name} has %{percentage}%% or less Shared Runner Pipeline minutes remaining. Once it runs out, no new jobs or pipelines in its projects will run."
msgstr ""
-msgid "Pipelines|Pipelines will not run anymore on shared Runners."
+msgid "Pipelines|Group %{namespace_name} has exceeded its pipeline minutes quota. Unless you buy additional pipeline minutes, no new jobs or pipelines in its projects will run."
+msgstr ""
+
+msgid "Pipelines|Loading Pipelines"
msgstr ""
msgid "Pipelines|Project cache successfully reset."
@@ -14958,6 +15271,9 @@ msgstr ""
msgid "Please select"
msgstr ""
+msgid "Please select a Jira project"
+msgstr ""
+
msgid "Please select a country"
msgstr ""
@@ -15003,6 +15319,9 @@ msgstr ""
msgid "Please wait while we import the repository for you. Refresh at will."
msgstr ""
+msgid "Plugins directory is deprecated and will be removed in 14.0. Please move this file into /file_hooks directory."
+msgstr ""
+
msgid "Pod does not exist"
msgstr ""
@@ -15120,7 +15439,7 @@ msgstr ""
msgid "Prevent users from changing their profile name"
msgstr ""
-msgid "Prevent users from modifing merge request approvers list"
+msgid "Prevent users from modifying merge request approvers list"
msgstr ""
msgid "Prevent users from performing write operations on GitLab while performing maintenance."
@@ -15141,10 +15460,10 @@ msgstr ""
msgid "Previous Artifacts"
msgstr ""
-msgid "Previous file in diff (MRs only)"
+msgid "Previous file in diff"
msgstr ""
-msgid "Previous unresolved discussion (MRs only)"
+msgid "Previous unresolved discussion"
msgstr ""
msgid "Primary"
@@ -15177,6 +15496,9 @@ msgstr ""
msgid "Private profile"
msgstr ""
+msgid "Private projects Minutes cost factor"
+msgstr ""
+
msgid "Private projects can be created in your personal namespace with:"
msgstr ""
@@ -16005,6 +16327,9 @@ msgstr ""
msgid "ProjectSettings|With GitLab Pages you can host your static websites on GitLab"
msgstr ""
+msgid "ProjectSettings|With Metrics Dashboard you can visualize this project performance metrics"
+msgstr ""
+
msgid "ProjectTemplates|.NET Core"
msgstr ""
@@ -16068,6 +16393,9 @@ msgstr ""
msgid "ProjectTemplates|Spring"
msgstr ""
+msgid "ProjectTemplates|Static Site Editor/Middleman"
+msgstr ""
+
msgid "ProjectTemplates|iOS (Swift)"
msgstr ""
@@ -16287,13 +16615,13 @@ msgstr ""
msgid "PrometheusService|Prometheus is being automatically managed on your clusters"
msgstr ""
-msgid "PrometheusService|These metrics will only be monitored after your first deployment to an environment"
+msgid "PrometheusService|Select the Active checkbox to override the Auto Configuration with custom settings. If unchecked, Auto Configuration settings are used."
msgstr ""
-msgid "PrometheusService|Time-series monitoring service"
+msgid "PrometheusService|These metrics will only be monitored after your first deployment to an environment"
msgstr ""
-msgid "PrometheusService|To enable manual configuration, uninstall Prometheus from your clusters"
+msgid "PrometheusService|Time-series monitoring service"
msgstr ""
msgid "PrometheusService|To enable the installation of Prometheus on your clusters, deactivate the manual configuration below"
@@ -16479,6 +16807,9 @@ msgstr ""
msgid "Protip:"
msgstr ""
+msgid "Protocol"
+msgstr ""
+
msgid "Provider"
msgstr ""
@@ -16503,6 +16834,9 @@ msgstr ""
msgid "Public pipelines"
msgstr ""
+msgid "Public projects Minutes cost factor"
+msgstr ""
+
msgid "Pull"
msgstr ""
@@ -16703,6 +17037,9 @@ msgid_plural "Refreshing in %d seconds to show the updated status..."
msgstr[0] ""
msgstr[1] ""
+msgid "Regenerate export"
+msgstr ""
+
msgid "Regenerate instance ID"
msgstr ""
@@ -17080,9 +17417,29 @@ msgstr ""
msgid "Reports|%{combinedString} and %{resolvedString}"
msgstr ""
+msgid "Reports|Accessibility scanning detected %d issue for the source branch only"
+msgid_plural "Reports|Accessibility scanning detected %d issues for the source branch only"
+msgstr[0] ""
+msgstr[1] ""
+
+msgid "Reports|Accessibility scanning detected no issues for the source branch only"
+msgstr ""
+
+msgid "Reports|Accessibility scanning failed loading results"
+msgstr ""
+
+msgid "Reports|Accessibility scanning results are being parsed"
+msgstr ""
+
msgid "Reports|Actions"
msgstr ""
+msgid "Reports|An error occured while loading report"
+msgstr ""
+
+msgid "Reports|An error occurred while loading %{name} results"
+msgstr ""
+
msgid "Reports|Class"
msgstr ""
@@ -17188,7 +17545,7 @@ msgstr ""
msgid "Requested %{time_ago}"
msgstr ""
-msgid "Requested design version does not exist"
+msgid "Requested design version does not exist."
msgstr ""
msgid "Requested states are invalid"
@@ -17372,6 +17729,9 @@ msgstr ""
msgid "Resync"
msgstr ""
+msgid "Resync all"
+msgstr ""
+
msgid "Resync all %{replicableType}"
msgstr ""
@@ -17440,6 +17800,9 @@ msgstr ""
msgid "Revoked personal access token %{personal_access_token_name}!"
msgstr ""
+msgid "Revoked project access token %{project_access_token_name}!"
+msgstr ""
+
msgid "RightSidebar|adding a"
msgstr ""
@@ -17488,6 +17851,9 @@ msgstr ""
msgid "Runner token"
msgstr ""
+msgid "Runner tokens"
+msgstr ""
+
msgid "Runner was not updated."
msgstr ""
@@ -17542,6 +17908,9 @@ msgstr ""
msgid "SAML SSO for %{group_name}"
msgstr ""
+msgid "SAML discovery tokens"
+msgstr ""
+
msgid "SAML for %{group_name}"
msgstr ""
@@ -17626,12 +17995,18 @@ msgstr ""
msgid "Scheduled"
msgstr ""
+msgid "Scheduled to merge this merge request (%{strategy})."
+msgstr ""
+
msgid "Scheduled to merge this merge request when the pipeline succeeds."
msgstr ""
msgid "Schedules"
msgstr ""
+msgid "Schedules to merge this merge request (%{strategy})."
+msgstr ""
+
msgid "Scheduling"
msgstr ""
@@ -17689,6 +18064,9 @@ msgstr ""
msgid "Search branches and tags"
msgstr ""
+msgid "Search by author"
+msgstr ""
+
msgid "Search files"
msgstr ""
@@ -17834,11 +18212,6 @@ msgid_plural "SearchResults|snippets"
msgstr[0] ""
msgstr[1] ""
-msgid "SearchResults|snippet result"
-msgid_plural "SearchResults|snippet results"
-msgstr[0] ""
-msgstr[1] ""
-
msgid "SearchResults|user"
msgid_plural "SearchResults|users"
msgstr[0] ""
@@ -17849,6 +18222,9 @@ msgid_plural "SearchResults|wiki results"
msgstr[0] ""
msgstr[1] ""
+msgid "Searching by both author and message is currently not supported."
+msgstr ""
+
msgid "Seat Link"
msgstr ""
@@ -17876,184 +18252,232 @@ msgstr ""
msgid "Security Dashboard"
msgstr ""
-msgid "Security Dashboard|Error fetching the vulnerability counts. Please check your network connection and try again."
+msgid "Security configuration help link"
+msgstr ""
+
+msgid "Security dashboard"
msgstr ""
-msgid "Security Dashboard|Error fetching the vulnerability list. Please check your network connection and try again."
+msgid "Security report is out of date. Please update your branch with the latest changes from the target branch (%{targetBranchName})"
msgstr ""
-msgid "Security Dashboard|Issue Created"
+msgid "Security report is out of date. Run %{newPipelineLinkStart}a new pipeline%{newPipelineLinkEnd} for the target branch (%{targetBranchName})"
msgstr ""
-msgid "Security Reports|Comment added to '%{vulnerabilityName}'"
+msgid "SecurityConfiguration|Configured"
msgstr ""
-msgid "Security Reports|Comment deleted on '%{vulnerabilityName}'"
+msgid "SecurityConfiguration|Feature"
msgstr ""
-msgid "Security Reports|Comment edited on '%{vulnerabilityName}'"
+msgid "SecurityConfiguration|Feature documentation for %{featureName}"
msgstr ""
-msgid "Security Reports|Create issue"
+msgid "SecurityConfiguration|Not yet configured"
msgstr ""
-msgid "Security Reports|Dismiss vulnerability"
+msgid "SecurityConfiguration|Secure features"
msgstr ""
-msgid "Security Reports|Dismissed '%{vulnerabilityName}'"
+msgid "SecurityConfiguration|Status"
msgstr ""
-msgid "Security Reports|Dismissed '%{vulnerabilityName}'. Turn off the hide dismissed toggle to view."
+msgid "SecurityReports|%{firstProject} and %{secondProject}"
msgstr ""
-msgid "Security Reports|Either you don't have permission to view this dashboard or the dashboard has not been setup. Please check your permission settings with your administrator or check your dashboard configurations to proceed."
+msgid "SecurityReports|%{firstProject}, %{secondProject}, and %{rest}"
msgstr ""
-msgid "Security Reports|Learn more about setting up your dashboard"
+msgid "SecurityReports|Add a project to your dashboard"
msgstr ""
-msgid "Security Reports|More info"
+msgid "SecurityReports|Add or remove projects from your dashboard"
msgstr ""
-msgid "Security Reports|Oops, something doesn't seem right."
+msgid "SecurityReports|Add projects"
msgstr ""
-msgid "Security Reports|Security reports can only be accessed by authorized users."
+msgid "SecurityReports|Comment added to '%{vulnerabilityName}'"
msgstr ""
-msgid "Security Reports|There was an error adding the comment."
+msgid "SecurityReports|Comment deleted on '%{vulnerabilityName}'"
msgstr ""
-msgid "Security Reports|There was an error creating the issue."
+msgid "SecurityReports|Comment edited on '%{vulnerabilityName}'"
msgstr ""
-msgid "Security Reports|There was an error creating the merge request."
+msgid "SecurityReports|Create issue"
msgstr ""
-msgid "Security Reports|There was an error deleting the comment."
+msgid "SecurityReports|Dismiss Selected"
msgstr ""
-msgid "Security Reports|There was an error dismissing the vulnerabilities."
+msgid "SecurityReports|Dismiss vulnerability"
msgstr ""
-msgid "Security Reports|There was an error dismissing the vulnerability."
+msgid "SecurityReports|Dismissed '%{vulnerabilityName}'"
msgstr ""
-msgid "Security Reports|There was an error reverting the dismissal."
+msgid "SecurityReports|Dismissed '%{vulnerabilityName}'. Turn off the hide dismissed toggle to view."
msgstr ""
-msgid "Security Reports|There was an error reverting this dismissal."
+msgid "SecurityReports|Each vulnerability now has a unique page that can be directly linked to, shared, referenced, and tracked as the single source of truth. Vulnerability occurrences also persist across scanner runs, which improves tracking and visibility and reduces duplicates between scans."
msgstr ""
-msgid "Security Reports|Undo dismiss"
+msgid "SecurityReports|Edit dashboard"
msgstr ""
-msgid "Security Reports|You do not have sufficient permissions to access this report"
+msgid "SecurityReports|Either you don't have permission to view this dashboard or the dashboard has not been setup. Please check your permission settings with your administrator or check your dashboard configurations to proceed."
msgstr ""
-msgid "Security Reports|You must sign in as an authorized user to see this report"
+msgid "SecurityReports|Error fetching the vulnerability counts. Please check your network connection and try again."
msgstr ""
-msgid "Security configuration help link"
+msgid "SecurityReports|Error fetching the vulnerability list. Please check your network connection and try again."
msgstr ""
-msgid "Security dashboard"
+msgid "SecurityReports|False positive"
msgstr ""
-msgid "Security report is out of date. Please update your branch with the latest changes from the target branch (%{targetBranchName})"
+msgid "SecurityReports|Hide dismissed"
msgstr ""
-msgid "Security report is out of date. Run %{newPipelineLinkStart}a new pipeline%{newPipelineLinkEnd} for the target branch (%{targetBranchName})"
+msgid "SecurityReports|Introducing standalone vulnerabilities"
msgstr ""
-msgid "SecurityConfiguration|Configured"
+msgid "SecurityReports|Issue Created"
msgstr ""
-msgid "SecurityConfiguration|Feature"
+msgid "SecurityReports|Learn More"
msgstr ""
-msgid "SecurityConfiguration|Feature documentation for %{featureName}"
+msgid "SecurityReports|Learn more about setting up your dashboard"
msgstr ""
-msgid "SecurityConfiguration|Not yet configured"
+msgid "SecurityReports|Load more vulnerabilities"
msgstr ""
-msgid "SecurityConfiguration|Secure features"
+msgid "SecurityReports|Monitor vulnerabilities in your code"
msgstr ""
-msgid "SecurityConfiguration|Status"
+msgid "SecurityReports|More info"
msgstr ""
-msgid "SecurityDashboard|%{firstProject} and %{secondProject}"
+msgid "SecurityReports|More information"
msgstr ""
-msgid "SecurityDashboard|%{firstProject}, %{secondProject}, and %{rest}"
+msgid "SecurityReports|No vulnerabilities found for dashboard"
msgstr ""
-msgid "SecurityDashboard|Add a project to your dashboard"
+msgid "SecurityReports|No vulnerabilities found for this group"
msgstr ""
-msgid "SecurityDashboard|Add or remove projects from your dashboard"
+msgid "SecurityReports|No vulnerabilities found for this pipeline"
msgstr ""
-msgid "SecurityDashboard|Add projects"
+msgid "SecurityReports|No vulnerabilities found for this project"
msgstr ""
-msgid "SecurityDashboard|Edit dashboard"
+msgid "SecurityReports|Oops, something doesn't seem right."
msgstr ""
-msgid "SecurityDashboard|Hide dismissed"
+msgid "SecurityReports|Pipeline %{pipelineLink} triggered %{timeago} by %{user}"
msgstr ""
-msgid "SecurityDashboard|Monitor vulnerabilities in your code"
+msgid "SecurityReports|Project"
msgstr ""
-msgid "SecurityDashboard|More information"
+msgid "SecurityReports|Projects added"
msgstr ""
-msgid "SecurityDashboard|No vulnerabilities found for dashboard"
+msgid "SecurityReports|Remove project from dashboard"
msgstr ""
-msgid "SecurityDashboard|Pipeline %{pipelineLink} triggered %{timeago} by %{user}"
+msgid "SecurityReports|Report type"
msgstr ""
-msgid "SecurityDashboard|Project"
+msgid "SecurityReports|Return to dashboard"
msgstr ""
-msgid "SecurityDashboard|Projects added"
+msgid "SecurityReports|Security Dashboard"
msgstr ""
-msgid "SecurityDashboard|Remove project from dashboard"
+msgid "SecurityReports|Security reports can only be accessed by authorized users."
msgstr ""
-msgid "SecurityDashboard|Report type"
+msgid "SecurityReports|Select a project to add by using the project search field above."
msgstr ""
-msgid "SecurityDashboard|Return to dashboard"
+msgid "SecurityReports|Select a reason"
msgstr ""
-msgid "SecurityDashboard|Security Dashboard"
+msgid "SecurityReports|Severity"
msgstr ""
-msgid "SecurityDashboard|Select a project to add by using the project search field above."
+msgid "SecurityReports|Status"
msgstr ""
-msgid "SecurityDashboard|Severity"
+msgid "SecurityReports|The security dashboard displays the latest security findings for projects you wish to monitor. Select \"Edit dashboard\" to add and remove projects."
msgstr ""
-msgid "SecurityDashboard|Status"
+msgid "SecurityReports|The security dashboard displays the latest security report. Use it to find and fix vulnerabilities."
msgstr ""
-msgid "SecurityDashboard|The security dashboard displays the latest security findings for projects you wish to monitor. Select \"Edit dashboard\" to add and remove projects."
+msgid "SecurityReports|There was an error adding the comment."
msgstr ""
-msgid "SecurityDashboard|The security dashboard displays the latest security report. Use it to find and fix vulnerabilities."
+msgid "SecurityReports|There was an error creating the issue."
msgstr ""
-msgid "SecurityDashboard|There was an error while generating the report."
+msgid "SecurityReports|There was an error creating the merge request."
msgstr ""
-msgid "SecurityDashboard|Unable to add %{invalidProjects}"
+msgid "SecurityReports|There was an error deleting the comment."
+msgstr ""
+
+msgid "SecurityReports|There was an error dismissing the vulnerabilities."
+msgstr ""
+
+msgid "SecurityReports|There was an error dismissing the vulnerability."
+msgstr ""
+
+msgid "SecurityReports|There was an error reverting the dismissal."
+msgstr ""
+
+msgid "SecurityReports|There was an error reverting this dismissal."
+msgstr ""
+
+msgid "SecurityReports|There was an error while generating the report."
+msgstr ""
+
+msgid "SecurityReports|Unable to add %{invalidProjects}"
+msgstr ""
+
+msgid "SecurityReports|Undo dismiss"
+msgstr ""
+
+msgid "SecurityReports|While it's rare to have no vulnerabilities for your group, it can happen. In any event, we ask that you double check your settings to make sure you've set up your dashboard correctly."
+msgstr ""
+
+msgid "SecurityReports|While it's rare to have no vulnerabilities for your pipeline, it can happen. In any event, we ask that you double check your settings to make sure all security scanning jobs have passed successfully."
+msgstr ""
+
+msgid "SecurityReports|While it's rare to have no vulnerabilities for your project, it can happen. In any event, we ask that you double check your settings to make sure you've set up your dashboard correctly."
+msgstr ""
+
+msgid "SecurityReports|While it's rare to have no vulnerabilities, it can happen. In any event, we ask that you please double check your settings to make sure you've set up your dashboard correctly."
+msgstr ""
+
+msgid "SecurityReports|Won't fix / Accept risk"
+msgstr ""
+
+msgid "SecurityReports|You do not have sufficient permissions to access this report"
+msgstr ""
+
+msgid "SecurityReports|You must sign in as an authorized user to see this report"
+msgstr ""
+
+msgid "SecurityReports|[No reason]"
msgstr ""
msgid "See GitLab's %{password_policy_guidelines}"
@@ -18065,6 +18489,9 @@ msgstr ""
msgid "See the affected projects in the GitLab admin panel"
msgstr ""
+msgid "See what's new at GitLab"
+msgstr ""
+
msgid "Select"
msgstr ""
@@ -18470,9 +18897,6 @@ msgstr ""
msgid "Set up Jira Integration"
msgstr ""
-msgid "Set up Jira Integration illustration"
-msgstr ""
-
msgid "Set up a %{type} Runner automatically"
msgstr ""
@@ -18557,6 +18981,9 @@ msgstr ""
msgid "Severity: %{severity}"
msgstr ""
+msgid "Shards (%{shards})"
+msgstr ""
+
msgid "Shards to synchronize"
msgstr ""
@@ -18871,6 +19298,9 @@ msgstr ""
msgid "Snowplow"
msgstr ""
+msgid "Some child epics may be hidden due to applied filters"
+msgstr ""
+
msgid "Some email servers do not support overriding the email sender name. Enable this option to include the name of the author of the issue, merge request or comment in the email body instead."
msgstr ""
@@ -19012,6 +19442,9 @@ msgstr ""
msgid "Something went wrong while updating your list settings"
msgstr ""
+msgid "Something went wrong with your automatic subscription renewal"
+msgstr ""
+
msgid "Something went wrong, unable to add %{project} to dashboard"
msgstr ""
@@ -19297,6 +19730,9 @@ msgstr ""
msgid "Stage removed"
msgstr ""
+msgid "Standard"
+msgstr ""
+
msgid "Star a label to make it a priority label. Order the prioritized labels to change their relative priority, by dragging."
msgstr ""
@@ -19420,6 +19856,9 @@ msgstr ""
msgid "Static Application Security Testing (SAST)"
msgstr ""
+msgid "StaticSiteEditor|An error occurred while submitting your changes."
+msgstr ""
+
msgid "StaticSiteEditor|Branch could not be created."
msgstr ""
@@ -19429,18 +19868,30 @@ msgstr ""
msgid "StaticSiteEditor|Could not create merge request."
msgstr ""
+msgid "StaticSiteEditor|Incompatible file content"
+msgstr ""
+
msgid "StaticSiteEditor|Return to site"
msgstr ""
+msgid "StaticSiteEditor|Static site editor"
+msgstr ""
+
msgid "StaticSiteEditor|Success!"
msgstr ""
msgid "StaticSiteEditor|Summary of changes"
msgstr ""
+msgid "StaticSiteEditor|The Static Site Editor is currently configured to only edit Markdown content on pages generated from Middleman. Visit the documentation to learn more about configuring your site to use the Static Site Editor."
+msgstr ""
+
msgid "StaticSiteEditor|Update %{sourcePath} file"
msgstr ""
+msgid "StaticSiteEditor|View documentation"
+msgstr ""
+
msgid "StaticSiteEditor|View merge request"
msgstr ""
@@ -19498,6 +19949,12 @@ msgstr ""
msgid "StatusPage|Status page"
msgstr ""
+msgid "StatusPage|Status page URL"
+msgstr ""
+
+msgid "StatusPage|Status page frontend documentation"
+msgstr ""
+
msgid "StatusPage|To publish incidents to an external status page, GitLab will store a JSON file in your Amazon S3 account in a location accessible to your external status page service. Make sure to also set up %{docsLink}"
msgstr ""
@@ -19846,6 +20303,12 @@ msgstr ""
msgid "Synced"
msgstr ""
+msgid "Synchronization disabled"
+msgstr ""
+
+msgid "Synchronization of container repositories is disabled."
+msgstr ""
+
msgid "System"
msgstr ""
@@ -19999,6 +20462,9 @@ msgstr ""
msgid "Telephone number"
msgstr ""
+msgid "Telephone number (Optional)"
+msgstr ""
+
msgid "Template"
msgstr ""
@@ -20305,6 +20771,9 @@ msgstr ""
msgid "The license was successfully uploaded and is now active. You can see the details below."
msgstr ""
+msgid "The license was successfully uploaded and will be active from %{starts_at}. You can see the details below."
+msgstr ""
+
msgid "The maximum file size allowed is %{size}."
msgstr ""
@@ -20626,6 +21095,9 @@ msgstr ""
msgid "There was an error getting the epic participants."
msgstr ""
+msgid "There was an error importing the Jira project."
+msgstr ""
+
msgid "There was an error loading users activity calendar."
msgstr ""
@@ -20641,7 +21113,7 @@ msgstr ""
msgid "There was an error resetting user pipeline minutes."
msgstr ""
-msgid "There was an error saving this Geo Node"
+msgid "There was an error saving this Geo Node."
msgstr ""
msgid "There was an error saving your changes."
@@ -20704,6 +21176,9 @@ msgstr ""
msgid "They can be managed using the %{link}."
msgstr ""
+msgid "Third Party Advisory Link"
+msgstr ""
+
msgid "Third party offers"
msgstr ""
@@ -20740,6 +21215,9 @@ msgstr ""
msgid "This application will be able to:"
msgstr ""
+msgid "This attachment has been truncated to avoid exceeding the maximum allowed attachment size of 15MB. %{written_count} of %{issues_count} issues have been included. Consider re-exporting with a narrower selection of issues."
+msgstr ""
+
msgid "This block is self-referential"
msgstr ""
@@ -20881,6 +21359,12 @@ msgstr ""
msgid "This issue is confidential"
msgstr ""
+msgid "This issue is currently blocked by the following issues: %{issues}."
+msgstr ""
+
+msgid "This issue is in a child epic of the filtered epic"
+msgstr ""
+
msgid "This issue is locked."
msgstr ""
@@ -21079,7 +21563,10 @@ msgstr ""
msgid "This user will be the author of all events in the activity feed that are the result of an update, like new branches being created or new commits being pushed to existing branches. Upon creation or when reassigning you can only assign yourself to be the mirror user."
msgstr ""
-msgid "This variable can not be masked"
+msgid "This variable can not be masked."
+msgstr ""
+
+msgid "This variable does not match the expected pattern."
msgstr ""
msgid "This will help us personalize your onboarding experience."
@@ -21109,10 +21596,10 @@ msgstr ""
msgid "ThreatMonitoring|Application firewall not detected"
msgstr ""
-msgid "ThreatMonitoring|Container Network Policy"
+msgid "ThreatMonitoring|Container Network Policies are not installed or have been disabled. To view this data, ensure your Network Policies are installed and enabled for your cluster."
msgstr ""
-msgid "ThreatMonitoring|Container NetworkPolicies are not installed or has been disabled. To view this data, ensure you NetworkPolicies are installed and enabled for your cluster."
+msgid "ThreatMonitoring|Container Network Policy"
msgstr ""
msgid "ThreatMonitoring|Container NetworkPolicies not detected"
@@ -21145,7 +21632,7 @@ msgstr ""
msgid "ThreatMonitoring|Something went wrong, unable to fetch statistics"
msgstr ""
-msgid "ThreatMonitoring|The firewall is not installed or has been disabled. To view this data, ensure you firewall is installed and enabled for your cluster."
+msgid "ThreatMonitoring|The firewall is not installed or has been disabled. To view this data, ensure the web application firewall is installed and enabled for your cluster."
msgstr ""
msgid "ThreatMonitoring|The graph below is an overview of traffic coming to your application as tracked by the Web Application Firewall (WAF). View the docs for instructions on how to access the WAF logs to see what type of malicious traffic is trying to access your app. The docs link is also accessible by clicking the \"?\" icon next to the title below."
@@ -21214,6 +21701,9 @@ msgstr ""
msgid "Time in seconds GitLab will wait for a response from the external service. When the service does not respond in time, access will be denied."
msgstr ""
+msgid "Time of import: %{importTime}"
+msgstr ""
+
msgid "Time remaining"
msgstr ""
@@ -21398,7 +21888,7 @@ msgstr ""
msgid "Title:"
msgstr ""
-msgid "Titles and Filenames"
+msgid "Titles and Descriptions"
msgstr ""
msgid "To"
@@ -21605,7 +22095,7 @@ msgstr ""
msgid "Too many projects enabled. You will need to manage them via the console or the API."
msgstr ""
-msgid "Topics"
+msgid "Topics (optional)"
msgstr ""
msgid "Total"
@@ -21767,6 +22257,9 @@ msgstr ""
msgid "Try all GitLab has to offer for 30 days."
msgstr ""
+msgid "Try changing or removing filters."
+msgstr ""
+
msgid "Try to fork again"
msgstr ""
@@ -21860,6 +22353,9 @@ msgstr ""
msgid "Unable to connect to server: %{error}"
msgstr ""
+msgid "Unable to connect to the Jira instance. Please check your Jira integration configuration."
+msgstr ""
+
msgid "Unable to convert Kubernetes logs encoding to UTF-8"
msgstr ""
@@ -22803,6 +23299,9 @@ msgstr ""
msgid "View issue"
msgstr ""
+msgid "View issues"
+msgstr ""
+
msgid "View it on GitLab"
msgstr ""
@@ -22980,6 +23479,15 @@ msgstr ""
msgid "VulnerabilityManagement|Resolved %{timeago} by %{user}"
msgstr ""
+msgid "VulnerabilityManagement|Something went wrong while trying to delete the comment. Please try again later."
+msgstr ""
+
+msgid "VulnerabilityManagement|Something went wrong while trying to retrieve the vulnerability history. Please try again later."
+msgstr ""
+
+msgid "VulnerabilityManagement|Something went wrong while trying to save the comment. Please try again later."
+msgstr ""
+
msgid "VulnerabilityManagement|Something went wrong, could not create an issue."
msgstr ""
@@ -23010,6 +23518,9 @@ msgstr ""
msgid "VulnerabilityStatusTypes|Resolved"
msgstr ""
+msgid "Vulnerability|%{scannerName} (version %{scannerVersion})"
+msgstr ""
+
msgid "Vulnerability|Class"
msgstr ""
@@ -23043,7 +23554,10 @@ msgstr ""
msgid "Vulnerability|Project"
msgstr ""
-msgid "Vulnerability|Report Type"
+msgid "Vulnerability|Scanner Provider"
+msgstr ""
+
+msgid "Vulnerability|Scanner Type"
msgstr ""
msgid "Vulnerability|Severity"
@@ -23097,9 +23611,15 @@ msgstr ""
msgid "We sent you an email with reset password instructions"
msgstr ""
+msgid "We tried to automatically renew your %{strong}%{plan_name}%{strong_close} subscription for %{strong}%{namespace_name}%{strong_close} on %{expires_on} but something went wrong so your subscription was downgraded to the free plan. Don't worry, your data is safe. We suggest you check your payment method and get in touch with our support team (%{support_link}). They'll gladly help with your subscription renewal."
+msgstr ""
+
msgid "We want to be sure it is you, please confirm you are not a robot."
msgstr ""
+msgid "We will automatically renew your %{strong}%{plan_name}%{strong_close} subscription for %{strong}%{namespace_name}%{strong_close} on %{strong}%{expires_on}%{strong_close}. There's nothing that you need to do, we'll let you know when the renewal is complete. Need more seats, a higher plan or just want to review your payment method?"
+msgstr ""
+
msgid "We've found no vulnerabilities"
msgstr ""
@@ -23204,15 +23724,6 @@ msgstr ""
msgid "When:"
msgstr ""
-msgid "While it's rare to have no vulnerabilities for your group, it can happen. In any event, we ask that you double check your settings to make sure you've set up your dashboard correctly."
-msgstr ""
-
-msgid "While it's rare to have no vulnerabilities for your pipeline, it can happen. In any event, we ask that you double check your settings to make sure all security scanning jobs have passed successfully."
-msgstr ""
-
-msgid "While it's rare to have no vulnerabilities for your project, it can happen. In any event, we ask that you double check your settings to make sure you've set up your dashboard correctly."
-msgstr ""
-
msgid "While it's rare to have no vulnerabilities, it can happen. In any event, we ask that you please double check your settings to make sure you've set up your dashboard correctly."
msgstr ""
@@ -23243,9 +23754,6 @@ msgstr ""
msgid "Wiki"
msgstr ""
-msgid "Wiki pages"
-msgstr ""
-
msgid "Wiki was successfully updated."
msgstr ""
@@ -23429,6 +23937,9 @@ msgstr ""
msgid "Yes, add it"
msgstr ""
+msgid "Yes, close issue"
+msgstr ""
+
msgid "Yes, let me map Google Code users to full names or GitLab users."
msgstr ""
@@ -23624,6 +24135,9 @@ msgstr ""
msgid "You could not create a new trigger."
msgstr ""
+msgid "You didn't renew your %{strong}%{plan_name}%{strong_close} subscription for %{strong}%{namespace_name}%{strong_close} so it was downgraded to the free plan."
+msgstr ""
+
msgid "You didn't renew your %{strong}%{plan_name}%{strong_close} subscription so it was downgraded to the GitLab Core Plan."
msgstr ""
@@ -23690,6 +24204,9 @@ msgstr ""
msgid "You have declined the invitation to join %{label}."
msgstr ""
+msgid "You have imported from this project %{numberOfPreviousImportsForProject} times before. Each new import will create duplicate issues."
+msgstr ""
+
msgid "You have no permissions"
msgstr ""
@@ -23771,6 +24288,9 @@ msgstr ""
msgid "You need to upload a Google Takeout archive."
msgstr ""
+msgid "You reached %{usage_in_percent} of %{namespace_name}'s capacity (%{used_storage} of %{storage_limit})"
+msgstr ""
+
msgid "You tried to fork %{link_to_the_project} but it failed for the following reason:"
msgstr ""
@@ -23855,9 +24375,21 @@ msgstr ""
msgid "YouTube"
msgstr ""
+msgid "Your %{strong}%{plan_name}%{strong_close} subscription for %{strong}%{namespace_name}%{strong_close} will expire on %{strong}%{expires_on}%{strong_close}. After that, you will not to be able to create issues or merge requests as well as many other features."
+msgstr ""
+
msgid "Your %{strong}%{plan_name}%{strong_close} subscription will expire on %{strong}%{expires_on}%{strong_close}. After that, you will not to be able to create issues or merge requests as well as many other features."
msgstr ""
+msgid "Your CSV export has started. It will be emailed to %{email} when complete."
+msgstr ""
+
+msgid "Your CSV export of %{issues_count} from project %{project_link} has been added to this email as an attachment."
+msgstr ""
+
+msgid "Your CSV export of %{written_count} from project %{project_name} (%{project_url}) has been added to this email as an attachment."
+msgstr ""
+
msgid "Your Commit Email will be used for web based operations, such as edits and merges."
msgstr ""
@@ -23954,6 +24486,9 @@ msgstr ""
msgid "Your comment could not be updated! Please check your network connection and try again."
msgstr ""
+msgid "Your comment will be discarded."
+msgstr ""
+
msgid "Your custom stage '%{title}' was created"
msgstr ""
@@ -23993,6 +24528,9 @@ msgstr ""
msgid "Your new personal access token has been created."
msgstr ""
+msgid "Your new project access token has been created."
+msgstr ""
+
msgid "Your password isn't required to view this page. If a password or any other personal details are requested, please contact your administrator to report abuse."
msgstr ""
@@ -24011,12 +24549,18 @@ msgstr ""
msgid "Your request for access has been queued for review."
msgstr ""
+msgid "Your search didn't match any commits."
+msgstr ""
+
msgid "Your subscription expired!"
msgstr ""
msgid "Your subscription has been downgraded"
msgstr ""
+msgid "Your subscription will automatically renew in %{remaining_days}"
+msgstr ""
+
msgid "Your subscription will expire in %{remaining_days}"
msgstr ""
@@ -24055,7 +24599,7 @@ msgstr ""
msgid "allowed to fail"
msgstr ""
-msgid "already being used for another group or project milestone."
+msgid "already being used for another group or project %{timebox_name}."
msgstr ""
msgid "already has a \"created\" issue link"
@@ -24133,6 +24677,9 @@ msgstr ""
msgid "ciReport|%{linkStartTag}Learn more about SAST %{linkEndTag}"
msgstr ""
+msgid "ciReport|%{linkStartTag}Learn more about Secret Scanning %{linkEndTag}"
+msgstr ""
+
msgid "ciReport|%{linkStartTag}Learn more about codequality reports %{linkEndTag}"
msgstr ""
@@ -24296,6 +24843,12 @@ msgstr ""
msgid "ciReport|SAST"
msgstr ""
+msgid "ciReport|Secret scanning"
+msgstr ""
+
+msgid "ciReport|Secret scanning detects secrets and credentials vulnerabilities in your source code."
+msgstr ""
+
msgid "ciReport|Security scanning"
msgstr ""
@@ -24493,6 +25046,9 @@ msgstr ""
msgid "group"
msgstr ""
+msgid "groups"
+msgstr ""
+
msgid "has already been linked to another vulnerability"
msgstr ""
@@ -24656,9 +25212,6 @@ msgstr[1] ""
msgid "merged %{time_ago}"
msgstr ""
-msgid "milestone should belong either to a project or a group."
-msgstr ""
-
msgid "missing"
msgstr ""
@@ -24995,6 +25548,9 @@ msgstr ""
msgid "no contributions"
msgstr ""
+msgid "no expiration"
+msgstr ""
+
msgid "no one can merge"
msgstr ""
@@ -25047,6 +25603,9 @@ msgstr ""
msgid "pending removal"
msgstr ""
+msgid "per day"
+msgstr ""
+
msgid "pipeline"
msgstr ""
@@ -25075,6 +25634,12 @@ msgstr[1] ""
msgid "project avatar"
msgstr ""
+msgid "projects"
+msgstr ""
+
+msgid "push to your repository, create pipelines, create issues or add comments. To reduce storage capacity, delete unused repositories, artifacts, wikis, issues, and pipelines."
+msgstr ""
+
msgid "quick actions"
msgstr ""
@@ -25221,11 +25786,6 @@ msgstr ""
msgid "this document"
msgstr ""
-msgid "thread resolved"
-msgid_plural "threads resolved"
-msgstr[0] ""
-msgstr[1] ""
-
msgid "to help your contributors communicate effectively!"
msgstr ""
@@ -25280,6 +25840,9 @@ msgstr ""
msgid "view the blob"
msgstr ""
+msgid "vulnerability|Add a comment"
+msgstr ""
+
msgid "vulnerability|Add a comment or reason for dismissal"
msgstr ""
diff --git a/locale/nl_NL/gitlab.po b/locale/nl_NL/gitlab.po
index ccf7d8dd411..97baedb5c41 100644
--- a/locale/nl_NL/gitlab.po
+++ b/locale/nl_NL/gitlab.po
@@ -12,7 +12,7 @@ msgstr ""
"X-Crowdin-Project: gitlab-ee\n"
"X-Crowdin-Language: nl\n"
"X-Crowdin-File: /master/locale/gitlab.pot\n"
-"PO-Revision-Date: 2020-04-15 00:31\n"
+"PO-Revision-Date: 2020-05-05 21:35\n"
msgid " %{start} to %{end}"
msgstr ""
@@ -212,6 +212,11 @@ msgid_plural "%d tags"
msgstr[0] ""
msgstr[1] ""
+msgid "%d unresolved thread"
+msgid_plural "%d unresolved threads"
+msgstr[0] ""
+msgstr[1] ""
+
msgid "%d vulnerability dismissed"
msgid_plural "%d vulnerabilities dismissed"
msgstr[0] ""
@@ -371,6 +376,9 @@ msgstr ""
msgid "%{mrText}, this issue will be closed automatically."
msgstr ""
+msgid "%{namespace_name} is now read-only. You cannot: %{base_message}"
+msgstr ""
+
msgid "%{name} contained %{resultsString}"
msgstr ""
@@ -383,6 +391,11 @@ msgstr ""
msgid "%{name}'s avatar"
msgstr "%{name};'s avatar"
+msgid "%{no_of_days} day"
+msgid_plural "%{no_of_days} days"
+msgstr[0] ""
+msgstr[1] ""
+
msgid "%{number_commits_behind} commits behind %{default_branch}, %{number_commits_ahead} commits ahead"
msgstr "%{number_commits_behind} commits achter %{default_branch}, %{number_commits_ahead} commits voor"
@@ -506,6 +519,9 @@ msgstr[1] "%{text}%{files} bestanden"
msgid "%{text} is available"
msgstr "%{text} is beschikbaar"
+msgid "%{timebox_name} should belong either to a project or a group."
+msgstr ""
+
msgid "%{title} %{operator} %{threshold}"
msgstr ""
@@ -654,8 +670,8 @@ msgstr[1] ""
msgid "1 closed issue"
msgid_plural "%{issues} closed issues"
-msgstr[0] ""
-msgstr[1] ""
+msgstr[0] "1 gesloten issue"
+msgstr[1] "%{issues} gesloten issues"
msgid "1 closed merge request"
msgid_plural "%{merge_requests} closed merge requests"
@@ -781,12 +797,18 @@ msgstr ""
msgid "<code>\"johnsmith@example.com\": \"johnsmith@example.com\"</code> will add \"By <a href=\"#\">johnsmith@example.com</a>\" to all issues and comments originally created by johnsmith@example.com. By default, the email address or username is masked to ensure the user's privacy. Use this option if you want to show the full email address."
msgstr ""
+msgid "<namespace / project>"
+msgstr ""
+
msgid "<no name set>"
msgstr ""
msgid "<no scopes selected>"
msgstr ""
+msgid "<project name>"
+msgstr ""
+
msgid "<strong>%{group_name}</strong> group members"
msgstr ""
@@ -820,6 +842,9 @@ msgstr ""
msgid "A basic page and serverless function that uses AWS Lambda, AWS API Gateway, and GitLab Pages"
msgstr ""
+msgid "A complete DevOps platform"
+msgstr ""
+
msgid "A default branch cannot be chosen for an empty project."
msgstr ""
@@ -1036,6 +1061,12 @@ msgstr ""
msgid "AccessTokens|reset it"
msgstr ""
+msgid "AccessibilityReport|Accessibility report artifact not found"
+msgstr ""
+
+msgid "AccessibilityReport|Failed to retrieve accessibility report"
+msgstr ""
+
msgid "AccessibilityReport|Learn More"
msgstr ""
@@ -1045,6 +1076,9 @@ msgstr ""
msgid "AccessibilityReport|New"
msgstr ""
+msgid "AccessibilityReport|The accessibility scanning found an error of the following type: %{code}"
+msgstr ""
+
msgid "Account"
msgstr "Account"
@@ -1287,6 +1321,9 @@ msgstr ""
msgid "Added at"
msgstr ""
+msgid "Added for this merge request"
+msgstr ""
+
msgid "Added in this version"
msgstr ""
@@ -1658,6 +1695,72 @@ msgid_plural "Alerts"
msgstr[0] ""
msgstr[1] ""
+msgid "Alert Details"
+msgstr ""
+
+msgid "AlertManagement|Acknowledged"
+msgstr ""
+
+msgid "AlertManagement|Alert"
+msgstr ""
+
+msgid "AlertManagement|Authorize external service"
+msgstr ""
+
+msgid "AlertManagement|Display alerts from all your monitoring tools directly within GitLab. Streamline the investigation of your alerts and the escalation of alerts to incidents."
+msgstr ""
+
+msgid "AlertManagement|End time"
+msgstr ""
+
+msgid "AlertManagement|End time:"
+msgstr ""
+
+msgid "AlertManagement|Events"
+msgstr ""
+
+msgid "AlertManagement|Events:"
+msgstr ""
+
+msgid "AlertManagement|Full Alert Details"
+msgstr ""
+
+msgid "AlertManagement|More information"
+msgstr ""
+
+msgid "AlertManagement|No alerts available to display. If you think you're seeing this message in error, refresh the page."
+msgstr ""
+
+msgid "AlertManagement|No alerts to display."
+msgstr ""
+
+msgid "AlertManagement|Overview"
+msgstr ""
+
+msgid "AlertManagement|Resolved"
+msgstr ""
+
+msgid "AlertManagement|Severity"
+msgstr ""
+
+msgid "AlertManagement|Start time"
+msgstr ""
+
+msgid "AlertManagement|Start time:"
+msgstr ""
+
+msgid "AlertManagement|Status"
+msgstr ""
+
+msgid "AlertManagement|Surface alerts in GitLab"
+msgstr ""
+
+msgid "AlertManagement|There was an error displaying the alerts. Confirm your endpoint's configuration details to ensure alerts appear."
+msgstr ""
+
+msgid "AlertManagement|Triggered"
+msgstr ""
+
msgid "AlertService|%{linkStart}Learn more%{linkEnd} about configuring this endpoint to receive alerts."
msgstr ""
@@ -1727,6 +1830,9 @@ msgstr ""
msgid "All security scans are enabled because %{linkStart}Auto DevOps%{linkEnd} is enabled on this project"
msgstr ""
+msgid "All threads resolved"
+msgstr ""
+
msgid "All users"
msgstr ""
@@ -1745,6 +1851,9 @@ msgstr ""
msgid "Allow only the selected protocols to be used for Git access."
msgstr ""
+msgid "Allow owners to manage default branch protection per group"
+msgstr ""
+
msgid "Allow owners to manually add users outside of LDAP"
msgstr ""
@@ -1853,6 +1962,9 @@ msgstr ""
msgid "An error occurred fetching the dropdown data."
msgstr ""
+msgid "An error occurred fetching the project authors."
+msgstr ""
+
msgid "An error occurred previewing the blob"
msgstr ""
@@ -1934,6 +2046,9 @@ msgstr ""
msgid "An error occurred while fetching sidebar data"
msgstr ""
+msgid "An error occurred while fetching terraform reports."
+msgstr ""
+
msgid "An error occurred while fetching the Service Desk address."
msgstr ""
@@ -2171,6 +2286,9 @@ msgstr ""
msgid "Any"
msgstr ""
+msgid "Any Author"
+msgstr ""
+
msgid "Any Label"
msgstr ""
@@ -2386,10 +2504,10 @@ msgstr ""
msgid "Are you sure that you want to unarchive this project?"
msgstr ""
-msgid "Are you sure you want to cancel creating this comment?"
+msgid "Are you sure you want to cancel editing this comment?"
msgstr ""
-msgid "Are you sure you want to cancel editing this comment?"
+msgid "Are you sure you want to close this blocked issue?"
msgstr ""
msgid "Are you sure you want to delete %{name}?"
@@ -2419,6 +2537,9 @@ msgstr ""
msgid "Are you sure you want to deploy this environment?"
msgstr ""
+msgid "Are you sure you want to discard this comment?"
+msgstr ""
+
msgid "Are you sure you want to erase this build?"
msgstr ""
@@ -2595,6 +2716,9 @@ msgstr ""
msgid "At least one approval from a code owner is required to change files matching the respective CODEOWNER rules."
msgstr ""
+msgid "At least one logging option is required to be enabled"
+msgstr ""
+
msgid "At least one of group_id or project_id must be specified"
msgstr ""
@@ -2633,6 +2757,27 @@ msgstr ""
msgid "AuditEvents|Target"
msgstr ""
+msgid "AuditLogs|(removed)"
+msgstr ""
+
+msgid "AuditLogs|Action"
+msgstr ""
+
+msgid "AuditLogs|Author"
+msgstr ""
+
+msgid "AuditLogs|Date"
+msgstr ""
+
+msgid "AuditLogs|IP Address"
+msgstr ""
+
+msgid "AuditLogs|Object"
+msgstr ""
+
+msgid "AuditLogs|Target"
+msgstr ""
+
msgid "Aug"
msgstr ""
@@ -2915,9 +3060,6 @@ msgstr ""
msgid "BambooService|You must set up automatic revision labeling and a repository trigger in Bamboo."
msgstr ""
-msgid "Batch operations"
-msgstr ""
-
msgid "BatchComments|Delete all pending comments"
msgstr ""
@@ -3047,6 +3189,9 @@ msgstr ""
msgid "Boards"
msgstr ""
+msgid "Boards and Board Lists"
+msgstr ""
+
msgid "Boards|Collapse"
msgstr ""
@@ -3266,6 +3411,9 @@ msgstr ""
msgid "Buy GitLab Enterprise Edition"
msgstr ""
+msgid "Buy more Pipeline minutes"
+msgstr ""
+
msgid "By %{user_name}"
msgstr ""
@@ -3386,6 +3534,9 @@ msgstr ""
msgid "Can't scan the code?"
msgstr ""
+msgid "Can't update snippet: %{err}"
+msgstr ""
+
msgid "Canary"
msgstr ""
@@ -3554,6 +3705,9 @@ msgstr ""
msgid "Changing group path can have unintended side effects."
msgstr ""
+msgid "Charts"
+msgstr ""
+
msgid "Charts can't be displayed as the request for data has timed out. %{documentationLink}"
msgstr ""
@@ -3590,9 +3744,6 @@ msgstr ""
msgid "ChatMessage|and [%{count} more](%{pipeline_failed_jobs_url})"
msgstr ""
-msgid "ChatMessage|failed"
-msgstr ""
-
msgid "ChatMessage|has failed"
msgstr ""
@@ -3608,9 +3759,6 @@ msgstr ""
msgid "ChatMessage|in %{project_link}"
msgstr ""
-msgid "ChatMessage|passed"
-msgstr ""
-
msgid "Check again"
msgstr ""
@@ -4061,9 +4209,6 @@ msgstr ""
msgid "Click any <strong>project name</strong> in the project list below to navigate to the project milestone."
msgstr ""
-msgid "Click here"
-msgstr ""
-
msgid "Click the <strong>Download</strong> button and wait for downloading to complete."
msgstr ""
@@ -4451,6 +4596,12 @@ msgstr ""
msgid "ClusterIntegration|Fetching zones"
msgstr ""
+msgid "ClusterIntegration|Fluentd"
+msgstr ""
+
+msgid "ClusterIntegration|Fluentd is an open source data collector, which lets you unify the data collection and consumption for a better use and understanding of data. It requires at least one of the following logs to be successfully installed."
+msgstr ""
+
msgid "ClusterIntegration|GitLab Integration"
msgstr ""
@@ -4757,6 +4908,15 @@ msgstr ""
msgid "ClusterIntegration|Request to begin uninstalling failed"
msgstr ""
+msgid "ClusterIntegration|SIEM Hostname"
+msgstr ""
+
+msgid "ClusterIntegration|SIEM Port"
+msgstr ""
+
+msgid "ClusterIntegration|SIEM Protocol"
+msgstr ""
+
msgid "ClusterIntegration|Save changes"
msgstr ""
@@ -4853,6 +5013,12 @@ msgstr ""
msgid "ClusterIntegration|Select zone to choose machine type"
msgstr ""
+msgid "ClusterIntegration|Send Cilium Logs"
+msgstr ""
+
+msgid "ClusterIntegration|Send ModSecurity Logs"
+msgstr ""
+
msgid "ClusterIntegration|Service Token"
msgstr ""
@@ -5281,18 +5447,33 @@ msgstr ""
msgid "Compliance framework (optional)"
msgstr ""
+msgid "ComplianceFramework|GDPR"
+msgstr ""
+
msgid "ComplianceFramework|GDPR - General Data Protection Regulation"
msgstr ""
+msgid "ComplianceFramework|HIPAA"
+msgstr ""
+
msgid "ComplianceFramework|HIPAA - Health Insurance Portability and Accountability Act"
msgstr ""
+msgid "ComplianceFramework|PCI-DSS"
+msgstr ""
+
msgid "ComplianceFramework|PCI-DSS - Payment Card Industry-Data Security Standard"
msgstr ""
+msgid "ComplianceFramework|SOC 2"
+msgstr ""
+
msgid "ComplianceFramework|SOC 2 - Service Organization Control 2"
msgstr ""
+msgid "ComplianceFramework|SOX"
+msgstr ""
+
msgid "ComplianceFramework|SOX - Sarbanes-Oxley"
msgstr ""
@@ -5440,9 +5621,15 @@ msgstr ""
msgid "Container repositories sync capacity"
msgstr ""
+msgid "ContainerRegistry| Please visit the %{linkStart}administration settings%{linkEnd} to enable this feature."
+msgstr ""
+
msgid "ContainerRegistry|%{imageName} tags"
msgstr ""
+msgid "ContainerRegistry|%{title} was successfully scheduled for deletion"
+msgstr ""
+
msgid "ContainerRegistry|Automatically remove extra images that aren't designed to be kept."
msgstr ""
@@ -5455,6 +5642,9 @@ msgstr ""
msgid "ContainerRegistry|Container Registry"
msgstr ""
+msgid "ContainerRegistry|Container Registry tag expiration and retention policy is disabled"
+msgstr ""
+
msgid "ContainerRegistry|Copy build command"
msgstr ""
@@ -5464,18 +5654,12 @@ msgstr ""
msgid "ContainerRegistry|Copy push command"
msgstr ""
-msgid "ContainerRegistry|Currently, the Container Registry tag expiration feature is not available for projects created before GitLab version 12.8. For updates and more information, visit Issue %{linkStart}#196124%{linkEnd}"
-msgstr ""
-
msgid "ContainerRegistry|Docker connection error"
msgstr ""
msgid "ContainerRegistry|Docker tag expiration policy is %{toggleStatus}"
msgstr ""
-msgid "ContainerRegistry|Docker tags with names matching this regex pattern will expire:"
-msgstr ""
-
msgid "ContainerRegistry|Edit Settings"
msgstr ""
@@ -5497,9 +5681,6 @@ msgstr ""
msgid "ContainerRegistry|Image ID"
msgstr ""
-msgid "ContainerRegistry|Image deleted successfully"
-msgstr ""
-
msgid "ContainerRegistry|Keep and protect the images that matter most."
msgstr ""
@@ -5515,12 +5696,24 @@ msgstr ""
msgid "ContainerRegistry|Number of tags to retain:"
msgstr ""
+msgid "ContainerRegistry|Please contact your administrator."
+msgstr ""
+
msgid "ContainerRegistry|Push an image"
msgstr ""
msgid "ContainerRegistry|Quick Start"
msgstr ""
+msgid "ContainerRegistry|Regular expressions such as %{codeStart}.*-test%{codeEnd} or %{codeStart}dev-.*%{codeEnd} are supported"
+msgstr ""
+
+msgid "ContainerRegistry|Regular expressions such as %{codeStart}.*-test%{codeEnd} or %{codeStart}dev-.*%{codeEnd} are supported. To select all tags, use %{codeStart}.*%{codeEnd}"
+msgstr ""
+
+msgid "ContainerRegistry|Remember to run %{docLinkStart}garbage collection%{docLinkEnd} to remove the stale data from storage."
+msgstr ""
+
msgid "ContainerRegistry|Remove repository"
msgstr ""
@@ -5535,22 +5728,22 @@ msgstr[1] ""
msgid "ContainerRegistry|Retention policy has been Enabled"
msgstr ""
-msgid "ContainerRegistry|Something went wrong while deleting the image."
+msgid "ContainerRegistry|Something went wrong while fetching the expiration policy."
msgstr ""
-msgid "ContainerRegistry|Something went wrong while deleting the tag."
+msgid "ContainerRegistry|Something went wrong while fetching the repository list."
msgstr ""
-msgid "ContainerRegistry|Something went wrong while deleting the tags."
+msgid "ContainerRegistry|Something went wrong while fetching the tags list."
msgstr ""
-msgid "ContainerRegistry|Something went wrong while fetching the expiration policy."
+msgid "ContainerRegistry|Something went wrong while marking the tag for deletion."
msgstr ""
-msgid "ContainerRegistry|Something went wrong while fetching the packages list."
+msgid "ContainerRegistry|Something went wrong while marking the tags for deletion."
msgstr ""
-msgid "ContainerRegistry|Something went wrong while fetching the tags list."
+msgid "ContainerRegistry|Something went wrong while scheduling %{title} for deletion. Please try again."
msgstr ""
msgid "ContainerRegistry|Something went wrong while updating the expiration policy."
@@ -5559,16 +5752,25 @@ msgstr ""
msgid "ContainerRegistry|Tag"
msgstr ""
-msgid "ContainerRegistry|Tag deleted successfully"
-msgstr ""
-
msgid "ContainerRegistry|Tag expiration policy"
msgstr ""
msgid "ContainerRegistry|Tag expiration policy is designed to:"
msgstr ""
-msgid "ContainerRegistry|Tags deleted successfully"
+msgid "ContainerRegistry|Tag successfully marked for deletion."
+msgstr ""
+
+msgid "ContainerRegistry|Tags successfully marked for deletion."
+msgstr ""
+
+msgid "ContainerRegistry|Tags with names matching this regex pattern will %{italicStart}be preserved:%{italicEnd}"
+msgstr ""
+
+msgid "ContainerRegistry|Tags with names matching this regex pattern will %{italicStart}expire:%{italicEnd}"
+msgstr ""
+
+msgid "ContainerRegistry|The Container Registry tag expiration and retention policies for this project have not been enabled."
msgstr ""
msgid "ContainerRegistry|The last tag related to this image was recently removed. This empty image and any associated data will be automatically removed as part of the regular Garbage Collection process. If you have any questions, contact your administrator."
@@ -5589,16 +5791,16 @@ msgstr ""
msgid "ContainerRegistry|There are no container images stored for this project"
msgstr ""
-msgid "ContainerRegistry|This Registry contains deleted image tag data. Remember to run %{docLinkStart}garbage collection%{docLinkEnd} to remove the stale data from storage."
+msgid "ContainerRegistry|There was an error during the deletion of this image repository, please try again."
msgstr ""
msgid "ContainerRegistry|This image has no active tags"
msgstr ""
-msgid "ContainerRegistry|We are having trouble connecting to Docker, which could be due to an issue with your project name or path. %{docLinkStart}More Information%{docLinkEnd}"
+msgid "ContainerRegistry|This image repository is scheduled for deletion"
msgstr ""
-msgid "ContainerRegistry|Wildcards such as %{codeStart}.*-stable%{codeEnd} or %{codeStart}production/.*%{codeEnd} are supported. To select all tags, use %{codeStart}.*%{codeEnd}"
+msgid "ContainerRegistry|We are having trouble connecting to Docker, which could be due to an issue with your project name or path. %{docLinkStart}More Information%{docLinkEnd}"
msgstr ""
msgid "ContainerRegistry|With the Container Registry, every project can have its own space to store its Docker images. %{docLinkStart}More Information%{docLinkEnd}"
@@ -5853,7 +6055,7 @@ msgstr ""
msgid "Could not delete chat nickname %{chat_name}."
msgstr ""
-msgid "Could not find design"
+msgid "Could not find design."
msgstr ""
msgid "Could not remove the trigger."
@@ -5868,6 +6070,9 @@ msgstr ""
msgid "Could not revoke personal access token %{personal_access_token_name}."
msgstr ""
+msgid "Could not revoke project access token %{project_access_token_name}."
+msgstr ""
+
msgid "Could not save group ID"
msgstr ""
@@ -5925,6 +6130,9 @@ msgstr ""
msgid "Create a new branch"
msgstr ""
+msgid "Create a new deploy key for this project"
+msgstr ""
+
msgid "Create a new file as there are no files yet. Afterwards, you'll be able to commit your changes."
msgstr ""
@@ -6030,6 +6238,9 @@ msgstr ""
msgid "Create requirement"
msgstr ""
+msgid "Create snippet"
+msgstr ""
+
msgid "Create wildcard: %{searchTerm}"
msgstr ""
@@ -6051,6 +6262,9 @@ msgstr ""
msgid "Created"
msgstr ""
+msgid "Created %{timestamp}"
+msgstr ""
+
msgid "Created At"
msgstr ""
@@ -6407,6 +6621,9 @@ msgstr ""
msgid "CycleAnalytics|stage dropdown"
msgstr ""
+msgid "DAG"
+msgstr ""
+
msgid "DNS"
msgstr ""
@@ -6773,6 +6990,9 @@ msgstr ""
msgid "Deploy key was successfully updated."
msgstr ""
+msgid "Deploy keys allow read-only or read-write (if enabled) access to your repository. Deploy keys can be used for CI, staging or production servers. You can create a deploy key or add an existing one."
+msgstr ""
+
msgid "Deploy progress not found. To see pods, ensure your environment matches %{linkStart}deploy board criteria%{linkEnd}."
msgstr ""
@@ -6926,6 +7146,9 @@ msgstr ""
msgid "Deploying to"
msgstr ""
+msgid "Deployment Frequency"
+msgstr ""
+
msgid "Deployment|API"
msgstr ""
@@ -7444,9 +7667,6 @@ msgstr ""
msgid "Edit environment"
msgstr ""
-msgid "Edit epic description"
-msgstr ""
-
msgid "Edit file"
msgstr ""
@@ -8068,9 +8288,6 @@ msgstr ""
msgid "Epics"
msgstr ""
-msgid "Epics (Ultimate / Gold license only)"
-msgstr ""
-
msgid "Epics Roadmap"
msgstr ""
@@ -8080,6 +8297,9 @@ msgstr ""
msgid "Epics let you manage your portfolio of projects more efficiently and with less effort"
msgstr ""
+msgid "Epics, Issues, and Merge Requests"
+msgstr ""
+
msgid "Epics|Add an epic"
msgstr ""
@@ -8131,6 +8351,9 @@ msgstr ""
msgid "Epics|Something went wrong while fetching group epics."
msgstr ""
+msgid "Epics|Something went wrong while moving item."
+msgstr ""
+
msgid "Epics|Something went wrong while ordering item."
msgstr ""
@@ -8185,6 +8408,9 @@ msgstr ""
msgid "Error fetching network graph."
msgstr ""
+msgid "Error fetching payload data."
+msgstr ""
+
msgid "Error fetching projects"
msgstr ""
@@ -8194,9 +8420,6 @@ msgstr ""
msgid "Error fetching the dependency list. Please check your network connection and try again."
msgstr ""
-msgid "Error fetching usage ping data."
-msgstr ""
-
msgid "Error loading branch data. Please try again."
msgstr ""
@@ -8338,6 +8561,9 @@ msgstr ""
msgid "Errors"
msgstr ""
+msgid "Errors:"
+msgstr ""
+
msgid "Estimated"
msgstr ""
@@ -8542,12 +8768,18 @@ msgstr ""
msgid "Export as CSV"
msgstr ""
+msgid "Export group"
+msgstr ""
+
msgid "Export issues"
msgstr ""
msgid "Export project"
msgstr ""
+msgid "Export this group with all related data to a new GitLab instance. Once complete, you can import the data file from the \"New Group\" page."
+msgstr ""
+
msgid "Export this project with all its related data in order to move your project to a new GitLab instance. Once the export is finished, you can import the file from the \"New Project\" page."
msgstr ""
@@ -8617,9 +8849,6 @@ msgstr ""
msgid "Failed Jobs"
msgstr ""
-msgid "Failed create wiki"
-msgstr ""
-
msgid "Failed to add a Zoom meeting"
msgstr ""
@@ -8659,6 +8888,9 @@ msgstr ""
msgid "Failed to create resources"
msgstr ""
+msgid "Failed to create wiki"
+msgstr ""
+
msgid "Failed to delete board. Please try again."
msgstr ""
@@ -8668,7 +8900,7 @@ msgstr ""
msgid "Failed to enqueue the rebase operation, possibly due to a long-lived transaction. Try again later."
msgstr ""
-msgid "Failed to find import label for jira import."
+msgid "Failed to find import label for Jira import."
msgstr ""
msgid "Failed to get ref."
@@ -8692,6 +8924,12 @@ msgstr ""
msgid "Failed to load groups & users."
msgstr ""
+msgid "Failed to load labels. Please try again."
+msgstr ""
+
+msgid "Failed to load milestones. Please try again."
+msgstr ""
+
msgid "Failed to load related branches"
msgstr ""
@@ -9067,7 +9305,10 @@ msgstr ""
msgid "Filter by milestone name"
msgstr ""
-msgid "Filter by name..."
+msgid "Filter by name"
+msgstr ""
+
+msgid "Filter by status"
msgstr ""
msgid "Filter by two-factor authentication"
@@ -9079,12 +9320,18 @@ msgstr ""
msgid "Filter projects"
msgstr ""
+msgid "Filter results"
+msgstr ""
+
msgid "Filter results by group"
msgstr ""
msgid "Filter results by project"
msgstr ""
+msgid "Filter results..."
+msgstr ""
+
msgid "Filter your projects by name"
msgstr ""
@@ -9373,10 +9620,10 @@ msgstr ""
msgid "GeoNodes|Checksummed"
msgstr ""
-msgid "GeoNodes|Container repositories"
+msgid "GeoNodes|Consult Geo troubleshooting information"
msgstr ""
-msgid "GeoNodes|Data is out of date from %{timeago}"
+msgid "GeoNodes|Container repositories"
msgstr ""
msgid "GeoNodes|Data replication lag"
@@ -9418,6 +9665,9 @@ msgstr ""
msgid "GeoNodes|Last event ID seen from primary"
msgstr ""
+msgid "GeoNodes|Learn more about Geo node statuses"
+msgstr ""
+
msgid "GeoNodes|Loading nodes"
msgstr ""
@@ -9433,6 +9683,9 @@ msgstr ""
msgid "GeoNodes|Node was successfully removed."
msgstr ""
+msgid "GeoNodes|Node's status was updated %{timeAgo}."
+msgstr ""
+
msgid "GeoNodes|Not checksummed"
msgstr ""
@@ -9463,7 +9716,10 @@ msgstr ""
msgid "GeoNodes|Repository verification progress"
msgstr ""
-msgid "GeoNodes|Selective"
+msgid "GeoNodes|Selective (%{syncLabel})"
+msgstr ""
+
+msgid "GeoNodes|Selective synchronization"
msgstr ""
msgid "GeoNodes|Something went wrong while changing node status"
@@ -9490,6 +9746,9 @@ msgstr ""
msgid "GeoNodes|Unverified"
msgstr ""
+msgid "GeoNodes|Updated %{timeAgo}"
+msgstr ""
+
msgid "GeoNodes|Used slots"
msgstr ""
@@ -9529,16 +9788,16 @@ msgstr ""
msgid "Geo|All"
msgstr ""
-msgid "Geo|All projects"
+msgid "Geo|All %{replicable_type}"
msgstr ""
-msgid "Geo|All projects are being scheduled for re-sync"
+msgid "Geo|All projects"
msgstr ""
-msgid "Geo|All projects are being scheduled for re-verify"
+msgid "Geo|All projects are being scheduled for resync"
msgstr ""
-msgid "Geo|Batch operations"
+msgid "Geo|All projects are being scheduled for reverify"
msgstr ""
msgid "Geo|Could not remove tracking entry for an existing project."
@@ -9550,9 +9809,15 @@ msgstr ""
msgid "Geo|Failed"
msgstr ""
+msgid "Geo|Filter by status"
+msgstr ""
+
msgid "Geo|Geo Status"
msgstr ""
+msgid "Geo|In progress"
+msgstr ""
+
msgid "Geo|In sync"
msgstr ""
@@ -9577,9 +9842,6 @@ msgstr ""
msgid "Geo|Not synced yet"
msgstr ""
-msgid "Geo|Pending"
-msgstr ""
-
msgid "Geo|Pending synchronization"
msgstr ""
@@ -9616,7 +9878,7 @@ msgstr ""
msgid "Geo|Resync"
msgstr ""
-msgid "Geo|Resync all projects"
+msgid "Geo|Resync all"
msgstr ""
msgid "Geo|Retry count"
@@ -9625,7 +9887,7 @@ msgstr ""
msgid "Geo|Reverify"
msgstr ""
-msgid "Geo|Reverify all projects"
+msgid "Geo|Reverify all"
msgstr ""
msgid "Geo|Status"
@@ -9967,9 +10229,6 @@ msgstr ""
msgid "Go to file"
msgstr ""
-msgid "Go to file (MRs only)"
-msgstr ""
-
msgid "Go to file permalink (while viewing a file)"
msgstr ""
@@ -10414,6 +10673,9 @@ msgstr ""
msgid "GroupSettings|Disable group mentions"
msgstr ""
+msgid "GroupSettings|Export group"
+msgstr ""
+
msgid "GroupSettings|If the parent group's visibility is lower than the group current visibility, visibility levels for subgroups and projects will be changed to match the new parent group's visibility."
msgstr ""
@@ -10492,6 +10754,9 @@ msgstr ""
msgid "Groups (%{count})"
msgstr ""
+msgid "Groups (%{groups})"
+msgstr ""
+
msgid "Groups can also be nested by creating %{subgroup_docs_link_start}subgroups%{subgroup_docs_link_end}."
msgstr ""
@@ -10757,15 +11022,12 @@ msgstr ""
msgid "ID:"
msgstr ""
-msgid "IDE|Allow live previews of JavaScript projects in the Web IDE using CodeSandbox client side evaluation."
+msgid "IDE|Allow live previews of JavaScript projects in the Web IDE using CodeSandbox Live Preview."
msgstr ""
msgid "IDE|Back"
msgstr ""
-msgid "IDE|Client side evaluation"
-msgstr ""
-
msgid "IDE|Commit"
msgstr ""
@@ -10829,6 +11091,9 @@ msgstr ""
msgid "If any job surpasses this timeout threshold, it will be marked as failed. Human readable time input language is accepted like \"1 hour\". Values without specification represent seconds."
msgstr ""
+msgid "If blank, set allowable lifetime to %{instance_level_policy_in_words}, as defined by the instance admin. Once set, existing tokens for users in this group may be revoked."
+msgstr ""
+
msgid "If checked, group owners can manage LDAP group links and LDAP member overrides"
msgstr ""
@@ -10868,6 +11133,9 @@ msgstr ""
msgid "If you lose your recovery codes you can generate new ones, invalidating all previous codes."
msgstr ""
+msgid "If you reach 100%% storage capacity, you will not be able to: %{base_message}"
+msgstr ""
+
msgid "If your HTTP repository is not publicly accessible, add your credentials."
msgstr ""
@@ -10979,6 +11247,9 @@ msgstr ""
msgid "Import repository"
msgstr ""
+msgid "Import started by: %{importInitiator}"
+msgstr ""
+
msgid "Import tasks"
msgstr ""
@@ -11045,6 +11316,9 @@ msgstr ""
msgid "In order to tailor your experience with GitLab we<br>would like to know a bit more about you."
msgstr ""
+msgid "In progress"
+msgstr ""
+
msgid "In the next step, you'll be able to select the projects you want to import."
msgstr ""
@@ -11392,15 +11666,15 @@ msgstr ""
msgid "Issues"
msgstr ""
-msgid "Issues / Merge Requests"
-msgstr ""
-
msgid "Issues Analytics"
msgstr ""
msgid "Issues Rate Limits"
msgstr ""
+msgid "Issues and Merge Requests"
+msgstr ""
+
msgid "Issues can be bugs, tasks or ideas to be discussed. Also, issues are searchable and filterable."
msgstr ""
@@ -11422,10 +11696,10 @@ msgstr ""
msgid "IssuesAnalytics|Avg/Month:"
msgstr ""
-msgid "IssuesAnalytics|Issues created"
+msgid "IssuesAnalytics|Issues opened"
msgstr ""
-msgid "IssuesAnalytics|Issues created per month"
+msgid "IssuesAnalytics|Issues opened per month"
msgstr ""
msgid "IssuesAnalytics|Last 12 months"
@@ -11479,6 +11753,9 @@ msgstr ""
msgid "Jira integration not configured."
msgstr ""
+msgid "Jira project: %{importProject}"
+msgstr ""
+
msgid "JiraService|Events for %{noteable_model_name} are disabled."
msgstr ""
@@ -11868,6 +12145,9 @@ msgstr ""
msgid "Lead"
msgstr ""
+msgid "Lead Time"
+msgstr ""
+
msgid "Learn GitLab"
msgstr ""
@@ -11880,6 +12160,9 @@ msgstr ""
msgid "Learn how to %{no_packages_link_start}publish and share your packages%{no_packages_link_end} with GitLab."
msgstr ""
+msgid "Learn how to enable synchronization"
+msgstr ""
+
msgid "Learn more"
msgstr ""
@@ -12211,9 +12494,6 @@ msgstr ""
msgid "Live preview"
msgstr ""
-msgid "Load more vulnerabilities"
-msgstr ""
-
msgid "Loading"
msgstr ""
@@ -12364,9 +12644,6 @@ msgstr ""
msgid "Manage"
msgstr ""
-msgid "Manage Git repositories with fine-grained access controls that keep your code secure. Perform code reviews and enhance collaboration with merge requests. Each project can also have an issue tracker and a wiki."
-msgstr ""
-
msgid "Manage Web IDE features"
msgstr ""
@@ -12397,6 +12674,9 @@ msgstr ""
msgid "Manage your license"
msgstr ""
+msgid "Managed Account"
+msgstr ""
+
msgid "Manifest"
msgstr ""
@@ -12661,6 +12941,9 @@ msgstr ""
msgid "Merge Requests in Review"
msgstr ""
+msgid "Merge automatically (%{strategy})"
+msgstr ""
+
msgid "Merge commit message"
msgstr ""
@@ -12814,6 +13097,12 @@ msgstr ""
msgid "Merged branches are being deleted. This can take some time depending on the number of branches. Please refresh the page to see changes."
msgstr ""
+msgid "Merged this merge request."
+msgstr ""
+
+msgid "Merges this merge request immediately."
+msgstr ""
+
msgid "Merges this merge request when the pipeline succeeds."
msgstr ""
@@ -12921,12 +13210,15 @@ msgid_plural "Metrics|Edit metrics"
msgstr[0] ""
msgstr[1] ""
-msgid "Metrics|Environment"
+msgid "Metrics|Expand panel"
msgstr ""
msgid "Metrics|For grouping similar metrics"
msgstr ""
+msgid "Metrics|Go back (Esc)"
+msgstr ""
+
msgid "Metrics|Invalid time range, please verify."
msgstr ""
@@ -12957,9 +13249,6 @@ msgstr ""
msgid "Metrics|Refresh dashboard"
msgstr ""
-msgid "Metrics|Show last"
-msgstr ""
-
msgid "Metrics|There was an error creating the dashboard."
msgstr ""
@@ -13038,6 +13327,9 @@ msgstr ""
msgid "Microsoft Azure"
msgstr ""
+msgid "Middleman project with Static Site Editor support"
+msgstr ""
+
msgid "Migrated %{success_count}/%{total_count} files."
msgstr ""
@@ -13483,10 +13775,10 @@ msgstr ""
msgid "Next"
msgstr ""
-msgid "Next file in diff (MRs only)"
+msgid "Next file in diff"
msgstr ""
-msgid "Next unresolved discussion (MRs only)"
+msgid "Next unresolved discussion"
msgstr ""
msgid "Nickname"
@@ -13546,6 +13838,9 @@ msgstr ""
msgid "No changes between %{ref_start}%{source_branch}%{ref_end} and %{ref_start}%{target_branch}%{ref_end}"
msgstr ""
+msgid "No child epics match applied filters"
+msgstr ""
+
msgid "No connection could be made to a Gitaly Server, please check your logs!"
msgstr ""
@@ -13684,15 +13979,6 @@ msgstr ""
msgid "No thanks, don't show this again"
msgstr ""
-msgid "No vulnerabilities found for this group"
-msgstr ""
-
-msgid "No vulnerabilities found for this pipeline"
-msgstr ""
-
-msgid "No vulnerabilities found for this project"
-msgstr ""
-
msgid "No vulnerabilities present"
msgstr ""
@@ -13816,6 +14102,9 @@ msgstr ""
msgid "Nothing to preview."
msgstr ""
+msgid "Nothing to synchronize"
+msgstr ""
+
msgid "Notification events"
msgstr ""
@@ -13987,6 +14276,9 @@ msgstr ""
msgid "Once removed, the fork relationship cannot be restored and you will no longer be able to send merge requests to the source."
msgstr ""
+msgid "Once the exported file is ready you can download it from this page."
+msgstr ""
+
msgid "Once the exported file is ready, you will receive a notification email with a download link, or you can download it from this page."
msgstr ""
@@ -14088,9 +14380,6 @@ msgstr ""
msgid "Open sidebar"
msgstr ""
-msgid "Open source software to collaborate on code"
-msgstr ""
-
msgid "Open: %{openIssuesCount}"
msgstr ""
@@ -14193,6 +14482,9 @@ msgstr ""
msgid "OutdatedBrowser|You can provide feedback %{feedback_link_start}on this issue%{feedback_link_end} or via your usual support channels."
msgstr ""
+msgid "Overridden"
+msgstr ""
+
msgid "Overview"
msgstr ""
@@ -14244,6 +14536,9 @@ msgstr ""
msgid "PackageRegistry|Conan Command"
msgstr ""
+msgid "PackageRegistry|Copy .pypirc content"
+msgstr ""
+
msgid "PackageRegistry|Copy Conan Command"
msgstr ""
@@ -14265,6 +14560,9 @@ msgstr ""
msgid "PackageRegistry|Copy NuGet Setup Command"
msgstr ""
+msgid "PackageRegistry|Copy Pip command"
+msgstr ""
+
msgid "PackageRegistry|Copy and paste this inside your %{codeStart}pom.xml%{codeEnd} %{codeStart}dependencies%{codeEnd} block."
msgstr ""
@@ -14298,6 +14596,12 @@ msgstr ""
msgid "PackageRegistry|For more information on the NuGet registry, %{linkStart}see the documentation%{linkEnd}."
msgstr ""
+msgid "PackageRegistry|For more information on the PyPi registry, %{linkStart}see the documentation%{linkEnd}."
+msgstr ""
+
+msgid "PackageRegistry|If you haven't already done so, you will need to add the below to your %{codeStart}.pypirc%{codeEnd} file."
+msgstr ""
+
msgid "PackageRegistry|If you haven't already done so, you will need to add the below to your %{codeStart}pom.xml%{codeEnd} file."
msgstr ""
@@ -14307,6 +14611,9 @@ msgstr ""
msgid "PackageRegistry|Learn how to %{noPackagesLinkStart}publish and share your packages%{noPackagesLinkEnd} with GitLab."
msgstr ""
+msgid "PackageRegistry|Manually Published"
+msgstr ""
+
msgid "PackageRegistry|Maven"
msgstr ""
@@ -14325,12 +14632,18 @@ msgstr ""
msgid "PackageRegistry|NuGet Command"
msgstr ""
+msgid "PackageRegistry|Pip Command"
+msgstr ""
+
msgid "PackageRegistry|Pipeline %{linkStart}%{linkEnd} triggered %{timestamp} by %{author}"
msgstr ""
msgid "PackageRegistry|Published to the repository at %{timestamp}"
msgstr ""
+msgid "PackageRegistry|PyPi"
+msgstr ""
+
msgid "PackageRegistry|Registry Setup"
msgstr ""
@@ -14367,6 +14680,9 @@ msgstr ""
msgid "PackageRegistry|npm"
msgstr ""
+msgid "PackageRegistry|published by %{author}"
+msgstr ""
+
msgid "PackageRegistry|yarn"
msgstr ""
@@ -14382,6 +14698,9 @@ msgstr ""
msgid "PackageType|NuGet"
msgstr ""
+msgid "PackageType|PyPi"
+msgstr ""
+
msgid "Packages"
msgstr ""
@@ -14475,6 +14794,9 @@ msgstr ""
msgid "Past due"
msgstr ""
+msgid "Paste a machine public key here. Read more about how to generate it"
+msgstr ""
+
msgid "Paste a machine public key here. Read more about how to generate it %{link_start}here%{link_end}"
msgstr ""
@@ -14490,9 +14812,6 @@ msgstr ""
msgid "Path"
msgstr ""
-msgid "Path, transfer, remove"
-msgstr ""
-
msgid "Path:"
msgstr ""
@@ -14523,7 +14842,7 @@ msgstr ""
msgid "Percentage"
msgstr ""
-msgid "Perform advanced options such as changing path, transferring, or removing the group."
+msgid "Perform advanced options such as changing path, transferring, exporting, or removing the group."
msgstr ""
msgid "Perform common operations on GitLab project"
@@ -14604,9 +14923,6 @@ msgstr ""
msgid "Pipeline minutes quota"
msgstr ""
-msgid "Pipeline quota"
-msgstr ""
-
msgid "Pipeline subscriptions"
msgstr ""
@@ -14616,6 +14932,9 @@ msgstr ""
msgid "Pipeline: %{status}"
msgstr ""
+msgid "PipelineCharts|CI / CD Analytics"
+msgstr ""
+
msgid "PipelineCharts|Failed:"
msgstr ""
@@ -14694,15 +15013,6 @@ msgstr ""
msgid "Pipelines settings for '%{project_name}' were successfully updated."
msgstr ""
-msgid "Pipelines| to purchase more minutes."
-msgstr ""
-
-msgid "Pipelines|%{namespace_name} has exceeded its pipeline minutes quota."
-msgstr ""
-
-msgid "Pipelines|%{namespace_name} has less than %{notification_level}%% of CI minutes available."
-msgstr ""
-
msgid "Pipelines|API"
msgstr ""
@@ -14724,10 +15034,13 @@ msgstr ""
msgid "Pipelines|Get started with Pipelines"
msgstr ""
-msgid "Pipelines|Loading Pipelines"
+msgid "Pipelines|Group %{namespace_name} has %{percentage}%% or less Shared Runner Pipeline minutes remaining. Once it runs out, no new jobs or pipelines in its projects will run."
msgstr ""
-msgid "Pipelines|Pipelines will not run anymore on shared Runners."
+msgid "Pipelines|Group %{namespace_name} has exceeded its pipeline minutes quota. Unless you buy additional pipeline minutes, no new jobs or pipelines in its projects will run."
+msgstr ""
+
+msgid "Pipelines|Loading Pipelines"
msgstr ""
msgid "Pipelines|Project cache successfully reset."
@@ -14958,6 +15271,9 @@ msgstr ""
msgid "Please select"
msgstr ""
+msgid "Please select a Jira project"
+msgstr ""
+
msgid "Please select a country"
msgstr ""
@@ -15003,6 +15319,9 @@ msgstr ""
msgid "Please wait while we import the repository for you. Refresh at will."
msgstr ""
+msgid "Plugins directory is deprecated and will be removed in 14.0. Please move this file into /file_hooks directory."
+msgstr ""
+
msgid "Pod does not exist"
msgstr ""
@@ -15120,7 +15439,7 @@ msgstr ""
msgid "Prevent users from changing their profile name"
msgstr ""
-msgid "Prevent users from modifing merge request approvers list"
+msgid "Prevent users from modifying merge request approvers list"
msgstr ""
msgid "Prevent users from performing write operations on GitLab while performing maintenance."
@@ -15141,10 +15460,10 @@ msgstr ""
msgid "Previous Artifacts"
msgstr ""
-msgid "Previous file in diff (MRs only)"
+msgid "Previous file in diff"
msgstr ""
-msgid "Previous unresolved discussion (MRs only)"
+msgid "Previous unresolved discussion"
msgstr ""
msgid "Primary"
@@ -15177,6 +15496,9 @@ msgstr ""
msgid "Private profile"
msgstr ""
+msgid "Private projects Minutes cost factor"
+msgstr ""
+
msgid "Private projects can be created in your personal namespace with:"
msgstr ""
@@ -16005,6 +16327,9 @@ msgstr ""
msgid "ProjectSettings|With GitLab Pages you can host your static websites on GitLab"
msgstr ""
+msgid "ProjectSettings|With Metrics Dashboard you can visualize this project performance metrics"
+msgstr ""
+
msgid "ProjectTemplates|.NET Core"
msgstr ""
@@ -16068,6 +16393,9 @@ msgstr ""
msgid "ProjectTemplates|Spring"
msgstr ""
+msgid "ProjectTemplates|Static Site Editor/Middleman"
+msgstr ""
+
msgid "ProjectTemplates|iOS (Swift)"
msgstr ""
@@ -16287,13 +16615,13 @@ msgstr ""
msgid "PrometheusService|Prometheus is being automatically managed on your clusters"
msgstr ""
-msgid "PrometheusService|These metrics will only be monitored after your first deployment to an environment"
+msgid "PrometheusService|Select the Active checkbox to override the Auto Configuration with custom settings. If unchecked, Auto Configuration settings are used."
msgstr ""
-msgid "PrometheusService|Time-series monitoring service"
+msgid "PrometheusService|These metrics will only be monitored after your first deployment to an environment"
msgstr ""
-msgid "PrometheusService|To enable manual configuration, uninstall Prometheus from your clusters"
+msgid "PrometheusService|Time-series monitoring service"
msgstr ""
msgid "PrometheusService|To enable the installation of Prometheus on your clusters, deactivate the manual configuration below"
@@ -16479,6 +16807,9 @@ msgstr ""
msgid "Protip:"
msgstr ""
+msgid "Protocol"
+msgstr ""
+
msgid "Provider"
msgstr ""
@@ -16503,6 +16834,9 @@ msgstr ""
msgid "Public pipelines"
msgstr ""
+msgid "Public projects Minutes cost factor"
+msgstr ""
+
msgid "Pull"
msgstr ""
@@ -16703,6 +17037,9 @@ msgid_plural "Refreshing in %d seconds to show the updated status..."
msgstr[0] ""
msgstr[1] ""
+msgid "Regenerate export"
+msgstr ""
+
msgid "Regenerate instance ID"
msgstr ""
@@ -17080,9 +17417,29 @@ msgstr ""
msgid "Reports|%{combinedString} and %{resolvedString}"
msgstr ""
+msgid "Reports|Accessibility scanning detected %d issue for the source branch only"
+msgid_plural "Reports|Accessibility scanning detected %d issues for the source branch only"
+msgstr[0] ""
+msgstr[1] ""
+
+msgid "Reports|Accessibility scanning detected no issues for the source branch only"
+msgstr ""
+
+msgid "Reports|Accessibility scanning failed loading results"
+msgstr ""
+
+msgid "Reports|Accessibility scanning results are being parsed"
+msgstr ""
+
msgid "Reports|Actions"
msgstr ""
+msgid "Reports|An error occured while loading report"
+msgstr ""
+
+msgid "Reports|An error occurred while loading %{name} results"
+msgstr ""
+
msgid "Reports|Class"
msgstr ""
@@ -17188,7 +17545,7 @@ msgstr ""
msgid "Requested %{time_ago}"
msgstr ""
-msgid "Requested design version does not exist"
+msgid "Requested design version does not exist."
msgstr ""
msgid "Requested states are invalid"
@@ -17372,6 +17729,9 @@ msgstr ""
msgid "Resync"
msgstr ""
+msgid "Resync all"
+msgstr ""
+
msgid "Resync all %{replicableType}"
msgstr ""
@@ -17440,6 +17800,9 @@ msgstr ""
msgid "Revoked personal access token %{personal_access_token_name}!"
msgstr ""
+msgid "Revoked project access token %{project_access_token_name}!"
+msgstr ""
+
msgid "RightSidebar|adding a"
msgstr ""
@@ -17488,6 +17851,9 @@ msgstr ""
msgid "Runner token"
msgstr ""
+msgid "Runner tokens"
+msgstr ""
+
msgid "Runner was not updated."
msgstr ""
@@ -17542,6 +17908,9 @@ msgstr ""
msgid "SAML SSO for %{group_name}"
msgstr ""
+msgid "SAML discovery tokens"
+msgstr ""
+
msgid "SAML for %{group_name}"
msgstr ""
@@ -17626,12 +17995,18 @@ msgstr ""
msgid "Scheduled"
msgstr ""
+msgid "Scheduled to merge this merge request (%{strategy})."
+msgstr ""
+
msgid "Scheduled to merge this merge request when the pipeline succeeds."
msgstr ""
msgid "Schedules"
msgstr ""
+msgid "Schedules to merge this merge request (%{strategy})."
+msgstr ""
+
msgid "Scheduling"
msgstr ""
@@ -17689,6 +18064,9 @@ msgstr ""
msgid "Search branches and tags"
msgstr ""
+msgid "Search by author"
+msgstr ""
+
msgid "Search files"
msgstr ""
@@ -17834,11 +18212,6 @@ msgid_plural "SearchResults|snippets"
msgstr[0] ""
msgstr[1] ""
-msgid "SearchResults|snippet result"
-msgid_plural "SearchResults|snippet results"
-msgstr[0] ""
-msgstr[1] ""
-
msgid "SearchResults|user"
msgid_plural "SearchResults|users"
msgstr[0] ""
@@ -17849,6 +18222,9 @@ msgid_plural "SearchResults|wiki results"
msgstr[0] ""
msgstr[1] ""
+msgid "Searching by both author and message is currently not supported."
+msgstr ""
+
msgid "Seat Link"
msgstr ""
@@ -17876,184 +18252,232 @@ msgstr ""
msgid "Security Dashboard"
msgstr ""
-msgid "Security Dashboard|Error fetching the vulnerability counts. Please check your network connection and try again."
+msgid "Security configuration help link"
+msgstr ""
+
+msgid "Security dashboard"
msgstr ""
-msgid "Security Dashboard|Error fetching the vulnerability list. Please check your network connection and try again."
+msgid "Security report is out of date. Please update your branch with the latest changes from the target branch (%{targetBranchName})"
msgstr ""
-msgid "Security Dashboard|Issue Created"
+msgid "Security report is out of date. Run %{newPipelineLinkStart}a new pipeline%{newPipelineLinkEnd} for the target branch (%{targetBranchName})"
msgstr ""
-msgid "Security Reports|Comment added to '%{vulnerabilityName}'"
+msgid "SecurityConfiguration|Configured"
msgstr ""
-msgid "Security Reports|Comment deleted on '%{vulnerabilityName}'"
+msgid "SecurityConfiguration|Feature"
msgstr ""
-msgid "Security Reports|Comment edited on '%{vulnerabilityName}'"
+msgid "SecurityConfiguration|Feature documentation for %{featureName}"
msgstr ""
-msgid "Security Reports|Create issue"
+msgid "SecurityConfiguration|Not yet configured"
msgstr ""
-msgid "Security Reports|Dismiss vulnerability"
+msgid "SecurityConfiguration|Secure features"
msgstr ""
-msgid "Security Reports|Dismissed '%{vulnerabilityName}'"
+msgid "SecurityConfiguration|Status"
msgstr ""
-msgid "Security Reports|Dismissed '%{vulnerabilityName}'. Turn off the hide dismissed toggle to view."
+msgid "SecurityReports|%{firstProject} and %{secondProject}"
msgstr ""
-msgid "Security Reports|Either you don't have permission to view this dashboard or the dashboard has not been setup. Please check your permission settings with your administrator or check your dashboard configurations to proceed."
+msgid "SecurityReports|%{firstProject}, %{secondProject}, and %{rest}"
msgstr ""
-msgid "Security Reports|Learn more about setting up your dashboard"
+msgid "SecurityReports|Add a project to your dashboard"
msgstr ""
-msgid "Security Reports|More info"
+msgid "SecurityReports|Add or remove projects from your dashboard"
msgstr ""
-msgid "Security Reports|Oops, something doesn't seem right."
+msgid "SecurityReports|Add projects"
msgstr ""
-msgid "Security Reports|Security reports can only be accessed by authorized users."
+msgid "SecurityReports|Comment added to '%{vulnerabilityName}'"
msgstr ""
-msgid "Security Reports|There was an error adding the comment."
+msgid "SecurityReports|Comment deleted on '%{vulnerabilityName}'"
msgstr ""
-msgid "Security Reports|There was an error creating the issue."
+msgid "SecurityReports|Comment edited on '%{vulnerabilityName}'"
msgstr ""
-msgid "Security Reports|There was an error creating the merge request."
+msgid "SecurityReports|Create issue"
msgstr ""
-msgid "Security Reports|There was an error deleting the comment."
+msgid "SecurityReports|Dismiss Selected"
msgstr ""
-msgid "Security Reports|There was an error dismissing the vulnerabilities."
+msgid "SecurityReports|Dismiss vulnerability"
msgstr ""
-msgid "Security Reports|There was an error dismissing the vulnerability."
+msgid "SecurityReports|Dismissed '%{vulnerabilityName}'"
msgstr ""
-msgid "Security Reports|There was an error reverting the dismissal."
+msgid "SecurityReports|Dismissed '%{vulnerabilityName}'. Turn off the hide dismissed toggle to view."
msgstr ""
-msgid "Security Reports|There was an error reverting this dismissal."
+msgid "SecurityReports|Each vulnerability now has a unique page that can be directly linked to, shared, referenced, and tracked as the single source of truth. Vulnerability occurrences also persist across scanner runs, which improves tracking and visibility and reduces duplicates between scans."
msgstr ""
-msgid "Security Reports|Undo dismiss"
+msgid "SecurityReports|Edit dashboard"
msgstr ""
-msgid "Security Reports|You do not have sufficient permissions to access this report"
+msgid "SecurityReports|Either you don't have permission to view this dashboard or the dashboard has not been setup. Please check your permission settings with your administrator or check your dashboard configurations to proceed."
msgstr ""
-msgid "Security Reports|You must sign in as an authorized user to see this report"
+msgid "SecurityReports|Error fetching the vulnerability counts. Please check your network connection and try again."
msgstr ""
-msgid "Security configuration help link"
+msgid "SecurityReports|Error fetching the vulnerability list. Please check your network connection and try again."
msgstr ""
-msgid "Security dashboard"
+msgid "SecurityReports|False positive"
msgstr ""
-msgid "Security report is out of date. Please update your branch with the latest changes from the target branch (%{targetBranchName})"
+msgid "SecurityReports|Hide dismissed"
msgstr ""
-msgid "Security report is out of date. Run %{newPipelineLinkStart}a new pipeline%{newPipelineLinkEnd} for the target branch (%{targetBranchName})"
+msgid "SecurityReports|Introducing standalone vulnerabilities"
msgstr ""
-msgid "SecurityConfiguration|Configured"
+msgid "SecurityReports|Issue Created"
msgstr ""
-msgid "SecurityConfiguration|Feature"
+msgid "SecurityReports|Learn More"
msgstr ""
-msgid "SecurityConfiguration|Feature documentation for %{featureName}"
+msgid "SecurityReports|Learn more about setting up your dashboard"
msgstr ""
-msgid "SecurityConfiguration|Not yet configured"
+msgid "SecurityReports|Load more vulnerabilities"
msgstr ""
-msgid "SecurityConfiguration|Secure features"
+msgid "SecurityReports|Monitor vulnerabilities in your code"
msgstr ""
-msgid "SecurityConfiguration|Status"
+msgid "SecurityReports|More info"
+msgstr ""
+
+msgid "SecurityReports|More information"
+msgstr ""
+
+msgid "SecurityReports|No vulnerabilities found for dashboard"
+msgstr ""
+
+msgid "SecurityReports|No vulnerabilities found for this group"
+msgstr ""
+
+msgid "SecurityReports|No vulnerabilities found for this pipeline"
+msgstr ""
+
+msgid "SecurityReports|No vulnerabilities found for this project"
+msgstr ""
+
+msgid "SecurityReports|Oops, something doesn't seem right."
+msgstr ""
+
+msgid "SecurityReports|Pipeline %{pipelineLink} triggered %{timeago} by %{user}"
msgstr ""
-msgid "SecurityDashboard|%{firstProject} and %{secondProject}"
+msgid "SecurityReports|Project"
msgstr ""
-msgid "SecurityDashboard|%{firstProject}, %{secondProject}, and %{rest}"
+msgid "SecurityReports|Projects added"
msgstr ""
-msgid "SecurityDashboard|Add a project to your dashboard"
+msgid "SecurityReports|Remove project from dashboard"
msgstr ""
-msgid "SecurityDashboard|Add or remove projects from your dashboard"
+msgid "SecurityReports|Report type"
msgstr ""
-msgid "SecurityDashboard|Add projects"
+msgid "SecurityReports|Return to dashboard"
msgstr ""
-msgid "SecurityDashboard|Edit dashboard"
+msgid "SecurityReports|Security Dashboard"
msgstr ""
-msgid "SecurityDashboard|Hide dismissed"
+msgid "SecurityReports|Security reports can only be accessed by authorized users."
msgstr ""
-msgid "SecurityDashboard|Monitor vulnerabilities in your code"
+msgid "SecurityReports|Select a project to add by using the project search field above."
msgstr ""
-msgid "SecurityDashboard|More information"
+msgid "SecurityReports|Select a reason"
msgstr ""
-msgid "SecurityDashboard|No vulnerabilities found for dashboard"
+msgid "SecurityReports|Severity"
msgstr ""
-msgid "SecurityDashboard|Pipeline %{pipelineLink} triggered %{timeago} by %{user}"
+msgid "SecurityReports|Status"
msgstr ""
-msgid "SecurityDashboard|Project"
+msgid "SecurityReports|The security dashboard displays the latest security findings for projects you wish to monitor. Select \"Edit dashboard\" to add and remove projects."
msgstr ""
-msgid "SecurityDashboard|Projects added"
+msgid "SecurityReports|The security dashboard displays the latest security report. Use it to find and fix vulnerabilities."
msgstr ""
-msgid "SecurityDashboard|Remove project from dashboard"
+msgid "SecurityReports|There was an error adding the comment."
msgstr ""
-msgid "SecurityDashboard|Report type"
+msgid "SecurityReports|There was an error creating the issue."
msgstr ""
-msgid "SecurityDashboard|Return to dashboard"
+msgid "SecurityReports|There was an error creating the merge request."
msgstr ""
-msgid "SecurityDashboard|Security Dashboard"
+msgid "SecurityReports|There was an error deleting the comment."
msgstr ""
-msgid "SecurityDashboard|Select a project to add by using the project search field above."
+msgid "SecurityReports|There was an error dismissing the vulnerabilities."
msgstr ""
-msgid "SecurityDashboard|Severity"
+msgid "SecurityReports|There was an error dismissing the vulnerability."
msgstr ""
-msgid "SecurityDashboard|Status"
+msgid "SecurityReports|There was an error reverting the dismissal."
msgstr ""
-msgid "SecurityDashboard|The security dashboard displays the latest security findings for projects you wish to monitor. Select \"Edit dashboard\" to add and remove projects."
+msgid "SecurityReports|There was an error reverting this dismissal."
msgstr ""
-msgid "SecurityDashboard|The security dashboard displays the latest security report. Use it to find and fix vulnerabilities."
+msgid "SecurityReports|There was an error while generating the report."
msgstr ""
-msgid "SecurityDashboard|There was an error while generating the report."
+msgid "SecurityReports|Unable to add %{invalidProjects}"
msgstr ""
-msgid "SecurityDashboard|Unable to add %{invalidProjects}"
+msgid "SecurityReports|Undo dismiss"
+msgstr ""
+
+msgid "SecurityReports|While it's rare to have no vulnerabilities for your group, it can happen. In any event, we ask that you double check your settings to make sure you've set up your dashboard correctly."
+msgstr ""
+
+msgid "SecurityReports|While it's rare to have no vulnerabilities for your pipeline, it can happen. In any event, we ask that you double check your settings to make sure all security scanning jobs have passed successfully."
+msgstr ""
+
+msgid "SecurityReports|While it's rare to have no vulnerabilities for your project, it can happen. In any event, we ask that you double check your settings to make sure you've set up your dashboard correctly."
+msgstr ""
+
+msgid "SecurityReports|While it's rare to have no vulnerabilities, it can happen. In any event, we ask that you please double check your settings to make sure you've set up your dashboard correctly."
+msgstr ""
+
+msgid "SecurityReports|Won't fix / Accept risk"
+msgstr ""
+
+msgid "SecurityReports|You do not have sufficient permissions to access this report"
+msgstr ""
+
+msgid "SecurityReports|You must sign in as an authorized user to see this report"
+msgstr ""
+
+msgid "SecurityReports|[No reason]"
msgstr ""
msgid "See GitLab's %{password_policy_guidelines}"
@@ -18065,6 +18489,9 @@ msgstr ""
msgid "See the affected projects in the GitLab admin panel"
msgstr ""
+msgid "See what's new at GitLab"
+msgstr ""
+
msgid "Select"
msgstr ""
@@ -18470,9 +18897,6 @@ msgstr ""
msgid "Set up Jira Integration"
msgstr ""
-msgid "Set up Jira Integration illustration"
-msgstr ""
-
msgid "Set up a %{type} Runner automatically"
msgstr ""
@@ -18557,6 +18981,9 @@ msgstr ""
msgid "Severity: %{severity}"
msgstr ""
+msgid "Shards (%{shards})"
+msgstr ""
+
msgid "Shards to synchronize"
msgstr ""
@@ -18871,6 +19298,9 @@ msgstr ""
msgid "Snowplow"
msgstr ""
+msgid "Some child epics may be hidden due to applied filters"
+msgstr ""
+
msgid "Some email servers do not support overriding the email sender name. Enable this option to include the name of the author of the issue, merge request or comment in the email body instead."
msgstr ""
@@ -19012,6 +19442,9 @@ msgstr ""
msgid "Something went wrong while updating your list settings"
msgstr ""
+msgid "Something went wrong with your automatic subscription renewal"
+msgstr ""
+
msgid "Something went wrong, unable to add %{project} to dashboard"
msgstr ""
@@ -19297,6 +19730,9 @@ msgstr ""
msgid "Stage removed"
msgstr ""
+msgid "Standard"
+msgstr ""
+
msgid "Star a label to make it a priority label. Order the prioritized labels to change their relative priority, by dragging."
msgstr ""
@@ -19420,6 +19856,9 @@ msgstr ""
msgid "Static Application Security Testing (SAST)"
msgstr ""
+msgid "StaticSiteEditor|An error occurred while submitting your changes."
+msgstr ""
+
msgid "StaticSiteEditor|Branch could not be created."
msgstr ""
@@ -19429,18 +19868,30 @@ msgstr ""
msgid "StaticSiteEditor|Could not create merge request."
msgstr ""
+msgid "StaticSiteEditor|Incompatible file content"
+msgstr ""
+
msgid "StaticSiteEditor|Return to site"
msgstr ""
+msgid "StaticSiteEditor|Static site editor"
+msgstr ""
+
msgid "StaticSiteEditor|Success!"
msgstr ""
msgid "StaticSiteEditor|Summary of changes"
msgstr ""
+msgid "StaticSiteEditor|The Static Site Editor is currently configured to only edit Markdown content on pages generated from Middleman. Visit the documentation to learn more about configuring your site to use the Static Site Editor."
+msgstr ""
+
msgid "StaticSiteEditor|Update %{sourcePath} file"
msgstr ""
+msgid "StaticSiteEditor|View documentation"
+msgstr ""
+
msgid "StaticSiteEditor|View merge request"
msgstr ""
@@ -19498,6 +19949,12 @@ msgstr ""
msgid "StatusPage|Status page"
msgstr ""
+msgid "StatusPage|Status page URL"
+msgstr ""
+
+msgid "StatusPage|Status page frontend documentation"
+msgstr ""
+
msgid "StatusPage|To publish incidents to an external status page, GitLab will store a JSON file in your Amazon S3 account in a location accessible to your external status page service. Make sure to also set up %{docsLink}"
msgstr ""
@@ -19846,6 +20303,12 @@ msgstr ""
msgid "Synced"
msgstr ""
+msgid "Synchronization disabled"
+msgstr ""
+
+msgid "Synchronization of container repositories is disabled."
+msgstr ""
+
msgid "System"
msgstr ""
@@ -19999,6 +20462,9 @@ msgstr ""
msgid "Telephone number"
msgstr ""
+msgid "Telephone number (Optional)"
+msgstr ""
+
msgid "Template"
msgstr ""
@@ -20305,6 +20771,9 @@ msgstr ""
msgid "The license was successfully uploaded and is now active. You can see the details below."
msgstr ""
+msgid "The license was successfully uploaded and will be active from %{starts_at}. You can see the details below."
+msgstr ""
+
msgid "The maximum file size allowed is %{size}."
msgstr ""
@@ -20626,6 +21095,9 @@ msgstr ""
msgid "There was an error getting the epic participants."
msgstr ""
+msgid "There was an error importing the Jira project."
+msgstr ""
+
msgid "There was an error loading users activity calendar."
msgstr ""
@@ -20641,7 +21113,7 @@ msgstr ""
msgid "There was an error resetting user pipeline minutes."
msgstr ""
-msgid "There was an error saving this Geo Node"
+msgid "There was an error saving this Geo Node."
msgstr ""
msgid "There was an error saving your changes."
@@ -20704,6 +21176,9 @@ msgstr ""
msgid "They can be managed using the %{link}."
msgstr ""
+msgid "Third Party Advisory Link"
+msgstr ""
+
msgid "Third party offers"
msgstr ""
@@ -20740,6 +21215,9 @@ msgstr ""
msgid "This application will be able to:"
msgstr ""
+msgid "This attachment has been truncated to avoid exceeding the maximum allowed attachment size of 15MB. %{written_count} of %{issues_count} issues have been included. Consider re-exporting with a narrower selection of issues."
+msgstr ""
+
msgid "This block is self-referential"
msgstr ""
@@ -20881,6 +21359,12 @@ msgstr ""
msgid "This issue is confidential"
msgstr ""
+msgid "This issue is currently blocked by the following issues: %{issues}."
+msgstr ""
+
+msgid "This issue is in a child epic of the filtered epic"
+msgstr ""
+
msgid "This issue is locked."
msgstr ""
@@ -21079,7 +21563,10 @@ msgstr ""
msgid "This user will be the author of all events in the activity feed that are the result of an update, like new branches being created or new commits being pushed to existing branches. Upon creation or when reassigning you can only assign yourself to be the mirror user."
msgstr ""
-msgid "This variable can not be masked"
+msgid "This variable can not be masked."
+msgstr ""
+
+msgid "This variable does not match the expected pattern."
msgstr ""
msgid "This will help us personalize your onboarding experience."
@@ -21109,10 +21596,10 @@ msgstr ""
msgid "ThreatMonitoring|Application firewall not detected"
msgstr ""
-msgid "ThreatMonitoring|Container Network Policy"
+msgid "ThreatMonitoring|Container Network Policies are not installed or have been disabled. To view this data, ensure your Network Policies are installed and enabled for your cluster."
msgstr ""
-msgid "ThreatMonitoring|Container NetworkPolicies are not installed or has been disabled. To view this data, ensure you NetworkPolicies are installed and enabled for your cluster."
+msgid "ThreatMonitoring|Container Network Policy"
msgstr ""
msgid "ThreatMonitoring|Container NetworkPolicies not detected"
@@ -21145,7 +21632,7 @@ msgstr ""
msgid "ThreatMonitoring|Something went wrong, unable to fetch statistics"
msgstr ""
-msgid "ThreatMonitoring|The firewall is not installed or has been disabled. To view this data, ensure you firewall is installed and enabled for your cluster."
+msgid "ThreatMonitoring|The firewall is not installed or has been disabled. To view this data, ensure the web application firewall is installed and enabled for your cluster."
msgstr ""
msgid "ThreatMonitoring|The graph below is an overview of traffic coming to your application as tracked by the Web Application Firewall (WAF). View the docs for instructions on how to access the WAF logs to see what type of malicious traffic is trying to access your app. The docs link is also accessible by clicking the \"?\" icon next to the title below."
@@ -21214,6 +21701,9 @@ msgstr ""
msgid "Time in seconds GitLab will wait for a response from the external service. When the service does not respond in time, access will be denied."
msgstr ""
+msgid "Time of import: %{importTime}"
+msgstr ""
+
msgid "Time remaining"
msgstr ""
@@ -21398,7 +21888,7 @@ msgstr ""
msgid "Title:"
msgstr ""
-msgid "Titles and Filenames"
+msgid "Titles and Descriptions"
msgstr ""
msgid "To"
@@ -21605,7 +22095,7 @@ msgstr ""
msgid "Too many projects enabled. You will need to manage them via the console or the API."
msgstr ""
-msgid "Topics"
+msgid "Topics (optional)"
msgstr ""
msgid "Total"
@@ -21767,6 +22257,9 @@ msgstr ""
msgid "Try all GitLab has to offer for 30 days."
msgstr ""
+msgid "Try changing or removing filters."
+msgstr ""
+
msgid "Try to fork again"
msgstr ""
@@ -21860,6 +22353,9 @@ msgstr ""
msgid "Unable to connect to server: %{error}"
msgstr ""
+msgid "Unable to connect to the Jira instance. Please check your Jira integration configuration."
+msgstr ""
+
msgid "Unable to convert Kubernetes logs encoding to UTF-8"
msgstr ""
@@ -22803,6 +23299,9 @@ msgstr ""
msgid "View issue"
msgstr ""
+msgid "View issues"
+msgstr ""
+
msgid "View it on GitLab"
msgstr ""
@@ -22980,6 +23479,15 @@ msgstr ""
msgid "VulnerabilityManagement|Resolved %{timeago} by %{user}"
msgstr ""
+msgid "VulnerabilityManagement|Something went wrong while trying to delete the comment. Please try again later."
+msgstr ""
+
+msgid "VulnerabilityManagement|Something went wrong while trying to retrieve the vulnerability history. Please try again later."
+msgstr ""
+
+msgid "VulnerabilityManagement|Something went wrong while trying to save the comment. Please try again later."
+msgstr ""
+
msgid "VulnerabilityManagement|Something went wrong, could not create an issue."
msgstr ""
@@ -23010,6 +23518,9 @@ msgstr ""
msgid "VulnerabilityStatusTypes|Resolved"
msgstr ""
+msgid "Vulnerability|%{scannerName} (version %{scannerVersion})"
+msgstr ""
+
msgid "Vulnerability|Class"
msgstr ""
@@ -23043,7 +23554,10 @@ msgstr ""
msgid "Vulnerability|Project"
msgstr ""
-msgid "Vulnerability|Report Type"
+msgid "Vulnerability|Scanner Provider"
+msgstr ""
+
+msgid "Vulnerability|Scanner Type"
msgstr ""
msgid "Vulnerability|Severity"
@@ -23097,9 +23611,15 @@ msgstr ""
msgid "We sent you an email with reset password instructions"
msgstr ""
+msgid "We tried to automatically renew your %{strong}%{plan_name}%{strong_close} subscription for %{strong}%{namespace_name}%{strong_close} on %{expires_on} but something went wrong so your subscription was downgraded to the free plan. Don't worry, your data is safe. We suggest you check your payment method and get in touch with our support team (%{support_link}). They'll gladly help with your subscription renewal."
+msgstr ""
+
msgid "We want to be sure it is you, please confirm you are not a robot."
msgstr ""
+msgid "We will automatically renew your %{strong}%{plan_name}%{strong_close} subscription for %{strong}%{namespace_name}%{strong_close} on %{strong}%{expires_on}%{strong_close}. There's nothing that you need to do, we'll let you know when the renewal is complete. Need more seats, a higher plan or just want to review your payment method?"
+msgstr ""
+
msgid "We've found no vulnerabilities"
msgstr ""
@@ -23204,15 +23724,6 @@ msgstr ""
msgid "When:"
msgstr ""
-msgid "While it's rare to have no vulnerabilities for your group, it can happen. In any event, we ask that you double check your settings to make sure you've set up your dashboard correctly."
-msgstr ""
-
-msgid "While it's rare to have no vulnerabilities for your pipeline, it can happen. In any event, we ask that you double check your settings to make sure all security scanning jobs have passed successfully."
-msgstr ""
-
-msgid "While it's rare to have no vulnerabilities for your project, it can happen. In any event, we ask that you double check your settings to make sure you've set up your dashboard correctly."
-msgstr ""
-
msgid "While it's rare to have no vulnerabilities, it can happen. In any event, we ask that you please double check your settings to make sure you've set up your dashboard correctly."
msgstr ""
@@ -23243,9 +23754,6 @@ msgstr ""
msgid "Wiki"
msgstr ""
-msgid "Wiki pages"
-msgstr ""
-
msgid "Wiki was successfully updated."
msgstr ""
@@ -23429,6 +23937,9 @@ msgstr ""
msgid "Yes, add it"
msgstr ""
+msgid "Yes, close issue"
+msgstr ""
+
msgid "Yes, let me map Google Code users to full names or GitLab users."
msgstr ""
@@ -23624,6 +24135,9 @@ msgstr ""
msgid "You could not create a new trigger."
msgstr ""
+msgid "You didn't renew your %{strong}%{plan_name}%{strong_close} subscription for %{strong}%{namespace_name}%{strong_close} so it was downgraded to the free plan."
+msgstr ""
+
msgid "You didn't renew your %{strong}%{plan_name}%{strong_close} subscription so it was downgraded to the GitLab Core Plan."
msgstr ""
@@ -23690,6 +24204,9 @@ msgstr ""
msgid "You have declined the invitation to join %{label}."
msgstr ""
+msgid "You have imported from this project %{numberOfPreviousImportsForProject} times before. Each new import will create duplicate issues."
+msgstr ""
+
msgid "You have no permissions"
msgstr ""
@@ -23771,6 +24288,9 @@ msgstr ""
msgid "You need to upload a Google Takeout archive."
msgstr ""
+msgid "You reached %{usage_in_percent} of %{namespace_name}'s capacity (%{used_storage} of %{storage_limit})"
+msgstr ""
+
msgid "You tried to fork %{link_to_the_project} but it failed for the following reason:"
msgstr ""
@@ -23855,9 +24375,21 @@ msgstr ""
msgid "YouTube"
msgstr ""
+msgid "Your %{strong}%{plan_name}%{strong_close} subscription for %{strong}%{namespace_name}%{strong_close} will expire on %{strong}%{expires_on}%{strong_close}. After that, you will not to be able to create issues or merge requests as well as many other features."
+msgstr ""
+
msgid "Your %{strong}%{plan_name}%{strong_close} subscription will expire on %{strong}%{expires_on}%{strong_close}. After that, you will not to be able to create issues or merge requests as well as many other features."
msgstr ""
+msgid "Your CSV export has started. It will be emailed to %{email} when complete."
+msgstr ""
+
+msgid "Your CSV export of %{issues_count} from project %{project_link} has been added to this email as an attachment."
+msgstr ""
+
+msgid "Your CSV export of %{written_count} from project %{project_name} (%{project_url}) has been added to this email as an attachment."
+msgstr ""
+
msgid "Your Commit Email will be used for web based operations, such as edits and merges."
msgstr ""
@@ -23954,6 +24486,9 @@ msgstr ""
msgid "Your comment could not be updated! Please check your network connection and try again."
msgstr ""
+msgid "Your comment will be discarded."
+msgstr ""
+
msgid "Your custom stage '%{title}' was created"
msgstr ""
@@ -23993,6 +24528,9 @@ msgstr ""
msgid "Your new personal access token has been created."
msgstr ""
+msgid "Your new project access token has been created."
+msgstr ""
+
msgid "Your password isn't required to view this page. If a password or any other personal details are requested, please contact your administrator to report abuse."
msgstr ""
@@ -24011,12 +24549,18 @@ msgstr ""
msgid "Your request for access has been queued for review."
msgstr ""
+msgid "Your search didn't match any commits."
+msgstr ""
+
msgid "Your subscription expired!"
msgstr ""
msgid "Your subscription has been downgraded"
msgstr ""
+msgid "Your subscription will automatically renew in %{remaining_days}"
+msgstr ""
+
msgid "Your subscription will expire in %{remaining_days}"
msgstr ""
@@ -24055,7 +24599,7 @@ msgstr ""
msgid "allowed to fail"
msgstr ""
-msgid "already being used for another group or project milestone."
+msgid "already being used for another group or project %{timebox_name}."
msgstr ""
msgid "already has a \"created\" issue link"
@@ -24133,6 +24677,9 @@ msgstr ""
msgid "ciReport|%{linkStartTag}Learn more about SAST %{linkEndTag}"
msgstr ""
+msgid "ciReport|%{linkStartTag}Learn more about Secret Scanning %{linkEndTag}"
+msgstr ""
+
msgid "ciReport|%{linkStartTag}Learn more about codequality reports %{linkEndTag}"
msgstr ""
@@ -24296,6 +24843,12 @@ msgstr ""
msgid "ciReport|SAST"
msgstr ""
+msgid "ciReport|Secret scanning"
+msgstr ""
+
+msgid "ciReport|Secret scanning detects secrets and credentials vulnerabilities in your source code."
+msgstr ""
+
msgid "ciReport|Security scanning"
msgstr ""
@@ -24493,6 +25046,9 @@ msgstr ""
msgid "group"
msgstr ""
+msgid "groups"
+msgstr ""
+
msgid "has already been linked to another vulnerability"
msgstr ""
@@ -24656,9 +25212,6 @@ msgstr[1] ""
msgid "merged %{time_ago}"
msgstr ""
-msgid "milestone should belong either to a project or a group."
-msgstr ""
-
msgid "missing"
msgstr ""
@@ -24995,6 +25548,9 @@ msgstr ""
msgid "no contributions"
msgstr ""
+msgid "no expiration"
+msgstr ""
+
msgid "no one can merge"
msgstr ""
@@ -25047,6 +25603,9 @@ msgstr ""
msgid "pending removal"
msgstr ""
+msgid "per day"
+msgstr ""
+
msgid "pipeline"
msgstr ""
@@ -25075,6 +25634,12 @@ msgstr[1] ""
msgid "project avatar"
msgstr ""
+msgid "projects"
+msgstr ""
+
+msgid "push to your repository, create pipelines, create issues or add comments. To reduce storage capacity, delete unused repositories, artifacts, wikis, issues, and pipelines."
+msgstr ""
+
msgid "quick actions"
msgstr ""
@@ -25221,11 +25786,6 @@ msgstr ""
msgid "this document"
msgstr ""
-msgid "thread resolved"
-msgid_plural "threads resolved"
-msgstr[0] ""
-msgstr[1] ""
-
msgid "to help your contributors communicate effectively!"
msgstr ""
@@ -25280,6 +25840,9 @@ msgstr ""
msgid "view the blob"
msgstr ""
+msgid "vulnerability|Add a comment"
+msgstr ""
+
msgid "vulnerability|Add a comment or reason for dismissal"
msgstr ""
diff --git a/locale/pa_IN/gitlab.po b/locale/pa_IN/gitlab.po
index a1ee84e7fa0..1b1d8f9bd99 100644
--- a/locale/pa_IN/gitlab.po
+++ b/locale/pa_IN/gitlab.po
@@ -12,7 +12,7 @@ msgstr ""
"X-Crowdin-Project: gitlab-ee\n"
"X-Crowdin-Language: pa-IN\n"
"X-Crowdin-File: /master/locale/gitlab.pot\n"
-"PO-Revision-Date: 2020-04-15 00:26\n"
+"PO-Revision-Date: 2020-05-05 21:35\n"
msgid " %{start} to %{end}"
msgstr ""
@@ -212,6 +212,11 @@ msgid_plural "%d tags"
msgstr[0] ""
msgstr[1] ""
+msgid "%d unresolved thread"
+msgid_plural "%d unresolved threads"
+msgstr[0] ""
+msgstr[1] ""
+
msgid "%d vulnerability dismissed"
msgid_plural "%d vulnerabilities dismissed"
msgstr[0] ""
@@ -371,6 +376,9 @@ msgstr ""
msgid "%{mrText}, this issue will be closed automatically."
msgstr ""
+msgid "%{namespace_name} is now read-only. You cannot: %{base_message}"
+msgstr ""
+
msgid "%{name} contained %{resultsString}"
msgstr ""
@@ -383,6 +391,11 @@ msgstr ""
msgid "%{name}'s avatar"
msgstr ""
+msgid "%{no_of_days} day"
+msgid_plural "%{no_of_days} days"
+msgstr[0] ""
+msgstr[1] ""
+
msgid "%{number_commits_behind} commits behind %{default_branch}, %{number_commits_ahead} commits ahead"
msgstr ""
@@ -506,6 +519,9 @@ msgstr[1] ""
msgid "%{text} is available"
msgstr ""
+msgid "%{timebox_name} should belong either to a project or a group."
+msgstr ""
+
msgid "%{title} %{operator} %{threshold}"
msgstr ""
@@ -781,12 +797,18 @@ msgstr ""
msgid "<code>\"johnsmith@example.com\": \"johnsmith@example.com\"</code> will add \"By <a href=\"#\">johnsmith@example.com</a>\" to all issues and comments originally created by johnsmith@example.com. By default, the email address or username is masked to ensure the user's privacy. Use this option if you want to show the full email address."
msgstr ""
+msgid "<namespace / project>"
+msgstr ""
+
msgid "<no name set>"
msgstr ""
msgid "<no scopes selected>"
msgstr ""
+msgid "<project name>"
+msgstr ""
+
msgid "<strong>%{group_name}</strong> group members"
msgstr ""
@@ -820,6 +842,9 @@ msgstr ""
msgid "A basic page and serverless function that uses AWS Lambda, AWS API Gateway, and GitLab Pages"
msgstr ""
+msgid "A complete DevOps platform"
+msgstr ""
+
msgid "A default branch cannot be chosen for an empty project."
msgstr ""
@@ -1036,6 +1061,12 @@ msgstr ""
msgid "AccessTokens|reset it"
msgstr ""
+msgid "AccessibilityReport|Accessibility report artifact not found"
+msgstr ""
+
+msgid "AccessibilityReport|Failed to retrieve accessibility report"
+msgstr ""
+
msgid "AccessibilityReport|Learn More"
msgstr ""
@@ -1045,6 +1076,9 @@ msgstr ""
msgid "AccessibilityReport|New"
msgstr ""
+msgid "AccessibilityReport|The accessibility scanning found an error of the following type: %{code}"
+msgstr ""
+
msgid "Account"
msgstr ""
@@ -1287,6 +1321,9 @@ msgstr ""
msgid "Added at"
msgstr ""
+msgid "Added for this merge request"
+msgstr ""
+
msgid "Added in this version"
msgstr ""
@@ -1658,6 +1695,72 @@ msgid_plural "Alerts"
msgstr[0] ""
msgstr[1] ""
+msgid "Alert Details"
+msgstr ""
+
+msgid "AlertManagement|Acknowledged"
+msgstr ""
+
+msgid "AlertManagement|Alert"
+msgstr ""
+
+msgid "AlertManagement|Authorize external service"
+msgstr ""
+
+msgid "AlertManagement|Display alerts from all your monitoring tools directly within GitLab. Streamline the investigation of your alerts and the escalation of alerts to incidents."
+msgstr ""
+
+msgid "AlertManagement|End time"
+msgstr ""
+
+msgid "AlertManagement|End time:"
+msgstr ""
+
+msgid "AlertManagement|Events"
+msgstr ""
+
+msgid "AlertManagement|Events:"
+msgstr ""
+
+msgid "AlertManagement|Full Alert Details"
+msgstr ""
+
+msgid "AlertManagement|More information"
+msgstr ""
+
+msgid "AlertManagement|No alerts available to display. If you think you're seeing this message in error, refresh the page."
+msgstr ""
+
+msgid "AlertManagement|No alerts to display."
+msgstr ""
+
+msgid "AlertManagement|Overview"
+msgstr ""
+
+msgid "AlertManagement|Resolved"
+msgstr ""
+
+msgid "AlertManagement|Severity"
+msgstr ""
+
+msgid "AlertManagement|Start time"
+msgstr ""
+
+msgid "AlertManagement|Start time:"
+msgstr ""
+
+msgid "AlertManagement|Status"
+msgstr ""
+
+msgid "AlertManagement|Surface alerts in GitLab"
+msgstr ""
+
+msgid "AlertManagement|There was an error displaying the alerts. Confirm your endpoint's configuration details to ensure alerts appear."
+msgstr ""
+
+msgid "AlertManagement|Triggered"
+msgstr ""
+
msgid "AlertService|%{linkStart}Learn more%{linkEnd} about configuring this endpoint to receive alerts."
msgstr ""
@@ -1727,6 +1830,9 @@ msgstr ""
msgid "All security scans are enabled because %{linkStart}Auto DevOps%{linkEnd} is enabled on this project"
msgstr ""
+msgid "All threads resolved"
+msgstr ""
+
msgid "All users"
msgstr ""
@@ -1745,6 +1851,9 @@ msgstr ""
msgid "Allow only the selected protocols to be used for Git access."
msgstr ""
+msgid "Allow owners to manage default branch protection per group"
+msgstr ""
+
msgid "Allow owners to manually add users outside of LDAP"
msgstr ""
@@ -1853,6 +1962,9 @@ msgstr ""
msgid "An error occurred fetching the dropdown data."
msgstr ""
+msgid "An error occurred fetching the project authors."
+msgstr ""
+
msgid "An error occurred previewing the blob"
msgstr ""
@@ -1934,6 +2046,9 @@ msgstr ""
msgid "An error occurred while fetching sidebar data"
msgstr ""
+msgid "An error occurred while fetching terraform reports."
+msgstr ""
+
msgid "An error occurred while fetching the Service Desk address."
msgstr ""
@@ -2171,6 +2286,9 @@ msgstr ""
msgid "Any"
msgstr ""
+msgid "Any Author"
+msgstr ""
+
msgid "Any Label"
msgstr ""
@@ -2386,10 +2504,10 @@ msgstr ""
msgid "Are you sure that you want to unarchive this project?"
msgstr ""
-msgid "Are you sure you want to cancel creating this comment?"
+msgid "Are you sure you want to cancel editing this comment?"
msgstr ""
-msgid "Are you sure you want to cancel editing this comment?"
+msgid "Are you sure you want to close this blocked issue?"
msgstr ""
msgid "Are you sure you want to delete %{name}?"
@@ -2419,6 +2537,9 @@ msgstr ""
msgid "Are you sure you want to deploy this environment?"
msgstr ""
+msgid "Are you sure you want to discard this comment?"
+msgstr ""
+
msgid "Are you sure you want to erase this build?"
msgstr ""
@@ -2595,6 +2716,9 @@ msgstr ""
msgid "At least one approval from a code owner is required to change files matching the respective CODEOWNER rules."
msgstr ""
+msgid "At least one logging option is required to be enabled"
+msgstr ""
+
msgid "At least one of group_id or project_id must be specified"
msgstr ""
@@ -2633,6 +2757,27 @@ msgstr ""
msgid "AuditEvents|Target"
msgstr ""
+msgid "AuditLogs|(removed)"
+msgstr ""
+
+msgid "AuditLogs|Action"
+msgstr ""
+
+msgid "AuditLogs|Author"
+msgstr ""
+
+msgid "AuditLogs|Date"
+msgstr ""
+
+msgid "AuditLogs|IP Address"
+msgstr ""
+
+msgid "AuditLogs|Object"
+msgstr ""
+
+msgid "AuditLogs|Target"
+msgstr ""
+
msgid "Aug"
msgstr ""
@@ -2915,9 +3060,6 @@ msgstr ""
msgid "BambooService|You must set up automatic revision labeling and a repository trigger in Bamboo."
msgstr ""
-msgid "Batch operations"
-msgstr ""
-
msgid "BatchComments|Delete all pending comments"
msgstr ""
@@ -3047,6 +3189,9 @@ msgstr ""
msgid "Boards"
msgstr ""
+msgid "Boards and Board Lists"
+msgstr ""
+
msgid "Boards|Collapse"
msgstr ""
@@ -3266,6 +3411,9 @@ msgstr ""
msgid "Buy GitLab Enterprise Edition"
msgstr ""
+msgid "Buy more Pipeline minutes"
+msgstr ""
+
msgid "By %{user_name}"
msgstr ""
@@ -3386,6 +3534,9 @@ msgstr ""
msgid "Can't scan the code?"
msgstr ""
+msgid "Can't update snippet: %{err}"
+msgstr ""
+
msgid "Canary"
msgstr ""
@@ -3554,6 +3705,9 @@ msgstr ""
msgid "Changing group path can have unintended side effects."
msgstr ""
+msgid "Charts"
+msgstr ""
+
msgid "Charts can't be displayed as the request for data has timed out. %{documentationLink}"
msgstr ""
@@ -3590,9 +3744,6 @@ msgstr ""
msgid "ChatMessage|and [%{count} more](%{pipeline_failed_jobs_url})"
msgstr ""
-msgid "ChatMessage|failed"
-msgstr ""
-
msgid "ChatMessage|has failed"
msgstr ""
@@ -3608,9 +3759,6 @@ msgstr ""
msgid "ChatMessage|in %{project_link}"
msgstr ""
-msgid "ChatMessage|passed"
-msgstr ""
-
msgid "Check again"
msgstr ""
@@ -4061,9 +4209,6 @@ msgstr ""
msgid "Click any <strong>project name</strong> in the project list below to navigate to the project milestone."
msgstr ""
-msgid "Click here"
-msgstr ""
-
msgid "Click the <strong>Download</strong> button and wait for downloading to complete."
msgstr ""
@@ -4451,6 +4596,12 @@ msgstr ""
msgid "ClusterIntegration|Fetching zones"
msgstr ""
+msgid "ClusterIntegration|Fluentd"
+msgstr ""
+
+msgid "ClusterIntegration|Fluentd is an open source data collector, which lets you unify the data collection and consumption for a better use and understanding of data. It requires at least one of the following logs to be successfully installed."
+msgstr ""
+
msgid "ClusterIntegration|GitLab Integration"
msgstr ""
@@ -4757,6 +4908,15 @@ msgstr ""
msgid "ClusterIntegration|Request to begin uninstalling failed"
msgstr ""
+msgid "ClusterIntegration|SIEM Hostname"
+msgstr ""
+
+msgid "ClusterIntegration|SIEM Port"
+msgstr ""
+
+msgid "ClusterIntegration|SIEM Protocol"
+msgstr ""
+
msgid "ClusterIntegration|Save changes"
msgstr ""
@@ -4853,6 +5013,12 @@ msgstr ""
msgid "ClusterIntegration|Select zone to choose machine type"
msgstr ""
+msgid "ClusterIntegration|Send Cilium Logs"
+msgstr ""
+
+msgid "ClusterIntegration|Send ModSecurity Logs"
+msgstr ""
+
msgid "ClusterIntegration|Service Token"
msgstr ""
@@ -5281,18 +5447,33 @@ msgstr ""
msgid "Compliance framework (optional)"
msgstr ""
+msgid "ComplianceFramework|GDPR"
+msgstr ""
+
msgid "ComplianceFramework|GDPR - General Data Protection Regulation"
msgstr ""
+msgid "ComplianceFramework|HIPAA"
+msgstr ""
+
msgid "ComplianceFramework|HIPAA - Health Insurance Portability and Accountability Act"
msgstr ""
+msgid "ComplianceFramework|PCI-DSS"
+msgstr ""
+
msgid "ComplianceFramework|PCI-DSS - Payment Card Industry-Data Security Standard"
msgstr ""
+msgid "ComplianceFramework|SOC 2"
+msgstr ""
+
msgid "ComplianceFramework|SOC 2 - Service Organization Control 2"
msgstr ""
+msgid "ComplianceFramework|SOX"
+msgstr ""
+
msgid "ComplianceFramework|SOX - Sarbanes-Oxley"
msgstr ""
@@ -5440,9 +5621,15 @@ msgstr ""
msgid "Container repositories sync capacity"
msgstr ""
+msgid "ContainerRegistry| Please visit the %{linkStart}administration settings%{linkEnd} to enable this feature."
+msgstr ""
+
msgid "ContainerRegistry|%{imageName} tags"
msgstr ""
+msgid "ContainerRegistry|%{title} was successfully scheduled for deletion"
+msgstr ""
+
msgid "ContainerRegistry|Automatically remove extra images that aren't designed to be kept."
msgstr ""
@@ -5455,6 +5642,9 @@ msgstr ""
msgid "ContainerRegistry|Container Registry"
msgstr ""
+msgid "ContainerRegistry|Container Registry tag expiration and retention policy is disabled"
+msgstr ""
+
msgid "ContainerRegistry|Copy build command"
msgstr ""
@@ -5464,18 +5654,12 @@ msgstr ""
msgid "ContainerRegistry|Copy push command"
msgstr ""
-msgid "ContainerRegistry|Currently, the Container Registry tag expiration feature is not available for projects created before GitLab version 12.8. For updates and more information, visit Issue %{linkStart}#196124%{linkEnd}"
-msgstr ""
-
msgid "ContainerRegistry|Docker connection error"
msgstr ""
msgid "ContainerRegistry|Docker tag expiration policy is %{toggleStatus}"
msgstr ""
-msgid "ContainerRegistry|Docker tags with names matching this regex pattern will expire:"
-msgstr ""
-
msgid "ContainerRegistry|Edit Settings"
msgstr ""
@@ -5497,9 +5681,6 @@ msgstr ""
msgid "ContainerRegistry|Image ID"
msgstr ""
-msgid "ContainerRegistry|Image deleted successfully"
-msgstr ""
-
msgid "ContainerRegistry|Keep and protect the images that matter most."
msgstr ""
@@ -5515,12 +5696,24 @@ msgstr ""
msgid "ContainerRegistry|Number of tags to retain:"
msgstr ""
+msgid "ContainerRegistry|Please contact your administrator."
+msgstr ""
+
msgid "ContainerRegistry|Push an image"
msgstr ""
msgid "ContainerRegistry|Quick Start"
msgstr ""
+msgid "ContainerRegistry|Regular expressions such as %{codeStart}.*-test%{codeEnd} or %{codeStart}dev-.*%{codeEnd} are supported"
+msgstr ""
+
+msgid "ContainerRegistry|Regular expressions such as %{codeStart}.*-test%{codeEnd} or %{codeStart}dev-.*%{codeEnd} are supported. To select all tags, use %{codeStart}.*%{codeEnd}"
+msgstr ""
+
+msgid "ContainerRegistry|Remember to run %{docLinkStart}garbage collection%{docLinkEnd} to remove the stale data from storage."
+msgstr ""
+
msgid "ContainerRegistry|Remove repository"
msgstr ""
@@ -5535,22 +5728,22 @@ msgstr[1] ""
msgid "ContainerRegistry|Retention policy has been Enabled"
msgstr ""
-msgid "ContainerRegistry|Something went wrong while deleting the image."
+msgid "ContainerRegistry|Something went wrong while fetching the expiration policy."
msgstr ""
-msgid "ContainerRegistry|Something went wrong while deleting the tag."
+msgid "ContainerRegistry|Something went wrong while fetching the repository list."
msgstr ""
-msgid "ContainerRegistry|Something went wrong while deleting the tags."
+msgid "ContainerRegistry|Something went wrong while fetching the tags list."
msgstr ""
-msgid "ContainerRegistry|Something went wrong while fetching the expiration policy."
+msgid "ContainerRegistry|Something went wrong while marking the tag for deletion."
msgstr ""
-msgid "ContainerRegistry|Something went wrong while fetching the packages list."
+msgid "ContainerRegistry|Something went wrong while marking the tags for deletion."
msgstr ""
-msgid "ContainerRegistry|Something went wrong while fetching the tags list."
+msgid "ContainerRegistry|Something went wrong while scheduling %{title} for deletion. Please try again."
msgstr ""
msgid "ContainerRegistry|Something went wrong while updating the expiration policy."
@@ -5559,16 +5752,25 @@ msgstr ""
msgid "ContainerRegistry|Tag"
msgstr ""
-msgid "ContainerRegistry|Tag deleted successfully"
-msgstr ""
-
msgid "ContainerRegistry|Tag expiration policy"
msgstr ""
msgid "ContainerRegistry|Tag expiration policy is designed to:"
msgstr ""
-msgid "ContainerRegistry|Tags deleted successfully"
+msgid "ContainerRegistry|Tag successfully marked for deletion."
+msgstr ""
+
+msgid "ContainerRegistry|Tags successfully marked for deletion."
+msgstr ""
+
+msgid "ContainerRegistry|Tags with names matching this regex pattern will %{italicStart}be preserved:%{italicEnd}"
+msgstr ""
+
+msgid "ContainerRegistry|Tags with names matching this regex pattern will %{italicStart}expire:%{italicEnd}"
+msgstr ""
+
+msgid "ContainerRegistry|The Container Registry tag expiration and retention policies for this project have not been enabled."
msgstr ""
msgid "ContainerRegistry|The last tag related to this image was recently removed. This empty image and any associated data will be automatically removed as part of the regular Garbage Collection process. If you have any questions, contact your administrator."
@@ -5589,16 +5791,16 @@ msgstr ""
msgid "ContainerRegistry|There are no container images stored for this project"
msgstr ""
-msgid "ContainerRegistry|This Registry contains deleted image tag data. Remember to run %{docLinkStart}garbage collection%{docLinkEnd} to remove the stale data from storage."
+msgid "ContainerRegistry|There was an error during the deletion of this image repository, please try again."
msgstr ""
msgid "ContainerRegistry|This image has no active tags"
msgstr ""
-msgid "ContainerRegistry|We are having trouble connecting to Docker, which could be due to an issue with your project name or path. %{docLinkStart}More Information%{docLinkEnd}"
+msgid "ContainerRegistry|This image repository is scheduled for deletion"
msgstr ""
-msgid "ContainerRegistry|Wildcards such as %{codeStart}.*-stable%{codeEnd} or %{codeStart}production/.*%{codeEnd} are supported. To select all tags, use %{codeStart}.*%{codeEnd}"
+msgid "ContainerRegistry|We are having trouble connecting to Docker, which could be due to an issue with your project name or path. %{docLinkStart}More Information%{docLinkEnd}"
msgstr ""
msgid "ContainerRegistry|With the Container Registry, every project can have its own space to store its Docker images. %{docLinkStart}More Information%{docLinkEnd}"
@@ -5853,7 +6055,7 @@ msgstr ""
msgid "Could not delete chat nickname %{chat_name}."
msgstr ""
-msgid "Could not find design"
+msgid "Could not find design."
msgstr ""
msgid "Could not remove the trigger."
@@ -5868,6 +6070,9 @@ msgstr ""
msgid "Could not revoke personal access token %{personal_access_token_name}."
msgstr ""
+msgid "Could not revoke project access token %{project_access_token_name}."
+msgstr ""
+
msgid "Could not save group ID"
msgstr ""
@@ -5925,6 +6130,9 @@ msgstr ""
msgid "Create a new branch"
msgstr ""
+msgid "Create a new deploy key for this project"
+msgstr ""
+
msgid "Create a new file as there are no files yet. Afterwards, you'll be able to commit your changes."
msgstr ""
@@ -6030,6 +6238,9 @@ msgstr ""
msgid "Create requirement"
msgstr ""
+msgid "Create snippet"
+msgstr ""
+
msgid "Create wildcard: %{searchTerm}"
msgstr ""
@@ -6051,6 +6262,9 @@ msgstr ""
msgid "Created"
msgstr ""
+msgid "Created %{timestamp}"
+msgstr ""
+
msgid "Created At"
msgstr ""
@@ -6407,6 +6621,9 @@ msgstr ""
msgid "CycleAnalytics|stage dropdown"
msgstr ""
+msgid "DAG"
+msgstr ""
+
msgid "DNS"
msgstr ""
@@ -6773,6 +6990,9 @@ msgstr ""
msgid "Deploy key was successfully updated."
msgstr ""
+msgid "Deploy keys allow read-only or read-write (if enabled) access to your repository. Deploy keys can be used for CI, staging or production servers. You can create a deploy key or add an existing one."
+msgstr ""
+
msgid "Deploy progress not found. To see pods, ensure your environment matches %{linkStart}deploy board criteria%{linkEnd}."
msgstr ""
@@ -6926,6 +7146,9 @@ msgstr ""
msgid "Deploying to"
msgstr ""
+msgid "Deployment Frequency"
+msgstr ""
+
msgid "Deployment|API"
msgstr ""
@@ -7444,9 +7667,6 @@ msgstr ""
msgid "Edit environment"
msgstr ""
-msgid "Edit epic description"
-msgstr ""
-
msgid "Edit file"
msgstr ""
@@ -8068,9 +8288,6 @@ msgstr ""
msgid "Epics"
msgstr ""
-msgid "Epics (Ultimate / Gold license only)"
-msgstr ""
-
msgid "Epics Roadmap"
msgstr ""
@@ -8080,6 +8297,9 @@ msgstr ""
msgid "Epics let you manage your portfolio of projects more efficiently and with less effort"
msgstr ""
+msgid "Epics, Issues, and Merge Requests"
+msgstr ""
+
msgid "Epics|Add an epic"
msgstr ""
@@ -8131,6 +8351,9 @@ msgstr ""
msgid "Epics|Something went wrong while fetching group epics."
msgstr ""
+msgid "Epics|Something went wrong while moving item."
+msgstr ""
+
msgid "Epics|Something went wrong while ordering item."
msgstr ""
@@ -8185,6 +8408,9 @@ msgstr ""
msgid "Error fetching network graph."
msgstr ""
+msgid "Error fetching payload data."
+msgstr ""
+
msgid "Error fetching projects"
msgstr ""
@@ -8194,9 +8420,6 @@ msgstr ""
msgid "Error fetching the dependency list. Please check your network connection and try again."
msgstr ""
-msgid "Error fetching usage ping data."
-msgstr ""
-
msgid "Error loading branch data. Please try again."
msgstr ""
@@ -8338,6 +8561,9 @@ msgstr ""
msgid "Errors"
msgstr ""
+msgid "Errors:"
+msgstr ""
+
msgid "Estimated"
msgstr ""
@@ -8542,12 +8768,18 @@ msgstr ""
msgid "Export as CSV"
msgstr ""
+msgid "Export group"
+msgstr ""
+
msgid "Export issues"
msgstr ""
msgid "Export project"
msgstr ""
+msgid "Export this group with all related data to a new GitLab instance. Once complete, you can import the data file from the \"New Group\" page."
+msgstr ""
+
msgid "Export this project with all its related data in order to move your project to a new GitLab instance. Once the export is finished, you can import the file from the \"New Project\" page."
msgstr ""
@@ -8617,9 +8849,6 @@ msgstr ""
msgid "Failed Jobs"
msgstr ""
-msgid "Failed create wiki"
-msgstr ""
-
msgid "Failed to add a Zoom meeting"
msgstr ""
@@ -8659,6 +8888,9 @@ msgstr ""
msgid "Failed to create resources"
msgstr ""
+msgid "Failed to create wiki"
+msgstr ""
+
msgid "Failed to delete board. Please try again."
msgstr ""
@@ -8668,7 +8900,7 @@ msgstr ""
msgid "Failed to enqueue the rebase operation, possibly due to a long-lived transaction. Try again later."
msgstr ""
-msgid "Failed to find import label for jira import."
+msgid "Failed to find import label for Jira import."
msgstr ""
msgid "Failed to get ref."
@@ -8692,6 +8924,12 @@ msgstr ""
msgid "Failed to load groups & users."
msgstr ""
+msgid "Failed to load labels. Please try again."
+msgstr ""
+
+msgid "Failed to load milestones. Please try again."
+msgstr ""
+
msgid "Failed to load related branches"
msgstr ""
@@ -9067,7 +9305,10 @@ msgstr ""
msgid "Filter by milestone name"
msgstr ""
-msgid "Filter by name..."
+msgid "Filter by name"
+msgstr ""
+
+msgid "Filter by status"
msgstr ""
msgid "Filter by two-factor authentication"
@@ -9079,12 +9320,18 @@ msgstr ""
msgid "Filter projects"
msgstr ""
+msgid "Filter results"
+msgstr ""
+
msgid "Filter results by group"
msgstr ""
msgid "Filter results by project"
msgstr ""
+msgid "Filter results..."
+msgstr ""
+
msgid "Filter your projects by name"
msgstr ""
@@ -9373,10 +9620,10 @@ msgstr ""
msgid "GeoNodes|Checksummed"
msgstr ""
-msgid "GeoNodes|Container repositories"
+msgid "GeoNodes|Consult Geo troubleshooting information"
msgstr ""
-msgid "GeoNodes|Data is out of date from %{timeago}"
+msgid "GeoNodes|Container repositories"
msgstr ""
msgid "GeoNodes|Data replication lag"
@@ -9418,6 +9665,9 @@ msgstr ""
msgid "GeoNodes|Last event ID seen from primary"
msgstr ""
+msgid "GeoNodes|Learn more about Geo node statuses"
+msgstr ""
+
msgid "GeoNodes|Loading nodes"
msgstr ""
@@ -9433,6 +9683,9 @@ msgstr ""
msgid "GeoNodes|Node was successfully removed."
msgstr ""
+msgid "GeoNodes|Node's status was updated %{timeAgo}."
+msgstr ""
+
msgid "GeoNodes|Not checksummed"
msgstr ""
@@ -9463,7 +9716,10 @@ msgstr ""
msgid "GeoNodes|Repository verification progress"
msgstr ""
-msgid "GeoNodes|Selective"
+msgid "GeoNodes|Selective (%{syncLabel})"
+msgstr ""
+
+msgid "GeoNodes|Selective synchronization"
msgstr ""
msgid "GeoNodes|Something went wrong while changing node status"
@@ -9490,6 +9746,9 @@ msgstr ""
msgid "GeoNodes|Unverified"
msgstr ""
+msgid "GeoNodes|Updated %{timeAgo}"
+msgstr ""
+
msgid "GeoNodes|Used slots"
msgstr ""
@@ -9529,16 +9788,16 @@ msgstr ""
msgid "Geo|All"
msgstr ""
-msgid "Geo|All projects"
+msgid "Geo|All %{replicable_type}"
msgstr ""
-msgid "Geo|All projects are being scheduled for re-sync"
+msgid "Geo|All projects"
msgstr ""
-msgid "Geo|All projects are being scheduled for re-verify"
+msgid "Geo|All projects are being scheduled for resync"
msgstr ""
-msgid "Geo|Batch operations"
+msgid "Geo|All projects are being scheduled for reverify"
msgstr ""
msgid "Geo|Could not remove tracking entry for an existing project."
@@ -9550,9 +9809,15 @@ msgstr ""
msgid "Geo|Failed"
msgstr ""
+msgid "Geo|Filter by status"
+msgstr ""
+
msgid "Geo|Geo Status"
msgstr ""
+msgid "Geo|In progress"
+msgstr ""
+
msgid "Geo|In sync"
msgstr ""
@@ -9577,9 +9842,6 @@ msgstr ""
msgid "Geo|Not synced yet"
msgstr ""
-msgid "Geo|Pending"
-msgstr ""
-
msgid "Geo|Pending synchronization"
msgstr ""
@@ -9616,7 +9878,7 @@ msgstr ""
msgid "Geo|Resync"
msgstr ""
-msgid "Geo|Resync all projects"
+msgid "Geo|Resync all"
msgstr ""
msgid "Geo|Retry count"
@@ -9625,7 +9887,7 @@ msgstr ""
msgid "Geo|Reverify"
msgstr ""
-msgid "Geo|Reverify all projects"
+msgid "Geo|Reverify all"
msgstr ""
msgid "Geo|Status"
@@ -9967,9 +10229,6 @@ msgstr ""
msgid "Go to file"
msgstr ""
-msgid "Go to file (MRs only)"
-msgstr ""
-
msgid "Go to file permalink (while viewing a file)"
msgstr ""
@@ -10414,6 +10673,9 @@ msgstr ""
msgid "GroupSettings|Disable group mentions"
msgstr ""
+msgid "GroupSettings|Export group"
+msgstr ""
+
msgid "GroupSettings|If the parent group's visibility is lower than the group current visibility, visibility levels for subgroups and projects will be changed to match the new parent group's visibility."
msgstr ""
@@ -10492,6 +10754,9 @@ msgstr ""
msgid "Groups (%{count})"
msgstr ""
+msgid "Groups (%{groups})"
+msgstr ""
+
msgid "Groups can also be nested by creating %{subgroup_docs_link_start}subgroups%{subgroup_docs_link_end}."
msgstr ""
@@ -10757,15 +11022,12 @@ msgstr ""
msgid "ID:"
msgstr ""
-msgid "IDE|Allow live previews of JavaScript projects in the Web IDE using CodeSandbox client side evaluation."
+msgid "IDE|Allow live previews of JavaScript projects in the Web IDE using CodeSandbox Live Preview."
msgstr ""
msgid "IDE|Back"
msgstr ""
-msgid "IDE|Client side evaluation"
-msgstr ""
-
msgid "IDE|Commit"
msgstr ""
@@ -10829,6 +11091,9 @@ msgstr ""
msgid "If any job surpasses this timeout threshold, it will be marked as failed. Human readable time input language is accepted like \"1 hour\". Values without specification represent seconds."
msgstr ""
+msgid "If blank, set allowable lifetime to %{instance_level_policy_in_words}, as defined by the instance admin. Once set, existing tokens for users in this group may be revoked."
+msgstr ""
+
msgid "If checked, group owners can manage LDAP group links and LDAP member overrides"
msgstr ""
@@ -10868,6 +11133,9 @@ msgstr ""
msgid "If you lose your recovery codes you can generate new ones, invalidating all previous codes."
msgstr ""
+msgid "If you reach 100%% storage capacity, you will not be able to: %{base_message}"
+msgstr ""
+
msgid "If your HTTP repository is not publicly accessible, add your credentials."
msgstr ""
@@ -10979,6 +11247,9 @@ msgstr ""
msgid "Import repository"
msgstr ""
+msgid "Import started by: %{importInitiator}"
+msgstr ""
+
msgid "Import tasks"
msgstr ""
@@ -11045,6 +11316,9 @@ msgstr ""
msgid "In order to tailor your experience with GitLab we<br>would like to know a bit more about you."
msgstr ""
+msgid "In progress"
+msgstr ""
+
msgid "In the next step, you'll be able to select the projects you want to import."
msgstr ""
@@ -11392,15 +11666,15 @@ msgstr ""
msgid "Issues"
msgstr ""
-msgid "Issues / Merge Requests"
-msgstr ""
-
msgid "Issues Analytics"
msgstr ""
msgid "Issues Rate Limits"
msgstr ""
+msgid "Issues and Merge Requests"
+msgstr ""
+
msgid "Issues can be bugs, tasks or ideas to be discussed. Also, issues are searchable and filterable."
msgstr ""
@@ -11422,10 +11696,10 @@ msgstr ""
msgid "IssuesAnalytics|Avg/Month:"
msgstr ""
-msgid "IssuesAnalytics|Issues created"
+msgid "IssuesAnalytics|Issues opened"
msgstr ""
-msgid "IssuesAnalytics|Issues created per month"
+msgid "IssuesAnalytics|Issues opened per month"
msgstr ""
msgid "IssuesAnalytics|Last 12 months"
@@ -11479,6 +11753,9 @@ msgstr ""
msgid "Jira integration not configured."
msgstr ""
+msgid "Jira project: %{importProject}"
+msgstr ""
+
msgid "JiraService|Events for %{noteable_model_name} are disabled."
msgstr ""
@@ -11868,6 +12145,9 @@ msgstr ""
msgid "Lead"
msgstr ""
+msgid "Lead Time"
+msgstr ""
+
msgid "Learn GitLab"
msgstr ""
@@ -11880,6 +12160,9 @@ msgstr ""
msgid "Learn how to %{no_packages_link_start}publish and share your packages%{no_packages_link_end} with GitLab."
msgstr ""
+msgid "Learn how to enable synchronization"
+msgstr ""
+
msgid "Learn more"
msgstr ""
@@ -12211,9 +12494,6 @@ msgstr ""
msgid "Live preview"
msgstr ""
-msgid "Load more vulnerabilities"
-msgstr ""
-
msgid "Loading"
msgstr ""
@@ -12364,9 +12644,6 @@ msgstr ""
msgid "Manage"
msgstr ""
-msgid "Manage Git repositories with fine-grained access controls that keep your code secure. Perform code reviews and enhance collaboration with merge requests. Each project can also have an issue tracker and a wiki."
-msgstr ""
-
msgid "Manage Web IDE features"
msgstr ""
@@ -12397,6 +12674,9 @@ msgstr ""
msgid "Manage your license"
msgstr ""
+msgid "Managed Account"
+msgstr ""
+
msgid "Manifest"
msgstr ""
@@ -12661,6 +12941,9 @@ msgstr ""
msgid "Merge Requests in Review"
msgstr ""
+msgid "Merge automatically (%{strategy})"
+msgstr ""
+
msgid "Merge commit message"
msgstr ""
@@ -12814,6 +13097,12 @@ msgstr ""
msgid "Merged branches are being deleted. This can take some time depending on the number of branches. Please refresh the page to see changes."
msgstr ""
+msgid "Merged this merge request."
+msgstr ""
+
+msgid "Merges this merge request immediately."
+msgstr ""
+
msgid "Merges this merge request when the pipeline succeeds."
msgstr ""
@@ -12921,12 +13210,15 @@ msgid_plural "Metrics|Edit metrics"
msgstr[0] ""
msgstr[1] ""
-msgid "Metrics|Environment"
+msgid "Metrics|Expand panel"
msgstr ""
msgid "Metrics|For grouping similar metrics"
msgstr ""
+msgid "Metrics|Go back (Esc)"
+msgstr ""
+
msgid "Metrics|Invalid time range, please verify."
msgstr ""
@@ -12957,9 +13249,6 @@ msgstr ""
msgid "Metrics|Refresh dashboard"
msgstr ""
-msgid "Metrics|Show last"
-msgstr ""
-
msgid "Metrics|There was an error creating the dashboard."
msgstr ""
@@ -13038,6 +13327,9 @@ msgstr ""
msgid "Microsoft Azure"
msgstr ""
+msgid "Middleman project with Static Site Editor support"
+msgstr ""
+
msgid "Migrated %{success_count}/%{total_count} files."
msgstr ""
@@ -13483,10 +13775,10 @@ msgstr ""
msgid "Next"
msgstr ""
-msgid "Next file in diff (MRs only)"
+msgid "Next file in diff"
msgstr ""
-msgid "Next unresolved discussion (MRs only)"
+msgid "Next unresolved discussion"
msgstr ""
msgid "Nickname"
@@ -13546,6 +13838,9 @@ msgstr ""
msgid "No changes between %{ref_start}%{source_branch}%{ref_end} and %{ref_start}%{target_branch}%{ref_end}"
msgstr ""
+msgid "No child epics match applied filters"
+msgstr ""
+
msgid "No connection could be made to a Gitaly Server, please check your logs!"
msgstr ""
@@ -13684,15 +13979,6 @@ msgstr ""
msgid "No thanks, don't show this again"
msgstr ""
-msgid "No vulnerabilities found for this group"
-msgstr ""
-
-msgid "No vulnerabilities found for this pipeline"
-msgstr ""
-
-msgid "No vulnerabilities found for this project"
-msgstr ""
-
msgid "No vulnerabilities present"
msgstr ""
@@ -13816,6 +14102,9 @@ msgstr ""
msgid "Nothing to preview."
msgstr ""
+msgid "Nothing to synchronize"
+msgstr ""
+
msgid "Notification events"
msgstr ""
@@ -13987,6 +14276,9 @@ msgstr ""
msgid "Once removed, the fork relationship cannot be restored and you will no longer be able to send merge requests to the source."
msgstr ""
+msgid "Once the exported file is ready you can download it from this page."
+msgstr ""
+
msgid "Once the exported file is ready, you will receive a notification email with a download link, or you can download it from this page."
msgstr ""
@@ -14088,9 +14380,6 @@ msgstr ""
msgid "Open sidebar"
msgstr ""
-msgid "Open source software to collaborate on code"
-msgstr ""
-
msgid "Open: %{openIssuesCount}"
msgstr ""
@@ -14193,6 +14482,9 @@ msgstr ""
msgid "OutdatedBrowser|You can provide feedback %{feedback_link_start}on this issue%{feedback_link_end} or via your usual support channels."
msgstr ""
+msgid "Overridden"
+msgstr ""
+
msgid "Overview"
msgstr ""
@@ -14244,6 +14536,9 @@ msgstr ""
msgid "PackageRegistry|Conan Command"
msgstr ""
+msgid "PackageRegistry|Copy .pypirc content"
+msgstr ""
+
msgid "PackageRegistry|Copy Conan Command"
msgstr ""
@@ -14265,6 +14560,9 @@ msgstr ""
msgid "PackageRegistry|Copy NuGet Setup Command"
msgstr ""
+msgid "PackageRegistry|Copy Pip command"
+msgstr ""
+
msgid "PackageRegistry|Copy and paste this inside your %{codeStart}pom.xml%{codeEnd} %{codeStart}dependencies%{codeEnd} block."
msgstr ""
@@ -14298,6 +14596,12 @@ msgstr ""
msgid "PackageRegistry|For more information on the NuGet registry, %{linkStart}see the documentation%{linkEnd}."
msgstr ""
+msgid "PackageRegistry|For more information on the PyPi registry, %{linkStart}see the documentation%{linkEnd}."
+msgstr ""
+
+msgid "PackageRegistry|If you haven't already done so, you will need to add the below to your %{codeStart}.pypirc%{codeEnd} file."
+msgstr ""
+
msgid "PackageRegistry|If you haven't already done so, you will need to add the below to your %{codeStart}pom.xml%{codeEnd} file."
msgstr ""
@@ -14307,6 +14611,9 @@ msgstr ""
msgid "PackageRegistry|Learn how to %{noPackagesLinkStart}publish and share your packages%{noPackagesLinkEnd} with GitLab."
msgstr ""
+msgid "PackageRegistry|Manually Published"
+msgstr ""
+
msgid "PackageRegistry|Maven"
msgstr ""
@@ -14325,12 +14632,18 @@ msgstr ""
msgid "PackageRegistry|NuGet Command"
msgstr ""
+msgid "PackageRegistry|Pip Command"
+msgstr ""
+
msgid "PackageRegistry|Pipeline %{linkStart}%{linkEnd} triggered %{timestamp} by %{author}"
msgstr ""
msgid "PackageRegistry|Published to the repository at %{timestamp}"
msgstr ""
+msgid "PackageRegistry|PyPi"
+msgstr ""
+
msgid "PackageRegistry|Registry Setup"
msgstr ""
@@ -14367,6 +14680,9 @@ msgstr ""
msgid "PackageRegistry|npm"
msgstr ""
+msgid "PackageRegistry|published by %{author}"
+msgstr ""
+
msgid "PackageRegistry|yarn"
msgstr ""
@@ -14382,6 +14698,9 @@ msgstr ""
msgid "PackageType|NuGet"
msgstr ""
+msgid "PackageType|PyPi"
+msgstr ""
+
msgid "Packages"
msgstr ""
@@ -14475,6 +14794,9 @@ msgstr ""
msgid "Past due"
msgstr ""
+msgid "Paste a machine public key here. Read more about how to generate it"
+msgstr ""
+
msgid "Paste a machine public key here. Read more about how to generate it %{link_start}here%{link_end}"
msgstr ""
@@ -14490,9 +14812,6 @@ msgstr ""
msgid "Path"
msgstr ""
-msgid "Path, transfer, remove"
-msgstr ""
-
msgid "Path:"
msgstr ""
@@ -14523,7 +14842,7 @@ msgstr ""
msgid "Percentage"
msgstr ""
-msgid "Perform advanced options such as changing path, transferring, or removing the group."
+msgid "Perform advanced options such as changing path, transferring, exporting, or removing the group."
msgstr ""
msgid "Perform common operations on GitLab project"
@@ -14604,9 +14923,6 @@ msgstr ""
msgid "Pipeline minutes quota"
msgstr ""
-msgid "Pipeline quota"
-msgstr ""
-
msgid "Pipeline subscriptions"
msgstr ""
@@ -14616,6 +14932,9 @@ msgstr ""
msgid "Pipeline: %{status}"
msgstr ""
+msgid "PipelineCharts|CI / CD Analytics"
+msgstr ""
+
msgid "PipelineCharts|Failed:"
msgstr ""
@@ -14694,15 +15013,6 @@ msgstr ""
msgid "Pipelines settings for '%{project_name}' were successfully updated."
msgstr ""
-msgid "Pipelines| to purchase more minutes."
-msgstr ""
-
-msgid "Pipelines|%{namespace_name} has exceeded its pipeline minutes quota."
-msgstr ""
-
-msgid "Pipelines|%{namespace_name} has less than %{notification_level}%% of CI minutes available."
-msgstr ""
-
msgid "Pipelines|API"
msgstr ""
@@ -14724,10 +15034,13 @@ msgstr ""
msgid "Pipelines|Get started with Pipelines"
msgstr ""
-msgid "Pipelines|Loading Pipelines"
+msgid "Pipelines|Group %{namespace_name} has %{percentage}%% or less Shared Runner Pipeline minutes remaining. Once it runs out, no new jobs or pipelines in its projects will run."
msgstr ""
-msgid "Pipelines|Pipelines will not run anymore on shared Runners."
+msgid "Pipelines|Group %{namespace_name} has exceeded its pipeline minutes quota. Unless you buy additional pipeline minutes, no new jobs or pipelines in its projects will run."
+msgstr ""
+
+msgid "Pipelines|Loading Pipelines"
msgstr ""
msgid "Pipelines|Project cache successfully reset."
@@ -14958,6 +15271,9 @@ msgstr ""
msgid "Please select"
msgstr ""
+msgid "Please select a Jira project"
+msgstr ""
+
msgid "Please select a country"
msgstr ""
@@ -15003,6 +15319,9 @@ msgstr ""
msgid "Please wait while we import the repository for you. Refresh at will."
msgstr ""
+msgid "Plugins directory is deprecated and will be removed in 14.0. Please move this file into /file_hooks directory."
+msgstr ""
+
msgid "Pod does not exist"
msgstr ""
@@ -15120,7 +15439,7 @@ msgstr ""
msgid "Prevent users from changing their profile name"
msgstr ""
-msgid "Prevent users from modifing merge request approvers list"
+msgid "Prevent users from modifying merge request approvers list"
msgstr ""
msgid "Prevent users from performing write operations on GitLab while performing maintenance."
@@ -15141,10 +15460,10 @@ msgstr ""
msgid "Previous Artifacts"
msgstr ""
-msgid "Previous file in diff (MRs only)"
+msgid "Previous file in diff"
msgstr ""
-msgid "Previous unresolved discussion (MRs only)"
+msgid "Previous unresolved discussion"
msgstr ""
msgid "Primary"
@@ -15177,6 +15496,9 @@ msgstr ""
msgid "Private profile"
msgstr ""
+msgid "Private projects Minutes cost factor"
+msgstr ""
+
msgid "Private projects can be created in your personal namespace with:"
msgstr ""
@@ -16005,6 +16327,9 @@ msgstr ""
msgid "ProjectSettings|With GitLab Pages you can host your static websites on GitLab"
msgstr ""
+msgid "ProjectSettings|With Metrics Dashboard you can visualize this project performance metrics"
+msgstr ""
+
msgid "ProjectTemplates|.NET Core"
msgstr ""
@@ -16068,6 +16393,9 @@ msgstr ""
msgid "ProjectTemplates|Spring"
msgstr ""
+msgid "ProjectTemplates|Static Site Editor/Middleman"
+msgstr ""
+
msgid "ProjectTemplates|iOS (Swift)"
msgstr ""
@@ -16287,13 +16615,13 @@ msgstr ""
msgid "PrometheusService|Prometheus is being automatically managed on your clusters"
msgstr ""
-msgid "PrometheusService|These metrics will only be monitored after your first deployment to an environment"
+msgid "PrometheusService|Select the Active checkbox to override the Auto Configuration with custom settings. If unchecked, Auto Configuration settings are used."
msgstr ""
-msgid "PrometheusService|Time-series monitoring service"
+msgid "PrometheusService|These metrics will only be monitored after your first deployment to an environment"
msgstr ""
-msgid "PrometheusService|To enable manual configuration, uninstall Prometheus from your clusters"
+msgid "PrometheusService|Time-series monitoring service"
msgstr ""
msgid "PrometheusService|To enable the installation of Prometheus on your clusters, deactivate the manual configuration below"
@@ -16479,6 +16807,9 @@ msgstr ""
msgid "Protip:"
msgstr ""
+msgid "Protocol"
+msgstr ""
+
msgid "Provider"
msgstr ""
@@ -16503,6 +16834,9 @@ msgstr ""
msgid "Public pipelines"
msgstr ""
+msgid "Public projects Minutes cost factor"
+msgstr ""
+
msgid "Pull"
msgstr ""
@@ -16703,6 +17037,9 @@ msgid_plural "Refreshing in %d seconds to show the updated status..."
msgstr[0] ""
msgstr[1] ""
+msgid "Regenerate export"
+msgstr ""
+
msgid "Regenerate instance ID"
msgstr ""
@@ -17080,9 +17417,29 @@ msgstr ""
msgid "Reports|%{combinedString} and %{resolvedString}"
msgstr ""
+msgid "Reports|Accessibility scanning detected %d issue for the source branch only"
+msgid_plural "Reports|Accessibility scanning detected %d issues for the source branch only"
+msgstr[0] ""
+msgstr[1] ""
+
+msgid "Reports|Accessibility scanning detected no issues for the source branch only"
+msgstr ""
+
+msgid "Reports|Accessibility scanning failed loading results"
+msgstr ""
+
+msgid "Reports|Accessibility scanning results are being parsed"
+msgstr ""
+
msgid "Reports|Actions"
msgstr ""
+msgid "Reports|An error occured while loading report"
+msgstr ""
+
+msgid "Reports|An error occurred while loading %{name} results"
+msgstr ""
+
msgid "Reports|Class"
msgstr ""
@@ -17188,7 +17545,7 @@ msgstr ""
msgid "Requested %{time_ago}"
msgstr ""
-msgid "Requested design version does not exist"
+msgid "Requested design version does not exist."
msgstr ""
msgid "Requested states are invalid"
@@ -17372,6 +17729,9 @@ msgstr ""
msgid "Resync"
msgstr ""
+msgid "Resync all"
+msgstr ""
+
msgid "Resync all %{replicableType}"
msgstr ""
@@ -17440,6 +17800,9 @@ msgstr ""
msgid "Revoked personal access token %{personal_access_token_name}!"
msgstr ""
+msgid "Revoked project access token %{project_access_token_name}!"
+msgstr ""
+
msgid "RightSidebar|adding a"
msgstr ""
@@ -17488,6 +17851,9 @@ msgstr ""
msgid "Runner token"
msgstr ""
+msgid "Runner tokens"
+msgstr ""
+
msgid "Runner was not updated."
msgstr ""
@@ -17542,6 +17908,9 @@ msgstr ""
msgid "SAML SSO for %{group_name}"
msgstr ""
+msgid "SAML discovery tokens"
+msgstr ""
+
msgid "SAML for %{group_name}"
msgstr ""
@@ -17626,12 +17995,18 @@ msgstr ""
msgid "Scheduled"
msgstr ""
+msgid "Scheduled to merge this merge request (%{strategy})."
+msgstr ""
+
msgid "Scheduled to merge this merge request when the pipeline succeeds."
msgstr ""
msgid "Schedules"
msgstr ""
+msgid "Schedules to merge this merge request (%{strategy})."
+msgstr ""
+
msgid "Scheduling"
msgstr ""
@@ -17689,6 +18064,9 @@ msgstr ""
msgid "Search branches and tags"
msgstr ""
+msgid "Search by author"
+msgstr ""
+
msgid "Search files"
msgstr ""
@@ -17834,11 +18212,6 @@ msgid_plural "SearchResults|snippets"
msgstr[0] ""
msgstr[1] ""
-msgid "SearchResults|snippet result"
-msgid_plural "SearchResults|snippet results"
-msgstr[0] ""
-msgstr[1] ""
-
msgid "SearchResults|user"
msgid_plural "SearchResults|users"
msgstr[0] ""
@@ -17849,6 +18222,9 @@ msgid_plural "SearchResults|wiki results"
msgstr[0] ""
msgstr[1] ""
+msgid "Searching by both author and message is currently not supported."
+msgstr ""
+
msgid "Seat Link"
msgstr ""
@@ -17876,184 +18252,232 @@ msgstr ""
msgid "Security Dashboard"
msgstr ""
-msgid "Security Dashboard|Error fetching the vulnerability counts. Please check your network connection and try again."
+msgid "Security configuration help link"
+msgstr ""
+
+msgid "Security dashboard"
msgstr ""
-msgid "Security Dashboard|Error fetching the vulnerability list. Please check your network connection and try again."
+msgid "Security report is out of date. Please update your branch with the latest changes from the target branch (%{targetBranchName})"
msgstr ""
-msgid "Security Dashboard|Issue Created"
+msgid "Security report is out of date. Run %{newPipelineLinkStart}a new pipeline%{newPipelineLinkEnd} for the target branch (%{targetBranchName})"
msgstr ""
-msgid "Security Reports|Comment added to '%{vulnerabilityName}'"
+msgid "SecurityConfiguration|Configured"
msgstr ""
-msgid "Security Reports|Comment deleted on '%{vulnerabilityName}'"
+msgid "SecurityConfiguration|Feature"
msgstr ""
-msgid "Security Reports|Comment edited on '%{vulnerabilityName}'"
+msgid "SecurityConfiguration|Feature documentation for %{featureName}"
msgstr ""
-msgid "Security Reports|Create issue"
+msgid "SecurityConfiguration|Not yet configured"
msgstr ""
-msgid "Security Reports|Dismiss vulnerability"
+msgid "SecurityConfiguration|Secure features"
msgstr ""
-msgid "Security Reports|Dismissed '%{vulnerabilityName}'"
+msgid "SecurityConfiguration|Status"
msgstr ""
-msgid "Security Reports|Dismissed '%{vulnerabilityName}'. Turn off the hide dismissed toggle to view."
+msgid "SecurityReports|%{firstProject} and %{secondProject}"
msgstr ""
-msgid "Security Reports|Either you don't have permission to view this dashboard or the dashboard has not been setup. Please check your permission settings with your administrator or check your dashboard configurations to proceed."
+msgid "SecurityReports|%{firstProject}, %{secondProject}, and %{rest}"
msgstr ""
-msgid "Security Reports|Learn more about setting up your dashboard"
+msgid "SecurityReports|Add a project to your dashboard"
msgstr ""
-msgid "Security Reports|More info"
+msgid "SecurityReports|Add or remove projects from your dashboard"
msgstr ""
-msgid "Security Reports|Oops, something doesn't seem right."
+msgid "SecurityReports|Add projects"
msgstr ""
-msgid "Security Reports|Security reports can only be accessed by authorized users."
+msgid "SecurityReports|Comment added to '%{vulnerabilityName}'"
msgstr ""
-msgid "Security Reports|There was an error adding the comment."
+msgid "SecurityReports|Comment deleted on '%{vulnerabilityName}'"
msgstr ""
-msgid "Security Reports|There was an error creating the issue."
+msgid "SecurityReports|Comment edited on '%{vulnerabilityName}'"
msgstr ""
-msgid "Security Reports|There was an error creating the merge request."
+msgid "SecurityReports|Create issue"
msgstr ""
-msgid "Security Reports|There was an error deleting the comment."
+msgid "SecurityReports|Dismiss Selected"
msgstr ""
-msgid "Security Reports|There was an error dismissing the vulnerabilities."
+msgid "SecurityReports|Dismiss vulnerability"
msgstr ""
-msgid "Security Reports|There was an error dismissing the vulnerability."
+msgid "SecurityReports|Dismissed '%{vulnerabilityName}'"
msgstr ""
-msgid "Security Reports|There was an error reverting the dismissal."
+msgid "SecurityReports|Dismissed '%{vulnerabilityName}'. Turn off the hide dismissed toggle to view."
msgstr ""
-msgid "Security Reports|There was an error reverting this dismissal."
+msgid "SecurityReports|Each vulnerability now has a unique page that can be directly linked to, shared, referenced, and tracked as the single source of truth. Vulnerability occurrences also persist across scanner runs, which improves tracking and visibility and reduces duplicates between scans."
msgstr ""
-msgid "Security Reports|Undo dismiss"
+msgid "SecurityReports|Edit dashboard"
msgstr ""
-msgid "Security Reports|You do not have sufficient permissions to access this report"
+msgid "SecurityReports|Either you don't have permission to view this dashboard or the dashboard has not been setup. Please check your permission settings with your administrator or check your dashboard configurations to proceed."
msgstr ""
-msgid "Security Reports|You must sign in as an authorized user to see this report"
+msgid "SecurityReports|Error fetching the vulnerability counts. Please check your network connection and try again."
msgstr ""
-msgid "Security configuration help link"
+msgid "SecurityReports|Error fetching the vulnerability list. Please check your network connection and try again."
msgstr ""
-msgid "Security dashboard"
+msgid "SecurityReports|False positive"
msgstr ""
-msgid "Security report is out of date. Please update your branch with the latest changes from the target branch (%{targetBranchName})"
+msgid "SecurityReports|Hide dismissed"
msgstr ""
-msgid "Security report is out of date. Run %{newPipelineLinkStart}a new pipeline%{newPipelineLinkEnd} for the target branch (%{targetBranchName})"
+msgid "SecurityReports|Introducing standalone vulnerabilities"
msgstr ""
-msgid "SecurityConfiguration|Configured"
+msgid "SecurityReports|Issue Created"
msgstr ""
-msgid "SecurityConfiguration|Feature"
+msgid "SecurityReports|Learn More"
msgstr ""
-msgid "SecurityConfiguration|Feature documentation for %{featureName}"
+msgid "SecurityReports|Learn more about setting up your dashboard"
msgstr ""
-msgid "SecurityConfiguration|Not yet configured"
+msgid "SecurityReports|Load more vulnerabilities"
msgstr ""
-msgid "SecurityConfiguration|Secure features"
+msgid "SecurityReports|Monitor vulnerabilities in your code"
msgstr ""
-msgid "SecurityConfiguration|Status"
+msgid "SecurityReports|More info"
msgstr ""
-msgid "SecurityDashboard|%{firstProject} and %{secondProject}"
+msgid "SecurityReports|More information"
msgstr ""
-msgid "SecurityDashboard|%{firstProject}, %{secondProject}, and %{rest}"
+msgid "SecurityReports|No vulnerabilities found for dashboard"
msgstr ""
-msgid "SecurityDashboard|Add a project to your dashboard"
+msgid "SecurityReports|No vulnerabilities found for this group"
msgstr ""
-msgid "SecurityDashboard|Add or remove projects from your dashboard"
+msgid "SecurityReports|No vulnerabilities found for this pipeline"
msgstr ""
-msgid "SecurityDashboard|Add projects"
+msgid "SecurityReports|No vulnerabilities found for this project"
msgstr ""
-msgid "SecurityDashboard|Edit dashboard"
+msgid "SecurityReports|Oops, something doesn't seem right."
msgstr ""
-msgid "SecurityDashboard|Hide dismissed"
+msgid "SecurityReports|Pipeline %{pipelineLink} triggered %{timeago} by %{user}"
msgstr ""
-msgid "SecurityDashboard|Monitor vulnerabilities in your code"
+msgid "SecurityReports|Project"
msgstr ""
-msgid "SecurityDashboard|More information"
+msgid "SecurityReports|Projects added"
msgstr ""
-msgid "SecurityDashboard|No vulnerabilities found for dashboard"
+msgid "SecurityReports|Remove project from dashboard"
msgstr ""
-msgid "SecurityDashboard|Pipeline %{pipelineLink} triggered %{timeago} by %{user}"
+msgid "SecurityReports|Report type"
msgstr ""
-msgid "SecurityDashboard|Project"
+msgid "SecurityReports|Return to dashboard"
msgstr ""
-msgid "SecurityDashboard|Projects added"
+msgid "SecurityReports|Security Dashboard"
msgstr ""
-msgid "SecurityDashboard|Remove project from dashboard"
+msgid "SecurityReports|Security reports can only be accessed by authorized users."
msgstr ""
-msgid "SecurityDashboard|Report type"
+msgid "SecurityReports|Select a project to add by using the project search field above."
msgstr ""
-msgid "SecurityDashboard|Return to dashboard"
+msgid "SecurityReports|Select a reason"
msgstr ""
-msgid "SecurityDashboard|Security Dashboard"
+msgid "SecurityReports|Severity"
msgstr ""
-msgid "SecurityDashboard|Select a project to add by using the project search field above."
+msgid "SecurityReports|Status"
msgstr ""
-msgid "SecurityDashboard|Severity"
+msgid "SecurityReports|The security dashboard displays the latest security findings for projects you wish to monitor. Select \"Edit dashboard\" to add and remove projects."
msgstr ""
-msgid "SecurityDashboard|Status"
+msgid "SecurityReports|The security dashboard displays the latest security report. Use it to find and fix vulnerabilities."
msgstr ""
-msgid "SecurityDashboard|The security dashboard displays the latest security findings for projects you wish to monitor. Select \"Edit dashboard\" to add and remove projects."
+msgid "SecurityReports|There was an error adding the comment."
msgstr ""
-msgid "SecurityDashboard|The security dashboard displays the latest security report. Use it to find and fix vulnerabilities."
+msgid "SecurityReports|There was an error creating the issue."
msgstr ""
-msgid "SecurityDashboard|There was an error while generating the report."
+msgid "SecurityReports|There was an error creating the merge request."
msgstr ""
-msgid "SecurityDashboard|Unable to add %{invalidProjects}"
+msgid "SecurityReports|There was an error deleting the comment."
+msgstr ""
+
+msgid "SecurityReports|There was an error dismissing the vulnerabilities."
+msgstr ""
+
+msgid "SecurityReports|There was an error dismissing the vulnerability."
+msgstr ""
+
+msgid "SecurityReports|There was an error reverting the dismissal."
+msgstr ""
+
+msgid "SecurityReports|There was an error reverting this dismissal."
+msgstr ""
+
+msgid "SecurityReports|There was an error while generating the report."
+msgstr ""
+
+msgid "SecurityReports|Unable to add %{invalidProjects}"
+msgstr ""
+
+msgid "SecurityReports|Undo dismiss"
+msgstr ""
+
+msgid "SecurityReports|While it's rare to have no vulnerabilities for your group, it can happen. In any event, we ask that you double check your settings to make sure you've set up your dashboard correctly."
+msgstr ""
+
+msgid "SecurityReports|While it's rare to have no vulnerabilities for your pipeline, it can happen. In any event, we ask that you double check your settings to make sure all security scanning jobs have passed successfully."
+msgstr ""
+
+msgid "SecurityReports|While it's rare to have no vulnerabilities for your project, it can happen. In any event, we ask that you double check your settings to make sure you've set up your dashboard correctly."
+msgstr ""
+
+msgid "SecurityReports|While it's rare to have no vulnerabilities, it can happen. In any event, we ask that you please double check your settings to make sure you've set up your dashboard correctly."
+msgstr ""
+
+msgid "SecurityReports|Won't fix / Accept risk"
+msgstr ""
+
+msgid "SecurityReports|You do not have sufficient permissions to access this report"
+msgstr ""
+
+msgid "SecurityReports|You must sign in as an authorized user to see this report"
+msgstr ""
+
+msgid "SecurityReports|[No reason]"
msgstr ""
msgid "See GitLab's %{password_policy_guidelines}"
@@ -18065,6 +18489,9 @@ msgstr ""
msgid "See the affected projects in the GitLab admin panel"
msgstr ""
+msgid "See what's new at GitLab"
+msgstr ""
+
msgid "Select"
msgstr ""
@@ -18470,9 +18897,6 @@ msgstr ""
msgid "Set up Jira Integration"
msgstr ""
-msgid "Set up Jira Integration illustration"
-msgstr ""
-
msgid "Set up a %{type} Runner automatically"
msgstr ""
@@ -18557,6 +18981,9 @@ msgstr ""
msgid "Severity: %{severity}"
msgstr ""
+msgid "Shards (%{shards})"
+msgstr ""
+
msgid "Shards to synchronize"
msgstr ""
@@ -18871,6 +19298,9 @@ msgstr ""
msgid "Snowplow"
msgstr ""
+msgid "Some child epics may be hidden due to applied filters"
+msgstr ""
+
msgid "Some email servers do not support overriding the email sender name. Enable this option to include the name of the author of the issue, merge request or comment in the email body instead."
msgstr ""
@@ -19012,6 +19442,9 @@ msgstr ""
msgid "Something went wrong while updating your list settings"
msgstr ""
+msgid "Something went wrong with your automatic subscription renewal"
+msgstr ""
+
msgid "Something went wrong, unable to add %{project} to dashboard"
msgstr ""
@@ -19297,6 +19730,9 @@ msgstr ""
msgid "Stage removed"
msgstr ""
+msgid "Standard"
+msgstr ""
+
msgid "Star a label to make it a priority label. Order the prioritized labels to change their relative priority, by dragging."
msgstr ""
@@ -19420,6 +19856,9 @@ msgstr ""
msgid "Static Application Security Testing (SAST)"
msgstr ""
+msgid "StaticSiteEditor|An error occurred while submitting your changes."
+msgstr ""
+
msgid "StaticSiteEditor|Branch could not be created."
msgstr ""
@@ -19429,18 +19868,30 @@ msgstr ""
msgid "StaticSiteEditor|Could not create merge request."
msgstr ""
+msgid "StaticSiteEditor|Incompatible file content"
+msgstr ""
+
msgid "StaticSiteEditor|Return to site"
msgstr ""
+msgid "StaticSiteEditor|Static site editor"
+msgstr ""
+
msgid "StaticSiteEditor|Success!"
msgstr ""
msgid "StaticSiteEditor|Summary of changes"
msgstr ""
+msgid "StaticSiteEditor|The Static Site Editor is currently configured to only edit Markdown content on pages generated from Middleman. Visit the documentation to learn more about configuring your site to use the Static Site Editor."
+msgstr ""
+
msgid "StaticSiteEditor|Update %{sourcePath} file"
msgstr ""
+msgid "StaticSiteEditor|View documentation"
+msgstr ""
+
msgid "StaticSiteEditor|View merge request"
msgstr ""
@@ -19498,6 +19949,12 @@ msgstr ""
msgid "StatusPage|Status page"
msgstr ""
+msgid "StatusPage|Status page URL"
+msgstr ""
+
+msgid "StatusPage|Status page frontend documentation"
+msgstr ""
+
msgid "StatusPage|To publish incidents to an external status page, GitLab will store a JSON file in your Amazon S3 account in a location accessible to your external status page service. Make sure to also set up %{docsLink}"
msgstr ""
@@ -19846,6 +20303,12 @@ msgstr ""
msgid "Synced"
msgstr ""
+msgid "Synchronization disabled"
+msgstr ""
+
+msgid "Synchronization of container repositories is disabled."
+msgstr ""
+
msgid "System"
msgstr ""
@@ -19999,6 +20462,9 @@ msgstr ""
msgid "Telephone number"
msgstr ""
+msgid "Telephone number (Optional)"
+msgstr ""
+
msgid "Template"
msgstr ""
@@ -20305,6 +20771,9 @@ msgstr ""
msgid "The license was successfully uploaded and is now active. You can see the details below."
msgstr ""
+msgid "The license was successfully uploaded and will be active from %{starts_at}. You can see the details below."
+msgstr ""
+
msgid "The maximum file size allowed is %{size}."
msgstr ""
@@ -20626,6 +21095,9 @@ msgstr ""
msgid "There was an error getting the epic participants."
msgstr ""
+msgid "There was an error importing the Jira project."
+msgstr ""
+
msgid "There was an error loading users activity calendar."
msgstr ""
@@ -20641,7 +21113,7 @@ msgstr ""
msgid "There was an error resetting user pipeline minutes."
msgstr ""
-msgid "There was an error saving this Geo Node"
+msgid "There was an error saving this Geo Node."
msgstr ""
msgid "There was an error saving your changes."
@@ -20704,6 +21176,9 @@ msgstr ""
msgid "They can be managed using the %{link}."
msgstr ""
+msgid "Third Party Advisory Link"
+msgstr ""
+
msgid "Third party offers"
msgstr ""
@@ -20740,6 +21215,9 @@ msgstr ""
msgid "This application will be able to:"
msgstr ""
+msgid "This attachment has been truncated to avoid exceeding the maximum allowed attachment size of 15MB. %{written_count} of %{issues_count} issues have been included. Consider re-exporting with a narrower selection of issues."
+msgstr ""
+
msgid "This block is self-referential"
msgstr ""
@@ -20881,6 +21359,12 @@ msgstr ""
msgid "This issue is confidential"
msgstr ""
+msgid "This issue is currently blocked by the following issues: %{issues}."
+msgstr ""
+
+msgid "This issue is in a child epic of the filtered epic"
+msgstr ""
+
msgid "This issue is locked."
msgstr ""
@@ -21079,7 +21563,10 @@ msgstr ""
msgid "This user will be the author of all events in the activity feed that are the result of an update, like new branches being created or new commits being pushed to existing branches. Upon creation or when reassigning you can only assign yourself to be the mirror user."
msgstr ""
-msgid "This variable can not be masked"
+msgid "This variable can not be masked."
+msgstr ""
+
+msgid "This variable does not match the expected pattern."
msgstr ""
msgid "This will help us personalize your onboarding experience."
@@ -21109,10 +21596,10 @@ msgstr ""
msgid "ThreatMonitoring|Application firewall not detected"
msgstr ""
-msgid "ThreatMonitoring|Container Network Policy"
+msgid "ThreatMonitoring|Container Network Policies are not installed or have been disabled. To view this data, ensure your Network Policies are installed and enabled for your cluster."
msgstr ""
-msgid "ThreatMonitoring|Container NetworkPolicies are not installed or has been disabled. To view this data, ensure you NetworkPolicies are installed and enabled for your cluster."
+msgid "ThreatMonitoring|Container Network Policy"
msgstr ""
msgid "ThreatMonitoring|Container NetworkPolicies not detected"
@@ -21145,7 +21632,7 @@ msgstr ""
msgid "ThreatMonitoring|Something went wrong, unable to fetch statistics"
msgstr ""
-msgid "ThreatMonitoring|The firewall is not installed or has been disabled. To view this data, ensure you firewall is installed and enabled for your cluster."
+msgid "ThreatMonitoring|The firewall is not installed or has been disabled. To view this data, ensure the web application firewall is installed and enabled for your cluster."
msgstr ""
msgid "ThreatMonitoring|The graph below is an overview of traffic coming to your application as tracked by the Web Application Firewall (WAF). View the docs for instructions on how to access the WAF logs to see what type of malicious traffic is trying to access your app. The docs link is also accessible by clicking the \"?\" icon next to the title below."
@@ -21214,6 +21701,9 @@ msgstr ""
msgid "Time in seconds GitLab will wait for a response from the external service. When the service does not respond in time, access will be denied."
msgstr ""
+msgid "Time of import: %{importTime}"
+msgstr ""
+
msgid "Time remaining"
msgstr ""
@@ -21398,7 +21888,7 @@ msgstr ""
msgid "Title:"
msgstr ""
-msgid "Titles and Filenames"
+msgid "Titles and Descriptions"
msgstr ""
msgid "To"
@@ -21605,7 +22095,7 @@ msgstr ""
msgid "Too many projects enabled. You will need to manage them via the console or the API."
msgstr ""
-msgid "Topics"
+msgid "Topics (optional)"
msgstr ""
msgid "Total"
@@ -21767,6 +22257,9 @@ msgstr ""
msgid "Try all GitLab has to offer for 30 days."
msgstr ""
+msgid "Try changing or removing filters."
+msgstr ""
+
msgid "Try to fork again"
msgstr ""
@@ -21860,6 +22353,9 @@ msgstr ""
msgid "Unable to connect to server: %{error}"
msgstr ""
+msgid "Unable to connect to the Jira instance. Please check your Jira integration configuration."
+msgstr ""
+
msgid "Unable to convert Kubernetes logs encoding to UTF-8"
msgstr ""
@@ -22803,6 +23299,9 @@ msgstr ""
msgid "View issue"
msgstr ""
+msgid "View issues"
+msgstr ""
+
msgid "View it on GitLab"
msgstr ""
@@ -22980,6 +23479,15 @@ msgstr ""
msgid "VulnerabilityManagement|Resolved %{timeago} by %{user}"
msgstr ""
+msgid "VulnerabilityManagement|Something went wrong while trying to delete the comment. Please try again later."
+msgstr ""
+
+msgid "VulnerabilityManagement|Something went wrong while trying to retrieve the vulnerability history. Please try again later."
+msgstr ""
+
+msgid "VulnerabilityManagement|Something went wrong while trying to save the comment. Please try again later."
+msgstr ""
+
msgid "VulnerabilityManagement|Something went wrong, could not create an issue."
msgstr ""
@@ -23010,6 +23518,9 @@ msgstr ""
msgid "VulnerabilityStatusTypes|Resolved"
msgstr ""
+msgid "Vulnerability|%{scannerName} (version %{scannerVersion})"
+msgstr ""
+
msgid "Vulnerability|Class"
msgstr ""
@@ -23043,7 +23554,10 @@ msgstr ""
msgid "Vulnerability|Project"
msgstr ""
-msgid "Vulnerability|Report Type"
+msgid "Vulnerability|Scanner Provider"
+msgstr ""
+
+msgid "Vulnerability|Scanner Type"
msgstr ""
msgid "Vulnerability|Severity"
@@ -23097,9 +23611,15 @@ msgstr ""
msgid "We sent you an email with reset password instructions"
msgstr ""
+msgid "We tried to automatically renew your %{strong}%{plan_name}%{strong_close} subscription for %{strong}%{namespace_name}%{strong_close} on %{expires_on} but something went wrong so your subscription was downgraded to the free plan. Don't worry, your data is safe. We suggest you check your payment method and get in touch with our support team (%{support_link}). They'll gladly help with your subscription renewal."
+msgstr ""
+
msgid "We want to be sure it is you, please confirm you are not a robot."
msgstr ""
+msgid "We will automatically renew your %{strong}%{plan_name}%{strong_close} subscription for %{strong}%{namespace_name}%{strong_close} on %{strong}%{expires_on}%{strong_close}. There's nothing that you need to do, we'll let you know when the renewal is complete. Need more seats, a higher plan or just want to review your payment method?"
+msgstr ""
+
msgid "We've found no vulnerabilities"
msgstr ""
@@ -23204,15 +23724,6 @@ msgstr ""
msgid "When:"
msgstr ""
-msgid "While it's rare to have no vulnerabilities for your group, it can happen. In any event, we ask that you double check your settings to make sure you've set up your dashboard correctly."
-msgstr ""
-
-msgid "While it's rare to have no vulnerabilities for your pipeline, it can happen. In any event, we ask that you double check your settings to make sure all security scanning jobs have passed successfully."
-msgstr ""
-
-msgid "While it's rare to have no vulnerabilities for your project, it can happen. In any event, we ask that you double check your settings to make sure you've set up your dashboard correctly."
-msgstr ""
-
msgid "While it's rare to have no vulnerabilities, it can happen. In any event, we ask that you please double check your settings to make sure you've set up your dashboard correctly."
msgstr ""
@@ -23243,9 +23754,6 @@ msgstr ""
msgid "Wiki"
msgstr ""
-msgid "Wiki pages"
-msgstr ""
-
msgid "Wiki was successfully updated."
msgstr ""
@@ -23429,6 +23937,9 @@ msgstr ""
msgid "Yes, add it"
msgstr ""
+msgid "Yes, close issue"
+msgstr ""
+
msgid "Yes, let me map Google Code users to full names or GitLab users."
msgstr ""
@@ -23624,6 +24135,9 @@ msgstr ""
msgid "You could not create a new trigger."
msgstr ""
+msgid "You didn't renew your %{strong}%{plan_name}%{strong_close} subscription for %{strong}%{namespace_name}%{strong_close} so it was downgraded to the free plan."
+msgstr ""
+
msgid "You didn't renew your %{strong}%{plan_name}%{strong_close} subscription so it was downgraded to the GitLab Core Plan."
msgstr ""
@@ -23690,6 +24204,9 @@ msgstr ""
msgid "You have declined the invitation to join %{label}."
msgstr ""
+msgid "You have imported from this project %{numberOfPreviousImportsForProject} times before. Each new import will create duplicate issues."
+msgstr ""
+
msgid "You have no permissions"
msgstr ""
@@ -23771,6 +24288,9 @@ msgstr ""
msgid "You need to upload a Google Takeout archive."
msgstr ""
+msgid "You reached %{usage_in_percent} of %{namespace_name}'s capacity (%{used_storage} of %{storage_limit})"
+msgstr ""
+
msgid "You tried to fork %{link_to_the_project} but it failed for the following reason:"
msgstr ""
@@ -23855,9 +24375,21 @@ msgstr ""
msgid "YouTube"
msgstr ""
+msgid "Your %{strong}%{plan_name}%{strong_close} subscription for %{strong}%{namespace_name}%{strong_close} will expire on %{strong}%{expires_on}%{strong_close}. After that, you will not to be able to create issues or merge requests as well as many other features."
+msgstr ""
+
msgid "Your %{strong}%{plan_name}%{strong_close} subscription will expire on %{strong}%{expires_on}%{strong_close}. After that, you will not to be able to create issues or merge requests as well as many other features."
msgstr ""
+msgid "Your CSV export has started. It will be emailed to %{email} when complete."
+msgstr ""
+
+msgid "Your CSV export of %{issues_count} from project %{project_link} has been added to this email as an attachment."
+msgstr ""
+
+msgid "Your CSV export of %{written_count} from project %{project_name} (%{project_url}) has been added to this email as an attachment."
+msgstr ""
+
msgid "Your Commit Email will be used for web based operations, such as edits and merges."
msgstr ""
@@ -23954,6 +24486,9 @@ msgstr ""
msgid "Your comment could not be updated! Please check your network connection and try again."
msgstr ""
+msgid "Your comment will be discarded."
+msgstr ""
+
msgid "Your custom stage '%{title}' was created"
msgstr ""
@@ -23993,6 +24528,9 @@ msgstr ""
msgid "Your new personal access token has been created."
msgstr ""
+msgid "Your new project access token has been created."
+msgstr ""
+
msgid "Your password isn't required to view this page. If a password or any other personal details are requested, please contact your administrator to report abuse."
msgstr ""
@@ -24011,12 +24549,18 @@ msgstr ""
msgid "Your request for access has been queued for review."
msgstr ""
+msgid "Your search didn't match any commits."
+msgstr ""
+
msgid "Your subscription expired!"
msgstr ""
msgid "Your subscription has been downgraded"
msgstr ""
+msgid "Your subscription will automatically renew in %{remaining_days}"
+msgstr ""
+
msgid "Your subscription will expire in %{remaining_days}"
msgstr ""
@@ -24055,7 +24599,7 @@ msgstr ""
msgid "allowed to fail"
msgstr ""
-msgid "already being used for another group or project milestone."
+msgid "already being used for another group or project %{timebox_name}."
msgstr ""
msgid "already has a \"created\" issue link"
@@ -24133,6 +24677,9 @@ msgstr ""
msgid "ciReport|%{linkStartTag}Learn more about SAST %{linkEndTag}"
msgstr ""
+msgid "ciReport|%{linkStartTag}Learn more about Secret Scanning %{linkEndTag}"
+msgstr ""
+
msgid "ciReport|%{linkStartTag}Learn more about codequality reports %{linkEndTag}"
msgstr ""
@@ -24296,6 +24843,12 @@ msgstr ""
msgid "ciReport|SAST"
msgstr ""
+msgid "ciReport|Secret scanning"
+msgstr ""
+
+msgid "ciReport|Secret scanning detects secrets and credentials vulnerabilities in your source code."
+msgstr ""
+
msgid "ciReport|Security scanning"
msgstr ""
@@ -24493,6 +25046,9 @@ msgstr ""
msgid "group"
msgstr ""
+msgid "groups"
+msgstr ""
+
msgid "has already been linked to another vulnerability"
msgstr ""
@@ -24656,9 +25212,6 @@ msgstr[1] ""
msgid "merged %{time_ago}"
msgstr ""
-msgid "milestone should belong either to a project or a group."
-msgstr ""
-
msgid "missing"
msgstr ""
@@ -24995,6 +25548,9 @@ msgstr ""
msgid "no contributions"
msgstr ""
+msgid "no expiration"
+msgstr ""
+
msgid "no one can merge"
msgstr ""
@@ -25047,6 +25603,9 @@ msgstr ""
msgid "pending removal"
msgstr ""
+msgid "per day"
+msgstr ""
+
msgid "pipeline"
msgstr ""
@@ -25075,6 +25634,12 @@ msgstr[1] ""
msgid "project avatar"
msgstr ""
+msgid "projects"
+msgstr ""
+
+msgid "push to your repository, create pipelines, create issues or add comments. To reduce storage capacity, delete unused repositories, artifacts, wikis, issues, and pipelines."
+msgstr ""
+
msgid "quick actions"
msgstr ""
@@ -25221,11 +25786,6 @@ msgstr ""
msgid "this document"
msgstr ""
-msgid "thread resolved"
-msgid_plural "threads resolved"
-msgstr[0] ""
-msgstr[1] ""
-
msgid "to help your contributors communicate effectively!"
msgstr ""
@@ -25280,6 +25840,9 @@ msgstr ""
msgid "view the blob"
msgstr ""
+msgid "vulnerability|Add a comment"
+msgstr ""
+
msgid "vulnerability|Add a comment or reason for dismissal"
msgstr ""
diff --git a/locale/pl_PL/gitlab.po b/locale/pl_PL/gitlab.po
index 58fb761bfab..b63db54190f 100644
--- a/locale/pl_PL/gitlab.po
+++ b/locale/pl_PL/gitlab.po
@@ -12,7 +12,7 @@ msgstr ""
"X-Crowdin-Project: gitlab-ee\n"
"X-Crowdin-Language: pl\n"
"X-Crowdin-File: /master/locale/gitlab.pot\n"
-"PO-Revision-Date: 2020-04-15 00:26\n"
+"PO-Revision-Date: 2020-05-05 21:35\n"
msgid " %{start} to %{end}"
msgstr ""
@@ -272,6 +272,13 @@ msgstr[1] ""
msgstr[2] ""
msgstr[3] ""
+msgid "%d unresolved thread"
+msgid_plural "%d unresolved threads"
+msgstr[0] ""
+msgstr[1] ""
+msgstr[2] ""
+msgstr[3] ""
+
msgid "%d vulnerability dismissed"
msgid_plural "%d vulnerabilities dismissed"
msgstr[0] ""
@@ -443,6 +450,9 @@ msgstr ""
msgid "%{mrText}, this issue will be closed automatically."
msgstr ""
+msgid "%{namespace_name} is now read-only. You cannot: %{base_message}"
+msgstr ""
+
msgid "%{name} contained %{resultsString}"
msgstr ""
@@ -455,6 +465,13 @@ msgstr ""
msgid "%{name}'s avatar"
msgstr ""
+msgid "%{no_of_days} day"
+msgid_plural "%{no_of_days} days"
+msgstr[0] ""
+msgstr[1] ""
+msgstr[2] ""
+msgstr[3] ""
+
msgid "%{number_commits_behind} commits behind %{default_branch}, %{number_commits_ahead} commits ahead"
msgstr ""
@@ -590,6 +607,9 @@ msgstr[3] ""
msgid "%{text} is available"
msgstr ""
+msgid "%{timebox_name} should belong either to a project or a group."
+msgstr ""
+
msgid "%{title} %{operator} %{threshold}"
msgstr ""
@@ -903,12 +923,18 @@ msgstr ""
msgid "<code>\"johnsmith@example.com\": \"johnsmith@example.com\"</code> will add \"By <a href=\"#\">johnsmith@example.com</a>\" to all issues and comments originally created by johnsmith@example.com. By default, the email address or username is masked to ensure the user's privacy. Use this option if you want to show the full email address."
msgstr ""
+msgid "<namespace / project>"
+msgstr ""
+
msgid "<no name set>"
msgstr ""
msgid "<no scopes selected>"
msgstr ""
+msgid "<project name>"
+msgstr ""
+
msgid "<strong>%{group_name}</strong> group members"
msgstr ""
@@ -942,6 +968,9 @@ msgstr ""
msgid "A basic page and serverless function that uses AWS Lambda, AWS API Gateway, and GitLab Pages"
msgstr ""
+msgid "A complete DevOps platform"
+msgstr ""
+
msgid "A default branch cannot be chosen for an empty project."
msgstr ""
@@ -1158,6 +1187,12 @@ msgstr ""
msgid "AccessTokens|reset it"
msgstr ""
+msgid "AccessibilityReport|Accessibility report artifact not found"
+msgstr ""
+
+msgid "AccessibilityReport|Failed to retrieve accessibility report"
+msgstr ""
+
msgid "AccessibilityReport|Learn More"
msgstr ""
@@ -1167,6 +1202,9 @@ msgstr ""
msgid "AccessibilityReport|New"
msgstr ""
+msgid "AccessibilityReport|The accessibility scanning found an error of the following type: %{code}"
+msgstr ""
+
msgid "Account"
msgstr ""
@@ -1411,6 +1449,9 @@ msgstr ""
msgid "Added at"
msgstr ""
+msgid "Added for this merge request"
+msgstr ""
+
msgid "Added in this version"
msgstr ""
@@ -1784,6 +1825,72 @@ msgstr[1] ""
msgstr[2] ""
msgstr[3] ""
+msgid "Alert Details"
+msgstr ""
+
+msgid "AlertManagement|Acknowledged"
+msgstr ""
+
+msgid "AlertManagement|Alert"
+msgstr ""
+
+msgid "AlertManagement|Authorize external service"
+msgstr ""
+
+msgid "AlertManagement|Display alerts from all your monitoring tools directly within GitLab. Streamline the investigation of your alerts and the escalation of alerts to incidents."
+msgstr ""
+
+msgid "AlertManagement|End time"
+msgstr ""
+
+msgid "AlertManagement|End time:"
+msgstr ""
+
+msgid "AlertManagement|Events"
+msgstr ""
+
+msgid "AlertManagement|Events:"
+msgstr ""
+
+msgid "AlertManagement|Full Alert Details"
+msgstr ""
+
+msgid "AlertManagement|More information"
+msgstr ""
+
+msgid "AlertManagement|No alerts available to display. If you think you're seeing this message in error, refresh the page."
+msgstr ""
+
+msgid "AlertManagement|No alerts to display."
+msgstr ""
+
+msgid "AlertManagement|Overview"
+msgstr ""
+
+msgid "AlertManagement|Resolved"
+msgstr ""
+
+msgid "AlertManagement|Severity"
+msgstr ""
+
+msgid "AlertManagement|Start time"
+msgstr ""
+
+msgid "AlertManagement|Start time:"
+msgstr ""
+
+msgid "AlertManagement|Status"
+msgstr ""
+
+msgid "AlertManagement|Surface alerts in GitLab"
+msgstr ""
+
+msgid "AlertManagement|There was an error displaying the alerts. Confirm your endpoint's configuration details to ensure alerts appear."
+msgstr ""
+
+msgid "AlertManagement|Triggered"
+msgstr ""
+
msgid "AlertService|%{linkStart}Learn more%{linkEnd} about configuring this endpoint to receive alerts."
msgstr ""
@@ -1853,6 +1960,9 @@ msgstr ""
msgid "All security scans are enabled because %{linkStart}Auto DevOps%{linkEnd} is enabled on this project"
msgstr ""
+msgid "All threads resolved"
+msgstr ""
+
msgid "All users"
msgstr ""
@@ -1871,6 +1981,9 @@ msgstr ""
msgid "Allow only the selected protocols to be used for Git access."
msgstr ""
+msgid "Allow owners to manage default branch protection per group"
+msgstr ""
+
msgid "Allow owners to manually add users outside of LDAP"
msgstr ""
@@ -1979,6 +2092,9 @@ msgstr ""
msgid "An error occurred fetching the dropdown data."
msgstr ""
+msgid "An error occurred fetching the project authors."
+msgstr ""
+
msgid "An error occurred previewing the blob"
msgstr ""
@@ -2060,6 +2176,9 @@ msgstr ""
msgid "An error occurred while fetching sidebar data"
msgstr ""
+msgid "An error occurred while fetching terraform reports."
+msgstr ""
+
msgid "An error occurred while fetching the Service Desk address."
msgstr ""
@@ -2297,6 +2416,9 @@ msgstr ""
msgid "Any"
msgstr ""
+msgid "Any Author"
+msgstr ""
+
msgid "Any Label"
msgstr ""
@@ -2520,10 +2642,10 @@ msgstr ""
msgid "Are you sure that you want to unarchive this project?"
msgstr ""
-msgid "Are you sure you want to cancel creating this comment?"
+msgid "Are you sure you want to cancel editing this comment?"
msgstr ""
-msgid "Are you sure you want to cancel editing this comment?"
+msgid "Are you sure you want to close this blocked issue?"
msgstr ""
msgid "Are you sure you want to delete %{name}?"
@@ -2553,6 +2675,9 @@ msgstr ""
msgid "Are you sure you want to deploy this environment?"
msgstr ""
+msgid "Are you sure you want to discard this comment?"
+msgstr ""
+
msgid "Are you sure you want to erase this build?"
msgstr "Czy na pewno chcesz usunąć tę kompilację?"
@@ -2731,6 +2856,9 @@ msgstr ""
msgid "At least one approval from a code owner is required to change files matching the respective CODEOWNER rules."
msgstr ""
+msgid "At least one logging option is required to be enabled"
+msgstr ""
+
msgid "At least one of group_id or project_id must be specified"
msgstr ""
@@ -2771,6 +2899,27 @@ msgstr ""
msgid "AuditEvents|Target"
msgstr ""
+msgid "AuditLogs|(removed)"
+msgstr ""
+
+msgid "AuditLogs|Action"
+msgstr ""
+
+msgid "AuditLogs|Author"
+msgstr ""
+
+msgid "AuditLogs|Date"
+msgstr ""
+
+msgid "AuditLogs|IP Address"
+msgstr ""
+
+msgid "AuditLogs|Object"
+msgstr ""
+
+msgid "AuditLogs|Target"
+msgstr ""
+
msgid "Aug"
msgstr ""
@@ -3053,9 +3202,6 @@ msgstr ""
msgid "BambooService|You must set up automatic revision labeling and a repository trigger in Bamboo."
msgstr ""
-msgid "Batch operations"
-msgstr ""
-
msgid "BatchComments|Delete all pending comments"
msgstr ""
@@ -3185,6 +3331,9 @@ msgstr ""
msgid "Boards"
msgstr ""
+msgid "Boards and Board Lists"
+msgstr ""
+
msgid "Boards|Collapse"
msgstr ""
@@ -3404,6 +3553,9 @@ msgstr ""
msgid "Buy GitLab Enterprise Edition"
msgstr ""
+msgid "Buy more Pipeline minutes"
+msgstr ""
+
msgid "By %{user_name}"
msgstr ""
@@ -3524,6 +3676,9 @@ msgstr ""
msgid "Can't scan the code?"
msgstr ""
+msgid "Can't update snippet: %{err}"
+msgstr ""
+
msgid "Canary"
msgstr ""
@@ -3692,6 +3847,9 @@ msgstr ""
msgid "Changing group path can have unintended side effects."
msgstr ""
+msgid "Charts"
+msgstr ""
+
msgid "Charts can't be displayed as the request for data has timed out. %{documentationLink}"
msgstr ""
@@ -3728,9 +3886,6 @@ msgstr ""
msgid "ChatMessage|and [%{count} more](%{pipeline_failed_jobs_url})"
msgstr ""
-msgid "ChatMessage|failed"
-msgstr ""
-
msgid "ChatMessage|has failed"
msgstr ""
@@ -3746,9 +3901,6 @@ msgstr ""
msgid "ChatMessage|in %{project_link}"
msgstr ""
-msgid "ChatMessage|passed"
-msgstr ""
-
msgid "Check again"
msgstr ""
@@ -4199,9 +4351,6 @@ msgstr ""
msgid "Click any <strong>project name</strong> in the project list below to navigate to the project milestone."
msgstr ""
-msgid "Click here"
-msgstr ""
-
msgid "Click the <strong>Download</strong> button and wait for downloading to complete."
msgstr ""
@@ -4589,6 +4738,12 @@ msgstr "Pobieranie projektów"
msgid "ClusterIntegration|Fetching zones"
msgstr "Pobieranie stref"
+msgid "ClusterIntegration|Fluentd"
+msgstr ""
+
+msgid "ClusterIntegration|Fluentd is an open source data collector, which lets you unify the data collection and consumption for a better use and understanding of data. It requires at least one of the following logs to be successfully installed."
+msgstr ""
+
msgid "ClusterIntegration|GitLab Integration"
msgstr ""
@@ -4895,6 +5050,15 @@ msgstr ""
msgid "ClusterIntegration|Request to begin uninstalling failed"
msgstr ""
+msgid "ClusterIntegration|SIEM Hostname"
+msgstr ""
+
+msgid "ClusterIntegration|SIEM Port"
+msgstr ""
+
+msgid "ClusterIntegration|SIEM Protocol"
+msgstr ""
+
msgid "ClusterIntegration|Save changes"
msgstr ""
@@ -4991,6 +5155,12 @@ msgstr ""
msgid "ClusterIntegration|Select zone to choose machine type"
msgstr ""
+msgid "ClusterIntegration|Send Cilium Logs"
+msgstr ""
+
+msgid "ClusterIntegration|Send ModSecurity Logs"
+msgstr ""
+
msgid "ClusterIntegration|Service Token"
msgstr ""
@@ -5421,18 +5591,33 @@ msgstr ""
msgid "Compliance framework (optional)"
msgstr ""
+msgid "ComplianceFramework|GDPR"
+msgstr ""
+
msgid "ComplianceFramework|GDPR - General Data Protection Regulation"
msgstr ""
+msgid "ComplianceFramework|HIPAA"
+msgstr ""
+
msgid "ComplianceFramework|HIPAA - Health Insurance Portability and Accountability Act"
msgstr ""
+msgid "ComplianceFramework|PCI-DSS"
+msgstr ""
+
msgid "ComplianceFramework|PCI-DSS - Payment Card Industry-Data Security Standard"
msgstr ""
+msgid "ComplianceFramework|SOC 2"
+msgstr ""
+
msgid "ComplianceFramework|SOC 2 - Service Organization Control 2"
msgstr ""
+msgid "ComplianceFramework|SOX"
+msgstr ""
+
msgid "ComplianceFramework|SOX - Sarbanes-Oxley"
msgstr ""
@@ -5580,9 +5765,15 @@ msgstr ""
msgid "Container repositories sync capacity"
msgstr ""
+msgid "ContainerRegistry| Please visit the %{linkStart}administration settings%{linkEnd} to enable this feature."
+msgstr ""
+
msgid "ContainerRegistry|%{imageName} tags"
msgstr ""
+msgid "ContainerRegistry|%{title} was successfully scheduled for deletion"
+msgstr ""
+
msgid "ContainerRegistry|Automatically remove extra images that aren't designed to be kept."
msgstr ""
@@ -5595,6 +5786,9 @@ msgstr ""
msgid "ContainerRegistry|Container Registry"
msgstr ""
+msgid "ContainerRegistry|Container Registry tag expiration and retention policy is disabled"
+msgstr ""
+
msgid "ContainerRegistry|Copy build command"
msgstr ""
@@ -5604,18 +5798,12 @@ msgstr ""
msgid "ContainerRegistry|Copy push command"
msgstr ""
-msgid "ContainerRegistry|Currently, the Container Registry tag expiration feature is not available for projects created before GitLab version 12.8. For updates and more information, visit Issue %{linkStart}#196124%{linkEnd}"
-msgstr ""
-
msgid "ContainerRegistry|Docker connection error"
msgstr ""
msgid "ContainerRegistry|Docker tag expiration policy is %{toggleStatus}"
msgstr ""
-msgid "ContainerRegistry|Docker tags with names matching this regex pattern will expire:"
-msgstr ""
-
msgid "ContainerRegistry|Edit Settings"
msgstr ""
@@ -5637,9 +5825,6 @@ msgstr ""
msgid "ContainerRegistry|Image ID"
msgstr ""
-msgid "ContainerRegistry|Image deleted successfully"
-msgstr ""
-
msgid "ContainerRegistry|Keep and protect the images that matter most."
msgstr ""
@@ -5655,12 +5840,24 @@ msgstr ""
msgid "ContainerRegistry|Number of tags to retain:"
msgstr ""
+msgid "ContainerRegistry|Please contact your administrator."
+msgstr ""
+
msgid "ContainerRegistry|Push an image"
msgstr ""
msgid "ContainerRegistry|Quick Start"
msgstr ""
+msgid "ContainerRegistry|Regular expressions such as %{codeStart}.*-test%{codeEnd} or %{codeStart}dev-.*%{codeEnd} are supported"
+msgstr ""
+
+msgid "ContainerRegistry|Regular expressions such as %{codeStart}.*-test%{codeEnd} or %{codeStart}dev-.*%{codeEnd} are supported. To select all tags, use %{codeStart}.*%{codeEnd}"
+msgstr ""
+
+msgid "ContainerRegistry|Remember to run %{docLinkStart}garbage collection%{docLinkEnd} to remove the stale data from storage."
+msgstr ""
+
msgid "ContainerRegistry|Remove repository"
msgstr ""
@@ -5677,22 +5874,22 @@ msgstr[3] ""
msgid "ContainerRegistry|Retention policy has been Enabled"
msgstr ""
-msgid "ContainerRegistry|Something went wrong while deleting the image."
+msgid "ContainerRegistry|Something went wrong while fetching the expiration policy."
msgstr ""
-msgid "ContainerRegistry|Something went wrong while deleting the tag."
+msgid "ContainerRegistry|Something went wrong while fetching the repository list."
msgstr ""
-msgid "ContainerRegistry|Something went wrong while deleting the tags."
+msgid "ContainerRegistry|Something went wrong while fetching the tags list."
msgstr ""
-msgid "ContainerRegistry|Something went wrong while fetching the expiration policy."
+msgid "ContainerRegistry|Something went wrong while marking the tag for deletion."
msgstr ""
-msgid "ContainerRegistry|Something went wrong while fetching the packages list."
+msgid "ContainerRegistry|Something went wrong while marking the tags for deletion."
msgstr ""
-msgid "ContainerRegistry|Something went wrong while fetching the tags list."
+msgid "ContainerRegistry|Something went wrong while scheduling %{title} for deletion. Please try again."
msgstr ""
msgid "ContainerRegistry|Something went wrong while updating the expiration policy."
@@ -5701,16 +5898,25 @@ msgstr ""
msgid "ContainerRegistry|Tag"
msgstr ""
-msgid "ContainerRegistry|Tag deleted successfully"
-msgstr ""
-
msgid "ContainerRegistry|Tag expiration policy"
msgstr ""
msgid "ContainerRegistry|Tag expiration policy is designed to:"
msgstr ""
-msgid "ContainerRegistry|Tags deleted successfully"
+msgid "ContainerRegistry|Tag successfully marked for deletion."
+msgstr ""
+
+msgid "ContainerRegistry|Tags successfully marked for deletion."
+msgstr ""
+
+msgid "ContainerRegistry|Tags with names matching this regex pattern will %{italicStart}be preserved:%{italicEnd}"
+msgstr ""
+
+msgid "ContainerRegistry|Tags with names matching this regex pattern will %{italicStart}expire:%{italicEnd}"
+msgstr ""
+
+msgid "ContainerRegistry|The Container Registry tag expiration and retention policies for this project have not been enabled."
msgstr ""
msgid "ContainerRegistry|The last tag related to this image was recently removed. This empty image and any associated data will be automatically removed as part of the regular Garbage Collection process. If you have any questions, contact your administrator."
@@ -5731,16 +5937,16 @@ msgstr ""
msgid "ContainerRegistry|There are no container images stored for this project"
msgstr ""
-msgid "ContainerRegistry|This Registry contains deleted image tag data. Remember to run %{docLinkStart}garbage collection%{docLinkEnd} to remove the stale data from storage."
+msgid "ContainerRegistry|There was an error during the deletion of this image repository, please try again."
msgstr ""
msgid "ContainerRegistry|This image has no active tags"
msgstr ""
-msgid "ContainerRegistry|We are having trouble connecting to Docker, which could be due to an issue with your project name or path. %{docLinkStart}More Information%{docLinkEnd}"
+msgid "ContainerRegistry|This image repository is scheduled for deletion"
msgstr ""
-msgid "ContainerRegistry|Wildcards such as %{codeStart}.*-stable%{codeEnd} or %{codeStart}production/.*%{codeEnd} are supported. To select all tags, use %{codeStart}.*%{codeEnd}"
+msgid "ContainerRegistry|We are having trouble connecting to Docker, which could be due to an issue with your project name or path. %{docLinkStart}More Information%{docLinkEnd}"
msgstr ""
msgid "ContainerRegistry|With the Container Registry, every project can have its own space to store its Docker images. %{docLinkStart}More Information%{docLinkEnd}"
@@ -5995,7 +6201,7 @@ msgstr ""
msgid "Could not delete chat nickname %{chat_name}."
msgstr ""
-msgid "Could not find design"
+msgid "Could not find design."
msgstr ""
msgid "Could not remove the trigger."
@@ -6010,6 +6216,9 @@ msgstr ""
msgid "Could not revoke personal access token %{personal_access_token_name}."
msgstr ""
+msgid "Could not revoke project access token %{project_access_token_name}."
+msgstr ""
+
msgid "Could not save group ID"
msgstr ""
@@ -6067,6 +6276,9 @@ msgstr ""
msgid "Create a new branch"
msgstr ""
+msgid "Create a new deploy key for this project"
+msgstr ""
+
msgid "Create a new file as there are no files yet. Afterwards, you'll be able to commit your changes."
msgstr ""
@@ -6172,6 +6384,9 @@ msgstr ""
msgid "Create requirement"
msgstr ""
+msgid "Create snippet"
+msgstr ""
+
msgid "Create wildcard: %{searchTerm}"
msgstr ""
@@ -6193,6 +6408,9 @@ msgstr ""
msgid "Created"
msgstr ""
+msgid "Created %{timestamp}"
+msgstr ""
+
msgid "Created At"
msgstr ""
@@ -6551,6 +6769,9 @@ msgstr ""
msgid "CycleAnalytics|stage dropdown"
msgstr ""
+msgid "DAG"
+msgstr ""
+
msgid "DNS"
msgstr ""
@@ -6929,6 +7150,9 @@ msgstr ""
msgid "Deploy key was successfully updated."
msgstr ""
+msgid "Deploy keys allow read-only or read-write (if enabled) access to your repository. Deploy keys can be used for CI, staging or production servers. You can create a deploy key or add an existing one."
+msgstr ""
+
msgid "Deploy progress not found. To see pods, ensure your environment matches %{linkStart}deploy board criteria%{linkEnd}."
msgstr ""
@@ -7082,6 +7306,9 @@ msgstr ""
msgid "Deploying to"
msgstr ""
+msgid "Deployment Frequency"
+msgstr ""
+
msgid "Deployment|API"
msgstr ""
@@ -7602,9 +7829,6 @@ msgstr ""
msgid "Edit environment"
msgstr ""
-msgid "Edit epic description"
-msgstr ""
-
msgid "Edit file"
msgstr ""
@@ -8226,9 +8450,6 @@ msgstr ""
msgid "Epics"
msgstr ""
-msgid "Epics (Ultimate / Gold license only)"
-msgstr ""
-
msgid "Epics Roadmap"
msgstr ""
@@ -8238,6 +8459,9 @@ msgstr ""
msgid "Epics let you manage your portfolio of projects more efficiently and with less effort"
msgstr ""
+msgid "Epics, Issues, and Merge Requests"
+msgstr ""
+
msgid "Epics|Add an epic"
msgstr ""
@@ -8289,6 +8513,9 @@ msgstr ""
msgid "Epics|Something went wrong while fetching group epics."
msgstr ""
+msgid "Epics|Something went wrong while moving item."
+msgstr ""
+
msgid "Epics|Something went wrong while ordering item."
msgstr ""
@@ -8343,6 +8570,9 @@ msgstr ""
msgid "Error fetching network graph."
msgstr ""
+msgid "Error fetching payload data."
+msgstr ""
+
msgid "Error fetching projects"
msgstr ""
@@ -8352,9 +8582,6 @@ msgstr ""
msgid "Error fetching the dependency list. Please check your network connection and try again."
msgstr ""
-msgid "Error fetching usage ping data."
-msgstr ""
-
msgid "Error loading branch data. Please try again."
msgstr ""
@@ -8496,6 +8723,9 @@ msgstr ""
msgid "Errors"
msgstr ""
+msgid "Errors:"
+msgstr ""
+
msgid "Estimated"
msgstr ""
@@ -8700,12 +8930,18 @@ msgstr ""
msgid "Export as CSV"
msgstr ""
+msgid "Export group"
+msgstr ""
+
msgid "Export issues"
msgstr ""
msgid "Export project"
msgstr ""
+msgid "Export this group with all related data to a new GitLab instance. Once complete, you can import the data file from the \"New Group\" page."
+msgstr ""
+
msgid "Export this project with all its related data in order to move your project to a new GitLab instance. Once the export is finished, you can import the file from the \"New Project\" page."
msgstr ""
@@ -8775,9 +9011,6 @@ msgstr ""
msgid "Failed Jobs"
msgstr ""
-msgid "Failed create wiki"
-msgstr ""
-
msgid "Failed to add a Zoom meeting"
msgstr ""
@@ -8817,6 +9050,9 @@ msgstr ""
msgid "Failed to create resources"
msgstr ""
+msgid "Failed to create wiki"
+msgstr ""
+
msgid "Failed to delete board. Please try again."
msgstr ""
@@ -8826,7 +9062,7 @@ msgstr ""
msgid "Failed to enqueue the rebase operation, possibly due to a long-lived transaction. Try again later."
msgstr ""
-msgid "Failed to find import label for jira import."
+msgid "Failed to find import label for Jira import."
msgstr ""
msgid "Failed to get ref."
@@ -8850,6 +9086,12 @@ msgstr ""
msgid "Failed to load groups & users."
msgstr ""
+msgid "Failed to load labels. Please try again."
+msgstr ""
+
+msgid "Failed to load milestones. Please try again."
+msgstr ""
+
msgid "Failed to load related branches"
msgstr ""
@@ -9225,7 +9467,10 @@ msgstr ""
msgid "Filter by milestone name"
msgstr ""
-msgid "Filter by name..."
+msgid "Filter by name"
+msgstr ""
+
+msgid "Filter by status"
msgstr ""
msgid "Filter by two-factor authentication"
@@ -9237,12 +9482,18 @@ msgstr ""
msgid "Filter projects"
msgstr ""
+msgid "Filter results"
+msgstr ""
+
msgid "Filter results by group"
msgstr ""
msgid "Filter results by project"
msgstr ""
+msgid "Filter results..."
+msgstr ""
+
msgid "Filter your projects by name"
msgstr ""
@@ -9531,10 +9782,10 @@ msgstr ""
msgid "GeoNodes|Checksummed"
msgstr ""
-msgid "GeoNodes|Container repositories"
+msgid "GeoNodes|Consult Geo troubleshooting information"
msgstr ""
-msgid "GeoNodes|Data is out of date from %{timeago}"
+msgid "GeoNodes|Container repositories"
msgstr ""
msgid "GeoNodes|Data replication lag"
@@ -9576,6 +9827,9 @@ msgstr ""
msgid "GeoNodes|Last event ID seen from primary"
msgstr ""
+msgid "GeoNodes|Learn more about Geo node statuses"
+msgstr ""
+
msgid "GeoNodes|Loading nodes"
msgstr ""
@@ -9591,6 +9845,9 @@ msgstr ""
msgid "GeoNodes|Node was successfully removed."
msgstr ""
+msgid "GeoNodes|Node's status was updated %{timeAgo}."
+msgstr ""
+
msgid "GeoNodes|Not checksummed"
msgstr ""
@@ -9621,7 +9878,10 @@ msgstr ""
msgid "GeoNodes|Repository verification progress"
msgstr ""
-msgid "GeoNodes|Selective"
+msgid "GeoNodes|Selective (%{syncLabel})"
+msgstr ""
+
+msgid "GeoNodes|Selective synchronization"
msgstr ""
msgid "GeoNodes|Something went wrong while changing node status"
@@ -9648,6 +9908,9 @@ msgstr ""
msgid "GeoNodes|Unverified"
msgstr ""
+msgid "GeoNodes|Updated %{timeAgo}"
+msgstr ""
+
msgid "GeoNodes|Used slots"
msgstr ""
@@ -9687,16 +9950,16 @@ msgstr ""
msgid "Geo|All"
msgstr ""
-msgid "Geo|All projects"
+msgid "Geo|All %{replicable_type}"
msgstr ""
-msgid "Geo|All projects are being scheduled for re-sync"
+msgid "Geo|All projects"
msgstr ""
-msgid "Geo|All projects are being scheduled for re-verify"
+msgid "Geo|All projects are being scheduled for resync"
msgstr ""
-msgid "Geo|Batch operations"
+msgid "Geo|All projects are being scheduled for reverify"
msgstr ""
msgid "Geo|Could not remove tracking entry for an existing project."
@@ -9708,9 +9971,15 @@ msgstr ""
msgid "Geo|Failed"
msgstr ""
+msgid "Geo|Filter by status"
+msgstr ""
+
msgid "Geo|Geo Status"
msgstr ""
+msgid "Geo|In progress"
+msgstr ""
+
msgid "Geo|In sync"
msgstr ""
@@ -9735,9 +10004,6 @@ msgstr ""
msgid "Geo|Not synced yet"
msgstr ""
-msgid "Geo|Pending"
-msgstr ""
-
msgid "Geo|Pending synchronization"
msgstr ""
@@ -9774,7 +10040,7 @@ msgstr ""
msgid "Geo|Resync"
msgstr ""
-msgid "Geo|Resync all projects"
+msgid "Geo|Resync all"
msgstr ""
msgid "Geo|Retry count"
@@ -9783,7 +10049,7 @@ msgstr ""
msgid "Geo|Reverify"
msgstr ""
-msgid "Geo|Reverify all projects"
+msgid "Geo|Reverify all"
msgstr ""
msgid "Geo|Status"
@@ -10125,9 +10391,6 @@ msgstr ""
msgid "Go to file"
msgstr ""
-msgid "Go to file (MRs only)"
-msgstr ""
-
msgid "Go to file permalink (while viewing a file)"
msgstr ""
@@ -10572,6 +10835,9 @@ msgstr ""
msgid "GroupSettings|Disable group mentions"
msgstr ""
+msgid "GroupSettings|Export group"
+msgstr ""
+
msgid "GroupSettings|If the parent group's visibility is lower than the group current visibility, visibility levels for subgroups and projects will be changed to match the new parent group's visibility."
msgstr ""
@@ -10650,6 +10916,9 @@ msgstr ""
msgid "Groups (%{count})"
msgstr ""
+msgid "Groups (%{groups})"
+msgstr ""
+
msgid "Groups can also be nested by creating %{subgroup_docs_link_start}subgroups%{subgroup_docs_link_end}."
msgstr ""
@@ -10919,15 +11188,12 @@ msgstr ""
msgid "ID:"
msgstr ""
-msgid "IDE|Allow live previews of JavaScript projects in the Web IDE using CodeSandbox client side evaluation."
+msgid "IDE|Allow live previews of JavaScript projects in the Web IDE using CodeSandbox Live Preview."
msgstr ""
msgid "IDE|Back"
msgstr ""
-msgid "IDE|Client side evaluation"
-msgstr ""
-
msgid "IDE|Commit"
msgstr ""
@@ -10991,6 +11257,9 @@ msgstr ""
msgid "If any job surpasses this timeout threshold, it will be marked as failed. Human readable time input language is accepted like \"1 hour\". Values without specification represent seconds."
msgstr ""
+msgid "If blank, set allowable lifetime to %{instance_level_policy_in_words}, as defined by the instance admin. Once set, existing tokens for users in this group may be revoked."
+msgstr ""
+
msgid "If checked, group owners can manage LDAP group links and LDAP member overrides"
msgstr ""
@@ -11030,6 +11299,9 @@ msgstr ""
msgid "If you lose your recovery codes you can generate new ones, invalidating all previous codes."
msgstr ""
+msgid "If you reach 100%% storage capacity, you will not be able to: %{base_message}"
+msgstr ""
+
msgid "If your HTTP repository is not publicly accessible, add your credentials."
msgstr ""
@@ -11141,6 +11413,9 @@ msgstr ""
msgid "Import repository"
msgstr ""
+msgid "Import started by: %{importInitiator}"
+msgstr ""
+
msgid "Import tasks"
msgstr ""
@@ -11207,6 +11482,9 @@ msgstr ""
msgid "In order to tailor your experience with GitLab we<br>would like to know a bit more about you."
msgstr ""
+msgid "In progress"
+msgstr ""
+
msgid "In the next step, you'll be able to select the projects you want to import."
msgstr ""
@@ -11556,15 +11834,15 @@ msgstr ""
msgid "Issues"
msgstr ""
-msgid "Issues / Merge Requests"
-msgstr ""
-
msgid "Issues Analytics"
msgstr ""
msgid "Issues Rate Limits"
msgstr ""
+msgid "Issues and Merge Requests"
+msgstr ""
+
msgid "Issues can be bugs, tasks or ideas to be discussed. Also, issues are searchable and filterable."
msgstr ""
@@ -11586,10 +11864,10 @@ msgstr ""
msgid "IssuesAnalytics|Avg/Month:"
msgstr ""
-msgid "IssuesAnalytics|Issues created"
+msgid "IssuesAnalytics|Issues opened"
msgstr ""
-msgid "IssuesAnalytics|Issues created per month"
+msgid "IssuesAnalytics|Issues opened per month"
msgstr ""
msgid "IssuesAnalytics|Last 12 months"
@@ -11643,6 +11921,9 @@ msgstr ""
msgid "Jira integration not configured."
msgstr ""
+msgid "Jira project: %{importProject}"
+msgstr ""
+
msgid "JiraService|Events for %{noteable_model_name} are disabled."
msgstr ""
@@ -12034,6 +12315,9 @@ msgstr ""
msgid "Lead"
msgstr ""
+msgid "Lead Time"
+msgstr ""
+
msgid "Learn GitLab"
msgstr ""
@@ -12046,6 +12330,9 @@ msgstr ""
msgid "Learn how to %{no_packages_link_start}publish and share your packages%{no_packages_link_end} with GitLab."
msgstr ""
+msgid "Learn how to enable synchronization"
+msgstr ""
+
msgid "Learn more"
msgstr ""
@@ -12387,9 +12674,6 @@ msgstr ""
msgid "Live preview"
msgstr ""
-msgid "Load more vulnerabilities"
-msgstr ""
-
msgid "Loading"
msgstr ""
@@ -12540,9 +12824,6 @@ msgstr ""
msgid "Manage"
msgstr ""
-msgid "Manage Git repositories with fine-grained access controls that keep your code secure. Perform code reviews and enhance collaboration with merge requests. Each project can also have an issue tracker and a wiki."
-msgstr ""
-
msgid "Manage Web IDE features"
msgstr ""
@@ -12573,6 +12854,9 @@ msgstr ""
msgid "Manage your license"
msgstr ""
+msgid "Managed Account"
+msgstr ""
+
msgid "Manifest"
msgstr ""
@@ -12837,6 +13121,9 @@ msgstr ""
msgid "Merge Requests in Review"
msgstr ""
+msgid "Merge automatically (%{strategy})"
+msgstr ""
+
msgid "Merge commit message"
msgstr ""
@@ -12990,6 +13277,12 @@ msgstr ""
msgid "Merged branches are being deleted. This can take some time depending on the number of branches. Please refresh the page to see changes."
msgstr ""
+msgid "Merged this merge request."
+msgstr ""
+
+msgid "Merges this merge request immediately."
+msgstr ""
+
msgid "Merges this merge request when the pipeline succeeds."
msgstr ""
@@ -13099,12 +13392,15 @@ msgstr[1] ""
msgstr[2] ""
msgstr[3] ""
-msgid "Metrics|Environment"
+msgid "Metrics|Expand panel"
msgstr ""
msgid "Metrics|For grouping similar metrics"
msgstr ""
+msgid "Metrics|Go back (Esc)"
+msgstr ""
+
msgid "Metrics|Invalid time range, please verify."
msgstr ""
@@ -13135,9 +13431,6 @@ msgstr ""
msgid "Metrics|Refresh dashboard"
msgstr ""
-msgid "Metrics|Show last"
-msgstr ""
-
msgid "Metrics|There was an error creating the dashboard."
msgstr ""
@@ -13216,6 +13509,9 @@ msgstr ""
msgid "Microsoft Azure"
msgstr ""
+msgid "Middleman project with Static Site Editor support"
+msgstr ""
+
msgid "Migrated %{success_count}/%{total_count} files."
msgstr ""
@@ -13665,10 +13961,10 @@ msgstr ""
msgid "Next"
msgstr ""
-msgid "Next file in diff (MRs only)"
+msgid "Next file in diff"
msgstr ""
-msgid "Next unresolved discussion (MRs only)"
+msgid "Next unresolved discussion"
msgstr ""
msgid "Nickname"
@@ -13728,6 +14024,9 @@ msgstr ""
msgid "No changes between %{ref_start}%{source_branch}%{ref_end} and %{ref_start}%{target_branch}%{ref_end}"
msgstr ""
+msgid "No child epics match applied filters"
+msgstr ""
+
msgid "No connection could be made to a Gitaly Server, please check your logs!"
msgstr ""
@@ -13866,15 +14165,6 @@ msgstr ""
msgid "No thanks, don't show this again"
msgstr ""
-msgid "No vulnerabilities found for this group"
-msgstr ""
-
-msgid "No vulnerabilities found for this pipeline"
-msgstr ""
-
-msgid "No vulnerabilities found for this project"
-msgstr ""
-
msgid "No vulnerabilities present"
msgstr ""
@@ -13998,6 +14288,9 @@ msgstr ""
msgid "Nothing to preview."
msgstr ""
+msgid "Nothing to synchronize"
+msgstr ""
+
msgid "Notification events"
msgstr ""
@@ -14169,6 +14462,9 @@ msgstr ""
msgid "Once removed, the fork relationship cannot be restored and you will no longer be able to send merge requests to the source."
msgstr ""
+msgid "Once the exported file is ready you can download it from this page."
+msgstr ""
+
msgid "Once the exported file is ready, you will receive a notification email with a download link, or you can download it from this page."
msgstr ""
@@ -14272,9 +14568,6 @@ msgstr ""
msgid "Open sidebar"
msgstr ""
-msgid "Open source software to collaborate on code"
-msgstr ""
-
msgid "Open: %{openIssuesCount}"
msgstr ""
@@ -14377,6 +14670,9 @@ msgstr ""
msgid "OutdatedBrowser|You can provide feedback %{feedback_link_start}on this issue%{feedback_link_end} or via your usual support channels."
msgstr ""
+msgid "Overridden"
+msgstr ""
+
msgid "Overview"
msgstr ""
@@ -14428,6 +14724,9 @@ msgstr ""
msgid "PackageRegistry|Conan Command"
msgstr ""
+msgid "PackageRegistry|Copy .pypirc content"
+msgstr ""
+
msgid "PackageRegistry|Copy Conan Command"
msgstr ""
@@ -14449,6 +14748,9 @@ msgstr ""
msgid "PackageRegistry|Copy NuGet Setup Command"
msgstr ""
+msgid "PackageRegistry|Copy Pip command"
+msgstr ""
+
msgid "PackageRegistry|Copy and paste this inside your %{codeStart}pom.xml%{codeEnd} %{codeStart}dependencies%{codeEnd} block."
msgstr ""
@@ -14482,6 +14784,12 @@ msgstr ""
msgid "PackageRegistry|For more information on the NuGet registry, %{linkStart}see the documentation%{linkEnd}."
msgstr ""
+msgid "PackageRegistry|For more information on the PyPi registry, %{linkStart}see the documentation%{linkEnd}."
+msgstr ""
+
+msgid "PackageRegistry|If you haven't already done so, you will need to add the below to your %{codeStart}.pypirc%{codeEnd} file."
+msgstr ""
+
msgid "PackageRegistry|If you haven't already done so, you will need to add the below to your %{codeStart}pom.xml%{codeEnd} file."
msgstr ""
@@ -14491,6 +14799,9 @@ msgstr ""
msgid "PackageRegistry|Learn how to %{noPackagesLinkStart}publish and share your packages%{noPackagesLinkEnd} with GitLab."
msgstr ""
+msgid "PackageRegistry|Manually Published"
+msgstr ""
+
msgid "PackageRegistry|Maven"
msgstr ""
@@ -14509,12 +14820,18 @@ msgstr ""
msgid "PackageRegistry|NuGet Command"
msgstr ""
+msgid "PackageRegistry|Pip Command"
+msgstr ""
+
msgid "PackageRegistry|Pipeline %{linkStart}%{linkEnd} triggered %{timestamp} by %{author}"
msgstr ""
msgid "PackageRegistry|Published to the repository at %{timestamp}"
msgstr ""
+msgid "PackageRegistry|PyPi"
+msgstr ""
+
msgid "PackageRegistry|Registry Setup"
msgstr ""
@@ -14551,6 +14868,9 @@ msgstr ""
msgid "PackageRegistry|npm"
msgstr ""
+msgid "PackageRegistry|published by %{author}"
+msgstr ""
+
msgid "PackageRegistry|yarn"
msgstr ""
@@ -14566,6 +14886,9 @@ msgstr ""
msgid "PackageType|NuGet"
msgstr ""
+msgid "PackageType|PyPi"
+msgstr ""
+
msgid "Packages"
msgstr ""
@@ -14659,6 +14982,9 @@ msgstr ""
msgid "Past due"
msgstr ""
+msgid "Paste a machine public key here. Read more about how to generate it"
+msgstr ""
+
msgid "Paste a machine public key here. Read more about how to generate it %{link_start}here%{link_end}"
msgstr ""
@@ -14674,9 +15000,6 @@ msgstr ""
msgid "Path"
msgstr ""
-msgid "Path, transfer, remove"
-msgstr ""
-
msgid "Path:"
msgstr ""
@@ -14707,7 +15030,7 @@ msgstr ""
msgid "Percentage"
msgstr ""
-msgid "Perform advanced options such as changing path, transferring, or removing the group."
+msgid "Perform advanced options such as changing path, transferring, exporting, or removing the group."
msgstr ""
msgid "Perform common operations on GitLab project"
@@ -14788,9 +15111,6 @@ msgstr ""
msgid "Pipeline minutes quota"
msgstr ""
-msgid "Pipeline quota"
-msgstr ""
-
msgid "Pipeline subscriptions"
msgstr ""
@@ -14800,6 +15120,9 @@ msgstr ""
msgid "Pipeline: %{status}"
msgstr ""
+msgid "PipelineCharts|CI / CD Analytics"
+msgstr ""
+
msgid "PipelineCharts|Failed:"
msgstr ""
@@ -14878,15 +15201,6 @@ msgstr ""
msgid "Pipelines settings for '%{project_name}' were successfully updated."
msgstr ""
-msgid "Pipelines| to purchase more minutes."
-msgstr ""
-
-msgid "Pipelines|%{namespace_name} has exceeded its pipeline minutes quota."
-msgstr ""
-
-msgid "Pipelines|%{namespace_name} has less than %{notification_level}%% of CI minutes available."
-msgstr ""
-
msgid "Pipelines|API"
msgstr ""
@@ -14908,10 +15222,13 @@ msgstr ""
msgid "Pipelines|Get started with Pipelines"
msgstr ""
-msgid "Pipelines|Loading Pipelines"
+msgid "Pipelines|Group %{namespace_name} has %{percentage}%% or less Shared Runner Pipeline minutes remaining. Once it runs out, no new jobs or pipelines in its projects will run."
msgstr ""
-msgid "Pipelines|Pipelines will not run anymore on shared Runners."
+msgid "Pipelines|Group %{namespace_name} has exceeded its pipeline minutes quota. Unless you buy additional pipeline minutes, no new jobs or pipelines in its projects will run."
+msgstr ""
+
+msgid "Pipelines|Loading Pipelines"
msgstr ""
msgid "Pipelines|Project cache successfully reset."
@@ -15142,6 +15459,9 @@ msgstr ""
msgid "Please select"
msgstr ""
+msgid "Please select a Jira project"
+msgstr ""
+
msgid "Please select a country"
msgstr ""
@@ -15187,6 +15507,9 @@ msgstr ""
msgid "Please wait while we import the repository for you. Refresh at will."
msgstr ""
+msgid "Plugins directory is deprecated and will be removed in 14.0. Please move this file into /file_hooks directory."
+msgstr ""
+
msgid "Pod does not exist"
msgstr ""
@@ -15304,7 +15627,7 @@ msgstr ""
msgid "Prevent users from changing their profile name"
msgstr ""
-msgid "Prevent users from modifing merge request approvers list"
+msgid "Prevent users from modifying merge request approvers list"
msgstr ""
msgid "Prevent users from performing write operations on GitLab while performing maintenance."
@@ -15325,10 +15648,10 @@ msgstr ""
msgid "Previous Artifacts"
msgstr ""
-msgid "Previous file in diff (MRs only)"
+msgid "Previous file in diff"
msgstr ""
-msgid "Previous unresolved discussion (MRs only)"
+msgid "Previous unresolved discussion"
msgstr ""
msgid "Primary"
@@ -15361,6 +15684,9 @@ msgstr ""
msgid "Private profile"
msgstr ""
+msgid "Private projects Minutes cost factor"
+msgstr ""
+
msgid "Private projects can be created in your personal namespace with:"
msgstr ""
@@ -16189,6 +16515,9 @@ msgstr ""
msgid "ProjectSettings|With GitLab Pages you can host your static websites on GitLab"
msgstr ""
+msgid "ProjectSettings|With Metrics Dashboard you can visualize this project performance metrics"
+msgstr ""
+
msgid "ProjectTemplates|.NET Core"
msgstr ""
@@ -16252,6 +16581,9 @@ msgstr ""
msgid "ProjectTemplates|Spring"
msgstr ""
+msgid "ProjectTemplates|Static Site Editor/Middleman"
+msgstr ""
+
msgid "ProjectTemplates|iOS (Swift)"
msgstr ""
@@ -16471,13 +16803,13 @@ msgstr ""
msgid "PrometheusService|Prometheus is being automatically managed on your clusters"
msgstr ""
-msgid "PrometheusService|These metrics will only be monitored after your first deployment to an environment"
+msgid "PrometheusService|Select the Active checkbox to override the Auto Configuration with custom settings. If unchecked, Auto Configuration settings are used."
msgstr ""
-msgid "PrometheusService|Time-series monitoring service"
+msgid "PrometheusService|These metrics will only be monitored after your first deployment to an environment"
msgstr ""
-msgid "PrometheusService|To enable manual configuration, uninstall Prometheus from your clusters"
+msgid "PrometheusService|Time-series monitoring service"
msgstr ""
msgid "PrometheusService|To enable the installation of Prometheus on your clusters, deactivate the manual configuration below"
@@ -16663,6 +16995,9 @@ msgstr ""
msgid "Protip:"
msgstr ""
+msgid "Protocol"
+msgstr ""
+
msgid "Provider"
msgstr ""
@@ -16687,6 +17022,9 @@ msgstr ""
msgid "Public pipelines"
msgstr ""
+msgid "Public projects Minutes cost factor"
+msgstr ""
+
msgid "Pull"
msgstr ""
@@ -16889,6 +17227,9 @@ msgstr[1] ""
msgstr[2] ""
msgstr[3] ""
+msgid "Regenerate export"
+msgstr ""
+
msgid "Regenerate instance ID"
msgstr ""
@@ -17268,9 +17609,31 @@ msgstr ""
msgid "Reports|%{combinedString} and %{resolvedString}"
msgstr ""
+msgid "Reports|Accessibility scanning detected %d issue for the source branch only"
+msgid_plural "Reports|Accessibility scanning detected %d issues for the source branch only"
+msgstr[0] ""
+msgstr[1] ""
+msgstr[2] ""
+msgstr[3] ""
+
+msgid "Reports|Accessibility scanning detected no issues for the source branch only"
+msgstr ""
+
+msgid "Reports|Accessibility scanning failed loading results"
+msgstr ""
+
+msgid "Reports|Accessibility scanning results are being parsed"
+msgstr ""
+
msgid "Reports|Actions"
msgstr ""
+msgid "Reports|An error occured while loading report"
+msgstr ""
+
+msgid "Reports|An error occurred while loading %{name} results"
+msgstr ""
+
msgid "Reports|Class"
msgstr ""
@@ -17376,7 +17739,7 @@ msgstr ""
msgid "Requested %{time_ago}"
msgstr ""
-msgid "Requested design version does not exist"
+msgid "Requested design version does not exist."
msgstr ""
msgid "Requested states are invalid"
@@ -17564,6 +17927,9 @@ msgstr ""
msgid "Resync"
msgstr ""
+msgid "Resync all"
+msgstr ""
+
msgid "Resync all %{replicableType}"
msgstr ""
@@ -17634,6 +18000,9 @@ msgstr ""
msgid "Revoked personal access token %{personal_access_token_name}!"
msgstr ""
+msgid "Revoked project access token %{project_access_token_name}!"
+msgstr ""
+
msgid "RightSidebar|adding a"
msgstr ""
@@ -17682,6 +18051,9 @@ msgstr "Runner uruchamia zadania z przypisanych projektów"
msgid "Runner token"
msgstr "Token robotnika"
+msgid "Runner tokens"
+msgstr ""
+
msgid "Runner was not updated."
msgstr ""
@@ -17736,6 +18108,9 @@ msgstr ""
msgid "SAML SSO for %{group_name}"
msgstr ""
+msgid "SAML discovery tokens"
+msgstr ""
+
msgid "SAML for %{group_name}"
msgstr ""
@@ -17820,12 +18195,18 @@ msgstr ""
msgid "Scheduled"
msgstr ""
+msgid "Scheduled to merge this merge request (%{strategy})."
+msgstr ""
+
msgid "Scheduled to merge this merge request when the pipeline succeeds."
msgstr ""
msgid "Schedules"
msgstr ""
+msgid "Schedules to merge this merge request (%{strategy})."
+msgstr ""
+
msgid "Scheduling"
msgstr ""
@@ -17883,6 +18264,9 @@ msgstr ""
msgid "Search branches and tags"
msgstr ""
+msgid "Search by author"
+msgstr ""
+
msgid "Search files"
msgstr ""
@@ -18044,13 +18428,6 @@ msgstr[1] ""
msgstr[2] ""
msgstr[3] ""
-msgid "SearchResults|snippet result"
-msgid_plural "SearchResults|snippet results"
-msgstr[0] ""
-msgstr[1] ""
-msgstr[2] ""
-msgstr[3] ""
-
msgid "SearchResults|user"
msgid_plural "SearchResults|users"
msgstr[0] ""
@@ -18065,6 +18442,9 @@ msgstr[1] ""
msgstr[2] ""
msgstr[3] ""
+msgid "Searching by both author and message is currently not supported."
+msgstr ""
+
msgid "Seat Link"
msgstr ""
@@ -18092,184 +18472,232 @@ msgstr ""
msgid "Security Dashboard"
msgstr ""
-msgid "Security Dashboard|Error fetching the vulnerability counts. Please check your network connection and try again."
+msgid "Security configuration help link"
msgstr ""
-msgid "Security Dashboard|Error fetching the vulnerability list. Please check your network connection and try again."
+msgid "Security dashboard"
msgstr ""
-msgid "Security Dashboard|Issue Created"
+msgid "Security report is out of date. Please update your branch with the latest changes from the target branch (%{targetBranchName})"
msgstr ""
-msgid "Security Reports|Comment added to '%{vulnerabilityName}'"
+msgid "Security report is out of date. Run %{newPipelineLinkStart}a new pipeline%{newPipelineLinkEnd} for the target branch (%{targetBranchName})"
msgstr ""
-msgid "Security Reports|Comment deleted on '%{vulnerabilityName}'"
+msgid "SecurityConfiguration|Configured"
msgstr ""
-msgid "Security Reports|Comment edited on '%{vulnerabilityName}'"
+msgid "SecurityConfiguration|Feature"
msgstr ""
-msgid "Security Reports|Create issue"
+msgid "SecurityConfiguration|Feature documentation for %{featureName}"
msgstr ""
-msgid "Security Reports|Dismiss vulnerability"
+msgid "SecurityConfiguration|Not yet configured"
msgstr ""
-msgid "Security Reports|Dismissed '%{vulnerabilityName}'"
+msgid "SecurityConfiguration|Secure features"
msgstr ""
-msgid "Security Reports|Dismissed '%{vulnerabilityName}'. Turn off the hide dismissed toggle to view."
+msgid "SecurityConfiguration|Status"
msgstr ""
-msgid "Security Reports|Either you don't have permission to view this dashboard or the dashboard has not been setup. Please check your permission settings with your administrator or check your dashboard configurations to proceed."
+msgid "SecurityReports|%{firstProject} and %{secondProject}"
msgstr ""
-msgid "Security Reports|Learn more about setting up your dashboard"
+msgid "SecurityReports|%{firstProject}, %{secondProject}, and %{rest}"
msgstr ""
-msgid "Security Reports|More info"
+msgid "SecurityReports|Add a project to your dashboard"
msgstr ""
-msgid "Security Reports|Oops, something doesn't seem right."
+msgid "SecurityReports|Add or remove projects from your dashboard"
msgstr ""
-msgid "Security Reports|Security reports can only be accessed by authorized users."
+msgid "SecurityReports|Add projects"
msgstr ""
-msgid "Security Reports|There was an error adding the comment."
+msgid "SecurityReports|Comment added to '%{vulnerabilityName}'"
msgstr ""
-msgid "Security Reports|There was an error creating the issue."
+msgid "SecurityReports|Comment deleted on '%{vulnerabilityName}'"
msgstr ""
-msgid "Security Reports|There was an error creating the merge request."
+msgid "SecurityReports|Comment edited on '%{vulnerabilityName}'"
msgstr ""
-msgid "Security Reports|There was an error deleting the comment."
+msgid "SecurityReports|Create issue"
msgstr ""
-msgid "Security Reports|There was an error dismissing the vulnerabilities."
+msgid "SecurityReports|Dismiss Selected"
msgstr ""
-msgid "Security Reports|There was an error dismissing the vulnerability."
+msgid "SecurityReports|Dismiss vulnerability"
msgstr ""
-msgid "Security Reports|There was an error reverting the dismissal."
+msgid "SecurityReports|Dismissed '%{vulnerabilityName}'"
msgstr ""
-msgid "Security Reports|There was an error reverting this dismissal."
+msgid "SecurityReports|Dismissed '%{vulnerabilityName}'. Turn off the hide dismissed toggle to view."
msgstr ""
-msgid "Security Reports|Undo dismiss"
+msgid "SecurityReports|Each vulnerability now has a unique page that can be directly linked to, shared, referenced, and tracked as the single source of truth. Vulnerability occurrences also persist across scanner runs, which improves tracking and visibility and reduces duplicates between scans."
msgstr ""
-msgid "Security Reports|You do not have sufficient permissions to access this report"
+msgid "SecurityReports|Edit dashboard"
msgstr ""
-msgid "Security Reports|You must sign in as an authorized user to see this report"
+msgid "SecurityReports|Either you don't have permission to view this dashboard or the dashboard has not been setup. Please check your permission settings with your administrator or check your dashboard configurations to proceed."
msgstr ""
-msgid "Security configuration help link"
+msgid "SecurityReports|Error fetching the vulnerability counts. Please check your network connection and try again."
msgstr ""
-msgid "Security dashboard"
+msgid "SecurityReports|Error fetching the vulnerability list. Please check your network connection and try again."
msgstr ""
-msgid "Security report is out of date. Please update your branch with the latest changes from the target branch (%{targetBranchName})"
+msgid "SecurityReports|False positive"
msgstr ""
-msgid "Security report is out of date. Run %{newPipelineLinkStart}a new pipeline%{newPipelineLinkEnd} for the target branch (%{targetBranchName})"
+msgid "SecurityReports|Hide dismissed"
msgstr ""
-msgid "SecurityConfiguration|Configured"
+msgid "SecurityReports|Introducing standalone vulnerabilities"
msgstr ""
-msgid "SecurityConfiguration|Feature"
+msgid "SecurityReports|Issue Created"
msgstr ""
-msgid "SecurityConfiguration|Feature documentation for %{featureName}"
+msgid "SecurityReports|Learn More"
msgstr ""
-msgid "SecurityConfiguration|Not yet configured"
+msgid "SecurityReports|Learn more about setting up your dashboard"
msgstr ""
-msgid "SecurityConfiguration|Secure features"
+msgid "SecurityReports|Load more vulnerabilities"
msgstr ""
-msgid "SecurityConfiguration|Status"
+msgid "SecurityReports|Monitor vulnerabilities in your code"
+msgstr ""
+
+msgid "SecurityReports|More info"
+msgstr ""
+
+msgid "SecurityReports|More information"
+msgstr ""
+
+msgid "SecurityReports|No vulnerabilities found for dashboard"
+msgstr ""
+
+msgid "SecurityReports|No vulnerabilities found for this group"
+msgstr ""
+
+msgid "SecurityReports|No vulnerabilities found for this pipeline"
+msgstr ""
+
+msgid "SecurityReports|No vulnerabilities found for this project"
+msgstr ""
+
+msgid "SecurityReports|Oops, something doesn't seem right."
+msgstr ""
+
+msgid "SecurityReports|Pipeline %{pipelineLink} triggered %{timeago} by %{user}"
msgstr ""
-msgid "SecurityDashboard|%{firstProject} and %{secondProject}"
+msgid "SecurityReports|Project"
msgstr ""
-msgid "SecurityDashboard|%{firstProject}, %{secondProject}, and %{rest}"
+msgid "SecurityReports|Projects added"
msgstr ""
-msgid "SecurityDashboard|Add a project to your dashboard"
+msgid "SecurityReports|Remove project from dashboard"
msgstr ""
-msgid "SecurityDashboard|Add or remove projects from your dashboard"
+msgid "SecurityReports|Report type"
msgstr ""
-msgid "SecurityDashboard|Add projects"
+msgid "SecurityReports|Return to dashboard"
msgstr ""
-msgid "SecurityDashboard|Edit dashboard"
+msgid "SecurityReports|Security Dashboard"
msgstr ""
-msgid "SecurityDashboard|Hide dismissed"
+msgid "SecurityReports|Security reports can only be accessed by authorized users."
msgstr ""
-msgid "SecurityDashboard|Monitor vulnerabilities in your code"
+msgid "SecurityReports|Select a project to add by using the project search field above."
msgstr ""
-msgid "SecurityDashboard|More information"
+msgid "SecurityReports|Select a reason"
msgstr ""
-msgid "SecurityDashboard|No vulnerabilities found for dashboard"
+msgid "SecurityReports|Severity"
msgstr ""
-msgid "SecurityDashboard|Pipeline %{pipelineLink} triggered %{timeago} by %{user}"
+msgid "SecurityReports|Status"
msgstr ""
-msgid "SecurityDashboard|Project"
+msgid "SecurityReports|The security dashboard displays the latest security findings for projects you wish to monitor. Select \"Edit dashboard\" to add and remove projects."
msgstr ""
-msgid "SecurityDashboard|Projects added"
+msgid "SecurityReports|The security dashboard displays the latest security report. Use it to find and fix vulnerabilities."
msgstr ""
-msgid "SecurityDashboard|Remove project from dashboard"
+msgid "SecurityReports|There was an error adding the comment."
msgstr ""
-msgid "SecurityDashboard|Report type"
+msgid "SecurityReports|There was an error creating the issue."
msgstr ""
-msgid "SecurityDashboard|Return to dashboard"
+msgid "SecurityReports|There was an error creating the merge request."
msgstr ""
-msgid "SecurityDashboard|Security Dashboard"
+msgid "SecurityReports|There was an error deleting the comment."
msgstr ""
-msgid "SecurityDashboard|Select a project to add by using the project search field above."
+msgid "SecurityReports|There was an error dismissing the vulnerabilities."
msgstr ""
-msgid "SecurityDashboard|Severity"
+msgid "SecurityReports|There was an error dismissing the vulnerability."
msgstr ""
-msgid "SecurityDashboard|Status"
+msgid "SecurityReports|There was an error reverting the dismissal."
msgstr ""
-msgid "SecurityDashboard|The security dashboard displays the latest security findings for projects you wish to monitor. Select \"Edit dashboard\" to add and remove projects."
+msgid "SecurityReports|There was an error reverting this dismissal."
msgstr ""
-msgid "SecurityDashboard|The security dashboard displays the latest security report. Use it to find and fix vulnerabilities."
+msgid "SecurityReports|There was an error while generating the report."
msgstr ""
-msgid "SecurityDashboard|There was an error while generating the report."
+msgid "SecurityReports|Unable to add %{invalidProjects}"
msgstr ""
-msgid "SecurityDashboard|Unable to add %{invalidProjects}"
+msgid "SecurityReports|Undo dismiss"
+msgstr ""
+
+msgid "SecurityReports|While it's rare to have no vulnerabilities for your group, it can happen. In any event, we ask that you double check your settings to make sure you've set up your dashboard correctly."
+msgstr ""
+
+msgid "SecurityReports|While it's rare to have no vulnerabilities for your pipeline, it can happen. In any event, we ask that you double check your settings to make sure all security scanning jobs have passed successfully."
+msgstr ""
+
+msgid "SecurityReports|While it's rare to have no vulnerabilities for your project, it can happen. In any event, we ask that you double check your settings to make sure you've set up your dashboard correctly."
+msgstr ""
+
+msgid "SecurityReports|While it's rare to have no vulnerabilities, it can happen. In any event, we ask that you please double check your settings to make sure you've set up your dashboard correctly."
+msgstr ""
+
+msgid "SecurityReports|Won't fix / Accept risk"
+msgstr ""
+
+msgid "SecurityReports|You do not have sufficient permissions to access this report"
+msgstr ""
+
+msgid "SecurityReports|You must sign in as an authorized user to see this report"
+msgstr ""
+
+msgid "SecurityReports|[No reason]"
msgstr ""
msgid "See GitLab's %{password_policy_guidelines}"
@@ -18281,6 +18709,9 @@ msgstr ""
msgid "See the affected projects in the GitLab admin panel"
msgstr ""
+msgid "See what's new at GitLab"
+msgstr ""
+
msgid "Select"
msgstr ""
@@ -18686,9 +19117,6 @@ msgstr ""
msgid "Set up Jira Integration"
msgstr ""
-msgid "Set up Jira Integration illustration"
-msgstr ""
-
msgid "Set up a %{type} Runner automatically"
msgstr ""
@@ -18773,6 +19201,9 @@ msgstr ""
msgid "Severity: %{severity}"
msgstr ""
+msgid "Shards (%{shards})"
+msgstr ""
+
msgid "Shards to synchronize"
msgstr ""
@@ -19089,6 +19520,9 @@ msgstr ""
msgid "Snowplow"
msgstr ""
+msgid "Some child epics may be hidden due to applied filters"
+msgstr ""
+
msgid "Some email servers do not support overriding the email sender name. Enable this option to include the name of the author of the issue, merge request or comment in the email body instead."
msgstr ""
@@ -19230,6 +19664,9 @@ msgstr ""
msgid "Something went wrong while updating your list settings"
msgstr ""
+msgid "Something went wrong with your automatic subscription renewal"
+msgstr ""
+
msgid "Something went wrong, unable to add %{project} to dashboard"
msgstr "Coś poszło nie tak, nie można dodać %{project} do pulpitu nawigacyjnego"
@@ -19515,6 +19952,9 @@ msgstr ""
msgid "Stage removed"
msgstr ""
+msgid "Standard"
+msgstr ""
+
msgid "Star a label to make it a priority label. Order the prioritized labels to change their relative priority, by dragging."
msgstr "Oznacz etykietę gwiazdką, by nadać jej status priorytetowy. Zorganizuj etykiety priorytetowe celem zmiany ich względnej ważności, poprzez przeciągnięcie."
@@ -19638,6 +20078,9 @@ msgstr "Określ swoją wiadomość, aby aktywować"
msgid "Static Application Security Testing (SAST)"
msgstr ""
+msgid "StaticSiteEditor|An error occurred while submitting your changes."
+msgstr ""
+
msgid "StaticSiteEditor|Branch could not be created."
msgstr ""
@@ -19647,18 +20090,30 @@ msgstr ""
msgid "StaticSiteEditor|Could not create merge request."
msgstr ""
+msgid "StaticSiteEditor|Incompatible file content"
+msgstr ""
+
msgid "StaticSiteEditor|Return to site"
msgstr ""
+msgid "StaticSiteEditor|Static site editor"
+msgstr ""
+
msgid "StaticSiteEditor|Success!"
msgstr ""
msgid "StaticSiteEditor|Summary of changes"
msgstr ""
+msgid "StaticSiteEditor|The Static Site Editor is currently configured to only edit Markdown content on pages generated from Middleman. Visit the documentation to learn more about configuring your site to use the Static Site Editor."
+msgstr ""
+
msgid "StaticSiteEditor|Update %{sourcePath} file"
msgstr ""
+msgid "StaticSiteEditor|View documentation"
+msgstr ""
+
msgid "StaticSiteEditor|View merge request"
msgstr ""
@@ -19716,6 +20171,12 @@ msgstr ""
msgid "StatusPage|Status page"
msgstr ""
+msgid "StatusPage|Status page URL"
+msgstr ""
+
+msgid "StatusPage|Status page frontend documentation"
+msgstr ""
+
msgid "StatusPage|To publish incidents to an external status page, GitLab will store a JSON file in your Amazon S3 account in a location accessible to your external status page service. Make sure to also set up %{docsLink}"
msgstr ""
@@ -20064,6 +20525,12 @@ msgstr "Synchronizuj informacje"
msgid "Synced"
msgstr ""
+msgid "Synchronization disabled"
+msgstr ""
+
+msgid "Synchronization of container repositories is disabled."
+msgstr ""
+
msgid "System"
msgstr ""
@@ -20217,6 +20684,9 @@ msgstr ""
msgid "Telephone number"
msgstr ""
+msgid "Telephone number (Optional)"
+msgstr ""
+
msgid "Template"
msgstr ""
@@ -20529,6 +20999,9 @@ msgstr ""
msgid "The license was successfully uploaded and is now active. You can see the details below."
msgstr ""
+msgid "The license was successfully uploaded and will be active from %{starts_at}. You can see the details below."
+msgstr ""
+
msgid "The maximum file size allowed is %{size}."
msgstr ""
@@ -20850,6 +21323,9 @@ msgstr ""
msgid "There was an error getting the epic participants."
msgstr ""
+msgid "There was an error importing the Jira project."
+msgstr ""
+
msgid "There was an error loading users activity calendar."
msgstr ""
@@ -20865,7 +21341,7 @@ msgstr ""
msgid "There was an error resetting user pipeline minutes."
msgstr ""
-msgid "There was an error saving this Geo Node"
+msgid "There was an error saving this Geo Node."
msgstr ""
msgid "There was an error saving your changes."
@@ -20928,6 +21404,9 @@ msgstr ""
msgid "They can be managed using the %{link}."
msgstr ""
+msgid "Third Party Advisory Link"
+msgstr ""
+
msgid "Third party offers"
msgstr ""
@@ -20964,6 +21443,9 @@ msgstr ""
msgid "This application will be able to:"
msgstr ""
+msgid "This attachment has been truncated to avoid exceeding the maximum allowed attachment size of 15MB. %{written_count} of %{issues_count} issues have been included. Consider re-exporting with a narrower selection of issues."
+msgstr ""
+
msgid "This block is self-referential"
msgstr ""
@@ -21105,6 +21587,12 @@ msgstr ""
msgid "This issue is confidential"
msgstr ""
+msgid "This issue is currently blocked by the following issues: %{issues}."
+msgstr ""
+
+msgid "This issue is in a child epic of the filtered epic"
+msgstr ""
+
msgid "This issue is locked."
msgstr ""
@@ -21303,7 +21791,10 @@ msgstr ""
msgid "This user will be the author of all events in the activity feed that are the result of an update, like new branches being created or new commits being pushed to existing branches. Upon creation or when reassigning you can only assign yourself to be the mirror user."
msgstr ""
-msgid "This variable can not be masked"
+msgid "This variable can not be masked."
+msgstr ""
+
+msgid "This variable does not match the expected pattern."
msgstr ""
msgid "This will help us personalize your onboarding experience."
@@ -21333,10 +21824,10 @@ msgstr ""
msgid "ThreatMonitoring|Application firewall not detected"
msgstr ""
-msgid "ThreatMonitoring|Container Network Policy"
+msgid "ThreatMonitoring|Container Network Policies are not installed or have been disabled. To view this data, ensure your Network Policies are installed and enabled for your cluster."
msgstr ""
-msgid "ThreatMonitoring|Container NetworkPolicies are not installed or has been disabled. To view this data, ensure you NetworkPolicies are installed and enabled for your cluster."
+msgid "ThreatMonitoring|Container Network Policy"
msgstr ""
msgid "ThreatMonitoring|Container NetworkPolicies not detected"
@@ -21369,7 +21860,7 @@ msgstr ""
msgid "ThreatMonitoring|Something went wrong, unable to fetch statistics"
msgstr ""
-msgid "ThreatMonitoring|The firewall is not installed or has been disabled. To view this data, ensure you firewall is installed and enabled for your cluster."
+msgid "ThreatMonitoring|The firewall is not installed or has been disabled. To view this data, ensure the web application firewall is installed and enabled for your cluster."
msgstr ""
msgid "ThreatMonitoring|The graph below is an overview of traffic coming to your application as tracked by the Web Application Firewall (WAF). View the docs for instructions on how to access the WAF logs to see what type of malicious traffic is trying to access your app. The docs link is also accessible by clicking the \"?\" icon next to the title below."
@@ -21438,6 +21929,9 @@ msgstr ""
msgid "Time in seconds GitLab will wait for a response from the external service. When the service does not respond in time, access will be denied."
msgstr ""
+msgid "Time of import: %{importTime}"
+msgstr ""
+
msgid "Time remaining"
msgstr ""
@@ -21626,7 +22120,7 @@ msgstr ""
msgid "Title:"
msgstr ""
-msgid "Titles and Filenames"
+msgid "Titles and Descriptions"
msgstr ""
msgid "To"
@@ -21833,7 +22327,7 @@ msgstr ""
msgid "Too many projects enabled. You will need to manage them via the console or the API."
msgstr ""
-msgid "Topics"
+msgid "Topics (optional)"
msgstr ""
msgid "Total"
@@ -21995,6 +22489,9 @@ msgstr ""
msgid "Try all GitLab has to offer for 30 days."
msgstr ""
+msgid "Try changing or removing filters."
+msgstr ""
+
msgid "Try to fork again"
msgstr ""
@@ -22088,6 +22585,9 @@ msgstr ""
msgid "Unable to connect to server: %{error}"
msgstr ""
+msgid "Unable to connect to the Jira instance. Please check your Jira integration configuration."
+msgstr ""
+
msgid "Unable to convert Kubernetes logs encoding to UTF-8"
msgstr ""
@@ -23035,6 +23535,9 @@ msgstr ""
msgid "View issue"
msgstr ""
+msgid "View issues"
+msgstr ""
+
msgid "View it on GitLab"
msgstr ""
@@ -23212,6 +23715,15 @@ msgstr ""
msgid "VulnerabilityManagement|Resolved %{timeago} by %{user}"
msgstr ""
+msgid "VulnerabilityManagement|Something went wrong while trying to delete the comment. Please try again later."
+msgstr ""
+
+msgid "VulnerabilityManagement|Something went wrong while trying to retrieve the vulnerability history. Please try again later."
+msgstr ""
+
+msgid "VulnerabilityManagement|Something went wrong while trying to save the comment. Please try again later."
+msgstr ""
+
msgid "VulnerabilityManagement|Something went wrong, could not create an issue."
msgstr ""
@@ -23242,6 +23754,9 @@ msgstr ""
msgid "VulnerabilityStatusTypes|Resolved"
msgstr ""
+msgid "Vulnerability|%{scannerName} (version %{scannerVersion})"
+msgstr ""
+
msgid "Vulnerability|Class"
msgstr ""
@@ -23275,7 +23790,10 @@ msgstr ""
msgid "Vulnerability|Project"
msgstr ""
-msgid "Vulnerability|Report Type"
+msgid "Vulnerability|Scanner Provider"
+msgstr ""
+
+msgid "Vulnerability|Scanner Type"
msgstr ""
msgid "Vulnerability|Severity"
@@ -23329,9 +23847,15 @@ msgstr ""
msgid "We sent you an email with reset password instructions"
msgstr ""
+msgid "We tried to automatically renew your %{strong}%{plan_name}%{strong_close} subscription for %{strong}%{namespace_name}%{strong_close} on %{expires_on} but something went wrong so your subscription was downgraded to the free plan. Don't worry, your data is safe. We suggest you check your payment method and get in touch with our support team (%{support_link}). They'll gladly help with your subscription renewal."
+msgstr ""
+
msgid "We want to be sure it is you, please confirm you are not a robot."
msgstr ""
+msgid "We will automatically renew your %{strong}%{plan_name}%{strong_close} subscription for %{strong}%{namespace_name}%{strong_close} on %{strong}%{expires_on}%{strong_close}. There's nothing that you need to do, we'll let you know when the renewal is complete. Need more seats, a higher plan or just want to review your payment method?"
+msgstr ""
+
msgid "We've found no vulnerabilities"
msgstr ""
@@ -23438,15 +23962,6 @@ msgstr ""
msgid "When:"
msgstr ""
-msgid "While it's rare to have no vulnerabilities for your group, it can happen. In any event, we ask that you double check your settings to make sure you've set up your dashboard correctly."
-msgstr ""
-
-msgid "While it's rare to have no vulnerabilities for your pipeline, it can happen. In any event, we ask that you double check your settings to make sure all security scanning jobs have passed successfully."
-msgstr ""
-
-msgid "While it's rare to have no vulnerabilities for your project, it can happen. In any event, we ask that you double check your settings to make sure you've set up your dashboard correctly."
-msgstr ""
-
msgid "While it's rare to have no vulnerabilities, it can happen. In any event, we ask that you please double check your settings to make sure you've set up your dashboard correctly."
msgstr ""
@@ -23477,9 +23992,6 @@ msgstr ""
msgid "Wiki"
msgstr ""
-msgid "Wiki pages"
-msgstr ""
-
msgid "Wiki was successfully updated."
msgstr ""
@@ -23663,6 +24175,9 @@ msgstr ""
msgid "Yes, add it"
msgstr ""
+msgid "Yes, close issue"
+msgstr ""
+
msgid "Yes, let me map Google Code users to full names or GitLab users."
msgstr ""
@@ -23858,6 +24373,9 @@ msgstr ""
msgid "You could not create a new trigger."
msgstr ""
+msgid "You didn't renew your %{strong}%{plan_name}%{strong_close} subscription for %{strong}%{namespace_name}%{strong_close} so it was downgraded to the free plan."
+msgstr ""
+
msgid "You didn't renew your %{strong}%{plan_name}%{strong_close} subscription so it was downgraded to the GitLab Core Plan."
msgstr ""
@@ -23924,6 +24442,9 @@ msgstr ""
msgid "You have declined the invitation to join %{label}."
msgstr ""
+msgid "You have imported from this project %{numberOfPreviousImportsForProject} times before. Each new import will create duplicate issues."
+msgstr ""
+
msgid "You have no permissions"
msgstr ""
@@ -24005,6 +24526,9 @@ msgstr ""
msgid "You need to upload a Google Takeout archive."
msgstr ""
+msgid "You reached %{usage_in_percent} of %{namespace_name}'s capacity (%{used_storage} of %{storage_limit})"
+msgstr ""
+
msgid "You tried to fork %{link_to_the_project} but it failed for the following reason:"
msgstr ""
@@ -24089,9 +24613,21 @@ msgstr ""
msgid "YouTube"
msgstr ""
+msgid "Your %{strong}%{plan_name}%{strong_close} subscription for %{strong}%{namespace_name}%{strong_close} will expire on %{strong}%{expires_on}%{strong_close}. After that, you will not to be able to create issues or merge requests as well as many other features."
+msgstr ""
+
msgid "Your %{strong}%{plan_name}%{strong_close} subscription will expire on %{strong}%{expires_on}%{strong_close}. After that, you will not to be able to create issues or merge requests as well as many other features."
msgstr ""
+msgid "Your CSV export has started. It will be emailed to %{email} when complete."
+msgstr ""
+
+msgid "Your CSV export of %{issues_count} from project %{project_link} has been added to this email as an attachment."
+msgstr ""
+
+msgid "Your CSV export of %{written_count} from project %{project_name} (%{project_url}) has been added to this email as an attachment."
+msgstr ""
+
msgid "Your Commit Email will be used for web based operations, such as edits and merges."
msgstr ""
@@ -24188,6 +24724,9 @@ msgstr ""
msgid "Your comment could not be updated! Please check your network connection and try again."
msgstr ""
+msgid "Your comment will be discarded."
+msgstr ""
+
msgid "Your custom stage '%{title}' was created"
msgstr ""
@@ -24227,6 +24766,9 @@ msgstr ""
msgid "Your new personal access token has been created."
msgstr ""
+msgid "Your new project access token has been created."
+msgstr ""
+
msgid "Your password isn't required to view this page. If a password or any other personal details are requested, please contact your administrator to report abuse."
msgstr ""
@@ -24245,12 +24787,18 @@ msgstr ""
msgid "Your request for access has been queued for review."
msgstr ""
+msgid "Your search didn't match any commits."
+msgstr ""
+
msgid "Your subscription expired!"
msgstr ""
msgid "Your subscription has been downgraded"
msgstr ""
+msgid "Your subscription will automatically renew in %{remaining_days}"
+msgstr ""
+
msgid "Your subscription will expire in %{remaining_days}"
msgstr ""
@@ -24291,7 +24839,7 @@ msgstr ""
msgid "allowed to fail"
msgstr ""
-msgid "already being used for another group or project milestone."
+msgid "already being used for another group or project %{timebox_name}."
msgstr ""
msgid "already has a \"created\" issue link"
@@ -24369,6 +24917,9 @@ msgstr ""
msgid "ciReport|%{linkStartTag}Learn more about SAST %{linkEndTag}"
msgstr ""
+msgid "ciReport|%{linkStartTag}Learn more about Secret Scanning %{linkEndTag}"
+msgstr ""
+
msgid "ciReport|%{linkStartTag}Learn more about codequality reports %{linkEndTag}"
msgstr ""
@@ -24542,6 +25093,12 @@ msgstr ""
msgid "ciReport|SAST"
msgstr ""
+msgid "ciReport|Secret scanning"
+msgstr ""
+
+msgid "ciReport|Secret scanning detects secrets and credentials vulnerabilities in your source code."
+msgstr ""
+
msgid "ciReport|Security scanning"
msgstr ""
@@ -24747,6 +25304,9 @@ msgstr ""
msgid "group"
msgstr ""
+msgid "groups"
+msgstr ""
+
msgid "has already been linked to another vulnerability"
msgstr ""
@@ -24914,9 +25474,6 @@ msgstr[3] ""
msgid "merged %{time_ago}"
msgstr ""
-msgid "milestone should belong either to a project or a group."
-msgstr ""
-
msgid "missing"
msgstr ""
@@ -25253,6 +25810,9 @@ msgstr ""
msgid "no contributions"
msgstr ""
+msgid "no expiration"
+msgstr ""
+
msgid "no one can merge"
msgstr ""
@@ -25309,6 +25869,9 @@ msgstr ""
msgid "pending removal"
msgstr ""
+msgid "per day"
+msgstr ""
+
msgid "pipeline"
msgstr ""
@@ -25341,6 +25904,12 @@ msgstr[3] ""
msgid "project avatar"
msgstr ""
+msgid "projects"
+msgstr ""
+
+msgid "push to your repository, create pipelines, create issues or add comments. To reduce storage capacity, delete unused repositories, artifacts, wikis, issues, and pipelines."
+msgstr ""
+
msgid "quick actions"
msgstr ""
@@ -25489,13 +26058,6 @@ msgstr ""
msgid "this document"
msgstr ""
-msgid "thread resolved"
-msgid_plural "threads resolved"
-msgstr[0] ""
-msgstr[1] ""
-msgstr[2] ""
-msgstr[3] ""
-
msgid "to help your contributors communicate effectively!"
msgstr ""
@@ -25550,6 +26112,9 @@ msgstr ""
msgid "view the blob"
msgstr ""
+msgid "vulnerability|Add a comment"
+msgstr ""
+
msgid "vulnerability|Add a comment or reason for dismissal"
msgstr ""
diff --git a/locale/pt_BR/gitlab.po b/locale/pt_BR/gitlab.po
index 9565027da4d..156cac9c516 100644
--- a/locale/pt_BR/gitlab.po
+++ b/locale/pt_BR/gitlab.po
@@ -12,7 +12,7 @@ msgstr ""
"X-Crowdin-Project: gitlab-ee\n"
"X-Crowdin-Language: pt-BR\n"
"X-Crowdin-File: /master/locale/gitlab.pot\n"
-"PO-Revision-Date: 2020-04-15 00:26\n"
+"PO-Revision-Date: 2020-05-05 21:33\n"
msgid " %{start} to %{end}"
msgstr ""
@@ -212,6 +212,11 @@ msgid_plural "%d tags"
msgstr[0] ""
msgstr[1] ""
+msgid "%d unresolved thread"
+msgid_plural "%d unresolved threads"
+msgstr[0] ""
+msgstr[1] ""
+
msgid "%d vulnerability dismissed"
msgid_plural "%d vulnerabilities dismissed"
msgstr[0] ""
@@ -371,6 +376,9 @@ msgstr ""
msgid "%{mrText}, this issue will be closed automatically."
msgstr "%{mrText}, essa issue foi fechada automaticamente."
+msgid "%{namespace_name} is now read-only. You cannot: %{base_message}"
+msgstr ""
+
msgid "%{name} contained %{resultsString}"
msgstr "%{name} continha %{resultsString}"
@@ -383,6 +391,11 @@ msgstr ""
msgid "%{name}'s avatar"
msgstr "Avatar de %{name}"
+msgid "%{no_of_days} day"
+msgid_plural "%{no_of_days} days"
+msgstr[0] ""
+msgstr[1] ""
+
msgid "%{number_commits_behind} commits behind %{default_branch}, %{number_commits_ahead} commits ahead"
msgstr "%{number_commits_behind} commits atrás de %{default_branch}, %{number_commits_ahead} commits à frente"
@@ -506,6 +519,9 @@ msgstr[1] "%{text} %{files} arquivos"
msgid "%{text} is available"
msgstr "%{text} está disponível"
+msgid "%{timebox_name} should belong either to a project or a group."
+msgstr ""
+
msgid "%{title} %{operator} %{threshold}"
msgstr "%{title} %{operator} %{threshold}"
@@ -781,12 +797,18 @@ msgstr "<code>\"johnsmith@example.com\": \"johnsm...@example.com\"</code> adicio
msgid "<code>\"johnsmith@example.com\": \"johnsmith@example.com\"</code> will add \"By <a href=\"#\">johnsmith@example.com</a>\" to all issues and comments originally created by johnsmith@example.com. By default, the email address or username is masked to ensure the user's privacy. Use this option if you want to show the full email address."
msgstr "<code>\"johnsmith@example.com\": \"johnsmith@example.com\"</code> adicionará \"Por <a href=\"#\">johnsmith@example.com</a>\" a todas as issues e comentários originalmente criados por johnsmith@example.com. Por padrão, o endereço de e-mail ou nome de usuário é mascarado para garantir a privacidade do usuário. Use esta opção se você quiser mostrar o endereço de e-mail completo."
+msgid "<namespace / project>"
+msgstr ""
+
msgid "<no name set>"
msgstr "<nenhum nome definido>"
msgid "<no scopes selected>"
msgstr "<nenhum escopo selecionado>"
+msgid "<project name>"
+msgstr ""
+
msgid "<strong>%{group_name}</strong> group members"
msgstr "Membros do grupo <strong>%{group_name}</strong>"
@@ -820,6 +842,9 @@ msgstr "Uma conta Let's Encrypt será configurada para esta instalação do GitL
msgid "A basic page and serverless function that uses AWS Lambda, AWS API Gateway, and GitLab Pages"
msgstr ""
+msgid "A complete DevOps platform"
+msgstr ""
+
msgid "A default branch cannot be chosen for an empty project."
msgstr "Um branch padrão não pode ser escolhido para um projeto vazio."
@@ -1036,6 +1061,12 @@ msgstr ""
msgid "AccessTokens|reset it"
msgstr "redefini-lo"
+msgid "AccessibilityReport|Accessibility report artifact not found"
+msgstr ""
+
+msgid "AccessibilityReport|Failed to retrieve accessibility report"
+msgstr ""
+
msgid "AccessibilityReport|Learn More"
msgstr ""
@@ -1045,6 +1076,9 @@ msgstr ""
msgid "AccessibilityReport|New"
msgstr ""
+msgid "AccessibilityReport|The accessibility scanning found an error of the following type: %{code}"
+msgstr ""
+
msgid "Account"
msgstr "Conta"
@@ -1287,6 +1321,9 @@ msgstr "Adicionado um issue a um épico."
msgid "Added at"
msgstr "Adicionado em"
+msgid "Added for this merge request"
+msgstr ""
+
msgid "Added in this version"
msgstr "Adicionado nesta versão"
@@ -1658,6 +1695,72 @@ msgid_plural "Alerts"
msgstr[0] "Alerta"
msgstr[1] "Alertas"
+msgid "Alert Details"
+msgstr ""
+
+msgid "AlertManagement|Acknowledged"
+msgstr ""
+
+msgid "AlertManagement|Alert"
+msgstr ""
+
+msgid "AlertManagement|Authorize external service"
+msgstr ""
+
+msgid "AlertManagement|Display alerts from all your monitoring tools directly within GitLab. Streamline the investigation of your alerts and the escalation of alerts to incidents."
+msgstr ""
+
+msgid "AlertManagement|End time"
+msgstr ""
+
+msgid "AlertManagement|End time:"
+msgstr ""
+
+msgid "AlertManagement|Events"
+msgstr ""
+
+msgid "AlertManagement|Events:"
+msgstr ""
+
+msgid "AlertManagement|Full Alert Details"
+msgstr ""
+
+msgid "AlertManagement|More information"
+msgstr ""
+
+msgid "AlertManagement|No alerts available to display. If you think you're seeing this message in error, refresh the page."
+msgstr ""
+
+msgid "AlertManagement|No alerts to display."
+msgstr ""
+
+msgid "AlertManagement|Overview"
+msgstr ""
+
+msgid "AlertManagement|Resolved"
+msgstr ""
+
+msgid "AlertManagement|Severity"
+msgstr ""
+
+msgid "AlertManagement|Start time"
+msgstr ""
+
+msgid "AlertManagement|Start time:"
+msgstr ""
+
+msgid "AlertManagement|Status"
+msgstr ""
+
+msgid "AlertManagement|Surface alerts in GitLab"
+msgstr ""
+
+msgid "AlertManagement|There was an error displaying the alerts. Confirm your endpoint's configuration details to ensure alerts appear."
+msgstr ""
+
+msgid "AlertManagement|Triggered"
+msgstr ""
+
msgid "AlertService|%{linkStart}Learn more%{linkEnd} about configuring this endpoint to receive alerts."
msgstr ""
@@ -1727,6 +1830,9 @@ msgstr "Todos os projetos"
msgid "All security scans are enabled because %{linkStart}Auto DevOps%{linkEnd} is enabled on this project"
msgstr ""
+msgid "All threads resolved"
+msgstr ""
+
msgid "All users"
msgstr "Todos os usuários"
@@ -1745,6 +1851,9 @@ msgstr "Permitir que proprietários de grupos gerenciem configurações relacion
msgid "Allow only the selected protocols to be used for Git access."
msgstr "Permitir que apenas os protocolos selecionados sejam usados para acesso ao Git."
+msgid "Allow owners to manage default branch protection per group"
+msgstr ""
+
msgid "Allow owners to manually add users outside of LDAP"
msgstr ""
@@ -1853,6 +1962,9 @@ msgstr "Ocorreu um erro ao buscar os aprovadores da nova regra."
msgid "An error occurred fetching the dropdown data."
msgstr "Erro ao buscar os dados do dropdown."
+msgid "An error occurred fetching the project authors."
+msgstr ""
+
msgid "An error occurred previewing the blob"
msgstr "Erro ao pré-visualizar o blob"
@@ -1934,6 +2046,9 @@ msgstr "Ocorreu um erro ao buscar o autocomplemento de projetos."
msgid "An error occurred while fetching sidebar data"
msgstr "Erro ao recuperar informações da barra lateral"
+msgid "An error occurred while fetching terraform reports."
+msgstr ""
+
msgid "An error occurred while fetching the Service Desk address."
msgstr "Um erro ocorreu ao consultar o endereço da Central de Serviços."
@@ -2171,6 +2286,9 @@ msgstr "Verificação anti-spam"
msgid "Any"
msgstr "Qualquer um"
+msgid "Any Author"
+msgstr ""
+
msgid "Any Label"
msgstr "Qualquer etiqueta"
@@ -2386,12 +2504,12 @@ msgstr "Tem certeza que deseja arquivar este projeto?"
msgid "Are you sure that you want to unarchive this project?"
msgstr "Tem certeza que deseja desarquivar este projeto?"
-msgid "Are you sure you want to cancel creating this comment?"
-msgstr "Tem certeza de que deseja cancelar a criação deste comentário?"
-
msgid "Are you sure you want to cancel editing this comment?"
msgstr "Tem certeza de que deseja cancelar a edição deste comentário?"
+msgid "Are you sure you want to close this blocked issue?"
+msgstr ""
+
msgid "Are you sure you want to delete %{name}?"
msgstr ""
@@ -2419,6 +2537,9 @@ msgstr ""
msgid "Are you sure you want to deploy this environment?"
msgstr ""
+msgid "Are you sure you want to discard this comment?"
+msgstr ""
+
msgid "Are you sure you want to erase this build?"
msgstr "Tem certeza de que deseja limpar este build?"
@@ -2595,6 +2716,9 @@ msgstr ""
msgid "At least one approval from a code owner is required to change files matching the respective CODEOWNER rules."
msgstr "Pelo menos uma aprovação de um proprietário do código é necessária para alterar arquivos que coincidem com as respectivas regras CODEOWNER."
+msgid "At least one logging option is required to be enabled"
+msgstr ""
+
msgid "At least one of group_id or project_id must be specified"
msgstr ""
@@ -2633,6 +2757,27 @@ msgstr ""
msgid "AuditEvents|Target"
msgstr ""
+msgid "AuditLogs|(removed)"
+msgstr ""
+
+msgid "AuditLogs|Action"
+msgstr ""
+
+msgid "AuditLogs|Author"
+msgstr ""
+
+msgid "AuditLogs|Date"
+msgstr ""
+
+msgid "AuditLogs|IP Address"
+msgstr ""
+
+msgid "AuditLogs|Object"
+msgstr ""
+
+msgid "AuditLogs|Target"
+msgstr ""
+
msgid "Aug"
msgstr "Ago"
@@ -2915,9 +3060,6 @@ msgstr "URL raiz de bambu como https://bamboo.exemplo.com"
msgid "BambooService|You must set up automatic revision labeling and a repository trigger in Bamboo."
msgstr "Você precisa configurar etiquetas de revisão automáticas e um gatilho de repositório no Bamboo."
-msgid "Batch operations"
-msgstr ""
-
msgid "BatchComments|Delete all pending comments"
msgstr "Excluir todos os comentários pendentes"
@@ -3047,6 +3189,9 @@ msgstr ""
msgid "Boards"
msgstr "Painéis"
+msgid "Boards and Board Lists"
+msgstr ""
+
msgid "Boards|Collapse"
msgstr "Recolher"
@@ -3266,6 +3411,9 @@ msgstr "Comprar EE"
msgid "Buy GitLab Enterprise Edition"
msgstr "Comprar GitLab Edição Enterprise"
+msgid "Buy more Pipeline minutes"
+msgstr ""
+
msgid "By %{user_name}"
msgstr "Por %{user_name}"
@@ -3386,6 +3534,9 @@ msgstr "Não é possível remover membros do grupo sem uma conta gerenciada do g
msgid "Can't scan the code?"
msgstr "Não consegue escanear o código?"
+msgid "Can't update snippet: %{err}"
+msgstr ""
+
msgid "Canary"
msgstr ""
@@ -3554,6 +3705,9 @@ msgstr ""
msgid "Changing group path can have unintended side effects."
msgstr ""
+msgid "Charts"
+msgstr ""
+
msgid "Charts can't be displayed as the request for data has timed out. %{documentationLink}"
msgstr ""
@@ -3590,9 +3744,6 @@ msgstr ""
msgid "ChatMessage|and [%{count} more](%{pipeline_failed_jobs_url})"
msgstr ""
-msgid "ChatMessage|failed"
-msgstr ""
-
msgid "ChatMessage|has failed"
msgstr ""
@@ -3608,9 +3759,6 @@ msgstr ""
msgid "ChatMessage|in %{project_link}"
msgstr ""
-msgid "ChatMessage|passed"
-msgstr ""
-
msgid "Check again"
msgstr "Verifique Novamente"
@@ -4061,9 +4209,6 @@ msgstr "Limpa o peso."
msgid "Click any <strong>project name</strong> in the project list below to navigate to the project milestone."
msgstr "Clique em qualquer <strong>nome de projeto</strong> na lista a seguir para navegar para o marco do projeto."
-msgid "Click here"
-msgstr "Clique aqui"
-
msgid "Click the <strong>Download</strong> button and wait for downloading to complete."
msgstr "Clique no botão <strong>Baixar</strong> e aguarde a conclusão do download."
@@ -4451,6 +4596,12 @@ msgstr "Recuperando projetos"
msgid "ClusterIntegration|Fetching zones"
msgstr "Recuperando zonas"
+msgid "ClusterIntegration|Fluentd"
+msgstr ""
+
+msgid "ClusterIntegration|Fluentd is an open source data collector, which lets you unify the data collection and consumption for a better use and understanding of data. It requires at least one of the following logs to be successfully installed."
+msgstr ""
+
msgid "ClusterIntegration|GitLab Integration"
msgstr "Integração GitLab"
@@ -4757,6 +4908,15 @@ msgstr "Solicitação para início de instalação falhou"
msgid "ClusterIntegration|Request to begin uninstalling failed"
msgstr "Solicitação para início de desinstalação falhou"
+msgid "ClusterIntegration|SIEM Hostname"
+msgstr ""
+
+msgid "ClusterIntegration|SIEM Port"
+msgstr ""
+
+msgid "ClusterIntegration|SIEM Protocol"
+msgstr ""
+
msgid "ClusterIntegration|Save changes"
msgstr "Salvar alterações"
@@ -4853,6 +5013,12 @@ msgstr "Selecione a zona"
msgid "ClusterIntegration|Select zone to choose machine type"
msgstr "Selecione a zone para escolher o tipo de máquina"
+msgid "ClusterIntegration|Send Cilium Logs"
+msgstr ""
+
+msgid "ClusterIntegration|Send ModSecurity Logs"
+msgstr ""
+
msgid "ClusterIntegration|Service Token"
msgstr "Token de serviço"
@@ -5281,18 +5447,33 @@ msgstr ""
msgid "Compliance framework (optional)"
msgstr ""
+msgid "ComplianceFramework|GDPR"
+msgstr ""
+
msgid "ComplianceFramework|GDPR - General Data Protection Regulation"
msgstr ""
+msgid "ComplianceFramework|HIPAA"
+msgstr ""
+
msgid "ComplianceFramework|HIPAA - Health Insurance Portability and Accountability Act"
msgstr ""
+msgid "ComplianceFramework|PCI-DSS"
+msgstr ""
+
msgid "ComplianceFramework|PCI-DSS - Payment Card Industry-Data Security Standard"
msgstr ""
+msgid "ComplianceFramework|SOC 2"
+msgstr ""
+
msgid "ComplianceFramework|SOC 2 - Service Organization Control 2"
msgstr ""
+msgid "ComplianceFramework|SOX"
+msgstr ""
+
msgid "ComplianceFramework|SOX - Sarbanes-Oxley"
msgstr ""
@@ -5440,9 +5621,15 @@ msgstr ""
msgid "Container repositories sync capacity"
msgstr ""
+msgid "ContainerRegistry| Please visit the %{linkStart}administration settings%{linkEnd} to enable this feature."
+msgstr ""
+
msgid "ContainerRegistry|%{imageName} tags"
msgstr ""
+msgid "ContainerRegistry|%{title} was successfully scheduled for deletion"
+msgstr ""
+
msgid "ContainerRegistry|Automatically remove extra images that aren't designed to be kept."
msgstr ""
@@ -5455,6 +5642,9 @@ msgstr ""
msgid "ContainerRegistry|Container Registry"
msgstr ""
+msgid "ContainerRegistry|Container Registry tag expiration and retention policy is disabled"
+msgstr ""
+
msgid "ContainerRegistry|Copy build command"
msgstr ""
@@ -5464,18 +5654,12 @@ msgstr ""
msgid "ContainerRegistry|Copy push command"
msgstr ""
-msgid "ContainerRegistry|Currently, the Container Registry tag expiration feature is not available for projects created before GitLab version 12.8. For updates and more information, visit Issue %{linkStart}#196124%{linkEnd}"
-msgstr ""
-
msgid "ContainerRegistry|Docker connection error"
msgstr ""
msgid "ContainerRegistry|Docker tag expiration policy is %{toggleStatus}"
msgstr ""
-msgid "ContainerRegistry|Docker tags with names matching this regex pattern will expire:"
-msgstr ""
-
msgid "ContainerRegistry|Edit Settings"
msgstr ""
@@ -5497,9 +5681,6 @@ msgstr ""
msgid "ContainerRegistry|Image ID"
msgstr ""
-msgid "ContainerRegistry|Image deleted successfully"
-msgstr ""
-
msgid "ContainerRegistry|Keep and protect the images that matter most."
msgstr ""
@@ -5515,12 +5696,24 @@ msgstr ""
msgid "ContainerRegistry|Number of tags to retain:"
msgstr ""
+msgid "ContainerRegistry|Please contact your administrator."
+msgstr ""
+
msgid "ContainerRegistry|Push an image"
msgstr ""
msgid "ContainerRegistry|Quick Start"
msgstr "Início rápido"
+msgid "ContainerRegistry|Regular expressions such as %{codeStart}.*-test%{codeEnd} or %{codeStart}dev-.*%{codeEnd} are supported"
+msgstr ""
+
+msgid "ContainerRegistry|Regular expressions such as %{codeStart}.*-test%{codeEnd} or %{codeStart}dev-.*%{codeEnd} are supported. To select all tags, use %{codeStart}.*%{codeEnd}"
+msgstr ""
+
+msgid "ContainerRegistry|Remember to run %{docLinkStart}garbage collection%{docLinkEnd} to remove the stale data from storage."
+msgstr ""
+
msgid "ContainerRegistry|Remove repository"
msgstr "Apagar repositório"
@@ -5535,22 +5728,22 @@ msgstr[1] ""
msgid "ContainerRegistry|Retention policy has been Enabled"
msgstr ""
-msgid "ContainerRegistry|Something went wrong while deleting the image."
+msgid "ContainerRegistry|Something went wrong while fetching the expiration policy."
msgstr ""
-msgid "ContainerRegistry|Something went wrong while deleting the tag."
+msgid "ContainerRegistry|Something went wrong while fetching the repository list."
msgstr ""
-msgid "ContainerRegistry|Something went wrong while deleting the tags."
+msgid "ContainerRegistry|Something went wrong while fetching the tags list."
msgstr ""
-msgid "ContainerRegistry|Something went wrong while fetching the expiration policy."
+msgid "ContainerRegistry|Something went wrong while marking the tag for deletion."
msgstr ""
-msgid "ContainerRegistry|Something went wrong while fetching the packages list."
+msgid "ContainerRegistry|Something went wrong while marking the tags for deletion."
msgstr ""
-msgid "ContainerRegistry|Something went wrong while fetching the tags list."
+msgid "ContainerRegistry|Something went wrong while scheduling %{title} for deletion. Please try again."
msgstr ""
msgid "ContainerRegistry|Something went wrong while updating the expiration policy."
@@ -5559,16 +5752,25 @@ msgstr ""
msgid "ContainerRegistry|Tag"
msgstr "Tag"
-msgid "ContainerRegistry|Tag deleted successfully"
-msgstr ""
-
msgid "ContainerRegistry|Tag expiration policy"
msgstr ""
msgid "ContainerRegistry|Tag expiration policy is designed to:"
msgstr ""
-msgid "ContainerRegistry|Tags deleted successfully"
+msgid "ContainerRegistry|Tag successfully marked for deletion."
+msgstr ""
+
+msgid "ContainerRegistry|Tags successfully marked for deletion."
+msgstr ""
+
+msgid "ContainerRegistry|Tags with names matching this regex pattern will %{italicStart}be preserved:%{italicEnd}"
+msgstr ""
+
+msgid "ContainerRegistry|Tags with names matching this regex pattern will %{italicStart}expire:%{italicEnd}"
+msgstr ""
+
+msgid "ContainerRegistry|The Container Registry tag expiration and retention policies for this project have not been enabled."
msgstr ""
msgid "ContainerRegistry|The last tag related to this image was recently removed. This empty image and any associated data will be automatically removed as part of the regular Garbage Collection process. If you have any questions, contact your administrator."
@@ -5589,16 +5791,16 @@ msgstr ""
msgid "ContainerRegistry|There are no container images stored for this project"
msgstr ""
-msgid "ContainerRegistry|This Registry contains deleted image tag data. Remember to run %{docLinkStart}garbage collection%{docLinkEnd} to remove the stale data from storage."
+msgid "ContainerRegistry|There was an error during the deletion of this image repository, please try again."
msgstr ""
msgid "ContainerRegistry|This image has no active tags"
msgstr ""
-msgid "ContainerRegistry|We are having trouble connecting to Docker, which could be due to an issue with your project name or path. %{docLinkStart}More Information%{docLinkEnd}"
+msgid "ContainerRegistry|This image repository is scheduled for deletion"
msgstr ""
-msgid "ContainerRegistry|Wildcards such as %{codeStart}.*-stable%{codeEnd} or %{codeStart}production/.*%{codeEnd} are supported. To select all tags, use %{codeStart}.*%{codeEnd}"
+msgid "ContainerRegistry|We are having trouble connecting to Docker, which could be due to an issue with your project name or path. %{docLinkStart}More Information%{docLinkEnd}"
msgstr ""
msgid "ContainerRegistry|With the Container Registry, every project can have its own space to store its Docker images. %{docLinkStart}More Information%{docLinkEnd}"
@@ -5853,7 +6055,7 @@ msgstr ""
msgid "Could not delete chat nickname %{chat_name}."
msgstr "Não foi possível excluir o apelido de bate-papo %{chat_name}."
-msgid "Could not find design"
+msgid "Could not find design."
msgstr ""
msgid "Could not remove the trigger."
@@ -5868,6 +6070,9 @@ msgstr ""
msgid "Could not revoke personal access token %{personal_access_token_name}."
msgstr ""
+msgid "Could not revoke project access token %{project_access_token_name}."
+msgstr ""
+
msgid "Could not save group ID"
msgstr ""
@@ -5925,6 +6130,9 @@ msgstr ""
msgid "Create a new branch"
msgstr "Criar uma nova branch"
+msgid "Create a new deploy key for this project"
+msgstr ""
+
msgid "Create a new file as there are no files yet. Afterwards, you'll be able to commit your changes."
msgstr ""
@@ -6030,6 +6238,9 @@ msgstr "Criar etiqueta de projeto"
msgid "Create requirement"
msgstr ""
+msgid "Create snippet"
+msgstr ""
+
msgid "Create wildcard: %{searchTerm}"
msgstr ""
@@ -6051,6 +6262,9 @@ msgstr "criar um token de acesso pessoal"
msgid "Created"
msgstr "Feito"
+msgid "Created %{timestamp}"
+msgstr ""
+
msgid "Created At"
msgstr "Criado em"
@@ -6407,6 +6621,9 @@ msgstr ""
msgid "CycleAnalytics|stage dropdown"
msgstr ""
+msgid "DAG"
+msgstr ""
+
msgid "DNS"
msgstr "DNS"
@@ -6773,6 +6990,9 @@ msgstr "Chaves para deploy"
msgid "Deploy key was successfully updated."
msgstr "A chave de deploy foi atualizada com sucesso."
+msgid "Deploy keys allow read-only or read-write (if enabled) access to your repository. Deploy keys can be used for CI, staging or production servers. You can create a deploy key or add an existing one."
+msgstr ""
+
msgid "Deploy progress not found. To see pods, ensure your environment matches %{linkStart}deploy board criteria%{linkEnd}."
msgstr ""
@@ -6926,6 +7146,9 @@ msgstr "Deploy para"
msgid "Deploying to"
msgstr "Fazer deploy para"
+msgid "Deployment Frequency"
+msgstr ""
+
msgid "Deployment|API"
msgstr ""
@@ -7444,9 +7667,6 @@ msgstr ""
msgid "Edit environment"
msgstr "Editar ambiente"
-msgid "Edit epic description"
-msgstr ""
-
msgid "Edit file"
msgstr "Editar arquivo"
@@ -8068,9 +8288,6 @@ msgstr ""
msgid "Epics"
msgstr "Epics"
-msgid "Epics (Ultimate / Gold license only)"
-msgstr "Épicos (apenas licenças Ultimate / Gold)"
-
msgid "Epics Roadmap"
msgstr "Roadmap de epics"
@@ -8080,6 +8297,9 @@ msgstr ""
msgid "Epics let you manage your portfolio of projects more efficiently and with less effort"
msgstr "Epics permitem gerenciar seu portfólio de projetos de forma mais eficiente e com menos esforço"
+msgid "Epics, Issues, and Merge Requests"
+msgstr ""
+
msgid "Epics|Add an epic"
msgstr "Adicionar um épico"
@@ -8131,6 +8351,9 @@ msgstr "Algo deu errado ao buscar épicos filhos."
msgid "Epics|Something went wrong while fetching group epics."
msgstr ""
+msgid "Epics|Something went wrong while moving item."
+msgstr ""
+
msgid "Epics|Something went wrong while ordering item."
msgstr ""
@@ -8185,6 +8408,9 @@ msgstr "Erro ao carregar etiquetas."
msgid "Error fetching network graph."
msgstr "Erro ao recuperar gráfico de rede."
+msgid "Error fetching payload data."
+msgstr ""
+
msgid "Error fetching projects"
msgstr "Erro ao obter projetos"
@@ -8194,9 +8420,6 @@ msgstr "Erro ao recuperar refs"
msgid "Error fetching the dependency list. Please check your network connection and try again."
msgstr ""
-msgid "Error fetching usage ping data."
-msgstr "Erro ao recupera dados de ping."
-
msgid "Error loading branch data. Please try again."
msgstr "Erro ao carregar dados de branch. Por favor, tente novamente."
@@ -8338,6 +8561,9 @@ msgstr ""
msgid "Errors"
msgstr "Erros"
+msgid "Errors:"
+msgstr ""
+
msgid "Estimated"
msgstr "Estimativa"
@@ -8542,12 +8768,18 @@ msgstr "Explorar grupos públicos"
msgid "Export as CSV"
msgstr "Exportar como CSV"
+msgid "Export group"
+msgstr ""
+
msgid "Export issues"
msgstr "Exportar issues"
msgid "Export project"
msgstr "Exportar projeto"
+msgid "Export this group with all related data to a new GitLab instance. Once complete, you can import the data file from the \"New Group\" page."
+msgstr ""
+
msgid "Export this project with all its related data in order to move your project to a new GitLab instance. Once the export is finished, you can import the file from the \"New Project\" page."
msgstr "Exporte este projeto com todos os dados relacionados para mover seu projeto para uma nova instância do GitLab. Quando a exportação estiver concluída, você poderá importar o arquivo da página \"Novo projeto\"."
@@ -8617,9 +8849,6 @@ msgstr "Falha"
msgid "Failed Jobs"
msgstr "Tarefas com falha"
-msgid "Failed create wiki"
-msgstr "Falha ao criar wiki"
-
msgid "Failed to add a Zoom meeting"
msgstr ""
@@ -8659,6 +8888,9 @@ msgstr ""
msgid "Failed to create resources"
msgstr "Falha ao criar recursos"
+msgid "Failed to create wiki"
+msgstr ""
+
msgid "Failed to delete board. Please try again."
msgstr ""
@@ -8668,7 +8900,7 @@ msgstr "Falha ao fazer deploy para"
msgid "Failed to enqueue the rebase operation, possibly due to a long-lived transaction. Try again later."
msgstr ""
-msgid "Failed to find import label for jira import."
+msgid "Failed to find import label for Jira import."
msgstr ""
msgid "Failed to get ref."
@@ -8692,6 +8924,12 @@ msgstr ""
msgid "Failed to load groups & users."
msgstr ""
+msgid "Failed to load labels. Please try again."
+msgstr ""
+
+msgid "Failed to load milestones. Please try again."
+msgstr ""
+
msgid "Failed to load related branches"
msgstr "Falha ao carregar branches relacionados"
@@ -9067,7 +9305,10 @@ msgstr "Filtrar por mensagem de commit"
msgid "Filter by milestone name"
msgstr "Filtrar por nome de marco"
-msgid "Filter by name..."
+msgid "Filter by name"
+msgstr ""
+
+msgid "Filter by status"
msgstr ""
msgid "Filter by two-factor authentication"
@@ -9079,12 +9320,18 @@ msgstr ""
msgid "Filter projects"
msgstr ""
+msgid "Filter results"
+msgstr ""
+
msgid "Filter results by group"
msgstr "Filtrar resultados por grupo"
msgid "Filter results by project"
msgstr "Filtrar resultados por projeto"
+msgid "Filter results..."
+msgstr ""
+
msgid "Filter your projects by name"
msgstr "Filtre seus projetos por nome"
@@ -9373,11 +9620,11 @@ msgstr ""
msgid "GeoNodes|Checksummed"
msgstr "Checksum verificado"
-msgid "GeoNodes|Container repositories"
+msgid "GeoNodes|Consult Geo troubleshooting information"
msgstr ""
-msgid "GeoNodes|Data is out of date from %{timeago}"
-msgstr "Os dados estão desatualizados de %{timeago}"
+msgid "GeoNodes|Container repositories"
+msgstr ""
msgid "GeoNodes|Data replication lag"
msgstr "Atraso de replicação de dados"
@@ -9418,6 +9665,9 @@ msgstr "O ID do último evento processado por cursor"
msgid "GeoNodes|Last event ID seen from primary"
msgstr "O ID do último evento visto do primário"
+msgid "GeoNodes|Learn more about Geo node statuses"
+msgstr ""
+
msgid "GeoNodes|Loading nodes"
msgstr "Carregando nós"
@@ -9433,6 +9683,9 @@ msgstr ""
msgid "GeoNodes|Node was successfully removed."
msgstr "O nó foi removido com sucesso."
+msgid "GeoNodes|Node's status was updated %{timeAgo}."
+msgstr ""
+
msgid "GeoNodes|Not checksummed"
msgstr "Soma de verificação não conferida"
@@ -9463,8 +9716,11 @@ msgstr "Progresso de soma de verificação de repositório"
msgid "GeoNodes|Repository verification progress"
msgstr "Progresso de verificação de repositório"
-msgid "GeoNodes|Selective"
-msgstr "Seletivo"
+msgid "GeoNodes|Selective (%{syncLabel})"
+msgstr ""
+
+msgid "GeoNodes|Selective synchronization"
+msgstr ""
msgid "GeoNodes|Something went wrong while changing node status"
msgstr "Alguma coisa deu errada ao mudar o status do nó"
@@ -9490,6 +9746,9 @@ msgstr "Slots não usados"
msgid "GeoNodes|Unverified"
msgstr "Não verificado"
+msgid "GeoNodes|Updated %{timeAgo}"
+msgstr ""
+
msgid "GeoNodes|Used slots"
msgstr "Slots usados"
@@ -9529,17 +9788,17 @@ msgstr ""
msgid "Geo|All"
msgstr "Todos"
+msgid "Geo|All %{replicable_type}"
+msgstr ""
+
msgid "Geo|All projects"
msgstr "Todos os projetos"
-msgid "Geo|All projects are being scheduled for re-sync"
-msgstr "Todos os projetos estão sendo agendados para re-sincronização"
-
-msgid "Geo|All projects are being scheduled for re-verify"
+msgid "Geo|All projects are being scheduled for resync"
msgstr ""
-msgid "Geo|Batch operations"
-msgstr "Operações em lote"
+msgid "Geo|All projects are being scheduled for reverify"
+msgstr ""
msgid "Geo|Could not remove tracking entry for an existing project."
msgstr "Não foi possível remover o registro de rastreamento de um projeto existente."
@@ -9550,9 +9809,15 @@ msgstr "Não foi possível remover a entrada de rastreamento para um envio exist
msgid "Geo|Failed"
msgstr "Falha"
+msgid "Geo|Filter by status"
+msgstr ""
+
msgid "Geo|Geo Status"
msgstr "Geo Status"
+msgid "Geo|In progress"
+msgstr ""
+
msgid "Geo|In sync"
msgstr "Em sincronia"
@@ -9577,9 +9842,6 @@ msgstr "Próxima sincronização programada às"
msgid "Geo|Not synced yet"
msgstr "Ainda não sincronizado"
-msgid "Geo|Pending"
-msgstr "Pendente"
-
msgid "Geo|Pending synchronization"
msgstr "Sincronização pendente"
@@ -9616,8 +9878,8 @@ msgstr ""
msgid "Geo|Resync"
msgstr "Ressincronizar"
-msgid "Geo|Resync all projects"
-msgstr "Sincronizar novamente todos os projetos"
+msgid "Geo|Resync all"
+msgstr ""
msgid "Geo|Retry count"
msgstr "Contagem de retentativas"
@@ -9625,7 +9887,7 @@ msgstr "Contagem de retentativas"
msgid "Geo|Reverify"
msgstr ""
-msgid "Geo|Reverify all projects"
+msgid "Geo|Reverify all"
msgstr ""
msgid "Geo|Status"
@@ -9967,9 +10229,6 @@ msgstr "Ir para ambientes"
msgid "Go to file"
msgstr "Ir para arquivo"
-msgid "Go to file (MRs only)"
-msgstr "Ir para arquivo (apenas MRs)"
-
msgid "Go to file permalink (while viewing a file)"
msgstr ""
@@ -10414,6 +10673,9 @@ msgstr ""
msgid "GroupSettings|Disable group mentions"
msgstr ""
+msgid "GroupSettings|Export group"
+msgstr ""
+
msgid "GroupSettings|If the parent group's visibility is lower than the group current visibility, visibility levels for subgroups and projects will be changed to match the new parent group's visibility."
msgstr ""
@@ -10492,6 +10754,9 @@ msgstr "Grupos"
msgid "Groups (%{count})"
msgstr ""
+msgid "Groups (%{groups})"
+msgstr ""
+
msgid "Groups can also be nested by creating %{subgroup_docs_link_start}subgroups%{subgroup_docs_link_end}."
msgstr "Grupos também podem ser aninhados criando %{subgroup_docs_link_start}subgrupos%{subgroup_docs_link_end}."
@@ -10757,15 +11022,12 @@ msgstr "ID"
msgid "ID:"
msgstr "ID:"
-msgid "IDE|Allow live previews of JavaScript projects in the Web IDE using CodeSandbox client side evaluation."
-msgstr "Permitir visualizações ao vivo de projetos JavaScript no Web IDE usando a avaliação do lado do cliente CodeSandbox."
+msgid "IDE|Allow live previews of JavaScript projects in the Web IDE using CodeSandbox Live Preview."
+msgstr ""
msgid "IDE|Back"
msgstr "Voltar"
-msgid "IDE|Client side evaluation"
-msgstr "Avaliação do lado do cliente"
-
msgid "IDE|Commit"
msgstr "Commit"
@@ -10829,6 +11091,9 @@ msgstr ""
msgid "If any job surpasses this timeout threshold, it will be marked as failed. Human readable time input language is accepted like \"1 hour\". Values without specification represent seconds."
msgstr ""
+msgid "If blank, set allowable lifetime to %{instance_level_policy_in_words}, as defined by the instance admin. Once set, existing tokens for users in this group may be revoked."
+msgstr ""
+
msgid "If checked, group owners can manage LDAP group links and LDAP member overrides"
msgstr ""
@@ -10868,6 +11133,9 @@ msgstr ""
msgid "If you lose your recovery codes you can generate new ones, invalidating all previous codes."
msgstr ""
+msgid "If you reach 100%% storage capacity, you will not be able to: %{base_message}"
+msgstr ""
+
msgid "If your HTTP repository is not publicly accessible, add your credentials."
msgstr ""
@@ -10979,6 +11247,9 @@ msgstr "Importar repositórios do GitHub"
msgid "Import repository"
msgstr "Importar repositório"
+msgid "Import started by: %{importInitiator}"
+msgstr ""
+
msgid "Import tasks"
msgstr ""
@@ -11045,6 +11316,9 @@ msgstr ""
msgid "In order to tailor your experience with GitLab we<br>would like to know a bit more about you."
msgstr ""
+msgid "In progress"
+msgstr ""
+
msgid "In the next step, you'll be able to select the projects you want to import."
msgstr "Na próxima etapa, você poderá selecionar os projetos que deseja importar."
@@ -11392,15 +11666,15 @@ msgstr ""
msgid "Issues"
msgstr "Issues"
-msgid "Issues / Merge Requests"
-msgstr ""
-
msgid "Issues Analytics"
msgstr ""
msgid "Issues Rate Limits"
msgstr ""
+msgid "Issues and Merge Requests"
+msgstr ""
+
msgid "Issues can be bugs, tasks or ideas to be discussed. Also, issues are searchable and filterable."
msgstr "Issues podem ser bugs, tarefas ou ideias a serem discutidas. Além disso, issues são pesquisáveis e filtráveis."
@@ -11422,11 +11696,11 @@ msgstr "Depois que você começa a criar issues para seus projetos, podemos come
msgid "IssuesAnalytics|Avg/Month:"
msgstr ""
-msgid "IssuesAnalytics|Issues created"
+msgid "IssuesAnalytics|Issues opened"
msgstr ""
-msgid "IssuesAnalytics|Issues created per month"
-msgstr "Issues criadas por mês"
+msgid "IssuesAnalytics|Issues opened per month"
+msgstr ""
msgid "IssuesAnalytics|Last 12 months"
msgstr "Últimos 12 meses"
@@ -11479,6 +11753,9 @@ msgstr ""
msgid "Jira integration not configured."
msgstr ""
+msgid "Jira project: %{importProject}"
+msgstr ""
+
msgid "JiraService|Events for %{noteable_model_name} are disabled."
msgstr "Eventos para %{noteable_model_name} estão desabilitados."
@@ -11868,6 +12145,9 @@ msgstr ""
msgid "Lead"
msgstr ""
+msgid "Lead Time"
+msgstr ""
+
msgid "Learn GitLab"
msgstr "Aprenda GitLab"
@@ -11880,6 +12160,9 @@ msgstr "Aprenda como %{link_start}contribuir para os modelos embutidos%{link_end
msgid "Learn how to %{no_packages_link_start}publish and share your packages%{no_packages_link_end} with GitLab."
msgstr "Saiba como %{no_packages_link_start}publicar e compartilhar seus pacotes%{no_packages_link_end} com o GitLab."
+msgid "Learn how to enable synchronization"
+msgstr ""
+
msgid "Learn more"
msgstr "Saiba mais"
@@ -12211,9 +12494,6 @@ msgstr "Liste seus repositórios do servidor Bitbucket"
msgid "Live preview"
msgstr "Pré-visualização ao vivo"
-msgid "Load more vulnerabilities"
-msgstr ""
-
msgid "Loading"
msgstr ""
@@ -12364,9 +12644,6 @@ msgstr ""
msgid "Manage"
msgstr "Gerenciar"
-msgid "Manage Git repositories with fine-grained access controls that keep your code secure. Perform code reviews and enhance collaboration with merge requests. Each project can also have an issue tracker and a wiki."
-msgstr "Gerencie repositórios Git com controles de acesso refinados que mantêm seu código seguro. Realize revisões de código e aprimore a colaboração com merge requests. Cada projeto também pode ter um rastreador de issue e um wiki."
-
msgid "Manage Web IDE features"
msgstr "Gerenciar recursos do Web IDE"
@@ -12397,6 +12674,9 @@ msgstr "Gerenciar a autenticação de dois fatores"
msgid "Manage your license"
msgstr ""
+msgid "Managed Account"
+msgstr ""
+
msgid "Manifest"
msgstr "Manifesto"
@@ -12661,6 +12941,9 @@ msgstr "Merge Requests criadas"
msgid "Merge Requests in Review"
msgstr ""
+msgid "Merge automatically (%{strategy})"
+msgstr ""
+
msgid "Merge commit message"
msgstr "Mensagem do commit de merge"
@@ -12814,6 +13097,12 @@ msgstr "Merge realizado"
msgid "Merged branches are being deleted. This can take some time depending on the number of branches. Please refresh the page to see changes."
msgstr "Branches com merge realizado estão sendo excluídas. Isso pode levar algum tempo dependendo do número de branches. Por favor, atualize a página para ver as alterações."
+msgid "Merged this merge request."
+msgstr ""
+
+msgid "Merges this merge request immediately."
+msgstr ""
+
msgid "Merges this merge request when the pipeline succeeds."
msgstr ""
@@ -12921,12 +13210,15 @@ msgid_plural "Metrics|Edit metrics"
msgstr[0] ""
msgstr[1] ""
-msgid "Metrics|Environment"
-msgstr "Ambiente"
+msgid "Metrics|Expand panel"
+msgstr ""
msgid "Metrics|For grouping similar metrics"
msgstr "Para agrupar métricas similares"
+msgid "Metrics|Go back (Esc)"
+msgstr ""
+
msgid "Metrics|Invalid time range, please verify."
msgstr ""
@@ -12957,9 +13249,6 @@ msgstr "Documentação de consulta do Prometheus"
msgid "Metrics|Refresh dashboard"
msgstr ""
-msgid "Metrics|Show last"
-msgstr ""
-
msgid "Metrics|There was an error creating the dashboard."
msgstr ""
@@ -13038,6 +13327,9 @@ msgstr ""
msgid "Microsoft Azure"
msgstr ""
+msgid "Middleman project with Static Site Editor support"
+msgstr ""
+
msgid "Migrated %{success_count}/%{total_count} files."
msgstr ""
@@ -13483,10 +13775,10 @@ msgstr ""
msgid "Next"
msgstr "Next"
-msgid "Next file in diff (MRs only)"
+msgid "Next file in diff"
msgstr ""
-msgid "Next unresolved discussion (MRs only)"
+msgid "Next unresolved discussion"
msgstr ""
msgid "Nickname"
@@ -13546,6 +13838,9 @@ msgstr "Sem alterarções"
msgid "No changes between %{ref_start}%{source_branch}%{ref_end} and %{ref_start}%{target_branch}%{ref_end}"
msgstr ""
+msgid "No child epics match applied filters"
+msgstr ""
+
msgid "No connection could be made to a Gitaly Server, please check your logs!"
msgstr "Nenhuma conexão pode ser feita para um servidor Gitaly, por favor check os logs!"
@@ -13684,15 +13979,6 @@ msgstr ""
msgid "No thanks, don't show this again"
msgstr ""
-msgid "No vulnerabilities found for this group"
-msgstr ""
-
-msgid "No vulnerabilities found for this pipeline"
-msgstr ""
-
-msgid "No vulnerabilities found for this project"
-msgstr ""
-
msgid "No vulnerabilities present"
msgstr ""
@@ -13816,6 +14102,9 @@ msgstr ""
msgid "Nothing to preview."
msgstr "Nada para pré-visualizar."
+msgid "Nothing to synchronize"
+msgstr ""
+
msgid "Notification events"
msgstr "Eventos de notificação"
@@ -13987,6 +14276,9 @@ msgstr "Uma vez importados, os repositórios podem ser espelhados por SSH. Leia
msgid "Once removed, the fork relationship cannot be restored and you will no longer be able to send merge requests to the source."
msgstr "Uma vez removido, o relacionamento do fork não pode ser restaurando e você não poderá mais enviar merge requests para o repositório original."
+msgid "Once the exported file is ready you can download it from this page."
+msgstr ""
+
msgid "Once the exported file is ready, you will receive a notification email with a download link, or you can download it from this page."
msgstr "Quando o arquivo exportado estiver pronto, você receberá um e-mail de notificação com um link para download ou poderá fazer o download a partir desta página."
@@ -14088,9 +14380,6 @@ msgstr ""
msgid "Open sidebar"
msgstr "Abrir barra lateral"
-msgid "Open source software to collaborate on code"
-msgstr "Software de código aberto para colaborar no código"
-
msgid "Open: %{openIssuesCount}"
msgstr ""
@@ -14193,6 +14482,9 @@ msgstr ""
msgid "OutdatedBrowser|You can provide feedback %{feedback_link_start}on this issue%{feedback_link_end} or via your usual support channels."
msgstr ""
+msgid "Overridden"
+msgstr ""
+
msgid "Overview"
msgstr "Visão geral"
@@ -14244,6 +14536,9 @@ msgstr ""
msgid "PackageRegistry|Conan Command"
msgstr ""
+msgid "PackageRegistry|Copy .pypirc content"
+msgstr ""
+
msgid "PackageRegistry|Copy Conan Command"
msgstr ""
@@ -14265,6 +14560,9 @@ msgstr ""
msgid "PackageRegistry|Copy NuGet Setup Command"
msgstr ""
+msgid "PackageRegistry|Copy Pip command"
+msgstr ""
+
msgid "PackageRegistry|Copy and paste this inside your %{codeStart}pom.xml%{codeEnd} %{codeStart}dependencies%{codeEnd} block."
msgstr ""
@@ -14298,6 +14596,12 @@ msgstr ""
msgid "PackageRegistry|For more information on the NuGet registry, %{linkStart}see the documentation%{linkEnd}."
msgstr ""
+msgid "PackageRegistry|For more information on the PyPi registry, %{linkStart}see the documentation%{linkEnd}."
+msgstr ""
+
+msgid "PackageRegistry|If you haven't already done so, you will need to add the below to your %{codeStart}.pypirc%{codeEnd} file."
+msgstr ""
+
msgid "PackageRegistry|If you haven't already done so, you will need to add the below to your %{codeStart}pom.xml%{codeEnd} file."
msgstr ""
@@ -14307,6 +14611,9 @@ msgstr ""
msgid "PackageRegistry|Learn how to %{noPackagesLinkStart}publish and share your packages%{noPackagesLinkEnd} with GitLab."
msgstr ""
+msgid "PackageRegistry|Manually Published"
+msgstr ""
+
msgid "PackageRegistry|Maven"
msgstr ""
@@ -14325,12 +14632,18 @@ msgstr ""
msgid "PackageRegistry|NuGet Command"
msgstr ""
+msgid "PackageRegistry|Pip Command"
+msgstr ""
+
msgid "PackageRegistry|Pipeline %{linkStart}%{linkEnd} triggered %{timestamp} by %{author}"
msgstr ""
msgid "PackageRegistry|Published to the repository at %{timestamp}"
msgstr ""
+msgid "PackageRegistry|PyPi"
+msgstr ""
+
msgid "PackageRegistry|Registry Setup"
msgstr ""
@@ -14367,6 +14680,9 @@ msgstr ""
msgid "PackageRegistry|npm"
msgstr ""
+msgid "PackageRegistry|published by %{author}"
+msgstr ""
+
msgid "PackageRegistry|yarn"
msgstr ""
@@ -14382,6 +14698,9 @@ msgstr ""
msgid "PackageType|NuGet"
msgstr ""
+msgid "PackageType|PyPi"
+msgstr ""
+
msgid "Packages"
msgstr "Pacotes"
@@ -14475,6 +14794,9 @@ msgstr ""
msgid "Past due"
msgstr "Atrasado"
+msgid "Paste a machine public key here. Read more about how to generate it"
+msgstr ""
+
msgid "Paste a machine public key here. Read more about how to generate it %{link_start}here%{link_end}"
msgstr ""
@@ -14490,9 +14812,6 @@ msgstr ""
msgid "Path"
msgstr ""
-msgid "Path, transfer, remove"
-msgstr "Caminho, transferir, remover"
-
msgid "Path:"
msgstr "Caminho:"
@@ -14523,8 +14842,8 @@ msgstr ""
msgid "Percentage"
msgstr ""
-msgid "Perform advanced options such as changing path, transferring, or removing the group."
-msgstr "Execute opções avançadas, como alterar o caminho, transferir ou remover o grupo."
+msgid "Perform advanced options such as changing path, transferring, exporting, or removing the group."
+msgstr ""
msgid "Perform common operations on GitLab project"
msgstr ""
@@ -14604,9 +14923,6 @@ msgstr "Agendamentos da Pipeline"
msgid "Pipeline minutes quota"
msgstr ""
-msgid "Pipeline quota"
-msgstr "Cota de pipeline"
-
msgid "Pipeline subscriptions"
msgstr ""
@@ -14616,6 +14932,9 @@ msgstr "Gatilho de pipeline"
msgid "Pipeline: %{status}"
msgstr "Pipeline: %{status}"
+msgid "PipelineCharts|CI / CD Analytics"
+msgstr ""
+
msgid "PipelineCharts|Failed:"
msgstr "Falhou:"
@@ -14694,15 +15013,6 @@ msgstr ""
msgid "Pipelines settings for '%{project_name}' were successfully updated."
msgstr ""
-msgid "Pipelines| to purchase more minutes."
-msgstr ""
-
-msgid "Pipelines|%{namespace_name} has exceeded its pipeline minutes quota."
-msgstr ""
-
-msgid "Pipelines|%{namespace_name} has less than %{notification_level}%% of CI minutes available."
-msgstr ""
-
msgid "Pipelines|API"
msgstr "API"
@@ -14724,12 +15034,15 @@ msgstr "A integração contínua pode ajudar a capturar bugs, executando seus te
msgid "Pipelines|Get started with Pipelines"
msgstr "Saiba como funcionam as pipelines"
-msgid "Pipelines|Loading Pipelines"
-msgstr "Carregando Pipelines"
+msgid "Pipelines|Group %{namespace_name} has %{percentage}%% or less Shared Runner Pipeline minutes remaining. Once it runs out, no new jobs or pipelines in its projects will run."
+msgstr ""
-msgid "Pipelines|Pipelines will not run anymore on shared Runners."
+msgid "Pipelines|Group %{namespace_name} has exceeded its pipeline minutes quota. Unless you buy additional pipeline minutes, no new jobs or pipelines in its projects will run."
msgstr ""
+msgid "Pipelines|Loading Pipelines"
+msgstr "Carregando Pipelines"
+
msgid "Pipelines|Project cache successfully reset."
msgstr "Cache do projeto redefinido com sucesso."
@@ -14958,6 +15271,9 @@ msgstr ""
msgid "Please select"
msgstr ""
+msgid "Please select a Jira project"
+msgstr ""
+
msgid "Please select a country"
msgstr ""
@@ -15003,6 +15319,9 @@ msgstr "Por favor, aguarde enquanto conectamos ao seu repositório. Atualize à
msgid "Please wait while we import the repository for you. Refresh at will."
msgstr "Por favor, aguarde enquanto importamos o repositório para você. Atualize à vontade."
+msgid "Plugins directory is deprecated and will be removed in 14.0. Please move this file into /file_hooks directory."
+msgstr ""
+
msgid "Pod does not exist"
msgstr ""
@@ -15120,7 +15439,7 @@ msgstr ""
msgid "Prevent users from changing their profile name"
msgstr ""
-msgid "Prevent users from modifing merge request approvers list"
+msgid "Prevent users from modifying merge request approvers list"
msgstr ""
msgid "Prevent users from performing write operations on GitLab while performing maintenance."
@@ -15141,10 +15460,10 @@ msgstr "Pré-visualização de carga"
msgid "Previous Artifacts"
msgstr ""
-msgid "Previous file in diff (MRs only)"
+msgid "Previous file in diff"
msgstr ""
-msgid "Previous unresolved discussion (MRs only)"
+msgid "Previous unresolved discussion"
msgstr ""
msgid "Primary"
@@ -15177,6 +15496,9 @@ msgstr "Grupo(s) privado(s)"
msgid "Private profile"
msgstr "Perfil privado"
+msgid "Private projects Minutes cost factor"
+msgstr ""
+
msgid "Private projects can be created in your personal namespace with:"
msgstr "Projetos privados podem ser criados em seu namespace pessoal com:"
@@ -16005,6 +16327,9 @@ msgstr ""
msgid "ProjectSettings|With GitLab Pages you can host your static websites on GitLab"
msgstr ""
+msgid "ProjectSettings|With Metrics Dashboard you can visualize this project performance metrics"
+msgstr ""
+
msgid "ProjectTemplates|.NET Core"
msgstr ".NET Core"
@@ -16068,6 +16393,9 @@ msgstr ""
msgid "ProjectTemplates|Spring"
msgstr "Spring"
+msgid "ProjectTemplates|Static Site Editor/Middleman"
+msgstr ""
+
msgid "ProjectTemplates|iOS (Swift)"
msgstr "iOS (Swift)"
@@ -16287,15 +16615,15 @@ msgstr "URL da API base do Prometheus. como http://prometheus.example.com/"
msgid "PrometheusService|Prometheus is being automatically managed on your clusters"
msgstr "Prometheus está sendo automaticamente gerenciado nos seus clusters"
+msgid "PrometheusService|Select the Active checkbox to override the Auto Configuration with custom settings. If unchecked, Auto Configuration settings are used."
+msgstr ""
+
msgid "PrometheusService|These metrics will only be monitored after your first deployment to an environment"
msgstr "Essas métricas serão monitoradas após sua primeira implantação para um ambiente"
msgid "PrometheusService|Time-series monitoring service"
msgstr "Serviço de monitoramento de tempo-de-série"
-msgid "PrometheusService|To enable manual configuration, uninstall Prometheus from your clusters"
-msgstr "Para ativar a configuração manual, desinstale o Prometheus dos seus clusters"
-
msgid "PrometheusService|To enable the installation of Prometheus on your clusters, deactivate the manual configuration below"
msgstr "Para ativar a instalação do Prometheus nos seus clusters, desative a configuração manual abaixo"
@@ -16479,6 +16807,9 @@ msgstr "Seu ambiente foi desprotegido"
msgid "Protip:"
msgstr "Dicas:"
+msgid "Protocol"
+msgstr ""
+
msgid "Provider"
msgstr "Provedor"
@@ -16503,6 +16834,9 @@ msgstr ""
msgid "Public pipelines"
msgstr "Pipelines públicos"
+msgid "Public projects Minutes cost factor"
+msgstr ""
+
msgid "Pull"
msgstr "Pull"
@@ -16703,6 +17037,9 @@ msgid_plural "Refreshing in %d seconds to show the updated status..."
msgstr[0] "Atualizando em um segundo para mostrar o status atualizado..."
msgstr[1] "Atualizando em %d segundos para mostrar o status atualizado..."
+msgid "Regenerate export"
+msgstr ""
+
msgid "Regenerate instance ID"
msgstr ""
@@ -17080,9 +17417,29 @@ msgstr "Reportando"
msgid "Reports|%{combinedString} and %{resolvedString}"
msgstr ""
+msgid "Reports|Accessibility scanning detected %d issue for the source branch only"
+msgid_plural "Reports|Accessibility scanning detected %d issues for the source branch only"
+msgstr[0] ""
+msgstr[1] ""
+
+msgid "Reports|Accessibility scanning detected no issues for the source branch only"
+msgstr ""
+
+msgid "Reports|Accessibility scanning failed loading results"
+msgstr ""
+
+msgid "Reports|Accessibility scanning results are being parsed"
+msgstr ""
+
msgid "Reports|Actions"
msgstr "Ações"
+msgid "Reports|An error occured while loading report"
+msgstr ""
+
+msgid "Reports|An error occurred while loading %{name} results"
+msgstr ""
+
msgid "Reports|Class"
msgstr "Classe"
@@ -17188,7 +17545,7 @@ msgstr ""
msgid "Requested %{time_ago}"
msgstr "Solicitado %{time_ago}"
-msgid "Requested design version does not exist"
+msgid "Requested design version does not exist."
msgstr ""
msgid "Requested states are invalid"
@@ -17372,6 +17729,9 @@ msgstr ""
msgid "Resync"
msgstr ""
+msgid "Resync all"
+msgstr ""
+
msgid "Resync all %{replicableType}"
msgstr ""
@@ -17440,6 +17800,9 @@ msgstr ""
msgid "Revoked personal access token %{personal_access_token_name}!"
msgstr ""
+msgid "Revoked project access token %{project_access_token_name}!"
+msgstr ""
+
msgid "RightSidebar|adding a"
msgstr ""
@@ -17488,6 +17851,9 @@ msgstr "O runner executa tarefas de projetos atribuídos"
msgid "Runner token"
msgstr "Token de runner"
+msgid "Runner tokens"
+msgstr ""
+
msgid "Runner was not updated."
msgstr ""
@@ -17542,6 +17908,9 @@ msgstr "SAML SSO"
msgid "SAML SSO for %{group_name}"
msgstr "SAML SSO para %{group_name}"
+msgid "SAML discovery tokens"
+msgstr ""
+
msgid "SAML for %{group_name}"
msgstr ""
@@ -17626,12 +17995,18 @@ msgstr "Agendar nova pipeline"
msgid "Scheduled"
msgstr "Agendado"
+msgid "Scheduled to merge this merge request (%{strategy})."
+msgstr ""
+
msgid "Scheduled to merge this merge request when the pipeline succeeds."
msgstr ""
msgid "Schedules"
msgstr "Agendamentos"
+msgid "Schedules to merge this merge request (%{strategy})."
+msgstr ""
+
msgid "Scheduling"
msgstr ""
@@ -17689,6 +18064,9 @@ msgstr "Pesquisar branches"
msgid "Search branches and tags"
msgstr "Procurar branch e tags"
+msgid "Search by author"
+msgstr ""
+
msgid "Search files"
msgstr "Procurar arquivos"
@@ -17834,11 +18212,6 @@ msgid_plural "SearchResults|snippets"
msgstr[0] ""
msgstr[1] ""
-msgid "SearchResults|snippet result"
-msgid_plural "SearchResults|snippet results"
-msgstr[0] ""
-msgstr[1] ""
-
msgid "SearchResults|user"
msgid_plural "SearchResults|users"
msgstr[0] ""
@@ -17849,6 +18222,9 @@ msgid_plural "SearchResults|wiki results"
msgstr[0] ""
msgstr[1] ""
+msgid "Searching by both author and message is currently not supported."
+msgstr ""
+
msgid "Seat Link"
msgstr ""
@@ -17876,184 +18252,232 @@ msgstr ""
msgid "Security Dashboard"
msgstr "Painel de segurança"
-msgid "Security Dashboard|Error fetching the vulnerability counts. Please check your network connection and try again."
-msgstr "Erro ao recuperar contagens de vulnerabilidades. Por favor, verifique sua conexão de rede e tente novamente."
+msgid "Security configuration help link"
+msgstr ""
-msgid "Security Dashboard|Error fetching the vulnerability list. Please check your network connection and try again."
-msgstr "Erro ao recuperar lista de vulnerabilidades. Por favor, verifique sua conexão de rede e tente novamente."
+msgid "Security dashboard"
+msgstr ""
-msgid "Security Dashboard|Issue Created"
-msgstr "Issue criado"
+msgid "Security report is out of date. Please update your branch with the latest changes from the target branch (%{targetBranchName})"
+msgstr ""
-msgid "Security Reports|Comment added to '%{vulnerabilityName}'"
+msgid "Security report is out of date. Run %{newPipelineLinkStart}a new pipeline%{newPipelineLinkEnd} for the target branch (%{targetBranchName})"
msgstr ""
-msgid "Security Reports|Comment deleted on '%{vulnerabilityName}'"
+msgid "SecurityConfiguration|Configured"
msgstr ""
-msgid "Security Reports|Comment edited on '%{vulnerabilityName}'"
+msgid "SecurityConfiguration|Feature"
msgstr ""
-msgid "Security Reports|Create issue"
-msgstr "Criar issue"
+msgid "SecurityConfiguration|Feature documentation for %{featureName}"
+msgstr ""
-msgid "Security Reports|Dismiss vulnerability"
-msgstr "Ignorar vulnerabilidade"
+msgid "SecurityConfiguration|Not yet configured"
+msgstr ""
-msgid "Security Reports|Dismissed '%{vulnerabilityName}'"
+msgid "SecurityConfiguration|Secure features"
msgstr ""
-msgid "Security Reports|Dismissed '%{vulnerabilityName}'. Turn off the hide dismissed toggle to view."
+msgid "SecurityConfiguration|Status"
msgstr ""
-msgid "Security Reports|Either you don't have permission to view this dashboard or the dashboard has not been setup. Please check your permission settings with your administrator or check your dashboard configurations to proceed."
+msgid "SecurityReports|%{firstProject} and %{secondProject}"
msgstr ""
-msgid "Security Reports|Learn more about setting up your dashboard"
+msgid "SecurityReports|%{firstProject}, %{secondProject}, and %{rest}"
msgstr ""
-msgid "Security Reports|More info"
-msgstr "Mais informações"
+msgid "SecurityReports|Add a project to your dashboard"
+msgstr ""
-msgid "Security Reports|Oops, something doesn't seem right."
+msgid "SecurityReports|Add or remove projects from your dashboard"
msgstr ""
-msgid "Security Reports|Security reports can only be accessed by authorized users."
+msgid "SecurityReports|Add projects"
msgstr ""
-msgid "Security Reports|There was an error adding the comment."
+msgid "SecurityReports|Comment added to '%{vulnerabilityName}'"
msgstr ""
-msgid "Security Reports|There was an error creating the issue."
-msgstr "Ocorreu um erro ao criar a issue."
+msgid "SecurityReports|Comment deleted on '%{vulnerabilityName}'"
+msgstr ""
-msgid "Security Reports|There was an error creating the merge request."
+msgid "SecurityReports|Comment edited on '%{vulnerabilityName}'"
msgstr ""
-msgid "Security Reports|There was an error deleting the comment."
+msgid "SecurityReports|Create issue"
msgstr ""
-msgid "Security Reports|There was an error dismissing the vulnerabilities."
+msgid "SecurityReports|Dismiss Selected"
msgstr ""
-msgid "Security Reports|There was an error dismissing the vulnerability."
-msgstr "Ocorreu um erro ao ignorar a vulnerabilidade."
+msgid "SecurityReports|Dismiss vulnerability"
+msgstr ""
-msgid "Security Reports|There was an error reverting the dismissal."
-msgstr "Ocorreu um erro ao reverter o descarte."
+msgid "SecurityReports|Dismissed '%{vulnerabilityName}'"
+msgstr ""
-msgid "Security Reports|There was an error reverting this dismissal."
-msgstr "Ocorreu um erro ao reverter este descarte."
+msgid "SecurityReports|Dismissed '%{vulnerabilityName}'. Turn off the hide dismissed toggle to view."
+msgstr ""
-msgid "Security Reports|Undo dismiss"
+msgid "SecurityReports|Each vulnerability now has a unique page that can be directly linked to, shared, referenced, and tracked as the single source of truth. Vulnerability occurrences also persist across scanner runs, which improves tracking and visibility and reduces duplicates between scans."
msgstr ""
-msgid "Security Reports|You do not have sufficient permissions to access this report"
+msgid "SecurityReports|Edit dashboard"
msgstr ""
-msgid "Security Reports|You must sign in as an authorized user to see this report"
+msgid "SecurityReports|Either you don't have permission to view this dashboard or the dashboard has not been setup. Please check your permission settings with your administrator or check your dashboard configurations to proceed."
msgstr ""
-msgid "Security configuration help link"
+msgid "SecurityReports|Error fetching the vulnerability counts. Please check your network connection and try again."
msgstr ""
-msgid "Security dashboard"
+msgid "SecurityReports|Error fetching the vulnerability list. Please check your network connection and try again."
msgstr ""
-msgid "Security report is out of date. Please update your branch with the latest changes from the target branch (%{targetBranchName})"
+msgid "SecurityReports|False positive"
msgstr ""
-msgid "Security report is out of date. Run %{newPipelineLinkStart}a new pipeline%{newPipelineLinkEnd} for the target branch (%{targetBranchName})"
+msgid "SecurityReports|Hide dismissed"
msgstr ""
-msgid "SecurityConfiguration|Configured"
+msgid "SecurityReports|Introducing standalone vulnerabilities"
msgstr ""
-msgid "SecurityConfiguration|Feature"
+msgid "SecurityReports|Issue Created"
msgstr ""
-msgid "SecurityConfiguration|Feature documentation for %{featureName}"
+msgid "SecurityReports|Learn More"
msgstr ""
-msgid "SecurityConfiguration|Not yet configured"
+msgid "SecurityReports|Learn more about setting up your dashboard"
msgstr ""
-msgid "SecurityConfiguration|Secure features"
+msgid "SecurityReports|Load more vulnerabilities"
msgstr ""
-msgid "SecurityConfiguration|Status"
+msgid "SecurityReports|Monitor vulnerabilities in your code"
+msgstr ""
+
+msgid "SecurityReports|More info"
+msgstr ""
+
+msgid "SecurityReports|More information"
+msgstr ""
+
+msgid "SecurityReports|No vulnerabilities found for dashboard"
msgstr ""
-msgid "SecurityDashboard|%{firstProject} and %{secondProject}"
+msgid "SecurityReports|No vulnerabilities found for this group"
msgstr ""
-msgid "SecurityDashboard|%{firstProject}, %{secondProject}, and %{rest}"
+msgid "SecurityReports|No vulnerabilities found for this pipeline"
msgstr ""
-msgid "SecurityDashboard|Add a project to your dashboard"
+msgid "SecurityReports|No vulnerabilities found for this project"
msgstr ""
-msgid "SecurityDashboard|Add or remove projects from your dashboard"
+msgid "SecurityReports|Oops, something doesn't seem right."
msgstr ""
-msgid "SecurityDashboard|Add projects"
+msgid "SecurityReports|Pipeline %{pipelineLink} triggered %{timeago} by %{user}"
msgstr ""
-msgid "SecurityDashboard|Edit dashboard"
+msgid "SecurityReports|Project"
msgstr ""
-msgid "SecurityDashboard|Hide dismissed"
+msgid "SecurityReports|Projects added"
msgstr ""
-msgid "SecurityDashboard|Monitor vulnerabilities in your code"
-msgstr "Monitorar vulnerabilidades no seu código"
+msgid "SecurityReports|Remove project from dashboard"
+msgstr ""
+
+msgid "SecurityReports|Report type"
+msgstr ""
+
+msgid "SecurityReports|Return to dashboard"
+msgstr ""
+
+msgid "SecurityReports|Security Dashboard"
+msgstr ""
+
+msgid "SecurityReports|Security reports can only be accessed by authorized users."
+msgstr ""
-msgid "SecurityDashboard|More information"
+msgid "SecurityReports|Select a project to add by using the project search field above."
msgstr ""
-msgid "SecurityDashboard|No vulnerabilities found for dashboard"
+msgid "SecurityReports|Select a reason"
msgstr ""
-msgid "SecurityDashboard|Pipeline %{pipelineLink} triggered %{timeago} by %{user}"
+msgid "SecurityReports|Severity"
msgstr ""
-msgid "SecurityDashboard|Project"
+msgid "SecurityReports|Status"
msgstr ""
-msgid "SecurityDashboard|Projects added"
+msgid "SecurityReports|The security dashboard displays the latest security findings for projects you wish to monitor. Select \"Edit dashboard\" to add and remove projects."
msgstr ""
-msgid "SecurityDashboard|Remove project from dashboard"
+msgid "SecurityReports|The security dashboard displays the latest security report. Use it to find and fix vulnerabilities."
msgstr ""
-msgid "SecurityDashboard|Report type"
+msgid "SecurityReports|There was an error adding the comment."
msgstr ""
-msgid "SecurityDashboard|Return to dashboard"
+msgid "SecurityReports|There was an error creating the issue."
msgstr ""
-msgid "SecurityDashboard|Security Dashboard"
+msgid "SecurityReports|There was an error creating the merge request."
msgstr ""
-msgid "SecurityDashboard|Select a project to add by using the project search field above."
+msgid "SecurityReports|There was an error deleting the comment."
msgstr ""
-msgid "SecurityDashboard|Severity"
+msgid "SecurityReports|There was an error dismissing the vulnerabilities."
msgstr ""
-msgid "SecurityDashboard|Status"
+msgid "SecurityReports|There was an error dismissing the vulnerability."
msgstr ""
-msgid "SecurityDashboard|The security dashboard displays the latest security findings for projects you wish to monitor. Select \"Edit dashboard\" to add and remove projects."
+msgid "SecurityReports|There was an error reverting the dismissal."
msgstr ""
-msgid "SecurityDashboard|The security dashboard displays the latest security report. Use it to find and fix vulnerabilities."
+msgid "SecurityReports|There was an error reverting this dismissal."
msgstr ""
-msgid "SecurityDashboard|There was an error while generating the report."
+msgid "SecurityReports|There was an error while generating the report."
msgstr ""
-msgid "SecurityDashboard|Unable to add %{invalidProjects}"
+msgid "SecurityReports|Unable to add %{invalidProjects}"
+msgstr ""
+
+msgid "SecurityReports|Undo dismiss"
+msgstr ""
+
+msgid "SecurityReports|While it's rare to have no vulnerabilities for your group, it can happen. In any event, we ask that you double check your settings to make sure you've set up your dashboard correctly."
+msgstr ""
+
+msgid "SecurityReports|While it's rare to have no vulnerabilities for your pipeline, it can happen. In any event, we ask that you double check your settings to make sure all security scanning jobs have passed successfully."
+msgstr ""
+
+msgid "SecurityReports|While it's rare to have no vulnerabilities for your project, it can happen. In any event, we ask that you double check your settings to make sure you've set up your dashboard correctly."
+msgstr ""
+
+msgid "SecurityReports|While it's rare to have no vulnerabilities, it can happen. In any event, we ask that you please double check your settings to make sure you've set up your dashboard correctly."
+msgstr ""
+
+msgid "SecurityReports|Won't fix / Accept risk"
+msgstr ""
+
+msgid "SecurityReports|You do not have sufficient permissions to access this report"
+msgstr ""
+
+msgid "SecurityReports|You must sign in as an authorized user to see this report"
+msgstr ""
+
+msgid "SecurityReports|[No reason]"
msgstr ""
msgid "See GitLab's %{password_policy_guidelines}"
@@ -18065,6 +18489,9 @@ msgstr "Ver métricas"
msgid "See the affected projects in the GitLab admin panel"
msgstr "Veja os projetos afetados no painel de administração do GitLab"
+msgid "See what's new at GitLab"
+msgstr ""
+
msgid "Select"
msgstr "Selecionar"
@@ -18470,9 +18897,6 @@ msgstr "Configurar CI/CD"
msgid "Set up Jira Integration"
msgstr ""
-msgid "Set up Jira Integration illustration"
-msgstr ""
-
msgid "Set up a %{type} Runner automatically"
msgstr ""
@@ -18557,6 +18981,9 @@ msgstr ""
msgid "Severity: %{severity}"
msgstr ""
+msgid "Shards (%{shards})"
+msgstr ""
+
msgid "Shards to synchronize"
msgstr ""
@@ -18871,6 +19298,9 @@ msgstr ""
msgid "Snowplow"
msgstr ""
+msgid "Some child epics may be hidden due to applied filters"
+msgstr ""
+
msgid "Some email servers do not support overriding the email sender name. Enable this option to include the name of the author of the issue, merge request or comment in the email body instead."
msgstr ""
@@ -19012,6 +19442,9 @@ msgstr ""
msgid "Something went wrong while updating your list settings"
msgstr ""
+msgid "Something went wrong with your automatic subscription renewal"
+msgstr ""
+
msgid "Something went wrong, unable to add %{project} to dashboard"
msgstr "Algo deu errado, incapaz de adicionar %{project} ao dashboard"
@@ -19297,6 +19730,9 @@ msgstr ""
msgid "Stage removed"
msgstr ""
+msgid "Standard"
+msgstr ""
+
msgid "Star a label to make it a priority label. Order the prioritized labels to change their relative priority, by dragging."
msgstr "Coloque uma estrela em uma etiqueta para dar prioridade. Altere a ordem das etiquetas priorizadas arrastando-as para alterar sua prioridade."
@@ -19420,6 +19856,9 @@ msgstr "Regitre sua mensagem para ativar"
msgid "Static Application Security Testing (SAST)"
msgstr ""
+msgid "StaticSiteEditor|An error occurred while submitting your changes."
+msgstr ""
+
msgid "StaticSiteEditor|Branch could not be created."
msgstr ""
@@ -19429,18 +19868,30 @@ msgstr ""
msgid "StaticSiteEditor|Could not create merge request."
msgstr ""
+msgid "StaticSiteEditor|Incompatible file content"
+msgstr ""
+
msgid "StaticSiteEditor|Return to site"
msgstr ""
+msgid "StaticSiteEditor|Static site editor"
+msgstr ""
+
msgid "StaticSiteEditor|Success!"
msgstr ""
msgid "StaticSiteEditor|Summary of changes"
msgstr ""
+msgid "StaticSiteEditor|The Static Site Editor is currently configured to only edit Markdown content on pages generated from Middleman. Visit the documentation to learn more about configuring your site to use the Static Site Editor."
+msgstr ""
+
msgid "StaticSiteEditor|Update %{sourcePath} file"
msgstr ""
+msgid "StaticSiteEditor|View documentation"
+msgstr ""
+
msgid "StaticSiteEditor|View merge request"
msgstr ""
@@ -19498,6 +19949,12 @@ msgstr ""
msgid "StatusPage|Status page"
msgstr ""
+msgid "StatusPage|Status page URL"
+msgstr ""
+
+msgid "StatusPage|Status page frontend documentation"
+msgstr ""
+
msgid "StatusPage|To publish incidents to an external status page, GitLab will store a JSON file in your Amazon S3 account in a location accessible to your external status page service. Make sure to also set up %{docsLink}"
msgstr ""
@@ -19846,6 +20303,12 @@ msgstr "Informação de sincronização"
msgid "Synced"
msgstr ""
+msgid "Synchronization disabled"
+msgstr ""
+
+msgid "Synchronization of container repositories is disabled."
+msgstr ""
+
msgid "System"
msgstr "Sistema"
@@ -19999,6 +20462,9 @@ msgstr "Domínio da equipe"
msgid "Telephone number"
msgstr ""
+msgid "Telephone number (Optional)"
+msgstr ""
+
msgid "Template"
msgstr "Modelo"
@@ -20305,6 +20771,9 @@ msgstr ""
msgid "The license was successfully uploaded and is now active. You can see the details below."
msgstr ""
+msgid "The license was successfully uploaded and will be active from %{starts_at}. You can see the details below."
+msgstr ""
+
msgid "The maximum file size allowed is %{size}."
msgstr ""
@@ -20626,6 +21095,9 @@ msgstr ""
msgid "There was an error getting the epic participants."
msgstr ""
+msgid "There was an error importing the Jira project."
+msgstr ""
+
msgid "There was an error loading users activity calendar."
msgstr "Erro ao carregar calendário de atividades."
@@ -20641,7 +21113,7 @@ msgstr ""
msgid "There was an error resetting user pipeline minutes."
msgstr ""
-msgid "There was an error saving this Geo Node"
+msgid "There was an error saving this Geo Node."
msgstr ""
msgid "There was an error saving your changes."
@@ -20704,6 +21176,9 @@ msgstr ""
msgid "They can be managed using the %{link}."
msgstr "Eles podem ser gerenciados usando o %{link}."
+msgid "Third Party Advisory Link"
+msgstr ""
+
msgid "Third party offers"
msgstr "Ofertas de terceiros"
@@ -20740,6 +21215,9 @@ msgstr "Esse aplicativo foi criado por %{link_to_owner}."
msgid "This application will be able to:"
msgstr "Esse aplicativo será capaz de:"
+msgid "This attachment has been truncated to avoid exceeding the maximum allowed attachment size of 15MB. %{written_count} of %{issues_count} issues have been included. Consider re-exporting with a narrower selection of issues."
+msgstr ""
+
msgid "This block is self-referential"
msgstr ""
@@ -20881,6 +21359,12 @@ msgstr ""
msgid "This issue is confidential"
msgstr "Essa issue é confidencial"
+msgid "This issue is currently blocked by the following issues: %{issues}."
+msgstr ""
+
+msgid "This issue is in a child epic of the filtered epic"
+msgstr ""
+
msgid "This issue is locked."
msgstr "Essa issue está bloqueada."
@@ -21079,7 +21563,10 @@ msgstr "Esse usuário será o autor de todos os eventos no feed de atividades qu
msgid "This user will be the author of all events in the activity feed that are the result of an update, like new branches being created or new commits being pushed to existing branches. Upon creation or when reassigning you can only assign yourself to be the mirror user."
msgstr "Esse usuário será o autor de todos os eventos no feed de atividades que são o resultado de uma atualização, como novos branches sendo criados ou novos commits sendo enviados para os branches existentes. Na criação ou na redistribuição, você só pode se atribuir para ser o usuário espelho."
-msgid "This variable can not be masked"
+msgid "This variable can not be masked."
+msgstr ""
+
+msgid "This variable does not match the expected pattern."
msgstr ""
msgid "This will help us personalize your onboarding experience."
@@ -21109,10 +21596,10 @@ msgstr ""
msgid "ThreatMonitoring|Application firewall not detected"
msgstr ""
-msgid "ThreatMonitoring|Container Network Policy"
+msgid "ThreatMonitoring|Container Network Policies are not installed or have been disabled. To view this data, ensure your Network Policies are installed and enabled for your cluster."
msgstr ""
-msgid "ThreatMonitoring|Container NetworkPolicies are not installed or has been disabled. To view this data, ensure you NetworkPolicies are installed and enabled for your cluster."
+msgid "ThreatMonitoring|Container Network Policy"
msgstr ""
msgid "ThreatMonitoring|Container NetworkPolicies not detected"
@@ -21145,7 +21632,7 @@ msgstr ""
msgid "ThreatMonitoring|Something went wrong, unable to fetch statistics"
msgstr ""
-msgid "ThreatMonitoring|The firewall is not installed or has been disabled. To view this data, ensure you firewall is installed and enabled for your cluster."
+msgid "ThreatMonitoring|The firewall is not installed or has been disabled. To view this data, ensure the web application firewall is installed and enabled for your cluster."
msgstr ""
msgid "ThreatMonitoring|The graph below is an overview of traffic coming to your application as tracked by the Web Application Firewall (WAF). View the docs for instructions on how to access the WAF logs to see what type of malicious traffic is trying to access your app. The docs link is also accessible by clicking the \"?\" icon next to the title below."
@@ -21214,6 +21701,9 @@ msgstr ""
msgid "Time in seconds GitLab will wait for a response from the external service. When the service does not respond in time, access will be denied."
msgstr "Tempo em segundos o GitLab aguardará uma resposta do serviço externo. Quando o serviço não responder a tempo, o acesso será negado."
+msgid "Time of import: %{importTime}"
+msgstr ""
+
msgid "Time remaining"
msgstr "Tempo restante"
@@ -21398,7 +21888,7 @@ msgstr "Título"
msgid "Title:"
msgstr "Título:"
-msgid "Titles and Filenames"
+msgid "Titles and Descriptions"
msgstr ""
msgid "To"
@@ -21605,8 +22095,8 @@ msgstr ""
msgid "Too many projects enabled. You will need to manage them via the console or the API."
msgstr ""
-msgid "Topics"
-msgstr "Tópicos"
+msgid "Topics (optional)"
+msgstr ""
msgid "Total"
msgstr "Total"
@@ -21767,6 +22257,9 @@ msgstr "Tentar novamente?"
msgid "Try all GitLab has to offer for 30 days."
msgstr "Use tudo que o GitLab tem para oferecer por 30 dias."
+msgid "Try changing or removing filters."
+msgstr ""
+
msgid "Try to fork again"
msgstr "Tente realizar o fork novamente"
@@ -21860,6 +22353,9 @@ msgstr "Não foi possível conectar ao servidor Prometheus"
msgid "Unable to connect to server: %{error}"
msgstr ""
+msgid "Unable to connect to the Jira instance. Please check your Jira integration configuration."
+msgstr ""
+
msgid "Unable to convert Kubernetes logs encoding to UTF-8"
msgstr ""
@@ -22803,6 +23299,9 @@ msgstr ""
msgid "View issue"
msgstr ""
+msgid "View issues"
+msgstr ""
+
msgid "View it on GitLab"
msgstr "Ver no GitLab"
@@ -22980,6 +23479,15 @@ msgstr ""
msgid "VulnerabilityManagement|Resolved %{timeago} by %{user}"
msgstr ""
+msgid "VulnerabilityManagement|Something went wrong while trying to delete the comment. Please try again later."
+msgstr ""
+
+msgid "VulnerabilityManagement|Something went wrong while trying to retrieve the vulnerability history. Please try again later."
+msgstr ""
+
+msgid "VulnerabilityManagement|Something went wrong while trying to save the comment. Please try again later."
+msgstr ""
+
msgid "VulnerabilityManagement|Something went wrong, could not create an issue."
msgstr ""
@@ -23010,6 +23518,9 @@ msgstr ""
msgid "VulnerabilityStatusTypes|Resolved"
msgstr ""
+msgid "Vulnerability|%{scannerName} (version %{scannerVersion})"
+msgstr ""
+
msgid "Vulnerability|Class"
msgstr "Classe"
@@ -23043,7 +23554,10 @@ msgstr ""
msgid "Vulnerability|Project"
msgstr "Projeto"
-msgid "Vulnerability|Report Type"
+msgid "Vulnerability|Scanner Provider"
+msgstr ""
+
+msgid "Vulnerability|Scanner Type"
msgstr ""
msgid "Vulnerability|Severity"
@@ -23097,9 +23611,15 @@ msgstr ""
msgid "We sent you an email with reset password instructions"
msgstr ""
+msgid "We tried to automatically renew your %{strong}%{plan_name}%{strong_close} subscription for %{strong}%{namespace_name}%{strong_close} on %{expires_on} but something went wrong so your subscription was downgraded to the free plan. Don't worry, your data is safe. We suggest you check your payment method and get in touch with our support team (%{support_link}). They'll gladly help with your subscription renewal."
+msgstr ""
+
msgid "We want to be sure it is you, please confirm you are not a robot."
msgstr "Queremos ter certeza de que é você, confirme que você não é um robô."
+msgid "We will automatically renew your %{strong}%{plan_name}%{strong_close} subscription for %{strong}%{namespace_name}%{strong_close} on %{strong}%{expires_on}%{strong_close}. There's nothing that you need to do, we'll let you know when the renewal is complete. Need more seats, a higher plan or just want to review your payment method?"
+msgstr ""
+
msgid "We've found no vulnerabilities"
msgstr ""
@@ -23204,15 +23724,6 @@ msgstr ""
msgid "When:"
msgstr "Quando:"
-msgid "While it's rare to have no vulnerabilities for your group, it can happen. In any event, we ask that you double check your settings to make sure you've set up your dashboard correctly."
-msgstr ""
-
-msgid "While it's rare to have no vulnerabilities for your pipeline, it can happen. In any event, we ask that you double check your settings to make sure all security scanning jobs have passed successfully."
-msgstr ""
-
-msgid "While it's rare to have no vulnerabilities for your project, it can happen. In any event, we ask that you double check your settings to make sure you've set up your dashboard correctly."
-msgstr ""
-
msgid "While it's rare to have no vulnerabilities, it can happen. In any event, we ask that you please double check your settings to make sure you've set up your dashboard correctly."
msgstr ""
@@ -23243,9 +23754,6 @@ msgstr ""
msgid "Wiki"
msgstr "Wiki"
-msgid "Wiki pages"
-msgstr ""
-
msgid "Wiki was successfully updated."
msgstr "Wiki foi atualizado com sucesso."
@@ -23429,6 +23937,9 @@ msgstr "Sim ou não"
msgid "Yes, add it"
msgstr "Sim, adicionar"
+msgid "Yes, close issue"
+msgstr ""
+
msgid "Yes, let me map Google Code users to full names or GitLab users."
msgstr "Sim, deixe-me associar usuários do Google Code para nomes completos ou usuários do GitLab."
@@ -23624,6 +24135,9 @@ msgstr "Você não pode escrever nesta instância somente-leitura do GitLab."
msgid "You could not create a new trigger."
msgstr ""
+msgid "You didn't renew your %{strong}%{plan_name}%{strong_close} subscription for %{strong}%{namespace_name}%{strong_close} so it was downgraded to the free plan."
+msgstr ""
+
msgid "You didn't renew your %{strong}%{plan_name}%{strong_close} subscription so it was downgraded to the GitLab Core Plan."
msgstr ""
@@ -23690,6 +24204,9 @@ msgstr ""
msgid "You have declined the invitation to join %{label}."
msgstr ""
+msgid "You have imported from this project %{numberOfPreviousImportsForProject} times before. Each new import will create duplicate issues."
+msgstr ""
+
msgid "You have no permissions"
msgstr "Você não tem permissão"
@@ -23771,6 +24288,9 @@ msgstr ""
msgid "You need to upload a Google Takeout archive."
msgstr ""
+msgid "You reached %{usage_in_percent} of %{namespace_name}'s capacity (%{used_storage} of %{storage_limit})"
+msgstr ""
+
msgid "You tried to fork %{link_to_the_project} but it failed for the following reason:"
msgstr "Você tentou criar o fork %{link_to_the_project}, mas ocorreu uma falha pelo seguinte motivo:"
@@ -23855,9 +24375,21 @@ msgstr ""
msgid "YouTube"
msgstr "YouTube"
+msgid "Your %{strong}%{plan_name}%{strong_close} subscription for %{strong}%{namespace_name}%{strong_close} will expire on %{strong}%{expires_on}%{strong_close}. After that, you will not to be able to create issues or merge requests as well as many other features."
+msgstr ""
+
msgid "Your %{strong}%{plan_name}%{strong_close} subscription will expire on %{strong}%{expires_on}%{strong_close}. After that, you will not to be able to create issues or merge requests as well as many other features."
msgstr ""
+msgid "Your CSV export has started. It will be emailed to %{email} when complete."
+msgstr ""
+
+msgid "Your CSV export of %{issues_count} from project %{project_link} has been added to this email as an attachment."
+msgstr ""
+
+msgid "Your CSV export of %{written_count} from project %{project_name} (%{project_url}) has been added to this email as an attachment."
+msgstr ""
+
msgid "Your Commit Email will be used for web based operations, such as edits and merges."
msgstr ""
@@ -23954,6 +24486,9 @@ msgstr "Seu comentário não pôde ser enviado! Por favor, verifique sua conexã
msgid "Your comment could not be updated! Please check your network connection and try again."
msgstr "Seu comentário não pôde ser atualizado! Por favor, verifique sua conexão de rede e tente novamente."
+msgid "Your comment will be discarded."
+msgstr ""
+
msgid "Your custom stage '%{title}' was created"
msgstr ""
@@ -23993,6 +24528,9 @@ msgstr ""
msgid "Your new personal access token has been created."
msgstr ""
+msgid "Your new project access token has been created."
+msgstr ""
+
msgid "Your password isn't required to view this page. If a password or any other personal details are requested, please contact your administrator to report abuse."
msgstr ""
@@ -24011,12 +24549,18 @@ msgstr "Seus projetos"
msgid "Your request for access has been queued for review."
msgstr ""
+msgid "Your search didn't match any commits."
+msgstr ""
+
msgid "Your subscription expired!"
msgstr ""
msgid "Your subscription has been downgraded"
msgstr ""
+msgid "Your subscription will automatically renew in %{remaining_days}"
+msgstr ""
+
msgid "Your subscription will expire in %{remaining_days}"
msgstr ""
@@ -24055,7 +24599,7 @@ msgstr "atrás"
msgid "allowed to fail"
msgstr ""
-msgid "already being used for another group or project milestone."
+msgid "already being used for another group or project %{timebox_name}."
msgstr ""
msgid "already has a \"created\" issue link"
@@ -24133,6 +24677,9 @@ msgstr "%{linkStartTag}Saiba mais sobre a Verificação de Dependência %{linkEn
msgid "ciReport|%{linkStartTag}Learn more about SAST %{linkEndTag}"
msgstr "%{linkStartTag}Saiba mais sobre o SAST %{linkEndTag}"
+msgid "ciReport|%{linkStartTag}Learn more about Secret Scanning %{linkEndTag}"
+msgstr ""
+
msgid "ciReport|%{linkStartTag}Learn more about codequality reports %{linkEndTag}"
msgstr ""
@@ -24296,6 +24843,12 @@ msgstr ""
msgid "ciReport|SAST"
msgstr "SAST"
+msgid "ciReport|Secret scanning"
+msgstr ""
+
+msgid "ciReport|Secret scanning detects secrets and credentials vulnerabilities in your source code."
+msgstr ""
+
msgid "ciReport|Security scanning"
msgstr "Verificação de segurança"
@@ -24493,6 +25046,9 @@ msgstr ""
msgid "group"
msgstr ""
+msgid "groups"
+msgstr ""
+
msgid "has already been linked to another vulnerability"
msgstr ""
@@ -24656,9 +25212,6 @@ msgstr[1] "merge requests"
msgid "merged %{time_ago}"
msgstr ""
-msgid "milestone should belong either to a project or a group."
-msgstr ""
-
msgid "missing"
msgstr ""
@@ -24995,6 +25548,9 @@ msgstr "novo merge request"
msgid "no contributions"
msgstr "nenhuma contribuição"
+msgid "no expiration"
+msgstr ""
+
msgid "no one can merge"
msgstr ""
@@ -25047,6 +25603,9 @@ msgstr ""
msgid "pending removal"
msgstr ""
+msgid "per day"
+msgstr ""
+
msgid "pipeline"
msgstr ""
@@ -25075,6 +25634,12 @@ msgstr[1] "projetos"
msgid "project avatar"
msgstr ""
+msgid "projects"
+msgstr ""
+
+msgid "push to your repository, create pipelines, create issues or add comments. To reduce storage capacity, delete unused repositories, artifacts, wikis, issues, and pipelines."
+msgstr ""
+
msgid "quick actions"
msgstr "ações rápidas"
@@ -25221,11 +25786,6 @@ msgstr ""
msgid "this document"
msgstr "este documento"
-msgid "thread resolved"
-msgid_plural "threads resolved"
-msgstr[0] ""
-msgstr[1] ""
-
msgid "to help your contributors communicate effectively!"
msgstr "para ajudar seus contribuintes à se comunicar de maneira eficaz!"
@@ -25280,6 +25840,9 @@ msgstr "ver no GitLab"
msgid "view the blob"
msgstr ""
+msgid "vulnerability|Add a comment"
+msgstr ""
+
msgid "vulnerability|Add a comment or reason for dismissal"
msgstr ""
diff --git a/locale/pt_PT/gitlab.po b/locale/pt_PT/gitlab.po
index 3437d06c656..6d8adc88d26 100644
--- a/locale/pt_PT/gitlab.po
+++ b/locale/pt_PT/gitlab.po
@@ -12,7 +12,7 @@ msgstr ""
"X-Crowdin-Project: gitlab-ee\n"
"X-Crowdin-Language: pt-PT\n"
"X-Crowdin-File: /master/locale/gitlab.pot\n"
-"PO-Revision-Date: 2020-04-15 00:26\n"
+"PO-Revision-Date: 2020-05-05 21:35\n"
msgid " %{start} to %{end}"
msgstr ""
@@ -212,6 +212,11 @@ msgid_plural "%d tags"
msgstr[0] ""
msgstr[1] ""
+msgid "%d unresolved thread"
+msgid_plural "%d unresolved threads"
+msgstr[0] ""
+msgstr[1] ""
+
msgid "%d vulnerability dismissed"
msgid_plural "%d vulnerabilities dismissed"
msgstr[0] ""
@@ -371,6 +376,9 @@ msgstr ""
msgid "%{mrText}, this issue will be closed automatically."
msgstr "%{mrText}, este problema será encerrado automaticamente."
+msgid "%{namespace_name} is now read-only. You cannot: %{base_message}"
+msgstr ""
+
msgid "%{name} contained %{resultsString}"
msgstr "%{name} continha %{resultsString}"
@@ -383,6 +391,11 @@ msgstr ""
msgid "%{name}'s avatar"
msgstr "Avatar de %{name}"
+msgid "%{no_of_days} day"
+msgid_plural "%{no_of_days} days"
+msgstr[0] ""
+msgstr[1] ""
+
msgid "%{number_commits_behind} commits behind %{default_branch}, %{number_commits_ahead} commits ahead"
msgstr "%{number_commits_behind} envios atrás %{default_branch}, %{number_commits_ahead} envios à frente"
@@ -506,6 +519,9 @@ msgstr[1] "%{text} %{files} ficheiros"
msgid "%{text} is available"
msgstr "%{text} está disponível"
+msgid "%{timebox_name} should belong either to a project or a group."
+msgstr ""
+
msgid "%{title} %{operator} %{threshold}"
msgstr "%{title} %{operator} %{threshold}"
@@ -781,12 +797,18 @@ msgstr "<code>\"johnsmith@example.com\": \"johnsm...@example.com\"</code> adicio
msgid "<code>\"johnsmith@example.com\": \"johnsmith@example.com\"</code> will add \"By <a href=\"#\">johnsmith@example.com</a>\" to all issues and comments originally created by johnsmith@example.com. By default, the email address or username is masked to ensure the user's privacy. Use this option if you want to show the full email address."
msgstr "<code>\"johnsmith@example.com\": \"johnsmith@example.com\"</code> adicionará \"Por <a href=\"#\">johnsmith@example.com</a>\" a todos os problemas e comentários, originalmente, criados por johnsmith@example.com. Por padrão, o endereço de email ou nome de utilizador é ocultado para garantir a privacidade do utilizador. Use esta opção se quiser mostrar o endereço de email completo."
+msgid "<namespace / project>"
+msgstr ""
+
msgid "<no name set>"
msgstr "<nenhum nome definido>"
msgid "<no scopes selected>"
msgstr "<nenhum escopo selecionado>"
+msgid "<project name>"
+msgstr ""
+
msgid "<strong>%{group_name}</strong> group members"
msgstr "Membros do grupo <strong>%{group_name}</strong>"
@@ -820,6 +842,9 @@ msgstr "Uma conta Vamos Criptografar será configurada para esta instalação no
msgid "A basic page and serverless function that uses AWS Lambda, AWS API Gateway, and GitLab Pages"
msgstr ""
+msgid "A complete DevOps platform"
+msgstr ""
+
msgid "A default branch cannot be chosen for an empty project."
msgstr "Um ramo padrão não pode ser escolhido para um projeto vazio."
@@ -1036,6 +1061,12 @@ msgstr ""
msgid "AccessTokens|reset it"
msgstr "redefini-lo"
+msgid "AccessibilityReport|Accessibility report artifact not found"
+msgstr ""
+
+msgid "AccessibilityReport|Failed to retrieve accessibility report"
+msgstr ""
+
msgid "AccessibilityReport|Learn More"
msgstr ""
@@ -1045,6 +1076,9 @@ msgstr ""
msgid "AccessibilityReport|New"
msgstr ""
+msgid "AccessibilityReport|The accessibility scanning found an error of the following type: %{code}"
+msgstr ""
+
msgid "Account"
msgstr "Conta"
@@ -1287,6 +1321,9 @@ msgstr ""
msgid "Added at"
msgstr "Adicionado em"
+msgid "Added for this merge request"
+msgstr ""
+
msgid "Added in this version"
msgstr ""
@@ -1658,6 +1695,72 @@ msgid_plural "Alerts"
msgstr[0] "Alerta"
msgstr[1] "Alertas"
+msgid "Alert Details"
+msgstr ""
+
+msgid "AlertManagement|Acknowledged"
+msgstr ""
+
+msgid "AlertManagement|Alert"
+msgstr ""
+
+msgid "AlertManagement|Authorize external service"
+msgstr ""
+
+msgid "AlertManagement|Display alerts from all your monitoring tools directly within GitLab. Streamline the investigation of your alerts and the escalation of alerts to incidents."
+msgstr ""
+
+msgid "AlertManagement|End time"
+msgstr ""
+
+msgid "AlertManagement|End time:"
+msgstr ""
+
+msgid "AlertManagement|Events"
+msgstr ""
+
+msgid "AlertManagement|Events:"
+msgstr ""
+
+msgid "AlertManagement|Full Alert Details"
+msgstr ""
+
+msgid "AlertManagement|More information"
+msgstr ""
+
+msgid "AlertManagement|No alerts available to display. If you think you're seeing this message in error, refresh the page."
+msgstr ""
+
+msgid "AlertManagement|No alerts to display."
+msgstr ""
+
+msgid "AlertManagement|Overview"
+msgstr ""
+
+msgid "AlertManagement|Resolved"
+msgstr ""
+
+msgid "AlertManagement|Severity"
+msgstr ""
+
+msgid "AlertManagement|Start time"
+msgstr ""
+
+msgid "AlertManagement|Start time:"
+msgstr ""
+
+msgid "AlertManagement|Status"
+msgstr ""
+
+msgid "AlertManagement|Surface alerts in GitLab"
+msgstr ""
+
+msgid "AlertManagement|There was an error displaying the alerts. Confirm your endpoint's configuration details to ensure alerts appear."
+msgstr ""
+
+msgid "AlertManagement|Triggered"
+msgstr ""
+
msgid "AlertService|%{linkStart}Learn more%{linkEnd} about configuring this endpoint to receive alerts."
msgstr ""
@@ -1727,6 +1830,9 @@ msgstr "Todos os projetos"
msgid "All security scans are enabled because %{linkStart}Auto DevOps%{linkEnd} is enabled on this project"
msgstr ""
+msgid "All threads resolved"
+msgstr ""
+
msgid "All users"
msgstr "Todos os utilizadores"
@@ -1745,6 +1851,9 @@ msgstr "Permitir que proprietários de grupos gerenciem as definições relacion
msgid "Allow only the selected protocols to be used for Git access."
msgstr "Permitir apenas, que os protocolos selecionados sejam usados para acessar o Git."
+msgid "Allow owners to manage default branch protection per group"
+msgstr ""
+
msgid "Allow owners to manually add users outside of LDAP"
msgstr ""
@@ -1853,6 +1962,9 @@ msgstr "Ocorreu um erro ao buscar os aprovadores para as novas regras."
msgid "An error occurred fetching the dropdown data."
msgstr "Ocorreu um erro ao buscar os dados suspensos."
+msgid "An error occurred fetching the project authors."
+msgstr ""
+
msgid "An error occurred previewing the blob"
msgstr "Ocorreu um erro ao pré-visualizar o blob"
@@ -1934,6 +2046,9 @@ msgstr "Ocorreu um erro ao buscar projetos de conclusão automática."
msgid "An error occurred while fetching sidebar data"
msgstr "Ocorreu um erro ao buscar os dados da barra lateral"
+msgid "An error occurred while fetching terraform reports."
+msgstr ""
+
msgid "An error occurred while fetching the Service Desk address."
msgstr "Ocorreu um erro ao buscar o endereço da Central de Serviços."
@@ -2171,6 +2286,9 @@ msgstr "Verificação de anti-spam"
msgid "Any"
msgstr "Qualquer"
+msgid "Any Author"
+msgstr ""
+
msgid "Any Label"
msgstr "Qualquer Rótulo"
@@ -2386,12 +2504,12 @@ msgstr "Tens a certeza de que desejas arquivar este projeto?"
msgid "Are you sure that you want to unarchive this project?"
msgstr "Tens a certeza de que desejas desarquivar este projeto?"
-msgid "Are you sure you want to cancel creating this comment?"
-msgstr "Tens a certeza de que desejas cancelar a criação deste comentário?"
-
msgid "Are you sure you want to cancel editing this comment?"
msgstr "Tens a certeza de que desejas cancelar a edição deste comentário?"
+msgid "Are you sure you want to close this blocked issue?"
+msgstr ""
+
msgid "Are you sure you want to delete %{name}?"
msgstr ""
@@ -2419,6 +2537,9 @@ msgstr ""
msgid "Are you sure you want to deploy this environment?"
msgstr ""
+msgid "Are you sure you want to discard this comment?"
+msgstr ""
+
msgid "Are you sure you want to erase this build?"
msgstr "Tens a certeza de que desejas apagar esta compilação?"
@@ -2595,6 +2716,9 @@ msgstr ""
msgid "At least one approval from a code owner is required to change files matching the respective CODEOWNER rules."
msgstr "Pelo menos uma aprovação de um código proprietário é necessário para alterar ficheiros que coincidem com as respetivas regras de CODEOWNER."
+msgid "At least one logging option is required to be enabled"
+msgstr ""
+
msgid "At least one of group_id or project_id must be specified"
msgstr ""
@@ -2633,6 +2757,27 @@ msgstr ""
msgid "AuditEvents|Target"
msgstr ""
+msgid "AuditLogs|(removed)"
+msgstr ""
+
+msgid "AuditLogs|Action"
+msgstr ""
+
+msgid "AuditLogs|Author"
+msgstr ""
+
+msgid "AuditLogs|Date"
+msgstr ""
+
+msgid "AuditLogs|IP Address"
+msgstr ""
+
+msgid "AuditLogs|Object"
+msgstr ""
+
+msgid "AuditLogs|Target"
+msgstr ""
+
msgid "Aug"
msgstr "ago"
@@ -2915,9 +3060,6 @@ msgstr "URL raiz do Bamboo como https://bamboo.example.com"
msgid "BambooService|You must set up automatic revision labeling and a repository trigger in Bamboo."
msgstr "Deves configurar uma etiqueta de revisão automático e um disparador de repositório no Bamboo."
-msgid "Batch operations"
-msgstr ""
-
msgid "BatchComments|Delete all pending comments"
msgstr "Apagar todos os comentários pendentes"
@@ -3047,6 +3189,9 @@ msgstr "Ao começar com o conjunto de listas padrão, estarás no caminho certo
msgid "Boards"
msgstr "Painéis"
+msgid "Boards and Board Lists"
+msgstr ""
+
msgid "Boards|Collapse"
msgstr "Colapsar"
@@ -3266,6 +3411,9 @@ msgstr "Comprar EE"
msgid "Buy GitLab Enterprise Edition"
msgstr "Comprar o GitLab Enterprise Edition"
+msgid "Buy more Pipeline minutes"
+msgstr ""
+
msgid "By %{user_name}"
msgstr "Por %{user_name}"
@@ -3386,6 +3534,9 @@ msgstr "Não é possível remover membros do grupo sem conta gerenciada do grupo
msgid "Can't scan the code?"
msgstr "Não consegues digitalizar o código?"
+msgid "Can't update snippet: %{err}"
+msgstr ""
+
msgid "Canary"
msgstr ""
@@ -3554,6 +3705,9 @@ msgstr ""
msgid "Changing group path can have unintended side effects."
msgstr ""
+msgid "Charts"
+msgstr ""
+
msgid "Charts can't be displayed as the request for data has timed out. %{documentationLink}"
msgstr ""
@@ -3590,9 +3744,6 @@ msgstr ""
msgid "ChatMessage|and [%{count} more](%{pipeline_failed_jobs_url})"
msgstr ""
-msgid "ChatMessage|failed"
-msgstr ""
-
msgid "ChatMessage|has failed"
msgstr ""
@@ -3608,9 +3759,6 @@ msgstr ""
msgid "ChatMessage|in %{project_link}"
msgstr ""
-msgid "ChatMessage|passed"
-msgstr ""
-
msgid "Check again"
msgstr "Verifica Novamente"
@@ -4061,9 +4209,6 @@ msgstr "Limpa peso."
msgid "Click any <strong>project name</strong> in the project list below to navigate to the project milestone."
msgstr "Clica em qualquer <strong>nome de projeto</strong> na lista a seguir para navegar para o objetivo do projeto."
-msgid "Click here"
-msgstr "Clica aqui"
-
msgid "Click the <strong>Download</strong> button and wait for downloading to complete."
msgstr "Clica no botão <strong>Transferir</strong> e aguarda a finalização do mesmo."
@@ -4451,6 +4596,12 @@ msgstr "A buscar projetos"
msgid "ClusterIntegration|Fetching zones"
msgstr "A buscar zonas"
+msgid "ClusterIntegration|Fluentd"
+msgstr ""
+
+msgid "ClusterIntegration|Fluentd is an open source data collector, which lets you unify the data collection and consumption for a better use and understanding of data. It requires at least one of the following logs to be successfully installed."
+msgstr ""
+
msgid "ClusterIntegration|GitLab Integration"
msgstr "Integração GitLab"
@@ -4757,6 +4908,15 @@ msgstr "Falha no pedido para iniciar a instalação"
msgid "ClusterIntegration|Request to begin uninstalling failed"
msgstr "Falha no pedido para iniciar a desinstalação"
+msgid "ClusterIntegration|SIEM Hostname"
+msgstr ""
+
+msgid "ClusterIntegration|SIEM Port"
+msgstr ""
+
+msgid "ClusterIntegration|SIEM Protocol"
+msgstr ""
+
msgid "ClusterIntegration|Save changes"
msgstr "Guardar alterações"
@@ -4853,6 +5013,12 @@ msgstr "Selecionar zona"
msgid "ClusterIntegration|Select zone to choose machine type"
msgstr "Seleciona a zona para escolher o tipo de máquina"
+msgid "ClusterIntegration|Send Cilium Logs"
+msgstr ""
+
+msgid "ClusterIntegration|Send ModSecurity Logs"
+msgstr ""
+
msgid "ClusterIntegration|Service Token"
msgstr "Token de Serviço"
@@ -5281,18 +5447,33 @@ msgstr ""
msgid "Compliance framework (optional)"
msgstr ""
+msgid "ComplianceFramework|GDPR"
+msgstr ""
+
msgid "ComplianceFramework|GDPR - General Data Protection Regulation"
msgstr ""
+msgid "ComplianceFramework|HIPAA"
+msgstr ""
+
msgid "ComplianceFramework|HIPAA - Health Insurance Portability and Accountability Act"
msgstr ""
+msgid "ComplianceFramework|PCI-DSS"
+msgstr ""
+
msgid "ComplianceFramework|PCI-DSS - Payment Card Industry-Data Security Standard"
msgstr ""
+msgid "ComplianceFramework|SOC 2"
+msgstr ""
+
msgid "ComplianceFramework|SOC 2 - Service Organization Control 2"
msgstr ""
+msgid "ComplianceFramework|SOX"
+msgstr ""
+
msgid "ComplianceFramework|SOX - Sarbanes-Oxley"
msgstr ""
@@ -5440,9 +5621,15 @@ msgstr ""
msgid "Container repositories sync capacity"
msgstr ""
+msgid "ContainerRegistry| Please visit the %{linkStart}administration settings%{linkEnd} to enable this feature."
+msgstr ""
+
msgid "ContainerRegistry|%{imageName} tags"
msgstr ""
+msgid "ContainerRegistry|%{title} was successfully scheduled for deletion"
+msgstr ""
+
msgid "ContainerRegistry|Automatically remove extra images that aren't designed to be kept."
msgstr ""
@@ -5455,6 +5642,9 @@ msgstr ""
msgid "ContainerRegistry|Container Registry"
msgstr ""
+msgid "ContainerRegistry|Container Registry tag expiration and retention policy is disabled"
+msgstr ""
+
msgid "ContainerRegistry|Copy build command"
msgstr ""
@@ -5464,18 +5654,12 @@ msgstr ""
msgid "ContainerRegistry|Copy push command"
msgstr ""
-msgid "ContainerRegistry|Currently, the Container Registry tag expiration feature is not available for projects created before GitLab version 12.8. For updates and more information, visit Issue %{linkStart}#196124%{linkEnd}"
-msgstr ""
-
msgid "ContainerRegistry|Docker connection error"
msgstr ""
msgid "ContainerRegistry|Docker tag expiration policy is %{toggleStatus}"
msgstr ""
-msgid "ContainerRegistry|Docker tags with names matching this regex pattern will expire:"
-msgstr ""
-
msgid "ContainerRegistry|Edit Settings"
msgstr ""
@@ -5497,9 +5681,6 @@ msgstr ""
msgid "ContainerRegistry|Image ID"
msgstr ""
-msgid "ContainerRegistry|Image deleted successfully"
-msgstr ""
-
msgid "ContainerRegistry|Keep and protect the images that matter most."
msgstr ""
@@ -5515,12 +5696,24 @@ msgstr ""
msgid "ContainerRegistry|Number of tags to retain:"
msgstr ""
+msgid "ContainerRegistry|Please contact your administrator."
+msgstr ""
+
msgid "ContainerRegistry|Push an image"
msgstr ""
msgid "ContainerRegistry|Quick Start"
msgstr ""
+msgid "ContainerRegistry|Regular expressions such as %{codeStart}.*-test%{codeEnd} or %{codeStart}dev-.*%{codeEnd} are supported"
+msgstr ""
+
+msgid "ContainerRegistry|Regular expressions such as %{codeStart}.*-test%{codeEnd} or %{codeStart}dev-.*%{codeEnd} are supported. To select all tags, use %{codeStart}.*%{codeEnd}"
+msgstr ""
+
+msgid "ContainerRegistry|Remember to run %{docLinkStart}garbage collection%{docLinkEnd} to remove the stale data from storage."
+msgstr ""
+
msgid "ContainerRegistry|Remove repository"
msgstr ""
@@ -5535,22 +5728,22 @@ msgstr[1] ""
msgid "ContainerRegistry|Retention policy has been Enabled"
msgstr ""
-msgid "ContainerRegistry|Something went wrong while deleting the image."
+msgid "ContainerRegistry|Something went wrong while fetching the expiration policy."
msgstr ""
-msgid "ContainerRegistry|Something went wrong while deleting the tag."
+msgid "ContainerRegistry|Something went wrong while fetching the repository list."
msgstr ""
-msgid "ContainerRegistry|Something went wrong while deleting the tags."
+msgid "ContainerRegistry|Something went wrong while fetching the tags list."
msgstr ""
-msgid "ContainerRegistry|Something went wrong while fetching the expiration policy."
+msgid "ContainerRegistry|Something went wrong while marking the tag for deletion."
msgstr ""
-msgid "ContainerRegistry|Something went wrong while fetching the packages list."
+msgid "ContainerRegistry|Something went wrong while marking the tags for deletion."
msgstr ""
-msgid "ContainerRegistry|Something went wrong while fetching the tags list."
+msgid "ContainerRegistry|Something went wrong while scheduling %{title} for deletion. Please try again."
msgstr ""
msgid "ContainerRegistry|Something went wrong while updating the expiration policy."
@@ -5559,16 +5752,25 @@ msgstr ""
msgid "ContainerRegistry|Tag"
msgstr ""
-msgid "ContainerRegistry|Tag deleted successfully"
-msgstr ""
-
msgid "ContainerRegistry|Tag expiration policy"
msgstr ""
msgid "ContainerRegistry|Tag expiration policy is designed to:"
msgstr ""
-msgid "ContainerRegistry|Tags deleted successfully"
+msgid "ContainerRegistry|Tag successfully marked for deletion."
+msgstr ""
+
+msgid "ContainerRegistry|Tags successfully marked for deletion."
+msgstr ""
+
+msgid "ContainerRegistry|Tags with names matching this regex pattern will %{italicStart}be preserved:%{italicEnd}"
+msgstr ""
+
+msgid "ContainerRegistry|Tags with names matching this regex pattern will %{italicStart}expire:%{italicEnd}"
+msgstr ""
+
+msgid "ContainerRegistry|The Container Registry tag expiration and retention policies for this project have not been enabled."
msgstr ""
msgid "ContainerRegistry|The last tag related to this image was recently removed. This empty image and any associated data will be automatically removed as part of the regular Garbage Collection process. If you have any questions, contact your administrator."
@@ -5589,16 +5791,16 @@ msgstr ""
msgid "ContainerRegistry|There are no container images stored for this project"
msgstr ""
-msgid "ContainerRegistry|This Registry contains deleted image tag data. Remember to run %{docLinkStart}garbage collection%{docLinkEnd} to remove the stale data from storage."
+msgid "ContainerRegistry|There was an error during the deletion of this image repository, please try again."
msgstr ""
msgid "ContainerRegistry|This image has no active tags"
msgstr ""
-msgid "ContainerRegistry|We are having trouble connecting to Docker, which could be due to an issue with your project name or path. %{docLinkStart}More Information%{docLinkEnd}"
+msgid "ContainerRegistry|This image repository is scheduled for deletion"
msgstr ""
-msgid "ContainerRegistry|Wildcards such as %{codeStart}.*-stable%{codeEnd} or %{codeStart}production/.*%{codeEnd} are supported. To select all tags, use %{codeStart}.*%{codeEnd}"
+msgid "ContainerRegistry|We are having trouble connecting to Docker, which could be due to an issue with your project name or path. %{docLinkStart}More Information%{docLinkEnd}"
msgstr ""
msgid "ContainerRegistry|With the Container Registry, every project can have its own space to store its Docker images. %{docLinkStart}More Information%{docLinkEnd}"
@@ -5853,7 +6055,7 @@ msgstr ""
msgid "Could not delete chat nickname %{chat_name}."
msgstr ""
-msgid "Could not find design"
+msgid "Could not find design."
msgstr ""
msgid "Could not remove the trigger."
@@ -5868,6 +6070,9 @@ msgstr ""
msgid "Could not revoke personal access token %{personal_access_token_name}."
msgstr ""
+msgid "Could not revoke project access token %{project_access_token_name}."
+msgstr ""
+
msgid "Could not save group ID"
msgstr ""
@@ -5925,6 +6130,9 @@ msgstr ""
msgid "Create a new branch"
msgstr ""
+msgid "Create a new deploy key for this project"
+msgstr ""
+
msgid "Create a new file as there are no files yet. Afterwards, you'll be able to commit your changes."
msgstr ""
@@ -6030,6 +6238,9 @@ msgstr "Criar etiqueta do projeto"
msgid "Create requirement"
msgstr ""
+msgid "Create snippet"
+msgstr ""
+
msgid "Create wildcard: %{searchTerm}"
msgstr ""
@@ -6051,6 +6262,9 @@ msgstr ""
msgid "Created"
msgstr ""
+msgid "Created %{timestamp}"
+msgstr ""
+
msgid "Created At"
msgstr ""
@@ -6407,6 +6621,9 @@ msgstr ""
msgid "CycleAnalytics|stage dropdown"
msgstr ""
+msgid "DAG"
+msgstr ""
+
msgid "DNS"
msgstr ""
@@ -6773,6 +6990,9 @@ msgstr ""
msgid "Deploy key was successfully updated."
msgstr ""
+msgid "Deploy keys allow read-only or read-write (if enabled) access to your repository. Deploy keys can be used for CI, staging or production servers. You can create a deploy key or add an existing one."
+msgstr ""
+
msgid "Deploy progress not found. To see pods, ensure your environment matches %{linkStart}deploy board criteria%{linkEnd}."
msgstr ""
@@ -6926,6 +7146,9 @@ msgstr ""
msgid "Deploying to"
msgstr ""
+msgid "Deployment Frequency"
+msgstr ""
+
msgid "Deployment|API"
msgstr ""
@@ -7444,9 +7667,6 @@ msgstr ""
msgid "Edit environment"
msgstr ""
-msgid "Edit epic description"
-msgstr ""
-
msgid "Edit file"
msgstr ""
@@ -8068,9 +8288,6 @@ msgstr ""
msgid "Epics"
msgstr ""
-msgid "Epics (Ultimate / Gold license only)"
-msgstr ""
-
msgid "Epics Roadmap"
msgstr ""
@@ -8080,6 +8297,9 @@ msgstr ""
msgid "Epics let you manage your portfolio of projects more efficiently and with less effort"
msgstr ""
+msgid "Epics, Issues, and Merge Requests"
+msgstr ""
+
msgid "Epics|Add an epic"
msgstr ""
@@ -8131,6 +8351,9 @@ msgstr ""
msgid "Epics|Something went wrong while fetching group epics."
msgstr ""
+msgid "Epics|Something went wrong while moving item."
+msgstr ""
+
msgid "Epics|Something went wrong while ordering item."
msgstr ""
@@ -8185,6 +8408,9 @@ msgstr ""
msgid "Error fetching network graph."
msgstr ""
+msgid "Error fetching payload data."
+msgstr ""
+
msgid "Error fetching projects"
msgstr ""
@@ -8194,9 +8420,6 @@ msgstr ""
msgid "Error fetching the dependency list. Please check your network connection and try again."
msgstr ""
-msgid "Error fetching usage ping data."
-msgstr ""
-
msgid "Error loading branch data. Please try again."
msgstr ""
@@ -8338,6 +8561,9 @@ msgstr ""
msgid "Errors"
msgstr ""
+msgid "Errors:"
+msgstr ""
+
msgid "Estimated"
msgstr ""
@@ -8542,12 +8768,18 @@ msgstr ""
msgid "Export as CSV"
msgstr ""
+msgid "Export group"
+msgstr ""
+
msgid "Export issues"
msgstr ""
msgid "Export project"
msgstr ""
+msgid "Export this group with all related data to a new GitLab instance. Once complete, you can import the data file from the \"New Group\" page."
+msgstr ""
+
msgid "Export this project with all its related data in order to move your project to a new GitLab instance. Once the export is finished, you can import the file from the \"New Project\" page."
msgstr ""
@@ -8617,9 +8849,6 @@ msgstr ""
msgid "Failed Jobs"
msgstr ""
-msgid "Failed create wiki"
-msgstr ""
-
msgid "Failed to add a Zoom meeting"
msgstr ""
@@ -8659,6 +8888,9 @@ msgstr ""
msgid "Failed to create resources"
msgstr ""
+msgid "Failed to create wiki"
+msgstr ""
+
msgid "Failed to delete board. Please try again."
msgstr ""
@@ -8668,7 +8900,7 @@ msgstr ""
msgid "Failed to enqueue the rebase operation, possibly due to a long-lived transaction. Try again later."
msgstr ""
-msgid "Failed to find import label for jira import."
+msgid "Failed to find import label for Jira import."
msgstr ""
msgid "Failed to get ref."
@@ -8692,6 +8924,12 @@ msgstr ""
msgid "Failed to load groups & users."
msgstr ""
+msgid "Failed to load labels. Please try again."
+msgstr ""
+
+msgid "Failed to load milestones. Please try again."
+msgstr ""
+
msgid "Failed to load related branches"
msgstr ""
@@ -9067,7 +9305,10 @@ msgstr ""
msgid "Filter by milestone name"
msgstr "Filtrar por nome de objetivo"
-msgid "Filter by name..."
+msgid "Filter by name"
+msgstr ""
+
+msgid "Filter by status"
msgstr ""
msgid "Filter by two-factor authentication"
@@ -9079,12 +9320,18 @@ msgstr ""
msgid "Filter projects"
msgstr ""
+msgid "Filter results"
+msgstr ""
+
msgid "Filter results by group"
msgstr ""
msgid "Filter results by project"
msgstr ""
+msgid "Filter results..."
+msgstr ""
+
msgid "Filter your projects by name"
msgstr ""
@@ -9373,10 +9620,10 @@ msgstr ""
msgid "GeoNodes|Checksummed"
msgstr ""
-msgid "GeoNodes|Container repositories"
+msgid "GeoNodes|Consult Geo troubleshooting information"
msgstr ""
-msgid "GeoNodes|Data is out of date from %{timeago}"
+msgid "GeoNodes|Container repositories"
msgstr ""
msgid "GeoNodes|Data replication lag"
@@ -9418,6 +9665,9 @@ msgstr ""
msgid "GeoNodes|Last event ID seen from primary"
msgstr ""
+msgid "GeoNodes|Learn more about Geo node statuses"
+msgstr ""
+
msgid "GeoNodes|Loading nodes"
msgstr ""
@@ -9433,6 +9683,9 @@ msgstr ""
msgid "GeoNodes|Node was successfully removed."
msgstr ""
+msgid "GeoNodes|Node's status was updated %{timeAgo}."
+msgstr ""
+
msgid "GeoNodes|Not checksummed"
msgstr ""
@@ -9463,7 +9716,10 @@ msgstr ""
msgid "GeoNodes|Repository verification progress"
msgstr ""
-msgid "GeoNodes|Selective"
+msgid "GeoNodes|Selective (%{syncLabel})"
+msgstr ""
+
+msgid "GeoNodes|Selective synchronization"
msgstr ""
msgid "GeoNodes|Something went wrong while changing node status"
@@ -9490,6 +9746,9 @@ msgstr ""
msgid "GeoNodes|Unverified"
msgstr ""
+msgid "GeoNodes|Updated %{timeAgo}"
+msgstr ""
+
msgid "GeoNodes|Used slots"
msgstr ""
@@ -9529,16 +9788,16 @@ msgstr ""
msgid "Geo|All"
msgstr ""
-msgid "Geo|All projects"
+msgid "Geo|All %{replicable_type}"
msgstr ""
-msgid "Geo|All projects are being scheduled for re-sync"
+msgid "Geo|All projects"
msgstr ""
-msgid "Geo|All projects are being scheduled for re-verify"
+msgid "Geo|All projects are being scheduled for resync"
msgstr ""
-msgid "Geo|Batch operations"
+msgid "Geo|All projects are being scheduled for reverify"
msgstr ""
msgid "Geo|Could not remove tracking entry for an existing project."
@@ -9550,9 +9809,15 @@ msgstr ""
msgid "Geo|Failed"
msgstr ""
+msgid "Geo|Filter by status"
+msgstr ""
+
msgid "Geo|Geo Status"
msgstr "Estado Geo"
+msgid "Geo|In progress"
+msgstr ""
+
msgid "Geo|In sync"
msgstr ""
@@ -9577,9 +9842,6 @@ msgstr ""
msgid "Geo|Not synced yet"
msgstr ""
-msgid "Geo|Pending"
-msgstr ""
-
msgid "Geo|Pending synchronization"
msgstr ""
@@ -9616,7 +9878,7 @@ msgstr ""
msgid "Geo|Resync"
msgstr ""
-msgid "Geo|Resync all projects"
+msgid "Geo|Resync all"
msgstr ""
msgid "Geo|Retry count"
@@ -9625,7 +9887,7 @@ msgstr ""
msgid "Geo|Reverify"
msgstr ""
-msgid "Geo|Reverify all projects"
+msgid "Geo|Reverify all"
msgstr ""
msgid "Geo|Status"
@@ -9967,9 +10229,6 @@ msgstr ""
msgid "Go to file"
msgstr ""
-msgid "Go to file (MRs only)"
-msgstr ""
-
msgid "Go to file permalink (while viewing a file)"
msgstr ""
@@ -10414,6 +10673,9 @@ msgstr ""
msgid "GroupSettings|Disable group mentions"
msgstr ""
+msgid "GroupSettings|Export group"
+msgstr ""
+
msgid "GroupSettings|If the parent group's visibility is lower than the group current visibility, visibility levels for subgroups and projects will be changed to match the new parent group's visibility."
msgstr ""
@@ -10492,6 +10754,9 @@ msgstr ""
msgid "Groups (%{count})"
msgstr "Grupos (%{count})"
+msgid "Groups (%{groups})"
+msgstr ""
+
msgid "Groups can also be nested by creating %{subgroup_docs_link_start}subgroups%{subgroup_docs_link_end}."
msgstr ""
@@ -10757,15 +11022,12 @@ msgstr ""
msgid "ID:"
msgstr ""
-msgid "IDE|Allow live previews of JavaScript projects in the Web IDE using CodeSandbox client side evaluation."
+msgid "IDE|Allow live previews of JavaScript projects in the Web IDE using CodeSandbox Live Preview."
msgstr ""
msgid "IDE|Back"
msgstr ""
-msgid "IDE|Client side evaluation"
-msgstr ""
-
msgid "IDE|Commit"
msgstr ""
@@ -10829,6 +11091,9 @@ msgstr ""
msgid "If any job surpasses this timeout threshold, it will be marked as failed. Human readable time input language is accepted like \"1 hour\". Values without specification represent seconds."
msgstr ""
+msgid "If blank, set allowable lifetime to %{instance_level_policy_in_words}, as defined by the instance admin. Once set, existing tokens for users in this group may be revoked."
+msgstr ""
+
msgid "If checked, group owners can manage LDAP group links and LDAP member overrides"
msgstr ""
@@ -10868,6 +11133,9 @@ msgstr ""
msgid "If you lose your recovery codes you can generate new ones, invalidating all previous codes."
msgstr ""
+msgid "If you reach 100%% storage capacity, you will not be able to: %{base_message}"
+msgstr ""
+
msgid "If your HTTP repository is not publicly accessible, add your credentials."
msgstr ""
@@ -10979,6 +11247,9 @@ msgstr ""
msgid "Import repository"
msgstr ""
+msgid "Import started by: %{importInitiator}"
+msgstr ""
+
msgid "Import tasks"
msgstr ""
@@ -11045,6 +11316,9 @@ msgstr ""
msgid "In order to tailor your experience with GitLab we<br>would like to know a bit more about you."
msgstr ""
+msgid "In progress"
+msgstr ""
+
msgid "In the next step, you'll be able to select the projects you want to import."
msgstr ""
@@ -11392,15 +11666,15 @@ msgstr ""
msgid "Issues"
msgstr ""
-msgid "Issues / Merge Requests"
-msgstr ""
-
msgid "Issues Analytics"
msgstr ""
msgid "Issues Rate Limits"
msgstr ""
+msgid "Issues and Merge Requests"
+msgstr ""
+
msgid "Issues can be bugs, tasks or ideas to be discussed. Also, issues are searchable and filterable."
msgstr ""
@@ -11422,10 +11696,10 @@ msgstr ""
msgid "IssuesAnalytics|Avg/Month:"
msgstr ""
-msgid "IssuesAnalytics|Issues created"
+msgid "IssuesAnalytics|Issues opened"
msgstr ""
-msgid "IssuesAnalytics|Issues created per month"
+msgid "IssuesAnalytics|Issues opened per month"
msgstr ""
msgid "IssuesAnalytics|Last 12 months"
@@ -11479,6 +11753,9 @@ msgstr ""
msgid "Jira integration not configured."
msgstr ""
+msgid "Jira project: %{importProject}"
+msgstr ""
+
msgid "JiraService|Events for %{noteable_model_name} are disabled."
msgstr ""
@@ -11868,6 +12145,9 @@ msgstr ""
msgid "Lead"
msgstr ""
+msgid "Lead Time"
+msgstr ""
+
msgid "Learn GitLab"
msgstr ""
@@ -11880,6 +12160,9 @@ msgstr ""
msgid "Learn how to %{no_packages_link_start}publish and share your packages%{no_packages_link_end} with GitLab."
msgstr ""
+msgid "Learn how to enable synchronization"
+msgstr ""
+
msgid "Learn more"
msgstr ""
@@ -12211,9 +12494,6 @@ msgstr ""
msgid "Live preview"
msgstr ""
-msgid "Load more vulnerabilities"
-msgstr ""
-
msgid "Loading"
msgstr ""
@@ -12364,9 +12644,6 @@ msgstr ""
msgid "Manage"
msgstr ""
-msgid "Manage Git repositories with fine-grained access controls that keep your code secure. Perform code reviews and enhance collaboration with merge requests. Each project can also have an issue tracker and a wiki."
-msgstr ""
-
msgid "Manage Web IDE features"
msgstr ""
@@ -12397,6 +12674,9 @@ msgstr ""
msgid "Manage your license"
msgstr ""
+msgid "Managed Account"
+msgstr ""
+
msgid "Manifest"
msgstr ""
@@ -12661,6 +12941,9 @@ msgstr ""
msgid "Merge Requests in Review"
msgstr ""
+msgid "Merge automatically (%{strategy})"
+msgstr ""
+
msgid "Merge commit message"
msgstr ""
@@ -12814,6 +13097,12 @@ msgstr ""
msgid "Merged branches are being deleted. This can take some time depending on the number of branches. Please refresh the page to see changes."
msgstr ""
+msgid "Merged this merge request."
+msgstr ""
+
+msgid "Merges this merge request immediately."
+msgstr ""
+
msgid "Merges this merge request when the pipeline succeeds."
msgstr ""
@@ -12921,12 +13210,15 @@ msgid_plural "Metrics|Edit metrics"
msgstr[0] ""
msgstr[1] ""
-msgid "Metrics|Environment"
+msgid "Metrics|Expand panel"
msgstr ""
msgid "Metrics|For grouping similar metrics"
msgstr ""
+msgid "Metrics|Go back (Esc)"
+msgstr ""
+
msgid "Metrics|Invalid time range, please verify."
msgstr ""
@@ -12957,9 +13249,6 @@ msgstr ""
msgid "Metrics|Refresh dashboard"
msgstr ""
-msgid "Metrics|Show last"
-msgstr ""
-
msgid "Metrics|There was an error creating the dashboard."
msgstr ""
@@ -13038,6 +13327,9 @@ msgstr ""
msgid "Microsoft Azure"
msgstr ""
+msgid "Middleman project with Static Site Editor support"
+msgstr ""
+
msgid "Migrated %{success_count}/%{total_count} files."
msgstr ""
@@ -13483,10 +13775,10 @@ msgstr ""
msgid "Next"
msgstr ""
-msgid "Next file in diff (MRs only)"
+msgid "Next file in diff"
msgstr ""
-msgid "Next unresolved discussion (MRs only)"
+msgid "Next unresolved discussion"
msgstr ""
msgid "Nickname"
@@ -13546,6 +13838,9 @@ msgstr ""
msgid "No changes between %{ref_start}%{source_branch}%{ref_end} and %{ref_start}%{target_branch}%{ref_end}"
msgstr ""
+msgid "No child epics match applied filters"
+msgstr ""
+
msgid "No connection could be made to a Gitaly Server, please check your logs!"
msgstr ""
@@ -13684,15 +13979,6 @@ msgstr ""
msgid "No thanks, don't show this again"
msgstr ""
-msgid "No vulnerabilities found for this group"
-msgstr ""
-
-msgid "No vulnerabilities found for this pipeline"
-msgstr ""
-
-msgid "No vulnerabilities found for this project"
-msgstr ""
-
msgid "No vulnerabilities present"
msgstr ""
@@ -13816,6 +14102,9 @@ msgstr ""
msgid "Nothing to preview."
msgstr ""
+msgid "Nothing to synchronize"
+msgstr ""
+
msgid "Notification events"
msgstr ""
@@ -13987,6 +14276,9 @@ msgstr ""
msgid "Once removed, the fork relationship cannot be restored and you will no longer be able to send merge requests to the source."
msgstr ""
+msgid "Once the exported file is ready you can download it from this page."
+msgstr ""
+
msgid "Once the exported file is ready, you will receive a notification email with a download link, or you can download it from this page."
msgstr ""
@@ -14088,9 +14380,6 @@ msgstr ""
msgid "Open sidebar"
msgstr ""
-msgid "Open source software to collaborate on code"
-msgstr ""
-
msgid "Open: %{openIssuesCount}"
msgstr ""
@@ -14193,6 +14482,9 @@ msgstr ""
msgid "OutdatedBrowser|You can provide feedback %{feedback_link_start}on this issue%{feedback_link_end} or via your usual support channels."
msgstr ""
+msgid "Overridden"
+msgstr ""
+
msgid "Overview"
msgstr ""
@@ -14244,6 +14536,9 @@ msgstr ""
msgid "PackageRegistry|Conan Command"
msgstr ""
+msgid "PackageRegistry|Copy .pypirc content"
+msgstr ""
+
msgid "PackageRegistry|Copy Conan Command"
msgstr ""
@@ -14265,6 +14560,9 @@ msgstr ""
msgid "PackageRegistry|Copy NuGet Setup Command"
msgstr ""
+msgid "PackageRegistry|Copy Pip command"
+msgstr ""
+
msgid "PackageRegistry|Copy and paste this inside your %{codeStart}pom.xml%{codeEnd} %{codeStart}dependencies%{codeEnd} block."
msgstr ""
@@ -14298,6 +14596,12 @@ msgstr ""
msgid "PackageRegistry|For more information on the NuGet registry, %{linkStart}see the documentation%{linkEnd}."
msgstr ""
+msgid "PackageRegistry|For more information on the PyPi registry, %{linkStart}see the documentation%{linkEnd}."
+msgstr ""
+
+msgid "PackageRegistry|If you haven't already done so, you will need to add the below to your %{codeStart}.pypirc%{codeEnd} file."
+msgstr ""
+
msgid "PackageRegistry|If you haven't already done so, you will need to add the below to your %{codeStart}pom.xml%{codeEnd} file."
msgstr ""
@@ -14307,6 +14611,9 @@ msgstr ""
msgid "PackageRegistry|Learn how to %{noPackagesLinkStart}publish and share your packages%{noPackagesLinkEnd} with GitLab."
msgstr ""
+msgid "PackageRegistry|Manually Published"
+msgstr ""
+
msgid "PackageRegistry|Maven"
msgstr ""
@@ -14325,12 +14632,18 @@ msgstr ""
msgid "PackageRegistry|NuGet Command"
msgstr ""
+msgid "PackageRegistry|Pip Command"
+msgstr ""
+
msgid "PackageRegistry|Pipeline %{linkStart}%{linkEnd} triggered %{timestamp} by %{author}"
msgstr ""
msgid "PackageRegistry|Published to the repository at %{timestamp}"
msgstr ""
+msgid "PackageRegistry|PyPi"
+msgstr ""
+
msgid "PackageRegistry|Registry Setup"
msgstr ""
@@ -14367,6 +14680,9 @@ msgstr ""
msgid "PackageRegistry|npm"
msgstr ""
+msgid "PackageRegistry|published by %{author}"
+msgstr ""
+
msgid "PackageRegistry|yarn"
msgstr ""
@@ -14382,6 +14698,9 @@ msgstr ""
msgid "PackageType|NuGet"
msgstr ""
+msgid "PackageType|PyPi"
+msgstr ""
+
msgid "Packages"
msgstr ""
@@ -14475,6 +14794,9 @@ msgstr ""
msgid "Past due"
msgstr ""
+msgid "Paste a machine public key here. Read more about how to generate it"
+msgstr ""
+
msgid "Paste a machine public key here. Read more about how to generate it %{link_start}here%{link_end}"
msgstr ""
@@ -14490,9 +14812,6 @@ msgstr ""
msgid "Path"
msgstr ""
-msgid "Path, transfer, remove"
-msgstr ""
-
msgid "Path:"
msgstr ""
@@ -14523,7 +14842,7 @@ msgstr ""
msgid "Percentage"
msgstr ""
-msgid "Perform advanced options such as changing path, transferring, or removing the group."
+msgid "Perform advanced options such as changing path, transferring, exporting, or removing the group."
msgstr ""
msgid "Perform common operations on GitLab project"
@@ -14604,9 +14923,6 @@ msgstr ""
msgid "Pipeline minutes quota"
msgstr ""
-msgid "Pipeline quota"
-msgstr ""
-
msgid "Pipeline subscriptions"
msgstr ""
@@ -14616,6 +14932,9 @@ msgstr ""
msgid "Pipeline: %{status}"
msgstr ""
+msgid "PipelineCharts|CI / CD Analytics"
+msgstr ""
+
msgid "PipelineCharts|Failed:"
msgstr ""
@@ -14694,15 +15013,6 @@ msgstr ""
msgid "Pipelines settings for '%{project_name}' were successfully updated."
msgstr ""
-msgid "Pipelines| to purchase more minutes."
-msgstr ""
-
-msgid "Pipelines|%{namespace_name} has exceeded its pipeline minutes quota."
-msgstr ""
-
-msgid "Pipelines|%{namespace_name} has less than %{notification_level}%% of CI minutes available."
-msgstr ""
-
msgid "Pipelines|API"
msgstr ""
@@ -14724,10 +15034,13 @@ msgstr ""
msgid "Pipelines|Get started with Pipelines"
msgstr ""
-msgid "Pipelines|Loading Pipelines"
+msgid "Pipelines|Group %{namespace_name} has %{percentage}%% or less Shared Runner Pipeline minutes remaining. Once it runs out, no new jobs or pipelines in its projects will run."
msgstr ""
-msgid "Pipelines|Pipelines will not run anymore on shared Runners."
+msgid "Pipelines|Group %{namespace_name} has exceeded its pipeline minutes quota. Unless you buy additional pipeline minutes, no new jobs or pipelines in its projects will run."
+msgstr ""
+
+msgid "Pipelines|Loading Pipelines"
msgstr ""
msgid "Pipelines|Project cache successfully reset."
@@ -14958,6 +15271,9 @@ msgstr ""
msgid "Please select"
msgstr ""
+msgid "Please select a Jira project"
+msgstr ""
+
msgid "Please select a country"
msgstr ""
@@ -15003,6 +15319,9 @@ msgstr ""
msgid "Please wait while we import the repository for you. Refresh at will."
msgstr ""
+msgid "Plugins directory is deprecated and will be removed in 14.0. Please move this file into /file_hooks directory."
+msgstr ""
+
msgid "Pod does not exist"
msgstr ""
@@ -15120,7 +15439,7 @@ msgstr ""
msgid "Prevent users from changing their profile name"
msgstr ""
-msgid "Prevent users from modifing merge request approvers list"
+msgid "Prevent users from modifying merge request approvers list"
msgstr ""
msgid "Prevent users from performing write operations on GitLab while performing maintenance."
@@ -15141,10 +15460,10 @@ msgstr ""
msgid "Previous Artifacts"
msgstr ""
-msgid "Previous file in diff (MRs only)"
+msgid "Previous file in diff"
msgstr ""
-msgid "Previous unresolved discussion (MRs only)"
+msgid "Previous unresolved discussion"
msgstr ""
msgid "Primary"
@@ -15177,6 +15496,9 @@ msgstr ""
msgid "Private profile"
msgstr ""
+msgid "Private projects Minutes cost factor"
+msgstr ""
+
msgid "Private projects can be created in your personal namespace with:"
msgstr ""
@@ -16005,6 +16327,9 @@ msgstr ""
msgid "ProjectSettings|With GitLab Pages you can host your static websites on GitLab"
msgstr ""
+msgid "ProjectSettings|With Metrics Dashboard you can visualize this project performance metrics"
+msgstr ""
+
msgid "ProjectTemplates|.NET Core"
msgstr ""
@@ -16068,6 +16393,9 @@ msgstr ""
msgid "ProjectTemplates|Spring"
msgstr ""
+msgid "ProjectTemplates|Static Site Editor/Middleman"
+msgstr ""
+
msgid "ProjectTemplates|iOS (Swift)"
msgstr ""
@@ -16287,13 +16615,13 @@ msgstr ""
msgid "PrometheusService|Prometheus is being automatically managed on your clusters"
msgstr ""
-msgid "PrometheusService|These metrics will only be monitored after your first deployment to an environment"
+msgid "PrometheusService|Select the Active checkbox to override the Auto Configuration with custom settings. If unchecked, Auto Configuration settings are used."
msgstr ""
-msgid "PrometheusService|Time-series monitoring service"
+msgid "PrometheusService|These metrics will only be monitored after your first deployment to an environment"
msgstr ""
-msgid "PrometheusService|To enable manual configuration, uninstall Prometheus from your clusters"
+msgid "PrometheusService|Time-series monitoring service"
msgstr ""
msgid "PrometheusService|To enable the installation of Prometheus on your clusters, deactivate the manual configuration below"
@@ -16479,6 +16807,9 @@ msgstr ""
msgid "Protip:"
msgstr ""
+msgid "Protocol"
+msgstr ""
+
msgid "Provider"
msgstr ""
@@ -16503,6 +16834,9 @@ msgstr ""
msgid "Public pipelines"
msgstr ""
+msgid "Public projects Minutes cost factor"
+msgstr ""
+
msgid "Pull"
msgstr ""
@@ -16703,6 +17037,9 @@ msgid_plural "Refreshing in %d seconds to show the updated status..."
msgstr[0] ""
msgstr[1] ""
+msgid "Regenerate export"
+msgstr ""
+
msgid "Regenerate instance ID"
msgstr ""
@@ -17080,9 +17417,29 @@ msgstr ""
msgid "Reports|%{combinedString} and %{resolvedString}"
msgstr ""
+msgid "Reports|Accessibility scanning detected %d issue for the source branch only"
+msgid_plural "Reports|Accessibility scanning detected %d issues for the source branch only"
+msgstr[0] ""
+msgstr[1] ""
+
+msgid "Reports|Accessibility scanning detected no issues for the source branch only"
+msgstr ""
+
+msgid "Reports|Accessibility scanning failed loading results"
+msgstr ""
+
+msgid "Reports|Accessibility scanning results are being parsed"
+msgstr ""
+
msgid "Reports|Actions"
msgstr ""
+msgid "Reports|An error occured while loading report"
+msgstr ""
+
+msgid "Reports|An error occurred while loading %{name} results"
+msgstr ""
+
msgid "Reports|Class"
msgstr ""
@@ -17188,7 +17545,7 @@ msgstr ""
msgid "Requested %{time_ago}"
msgstr ""
-msgid "Requested design version does not exist"
+msgid "Requested design version does not exist."
msgstr ""
msgid "Requested states are invalid"
@@ -17372,6 +17729,9 @@ msgstr ""
msgid "Resync"
msgstr ""
+msgid "Resync all"
+msgstr ""
+
msgid "Resync all %{replicableType}"
msgstr ""
@@ -17440,6 +17800,9 @@ msgstr ""
msgid "Revoked personal access token %{personal_access_token_name}!"
msgstr ""
+msgid "Revoked project access token %{project_access_token_name}!"
+msgstr ""
+
msgid "RightSidebar|adding a"
msgstr ""
@@ -17488,6 +17851,9 @@ msgstr ""
msgid "Runner token"
msgstr ""
+msgid "Runner tokens"
+msgstr ""
+
msgid "Runner was not updated."
msgstr ""
@@ -17542,6 +17908,9 @@ msgstr ""
msgid "SAML SSO for %{group_name}"
msgstr ""
+msgid "SAML discovery tokens"
+msgstr ""
+
msgid "SAML for %{group_name}"
msgstr ""
@@ -17626,12 +17995,18 @@ msgstr ""
msgid "Scheduled"
msgstr ""
+msgid "Scheduled to merge this merge request (%{strategy})."
+msgstr ""
+
msgid "Scheduled to merge this merge request when the pipeline succeeds."
msgstr ""
msgid "Schedules"
msgstr ""
+msgid "Schedules to merge this merge request (%{strategy})."
+msgstr ""
+
msgid "Scheduling"
msgstr ""
@@ -17689,6 +18064,9 @@ msgstr ""
msgid "Search branches and tags"
msgstr ""
+msgid "Search by author"
+msgstr ""
+
msgid "Search files"
msgstr ""
@@ -17834,11 +18212,6 @@ msgid_plural "SearchResults|snippets"
msgstr[0] ""
msgstr[1] ""
-msgid "SearchResults|snippet result"
-msgid_plural "SearchResults|snippet results"
-msgstr[0] ""
-msgstr[1] ""
-
msgid "SearchResults|user"
msgid_plural "SearchResults|users"
msgstr[0] ""
@@ -17849,6 +18222,9 @@ msgid_plural "SearchResults|wiki results"
msgstr[0] ""
msgstr[1] ""
+msgid "Searching by both author and message is currently not supported."
+msgstr ""
+
msgid "Seat Link"
msgstr ""
@@ -17876,184 +18252,232 @@ msgstr ""
msgid "Security Dashboard"
msgstr ""
-msgid "Security Dashboard|Error fetching the vulnerability counts. Please check your network connection and try again."
+msgid "Security configuration help link"
+msgstr ""
+
+msgid "Security dashboard"
msgstr ""
-msgid "Security Dashboard|Error fetching the vulnerability list. Please check your network connection and try again."
+msgid "Security report is out of date. Please update your branch with the latest changes from the target branch (%{targetBranchName})"
msgstr ""
-msgid "Security Dashboard|Issue Created"
+msgid "Security report is out of date. Run %{newPipelineLinkStart}a new pipeline%{newPipelineLinkEnd} for the target branch (%{targetBranchName})"
msgstr ""
-msgid "Security Reports|Comment added to '%{vulnerabilityName}'"
+msgid "SecurityConfiguration|Configured"
msgstr ""
-msgid "Security Reports|Comment deleted on '%{vulnerabilityName}'"
+msgid "SecurityConfiguration|Feature"
msgstr ""
-msgid "Security Reports|Comment edited on '%{vulnerabilityName}'"
+msgid "SecurityConfiguration|Feature documentation for %{featureName}"
msgstr ""
-msgid "Security Reports|Create issue"
+msgid "SecurityConfiguration|Not yet configured"
msgstr ""
-msgid "Security Reports|Dismiss vulnerability"
+msgid "SecurityConfiguration|Secure features"
msgstr ""
-msgid "Security Reports|Dismissed '%{vulnerabilityName}'"
+msgid "SecurityConfiguration|Status"
msgstr ""
-msgid "Security Reports|Dismissed '%{vulnerabilityName}'. Turn off the hide dismissed toggle to view."
+msgid "SecurityReports|%{firstProject} and %{secondProject}"
msgstr ""
-msgid "Security Reports|Either you don't have permission to view this dashboard or the dashboard has not been setup. Please check your permission settings with your administrator or check your dashboard configurations to proceed."
+msgid "SecurityReports|%{firstProject}, %{secondProject}, and %{rest}"
msgstr ""
-msgid "Security Reports|Learn more about setting up your dashboard"
+msgid "SecurityReports|Add a project to your dashboard"
msgstr ""
-msgid "Security Reports|More info"
+msgid "SecurityReports|Add or remove projects from your dashboard"
msgstr ""
-msgid "Security Reports|Oops, something doesn't seem right."
+msgid "SecurityReports|Add projects"
msgstr ""
-msgid "Security Reports|Security reports can only be accessed by authorized users."
+msgid "SecurityReports|Comment added to '%{vulnerabilityName}'"
msgstr ""
-msgid "Security Reports|There was an error adding the comment."
+msgid "SecurityReports|Comment deleted on '%{vulnerabilityName}'"
msgstr ""
-msgid "Security Reports|There was an error creating the issue."
+msgid "SecurityReports|Comment edited on '%{vulnerabilityName}'"
msgstr ""
-msgid "Security Reports|There was an error creating the merge request."
+msgid "SecurityReports|Create issue"
msgstr ""
-msgid "Security Reports|There was an error deleting the comment."
+msgid "SecurityReports|Dismiss Selected"
msgstr ""
-msgid "Security Reports|There was an error dismissing the vulnerabilities."
+msgid "SecurityReports|Dismiss vulnerability"
msgstr ""
-msgid "Security Reports|There was an error dismissing the vulnerability."
+msgid "SecurityReports|Dismissed '%{vulnerabilityName}'"
msgstr ""
-msgid "Security Reports|There was an error reverting the dismissal."
+msgid "SecurityReports|Dismissed '%{vulnerabilityName}'. Turn off the hide dismissed toggle to view."
msgstr ""
-msgid "Security Reports|There was an error reverting this dismissal."
+msgid "SecurityReports|Each vulnerability now has a unique page that can be directly linked to, shared, referenced, and tracked as the single source of truth. Vulnerability occurrences also persist across scanner runs, which improves tracking and visibility and reduces duplicates between scans."
msgstr ""
-msgid "Security Reports|Undo dismiss"
+msgid "SecurityReports|Edit dashboard"
msgstr ""
-msgid "Security Reports|You do not have sufficient permissions to access this report"
+msgid "SecurityReports|Either you don't have permission to view this dashboard or the dashboard has not been setup. Please check your permission settings with your administrator or check your dashboard configurations to proceed."
msgstr ""
-msgid "Security Reports|You must sign in as an authorized user to see this report"
+msgid "SecurityReports|Error fetching the vulnerability counts. Please check your network connection and try again."
msgstr ""
-msgid "Security configuration help link"
+msgid "SecurityReports|Error fetching the vulnerability list. Please check your network connection and try again."
msgstr ""
-msgid "Security dashboard"
+msgid "SecurityReports|False positive"
msgstr ""
-msgid "Security report is out of date. Please update your branch with the latest changes from the target branch (%{targetBranchName})"
+msgid "SecurityReports|Hide dismissed"
msgstr ""
-msgid "Security report is out of date. Run %{newPipelineLinkStart}a new pipeline%{newPipelineLinkEnd} for the target branch (%{targetBranchName})"
+msgid "SecurityReports|Introducing standalone vulnerabilities"
msgstr ""
-msgid "SecurityConfiguration|Configured"
+msgid "SecurityReports|Issue Created"
msgstr ""
-msgid "SecurityConfiguration|Feature"
+msgid "SecurityReports|Learn More"
msgstr ""
-msgid "SecurityConfiguration|Feature documentation for %{featureName}"
+msgid "SecurityReports|Learn more about setting up your dashboard"
msgstr ""
-msgid "SecurityConfiguration|Not yet configured"
+msgid "SecurityReports|Load more vulnerabilities"
msgstr ""
-msgid "SecurityConfiguration|Secure features"
+msgid "SecurityReports|Monitor vulnerabilities in your code"
msgstr ""
-msgid "SecurityConfiguration|Status"
+msgid "SecurityReports|More info"
+msgstr ""
+
+msgid "SecurityReports|More information"
+msgstr ""
+
+msgid "SecurityReports|No vulnerabilities found for dashboard"
+msgstr ""
+
+msgid "SecurityReports|No vulnerabilities found for this group"
+msgstr ""
+
+msgid "SecurityReports|No vulnerabilities found for this pipeline"
+msgstr ""
+
+msgid "SecurityReports|No vulnerabilities found for this project"
+msgstr ""
+
+msgid "SecurityReports|Oops, something doesn't seem right."
+msgstr ""
+
+msgid "SecurityReports|Pipeline %{pipelineLink} triggered %{timeago} by %{user}"
+msgstr ""
+
+msgid "SecurityReports|Project"
+msgstr ""
+
+msgid "SecurityReports|Projects added"
+msgstr ""
+
+msgid "SecurityReports|Remove project from dashboard"
+msgstr ""
+
+msgid "SecurityReports|Report type"
+msgstr ""
+
+msgid "SecurityReports|Return to dashboard"
msgstr ""
-msgid "SecurityDashboard|%{firstProject} and %{secondProject}"
+msgid "SecurityReports|Security Dashboard"
msgstr ""
-msgid "SecurityDashboard|%{firstProject}, %{secondProject}, and %{rest}"
+msgid "SecurityReports|Security reports can only be accessed by authorized users."
msgstr ""
-msgid "SecurityDashboard|Add a project to your dashboard"
+msgid "SecurityReports|Select a project to add by using the project search field above."
msgstr ""
-msgid "SecurityDashboard|Add or remove projects from your dashboard"
+msgid "SecurityReports|Select a reason"
msgstr ""
-msgid "SecurityDashboard|Add projects"
+msgid "SecurityReports|Severity"
msgstr ""
-msgid "SecurityDashboard|Edit dashboard"
+msgid "SecurityReports|Status"
msgstr ""
-msgid "SecurityDashboard|Hide dismissed"
+msgid "SecurityReports|The security dashboard displays the latest security findings for projects you wish to monitor. Select \"Edit dashboard\" to add and remove projects."
msgstr ""
-msgid "SecurityDashboard|Monitor vulnerabilities in your code"
+msgid "SecurityReports|The security dashboard displays the latest security report. Use it to find and fix vulnerabilities."
msgstr ""
-msgid "SecurityDashboard|More information"
+msgid "SecurityReports|There was an error adding the comment."
msgstr ""
-msgid "SecurityDashboard|No vulnerabilities found for dashboard"
+msgid "SecurityReports|There was an error creating the issue."
msgstr ""
-msgid "SecurityDashboard|Pipeline %{pipelineLink} triggered %{timeago} by %{user}"
+msgid "SecurityReports|There was an error creating the merge request."
msgstr ""
-msgid "SecurityDashboard|Project"
+msgid "SecurityReports|There was an error deleting the comment."
msgstr ""
-msgid "SecurityDashboard|Projects added"
+msgid "SecurityReports|There was an error dismissing the vulnerabilities."
msgstr ""
-msgid "SecurityDashboard|Remove project from dashboard"
+msgid "SecurityReports|There was an error dismissing the vulnerability."
msgstr ""
-msgid "SecurityDashboard|Report type"
+msgid "SecurityReports|There was an error reverting the dismissal."
msgstr ""
-msgid "SecurityDashboard|Return to dashboard"
+msgid "SecurityReports|There was an error reverting this dismissal."
msgstr ""
-msgid "SecurityDashboard|Security Dashboard"
+msgid "SecurityReports|There was an error while generating the report."
msgstr ""
-msgid "SecurityDashboard|Select a project to add by using the project search field above."
+msgid "SecurityReports|Unable to add %{invalidProjects}"
msgstr ""
-msgid "SecurityDashboard|Severity"
+msgid "SecurityReports|Undo dismiss"
msgstr ""
-msgid "SecurityDashboard|Status"
+msgid "SecurityReports|While it's rare to have no vulnerabilities for your group, it can happen. In any event, we ask that you double check your settings to make sure you've set up your dashboard correctly."
msgstr ""
-msgid "SecurityDashboard|The security dashboard displays the latest security findings for projects you wish to monitor. Select \"Edit dashboard\" to add and remove projects."
+msgid "SecurityReports|While it's rare to have no vulnerabilities for your pipeline, it can happen. In any event, we ask that you double check your settings to make sure all security scanning jobs have passed successfully."
msgstr ""
-msgid "SecurityDashboard|The security dashboard displays the latest security report. Use it to find and fix vulnerabilities."
+msgid "SecurityReports|While it's rare to have no vulnerabilities for your project, it can happen. In any event, we ask that you double check your settings to make sure you've set up your dashboard correctly."
msgstr ""
-msgid "SecurityDashboard|There was an error while generating the report."
+msgid "SecurityReports|While it's rare to have no vulnerabilities, it can happen. In any event, we ask that you please double check your settings to make sure you've set up your dashboard correctly."
msgstr ""
-msgid "SecurityDashboard|Unable to add %{invalidProjects}"
+msgid "SecurityReports|Won't fix / Accept risk"
+msgstr ""
+
+msgid "SecurityReports|You do not have sufficient permissions to access this report"
+msgstr ""
+
+msgid "SecurityReports|You must sign in as an authorized user to see this report"
+msgstr ""
+
+msgid "SecurityReports|[No reason]"
msgstr ""
msgid "See GitLab's %{password_policy_guidelines}"
@@ -18065,6 +18489,9 @@ msgstr ""
msgid "See the affected projects in the GitLab admin panel"
msgstr ""
+msgid "See what's new at GitLab"
+msgstr ""
+
msgid "Select"
msgstr ""
@@ -18470,9 +18897,6 @@ msgstr ""
msgid "Set up Jira Integration"
msgstr ""
-msgid "Set up Jira Integration illustration"
-msgstr ""
-
msgid "Set up a %{type} Runner automatically"
msgstr ""
@@ -18557,6 +18981,9 @@ msgstr ""
msgid "Severity: %{severity}"
msgstr ""
+msgid "Shards (%{shards})"
+msgstr ""
+
msgid "Shards to synchronize"
msgstr ""
@@ -18871,6 +19298,9 @@ msgstr ""
msgid "Snowplow"
msgstr ""
+msgid "Some child epics may be hidden due to applied filters"
+msgstr ""
+
msgid "Some email servers do not support overriding the email sender name. Enable this option to include the name of the author of the issue, merge request or comment in the email body instead."
msgstr ""
@@ -19012,6 +19442,9 @@ msgstr ""
msgid "Something went wrong while updating your list settings"
msgstr ""
+msgid "Something went wrong with your automatic subscription renewal"
+msgstr ""
+
msgid "Something went wrong, unable to add %{project} to dashboard"
msgstr ""
@@ -19297,6 +19730,9 @@ msgstr ""
msgid "Stage removed"
msgstr ""
+msgid "Standard"
+msgstr ""
+
msgid "Star a label to make it a priority label. Order the prioritized labels to change their relative priority, by dragging."
msgstr ""
@@ -19420,6 +19856,9 @@ msgstr ""
msgid "Static Application Security Testing (SAST)"
msgstr ""
+msgid "StaticSiteEditor|An error occurred while submitting your changes."
+msgstr ""
+
msgid "StaticSiteEditor|Branch could not be created."
msgstr ""
@@ -19429,18 +19868,30 @@ msgstr ""
msgid "StaticSiteEditor|Could not create merge request."
msgstr ""
+msgid "StaticSiteEditor|Incompatible file content"
+msgstr ""
+
msgid "StaticSiteEditor|Return to site"
msgstr ""
+msgid "StaticSiteEditor|Static site editor"
+msgstr ""
+
msgid "StaticSiteEditor|Success!"
msgstr ""
msgid "StaticSiteEditor|Summary of changes"
msgstr ""
+msgid "StaticSiteEditor|The Static Site Editor is currently configured to only edit Markdown content on pages generated from Middleman. Visit the documentation to learn more about configuring your site to use the Static Site Editor."
+msgstr ""
+
msgid "StaticSiteEditor|Update %{sourcePath} file"
msgstr ""
+msgid "StaticSiteEditor|View documentation"
+msgstr ""
+
msgid "StaticSiteEditor|View merge request"
msgstr ""
@@ -19498,6 +19949,12 @@ msgstr ""
msgid "StatusPage|Status page"
msgstr ""
+msgid "StatusPage|Status page URL"
+msgstr ""
+
+msgid "StatusPage|Status page frontend documentation"
+msgstr ""
+
msgid "StatusPage|To publish incidents to an external status page, GitLab will store a JSON file in your Amazon S3 account in a location accessible to your external status page service. Make sure to also set up %{docsLink}"
msgstr ""
@@ -19846,6 +20303,12 @@ msgstr ""
msgid "Synced"
msgstr ""
+msgid "Synchronization disabled"
+msgstr ""
+
+msgid "Synchronization of container repositories is disabled."
+msgstr ""
+
msgid "System"
msgstr ""
@@ -19999,6 +20462,9 @@ msgstr ""
msgid "Telephone number"
msgstr ""
+msgid "Telephone number (Optional)"
+msgstr ""
+
msgid "Template"
msgstr ""
@@ -20305,6 +20771,9 @@ msgstr ""
msgid "The license was successfully uploaded and is now active. You can see the details below."
msgstr ""
+msgid "The license was successfully uploaded and will be active from %{starts_at}. You can see the details below."
+msgstr ""
+
msgid "The maximum file size allowed is %{size}."
msgstr ""
@@ -20626,6 +21095,9 @@ msgstr ""
msgid "There was an error getting the epic participants."
msgstr ""
+msgid "There was an error importing the Jira project."
+msgstr ""
+
msgid "There was an error loading users activity calendar."
msgstr ""
@@ -20641,7 +21113,7 @@ msgstr ""
msgid "There was an error resetting user pipeline minutes."
msgstr ""
-msgid "There was an error saving this Geo Node"
+msgid "There was an error saving this Geo Node."
msgstr ""
msgid "There was an error saving your changes."
@@ -20704,6 +21176,9 @@ msgstr ""
msgid "They can be managed using the %{link}."
msgstr ""
+msgid "Third Party Advisory Link"
+msgstr ""
+
msgid "Third party offers"
msgstr ""
@@ -20740,6 +21215,9 @@ msgstr ""
msgid "This application will be able to:"
msgstr ""
+msgid "This attachment has been truncated to avoid exceeding the maximum allowed attachment size of 15MB. %{written_count} of %{issues_count} issues have been included. Consider re-exporting with a narrower selection of issues."
+msgstr ""
+
msgid "This block is self-referential"
msgstr ""
@@ -20881,6 +21359,12 @@ msgstr ""
msgid "This issue is confidential"
msgstr ""
+msgid "This issue is currently blocked by the following issues: %{issues}."
+msgstr ""
+
+msgid "This issue is in a child epic of the filtered epic"
+msgstr ""
+
msgid "This issue is locked."
msgstr ""
@@ -21079,7 +21563,10 @@ msgstr ""
msgid "This user will be the author of all events in the activity feed that are the result of an update, like new branches being created or new commits being pushed to existing branches. Upon creation or when reassigning you can only assign yourself to be the mirror user."
msgstr ""
-msgid "This variable can not be masked"
+msgid "This variable can not be masked."
+msgstr ""
+
+msgid "This variable does not match the expected pattern."
msgstr ""
msgid "This will help us personalize your onboarding experience."
@@ -21109,10 +21596,10 @@ msgstr ""
msgid "ThreatMonitoring|Application firewall not detected"
msgstr ""
-msgid "ThreatMonitoring|Container Network Policy"
+msgid "ThreatMonitoring|Container Network Policies are not installed or have been disabled. To view this data, ensure your Network Policies are installed and enabled for your cluster."
msgstr ""
-msgid "ThreatMonitoring|Container NetworkPolicies are not installed or has been disabled. To view this data, ensure you NetworkPolicies are installed and enabled for your cluster."
+msgid "ThreatMonitoring|Container Network Policy"
msgstr ""
msgid "ThreatMonitoring|Container NetworkPolicies not detected"
@@ -21145,7 +21632,7 @@ msgstr ""
msgid "ThreatMonitoring|Something went wrong, unable to fetch statistics"
msgstr ""
-msgid "ThreatMonitoring|The firewall is not installed or has been disabled. To view this data, ensure you firewall is installed and enabled for your cluster."
+msgid "ThreatMonitoring|The firewall is not installed or has been disabled. To view this data, ensure the web application firewall is installed and enabled for your cluster."
msgstr ""
msgid "ThreatMonitoring|The graph below is an overview of traffic coming to your application as tracked by the Web Application Firewall (WAF). View the docs for instructions on how to access the WAF logs to see what type of malicious traffic is trying to access your app. The docs link is also accessible by clicking the \"?\" icon next to the title below."
@@ -21214,6 +21701,9 @@ msgstr ""
msgid "Time in seconds GitLab will wait for a response from the external service. When the service does not respond in time, access will be denied."
msgstr ""
+msgid "Time of import: %{importTime}"
+msgstr ""
+
msgid "Time remaining"
msgstr ""
@@ -21398,7 +21888,7 @@ msgstr ""
msgid "Title:"
msgstr ""
-msgid "Titles and Filenames"
+msgid "Titles and Descriptions"
msgstr ""
msgid "To"
@@ -21605,7 +22095,7 @@ msgstr ""
msgid "Too many projects enabled. You will need to manage them via the console or the API."
msgstr ""
-msgid "Topics"
+msgid "Topics (optional)"
msgstr ""
msgid "Total"
@@ -21767,6 +22257,9 @@ msgstr ""
msgid "Try all GitLab has to offer for 30 days."
msgstr ""
+msgid "Try changing or removing filters."
+msgstr ""
+
msgid "Try to fork again"
msgstr ""
@@ -21860,6 +22353,9 @@ msgstr ""
msgid "Unable to connect to server: %{error}"
msgstr ""
+msgid "Unable to connect to the Jira instance. Please check your Jira integration configuration."
+msgstr ""
+
msgid "Unable to convert Kubernetes logs encoding to UTF-8"
msgstr ""
@@ -22803,6 +23299,9 @@ msgstr ""
msgid "View issue"
msgstr ""
+msgid "View issues"
+msgstr ""
+
msgid "View it on GitLab"
msgstr ""
@@ -22980,6 +23479,15 @@ msgstr ""
msgid "VulnerabilityManagement|Resolved %{timeago} by %{user}"
msgstr ""
+msgid "VulnerabilityManagement|Something went wrong while trying to delete the comment. Please try again later."
+msgstr ""
+
+msgid "VulnerabilityManagement|Something went wrong while trying to retrieve the vulnerability history. Please try again later."
+msgstr ""
+
+msgid "VulnerabilityManagement|Something went wrong while trying to save the comment. Please try again later."
+msgstr ""
+
msgid "VulnerabilityManagement|Something went wrong, could not create an issue."
msgstr ""
@@ -23010,6 +23518,9 @@ msgstr ""
msgid "VulnerabilityStatusTypes|Resolved"
msgstr ""
+msgid "Vulnerability|%{scannerName} (version %{scannerVersion})"
+msgstr ""
+
msgid "Vulnerability|Class"
msgstr ""
@@ -23043,7 +23554,10 @@ msgstr ""
msgid "Vulnerability|Project"
msgstr ""
-msgid "Vulnerability|Report Type"
+msgid "Vulnerability|Scanner Provider"
+msgstr ""
+
+msgid "Vulnerability|Scanner Type"
msgstr ""
msgid "Vulnerability|Severity"
@@ -23097,9 +23611,15 @@ msgstr ""
msgid "We sent you an email with reset password instructions"
msgstr ""
+msgid "We tried to automatically renew your %{strong}%{plan_name}%{strong_close} subscription for %{strong}%{namespace_name}%{strong_close} on %{expires_on} but something went wrong so your subscription was downgraded to the free plan. Don't worry, your data is safe. We suggest you check your payment method and get in touch with our support team (%{support_link}). They'll gladly help with your subscription renewal."
+msgstr ""
+
msgid "We want to be sure it is you, please confirm you are not a robot."
msgstr ""
+msgid "We will automatically renew your %{strong}%{plan_name}%{strong_close} subscription for %{strong}%{namespace_name}%{strong_close} on %{strong}%{expires_on}%{strong_close}. There's nothing that you need to do, we'll let you know when the renewal is complete. Need more seats, a higher plan or just want to review your payment method?"
+msgstr ""
+
msgid "We've found no vulnerabilities"
msgstr ""
@@ -23204,15 +23724,6 @@ msgstr ""
msgid "When:"
msgstr ""
-msgid "While it's rare to have no vulnerabilities for your group, it can happen. In any event, we ask that you double check your settings to make sure you've set up your dashboard correctly."
-msgstr ""
-
-msgid "While it's rare to have no vulnerabilities for your pipeline, it can happen. In any event, we ask that you double check your settings to make sure all security scanning jobs have passed successfully."
-msgstr ""
-
-msgid "While it's rare to have no vulnerabilities for your project, it can happen. In any event, we ask that you double check your settings to make sure you've set up your dashboard correctly."
-msgstr ""
-
msgid "While it's rare to have no vulnerabilities, it can happen. In any event, we ask that you please double check your settings to make sure you've set up your dashboard correctly."
msgstr ""
@@ -23243,9 +23754,6 @@ msgstr ""
msgid "Wiki"
msgstr ""
-msgid "Wiki pages"
-msgstr ""
-
msgid "Wiki was successfully updated."
msgstr ""
@@ -23429,6 +23937,9 @@ msgstr ""
msgid "Yes, add it"
msgstr ""
+msgid "Yes, close issue"
+msgstr ""
+
msgid "Yes, let me map Google Code users to full names or GitLab users."
msgstr ""
@@ -23624,6 +24135,9 @@ msgstr ""
msgid "You could not create a new trigger."
msgstr ""
+msgid "You didn't renew your %{strong}%{plan_name}%{strong_close} subscription for %{strong}%{namespace_name}%{strong_close} so it was downgraded to the free plan."
+msgstr ""
+
msgid "You didn't renew your %{strong}%{plan_name}%{strong_close} subscription so it was downgraded to the GitLab Core Plan."
msgstr ""
@@ -23690,6 +24204,9 @@ msgstr ""
msgid "You have declined the invitation to join %{label}."
msgstr ""
+msgid "You have imported from this project %{numberOfPreviousImportsForProject} times before. Each new import will create duplicate issues."
+msgstr ""
+
msgid "You have no permissions"
msgstr ""
@@ -23771,6 +24288,9 @@ msgstr ""
msgid "You need to upload a Google Takeout archive."
msgstr ""
+msgid "You reached %{usage_in_percent} of %{namespace_name}'s capacity (%{used_storage} of %{storage_limit})"
+msgstr ""
+
msgid "You tried to fork %{link_to_the_project} but it failed for the following reason:"
msgstr ""
@@ -23855,9 +24375,21 @@ msgstr ""
msgid "YouTube"
msgstr ""
+msgid "Your %{strong}%{plan_name}%{strong_close} subscription for %{strong}%{namespace_name}%{strong_close} will expire on %{strong}%{expires_on}%{strong_close}. After that, you will not to be able to create issues or merge requests as well as many other features."
+msgstr ""
+
msgid "Your %{strong}%{plan_name}%{strong_close} subscription will expire on %{strong}%{expires_on}%{strong_close}. After that, you will not to be able to create issues or merge requests as well as many other features."
msgstr ""
+msgid "Your CSV export has started. It will be emailed to %{email} when complete."
+msgstr ""
+
+msgid "Your CSV export of %{issues_count} from project %{project_link} has been added to this email as an attachment."
+msgstr ""
+
+msgid "Your CSV export of %{written_count} from project %{project_name} (%{project_url}) has been added to this email as an attachment."
+msgstr ""
+
msgid "Your Commit Email will be used for web based operations, such as edits and merges."
msgstr ""
@@ -23954,6 +24486,9 @@ msgstr ""
msgid "Your comment could not be updated! Please check your network connection and try again."
msgstr ""
+msgid "Your comment will be discarded."
+msgstr ""
+
msgid "Your custom stage '%{title}' was created"
msgstr ""
@@ -23993,6 +24528,9 @@ msgstr ""
msgid "Your new personal access token has been created."
msgstr ""
+msgid "Your new project access token has been created."
+msgstr ""
+
msgid "Your password isn't required to view this page. If a password or any other personal details are requested, please contact your administrator to report abuse."
msgstr ""
@@ -24011,12 +24549,18 @@ msgstr ""
msgid "Your request for access has been queued for review."
msgstr ""
+msgid "Your search didn't match any commits."
+msgstr ""
+
msgid "Your subscription expired!"
msgstr ""
msgid "Your subscription has been downgraded"
msgstr ""
+msgid "Your subscription will automatically renew in %{remaining_days}"
+msgstr ""
+
msgid "Your subscription will expire in %{remaining_days}"
msgstr ""
@@ -24055,8 +24599,8 @@ msgstr "atrás"
msgid "allowed to fail"
msgstr "permissão para falhar"
-msgid "already being used for another group or project milestone."
-msgstr "já está a ser usado por outro grupo ou objetivos do projeto."
+msgid "already being used for another group or project %{timebox_name}."
+msgstr ""
msgid "already has a \"created\" issue link"
msgstr ""
@@ -24133,6 +24677,9 @@ msgstr ""
msgid "ciReport|%{linkStartTag}Learn more about SAST %{linkEndTag}"
msgstr ""
+msgid "ciReport|%{linkStartTag}Learn more about Secret Scanning %{linkEndTag}"
+msgstr ""
+
msgid "ciReport|%{linkStartTag}Learn more about codequality reports %{linkEndTag}"
msgstr ""
@@ -24296,6 +24843,12 @@ msgstr ""
msgid "ciReport|SAST"
msgstr ""
+msgid "ciReport|Secret scanning"
+msgstr ""
+
+msgid "ciReport|Secret scanning detects secrets and credentials vulnerabilities in your source code."
+msgstr ""
+
msgid "ciReport|Security scanning"
msgstr ""
@@ -24493,6 +25046,9 @@ msgstr ""
msgid "group"
msgstr ""
+msgid "groups"
+msgstr ""
+
msgid "has already been linked to another vulnerability"
msgstr ""
@@ -24656,9 +25212,6 @@ msgstr[1] ""
msgid "merged %{time_ago}"
msgstr ""
-msgid "milestone should belong either to a project or a group."
-msgstr ""
-
msgid "missing"
msgstr ""
@@ -24995,6 +25548,9 @@ msgstr ""
msgid "no contributions"
msgstr ""
+msgid "no expiration"
+msgstr ""
+
msgid "no one can merge"
msgstr ""
@@ -25047,6 +25603,9 @@ msgstr ""
msgid "pending removal"
msgstr ""
+msgid "per day"
+msgstr ""
+
msgid "pipeline"
msgstr ""
@@ -25075,6 +25634,12 @@ msgstr[1] ""
msgid "project avatar"
msgstr ""
+msgid "projects"
+msgstr ""
+
+msgid "push to your repository, create pipelines, create issues or add comments. To reduce storage capacity, delete unused repositories, artifacts, wikis, issues, and pipelines."
+msgstr ""
+
msgid "quick actions"
msgstr ""
@@ -25221,11 +25786,6 @@ msgstr ""
msgid "this document"
msgstr ""
-msgid "thread resolved"
-msgid_plural "threads resolved"
-msgstr[0] ""
-msgstr[1] ""
-
msgid "to help your contributors communicate effectively!"
msgstr ""
@@ -25280,6 +25840,9 @@ msgstr ""
msgid "view the blob"
msgstr ""
+msgid "vulnerability|Add a comment"
+msgstr ""
+
msgid "vulnerability|Add a comment or reason for dismissal"
msgstr "Adiciona um comentário ou razão para dispensar"
diff --git a/locale/ro_RO/gitlab.po b/locale/ro_RO/gitlab.po
index d979547f361..1c75c655d1d 100644
--- a/locale/ro_RO/gitlab.po
+++ b/locale/ro_RO/gitlab.po
@@ -12,7 +12,7 @@ msgstr ""
"X-Crowdin-Project: gitlab-ee\n"
"X-Crowdin-Language: ro\n"
"X-Crowdin-File: /master/locale/gitlab.pot\n"
-"PO-Revision-Date: 2020-04-15 00:26\n"
+"PO-Revision-Date: 2020-05-05 21:10\n"
msgid " %{start} to %{end}"
msgstr ""
@@ -242,6 +242,12 @@ msgstr[0] ""
msgstr[1] ""
msgstr[2] ""
+msgid "%d unresolved thread"
+msgid_plural "%d unresolved threads"
+msgstr[0] ""
+msgstr[1] ""
+msgstr[2] ""
+
msgid "%d vulnerability dismissed"
msgid_plural "%d vulnerabilities dismissed"
msgstr[0] ""
@@ -407,6 +413,9 @@ msgstr ""
msgid "%{mrText}, this issue will be closed automatically."
msgstr ""
+msgid "%{namespace_name} is now read-only. You cannot: %{base_message}"
+msgstr ""
+
msgid "%{name} contained %{resultsString}"
msgstr ""
@@ -419,6 +428,12 @@ msgstr ""
msgid "%{name}'s avatar"
msgstr ""
+msgid "%{no_of_days} day"
+msgid_plural "%{no_of_days} days"
+msgstr[0] ""
+msgstr[1] ""
+msgstr[2] ""
+
msgid "%{number_commits_behind} commits behind %{default_branch}, %{number_commits_ahead} commits ahead"
msgstr ""
@@ -548,6 +563,9 @@ msgstr[2] ""
msgid "%{text} is available"
msgstr ""
+msgid "%{timebox_name} should belong either to a project or a group."
+msgstr ""
+
msgid "%{title} %{operator} %{threshold}"
msgstr ""
@@ -842,12 +860,18 @@ msgstr ""
msgid "<code>\"johnsmith@example.com\": \"johnsmith@example.com\"</code> will add \"By <a href=\"#\">johnsmith@example.com</a>\" to all issues and comments originally created by johnsmith@example.com. By default, the email address or username is masked to ensure the user's privacy. Use this option if you want to show the full email address."
msgstr ""
+msgid "<namespace / project>"
+msgstr ""
+
msgid "<no name set>"
msgstr ""
msgid "<no scopes selected>"
msgstr ""
+msgid "<project name>"
+msgstr ""
+
msgid "<strong>%{group_name}</strong> group members"
msgstr ""
@@ -881,6 +905,9 @@ msgstr ""
msgid "A basic page and serverless function that uses AWS Lambda, AWS API Gateway, and GitLab Pages"
msgstr ""
+msgid "A complete DevOps platform"
+msgstr ""
+
msgid "A default branch cannot be chosen for an empty project."
msgstr ""
@@ -1097,6 +1124,12 @@ msgstr ""
msgid "AccessTokens|reset it"
msgstr ""
+msgid "AccessibilityReport|Accessibility report artifact not found"
+msgstr ""
+
+msgid "AccessibilityReport|Failed to retrieve accessibility report"
+msgstr ""
+
msgid "AccessibilityReport|Learn More"
msgstr ""
@@ -1106,6 +1139,9 @@ msgstr ""
msgid "AccessibilityReport|New"
msgstr ""
+msgid "AccessibilityReport|The accessibility scanning found an error of the following type: %{code}"
+msgstr ""
+
msgid "Account"
msgstr ""
@@ -1349,6 +1385,9 @@ msgstr ""
msgid "Added at"
msgstr ""
+msgid "Added for this merge request"
+msgstr ""
+
msgid "Added in this version"
msgstr ""
@@ -1721,6 +1760,72 @@ msgstr[0] ""
msgstr[1] ""
msgstr[2] ""
+msgid "Alert Details"
+msgstr ""
+
+msgid "AlertManagement|Acknowledged"
+msgstr ""
+
+msgid "AlertManagement|Alert"
+msgstr ""
+
+msgid "AlertManagement|Authorize external service"
+msgstr ""
+
+msgid "AlertManagement|Display alerts from all your monitoring tools directly within GitLab. Streamline the investigation of your alerts and the escalation of alerts to incidents."
+msgstr ""
+
+msgid "AlertManagement|End time"
+msgstr ""
+
+msgid "AlertManagement|End time:"
+msgstr ""
+
+msgid "AlertManagement|Events"
+msgstr ""
+
+msgid "AlertManagement|Events:"
+msgstr ""
+
+msgid "AlertManagement|Full Alert Details"
+msgstr ""
+
+msgid "AlertManagement|More information"
+msgstr ""
+
+msgid "AlertManagement|No alerts available to display. If you think you're seeing this message in error, refresh the page."
+msgstr ""
+
+msgid "AlertManagement|No alerts to display."
+msgstr ""
+
+msgid "AlertManagement|Overview"
+msgstr ""
+
+msgid "AlertManagement|Resolved"
+msgstr ""
+
+msgid "AlertManagement|Severity"
+msgstr ""
+
+msgid "AlertManagement|Start time"
+msgstr ""
+
+msgid "AlertManagement|Start time:"
+msgstr ""
+
+msgid "AlertManagement|Status"
+msgstr ""
+
+msgid "AlertManagement|Surface alerts in GitLab"
+msgstr ""
+
+msgid "AlertManagement|There was an error displaying the alerts. Confirm your endpoint's configuration details to ensure alerts appear."
+msgstr ""
+
+msgid "AlertManagement|Triggered"
+msgstr ""
+
msgid "AlertService|%{linkStart}Learn more%{linkEnd} about configuring this endpoint to receive alerts."
msgstr ""
@@ -1790,6 +1895,9 @@ msgstr ""
msgid "All security scans are enabled because %{linkStart}Auto DevOps%{linkEnd} is enabled on this project"
msgstr ""
+msgid "All threads resolved"
+msgstr ""
+
msgid "All users"
msgstr ""
@@ -1808,6 +1916,9 @@ msgstr ""
msgid "Allow only the selected protocols to be used for Git access."
msgstr ""
+msgid "Allow owners to manage default branch protection per group"
+msgstr ""
+
msgid "Allow owners to manually add users outside of LDAP"
msgstr ""
@@ -1916,6 +2027,9 @@ msgstr ""
msgid "An error occurred fetching the dropdown data."
msgstr ""
+msgid "An error occurred fetching the project authors."
+msgstr ""
+
msgid "An error occurred previewing the blob"
msgstr ""
@@ -1997,6 +2111,9 @@ msgstr ""
msgid "An error occurred while fetching sidebar data"
msgstr ""
+msgid "An error occurred while fetching terraform reports."
+msgstr ""
+
msgid "An error occurred while fetching the Service Desk address."
msgstr ""
@@ -2234,6 +2351,9 @@ msgstr ""
msgid "Any"
msgstr ""
+msgid "Any Author"
+msgstr ""
+
msgid "Any Label"
msgstr ""
@@ -2453,10 +2573,10 @@ msgstr ""
msgid "Are you sure that you want to unarchive this project?"
msgstr ""
-msgid "Are you sure you want to cancel creating this comment?"
+msgid "Are you sure you want to cancel editing this comment?"
msgstr ""
-msgid "Are you sure you want to cancel editing this comment?"
+msgid "Are you sure you want to close this blocked issue?"
msgstr ""
msgid "Are you sure you want to delete %{name}?"
@@ -2486,6 +2606,9 @@ msgstr ""
msgid "Are you sure you want to deploy this environment?"
msgstr ""
+msgid "Are you sure you want to discard this comment?"
+msgstr ""
+
msgid "Are you sure you want to erase this build?"
msgstr ""
@@ -2663,6 +2786,9 @@ msgstr ""
msgid "At least one approval from a code owner is required to change files matching the respective CODEOWNER rules."
msgstr ""
+msgid "At least one logging option is required to be enabled"
+msgstr ""
+
msgid "At least one of group_id or project_id must be specified"
msgstr ""
@@ -2702,6 +2828,27 @@ msgstr ""
msgid "AuditEvents|Target"
msgstr ""
+msgid "AuditLogs|(removed)"
+msgstr ""
+
+msgid "AuditLogs|Action"
+msgstr ""
+
+msgid "AuditLogs|Author"
+msgstr ""
+
+msgid "AuditLogs|Date"
+msgstr ""
+
+msgid "AuditLogs|IP Address"
+msgstr ""
+
+msgid "AuditLogs|Object"
+msgstr ""
+
+msgid "AuditLogs|Target"
+msgstr ""
+
msgid "Aug"
msgstr ""
@@ -2984,9 +3131,6 @@ msgstr ""
msgid "BambooService|You must set up automatic revision labeling and a repository trigger in Bamboo."
msgstr ""
-msgid "Batch operations"
-msgstr ""
-
msgid "BatchComments|Delete all pending comments"
msgstr ""
@@ -3116,6 +3260,9 @@ msgstr ""
msgid "Boards"
msgstr ""
+msgid "Boards and Board Lists"
+msgstr ""
+
msgid "Boards|Collapse"
msgstr ""
@@ -3335,6 +3482,9 @@ msgstr ""
msgid "Buy GitLab Enterprise Edition"
msgstr ""
+msgid "Buy more Pipeline minutes"
+msgstr ""
+
msgid "By %{user_name}"
msgstr ""
@@ -3455,6 +3605,9 @@ msgstr ""
msgid "Can't scan the code?"
msgstr ""
+msgid "Can't update snippet: %{err}"
+msgstr ""
+
msgid "Canary"
msgstr ""
@@ -3623,6 +3776,9 @@ msgstr ""
msgid "Changing group path can have unintended side effects."
msgstr ""
+msgid "Charts"
+msgstr ""
+
msgid "Charts can't be displayed as the request for data has timed out. %{documentationLink}"
msgstr ""
@@ -3659,9 +3815,6 @@ msgstr ""
msgid "ChatMessage|and [%{count} more](%{pipeline_failed_jobs_url})"
msgstr ""
-msgid "ChatMessage|failed"
-msgstr ""
-
msgid "ChatMessage|has failed"
msgstr ""
@@ -3677,9 +3830,6 @@ msgstr ""
msgid "ChatMessage|in %{project_link}"
msgstr ""
-msgid "ChatMessage|passed"
-msgstr ""
-
msgid "Check again"
msgstr ""
@@ -4130,9 +4280,6 @@ msgstr ""
msgid "Click any <strong>project name</strong> in the project list below to navigate to the project milestone."
msgstr ""
-msgid "Click here"
-msgstr ""
-
msgid "Click the <strong>Download</strong> button and wait for downloading to complete."
msgstr ""
@@ -4520,6 +4667,12 @@ msgstr ""
msgid "ClusterIntegration|Fetching zones"
msgstr ""
+msgid "ClusterIntegration|Fluentd"
+msgstr ""
+
+msgid "ClusterIntegration|Fluentd is an open source data collector, which lets you unify the data collection and consumption for a better use and understanding of data. It requires at least one of the following logs to be successfully installed."
+msgstr ""
+
msgid "ClusterIntegration|GitLab Integration"
msgstr ""
@@ -4826,6 +4979,15 @@ msgstr ""
msgid "ClusterIntegration|Request to begin uninstalling failed"
msgstr ""
+msgid "ClusterIntegration|SIEM Hostname"
+msgstr ""
+
+msgid "ClusterIntegration|SIEM Port"
+msgstr ""
+
+msgid "ClusterIntegration|SIEM Protocol"
+msgstr ""
+
msgid "ClusterIntegration|Save changes"
msgstr ""
@@ -4922,6 +5084,12 @@ msgstr ""
msgid "ClusterIntegration|Select zone to choose machine type"
msgstr ""
+msgid "ClusterIntegration|Send Cilium Logs"
+msgstr ""
+
+msgid "ClusterIntegration|Send ModSecurity Logs"
+msgstr ""
+
msgid "ClusterIntegration|Service Token"
msgstr ""
@@ -5351,18 +5519,33 @@ msgstr ""
msgid "Compliance framework (optional)"
msgstr ""
+msgid "ComplianceFramework|GDPR"
+msgstr ""
+
msgid "ComplianceFramework|GDPR - General Data Protection Regulation"
msgstr ""
+msgid "ComplianceFramework|HIPAA"
+msgstr ""
+
msgid "ComplianceFramework|HIPAA - Health Insurance Portability and Accountability Act"
msgstr ""
+msgid "ComplianceFramework|PCI-DSS"
+msgstr ""
+
msgid "ComplianceFramework|PCI-DSS - Payment Card Industry-Data Security Standard"
msgstr ""
+msgid "ComplianceFramework|SOC 2"
+msgstr ""
+
msgid "ComplianceFramework|SOC 2 - Service Organization Control 2"
msgstr ""
+msgid "ComplianceFramework|SOX"
+msgstr ""
+
msgid "ComplianceFramework|SOX - Sarbanes-Oxley"
msgstr ""
@@ -5510,9 +5693,15 @@ msgstr ""
msgid "Container repositories sync capacity"
msgstr ""
+msgid "ContainerRegistry| Please visit the %{linkStart}administration settings%{linkEnd} to enable this feature."
+msgstr ""
+
msgid "ContainerRegistry|%{imageName} tags"
msgstr ""
+msgid "ContainerRegistry|%{title} was successfully scheduled for deletion"
+msgstr ""
+
msgid "ContainerRegistry|Automatically remove extra images that aren't designed to be kept."
msgstr ""
@@ -5525,6 +5714,9 @@ msgstr ""
msgid "ContainerRegistry|Container Registry"
msgstr ""
+msgid "ContainerRegistry|Container Registry tag expiration and retention policy is disabled"
+msgstr ""
+
msgid "ContainerRegistry|Copy build command"
msgstr ""
@@ -5534,18 +5726,12 @@ msgstr ""
msgid "ContainerRegistry|Copy push command"
msgstr ""
-msgid "ContainerRegistry|Currently, the Container Registry tag expiration feature is not available for projects created before GitLab version 12.8. For updates and more information, visit Issue %{linkStart}#196124%{linkEnd}"
-msgstr ""
-
msgid "ContainerRegistry|Docker connection error"
msgstr ""
msgid "ContainerRegistry|Docker tag expiration policy is %{toggleStatus}"
msgstr ""
-msgid "ContainerRegistry|Docker tags with names matching this regex pattern will expire:"
-msgstr ""
-
msgid "ContainerRegistry|Edit Settings"
msgstr ""
@@ -5567,9 +5753,6 @@ msgstr ""
msgid "ContainerRegistry|Image ID"
msgstr ""
-msgid "ContainerRegistry|Image deleted successfully"
-msgstr ""
-
msgid "ContainerRegistry|Keep and protect the images that matter most."
msgstr ""
@@ -5585,12 +5768,24 @@ msgstr ""
msgid "ContainerRegistry|Number of tags to retain:"
msgstr ""
+msgid "ContainerRegistry|Please contact your administrator."
+msgstr ""
+
msgid "ContainerRegistry|Push an image"
msgstr ""
msgid "ContainerRegistry|Quick Start"
msgstr ""
+msgid "ContainerRegistry|Regular expressions such as %{codeStart}.*-test%{codeEnd} or %{codeStart}dev-.*%{codeEnd} are supported"
+msgstr ""
+
+msgid "ContainerRegistry|Regular expressions such as %{codeStart}.*-test%{codeEnd} or %{codeStart}dev-.*%{codeEnd} are supported. To select all tags, use %{codeStart}.*%{codeEnd}"
+msgstr ""
+
+msgid "ContainerRegistry|Remember to run %{docLinkStart}garbage collection%{docLinkEnd} to remove the stale data from storage."
+msgstr ""
+
msgid "ContainerRegistry|Remove repository"
msgstr ""
@@ -5606,22 +5801,22 @@ msgstr[2] ""
msgid "ContainerRegistry|Retention policy has been Enabled"
msgstr ""
-msgid "ContainerRegistry|Something went wrong while deleting the image."
+msgid "ContainerRegistry|Something went wrong while fetching the expiration policy."
msgstr ""
-msgid "ContainerRegistry|Something went wrong while deleting the tag."
+msgid "ContainerRegistry|Something went wrong while fetching the repository list."
msgstr ""
-msgid "ContainerRegistry|Something went wrong while deleting the tags."
+msgid "ContainerRegistry|Something went wrong while fetching the tags list."
msgstr ""
-msgid "ContainerRegistry|Something went wrong while fetching the expiration policy."
+msgid "ContainerRegistry|Something went wrong while marking the tag for deletion."
msgstr ""
-msgid "ContainerRegistry|Something went wrong while fetching the packages list."
+msgid "ContainerRegistry|Something went wrong while marking the tags for deletion."
msgstr ""
-msgid "ContainerRegistry|Something went wrong while fetching the tags list."
+msgid "ContainerRegistry|Something went wrong while scheduling %{title} for deletion. Please try again."
msgstr ""
msgid "ContainerRegistry|Something went wrong while updating the expiration policy."
@@ -5630,16 +5825,25 @@ msgstr ""
msgid "ContainerRegistry|Tag"
msgstr ""
-msgid "ContainerRegistry|Tag deleted successfully"
-msgstr ""
-
msgid "ContainerRegistry|Tag expiration policy"
msgstr ""
msgid "ContainerRegistry|Tag expiration policy is designed to:"
msgstr ""
-msgid "ContainerRegistry|Tags deleted successfully"
+msgid "ContainerRegistry|Tag successfully marked for deletion."
+msgstr ""
+
+msgid "ContainerRegistry|Tags successfully marked for deletion."
+msgstr ""
+
+msgid "ContainerRegistry|Tags with names matching this regex pattern will %{italicStart}be preserved:%{italicEnd}"
+msgstr ""
+
+msgid "ContainerRegistry|Tags with names matching this regex pattern will %{italicStart}expire:%{italicEnd}"
+msgstr ""
+
+msgid "ContainerRegistry|The Container Registry tag expiration and retention policies for this project have not been enabled."
msgstr ""
msgid "ContainerRegistry|The last tag related to this image was recently removed. This empty image and any associated data will be automatically removed as part of the regular Garbage Collection process. If you have any questions, contact your administrator."
@@ -5660,16 +5864,16 @@ msgstr ""
msgid "ContainerRegistry|There are no container images stored for this project"
msgstr ""
-msgid "ContainerRegistry|This Registry contains deleted image tag data. Remember to run %{docLinkStart}garbage collection%{docLinkEnd} to remove the stale data from storage."
+msgid "ContainerRegistry|There was an error during the deletion of this image repository, please try again."
msgstr ""
msgid "ContainerRegistry|This image has no active tags"
msgstr ""
-msgid "ContainerRegistry|We are having trouble connecting to Docker, which could be due to an issue with your project name or path. %{docLinkStart}More Information%{docLinkEnd}"
+msgid "ContainerRegistry|This image repository is scheduled for deletion"
msgstr ""
-msgid "ContainerRegistry|Wildcards such as %{codeStart}.*-stable%{codeEnd} or %{codeStart}production/.*%{codeEnd} are supported. To select all tags, use %{codeStart}.*%{codeEnd}"
+msgid "ContainerRegistry|We are having trouble connecting to Docker, which could be due to an issue with your project name or path. %{docLinkStart}More Information%{docLinkEnd}"
msgstr ""
msgid "ContainerRegistry|With the Container Registry, every project can have its own space to store its Docker images. %{docLinkStart}More Information%{docLinkEnd}"
@@ -5924,7 +6128,7 @@ msgstr ""
msgid "Could not delete chat nickname %{chat_name}."
msgstr ""
-msgid "Could not find design"
+msgid "Could not find design."
msgstr ""
msgid "Could not remove the trigger."
@@ -5939,6 +6143,9 @@ msgstr ""
msgid "Could not revoke personal access token %{personal_access_token_name}."
msgstr ""
+msgid "Could not revoke project access token %{project_access_token_name}."
+msgstr ""
+
msgid "Could not save group ID"
msgstr ""
@@ -5996,6 +6203,9 @@ msgstr ""
msgid "Create a new branch"
msgstr ""
+msgid "Create a new deploy key for this project"
+msgstr ""
+
msgid "Create a new file as there are no files yet. Afterwards, you'll be able to commit your changes."
msgstr ""
@@ -6101,6 +6311,9 @@ msgstr ""
msgid "Create requirement"
msgstr ""
+msgid "Create snippet"
+msgstr ""
+
msgid "Create wildcard: %{searchTerm}"
msgstr ""
@@ -6122,6 +6335,9 @@ msgstr ""
msgid "Created"
msgstr ""
+msgid "Created %{timestamp}"
+msgstr ""
+
msgid "Created At"
msgstr ""
@@ -6479,6 +6695,9 @@ msgstr ""
msgid "CycleAnalytics|stage dropdown"
msgstr ""
+msgid "DAG"
+msgstr ""
+
msgid "DNS"
msgstr ""
@@ -6851,6 +7070,9 @@ msgstr ""
msgid "Deploy key was successfully updated."
msgstr ""
+msgid "Deploy keys allow read-only or read-write (if enabled) access to your repository. Deploy keys can be used for CI, staging or production servers. You can create a deploy key or add an existing one."
+msgstr ""
+
msgid "Deploy progress not found. To see pods, ensure your environment matches %{linkStart}deploy board criteria%{linkEnd}."
msgstr ""
@@ -7004,6 +7226,9 @@ msgstr ""
msgid "Deploying to"
msgstr ""
+msgid "Deployment Frequency"
+msgstr ""
+
msgid "Deployment|API"
msgstr ""
@@ -7523,9 +7748,6 @@ msgstr ""
msgid "Edit environment"
msgstr ""
-msgid "Edit epic description"
-msgstr ""
-
msgid "Edit file"
msgstr ""
@@ -8147,9 +8369,6 @@ msgstr ""
msgid "Epics"
msgstr ""
-msgid "Epics (Ultimate / Gold license only)"
-msgstr ""
-
msgid "Epics Roadmap"
msgstr ""
@@ -8159,6 +8378,9 @@ msgstr ""
msgid "Epics let you manage your portfolio of projects more efficiently and with less effort"
msgstr ""
+msgid "Epics, Issues, and Merge Requests"
+msgstr ""
+
msgid "Epics|Add an epic"
msgstr ""
@@ -8210,6 +8432,9 @@ msgstr ""
msgid "Epics|Something went wrong while fetching group epics."
msgstr ""
+msgid "Epics|Something went wrong while moving item."
+msgstr ""
+
msgid "Epics|Something went wrong while ordering item."
msgstr ""
@@ -8264,6 +8489,9 @@ msgstr ""
msgid "Error fetching network graph."
msgstr ""
+msgid "Error fetching payload data."
+msgstr ""
+
msgid "Error fetching projects"
msgstr ""
@@ -8273,9 +8501,6 @@ msgstr ""
msgid "Error fetching the dependency list. Please check your network connection and try again."
msgstr ""
-msgid "Error fetching usage ping data."
-msgstr ""
-
msgid "Error loading branch data. Please try again."
msgstr ""
@@ -8417,6 +8642,9 @@ msgstr ""
msgid "Errors"
msgstr ""
+msgid "Errors:"
+msgstr ""
+
msgid "Estimated"
msgstr ""
@@ -8621,12 +8849,18 @@ msgstr ""
msgid "Export as CSV"
msgstr ""
+msgid "Export group"
+msgstr ""
+
msgid "Export issues"
msgstr ""
msgid "Export project"
msgstr ""
+msgid "Export this group with all related data to a new GitLab instance. Once complete, you can import the data file from the \"New Group\" page."
+msgstr ""
+
msgid "Export this project with all its related data in order to move your project to a new GitLab instance. Once the export is finished, you can import the file from the \"New Project\" page."
msgstr ""
@@ -8696,9 +8930,6 @@ msgstr ""
msgid "Failed Jobs"
msgstr ""
-msgid "Failed create wiki"
-msgstr ""
-
msgid "Failed to add a Zoom meeting"
msgstr ""
@@ -8738,6 +8969,9 @@ msgstr ""
msgid "Failed to create resources"
msgstr ""
+msgid "Failed to create wiki"
+msgstr ""
+
msgid "Failed to delete board. Please try again."
msgstr ""
@@ -8747,7 +8981,7 @@ msgstr ""
msgid "Failed to enqueue the rebase operation, possibly due to a long-lived transaction. Try again later."
msgstr ""
-msgid "Failed to find import label for jira import."
+msgid "Failed to find import label for Jira import."
msgstr ""
msgid "Failed to get ref."
@@ -8771,6 +9005,12 @@ msgstr ""
msgid "Failed to load groups & users."
msgstr ""
+msgid "Failed to load labels. Please try again."
+msgstr ""
+
+msgid "Failed to load milestones. Please try again."
+msgstr ""
+
msgid "Failed to load related branches"
msgstr ""
@@ -9146,7 +9386,10 @@ msgstr ""
msgid "Filter by milestone name"
msgstr ""
-msgid "Filter by name..."
+msgid "Filter by name"
+msgstr ""
+
+msgid "Filter by status"
msgstr ""
msgid "Filter by two-factor authentication"
@@ -9158,12 +9401,18 @@ msgstr ""
msgid "Filter projects"
msgstr ""
+msgid "Filter results"
+msgstr ""
+
msgid "Filter results by group"
msgstr ""
msgid "Filter results by project"
msgstr ""
+msgid "Filter results..."
+msgstr ""
+
msgid "Filter your projects by name"
msgstr ""
@@ -9452,10 +9701,10 @@ msgstr ""
msgid "GeoNodes|Checksummed"
msgstr ""
-msgid "GeoNodes|Container repositories"
+msgid "GeoNodes|Consult Geo troubleshooting information"
msgstr ""
-msgid "GeoNodes|Data is out of date from %{timeago}"
+msgid "GeoNodes|Container repositories"
msgstr ""
msgid "GeoNodes|Data replication lag"
@@ -9497,6 +9746,9 @@ msgstr ""
msgid "GeoNodes|Last event ID seen from primary"
msgstr ""
+msgid "GeoNodes|Learn more about Geo node statuses"
+msgstr ""
+
msgid "GeoNodes|Loading nodes"
msgstr ""
@@ -9512,6 +9764,9 @@ msgstr ""
msgid "GeoNodes|Node was successfully removed."
msgstr ""
+msgid "GeoNodes|Node's status was updated %{timeAgo}."
+msgstr ""
+
msgid "GeoNodes|Not checksummed"
msgstr ""
@@ -9542,7 +9797,10 @@ msgstr ""
msgid "GeoNodes|Repository verification progress"
msgstr ""
-msgid "GeoNodes|Selective"
+msgid "GeoNodes|Selective (%{syncLabel})"
+msgstr ""
+
+msgid "GeoNodes|Selective synchronization"
msgstr ""
msgid "GeoNodes|Something went wrong while changing node status"
@@ -9569,6 +9827,9 @@ msgstr ""
msgid "GeoNodes|Unverified"
msgstr ""
+msgid "GeoNodes|Updated %{timeAgo}"
+msgstr ""
+
msgid "GeoNodes|Used slots"
msgstr ""
@@ -9608,16 +9869,16 @@ msgstr ""
msgid "Geo|All"
msgstr ""
-msgid "Geo|All projects"
+msgid "Geo|All %{replicable_type}"
msgstr ""
-msgid "Geo|All projects are being scheduled for re-sync"
+msgid "Geo|All projects"
msgstr ""
-msgid "Geo|All projects are being scheduled for re-verify"
+msgid "Geo|All projects are being scheduled for resync"
msgstr ""
-msgid "Geo|Batch operations"
+msgid "Geo|All projects are being scheduled for reverify"
msgstr ""
msgid "Geo|Could not remove tracking entry for an existing project."
@@ -9629,9 +9890,15 @@ msgstr ""
msgid "Geo|Failed"
msgstr ""
+msgid "Geo|Filter by status"
+msgstr ""
+
msgid "Geo|Geo Status"
msgstr ""
+msgid "Geo|In progress"
+msgstr ""
+
msgid "Geo|In sync"
msgstr ""
@@ -9656,9 +9923,6 @@ msgstr ""
msgid "Geo|Not synced yet"
msgstr ""
-msgid "Geo|Pending"
-msgstr ""
-
msgid "Geo|Pending synchronization"
msgstr ""
@@ -9695,7 +9959,7 @@ msgstr ""
msgid "Geo|Resync"
msgstr ""
-msgid "Geo|Resync all projects"
+msgid "Geo|Resync all"
msgstr ""
msgid "Geo|Retry count"
@@ -9704,7 +9968,7 @@ msgstr ""
msgid "Geo|Reverify"
msgstr ""
-msgid "Geo|Reverify all projects"
+msgid "Geo|Reverify all"
msgstr ""
msgid "Geo|Status"
@@ -10046,9 +10310,6 @@ msgstr ""
msgid "Go to file"
msgstr ""
-msgid "Go to file (MRs only)"
-msgstr ""
-
msgid "Go to file permalink (while viewing a file)"
msgstr ""
@@ -10493,6 +10754,9 @@ msgstr ""
msgid "GroupSettings|Disable group mentions"
msgstr ""
+msgid "GroupSettings|Export group"
+msgstr ""
+
msgid "GroupSettings|If the parent group's visibility is lower than the group current visibility, visibility levels for subgroups and projects will be changed to match the new parent group's visibility."
msgstr ""
@@ -10571,6 +10835,9 @@ msgstr ""
msgid "Groups (%{count})"
msgstr ""
+msgid "Groups (%{groups})"
+msgstr ""
+
msgid "Groups can also be nested by creating %{subgroup_docs_link_start}subgroups%{subgroup_docs_link_end}."
msgstr ""
@@ -10838,15 +11105,12 @@ msgstr ""
msgid "ID:"
msgstr ""
-msgid "IDE|Allow live previews of JavaScript projects in the Web IDE using CodeSandbox client side evaluation."
+msgid "IDE|Allow live previews of JavaScript projects in the Web IDE using CodeSandbox Live Preview."
msgstr ""
msgid "IDE|Back"
msgstr ""
-msgid "IDE|Client side evaluation"
-msgstr ""
-
msgid "IDE|Commit"
msgstr ""
@@ -10910,6 +11174,9 @@ msgstr ""
msgid "If any job surpasses this timeout threshold, it will be marked as failed. Human readable time input language is accepted like \"1 hour\". Values without specification represent seconds."
msgstr ""
+msgid "If blank, set allowable lifetime to %{instance_level_policy_in_words}, as defined by the instance admin. Once set, existing tokens for users in this group may be revoked."
+msgstr ""
+
msgid "If checked, group owners can manage LDAP group links and LDAP member overrides"
msgstr ""
@@ -10949,6 +11216,9 @@ msgstr ""
msgid "If you lose your recovery codes you can generate new ones, invalidating all previous codes."
msgstr ""
+msgid "If you reach 100%% storage capacity, you will not be able to: %{base_message}"
+msgstr ""
+
msgid "If your HTTP repository is not publicly accessible, add your credentials."
msgstr ""
@@ -11060,6 +11330,9 @@ msgstr ""
msgid "Import repository"
msgstr ""
+msgid "Import started by: %{importInitiator}"
+msgstr ""
+
msgid "Import tasks"
msgstr ""
@@ -11126,6 +11399,9 @@ msgstr ""
msgid "In order to tailor your experience with GitLab we<br>would like to know a bit more about you."
msgstr ""
+msgid "In progress"
+msgstr ""
+
msgid "In the next step, you'll be able to select the projects you want to import."
msgstr ""
@@ -11474,15 +11750,15 @@ msgstr ""
msgid "Issues"
msgstr ""
-msgid "Issues / Merge Requests"
-msgstr ""
-
msgid "Issues Analytics"
msgstr ""
msgid "Issues Rate Limits"
msgstr ""
+msgid "Issues and Merge Requests"
+msgstr ""
+
msgid "Issues can be bugs, tasks or ideas to be discussed. Also, issues are searchable and filterable."
msgstr ""
@@ -11504,10 +11780,10 @@ msgstr ""
msgid "IssuesAnalytics|Avg/Month:"
msgstr ""
-msgid "IssuesAnalytics|Issues created"
+msgid "IssuesAnalytics|Issues opened"
msgstr ""
-msgid "IssuesAnalytics|Issues created per month"
+msgid "IssuesAnalytics|Issues opened per month"
msgstr ""
msgid "IssuesAnalytics|Last 12 months"
@@ -11561,6 +11837,9 @@ msgstr ""
msgid "Jira integration not configured."
msgstr ""
+msgid "Jira project: %{importProject}"
+msgstr ""
+
msgid "JiraService|Events for %{noteable_model_name} are disabled."
msgstr ""
@@ -11951,6 +12230,9 @@ msgstr ""
msgid "Lead"
msgstr ""
+msgid "Lead Time"
+msgstr ""
+
msgid "Learn GitLab"
msgstr ""
@@ -11963,6 +12245,9 @@ msgstr ""
msgid "Learn how to %{no_packages_link_start}publish and share your packages%{no_packages_link_end} with GitLab."
msgstr ""
+msgid "Learn how to enable synchronization"
+msgstr ""
+
msgid "Learn more"
msgstr ""
@@ -12299,9 +12584,6 @@ msgstr ""
msgid "Live preview"
msgstr ""
-msgid "Load more vulnerabilities"
-msgstr ""
-
msgid "Loading"
msgstr ""
@@ -12452,9 +12734,6 @@ msgstr ""
msgid "Manage"
msgstr ""
-msgid "Manage Git repositories with fine-grained access controls that keep your code secure. Perform code reviews and enhance collaboration with merge requests. Each project can also have an issue tracker and a wiki."
-msgstr ""
-
msgid "Manage Web IDE features"
msgstr ""
@@ -12485,6 +12764,9 @@ msgstr ""
msgid "Manage your license"
msgstr ""
+msgid "Managed Account"
+msgstr ""
+
msgid "Manifest"
msgstr ""
@@ -12749,6 +13031,9 @@ msgstr ""
msgid "Merge Requests in Review"
msgstr ""
+msgid "Merge automatically (%{strategy})"
+msgstr ""
+
msgid "Merge commit message"
msgstr ""
@@ -12902,6 +13187,12 @@ msgstr ""
msgid "Merged branches are being deleted. This can take some time depending on the number of branches. Please refresh the page to see changes."
msgstr ""
+msgid "Merged this merge request."
+msgstr ""
+
+msgid "Merges this merge request immediately."
+msgstr ""
+
msgid "Merges this merge request when the pipeline succeeds."
msgstr ""
@@ -13010,12 +13301,15 @@ msgstr[0] ""
msgstr[1] ""
msgstr[2] ""
-msgid "Metrics|Environment"
+msgid "Metrics|Expand panel"
msgstr ""
msgid "Metrics|For grouping similar metrics"
msgstr ""
+msgid "Metrics|Go back (Esc)"
+msgstr ""
+
msgid "Metrics|Invalid time range, please verify."
msgstr ""
@@ -13046,9 +13340,6 @@ msgstr ""
msgid "Metrics|Refresh dashboard"
msgstr ""
-msgid "Metrics|Show last"
-msgstr ""
-
msgid "Metrics|There was an error creating the dashboard."
msgstr ""
@@ -13127,6 +13418,9 @@ msgstr ""
msgid "Microsoft Azure"
msgstr ""
+msgid "Middleman project with Static Site Editor support"
+msgstr ""
+
msgid "Migrated %{success_count}/%{total_count} files."
msgstr ""
@@ -13574,10 +13868,10 @@ msgstr ""
msgid "Next"
msgstr ""
-msgid "Next file in diff (MRs only)"
+msgid "Next file in diff"
msgstr ""
-msgid "Next unresolved discussion (MRs only)"
+msgid "Next unresolved discussion"
msgstr ""
msgid "Nickname"
@@ -13637,6 +13931,9 @@ msgstr ""
msgid "No changes between %{ref_start}%{source_branch}%{ref_end} and %{ref_start}%{target_branch}%{ref_end}"
msgstr ""
+msgid "No child epics match applied filters"
+msgstr ""
+
msgid "No connection could be made to a Gitaly Server, please check your logs!"
msgstr ""
@@ -13775,15 +14072,6 @@ msgstr ""
msgid "No thanks, don't show this again"
msgstr ""
-msgid "No vulnerabilities found for this group"
-msgstr ""
-
-msgid "No vulnerabilities found for this pipeline"
-msgstr ""
-
-msgid "No vulnerabilities found for this project"
-msgstr ""
-
msgid "No vulnerabilities present"
msgstr ""
@@ -13907,6 +14195,9 @@ msgstr ""
msgid "Nothing to preview."
msgstr ""
+msgid "Nothing to synchronize"
+msgstr ""
+
msgid "Notification events"
msgstr ""
@@ -14078,6 +14369,9 @@ msgstr ""
msgid "Once removed, the fork relationship cannot be restored and you will no longer be able to send merge requests to the source."
msgstr ""
+msgid "Once the exported file is ready you can download it from this page."
+msgstr ""
+
msgid "Once the exported file is ready, you will receive a notification email with a download link, or you can download it from this page."
msgstr ""
@@ -14180,9 +14474,6 @@ msgstr ""
msgid "Open sidebar"
msgstr ""
-msgid "Open source software to collaborate on code"
-msgstr ""
-
msgid "Open: %{openIssuesCount}"
msgstr ""
@@ -14285,6 +14576,9 @@ msgstr ""
msgid "OutdatedBrowser|You can provide feedback %{feedback_link_start}on this issue%{feedback_link_end} or via your usual support channels."
msgstr ""
+msgid "Overridden"
+msgstr ""
+
msgid "Overview"
msgstr ""
@@ -14336,6 +14630,9 @@ msgstr ""
msgid "PackageRegistry|Conan Command"
msgstr ""
+msgid "PackageRegistry|Copy .pypirc content"
+msgstr ""
+
msgid "PackageRegistry|Copy Conan Command"
msgstr ""
@@ -14357,6 +14654,9 @@ msgstr ""
msgid "PackageRegistry|Copy NuGet Setup Command"
msgstr ""
+msgid "PackageRegistry|Copy Pip command"
+msgstr ""
+
msgid "PackageRegistry|Copy and paste this inside your %{codeStart}pom.xml%{codeEnd} %{codeStart}dependencies%{codeEnd} block."
msgstr ""
@@ -14390,6 +14690,12 @@ msgstr ""
msgid "PackageRegistry|For more information on the NuGet registry, %{linkStart}see the documentation%{linkEnd}."
msgstr ""
+msgid "PackageRegistry|For more information on the PyPi registry, %{linkStart}see the documentation%{linkEnd}."
+msgstr ""
+
+msgid "PackageRegistry|If you haven't already done so, you will need to add the below to your %{codeStart}.pypirc%{codeEnd} file."
+msgstr ""
+
msgid "PackageRegistry|If you haven't already done so, you will need to add the below to your %{codeStart}pom.xml%{codeEnd} file."
msgstr ""
@@ -14399,6 +14705,9 @@ msgstr ""
msgid "PackageRegistry|Learn how to %{noPackagesLinkStart}publish and share your packages%{noPackagesLinkEnd} with GitLab."
msgstr ""
+msgid "PackageRegistry|Manually Published"
+msgstr ""
+
msgid "PackageRegistry|Maven"
msgstr ""
@@ -14417,12 +14726,18 @@ msgstr ""
msgid "PackageRegistry|NuGet Command"
msgstr ""
+msgid "PackageRegistry|Pip Command"
+msgstr ""
+
msgid "PackageRegistry|Pipeline %{linkStart}%{linkEnd} triggered %{timestamp} by %{author}"
msgstr ""
msgid "PackageRegistry|Published to the repository at %{timestamp}"
msgstr ""
+msgid "PackageRegistry|PyPi"
+msgstr ""
+
msgid "PackageRegistry|Registry Setup"
msgstr ""
@@ -14459,6 +14774,9 @@ msgstr ""
msgid "PackageRegistry|npm"
msgstr ""
+msgid "PackageRegistry|published by %{author}"
+msgstr ""
+
msgid "PackageRegistry|yarn"
msgstr ""
@@ -14474,6 +14792,9 @@ msgstr ""
msgid "PackageType|NuGet"
msgstr ""
+msgid "PackageType|PyPi"
+msgstr ""
+
msgid "Packages"
msgstr ""
@@ -14567,6 +14888,9 @@ msgstr ""
msgid "Past due"
msgstr ""
+msgid "Paste a machine public key here. Read more about how to generate it"
+msgstr ""
+
msgid "Paste a machine public key here. Read more about how to generate it %{link_start}here%{link_end}"
msgstr ""
@@ -14582,9 +14906,6 @@ msgstr ""
msgid "Path"
msgstr ""
-msgid "Path, transfer, remove"
-msgstr ""
-
msgid "Path:"
msgstr ""
@@ -14615,7 +14936,7 @@ msgstr ""
msgid "Percentage"
msgstr ""
-msgid "Perform advanced options such as changing path, transferring, or removing the group."
+msgid "Perform advanced options such as changing path, transferring, exporting, or removing the group."
msgstr ""
msgid "Perform common operations on GitLab project"
@@ -14696,9 +15017,6 @@ msgstr ""
msgid "Pipeline minutes quota"
msgstr ""
-msgid "Pipeline quota"
-msgstr ""
-
msgid "Pipeline subscriptions"
msgstr ""
@@ -14708,6 +15026,9 @@ msgstr ""
msgid "Pipeline: %{status}"
msgstr ""
+msgid "PipelineCharts|CI / CD Analytics"
+msgstr ""
+
msgid "PipelineCharts|Failed:"
msgstr ""
@@ -14786,15 +15107,6 @@ msgstr ""
msgid "Pipelines settings for '%{project_name}' were successfully updated."
msgstr ""
-msgid "Pipelines| to purchase more minutes."
-msgstr ""
-
-msgid "Pipelines|%{namespace_name} has exceeded its pipeline minutes quota."
-msgstr ""
-
-msgid "Pipelines|%{namespace_name} has less than %{notification_level}%% of CI minutes available."
-msgstr ""
-
msgid "Pipelines|API"
msgstr ""
@@ -14816,10 +15128,13 @@ msgstr ""
msgid "Pipelines|Get started with Pipelines"
msgstr ""
-msgid "Pipelines|Loading Pipelines"
+msgid "Pipelines|Group %{namespace_name} has %{percentage}%% or less Shared Runner Pipeline minutes remaining. Once it runs out, no new jobs or pipelines in its projects will run."
msgstr ""
-msgid "Pipelines|Pipelines will not run anymore on shared Runners."
+msgid "Pipelines|Group %{namespace_name} has exceeded its pipeline minutes quota. Unless you buy additional pipeline minutes, no new jobs or pipelines in its projects will run."
+msgstr ""
+
+msgid "Pipelines|Loading Pipelines"
msgstr ""
msgid "Pipelines|Project cache successfully reset."
@@ -15050,6 +15365,9 @@ msgstr ""
msgid "Please select"
msgstr ""
+msgid "Please select a Jira project"
+msgstr ""
+
msgid "Please select a country"
msgstr ""
@@ -15095,6 +15413,9 @@ msgstr ""
msgid "Please wait while we import the repository for you. Refresh at will."
msgstr ""
+msgid "Plugins directory is deprecated and will be removed in 14.0. Please move this file into /file_hooks directory."
+msgstr ""
+
msgid "Pod does not exist"
msgstr ""
@@ -15212,7 +15533,7 @@ msgstr ""
msgid "Prevent users from changing their profile name"
msgstr ""
-msgid "Prevent users from modifing merge request approvers list"
+msgid "Prevent users from modifying merge request approvers list"
msgstr ""
msgid "Prevent users from performing write operations on GitLab while performing maintenance."
@@ -15233,10 +15554,10 @@ msgstr ""
msgid "Previous Artifacts"
msgstr ""
-msgid "Previous file in diff (MRs only)"
+msgid "Previous file in diff"
msgstr ""
-msgid "Previous unresolved discussion (MRs only)"
+msgid "Previous unresolved discussion"
msgstr ""
msgid "Primary"
@@ -15269,6 +15590,9 @@ msgstr ""
msgid "Private profile"
msgstr ""
+msgid "Private projects Minutes cost factor"
+msgstr ""
+
msgid "Private projects can be created in your personal namespace with:"
msgstr ""
@@ -16097,6 +16421,9 @@ msgstr ""
msgid "ProjectSettings|With GitLab Pages you can host your static websites on GitLab"
msgstr ""
+msgid "ProjectSettings|With Metrics Dashboard you can visualize this project performance metrics"
+msgstr ""
+
msgid "ProjectTemplates|.NET Core"
msgstr ""
@@ -16160,6 +16487,9 @@ msgstr ""
msgid "ProjectTemplates|Spring"
msgstr ""
+msgid "ProjectTemplates|Static Site Editor/Middleman"
+msgstr ""
+
msgid "ProjectTemplates|iOS (Swift)"
msgstr ""
@@ -16379,13 +16709,13 @@ msgstr ""
msgid "PrometheusService|Prometheus is being automatically managed on your clusters"
msgstr ""
-msgid "PrometheusService|These metrics will only be monitored after your first deployment to an environment"
+msgid "PrometheusService|Select the Active checkbox to override the Auto Configuration with custom settings. If unchecked, Auto Configuration settings are used."
msgstr ""
-msgid "PrometheusService|Time-series monitoring service"
+msgid "PrometheusService|These metrics will only be monitored after your first deployment to an environment"
msgstr ""
-msgid "PrometheusService|To enable manual configuration, uninstall Prometheus from your clusters"
+msgid "PrometheusService|Time-series monitoring service"
msgstr ""
msgid "PrometheusService|To enable the installation of Prometheus on your clusters, deactivate the manual configuration below"
@@ -16571,6 +16901,9 @@ msgstr ""
msgid "Protip:"
msgstr ""
+msgid "Protocol"
+msgstr ""
+
msgid "Provider"
msgstr ""
@@ -16595,6 +16928,9 @@ msgstr ""
msgid "Public pipelines"
msgstr ""
+msgid "Public projects Minutes cost factor"
+msgstr ""
+
msgid "Pull"
msgstr ""
@@ -16796,6 +17132,9 @@ msgstr[0] ""
msgstr[1] ""
msgstr[2] ""
+msgid "Regenerate export"
+msgstr ""
+
msgid "Regenerate instance ID"
msgstr ""
@@ -17174,9 +17513,30 @@ msgstr ""
msgid "Reports|%{combinedString} and %{resolvedString}"
msgstr ""
+msgid "Reports|Accessibility scanning detected %d issue for the source branch only"
+msgid_plural "Reports|Accessibility scanning detected %d issues for the source branch only"
+msgstr[0] ""
+msgstr[1] ""
+msgstr[2] ""
+
+msgid "Reports|Accessibility scanning detected no issues for the source branch only"
+msgstr ""
+
+msgid "Reports|Accessibility scanning failed loading results"
+msgstr ""
+
+msgid "Reports|Accessibility scanning results are being parsed"
+msgstr ""
+
msgid "Reports|Actions"
msgstr ""
+msgid "Reports|An error occured while loading report"
+msgstr ""
+
+msgid "Reports|An error occurred while loading %{name} results"
+msgstr ""
+
msgid "Reports|Class"
msgstr ""
@@ -17282,7 +17642,7 @@ msgstr ""
msgid "Requested %{time_ago}"
msgstr ""
-msgid "Requested design version does not exist"
+msgid "Requested design version does not exist."
msgstr ""
msgid "Requested states are invalid"
@@ -17468,6 +17828,9 @@ msgstr ""
msgid "Resync"
msgstr ""
+msgid "Resync all"
+msgstr ""
+
msgid "Resync all %{replicableType}"
msgstr ""
@@ -17537,6 +17900,9 @@ msgstr ""
msgid "Revoked personal access token %{personal_access_token_name}!"
msgstr ""
+msgid "Revoked project access token %{project_access_token_name}!"
+msgstr ""
+
msgid "RightSidebar|adding a"
msgstr ""
@@ -17585,6 +17951,9 @@ msgstr ""
msgid "Runner token"
msgstr ""
+msgid "Runner tokens"
+msgstr ""
+
msgid "Runner was not updated."
msgstr ""
@@ -17639,6 +18008,9 @@ msgstr ""
msgid "SAML SSO for %{group_name}"
msgstr ""
+msgid "SAML discovery tokens"
+msgstr ""
+
msgid "SAML for %{group_name}"
msgstr ""
@@ -17723,12 +18095,18 @@ msgstr ""
msgid "Scheduled"
msgstr ""
+msgid "Scheduled to merge this merge request (%{strategy})."
+msgstr ""
+
msgid "Scheduled to merge this merge request when the pipeline succeeds."
msgstr ""
msgid "Schedules"
msgstr ""
+msgid "Schedules to merge this merge request (%{strategy})."
+msgstr ""
+
msgid "Scheduling"
msgstr ""
@@ -17786,6 +18164,9 @@ msgstr ""
msgid "Search branches and tags"
msgstr ""
+msgid "Search by author"
+msgstr ""
+
msgid "Search files"
msgstr ""
@@ -17939,12 +18320,6 @@ msgstr[0] ""
msgstr[1] ""
msgstr[2] ""
-msgid "SearchResults|snippet result"
-msgid_plural "SearchResults|snippet results"
-msgstr[0] ""
-msgstr[1] ""
-msgstr[2] ""
-
msgid "SearchResults|user"
msgid_plural "SearchResults|users"
msgstr[0] ""
@@ -17957,6 +18332,9 @@ msgstr[0] ""
msgstr[1] ""
msgstr[2] ""
+msgid "Searching by both author and message is currently not supported."
+msgstr ""
+
msgid "Seat Link"
msgstr ""
@@ -17984,184 +18362,232 @@ msgstr ""
msgid "Security Dashboard"
msgstr ""
-msgid "Security Dashboard|Error fetching the vulnerability counts. Please check your network connection and try again."
+msgid "Security configuration help link"
msgstr ""
-msgid "Security Dashboard|Error fetching the vulnerability list. Please check your network connection and try again."
+msgid "Security dashboard"
msgstr ""
-msgid "Security Dashboard|Issue Created"
+msgid "Security report is out of date. Please update your branch with the latest changes from the target branch (%{targetBranchName})"
msgstr ""
-msgid "Security Reports|Comment added to '%{vulnerabilityName}'"
+msgid "Security report is out of date. Run %{newPipelineLinkStart}a new pipeline%{newPipelineLinkEnd} for the target branch (%{targetBranchName})"
msgstr ""
-msgid "Security Reports|Comment deleted on '%{vulnerabilityName}'"
+msgid "SecurityConfiguration|Configured"
msgstr ""
-msgid "Security Reports|Comment edited on '%{vulnerabilityName}'"
+msgid "SecurityConfiguration|Feature"
msgstr ""
-msgid "Security Reports|Create issue"
+msgid "SecurityConfiguration|Feature documentation for %{featureName}"
msgstr ""
-msgid "Security Reports|Dismiss vulnerability"
+msgid "SecurityConfiguration|Not yet configured"
msgstr ""
-msgid "Security Reports|Dismissed '%{vulnerabilityName}'"
+msgid "SecurityConfiguration|Secure features"
msgstr ""
-msgid "Security Reports|Dismissed '%{vulnerabilityName}'. Turn off the hide dismissed toggle to view."
+msgid "SecurityConfiguration|Status"
msgstr ""
-msgid "Security Reports|Either you don't have permission to view this dashboard or the dashboard has not been setup. Please check your permission settings with your administrator or check your dashboard configurations to proceed."
+msgid "SecurityReports|%{firstProject} and %{secondProject}"
msgstr ""
-msgid "Security Reports|Learn more about setting up your dashboard"
+msgid "SecurityReports|%{firstProject}, %{secondProject}, and %{rest}"
msgstr ""
-msgid "Security Reports|More info"
+msgid "SecurityReports|Add a project to your dashboard"
msgstr ""
-msgid "Security Reports|Oops, something doesn't seem right."
+msgid "SecurityReports|Add or remove projects from your dashboard"
msgstr ""
-msgid "Security Reports|Security reports can only be accessed by authorized users."
+msgid "SecurityReports|Add projects"
msgstr ""
-msgid "Security Reports|There was an error adding the comment."
+msgid "SecurityReports|Comment added to '%{vulnerabilityName}'"
msgstr ""
-msgid "Security Reports|There was an error creating the issue."
+msgid "SecurityReports|Comment deleted on '%{vulnerabilityName}'"
msgstr ""
-msgid "Security Reports|There was an error creating the merge request."
+msgid "SecurityReports|Comment edited on '%{vulnerabilityName}'"
msgstr ""
-msgid "Security Reports|There was an error deleting the comment."
+msgid "SecurityReports|Create issue"
msgstr ""
-msgid "Security Reports|There was an error dismissing the vulnerabilities."
+msgid "SecurityReports|Dismiss Selected"
msgstr ""
-msgid "Security Reports|There was an error dismissing the vulnerability."
+msgid "SecurityReports|Dismiss vulnerability"
msgstr ""
-msgid "Security Reports|There was an error reverting the dismissal."
+msgid "SecurityReports|Dismissed '%{vulnerabilityName}'"
msgstr ""
-msgid "Security Reports|There was an error reverting this dismissal."
+msgid "SecurityReports|Dismissed '%{vulnerabilityName}'. Turn off the hide dismissed toggle to view."
msgstr ""
-msgid "Security Reports|Undo dismiss"
+msgid "SecurityReports|Each vulnerability now has a unique page that can be directly linked to, shared, referenced, and tracked as the single source of truth. Vulnerability occurrences also persist across scanner runs, which improves tracking and visibility and reduces duplicates between scans."
msgstr ""
-msgid "Security Reports|You do not have sufficient permissions to access this report"
+msgid "SecurityReports|Edit dashboard"
msgstr ""
-msgid "Security Reports|You must sign in as an authorized user to see this report"
+msgid "SecurityReports|Either you don't have permission to view this dashboard or the dashboard has not been setup. Please check your permission settings with your administrator or check your dashboard configurations to proceed."
msgstr ""
-msgid "Security configuration help link"
+msgid "SecurityReports|Error fetching the vulnerability counts. Please check your network connection and try again."
msgstr ""
-msgid "Security dashboard"
+msgid "SecurityReports|Error fetching the vulnerability list. Please check your network connection and try again."
msgstr ""
-msgid "Security report is out of date. Please update your branch with the latest changes from the target branch (%{targetBranchName})"
+msgid "SecurityReports|False positive"
msgstr ""
-msgid "Security report is out of date. Run %{newPipelineLinkStart}a new pipeline%{newPipelineLinkEnd} for the target branch (%{targetBranchName})"
+msgid "SecurityReports|Hide dismissed"
msgstr ""
-msgid "SecurityConfiguration|Configured"
+msgid "SecurityReports|Introducing standalone vulnerabilities"
msgstr ""
-msgid "SecurityConfiguration|Feature"
+msgid "SecurityReports|Issue Created"
msgstr ""
-msgid "SecurityConfiguration|Feature documentation for %{featureName}"
+msgid "SecurityReports|Learn More"
msgstr ""
-msgid "SecurityConfiguration|Not yet configured"
+msgid "SecurityReports|Learn more about setting up your dashboard"
msgstr ""
-msgid "SecurityConfiguration|Secure features"
+msgid "SecurityReports|Load more vulnerabilities"
msgstr ""
-msgid "SecurityConfiguration|Status"
+msgid "SecurityReports|Monitor vulnerabilities in your code"
+msgstr ""
+
+msgid "SecurityReports|More info"
+msgstr ""
+
+msgid "SecurityReports|More information"
+msgstr ""
+
+msgid "SecurityReports|No vulnerabilities found for dashboard"
+msgstr ""
+
+msgid "SecurityReports|No vulnerabilities found for this group"
+msgstr ""
+
+msgid "SecurityReports|No vulnerabilities found for this pipeline"
+msgstr ""
+
+msgid "SecurityReports|No vulnerabilities found for this project"
+msgstr ""
+
+msgid "SecurityReports|Oops, something doesn't seem right."
+msgstr ""
+
+msgid "SecurityReports|Pipeline %{pipelineLink} triggered %{timeago} by %{user}"
msgstr ""
-msgid "SecurityDashboard|%{firstProject} and %{secondProject}"
+msgid "SecurityReports|Project"
msgstr ""
-msgid "SecurityDashboard|%{firstProject}, %{secondProject}, and %{rest}"
+msgid "SecurityReports|Projects added"
msgstr ""
-msgid "SecurityDashboard|Add a project to your dashboard"
+msgid "SecurityReports|Remove project from dashboard"
msgstr ""
-msgid "SecurityDashboard|Add or remove projects from your dashboard"
+msgid "SecurityReports|Report type"
msgstr ""
-msgid "SecurityDashboard|Add projects"
+msgid "SecurityReports|Return to dashboard"
msgstr ""
-msgid "SecurityDashboard|Edit dashboard"
+msgid "SecurityReports|Security Dashboard"
msgstr ""
-msgid "SecurityDashboard|Hide dismissed"
+msgid "SecurityReports|Security reports can only be accessed by authorized users."
msgstr ""
-msgid "SecurityDashboard|Monitor vulnerabilities in your code"
+msgid "SecurityReports|Select a project to add by using the project search field above."
msgstr ""
-msgid "SecurityDashboard|More information"
+msgid "SecurityReports|Select a reason"
msgstr ""
-msgid "SecurityDashboard|No vulnerabilities found for dashboard"
+msgid "SecurityReports|Severity"
msgstr ""
-msgid "SecurityDashboard|Pipeline %{pipelineLink} triggered %{timeago} by %{user}"
+msgid "SecurityReports|Status"
msgstr ""
-msgid "SecurityDashboard|Project"
+msgid "SecurityReports|The security dashboard displays the latest security findings for projects you wish to monitor. Select \"Edit dashboard\" to add and remove projects."
msgstr ""
-msgid "SecurityDashboard|Projects added"
+msgid "SecurityReports|The security dashboard displays the latest security report. Use it to find and fix vulnerabilities."
msgstr ""
-msgid "SecurityDashboard|Remove project from dashboard"
+msgid "SecurityReports|There was an error adding the comment."
msgstr ""
-msgid "SecurityDashboard|Report type"
+msgid "SecurityReports|There was an error creating the issue."
msgstr ""
-msgid "SecurityDashboard|Return to dashboard"
+msgid "SecurityReports|There was an error creating the merge request."
msgstr ""
-msgid "SecurityDashboard|Security Dashboard"
+msgid "SecurityReports|There was an error deleting the comment."
msgstr ""
-msgid "SecurityDashboard|Select a project to add by using the project search field above."
+msgid "SecurityReports|There was an error dismissing the vulnerabilities."
msgstr ""
-msgid "SecurityDashboard|Severity"
+msgid "SecurityReports|There was an error dismissing the vulnerability."
msgstr ""
-msgid "SecurityDashboard|Status"
+msgid "SecurityReports|There was an error reverting the dismissal."
msgstr ""
-msgid "SecurityDashboard|The security dashboard displays the latest security findings for projects you wish to monitor. Select \"Edit dashboard\" to add and remove projects."
+msgid "SecurityReports|There was an error reverting this dismissal."
msgstr ""
-msgid "SecurityDashboard|The security dashboard displays the latest security report. Use it to find and fix vulnerabilities."
+msgid "SecurityReports|There was an error while generating the report."
msgstr ""
-msgid "SecurityDashboard|There was an error while generating the report."
+msgid "SecurityReports|Unable to add %{invalidProjects}"
msgstr ""
-msgid "SecurityDashboard|Unable to add %{invalidProjects}"
+msgid "SecurityReports|Undo dismiss"
+msgstr ""
+
+msgid "SecurityReports|While it's rare to have no vulnerabilities for your group, it can happen. In any event, we ask that you double check your settings to make sure you've set up your dashboard correctly."
+msgstr ""
+
+msgid "SecurityReports|While it's rare to have no vulnerabilities for your pipeline, it can happen. In any event, we ask that you double check your settings to make sure all security scanning jobs have passed successfully."
+msgstr ""
+
+msgid "SecurityReports|While it's rare to have no vulnerabilities for your project, it can happen. In any event, we ask that you double check your settings to make sure you've set up your dashboard correctly."
+msgstr ""
+
+msgid "SecurityReports|While it's rare to have no vulnerabilities, it can happen. In any event, we ask that you please double check your settings to make sure you've set up your dashboard correctly."
+msgstr ""
+
+msgid "SecurityReports|Won't fix / Accept risk"
+msgstr ""
+
+msgid "SecurityReports|You do not have sufficient permissions to access this report"
+msgstr ""
+
+msgid "SecurityReports|You must sign in as an authorized user to see this report"
+msgstr ""
+
+msgid "SecurityReports|[No reason]"
msgstr ""
msgid "See GitLab's %{password_policy_guidelines}"
@@ -18173,6 +18599,9 @@ msgstr ""
msgid "See the affected projects in the GitLab admin panel"
msgstr ""
+msgid "See what's new at GitLab"
+msgstr ""
+
msgid "Select"
msgstr ""
@@ -18578,9 +19007,6 @@ msgstr ""
msgid "Set up Jira Integration"
msgstr ""
-msgid "Set up Jira Integration illustration"
-msgstr ""
-
msgid "Set up a %{type} Runner automatically"
msgstr ""
@@ -18665,6 +19091,9 @@ msgstr ""
msgid "Severity: %{severity}"
msgstr ""
+msgid "Shards (%{shards})"
+msgstr ""
+
msgid "Shards to synchronize"
msgstr ""
@@ -18980,6 +19409,9 @@ msgstr ""
msgid "Snowplow"
msgstr ""
+msgid "Some child epics may be hidden due to applied filters"
+msgstr ""
+
msgid "Some email servers do not support overriding the email sender name. Enable this option to include the name of the author of the issue, merge request or comment in the email body instead."
msgstr ""
@@ -19121,6 +19553,9 @@ msgstr ""
msgid "Something went wrong while updating your list settings"
msgstr ""
+msgid "Something went wrong with your automatic subscription renewal"
+msgstr ""
+
msgid "Something went wrong, unable to add %{project} to dashboard"
msgstr ""
@@ -19406,6 +19841,9 @@ msgstr ""
msgid "Stage removed"
msgstr ""
+msgid "Standard"
+msgstr ""
+
msgid "Star a label to make it a priority label. Order the prioritized labels to change their relative priority, by dragging."
msgstr ""
@@ -19529,6 +19967,9 @@ msgstr ""
msgid "Static Application Security Testing (SAST)"
msgstr ""
+msgid "StaticSiteEditor|An error occurred while submitting your changes."
+msgstr ""
+
msgid "StaticSiteEditor|Branch could not be created."
msgstr ""
@@ -19538,18 +19979,30 @@ msgstr ""
msgid "StaticSiteEditor|Could not create merge request."
msgstr ""
+msgid "StaticSiteEditor|Incompatible file content"
+msgstr ""
+
msgid "StaticSiteEditor|Return to site"
msgstr ""
+msgid "StaticSiteEditor|Static site editor"
+msgstr ""
+
msgid "StaticSiteEditor|Success!"
msgstr ""
msgid "StaticSiteEditor|Summary of changes"
msgstr ""
+msgid "StaticSiteEditor|The Static Site Editor is currently configured to only edit Markdown content on pages generated from Middleman. Visit the documentation to learn more about configuring your site to use the Static Site Editor."
+msgstr ""
+
msgid "StaticSiteEditor|Update %{sourcePath} file"
msgstr ""
+msgid "StaticSiteEditor|View documentation"
+msgstr ""
+
msgid "StaticSiteEditor|View merge request"
msgstr ""
@@ -19607,6 +20060,12 @@ msgstr ""
msgid "StatusPage|Status page"
msgstr ""
+msgid "StatusPage|Status page URL"
+msgstr ""
+
+msgid "StatusPage|Status page frontend documentation"
+msgstr ""
+
msgid "StatusPage|To publish incidents to an external status page, GitLab will store a JSON file in your Amazon S3 account in a location accessible to your external status page service. Make sure to also set up %{docsLink}"
msgstr ""
@@ -19955,6 +20414,12 @@ msgstr ""
msgid "Synced"
msgstr ""
+msgid "Synchronization disabled"
+msgstr ""
+
+msgid "Synchronization of container repositories is disabled."
+msgstr ""
+
msgid "System"
msgstr ""
@@ -20108,6 +20573,9 @@ msgstr ""
msgid "Telephone number"
msgstr ""
+msgid "Telephone number (Optional)"
+msgstr ""
+
msgid "Template"
msgstr ""
@@ -20417,6 +20885,9 @@ msgstr ""
msgid "The license was successfully uploaded and is now active. You can see the details below."
msgstr ""
+msgid "The license was successfully uploaded and will be active from %{starts_at}. You can see the details below."
+msgstr ""
+
msgid "The maximum file size allowed is %{size}."
msgstr ""
@@ -20738,6 +21209,9 @@ msgstr ""
msgid "There was an error getting the epic participants."
msgstr ""
+msgid "There was an error importing the Jira project."
+msgstr ""
+
msgid "There was an error loading users activity calendar."
msgstr ""
@@ -20753,7 +21227,7 @@ msgstr ""
msgid "There was an error resetting user pipeline minutes."
msgstr ""
-msgid "There was an error saving this Geo Node"
+msgid "There was an error saving this Geo Node."
msgstr ""
msgid "There was an error saving your changes."
@@ -20816,6 +21290,9 @@ msgstr ""
msgid "They can be managed using the %{link}."
msgstr ""
+msgid "Third Party Advisory Link"
+msgstr ""
+
msgid "Third party offers"
msgstr ""
@@ -20852,6 +21329,9 @@ msgstr ""
msgid "This application will be able to:"
msgstr ""
+msgid "This attachment has been truncated to avoid exceeding the maximum allowed attachment size of 15MB. %{written_count} of %{issues_count} issues have been included. Consider re-exporting with a narrower selection of issues."
+msgstr ""
+
msgid "This block is self-referential"
msgstr ""
@@ -20993,6 +21473,12 @@ msgstr ""
msgid "This issue is confidential"
msgstr ""
+msgid "This issue is currently blocked by the following issues: %{issues}."
+msgstr ""
+
+msgid "This issue is in a child epic of the filtered epic"
+msgstr ""
+
msgid "This issue is locked."
msgstr ""
@@ -21191,7 +21677,10 @@ msgstr ""
msgid "This user will be the author of all events in the activity feed that are the result of an update, like new branches being created or new commits being pushed to existing branches. Upon creation or when reassigning you can only assign yourself to be the mirror user."
msgstr ""
-msgid "This variable can not be masked"
+msgid "This variable can not be masked."
+msgstr ""
+
+msgid "This variable does not match the expected pattern."
msgstr ""
msgid "This will help us personalize your onboarding experience."
@@ -21221,10 +21710,10 @@ msgstr ""
msgid "ThreatMonitoring|Application firewall not detected"
msgstr ""
-msgid "ThreatMonitoring|Container Network Policy"
+msgid "ThreatMonitoring|Container Network Policies are not installed or have been disabled. To view this data, ensure your Network Policies are installed and enabled for your cluster."
msgstr ""
-msgid "ThreatMonitoring|Container NetworkPolicies are not installed or has been disabled. To view this data, ensure you NetworkPolicies are installed and enabled for your cluster."
+msgid "ThreatMonitoring|Container Network Policy"
msgstr ""
msgid "ThreatMonitoring|Container NetworkPolicies not detected"
@@ -21257,7 +21746,7 @@ msgstr ""
msgid "ThreatMonitoring|Something went wrong, unable to fetch statistics"
msgstr ""
-msgid "ThreatMonitoring|The firewall is not installed or has been disabled. To view this data, ensure you firewall is installed and enabled for your cluster."
+msgid "ThreatMonitoring|The firewall is not installed or has been disabled. To view this data, ensure the web application firewall is installed and enabled for your cluster."
msgstr ""
msgid "ThreatMonitoring|The graph below is an overview of traffic coming to your application as tracked by the Web Application Firewall (WAF). View the docs for instructions on how to access the WAF logs to see what type of malicious traffic is trying to access your app. The docs link is also accessible by clicking the \"?\" icon next to the title below."
@@ -21326,6 +21815,9 @@ msgstr ""
msgid "Time in seconds GitLab will wait for a response from the external service. When the service does not respond in time, access will be denied."
msgstr ""
+msgid "Time of import: %{importTime}"
+msgstr ""
+
msgid "Time remaining"
msgstr ""
@@ -21512,7 +22004,7 @@ msgstr ""
msgid "Title:"
msgstr ""
-msgid "Titles and Filenames"
+msgid "Titles and Descriptions"
msgstr ""
msgid "To"
@@ -21719,7 +22211,7 @@ msgstr ""
msgid "Too many projects enabled. You will need to manage them via the console or the API."
msgstr ""
-msgid "Topics"
+msgid "Topics (optional)"
msgstr ""
msgid "Total"
@@ -21881,6 +22373,9 @@ msgstr ""
msgid "Try all GitLab has to offer for 30 days."
msgstr ""
+msgid "Try changing or removing filters."
+msgstr ""
+
msgid "Try to fork again"
msgstr ""
@@ -21974,6 +22469,9 @@ msgstr ""
msgid "Unable to connect to server: %{error}"
msgstr ""
+msgid "Unable to connect to the Jira instance. Please check your Jira integration configuration."
+msgstr ""
+
msgid "Unable to convert Kubernetes logs encoding to UTF-8"
msgstr ""
@@ -22919,6 +23417,9 @@ msgstr ""
msgid "View issue"
msgstr ""
+msgid "View issues"
+msgstr ""
+
msgid "View it on GitLab"
msgstr ""
@@ -23096,6 +23597,15 @@ msgstr ""
msgid "VulnerabilityManagement|Resolved %{timeago} by %{user}"
msgstr ""
+msgid "VulnerabilityManagement|Something went wrong while trying to delete the comment. Please try again later."
+msgstr ""
+
+msgid "VulnerabilityManagement|Something went wrong while trying to retrieve the vulnerability history. Please try again later."
+msgstr ""
+
+msgid "VulnerabilityManagement|Something went wrong while trying to save the comment. Please try again later."
+msgstr ""
+
msgid "VulnerabilityManagement|Something went wrong, could not create an issue."
msgstr ""
@@ -23126,6 +23636,9 @@ msgstr ""
msgid "VulnerabilityStatusTypes|Resolved"
msgstr ""
+msgid "Vulnerability|%{scannerName} (version %{scannerVersion})"
+msgstr ""
+
msgid "Vulnerability|Class"
msgstr ""
@@ -23159,7 +23672,10 @@ msgstr ""
msgid "Vulnerability|Project"
msgstr ""
-msgid "Vulnerability|Report Type"
+msgid "Vulnerability|Scanner Provider"
+msgstr ""
+
+msgid "Vulnerability|Scanner Type"
msgstr ""
msgid "Vulnerability|Severity"
@@ -23213,9 +23729,15 @@ msgstr ""
msgid "We sent you an email with reset password instructions"
msgstr ""
+msgid "We tried to automatically renew your %{strong}%{plan_name}%{strong_close} subscription for %{strong}%{namespace_name}%{strong_close} on %{expires_on} but something went wrong so your subscription was downgraded to the free plan. Don't worry, your data is safe. We suggest you check your payment method and get in touch with our support team (%{support_link}). They'll gladly help with your subscription renewal."
+msgstr ""
+
msgid "We want to be sure it is you, please confirm you are not a robot."
msgstr ""
+msgid "We will automatically renew your %{strong}%{plan_name}%{strong_close} subscription for %{strong}%{namespace_name}%{strong_close} on %{strong}%{expires_on}%{strong_close}. There's nothing that you need to do, we'll let you know when the renewal is complete. Need more seats, a higher plan or just want to review your payment method?"
+msgstr ""
+
msgid "We've found no vulnerabilities"
msgstr ""
@@ -23321,15 +23843,6 @@ msgstr ""
msgid "When:"
msgstr ""
-msgid "While it's rare to have no vulnerabilities for your group, it can happen. In any event, we ask that you double check your settings to make sure you've set up your dashboard correctly."
-msgstr ""
-
-msgid "While it's rare to have no vulnerabilities for your pipeline, it can happen. In any event, we ask that you double check your settings to make sure all security scanning jobs have passed successfully."
-msgstr ""
-
-msgid "While it's rare to have no vulnerabilities for your project, it can happen. In any event, we ask that you double check your settings to make sure you've set up your dashboard correctly."
-msgstr ""
-
msgid "While it's rare to have no vulnerabilities, it can happen. In any event, we ask that you please double check your settings to make sure you've set up your dashboard correctly."
msgstr ""
@@ -23360,9 +23873,6 @@ msgstr ""
msgid "Wiki"
msgstr ""
-msgid "Wiki pages"
-msgstr ""
-
msgid "Wiki was successfully updated."
msgstr ""
@@ -23546,6 +24056,9 @@ msgstr ""
msgid "Yes, add it"
msgstr ""
+msgid "Yes, close issue"
+msgstr ""
+
msgid "Yes, let me map Google Code users to full names or GitLab users."
msgstr ""
@@ -23741,6 +24254,9 @@ msgstr ""
msgid "You could not create a new trigger."
msgstr ""
+msgid "You didn't renew your %{strong}%{plan_name}%{strong_close} subscription for %{strong}%{namespace_name}%{strong_close} so it was downgraded to the free plan."
+msgstr ""
+
msgid "You didn't renew your %{strong}%{plan_name}%{strong_close} subscription so it was downgraded to the GitLab Core Plan."
msgstr ""
@@ -23807,6 +24323,9 @@ msgstr ""
msgid "You have declined the invitation to join %{label}."
msgstr ""
+msgid "You have imported from this project %{numberOfPreviousImportsForProject} times before. Each new import will create duplicate issues."
+msgstr ""
+
msgid "You have no permissions"
msgstr ""
@@ -23888,6 +24407,9 @@ msgstr ""
msgid "You need to upload a Google Takeout archive."
msgstr ""
+msgid "You reached %{usage_in_percent} of %{namespace_name}'s capacity (%{used_storage} of %{storage_limit})"
+msgstr ""
+
msgid "You tried to fork %{link_to_the_project} but it failed for the following reason:"
msgstr ""
@@ -23972,9 +24494,21 @@ msgstr ""
msgid "YouTube"
msgstr ""
+msgid "Your %{strong}%{plan_name}%{strong_close} subscription for %{strong}%{namespace_name}%{strong_close} will expire on %{strong}%{expires_on}%{strong_close}. After that, you will not to be able to create issues or merge requests as well as many other features."
+msgstr ""
+
msgid "Your %{strong}%{plan_name}%{strong_close} subscription will expire on %{strong}%{expires_on}%{strong_close}. After that, you will not to be able to create issues or merge requests as well as many other features."
msgstr ""
+msgid "Your CSV export has started. It will be emailed to %{email} when complete."
+msgstr ""
+
+msgid "Your CSV export of %{issues_count} from project %{project_link} has been added to this email as an attachment."
+msgstr ""
+
+msgid "Your CSV export of %{written_count} from project %{project_name} (%{project_url}) has been added to this email as an attachment."
+msgstr ""
+
msgid "Your Commit Email will be used for web based operations, such as edits and merges."
msgstr ""
@@ -24071,6 +24605,9 @@ msgstr ""
msgid "Your comment could not be updated! Please check your network connection and try again."
msgstr ""
+msgid "Your comment will be discarded."
+msgstr ""
+
msgid "Your custom stage '%{title}' was created"
msgstr ""
@@ -24110,6 +24647,9 @@ msgstr ""
msgid "Your new personal access token has been created."
msgstr ""
+msgid "Your new project access token has been created."
+msgstr ""
+
msgid "Your password isn't required to view this page. If a password or any other personal details are requested, please contact your administrator to report abuse."
msgstr ""
@@ -24128,12 +24668,18 @@ msgstr ""
msgid "Your request for access has been queued for review."
msgstr ""
+msgid "Your search didn't match any commits."
+msgstr ""
+
msgid "Your subscription expired!"
msgstr ""
msgid "Your subscription has been downgraded"
msgstr ""
+msgid "Your subscription will automatically renew in %{remaining_days}"
+msgstr ""
+
msgid "Your subscription will expire in %{remaining_days}"
msgstr ""
@@ -24173,7 +24719,7 @@ msgstr ""
msgid "allowed to fail"
msgstr ""
-msgid "already being used for another group or project milestone."
+msgid "already being used for another group or project %{timebox_name}."
msgstr ""
msgid "already has a \"created\" issue link"
@@ -24251,6 +24797,9 @@ msgstr ""
msgid "ciReport|%{linkStartTag}Learn more about SAST %{linkEndTag}"
msgstr ""
+msgid "ciReport|%{linkStartTag}Learn more about Secret Scanning %{linkEndTag}"
+msgstr ""
+
msgid "ciReport|%{linkStartTag}Learn more about codequality reports %{linkEndTag}"
msgstr ""
@@ -24419,6 +24968,12 @@ msgstr ""
msgid "ciReport|SAST"
msgstr ""
+msgid "ciReport|Secret scanning"
+msgstr ""
+
+msgid "ciReport|Secret scanning detects secrets and credentials vulnerabilities in your source code."
+msgstr ""
+
msgid "ciReport|Security scanning"
msgstr ""
@@ -24620,6 +25175,9 @@ msgstr ""
msgid "group"
msgstr ""
+msgid "groups"
+msgstr ""
+
msgid "has already been linked to another vulnerability"
msgstr ""
@@ -24785,9 +25343,6 @@ msgstr[2] ""
msgid "merged %{time_ago}"
msgstr ""
-msgid "milestone should belong either to a project or a group."
-msgstr ""
-
msgid "missing"
msgstr ""
@@ -25124,6 +25679,9 @@ msgstr ""
msgid "no contributions"
msgstr ""
+msgid "no expiration"
+msgstr ""
+
msgid "no one can merge"
msgstr ""
@@ -25178,6 +25736,9 @@ msgstr ""
msgid "pending removal"
msgstr ""
+msgid "per day"
+msgstr ""
+
msgid "pipeline"
msgstr ""
@@ -25208,6 +25769,12 @@ msgstr[2] ""
msgid "project avatar"
msgstr ""
+msgid "projects"
+msgstr ""
+
+msgid "push to your repository, create pipelines, create issues or add comments. To reduce storage capacity, delete unused repositories, artifacts, wikis, issues, and pipelines."
+msgstr ""
+
msgid "quick actions"
msgstr ""
@@ -25355,12 +25922,6 @@ msgstr ""
msgid "this document"
msgstr ""
-msgid "thread resolved"
-msgid_plural "threads resolved"
-msgstr[0] ""
-msgstr[1] ""
-msgstr[2] ""
-
msgid "to help your contributors communicate effectively!"
msgstr ""
@@ -25415,6 +25976,9 @@ msgstr ""
msgid "view the blob"
msgstr ""
+msgid "vulnerability|Add a comment"
+msgstr ""
+
msgid "vulnerability|Add a comment or reason for dismissal"
msgstr ""
diff --git a/locale/ru/gitlab.po b/locale/ru/gitlab.po
index ccaba60a9ca..41ab1cab863 100644
--- a/locale/ru/gitlab.po
+++ b/locale/ru/gitlab.po
@@ -12,16 +12,16 @@ msgstr ""
"X-Crowdin-Project: gitlab-ee\n"
"X-Crowdin-Language: ru\n"
"X-Crowdin-File: /master/locale/gitlab.pot\n"
-"PO-Revision-Date: 2020-04-15 00:25\n"
+"PO-Revision-Date: 2020-05-05 22:46\n"
msgid " %{start} to %{end}"
-msgstr ""
+msgstr " %{start} по %{end}"
msgid " (from %{timeoutSource})"
msgstr " (из %{timeoutSource})"
msgid " Collected %{time}"
-msgstr ""
+msgstr " Получено %{time}"
msgid " Please sign in."
msgstr "ПожалуйÑта, войдите."
@@ -75,31 +75,31 @@ msgstr "\"%{path}\" не ÑущеÑтвует на \"%{ref}\""
msgid "%d URL scanned"
msgid_plural "%d URLs scanned"
-msgstr[0] ""
-msgstr[1] ""
-msgstr[2] ""
-msgstr[3] ""
+msgstr[0] "%d URL проÑканирован"
+msgstr[1] "%dURL проÑканировано"
+msgstr[2] "%d URL проÑканировано"
+msgstr[3] "%d URL проÑканировано"
msgid "%d changed file"
msgid_plural "%d changed files"
-msgstr[0] ""
-msgstr[1] ""
-msgstr[2] ""
-msgstr[3] ""
+msgstr[0] "%d измененный файл"
+msgstr[1] "%d измененных файла"
+msgstr[2] "%d измененных файлов"
+msgstr[3] "%d измененных файлов"
msgid "%d child epic"
msgid_plural "%d child epics"
-msgstr[0] ""
-msgstr[1] ""
-msgstr[2] ""
-msgstr[3] ""
+msgstr[0] "%d дочернÑÑ Ñ†ÐµÐ»ÑŒ"
+msgstr[1] "%d дочерние цели"
+msgstr[2] "%d дочерних целей"
+msgstr[3] "%d дочерних целей"
msgid "%d code quality issue"
msgid_plural "%d code quality issues"
-msgstr[0] ""
-msgstr[1] ""
-msgstr[2] ""
-msgstr[3] ""
+msgstr[0] "%d проблем качеÑтва кода"
+msgstr[1] "%d проблемы качеÑтва кода"
+msgstr[2] "%d проблем качеÑтва кода"
+msgstr[3] "%d проблем качеÑтва кода"
msgid "%d comment"
msgid_plural "%d comments"
@@ -141,10 +141,10 @@ msgstr[3] "%d вкладов"
msgid "%d error"
msgid_plural "%d errors"
-msgstr[0] ""
-msgstr[1] ""
-msgstr[2] ""
-msgstr[3] ""
+msgstr[0] "%d ошибка"
+msgstr[1] "%d ошибки"
+msgstr[2] "%d ошибок"
+msgstr[3] "%d ошибок"
msgid "%d exporter"
msgid_plural "%d exporters"
@@ -155,10 +155,10 @@ msgstr[3] "%d ÑкÑпортеров"
msgid "%d failed"
msgid_plural "%d failed"
-msgstr[0] ""
-msgstr[1] ""
-msgstr[2] ""
-msgstr[3] ""
+msgstr[0] "%d провален"
+msgstr[1] "%d провалено"
+msgstr[2] "%d провалено"
+msgstr[3] "%d провалено"
msgid "%d fixed test result"
msgid_plural "%d fixed test results"
@@ -169,10 +169,10 @@ msgstr[3] "%d иÑправленные результаты теÑта"
msgid "%d group selected"
msgid_plural "%d groups selected"
-msgstr[0] ""
-msgstr[1] ""
-msgstr[2] ""
-msgstr[3] ""
+msgstr[0] "%d группа выбрана"
+msgstr[1] "%d группы выбраны"
+msgstr[2] "%d групп выбрано"
+msgstr[3] "%d групп выбрано"
msgid "%d inaccessible merge request"
msgid_plural "%d inaccessible merge requests"
@@ -260,13 +260,20 @@ msgstr[3] "%d Ñекунд"
msgid "%d shard selected"
msgid_plural "%d shards selected"
-msgstr[0] ""
-msgstr[1] ""
-msgstr[2] ""
-msgstr[3] ""
+msgstr[0] "%d шард выбран"
+msgstr[1] "%d шарда выбрано"
+msgstr[2] "%d шардов выбрано"
+msgstr[3] "%d шардов выбрано"
msgid "%d tag"
msgid_plural "%d tags"
+msgstr[0] "%d тег"
+msgstr[1] "%d тега"
+msgstr[2] "%d тегов"
+msgstr[3] "%d тегов"
+
+msgid "%d unresolved thread"
+msgid_plural "%d unresolved threads"
msgstr[0] ""
msgstr[1] ""
msgstr[2] ""
@@ -274,10 +281,10 @@ msgstr[3] ""
msgid "%d vulnerability dismissed"
msgid_plural "%d vulnerabilities dismissed"
-msgstr[0] ""
-msgstr[1] ""
-msgstr[2] ""
-msgstr[3] ""
+msgstr[0] "%d уÑзвимоÑÑ‚ÑŒ отклонена"
+msgstr[1] "%d уÑзвимоÑти отклонено"
+msgstr[2] "%d уÑзвимоÑтей отклонено"
+msgstr[3] "%d уÑзвимоÑтей отклонено"
msgid "%s additional commit has been omitted to prevent performance issues."
msgid_plural "%s additional commits have been omitted to prevent performance issues."
@@ -290,13 +297,13 @@ msgid "%{actionText} & %{openOrClose} %{noteable}"
msgstr "%{actionText} & %{openOrClose} %{noteable}"
msgid "%{authorsName}'s thread"
-msgstr ""
+msgstr "тема %{authorsName}"
msgid "%{commit_author_link} authored %{commit_timeago}"
msgstr "%{commit_author_link} Ñоздал %{commit_timeago}"
msgid "%{completedWeight} of %{totalWeight} weight completed"
-msgstr ""
+msgstr "Завершено %{completedWeight} из %{totalWeight} приоритета"
msgid "%{cores} cores"
msgstr "%{cores} Ñдер"
@@ -354,10 +361,10 @@ msgid "%{count} related %{pluralized_subject}: %{links}"
msgstr "%{count} ÑвÑзанный %{pluralized_subject}: %{links}"
msgid "%{days} days until tags are automatically removed"
-msgstr ""
+msgstr "%{days} дней до автоматичеÑкого ÑƒÐ´Ð°Ð»ÐµÐ½Ð¸Ñ Ñ‚ÐµÐ³Ð¾Ð²"
msgid "%{description}- Sentry event: %{errorUrl}- First seen: %{firstSeen}- Last seen: %{lastSeen} %{countLabel}: %{count}%{userCountLabel}: %{userCount}"
-msgstr ""
+msgstr "%{description}- Событие Sentry: %{errorUrl}- Первый проÑмотр: %{firstSeen}- ПоÑледний проÑмотр: %{lastSeen} %{countLabel}: %{count}%{userCountLabel}: %{userCount}"
msgid "%{duration}ms"
msgstr "%{duration}мÑ"
@@ -369,7 +376,7 @@ msgid "%{edit_in_new_fork_notice} Try to create a new directory again."
msgstr "%{edit_in_new_fork_notice} Попробуйте Ñоздать новый каталог еще раз."
msgid "%{edit_in_new_fork_notice} Try to revert this commit again."
-msgstr ""
+msgstr "%{edit_in_new_fork_notice} Попробуйте откатить Ñтот коммит ещё раз."
msgid "%{edit_in_new_fork_notice} Try to upload a file again."
msgstr "%{edit_in_new_fork_notice} Попробуйте загрузить файл еще раз."
@@ -384,7 +391,7 @@ msgid "%{firstLabel} +%{labelCount} more"
msgstr "%{firstLabel} + ещё %{labelCount}"
msgid "%{global_id} is not a valid id for %{expected_type}."
-msgstr ""
+msgstr "%{global_id} не ÑвлÑетÑÑ Ð´Ð¾Ð¿ÑƒÑтимым ID Ð´Ð»Ñ %{expected_type}."
msgid "%{group_docs_link_start}Groups%{group_docs_link_end} allow you to manage and collaborate across multiple projects. Members of a group have access to all of its projects."
msgstr "%{group_docs_link_start}Группы%{group_docs_link_end} позволÑÑŽÑ‚ управлÑÑ‚ÑŒ неÑколькими проектами и Ñотрудничать Ñ Ð½Ð¸Ð¼Ð¸. Члены группы имеют доÑтуп ко вÑем ее проектам."
@@ -399,25 +406,25 @@ msgid "%{issuableType} will be removed! Are you sure?"
msgstr "%{issuableType} будет удален! Вы уверены?"
msgid "%{issuesSize} issues"
-msgstr ""
+msgstr "%{issuesSize} обÑуждений"
msgid "%{issuesSize} issues with a limit of %{maxIssueCount}"
-msgstr ""
+msgstr "%{issuesSize} обÑуждение Ñ Ð»Ð¸Ð¼Ð¸Ñ‚Ð¾Ð¼ в %{maxIssueCount}"
msgid "%{label_for_message} unavailable"
msgstr "%{label_for_message} недоÑтупно"
msgid "%{lets_encrypt_link_start}Let's Encrypt%{lets_encrypt_link_end} is a free, automated, and open certificate authority (CA), that give digital certificates in order to enable HTTPS (SSL/TLS) for websites."
-msgstr ""
+msgstr "%{lets_encrypt_link_start}Let's Encrypt%{lets_encrypt_link_end} - Ñто беÑплатный, автоматизированный и открытый центр Ñертификации (CA), который предоÑтавлÑет цифровые Ñертификаты Ð´Ð»Ñ Ð²ÐºÐ»ÑŽÑ‡ÐµÐ½Ð¸Ñ HTTPS (SSL/TLS) на веб-Ñайтах."
msgid "%{level_name} is not allowed in a %{group_level_name} group."
msgstr "%{level_name} не допуÑкаетÑÑ Ð² %{group_level_name} группе."
msgid "%{level_name} is not allowed since the fork source project has lower visibility."
-msgstr ""
+msgstr "%{level_name} запрещено, Ñ‚.к. проект-иÑточник Ð¾Ñ‚Ð²ÐµÑ‚Ð²Ð»ÐµÐ½Ð¸Ñ Ð¸Ð¼ÐµÐµÑ‚ более узкую облаÑÑ‚ÑŒ видимоÑти."
msgid "%{lineOneStart}Drag and drop to upload your designs%{lineOneEnd} or %{linkStart}click to upload%{linkEnd}."
-msgstr ""
+msgstr "%{lineOneStart}Перетащите, чтобы загрузить Ñвои дизайны%{lineOneEnd} или %{linkStart}нажмите, чтобы загрузить%{linkEnd}."
msgid "%{link_start}Learn more%{link_end} about what information is shared with GitLab Inc."
msgstr ""
@@ -441,20 +448,30 @@ msgid "%{mergeLength}/%{usersLength} can merge"
msgstr "%{mergeLength}/%{usersLength} можно объединить"
msgid "%{mrText}, this issue will be closed automatically."
+msgstr "%{mrText}, Ñто обÑуждение будет закрыто автоматичеÑки."
+
+msgid "%{namespace_name} is now read-only. You cannot: %{base_message}"
msgstr ""
msgid "%{name} contained %{resultsString}"
-msgstr ""
+msgstr "%{name} Ñодержал %{resultsString}"
msgid "%{name} found %{resultsString}"
-msgstr ""
+msgstr "%{name} нашел %{resultsString}"
msgid "%{name} is scheduled for %{action}"
-msgstr ""
+msgstr "%{name} запланирован на %{action}"
msgid "%{name}'s avatar"
msgstr "Ðватар Ð¿Ð¾Ð»ÑŒÐ·Ð¾Ð²Ð°Ñ‚ÐµÐ»Ñ %{name}"
+msgid "%{no_of_days} day"
+msgid_plural "%{no_of_days} days"
+msgstr[0] ""
+msgstr[1] ""
+msgstr[2] ""
+msgstr[3] ""
+
msgid "%{number_commits_behind} commits behind %{default_branch}, %{number_commits_ahead} commits ahead"
msgstr "на %{number_commits_behind} коммитов позади %{default_branch}, на %{number_commits_ahead} коммитов впереди"
@@ -462,28 +479,28 @@ msgid "%{openOrClose} %{noteable}"
msgstr "%{openOrClose} %{noteable}"
msgid "%{openedEpics} open, %{closedEpics} closed"
-msgstr ""
+msgstr "%{openedEpics} открыто, %{closedEpics} закрыто"
msgid "%{openedIssues} open, %{closedIssues} closed"
-msgstr ""
+msgstr "%{openedIssues} открыто, %{closedIssues} закрыто"
msgid "%{percentage}%% weight completed"
-msgstr ""
+msgstr "%{percentage}%% приоритета завершено"
msgid "%{percent}%% complete"
msgstr "%{percent}%% выполнено"
msgid "%{percent}%{percentSymbol} complete"
-msgstr ""
+msgstr "%{percent}%{percentSymbol} выполнено"
msgid "%{placeholder} is not a valid color scheme"
-msgstr ""
+msgstr "%{placeholder} не ÑвлÑетÑÑ Ð´Ð¾Ð¿ÑƒÑтимой цветовой Ñхемой"
msgid "%{placeholder} is not a valid theme"
-msgstr ""
+msgstr "%{placeholder} не ÑвлÑетÑÑ Ð´Ð¾Ð¿ÑƒÑтимой темой"
msgid "%{primary} (%{secondary})"
-msgstr ""
+msgstr "%{primary} (%{secondary})"
msgid "%{releases} release"
msgid_plural "%{releases} releases"
@@ -499,7 +516,7 @@ msgid "%{screenreaderOnlyStart}Keyboard shorcuts%{screenreaderOnlyEnd} Enabled"
msgstr ""
msgid "%{service_title} %{message}."
-msgstr ""
+msgstr "%{service_title} %{message}."
msgid "%{size} GiB"
msgstr "%{size} ГиБ"
@@ -553,10 +570,10 @@ msgstr ""
msgid "%{strong_start}%{release_count}%{strong_end} Release"
msgid_plural "%{strong_start}%{release_count}%{strong_end} Releases"
-msgstr[0] ""
-msgstr[1] ""
-msgstr[2] ""
-msgstr[3] ""
+msgstr[0] "%{strong_start}%{release_count}%{strong_end} релиз"
+msgstr[1] "%{strong_start}%{release_count}%{strong_end} релиза"
+msgstr[2] "%{strong_start}%{release_count}%{strong_end} релизов"
+msgstr[3] "%{strong_start}%{release_count}%{strong_end} релизов"
msgid "%{strong_start}%{tag_count}%{strong_end} Tag"
msgid_plural "%{strong_start}%{tag_count}%{strong_end} Tags"
@@ -569,13 +586,13 @@ msgid "%{tabname} changed"
msgstr "%{tabname} изменено"
msgid "%{tags} tag per image name"
-msgstr ""
+msgstr "%{tags} тег на Ð¸Ð¼Ñ Ð¾Ð±Ñ€Ð°Ð·Ð°"
msgid "%{tags} tags per image name"
-msgstr ""
+msgstr "%{tags} тегов на Ð¸Ð¼Ñ Ð¾Ð±Ñ€Ð°Ð·Ð°"
msgid "%{tag}-%{evidence}-%{filename}"
-msgstr ""
+msgstr "%{tag}-%{evidence}-%{filename}"
msgid "%{template_project_id} is unknown or invalid"
msgstr "%{template_project_id}' неизвеÑтен или недейÑтвителен"
@@ -583,13 +600,16 @@ msgstr "%{template_project_id}' неизвеÑтен или недейÑтвит
msgid "%{text} %{files}"
msgid_plural "%{text} %{files} files"
msgstr[0] "%{text} %{files}"
-msgstr[1] "%{text} %{files} файлах"
+msgstr[1] "%{text} %{files} файлов"
msgstr[2] "%{text} %{files} файлов"
-msgstr[3] "%{text} %{files} файлах"
+msgstr[3] "%{text} %{files} файлов"
msgid "%{text} is available"
msgstr "%{text} доÑтупен"
+msgid "%{timebox_name} should belong either to a project or a group."
+msgstr ""
+
msgid "%{title} %{operator} %{threshold}"
msgstr "%{title} %{operator} %{threshold}"
@@ -597,13 +617,13 @@ msgid "%{title} changes"
msgstr "Ð˜Ð·Ð¼ÐµÐ½ÐµÐ½Ð¸Ñ Ð² %{title}"
msgid "%{token}..."
-msgstr ""
+msgstr "%{token}..."
msgid "%{totalWeight} total weight"
-msgstr ""
+msgstr "общий приоритет %{totalWeight}"
msgid "%{total} open issue weight"
-msgstr "%{total} Ð²ÐµÑ Ð¾Ñ‚ÐºÑ€Ñ‹Ñ‚Ñ‹Ñ… задач"
+msgstr "приоритет открытого обÑÑƒÐ¶Ð´ÐµÐ½Ð¸Ñ %{total}"
msgid "%{total} open issues"
msgstr "%{total} открытых задач"
@@ -624,7 +644,7 @@ msgid "%{username}'s avatar"
msgstr "аватар %{username}"
msgid "%{value} s"
-msgstr ""
+msgstr "%{value} Ñ"
msgid "%{verb} %{time_spent_value} spent time."
msgstr ""
@@ -633,7 +653,7 @@ msgid "'%{level}' is not a valid visibility level"
msgstr ""
msgid "'%{name}' stage already exists"
-msgstr ""
+msgstr "Этап '%{name}' уже ÑущеÑтвует"
msgid "'%{source}' is not a import source"
msgstr ""
@@ -658,7 +678,7 @@ msgid "(Show all)"
msgstr "(Показать вÑе)"
msgid "(check progress)"
-msgstr ""
+msgstr "(прогреÑÑ Ð¿Ñ€Ð¾Ð²ÐµÑ€ÐºÐ¸)"
msgid "(external source)"
msgstr "(внешний иÑточник)"
@@ -667,10 +687,10 @@ msgid "(removed)"
msgstr "(удалено)"
msgid "(revoked)"
-msgstr ""
+msgstr "(отозван)"
msgid "*"
-msgstr ""
+msgstr "*"
msgid "+ %{amount} more"
msgstr "+%{amount} ещё"
@@ -686,16 +706,16 @@ msgstr "+ %{numberOfHiddenAssignees} больше"
msgid "+%d more"
msgid_plural "+%d more"
-msgstr[0] ""
-msgstr[1] ""
-msgstr[2] ""
-msgstr[3] ""
+msgstr[0] "+ещё %d"
+msgstr[1] "+ещё %d"
+msgstr[2] "+ещё %d"
+msgstr[3] "+ещё %d"
msgid "+%{approvers} more approvers"
-msgstr ""
+msgstr "+ещё %{approvers} утверждающих"
msgid "+%{tags} more"
-msgstr ""
+msgstr "+ещё %{tags}"
msgid ", or "
msgstr ", или "
@@ -721,7 +741,7 @@ msgstr[2] "- Пользователи"
msgstr[3] "- Пользователи"
msgid "- of - weight completed"
-msgstr ""
+msgstr "- из - приоритета завершено"
msgid "- show less"
msgstr "- Ñвернуть"
@@ -766,10 +786,10 @@ msgstr[3] "%{merge_requests} закрытых запроÑов на ÑлиÑни
msgid "1 day"
msgid_plural "%d days"
-msgstr[0] ""
-msgstr[1] ""
-msgstr[2] ""
-msgstr[3] ""
+msgstr[0] "1 день"
+msgstr[1] "%d днÑ"
+msgstr[2] "%d дней"
+msgstr[3] "%d дней"
msgid "1 group"
msgid_plural "%d groups"
@@ -780,10 +800,10 @@ msgstr[3] "%d групп"
msgid "1 hour"
msgid_plural "%d hours"
-msgstr[0] ""
-msgstr[1] ""
-msgstr[2] ""
-msgstr[3] ""
+msgstr[0] "1 чаÑ"
+msgstr[1] "%d чаÑа"
+msgstr[2] "%d чаÑов"
+msgstr[3] "%d чаÑов"
msgid "1 merged merge request"
msgid_plural "%{merge_requests} merged merge requests"
@@ -794,13 +814,13 @@ msgstr[3] "%{merge_requests} объединенные запроÑов на Ñл
msgid "1 minute"
msgid_plural "%d minutes"
-msgstr[0] ""
-msgstr[1] ""
-msgstr[2] ""
-msgstr[3] ""
+msgstr[0] "1 минута"
+msgstr[1] "%d минуты"
+msgstr[2] "%d минут"
+msgstr[3] "%d минут"
msgid "1 month"
-msgstr ""
+msgstr "1 меÑÑц"
msgid "1 open issue"
msgid_plural "%{issues} open issues"
@@ -841,7 +861,7 @@ msgid "1 week"
msgstr "1 неделÑ"
msgid "1-9 contributions"
-msgstr ""
+msgstr "1-9 ÑодейÑтвий"
msgid "10-19 contributions"
msgstr "10-19 ÑодейÑтвий"
@@ -850,7 +870,7 @@ msgid "1st contribution!"
msgstr "Первый вклад!"
msgid "20-29 contributions"
-msgstr ""
+msgstr "20-29 ÑодейÑтвий"
msgid "2FA"
msgstr "Ð”Ð²ÑƒÑ…Ñ„Ð°ÐºÑ‚Ð¾Ñ€Ð½Ð°Ñ Ð°ÑƒÑ‚ÐµÐ½Ñ‚Ð¸Ñ„Ð¸ÐºÐ°Ñ†Ð¸Ñ"
@@ -868,7 +888,7 @@ msgid "30 minutes"
msgstr "30 минут"
msgid "30+ contributions"
-msgstr ""
+msgstr "30+ ÑодейÑтвий"
msgid "403|Please contact your GitLab administrator to get permission."
msgstr "ПожалуйÑта, обратитеÑÑŒ к вашему админиÑтратору GitLab Ð´Ð»Ñ Ð¿Ð¾Ð»ÑƒÑ‡ÐµÐ½Ð¸Ñ Ñ€Ð°Ð·Ñ€ÐµÑˆÐµÐ½Ð¸Ñ."
@@ -889,7 +909,7 @@ msgid "8 hours"
msgstr "8 чаÑов"
msgid "< 1 hour"
-msgstr ""
+msgstr "< 1 чаÑа"
msgid "<code>\"johnsmith@example.com\": \"@johnsmith\"</code> will add \"By <a href=\"#\">@johnsmith</a>\" to all issues and comments originally created by johnsmith@example.com, and will set <a href=\"#\">@johnsmith</a> as the assignee on all issues originally assigned to johnsmith@example.com."
msgstr "<code>\"johnsmith@example.com\": \"@johnsmith\"</code> добавил \"<a href=\"#\">@johnsmith</a>\" По вÑем вопроÑам и комментариÑм, изначально Ñозданным johnsmith@example.com, и уÑтановил <a href=\"#\">@johnsmith</a> в качеÑтве правопреемника по вÑем задачам первоначально назначен johnsmith@example.com."
@@ -903,12 +923,18 @@ msgstr ""
msgid "<code>\"johnsmith@example.com\": \"johnsmith@example.com\"</code> will add \"By <a href=\"#\">johnsmith@example.com</a>\" to all issues and comments originally created by johnsmith@example.com. By default, the email address or username is masked to ensure the user's privacy. Use this option if you want to show the full email address."
msgstr ""
+msgid "<namespace / project>"
+msgstr ""
+
msgid "<no name set>"
msgstr "<no name set>"
msgid "<no scopes selected>"
msgstr "<no scopes selected>"
+msgid "<project name>"
+msgstr ""
+
msgid "<strong>%{group_name}</strong> group members"
msgstr "<strong>%{group_name}</strong> учаÑтников группы"
@@ -937,11 +963,14 @@ msgid "A Let's Encrypt SSL certificate can not be obtained until your domain is
msgstr "SSL Ñертификат Let's Encrypt не может быть получен, пока ваш домен не будет проверен."
msgid "A Let's Encrypt account will be configured for this GitLab installation using your email address. You will receive emails to warn of expiring certificates."
-msgstr "Ð£Ñ‡ÐµÑ‚Ð½Ð°Ñ Ð·Ð°Ð¿Ð¸ÑÑŒ Let's Encrypt будет наÑтроена Ð´Ð»Ñ Ð´Ð°Ð½Ð½Ð¾Ð¹ уÑтановки GitLab Ñ Ð¸Ñпользованием адреÑа вашей Ñлектронной почты. Ð’Ñ‹ получите Ñлектронное пиÑьмо Ñ Ð¿Ñ€ÐµÐ´ÑƒÐ¿Ñ€ÐµÐ¶Ð´ÐµÐ½Ð¸ÐµÐ¼ об иÑтечении Ñрока дейÑÑ‚Ð²Ð¸Ñ Ñертификатов."
+msgstr "Ð£Ñ‡Ñ‘Ñ‚Ð½Ð°Ñ Ð·Ð°Ð¿Ð¸ÑÑŒ Let's Encrypt будет наÑтроена Ð´Ð»Ñ Ð´Ð°Ð½Ð½Ð¾Ð¹ уÑтановки GitLab Ñ Ð¸Ñпользованием адреÑа вашей Ñлектронной почты. Ð’Ñ‹ получите Ñлектронное пиÑьмо Ñ Ð¿Ñ€ÐµÐ´ÑƒÐ¿Ñ€ÐµÐ¶Ð´ÐµÐ½Ð¸ÐµÐ¼ об иÑтечении Ñрока дейÑÑ‚Ð²Ð¸Ñ Ñертификатов."
msgid "A basic page and serverless function that uses AWS Lambda, AWS API Gateway, and GitLab Pages"
msgstr ""
+msgid "A complete DevOps platform"
+msgstr ""
+
msgid "A default branch cannot be chosen for an empty project."
msgstr "Ð”Ð»Ñ Ð¿ÑƒÑтого проекта Ð½ÐµÐ»ÑŒÐ·Ñ Ð²Ñ‹Ð±Ñ€Ð°Ñ‚ÑŒ ветку по умолчанию."
@@ -955,7 +984,7 @@ msgid "A fork is a copy of a project.<br />Forking a repository allows you to ma
msgstr "Ответвление - Ñто ÐºÐ¾Ð¿Ð¸Ñ Ð¿Ñ€Ð¾ÐµÐºÑ‚Ð°.<br />Ответвление Ñ€ÐµÐ¿Ð¾Ð·Ð¸Ñ‚Ð¾Ñ€Ð¸Ñ Ð¿Ð¾Ð·Ð²Ð¾Ð»Ñет вноÑить изменениÑ, не влиÑÑ Ð½Ð° иÑходный проект."
msgid "A group represents your organization in GitLab."
-msgstr ""
+msgstr "Группа предÑтавлÑет вашу организацию в GitLab."
msgid "A member of the abuse team will review your report as soon as possible."
msgstr ""
@@ -964,7 +993,7 @@ msgid "A merge request approval is required when a security report contains a ne
msgstr ""
msgid "A merge request approval is required when the license compliance report contains a blacklisted license."
-msgstr ""
+msgstr "Утверждение запроÑа на ÑлиÑние требуетÑÑ, когда отчёт о комплаенÑе лицензий отноÑитÑÑ Ðº лицензии, включённой в чёрный ÑпиÑок."
msgid "A new Auto DevOps pipeline has been created, go to %{pipelines_link_start}Pipelines page%{pipelines_link_end} for details"
msgstr "ÐÐ¾Ð²Ð°Ñ ÑÐ±Ð¾Ñ€Ð¾Ñ‡Ð½Ð°Ñ Ð»Ð¸Ð½Ð¸Ñ Auto DevOps Ñоздана, обратитеÑÑŒ к %{pipelines_link_start}Ñтранице, поÑвÑщенной Сборочным линиÑм%{pipelines_link_end} за подробноÑÑ‚Ñми"
@@ -1024,7 +1053,7 @@ msgid "API Token"
msgstr "API токен"
msgid "AWS Access Key"
-msgstr ""
+msgstr "Ключ доÑтупа AWS"
msgid "AWS Access Key. Only required if not using role instance credentials"
msgstr "Ключ доÑтупа AWS. ТребуетÑÑ, только еÑли Ð´Ð»Ñ Ð´Ð¾Ñтупа к инÑтанÑу не иÑпользуетÑÑ Ñ€Ð¾Ð»ÑŒ"
@@ -1063,7 +1092,7 @@ msgid "Accept terms"
msgstr "ПринÑÑ‚ÑŒ уÑловиÑ"
msgid "Acceptable for use in this project"
-msgstr ""
+msgstr "Разрешены к иÑпользованию в Ñтом проекте"
msgid "Accepted MR"
msgstr "ПринÑтый Ð·Ð°Ð¿Ñ€Ð¾Ñ Ð½Ð° ÑлиÑние"
@@ -1123,7 +1152,7 @@ msgid "AccessTokens|It cannot be used to access any other data."
msgstr "Его Ð½ÐµÐ»ÑŒÐ·Ñ Ð¸Ñпользовать Ð´Ð»Ñ Ð´Ð¾Ñтупа к любым другим данным."
msgid "AccessTokens|Keep this token secret. Anyone who gets ahold of it can access repository static objects as if they were you. You should %{reset_link_start}reset it%{reset_link_end} if that ever happens."
-msgstr ""
+msgstr "Держите Ñтот токен в Ñекрете. Любой, кто получит его, может Ñоздать обÑуждениÑ, как будто бы Ñто были вы. ЕÑли Ñто когда-либо произойдет, то вам Ñледует %{reset_link_start} ÑброÑить его %{reset_link_end}."
msgid "AccessTokens|Keep this token secret. Anyone who gets ahold of it can create issues as if they were you. You should %{link_reset_it} if that ever happens."
msgstr "Держите Ñтот токен в Ñекрете. Любой, кто получит его, может Ñоздать обÑуждениÑ, как будто бы Ñто были вы. ЕÑли Ñто когда-либо произойдет, то вам Ñледует %{link_reset_it}."
@@ -1153,25 +1182,34 @@ msgid "AccessTokens|Your incoming email token is used to authenticate you when y
msgstr "Ваш токен входÑщей Ñлектронной почты иÑпользуетÑÑ Ð´Ñл аутентификации когда вы Ñоздаете новую задачу при помощи Ñлектронной почты и включен в ваши перÑональные Ñлектронные адреÑа, отноÑÑщиеÑÑ Ðº проекту."
msgid "AccessTokens|Your static object token is used to authenticate you when repository static objects (e.g. archives, blobs, ...) are being served from an external storage."
-msgstr ""
+msgstr "Ваш токен ÑтатичеÑких объектов иÑпользуетÑÑ Ð´Ð»Ñ Ð°ÑƒÑ‚ÐµÐ½Ñ‚Ð¸Ñ„Ð¸ÐºÐ°Ñ†Ð¸Ð¸ при обÑлуживании ÑтатичеÑких объектов (например, архивов, бинарных данных, ...) из внешнего хранилища."
msgid "AccessTokens|reset it"
msgstr "ÑброÑить"
-msgid "AccessibilityReport|Learn More"
+msgid "AccessibilityReport|Accessibility report artifact not found"
msgstr ""
-msgid "AccessibilityReport|Message: %{message}"
+msgid "AccessibilityReport|Failed to retrieve accessibility report"
msgstr ""
+msgid "AccessibilityReport|Learn More"
+msgstr "Узнать больше"
+
+msgid "AccessibilityReport|Message: %{message}"
+msgstr "Сообщение: %{message}"
+
msgid "AccessibilityReport|New"
+msgstr "Ðовый"
+
+msgid "AccessibilityReport|The accessibility scanning found an error of the following type: %{code}"
msgstr ""
msgid "Account"
-msgstr "Ð£Ñ‡ÐµÑ‚Ð½Ð°Ñ Ð·Ð°Ð¿Ð¸ÑÑŒ"
+msgstr "Ð£Ñ‡Ñ‘Ñ‚Ð½Ð°Ñ Ð·Ð°Ð¿Ð¸ÑÑŒ"
msgid "Account ID"
-msgstr ""
+msgstr "ID аккаунта"
msgid "Account and limit"
msgstr "Ðккаунт и ограничениÑ"
@@ -1198,7 +1236,7 @@ msgid "Active Sessions"
msgstr "Ðктивные ÑеÑÑии"
msgid "Active Users:"
-msgstr ""
+msgstr "Ðктивные пользователи:"
msgid "Activity"
msgstr "ÐктивноÑÑ‚ÑŒ"
@@ -1232,16 +1270,16 @@ msgid "Add Jaeger URL"
msgstr "Добавить URL-Ð°Ð´Ñ€ÐµÑ Jaeger"
msgid "Add Kubernetes cluster"
-msgstr "Добавить Kubernetes клаÑтер"
+msgstr "Добавить клаÑтер Kubernetes"
msgid "Add LICENSE"
-msgstr ""
+msgstr "Добавить LICENSE"
msgid "Add README"
msgstr "Добавить README"
msgid "Add Variable"
-msgstr ""
+msgstr "Добавить переменную"
msgid "Add Zoom meeting"
msgstr "ПриÑоединитьÑÑ Ðº вÑтрече в Zoom"
@@ -1250,7 +1288,7 @@ msgid "Add a %{type} token"
msgstr "Добавить токен %{type}"
msgid "Add a GPG key"
-msgstr "Добавить GPG ключ"
+msgstr "Добавить ключ GPG"
msgid "Add a Grafana button in the admin sidebar, monitoring section, to access a variety of statistics on the health and performance of GitLab."
msgstr "Добавьте кнопку Grafana на боковой панели админиÑтратора в разделе мониторинга, чтобы получить доÑтуп к различным ÑтатиÑтичеÑким данным о работоÑпоÑобноÑти и производительноÑти GitLab."
@@ -1295,7 +1333,7 @@ msgid "Add an issue"
msgstr "Добавить обÑуждение"
msgid "Add another link"
-msgstr ""
+msgstr "Добавить ещё ÑÑылку"
msgid "Add approval rule"
msgstr "Добавить правило утверждениÑ"
@@ -1310,13 +1348,13 @@ msgid "Add comment now"
msgstr "Добавить комментарий"
msgid "Add domain"
-msgstr ""
+msgstr "Добавить домен"
msgid "Add email address"
msgstr "Добавить Ð°Ð´Ñ€ÐµÑ Ñлектронной почты"
msgid "Add environment"
-msgstr ""
+msgstr "Добавить окружение"
msgid "Add header and footer to emails. Please note that color settings will only be applied within the application interface"
msgstr "Добавить заголовок и подвал в Ñлектронные пиÑьма. ПожалуйÑта, обратите внимание, что наÑтройки цвета будут применены только в интерфейÑе приложениÑ"
@@ -1352,7 +1390,7 @@ msgid "Add reaction"
msgstr "Добавить реакцию"
msgid "Add request manually"
-msgstr ""
+msgstr "Добавить Ð·Ð°Ð¿Ñ€Ð¾Ñ Ð²Ñ€ÑƒÑ‡Ð½ÑƒÑŽ"
msgid "Add system hook"
msgstr "Добавить ÑиÑтемный обработчик"
@@ -1382,7 +1420,7 @@ msgid "Add users to group"
msgstr "Добавить пользователей в группу"
msgid "Add variable"
-msgstr ""
+msgstr "Добавить переменную"
msgid "Add webhook"
msgstr "Добавить веб-обработчик"
@@ -1411,6 +1449,9 @@ msgstr "К цели добавлена задача."
msgid "Added at"
msgstr "Добавлено"
+msgid "Added for this merge request"
+msgstr ""
+
msgid "Added in this version"
msgstr "Добавлено в Ñтой верÑии"
@@ -1424,7 +1465,7 @@ msgid "Additional text"
msgstr "Дополнительный текÑÑ‚"
msgid "Adds"
-msgstr ""
+msgstr "ДобавлÑет"
msgid "Adds %{epic_ref} as child epic."
msgstr "ДобавлÑет %{epic_ref} как дочерний Ñлемент."
@@ -1460,22 +1501,22 @@ msgid "Admin mode disabled"
msgstr "Режим админиÑтратора отключен"
msgid "Admin mode enabled"
-msgstr ""
+msgstr "Режим админиÑтратора включен"
msgid "Admin notes"
msgstr "Заметки админиÑтратора"
msgid "AdminArea|Active users"
-msgstr ""
+msgstr "Ðктивные пользователи"
msgid "AdminArea|Billable users"
-msgstr ""
+msgstr "Оплачиваемые пользователи"
msgid "AdminArea|Blocked users"
-msgstr ""
+msgstr "Заблокированные пользователи"
msgid "AdminArea|Bots"
-msgstr ""
+msgstr "Боты"
msgid "AdminArea|Developer"
msgstr ""
@@ -1484,7 +1525,7 @@ msgid "AdminArea|Guest"
msgstr ""
msgid "AdminArea|Included Free in license"
-msgstr ""
+msgstr "Включены в лицензию беÑплатно"
msgid "AdminArea|Maintainer"
msgstr ""
@@ -1508,16 +1549,16 @@ msgid "AdminArea|Stopping jobs failed"
msgstr "ОÑтановка заданий не удалаÑÑŒ"
msgid "AdminArea|Total users"
-msgstr ""
+msgstr "Ð’Ñего пользователей"
msgid "AdminArea|Users statistics"
-msgstr ""
+msgstr "СтатиÑтика пользователей"
msgid "AdminArea|Users with highest role"
-msgstr ""
+msgstr "Пользователи Ñ Ð½Ð°Ð¸Ð²Ñ‹Ñшей ролью"
msgid "AdminArea|Users without a Group and Project"
-msgstr ""
+msgstr "Пользователи без группы и проекта"
msgid "AdminArea|You’re about to stop all jobs.This will halt all current jobs that are running."
msgstr "Ð’Ñ‹ ÑобираетеÑÑŒ оÑтановить вÑе заданиÑ. Это дейÑтвие оÑтановит вÑе текущие заданиÑ, находÑщиеÑÑ Ð² процеÑÑе выполнениÑ."
@@ -1541,13 +1582,13 @@ msgid "AdminProjects|Delete project"
msgstr "Удалить проект"
msgid "AdminSettings|Apply integration settings to all Projects"
-msgstr ""
+msgstr "Применить наÑтройки интеграции ко вÑем проектам"
msgid "AdminSettings|Auto DevOps domain"
msgstr "Домен Auto DevOps"
msgid "AdminSettings|Elasticsearch, PlantUML, Slack application, Third party offers, Snowplow, Amazon EKS have moved to Settings > General."
-msgstr ""
+msgstr "Elasticsearch, PlantUML, приложение Slack, Ð¿Ñ€ÐµÐ´Ð»Ð¾Ð¶ÐµÐ½Ð¸Ñ Ð¾Ñ‚ третьих лиц, Snowplow, Amazon EKS были перемещены в ÐаÑтройки > Общие"
msgid "AdminSettings|Enable shared runners for new projects"
msgstr "ИÑпользовать общие Runner'Ñ‹ в новых проектах"
@@ -1556,10 +1597,10 @@ msgid "AdminSettings|Environment variables are protected by default"
msgstr "Переменные Ð¾ÐºÑ€ÑƒÐ¶ÐµÐ½Ð¸Ñ Ð·Ð°Ñ‰Ð¸Ñ‰ÐµÐ½Ñ‹ по умолчанию"
msgid "AdminSettings|Go to General Settings"
-msgstr ""
+msgstr "Перейти к общим наÑтройкам"
msgid "AdminSettings|Integrations configured here will automatically apply to all projects on this instance."
-msgstr ""
+msgstr "Интеграции, наÑтроенные здеÑÑŒ, будут автоматичеÑки применÑÑ‚ÑŒÑÑ ÐºÐ¾ вÑем проектам на Ñтом ÑкземплÑре."
msgid "AdminSettings|No required pipeline"
msgstr "Ðе требуетÑÑ ÑÐ±Ð¾Ñ€Ð¾Ñ‡Ð½Ð°Ñ Ð»Ð¸Ð½Ð¸Ñ"
@@ -1580,7 +1621,7 @@ msgid "AdminSettings|Set an instance-wide auto included %{link_start}pipeline co
msgstr "УÑтановить автоматичеÑкую включенную %{link_start}конфигурацию Ñборочной линии%{link_end} Ð´Ð»Ñ Ð²Ñего ÑкземплÑра. Эта ÐºÐ¾Ð½Ñ„Ð¸Ð³ÑƒÑ€Ð°Ñ†Ð¸Ñ Ñборочной линии будет запуÑкатьÑÑ Ð¿Ð¾Ñле ÑобÑтвенной конфигурации проекта."
msgid "AdminSettings|Some settings have moved"
-msgstr ""
+msgstr "Ðекоторые наÑтройки были перемещены"
msgid "AdminSettings|Specify a domain to use by default for every project's Auto Review Apps and Auto Deploy stages."
msgstr "Укажите домен, который будет иÑпользоватьÑÑ Ð¿Ð¾ умолчанию Ð´Ð»Ñ Ð²Ñех проектов в Auto Review приложениÑÑ… и ÑтадиÑÑ… Auto Deploy."
@@ -1610,7 +1651,7 @@ msgid "AdminStatistics|Notes"
msgstr "Заметки"
msgid "AdminStatistics|SSH Keys"
-msgstr "SSH ключи"
+msgstr "Ключи SSH"
msgid "AdminStatistics|Snippets"
msgstr "Сниппеты"
@@ -1679,7 +1720,7 @@ msgid "AdminUsers|External"
msgstr "Внешние"
msgid "AdminUsers|Is using seat"
-msgstr ""
+msgstr "ИÑпользует меÑто"
msgid "AdminUsers|It's you!"
msgstr "Это вы!"
@@ -1745,25 +1786,25 @@ msgid "AdminUsers|User will not be able to login"
msgstr "Пользователь не Ñможет войти"
msgid "AdminUsers|When the user logs back in, their account will reactivate as a fully active account"
-msgstr "Когда пользователь в Ñледующий раз войдёт в ÑиÑтему, его ÑƒÑ‡ÐµÑ‚Ð½Ð°Ñ Ð·Ð°Ð¿Ð¸ÑÑŒ Ñнова Ñтанет полноÑтью активной"
+msgstr "Когда пользователь в Ñледующий раз войдёт в ÑиÑтему, его ÑƒÑ‡Ñ‘Ñ‚Ð½Ð°Ñ Ð·Ð°Ð¿Ð¸ÑÑŒ Ñнова Ñтанет полноÑтью активной"
msgid "AdminUsers|Without projects"
msgstr "Без проектов"
msgid "AdminUsers|You are about to permanently delete the user %{username}. Issues, merge requests, and groups linked to them will be transferred to a system-wide \"Ghost-user\". To avoid data loss, consider using the %{strong_start}block user%{strong_end} feature instead. Once you %{strong_start}Delete user%{strong_end}, it cannot be undone or recovered."
-msgstr "Ð’Ñ‹ ÑобираетеÑÑŒ окончательно удалить Ð¿Ð¾Ð»ÑŒÐ·Ð¾Ð²Ð°Ñ‚ÐµÐ»Ñ %{username}. ОбÑуждениÑ, запроÑÑ‹ ÑлиÑÐ½Ð¸Ñ Ð¸ ÑвÑзанные Ñ Ð½Ð¸Ð¼Ð¸ группы будут переданы общеÑиÑтемному «Ghost-user». Чтобы избежать потери данных, раÑÑмотрите возможноÑÑ‚ÑŒ иÑÐ¿Ð¾Ð»ÑŒÐ·Ð¾Ð²Ð°Ð½Ð¸Ñ Ñ„ÑƒÐ½ÐºÑ†Ð¸Ð¸ %{strong_start}блокировки пользователÑ%{strong_end}. ПоÑле %{strong_start}ÑƒÐ´Ð°Ð»ÐµÐ½Ð¸Ñ Ð¿Ð¾Ð»ÑŒÐ·Ð¾Ð²Ð°Ñ‚ÐµÐ»Ñ%{strong_end}, Ð½ÐµÐ»ÑŒÐ·Ñ Ð±ÑƒÐ´ÐµÑ‚ его воÑÑтановить или отменить удаление."
+msgstr "Ð’Ñ‹ ÑобираетеÑÑŒ окончательно удалить Ð¿Ð¾Ð»ÑŒÐ·Ð¾Ð²Ð°Ñ‚ÐµÐ»Ñ %{username}. ОбÑуждениÑ, запроÑÑ‹ ÑлиÑÐ½Ð¸Ñ Ð¸ ÑвÑзанные Ñ Ð½Ð¸Ð¼Ð¸ группы будут переданы глобальному \"пользователю-призраку\". Чтобы избежать потери данных, раÑÑмотрите возможноÑÑ‚ÑŒ иÑÐ¿Ð¾Ð»ÑŒÐ·Ð¾Ð²Ð°Ð½Ð¸Ñ Ñ„ÑƒÐ½ÐºÑ†Ð¸Ð¸ %{strong_start}блокировки пользователÑ%{strong_end}. ПоÑле %{strong_start}ÑƒÐ´Ð°Ð»ÐµÐ½Ð¸Ñ Ð¿Ð¾Ð»ÑŒÐ·Ð¾Ð²Ð°Ñ‚ÐµÐ»Ñ%{strong_end}, Ð½ÐµÐ»ÑŒÐ·Ñ Ð±ÑƒÐ´ÐµÑ‚ его воÑÑтановить или отменить удаление."
msgid "AdminUsers|You are about to permanently delete the user %{username}. This will delete all of the issues, merge requests, and groups linked to them. To avoid data loss, consider using the %{strong_start}block user%{strong_end} feature instead. Once you %{strong_start}Delete user%{strong_end}, it cannot be undone or recovered."
msgstr "Ð’Ñ‹ ÑобираетеÑÑŒ окончательно удалить Ð¿Ð¾Ð»ÑŒÐ·Ð¾Ð²Ð°Ñ‚ÐµÐ»Ñ %{username}. Это удалит вÑе его обÑуждениÑ, запроÑÑ‹ ÑлиÑÐ½Ð¸Ñ Ð¸ ÑвÑзанные Ñ Ð½Ð¸Ð¼Ð¸ группы. Чтобы избежать потери данных, раÑÑмотрите возможноÑÑ‚ÑŒ иÑÐ¿Ð¾Ð»ÑŒÐ·Ð¾Ð²Ð°Ð½Ð¸Ñ Ñ„ÑƒÐ½ÐºÑ†Ð¸Ð¸ %{strong_start}блокировки пользователÑ%{strong_end}. ПоÑле %{strong_start}ÑƒÐ´Ð°Ð»ÐµÐ½Ð¸Ñ Ð¿Ð¾Ð»ÑŒÐ·Ð¾Ð²Ð°Ñ‚ÐµÐ»Ñ%{strong_end}, Ð½ÐµÐ»ÑŒÐ·Ñ Ð±ÑƒÐ´ÐµÑ‚ его воÑÑтановить или отменить удаление."
msgid "Administration"
-msgstr ""
+msgstr "ÐдминиÑтрирование"
msgid "Advanced"
msgstr "РаÑширенные"
msgid "Advanced Settings"
-msgstr ""
+msgstr "Дополнительные наÑтройки"
msgid "Advanced permissions, Large File Storage and Two-Factor authentication settings."
msgstr "Дополнительные разрешениÑ, хранилище больших файлов и наÑтройки двухфакторной аутентификации."
@@ -1784,6 +1825,72 @@ msgstr[1] "Предупреждений"
msgstr[2] "Предупреждений"
msgstr[3] "Предупреждений"
+msgid "Alert Details"
+msgstr ""
+
+msgid "AlertManagement|Acknowledged"
+msgstr ""
+
+msgid "AlertManagement|Alert"
+msgstr ""
+
+msgid "AlertManagement|Authorize external service"
+msgstr ""
+
+msgid "AlertManagement|Display alerts from all your monitoring tools directly within GitLab. Streamline the investigation of your alerts and the escalation of alerts to incidents."
+msgstr ""
+
+msgid "AlertManagement|End time"
+msgstr ""
+
+msgid "AlertManagement|End time:"
+msgstr ""
+
+msgid "AlertManagement|Events"
+msgstr ""
+
+msgid "AlertManagement|Events:"
+msgstr ""
+
+msgid "AlertManagement|Full Alert Details"
+msgstr ""
+
+msgid "AlertManagement|More information"
+msgstr ""
+
+msgid "AlertManagement|No alerts available to display. If you think you're seeing this message in error, refresh the page."
+msgstr ""
+
+msgid "AlertManagement|No alerts to display."
+msgstr ""
+
+msgid "AlertManagement|Overview"
+msgstr ""
+
+msgid "AlertManagement|Resolved"
+msgstr ""
+
+msgid "AlertManagement|Severity"
+msgstr ""
+
+msgid "AlertManagement|Start time"
+msgstr ""
+
+msgid "AlertManagement|Start time:"
+msgstr ""
+
+msgid "AlertManagement|Status"
+msgstr ""
+
+msgid "AlertManagement|Surface alerts in GitLab"
+msgstr ""
+
+msgid "AlertManagement|There was an error displaying the alerts. Confirm your endpoint's configuration details to ensure alerts appear."
+msgstr ""
+
+msgid "AlertManagement|Triggered"
+msgstr ""
+
msgid "AlertService|%{linkStart}Learn more%{linkEnd} about configuring this endpoint to receive alerts."
msgstr "%{linkStart}Узнайте подробнее%{linkEnd} о наÑтройке Ñтой конечной точки Ð´Ð»Ñ Ð¿Ð¾Ð»ÑƒÑ‡ÐµÐ½Ð¸Ñ Ð¾Ð¿Ð¾Ð²ÐµÑ‰ÐµÐ½Ð¸Ð¹."
@@ -1806,7 +1913,7 @@ msgid "All %{replicableType} are being scheduled for %{action}"
msgstr ""
msgid "All (default)"
-msgstr ""
+msgstr "Ð’Ñе (по умолчанию)"
msgid "All Members"
msgstr "Ð’Ñе УчаÑтники"
@@ -1818,13 +1925,13 @@ msgid "All changes are committed"
msgstr "Ð’Ñе Ð¸Ð·Ð¼ÐµÐ½ÐµÐ½Ð¸Ñ Ð·Ð°Ñ„Ð¸ÐºÑированы"
msgid "All default stages are currently visible"
-msgstr ""
+msgstr "Ðа данный момент вÑе Ñтапы по умолчанию видны"
msgid "All email addresses will be used to identify your commits."
msgstr "Ð’Ñе адреÑа Ñлектронной почты будут иÑпользоватьÑÑ Ð´Ð»Ñ Ð¸Ð´ÐµÐ½Ñ‚Ð¸Ñ„Ð¸ÐºÐ°Ñ†Ð¸Ð¸ ваших коммитов."
msgid "All environments"
-msgstr ""
+msgstr "Ð’Ñе окружениÑ"
msgid "All features are enabled for blank projects, from templates, or when importing, but you can disable them afterward in the project settings."
msgstr "Ð’Ñе функции ÑиÑтемы включаютÑÑ Ð´Ð»Ñ Ð¿ÑƒÑÑ‚Ñ‹Ñ… проектов, Ñозданных из шаблонов или импортированных, но вы можете отключить их впоÑледÑтвии в наÑтройках проекта."
@@ -1833,13 +1940,13 @@ msgid "All groups and projects"
msgstr "Ð’Ñе группы и проекты"
msgid "All issues for this milestone are closed."
-msgstr ""
+msgstr "Ð’Ñе обÑÑƒÐ¶Ð´ÐµÐ½Ð¸Ñ Ð¿Ð¾ Ñтому Ñтапу закрыты."
msgid "All issues for this milestone are closed. You may close this milestone now."
-msgstr ""
+msgstr "Ð’Ñе обÑÑƒÐ¶Ð´ÐµÐ½Ð¸Ñ Ð¿Ð¾ Ñтому Ñтапу закрыты. Теперь вы можете закрыть Ñтот Ñтап."
msgid "All merge conflicts were resolved. The merge request can now be merged."
-msgstr ""
+msgstr "Ð’Ñе конфликты ÑлиÑÐ½Ð¸Ñ Ð±Ñ‹Ð»Ð¸ разрешены. Теперь Ð·Ð°Ð¿Ñ€Ð¾Ñ Ð½Ð° ÑлиÑние может быть выполнен."
msgid "All merge request dependencies have been merged"
msgstr "Ð’Ñе завиÑимоÑти запроÑа на ÑлиÑние были объединены"
@@ -1853,6 +1960,9 @@ msgstr "Ð’Ñе проекты"
msgid "All security scans are enabled because %{linkStart}Auto DevOps%{linkEnd} is enabled on this project"
msgstr ""
+msgid "All threads resolved"
+msgstr ""
+
msgid "All users"
msgstr "Ð’Ñе пользователи"
@@ -1866,14 +1976,17 @@ msgid "Allow commits from members who can merge to the target branch."
msgstr "Разрешить коммиты от учаÑтников, которые могут выполнÑÑ‚ÑŒ ÑлиÑние Ñ Ñ†ÐµÐ»ÐµÐ²Ð¾Ð¹ веткой."
msgid "Allow group owners to manage LDAP-related settings"
-msgstr ""
+msgstr "Разрешить владельцам групп управлÑÑ‚ÑŒ наÑтройками, ÑвÑзанными Ñ LDAP"
msgid "Allow only the selected protocols to be used for Git access."
msgstr ""
-msgid "Allow owners to manually add users outside of LDAP"
+msgid "Allow owners to manage default branch protection per group"
msgstr ""
+msgid "Allow owners to manually add users outside of LDAP"
+msgstr "Разрешить владельцам вручную добавлÑÑ‚ÑŒ пользователей вне LDAP"
+
msgid "Allow projects within this group to use Git LFS"
msgstr ""
@@ -1902,7 +2015,7 @@ msgid "Allow this secondary node to replicate content on Object Storage"
msgstr ""
msgid "Allow users to dismiss the broadcast message"
-msgstr ""
+msgstr "ПозволÑет пользователÑм отклонÑÑ‚ÑŒ раÑÑылаемое Ñообщение"
msgid "Allow users to register any application to use GitLab as an OAuth provider"
msgstr "Разрешить пользователÑм региÑтрировать любое приложение Ð´Ð»Ñ Ð¸ÑÐ¿Ð¾Ð»ÑŒÐ·Ð¾Ð²Ð°Ð½Ð¸Ñ GitLab как провайдера OAuth"
@@ -1923,7 +2036,7 @@ msgid "Almost there"
msgstr "Почти готово"
msgid "Also called \"Issuer\" or \"Relying party trust identifier\""
-msgstr ""
+msgstr "Также называетÑÑ \"Ñмитент\" или \"идентификатор Ð´Ð¾Ð²ÐµÑ€Ð¸Ñ Ñтороны\""
msgid "Also called \"Relying party service URL\" or \"Reply URL\""
msgstr ""
@@ -1932,7 +2045,7 @@ msgid "Alternate support URL for help page and help dropdown"
msgstr ""
msgid "Alternatively, you can convert your account to a managed account by the %{group_name} group."
-msgstr ""
+msgstr "Кроме того, вы можете преобразовать Ñвой аккаунт в аккаунт, управлÑемый группой %{group_name}."
msgid "Amazon EKS"
msgstr "Amazon EKS"
@@ -1941,7 +2054,7 @@ msgid "Amazon EKS integration allows you to provision EKS clusters from GitLab."
msgstr "Ð˜Ð½Ñ‚ÐµÐ³Ñ€Ð°Ñ†Ð¸Ñ Ñ Amazon EKS позволÑет вам предоÑтавлÑÑ‚ÑŒ клаÑтеры EKS из GitLab."
msgid "Amazon Web Services"
-msgstr ""
+msgstr "Amazon Web Services"
msgid "Amazon authentication is not %{link_start}correctly configured%{link_end}. Ask your GitLab administrator if you want to use this service."
msgstr "ÐÑƒÑ‚ÐµÐ½Ñ‚Ð¸Ñ„Ð¸ÐºÐ°Ñ†Ð¸Ñ Amazon не %{link_start}наÑтроена%{link_end}. ОбратитеÑÑŒ к Ñвоему админиÑтратору GitLab, еÑли хотите иÑпользовать Ñту Ñлужбу."
@@ -1950,7 +2063,7 @@ msgid "Amount of time (in hours) that users are allowed to skip forced configura
msgstr "Ð’Ñ€ÐµÐ¼Ñ (в чаÑах), на которое пользователÑм позволено пропуÑкать принудительную наÑтройку двухфакторной аутентификации"
msgid "An alert has been triggered in %{project_path}."
-msgstr ""
+msgstr "Предупреждение было вызвано в %{project_path}."
msgid "An application called %{link_to_client} is requesting access to your GitLab account."
msgstr "Приложение под названием %{link_to_client} запрашивает доÑтуп к вашей учетной запиÑи GitLab."
@@ -1977,6 +2090,9 @@ msgid "An error occurred fetching the approvers for the new rule."
msgstr ""
msgid "An error occurred fetching the dropdown data."
+msgstr "Произошла ошибка при извлечении данных выпадающего ÑпиÑка."
+
+msgid "An error occurred fetching the project authors."
msgstr ""
msgid "An error occurred previewing the blob"
@@ -1986,16 +2102,16 @@ msgid "An error occurred when toggling the notification subscription"
msgstr "Произошла ошибка при переключении подпиÑки на оповещениÑ"
msgid "An error occurred when trying to resolve a comment. Please try again."
-msgstr ""
+msgstr "Произошла ошибка при попытке разрешить комментарий. ПожалуйÑта, попробуйте ещё раз."
msgid "An error occurred when trying to resolve a discussion. Please try again."
-msgstr ""
+msgstr "Произошла ошибка при попытке разрешить диÑкуÑÑию. ПожалуйÑта, попробуйте ещё раз."
msgid "An error occurred when updating the issue weight"
-msgstr "Произошла ошибка при обновлении веÑа обÑуждениÑ"
+msgstr "Произошла ошибка при обновлении приоритета обÑуждениÑ"
msgid "An error occurred while adding formatted title for epic"
-msgstr ""
+msgstr "Произошла ошибка при Ñоздании форматированного заголовка Ð´Ð»Ñ Ñ†ÐµÐ»Ð¸."
msgid "An error occurred while checking group path"
msgstr "Произошла ошибка при проверке пути группы"
@@ -2004,7 +2120,7 @@ msgid "An error occurred while committing your changes."
msgstr ""
msgid "An error occurred while decoding the file."
-msgstr ""
+msgstr "Произошла ошибка при декодировании файла."
msgid "An error occurred while deleting the approvers group"
msgstr "Произошла ошибка при удалении группы подтверждений"
@@ -2013,7 +2129,7 @@ msgid "An error occurred while deleting the comment"
msgstr "Во Ð²Ñ€ÐµÐ¼Ñ ÑƒÐ´Ð°Ð»ÐµÐ½Ð¸Ñ ÐºÐ¾Ð¼Ð¼ÐµÐ½Ñ‚Ð°Ñ€Ð¸Ñ Ð¿Ñ€Ð¾Ð¸Ð·Ð¾ÑˆÐ»Ð° ошибка"
msgid "An error occurred while deleting the pipeline."
-msgstr ""
+msgstr "Произошла ошибка при удалении Ñборочной линии."
msgid "An error occurred while detecting host keys"
msgstr "Произошла ошибка при обнаружении ключей хоÑта"
@@ -2031,7 +2147,7 @@ msgid "An error occurred while enabling Service Desk."
msgstr "Произошла ошибка при включении Ñлужбы поддержки."
msgid "An error occurred while fetching coverage reports."
-msgstr ""
+msgstr "Произошла ошибка при извлечении отчетов о покрытии теÑтами."
msgid "An error occurred while fetching environments."
msgstr "Произошла ошибка при получении окружениÑ."
@@ -2060,6 +2176,9 @@ msgstr ""
msgid "An error occurred while fetching sidebar data"
msgstr "Произошла ошибка при получении денег данных Ð´Ð»Ñ Ð±Ð¾ÐºÐ¾Ð²Ð¾Ð¹ панели"
+msgid "An error occurred while fetching terraform reports."
+msgstr ""
+
msgid "An error occurred while fetching the Service Desk address."
msgstr ""
@@ -2073,55 +2192,55 @@ msgid "An error occurred while fetching the job log."
msgstr ""
msgid "An error occurred while fetching the job trace."
-msgstr ""
+msgstr "Произошла ошибка при извлечении траÑÑировки заданиÑ."
msgid "An error occurred while fetching the job."
-msgstr ""
+msgstr "Произошла ошибка при извлечении заданиÑ."
msgid "An error occurred while fetching the jobs."
-msgstr ""
+msgstr "Ошибка при извлечении заданий."
msgid "An error occurred while fetching the latest pipeline."
-msgstr ""
+msgstr "Произошла ошибка при извлечении поÑледней Ñборочной линии."
msgid "An error occurred while fetching the pipeline."
msgstr "Произошла ошибка при получении Ñборочной линии."
msgid "An error occurred while fetching the releases. Please try again."
-msgstr ""
+msgstr "Произошла ошибка при выборке релизов. ПожалуйÑта, попробуйте ещё раз."
msgid "An error occurred while fetching this tab."
msgstr "Произошла ошибка при получении Ñтой вкладки."
msgid "An error occurred while generating a username. Please try again."
-msgstr ""
+msgstr "Произошла ошибка при генерации имени пользователÑ. ПожалуйÑта, попробуйте ещё раз."
msgid "An error occurred while getting files for - %{branchId}"
-msgstr ""
+msgstr "Произошла ошибка во Ð²Ñ€ÐµÐ¼Ñ Ð¿Ð¾Ð»ÑƒÑ‡ÐµÐ½Ð¸Ñ Ñ„Ð°Ð¹Ð»Ð¾Ð² Ð´Ð»Ñ - %{branchId}"
msgid "An error occurred while getting projects"
msgstr "Произошла ошибка при получении ÑпиÑка проектов"
msgid "An error occurred while importing project: %{details}"
-msgstr ""
+msgstr "Ошибка при импорте проекта: %{details}"
msgid "An error occurred while initializing path locks"
msgstr ""
msgid "An error occurred while loading all the files."
-msgstr ""
+msgstr "Произошла ошибка при загрузке вÑех файлов."
msgid "An error occurred while loading chart data"
-msgstr ""
+msgstr "Произошла ошибка при загрузке данных диаграммы "
msgid "An error occurred while loading clusters"
-msgstr ""
+msgstr "Произошла ошибка при загрузке клаÑтеров"
msgid "An error occurred while loading commit signatures"
-msgstr ""
+msgstr "Произошла ошибка при загрузке подпиÑей коммита"
msgid "An error occurred while loading designs. Please try again."
-msgstr ""
+msgstr "Произошла ошибка при загрузке дизайнов. ПожалуйÑта, повторите попытку."
msgid "An error occurred while loading diff"
msgstr "Произошла ошибка при загрузке различий"
@@ -2130,16 +2249,16 @@ msgid "An error occurred while loading filenames"
msgstr "Произошла ошибка при загрузке ÑпиÑка файлов"
msgid "An error occurred while loading group members."
-msgstr ""
+msgstr "Произошла ошибка при загрузке членов группы."
msgid "An error occurred while loading issues"
msgstr "Произошла ошибка при загрузке обÑуждений"
msgid "An error occurred while loading merge requests."
-msgstr ""
+msgstr "Произошла ошибка при загрузке запроÑов на ÑлиÑние."
msgid "An error occurred while loading terraform report"
-msgstr ""
+msgstr "Произошла ошибка при загрузке отчёта Terraform"
msgid "An error occurred while loading the data. Please try again."
msgstr "Произошла ошибка при загрузке данных. ПожалуйÑта, повторите попытку."
@@ -2148,16 +2267,16 @@ msgid "An error occurred while loading the file"
msgstr "Произошла ошибка при загрузке файла"
msgid "An error occurred while loading the file content."
-msgstr ""
+msgstr "Произошла ошибка при загрузке Ñодержимого файла."
msgid "An error occurred while loading the file."
-msgstr ""
+msgstr "Произошла ошибка при загрузке файла."
msgid "An error occurred while loading the file. Please try again later."
-msgstr ""
+msgstr "Произошла ошибка при загрузке файла. ПожалуйÑта, попробуйте ещё раз позже."
msgid "An error occurred while loading the merge request changes."
-msgstr ""
+msgstr "Произошла ошибка при загрузке изменений запроÑа на ÑлиÑние."
msgid "An error occurred while loading the merge request version data."
msgstr ""
@@ -2181,13 +2300,13 @@ msgid "An error occurred while parsing recent searches"
msgstr ""
msgid "An error occurred while parsing the file."
-msgstr ""
+msgstr "Произошла ошибка при обработке файла."
msgid "An error occurred while removing epics."
msgstr "Произошла ошибка при удалении целей."
msgid "An error occurred while removing issues."
-msgstr ""
+msgstr "Произошла ошибка при удалении обÑуждений."
msgid "An error occurred while rendering preview broadcast message"
msgstr "Произошла ошибка при визуализации проÑмотра широковещательного ÑообщениÑ"
@@ -2202,7 +2321,7 @@ msgid "An error occurred while retrieving diff"
msgstr "Произошла ошибка при получении различий"
msgid "An error occurred while saving LDAP override status. Please try again."
-msgstr ""
+msgstr "Произошла ошибка при Ñохранении ÑтатуÑа Ð¿ÐµÑ€ÐµÐ¾Ð¿Ñ€ÐµÐ´ÐµÐ»ÐµÐ½Ð¸Ñ LDAP. ПожалуйÑта, попробуйте ещё раз."
msgid "An error occurred while saving assignees"
msgstr "Произошла ошибка при Ñохранении ответÑтвенных"
@@ -2229,7 +2348,7 @@ msgid "An error occurred while updating approvers"
msgstr ""
msgid "An error occurred while updating the comment"
-msgstr ""
+msgstr "Произошла ошибка при обновлении комментариÑ"
msgid "An error occurred while validating group path"
msgstr "Произошла ошибка при валидации пути группы"
@@ -2247,25 +2366,25 @@ msgid "An example project for managing Kubernetes clusters integrated with GitLa
msgstr ""
msgid "An instance-level serverless domain already exists."
-msgstr ""
+msgstr "Домен Serverless ÑƒÑ€Ð¾Ð²Ð½Ñ ÑкземплÑра уже ÑущеÑтвует."
msgid "An issue can be a bug, a todo or a feature request that needs to be discussed in a project. Besides, issues are searchable and filterable."
msgstr "Ðа обÑуждение может выноÑитÑÑ Ð±Ð°Ð³, задача или Ð·Ð°Ð¿Ñ€Ð¾Ñ Ñ„ÑƒÐ½ÐºÑ†Ð¸Ð¸, ÐºÐ¾Ñ‚Ð¾Ñ€Ð°Ñ Ð´Ð¾Ð»Ð¶Ð½Ð° быть раÑÑмотрена в проекте. Кроме того, обÑÑƒÐ¶Ð´ÐµÐ½Ð¸Ñ Ð´Ð¾Ñтупны Ð´Ð»Ñ Ð¿Ð¾Ð¸Ñка и фильтрации."
msgid "An unexpected error occurred while checking the project environment."
-msgstr ""
+msgstr "Произошла Ð½ÐµÐ¿Ñ€ÐµÐ´Ð²Ð¸Ð´ÐµÐ½Ð½Ð°Ñ Ð¾ÑˆÐ¸Ð±ÐºÐ° при проверке Ð¾ÐºÑ€ÑƒÐ¶ÐµÐ½Ð¸Ñ Ð¿Ñ€Ð¾ÐµÐºÑ‚Ð°."
msgid "An unexpected error occurred while checking the project runners."
msgstr "Произошла Ð½ÐµÐ¿Ñ€ÐµÐ´Ð²Ð¸Ð´ÐµÐ½Ð½Ð°Ñ Ð¾ÑˆÐ¸Ð±ÐºÐ° при проверке runner'ов проекта."
msgid "An unexpected error occurred while communicating with the Web Terminal."
-msgstr ""
+msgstr "Произошла Ð½ÐµÐ¿Ñ€ÐµÐ´Ð²Ð¸Ð´ÐµÐ½Ð½Ð°Ñ Ð¾ÑˆÐ¸Ð±ÐºÐ° при общении к Web Terminal."
msgid "An unexpected error occurred while starting the Web Terminal."
-msgstr ""
+msgstr "Произошла Ð½ÐµÐ¿Ñ€ÐµÐ´Ð²Ð¸Ð´ÐµÐ½Ð½Ð°Ñ Ð¾ÑˆÐ¸Ð±ÐºÐ° при запуÑке Web Terminal."
msgid "An unexpected error occurred while stopping the Web Terminal."
-msgstr ""
+msgstr "Произошла Ð½ÐµÐ¿Ñ€ÐµÐ´Ð²Ð¸Ð´ÐµÐ½Ð½Ð°Ñ Ð¾ÑˆÐ¸Ð±ÐºÐ° при оÑтановке Web Terminal."
msgid "Analytics"
msgstr "Ðналитика"
@@ -2274,10 +2393,10 @@ msgid "Analyze a review version of your web application."
msgstr "Проанализировать рецензируемую верÑию вашего веб-приложениÑ."
msgid "Analyze your dependencies for known vulnerabilities."
-msgstr ""
+msgstr "Ðнализируйте ваши завиÑимоÑти на предмет извеÑтных уÑзвимоÑтей."
msgid "Analyze your source code for known vulnerabilities."
-msgstr ""
+msgstr "Ðнализируйте ваш иÑходный код на предмет извеÑтных уÑзвимоÑтей."
msgid "Ancestors"
msgstr ""
@@ -2286,7 +2405,7 @@ msgid "Anonymous"
msgstr "Ðноним"
msgid "Another action is currently in progress"
-msgstr ""
+msgstr "Другое дейÑтвие уже в процеÑÑе"
msgid "Another issue tracker is already in use. Only one issue tracker service can be active at a time"
msgstr ""
@@ -2297,6 +2416,9 @@ msgstr "Ðнти-Ñпам проверка"
msgid "Any"
msgstr "Любой"
+msgid "Any Author"
+msgstr ""
+
msgid "Any Label"
msgstr "Ð›ÑŽÐ±Ð°Ñ ÐœÐµÑ‚ÐºÐ°"
@@ -2304,7 +2426,7 @@ msgid "Any Milestone"
msgstr "Любой Ñтап"
msgid "Any branch"
-msgstr ""
+msgstr "Ð›ÑŽÐ±Ð°Ñ Ð²ÐµÑ‚ÐºÐ°"
msgid "Any eligible user"
msgstr "Любой подходÑщий пользователь"
@@ -2322,13 +2444,13 @@ msgid "Any user"
msgstr "Любой пользователь"
msgid "App ID"
-msgstr ""
+msgstr "ID приложениÑ"
msgid "Appearance"
msgstr "Оформление"
msgid "Appearance was successfully created."
-msgstr ""
+msgstr "Внешний вид уÑпешно Ñоздан."
msgid "Appearance was successfully updated."
msgstr "Внешний вид уÑпешно изменен."
@@ -2346,16 +2468,16 @@ msgid "Application ID"
msgstr "Идентификатор приложениÑ"
msgid "Application settings saved successfully"
-msgstr ""
+msgstr "ÐаÑтройки Ð¿Ñ€Ð¸Ð»Ð¾Ð¶ÐµÐ½Ð¸Ñ ÑƒÑпешно Ñохранены"
msgid "Application settings update failed"
-msgstr ""
+msgstr "Ðе удалоÑÑŒ обновить наÑтройки приложениÑ"
msgid "Application uninstalled but failed to destroy: %{error_message}"
msgstr ""
msgid "Application was successfully destroyed."
-msgstr ""
+msgstr "Приложение было уÑпешно удалено."
msgid "Application was successfully updated."
msgstr "Приложение было уÑпешно обновлено."
@@ -2403,7 +2525,7 @@ msgid "Applying suggestion"
msgstr "Применение предложениÑ"
msgid "Approval rules"
-msgstr ""
+msgstr "Правила утверждениÑ"
msgid "ApprovalRuleRemove|%d member"
msgid_plural "ApprovalRuleRemove|%d members"
@@ -2415,9 +2537,9 @@ msgstr[3] "%d учаÑтников"
msgid "ApprovalRuleRemove|Approvals from this member are not revoked."
msgid_plural "ApprovalRuleRemove|Approvals from these members are not revoked."
msgstr[0] "ÐžÐ´Ð¾Ð±Ñ€ÐµÐ½Ð¸Ñ Ð¾Ñ‚ Ñтого учаÑтника не отменÑÑŽÑ‚ÑÑ."
-msgstr[1] "ÐžÐ´Ð¾Ð±Ñ€ÐµÐ½Ð¸Ñ Ð¾Ñ‚ Ñтого учаÑтника не отменÑÑŽÑ‚ÑÑ."
-msgstr[2] "ÐžÐ´Ð¾Ð±Ñ€ÐµÐ½Ð¸Ñ Ð¾Ñ‚ Ñтого учаÑтника не отменÑÑŽÑ‚ÑÑ."
-msgstr[3] "ÐžÐ´Ð¾Ð±Ñ€ÐµÐ½Ð¸Ñ Ð¾Ñ‚ Ñтого учаÑтника не отменÑÑŽÑ‚ÑÑ."
+msgstr[1] "ÐžÐ´Ð¾Ð±Ñ€ÐµÐ½Ð¸Ñ Ð¾Ñ‚ Ñтих учаÑтников не отменÑÑŽÑ‚ÑÑ."
+msgstr[2] "ÐžÐ´Ð¾Ð±Ñ€ÐµÐ½Ð¸Ñ Ð¾Ñ‚ Ñтих учаÑтников не отменÑÑŽÑ‚ÑÑ."
+msgstr[3] "ÐžÐ´Ð¾Ð±Ñ€ÐµÐ½Ð¸Ñ Ð¾Ñ‚ Ñтих учаÑтников не отменÑÑŽÑ‚ÑÑ."
msgid "ApprovalRuleRemove|You are about to remove the %{name} approver group which has %{nMembers}."
msgstr "Ð’Ñ‹ ÑобираетеÑÑŒ удалить группу %{name} утверждающих, ÐºÐ¾Ñ‚Ð¾Ñ€Ð°Ñ Ð¸Ð¼ÐµÐµÑ‚ %{nMembers}."
@@ -2449,37 +2571,37 @@ msgid "ApprovalRule|Rule name"
msgstr "Ðазвание правила"
msgid "ApprovalRule|Target branch"
-msgstr ""
+msgstr "Ð¦ÐµÐ»ÐµÐ²Ð°Ñ Ð²ÐµÑ‚ÐºÐ°"
msgid "ApprovalRule|e.g. QA, Security, etc."
-msgstr ""
+msgstr "например, QA, Security и т. д."
msgid "Approvals"
msgstr "СоглаÑованиÑ"
msgid "Approvals (you've approved)"
-msgstr ""
+msgstr "Ð£Ñ‚Ð²ÐµÑ€Ð¶Ð´ÐµÐ½Ð¸Ñ (ваши)"
msgid "Approve"
msgstr "Одобрить"
msgid "Approve a merge request"
-msgstr ""
+msgstr "Утвердить Ð·Ð°Ð¿Ñ€Ð¾Ñ Ð½Ð° ÑлиÑние"
msgid "Approve the current merge request."
-msgstr ""
+msgstr "Утвердить текущий Ð·Ð°Ð¿Ñ€Ð¾Ñ Ð½Ð° ÑлиÑние."
msgid "Approved by: "
-msgstr ""
+msgstr "Утверждено: "
msgid "Approved the current merge request."
msgstr ""
msgid "Approver"
-msgstr ""
+msgstr "Утверждающий"
msgid "Approvers"
-msgstr ""
+msgstr "Утверждающие"
msgid "Apr"
msgstr "Ðпр."
@@ -2488,7 +2610,7 @@ msgid "April"
msgstr "Ðпрель"
msgid "Archive"
-msgstr ""
+msgstr "Ðрхив"
msgid "Archive jobs"
msgstr ""
@@ -2497,7 +2619,7 @@ msgid "Archive project"
msgstr "Ðрхивировать проект"
msgid "Archived"
-msgstr ""
+msgstr "Ðрхивные"
msgid "Archived project! Repository and other project resources are read only"
msgstr "Ðрхивный проект! Репозиторий и другие реÑурÑÑ‹ проекта доÑтупны только Ð´Ð»Ñ Ñ‡Ñ‚ÐµÐ½Ð¸Ñ"
@@ -2512,7 +2634,7 @@ msgid "Archiving the project will make it entirely read only. It is hidden from
msgstr "Ðрхивирование Ñтого проекта Ñделает его полноÑтью недоÑтупным Ð´Ð»Ñ Ð¸Ð·Ð¼ÐµÐ½ÐµÐ½Ð¸Ñ. Он не будет отображатьÑÑ Ð² панели ÑƒÐ¿Ñ€Ð°Ð²Ð»ÐµÐ½Ð¸Ñ Ð¸ в поиÑке.%{strong_start}Ð’ репозиторий проекта Ð½ÐµÐ»ÑŒÐ·Ñ Ð±ÑƒÐ´ÐµÑ‚ вноÑить коммиты, а также невозможно будет Ñоздавать обÑуждениÑ, комментарии и прочее.%{strong_end}"
msgid "Are you setting up GitLab for a company?"
-msgstr ""
+msgstr "УÑтанавливаете GitLab Ð´Ð»Ñ ÐºÐ¾Ð¼Ð¿Ð°Ð½Ð¸Ð¸?"
msgid "Are you sure that you want to archive this project?"
msgstr "Ð’Ñ‹ уверены, что хотите архивировать Ñтот проект?"
@@ -2520,15 +2642,15 @@ msgstr "Ð’Ñ‹ уверены, что хотите архивировать Ñто
msgid "Are you sure that you want to unarchive this project?"
msgstr "Ð’Ñ‹ уверены, что хотите воÑÑтановить Ñтот проект?"
-msgid "Are you sure you want to cancel creating this comment?"
-msgstr "Ð’Ñ‹ уверены, что вы хотите отменить Ñоздание Ñтого комментариÑ?"
-
msgid "Are you sure you want to cancel editing this comment?"
msgstr "Ð’Ñ‹ уверены, что хотите отменить редактирование данного комментариÑ?"
-msgid "Are you sure you want to delete %{name}?"
+msgid "Are you sure you want to close this blocked issue?"
msgstr ""
+msgid "Are you sure you want to delete %{name}?"
+msgstr "Вы точно хотите удалить %{name}?"
+
msgid "Are you sure you want to delete these artifacts?"
msgstr "Ð’Ñ‹ уверены, что хотите удалить Ñти артефакты?"
@@ -2553,6 +2675,9 @@ msgstr ""
msgid "Are you sure you want to deploy this environment?"
msgstr ""
+msgid "Are you sure you want to discard this comment?"
+msgstr ""
+
msgid "Are you sure you want to erase this build?"
msgstr "Ð’Ñ‹ уверены, что вы хотите удалить Ñту Ñборку?"
@@ -2611,10 +2736,10 @@ msgid "Are you sure?"
msgstr "Вы уверены?"
msgid "Are you sure? All commits that were signed with this GPG key will be unverified."
-msgstr "Ð’Ñ‹ уверены? Ð’Ñе коммиты, которые были подпиÑаны Ñ Ñтим GPG ключом, будут не подтверждены."
+msgstr "Ð’Ñ‹ уверены? Ð’Ñе коммиты, которые были подпиÑаны Ñ Ñтим ключом GPG, не будут подтверждены."
msgid "Are you sure? Removing this GPG key does not affect already signed commits."
-msgstr "Ð’Ñ‹ уверены? Удаление Ñтого GPG ключа не повлиÑет на уже подпиÑанные коммиты."
+msgstr "Ð’Ñ‹ уверены? Удаление Ñтого ключа GPG не повлиÑет на уже подпиÑанные коммиты."
msgid "Are you sure? The device will be signed out of GitLab."
msgstr ""
@@ -2731,6 +2856,9 @@ msgstr "Ðазначение %{assignee_users_sentence}."
msgid "At least one approval from a code owner is required to change files matching the respective CODEOWNER rules."
msgstr ""
+msgid "At least one logging option is required to be enabled"
+msgstr ""
+
msgid "At least one of group_id or project_id must be specified"
msgstr ""
@@ -2738,7 +2866,7 @@ msgid "At risk"
msgstr ""
msgid "Attach a file"
-msgstr ""
+msgstr "Прикрепить файл"
msgid "Attach a file by drag &amp; drop or %{upload_link}"
msgstr "Приложить файл через drag &amp; drop или %{upload_link}"
@@ -2763,7 +2891,7 @@ msgid "AuditEvents|(removed)"
msgstr ""
msgid "AuditEvents|Action"
-msgstr ""
+msgstr "ДейÑтвие"
msgid "AuditEvents|At"
msgstr ""
@@ -2771,6 +2899,27 @@ msgstr ""
msgid "AuditEvents|Target"
msgstr ""
+msgid "AuditLogs|(removed)"
+msgstr ""
+
+msgid "AuditLogs|Action"
+msgstr ""
+
+msgid "AuditLogs|Author"
+msgstr ""
+
+msgid "AuditLogs|Date"
+msgstr ""
+
+msgid "AuditLogs|IP Address"
+msgstr ""
+
+msgid "AuditLogs|Object"
+msgstr ""
+
+msgid "AuditLogs|Target"
+msgstr ""
+
msgid "Aug"
msgstr "Ðвг."
@@ -2793,7 +2942,7 @@ msgid "Authentication Log"
msgstr "Журнал аутентификации"
msgid "Authentication failed: %{error_message}"
-msgstr ""
+msgstr "Ошибка аутентификации: %{error_message}"
msgid "Authentication log"
msgstr "Журнал аутентификации"
@@ -2829,7 +2978,7 @@ msgid "Authorize %{link_to_client} to use your account?"
msgstr "ÐвторизуйтеÑÑŒ %{link_to_client} Ð´Ð»Ñ Ð¸ÑÐ¿Ð¾Ð»ÑŒÐ·Ð¾Ð²Ð°Ð½Ð¸Ñ Ð²Ð°ÑˆÐµÐ¹ учетной запиÑи?"
msgid "Authorized %{new_chat_name}"
-msgstr ""
+msgstr "Ðвторизован %{new_chat_name}"
msgid "Authorized At"
msgstr "Ðвторизован в"
@@ -2850,7 +2999,7 @@ msgid "Auto DevOps, runners and job artifacts"
msgstr "ÐвтоматичеÑкий DevOps, обработчики и артефакты заданий"
msgid "Auto License Compliance"
-msgstr "ÐвтоматичеÑкое Ñлужба ÑÐ¾Ð±Ð»ÑŽÐ´ÐµÐ½Ð¸Ñ Ð»Ð¸Ñ†ÐµÐ½Ð·Ð¸Ð¹"
+msgstr "ÐвтоматичеÑÐºÐ°Ñ Ñлужба комплаенÑа лицензий"
msgid "Auto stop successfully canceled."
msgstr ""
@@ -2865,7 +3014,7 @@ msgid "AutoDevOps|Auto DevOps"
msgstr "ÐвтоматичеÑкий DevOps"
msgid "AutoDevOps|Auto DevOps can automatically build, test, and deploy applications based on predefined continuous integration and delivery configuration. %{auto_devops_start}Learn more about Auto DevOps%{auto_devops_end} or use our %{quickstart_start}quick start guide%{quickstart_end} to get started right away."
-msgstr ""
+msgstr "ÐвтоматичеÑкий DevOps может автоматичеÑки Ñобирать, теÑтировать и развертывать приложениÑ, оÑновываÑÑÑŒ на предпопределенных наÑтройках непрерывной интеграции и развертываниÑ. %{auto_devops_start}Узнайте больше про автоматичеÑкий ÐвтоматичеÑкий DevOps%{auto_devops_end} или иÑпользуйте наше %{quickstart_start}рукодводÑтво по быÑтрому Ñтарту%{quickstart_end}, чтобы приÑтупить Ñразу же."
msgid "AutoDevOps|Auto DevOps documentation"
msgstr "Ð”Ð¾ÐºÑƒÐ¼ÐµÐ½Ñ‚Ð°Ñ†Ð¸Ñ Ð¿Ð¾ Auto DevOps"
@@ -2919,16 +3068,16 @@ msgid "Available group Runners: %{runners}"
msgstr "ДоÑÑ‚ÑƒÐ¿Ð½Ð°Ñ Ð³Ñ€ÑƒÐ¿Ð¿Ð° runner'ов: %{runners}"
msgid "Available shared Runners:"
-msgstr ""
+msgstr "ДоÑтупные общие Runner'Ñ‹:"
msgid "Available specific runners"
msgstr "ДоÑтупные ÑпецифичеÑкие runner'Ñ‹"
msgid "Avatar for %{assigneeName}"
-msgstr ""
+msgstr "Ðватар Ð´Ð»Ñ %{assigneeName}"
msgid "Avatar for %{name}"
-msgstr ""
+msgstr "Ðватар Ð´Ð»Ñ %{name}"
msgid "Avatar will be removed. Are you sure?"
msgstr "Ðватар будет удален. Ð’Ñ‹ уверены?"
@@ -2958,7 +3107,7 @@ msgid "Badges|Add badge"
msgstr "Добавить значок"
msgid "Badges|Adding the badge failed, please check the entered URLs and try again."
-msgstr "Добавление значка не удалоÑÑŒ, пожалуйÑта, проверьте введенный URL и попробуйте еще раз."
+msgstr "Добавление значка не удалоÑÑŒ, пожалуйÑта, проверьте введенный URL и попробуйте ещё раз."
msgid "Badges|Badge image URL"
msgstr "URL-Ð°Ð´Ñ€ÐµÑ Ð¸Ð·Ð¾Ð±Ñ€Ð°Ð¶ÐµÐ½Ð¸Ñ Ð·Ð½Ð°Ñ‡ÐºÐ°"
@@ -2982,7 +3131,7 @@ msgid "Badges|Link"
msgstr "СÑылка"
msgid "Badges|Name"
-msgstr ""
+msgstr "ИмÑ"
msgid "Badges|No badge image"
msgstr "Ðет Ð¸Ð·Ð¾Ð±Ñ€Ð°Ð¶ÐµÐ½Ð¸Ñ Ð·Ð½Ð°Ñ‡ÐºÐ°"
@@ -3033,7 +3182,7 @@ msgid "Badge|New"
msgstr "Ðовый"
msgid "Balsamiq file could not be loaded."
-msgstr ""
+msgstr "Файл Balsamiq не может быть загружен."
msgid "BambooService|A continuous integration and build server"
msgstr "Сервер непрерывной интеграции и Ñборки"
@@ -3045,22 +3194,19 @@ msgid "BambooService|Atlassian Bamboo CI"
msgstr "Atlassian Bamboo CI"
msgid "BambooService|Bamboo build plan key like KEY"
-msgstr ""
+msgstr "Ключ плана Ñборки Bamboo, подобный KEY"
msgid "BambooService|Bamboo root URL like https://bamboo.example.com"
-msgstr ""
+msgstr "Корневой URL-Ð°Ð´Ñ€ÐµÑ Bamboo, подобный https://bamboo.example.com"
msgid "BambooService|You must set up automatic revision labeling and a repository trigger in Bamboo."
-msgstr ""
-
-msgid "Batch operations"
-msgstr ""
+msgstr "Ð’Ñ‹ должны наÑтроить автоматичеÑкое приÑвоение меток ревизиÑм и триггер Ñ€ÐµÐ¿Ð¾Ð·Ð¸Ñ‚Ð¾Ñ€Ð¸Ñ Ð² ÑервиÑе Bamboo."
msgid "BatchComments|Delete all pending comments"
msgstr "Удалить вÑе находÑщиеÑÑ Ð½Ð° раÑÑмотрении комментарии"
msgid "BatchComments|Discard review?"
-msgstr ""
+msgstr "Закрыть отзыв без ÑохранениÑ?"
msgid "BatchComments|You're about to discard your review which will delete all of your pending comments. The deleted comments %{strong_start}cannot%{strong_end} be restored."
msgstr ""
@@ -3078,25 +3224,25 @@ msgid "Below are examples of regex for existing tools:"
msgstr "Ðиже приведены примеры регулÑрных выражений Ð´Ð»Ñ ÑущеÑтвующих инÑтрументов:"
msgid "Below are the fingerprints for the current instance SSH host keys."
-msgstr ""
+msgstr "Ðиже приведены отпечатки ключей SSH текущего ÑкземплÑра."
msgid "Below you will find all the groups that are public."
-msgstr "Ðиже вы найдете вÑе группы, которые ÑвлÑÑŽÑ‚ÑÑ Ð¾Ñ‚ÐºÑ€Ñ‹Ñ‚Ñ‹Ð¼Ð¸."
+msgstr "Ðиже показаны вÑе открытые группы."
msgid "Billing"
msgstr "Тарифы"
msgid "BillingPlans|%{group_name} is currently using the %{plan_name} plan."
-msgstr ""
+msgstr "%{group_name} ÑÐµÐ¹Ñ‡Ð°Ñ Ð¸Ñпользует тарифный план %{plan_name}."
msgid "BillingPlans|@%{user_name} you are currently using the %{plan_name} plan."
-msgstr ""
+msgstr "@%{user_name}, ÑÐµÐ¹Ñ‡Ð°Ñ Ð²Ñ‹ иÑпользуете тарифный план %{plan_name}."
msgid "BillingPlans|Congratulations, your new trial is activated"
msgstr ""
msgid "BillingPlans|If you would like to downgrade your plan please contact %{support_link_start}Customer Support%{support_link_end}."
-msgstr ""
+msgstr "ЕÑли вы хотите понизить уровень Ñвоего тарифного плана, пожалуйÑта, ÑвÑжитеÑÑŒ Ñо %{support_link_start}Ñлужбой поддержки пользователей%{support_link_end}."
msgid "BillingPlans|Learn more about each plan by reading our %{faq_link}, or start a free 30-day trial of GitLab.com Gold."
msgstr "Узнайте больше о каждом плане, прочитав %{faq_link}, или начните иÑпользовать беÑплатный 30-дневный пробный период GitLab.com Gold."
@@ -3111,7 +3257,7 @@ msgid "BillingPlans|Pricing page"
msgstr "Страница Ñ Ñ†ÐµÐ½Ð°Ð¼Ð¸"
msgid "BillingPlans|See all %{plan_name} features"
-msgstr ""
+msgstr "Ð’Ñе возможноÑти %{plan_name}"
msgid "BillingPlans|This group uses the plan associated with its parent group."
msgstr ""
@@ -3126,7 +3272,7 @@ msgid "BillingPlans|Your GitLab.com trial expired on %{expiration_date}. You can
msgstr ""
msgid "BillingPlans|billed annually at %{price_per_year}"
-msgstr ""
+msgstr "%{price_per_year} в год"
msgid "BillingPlans|frequently asked questions"
msgstr "чаÑто задаваемые вопроÑÑ‹"
@@ -3138,7 +3284,7 @@ msgid "BillingPlans|per user"
msgstr "на одного пользователÑ"
msgid "BillingPlan|Upgrade"
-msgstr "Обновление"
+msgstr "Улучшить"
msgid "Bitbucket Server Import"
msgstr "Импорт из Bitbucket Server"
@@ -3150,10 +3296,10 @@ msgid "Blocked"
msgstr ""
msgid "Blocked issue"
-msgstr ""
+msgstr "Заблокированное обÑуждение"
msgid "Blocks"
-msgstr ""
+msgstr "Блокирует"
msgid "Blog"
msgstr "Блог"
@@ -3185,17 +3331,20 @@ msgstr "ЗапуÑк Ñ Ð½Ð°Ð±Ð¾Ñ€Ð¾Ð¼ ÑпиÑков по умолчанию п
msgid "Boards"
msgstr "ДоÑки"
+msgid "Boards and Board Lists"
+msgstr ""
+
msgid "Boards|Collapse"
msgstr "Свернуть"
msgid "Boards|Edit board"
-msgstr ""
+msgstr "Редактировать доÑку"
msgid "Boards|Expand"
msgstr "Развернуть"
msgid "Boards|View scope"
-msgstr ""
+msgstr "ПроÑмотр облаÑти"
msgid "Branch"
msgstr "Ветвь"
@@ -3378,22 +3527,22 @@ msgid "BuildArtifacts|Loading artifacts"
msgstr "Загрузка артефактов"
msgid "Built-in"
-msgstr ""
+msgstr "Ð’Ñтроенный"
msgid "Bulk request concurrency"
-msgstr ""
+msgstr "РаÑпараллеливание маÑÑовых запроÑов"
msgid "Burndown chart"
msgstr "ÐиÑходÑщий график"
msgid "BurndownChartLabel|Open issue weight"
-msgstr "Открыть Ð²ÐµÑ Ð·Ð°Ð´Ð°Ñ‡Ð¸"
+msgstr "Открыть приоритет задачи"
msgid "BurndownChartLabel|Open issues"
msgstr "Открыть обÑуждениÑ"
msgid "Business"
-msgstr ""
+msgstr "БизнеÑ"
msgid "Business metrics (Custom)"
msgstr ""
@@ -3404,11 +3553,14 @@ msgstr "Купить EE"
msgid "Buy GitLab Enterprise Edition"
msgstr "Купить верÑию GitLab Enterprise"
-msgid "By %{user_name}"
+msgid "Buy more Pipeline minutes"
msgstr ""
+msgid "By %{user_name}"
+msgstr "От %{user_name}"
+
msgid "By default GitLab sends emails in HTML and plain text formats so mail clients can choose what format to use. Disable this option if you only want to send emails in plain text format."
-msgstr ""
+msgstr "По умолчанию GitLab отправлÑет пиÑьма в формате HTML и обычного текÑта, так чтобы почтовые клиенты могли выбрать, какой формат иÑпользовать. Отключите Ñту опцию, еÑли вы хотите отправлÑÑ‚ÑŒ пиÑьма только в текÑтовом формате."
msgid "By default, all projects and groups will use the global notifications setting."
msgstr "По умолчанию вÑе проекты и группы будут иÑпользовать глобальные наÑтройки уведомлений."
@@ -3423,7 +3575,7 @@ msgid "CI / CD"
msgstr "CI / CD"
msgid "CI / CD Charts"
-msgstr ""
+msgstr "Графики CI / CD"
msgid "CI / CD Settings"
msgstr "ÐаÑтройки CI / CD"
@@ -3465,7 +3617,7 @@ msgid "CICD|Default to Auto DevOps pipeline"
msgstr "По умолчанию Ð´Ð»Ñ Ñборочной линии Auto DevOps"
msgid "CICD|Default to Auto DevOps pipeline for all projects"
-msgstr ""
+msgstr "Параметры автоматичеÑкого DevOps по умолчанию Ð´Ð»Ñ Ð²Ñех проектов"
msgid "CICD|Deployment strategy"
msgstr "Ð¡Ñ‚Ñ€Ð°Ñ‚ÐµÐ³Ð¸Ñ Ñ€Ð°Ð·Ð²Ñ‘Ñ€Ñ‚Ñ‹Ð²Ð°Ð½Ð¸Ñ"
@@ -3477,10 +3629,10 @@ msgid "CICD|The Auto DevOps pipeline will run if no alternative CI configuration
msgstr "Сборка Auto DevOps будет запущена, еÑли не будет найден иной файл конфигурации CI."
msgid "CICD|You must add a %{base_domain_link_start}base domain%{link_end} to your %{kubernetes_cluster_link_start}Kubernetes cluster%{link_end} in order for your deployment strategy to work."
-msgstr ""
+msgstr "Ð’Ñ‹ должны добавить %{base_domain_link_start}базовый домен%{link_end} в Ñвой %{kubernetes_cluster_link_start}Kubernetes клаÑтер%{link_end} Ð´Ð»Ñ Ñ‚Ð¾Ð³Ð¾, чтобы ваша ÑÑ‚Ñ€Ð°Ñ‚ÐµÐ³Ð¸Ñ Ñ€Ð°Ð·Ð²ÐµÑ€Ñ‚Ñ‹Ð²Ð°Ð½Ð¸Ñ Ñ€Ð°Ð±Ð¾Ñ‚Ð°Ð»Ð°."
msgid "CICD|You must add a %{kubernetes_cluster_link_start}Kubernetes cluster integration%{link_end} to this project with a domain in order for your deployment strategy to work correctly."
-msgstr ""
+msgstr "Ð”Ð»Ñ Ñ‚Ð¾Ð³Ð¾, чтобы ÑÑ‚Ñ€Ð°Ñ‚ÐµÐ³Ð¸Ñ Ñ€Ð°Ð·Ð²ÐµÑ€Ñ‚Ñ‹Ð²Ð°Ð½Ð¸Ñ Ð¿Ñ€Ð°Ð²Ð¸Ð»ÑŒÐ½Ð¾ работала, необходимо добавить в Ñтот проект %{kubernetes_cluster_link_start}интеграцию клаÑтера Kubernetes%{link_end} Ñ Ð´Ð¾Ð¼ÐµÐ½Ð¾Ð¼."
msgid "CICD|group enabled"
msgstr "группа включена"
@@ -3516,7 +3668,7 @@ msgid "Can't find variable: ZiteReader"
msgstr ""
msgid "Can't load mermaid module: %{err}"
-msgstr ""
+msgstr "Ðе удалоÑÑŒ загрузить Mermaid-модуль: %{err}"
msgid "Can't remove group members without group managed account"
msgstr ""
@@ -3524,11 +3676,14 @@ msgstr ""
msgid "Can't scan the code?"
msgstr "Ðе удаетÑÑ Ð¾Ñ‚Ñканировать QR-код?"
-msgid "Canary"
+msgid "Can't update snippet: %{err}"
msgstr ""
+msgid "Canary"
+msgstr "Canary"
+
msgid "Canary Deployments is a popular CI strategy, where a small portion of the fleet is updated to the new version of your application."
-msgstr ""
+msgstr "Ð Ð°Ð·Ð²Ñ‘Ñ€Ñ‚Ñ‹Ð²Ð°Ð½Ð¸Ñ Canary - популÑÑ€Ð½Ð°Ñ Ñтратегии CI, когда Ð½ÐµÐ±Ð¾Ð»ÑŒÑˆÐ°Ñ Ñ‡Ð°ÑÑ‚ÑŒ аудитории получает новую верÑию приложениÑ."
msgid "Cancel"
msgstr "Отмена"
@@ -3540,7 +3695,7 @@ msgid "Cancel this job"
msgstr "Отменить Ñто задание"
msgid "Canceled deployment to"
-msgstr ""
+msgstr "Отменено развёртывание в"
msgid "Cancelling Preview"
msgstr "Отмена предварительного проÑмотра"
@@ -3558,13 +3713,13 @@ msgid "Cannot have multiple Jira imports running at the same time"
msgstr ""
msgid "Cannot import because issues are not available in this project."
-msgstr ""
+msgstr "Ðе удаётÑÑ Ð¸Ð¼Ð¿Ð¾Ñ€Ñ‚Ð¸Ñ€Ð¾Ð²Ð°Ñ‚ÑŒ, потому что обÑÑƒÐ¶Ð´ÐµÐ½Ð¸Ñ Ð½Ðµ доÑтупны в Ñтом проекте."
msgid "Cannot make epic confidential if it contains not-confidential issues"
-msgstr ""
+msgstr "ÐÐµÐ»ÑŒÐ·Ñ Ñделать цель конфиденциальной, еÑли она Ñодержит неконфиденциальные обÑуждениÑ"
msgid "Cannot make epic confidential if it contains not-confidential sub-epics"
-msgstr ""
+msgstr "ÐÐµÐ»ÑŒÐ·Ñ Ñделать цель конфиденциальной, еÑли она Ñодержит неконфиденциальные подцели"
msgid "Cannot merge"
msgstr "Ðе удаетÑÑ Ð¾Ð±ÑŠÐµÐ´Ð¸Ð½Ð¸Ñ‚ÑŒ"
@@ -3585,7 +3740,7 @@ msgid "Cannot refer to a group milestone by an internal id!"
msgstr ""
msgid "Cannot set confidential epic for not-confidential issue"
-msgstr ""
+msgstr "ÐÐµÐ»ÑŒÐ·Ñ Ð´Ð¾Ð±Ð°Ð²Ð¸Ñ‚ÑŒ неконфиденциальое обÑуждение к конфиденциальной цели"
msgid "Cannot show preview. For previews on sketch files, they must have the file format introduced by Sketch version 43 and above."
msgstr ""
@@ -3603,7 +3758,7 @@ msgid "Certificate (PEM)"
msgstr "Сертификат (PEM)"
msgid "Certificate Issuer"
-msgstr ""
+msgstr "Эмитент Ñертификата"
msgid "Certificate Subject"
msgstr ""
@@ -3618,7 +3773,7 @@ msgid "Change assignee(s)."
msgstr ""
msgid "Change branches"
-msgstr ""
+msgstr "Сменить ветки"
msgid "Change label"
msgstr "Изменить метку"
@@ -3627,7 +3782,7 @@ msgid "Change milestone"
msgstr "Изменить Ñтап"
msgid "Change path"
-msgstr ""
+msgstr "Изменить путь"
msgid "Change permissions"
msgstr "Изменить разрешениÑ"
@@ -3692,6 +3847,9 @@ msgstr ""
msgid "Changing group path can have unintended side effects."
msgstr "Изменение пути группы может иметь побочные Ñффекты."
+msgid "Charts"
+msgstr ""
+
msgid "Charts can't be displayed as the request for data has timed out. %{documentationLink}"
msgstr ""
@@ -3699,7 +3857,7 @@ msgid "Chat"
msgstr "Чат"
msgid "ChatMessage|%{project_link}: Pipeline %{pipeline_link} of %{ref_type} %{ref_link} by %{user_combined_name} %{humanized_status} in %{duration}"
-msgstr ""
+msgstr "%{project_link}: Ð¡Ð±Ð¾Ñ€Ð¾Ñ‡Ð½Ð°Ñ Ð»Ð¸Ð½Ð¸Ñ %{pipeline_link} из %{ref_type} %{ref_link} Ð¿Ð¾Ð»ÑŒÐ·Ð¾Ð²Ð°Ñ‚ÐµÐ»Ñ %{user_combined_name} %{humanized_status} за %{duration}"
msgid "ChatMessage|Branch"
msgstr "Ветка"
@@ -3708,10 +3866,10 @@ msgid "ChatMessage|Commit"
msgstr "Коммит"
msgid "ChatMessage|Failed job"
-msgstr ""
+msgstr "Задание не выполнено"
msgid "ChatMessage|Failed stage"
-msgstr ""
+msgstr "Этап не выполнен"
msgid "ChatMessage|Invalid CI config YAML file"
msgstr "Ðеверный YAML файл конфигурации CI"
@@ -3720,7 +3878,7 @@ msgid "ChatMessage|Pipeline #%{pipeline_id} %{humanized_status} in %{duration}"
msgstr ""
msgid "ChatMessage|Pipeline %{pipeline_link} of %{ref_type} %{ref_link} by %{user_combined_name} %{humanized_status}"
-msgstr ""
+msgstr "Ð¡Ð±Ð¾Ñ€Ð¾Ñ‡Ð½Ð°Ñ Ð»Ð¸Ð½Ð¸Ñ %{pipeline_link} из %{ref_type} %{ref_link} Ð¿Ð¾Ð»ÑŒÐ·Ð¾Ð²Ð°Ñ‚ÐµÐ»Ñ %{user_combined_name} %{humanized_status}"
msgid "ChatMessage|Tag"
msgstr "Тег"
@@ -3728,9 +3886,6 @@ msgstr "Тег"
msgid "ChatMessage|and [%{count} more](%{pipeline_failed_jobs_url})"
msgstr ""
-msgid "ChatMessage|failed"
-msgstr ""
-
msgid "ChatMessage|has failed"
msgstr ""
@@ -3746,9 +3901,6 @@ msgstr "в %{duration}"
msgid "ChatMessage|in %{project_link}"
msgstr ""
-msgid "ChatMessage|passed"
-msgstr ""
-
msgid "Check again"
msgstr "Проверить Ñнова"
@@ -3762,13 +3914,13 @@ msgid "Check your .gitlab-ci.yml"
msgstr "Проверьте ваш .gitlab-ci.yml"
msgid "Check your Docker images for known vulnerabilities."
-msgstr ""
+msgstr "Проверьте ваши образы Docker на наличие извеÑтных уÑзвимоÑтей."
msgid "Checking %{text} availability…"
msgstr "Проверка доÑтупноÑти %{text} ..."
msgid "Checking approval status"
-msgstr ""
+msgstr "Проверка ÑтатуÑа утверждениÑ"
msgid "Checking branch availability..."
msgstr "Проверка доÑтупноÑти ветви..."
@@ -3780,7 +3932,7 @@ msgid "Checking username availability..."
msgstr "Проверка доÑтупноÑти имени пользователÑ..."
msgid "Checkout"
-msgstr ""
+msgstr "Оформление заказа"
msgid "Checkout|$%{selectedPlanPrice} per user per year"
msgstr ""
@@ -3792,7 +3944,7 @@ msgid "Checkout|%{name}'s GitLab subscription"
msgstr ""
msgid "Checkout|%{selectedPlanText} plan"
-msgstr ""
+msgstr "план %{selectedPlanText}"
msgid "Checkout|%{startDate} - %{endDate}"
msgstr ""
@@ -3810,7 +3962,7 @@ msgid "Checkout|3. Your GitLab group"
msgstr ""
msgid "Checkout|Billing address"
-msgstr ""
+msgstr "Платёжный адреÑ"
msgid "Checkout|Checkout"
msgstr ""
@@ -3825,7 +3977,7 @@ msgid "Checkout|Confirming..."
msgstr ""
msgid "Checkout|Continue to billing"
-msgstr ""
+msgstr "Перейти к оплате"
msgid "Checkout|Continue to payment"
msgstr ""
@@ -3864,13 +4016,13 @@ msgid "Checkout|Failed to register credit card. Please try again."
msgstr ""
msgid "Checkout|GitLab group"
-msgstr ""
+msgstr "Группа GitLab"
msgid "Checkout|GitLab plan"
-msgstr ""
+msgstr "План GitLab"
msgid "Checkout|Group"
-msgstr ""
+msgstr "Группа"
msgid "Checkout|Name of company or organization using GitLab"
msgstr ""
@@ -3879,49 +4031,49 @@ msgid "Checkout|Need more users? Purchase GitLab for your %{company}."
msgstr ""
msgid "Checkout|Number of users"
-msgstr ""
+msgstr "КоличеÑтво пользователей"
msgid "Checkout|Payment method"
-msgstr ""
+msgstr "СпоÑоб оплаты"
msgid "Checkout|Please select a country"
-msgstr ""
+msgstr "ПожалуйÑта, выберите Ñтрану"
msgid "Checkout|Please select a state"
-msgstr ""
+msgstr "ПожалуйÑта, выберите штат"
msgid "Checkout|Select"
-msgstr ""
+msgstr "Выбрать"
msgid "Checkout|State"
-msgstr ""
+msgstr "СоÑтоÑние"
msgid "Checkout|Street address"
-msgstr ""
+msgstr "ÐÐ´Ñ€ÐµÑ ÑƒÐ»Ð¸Ñ†Ñ‹"
msgid "Checkout|Submitting the credit card form failed with code %{errorCode}: %{errorMessage}"
msgstr ""
msgid "Checkout|Subscription details"
-msgstr ""
+msgstr "Детали подпиÑки"
msgid "Checkout|Subtotal"
-msgstr ""
+msgstr "Подытог"
msgid "Checkout|Tax"
-msgstr ""
+msgstr "Ðалоги"
msgid "Checkout|Total"
-msgstr ""
+msgstr "Итого"
msgid "Checkout|Users"
-msgstr ""
+msgstr "Пользователи"
msgid "Checkout|You'll create your new group after checkout"
msgstr ""
msgid "Checkout|Your organization"
-msgstr ""
+msgstr "Ваша организациÑ"
msgid "Checkout|Your subscription will be applied to this group"
msgstr ""
@@ -3930,7 +4082,7 @@ msgid "Checkout|Zip code"
msgstr ""
msgid "Checkout|company or team"
-msgstr ""
+msgstr "ÐºÐ¾Ð¼Ð¿Ð°Ð½Ð¸Ñ Ð¸Ð»Ð¸ команда"
msgid "Cherry-pick this commit"
msgstr "Подобрать в Ñтом коммите"
@@ -3981,7 +4133,7 @@ msgid "Choose between <code>clone</code> or <code>fetch</code> to get the recent
msgstr ""
msgid "Choose file…"
-msgstr ""
+msgstr "Выберите файл…"
msgid "Choose the top-level group for your repository imports."
msgstr ""
@@ -3993,7 +4145,7 @@ msgid "Choose what content you want to see on a group’s overview page"
msgstr ""
msgid "Choose which groups you wish to synchronize to this secondary node"
-msgstr ""
+msgstr "Выберите, какие группы вы хотите Ñинхронизировать Ñо вторичным узлом"
msgid "Choose which repositories you want to connect and run CI/CD pipelines."
msgstr ""
@@ -4002,7 +4154,7 @@ msgid "Choose which shards you wish to synchronize to this secondary node"
msgstr ""
msgid "Choose your framework"
-msgstr ""
+msgstr "Выберите фреймворк"
msgid "CiStatusLabel|canceled"
msgstr "отменено"
@@ -4011,7 +4163,7 @@ msgid "CiStatusLabel|created"
msgstr "Ñоздано"
msgid "CiStatusLabel|delayed"
-msgstr ""
+msgstr "отложено"
msgid "CiStatusLabel|failed"
msgstr "неудачно"
@@ -4035,13 +4187,13 @@ msgid "CiStatusLabel|skipped"
msgstr "пропущено"
msgid "CiStatusLabel|waiting for delayed job"
-msgstr ""
+msgstr "ожидание отложенного заданиÑ"
msgid "CiStatusLabel|waiting for manual action"
msgstr "ожидание ручных дейÑтвий"
msgid "CiStatusLabel|waiting for resource"
-msgstr ""
+msgstr "ожидание реÑурÑа"
msgid "CiStatusText|blocked"
msgstr "заблокировано"
@@ -4053,7 +4205,7 @@ msgid "CiStatusText|created"
msgstr "Ñоздано"
msgid "CiStatusText|delayed"
-msgstr ""
+msgstr "отложено"
msgid "CiStatusText|failed"
msgstr "неудачно"
@@ -4068,22 +4220,22 @@ msgid "CiStatusText|pending"
msgstr "в ожидании"
msgid "CiStatusText|preparing"
-msgstr ""
+msgstr "подготавливаетÑÑ"
msgid "CiStatusText|skipped"
msgstr "пропущено"
msgid "CiStatusText|waiting"
-msgstr ""
+msgstr "в ожидании"
msgid "CiStatus|running"
msgstr "выполнÑетÑÑ"
msgid "CiVariables|Cannot use Masked Variable with current value"
-msgstr ""
+msgstr "Ðевозможно иÑпользовать маÑкируемую переменную Ñ Ñ‚ÐµÐºÑƒÑ‰Ð¸Ð¼ значением"
msgid "CiVariables|Environments"
-msgstr ""
+msgstr "ОкружениÑ"
msgid "CiVariables|Input variable key"
msgstr "Ключ входной переменной"
@@ -4092,31 +4244,31 @@ msgid "CiVariables|Input variable value"
msgstr "Значение входной переменной"
msgid "CiVariables|Key"
-msgstr ""
+msgstr "Ключ"
msgid "CiVariables|Masked"
-msgstr ""
+msgstr "МаÑкируемаÑ"
msgid "CiVariables|Protected"
-msgstr ""
+msgstr "ЗащищеннаÑ"
msgid "CiVariables|Remove variable row"
msgstr "Удалить Ñтроку переменных"
msgid "CiVariables|Scope"
-msgstr ""
+msgstr "ОблаÑÑ‚ÑŒ видимоÑти"
msgid "CiVariables|Specify variable values to be used in this run. The values specified in %{linkStart}CI/CD settings%{linkEnd} will be used as default"
msgstr "Укажите Ð·Ð½Ð°Ñ‡ÐµÐ½Ð¸Ñ Ð¿ÐµÑ€ÐµÐ¼ÐµÐ½Ð½Ñ‹Ñ…, которые будут иÑпользоватьÑÑ Ð² Ñтой Ñборочной линии. ЗначениÑ, указанные в %{linkStart} наÑтройках CI / CD %{linkEnd} будут иÑпользоватьÑÑ Ð¿Ð¾ умолчанию"
msgid "CiVariables|State"
-msgstr ""
+msgstr "СоÑтоÑние"
msgid "CiVariables|Type"
-msgstr ""
+msgstr "Тип"
msgid "CiVariables|Value"
-msgstr ""
+msgstr "Значение"
msgid "CiVariables|Variables"
msgstr "Переменные"
@@ -4128,22 +4280,22 @@ msgid "CiVariable|All environments"
msgstr "Ð’Ñе Ñреды"
msgid "CiVariable|Create wildcard"
-msgstr ""
+msgstr "Создать шаблон"
msgid "CiVariable|Error occurred while saving variables"
-msgstr ""
+msgstr "Ошибка при Ñохранении переменных"
msgid "CiVariable|Masked"
-msgstr ""
+msgstr "ЗамаÑкировано"
msgid "CiVariable|New environment"
-msgstr ""
+msgstr "Ðовое окружение"
msgid "CiVariable|Protected"
msgstr "Защищено"
msgid "CiVariable|Search environments"
-msgstr ""
+msgstr "ИÑкать окружениÑ"
msgid "CiVariable|Toggle masked"
msgstr ""
@@ -4155,13 +4307,13 @@ msgid "CiVariable|Validation failed"
msgstr "Проверка не удалаÑÑŒ"
msgid "Class"
-msgstr ""
+msgstr "КлаÑÑ"
msgid "Class:"
-msgstr ""
+msgstr "КлаÑÑ:"
msgid "Classification Label (optional)"
-msgstr ""
+msgstr "Метка клаÑÑификации (опционально)"
msgid "ClassificationLabelUnavailable|is unavailable: %{reason}"
msgstr ""
@@ -4188,20 +4340,17 @@ msgid "Clear templates search input"
msgstr "ОчиÑтить шаблоны ввода данных Ð´Ð»Ñ Ð¿Ð¾Ð¸Ñка"
msgid "Clear weight"
-msgstr ""
+msgstr "ОчиÑтить приоритет"
msgid "Cleared weight."
-msgstr ""
+msgstr "Приоритет очищен."
msgid "Clears weight."
-msgstr ""
+msgstr "Очищает приоритет."
msgid "Click any <strong>project name</strong> in the project list below to navigate to the project milestone."
msgstr "Выберите из ÑпиÑка любой <strong>проект</strong>, чтобы перейти к Ñтапу проекта."
-msgid "Click here"
-msgstr "Ðажмите здеÑÑŒ"
-
msgid "Click the <strong>Download</strong> button and wait for downloading to complete."
msgstr ""
@@ -4215,7 +4364,7 @@ msgid "Click the button below to begin the install process by navigating to the
msgstr "Ðажмите кнопку ниже, чтобы начать процеÑÑ ÑƒÑтановки, Ð¿ÐµÑ€ÐµÐ¹Ð´Ñ Ð½Ð° Ñтраницу Kubernetes"
msgid "Click the image where you'd like to start a new discussion"
-msgstr ""
+msgstr "Ðажмите на тот образ, где вы хотите начать новую диÑкуÑÑию"
msgid "Click to expand it."
msgstr "Ðажмите, чтобы развернуть."
@@ -4311,7 +4460,7 @@ msgid "ClusterIntegration| can be used instead of a custom domain."
msgstr ""
msgid "ClusterIntegration| is the default environment scope for this cluster. This means that all jobs, regardless of their environment, will use this cluster. %{environment_scope_start}More information%{environment_scope_end}"
-msgstr ""
+msgstr "ÑвлÑетÑÑ Ð¾Ð±Ð»Ð°Ñтью Ñреды Ð´Ð»Ñ Ñтого клаÑтера по умолчанию. Это значит, что вÑе заданиÑ, незавиÑимо от Ñреды, будут иÑпользовать Ñтот клаÑтер. %{environment_scope_start}Подробнее%{environment_scope_end}"
msgid "ClusterIntegration|%{appList} was successfully installed on your Kubernetes cluster"
msgstr "%{appList} уÑпешно уÑтановлены на вашем клаÑтере Kubernetes"
@@ -4320,34 +4469,34 @@ msgid "ClusterIntegration|%{external_ip}.nip.io"
msgstr ""
msgid "ClusterIntegration|%{title} uninstalled successfully."
-msgstr ""
+msgstr "%{title} уÑпешно удалён."
msgid "ClusterIntegration|%{title} updated successfully."
msgstr "%{title} уÑпешно обновлен."
msgid "ClusterIntegration|A cluster management project can be used to run deployment jobs with Kubernetes <code>cluster-admin</code> privileges."
-msgstr ""
+msgstr "Проект ÑƒÐ¿Ñ€Ð°Ð²Ð»ÐµÐ½Ð¸Ñ ÐºÐ»Ð°Ñтером можно иÑпользовать Ð´Ð»Ñ Ð·Ð°Ð¿ÑƒÑка заданий Ñ€Ð°Ð·Ð²ÐµÑ€Ñ‚Ñ‹Ð²Ð°Ð½Ð¸Ñ Ñ Ð¿Ñ€Ð¸Ð²Ð¸Ð»ÐµÐ³Ð¸Ñми Kubernetes <code>cluster-admin</code>."
msgid "ClusterIntegration|A service token scoped to %{code}kube-system%{end_code} with %{code}cluster-admin%{end_code} privileges."
-msgstr ""
+msgstr "ОблаÑÑ‚ÑŒ доÑтупа токена Ñлужбы уÑтановлена на %{code}kube-system%{end_code} Ñ Ð¿Ñ€Ð¸Ð²Ð¸Ð»ÐµÐ³Ð¸Ñми %{code}cluster-admin%{end_code}."
msgid "ClusterIntegration|API URL"
msgstr "ÐÐ´Ñ€ÐµÑ API"
msgid "ClusterIntegration|API URL should be a valid http/https url."
-msgstr ""
+msgstr "URL API должен быть правильным адреÑом http/https."
msgid "ClusterIntegration|Add Kubernetes cluster"
msgstr "Добавить клаÑтер Kubernetes"
msgid "ClusterIntegration|Add a Kubernetes cluster integration"
-msgstr ""
+msgstr "Добавить интеграцию Ñ ÐºÐ»Ð°Ñтером Kubernetes"
msgid "ClusterIntegration|Adding a Kubernetes cluster to your group will automatically share the cluster across all your projects. Use review apps, deploy your applications, and easily run your pipelines for all projects using the same cluster."
msgstr ""
msgid "ClusterIntegration|Adding a Kubernetes cluster will automatically share the cluster across all projects. Use review apps, deploy your applications, and easily run your pipelines for all projects using the same cluster."
-msgstr ""
+msgstr "Добавление клаÑтера Kubernetes автоматичеÑки разделит клаÑтер между вÑеми проектами. ИÑпользуйте Ð¿Ñ€Ð¸Ð»Ð¾Ð¶ÐµÐ½Ð¸Ñ Ð´Ð»Ñ Ñ€ÐµÑ†ÐµÐ½Ð·Ð¸Ñ€Ð¾Ð²Ð°Ð½Ð¸Ñ, развертывайте Ð¿Ñ€Ð¸Ð»Ð¾Ð¶ÐµÐ½Ð¸Ñ Ð¸ легко запуÑкайте Ñборочные линии Ð´Ð»Ñ Ð²Ñех проектов, иÑпользующих один и тот же клаÑтер."
msgid "ClusterIntegration|Adding an integration to your group will share the cluster across all your projects."
msgstr ""
@@ -4362,49 +4511,49 @@ msgid "ClusterIntegration|All data not committed to GitLab will be deleted and c
msgstr "Ð’Ñе данные, не переданные в GitLab, будут удалены и не могут быть воÑÑтановлены."
msgid "ClusterIntegration|All data will be deleted and cannot be restored."
-msgstr ""
+msgstr "Ð’Ñе данные будут удалены без возможноÑти воÑÑтановлениÑ."
msgid "ClusterIntegration|Allow GitLab to manage namespace and service accounts for this cluster."
-msgstr ""
+msgstr "Разрешить GitLab управлÑÑ‚ÑŒ проÑтранÑтвами имен и Ñлужебными аккаунтами Ñтого клаÑтера."
msgid "ClusterIntegration|Allow GitLab to manage namespace and service accounts for this cluster. %{startLink}More information%{endLink}"
msgstr "Разрешить GitLab управлÑÑ‚ÑŒ проÑтранÑтвом имен и учетными запиÑÑми Ñлужб Ð´Ð»Ñ Ñтого клаÑтера. %{startLink}Ð”Ð¾Ð¿Ð¾Ð»Ð½Ð¸Ñ‚ÐµÐ»ÑŒÐ½Ð°Ñ Ð¸Ð½Ñ„Ð¾Ñ€Ð¼Ð°Ñ†Ð¸Ñ%{endLink}"
msgid "ClusterIntegration|Alternatively"
-msgstr ""
+msgstr "Как вариант"
msgid "ClusterIntegration|Amazon EKS"
msgstr "Amazon EKS"
msgid "ClusterIntegration|An error occurred when trying to contact the Google Cloud API. Please try again later."
-msgstr ""
+msgstr "Произошла ошибка при попытке ÑвÑзатьÑÑ Ñ Google Cloud API. ПожалуйÑта, попробуйте ещё раз позже."
msgid "ClusterIntegration|An error occurred while trying to fetch project zones: %{error}"
-msgstr ""
+msgstr "Произошла ошибка при попытке Ð¿Ð¾Ð»ÑƒÑ‡ÐµÐ½Ð¸Ñ Ð¾Ð±Ð»Ð°Ñтей проекта: %{error}"
msgid "ClusterIntegration|An error occurred while trying to fetch your projects: %{error}"
-msgstr ""
+msgstr "Произошла ошибка при попытке Ð¿Ð¾Ð»ÑƒÑ‡ÐµÐ½Ð¸Ñ Ð²Ð°ÑˆÐ¸Ñ… проектов: %{error}"
msgid "ClusterIntegration|An error occurred while trying to fetch zone machine types: %{error}"
-msgstr ""
+msgstr "Произошла ошибка при попытке извлечь типы машин облаÑти: %{error}"
msgid "ClusterIntegration|Any running pipelines will be canceled."
msgstr ""
msgid "ClusterIntegration|Apply for credit"
-msgstr ""
+msgstr "Подать заÑвку на кредит"
msgid "ClusterIntegration|Authenticate with AWS"
-msgstr ""
+msgstr "ÐÑƒÑ‚ÐµÐ½Ñ‚Ð¸Ñ„Ð¸ÐºÐ°Ñ†Ð¸Ñ Ñ Ð¿Ð¾Ð¼Ð¾Ñ‰ÑŒÑŽ AWS"
msgid "ClusterIntegration|Authenticate with Amazon Web Services"
-msgstr ""
+msgstr "ÐÑƒÑ‚ÐµÐ½Ñ‚Ð¸Ñ„Ð¸ÐºÐ°Ñ†Ð¸Ñ Ñ Ð¿Ð¾Ð¼Ð¾Ñ‰ÑŒÑŽ Amazon Web Services"
msgid "ClusterIntegration|Base domain"
-msgstr ""
+msgstr "Базовое доменное имÑ"
msgid "ClusterIntegration|Blocking mode"
-msgstr ""
+msgstr "Блокирующий режим"
msgid "ClusterIntegration|CA Certificate"
msgstr "Сертификат удоÑтоверÑющего центра"
@@ -4428,31 +4577,31 @@ msgid "ClusterIntegration|Choose the worker node %{startLink}instance type %{ext
msgstr ""
msgid "ClusterIntegration|Choose which applications to install on your Kubernetes cluster. Helm Tiller is required to install any of the following applications."
-msgstr ""
+msgstr "Выберите, какие Ð¿Ñ€Ð¸Ð»Ð¾Ð¶ÐµÐ½Ð¸Ñ Ð½ÐµÐ¾Ð±Ñ…Ð¾Ð´Ð¸Ð¼Ð¾ уÑтановить на ваш клаÑтер Kubernetes. Ð”Ð»Ñ ÑƒÑтановки любого из Ñледующих приложений требуетÑÑ Helm Tiller.\n\n"
msgid "ClusterIntegration|Choose which of your environments will use this cluster."
-msgstr ""
+msgstr "Выберите, какие из ваших окружений будут иÑпользовать Ñтот клаÑтер."
msgid "ClusterIntegration|Clear cluster cache"
-msgstr ""
+msgstr "ОчиÑтить кÑш клаÑтера"
msgid "ClusterIntegration|Clear the local cache of namespace and service accounts. This is necessary if your integration has become out of sync. The cache is repopulated during the next CI job that requires namespace and service accounts."
msgstr ""
msgid "ClusterIntegration|Cloud Run"
-msgstr ""
+msgstr "Cloud Run"
msgid "ClusterIntegration|Cluster being created"
-msgstr ""
+msgstr "КлаÑтер ÑоздаетÑÑ"
msgid "ClusterIntegration|Cluster management project (alpha)"
-msgstr ""
+msgstr "Проект ÑƒÐ¿Ñ€Ð°Ð²Ð»ÐµÐ½Ð¸Ñ ÐºÐ»Ð°Ñтером (alpha)"
msgid "ClusterIntegration|Cluster name is required."
-msgstr ""
+msgstr "ТребуетÑÑ Ð½Ð°Ð·Ð²Ð°Ð½Ð¸Ðµ клаÑтера."
msgid "ClusterIntegration|Clusters are utilized by selecting the nearest ancestor with a matching environment scope. For example, project clusters will override group clusters."
-msgstr ""
+msgstr "КлаÑтеры иÑпользуютÑÑ Ð¿ÑƒÑ‚Ñ‘Ð¼ выбора ближайшего предка Ñ Ð¿Ð¾Ð´Ñ…Ð¾Ð´Ñщей облаÑтью видимоÑти Ñреды. Ðапример, клаÑтеры проектов будут переопределÑÑ‚ÑŒ клаÑтеры групп."
msgid "ClusterIntegration|Copy API URL"
msgstr "Скопировать Ð°Ð´Ñ€ÐµÑ API"
@@ -4461,46 +4610,46 @@ msgid "ClusterIntegration|Copy CA Certificate"
msgstr "Копировать Сертификат УдоÑтоверÑющего Центра"
msgid "ClusterIntegration|Copy Ingress Endpoint"
-msgstr ""
+msgstr "Скопировать точку Ð¿Ð¾Ð´ÐºÐ»ÑŽÑ‡ÐµÐ½Ð¸Ñ Ingress"
msgid "ClusterIntegration|Copy Jupyter Hostname"
msgstr "Скопировать Ð¸Ð¼Ñ Ñ…Ð¾Ñта Jupyter"
msgid "ClusterIntegration|Copy Knative Endpoint"
-msgstr ""
+msgstr "Скопировать точку Ð¿Ð¾Ð´ÐºÐ»ÑŽÑ‡ÐµÐ½Ð¸Ñ Knative"
msgid "ClusterIntegration|Copy Kubernetes cluster name"
msgstr "Скопировать Ð¸Ð¼Ñ ÐºÐ»Ð°Ñтера Kubernetes"
msgid "ClusterIntegration|Copy Service Token"
-msgstr ""
+msgstr "Скопировать токен ÑервиÑа"
msgid "ClusterIntegration|Could not load IAM roles"
msgstr "Ðе удалоÑÑŒ загрузить IAM роли"
msgid "ClusterIntegration|Could not load Key Pairs"
-msgstr ""
+msgstr "Ðе удалоÑÑŒ загрузить пары ключей"
msgid "ClusterIntegration|Could not load VPCs for the selected region"
msgstr "Ðе удалоÑÑŒ загрузить VPC Ð´Ð»Ñ Ð²Ñ‹Ð±Ñ€Ð°Ð½Ð½Ð¾Ð³Ð¾ региона"
msgid "ClusterIntegration|Could not load instance types"
-msgstr ""
+msgstr "Ðе удалоÑÑŒ загрузить типы ÑкземплÑров"
msgid "ClusterIntegration|Could not load networks"
msgstr ""
msgid "ClusterIntegration|Could not load regions from your AWS account"
-msgstr ""
+msgstr "Ðе удалоÑÑŒ загрузить регионы из вашего аккаунта AWS"
msgid "ClusterIntegration|Could not load security groups for the selected VPC"
msgstr "Ðе удалоÑÑŒ загрузить группы безопаÑноÑти Ð´Ð»Ñ Ð²Ñ‹Ð±Ñ€Ð°Ð½Ð½Ð¾Ð³Ð¾ VPC"
msgid "ClusterIntegration|Could not load subnets for the selected VPC"
-msgstr ""
+msgstr "Ðе удалоÑÑŒ загрузить подÑети Ð´Ð»Ñ Ð²Ñ‹Ð±Ñ€Ð°Ð½Ð½Ð¾Ð³Ð¾ VPC"
msgid "ClusterIntegration|Could not load subnetworks"
-msgstr ""
+msgstr "Ðе удалоÑÑŒ загрузить подÑети"
msgid "ClusterIntegration|Create Kubernetes cluster"
msgstr "Создать клаÑтер Kubernetes"
@@ -4509,22 +4658,22 @@ msgid "ClusterIntegration|Create a provision role on %{startAwsLink}Amazon Web S
msgstr ""
msgid "ClusterIntegration|Create cluster on"
-msgstr ""
+msgstr "Создать клаÑтер на"
msgid "ClusterIntegration|Create new cluster"
-msgstr ""
+msgstr "Создать новый клаÑтер"
msgid "ClusterIntegration|Create new cluster on EKS"
-msgstr ""
+msgstr "Создать новый клаÑтер на EKS"
msgid "ClusterIntegration|Create new cluster on GKE"
-msgstr ""
+msgstr "Создать новый клаÑтер на GKE"
msgid "ClusterIntegration|Creating Kubernetes cluster"
-msgstr ""
+msgstr "СоздаетÑÑ ÐºÐ»Ð°Ñтер Kubernetes"
msgid "ClusterIntegration|Crossplane"
-msgstr ""
+msgstr "Crossplane"
msgid "ClusterIntegration|Crossplane enables declarative provisioning of managed services from your cloud of choice using %{kubectl} or %{gitlabIntegrationLink}. Crossplane runs inside your Kubernetes cluster and supports secure connectivity and secrets management between app containers and the cloud services they depend on."
msgstr ""
@@ -4533,22 +4682,22 @@ msgid "ClusterIntegration|Deletes all GitLab resources attached to this cluster
msgstr ""
msgid "ClusterIntegration|Did you know?"
-msgstr ""
+msgstr "Знаете ли вы?"
msgid "ClusterIntegration|Elastic Kubernetes Service"
-msgstr ""
+msgstr "Служба Elastic Kubernetes"
msgid "ClusterIntegration|Elastic Stack"
-msgstr ""
+msgstr "Elastic Stack"
msgid "ClusterIntegration|Enable Cloud Run for Anthos"
msgstr ""
msgid "ClusterIntegration|Enable or disable GitLab's connection to your Kubernetes cluster."
-msgstr ""
+msgstr "Включить или выключить Ñоединение GitLab Ñ Ð²Ð°ÑˆÐ¸Ð¼ Kubernetes клаÑтером."
msgid "ClusterIntegration|Enable this setting if using role-based access control (RBAC)."
-msgstr ""
+msgstr "Включите Ñтот параметр, еÑли иÑпользуетÑÑ Ñ€Ð¾Ð»ÐµÐ²Ð°Ñ Ð¼Ð¾Ð´ÐµÐ»ÑŒ доÑтупа (RBAC)."
msgid "ClusterIntegration|Enabled stack"
msgstr ""
@@ -4563,22 +4712,22 @@ msgid "ClusterIntegration|Environment scope"
msgstr "Среда окружениÑ"
msgid "ClusterIntegration|Every new Google Cloud Platform (GCP) account receives $300 in credit upon %{sign_up_link}. In partnership with Google, GitLab is able to offer an additional $200 for both new and existing GCP accounts to get started with GitLab's Google Kubernetes Engine Integration."
-msgstr ""
+msgstr "ÐšÐ°Ð¶Ð´Ð°Ñ Ð½Ð¾Ð²Ð°Ñ ÑƒÑ‡ÐµÑ‚Ð½Ð°Ñ Ð·Ð°Ð¿Ð¸ÑÑŒ Google Cloud Platform (GCP) получает по $300 в кредит по %{sign_up_link}. Ð’ партнерÑтве Ñ Google, GitLab может предложить дополнительные $200 Ð´Ð»Ñ Ð½Ð¾Ð²Ñ‹Ñ… учетных запиÑей GCP, чтобы начать работу Ñ GitLab Ñ Ð¸Ð½Ñ‚ÐµÐ³Ñ€Ð°Ñ†Ð¸ÐµÐ¹ Google Kubernetes Engine."
msgid "ClusterIntegration|Failed to configure EKS provider: %{message}"
msgstr ""
msgid "ClusterIntegration|Failed to configure Google Kubernetes Engine Cluster: %{message}"
-msgstr ""
+msgstr "Ðе удалоÑÑŒ наÑтроить клаÑтер Google Kubernetes Engine: %{message}"
msgid "ClusterIntegration|Failed to fetch CloudFormation stack: %{message}"
msgstr ""
msgid "ClusterIntegration|Failed to request to Google Cloud Platform: %{message}"
-msgstr ""
+msgstr "Ðе удалоÑÑŒ обратитьÑÑ Ðº Google Cloud Platform: %{message}"
msgid "ClusterIntegration|Failed to run Kubeclient: %{message}"
-msgstr ""
+msgstr "Ðе удалоÑÑŒ запуÑтить Kubeclient: %{message}"
msgid "ClusterIntegration|Fetching machine types"
msgstr "Получение типов машин клаÑтера"
@@ -4589,6 +4738,12 @@ msgstr "Получение проектов"
msgid "ClusterIntegration|Fetching zones"
msgstr "Получение зон"
+msgid "ClusterIntegration|Fluentd"
+msgstr ""
+
+msgid "ClusterIntegration|Fluentd is an open source data collector, which lets you unify the data collection and consumption for a better use and understanding of data. It requires at least one of the following logs to be successfully installed."
+msgstr ""
+
msgid "ClusterIntegration|GitLab Integration"
msgstr "Ð˜Ð½Ñ‚ÐµÐ³Ñ€Ð°Ñ†Ð¸Ñ Ñ GitLab"
@@ -4602,13 +4757,13 @@ msgid "ClusterIntegration|GitLab-managed cluster"
msgstr "КлаÑтер управлÑемый GitLab"
msgid "ClusterIntegration|Gitlab Integration"
-msgstr ""
+msgstr "Ð˜Ð½Ñ‚ÐµÐ³Ñ€Ð°Ñ†Ð¸Ñ Ñ Gitlab"
msgid "ClusterIntegration|Global default"
-msgstr ""
+msgstr "Везде по умолчанию"
msgid "ClusterIntegration|Google Cloud Platform project"
-msgstr ""
+msgstr "Проект в Google Cloud Platform"
msgid "ClusterIntegration|Google GKE"
msgstr "Google GKE"
@@ -4626,7 +4781,7 @@ msgid "ClusterIntegration|Helm Tiller"
msgstr "Helm Tiller"
msgid "ClusterIntegration|Helm streamlines installing and managing Kubernetes applications. Tiller runs inside of your Kubernetes Cluster, and manages releases of your charts."
-msgstr ""
+msgstr "Приложение Helm упрощает уÑтановку и управление приложениÑми Kubernetes. Приложение Tiller запуÑкаетÑÑ Ð²Ð½ÑƒÑ‚Ñ€Ð¸ вашего клаÑтера Kubernetes и управлÑет релизами ваших реÑурÑов."
msgid "ClusterIntegration|Hide"
msgstr "Скрыть"
@@ -4644,28 +4799,28 @@ msgid "ClusterIntegration|Ingress"
msgstr "Ingress"
msgid "ClusterIntegration|Ingress Endpoint"
-msgstr ""
+msgstr "Точка Ð¿Ð¾Ð´ÐºÐ»ÑŽÑ‡ÐµÐ½Ð¸Ñ Ingress"
msgid "ClusterIntegration|Ingress gives you a way to route requests to services based on the request host or path, centralizing a number of services into a single entrypoint."
-msgstr ""
+msgstr "Ingress позволÑет маршрутизировать запроÑÑ‹ к Ñлужбам на оÑнове запрошенного хоÑта или пути, объединÑÑ Ñ€Ñд ÑервиÑов в одну точку входа."
msgid "ClusterIntegration|Installing Ingress may incur additional costs. Learn more about %{pricingLink}."
-msgstr ""
+msgstr "УÑтановка Ingress может повлечь дополнительные затраты. Узнайте больше по ÑÑылке %{pricingLink}."
msgid "ClusterIntegration|Instance cluster"
msgstr "ЭкземплÑÑ€ клаÑтера"
msgid "ClusterIntegration|Instance type"
-msgstr ""
+msgstr "Тип ÑкземплÑра"
msgid "ClusterIntegration|Integrate Kubernetes cluster automation"
msgstr "Ð˜Ð½Ñ‚ÐµÐ³Ñ€Ð°Ñ†Ð¸Ñ Ð°Ð²Ñ‚Ð¾Ð¼Ð°Ñ‚Ð¸Ð·Ð°Ñ†Ð¸Ð¸ клаÑтеров Kubernetes"
msgid "ClusterIntegration|Issuer Email"
-msgstr ""
+msgstr "Ð­Ð»ÐµÐºÑ‚Ñ€Ð¾Ð½Ð½Ð°Ñ Ð¿Ð¾Ñ‡Ñ‚Ð° Ñмитента"
msgid "ClusterIntegration|Issuers represent a certificate authority. You must provide an email address for your Issuer. "
-msgstr ""
+msgstr "Эмитенты предÑтавлÑÑŽÑ‚ центры Ñертификации. Ð’Ñ‹ должны указать Ñлектронную почту вашего Ñмитента. "
msgid "ClusterIntegration|Jupyter Hostname"
msgstr "Ð˜Ð¼Ñ Ñ…Ð¾Ñта ÑервиÑа Jupyter"
@@ -4680,19 +4835,19 @@ msgid "ClusterIntegration|Key pair name"
msgstr "Ð˜Ð¼Ñ Ð¿Ð°Ñ€Ñ‹ ключей"
msgid "ClusterIntegration|Knative"
-msgstr ""
+msgstr "Knative"
msgid "ClusterIntegration|Knative Domain Name:"
-msgstr ""
+msgstr "Доменное Ð¸Ð¼Ñ Knative:"
msgid "ClusterIntegration|Knative Endpoint:"
-msgstr ""
+msgstr "Точка Ð¿Ð¾Ð´ÐºÐ»ÑŽÑ‡ÐµÐ½Ð¸Ñ Knative:"
msgid "ClusterIntegration|Knative domain name was updated successfully."
-msgstr ""
+msgstr "Доменное Ð¸Ð¼Ñ Knative уÑпешно обновлено."
msgid "ClusterIntegration|Knative extends Kubernetes to provide a set of middleware components that are essential to build modern, source-centric, and container-based applications that can run anywhere: on premises, in the cloud, or even in a third-party data center."
-msgstr ""
+msgstr "Knative раÑширÑет возможноÑти Kubernetes в чаÑти обеÑÐ¿ÐµÑ‡ÐµÐ½Ð¸Ñ Ð½Ð°Ð±Ð¾Ñ€Ð¾Ð¼ промежуточных компонентов, которые необходимы Ð´Ð»Ñ Ñ€Ð°Ð·Ñ€Ð°Ð±Ð¾Ñ‚ÐºÐ¸ Ñовременных, оÑнованных на иÑходных данных и контейнерах, приложений, которые могут выполнÑÑ‚ÑŒÑÑ Ð³Ð´Ðµ угодно: локально, в облаках и даже в Ñторонних дата-центрах."
msgid "ClusterIntegration|Kubernetes cluster"
msgstr "КлаÑтер Kubernetes"
@@ -4746,7 +4901,7 @@ msgid "ClusterIntegration|Loading Key Pairs"
msgstr ""
msgid "ClusterIntegration|Loading Regions"
-msgstr ""
+msgstr "Загрузка регионов"
msgid "ClusterIntegration|Loading VPCs"
msgstr "Загрузка VPCs"
@@ -4773,7 +4928,7 @@ msgid "ClusterIntegration|Machine type"
msgstr "Тип машины"
msgid "ClusterIntegration|Make sure your account %{link_to_requirements} to create Kubernetes clusters"
-msgstr "УбедитеÑÑŒ, что ваша ÑƒÑ‡ÐµÑ‚Ð½Ð°Ñ Ð·Ð°Ð¿Ð¸ÑÑŒ %{link_to_requirements} Ð´Ð»Ñ ÑÐ¾Ð·Ð´Ð°Ð½Ð¸Ñ ÐºÐ»Ð°Ñтеров"
+msgstr "УбедитеÑÑŒ, что ваша ÑƒÑ‡Ñ‘Ñ‚Ð½Ð°Ñ Ð·Ð°Ð¿Ð¸ÑÑŒ %{link_to_requirements} Ð´Ð»Ñ ÑÐ¾Ð·Ð´Ð°Ð½Ð¸Ñ ÐºÐ»Ð°Ñтеров"
msgid "ClusterIntegration|Manage your Kubernetes cluster by visiting %{provider_link}"
msgstr ""
@@ -4821,7 +4976,7 @@ msgid "ClusterIntegration|Number of nodes"
msgstr "КоличеÑтво узлов"
msgid "ClusterIntegration|Number of nodes must be a numerical value."
-msgstr ""
+msgstr "КоличеÑтво узлов должно быть чиÑлом."
msgid "ClusterIntegration|Please enter access information for your Kubernetes cluster. If you need help, you can read our %{link_to_help_page} on Kubernetes"
msgstr "ПожалуйÑта, укажите параметры доÑтупа к вашему клаÑтеру Kubernetes. ЕÑли вам необходима помощь, вы можете ознакомитьÑÑ Ñ Ð½Ð°ÑˆÐµÐ¹ Ñтраницей %{link_to_help_page} по Kubernetes"
@@ -4830,7 +4985,7 @@ msgid "ClusterIntegration|Please make sure that your Google account meets the fo
msgstr "ПожалуйÑта, убедитеÑÑŒ, что ваш аккаунт Google отвечает Ñледующим требованиÑм:"
msgid "ClusterIntegration|Point a wildcard DNS to this generated endpoint in order to access your application after it has been deployed."
-msgstr ""
+msgstr "Выберите шаблон DNS Ð´Ð»Ñ Ð´Ð°Ð½Ð½Ð¾Ð¹ Ñозданной конечной точки Ñ Ñ†ÐµÐ»ÑŒÑŽ возможноÑти Ð¾Ð±Ñ€Ð°Ñ‰ÐµÐ½Ð¸Ñ Ðº вашему приложению поÑле того, как оно было развернуто."
msgid "ClusterIntegration|Project cluster"
msgstr ""
@@ -4845,7 +5000,7 @@ msgid "ClusterIntegration|Prometheus"
msgstr "Prometheus"
msgid "ClusterIntegration|Prometheus is an open-source monitoring system with %{gitlabIntegrationLink} to monitor deployed applications."
-msgstr ""
+msgstr "Prometheus - Ñто ÑиÑтема мониторинга Ñ Ð¾Ñ‚ÐºÑ€Ñ‹Ñ‚Ñ‹Ð¼ иÑходным кодом (%{gitlabIntegrationLink}) Ð´Ð»Ñ Ð¼Ð¾Ð½Ð¸Ñ‚Ð¾Ñ€Ð¸Ð½Ð³Ð° развернутых приложений."
msgid "ClusterIntegration|Provider details"
msgstr ""
@@ -4854,7 +5009,7 @@ msgid "ClusterIntegration|Provision Role ARN"
msgstr ""
msgid "ClusterIntegration|RBAC-enabled cluster"
-msgstr ""
+msgstr "КлаÑтер Ñ Ð¿Ð¾Ð´Ð´ÐµÑ€Ð¶ÐºÐ¾Ð¹ RBAC"
msgid "ClusterIntegration|Read our %{link_start}help page%{link_end} on Kubernetes cluster integration."
msgstr ""
@@ -4863,7 +5018,7 @@ msgid "ClusterIntegration|Real-time web application monitoring, logging and acce
msgstr ""
msgid "ClusterIntegration|Region"
-msgstr ""
+msgstr "Регион"
msgid "ClusterIntegration|Remove Kubernetes cluster integration"
msgstr "Удалить интеграцию клаÑтера Kubernetes"
@@ -4872,13 +5027,13 @@ msgid "ClusterIntegration|Remove integration"
msgstr "Удалить интеграцию"
msgid "ClusterIntegration|Remove integration and resources"
-msgstr ""
+msgstr "Удалить интеграцию и реÑурÑÑ‹"
msgid "ClusterIntegration|Remove integration and resources?"
-msgstr ""
+msgstr "Удалить интеграцию и реÑурÑÑ‹?"
msgid "ClusterIntegration|Remove integration?"
-msgstr ""
+msgstr "Удалить интеграцию?"
msgid "ClusterIntegration|Remove this Kubernetes cluster's configuration from this project. This will not delete your actual Kubernetes cluster."
msgstr "Удалить Ñту конфигурацию клаÑтера Kubernetes из Ñтого проекта. Это не приведет к удалению вашего фактичеÑкого клаÑтера Kubernetes."
@@ -4887,7 +5042,7 @@ msgid "ClusterIntegration|Removes cluster from project but keeps associated reso
msgstr ""
msgid "ClusterIntegration|Replace this with your own hostname if you want. If you do so, point hostname to Ingress IP Address from above."
-msgstr ""
+msgstr "Замените Ñто Ñвоим ÑобÑтвенным именем хоÑта, еÑли хотите. При замене, укажите Ð¸Ð¼Ñ Ñ…Ð¾Ñта в Ingress IP Address выше."
msgid "ClusterIntegration|Request to begin installing failed"
msgstr "Ðе удалоÑÑŒ выполнить Ð·Ð°Ð¿Ñ€Ð¾Ñ Ð½Ð° запуÑк процеÑÑа уÑтановки"
@@ -4895,65 +5050,74 @@ msgstr "Ðе удалоÑÑŒ выполнить Ð·Ð°Ð¿Ñ€Ð¾Ñ Ð½Ð° запуÑк п
msgid "ClusterIntegration|Request to begin uninstalling failed"
msgstr "Ð—Ð°Ð¿Ñ€Ð¾Ñ Ð½Ð° начало ÑƒÐ´Ð°Ð»ÐµÐ½Ð¸Ñ Ð·Ð°Ð²ÐµÑ€ÑˆÑ‘Ð½ Ñ Ð¾ÑˆÐ¸Ð±ÐºÐ¾Ð¹"
+msgid "ClusterIntegration|SIEM Hostname"
+msgstr ""
+
+msgid "ClusterIntegration|SIEM Port"
+msgstr ""
+
+msgid "ClusterIntegration|SIEM Protocol"
+msgstr ""
+
msgid "ClusterIntegration|Save changes"
msgstr "Сохранить изменениÑ"
msgid "ClusterIntegration|Search IAM Roles"
-msgstr ""
+msgstr "ПоиÑк ролей IAM"
msgid "ClusterIntegration|Search Key Pairs"
-msgstr ""
+msgstr "ПоиÑк пар ключей"
msgid "ClusterIntegration|Search VPCs"
-msgstr ""
+msgstr "ПоиÑк VPC"
msgid "ClusterIntegration|Search domains"
-msgstr ""
+msgstr "ПоиÑк доменов"
msgid "ClusterIntegration|Search instance types"
-msgstr ""
+msgstr "ИÑкать типы ÑкземплÑров"
msgid "ClusterIntegration|Search machine types"
msgstr "ПоиÑк типов машин клаÑтера"
msgid "ClusterIntegration|Search networks"
-msgstr ""
+msgstr "ПоиÑк Ñетей"
msgid "ClusterIntegration|Search projects"
msgstr "ПоиÑк проектов"
msgid "ClusterIntegration|Search regions"
-msgstr ""
+msgstr "ПоиÑк регионов"
msgid "ClusterIntegration|Search security groups"
-msgstr ""
+msgstr "ПоиÑк групп безопаÑноÑти"
msgid "ClusterIntegration|Search subnets"
-msgstr ""
+msgstr "ИÑкать подÑети"
msgid "ClusterIntegration|Search subnetworks"
-msgstr ""
+msgstr "ИÑкать подÑети"
msgid "ClusterIntegration|Search zones"
msgstr "ПоиÑк зон"
msgid "ClusterIntegration|Security group"
-msgstr ""
+msgstr "Группа безопаÑноÑи"
msgid "ClusterIntegration|See and edit the details for your Kubernetes cluster"
msgstr "ПроÑмотр и редактирование информации о вашем клаÑтере Kubernetes"
msgid "ClusterIntegration|Select a VPC to choose a security group"
-msgstr ""
+msgstr "Ð”Ð»Ñ Ð²Ñ‹Ð±Ð¾Ñ€Ð° группы безопаÑноÑти необходимо выбрать VPC"
msgid "ClusterIntegration|Select a VPC to choose a subnet"
-msgstr ""
+msgstr "Ð”Ð»Ñ Ð²Ñ‹Ð±Ð¾Ñ€Ð° подÑети необходимо выбрать VPC"
msgid "ClusterIntegration|Select a VPC to use for your EKS Cluster resources. To use a new VPC, first create one on %{startLink}Amazon Web Services %{externalLinkIcon} %{endLink}."
msgstr ""
msgid "ClusterIntegration|Select a network to choose a subnetwork"
-msgstr ""
+msgstr "Ð”Ð»Ñ Ð²Ñ‹Ð±Ð¾Ñ€Ð° подÑети нужно выбрать Ñеть"
msgid "ClusterIntegration|Select a region to choose a Key Pair"
msgstr ""
@@ -4991,14 +5155,20 @@ msgstr "Выберете зону"
msgid "ClusterIntegration|Select zone to choose machine type"
msgstr "Выберите зону, чтобы выбрать тип машины"
-msgid "ClusterIntegration|Service Token"
+msgid "ClusterIntegration|Send Cilium Logs"
msgstr ""
-msgid "ClusterIntegration|Service role"
+msgid "ClusterIntegration|Send ModSecurity Logs"
msgstr ""
+msgid "ClusterIntegration|Service Token"
+msgstr "Токен Ñлужбы"
+
+msgid "ClusterIntegration|Service role"
+msgstr "Роль Ñлужбы"
+
msgid "ClusterIntegration|Service token is required."
-msgstr ""
+msgstr "ТребуетÑÑ Ñ‚Ð¾ÐºÐµÐ½ Ñлужбы."
msgid "ClusterIntegration|Set a prefix for your namespaces. If not set, defaults to your project path. If modified, existing environments will use their current namespaces until the cluster cache is cleared."
msgstr ""
@@ -5013,7 +5183,7 @@ msgid "ClusterIntegration|Something went wrong on our end."
msgstr " У Ð½Ð°Ñ Ñ‡Ñ‚Ð¾-то пошло не так."
msgid "ClusterIntegration|Something went wrong while creating your Kubernetes cluster"
-msgstr ""
+msgstr "Что-то пошло не так при Ñоздании вашего клаÑтера Kubernetes"
msgid "ClusterIntegration|Something went wrong while installing %{title}"
msgstr "Произошли ошибки во Ð²Ñ€ÐµÐ¼Ñ ÑƒÑтановки %{title}"
@@ -5025,13 +5195,13 @@ msgid "ClusterIntegration|Something went wrong while uninstalling %{title}"
msgstr "Что-то пошло не так при удалении %{title}"
msgid "ClusterIntegration|Something went wrong while updating Knative domain name."
-msgstr ""
+msgstr "Что-то пошло не так при обновлении доменного имени Knative."
msgid "ClusterIntegration|Specifying a domain will allow you to use Auto Review Apps and Auto Deploy stages for %{auto_devops_start}Auto DevOps%{auto_devops_end}. The domain should have a wildcard DNS configured matching the domain."
-msgstr ""
+msgstr "Определение домена позволит вам иÑпользовать Ð¿Ñ€Ð¸Ð»Ð¾Ð¶ÐµÐ½Ð¸Ñ Ð´Ð»Ñ Ð°Ð²Ñ‚Ð¾Ð¾Ð±Ð·Ð¾Ñ€Ð° и Ñтапы автоматичеÑкого Ñ€Ð°Ð·Ð²Ð¾Ñ€Ð°Ñ‡Ð¸Ð²Ð°Ð½Ð¸Ñ Ñ %{auto_devops_start}Auto DevOps%{auto_devops_end}. Этот домен должен иметь наÑтроенный ÑоответÑтвующим Ð´Ð»Ñ Ð´Ð¾Ð¼ÐµÐ½Ð° образом шаблон DNS."
msgid "ClusterIntegration|Subnets"
-msgstr ""
+msgstr "ПодÑети"
msgid "ClusterIntegration|The Amazon Resource Name (ARN) associated with your role. If you do not have a provision role, first create one on %{startAwsLink}Amazon Web Services %{externalLinkIcon}%{endLink} using the above account and external IDs. %{startMoreInfoLink}More information%{endLink}"
msgstr ""
@@ -5046,7 +5216,7 @@ msgid "ClusterIntegration|The associated IP and all deployed services will be de
msgstr ""
msgid "ClusterIntegration|The associated Tiller pod, the %{gitlabManagedAppsNamespace} namespace, and all of its resources will be deleted and cannot be restored."
-msgstr ""
+msgstr "СвÑзанный под Tiller, проÑтранÑтво имён %{gitlabManagedAppsNamespace} и вÑе его реÑурÑÑ‹ будут удалены без возможноÑти воÑÑтановлениÑ."
msgid "ClusterIntegration|The associated load balancer and IP will be deleted and cannot be restored."
msgstr ""
@@ -5055,25 +5225,25 @@ msgid "ClusterIntegration|The associated private key will be deleted and cannot
msgstr ""
msgid "ClusterIntegration|The elastic stack collects logs from all pods in your cluster"
-msgstr ""
+msgstr "Elastic Stack Ñобирает журналы Ñо вÑех подов вашего клаÑтера"
msgid "ClusterIntegration|The endpoint is in the process of being assigned. Please check your Kubernetes cluster or Quotas on Google Kubernetes Engine if it takes a long time."
-msgstr ""
+msgstr "Конечный Ñтап заключаетÑÑ Ð² процеÑÑе назначениÑ. ПожалуйÑта, проверьте клаÑтер Kubernetes или квоты в Google Kubernetes Engine, еÑли Ñтот Ñтап длитÑÑ Ñлишком долго."
msgid "ClusterIntegration|The namespace associated with your project. This will be used for deploy boards, logs, and Web terminals."
msgstr ""
msgid "ClusterIntegration|There was a problem authenticating with your cluster. Please ensure your CA Certificate and Token are valid."
-msgstr ""
+msgstr "Произошла ошибка при аутентификации Ñ Ð²Ð°ÑˆÐ¸Ð¼ клаÑтером. УбедитеÑÑŒ, что Ñертификат CA и токен дейÑтвительны."
msgid "ClusterIntegration|This account must have permissions to create a Kubernetes cluster in the %{link_to_container_project} specified below"
msgstr " У Ñтой учетной запиÑи должны быть Ñ€Ð°Ð·Ñ€ÐµÑˆÐµÐ½Ð¸Ñ Ð½Ð° Ñоздание клаÑтера Kubernetes в %{link_to_container_project} указанных ниже"
msgid "ClusterIntegration|This option will allow you to install applications on RBAC clusters."
-msgstr ""
+msgstr "Эта Ð¾Ð¿Ñ†Ð¸Ñ Ð¿Ð¾Ð·Ð²Ð¾Ð»Ð¸Ñ‚ вам уÑтанавливать Ð¿Ñ€Ð¸Ð»Ð¾Ð¶ÐµÐ½Ð¸Ñ Ð½Ð° клаÑтеры RBAC."
msgid "ClusterIntegration|To access your application after deployment, point a wildcard DNS to the Knative Endpoint."
-msgstr ""
+msgstr "Ð”Ð»Ñ Ð´Ð¾Ñтупа к вашему приложению поÑле Ñ€Ð°Ð·Ð²Ð¾Ñ€Ð°Ñ‡Ð¸Ð²Ð°Ð½Ð¸Ñ Ð²Ñ‹Ð±ÐµÑ€Ð¸Ñ‚Ðµ шаблон DNS Ð´Ð»Ñ Ð¿Ð¾Ð´ÐºÐ»ÑŽÑ‡ÐµÐ½Ð¸Ñ Ðº узлу Knative."
msgid "ClusterIntegration|To remove your integration and resources, type %{clusterName} to confirm:"
msgstr ""
@@ -5088,7 +5258,7 @@ msgid "ClusterIntegration|Uninstall %{appTitle}"
msgstr "Удалить %{appTitle}"
msgid "ClusterIntegration|Update failed. Please check the logs and try again."
-msgstr ""
+msgstr "Обновление не удалоÑÑŒ. ПожалуйÑта, проверьте журналы и попытайтеÑÑŒ Ñнова."
msgid "ClusterIntegration|Use %{query}"
msgstr ""
@@ -5103,7 +5273,7 @@ msgid "ClusterIntegration|Validating project billing status"
msgstr "Проверка ÑтатуÑа тарификации проекта"
msgid "ClusterIntegration|We could not verify that one of your projects on GCP has billing enabled. Please try again."
-msgstr ""
+msgstr "Мы не Ñмогли подтвердить, что один из ваших проектов на GCP имеет включенный биллинг. ПожалуйÑта, попробуйте ещё раз."
msgid "ClusterIntegration|With a Kubernetes cluster associated to this project, you can use review apps, deploy your applications, run your pipelines, and much more in an easy way."
msgstr "ЕÑли привÑзать клаÑтер Kubernetes к Ñтому проекту, вы Ñ Ð»Ñ‘Ð³ÐºÐ¾Ñтью Ñможете иÑпользовать Ð¿Ñ€Ð¸Ð»Ð¾Ð¶ÐµÐ½Ð¸Ñ Ð´Ð»Ñ Ñ€ÐµÐ²ÑŒÑŽ, развертывать ваши приложениÑ, запуÑкать Ñборочные линии и многое другое."
@@ -5115,10 +5285,10 @@ msgid "ClusterIntegration|You are about to remove your cluster integration."
msgstr ""
msgid "ClusterIntegration|You are about to uninstall %{appTitle} from your cluster."
-msgstr ""
+msgstr "Ð’Ñ‹ ÑобираетеÑÑŒ удалить %{appTitle} из вашего клаÑтера."
msgid "ClusterIntegration|You must first install Helm Tiller before installing the applications below"
-msgstr ""
+msgstr "Перед уÑтановкой приложений ниже вы должны уÑтановить Helm Tiller"
msgid "ClusterIntegration|You must grant access to your organization’s AWS resources in order to create a new EKS cluster. To grant access, create a provision role using the account and external ID below and provide us the ARN."
msgstr ""
@@ -5130,7 +5300,7 @@ msgid "ClusterIntegration|You should select at least two subnets"
msgstr ""
msgid "ClusterIntegration|Your account must have %{link_to_kubernetes_engine}"
-msgstr "Ваша ÑƒÑ‡ÐµÑ‚Ð½Ð°Ñ Ð·Ð°Ð¿Ð¸ÑÑŒ должна иметь %{link_to_kubernetes_engine}"
+msgstr "Ваша ÑƒÑ‡Ñ‘Ñ‚Ð½Ð°Ñ Ð·Ð°Ð¿Ð¸ÑÑŒ должна иметь %{link_to_kubernetes_engine}"
msgid "ClusterIntegration|Your cluster API is unreachable. Please ensure your API URL is correct."
msgstr "API клаÑтера недоÑтупно. ПожалуйÑта, убедитеÑÑŒ, что ваш API URL правильный."
@@ -5181,7 +5351,7 @@ msgid "ClusterIntergation|Select an instance type"
msgstr ""
msgid "ClusterIntergation|Select key pair"
-msgstr ""
+msgstr "Выберите пару ключей"
msgid "ClusterIntergation|Select service role"
msgstr ""
@@ -5241,7 +5411,7 @@ msgid "Collapse approvers"
msgstr ""
msgid "Collapse child epics"
-msgstr ""
+msgstr "Свернуть дочерние цели"
msgid "Collapse sidebar"
msgstr "Свернуть панель"
@@ -5302,7 +5472,7 @@ msgstr[2] "Коммитов"
msgstr[3] "Коммитов"
msgid "Commit %{commit_id}"
-msgstr ""
+msgstr "Коммит %{commit_id}"
msgid "Commit (when editing commit message)"
msgstr ""
@@ -5311,7 +5481,7 @@ msgid "Commit Message"
msgstr "ОпиÑание Коммита"
msgid "Commit deleted"
-msgstr ""
+msgstr "Коммит удален"
msgid "Commit message"
msgstr "ОпиÑание коммита"
@@ -5332,7 +5502,7 @@ msgid "CommitMessage|Add %{file_name}"
msgstr "Добавить %{file_name}"
msgid "CommitWidget|authored"
-msgstr ""
+msgstr "Ñоздал"
msgid "Commits"
msgstr "Коммиты"
@@ -5413,28 +5583,43 @@ msgid "Complete"
msgstr "Завершено"
msgid "Compliance"
-msgstr ""
+msgstr "КомплаенÑ"
msgid "Compliance Dashboard"
-msgstr ""
+msgstr "Панель ÑƒÐ¿Ñ€Ð°Ð²Ð»ÐµÐ½Ð¸Ñ Ñлужбы комплаенÑа"
msgid "Compliance framework (optional)"
+msgstr "Фреймворк комплаенÑа (необÑзательно)"
+
+msgid "ComplianceFramework|GDPR"
msgstr ""
msgid "ComplianceFramework|GDPR - General Data Protection Regulation"
+msgstr "GDPR - общий регламент по защите данных"
+
+msgid "ComplianceFramework|HIPAA"
msgstr ""
msgid "ComplianceFramework|HIPAA - Health Insurance Portability and Accountability Act"
+msgstr "HIPAA - акт о мобильноÑти и подотчётноÑти медицинÑкого ÑтрахованиÑ"
+
+msgid "ComplianceFramework|PCI-DSS"
msgstr ""
msgid "ComplianceFramework|PCI-DSS - Payment Card Industry-Data Security Standard"
+msgstr "PCI-DSS - Ñтандарт безопаÑноÑти данных индуÑтрии платёжных карт"
+
+msgid "ComplianceFramework|SOC 2"
msgstr ""
msgid "ComplianceFramework|SOC 2 - Service Organization Control 2"
+msgstr "SOC 2 - Service Organization Control 2"
+
+msgid "ComplianceFramework|SOX"
msgstr ""
msgid "ComplianceFramework|SOX - Sarbanes-Oxley"
-msgstr ""
+msgstr "SOX - акт Сарбейнза - ОкÑли"
msgid "Confidence: %{confidence}"
msgstr ""
@@ -5461,7 +5646,7 @@ msgid "Configure Prometheus"
msgstr ""
msgid "Configure Security %{wordBreakOpportunity}and Compliance"
-msgstr ""
+msgstr "ÐаÑтройте безопаÑноÑÑ‚ÑŒ %{wordBreakOpportunity}и комплаенÑ"
msgid "Configure Tracing"
msgstr ""
@@ -5527,10 +5712,10 @@ msgid "Connect your external repositories, and CI/CD pipelines will run for new
msgstr ""
msgid "Connected"
-msgstr ""
+msgstr "Подключено"
msgid "Connecting"
-msgstr ""
+msgstr "Подключение"
msgid "Connecting to terminal sync service"
msgstr ""
@@ -5563,13 +5748,13 @@ msgid "Container Registry"
msgstr "РееÑÑ‚Ñ€ Контейнеров"
msgid "Container Registry tag expiration policy"
-msgstr ""
+msgstr "Политика иÑÑ‚ÐµÑ‡ÐµÐ½Ð¸Ñ Ñрока дейÑÑ‚Ð²Ð¸Ñ Ñ‚ÐµÐ³Ð¾Ð² рееÑтра контейнеров"
msgid "Container Scanning"
-msgstr ""
+msgstr "Сканирование контейнеров"
msgid "Container does not exist"
-msgstr ""
+msgstr "Контейнер не ÑущеÑтвует"
msgid "Container registry images"
msgstr "Образы Docker Registry"
@@ -5578,23 +5763,32 @@ msgid "Container registry is not enabled on this GitLab instance. Ask an adminis
msgstr ""
msgid "Container repositories sync capacity"
+msgstr "Объем Ñинхронизации рееÑтра контейнеров"
+
+msgid "ContainerRegistry| Please visit the %{linkStart}administration settings%{linkEnd} to enable this feature."
msgstr ""
msgid "ContainerRegistry|%{imageName} tags"
+msgstr "%{imageName} тегов"
+
+msgid "ContainerRegistry|%{title} was successfully scheduled for deletion"
msgstr ""
msgid "ContainerRegistry|Automatically remove extra images that aren't designed to be kept."
-msgstr ""
+msgstr "ÐвтоматичеÑки удалÑÑ‚ÑŒ лишние образы, не предназначенные Ð´Ð»Ñ Ñ…Ñ€Ð°Ð½ÐµÐ½Ð¸Ñ."
msgid "ContainerRegistry|Build an image"
-msgstr ""
+msgstr "Собрать образ"
msgid "ContainerRegistry|Compressed Size"
-msgstr ""
+msgstr "Сжатый размер"
msgid "ContainerRegistry|Container Registry"
msgstr "РееÑÑ‚Ñ€ Контейнеров"
+msgid "ContainerRegistry|Container Registry tag expiration and retention policy is disabled"
+msgstr ""
+
msgid "ContainerRegistry|Copy build command"
msgstr "Скопировать команду Ñборки"
@@ -5602,10 +5796,7 @@ msgid "ContainerRegistry|Copy login command"
msgstr "Скопировать команду входа"
msgid "ContainerRegistry|Copy push command"
-msgstr ""
-
-msgid "ContainerRegistry|Currently, the Container Registry tag expiration feature is not available for projects created before GitLab version 12.8. For updates and more information, visit Issue %{linkStart}#196124%{linkEnd}"
-msgstr ""
+msgstr "Скопировать команду Ð´Ð»Ñ Ð¾Ñ‚Ð¿Ñ€Ð°Ð²ÐºÐ¸"
msgid "ContainerRegistry|Docker connection error"
msgstr "Ошибка Ð¿Ð¾Ð´ÐºÐ»ÑŽÑ‡ÐµÐ½Ð¸Ñ Docker"
@@ -5613,54 +5804,60 @@ msgstr "Ошибка Ð¿Ð¾Ð´ÐºÐ»ÑŽÑ‡ÐµÐ½Ð¸Ñ Docker"
msgid "ContainerRegistry|Docker tag expiration policy is %{toggleStatus}"
msgstr ""
-msgid "ContainerRegistry|Docker tags with names matching this regex pattern will expire:"
-msgstr ""
-
msgid "ContainerRegistry|Edit Settings"
-msgstr ""
+msgstr "Редактировать наÑтройки"
msgid "ContainerRegistry|Expiration interval:"
-msgstr ""
+msgstr "Интервал иÑтечениÑ:"
msgid "ContainerRegistry|Expiration policy successfully saved."
-msgstr ""
+msgstr "Политика иÑÑ‚ÐµÑ‡ÐµÐ½Ð¸Ñ ÑƒÑпешно Ñохранена."
msgid "ContainerRegistry|Expiration policy:"
-msgstr ""
+msgstr "Политика иÑтечениÑ:"
msgid "ContainerRegistry|Expiration schedule:"
-msgstr ""
+msgstr "РаÑпиÑание иÑтечениÑ:"
msgid "ContainerRegistry|If you are not already logged in, you need to authenticate to the Container Registry by using your GitLab username and password. If you have %{twofaDocLinkStart}Two-Factor Authentication%{twofaDocLinkEnd} enabled, use a %{personalAccessTokensDocLinkStart}Personal Access Token%{personalAccessTokensDocLinkEnd} instead of a password."
msgstr ""
msgid "ContainerRegistry|Image ID"
-msgstr ""
-
-msgid "ContainerRegistry|Image deleted successfully"
-msgstr ""
+msgstr "ID образа"
msgid "ContainerRegistry|Keep and protect the images that matter most."
-msgstr ""
+msgstr "СохранÑÑ‚ÑŒ и защищать образы, которые имеют наибольшее значение."
msgid "ContainerRegistry|Last Updated"
msgstr "ПоÑледнее обновление"
msgid "ContainerRegistry|Login"
-msgstr ""
+msgstr "Вход"
msgid "ContainerRegistry|Missing or insufficient permission, delete button disabled"
msgstr ""
msgid "ContainerRegistry|Number of tags to retain:"
+msgstr "КоличеÑтво тегов Ð´Ð»Ñ ÑохранениÑ:"
+
+msgid "ContainerRegistry|Please contact your administrator."
msgstr ""
msgid "ContainerRegistry|Push an image"
-msgstr ""
+msgstr "Отправить образ"
msgid "ContainerRegistry|Quick Start"
msgstr "БыÑтрый Ñтарт"
+msgid "ContainerRegistry|Regular expressions such as %{codeStart}.*-test%{codeEnd} or %{codeStart}dev-.*%{codeEnd} are supported"
+msgstr ""
+
+msgid "ContainerRegistry|Regular expressions such as %{codeStart}.*-test%{codeEnd} or %{codeStart}dev-.*%{codeEnd} are supported. To select all tags, use %{codeStart}.*%{codeEnd}"
+msgstr ""
+
+msgid "ContainerRegistry|Remember to run %{docLinkStart}garbage collection%{docLinkEnd} to remove the stale data from storage."
+msgstr ""
+
msgid "ContainerRegistry|Remove repository"
msgstr "Удалить репозиторий"
@@ -5675,47 +5872,56 @@ msgstr[2] "Удалить тегов"
msgstr[3] "Удалить тегов"
msgid "ContainerRegistry|Retention policy has been Enabled"
-msgstr ""
+msgstr "Политика ÑÐ¾Ñ…Ñ€Ð°Ð½ÐµÐ½Ð¸Ñ Ð±Ñ‹Ð»Ð° включена"
-msgid "ContainerRegistry|Something went wrong while deleting the image."
-msgstr ""
+msgid "ContainerRegistry|Something went wrong while fetching the expiration policy."
+msgstr "Что-то пошло не так при извлечении политики иÑтечениÑ."
-msgid "ContainerRegistry|Something went wrong while deleting the tag."
+msgid "ContainerRegistry|Something went wrong while fetching the repository list."
msgstr ""
-msgid "ContainerRegistry|Something went wrong while deleting the tags."
-msgstr ""
+msgid "ContainerRegistry|Something went wrong while fetching the tags list."
+msgstr "Что-то пошло не так при извлечении ÑпиÑка тегов."
-msgid "ContainerRegistry|Something went wrong while fetching the expiration policy."
+msgid "ContainerRegistry|Something went wrong while marking the tag for deletion."
msgstr ""
-msgid "ContainerRegistry|Something went wrong while fetching the packages list."
+msgid "ContainerRegistry|Something went wrong while marking the tags for deletion."
msgstr ""
-msgid "ContainerRegistry|Something went wrong while fetching the tags list."
+msgid "ContainerRegistry|Something went wrong while scheduling %{title} for deletion. Please try again."
msgstr ""
msgid "ContainerRegistry|Something went wrong while updating the expiration policy."
-msgstr ""
+msgstr "Что-то пошло не так во Ð²Ñ€ÐµÐ¼Ñ Ð¾Ð±Ð½Ð¾Ð²Ð»ÐµÐ½Ð¸Ñ Ð¿Ð¾Ð»Ð¸Ñ‚Ð¸ÐºÐ¸ иÑтечениÑ."
msgid "ContainerRegistry|Tag"
msgstr "Тег"
-msgid "ContainerRegistry|Tag deleted successfully"
+msgid "ContainerRegistry|Tag expiration policy"
+msgstr "Политика иÑÑ‚ÐµÑ‡ÐµÐ½Ð¸Ñ Ñ‚ÐµÐ³Ð¾Ð²"
+
+msgid "ContainerRegistry|Tag expiration policy is designed to:"
+msgstr "Политика иÑÑ‚ÐµÑ‡ÐµÐ½Ð¸Ñ Ñ‚ÐµÐ³Ð° предназначена длÑ:"
+
+msgid "ContainerRegistry|Tag successfully marked for deletion."
msgstr ""
-msgid "ContainerRegistry|Tag expiration policy"
+msgid "ContainerRegistry|Tags successfully marked for deletion."
msgstr ""
-msgid "ContainerRegistry|Tag expiration policy is designed to:"
+msgid "ContainerRegistry|Tags with names matching this regex pattern will %{italicStart}be preserved:%{italicEnd}"
msgstr ""
-msgid "ContainerRegistry|Tags deleted successfully"
+msgid "ContainerRegistry|Tags with names matching this regex pattern will %{italicStart}expire:%{italicEnd}"
msgstr ""
-msgid "ContainerRegistry|The last tag related to this image was recently removed. This empty image and any associated data will be automatically removed as part of the regular Garbage Collection process. If you have any questions, contact your administrator."
+msgid "ContainerRegistry|The Container Registry tag expiration and retention policies for this project have not been enabled."
msgstr ""
+msgid "ContainerRegistry|The last tag related to this image was recently removed. This empty image and any associated data will be automatically removed as part of the regular Garbage Collection process. If you have any questions, contact your administrator."
+msgstr "ПоÑледний тег, ÑвÑзанный Ñ Ñтим образом недавно был удалён. Этот пуÑтой образ и любые ÑвÑзанные Ñ Ð½Ð¸Ð¼ данные будут автоматичеÑки удалены в ходе регулÑрной Ñборки муÑора. ЕÑли у Ð²Ð°Ñ ÐµÑÑ‚ÑŒ вопроÑÑ‹, ÑвÑжитеÑÑŒ Ñо Ñвоим админиÑтратором."
+
msgid "ContainerRegistry|The retention and expiration policy for this Container Registry has been enabled and will run in %{days}. For more information visit the %{linkStart}documentation%{linkEnd}"
msgstr ""
@@ -5731,18 +5937,18 @@ msgstr "Ð’ Ñтой группе нет доÑтупных образов кон
msgid "ContainerRegistry|There are no container images stored for this project"
msgstr "Ð’ Ñтом проекте нет Docker образов"
-msgid "ContainerRegistry|This Registry contains deleted image tag data. Remember to run %{docLinkStart}garbage collection%{docLinkEnd} to remove the stale data from storage."
+msgid "ContainerRegistry|There was an error during the deletion of this image repository, please try again."
msgstr ""
msgid "ContainerRegistry|This image has no active tags"
msgstr "Этот образ не имеет активных тегов"
+msgid "ContainerRegistry|This image repository is scheduled for deletion"
+msgstr ""
+
msgid "ContainerRegistry|We are having trouble connecting to Docker, which could be due to an issue with your project name or path. %{docLinkStart}More Information%{docLinkEnd}"
msgstr "Мы не можем подключитьÑÑ Ðº Docker, что может быть вызвано проблемой Ñ Ð¸Ð¼ÐµÐ½ÐµÐ¼ или путём вашего проекта. %{docLinkStart}ÐŸÐ¾Ð´Ñ€Ð¾Ð±Ð½Ð°Ñ Ð¸Ð½Ñ„Ð¾Ñ€Ð¼Ð°Ñ†Ð¸Ñ%{docLinkEnd}"
-msgid "ContainerRegistry|Wildcards such as %{codeStart}.*-stable%{codeEnd} or %{codeStart}production/.*%{codeEnd} are supported. To select all tags, use %{codeStart}.*%{codeEnd}"
-msgstr ""
-
msgid "ContainerRegistry|With the Container Registry, every project can have its own space to store its Docker images. %{docLinkStart}More Information%{docLinkEnd}"
msgstr "С рееÑтром контейнеров каждый проект может иметь Ñвое меÑто Ð´Ð»Ñ Ñ…Ñ€Ð°Ð½ÐµÐ½Ð¸Ñ Ñвоих Docker образов. %{docLinkStart}ÐŸÐ¾Ð´Ñ€Ð¾Ð±Ð½Ð°Ñ Ð¸Ð½Ñ„Ð¾Ñ€Ð¼Ð°Ñ†Ð¸Ñ%{docLinkEnd}"
@@ -5753,7 +5959,7 @@ msgid "ContainerRegistry|With the Docker Container Registry integrated into GitL
msgstr "С помощью РееÑтра Docker Контейнеров, интегрированного в GitLab, каждый проект может иметь Ñвое ÑобÑтвенное проÑтранÑтво Ð´Ð»Ñ Ñ…Ñ€Ð°Ð½ÐµÐ½Ð¸Ñ Ñвоих Docker образов. %{docLinkStart}ÐŸÐ¾Ð´Ñ€Ð¾Ð±Ð½Ð°Ñ Ð¸Ð½Ñ„Ð¾Ñ€Ð¼Ð°Ñ†Ð¸Ñ%{docLinkEnd}"
msgid "ContainerRegistry|You are about to remove %{item} tags. Are you sure?"
-msgstr ""
+msgstr "Ð’Ñ‹ ÑобираетеÑÑŒ удалить %{item} тегов. Ð’Ñ‹ уверены?"
msgid "ContainerRegistry|You are about to remove %{item}. Are you sure?"
msgstr ""
@@ -5801,22 +6007,22 @@ msgid "ContributionAnalytics|Contribution analytics for issues, merge requests a
msgstr ""
msgid "ContributionAnalytics|Issues"
-msgstr ""
+msgstr "ОбÑуждениÑ"
msgid "ContributionAnalytics|Last 3 months"
-msgstr ""
+msgstr "ПоÑледние 3 меÑÑца"
msgid "ContributionAnalytics|Last month"
-msgstr ""
+msgstr "ПоÑледний меÑÑц"
msgid "ContributionAnalytics|Last week"
-msgstr ""
+msgstr "ПоÑледнÑÑ Ð½ÐµÐ´ÐµÐ»Ñ"
msgid "ContributionAnalytics|Merge Requests"
-msgstr ""
+msgstr "ЗапроÑÑ‹ на ÑлиÑние"
msgid "ContributionAnalytics|No issues for the selected time period."
-msgstr ""
+msgstr "Ðет задач Ð´Ð»Ñ Ð²Ñ‹Ð±Ñ€Ð°Ð½Ð½Ð¾Ð³Ð¾ периода."
msgid "ContributionAnalytics|No merge requests for the selected time period."
msgstr ""
@@ -5891,7 +6097,7 @@ msgid "Copy KRB5 clone URL"
msgstr "Скопировать URL Ð´Ð»Ñ ÐºÐ»Ð¾Ð½Ð¸Ñ€Ð¾Ð²Ð°Ð½Ð¸Ñ Ñ‡ÐµÑ€ÐµÐ· KRB5"
msgid "Copy SSH clone URL"
-msgstr ""
+msgstr "Скопировать URL Ð´Ð»Ñ ÐºÐ»Ð¾Ð½Ð¸Ñ€Ð¾Ð²Ð°Ð½Ð¸Ñ Ð¿Ð¾ SSH"
msgid "Copy SSH public key"
msgstr "Копировать открытый ключ SSH"
@@ -5963,7 +6169,7 @@ msgid "Could not add admins as members"
msgstr ""
msgid "Could not authorize chat nickname. Try again!"
-msgstr "Ðе удалоÑÑŒ авторизовать никнейм. Попробуйте еще раз!"
+msgstr "Ðе удалоÑÑŒ авторизовать никнейм чата. Попробуйте еще раз!"
msgid "Could not change HEAD: branch '%{branch}' does not exist"
msgstr ""
@@ -5993,9 +6199,9 @@ msgid "Could not delete %{design}. Please try again."
msgstr ""
msgid "Could not delete chat nickname %{chat_name}."
-msgstr "Ðе удалоÑÑŒ удалить ник чата %{chat_name}."
+msgstr "Ðе удалоÑÑŒ удалить никнейм чата %{chat_name}."
-msgid "Could not find design"
+msgid "Could not find design."
msgstr ""
msgid "Could not remove the trigger."
@@ -6010,6 +6216,9 @@ msgstr "Ðе удалоÑÑŒ отозвать токен имитированиÑ
msgid "Could not revoke personal access token %{personal_access_token_name}."
msgstr "Ðе удалоÑÑŒ отозвать токен личного доÑтупа %{personal_access_token_name}."
+msgid "Could not revoke project access token %{project_access_token_name}."
+msgstr ""
+
msgid "Could not save group ID"
msgstr ""
@@ -6020,7 +6229,7 @@ msgid "Could not save prometheus manual configuration"
msgstr "Ðе удалоÑÑŒ Ñохранить ручную конфигурацию prometheus"
msgid "Could not update the LDAP settings"
-msgstr ""
+msgstr "Ðе удалоÑÑŒ обновить наÑтройки LDAP"
msgid "Could not upload your designs as one or more files uploaded are not supported."
msgstr ""
@@ -6067,6 +6276,9 @@ msgstr ""
msgid "Create a new branch"
msgstr "Создать новую ветку"
+msgid "Create a new deploy key for this project"
+msgstr ""
+
msgid "Create a new file as there are no files yet. Afterwards, you'll be able to commit your changes."
msgstr "Создайте новый файл, поÑкольку еще нет файлов. ПоÑле Ñтого вы Ñможете зафикÑировать Ñвои изменениÑ."
@@ -6170,6 +6382,9 @@ msgid "Create project label"
msgstr "Создать метку проекта"
msgid "Create requirement"
+msgstr "Создать требование"
+
+msgid "Create snippet"
msgstr ""
msgid "Create wildcard: %{searchTerm}"
@@ -6182,7 +6397,7 @@ msgid "CreateGroup|You don’t have permission to create a subgroup in this grou
msgstr "У Ð²Ð°Ñ Ð½ÐµÑ‚ прав на Ñоздание подгруппы в Ñтой группе."
msgid "CreateGroup|You don’t have permission to create groups."
-msgstr ""
+msgstr "У Ð²Ð°Ñ Ð½ÐµÑ‚ Ñ€Ð°Ð·Ñ€ÐµÑˆÐµÐ½Ð¸Ñ Ð½Ð° Ñоздание групп."
msgid "CreateTag|Tag"
msgstr "Тег"
@@ -6193,6 +6408,9 @@ msgstr "Ñоздать перÑональный токен доÑтупа"
msgid "Created"
msgstr "Создан"
+msgid "Created %{timestamp}"
+msgstr ""
+
msgid "Created At"
msgstr ""
@@ -6251,7 +6469,7 @@ msgid "CredentialsInventory|Personal Access Tokens"
msgstr ""
msgid "CredentialsInventory|SSH Keys"
-msgstr ""
+msgstr "Ключи SSH"
msgid "Critical vulnerabilities present"
msgstr ""
@@ -6269,7 +6487,7 @@ msgid "Current Branch"
msgstr "Ð¢ÐµÐºÑƒÑ‰Ð°Ñ Ð’ÐµÑ‚ÐºÐ°"
msgid "Current Plan"
-msgstr ""
+msgstr "Текущий план"
msgid "Current Project"
msgstr "Текущий проект"
@@ -6287,7 +6505,7 @@ msgid "Current vulnerabilities count"
msgstr ""
msgid "CurrentUser|Buy CI minutes"
-msgstr ""
+msgstr "ПриобреÑти минуты Ð´Ð»Ñ CI"
msgid "CurrentUser|Profile"
msgstr "Профиль"
@@ -6296,7 +6514,7 @@ msgid "CurrentUser|Settings"
msgstr "ÐаÑтройки"
msgid "CurrentUser|Start a Gold trial"
-msgstr ""
+msgstr "Ðачать пробный период Gold"
msgid "Custom CI configuration path"
msgstr ""
@@ -6305,7 +6523,7 @@ msgid "Custom Git clone URL for HTTP(S)"
msgstr ""
msgid "Custom hostname (for private commit emails)"
-msgstr ""
+msgstr "ПользовательÑкое Ð¸Ð¼Ñ Ñ…Ð¾Ñта (Ð´Ð»Ñ Ð¸ÑÐ¿Ð¾Ð»ÑŒÐ·Ð¾Ð²Ð°Ð½Ð¸Ñ Ð² приватных Ñлектронных адреÑах почты при коммите)"
msgid "Custom notification events"
msgstr "Ð¡Ð¾Ð±Ñ‹Ñ‚Ð¸Ñ Ð½Ð°Ñтраиваемых уведомлений"
@@ -6422,7 +6640,7 @@ msgid "CycleAnalyticsEvent|Issue last edited"
msgstr ""
msgid "CycleAnalyticsEvent|Merge request closed"
-msgstr ""
+msgstr "Ð—Ð°Ð¿Ñ€Ð¾Ñ Ð½Ð° ÑлиÑние закрыт"
msgid "CycleAnalyticsEvent|Merge request created"
msgstr "Создан Ð·Ð°Ð¿Ñ€Ð¾Ñ Ð½Ð° ÑлиÑние"
@@ -6476,7 +6694,7 @@ msgid "CycleAnalyticsStage|should be under a group"
msgstr ""
msgid "CycleAnalytics|%{selectedLabelsCount} selected (%{maxLabels} max)"
-msgstr ""
+msgstr "%{selectedLabelsCount} выбрано (макÑимум — %{maxLabels})"
msgid "CycleAnalytics|%{stageCount} stages selected"
msgstr ""
@@ -6551,6 +6769,9 @@ msgstr "Выпадающий фильтр проекта"
msgid "CycleAnalytics|stage dropdown"
msgstr "Выпадающий ÑпиÑок Ñтадий"
+msgid "DAG"
+msgstr ""
+
msgid "DNS"
msgstr "DNS"
@@ -6570,10 +6791,10 @@ msgid "Dashboards"
msgstr "Панели инÑтрументов"
msgid "Dashboard|%{firstProject} and %{secondProject}"
-msgstr ""
+msgstr "%{firstProject} и %{secondProject}"
msgid "Dashboard|%{firstProject}, %{rest}, and %{secondProject}"
-msgstr ""
+msgstr "%{firstProject}, %{rest}, и %{secondProject}"
msgid "Dashboard|Unable to add %{invalidProjects}. This dashboard is available for public projects, and private projects in groups with a Silver plan."
msgstr ""
@@ -6681,7 +6902,7 @@ msgid "Define a custom pattern with cron syntax"
msgstr "Определить наÑтраиваемый шаблон Ñ ÑинтакÑиÑом cron"
msgid "Define environments in the deploy stage(s) in <code>.gitlab-ci.yml</code> to track deployments here."
-msgstr ""
+msgstr "Обозначьте Ð¾ÐºÑ€ÑƒÐ¶ÐµÐ½Ð¸Ñ Ð½Ð° Ñтапе(ах) Ñ€Ð°Ð·Ð²Ñ‘Ñ€Ñ‚Ñ‹Ð²Ð°Ð½Ð¸Ñ Ð² <code>.gitlab-ci.yml</code> Ð´Ð»Ñ Ð¾Ñ‚ÑÐ»ÐµÐ¶Ð¸Ð²Ð°Ð½Ð¸Ñ Ñ€Ð°Ð·Ð²Ñ‘Ñ€Ñ‚Ñ‹Ð²Ð°Ð½Ð¸Ñ Ð·Ð´ÐµÑÑŒ."
msgid "DelayedJobs|Are you sure you want to run %{jobName} immediately? Otherwise this job will run automatically after it's timer finishes."
msgstr "Ð’Ñ‹ уверены, что вы хотите запуÑтить %{jobName} Ñразу? Ð’ противном Ñлучае Ñто задание будет выполнено автоматичеÑки по завершению таймера."
@@ -6774,7 +6995,7 @@ msgid "Deleted"
msgstr "Удалено"
msgid "Deleted chat nickname: %{chat_name}!"
-msgstr ""
+msgstr "Удалённый никнейм чата: %{chat_name}!"
msgid "Deleted in this version"
msgstr "Удалено в Ñтой верÑии"
@@ -6828,7 +7049,7 @@ msgstr[2] ""
msgstr[3] ""
msgid "Dependencies|%{remainingLicensesCount} more"
-msgstr ""
+msgstr "ещё %{remainingLicensesCount}"
msgid "Dependencies|All"
msgstr "Ð’Ñе"
@@ -6855,7 +7076,7 @@ msgid "Dependencies|Location"
msgstr ""
msgid "Dependencies|Packager"
-msgstr ""
+msgstr "Упаковщик"
msgid "Dependencies|Safe"
msgstr "БезопаÑно"
@@ -6870,7 +7091,7 @@ msgid "Dependencies|Toggle vulnerability list"
msgstr ""
msgid "Dependencies|Unsupported file(s) detected"
-msgstr ""
+msgstr "Обнаружен(ы) неподдерживаемый файл(ы)"
msgid "Dependencies|Version"
msgstr ""
@@ -6885,22 +7106,22 @@ msgid "Dependency List has no entries"
msgstr ""
msgid "Dependency Proxy"
-msgstr ""
+msgstr "ПрокÑи завиÑимоÑтей"
msgid "Dependency Scanning"
-msgstr ""
+msgstr "Сканирование ЗавиÑимоÑтей"
msgid "Dependency proxy"
-msgstr ""
+msgstr "ПрокÑи завиÑимоÑтей"
msgid "Dependency proxy URL"
-msgstr ""
+msgstr "URL прокÑи-Ñервера завиÑимоÑтей"
msgid "Dependency proxy feature is limited to public groups for now."
-msgstr ""
+msgstr "Ð¤ÑƒÐ½ÐºÑ†Ð¸Ñ Ð¿Ñ€Ð¾ÐºÑи-завиÑимоÑтей на данный момент ограничиваетÑÑ Ð¿ÑƒÐ±Ð»Ð¸Ñ‡Ð½Ñ‹Ð¼Ð¸ группами."
msgid "DependencyProxy|Toggle Dependency Proxy"
-msgstr ""
+msgstr "Переключить прокÑи-завиÑимоÑÑ‚ÑŒ"
msgid "Depends on %d merge request being merged"
msgid_plural "Depends on %d merge requests being merged"
@@ -6929,9 +7150,12 @@ msgstr "Ключи РазвертываниÑ"
msgid "Deploy key was successfully updated."
msgstr "Ключ Ñ€Ð°Ð·Ð²ÐµÑ€Ñ‚Ñ‹Ð²Ð°Ð½Ð¸Ñ ÑƒÑпешно обновлен."
-msgid "Deploy progress not found. To see pods, ensure your environment matches %{linkStart}deploy board criteria%{linkEnd}."
+msgid "Deploy keys allow read-only or read-write (if enabled) access to your repository. Deploy keys can be used for CI, staging or production servers. You can create a deploy key or add an existing one."
msgstr ""
+msgid "Deploy progress not found. To see pods, ensure your environment matches %{linkStart}deploy board criteria%{linkEnd}."
+msgstr "ПрогреÑÑ Ñ€Ð°Ð·Ð²ÐµÑ€Ñ‚Ñ‹Ð²Ð°Ð½Ð¸Ñ Ð½Ðµ найден. Чтобы увидеть поды, убедитеÑÑŒ, что ваша Ñреда ÑоответÑтвует %{linkStart}критериÑм уÑтановки таблицы%{linkEnd}."
+
msgid "Deploy to..."
msgstr "Развернуть в..."
@@ -7002,10 +7226,10 @@ msgid "DeployTokens|Allows write access to the registry images"
msgstr ""
msgid "DeployTokens|Copy deploy token"
-msgstr ""
+msgstr "Копировать токен развёртываниÑ"
msgid "DeployTokens|Copy username"
-msgstr ""
+msgstr "Копировать Ð¸Ð¼Ñ Ð¿Ð¾Ð»ÑŒÐ·Ð¾Ð²Ð°Ñ‚ÐµÐ»Ñ"
msgid "DeployTokens|Create deploy token"
msgstr "Создать токен развертываниÑ"
@@ -7082,26 +7306,29 @@ msgstr ""
msgid "Deploying to"
msgstr ""
-msgid "Deployment|API"
+msgid "Deployment Frequency"
msgstr ""
+msgid "Deployment|API"
+msgstr "API"
+
msgid "Deployment|This deployment was created using the API"
-msgstr ""
+msgstr "Это развёртывание было Ñоздано Ñ Ð¸Ñпользованием API"
msgid "Deployment|canceled"
-msgstr ""
+msgstr "отменено"
msgid "Deployment|created"
-msgstr ""
+msgstr "Ñоздано"
msgid "Deployment|failed"
-msgstr ""
+msgstr "не удалоÑÑŒ"
msgid "Deployment|running"
-msgstr ""
+msgstr "запущено"
msgid "Deployment|success"
-msgstr ""
+msgstr "уÑпешно"
msgid "Deprioritize label"
msgstr "Понизить приоритет метки"
@@ -7113,7 +7340,7 @@ msgid "Describe the goal of the changes and what reviewers should be aware of."
msgstr ""
msgid "Describe the requirement here"
-msgstr ""
+msgstr "Опишите требование здеÑÑŒ"
msgid "Description"
msgstr "ОпиÑание"
@@ -7212,10 +7439,10 @@ msgid "DesignManagement|Upload skipped."
msgstr ""
msgid "DesignManagement|and %{moreCount} more."
-msgstr ""
+msgstr "и ещё %{moreCount}."
msgid "Designs"
-msgstr ""
+msgstr "Дизайны"
msgid "Destroy"
msgstr ""
@@ -7239,7 +7466,7 @@ msgid "Diff limits"
msgstr ""
msgid "Difference between start date and now"
-msgstr ""
+msgstr "Разница между начальной датой и нынешней"
msgid "DiffsCompareBaseBranch|(HEAD)"
msgstr ""
@@ -7254,7 +7481,7 @@ msgid "Diffs|Show unchanged lines"
msgstr ""
msgid "Diffs|Something went wrong while fetching diff lines."
-msgstr ""
+msgstr "Что-то пошло не так при извлечении отличий."
msgid "Direction"
msgstr ""
@@ -7272,7 +7499,7 @@ msgid "Disable group Runners"
msgstr "Выключить групповые обработчиков заданий"
msgid "Disable public access to Pages sites"
-msgstr ""
+msgstr "Отключить общий доÑтуп к Ñайтам Pages"
msgid "Disable shared Runners"
msgstr "Отключить общие Runner'ы"
@@ -7281,7 +7508,7 @@ msgid "Disable two-factor authentication"
msgstr "Отключить двухфакторную аутентификацию"
msgid "Disabled"
-msgstr "Отключено"
+msgstr "отключено"
msgid "Disabled mirrors can only be enabled by instance owners. It is recommended that you delete them."
msgstr "Отключенные зеркала могут быть включены только владельцами ÑкземплÑров. РекомендуетÑÑ ÑƒÐ´Ð°Ð»Ð¸Ñ‚ÑŒ их."
@@ -7296,7 +7523,7 @@ msgid "Discard changes"
msgstr "Отменить изменениÑ"
msgid "Discard changes to %{path}?"
-msgstr ""
+msgstr "Отменить Ð¸Ð·Ð¼ÐµÐ½ÐµÐ½Ð¸Ñ Ð² %{path}?"
msgid "Discard draft"
msgstr "Удалить черновик"
@@ -7332,13 +7559,13 @@ msgid "Discover|Security capabilities, integrated into your development lifecycl
msgstr ""
msgid "Discover|See the other features of the %{linkStart}gold plan%{linkEnd}"
-msgstr ""
+msgstr "Узнайте о других возможноÑÑ‚ÑÑ… %{linkStart}тарифного плана Gold%{linkEnd}"
msgid "Discover|Start a free trial"
-msgstr ""
+msgstr "Ðачать беÑплатный пробный период"
msgid "Discover|Upgrade now"
-msgstr ""
+msgstr "Улучшить ÑейчаÑ"
msgid "Discuss a specific suggestion or question"
msgstr "ОбÑудить конкретное предложение или вопроÑ"
@@ -7450,7 +7677,7 @@ msgid "Don't have an account yet?"
msgstr ""
msgid "Don't paste the private part of the GPG key. Paste the public part which begins with '-----BEGIN PGP PUBLIC KEY BLOCK-----'."
-msgstr "Ðе вÑтавлÑйте закрытую чаÑÑ‚ÑŒ GPG ключа. Ð’Ñтавьте открытую чаÑÑ‚ÑŒ, ÐºÐ¾Ñ‚Ð¾Ñ€Ð°Ñ Ð½Ð°Ñ‡Ð¸Ð½Ð°ÐµÑ‚ÑÑ Ñ Â«----- BEGIN PGP PUBLIC KEY BLOCK -----»."
+msgstr "Ðе вÑтавлÑйте закрытую чаÑÑ‚ÑŒ ключа GPG. Ð’Ñтавьте открытую чаÑÑ‚ÑŒ, начинающуюÑÑ Ð½Ð° «----- BEGIN PGP PUBLIC KEY BLOCK -----»."
msgid "Don't show again"
msgstr "Ðе показывать Ñнова"
@@ -7492,7 +7719,7 @@ msgid "Download export"
msgstr ""
msgid "Download image"
-msgstr ""
+msgstr "Загрузить изображение"
msgid "Download license"
msgstr "Скачать лицензию"
@@ -7507,7 +7734,7 @@ msgid "DownloadCommit|Email Patches"
msgstr "Email-патчи"
msgid "DownloadCommit|Plain Diff"
-msgstr "ПроÑтой Diff"
+msgstr "ПроÑтое отличие"
msgid "DownloadSource|Download"
msgstr "Скачать"
@@ -7594,7 +7821,7 @@ msgid "Edit comment"
msgstr "Редактировать комментарий"
msgid "Edit dashboard"
-msgstr ""
+msgstr "Редактировать панель управлениÑ"
msgid "Edit description"
msgstr "Изменить опиÑание"
@@ -7602,9 +7829,6 @@ msgstr "Изменить опиÑание"
msgid "Edit environment"
msgstr ""
-msgid "Edit epic description"
-msgstr ""
-
msgid "Edit file"
msgstr "Изменить файл"
@@ -7684,22 +7908,22 @@ msgid "Email restrictions"
msgstr ""
msgid "Email restrictions for sign-ups"
-msgstr ""
+msgstr "ÐžÐ³Ñ€Ð°Ð½Ð¸Ñ‡ÐµÐ½Ð¸Ñ Ð´Ð»Ñ Ñ€ÐµÐ³Ð¸Ñтрации по Ñлектронной почте"
msgid "Email the pipelines status to a list of recipients."
msgstr "Отправить по Ñлектронной почте Ñообщение о ÑтатуÑе Ñборочной ÑпиÑку получателей."
msgid "EmailError|It appears that the email is blank. Make sure your reply is at the top of the email, we can't process inline replies."
-msgstr ""
+msgstr "Похоже, что ÑÐ»ÐµÐºÑ‚Ñ€Ð¾Ð½Ð½Ð°Ñ Ð¿Ð¾Ñ‡Ñ‚Ð° пуÑта. УбедитеÑÑŒ, что ваш ответ находитÑÑ Ð²Ð²ÐµÑ€Ñ…Ñƒ пиÑьма, мы не можем обрабатывать вÑтроенные ответы."
msgid "EmailError|The thread you are replying to no longer exists, perhaps it was deleted? If you believe this is in error, contact a staff member."
msgstr "Тема, на которую вы отвечаете, больше не ÑущеÑтвует, возможно, она была удалена? ЕÑли вы Ñчитаете, что Ñто ошибка, ÑвÑжитеÑÑŒ Ñ ÑƒÑ‡Ð°Ñтником."
msgid "EmailError|We couldn't figure out what the email is for. Please create your issue or comment through the web interface."
-msgstr ""
+msgstr "Мы не Ñмогли понÑÑ‚ÑŒ, Ð´Ð»Ñ Ñ‡ÐµÐ³Ð¾ Ñто пиÑьмо. ПожалуйÑта, Ñоздайте Ñвое обÑуждение или комментарий через веб-интерфейÑ."
msgid "EmailError|We couldn't figure out what the email is in reply to. Please create your comment through the web interface."
-msgstr ""
+msgstr "Мы не Ñмогли выÑÑнить, на что пиÑьмо отвечает. ПожалуйÑта, Ñоздайте Ñвой комментарий через веб-интерфейÑ."
msgid "EmailError|We couldn't figure out what user corresponds to the email. Please create your comment through the web interface."
msgstr ""
@@ -7708,19 +7932,19 @@ msgid "EmailError|We couldn't find the project. Please check if there's any typo
msgstr "Мы не Ñмогли найти проект. ПожалуйÑта, проверьте, еÑÑ‚ÑŒ ли какие-либо опечатки."
msgid "EmailError|You are not allowed to perform this action. If you believe this is in error, contact a staff member."
-msgstr ""
+msgstr "Вам не разрешено выполнÑÑ‚ÑŒ Ñто дейÑтвие. ЕÑли вы Ñчитаете, что Ñто ошибка, обратитеÑÑŒ к Ñотруднику."
msgid "EmailError|Your account has been blocked. If you believe this is in error, contact a staff member."
-msgstr ""
+msgstr "Ваша ÑƒÑ‡ÐµÑ‚Ð½Ð°Ñ Ð·Ð°Ð¿Ð¸ÑÑŒ была заблокирована. ЕÑли вы Ñчитаете, что Ñто ошибка, ÑвÑжитеÑÑŒ Ñ Ñотрудником."
msgid "EmailToken|reset it"
-msgstr ""
+msgstr "ÑброÑить Ñто"
msgid "EmailToken|resetting..."
-msgstr ""
+msgstr "ÑброÑ..."
msgid "Emails"
-msgstr "Email-адреÑа"
+msgstr "ÐдреÑа Ñлектронной почты"
msgid "Emails sent from Service Desk will have this name"
msgstr ""
@@ -7729,7 +7953,7 @@ msgid "Emails separated by comma"
msgstr "Электронные адреÑа разделенные запÑтой"
msgid "EmailsOnPushService|Disable code diffs"
-msgstr ""
+msgstr "Отключить Ñ€Ð°Ð·Ð»Ð¸Ñ‡Ð¸Ñ Ð² коде"
msgid "EmailsOnPushService|Don't include possibly sensitive code diffs in notification body."
msgstr ""
@@ -7741,13 +7965,13 @@ msgid "EmailsOnPushService|Emails on push"
msgstr ""
msgid "EmailsOnPushService|Emails separated by whitespace"
-msgstr ""
+msgstr "ПиÑьма, разделенные пробелами"
msgid "EmailsOnPushService|Send from committer"
-msgstr ""
+msgstr "Отправить от автора коммита"
msgid "EmailsOnPushService|Send notifications from the committer's email address if the domain is part of the domain GitLab is running on (e.g. %{domains})."
-msgstr ""
+msgstr "ОтправлÑÑ‚ÑŒ ÑƒÐ²ÐµÐ´Ð¾Ð¼Ð»ÐµÐ½Ð¸Ñ Ñ Ð°Ð´Ñ€ÐµÑа Ñлектронной почты автора коммита, еÑли домен ÑвлÑетÑÑ Ñ‡Ð°Ñтью домена, на котором работает GitLab (например, %{domains})."
msgid "Embed"
msgstr "Ð’Ñтроить"
@@ -7801,7 +8025,7 @@ msgid "Enable container expiration and retention policies for projects created e
msgstr ""
msgid "Enable email restrictions for sign ups"
-msgstr ""
+msgstr "Включить Ð¾Ð³Ñ€Ð°Ð½Ð¸Ñ‡ÐµÐ½Ð¸Ñ Ð´Ð»Ñ Ñ€ÐµÐ³Ð¸Ñтрации по Ñлектронной почте"
msgid "Enable error tracking"
msgstr ""
@@ -7906,7 +8130,7 @@ msgid "Ensure connectivity is available from the GitLab server to the Prometheus
msgstr ""
msgid "Ensure your %{linkStart}environment is part of the deploy stage%{linkEnd} of your CI pipeline to track deployments to your cluster."
-msgstr ""
+msgstr "УбедитеÑÑŒ, что ваша Ñреда %{linkStart}ÑвлÑетÑÑ Ñ‡Ð°Ñтью Ñтадии развёртываниÑ%{linkEnd} Ñборочной линии CI Ð´Ð»Ñ Ð¾Ñ‚ÑÐ»ÐµÐ¶Ð¸Ð²Ð°Ð½Ð¸Ñ Ñ€Ð°Ð·Ð²Ñ‘Ñ€Ñ‚Ñ‹Ð²Ð°Ð½Ð¸Ñ Ð² ваш клаÑтер."
msgid "Enter 2FA for Admin Mode"
msgstr ""
@@ -7975,13 +8199,13 @@ msgid "Environment"
msgstr "Окружение"
msgid "Environment does not have deployments"
-msgstr ""
+msgstr "Окружение не имеет развёртываний"
msgid "Environment scope"
msgstr ""
msgid "Environment variables are applied to environments via the runner. They can be protected by only exposing them to protected branches or tags. Additionally, they can be masked so they are hidden in job logs, though they must match certain regexp requirements to do so. You can use environment variables for passwords, secret keys, or whatever you want."
-msgstr ""
+msgstr "Переменные Ñреды Ð¾ÐºÑ€ÑƒÐ¶ÐµÐ½Ð¸Ñ Ð¿Ñ€Ð¸Ð¼ÐµÐ½ÑÑŽÑ‚ÑÑ Ðº Ñредам через Runner. Они могут быть защищены только при нахождении в защищенных ветках и тегах. Кроме того, они могут быть замаÑкированы, чтобы Ñкрыть их в журналах заданий, Ñ…Ð¾Ñ‚Ñ Ð´Ð»Ñ Ñтого они должны ÑоответÑтвовать требованиÑм правильного регулÑрного выражениÑ. Ð’Ñ‹ можете иÑпользовать переменные Ñреды Ð¾ÐºÑ€ÑƒÐ¶ÐµÐ½Ð¸Ñ Ð´Ð»Ñ Ð¿Ð°Ñ€Ð¾Ð»ÐµÐ¹, Ñекретных ключей или Ð´Ð»Ñ Ñ‡ÐµÐ³Ð¾ угодного, чего бы вы хотели."
msgid "Environment variables are configured by your administrator to be %{link_start}protected%{link_end} by default"
msgstr "Переменные Ð¾ÐºÑ€ÑƒÐ¶ÐµÐ½Ð¸Ñ Ð½Ð°Ñтроены админиÑтратором, чтобы быть %{link_start}защищенными%{link_end} по умолчанию"
@@ -7990,19 +8214,19 @@ msgid "Environment:"
msgstr "Окружение:"
msgid "EnvironmentDashboard|API"
-msgstr ""
+msgstr "API"
msgid "EnvironmentDashboard|Created through the Deployment API"
-msgstr ""
+msgstr "Создано Ñ Ð¿Ð¾Ð¼Ð¾Ñ‰ÑŒÑŽ API развёртываниÑ"
msgid "EnvironmentDashboard|You are looking at the last updated environment"
-msgstr ""
+msgstr "Ð’Ñ‹ проÑматриваете поÑледнее обновленное окружение"
msgid "Environments"
msgstr "Среды"
msgid "Environments Dashboard"
-msgstr ""
+msgstr "Панель ÑƒÐ¿Ñ€Ð°Ð²Ð»ÐµÐ½Ð¸Ñ Ð¾ÐºÑ€ÑƒÐ¶ÐµÐ½Ð¸Ñми"
msgid "Environments allow you to track deployments of your application %{link_to_read_more}."
msgstr "Окружение позволÑет отÑлеживать развертывание вашего Ð¿Ñ€Ð¸Ð»Ð¾Ð¶ÐµÐ½Ð¸Ñ %{link_to_read_more}."
@@ -8011,28 +8235,28 @@ msgid "Environments in %{name}"
msgstr ""
msgid "EnvironmentsDashboard|Add a project to the dashboard"
-msgstr ""
+msgstr "Добавить проект на панель управлениÑ"
msgid "EnvironmentsDashboard|Add projects"
-msgstr ""
+msgstr "Добавить проекты"
msgid "EnvironmentsDashboard|Environments Dashboard"
-msgstr ""
+msgstr "Панель ÑƒÐ¿Ñ€Ð°Ð²Ð»ÐµÐ½Ð¸Ñ Ð¾ÐºÑ€ÑƒÐ¶ÐµÐ½Ð¸Ñми"
msgid "EnvironmentsDashboard|Job: %{job}"
-msgstr ""
+msgstr "Задание: %{job}"
msgid "EnvironmentsDashboard|More actions"
-msgstr ""
+msgstr "Дополнительные дейÑтвиÑ"
msgid "EnvironmentsDashboard|Read more."
-msgstr ""
+msgstr "Прочитать больше."
msgid "EnvironmentsDashboard|Remove"
-msgstr ""
+msgstr "Удалить"
msgid "EnvironmentsDashboard|The environments dashboard provides a summary of each project's environments' status, including pipeline and alert statuses."
-msgstr ""
+msgstr "Панель ÑƒÐ¿Ñ€Ð°Ð²Ð»ÐµÐ½Ð¸Ñ Ñредами предоÑтавлÑет Ñводку ÑоÑтоÑÐ½Ð¸Ñ ÐºÐ°Ð¶Ð´Ð¾Ð³Ð¾ проекта, Ð²ÐºÐ»ÑŽÑ‡Ð°Ñ ÑоÑтоÑÐ½Ð¸Ñ Ñборочной линии и оповещений."
msgid "EnvironmentsDashboard|This dashboard displays a maximum of 7 projects and 3 environments per project. %{readMoreLink}"
msgstr ""
@@ -8050,7 +8274,7 @@ msgid "Environments|An error occurred while making the request."
msgstr "Произошла ошибка во Ð²Ñ€ÐµÐ¼Ñ Ð²Ñ‹Ð¿Ð¾Ð»Ð½ÐµÐ½Ð¸Ñ Ð·Ð°Ð¿Ñ€Ð¾Ñа."
msgid "Environments|An error occurred while re-deploying the environment, please try again"
-msgstr "Произошла ошибка при повторном развертывании окружениÑ, пожалуйÑта, попробуйте еще раз"
+msgstr "Произошла ошибка при повторном развертывании окружениÑ, пожалуйÑта, попробуйте ещё раз"
msgid "Environments|An error occurred while rolling back the environment, please try again"
msgstr ""
@@ -8110,7 +8334,7 @@ msgid "Environments|Job"
msgstr "Задание"
msgid "Environments|Learn about environments"
-msgstr ""
+msgstr "Узнайте больше об окружениÑÑ…"
msgid "Environments|Learn more about stopping environments"
msgstr ""
@@ -8128,22 +8352,22 @@ msgid "Environments|No deployments yet"
msgstr "Еще нет развертываний"
msgid "Environments|No pod selected"
-msgstr ""
+msgstr "Под не выбран"
msgid "Environments|No pods to display"
-msgstr ""
+msgstr "Ðет подов Ð´Ð»Ñ Ð¾Ñ‚Ð¾Ð±Ñ€Ð°Ð¶ÐµÐ½Ð¸Ñ"
msgid "Environments|Note that this action will stop the environment, but it will %{emphasisStart}not%{emphasisEnd} have an effect on any existing deployment due to no “stop environment action†being defined in the %{ciConfigLinkStart}.gitlab-ci.yml%{ciConfigLinkEnd} file."
-msgstr ""
+msgstr "Обратите внимание, что Ñто дейÑтвие оÑтановит окружение, но %{emphasisStart}не%{emphasisEnd} повлиÑет на уже запущенные фазы развёртываниÑ, в Ñлучае еÑли в файле %{ciConfigLinkStart}.gitlab-ci.yml%{ciConfigLinkEnd} не определено дейÑтвие \"оÑтановка окружениÑ\"."
msgid "Environments|Note that this action will stop the environment, but it will %{emphasis_start}not%{emphasis_end} have an effect on any existing deployment due to no “stop environment action†being defined in the %{ci_config_link_start}.gitlab-ci.yml%{ci_config_link_end} file."
-msgstr ""
+msgstr "Обратите внимание, что Ñто дейÑтвие оÑтановит окружение, но %{emphasis_start}не%{emphasis_end} повлиÑет на любое ÑущеÑтвующее развёртывание из-за того, что в файле %{ci_config_link_start}.gitlab-ci.yml%{ci_config_link_end} не указано \"дейÑтвие в Ñлучае оÑтановки окружениÑ\"."
msgid "Environments|Open live environment"
-msgstr ""
+msgstr "Открыть живое окружение"
msgid "Environments|Pod name"
-msgstr ""
+msgstr "Ðазвание пода"
msgid "Environments|Re-deploy"
msgstr "Повторное развертывание"
@@ -8161,7 +8385,7 @@ msgid "Environments|Read more about environments"
msgstr "Подробнее об окружениÑÑ…"
msgid "Environments|Rollback"
-msgstr ""
+msgstr "Откатить"
msgid "Environments|Rollback environment"
msgstr ""
@@ -8173,10 +8397,10 @@ msgid "Environments|Rollback environment %{name}?"
msgstr "Откатить окружение %{name}?"
msgid "Environments|Select environment"
-msgstr ""
+msgstr "Выберите окружение"
msgid "Environments|Select pod"
-msgstr ""
+msgstr "Выберите под"
msgid "Environments|Show all"
msgstr "Показать вÑе"
@@ -8185,13 +8409,13 @@ msgid "Environments|Stop"
msgstr "ОÑтановить"
msgid "Environments|Stop environment"
-msgstr ""
+msgstr "ОÑтановить окружение"
msgid "Environments|Stopping"
-msgstr ""
+msgstr "ОÑтановка"
msgid "Environments|There was an error fetching the logs. Please try again."
-msgstr ""
+msgstr "Произошла ошибка при извлечении журналов. ПожалуйÑта, попробуйте ещё раз."
msgid "Environments|This action will relaunch the job for commit %{commit_id}, putting the environment in a previous version. Are you sure you want to continue?"
msgstr ""
@@ -8209,10 +8433,10 @@ msgid "Environments|Updated"
msgstr "Обновлено"
msgid "Environments|You don't have any environments right now"
-msgstr ""
+msgstr "У Ð²Ð°Ñ Ð½ÐµÑ‚ ни одного Ð¾ÐºÑ€ÑƒÐ¶ÐµÐ½Ð¸Ñ Ð½Ð° данный момент"
msgid "Environments|protected"
-msgstr ""
+msgstr "защищенное"
msgid "Epic"
msgstr "Цель"
@@ -8221,16 +8445,13 @@ msgid "Epic cannot be found."
msgstr "Цель не может быть найдена."
msgid "Epic events"
-msgstr ""
+msgstr "Ð¡Ð¾Ð±Ñ‹Ñ‚Ð¸Ñ Ñ†ÐµÐ»Ð¸"
msgid "Epics"
msgstr "Цели"
-msgid "Epics (Ultimate / Gold license only)"
-msgstr "Цели (только Ð´Ð»Ñ Ð»Ð¸Ñ†ÐµÐ½Ð·Ð¸Ð¸ Ultimate / Gold)"
-
msgid "Epics Roadmap"
-msgstr ""
+msgstr "Глобальный план развитиÑ"
msgid "Epics and Issues"
msgstr "Цели и обÑуждениÑ"
@@ -8238,17 +8459,20 @@ msgstr "Цели и обÑуждениÑ"
msgid "Epics let you manage your portfolio of projects more efficiently and with less effort"
msgstr "Цели позволÑÑ‚ вам управлÑÑ‚ÑŒ портфелем проектов более Ñффективно и Ñ Ð¼ÐµÐ½ÑŒÑˆÐ¸Ð¼Ð¸ уÑилиÑми"
-msgid "Epics|Add an epic"
+msgid "Epics, Issues, and Merge Requests"
msgstr ""
+msgid "Epics|Add an epic"
+msgstr "Добавить цель"
+
msgid "Epics|Add an existing epic as a child epic."
-msgstr ""
+msgstr "Добавить ÑущеÑтвующую цель как дочернюю."
msgid "Epics|An error occurred while saving the %{epicDateType} date"
msgstr "Произошла ошибка при Ñохранении %{epicDateType} даты"
msgid "Epics|An error occurred while updating labels."
-msgstr ""
+msgstr "Произошла ошибка при обновлении меток."
msgid "Epics|Are you sure you want to remove %{bStart}%{targetIssueTitle}%{bEnd} from %{bStart}%{parentEpicTitle}%{bEnd}?"
msgstr ""
@@ -8257,43 +8481,46 @@ msgid "Epics|Create an epic within this group and add it as a child epic."
msgstr ""
msgid "Epics|Create new epic"
-msgstr ""
+msgstr "Создать новую цель"
msgid "Epics|How can I solve this?"
-msgstr ""
+msgstr "Как Ñ Ð¼Ð¾Ð³Ñƒ Ñто иÑправить?"
msgid "Epics|More information"
msgstr "Ð”Ð¾Ð¿Ð¾Ð»Ð½Ð¸Ñ‚ÐµÐ»ÑŒÐ½Ð°Ñ Ð¸Ð½Ñ„Ð¾Ñ€Ð¼Ð°Ñ†Ð¸Ñ"
msgid "Epics|Remove epic"
-msgstr ""
+msgstr "Удалить цель"
msgid "Epics|Remove issue"
-msgstr ""
+msgstr "Удалить обÑуждение"
msgid "Epics|Show more"
msgstr "Показать еще"
msgid "Epics|Something went wrong while assigning issue to epic."
-msgstr ""
+msgstr "Что-то пошло не так при назначении обÑÑƒÐ¶Ð´ÐµÐ½Ð¸Ñ Ð² цель."
msgid "Epics|Something went wrong while creating child epics."
-msgstr ""
+msgstr "Что-то пошло не так при Ñоздании дочерней цели."
msgid "Epics|Something went wrong while creating issue."
-msgstr ""
+msgstr "Что-то пошло не так при Ñоздании обÑуждениÑ."
msgid "Epics|Something went wrong while fetching child epics."
-msgstr ""
+msgstr "Что-то пошло не так при получении дочерней цели."
msgid "Epics|Something went wrong while fetching group epics."
+msgstr "Что-то пошло не так при получении целей группы."
+
+msgid "Epics|Something went wrong while moving item."
msgstr ""
msgid "Epics|Something went wrong while ordering item."
-msgstr ""
+msgstr "Что-то пошло не так при заказе товара."
msgid "Epics|Something went wrong while removing issue from epic."
-msgstr ""
+msgstr "Что-то пошло не так при удалении обÑÑƒÐ¶Ð´ÐµÐ½Ð¸Ñ Ð¸Ð· цели."
msgid "Epics|These dates affect how your epics appear in the roadmap. Dates from milestones come from the milestones assigned to issues in the epic. You can also set fixed dates or remove them entirely."
msgstr ""
@@ -8308,34 +8535,34 @@ msgid "Epics|due"
msgstr "до"
msgid "Epics|start"
-msgstr ""
+msgstr "начать"
msgid "Error"
msgstr "Ошибка"
msgid "Error Details"
-msgstr ""
+msgstr "ПодробноÑти об ошибке"
msgid "Error Tracking"
-msgstr ""
+msgstr "ОтÑлеживание ошибок"
msgid "Error creating epic"
-msgstr ""
+msgstr "Ошибка ÑÐ¾Ð·Ð´Ð°Ð½Ð¸Ñ Ñ†ÐµÐ»Ð¸"
msgid "Error creating label."
-msgstr ""
+msgstr "Ошибка при Ñоздании метки."
msgid "Error deleting %{issuableType}"
-msgstr ""
+msgstr "Ошибка ÑƒÐ´Ð°Ð»ÐµÐ½Ð¸Ñ %{issuableType}"
msgid "Error deleting project. Check logs for error details."
-msgstr ""
+msgstr "Ошибка при удалении проекта. Проверьте журналы Ð´Ð»Ñ Ð¿Ð¾Ð´Ñ€Ð¾Ð±Ð½Ð¾Ñтей об ошибке."
msgid "Error fetching diverging counts for branches. Please try again."
msgstr ""
msgid "Error fetching forked projects. Please try again."
-msgstr ""
+msgstr "Ошибка при извлечении ответвлений. ПожалуйÑта, попробуйте ещё раз."
msgid "Error fetching labels."
msgstr "Ошибка Ð¿Ð¾Ð»ÑƒÑ‡ÐµÐ½Ð¸Ñ Ð¼ÐµÑ‚Ð¾Ðº."
@@ -8343,6 +8570,9 @@ msgstr "Ошибка Ð¿Ð¾Ð»ÑƒÑ‡ÐµÐ½Ð¸Ñ Ð¼ÐµÑ‚Ð¾Ðº."
msgid "Error fetching network graph."
msgstr "Ошибка Ð¿Ð¾Ð»ÑƒÑ‡ÐµÐ½Ð¸Ñ Ñетевого графа."
+msgid "Error fetching payload data."
+msgstr ""
+
msgid "Error fetching projects"
msgstr "Ошибка при загрузке проектов"
@@ -8352,11 +8582,8 @@ msgstr "Ошибка Ð¿Ð¾Ð»ÑƒÑ‡ÐµÐ½Ð¸Ñ ÑÑылок"
msgid "Error fetching the dependency list. Please check your network connection and try again."
msgstr ""
-msgid "Error fetching usage ping data."
-msgstr "Ошибка при получении данных об иÑпользовании ping."
-
msgid "Error loading branch data. Please try again."
-msgstr "Ошибка загрузки данных ветки. ПожалуйÑта, попробуйте еще раз."
+msgstr "Ошибка загрузки данных ветки. ПожалуйÑта, попробуйте ещё раз."
msgid "Error loading branches."
msgstr "Ошибка при загрузке веток."
@@ -8365,7 +8592,7 @@ msgid "Error loading burndown chart data"
msgstr ""
msgid "Error loading countries data."
-msgstr ""
+msgstr "Ошибка при загрузке данных Ñтран."
msgid "Error loading file viewer."
msgstr "Ошибка загрузки программы проÑмотра файлов."
@@ -8383,10 +8610,10 @@ msgid "Error loading milestone tab"
msgstr "Ошибка загрузки вкладки Ñтапа"
msgid "Error loading project data. Please try again."
-msgstr "Ошибка загрузки данных проекта. ПожалуйÑта, попробуйте еще раз."
+msgstr "Ошибка загрузки данных проекта. ПожалуйÑта, попробуйте ещё раз."
msgid "Error loading template types."
-msgstr ""
+msgstr "Ошибка при загрузке типов шаблонов."
msgid "Error loading template."
msgstr "Ошибка загрузки шаблона."
@@ -8398,16 +8625,16 @@ msgid "Error occurred when fetching sidebar data"
msgstr "Произошла ошибка при получении данных боковой панели"
msgid "Error occurred when saving assignees"
-msgstr ""
+msgstr "Произошла ошибка при Ñохранении назначенных"
msgid "Error occurred when toggling the notification subscription"
msgstr "Произошла ошибка при переключении подпиÑки на оповещение"
msgid "Error occurred while updating the issue status"
-msgstr ""
+msgstr "Произошла ошибка при обновлении ÑтатуÑа обÑуждениÑ"
msgid "Error occurred while updating the issue weight"
-msgstr ""
+msgstr "Произошла ошибка при изменении приоритета задачи"
msgid "Error occurred. A blocked user cannot be deactivated"
msgstr ""
@@ -8488,14 +8715,17 @@ msgid "ErrorTracking|No projects available"
msgstr "Ðет доÑтупных проектов"
msgid "ErrorTracking|Select project"
-msgstr ""
+msgstr "Выбрать проект"
msgid "ErrorTracking|To enable project selection, enter a valid Auth Token"
-msgstr ""
+msgstr "Чтобы включить выбор проекта, введите правильный токен аутентификации"
msgid "Errors"
msgstr "Ошибки"
+msgid "Errors:"
+msgstr ""
+
msgid "Estimated"
msgstr ""
@@ -8509,7 +8739,7 @@ msgid "EventFilterBy|Filter by comments"
msgstr "Фильтр по комментарию"
msgid "EventFilterBy|Filter by epic events"
-msgstr ""
+msgstr "Фильтровать по ÑобытиÑм целей"
msgid "EventFilterBy|Filter by issue events"
msgstr "Фильтр по ÑобытиÑм обÑуждений"
@@ -8575,7 +8805,7 @@ msgid "Everything on your to-do list is marked as done."
msgstr ""
msgid "Everything you need to create a GitLab Pages site using Gatsby."
-msgstr ""
+msgstr "Ð’Ñе, что вам нужно, чтобы Ñоздать Ñайт GitLab Pages Ñ Ð¿Ð¾Ð¼Ð¾Ñ‰ÑŒÑŽ Gatsby."
msgid "Everything you need to create a GitLab Pages site using GitBook."
msgstr "Ð’Ñе необходимое Ð´Ð»Ñ ÑÐ¾Ð·Ð´Ð°Ð½Ð¸Ñ Ñайта GitLab Pages Ñ Ð¿Ð¾Ð¼Ð¾Ñ‰ÑŒÑŽ GitBook."
@@ -8590,7 +8820,7 @@ msgid "Everything you need to create a GitLab Pages site using Jekyll."
msgstr "Ð’Ñе необходимое Ð´Ð»Ñ ÑÐ¾Ð·Ð´Ð°Ð½Ð¸Ñ Ñайта GitLab Pages Ñ Ð¿Ð¾Ð¼Ð¾Ñ‰ÑŒÑŽ Jekyll."
msgid "Everything you need to create a GitLab Pages site using plain HTML."
-msgstr "Ð’Ñе необходимое Ð´Ð»Ñ ÑÐ¾Ð·Ð´Ð°Ð½Ð¸Ñ Ñайта GitLab Pages Ñ Ð¿Ð¾Ð¼Ð¾Ñ‰ÑŒÑŽ HTML."
+msgstr "Ð’ÑÑ‘ необходимое Ð´Ð»Ñ ÑÐ¾Ð·Ð´Ð°Ð½Ð¸Ñ Ñайта GitLab Pages Ñ Ð¿Ð¾Ð¼Ð¾Ñ‰ÑŒÑŽ HTML."
msgid "Evidence collection"
msgstr ""
@@ -8635,7 +8865,7 @@ msgid "Expand approvers"
msgstr "Развернуть утверждающих"
msgid "Expand child epics"
-msgstr ""
+msgstr "Развернуть дочерние цели"
msgid "Expand down"
msgstr ""
@@ -8677,7 +8907,7 @@ msgid "Expires:"
msgstr ""
msgid "Explain the problem. If appropriate, provide a link to the relevant issue or comment."
-msgstr ""
+msgstr "ОбъÑÑните проблему. ЕÑли необходимо, укажите ÑÑылку на ÑоответÑтвующее обÑуждение или комментарий."
msgid "Explore"
msgstr "Обзор"
@@ -8700,12 +8930,18 @@ msgstr "Обзор публичных групп"
msgid "Export as CSV"
msgstr "ЭкÑпортировать в CSV"
+msgid "Export group"
+msgstr ""
+
msgid "Export issues"
msgstr ""
msgid "Export project"
msgstr "ЭкÑпорт проекта"
+msgid "Export this group with all related data to a new GitLab instance. Once complete, you can import the data file from the \"New Group\" page."
+msgstr ""
+
msgid "Export this project with all its related data in order to move your project to a new GitLab instance. Once the export is finished, you can import the file from the \"New Project\" page."
msgstr "ЭкÑпортировать данный проект Ñо вÑеми ÑоответÑтвующими к нему данными, Ð´Ð»Ñ Ñ‚Ð¾Ð³Ð¾ чтобы перенеÑти проект на новую копию GitLab. Как только ÑкÑпорт будет завершён, вы Ñможете импортировать файл через Ñтраницу нового проекта."
@@ -8775,9 +9011,6 @@ msgstr "Ðеудачно"
msgid "Failed Jobs"
msgstr "Ðевыполненные ЗаданиÑ"
-msgid "Failed create wiki"
-msgstr "Ðе удалоÑÑŒ Ñоздать Wiki"
-
msgid "Failed to add a Zoom meeting"
msgstr ""
@@ -8817,8 +9050,11 @@ msgstr ""
msgid "Failed to create resources"
msgstr ""
+msgid "Failed to create wiki"
+msgstr ""
+
msgid "Failed to delete board. Please try again."
-msgstr "Ðе удалоÑÑŒ удалить доÑку. Попробуйте еще раз."
+msgstr "Ðе удалоÑÑŒ удалить доÑку. Попробуйте ещё раз."
msgid "Failed to deploy to"
msgstr ""
@@ -8826,7 +9062,7 @@ msgstr ""
msgid "Failed to enqueue the rebase operation, possibly due to a long-lived transaction. Try again later."
msgstr ""
-msgid "Failed to find import label for jira import."
+msgid "Failed to find import label for Jira import."
msgstr ""
msgid "Failed to get ref."
@@ -8850,6 +9086,12 @@ msgstr ""
msgid "Failed to load groups & users."
msgstr "Ðе удалоÑÑŒ загрузить группы и пользователей."
+msgid "Failed to load labels. Please try again."
+msgstr ""
+
+msgid "Failed to load milestones. Please try again."
+msgstr ""
+
msgid "Failed to load related branches"
msgstr ""
@@ -8977,10 +9219,10 @@ msgid "Feature flag was successfully removed."
msgstr ""
msgid "FeatureFlags|* (All Environments)"
-msgstr ""
+msgstr "* (Ð’Ñе окружениÑ)"
msgid "FeatureFlags|* (All environments)"
-msgstr ""
+msgstr "* (Ð’Ñе окружениÑ)"
msgid "FeatureFlags|API URL"
msgstr ""
@@ -9004,10 +9246,10 @@ msgid "FeatureFlags|Create feature flag"
msgstr "Создать функциональную опцию"
msgid "FeatureFlags|Delete %{name}?"
-msgstr ""
+msgstr "Удалить %{name}?"
msgid "FeatureFlags|Delete feature flag"
-msgstr ""
+msgstr "Удалить функциональную опцию"
msgid "FeatureFlags|Description"
msgstr "ОпиÑание"
@@ -9019,10 +9261,10 @@ msgid "FeatureFlags|Enable features for specific users and specific environments
msgstr ""
msgid "FeatureFlags|Environment Spec"
-msgstr ""
+msgstr "ХарактериÑтика окружениÑ"
msgid "FeatureFlags|Environment Specs"
-msgstr ""
+msgstr "ХарактериÑтики окружений"
msgid "FeatureFlags|Feature Flag"
msgstr ""
@@ -9061,7 +9303,7 @@ msgid "FeatureFlags|Install a %{docs_link_anchored_start}compatible client libra
msgstr "УÑтановите %{docs_link_anchored_start}ÑовмеÑтимую клиентÑкую библиотеку%{docs_link_anchored_end} и укажите URL Ð°Ð´Ñ€ÐµÑ API, наименование Ð¿Ñ€Ð¸Ð»Ð¾Ð¶ÐµÐ½Ð¸Ñ Ð¸ идентификатор ÑкземплÑра Ð¾ÐºÑ€ÑƒÐ¶ÐµÐ½Ð¸Ñ Ð² процеÑÑе наÑтройки конфигурации. %{docs_link_start} Ð”Ð»Ñ Ð±Ð¾Ð»ÑŒÑˆÐµÐ¹ информации%{docs_link_end}"
msgid "FeatureFlags|Instance ID"
-msgstr ""
+msgstr "ID ÑкземплÑра"
msgid "FeatureFlags|Loading feature flags"
msgstr "Загрузка Функциональных опций"
@@ -9088,7 +9330,7 @@ msgid "FeatureFlags|Percent rollout must be a whole number between 0 and 100"
msgstr "Процент Ñ€Ð°Ð·Ð²ÐµÑ€Ñ‚Ñ‹Ð²Ð°Ð½Ð¸Ñ Ð´Ð¾Ð»Ð¶ÐµÐ½ быть целым чиÑлом от 0 до 100"
msgid "FeatureFlags|Protected"
-msgstr ""
+msgstr "Защищено"
msgid "FeatureFlags|Remove"
msgstr "Удалить"
@@ -9225,7 +9467,10 @@ msgstr "Фильтр по комментариÑми к коммитам"
msgid "Filter by milestone name"
msgstr "Фильтр по названию Ñтапа"
-msgid "Filter by name..."
+msgid "Filter by name"
+msgstr ""
+
+msgid "Filter by status"
msgstr ""
msgid "Filter by two-factor authentication"
@@ -9237,12 +9482,18 @@ msgstr ""
msgid "Filter projects"
msgstr ""
+msgid "Filter results"
+msgstr ""
+
msgid "Filter results by group"
msgstr "Фильтровать результаты по группе"
msgid "Filter results by project"
msgstr "Фильтровать результаты по проекту"
+msgid "Filter results..."
+msgstr ""
+
msgid "Filter your projects by name"
msgstr "Отфильтровать проекты по названию"
@@ -9316,7 +9567,7 @@ msgid "FlowdockService|Flowdock Git source token"
msgstr "Токен иÑточника Flowdock Git"
msgid "FlowdockService|Flowdock is a collaboration web app for technical teams."
-msgstr ""
+msgstr "Flowdock - Ñто веб-приложение Ð´Ð»Ñ ÑовмеÑтной работы техничеÑких групп."
msgid "FogBugz Email"
msgstr ""
@@ -9328,7 +9579,7 @@ msgid "FogBugz Password"
msgstr ""
msgid "FogBugz URL"
-msgstr ""
+msgstr "URL FogBugz"
msgid "FogBugz import"
msgstr ""
@@ -9349,7 +9600,7 @@ msgid "For each Jira issue successfully imported, we'll create a new GitLab issu
msgstr ""
msgid "For internal projects, any logged in user can view pipelines and access job details (output logs and artifacts)"
-msgstr ""
+msgstr "Во внутренних проектах любой зарегиÑтрированный пользователь может проÑматривать конвейеры и получать доÑтуп к заданиÑм (логам и артефактам)"
msgid "For more info, read the documentation."
msgstr "Ð”Ð»Ñ Ð¿Ð¾Ð»ÑƒÑ‡ÐµÐ½Ð¸Ñ Ð´Ð¾Ð¿Ð¾Ð»Ð½Ð¸Ñ‚ÐµÐ»ÑŒÐ½Ð¾Ð¹ информации читайте документацию."
@@ -9370,7 +9621,7 @@ msgid "For more information, see the documentation on %{link_start}disabling Sea
msgstr ""
msgid "For private projects, any member (guest or higher) can view pipelines and access job details (output logs and artifacts)"
-msgstr ""
+msgstr "Ð’ чаÑтных проектах любой пользователь (ГоÑÑ‚ÑŒ и выше) может проÑматривать конвейеры и получать доÑтуп к заданиÑм (логам и артефактам)"
msgid "For public projects, anyone can view pipelines and access job details (output logs and artifacts)"
msgstr ""
@@ -9415,7 +9666,7 @@ msgid "Found errors in your %{gitlab_ci_yml}:"
msgstr ""
msgid "Found errors in your .gitlab-ci.yml:"
-msgstr ""
+msgstr "Ðайдены ошибки в вашем .gitlab-ci.yml:"
msgid "Free Trial"
msgstr "БеÑплатный пробный период"
@@ -9469,16 +9720,16 @@ msgid "Full name"
msgstr "Полное имÑ"
msgid "GPG Key ID:"
-msgstr "ID GPG ключа:"
+msgstr "ID ключа GPG:"
msgid "GPG Keys"
-msgstr "GPG Ключи"
+msgstr "Ключи GPG"
msgid "GPG keys allow you to verify signed commits."
-msgstr "GPG ключи позволÑÑŽÑ‚ вам проверить подпиÑанные коммиты."
+msgstr "Ключи GPG позволÑÑŽÑ‚ верифицировать подпиÑанные коммиты."
msgid "GPG signature (loading...)"
-msgstr "GPG подпиÑÑŒ (загрузка...)"
+msgstr "ПодпиÑÑŒ GPG (загрузка...)"
msgid "General"
msgstr "ОÑновныe"
@@ -9531,17 +9782,17 @@ msgstr ""
msgid "GeoNodes|Checksummed"
msgstr ""
-msgid "GeoNodes|Container repositories"
+msgid "GeoNodes|Consult Geo troubleshooting information"
msgstr ""
-msgid "GeoNodes|Data is out of date from %{timeago}"
-msgstr ""
+msgid "GeoNodes|Container repositories"
+msgstr "Репозитории контейнеров"
msgid "GeoNodes|Data replication lag"
msgstr ""
msgid "GeoNodes|Design repositories"
-msgstr ""
+msgstr "Репозитории дизайнов"
msgid "GeoNodes|Does not match the primary storage configuration"
msgstr ""
@@ -9565,10 +9816,10 @@ msgid "GeoNodes|Internal URL"
msgstr "Внутренний URL"
msgid "GeoNodes|Job artifacts"
-msgstr ""
+msgstr "Ðртефакты заданий"
msgid "GeoNodes|LFS objects"
-msgstr ""
+msgstr "Объекты LFS"
msgid "GeoNodes|Last event ID processed by cursor"
msgstr ""
@@ -9576,6 +9827,9 @@ msgstr ""
msgid "GeoNodes|Last event ID seen from primary"
msgstr ""
+msgid "GeoNodes|Learn more about Geo node statuses"
+msgstr ""
+
msgid "GeoNodes|Loading nodes"
msgstr ""
@@ -9591,6 +9845,9 @@ msgstr ""
msgid "GeoNodes|Node was successfully removed."
msgstr ""
+msgid "GeoNodes|Node's status was updated %{timeAgo}."
+msgstr ""
+
msgid "GeoNodes|Not checksummed"
msgstr ""
@@ -9621,7 +9878,10 @@ msgstr ""
msgid "GeoNodes|Repository verification progress"
msgstr ""
-msgid "GeoNodes|Selective"
+msgid "GeoNodes|Selective (%{syncLabel})"
+msgstr ""
+
+msgid "GeoNodes|Selective synchronization"
msgstr ""
msgid "GeoNodes|Something went wrong while changing node status"
@@ -9648,6 +9908,9 @@ msgstr ""
msgid "GeoNodes|Unverified"
msgstr ""
+msgid "GeoNodes|Updated %{timeAgo}"
+msgstr ""
+
msgid "GeoNodes|Used slots"
msgstr ""
@@ -9676,31 +9939,31 @@ msgid "GeoNodes|secondary nodes"
msgstr ""
msgid "Geo|%{name} is scheduled for forced re-download"
-msgstr ""
+msgstr "%{name} запланирован Ð´Ð»Ñ Ð¿Ñ€Ð¸Ð½ÑƒÐ´Ð¸Ñ‚ÐµÐ»ÑŒÐ½Ð¾Ð¹ повторной загрузки"
msgid "Geo|%{name} is scheduled for re-sync"
-msgstr ""
+msgstr "%{name} запланирован Ð´Ð»Ñ Ð¿Ð¾Ð²Ñ‚Ð¾Ñ€Ð½Ð¾Ð¹ Ñинхронизации"
msgid "Geo|%{name} is scheduled for re-verify"
msgstr "%{name} запланирован Ð´Ð»Ñ Ð¿Ð¾Ð²Ñ‚Ð¾Ñ€Ð½Ð¾Ð³Ð¾ подтверждениÑ"
msgid "Geo|All"
+msgstr "Ð’Ñе"
+
+msgid "Geo|All %{replicable_type}"
msgstr ""
msgid "Geo|All projects"
msgstr "Ð’Ñе проекты"
-msgid "Geo|All projects are being scheduled for re-sync"
+msgid "Geo|All projects are being scheduled for resync"
msgstr ""
-msgid "Geo|All projects are being scheduled for re-verify"
-msgstr "Ð’Ñе проекты запланированы Ð´Ð»Ñ Ð¿Ð¾Ð²Ñ‚Ð¾Ñ€Ð½Ð¾Ð³Ð¾ подтверждениÑ"
-
-msgid "Geo|Batch operations"
+msgid "Geo|All projects are being scheduled for reverify"
msgstr ""
msgid "Geo|Could not remove tracking entry for an existing project."
-msgstr ""
+msgstr "Ðе удалоÑÑŒ удалить запиÑÑŒ отÑÐ»ÐµÐ¶Ð¸Ð²Ð°Ð½Ð¸Ñ Ð´Ð»Ñ ÑущеÑтвующего проекта."
msgid "Geo|Could not remove tracking entry for an existing upload."
msgstr "Ðе удалоÑÑŒ удалить запиÑÑŒ отÑÐ»ÐµÐ¶Ð¸Ð²Ð°Ð½Ð¸Ñ Ð´Ð»Ñ ÑущеÑтвующей загрузки."
@@ -9708,7 +9971,13 @@ msgstr "Ðе удалоÑÑŒ удалить запиÑÑŒ отÑлеживаниÑ
msgid "Geo|Failed"
msgstr ""
+msgid "Geo|Filter by status"
+msgstr ""
+
msgid "Geo|Geo Status"
+msgstr "Ð¡Ñ‚Ð°Ñ‚ÑƒÑ Geo"
+
+msgid "Geo|In progress"
msgstr ""
msgid "Geo|In sync"
@@ -9730,16 +9999,13 @@ msgid "Geo|Never"
msgstr ""
msgid "Geo|Next sync scheduled at"
-msgstr ""
+msgstr "Ð¡Ð»ÐµÐ´ÑƒÑŽÑ‰Ð°Ñ ÑÐ¸Ð½Ñ…Ñ€Ð¾Ð½Ð¸Ð·Ð°Ñ†Ð¸Ñ Ð·Ð°Ð¿Ð»Ð°Ð½Ð¸Ñ€Ð¾Ð²Ð°Ð½Ð° на"
msgid "Geo|Not synced yet"
-msgstr ""
-
-msgid "Geo|Pending"
-msgstr ""
+msgstr "Ðе Ñинхронизировано"
msgid "Geo|Pending synchronization"
-msgstr ""
+msgstr "Ожидание Ñинхронизации"
msgid "Geo|Pending verification"
msgstr ""
@@ -9751,7 +10017,7 @@ msgid "Geo|Project"
msgstr "Проект"
msgid "Geo|Project (ID: %{project_id}) no longer exists on the primary. It is safe to remove this entry, as this will not remove any data on disk."
-msgstr ""
+msgstr "Проект (ID: %{project_id}) больше не ÑущеÑтвует на первичном узле. Удалить Ñту запиÑÑŒ безопаÑно, так как Ñто не удалит никакие данные на диÑке."
msgid "Geo|Projects in certain groups"
msgstr ""
@@ -9760,7 +10026,7 @@ msgid "Geo|Projects in certain storage shards"
msgstr ""
msgid "Geo|Redownload"
-msgstr ""
+msgstr "Скачать заново"
msgid "Geo|Remove"
msgstr "Удалить"
@@ -9772,19 +10038,19 @@ msgid "Geo|Remove tracking database entry"
msgstr ""
msgid "Geo|Resync"
-msgstr ""
+msgstr "ПереÑинхронизировать"
-msgid "Geo|Resync all projects"
+msgid "Geo|Resync all"
msgstr ""
msgid "Geo|Retry count"
-msgstr ""
+msgstr "КоличеÑтво повторных попыток"
msgid "Geo|Reverify"
msgstr "Повторить проверку"
-msgid "Geo|Reverify all projects"
-msgstr "Повторить проверку вÑех проектов"
+msgid "Geo|Reverify all"
+msgstr ""
msgid "Geo|Status"
msgstr "СтатуÑ"
@@ -9796,7 +10062,7 @@ msgid "Geo|Synced at"
msgstr "Синхронизировано в"
msgid "Geo|Synchronization failed - %{error}"
-msgstr ""
+msgstr "Ошибка Ñинхронизации - %{error}"
msgid "Geo|The database is currently %{db_lag} behind the primary node."
msgstr "Ð’ наÑтоÑщее Ð²Ñ€ÐµÐ¼Ñ Ð±Ð°Ð·Ð° данных отÑтает от оÑновного узла на %{db_lag}."
@@ -9808,19 +10074,19 @@ msgid "Geo|Tracking database entry will be removed. Are you sure?"
msgstr ""
msgid "Geo|Tracking entry for project (%{project_id}) was successfully removed."
-msgstr ""
+msgstr "ЗапиÑÑŒ отÑÐ»ÐµÐ¶Ð¸Ð²Ð°Ð½Ð¸Ñ Ð´Ð»Ñ Ð¿Ñ€Ð¾ÐµÐºÑ‚Ð° (%{project_id}) была уÑпешно удалена."
msgid "Geo|Tracking entry for upload (%{type}/%{id}) was successfully removed."
msgstr "ЗапиÑÑŒ отÑÐ»ÐµÐ¶Ð¸Ð²Ð°Ð½Ð¸Ñ Ð´Ð»Ñ Ð·Ð°Ð³Ñ€ÑƒÐ·ÐºÐ¸ (%{type}/%{id}) была уÑпешно удалена."
msgid "Geo|Unknown state"
-msgstr ""
+msgstr "ÐеизвеÑтное ÑоÑтоÑние"
msgid "Geo|Verification failed - %{error}"
-msgstr ""
+msgstr "Ошибка Ð¿Ð¾Ð´Ñ‚Ð²ÐµÑ€Ð¶Ð´ÐµÐ½Ð¸Ñ - %{error}"
msgid "Geo|Waiting for scheduler"
-msgstr ""
+msgstr "Ожидание планировщика"
msgid "Geo|You are on a secondary, <b>read-only</b> Geo node. If you want to make changes, you must visit this page on the %{primary_node}."
msgstr ""
@@ -9829,13 +10095,13 @@ msgid "Geo|You are on a secondary, <b>read-only</b> Geo node. You may be able to
msgstr ""
msgid "Geo|misconfigured"
-msgstr ""
+msgstr "наÑтроен неверно"
msgid "Geo|primary"
-msgstr ""
+msgstr "первичный"
msgid "Geo|secondary"
-msgstr ""
+msgstr "вторичный"
msgid "Get a free instance review"
msgstr ""
@@ -9961,7 +10227,7 @@ msgid "GitLabPagesDomains|Retry"
msgstr ""
msgid "GitLabPages|%{domain} is not verified. To learn how to verify ownership, visit your %{link_start}domain details%{link_end}."
-msgstr ""
+msgstr "%{domain} не подтвержден. Чтобы узнать, как подтвердить право ÑобÑтвенноÑти, перейдите на Ñтраницу %{link_start}вашего домена%{link_end}."
msgid "GitLabPages|Access Control is enabled for this Pages website; only authorized users will be able to access it. To make your website publicly available, navigate to your project's %{strong_start}Settings > General > Visibility%{strong_end} and select %{strong_start}Everyone%{strong_end} in pages section. Read the %{link_start}documentation%{link_end} for more information."
msgstr ""
@@ -9988,13 +10254,13 @@ msgid "GitLabPages|Expired"
msgstr ""
msgid "GitLabPages|Force HTTPS (requires valid certificates)"
-msgstr ""
+msgstr "Принудительный HTTPS (требуютÑÑ Ð´ÐµÐ¹Ñтвительные Ñертификаты)"
msgid "GitLabPages|GitLab Pages are disabled for this project. You can enable them on your project's %{strong_start}Settings > General > Visibility%{strong_end} page."
-msgstr ""
+msgstr "GitLab Pages отключены Ð´Ð»Ñ Ñтого проекта. Ð’Ñ‹ можете включить в поле %{strong_start}ÐаÑтройки > Общие > ВидимоÑÑ‚ÑŒ%{strong_end} вашего проекта."
msgid "GitLabPages|It may take up to 30 minutes before the site is available after the first deployment."
-msgstr ""
+msgstr "Может пройти до 30 минут, прежде чем Ñайт Ñтанет доÑтупен поÑле первого развёртываниÑ."
msgid "GitLabPages|Learn how to upload your static site and have it served by GitLab by following the %{link_start}documentation on GitLab Pages%{link_end}."
msgstr ""
@@ -10003,7 +10269,7 @@ msgid "GitLabPages|Learn more."
msgstr ""
msgid "GitLabPages|Maximum size of pages (MB)"
-msgstr ""
+msgstr "МакÑимальный размер Ñтраниц (Мбайт)"
msgid "GitLabPages|New Domain"
msgstr "Ðовый домен"
@@ -10039,7 +10305,7 @@ msgid "GitLabPages|Unverified"
msgstr ""
msgid "GitLabPages|Verified"
-msgstr ""
+msgstr "Проверенные"
msgid "GitLabPages|When using Pages under the general domain of a GitLab instance (%{pages_host}), you cannot use HTTPS with sub-subdomains. This means that if your username/groupname contains a dot it will not work. This is a limitation of the HTTP Over TLS protocol. HTTP pages will continue to work provided you don't redirect HTTP to HTTPS."
msgstr ""
@@ -10048,7 +10314,7 @@ msgid "GitLabPages|With GitLab Pages you can host your static websites on GitLab
msgstr ""
msgid "GitLabPages|Your pages are served under:"
-msgstr ""
+msgstr "Ваши Ñтраницы обÑлуживаютÑÑ Ð¿Ð¾Ð´:"
msgid "Gitaly"
msgstr "Gitaly"
@@ -10081,7 +10347,7 @@ msgid "Global Shortcuts"
msgstr "Глобальные ÑÐ¾Ñ‡ÐµÑ‚Ð°Ð½Ð¸Ñ ÐºÐ»Ð°Ð²Ð¸Ñˆ"
msgid "Global notification settings"
-msgstr ""
+msgstr "Глобальные наÑтройки уведомлений"
msgid "Go Back"
msgstr "ВернутьÑÑ Ð½Ð°Ð·Ð°Ð´"
@@ -10111,7 +10377,7 @@ msgid "Go to Pipelines"
msgstr ""
msgid "Go to Webhooks"
-msgstr ""
+msgstr "Перейти к веб-обработчикам"
msgid "Go to commits"
msgstr "Перейти к коммитам"
@@ -10125,9 +10391,6 @@ msgstr "Перейти в окружениÑ"
msgid "Go to file"
msgstr "Перейти к файлу"
-msgid "Go to file (MRs only)"
-msgstr ""
-
msgid "Go to file permalink (while viewing a file)"
msgstr ""
@@ -10264,7 +10527,7 @@ msgid "Gravatar"
msgstr "Gravatar"
msgid "Gravatar enabled"
-msgstr ""
+msgstr "Gravatar включён"
msgid "Group"
msgstr "Группа"
@@ -10399,13 +10662,13 @@ msgid "GroupRoadmap|No start date – %{dateWord}"
msgstr ""
msgid "GroupRoadmap|Something went wrong while fetching epics"
-msgstr ""
+msgstr "Что-то пошло не так при извлечении целей"
msgid "GroupRoadmap|Something went wrong while fetching milestones"
msgstr ""
msgid "GroupRoadmap|Sorry, no epics matched your search"
-msgstr ""
+msgstr "К Ñожалению, по вашему запроÑу цели не найдены"
msgid "GroupRoadmap|The roadmap shows the progress of your epics along a timeline"
msgstr ""
@@ -10429,7 +10692,7 @@ msgid "GroupSAML|Copy SAML Response XML"
msgstr ""
msgid "GroupSAML|Enable SAML authentication for this group."
-msgstr ""
+msgstr "Включить аутентификацию SAML Ð´Ð»Ñ Ñтой группы."
msgid "GroupSAML|Enforce SSO-only authentication for this group."
msgstr "Принудительно иÑпользовать SSO-аутентификацию Ð´Ð»Ñ Ñтой группы."
@@ -10572,6 +10835,9 @@ msgstr ""
msgid "GroupSettings|Disable group mentions"
msgstr ""
+msgid "GroupSettings|Export group"
+msgstr ""
+
msgid "GroupSettings|If the parent group's visibility is lower than the group current visibility, visibility levels for subgroups and projects will be changed to match the new parent group's visibility."
msgstr "ЕÑли видимоÑÑ‚ÑŒ родительÑкой группы ниже текущей видимоÑти группы, уровни видимоÑти Ð´Ð»Ñ Ð¿Ð¾Ð´Ð³Ñ€ÑƒÐ¿Ð¿ и проектов будут изменены на ÑоответÑтвие видимоÑти новой родительÑкой группы."
@@ -10621,7 +10887,7 @@ msgid "GroupSettings|This setting will be applied to all subgroups unless overri
msgstr "Эта наÑтройка будет применена Ð´Ð»Ñ Ð²Ñех подгрупп еÑли не будет переопределена владельцем группы. Группы которые уже имеют доÑтуп к проекту, будут иметь его и дальше пока не будут удалены вручную."
msgid "GroupSettings|This setting will override user notification preferences for all members of the group, subgroups, and projects."
-msgstr ""
+msgstr "Эта наÑтройка переопределит наÑтройки уведомлений Ð´Ð»Ñ Ð²Ñех пользователей в группе, подгруппах и проектах."
msgid "GroupSettings|This setting will prevent group members from being notified if the group is mentioned."
msgstr ""
@@ -10648,13 +10914,16 @@ msgid "Groups"
msgstr "Группы"
msgid "Groups (%{count})"
+msgstr "Группы (%{count})"
+
+msgid "Groups (%{groups})"
msgstr ""
msgid "Groups can also be nested by creating %{subgroup_docs_link_start}subgroups%{subgroup_docs_link_end}."
msgstr ""
msgid "Groups to synchronize"
-msgstr ""
+msgstr "Группы Ð´Ð»Ñ Ñинхронизации"
msgid "Groups with access to %{strong_start}%{group_name}%{strong_end}"
msgstr ""
@@ -10672,7 +10941,7 @@ msgid "GroupsDropdown|Loading groups"
msgstr ""
msgid "GroupsDropdown|Search your groups"
-msgstr ""
+msgstr "ПоиÑк в ваших группах"
msgid "GroupsDropdown|Something went wrong on our end."
msgstr ""
@@ -10717,13 +10986,13 @@ msgid "GroupsTree|Loading groups"
msgstr "Загрузка групп"
msgid "GroupsTree|No groups matched your search"
-msgstr ""
+msgstr "ПоиÑк групп не дал результатов"
msgid "GroupsTree|No groups or projects matched your search"
-msgstr ""
+msgstr "ПоиÑк групп или проектов не дал результатов"
msgid "GroupsTree|Search by name"
-msgstr ""
+msgstr "ПоиÑк по имени"
msgid "Guideline"
msgstr ""
@@ -10798,7 +11067,7 @@ msgid "Hi %{username}!"
msgstr ""
msgid "Hide archived projects"
-msgstr "Скрыть архивные проекты"
+msgstr "Скрывать архивные проекты"
msgid "Hide chart"
msgid_plural "Hide charts"
@@ -10842,7 +11111,7 @@ msgid "Hiding all labels"
msgstr "Ð’Ñе метки Ñкрыты"
msgid "High or unknown vulnerabilities present"
-msgstr ""
+msgstr "ПриÑутÑтвуют уÑзвимоÑти выÑокого или неизвеÑтного уровнÑ"
msgid "Highest number of requests per minute for each raw path, default to 300. To disable throttling set to 0."
msgstr ""
@@ -10854,7 +11123,7 @@ msgid "History"
msgstr "ИÑториÑ"
msgid "History of authentications"
-msgstr ""
+msgstr "ИÑÑ‚Ð¾Ñ€Ð¸Ñ Ð°ÑƒÑ‚ÐµÐ½Ñ‚Ð¸Ñ„Ð¸ÐºÐ°Ñ†Ð¸Ð¹"
msgid "Hook execution failed. Ensure the group has a project with commits."
msgstr "Ðе удалоÑÑŒ выполнить обработчик. УбедитеÑÑŒ, что группа имеет проект Ñ ÐºÐ¾Ð¼Ð¼Ð¸Ñ‚Ð°Ð¼Ð¸."
@@ -10902,10 +11171,10 @@ msgid "I accept the %{terms_link}"
msgstr ""
msgid "I accept the|Terms of Service and Privacy Policy"
-msgstr ""
+msgstr "Я принимаю|уÑÐ»Ð¾Ð²Ð¸Ñ Ð¸ÑÐ¿Ð¾Ð»ÑŒÐ·Ð¾Ð²Ð°Ð½Ð¸Ñ Ð¸ политику конфиденциальноÑти"
msgid "I forgot my password"
-msgstr ""
+msgstr "Я забыл пароль"
msgid "I have read and agree to the Let's Encrypt %{link_start}Terms of Service%{link_end} (PDF)"
msgstr ""
@@ -10919,14 +11188,11 @@ msgstr "ID"
msgid "ID:"
msgstr "ID:"
-msgid "IDE|Allow live previews of JavaScript projects in the Web IDE using CodeSandbox client side evaluation."
+msgid "IDE|Allow live previews of JavaScript projects in the Web IDE using CodeSandbox Live Preview."
msgstr ""
msgid "IDE|Back"
-msgstr ""
-
-msgid "IDE|Client side evaluation"
-msgstr ""
+msgstr "Ðазад"
msgid "IDE|Commit"
msgstr "Коммит"
@@ -10938,19 +11204,19 @@ msgid "IDE|Edit"
msgstr "Редактировать"
msgid "IDE|Get started with Live Preview"
-msgstr ""
+msgstr "Ðачало работы Ñ Live Preview"
msgid "IDE|Go to project"
-msgstr ""
+msgstr "Перейти к проекту"
msgid "IDE|Live Preview"
-msgstr ""
+msgstr "Предварительный проÑмотр"
msgid "IDE|Preview your web application using Web IDE client-side evaluation."
-msgstr ""
+msgstr "ПредпроÑмотр веб-Ð¿Ñ€Ð¸Ð»Ð¾Ð¶ÐµÐ½Ð¸Ñ Ñ Ð¸Ñпользованием Web IDE на Ñтороне клиента."
msgid "IDE|Refresh preview"
-msgstr ""
+msgstr "Обновить предварительный проÑмотр"
msgid "IDE|Review"
msgstr "РецензиÑ"
@@ -10991,11 +11257,14 @@ msgstr ""
msgid "If any job surpasses this timeout threshold, it will be marked as failed. Human readable time input language is accepted like \"1 hour\". Values without specification represent seconds."
msgstr ""
-msgid "If checked, group owners can manage LDAP group links and LDAP member overrides"
+msgid "If blank, set allowable lifetime to %{instance_level_policy_in_words}, as defined by the instance admin. Once set, existing tokens for users in this group may be revoked."
msgstr ""
+msgid "If checked, group owners can manage LDAP group links and LDAP member overrides"
+msgstr "ЕÑли отмечено, владельцы групп могут управлÑÑ‚ÑŒ ÑÑылками на группы LDAP и переопределÑÑ‚ÑŒ пользователей LDAP"
+
msgid "If checked, new group memberships and permissions can only be added via LDAP synchronization"
-msgstr ""
+msgstr "ЕÑли отмечено, новые членÑтва в группах и Ñ€Ð°Ð·Ñ€ÐµÑˆÐµÐ½Ð¸Ñ Ð¼Ð¾Ð¶Ð½Ð¾ добавлÑÑ‚ÑŒ только через Ñинхронизацию LDAP"
msgid "If disabled, a diverged local branch will not be automatically updated with commits from its remote counterpart, to prevent local data loss. If the default branch (%{default_branch}) has diverged and cannot be updated, mirroring will fail. Other diverged branches are silently ignored."
msgstr ""
@@ -11030,6 +11299,9 @@ msgstr ""
msgid "If you lose your recovery codes you can generate new ones, invalidating all previous codes."
msgstr "ЕÑли вы потерÑете Ñвои коды воÑÑтановлениÑ, вы можете Ñоздать новые, Ð°Ð½Ð½ÑƒÐ»Ð¸Ñ€ÑƒÑ Ð²Ñе предыдущие коды."
+msgid "If you reach 100%% storage capacity, you will not be able to: %{base_message}"
+msgstr ""
+
msgid "If your HTTP repository is not publicly accessible, add your credentials."
msgstr ""
@@ -11043,7 +11315,7 @@ msgid "Ignored"
msgstr ""
msgid "Image: %{image}"
-msgstr ""
+msgstr "Изображение: %{image}"
msgid "ImageDiffViewer|2-up"
msgstr ""
@@ -11141,6 +11413,9 @@ msgstr "Импорт репозиториев из GitHub"
msgid "Import repository"
msgstr "Импорт репозиториÑ"
+msgid "Import started by: %{importInitiator}"
+msgstr ""
+
msgid "Import tasks"
msgstr ""
@@ -11190,7 +11465,7 @@ msgid "Improve Merge Requests and customer support with GitLab Enterprise Editio
msgstr ""
msgid "Improve issues management with Issue weight and GitLab Enterprise Edition."
-msgstr ""
+msgstr "Улучшить управление обÑуждениÑми Ñ Ð²Ð¾Ð·Ð¼Ð¾Ð¶Ð½Ð¾Ñтью Ð¾Ð¿Ñ€ÐµÐ´ÐµÐ»ÐµÐ½Ð¸Ñ Ð¿Ñ€Ð¸Ð¾Ñ€Ð¸Ñ‚ÐµÑ‚Ð° обÑÑƒÐ¶Ð´ÐµÐ½Ð¸Ñ Ð¿Ñ€Ð¸ помощи GitLab Enterprise Edition."
msgid "Improve search with Advanced Global Search and GitLab Enterprise Edition."
msgstr "Улучшить поиÑк при помощи РаÑширенного Глобального ПоиÑка в верÑии GitLab Enterprise Edition."
@@ -11207,6 +11482,9 @@ msgstr ""
msgid "In order to tailor your experience with GitLab we<br>would like to know a bit more about you."
msgstr ""
+msgid "In progress"
+msgstr ""
+
msgid "In the next step, you'll be able to select the projects you want to import."
msgstr ""
@@ -11220,7 +11498,7 @@ msgid "Include a Terms of Service agreement and Privacy Policy that all users mu
msgstr ""
msgid "Include author name in notification email body"
-msgstr ""
+msgstr "Включать Ð¸Ð¼Ñ Ð°Ð²Ñ‚Ð¾Ñ€Ð° в текÑте ÑƒÐ²ÐµÐ´Ð¾Ð¼Ð»ÐµÐ½Ð¸Ñ Ð¿Ð¾ Ñлектронной почте"
msgid "Include merge request description"
msgstr ""
@@ -11356,10 +11634,10 @@ msgid "Internal - The project can be accessed by any logged in user."
msgstr "Внутренний - Проект доÑтупен любому зарегиÑтрированному пользователю."
msgid "Internal URL (optional)"
-msgstr ""
+msgstr "Внутренний URL (необÑзательно)"
msgid "Internal users"
-msgstr ""
+msgstr "Внутренние пользователи"
msgid "Interval Pattern"
msgstr "Шаблон интервала"
@@ -11488,7 +11766,7 @@ msgid "Issue Boards"
msgstr "ДоÑки ОбÑуждений"
msgid "Issue already promoted to epic."
-msgstr ""
+msgstr "ОбÑуждение уже было продвинуто до цели."
msgid "Issue cannot be found."
msgstr ""
@@ -11515,7 +11793,7 @@ msgid "Issue was closed by %{name} %{reason}"
msgstr "ОбÑуждение было закрыто %{name} %{reason}"
msgid "Issue weight"
-msgstr ""
+msgstr "Приоритет обÑуждениÑ"
msgid "IssueBoards|Board"
msgstr "ДоÑка"
@@ -11527,16 +11805,16 @@ msgid "IssueBoards|Create new board"
msgstr "Создать новую доÑку"
msgid "IssueBoards|Delete board"
-msgstr ""
+msgstr "Удалить доÑку"
msgid "IssueBoards|No matching boards found"
-msgstr ""
+msgstr "Ðет подходÑщих доÑок"
msgid "IssueBoards|Some of your boards are hidden, activate a license to see them again."
-msgstr ""
+msgstr "Ðекоторые из ваших доÑок Ñкрыты. Ðктивируйте лицензию, чтобы Ñнова увидеть их."
msgid "IssueBoards|Switch board"
-msgstr ""
+msgstr "Переключить доÑку"
msgid "IssueTracker|Bugzilla issue tracker"
msgstr "СиÑтема ÐºÐ¾Ð½Ñ‚Ñ€Ð¾Ð»Ñ Ð¾Ð±Ñуждений Bugzilla"
@@ -11556,15 +11834,15 @@ msgstr "СиÑтема ÐºÐ¾Ð½Ñ‚Ñ€Ð¾Ð»Ñ Ð¾Ð±Ñуждений YouTrack"
msgid "Issues"
msgstr "ОбÑуждениÑ"
-msgid "Issues / Merge Requests"
-msgstr "ОбÑуждениÑ/ЗапроÑÑ‹ на ÑлиÑние"
-
msgid "Issues Analytics"
msgstr ""
msgid "Issues Rate Limits"
msgstr ""
+msgid "Issues and Merge Requests"
+msgstr ""
+
msgid "Issues can be bugs, tasks or ideas to be discussed. Also, issues are searchable and filterable."
msgstr "ОбÑуждениÑми могут быть ошибки, задачи или идеи. Также, по обÑуждениÑм можно выполнÑÑ‚ÑŒ поиÑк и отбор."
@@ -11586,11 +11864,11 @@ msgstr ""
msgid "IssuesAnalytics|Avg/Month:"
msgstr ""
-msgid "IssuesAnalytics|Issues created"
-msgstr "ОбÑуждений Ñоздано"
+msgid "IssuesAnalytics|Issues opened"
+msgstr ""
-msgid "IssuesAnalytics|Issues created per month"
-msgstr "Создано обÑуждений за меÑÑц"
+msgid "IssuesAnalytics|Issues opened per month"
+msgstr ""
msgid "IssuesAnalytics|Last 12 months"
msgstr "ПоÑледние 12 меÑÑцев"
@@ -11643,6 +11921,9 @@ msgstr ""
msgid "Jira integration not configured."
msgstr ""
+msgid "Jira project: %{importProject}"
+msgstr ""
+
msgid "JiraService|Events for %{noteable_model_name} are disabled."
msgstr "Ð¡Ð¾Ð±Ñ‹Ñ‚Ð¸Ñ Ð´Ð»Ñ %{noteable_model_name} отключены."
@@ -11731,37 +12012,37 @@ msgid "Job|Complete Raw"
msgstr ""
msgid "Job|Download"
-msgstr ""
+msgstr "Скачать"
msgid "Job|Erase job log"
-msgstr ""
+msgstr "ОчиÑтить журнал заданиÑ"
msgid "Job|Job artifacts"
-msgstr ""
+msgstr "Ðртефакты заданиÑ"
msgid "Job|Job has been erased"
-msgstr ""
+msgstr "Задание было удалено"
msgid "Job|Job has been erased by"
-msgstr ""
+msgstr "Задание было удалено пользователем"
msgid "Job|Keep"
-msgstr ""
+msgstr "ОÑтавить"
msgid "Job|Pipeline"
msgstr "Ð¡Ð±Ð¾Ñ€Ð¾Ñ‡Ð½Ð°Ñ Ð»Ð¸Ð½Ð¸Ñ"
msgid "Job|Scroll to bottom"
-msgstr ""
+msgstr "Прокрутить вниз"
msgid "Job|Scroll to top"
-msgstr ""
+msgstr "Прокрутить вверх"
msgid "Job|Show complete raw"
-msgstr ""
+msgstr "Показать полный иÑходный текÑÑ‚"
msgid "Job|The artifacts were removed"
-msgstr ""
+msgstr "Ðртефакты Ð·Ð°Ð´Ð°Ð½Ð¸Ñ Ð±Ñ‹Ð»Ð¸ удалены"
msgid "Job|The artifacts will be removed"
msgstr "Ðртефакты будут удалены"
@@ -11848,7 +12129,7 @@ msgid "Kubernetes cluster was successfully updated."
msgstr "КлаÑтер Kubernetes был уÑпешно обновлён."
msgid "Kubernetes deployment not found"
-msgstr ""
+msgstr "Развёртывание Kubernetes не найдено"
msgid "Kubernetes error: %{error_code}"
msgstr "Ошибка Kubernetes: %{error_code}"
@@ -11860,10 +12141,10 @@ msgid "LDAP"
msgstr "LDAP"
msgid "LDAP settings"
-msgstr ""
+msgstr "ÐаÑтройки LDAP"
msgid "LDAP settings updated"
-msgstr ""
+msgstr "ÐаÑтройки LDAP обновлены"
msgid "LDAP sync in progress. This could take a few minutes. Refresh the page to see the changes."
msgstr "Идет ÑÐ¸Ð½Ñ…Ñ€Ð¾Ð½Ð¸Ð·Ð°Ñ†Ð¸Ñ LDAP. Это может занÑÑ‚ÑŒ неÑколько минут. Обновите Ñтраницу, чтобы увидеть изменениÑ."
@@ -11938,7 +12219,7 @@ msgid "Language"
msgstr "Язык"
msgid "Large File Storage"
-msgstr ""
+msgstr "Хранилище больших файлов"
msgid "Last %d day"
msgid_plural "Last %d days"
@@ -11951,7 +12232,7 @@ msgid "Last %{days} days"
msgstr "ПоÑледние %{days} днÑ(ей)"
msgid "Last Accessed On"
-msgstr ""
+msgstr "ПоÑледнее обращение"
msgid "Last Name is too long (maximum is %{max_length} characters)."
msgstr ""
@@ -11984,7 +12265,7 @@ msgid "Last name"
msgstr "ФамилиÑ"
msgid "Last reply by"
-msgstr ""
+msgstr "ПоÑледний ответ от"
msgid "Last repository check run"
msgstr ""
@@ -12017,7 +12298,7 @@ msgid "Last used on:"
msgstr "ПоÑледнее иÑпользование:"
msgid "LastCommit|authored"
-msgstr ""
+msgstr "Ñоздал"
msgid "LastPushEvent|You pushed to"
msgstr "Вы отправили в"
@@ -12034,11 +12315,14 @@ msgstr ""
msgid "Lead"
msgstr ""
-msgid "Learn GitLab"
+msgid "Lead Time"
msgstr ""
+msgid "Learn GitLab"
+msgstr "Узнайте о GitLab"
+
msgid "Learn More"
-msgstr ""
+msgstr "Узнать больше"
msgid "Learn how to %{link_start}contribute to the built-in templates%{link_end}"
msgstr "Узнайте, как %{link_start}внеÑти Ñвой вклад во вÑтроенные шаблоны%{link_end}"
@@ -12046,6 +12330,9 @@ msgstr "Узнайте, как %{link_start}внеÑти Ñвой вклад вÐ
msgid "Learn how to %{no_packages_link_start}publish and share your packages%{no_packages_link_end} with GitLab."
msgstr "Узнайте, как %{no_packages_link_start}опубликовать и делитьÑÑ Ñвоими пакетами%{no_packages_link_end} Ñ GitLab."
+msgid "Learn how to enable synchronization"
+msgstr ""
+
msgid "Learn more"
msgstr "Подробнее"
@@ -12062,7 +12349,7 @@ msgid "Learn more about Vulnerability-Check"
msgstr "Узнайте больше о Vulnerability-Check"
msgid "Learn more about Web Terminal"
-msgstr ""
+msgstr "Узнайте больше о Web Terminal"
msgid "Learn more about adding certificates to your project by following the %{docs_link_start}documentation on GitLab Pages%{docs_link_end}."
msgstr ""
@@ -12125,116 +12412,116 @@ msgid "License"
msgstr "ЛицензиÑ"
msgid "License Compliance"
-msgstr ""
+msgstr "Служба комплаенÑа лицензий"
msgid "License-Check"
msgstr ""
msgid "LicenseCompliance|Add a license"
-msgstr ""
+msgstr "Добавить лицензию"
msgid "LicenseCompliance|Add license and related policy"
-msgstr ""
+msgstr "Добавить лицензию и ÑвÑзанную политику"
msgid "LicenseCompliance|Allow"
-msgstr ""
+msgstr "Разрешить"
msgid "LicenseCompliance|Allowed"
-msgstr ""
+msgstr "Разрешено"
msgid "LicenseCompliance|Cancel"
-msgstr ""
+msgstr "Отмена"
msgid "LicenseCompliance|Denied"
-msgstr ""
+msgstr "Запрещено"
msgid "LicenseCompliance|Deny"
-msgstr ""
+msgstr "Запретить"
msgid "LicenseCompliance|Here you can allow or deny licenses for this project. Using %{ci} or %{license} will allow you to see if there are any unmanaged licenses and allow or deny them in merge request."
-msgstr ""
+msgstr "ЗдеÑÑŒ вы можете разрешить или запрещать лицензии в Ñтом проекте. ИÑÐ¿Ð¾Ð»ÑŒÐ·ÑƒÑ %{ci} или %{license}, вы можете проÑматировать, еÑÑ‚ÑŒ ли здеÑÑŒ лицензии Ñ Ð½ÐµÐ¾Ð¿Ñ€ÐµÐ´ÐµÐ»ÐµÐ½Ð½Ñ‹Ð¼ ÑтатуÑом, а затем разрешить или запретить их в запроÑе на ÑлиÑние."
msgid "LicenseCompliance|License"
-msgstr ""
+msgstr "ЛицензиÑ"
msgid "LicenseCompliance|License Compliance"
-msgstr ""
+msgstr "Служба комплаенÑа лицензий"
msgid "LicenseCompliance|License Compliance detected %d license and policy violation for the source branch only; approval required"
msgid_plural "LicenseCompliance|License Compliance detected %d licenses and policy violations for the source branch only; approval required"
-msgstr[0] ""
-msgstr[1] ""
-msgstr[2] ""
-msgstr[3] ""
+msgstr[0] "Служба комплаенÑа лицензий обнаружила %d лицензию и нарушение только Ð´Ð»Ñ Ð¸Ñходной ветки; необходимо утверждение"
+msgstr[1] "Служба комплаенÑа лицензий обнаружила %d лицензии и Ð½Ð°Ñ€ÑƒÑˆÐµÐ½Ð¸Ñ Ñ‚Ð¾Ð»ÑŒÐºÐ¾ Ð´Ð»Ñ Ð¸Ñходной ветки; необходимо утверждение"
+msgstr[2] "Служба комплаенÑа лицензий обнаружила %d лицензий и нарушений только Ð´Ð»Ñ Ð¸Ñходной ветки; необходимо утверждение"
+msgstr[3] "Служба комплаенÑа лицензий обнаружила %d лицензий и нарушений только Ð´Ð»Ñ Ð¸Ñходной ветки; необходимо утверждение"
msgid "LicenseCompliance|License Compliance detected %d license for the source branch only"
msgid_plural "LicenseCompliance|License Compliance detected %d licenses for the source branch only"
-msgstr[0] ""
-msgstr[1] ""
-msgstr[2] ""
-msgstr[3] ""
+msgstr[0] "Служба комплаенÑа лицензий обнаружила %d лицензию только Ð´Ð»Ñ Ð¸Ñходной ветки"
+msgstr[1] "Служба комплаенÑа лицензий обнаружила %d лицензии только Ð´Ð»Ñ Ð¸Ñходной ветки"
+msgstr[2] "Служба комплаенÑа лицензий обнаружила %d лицензий только Ð´Ð»Ñ Ð¸Ñходной ветки"
+msgstr[3] "Служба комплаенÑа лицензий обнаружила %d лицензий только Ð´Ð»Ñ Ð¸Ñходной ветки"
msgid "LicenseCompliance|License Compliance detected %d new license"
msgid_plural "LicenseCompliance|License Compliance detected %d new licenses"
-msgstr[0] ""
-msgstr[1] ""
-msgstr[2] ""
-msgstr[3] ""
+msgstr[0] "Служба комплаенÑа лицензий обнаружила %d новую лицензию"
+msgstr[1] "Служба комплаенÑа лицензий обнаружила %d новых лицензии"
+msgstr[2] "Служба комплаенÑа лицензий обнаружила %d новых лицензий"
+msgstr[3] "Служба комплаенÑа лицензий обнаружила %d новых лицензий"
msgid "LicenseCompliance|License Compliance detected %d new license and policy violation; approval required"
msgid_plural "LicenseCompliance|License Compliance detected %d new licenses and policy violations; approval required"
-msgstr[0] ""
-msgstr[1] ""
-msgstr[2] ""
-msgstr[3] ""
+msgstr[0] "Служба комплаенÑа обнаружила %d новую лицензию и политику нарушений; необходимо утверждение"
+msgstr[1] "Служба комплаенÑа лицензий обнаружил %d новых лицензии и политики нарушений; необходимо утверждение"
+msgstr[2] "Служба комплаенÑа лицензий обнаружил %d новых лицензий и политик нарушений; необходимо утверждение"
+msgstr[3] "Служба комплаенÑа лицензий обнаружил %d новых лицензий и политик нарушений; необходимо утверждение"
msgid "LicenseCompliance|License Compliance detected no licenses for the source branch only"
-msgstr ""
+msgstr "Служба комплаенÑа лицензий не обнаружила лицензий только Ð´Ð»Ñ Ð¸Ñходной ветки"
msgid "LicenseCompliance|License Compliance detected no new licenses"
-msgstr ""
+msgstr "Служба комплаенÑа лицензий не обнаружила новых"
msgid "LicenseCompliance|License details"
-msgstr ""
+msgstr "Ð¡Ð²ÐµÐ´ÐµÐ½Ð¸Ñ Ð¾ лицензии"
msgid "LicenseCompliance|License name"
-msgstr ""
+msgstr "Ðазвание лицензии"
msgid "LicenseCompliance|License review"
-msgstr ""
+msgstr "Обзор лицензии"
msgid "LicenseCompliance|Packages"
-msgstr ""
+msgstr "Пакеты"
msgid "LicenseCompliance|Remove license"
-msgstr ""
+msgstr "Удалить лицензию"
msgid "LicenseCompliance|Remove license?"
-msgstr ""
+msgstr "Удалить лицензию?"
msgid "LicenseCompliance|Submit"
-msgstr ""
+msgstr "Отправить"
msgid "LicenseCompliance|There are currently no approved or blacklisted licenses in this project."
-msgstr ""
+msgstr "Ðа данный момент в Ñтом проекте нет утвержденных или занеÑённых в чёрный ÑпиÑок лицензий."
msgid "LicenseCompliance|There are currently no approved or blacklisted licenses that match in this project."
-msgstr ""
+msgstr "Ðа данный момент нет одобренных или занеÑенных в чёрный ÑпиÑок лицензий, ÑоответÑтвующих Ñтому проекту."
msgid "LicenseCompliance|This license already exists in this project."
-msgstr ""
+msgstr "Эта Ð»Ð¸Ñ†ÐµÐ½Ð·Ð¸Ñ ÑƒÐ¶Ðµ еÑÑ‚ÑŒ в Ñтом проекте."
msgid "LicenseCompliance|URL"
-msgstr ""
+msgstr "URL"
msgid "LicenseCompliance|You are about to remove the license, %{name}, from this project."
-msgstr ""
+msgstr "Ð’Ñ‹ ÑобираетеÑÑŒ удалить лицензию %{name} из Ñтого проекта."
msgid "LicenseManagement|Allowed"
-msgstr ""
+msgstr "Разрешено"
msgid "LicenseManagement|Denied"
-msgstr ""
+msgstr "Запрещено"
msgid "LicenseManagement|Uncategorized"
msgstr ""
@@ -12243,13 +12530,13 @@ msgid "Licensed Features"
msgstr ""
msgid "Licensed to"
-msgstr ""
+msgstr "Лицензировано длÑ"
msgid "Licenses"
msgstr "Лицензии"
msgid "Licenses|%{remainingComponentsCount} more"
-msgstr ""
+msgstr "ещё %{remainingComponentsCount}"
msgid "Licenses|Component"
msgstr ""
@@ -12261,7 +12548,7 @@ msgid "Licenses|Detected in Project"
msgstr ""
msgid "Licenses|Detected licenses that are out-of-compliance with the project's assigned policies"
-msgstr ""
+msgstr "Обнаружены лицензии Ñ Ð½ÐµÐ¾Ð¿Ñ€ÐµÐ´ÐµÐ»ÐµÐ½Ð½Ñ‹Ð¼ ÑтатуÑом комплаенÑа и политик проекта"
msgid "Licenses|Displays licenses detected in the project, based on the %{linkStart}latest successful%{linkEnd} scan"
msgstr ""
@@ -12270,10 +12557,10 @@ msgid "Licenses|Error fetching the license list. Please check your network conne
msgstr ""
msgid "Licenses|Learn more about license compliance"
-msgstr ""
+msgstr "Узнайте больше о комплаенÑе лицензий"
msgid "Licenses|License Compliance"
-msgstr ""
+msgstr "Служба комплаенÑа лицензий"
msgid "Licenses|Name"
msgstr ""
@@ -12346,7 +12633,7 @@ msgid "Link title is required"
msgstr ""
msgid "Linked emails (%{email_count})"
-msgstr ""
+msgstr "ПривÑзанные адреÑа Ñлектронной почты (%{email_count})"
msgid "Linked issues"
msgstr ""
@@ -12373,7 +12660,7 @@ msgid "List of IPs and CIDRs of allowed secondary nodes. Comma-separated, e.g. \
msgstr ""
msgid "List settings"
-msgstr ""
+msgstr "ПеречиÑлить наÑтройки"
msgid "List the merge requests that must be merged before this one."
msgstr ""
@@ -12387,11 +12674,8 @@ msgstr "СпиÑок репозиториев из Bitbucket Server"
msgid "Live preview"
msgstr "Предварительный проÑмотр в реальном времени"
-msgid "Load more vulnerabilities"
-msgstr ""
-
msgid "Loading"
-msgstr ""
+msgstr "Загрузка"
msgid "Loading contribution stats for group members"
msgstr ""
@@ -12406,7 +12690,7 @@ msgid "Loading issues"
msgstr ""
msgid "Loading snippet"
-msgstr ""
+msgstr "Загрузка Ñниппета"
msgid "Loading the GitLab IDE..."
msgstr "Загрузка GitLab IDE..."
@@ -12415,13 +12699,13 @@ msgid "Loading..."
msgstr "Загрузка..."
msgid "Loading…"
-msgstr ""
+msgstr "Загрузка…"
msgid "Localization"
msgstr "ЛокализациÑ"
msgid "Location"
-msgstr ""
+msgstr "МеÑтоположение"
msgid "Lock"
msgstr "Блокировка"
@@ -12430,7 +12714,7 @@ msgid "Lock %{issuableDisplayName}"
msgstr "Заблокировать %{issuableDisplayName}"
msgid "Lock memberships to LDAP synchronization"
-msgstr ""
+msgstr "Блокировка членÑтва в Ñинхронизации LDAP"
msgid "Lock not found"
msgstr "Блокировка не найдена"
@@ -12481,10 +12765,10 @@ msgid "Low vulnerabilities present"
msgstr ""
msgid "MB"
-msgstr ""
+msgstr "Мбайт"
msgid "MD5"
-msgstr ""
+msgstr "MD5"
msgid "MERGED"
msgstr "СЛИТО"
@@ -12493,7 +12777,7 @@ msgid "MR widget|Take a look at our %{beginnerLinkStart}Beginner's Guide to Cont
msgstr ""
msgid "MR widget|The pipeline will now run automatically every time you commit code. Pipelines are useful for deploying static web pages, detecting vulnerabilities in dependencies, static or dynamic application security testing (SAST and DAST), and so much more!"
-msgstr ""
+msgstr "Теперь ÑÐ±Ð¾Ñ€Ð¾Ñ‡Ð½Ð°Ñ Ð»Ð¸Ð½Ð¸Ñ Ð±ÑƒÐ´ÐµÑ‚ автоматичеÑки запуÑкатьÑÑ, когда вы фикÑируете код. Сборочные линии полезны Ð´Ð»Ñ Ñ€Ð°Ð·Ð²ÐµÑ€Ñ‚Ñ‹Ð²Ð°Ð½Ð¸Ñ ÑтатичеÑких веб-Ñтраниц, Ð¾Ð±Ð½Ð°Ñ€ÑƒÐ¶ÐµÐ½Ð¸Ñ ÑƒÑзвимоÑтей в завиÑимоÑÑ‚ÑÑ…, ÑтатичеÑкого или динамичеÑкого теÑÑ‚Ð¸Ñ€Ð¾Ð²Ð°Ð½Ð¸Ñ Ð±ÐµÐ·Ð¾Ð¿Ð°ÑноÑти (SAST и DAST) и многого другого!"
msgid "MRApprovals|Approvals"
msgstr ""
@@ -12514,7 +12798,7 @@ msgid "Made this issue confidential."
msgstr ""
msgid "Maintenance mode"
-msgstr ""
+msgstr "Режим обÑлуживаниÑ"
msgid "Make and review changes in the browser with the Web IDE"
msgstr "ВнеÑите и проÑмотрите Ð¸Ð·Ð¼ÐµÐ½ÐµÐ½Ð¸Ñ Ð² браузере Ñ Ð¿Ð¾Ð¼Ð¾Ñ‰ÑŒÑŽ Web IDE"
@@ -12540,11 +12824,8 @@ msgstr ""
msgid "Manage"
msgstr ""
-msgid "Manage Git repositories with fine-grained access controls that keep your code secure. Perform code reviews and enhance collaboration with merge requests. Each project can also have an issue tracker and a wiki."
-msgstr "УправлÑйте Git репозиториÑми иÑÐ¿Ð¾Ð»ÑŒÐ·ÑƒÑ Ð´ÐµÑ‚Ð°Ð»ÑŒÐ½Ñ‹Ðµ наÑтройки доÑтупа, которые защитÑÑ‚ ваш код. ВыполнÑйте проверку кода и улучшите взаимодейÑтвие вашей команды запроÑами на ÑлиÑние. Ð”Ð»Ñ ÐºÐ°Ð¶Ð´Ð¾Ð³Ð¾ проекта также доÑтупны ÑиÑтема обÑуждений и Wiki."
-
msgid "Manage Web IDE features"
-msgstr ""
+msgstr "Управление функциÑми Web IDE"
msgid "Manage access"
msgstr ""
@@ -12571,6 +12852,9 @@ msgid "Manage two-factor authentication"
msgstr "Управление двухфакторной аутентификацией"
msgid "Manage your license"
+msgstr "УправлÑйте Ñвоей лицензией"
+
+msgid "Managed Account"
msgstr ""
msgid "Manifest"
@@ -12709,10 +12993,10 @@ msgid "Maximum allowable lifetime for personal access token (days)"
msgstr ""
msgid "Maximum artifacts size (MB)"
-msgstr ""
+msgstr "МакÑимальный размер артефактов (Мбайт)"
msgid "Maximum attachment size (MB)"
-msgstr ""
+msgstr "МакÑимальный размер Ð²Ð»Ð¾Ð¶ÐµÐ½Ð¸Ñ (Мбайт)"
msgid "Maximum bulk request size (MiB)"
msgstr ""
@@ -12724,7 +13008,7 @@ msgid "Maximum concurrency of Elasticsearch bulk requests per indexing operation
msgstr ""
msgid "Maximum delay (Minutes)"
-msgstr ""
+msgstr "МакÑÐ¸Ð¼Ð°Ð»ÑŒÐ½Ð°Ñ Ð·Ð°Ð´ÐµÑ€Ð¶ÐºÐ° (в минутах)"
msgid "Maximum duration of a session."
msgstr ""
@@ -12742,7 +13026,7 @@ msgid "Maximum lifetime allowable for Personal Access Tokens is active, your exp
msgstr ""
msgid "Maximum namespace storage (MB)"
-msgstr ""
+msgstr "МакÑимальный размер проÑтранÑтва имён (Мбайт)"
msgid "Maximum number of %{name} (%{count}) exceeded"
msgstr ""
@@ -12760,7 +13044,7 @@ msgid "Maximum page reached"
msgstr ""
msgid "Maximum push size (MB)"
-msgstr ""
+msgstr "МакÑимальный размер отправки (Мбайт)"
msgid "Maximum size limit for a single commit."
msgstr ""
@@ -12837,6 +13121,9 @@ msgstr "Ð—Ð°Ð¿Ñ€Ð¾Ñ Ð½Ð° ÑлиÑние Ñоздан"
msgid "Merge Requests in Review"
msgstr ""
+msgid "Merge automatically (%{strategy})"
+msgstr ""
+
msgid "Merge commit message"
msgstr "Ðапишите комментарий к коммиту запроÑа на ÑлиÑние"
@@ -12919,7 +13206,7 @@ msgid "MergeRequests|Jump to next unresolved thread"
msgstr ""
msgid "MergeRequests|Reply..."
-msgstr ""
+msgstr "Ответить..."
msgid "MergeRequests|Resolve this thread in a new issue"
msgstr ""
@@ -12946,7 +13233,7 @@ msgid "MergeRequests|Toggle comments for this file"
msgstr ""
msgid "MergeRequests|View file @ %{commitId}"
-msgstr ""
+msgstr "ПроÑмотр файла @ %{commitId}"
msgid "MergeRequests|View replaced file @ %{commitId}"
msgstr ""
@@ -12976,7 +13263,7 @@ msgid "MergeRequest|Error dismissing suggestion popover. Please try again."
msgstr ""
msgid "MergeRequest|Error loading full diff. Please try again."
-msgstr "Ошибка при загрузке полной разницы. ПожалуйÑта, попробуйте еще раз."
+msgstr "Ошибка при загрузке полной разницы. ПожалуйÑта, попробуйте ещё раз."
msgid "MergeRequest|No files found"
msgstr ""
@@ -12990,6 +13277,12 @@ msgstr "Слито"
msgid "Merged branches are being deleted. This can take some time depending on the number of branches. Please refresh the page to see changes."
msgstr "Объединенные ветви удалÑÑŽÑ‚ÑÑ. Это может занÑÑ‚ÑŒ некоторое Ð²Ñ€ÐµÐ¼Ñ Ð² завиÑимоÑти от количеÑтва ветвей. ПожалуйÑта, обновите Ñтраницу, чтобы увидеть изменениÑ."
+msgid "Merged this merge request."
+msgstr ""
+
+msgid "Merges this merge request immediately."
+msgstr ""
+
msgid "Merges this merge request when the pipeline succeeds."
msgstr ""
@@ -13033,7 +13326,7 @@ msgid "Metrics - Prometheus"
msgstr "Метрики - Prometheus"
msgid "Metrics Dashboard"
-msgstr ""
+msgstr "Панель ÑƒÐ¿Ñ€Ð°Ð²Ð»ÐµÐ½Ð¸Ñ Ð¼ÐµÑ‚Ñ€Ð¸ÐºÐ°Ð¼Ð¸"
msgid "Metrics and profiling"
msgstr "Метрики и профилирование"
@@ -13066,13 +13359,13 @@ msgid "Metrics|Add metric"
msgstr "Добавить метрику"
msgid "Metrics|Avg"
-msgstr ""
+msgstr "Ср"
msgid "Metrics|Check out the CI/CD documentation on deploying to an environment"
msgstr ""
msgid "Metrics|Create custom dashboard %{fileName}"
-msgstr ""
+msgstr "Создать пользовательÑкую панель ÑƒÐ¿Ñ€Ð°Ð²Ð»ÐµÐ½Ð¸Ñ %{fileName}"
msgid "Metrics|Create metric"
msgstr "Создать метрику"
@@ -13087,7 +13380,7 @@ msgid "Metrics|Duplicate"
msgstr ""
msgid "Metrics|Duplicate dashboard"
-msgstr ""
+msgstr "Дублировать панель управлениÑ"
msgid "Metrics|Duplicating..."
msgstr ""
@@ -13099,12 +13392,15 @@ msgstr[1] ""
msgstr[2] ""
msgstr[3] ""
-msgid "Metrics|Environment"
+msgid "Metrics|Expand panel"
msgstr ""
msgid "Metrics|For grouping similar metrics"
msgstr ""
+msgid "Metrics|Go back (Esc)"
+msgstr ""
+
msgid "Metrics|Invalid time range, please verify."
msgstr ""
@@ -13118,7 +13414,7 @@ msgid "Metrics|Link contains an invalid time window, please verify the link to s
msgstr ""
msgid "Metrics|Max"
-msgstr ""
+msgstr "МакÑ"
msgid "Metrics|Must be a valid PromQL query."
msgstr ""
@@ -13133,16 +13429,13 @@ msgid "Metrics|Prometheus Query Documentation"
msgstr ""
msgid "Metrics|Refresh dashboard"
-msgstr ""
-
-msgid "Metrics|Show last"
-msgstr "Показать поÑледнее"
+msgstr "Обновить панель управлениÑ"
msgid "Metrics|There was an error creating the dashboard."
-msgstr ""
+msgstr "Произошла ошибка при Ñоздании панели управлениÑ"
msgid "Metrics|There was an error creating the dashboard. %{error}"
-msgstr ""
+msgstr "Произошла ошибка при Ñоздании панели управлениÑ. %{error}"
msgid "Metrics|There was an error fetching annotations. Please try again."
msgstr ""
@@ -13151,10 +13444,10 @@ msgid "Metrics|There was an error fetching the environments data, please try aga
msgstr ""
msgid "Metrics|There was an error getting annotations information."
-msgstr ""
+msgstr "Произошла ошибка при получении информации об аннотациÑÑ…."
msgid "Metrics|There was an error getting deployment information."
-msgstr ""
+msgstr "Произошла ошибка при получении Ñведений о развёртывании."
msgid "Metrics|There was an error getting environments information."
msgstr ""
@@ -13169,7 +13462,7 @@ msgid "Metrics|There was an error while retrieving metrics. %{message}"
msgstr ""
msgid "Metrics|Unexpected deployment data response from prometheus endpoint"
-msgstr ""
+msgstr "Ðеожиданные данные Ñ€Ð°Ð·Ð²Ñ‘Ñ€Ñ‚Ñ‹Ð²Ð°Ð½Ð¸Ñ Ð² ответе от точки Ð¿Ð¾Ð´ÐºÐ»ÑŽÑ‡ÐµÐ½Ð¸Ñ Prometheus"
msgid "Metrics|Unit label"
msgstr ""
@@ -13181,7 +13474,7 @@ msgid "Metrics|Used if the query returns a single series. If it returns multiple
msgstr ""
msgid "Metrics|Validating query"
-msgstr ""
+msgstr "Проверка запроÑа"
msgid "Metrics|Values"
msgstr ""
@@ -13216,6 +13509,9 @@ msgstr "например, запроÑов в Ñекунду"
msgid "Microsoft Azure"
msgstr ""
+msgid "Middleman project with Static Site Editor support"
+msgstr ""
+
msgid "Migrated %{success_count}/%{total_count} files."
msgstr "ПеренеÑено %{success_count}/%{total_count} файлов."
@@ -13224,10 +13520,10 @@ msgstr ""
msgid "Milestone"
msgid_plural "Milestones"
-msgstr[0] ""
-msgstr[1] ""
-msgstr[2] ""
-msgstr[3] ""
+msgstr[0] "Этап"
+msgstr[1] "Этапы"
+msgstr[2] "Этапы"
+msgstr[3] "Этапы"
msgid "Milestone lists not available with your current license"
msgstr ""
@@ -13239,10 +13535,10 @@ msgid "Milestones"
msgstr "Этапы"
msgid "Milestones| You’re about to permanently delete the milestone %{milestoneTitle} and remove it from %{issuesWithCount} and %{mergeRequestsWithCount}. Once deleted, it cannot be undone or recovered."
-msgstr ""
+msgstr "Ð’Ñ‹ ÑобираетеÑÑŒ окончательно удалить Ñтап %{milestoneTitle} и удалить его из %{issuesWithCount} и %{mergeRequestsWithCount}. Это необратимо."
msgid "Milestones| You’re about to permanently delete the milestone %{milestoneTitle}. This milestone is not currently used in any issues or merge requests."
-msgstr ""
+msgstr "Ð’Ñ‹ ÑобираетеÑÑŒ безвозвратно удалить Ñтап %{milestoneTitle}. Этот Ñтап в наÑтоÑщее Ð²Ñ€ÐµÐ¼Ñ Ð½Ðµ иÑпользуетÑÑ Ð½Ð¸ в обÑуждениÑÑ…, ни в запроÑах на ÑлиÑние."
msgid "Milestones|Delete milestone"
msgstr "Удалить Ñтап"
@@ -13266,7 +13562,7 @@ msgid "Milestones|Promoting %{milestoneTitle} will make it available for all pro
msgstr "Повышение ÑƒÑ€Ð¾Ð²Ð½Ñ %{milestoneTitle} Ñделает его доÑтупным Ð´Ð»Ñ Ð²Ñех проектов внутри %{groupName}. СущеÑтвующие Ñтапы проекта Ñ Ñ‚Ð°ÐºÐ¸Ð¼ же заголовком будут объединены."
msgid "Milestones|This action cannot be reversed."
-msgstr ""
+msgstr "Это дейÑтвие не может быть отменено."
msgid "Minimum capacity to be available before we schedule more mirrors preemptively."
msgstr ""
@@ -13278,16 +13574,16 @@ msgid "Minimum length is %{minimum_password_length} characters."
msgstr "ÐœÐ¸Ð½Ð¸Ð¼Ð°Ð»ÑŒÐ½Ð°Ñ Ð´Ð»Ð¸Ð½Ð° - %{minimum_password_length} Ñимволов."
msgid "Minimum password length (number of characters)"
-msgstr ""
+msgstr "ÐœÐ¸Ð½Ð¸Ð¼Ð°Ð»ÑŒÐ½Ð°Ñ Ð´Ð»Ð¸Ð½Ð° Ð¿Ð°Ñ€Ð¾Ð»Ñ (количеÑтво Ñимволов)"
msgid "Minutes"
-msgstr ""
+msgstr "Минуты"
msgid "Mirror direction"
-msgstr ""
+msgstr "Ðаправление зеркалированиÑ"
msgid "Mirror repository"
-msgstr ""
+msgstr "Зеркалировать репозиторий"
msgid "Mirror settings are only available to GitLab administrators."
msgstr ""
@@ -13299,19 +13595,19 @@ msgid "Mirrored branches will have this prefix. If you enabled 'Only mirror prot
msgstr ""
msgid "Mirrored repositories"
-msgstr ""
+msgstr "Отзеркаленные репозитории"
msgid "Mirroring repositories"
-msgstr ""
+msgstr "Зеркалирование репозиториев"
msgid "Mirroring settings were successfully updated."
-msgstr ""
+msgstr "ÐаÑтройки Ð·ÐµÑ€ÐºÐ°Ð»Ð¸Ñ€Ð¾Ð²Ð°Ð½Ð¸Ñ Ð±Ñ‹Ð»Ð¸ уÑпешно обновлены."
msgid "Mirroring settings were successfully updated. The project is being updated."
msgstr ""
msgid "Mirroring was successfully disabled."
-msgstr ""
+msgstr "Зеркалирование было уÑпешно отключено."
msgid "Mirroring will only be available if the feature is included in the plan of the selected group or user."
msgstr ""
@@ -13320,13 +13616,13 @@ msgid "Missing commit signatures endpoint!"
msgstr ""
msgid "MissingSSHKeyWarningLink|Add SSH key"
-msgstr ""
+msgstr "Добавить ключ SSH"
msgid "MissingSSHKeyWarningLink|Don't show again"
-msgstr ""
+msgstr "Больше не показывать"
msgid "MissingSSHKeyWarningLink|You won't be able to pull or push project code via SSH until you add an SSH key to your profile"
-msgstr ""
+msgstr "Ð’Ñ‹ не Ñможете отправлÑÑ‚ÑŒ или получать код проекта Ñ Ð¿Ð¾Ð¼Ð¾Ñ‰ÑŒÑŽ SSH, пока не добавите ключ SSH в ваш профиль"
msgid "Modal|Cancel"
msgstr "Отмена"
@@ -13374,10 +13670,10 @@ msgid "More actions"
msgstr "Больше дейÑтвий"
msgid "More details"
-msgstr ""
+msgstr "Подробнее"
msgid "More info"
-msgstr ""
+msgstr "Подробнее"
msgid "More information"
msgstr "Ð”Ð¾Ð¿Ð¾Ð»Ð½Ð¸Ñ‚ÐµÐ»ÑŒÐ½Ð°Ñ Ð¸Ð½Ñ„Ð¾Ñ€Ð¼Ð°Ñ†Ð¸Ñ"
@@ -13416,10 +13712,10 @@ msgid "Move this issue to another project."
msgstr ""
msgid "MoveIssue|Cannot move issue due to insufficient permissions!"
-msgstr ""
+msgstr "Ðевозможно перемеÑтить обÑуждение из-за нехватки прав!"
msgid "MoveIssue|Cannot move issue to project it originates from!"
-msgstr ""
+msgstr "Ðевозможно перемеÑтить обÑуждение в проект, в котором оно было первоначально Ñоздано!"
msgid "Moved issue to %{label} column in the board."
msgstr "ОбÑуждение перенеÑено в колонку %{label} на доÑке."
@@ -13434,10 +13730,10 @@ msgid "Moves this issue to %{path_to_project}."
msgstr ""
msgid "MrDeploymentActions|Deploy"
-msgstr ""
+msgstr "Развернуть"
msgid "MrDeploymentActions|Re-deploy"
-msgstr ""
+msgstr "Развернуть повторно"
msgid "MrDeploymentActions|Stop environment"
msgstr ""
@@ -13452,10 +13748,10 @@ msgid "Multiple uploaders found: %{uploader_types}"
msgstr "Ðайдено неÑколько загрузчиков: %{uploader_types}"
msgid "My company or team"
-msgstr ""
+msgstr "ÐœÐ¾Ñ ÐºÐ¾Ð¼Ð¿Ð°Ð½Ð¸Ñ Ð¸Ð»Ð¸ команда"
msgid "My-Reaction"
-msgstr ""
+msgstr "ÐœÐ¾Ñ Ñ€ÐµÐ°ÐºÑ†Ð¸Ñ"
msgid "Name"
msgstr "ИмÑ"
@@ -13473,7 +13769,7 @@ msgid "Name:"
msgstr "Ðаименование:"
msgid "Namespace is empty"
-msgstr ""
+msgstr "ПроÑтранÑтво имён пуÑто"
msgid "Namespace: %{namespace}"
msgstr ""
@@ -13588,7 +13884,7 @@ msgid "New epic"
msgstr "ÐÐ¾Ð²Ð°Ñ Ñ†ÐµÐ»ÑŒ"
msgid "New epic title"
-msgstr ""
+msgstr "Ðовый заголовок цели"
msgid "New file"
msgstr "Ðовый файл"
@@ -13630,7 +13926,7 @@ msgid "New release"
msgstr ""
msgid "New requirement"
-msgstr ""
+msgstr "Ðовое требование"
msgid "New runners registration token has been generated!"
msgstr "Ðовый региÑтрационный токен обработчика заданий Ñгенерирован!"
@@ -13663,13 +13959,13 @@ msgid "Newly registered users will by default be external"
msgstr ""
msgid "Next"
-msgstr ""
+msgstr "Next"
-msgid "Next file in diff (MRs only)"
+msgid "Next file in diff"
msgstr ""
-msgid "Next unresolved discussion (MRs only)"
-msgstr "Следующее неразрешенное обÑуждение (только MRs)"
+msgid "Next unresolved discussion"
+msgstr ""
msgid "Nickname"
msgstr "Ðикнейм"
@@ -13687,7 +13983,7 @@ msgid "No %{replicableType} match this filter"
msgstr ""
msgid "No Epic"
-msgstr ""
+msgstr "Ðет цели"
msgid "No Label"
msgstr ""
@@ -13728,6 +14024,9 @@ msgstr "Ðет изменений"
msgid "No changes between %{ref_start}%{source_branch}%{ref_end} and %{ref_start}%{target_branch}%{ref_end}"
msgstr ""
+msgid "No child epics match applied filters"
+msgstr ""
+
msgid "No connection could be made to a Gitaly Server, please check your logs!"
msgstr ""
@@ -13750,7 +14049,7 @@ msgid "No data to display"
msgstr "Ðет данных Ð´Ð»Ñ Ð¾Ñ‚Ð¾Ð±Ñ€Ð°Ð¶ÐµÐ½Ð¸Ñ"
msgid "No deployments found"
-msgstr ""
+msgstr "Ð Ð°Ð·Ð²Ñ‘Ñ€Ñ‚Ñ‹Ð²Ð°Ð½Ð¸Ñ Ð½Ðµ найдены"
msgid "No due date"
msgstr "Дата Ð·Ð°Ð²ÐµÑ€ÑˆÐµÐ½Ð¸Ñ Ð½Ðµ указана"
@@ -13822,7 +14121,7 @@ msgid "No parent group"
msgstr ""
msgid "No pods available"
-msgstr ""
+msgstr "Ðет доÑтупных подов"
msgid "No policy matches this license"
msgstr ""
@@ -13834,7 +14133,7 @@ msgid "No prioritized labels with such name or description"
msgstr ""
msgid "No public groups"
-msgstr ""
+msgstr "Ðет публичных групп"
msgid "No related merge requests found."
msgstr ""
@@ -13866,20 +14165,11 @@ msgstr ""
msgid "No thanks, don't show this again"
msgstr ""
-msgid "No vulnerabilities found for this group"
-msgstr ""
-
-msgid "No vulnerabilities found for this pipeline"
-msgstr ""
-
-msgid "No vulnerabilities found for this project"
-msgstr ""
-
msgid "No vulnerabilities present"
msgstr ""
msgid "No webhooks found, add one in the form above."
-msgstr ""
+msgstr "Веб-обработчики не найдены, добавьте один в форме выше."
msgid "No worries, you can still use all the %{strong}%{plan_name}%{strong_close} features for now. You have %{remaining_days} to renew your subscription."
msgstr ""
@@ -13948,7 +14238,7 @@ msgid "Not started"
msgstr ""
msgid "Not-confidential epic cannot be assigned to a confidential parent epic"
-msgstr ""
+msgstr "ÐÐµÐºÐ¾Ð½Ñ„Ð¸Ð´ÐµÐ½Ñ†Ð¸Ð°Ð»ÑŒÐ½Ð°Ñ Ñ†ÐµÐ»ÑŒ не может быть назначена к конфиденциальной родительÑкой цели"
msgid "Note"
msgstr "Заметка"
@@ -13972,25 +14262,25 @@ msgid "Note: Consider asking your GitLab administrator to configure %{github_int
msgstr ""
msgid "NoteForm|Note"
-msgstr ""
+msgstr "Примечание"
msgid "Notes|Are you sure you want to cancel creating this comment?"
msgstr "Ð’Ñ‹ уверены, что вы хотите отменить Ñоздание Ñтого комментариÑ?"
msgid "Notes|Collapse replies"
-msgstr ""
+msgstr "Свернуть ответы"
msgid "Notes|Show all activity"
msgstr ""
msgid "Notes|Show comments only"
-msgstr ""
+msgstr "Показывать только комментарии"
msgid "Notes|Show history only"
-msgstr ""
+msgstr "Показывать только иÑторию"
msgid "Notes|This comment has changed since you started editing, please review the %{open_link}updated comment%{close_link} to ensure information is not lost"
-msgstr ""
+msgstr "Этот комментарий изменилÑÑ Ñ Ñ‚ÐµÑ… пор, как вы начали редактирование, проÑмотрите %{open_link}обновленный комментарий%{close_link}, чтобы убедитьÑÑ, что Ð¸Ð½Ñ„Ð¾Ñ€Ð¼Ð°Ñ†Ð¸Ñ Ð½Ðµ потерÑна"
msgid "Nothing found…"
msgstr ""
@@ -13998,17 +14288,20 @@ msgstr ""
msgid "Nothing to preview."
msgstr ""
+msgid "Nothing to synchronize"
+msgstr ""
+
msgid "Notification events"
msgstr "Ð£Ð²ÐµÐ´Ð¾Ð¼Ð»ÐµÐ½Ð¸Ñ Ð¾ ÑобытиÑÑ…"
msgid "Notification setting"
-msgstr "ÐаÑтройка уведомлениÑ"
+msgstr "Режим уведомлений"
msgid "Notification setting - %{notification_title}"
msgstr ""
msgid "Notification settings saved"
-msgstr ""
+msgstr "ÐаÑтройки уведомлений Ñохранены"
msgid "NotificationEvent|Close issue"
msgstr "ОбÑуждение закрыто"
@@ -14020,13 +14313,13 @@ msgid "NotificationEvent|Failed pipeline"
msgstr "Ðеудача в Ñборочной линии"
msgid "NotificationEvent|Fixed pipeline"
-msgstr ""
+msgstr "Ð¡Ð±Ð¾Ñ€Ð¾Ñ‡Ð½Ð°Ñ Ð»Ð¸Ð½Ð¸Ñ Ð¸Ñправлена"
msgid "NotificationEvent|Merge merge request"
msgstr "Влит Ð·Ð°Ð¿Ñ€Ð¾Ñ Ð½Ð° ÑлиÑние"
msgid "NotificationEvent|New epic"
-msgstr ""
+msgstr "ÐÐ¾Ð²Ð°Ñ Ñ†ÐµÐ»ÑŒ"
msgid "NotificationEvent|New issue"
msgstr "Ðовое обÑуждение"
@@ -14071,7 +14364,7 @@ msgid "NotificationLevel|Watch"
msgstr "ОтÑлеживать"
msgid "NotificationSetting|Custom"
-msgstr ""
+msgstr "ПользовательÑкий"
msgid "Notifications"
msgstr "УведомлениÑ"
@@ -14110,7 +14403,7 @@ msgid "Number of changes (branches or tags) in a single push to determine whethe
msgstr ""
msgid "Number of changes (branches or tags) in a single push to determine whether webhooks and services will be fired or not. Webhooks and services won't be submitted if it surpasses that value."
-msgstr ""
+msgstr "КоличеÑтво изменений (веток или тегов) в одной отправке, которое определÑет, будут ли запущены веб-обработчики и Ñлужбы. Веб-обработчики и Ñлужбы не будут выполнÑÑ‚ÑŒÑÑ, еÑли Ñто значение будет превышено."
msgid "Number of commits"
msgstr ""
@@ -14169,6 +14462,9 @@ msgstr "ПоÑле импорта, репозитории могут зеркаÐ
msgid "Once removed, the fork relationship cannot be restored and you will no longer be able to send merge requests to the source."
msgstr "ПоÑле удалениÑ, ÑвÑзь Ð¾Ñ‚Ð²ÐµÑ‚Ð²Ð»ÐµÐ½Ð¸Ñ Ð½Ðµ может быть воÑÑтановлена, и вы больше не Ñможете отправлÑÑ‚ÑŒ запроÑÑ‹ на ÑлиÑние в иÑточник."
+msgid "Once the exported file is ready you can download it from this page."
+msgstr ""
+
msgid "Once the exported file is ready, you will receive a notification email with a download link, or you can download it from this page."
msgstr "ПоÑле того, как ÑкÑпортируемый файл будет готов, вы получите уведомление по Ñлектронной почте Ñ ÑÑылкой Ð´Ð»Ñ ÑкачиваниÑ, или вы Ñможете Ñкачать его Ñ Ñтой Ñтраницы."
@@ -14272,9 +14568,6 @@ msgstr "Открыть иÑходник"
msgid "Open sidebar"
msgstr ""
-msgid "Open source software to collaborate on code"
-msgstr "Открытое ПО Ð´Ð»Ñ ÑовмеÑтной работы над кодом"
-
msgid "Open: %{openIssuesCount}"
msgstr ""
@@ -14324,7 +14617,7 @@ msgid "OperationsDashboard|Operations Dashboard"
msgstr "Панель ÑƒÐ¿Ñ€Ð°Ð²Ð»ÐµÐ½Ð¸Ñ Ð¾Ð¿ÐµÑ€Ð°Ñ†Ð¸Ñми"
msgid "OperationsDashboard|The operations dashboard provides a summary of each project's operational health, including pipeline and alert statuses."
-msgstr ""
+msgstr "Панель ÑƒÐ¿Ñ€Ð°Ð²Ð»ÐµÐ½Ð¸Ñ Ð¾Ð¿ÐµÑ€Ð°Ñ†Ð¸Ñми Ñодержит Ñводку о работоÑпоÑобноÑти каждого проекта, Ð²ÐºÐ»ÑŽÑ‡Ð°Ñ ÑоÑтоÑÐ½Ð¸Ñ ÐºÐ¾Ð½Ð²ÐµÐ¹ÐµÑ€Ð° и предупреждений."
msgid "Optional"
msgstr ""
@@ -14360,7 +14653,7 @@ msgid "Other visibility settings have been disabled by the administrator."
msgstr ""
msgid "Out-of-compliance with this project's policies and should be removed"
-msgstr ""
+msgstr "Ðе определена политиками проекта и должна быть удалена"
msgid "Outbound requests"
msgstr "ИÑходÑщие запроÑÑ‹"
@@ -14377,6 +14670,9 @@ msgstr ""
msgid "OutdatedBrowser|You can provide feedback %{feedback_link_start}on this issue%{feedback_link_end} or via your usual support channels."
msgstr ""
+msgid "Overridden"
+msgstr ""
+
msgid "Overview"
msgstr "Обзор"
@@ -14428,6 +14724,9 @@ msgstr ""
msgid "PackageRegistry|Conan Command"
msgstr ""
+msgid "PackageRegistry|Copy .pypirc content"
+msgstr ""
+
msgid "PackageRegistry|Copy Conan Command"
msgstr ""
@@ -14449,20 +14748,23 @@ msgstr ""
msgid "PackageRegistry|Copy NuGet Setup Command"
msgstr ""
+msgid "PackageRegistry|Copy Pip command"
+msgstr ""
+
msgid "PackageRegistry|Copy and paste this inside your %{codeStart}pom.xml%{codeEnd} %{codeStart}dependencies%{codeEnd} block."
msgstr ""
msgid "PackageRegistry|Copy npm command"
-msgstr ""
+msgstr "Копировать команду npm"
msgid "PackageRegistry|Copy npm setup command"
-msgstr ""
+msgstr "Копировать команду наÑтройки npm"
msgid "PackageRegistry|Copy yarn command"
-msgstr ""
+msgstr "Копировать команду yarn"
msgid "PackageRegistry|Copy yarn setup command"
-msgstr ""
+msgstr "Копировать команду наÑтройки yarn"
msgid "PackageRegistry|Delete Package Version"
msgstr "Удалить верÑию пакета"
@@ -14482,15 +14784,24 @@ msgstr ""
msgid "PackageRegistry|For more information on the NuGet registry, %{linkStart}see the documentation%{linkEnd}."
msgstr ""
+msgid "PackageRegistry|For more information on the PyPi registry, %{linkStart}see the documentation%{linkEnd}."
+msgstr ""
+
+msgid "PackageRegistry|If you haven't already done so, you will need to add the below to your %{codeStart}.pypirc%{codeEnd} file."
+msgstr ""
+
msgid "PackageRegistry|If you haven't already done so, you will need to add the below to your %{codeStart}pom.xml%{codeEnd} file."
msgstr ""
msgid "PackageRegistry|Installation"
-msgstr ""
+msgstr "УÑтановка"
msgid "PackageRegistry|Learn how to %{noPackagesLinkStart}publish and share your packages%{noPackagesLinkEnd} with GitLab."
msgstr ""
+msgid "PackageRegistry|Manually Published"
+msgstr ""
+
msgid "PackageRegistry|Maven"
msgstr ""
@@ -14509,17 +14820,23 @@ msgstr ""
msgid "PackageRegistry|NuGet Command"
msgstr ""
+msgid "PackageRegistry|Pip Command"
+msgstr ""
+
msgid "PackageRegistry|Pipeline %{linkStart}%{linkEnd} triggered %{timestamp} by %{author}"
msgstr ""
msgid "PackageRegistry|Published to the repository at %{timestamp}"
msgstr ""
-msgid "PackageRegistry|Registry Setup"
+msgid "PackageRegistry|PyPi"
msgstr ""
+msgid "PackageRegistry|Registry Setup"
+msgstr "ÐаÑтройка рееÑтра"
+
msgid "PackageRegistry|Remove package"
-msgstr ""
+msgstr "Удалить пакет"
msgid "PackageRegistry|Sorry, your filter produced no results"
msgstr ""
@@ -14528,7 +14845,7 @@ msgid "PackageRegistry|There are no %{packageType} packages yet"
msgstr ""
msgid "PackageRegistry|There are no packages yet"
-msgstr ""
+msgstr "Пока что нет пакетов"
msgid "PackageRegistry|There was a problem fetching the details for this package."
msgstr "Ðе удалоÑÑŒ получить ÑÐ²ÐµÐ´ÐµÐ½Ð¸Ñ Ð¾Ð± Ñтом пакете."
@@ -14549,10 +14866,13 @@ msgid "PackageRegistry|You may also need to setup authentication using an auth t
msgstr ""
msgid "PackageRegistry|npm"
+msgstr "npm"
+
+msgid "PackageRegistry|published by %{author}"
msgstr ""
msgid "PackageRegistry|yarn"
-msgstr ""
+msgstr "yarn"
msgid "PackageType|Conan"
msgstr ""
@@ -14566,6 +14886,9 @@ msgstr ""
msgid "PackageType|NuGet"
msgstr ""
+msgid "PackageType|PyPi"
+msgstr ""
+
msgid "Packages"
msgstr "Пакеты"
@@ -14579,7 +14902,7 @@ msgid "Pages"
msgstr "Страницы"
msgid "Pages Domain"
-msgstr ""
+msgstr "Домен Pages"
msgid "Pages getting started guide"
msgstr "РуководÑтво по работе Ñ Ñтраницами"
@@ -14600,10 +14923,10 @@ msgid "Pagination|Last »"
msgstr "ПоÑледнÑÑ Â»"
msgid "Pagination|Next"
-msgstr ""
+msgstr "След."
msgid "Pagination|Prev"
-msgstr ""
+msgstr "Пред."
msgid "Pagination|« First"
msgstr "« ПерваÑ"
@@ -14618,7 +14941,7 @@ msgid "Parent"
msgstr ""
msgid "Parent epic doesn't exist."
-msgstr ""
+msgstr "РодительÑÐºÐ°Ñ Ñ†ÐµÐ»ÑŒ не ÑущеÑтвует."
msgid "Parent epic is not present."
msgstr ""
@@ -14659,11 +14982,14 @@ msgstr ""
msgid "Past due"
msgstr ""
+msgid "Paste a machine public key here. Read more about how to generate it"
+msgstr ""
+
msgid "Paste a machine public key here. Read more about how to generate it %{link_start}here%{link_end}"
msgstr "Ð’Ñтавьте открытый ключ машины здеÑÑŒ. Узнайте больше о том, как его Ñгенерировать %{link_start}здеÑÑŒ%{link_end}"
msgid "Paste epic link"
-msgstr ""
+msgstr "Ð’Ñтавить ÑÑылку на цель"
msgid "Paste issue link"
msgstr ""
@@ -14674,9 +15000,6 @@ msgstr "Ð’Ñтавьте ваш открытый ключ SSH, который о
msgid "Path"
msgstr ""
-msgid "Path, transfer, remove"
-msgstr "Путь, перемещение, удаление"
-
msgid "Path:"
msgstr "Путь:"
@@ -14707,8 +15030,8 @@ msgstr ""
msgid "Percentage"
msgstr ""
-msgid "Perform advanced options such as changing path, transferring, or removing the group."
-msgstr "Выполнение раÑширенных операций, таких как изменение пути, перемещение или удаление группы."
+msgid "Perform advanced options such as changing path, transferring, exporting, or removing the group."
+msgstr ""
msgid "Perform common operations on GitLab project"
msgstr ""
@@ -14717,7 +15040,7 @@ msgid "Performance optimization"
msgstr "ÐžÐ¿Ñ‚Ð¸Ð¼Ð¸Ð·Ð°Ñ†Ð¸Ñ Ð¿Ñ€Ð¾Ð¸Ð·Ð²Ð¾Ð´Ð¸Ñ‚ÐµÐ»ÑŒÐ½Ð¾Ñти"
msgid "PerformanceBar|Download"
-msgstr ""
+msgstr "Скачать"
msgid "PerformanceBar|Frontend resources"
msgstr ""
@@ -14786,10 +15109,7 @@ msgid "Pipeline Schedules"
msgstr "РаÑпиÑÐ°Ð½Ð¸Ñ Ð¡Ð±Ð¾Ñ€Ð¾Ñ‡Ð½Ñ‹Ñ… Линий"
msgid "Pipeline minutes quota"
-msgstr ""
-
-msgid "Pipeline quota"
-msgstr ""
+msgstr "Квота иÑÐ¿Ð¾Ð»ÑŒÐ·Ð¾Ð²Ð°Ð½Ð¸Ñ Ñборочных линий в минутах"
msgid "Pipeline subscriptions"
msgstr ""
@@ -14800,6 +15120,9 @@ msgstr ""
msgid "Pipeline: %{status}"
msgstr "Ð¡Ð±Ð¾Ñ€Ð¾Ñ‡Ð½Ð°Ñ Ð»Ð¸Ð½Ð¸Ñ:%{status}"
+msgid "PipelineCharts|CI / CD Analytics"
+msgstr ""
+
msgid "PipelineCharts|Failed:"
msgstr "Ðеудача:"
@@ -14878,23 +15201,14 @@ msgstr ""
msgid "Pipelines settings for '%{project_name}' were successfully updated."
msgstr "ÐаÑтройки Ñборочных линий Ð´Ð»Ñ '%{project_name}' были уÑпешно обновлены."
-msgid "Pipelines| to purchase more minutes."
-msgstr ""
-
-msgid "Pipelines|%{namespace_name} has exceeded its pipeline minutes quota."
-msgstr ""
-
-msgid "Pipelines|%{namespace_name} has less than %{notification_level}%% of CI minutes available."
-msgstr ""
-
msgid "Pipelines|API"
-msgstr ""
+msgstr "API"
msgid "Pipelines|Build with confidence"
msgstr "ВыполнÑйте Ñборки Ñ ÑƒÐ²ÐµÑ€ÐµÐ½Ð½Ð¾Ñтью"
msgid "Pipelines|CI Lint"
-msgstr ""
+msgstr "CI Lint"
msgid "Pipelines|Child pipeline"
msgstr ""
@@ -14903,17 +15217,20 @@ msgid "Pipelines|Clear Runner Caches"
msgstr "ОчиÑтить Runner кÑши "
msgid "Pipelines|Continuous Integration can help catch bugs by running your tests automatically, while Continuous Deployment can help you deliver code to your product environment."
-msgstr ""
+msgstr "ÐÐµÐ¿Ñ€ÐµÑ€Ñ‹Ð²Ð½Ð°Ñ Ð¸Ð½Ñ‚ÐµÐ³Ñ€Ð°Ñ†Ð¸Ñ Ð¼Ð¾Ð¶ÐµÑ‚ помочь выÑвить ошибки, автоматичеÑки выполнÑÑ Ñ‚ÐµÑÑ‚Ñ‹, а непрерывное развёртывание может помочь доÑтавить код в окружение вашего продукта."
msgid "Pipelines|Get started with Pipelines"
msgstr "Ðачало работы Ñо Ñборочными линиÑми"
-msgid "Pipelines|Loading Pipelines"
-msgstr "ЗагружаютÑÑ Ñборочные линии"
+msgid "Pipelines|Group %{namespace_name} has %{percentage}%% or less Shared Runner Pipeline minutes remaining. Once it runs out, no new jobs or pipelines in its projects will run."
+msgstr ""
-msgid "Pipelines|Pipelines will not run anymore on shared Runners."
+msgid "Pipelines|Group %{namespace_name} has exceeded its pipeline minutes quota. Unless you buy additional pipeline minutes, no new jobs or pipelines in its projects will run."
msgstr ""
+msgid "Pipelines|Loading Pipelines"
+msgstr "ЗагружаютÑÑ Ñборочные линии"
+
msgid "Pipelines|Project cache successfully reset."
msgstr "КÑш проекта уÑпешно очищен."
@@ -14954,7 +15271,7 @@ msgid "Pipeline|Date"
msgstr ""
msgid "Pipeline|Detached merge request pipeline"
-msgstr ""
+msgstr "Ð¡Ð±Ð¾Ñ€Ð¾Ñ‡Ð½Ð°Ñ Ð»Ð¸Ð½Ð¸Ñ Ð¾Ñ‚ÑоединившегоÑÑ Ð·Ð°Ð¿Ñ€Ð¾Ñа на ÑлиÑние"
msgid "Pipeline|Duration"
msgstr ""
@@ -14987,7 +15304,7 @@ msgid "Pipeline|Search branches"
msgstr "ПоиÑк веток"
msgid "Pipeline|Specify variable values to be used in this run. The values specified in %{settings_link} will be used by default."
-msgstr ""
+msgstr "Укажите Ð·Ð½Ð°Ñ‡ÐµÐ½Ð¸Ñ Ð¿ÐµÑ€ÐµÐ¼ÐµÐ½Ð½Ñ‹Ñ…, которые будут иÑпользоватьÑÑ Ð² Ñтом запуÑке. ЗначениÑ, указанные в %{settings_link} будут иÑпользоватьÑÑ Ð¿Ð¾ умолчанию."
msgid "Pipeline|Stages"
msgstr "Стадии"
@@ -15008,13 +15325,13 @@ msgid "Pipeline|Value"
msgstr ""
msgid "Pipeline|Variables"
-msgstr ""
+msgstr "Переменные"
msgid "Pipeline|You’re about to stop pipeline %{pipelineId}."
msgstr "Ð’Ñ‹ ÑобираетеÑÑŒ оÑтановить Ñборочную линию %{pipelineId}."
msgid "Pipeline|for"
-msgstr ""
+msgstr "длÑ"
msgid "Pipeline|on"
msgstr ""
@@ -15035,7 +15352,7 @@ msgid "PivotalTrackerService|Project Management Software (Source Commits Endpoin
msgstr ""
msgid "Plain diff"
-msgstr ""
+msgstr "ПроÑтое отличие"
msgid "PlantUML"
msgstr "PlantUML"
@@ -15142,6 +15459,9 @@ msgstr ""
msgid "Please select"
msgstr ""
+msgid "Please select a Jira project"
+msgstr ""
+
msgid "Please select a country"
msgstr ""
@@ -15187,9 +15507,12 @@ msgstr ""
msgid "Please wait while we import the repository for you. Refresh at will."
msgstr "ПожалуйÑта, подождите пока мы импортируем к ваш репозиторий. ОбновлÑйте Ñтраницу по желанию."
-msgid "Pod does not exist"
+msgid "Plugins directory is deprecated and will be removed in 14.0. Please move this file into /file_hooks directory."
msgstr ""
+msgid "Pod does not exist"
+msgstr "Под не ÑущеÑтвует"
+
msgid "Pod not found"
msgstr "Под не найден"
@@ -15215,7 +15538,7 @@ msgid "Preferences|Choose what content you want to see on a project’s overview
msgstr "Выберите, какое Ñодержимое вы хотите видеть на Ñтранице обзора проекта."
msgid "Preferences|Customize integrations with third party services."
-msgstr ""
+msgstr "ÐаÑтройте интеграцию Ñо Ñторонними ÑервиÑами."
msgid "Preferences|Customize the appearance of the application header and navigation sidebar."
msgstr "ÐаÑтройте внешний вид верхней и боковой панели навигации."
@@ -15224,16 +15547,16 @@ msgid "Preferences|Default dashboard"
msgstr "Страница по умолчанию"
msgid "Preferences|Display time in 24-hour format"
-msgstr ""
+msgstr "Отображать Ð²Ñ€ÐµÐ¼Ñ Ð² 24-чаÑовом формате"
msgid "Preferences|Enable integrated code intelligence on code views"
msgstr ""
msgid "Preferences|For example: 30 mins ago."
-msgstr ""
+msgstr "Ðапример, 30 минут назад."
msgid "Preferences|Integrations"
-msgstr ""
+msgstr "Интеграции"
msgid "Preferences|Layout width"
msgstr "Ширина макета"
@@ -15248,10 +15571,10 @@ msgid "Preferences|Project overview content"
msgstr "Обзор Ñодержимого проекта"
msgid "Preferences|Render whitespace characters in the Web IDE"
-msgstr ""
+msgstr "Отображать Ñимволы пробелов в Web IDE"
msgid "Preferences|Show whitespace changes in diffs"
-msgstr ""
+msgstr "Показывать Ð¸Ð·Ð¼ÐµÐ½ÐµÐ½Ð¸Ñ Ð¿Ñ€Ð¾Ð±ÐµÐ»Ð¾Ð² в отличиÑÑ…"
msgid "Preferences|Sourcegraph"
msgstr ""
@@ -15263,7 +15586,7 @@ msgid "Preferences|Tab width"
msgstr ""
msgid "Preferences|These settings will update how dates and times are displayed for you."
-msgstr ""
+msgstr "Эти наÑтройки обновÑÑ‚ отображение даты и времени Ð´Ð»Ñ Ð²Ð°Ñ."
msgid "Preferences|This feature is experimental and translations are not complete yet"
msgstr "Эта Ñ„ÑƒÐ½ÐºÑ†Ð¸Ñ ÑвлÑетÑÑ ÑкÑпериментальной и перевод еще не полный"
@@ -15272,19 +15595,19 @@ msgid "Preferences|This setting allows you to customize the appearance of the sy
msgstr "Эта наÑтройка позволÑет изменить внешний вид ÑинтакÑиÑа."
msgid "Preferences|This setting allows you to customize the behavior of the system layout and default views."
-msgstr "Этот параметр позволÑет наÑтроить ширину макета Ñтраницы, а так же главную Ñтраницу при открытии Ñайта."
+msgstr "Этот параметр позволÑет наÑтроить ширину макета Ñтраницы, а также главную Ñтраницу при открытии Ñайта."
msgid "Preferences|Time display"
-msgstr ""
+msgstr "Отображение времени"
msgid "Preferences|Time format"
-msgstr ""
+msgstr "Формат времени"
msgid "Preferences|Time preferences"
-msgstr ""
+msgstr "ÐаÑтройки времени"
msgid "Preferences|Use relative times"
-msgstr ""
+msgstr "ИÑпользовать отноÑительное времÑ"
msgid "Press %{key}-C to copy"
msgstr ""
@@ -15304,7 +15627,7 @@ msgstr ""
msgid "Prevent users from changing their profile name"
msgstr ""
-msgid "Prevent users from modifing merge request approvers list"
+msgid "Prevent users from modifying merge request approvers list"
msgstr ""
msgid "Prevent users from performing write operations on GitLab while performing maintenance."
@@ -15325,10 +15648,10 @@ msgstr ""
msgid "Previous Artifacts"
msgstr ""
-msgid "Previous file in diff (MRs only)"
+msgid "Previous file in diff"
msgstr ""
-msgid "Previous unresolved discussion (MRs only)"
+msgid "Previous unresolved discussion"
msgstr ""
msgid "Primary"
@@ -15359,7 +15682,10 @@ msgid "Private group(s)"
msgstr ""
msgid "Private profile"
-msgstr "Приватный профиль"
+msgstr "Закрытый профиль"
+
+msgid "Private projects Minutes cost factor"
+msgstr ""
msgid "Private projects can be created in your personal namespace with:"
msgstr "Личные проекты могут быть Ñозданы в вашем перÑональном проÑтранÑтве Ñ:"
@@ -15410,10 +15736,10 @@ msgid "ProductivityAnalytics|Merge requests"
msgstr "ЗапроÑÑ‹ на ÑлиÑние"
msgid "ProductivityAnalytics|Time to merge"
-msgstr ""
+msgstr "Ð’Ñ€ÐµÐ¼Ñ Ð½Ð° ÑлиÑние"
msgid "ProductivityAnalytics|Trendline"
-msgstr ""
+msgstr "Ð›Ð¸Ð½Ð¸Ñ Ñ‚ÐµÐ½Ð´ÐµÐ½Ñ†Ð¸Ð¸"
msgid "ProductivityAnalytics|is earlier than the given merged at after date"
msgstr ""
@@ -15425,22 +15751,22 @@ msgid "Profile Settings"
msgstr "ÐаÑтройки профилÑ"
msgid "ProfileSession|on"
-msgstr ""
+msgstr "на"
msgid "Profiles| You are about to permanently delete %{yourAccount}, and all of the issues, merge requests, and groups linked to your account. Once you confirm %{deleteAccount}, it cannot be undone or recovered."
-msgstr ""
+msgstr "Ð’Ñ‹ ÑобираетеÑÑŒ навÑегда удалить %{yourAccount}, а также вÑе запроÑÑ‹ ÑлиÑÐ½Ð¸Ñ Ð¸ группы, ÑвÑзанные Ñ Ð²Ð°ÑˆÐµÐ¹ учетной запиÑью. ПоÑле Ð¿Ð¾Ð´Ñ‚Ð²ÐµÑ€Ð¶Ð´ÐµÐ½Ð¸Ñ %{deleteAccount} его Ð½ÐµÐ»ÑŒÐ·Ñ Ð¾Ñ‚Ð¼ÐµÐ½Ð¸Ñ‚ÑŒ или воÑÑтановить."
msgid "Profiles| You are going to change the username %{currentUsernameBold} to %{newUsernameBold}. Profile and projects will be redirected to the %{newUsername} namespace but this redirect will expire once the %{currentUsername} namespace is registered by another user or group. Please update your Git repository remotes as soon as possible."
msgstr "Ð’Ñ‹ ÑобираетеÑÑŒ изменить Ð¸Ð¼Ñ Ð¿Ð¾Ð»ÑŒÐ·Ð¾Ð²Ð°Ñ‚ÐµÐ»Ñ %{currentUsernameBold} на %{newUsernameBold}. Профиль и проекты будут перенаправлены в проÑтранÑтво имен %{newUsername}, но Ñто перенаправление переÑтанет дейÑтвовать, как только проÑтранÑтво имён %{currentUsername} будет зарегиÑтрировано другим пользователем или группой. ПожалуйÑта, как можно Ñкорее, обновите указатели на Ñвои удаленные репозитории Git."
msgid "Profiles|@username"
-msgstr "@Ð˜Ð¼Ñ Ð¿Ð¾Ð»ÑŒÐ·Ð¾Ð²Ð°Ñ‚ÐµÐ»Ñ"
+msgstr "@username"
msgid "Profiles|Account scheduled for removal."
-msgstr "Ð£Ñ‡ÐµÑ‚Ð½Ð°Ñ Ð·Ð°Ð¿Ð¸ÑÑŒ запланирована к удалению."
+msgstr "Ð£Ñ‡Ñ‘Ñ‚Ð½Ð°Ñ Ð·Ð°Ð¿Ð¸ÑÑŒ запланирована к удалению."
msgid "Profiles|Activate signin with one of the following services"
-msgstr ""
+msgstr "Ðктивировать вход Ñ Ð¿Ð¾Ð¼Ð¾Ñ‰ÑŒÑŽ одного из Ñледующих ÑервиÑов"
msgid "Profiles|Active"
msgstr "ÐктивноÑÑ‚ÑŒ"
@@ -15449,13 +15775,13 @@ msgid "Profiles|Add key"
msgstr "Добавить ключ"
msgid "Profiles|Add status emoji"
-msgstr ""
+msgstr "Добавить Ñмайл к ÑтатуÑу"
msgid "Profiles|Avatar cropper"
-msgstr ""
+msgstr "Обрезать аватар"
msgid "Profiles|Avatar will be removed. Are you sure?"
-msgstr ""
+msgstr "Ðватар будет удален. Ð’Ñ‹ уверены?"
msgid "Profiles|Bio"
msgstr "О Ñебе"
@@ -15467,7 +15793,7 @@ msgid "Profiles|Changing your username can have unintended side effects."
msgstr "Изменение вашего имени Ð¿Ð¾Ð»ÑŒÐ·Ð¾Ð²Ð°Ñ‚ÐµÐ»Ñ Ð¼Ð¾Ð¶ÐµÑ‚ иметь непредвиденные побочные Ñффекты."
msgid "Profiles|Choose file..."
-msgstr ""
+msgstr "Выбрать файл..."
msgid "Profiles|Choose to show contributions of private projects on your public profile without any project, repository or organization information"
msgstr "Выберите, чтобы показывать вклад от закрытых проектов в ваш публичный профиль без какой-либо информации о проекте, репозитории или организации"
@@ -15479,16 +15805,16 @@ msgid "Profiles|Clear status"
msgstr "ОчиÑтить ÑтатуÑ"
msgid "Profiles|Click on icon to activate signin with one of the following services"
-msgstr ""
+msgstr "Ðажмите на значок, чтобы активировать вход Ñ Ð¿Ð¾Ð¼Ð¾Ñ‰ÑŒÑŽ одной из Ñледующих Ñлужб"
msgid "Profiles|Commit email"
msgstr "Email коммита"
msgid "Profiles|Connect"
-msgstr ""
+msgstr "Подключить"
msgid "Profiles|Connected Accounts"
-msgstr ""
+msgstr "Подключенные аккаунты"
msgid "Profiles|Current path: %{path}"
msgstr "Текущий путь: %{path}"
@@ -15497,7 +15823,7 @@ msgid "Profiles|Current status"
msgstr "Текущий ÑтатуÑ"
msgid "Profiles|Default notification email"
-msgstr ""
+msgstr "Ð­Ð»ÐµÐºÑ‚Ñ€Ð¾Ð½Ð½Ð°Ñ Ð¿Ð¾Ñ‡Ñ‚Ð° Ð´Ð»Ñ ÑƒÐ²ÐµÐ´Ð¾Ð¼Ð»ÐµÐ½Ð¸Ð¹ по умолчанию"
msgid "Profiles|Delete Account"
msgstr "Удалить Учетную запиÑÑŒ"
@@ -15512,10 +15838,10 @@ msgid "Profiles|Deleting an account has the following effects:"
msgstr "Удаление учетной запиÑи приведет к Ñледующим поÑледÑтвиÑм:"
msgid "Profiles|Disconnect"
-msgstr ""
+msgstr "Отключить"
msgid "Profiles|Do not show on profile"
-msgstr ""
+msgstr "Ðе показывать в профиле"
msgid "Profiles|Don't display activity-related personal information on your profiles"
msgstr "Ðе отображать информацию о перÑональной активноÑти, в вашем профиле"
@@ -15527,7 +15853,7 @@ msgid "Profiles|Enter your name, so people you know can recognize you"
msgstr "Введите ваше имÑ, чтобы люди, которых вы знаете, могли раÑпознать ваÑ"
msgid "Profiles|Expires at"
-msgstr ""
+msgstr "ИÑтекает"
msgid "Profiles|Expires:"
msgstr ""
@@ -15539,13 +15865,13 @@ msgid "Profiles|Full name"
msgstr "Полное имÑ"
msgid "Profiles|Give your individual key a title"
-msgstr ""
+msgstr "Укажите заголовок ключа"
msgid "Profiles|Impersonation"
msgstr ""
msgid "Profiles|Include private contributions on my profile"
-msgstr "Включить приватный вклад в мой профиль"
+msgstr "Включить вклад в приватные проекты в мой профиль"
msgid "Profiles|Incoming email token was successfully reset"
msgstr "Токен входÑщей Ñлектронной почты был уÑпешно Ñброшен"
@@ -15572,7 +15898,7 @@ msgid "Profiles|Location"
msgstr "МеÑтораÑположение"
msgid "Profiles|Made a private contribution"
-msgstr ""
+msgstr "Сделал личный вклад"
msgid "Profiles|Main settings"
msgstr "ОÑновные наÑтройки"
@@ -15581,7 +15907,7 @@ msgid "Profiles|No file chosen"
msgstr "Файл не выбран"
msgid "Profiles|Notification email"
-msgstr ""
+msgstr "Ð­Ð»ÐµÐºÑ‚Ñ€Ð¾Ð½Ð½Ð°Ñ Ð¿Ð¾Ñ‡Ñ‚Ð° уведомлений"
msgid "Profiles|Organization"
msgstr "ОрганизациÑ"
@@ -15593,7 +15919,7 @@ msgid "Profiles|Personal Access"
msgstr "Личный доÑтуп"
msgid "Profiles|Position and size your new avatar"
-msgstr ""
+msgstr "Положение и размер вашего нового аватара"
msgid "Profiles|Primary email"
msgstr "ОÑновной email"
@@ -15605,7 +15931,7 @@ msgid "Profiles|Profile was successfully updated"
msgstr "Профиль уÑпешно обновлен"
msgid "Profiles|Public Avatar"
-msgstr "Публичный Ðватар"
+msgstr "Публичный аватар"
msgid "Profiles|Public email"
msgstr "Публичный email"
@@ -15620,7 +15946,7 @@ msgid "Profiles|Social sign-in"
msgstr "Вход через Ñоциальные Ñети"
msgid "Profiles|Some options are unavailable for LDAP accounts"
-msgstr ""
+msgstr "Ðекоторые параметры недоÑтупны Ð´Ð»Ñ ÑƒÑ‡ÐµÑ‚Ð½Ñ‹Ñ… запиÑей LDAP"
msgid "Profiles|Static object token was successfully reset"
msgstr ""
@@ -15638,7 +15964,7 @@ msgid "Profiles|This doesn't look like a public SSH key, are you sure you want t
msgstr "Это не выглÑдит как публичный SSH-ключ, вы уверены, что вы хотите добавить его?"
msgid "Profiles|This email will be displayed on your public profile"
-msgstr "Этот Ð°Ð´Ñ€ÐµÑ Ð±ÑƒÐ´ÐµÑ‚ отображатьÑÑ Ð² вашем публичном профиле"
+msgstr "Этот email будет отображатьÑÑ Ð² вашем профиле"
msgid "Profiles|This email will be used for web based operations, such as edits and merges. %{commit_email_link_start}Learn more%{commit_email_link_end}"
msgstr "Этот Ð°Ð´Ñ€ÐµÑ Ð±ÑƒÐ´ÐµÑ‚ иÑпользоватьÑÑ Ð´Ð»Ñ Ð²ÐµÐ±-операций, таких как редактирование и ÑлиÑние. %{commit_email_link_start}Узнайте больше%{commit_email_link_end}"
@@ -15650,7 +15976,7 @@ msgid "Profiles|This information will appear on your profile"
msgstr "Эта Ð¸Ð½Ñ„Ð¾Ñ€Ð¼Ð°Ñ†Ð¸Ñ Ð±ÑƒÐ´ÐµÑ‚ отображатьÑÑ Ð² вашем профиле"
msgid "Profiles|Time settings"
-msgstr ""
+msgstr "ÐаÑтройки времени"
msgid "Profiles|Two-Factor Authentication"
msgstr "Ð”Ð²ÑƒÑ…Ñ„Ð°ÐºÑ‚Ð¾Ñ€Ð½Ð°Ñ Ð°ÑƒÑ‚ÐµÐ½Ñ‚Ð¸Ñ„Ð¸ÐºÐ°Ñ†Ð¸Ñ"
@@ -15689,7 +16015,7 @@ msgid "Profiles|What's your status?"
msgstr "Как у Ð²Ð°Ñ Ð´ÐµÐ»Ð°?"
msgid "Profiles|Who you represent or work for"
-msgstr "Кого вы предÑтавлÑете или на кого работаете"
+msgstr "Кого вы предÑтавлÑете, или на кого работаете"
msgid "Profiles|You can change your avatar here"
msgstr "Ð’Ñ‹ можете изменить Ñвой аватар здеÑÑŒ"
@@ -15698,10 +16024,10 @@ msgid "Profiles|You can change your avatar here or remove the current avatar to
msgstr "Ð’Ñ‹ можете изменить Ñвой аватар здеÑÑŒ или удалить текущий аватар, чтобы вернутьÑÑ Ðº %{gravatar_link}"
msgid "Profiles|You can set your current timezone here"
-msgstr ""
+msgstr "Ð’Ñ‹ можете уÑтановить текущий чаÑовой поÑÑ Ð·Ð´ÐµÑÑŒ"
msgid "Profiles|You can upload your avatar here"
-msgstr ""
+msgstr "Ð’Ñ‹ можете загрузить Ñвой аватар здеÑÑŒ"
msgid "Profiles|You can upload your avatar here or change it at %{gravatar_link}"
msgstr "Ð’Ñ‹ можете загрузить Ñвой аватар здеÑÑŒ или изменить его на %{gravatar_link}"
@@ -15713,10 +16039,10 @@ msgid "Profiles|You must transfer ownership or delete these groups before you ca
msgstr "Перед удалением учётной запиÑи, вам необходимо передать право Ð²Ð»Ð°Ð´ÐµÐ½Ð¸Ñ Ð¸Ð»Ð¸ удалить Ñти группы."
msgid "Profiles|Your LinkedIn profile name from linkedin.com/in/profilename"
-msgstr ""
+msgstr "Ваше Ð¸Ð¼Ñ Ð¿Ñ€Ð¾Ñ„Ð¸Ð»Ñ Ð² LinkedIn, как в linkedin.com/in/profilename"
msgid "Profiles|Your account is currently an owner in these groups:"
-msgstr "Ваша ÑƒÑ‡ÐµÑ‚Ð½Ð°Ñ Ð·Ð°Ð¿Ð¸ÑÑŒ в наÑтоÑщее Ð²Ñ€ÐµÐ¼Ñ ÑвлÑетÑÑ Ð²Ð»Ð°Ð´ÐµÐ»ÑŒÑ†ÐµÐ¼ Ñледующих групп:"
+msgstr "Ваша ÑƒÑ‡Ñ‘Ñ‚Ð½Ð°Ñ Ð·Ð°Ð¿Ð¸ÑÑŒ в наÑтоÑщее Ð²Ñ€ÐµÐ¼Ñ ÑвлÑетÑÑ Ð²Ð»Ð°Ð´ÐµÐ»ÑŒÑ†ÐµÐ¼ Ñледующих групп:"
msgid "Profiles|Your email address was automatically set based on your %{provider_label} account"
msgstr "Ваш Ð°Ð´Ñ€ÐµÑ Ñлектронной почты был автоматичеÑки уÑтановлен на оÑнове вашей учетной запиÑи %{provider_label}"
@@ -15728,7 +16054,7 @@ msgid "Profiles|Your location was automatically set based on your %{provider_lab
msgstr "Ваше меÑтоположение было автоматичеÑки уÑтановлено на оÑнове вашего %{provider_label} аккаунта"
msgid "Profiles|Your name was automatically set based on your %{provider_label} account, so people you know can recognize you"
-msgstr ""
+msgstr "Ваше Ð¸Ð¼Ñ Ð±Ñ‹Ð»Ð¾ автоматичеÑки уÑтановлено на оÑнове вашей учетной запиÑи %{provider_label}, чтобы ваши знакомые могли Ð²Ð°Ñ ÑƒÐ·Ð½Ð°Ñ‚ÑŒ"
msgid "Profiles|Your status"
msgstr "Ваш ÑтатуÑ"
@@ -15749,10 +16075,10 @@ msgid "Profiles|website.com"
msgstr "website.com"
msgid "Profiles|your account"
-msgstr "ваша ÑƒÑ‡ÐµÑ‚Ð½Ð°Ñ Ð·Ð°Ð¿Ð¸ÑÑŒ"
+msgstr "ваша ÑƒÑ‡Ñ‘Ñ‚Ð½Ð°Ñ Ð·Ð°Ð¿Ð¸ÑÑŒ"
msgid "Profile|%{job_title} at %{organization}"
-msgstr ""
+msgstr "%{job_title} в %{organization}"
msgid "Profiling - Performance bar"
msgstr "Профилирование - панель производительноÑти"
@@ -15803,7 +16129,7 @@ msgid "Project Files"
msgstr "Файлы проекта"
msgid "Project ID"
-msgstr ""
+msgstr "ID проекта"
msgid "Project URL"
msgstr "ÐÐ´Ñ€ÐµÑ ÐŸÑ€Ð¾ÐµÐºÑ‚Ð°"
@@ -15863,7 +16189,7 @@ msgid "Project name suffix"
msgstr ""
msgid "Project name suffix is a user-defined string which will be appended to the project path, and will form the Service Desk email address."
-msgstr ""
+msgstr "Ð¡ÑƒÑ„Ñ„Ð¸ÐºÑ Ð¸Ð¼ÐµÐ½Ð¸ проекта - Ñто пользовательÑÐºÐ°Ñ Ñтрока, ÐºÐ¾Ñ‚Ð¾Ñ€Ð°Ñ Ð±ÑƒÐ´ÐµÑ‚ добавлена к пути проекта, и будет формировать Ð°Ð´Ñ€ÐµÑ email в Ñлужбе поддержки"
msgid "Project order will not be saved as local storage is not available."
msgstr ""
@@ -15887,7 +16213,7 @@ msgid "Project slug"
msgstr "URL проекта"
msgid "Project uploads"
-msgstr ""
+msgstr "Загрузки проекта"
msgid "Project visibility level will be changed to match namespace rules when transferring to a group."
msgstr "При перемещении в группу уровень доÑтупа проекта будет изменен в ÑоответÑтвии Ñ Ð¿Ñ€Ð°Ð²Ð¸Ð»Ð°Ð¼Ð¸ Ñтой группы."
@@ -15950,13 +16276,13 @@ msgid "ProjectOverview|You must sign in to star a project"
msgstr ""
msgid "ProjectPage|Project ID: %{project_id}"
-msgstr ""
+msgstr "ID проекта: %{project_id}"
msgid "ProjectSelect| or group"
msgstr "или группа"
msgid "ProjectSelect|Search for project"
-msgstr ""
+msgstr "ПоиÑк проекта"
msgid "ProjectService|%{service_title}: status off"
msgstr ""
@@ -15986,10 +16312,10 @@ msgid "ProjectSettings|All discussions must be resolved"
msgstr "Ð’Ñе обÑÑƒÐ¶Ð´ÐµÐ½Ð¸Ñ Ð´Ð¾Ð»Ð¶Ð½Ñ‹ быть разрешены"
msgid "ProjectSettings|Allow users to make copies of your repository to a new project"
-msgstr ""
+msgstr "Разрешить пользователÑм Ñоздавать копии вашего Ñ€ÐµÐ¿Ð¾Ð·Ð¸Ñ‚Ð¾Ñ€Ð¸Ñ Ð² новый проект"
msgid "ProjectSettings|Allow users to request access"
-msgstr ""
+msgstr "Разрешить пользователÑм запрашивать доÑтуп"
msgid "ProjectSettings|Automatically resolve merge request diff discussions when they become outdated"
msgstr "ÐвтоматичеÑки разрешать обÑÑƒÐ¶Ð´ÐµÐ½Ð¸Ñ Ð¸Ð·Ð¼ÐµÐ½ÐµÐ½Ð¸Ð¹ по запроÑу на ÑлиÑние, когда они ÑтановÑÑ‚ÑÑ Ð½ÐµÐ°ÐºÑ‚ÑƒÐ°Ð»ÑŒÐ½Ñ‹Ð¼Ð¸"
@@ -15998,25 +16324,25 @@ msgid "ProjectSettings|Badges"
msgstr "Значки"
msgid "ProjectSettings|Build, test, and deploy your changes"
-msgstr ""
+msgstr "Сборка, теÑтирование и развертывание ваших изменений"
msgid "ProjectSettings|Choose your merge method, merge options, merge checks, and merge suggestions."
-msgstr ""
+msgstr "Выберите ваш метод, наÑтройки, проверки и Ð¿Ñ€ÐµÐ´Ð»Ð¾Ð¶ÐµÐ½Ð¸Ñ ÑлиÑниÑ."
msgid "ProjectSettings|Choose your merge method, merge options, merge checks, merge suggestions, and set up a default description template for merge requests."
-msgstr ""
+msgstr "Выберите метод ÑлиÑниÑ, параметры ÑлиÑниÑ, проверки ÑлиÑниÑ, Ð¿Ñ€ÐµÐ´Ð»Ð¾Ð¶ÐµÐ½Ð¸Ñ Ð¿Ð¾ ÑлиÑнию и наÑтройте шаблон опиÑÐ°Ð½Ð¸Ñ Ð¿Ð¾ умолчанию Ð´Ð»Ñ Ð·Ð°Ð¿Ñ€Ð¾Ñов на ÑлиÑние."
msgid "ProjectSettings|Contact an admin to change this setting."
msgstr ""
msgid "ProjectSettings|Container registry"
-msgstr ""
+msgstr "РееÑÑ‚Ñ€ контейнеров"
msgid "ProjectSettings|Customize your project badges."
msgstr "ÐаÑтройте значки вашего проекта."
msgid "ProjectSettings|Disable email notifications"
-msgstr ""
+msgstr "Отключить ÑƒÐ²ÐµÐ´Ð¾Ð¼Ð»ÐµÐ½Ð¸Ñ Ð¿Ð¾ Ñлектронной почте"
msgid "ProjectSettings|Enable 'Delete source branch' option by default"
msgstr ""
@@ -16025,16 +16351,16 @@ msgid "ProjectSettings|Every merge creates a merge commit"
msgstr "Каждый ÑлиÑние Ñоздает коммит ÑлиÑниÑ"
msgid "ProjectSettings|Every project can have its own space to store its Docker images"
-msgstr ""
+msgstr "Каждый проект может иметь Ñвоё ÑобÑтвенное проÑтранÑтво Ð´Ð»Ñ Ñ…Ñ€Ð°Ð½ÐµÐ½Ð¸Ñ Ð¾Ð±Ñ€Ð°Ð·Ð¾Ð² Docker"
msgid "ProjectSettings|Every project can have its own space to store its packages"
-msgstr ""
+msgstr "Каждый проект может иметь ÑобÑтвенное проÑтранÑтво Ð´Ð»Ñ Ñ…Ñ€Ð°Ð½ÐµÐ½Ð¸Ñ Ð¿Ð°ÐºÐµÑ‚Ð¾Ð²"
msgid "ProjectSettings|Everyone"
-msgstr ""
+msgstr "Каждый"
msgid "ProjectSettings|Existing merge requests and protected branches are not affected"
-msgstr ""
+msgstr "СущеÑтвующие запроÑÑ‹ на ÑлиÑние и защищенные ветки не затрагиваютÑÑ"
msgid "ProjectSettings|Failed to protect the tag"
msgstr "Ðе удалоÑÑŒ защитить метку"
@@ -16049,16 +16375,16 @@ msgid "ProjectSettings|Fast-forward merges only"
msgstr "Только быÑтрые ÑлиÑниÑ"
msgid "ProjectSettings|Forks"
-msgstr ""
+msgstr "ОтветвлениÑ"
msgid "ProjectSettings|Git Large File Storage"
-msgstr ""
+msgstr "Хранилище больших файлов Git"
msgid "ProjectSettings|Internal"
-msgstr ""
+msgstr "ВнутреннÑÑ"
msgid "ProjectSettings|Issues"
-msgstr ""
+msgstr "ОбÑуждениÑ"
msgid "ProjectSettings|LFS objects from this repository are still available to forks. %{linkStart}How do I remove them?%{linkEnd}"
msgstr ""
@@ -16067,10 +16393,10 @@ msgid "ProjectSettings|Learn more about badges."
msgstr "Узнать больше о значках."
msgid "ProjectSettings|Lightweight issue tracking system for this project"
-msgstr ""
+msgstr "ПроÑÑ‚Ð°Ñ ÑиÑтема обÑуждений Ð´Ð»Ñ Ñтого проекта"
msgid "ProjectSettings|Manages large files such as audio, video, and graphics files"
-msgstr ""
+msgstr "УправлÑет большими графичеÑкими, аудио- и видефайлами"
msgid "ProjectSettings|Merge checks"
msgstr "Проверки ÑлиÑниÑ"
@@ -16088,34 +16414,34 @@ msgid "ProjectSettings|Merge options"
msgstr "Параметры ÑлиÑниÑ"
msgid "ProjectSettings|Merge pipelines will try to validate the post-merge result prior to merging"
-msgstr ""
+msgstr "Перед ÑлиÑнием Ñборочные линии ÑлиÑний попытаютÑÑ Ð¿Ñ€Ð¾Ð²ÐµÑ€Ð¸Ñ‚ÑŒ результат поÑле ÑлиÑниÑ"
msgid "ProjectSettings|Merge requests"
-msgstr ""
+msgstr "ЗапроÑÑ‹ на ÑлиÑние"
msgid "ProjectSettings|Merge suggestions"
-msgstr ""
+msgstr "ÐŸÑ€ÐµÐ´Ð»Ð¾Ð¶ÐµÐ½Ð¸Ñ Ð¿Ð¾ ÑлиÑнию"
msgid "ProjectSettings|No merge commits are created"
msgstr "Коммиты ÑлиÑÐ½Ð¸Ñ Ð½Ðµ ÑоздаютÑÑ"
msgid "ProjectSettings|Note: the container registry is always visible when a project is public"
-msgstr ""
+msgstr "Примечание: рееÑÑ‚Ñ€ контейнеров вÑегда виден, еÑли проект публичный"
msgid "ProjectSettings|Only signed commits can be pushed to this repository."
msgstr ""
msgid "ProjectSettings|Packages"
-msgstr ""
+msgstr "Пакеты"
msgid "ProjectSettings|Pages"
-msgstr ""
+msgstr "Pages"
msgid "ProjectSettings|Pages for project documentation"
-msgstr ""
+msgstr "Страницы Ð´Ð»Ñ Ð´Ð¾ÐºÑƒÐ¼ÐµÐ½Ñ‚Ð°Ñ†Ð¸Ð¸ проекта"
msgid "ProjectSettings|Pipelines"
-msgstr ""
+msgstr "Сборочные линии"
msgid "ProjectSettings|Pipelines must succeed"
msgstr "Сборочные линии должны уÑпешно выполнитьÑÑ"
@@ -16124,69 +16450,72 @@ msgid "ProjectSettings|Pipelines need to be configured to enable this feature."
msgstr "Сборочные линии должны быть наÑтроены, чтобы включить Ñту функциональноÑÑ‚ÑŒ."
msgid "ProjectSettings|Private"
-msgstr ""
+msgstr "ЛичнаÑ"
msgid "ProjectSettings|Project visibility"
-msgstr ""
+msgstr "ВидимоÑÑ‚ÑŒ проекта"
msgid "ProjectSettings|Public"
-msgstr ""
+msgstr "ПубличнаÑ"
msgid "ProjectSettings|Repository"
-msgstr ""
+msgstr "Репозиторий"
msgid "ProjectSettings|Share code pastes with others out of Git repository"
-msgstr ""
+msgstr "ДелитеÑÑŒ фрагментами кода Ñ Ð´Ñ€ÑƒÐ³Ð¸Ð¼Ð¸ за пределами Git-репозиториÑ"
msgid "ProjectSettings|Show link to create/view merge request when pushing from the command line"
msgstr "Показать ÑÑылку Ð´Ð»Ñ ÑÐ¾Ð·Ð´Ð°Ð½Ð¸Ñ / проÑмотра запроÑа на ÑлиÑние при отправке из командной Ñтроки"
msgid "ProjectSettings|Snippets"
-msgstr ""
+msgstr "Сниппеты"
msgid "ProjectSettings|Submit changes to be merged upstream"
-msgstr ""
+msgstr "ОтправлÑйте изменениÑ, который могут быть Ñлиты в родительÑкий репозиторий"
msgid "ProjectSettings|The commit message used to apply merge request suggestions"
-msgstr ""
+msgstr "Сообщение коммита иÑпользуетÑÑ, чтобы применÑÑ‚ÑŒ Ð¿Ñ€ÐµÐ´Ð»Ð¾Ð¶ÐµÐ½Ð¸Ñ Ð·Ð°Ð¿Ñ€Ð¾Ñа на ÑлиÑние"
msgid "ProjectSettings|The variables GitLab supports:"
-msgstr ""
+msgstr "Переменные, которые поддерживаютÑÑ GitLab:"
msgid "ProjectSettings|These checks must pass before merge requests can be merged"
msgstr "Эти проверки должны пройти, прежде чем запроÑÑ‹ на ÑлиÑние будут объединены"
msgid "ProjectSettings|This setting is applied on the server level and can be overridden by an admin."
-msgstr ""
+msgstr "Эта наÑтройка применÑетÑÑ Ð½Ð° уровне Ñервера и может быть переопределена админиÑтратором."
msgid "ProjectSettings|This setting is applied on the server level but has been overridden for this project."
-msgstr ""
+msgstr "Эта наÑтройка применÑетÑÑ Ð½Ð° уровне Ñервера, но была переопределена Ð´Ð»Ñ Ñтого проекта."
msgid "ProjectSettings|This setting will be applied to all projects unless overridden by an admin."
-msgstr ""
+msgstr "Эта наÑтройка будет применена Ð´Ð»Ñ Ð²Ñех проектов, еÑли иное поведение не переопределено админиÑтратором."
msgid "ProjectSettings|This setting will override user notification preferences for all project members."
-msgstr ""
+msgstr "Эта наÑтройка переопределит пользовательÑкие Ð¿Ñ€ÐµÐ´Ð¿Ð¾Ñ‡Ñ‚ÐµÐ½Ð¸Ñ ÑƒÐ²ÐµÐ´Ð¾Ð¼Ð»ÐµÐ½Ð¸Ð¹ Ð´Ð»Ñ Ð²Ñех членов проекта."
msgid "ProjectSettings|This will dictate the commit history when you merge a merge request"
-msgstr ""
+msgstr "Это будет показывать иÑторию коммитов, когда вы Ñливаете Ð·Ð°Ð¿Ñ€Ð¾Ñ Ð½Ð° ÑлиÑние"
msgid "ProjectSettings|Users can only push commits to this repository that were committed with one of their own verified emails."
-msgstr ""
+msgstr "Пользователи могут отправлÑÑ‚ÑŒ коммиты в данный репозиторий, только в Ñлучае еÑли коммит подпиÑан одним из подтвержденных адреÑов почты."
msgid "ProjectSettings|View and edit files in this project"
-msgstr ""
+msgstr "ПроÑмотр и редактирование файлов в Ñтом проекте"
msgid "ProjectSettings|View and edit files in this project. Non-project members will only have read access"
msgstr ""
msgid "ProjectSettings|When conflicts arise the user is given the option to rebase"
-msgstr ""
+msgstr "Когда возникают конфликты, пользователю предоÑтавлÑетÑÑ Ð²Ð¾Ð·Ð¼Ð¾Ð¶Ð½Ð¾ÑÑ‚ÑŒ объединить изменениÑ, Ñделанные в одной ветке, Ñ Ð´Ñ€ÑƒÐ³Ð¾Ð¹ веткой"
msgid "ProjectSettings|Wiki"
-msgstr ""
+msgstr "Wiki"
msgid "ProjectSettings|With GitLab Pages you can host your static websites on GitLab"
+msgstr "С GitLab Pages вы можете размещать Ñвои ÑтатичеÑкие веб-Ñайты на GitLab"
+
+msgid "ProjectSettings|With Metrics Dashboard you can visualize this project performance metrics"
msgstr ""
msgid "ProjectTemplates|.NET Core"
@@ -16223,7 +16552,7 @@ msgid "ProjectTemplates|NodeJS Express"
msgstr "NodeJS Express"
msgid "ProjectTemplates|Pages/Gatsby"
-msgstr ""
+msgstr "Pages/Gatsby"
msgid "ProjectTemplates|Pages/GitBook"
msgstr "Pages/GitBook"
@@ -16252,6 +16581,9 @@ msgstr ""
msgid "ProjectTemplates|Spring"
msgstr "Spring"
+msgid "ProjectTemplates|Static Site Editor/Middleman"
+msgstr ""
+
msgid "ProjectTemplates|iOS (Swift)"
msgstr "iOS (Swift)"
@@ -16259,7 +16591,7 @@ msgid "Projects"
msgstr "Проекты"
msgid "Projects (%{count})"
-msgstr ""
+msgstr "Проекты (%{count})"
msgid "Projects Successfully Retrieved"
msgstr ""
@@ -16373,16 +16705,16 @@ msgid "PrometheusAlerts|%{firingCount} firing"
msgstr ""
msgid "PrometheusAlerts|Add alert"
-msgstr ""
+msgstr "Добавить оповещение"
msgid "PrometheusAlerts|Edit alert"
-msgstr ""
+msgstr "Редактировать оповещение"
msgid "PrometheusAlerts|Error creating alert"
-msgstr ""
+msgstr "Ошибка ÑÐ¾Ð·Ð´Ð°Ð½Ð¸Ñ Ð¾Ð¿Ð¾Ð²ÐµÑ‰ÐµÐ½Ð¸Ñ"
msgid "PrometheusAlerts|Error deleting alert"
-msgstr ""
+msgstr "Ошибка ÑƒÐ´Ð°Ð»ÐµÐ½Ð¸Ñ Ð¾Ð¿Ð¾Ð²ÐµÑ‰ÐµÐ½Ð¸Ñ"
msgid "PrometheusAlerts|Error fetching alert"
msgstr ""
@@ -16427,7 +16759,7 @@ msgid "PrometheusService|Common metrics are automatically monitored based on a l
msgstr ""
msgid "PrometheusService|Custom metrics"
-msgstr ""
+msgstr "ÐаÑтраиваемые метрики"
msgid "PrometheusService|Custom metrics require Prometheus installed on a cluster with environment scope \"*\" OR a manually configured Prometheus to be available."
msgstr "ПользовательÑкие метрики требуют, чтобы Prometheus был уÑтановлен в клаÑтер Ñ Ð¾Ð±Ð»Ð°Ñтью дейÑÑ‚Ð²Ð¸Ñ Ñреды \"*\" ИЛИ наÑтроенной вручную, чтобы Prometheus был доÑтупен."
@@ -16471,20 +16803,20 @@ msgstr "Базовый Ð°Ð´Ñ€ÐµÑ Prometheus API, например http://promet
msgid "PrometheusService|Prometheus is being automatically managed on your clusters"
msgstr ""
-msgid "PrometheusService|These metrics will only be monitored after your first deployment to an environment"
+msgid "PrometheusService|Select the Active checkbox to override the Auto Configuration with custom settings. If unchecked, Auto Configuration settings are used."
msgstr ""
+msgid "PrometheusService|These metrics will only be monitored after your first deployment to an environment"
+msgstr "Эти показатели будут отÑлеживатьÑÑ Ñ‚Ð¾Ð»ÑŒÐºÐ¾ поÑле вашего первого Ñ€Ð°Ð·Ð²Ñ‘Ñ€Ñ‚Ñ‹Ð²Ð°Ð½Ð¸Ñ Ð² окружении"
+
msgid "PrometheusService|Time-series monitoring service"
msgstr "Служба мониторинга временных Ñ€Ñдов"
-msgid "PrometheusService|To enable manual configuration, uninstall Prometheus from your clusters"
-msgstr ""
-
msgid "PrometheusService|To enable the installation of Prometheus on your clusters, deactivate the manual configuration below"
-msgstr ""
+msgstr "Чтобы включить уÑтановку Prometheus на ваших клаÑтерах, отключите ручную конфигурацию ниже"
msgid "PrometheusService|Waiting for your first deployment to an environment to find common metrics"
-msgstr ""
+msgstr "Ожидание вашего первого Ñ€Ð°Ð·Ð²Ñ‘Ñ€Ñ‚Ñ‹Ð²Ð°Ð½Ð¸Ñ Ð² Ñреде Ð´Ð»Ñ Ð¿Ð¾Ð¸Ñка общих показателей"
msgid "PrometheusService|You can now manage your Prometheus settings on the %{operations_link_start}Operations%{operations_link_end} page. Fields on this page has been deprecated."
msgstr ""
@@ -16529,7 +16861,7 @@ msgid "Promotions|Epics let you manage your portfolio of projects more efficient
msgstr ""
msgid "Promotions|Learn more"
-msgstr ""
+msgstr "Узнать больше"
msgid "Promotions|See the other features in the %{subscription_link_start}bronze plan%{subscription_link_end}"
msgstr ""
@@ -16541,13 +16873,13 @@ msgid "Promotions|Upgrade plan"
msgstr ""
msgid "Promotions|Weighting your issue"
-msgstr ""
+msgstr "Обозначить приоритет обÑуждениÑ"
msgid "Promotions|When you have a lot of issues, it can be hard to get an overview. By adding a weight to your issues, you can get a better idea of the effort, cost, required time, or value of each, and so better manage them."
-msgstr ""
+msgstr "При большом количеÑтве обÑуждений Ñложно оценить общую картину. Добавив приоритет к вашим обÑуждениÑм, вы можете получить лучшее предÑтавление о затрачиваемых уÑилиÑÑ…, времени, ÑтоимоÑти или значимоÑти каждого из них и, таким образом, лучше ими управлÑÑ‚ÑŒ."
msgid "Prompt users to upload SSH keys"
-msgstr ""
+msgstr "Предлагать пользователÑм загружать ключи SSH"
msgid "Protect variable"
msgstr ""
@@ -16583,16 +16915,16 @@ msgid "ProtectedBranch|Allowed to merge:"
msgstr "Разрешено ÑлиÑние:"
msgid "ProtectedBranch|Allowed to push"
-msgstr ""
+msgstr "Отправка разрешена"
msgid "ProtectedBranch|Allowed to push:"
-msgstr ""
+msgstr "Отправка разрешена:"
msgid "ProtectedBranch|Branch"
msgstr "Ветка"
msgid "ProtectedBranch|Code owner approval"
-msgstr ""
+msgstr "Одобрение владельцем кода"
msgid "ProtectedBranch|Protect"
msgstr "Защитить"
@@ -16604,16 +16936,16 @@ msgid "ProtectedBranch|Protected branch (%{protected_branches_count})"
msgstr ""
msgid "ProtectedBranch|Pushes that change filenames matched by the CODEOWNERS file will be rejected"
-msgstr ""
+msgstr "Отправки, менÑющие имена файлов, ÑоответÑтвующих файлу CODEOWNERS будут отклонены"
msgid "ProtectedBranch|Require approval from code owners:"
-msgstr ""
+msgstr "ТребуетÑÑ Ñ€Ð°Ð·Ñ€ÐµÑˆÐµÐ½Ð¸Ðµ от владельцев кода:"
msgid "ProtectedBranch|There are currently no protected branches, protect a branch with the form above."
-msgstr ""
+msgstr "Ðа данный момент нет защищённых веток, защитите ветку Ñ Ð¿Ð¾Ð¼Ð¾Ñ‰ÑŒÑŽ формы Ñверху."
msgid "ProtectedBranch|Toggle code owner approval"
-msgstr ""
+msgstr "Переключить утверждение владельцами кода"
msgid "ProtectedEnvironment|%{environment_name} will be writable for developers. Are you sure?"
msgstr ""
@@ -16625,7 +16957,7 @@ msgid "ProtectedEnvironment|Environment"
msgstr ""
msgid "ProtectedEnvironment|Protect"
-msgstr ""
+msgstr "Защитить"
msgid "ProtectedEnvironment|Protect an environment"
msgstr ""
@@ -16634,7 +16966,7 @@ msgid "ProtectedEnvironment|Protected Environment (%{protected_environments_coun
msgstr ""
msgid "ProtectedEnvironment|Protecting an environment restricts the users who can execute deployments."
-msgstr "Защищенное окружение ограничивает пользователей, которые могут выполнÑÑ‚ÑŒ развертывание."
+msgstr "Защищённое окружение ограничивает пользователей, которые могут выполнÑÑ‚ÑŒ развёртываниÑ."
msgid "ProtectedEnvironment|Select an environment"
msgstr ""
@@ -16663,6 +16995,9 @@ msgstr ""
msgid "Protip:"
msgstr "ПодÑказка:"
+msgid "Protocol"
+msgstr ""
+
msgid "Provider"
msgstr "Провайдер"
@@ -16687,6 +17022,9 @@ msgstr "Публичные ключи Ñ€Ð°Ð·Ð²ÐµÑ€Ñ‚Ñ‹Ð²Ð°Ð½Ð¸Ñ (%{deploy_keys_
msgid "Public pipelines"
msgstr ""
+msgid "Public projects Minutes cost factor"
+msgstr ""
+
msgid "Pull"
msgstr "Получить"
@@ -16727,7 +17065,7 @@ msgid "Push to create a project"
msgstr "ИÑпользуйте push, чтобы Ñоздать проект"
msgid "PushRule|Committer restriction"
-msgstr ""
+msgstr "Ограничение автора коммита"
msgid "Pushed"
msgstr ""
@@ -16772,7 +17110,7 @@ msgid "PushoverService|Your application key"
msgstr "Ключ приложениÑ"
msgid "PushoverService|Your user key"
-msgstr ""
+msgstr "Ваш пользовательÑкий ключ"
msgid "Quarters"
msgstr ""
@@ -16802,7 +17140,7 @@ msgid "Rake Tasks Help"
msgstr "Помощь по Rake задачам"
msgid "Raw blob request rate limit per minute"
-msgstr ""
+msgstr "Ограничение количеÑтва запроÑов на бинарные данные в минуту"
msgid "Re-authentication period expired or never requested. Please try again"
msgstr ""
@@ -16841,7 +17179,7 @@ msgid "Receive alerts on GitLab from any source"
msgstr ""
msgid "Receive notifications about your own activity"
-msgstr ""
+msgstr "Получать ÑƒÐ²ÐµÐ´Ð¾Ð¼Ð»ÐµÐ½Ð¸Ñ Ð¾ Ñвоей деÑтельноÑти"
msgid "Recent"
msgstr "ПоÑледние"
@@ -16871,10 +17209,10 @@ msgid "Redirect to SAML provider to test configuration"
msgstr ""
msgid "Reduce project visibility"
-msgstr ""
+msgstr "Уменьшить облаÑÑ‚ÑŒ видимоÑти проекта"
msgid "Reduce this project’s visibility?"
-msgstr ""
+msgstr "Уменьшить облаÑÑ‚ÑŒ видимоÑти Ñтого проекта?"
msgid "Reference:"
msgstr "СÑылка:"
@@ -16889,6 +17227,9 @@ msgstr[1] ""
msgstr[2] ""
msgstr[3] ""
+msgid "Regenerate export"
+msgstr ""
+
msgid "Regenerate instance ID"
msgstr ""
@@ -16984,7 +17325,7 @@ msgid "Releases"
msgstr "Релизы"
msgid "Releases are based on Git tags and mark specific points in a project's development history. They can contain information about the type of changes and can also deliver binaries, like compiled versions of your software."
-msgstr ""
+msgstr "Релизы базируютÑÑ Ð½Ð° тегах Git и отмечают конкретные Ð¿Ñ€Ð¾Ð´Ð²Ð¸Ð¶ÐµÐ½Ð¸Ñ Ð² иÑтории разработки проекта. Они Ñодержат информацию про изменениÑ, а также могут предоÑтавлÑÑ‚ÑŒ артефакты проекта - Ñкомпилированные верÑии ПО, например."
msgid "Releases are based on Git tags. We recommend tags that use semantic versioning, for example %{codeStart}v1.0%{codeEnd}, %{codeStart}v2.0-pre%{codeEnd}."
msgstr ""
@@ -17050,10 +17391,10 @@ msgid "Remove card"
msgstr "Удалить карту"
msgid "Remove child epic from an epic"
-msgstr ""
+msgstr "Удалить дочернюю цель у текущей"
msgid "Remove description history"
-msgstr ""
+msgstr "Удалить иÑторию опиÑаний"
msgid "Remove due date"
msgstr "Убрать Ñрок выполнениÑ"
@@ -17065,7 +17406,7 @@ msgid "Remove from board"
msgstr "Удалить из доÑки"
msgid "Remove from epic"
-msgstr ""
+msgstr "Удалить из цели"
msgid "Remove group"
msgstr ""
@@ -17080,7 +17421,7 @@ msgid "Remove node"
msgstr ""
msgid "Remove parent epic from an epic"
-msgstr ""
+msgstr "Удалить родительÑкую цель"
msgid "Remove primary node"
msgstr ""
@@ -17110,7 +17451,7 @@ msgid "Removed %{assignee_text} %{assignee_references}."
msgstr ""
msgid "Removed %{epic_ref} from child epics."
-msgstr ""
+msgstr "Из дочерних целей удалена %{epic_ref}"
msgid "Removed %{label_references} %{label_text}."
msgstr ""
@@ -17125,13 +17466,13 @@ msgid "Removed all labels."
msgstr "Ð’Ñе метки удалены."
msgid "Removed an issue from an epic."
-msgstr ""
+msgstr "ОбÑуждение удалено из цели."
msgid "Removed group can not be restored!"
msgstr ""
msgid "Removed parent epic %{epic_ref}."
-msgstr ""
+msgstr "Удалена родительÑÐºÐ°Ñ Ñ†ÐµÐ»ÑŒ %{epic_ref}."
msgid "Removed projects cannot be restored!"
msgstr "Удаленные проекты не могут быть воÑÑтановлены!"
@@ -17149,7 +17490,7 @@ msgid "Removes %{assignee_text} %{assignee_references}."
msgstr ""
msgid "Removes %{epic_ref} from child epics."
-msgstr ""
+msgstr "Удалить %{epic_ref} у дочерних целей."
msgid "Removes %{label_references} %{label_text}."
msgstr ""
@@ -17161,10 +17502,10 @@ msgid "Removes all labels."
msgstr "УдалÑет вÑе метки."
msgid "Removes an issue from an epic."
-msgstr ""
+msgstr "УдалÑет обÑуждение из цели."
msgid "Removes parent epic %{epic_ref}."
-msgstr ""
+msgstr "УдалÑет родительÑкую цель %{epic_ref}."
msgid "Removes spent time."
msgstr ""
@@ -17203,7 +17544,7 @@ msgid "Reopen"
msgstr ""
msgid "Reopen epic"
-msgstr ""
+msgstr "Открыть цель повторно"
msgid "Reopen milestone"
msgstr "Вновь открыть Ñтап"
@@ -17268,9 +17609,31 @@ msgstr ""
msgid "Reports|%{combinedString} and %{resolvedString}"
msgstr ""
+msgid "Reports|Accessibility scanning detected %d issue for the source branch only"
+msgid_plural "Reports|Accessibility scanning detected %d issues for the source branch only"
+msgstr[0] ""
+msgstr[1] ""
+msgstr[2] ""
+msgstr[3] ""
+
+msgid "Reports|Accessibility scanning detected no issues for the source branch only"
+msgstr ""
+
+msgid "Reports|Accessibility scanning failed loading results"
+msgstr ""
+
+msgid "Reports|Accessibility scanning results are being parsed"
+msgstr ""
+
msgid "Reports|Actions"
msgstr ""
+msgid "Reports|An error occured while loading report"
+msgstr ""
+
+msgid "Reports|An error occurred while loading %{name} results"
+msgstr ""
+
msgid "Reports|Class"
msgstr ""
@@ -17281,7 +17644,7 @@ msgid "Reports|Execution time"
msgstr ""
msgid "Reports|Failure"
-msgstr ""
+msgstr "Отказ"
msgid "Reports|Metrics reports are loading"
msgstr ""
@@ -17347,13 +17710,13 @@ msgid "Repository maintenance"
msgstr ""
msgid "Repository mirroring"
-msgstr ""
+msgstr "Зеркалирование репозиториÑ"
msgid "Repository static objects"
msgstr ""
msgid "Repository storage"
-msgstr ""
+msgstr "Хранилище репозиториÑ"
msgid "Repository sync capacity"
msgstr ""
@@ -17376,7 +17739,7 @@ msgstr ""
msgid "Requested %{time_ago}"
msgstr "Запрошено %{time_ago}"
-msgid "Requested design version does not exist"
+msgid "Requested design version does not exist."
msgstr ""
msgid "Requested states are invalid"
@@ -17401,31 +17764,31 @@ msgid "Require users to prove ownership of custom domains"
msgstr ""
msgid "Requirement"
-msgstr ""
+msgstr "Требование"
msgid "Requirement %{reference} has been added"
-msgstr ""
+msgstr "Требование %{reference} было добавлено"
msgid "Requirement %{reference} has been archived"
-msgstr ""
+msgstr "Требование %{reference} было архивировано"
msgid "Requirement %{reference} has been reopened"
-msgstr ""
+msgstr "Требование %{reference} было открыто повторно"
msgid "Requirement %{reference} has been updated"
-msgstr ""
+msgstr "Требование %{reference} было обновлено"
msgid "Requirement title cannot have more than %{limit} characters."
-msgstr ""
+msgstr "Заголовок Ñ‚Ñ€ÐµÐ±Ð¾Ð²Ð°Ð½Ð¸Ñ Ð½Ðµ может Ñодержать больше %{limit} Ñимволов."
msgid "Requirements"
-msgstr ""
+msgstr "ТребованиÑ"
msgid "Requirements allow you to create criteria to check your products against."
-msgstr ""
+msgstr "Ð¢Ñ€ÐµÐ±Ð¾Ð²Ð°Ð½Ð¸Ñ Ð¿Ð¾Ð·Ð²Ð¾Ð»ÑÑŽÑ‚ вам Ñоздавать критерии, чтобы проверÑÑ‚ÑŒ Ñвои продукты по ним."
msgid "Requirements can be based on users, stakeholders, system, software or anything else you find important to capture."
-msgstr ""
+msgstr "Ð¢Ñ€ÐµÐ±Ð¾Ð²Ð°Ð½Ð¸Ñ Ð¼Ð¾Ð³ÑƒÑ‚ быть оÑнованы на пользователÑÑ…, заинтереÑованных Ñторонах, ПО или на чём угодно, что вы Ñчитаете важным отÑлеживать."
msgid "Requires approval from %{names}."
msgid_plural "Requires %{count} more approvals from %{names}."
@@ -17442,7 +17805,7 @@ msgstr[2] "Ðеобходимо %d подтверждений."
msgstr[3] "Ðеобходимо %d подтверждений."
msgid "Requires values to meet regular expression requirements."
-msgstr ""
+msgstr "Требует, чтобы Ð·Ð½Ð°Ñ‡ÐµÐ½Ð¸Ñ ÑоответÑтвовали регулÑрному выражению."
msgid "Resend confirmation email"
msgstr "Повторно отправить подтверждение по Ñлектронной почте"
@@ -17523,7 +17886,7 @@ msgid "Response metrics (HA Proxy)"
msgstr "Метрики откликов (HA Proxy)"
msgid "Response metrics (NGINX Ingress VTS)"
-msgstr ""
+msgstr "Метрики ответов (NGINX Ingress VTS)"
msgid "Response metrics (NGINX Ingress)"
msgstr "Метрики откликов (NGINX Ingress)"
@@ -17564,6 +17927,9 @@ msgstr "Возобновить репликацию"
msgid "Resync"
msgstr ""
+msgid "Resync all"
+msgstr ""
+
msgid "Resync all %{replicableType}"
msgstr ""
@@ -17634,6 +18000,9 @@ msgstr ""
msgid "Revoked personal access token %{personal_access_token_name}!"
msgstr ""
+msgid "Revoked project access token %{project_access_token_name}!"
+msgstr ""
+
msgid "RightSidebar|adding a"
msgstr ""
@@ -17682,6 +18051,9 @@ msgstr ""
msgid "Runner token"
msgstr "Runner токен"
+msgid "Runner tokens"
+msgstr ""
+
msgid "Runner was not updated."
msgstr "Обработчик заданий не был обновлен."
@@ -17719,7 +18091,7 @@ msgid "Runners page."
msgstr ""
msgid "Runners|You have used %{quotaUsed} out of %{quotaLimit} of your shared Runners pipeline minutes."
-msgstr ""
+msgstr "Ð’Ñ‹ иÑпользовали %{quotaUsed} из %{quotaLimit} ваших минут Ñборки на общих Runner'ах."
msgid "Running"
msgstr "ВыполнÑетÑÑ"
@@ -17736,6 +18108,9 @@ msgstr ""
msgid "SAML SSO for %{group_name}"
msgstr ""
+msgid "SAML discovery tokens"
+msgstr ""
+
msgid "SAML for %{group_name}"
msgstr "SAML Ð´Ð»Ñ %{group_name}"
@@ -17743,25 +18118,25 @@ msgid "SHA256"
msgstr ""
msgid "SSH Key"
-msgstr "SSH ключ"
+msgstr "Ключ SSH"
msgid "SSH Keys"
-msgstr "SSH-ключи"
+msgstr "Ключи SSH"
msgid "SSH Keys Help"
msgstr "Справка по ключам SSH"
msgid "SSH host key fingerprints"
-msgstr ""
+msgstr "Отпечатки ключа SSH хоÑта"
msgid "SSH host keys"
-msgstr ""
+msgstr "Ключи SSH хоÑта"
msgid "SSH host keys are not available on this system. Please use <code>ssh-keyscan</code> command or contact your GitLab administrator for more information."
-msgstr ""
+msgstr "Ключи SSH хоÑта недоÑтупны в Ñтой ÑиÑтеме. ПожалуйÑта, иÑпользуйте команду <code>ssh-keyscan</code> или ÑвÑжитеÑÑŒ Ñо Ñвоим админиÑтратором GitLab Ð´Ð»Ñ Ð¿Ð¾Ð»ÑƒÑ‡ÐµÐ½Ð¸Ñ Ð´Ð¾Ð¿Ð¾Ð»Ð½Ð¸Ñ‚ÐµÐ»ÑŒÐ½Ð¾Ð¹ информации."
msgid "SSH keys allow you to establish a secure connection between your computer and GitLab."
-msgstr "SSH ключи позволÑÑŽÑ‚ уÑтановить безопаÑное Ñоединение между вашим компьютером и GitLab."
+msgstr "Ключи SSH позволÑÑŽÑ‚ уÑтановить безопаÑное Ñоединение между вашим компьютером и GitLab."
msgid "SSH public key"
msgstr "Открытый ключ SSH"
@@ -17820,12 +18195,18 @@ msgstr "РаÑпиÑание новой Ñборочной линии"
msgid "Scheduled"
msgstr "Запланировано"
+msgid "Scheduled to merge this merge request (%{strategy})."
+msgstr ""
+
msgid "Scheduled to merge this merge request when the pipeline succeeds."
msgstr ""
msgid "Schedules"
msgstr "РаÑпиÑаниÑ"
+msgid "Schedules to merge this merge request (%{strategy})."
+msgstr ""
+
msgid "Scheduling"
msgstr ""
@@ -17883,6 +18264,9 @@ msgstr ""
msgid "Search branches and tags"
msgstr "Ðайти ветки и теги"
+msgid "Search by author"
+msgstr ""
+
msgid "Search files"
msgstr ""
@@ -17944,28 +18328,28 @@ msgid "Search your projects"
msgstr ""
msgid "SearchAutocomplete|All GitLab"
-msgstr ""
+msgstr "Ð’Ñе GitLab"
msgid "SearchAutocomplete|Issues I've created"
-msgstr ""
+msgstr "ОбÑуждениÑ, которые Ñ Ñоздал"
msgid "SearchAutocomplete|Issues assigned to me"
-msgstr ""
+msgstr "ОбÑуждениÑ, на которые Ñ Ð½Ð°Ð·Ð½Ð°Ñ‡ÐµÐ½"
msgid "SearchAutocomplete|Merge requests I've created"
-msgstr ""
+msgstr "ЗапроÑÑ‹ на ÑлиÑние, которые Ñ Ñоздал"
msgid "SearchAutocomplete|Merge requests assigned to me"
-msgstr ""
+msgstr "ЗапроÑÑ‹ на ÑлиÑниÑ, на которые Ñ Ð½Ð°Ð·Ð½Ð°Ñ‡ÐµÐ½"
msgid "SearchAutocomplete|in all GitLab"
-msgstr ""
+msgstr "во вÑех GitLab"
msgid "SearchAutocomplete|in this group"
-msgstr ""
+msgstr "в Ñтой группе"
msgid "SearchAutocomplete|in this project"
-msgstr ""
+msgstr "в Ñтом проекте"
msgid "SearchCodeResults|in"
msgstr "в"
@@ -17986,7 +18370,7 @@ msgid "SearchResults|Showing %{from} - %{to} of %{count} %{scope} for%{term_elem
msgstr ""
msgid "SearchResults|We couldn't find any %{scope} matching %{term}"
-msgstr ""
+msgstr "Мы не Ñмогли найти никаких %{scope}, ÑоответÑтвующих %{term}"
msgid "SearchResults|code result"
msgid_plural "SearchResults|code results"
@@ -18044,13 +18428,6 @@ msgstr[1] ""
msgstr[2] ""
msgstr[3] ""
-msgid "SearchResults|snippet result"
-msgid_plural "SearchResults|snippet results"
-msgstr[0] ""
-msgstr[1] ""
-msgstr[2] ""
-msgstr[3] ""
-
msgid "SearchResults|user"
msgid_plural "SearchResults|users"
msgstr[0] ""
@@ -18065,6 +18442,9 @@ msgstr[1] "Результата Wiki"
msgstr[2] "Результатов Wiki"
msgstr[3] "Результат Wiki"
+msgid "Searching by both author and message is currently not supported."
+msgstr ""
+
msgid "Seat Link"
msgstr ""
@@ -18081,196 +18461,244 @@ msgid "Secret"
msgstr ""
msgid "Security"
-msgstr ""
+msgstr "БезопаÑноÑÑ‚ÑŒ"
msgid "Security & Compliance"
-msgstr ""
+msgstr "БезопаÑноÑÑ‚ÑŒ и комплаенÑ"
msgid "Security Configuration"
msgstr ""
msgid "Security Dashboard"
+msgstr "Панель безопаÑноÑти"
+
+msgid "Security configuration help link"
+msgstr ""
+
+msgid "Security dashboard"
+msgstr "Панель безопаÑноÑти"
+
+msgid "Security report is out of date. Please update your branch with the latest changes from the target branch (%{targetBranchName})"
+msgstr ""
+
+msgid "Security report is out of date. Run %{newPipelineLinkStart}a new pipeline%{newPipelineLinkEnd} for the target branch (%{targetBranchName})"
+msgstr ""
+
+msgid "SecurityConfiguration|Configured"
+msgstr "ÐаÑтроено"
+
+msgid "SecurityConfiguration|Feature"
+msgstr "ФункциÑ"
+
+msgid "SecurityConfiguration|Feature documentation for %{featureName}"
msgstr ""
-msgid "Security Dashboard|Error fetching the vulnerability counts. Please check your network connection and try again."
+msgid "SecurityConfiguration|Not yet configured"
+msgstr "Пока не наÑтроено"
+
+msgid "SecurityConfiguration|Secure features"
+msgstr "Функции безопаÑноÑти"
+
+msgid "SecurityConfiguration|Status"
+msgstr "СтатуÑ"
+
+msgid "SecurityReports|%{firstProject} and %{secondProject}"
msgstr ""
-msgid "Security Dashboard|Error fetching the vulnerability list. Please check your network connection and try again."
+msgid "SecurityReports|%{firstProject}, %{secondProject}, and %{rest}"
msgstr ""
-msgid "Security Dashboard|Issue Created"
+msgid "SecurityReports|Add a project to your dashboard"
msgstr ""
-msgid "Security Reports|Comment added to '%{vulnerabilityName}'"
+msgid "SecurityReports|Add or remove projects from your dashboard"
msgstr ""
-msgid "Security Reports|Comment deleted on '%{vulnerabilityName}'"
+msgid "SecurityReports|Add projects"
msgstr ""
-msgid "Security Reports|Comment edited on '%{vulnerabilityName}'"
+msgid "SecurityReports|Comment added to '%{vulnerabilityName}'"
msgstr ""
-msgid "Security Reports|Create issue"
+msgid "SecurityReports|Comment deleted on '%{vulnerabilityName}'"
msgstr ""
-msgid "Security Reports|Dismiss vulnerability"
+msgid "SecurityReports|Comment edited on '%{vulnerabilityName}'"
msgstr ""
-msgid "Security Reports|Dismissed '%{vulnerabilityName}'"
+msgid "SecurityReports|Create issue"
msgstr ""
-msgid "Security Reports|Dismissed '%{vulnerabilityName}'. Turn off the hide dismissed toggle to view."
+msgid "SecurityReports|Dismiss Selected"
msgstr ""
-msgid "Security Reports|Either you don't have permission to view this dashboard or the dashboard has not been setup. Please check your permission settings with your administrator or check your dashboard configurations to proceed."
+msgid "SecurityReports|Dismiss vulnerability"
msgstr ""
-msgid "Security Reports|Learn more about setting up your dashboard"
-msgstr "Подробнее о наÑтройке панели управлениÑ"
+msgid "SecurityReports|Dismissed '%{vulnerabilityName}'"
+msgstr ""
-msgid "Security Reports|More info"
+msgid "SecurityReports|Dismissed '%{vulnerabilityName}'. Turn off the hide dismissed toggle to view."
msgstr ""
-msgid "Security Reports|Oops, something doesn't seem right."
+msgid "SecurityReports|Each vulnerability now has a unique page that can be directly linked to, shared, referenced, and tracked as the single source of truth. Vulnerability occurrences also persist across scanner runs, which improves tracking and visibility and reduces duplicates between scans."
msgstr ""
-msgid "Security Reports|Security reports can only be accessed by authorized users."
+msgid "SecurityReports|Edit dashboard"
msgstr ""
-msgid "Security Reports|There was an error adding the comment."
+msgid "SecurityReports|Either you don't have permission to view this dashboard or the dashboard has not been setup. Please check your permission settings with your administrator or check your dashboard configurations to proceed."
msgstr ""
-msgid "Security Reports|There was an error creating the issue."
+msgid "SecurityReports|Error fetching the vulnerability counts. Please check your network connection and try again."
msgstr ""
-msgid "Security Reports|There was an error creating the merge request."
-msgstr "Произошла ошибка при Ñоздании запроÑа на ÑлиÑние."
+msgid "SecurityReports|Error fetching the vulnerability list. Please check your network connection and try again."
+msgstr ""
-msgid "Security Reports|There was an error deleting the comment."
+msgid "SecurityReports|False positive"
msgstr ""
-msgid "Security Reports|There was an error dismissing the vulnerabilities."
+msgid "SecurityReports|Hide dismissed"
msgstr ""
-msgid "Security Reports|There was an error dismissing the vulnerability."
+msgid "SecurityReports|Introducing standalone vulnerabilities"
msgstr ""
-msgid "Security Reports|There was an error reverting the dismissal."
+msgid "SecurityReports|Issue Created"
msgstr ""
-msgid "Security Reports|There was an error reverting this dismissal."
+msgid "SecurityReports|Learn More"
msgstr ""
-msgid "Security Reports|Undo dismiss"
+msgid "SecurityReports|Learn more about setting up your dashboard"
msgstr ""
-msgid "Security Reports|You do not have sufficient permissions to access this report"
+msgid "SecurityReports|Load more vulnerabilities"
msgstr ""
-msgid "Security Reports|You must sign in as an authorized user to see this report"
+msgid "SecurityReports|Monitor vulnerabilities in your code"
msgstr ""
-msgid "Security configuration help link"
+msgid "SecurityReports|More info"
msgstr ""
-msgid "Security dashboard"
-msgstr "Панель безопаÑноÑти"
+msgid "SecurityReports|More information"
+msgstr ""
-msgid "Security report is out of date. Please update your branch with the latest changes from the target branch (%{targetBranchName})"
+msgid "SecurityReports|No vulnerabilities found for dashboard"
msgstr ""
-msgid "Security report is out of date. Run %{newPipelineLinkStart}a new pipeline%{newPipelineLinkEnd} for the target branch (%{targetBranchName})"
+msgid "SecurityReports|No vulnerabilities found for this group"
msgstr ""
-msgid "SecurityConfiguration|Configured"
+msgid "SecurityReports|No vulnerabilities found for this pipeline"
msgstr ""
-msgid "SecurityConfiguration|Feature"
+msgid "SecurityReports|No vulnerabilities found for this project"
msgstr ""
-msgid "SecurityConfiguration|Feature documentation for %{featureName}"
+msgid "SecurityReports|Oops, something doesn't seem right."
msgstr ""
-msgid "SecurityConfiguration|Not yet configured"
+msgid "SecurityReports|Pipeline %{pipelineLink} triggered %{timeago} by %{user}"
msgstr ""
-msgid "SecurityConfiguration|Secure features"
+msgid "SecurityReports|Project"
msgstr ""
-msgid "SecurityConfiguration|Status"
+msgid "SecurityReports|Projects added"
msgstr ""
-msgid "SecurityDashboard|%{firstProject} and %{secondProject}"
-msgstr "%{firstProject} и %{secondProject}"
+msgid "SecurityReports|Remove project from dashboard"
+msgstr ""
-msgid "SecurityDashboard|%{firstProject}, %{secondProject}, and %{rest}"
-msgstr "%{firstProject}, %{secondProject}, и %{rest}"
+msgid "SecurityReports|Report type"
+msgstr ""
-msgid "SecurityDashboard|Add a project to your dashboard"
-msgstr "Добавить проект на вашу панель управлениÑ"
+msgid "SecurityReports|Return to dashboard"
+msgstr ""
-msgid "SecurityDashboard|Add or remove projects from your dashboard"
-msgstr "Добавить или удалить проекты из панели управлениÑ"
+msgid "SecurityReports|Security Dashboard"
+msgstr ""
-msgid "SecurityDashboard|Add projects"
-msgstr "Добавить проекты"
+msgid "SecurityReports|Security reports can only be accessed by authorized users."
+msgstr ""
-msgid "SecurityDashboard|Edit dashboard"
-msgstr "Редактировать панель управлениÑ"
+msgid "SecurityReports|Select a project to add by using the project search field above."
+msgstr ""
-msgid "SecurityDashboard|Hide dismissed"
+msgid "SecurityReports|Select a reason"
msgstr ""
-msgid "SecurityDashboard|Monitor vulnerabilities in your code"
+msgid "SecurityReports|Severity"
msgstr ""
-msgid "SecurityDashboard|More information"
-msgstr "Подробнее"
+msgid "SecurityReports|Status"
+msgstr ""
-msgid "SecurityDashboard|No vulnerabilities found for dashboard"
+msgid "SecurityReports|The security dashboard displays the latest security findings for projects you wish to monitor. Select \"Edit dashboard\" to add and remove projects."
msgstr ""
-msgid "SecurityDashboard|Pipeline %{pipelineLink} triggered %{timeago} by %{user}"
+msgid "SecurityReports|The security dashboard displays the latest security report. Use it to find and fix vulnerabilities."
msgstr ""
-msgid "SecurityDashboard|Project"
-msgstr "Проект"
+msgid "SecurityReports|There was an error adding the comment."
+msgstr ""
-msgid "SecurityDashboard|Projects added"
-msgstr "Добавлены проекты"
+msgid "SecurityReports|There was an error creating the issue."
+msgstr ""
-msgid "SecurityDashboard|Remove project from dashboard"
-msgstr "Удалить проект из панели управлениÑ"
+msgid "SecurityReports|There was an error creating the merge request."
+msgstr ""
-msgid "SecurityDashboard|Report type"
-msgstr "Тип отчета"
+msgid "SecurityReports|There was an error deleting the comment."
+msgstr ""
-msgid "SecurityDashboard|Return to dashboard"
-msgstr "ВернутьÑÑ Ðº панели управлениÑ"
+msgid "SecurityReports|There was an error dismissing the vulnerabilities."
+msgstr ""
-msgid "SecurityDashboard|Security Dashboard"
-msgstr "Панель безопаÑноÑти"
+msgid "SecurityReports|There was an error dismissing the vulnerability."
+msgstr ""
+
+msgid "SecurityReports|There was an error reverting the dismissal."
+msgstr ""
+
+msgid "SecurityReports|There was an error reverting this dismissal."
+msgstr ""
+
+msgid "SecurityReports|There was an error while generating the report."
+msgstr ""
-msgid "SecurityDashboard|Select a project to add by using the project search field above."
-msgstr "Выберите проект, чтобы добавить, иÑÐ¿Ð¾Ð»ÑŒÐ·ÑƒÑ Ð¿Ð¾Ð»Ðµ поиÑка проекта выше."
+msgid "SecurityReports|Unable to add %{invalidProjects}"
+msgstr ""
-msgid "SecurityDashboard|Severity"
+msgid "SecurityReports|Undo dismiss"
msgstr ""
-msgid "SecurityDashboard|Status"
+msgid "SecurityReports|While it's rare to have no vulnerabilities for your group, it can happen. In any event, we ask that you double check your settings to make sure you've set up your dashboard correctly."
msgstr ""
-msgid "SecurityDashboard|The security dashboard displays the latest security findings for projects you wish to monitor. Select \"Edit dashboard\" to add and remove projects."
-msgstr "Панель безопаÑноÑти отображает поÑледние выводы по безопаÑноÑти Ð´Ð»Ñ Ð¿Ñ€Ð¾ÐµÐºÑ‚Ð¾Ð², которые вы хотите отÑлеживать. Выберите \"Редактировать панель\" Ð´Ð»Ñ Ð´Ð¾Ð±Ð°Ð²Ð»ÐµÐ½Ð¸Ñ Ð¸ ÑƒÐ´Ð°Ð»ÐµÐ½Ð¸Ñ Ð¿Ñ€Ð¾ÐµÐºÑ‚Ð¾Ð²."
+msgid "SecurityReports|While it's rare to have no vulnerabilities for your pipeline, it can happen. In any event, we ask that you double check your settings to make sure all security scanning jobs have passed successfully."
+msgstr ""
-msgid "SecurityDashboard|The security dashboard displays the latest security report. Use it to find and fix vulnerabilities."
+msgid "SecurityReports|While it's rare to have no vulnerabilities for your project, it can happen. In any event, we ask that you double check your settings to make sure you've set up your dashboard correctly."
msgstr ""
-msgid "SecurityDashboard|There was an error while generating the report."
+msgid "SecurityReports|While it's rare to have no vulnerabilities, it can happen. In any event, we ask that you please double check your settings to make sure you've set up your dashboard correctly."
msgstr ""
-msgid "SecurityDashboard|Unable to add %{invalidProjects}"
-msgstr "Ðе удаетÑÑ Ð´Ð¾Ð±Ð°Ð²Ð¸Ñ‚ÑŒ %{invalidProjects}"
+msgid "SecurityReports|Won't fix / Accept risk"
+msgstr ""
+
+msgid "SecurityReports|You do not have sufficient permissions to access this report"
+msgstr ""
+
+msgid "SecurityReports|You must sign in as an authorized user to see this report"
+msgstr ""
+
+msgid "SecurityReports|[No reason]"
+msgstr ""
msgid "See GitLab's %{password_policy_guidelines}"
msgstr ""
@@ -18281,6 +18709,9 @@ msgstr "ПоÑмотреть метрики"
msgid "See the affected projects in the GitLab admin panel"
msgstr "ПоÑмотреть затрагиваемые проекты в панели админиÑтратора GitLab"
+msgid "See what's new at GitLab"
+msgstr ""
+
msgid "Select"
msgstr ""
@@ -18381,7 +18812,7 @@ msgid "Select projects you want to import."
msgstr ""
msgid "Select required regulatory standard"
-msgstr ""
+msgstr "Выберите необходимый регулÑторный Ñтандарт"
msgid "Select shards to replicate"
msgstr ""
@@ -18489,7 +18920,7 @@ msgid "SeriesFinalConjunction|and"
msgstr "и"
msgid "Serve repository static objects (e.g. archives, blobs, ...) from an external storage (e.g. a CDN)."
-msgstr ""
+msgstr "Сервер ÑтатичеÑких объектов Ñ€ÐµÐ¿Ð¾Ð·Ð¸Ñ‚Ð¾Ñ€Ð¸Ñ (например, архивы, бинарные данные, ...) из внешнего хранилища (например, CDN)."
msgid "Server supports batch API only, please update your Git LFS client to version 1.0.1 and up."
msgstr "Сервер поддерживает только пакетный API, пожалуйÑта, обновите ваш клиент Git LFS до верÑии 1.0.1 и выше."
@@ -18504,7 +18935,7 @@ msgid "Serverless domain"
msgstr ""
msgid "ServerlessDetails|Function invocation metrics require Prometheus to be installed first."
-msgstr ""
+msgstr "Ð”Ð»Ñ Ð¸Ð·Ð¼ÐµÑ€ÐµÐ½Ð¸Ñ Ð²Ñ‹Ð·Ð¾Ð²Ð° функций Ñначала нужно уÑтановить Prometheus."
msgid "ServerlessDetails|Install Prometheus"
msgstr "УÑтановить Prometheus"
@@ -18522,16 +18953,16 @@ msgid "ServerlessDetails|More information"
msgstr "Подробнее"
msgid "ServerlessDetails|No pods loaded at this time."
-msgstr ""
+msgstr "Ðа данный момент нет загруженных подов."
msgid "ServerlessDetails|Number of Kubernetes pods in use over time based on necessity."
msgstr "КоличеÑтво подов Kubernetes, иÑпользуемых за определенный период времени, в завиÑимоÑти от необходимоÑти."
msgid "ServerlessDetails|pod in use"
-msgstr "ИÑпользуемый под"
+msgstr "иÑпользуемый под"
msgid "ServerlessDetails|pods in use"
-msgstr "ИÑпользуемые поды"
+msgstr "иÑпользуемые поды"
msgid "ServerlessURL|Copy URL"
msgstr ""
@@ -18642,7 +19073,7 @@ msgid "Set notification email for abuse reports."
msgstr "ÐаÑтроить ÑƒÐ²ÐµÐ´Ð¾Ð¼Ð»ÐµÐ½Ð¸Ñ Ð¿Ð¾ Ñлектронной почте Ð´Ð»Ñ ÑÐ¾Ð¾Ð±Ñ‰ÐµÐ½Ð¸Ñ Ð¾ злоупотреблениÑÑ…."
msgid "Set parent epic to an epic"
-msgstr ""
+msgstr "УÑтановить родительÑкую цель"
msgid "Set projects and maximum size limits, session duration, user options, and check feature availability for namespace plan."
msgstr ""
@@ -18669,7 +19100,7 @@ msgid "Set the maximum file size for each job's artifacts"
msgstr ""
msgid "Set the maximum number of pipeline minutes that a group can use on shared Runners per month. 0 for unlimited."
-msgstr ""
+msgstr "ÐаÑтройте макÑимальное чиÑло минут Ð´Ð»Ñ Ñборочной линии, которые группа может иÑпользовать в общих Runner'ах за меÑÑц. 0 Ð´Ð»Ñ Ð½ÐµÐ¾Ð³Ñ€Ð°Ð½Ð¸Ñ‡ÐµÐ½Ð½Ð¾Ð³Ð¾ чиÑла."
msgid "Set the milestone to %{milestone_reference}."
msgstr ""
@@ -18686,9 +19117,6 @@ msgstr "ÐаÑтройка CI/CD"
msgid "Set up Jira Integration"
msgstr ""
-msgid "Set up Jira Integration illustration"
-msgstr ""
-
msgid "Set up a %{type} Runner automatically"
msgstr ""
@@ -18711,10 +19139,10 @@ msgid "Set up your project to automatically push and/or pull changes to/from ano
msgstr "ÐаÑтройте Ñвой проект, чтобы автоматичеÑки загружать и/или Ñкачивать Ð¸Ð·Ð¼ÐµÐ½ÐµÐ½Ð¸Ñ Ð²/из другого репозиториÑ. Ветки, теги и коммиты будут ÑинхронизироватьÑÑ Ð°Ð²Ñ‚Ð¾Ð¼Ð°Ñ‚Ð¸Ñ‡ÐµÑки."
msgid "Set weight"
-msgstr "УÑтановить веÑ"
+msgstr "УÑтановить приоритет"
msgid "Set weight to %{weight}."
-msgstr ""
+msgstr "УÑтановить приоритет на %{weight}."
msgid "SetPasswordToCloneLink|set a password"
msgstr "уÑтановите пароль"
@@ -18744,7 +19172,7 @@ msgid "SetStatusModal|What's your status?"
msgstr "Как у Ð²Ð°Ñ Ð´ÐµÐ»Ð°?"
msgid "Sets %{epic_ref} as parent epic."
-msgstr ""
+msgstr "УÑтанавливает %{epic_ref} как родительÑкую цель."
msgid "Sets target branch to %{branch_name}."
msgstr ""
@@ -18759,7 +19187,7 @@ msgid "Sets time estimate to %{time_estimate}."
msgstr ""
msgid "Sets weight to %{weight}."
-msgstr ""
+msgstr "УÑтанавливает приоритет на %{weight}."
msgid "Settings"
msgstr "ÐаÑтройки"
@@ -18773,6 +19201,9 @@ msgstr ""
msgid "Severity: %{severity}"
msgstr ""
+msgid "Shards (%{shards})"
+msgstr ""
+
msgid "Shards to synchronize"
msgstr ""
@@ -18792,13 +19223,13 @@ msgid "Shared runners help link"
msgstr "Справка по общим Runner'ам"
msgid "SharedRunnersMinutesSettings|By resetting the pipeline minutes for this namespace, the currently used minutes will be set to zero."
-msgstr ""
+msgstr "При ÑброÑе минут Ñборочных линий Ð´Ð»Ñ Ñтого проÑтранÑтва имен, текущие иÑпользованные минуты будут обнулены."
msgid "SharedRunnersMinutesSettings|Reset pipeline minutes"
-msgstr ""
+msgstr "Ð¡Ð±Ñ€Ð¾Ñ Ð¼Ð¸Ð½ÑƒÑ‚ Ñборочной линии"
msgid "SharedRunnersMinutesSettings|Reset used pipeline minutes"
-msgstr ""
+msgstr "Ð¡Ð±Ñ€Ð¾Ñ Ð¸Ñпользованных минут Ñборочной линии"
msgid "Sherlock Transactions"
msgstr ""
@@ -18816,10 +19247,10 @@ msgid "Show all members"
msgstr ""
msgid "Show archived projects"
-msgstr "Показать архивные проекты"
+msgstr "Показывать архивные проекты"
msgid "Show archived projects only"
-msgstr "Показать только архивные проекты"
+msgstr "Показывать только архивные проекты"
msgid "Show command"
msgstr "Показать команду"
@@ -18858,7 +19289,7 @@ msgid "Show parent subgroups"
msgstr "Показать родительÑкие подгруппы"
msgid "Show whitespace changes"
-msgstr ""
+msgstr "Показать Ð¸Ð·Ð¼ÐµÐ½ÐµÐ½Ð¸Ñ Ð¿Ñ€Ð¾Ð±ÐµÐ»Ð¾Ð²"
msgid "Showing %d event"
msgid_plural "Showing %d events"
@@ -18895,7 +19326,7 @@ msgid "Sidebar|Assign health status"
msgstr ""
msgid "Sidebar|Change weight"
-msgstr ""
+msgstr "Изменить приоритет"
msgid "Sidebar|Health status"
msgstr ""
@@ -18910,7 +19341,7 @@ msgid "Sidebar|Only numeral characters allowed"
msgstr ""
msgid "Sidebar|Weight"
-msgstr ""
+msgstr "Приоритет"
msgid "Sign in"
msgstr "Вход"
@@ -18952,10 +19383,10 @@ msgid "Sign-up restrictions"
msgstr "ÐžÐ³Ñ€Ð°Ð½Ð¸Ñ‡ÐµÐ½Ð¸Ñ Ñ€ÐµÐ³Ð¸Ñтрации"
msgid "SignUp|First Name is too long (maximum is %{max_length} characters)."
-msgstr ""
+msgstr "Ð˜Ð¼Ñ Ñлишком длинное (макÑимум - %{max_length} Ñимволов)."
msgid "SignUp|Last Name is too long (maximum is %{max_length} characters)."
-msgstr ""
+msgstr "Ð¤Ð°Ð¼Ð¸Ð»Ð¸Ñ Ñлишком Ð´Ð»Ð¸Ð½Ð½Ð°Ñ (макÑимум - %{max_length} Ñимволов)."
msgid "SignUp|Name is too long (maximum is %{max_length} characters)."
msgstr "Слишком длинное Ð¸Ð¼Ñ (макÑимум %{max_length} Ñимволов)."
@@ -18964,7 +19395,7 @@ msgid "SignUp|Username is too long (maximum is %{max_length} characters)."
msgstr "Слишком длинное Ð¸Ð¼Ñ Ð¿Ð¾Ð»ÑŒÐ·Ð¾Ð²Ð°Ñ‚ÐµÐ»Ñ (макÑимум %{max_length} Ñимволов)."
msgid "Signed in"
-msgstr ""
+msgstr "ÐвторизовалиÑÑŒ"
msgid "Signed in with %{authentication} authentication"
msgstr "Вход Ñ %{authentication} аутентификацией"
@@ -18988,13 +19419,13 @@ msgid "Size and domain settings for static websites"
msgstr "ÐаÑтройки размера и доменных имён Ð´Ð»Ñ ÑтатичеÑких веб-Ñайтов"
msgid "Size limit per repository (MB)"
-msgstr ""
+msgstr "Лимит размера на репозиторий (Мбайт)"
msgid "Size settings for static websites"
msgstr ""
msgid "Skip outdated deployment jobs"
-msgstr ""
+msgstr "ПропуÑтить уÑтаревшие Ð·Ð°Ð´Ð°Ð½Ð¸Ñ Ñ€Ð°Ð·Ð²Ñ‘Ñ€Ñ‚Ñ‹Ð²Ð°Ð½Ð¸Ñ"
msgid "Skip this for now"
msgstr "ПропуÑтить Ñто ÑейчаÑ"
@@ -19018,7 +19449,7 @@ msgid "SlackIntegration|<strong>Note:</strong> Usernames and private channels ar
msgstr ""
msgid "SlackIntegration|Paste the <strong>Webhook URL</strong> into the field below."
-msgstr ""
+msgstr "Ð’Ñтавьте <strong>URL веб-обработчика</strong> в поле ниже."
msgid "SlackIntegration|Select events below to enable notifications. The <strong>Slack channel names</strong> and <strong>Slack username</strong> fields are optional."
msgstr ""
@@ -19030,7 +19461,7 @@ msgid "SlackService|2. Paste the <strong>Token</strong> into the field below"
msgstr ""
msgid "SlackService|3. Select the <strong>Active</strong> checkbox, press <strong>Save changes</strong> and start using GitLab inside Slack!"
-msgstr ""
+msgstr "Выберите флажок <strong>Ðктивный</strong>, нажмите на <strong>Сохранить изменениÑ</strong> и начните иÑпользовать GitLab внутри Slack!"
msgid "SlackService|Fill in the word that works best for your team."
msgstr ""
@@ -19089,6 +19520,9 @@ msgstr ""
msgid "Snowplow"
msgstr ""
+msgid "Some child epics may be hidden due to applied filters"
+msgstr ""
+
msgid "Some email servers do not support overriding the email sender name. Enable this option to include the name of the author of the issue, merge request or comment in the email body instead."
msgstr ""
@@ -19117,7 +19551,7 @@ msgid "Something went wrong on our end. Please try again!"
msgstr ""
msgid "Something went wrong on our end. Please try again."
-msgstr "Что-то пошло не так на нашей Ñтороне. ПожалуйÑта, попробуйте еще раз."
+msgstr "Что-то пошло не так на нашей Ñтороне. ПожалуйÑта, попробуйте ещё раз."
msgid "Something went wrong trying to change the confidentiality of this issue"
msgstr ""
@@ -19129,19 +19563,19 @@ msgid "Something went wrong when toggling the button"
msgstr "Что-то пошло не так при переключении кнопки"
msgid "Something went wrong while adding your award. Please try again."
-msgstr "Что-то пошло не так при добавлении вашей награды. ПожалуйÑта, попробуйте еще раз."
+msgstr "Что-то пошло не так при добавлении вашей награды. ПожалуйÑта, попробуйте ещё раз."
msgid "Something went wrong while applying the suggestion. Please try again."
msgstr ""
msgid "Something went wrong while archiving a requirement."
-msgstr ""
+msgstr "Что-то пошло не так при архивировании требованиÑ."
msgid "Something went wrong while closing the %{issuable}. Please try again later"
msgstr ""
msgid "Something went wrong while creating a requirement."
-msgstr ""
+msgstr "Что-то пошло не так при Ñоздании требованиÑ."
msgid "Something went wrong while deleting description changes. Please try again."
msgstr ""
@@ -19186,7 +19620,7 @@ msgid "Something went wrong while fetching related merge requests."
msgstr "Что-то пошло не так при получении ÑвÑзанных запроÑов на ÑлиÑние."
msgid "Something went wrong while fetching requirements list."
-msgstr ""
+msgstr "Что-то пошло не так при извлечении ÑпиÑка требований."
msgid "Something went wrong while fetching the environments for this merge request. Please try again."
msgstr ""
@@ -19213,7 +19647,7 @@ msgid "Something went wrong while performing the action."
msgstr ""
msgid "Something went wrong while reopening a requirement."
-msgstr ""
+msgstr "Что-то пошло не так при повторном открытии требованиÑ."
msgid "Something went wrong while reopening the %{issuable}. Please try again later"
msgstr ""
@@ -19225,11 +19659,14 @@ msgid "Something went wrong while stopping this environment. Please try again."
msgstr ""
msgid "Something went wrong while updating a requirement."
-msgstr ""
+msgstr "Что-то пошло не так при обновлении требованиÑ."
msgid "Something went wrong while updating your list settings"
msgstr ""
+msgid "Something went wrong with your automatic subscription renewal"
+msgstr ""
+
msgid "Something went wrong, unable to add %{project} to dashboard"
msgstr ""
@@ -19252,7 +19689,7 @@ msgid "Something went wrong. Try again later."
msgstr ""
msgid "Sorry, no epics matched your search"
-msgstr ""
+msgstr "Извините, нет целей, ÑоответÑтвующих вашему поиÑку"
msgid "Sorry, no projects matched your search"
msgstr ""
@@ -19264,10 +19701,10 @@ msgid "Sorry, your filter produced no results"
msgstr ""
msgid "Sort by"
-msgstr "Сортировать по"
+msgstr "Сортировка"
msgid "Sort direction"
-msgstr ""
+msgstr "ПорÑдок Ñортировки"
msgid "SortOptions|Access level, ascending"
msgstr "Уровень доÑтупа, по возраÑтанию"
@@ -19288,7 +19725,7 @@ msgid "SortOptions|Due soon"
msgstr "Срок раньше"
msgid "SortOptions|Expired date"
-msgstr ""
+msgstr "По дате иÑтечениÑ"
msgid "SortOptions|Label priority"
msgstr "Приоритет метки"
@@ -19300,22 +19737,22 @@ msgid "SortOptions|Largest repository"
msgstr "Крупнейший репозиторий"
msgid "SortOptions|Last Contact"
-msgstr ""
+msgstr "По поÑледнему контакту"
msgid "SortOptions|Last created"
-msgstr "ПоÑледние Ñозданные"
+msgstr "Сначала недавно Ñозданные"
msgid "SortOptions|Last joined"
msgstr "ПоÑледние проÑоединившиеÑÑ"
msgid "SortOptions|Last updated"
-msgstr "ПоÑледние обновлённые"
+msgstr "Сначала недавно обновлённые"
msgid "SortOptions|Least popular"
msgstr "Ðаименее популÑрный"
msgid "SortOptions|Less weight"
-msgstr ""
+msgstr "Сначала Ñ Ð¼ÐµÐ½ÑŒÑˆÐ¸Ð¼ приоритетом"
msgid "SortOptions|Manual"
msgstr "РучнаÑ"
@@ -19330,25 +19767,25 @@ msgid "SortOptions|Milestone due soon"
msgstr "Веха, наÑÑ‚ÑƒÐ¿Ð°ÑŽÑ‰Ð°Ñ Ñ€Ð°Ð½ÑŒÑˆÐµ"
msgid "SortOptions|More weight"
-msgstr ""
+msgstr "Сначала Ñ Ð±Ð¾Ð»ÑŒÑˆÐ¸Ð¼ приоритетом"
msgid "SortOptions|Most popular"
msgstr "Ðаиболее популÑрный"
msgid "SortOptions|Most stars"
-msgstr "По рейтингу"
+msgstr "по рейтингу"
msgid "SortOptions|Name"
-msgstr "ИмÑ"
+msgstr "По алфавиту"
msgid "SortOptions|Name, ascending"
-msgstr "ИмÑ, по возраÑтанию"
+msgstr "По алфавиту"
msgid "SortOptions|Name, descending"
-msgstr "ИмÑ, по убыванию"
+msgstr "По алфавиту Ñ ÐºÐ¾Ð½Ñ†Ð°"
msgid "SortOptions|Oldest created"
-msgstr "Давно Ñозданные"
+msgstr "Сначала давно Ñозданные"
msgid "SortOptions|Oldest joined"
msgstr "Старейшие из приÑоединившихÑÑ"
@@ -19360,10 +19797,10 @@ msgid "SortOptions|Oldest sign in"
msgstr "Старейшие из заходивших"
msgid "SortOptions|Oldest starred"
-msgstr ""
+msgstr "Сначала давно избранные"
msgid "SortOptions|Oldest updated"
-msgstr "Давно обновленные"
+msgstr "Сначала давно обновлённые"
msgid "SortOptions|Popularity"
msgstr "ПопулÑронÑÑ‚ÑŒ"
@@ -19372,28 +19809,28 @@ msgid "SortOptions|Priority"
msgstr "Приоритет"
msgid "SortOptions|Project"
-msgstr ""
+msgstr "Проект"
msgid "SortOptions|Recent last activity"
-msgstr ""
+msgstr "По поÑледней активноÑти"
msgid "SortOptions|Recent sign in"
msgstr "Ðедавно заходившие"
msgid "SortOptions|Recently starred"
-msgstr ""
+msgstr "Сначала недавно избранные"
msgid "SortOptions|Size"
msgstr "Размер"
msgid "SortOptions|Sort direction"
-msgstr ""
+msgstr "Ðаправление Ñортировки"
msgid "SortOptions|Stars"
-msgstr ""
+msgstr "По рейтингу"
msgid "SortOptions|Start date"
-msgstr ""
+msgstr "По дате начала"
msgid "SortOptions|Start later"
msgstr "Ðачатые позже"
@@ -19402,13 +19839,13 @@ msgid "SortOptions|Start soon"
msgstr "Ðачатые недавно"
msgid "SortOptions|Type"
-msgstr ""
+msgstr "По типу"
msgid "SortOptions|Version"
-msgstr ""
+msgstr "По верÑии"
msgid "SortOptions|Weight"
-msgstr ""
+msgstr "Приоритет"
msgid "Source"
msgstr "ИÑточник"
@@ -19456,16 +19893,16 @@ msgid "SourcegraphAdmin|e.g. https://sourcegraph.example.com"
msgstr ""
msgid "SourcegraphPreferences|This feature is experimental and currently limited to certain projects."
-msgstr ""
+msgstr "Эта возможноÑÑ‚ÑŒ ÑкÑÐ¿ÐµÑ€Ð¸Ð¼ÐµÐ½Ñ‚Ð°Ð»ÑŒÐ½Ð°Ñ Ð¸ в наÑтоÑщее Ð²Ñ€ÐµÐ¼Ñ Ð¾Ð³Ñ€Ð°Ð½Ð¸Ñ‡ÐµÐ½Ð° определенными проектами."
msgid "SourcegraphPreferences|This feature is experimental and limited to public projects."
-msgstr ""
+msgstr "Эта возможноÑÑ‚ÑŒ ÑкÑÐ¿ÐµÑ€Ð¸Ð¼ÐµÐ½Ñ‚Ð°Ð»ÑŒÐ½Ð°Ñ Ð¸ доÑтупна только Ð´Ð»Ñ Ð¿ÑƒÐ±Ð»Ð¸Ñ‡Ð½Ñ‹Ñ… проектов."
msgid "SourcegraphPreferences|This feature is experimental."
-msgstr ""
+msgstr "Эта возможноÑÑ‚ÑŒ ÑкÑпериментальнаÑ."
msgid "SourcegraphPreferences|Uses %{link_start}Sourcegraph.com%{link_end}."
-msgstr ""
+msgstr "ИÑпользует %{link_start}Sourcegraph.com%{link_end}."
msgid "SourcegraphPreferences|Uses a custom %{link_start}Sourcegraph instance%{link_end}."
msgstr ""
@@ -19515,6 +19952,9 @@ msgstr ""
msgid "Stage removed"
msgstr ""
+msgid "Standard"
+msgstr ""
+
msgid "Star a label to make it a priority label. Order the prioritized labels to change their relative priority, by dragging."
msgstr "ПоÑтавьте звёздочку, чтобы Ñделать метку приоритетной. УпорÑдочивайте приоритетные метки перетаÑкиванием Ð´Ð»Ñ Ð¸Ð·Ð¼ÐµÐ½ÐµÐ½Ð¸Ñ Ð¸Ñ… отноÑительного приоритета."
@@ -19558,7 +19998,7 @@ msgid "Start a %{new_merge_request} with these changes"
msgstr "Ðачать %{new_merge_request} Ñ Ñтих изменений"
msgid "Start a Free Gold Trial"
-msgstr ""
+msgstr "Ðачать беÑплатный пробный период Gold"
msgid "Start a new discussion..."
msgstr "Ðачать новое обÑуждение ..."
@@ -19609,7 +20049,7 @@ msgid "Start your Free Gold Trial"
msgstr "Ðачните беÑплатный пробный период Gold"
msgid "Start your free trial"
-msgstr ""
+msgstr "Ðачать беÑплатный пробный период"
msgid "Start your trial"
msgstr ""
@@ -19638,6 +20078,9 @@ msgstr ""
msgid "Static Application Security Testing (SAST)"
msgstr ""
+msgid "StaticSiteEditor|An error occurred while submitting your changes."
+msgstr ""
+
msgid "StaticSiteEditor|Branch could not be created."
msgstr ""
@@ -19647,18 +20090,30 @@ msgstr ""
msgid "StaticSiteEditor|Could not create merge request."
msgstr ""
+msgid "StaticSiteEditor|Incompatible file content"
+msgstr ""
+
msgid "StaticSiteEditor|Return to site"
msgstr ""
+msgid "StaticSiteEditor|Static site editor"
+msgstr ""
+
msgid "StaticSiteEditor|Success!"
msgstr ""
msgid "StaticSiteEditor|Summary of changes"
msgstr ""
+msgid "StaticSiteEditor|The Static Site Editor is currently configured to only edit Markdown content on pages generated from Middleman. Visit the documentation to learn more about configuring your site to use the Static Site Editor."
+msgstr ""
+
msgid "StaticSiteEditor|Update %{sourcePath} file"
msgstr ""
+msgid "StaticSiteEditor|View documentation"
+msgstr ""
+
msgid "StaticSiteEditor|View merge request"
msgstr ""
@@ -19702,7 +20157,7 @@ msgid "StatusPage|Active"
msgstr ""
msgid "StatusPage|Bucket %{docsLink}"
-msgstr ""
+msgstr "Bucket %{docsLink}"
msgid "StatusPage|Configure file storage settings to link issues in this project to an external status page."
msgstr ""
@@ -19711,11 +20166,17 @@ msgid "StatusPage|For help with configuration, visit %{docsLink}"
msgstr ""
msgid "StatusPage|S3 Bucket name"
-msgstr ""
+msgstr "Ð˜Ð¼Ñ Bucket'а S3"
msgid "StatusPage|Status page"
msgstr ""
+msgid "StatusPage|Status page URL"
+msgstr ""
+
+msgid "StatusPage|Status page frontend documentation"
+msgstr ""
+
msgid "StatusPage|To publish incidents to an external status page, GitLab will store a JSON file in your Amazon S3 account in a location accessible to your external status page service. Make sure to also set up %{docsLink}"
msgstr ""
@@ -19726,7 +20187,7 @@ msgid "StatusPage|your status page frontend."
msgstr ""
msgid "Stay updated about the performance and health of your environment by configuring Prometheus to monitor your deployments."
-msgstr ""
+msgstr "Будьте в курÑе производительноÑти и работоÑпоÑобноÑти вашего Ð¾ÐºÑ€ÑƒÐ¶ÐµÐ½Ð¸Ñ Ñ Ð¿Ð¾Ð¼Ð¾Ñ‰ÑŒÑŽ наÑтройки Prometheus Ð´Ð»Ñ Ð¼Ð¾Ð½Ð¸Ñ‚Ð¾Ñ€Ð¸Ð½Ð³Ð° ваших развёртываний."
msgid "Stop Terminal"
msgstr ""
@@ -19750,7 +20211,7 @@ msgid "Storage nodes for new repositories"
msgstr ""
msgid "Storage:"
-msgstr ""
+msgstr "Хранилище:"
msgid "StorageSize|Unknown"
msgstr "ÐеизвеÑтный"
@@ -19774,7 +20235,7 @@ msgid "Subgroups"
msgstr "Подгруппы"
msgid "Subgroups and projects"
-msgstr ""
+msgstr "Подгруппы и проекты"
msgid "Subject Key Identifier:"
msgstr ""
@@ -19849,7 +20310,7 @@ msgid "Subscription successfully deleted."
msgstr ""
msgid "SubscriptionTable|Billing"
-msgstr ""
+msgstr "Оплата"
msgid "SubscriptionTable|Free"
msgstr "БеÑплатно"
@@ -19864,13 +20325,13 @@ msgid "SubscriptionTable|Loading subscriptions"
msgstr "Загрузка подпиÑок"
msgid "SubscriptionTable|Manage"
-msgstr ""
+msgstr "Управление"
msgid "SubscriptionTable|Max seats used"
msgstr "МакÑимальное количеÑтво иÑпользуемых меÑÑ‚"
msgid "SubscriptionTable|Next invoice"
-msgstr ""
+msgstr "Следующий Ñчет"
msgid "SubscriptionTable|Seats currently in use"
msgstr "МеÑта, иÑпользуемые в наÑтоÑщее времÑ"
@@ -19900,7 +20361,7 @@ msgid "SubscriptionTable|This is the number of seats you will be required to pur
msgstr "Это количеÑтво меÑÑ‚, которое вам необходимо будет приобреÑти, еÑли вы перейдете на платный тарифный план."
msgid "SubscriptionTable|Trial"
-msgstr ""
+msgstr "Пробный период"
msgid "SubscriptionTable|Trial end date"
msgstr "Дата Ð¾ÐºÐ¾Ð½Ñ‡Ð°Ð½Ð¸Ñ Ð¿Ñ€Ð¾Ð±Ð½Ð¾Ð³Ð¾ периода"
@@ -19909,13 +20370,13 @@ msgid "SubscriptionTable|Trial start date"
msgstr "Дата начала пробного периода"
msgid "SubscriptionTable|Upgrade"
-msgstr ""
+msgstr "ПовыÑить"
msgid "SubscriptionTable|Usage"
-msgstr ""
+msgstr "ИÑпользование"
msgid "SubscriptionTable|Usage count is performed once a day at 12:00 PM."
-msgstr ""
+msgstr "ПодÑчет иÑÐ¿Ð¾Ð»ÑŒÐ·Ð¾Ð²Ð°Ð½Ð¸Ñ Ð²Ñ‹Ð¿Ð¾Ð»Ð½ÑетÑÑ Ð¾Ð´Ð¸Ð½ раз в день в 12:00 ночи."
msgid "Subscriptions"
msgstr ""
@@ -20029,7 +20490,7 @@ msgid "SuggestedColors|Very pale orange"
msgstr ""
msgid "Suggestions:"
-msgstr ""
+msgstr "ПредложениÑ:"
msgid "Suite"
msgstr ""
@@ -20064,6 +20525,12 @@ msgstr ""
msgid "Synced"
msgstr ""
+msgid "Synchronization disabled"
+msgstr ""
+
+msgid "Synchronization of container repositories is disabled."
+msgstr ""
+
msgid "System"
msgstr "СиÑтема"
@@ -20164,7 +20631,7 @@ msgid "TagsPage|New tag"
msgstr "Ðовый тег"
msgid "TagsPage|Optionally, add a message to the tag. Leaving this blank creates a %{link_start}lightweight tag.%{link_end}"
-msgstr ""
+msgstr "При желании, добавьте Ñообщение к метке. ЕÑли оÑтавить его пуÑтым, будет Ñоздана %{link_start}легковеÑÐ½Ð°Ñ Ð¼ÐµÑ‚ÐºÐ°.%{link_end}"
msgid "TagsPage|Optionally, create a public Release of your project, based on this tag. Release notes are displayed on the %{releases_page_link_start}Releases%{link_end} page. %{docs_link_start}More information%{link_end}"
msgstr ""
@@ -20217,6 +20684,9 @@ msgstr ""
msgid "Telephone number"
msgstr ""
+msgid "Telephone number (Optional)"
+msgstr ""
+
msgid "Template"
msgstr "Шаблон"
@@ -20276,7 +20746,7 @@ msgid "TestHooks|Ensure the project has at least one commit."
msgstr "УбедитеÑÑŒ, что в проекте еÑÑ‚ÑŒ Ñ…Ð¾Ñ‚Ñ Ð±Ñ‹ один коммит."
msgid "TestHooks|Ensure the project has issues."
-msgstr ""
+msgstr "УбедитеÑÑŒ, что у проекта еÑÑ‚ÑŒ обÑуждениÑ."
msgid "TestHooks|Ensure the project has merge requests."
msgstr "УбедитеÑÑŒ, что у проекта еÑÑ‚ÑŒ запроÑÑ‹ на ÑлиÑние."
@@ -20291,31 +20761,31 @@ msgid "TestReports|%{count} errors"
msgstr ""
msgid "TestReports|%{count} failures"
-msgstr ""
+msgstr "%{count} Ñбоев"
msgid "TestReports|%{count} jobs"
-msgstr ""
+msgstr "%{count} заданий"
msgid "TestReports|%{rate}%{sign} success rate"
msgstr ""
msgid "TestReports|Test suites"
-msgstr ""
+msgstr "Ðаборы теÑтов"
msgid "TestReports|Tests"
-msgstr ""
+msgstr "ТеÑÑ‚Ñ‹"
msgid "TestReports|There are no test cases to display."
-msgstr ""
+msgstr "Ðет теÑтовых примеров Ð´Ð»Ñ Ð¾Ñ‚Ð¾Ð±Ñ€Ð°Ð¶ÐµÐ½Ð¸Ñ."
msgid "TestReports|There are no test suites to show."
-msgstr ""
+msgstr "Ðет теÑтовых наборов Ð´Ð»Ñ Ð¾Ñ‚Ð¾Ð±Ñ€Ð°Ð¶ÐµÐ½Ð¸Ñ."
msgid "TestReports|There are no tests to show."
-msgstr ""
+msgstr "Ðет теÑтов Ð´Ð»Ñ Ð¾Ñ‚Ð¾Ð±Ñ€Ð°Ð¶ÐµÐ½Ð¸Ñ."
msgid "TestReports|There was an error fetching the test reports."
-msgstr ""
+msgstr "При извлечении отчетов о теÑтировании произошла ошибка."
msgid "Tests"
msgstr ""
@@ -20412,7 +20882,7 @@ msgid "The configuration status of the table below only applies to the default b
msgstr ""
msgid "The connection will time out after %{timeout}. For repositories that take longer, use a clone/push combination."
-msgstr ""
+msgstr "Соединение будет отключено через %{timeout}. Ð”Ð»Ñ Ñ€ÐµÐ¿Ð¾Ð·Ð¸Ñ‚Ð¾Ñ€Ð¸ÐµÐ², требующих больше времени, иÑпользуйте комбинацию clone/push."
msgid "The content of this page is not encoded in UTF-8. Edits can only be made via the Git repository."
msgstr "Содержание Ñтой Ñтраницы не закодировано в UTF-8. Редактировать можно только через Git репозиторий."
@@ -20433,7 +20903,7 @@ msgid "The dependency list details information about the components used within
msgstr ""
msgid "The deployment of this job to %{environmentLink} did not succeed."
-msgstr ""
+msgstr "Задание Ñ€Ð°Ð·Ð²Ñ‘Ñ€Ñ‚Ñ‹Ð²Ð°Ð½Ð¸Ñ Ð½Ð° %{environmentLink} не удалоÑÑŒ."
msgid "The designs you tried uploading did not change."
msgstr ""
@@ -20529,11 +20999,14 @@ msgstr "Ð›Ð¸Ñ†ÐµÐ½Ð·Ð¸Ñ Ð±Ñ‹Ð»Ð° удалена. У GitLab больше нет
msgid "The license was successfully uploaded and is now active. You can see the details below."
msgstr "Ð›Ð¸Ñ†ÐµÐ½Ð·Ð¸Ñ Ð±Ñ‹Ð»Ð° уÑпешно загружена и теперь активна. Ð’Ñ‹ можете увидеть подробноÑти ниже."
+msgid "The license was successfully uploaded and will be active from %{starts_at}. You can see the details below."
+msgstr ""
+
msgid "The maximum file size allowed is %{size}."
msgstr "МакÑимальный размер файла - %{size}."
msgid "The maximum file size allowed is 200KB."
-msgstr "МакÑимально допуÑтимый размер файла ÑоÑтавлÑет 200 Кб."
+msgstr "МакÑимально допуÑтимый размер файла ÑоÑтавлÑет 200 Кбайт."
msgid "The merge conflicts for this merge request cannot be resolved through GitLab. Please try to resolve them locally."
msgstr "Конфликты ÑлиÑÐ½Ð¸Ñ Ð´Ð»Ñ Ñтого запроÑа на ÑлиÑние не могут быть разрешены Ñ Ð¿Ð¾Ð¼Ð¾Ñ‰ÑŒÑŽ GitLab. ПожалуйÑта, попробуйте разрешить их локально."
@@ -20674,7 +21147,7 @@ msgid "The unique identifier for the Geo node. Must match %{geoNodeName} if it i
msgstr ""
msgid "The update action will time out after %{number_of_minutes} minutes. For big repositories, use a clone/push combination."
-msgstr ""
+msgstr "Ð’Ñ€ÐµÐ¼Ñ Ð´ÐµÐ¹ÑÑ‚Ð²Ð¸Ñ Ð¾Ð±Ð½Ð¾Ð²Ð»ÐµÐ½Ð¸Ñ Ð¸Ñтечет через %{number_of_minutes} минут. Ð”Ð»Ñ Ð±Ð¾Ð»ÑŒÑˆÐ¸Ñ… репозиториев иÑпользуйте комбинацию clone/push."
msgid "The uploaded file is not a valid Google Takeout archive."
msgstr "Загруженный файл не ÑвлÑетÑÑ Ð´Ð¾Ð¿ÑƒÑтимым архивом Google Takeout."
@@ -20710,22 +21183,22 @@ msgid "The vulnerability is no longer detected. Verify the vulnerability has bee
msgstr ""
msgid "There are no GPG keys associated with this account."
-msgstr "Ðет GPG ключей, ÑвÑзанных Ñ Ñтой учетной запиÑью."
+msgstr "Ðет ключей GPG, ÑвÑзанных Ñ Ñтой учетной запиÑью."
msgid "There are no GPG keys with access to your account."
-msgstr "Ðет GPG ключей Ñ Ð´Ð¾Ñтупом к вашей учетной запиÑи."
+msgstr "Ðет ключей GPG Ñ Ð´Ð¾Ñтупом к вашей учетной запиÑи."
msgid "There are no SSH keys associated with this account."
-msgstr "Ðет SSH ключей, ÑвÑзанных Ñ Ñтой учетной запиÑью."
+msgstr "Ðет ключей SSH, ÑвÑзанных Ñ Ñтой учетной запиÑью."
msgid "There are no SSH keys with access to your account."
-msgstr "Ðет SSH ключей Ñ Ð´Ð¾Ñтупом к вашей учетной запиÑи."
+msgstr "Ðет ключей SSH Ñ Ð´Ð¾Ñтупом к вашей учетной запиÑи."
msgid "There are no archived projects yet"
msgstr "Ðрхивных проектов пока нет"
msgid "There are no archived requirements"
-msgstr ""
+msgstr "Ðет архивных требований"
msgid "There are no changes"
msgstr ""
@@ -20761,7 +21234,7 @@ msgid "There are no open merge requests"
msgstr ""
msgid "There are no open requirements"
-msgstr ""
+msgstr "Открытых требований нет"
msgid "There are no packages yet"
msgstr "Пакетов пока нет"
@@ -20800,10 +21273,10 @@ msgid "There was an error adding a To Do."
msgstr ""
msgid "There was an error creating the dashboard, branch name is invalid."
-msgstr ""
+msgstr "Произошла ошибка при Ñоздании панели управлениÑ: Ð¸Ð¼Ñ Ð²ÐµÑ‚ÐºÐ¸ неверно."
msgid "There was an error creating the dashboard, branch named: %{branch} already exists."
-msgstr ""
+msgstr "Произошла ошибка при Ñоздании панели управлениÑ: ветка, называющаÑÑÑ %{branch}, уже ÑущеÑтвует."
msgid "There was an error creating the issue"
msgstr "При Ñоздании обÑÑƒÐ¶Ð´ÐµÐ½Ð¸Ñ Ð¿Ñ€Ð¾Ð¸Ð·Ð¾ÑˆÐ»Ð° ошибка"
@@ -20848,6 +21321,9 @@ msgid "There was an error gathering the chart data"
msgstr "При Ñборе данных диаграммы произошла ошибка"
msgid "There was an error getting the epic participants."
+msgstr "Произошла ошибка при получении учаÑтников цели."
+
+msgid "There was an error importing the Jira project."
msgstr ""
msgid "There was an error loading users activity calendar."
@@ -20860,12 +21336,12 @@ msgid "There was an error removing your custom stage, please try again"
msgstr ""
msgid "There was an error resetting group pipeline minutes."
-msgstr ""
+msgstr "Произошла ошибка при ÑброÑе минут Ñборочной линии группы."
msgid "There was an error resetting user pipeline minutes."
-msgstr ""
+msgstr "Произошла ошибка при ÑброÑе минут пользовательÑкой Ñборочной линии."
-msgid "There was an error saving this Geo Node"
+msgid "There was an error saving this Geo Node."
msgstr ""
msgid "There was an error saving your changes."
@@ -20928,6 +21404,9 @@ msgstr ""
msgid "They can be managed using the %{link}."
msgstr ""
+msgid "Third Party Advisory Link"
+msgstr ""
+
msgid "Third party offers"
msgstr ""
@@ -20964,6 +21443,9 @@ msgstr ""
msgid "This application will be able to:"
msgstr ""
+msgid "This attachment has been truncated to avoid exceeding the maximum allowed attachment size of 15MB. %{written_count} of %{issues_count} issues have been included. Consider re-exporting with a narrower selection of issues."
+msgstr ""
+
msgid "This block is self-referential"
msgstr ""
@@ -21007,10 +21489,10 @@ msgid "This device has not been registered with us."
msgstr ""
msgid "This diff is collapsed."
-msgstr ""
+msgstr "Это отличие Ñвёрнуто."
msgid "This diff was suppressed by a .gitattributes entry."
-msgstr ""
+msgstr "Это отличие было подавлено запиÑью .gitattributes."
msgid "This directory"
msgstr "Этот каталог"
@@ -21022,7 +21504,7 @@ msgid "This endpoint has been requested too many times. Try again later."
msgstr ""
msgid "This environment has no deployments yet."
-msgstr ""
+msgstr "В данном окружении пока нет развёртываний."
msgid "This environment is being deployed"
msgstr ""
@@ -21037,7 +21519,7 @@ msgid "This epic does not exist or you don't have sufficient permission."
msgstr "Этой цели не ÑущеÑтвует, или у Ð²Ð°Ñ Ð½ÐµÐ´Ð¾Ñтаточно прав."
msgid "This feature requires local storage to be enabled"
-msgstr ""
+msgstr "Эта Ñ„ÑƒÐ½ÐºÑ†Ð¸Ñ Ñ‚Ñ€ÐµÐ±ÑƒÐµÑ‚, чтобы локальное хранилище было включено"
msgid "This field is required."
msgstr ""
@@ -21097,7 +21579,7 @@ msgid "This is the number of currently active users on your installation, and th
msgstr ""
msgid "This is your current session"
-msgstr ""
+msgstr "Это ваша Ñ‚ÐµÐºÑƒÑ‰Ð°Ñ ÑеÑÑиÑ"
msgid "This issue is %{confidentialLinkStart}confidential%{linkEnd} and %{lockedLinkStart}locked%{linkEnd}."
msgstr ""
@@ -21105,6 +21587,12 @@ msgstr ""
msgid "This issue is confidential"
msgstr "Это обÑуждение ÑвлÑетÑÑ ÐºÐ¾Ð½Ñ„Ð¸Ð´ÐµÐ½Ñ†Ð¸Ð°Ð»ÑŒÐ½Ñ‹Ð¼"
+msgid "This issue is currently blocked by the following issues: %{issues}."
+msgstr ""
+
+msgid "This issue is in a child epic of the filtered epic"
+msgstr ""
+
msgid "This issue is locked."
msgstr "ОбÑуждение заблокировано."
@@ -21136,16 +21624,16 @@ msgid "This job is an out-of-date deployment to %{environmentLink} using cluster
msgstr ""
msgid "This job is an out-of-date deployment to %{environmentLink} using cluster %{clusterNameOrLink}."
-msgstr ""
+msgstr "Это задание ÑвлÑетÑÑ ÑƒÑтаревшим развёртыванием в %{environmentLink} Ñ Ð¸Ñпользованием клаÑтера %{clusterNameOrLink}."
msgid "This job is an out-of-date deployment to %{environmentLink} using cluster %{clusterNameOrLink}. View the %{deploymentLink}."
-msgstr ""
+msgstr "Это задание ÑвлÑетÑÑ ÑƒÑтаревшим развёртыванием в %{environmentLink} Ñ Ð¸Ñпользованием клаÑтера %{clusterNameOrLink}. Смотрите %{deploymentLink}."
msgid "This job is an out-of-date deployment to %{environmentLink}."
-msgstr ""
+msgstr "Эта работа ÑвлÑетÑÑ ÑƒÑтаревшим развёртыванием в %{environmentLink}."
msgid "This job is an out-of-date deployment to %{environmentLink}. View the %{deploymentLink}."
-msgstr ""
+msgstr "Это задание ÑвлÑетÑÑ ÑƒÑтаревшим развёртыванием в %{environmentLink}. Смотрите %{deploymentLink}."
msgid "This job is archived. Only the complete pipeline can be retried."
msgstr ""
@@ -21160,13 +21648,13 @@ msgid "This job is creating a deployment to %{environmentLink} using cluster %{c
msgstr ""
msgid "This job is creating a deployment to %{environmentLink} using cluster %{clusterNameOrLink}. This will overwrite the %{deploymentLink}."
-msgstr ""
+msgstr "Это задание ÑоздаÑÑ‚ развёртывание в %{environmentLink} Ñ Ð¸Ñпользованием клаÑтера %{clusterNameOrLink}. Оно перезапишет %{deploymentLink}."
msgid "This job is creating a deployment to %{environmentLink}."
-msgstr ""
+msgstr "Это задание Ñоздает развёртывание в %{environmentLink}."
msgid "This job is creating a deployment to %{environmentLink}. This will overwrite the %{deploymentLink}."
-msgstr ""
+msgstr "Это задание ÑоздаÑÑ‚ развёртывание в %{environmentLink}. Оно перезапишет %{deploymentLink}."
msgid "This job is deployed to %{environmentLink} using cluster %{clusterNameOrLink} and namespace %{kubernetesNamespace}."
msgstr ""
@@ -21303,7 +21791,10 @@ msgstr ""
msgid "This user will be the author of all events in the activity feed that are the result of an update, like new branches being created or new commits being pushed to existing branches. Upon creation or when reassigning you can only assign yourself to be the mirror user."
msgstr ""
-msgid "This variable can not be masked"
+msgid "This variable can not be masked."
+msgstr ""
+
+msgid "This variable does not match the expected pattern."
msgstr ""
msgid "This will help us personalize your onboarding experience."
@@ -21333,10 +21824,10 @@ msgstr ""
msgid "ThreatMonitoring|Application firewall not detected"
msgstr ""
-msgid "ThreatMonitoring|Container Network Policy"
+msgid "ThreatMonitoring|Container Network Policies are not installed or have been disabled. To view this data, ensure your Network Policies are installed and enabled for your cluster."
msgstr ""
-msgid "ThreatMonitoring|Container NetworkPolicies are not installed or has been disabled. To view this data, ensure you NetworkPolicies are installed and enabled for your cluster."
+msgid "ThreatMonitoring|Container Network Policy"
msgstr ""
msgid "ThreatMonitoring|Container NetworkPolicies not detected"
@@ -21369,7 +21860,7 @@ msgstr ""
msgid "ThreatMonitoring|Something went wrong, unable to fetch statistics"
msgstr ""
-msgid "ThreatMonitoring|The firewall is not installed or has been disabled. To view this data, ensure you firewall is installed and enabled for your cluster."
+msgid "ThreatMonitoring|The firewall is not installed or has been disabled. To view this data, ensure the web application firewall is installed and enabled for your cluster."
msgstr ""
msgid "ThreatMonitoring|The graph below is an overview of traffic coming to your application as tracked by the Web Application Firewall (WAF). View the docs for instructions on how to access the WAF logs to see what type of malicious traffic is trying to access your app. The docs link is also accessible by clicking the \"?\" icon next to the title below."
@@ -21438,6 +21929,9 @@ msgstr ""
msgid "Time in seconds GitLab will wait for a response from the external service. When the service does not respond in time, access will be denied."
msgstr ""
+msgid "Time of import: %{importTime}"
+msgstr ""
+
msgid "Time remaining"
msgstr ""
@@ -21457,19 +21951,19 @@ msgid "Time until first merge request"
msgstr "Ð’Ñ€ÐµÐ¼Ñ Ð´Ð¾ первого запроÑа на ÑлиÑние"
msgid "TimeTrackingEstimated|Est"
-msgstr ""
+msgstr "Оцен."
msgid "TimeTracking|%{startTag}Spent: %{endTag}%{timeSpentHumanReadable}"
msgstr ""
msgid "TimeTracking|Estimated:"
-msgstr ""
+msgstr "Запланировано:"
msgid "TimeTracking|Over by %{timeRemainingHumanReadable}"
msgstr ""
msgid "TimeTracking|Spent"
-msgstr ""
+msgstr "Затрачено"
msgid "TimeTracking|Time remaining: %{timeRemainingHumanReadable}"
msgstr ""
@@ -21481,7 +21975,7 @@ msgid "Timeago|%s days remaining"
msgstr "ОÑталоÑÑŒ %s дней"
msgid "Timeago|%s hours ago"
-msgstr "%s чаÑов назад"
+msgstr "%s ч назад"
msgid "Timeago|%s hours remaining"
msgstr "ОÑталоÑÑŒ %s чаÑов"
@@ -21493,10 +21987,10 @@ msgid "Timeago|%s minutes remaining"
msgstr "ОÑталоÑÑŒ %s минут"
msgid "Timeago|%s months ago"
-msgstr "%s меÑÑцев назад"
+msgstr "%s меÑ. назад"
msgid "Timeago|%s months remaining"
-msgstr "ОÑталоÑÑŒ %s меÑÑцев"
+msgstr "ОÑталоÑÑŒ %s меÑ."
msgid "Timeago|%s seconds remaining"
msgstr "ОÑталоÑÑŒ %s Ñекунд(Ñ‹)"
@@ -21595,7 +22089,7 @@ msgid "Timeago|just now"
msgstr "Только что"
msgid "Timeago|right now"
-msgstr "ПрÑмо ÑейчаÑ"
+msgstr "прÑмо ÑейчаÑ"
msgid "Timeout"
msgstr ""
@@ -21626,8 +22120,8 @@ msgstr "Заголовок"
msgid "Title:"
msgstr "Заголовок:"
-msgid "Titles and Filenames"
-msgstr "Заголовки и имена файлов"
+msgid "Titles and Descriptions"
+msgstr ""
msgid "To"
msgstr ""
@@ -21645,7 +22139,7 @@ msgid "To access this domain create a new DNS record"
msgstr ""
msgid "To add an SSH key you need to %{generate_link_start}generate one%{link_end} or use an %{existing_link_start}existing key%{link_end}."
-msgstr "Чтобы добавить SSH-ключ вам нужно %{generate_link_start}Ñгенерировать его%{link_end} или иÑпользовать %{existing_link_start}ÑущеÑтвующий ключ%{link_end}."
+msgstr "Чтобы добавить ключ SSH, вам нужно %{generate_link_start}Ñгенерировать его%{link_end} или иÑпользовать %{existing_link_start}ÑущеÑтвующий ключ%{link_end}."
msgid "To add the entry manually, provide the following details to the application on your phone."
msgstr "Чтобы добавить запиÑÑŒ вручную, предоÑтавьте Ñледующую информацию в приложении на Ñвоем телефоне."
@@ -21660,7 +22154,7 @@ msgid "To connect GitHub repositories, you first need to authorize GitLab to acc
msgstr ""
msgid "To connect an SVN repository, check out %{svn_link}."
-msgstr ""
+msgstr "Чтобы подключить репозиторий SVN, проверьте %{svn_link}."
msgid "To define internal users, first enable new users set to external"
msgstr ""
@@ -21735,7 +22229,7 @@ msgid "To simplify the billing process, GitLab will collect user counts in order
msgstr ""
msgid "To specify the notification level per project of a group you belong to, you need to visit project page and change notification level there."
-msgstr ""
+msgstr "Чтобы указать уровень уведомлений по проекту группы, к которой вы принадлежите, необходимо поÑетить Ñтраницу проекта и изменить уровень уведомлений."
msgid "To start serving your jobs you can add Runners to your group"
msgstr "Чтобы начать выполнÑÑ‚ÑŒ Ñвои заданиÑ, вы можете добавить обработчик заданий в вашу группу"
@@ -21810,10 +22304,10 @@ msgid "Toggle thread"
msgstr ""
msgid "ToggleButton|Toggle Status: OFF"
-msgstr ""
+msgstr "Переключатель: Выкл."
msgid "ToggleButton|Toggle Status: ON"
-msgstr ""
+msgstr "Переключатель: Вкл."
msgid "Toggled :%{name}: emoji award."
msgstr ""
@@ -21828,13 +22322,13 @@ msgid "Too many changes to show."
msgstr "Слишком много изменений Ð´Ð»Ñ Ð¾Ñ‚Ð¾Ð±Ñ€Ð°Ð¶ÐµÐ½Ð¸Ñ."
msgid "Too many namespaces enabled. You will need to manage them via the console or the API."
-msgstr ""
+msgstr "Слишком много проÑтранÑтв имен включено. Ð”Ð»Ñ ÑƒÐ¿Ñ€Ð°Ð²Ð»ÐµÐ½Ð¸Ñ Ð¸Ð¼Ð¸ вам потребуетÑÑ ÐºÐ¾Ð½Ñоль или API."
msgid "Too many projects enabled. You will need to manage them via the console or the API."
-msgstr ""
+msgstr "Слишком много проектов включено. Ð”Ð»Ñ ÑƒÐ¿Ñ€Ð°Ð²Ð»ÐµÐ½Ð¸Ñ Ð¸Ð¼Ð¸ вам потребуетÑÑ ÐºÐ¾Ð½Ñоль или API."
-msgid "Topics"
-msgstr "Теги"
+msgid "Topics (optional)"
+msgstr ""
msgid "Total"
msgstr ""
@@ -21858,7 +22352,7 @@ msgid "Total test time for all commits/merges"
msgstr "Общее Ð²Ñ€ÐµÐ¼Ñ Ñ‚ÐµÑÑ‚Ð¸Ñ€Ð¾Ð²Ð°Ð½Ð¸Ñ Ñ„Ð¸ÐºÑаций/ÑлиÑний"
msgid "Total weight"
-msgstr ""
+msgstr "Итоговый приоритет"
msgid "Total: %{total}"
msgstr "Ð’Ñего: %{total}"
@@ -21888,19 +22382,19 @@ msgid "Transfer ownership"
msgstr ""
msgid "Transfer project"
-msgstr ""
+msgstr "ÐŸÐµÑ€ÐµÐ½Ð¾Ñ Ð¿Ñ€Ð¾ÐµÐºÑ‚Ð°"
msgid "TransferGroup|Cannot update the path because there are projects under this group that contain Docker images in their Container Registry. Please remove the images from your projects first and try again."
-msgstr ""
+msgstr "Ðевозможно обновить путь поÑкольку в Ñтой группе еÑÑ‚ÑŒ проекты, Ñодержащие образы Docker в Ñвоих рееÑтрах контейнеров. ПожалуйÑта, Ñначала удалите образы из Ñвоих проектов, а затем попробуйте Ñнова."
msgid "TransferGroup|Database is not supported."
-msgstr ""
+msgstr "База данных не поддерживаетÑÑ."
msgid "TransferGroup|Group contains projects with NPM packages."
-msgstr ""
+msgstr "Группа Ñодержит проекты Ñ Ð¿Ð°ÐºÐµÑ‚Ð°Ð¼Ð¸ NPM."
msgid "TransferGroup|Group is already a root group."
-msgstr ""
+msgstr "Группа уже ÑвлÑетÑÑ ÐºÐ¾Ñ€Ð½ÐµÐ²Ð¾Ð¹."
msgid "TransferGroup|Group is already associated to the parent group."
msgstr "Группа уже ÑвÑзана Ñ Ñ€Ð¾Ð´Ð¸Ñ‚ÐµÐ»ÑŒÑкой группой."
@@ -21921,13 +22415,13 @@ msgid "TransferProject|Please select a new namespace for your project."
msgstr "ПожалуйÑта, выберите новое проÑтранÑтво имен Ð´Ð»Ñ Ð²Ð°ÑˆÐµÐ³Ð¾ проекта."
msgid "TransferProject|Project cannot be transferred, because tags are present in its container registry"
-msgstr ""
+msgstr "Проект Ð½ÐµÐ»ÑŒÐ·Ñ Ð¿ÐµÑ€ÐµÐ½ÐµÑти, потому что в его рееÑтре контейнеров объÑвлены теги"
msgid "TransferProject|Project with same name or path in target namespace already exists"
msgstr "Проект Ñ Ñ‚Ð°ÐºÐ¸Ð¼ именем или путем в целевом проÑтранÑтве имен уже ÑущеÑтвует"
msgid "TransferProject|Root namespace can't be updated if project has NPM packages"
-msgstr ""
+msgstr "Корневое проÑтранÑтво имён не может быть обновлено, пока проект Ñодержит NPM пакеты"
msgid "TransferProject|Transfer failed, please contact an admin."
msgstr "ÐŸÐµÑ€ÐµÐ½Ð¾Ñ Ð½Ðµ удалÑÑ, пожалуйÑта, ÑвÑжитеÑÑŒ Ñ Ð°Ð´Ð¼Ð¸Ð½Ð¸Ñтратором."
@@ -21945,10 +22439,10 @@ msgid "Trials|Skip Trial (Continue with Free Account)"
msgstr ""
msgid "Trials|You can always resume this process by selecting your avatar and choosing 'Start a Gold trial'"
-msgstr ""
+msgstr "Ð’Ñ‹ вÑегда можете продолжить Ñтот процеÑÑ, нажав на Ñвой аватар и выбрав \"Ðачать пробный период Gold\""
msgid "Trials|You won't get a free trial right now but you can always resume this process by clicking on your avatar and choosing 'Start a free trial'"
-msgstr ""
+msgstr "Ð’Ñ‹ не получите беÑплатный пробный период прÑмо ÑейчаÑ, но вы вÑегда можете продолжить Ñтот процеÑÑ, нажав на Ñвой аватар и выбрав \"Ðачать беÑплатный пробный период\""
msgid "Trigger"
msgstr ""
@@ -21995,6 +22489,9 @@ msgstr ""
msgid "Try all GitLab has to offer for 30 days."
msgstr ""
+msgid "Try changing or removing filters."
+msgstr ""
+
msgid "Try to fork again"
msgstr ""
@@ -22062,7 +22559,7 @@ msgid "URL must start with %{codeStart}http://%{codeEnd}, %{codeStart}https://%{
msgstr ""
msgid "URL of the external storage that will serve the repository static objects (e.g. archives, blobs, ...)."
-msgstr ""
+msgstr "URL внешнего хранилища, которое будет обÑлуживать ÑтатичеÑкие объекты хранилища (например, архивы, бинарные данные, ...)."
msgid "URL or request ID"
msgstr ""
@@ -22083,11 +22580,14 @@ msgid "Unable to connect to Elasticsearch"
msgstr ""
msgid "Unable to connect to Prometheus server"
-msgstr ""
+msgstr "Ðевозможно подключитьÑÑ Ðº Ñерверу Prometheus"
msgid "Unable to connect to server: %{error}"
msgstr "Ðевозможно подключитьÑÑ Ðº Ñерверу:%{error}"
+msgid "Unable to connect to the Jira instance. Please check your Jira integration configuration."
+msgstr ""
+
msgid "Unable to convert Kubernetes logs encoding to UTF-8"
msgstr ""
@@ -22104,7 +22604,7 @@ msgid "Unable to generate new instance ID"
msgstr ""
msgid "Unable to load the diff"
-msgstr ""
+msgstr "Ðе удалоÑÑŒ загрузить отличиÑ"
msgid "Unable to load the diff. %{button_try_again}"
msgstr "Ðе удаетÑÑ Ð·Ð°Ð³Ñ€ÑƒÐ·Ð¸Ñ‚ÑŒ отличиÑ. %{button_try_again}"
@@ -22152,7 +22652,7 @@ msgid "Undo ignore"
msgstr ""
msgid "Unfortunately, your email message to GitLab could not be processed."
-msgstr ""
+msgstr "К Ñожалению, ваше пиÑьмо в GitLab не может быть обработано."
msgid "Uninstall"
msgstr "Удалить"
@@ -22305,7 +22805,7 @@ msgid "Update failed"
msgstr ""
msgid "Update failed. Please try again."
-msgstr "Обновление не удалоÑÑŒ. Попробуйте еще раз."
+msgstr "Обновление не удалоÑÑŒ. Попробуйте ещё раз."
msgid "Update it"
msgstr ""
@@ -22317,7 +22817,7 @@ msgid "Update variable"
msgstr ""
msgid "Update your bookmarked URLs as filtered/sorted branches URL has been changed."
-msgstr ""
+msgstr "Обновите ваши URL-адреÑа из закладок, поÑкольку отфильтрованные/отÑортированные ветки URL были изменены."
msgid "Update your group name, description, avatar, and visibility."
msgstr "Обновить наименование вашей группы, её опиÑание, аватар и видимоÑÑ‚ÑŒ."
@@ -22362,10 +22862,10 @@ msgid "Updating"
msgstr ""
msgid "Upgrade plan to unlock Canary Deployments feature"
-msgstr "ОбновитеÑÑŒ на более выÑокий тарифный план, чтобы разблокировать функциональноÑÑ‚ÑŒ Canary Deployments"
+msgstr "ОбновитеÑÑŒ на более выÑокий тарифный план, чтобы иÑпользовать развёртывание Canary"
msgid "Upgrade your plan"
-msgstr ""
+msgstr "Улучшите Ñвой план"
msgid "Upgrade your plan to activate Advanced Global Search."
msgstr "Обновите Вашу подпиÑку, чтобы активировать раÑширенный глобальный поиÑк."
@@ -22377,7 +22877,7 @@ msgid "Upgrade your plan to activate Contribution Analytics."
msgstr ""
msgid "Upgrade your plan to activate Group Webhooks."
-msgstr ""
+msgstr "Улучшите Ñвой тарифный план, чтобы иÑпользовать веб-обработчики на уровне группы."
msgid "Upgrade your plan to improve Issue boards."
msgstr ""
@@ -22431,28 +22931,28 @@ msgid "Upvotes"
msgstr "ГолоÑа \"за\""
msgid "Usage"
-msgstr ""
+msgstr "Ð¡ÐµÐ¹Ñ‡Ð°Ñ Ð¸ÑпользуетÑÑ"
msgid "Usage ping is not enabled"
msgstr ""
msgid "Usage quotas help link"
-msgstr ""
+msgstr "Справка по квотам иÑпользованиÑ"
msgid "Usage statistics"
msgstr "СтатиÑтика иÑпользованиÑ"
msgid "UsageQuota|%{help_link_start}Shared runners%{help_link_end} are disabled, so there are no limits set on pipeline usage"
-msgstr ""
+msgstr "%{help_link_start}Общие Runner'Ñ‹%{help_link_end} отключены, поÑтому нет наÑтроенных ограничений в иÑпользовании Ñборочных линий"
msgid "UsageQuota|Artifacts"
msgstr "Ðртефакты"
msgid "UsageQuota|Buy additional minutes"
-msgstr ""
+msgstr "Купить дополнительные минуты"
msgid "UsageQuota|Current period usage"
-msgstr ""
+msgstr "Текущий период иÑпользованиÑ"
msgid "UsageQuota|LFS Storage"
msgstr "Хранилище LFS"
@@ -22473,22 +22973,22 @@ msgid "UsageQuota|This namespace has no projects which use shared runners"
msgstr "Ð’ Ñтом проÑтранÑтве имен нет проектов, которые иÑпользуют общие обработчики заданий"
msgid "UsageQuota|Unlimited"
-msgstr ""
+msgstr "Ðе ограничена"
msgid "UsageQuota|Usage"
-msgstr ""
+msgstr "ИÑпользование"
msgid "UsageQuota|Usage Quotas"
-msgstr ""
+msgstr "Квоты иÑпользованиÑ"
msgid "UsageQuota|Usage of group resources across the projects in the %{strong_start}%{group_name}%{strong_end} group"
msgstr "ИÑпользование групповых реÑурÑов по проектам в группе %{strong_start}%{group_name}%{strong_end}"
msgid "UsageQuota|Usage of resources across your projects"
-msgstr ""
+msgstr "ИÑпользование реÑурÑов в ваших проектах"
msgid "UsageQuota|Usage since"
-msgstr ""
+msgstr "ИÑпользование Ñ"
msgid "UsageQuota|Wiki"
msgstr "Wiki"
@@ -22641,7 +23141,7 @@ msgid "UserOnboardingTour|Commits are shown in chronological order and can be fi
msgstr ""
msgid "UserOnboardingTour|Create a project"
-msgstr ""
+msgstr "Создайте проект"
msgid "UserOnboardingTour|Exit 'Learn GitLab'"
msgstr "Выйти из 'Изучить GitLab'"
@@ -22653,16 +23153,16 @@ msgid "UserOnboardingTour|Great job! %{clapHands} We hope the tour was helpful a
msgstr ""
msgid "UserOnboardingTour|Guided GitLab Tour"
-msgstr ""
+msgstr "ЭкÑкурÑÐ¸Ñ Ð¿Ð¾ GitLab"
msgid "UserOnboardingTour|Here you can compare the changes of this branch to another one. Changes are divided by files so that it's easier to see what was changed where."
msgstr ""
msgid "UserOnboardingTour|Here you can create a project from scratch, start with a template or import a repository from other platforms. Whatever you choose, we'll guide you through the process.%{lineBreak}%{lineBreak}Fill in your new project information and click on %{emphasisStart}Create Project%{emphasisEnd} to progress to the next step."
-msgstr ""
+msgstr "ЗдеÑÑŒ вы можете Ñоздать проект Ñ Ð½ÑƒÐ»Ñ, начать Ñ ÑˆÐ°Ð±Ð»Ð¾Ð½Ð° или импортировать репозиторий Ñ Ð´Ñ€ÑƒÐ³Ð¸Ñ… платформ. Что бы вы ни выбрали, мы будем Ñопровождать Ð²Ð°Ñ Ð² течение вÑего процеÑÑа.%{lineBreak}%{lineBreak}Заполните информацию о вашем новом проекте и нажмите %{emphasisStart}Создать проект%{emphasisEnd} Ð´Ð»Ñ Ð¿ÐµÑ€ÐµÑ…Ð¾Ð´Ð° к Ñледующему шагу."
msgid "UserOnboardingTour|Here you can see the breakdown of the pipelines: its stages and jobs in each of the stages and their status.%{lineBreak}%{lineBreak}Our CI/CD pipelines are quite complex, most of our users have fewer and simpler pipelines."
-msgstr ""
+msgstr "ЗдеÑÑŒ вы можете видеть разбивку Ñборочных линий: Ñтадии, Ð·Ð°Ð´Ð°Ð½Ð¸Ñ Ð½Ð° каждой из Ñтадий, а также их ÑоÑтоÑние.%{lineBreak}%{lineBreak}Ðаши ÑобÑтвенные Ñборочные линии CI/CD довольно Ñложны, у большинÑтва наших пользователей их меньше и они проще."
msgid "UserOnboardingTour|Here you can see the current members of the project (just you at the moment) and invite new members.%{lineBreak}%{lineBreak}You can invite multiple members at once (existing GitLab users or invite by email) and you can also set their roles and permissions.%{lineBreak}%{lineBreak}Add a few members and click on %{emphasisStart}Add to project%{emphasisEnd} to complete this step."
msgstr ""
@@ -22671,10 +23171,10 @@ msgid "UserOnboardingTour|Here you can see what changes were made with this comm
msgstr "ЗдеÑÑŒ вы можете увидеть какие Ð¸Ð·Ð¼ÐµÐ½ÐµÐ½Ð¸Ñ Ñодержит Ñтот коммит, в какой ветке, и еÑÑ‚ÑŒ ли ÑвÑзанные Ñ Ð½Ð¸Ð¼ запроÑÑ‹ на ÑлиÑние. СоÑтоÑние Ñборочной линии также будет отображатьÑÑ, еÑли наÑтроены CI/CD. %{lineBreak}%{lineBreak} Ещё вы можете прокомментировать Ñтроки кода, которые были изменены, и начать диÑкуÑÑию Ñо Ñвоими коллегами!"
msgid "UserOnboardingTour|Here's an overview of branches in the %{emphasisStart}%{projectName}%{emphasisEnd} project. They're split into Active and Stale.%{lineBreak}%{lineBreak}From here, you can create a new merge request from a branch, or compare the branch to any other branch in the project. By default, it will compare it to the master branch."
-msgstr ""
+msgstr "Это обзор ветвей проекта %{emphasisStart}%{projectName}%{emphasisEnd}. Они разделены на активные и уÑтаревшие.%{lineBreak}%{lineBreak}ЗдеÑÑŒ вы можете Ñоздать новый Ð·Ð°Ð¿Ñ€Ð¾Ñ Ð½Ð° ÑлиÑние или Ñравнить одну ветку проекта Ñ Ð´Ñ€ÑƒÐ³Ð¾Ð¹. По умолчанию Ñравнение будет производитьÑÑ Ñ Ð²ÐµÑ‚ÐºÐ¾Ð¹ master."
msgid "UserOnboardingTour|Invite colleagues"
-msgstr ""
+msgstr "ПриглаÑить коллег"
msgid "UserOnboardingTour|Issues are great for communicating and keeping track of progress in GitLab. These are all issues that are open in the %{emphasisStart}%{projectName}%{emphasisEnd}.%{lineBreak}%{lineBreak}You can help us improve GitLab by contributing work to issues that are labeled <span class=\"badge color-label accept-mr-label\">Accepting merge requests</span>.%{lineBreak}%{lineBreak}This list can be filtered by labels, milestones, assignees, authors... We'll show you how it looks when the list is filtered by a label."
msgstr "ОбÑÑƒÐ¶Ð´ÐµÐ½Ð¸Ñ Ð² GitLab отлично подходÑÑ‚ Ð´Ð»Ñ ÐºÐ¾Ð¼Ð¼ÑƒÐ½Ð¸ÐºÐ°Ñ†Ð¸Ð¸ и отÑÐ»ÐµÐ¶Ð¸Ð²Ð°Ð½Ð¸Ñ Ð¿Ñ€Ð¾Ð³Ñ€ÐµÑÑа. Перед вами вÑе обÑуждениÑ, открытые в %{emphasisStart}%{projectName}%{emphasisEnd}.%{lineBreak}%{lineBreak}Ð’Ñ‹ можете помочь нам улучшить GitLab, принÑв учаÑтие в работе над обÑуждениÑми Ñ Ð¼ÐµÑ‚ÐºÐ¾Ð¹ <span class=\"badge color-label accept-mr-label\">Accepting merge requests</span> (принимаютÑÑ Ð·Ð°Ð¿Ñ€Ð¾ÑÑ‹ на ÑлиÑние).%{lineBreak}%{lineBreak}Этот ÑпиÑок может быть отфильтрован по меткам, Ñтапам, иÑпонителÑм, авторам... Мы покажем вам, как выглÑдит отфильтрованный по метке ÑпиÑок."
@@ -22692,10 +23192,10 @@ msgid "UserOnboardingTour|Let's take a closer look at the repository of this pro
msgstr ""
msgid "UserOnboardingTour|No thanks"
-msgstr ""
+msgstr "Ðет, ÑпаÑибо"
msgid "UserOnboardingTour|Ok, let's go"
-msgstr ""
+msgstr "Хорошо, поехали"
msgid "UserOnboardingTour|Ok, show me"
msgstr ""
@@ -22734,7 +23234,7 @@ msgid "UserOnboardingTour|The structure of this page is very similar to issues.
msgstr ""
msgid "UserOnboardingTour|There's a lot of information here but don't worry, we'll go through it.%{lineBreak}%{lineBreak}On the top you can see the status of the issue and when it was opened and by whom. Directly below it is the issue description and below that are other %{emphasisStart}related issues%{emphasisEnd} and %{emphasisStart}merge requests%{emphasisEnd} (if any). Then below that is the %{emphasisStart}discussion%{emphasisEnd}, that's where most of the communication happens.%{lineBreak}%{lineBreak}On the right, there's a sidebar where you can view/change the %{emphasisStart}assignee, milestone, due date, labels, weight%{emphasisEnd}, etc."
-msgstr "Информации много, но не волнуйтеÑÑŒ, мы через Ñто пройдем.%{lineBreak}%{lineBreak}Вверху вы можете видеть ÑоÑтоÑние обÑуждениÑ, когда оно было открыто и кем. ÐепоÑредÑтвенно под ним раÑположено опиÑание обÑуждениÑ, а еще ниже — другие %{emphasisStart}ÑвÑзанные обÑуждениÑ%{emphasisEnd} и %{emphasisStart}запроÑÑ‹ на ÑлиÑние%{emphasisEnd} (при наличии). Далее находитÑÑ %{emphasisStart}диÑкуÑÑиÑ%{emphasisEnd}, где проиÑходит Ð±Ð¾Ð»ÑŒÑˆÐ°Ñ Ñ‡Ð°ÑÑ‚ÑŒ общениÑ.%{lineBreak}%{lineBreak}Справа — Ð±Ð¾ÐºÐ¾Ð²Ð°Ñ Ð¿Ð°Ð½ÐµÐ»ÑŒ, где вы можете проÑмотреть / изменить %{emphasisStart}назначенных иÑполнителей, Ñтап, Ñрок выполнениÑ, метки, веÑ%{emphasisEnd} и Ñ‚. д."
+msgstr "Информации много, но не волнуйтеÑÑŒ, мы через Ñто пройдем.%{lineBreak}%{lineBreak}Вверху вы можете видеть ÑоÑтоÑние обÑуждениÑ, когда оно было открыто и кем. ÐепоÑредÑтвенно под ним раÑположено опиÑание обÑуждениÑ, а еще ниже — другие %{emphasisStart}ÑвÑзанные обÑуждениÑ%{emphasisEnd} и %{emphasisStart}запроÑÑ‹ на ÑлиÑние%{emphasisEnd} (при наличии). Далее находитÑÑ %{emphasisStart}диÑкуÑÑиÑ%{emphasisEnd}, где проиÑходит Ð±Ð¾Ð»ÑŒÑˆÐ°Ñ Ñ‡Ð°ÑÑ‚ÑŒ общениÑ.%{lineBreak}%{lineBreak}Справа — Ð±Ð¾ÐºÐ¾Ð²Ð°Ñ Ð¿Ð°Ð½ÐµÐ»ÑŒ, где вы можете проÑмотреть / изменить %{emphasisStart}назначенных иÑполнителей, Ñтап, Ñрок выполнениÑ, метки, приоритет%{emphasisEnd} и Ñ‚. д."
msgid "UserOnboardingTour|These are all the CI/CD pipelines we have for our %{emphasisStart}%{projectName}%{emphasisEnd} project.%{lineBreak}%{lineBreak}Here you can see the status of each pipeline, for what commit it's running for, its stages and the status for them."
msgstr ""
@@ -22812,7 +23312,7 @@ msgid "UserProfile|This user doesn't have any personal projects"
msgstr "У Ñтого Ð¿Ð¾Ð»ÑŒÐ·Ð¾Ð²Ð°Ñ‚ÐµÐ»Ñ Ð½ÐµÑ‚ личных проектов"
msgid "UserProfile|This user has a private profile"
-msgstr "У Ñтого Ð¿Ð¾Ð»ÑŒÐ·Ð¾Ð²Ð°Ñ‚ÐµÐ»Ñ Ð¿Ñ€Ð¸Ð²Ð°Ñ‚Ð½Ñ‹Ð¹ профиль"
+msgstr "У Ñтого Ð¿Ð¾Ð»ÑŒÐ·Ð¾Ð²Ð°Ñ‚ÐµÐ»Ñ Ð·Ð°ÐºÑ€Ñ‹Ñ‚Ñ‹Ð¹ профиль"
msgid "UserProfile|This user hasn't contributed to any projects"
msgstr ""
@@ -22839,7 +23339,7 @@ msgid "UserProfile|You haven't created any snippets."
msgstr "Ð’Ñ‹ не Ñоздали ни одного Ñниппета."
msgid "UserProfile|Your projects can be available publicly, internally, or privately, at your choice."
-msgstr ""
+msgstr "Ðа ваш выбор проекты могут быть публичными, внутренними или личными."
msgid "Username (optional)"
msgstr "Ð˜Ð¼Ñ Ð¿Ð¾Ð»ÑŒÐ·Ð¾Ð²Ð°Ñ‚ÐµÐ»Ñ (необÑзательно)"
@@ -22881,19 +23381,19 @@ msgid "Users with a Guest role or those who don't belong to any projects or grou
msgstr ""
msgid "UsersSelect|%{name} + %{length} more"
-msgstr ""
+msgstr "еще %{name} + %{length}"
msgid "UsersSelect|Any User"
-msgstr ""
+msgstr "Любой пользователь"
msgid "UsersSelect|Assignee"
-msgstr ""
+msgstr "Ðазначенный"
msgid "UsersSelect|No assignee - %{openingTag} assign yourself %{closingTag}"
-msgstr ""
+msgstr "Ðет назначенного - %{openingTag} назначить ÑÐµÐ±Ñ %{closingTag}"
msgid "UsersSelect|Unassigned"
-msgstr ""
+msgstr "Ðе назначено"
msgid "Using %{code_start}::%{code_end} denotes a %{link_start}scoped label set%{link_end}"
msgstr ""
@@ -22959,7 +23459,7 @@ msgid "Verification status"
msgstr ""
msgid "Verified"
-msgstr "Проверено"
+msgstr "Подтверждена"
msgid "Verify SAML Configuration"
msgstr ""
@@ -22993,7 +23493,7 @@ msgid "View dependency details for your project"
msgstr ""
msgid "View deployment"
-msgstr ""
+msgstr "ПроÑмотр развёртываниÑ"
msgid "View details"
msgstr ""
@@ -23021,7 +23521,7 @@ msgid "View file @ "
msgstr "ПроÑмотр файла @ "
msgid "View full dashboard"
-msgstr ""
+msgstr "Показать вÑÑŽ панель"
msgid "View full log"
msgstr ""
@@ -23035,6 +23535,9 @@ msgstr ""
msgid "View issue"
msgstr ""
+msgid "View issues"
+msgstr ""
+
msgid "View it on GitLab"
msgstr ""
@@ -23057,7 +23560,7 @@ msgid "View open merge request"
msgstr "ПроÑмотреть открытый Ð·Ð°Ð¿Ñ€Ð¾Ñ Ð½Ð° ÑлиÑние"
msgid "View performance dashboard."
-msgstr ""
+msgstr "ПроÑмотр панели производительноÑти."
msgid "View project"
msgstr ""
@@ -23069,13 +23572,13 @@ msgid "View replaced file @ "
msgstr "ПроÑмотр заменённого файла @ "
msgid "View supported languages and frameworks"
-msgstr ""
+msgstr "ПроÑмотр поддерживаемых Ñзыков и фреймворков"
msgid "View the documentation"
msgstr ""
msgid "View the latest successful deployment to this environment"
-msgstr ""
+msgstr "ПроÑмотр поÑледнего уÑпешного Ñ€Ð°Ð·Ð²Ñ‘Ñ€Ñ‚Ñ‹Ð²Ð°Ð½Ð¸Ñ Ð² Ñто окружение"
msgid "View the performance dashboard at"
msgstr ""
@@ -23132,7 +23635,7 @@ msgid "VisualReviewApp|Cancel"
msgstr ""
msgid "VisualReviewApp|Copy merge request ID"
-msgstr ""
+msgstr "Скопировать ID запроÑа на ÑлиÑние"
msgid "VisualReviewApp|Copy script"
msgstr "Копировать Ñкрипт"
@@ -23180,7 +23683,7 @@ msgid "VulnerabilityChart|%{formattedStartDate} to today"
msgstr ""
msgid "VulnerabilityChart|Severity"
-msgstr ""
+msgstr "СерьёзноÑÑ‚ÑŒ"
msgid "VulnerabilityManagement|A true-positive and will fix"
msgstr ""
@@ -23212,6 +23715,15 @@ msgstr ""
msgid "VulnerabilityManagement|Resolved %{timeago} by %{user}"
msgstr ""
+msgid "VulnerabilityManagement|Something went wrong while trying to delete the comment. Please try again later."
+msgstr ""
+
+msgid "VulnerabilityManagement|Something went wrong while trying to retrieve the vulnerability history. Please try again later."
+msgstr ""
+
+msgid "VulnerabilityManagement|Something went wrong while trying to save the comment. Please try again later."
+msgstr ""
+
msgid "VulnerabilityManagement|Something went wrong, could not create an issue."
msgstr ""
@@ -23242,6 +23754,9 @@ msgstr ""
msgid "VulnerabilityStatusTypes|Resolved"
msgstr ""
+msgid "Vulnerability|%{scannerName} (version %{scannerVersion})"
+msgstr ""
+
msgid "Vulnerability|Class"
msgstr ""
@@ -23258,7 +23773,7 @@ msgid "Vulnerability|Identifiers"
msgstr ""
msgid "Vulnerability|Image"
-msgstr ""
+msgstr "Образ"
msgid "Vulnerability|Instances"
msgstr ""
@@ -23275,8 +23790,11 @@ msgstr "ПроÑтранÑтво имён"
msgid "Vulnerability|Project"
msgstr ""
-msgid "Vulnerability|Report Type"
-msgstr "Тип отчета"
+msgid "Vulnerability|Scanner Provider"
+msgstr ""
+
+msgid "Vulnerability|Scanner Type"
+msgstr ""
msgid "Vulnerability|Severity"
msgstr ""
@@ -23303,7 +23821,7 @@ msgid "Warning: Displaying this diagram might cause performance issues on this p
msgstr ""
msgid "We could not determine the path to remove the epic"
-msgstr ""
+msgstr "Мы не Ñмогли раÑпознать путь Ð´Ð»Ñ ÑƒÐ´Ð°Ð»ÐµÐ½Ð¸Ñ Ñ†ÐµÐ»Ð¸"
msgid "We could not determine the path to remove the issue"
msgstr "Мы не можем определить путь Ð´Ð»Ñ ÑƒÐ´Ð°Ð»ÐµÐ½Ð¸Ñ Ð¾Ð±ÑуждениÑ"
@@ -23329,9 +23847,15 @@ msgstr "Мы получили ответ от вашего уÑтройÑтва
msgid "We sent you an email with reset password instructions"
msgstr ""
+msgid "We tried to automatically renew your %{strong}%{plan_name}%{strong_close} subscription for %{strong}%{namespace_name}%{strong_close} on %{expires_on} but something went wrong so your subscription was downgraded to the free plan. Don't worry, your data is safe. We suggest you check your payment method and get in touch with our support team (%{support_link}). They'll gladly help with your subscription renewal."
+msgstr ""
+
msgid "We want to be sure it is you, please confirm you are not a robot."
msgstr "Мы хотим быть уверены, что Ñто вы, пожалуйÑта, подтвердите, что вы не робот."
+msgid "We will automatically renew your %{strong}%{plan_name}%{strong_close} subscription for %{strong}%{namespace_name}%{strong_close} on %{strong}%{expires_on}%{strong_close}. There's nothing that you need to do, we'll let you know when the renewal is complete. Need more seats, a higher plan or just want to review your payment method?"
+msgstr ""
+
msgid "We've found no vulnerabilities"
msgstr ""
@@ -23345,28 +23869,28 @@ msgid "Web terminal"
msgstr "Web терминал"
msgid "WebIDE|Merge request"
-msgstr ""
+msgstr "Ð—Ð°Ð¿Ñ€Ð¾Ñ Ð½Ð° ÑлиÑние"
msgid "Webhook"
-msgstr ""
+msgstr "Веб-обработчик"
msgid "Webhook Logs"
-msgstr ""
+msgstr "Журналы веб-обработчика"
msgid "Webhook Settings"
-msgstr ""
+msgstr "ÐаÑтройки веб-обработчика"
msgid "Webhooks"
-msgstr ""
+msgstr "Веб-обработчики"
msgid "Webhooks Help"
-msgstr ""
+msgstr "Справка по веб-обработчикам"
msgid "Webhooks allow you to trigger a URL if, for example, new code is pushed or a new issue is created. You can configure webhooks to listen for specific events like pushes, issues or merge requests. Group webhooks will apply to all projects in a group, allowing you to standardize webhook functionality across your entire group."
msgstr "Веб-обработчики позволÑÑŽÑ‚ вам вызывать Ð°Ð´Ñ€ÐµÑ URL еÑли, например, отправлен новый код или Ñоздано новое обÑуждение. Ð’Ñ‹ можете наÑтроить веб-обработчики так, чтобы они реагировали на определённые ÑобытиÑ, такие как отправки кода, обÑÑƒÐ¶Ð´ÐµÐ½Ð¸Ñ Ð¸Ð»Ð¸ запроÑÑ‹ на ÑлиÑние. Групповые веб-обработчики применÑÑŽÑ‚ÑÑ ÐºÐ¾ вÑем проектам в группе и позволÑÑŽÑ‚ вам Ñтандартизовать функциональноÑÑ‚ÑŒ веб-обработчиков Ð´Ð»Ñ Ð²Ñей вашей группы."
msgid "Webhooks have moved. They can now be found under the Settings menu."
-msgstr ""
+msgstr "Веб-обработчики были перемещены. Теперь их можно найти под меню наÑтроек."
msgid "Wednesday"
msgstr "Среда"
@@ -23378,13 +23902,13 @@ msgid "Weeks"
msgstr ""
msgid "Weight"
-msgstr "ВеÑ"
+msgstr "Приоритет"
msgid "Weight %{weight}"
-msgstr "Ð’ÐµÑ %{weight}"
+msgstr "Приоритет %{weight}"
msgid "Welcome back! Your account had been deactivated due to inactivity but is now reactivated."
-msgstr "Добро пожаловать! Ваша ÑƒÑ‡ÐµÑ‚Ð½Ð°Ñ Ð·Ð°Ð¿Ð¸ÑÑŒ была деактивирована из-за неактивноÑти, но ÑÐµÐ¹Ñ‡Ð°Ñ Ð²Ð¾ÑÑтановлена."
+msgstr "Добро пожаловать! Ваша ÑƒÑ‡Ñ‘Ñ‚Ð½Ð°Ñ Ð·Ð°Ð¿Ð¸ÑÑŒ была деактивирована из-за неактивноÑти, но ÑÐµÐ¹Ñ‡Ð°Ñ Ð²Ð¾ÑÑтановлена."
msgid "Welcome to GitLab"
msgstr ""
@@ -23396,7 +23920,7 @@ msgid "Welcome to GitLab, %{first_name}!"
msgstr ""
msgid "Welcome to GitLab.com<br>@%{name}!"
-msgstr ""
+msgstr "Добро пожаловать в GitLab.com<br>@%{name}!"
msgid "Welcome to the Guided GitLab Tour"
msgstr ""
@@ -23438,15 +23962,6 @@ msgstr "ИÑÐ¿Ð¾Ð»ÑŒÐ·ÑƒÑ <code>http://</code> или <code>https://</code> пÑ
msgid "When:"
msgstr ""
-msgid "While it's rare to have no vulnerabilities for your group, it can happen. In any event, we ask that you double check your settings to make sure you've set up your dashboard correctly."
-msgstr ""
-
-msgid "While it's rare to have no vulnerabilities for your pipeline, it can happen. In any event, we ask that you double check your settings to make sure all security scanning jobs have passed successfully."
-msgstr ""
-
-msgid "While it's rare to have no vulnerabilities for your project, it can happen. In any event, we ask that you double check your settings to make sure you've set up your dashboard correctly."
-msgstr ""
-
msgid "While it's rare to have no vulnerabilities, it can happen. In any event, we ask that you please double check your settings to make sure you've set up your dashboard correctly."
msgstr ""
@@ -23477,9 +23992,6 @@ msgstr ""
msgid "Wiki"
msgstr "Wiki"
-msgid "Wiki pages"
-msgstr "Wiki Ñтраницы"
-
msgid "Wiki was successfully updated."
msgstr "Wiki была уÑпешно обновлена."
@@ -23499,10 +24011,10 @@ msgid "WikiClone|Start Gollum and edit locally"
msgstr "ЗапуÑтите Gollum и редактируете локально"
msgid "WikiEditPageTip|Tip: You can move this page by adding the path to the beginning of the title."
-msgstr ""
+msgstr "Совет: вы можете перемеÑтить Ñту Ñтраницу, добавив путь к началу заголовка."
msgid "WikiEdit|There is already a page with the same title in that path."
-msgstr ""
+msgstr "По Ñтому пути уже еÑÑ‚ÑŒ Ñтраница Ñ Ñ‚Ð°ÐºÐ¸Ð¼ названием."
msgid "WikiEmptyIssueMessage|Suggest wiki improvement"
msgstr ""
@@ -23559,16 +24071,16 @@ msgid "WikiPageConfirmDelete|Are you sure you want to delete this page?"
msgstr "Ð’Ñ‹ уверены, что вы хотите удалить Ñту Ñтраницу?"
msgid "WikiPageConfirmDelete|Delete page"
-msgstr ""
+msgstr "Удалить Ñтраницу"
msgid "WikiPageConfirmDelete|Delete page %{pageTitle}?"
-msgstr ""
+msgstr "Удалить Ñтраницу %{pageTitle}?"
msgid "WikiPageConflictMessage|Someone edited the page the same time you did. Please check out %{page_link} and make sure your changes will not unintentionally remove theirs."
msgstr "Кто-то редактирует Ñтраницу одновременно Ñ Ð²Ð°Ð¼Ð¸. ПожалуйÑта проверьте %{page_link} и убедитеÑÑŒ, что внеÑенные Вами Ð¸Ð·Ð¼ÐµÐ½ÐµÐ½Ð¸Ñ Ð½Ðµ затрут чужие."
msgid "WikiPageConflictMessage|the page"
-msgstr "Ñтраница"
+msgstr "WikiСтраницаСообщениÑПриКонфликте | Ñтраницы"
msgid "WikiPageCreate|Create %{pageTitle}"
msgstr ""
@@ -23663,6 +24175,9 @@ msgstr ""
msgid "Yes, add it"
msgstr ""
+msgid "Yes, close issue"
+msgstr ""
+
msgid "Yes, let me map Google Code users to full names or GitLab users."
msgstr ""
@@ -23676,7 +24191,7 @@ msgid "You are about to delete %{domain} from your instance. This domain will no
msgstr ""
msgid "You are about to transfer the control of your account to %{group_name} group. This action is NOT reversible, you won't be able to access any of your groups and projects outside of %{group_name} once this transfer is complete."
-msgstr ""
+msgstr "Ð’Ñ‹ ÑобираетеÑÑŒ передать контроль над вашим аккаунтом группе %{group_name}. Это ÐЕОБРÐТИМО, вы не будете иметь доÑтуп ко вÑем группам и проектам %{group_name}, как только передача будет завершена."
msgid "You are an admin, which means granting access to <strong>%{client_name}</strong> will allow them to interact with GitLab as an admin as well. Proceed with caution."
msgstr ""
@@ -23688,7 +24203,7 @@ msgid "You are attempting to update a file that has changed since you started ed
msgstr "Ð’Ñ‹ пытаетеÑÑŒ обновить файл, который изменилÑÑ Ñ Ñ‚ÐµÑ… пор, как вы начали его редактировать."
msgid "You are connected to the Prometheus server, but there is currently no data to display."
-msgstr ""
+msgstr "Ð’Ñ‹ подключены к Ñерверу Prometheus, но в наÑтоÑщее Ð²Ñ€ÐµÐ¼Ñ Ð½ÐµÑ‚ данных Ð´Ð»Ñ Ð¾Ñ‚Ð¾Ð±Ñ€Ð°Ð¶ÐµÐ½Ð¸Ñ."
msgid "You are going to remove %{group_name}, this will also remove all of its subgroups and projects. Removed groups CANNOT be restored! Are you ABSOLUTELY sure?"
msgstr "Ð’Ñ‹ ÑобираетеÑÑŒ удалить %{group_name}, при Ñтом также удалитÑÑ Ð²Ñе подгруппы и проекты. Удаленные группы ÐЕ МОГУТ быть воÑÑтановлены! Ð’Ñ‹ ÐБСОЛЮТÐО уверены?"
@@ -23724,7 +24239,7 @@ msgid "You are trying to upload something other than an image. Please upload a .
msgstr ""
msgid "You can %{linkStart}view the blob%{linkEnd} instead."
-msgstr ""
+msgstr "ВмеÑто Ñтого вы можете %{linkStart}проÑмотреть бинарные данные%{linkEnd}."
msgid "You can also create a project from the command line."
msgstr "Ð’Ñ‹ также можете Ñоздать проект из командной Ñтроки."
@@ -23739,7 +24254,7 @@ msgid "You can also star a label to make it a priority label."
msgstr ""
msgid "You can also test your %{gitlab_ci_yml} in %{lint_link_start}CI Lint%{lint_link_end}"
-msgstr ""
+msgstr "Вы также можете проверить ваш %{gitlab_ci_yml} в %{lint_link_start}CI Lint%{lint_link_end}"
msgid "You can also upload existing files from your computer using the instructions below."
msgstr "Ð’Ñ‹ также можете загрузить ÑущеÑтвующие файлы Ñ ÐºÐ¾Ð¼Ð¿ÑŒÑŽÑ‚ÐµÑ€Ð°, иÑÐ¿Ð¾Ð»ÑŒÐ·ÑƒÑ Ð¿Ñ€Ð¸Ð²ÐµÐ´Ñ‘Ð½Ð½Ñ‹Ðµ ниже инÑтрукции."
@@ -23763,7 +24278,7 @@ msgid "You can create new ones at your Personal Access Tokens settings %{pat_lin
msgstr ""
msgid "You can easily contribute to them by requesting to join these groups."
-msgstr "Ð’Ñ‹ можете легко внеÑти Ñвой вклад в них, попроÑив приÑоединить Ð²Ð°Ñ Ðº Ñтим группам."
+msgstr "Ð’Ñ‹ можете легко поучаÑтвовать в них, попроÑив приÑоединитьÑÑ."
msgid "You can easily install a Runner on a Kubernetes cluster. %{link_to_help_page}"
msgstr "Ð’Ñ‹ можете легко уÑтановить Runner в клаÑтер Kubernetes. %{link_to_help_page}"
@@ -23829,7 +24344,7 @@ msgid "You can specify notification level per group or per project."
msgstr "Вы можете указать уровень уведомлений в каждой группе или в проекте."
msgid "You can test your .gitlab-ci.yml in %{linkStart}CI Lint%{linkEnd}."
-msgstr ""
+msgstr "Ð’Ñ‹ можете протеÑтировать Ñвой файл .gitlab-ci.yml Ñ Ð¿Ð¾Ð¼Ð¾Ñ‰ÑŒÑŽ %{linkStart}CI Lint%{linkEnd}."
msgid "You can try again using %{begin_link}basic search%{end_link}"
msgstr ""
@@ -23844,7 +24359,7 @@ msgid "You cannot impersonate a user who cannot log in"
msgstr ""
msgid "You cannot impersonate an internal user"
-msgstr ""
+msgstr "Ð’Ñ‹ не можете выдать ÑÐµÐ±Ñ Ð·Ð° внутреннего пользователÑ"
msgid "You cannot play this scheduled pipeline at the moment. Please wait a minute."
msgstr ""
@@ -23858,6 +24373,9 @@ msgstr "Ð’Ñ‹ не можете запиÑывать на Ñтот Ñкземпл
msgid "You could not create a new trigger."
msgstr ""
+msgid "You didn't renew your %{strong}%{plan_name}%{strong_close} subscription for %{strong}%{namespace_name}%{strong_close} so it was downgraded to the free plan."
+msgstr ""
+
msgid "You didn't renew your %{strong}%{plan_name}%{strong_close} subscription so it was downgraded to the GitLab Core Plan."
msgstr ""
@@ -23874,13 +24392,13 @@ msgid "You do not have permissions to run the import."
msgstr ""
msgid "You do not have the correct permissions to override the settings from the LDAP group sync."
-msgstr ""
+msgstr "У Ð²Ð°Ñ Ð½ÐµÑ‚ необходимых прав Ð´Ð»Ñ Ð¿ÐµÑ€ÐµÐ¾Ð¿Ñ€ÐµÐ´ÐµÐ»ÐµÐ½Ð¸Ñ Ð¿Ð°Ñ€Ð°Ð¼ÐµÑ‚Ñ€Ð¾Ð² из групповой Ñинхронизации LDAP."
msgid "You don't have any U2F devices registered yet."
-msgstr "У Ð²Ð°Ñ ÐµÑ‰Ðµ нет зарегиÑтрированных U2F уÑтройÑтв."
+msgstr "У Ð²Ð°Ñ ÐµÑ‰Ñ‘ нет зарегиÑтрированных уÑтройÑтв U2F."
msgid "You don't have any active chat names."
-msgstr ""
+msgstr "У Ð²Ð°Ñ Ð½ÐµÑ‚ активных имен в чатах."
msgid "You don't have any applications"
msgstr ""
@@ -23889,7 +24407,7 @@ msgid "You don't have any authorized applications"
msgstr ""
msgid "You don't have any deployments right now."
-msgstr ""
+msgstr "Ðа текущий момент у Ð²Ð°Ñ Ð½ÐµÑ‚ развёртываний."
msgid "You don't have any open merge requests"
msgstr ""
@@ -23924,6 +24442,9 @@ msgstr ""
msgid "You have declined the invitation to join %{label}."
msgstr ""
+msgid "You have imported from this project %{numberOfPreviousImportsForProject} times before. Each new import will create duplicate issues."
+msgstr ""
+
msgid "You have no permissions"
msgstr ""
@@ -23934,7 +24455,7 @@ msgid "You have reached your project limit"
msgstr "Ð’Ñ‹ доÑтигли Ð¾Ð³Ñ€Ð°Ð½Ð¸Ñ‡ÐµÐ½Ð¸Ñ Ð² вашем проекте"
msgid "You have successfully purchased a %{plan} plan subscription for %{seats}. You’ll receive a receipt via email."
-msgstr ""
+msgstr "Ð’Ñ‹ уÑпешно приобрели подпиÑку на план %{plan} на %{seats} меÑÑ‚. Ð’Ñ‹ получите чек по Ñлектронной почте."
msgid "You haven't added any issues to your project yet"
msgstr ""
@@ -23964,16 +24485,16 @@ msgid "You must have permission to create a project in a namespace before forkin
msgstr ""
msgid "You must provide a valid current password"
-msgstr ""
+msgstr "Ð’Ñ‹ должны ввеÑти правильный текущий пароль"
msgid "You must provide your current password in order to change it."
-msgstr ""
+msgstr "Ð”Ð»Ñ Ð¸Ð·Ð¼ÐµÐ½ÐµÐ½Ð¸Ñ Ð¿Ð°Ñ€Ð¾Ð»Ñ Ð½ÐµÐ¾Ð±Ñ…Ð¾Ð´Ð¸Ð¼Ð¾ ввеÑти текущий пароль."
msgid "You must select a stack for configuring your cloud provider. Learn more about"
msgstr ""
msgid "You must set up incoming email before it becomes active."
-msgstr ""
+msgstr "Ð’Ñ‹ должны наÑтроить входÑщую почту, прежде чем она Ñтанет активной."
msgid "You must upload a file with the same file name when dropping onto an existing design."
msgstr ""
@@ -23988,7 +24509,7 @@ msgid "You need permission."
msgstr "Вам нужно разрешение."
msgid "You need to be logged in."
-msgstr ""
+msgstr "Вы должны быть авторизованы."
msgid "You need to register a two-factor authentication app before you can set up a U2F device."
msgstr "Вам необходимо зарегиÑтрировать приложение Ð´Ð»Ñ Ð´Ð²ÑƒÑ…Ñ„Ð°ÐºÑ‚Ð¾Ñ€Ð½Ð¾Ð¹ аутентификации, прежде чем вы Ñможете наÑтроить уÑтройÑтво U2F."
@@ -23997,7 +24518,7 @@ msgid "You need to set terms to be enforced"
msgstr ""
msgid "You need to specify both an Access Token and a Host URL."
-msgstr ""
+msgstr "Вам необходимо указать и токен доÑтупа, так и URL хоÑта."
msgid "You need to upload a GitLab project export archive (ending in .gz)."
msgstr "Вам необходимо загрузить ÑкÑпортированный архив проекта GitLab (заканчивающийÑÑ Ð½Ð° .gz)."
@@ -24005,6 +24526,9 @@ msgstr "Вам необходимо загрузить ÑкÑпортироваÐ
msgid "You need to upload a Google Takeout archive."
msgstr ""
+msgid "You reached %{usage_in_percent} of %{namespace_name}'s capacity (%{used_storage} of %{storage_limit})"
+msgstr ""
+
msgid "You tried to fork %{link_to_the_project} but it failed for the following reason:"
msgstr ""
@@ -24021,7 +24545,7 @@ msgid "You will lose all uncommitted changes you've made in this project. This a
msgstr ""
msgid "You will need to update your local repositories to point to the new location."
-msgstr ""
+msgstr "Вам нужно будет обновить ваши локальные репозитории, чтобы указать новое меÑтоположение."
msgid "You will not get any notifications via email"
msgstr "Ð’Ñ‹ не получите никаких уведомлений по Ñлектронной почте"
@@ -24042,7 +24566,7 @@ msgid "You won't be able to pull or push project code via %{protocol} until you
msgstr "Ð’Ñ‹ не Ñможете получать и отправлÑÑ‚ÑŒ код в данный проект через %{protocol} пока не %{set_password_link} в вашей учетной запиÑи"
msgid "You won't be able to pull or push project code via SSH until you add an SSH key to your profile"
-msgstr "Ð’Ñ‹ не Ñможете работать Ñ Ð¿Ñ€Ð¾ÐµÐºÑ‚Ð¾Ð¼ через SSH, пока не добавите в Ñвой профиль SSH ключ"
+msgstr "Ð’Ñ‹ не Ñможете работать Ñ Ð¿Ñ€Ð¾ÐµÐºÑ‚Ð¾Ð¼ через SSH, пока не добавите в Ñвой профиль ключ SSH"
msgid "You'll be signed out from your current account automatically."
msgstr ""
@@ -24051,7 +24575,7 @@ msgid "You'll need to use different branch names to get a valid comparison."
msgstr "Ð”Ð»Ñ Ð¿Ð¾Ð»ÑƒÑ‡ÐµÐ½Ð¸Ñ ÐºÐ¾Ñ€Ñ€ÐµÐºÑ‚Ð½Ð¾Ð³Ð¾ ÑÑ€Ð°Ð²Ð½ÐµÐ½Ð¸Ñ Ð²Ð°Ð¼ нужно иÑпользовать разные имена веток."
msgid "You're about to reduce the visibility of the project %{strong_start}%{project_name}%{strong_end} in %{strong_start}%{group_name}%{strong_end}."
-msgstr ""
+msgstr "Ð’Ñ‹ ÑобираетеÑÑŒ уменьшить облаÑÑ‚ÑŒ видимоÑти проекта %{strong_start}%{project_name}%{strong_end} в %{strong_start}%{group_name}%{strong_end}."
msgid "You're about to reduce the visibility of the project %{strong_start}%{project_name}%{strong_end}."
msgstr ""
@@ -24089,12 +24613,24 @@ msgstr "Вы уже включили двухфакторную аутентиф
msgid "YouTube"
msgstr "YouTube"
+msgid "Your %{strong}%{plan_name}%{strong_close} subscription for %{strong}%{namespace_name}%{strong_close} will expire on %{strong}%{expires_on}%{strong_close}. After that, you will not to be able to create issues or merge requests as well as many other features."
+msgstr ""
+
msgid "Your %{strong}%{plan_name}%{strong_close} subscription will expire on %{strong}%{expires_on}%{strong_close}. After that, you will not to be able to create issues or merge requests as well as many other features."
msgstr ""
-msgid "Your Commit Email will be used for web based operations, such as edits and merges."
+msgid "Your CSV export has started. It will be emailed to %{email} when complete."
msgstr ""
+msgid "Your CSV export of %{issues_count} from project %{project_link} has been added to this email as an attachment."
+msgstr ""
+
+msgid "Your CSV export of %{written_count} from project %{project_name} (%{project_url}) has been added to this email as an attachment."
+msgstr ""
+
+msgid "Your Commit Email will be used for web based operations, such as edits and merges."
+msgstr "Ваш Ð°Ð´Ñ€ÐµÑ Ñлектронной почты Ð´Ð»Ñ ÐºÐ¾Ð¼Ð¼Ð¸Ñ‚Ð¾Ð² будет иÑпользоватьÑÑ Ð´Ð»Ñ Ð²ÐµÐ±-операций, таких как редактирование и ÑлиÑние."
+
msgid "Your Default Notification Email will be used for account notifications if a %{openingTag}group-specific email address%{closingTag} is not set."
msgstr ""
@@ -24102,13 +24638,13 @@ msgid "Your DevOps Score gives an overview of how you are using GitLab from a fe
msgstr ""
msgid "Your GPG keys (%{count})"
-msgstr "Ваши GPG ключи (%{count})"
+msgstr "Ваши ключи GPG (%{count})"
msgid "Your GitLab group"
msgstr ""
msgid "Your Gitlab Gold trial will last 30 days after which point you can keep your free Gitlab account forever. We just need some additional information to activate your trial."
-msgstr ""
+msgstr "Ваша пробный период GitLab Gold продлитÑÑ 30 дней, поÑле которых вы будете иметь возможноÑÑ‚ÑŒ оÑтавить Ñвой беÑплатный аккаунт GitLab навÑегда. Ðам потребуетÑÑ Ð»Ð¸ÑˆÑŒ немного дополнительной информации, чтобы активировать ваш пробный период."
msgid "Your Groups"
msgstr "Ваши Группы"
@@ -24120,7 +24656,7 @@ msgid "Your Personal Access Tokens will expire in %{days_to_expire} days or less
msgstr ""
msgid "Your Primary Email will be used for avatar detection."
-msgstr ""
+msgstr "Ваш оÑновной email будет иÑпользоватьÑÑ Ð´Ð»Ñ Ð¾Ð±Ð½Ð°Ñ€ÑƒÐ¶ÐµÐ½Ð¸Ñ Ð°Ð²Ð°Ñ‚Ð°Ñ€Ð°."
msgid "Your Projects (default)"
msgstr "Ваши проекты (по умолчанию)"
@@ -24129,10 +24665,10 @@ msgid "Your Projects' Activity"
msgstr "ÐктивноÑÑ‚ÑŒ ваших проектов"
msgid "Your Public Email will be displayed on your public profile."
-msgstr ""
+msgstr "Ваш публичный email будет отображатьÑÑ Ð² вашем профиле."
msgid "Your SSH keys (%{count})"
-msgstr "Ваши SSH ключи (%{count})"
+msgstr "Ваши ключи SSH (%{count})"
msgid "Your To-Do List"
msgstr ""
@@ -24150,7 +24686,7 @@ msgid "Your access request to the %{source_type} has been withdrawn."
msgstr "Ваш Ð·Ð°Ð¿Ñ€Ð¾Ñ Ð´Ð¾Ñтупа к %{source_type} был отозван."
msgid "Your account has been deactivated by your administrator. Please log back in to reactivate your account."
-msgstr ""
+msgstr "Ваша ÑƒÑ‡Ñ‘Ñ‚Ð½Ð°Ñ Ð·Ð°Ð¿Ð¸ÑÑŒ была деактивирована админиÑтратором. ПожалуйÑта Ñнова войдите в ÑиÑтему Ð´Ð»Ñ Ñ€ÐµÐ°ÐºÑ‚Ð¸Ð²Ð°Ñ†Ð¸Ð¸ вашей учётной запиÑи."
msgid "Your account is locked."
msgstr ""
@@ -24188,17 +24724,20 @@ msgstr "Ваш комментарий не может быть отправлеÐ
msgid "Your comment could not be updated! Please check your network connection and try again."
msgstr "Ваш комментарий не может быть обновлен! ПожалуйÑта, проверьте подключение к Ñети и повторите попытку."
+msgid "Your comment will be discarded."
+msgstr ""
+
msgid "Your custom stage '%{title}' was created"
msgstr ""
msgid "Your dashboard has been copied. You can %{web_ide_link_start}edit it here%{web_ide_link_end}."
-msgstr ""
+msgstr "Ваша панель ÑƒÐ¿Ñ€Ð°Ð²Ð»ÐµÐ½Ð¸Ñ Ð±Ñ‹Ð»Ð° Ñкопирована. Ð’Ñ‹ можете %{web_ide_link_start}отредактировать Ñто здеÑÑŒ%{web_ide_link_end}."
msgid "Your dashboard has been updated. You can %{web_ide_link_start}edit it here%{web_ide_link_end}."
-msgstr ""
+msgstr "Ваша панель ÑƒÐ¿Ñ€Ð°Ð²Ð»ÐµÐ½Ð¸Ñ Ð±Ñ‹Ð»Ð° обновлена. Ð’Ñ‹ можете %{web_ide_link_start}отредактировать её здеÑÑŒ%{web_ide_link_end}."
msgid "Your deployment services will be broken, you will need to manually fix the services after renaming."
-msgstr ""
+msgstr "Ваши Ñлужбы Ñ€Ð°Ð·Ð²Ñ‘Ñ€Ñ‚Ñ‹Ð²Ð°Ð½Ð¸Ñ ÑломаютÑÑ, и вам нужно будет вручную иÑправить Ñлужбы поÑле переименованиÑ."
msgid "Your device was successfully set up! Give it a name and register it with the GitLab server."
msgstr "Ваше уÑтройÑтво было уÑпешно наÑтроено! Дайте ему Ð¸Ð¼Ñ Ð¸ зарегиÑтрируйте его на Ñервере GitLab."
@@ -24227,6 +24766,9 @@ msgstr "Ваш новый токен SCIM"
msgid "Your new personal access token has been created."
msgstr "Ваш новый перÑональный токен доÑтупа был Ñоздан."
+msgid "Your new project access token has been created."
+msgstr ""
+
msgid "Your password isn't required to view this page. If a password or any other personal details are requested, please contact your administrator to report abuse."
msgstr ""
@@ -24245,12 +24787,18 @@ msgstr "Ваши проекты"
msgid "Your request for access has been queued for review."
msgstr "Ваш Ð·Ð°Ð¿Ñ€Ð¾Ñ Ð½Ð° доÑтуп был поÑтавлен в очередь на проверку."
+msgid "Your search didn't match any commits."
+msgstr ""
+
msgid "Your subscription expired!"
msgstr ""
msgid "Your subscription has been downgraded"
msgstr ""
+msgid "Your subscription will automatically renew in %{remaining_days}"
+msgstr ""
+
msgid "Your subscription will expire in %{remaining_days}"
msgstr ""
@@ -24291,8 +24839,8 @@ msgstr ""
msgid "allowed to fail"
msgstr ""
-msgid "already being used for another group or project milestone."
-msgstr "уже иÑпользуетÑÑ Ð´Ð»Ñ Ð´Ñ€ÑƒÐ³Ð¾Ð¹ группы или Ñтапа проекта."
+msgid "already being used for another group or project %{timebox_name}."
+msgstr ""
msgid "already has a \"created\" issue link"
msgstr ""
@@ -24369,6 +24917,9 @@ msgstr ""
msgid "ciReport|%{linkStartTag}Learn more about SAST %{linkEndTag}"
msgstr ""
+msgid "ciReport|%{linkStartTag}Learn more about Secret Scanning %{linkEndTag}"
+msgstr ""
+
msgid "ciReport|%{linkStartTag}Learn more about codequality reports %{linkEndTag}"
msgstr ""
@@ -24471,10 +25022,10 @@ msgid "ciReport|Container Scanning"
msgstr "Сканирование контейнеров"
msgid "ciReport|Container scanning"
-msgstr ""
+msgstr "Сканирование контейнера"
msgid "ciReport|Container scanning detects known vulnerabilities in your docker images."
-msgstr ""
+msgstr "Сканирование контейнеров обнаруживает извеÑтные уÑзвимоÑти в ваших образах Docker."
msgid "ciReport|Create a merge request to implement this solution, or download and apply the patch manually."
msgstr ""
@@ -24513,7 +25064,7 @@ msgid "ciReport|Found %{issuesWithCount}"
msgstr ""
msgid "ciReport|Investigate this vulnerability by creating an issue"
-msgstr ""
+msgstr "РаÑÑледуйте Ñту уÑзвимоÑÑ‚ÑŒ, вынеÑÑ Ð½Ð° обÑуждение"
msgid "ciReport|Learn more about interacting with security reports"
msgstr ""
@@ -24542,6 +25093,12 @@ msgstr ""
msgid "ciReport|SAST"
msgstr ""
+msgid "ciReport|Secret scanning"
+msgstr ""
+
+msgid "ciReport|Secret scanning detects secrets and credentials vulnerabilities in your source code."
+msgstr ""
+
msgid "ciReport|Security scanning"
msgstr ""
@@ -24595,7 +25152,7 @@ msgid "confidentiality|You are going to turn on the confidentiality. This means
msgstr ""
msgid "connecting"
-msgstr ""
+msgstr "подключение"
msgid "container_name cannot be larger than %{max_length} chars"
msgstr ""
@@ -24687,7 +25244,7 @@ msgid "error code:"
msgstr ""
msgid "estimateCommand|%{slash_command} will update the estimated time with the latest command."
-msgstr ""
+msgstr "%{slash_command} обновит раÑчетное Ð²Ñ€ÐµÐ¼Ñ Ñ Ð¿Ð¾Ð¼Ð¾Ñ‰ÑŒÑŽ поÑледней команды."
msgid "exceeds the limit of %{bytes} bytes"
msgstr ""
@@ -24747,6 +25304,9 @@ msgstr ""
msgid "group"
msgstr ""
+msgid "groups"
+msgstr ""
+
msgid "has already been linked to another vulnerability"
msgstr ""
@@ -24821,7 +25381,7 @@ msgid "is not a valid X509 certificate."
msgstr ""
msgid "is not allowed. Try again with a different email address, or contact your GitLab admin."
-msgstr ""
+msgstr "не разрешено. Попробуйте ещё раз Ñ Ð´Ñ€ÑƒÐ³Ð¸Ð¼ адреÑом Ñлектронной почты или ÑвÑжитеÑÑŒ Ñ Ð°Ð´Ð¼Ð¸Ð½Ð¸Ñтратором GitLab."
msgid "is not an email you own"
msgstr "не Ñ Ñлектронной почты, которой вы владеете"
@@ -24872,7 +25432,7 @@ msgid "latest"
msgstr "поÑледние"
msgid "latest deployment"
-msgstr ""
+msgstr "поÑледнее развертывание"
msgid "latest version"
msgstr "поÑледнÑÑ Ð²ÐµÑ€ÑиÑ"
@@ -24899,7 +25459,7 @@ msgid "manual"
msgstr "ручной"
msgid "math|The math in this entry is taking too long to render and may not be displayed as expected. For performance reasons, math blocks are also limited to %{maxChars} characters. Consider splitting up large formulae, splitting math blocks among multiple entries, or using an image instead."
-msgstr ""
+msgstr "МатематичеÑÐºÐ°Ñ ÑоÑтавлÑÑŽÑ‰Ð°Ñ Ñтой запиÑи занимает Ñлишком долго рендеритÑÑ Ð¸ может отображатьÑÑ Ð½ÐµÐ¿Ñ€Ð°Ð²Ð¸Ð»ÑŒÐ½Ð¾. Ð”Ð»Ñ Ð»ÑƒÑ‡ÑˆÐµÐ¹ производительноÑти, математичеÑкие блоки также ограничены %{maxChars} Ñимволами. Попробуйте разделить большие формулы, разделить математичеÑкие блоки на неÑколько запиÑей или иÑпользовать вмеÑто них изображение."
msgid "math|There was an error rendering this math block"
msgstr ""
@@ -24914,14 +25474,11 @@ msgstr[3] "запроÑов на ÑлиÑние"
msgid "merged %{time_ago}"
msgstr ""
-msgid "milestone should belong either to a project or a group."
-msgstr "Ñтап должен принадлежать либо проекту, либо группе."
-
msgid "missing"
msgstr "отÑутÑтвует"
msgid "most recent deployment"
-msgstr ""
+msgstr "наиболее недавнее развёртывание"
msgid "mrWidgetCommitsAdded|%{commitCount} and %{mergeCommitCount} will be added to %{targetBranch}."
msgstr "%{commitCount} и %{mergeCommitCount} будут добавлены в %{targetBranch}."
@@ -24942,19 +25499,19 @@ msgid "mrWidgetNothingToMerge|Merge requests are a place to propose changes you
msgstr ""
msgid "mrWidget| Please restore it or use a different %{missingBranchName} branch"
-msgstr ""
+msgstr " ПожалуйÑта, воÑÑтановите её или иÑпользуйте другую ветку %{missingBranchName}"
msgid "mrWidget|%{link_start}Learn more about resolving conflicts%{link_end}"
msgstr "%{link_start}подробнее о разрешении конфликтов%{link_end}"
msgid "mrWidget|%{metricsLinkStart} Memory %{metricsLinkEnd} usage %{emphasisStart} decreased %{emphasisEnd} from %{memoryFrom}MB to %{memoryTo}MB"
-msgstr ""
+msgstr "Потребление %{metricsLinkStart} памÑти %{metricsLinkEnd} %{emphasisStart} уменьшено %{emphasisEnd} Ñ %{memoryFrom}Мбайт до %{memoryTo}Мбайт"
msgid "mrWidget|%{metricsLinkStart} Memory %{metricsLinkEnd} usage %{emphasisStart} increased %{emphasisEnd} from %{memoryFrom}MB to %{memoryTo}MB"
-msgstr ""
+msgstr "Потребление %{metricsLinkStart} памÑти %{metricsLinkEnd} %{emphasisStart} увеличелоÑÑŒ %{emphasisEnd} Ñ %{memoryFrom}Мбайт до %{memoryTo}Мбайт"
msgid "mrWidget|%{metricsLinkStart} Memory %{metricsLinkEnd} usage is %{emphasisStart} unchanged %{emphasisEnd} at %{memoryFrom}MB"
-msgstr ""
+msgstr "Потребление %{metricsLinkStart} памÑти %{metricsLinkEnd} оÑталоÑÑŒ %{emphasisStart} неизменным %{emphasisEnd} на %{memoryFrom}Мбайт"
msgid "mrWidget|%{prefixToLinkStart}No pipeline%{prefixToLinkEnd} %{addPipelineLinkStart}Add the .gitlab-ci.yml file%{addPipelineLinkEnd} to create one."
msgstr ""
@@ -24993,25 +25550,25 @@ msgid "mrWidget|Cancel automatic merge"
msgstr "Отменить автоматичеÑкое ÑлиÑние"
msgid "mrWidget|Check out branch"
-msgstr ""
+msgstr "Проверить ветку"
msgid "mrWidget|Checking ability to merge automatically…"
msgstr ""
msgid "mrWidget|Cherry-pick"
-msgstr ""
+msgstr "Подобрать"
msgid "mrWidget|Cherry-pick this merge request in a new merge request"
-msgstr ""
+msgstr "Подберите Ñтот Ð·Ð°Ð¿Ñ€Ð¾Ñ Ð½Ð° ÑлиÑние в новый"
msgid "mrWidget|Closed"
-msgstr ""
+msgstr "Закрыт"
msgid "mrWidget|Closed by"
-msgstr ""
+msgstr "закрыто"
msgid "mrWidget|Closes"
-msgstr ""
+msgstr "Закрывает"
msgid "mrWidget|Create an issue to resolve them later"
msgstr "Создать обÑуждение Ð´Ð»Ñ ÐµÐ³Ð¾ поÑледующего решениÑ"
@@ -25020,19 +25577,19 @@ msgid "mrWidget|Delete source branch"
msgstr ""
msgid "mrWidget|Deployment statistics are not available currently"
-msgstr ""
+msgstr "Ðа данный момент ÑтатиÑтика Ñ€Ð°Ð·Ð²Ñ‘Ñ€Ñ‚Ñ‹Ð²Ð°Ð½Ð¸Ñ Ð½ÐµÐ´Ð¾Ñтупна"
msgid "mrWidget|Detect issues before deployment with a CI pipeline"
msgstr ""
msgid "mrWidget|Did not close"
-msgstr ""
+msgstr "Ðе закрыт"
msgid "mrWidget|Email patches"
-msgstr ""
+msgstr "Патчи по Ñлектронной почте"
msgid "mrWidget|Failed to load deployment statistics"
-msgstr ""
+msgstr "Ðе удалоÑÑŒ загрузить ÑтатиÑтику развёртываниÑ"
msgid "mrWidget|Fast-forward merge is not possible. To merge this request, first rebase locally."
msgstr ""
@@ -25041,31 +25598,31 @@ msgid "mrWidget|Fork merge requests do not create merge request pipelines which
msgstr ""
msgid "mrWidget|If the %{branch} branch exists in your local repository, you can merge this merge request manually using the"
-msgstr ""
+msgstr "ЕÑли ветка %{branch} ÑущеÑтвует в вашем локальном репозитории, вы можете объединить Ñтот Ð·Ð°Ð¿Ñ€Ð¾Ñ Ð½Ð° ÑлиÑние вручную Ñ Ð¿Ð¾Ð¼Ð¾Ñ‰ÑŒÑŽ"
msgid "mrWidget|If the %{missingBranchName} branch exists in your local repository, you can merge this merge request manually using the command line"
msgstr ""
msgid "mrWidget|In the merge train at position %{mergeTrainPosition}"
-msgstr ""
+msgstr "Добавлено в цепочку ÑлиÑÐ½Ð¸Ñ Ð½Ð° позицию %{mergeTrainPosition}"
msgid "mrWidget|Loading deployment statistics"
-msgstr ""
+msgstr "Загрузка ÑтатиÑтики развертываниÑ"
msgid "mrWidget|Mentions"
-msgstr ""
+msgstr "УпоминаниÑ"
msgid "mrWidget|Merge"
-msgstr ""
+msgstr "Слить"
msgid "mrWidget|Merge failed."
-msgstr ""
+msgstr "СлиÑние не удалоÑÑŒ."
msgid "mrWidget|Merge failed: %{mergeError}. Please try again."
-msgstr "СлиÑние не удалоÑÑŒ: %{mergeError}. ПожалуйÑта, попробуйте еще раз."
+msgstr "СлиÑние не удалоÑÑŒ: %{mergeError}. ПожалуйÑта, попробуйте ещё раз."
msgid "mrWidget|Merge locally"
-msgstr ""
+msgstr "Слить локально"
msgid "mrWidget|Merge request approved."
msgstr "Ð—Ð°Ð¿Ñ€Ð¾Ñ Ð½Ð° ÑлиÑние одобрен."
@@ -25083,19 +25640,19 @@ msgid "mrWidget|No approval required; you can still approve"
msgstr ""
msgid "mrWidget|Open in Web IDE"
-msgstr ""
+msgstr "Открыть в Web IDE"
msgid "mrWidget|Pipeline blocked. The pipeline for this merge request requires a manual action to proceed"
msgstr ""
msgid "mrWidget|Plain diff"
-msgstr ""
+msgstr "Обычное отличие"
msgid "mrWidget|Ready to be merged automatically. Ask someone with write access to this repository to merge this request"
-msgstr ""
+msgstr "Готово к автоматичеÑкому ÑлиÑнию. ПопроÑите кого-нибудь Ñ Ð¿Ñ€Ð°Ð²Ð¾Ð¼ запиÑи в Ñтот репозиторий Ñделать ÑлиÑние данного запроÑа"
msgid "mrWidget|Refresh"
-msgstr ""
+msgstr "Обновить"
msgid "mrWidget|Refresh now"
msgstr "Обновить ÑейчаÑ"
@@ -25107,7 +25664,7 @@ msgid "mrWidget|Remove from merge train"
msgstr ""
msgid "mrWidget|Request to merge"
-msgstr ""
+msgstr "Ð—Ð°Ð¿Ñ€Ð¾Ñ Ð½Ð° ÑлиÑние"
msgid "mrWidget|Resolve WIP status"
msgstr ""
@@ -25116,34 +25673,34 @@ msgid "mrWidget|Resolve conflicts"
msgstr "Разрешить конфликты"
msgid "mrWidget|Resolve these conflicts or ask someone with write access to this repository to merge it locally"
-msgstr ""
+msgstr "Разрешите Ñти конфликты или попроÑите кого-нибудь Ñ Ð´Ð¾Ñтупом на запиÑÑŒ в Ñтот репозиторий Ñлить его локально"
msgid "mrWidget|Revert"
-msgstr ""
+msgstr "Откатить"
msgid "mrWidget|Revert this merge request in a new merge request"
-msgstr ""
+msgstr "Откатить Ñтот Ð·Ð°Ð¿Ñ€Ð¾Ñ Ð½Ð° ÑлиÑние в новом запроÑе"
msgid "mrWidget|Revoke approval"
msgstr ""
msgid "mrWidget|Set by"
-msgstr ""
+msgstr "Ðазначено"
msgid "mrWidget|The changes were merged into"
-msgstr ""
+msgstr "Ð˜Ð·Ð¼ÐµÐ½ÐµÐ½Ð¸Ñ Ð±Ñ‹Ð»Ð¸ Ñлиты в"
msgid "mrWidget|The changes were not merged into"
-msgstr ""
+msgstr "Ð˜Ð·Ð¼ÐµÐ½ÐµÐ½Ð¸Ñ Ð½Ðµ были Ñлиты в"
msgid "mrWidget|The changes will be merged into"
-msgstr ""
+msgstr "Ð˜Ð·Ð¼ÐµÐ½ÐµÐ½Ð¸Ñ Ð±ÑƒÐ´ÑƒÑ‚ Ñлиты в"
msgid "mrWidget|The pipeline for this merge request failed. Please retry the job or push a new commit to fix the failure"
msgstr ""
msgid "mrWidget|The source branch HEAD has recently changed. Please reload the page and review the changes before merging"
-msgstr ""
+msgstr "СÑылка HEAD иÑходной ветви недавно изменилаÑÑŒ. ПожалуйÑта, обновите Ñтраницу и проÑмотрите Ð¸Ð·Ð¼ÐµÐ½ÐµÐ½Ð¸Ñ Ð¿ÐµÑ€ÐµÐ´ ÑлиÑнием"
msgid "mrWidget|The source branch has been deleted"
msgstr ""
@@ -25152,28 +25709,28 @@ msgid "mrWidget|The source branch is %{commitsBehindLinkStart}%{commitsBehind}%{
msgstr ""
msgid "mrWidget|The source branch is being deleted"
-msgstr ""
+msgstr "ИÑÑ…Ð¾Ð´Ð½Ð°Ñ Ð²ÐµÑ‚ÐºÐ° удалÑетÑÑ"
msgid "mrWidget|The source branch will be deleted"
-msgstr ""
+msgstr "ИÑÑ…Ð¾Ð´Ð½Ð°Ñ Ð²ÐµÑ‚ÐºÐ° будет удалена"
msgid "mrWidget|The source branch will not be deleted"
-msgstr ""
+msgstr "ИÑÑ…Ð¾Ð´Ð½Ð°Ñ Ð²ÐµÑ‚ÐºÐ° не будет удалена"
msgid "mrWidget|There are merge conflicts"
-msgstr ""
+msgstr "ЕÑÑ‚ÑŒ конфликты ÑлиÑниÑ"
msgid "mrWidget|There are unresolved threads. Please resolve these threads"
-msgstr ""
+msgstr "ЕÑÑ‚ÑŒ неразрешенные темы. ПожалуйÑта, разрешите их"
msgid "mrWidget|This feature merges changes from the target branch to the source branch. You cannot use this feature since the source branch is protected."
msgstr ""
msgid "mrWidget|This merge request failed to be merged automatically"
-msgstr ""
+msgstr "Этот Ð·Ð°Ð¿Ñ€Ð¾Ñ Ð½Ð° ÑлиÑние не может быть выполнен автоматичеÑки"
msgid "mrWidget|This merge request is in the process of being merged"
-msgstr ""
+msgstr "Этот Ð·Ð°Ð¿Ñ€Ð¾Ñ Ð¾Ð±ÑŠÐµÐ´Ð¸Ð½ÐµÐ½Ð¸Ñ ÑÐµÐ¹Ñ‡Ð°Ñ Ð²Ñ‹Ð¿Ð¾Ð»Ð½ÑетÑÑ"
msgid "mrWidget|This merge request will be added to the merge train when pipeline %{linkStart}#%{pipelineId}%{linkEnd} succeeds."
msgstr ""
@@ -25197,7 +25754,7 @@ msgid "mrWidget|You can delete the source branch now"
msgstr "Ð’Ñ‹ можете удалить иÑходную ветку прÑмо ÑейчаÑ"
msgid "mrWidget|You can merge this merge request manually using the"
-msgstr ""
+msgstr "Ð’Ñ‹ можете выполнить Ñтот Ð·Ð°Ð¿Ñ€Ð¾Ñ Ð½Ð° ÑлиÑние вручную Ñ Ð¿Ð¾Ð¼Ð¾Ñ‰ÑŒÑŽ"
msgid "mrWidget|Your password"
msgstr "Ваш пароль"
@@ -25206,13 +25763,13 @@ msgid "mrWidget|a quick guide that'll show you how to create"
msgstr ""
msgid "mrWidget|branch does not exist."
-msgstr ""
+msgstr "ветка не ÑущеÑтвует."
msgid "mrWidget|command line"
msgstr "командную Ñтроку"
msgid "mrWidget|into"
-msgstr ""
+msgstr "в"
msgid "mrWidget|one. Make your code more secure and more"
msgstr ""
@@ -25227,7 +25784,7 @@ msgid "mrWidget|to be added to the merge train when the pipeline succeeds"
msgstr "будет добавлен в цепочку ÑлиÑÐ½Ð¸Ñ Ð¿Ñ€Ð¸ уÑпешном выполнении Ñборочной линии"
msgid "mrWidget|to be merged automatically when the pipeline succeeds"
-msgstr ""
+msgstr "будет Ñлито автоматичеÑки, еÑли ÑÐ±Ð¾Ñ€Ð¾Ñ‡Ð½Ð°Ñ Ð»Ð¸Ð½Ð¸Ñ Ð²Ñ‹Ð¿Ð¾Ð»Ð½Ð¸Ñ‚ÑÑ ÑƒÑпешно"
msgid "mrWidget|to start a merge train when the pipeline succeeds"
msgstr ""
@@ -25239,18 +25796,21 @@ msgid "n/a"
msgstr "н/д"
msgid "need attention"
-msgstr ""
+msgstr "требуют вниманиÑ"
msgid "needs to be between 10 minutes and 1 month"
-msgstr ""
+msgstr "должно быть от 10 минут до 1 меÑÑца"
msgid "never expires"
-msgstr ""
+msgstr "никогда не иÑтекает"
msgid "new merge request"
msgstr "новый Ð·Ð°Ð¿Ñ€Ð¾Ñ Ð½Ð° ÑлиÑние"
msgid "no contributions"
+msgstr "не учаÑтвовали"
+
+msgid "no expiration"
msgstr ""
msgid "no one can merge"
@@ -25309,6 +25869,9 @@ msgstr ""
msgid "pending removal"
msgstr ""
+msgid "per day"
+msgstr ""
+
msgid "pipeline"
msgstr ""
@@ -25341,6 +25904,12 @@ msgstr[3] "проектов"
msgid "project avatar"
msgstr "логотип проекта"
+msgid "projects"
+msgstr ""
+
+msgid "push to your repository, create pipelines, create issues or add comments. To reduce storage capacity, delete unused repositories, artifacts, wikis, issues, and pipelines."
+msgstr ""
+
msgid "quick actions"
msgstr "быÑтрые дейÑтвиÑ"
@@ -25363,13 +25932,13 @@ msgid "remove due date"
msgstr "удалить дату завершениÑ"
msgid "remove weight"
-msgstr ""
+msgstr "удалить приоритет"
msgid "removed a Zoom call from this issue"
msgstr ""
msgid "rendered diff"
-msgstr ""
+msgstr "отображенное отличие"
msgid "reply"
msgid_plural "replies"
@@ -25418,37 +25987,37 @@ msgid "should be greater than or equal to %{access} inherited membership from gr
msgstr ""
msgid "show %{count} more"
-msgstr ""
+msgstr "показать еще %{count}"
msgid "show fewer"
-msgstr ""
+msgstr "показать меньше"
msgid "show less"
msgstr "Ñвернуть"
msgid "sign in"
-msgstr ""
+msgstr "войти"
msgid "sort:"
-msgstr ""
+msgstr "Ñортировка:"
msgid "source"
msgstr "иÑходный текÑÑ‚"
msgid "source diff"
-msgstr ""
+msgstr "отличиÑ"
msgid "specified top is not part of the tree"
msgstr "ÑƒÐºÐ°Ð·Ð°Ð½Ð½Ð°Ñ Ð²ÐµÑ€ÑˆÐ¸Ð½Ð° не ÑвлÑетÑÑ Ñ‡Ð°Ñтью дерева"
msgid "spendCommand|%{slash_command} will update the sum of the time spent."
-msgstr ""
+msgstr "%{slash_command} обновит Ñумму потраченного времени."
msgid "started"
msgstr "запущено"
msgid "started a discussion on %{design_link}"
-msgstr ""
+msgstr "начата диÑкуÑÑÐ¸Ñ Ð¿Ð¾ %{design_link}"
msgid "started on %{milestone_start_date}"
msgstr "началоÑÑŒ Ñ %{milestone_start_date}"
@@ -25457,7 +26026,7 @@ msgid "starts on %{milestone_start_date}"
msgstr "начинаетÑÑ Ñ %{milestone_start_date}"
msgid "stuck"
-msgstr ""
+msgstr "заÑÑ‚Ñ€Ñл"
msgid "success"
msgstr "уÑпешно"
@@ -25475,27 +26044,20 @@ msgid "suggestPipeline|We recommend the %{boldStart}Code Quality%{boldEnd} templ
msgstr ""
msgid "syntax is correct"
-msgstr ""
+msgstr "ÑинтакÑÐ¸Ñ Ð¿Ñ€Ð°Ð²Ð¸Ð»ÑŒÐ½Ñ‹Ð¹"
msgid "syntax is incorrect"
-msgstr ""
+msgstr "ÑинтакÑÐ¸Ñ Ð½ÐµÐ¿Ñ€Ð°Ð²Ð¸Ð»ÑŒÐ½Ñ‹Ð¹"
msgid "tag name"
msgstr "название тега"
msgid "the following issue(s)"
-msgstr ""
+msgstr "Ñледующее(ие) обÑуждение(Ñ)"
msgid "this document"
msgstr "Ñтот документ"
-msgid "thread resolved"
-msgid_plural "threads resolved"
-msgstr[0] ""
-msgstr[1] ""
-msgstr[2] ""
-msgstr[3] ""
-
msgid "to help your contributors communicate effectively!"
msgstr "чтобы помочь вашим учаÑтникам взаимодейÑтвовать Ñффективнее!"
@@ -25518,7 +26080,7 @@ msgid "updated"
msgstr "обновлено"
msgid "updated %{timeAgo}"
-msgstr ""
+msgstr "обновлено %{timeAgo}"
msgid "updated %{time_ago}"
msgstr "обновлено %{time_ago}"
@@ -25530,19 +26092,19 @@ msgid "username"
msgstr "Ð¸Ð¼Ñ Ð¿Ð¾Ð»ÑŒÐ·Ð¾Ð²Ð°Ñ‚ÐµÐ»Ñ"
msgid "uses Kubernetes clusters to deploy your code!"
-msgstr ""
+msgstr "иÑпользует клаÑтеры Kubernetes Ð´Ð»Ñ Ñ€Ð°Ð·Ð²ÐµÑ€Ñ‚Ñ‹Ð²Ð°Ð½Ð¸Ñ Ð²Ð°ÑˆÐµÐ³Ð¾ кода!"
msgid "verify ownership"
-msgstr ""
+msgstr "подтвердить право ÑобÑтвенноÑти"
msgid "version %{versionIndex}"
-msgstr ""
+msgstr "верÑÐ¸Ñ %{versionIndex}"
msgid "via %{closed_via}"
-msgstr ""
+msgstr "через %{closed_via}"
msgid "via merge request %{link}"
-msgstr ""
+msgstr "через Ð·Ð°Ð¿Ñ€Ð¾Ñ Ð½Ð° ÑлиÑние %{link}"
msgid "view it on GitLab"
msgstr "проÑмотреть Ñто на GitLab"
@@ -25550,9 +26112,12 @@ msgstr "проÑмотреть Ñто на GitLab"
msgid "view the blob"
msgstr "проÑмотреть бинарные данные"
-msgid "vulnerability|Add a comment or reason for dismissal"
+msgid "vulnerability|Add a comment"
msgstr ""
+msgid "vulnerability|Add a comment or reason for dismissal"
+msgstr "Добавить комментарий или причину отклонениÑ"
+
msgid "vulnerability|Add comment"
msgstr "Добавить комментарий"
@@ -25560,29 +26125,29 @@ msgid "vulnerability|Add comment & dismiss"
msgstr "Добавить комментарий и отклонить"
msgid "vulnerability|Dismiss vulnerability"
-msgstr ""
+msgstr "ПропуÑтить уÑзвимоÑÑ‚ÑŒ"
msgid "vulnerability|Save comment"
-msgstr ""
+msgstr "Сохранить комментарий"
msgid "vulnerability|Undo dismiss"
-msgstr ""
+msgstr "Отменить отклонение"
msgid "vulnerability|dismissed"
-msgstr ""
+msgstr "отклонено"
msgid "wiki page"
-msgstr ""
+msgstr "wiki-Ñтраница"
msgid "with %{additions} additions, %{deletions} deletions."
-msgstr ""
+msgstr "Ñ %{additions} добавлениÑми, %{deletions} удалениÑми."
msgid "with expiry changing from %{old_expiry} to %{new_expiry}"
-msgstr ""
+msgstr "Ñ Ð¸Ñтечением, изменившимÑÑ Ñ %{old_expiry} на %{new_expiry}"
msgid "with expiry remaining unchanged at %{old_expiry}"
-msgstr ""
+msgstr "Ñ Ð¾ÑтавшимÑÑ Ð¸Ñтечением, неизменным на %{old_expiry}"
msgid "yaml invalid"
-msgstr ""
+msgstr "неверный YAML"
diff --git a/locale/sk_SK/gitlab.po b/locale/sk_SK/gitlab.po
index c7573f21cc8..57ed9829c0c 100644
--- a/locale/sk_SK/gitlab.po
+++ b/locale/sk_SK/gitlab.po
@@ -12,7 +12,7 @@ msgstr ""
"X-Crowdin-Project: gitlab-ee\n"
"X-Crowdin-Language: sk\n"
"X-Crowdin-File: /master/locale/gitlab.pot\n"
-"PO-Revision-Date: 2020-04-15 00:27\n"
+"PO-Revision-Date: 2020-05-05 21:35\n"
msgid " %{start} to %{end}"
msgstr ""
@@ -272,6 +272,13 @@ msgstr[1] ""
msgstr[2] ""
msgstr[3] ""
+msgid "%d unresolved thread"
+msgid_plural "%d unresolved threads"
+msgstr[0] ""
+msgstr[1] ""
+msgstr[2] ""
+msgstr[3] ""
+
msgid "%d vulnerability dismissed"
msgid_plural "%d vulnerabilities dismissed"
msgstr[0] ""
@@ -443,6 +450,9 @@ msgstr ""
msgid "%{mrText}, this issue will be closed automatically."
msgstr ""
+msgid "%{namespace_name} is now read-only. You cannot: %{base_message}"
+msgstr ""
+
msgid "%{name} contained %{resultsString}"
msgstr ""
@@ -455,6 +465,13 @@ msgstr ""
msgid "%{name}'s avatar"
msgstr ""
+msgid "%{no_of_days} day"
+msgid_plural "%{no_of_days} days"
+msgstr[0] ""
+msgstr[1] ""
+msgstr[2] ""
+msgstr[3] ""
+
msgid "%{number_commits_behind} commits behind %{default_branch}, %{number_commits_ahead} commits ahead"
msgstr ""
@@ -590,6 +607,9 @@ msgstr[3] ""
msgid "%{text} is available"
msgstr ""
+msgid "%{timebox_name} should belong either to a project or a group."
+msgstr ""
+
msgid "%{title} %{operator} %{threshold}"
msgstr ""
@@ -903,12 +923,18 @@ msgstr ""
msgid "<code>\"johnsmith@example.com\": \"johnsmith@example.com\"</code> will add \"By <a href=\"#\">johnsmith@example.com</a>\" to all issues and comments originally created by johnsmith@example.com. By default, the email address or username is masked to ensure the user's privacy. Use this option if you want to show the full email address."
msgstr ""
+msgid "<namespace / project>"
+msgstr ""
+
msgid "<no name set>"
msgstr ""
msgid "<no scopes selected>"
msgstr ""
+msgid "<project name>"
+msgstr ""
+
msgid "<strong>%{group_name}</strong> group members"
msgstr ""
@@ -942,6 +968,9 @@ msgstr ""
msgid "A basic page and serverless function that uses AWS Lambda, AWS API Gateway, and GitLab Pages"
msgstr ""
+msgid "A complete DevOps platform"
+msgstr ""
+
msgid "A default branch cannot be chosen for an empty project."
msgstr ""
@@ -1158,6 +1187,12 @@ msgstr ""
msgid "AccessTokens|reset it"
msgstr ""
+msgid "AccessibilityReport|Accessibility report artifact not found"
+msgstr ""
+
+msgid "AccessibilityReport|Failed to retrieve accessibility report"
+msgstr ""
+
msgid "AccessibilityReport|Learn More"
msgstr ""
@@ -1167,6 +1202,9 @@ msgstr ""
msgid "AccessibilityReport|New"
msgstr ""
+msgid "AccessibilityReport|The accessibility scanning found an error of the following type: %{code}"
+msgstr ""
+
msgid "Account"
msgstr ""
@@ -1411,6 +1449,9 @@ msgstr ""
msgid "Added at"
msgstr ""
+msgid "Added for this merge request"
+msgstr ""
+
msgid "Added in this version"
msgstr ""
@@ -1784,6 +1825,72 @@ msgstr[1] ""
msgstr[2] ""
msgstr[3] ""
+msgid "Alert Details"
+msgstr ""
+
+msgid "AlertManagement|Acknowledged"
+msgstr ""
+
+msgid "AlertManagement|Alert"
+msgstr ""
+
+msgid "AlertManagement|Authorize external service"
+msgstr ""
+
+msgid "AlertManagement|Display alerts from all your monitoring tools directly within GitLab. Streamline the investigation of your alerts and the escalation of alerts to incidents."
+msgstr ""
+
+msgid "AlertManagement|End time"
+msgstr ""
+
+msgid "AlertManagement|End time:"
+msgstr ""
+
+msgid "AlertManagement|Events"
+msgstr ""
+
+msgid "AlertManagement|Events:"
+msgstr ""
+
+msgid "AlertManagement|Full Alert Details"
+msgstr ""
+
+msgid "AlertManagement|More information"
+msgstr ""
+
+msgid "AlertManagement|No alerts available to display. If you think you're seeing this message in error, refresh the page."
+msgstr ""
+
+msgid "AlertManagement|No alerts to display."
+msgstr ""
+
+msgid "AlertManagement|Overview"
+msgstr ""
+
+msgid "AlertManagement|Resolved"
+msgstr ""
+
+msgid "AlertManagement|Severity"
+msgstr ""
+
+msgid "AlertManagement|Start time"
+msgstr ""
+
+msgid "AlertManagement|Start time:"
+msgstr ""
+
+msgid "AlertManagement|Status"
+msgstr ""
+
+msgid "AlertManagement|Surface alerts in GitLab"
+msgstr ""
+
+msgid "AlertManagement|There was an error displaying the alerts. Confirm your endpoint's configuration details to ensure alerts appear."
+msgstr ""
+
+msgid "AlertManagement|Triggered"
+msgstr ""
+
msgid "AlertService|%{linkStart}Learn more%{linkEnd} about configuring this endpoint to receive alerts."
msgstr ""
@@ -1853,6 +1960,9 @@ msgstr ""
msgid "All security scans are enabled because %{linkStart}Auto DevOps%{linkEnd} is enabled on this project"
msgstr ""
+msgid "All threads resolved"
+msgstr ""
+
msgid "All users"
msgstr ""
@@ -1871,6 +1981,9 @@ msgstr ""
msgid "Allow only the selected protocols to be used for Git access."
msgstr ""
+msgid "Allow owners to manage default branch protection per group"
+msgstr ""
+
msgid "Allow owners to manually add users outside of LDAP"
msgstr ""
@@ -1979,6 +2092,9 @@ msgstr ""
msgid "An error occurred fetching the dropdown data."
msgstr ""
+msgid "An error occurred fetching the project authors."
+msgstr ""
+
msgid "An error occurred previewing the blob"
msgstr ""
@@ -2060,6 +2176,9 @@ msgstr ""
msgid "An error occurred while fetching sidebar data"
msgstr ""
+msgid "An error occurred while fetching terraform reports."
+msgstr ""
+
msgid "An error occurred while fetching the Service Desk address."
msgstr ""
@@ -2297,6 +2416,9 @@ msgstr ""
msgid "Any"
msgstr ""
+msgid "Any Author"
+msgstr ""
+
msgid "Any Label"
msgstr ""
@@ -2520,10 +2642,10 @@ msgstr ""
msgid "Are you sure that you want to unarchive this project?"
msgstr ""
-msgid "Are you sure you want to cancel creating this comment?"
+msgid "Are you sure you want to cancel editing this comment?"
msgstr ""
-msgid "Are you sure you want to cancel editing this comment?"
+msgid "Are you sure you want to close this blocked issue?"
msgstr ""
msgid "Are you sure you want to delete %{name}?"
@@ -2553,6 +2675,9 @@ msgstr ""
msgid "Are you sure you want to deploy this environment?"
msgstr ""
+msgid "Are you sure you want to discard this comment?"
+msgstr ""
+
msgid "Are you sure you want to erase this build?"
msgstr ""
@@ -2731,6 +2856,9 @@ msgstr ""
msgid "At least one approval from a code owner is required to change files matching the respective CODEOWNER rules."
msgstr ""
+msgid "At least one logging option is required to be enabled"
+msgstr ""
+
msgid "At least one of group_id or project_id must be specified"
msgstr ""
@@ -2771,6 +2899,27 @@ msgstr ""
msgid "AuditEvents|Target"
msgstr ""
+msgid "AuditLogs|(removed)"
+msgstr ""
+
+msgid "AuditLogs|Action"
+msgstr ""
+
+msgid "AuditLogs|Author"
+msgstr ""
+
+msgid "AuditLogs|Date"
+msgstr ""
+
+msgid "AuditLogs|IP Address"
+msgstr ""
+
+msgid "AuditLogs|Object"
+msgstr ""
+
+msgid "AuditLogs|Target"
+msgstr ""
+
msgid "Aug"
msgstr ""
@@ -3053,9 +3202,6 @@ msgstr ""
msgid "BambooService|You must set up automatic revision labeling and a repository trigger in Bamboo."
msgstr ""
-msgid "Batch operations"
-msgstr ""
-
msgid "BatchComments|Delete all pending comments"
msgstr ""
@@ -3185,6 +3331,9 @@ msgstr ""
msgid "Boards"
msgstr ""
+msgid "Boards and Board Lists"
+msgstr ""
+
msgid "Boards|Collapse"
msgstr ""
@@ -3404,6 +3553,9 @@ msgstr ""
msgid "Buy GitLab Enterprise Edition"
msgstr ""
+msgid "Buy more Pipeline minutes"
+msgstr ""
+
msgid "By %{user_name}"
msgstr ""
@@ -3524,6 +3676,9 @@ msgstr ""
msgid "Can't scan the code?"
msgstr ""
+msgid "Can't update snippet: %{err}"
+msgstr ""
+
msgid "Canary"
msgstr ""
@@ -3692,6 +3847,9 @@ msgstr ""
msgid "Changing group path can have unintended side effects."
msgstr ""
+msgid "Charts"
+msgstr ""
+
msgid "Charts can't be displayed as the request for data has timed out. %{documentationLink}"
msgstr ""
@@ -3728,9 +3886,6 @@ msgstr ""
msgid "ChatMessage|and [%{count} more](%{pipeline_failed_jobs_url})"
msgstr ""
-msgid "ChatMessage|failed"
-msgstr ""
-
msgid "ChatMessage|has failed"
msgstr ""
@@ -3746,9 +3901,6 @@ msgstr ""
msgid "ChatMessage|in %{project_link}"
msgstr ""
-msgid "ChatMessage|passed"
-msgstr ""
-
msgid "Check again"
msgstr ""
@@ -4199,9 +4351,6 @@ msgstr ""
msgid "Click any <strong>project name</strong> in the project list below to navigate to the project milestone."
msgstr ""
-msgid "Click here"
-msgstr ""
-
msgid "Click the <strong>Download</strong> button and wait for downloading to complete."
msgstr ""
@@ -4589,6 +4738,12 @@ msgstr ""
msgid "ClusterIntegration|Fetching zones"
msgstr ""
+msgid "ClusterIntegration|Fluentd"
+msgstr ""
+
+msgid "ClusterIntegration|Fluentd is an open source data collector, which lets you unify the data collection and consumption for a better use and understanding of data. It requires at least one of the following logs to be successfully installed."
+msgstr ""
+
msgid "ClusterIntegration|GitLab Integration"
msgstr ""
@@ -4895,6 +5050,15 @@ msgstr ""
msgid "ClusterIntegration|Request to begin uninstalling failed"
msgstr ""
+msgid "ClusterIntegration|SIEM Hostname"
+msgstr ""
+
+msgid "ClusterIntegration|SIEM Port"
+msgstr ""
+
+msgid "ClusterIntegration|SIEM Protocol"
+msgstr ""
+
msgid "ClusterIntegration|Save changes"
msgstr ""
@@ -4991,6 +5155,12 @@ msgstr ""
msgid "ClusterIntegration|Select zone to choose machine type"
msgstr ""
+msgid "ClusterIntegration|Send Cilium Logs"
+msgstr ""
+
+msgid "ClusterIntegration|Send ModSecurity Logs"
+msgstr ""
+
msgid "ClusterIntegration|Service Token"
msgstr ""
@@ -5421,18 +5591,33 @@ msgstr ""
msgid "Compliance framework (optional)"
msgstr ""
+msgid "ComplianceFramework|GDPR"
+msgstr ""
+
msgid "ComplianceFramework|GDPR - General Data Protection Regulation"
msgstr ""
+msgid "ComplianceFramework|HIPAA"
+msgstr ""
+
msgid "ComplianceFramework|HIPAA - Health Insurance Portability and Accountability Act"
msgstr ""
+msgid "ComplianceFramework|PCI-DSS"
+msgstr ""
+
msgid "ComplianceFramework|PCI-DSS - Payment Card Industry-Data Security Standard"
msgstr ""
+msgid "ComplianceFramework|SOC 2"
+msgstr ""
+
msgid "ComplianceFramework|SOC 2 - Service Organization Control 2"
msgstr ""
+msgid "ComplianceFramework|SOX"
+msgstr ""
+
msgid "ComplianceFramework|SOX - Sarbanes-Oxley"
msgstr ""
@@ -5580,9 +5765,15 @@ msgstr ""
msgid "Container repositories sync capacity"
msgstr ""
+msgid "ContainerRegistry| Please visit the %{linkStart}administration settings%{linkEnd} to enable this feature."
+msgstr ""
+
msgid "ContainerRegistry|%{imageName} tags"
msgstr ""
+msgid "ContainerRegistry|%{title} was successfully scheduled for deletion"
+msgstr ""
+
msgid "ContainerRegistry|Automatically remove extra images that aren't designed to be kept."
msgstr ""
@@ -5595,6 +5786,9 @@ msgstr ""
msgid "ContainerRegistry|Container Registry"
msgstr ""
+msgid "ContainerRegistry|Container Registry tag expiration and retention policy is disabled"
+msgstr ""
+
msgid "ContainerRegistry|Copy build command"
msgstr ""
@@ -5604,18 +5798,12 @@ msgstr ""
msgid "ContainerRegistry|Copy push command"
msgstr ""
-msgid "ContainerRegistry|Currently, the Container Registry tag expiration feature is not available for projects created before GitLab version 12.8. For updates and more information, visit Issue %{linkStart}#196124%{linkEnd}"
-msgstr ""
-
msgid "ContainerRegistry|Docker connection error"
msgstr ""
msgid "ContainerRegistry|Docker tag expiration policy is %{toggleStatus}"
msgstr ""
-msgid "ContainerRegistry|Docker tags with names matching this regex pattern will expire:"
-msgstr ""
-
msgid "ContainerRegistry|Edit Settings"
msgstr ""
@@ -5637,9 +5825,6 @@ msgstr ""
msgid "ContainerRegistry|Image ID"
msgstr ""
-msgid "ContainerRegistry|Image deleted successfully"
-msgstr ""
-
msgid "ContainerRegistry|Keep and protect the images that matter most."
msgstr ""
@@ -5655,12 +5840,24 @@ msgstr ""
msgid "ContainerRegistry|Number of tags to retain:"
msgstr ""
+msgid "ContainerRegistry|Please contact your administrator."
+msgstr ""
+
msgid "ContainerRegistry|Push an image"
msgstr ""
msgid "ContainerRegistry|Quick Start"
msgstr ""
+msgid "ContainerRegistry|Regular expressions such as %{codeStart}.*-test%{codeEnd} or %{codeStart}dev-.*%{codeEnd} are supported"
+msgstr ""
+
+msgid "ContainerRegistry|Regular expressions such as %{codeStart}.*-test%{codeEnd} or %{codeStart}dev-.*%{codeEnd} are supported. To select all tags, use %{codeStart}.*%{codeEnd}"
+msgstr ""
+
+msgid "ContainerRegistry|Remember to run %{docLinkStart}garbage collection%{docLinkEnd} to remove the stale data from storage."
+msgstr ""
+
msgid "ContainerRegistry|Remove repository"
msgstr ""
@@ -5677,22 +5874,22 @@ msgstr[3] ""
msgid "ContainerRegistry|Retention policy has been Enabled"
msgstr ""
-msgid "ContainerRegistry|Something went wrong while deleting the image."
+msgid "ContainerRegistry|Something went wrong while fetching the expiration policy."
msgstr ""
-msgid "ContainerRegistry|Something went wrong while deleting the tag."
+msgid "ContainerRegistry|Something went wrong while fetching the repository list."
msgstr ""
-msgid "ContainerRegistry|Something went wrong while deleting the tags."
+msgid "ContainerRegistry|Something went wrong while fetching the tags list."
msgstr ""
-msgid "ContainerRegistry|Something went wrong while fetching the expiration policy."
+msgid "ContainerRegistry|Something went wrong while marking the tag for deletion."
msgstr ""
-msgid "ContainerRegistry|Something went wrong while fetching the packages list."
+msgid "ContainerRegistry|Something went wrong while marking the tags for deletion."
msgstr ""
-msgid "ContainerRegistry|Something went wrong while fetching the tags list."
+msgid "ContainerRegistry|Something went wrong while scheduling %{title} for deletion. Please try again."
msgstr ""
msgid "ContainerRegistry|Something went wrong while updating the expiration policy."
@@ -5701,16 +5898,25 @@ msgstr ""
msgid "ContainerRegistry|Tag"
msgstr ""
-msgid "ContainerRegistry|Tag deleted successfully"
-msgstr ""
-
msgid "ContainerRegistry|Tag expiration policy"
msgstr ""
msgid "ContainerRegistry|Tag expiration policy is designed to:"
msgstr ""
-msgid "ContainerRegistry|Tags deleted successfully"
+msgid "ContainerRegistry|Tag successfully marked for deletion."
+msgstr ""
+
+msgid "ContainerRegistry|Tags successfully marked for deletion."
+msgstr ""
+
+msgid "ContainerRegistry|Tags with names matching this regex pattern will %{italicStart}be preserved:%{italicEnd}"
+msgstr ""
+
+msgid "ContainerRegistry|Tags with names matching this regex pattern will %{italicStart}expire:%{italicEnd}"
+msgstr ""
+
+msgid "ContainerRegistry|The Container Registry tag expiration and retention policies for this project have not been enabled."
msgstr ""
msgid "ContainerRegistry|The last tag related to this image was recently removed. This empty image and any associated data will be automatically removed as part of the regular Garbage Collection process. If you have any questions, contact your administrator."
@@ -5731,16 +5937,16 @@ msgstr ""
msgid "ContainerRegistry|There are no container images stored for this project"
msgstr ""
-msgid "ContainerRegistry|This Registry contains deleted image tag data. Remember to run %{docLinkStart}garbage collection%{docLinkEnd} to remove the stale data from storage."
+msgid "ContainerRegistry|There was an error during the deletion of this image repository, please try again."
msgstr ""
msgid "ContainerRegistry|This image has no active tags"
msgstr ""
-msgid "ContainerRegistry|We are having trouble connecting to Docker, which could be due to an issue with your project name or path. %{docLinkStart}More Information%{docLinkEnd}"
+msgid "ContainerRegistry|This image repository is scheduled for deletion"
msgstr ""
-msgid "ContainerRegistry|Wildcards such as %{codeStart}.*-stable%{codeEnd} or %{codeStart}production/.*%{codeEnd} are supported. To select all tags, use %{codeStart}.*%{codeEnd}"
+msgid "ContainerRegistry|We are having trouble connecting to Docker, which could be due to an issue with your project name or path. %{docLinkStart}More Information%{docLinkEnd}"
msgstr ""
msgid "ContainerRegistry|With the Container Registry, every project can have its own space to store its Docker images. %{docLinkStart}More Information%{docLinkEnd}"
@@ -5995,7 +6201,7 @@ msgstr ""
msgid "Could not delete chat nickname %{chat_name}."
msgstr ""
-msgid "Could not find design"
+msgid "Could not find design."
msgstr ""
msgid "Could not remove the trigger."
@@ -6010,6 +6216,9 @@ msgstr ""
msgid "Could not revoke personal access token %{personal_access_token_name}."
msgstr ""
+msgid "Could not revoke project access token %{project_access_token_name}."
+msgstr ""
+
msgid "Could not save group ID"
msgstr ""
@@ -6067,6 +6276,9 @@ msgstr ""
msgid "Create a new branch"
msgstr ""
+msgid "Create a new deploy key for this project"
+msgstr ""
+
msgid "Create a new file as there are no files yet. Afterwards, you'll be able to commit your changes."
msgstr ""
@@ -6172,6 +6384,9 @@ msgstr ""
msgid "Create requirement"
msgstr ""
+msgid "Create snippet"
+msgstr ""
+
msgid "Create wildcard: %{searchTerm}"
msgstr ""
@@ -6193,6 +6408,9 @@ msgstr ""
msgid "Created"
msgstr ""
+msgid "Created %{timestamp}"
+msgstr ""
+
msgid "Created At"
msgstr ""
@@ -6551,6 +6769,9 @@ msgstr ""
msgid "CycleAnalytics|stage dropdown"
msgstr ""
+msgid "DAG"
+msgstr ""
+
msgid "DNS"
msgstr ""
@@ -6929,6 +7150,9 @@ msgstr ""
msgid "Deploy key was successfully updated."
msgstr ""
+msgid "Deploy keys allow read-only or read-write (if enabled) access to your repository. Deploy keys can be used for CI, staging or production servers. You can create a deploy key or add an existing one."
+msgstr ""
+
msgid "Deploy progress not found. To see pods, ensure your environment matches %{linkStart}deploy board criteria%{linkEnd}."
msgstr ""
@@ -7082,6 +7306,9 @@ msgstr ""
msgid "Deploying to"
msgstr ""
+msgid "Deployment Frequency"
+msgstr ""
+
msgid "Deployment|API"
msgstr ""
@@ -7602,9 +7829,6 @@ msgstr ""
msgid "Edit environment"
msgstr ""
-msgid "Edit epic description"
-msgstr ""
-
msgid "Edit file"
msgstr ""
@@ -8226,9 +8450,6 @@ msgstr ""
msgid "Epics"
msgstr ""
-msgid "Epics (Ultimate / Gold license only)"
-msgstr ""
-
msgid "Epics Roadmap"
msgstr ""
@@ -8238,6 +8459,9 @@ msgstr ""
msgid "Epics let you manage your portfolio of projects more efficiently and with less effort"
msgstr ""
+msgid "Epics, Issues, and Merge Requests"
+msgstr ""
+
msgid "Epics|Add an epic"
msgstr ""
@@ -8289,6 +8513,9 @@ msgstr ""
msgid "Epics|Something went wrong while fetching group epics."
msgstr ""
+msgid "Epics|Something went wrong while moving item."
+msgstr ""
+
msgid "Epics|Something went wrong while ordering item."
msgstr ""
@@ -8343,6 +8570,9 @@ msgstr ""
msgid "Error fetching network graph."
msgstr ""
+msgid "Error fetching payload data."
+msgstr ""
+
msgid "Error fetching projects"
msgstr ""
@@ -8352,9 +8582,6 @@ msgstr ""
msgid "Error fetching the dependency list. Please check your network connection and try again."
msgstr ""
-msgid "Error fetching usage ping data."
-msgstr ""
-
msgid "Error loading branch data. Please try again."
msgstr ""
@@ -8496,6 +8723,9 @@ msgstr ""
msgid "Errors"
msgstr ""
+msgid "Errors:"
+msgstr ""
+
msgid "Estimated"
msgstr ""
@@ -8700,12 +8930,18 @@ msgstr ""
msgid "Export as CSV"
msgstr ""
+msgid "Export group"
+msgstr ""
+
msgid "Export issues"
msgstr ""
msgid "Export project"
msgstr ""
+msgid "Export this group with all related data to a new GitLab instance. Once complete, you can import the data file from the \"New Group\" page."
+msgstr ""
+
msgid "Export this project with all its related data in order to move your project to a new GitLab instance. Once the export is finished, you can import the file from the \"New Project\" page."
msgstr ""
@@ -8775,9 +9011,6 @@ msgstr ""
msgid "Failed Jobs"
msgstr ""
-msgid "Failed create wiki"
-msgstr ""
-
msgid "Failed to add a Zoom meeting"
msgstr ""
@@ -8817,6 +9050,9 @@ msgstr ""
msgid "Failed to create resources"
msgstr ""
+msgid "Failed to create wiki"
+msgstr ""
+
msgid "Failed to delete board. Please try again."
msgstr ""
@@ -8826,7 +9062,7 @@ msgstr ""
msgid "Failed to enqueue the rebase operation, possibly due to a long-lived transaction. Try again later."
msgstr ""
-msgid "Failed to find import label for jira import."
+msgid "Failed to find import label for Jira import."
msgstr ""
msgid "Failed to get ref."
@@ -8850,6 +9086,12 @@ msgstr ""
msgid "Failed to load groups & users."
msgstr ""
+msgid "Failed to load labels. Please try again."
+msgstr ""
+
+msgid "Failed to load milestones. Please try again."
+msgstr ""
+
msgid "Failed to load related branches"
msgstr ""
@@ -9225,7 +9467,10 @@ msgstr ""
msgid "Filter by milestone name"
msgstr ""
-msgid "Filter by name..."
+msgid "Filter by name"
+msgstr ""
+
+msgid "Filter by status"
msgstr ""
msgid "Filter by two-factor authentication"
@@ -9237,12 +9482,18 @@ msgstr ""
msgid "Filter projects"
msgstr ""
+msgid "Filter results"
+msgstr ""
+
msgid "Filter results by group"
msgstr ""
msgid "Filter results by project"
msgstr ""
+msgid "Filter results..."
+msgstr ""
+
msgid "Filter your projects by name"
msgstr ""
@@ -9531,10 +9782,10 @@ msgstr ""
msgid "GeoNodes|Checksummed"
msgstr ""
-msgid "GeoNodes|Container repositories"
+msgid "GeoNodes|Consult Geo troubleshooting information"
msgstr ""
-msgid "GeoNodes|Data is out of date from %{timeago}"
+msgid "GeoNodes|Container repositories"
msgstr ""
msgid "GeoNodes|Data replication lag"
@@ -9576,6 +9827,9 @@ msgstr ""
msgid "GeoNodes|Last event ID seen from primary"
msgstr ""
+msgid "GeoNodes|Learn more about Geo node statuses"
+msgstr ""
+
msgid "GeoNodes|Loading nodes"
msgstr ""
@@ -9591,6 +9845,9 @@ msgstr ""
msgid "GeoNodes|Node was successfully removed."
msgstr ""
+msgid "GeoNodes|Node's status was updated %{timeAgo}."
+msgstr ""
+
msgid "GeoNodes|Not checksummed"
msgstr ""
@@ -9621,7 +9878,10 @@ msgstr ""
msgid "GeoNodes|Repository verification progress"
msgstr ""
-msgid "GeoNodes|Selective"
+msgid "GeoNodes|Selective (%{syncLabel})"
+msgstr ""
+
+msgid "GeoNodes|Selective synchronization"
msgstr ""
msgid "GeoNodes|Something went wrong while changing node status"
@@ -9648,6 +9908,9 @@ msgstr ""
msgid "GeoNodes|Unverified"
msgstr ""
+msgid "GeoNodes|Updated %{timeAgo}"
+msgstr ""
+
msgid "GeoNodes|Used slots"
msgstr ""
@@ -9687,16 +9950,16 @@ msgstr ""
msgid "Geo|All"
msgstr ""
-msgid "Geo|All projects"
+msgid "Geo|All %{replicable_type}"
msgstr ""
-msgid "Geo|All projects are being scheduled for re-sync"
+msgid "Geo|All projects"
msgstr ""
-msgid "Geo|All projects are being scheduled for re-verify"
+msgid "Geo|All projects are being scheduled for resync"
msgstr ""
-msgid "Geo|Batch operations"
+msgid "Geo|All projects are being scheduled for reverify"
msgstr ""
msgid "Geo|Could not remove tracking entry for an existing project."
@@ -9708,9 +9971,15 @@ msgstr ""
msgid "Geo|Failed"
msgstr ""
+msgid "Geo|Filter by status"
+msgstr ""
+
msgid "Geo|Geo Status"
msgstr ""
+msgid "Geo|In progress"
+msgstr ""
+
msgid "Geo|In sync"
msgstr ""
@@ -9735,9 +10004,6 @@ msgstr ""
msgid "Geo|Not synced yet"
msgstr ""
-msgid "Geo|Pending"
-msgstr ""
-
msgid "Geo|Pending synchronization"
msgstr ""
@@ -9774,7 +10040,7 @@ msgstr ""
msgid "Geo|Resync"
msgstr ""
-msgid "Geo|Resync all projects"
+msgid "Geo|Resync all"
msgstr ""
msgid "Geo|Retry count"
@@ -9783,7 +10049,7 @@ msgstr ""
msgid "Geo|Reverify"
msgstr ""
-msgid "Geo|Reverify all projects"
+msgid "Geo|Reverify all"
msgstr ""
msgid "Geo|Status"
@@ -10125,9 +10391,6 @@ msgstr ""
msgid "Go to file"
msgstr ""
-msgid "Go to file (MRs only)"
-msgstr ""
-
msgid "Go to file permalink (while viewing a file)"
msgstr ""
@@ -10572,6 +10835,9 @@ msgstr ""
msgid "GroupSettings|Disable group mentions"
msgstr ""
+msgid "GroupSettings|Export group"
+msgstr ""
+
msgid "GroupSettings|If the parent group's visibility is lower than the group current visibility, visibility levels for subgroups and projects will be changed to match the new parent group's visibility."
msgstr ""
@@ -10650,6 +10916,9 @@ msgstr ""
msgid "Groups (%{count})"
msgstr ""
+msgid "Groups (%{groups})"
+msgstr ""
+
msgid "Groups can also be nested by creating %{subgroup_docs_link_start}subgroups%{subgroup_docs_link_end}."
msgstr ""
@@ -10919,15 +11188,12 @@ msgstr ""
msgid "ID:"
msgstr ""
-msgid "IDE|Allow live previews of JavaScript projects in the Web IDE using CodeSandbox client side evaluation."
+msgid "IDE|Allow live previews of JavaScript projects in the Web IDE using CodeSandbox Live Preview."
msgstr ""
msgid "IDE|Back"
msgstr ""
-msgid "IDE|Client side evaluation"
-msgstr ""
-
msgid "IDE|Commit"
msgstr ""
@@ -10991,6 +11257,9 @@ msgstr ""
msgid "If any job surpasses this timeout threshold, it will be marked as failed. Human readable time input language is accepted like \"1 hour\". Values without specification represent seconds."
msgstr ""
+msgid "If blank, set allowable lifetime to %{instance_level_policy_in_words}, as defined by the instance admin. Once set, existing tokens for users in this group may be revoked."
+msgstr ""
+
msgid "If checked, group owners can manage LDAP group links and LDAP member overrides"
msgstr ""
@@ -11030,6 +11299,9 @@ msgstr ""
msgid "If you lose your recovery codes you can generate new ones, invalidating all previous codes."
msgstr ""
+msgid "If you reach 100%% storage capacity, you will not be able to: %{base_message}"
+msgstr ""
+
msgid "If your HTTP repository is not publicly accessible, add your credentials."
msgstr ""
@@ -11141,6 +11413,9 @@ msgstr ""
msgid "Import repository"
msgstr ""
+msgid "Import started by: %{importInitiator}"
+msgstr ""
+
msgid "Import tasks"
msgstr ""
@@ -11207,6 +11482,9 @@ msgstr ""
msgid "In order to tailor your experience with GitLab we<br>would like to know a bit more about you."
msgstr ""
+msgid "In progress"
+msgstr ""
+
msgid "In the next step, you'll be able to select the projects you want to import."
msgstr ""
@@ -11556,15 +11834,15 @@ msgstr ""
msgid "Issues"
msgstr ""
-msgid "Issues / Merge Requests"
-msgstr ""
-
msgid "Issues Analytics"
msgstr ""
msgid "Issues Rate Limits"
msgstr ""
+msgid "Issues and Merge Requests"
+msgstr ""
+
msgid "Issues can be bugs, tasks or ideas to be discussed. Also, issues are searchable and filterable."
msgstr ""
@@ -11586,10 +11864,10 @@ msgstr ""
msgid "IssuesAnalytics|Avg/Month:"
msgstr ""
-msgid "IssuesAnalytics|Issues created"
+msgid "IssuesAnalytics|Issues opened"
msgstr ""
-msgid "IssuesAnalytics|Issues created per month"
+msgid "IssuesAnalytics|Issues opened per month"
msgstr ""
msgid "IssuesAnalytics|Last 12 months"
@@ -11643,6 +11921,9 @@ msgstr ""
msgid "Jira integration not configured."
msgstr ""
+msgid "Jira project: %{importProject}"
+msgstr ""
+
msgid "JiraService|Events for %{noteable_model_name} are disabled."
msgstr ""
@@ -12034,6 +12315,9 @@ msgstr ""
msgid "Lead"
msgstr ""
+msgid "Lead Time"
+msgstr ""
+
msgid "Learn GitLab"
msgstr ""
@@ -12046,6 +12330,9 @@ msgstr ""
msgid "Learn how to %{no_packages_link_start}publish and share your packages%{no_packages_link_end} with GitLab."
msgstr ""
+msgid "Learn how to enable synchronization"
+msgstr ""
+
msgid "Learn more"
msgstr ""
@@ -12387,9 +12674,6 @@ msgstr ""
msgid "Live preview"
msgstr ""
-msgid "Load more vulnerabilities"
-msgstr ""
-
msgid "Loading"
msgstr ""
@@ -12540,9 +12824,6 @@ msgstr ""
msgid "Manage"
msgstr ""
-msgid "Manage Git repositories with fine-grained access controls that keep your code secure. Perform code reviews and enhance collaboration with merge requests. Each project can also have an issue tracker and a wiki."
-msgstr ""
-
msgid "Manage Web IDE features"
msgstr ""
@@ -12573,6 +12854,9 @@ msgstr ""
msgid "Manage your license"
msgstr ""
+msgid "Managed Account"
+msgstr ""
+
msgid "Manifest"
msgstr ""
@@ -12837,6 +13121,9 @@ msgstr ""
msgid "Merge Requests in Review"
msgstr ""
+msgid "Merge automatically (%{strategy})"
+msgstr ""
+
msgid "Merge commit message"
msgstr ""
@@ -12990,6 +13277,12 @@ msgstr ""
msgid "Merged branches are being deleted. This can take some time depending on the number of branches. Please refresh the page to see changes."
msgstr ""
+msgid "Merged this merge request."
+msgstr ""
+
+msgid "Merges this merge request immediately."
+msgstr ""
+
msgid "Merges this merge request when the pipeline succeeds."
msgstr ""
@@ -13099,12 +13392,15 @@ msgstr[1] ""
msgstr[2] ""
msgstr[3] ""
-msgid "Metrics|Environment"
+msgid "Metrics|Expand panel"
msgstr ""
msgid "Metrics|For grouping similar metrics"
msgstr ""
+msgid "Metrics|Go back (Esc)"
+msgstr ""
+
msgid "Metrics|Invalid time range, please verify."
msgstr ""
@@ -13135,9 +13431,6 @@ msgstr ""
msgid "Metrics|Refresh dashboard"
msgstr ""
-msgid "Metrics|Show last"
-msgstr ""
-
msgid "Metrics|There was an error creating the dashboard."
msgstr ""
@@ -13216,6 +13509,9 @@ msgstr ""
msgid "Microsoft Azure"
msgstr ""
+msgid "Middleman project with Static Site Editor support"
+msgstr ""
+
msgid "Migrated %{success_count}/%{total_count} files."
msgstr ""
@@ -13665,10 +13961,10 @@ msgstr ""
msgid "Next"
msgstr ""
-msgid "Next file in diff (MRs only)"
+msgid "Next file in diff"
msgstr ""
-msgid "Next unresolved discussion (MRs only)"
+msgid "Next unresolved discussion"
msgstr ""
msgid "Nickname"
@@ -13728,6 +14024,9 @@ msgstr ""
msgid "No changes between %{ref_start}%{source_branch}%{ref_end} and %{ref_start}%{target_branch}%{ref_end}"
msgstr ""
+msgid "No child epics match applied filters"
+msgstr ""
+
msgid "No connection could be made to a Gitaly Server, please check your logs!"
msgstr ""
@@ -13866,15 +14165,6 @@ msgstr ""
msgid "No thanks, don't show this again"
msgstr ""
-msgid "No vulnerabilities found for this group"
-msgstr ""
-
-msgid "No vulnerabilities found for this pipeline"
-msgstr ""
-
-msgid "No vulnerabilities found for this project"
-msgstr ""
-
msgid "No vulnerabilities present"
msgstr ""
@@ -13998,6 +14288,9 @@ msgstr ""
msgid "Nothing to preview."
msgstr ""
+msgid "Nothing to synchronize"
+msgstr ""
+
msgid "Notification events"
msgstr ""
@@ -14169,6 +14462,9 @@ msgstr ""
msgid "Once removed, the fork relationship cannot be restored and you will no longer be able to send merge requests to the source."
msgstr ""
+msgid "Once the exported file is ready you can download it from this page."
+msgstr ""
+
msgid "Once the exported file is ready, you will receive a notification email with a download link, or you can download it from this page."
msgstr ""
@@ -14272,9 +14568,6 @@ msgstr ""
msgid "Open sidebar"
msgstr ""
-msgid "Open source software to collaborate on code"
-msgstr ""
-
msgid "Open: %{openIssuesCount}"
msgstr ""
@@ -14377,6 +14670,9 @@ msgstr ""
msgid "OutdatedBrowser|You can provide feedback %{feedback_link_start}on this issue%{feedback_link_end} or via your usual support channels."
msgstr ""
+msgid "Overridden"
+msgstr ""
+
msgid "Overview"
msgstr ""
@@ -14428,6 +14724,9 @@ msgstr ""
msgid "PackageRegistry|Conan Command"
msgstr ""
+msgid "PackageRegistry|Copy .pypirc content"
+msgstr ""
+
msgid "PackageRegistry|Copy Conan Command"
msgstr ""
@@ -14449,6 +14748,9 @@ msgstr ""
msgid "PackageRegistry|Copy NuGet Setup Command"
msgstr ""
+msgid "PackageRegistry|Copy Pip command"
+msgstr ""
+
msgid "PackageRegistry|Copy and paste this inside your %{codeStart}pom.xml%{codeEnd} %{codeStart}dependencies%{codeEnd} block."
msgstr ""
@@ -14482,6 +14784,12 @@ msgstr ""
msgid "PackageRegistry|For more information on the NuGet registry, %{linkStart}see the documentation%{linkEnd}."
msgstr ""
+msgid "PackageRegistry|For more information on the PyPi registry, %{linkStart}see the documentation%{linkEnd}."
+msgstr ""
+
+msgid "PackageRegistry|If you haven't already done so, you will need to add the below to your %{codeStart}.pypirc%{codeEnd} file."
+msgstr ""
+
msgid "PackageRegistry|If you haven't already done so, you will need to add the below to your %{codeStart}pom.xml%{codeEnd} file."
msgstr ""
@@ -14491,6 +14799,9 @@ msgstr ""
msgid "PackageRegistry|Learn how to %{noPackagesLinkStart}publish and share your packages%{noPackagesLinkEnd} with GitLab."
msgstr ""
+msgid "PackageRegistry|Manually Published"
+msgstr ""
+
msgid "PackageRegistry|Maven"
msgstr ""
@@ -14509,12 +14820,18 @@ msgstr ""
msgid "PackageRegistry|NuGet Command"
msgstr ""
+msgid "PackageRegistry|Pip Command"
+msgstr ""
+
msgid "PackageRegistry|Pipeline %{linkStart}%{linkEnd} triggered %{timestamp} by %{author}"
msgstr ""
msgid "PackageRegistry|Published to the repository at %{timestamp}"
msgstr ""
+msgid "PackageRegistry|PyPi"
+msgstr ""
+
msgid "PackageRegistry|Registry Setup"
msgstr ""
@@ -14551,6 +14868,9 @@ msgstr ""
msgid "PackageRegistry|npm"
msgstr ""
+msgid "PackageRegistry|published by %{author}"
+msgstr ""
+
msgid "PackageRegistry|yarn"
msgstr ""
@@ -14566,6 +14886,9 @@ msgstr ""
msgid "PackageType|NuGet"
msgstr ""
+msgid "PackageType|PyPi"
+msgstr ""
+
msgid "Packages"
msgstr ""
@@ -14659,6 +14982,9 @@ msgstr ""
msgid "Past due"
msgstr ""
+msgid "Paste a machine public key here. Read more about how to generate it"
+msgstr ""
+
msgid "Paste a machine public key here. Read more about how to generate it %{link_start}here%{link_end}"
msgstr ""
@@ -14674,9 +15000,6 @@ msgstr ""
msgid "Path"
msgstr ""
-msgid "Path, transfer, remove"
-msgstr ""
-
msgid "Path:"
msgstr ""
@@ -14707,7 +15030,7 @@ msgstr ""
msgid "Percentage"
msgstr ""
-msgid "Perform advanced options such as changing path, transferring, or removing the group."
+msgid "Perform advanced options such as changing path, transferring, exporting, or removing the group."
msgstr ""
msgid "Perform common operations on GitLab project"
@@ -14788,9 +15111,6 @@ msgstr ""
msgid "Pipeline minutes quota"
msgstr ""
-msgid "Pipeline quota"
-msgstr ""
-
msgid "Pipeline subscriptions"
msgstr ""
@@ -14800,6 +15120,9 @@ msgstr ""
msgid "Pipeline: %{status}"
msgstr ""
+msgid "PipelineCharts|CI / CD Analytics"
+msgstr ""
+
msgid "PipelineCharts|Failed:"
msgstr ""
@@ -14878,15 +15201,6 @@ msgstr ""
msgid "Pipelines settings for '%{project_name}' were successfully updated."
msgstr ""
-msgid "Pipelines| to purchase more minutes."
-msgstr ""
-
-msgid "Pipelines|%{namespace_name} has exceeded its pipeline minutes quota."
-msgstr ""
-
-msgid "Pipelines|%{namespace_name} has less than %{notification_level}%% of CI minutes available."
-msgstr ""
-
msgid "Pipelines|API"
msgstr ""
@@ -14908,10 +15222,13 @@ msgstr ""
msgid "Pipelines|Get started with Pipelines"
msgstr ""
-msgid "Pipelines|Loading Pipelines"
+msgid "Pipelines|Group %{namespace_name} has %{percentage}%% or less Shared Runner Pipeline minutes remaining. Once it runs out, no new jobs or pipelines in its projects will run."
msgstr ""
-msgid "Pipelines|Pipelines will not run anymore on shared Runners."
+msgid "Pipelines|Group %{namespace_name} has exceeded its pipeline minutes quota. Unless you buy additional pipeline minutes, no new jobs or pipelines in its projects will run."
+msgstr ""
+
+msgid "Pipelines|Loading Pipelines"
msgstr ""
msgid "Pipelines|Project cache successfully reset."
@@ -15142,6 +15459,9 @@ msgstr ""
msgid "Please select"
msgstr ""
+msgid "Please select a Jira project"
+msgstr ""
+
msgid "Please select a country"
msgstr ""
@@ -15187,6 +15507,9 @@ msgstr ""
msgid "Please wait while we import the repository for you. Refresh at will."
msgstr ""
+msgid "Plugins directory is deprecated and will be removed in 14.0. Please move this file into /file_hooks directory."
+msgstr ""
+
msgid "Pod does not exist"
msgstr ""
@@ -15304,7 +15627,7 @@ msgstr ""
msgid "Prevent users from changing their profile name"
msgstr ""
-msgid "Prevent users from modifing merge request approvers list"
+msgid "Prevent users from modifying merge request approvers list"
msgstr ""
msgid "Prevent users from performing write operations on GitLab while performing maintenance."
@@ -15325,10 +15648,10 @@ msgstr ""
msgid "Previous Artifacts"
msgstr ""
-msgid "Previous file in diff (MRs only)"
+msgid "Previous file in diff"
msgstr ""
-msgid "Previous unresolved discussion (MRs only)"
+msgid "Previous unresolved discussion"
msgstr ""
msgid "Primary"
@@ -15361,6 +15684,9 @@ msgstr ""
msgid "Private profile"
msgstr ""
+msgid "Private projects Minutes cost factor"
+msgstr ""
+
msgid "Private projects can be created in your personal namespace with:"
msgstr ""
@@ -16189,6 +16515,9 @@ msgstr ""
msgid "ProjectSettings|With GitLab Pages you can host your static websites on GitLab"
msgstr ""
+msgid "ProjectSettings|With Metrics Dashboard you can visualize this project performance metrics"
+msgstr ""
+
msgid "ProjectTemplates|.NET Core"
msgstr ""
@@ -16252,6 +16581,9 @@ msgstr ""
msgid "ProjectTemplates|Spring"
msgstr ""
+msgid "ProjectTemplates|Static Site Editor/Middleman"
+msgstr ""
+
msgid "ProjectTemplates|iOS (Swift)"
msgstr ""
@@ -16471,13 +16803,13 @@ msgstr ""
msgid "PrometheusService|Prometheus is being automatically managed on your clusters"
msgstr ""
-msgid "PrometheusService|These metrics will only be monitored after your first deployment to an environment"
+msgid "PrometheusService|Select the Active checkbox to override the Auto Configuration with custom settings. If unchecked, Auto Configuration settings are used."
msgstr ""
-msgid "PrometheusService|Time-series monitoring service"
+msgid "PrometheusService|These metrics will only be monitored after your first deployment to an environment"
msgstr ""
-msgid "PrometheusService|To enable manual configuration, uninstall Prometheus from your clusters"
+msgid "PrometheusService|Time-series monitoring service"
msgstr ""
msgid "PrometheusService|To enable the installation of Prometheus on your clusters, deactivate the manual configuration below"
@@ -16663,6 +16995,9 @@ msgstr ""
msgid "Protip:"
msgstr ""
+msgid "Protocol"
+msgstr ""
+
msgid "Provider"
msgstr ""
@@ -16687,6 +17022,9 @@ msgstr ""
msgid "Public pipelines"
msgstr ""
+msgid "Public projects Minutes cost factor"
+msgstr ""
+
msgid "Pull"
msgstr ""
@@ -16889,6 +17227,9 @@ msgstr[1] ""
msgstr[2] ""
msgstr[3] ""
+msgid "Regenerate export"
+msgstr ""
+
msgid "Regenerate instance ID"
msgstr ""
@@ -17268,9 +17609,31 @@ msgstr ""
msgid "Reports|%{combinedString} and %{resolvedString}"
msgstr ""
+msgid "Reports|Accessibility scanning detected %d issue for the source branch only"
+msgid_plural "Reports|Accessibility scanning detected %d issues for the source branch only"
+msgstr[0] ""
+msgstr[1] ""
+msgstr[2] ""
+msgstr[3] ""
+
+msgid "Reports|Accessibility scanning detected no issues for the source branch only"
+msgstr ""
+
+msgid "Reports|Accessibility scanning failed loading results"
+msgstr ""
+
+msgid "Reports|Accessibility scanning results are being parsed"
+msgstr ""
+
msgid "Reports|Actions"
msgstr ""
+msgid "Reports|An error occured while loading report"
+msgstr ""
+
+msgid "Reports|An error occurred while loading %{name} results"
+msgstr ""
+
msgid "Reports|Class"
msgstr ""
@@ -17376,7 +17739,7 @@ msgstr ""
msgid "Requested %{time_ago}"
msgstr ""
-msgid "Requested design version does not exist"
+msgid "Requested design version does not exist."
msgstr ""
msgid "Requested states are invalid"
@@ -17564,6 +17927,9 @@ msgstr ""
msgid "Resync"
msgstr ""
+msgid "Resync all"
+msgstr ""
+
msgid "Resync all %{replicableType}"
msgstr ""
@@ -17634,6 +18000,9 @@ msgstr ""
msgid "Revoked personal access token %{personal_access_token_name}!"
msgstr ""
+msgid "Revoked project access token %{project_access_token_name}!"
+msgstr ""
+
msgid "RightSidebar|adding a"
msgstr ""
@@ -17682,6 +18051,9 @@ msgstr ""
msgid "Runner token"
msgstr ""
+msgid "Runner tokens"
+msgstr ""
+
msgid "Runner was not updated."
msgstr ""
@@ -17736,6 +18108,9 @@ msgstr ""
msgid "SAML SSO for %{group_name}"
msgstr ""
+msgid "SAML discovery tokens"
+msgstr ""
+
msgid "SAML for %{group_name}"
msgstr ""
@@ -17820,12 +18195,18 @@ msgstr ""
msgid "Scheduled"
msgstr ""
+msgid "Scheduled to merge this merge request (%{strategy})."
+msgstr ""
+
msgid "Scheduled to merge this merge request when the pipeline succeeds."
msgstr ""
msgid "Schedules"
msgstr ""
+msgid "Schedules to merge this merge request (%{strategy})."
+msgstr ""
+
msgid "Scheduling"
msgstr ""
@@ -17883,6 +18264,9 @@ msgstr ""
msgid "Search branches and tags"
msgstr ""
+msgid "Search by author"
+msgstr ""
+
msgid "Search files"
msgstr ""
@@ -18044,13 +18428,6 @@ msgstr[1] ""
msgstr[2] ""
msgstr[3] ""
-msgid "SearchResults|snippet result"
-msgid_plural "SearchResults|snippet results"
-msgstr[0] ""
-msgstr[1] ""
-msgstr[2] ""
-msgstr[3] ""
-
msgid "SearchResults|user"
msgid_plural "SearchResults|users"
msgstr[0] ""
@@ -18065,6 +18442,9 @@ msgstr[1] ""
msgstr[2] ""
msgstr[3] ""
+msgid "Searching by both author and message is currently not supported."
+msgstr ""
+
msgid "Seat Link"
msgstr ""
@@ -18092,184 +18472,232 @@ msgstr ""
msgid "Security Dashboard"
msgstr ""
-msgid "Security Dashboard|Error fetching the vulnerability counts. Please check your network connection and try again."
+msgid "Security configuration help link"
msgstr ""
-msgid "Security Dashboard|Error fetching the vulnerability list. Please check your network connection and try again."
+msgid "Security dashboard"
msgstr ""
-msgid "Security Dashboard|Issue Created"
+msgid "Security report is out of date. Please update your branch with the latest changes from the target branch (%{targetBranchName})"
msgstr ""
-msgid "Security Reports|Comment added to '%{vulnerabilityName}'"
+msgid "Security report is out of date. Run %{newPipelineLinkStart}a new pipeline%{newPipelineLinkEnd} for the target branch (%{targetBranchName})"
msgstr ""
-msgid "Security Reports|Comment deleted on '%{vulnerabilityName}'"
+msgid "SecurityConfiguration|Configured"
msgstr ""
-msgid "Security Reports|Comment edited on '%{vulnerabilityName}'"
+msgid "SecurityConfiguration|Feature"
msgstr ""
-msgid "Security Reports|Create issue"
+msgid "SecurityConfiguration|Feature documentation for %{featureName}"
msgstr ""
-msgid "Security Reports|Dismiss vulnerability"
+msgid "SecurityConfiguration|Not yet configured"
msgstr ""
-msgid "Security Reports|Dismissed '%{vulnerabilityName}'"
+msgid "SecurityConfiguration|Secure features"
msgstr ""
-msgid "Security Reports|Dismissed '%{vulnerabilityName}'. Turn off the hide dismissed toggle to view."
+msgid "SecurityConfiguration|Status"
msgstr ""
-msgid "Security Reports|Either you don't have permission to view this dashboard or the dashboard has not been setup. Please check your permission settings with your administrator or check your dashboard configurations to proceed."
+msgid "SecurityReports|%{firstProject} and %{secondProject}"
msgstr ""
-msgid "Security Reports|Learn more about setting up your dashboard"
+msgid "SecurityReports|%{firstProject}, %{secondProject}, and %{rest}"
msgstr ""
-msgid "Security Reports|More info"
+msgid "SecurityReports|Add a project to your dashboard"
msgstr ""
-msgid "Security Reports|Oops, something doesn't seem right."
+msgid "SecurityReports|Add or remove projects from your dashboard"
msgstr ""
-msgid "Security Reports|Security reports can only be accessed by authorized users."
+msgid "SecurityReports|Add projects"
msgstr ""
-msgid "Security Reports|There was an error adding the comment."
+msgid "SecurityReports|Comment added to '%{vulnerabilityName}'"
msgstr ""
-msgid "Security Reports|There was an error creating the issue."
+msgid "SecurityReports|Comment deleted on '%{vulnerabilityName}'"
msgstr ""
-msgid "Security Reports|There was an error creating the merge request."
+msgid "SecurityReports|Comment edited on '%{vulnerabilityName}'"
msgstr ""
-msgid "Security Reports|There was an error deleting the comment."
+msgid "SecurityReports|Create issue"
msgstr ""
-msgid "Security Reports|There was an error dismissing the vulnerabilities."
+msgid "SecurityReports|Dismiss Selected"
msgstr ""
-msgid "Security Reports|There was an error dismissing the vulnerability."
+msgid "SecurityReports|Dismiss vulnerability"
msgstr ""
-msgid "Security Reports|There was an error reverting the dismissal."
+msgid "SecurityReports|Dismissed '%{vulnerabilityName}'"
msgstr ""
-msgid "Security Reports|There was an error reverting this dismissal."
+msgid "SecurityReports|Dismissed '%{vulnerabilityName}'. Turn off the hide dismissed toggle to view."
msgstr ""
-msgid "Security Reports|Undo dismiss"
+msgid "SecurityReports|Each vulnerability now has a unique page that can be directly linked to, shared, referenced, and tracked as the single source of truth. Vulnerability occurrences also persist across scanner runs, which improves tracking and visibility and reduces duplicates between scans."
msgstr ""
-msgid "Security Reports|You do not have sufficient permissions to access this report"
+msgid "SecurityReports|Edit dashboard"
msgstr ""
-msgid "Security Reports|You must sign in as an authorized user to see this report"
+msgid "SecurityReports|Either you don't have permission to view this dashboard or the dashboard has not been setup. Please check your permission settings with your administrator or check your dashboard configurations to proceed."
msgstr ""
-msgid "Security configuration help link"
+msgid "SecurityReports|Error fetching the vulnerability counts. Please check your network connection and try again."
msgstr ""
-msgid "Security dashboard"
+msgid "SecurityReports|Error fetching the vulnerability list. Please check your network connection and try again."
msgstr ""
-msgid "Security report is out of date. Please update your branch with the latest changes from the target branch (%{targetBranchName})"
+msgid "SecurityReports|False positive"
msgstr ""
-msgid "Security report is out of date. Run %{newPipelineLinkStart}a new pipeline%{newPipelineLinkEnd} for the target branch (%{targetBranchName})"
+msgid "SecurityReports|Hide dismissed"
msgstr ""
-msgid "SecurityConfiguration|Configured"
+msgid "SecurityReports|Introducing standalone vulnerabilities"
msgstr ""
-msgid "SecurityConfiguration|Feature"
+msgid "SecurityReports|Issue Created"
msgstr ""
-msgid "SecurityConfiguration|Feature documentation for %{featureName}"
+msgid "SecurityReports|Learn More"
msgstr ""
-msgid "SecurityConfiguration|Not yet configured"
+msgid "SecurityReports|Learn more about setting up your dashboard"
msgstr ""
-msgid "SecurityConfiguration|Secure features"
+msgid "SecurityReports|Load more vulnerabilities"
msgstr ""
-msgid "SecurityConfiguration|Status"
+msgid "SecurityReports|Monitor vulnerabilities in your code"
+msgstr ""
+
+msgid "SecurityReports|More info"
+msgstr ""
+
+msgid "SecurityReports|More information"
+msgstr ""
+
+msgid "SecurityReports|No vulnerabilities found for dashboard"
+msgstr ""
+
+msgid "SecurityReports|No vulnerabilities found for this group"
+msgstr ""
+
+msgid "SecurityReports|No vulnerabilities found for this pipeline"
+msgstr ""
+
+msgid "SecurityReports|No vulnerabilities found for this project"
+msgstr ""
+
+msgid "SecurityReports|Oops, something doesn't seem right."
+msgstr ""
+
+msgid "SecurityReports|Pipeline %{pipelineLink} triggered %{timeago} by %{user}"
msgstr ""
-msgid "SecurityDashboard|%{firstProject} and %{secondProject}"
+msgid "SecurityReports|Project"
msgstr ""
-msgid "SecurityDashboard|%{firstProject}, %{secondProject}, and %{rest}"
+msgid "SecurityReports|Projects added"
msgstr ""
-msgid "SecurityDashboard|Add a project to your dashboard"
+msgid "SecurityReports|Remove project from dashboard"
msgstr ""
-msgid "SecurityDashboard|Add or remove projects from your dashboard"
+msgid "SecurityReports|Report type"
msgstr ""
-msgid "SecurityDashboard|Add projects"
+msgid "SecurityReports|Return to dashboard"
msgstr ""
-msgid "SecurityDashboard|Edit dashboard"
+msgid "SecurityReports|Security Dashboard"
msgstr ""
-msgid "SecurityDashboard|Hide dismissed"
+msgid "SecurityReports|Security reports can only be accessed by authorized users."
msgstr ""
-msgid "SecurityDashboard|Monitor vulnerabilities in your code"
+msgid "SecurityReports|Select a project to add by using the project search field above."
msgstr ""
-msgid "SecurityDashboard|More information"
+msgid "SecurityReports|Select a reason"
msgstr ""
-msgid "SecurityDashboard|No vulnerabilities found for dashboard"
+msgid "SecurityReports|Severity"
msgstr ""
-msgid "SecurityDashboard|Pipeline %{pipelineLink} triggered %{timeago} by %{user}"
+msgid "SecurityReports|Status"
msgstr ""
-msgid "SecurityDashboard|Project"
+msgid "SecurityReports|The security dashboard displays the latest security findings for projects you wish to monitor. Select \"Edit dashboard\" to add and remove projects."
msgstr ""
-msgid "SecurityDashboard|Projects added"
+msgid "SecurityReports|The security dashboard displays the latest security report. Use it to find and fix vulnerabilities."
msgstr ""
-msgid "SecurityDashboard|Remove project from dashboard"
+msgid "SecurityReports|There was an error adding the comment."
msgstr ""
-msgid "SecurityDashboard|Report type"
+msgid "SecurityReports|There was an error creating the issue."
msgstr ""
-msgid "SecurityDashboard|Return to dashboard"
+msgid "SecurityReports|There was an error creating the merge request."
msgstr ""
-msgid "SecurityDashboard|Security Dashboard"
+msgid "SecurityReports|There was an error deleting the comment."
msgstr ""
-msgid "SecurityDashboard|Select a project to add by using the project search field above."
+msgid "SecurityReports|There was an error dismissing the vulnerabilities."
msgstr ""
-msgid "SecurityDashboard|Severity"
+msgid "SecurityReports|There was an error dismissing the vulnerability."
msgstr ""
-msgid "SecurityDashboard|Status"
+msgid "SecurityReports|There was an error reverting the dismissal."
msgstr ""
-msgid "SecurityDashboard|The security dashboard displays the latest security findings for projects you wish to monitor. Select \"Edit dashboard\" to add and remove projects."
+msgid "SecurityReports|There was an error reverting this dismissal."
msgstr ""
-msgid "SecurityDashboard|The security dashboard displays the latest security report. Use it to find and fix vulnerabilities."
+msgid "SecurityReports|There was an error while generating the report."
msgstr ""
-msgid "SecurityDashboard|There was an error while generating the report."
+msgid "SecurityReports|Unable to add %{invalidProjects}"
msgstr ""
-msgid "SecurityDashboard|Unable to add %{invalidProjects}"
+msgid "SecurityReports|Undo dismiss"
+msgstr ""
+
+msgid "SecurityReports|While it's rare to have no vulnerabilities for your group, it can happen. In any event, we ask that you double check your settings to make sure you've set up your dashboard correctly."
+msgstr ""
+
+msgid "SecurityReports|While it's rare to have no vulnerabilities for your pipeline, it can happen. In any event, we ask that you double check your settings to make sure all security scanning jobs have passed successfully."
+msgstr ""
+
+msgid "SecurityReports|While it's rare to have no vulnerabilities for your project, it can happen. In any event, we ask that you double check your settings to make sure you've set up your dashboard correctly."
+msgstr ""
+
+msgid "SecurityReports|While it's rare to have no vulnerabilities, it can happen. In any event, we ask that you please double check your settings to make sure you've set up your dashboard correctly."
+msgstr ""
+
+msgid "SecurityReports|Won't fix / Accept risk"
+msgstr ""
+
+msgid "SecurityReports|You do not have sufficient permissions to access this report"
+msgstr ""
+
+msgid "SecurityReports|You must sign in as an authorized user to see this report"
+msgstr ""
+
+msgid "SecurityReports|[No reason]"
msgstr ""
msgid "See GitLab's %{password_policy_guidelines}"
@@ -18281,6 +18709,9 @@ msgstr ""
msgid "See the affected projects in the GitLab admin panel"
msgstr ""
+msgid "See what's new at GitLab"
+msgstr ""
+
msgid "Select"
msgstr ""
@@ -18686,9 +19117,6 @@ msgstr ""
msgid "Set up Jira Integration"
msgstr ""
-msgid "Set up Jira Integration illustration"
-msgstr ""
-
msgid "Set up a %{type} Runner automatically"
msgstr ""
@@ -18773,6 +19201,9 @@ msgstr ""
msgid "Severity: %{severity}"
msgstr ""
+msgid "Shards (%{shards})"
+msgstr ""
+
msgid "Shards to synchronize"
msgstr ""
@@ -19089,6 +19520,9 @@ msgstr ""
msgid "Snowplow"
msgstr ""
+msgid "Some child epics may be hidden due to applied filters"
+msgstr ""
+
msgid "Some email servers do not support overriding the email sender name. Enable this option to include the name of the author of the issue, merge request or comment in the email body instead."
msgstr ""
@@ -19230,6 +19664,9 @@ msgstr ""
msgid "Something went wrong while updating your list settings"
msgstr ""
+msgid "Something went wrong with your automatic subscription renewal"
+msgstr ""
+
msgid "Something went wrong, unable to add %{project} to dashboard"
msgstr ""
@@ -19515,6 +19952,9 @@ msgstr ""
msgid "Stage removed"
msgstr ""
+msgid "Standard"
+msgstr ""
+
msgid "Star a label to make it a priority label. Order the prioritized labels to change their relative priority, by dragging."
msgstr ""
@@ -19638,6 +20078,9 @@ msgstr ""
msgid "Static Application Security Testing (SAST)"
msgstr ""
+msgid "StaticSiteEditor|An error occurred while submitting your changes."
+msgstr ""
+
msgid "StaticSiteEditor|Branch could not be created."
msgstr ""
@@ -19647,18 +20090,30 @@ msgstr ""
msgid "StaticSiteEditor|Could not create merge request."
msgstr ""
+msgid "StaticSiteEditor|Incompatible file content"
+msgstr ""
+
msgid "StaticSiteEditor|Return to site"
msgstr ""
+msgid "StaticSiteEditor|Static site editor"
+msgstr ""
+
msgid "StaticSiteEditor|Success!"
msgstr ""
msgid "StaticSiteEditor|Summary of changes"
msgstr ""
+msgid "StaticSiteEditor|The Static Site Editor is currently configured to only edit Markdown content on pages generated from Middleman. Visit the documentation to learn more about configuring your site to use the Static Site Editor."
+msgstr ""
+
msgid "StaticSiteEditor|Update %{sourcePath} file"
msgstr ""
+msgid "StaticSiteEditor|View documentation"
+msgstr ""
+
msgid "StaticSiteEditor|View merge request"
msgstr ""
@@ -19716,6 +20171,12 @@ msgstr ""
msgid "StatusPage|Status page"
msgstr ""
+msgid "StatusPage|Status page URL"
+msgstr ""
+
+msgid "StatusPage|Status page frontend documentation"
+msgstr ""
+
msgid "StatusPage|To publish incidents to an external status page, GitLab will store a JSON file in your Amazon S3 account in a location accessible to your external status page service. Make sure to also set up %{docsLink}"
msgstr ""
@@ -20064,6 +20525,12 @@ msgstr ""
msgid "Synced"
msgstr ""
+msgid "Synchronization disabled"
+msgstr ""
+
+msgid "Synchronization of container repositories is disabled."
+msgstr ""
+
msgid "System"
msgstr ""
@@ -20217,6 +20684,9 @@ msgstr ""
msgid "Telephone number"
msgstr ""
+msgid "Telephone number (Optional)"
+msgstr ""
+
msgid "Template"
msgstr ""
@@ -20529,6 +20999,9 @@ msgstr ""
msgid "The license was successfully uploaded and is now active. You can see the details below."
msgstr ""
+msgid "The license was successfully uploaded and will be active from %{starts_at}. You can see the details below."
+msgstr ""
+
msgid "The maximum file size allowed is %{size}."
msgstr ""
@@ -20850,6 +21323,9 @@ msgstr ""
msgid "There was an error getting the epic participants."
msgstr ""
+msgid "There was an error importing the Jira project."
+msgstr ""
+
msgid "There was an error loading users activity calendar."
msgstr ""
@@ -20865,7 +21341,7 @@ msgstr ""
msgid "There was an error resetting user pipeline minutes."
msgstr ""
-msgid "There was an error saving this Geo Node"
+msgid "There was an error saving this Geo Node."
msgstr ""
msgid "There was an error saving your changes."
@@ -20928,6 +21404,9 @@ msgstr ""
msgid "They can be managed using the %{link}."
msgstr ""
+msgid "Third Party Advisory Link"
+msgstr ""
+
msgid "Third party offers"
msgstr ""
@@ -20964,6 +21443,9 @@ msgstr ""
msgid "This application will be able to:"
msgstr ""
+msgid "This attachment has been truncated to avoid exceeding the maximum allowed attachment size of 15MB. %{written_count} of %{issues_count} issues have been included. Consider re-exporting with a narrower selection of issues."
+msgstr ""
+
msgid "This block is self-referential"
msgstr ""
@@ -21105,6 +21587,12 @@ msgstr ""
msgid "This issue is confidential"
msgstr ""
+msgid "This issue is currently blocked by the following issues: %{issues}."
+msgstr ""
+
+msgid "This issue is in a child epic of the filtered epic"
+msgstr ""
+
msgid "This issue is locked."
msgstr ""
@@ -21303,7 +21791,10 @@ msgstr ""
msgid "This user will be the author of all events in the activity feed that are the result of an update, like new branches being created or new commits being pushed to existing branches. Upon creation or when reassigning you can only assign yourself to be the mirror user."
msgstr ""
-msgid "This variable can not be masked"
+msgid "This variable can not be masked."
+msgstr ""
+
+msgid "This variable does not match the expected pattern."
msgstr ""
msgid "This will help us personalize your onboarding experience."
@@ -21333,10 +21824,10 @@ msgstr ""
msgid "ThreatMonitoring|Application firewall not detected"
msgstr ""
-msgid "ThreatMonitoring|Container Network Policy"
+msgid "ThreatMonitoring|Container Network Policies are not installed or have been disabled. To view this data, ensure your Network Policies are installed and enabled for your cluster."
msgstr ""
-msgid "ThreatMonitoring|Container NetworkPolicies are not installed or has been disabled. To view this data, ensure you NetworkPolicies are installed and enabled for your cluster."
+msgid "ThreatMonitoring|Container Network Policy"
msgstr ""
msgid "ThreatMonitoring|Container NetworkPolicies not detected"
@@ -21369,7 +21860,7 @@ msgstr ""
msgid "ThreatMonitoring|Something went wrong, unable to fetch statistics"
msgstr ""
-msgid "ThreatMonitoring|The firewall is not installed or has been disabled. To view this data, ensure you firewall is installed and enabled for your cluster."
+msgid "ThreatMonitoring|The firewall is not installed or has been disabled. To view this data, ensure the web application firewall is installed and enabled for your cluster."
msgstr ""
msgid "ThreatMonitoring|The graph below is an overview of traffic coming to your application as tracked by the Web Application Firewall (WAF). View the docs for instructions on how to access the WAF logs to see what type of malicious traffic is trying to access your app. The docs link is also accessible by clicking the \"?\" icon next to the title below."
@@ -21438,6 +21929,9 @@ msgstr ""
msgid "Time in seconds GitLab will wait for a response from the external service. When the service does not respond in time, access will be denied."
msgstr ""
+msgid "Time of import: %{importTime}"
+msgstr ""
+
msgid "Time remaining"
msgstr ""
@@ -21626,7 +22120,7 @@ msgstr ""
msgid "Title:"
msgstr ""
-msgid "Titles and Filenames"
+msgid "Titles and Descriptions"
msgstr ""
msgid "To"
@@ -21833,7 +22327,7 @@ msgstr ""
msgid "Too many projects enabled. You will need to manage them via the console or the API."
msgstr ""
-msgid "Topics"
+msgid "Topics (optional)"
msgstr ""
msgid "Total"
@@ -21995,6 +22489,9 @@ msgstr ""
msgid "Try all GitLab has to offer for 30 days."
msgstr ""
+msgid "Try changing or removing filters."
+msgstr ""
+
msgid "Try to fork again"
msgstr ""
@@ -22088,6 +22585,9 @@ msgstr ""
msgid "Unable to connect to server: %{error}"
msgstr ""
+msgid "Unable to connect to the Jira instance. Please check your Jira integration configuration."
+msgstr ""
+
msgid "Unable to convert Kubernetes logs encoding to UTF-8"
msgstr ""
@@ -23035,6 +23535,9 @@ msgstr ""
msgid "View issue"
msgstr ""
+msgid "View issues"
+msgstr ""
+
msgid "View it on GitLab"
msgstr ""
@@ -23212,6 +23715,15 @@ msgstr ""
msgid "VulnerabilityManagement|Resolved %{timeago} by %{user}"
msgstr ""
+msgid "VulnerabilityManagement|Something went wrong while trying to delete the comment. Please try again later."
+msgstr ""
+
+msgid "VulnerabilityManagement|Something went wrong while trying to retrieve the vulnerability history. Please try again later."
+msgstr ""
+
+msgid "VulnerabilityManagement|Something went wrong while trying to save the comment. Please try again later."
+msgstr ""
+
msgid "VulnerabilityManagement|Something went wrong, could not create an issue."
msgstr ""
@@ -23242,6 +23754,9 @@ msgstr ""
msgid "VulnerabilityStatusTypes|Resolved"
msgstr ""
+msgid "Vulnerability|%{scannerName} (version %{scannerVersion})"
+msgstr ""
+
msgid "Vulnerability|Class"
msgstr ""
@@ -23275,7 +23790,10 @@ msgstr ""
msgid "Vulnerability|Project"
msgstr ""
-msgid "Vulnerability|Report Type"
+msgid "Vulnerability|Scanner Provider"
+msgstr ""
+
+msgid "Vulnerability|Scanner Type"
msgstr ""
msgid "Vulnerability|Severity"
@@ -23329,9 +23847,15 @@ msgstr ""
msgid "We sent you an email with reset password instructions"
msgstr ""
+msgid "We tried to automatically renew your %{strong}%{plan_name}%{strong_close} subscription for %{strong}%{namespace_name}%{strong_close} on %{expires_on} but something went wrong so your subscription was downgraded to the free plan. Don't worry, your data is safe. We suggest you check your payment method and get in touch with our support team (%{support_link}). They'll gladly help with your subscription renewal."
+msgstr ""
+
msgid "We want to be sure it is you, please confirm you are not a robot."
msgstr ""
+msgid "We will automatically renew your %{strong}%{plan_name}%{strong_close} subscription for %{strong}%{namespace_name}%{strong_close} on %{strong}%{expires_on}%{strong_close}. There's nothing that you need to do, we'll let you know when the renewal is complete. Need more seats, a higher plan or just want to review your payment method?"
+msgstr ""
+
msgid "We've found no vulnerabilities"
msgstr ""
@@ -23438,15 +23962,6 @@ msgstr ""
msgid "When:"
msgstr ""
-msgid "While it's rare to have no vulnerabilities for your group, it can happen. In any event, we ask that you double check your settings to make sure you've set up your dashboard correctly."
-msgstr ""
-
-msgid "While it's rare to have no vulnerabilities for your pipeline, it can happen. In any event, we ask that you double check your settings to make sure all security scanning jobs have passed successfully."
-msgstr ""
-
-msgid "While it's rare to have no vulnerabilities for your project, it can happen. In any event, we ask that you double check your settings to make sure you've set up your dashboard correctly."
-msgstr ""
-
msgid "While it's rare to have no vulnerabilities, it can happen. In any event, we ask that you please double check your settings to make sure you've set up your dashboard correctly."
msgstr ""
@@ -23477,9 +23992,6 @@ msgstr ""
msgid "Wiki"
msgstr ""
-msgid "Wiki pages"
-msgstr ""
-
msgid "Wiki was successfully updated."
msgstr ""
@@ -23663,6 +24175,9 @@ msgstr ""
msgid "Yes, add it"
msgstr ""
+msgid "Yes, close issue"
+msgstr ""
+
msgid "Yes, let me map Google Code users to full names or GitLab users."
msgstr ""
@@ -23858,6 +24373,9 @@ msgstr ""
msgid "You could not create a new trigger."
msgstr ""
+msgid "You didn't renew your %{strong}%{plan_name}%{strong_close} subscription for %{strong}%{namespace_name}%{strong_close} so it was downgraded to the free plan."
+msgstr ""
+
msgid "You didn't renew your %{strong}%{plan_name}%{strong_close} subscription so it was downgraded to the GitLab Core Plan."
msgstr ""
@@ -23924,6 +24442,9 @@ msgstr ""
msgid "You have declined the invitation to join %{label}."
msgstr ""
+msgid "You have imported from this project %{numberOfPreviousImportsForProject} times before. Each new import will create duplicate issues."
+msgstr ""
+
msgid "You have no permissions"
msgstr ""
@@ -24005,6 +24526,9 @@ msgstr ""
msgid "You need to upload a Google Takeout archive."
msgstr ""
+msgid "You reached %{usage_in_percent} of %{namespace_name}'s capacity (%{used_storage} of %{storage_limit})"
+msgstr ""
+
msgid "You tried to fork %{link_to_the_project} but it failed for the following reason:"
msgstr ""
@@ -24089,9 +24613,21 @@ msgstr ""
msgid "YouTube"
msgstr ""
+msgid "Your %{strong}%{plan_name}%{strong_close} subscription for %{strong}%{namespace_name}%{strong_close} will expire on %{strong}%{expires_on}%{strong_close}. After that, you will not to be able to create issues or merge requests as well as many other features."
+msgstr ""
+
msgid "Your %{strong}%{plan_name}%{strong_close} subscription will expire on %{strong}%{expires_on}%{strong_close}. After that, you will not to be able to create issues or merge requests as well as many other features."
msgstr ""
+msgid "Your CSV export has started. It will be emailed to %{email} when complete."
+msgstr ""
+
+msgid "Your CSV export of %{issues_count} from project %{project_link} has been added to this email as an attachment."
+msgstr ""
+
+msgid "Your CSV export of %{written_count} from project %{project_name} (%{project_url}) has been added to this email as an attachment."
+msgstr ""
+
msgid "Your Commit Email will be used for web based operations, such as edits and merges."
msgstr ""
@@ -24188,6 +24724,9 @@ msgstr ""
msgid "Your comment could not be updated! Please check your network connection and try again."
msgstr ""
+msgid "Your comment will be discarded."
+msgstr ""
+
msgid "Your custom stage '%{title}' was created"
msgstr ""
@@ -24227,6 +24766,9 @@ msgstr ""
msgid "Your new personal access token has been created."
msgstr ""
+msgid "Your new project access token has been created."
+msgstr ""
+
msgid "Your password isn't required to view this page. If a password or any other personal details are requested, please contact your administrator to report abuse."
msgstr ""
@@ -24245,12 +24787,18 @@ msgstr ""
msgid "Your request for access has been queued for review."
msgstr ""
+msgid "Your search didn't match any commits."
+msgstr ""
+
msgid "Your subscription expired!"
msgstr ""
msgid "Your subscription has been downgraded"
msgstr ""
+msgid "Your subscription will automatically renew in %{remaining_days}"
+msgstr ""
+
msgid "Your subscription will expire in %{remaining_days}"
msgstr ""
@@ -24291,7 +24839,7 @@ msgstr ""
msgid "allowed to fail"
msgstr ""
-msgid "already being used for another group or project milestone."
+msgid "already being used for another group or project %{timebox_name}."
msgstr ""
msgid "already has a \"created\" issue link"
@@ -24369,6 +24917,9 @@ msgstr ""
msgid "ciReport|%{linkStartTag}Learn more about SAST %{linkEndTag}"
msgstr ""
+msgid "ciReport|%{linkStartTag}Learn more about Secret Scanning %{linkEndTag}"
+msgstr ""
+
msgid "ciReport|%{linkStartTag}Learn more about codequality reports %{linkEndTag}"
msgstr ""
@@ -24542,6 +25093,12 @@ msgstr ""
msgid "ciReport|SAST"
msgstr ""
+msgid "ciReport|Secret scanning"
+msgstr ""
+
+msgid "ciReport|Secret scanning detects secrets and credentials vulnerabilities in your source code."
+msgstr ""
+
msgid "ciReport|Security scanning"
msgstr ""
@@ -24747,6 +25304,9 @@ msgstr ""
msgid "group"
msgstr ""
+msgid "groups"
+msgstr ""
+
msgid "has already been linked to another vulnerability"
msgstr ""
@@ -24914,9 +25474,6 @@ msgstr[3] ""
msgid "merged %{time_ago}"
msgstr ""
-msgid "milestone should belong either to a project or a group."
-msgstr ""
-
msgid "missing"
msgstr ""
@@ -25253,6 +25810,9 @@ msgstr ""
msgid "no contributions"
msgstr ""
+msgid "no expiration"
+msgstr ""
+
msgid "no one can merge"
msgstr ""
@@ -25309,6 +25869,9 @@ msgstr ""
msgid "pending removal"
msgstr ""
+msgid "per day"
+msgstr ""
+
msgid "pipeline"
msgstr ""
@@ -25341,6 +25904,12 @@ msgstr[3] ""
msgid "project avatar"
msgstr ""
+msgid "projects"
+msgstr ""
+
+msgid "push to your repository, create pipelines, create issues or add comments. To reduce storage capacity, delete unused repositories, artifacts, wikis, issues, and pipelines."
+msgstr ""
+
msgid "quick actions"
msgstr ""
@@ -25489,13 +26058,6 @@ msgstr ""
msgid "this document"
msgstr ""
-msgid "thread resolved"
-msgid_plural "threads resolved"
-msgstr[0] ""
-msgstr[1] ""
-msgstr[2] ""
-msgstr[3] ""
-
msgid "to help your contributors communicate effectively!"
msgstr ""
@@ -25550,6 +26112,9 @@ msgstr ""
msgid "view the blob"
msgstr ""
+msgid "vulnerability|Add a comment"
+msgstr ""
+
msgid "vulnerability|Add a comment or reason for dismissal"
msgstr ""
diff --git a/locale/sl_SI/gitlab.po b/locale/sl_SI/gitlab.po
index 0efa281f6c2..283b8912e67 100644
--- a/locale/sl_SI/gitlab.po
+++ b/locale/sl_SI/gitlab.po
@@ -12,7 +12,7 @@ msgstr ""
"X-Crowdin-Project: gitlab-ee\n"
"X-Crowdin-Language: sl\n"
"X-Crowdin-File: /master/locale/gitlab.pot\n"
-"PO-Revision-Date: 2020-04-15 00:27\n"
+"PO-Revision-Date: 2020-05-05 21:34\n"
msgid " %{start} to %{end}"
msgstr ""
@@ -272,6 +272,13 @@ msgstr[1] ""
msgstr[2] ""
msgstr[3] ""
+msgid "%d unresolved thread"
+msgid_plural "%d unresolved threads"
+msgstr[0] ""
+msgstr[1] ""
+msgstr[2] ""
+msgstr[3] ""
+
msgid "%d vulnerability dismissed"
msgid_plural "%d vulnerabilities dismissed"
msgstr[0] ""
@@ -443,6 +450,9 @@ msgstr ""
msgid "%{mrText}, this issue will be closed automatically."
msgstr ""
+msgid "%{namespace_name} is now read-only. You cannot: %{base_message}"
+msgstr ""
+
msgid "%{name} contained %{resultsString}"
msgstr ""
@@ -455,6 +465,13 @@ msgstr ""
msgid "%{name}'s avatar"
msgstr ""
+msgid "%{no_of_days} day"
+msgid_plural "%{no_of_days} days"
+msgstr[0] ""
+msgstr[1] ""
+msgstr[2] ""
+msgstr[3] ""
+
msgid "%{number_commits_behind} commits behind %{default_branch}, %{number_commits_ahead} commits ahead"
msgstr ""
@@ -590,6 +607,9 @@ msgstr[3] ""
msgid "%{text} is available"
msgstr ""
+msgid "%{timebox_name} should belong either to a project or a group."
+msgstr ""
+
msgid "%{title} %{operator} %{threshold}"
msgstr ""
@@ -903,12 +923,18 @@ msgstr ""
msgid "<code>\"johnsmith@example.com\": \"johnsmith@example.com\"</code> will add \"By <a href=\"#\">johnsmith@example.com</a>\" to all issues and comments originally created by johnsmith@example.com. By default, the email address or username is masked to ensure the user's privacy. Use this option if you want to show the full email address."
msgstr ""
+msgid "<namespace / project>"
+msgstr ""
+
msgid "<no name set>"
msgstr ""
msgid "<no scopes selected>"
msgstr ""
+msgid "<project name>"
+msgstr ""
+
msgid "<strong>%{group_name}</strong> group members"
msgstr ""
@@ -942,6 +968,9 @@ msgstr ""
msgid "A basic page and serverless function that uses AWS Lambda, AWS API Gateway, and GitLab Pages"
msgstr ""
+msgid "A complete DevOps platform"
+msgstr ""
+
msgid "A default branch cannot be chosen for an empty project."
msgstr ""
@@ -1158,6 +1187,12 @@ msgstr ""
msgid "AccessTokens|reset it"
msgstr ""
+msgid "AccessibilityReport|Accessibility report artifact not found"
+msgstr ""
+
+msgid "AccessibilityReport|Failed to retrieve accessibility report"
+msgstr ""
+
msgid "AccessibilityReport|Learn More"
msgstr ""
@@ -1167,6 +1202,9 @@ msgstr ""
msgid "AccessibilityReport|New"
msgstr ""
+msgid "AccessibilityReport|The accessibility scanning found an error of the following type: %{code}"
+msgstr ""
+
msgid "Account"
msgstr ""
@@ -1411,6 +1449,9 @@ msgstr ""
msgid "Added at"
msgstr ""
+msgid "Added for this merge request"
+msgstr ""
+
msgid "Added in this version"
msgstr ""
@@ -1784,6 +1825,72 @@ msgstr[1] ""
msgstr[2] ""
msgstr[3] ""
+msgid "Alert Details"
+msgstr ""
+
+msgid "AlertManagement|Acknowledged"
+msgstr ""
+
+msgid "AlertManagement|Alert"
+msgstr ""
+
+msgid "AlertManagement|Authorize external service"
+msgstr ""
+
+msgid "AlertManagement|Display alerts from all your monitoring tools directly within GitLab. Streamline the investigation of your alerts and the escalation of alerts to incidents."
+msgstr ""
+
+msgid "AlertManagement|End time"
+msgstr ""
+
+msgid "AlertManagement|End time:"
+msgstr ""
+
+msgid "AlertManagement|Events"
+msgstr ""
+
+msgid "AlertManagement|Events:"
+msgstr ""
+
+msgid "AlertManagement|Full Alert Details"
+msgstr ""
+
+msgid "AlertManagement|More information"
+msgstr ""
+
+msgid "AlertManagement|No alerts available to display. If you think you're seeing this message in error, refresh the page."
+msgstr ""
+
+msgid "AlertManagement|No alerts to display."
+msgstr ""
+
+msgid "AlertManagement|Overview"
+msgstr ""
+
+msgid "AlertManagement|Resolved"
+msgstr ""
+
+msgid "AlertManagement|Severity"
+msgstr ""
+
+msgid "AlertManagement|Start time"
+msgstr ""
+
+msgid "AlertManagement|Start time:"
+msgstr ""
+
+msgid "AlertManagement|Status"
+msgstr ""
+
+msgid "AlertManagement|Surface alerts in GitLab"
+msgstr ""
+
+msgid "AlertManagement|There was an error displaying the alerts. Confirm your endpoint's configuration details to ensure alerts appear."
+msgstr ""
+
+msgid "AlertManagement|Triggered"
+msgstr ""
+
msgid "AlertService|%{linkStart}Learn more%{linkEnd} about configuring this endpoint to receive alerts."
msgstr ""
@@ -1853,6 +1960,9 @@ msgstr ""
msgid "All security scans are enabled because %{linkStart}Auto DevOps%{linkEnd} is enabled on this project"
msgstr ""
+msgid "All threads resolved"
+msgstr ""
+
msgid "All users"
msgstr ""
@@ -1871,6 +1981,9 @@ msgstr ""
msgid "Allow only the selected protocols to be used for Git access."
msgstr ""
+msgid "Allow owners to manage default branch protection per group"
+msgstr ""
+
msgid "Allow owners to manually add users outside of LDAP"
msgstr ""
@@ -1979,6 +2092,9 @@ msgstr ""
msgid "An error occurred fetching the dropdown data."
msgstr ""
+msgid "An error occurred fetching the project authors."
+msgstr ""
+
msgid "An error occurred previewing the blob"
msgstr ""
@@ -2060,6 +2176,9 @@ msgstr ""
msgid "An error occurred while fetching sidebar data"
msgstr ""
+msgid "An error occurred while fetching terraform reports."
+msgstr ""
+
msgid "An error occurred while fetching the Service Desk address."
msgstr ""
@@ -2297,6 +2416,9 @@ msgstr ""
msgid "Any"
msgstr ""
+msgid "Any Author"
+msgstr ""
+
msgid "Any Label"
msgstr ""
@@ -2520,10 +2642,10 @@ msgstr ""
msgid "Are you sure that you want to unarchive this project?"
msgstr ""
-msgid "Are you sure you want to cancel creating this comment?"
+msgid "Are you sure you want to cancel editing this comment?"
msgstr ""
-msgid "Are you sure you want to cancel editing this comment?"
+msgid "Are you sure you want to close this blocked issue?"
msgstr ""
msgid "Are you sure you want to delete %{name}?"
@@ -2553,6 +2675,9 @@ msgstr ""
msgid "Are you sure you want to deploy this environment?"
msgstr ""
+msgid "Are you sure you want to discard this comment?"
+msgstr ""
+
msgid "Are you sure you want to erase this build?"
msgstr ""
@@ -2731,6 +2856,9 @@ msgstr ""
msgid "At least one approval from a code owner is required to change files matching the respective CODEOWNER rules."
msgstr ""
+msgid "At least one logging option is required to be enabled"
+msgstr ""
+
msgid "At least one of group_id or project_id must be specified"
msgstr ""
@@ -2771,6 +2899,27 @@ msgstr ""
msgid "AuditEvents|Target"
msgstr ""
+msgid "AuditLogs|(removed)"
+msgstr ""
+
+msgid "AuditLogs|Action"
+msgstr ""
+
+msgid "AuditLogs|Author"
+msgstr ""
+
+msgid "AuditLogs|Date"
+msgstr ""
+
+msgid "AuditLogs|IP Address"
+msgstr ""
+
+msgid "AuditLogs|Object"
+msgstr ""
+
+msgid "AuditLogs|Target"
+msgstr ""
+
msgid "Aug"
msgstr ""
@@ -3053,9 +3202,6 @@ msgstr ""
msgid "BambooService|You must set up automatic revision labeling and a repository trigger in Bamboo."
msgstr ""
-msgid "Batch operations"
-msgstr ""
-
msgid "BatchComments|Delete all pending comments"
msgstr ""
@@ -3185,6 +3331,9 @@ msgstr ""
msgid "Boards"
msgstr ""
+msgid "Boards and Board Lists"
+msgstr ""
+
msgid "Boards|Collapse"
msgstr ""
@@ -3404,6 +3553,9 @@ msgstr ""
msgid "Buy GitLab Enterprise Edition"
msgstr ""
+msgid "Buy more Pipeline minutes"
+msgstr ""
+
msgid "By %{user_name}"
msgstr ""
@@ -3524,6 +3676,9 @@ msgstr ""
msgid "Can't scan the code?"
msgstr ""
+msgid "Can't update snippet: %{err}"
+msgstr ""
+
msgid "Canary"
msgstr ""
@@ -3692,6 +3847,9 @@ msgstr ""
msgid "Changing group path can have unintended side effects."
msgstr ""
+msgid "Charts"
+msgstr ""
+
msgid "Charts can't be displayed as the request for data has timed out. %{documentationLink}"
msgstr ""
@@ -3728,9 +3886,6 @@ msgstr ""
msgid "ChatMessage|and [%{count} more](%{pipeline_failed_jobs_url})"
msgstr ""
-msgid "ChatMessage|failed"
-msgstr ""
-
msgid "ChatMessage|has failed"
msgstr ""
@@ -3746,9 +3901,6 @@ msgstr ""
msgid "ChatMessage|in %{project_link}"
msgstr ""
-msgid "ChatMessage|passed"
-msgstr ""
-
msgid "Check again"
msgstr ""
@@ -4199,9 +4351,6 @@ msgstr ""
msgid "Click any <strong>project name</strong> in the project list below to navigate to the project milestone."
msgstr ""
-msgid "Click here"
-msgstr ""
-
msgid "Click the <strong>Download</strong> button and wait for downloading to complete."
msgstr ""
@@ -4589,6 +4738,12 @@ msgstr ""
msgid "ClusterIntegration|Fetching zones"
msgstr ""
+msgid "ClusterIntegration|Fluentd"
+msgstr ""
+
+msgid "ClusterIntegration|Fluentd is an open source data collector, which lets you unify the data collection and consumption for a better use and understanding of data. It requires at least one of the following logs to be successfully installed."
+msgstr ""
+
msgid "ClusterIntegration|GitLab Integration"
msgstr ""
@@ -4895,6 +5050,15 @@ msgstr ""
msgid "ClusterIntegration|Request to begin uninstalling failed"
msgstr ""
+msgid "ClusterIntegration|SIEM Hostname"
+msgstr ""
+
+msgid "ClusterIntegration|SIEM Port"
+msgstr ""
+
+msgid "ClusterIntegration|SIEM Protocol"
+msgstr ""
+
msgid "ClusterIntegration|Save changes"
msgstr ""
@@ -4991,6 +5155,12 @@ msgstr ""
msgid "ClusterIntegration|Select zone to choose machine type"
msgstr ""
+msgid "ClusterIntegration|Send Cilium Logs"
+msgstr ""
+
+msgid "ClusterIntegration|Send ModSecurity Logs"
+msgstr ""
+
msgid "ClusterIntegration|Service Token"
msgstr ""
@@ -5421,18 +5591,33 @@ msgstr ""
msgid "Compliance framework (optional)"
msgstr ""
+msgid "ComplianceFramework|GDPR"
+msgstr ""
+
msgid "ComplianceFramework|GDPR - General Data Protection Regulation"
msgstr ""
+msgid "ComplianceFramework|HIPAA"
+msgstr ""
+
msgid "ComplianceFramework|HIPAA - Health Insurance Portability and Accountability Act"
msgstr ""
+msgid "ComplianceFramework|PCI-DSS"
+msgstr ""
+
msgid "ComplianceFramework|PCI-DSS - Payment Card Industry-Data Security Standard"
msgstr ""
+msgid "ComplianceFramework|SOC 2"
+msgstr ""
+
msgid "ComplianceFramework|SOC 2 - Service Organization Control 2"
msgstr ""
+msgid "ComplianceFramework|SOX"
+msgstr ""
+
msgid "ComplianceFramework|SOX - Sarbanes-Oxley"
msgstr ""
@@ -5580,9 +5765,15 @@ msgstr ""
msgid "Container repositories sync capacity"
msgstr ""
+msgid "ContainerRegistry| Please visit the %{linkStart}administration settings%{linkEnd} to enable this feature."
+msgstr ""
+
msgid "ContainerRegistry|%{imageName} tags"
msgstr ""
+msgid "ContainerRegistry|%{title} was successfully scheduled for deletion"
+msgstr ""
+
msgid "ContainerRegistry|Automatically remove extra images that aren't designed to be kept."
msgstr ""
@@ -5595,6 +5786,9 @@ msgstr ""
msgid "ContainerRegistry|Container Registry"
msgstr ""
+msgid "ContainerRegistry|Container Registry tag expiration and retention policy is disabled"
+msgstr ""
+
msgid "ContainerRegistry|Copy build command"
msgstr ""
@@ -5604,18 +5798,12 @@ msgstr ""
msgid "ContainerRegistry|Copy push command"
msgstr ""
-msgid "ContainerRegistry|Currently, the Container Registry tag expiration feature is not available for projects created before GitLab version 12.8. For updates and more information, visit Issue %{linkStart}#196124%{linkEnd}"
-msgstr ""
-
msgid "ContainerRegistry|Docker connection error"
msgstr ""
msgid "ContainerRegistry|Docker tag expiration policy is %{toggleStatus}"
msgstr ""
-msgid "ContainerRegistry|Docker tags with names matching this regex pattern will expire:"
-msgstr ""
-
msgid "ContainerRegistry|Edit Settings"
msgstr ""
@@ -5637,9 +5825,6 @@ msgstr ""
msgid "ContainerRegistry|Image ID"
msgstr ""
-msgid "ContainerRegistry|Image deleted successfully"
-msgstr ""
-
msgid "ContainerRegistry|Keep and protect the images that matter most."
msgstr ""
@@ -5655,12 +5840,24 @@ msgstr ""
msgid "ContainerRegistry|Number of tags to retain:"
msgstr ""
+msgid "ContainerRegistry|Please contact your administrator."
+msgstr ""
+
msgid "ContainerRegistry|Push an image"
msgstr ""
msgid "ContainerRegistry|Quick Start"
msgstr ""
+msgid "ContainerRegistry|Regular expressions such as %{codeStart}.*-test%{codeEnd} or %{codeStart}dev-.*%{codeEnd} are supported"
+msgstr ""
+
+msgid "ContainerRegistry|Regular expressions such as %{codeStart}.*-test%{codeEnd} or %{codeStart}dev-.*%{codeEnd} are supported. To select all tags, use %{codeStart}.*%{codeEnd}"
+msgstr ""
+
+msgid "ContainerRegistry|Remember to run %{docLinkStart}garbage collection%{docLinkEnd} to remove the stale data from storage."
+msgstr ""
+
msgid "ContainerRegistry|Remove repository"
msgstr ""
@@ -5677,22 +5874,22 @@ msgstr[3] ""
msgid "ContainerRegistry|Retention policy has been Enabled"
msgstr ""
-msgid "ContainerRegistry|Something went wrong while deleting the image."
+msgid "ContainerRegistry|Something went wrong while fetching the expiration policy."
msgstr ""
-msgid "ContainerRegistry|Something went wrong while deleting the tag."
+msgid "ContainerRegistry|Something went wrong while fetching the repository list."
msgstr ""
-msgid "ContainerRegistry|Something went wrong while deleting the tags."
+msgid "ContainerRegistry|Something went wrong while fetching the tags list."
msgstr ""
-msgid "ContainerRegistry|Something went wrong while fetching the expiration policy."
+msgid "ContainerRegistry|Something went wrong while marking the tag for deletion."
msgstr ""
-msgid "ContainerRegistry|Something went wrong while fetching the packages list."
+msgid "ContainerRegistry|Something went wrong while marking the tags for deletion."
msgstr ""
-msgid "ContainerRegistry|Something went wrong while fetching the tags list."
+msgid "ContainerRegistry|Something went wrong while scheduling %{title} for deletion. Please try again."
msgstr ""
msgid "ContainerRegistry|Something went wrong while updating the expiration policy."
@@ -5701,16 +5898,25 @@ msgstr ""
msgid "ContainerRegistry|Tag"
msgstr ""
-msgid "ContainerRegistry|Tag deleted successfully"
-msgstr ""
-
msgid "ContainerRegistry|Tag expiration policy"
msgstr ""
msgid "ContainerRegistry|Tag expiration policy is designed to:"
msgstr ""
-msgid "ContainerRegistry|Tags deleted successfully"
+msgid "ContainerRegistry|Tag successfully marked for deletion."
+msgstr ""
+
+msgid "ContainerRegistry|Tags successfully marked for deletion."
+msgstr ""
+
+msgid "ContainerRegistry|Tags with names matching this regex pattern will %{italicStart}be preserved:%{italicEnd}"
+msgstr ""
+
+msgid "ContainerRegistry|Tags with names matching this regex pattern will %{italicStart}expire:%{italicEnd}"
+msgstr ""
+
+msgid "ContainerRegistry|The Container Registry tag expiration and retention policies for this project have not been enabled."
msgstr ""
msgid "ContainerRegistry|The last tag related to this image was recently removed. This empty image and any associated data will be automatically removed as part of the regular Garbage Collection process. If you have any questions, contact your administrator."
@@ -5731,16 +5937,16 @@ msgstr ""
msgid "ContainerRegistry|There are no container images stored for this project"
msgstr ""
-msgid "ContainerRegistry|This Registry contains deleted image tag data. Remember to run %{docLinkStart}garbage collection%{docLinkEnd} to remove the stale data from storage."
+msgid "ContainerRegistry|There was an error during the deletion of this image repository, please try again."
msgstr ""
msgid "ContainerRegistry|This image has no active tags"
msgstr ""
-msgid "ContainerRegistry|We are having trouble connecting to Docker, which could be due to an issue with your project name or path. %{docLinkStart}More Information%{docLinkEnd}"
+msgid "ContainerRegistry|This image repository is scheduled for deletion"
msgstr ""
-msgid "ContainerRegistry|Wildcards such as %{codeStart}.*-stable%{codeEnd} or %{codeStart}production/.*%{codeEnd} are supported. To select all tags, use %{codeStart}.*%{codeEnd}"
+msgid "ContainerRegistry|We are having trouble connecting to Docker, which could be due to an issue with your project name or path. %{docLinkStart}More Information%{docLinkEnd}"
msgstr ""
msgid "ContainerRegistry|With the Container Registry, every project can have its own space to store its Docker images. %{docLinkStart}More Information%{docLinkEnd}"
@@ -5995,7 +6201,7 @@ msgstr ""
msgid "Could not delete chat nickname %{chat_name}."
msgstr ""
-msgid "Could not find design"
+msgid "Could not find design."
msgstr ""
msgid "Could not remove the trigger."
@@ -6010,6 +6216,9 @@ msgstr ""
msgid "Could not revoke personal access token %{personal_access_token_name}."
msgstr ""
+msgid "Could not revoke project access token %{project_access_token_name}."
+msgstr ""
+
msgid "Could not save group ID"
msgstr ""
@@ -6067,6 +6276,9 @@ msgstr ""
msgid "Create a new branch"
msgstr ""
+msgid "Create a new deploy key for this project"
+msgstr ""
+
msgid "Create a new file as there are no files yet. Afterwards, you'll be able to commit your changes."
msgstr ""
@@ -6172,6 +6384,9 @@ msgstr ""
msgid "Create requirement"
msgstr ""
+msgid "Create snippet"
+msgstr ""
+
msgid "Create wildcard: %{searchTerm}"
msgstr ""
@@ -6193,6 +6408,9 @@ msgstr ""
msgid "Created"
msgstr ""
+msgid "Created %{timestamp}"
+msgstr ""
+
msgid "Created At"
msgstr ""
@@ -6551,6 +6769,9 @@ msgstr ""
msgid "CycleAnalytics|stage dropdown"
msgstr ""
+msgid "DAG"
+msgstr ""
+
msgid "DNS"
msgstr ""
@@ -6929,6 +7150,9 @@ msgstr ""
msgid "Deploy key was successfully updated."
msgstr ""
+msgid "Deploy keys allow read-only or read-write (if enabled) access to your repository. Deploy keys can be used for CI, staging or production servers. You can create a deploy key or add an existing one."
+msgstr ""
+
msgid "Deploy progress not found. To see pods, ensure your environment matches %{linkStart}deploy board criteria%{linkEnd}."
msgstr ""
@@ -7082,6 +7306,9 @@ msgstr ""
msgid "Deploying to"
msgstr ""
+msgid "Deployment Frequency"
+msgstr ""
+
msgid "Deployment|API"
msgstr ""
@@ -7602,9 +7829,6 @@ msgstr ""
msgid "Edit environment"
msgstr ""
-msgid "Edit epic description"
-msgstr ""
-
msgid "Edit file"
msgstr ""
@@ -8226,9 +8450,6 @@ msgstr ""
msgid "Epics"
msgstr ""
-msgid "Epics (Ultimate / Gold license only)"
-msgstr ""
-
msgid "Epics Roadmap"
msgstr ""
@@ -8238,6 +8459,9 @@ msgstr ""
msgid "Epics let you manage your portfolio of projects more efficiently and with less effort"
msgstr ""
+msgid "Epics, Issues, and Merge Requests"
+msgstr ""
+
msgid "Epics|Add an epic"
msgstr ""
@@ -8289,6 +8513,9 @@ msgstr ""
msgid "Epics|Something went wrong while fetching group epics."
msgstr ""
+msgid "Epics|Something went wrong while moving item."
+msgstr ""
+
msgid "Epics|Something went wrong while ordering item."
msgstr ""
@@ -8343,6 +8570,9 @@ msgstr ""
msgid "Error fetching network graph."
msgstr ""
+msgid "Error fetching payload data."
+msgstr ""
+
msgid "Error fetching projects"
msgstr ""
@@ -8352,9 +8582,6 @@ msgstr ""
msgid "Error fetching the dependency list. Please check your network connection and try again."
msgstr ""
-msgid "Error fetching usage ping data."
-msgstr ""
-
msgid "Error loading branch data. Please try again."
msgstr ""
@@ -8496,6 +8723,9 @@ msgstr ""
msgid "Errors"
msgstr ""
+msgid "Errors:"
+msgstr ""
+
msgid "Estimated"
msgstr ""
@@ -8700,12 +8930,18 @@ msgstr ""
msgid "Export as CSV"
msgstr ""
+msgid "Export group"
+msgstr ""
+
msgid "Export issues"
msgstr ""
msgid "Export project"
msgstr ""
+msgid "Export this group with all related data to a new GitLab instance. Once complete, you can import the data file from the \"New Group\" page."
+msgstr ""
+
msgid "Export this project with all its related data in order to move your project to a new GitLab instance. Once the export is finished, you can import the file from the \"New Project\" page."
msgstr ""
@@ -8775,9 +9011,6 @@ msgstr ""
msgid "Failed Jobs"
msgstr ""
-msgid "Failed create wiki"
-msgstr ""
-
msgid "Failed to add a Zoom meeting"
msgstr ""
@@ -8817,6 +9050,9 @@ msgstr ""
msgid "Failed to create resources"
msgstr ""
+msgid "Failed to create wiki"
+msgstr ""
+
msgid "Failed to delete board. Please try again."
msgstr ""
@@ -8826,7 +9062,7 @@ msgstr ""
msgid "Failed to enqueue the rebase operation, possibly due to a long-lived transaction. Try again later."
msgstr ""
-msgid "Failed to find import label for jira import."
+msgid "Failed to find import label for Jira import."
msgstr ""
msgid "Failed to get ref."
@@ -8850,6 +9086,12 @@ msgstr ""
msgid "Failed to load groups & users."
msgstr ""
+msgid "Failed to load labels. Please try again."
+msgstr ""
+
+msgid "Failed to load milestones. Please try again."
+msgstr ""
+
msgid "Failed to load related branches"
msgstr ""
@@ -9225,7 +9467,10 @@ msgstr ""
msgid "Filter by milestone name"
msgstr ""
-msgid "Filter by name..."
+msgid "Filter by name"
+msgstr ""
+
+msgid "Filter by status"
msgstr ""
msgid "Filter by two-factor authentication"
@@ -9237,12 +9482,18 @@ msgstr ""
msgid "Filter projects"
msgstr ""
+msgid "Filter results"
+msgstr ""
+
msgid "Filter results by group"
msgstr ""
msgid "Filter results by project"
msgstr ""
+msgid "Filter results..."
+msgstr ""
+
msgid "Filter your projects by name"
msgstr ""
@@ -9531,10 +9782,10 @@ msgstr ""
msgid "GeoNodes|Checksummed"
msgstr ""
-msgid "GeoNodes|Container repositories"
+msgid "GeoNodes|Consult Geo troubleshooting information"
msgstr ""
-msgid "GeoNodes|Data is out of date from %{timeago}"
+msgid "GeoNodes|Container repositories"
msgstr ""
msgid "GeoNodes|Data replication lag"
@@ -9576,6 +9827,9 @@ msgstr ""
msgid "GeoNodes|Last event ID seen from primary"
msgstr ""
+msgid "GeoNodes|Learn more about Geo node statuses"
+msgstr ""
+
msgid "GeoNodes|Loading nodes"
msgstr ""
@@ -9591,6 +9845,9 @@ msgstr ""
msgid "GeoNodes|Node was successfully removed."
msgstr ""
+msgid "GeoNodes|Node's status was updated %{timeAgo}."
+msgstr ""
+
msgid "GeoNodes|Not checksummed"
msgstr ""
@@ -9621,7 +9878,10 @@ msgstr ""
msgid "GeoNodes|Repository verification progress"
msgstr ""
-msgid "GeoNodes|Selective"
+msgid "GeoNodes|Selective (%{syncLabel})"
+msgstr ""
+
+msgid "GeoNodes|Selective synchronization"
msgstr ""
msgid "GeoNodes|Something went wrong while changing node status"
@@ -9648,6 +9908,9 @@ msgstr ""
msgid "GeoNodes|Unverified"
msgstr ""
+msgid "GeoNodes|Updated %{timeAgo}"
+msgstr ""
+
msgid "GeoNodes|Used slots"
msgstr ""
@@ -9687,16 +9950,16 @@ msgstr ""
msgid "Geo|All"
msgstr ""
-msgid "Geo|All projects"
+msgid "Geo|All %{replicable_type}"
msgstr ""
-msgid "Geo|All projects are being scheduled for re-sync"
+msgid "Geo|All projects"
msgstr ""
-msgid "Geo|All projects are being scheduled for re-verify"
+msgid "Geo|All projects are being scheduled for resync"
msgstr ""
-msgid "Geo|Batch operations"
+msgid "Geo|All projects are being scheduled for reverify"
msgstr ""
msgid "Geo|Could not remove tracking entry for an existing project."
@@ -9708,9 +9971,15 @@ msgstr ""
msgid "Geo|Failed"
msgstr ""
+msgid "Geo|Filter by status"
+msgstr ""
+
msgid "Geo|Geo Status"
msgstr ""
+msgid "Geo|In progress"
+msgstr ""
+
msgid "Geo|In sync"
msgstr ""
@@ -9735,9 +10004,6 @@ msgstr ""
msgid "Geo|Not synced yet"
msgstr ""
-msgid "Geo|Pending"
-msgstr ""
-
msgid "Geo|Pending synchronization"
msgstr ""
@@ -9774,7 +10040,7 @@ msgstr ""
msgid "Geo|Resync"
msgstr ""
-msgid "Geo|Resync all projects"
+msgid "Geo|Resync all"
msgstr ""
msgid "Geo|Retry count"
@@ -9783,7 +10049,7 @@ msgstr ""
msgid "Geo|Reverify"
msgstr ""
-msgid "Geo|Reverify all projects"
+msgid "Geo|Reverify all"
msgstr ""
msgid "Geo|Status"
@@ -10125,9 +10391,6 @@ msgstr ""
msgid "Go to file"
msgstr ""
-msgid "Go to file (MRs only)"
-msgstr ""
-
msgid "Go to file permalink (while viewing a file)"
msgstr ""
@@ -10572,6 +10835,9 @@ msgstr ""
msgid "GroupSettings|Disable group mentions"
msgstr ""
+msgid "GroupSettings|Export group"
+msgstr ""
+
msgid "GroupSettings|If the parent group's visibility is lower than the group current visibility, visibility levels for subgroups and projects will be changed to match the new parent group's visibility."
msgstr ""
@@ -10650,6 +10916,9 @@ msgstr ""
msgid "Groups (%{count})"
msgstr ""
+msgid "Groups (%{groups})"
+msgstr ""
+
msgid "Groups can also be nested by creating %{subgroup_docs_link_start}subgroups%{subgroup_docs_link_end}."
msgstr ""
@@ -10919,15 +11188,12 @@ msgstr ""
msgid "ID:"
msgstr ""
-msgid "IDE|Allow live previews of JavaScript projects in the Web IDE using CodeSandbox client side evaluation."
+msgid "IDE|Allow live previews of JavaScript projects in the Web IDE using CodeSandbox Live Preview."
msgstr ""
msgid "IDE|Back"
msgstr ""
-msgid "IDE|Client side evaluation"
-msgstr ""
-
msgid "IDE|Commit"
msgstr ""
@@ -10991,6 +11257,9 @@ msgstr ""
msgid "If any job surpasses this timeout threshold, it will be marked as failed. Human readable time input language is accepted like \"1 hour\". Values without specification represent seconds."
msgstr ""
+msgid "If blank, set allowable lifetime to %{instance_level_policy_in_words}, as defined by the instance admin. Once set, existing tokens for users in this group may be revoked."
+msgstr ""
+
msgid "If checked, group owners can manage LDAP group links and LDAP member overrides"
msgstr ""
@@ -11030,6 +11299,9 @@ msgstr ""
msgid "If you lose your recovery codes you can generate new ones, invalidating all previous codes."
msgstr ""
+msgid "If you reach 100%% storage capacity, you will not be able to: %{base_message}"
+msgstr ""
+
msgid "If your HTTP repository is not publicly accessible, add your credentials."
msgstr ""
@@ -11141,6 +11413,9 @@ msgstr ""
msgid "Import repository"
msgstr ""
+msgid "Import started by: %{importInitiator}"
+msgstr ""
+
msgid "Import tasks"
msgstr ""
@@ -11207,6 +11482,9 @@ msgstr ""
msgid "In order to tailor your experience with GitLab we<br>would like to know a bit more about you."
msgstr ""
+msgid "In progress"
+msgstr ""
+
msgid "In the next step, you'll be able to select the projects you want to import."
msgstr ""
@@ -11556,15 +11834,15 @@ msgstr ""
msgid "Issues"
msgstr ""
-msgid "Issues / Merge Requests"
-msgstr ""
-
msgid "Issues Analytics"
msgstr ""
msgid "Issues Rate Limits"
msgstr ""
+msgid "Issues and Merge Requests"
+msgstr ""
+
msgid "Issues can be bugs, tasks or ideas to be discussed. Also, issues are searchable and filterable."
msgstr ""
@@ -11586,10 +11864,10 @@ msgstr ""
msgid "IssuesAnalytics|Avg/Month:"
msgstr ""
-msgid "IssuesAnalytics|Issues created"
+msgid "IssuesAnalytics|Issues opened"
msgstr ""
-msgid "IssuesAnalytics|Issues created per month"
+msgid "IssuesAnalytics|Issues opened per month"
msgstr ""
msgid "IssuesAnalytics|Last 12 months"
@@ -11643,6 +11921,9 @@ msgstr ""
msgid "Jira integration not configured."
msgstr ""
+msgid "Jira project: %{importProject}"
+msgstr ""
+
msgid "JiraService|Events for %{noteable_model_name} are disabled."
msgstr ""
@@ -12034,6 +12315,9 @@ msgstr ""
msgid "Lead"
msgstr ""
+msgid "Lead Time"
+msgstr ""
+
msgid "Learn GitLab"
msgstr ""
@@ -12046,6 +12330,9 @@ msgstr ""
msgid "Learn how to %{no_packages_link_start}publish and share your packages%{no_packages_link_end} with GitLab."
msgstr ""
+msgid "Learn how to enable synchronization"
+msgstr ""
+
msgid "Learn more"
msgstr ""
@@ -12387,9 +12674,6 @@ msgstr ""
msgid "Live preview"
msgstr ""
-msgid "Load more vulnerabilities"
-msgstr ""
-
msgid "Loading"
msgstr ""
@@ -12540,9 +12824,6 @@ msgstr ""
msgid "Manage"
msgstr ""
-msgid "Manage Git repositories with fine-grained access controls that keep your code secure. Perform code reviews and enhance collaboration with merge requests. Each project can also have an issue tracker and a wiki."
-msgstr ""
-
msgid "Manage Web IDE features"
msgstr ""
@@ -12573,6 +12854,9 @@ msgstr ""
msgid "Manage your license"
msgstr ""
+msgid "Managed Account"
+msgstr ""
+
msgid "Manifest"
msgstr ""
@@ -12837,6 +13121,9 @@ msgstr ""
msgid "Merge Requests in Review"
msgstr ""
+msgid "Merge automatically (%{strategy})"
+msgstr ""
+
msgid "Merge commit message"
msgstr ""
@@ -12990,6 +13277,12 @@ msgstr ""
msgid "Merged branches are being deleted. This can take some time depending on the number of branches. Please refresh the page to see changes."
msgstr ""
+msgid "Merged this merge request."
+msgstr ""
+
+msgid "Merges this merge request immediately."
+msgstr ""
+
msgid "Merges this merge request when the pipeline succeeds."
msgstr ""
@@ -13099,12 +13392,15 @@ msgstr[1] ""
msgstr[2] ""
msgstr[3] ""
-msgid "Metrics|Environment"
+msgid "Metrics|Expand panel"
msgstr ""
msgid "Metrics|For grouping similar metrics"
msgstr ""
+msgid "Metrics|Go back (Esc)"
+msgstr ""
+
msgid "Metrics|Invalid time range, please verify."
msgstr ""
@@ -13135,9 +13431,6 @@ msgstr ""
msgid "Metrics|Refresh dashboard"
msgstr ""
-msgid "Metrics|Show last"
-msgstr ""
-
msgid "Metrics|There was an error creating the dashboard."
msgstr ""
@@ -13216,6 +13509,9 @@ msgstr ""
msgid "Microsoft Azure"
msgstr ""
+msgid "Middleman project with Static Site Editor support"
+msgstr ""
+
msgid "Migrated %{success_count}/%{total_count} files."
msgstr ""
@@ -13665,10 +13961,10 @@ msgstr ""
msgid "Next"
msgstr ""
-msgid "Next file in diff (MRs only)"
+msgid "Next file in diff"
msgstr ""
-msgid "Next unresolved discussion (MRs only)"
+msgid "Next unresolved discussion"
msgstr ""
msgid "Nickname"
@@ -13728,6 +14024,9 @@ msgstr ""
msgid "No changes between %{ref_start}%{source_branch}%{ref_end} and %{ref_start}%{target_branch}%{ref_end}"
msgstr ""
+msgid "No child epics match applied filters"
+msgstr ""
+
msgid "No connection could be made to a Gitaly Server, please check your logs!"
msgstr ""
@@ -13866,15 +14165,6 @@ msgstr ""
msgid "No thanks, don't show this again"
msgstr ""
-msgid "No vulnerabilities found for this group"
-msgstr ""
-
-msgid "No vulnerabilities found for this pipeline"
-msgstr ""
-
-msgid "No vulnerabilities found for this project"
-msgstr ""
-
msgid "No vulnerabilities present"
msgstr ""
@@ -13998,6 +14288,9 @@ msgstr ""
msgid "Nothing to preview."
msgstr ""
+msgid "Nothing to synchronize"
+msgstr ""
+
msgid "Notification events"
msgstr ""
@@ -14169,6 +14462,9 @@ msgstr ""
msgid "Once removed, the fork relationship cannot be restored and you will no longer be able to send merge requests to the source."
msgstr ""
+msgid "Once the exported file is ready you can download it from this page."
+msgstr ""
+
msgid "Once the exported file is ready, you will receive a notification email with a download link, or you can download it from this page."
msgstr ""
@@ -14272,9 +14568,6 @@ msgstr ""
msgid "Open sidebar"
msgstr ""
-msgid "Open source software to collaborate on code"
-msgstr ""
-
msgid "Open: %{openIssuesCount}"
msgstr ""
@@ -14377,6 +14670,9 @@ msgstr ""
msgid "OutdatedBrowser|You can provide feedback %{feedback_link_start}on this issue%{feedback_link_end} or via your usual support channels."
msgstr ""
+msgid "Overridden"
+msgstr ""
+
msgid "Overview"
msgstr ""
@@ -14428,6 +14724,9 @@ msgstr ""
msgid "PackageRegistry|Conan Command"
msgstr ""
+msgid "PackageRegistry|Copy .pypirc content"
+msgstr ""
+
msgid "PackageRegistry|Copy Conan Command"
msgstr ""
@@ -14449,6 +14748,9 @@ msgstr ""
msgid "PackageRegistry|Copy NuGet Setup Command"
msgstr ""
+msgid "PackageRegistry|Copy Pip command"
+msgstr ""
+
msgid "PackageRegistry|Copy and paste this inside your %{codeStart}pom.xml%{codeEnd} %{codeStart}dependencies%{codeEnd} block."
msgstr ""
@@ -14482,6 +14784,12 @@ msgstr ""
msgid "PackageRegistry|For more information on the NuGet registry, %{linkStart}see the documentation%{linkEnd}."
msgstr ""
+msgid "PackageRegistry|For more information on the PyPi registry, %{linkStart}see the documentation%{linkEnd}."
+msgstr ""
+
+msgid "PackageRegistry|If you haven't already done so, you will need to add the below to your %{codeStart}.pypirc%{codeEnd} file."
+msgstr ""
+
msgid "PackageRegistry|If you haven't already done so, you will need to add the below to your %{codeStart}pom.xml%{codeEnd} file."
msgstr ""
@@ -14491,6 +14799,9 @@ msgstr ""
msgid "PackageRegistry|Learn how to %{noPackagesLinkStart}publish and share your packages%{noPackagesLinkEnd} with GitLab."
msgstr ""
+msgid "PackageRegistry|Manually Published"
+msgstr ""
+
msgid "PackageRegistry|Maven"
msgstr ""
@@ -14509,12 +14820,18 @@ msgstr ""
msgid "PackageRegistry|NuGet Command"
msgstr ""
+msgid "PackageRegistry|Pip Command"
+msgstr ""
+
msgid "PackageRegistry|Pipeline %{linkStart}%{linkEnd} triggered %{timestamp} by %{author}"
msgstr ""
msgid "PackageRegistry|Published to the repository at %{timestamp}"
msgstr ""
+msgid "PackageRegistry|PyPi"
+msgstr ""
+
msgid "PackageRegistry|Registry Setup"
msgstr ""
@@ -14551,6 +14868,9 @@ msgstr ""
msgid "PackageRegistry|npm"
msgstr ""
+msgid "PackageRegistry|published by %{author}"
+msgstr ""
+
msgid "PackageRegistry|yarn"
msgstr ""
@@ -14566,6 +14886,9 @@ msgstr ""
msgid "PackageType|NuGet"
msgstr ""
+msgid "PackageType|PyPi"
+msgstr ""
+
msgid "Packages"
msgstr ""
@@ -14659,6 +14982,9 @@ msgstr ""
msgid "Past due"
msgstr ""
+msgid "Paste a machine public key here. Read more about how to generate it"
+msgstr ""
+
msgid "Paste a machine public key here. Read more about how to generate it %{link_start}here%{link_end}"
msgstr ""
@@ -14674,9 +15000,6 @@ msgstr ""
msgid "Path"
msgstr ""
-msgid "Path, transfer, remove"
-msgstr ""
-
msgid "Path:"
msgstr ""
@@ -14707,7 +15030,7 @@ msgstr ""
msgid "Percentage"
msgstr ""
-msgid "Perform advanced options such as changing path, transferring, or removing the group."
+msgid "Perform advanced options such as changing path, transferring, exporting, or removing the group."
msgstr ""
msgid "Perform common operations on GitLab project"
@@ -14788,9 +15111,6 @@ msgstr ""
msgid "Pipeline minutes quota"
msgstr ""
-msgid "Pipeline quota"
-msgstr ""
-
msgid "Pipeline subscriptions"
msgstr ""
@@ -14800,6 +15120,9 @@ msgstr ""
msgid "Pipeline: %{status}"
msgstr ""
+msgid "PipelineCharts|CI / CD Analytics"
+msgstr ""
+
msgid "PipelineCharts|Failed:"
msgstr ""
@@ -14878,15 +15201,6 @@ msgstr ""
msgid "Pipelines settings for '%{project_name}' were successfully updated."
msgstr ""
-msgid "Pipelines| to purchase more minutes."
-msgstr ""
-
-msgid "Pipelines|%{namespace_name} has exceeded its pipeline minutes quota."
-msgstr ""
-
-msgid "Pipelines|%{namespace_name} has less than %{notification_level}%% of CI minutes available."
-msgstr ""
-
msgid "Pipelines|API"
msgstr ""
@@ -14908,10 +15222,13 @@ msgstr ""
msgid "Pipelines|Get started with Pipelines"
msgstr ""
-msgid "Pipelines|Loading Pipelines"
+msgid "Pipelines|Group %{namespace_name} has %{percentage}%% or less Shared Runner Pipeline minutes remaining. Once it runs out, no new jobs or pipelines in its projects will run."
msgstr ""
-msgid "Pipelines|Pipelines will not run anymore on shared Runners."
+msgid "Pipelines|Group %{namespace_name} has exceeded its pipeline minutes quota. Unless you buy additional pipeline minutes, no new jobs or pipelines in its projects will run."
+msgstr ""
+
+msgid "Pipelines|Loading Pipelines"
msgstr ""
msgid "Pipelines|Project cache successfully reset."
@@ -15142,6 +15459,9 @@ msgstr ""
msgid "Please select"
msgstr ""
+msgid "Please select a Jira project"
+msgstr ""
+
msgid "Please select a country"
msgstr ""
@@ -15187,6 +15507,9 @@ msgstr ""
msgid "Please wait while we import the repository for you. Refresh at will."
msgstr ""
+msgid "Plugins directory is deprecated and will be removed in 14.0. Please move this file into /file_hooks directory."
+msgstr ""
+
msgid "Pod does not exist"
msgstr ""
@@ -15304,7 +15627,7 @@ msgstr ""
msgid "Prevent users from changing their profile name"
msgstr ""
-msgid "Prevent users from modifing merge request approvers list"
+msgid "Prevent users from modifying merge request approvers list"
msgstr ""
msgid "Prevent users from performing write operations on GitLab while performing maintenance."
@@ -15325,10 +15648,10 @@ msgstr ""
msgid "Previous Artifacts"
msgstr ""
-msgid "Previous file in diff (MRs only)"
+msgid "Previous file in diff"
msgstr ""
-msgid "Previous unresolved discussion (MRs only)"
+msgid "Previous unresolved discussion"
msgstr ""
msgid "Primary"
@@ -15361,6 +15684,9 @@ msgstr ""
msgid "Private profile"
msgstr ""
+msgid "Private projects Minutes cost factor"
+msgstr ""
+
msgid "Private projects can be created in your personal namespace with:"
msgstr ""
@@ -16189,6 +16515,9 @@ msgstr ""
msgid "ProjectSettings|With GitLab Pages you can host your static websites on GitLab"
msgstr ""
+msgid "ProjectSettings|With Metrics Dashboard you can visualize this project performance metrics"
+msgstr ""
+
msgid "ProjectTemplates|.NET Core"
msgstr ""
@@ -16252,6 +16581,9 @@ msgstr ""
msgid "ProjectTemplates|Spring"
msgstr ""
+msgid "ProjectTemplates|Static Site Editor/Middleman"
+msgstr ""
+
msgid "ProjectTemplates|iOS (Swift)"
msgstr ""
@@ -16471,13 +16803,13 @@ msgstr ""
msgid "PrometheusService|Prometheus is being automatically managed on your clusters"
msgstr ""
-msgid "PrometheusService|These metrics will only be monitored after your first deployment to an environment"
+msgid "PrometheusService|Select the Active checkbox to override the Auto Configuration with custom settings. If unchecked, Auto Configuration settings are used."
msgstr ""
-msgid "PrometheusService|Time-series monitoring service"
+msgid "PrometheusService|These metrics will only be monitored after your first deployment to an environment"
msgstr ""
-msgid "PrometheusService|To enable manual configuration, uninstall Prometheus from your clusters"
+msgid "PrometheusService|Time-series monitoring service"
msgstr ""
msgid "PrometheusService|To enable the installation of Prometheus on your clusters, deactivate the manual configuration below"
@@ -16663,6 +16995,9 @@ msgstr ""
msgid "Protip:"
msgstr ""
+msgid "Protocol"
+msgstr ""
+
msgid "Provider"
msgstr ""
@@ -16687,6 +17022,9 @@ msgstr ""
msgid "Public pipelines"
msgstr ""
+msgid "Public projects Minutes cost factor"
+msgstr ""
+
msgid "Pull"
msgstr ""
@@ -16889,6 +17227,9 @@ msgstr[1] ""
msgstr[2] ""
msgstr[3] ""
+msgid "Regenerate export"
+msgstr ""
+
msgid "Regenerate instance ID"
msgstr ""
@@ -17268,9 +17609,31 @@ msgstr ""
msgid "Reports|%{combinedString} and %{resolvedString}"
msgstr ""
+msgid "Reports|Accessibility scanning detected %d issue for the source branch only"
+msgid_plural "Reports|Accessibility scanning detected %d issues for the source branch only"
+msgstr[0] ""
+msgstr[1] ""
+msgstr[2] ""
+msgstr[3] ""
+
+msgid "Reports|Accessibility scanning detected no issues for the source branch only"
+msgstr ""
+
+msgid "Reports|Accessibility scanning failed loading results"
+msgstr ""
+
+msgid "Reports|Accessibility scanning results are being parsed"
+msgstr ""
+
msgid "Reports|Actions"
msgstr ""
+msgid "Reports|An error occured while loading report"
+msgstr ""
+
+msgid "Reports|An error occurred while loading %{name} results"
+msgstr ""
+
msgid "Reports|Class"
msgstr ""
@@ -17376,7 +17739,7 @@ msgstr ""
msgid "Requested %{time_ago}"
msgstr ""
-msgid "Requested design version does not exist"
+msgid "Requested design version does not exist."
msgstr ""
msgid "Requested states are invalid"
@@ -17564,6 +17927,9 @@ msgstr ""
msgid "Resync"
msgstr ""
+msgid "Resync all"
+msgstr ""
+
msgid "Resync all %{replicableType}"
msgstr ""
@@ -17634,6 +18000,9 @@ msgstr ""
msgid "Revoked personal access token %{personal_access_token_name}!"
msgstr ""
+msgid "Revoked project access token %{project_access_token_name}!"
+msgstr ""
+
msgid "RightSidebar|adding a"
msgstr ""
@@ -17682,6 +18051,9 @@ msgstr ""
msgid "Runner token"
msgstr ""
+msgid "Runner tokens"
+msgstr ""
+
msgid "Runner was not updated."
msgstr ""
@@ -17736,6 +18108,9 @@ msgstr ""
msgid "SAML SSO for %{group_name}"
msgstr ""
+msgid "SAML discovery tokens"
+msgstr ""
+
msgid "SAML for %{group_name}"
msgstr ""
@@ -17820,12 +18195,18 @@ msgstr ""
msgid "Scheduled"
msgstr ""
+msgid "Scheduled to merge this merge request (%{strategy})."
+msgstr ""
+
msgid "Scheduled to merge this merge request when the pipeline succeeds."
msgstr ""
msgid "Schedules"
msgstr ""
+msgid "Schedules to merge this merge request (%{strategy})."
+msgstr ""
+
msgid "Scheduling"
msgstr ""
@@ -17883,6 +18264,9 @@ msgstr ""
msgid "Search branches and tags"
msgstr ""
+msgid "Search by author"
+msgstr ""
+
msgid "Search files"
msgstr ""
@@ -18044,13 +18428,6 @@ msgstr[1] ""
msgstr[2] ""
msgstr[3] ""
-msgid "SearchResults|snippet result"
-msgid_plural "SearchResults|snippet results"
-msgstr[0] ""
-msgstr[1] ""
-msgstr[2] ""
-msgstr[3] ""
-
msgid "SearchResults|user"
msgid_plural "SearchResults|users"
msgstr[0] ""
@@ -18065,6 +18442,9 @@ msgstr[1] ""
msgstr[2] ""
msgstr[3] ""
+msgid "Searching by both author and message is currently not supported."
+msgstr ""
+
msgid "Seat Link"
msgstr ""
@@ -18092,184 +18472,232 @@ msgstr ""
msgid "Security Dashboard"
msgstr ""
-msgid "Security Dashboard|Error fetching the vulnerability counts. Please check your network connection and try again."
+msgid "Security configuration help link"
msgstr ""
-msgid "Security Dashboard|Error fetching the vulnerability list. Please check your network connection and try again."
+msgid "Security dashboard"
msgstr ""
-msgid "Security Dashboard|Issue Created"
+msgid "Security report is out of date. Please update your branch with the latest changes from the target branch (%{targetBranchName})"
msgstr ""
-msgid "Security Reports|Comment added to '%{vulnerabilityName}'"
+msgid "Security report is out of date. Run %{newPipelineLinkStart}a new pipeline%{newPipelineLinkEnd} for the target branch (%{targetBranchName})"
msgstr ""
-msgid "Security Reports|Comment deleted on '%{vulnerabilityName}'"
+msgid "SecurityConfiguration|Configured"
msgstr ""
-msgid "Security Reports|Comment edited on '%{vulnerabilityName}'"
+msgid "SecurityConfiguration|Feature"
msgstr ""
-msgid "Security Reports|Create issue"
+msgid "SecurityConfiguration|Feature documentation for %{featureName}"
msgstr ""
-msgid "Security Reports|Dismiss vulnerability"
+msgid "SecurityConfiguration|Not yet configured"
msgstr ""
-msgid "Security Reports|Dismissed '%{vulnerabilityName}'"
+msgid "SecurityConfiguration|Secure features"
msgstr ""
-msgid "Security Reports|Dismissed '%{vulnerabilityName}'. Turn off the hide dismissed toggle to view."
+msgid "SecurityConfiguration|Status"
msgstr ""
-msgid "Security Reports|Either you don't have permission to view this dashboard or the dashboard has not been setup. Please check your permission settings with your administrator or check your dashboard configurations to proceed."
+msgid "SecurityReports|%{firstProject} and %{secondProject}"
msgstr ""
-msgid "Security Reports|Learn more about setting up your dashboard"
+msgid "SecurityReports|%{firstProject}, %{secondProject}, and %{rest}"
msgstr ""
-msgid "Security Reports|More info"
+msgid "SecurityReports|Add a project to your dashboard"
msgstr ""
-msgid "Security Reports|Oops, something doesn't seem right."
+msgid "SecurityReports|Add or remove projects from your dashboard"
msgstr ""
-msgid "Security Reports|Security reports can only be accessed by authorized users."
+msgid "SecurityReports|Add projects"
msgstr ""
-msgid "Security Reports|There was an error adding the comment."
+msgid "SecurityReports|Comment added to '%{vulnerabilityName}'"
msgstr ""
-msgid "Security Reports|There was an error creating the issue."
+msgid "SecurityReports|Comment deleted on '%{vulnerabilityName}'"
msgstr ""
-msgid "Security Reports|There was an error creating the merge request."
+msgid "SecurityReports|Comment edited on '%{vulnerabilityName}'"
msgstr ""
-msgid "Security Reports|There was an error deleting the comment."
+msgid "SecurityReports|Create issue"
msgstr ""
-msgid "Security Reports|There was an error dismissing the vulnerabilities."
+msgid "SecurityReports|Dismiss Selected"
msgstr ""
-msgid "Security Reports|There was an error dismissing the vulnerability."
+msgid "SecurityReports|Dismiss vulnerability"
msgstr ""
-msgid "Security Reports|There was an error reverting the dismissal."
+msgid "SecurityReports|Dismissed '%{vulnerabilityName}'"
msgstr ""
-msgid "Security Reports|There was an error reverting this dismissal."
+msgid "SecurityReports|Dismissed '%{vulnerabilityName}'. Turn off the hide dismissed toggle to view."
msgstr ""
-msgid "Security Reports|Undo dismiss"
+msgid "SecurityReports|Each vulnerability now has a unique page that can be directly linked to, shared, referenced, and tracked as the single source of truth. Vulnerability occurrences also persist across scanner runs, which improves tracking and visibility and reduces duplicates between scans."
msgstr ""
-msgid "Security Reports|You do not have sufficient permissions to access this report"
+msgid "SecurityReports|Edit dashboard"
msgstr ""
-msgid "Security Reports|You must sign in as an authorized user to see this report"
+msgid "SecurityReports|Either you don't have permission to view this dashboard or the dashboard has not been setup. Please check your permission settings with your administrator or check your dashboard configurations to proceed."
msgstr ""
-msgid "Security configuration help link"
+msgid "SecurityReports|Error fetching the vulnerability counts. Please check your network connection and try again."
msgstr ""
-msgid "Security dashboard"
+msgid "SecurityReports|Error fetching the vulnerability list. Please check your network connection and try again."
msgstr ""
-msgid "Security report is out of date. Please update your branch with the latest changes from the target branch (%{targetBranchName})"
+msgid "SecurityReports|False positive"
msgstr ""
-msgid "Security report is out of date. Run %{newPipelineLinkStart}a new pipeline%{newPipelineLinkEnd} for the target branch (%{targetBranchName})"
+msgid "SecurityReports|Hide dismissed"
msgstr ""
-msgid "SecurityConfiguration|Configured"
+msgid "SecurityReports|Introducing standalone vulnerabilities"
msgstr ""
-msgid "SecurityConfiguration|Feature"
+msgid "SecurityReports|Issue Created"
msgstr ""
-msgid "SecurityConfiguration|Feature documentation for %{featureName}"
+msgid "SecurityReports|Learn More"
msgstr ""
-msgid "SecurityConfiguration|Not yet configured"
+msgid "SecurityReports|Learn more about setting up your dashboard"
msgstr ""
-msgid "SecurityConfiguration|Secure features"
+msgid "SecurityReports|Load more vulnerabilities"
msgstr ""
-msgid "SecurityConfiguration|Status"
+msgid "SecurityReports|Monitor vulnerabilities in your code"
+msgstr ""
+
+msgid "SecurityReports|More info"
+msgstr ""
+
+msgid "SecurityReports|More information"
+msgstr ""
+
+msgid "SecurityReports|No vulnerabilities found for dashboard"
+msgstr ""
+
+msgid "SecurityReports|No vulnerabilities found for this group"
+msgstr ""
+
+msgid "SecurityReports|No vulnerabilities found for this pipeline"
+msgstr ""
+
+msgid "SecurityReports|No vulnerabilities found for this project"
+msgstr ""
+
+msgid "SecurityReports|Oops, something doesn't seem right."
+msgstr ""
+
+msgid "SecurityReports|Pipeline %{pipelineLink} triggered %{timeago} by %{user}"
msgstr ""
-msgid "SecurityDashboard|%{firstProject} and %{secondProject}"
+msgid "SecurityReports|Project"
msgstr ""
-msgid "SecurityDashboard|%{firstProject}, %{secondProject}, and %{rest}"
+msgid "SecurityReports|Projects added"
msgstr ""
-msgid "SecurityDashboard|Add a project to your dashboard"
+msgid "SecurityReports|Remove project from dashboard"
msgstr ""
-msgid "SecurityDashboard|Add or remove projects from your dashboard"
+msgid "SecurityReports|Report type"
msgstr ""
-msgid "SecurityDashboard|Add projects"
+msgid "SecurityReports|Return to dashboard"
msgstr ""
-msgid "SecurityDashboard|Edit dashboard"
+msgid "SecurityReports|Security Dashboard"
msgstr ""
-msgid "SecurityDashboard|Hide dismissed"
+msgid "SecurityReports|Security reports can only be accessed by authorized users."
msgstr ""
-msgid "SecurityDashboard|Monitor vulnerabilities in your code"
+msgid "SecurityReports|Select a project to add by using the project search field above."
msgstr ""
-msgid "SecurityDashboard|More information"
+msgid "SecurityReports|Select a reason"
msgstr ""
-msgid "SecurityDashboard|No vulnerabilities found for dashboard"
+msgid "SecurityReports|Severity"
msgstr ""
-msgid "SecurityDashboard|Pipeline %{pipelineLink} triggered %{timeago} by %{user}"
+msgid "SecurityReports|Status"
msgstr ""
-msgid "SecurityDashboard|Project"
+msgid "SecurityReports|The security dashboard displays the latest security findings for projects you wish to monitor. Select \"Edit dashboard\" to add and remove projects."
msgstr ""
-msgid "SecurityDashboard|Projects added"
+msgid "SecurityReports|The security dashboard displays the latest security report. Use it to find and fix vulnerabilities."
msgstr ""
-msgid "SecurityDashboard|Remove project from dashboard"
+msgid "SecurityReports|There was an error adding the comment."
msgstr ""
-msgid "SecurityDashboard|Report type"
+msgid "SecurityReports|There was an error creating the issue."
msgstr ""
-msgid "SecurityDashboard|Return to dashboard"
+msgid "SecurityReports|There was an error creating the merge request."
msgstr ""
-msgid "SecurityDashboard|Security Dashboard"
+msgid "SecurityReports|There was an error deleting the comment."
msgstr ""
-msgid "SecurityDashboard|Select a project to add by using the project search field above."
+msgid "SecurityReports|There was an error dismissing the vulnerabilities."
msgstr ""
-msgid "SecurityDashboard|Severity"
+msgid "SecurityReports|There was an error dismissing the vulnerability."
msgstr ""
-msgid "SecurityDashboard|Status"
+msgid "SecurityReports|There was an error reverting the dismissal."
msgstr ""
-msgid "SecurityDashboard|The security dashboard displays the latest security findings for projects you wish to monitor. Select \"Edit dashboard\" to add and remove projects."
+msgid "SecurityReports|There was an error reverting this dismissal."
msgstr ""
-msgid "SecurityDashboard|The security dashboard displays the latest security report. Use it to find and fix vulnerabilities."
+msgid "SecurityReports|There was an error while generating the report."
msgstr ""
-msgid "SecurityDashboard|There was an error while generating the report."
+msgid "SecurityReports|Unable to add %{invalidProjects}"
msgstr ""
-msgid "SecurityDashboard|Unable to add %{invalidProjects}"
+msgid "SecurityReports|Undo dismiss"
+msgstr ""
+
+msgid "SecurityReports|While it's rare to have no vulnerabilities for your group, it can happen. In any event, we ask that you double check your settings to make sure you've set up your dashboard correctly."
+msgstr ""
+
+msgid "SecurityReports|While it's rare to have no vulnerabilities for your pipeline, it can happen. In any event, we ask that you double check your settings to make sure all security scanning jobs have passed successfully."
+msgstr ""
+
+msgid "SecurityReports|While it's rare to have no vulnerabilities for your project, it can happen. In any event, we ask that you double check your settings to make sure you've set up your dashboard correctly."
+msgstr ""
+
+msgid "SecurityReports|While it's rare to have no vulnerabilities, it can happen. In any event, we ask that you please double check your settings to make sure you've set up your dashboard correctly."
+msgstr ""
+
+msgid "SecurityReports|Won't fix / Accept risk"
+msgstr ""
+
+msgid "SecurityReports|You do not have sufficient permissions to access this report"
+msgstr ""
+
+msgid "SecurityReports|You must sign in as an authorized user to see this report"
+msgstr ""
+
+msgid "SecurityReports|[No reason]"
msgstr ""
msgid "See GitLab's %{password_policy_guidelines}"
@@ -18281,6 +18709,9 @@ msgstr ""
msgid "See the affected projects in the GitLab admin panel"
msgstr ""
+msgid "See what's new at GitLab"
+msgstr ""
+
msgid "Select"
msgstr ""
@@ -18686,9 +19117,6 @@ msgstr ""
msgid "Set up Jira Integration"
msgstr ""
-msgid "Set up Jira Integration illustration"
-msgstr ""
-
msgid "Set up a %{type} Runner automatically"
msgstr ""
@@ -18773,6 +19201,9 @@ msgstr ""
msgid "Severity: %{severity}"
msgstr ""
+msgid "Shards (%{shards})"
+msgstr ""
+
msgid "Shards to synchronize"
msgstr ""
@@ -19089,6 +19520,9 @@ msgstr ""
msgid "Snowplow"
msgstr ""
+msgid "Some child epics may be hidden due to applied filters"
+msgstr ""
+
msgid "Some email servers do not support overriding the email sender name. Enable this option to include the name of the author of the issue, merge request or comment in the email body instead."
msgstr ""
@@ -19230,6 +19664,9 @@ msgstr ""
msgid "Something went wrong while updating your list settings"
msgstr ""
+msgid "Something went wrong with your automatic subscription renewal"
+msgstr ""
+
msgid "Something went wrong, unable to add %{project} to dashboard"
msgstr ""
@@ -19515,6 +19952,9 @@ msgstr ""
msgid "Stage removed"
msgstr ""
+msgid "Standard"
+msgstr ""
+
msgid "Star a label to make it a priority label. Order the prioritized labels to change their relative priority, by dragging."
msgstr ""
@@ -19638,6 +20078,9 @@ msgstr ""
msgid "Static Application Security Testing (SAST)"
msgstr ""
+msgid "StaticSiteEditor|An error occurred while submitting your changes."
+msgstr ""
+
msgid "StaticSiteEditor|Branch could not be created."
msgstr ""
@@ -19647,18 +20090,30 @@ msgstr ""
msgid "StaticSiteEditor|Could not create merge request."
msgstr ""
+msgid "StaticSiteEditor|Incompatible file content"
+msgstr ""
+
msgid "StaticSiteEditor|Return to site"
msgstr ""
+msgid "StaticSiteEditor|Static site editor"
+msgstr ""
+
msgid "StaticSiteEditor|Success!"
msgstr ""
msgid "StaticSiteEditor|Summary of changes"
msgstr ""
+msgid "StaticSiteEditor|The Static Site Editor is currently configured to only edit Markdown content on pages generated from Middleman. Visit the documentation to learn more about configuring your site to use the Static Site Editor."
+msgstr ""
+
msgid "StaticSiteEditor|Update %{sourcePath} file"
msgstr ""
+msgid "StaticSiteEditor|View documentation"
+msgstr ""
+
msgid "StaticSiteEditor|View merge request"
msgstr ""
@@ -19716,6 +20171,12 @@ msgstr ""
msgid "StatusPage|Status page"
msgstr ""
+msgid "StatusPage|Status page URL"
+msgstr ""
+
+msgid "StatusPage|Status page frontend documentation"
+msgstr ""
+
msgid "StatusPage|To publish incidents to an external status page, GitLab will store a JSON file in your Amazon S3 account in a location accessible to your external status page service. Make sure to also set up %{docsLink}"
msgstr ""
@@ -20064,6 +20525,12 @@ msgstr ""
msgid "Synced"
msgstr ""
+msgid "Synchronization disabled"
+msgstr ""
+
+msgid "Synchronization of container repositories is disabled."
+msgstr ""
+
msgid "System"
msgstr ""
@@ -20217,6 +20684,9 @@ msgstr ""
msgid "Telephone number"
msgstr ""
+msgid "Telephone number (Optional)"
+msgstr ""
+
msgid "Template"
msgstr ""
@@ -20529,6 +20999,9 @@ msgstr ""
msgid "The license was successfully uploaded and is now active. You can see the details below."
msgstr ""
+msgid "The license was successfully uploaded and will be active from %{starts_at}. You can see the details below."
+msgstr ""
+
msgid "The maximum file size allowed is %{size}."
msgstr ""
@@ -20850,6 +21323,9 @@ msgstr ""
msgid "There was an error getting the epic participants."
msgstr ""
+msgid "There was an error importing the Jira project."
+msgstr ""
+
msgid "There was an error loading users activity calendar."
msgstr ""
@@ -20865,7 +21341,7 @@ msgstr ""
msgid "There was an error resetting user pipeline minutes."
msgstr ""
-msgid "There was an error saving this Geo Node"
+msgid "There was an error saving this Geo Node."
msgstr ""
msgid "There was an error saving your changes."
@@ -20928,6 +21404,9 @@ msgstr ""
msgid "They can be managed using the %{link}."
msgstr ""
+msgid "Third Party Advisory Link"
+msgstr ""
+
msgid "Third party offers"
msgstr ""
@@ -20964,6 +21443,9 @@ msgstr ""
msgid "This application will be able to:"
msgstr ""
+msgid "This attachment has been truncated to avoid exceeding the maximum allowed attachment size of 15MB. %{written_count} of %{issues_count} issues have been included. Consider re-exporting with a narrower selection of issues."
+msgstr ""
+
msgid "This block is self-referential"
msgstr ""
@@ -21105,6 +21587,12 @@ msgstr ""
msgid "This issue is confidential"
msgstr ""
+msgid "This issue is currently blocked by the following issues: %{issues}."
+msgstr ""
+
+msgid "This issue is in a child epic of the filtered epic"
+msgstr ""
+
msgid "This issue is locked."
msgstr ""
@@ -21303,7 +21791,10 @@ msgstr ""
msgid "This user will be the author of all events in the activity feed that are the result of an update, like new branches being created or new commits being pushed to existing branches. Upon creation or when reassigning you can only assign yourself to be the mirror user."
msgstr ""
-msgid "This variable can not be masked"
+msgid "This variable can not be masked."
+msgstr ""
+
+msgid "This variable does not match the expected pattern."
msgstr ""
msgid "This will help us personalize your onboarding experience."
@@ -21333,10 +21824,10 @@ msgstr ""
msgid "ThreatMonitoring|Application firewall not detected"
msgstr ""
-msgid "ThreatMonitoring|Container Network Policy"
+msgid "ThreatMonitoring|Container Network Policies are not installed or have been disabled. To view this data, ensure your Network Policies are installed and enabled for your cluster."
msgstr ""
-msgid "ThreatMonitoring|Container NetworkPolicies are not installed or has been disabled. To view this data, ensure you NetworkPolicies are installed and enabled for your cluster."
+msgid "ThreatMonitoring|Container Network Policy"
msgstr ""
msgid "ThreatMonitoring|Container NetworkPolicies not detected"
@@ -21369,7 +21860,7 @@ msgstr ""
msgid "ThreatMonitoring|Something went wrong, unable to fetch statistics"
msgstr ""
-msgid "ThreatMonitoring|The firewall is not installed or has been disabled. To view this data, ensure you firewall is installed and enabled for your cluster."
+msgid "ThreatMonitoring|The firewall is not installed or has been disabled. To view this data, ensure the web application firewall is installed and enabled for your cluster."
msgstr ""
msgid "ThreatMonitoring|The graph below is an overview of traffic coming to your application as tracked by the Web Application Firewall (WAF). View the docs for instructions on how to access the WAF logs to see what type of malicious traffic is trying to access your app. The docs link is also accessible by clicking the \"?\" icon next to the title below."
@@ -21438,6 +21929,9 @@ msgstr ""
msgid "Time in seconds GitLab will wait for a response from the external service. When the service does not respond in time, access will be denied."
msgstr ""
+msgid "Time of import: %{importTime}"
+msgstr ""
+
msgid "Time remaining"
msgstr ""
@@ -21626,7 +22120,7 @@ msgstr ""
msgid "Title:"
msgstr ""
-msgid "Titles and Filenames"
+msgid "Titles and Descriptions"
msgstr ""
msgid "To"
@@ -21833,7 +22327,7 @@ msgstr ""
msgid "Too many projects enabled. You will need to manage them via the console or the API."
msgstr ""
-msgid "Topics"
+msgid "Topics (optional)"
msgstr ""
msgid "Total"
@@ -21995,6 +22489,9 @@ msgstr ""
msgid "Try all GitLab has to offer for 30 days."
msgstr ""
+msgid "Try changing or removing filters."
+msgstr ""
+
msgid "Try to fork again"
msgstr ""
@@ -22088,6 +22585,9 @@ msgstr ""
msgid "Unable to connect to server: %{error}"
msgstr ""
+msgid "Unable to connect to the Jira instance. Please check your Jira integration configuration."
+msgstr ""
+
msgid "Unable to convert Kubernetes logs encoding to UTF-8"
msgstr ""
@@ -23035,6 +23535,9 @@ msgstr ""
msgid "View issue"
msgstr ""
+msgid "View issues"
+msgstr ""
+
msgid "View it on GitLab"
msgstr ""
@@ -23212,6 +23715,15 @@ msgstr ""
msgid "VulnerabilityManagement|Resolved %{timeago} by %{user}"
msgstr ""
+msgid "VulnerabilityManagement|Something went wrong while trying to delete the comment. Please try again later."
+msgstr ""
+
+msgid "VulnerabilityManagement|Something went wrong while trying to retrieve the vulnerability history. Please try again later."
+msgstr ""
+
+msgid "VulnerabilityManagement|Something went wrong while trying to save the comment. Please try again later."
+msgstr ""
+
msgid "VulnerabilityManagement|Something went wrong, could not create an issue."
msgstr ""
@@ -23242,6 +23754,9 @@ msgstr ""
msgid "VulnerabilityStatusTypes|Resolved"
msgstr ""
+msgid "Vulnerability|%{scannerName} (version %{scannerVersion})"
+msgstr ""
+
msgid "Vulnerability|Class"
msgstr ""
@@ -23275,7 +23790,10 @@ msgstr ""
msgid "Vulnerability|Project"
msgstr ""
-msgid "Vulnerability|Report Type"
+msgid "Vulnerability|Scanner Provider"
+msgstr ""
+
+msgid "Vulnerability|Scanner Type"
msgstr ""
msgid "Vulnerability|Severity"
@@ -23329,9 +23847,15 @@ msgstr ""
msgid "We sent you an email with reset password instructions"
msgstr ""
+msgid "We tried to automatically renew your %{strong}%{plan_name}%{strong_close} subscription for %{strong}%{namespace_name}%{strong_close} on %{expires_on} but something went wrong so your subscription was downgraded to the free plan. Don't worry, your data is safe. We suggest you check your payment method and get in touch with our support team (%{support_link}). They'll gladly help with your subscription renewal."
+msgstr ""
+
msgid "We want to be sure it is you, please confirm you are not a robot."
msgstr ""
+msgid "We will automatically renew your %{strong}%{plan_name}%{strong_close} subscription for %{strong}%{namespace_name}%{strong_close} on %{strong}%{expires_on}%{strong_close}. There's nothing that you need to do, we'll let you know when the renewal is complete. Need more seats, a higher plan or just want to review your payment method?"
+msgstr ""
+
msgid "We've found no vulnerabilities"
msgstr ""
@@ -23438,15 +23962,6 @@ msgstr ""
msgid "When:"
msgstr ""
-msgid "While it's rare to have no vulnerabilities for your group, it can happen. In any event, we ask that you double check your settings to make sure you've set up your dashboard correctly."
-msgstr ""
-
-msgid "While it's rare to have no vulnerabilities for your pipeline, it can happen. In any event, we ask that you double check your settings to make sure all security scanning jobs have passed successfully."
-msgstr ""
-
-msgid "While it's rare to have no vulnerabilities for your project, it can happen. In any event, we ask that you double check your settings to make sure you've set up your dashboard correctly."
-msgstr ""
-
msgid "While it's rare to have no vulnerabilities, it can happen. In any event, we ask that you please double check your settings to make sure you've set up your dashboard correctly."
msgstr ""
@@ -23477,9 +23992,6 @@ msgstr ""
msgid "Wiki"
msgstr ""
-msgid "Wiki pages"
-msgstr ""
-
msgid "Wiki was successfully updated."
msgstr ""
@@ -23663,6 +24175,9 @@ msgstr ""
msgid "Yes, add it"
msgstr ""
+msgid "Yes, close issue"
+msgstr ""
+
msgid "Yes, let me map Google Code users to full names or GitLab users."
msgstr ""
@@ -23858,6 +24373,9 @@ msgstr ""
msgid "You could not create a new trigger."
msgstr ""
+msgid "You didn't renew your %{strong}%{plan_name}%{strong_close} subscription for %{strong}%{namespace_name}%{strong_close} so it was downgraded to the free plan."
+msgstr ""
+
msgid "You didn't renew your %{strong}%{plan_name}%{strong_close} subscription so it was downgraded to the GitLab Core Plan."
msgstr ""
@@ -23924,6 +24442,9 @@ msgstr ""
msgid "You have declined the invitation to join %{label}."
msgstr ""
+msgid "You have imported from this project %{numberOfPreviousImportsForProject} times before. Each new import will create duplicate issues."
+msgstr ""
+
msgid "You have no permissions"
msgstr ""
@@ -24005,6 +24526,9 @@ msgstr ""
msgid "You need to upload a Google Takeout archive."
msgstr ""
+msgid "You reached %{usage_in_percent} of %{namespace_name}'s capacity (%{used_storage} of %{storage_limit})"
+msgstr ""
+
msgid "You tried to fork %{link_to_the_project} but it failed for the following reason:"
msgstr ""
@@ -24089,9 +24613,21 @@ msgstr ""
msgid "YouTube"
msgstr ""
+msgid "Your %{strong}%{plan_name}%{strong_close} subscription for %{strong}%{namespace_name}%{strong_close} will expire on %{strong}%{expires_on}%{strong_close}. After that, you will not to be able to create issues or merge requests as well as many other features."
+msgstr ""
+
msgid "Your %{strong}%{plan_name}%{strong_close} subscription will expire on %{strong}%{expires_on}%{strong_close}. After that, you will not to be able to create issues or merge requests as well as many other features."
msgstr ""
+msgid "Your CSV export has started. It will be emailed to %{email} when complete."
+msgstr ""
+
+msgid "Your CSV export of %{issues_count} from project %{project_link} has been added to this email as an attachment."
+msgstr ""
+
+msgid "Your CSV export of %{written_count} from project %{project_name} (%{project_url}) has been added to this email as an attachment."
+msgstr ""
+
msgid "Your Commit Email will be used for web based operations, such as edits and merges."
msgstr ""
@@ -24188,6 +24724,9 @@ msgstr ""
msgid "Your comment could not be updated! Please check your network connection and try again."
msgstr ""
+msgid "Your comment will be discarded."
+msgstr ""
+
msgid "Your custom stage '%{title}' was created"
msgstr ""
@@ -24227,6 +24766,9 @@ msgstr ""
msgid "Your new personal access token has been created."
msgstr ""
+msgid "Your new project access token has been created."
+msgstr ""
+
msgid "Your password isn't required to view this page. If a password or any other personal details are requested, please contact your administrator to report abuse."
msgstr ""
@@ -24245,12 +24787,18 @@ msgstr ""
msgid "Your request for access has been queued for review."
msgstr ""
+msgid "Your search didn't match any commits."
+msgstr ""
+
msgid "Your subscription expired!"
msgstr ""
msgid "Your subscription has been downgraded"
msgstr ""
+msgid "Your subscription will automatically renew in %{remaining_days}"
+msgstr ""
+
msgid "Your subscription will expire in %{remaining_days}"
msgstr ""
@@ -24291,7 +24839,7 @@ msgstr ""
msgid "allowed to fail"
msgstr ""
-msgid "already being used for another group or project milestone."
+msgid "already being used for another group or project %{timebox_name}."
msgstr ""
msgid "already has a \"created\" issue link"
@@ -24369,6 +24917,9 @@ msgstr ""
msgid "ciReport|%{linkStartTag}Learn more about SAST %{linkEndTag}"
msgstr ""
+msgid "ciReport|%{linkStartTag}Learn more about Secret Scanning %{linkEndTag}"
+msgstr ""
+
msgid "ciReport|%{linkStartTag}Learn more about codequality reports %{linkEndTag}"
msgstr ""
@@ -24542,6 +25093,12 @@ msgstr ""
msgid "ciReport|SAST"
msgstr ""
+msgid "ciReport|Secret scanning"
+msgstr ""
+
+msgid "ciReport|Secret scanning detects secrets and credentials vulnerabilities in your source code."
+msgstr ""
+
msgid "ciReport|Security scanning"
msgstr ""
@@ -24747,6 +25304,9 @@ msgstr ""
msgid "group"
msgstr ""
+msgid "groups"
+msgstr ""
+
msgid "has already been linked to another vulnerability"
msgstr ""
@@ -24914,9 +25474,6 @@ msgstr[3] ""
msgid "merged %{time_ago}"
msgstr ""
-msgid "milestone should belong either to a project or a group."
-msgstr ""
-
msgid "missing"
msgstr ""
@@ -25253,6 +25810,9 @@ msgstr ""
msgid "no contributions"
msgstr ""
+msgid "no expiration"
+msgstr ""
+
msgid "no one can merge"
msgstr ""
@@ -25309,6 +25869,9 @@ msgstr ""
msgid "pending removal"
msgstr ""
+msgid "per day"
+msgstr ""
+
msgid "pipeline"
msgstr ""
@@ -25341,6 +25904,12 @@ msgstr[3] ""
msgid "project avatar"
msgstr ""
+msgid "projects"
+msgstr ""
+
+msgid "push to your repository, create pipelines, create issues or add comments. To reduce storage capacity, delete unused repositories, artifacts, wikis, issues, and pipelines."
+msgstr ""
+
msgid "quick actions"
msgstr ""
@@ -25489,13 +26058,6 @@ msgstr ""
msgid "this document"
msgstr ""
-msgid "thread resolved"
-msgid_plural "threads resolved"
-msgstr[0] ""
-msgstr[1] ""
-msgstr[2] ""
-msgstr[3] ""
-
msgid "to help your contributors communicate effectively!"
msgstr ""
@@ -25550,6 +26112,9 @@ msgstr ""
msgid "view the blob"
msgstr ""
+msgid "vulnerability|Add a comment"
+msgstr ""
+
msgid "vulnerability|Add a comment or reason for dismissal"
msgstr ""
diff --git a/locale/sq_AL/gitlab.po b/locale/sq_AL/gitlab.po
index b4854a29402..0aac7350673 100644
--- a/locale/sq_AL/gitlab.po
+++ b/locale/sq_AL/gitlab.po
@@ -12,7 +12,7 @@ msgstr ""
"X-Crowdin-Project: gitlab-ee\n"
"X-Crowdin-Language: sq\n"
"X-Crowdin-File: /master/locale/gitlab.pot\n"
-"PO-Revision-Date: 2020-04-15 00:25\n"
+"PO-Revision-Date: 2020-05-05 21:36\n"
msgid " %{start} to %{end}"
msgstr ""
@@ -212,6 +212,11 @@ msgid_plural "%d tags"
msgstr[0] ""
msgstr[1] ""
+msgid "%d unresolved thread"
+msgid_plural "%d unresolved threads"
+msgstr[0] ""
+msgstr[1] ""
+
msgid "%d vulnerability dismissed"
msgid_plural "%d vulnerabilities dismissed"
msgstr[0] ""
@@ -371,6 +376,9 @@ msgstr ""
msgid "%{mrText}, this issue will be closed automatically."
msgstr ""
+msgid "%{namespace_name} is now read-only. You cannot: %{base_message}"
+msgstr ""
+
msgid "%{name} contained %{resultsString}"
msgstr ""
@@ -383,6 +391,11 @@ msgstr ""
msgid "%{name}'s avatar"
msgstr ""
+msgid "%{no_of_days} day"
+msgid_plural "%{no_of_days} days"
+msgstr[0] ""
+msgstr[1] ""
+
msgid "%{number_commits_behind} commits behind %{default_branch}, %{number_commits_ahead} commits ahead"
msgstr ""
@@ -506,6 +519,9 @@ msgstr[1] ""
msgid "%{text} is available"
msgstr ""
+msgid "%{timebox_name} should belong either to a project or a group."
+msgstr ""
+
msgid "%{title} %{operator} %{threshold}"
msgstr ""
@@ -781,12 +797,18 @@ msgstr ""
msgid "<code>\"johnsmith@example.com\": \"johnsmith@example.com\"</code> will add \"By <a href=\"#\">johnsmith@example.com</a>\" to all issues and comments originally created by johnsmith@example.com. By default, the email address or username is masked to ensure the user's privacy. Use this option if you want to show the full email address."
msgstr ""
+msgid "<namespace / project>"
+msgstr ""
+
msgid "<no name set>"
msgstr ""
msgid "<no scopes selected>"
msgstr ""
+msgid "<project name>"
+msgstr ""
+
msgid "<strong>%{group_name}</strong> group members"
msgstr ""
@@ -820,6 +842,9 @@ msgstr ""
msgid "A basic page and serverless function that uses AWS Lambda, AWS API Gateway, and GitLab Pages"
msgstr ""
+msgid "A complete DevOps platform"
+msgstr ""
+
msgid "A default branch cannot be chosen for an empty project."
msgstr ""
@@ -1036,6 +1061,12 @@ msgstr ""
msgid "AccessTokens|reset it"
msgstr ""
+msgid "AccessibilityReport|Accessibility report artifact not found"
+msgstr ""
+
+msgid "AccessibilityReport|Failed to retrieve accessibility report"
+msgstr ""
+
msgid "AccessibilityReport|Learn More"
msgstr ""
@@ -1045,6 +1076,9 @@ msgstr ""
msgid "AccessibilityReport|New"
msgstr ""
+msgid "AccessibilityReport|The accessibility scanning found an error of the following type: %{code}"
+msgstr ""
+
msgid "Account"
msgstr ""
@@ -1287,6 +1321,9 @@ msgstr ""
msgid "Added at"
msgstr ""
+msgid "Added for this merge request"
+msgstr ""
+
msgid "Added in this version"
msgstr ""
@@ -1658,6 +1695,72 @@ msgid_plural "Alerts"
msgstr[0] ""
msgstr[1] ""
+msgid "Alert Details"
+msgstr ""
+
+msgid "AlertManagement|Acknowledged"
+msgstr ""
+
+msgid "AlertManagement|Alert"
+msgstr ""
+
+msgid "AlertManagement|Authorize external service"
+msgstr ""
+
+msgid "AlertManagement|Display alerts from all your monitoring tools directly within GitLab. Streamline the investigation of your alerts and the escalation of alerts to incidents."
+msgstr ""
+
+msgid "AlertManagement|End time"
+msgstr ""
+
+msgid "AlertManagement|End time:"
+msgstr ""
+
+msgid "AlertManagement|Events"
+msgstr ""
+
+msgid "AlertManagement|Events:"
+msgstr ""
+
+msgid "AlertManagement|Full Alert Details"
+msgstr ""
+
+msgid "AlertManagement|More information"
+msgstr ""
+
+msgid "AlertManagement|No alerts available to display. If you think you're seeing this message in error, refresh the page."
+msgstr ""
+
+msgid "AlertManagement|No alerts to display."
+msgstr ""
+
+msgid "AlertManagement|Overview"
+msgstr ""
+
+msgid "AlertManagement|Resolved"
+msgstr ""
+
+msgid "AlertManagement|Severity"
+msgstr ""
+
+msgid "AlertManagement|Start time"
+msgstr ""
+
+msgid "AlertManagement|Start time:"
+msgstr ""
+
+msgid "AlertManagement|Status"
+msgstr ""
+
+msgid "AlertManagement|Surface alerts in GitLab"
+msgstr ""
+
+msgid "AlertManagement|There was an error displaying the alerts. Confirm your endpoint's configuration details to ensure alerts appear."
+msgstr ""
+
+msgid "AlertManagement|Triggered"
+msgstr ""
+
msgid "AlertService|%{linkStart}Learn more%{linkEnd} about configuring this endpoint to receive alerts."
msgstr ""
@@ -1727,6 +1830,9 @@ msgstr ""
msgid "All security scans are enabled because %{linkStart}Auto DevOps%{linkEnd} is enabled on this project"
msgstr ""
+msgid "All threads resolved"
+msgstr ""
+
msgid "All users"
msgstr ""
@@ -1745,6 +1851,9 @@ msgstr ""
msgid "Allow only the selected protocols to be used for Git access."
msgstr ""
+msgid "Allow owners to manage default branch protection per group"
+msgstr ""
+
msgid "Allow owners to manually add users outside of LDAP"
msgstr ""
@@ -1853,6 +1962,9 @@ msgstr ""
msgid "An error occurred fetching the dropdown data."
msgstr ""
+msgid "An error occurred fetching the project authors."
+msgstr ""
+
msgid "An error occurred previewing the blob"
msgstr ""
@@ -1934,6 +2046,9 @@ msgstr ""
msgid "An error occurred while fetching sidebar data"
msgstr ""
+msgid "An error occurred while fetching terraform reports."
+msgstr ""
+
msgid "An error occurred while fetching the Service Desk address."
msgstr ""
@@ -2171,6 +2286,9 @@ msgstr ""
msgid "Any"
msgstr ""
+msgid "Any Author"
+msgstr ""
+
msgid "Any Label"
msgstr ""
@@ -2386,10 +2504,10 @@ msgstr ""
msgid "Are you sure that you want to unarchive this project?"
msgstr ""
-msgid "Are you sure you want to cancel creating this comment?"
+msgid "Are you sure you want to cancel editing this comment?"
msgstr ""
-msgid "Are you sure you want to cancel editing this comment?"
+msgid "Are you sure you want to close this blocked issue?"
msgstr ""
msgid "Are you sure you want to delete %{name}?"
@@ -2419,6 +2537,9 @@ msgstr ""
msgid "Are you sure you want to deploy this environment?"
msgstr ""
+msgid "Are you sure you want to discard this comment?"
+msgstr ""
+
msgid "Are you sure you want to erase this build?"
msgstr ""
@@ -2595,6 +2716,9 @@ msgstr ""
msgid "At least one approval from a code owner is required to change files matching the respective CODEOWNER rules."
msgstr ""
+msgid "At least one logging option is required to be enabled"
+msgstr ""
+
msgid "At least one of group_id or project_id must be specified"
msgstr ""
@@ -2633,6 +2757,27 @@ msgstr ""
msgid "AuditEvents|Target"
msgstr ""
+msgid "AuditLogs|(removed)"
+msgstr ""
+
+msgid "AuditLogs|Action"
+msgstr ""
+
+msgid "AuditLogs|Author"
+msgstr ""
+
+msgid "AuditLogs|Date"
+msgstr ""
+
+msgid "AuditLogs|IP Address"
+msgstr ""
+
+msgid "AuditLogs|Object"
+msgstr ""
+
+msgid "AuditLogs|Target"
+msgstr ""
+
msgid "Aug"
msgstr ""
@@ -2915,9 +3060,6 @@ msgstr ""
msgid "BambooService|You must set up automatic revision labeling and a repository trigger in Bamboo."
msgstr ""
-msgid "Batch operations"
-msgstr ""
-
msgid "BatchComments|Delete all pending comments"
msgstr ""
@@ -3047,6 +3189,9 @@ msgstr ""
msgid "Boards"
msgstr ""
+msgid "Boards and Board Lists"
+msgstr ""
+
msgid "Boards|Collapse"
msgstr ""
@@ -3266,6 +3411,9 @@ msgstr ""
msgid "Buy GitLab Enterprise Edition"
msgstr ""
+msgid "Buy more Pipeline minutes"
+msgstr ""
+
msgid "By %{user_name}"
msgstr ""
@@ -3386,6 +3534,9 @@ msgstr ""
msgid "Can't scan the code?"
msgstr ""
+msgid "Can't update snippet: %{err}"
+msgstr ""
+
msgid "Canary"
msgstr ""
@@ -3554,6 +3705,9 @@ msgstr ""
msgid "Changing group path can have unintended side effects."
msgstr ""
+msgid "Charts"
+msgstr ""
+
msgid "Charts can't be displayed as the request for data has timed out. %{documentationLink}"
msgstr ""
@@ -3590,9 +3744,6 @@ msgstr ""
msgid "ChatMessage|and [%{count} more](%{pipeline_failed_jobs_url})"
msgstr ""
-msgid "ChatMessage|failed"
-msgstr ""
-
msgid "ChatMessage|has failed"
msgstr ""
@@ -3608,9 +3759,6 @@ msgstr ""
msgid "ChatMessage|in %{project_link}"
msgstr ""
-msgid "ChatMessage|passed"
-msgstr ""
-
msgid "Check again"
msgstr ""
@@ -4061,9 +4209,6 @@ msgstr ""
msgid "Click any <strong>project name</strong> in the project list below to navigate to the project milestone."
msgstr ""
-msgid "Click here"
-msgstr ""
-
msgid "Click the <strong>Download</strong> button and wait for downloading to complete."
msgstr ""
@@ -4451,6 +4596,12 @@ msgstr ""
msgid "ClusterIntegration|Fetching zones"
msgstr ""
+msgid "ClusterIntegration|Fluentd"
+msgstr ""
+
+msgid "ClusterIntegration|Fluentd is an open source data collector, which lets you unify the data collection and consumption for a better use and understanding of data. It requires at least one of the following logs to be successfully installed."
+msgstr ""
+
msgid "ClusterIntegration|GitLab Integration"
msgstr ""
@@ -4757,6 +4908,15 @@ msgstr ""
msgid "ClusterIntegration|Request to begin uninstalling failed"
msgstr ""
+msgid "ClusterIntegration|SIEM Hostname"
+msgstr ""
+
+msgid "ClusterIntegration|SIEM Port"
+msgstr ""
+
+msgid "ClusterIntegration|SIEM Protocol"
+msgstr ""
+
msgid "ClusterIntegration|Save changes"
msgstr ""
@@ -4853,6 +5013,12 @@ msgstr ""
msgid "ClusterIntegration|Select zone to choose machine type"
msgstr ""
+msgid "ClusterIntegration|Send Cilium Logs"
+msgstr ""
+
+msgid "ClusterIntegration|Send ModSecurity Logs"
+msgstr ""
+
msgid "ClusterIntegration|Service Token"
msgstr ""
@@ -5281,18 +5447,33 @@ msgstr ""
msgid "Compliance framework (optional)"
msgstr ""
+msgid "ComplianceFramework|GDPR"
+msgstr ""
+
msgid "ComplianceFramework|GDPR - General Data Protection Regulation"
msgstr ""
+msgid "ComplianceFramework|HIPAA"
+msgstr ""
+
msgid "ComplianceFramework|HIPAA - Health Insurance Portability and Accountability Act"
msgstr ""
+msgid "ComplianceFramework|PCI-DSS"
+msgstr ""
+
msgid "ComplianceFramework|PCI-DSS - Payment Card Industry-Data Security Standard"
msgstr ""
+msgid "ComplianceFramework|SOC 2"
+msgstr ""
+
msgid "ComplianceFramework|SOC 2 - Service Organization Control 2"
msgstr ""
+msgid "ComplianceFramework|SOX"
+msgstr ""
+
msgid "ComplianceFramework|SOX - Sarbanes-Oxley"
msgstr ""
@@ -5440,9 +5621,15 @@ msgstr ""
msgid "Container repositories sync capacity"
msgstr ""
+msgid "ContainerRegistry| Please visit the %{linkStart}administration settings%{linkEnd} to enable this feature."
+msgstr ""
+
msgid "ContainerRegistry|%{imageName} tags"
msgstr ""
+msgid "ContainerRegistry|%{title} was successfully scheduled for deletion"
+msgstr ""
+
msgid "ContainerRegistry|Automatically remove extra images that aren't designed to be kept."
msgstr ""
@@ -5455,6 +5642,9 @@ msgstr ""
msgid "ContainerRegistry|Container Registry"
msgstr ""
+msgid "ContainerRegistry|Container Registry tag expiration and retention policy is disabled"
+msgstr ""
+
msgid "ContainerRegistry|Copy build command"
msgstr ""
@@ -5464,18 +5654,12 @@ msgstr ""
msgid "ContainerRegistry|Copy push command"
msgstr ""
-msgid "ContainerRegistry|Currently, the Container Registry tag expiration feature is not available for projects created before GitLab version 12.8. For updates and more information, visit Issue %{linkStart}#196124%{linkEnd}"
-msgstr ""
-
msgid "ContainerRegistry|Docker connection error"
msgstr ""
msgid "ContainerRegistry|Docker tag expiration policy is %{toggleStatus}"
msgstr ""
-msgid "ContainerRegistry|Docker tags with names matching this regex pattern will expire:"
-msgstr ""
-
msgid "ContainerRegistry|Edit Settings"
msgstr ""
@@ -5497,9 +5681,6 @@ msgstr ""
msgid "ContainerRegistry|Image ID"
msgstr ""
-msgid "ContainerRegistry|Image deleted successfully"
-msgstr ""
-
msgid "ContainerRegistry|Keep and protect the images that matter most."
msgstr ""
@@ -5515,12 +5696,24 @@ msgstr ""
msgid "ContainerRegistry|Number of tags to retain:"
msgstr ""
+msgid "ContainerRegistry|Please contact your administrator."
+msgstr ""
+
msgid "ContainerRegistry|Push an image"
msgstr ""
msgid "ContainerRegistry|Quick Start"
msgstr ""
+msgid "ContainerRegistry|Regular expressions such as %{codeStart}.*-test%{codeEnd} or %{codeStart}dev-.*%{codeEnd} are supported"
+msgstr ""
+
+msgid "ContainerRegistry|Regular expressions such as %{codeStart}.*-test%{codeEnd} or %{codeStart}dev-.*%{codeEnd} are supported. To select all tags, use %{codeStart}.*%{codeEnd}"
+msgstr ""
+
+msgid "ContainerRegistry|Remember to run %{docLinkStart}garbage collection%{docLinkEnd} to remove the stale data from storage."
+msgstr ""
+
msgid "ContainerRegistry|Remove repository"
msgstr ""
@@ -5535,22 +5728,22 @@ msgstr[1] ""
msgid "ContainerRegistry|Retention policy has been Enabled"
msgstr ""
-msgid "ContainerRegistry|Something went wrong while deleting the image."
+msgid "ContainerRegistry|Something went wrong while fetching the expiration policy."
msgstr ""
-msgid "ContainerRegistry|Something went wrong while deleting the tag."
+msgid "ContainerRegistry|Something went wrong while fetching the repository list."
msgstr ""
-msgid "ContainerRegistry|Something went wrong while deleting the tags."
+msgid "ContainerRegistry|Something went wrong while fetching the tags list."
msgstr ""
-msgid "ContainerRegistry|Something went wrong while fetching the expiration policy."
+msgid "ContainerRegistry|Something went wrong while marking the tag for deletion."
msgstr ""
-msgid "ContainerRegistry|Something went wrong while fetching the packages list."
+msgid "ContainerRegistry|Something went wrong while marking the tags for deletion."
msgstr ""
-msgid "ContainerRegistry|Something went wrong while fetching the tags list."
+msgid "ContainerRegistry|Something went wrong while scheduling %{title} for deletion. Please try again."
msgstr ""
msgid "ContainerRegistry|Something went wrong while updating the expiration policy."
@@ -5559,16 +5752,25 @@ msgstr ""
msgid "ContainerRegistry|Tag"
msgstr ""
-msgid "ContainerRegistry|Tag deleted successfully"
-msgstr ""
-
msgid "ContainerRegistry|Tag expiration policy"
msgstr ""
msgid "ContainerRegistry|Tag expiration policy is designed to:"
msgstr ""
-msgid "ContainerRegistry|Tags deleted successfully"
+msgid "ContainerRegistry|Tag successfully marked for deletion."
+msgstr ""
+
+msgid "ContainerRegistry|Tags successfully marked for deletion."
+msgstr ""
+
+msgid "ContainerRegistry|Tags with names matching this regex pattern will %{italicStart}be preserved:%{italicEnd}"
+msgstr ""
+
+msgid "ContainerRegistry|Tags with names matching this regex pattern will %{italicStart}expire:%{italicEnd}"
+msgstr ""
+
+msgid "ContainerRegistry|The Container Registry tag expiration and retention policies for this project have not been enabled."
msgstr ""
msgid "ContainerRegistry|The last tag related to this image was recently removed. This empty image and any associated data will be automatically removed as part of the regular Garbage Collection process. If you have any questions, contact your administrator."
@@ -5589,16 +5791,16 @@ msgstr ""
msgid "ContainerRegistry|There are no container images stored for this project"
msgstr ""
-msgid "ContainerRegistry|This Registry contains deleted image tag data. Remember to run %{docLinkStart}garbage collection%{docLinkEnd} to remove the stale data from storage."
+msgid "ContainerRegistry|There was an error during the deletion of this image repository, please try again."
msgstr ""
msgid "ContainerRegistry|This image has no active tags"
msgstr ""
-msgid "ContainerRegistry|We are having trouble connecting to Docker, which could be due to an issue with your project name or path. %{docLinkStart}More Information%{docLinkEnd}"
+msgid "ContainerRegistry|This image repository is scheduled for deletion"
msgstr ""
-msgid "ContainerRegistry|Wildcards such as %{codeStart}.*-stable%{codeEnd} or %{codeStart}production/.*%{codeEnd} are supported. To select all tags, use %{codeStart}.*%{codeEnd}"
+msgid "ContainerRegistry|We are having trouble connecting to Docker, which could be due to an issue with your project name or path. %{docLinkStart}More Information%{docLinkEnd}"
msgstr ""
msgid "ContainerRegistry|With the Container Registry, every project can have its own space to store its Docker images. %{docLinkStart}More Information%{docLinkEnd}"
@@ -5853,7 +6055,7 @@ msgstr ""
msgid "Could not delete chat nickname %{chat_name}."
msgstr ""
-msgid "Could not find design"
+msgid "Could not find design."
msgstr ""
msgid "Could not remove the trigger."
@@ -5868,6 +6070,9 @@ msgstr ""
msgid "Could not revoke personal access token %{personal_access_token_name}."
msgstr ""
+msgid "Could not revoke project access token %{project_access_token_name}."
+msgstr ""
+
msgid "Could not save group ID"
msgstr ""
@@ -5925,6 +6130,9 @@ msgstr ""
msgid "Create a new branch"
msgstr ""
+msgid "Create a new deploy key for this project"
+msgstr ""
+
msgid "Create a new file as there are no files yet. Afterwards, you'll be able to commit your changes."
msgstr ""
@@ -6030,6 +6238,9 @@ msgstr ""
msgid "Create requirement"
msgstr ""
+msgid "Create snippet"
+msgstr ""
+
msgid "Create wildcard: %{searchTerm}"
msgstr ""
@@ -6051,6 +6262,9 @@ msgstr ""
msgid "Created"
msgstr ""
+msgid "Created %{timestamp}"
+msgstr ""
+
msgid "Created At"
msgstr ""
@@ -6407,6 +6621,9 @@ msgstr ""
msgid "CycleAnalytics|stage dropdown"
msgstr ""
+msgid "DAG"
+msgstr ""
+
msgid "DNS"
msgstr ""
@@ -6773,6 +6990,9 @@ msgstr ""
msgid "Deploy key was successfully updated."
msgstr ""
+msgid "Deploy keys allow read-only or read-write (if enabled) access to your repository. Deploy keys can be used for CI, staging or production servers. You can create a deploy key or add an existing one."
+msgstr ""
+
msgid "Deploy progress not found. To see pods, ensure your environment matches %{linkStart}deploy board criteria%{linkEnd}."
msgstr ""
@@ -6926,6 +7146,9 @@ msgstr ""
msgid "Deploying to"
msgstr ""
+msgid "Deployment Frequency"
+msgstr ""
+
msgid "Deployment|API"
msgstr ""
@@ -7444,9 +7667,6 @@ msgstr ""
msgid "Edit environment"
msgstr ""
-msgid "Edit epic description"
-msgstr ""
-
msgid "Edit file"
msgstr ""
@@ -8068,9 +8288,6 @@ msgstr ""
msgid "Epics"
msgstr ""
-msgid "Epics (Ultimate / Gold license only)"
-msgstr ""
-
msgid "Epics Roadmap"
msgstr ""
@@ -8080,6 +8297,9 @@ msgstr ""
msgid "Epics let you manage your portfolio of projects more efficiently and with less effort"
msgstr ""
+msgid "Epics, Issues, and Merge Requests"
+msgstr ""
+
msgid "Epics|Add an epic"
msgstr ""
@@ -8131,6 +8351,9 @@ msgstr ""
msgid "Epics|Something went wrong while fetching group epics."
msgstr ""
+msgid "Epics|Something went wrong while moving item."
+msgstr ""
+
msgid "Epics|Something went wrong while ordering item."
msgstr ""
@@ -8185,6 +8408,9 @@ msgstr ""
msgid "Error fetching network graph."
msgstr ""
+msgid "Error fetching payload data."
+msgstr ""
+
msgid "Error fetching projects"
msgstr ""
@@ -8194,9 +8420,6 @@ msgstr ""
msgid "Error fetching the dependency list. Please check your network connection and try again."
msgstr ""
-msgid "Error fetching usage ping data."
-msgstr ""
-
msgid "Error loading branch data. Please try again."
msgstr ""
@@ -8338,6 +8561,9 @@ msgstr ""
msgid "Errors"
msgstr ""
+msgid "Errors:"
+msgstr ""
+
msgid "Estimated"
msgstr ""
@@ -8542,12 +8768,18 @@ msgstr ""
msgid "Export as CSV"
msgstr ""
+msgid "Export group"
+msgstr ""
+
msgid "Export issues"
msgstr ""
msgid "Export project"
msgstr ""
+msgid "Export this group with all related data to a new GitLab instance. Once complete, you can import the data file from the \"New Group\" page."
+msgstr ""
+
msgid "Export this project with all its related data in order to move your project to a new GitLab instance. Once the export is finished, you can import the file from the \"New Project\" page."
msgstr ""
@@ -8617,9 +8849,6 @@ msgstr ""
msgid "Failed Jobs"
msgstr ""
-msgid "Failed create wiki"
-msgstr ""
-
msgid "Failed to add a Zoom meeting"
msgstr ""
@@ -8659,6 +8888,9 @@ msgstr ""
msgid "Failed to create resources"
msgstr ""
+msgid "Failed to create wiki"
+msgstr ""
+
msgid "Failed to delete board. Please try again."
msgstr ""
@@ -8668,7 +8900,7 @@ msgstr ""
msgid "Failed to enqueue the rebase operation, possibly due to a long-lived transaction. Try again later."
msgstr ""
-msgid "Failed to find import label for jira import."
+msgid "Failed to find import label for Jira import."
msgstr ""
msgid "Failed to get ref."
@@ -8692,6 +8924,12 @@ msgstr ""
msgid "Failed to load groups & users."
msgstr ""
+msgid "Failed to load labels. Please try again."
+msgstr ""
+
+msgid "Failed to load milestones. Please try again."
+msgstr ""
+
msgid "Failed to load related branches"
msgstr ""
@@ -9067,7 +9305,10 @@ msgstr ""
msgid "Filter by milestone name"
msgstr ""
-msgid "Filter by name..."
+msgid "Filter by name"
+msgstr ""
+
+msgid "Filter by status"
msgstr ""
msgid "Filter by two-factor authentication"
@@ -9079,12 +9320,18 @@ msgstr ""
msgid "Filter projects"
msgstr ""
+msgid "Filter results"
+msgstr ""
+
msgid "Filter results by group"
msgstr ""
msgid "Filter results by project"
msgstr ""
+msgid "Filter results..."
+msgstr ""
+
msgid "Filter your projects by name"
msgstr ""
@@ -9373,10 +9620,10 @@ msgstr ""
msgid "GeoNodes|Checksummed"
msgstr ""
-msgid "GeoNodes|Container repositories"
+msgid "GeoNodes|Consult Geo troubleshooting information"
msgstr ""
-msgid "GeoNodes|Data is out of date from %{timeago}"
+msgid "GeoNodes|Container repositories"
msgstr ""
msgid "GeoNodes|Data replication lag"
@@ -9418,6 +9665,9 @@ msgstr ""
msgid "GeoNodes|Last event ID seen from primary"
msgstr ""
+msgid "GeoNodes|Learn more about Geo node statuses"
+msgstr ""
+
msgid "GeoNodes|Loading nodes"
msgstr ""
@@ -9433,6 +9683,9 @@ msgstr ""
msgid "GeoNodes|Node was successfully removed."
msgstr ""
+msgid "GeoNodes|Node's status was updated %{timeAgo}."
+msgstr ""
+
msgid "GeoNodes|Not checksummed"
msgstr ""
@@ -9463,7 +9716,10 @@ msgstr ""
msgid "GeoNodes|Repository verification progress"
msgstr ""
-msgid "GeoNodes|Selective"
+msgid "GeoNodes|Selective (%{syncLabel})"
+msgstr ""
+
+msgid "GeoNodes|Selective synchronization"
msgstr ""
msgid "GeoNodes|Something went wrong while changing node status"
@@ -9490,6 +9746,9 @@ msgstr ""
msgid "GeoNodes|Unverified"
msgstr ""
+msgid "GeoNodes|Updated %{timeAgo}"
+msgstr ""
+
msgid "GeoNodes|Used slots"
msgstr ""
@@ -9529,16 +9788,16 @@ msgstr ""
msgid "Geo|All"
msgstr ""
-msgid "Geo|All projects"
+msgid "Geo|All %{replicable_type}"
msgstr ""
-msgid "Geo|All projects are being scheduled for re-sync"
+msgid "Geo|All projects"
msgstr ""
-msgid "Geo|All projects are being scheduled for re-verify"
+msgid "Geo|All projects are being scheduled for resync"
msgstr ""
-msgid "Geo|Batch operations"
+msgid "Geo|All projects are being scheduled for reverify"
msgstr ""
msgid "Geo|Could not remove tracking entry for an existing project."
@@ -9550,9 +9809,15 @@ msgstr ""
msgid "Geo|Failed"
msgstr ""
+msgid "Geo|Filter by status"
+msgstr ""
+
msgid "Geo|Geo Status"
msgstr ""
+msgid "Geo|In progress"
+msgstr ""
+
msgid "Geo|In sync"
msgstr ""
@@ -9577,9 +9842,6 @@ msgstr ""
msgid "Geo|Not synced yet"
msgstr ""
-msgid "Geo|Pending"
-msgstr ""
-
msgid "Geo|Pending synchronization"
msgstr ""
@@ -9616,7 +9878,7 @@ msgstr ""
msgid "Geo|Resync"
msgstr ""
-msgid "Geo|Resync all projects"
+msgid "Geo|Resync all"
msgstr ""
msgid "Geo|Retry count"
@@ -9625,7 +9887,7 @@ msgstr ""
msgid "Geo|Reverify"
msgstr ""
-msgid "Geo|Reverify all projects"
+msgid "Geo|Reverify all"
msgstr ""
msgid "Geo|Status"
@@ -9967,9 +10229,6 @@ msgstr ""
msgid "Go to file"
msgstr ""
-msgid "Go to file (MRs only)"
-msgstr ""
-
msgid "Go to file permalink (while viewing a file)"
msgstr ""
@@ -10414,6 +10673,9 @@ msgstr ""
msgid "GroupSettings|Disable group mentions"
msgstr ""
+msgid "GroupSettings|Export group"
+msgstr ""
+
msgid "GroupSettings|If the parent group's visibility is lower than the group current visibility, visibility levels for subgroups and projects will be changed to match the new parent group's visibility."
msgstr ""
@@ -10492,6 +10754,9 @@ msgstr ""
msgid "Groups (%{count})"
msgstr ""
+msgid "Groups (%{groups})"
+msgstr ""
+
msgid "Groups can also be nested by creating %{subgroup_docs_link_start}subgroups%{subgroup_docs_link_end}."
msgstr ""
@@ -10757,15 +11022,12 @@ msgstr ""
msgid "ID:"
msgstr ""
-msgid "IDE|Allow live previews of JavaScript projects in the Web IDE using CodeSandbox client side evaluation."
+msgid "IDE|Allow live previews of JavaScript projects in the Web IDE using CodeSandbox Live Preview."
msgstr ""
msgid "IDE|Back"
msgstr ""
-msgid "IDE|Client side evaluation"
-msgstr ""
-
msgid "IDE|Commit"
msgstr ""
@@ -10829,6 +11091,9 @@ msgstr ""
msgid "If any job surpasses this timeout threshold, it will be marked as failed. Human readable time input language is accepted like \"1 hour\". Values without specification represent seconds."
msgstr ""
+msgid "If blank, set allowable lifetime to %{instance_level_policy_in_words}, as defined by the instance admin. Once set, existing tokens for users in this group may be revoked."
+msgstr ""
+
msgid "If checked, group owners can manage LDAP group links and LDAP member overrides"
msgstr ""
@@ -10868,6 +11133,9 @@ msgstr ""
msgid "If you lose your recovery codes you can generate new ones, invalidating all previous codes."
msgstr ""
+msgid "If you reach 100%% storage capacity, you will not be able to: %{base_message}"
+msgstr ""
+
msgid "If your HTTP repository is not publicly accessible, add your credentials."
msgstr ""
@@ -10979,6 +11247,9 @@ msgstr ""
msgid "Import repository"
msgstr ""
+msgid "Import started by: %{importInitiator}"
+msgstr ""
+
msgid "Import tasks"
msgstr ""
@@ -11045,6 +11316,9 @@ msgstr ""
msgid "In order to tailor your experience with GitLab we<br>would like to know a bit more about you."
msgstr ""
+msgid "In progress"
+msgstr ""
+
msgid "In the next step, you'll be able to select the projects you want to import."
msgstr ""
@@ -11392,15 +11666,15 @@ msgstr ""
msgid "Issues"
msgstr ""
-msgid "Issues / Merge Requests"
-msgstr ""
-
msgid "Issues Analytics"
msgstr ""
msgid "Issues Rate Limits"
msgstr ""
+msgid "Issues and Merge Requests"
+msgstr ""
+
msgid "Issues can be bugs, tasks or ideas to be discussed. Also, issues are searchable and filterable."
msgstr ""
@@ -11422,10 +11696,10 @@ msgstr ""
msgid "IssuesAnalytics|Avg/Month:"
msgstr ""
-msgid "IssuesAnalytics|Issues created"
+msgid "IssuesAnalytics|Issues opened"
msgstr ""
-msgid "IssuesAnalytics|Issues created per month"
+msgid "IssuesAnalytics|Issues opened per month"
msgstr ""
msgid "IssuesAnalytics|Last 12 months"
@@ -11479,6 +11753,9 @@ msgstr ""
msgid "Jira integration not configured."
msgstr ""
+msgid "Jira project: %{importProject}"
+msgstr ""
+
msgid "JiraService|Events for %{noteable_model_name} are disabled."
msgstr ""
@@ -11868,6 +12145,9 @@ msgstr ""
msgid "Lead"
msgstr ""
+msgid "Lead Time"
+msgstr ""
+
msgid "Learn GitLab"
msgstr ""
@@ -11880,6 +12160,9 @@ msgstr ""
msgid "Learn how to %{no_packages_link_start}publish and share your packages%{no_packages_link_end} with GitLab."
msgstr ""
+msgid "Learn how to enable synchronization"
+msgstr ""
+
msgid "Learn more"
msgstr ""
@@ -12211,9 +12494,6 @@ msgstr ""
msgid "Live preview"
msgstr ""
-msgid "Load more vulnerabilities"
-msgstr ""
-
msgid "Loading"
msgstr ""
@@ -12364,9 +12644,6 @@ msgstr ""
msgid "Manage"
msgstr ""
-msgid "Manage Git repositories with fine-grained access controls that keep your code secure. Perform code reviews and enhance collaboration with merge requests. Each project can also have an issue tracker and a wiki."
-msgstr ""
-
msgid "Manage Web IDE features"
msgstr ""
@@ -12397,6 +12674,9 @@ msgstr ""
msgid "Manage your license"
msgstr ""
+msgid "Managed Account"
+msgstr ""
+
msgid "Manifest"
msgstr ""
@@ -12661,6 +12941,9 @@ msgstr ""
msgid "Merge Requests in Review"
msgstr ""
+msgid "Merge automatically (%{strategy})"
+msgstr ""
+
msgid "Merge commit message"
msgstr ""
@@ -12814,6 +13097,12 @@ msgstr ""
msgid "Merged branches are being deleted. This can take some time depending on the number of branches. Please refresh the page to see changes."
msgstr ""
+msgid "Merged this merge request."
+msgstr ""
+
+msgid "Merges this merge request immediately."
+msgstr ""
+
msgid "Merges this merge request when the pipeline succeeds."
msgstr ""
@@ -12921,12 +13210,15 @@ msgid_plural "Metrics|Edit metrics"
msgstr[0] ""
msgstr[1] ""
-msgid "Metrics|Environment"
+msgid "Metrics|Expand panel"
msgstr ""
msgid "Metrics|For grouping similar metrics"
msgstr ""
+msgid "Metrics|Go back (Esc)"
+msgstr ""
+
msgid "Metrics|Invalid time range, please verify."
msgstr ""
@@ -12957,9 +13249,6 @@ msgstr ""
msgid "Metrics|Refresh dashboard"
msgstr ""
-msgid "Metrics|Show last"
-msgstr ""
-
msgid "Metrics|There was an error creating the dashboard."
msgstr ""
@@ -13038,6 +13327,9 @@ msgstr ""
msgid "Microsoft Azure"
msgstr ""
+msgid "Middleman project with Static Site Editor support"
+msgstr ""
+
msgid "Migrated %{success_count}/%{total_count} files."
msgstr ""
@@ -13483,10 +13775,10 @@ msgstr ""
msgid "Next"
msgstr ""
-msgid "Next file in diff (MRs only)"
+msgid "Next file in diff"
msgstr ""
-msgid "Next unresolved discussion (MRs only)"
+msgid "Next unresolved discussion"
msgstr ""
msgid "Nickname"
@@ -13546,6 +13838,9 @@ msgstr ""
msgid "No changes between %{ref_start}%{source_branch}%{ref_end} and %{ref_start}%{target_branch}%{ref_end}"
msgstr ""
+msgid "No child epics match applied filters"
+msgstr ""
+
msgid "No connection could be made to a Gitaly Server, please check your logs!"
msgstr ""
@@ -13684,15 +13979,6 @@ msgstr ""
msgid "No thanks, don't show this again"
msgstr ""
-msgid "No vulnerabilities found for this group"
-msgstr ""
-
-msgid "No vulnerabilities found for this pipeline"
-msgstr ""
-
-msgid "No vulnerabilities found for this project"
-msgstr ""
-
msgid "No vulnerabilities present"
msgstr ""
@@ -13816,6 +14102,9 @@ msgstr ""
msgid "Nothing to preview."
msgstr ""
+msgid "Nothing to synchronize"
+msgstr ""
+
msgid "Notification events"
msgstr ""
@@ -13987,6 +14276,9 @@ msgstr ""
msgid "Once removed, the fork relationship cannot be restored and you will no longer be able to send merge requests to the source."
msgstr ""
+msgid "Once the exported file is ready you can download it from this page."
+msgstr ""
+
msgid "Once the exported file is ready, you will receive a notification email with a download link, or you can download it from this page."
msgstr ""
@@ -14088,9 +14380,6 @@ msgstr ""
msgid "Open sidebar"
msgstr ""
-msgid "Open source software to collaborate on code"
-msgstr ""
-
msgid "Open: %{openIssuesCount}"
msgstr ""
@@ -14193,6 +14482,9 @@ msgstr ""
msgid "OutdatedBrowser|You can provide feedback %{feedback_link_start}on this issue%{feedback_link_end} or via your usual support channels."
msgstr ""
+msgid "Overridden"
+msgstr ""
+
msgid "Overview"
msgstr ""
@@ -14244,6 +14536,9 @@ msgstr ""
msgid "PackageRegistry|Conan Command"
msgstr ""
+msgid "PackageRegistry|Copy .pypirc content"
+msgstr ""
+
msgid "PackageRegistry|Copy Conan Command"
msgstr ""
@@ -14265,6 +14560,9 @@ msgstr ""
msgid "PackageRegistry|Copy NuGet Setup Command"
msgstr ""
+msgid "PackageRegistry|Copy Pip command"
+msgstr ""
+
msgid "PackageRegistry|Copy and paste this inside your %{codeStart}pom.xml%{codeEnd} %{codeStart}dependencies%{codeEnd} block."
msgstr ""
@@ -14298,6 +14596,12 @@ msgstr ""
msgid "PackageRegistry|For more information on the NuGet registry, %{linkStart}see the documentation%{linkEnd}."
msgstr ""
+msgid "PackageRegistry|For more information on the PyPi registry, %{linkStart}see the documentation%{linkEnd}."
+msgstr ""
+
+msgid "PackageRegistry|If you haven't already done so, you will need to add the below to your %{codeStart}.pypirc%{codeEnd} file."
+msgstr ""
+
msgid "PackageRegistry|If you haven't already done so, you will need to add the below to your %{codeStart}pom.xml%{codeEnd} file."
msgstr ""
@@ -14307,6 +14611,9 @@ msgstr ""
msgid "PackageRegistry|Learn how to %{noPackagesLinkStart}publish and share your packages%{noPackagesLinkEnd} with GitLab."
msgstr ""
+msgid "PackageRegistry|Manually Published"
+msgstr ""
+
msgid "PackageRegistry|Maven"
msgstr ""
@@ -14325,12 +14632,18 @@ msgstr ""
msgid "PackageRegistry|NuGet Command"
msgstr ""
+msgid "PackageRegistry|Pip Command"
+msgstr ""
+
msgid "PackageRegistry|Pipeline %{linkStart}%{linkEnd} triggered %{timestamp} by %{author}"
msgstr ""
msgid "PackageRegistry|Published to the repository at %{timestamp}"
msgstr ""
+msgid "PackageRegistry|PyPi"
+msgstr ""
+
msgid "PackageRegistry|Registry Setup"
msgstr ""
@@ -14367,6 +14680,9 @@ msgstr ""
msgid "PackageRegistry|npm"
msgstr ""
+msgid "PackageRegistry|published by %{author}"
+msgstr ""
+
msgid "PackageRegistry|yarn"
msgstr ""
@@ -14382,6 +14698,9 @@ msgstr ""
msgid "PackageType|NuGet"
msgstr ""
+msgid "PackageType|PyPi"
+msgstr ""
+
msgid "Packages"
msgstr ""
@@ -14475,6 +14794,9 @@ msgstr ""
msgid "Past due"
msgstr ""
+msgid "Paste a machine public key here. Read more about how to generate it"
+msgstr ""
+
msgid "Paste a machine public key here. Read more about how to generate it %{link_start}here%{link_end}"
msgstr ""
@@ -14490,9 +14812,6 @@ msgstr ""
msgid "Path"
msgstr ""
-msgid "Path, transfer, remove"
-msgstr ""
-
msgid "Path:"
msgstr ""
@@ -14523,7 +14842,7 @@ msgstr ""
msgid "Percentage"
msgstr ""
-msgid "Perform advanced options such as changing path, transferring, or removing the group."
+msgid "Perform advanced options such as changing path, transferring, exporting, or removing the group."
msgstr ""
msgid "Perform common operations on GitLab project"
@@ -14604,9 +14923,6 @@ msgstr ""
msgid "Pipeline minutes quota"
msgstr ""
-msgid "Pipeline quota"
-msgstr ""
-
msgid "Pipeline subscriptions"
msgstr ""
@@ -14616,6 +14932,9 @@ msgstr ""
msgid "Pipeline: %{status}"
msgstr ""
+msgid "PipelineCharts|CI / CD Analytics"
+msgstr ""
+
msgid "PipelineCharts|Failed:"
msgstr ""
@@ -14694,15 +15013,6 @@ msgstr ""
msgid "Pipelines settings for '%{project_name}' were successfully updated."
msgstr ""
-msgid "Pipelines| to purchase more minutes."
-msgstr ""
-
-msgid "Pipelines|%{namespace_name} has exceeded its pipeline minutes quota."
-msgstr ""
-
-msgid "Pipelines|%{namespace_name} has less than %{notification_level}%% of CI minutes available."
-msgstr ""
-
msgid "Pipelines|API"
msgstr ""
@@ -14724,10 +15034,13 @@ msgstr ""
msgid "Pipelines|Get started with Pipelines"
msgstr ""
-msgid "Pipelines|Loading Pipelines"
+msgid "Pipelines|Group %{namespace_name} has %{percentage}%% or less Shared Runner Pipeline minutes remaining. Once it runs out, no new jobs or pipelines in its projects will run."
msgstr ""
-msgid "Pipelines|Pipelines will not run anymore on shared Runners."
+msgid "Pipelines|Group %{namespace_name} has exceeded its pipeline minutes quota. Unless you buy additional pipeline minutes, no new jobs or pipelines in its projects will run."
+msgstr ""
+
+msgid "Pipelines|Loading Pipelines"
msgstr ""
msgid "Pipelines|Project cache successfully reset."
@@ -14958,6 +15271,9 @@ msgstr ""
msgid "Please select"
msgstr ""
+msgid "Please select a Jira project"
+msgstr ""
+
msgid "Please select a country"
msgstr ""
@@ -15003,6 +15319,9 @@ msgstr ""
msgid "Please wait while we import the repository for you. Refresh at will."
msgstr ""
+msgid "Plugins directory is deprecated and will be removed in 14.0. Please move this file into /file_hooks directory."
+msgstr ""
+
msgid "Pod does not exist"
msgstr ""
@@ -15120,7 +15439,7 @@ msgstr ""
msgid "Prevent users from changing their profile name"
msgstr ""
-msgid "Prevent users from modifing merge request approvers list"
+msgid "Prevent users from modifying merge request approvers list"
msgstr ""
msgid "Prevent users from performing write operations on GitLab while performing maintenance."
@@ -15141,10 +15460,10 @@ msgstr ""
msgid "Previous Artifacts"
msgstr ""
-msgid "Previous file in diff (MRs only)"
+msgid "Previous file in diff"
msgstr ""
-msgid "Previous unresolved discussion (MRs only)"
+msgid "Previous unresolved discussion"
msgstr ""
msgid "Primary"
@@ -15177,6 +15496,9 @@ msgstr ""
msgid "Private profile"
msgstr ""
+msgid "Private projects Minutes cost factor"
+msgstr ""
+
msgid "Private projects can be created in your personal namespace with:"
msgstr ""
@@ -16005,6 +16327,9 @@ msgstr ""
msgid "ProjectSettings|With GitLab Pages you can host your static websites on GitLab"
msgstr ""
+msgid "ProjectSettings|With Metrics Dashboard you can visualize this project performance metrics"
+msgstr ""
+
msgid "ProjectTemplates|.NET Core"
msgstr ""
@@ -16068,6 +16393,9 @@ msgstr ""
msgid "ProjectTemplates|Spring"
msgstr ""
+msgid "ProjectTemplates|Static Site Editor/Middleman"
+msgstr ""
+
msgid "ProjectTemplates|iOS (Swift)"
msgstr ""
@@ -16287,13 +16615,13 @@ msgstr ""
msgid "PrometheusService|Prometheus is being automatically managed on your clusters"
msgstr ""
-msgid "PrometheusService|These metrics will only be monitored after your first deployment to an environment"
+msgid "PrometheusService|Select the Active checkbox to override the Auto Configuration with custom settings. If unchecked, Auto Configuration settings are used."
msgstr ""
-msgid "PrometheusService|Time-series monitoring service"
+msgid "PrometheusService|These metrics will only be monitored after your first deployment to an environment"
msgstr ""
-msgid "PrometheusService|To enable manual configuration, uninstall Prometheus from your clusters"
+msgid "PrometheusService|Time-series monitoring service"
msgstr ""
msgid "PrometheusService|To enable the installation of Prometheus on your clusters, deactivate the manual configuration below"
@@ -16479,6 +16807,9 @@ msgstr ""
msgid "Protip:"
msgstr ""
+msgid "Protocol"
+msgstr ""
+
msgid "Provider"
msgstr ""
@@ -16503,6 +16834,9 @@ msgstr ""
msgid "Public pipelines"
msgstr ""
+msgid "Public projects Minutes cost factor"
+msgstr ""
+
msgid "Pull"
msgstr ""
@@ -16703,6 +17037,9 @@ msgid_plural "Refreshing in %d seconds to show the updated status..."
msgstr[0] ""
msgstr[1] ""
+msgid "Regenerate export"
+msgstr ""
+
msgid "Regenerate instance ID"
msgstr ""
@@ -17080,9 +17417,29 @@ msgstr ""
msgid "Reports|%{combinedString} and %{resolvedString}"
msgstr ""
+msgid "Reports|Accessibility scanning detected %d issue for the source branch only"
+msgid_plural "Reports|Accessibility scanning detected %d issues for the source branch only"
+msgstr[0] ""
+msgstr[1] ""
+
+msgid "Reports|Accessibility scanning detected no issues for the source branch only"
+msgstr ""
+
+msgid "Reports|Accessibility scanning failed loading results"
+msgstr ""
+
+msgid "Reports|Accessibility scanning results are being parsed"
+msgstr ""
+
msgid "Reports|Actions"
msgstr ""
+msgid "Reports|An error occured while loading report"
+msgstr ""
+
+msgid "Reports|An error occurred while loading %{name} results"
+msgstr ""
+
msgid "Reports|Class"
msgstr ""
@@ -17188,7 +17545,7 @@ msgstr ""
msgid "Requested %{time_ago}"
msgstr ""
-msgid "Requested design version does not exist"
+msgid "Requested design version does not exist."
msgstr ""
msgid "Requested states are invalid"
@@ -17372,6 +17729,9 @@ msgstr ""
msgid "Resync"
msgstr ""
+msgid "Resync all"
+msgstr ""
+
msgid "Resync all %{replicableType}"
msgstr ""
@@ -17440,6 +17800,9 @@ msgstr ""
msgid "Revoked personal access token %{personal_access_token_name}!"
msgstr ""
+msgid "Revoked project access token %{project_access_token_name}!"
+msgstr ""
+
msgid "RightSidebar|adding a"
msgstr ""
@@ -17488,6 +17851,9 @@ msgstr ""
msgid "Runner token"
msgstr ""
+msgid "Runner tokens"
+msgstr ""
+
msgid "Runner was not updated."
msgstr ""
@@ -17542,6 +17908,9 @@ msgstr ""
msgid "SAML SSO for %{group_name}"
msgstr ""
+msgid "SAML discovery tokens"
+msgstr ""
+
msgid "SAML for %{group_name}"
msgstr ""
@@ -17626,12 +17995,18 @@ msgstr ""
msgid "Scheduled"
msgstr ""
+msgid "Scheduled to merge this merge request (%{strategy})."
+msgstr ""
+
msgid "Scheduled to merge this merge request when the pipeline succeeds."
msgstr ""
msgid "Schedules"
msgstr ""
+msgid "Schedules to merge this merge request (%{strategy})."
+msgstr ""
+
msgid "Scheduling"
msgstr ""
@@ -17689,6 +18064,9 @@ msgstr ""
msgid "Search branches and tags"
msgstr ""
+msgid "Search by author"
+msgstr ""
+
msgid "Search files"
msgstr ""
@@ -17834,11 +18212,6 @@ msgid_plural "SearchResults|snippets"
msgstr[0] ""
msgstr[1] ""
-msgid "SearchResults|snippet result"
-msgid_plural "SearchResults|snippet results"
-msgstr[0] ""
-msgstr[1] ""
-
msgid "SearchResults|user"
msgid_plural "SearchResults|users"
msgstr[0] ""
@@ -17849,6 +18222,9 @@ msgid_plural "SearchResults|wiki results"
msgstr[0] ""
msgstr[1] ""
+msgid "Searching by both author and message is currently not supported."
+msgstr ""
+
msgid "Seat Link"
msgstr ""
@@ -17876,184 +18252,232 @@ msgstr ""
msgid "Security Dashboard"
msgstr ""
-msgid "Security Dashboard|Error fetching the vulnerability counts. Please check your network connection and try again."
+msgid "Security configuration help link"
+msgstr ""
+
+msgid "Security dashboard"
msgstr ""
-msgid "Security Dashboard|Error fetching the vulnerability list. Please check your network connection and try again."
+msgid "Security report is out of date. Please update your branch with the latest changes from the target branch (%{targetBranchName})"
msgstr ""
-msgid "Security Dashboard|Issue Created"
+msgid "Security report is out of date. Run %{newPipelineLinkStart}a new pipeline%{newPipelineLinkEnd} for the target branch (%{targetBranchName})"
msgstr ""
-msgid "Security Reports|Comment added to '%{vulnerabilityName}'"
+msgid "SecurityConfiguration|Configured"
msgstr ""
-msgid "Security Reports|Comment deleted on '%{vulnerabilityName}'"
+msgid "SecurityConfiguration|Feature"
msgstr ""
-msgid "Security Reports|Comment edited on '%{vulnerabilityName}'"
+msgid "SecurityConfiguration|Feature documentation for %{featureName}"
msgstr ""
-msgid "Security Reports|Create issue"
+msgid "SecurityConfiguration|Not yet configured"
msgstr ""
-msgid "Security Reports|Dismiss vulnerability"
+msgid "SecurityConfiguration|Secure features"
msgstr ""
-msgid "Security Reports|Dismissed '%{vulnerabilityName}'"
+msgid "SecurityConfiguration|Status"
msgstr ""
-msgid "Security Reports|Dismissed '%{vulnerabilityName}'. Turn off the hide dismissed toggle to view."
+msgid "SecurityReports|%{firstProject} and %{secondProject}"
msgstr ""
-msgid "Security Reports|Either you don't have permission to view this dashboard or the dashboard has not been setup. Please check your permission settings with your administrator or check your dashboard configurations to proceed."
+msgid "SecurityReports|%{firstProject}, %{secondProject}, and %{rest}"
msgstr ""
-msgid "Security Reports|Learn more about setting up your dashboard"
+msgid "SecurityReports|Add a project to your dashboard"
msgstr ""
-msgid "Security Reports|More info"
+msgid "SecurityReports|Add or remove projects from your dashboard"
msgstr ""
-msgid "Security Reports|Oops, something doesn't seem right."
+msgid "SecurityReports|Add projects"
msgstr ""
-msgid "Security Reports|Security reports can only be accessed by authorized users."
+msgid "SecurityReports|Comment added to '%{vulnerabilityName}'"
msgstr ""
-msgid "Security Reports|There was an error adding the comment."
+msgid "SecurityReports|Comment deleted on '%{vulnerabilityName}'"
msgstr ""
-msgid "Security Reports|There was an error creating the issue."
+msgid "SecurityReports|Comment edited on '%{vulnerabilityName}'"
msgstr ""
-msgid "Security Reports|There was an error creating the merge request."
+msgid "SecurityReports|Create issue"
msgstr ""
-msgid "Security Reports|There was an error deleting the comment."
+msgid "SecurityReports|Dismiss Selected"
msgstr ""
-msgid "Security Reports|There was an error dismissing the vulnerabilities."
+msgid "SecurityReports|Dismiss vulnerability"
msgstr ""
-msgid "Security Reports|There was an error dismissing the vulnerability."
+msgid "SecurityReports|Dismissed '%{vulnerabilityName}'"
msgstr ""
-msgid "Security Reports|There was an error reverting the dismissal."
+msgid "SecurityReports|Dismissed '%{vulnerabilityName}'. Turn off the hide dismissed toggle to view."
msgstr ""
-msgid "Security Reports|There was an error reverting this dismissal."
+msgid "SecurityReports|Each vulnerability now has a unique page that can be directly linked to, shared, referenced, and tracked as the single source of truth. Vulnerability occurrences also persist across scanner runs, which improves tracking and visibility and reduces duplicates between scans."
msgstr ""
-msgid "Security Reports|Undo dismiss"
+msgid "SecurityReports|Edit dashboard"
msgstr ""
-msgid "Security Reports|You do not have sufficient permissions to access this report"
+msgid "SecurityReports|Either you don't have permission to view this dashboard or the dashboard has not been setup. Please check your permission settings with your administrator or check your dashboard configurations to proceed."
msgstr ""
-msgid "Security Reports|You must sign in as an authorized user to see this report"
+msgid "SecurityReports|Error fetching the vulnerability counts. Please check your network connection and try again."
msgstr ""
-msgid "Security configuration help link"
+msgid "SecurityReports|Error fetching the vulnerability list. Please check your network connection and try again."
msgstr ""
-msgid "Security dashboard"
+msgid "SecurityReports|False positive"
msgstr ""
-msgid "Security report is out of date. Please update your branch with the latest changes from the target branch (%{targetBranchName})"
+msgid "SecurityReports|Hide dismissed"
msgstr ""
-msgid "Security report is out of date. Run %{newPipelineLinkStart}a new pipeline%{newPipelineLinkEnd} for the target branch (%{targetBranchName})"
+msgid "SecurityReports|Introducing standalone vulnerabilities"
msgstr ""
-msgid "SecurityConfiguration|Configured"
+msgid "SecurityReports|Issue Created"
msgstr ""
-msgid "SecurityConfiguration|Feature"
+msgid "SecurityReports|Learn More"
msgstr ""
-msgid "SecurityConfiguration|Feature documentation for %{featureName}"
+msgid "SecurityReports|Learn more about setting up your dashboard"
msgstr ""
-msgid "SecurityConfiguration|Not yet configured"
+msgid "SecurityReports|Load more vulnerabilities"
msgstr ""
-msgid "SecurityConfiguration|Secure features"
+msgid "SecurityReports|Monitor vulnerabilities in your code"
msgstr ""
-msgid "SecurityConfiguration|Status"
+msgid "SecurityReports|More info"
msgstr ""
-msgid "SecurityDashboard|%{firstProject} and %{secondProject}"
+msgid "SecurityReports|More information"
msgstr ""
-msgid "SecurityDashboard|%{firstProject}, %{secondProject}, and %{rest}"
+msgid "SecurityReports|No vulnerabilities found for dashboard"
msgstr ""
-msgid "SecurityDashboard|Add a project to your dashboard"
+msgid "SecurityReports|No vulnerabilities found for this group"
msgstr ""
-msgid "SecurityDashboard|Add or remove projects from your dashboard"
+msgid "SecurityReports|No vulnerabilities found for this pipeline"
msgstr ""
-msgid "SecurityDashboard|Add projects"
+msgid "SecurityReports|No vulnerabilities found for this project"
msgstr ""
-msgid "SecurityDashboard|Edit dashboard"
+msgid "SecurityReports|Oops, something doesn't seem right."
msgstr ""
-msgid "SecurityDashboard|Hide dismissed"
+msgid "SecurityReports|Pipeline %{pipelineLink} triggered %{timeago} by %{user}"
msgstr ""
-msgid "SecurityDashboard|Monitor vulnerabilities in your code"
+msgid "SecurityReports|Project"
msgstr ""
-msgid "SecurityDashboard|More information"
+msgid "SecurityReports|Projects added"
msgstr ""
-msgid "SecurityDashboard|No vulnerabilities found for dashboard"
+msgid "SecurityReports|Remove project from dashboard"
msgstr ""
-msgid "SecurityDashboard|Pipeline %{pipelineLink} triggered %{timeago} by %{user}"
+msgid "SecurityReports|Report type"
msgstr ""
-msgid "SecurityDashboard|Project"
+msgid "SecurityReports|Return to dashboard"
msgstr ""
-msgid "SecurityDashboard|Projects added"
+msgid "SecurityReports|Security Dashboard"
msgstr ""
-msgid "SecurityDashboard|Remove project from dashboard"
+msgid "SecurityReports|Security reports can only be accessed by authorized users."
msgstr ""
-msgid "SecurityDashboard|Report type"
+msgid "SecurityReports|Select a project to add by using the project search field above."
msgstr ""
-msgid "SecurityDashboard|Return to dashboard"
+msgid "SecurityReports|Select a reason"
msgstr ""
-msgid "SecurityDashboard|Security Dashboard"
+msgid "SecurityReports|Severity"
msgstr ""
-msgid "SecurityDashboard|Select a project to add by using the project search field above."
+msgid "SecurityReports|Status"
msgstr ""
-msgid "SecurityDashboard|Severity"
+msgid "SecurityReports|The security dashboard displays the latest security findings for projects you wish to monitor. Select \"Edit dashboard\" to add and remove projects."
msgstr ""
-msgid "SecurityDashboard|Status"
+msgid "SecurityReports|The security dashboard displays the latest security report. Use it to find and fix vulnerabilities."
msgstr ""
-msgid "SecurityDashboard|The security dashboard displays the latest security findings for projects you wish to monitor. Select \"Edit dashboard\" to add and remove projects."
+msgid "SecurityReports|There was an error adding the comment."
msgstr ""
-msgid "SecurityDashboard|The security dashboard displays the latest security report. Use it to find and fix vulnerabilities."
+msgid "SecurityReports|There was an error creating the issue."
msgstr ""
-msgid "SecurityDashboard|There was an error while generating the report."
+msgid "SecurityReports|There was an error creating the merge request."
msgstr ""
-msgid "SecurityDashboard|Unable to add %{invalidProjects}"
+msgid "SecurityReports|There was an error deleting the comment."
+msgstr ""
+
+msgid "SecurityReports|There was an error dismissing the vulnerabilities."
+msgstr ""
+
+msgid "SecurityReports|There was an error dismissing the vulnerability."
+msgstr ""
+
+msgid "SecurityReports|There was an error reverting the dismissal."
+msgstr ""
+
+msgid "SecurityReports|There was an error reverting this dismissal."
+msgstr ""
+
+msgid "SecurityReports|There was an error while generating the report."
+msgstr ""
+
+msgid "SecurityReports|Unable to add %{invalidProjects}"
+msgstr ""
+
+msgid "SecurityReports|Undo dismiss"
+msgstr ""
+
+msgid "SecurityReports|While it's rare to have no vulnerabilities for your group, it can happen. In any event, we ask that you double check your settings to make sure you've set up your dashboard correctly."
+msgstr ""
+
+msgid "SecurityReports|While it's rare to have no vulnerabilities for your pipeline, it can happen. In any event, we ask that you double check your settings to make sure all security scanning jobs have passed successfully."
+msgstr ""
+
+msgid "SecurityReports|While it's rare to have no vulnerabilities for your project, it can happen. In any event, we ask that you double check your settings to make sure you've set up your dashboard correctly."
+msgstr ""
+
+msgid "SecurityReports|While it's rare to have no vulnerabilities, it can happen. In any event, we ask that you please double check your settings to make sure you've set up your dashboard correctly."
+msgstr ""
+
+msgid "SecurityReports|Won't fix / Accept risk"
+msgstr ""
+
+msgid "SecurityReports|You do not have sufficient permissions to access this report"
+msgstr ""
+
+msgid "SecurityReports|You must sign in as an authorized user to see this report"
+msgstr ""
+
+msgid "SecurityReports|[No reason]"
msgstr ""
msgid "See GitLab's %{password_policy_guidelines}"
@@ -18065,6 +18489,9 @@ msgstr ""
msgid "See the affected projects in the GitLab admin panel"
msgstr ""
+msgid "See what's new at GitLab"
+msgstr ""
+
msgid "Select"
msgstr ""
@@ -18470,9 +18897,6 @@ msgstr ""
msgid "Set up Jira Integration"
msgstr ""
-msgid "Set up Jira Integration illustration"
-msgstr ""
-
msgid "Set up a %{type} Runner automatically"
msgstr ""
@@ -18557,6 +18981,9 @@ msgstr ""
msgid "Severity: %{severity}"
msgstr ""
+msgid "Shards (%{shards})"
+msgstr ""
+
msgid "Shards to synchronize"
msgstr ""
@@ -18871,6 +19298,9 @@ msgstr ""
msgid "Snowplow"
msgstr ""
+msgid "Some child epics may be hidden due to applied filters"
+msgstr ""
+
msgid "Some email servers do not support overriding the email sender name. Enable this option to include the name of the author of the issue, merge request or comment in the email body instead."
msgstr ""
@@ -19012,6 +19442,9 @@ msgstr ""
msgid "Something went wrong while updating your list settings"
msgstr ""
+msgid "Something went wrong with your automatic subscription renewal"
+msgstr ""
+
msgid "Something went wrong, unable to add %{project} to dashboard"
msgstr ""
@@ -19297,6 +19730,9 @@ msgstr ""
msgid "Stage removed"
msgstr ""
+msgid "Standard"
+msgstr ""
+
msgid "Star a label to make it a priority label. Order the prioritized labels to change their relative priority, by dragging."
msgstr ""
@@ -19420,6 +19856,9 @@ msgstr ""
msgid "Static Application Security Testing (SAST)"
msgstr ""
+msgid "StaticSiteEditor|An error occurred while submitting your changes."
+msgstr ""
+
msgid "StaticSiteEditor|Branch could not be created."
msgstr ""
@@ -19429,18 +19868,30 @@ msgstr ""
msgid "StaticSiteEditor|Could not create merge request."
msgstr ""
+msgid "StaticSiteEditor|Incompatible file content"
+msgstr ""
+
msgid "StaticSiteEditor|Return to site"
msgstr ""
+msgid "StaticSiteEditor|Static site editor"
+msgstr ""
+
msgid "StaticSiteEditor|Success!"
msgstr ""
msgid "StaticSiteEditor|Summary of changes"
msgstr ""
+msgid "StaticSiteEditor|The Static Site Editor is currently configured to only edit Markdown content on pages generated from Middleman. Visit the documentation to learn more about configuring your site to use the Static Site Editor."
+msgstr ""
+
msgid "StaticSiteEditor|Update %{sourcePath} file"
msgstr ""
+msgid "StaticSiteEditor|View documentation"
+msgstr ""
+
msgid "StaticSiteEditor|View merge request"
msgstr ""
@@ -19498,6 +19949,12 @@ msgstr ""
msgid "StatusPage|Status page"
msgstr ""
+msgid "StatusPage|Status page URL"
+msgstr ""
+
+msgid "StatusPage|Status page frontend documentation"
+msgstr ""
+
msgid "StatusPage|To publish incidents to an external status page, GitLab will store a JSON file in your Amazon S3 account in a location accessible to your external status page service. Make sure to also set up %{docsLink}"
msgstr ""
@@ -19846,6 +20303,12 @@ msgstr ""
msgid "Synced"
msgstr ""
+msgid "Synchronization disabled"
+msgstr ""
+
+msgid "Synchronization of container repositories is disabled."
+msgstr ""
+
msgid "System"
msgstr ""
@@ -19999,6 +20462,9 @@ msgstr ""
msgid "Telephone number"
msgstr ""
+msgid "Telephone number (Optional)"
+msgstr ""
+
msgid "Template"
msgstr ""
@@ -20305,6 +20771,9 @@ msgstr ""
msgid "The license was successfully uploaded and is now active. You can see the details below."
msgstr ""
+msgid "The license was successfully uploaded and will be active from %{starts_at}. You can see the details below."
+msgstr ""
+
msgid "The maximum file size allowed is %{size}."
msgstr ""
@@ -20626,6 +21095,9 @@ msgstr ""
msgid "There was an error getting the epic participants."
msgstr ""
+msgid "There was an error importing the Jira project."
+msgstr ""
+
msgid "There was an error loading users activity calendar."
msgstr ""
@@ -20641,7 +21113,7 @@ msgstr ""
msgid "There was an error resetting user pipeline minutes."
msgstr ""
-msgid "There was an error saving this Geo Node"
+msgid "There was an error saving this Geo Node."
msgstr ""
msgid "There was an error saving your changes."
@@ -20704,6 +21176,9 @@ msgstr ""
msgid "They can be managed using the %{link}."
msgstr ""
+msgid "Third Party Advisory Link"
+msgstr ""
+
msgid "Third party offers"
msgstr ""
@@ -20740,6 +21215,9 @@ msgstr ""
msgid "This application will be able to:"
msgstr ""
+msgid "This attachment has been truncated to avoid exceeding the maximum allowed attachment size of 15MB. %{written_count} of %{issues_count} issues have been included. Consider re-exporting with a narrower selection of issues."
+msgstr ""
+
msgid "This block is self-referential"
msgstr ""
@@ -20881,6 +21359,12 @@ msgstr ""
msgid "This issue is confidential"
msgstr ""
+msgid "This issue is currently blocked by the following issues: %{issues}."
+msgstr ""
+
+msgid "This issue is in a child epic of the filtered epic"
+msgstr ""
+
msgid "This issue is locked."
msgstr ""
@@ -21079,7 +21563,10 @@ msgstr ""
msgid "This user will be the author of all events in the activity feed that are the result of an update, like new branches being created or new commits being pushed to existing branches. Upon creation or when reassigning you can only assign yourself to be the mirror user."
msgstr ""
-msgid "This variable can not be masked"
+msgid "This variable can not be masked."
+msgstr ""
+
+msgid "This variable does not match the expected pattern."
msgstr ""
msgid "This will help us personalize your onboarding experience."
@@ -21109,10 +21596,10 @@ msgstr ""
msgid "ThreatMonitoring|Application firewall not detected"
msgstr ""
-msgid "ThreatMonitoring|Container Network Policy"
+msgid "ThreatMonitoring|Container Network Policies are not installed or have been disabled. To view this data, ensure your Network Policies are installed and enabled for your cluster."
msgstr ""
-msgid "ThreatMonitoring|Container NetworkPolicies are not installed or has been disabled. To view this data, ensure you NetworkPolicies are installed and enabled for your cluster."
+msgid "ThreatMonitoring|Container Network Policy"
msgstr ""
msgid "ThreatMonitoring|Container NetworkPolicies not detected"
@@ -21145,7 +21632,7 @@ msgstr ""
msgid "ThreatMonitoring|Something went wrong, unable to fetch statistics"
msgstr ""
-msgid "ThreatMonitoring|The firewall is not installed or has been disabled. To view this data, ensure you firewall is installed and enabled for your cluster."
+msgid "ThreatMonitoring|The firewall is not installed or has been disabled. To view this data, ensure the web application firewall is installed and enabled for your cluster."
msgstr ""
msgid "ThreatMonitoring|The graph below is an overview of traffic coming to your application as tracked by the Web Application Firewall (WAF). View the docs for instructions on how to access the WAF logs to see what type of malicious traffic is trying to access your app. The docs link is also accessible by clicking the \"?\" icon next to the title below."
@@ -21214,6 +21701,9 @@ msgstr ""
msgid "Time in seconds GitLab will wait for a response from the external service. When the service does not respond in time, access will be denied."
msgstr ""
+msgid "Time of import: %{importTime}"
+msgstr ""
+
msgid "Time remaining"
msgstr ""
@@ -21398,7 +21888,7 @@ msgstr ""
msgid "Title:"
msgstr ""
-msgid "Titles and Filenames"
+msgid "Titles and Descriptions"
msgstr ""
msgid "To"
@@ -21605,7 +22095,7 @@ msgstr ""
msgid "Too many projects enabled. You will need to manage them via the console or the API."
msgstr ""
-msgid "Topics"
+msgid "Topics (optional)"
msgstr ""
msgid "Total"
@@ -21767,6 +22257,9 @@ msgstr ""
msgid "Try all GitLab has to offer for 30 days."
msgstr ""
+msgid "Try changing or removing filters."
+msgstr ""
+
msgid "Try to fork again"
msgstr ""
@@ -21860,6 +22353,9 @@ msgstr ""
msgid "Unable to connect to server: %{error}"
msgstr ""
+msgid "Unable to connect to the Jira instance. Please check your Jira integration configuration."
+msgstr ""
+
msgid "Unable to convert Kubernetes logs encoding to UTF-8"
msgstr ""
@@ -22803,6 +23299,9 @@ msgstr ""
msgid "View issue"
msgstr ""
+msgid "View issues"
+msgstr ""
+
msgid "View it on GitLab"
msgstr ""
@@ -22980,6 +23479,15 @@ msgstr ""
msgid "VulnerabilityManagement|Resolved %{timeago} by %{user}"
msgstr ""
+msgid "VulnerabilityManagement|Something went wrong while trying to delete the comment. Please try again later."
+msgstr ""
+
+msgid "VulnerabilityManagement|Something went wrong while trying to retrieve the vulnerability history. Please try again later."
+msgstr ""
+
+msgid "VulnerabilityManagement|Something went wrong while trying to save the comment. Please try again later."
+msgstr ""
+
msgid "VulnerabilityManagement|Something went wrong, could not create an issue."
msgstr ""
@@ -23010,6 +23518,9 @@ msgstr ""
msgid "VulnerabilityStatusTypes|Resolved"
msgstr ""
+msgid "Vulnerability|%{scannerName} (version %{scannerVersion})"
+msgstr ""
+
msgid "Vulnerability|Class"
msgstr ""
@@ -23043,7 +23554,10 @@ msgstr ""
msgid "Vulnerability|Project"
msgstr ""
-msgid "Vulnerability|Report Type"
+msgid "Vulnerability|Scanner Provider"
+msgstr ""
+
+msgid "Vulnerability|Scanner Type"
msgstr ""
msgid "Vulnerability|Severity"
@@ -23097,9 +23611,15 @@ msgstr ""
msgid "We sent you an email with reset password instructions"
msgstr ""
+msgid "We tried to automatically renew your %{strong}%{plan_name}%{strong_close} subscription for %{strong}%{namespace_name}%{strong_close} on %{expires_on} but something went wrong so your subscription was downgraded to the free plan. Don't worry, your data is safe. We suggest you check your payment method and get in touch with our support team (%{support_link}). They'll gladly help with your subscription renewal."
+msgstr ""
+
msgid "We want to be sure it is you, please confirm you are not a robot."
msgstr ""
+msgid "We will automatically renew your %{strong}%{plan_name}%{strong_close} subscription for %{strong}%{namespace_name}%{strong_close} on %{strong}%{expires_on}%{strong_close}. There's nothing that you need to do, we'll let you know when the renewal is complete. Need more seats, a higher plan or just want to review your payment method?"
+msgstr ""
+
msgid "We've found no vulnerabilities"
msgstr ""
@@ -23204,15 +23724,6 @@ msgstr ""
msgid "When:"
msgstr ""
-msgid "While it's rare to have no vulnerabilities for your group, it can happen. In any event, we ask that you double check your settings to make sure you've set up your dashboard correctly."
-msgstr ""
-
-msgid "While it's rare to have no vulnerabilities for your pipeline, it can happen. In any event, we ask that you double check your settings to make sure all security scanning jobs have passed successfully."
-msgstr ""
-
-msgid "While it's rare to have no vulnerabilities for your project, it can happen. In any event, we ask that you double check your settings to make sure you've set up your dashboard correctly."
-msgstr ""
-
msgid "While it's rare to have no vulnerabilities, it can happen. In any event, we ask that you please double check your settings to make sure you've set up your dashboard correctly."
msgstr ""
@@ -23243,9 +23754,6 @@ msgstr ""
msgid "Wiki"
msgstr ""
-msgid "Wiki pages"
-msgstr ""
-
msgid "Wiki was successfully updated."
msgstr ""
@@ -23429,6 +23937,9 @@ msgstr ""
msgid "Yes, add it"
msgstr ""
+msgid "Yes, close issue"
+msgstr ""
+
msgid "Yes, let me map Google Code users to full names or GitLab users."
msgstr ""
@@ -23624,6 +24135,9 @@ msgstr ""
msgid "You could not create a new trigger."
msgstr ""
+msgid "You didn't renew your %{strong}%{plan_name}%{strong_close} subscription for %{strong}%{namespace_name}%{strong_close} so it was downgraded to the free plan."
+msgstr ""
+
msgid "You didn't renew your %{strong}%{plan_name}%{strong_close} subscription so it was downgraded to the GitLab Core Plan."
msgstr ""
@@ -23690,6 +24204,9 @@ msgstr ""
msgid "You have declined the invitation to join %{label}."
msgstr ""
+msgid "You have imported from this project %{numberOfPreviousImportsForProject} times before. Each new import will create duplicate issues."
+msgstr ""
+
msgid "You have no permissions"
msgstr ""
@@ -23771,6 +24288,9 @@ msgstr ""
msgid "You need to upload a Google Takeout archive."
msgstr ""
+msgid "You reached %{usage_in_percent} of %{namespace_name}'s capacity (%{used_storage} of %{storage_limit})"
+msgstr ""
+
msgid "You tried to fork %{link_to_the_project} but it failed for the following reason:"
msgstr ""
@@ -23855,9 +24375,21 @@ msgstr ""
msgid "YouTube"
msgstr ""
+msgid "Your %{strong}%{plan_name}%{strong_close} subscription for %{strong}%{namespace_name}%{strong_close} will expire on %{strong}%{expires_on}%{strong_close}. After that, you will not to be able to create issues or merge requests as well as many other features."
+msgstr ""
+
msgid "Your %{strong}%{plan_name}%{strong_close} subscription will expire on %{strong}%{expires_on}%{strong_close}. After that, you will not to be able to create issues or merge requests as well as many other features."
msgstr ""
+msgid "Your CSV export has started. It will be emailed to %{email} when complete."
+msgstr ""
+
+msgid "Your CSV export of %{issues_count} from project %{project_link} has been added to this email as an attachment."
+msgstr ""
+
+msgid "Your CSV export of %{written_count} from project %{project_name} (%{project_url}) has been added to this email as an attachment."
+msgstr ""
+
msgid "Your Commit Email will be used for web based operations, such as edits and merges."
msgstr ""
@@ -23954,6 +24486,9 @@ msgstr ""
msgid "Your comment could not be updated! Please check your network connection and try again."
msgstr ""
+msgid "Your comment will be discarded."
+msgstr ""
+
msgid "Your custom stage '%{title}' was created"
msgstr ""
@@ -23993,6 +24528,9 @@ msgstr ""
msgid "Your new personal access token has been created."
msgstr ""
+msgid "Your new project access token has been created."
+msgstr ""
+
msgid "Your password isn't required to view this page. If a password or any other personal details are requested, please contact your administrator to report abuse."
msgstr ""
@@ -24011,12 +24549,18 @@ msgstr ""
msgid "Your request for access has been queued for review."
msgstr ""
+msgid "Your search didn't match any commits."
+msgstr ""
+
msgid "Your subscription expired!"
msgstr ""
msgid "Your subscription has been downgraded"
msgstr ""
+msgid "Your subscription will automatically renew in %{remaining_days}"
+msgstr ""
+
msgid "Your subscription will expire in %{remaining_days}"
msgstr ""
@@ -24055,7 +24599,7 @@ msgstr ""
msgid "allowed to fail"
msgstr ""
-msgid "already being used for another group or project milestone."
+msgid "already being used for another group or project %{timebox_name}."
msgstr ""
msgid "already has a \"created\" issue link"
@@ -24133,6 +24677,9 @@ msgstr ""
msgid "ciReport|%{linkStartTag}Learn more about SAST %{linkEndTag}"
msgstr ""
+msgid "ciReport|%{linkStartTag}Learn more about Secret Scanning %{linkEndTag}"
+msgstr ""
+
msgid "ciReport|%{linkStartTag}Learn more about codequality reports %{linkEndTag}"
msgstr ""
@@ -24296,6 +24843,12 @@ msgstr ""
msgid "ciReport|SAST"
msgstr ""
+msgid "ciReport|Secret scanning"
+msgstr ""
+
+msgid "ciReport|Secret scanning detects secrets and credentials vulnerabilities in your source code."
+msgstr ""
+
msgid "ciReport|Security scanning"
msgstr ""
@@ -24493,6 +25046,9 @@ msgstr ""
msgid "group"
msgstr ""
+msgid "groups"
+msgstr ""
+
msgid "has already been linked to another vulnerability"
msgstr ""
@@ -24656,9 +25212,6 @@ msgstr[1] ""
msgid "merged %{time_ago}"
msgstr ""
-msgid "milestone should belong either to a project or a group."
-msgstr ""
-
msgid "missing"
msgstr ""
@@ -24995,6 +25548,9 @@ msgstr ""
msgid "no contributions"
msgstr ""
+msgid "no expiration"
+msgstr ""
+
msgid "no one can merge"
msgstr ""
@@ -25047,6 +25603,9 @@ msgstr ""
msgid "pending removal"
msgstr ""
+msgid "per day"
+msgstr ""
+
msgid "pipeline"
msgstr ""
@@ -25075,6 +25634,12 @@ msgstr[1] ""
msgid "project avatar"
msgstr ""
+msgid "projects"
+msgstr ""
+
+msgid "push to your repository, create pipelines, create issues or add comments. To reduce storage capacity, delete unused repositories, artifacts, wikis, issues, and pipelines."
+msgstr ""
+
msgid "quick actions"
msgstr ""
@@ -25221,11 +25786,6 @@ msgstr ""
msgid "this document"
msgstr ""
-msgid "thread resolved"
-msgid_plural "threads resolved"
-msgstr[0] ""
-msgstr[1] ""
-
msgid "to help your contributors communicate effectively!"
msgstr ""
@@ -25280,6 +25840,9 @@ msgstr ""
msgid "view the blob"
msgstr ""
+msgid "vulnerability|Add a comment"
+msgstr ""
+
msgid "vulnerability|Add a comment or reason for dismissal"
msgstr ""
diff --git a/locale/sr_CS/gitlab.po b/locale/sr_CS/gitlab.po
index faca2e9cd51..b50e11c7485 100644
--- a/locale/sr_CS/gitlab.po
+++ b/locale/sr_CS/gitlab.po
@@ -12,7 +12,7 @@ msgstr ""
"X-Crowdin-Project: gitlab-ee\n"
"X-Crowdin-Language: sr-CS\n"
"X-Crowdin-File: /master/locale/gitlab.pot\n"
-"PO-Revision-Date: 2020-04-15 00:27\n"
+"PO-Revision-Date: 2020-05-05 21:34\n"
msgid " %{start} to %{end}"
msgstr ""
@@ -242,6 +242,12 @@ msgstr[0] ""
msgstr[1] ""
msgstr[2] ""
+msgid "%d unresolved thread"
+msgid_plural "%d unresolved threads"
+msgstr[0] ""
+msgstr[1] ""
+msgstr[2] ""
+
msgid "%d vulnerability dismissed"
msgid_plural "%d vulnerabilities dismissed"
msgstr[0] ""
@@ -407,6 +413,9 @@ msgstr ""
msgid "%{mrText}, this issue will be closed automatically."
msgstr ""
+msgid "%{namespace_name} is now read-only. You cannot: %{base_message}"
+msgstr ""
+
msgid "%{name} contained %{resultsString}"
msgstr ""
@@ -419,6 +428,12 @@ msgstr ""
msgid "%{name}'s avatar"
msgstr ""
+msgid "%{no_of_days} day"
+msgid_plural "%{no_of_days} days"
+msgstr[0] ""
+msgstr[1] ""
+msgstr[2] ""
+
msgid "%{number_commits_behind} commits behind %{default_branch}, %{number_commits_ahead} commits ahead"
msgstr ""
@@ -548,6 +563,9 @@ msgstr[2] ""
msgid "%{text} is available"
msgstr ""
+msgid "%{timebox_name} should belong either to a project or a group."
+msgstr ""
+
msgid "%{title} %{operator} %{threshold}"
msgstr ""
@@ -842,12 +860,18 @@ msgstr ""
msgid "<code>\"johnsmith@example.com\": \"johnsmith@example.com\"</code> will add \"By <a href=\"#\">johnsmith@example.com</a>\" to all issues and comments originally created by johnsmith@example.com. By default, the email address or username is masked to ensure the user's privacy. Use this option if you want to show the full email address."
msgstr ""
+msgid "<namespace / project>"
+msgstr ""
+
msgid "<no name set>"
msgstr ""
msgid "<no scopes selected>"
msgstr ""
+msgid "<project name>"
+msgstr ""
+
msgid "<strong>%{group_name}</strong> group members"
msgstr ""
@@ -881,6 +905,9 @@ msgstr ""
msgid "A basic page and serverless function that uses AWS Lambda, AWS API Gateway, and GitLab Pages"
msgstr ""
+msgid "A complete DevOps platform"
+msgstr ""
+
msgid "A default branch cannot be chosen for an empty project."
msgstr ""
@@ -1097,6 +1124,12 @@ msgstr ""
msgid "AccessTokens|reset it"
msgstr ""
+msgid "AccessibilityReport|Accessibility report artifact not found"
+msgstr ""
+
+msgid "AccessibilityReport|Failed to retrieve accessibility report"
+msgstr ""
+
msgid "AccessibilityReport|Learn More"
msgstr ""
@@ -1106,6 +1139,9 @@ msgstr ""
msgid "AccessibilityReport|New"
msgstr ""
+msgid "AccessibilityReport|The accessibility scanning found an error of the following type: %{code}"
+msgstr ""
+
msgid "Account"
msgstr ""
@@ -1349,6 +1385,9 @@ msgstr ""
msgid "Added at"
msgstr ""
+msgid "Added for this merge request"
+msgstr ""
+
msgid "Added in this version"
msgstr ""
@@ -1721,6 +1760,72 @@ msgstr[0] ""
msgstr[1] ""
msgstr[2] ""
+msgid "Alert Details"
+msgstr ""
+
+msgid "AlertManagement|Acknowledged"
+msgstr ""
+
+msgid "AlertManagement|Alert"
+msgstr ""
+
+msgid "AlertManagement|Authorize external service"
+msgstr ""
+
+msgid "AlertManagement|Display alerts from all your monitoring tools directly within GitLab. Streamline the investigation of your alerts and the escalation of alerts to incidents."
+msgstr ""
+
+msgid "AlertManagement|End time"
+msgstr ""
+
+msgid "AlertManagement|End time:"
+msgstr ""
+
+msgid "AlertManagement|Events"
+msgstr ""
+
+msgid "AlertManagement|Events:"
+msgstr ""
+
+msgid "AlertManagement|Full Alert Details"
+msgstr ""
+
+msgid "AlertManagement|More information"
+msgstr ""
+
+msgid "AlertManagement|No alerts available to display. If you think you're seeing this message in error, refresh the page."
+msgstr ""
+
+msgid "AlertManagement|No alerts to display."
+msgstr ""
+
+msgid "AlertManagement|Overview"
+msgstr ""
+
+msgid "AlertManagement|Resolved"
+msgstr ""
+
+msgid "AlertManagement|Severity"
+msgstr ""
+
+msgid "AlertManagement|Start time"
+msgstr ""
+
+msgid "AlertManagement|Start time:"
+msgstr ""
+
+msgid "AlertManagement|Status"
+msgstr ""
+
+msgid "AlertManagement|Surface alerts in GitLab"
+msgstr ""
+
+msgid "AlertManagement|There was an error displaying the alerts. Confirm your endpoint's configuration details to ensure alerts appear."
+msgstr ""
+
+msgid "AlertManagement|Triggered"
+msgstr ""
+
msgid "AlertService|%{linkStart}Learn more%{linkEnd} about configuring this endpoint to receive alerts."
msgstr ""
@@ -1790,6 +1895,9 @@ msgstr ""
msgid "All security scans are enabled because %{linkStart}Auto DevOps%{linkEnd} is enabled on this project"
msgstr ""
+msgid "All threads resolved"
+msgstr ""
+
msgid "All users"
msgstr ""
@@ -1808,6 +1916,9 @@ msgstr ""
msgid "Allow only the selected protocols to be used for Git access."
msgstr ""
+msgid "Allow owners to manage default branch protection per group"
+msgstr ""
+
msgid "Allow owners to manually add users outside of LDAP"
msgstr ""
@@ -1916,6 +2027,9 @@ msgstr ""
msgid "An error occurred fetching the dropdown data."
msgstr ""
+msgid "An error occurred fetching the project authors."
+msgstr ""
+
msgid "An error occurred previewing the blob"
msgstr ""
@@ -1997,6 +2111,9 @@ msgstr ""
msgid "An error occurred while fetching sidebar data"
msgstr ""
+msgid "An error occurred while fetching terraform reports."
+msgstr ""
+
msgid "An error occurred while fetching the Service Desk address."
msgstr ""
@@ -2234,6 +2351,9 @@ msgstr ""
msgid "Any"
msgstr ""
+msgid "Any Author"
+msgstr ""
+
msgid "Any Label"
msgstr ""
@@ -2453,10 +2573,10 @@ msgstr ""
msgid "Are you sure that you want to unarchive this project?"
msgstr ""
-msgid "Are you sure you want to cancel creating this comment?"
+msgid "Are you sure you want to cancel editing this comment?"
msgstr ""
-msgid "Are you sure you want to cancel editing this comment?"
+msgid "Are you sure you want to close this blocked issue?"
msgstr ""
msgid "Are you sure you want to delete %{name}?"
@@ -2486,6 +2606,9 @@ msgstr ""
msgid "Are you sure you want to deploy this environment?"
msgstr ""
+msgid "Are you sure you want to discard this comment?"
+msgstr ""
+
msgid "Are you sure you want to erase this build?"
msgstr ""
@@ -2663,6 +2786,9 @@ msgstr ""
msgid "At least one approval from a code owner is required to change files matching the respective CODEOWNER rules."
msgstr ""
+msgid "At least one logging option is required to be enabled"
+msgstr ""
+
msgid "At least one of group_id or project_id must be specified"
msgstr ""
@@ -2702,6 +2828,27 @@ msgstr ""
msgid "AuditEvents|Target"
msgstr ""
+msgid "AuditLogs|(removed)"
+msgstr ""
+
+msgid "AuditLogs|Action"
+msgstr ""
+
+msgid "AuditLogs|Author"
+msgstr ""
+
+msgid "AuditLogs|Date"
+msgstr ""
+
+msgid "AuditLogs|IP Address"
+msgstr ""
+
+msgid "AuditLogs|Object"
+msgstr ""
+
+msgid "AuditLogs|Target"
+msgstr ""
+
msgid "Aug"
msgstr ""
@@ -2984,9 +3131,6 @@ msgstr ""
msgid "BambooService|You must set up automatic revision labeling and a repository trigger in Bamboo."
msgstr ""
-msgid "Batch operations"
-msgstr ""
-
msgid "BatchComments|Delete all pending comments"
msgstr ""
@@ -3116,6 +3260,9 @@ msgstr ""
msgid "Boards"
msgstr ""
+msgid "Boards and Board Lists"
+msgstr ""
+
msgid "Boards|Collapse"
msgstr ""
@@ -3335,6 +3482,9 @@ msgstr ""
msgid "Buy GitLab Enterprise Edition"
msgstr ""
+msgid "Buy more Pipeline minutes"
+msgstr ""
+
msgid "By %{user_name}"
msgstr ""
@@ -3455,6 +3605,9 @@ msgstr ""
msgid "Can't scan the code?"
msgstr ""
+msgid "Can't update snippet: %{err}"
+msgstr ""
+
msgid "Canary"
msgstr ""
@@ -3623,6 +3776,9 @@ msgstr ""
msgid "Changing group path can have unintended side effects."
msgstr ""
+msgid "Charts"
+msgstr ""
+
msgid "Charts can't be displayed as the request for data has timed out. %{documentationLink}"
msgstr ""
@@ -3659,9 +3815,6 @@ msgstr ""
msgid "ChatMessage|and [%{count} more](%{pipeline_failed_jobs_url})"
msgstr ""
-msgid "ChatMessage|failed"
-msgstr ""
-
msgid "ChatMessage|has failed"
msgstr ""
@@ -3677,9 +3830,6 @@ msgstr ""
msgid "ChatMessage|in %{project_link}"
msgstr ""
-msgid "ChatMessage|passed"
-msgstr ""
-
msgid "Check again"
msgstr ""
@@ -4130,9 +4280,6 @@ msgstr ""
msgid "Click any <strong>project name</strong> in the project list below to navigate to the project milestone."
msgstr ""
-msgid "Click here"
-msgstr ""
-
msgid "Click the <strong>Download</strong> button and wait for downloading to complete."
msgstr ""
@@ -4520,6 +4667,12 @@ msgstr ""
msgid "ClusterIntegration|Fetching zones"
msgstr ""
+msgid "ClusterIntegration|Fluentd"
+msgstr ""
+
+msgid "ClusterIntegration|Fluentd is an open source data collector, which lets you unify the data collection and consumption for a better use and understanding of data. It requires at least one of the following logs to be successfully installed."
+msgstr ""
+
msgid "ClusterIntegration|GitLab Integration"
msgstr ""
@@ -4826,6 +4979,15 @@ msgstr ""
msgid "ClusterIntegration|Request to begin uninstalling failed"
msgstr ""
+msgid "ClusterIntegration|SIEM Hostname"
+msgstr ""
+
+msgid "ClusterIntegration|SIEM Port"
+msgstr ""
+
+msgid "ClusterIntegration|SIEM Protocol"
+msgstr ""
+
msgid "ClusterIntegration|Save changes"
msgstr ""
@@ -4922,6 +5084,12 @@ msgstr ""
msgid "ClusterIntegration|Select zone to choose machine type"
msgstr ""
+msgid "ClusterIntegration|Send Cilium Logs"
+msgstr ""
+
+msgid "ClusterIntegration|Send ModSecurity Logs"
+msgstr ""
+
msgid "ClusterIntegration|Service Token"
msgstr ""
@@ -5351,18 +5519,33 @@ msgstr ""
msgid "Compliance framework (optional)"
msgstr ""
+msgid "ComplianceFramework|GDPR"
+msgstr ""
+
msgid "ComplianceFramework|GDPR - General Data Protection Regulation"
msgstr ""
+msgid "ComplianceFramework|HIPAA"
+msgstr ""
+
msgid "ComplianceFramework|HIPAA - Health Insurance Portability and Accountability Act"
msgstr ""
+msgid "ComplianceFramework|PCI-DSS"
+msgstr ""
+
msgid "ComplianceFramework|PCI-DSS - Payment Card Industry-Data Security Standard"
msgstr ""
+msgid "ComplianceFramework|SOC 2"
+msgstr ""
+
msgid "ComplianceFramework|SOC 2 - Service Organization Control 2"
msgstr ""
+msgid "ComplianceFramework|SOX"
+msgstr ""
+
msgid "ComplianceFramework|SOX - Sarbanes-Oxley"
msgstr ""
@@ -5510,9 +5693,15 @@ msgstr ""
msgid "Container repositories sync capacity"
msgstr ""
+msgid "ContainerRegistry| Please visit the %{linkStart}administration settings%{linkEnd} to enable this feature."
+msgstr ""
+
msgid "ContainerRegistry|%{imageName} tags"
msgstr ""
+msgid "ContainerRegistry|%{title} was successfully scheduled for deletion"
+msgstr ""
+
msgid "ContainerRegistry|Automatically remove extra images that aren't designed to be kept."
msgstr ""
@@ -5525,6 +5714,9 @@ msgstr ""
msgid "ContainerRegistry|Container Registry"
msgstr ""
+msgid "ContainerRegistry|Container Registry tag expiration and retention policy is disabled"
+msgstr ""
+
msgid "ContainerRegistry|Copy build command"
msgstr ""
@@ -5534,18 +5726,12 @@ msgstr ""
msgid "ContainerRegistry|Copy push command"
msgstr ""
-msgid "ContainerRegistry|Currently, the Container Registry tag expiration feature is not available for projects created before GitLab version 12.8. For updates and more information, visit Issue %{linkStart}#196124%{linkEnd}"
-msgstr ""
-
msgid "ContainerRegistry|Docker connection error"
msgstr ""
msgid "ContainerRegistry|Docker tag expiration policy is %{toggleStatus}"
msgstr ""
-msgid "ContainerRegistry|Docker tags with names matching this regex pattern will expire:"
-msgstr ""
-
msgid "ContainerRegistry|Edit Settings"
msgstr ""
@@ -5567,9 +5753,6 @@ msgstr ""
msgid "ContainerRegistry|Image ID"
msgstr ""
-msgid "ContainerRegistry|Image deleted successfully"
-msgstr ""
-
msgid "ContainerRegistry|Keep and protect the images that matter most."
msgstr ""
@@ -5585,12 +5768,24 @@ msgstr ""
msgid "ContainerRegistry|Number of tags to retain:"
msgstr ""
+msgid "ContainerRegistry|Please contact your administrator."
+msgstr ""
+
msgid "ContainerRegistry|Push an image"
msgstr ""
msgid "ContainerRegistry|Quick Start"
msgstr ""
+msgid "ContainerRegistry|Regular expressions such as %{codeStart}.*-test%{codeEnd} or %{codeStart}dev-.*%{codeEnd} are supported"
+msgstr ""
+
+msgid "ContainerRegistry|Regular expressions such as %{codeStart}.*-test%{codeEnd} or %{codeStart}dev-.*%{codeEnd} are supported. To select all tags, use %{codeStart}.*%{codeEnd}"
+msgstr ""
+
+msgid "ContainerRegistry|Remember to run %{docLinkStart}garbage collection%{docLinkEnd} to remove the stale data from storage."
+msgstr ""
+
msgid "ContainerRegistry|Remove repository"
msgstr ""
@@ -5606,22 +5801,22 @@ msgstr[2] ""
msgid "ContainerRegistry|Retention policy has been Enabled"
msgstr ""
-msgid "ContainerRegistry|Something went wrong while deleting the image."
+msgid "ContainerRegistry|Something went wrong while fetching the expiration policy."
msgstr ""
-msgid "ContainerRegistry|Something went wrong while deleting the tag."
+msgid "ContainerRegistry|Something went wrong while fetching the repository list."
msgstr ""
-msgid "ContainerRegistry|Something went wrong while deleting the tags."
+msgid "ContainerRegistry|Something went wrong while fetching the tags list."
msgstr ""
-msgid "ContainerRegistry|Something went wrong while fetching the expiration policy."
+msgid "ContainerRegistry|Something went wrong while marking the tag for deletion."
msgstr ""
-msgid "ContainerRegistry|Something went wrong while fetching the packages list."
+msgid "ContainerRegistry|Something went wrong while marking the tags for deletion."
msgstr ""
-msgid "ContainerRegistry|Something went wrong while fetching the tags list."
+msgid "ContainerRegistry|Something went wrong while scheduling %{title} for deletion. Please try again."
msgstr ""
msgid "ContainerRegistry|Something went wrong while updating the expiration policy."
@@ -5630,16 +5825,25 @@ msgstr ""
msgid "ContainerRegistry|Tag"
msgstr ""
-msgid "ContainerRegistry|Tag deleted successfully"
-msgstr ""
-
msgid "ContainerRegistry|Tag expiration policy"
msgstr ""
msgid "ContainerRegistry|Tag expiration policy is designed to:"
msgstr ""
-msgid "ContainerRegistry|Tags deleted successfully"
+msgid "ContainerRegistry|Tag successfully marked for deletion."
+msgstr ""
+
+msgid "ContainerRegistry|Tags successfully marked for deletion."
+msgstr ""
+
+msgid "ContainerRegistry|Tags with names matching this regex pattern will %{italicStart}be preserved:%{italicEnd}"
+msgstr ""
+
+msgid "ContainerRegistry|Tags with names matching this regex pattern will %{italicStart}expire:%{italicEnd}"
+msgstr ""
+
+msgid "ContainerRegistry|The Container Registry tag expiration and retention policies for this project have not been enabled."
msgstr ""
msgid "ContainerRegistry|The last tag related to this image was recently removed. This empty image and any associated data will be automatically removed as part of the regular Garbage Collection process. If you have any questions, contact your administrator."
@@ -5660,16 +5864,16 @@ msgstr ""
msgid "ContainerRegistry|There are no container images stored for this project"
msgstr ""
-msgid "ContainerRegistry|This Registry contains deleted image tag data. Remember to run %{docLinkStart}garbage collection%{docLinkEnd} to remove the stale data from storage."
+msgid "ContainerRegistry|There was an error during the deletion of this image repository, please try again."
msgstr ""
msgid "ContainerRegistry|This image has no active tags"
msgstr ""
-msgid "ContainerRegistry|We are having trouble connecting to Docker, which could be due to an issue with your project name or path. %{docLinkStart}More Information%{docLinkEnd}"
+msgid "ContainerRegistry|This image repository is scheduled for deletion"
msgstr ""
-msgid "ContainerRegistry|Wildcards such as %{codeStart}.*-stable%{codeEnd} or %{codeStart}production/.*%{codeEnd} are supported. To select all tags, use %{codeStart}.*%{codeEnd}"
+msgid "ContainerRegistry|We are having trouble connecting to Docker, which could be due to an issue with your project name or path. %{docLinkStart}More Information%{docLinkEnd}"
msgstr ""
msgid "ContainerRegistry|With the Container Registry, every project can have its own space to store its Docker images. %{docLinkStart}More Information%{docLinkEnd}"
@@ -5924,7 +6128,7 @@ msgstr ""
msgid "Could not delete chat nickname %{chat_name}."
msgstr ""
-msgid "Could not find design"
+msgid "Could not find design."
msgstr ""
msgid "Could not remove the trigger."
@@ -5939,6 +6143,9 @@ msgstr ""
msgid "Could not revoke personal access token %{personal_access_token_name}."
msgstr ""
+msgid "Could not revoke project access token %{project_access_token_name}."
+msgstr ""
+
msgid "Could not save group ID"
msgstr ""
@@ -5996,6 +6203,9 @@ msgstr ""
msgid "Create a new branch"
msgstr ""
+msgid "Create a new deploy key for this project"
+msgstr ""
+
msgid "Create a new file as there are no files yet. Afterwards, you'll be able to commit your changes."
msgstr ""
@@ -6101,6 +6311,9 @@ msgstr ""
msgid "Create requirement"
msgstr ""
+msgid "Create snippet"
+msgstr ""
+
msgid "Create wildcard: %{searchTerm}"
msgstr ""
@@ -6122,6 +6335,9 @@ msgstr ""
msgid "Created"
msgstr ""
+msgid "Created %{timestamp}"
+msgstr ""
+
msgid "Created At"
msgstr ""
@@ -6479,6 +6695,9 @@ msgstr ""
msgid "CycleAnalytics|stage dropdown"
msgstr ""
+msgid "DAG"
+msgstr ""
+
msgid "DNS"
msgstr ""
@@ -6851,6 +7070,9 @@ msgstr ""
msgid "Deploy key was successfully updated."
msgstr ""
+msgid "Deploy keys allow read-only or read-write (if enabled) access to your repository. Deploy keys can be used for CI, staging or production servers. You can create a deploy key or add an existing one."
+msgstr ""
+
msgid "Deploy progress not found. To see pods, ensure your environment matches %{linkStart}deploy board criteria%{linkEnd}."
msgstr ""
@@ -7004,6 +7226,9 @@ msgstr ""
msgid "Deploying to"
msgstr ""
+msgid "Deployment Frequency"
+msgstr ""
+
msgid "Deployment|API"
msgstr ""
@@ -7523,9 +7748,6 @@ msgstr ""
msgid "Edit environment"
msgstr ""
-msgid "Edit epic description"
-msgstr ""
-
msgid "Edit file"
msgstr ""
@@ -8147,9 +8369,6 @@ msgstr ""
msgid "Epics"
msgstr ""
-msgid "Epics (Ultimate / Gold license only)"
-msgstr ""
-
msgid "Epics Roadmap"
msgstr ""
@@ -8159,6 +8378,9 @@ msgstr ""
msgid "Epics let you manage your portfolio of projects more efficiently and with less effort"
msgstr ""
+msgid "Epics, Issues, and Merge Requests"
+msgstr ""
+
msgid "Epics|Add an epic"
msgstr ""
@@ -8210,6 +8432,9 @@ msgstr ""
msgid "Epics|Something went wrong while fetching group epics."
msgstr ""
+msgid "Epics|Something went wrong while moving item."
+msgstr ""
+
msgid "Epics|Something went wrong while ordering item."
msgstr ""
@@ -8264,6 +8489,9 @@ msgstr ""
msgid "Error fetching network graph."
msgstr ""
+msgid "Error fetching payload data."
+msgstr ""
+
msgid "Error fetching projects"
msgstr ""
@@ -8273,9 +8501,6 @@ msgstr ""
msgid "Error fetching the dependency list. Please check your network connection and try again."
msgstr ""
-msgid "Error fetching usage ping data."
-msgstr ""
-
msgid "Error loading branch data. Please try again."
msgstr ""
@@ -8417,6 +8642,9 @@ msgstr ""
msgid "Errors"
msgstr ""
+msgid "Errors:"
+msgstr ""
+
msgid "Estimated"
msgstr ""
@@ -8621,12 +8849,18 @@ msgstr ""
msgid "Export as CSV"
msgstr ""
+msgid "Export group"
+msgstr ""
+
msgid "Export issues"
msgstr ""
msgid "Export project"
msgstr ""
+msgid "Export this group with all related data to a new GitLab instance. Once complete, you can import the data file from the \"New Group\" page."
+msgstr ""
+
msgid "Export this project with all its related data in order to move your project to a new GitLab instance. Once the export is finished, you can import the file from the \"New Project\" page."
msgstr ""
@@ -8696,9 +8930,6 @@ msgstr ""
msgid "Failed Jobs"
msgstr ""
-msgid "Failed create wiki"
-msgstr ""
-
msgid "Failed to add a Zoom meeting"
msgstr ""
@@ -8738,6 +8969,9 @@ msgstr ""
msgid "Failed to create resources"
msgstr ""
+msgid "Failed to create wiki"
+msgstr ""
+
msgid "Failed to delete board. Please try again."
msgstr ""
@@ -8747,7 +8981,7 @@ msgstr ""
msgid "Failed to enqueue the rebase operation, possibly due to a long-lived transaction. Try again later."
msgstr ""
-msgid "Failed to find import label for jira import."
+msgid "Failed to find import label for Jira import."
msgstr ""
msgid "Failed to get ref."
@@ -8771,6 +9005,12 @@ msgstr ""
msgid "Failed to load groups & users."
msgstr ""
+msgid "Failed to load labels. Please try again."
+msgstr ""
+
+msgid "Failed to load milestones. Please try again."
+msgstr ""
+
msgid "Failed to load related branches"
msgstr ""
@@ -9146,7 +9386,10 @@ msgstr ""
msgid "Filter by milestone name"
msgstr ""
-msgid "Filter by name..."
+msgid "Filter by name"
+msgstr ""
+
+msgid "Filter by status"
msgstr ""
msgid "Filter by two-factor authentication"
@@ -9158,12 +9401,18 @@ msgstr ""
msgid "Filter projects"
msgstr ""
+msgid "Filter results"
+msgstr ""
+
msgid "Filter results by group"
msgstr ""
msgid "Filter results by project"
msgstr ""
+msgid "Filter results..."
+msgstr ""
+
msgid "Filter your projects by name"
msgstr ""
@@ -9452,10 +9701,10 @@ msgstr ""
msgid "GeoNodes|Checksummed"
msgstr ""
-msgid "GeoNodes|Container repositories"
+msgid "GeoNodes|Consult Geo troubleshooting information"
msgstr ""
-msgid "GeoNodes|Data is out of date from %{timeago}"
+msgid "GeoNodes|Container repositories"
msgstr ""
msgid "GeoNodes|Data replication lag"
@@ -9497,6 +9746,9 @@ msgstr ""
msgid "GeoNodes|Last event ID seen from primary"
msgstr ""
+msgid "GeoNodes|Learn more about Geo node statuses"
+msgstr ""
+
msgid "GeoNodes|Loading nodes"
msgstr ""
@@ -9512,6 +9764,9 @@ msgstr ""
msgid "GeoNodes|Node was successfully removed."
msgstr ""
+msgid "GeoNodes|Node's status was updated %{timeAgo}."
+msgstr ""
+
msgid "GeoNodes|Not checksummed"
msgstr ""
@@ -9542,7 +9797,10 @@ msgstr ""
msgid "GeoNodes|Repository verification progress"
msgstr ""
-msgid "GeoNodes|Selective"
+msgid "GeoNodes|Selective (%{syncLabel})"
+msgstr ""
+
+msgid "GeoNodes|Selective synchronization"
msgstr ""
msgid "GeoNodes|Something went wrong while changing node status"
@@ -9569,6 +9827,9 @@ msgstr ""
msgid "GeoNodes|Unverified"
msgstr ""
+msgid "GeoNodes|Updated %{timeAgo}"
+msgstr ""
+
msgid "GeoNodes|Used slots"
msgstr ""
@@ -9608,16 +9869,16 @@ msgstr ""
msgid "Geo|All"
msgstr ""
-msgid "Geo|All projects"
+msgid "Geo|All %{replicable_type}"
msgstr ""
-msgid "Geo|All projects are being scheduled for re-sync"
+msgid "Geo|All projects"
msgstr ""
-msgid "Geo|All projects are being scheduled for re-verify"
+msgid "Geo|All projects are being scheduled for resync"
msgstr ""
-msgid "Geo|Batch operations"
+msgid "Geo|All projects are being scheduled for reverify"
msgstr ""
msgid "Geo|Could not remove tracking entry for an existing project."
@@ -9629,9 +9890,15 @@ msgstr ""
msgid "Geo|Failed"
msgstr ""
+msgid "Geo|Filter by status"
+msgstr ""
+
msgid "Geo|Geo Status"
msgstr ""
+msgid "Geo|In progress"
+msgstr ""
+
msgid "Geo|In sync"
msgstr ""
@@ -9656,9 +9923,6 @@ msgstr ""
msgid "Geo|Not synced yet"
msgstr ""
-msgid "Geo|Pending"
-msgstr ""
-
msgid "Geo|Pending synchronization"
msgstr ""
@@ -9695,7 +9959,7 @@ msgstr ""
msgid "Geo|Resync"
msgstr ""
-msgid "Geo|Resync all projects"
+msgid "Geo|Resync all"
msgstr ""
msgid "Geo|Retry count"
@@ -9704,7 +9968,7 @@ msgstr ""
msgid "Geo|Reverify"
msgstr ""
-msgid "Geo|Reverify all projects"
+msgid "Geo|Reverify all"
msgstr ""
msgid "Geo|Status"
@@ -10046,9 +10310,6 @@ msgstr ""
msgid "Go to file"
msgstr ""
-msgid "Go to file (MRs only)"
-msgstr ""
-
msgid "Go to file permalink (while viewing a file)"
msgstr ""
@@ -10493,6 +10754,9 @@ msgstr ""
msgid "GroupSettings|Disable group mentions"
msgstr ""
+msgid "GroupSettings|Export group"
+msgstr ""
+
msgid "GroupSettings|If the parent group's visibility is lower than the group current visibility, visibility levels for subgroups and projects will be changed to match the new parent group's visibility."
msgstr ""
@@ -10571,6 +10835,9 @@ msgstr ""
msgid "Groups (%{count})"
msgstr ""
+msgid "Groups (%{groups})"
+msgstr ""
+
msgid "Groups can also be nested by creating %{subgroup_docs_link_start}subgroups%{subgroup_docs_link_end}."
msgstr ""
@@ -10838,15 +11105,12 @@ msgstr ""
msgid "ID:"
msgstr ""
-msgid "IDE|Allow live previews of JavaScript projects in the Web IDE using CodeSandbox client side evaluation."
+msgid "IDE|Allow live previews of JavaScript projects in the Web IDE using CodeSandbox Live Preview."
msgstr ""
msgid "IDE|Back"
msgstr ""
-msgid "IDE|Client side evaluation"
-msgstr ""
-
msgid "IDE|Commit"
msgstr ""
@@ -10910,6 +11174,9 @@ msgstr ""
msgid "If any job surpasses this timeout threshold, it will be marked as failed. Human readable time input language is accepted like \"1 hour\". Values without specification represent seconds."
msgstr ""
+msgid "If blank, set allowable lifetime to %{instance_level_policy_in_words}, as defined by the instance admin. Once set, existing tokens for users in this group may be revoked."
+msgstr ""
+
msgid "If checked, group owners can manage LDAP group links and LDAP member overrides"
msgstr ""
@@ -10949,6 +11216,9 @@ msgstr ""
msgid "If you lose your recovery codes you can generate new ones, invalidating all previous codes."
msgstr ""
+msgid "If you reach 100%% storage capacity, you will not be able to: %{base_message}"
+msgstr ""
+
msgid "If your HTTP repository is not publicly accessible, add your credentials."
msgstr ""
@@ -11060,6 +11330,9 @@ msgstr ""
msgid "Import repository"
msgstr ""
+msgid "Import started by: %{importInitiator}"
+msgstr ""
+
msgid "Import tasks"
msgstr ""
@@ -11126,6 +11399,9 @@ msgstr ""
msgid "In order to tailor your experience with GitLab we<br>would like to know a bit more about you."
msgstr ""
+msgid "In progress"
+msgstr ""
+
msgid "In the next step, you'll be able to select the projects you want to import."
msgstr ""
@@ -11474,15 +11750,15 @@ msgstr ""
msgid "Issues"
msgstr ""
-msgid "Issues / Merge Requests"
-msgstr ""
-
msgid "Issues Analytics"
msgstr ""
msgid "Issues Rate Limits"
msgstr ""
+msgid "Issues and Merge Requests"
+msgstr ""
+
msgid "Issues can be bugs, tasks or ideas to be discussed. Also, issues are searchable and filterable."
msgstr ""
@@ -11504,10 +11780,10 @@ msgstr ""
msgid "IssuesAnalytics|Avg/Month:"
msgstr ""
-msgid "IssuesAnalytics|Issues created"
+msgid "IssuesAnalytics|Issues opened"
msgstr ""
-msgid "IssuesAnalytics|Issues created per month"
+msgid "IssuesAnalytics|Issues opened per month"
msgstr ""
msgid "IssuesAnalytics|Last 12 months"
@@ -11561,6 +11837,9 @@ msgstr ""
msgid "Jira integration not configured."
msgstr ""
+msgid "Jira project: %{importProject}"
+msgstr ""
+
msgid "JiraService|Events for %{noteable_model_name} are disabled."
msgstr ""
@@ -11951,6 +12230,9 @@ msgstr ""
msgid "Lead"
msgstr ""
+msgid "Lead Time"
+msgstr ""
+
msgid "Learn GitLab"
msgstr ""
@@ -11963,6 +12245,9 @@ msgstr ""
msgid "Learn how to %{no_packages_link_start}publish and share your packages%{no_packages_link_end} with GitLab."
msgstr ""
+msgid "Learn how to enable synchronization"
+msgstr ""
+
msgid "Learn more"
msgstr ""
@@ -12299,9 +12584,6 @@ msgstr ""
msgid "Live preview"
msgstr ""
-msgid "Load more vulnerabilities"
-msgstr ""
-
msgid "Loading"
msgstr ""
@@ -12452,9 +12734,6 @@ msgstr ""
msgid "Manage"
msgstr ""
-msgid "Manage Git repositories with fine-grained access controls that keep your code secure. Perform code reviews and enhance collaboration with merge requests. Each project can also have an issue tracker and a wiki."
-msgstr ""
-
msgid "Manage Web IDE features"
msgstr ""
@@ -12485,6 +12764,9 @@ msgstr ""
msgid "Manage your license"
msgstr ""
+msgid "Managed Account"
+msgstr ""
+
msgid "Manifest"
msgstr ""
@@ -12749,6 +13031,9 @@ msgstr ""
msgid "Merge Requests in Review"
msgstr ""
+msgid "Merge automatically (%{strategy})"
+msgstr ""
+
msgid "Merge commit message"
msgstr ""
@@ -12902,6 +13187,12 @@ msgstr ""
msgid "Merged branches are being deleted. This can take some time depending on the number of branches. Please refresh the page to see changes."
msgstr ""
+msgid "Merged this merge request."
+msgstr ""
+
+msgid "Merges this merge request immediately."
+msgstr ""
+
msgid "Merges this merge request when the pipeline succeeds."
msgstr ""
@@ -13010,12 +13301,15 @@ msgstr[0] ""
msgstr[1] ""
msgstr[2] ""
-msgid "Metrics|Environment"
+msgid "Metrics|Expand panel"
msgstr ""
msgid "Metrics|For grouping similar metrics"
msgstr ""
+msgid "Metrics|Go back (Esc)"
+msgstr ""
+
msgid "Metrics|Invalid time range, please verify."
msgstr ""
@@ -13046,9 +13340,6 @@ msgstr ""
msgid "Metrics|Refresh dashboard"
msgstr ""
-msgid "Metrics|Show last"
-msgstr ""
-
msgid "Metrics|There was an error creating the dashboard."
msgstr ""
@@ -13127,6 +13418,9 @@ msgstr ""
msgid "Microsoft Azure"
msgstr ""
+msgid "Middleman project with Static Site Editor support"
+msgstr ""
+
msgid "Migrated %{success_count}/%{total_count} files."
msgstr ""
@@ -13574,10 +13868,10 @@ msgstr ""
msgid "Next"
msgstr ""
-msgid "Next file in diff (MRs only)"
+msgid "Next file in diff"
msgstr ""
-msgid "Next unresolved discussion (MRs only)"
+msgid "Next unresolved discussion"
msgstr ""
msgid "Nickname"
@@ -13637,6 +13931,9 @@ msgstr ""
msgid "No changes between %{ref_start}%{source_branch}%{ref_end} and %{ref_start}%{target_branch}%{ref_end}"
msgstr ""
+msgid "No child epics match applied filters"
+msgstr ""
+
msgid "No connection could be made to a Gitaly Server, please check your logs!"
msgstr ""
@@ -13775,15 +14072,6 @@ msgstr ""
msgid "No thanks, don't show this again"
msgstr ""
-msgid "No vulnerabilities found for this group"
-msgstr ""
-
-msgid "No vulnerabilities found for this pipeline"
-msgstr ""
-
-msgid "No vulnerabilities found for this project"
-msgstr ""
-
msgid "No vulnerabilities present"
msgstr ""
@@ -13907,6 +14195,9 @@ msgstr ""
msgid "Nothing to preview."
msgstr ""
+msgid "Nothing to synchronize"
+msgstr ""
+
msgid "Notification events"
msgstr ""
@@ -14078,6 +14369,9 @@ msgstr ""
msgid "Once removed, the fork relationship cannot be restored and you will no longer be able to send merge requests to the source."
msgstr ""
+msgid "Once the exported file is ready you can download it from this page."
+msgstr ""
+
msgid "Once the exported file is ready, you will receive a notification email with a download link, or you can download it from this page."
msgstr ""
@@ -14180,9 +14474,6 @@ msgstr ""
msgid "Open sidebar"
msgstr ""
-msgid "Open source software to collaborate on code"
-msgstr ""
-
msgid "Open: %{openIssuesCount}"
msgstr ""
@@ -14285,6 +14576,9 @@ msgstr ""
msgid "OutdatedBrowser|You can provide feedback %{feedback_link_start}on this issue%{feedback_link_end} or via your usual support channels."
msgstr ""
+msgid "Overridden"
+msgstr ""
+
msgid "Overview"
msgstr ""
@@ -14336,6 +14630,9 @@ msgstr ""
msgid "PackageRegistry|Conan Command"
msgstr ""
+msgid "PackageRegistry|Copy .pypirc content"
+msgstr ""
+
msgid "PackageRegistry|Copy Conan Command"
msgstr ""
@@ -14357,6 +14654,9 @@ msgstr ""
msgid "PackageRegistry|Copy NuGet Setup Command"
msgstr ""
+msgid "PackageRegistry|Copy Pip command"
+msgstr ""
+
msgid "PackageRegistry|Copy and paste this inside your %{codeStart}pom.xml%{codeEnd} %{codeStart}dependencies%{codeEnd} block."
msgstr ""
@@ -14390,6 +14690,12 @@ msgstr ""
msgid "PackageRegistry|For more information on the NuGet registry, %{linkStart}see the documentation%{linkEnd}."
msgstr ""
+msgid "PackageRegistry|For more information on the PyPi registry, %{linkStart}see the documentation%{linkEnd}."
+msgstr ""
+
+msgid "PackageRegistry|If you haven't already done so, you will need to add the below to your %{codeStart}.pypirc%{codeEnd} file."
+msgstr ""
+
msgid "PackageRegistry|If you haven't already done so, you will need to add the below to your %{codeStart}pom.xml%{codeEnd} file."
msgstr ""
@@ -14399,6 +14705,9 @@ msgstr ""
msgid "PackageRegistry|Learn how to %{noPackagesLinkStart}publish and share your packages%{noPackagesLinkEnd} with GitLab."
msgstr ""
+msgid "PackageRegistry|Manually Published"
+msgstr ""
+
msgid "PackageRegistry|Maven"
msgstr ""
@@ -14417,12 +14726,18 @@ msgstr ""
msgid "PackageRegistry|NuGet Command"
msgstr ""
+msgid "PackageRegistry|Pip Command"
+msgstr ""
+
msgid "PackageRegistry|Pipeline %{linkStart}%{linkEnd} triggered %{timestamp} by %{author}"
msgstr ""
msgid "PackageRegistry|Published to the repository at %{timestamp}"
msgstr ""
+msgid "PackageRegistry|PyPi"
+msgstr ""
+
msgid "PackageRegistry|Registry Setup"
msgstr ""
@@ -14459,6 +14774,9 @@ msgstr ""
msgid "PackageRegistry|npm"
msgstr ""
+msgid "PackageRegistry|published by %{author}"
+msgstr ""
+
msgid "PackageRegistry|yarn"
msgstr ""
@@ -14474,6 +14792,9 @@ msgstr ""
msgid "PackageType|NuGet"
msgstr ""
+msgid "PackageType|PyPi"
+msgstr ""
+
msgid "Packages"
msgstr ""
@@ -14567,6 +14888,9 @@ msgstr ""
msgid "Past due"
msgstr ""
+msgid "Paste a machine public key here. Read more about how to generate it"
+msgstr ""
+
msgid "Paste a machine public key here. Read more about how to generate it %{link_start}here%{link_end}"
msgstr ""
@@ -14582,9 +14906,6 @@ msgstr ""
msgid "Path"
msgstr ""
-msgid "Path, transfer, remove"
-msgstr ""
-
msgid "Path:"
msgstr ""
@@ -14615,7 +14936,7 @@ msgstr ""
msgid "Percentage"
msgstr ""
-msgid "Perform advanced options such as changing path, transferring, or removing the group."
+msgid "Perform advanced options such as changing path, transferring, exporting, or removing the group."
msgstr ""
msgid "Perform common operations on GitLab project"
@@ -14696,9 +15017,6 @@ msgstr ""
msgid "Pipeline minutes quota"
msgstr ""
-msgid "Pipeline quota"
-msgstr ""
-
msgid "Pipeline subscriptions"
msgstr ""
@@ -14708,6 +15026,9 @@ msgstr ""
msgid "Pipeline: %{status}"
msgstr ""
+msgid "PipelineCharts|CI / CD Analytics"
+msgstr ""
+
msgid "PipelineCharts|Failed:"
msgstr ""
@@ -14786,15 +15107,6 @@ msgstr ""
msgid "Pipelines settings for '%{project_name}' were successfully updated."
msgstr ""
-msgid "Pipelines| to purchase more minutes."
-msgstr ""
-
-msgid "Pipelines|%{namespace_name} has exceeded its pipeline minutes quota."
-msgstr ""
-
-msgid "Pipelines|%{namespace_name} has less than %{notification_level}%% of CI minutes available."
-msgstr ""
-
msgid "Pipelines|API"
msgstr ""
@@ -14816,10 +15128,13 @@ msgstr ""
msgid "Pipelines|Get started with Pipelines"
msgstr ""
-msgid "Pipelines|Loading Pipelines"
+msgid "Pipelines|Group %{namespace_name} has %{percentage}%% or less Shared Runner Pipeline minutes remaining. Once it runs out, no new jobs or pipelines in its projects will run."
msgstr ""
-msgid "Pipelines|Pipelines will not run anymore on shared Runners."
+msgid "Pipelines|Group %{namespace_name} has exceeded its pipeline minutes quota. Unless you buy additional pipeline minutes, no new jobs or pipelines in its projects will run."
+msgstr ""
+
+msgid "Pipelines|Loading Pipelines"
msgstr ""
msgid "Pipelines|Project cache successfully reset."
@@ -15050,6 +15365,9 @@ msgstr ""
msgid "Please select"
msgstr ""
+msgid "Please select a Jira project"
+msgstr ""
+
msgid "Please select a country"
msgstr ""
@@ -15095,6 +15413,9 @@ msgstr ""
msgid "Please wait while we import the repository for you. Refresh at will."
msgstr ""
+msgid "Plugins directory is deprecated and will be removed in 14.0. Please move this file into /file_hooks directory."
+msgstr ""
+
msgid "Pod does not exist"
msgstr ""
@@ -15212,7 +15533,7 @@ msgstr ""
msgid "Prevent users from changing their profile name"
msgstr ""
-msgid "Prevent users from modifing merge request approvers list"
+msgid "Prevent users from modifying merge request approvers list"
msgstr ""
msgid "Prevent users from performing write operations on GitLab while performing maintenance."
@@ -15233,10 +15554,10 @@ msgstr ""
msgid "Previous Artifacts"
msgstr ""
-msgid "Previous file in diff (MRs only)"
+msgid "Previous file in diff"
msgstr ""
-msgid "Previous unresolved discussion (MRs only)"
+msgid "Previous unresolved discussion"
msgstr ""
msgid "Primary"
@@ -15269,6 +15590,9 @@ msgstr ""
msgid "Private profile"
msgstr ""
+msgid "Private projects Minutes cost factor"
+msgstr ""
+
msgid "Private projects can be created in your personal namespace with:"
msgstr ""
@@ -16097,6 +16421,9 @@ msgstr ""
msgid "ProjectSettings|With GitLab Pages you can host your static websites on GitLab"
msgstr ""
+msgid "ProjectSettings|With Metrics Dashboard you can visualize this project performance metrics"
+msgstr ""
+
msgid "ProjectTemplates|.NET Core"
msgstr ""
@@ -16160,6 +16487,9 @@ msgstr ""
msgid "ProjectTemplates|Spring"
msgstr ""
+msgid "ProjectTemplates|Static Site Editor/Middleman"
+msgstr ""
+
msgid "ProjectTemplates|iOS (Swift)"
msgstr ""
@@ -16379,13 +16709,13 @@ msgstr ""
msgid "PrometheusService|Prometheus is being automatically managed on your clusters"
msgstr ""
-msgid "PrometheusService|These metrics will only be monitored after your first deployment to an environment"
+msgid "PrometheusService|Select the Active checkbox to override the Auto Configuration with custom settings. If unchecked, Auto Configuration settings are used."
msgstr ""
-msgid "PrometheusService|Time-series monitoring service"
+msgid "PrometheusService|These metrics will only be monitored after your first deployment to an environment"
msgstr ""
-msgid "PrometheusService|To enable manual configuration, uninstall Prometheus from your clusters"
+msgid "PrometheusService|Time-series monitoring service"
msgstr ""
msgid "PrometheusService|To enable the installation of Prometheus on your clusters, deactivate the manual configuration below"
@@ -16571,6 +16901,9 @@ msgstr ""
msgid "Protip:"
msgstr ""
+msgid "Protocol"
+msgstr ""
+
msgid "Provider"
msgstr ""
@@ -16595,6 +16928,9 @@ msgstr ""
msgid "Public pipelines"
msgstr ""
+msgid "Public projects Minutes cost factor"
+msgstr ""
+
msgid "Pull"
msgstr ""
@@ -16796,6 +17132,9 @@ msgstr[0] ""
msgstr[1] ""
msgstr[2] ""
+msgid "Regenerate export"
+msgstr ""
+
msgid "Regenerate instance ID"
msgstr ""
@@ -17174,9 +17513,30 @@ msgstr ""
msgid "Reports|%{combinedString} and %{resolvedString}"
msgstr ""
+msgid "Reports|Accessibility scanning detected %d issue for the source branch only"
+msgid_plural "Reports|Accessibility scanning detected %d issues for the source branch only"
+msgstr[0] ""
+msgstr[1] ""
+msgstr[2] ""
+
+msgid "Reports|Accessibility scanning detected no issues for the source branch only"
+msgstr ""
+
+msgid "Reports|Accessibility scanning failed loading results"
+msgstr ""
+
+msgid "Reports|Accessibility scanning results are being parsed"
+msgstr ""
+
msgid "Reports|Actions"
msgstr ""
+msgid "Reports|An error occured while loading report"
+msgstr ""
+
+msgid "Reports|An error occurred while loading %{name} results"
+msgstr ""
+
msgid "Reports|Class"
msgstr ""
@@ -17282,7 +17642,7 @@ msgstr ""
msgid "Requested %{time_ago}"
msgstr ""
-msgid "Requested design version does not exist"
+msgid "Requested design version does not exist."
msgstr ""
msgid "Requested states are invalid"
@@ -17468,6 +17828,9 @@ msgstr ""
msgid "Resync"
msgstr ""
+msgid "Resync all"
+msgstr ""
+
msgid "Resync all %{replicableType}"
msgstr ""
@@ -17537,6 +17900,9 @@ msgstr ""
msgid "Revoked personal access token %{personal_access_token_name}!"
msgstr ""
+msgid "Revoked project access token %{project_access_token_name}!"
+msgstr ""
+
msgid "RightSidebar|adding a"
msgstr ""
@@ -17585,6 +17951,9 @@ msgstr ""
msgid "Runner token"
msgstr ""
+msgid "Runner tokens"
+msgstr ""
+
msgid "Runner was not updated."
msgstr ""
@@ -17639,6 +18008,9 @@ msgstr ""
msgid "SAML SSO for %{group_name}"
msgstr ""
+msgid "SAML discovery tokens"
+msgstr ""
+
msgid "SAML for %{group_name}"
msgstr ""
@@ -17723,12 +18095,18 @@ msgstr ""
msgid "Scheduled"
msgstr ""
+msgid "Scheduled to merge this merge request (%{strategy})."
+msgstr ""
+
msgid "Scheduled to merge this merge request when the pipeline succeeds."
msgstr ""
msgid "Schedules"
msgstr ""
+msgid "Schedules to merge this merge request (%{strategy})."
+msgstr ""
+
msgid "Scheduling"
msgstr ""
@@ -17786,6 +18164,9 @@ msgstr ""
msgid "Search branches and tags"
msgstr ""
+msgid "Search by author"
+msgstr ""
+
msgid "Search files"
msgstr ""
@@ -17939,12 +18320,6 @@ msgstr[0] ""
msgstr[1] ""
msgstr[2] ""
-msgid "SearchResults|snippet result"
-msgid_plural "SearchResults|snippet results"
-msgstr[0] ""
-msgstr[1] ""
-msgstr[2] ""
-
msgid "SearchResults|user"
msgid_plural "SearchResults|users"
msgstr[0] ""
@@ -17957,6 +18332,9 @@ msgstr[0] ""
msgstr[1] ""
msgstr[2] ""
+msgid "Searching by both author and message is currently not supported."
+msgstr ""
+
msgid "Seat Link"
msgstr ""
@@ -17984,184 +18362,232 @@ msgstr ""
msgid "Security Dashboard"
msgstr ""
-msgid "Security Dashboard|Error fetching the vulnerability counts. Please check your network connection and try again."
+msgid "Security configuration help link"
msgstr ""
-msgid "Security Dashboard|Error fetching the vulnerability list. Please check your network connection and try again."
+msgid "Security dashboard"
msgstr ""
-msgid "Security Dashboard|Issue Created"
+msgid "Security report is out of date. Please update your branch with the latest changes from the target branch (%{targetBranchName})"
msgstr ""
-msgid "Security Reports|Comment added to '%{vulnerabilityName}'"
+msgid "Security report is out of date. Run %{newPipelineLinkStart}a new pipeline%{newPipelineLinkEnd} for the target branch (%{targetBranchName})"
msgstr ""
-msgid "Security Reports|Comment deleted on '%{vulnerabilityName}'"
+msgid "SecurityConfiguration|Configured"
msgstr ""
-msgid "Security Reports|Comment edited on '%{vulnerabilityName}'"
+msgid "SecurityConfiguration|Feature"
msgstr ""
-msgid "Security Reports|Create issue"
+msgid "SecurityConfiguration|Feature documentation for %{featureName}"
msgstr ""
-msgid "Security Reports|Dismiss vulnerability"
+msgid "SecurityConfiguration|Not yet configured"
msgstr ""
-msgid "Security Reports|Dismissed '%{vulnerabilityName}'"
+msgid "SecurityConfiguration|Secure features"
msgstr ""
-msgid "Security Reports|Dismissed '%{vulnerabilityName}'. Turn off the hide dismissed toggle to view."
+msgid "SecurityConfiguration|Status"
msgstr ""
-msgid "Security Reports|Either you don't have permission to view this dashboard or the dashboard has not been setup. Please check your permission settings with your administrator or check your dashboard configurations to proceed."
+msgid "SecurityReports|%{firstProject} and %{secondProject}"
msgstr ""
-msgid "Security Reports|Learn more about setting up your dashboard"
+msgid "SecurityReports|%{firstProject}, %{secondProject}, and %{rest}"
msgstr ""
-msgid "Security Reports|More info"
+msgid "SecurityReports|Add a project to your dashboard"
msgstr ""
-msgid "Security Reports|Oops, something doesn't seem right."
+msgid "SecurityReports|Add or remove projects from your dashboard"
msgstr ""
-msgid "Security Reports|Security reports can only be accessed by authorized users."
+msgid "SecurityReports|Add projects"
msgstr ""
-msgid "Security Reports|There was an error adding the comment."
+msgid "SecurityReports|Comment added to '%{vulnerabilityName}'"
msgstr ""
-msgid "Security Reports|There was an error creating the issue."
+msgid "SecurityReports|Comment deleted on '%{vulnerabilityName}'"
msgstr ""
-msgid "Security Reports|There was an error creating the merge request."
+msgid "SecurityReports|Comment edited on '%{vulnerabilityName}'"
msgstr ""
-msgid "Security Reports|There was an error deleting the comment."
+msgid "SecurityReports|Create issue"
msgstr ""
-msgid "Security Reports|There was an error dismissing the vulnerabilities."
+msgid "SecurityReports|Dismiss Selected"
msgstr ""
-msgid "Security Reports|There was an error dismissing the vulnerability."
+msgid "SecurityReports|Dismiss vulnerability"
msgstr ""
-msgid "Security Reports|There was an error reverting the dismissal."
+msgid "SecurityReports|Dismissed '%{vulnerabilityName}'"
msgstr ""
-msgid "Security Reports|There was an error reverting this dismissal."
+msgid "SecurityReports|Dismissed '%{vulnerabilityName}'. Turn off the hide dismissed toggle to view."
msgstr ""
-msgid "Security Reports|Undo dismiss"
+msgid "SecurityReports|Each vulnerability now has a unique page that can be directly linked to, shared, referenced, and tracked as the single source of truth. Vulnerability occurrences also persist across scanner runs, which improves tracking and visibility and reduces duplicates between scans."
msgstr ""
-msgid "Security Reports|You do not have sufficient permissions to access this report"
+msgid "SecurityReports|Edit dashboard"
msgstr ""
-msgid "Security Reports|You must sign in as an authorized user to see this report"
+msgid "SecurityReports|Either you don't have permission to view this dashboard or the dashboard has not been setup. Please check your permission settings with your administrator or check your dashboard configurations to proceed."
msgstr ""
-msgid "Security configuration help link"
+msgid "SecurityReports|Error fetching the vulnerability counts. Please check your network connection and try again."
msgstr ""
-msgid "Security dashboard"
+msgid "SecurityReports|Error fetching the vulnerability list. Please check your network connection and try again."
msgstr ""
-msgid "Security report is out of date. Please update your branch with the latest changes from the target branch (%{targetBranchName})"
+msgid "SecurityReports|False positive"
msgstr ""
-msgid "Security report is out of date. Run %{newPipelineLinkStart}a new pipeline%{newPipelineLinkEnd} for the target branch (%{targetBranchName})"
+msgid "SecurityReports|Hide dismissed"
msgstr ""
-msgid "SecurityConfiguration|Configured"
+msgid "SecurityReports|Introducing standalone vulnerabilities"
msgstr ""
-msgid "SecurityConfiguration|Feature"
+msgid "SecurityReports|Issue Created"
msgstr ""
-msgid "SecurityConfiguration|Feature documentation for %{featureName}"
+msgid "SecurityReports|Learn More"
msgstr ""
-msgid "SecurityConfiguration|Not yet configured"
+msgid "SecurityReports|Learn more about setting up your dashboard"
msgstr ""
-msgid "SecurityConfiguration|Secure features"
+msgid "SecurityReports|Load more vulnerabilities"
msgstr ""
-msgid "SecurityConfiguration|Status"
+msgid "SecurityReports|Monitor vulnerabilities in your code"
+msgstr ""
+
+msgid "SecurityReports|More info"
+msgstr ""
+
+msgid "SecurityReports|More information"
+msgstr ""
+
+msgid "SecurityReports|No vulnerabilities found for dashboard"
+msgstr ""
+
+msgid "SecurityReports|No vulnerabilities found for this group"
+msgstr ""
+
+msgid "SecurityReports|No vulnerabilities found for this pipeline"
+msgstr ""
+
+msgid "SecurityReports|No vulnerabilities found for this project"
+msgstr ""
+
+msgid "SecurityReports|Oops, something doesn't seem right."
+msgstr ""
+
+msgid "SecurityReports|Pipeline %{pipelineLink} triggered %{timeago} by %{user}"
msgstr ""
-msgid "SecurityDashboard|%{firstProject} and %{secondProject}"
+msgid "SecurityReports|Project"
msgstr ""
-msgid "SecurityDashboard|%{firstProject}, %{secondProject}, and %{rest}"
+msgid "SecurityReports|Projects added"
msgstr ""
-msgid "SecurityDashboard|Add a project to your dashboard"
+msgid "SecurityReports|Remove project from dashboard"
msgstr ""
-msgid "SecurityDashboard|Add or remove projects from your dashboard"
+msgid "SecurityReports|Report type"
msgstr ""
-msgid "SecurityDashboard|Add projects"
+msgid "SecurityReports|Return to dashboard"
msgstr ""
-msgid "SecurityDashboard|Edit dashboard"
+msgid "SecurityReports|Security Dashboard"
msgstr ""
-msgid "SecurityDashboard|Hide dismissed"
+msgid "SecurityReports|Security reports can only be accessed by authorized users."
msgstr ""
-msgid "SecurityDashboard|Monitor vulnerabilities in your code"
+msgid "SecurityReports|Select a project to add by using the project search field above."
msgstr ""
-msgid "SecurityDashboard|More information"
+msgid "SecurityReports|Select a reason"
msgstr ""
-msgid "SecurityDashboard|No vulnerabilities found for dashboard"
+msgid "SecurityReports|Severity"
msgstr ""
-msgid "SecurityDashboard|Pipeline %{pipelineLink} triggered %{timeago} by %{user}"
+msgid "SecurityReports|Status"
msgstr ""
-msgid "SecurityDashboard|Project"
+msgid "SecurityReports|The security dashboard displays the latest security findings for projects you wish to monitor. Select \"Edit dashboard\" to add and remove projects."
msgstr ""
-msgid "SecurityDashboard|Projects added"
+msgid "SecurityReports|The security dashboard displays the latest security report. Use it to find and fix vulnerabilities."
msgstr ""
-msgid "SecurityDashboard|Remove project from dashboard"
+msgid "SecurityReports|There was an error adding the comment."
msgstr ""
-msgid "SecurityDashboard|Report type"
+msgid "SecurityReports|There was an error creating the issue."
msgstr ""
-msgid "SecurityDashboard|Return to dashboard"
+msgid "SecurityReports|There was an error creating the merge request."
msgstr ""
-msgid "SecurityDashboard|Security Dashboard"
+msgid "SecurityReports|There was an error deleting the comment."
msgstr ""
-msgid "SecurityDashboard|Select a project to add by using the project search field above."
+msgid "SecurityReports|There was an error dismissing the vulnerabilities."
msgstr ""
-msgid "SecurityDashboard|Severity"
+msgid "SecurityReports|There was an error dismissing the vulnerability."
msgstr ""
-msgid "SecurityDashboard|Status"
+msgid "SecurityReports|There was an error reverting the dismissal."
msgstr ""
-msgid "SecurityDashboard|The security dashboard displays the latest security findings for projects you wish to monitor. Select \"Edit dashboard\" to add and remove projects."
+msgid "SecurityReports|There was an error reverting this dismissal."
msgstr ""
-msgid "SecurityDashboard|The security dashboard displays the latest security report. Use it to find and fix vulnerabilities."
+msgid "SecurityReports|There was an error while generating the report."
msgstr ""
-msgid "SecurityDashboard|There was an error while generating the report."
+msgid "SecurityReports|Unable to add %{invalidProjects}"
msgstr ""
-msgid "SecurityDashboard|Unable to add %{invalidProjects}"
+msgid "SecurityReports|Undo dismiss"
+msgstr ""
+
+msgid "SecurityReports|While it's rare to have no vulnerabilities for your group, it can happen. In any event, we ask that you double check your settings to make sure you've set up your dashboard correctly."
+msgstr ""
+
+msgid "SecurityReports|While it's rare to have no vulnerabilities for your pipeline, it can happen. In any event, we ask that you double check your settings to make sure all security scanning jobs have passed successfully."
+msgstr ""
+
+msgid "SecurityReports|While it's rare to have no vulnerabilities for your project, it can happen. In any event, we ask that you double check your settings to make sure you've set up your dashboard correctly."
+msgstr ""
+
+msgid "SecurityReports|While it's rare to have no vulnerabilities, it can happen. In any event, we ask that you please double check your settings to make sure you've set up your dashboard correctly."
+msgstr ""
+
+msgid "SecurityReports|Won't fix / Accept risk"
+msgstr ""
+
+msgid "SecurityReports|You do not have sufficient permissions to access this report"
+msgstr ""
+
+msgid "SecurityReports|You must sign in as an authorized user to see this report"
+msgstr ""
+
+msgid "SecurityReports|[No reason]"
msgstr ""
msgid "See GitLab's %{password_policy_guidelines}"
@@ -18173,6 +18599,9 @@ msgstr ""
msgid "See the affected projects in the GitLab admin panel"
msgstr ""
+msgid "See what's new at GitLab"
+msgstr ""
+
msgid "Select"
msgstr ""
@@ -18578,9 +19007,6 @@ msgstr ""
msgid "Set up Jira Integration"
msgstr ""
-msgid "Set up Jira Integration illustration"
-msgstr ""
-
msgid "Set up a %{type} Runner automatically"
msgstr ""
@@ -18665,6 +19091,9 @@ msgstr ""
msgid "Severity: %{severity}"
msgstr ""
+msgid "Shards (%{shards})"
+msgstr ""
+
msgid "Shards to synchronize"
msgstr ""
@@ -18980,6 +19409,9 @@ msgstr ""
msgid "Snowplow"
msgstr ""
+msgid "Some child epics may be hidden due to applied filters"
+msgstr ""
+
msgid "Some email servers do not support overriding the email sender name. Enable this option to include the name of the author of the issue, merge request or comment in the email body instead."
msgstr ""
@@ -19121,6 +19553,9 @@ msgstr ""
msgid "Something went wrong while updating your list settings"
msgstr ""
+msgid "Something went wrong with your automatic subscription renewal"
+msgstr ""
+
msgid "Something went wrong, unable to add %{project} to dashboard"
msgstr ""
@@ -19406,6 +19841,9 @@ msgstr ""
msgid "Stage removed"
msgstr ""
+msgid "Standard"
+msgstr ""
+
msgid "Star a label to make it a priority label. Order the prioritized labels to change their relative priority, by dragging."
msgstr ""
@@ -19529,6 +19967,9 @@ msgstr ""
msgid "Static Application Security Testing (SAST)"
msgstr ""
+msgid "StaticSiteEditor|An error occurred while submitting your changes."
+msgstr ""
+
msgid "StaticSiteEditor|Branch could not be created."
msgstr ""
@@ -19538,18 +19979,30 @@ msgstr ""
msgid "StaticSiteEditor|Could not create merge request."
msgstr ""
+msgid "StaticSiteEditor|Incompatible file content"
+msgstr ""
+
msgid "StaticSiteEditor|Return to site"
msgstr ""
+msgid "StaticSiteEditor|Static site editor"
+msgstr ""
+
msgid "StaticSiteEditor|Success!"
msgstr ""
msgid "StaticSiteEditor|Summary of changes"
msgstr ""
+msgid "StaticSiteEditor|The Static Site Editor is currently configured to only edit Markdown content on pages generated from Middleman. Visit the documentation to learn more about configuring your site to use the Static Site Editor."
+msgstr ""
+
msgid "StaticSiteEditor|Update %{sourcePath} file"
msgstr ""
+msgid "StaticSiteEditor|View documentation"
+msgstr ""
+
msgid "StaticSiteEditor|View merge request"
msgstr ""
@@ -19607,6 +20060,12 @@ msgstr ""
msgid "StatusPage|Status page"
msgstr ""
+msgid "StatusPage|Status page URL"
+msgstr ""
+
+msgid "StatusPage|Status page frontend documentation"
+msgstr ""
+
msgid "StatusPage|To publish incidents to an external status page, GitLab will store a JSON file in your Amazon S3 account in a location accessible to your external status page service. Make sure to also set up %{docsLink}"
msgstr ""
@@ -19955,6 +20414,12 @@ msgstr ""
msgid "Synced"
msgstr ""
+msgid "Synchronization disabled"
+msgstr ""
+
+msgid "Synchronization of container repositories is disabled."
+msgstr ""
+
msgid "System"
msgstr ""
@@ -20108,6 +20573,9 @@ msgstr ""
msgid "Telephone number"
msgstr ""
+msgid "Telephone number (Optional)"
+msgstr ""
+
msgid "Template"
msgstr ""
@@ -20417,6 +20885,9 @@ msgstr ""
msgid "The license was successfully uploaded and is now active. You can see the details below."
msgstr ""
+msgid "The license was successfully uploaded and will be active from %{starts_at}. You can see the details below."
+msgstr ""
+
msgid "The maximum file size allowed is %{size}."
msgstr ""
@@ -20738,6 +21209,9 @@ msgstr ""
msgid "There was an error getting the epic participants."
msgstr ""
+msgid "There was an error importing the Jira project."
+msgstr ""
+
msgid "There was an error loading users activity calendar."
msgstr ""
@@ -20753,7 +21227,7 @@ msgstr ""
msgid "There was an error resetting user pipeline minutes."
msgstr ""
-msgid "There was an error saving this Geo Node"
+msgid "There was an error saving this Geo Node."
msgstr ""
msgid "There was an error saving your changes."
@@ -20816,6 +21290,9 @@ msgstr ""
msgid "They can be managed using the %{link}."
msgstr ""
+msgid "Third Party Advisory Link"
+msgstr ""
+
msgid "Third party offers"
msgstr ""
@@ -20852,6 +21329,9 @@ msgstr ""
msgid "This application will be able to:"
msgstr ""
+msgid "This attachment has been truncated to avoid exceeding the maximum allowed attachment size of 15MB. %{written_count} of %{issues_count} issues have been included. Consider re-exporting with a narrower selection of issues."
+msgstr ""
+
msgid "This block is self-referential"
msgstr ""
@@ -20993,6 +21473,12 @@ msgstr ""
msgid "This issue is confidential"
msgstr ""
+msgid "This issue is currently blocked by the following issues: %{issues}."
+msgstr ""
+
+msgid "This issue is in a child epic of the filtered epic"
+msgstr ""
+
msgid "This issue is locked."
msgstr ""
@@ -21191,7 +21677,10 @@ msgstr ""
msgid "This user will be the author of all events in the activity feed that are the result of an update, like new branches being created or new commits being pushed to existing branches. Upon creation or when reassigning you can only assign yourself to be the mirror user."
msgstr ""
-msgid "This variable can not be masked"
+msgid "This variable can not be masked."
+msgstr ""
+
+msgid "This variable does not match the expected pattern."
msgstr ""
msgid "This will help us personalize your onboarding experience."
@@ -21221,10 +21710,10 @@ msgstr ""
msgid "ThreatMonitoring|Application firewall not detected"
msgstr ""
-msgid "ThreatMonitoring|Container Network Policy"
+msgid "ThreatMonitoring|Container Network Policies are not installed or have been disabled. To view this data, ensure your Network Policies are installed and enabled for your cluster."
msgstr ""
-msgid "ThreatMonitoring|Container NetworkPolicies are not installed or has been disabled. To view this data, ensure you NetworkPolicies are installed and enabled for your cluster."
+msgid "ThreatMonitoring|Container Network Policy"
msgstr ""
msgid "ThreatMonitoring|Container NetworkPolicies not detected"
@@ -21257,7 +21746,7 @@ msgstr ""
msgid "ThreatMonitoring|Something went wrong, unable to fetch statistics"
msgstr ""
-msgid "ThreatMonitoring|The firewall is not installed or has been disabled. To view this data, ensure you firewall is installed and enabled for your cluster."
+msgid "ThreatMonitoring|The firewall is not installed or has been disabled. To view this data, ensure the web application firewall is installed and enabled for your cluster."
msgstr ""
msgid "ThreatMonitoring|The graph below is an overview of traffic coming to your application as tracked by the Web Application Firewall (WAF). View the docs for instructions on how to access the WAF logs to see what type of malicious traffic is trying to access your app. The docs link is also accessible by clicking the \"?\" icon next to the title below."
@@ -21326,6 +21815,9 @@ msgstr ""
msgid "Time in seconds GitLab will wait for a response from the external service. When the service does not respond in time, access will be denied."
msgstr ""
+msgid "Time of import: %{importTime}"
+msgstr ""
+
msgid "Time remaining"
msgstr ""
@@ -21512,7 +22004,7 @@ msgstr ""
msgid "Title:"
msgstr ""
-msgid "Titles and Filenames"
+msgid "Titles and Descriptions"
msgstr ""
msgid "To"
@@ -21719,7 +22211,7 @@ msgstr ""
msgid "Too many projects enabled. You will need to manage them via the console or the API."
msgstr ""
-msgid "Topics"
+msgid "Topics (optional)"
msgstr ""
msgid "Total"
@@ -21881,6 +22373,9 @@ msgstr ""
msgid "Try all GitLab has to offer for 30 days."
msgstr ""
+msgid "Try changing or removing filters."
+msgstr ""
+
msgid "Try to fork again"
msgstr ""
@@ -21974,6 +22469,9 @@ msgstr ""
msgid "Unable to connect to server: %{error}"
msgstr ""
+msgid "Unable to connect to the Jira instance. Please check your Jira integration configuration."
+msgstr ""
+
msgid "Unable to convert Kubernetes logs encoding to UTF-8"
msgstr ""
@@ -22919,6 +23417,9 @@ msgstr ""
msgid "View issue"
msgstr ""
+msgid "View issues"
+msgstr ""
+
msgid "View it on GitLab"
msgstr ""
@@ -23096,6 +23597,15 @@ msgstr ""
msgid "VulnerabilityManagement|Resolved %{timeago} by %{user}"
msgstr ""
+msgid "VulnerabilityManagement|Something went wrong while trying to delete the comment. Please try again later."
+msgstr ""
+
+msgid "VulnerabilityManagement|Something went wrong while trying to retrieve the vulnerability history. Please try again later."
+msgstr ""
+
+msgid "VulnerabilityManagement|Something went wrong while trying to save the comment. Please try again later."
+msgstr ""
+
msgid "VulnerabilityManagement|Something went wrong, could not create an issue."
msgstr ""
@@ -23126,6 +23636,9 @@ msgstr ""
msgid "VulnerabilityStatusTypes|Resolved"
msgstr ""
+msgid "Vulnerability|%{scannerName} (version %{scannerVersion})"
+msgstr ""
+
msgid "Vulnerability|Class"
msgstr ""
@@ -23159,7 +23672,10 @@ msgstr ""
msgid "Vulnerability|Project"
msgstr ""
-msgid "Vulnerability|Report Type"
+msgid "Vulnerability|Scanner Provider"
+msgstr ""
+
+msgid "Vulnerability|Scanner Type"
msgstr ""
msgid "Vulnerability|Severity"
@@ -23213,9 +23729,15 @@ msgstr ""
msgid "We sent you an email with reset password instructions"
msgstr ""
+msgid "We tried to automatically renew your %{strong}%{plan_name}%{strong_close} subscription for %{strong}%{namespace_name}%{strong_close} on %{expires_on} but something went wrong so your subscription was downgraded to the free plan. Don't worry, your data is safe. We suggest you check your payment method and get in touch with our support team (%{support_link}). They'll gladly help with your subscription renewal."
+msgstr ""
+
msgid "We want to be sure it is you, please confirm you are not a robot."
msgstr ""
+msgid "We will automatically renew your %{strong}%{plan_name}%{strong_close} subscription for %{strong}%{namespace_name}%{strong_close} on %{strong}%{expires_on}%{strong_close}. There's nothing that you need to do, we'll let you know when the renewal is complete. Need more seats, a higher plan or just want to review your payment method?"
+msgstr ""
+
msgid "We've found no vulnerabilities"
msgstr ""
@@ -23321,15 +23843,6 @@ msgstr ""
msgid "When:"
msgstr ""
-msgid "While it's rare to have no vulnerabilities for your group, it can happen. In any event, we ask that you double check your settings to make sure you've set up your dashboard correctly."
-msgstr ""
-
-msgid "While it's rare to have no vulnerabilities for your pipeline, it can happen. In any event, we ask that you double check your settings to make sure all security scanning jobs have passed successfully."
-msgstr ""
-
-msgid "While it's rare to have no vulnerabilities for your project, it can happen. In any event, we ask that you double check your settings to make sure you've set up your dashboard correctly."
-msgstr ""
-
msgid "While it's rare to have no vulnerabilities, it can happen. In any event, we ask that you please double check your settings to make sure you've set up your dashboard correctly."
msgstr ""
@@ -23360,9 +23873,6 @@ msgstr ""
msgid "Wiki"
msgstr ""
-msgid "Wiki pages"
-msgstr ""
-
msgid "Wiki was successfully updated."
msgstr ""
@@ -23546,6 +24056,9 @@ msgstr ""
msgid "Yes, add it"
msgstr ""
+msgid "Yes, close issue"
+msgstr ""
+
msgid "Yes, let me map Google Code users to full names or GitLab users."
msgstr ""
@@ -23741,6 +24254,9 @@ msgstr ""
msgid "You could not create a new trigger."
msgstr ""
+msgid "You didn't renew your %{strong}%{plan_name}%{strong_close} subscription for %{strong}%{namespace_name}%{strong_close} so it was downgraded to the free plan."
+msgstr ""
+
msgid "You didn't renew your %{strong}%{plan_name}%{strong_close} subscription so it was downgraded to the GitLab Core Plan."
msgstr ""
@@ -23807,6 +24323,9 @@ msgstr ""
msgid "You have declined the invitation to join %{label}."
msgstr ""
+msgid "You have imported from this project %{numberOfPreviousImportsForProject} times before. Each new import will create duplicate issues."
+msgstr ""
+
msgid "You have no permissions"
msgstr ""
@@ -23888,6 +24407,9 @@ msgstr ""
msgid "You need to upload a Google Takeout archive."
msgstr ""
+msgid "You reached %{usage_in_percent} of %{namespace_name}'s capacity (%{used_storage} of %{storage_limit})"
+msgstr ""
+
msgid "You tried to fork %{link_to_the_project} but it failed for the following reason:"
msgstr ""
@@ -23972,9 +24494,21 @@ msgstr ""
msgid "YouTube"
msgstr ""
+msgid "Your %{strong}%{plan_name}%{strong_close} subscription for %{strong}%{namespace_name}%{strong_close} will expire on %{strong}%{expires_on}%{strong_close}. After that, you will not to be able to create issues or merge requests as well as many other features."
+msgstr ""
+
msgid "Your %{strong}%{plan_name}%{strong_close} subscription will expire on %{strong}%{expires_on}%{strong_close}. After that, you will not to be able to create issues or merge requests as well as many other features."
msgstr ""
+msgid "Your CSV export has started. It will be emailed to %{email} when complete."
+msgstr ""
+
+msgid "Your CSV export of %{issues_count} from project %{project_link} has been added to this email as an attachment."
+msgstr ""
+
+msgid "Your CSV export of %{written_count} from project %{project_name} (%{project_url}) has been added to this email as an attachment."
+msgstr ""
+
msgid "Your Commit Email will be used for web based operations, such as edits and merges."
msgstr ""
@@ -24071,6 +24605,9 @@ msgstr ""
msgid "Your comment could not be updated! Please check your network connection and try again."
msgstr ""
+msgid "Your comment will be discarded."
+msgstr ""
+
msgid "Your custom stage '%{title}' was created"
msgstr ""
@@ -24110,6 +24647,9 @@ msgstr ""
msgid "Your new personal access token has been created."
msgstr ""
+msgid "Your new project access token has been created."
+msgstr ""
+
msgid "Your password isn't required to view this page. If a password or any other personal details are requested, please contact your administrator to report abuse."
msgstr ""
@@ -24128,12 +24668,18 @@ msgstr ""
msgid "Your request for access has been queued for review."
msgstr ""
+msgid "Your search didn't match any commits."
+msgstr ""
+
msgid "Your subscription expired!"
msgstr ""
msgid "Your subscription has been downgraded"
msgstr ""
+msgid "Your subscription will automatically renew in %{remaining_days}"
+msgstr ""
+
msgid "Your subscription will expire in %{remaining_days}"
msgstr ""
@@ -24173,7 +24719,7 @@ msgstr ""
msgid "allowed to fail"
msgstr ""
-msgid "already being used for another group or project milestone."
+msgid "already being used for another group or project %{timebox_name}."
msgstr ""
msgid "already has a \"created\" issue link"
@@ -24251,6 +24797,9 @@ msgstr ""
msgid "ciReport|%{linkStartTag}Learn more about SAST %{linkEndTag}"
msgstr ""
+msgid "ciReport|%{linkStartTag}Learn more about Secret Scanning %{linkEndTag}"
+msgstr ""
+
msgid "ciReport|%{linkStartTag}Learn more about codequality reports %{linkEndTag}"
msgstr ""
@@ -24419,6 +24968,12 @@ msgstr ""
msgid "ciReport|SAST"
msgstr ""
+msgid "ciReport|Secret scanning"
+msgstr ""
+
+msgid "ciReport|Secret scanning detects secrets and credentials vulnerabilities in your source code."
+msgstr ""
+
msgid "ciReport|Security scanning"
msgstr ""
@@ -24620,6 +25175,9 @@ msgstr ""
msgid "group"
msgstr ""
+msgid "groups"
+msgstr ""
+
msgid "has already been linked to another vulnerability"
msgstr ""
@@ -24785,9 +25343,6 @@ msgstr[2] ""
msgid "merged %{time_ago}"
msgstr ""
-msgid "milestone should belong either to a project or a group."
-msgstr ""
-
msgid "missing"
msgstr ""
@@ -25124,6 +25679,9 @@ msgstr ""
msgid "no contributions"
msgstr ""
+msgid "no expiration"
+msgstr ""
+
msgid "no one can merge"
msgstr ""
@@ -25178,6 +25736,9 @@ msgstr ""
msgid "pending removal"
msgstr ""
+msgid "per day"
+msgstr ""
+
msgid "pipeline"
msgstr ""
@@ -25208,6 +25769,12 @@ msgstr[2] ""
msgid "project avatar"
msgstr ""
+msgid "projects"
+msgstr ""
+
+msgid "push to your repository, create pipelines, create issues or add comments. To reduce storage capacity, delete unused repositories, artifacts, wikis, issues, and pipelines."
+msgstr ""
+
msgid "quick actions"
msgstr ""
@@ -25355,12 +25922,6 @@ msgstr ""
msgid "this document"
msgstr ""
-msgid "thread resolved"
-msgid_plural "threads resolved"
-msgstr[0] ""
-msgstr[1] ""
-msgstr[2] ""
-
msgid "to help your contributors communicate effectively!"
msgstr ""
@@ -25415,6 +25976,9 @@ msgstr ""
msgid "view the blob"
msgstr ""
+msgid "vulnerability|Add a comment"
+msgstr ""
+
msgid "vulnerability|Add a comment or reason for dismissal"
msgstr ""
diff --git a/locale/sr_SP/gitlab.po b/locale/sr_SP/gitlab.po
index 8bcbed5adcc..b856d8a60ea 100644
--- a/locale/sr_SP/gitlab.po
+++ b/locale/sr_SP/gitlab.po
@@ -12,7 +12,7 @@ msgstr ""
"X-Crowdin-Project: gitlab-ee\n"
"X-Crowdin-Language: sr\n"
"X-Crowdin-File: /master/locale/gitlab.pot\n"
-"PO-Revision-Date: 2020-04-15 00:27\n"
+"PO-Revision-Date: 2020-05-05 21:36\n"
msgid " %{start} to %{end}"
msgstr ""
@@ -242,6 +242,12 @@ msgstr[0] ""
msgstr[1] ""
msgstr[2] ""
+msgid "%d unresolved thread"
+msgid_plural "%d unresolved threads"
+msgstr[0] ""
+msgstr[1] ""
+msgstr[2] ""
+
msgid "%d vulnerability dismissed"
msgid_plural "%d vulnerabilities dismissed"
msgstr[0] ""
@@ -407,6 +413,9 @@ msgstr ""
msgid "%{mrText}, this issue will be closed automatically."
msgstr ""
+msgid "%{namespace_name} is now read-only. You cannot: %{base_message}"
+msgstr ""
+
msgid "%{name} contained %{resultsString}"
msgstr ""
@@ -419,6 +428,12 @@ msgstr ""
msgid "%{name}'s avatar"
msgstr ""
+msgid "%{no_of_days} day"
+msgid_plural "%{no_of_days} days"
+msgstr[0] ""
+msgstr[1] ""
+msgstr[2] ""
+
msgid "%{number_commits_behind} commits behind %{default_branch}, %{number_commits_ahead} commits ahead"
msgstr ""
@@ -548,6 +563,9 @@ msgstr[2] ""
msgid "%{text} is available"
msgstr ""
+msgid "%{timebox_name} should belong either to a project or a group."
+msgstr ""
+
msgid "%{title} %{operator} %{threshold}"
msgstr ""
@@ -842,12 +860,18 @@ msgstr ""
msgid "<code>\"johnsmith@example.com\": \"johnsmith@example.com\"</code> will add \"By <a href=\"#\">johnsmith@example.com</a>\" to all issues and comments originally created by johnsmith@example.com. By default, the email address or username is masked to ensure the user's privacy. Use this option if you want to show the full email address."
msgstr ""
+msgid "<namespace / project>"
+msgstr ""
+
msgid "<no name set>"
msgstr ""
msgid "<no scopes selected>"
msgstr ""
+msgid "<project name>"
+msgstr ""
+
msgid "<strong>%{group_name}</strong> group members"
msgstr ""
@@ -881,6 +905,9 @@ msgstr ""
msgid "A basic page and serverless function that uses AWS Lambda, AWS API Gateway, and GitLab Pages"
msgstr ""
+msgid "A complete DevOps platform"
+msgstr ""
+
msgid "A default branch cannot be chosen for an empty project."
msgstr ""
@@ -1097,6 +1124,12 @@ msgstr ""
msgid "AccessTokens|reset it"
msgstr ""
+msgid "AccessibilityReport|Accessibility report artifact not found"
+msgstr ""
+
+msgid "AccessibilityReport|Failed to retrieve accessibility report"
+msgstr ""
+
msgid "AccessibilityReport|Learn More"
msgstr ""
@@ -1106,6 +1139,9 @@ msgstr ""
msgid "AccessibilityReport|New"
msgstr ""
+msgid "AccessibilityReport|The accessibility scanning found an error of the following type: %{code}"
+msgstr ""
+
msgid "Account"
msgstr ""
@@ -1349,6 +1385,9 @@ msgstr ""
msgid "Added at"
msgstr ""
+msgid "Added for this merge request"
+msgstr ""
+
msgid "Added in this version"
msgstr ""
@@ -1721,6 +1760,72 @@ msgstr[0] ""
msgstr[1] ""
msgstr[2] ""
+msgid "Alert Details"
+msgstr ""
+
+msgid "AlertManagement|Acknowledged"
+msgstr ""
+
+msgid "AlertManagement|Alert"
+msgstr ""
+
+msgid "AlertManagement|Authorize external service"
+msgstr ""
+
+msgid "AlertManagement|Display alerts from all your monitoring tools directly within GitLab. Streamline the investigation of your alerts and the escalation of alerts to incidents."
+msgstr ""
+
+msgid "AlertManagement|End time"
+msgstr ""
+
+msgid "AlertManagement|End time:"
+msgstr ""
+
+msgid "AlertManagement|Events"
+msgstr ""
+
+msgid "AlertManagement|Events:"
+msgstr ""
+
+msgid "AlertManagement|Full Alert Details"
+msgstr ""
+
+msgid "AlertManagement|More information"
+msgstr ""
+
+msgid "AlertManagement|No alerts available to display. If you think you're seeing this message in error, refresh the page."
+msgstr ""
+
+msgid "AlertManagement|No alerts to display."
+msgstr ""
+
+msgid "AlertManagement|Overview"
+msgstr ""
+
+msgid "AlertManagement|Resolved"
+msgstr ""
+
+msgid "AlertManagement|Severity"
+msgstr ""
+
+msgid "AlertManagement|Start time"
+msgstr ""
+
+msgid "AlertManagement|Start time:"
+msgstr ""
+
+msgid "AlertManagement|Status"
+msgstr ""
+
+msgid "AlertManagement|Surface alerts in GitLab"
+msgstr ""
+
+msgid "AlertManagement|There was an error displaying the alerts. Confirm your endpoint's configuration details to ensure alerts appear."
+msgstr ""
+
+msgid "AlertManagement|Triggered"
+msgstr ""
+
msgid "AlertService|%{linkStart}Learn more%{linkEnd} about configuring this endpoint to receive alerts."
msgstr ""
@@ -1790,6 +1895,9 @@ msgstr ""
msgid "All security scans are enabled because %{linkStart}Auto DevOps%{linkEnd} is enabled on this project"
msgstr ""
+msgid "All threads resolved"
+msgstr ""
+
msgid "All users"
msgstr ""
@@ -1808,6 +1916,9 @@ msgstr ""
msgid "Allow only the selected protocols to be used for Git access."
msgstr ""
+msgid "Allow owners to manage default branch protection per group"
+msgstr ""
+
msgid "Allow owners to manually add users outside of LDAP"
msgstr ""
@@ -1916,6 +2027,9 @@ msgstr ""
msgid "An error occurred fetching the dropdown data."
msgstr ""
+msgid "An error occurred fetching the project authors."
+msgstr ""
+
msgid "An error occurred previewing the blob"
msgstr ""
@@ -1997,6 +2111,9 @@ msgstr ""
msgid "An error occurred while fetching sidebar data"
msgstr ""
+msgid "An error occurred while fetching terraform reports."
+msgstr ""
+
msgid "An error occurred while fetching the Service Desk address."
msgstr ""
@@ -2234,6 +2351,9 @@ msgstr ""
msgid "Any"
msgstr ""
+msgid "Any Author"
+msgstr ""
+
msgid "Any Label"
msgstr ""
@@ -2453,10 +2573,10 @@ msgstr ""
msgid "Are you sure that you want to unarchive this project?"
msgstr ""
-msgid "Are you sure you want to cancel creating this comment?"
+msgid "Are you sure you want to cancel editing this comment?"
msgstr ""
-msgid "Are you sure you want to cancel editing this comment?"
+msgid "Are you sure you want to close this blocked issue?"
msgstr ""
msgid "Are you sure you want to delete %{name}?"
@@ -2486,6 +2606,9 @@ msgstr ""
msgid "Are you sure you want to deploy this environment?"
msgstr ""
+msgid "Are you sure you want to discard this comment?"
+msgstr ""
+
msgid "Are you sure you want to erase this build?"
msgstr ""
@@ -2663,6 +2786,9 @@ msgstr ""
msgid "At least one approval from a code owner is required to change files matching the respective CODEOWNER rules."
msgstr ""
+msgid "At least one logging option is required to be enabled"
+msgstr ""
+
msgid "At least one of group_id or project_id must be specified"
msgstr ""
@@ -2702,6 +2828,27 @@ msgstr ""
msgid "AuditEvents|Target"
msgstr ""
+msgid "AuditLogs|(removed)"
+msgstr ""
+
+msgid "AuditLogs|Action"
+msgstr ""
+
+msgid "AuditLogs|Author"
+msgstr ""
+
+msgid "AuditLogs|Date"
+msgstr ""
+
+msgid "AuditLogs|IP Address"
+msgstr ""
+
+msgid "AuditLogs|Object"
+msgstr ""
+
+msgid "AuditLogs|Target"
+msgstr ""
+
msgid "Aug"
msgstr ""
@@ -2984,9 +3131,6 @@ msgstr ""
msgid "BambooService|You must set up automatic revision labeling and a repository trigger in Bamboo."
msgstr ""
-msgid "Batch operations"
-msgstr ""
-
msgid "BatchComments|Delete all pending comments"
msgstr ""
@@ -3116,6 +3260,9 @@ msgstr ""
msgid "Boards"
msgstr ""
+msgid "Boards and Board Lists"
+msgstr ""
+
msgid "Boards|Collapse"
msgstr ""
@@ -3335,6 +3482,9 @@ msgstr ""
msgid "Buy GitLab Enterprise Edition"
msgstr ""
+msgid "Buy more Pipeline minutes"
+msgstr ""
+
msgid "By %{user_name}"
msgstr ""
@@ -3455,6 +3605,9 @@ msgstr ""
msgid "Can't scan the code?"
msgstr ""
+msgid "Can't update snippet: %{err}"
+msgstr ""
+
msgid "Canary"
msgstr ""
@@ -3623,6 +3776,9 @@ msgstr ""
msgid "Changing group path can have unintended side effects."
msgstr ""
+msgid "Charts"
+msgstr ""
+
msgid "Charts can't be displayed as the request for data has timed out. %{documentationLink}"
msgstr ""
@@ -3659,9 +3815,6 @@ msgstr ""
msgid "ChatMessage|and [%{count} more](%{pipeline_failed_jobs_url})"
msgstr ""
-msgid "ChatMessage|failed"
-msgstr ""
-
msgid "ChatMessage|has failed"
msgstr ""
@@ -3677,9 +3830,6 @@ msgstr ""
msgid "ChatMessage|in %{project_link}"
msgstr ""
-msgid "ChatMessage|passed"
-msgstr ""
-
msgid "Check again"
msgstr ""
@@ -4130,9 +4280,6 @@ msgstr ""
msgid "Click any <strong>project name</strong> in the project list below to navigate to the project milestone."
msgstr ""
-msgid "Click here"
-msgstr ""
-
msgid "Click the <strong>Download</strong> button and wait for downloading to complete."
msgstr ""
@@ -4520,6 +4667,12 @@ msgstr ""
msgid "ClusterIntegration|Fetching zones"
msgstr ""
+msgid "ClusterIntegration|Fluentd"
+msgstr ""
+
+msgid "ClusterIntegration|Fluentd is an open source data collector, which lets you unify the data collection and consumption for a better use and understanding of data. It requires at least one of the following logs to be successfully installed."
+msgstr ""
+
msgid "ClusterIntegration|GitLab Integration"
msgstr ""
@@ -4826,6 +4979,15 @@ msgstr ""
msgid "ClusterIntegration|Request to begin uninstalling failed"
msgstr ""
+msgid "ClusterIntegration|SIEM Hostname"
+msgstr ""
+
+msgid "ClusterIntegration|SIEM Port"
+msgstr ""
+
+msgid "ClusterIntegration|SIEM Protocol"
+msgstr ""
+
msgid "ClusterIntegration|Save changes"
msgstr ""
@@ -4922,6 +5084,12 @@ msgstr ""
msgid "ClusterIntegration|Select zone to choose machine type"
msgstr ""
+msgid "ClusterIntegration|Send Cilium Logs"
+msgstr ""
+
+msgid "ClusterIntegration|Send ModSecurity Logs"
+msgstr ""
+
msgid "ClusterIntegration|Service Token"
msgstr ""
@@ -5351,18 +5519,33 @@ msgstr ""
msgid "Compliance framework (optional)"
msgstr ""
+msgid "ComplianceFramework|GDPR"
+msgstr ""
+
msgid "ComplianceFramework|GDPR - General Data Protection Regulation"
msgstr ""
+msgid "ComplianceFramework|HIPAA"
+msgstr ""
+
msgid "ComplianceFramework|HIPAA - Health Insurance Portability and Accountability Act"
msgstr ""
+msgid "ComplianceFramework|PCI-DSS"
+msgstr ""
+
msgid "ComplianceFramework|PCI-DSS - Payment Card Industry-Data Security Standard"
msgstr ""
+msgid "ComplianceFramework|SOC 2"
+msgstr ""
+
msgid "ComplianceFramework|SOC 2 - Service Organization Control 2"
msgstr ""
+msgid "ComplianceFramework|SOX"
+msgstr ""
+
msgid "ComplianceFramework|SOX - Sarbanes-Oxley"
msgstr ""
@@ -5510,9 +5693,15 @@ msgstr ""
msgid "Container repositories sync capacity"
msgstr ""
+msgid "ContainerRegistry| Please visit the %{linkStart}administration settings%{linkEnd} to enable this feature."
+msgstr ""
+
msgid "ContainerRegistry|%{imageName} tags"
msgstr ""
+msgid "ContainerRegistry|%{title} was successfully scheduled for deletion"
+msgstr ""
+
msgid "ContainerRegistry|Automatically remove extra images that aren't designed to be kept."
msgstr ""
@@ -5525,6 +5714,9 @@ msgstr ""
msgid "ContainerRegistry|Container Registry"
msgstr ""
+msgid "ContainerRegistry|Container Registry tag expiration and retention policy is disabled"
+msgstr ""
+
msgid "ContainerRegistry|Copy build command"
msgstr ""
@@ -5534,18 +5726,12 @@ msgstr ""
msgid "ContainerRegistry|Copy push command"
msgstr ""
-msgid "ContainerRegistry|Currently, the Container Registry tag expiration feature is not available for projects created before GitLab version 12.8. For updates and more information, visit Issue %{linkStart}#196124%{linkEnd}"
-msgstr ""
-
msgid "ContainerRegistry|Docker connection error"
msgstr ""
msgid "ContainerRegistry|Docker tag expiration policy is %{toggleStatus}"
msgstr ""
-msgid "ContainerRegistry|Docker tags with names matching this regex pattern will expire:"
-msgstr ""
-
msgid "ContainerRegistry|Edit Settings"
msgstr ""
@@ -5567,9 +5753,6 @@ msgstr ""
msgid "ContainerRegistry|Image ID"
msgstr ""
-msgid "ContainerRegistry|Image deleted successfully"
-msgstr ""
-
msgid "ContainerRegistry|Keep and protect the images that matter most."
msgstr ""
@@ -5585,12 +5768,24 @@ msgstr ""
msgid "ContainerRegistry|Number of tags to retain:"
msgstr ""
+msgid "ContainerRegistry|Please contact your administrator."
+msgstr ""
+
msgid "ContainerRegistry|Push an image"
msgstr ""
msgid "ContainerRegistry|Quick Start"
msgstr ""
+msgid "ContainerRegistry|Regular expressions such as %{codeStart}.*-test%{codeEnd} or %{codeStart}dev-.*%{codeEnd} are supported"
+msgstr ""
+
+msgid "ContainerRegistry|Regular expressions such as %{codeStart}.*-test%{codeEnd} or %{codeStart}dev-.*%{codeEnd} are supported. To select all tags, use %{codeStart}.*%{codeEnd}"
+msgstr ""
+
+msgid "ContainerRegistry|Remember to run %{docLinkStart}garbage collection%{docLinkEnd} to remove the stale data from storage."
+msgstr ""
+
msgid "ContainerRegistry|Remove repository"
msgstr ""
@@ -5606,22 +5801,22 @@ msgstr[2] ""
msgid "ContainerRegistry|Retention policy has been Enabled"
msgstr ""
-msgid "ContainerRegistry|Something went wrong while deleting the image."
+msgid "ContainerRegistry|Something went wrong while fetching the expiration policy."
msgstr ""
-msgid "ContainerRegistry|Something went wrong while deleting the tag."
+msgid "ContainerRegistry|Something went wrong while fetching the repository list."
msgstr ""
-msgid "ContainerRegistry|Something went wrong while deleting the tags."
+msgid "ContainerRegistry|Something went wrong while fetching the tags list."
msgstr ""
-msgid "ContainerRegistry|Something went wrong while fetching the expiration policy."
+msgid "ContainerRegistry|Something went wrong while marking the tag for deletion."
msgstr ""
-msgid "ContainerRegistry|Something went wrong while fetching the packages list."
+msgid "ContainerRegistry|Something went wrong while marking the tags for deletion."
msgstr ""
-msgid "ContainerRegistry|Something went wrong while fetching the tags list."
+msgid "ContainerRegistry|Something went wrong while scheduling %{title} for deletion. Please try again."
msgstr ""
msgid "ContainerRegistry|Something went wrong while updating the expiration policy."
@@ -5630,16 +5825,25 @@ msgstr ""
msgid "ContainerRegistry|Tag"
msgstr ""
-msgid "ContainerRegistry|Tag deleted successfully"
-msgstr ""
-
msgid "ContainerRegistry|Tag expiration policy"
msgstr ""
msgid "ContainerRegistry|Tag expiration policy is designed to:"
msgstr ""
-msgid "ContainerRegistry|Tags deleted successfully"
+msgid "ContainerRegistry|Tag successfully marked for deletion."
+msgstr ""
+
+msgid "ContainerRegistry|Tags successfully marked for deletion."
+msgstr ""
+
+msgid "ContainerRegistry|Tags with names matching this regex pattern will %{italicStart}be preserved:%{italicEnd}"
+msgstr ""
+
+msgid "ContainerRegistry|Tags with names matching this regex pattern will %{italicStart}expire:%{italicEnd}"
+msgstr ""
+
+msgid "ContainerRegistry|The Container Registry tag expiration and retention policies for this project have not been enabled."
msgstr ""
msgid "ContainerRegistry|The last tag related to this image was recently removed. This empty image and any associated data will be automatically removed as part of the regular Garbage Collection process. If you have any questions, contact your administrator."
@@ -5660,16 +5864,16 @@ msgstr ""
msgid "ContainerRegistry|There are no container images stored for this project"
msgstr ""
-msgid "ContainerRegistry|This Registry contains deleted image tag data. Remember to run %{docLinkStart}garbage collection%{docLinkEnd} to remove the stale data from storage."
+msgid "ContainerRegistry|There was an error during the deletion of this image repository, please try again."
msgstr ""
msgid "ContainerRegistry|This image has no active tags"
msgstr ""
-msgid "ContainerRegistry|We are having trouble connecting to Docker, which could be due to an issue with your project name or path. %{docLinkStart}More Information%{docLinkEnd}"
+msgid "ContainerRegistry|This image repository is scheduled for deletion"
msgstr ""
-msgid "ContainerRegistry|Wildcards such as %{codeStart}.*-stable%{codeEnd} or %{codeStart}production/.*%{codeEnd} are supported. To select all tags, use %{codeStart}.*%{codeEnd}"
+msgid "ContainerRegistry|We are having trouble connecting to Docker, which could be due to an issue with your project name or path. %{docLinkStart}More Information%{docLinkEnd}"
msgstr ""
msgid "ContainerRegistry|With the Container Registry, every project can have its own space to store its Docker images. %{docLinkStart}More Information%{docLinkEnd}"
@@ -5924,7 +6128,7 @@ msgstr ""
msgid "Could not delete chat nickname %{chat_name}."
msgstr ""
-msgid "Could not find design"
+msgid "Could not find design."
msgstr ""
msgid "Could not remove the trigger."
@@ -5939,6 +6143,9 @@ msgstr ""
msgid "Could not revoke personal access token %{personal_access_token_name}."
msgstr ""
+msgid "Could not revoke project access token %{project_access_token_name}."
+msgstr ""
+
msgid "Could not save group ID"
msgstr ""
@@ -5996,6 +6203,9 @@ msgstr ""
msgid "Create a new branch"
msgstr ""
+msgid "Create a new deploy key for this project"
+msgstr ""
+
msgid "Create a new file as there are no files yet. Afterwards, you'll be able to commit your changes."
msgstr ""
@@ -6101,6 +6311,9 @@ msgstr ""
msgid "Create requirement"
msgstr ""
+msgid "Create snippet"
+msgstr ""
+
msgid "Create wildcard: %{searchTerm}"
msgstr ""
@@ -6122,6 +6335,9 @@ msgstr ""
msgid "Created"
msgstr ""
+msgid "Created %{timestamp}"
+msgstr ""
+
msgid "Created At"
msgstr ""
@@ -6479,6 +6695,9 @@ msgstr ""
msgid "CycleAnalytics|stage dropdown"
msgstr ""
+msgid "DAG"
+msgstr ""
+
msgid "DNS"
msgstr ""
@@ -6851,6 +7070,9 @@ msgstr ""
msgid "Deploy key was successfully updated."
msgstr ""
+msgid "Deploy keys allow read-only or read-write (if enabled) access to your repository. Deploy keys can be used for CI, staging or production servers. You can create a deploy key or add an existing one."
+msgstr ""
+
msgid "Deploy progress not found. To see pods, ensure your environment matches %{linkStart}deploy board criteria%{linkEnd}."
msgstr ""
@@ -7004,6 +7226,9 @@ msgstr ""
msgid "Deploying to"
msgstr ""
+msgid "Deployment Frequency"
+msgstr ""
+
msgid "Deployment|API"
msgstr ""
@@ -7523,9 +7748,6 @@ msgstr ""
msgid "Edit environment"
msgstr ""
-msgid "Edit epic description"
-msgstr ""
-
msgid "Edit file"
msgstr ""
@@ -8147,9 +8369,6 @@ msgstr ""
msgid "Epics"
msgstr ""
-msgid "Epics (Ultimate / Gold license only)"
-msgstr ""
-
msgid "Epics Roadmap"
msgstr ""
@@ -8159,6 +8378,9 @@ msgstr ""
msgid "Epics let you manage your portfolio of projects more efficiently and with less effort"
msgstr ""
+msgid "Epics, Issues, and Merge Requests"
+msgstr ""
+
msgid "Epics|Add an epic"
msgstr ""
@@ -8210,6 +8432,9 @@ msgstr ""
msgid "Epics|Something went wrong while fetching group epics."
msgstr ""
+msgid "Epics|Something went wrong while moving item."
+msgstr ""
+
msgid "Epics|Something went wrong while ordering item."
msgstr ""
@@ -8264,6 +8489,9 @@ msgstr ""
msgid "Error fetching network graph."
msgstr ""
+msgid "Error fetching payload data."
+msgstr ""
+
msgid "Error fetching projects"
msgstr ""
@@ -8273,9 +8501,6 @@ msgstr ""
msgid "Error fetching the dependency list. Please check your network connection and try again."
msgstr ""
-msgid "Error fetching usage ping data."
-msgstr ""
-
msgid "Error loading branch data. Please try again."
msgstr ""
@@ -8417,6 +8642,9 @@ msgstr ""
msgid "Errors"
msgstr ""
+msgid "Errors:"
+msgstr ""
+
msgid "Estimated"
msgstr ""
@@ -8621,12 +8849,18 @@ msgstr ""
msgid "Export as CSV"
msgstr ""
+msgid "Export group"
+msgstr ""
+
msgid "Export issues"
msgstr ""
msgid "Export project"
msgstr ""
+msgid "Export this group with all related data to a new GitLab instance. Once complete, you can import the data file from the \"New Group\" page."
+msgstr ""
+
msgid "Export this project with all its related data in order to move your project to a new GitLab instance. Once the export is finished, you can import the file from the \"New Project\" page."
msgstr ""
@@ -8696,9 +8930,6 @@ msgstr ""
msgid "Failed Jobs"
msgstr ""
-msgid "Failed create wiki"
-msgstr ""
-
msgid "Failed to add a Zoom meeting"
msgstr ""
@@ -8738,6 +8969,9 @@ msgstr ""
msgid "Failed to create resources"
msgstr ""
+msgid "Failed to create wiki"
+msgstr ""
+
msgid "Failed to delete board. Please try again."
msgstr ""
@@ -8747,7 +8981,7 @@ msgstr ""
msgid "Failed to enqueue the rebase operation, possibly due to a long-lived transaction. Try again later."
msgstr ""
-msgid "Failed to find import label for jira import."
+msgid "Failed to find import label for Jira import."
msgstr ""
msgid "Failed to get ref."
@@ -8771,6 +9005,12 @@ msgstr ""
msgid "Failed to load groups & users."
msgstr ""
+msgid "Failed to load labels. Please try again."
+msgstr ""
+
+msgid "Failed to load milestones. Please try again."
+msgstr ""
+
msgid "Failed to load related branches"
msgstr ""
@@ -9146,7 +9386,10 @@ msgstr ""
msgid "Filter by milestone name"
msgstr ""
-msgid "Filter by name..."
+msgid "Filter by name"
+msgstr ""
+
+msgid "Filter by status"
msgstr ""
msgid "Filter by two-factor authentication"
@@ -9158,12 +9401,18 @@ msgstr ""
msgid "Filter projects"
msgstr ""
+msgid "Filter results"
+msgstr ""
+
msgid "Filter results by group"
msgstr ""
msgid "Filter results by project"
msgstr ""
+msgid "Filter results..."
+msgstr ""
+
msgid "Filter your projects by name"
msgstr ""
@@ -9452,10 +9701,10 @@ msgstr ""
msgid "GeoNodes|Checksummed"
msgstr ""
-msgid "GeoNodes|Container repositories"
+msgid "GeoNodes|Consult Geo troubleshooting information"
msgstr ""
-msgid "GeoNodes|Data is out of date from %{timeago}"
+msgid "GeoNodes|Container repositories"
msgstr ""
msgid "GeoNodes|Data replication lag"
@@ -9497,6 +9746,9 @@ msgstr ""
msgid "GeoNodes|Last event ID seen from primary"
msgstr ""
+msgid "GeoNodes|Learn more about Geo node statuses"
+msgstr ""
+
msgid "GeoNodes|Loading nodes"
msgstr ""
@@ -9512,6 +9764,9 @@ msgstr ""
msgid "GeoNodes|Node was successfully removed."
msgstr ""
+msgid "GeoNodes|Node's status was updated %{timeAgo}."
+msgstr ""
+
msgid "GeoNodes|Not checksummed"
msgstr ""
@@ -9542,7 +9797,10 @@ msgstr ""
msgid "GeoNodes|Repository verification progress"
msgstr ""
-msgid "GeoNodes|Selective"
+msgid "GeoNodes|Selective (%{syncLabel})"
+msgstr ""
+
+msgid "GeoNodes|Selective synchronization"
msgstr ""
msgid "GeoNodes|Something went wrong while changing node status"
@@ -9569,6 +9827,9 @@ msgstr ""
msgid "GeoNodes|Unverified"
msgstr ""
+msgid "GeoNodes|Updated %{timeAgo}"
+msgstr ""
+
msgid "GeoNodes|Used slots"
msgstr ""
@@ -9608,16 +9869,16 @@ msgstr ""
msgid "Geo|All"
msgstr ""
-msgid "Geo|All projects"
+msgid "Geo|All %{replicable_type}"
msgstr ""
-msgid "Geo|All projects are being scheduled for re-sync"
+msgid "Geo|All projects"
msgstr ""
-msgid "Geo|All projects are being scheduled for re-verify"
+msgid "Geo|All projects are being scheduled for resync"
msgstr ""
-msgid "Geo|Batch operations"
+msgid "Geo|All projects are being scheduled for reverify"
msgstr ""
msgid "Geo|Could not remove tracking entry for an existing project."
@@ -9629,9 +9890,15 @@ msgstr ""
msgid "Geo|Failed"
msgstr ""
+msgid "Geo|Filter by status"
+msgstr ""
+
msgid "Geo|Geo Status"
msgstr ""
+msgid "Geo|In progress"
+msgstr ""
+
msgid "Geo|In sync"
msgstr ""
@@ -9656,9 +9923,6 @@ msgstr ""
msgid "Geo|Not synced yet"
msgstr ""
-msgid "Geo|Pending"
-msgstr ""
-
msgid "Geo|Pending synchronization"
msgstr ""
@@ -9695,7 +9959,7 @@ msgstr ""
msgid "Geo|Resync"
msgstr ""
-msgid "Geo|Resync all projects"
+msgid "Geo|Resync all"
msgstr ""
msgid "Geo|Retry count"
@@ -9704,7 +9968,7 @@ msgstr ""
msgid "Geo|Reverify"
msgstr ""
-msgid "Geo|Reverify all projects"
+msgid "Geo|Reverify all"
msgstr ""
msgid "Geo|Status"
@@ -10046,9 +10310,6 @@ msgstr ""
msgid "Go to file"
msgstr ""
-msgid "Go to file (MRs only)"
-msgstr ""
-
msgid "Go to file permalink (while viewing a file)"
msgstr ""
@@ -10493,6 +10754,9 @@ msgstr ""
msgid "GroupSettings|Disable group mentions"
msgstr ""
+msgid "GroupSettings|Export group"
+msgstr ""
+
msgid "GroupSettings|If the parent group's visibility is lower than the group current visibility, visibility levels for subgroups and projects will be changed to match the new parent group's visibility."
msgstr ""
@@ -10571,6 +10835,9 @@ msgstr ""
msgid "Groups (%{count})"
msgstr ""
+msgid "Groups (%{groups})"
+msgstr ""
+
msgid "Groups can also be nested by creating %{subgroup_docs_link_start}subgroups%{subgroup_docs_link_end}."
msgstr ""
@@ -10838,15 +11105,12 @@ msgstr ""
msgid "ID:"
msgstr ""
-msgid "IDE|Allow live previews of JavaScript projects in the Web IDE using CodeSandbox client side evaluation."
+msgid "IDE|Allow live previews of JavaScript projects in the Web IDE using CodeSandbox Live Preview."
msgstr ""
msgid "IDE|Back"
msgstr ""
-msgid "IDE|Client side evaluation"
-msgstr ""
-
msgid "IDE|Commit"
msgstr ""
@@ -10910,6 +11174,9 @@ msgstr ""
msgid "If any job surpasses this timeout threshold, it will be marked as failed. Human readable time input language is accepted like \"1 hour\". Values without specification represent seconds."
msgstr ""
+msgid "If blank, set allowable lifetime to %{instance_level_policy_in_words}, as defined by the instance admin. Once set, existing tokens for users in this group may be revoked."
+msgstr ""
+
msgid "If checked, group owners can manage LDAP group links and LDAP member overrides"
msgstr ""
@@ -10949,6 +11216,9 @@ msgstr ""
msgid "If you lose your recovery codes you can generate new ones, invalidating all previous codes."
msgstr ""
+msgid "If you reach 100%% storage capacity, you will not be able to: %{base_message}"
+msgstr ""
+
msgid "If your HTTP repository is not publicly accessible, add your credentials."
msgstr ""
@@ -11060,6 +11330,9 @@ msgstr ""
msgid "Import repository"
msgstr ""
+msgid "Import started by: %{importInitiator}"
+msgstr ""
+
msgid "Import tasks"
msgstr ""
@@ -11126,6 +11399,9 @@ msgstr ""
msgid "In order to tailor your experience with GitLab we<br>would like to know a bit more about you."
msgstr ""
+msgid "In progress"
+msgstr ""
+
msgid "In the next step, you'll be able to select the projects you want to import."
msgstr ""
@@ -11474,15 +11750,15 @@ msgstr ""
msgid "Issues"
msgstr ""
-msgid "Issues / Merge Requests"
-msgstr ""
-
msgid "Issues Analytics"
msgstr ""
msgid "Issues Rate Limits"
msgstr ""
+msgid "Issues and Merge Requests"
+msgstr ""
+
msgid "Issues can be bugs, tasks or ideas to be discussed. Also, issues are searchable and filterable."
msgstr ""
@@ -11504,10 +11780,10 @@ msgstr ""
msgid "IssuesAnalytics|Avg/Month:"
msgstr ""
-msgid "IssuesAnalytics|Issues created"
+msgid "IssuesAnalytics|Issues opened"
msgstr ""
-msgid "IssuesAnalytics|Issues created per month"
+msgid "IssuesAnalytics|Issues opened per month"
msgstr ""
msgid "IssuesAnalytics|Last 12 months"
@@ -11561,6 +11837,9 @@ msgstr ""
msgid "Jira integration not configured."
msgstr ""
+msgid "Jira project: %{importProject}"
+msgstr ""
+
msgid "JiraService|Events for %{noteable_model_name} are disabled."
msgstr ""
@@ -11951,6 +12230,9 @@ msgstr ""
msgid "Lead"
msgstr ""
+msgid "Lead Time"
+msgstr ""
+
msgid "Learn GitLab"
msgstr ""
@@ -11963,6 +12245,9 @@ msgstr ""
msgid "Learn how to %{no_packages_link_start}publish and share your packages%{no_packages_link_end} with GitLab."
msgstr ""
+msgid "Learn how to enable synchronization"
+msgstr ""
+
msgid "Learn more"
msgstr ""
@@ -12299,9 +12584,6 @@ msgstr ""
msgid "Live preview"
msgstr ""
-msgid "Load more vulnerabilities"
-msgstr ""
-
msgid "Loading"
msgstr ""
@@ -12452,9 +12734,6 @@ msgstr ""
msgid "Manage"
msgstr ""
-msgid "Manage Git repositories with fine-grained access controls that keep your code secure. Perform code reviews and enhance collaboration with merge requests. Each project can also have an issue tracker and a wiki."
-msgstr ""
-
msgid "Manage Web IDE features"
msgstr ""
@@ -12485,6 +12764,9 @@ msgstr ""
msgid "Manage your license"
msgstr ""
+msgid "Managed Account"
+msgstr ""
+
msgid "Manifest"
msgstr ""
@@ -12749,6 +13031,9 @@ msgstr ""
msgid "Merge Requests in Review"
msgstr ""
+msgid "Merge automatically (%{strategy})"
+msgstr ""
+
msgid "Merge commit message"
msgstr ""
@@ -12902,6 +13187,12 @@ msgstr ""
msgid "Merged branches are being deleted. This can take some time depending on the number of branches. Please refresh the page to see changes."
msgstr ""
+msgid "Merged this merge request."
+msgstr ""
+
+msgid "Merges this merge request immediately."
+msgstr ""
+
msgid "Merges this merge request when the pipeline succeeds."
msgstr ""
@@ -13010,12 +13301,15 @@ msgstr[0] ""
msgstr[1] ""
msgstr[2] ""
-msgid "Metrics|Environment"
+msgid "Metrics|Expand panel"
msgstr ""
msgid "Metrics|For grouping similar metrics"
msgstr ""
+msgid "Metrics|Go back (Esc)"
+msgstr ""
+
msgid "Metrics|Invalid time range, please verify."
msgstr ""
@@ -13046,9 +13340,6 @@ msgstr ""
msgid "Metrics|Refresh dashboard"
msgstr ""
-msgid "Metrics|Show last"
-msgstr ""
-
msgid "Metrics|There was an error creating the dashboard."
msgstr ""
@@ -13127,6 +13418,9 @@ msgstr ""
msgid "Microsoft Azure"
msgstr ""
+msgid "Middleman project with Static Site Editor support"
+msgstr ""
+
msgid "Migrated %{success_count}/%{total_count} files."
msgstr ""
@@ -13574,10 +13868,10 @@ msgstr ""
msgid "Next"
msgstr ""
-msgid "Next file in diff (MRs only)"
+msgid "Next file in diff"
msgstr ""
-msgid "Next unresolved discussion (MRs only)"
+msgid "Next unresolved discussion"
msgstr ""
msgid "Nickname"
@@ -13637,6 +13931,9 @@ msgstr ""
msgid "No changes between %{ref_start}%{source_branch}%{ref_end} and %{ref_start}%{target_branch}%{ref_end}"
msgstr ""
+msgid "No child epics match applied filters"
+msgstr ""
+
msgid "No connection could be made to a Gitaly Server, please check your logs!"
msgstr ""
@@ -13775,15 +14072,6 @@ msgstr ""
msgid "No thanks, don't show this again"
msgstr ""
-msgid "No vulnerabilities found for this group"
-msgstr ""
-
-msgid "No vulnerabilities found for this pipeline"
-msgstr ""
-
-msgid "No vulnerabilities found for this project"
-msgstr ""
-
msgid "No vulnerabilities present"
msgstr ""
@@ -13907,6 +14195,9 @@ msgstr ""
msgid "Nothing to preview."
msgstr ""
+msgid "Nothing to synchronize"
+msgstr ""
+
msgid "Notification events"
msgstr ""
@@ -14078,6 +14369,9 @@ msgstr ""
msgid "Once removed, the fork relationship cannot be restored and you will no longer be able to send merge requests to the source."
msgstr ""
+msgid "Once the exported file is ready you can download it from this page."
+msgstr ""
+
msgid "Once the exported file is ready, you will receive a notification email with a download link, or you can download it from this page."
msgstr ""
@@ -14180,9 +14474,6 @@ msgstr ""
msgid "Open sidebar"
msgstr ""
-msgid "Open source software to collaborate on code"
-msgstr ""
-
msgid "Open: %{openIssuesCount}"
msgstr ""
@@ -14285,6 +14576,9 @@ msgstr ""
msgid "OutdatedBrowser|You can provide feedback %{feedback_link_start}on this issue%{feedback_link_end} or via your usual support channels."
msgstr ""
+msgid "Overridden"
+msgstr ""
+
msgid "Overview"
msgstr ""
@@ -14336,6 +14630,9 @@ msgstr ""
msgid "PackageRegistry|Conan Command"
msgstr ""
+msgid "PackageRegistry|Copy .pypirc content"
+msgstr ""
+
msgid "PackageRegistry|Copy Conan Command"
msgstr ""
@@ -14357,6 +14654,9 @@ msgstr ""
msgid "PackageRegistry|Copy NuGet Setup Command"
msgstr ""
+msgid "PackageRegistry|Copy Pip command"
+msgstr ""
+
msgid "PackageRegistry|Copy and paste this inside your %{codeStart}pom.xml%{codeEnd} %{codeStart}dependencies%{codeEnd} block."
msgstr ""
@@ -14390,6 +14690,12 @@ msgstr ""
msgid "PackageRegistry|For more information on the NuGet registry, %{linkStart}see the documentation%{linkEnd}."
msgstr ""
+msgid "PackageRegistry|For more information on the PyPi registry, %{linkStart}see the documentation%{linkEnd}."
+msgstr ""
+
+msgid "PackageRegistry|If you haven't already done so, you will need to add the below to your %{codeStart}.pypirc%{codeEnd} file."
+msgstr ""
+
msgid "PackageRegistry|If you haven't already done so, you will need to add the below to your %{codeStart}pom.xml%{codeEnd} file."
msgstr ""
@@ -14399,6 +14705,9 @@ msgstr ""
msgid "PackageRegistry|Learn how to %{noPackagesLinkStart}publish and share your packages%{noPackagesLinkEnd} with GitLab."
msgstr ""
+msgid "PackageRegistry|Manually Published"
+msgstr ""
+
msgid "PackageRegistry|Maven"
msgstr ""
@@ -14417,12 +14726,18 @@ msgstr ""
msgid "PackageRegistry|NuGet Command"
msgstr ""
+msgid "PackageRegistry|Pip Command"
+msgstr ""
+
msgid "PackageRegistry|Pipeline %{linkStart}%{linkEnd} triggered %{timestamp} by %{author}"
msgstr ""
msgid "PackageRegistry|Published to the repository at %{timestamp}"
msgstr ""
+msgid "PackageRegistry|PyPi"
+msgstr ""
+
msgid "PackageRegistry|Registry Setup"
msgstr ""
@@ -14459,6 +14774,9 @@ msgstr ""
msgid "PackageRegistry|npm"
msgstr ""
+msgid "PackageRegistry|published by %{author}"
+msgstr ""
+
msgid "PackageRegistry|yarn"
msgstr ""
@@ -14474,6 +14792,9 @@ msgstr ""
msgid "PackageType|NuGet"
msgstr ""
+msgid "PackageType|PyPi"
+msgstr ""
+
msgid "Packages"
msgstr ""
@@ -14567,6 +14888,9 @@ msgstr ""
msgid "Past due"
msgstr ""
+msgid "Paste a machine public key here. Read more about how to generate it"
+msgstr ""
+
msgid "Paste a machine public key here. Read more about how to generate it %{link_start}here%{link_end}"
msgstr ""
@@ -14582,9 +14906,6 @@ msgstr ""
msgid "Path"
msgstr ""
-msgid "Path, transfer, remove"
-msgstr ""
-
msgid "Path:"
msgstr ""
@@ -14615,7 +14936,7 @@ msgstr ""
msgid "Percentage"
msgstr ""
-msgid "Perform advanced options such as changing path, transferring, or removing the group."
+msgid "Perform advanced options such as changing path, transferring, exporting, or removing the group."
msgstr ""
msgid "Perform common operations on GitLab project"
@@ -14696,9 +15017,6 @@ msgstr ""
msgid "Pipeline minutes quota"
msgstr ""
-msgid "Pipeline quota"
-msgstr ""
-
msgid "Pipeline subscriptions"
msgstr ""
@@ -14708,6 +15026,9 @@ msgstr ""
msgid "Pipeline: %{status}"
msgstr ""
+msgid "PipelineCharts|CI / CD Analytics"
+msgstr ""
+
msgid "PipelineCharts|Failed:"
msgstr ""
@@ -14786,15 +15107,6 @@ msgstr ""
msgid "Pipelines settings for '%{project_name}' were successfully updated."
msgstr ""
-msgid "Pipelines| to purchase more minutes."
-msgstr ""
-
-msgid "Pipelines|%{namespace_name} has exceeded its pipeline minutes quota."
-msgstr ""
-
-msgid "Pipelines|%{namespace_name} has less than %{notification_level}%% of CI minutes available."
-msgstr ""
-
msgid "Pipelines|API"
msgstr ""
@@ -14816,10 +15128,13 @@ msgstr ""
msgid "Pipelines|Get started with Pipelines"
msgstr ""
-msgid "Pipelines|Loading Pipelines"
+msgid "Pipelines|Group %{namespace_name} has %{percentage}%% or less Shared Runner Pipeline minutes remaining. Once it runs out, no new jobs or pipelines in its projects will run."
msgstr ""
-msgid "Pipelines|Pipelines will not run anymore on shared Runners."
+msgid "Pipelines|Group %{namespace_name} has exceeded its pipeline minutes quota. Unless you buy additional pipeline minutes, no new jobs or pipelines in its projects will run."
+msgstr ""
+
+msgid "Pipelines|Loading Pipelines"
msgstr ""
msgid "Pipelines|Project cache successfully reset."
@@ -15050,6 +15365,9 @@ msgstr ""
msgid "Please select"
msgstr ""
+msgid "Please select a Jira project"
+msgstr ""
+
msgid "Please select a country"
msgstr ""
@@ -15095,6 +15413,9 @@ msgstr ""
msgid "Please wait while we import the repository for you. Refresh at will."
msgstr ""
+msgid "Plugins directory is deprecated and will be removed in 14.0. Please move this file into /file_hooks directory."
+msgstr ""
+
msgid "Pod does not exist"
msgstr ""
@@ -15212,7 +15533,7 @@ msgstr ""
msgid "Prevent users from changing their profile name"
msgstr ""
-msgid "Prevent users from modifing merge request approvers list"
+msgid "Prevent users from modifying merge request approvers list"
msgstr ""
msgid "Prevent users from performing write operations on GitLab while performing maintenance."
@@ -15233,10 +15554,10 @@ msgstr ""
msgid "Previous Artifacts"
msgstr ""
-msgid "Previous file in diff (MRs only)"
+msgid "Previous file in diff"
msgstr ""
-msgid "Previous unresolved discussion (MRs only)"
+msgid "Previous unresolved discussion"
msgstr ""
msgid "Primary"
@@ -15269,6 +15590,9 @@ msgstr ""
msgid "Private profile"
msgstr ""
+msgid "Private projects Minutes cost factor"
+msgstr ""
+
msgid "Private projects can be created in your personal namespace with:"
msgstr ""
@@ -16097,6 +16421,9 @@ msgstr ""
msgid "ProjectSettings|With GitLab Pages you can host your static websites on GitLab"
msgstr ""
+msgid "ProjectSettings|With Metrics Dashboard you can visualize this project performance metrics"
+msgstr ""
+
msgid "ProjectTemplates|.NET Core"
msgstr ""
@@ -16160,6 +16487,9 @@ msgstr ""
msgid "ProjectTemplates|Spring"
msgstr ""
+msgid "ProjectTemplates|Static Site Editor/Middleman"
+msgstr ""
+
msgid "ProjectTemplates|iOS (Swift)"
msgstr ""
@@ -16379,13 +16709,13 @@ msgstr ""
msgid "PrometheusService|Prometheus is being automatically managed on your clusters"
msgstr ""
-msgid "PrometheusService|These metrics will only be monitored after your first deployment to an environment"
+msgid "PrometheusService|Select the Active checkbox to override the Auto Configuration with custom settings. If unchecked, Auto Configuration settings are used."
msgstr ""
-msgid "PrometheusService|Time-series monitoring service"
+msgid "PrometheusService|These metrics will only be monitored after your first deployment to an environment"
msgstr ""
-msgid "PrometheusService|To enable manual configuration, uninstall Prometheus from your clusters"
+msgid "PrometheusService|Time-series monitoring service"
msgstr ""
msgid "PrometheusService|To enable the installation of Prometheus on your clusters, deactivate the manual configuration below"
@@ -16571,6 +16901,9 @@ msgstr ""
msgid "Protip:"
msgstr ""
+msgid "Protocol"
+msgstr ""
+
msgid "Provider"
msgstr ""
@@ -16595,6 +16928,9 @@ msgstr ""
msgid "Public pipelines"
msgstr ""
+msgid "Public projects Minutes cost factor"
+msgstr ""
+
msgid "Pull"
msgstr ""
@@ -16796,6 +17132,9 @@ msgstr[0] ""
msgstr[1] ""
msgstr[2] ""
+msgid "Regenerate export"
+msgstr ""
+
msgid "Regenerate instance ID"
msgstr ""
@@ -17174,9 +17513,30 @@ msgstr ""
msgid "Reports|%{combinedString} and %{resolvedString}"
msgstr ""
+msgid "Reports|Accessibility scanning detected %d issue for the source branch only"
+msgid_plural "Reports|Accessibility scanning detected %d issues for the source branch only"
+msgstr[0] ""
+msgstr[1] ""
+msgstr[2] ""
+
+msgid "Reports|Accessibility scanning detected no issues for the source branch only"
+msgstr ""
+
+msgid "Reports|Accessibility scanning failed loading results"
+msgstr ""
+
+msgid "Reports|Accessibility scanning results are being parsed"
+msgstr ""
+
msgid "Reports|Actions"
msgstr ""
+msgid "Reports|An error occured while loading report"
+msgstr ""
+
+msgid "Reports|An error occurred while loading %{name} results"
+msgstr ""
+
msgid "Reports|Class"
msgstr ""
@@ -17282,7 +17642,7 @@ msgstr ""
msgid "Requested %{time_ago}"
msgstr ""
-msgid "Requested design version does not exist"
+msgid "Requested design version does not exist."
msgstr ""
msgid "Requested states are invalid"
@@ -17468,6 +17828,9 @@ msgstr ""
msgid "Resync"
msgstr ""
+msgid "Resync all"
+msgstr ""
+
msgid "Resync all %{replicableType}"
msgstr ""
@@ -17537,6 +17900,9 @@ msgstr ""
msgid "Revoked personal access token %{personal_access_token_name}!"
msgstr ""
+msgid "Revoked project access token %{project_access_token_name}!"
+msgstr ""
+
msgid "RightSidebar|adding a"
msgstr ""
@@ -17585,6 +17951,9 @@ msgstr ""
msgid "Runner token"
msgstr ""
+msgid "Runner tokens"
+msgstr ""
+
msgid "Runner was not updated."
msgstr ""
@@ -17639,6 +18008,9 @@ msgstr ""
msgid "SAML SSO for %{group_name}"
msgstr ""
+msgid "SAML discovery tokens"
+msgstr ""
+
msgid "SAML for %{group_name}"
msgstr ""
@@ -17723,12 +18095,18 @@ msgstr ""
msgid "Scheduled"
msgstr ""
+msgid "Scheduled to merge this merge request (%{strategy})."
+msgstr ""
+
msgid "Scheduled to merge this merge request when the pipeline succeeds."
msgstr ""
msgid "Schedules"
msgstr ""
+msgid "Schedules to merge this merge request (%{strategy})."
+msgstr ""
+
msgid "Scheduling"
msgstr ""
@@ -17786,6 +18164,9 @@ msgstr ""
msgid "Search branches and tags"
msgstr ""
+msgid "Search by author"
+msgstr ""
+
msgid "Search files"
msgstr ""
@@ -17939,12 +18320,6 @@ msgstr[0] ""
msgstr[1] ""
msgstr[2] ""
-msgid "SearchResults|snippet result"
-msgid_plural "SearchResults|snippet results"
-msgstr[0] ""
-msgstr[1] ""
-msgstr[2] ""
-
msgid "SearchResults|user"
msgid_plural "SearchResults|users"
msgstr[0] ""
@@ -17957,6 +18332,9 @@ msgstr[0] ""
msgstr[1] ""
msgstr[2] ""
+msgid "Searching by both author and message is currently not supported."
+msgstr ""
+
msgid "Seat Link"
msgstr ""
@@ -17984,184 +18362,232 @@ msgstr ""
msgid "Security Dashboard"
msgstr ""
-msgid "Security Dashboard|Error fetching the vulnerability counts. Please check your network connection and try again."
+msgid "Security configuration help link"
msgstr ""
-msgid "Security Dashboard|Error fetching the vulnerability list. Please check your network connection and try again."
+msgid "Security dashboard"
msgstr ""
-msgid "Security Dashboard|Issue Created"
+msgid "Security report is out of date. Please update your branch with the latest changes from the target branch (%{targetBranchName})"
msgstr ""
-msgid "Security Reports|Comment added to '%{vulnerabilityName}'"
+msgid "Security report is out of date. Run %{newPipelineLinkStart}a new pipeline%{newPipelineLinkEnd} for the target branch (%{targetBranchName})"
msgstr ""
-msgid "Security Reports|Comment deleted on '%{vulnerabilityName}'"
+msgid "SecurityConfiguration|Configured"
msgstr ""
-msgid "Security Reports|Comment edited on '%{vulnerabilityName}'"
+msgid "SecurityConfiguration|Feature"
msgstr ""
-msgid "Security Reports|Create issue"
+msgid "SecurityConfiguration|Feature documentation for %{featureName}"
msgstr ""
-msgid "Security Reports|Dismiss vulnerability"
+msgid "SecurityConfiguration|Not yet configured"
msgstr ""
-msgid "Security Reports|Dismissed '%{vulnerabilityName}'"
+msgid "SecurityConfiguration|Secure features"
msgstr ""
-msgid "Security Reports|Dismissed '%{vulnerabilityName}'. Turn off the hide dismissed toggle to view."
+msgid "SecurityConfiguration|Status"
msgstr ""
-msgid "Security Reports|Either you don't have permission to view this dashboard or the dashboard has not been setup. Please check your permission settings with your administrator or check your dashboard configurations to proceed."
+msgid "SecurityReports|%{firstProject} and %{secondProject}"
msgstr ""
-msgid "Security Reports|Learn more about setting up your dashboard"
+msgid "SecurityReports|%{firstProject}, %{secondProject}, and %{rest}"
msgstr ""
-msgid "Security Reports|More info"
+msgid "SecurityReports|Add a project to your dashboard"
msgstr ""
-msgid "Security Reports|Oops, something doesn't seem right."
+msgid "SecurityReports|Add or remove projects from your dashboard"
msgstr ""
-msgid "Security Reports|Security reports can only be accessed by authorized users."
+msgid "SecurityReports|Add projects"
msgstr ""
-msgid "Security Reports|There was an error adding the comment."
+msgid "SecurityReports|Comment added to '%{vulnerabilityName}'"
msgstr ""
-msgid "Security Reports|There was an error creating the issue."
+msgid "SecurityReports|Comment deleted on '%{vulnerabilityName}'"
msgstr ""
-msgid "Security Reports|There was an error creating the merge request."
+msgid "SecurityReports|Comment edited on '%{vulnerabilityName}'"
msgstr ""
-msgid "Security Reports|There was an error deleting the comment."
+msgid "SecurityReports|Create issue"
msgstr ""
-msgid "Security Reports|There was an error dismissing the vulnerabilities."
+msgid "SecurityReports|Dismiss Selected"
msgstr ""
-msgid "Security Reports|There was an error dismissing the vulnerability."
+msgid "SecurityReports|Dismiss vulnerability"
msgstr ""
-msgid "Security Reports|There was an error reverting the dismissal."
+msgid "SecurityReports|Dismissed '%{vulnerabilityName}'"
msgstr ""
-msgid "Security Reports|There was an error reverting this dismissal."
+msgid "SecurityReports|Dismissed '%{vulnerabilityName}'. Turn off the hide dismissed toggle to view."
msgstr ""
-msgid "Security Reports|Undo dismiss"
+msgid "SecurityReports|Each vulnerability now has a unique page that can be directly linked to, shared, referenced, and tracked as the single source of truth. Vulnerability occurrences also persist across scanner runs, which improves tracking and visibility and reduces duplicates between scans."
msgstr ""
-msgid "Security Reports|You do not have sufficient permissions to access this report"
+msgid "SecurityReports|Edit dashboard"
msgstr ""
-msgid "Security Reports|You must sign in as an authorized user to see this report"
+msgid "SecurityReports|Either you don't have permission to view this dashboard or the dashboard has not been setup. Please check your permission settings with your administrator or check your dashboard configurations to proceed."
msgstr ""
-msgid "Security configuration help link"
+msgid "SecurityReports|Error fetching the vulnerability counts. Please check your network connection and try again."
msgstr ""
-msgid "Security dashboard"
+msgid "SecurityReports|Error fetching the vulnerability list. Please check your network connection and try again."
msgstr ""
-msgid "Security report is out of date. Please update your branch with the latest changes from the target branch (%{targetBranchName})"
+msgid "SecurityReports|False positive"
msgstr ""
-msgid "Security report is out of date. Run %{newPipelineLinkStart}a new pipeline%{newPipelineLinkEnd} for the target branch (%{targetBranchName})"
+msgid "SecurityReports|Hide dismissed"
msgstr ""
-msgid "SecurityConfiguration|Configured"
+msgid "SecurityReports|Introducing standalone vulnerabilities"
msgstr ""
-msgid "SecurityConfiguration|Feature"
+msgid "SecurityReports|Issue Created"
msgstr ""
-msgid "SecurityConfiguration|Feature documentation for %{featureName}"
+msgid "SecurityReports|Learn More"
msgstr ""
-msgid "SecurityConfiguration|Not yet configured"
+msgid "SecurityReports|Learn more about setting up your dashboard"
msgstr ""
-msgid "SecurityConfiguration|Secure features"
+msgid "SecurityReports|Load more vulnerabilities"
msgstr ""
-msgid "SecurityConfiguration|Status"
+msgid "SecurityReports|Monitor vulnerabilities in your code"
+msgstr ""
+
+msgid "SecurityReports|More info"
+msgstr ""
+
+msgid "SecurityReports|More information"
+msgstr ""
+
+msgid "SecurityReports|No vulnerabilities found for dashboard"
+msgstr ""
+
+msgid "SecurityReports|No vulnerabilities found for this group"
+msgstr ""
+
+msgid "SecurityReports|No vulnerabilities found for this pipeline"
+msgstr ""
+
+msgid "SecurityReports|No vulnerabilities found for this project"
+msgstr ""
+
+msgid "SecurityReports|Oops, something doesn't seem right."
+msgstr ""
+
+msgid "SecurityReports|Pipeline %{pipelineLink} triggered %{timeago} by %{user}"
msgstr ""
-msgid "SecurityDashboard|%{firstProject} and %{secondProject}"
+msgid "SecurityReports|Project"
msgstr ""
-msgid "SecurityDashboard|%{firstProject}, %{secondProject}, and %{rest}"
+msgid "SecurityReports|Projects added"
msgstr ""
-msgid "SecurityDashboard|Add a project to your dashboard"
+msgid "SecurityReports|Remove project from dashboard"
msgstr ""
-msgid "SecurityDashboard|Add or remove projects from your dashboard"
+msgid "SecurityReports|Report type"
msgstr ""
-msgid "SecurityDashboard|Add projects"
+msgid "SecurityReports|Return to dashboard"
msgstr ""
-msgid "SecurityDashboard|Edit dashboard"
+msgid "SecurityReports|Security Dashboard"
msgstr ""
-msgid "SecurityDashboard|Hide dismissed"
+msgid "SecurityReports|Security reports can only be accessed by authorized users."
msgstr ""
-msgid "SecurityDashboard|Monitor vulnerabilities in your code"
+msgid "SecurityReports|Select a project to add by using the project search field above."
msgstr ""
-msgid "SecurityDashboard|More information"
+msgid "SecurityReports|Select a reason"
msgstr ""
-msgid "SecurityDashboard|No vulnerabilities found for dashboard"
+msgid "SecurityReports|Severity"
msgstr ""
-msgid "SecurityDashboard|Pipeline %{pipelineLink} triggered %{timeago} by %{user}"
+msgid "SecurityReports|Status"
msgstr ""
-msgid "SecurityDashboard|Project"
+msgid "SecurityReports|The security dashboard displays the latest security findings for projects you wish to monitor. Select \"Edit dashboard\" to add and remove projects."
msgstr ""
-msgid "SecurityDashboard|Projects added"
+msgid "SecurityReports|The security dashboard displays the latest security report. Use it to find and fix vulnerabilities."
msgstr ""
-msgid "SecurityDashboard|Remove project from dashboard"
+msgid "SecurityReports|There was an error adding the comment."
msgstr ""
-msgid "SecurityDashboard|Report type"
+msgid "SecurityReports|There was an error creating the issue."
msgstr ""
-msgid "SecurityDashboard|Return to dashboard"
+msgid "SecurityReports|There was an error creating the merge request."
msgstr ""
-msgid "SecurityDashboard|Security Dashboard"
+msgid "SecurityReports|There was an error deleting the comment."
msgstr ""
-msgid "SecurityDashboard|Select a project to add by using the project search field above."
+msgid "SecurityReports|There was an error dismissing the vulnerabilities."
msgstr ""
-msgid "SecurityDashboard|Severity"
+msgid "SecurityReports|There was an error dismissing the vulnerability."
msgstr ""
-msgid "SecurityDashboard|Status"
+msgid "SecurityReports|There was an error reverting the dismissal."
msgstr ""
-msgid "SecurityDashboard|The security dashboard displays the latest security findings for projects you wish to monitor. Select \"Edit dashboard\" to add and remove projects."
+msgid "SecurityReports|There was an error reverting this dismissal."
msgstr ""
-msgid "SecurityDashboard|The security dashboard displays the latest security report. Use it to find and fix vulnerabilities."
+msgid "SecurityReports|There was an error while generating the report."
msgstr ""
-msgid "SecurityDashboard|There was an error while generating the report."
+msgid "SecurityReports|Unable to add %{invalidProjects}"
msgstr ""
-msgid "SecurityDashboard|Unable to add %{invalidProjects}"
+msgid "SecurityReports|Undo dismiss"
+msgstr ""
+
+msgid "SecurityReports|While it's rare to have no vulnerabilities for your group, it can happen. In any event, we ask that you double check your settings to make sure you've set up your dashboard correctly."
+msgstr ""
+
+msgid "SecurityReports|While it's rare to have no vulnerabilities for your pipeline, it can happen. In any event, we ask that you double check your settings to make sure all security scanning jobs have passed successfully."
+msgstr ""
+
+msgid "SecurityReports|While it's rare to have no vulnerabilities for your project, it can happen. In any event, we ask that you double check your settings to make sure you've set up your dashboard correctly."
+msgstr ""
+
+msgid "SecurityReports|While it's rare to have no vulnerabilities, it can happen. In any event, we ask that you please double check your settings to make sure you've set up your dashboard correctly."
+msgstr ""
+
+msgid "SecurityReports|Won't fix / Accept risk"
+msgstr ""
+
+msgid "SecurityReports|You do not have sufficient permissions to access this report"
+msgstr ""
+
+msgid "SecurityReports|You must sign in as an authorized user to see this report"
+msgstr ""
+
+msgid "SecurityReports|[No reason]"
msgstr ""
msgid "See GitLab's %{password_policy_guidelines}"
@@ -18173,6 +18599,9 @@ msgstr ""
msgid "See the affected projects in the GitLab admin panel"
msgstr ""
+msgid "See what's new at GitLab"
+msgstr ""
+
msgid "Select"
msgstr ""
@@ -18578,9 +19007,6 @@ msgstr ""
msgid "Set up Jira Integration"
msgstr ""
-msgid "Set up Jira Integration illustration"
-msgstr ""
-
msgid "Set up a %{type} Runner automatically"
msgstr ""
@@ -18665,6 +19091,9 @@ msgstr ""
msgid "Severity: %{severity}"
msgstr ""
+msgid "Shards (%{shards})"
+msgstr ""
+
msgid "Shards to synchronize"
msgstr ""
@@ -18980,6 +19409,9 @@ msgstr ""
msgid "Snowplow"
msgstr ""
+msgid "Some child epics may be hidden due to applied filters"
+msgstr ""
+
msgid "Some email servers do not support overriding the email sender name. Enable this option to include the name of the author of the issue, merge request or comment in the email body instead."
msgstr ""
@@ -19121,6 +19553,9 @@ msgstr ""
msgid "Something went wrong while updating your list settings"
msgstr ""
+msgid "Something went wrong with your automatic subscription renewal"
+msgstr ""
+
msgid "Something went wrong, unable to add %{project} to dashboard"
msgstr ""
@@ -19406,6 +19841,9 @@ msgstr ""
msgid "Stage removed"
msgstr ""
+msgid "Standard"
+msgstr ""
+
msgid "Star a label to make it a priority label. Order the prioritized labels to change their relative priority, by dragging."
msgstr ""
@@ -19529,6 +19967,9 @@ msgstr ""
msgid "Static Application Security Testing (SAST)"
msgstr ""
+msgid "StaticSiteEditor|An error occurred while submitting your changes."
+msgstr ""
+
msgid "StaticSiteEditor|Branch could not be created."
msgstr ""
@@ -19538,18 +19979,30 @@ msgstr ""
msgid "StaticSiteEditor|Could not create merge request."
msgstr ""
+msgid "StaticSiteEditor|Incompatible file content"
+msgstr ""
+
msgid "StaticSiteEditor|Return to site"
msgstr ""
+msgid "StaticSiteEditor|Static site editor"
+msgstr ""
+
msgid "StaticSiteEditor|Success!"
msgstr ""
msgid "StaticSiteEditor|Summary of changes"
msgstr ""
+msgid "StaticSiteEditor|The Static Site Editor is currently configured to only edit Markdown content on pages generated from Middleman. Visit the documentation to learn more about configuring your site to use the Static Site Editor."
+msgstr ""
+
msgid "StaticSiteEditor|Update %{sourcePath} file"
msgstr ""
+msgid "StaticSiteEditor|View documentation"
+msgstr ""
+
msgid "StaticSiteEditor|View merge request"
msgstr ""
@@ -19607,6 +20060,12 @@ msgstr ""
msgid "StatusPage|Status page"
msgstr ""
+msgid "StatusPage|Status page URL"
+msgstr ""
+
+msgid "StatusPage|Status page frontend documentation"
+msgstr ""
+
msgid "StatusPage|To publish incidents to an external status page, GitLab will store a JSON file in your Amazon S3 account in a location accessible to your external status page service. Make sure to also set up %{docsLink}"
msgstr ""
@@ -19955,6 +20414,12 @@ msgstr ""
msgid "Synced"
msgstr ""
+msgid "Synchronization disabled"
+msgstr ""
+
+msgid "Synchronization of container repositories is disabled."
+msgstr ""
+
msgid "System"
msgstr ""
@@ -20108,6 +20573,9 @@ msgstr ""
msgid "Telephone number"
msgstr ""
+msgid "Telephone number (Optional)"
+msgstr ""
+
msgid "Template"
msgstr ""
@@ -20417,6 +20885,9 @@ msgstr ""
msgid "The license was successfully uploaded and is now active. You can see the details below."
msgstr ""
+msgid "The license was successfully uploaded and will be active from %{starts_at}. You can see the details below."
+msgstr ""
+
msgid "The maximum file size allowed is %{size}."
msgstr ""
@@ -20738,6 +21209,9 @@ msgstr ""
msgid "There was an error getting the epic participants."
msgstr ""
+msgid "There was an error importing the Jira project."
+msgstr ""
+
msgid "There was an error loading users activity calendar."
msgstr ""
@@ -20753,7 +21227,7 @@ msgstr ""
msgid "There was an error resetting user pipeline minutes."
msgstr ""
-msgid "There was an error saving this Geo Node"
+msgid "There was an error saving this Geo Node."
msgstr ""
msgid "There was an error saving your changes."
@@ -20816,6 +21290,9 @@ msgstr ""
msgid "They can be managed using the %{link}."
msgstr ""
+msgid "Third Party Advisory Link"
+msgstr ""
+
msgid "Third party offers"
msgstr ""
@@ -20852,6 +21329,9 @@ msgstr ""
msgid "This application will be able to:"
msgstr ""
+msgid "This attachment has been truncated to avoid exceeding the maximum allowed attachment size of 15MB. %{written_count} of %{issues_count} issues have been included. Consider re-exporting with a narrower selection of issues."
+msgstr ""
+
msgid "This block is self-referential"
msgstr ""
@@ -20993,6 +21473,12 @@ msgstr ""
msgid "This issue is confidential"
msgstr ""
+msgid "This issue is currently blocked by the following issues: %{issues}."
+msgstr ""
+
+msgid "This issue is in a child epic of the filtered epic"
+msgstr ""
+
msgid "This issue is locked."
msgstr ""
@@ -21191,7 +21677,10 @@ msgstr ""
msgid "This user will be the author of all events in the activity feed that are the result of an update, like new branches being created or new commits being pushed to existing branches. Upon creation or when reassigning you can only assign yourself to be the mirror user."
msgstr ""
-msgid "This variable can not be masked"
+msgid "This variable can not be masked."
+msgstr ""
+
+msgid "This variable does not match the expected pattern."
msgstr ""
msgid "This will help us personalize your onboarding experience."
@@ -21221,10 +21710,10 @@ msgstr ""
msgid "ThreatMonitoring|Application firewall not detected"
msgstr ""
-msgid "ThreatMonitoring|Container Network Policy"
+msgid "ThreatMonitoring|Container Network Policies are not installed or have been disabled. To view this data, ensure your Network Policies are installed and enabled for your cluster."
msgstr ""
-msgid "ThreatMonitoring|Container NetworkPolicies are not installed or has been disabled. To view this data, ensure you NetworkPolicies are installed and enabled for your cluster."
+msgid "ThreatMonitoring|Container Network Policy"
msgstr ""
msgid "ThreatMonitoring|Container NetworkPolicies not detected"
@@ -21257,7 +21746,7 @@ msgstr ""
msgid "ThreatMonitoring|Something went wrong, unable to fetch statistics"
msgstr ""
-msgid "ThreatMonitoring|The firewall is not installed or has been disabled. To view this data, ensure you firewall is installed and enabled for your cluster."
+msgid "ThreatMonitoring|The firewall is not installed or has been disabled. To view this data, ensure the web application firewall is installed and enabled for your cluster."
msgstr ""
msgid "ThreatMonitoring|The graph below is an overview of traffic coming to your application as tracked by the Web Application Firewall (WAF). View the docs for instructions on how to access the WAF logs to see what type of malicious traffic is trying to access your app. The docs link is also accessible by clicking the \"?\" icon next to the title below."
@@ -21326,6 +21815,9 @@ msgstr ""
msgid "Time in seconds GitLab will wait for a response from the external service. When the service does not respond in time, access will be denied."
msgstr ""
+msgid "Time of import: %{importTime}"
+msgstr ""
+
msgid "Time remaining"
msgstr ""
@@ -21512,7 +22004,7 @@ msgstr ""
msgid "Title:"
msgstr ""
-msgid "Titles and Filenames"
+msgid "Titles and Descriptions"
msgstr ""
msgid "To"
@@ -21719,7 +22211,7 @@ msgstr ""
msgid "Too many projects enabled. You will need to manage them via the console or the API."
msgstr ""
-msgid "Topics"
+msgid "Topics (optional)"
msgstr ""
msgid "Total"
@@ -21881,6 +22373,9 @@ msgstr ""
msgid "Try all GitLab has to offer for 30 days."
msgstr ""
+msgid "Try changing or removing filters."
+msgstr ""
+
msgid "Try to fork again"
msgstr ""
@@ -21974,6 +22469,9 @@ msgstr ""
msgid "Unable to connect to server: %{error}"
msgstr ""
+msgid "Unable to connect to the Jira instance. Please check your Jira integration configuration."
+msgstr ""
+
msgid "Unable to convert Kubernetes logs encoding to UTF-8"
msgstr ""
@@ -22919,6 +23417,9 @@ msgstr ""
msgid "View issue"
msgstr ""
+msgid "View issues"
+msgstr ""
+
msgid "View it on GitLab"
msgstr ""
@@ -23096,6 +23597,15 @@ msgstr ""
msgid "VulnerabilityManagement|Resolved %{timeago} by %{user}"
msgstr ""
+msgid "VulnerabilityManagement|Something went wrong while trying to delete the comment. Please try again later."
+msgstr ""
+
+msgid "VulnerabilityManagement|Something went wrong while trying to retrieve the vulnerability history. Please try again later."
+msgstr ""
+
+msgid "VulnerabilityManagement|Something went wrong while trying to save the comment. Please try again later."
+msgstr ""
+
msgid "VulnerabilityManagement|Something went wrong, could not create an issue."
msgstr ""
@@ -23126,6 +23636,9 @@ msgstr ""
msgid "VulnerabilityStatusTypes|Resolved"
msgstr ""
+msgid "Vulnerability|%{scannerName} (version %{scannerVersion})"
+msgstr ""
+
msgid "Vulnerability|Class"
msgstr ""
@@ -23159,7 +23672,10 @@ msgstr ""
msgid "Vulnerability|Project"
msgstr ""
-msgid "Vulnerability|Report Type"
+msgid "Vulnerability|Scanner Provider"
+msgstr ""
+
+msgid "Vulnerability|Scanner Type"
msgstr ""
msgid "Vulnerability|Severity"
@@ -23213,9 +23729,15 @@ msgstr ""
msgid "We sent you an email with reset password instructions"
msgstr ""
+msgid "We tried to automatically renew your %{strong}%{plan_name}%{strong_close} subscription for %{strong}%{namespace_name}%{strong_close} on %{expires_on} but something went wrong so your subscription was downgraded to the free plan. Don't worry, your data is safe. We suggest you check your payment method and get in touch with our support team (%{support_link}). They'll gladly help with your subscription renewal."
+msgstr ""
+
msgid "We want to be sure it is you, please confirm you are not a robot."
msgstr ""
+msgid "We will automatically renew your %{strong}%{plan_name}%{strong_close} subscription for %{strong}%{namespace_name}%{strong_close} on %{strong}%{expires_on}%{strong_close}. There's nothing that you need to do, we'll let you know when the renewal is complete. Need more seats, a higher plan or just want to review your payment method?"
+msgstr ""
+
msgid "We've found no vulnerabilities"
msgstr ""
@@ -23321,15 +23843,6 @@ msgstr ""
msgid "When:"
msgstr ""
-msgid "While it's rare to have no vulnerabilities for your group, it can happen. In any event, we ask that you double check your settings to make sure you've set up your dashboard correctly."
-msgstr ""
-
-msgid "While it's rare to have no vulnerabilities for your pipeline, it can happen. In any event, we ask that you double check your settings to make sure all security scanning jobs have passed successfully."
-msgstr ""
-
-msgid "While it's rare to have no vulnerabilities for your project, it can happen. In any event, we ask that you double check your settings to make sure you've set up your dashboard correctly."
-msgstr ""
-
msgid "While it's rare to have no vulnerabilities, it can happen. In any event, we ask that you please double check your settings to make sure you've set up your dashboard correctly."
msgstr ""
@@ -23360,9 +23873,6 @@ msgstr ""
msgid "Wiki"
msgstr ""
-msgid "Wiki pages"
-msgstr ""
-
msgid "Wiki was successfully updated."
msgstr ""
@@ -23546,6 +24056,9 @@ msgstr ""
msgid "Yes, add it"
msgstr ""
+msgid "Yes, close issue"
+msgstr ""
+
msgid "Yes, let me map Google Code users to full names or GitLab users."
msgstr ""
@@ -23741,6 +24254,9 @@ msgstr ""
msgid "You could not create a new trigger."
msgstr ""
+msgid "You didn't renew your %{strong}%{plan_name}%{strong_close} subscription for %{strong}%{namespace_name}%{strong_close} so it was downgraded to the free plan."
+msgstr ""
+
msgid "You didn't renew your %{strong}%{plan_name}%{strong_close} subscription so it was downgraded to the GitLab Core Plan."
msgstr ""
@@ -23807,6 +24323,9 @@ msgstr ""
msgid "You have declined the invitation to join %{label}."
msgstr ""
+msgid "You have imported from this project %{numberOfPreviousImportsForProject} times before. Each new import will create duplicate issues."
+msgstr ""
+
msgid "You have no permissions"
msgstr ""
@@ -23888,6 +24407,9 @@ msgstr ""
msgid "You need to upload a Google Takeout archive."
msgstr ""
+msgid "You reached %{usage_in_percent} of %{namespace_name}'s capacity (%{used_storage} of %{storage_limit})"
+msgstr ""
+
msgid "You tried to fork %{link_to_the_project} but it failed for the following reason:"
msgstr ""
@@ -23972,9 +24494,21 @@ msgstr ""
msgid "YouTube"
msgstr ""
+msgid "Your %{strong}%{plan_name}%{strong_close} subscription for %{strong}%{namespace_name}%{strong_close} will expire on %{strong}%{expires_on}%{strong_close}. After that, you will not to be able to create issues or merge requests as well as many other features."
+msgstr ""
+
msgid "Your %{strong}%{plan_name}%{strong_close} subscription will expire on %{strong}%{expires_on}%{strong_close}. After that, you will not to be able to create issues or merge requests as well as many other features."
msgstr ""
+msgid "Your CSV export has started. It will be emailed to %{email} when complete."
+msgstr ""
+
+msgid "Your CSV export of %{issues_count} from project %{project_link} has been added to this email as an attachment."
+msgstr ""
+
+msgid "Your CSV export of %{written_count} from project %{project_name} (%{project_url}) has been added to this email as an attachment."
+msgstr ""
+
msgid "Your Commit Email will be used for web based operations, such as edits and merges."
msgstr ""
@@ -24071,6 +24605,9 @@ msgstr ""
msgid "Your comment could not be updated! Please check your network connection and try again."
msgstr ""
+msgid "Your comment will be discarded."
+msgstr ""
+
msgid "Your custom stage '%{title}' was created"
msgstr ""
@@ -24110,6 +24647,9 @@ msgstr ""
msgid "Your new personal access token has been created."
msgstr ""
+msgid "Your new project access token has been created."
+msgstr ""
+
msgid "Your password isn't required to view this page. If a password or any other personal details are requested, please contact your administrator to report abuse."
msgstr ""
@@ -24128,12 +24668,18 @@ msgstr ""
msgid "Your request for access has been queued for review."
msgstr ""
+msgid "Your search didn't match any commits."
+msgstr ""
+
msgid "Your subscription expired!"
msgstr ""
msgid "Your subscription has been downgraded"
msgstr ""
+msgid "Your subscription will automatically renew in %{remaining_days}"
+msgstr ""
+
msgid "Your subscription will expire in %{remaining_days}"
msgstr ""
@@ -24173,7 +24719,7 @@ msgstr ""
msgid "allowed to fail"
msgstr ""
-msgid "already being used for another group or project milestone."
+msgid "already being used for another group or project %{timebox_name}."
msgstr ""
msgid "already has a \"created\" issue link"
@@ -24251,6 +24797,9 @@ msgstr ""
msgid "ciReport|%{linkStartTag}Learn more about SAST %{linkEndTag}"
msgstr ""
+msgid "ciReport|%{linkStartTag}Learn more about Secret Scanning %{linkEndTag}"
+msgstr ""
+
msgid "ciReport|%{linkStartTag}Learn more about codequality reports %{linkEndTag}"
msgstr ""
@@ -24419,6 +24968,12 @@ msgstr ""
msgid "ciReport|SAST"
msgstr ""
+msgid "ciReport|Secret scanning"
+msgstr ""
+
+msgid "ciReport|Secret scanning detects secrets and credentials vulnerabilities in your source code."
+msgstr ""
+
msgid "ciReport|Security scanning"
msgstr ""
@@ -24620,6 +25175,9 @@ msgstr ""
msgid "group"
msgstr ""
+msgid "groups"
+msgstr ""
+
msgid "has already been linked to another vulnerability"
msgstr ""
@@ -24785,9 +25343,6 @@ msgstr[2] ""
msgid "merged %{time_ago}"
msgstr ""
-msgid "milestone should belong either to a project or a group."
-msgstr ""
-
msgid "missing"
msgstr ""
@@ -25124,6 +25679,9 @@ msgstr ""
msgid "no contributions"
msgstr ""
+msgid "no expiration"
+msgstr ""
+
msgid "no one can merge"
msgstr ""
@@ -25178,6 +25736,9 @@ msgstr ""
msgid "pending removal"
msgstr ""
+msgid "per day"
+msgstr ""
+
msgid "pipeline"
msgstr ""
@@ -25208,6 +25769,12 @@ msgstr[2] ""
msgid "project avatar"
msgstr ""
+msgid "projects"
+msgstr ""
+
+msgid "push to your repository, create pipelines, create issues or add comments. To reduce storage capacity, delete unused repositories, artifacts, wikis, issues, and pipelines."
+msgstr ""
+
msgid "quick actions"
msgstr ""
@@ -25355,12 +25922,6 @@ msgstr ""
msgid "this document"
msgstr ""
-msgid "thread resolved"
-msgid_plural "threads resolved"
-msgstr[0] ""
-msgstr[1] ""
-msgstr[2] ""
-
msgid "to help your contributors communicate effectively!"
msgstr ""
@@ -25415,6 +25976,9 @@ msgstr ""
msgid "view the blob"
msgstr ""
+msgid "vulnerability|Add a comment"
+msgstr ""
+
msgid "vulnerability|Add a comment or reason for dismissal"
msgstr ""
diff --git a/locale/sv_SE/gitlab.po b/locale/sv_SE/gitlab.po
index 6a721987e6c..144b97410e8 100644
--- a/locale/sv_SE/gitlab.po
+++ b/locale/sv_SE/gitlab.po
@@ -12,7 +12,7 @@ msgstr ""
"X-Crowdin-Project: gitlab-ee\n"
"X-Crowdin-Language: sv-SE\n"
"X-Crowdin-File: /master/locale/gitlab.pot\n"
-"PO-Revision-Date: 2020-04-15 00:28\n"
+"PO-Revision-Date: 2020-05-05 21:36\n"
msgid " %{start} to %{end}"
msgstr ""
@@ -212,6 +212,11 @@ msgid_plural "%d tags"
msgstr[0] ""
msgstr[1] ""
+msgid "%d unresolved thread"
+msgid_plural "%d unresolved threads"
+msgstr[0] ""
+msgstr[1] ""
+
msgid "%d vulnerability dismissed"
msgid_plural "%d vulnerabilities dismissed"
msgstr[0] ""
@@ -371,6 +376,9 @@ msgstr ""
msgid "%{mrText}, this issue will be closed automatically."
msgstr ""
+msgid "%{namespace_name} is now read-only. You cannot: %{base_message}"
+msgstr ""
+
msgid "%{name} contained %{resultsString}"
msgstr ""
@@ -383,6 +391,11 @@ msgstr ""
msgid "%{name}'s avatar"
msgstr ""
+msgid "%{no_of_days} day"
+msgid_plural "%{no_of_days} days"
+msgstr[0] ""
+msgstr[1] ""
+
msgid "%{number_commits_behind} commits behind %{default_branch}, %{number_commits_ahead} commits ahead"
msgstr ""
@@ -506,6 +519,9 @@ msgstr[1] ""
msgid "%{text} is available"
msgstr ""
+msgid "%{timebox_name} should belong either to a project or a group."
+msgstr ""
+
msgid "%{title} %{operator} %{threshold}"
msgstr ""
@@ -781,12 +797,18 @@ msgstr ""
msgid "<code>\"johnsmith@example.com\": \"johnsmith@example.com\"</code> will add \"By <a href=\"#\">johnsmith@example.com</a>\" to all issues and comments originally created by johnsmith@example.com. By default, the email address or username is masked to ensure the user's privacy. Use this option if you want to show the full email address."
msgstr ""
+msgid "<namespace / project>"
+msgstr ""
+
msgid "<no name set>"
msgstr ""
msgid "<no scopes selected>"
msgstr ""
+msgid "<project name>"
+msgstr ""
+
msgid "<strong>%{group_name}</strong> group members"
msgstr ""
@@ -820,6 +842,9 @@ msgstr ""
msgid "A basic page and serverless function that uses AWS Lambda, AWS API Gateway, and GitLab Pages"
msgstr ""
+msgid "A complete DevOps platform"
+msgstr ""
+
msgid "A default branch cannot be chosen for an empty project."
msgstr ""
@@ -1036,6 +1061,12 @@ msgstr ""
msgid "AccessTokens|reset it"
msgstr ""
+msgid "AccessibilityReport|Accessibility report artifact not found"
+msgstr ""
+
+msgid "AccessibilityReport|Failed to retrieve accessibility report"
+msgstr ""
+
msgid "AccessibilityReport|Learn More"
msgstr ""
@@ -1045,6 +1076,9 @@ msgstr ""
msgid "AccessibilityReport|New"
msgstr ""
+msgid "AccessibilityReport|The accessibility scanning found an error of the following type: %{code}"
+msgstr ""
+
msgid "Account"
msgstr ""
@@ -1287,6 +1321,9 @@ msgstr ""
msgid "Added at"
msgstr ""
+msgid "Added for this merge request"
+msgstr ""
+
msgid "Added in this version"
msgstr ""
@@ -1658,6 +1695,72 @@ msgid_plural "Alerts"
msgstr[0] ""
msgstr[1] ""
+msgid "Alert Details"
+msgstr ""
+
+msgid "AlertManagement|Acknowledged"
+msgstr ""
+
+msgid "AlertManagement|Alert"
+msgstr ""
+
+msgid "AlertManagement|Authorize external service"
+msgstr ""
+
+msgid "AlertManagement|Display alerts from all your monitoring tools directly within GitLab. Streamline the investigation of your alerts and the escalation of alerts to incidents."
+msgstr ""
+
+msgid "AlertManagement|End time"
+msgstr ""
+
+msgid "AlertManagement|End time:"
+msgstr ""
+
+msgid "AlertManagement|Events"
+msgstr ""
+
+msgid "AlertManagement|Events:"
+msgstr ""
+
+msgid "AlertManagement|Full Alert Details"
+msgstr ""
+
+msgid "AlertManagement|More information"
+msgstr ""
+
+msgid "AlertManagement|No alerts available to display. If you think you're seeing this message in error, refresh the page."
+msgstr ""
+
+msgid "AlertManagement|No alerts to display."
+msgstr ""
+
+msgid "AlertManagement|Overview"
+msgstr ""
+
+msgid "AlertManagement|Resolved"
+msgstr ""
+
+msgid "AlertManagement|Severity"
+msgstr ""
+
+msgid "AlertManagement|Start time"
+msgstr ""
+
+msgid "AlertManagement|Start time:"
+msgstr ""
+
+msgid "AlertManagement|Status"
+msgstr ""
+
+msgid "AlertManagement|Surface alerts in GitLab"
+msgstr ""
+
+msgid "AlertManagement|There was an error displaying the alerts. Confirm your endpoint's configuration details to ensure alerts appear."
+msgstr ""
+
+msgid "AlertManagement|Triggered"
+msgstr ""
+
msgid "AlertService|%{linkStart}Learn more%{linkEnd} about configuring this endpoint to receive alerts."
msgstr ""
@@ -1727,6 +1830,9 @@ msgstr ""
msgid "All security scans are enabled because %{linkStart}Auto DevOps%{linkEnd} is enabled on this project"
msgstr ""
+msgid "All threads resolved"
+msgstr ""
+
msgid "All users"
msgstr ""
@@ -1745,6 +1851,9 @@ msgstr ""
msgid "Allow only the selected protocols to be used for Git access."
msgstr ""
+msgid "Allow owners to manage default branch protection per group"
+msgstr ""
+
msgid "Allow owners to manually add users outside of LDAP"
msgstr ""
@@ -1853,6 +1962,9 @@ msgstr ""
msgid "An error occurred fetching the dropdown data."
msgstr ""
+msgid "An error occurred fetching the project authors."
+msgstr ""
+
msgid "An error occurred previewing the blob"
msgstr ""
@@ -1934,6 +2046,9 @@ msgstr ""
msgid "An error occurred while fetching sidebar data"
msgstr ""
+msgid "An error occurred while fetching terraform reports."
+msgstr ""
+
msgid "An error occurred while fetching the Service Desk address."
msgstr ""
@@ -2171,6 +2286,9 @@ msgstr ""
msgid "Any"
msgstr ""
+msgid "Any Author"
+msgstr ""
+
msgid "Any Label"
msgstr ""
@@ -2386,10 +2504,10 @@ msgstr ""
msgid "Are you sure that you want to unarchive this project?"
msgstr ""
-msgid "Are you sure you want to cancel creating this comment?"
+msgid "Are you sure you want to cancel editing this comment?"
msgstr ""
-msgid "Are you sure you want to cancel editing this comment?"
+msgid "Are you sure you want to close this blocked issue?"
msgstr ""
msgid "Are you sure you want to delete %{name}?"
@@ -2419,6 +2537,9 @@ msgstr ""
msgid "Are you sure you want to deploy this environment?"
msgstr ""
+msgid "Are you sure you want to discard this comment?"
+msgstr ""
+
msgid "Are you sure you want to erase this build?"
msgstr ""
@@ -2595,6 +2716,9 @@ msgstr ""
msgid "At least one approval from a code owner is required to change files matching the respective CODEOWNER rules."
msgstr ""
+msgid "At least one logging option is required to be enabled"
+msgstr ""
+
msgid "At least one of group_id or project_id must be specified"
msgstr ""
@@ -2633,6 +2757,27 @@ msgstr ""
msgid "AuditEvents|Target"
msgstr ""
+msgid "AuditLogs|(removed)"
+msgstr ""
+
+msgid "AuditLogs|Action"
+msgstr ""
+
+msgid "AuditLogs|Author"
+msgstr ""
+
+msgid "AuditLogs|Date"
+msgstr ""
+
+msgid "AuditLogs|IP Address"
+msgstr ""
+
+msgid "AuditLogs|Object"
+msgstr ""
+
+msgid "AuditLogs|Target"
+msgstr ""
+
msgid "Aug"
msgstr ""
@@ -2915,9 +3060,6 @@ msgstr ""
msgid "BambooService|You must set up automatic revision labeling and a repository trigger in Bamboo."
msgstr ""
-msgid "Batch operations"
-msgstr ""
-
msgid "BatchComments|Delete all pending comments"
msgstr ""
@@ -3047,6 +3189,9 @@ msgstr ""
msgid "Boards"
msgstr ""
+msgid "Boards and Board Lists"
+msgstr ""
+
msgid "Boards|Collapse"
msgstr ""
@@ -3266,6 +3411,9 @@ msgstr ""
msgid "Buy GitLab Enterprise Edition"
msgstr ""
+msgid "Buy more Pipeline minutes"
+msgstr ""
+
msgid "By %{user_name}"
msgstr ""
@@ -3386,6 +3534,9 @@ msgstr ""
msgid "Can't scan the code?"
msgstr ""
+msgid "Can't update snippet: %{err}"
+msgstr ""
+
msgid "Canary"
msgstr ""
@@ -3554,6 +3705,9 @@ msgstr ""
msgid "Changing group path can have unintended side effects."
msgstr ""
+msgid "Charts"
+msgstr ""
+
msgid "Charts can't be displayed as the request for data has timed out. %{documentationLink}"
msgstr ""
@@ -3590,9 +3744,6 @@ msgstr ""
msgid "ChatMessage|and [%{count} more](%{pipeline_failed_jobs_url})"
msgstr ""
-msgid "ChatMessage|failed"
-msgstr ""
-
msgid "ChatMessage|has failed"
msgstr ""
@@ -3608,9 +3759,6 @@ msgstr ""
msgid "ChatMessage|in %{project_link}"
msgstr ""
-msgid "ChatMessage|passed"
-msgstr ""
-
msgid "Check again"
msgstr ""
@@ -4061,9 +4209,6 @@ msgstr ""
msgid "Click any <strong>project name</strong> in the project list below to navigate to the project milestone."
msgstr ""
-msgid "Click here"
-msgstr ""
-
msgid "Click the <strong>Download</strong> button and wait for downloading to complete."
msgstr ""
@@ -4451,6 +4596,12 @@ msgstr ""
msgid "ClusterIntegration|Fetching zones"
msgstr ""
+msgid "ClusterIntegration|Fluentd"
+msgstr ""
+
+msgid "ClusterIntegration|Fluentd is an open source data collector, which lets you unify the data collection and consumption for a better use and understanding of data. It requires at least one of the following logs to be successfully installed."
+msgstr ""
+
msgid "ClusterIntegration|GitLab Integration"
msgstr ""
@@ -4757,6 +4908,15 @@ msgstr ""
msgid "ClusterIntegration|Request to begin uninstalling failed"
msgstr ""
+msgid "ClusterIntegration|SIEM Hostname"
+msgstr ""
+
+msgid "ClusterIntegration|SIEM Port"
+msgstr ""
+
+msgid "ClusterIntegration|SIEM Protocol"
+msgstr ""
+
msgid "ClusterIntegration|Save changes"
msgstr ""
@@ -4853,6 +5013,12 @@ msgstr ""
msgid "ClusterIntegration|Select zone to choose machine type"
msgstr ""
+msgid "ClusterIntegration|Send Cilium Logs"
+msgstr ""
+
+msgid "ClusterIntegration|Send ModSecurity Logs"
+msgstr ""
+
msgid "ClusterIntegration|Service Token"
msgstr ""
@@ -5281,18 +5447,33 @@ msgstr ""
msgid "Compliance framework (optional)"
msgstr ""
+msgid "ComplianceFramework|GDPR"
+msgstr ""
+
msgid "ComplianceFramework|GDPR - General Data Protection Regulation"
msgstr ""
+msgid "ComplianceFramework|HIPAA"
+msgstr ""
+
msgid "ComplianceFramework|HIPAA - Health Insurance Portability and Accountability Act"
msgstr ""
+msgid "ComplianceFramework|PCI-DSS"
+msgstr ""
+
msgid "ComplianceFramework|PCI-DSS - Payment Card Industry-Data Security Standard"
msgstr ""
+msgid "ComplianceFramework|SOC 2"
+msgstr ""
+
msgid "ComplianceFramework|SOC 2 - Service Organization Control 2"
msgstr ""
+msgid "ComplianceFramework|SOX"
+msgstr ""
+
msgid "ComplianceFramework|SOX - Sarbanes-Oxley"
msgstr ""
@@ -5440,9 +5621,15 @@ msgstr ""
msgid "Container repositories sync capacity"
msgstr ""
+msgid "ContainerRegistry| Please visit the %{linkStart}administration settings%{linkEnd} to enable this feature."
+msgstr ""
+
msgid "ContainerRegistry|%{imageName} tags"
msgstr ""
+msgid "ContainerRegistry|%{title} was successfully scheduled for deletion"
+msgstr ""
+
msgid "ContainerRegistry|Automatically remove extra images that aren't designed to be kept."
msgstr ""
@@ -5455,6 +5642,9 @@ msgstr ""
msgid "ContainerRegistry|Container Registry"
msgstr ""
+msgid "ContainerRegistry|Container Registry tag expiration and retention policy is disabled"
+msgstr ""
+
msgid "ContainerRegistry|Copy build command"
msgstr ""
@@ -5464,18 +5654,12 @@ msgstr ""
msgid "ContainerRegistry|Copy push command"
msgstr ""
-msgid "ContainerRegistry|Currently, the Container Registry tag expiration feature is not available for projects created before GitLab version 12.8. For updates and more information, visit Issue %{linkStart}#196124%{linkEnd}"
-msgstr ""
-
msgid "ContainerRegistry|Docker connection error"
msgstr ""
msgid "ContainerRegistry|Docker tag expiration policy is %{toggleStatus}"
msgstr ""
-msgid "ContainerRegistry|Docker tags with names matching this regex pattern will expire:"
-msgstr ""
-
msgid "ContainerRegistry|Edit Settings"
msgstr ""
@@ -5497,9 +5681,6 @@ msgstr ""
msgid "ContainerRegistry|Image ID"
msgstr ""
-msgid "ContainerRegistry|Image deleted successfully"
-msgstr ""
-
msgid "ContainerRegistry|Keep and protect the images that matter most."
msgstr ""
@@ -5515,12 +5696,24 @@ msgstr ""
msgid "ContainerRegistry|Number of tags to retain:"
msgstr ""
+msgid "ContainerRegistry|Please contact your administrator."
+msgstr ""
+
msgid "ContainerRegistry|Push an image"
msgstr ""
msgid "ContainerRegistry|Quick Start"
msgstr ""
+msgid "ContainerRegistry|Regular expressions such as %{codeStart}.*-test%{codeEnd} or %{codeStart}dev-.*%{codeEnd} are supported"
+msgstr ""
+
+msgid "ContainerRegistry|Regular expressions such as %{codeStart}.*-test%{codeEnd} or %{codeStart}dev-.*%{codeEnd} are supported. To select all tags, use %{codeStart}.*%{codeEnd}"
+msgstr ""
+
+msgid "ContainerRegistry|Remember to run %{docLinkStart}garbage collection%{docLinkEnd} to remove the stale data from storage."
+msgstr ""
+
msgid "ContainerRegistry|Remove repository"
msgstr ""
@@ -5535,22 +5728,22 @@ msgstr[1] ""
msgid "ContainerRegistry|Retention policy has been Enabled"
msgstr ""
-msgid "ContainerRegistry|Something went wrong while deleting the image."
+msgid "ContainerRegistry|Something went wrong while fetching the expiration policy."
msgstr ""
-msgid "ContainerRegistry|Something went wrong while deleting the tag."
+msgid "ContainerRegistry|Something went wrong while fetching the repository list."
msgstr ""
-msgid "ContainerRegistry|Something went wrong while deleting the tags."
+msgid "ContainerRegistry|Something went wrong while fetching the tags list."
msgstr ""
-msgid "ContainerRegistry|Something went wrong while fetching the expiration policy."
+msgid "ContainerRegistry|Something went wrong while marking the tag for deletion."
msgstr ""
-msgid "ContainerRegistry|Something went wrong while fetching the packages list."
+msgid "ContainerRegistry|Something went wrong while marking the tags for deletion."
msgstr ""
-msgid "ContainerRegistry|Something went wrong while fetching the tags list."
+msgid "ContainerRegistry|Something went wrong while scheduling %{title} for deletion. Please try again."
msgstr ""
msgid "ContainerRegistry|Something went wrong while updating the expiration policy."
@@ -5559,16 +5752,25 @@ msgstr ""
msgid "ContainerRegistry|Tag"
msgstr ""
-msgid "ContainerRegistry|Tag deleted successfully"
-msgstr ""
-
msgid "ContainerRegistry|Tag expiration policy"
msgstr ""
msgid "ContainerRegistry|Tag expiration policy is designed to:"
msgstr ""
-msgid "ContainerRegistry|Tags deleted successfully"
+msgid "ContainerRegistry|Tag successfully marked for deletion."
+msgstr ""
+
+msgid "ContainerRegistry|Tags successfully marked for deletion."
+msgstr ""
+
+msgid "ContainerRegistry|Tags with names matching this regex pattern will %{italicStart}be preserved:%{italicEnd}"
+msgstr ""
+
+msgid "ContainerRegistry|Tags with names matching this regex pattern will %{italicStart}expire:%{italicEnd}"
+msgstr ""
+
+msgid "ContainerRegistry|The Container Registry tag expiration and retention policies for this project have not been enabled."
msgstr ""
msgid "ContainerRegistry|The last tag related to this image was recently removed. This empty image and any associated data will be automatically removed as part of the regular Garbage Collection process. If you have any questions, contact your administrator."
@@ -5589,16 +5791,16 @@ msgstr ""
msgid "ContainerRegistry|There are no container images stored for this project"
msgstr ""
-msgid "ContainerRegistry|This Registry contains deleted image tag data. Remember to run %{docLinkStart}garbage collection%{docLinkEnd} to remove the stale data from storage."
+msgid "ContainerRegistry|There was an error during the deletion of this image repository, please try again."
msgstr ""
msgid "ContainerRegistry|This image has no active tags"
msgstr ""
-msgid "ContainerRegistry|We are having trouble connecting to Docker, which could be due to an issue with your project name or path. %{docLinkStart}More Information%{docLinkEnd}"
+msgid "ContainerRegistry|This image repository is scheduled for deletion"
msgstr ""
-msgid "ContainerRegistry|Wildcards such as %{codeStart}.*-stable%{codeEnd} or %{codeStart}production/.*%{codeEnd} are supported. To select all tags, use %{codeStart}.*%{codeEnd}"
+msgid "ContainerRegistry|We are having trouble connecting to Docker, which could be due to an issue with your project name or path. %{docLinkStart}More Information%{docLinkEnd}"
msgstr ""
msgid "ContainerRegistry|With the Container Registry, every project can have its own space to store its Docker images. %{docLinkStart}More Information%{docLinkEnd}"
@@ -5853,7 +6055,7 @@ msgstr ""
msgid "Could not delete chat nickname %{chat_name}."
msgstr ""
-msgid "Could not find design"
+msgid "Could not find design."
msgstr ""
msgid "Could not remove the trigger."
@@ -5868,6 +6070,9 @@ msgstr ""
msgid "Could not revoke personal access token %{personal_access_token_name}."
msgstr ""
+msgid "Could not revoke project access token %{project_access_token_name}."
+msgstr ""
+
msgid "Could not save group ID"
msgstr ""
@@ -5925,6 +6130,9 @@ msgstr ""
msgid "Create a new branch"
msgstr ""
+msgid "Create a new deploy key for this project"
+msgstr ""
+
msgid "Create a new file as there are no files yet. Afterwards, you'll be able to commit your changes."
msgstr ""
@@ -6030,6 +6238,9 @@ msgstr ""
msgid "Create requirement"
msgstr ""
+msgid "Create snippet"
+msgstr ""
+
msgid "Create wildcard: %{searchTerm}"
msgstr ""
@@ -6051,6 +6262,9 @@ msgstr ""
msgid "Created"
msgstr ""
+msgid "Created %{timestamp}"
+msgstr ""
+
msgid "Created At"
msgstr ""
@@ -6407,6 +6621,9 @@ msgstr ""
msgid "CycleAnalytics|stage dropdown"
msgstr ""
+msgid "DAG"
+msgstr ""
+
msgid "DNS"
msgstr ""
@@ -6773,6 +6990,9 @@ msgstr ""
msgid "Deploy key was successfully updated."
msgstr ""
+msgid "Deploy keys allow read-only or read-write (if enabled) access to your repository. Deploy keys can be used for CI, staging or production servers. You can create a deploy key or add an existing one."
+msgstr ""
+
msgid "Deploy progress not found. To see pods, ensure your environment matches %{linkStart}deploy board criteria%{linkEnd}."
msgstr ""
@@ -6926,6 +7146,9 @@ msgstr ""
msgid "Deploying to"
msgstr ""
+msgid "Deployment Frequency"
+msgstr ""
+
msgid "Deployment|API"
msgstr ""
@@ -7444,9 +7667,6 @@ msgstr ""
msgid "Edit environment"
msgstr ""
-msgid "Edit epic description"
-msgstr ""
-
msgid "Edit file"
msgstr ""
@@ -8068,9 +8288,6 @@ msgstr ""
msgid "Epics"
msgstr ""
-msgid "Epics (Ultimate / Gold license only)"
-msgstr ""
-
msgid "Epics Roadmap"
msgstr ""
@@ -8080,6 +8297,9 @@ msgstr ""
msgid "Epics let you manage your portfolio of projects more efficiently and with less effort"
msgstr ""
+msgid "Epics, Issues, and Merge Requests"
+msgstr ""
+
msgid "Epics|Add an epic"
msgstr ""
@@ -8131,6 +8351,9 @@ msgstr ""
msgid "Epics|Something went wrong while fetching group epics."
msgstr ""
+msgid "Epics|Something went wrong while moving item."
+msgstr ""
+
msgid "Epics|Something went wrong while ordering item."
msgstr ""
@@ -8185,6 +8408,9 @@ msgstr ""
msgid "Error fetching network graph."
msgstr ""
+msgid "Error fetching payload data."
+msgstr ""
+
msgid "Error fetching projects"
msgstr ""
@@ -8194,9 +8420,6 @@ msgstr ""
msgid "Error fetching the dependency list. Please check your network connection and try again."
msgstr ""
-msgid "Error fetching usage ping data."
-msgstr ""
-
msgid "Error loading branch data. Please try again."
msgstr ""
@@ -8338,6 +8561,9 @@ msgstr ""
msgid "Errors"
msgstr ""
+msgid "Errors:"
+msgstr ""
+
msgid "Estimated"
msgstr ""
@@ -8542,12 +8768,18 @@ msgstr ""
msgid "Export as CSV"
msgstr ""
+msgid "Export group"
+msgstr ""
+
msgid "Export issues"
msgstr ""
msgid "Export project"
msgstr ""
+msgid "Export this group with all related data to a new GitLab instance. Once complete, you can import the data file from the \"New Group\" page."
+msgstr ""
+
msgid "Export this project with all its related data in order to move your project to a new GitLab instance. Once the export is finished, you can import the file from the \"New Project\" page."
msgstr ""
@@ -8617,9 +8849,6 @@ msgstr ""
msgid "Failed Jobs"
msgstr ""
-msgid "Failed create wiki"
-msgstr ""
-
msgid "Failed to add a Zoom meeting"
msgstr ""
@@ -8659,6 +8888,9 @@ msgstr ""
msgid "Failed to create resources"
msgstr ""
+msgid "Failed to create wiki"
+msgstr ""
+
msgid "Failed to delete board. Please try again."
msgstr ""
@@ -8668,7 +8900,7 @@ msgstr ""
msgid "Failed to enqueue the rebase operation, possibly due to a long-lived transaction. Try again later."
msgstr ""
-msgid "Failed to find import label for jira import."
+msgid "Failed to find import label for Jira import."
msgstr ""
msgid "Failed to get ref."
@@ -8692,6 +8924,12 @@ msgstr ""
msgid "Failed to load groups & users."
msgstr ""
+msgid "Failed to load labels. Please try again."
+msgstr ""
+
+msgid "Failed to load milestones. Please try again."
+msgstr ""
+
msgid "Failed to load related branches"
msgstr ""
@@ -9067,7 +9305,10 @@ msgstr ""
msgid "Filter by milestone name"
msgstr ""
-msgid "Filter by name..."
+msgid "Filter by name"
+msgstr ""
+
+msgid "Filter by status"
msgstr ""
msgid "Filter by two-factor authentication"
@@ -9079,12 +9320,18 @@ msgstr ""
msgid "Filter projects"
msgstr ""
+msgid "Filter results"
+msgstr ""
+
msgid "Filter results by group"
msgstr ""
msgid "Filter results by project"
msgstr ""
+msgid "Filter results..."
+msgstr ""
+
msgid "Filter your projects by name"
msgstr ""
@@ -9373,10 +9620,10 @@ msgstr ""
msgid "GeoNodes|Checksummed"
msgstr ""
-msgid "GeoNodes|Container repositories"
+msgid "GeoNodes|Consult Geo troubleshooting information"
msgstr ""
-msgid "GeoNodes|Data is out of date from %{timeago}"
+msgid "GeoNodes|Container repositories"
msgstr ""
msgid "GeoNodes|Data replication lag"
@@ -9418,6 +9665,9 @@ msgstr ""
msgid "GeoNodes|Last event ID seen from primary"
msgstr ""
+msgid "GeoNodes|Learn more about Geo node statuses"
+msgstr ""
+
msgid "GeoNodes|Loading nodes"
msgstr ""
@@ -9433,6 +9683,9 @@ msgstr ""
msgid "GeoNodes|Node was successfully removed."
msgstr ""
+msgid "GeoNodes|Node's status was updated %{timeAgo}."
+msgstr ""
+
msgid "GeoNodes|Not checksummed"
msgstr ""
@@ -9463,7 +9716,10 @@ msgstr ""
msgid "GeoNodes|Repository verification progress"
msgstr ""
-msgid "GeoNodes|Selective"
+msgid "GeoNodes|Selective (%{syncLabel})"
+msgstr ""
+
+msgid "GeoNodes|Selective synchronization"
msgstr ""
msgid "GeoNodes|Something went wrong while changing node status"
@@ -9490,6 +9746,9 @@ msgstr ""
msgid "GeoNodes|Unverified"
msgstr ""
+msgid "GeoNodes|Updated %{timeAgo}"
+msgstr ""
+
msgid "GeoNodes|Used slots"
msgstr ""
@@ -9529,16 +9788,16 @@ msgstr ""
msgid "Geo|All"
msgstr ""
-msgid "Geo|All projects"
+msgid "Geo|All %{replicable_type}"
msgstr ""
-msgid "Geo|All projects are being scheduled for re-sync"
+msgid "Geo|All projects"
msgstr ""
-msgid "Geo|All projects are being scheduled for re-verify"
+msgid "Geo|All projects are being scheduled for resync"
msgstr ""
-msgid "Geo|Batch operations"
+msgid "Geo|All projects are being scheduled for reverify"
msgstr ""
msgid "Geo|Could not remove tracking entry for an existing project."
@@ -9550,9 +9809,15 @@ msgstr ""
msgid "Geo|Failed"
msgstr ""
+msgid "Geo|Filter by status"
+msgstr ""
+
msgid "Geo|Geo Status"
msgstr ""
+msgid "Geo|In progress"
+msgstr ""
+
msgid "Geo|In sync"
msgstr ""
@@ -9577,9 +9842,6 @@ msgstr ""
msgid "Geo|Not synced yet"
msgstr ""
-msgid "Geo|Pending"
-msgstr ""
-
msgid "Geo|Pending synchronization"
msgstr ""
@@ -9616,7 +9878,7 @@ msgstr ""
msgid "Geo|Resync"
msgstr ""
-msgid "Geo|Resync all projects"
+msgid "Geo|Resync all"
msgstr ""
msgid "Geo|Retry count"
@@ -9625,7 +9887,7 @@ msgstr ""
msgid "Geo|Reverify"
msgstr ""
-msgid "Geo|Reverify all projects"
+msgid "Geo|Reverify all"
msgstr ""
msgid "Geo|Status"
@@ -9967,9 +10229,6 @@ msgstr ""
msgid "Go to file"
msgstr ""
-msgid "Go to file (MRs only)"
-msgstr ""
-
msgid "Go to file permalink (while viewing a file)"
msgstr ""
@@ -10414,6 +10673,9 @@ msgstr ""
msgid "GroupSettings|Disable group mentions"
msgstr ""
+msgid "GroupSettings|Export group"
+msgstr ""
+
msgid "GroupSettings|If the parent group's visibility is lower than the group current visibility, visibility levels for subgroups and projects will be changed to match the new parent group's visibility."
msgstr ""
@@ -10492,6 +10754,9 @@ msgstr ""
msgid "Groups (%{count})"
msgstr ""
+msgid "Groups (%{groups})"
+msgstr ""
+
msgid "Groups can also be nested by creating %{subgroup_docs_link_start}subgroups%{subgroup_docs_link_end}."
msgstr ""
@@ -10757,15 +11022,12 @@ msgstr ""
msgid "ID:"
msgstr ""
-msgid "IDE|Allow live previews of JavaScript projects in the Web IDE using CodeSandbox client side evaluation."
+msgid "IDE|Allow live previews of JavaScript projects in the Web IDE using CodeSandbox Live Preview."
msgstr ""
msgid "IDE|Back"
msgstr ""
-msgid "IDE|Client side evaluation"
-msgstr ""
-
msgid "IDE|Commit"
msgstr ""
@@ -10829,6 +11091,9 @@ msgstr ""
msgid "If any job surpasses this timeout threshold, it will be marked as failed. Human readable time input language is accepted like \"1 hour\". Values without specification represent seconds."
msgstr ""
+msgid "If blank, set allowable lifetime to %{instance_level_policy_in_words}, as defined by the instance admin. Once set, existing tokens for users in this group may be revoked."
+msgstr ""
+
msgid "If checked, group owners can manage LDAP group links and LDAP member overrides"
msgstr ""
@@ -10868,6 +11133,9 @@ msgstr ""
msgid "If you lose your recovery codes you can generate new ones, invalidating all previous codes."
msgstr ""
+msgid "If you reach 100%% storage capacity, you will not be able to: %{base_message}"
+msgstr ""
+
msgid "If your HTTP repository is not publicly accessible, add your credentials."
msgstr ""
@@ -10979,6 +11247,9 @@ msgstr ""
msgid "Import repository"
msgstr ""
+msgid "Import started by: %{importInitiator}"
+msgstr ""
+
msgid "Import tasks"
msgstr ""
@@ -11045,6 +11316,9 @@ msgstr ""
msgid "In order to tailor your experience with GitLab we<br>would like to know a bit more about you."
msgstr ""
+msgid "In progress"
+msgstr ""
+
msgid "In the next step, you'll be able to select the projects you want to import."
msgstr ""
@@ -11392,15 +11666,15 @@ msgstr ""
msgid "Issues"
msgstr ""
-msgid "Issues / Merge Requests"
-msgstr ""
-
msgid "Issues Analytics"
msgstr ""
msgid "Issues Rate Limits"
msgstr ""
+msgid "Issues and Merge Requests"
+msgstr ""
+
msgid "Issues can be bugs, tasks or ideas to be discussed. Also, issues are searchable and filterable."
msgstr ""
@@ -11422,10 +11696,10 @@ msgstr ""
msgid "IssuesAnalytics|Avg/Month:"
msgstr ""
-msgid "IssuesAnalytics|Issues created"
+msgid "IssuesAnalytics|Issues opened"
msgstr ""
-msgid "IssuesAnalytics|Issues created per month"
+msgid "IssuesAnalytics|Issues opened per month"
msgstr ""
msgid "IssuesAnalytics|Last 12 months"
@@ -11479,6 +11753,9 @@ msgstr ""
msgid "Jira integration not configured."
msgstr ""
+msgid "Jira project: %{importProject}"
+msgstr ""
+
msgid "JiraService|Events for %{noteable_model_name} are disabled."
msgstr ""
@@ -11868,6 +12145,9 @@ msgstr ""
msgid "Lead"
msgstr ""
+msgid "Lead Time"
+msgstr ""
+
msgid "Learn GitLab"
msgstr ""
@@ -11880,6 +12160,9 @@ msgstr ""
msgid "Learn how to %{no_packages_link_start}publish and share your packages%{no_packages_link_end} with GitLab."
msgstr ""
+msgid "Learn how to enable synchronization"
+msgstr ""
+
msgid "Learn more"
msgstr ""
@@ -12211,9 +12494,6 @@ msgstr ""
msgid "Live preview"
msgstr ""
-msgid "Load more vulnerabilities"
-msgstr ""
-
msgid "Loading"
msgstr ""
@@ -12364,9 +12644,6 @@ msgstr ""
msgid "Manage"
msgstr ""
-msgid "Manage Git repositories with fine-grained access controls that keep your code secure. Perform code reviews and enhance collaboration with merge requests. Each project can also have an issue tracker and a wiki."
-msgstr ""
-
msgid "Manage Web IDE features"
msgstr ""
@@ -12397,6 +12674,9 @@ msgstr ""
msgid "Manage your license"
msgstr ""
+msgid "Managed Account"
+msgstr ""
+
msgid "Manifest"
msgstr ""
@@ -12661,6 +12941,9 @@ msgstr ""
msgid "Merge Requests in Review"
msgstr ""
+msgid "Merge automatically (%{strategy})"
+msgstr ""
+
msgid "Merge commit message"
msgstr ""
@@ -12814,6 +13097,12 @@ msgstr ""
msgid "Merged branches are being deleted. This can take some time depending on the number of branches. Please refresh the page to see changes."
msgstr ""
+msgid "Merged this merge request."
+msgstr ""
+
+msgid "Merges this merge request immediately."
+msgstr ""
+
msgid "Merges this merge request when the pipeline succeeds."
msgstr ""
@@ -12921,12 +13210,15 @@ msgid_plural "Metrics|Edit metrics"
msgstr[0] ""
msgstr[1] ""
-msgid "Metrics|Environment"
+msgid "Metrics|Expand panel"
msgstr ""
msgid "Metrics|For grouping similar metrics"
msgstr ""
+msgid "Metrics|Go back (Esc)"
+msgstr ""
+
msgid "Metrics|Invalid time range, please verify."
msgstr ""
@@ -12957,9 +13249,6 @@ msgstr ""
msgid "Metrics|Refresh dashboard"
msgstr ""
-msgid "Metrics|Show last"
-msgstr ""
-
msgid "Metrics|There was an error creating the dashboard."
msgstr ""
@@ -13038,6 +13327,9 @@ msgstr ""
msgid "Microsoft Azure"
msgstr ""
+msgid "Middleman project with Static Site Editor support"
+msgstr ""
+
msgid "Migrated %{success_count}/%{total_count} files."
msgstr ""
@@ -13483,10 +13775,10 @@ msgstr ""
msgid "Next"
msgstr ""
-msgid "Next file in diff (MRs only)"
+msgid "Next file in diff"
msgstr ""
-msgid "Next unresolved discussion (MRs only)"
+msgid "Next unresolved discussion"
msgstr ""
msgid "Nickname"
@@ -13546,6 +13838,9 @@ msgstr ""
msgid "No changes between %{ref_start}%{source_branch}%{ref_end} and %{ref_start}%{target_branch}%{ref_end}"
msgstr ""
+msgid "No child epics match applied filters"
+msgstr ""
+
msgid "No connection could be made to a Gitaly Server, please check your logs!"
msgstr ""
@@ -13684,15 +13979,6 @@ msgstr ""
msgid "No thanks, don't show this again"
msgstr ""
-msgid "No vulnerabilities found for this group"
-msgstr ""
-
-msgid "No vulnerabilities found for this pipeline"
-msgstr ""
-
-msgid "No vulnerabilities found for this project"
-msgstr ""
-
msgid "No vulnerabilities present"
msgstr ""
@@ -13816,6 +14102,9 @@ msgstr ""
msgid "Nothing to preview."
msgstr ""
+msgid "Nothing to synchronize"
+msgstr ""
+
msgid "Notification events"
msgstr ""
@@ -13987,6 +14276,9 @@ msgstr ""
msgid "Once removed, the fork relationship cannot be restored and you will no longer be able to send merge requests to the source."
msgstr ""
+msgid "Once the exported file is ready you can download it from this page."
+msgstr ""
+
msgid "Once the exported file is ready, you will receive a notification email with a download link, or you can download it from this page."
msgstr ""
@@ -14088,9 +14380,6 @@ msgstr ""
msgid "Open sidebar"
msgstr ""
-msgid "Open source software to collaborate on code"
-msgstr ""
-
msgid "Open: %{openIssuesCount}"
msgstr ""
@@ -14193,6 +14482,9 @@ msgstr ""
msgid "OutdatedBrowser|You can provide feedback %{feedback_link_start}on this issue%{feedback_link_end} or via your usual support channels."
msgstr ""
+msgid "Overridden"
+msgstr ""
+
msgid "Overview"
msgstr ""
@@ -14244,6 +14536,9 @@ msgstr ""
msgid "PackageRegistry|Conan Command"
msgstr ""
+msgid "PackageRegistry|Copy .pypirc content"
+msgstr ""
+
msgid "PackageRegistry|Copy Conan Command"
msgstr ""
@@ -14265,6 +14560,9 @@ msgstr ""
msgid "PackageRegistry|Copy NuGet Setup Command"
msgstr ""
+msgid "PackageRegistry|Copy Pip command"
+msgstr ""
+
msgid "PackageRegistry|Copy and paste this inside your %{codeStart}pom.xml%{codeEnd} %{codeStart}dependencies%{codeEnd} block."
msgstr ""
@@ -14298,6 +14596,12 @@ msgstr ""
msgid "PackageRegistry|For more information on the NuGet registry, %{linkStart}see the documentation%{linkEnd}."
msgstr ""
+msgid "PackageRegistry|For more information on the PyPi registry, %{linkStart}see the documentation%{linkEnd}."
+msgstr ""
+
+msgid "PackageRegistry|If you haven't already done so, you will need to add the below to your %{codeStart}.pypirc%{codeEnd} file."
+msgstr ""
+
msgid "PackageRegistry|If you haven't already done so, you will need to add the below to your %{codeStart}pom.xml%{codeEnd} file."
msgstr ""
@@ -14307,6 +14611,9 @@ msgstr ""
msgid "PackageRegistry|Learn how to %{noPackagesLinkStart}publish and share your packages%{noPackagesLinkEnd} with GitLab."
msgstr ""
+msgid "PackageRegistry|Manually Published"
+msgstr ""
+
msgid "PackageRegistry|Maven"
msgstr ""
@@ -14325,12 +14632,18 @@ msgstr ""
msgid "PackageRegistry|NuGet Command"
msgstr ""
+msgid "PackageRegistry|Pip Command"
+msgstr ""
+
msgid "PackageRegistry|Pipeline %{linkStart}%{linkEnd} triggered %{timestamp} by %{author}"
msgstr ""
msgid "PackageRegistry|Published to the repository at %{timestamp}"
msgstr ""
+msgid "PackageRegistry|PyPi"
+msgstr ""
+
msgid "PackageRegistry|Registry Setup"
msgstr ""
@@ -14367,6 +14680,9 @@ msgstr ""
msgid "PackageRegistry|npm"
msgstr ""
+msgid "PackageRegistry|published by %{author}"
+msgstr ""
+
msgid "PackageRegistry|yarn"
msgstr ""
@@ -14382,6 +14698,9 @@ msgstr ""
msgid "PackageType|NuGet"
msgstr ""
+msgid "PackageType|PyPi"
+msgstr ""
+
msgid "Packages"
msgstr ""
@@ -14475,6 +14794,9 @@ msgstr ""
msgid "Past due"
msgstr ""
+msgid "Paste a machine public key here. Read more about how to generate it"
+msgstr ""
+
msgid "Paste a machine public key here. Read more about how to generate it %{link_start}here%{link_end}"
msgstr ""
@@ -14490,9 +14812,6 @@ msgstr ""
msgid "Path"
msgstr ""
-msgid "Path, transfer, remove"
-msgstr ""
-
msgid "Path:"
msgstr ""
@@ -14523,7 +14842,7 @@ msgstr ""
msgid "Percentage"
msgstr ""
-msgid "Perform advanced options such as changing path, transferring, or removing the group."
+msgid "Perform advanced options such as changing path, transferring, exporting, or removing the group."
msgstr ""
msgid "Perform common operations on GitLab project"
@@ -14604,9 +14923,6 @@ msgstr ""
msgid "Pipeline minutes quota"
msgstr ""
-msgid "Pipeline quota"
-msgstr ""
-
msgid "Pipeline subscriptions"
msgstr ""
@@ -14616,6 +14932,9 @@ msgstr ""
msgid "Pipeline: %{status}"
msgstr ""
+msgid "PipelineCharts|CI / CD Analytics"
+msgstr ""
+
msgid "PipelineCharts|Failed:"
msgstr ""
@@ -14694,15 +15013,6 @@ msgstr ""
msgid "Pipelines settings for '%{project_name}' were successfully updated."
msgstr ""
-msgid "Pipelines| to purchase more minutes."
-msgstr ""
-
-msgid "Pipelines|%{namespace_name} has exceeded its pipeline minutes quota."
-msgstr ""
-
-msgid "Pipelines|%{namespace_name} has less than %{notification_level}%% of CI minutes available."
-msgstr ""
-
msgid "Pipelines|API"
msgstr ""
@@ -14724,10 +15034,13 @@ msgstr ""
msgid "Pipelines|Get started with Pipelines"
msgstr ""
-msgid "Pipelines|Loading Pipelines"
+msgid "Pipelines|Group %{namespace_name} has %{percentage}%% or less Shared Runner Pipeline minutes remaining. Once it runs out, no new jobs or pipelines in its projects will run."
msgstr ""
-msgid "Pipelines|Pipelines will not run anymore on shared Runners."
+msgid "Pipelines|Group %{namespace_name} has exceeded its pipeline minutes quota. Unless you buy additional pipeline minutes, no new jobs or pipelines in its projects will run."
+msgstr ""
+
+msgid "Pipelines|Loading Pipelines"
msgstr ""
msgid "Pipelines|Project cache successfully reset."
@@ -14958,6 +15271,9 @@ msgstr ""
msgid "Please select"
msgstr ""
+msgid "Please select a Jira project"
+msgstr ""
+
msgid "Please select a country"
msgstr ""
@@ -15003,6 +15319,9 @@ msgstr ""
msgid "Please wait while we import the repository for you. Refresh at will."
msgstr ""
+msgid "Plugins directory is deprecated and will be removed in 14.0. Please move this file into /file_hooks directory."
+msgstr ""
+
msgid "Pod does not exist"
msgstr ""
@@ -15120,7 +15439,7 @@ msgstr ""
msgid "Prevent users from changing their profile name"
msgstr ""
-msgid "Prevent users from modifing merge request approvers list"
+msgid "Prevent users from modifying merge request approvers list"
msgstr ""
msgid "Prevent users from performing write operations on GitLab while performing maintenance."
@@ -15141,10 +15460,10 @@ msgstr ""
msgid "Previous Artifacts"
msgstr ""
-msgid "Previous file in diff (MRs only)"
+msgid "Previous file in diff"
msgstr ""
-msgid "Previous unresolved discussion (MRs only)"
+msgid "Previous unresolved discussion"
msgstr ""
msgid "Primary"
@@ -15177,6 +15496,9 @@ msgstr ""
msgid "Private profile"
msgstr ""
+msgid "Private projects Minutes cost factor"
+msgstr ""
+
msgid "Private projects can be created in your personal namespace with:"
msgstr ""
@@ -16005,6 +16327,9 @@ msgstr ""
msgid "ProjectSettings|With GitLab Pages you can host your static websites on GitLab"
msgstr ""
+msgid "ProjectSettings|With Metrics Dashboard you can visualize this project performance metrics"
+msgstr ""
+
msgid "ProjectTemplates|.NET Core"
msgstr ""
@@ -16068,6 +16393,9 @@ msgstr ""
msgid "ProjectTemplates|Spring"
msgstr ""
+msgid "ProjectTemplates|Static Site Editor/Middleman"
+msgstr ""
+
msgid "ProjectTemplates|iOS (Swift)"
msgstr ""
@@ -16287,13 +16615,13 @@ msgstr ""
msgid "PrometheusService|Prometheus is being automatically managed on your clusters"
msgstr ""
-msgid "PrometheusService|These metrics will only be monitored after your first deployment to an environment"
+msgid "PrometheusService|Select the Active checkbox to override the Auto Configuration with custom settings. If unchecked, Auto Configuration settings are used."
msgstr ""
-msgid "PrometheusService|Time-series monitoring service"
+msgid "PrometheusService|These metrics will only be monitored after your first deployment to an environment"
msgstr ""
-msgid "PrometheusService|To enable manual configuration, uninstall Prometheus from your clusters"
+msgid "PrometheusService|Time-series monitoring service"
msgstr ""
msgid "PrometheusService|To enable the installation of Prometheus on your clusters, deactivate the manual configuration below"
@@ -16479,6 +16807,9 @@ msgstr ""
msgid "Protip:"
msgstr ""
+msgid "Protocol"
+msgstr ""
+
msgid "Provider"
msgstr ""
@@ -16503,6 +16834,9 @@ msgstr ""
msgid "Public pipelines"
msgstr ""
+msgid "Public projects Minutes cost factor"
+msgstr ""
+
msgid "Pull"
msgstr ""
@@ -16703,6 +17037,9 @@ msgid_plural "Refreshing in %d seconds to show the updated status..."
msgstr[0] ""
msgstr[1] ""
+msgid "Regenerate export"
+msgstr ""
+
msgid "Regenerate instance ID"
msgstr ""
@@ -17080,9 +17417,29 @@ msgstr ""
msgid "Reports|%{combinedString} and %{resolvedString}"
msgstr ""
+msgid "Reports|Accessibility scanning detected %d issue for the source branch only"
+msgid_plural "Reports|Accessibility scanning detected %d issues for the source branch only"
+msgstr[0] ""
+msgstr[1] ""
+
+msgid "Reports|Accessibility scanning detected no issues for the source branch only"
+msgstr ""
+
+msgid "Reports|Accessibility scanning failed loading results"
+msgstr ""
+
+msgid "Reports|Accessibility scanning results are being parsed"
+msgstr ""
+
msgid "Reports|Actions"
msgstr ""
+msgid "Reports|An error occured while loading report"
+msgstr ""
+
+msgid "Reports|An error occurred while loading %{name} results"
+msgstr ""
+
msgid "Reports|Class"
msgstr ""
@@ -17188,7 +17545,7 @@ msgstr ""
msgid "Requested %{time_ago}"
msgstr ""
-msgid "Requested design version does not exist"
+msgid "Requested design version does not exist."
msgstr ""
msgid "Requested states are invalid"
@@ -17372,6 +17729,9 @@ msgstr ""
msgid "Resync"
msgstr ""
+msgid "Resync all"
+msgstr ""
+
msgid "Resync all %{replicableType}"
msgstr ""
@@ -17440,6 +17800,9 @@ msgstr ""
msgid "Revoked personal access token %{personal_access_token_name}!"
msgstr ""
+msgid "Revoked project access token %{project_access_token_name}!"
+msgstr ""
+
msgid "RightSidebar|adding a"
msgstr ""
@@ -17488,6 +17851,9 @@ msgstr ""
msgid "Runner token"
msgstr ""
+msgid "Runner tokens"
+msgstr ""
+
msgid "Runner was not updated."
msgstr ""
@@ -17542,6 +17908,9 @@ msgstr ""
msgid "SAML SSO for %{group_name}"
msgstr ""
+msgid "SAML discovery tokens"
+msgstr ""
+
msgid "SAML for %{group_name}"
msgstr ""
@@ -17626,12 +17995,18 @@ msgstr ""
msgid "Scheduled"
msgstr ""
+msgid "Scheduled to merge this merge request (%{strategy})."
+msgstr ""
+
msgid "Scheduled to merge this merge request when the pipeline succeeds."
msgstr ""
msgid "Schedules"
msgstr ""
+msgid "Schedules to merge this merge request (%{strategy})."
+msgstr ""
+
msgid "Scheduling"
msgstr ""
@@ -17689,6 +18064,9 @@ msgstr ""
msgid "Search branches and tags"
msgstr ""
+msgid "Search by author"
+msgstr ""
+
msgid "Search files"
msgstr ""
@@ -17834,11 +18212,6 @@ msgid_plural "SearchResults|snippets"
msgstr[0] ""
msgstr[1] ""
-msgid "SearchResults|snippet result"
-msgid_plural "SearchResults|snippet results"
-msgstr[0] ""
-msgstr[1] ""
-
msgid "SearchResults|user"
msgid_plural "SearchResults|users"
msgstr[0] ""
@@ -17849,6 +18222,9 @@ msgid_plural "SearchResults|wiki results"
msgstr[0] ""
msgstr[1] ""
+msgid "Searching by both author and message is currently not supported."
+msgstr ""
+
msgid "Seat Link"
msgstr ""
@@ -17876,184 +18252,232 @@ msgstr ""
msgid "Security Dashboard"
msgstr ""
-msgid "Security Dashboard|Error fetching the vulnerability counts. Please check your network connection and try again."
+msgid "Security configuration help link"
+msgstr ""
+
+msgid "Security dashboard"
msgstr ""
-msgid "Security Dashboard|Error fetching the vulnerability list. Please check your network connection and try again."
+msgid "Security report is out of date. Please update your branch with the latest changes from the target branch (%{targetBranchName})"
msgstr ""
-msgid "Security Dashboard|Issue Created"
+msgid "Security report is out of date. Run %{newPipelineLinkStart}a new pipeline%{newPipelineLinkEnd} for the target branch (%{targetBranchName})"
msgstr ""
-msgid "Security Reports|Comment added to '%{vulnerabilityName}'"
+msgid "SecurityConfiguration|Configured"
msgstr ""
-msgid "Security Reports|Comment deleted on '%{vulnerabilityName}'"
+msgid "SecurityConfiguration|Feature"
msgstr ""
-msgid "Security Reports|Comment edited on '%{vulnerabilityName}'"
+msgid "SecurityConfiguration|Feature documentation for %{featureName}"
msgstr ""
-msgid "Security Reports|Create issue"
+msgid "SecurityConfiguration|Not yet configured"
msgstr ""
-msgid "Security Reports|Dismiss vulnerability"
+msgid "SecurityConfiguration|Secure features"
msgstr ""
-msgid "Security Reports|Dismissed '%{vulnerabilityName}'"
+msgid "SecurityConfiguration|Status"
msgstr ""
-msgid "Security Reports|Dismissed '%{vulnerabilityName}'. Turn off the hide dismissed toggle to view."
+msgid "SecurityReports|%{firstProject} and %{secondProject}"
msgstr ""
-msgid "Security Reports|Either you don't have permission to view this dashboard or the dashboard has not been setup. Please check your permission settings with your administrator or check your dashboard configurations to proceed."
+msgid "SecurityReports|%{firstProject}, %{secondProject}, and %{rest}"
msgstr ""
-msgid "Security Reports|Learn more about setting up your dashboard"
+msgid "SecurityReports|Add a project to your dashboard"
msgstr ""
-msgid "Security Reports|More info"
+msgid "SecurityReports|Add or remove projects from your dashboard"
msgstr ""
-msgid "Security Reports|Oops, something doesn't seem right."
+msgid "SecurityReports|Add projects"
msgstr ""
-msgid "Security Reports|Security reports can only be accessed by authorized users."
+msgid "SecurityReports|Comment added to '%{vulnerabilityName}'"
msgstr ""
-msgid "Security Reports|There was an error adding the comment."
+msgid "SecurityReports|Comment deleted on '%{vulnerabilityName}'"
msgstr ""
-msgid "Security Reports|There was an error creating the issue."
+msgid "SecurityReports|Comment edited on '%{vulnerabilityName}'"
msgstr ""
-msgid "Security Reports|There was an error creating the merge request."
+msgid "SecurityReports|Create issue"
msgstr ""
-msgid "Security Reports|There was an error deleting the comment."
+msgid "SecurityReports|Dismiss Selected"
msgstr ""
-msgid "Security Reports|There was an error dismissing the vulnerabilities."
+msgid "SecurityReports|Dismiss vulnerability"
msgstr ""
-msgid "Security Reports|There was an error dismissing the vulnerability."
+msgid "SecurityReports|Dismissed '%{vulnerabilityName}'"
msgstr ""
-msgid "Security Reports|There was an error reverting the dismissal."
+msgid "SecurityReports|Dismissed '%{vulnerabilityName}'. Turn off the hide dismissed toggle to view."
msgstr ""
-msgid "Security Reports|There was an error reverting this dismissal."
+msgid "SecurityReports|Each vulnerability now has a unique page that can be directly linked to, shared, referenced, and tracked as the single source of truth. Vulnerability occurrences also persist across scanner runs, which improves tracking and visibility and reduces duplicates between scans."
msgstr ""
-msgid "Security Reports|Undo dismiss"
+msgid "SecurityReports|Edit dashboard"
msgstr ""
-msgid "Security Reports|You do not have sufficient permissions to access this report"
+msgid "SecurityReports|Either you don't have permission to view this dashboard or the dashboard has not been setup. Please check your permission settings with your administrator or check your dashboard configurations to proceed."
msgstr ""
-msgid "Security Reports|You must sign in as an authorized user to see this report"
+msgid "SecurityReports|Error fetching the vulnerability counts. Please check your network connection and try again."
msgstr ""
-msgid "Security configuration help link"
+msgid "SecurityReports|Error fetching the vulnerability list. Please check your network connection and try again."
msgstr ""
-msgid "Security dashboard"
+msgid "SecurityReports|False positive"
msgstr ""
-msgid "Security report is out of date. Please update your branch with the latest changes from the target branch (%{targetBranchName})"
+msgid "SecurityReports|Hide dismissed"
msgstr ""
-msgid "Security report is out of date. Run %{newPipelineLinkStart}a new pipeline%{newPipelineLinkEnd} for the target branch (%{targetBranchName})"
+msgid "SecurityReports|Introducing standalone vulnerabilities"
msgstr ""
-msgid "SecurityConfiguration|Configured"
+msgid "SecurityReports|Issue Created"
msgstr ""
-msgid "SecurityConfiguration|Feature"
+msgid "SecurityReports|Learn More"
msgstr ""
-msgid "SecurityConfiguration|Feature documentation for %{featureName}"
+msgid "SecurityReports|Learn more about setting up your dashboard"
msgstr ""
-msgid "SecurityConfiguration|Not yet configured"
+msgid "SecurityReports|Load more vulnerabilities"
msgstr ""
-msgid "SecurityConfiguration|Secure features"
+msgid "SecurityReports|Monitor vulnerabilities in your code"
msgstr ""
-msgid "SecurityConfiguration|Status"
+msgid "SecurityReports|More info"
msgstr ""
-msgid "SecurityDashboard|%{firstProject} and %{secondProject}"
+msgid "SecurityReports|More information"
msgstr ""
-msgid "SecurityDashboard|%{firstProject}, %{secondProject}, and %{rest}"
+msgid "SecurityReports|No vulnerabilities found for dashboard"
msgstr ""
-msgid "SecurityDashboard|Add a project to your dashboard"
+msgid "SecurityReports|No vulnerabilities found for this group"
msgstr ""
-msgid "SecurityDashboard|Add or remove projects from your dashboard"
+msgid "SecurityReports|No vulnerabilities found for this pipeline"
msgstr ""
-msgid "SecurityDashboard|Add projects"
+msgid "SecurityReports|No vulnerabilities found for this project"
msgstr ""
-msgid "SecurityDashboard|Edit dashboard"
+msgid "SecurityReports|Oops, something doesn't seem right."
msgstr ""
-msgid "SecurityDashboard|Hide dismissed"
+msgid "SecurityReports|Pipeline %{pipelineLink} triggered %{timeago} by %{user}"
msgstr ""
-msgid "SecurityDashboard|Monitor vulnerabilities in your code"
+msgid "SecurityReports|Project"
msgstr ""
-msgid "SecurityDashboard|More information"
+msgid "SecurityReports|Projects added"
msgstr ""
-msgid "SecurityDashboard|No vulnerabilities found for dashboard"
+msgid "SecurityReports|Remove project from dashboard"
msgstr ""
-msgid "SecurityDashboard|Pipeline %{pipelineLink} triggered %{timeago} by %{user}"
+msgid "SecurityReports|Report type"
msgstr ""
-msgid "SecurityDashboard|Project"
+msgid "SecurityReports|Return to dashboard"
msgstr ""
-msgid "SecurityDashboard|Projects added"
+msgid "SecurityReports|Security Dashboard"
msgstr ""
-msgid "SecurityDashboard|Remove project from dashboard"
+msgid "SecurityReports|Security reports can only be accessed by authorized users."
msgstr ""
-msgid "SecurityDashboard|Report type"
+msgid "SecurityReports|Select a project to add by using the project search field above."
msgstr ""
-msgid "SecurityDashboard|Return to dashboard"
+msgid "SecurityReports|Select a reason"
msgstr ""
-msgid "SecurityDashboard|Security Dashboard"
+msgid "SecurityReports|Severity"
msgstr ""
-msgid "SecurityDashboard|Select a project to add by using the project search field above."
+msgid "SecurityReports|Status"
msgstr ""
-msgid "SecurityDashboard|Severity"
+msgid "SecurityReports|The security dashboard displays the latest security findings for projects you wish to monitor. Select \"Edit dashboard\" to add and remove projects."
msgstr ""
-msgid "SecurityDashboard|Status"
+msgid "SecurityReports|The security dashboard displays the latest security report. Use it to find and fix vulnerabilities."
msgstr ""
-msgid "SecurityDashboard|The security dashboard displays the latest security findings for projects you wish to monitor. Select \"Edit dashboard\" to add and remove projects."
+msgid "SecurityReports|There was an error adding the comment."
msgstr ""
-msgid "SecurityDashboard|The security dashboard displays the latest security report. Use it to find and fix vulnerabilities."
+msgid "SecurityReports|There was an error creating the issue."
msgstr ""
-msgid "SecurityDashboard|There was an error while generating the report."
+msgid "SecurityReports|There was an error creating the merge request."
msgstr ""
-msgid "SecurityDashboard|Unable to add %{invalidProjects}"
+msgid "SecurityReports|There was an error deleting the comment."
+msgstr ""
+
+msgid "SecurityReports|There was an error dismissing the vulnerabilities."
+msgstr ""
+
+msgid "SecurityReports|There was an error dismissing the vulnerability."
+msgstr ""
+
+msgid "SecurityReports|There was an error reverting the dismissal."
+msgstr ""
+
+msgid "SecurityReports|There was an error reverting this dismissal."
+msgstr ""
+
+msgid "SecurityReports|There was an error while generating the report."
+msgstr ""
+
+msgid "SecurityReports|Unable to add %{invalidProjects}"
+msgstr ""
+
+msgid "SecurityReports|Undo dismiss"
+msgstr ""
+
+msgid "SecurityReports|While it's rare to have no vulnerabilities for your group, it can happen. In any event, we ask that you double check your settings to make sure you've set up your dashboard correctly."
+msgstr ""
+
+msgid "SecurityReports|While it's rare to have no vulnerabilities for your pipeline, it can happen. In any event, we ask that you double check your settings to make sure all security scanning jobs have passed successfully."
+msgstr ""
+
+msgid "SecurityReports|While it's rare to have no vulnerabilities for your project, it can happen. In any event, we ask that you double check your settings to make sure you've set up your dashboard correctly."
+msgstr ""
+
+msgid "SecurityReports|While it's rare to have no vulnerabilities, it can happen. In any event, we ask that you please double check your settings to make sure you've set up your dashboard correctly."
+msgstr ""
+
+msgid "SecurityReports|Won't fix / Accept risk"
+msgstr ""
+
+msgid "SecurityReports|You do not have sufficient permissions to access this report"
+msgstr ""
+
+msgid "SecurityReports|You must sign in as an authorized user to see this report"
+msgstr ""
+
+msgid "SecurityReports|[No reason]"
msgstr ""
msgid "See GitLab's %{password_policy_guidelines}"
@@ -18065,6 +18489,9 @@ msgstr ""
msgid "See the affected projects in the GitLab admin panel"
msgstr ""
+msgid "See what's new at GitLab"
+msgstr ""
+
msgid "Select"
msgstr ""
@@ -18470,9 +18897,6 @@ msgstr ""
msgid "Set up Jira Integration"
msgstr ""
-msgid "Set up Jira Integration illustration"
-msgstr ""
-
msgid "Set up a %{type} Runner automatically"
msgstr ""
@@ -18557,6 +18981,9 @@ msgstr ""
msgid "Severity: %{severity}"
msgstr ""
+msgid "Shards (%{shards})"
+msgstr ""
+
msgid "Shards to synchronize"
msgstr ""
@@ -18871,6 +19298,9 @@ msgstr ""
msgid "Snowplow"
msgstr ""
+msgid "Some child epics may be hidden due to applied filters"
+msgstr ""
+
msgid "Some email servers do not support overriding the email sender name. Enable this option to include the name of the author of the issue, merge request or comment in the email body instead."
msgstr ""
@@ -19012,6 +19442,9 @@ msgstr ""
msgid "Something went wrong while updating your list settings"
msgstr ""
+msgid "Something went wrong with your automatic subscription renewal"
+msgstr ""
+
msgid "Something went wrong, unable to add %{project} to dashboard"
msgstr ""
@@ -19297,6 +19730,9 @@ msgstr ""
msgid "Stage removed"
msgstr ""
+msgid "Standard"
+msgstr ""
+
msgid "Star a label to make it a priority label. Order the prioritized labels to change their relative priority, by dragging."
msgstr ""
@@ -19420,6 +19856,9 @@ msgstr ""
msgid "Static Application Security Testing (SAST)"
msgstr ""
+msgid "StaticSiteEditor|An error occurred while submitting your changes."
+msgstr ""
+
msgid "StaticSiteEditor|Branch could not be created."
msgstr ""
@@ -19429,18 +19868,30 @@ msgstr ""
msgid "StaticSiteEditor|Could not create merge request."
msgstr ""
+msgid "StaticSiteEditor|Incompatible file content"
+msgstr ""
+
msgid "StaticSiteEditor|Return to site"
msgstr ""
+msgid "StaticSiteEditor|Static site editor"
+msgstr ""
+
msgid "StaticSiteEditor|Success!"
msgstr ""
msgid "StaticSiteEditor|Summary of changes"
msgstr ""
+msgid "StaticSiteEditor|The Static Site Editor is currently configured to only edit Markdown content on pages generated from Middleman. Visit the documentation to learn more about configuring your site to use the Static Site Editor."
+msgstr ""
+
msgid "StaticSiteEditor|Update %{sourcePath} file"
msgstr ""
+msgid "StaticSiteEditor|View documentation"
+msgstr ""
+
msgid "StaticSiteEditor|View merge request"
msgstr ""
@@ -19498,6 +19949,12 @@ msgstr ""
msgid "StatusPage|Status page"
msgstr ""
+msgid "StatusPage|Status page URL"
+msgstr ""
+
+msgid "StatusPage|Status page frontend documentation"
+msgstr ""
+
msgid "StatusPage|To publish incidents to an external status page, GitLab will store a JSON file in your Amazon S3 account in a location accessible to your external status page service. Make sure to also set up %{docsLink}"
msgstr ""
@@ -19846,6 +20303,12 @@ msgstr ""
msgid "Synced"
msgstr ""
+msgid "Synchronization disabled"
+msgstr ""
+
+msgid "Synchronization of container repositories is disabled."
+msgstr ""
+
msgid "System"
msgstr ""
@@ -19999,6 +20462,9 @@ msgstr ""
msgid "Telephone number"
msgstr ""
+msgid "Telephone number (Optional)"
+msgstr ""
+
msgid "Template"
msgstr ""
@@ -20305,6 +20771,9 @@ msgstr ""
msgid "The license was successfully uploaded and is now active. You can see the details below."
msgstr ""
+msgid "The license was successfully uploaded and will be active from %{starts_at}. You can see the details below."
+msgstr ""
+
msgid "The maximum file size allowed is %{size}."
msgstr ""
@@ -20626,6 +21095,9 @@ msgstr ""
msgid "There was an error getting the epic participants."
msgstr ""
+msgid "There was an error importing the Jira project."
+msgstr ""
+
msgid "There was an error loading users activity calendar."
msgstr ""
@@ -20641,7 +21113,7 @@ msgstr ""
msgid "There was an error resetting user pipeline minutes."
msgstr ""
-msgid "There was an error saving this Geo Node"
+msgid "There was an error saving this Geo Node."
msgstr ""
msgid "There was an error saving your changes."
@@ -20704,6 +21176,9 @@ msgstr ""
msgid "They can be managed using the %{link}."
msgstr ""
+msgid "Third Party Advisory Link"
+msgstr ""
+
msgid "Third party offers"
msgstr ""
@@ -20740,6 +21215,9 @@ msgstr ""
msgid "This application will be able to:"
msgstr ""
+msgid "This attachment has been truncated to avoid exceeding the maximum allowed attachment size of 15MB. %{written_count} of %{issues_count} issues have been included. Consider re-exporting with a narrower selection of issues."
+msgstr ""
+
msgid "This block is self-referential"
msgstr ""
@@ -20881,6 +21359,12 @@ msgstr ""
msgid "This issue is confidential"
msgstr ""
+msgid "This issue is currently blocked by the following issues: %{issues}."
+msgstr ""
+
+msgid "This issue is in a child epic of the filtered epic"
+msgstr ""
+
msgid "This issue is locked."
msgstr ""
@@ -21079,7 +21563,10 @@ msgstr ""
msgid "This user will be the author of all events in the activity feed that are the result of an update, like new branches being created or new commits being pushed to existing branches. Upon creation or when reassigning you can only assign yourself to be the mirror user."
msgstr ""
-msgid "This variable can not be masked"
+msgid "This variable can not be masked."
+msgstr ""
+
+msgid "This variable does not match the expected pattern."
msgstr ""
msgid "This will help us personalize your onboarding experience."
@@ -21109,10 +21596,10 @@ msgstr ""
msgid "ThreatMonitoring|Application firewall not detected"
msgstr ""
-msgid "ThreatMonitoring|Container Network Policy"
+msgid "ThreatMonitoring|Container Network Policies are not installed or have been disabled. To view this data, ensure your Network Policies are installed and enabled for your cluster."
msgstr ""
-msgid "ThreatMonitoring|Container NetworkPolicies are not installed or has been disabled. To view this data, ensure you NetworkPolicies are installed and enabled for your cluster."
+msgid "ThreatMonitoring|Container Network Policy"
msgstr ""
msgid "ThreatMonitoring|Container NetworkPolicies not detected"
@@ -21145,7 +21632,7 @@ msgstr ""
msgid "ThreatMonitoring|Something went wrong, unable to fetch statistics"
msgstr ""
-msgid "ThreatMonitoring|The firewall is not installed or has been disabled. To view this data, ensure you firewall is installed and enabled for your cluster."
+msgid "ThreatMonitoring|The firewall is not installed or has been disabled. To view this data, ensure the web application firewall is installed and enabled for your cluster."
msgstr ""
msgid "ThreatMonitoring|The graph below is an overview of traffic coming to your application as tracked by the Web Application Firewall (WAF). View the docs for instructions on how to access the WAF logs to see what type of malicious traffic is trying to access your app. The docs link is also accessible by clicking the \"?\" icon next to the title below."
@@ -21214,6 +21701,9 @@ msgstr ""
msgid "Time in seconds GitLab will wait for a response from the external service. When the service does not respond in time, access will be denied."
msgstr ""
+msgid "Time of import: %{importTime}"
+msgstr ""
+
msgid "Time remaining"
msgstr ""
@@ -21398,7 +21888,7 @@ msgstr ""
msgid "Title:"
msgstr ""
-msgid "Titles and Filenames"
+msgid "Titles and Descriptions"
msgstr ""
msgid "To"
@@ -21605,7 +22095,7 @@ msgstr ""
msgid "Too many projects enabled. You will need to manage them via the console or the API."
msgstr ""
-msgid "Topics"
+msgid "Topics (optional)"
msgstr ""
msgid "Total"
@@ -21767,6 +22257,9 @@ msgstr ""
msgid "Try all GitLab has to offer for 30 days."
msgstr ""
+msgid "Try changing or removing filters."
+msgstr ""
+
msgid "Try to fork again"
msgstr ""
@@ -21860,6 +22353,9 @@ msgstr ""
msgid "Unable to connect to server: %{error}"
msgstr ""
+msgid "Unable to connect to the Jira instance. Please check your Jira integration configuration."
+msgstr ""
+
msgid "Unable to convert Kubernetes logs encoding to UTF-8"
msgstr ""
@@ -22803,6 +23299,9 @@ msgstr ""
msgid "View issue"
msgstr ""
+msgid "View issues"
+msgstr ""
+
msgid "View it on GitLab"
msgstr ""
@@ -22980,6 +23479,15 @@ msgstr ""
msgid "VulnerabilityManagement|Resolved %{timeago} by %{user}"
msgstr ""
+msgid "VulnerabilityManagement|Something went wrong while trying to delete the comment. Please try again later."
+msgstr ""
+
+msgid "VulnerabilityManagement|Something went wrong while trying to retrieve the vulnerability history. Please try again later."
+msgstr ""
+
+msgid "VulnerabilityManagement|Something went wrong while trying to save the comment. Please try again later."
+msgstr ""
+
msgid "VulnerabilityManagement|Something went wrong, could not create an issue."
msgstr ""
@@ -23010,6 +23518,9 @@ msgstr ""
msgid "VulnerabilityStatusTypes|Resolved"
msgstr ""
+msgid "Vulnerability|%{scannerName} (version %{scannerVersion})"
+msgstr ""
+
msgid "Vulnerability|Class"
msgstr ""
@@ -23043,7 +23554,10 @@ msgstr ""
msgid "Vulnerability|Project"
msgstr ""
-msgid "Vulnerability|Report Type"
+msgid "Vulnerability|Scanner Provider"
+msgstr ""
+
+msgid "Vulnerability|Scanner Type"
msgstr ""
msgid "Vulnerability|Severity"
@@ -23097,9 +23611,15 @@ msgstr ""
msgid "We sent you an email with reset password instructions"
msgstr ""
+msgid "We tried to automatically renew your %{strong}%{plan_name}%{strong_close} subscription for %{strong}%{namespace_name}%{strong_close} on %{expires_on} but something went wrong so your subscription was downgraded to the free plan. Don't worry, your data is safe. We suggest you check your payment method and get in touch with our support team (%{support_link}). They'll gladly help with your subscription renewal."
+msgstr ""
+
msgid "We want to be sure it is you, please confirm you are not a robot."
msgstr ""
+msgid "We will automatically renew your %{strong}%{plan_name}%{strong_close} subscription for %{strong}%{namespace_name}%{strong_close} on %{strong}%{expires_on}%{strong_close}. There's nothing that you need to do, we'll let you know when the renewal is complete. Need more seats, a higher plan or just want to review your payment method?"
+msgstr ""
+
msgid "We've found no vulnerabilities"
msgstr ""
@@ -23204,15 +23724,6 @@ msgstr ""
msgid "When:"
msgstr ""
-msgid "While it's rare to have no vulnerabilities for your group, it can happen. In any event, we ask that you double check your settings to make sure you've set up your dashboard correctly."
-msgstr ""
-
-msgid "While it's rare to have no vulnerabilities for your pipeline, it can happen. In any event, we ask that you double check your settings to make sure all security scanning jobs have passed successfully."
-msgstr ""
-
-msgid "While it's rare to have no vulnerabilities for your project, it can happen. In any event, we ask that you double check your settings to make sure you've set up your dashboard correctly."
-msgstr ""
-
msgid "While it's rare to have no vulnerabilities, it can happen. In any event, we ask that you please double check your settings to make sure you've set up your dashboard correctly."
msgstr ""
@@ -23243,9 +23754,6 @@ msgstr ""
msgid "Wiki"
msgstr ""
-msgid "Wiki pages"
-msgstr ""
-
msgid "Wiki was successfully updated."
msgstr ""
@@ -23429,6 +23937,9 @@ msgstr ""
msgid "Yes, add it"
msgstr ""
+msgid "Yes, close issue"
+msgstr ""
+
msgid "Yes, let me map Google Code users to full names or GitLab users."
msgstr ""
@@ -23624,6 +24135,9 @@ msgstr ""
msgid "You could not create a new trigger."
msgstr ""
+msgid "You didn't renew your %{strong}%{plan_name}%{strong_close} subscription for %{strong}%{namespace_name}%{strong_close} so it was downgraded to the free plan."
+msgstr ""
+
msgid "You didn't renew your %{strong}%{plan_name}%{strong_close} subscription so it was downgraded to the GitLab Core Plan."
msgstr ""
@@ -23690,6 +24204,9 @@ msgstr ""
msgid "You have declined the invitation to join %{label}."
msgstr ""
+msgid "You have imported from this project %{numberOfPreviousImportsForProject} times before. Each new import will create duplicate issues."
+msgstr ""
+
msgid "You have no permissions"
msgstr ""
@@ -23771,6 +24288,9 @@ msgstr ""
msgid "You need to upload a Google Takeout archive."
msgstr ""
+msgid "You reached %{usage_in_percent} of %{namespace_name}'s capacity (%{used_storage} of %{storage_limit})"
+msgstr ""
+
msgid "You tried to fork %{link_to_the_project} but it failed for the following reason:"
msgstr ""
@@ -23855,9 +24375,21 @@ msgstr ""
msgid "YouTube"
msgstr ""
+msgid "Your %{strong}%{plan_name}%{strong_close} subscription for %{strong}%{namespace_name}%{strong_close} will expire on %{strong}%{expires_on}%{strong_close}. After that, you will not to be able to create issues or merge requests as well as many other features."
+msgstr ""
+
msgid "Your %{strong}%{plan_name}%{strong_close} subscription will expire on %{strong}%{expires_on}%{strong_close}. After that, you will not to be able to create issues or merge requests as well as many other features."
msgstr ""
+msgid "Your CSV export has started. It will be emailed to %{email} when complete."
+msgstr ""
+
+msgid "Your CSV export of %{issues_count} from project %{project_link} has been added to this email as an attachment."
+msgstr ""
+
+msgid "Your CSV export of %{written_count} from project %{project_name} (%{project_url}) has been added to this email as an attachment."
+msgstr ""
+
msgid "Your Commit Email will be used for web based operations, such as edits and merges."
msgstr ""
@@ -23954,6 +24486,9 @@ msgstr ""
msgid "Your comment could not be updated! Please check your network connection and try again."
msgstr ""
+msgid "Your comment will be discarded."
+msgstr ""
+
msgid "Your custom stage '%{title}' was created"
msgstr ""
@@ -23993,6 +24528,9 @@ msgstr ""
msgid "Your new personal access token has been created."
msgstr ""
+msgid "Your new project access token has been created."
+msgstr ""
+
msgid "Your password isn't required to view this page. If a password or any other personal details are requested, please contact your administrator to report abuse."
msgstr ""
@@ -24011,12 +24549,18 @@ msgstr ""
msgid "Your request for access has been queued for review."
msgstr ""
+msgid "Your search didn't match any commits."
+msgstr ""
+
msgid "Your subscription expired!"
msgstr ""
msgid "Your subscription has been downgraded"
msgstr ""
+msgid "Your subscription will automatically renew in %{remaining_days}"
+msgstr ""
+
msgid "Your subscription will expire in %{remaining_days}"
msgstr ""
@@ -24055,7 +24599,7 @@ msgstr ""
msgid "allowed to fail"
msgstr ""
-msgid "already being used for another group or project milestone."
+msgid "already being used for another group or project %{timebox_name}."
msgstr ""
msgid "already has a \"created\" issue link"
@@ -24133,6 +24677,9 @@ msgstr ""
msgid "ciReport|%{linkStartTag}Learn more about SAST %{linkEndTag}"
msgstr ""
+msgid "ciReport|%{linkStartTag}Learn more about Secret Scanning %{linkEndTag}"
+msgstr ""
+
msgid "ciReport|%{linkStartTag}Learn more about codequality reports %{linkEndTag}"
msgstr ""
@@ -24296,6 +24843,12 @@ msgstr ""
msgid "ciReport|SAST"
msgstr ""
+msgid "ciReport|Secret scanning"
+msgstr ""
+
+msgid "ciReport|Secret scanning detects secrets and credentials vulnerabilities in your source code."
+msgstr ""
+
msgid "ciReport|Security scanning"
msgstr ""
@@ -24493,6 +25046,9 @@ msgstr ""
msgid "group"
msgstr ""
+msgid "groups"
+msgstr ""
+
msgid "has already been linked to another vulnerability"
msgstr ""
@@ -24656,9 +25212,6 @@ msgstr[1] ""
msgid "merged %{time_ago}"
msgstr ""
-msgid "milestone should belong either to a project or a group."
-msgstr ""
-
msgid "missing"
msgstr ""
@@ -24995,6 +25548,9 @@ msgstr ""
msgid "no contributions"
msgstr ""
+msgid "no expiration"
+msgstr ""
+
msgid "no one can merge"
msgstr ""
@@ -25047,6 +25603,9 @@ msgstr ""
msgid "pending removal"
msgstr ""
+msgid "per day"
+msgstr ""
+
msgid "pipeline"
msgstr ""
@@ -25075,6 +25634,12 @@ msgstr[1] ""
msgid "project avatar"
msgstr ""
+msgid "projects"
+msgstr ""
+
+msgid "push to your repository, create pipelines, create issues or add comments. To reduce storage capacity, delete unused repositories, artifacts, wikis, issues, and pipelines."
+msgstr ""
+
msgid "quick actions"
msgstr ""
@@ -25221,11 +25786,6 @@ msgstr ""
msgid "this document"
msgstr ""
-msgid "thread resolved"
-msgid_plural "threads resolved"
-msgstr[0] ""
-msgstr[1] ""
-
msgid "to help your contributors communicate effectively!"
msgstr ""
@@ -25280,6 +25840,9 @@ msgstr ""
msgid "view the blob"
msgstr ""
+msgid "vulnerability|Add a comment"
+msgstr ""
+
msgid "vulnerability|Add a comment or reason for dismissal"
msgstr ""
diff --git a/locale/sw_KE/gitlab.po b/locale/sw_KE/gitlab.po
index 32e344661ae..1a6b5db2c39 100644
--- a/locale/sw_KE/gitlab.po
+++ b/locale/sw_KE/gitlab.po
@@ -12,7 +12,7 @@ msgstr ""
"X-Crowdin-Project: gitlab-ee\n"
"X-Crowdin-Language: sw\n"
"X-Crowdin-File: /master/locale/gitlab.pot\n"
-"PO-Revision-Date: 2020-04-15 00:27\n"
+"PO-Revision-Date: 2020-05-05 21:33\n"
msgid " %{start} to %{end}"
msgstr ""
@@ -212,6 +212,11 @@ msgid_plural "%d tags"
msgstr[0] ""
msgstr[1] ""
+msgid "%d unresolved thread"
+msgid_plural "%d unresolved threads"
+msgstr[0] ""
+msgstr[1] ""
+
msgid "%d vulnerability dismissed"
msgid_plural "%d vulnerabilities dismissed"
msgstr[0] ""
@@ -371,6 +376,9 @@ msgstr ""
msgid "%{mrText}, this issue will be closed automatically."
msgstr ""
+msgid "%{namespace_name} is now read-only. You cannot: %{base_message}"
+msgstr ""
+
msgid "%{name} contained %{resultsString}"
msgstr ""
@@ -383,6 +391,11 @@ msgstr ""
msgid "%{name}'s avatar"
msgstr ""
+msgid "%{no_of_days} day"
+msgid_plural "%{no_of_days} days"
+msgstr[0] ""
+msgstr[1] ""
+
msgid "%{number_commits_behind} commits behind %{default_branch}, %{number_commits_ahead} commits ahead"
msgstr ""
@@ -506,6 +519,9 @@ msgstr[1] ""
msgid "%{text} is available"
msgstr ""
+msgid "%{timebox_name} should belong either to a project or a group."
+msgstr ""
+
msgid "%{title} %{operator} %{threshold}"
msgstr ""
@@ -781,12 +797,18 @@ msgstr ""
msgid "<code>\"johnsmith@example.com\": \"johnsmith@example.com\"</code> will add \"By <a href=\"#\">johnsmith@example.com</a>\" to all issues and comments originally created by johnsmith@example.com. By default, the email address or username is masked to ensure the user's privacy. Use this option if you want to show the full email address."
msgstr ""
+msgid "<namespace / project>"
+msgstr ""
+
msgid "<no name set>"
msgstr ""
msgid "<no scopes selected>"
msgstr ""
+msgid "<project name>"
+msgstr ""
+
msgid "<strong>%{group_name}</strong> group members"
msgstr ""
@@ -820,6 +842,9 @@ msgstr ""
msgid "A basic page and serverless function that uses AWS Lambda, AWS API Gateway, and GitLab Pages"
msgstr ""
+msgid "A complete DevOps platform"
+msgstr ""
+
msgid "A default branch cannot be chosen for an empty project."
msgstr ""
@@ -1036,6 +1061,12 @@ msgstr ""
msgid "AccessTokens|reset it"
msgstr ""
+msgid "AccessibilityReport|Accessibility report artifact not found"
+msgstr ""
+
+msgid "AccessibilityReport|Failed to retrieve accessibility report"
+msgstr ""
+
msgid "AccessibilityReport|Learn More"
msgstr ""
@@ -1045,6 +1076,9 @@ msgstr ""
msgid "AccessibilityReport|New"
msgstr ""
+msgid "AccessibilityReport|The accessibility scanning found an error of the following type: %{code}"
+msgstr ""
+
msgid "Account"
msgstr ""
@@ -1287,6 +1321,9 @@ msgstr ""
msgid "Added at"
msgstr ""
+msgid "Added for this merge request"
+msgstr ""
+
msgid "Added in this version"
msgstr ""
@@ -1658,6 +1695,72 @@ msgid_plural "Alerts"
msgstr[0] ""
msgstr[1] ""
+msgid "Alert Details"
+msgstr ""
+
+msgid "AlertManagement|Acknowledged"
+msgstr ""
+
+msgid "AlertManagement|Alert"
+msgstr ""
+
+msgid "AlertManagement|Authorize external service"
+msgstr ""
+
+msgid "AlertManagement|Display alerts from all your monitoring tools directly within GitLab. Streamline the investigation of your alerts and the escalation of alerts to incidents."
+msgstr ""
+
+msgid "AlertManagement|End time"
+msgstr ""
+
+msgid "AlertManagement|End time:"
+msgstr ""
+
+msgid "AlertManagement|Events"
+msgstr ""
+
+msgid "AlertManagement|Events:"
+msgstr ""
+
+msgid "AlertManagement|Full Alert Details"
+msgstr ""
+
+msgid "AlertManagement|More information"
+msgstr ""
+
+msgid "AlertManagement|No alerts available to display. If you think you're seeing this message in error, refresh the page."
+msgstr ""
+
+msgid "AlertManagement|No alerts to display."
+msgstr ""
+
+msgid "AlertManagement|Overview"
+msgstr ""
+
+msgid "AlertManagement|Resolved"
+msgstr ""
+
+msgid "AlertManagement|Severity"
+msgstr ""
+
+msgid "AlertManagement|Start time"
+msgstr ""
+
+msgid "AlertManagement|Start time:"
+msgstr ""
+
+msgid "AlertManagement|Status"
+msgstr ""
+
+msgid "AlertManagement|Surface alerts in GitLab"
+msgstr ""
+
+msgid "AlertManagement|There was an error displaying the alerts. Confirm your endpoint's configuration details to ensure alerts appear."
+msgstr ""
+
+msgid "AlertManagement|Triggered"
+msgstr ""
+
msgid "AlertService|%{linkStart}Learn more%{linkEnd} about configuring this endpoint to receive alerts."
msgstr ""
@@ -1727,6 +1830,9 @@ msgstr ""
msgid "All security scans are enabled because %{linkStart}Auto DevOps%{linkEnd} is enabled on this project"
msgstr ""
+msgid "All threads resolved"
+msgstr ""
+
msgid "All users"
msgstr ""
@@ -1745,6 +1851,9 @@ msgstr ""
msgid "Allow only the selected protocols to be used for Git access."
msgstr ""
+msgid "Allow owners to manage default branch protection per group"
+msgstr ""
+
msgid "Allow owners to manually add users outside of LDAP"
msgstr ""
@@ -1853,6 +1962,9 @@ msgstr ""
msgid "An error occurred fetching the dropdown data."
msgstr ""
+msgid "An error occurred fetching the project authors."
+msgstr ""
+
msgid "An error occurred previewing the blob"
msgstr ""
@@ -1934,6 +2046,9 @@ msgstr ""
msgid "An error occurred while fetching sidebar data"
msgstr ""
+msgid "An error occurred while fetching terraform reports."
+msgstr ""
+
msgid "An error occurred while fetching the Service Desk address."
msgstr ""
@@ -2171,6 +2286,9 @@ msgstr ""
msgid "Any"
msgstr ""
+msgid "Any Author"
+msgstr ""
+
msgid "Any Label"
msgstr ""
@@ -2386,10 +2504,10 @@ msgstr ""
msgid "Are you sure that you want to unarchive this project?"
msgstr ""
-msgid "Are you sure you want to cancel creating this comment?"
+msgid "Are you sure you want to cancel editing this comment?"
msgstr ""
-msgid "Are you sure you want to cancel editing this comment?"
+msgid "Are you sure you want to close this blocked issue?"
msgstr ""
msgid "Are you sure you want to delete %{name}?"
@@ -2419,6 +2537,9 @@ msgstr ""
msgid "Are you sure you want to deploy this environment?"
msgstr ""
+msgid "Are you sure you want to discard this comment?"
+msgstr ""
+
msgid "Are you sure you want to erase this build?"
msgstr ""
@@ -2595,6 +2716,9 @@ msgstr ""
msgid "At least one approval from a code owner is required to change files matching the respective CODEOWNER rules."
msgstr ""
+msgid "At least one logging option is required to be enabled"
+msgstr ""
+
msgid "At least one of group_id or project_id must be specified"
msgstr ""
@@ -2633,6 +2757,27 @@ msgstr ""
msgid "AuditEvents|Target"
msgstr ""
+msgid "AuditLogs|(removed)"
+msgstr ""
+
+msgid "AuditLogs|Action"
+msgstr ""
+
+msgid "AuditLogs|Author"
+msgstr ""
+
+msgid "AuditLogs|Date"
+msgstr ""
+
+msgid "AuditLogs|IP Address"
+msgstr ""
+
+msgid "AuditLogs|Object"
+msgstr ""
+
+msgid "AuditLogs|Target"
+msgstr ""
+
msgid "Aug"
msgstr ""
@@ -2915,9 +3060,6 @@ msgstr ""
msgid "BambooService|You must set up automatic revision labeling and a repository trigger in Bamboo."
msgstr ""
-msgid "Batch operations"
-msgstr ""
-
msgid "BatchComments|Delete all pending comments"
msgstr ""
@@ -3047,6 +3189,9 @@ msgstr ""
msgid "Boards"
msgstr ""
+msgid "Boards and Board Lists"
+msgstr ""
+
msgid "Boards|Collapse"
msgstr ""
@@ -3266,6 +3411,9 @@ msgstr ""
msgid "Buy GitLab Enterprise Edition"
msgstr ""
+msgid "Buy more Pipeline minutes"
+msgstr ""
+
msgid "By %{user_name}"
msgstr ""
@@ -3386,6 +3534,9 @@ msgstr ""
msgid "Can't scan the code?"
msgstr ""
+msgid "Can't update snippet: %{err}"
+msgstr ""
+
msgid "Canary"
msgstr ""
@@ -3554,6 +3705,9 @@ msgstr ""
msgid "Changing group path can have unintended side effects."
msgstr ""
+msgid "Charts"
+msgstr ""
+
msgid "Charts can't be displayed as the request for data has timed out. %{documentationLink}"
msgstr ""
@@ -3590,9 +3744,6 @@ msgstr ""
msgid "ChatMessage|and [%{count} more](%{pipeline_failed_jobs_url})"
msgstr ""
-msgid "ChatMessage|failed"
-msgstr ""
-
msgid "ChatMessage|has failed"
msgstr ""
@@ -3608,9 +3759,6 @@ msgstr ""
msgid "ChatMessage|in %{project_link}"
msgstr ""
-msgid "ChatMessage|passed"
-msgstr ""
-
msgid "Check again"
msgstr ""
@@ -4061,9 +4209,6 @@ msgstr ""
msgid "Click any <strong>project name</strong> in the project list below to navigate to the project milestone."
msgstr ""
-msgid "Click here"
-msgstr ""
-
msgid "Click the <strong>Download</strong> button and wait for downloading to complete."
msgstr ""
@@ -4451,6 +4596,12 @@ msgstr ""
msgid "ClusterIntegration|Fetching zones"
msgstr ""
+msgid "ClusterIntegration|Fluentd"
+msgstr ""
+
+msgid "ClusterIntegration|Fluentd is an open source data collector, which lets you unify the data collection and consumption for a better use and understanding of data. It requires at least one of the following logs to be successfully installed."
+msgstr ""
+
msgid "ClusterIntegration|GitLab Integration"
msgstr ""
@@ -4757,6 +4908,15 @@ msgstr ""
msgid "ClusterIntegration|Request to begin uninstalling failed"
msgstr ""
+msgid "ClusterIntegration|SIEM Hostname"
+msgstr ""
+
+msgid "ClusterIntegration|SIEM Port"
+msgstr ""
+
+msgid "ClusterIntegration|SIEM Protocol"
+msgstr ""
+
msgid "ClusterIntegration|Save changes"
msgstr ""
@@ -4853,6 +5013,12 @@ msgstr ""
msgid "ClusterIntegration|Select zone to choose machine type"
msgstr ""
+msgid "ClusterIntegration|Send Cilium Logs"
+msgstr ""
+
+msgid "ClusterIntegration|Send ModSecurity Logs"
+msgstr ""
+
msgid "ClusterIntegration|Service Token"
msgstr ""
@@ -5281,18 +5447,33 @@ msgstr ""
msgid "Compliance framework (optional)"
msgstr ""
+msgid "ComplianceFramework|GDPR"
+msgstr ""
+
msgid "ComplianceFramework|GDPR - General Data Protection Regulation"
msgstr ""
+msgid "ComplianceFramework|HIPAA"
+msgstr ""
+
msgid "ComplianceFramework|HIPAA - Health Insurance Portability and Accountability Act"
msgstr ""
+msgid "ComplianceFramework|PCI-DSS"
+msgstr ""
+
msgid "ComplianceFramework|PCI-DSS - Payment Card Industry-Data Security Standard"
msgstr ""
+msgid "ComplianceFramework|SOC 2"
+msgstr ""
+
msgid "ComplianceFramework|SOC 2 - Service Organization Control 2"
msgstr ""
+msgid "ComplianceFramework|SOX"
+msgstr ""
+
msgid "ComplianceFramework|SOX - Sarbanes-Oxley"
msgstr ""
@@ -5440,9 +5621,15 @@ msgstr ""
msgid "Container repositories sync capacity"
msgstr ""
+msgid "ContainerRegistry| Please visit the %{linkStart}administration settings%{linkEnd} to enable this feature."
+msgstr ""
+
msgid "ContainerRegistry|%{imageName} tags"
msgstr ""
+msgid "ContainerRegistry|%{title} was successfully scheduled for deletion"
+msgstr ""
+
msgid "ContainerRegistry|Automatically remove extra images that aren't designed to be kept."
msgstr ""
@@ -5455,6 +5642,9 @@ msgstr ""
msgid "ContainerRegistry|Container Registry"
msgstr ""
+msgid "ContainerRegistry|Container Registry tag expiration and retention policy is disabled"
+msgstr ""
+
msgid "ContainerRegistry|Copy build command"
msgstr ""
@@ -5464,18 +5654,12 @@ msgstr ""
msgid "ContainerRegistry|Copy push command"
msgstr ""
-msgid "ContainerRegistry|Currently, the Container Registry tag expiration feature is not available for projects created before GitLab version 12.8. For updates and more information, visit Issue %{linkStart}#196124%{linkEnd}"
-msgstr ""
-
msgid "ContainerRegistry|Docker connection error"
msgstr ""
msgid "ContainerRegistry|Docker tag expiration policy is %{toggleStatus}"
msgstr ""
-msgid "ContainerRegistry|Docker tags with names matching this regex pattern will expire:"
-msgstr ""
-
msgid "ContainerRegistry|Edit Settings"
msgstr ""
@@ -5497,9 +5681,6 @@ msgstr ""
msgid "ContainerRegistry|Image ID"
msgstr ""
-msgid "ContainerRegistry|Image deleted successfully"
-msgstr ""
-
msgid "ContainerRegistry|Keep and protect the images that matter most."
msgstr ""
@@ -5515,12 +5696,24 @@ msgstr ""
msgid "ContainerRegistry|Number of tags to retain:"
msgstr ""
+msgid "ContainerRegistry|Please contact your administrator."
+msgstr ""
+
msgid "ContainerRegistry|Push an image"
msgstr ""
msgid "ContainerRegistry|Quick Start"
msgstr ""
+msgid "ContainerRegistry|Regular expressions such as %{codeStart}.*-test%{codeEnd} or %{codeStart}dev-.*%{codeEnd} are supported"
+msgstr ""
+
+msgid "ContainerRegistry|Regular expressions such as %{codeStart}.*-test%{codeEnd} or %{codeStart}dev-.*%{codeEnd} are supported. To select all tags, use %{codeStart}.*%{codeEnd}"
+msgstr ""
+
+msgid "ContainerRegistry|Remember to run %{docLinkStart}garbage collection%{docLinkEnd} to remove the stale data from storage."
+msgstr ""
+
msgid "ContainerRegistry|Remove repository"
msgstr ""
@@ -5535,22 +5728,22 @@ msgstr[1] ""
msgid "ContainerRegistry|Retention policy has been Enabled"
msgstr ""
-msgid "ContainerRegistry|Something went wrong while deleting the image."
+msgid "ContainerRegistry|Something went wrong while fetching the expiration policy."
msgstr ""
-msgid "ContainerRegistry|Something went wrong while deleting the tag."
+msgid "ContainerRegistry|Something went wrong while fetching the repository list."
msgstr ""
-msgid "ContainerRegistry|Something went wrong while deleting the tags."
+msgid "ContainerRegistry|Something went wrong while fetching the tags list."
msgstr ""
-msgid "ContainerRegistry|Something went wrong while fetching the expiration policy."
+msgid "ContainerRegistry|Something went wrong while marking the tag for deletion."
msgstr ""
-msgid "ContainerRegistry|Something went wrong while fetching the packages list."
+msgid "ContainerRegistry|Something went wrong while marking the tags for deletion."
msgstr ""
-msgid "ContainerRegistry|Something went wrong while fetching the tags list."
+msgid "ContainerRegistry|Something went wrong while scheduling %{title} for deletion. Please try again."
msgstr ""
msgid "ContainerRegistry|Something went wrong while updating the expiration policy."
@@ -5559,16 +5752,25 @@ msgstr ""
msgid "ContainerRegistry|Tag"
msgstr ""
-msgid "ContainerRegistry|Tag deleted successfully"
-msgstr ""
-
msgid "ContainerRegistry|Tag expiration policy"
msgstr ""
msgid "ContainerRegistry|Tag expiration policy is designed to:"
msgstr ""
-msgid "ContainerRegistry|Tags deleted successfully"
+msgid "ContainerRegistry|Tag successfully marked for deletion."
+msgstr ""
+
+msgid "ContainerRegistry|Tags successfully marked for deletion."
+msgstr ""
+
+msgid "ContainerRegistry|Tags with names matching this regex pattern will %{italicStart}be preserved:%{italicEnd}"
+msgstr ""
+
+msgid "ContainerRegistry|Tags with names matching this regex pattern will %{italicStart}expire:%{italicEnd}"
+msgstr ""
+
+msgid "ContainerRegistry|The Container Registry tag expiration and retention policies for this project have not been enabled."
msgstr ""
msgid "ContainerRegistry|The last tag related to this image was recently removed. This empty image and any associated data will be automatically removed as part of the regular Garbage Collection process. If you have any questions, contact your administrator."
@@ -5589,16 +5791,16 @@ msgstr ""
msgid "ContainerRegistry|There are no container images stored for this project"
msgstr ""
-msgid "ContainerRegistry|This Registry contains deleted image tag data. Remember to run %{docLinkStart}garbage collection%{docLinkEnd} to remove the stale data from storage."
+msgid "ContainerRegistry|There was an error during the deletion of this image repository, please try again."
msgstr ""
msgid "ContainerRegistry|This image has no active tags"
msgstr ""
-msgid "ContainerRegistry|We are having trouble connecting to Docker, which could be due to an issue with your project name or path. %{docLinkStart}More Information%{docLinkEnd}"
+msgid "ContainerRegistry|This image repository is scheduled for deletion"
msgstr ""
-msgid "ContainerRegistry|Wildcards such as %{codeStart}.*-stable%{codeEnd} or %{codeStart}production/.*%{codeEnd} are supported. To select all tags, use %{codeStart}.*%{codeEnd}"
+msgid "ContainerRegistry|We are having trouble connecting to Docker, which could be due to an issue with your project name or path. %{docLinkStart}More Information%{docLinkEnd}"
msgstr ""
msgid "ContainerRegistry|With the Container Registry, every project can have its own space to store its Docker images. %{docLinkStart}More Information%{docLinkEnd}"
@@ -5853,7 +6055,7 @@ msgstr ""
msgid "Could not delete chat nickname %{chat_name}."
msgstr ""
-msgid "Could not find design"
+msgid "Could not find design."
msgstr ""
msgid "Could not remove the trigger."
@@ -5868,6 +6070,9 @@ msgstr ""
msgid "Could not revoke personal access token %{personal_access_token_name}."
msgstr ""
+msgid "Could not revoke project access token %{project_access_token_name}."
+msgstr ""
+
msgid "Could not save group ID"
msgstr ""
@@ -5925,6 +6130,9 @@ msgstr ""
msgid "Create a new branch"
msgstr ""
+msgid "Create a new deploy key for this project"
+msgstr ""
+
msgid "Create a new file as there are no files yet. Afterwards, you'll be able to commit your changes."
msgstr ""
@@ -6030,6 +6238,9 @@ msgstr ""
msgid "Create requirement"
msgstr ""
+msgid "Create snippet"
+msgstr ""
+
msgid "Create wildcard: %{searchTerm}"
msgstr ""
@@ -6051,6 +6262,9 @@ msgstr ""
msgid "Created"
msgstr ""
+msgid "Created %{timestamp}"
+msgstr ""
+
msgid "Created At"
msgstr ""
@@ -6407,6 +6621,9 @@ msgstr ""
msgid "CycleAnalytics|stage dropdown"
msgstr ""
+msgid "DAG"
+msgstr ""
+
msgid "DNS"
msgstr ""
@@ -6773,6 +6990,9 @@ msgstr ""
msgid "Deploy key was successfully updated."
msgstr ""
+msgid "Deploy keys allow read-only or read-write (if enabled) access to your repository. Deploy keys can be used for CI, staging or production servers. You can create a deploy key or add an existing one."
+msgstr ""
+
msgid "Deploy progress not found. To see pods, ensure your environment matches %{linkStart}deploy board criteria%{linkEnd}."
msgstr ""
@@ -6926,6 +7146,9 @@ msgstr ""
msgid "Deploying to"
msgstr ""
+msgid "Deployment Frequency"
+msgstr ""
+
msgid "Deployment|API"
msgstr ""
@@ -7444,9 +7667,6 @@ msgstr ""
msgid "Edit environment"
msgstr ""
-msgid "Edit epic description"
-msgstr ""
-
msgid "Edit file"
msgstr ""
@@ -8068,9 +8288,6 @@ msgstr ""
msgid "Epics"
msgstr ""
-msgid "Epics (Ultimate / Gold license only)"
-msgstr ""
-
msgid "Epics Roadmap"
msgstr ""
@@ -8080,6 +8297,9 @@ msgstr ""
msgid "Epics let you manage your portfolio of projects more efficiently and with less effort"
msgstr ""
+msgid "Epics, Issues, and Merge Requests"
+msgstr ""
+
msgid "Epics|Add an epic"
msgstr ""
@@ -8131,6 +8351,9 @@ msgstr ""
msgid "Epics|Something went wrong while fetching group epics."
msgstr ""
+msgid "Epics|Something went wrong while moving item."
+msgstr ""
+
msgid "Epics|Something went wrong while ordering item."
msgstr ""
@@ -8185,6 +8408,9 @@ msgstr ""
msgid "Error fetching network graph."
msgstr ""
+msgid "Error fetching payload data."
+msgstr ""
+
msgid "Error fetching projects"
msgstr ""
@@ -8194,9 +8420,6 @@ msgstr ""
msgid "Error fetching the dependency list. Please check your network connection and try again."
msgstr ""
-msgid "Error fetching usage ping data."
-msgstr ""
-
msgid "Error loading branch data. Please try again."
msgstr ""
@@ -8338,6 +8561,9 @@ msgstr ""
msgid "Errors"
msgstr ""
+msgid "Errors:"
+msgstr ""
+
msgid "Estimated"
msgstr ""
@@ -8542,12 +8768,18 @@ msgstr ""
msgid "Export as CSV"
msgstr ""
+msgid "Export group"
+msgstr ""
+
msgid "Export issues"
msgstr ""
msgid "Export project"
msgstr ""
+msgid "Export this group with all related data to a new GitLab instance. Once complete, you can import the data file from the \"New Group\" page."
+msgstr ""
+
msgid "Export this project with all its related data in order to move your project to a new GitLab instance. Once the export is finished, you can import the file from the \"New Project\" page."
msgstr ""
@@ -8617,9 +8849,6 @@ msgstr ""
msgid "Failed Jobs"
msgstr ""
-msgid "Failed create wiki"
-msgstr ""
-
msgid "Failed to add a Zoom meeting"
msgstr ""
@@ -8659,6 +8888,9 @@ msgstr ""
msgid "Failed to create resources"
msgstr ""
+msgid "Failed to create wiki"
+msgstr ""
+
msgid "Failed to delete board. Please try again."
msgstr ""
@@ -8668,7 +8900,7 @@ msgstr ""
msgid "Failed to enqueue the rebase operation, possibly due to a long-lived transaction. Try again later."
msgstr ""
-msgid "Failed to find import label for jira import."
+msgid "Failed to find import label for Jira import."
msgstr ""
msgid "Failed to get ref."
@@ -8692,6 +8924,12 @@ msgstr ""
msgid "Failed to load groups & users."
msgstr ""
+msgid "Failed to load labels. Please try again."
+msgstr ""
+
+msgid "Failed to load milestones. Please try again."
+msgstr ""
+
msgid "Failed to load related branches"
msgstr ""
@@ -9067,7 +9305,10 @@ msgstr ""
msgid "Filter by milestone name"
msgstr ""
-msgid "Filter by name..."
+msgid "Filter by name"
+msgstr ""
+
+msgid "Filter by status"
msgstr ""
msgid "Filter by two-factor authentication"
@@ -9079,12 +9320,18 @@ msgstr ""
msgid "Filter projects"
msgstr ""
+msgid "Filter results"
+msgstr ""
+
msgid "Filter results by group"
msgstr ""
msgid "Filter results by project"
msgstr ""
+msgid "Filter results..."
+msgstr ""
+
msgid "Filter your projects by name"
msgstr ""
@@ -9373,10 +9620,10 @@ msgstr ""
msgid "GeoNodes|Checksummed"
msgstr ""
-msgid "GeoNodes|Container repositories"
+msgid "GeoNodes|Consult Geo troubleshooting information"
msgstr ""
-msgid "GeoNodes|Data is out of date from %{timeago}"
+msgid "GeoNodes|Container repositories"
msgstr ""
msgid "GeoNodes|Data replication lag"
@@ -9418,6 +9665,9 @@ msgstr ""
msgid "GeoNodes|Last event ID seen from primary"
msgstr ""
+msgid "GeoNodes|Learn more about Geo node statuses"
+msgstr ""
+
msgid "GeoNodes|Loading nodes"
msgstr ""
@@ -9433,6 +9683,9 @@ msgstr ""
msgid "GeoNodes|Node was successfully removed."
msgstr ""
+msgid "GeoNodes|Node's status was updated %{timeAgo}."
+msgstr ""
+
msgid "GeoNodes|Not checksummed"
msgstr ""
@@ -9463,7 +9716,10 @@ msgstr ""
msgid "GeoNodes|Repository verification progress"
msgstr ""
-msgid "GeoNodes|Selective"
+msgid "GeoNodes|Selective (%{syncLabel})"
+msgstr ""
+
+msgid "GeoNodes|Selective synchronization"
msgstr ""
msgid "GeoNodes|Something went wrong while changing node status"
@@ -9490,6 +9746,9 @@ msgstr ""
msgid "GeoNodes|Unverified"
msgstr ""
+msgid "GeoNodes|Updated %{timeAgo}"
+msgstr ""
+
msgid "GeoNodes|Used slots"
msgstr ""
@@ -9529,16 +9788,16 @@ msgstr ""
msgid "Geo|All"
msgstr ""
-msgid "Geo|All projects"
+msgid "Geo|All %{replicable_type}"
msgstr ""
-msgid "Geo|All projects are being scheduled for re-sync"
+msgid "Geo|All projects"
msgstr ""
-msgid "Geo|All projects are being scheduled for re-verify"
+msgid "Geo|All projects are being scheduled for resync"
msgstr ""
-msgid "Geo|Batch operations"
+msgid "Geo|All projects are being scheduled for reverify"
msgstr ""
msgid "Geo|Could not remove tracking entry for an existing project."
@@ -9550,9 +9809,15 @@ msgstr ""
msgid "Geo|Failed"
msgstr ""
+msgid "Geo|Filter by status"
+msgstr ""
+
msgid "Geo|Geo Status"
msgstr ""
+msgid "Geo|In progress"
+msgstr ""
+
msgid "Geo|In sync"
msgstr ""
@@ -9577,9 +9842,6 @@ msgstr ""
msgid "Geo|Not synced yet"
msgstr ""
-msgid "Geo|Pending"
-msgstr ""
-
msgid "Geo|Pending synchronization"
msgstr ""
@@ -9616,7 +9878,7 @@ msgstr ""
msgid "Geo|Resync"
msgstr ""
-msgid "Geo|Resync all projects"
+msgid "Geo|Resync all"
msgstr ""
msgid "Geo|Retry count"
@@ -9625,7 +9887,7 @@ msgstr ""
msgid "Geo|Reverify"
msgstr ""
-msgid "Geo|Reverify all projects"
+msgid "Geo|Reverify all"
msgstr ""
msgid "Geo|Status"
@@ -9967,9 +10229,6 @@ msgstr ""
msgid "Go to file"
msgstr ""
-msgid "Go to file (MRs only)"
-msgstr ""
-
msgid "Go to file permalink (while viewing a file)"
msgstr ""
@@ -10414,6 +10673,9 @@ msgstr ""
msgid "GroupSettings|Disable group mentions"
msgstr ""
+msgid "GroupSettings|Export group"
+msgstr ""
+
msgid "GroupSettings|If the parent group's visibility is lower than the group current visibility, visibility levels for subgroups and projects will be changed to match the new parent group's visibility."
msgstr ""
@@ -10492,6 +10754,9 @@ msgstr ""
msgid "Groups (%{count})"
msgstr ""
+msgid "Groups (%{groups})"
+msgstr ""
+
msgid "Groups can also be nested by creating %{subgroup_docs_link_start}subgroups%{subgroup_docs_link_end}."
msgstr ""
@@ -10757,15 +11022,12 @@ msgstr ""
msgid "ID:"
msgstr ""
-msgid "IDE|Allow live previews of JavaScript projects in the Web IDE using CodeSandbox client side evaluation."
+msgid "IDE|Allow live previews of JavaScript projects in the Web IDE using CodeSandbox Live Preview."
msgstr ""
msgid "IDE|Back"
msgstr ""
-msgid "IDE|Client side evaluation"
-msgstr ""
-
msgid "IDE|Commit"
msgstr ""
@@ -10829,6 +11091,9 @@ msgstr ""
msgid "If any job surpasses this timeout threshold, it will be marked as failed. Human readable time input language is accepted like \"1 hour\". Values without specification represent seconds."
msgstr ""
+msgid "If blank, set allowable lifetime to %{instance_level_policy_in_words}, as defined by the instance admin. Once set, existing tokens for users in this group may be revoked."
+msgstr ""
+
msgid "If checked, group owners can manage LDAP group links and LDAP member overrides"
msgstr ""
@@ -10868,6 +11133,9 @@ msgstr ""
msgid "If you lose your recovery codes you can generate new ones, invalidating all previous codes."
msgstr ""
+msgid "If you reach 100%% storage capacity, you will not be able to: %{base_message}"
+msgstr ""
+
msgid "If your HTTP repository is not publicly accessible, add your credentials."
msgstr ""
@@ -10979,6 +11247,9 @@ msgstr ""
msgid "Import repository"
msgstr ""
+msgid "Import started by: %{importInitiator}"
+msgstr ""
+
msgid "Import tasks"
msgstr ""
@@ -11045,6 +11316,9 @@ msgstr ""
msgid "In order to tailor your experience with GitLab we<br>would like to know a bit more about you."
msgstr ""
+msgid "In progress"
+msgstr ""
+
msgid "In the next step, you'll be able to select the projects you want to import."
msgstr ""
@@ -11392,15 +11666,15 @@ msgstr ""
msgid "Issues"
msgstr ""
-msgid "Issues / Merge Requests"
-msgstr ""
-
msgid "Issues Analytics"
msgstr ""
msgid "Issues Rate Limits"
msgstr ""
+msgid "Issues and Merge Requests"
+msgstr ""
+
msgid "Issues can be bugs, tasks or ideas to be discussed. Also, issues are searchable and filterable."
msgstr ""
@@ -11422,10 +11696,10 @@ msgstr ""
msgid "IssuesAnalytics|Avg/Month:"
msgstr ""
-msgid "IssuesAnalytics|Issues created"
+msgid "IssuesAnalytics|Issues opened"
msgstr ""
-msgid "IssuesAnalytics|Issues created per month"
+msgid "IssuesAnalytics|Issues opened per month"
msgstr ""
msgid "IssuesAnalytics|Last 12 months"
@@ -11479,6 +11753,9 @@ msgstr ""
msgid "Jira integration not configured."
msgstr ""
+msgid "Jira project: %{importProject}"
+msgstr ""
+
msgid "JiraService|Events for %{noteable_model_name} are disabled."
msgstr ""
@@ -11868,6 +12145,9 @@ msgstr ""
msgid "Lead"
msgstr ""
+msgid "Lead Time"
+msgstr ""
+
msgid "Learn GitLab"
msgstr ""
@@ -11880,6 +12160,9 @@ msgstr ""
msgid "Learn how to %{no_packages_link_start}publish and share your packages%{no_packages_link_end} with GitLab."
msgstr ""
+msgid "Learn how to enable synchronization"
+msgstr ""
+
msgid "Learn more"
msgstr ""
@@ -12211,9 +12494,6 @@ msgstr ""
msgid "Live preview"
msgstr ""
-msgid "Load more vulnerabilities"
-msgstr ""
-
msgid "Loading"
msgstr ""
@@ -12364,9 +12644,6 @@ msgstr ""
msgid "Manage"
msgstr ""
-msgid "Manage Git repositories with fine-grained access controls that keep your code secure. Perform code reviews and enhance collaboration with merge requests. Each project can also have an issue tracker and a wiki."
-msgstr ""
-
msgid "Manage Web IDE features"
msgstr ""
@@ -12397,6 +12674,9 @@ msgstr ""
msgid "Manage your license"
msgstr ""
+msgid "Managed Account"
+msgstr ""
+
msgid "Manifest"
msgstr ""
@@ -12661,6 +12941,9 @@ msgstr ""
msgid "Merge Requests in Review"
msgstr ""
+msgid "Merge automatically (%{strategy})"
+msgstr ""
+
msgid "Merge commit message"
msgstr ""
@@ -12814,6 +13097,12 @@ msgstr ""
msgid "Merged branches are being deleted. This can take some time depending on the number of branches. Please refresh the page to see changes."
msgstr ""
+msgid "Merged this merge request."
+msgstr ""
+
+msgid "Merges this merge request immediately."
+msgstr ""
+
msgid "Merges this merge request when the pipeline succeeds."
msgstr ""
@@ -12921,12 +13210,15 @@ msgid_plural "Metrics|Edit metrics"
msgstr[0] ""
msgstr[1] ""
-msgid "Metrics|Environment"
+msgid "Metrics|Expand panel"
msgstr ""
msgid "Metrics|For grouping similar metrics"
msgstr ""
+msgid "Metrics|Go back (Esc)"
+msgstr ""
+
msgid "Metrics|Invalid time range, please verify."
msgstr ""
@@ -12957,9 +13249,6 @@ msgstr ""
msgid "Metrics|Refresh dashboard"
msgstr ""
-msgid "Metrics|Show last"
-msgstr ""
-
msgid "Metrics|There was an error creating the dashboard."
msgstr ""
@@ -13038,6 +13327,9 @@ msgstr ""
msgid "Microsoft Azure"
msgstr ""
+msgid "Middleman project with Static Site Editor support"
+msgstr ""
+
msgid "Migrated %{success_count}/%{total_count} files."
msgstr ""
@@ -13483,10 +13775,10 @@ msgstr ""
msgid "Next"
msgstr ""
-msgid "Next file in diff (MRs only)"
+msgid "Next file in diff"
msgstr ""
-msgid "Next unresolved discussion (MRs only)"
+msgid "Next unresolved discussion"
msgstr ""
msgid "Nickname"
@@ -13546,6 +13838,9 @@ msgstr ""
msgid "No changes between %{ref_start}%{source_branch}%{ref_end} and %{ref_start}%{target_branch}%{ref_end}"
msgstr ""
+msgid "No child epics match applied filters"
+msgstr ""
+
msgid "No connection could be made to a Gitaly Server, please check your logs!"
msgstr ""
@@ -13684,15 +13979,6 @@ msgstr ""
msgid "No thanks, don't show this again"
msgstr ""
-msgid "No vulnerabilities found for this group"
-msgstr ""
-
-msgid "No vulnerabilities found for this pipeline"
-msgstr ""
-
-msgid "No vulnerabilities found for this project"
-msgstr ""
-
msgid "No vulnerabilities present"
msgstr ""
@@ -13816,6 +14102,9 @@ msgstr ""
msgid "Nothing to preview."
msgstr ""
+msgid "Nothing to synchronize"
+msgstr ""
+
msgid "Notification events"
msgstr ""
@@ -13987,6 +14276,9 @@ msgstr ""
msgid "Once removed, the fork relationship cannot be restored and you will no longer be able to send merge requests to the source."
msgstr ""
+msgid "Once the exported file is ready you can download it from this page."
+msgstr ""
+
msgid "Once the exported file is ready, you will receive a notification email with a download link, or you can download it from this page."
msgstr ""
@@ -14088,9 +14380,6 @@ msgstr ""
msgid "Open sidebar"
msgstr ""
-msgid "Open source software to collaborate on code"
-msgstr ""
-
msgid "Open: %{openIssuesCount}"
msgstr ""
@@ -14193,6 +14482,9 @@ msgstr ""
msgid "OutdatedBrowser|You can provide feedback %{feedback_link_start}on this issue%{feedback_link_end} or via your usual support channels."
msgstr ""
+msgid "Overridden"
+msgstr ""
+
msgid "Overview"
msgstr ""
@@ -14244,6 +14536,9 @@ msgstr ""
msgid "PackageRegistry|Conan Command"
msgstr ""
+msgid "PackageRegistry|Copy .pypirc content"
+msgstr ""
+
msgid "PackageRegistry|Copy Conan Command"
msgstr ""
@@ -14265,6 +14560,9 @@ msgstr ""
msgid "PackageRegistry|Copy NuGet Setup Command"
msgstr ""
+msgid "PackageRegistry|Copy Pip command"
+msgstr ""
+
msgid "PackageRegistry|Copy and paste this inside your %{codeStart}pom.xml%{codeEnd} %{codeStart}dependencies%{codeEnd} block."
msgstr ""
@@ -14298,6 +14596,12 @@ msgstr ""
msgid "PackageRegistry|For more information on the NuGet registry, %{linkStart}see the documentation%{linkEnd}."
msgstr ""
+msgid "PackageRegistry|For more information on the PyPi registry, %{linkStart}see the documentation%{linkEnd}."
+msgstr ""
+
+msgid "PackageRegistry|If you haven't already done so, you will need to add the below to your %{codeStart}.pypirc%{codeEnd} file."
+msgstr ""
+
msgid "PackageRegistry|If you haven't already done so, you will need to add the below to your %{codeStart}pom.xml%{codeEnd} file."
msgstr ""
@@ -14307,6 +14611,9 @@ msgstr ""
msgid "PackageRegistry|Learn how to %{noPackagesLinkStart}publish and share your packages%{noPackagesLinkEnd} with GitLab."
msgstr ""
+msgid "PackageRegistry|Manually Published"
+msgstr ""
+
msgid "PackageRegistry|Maven"
msgstr ""
@@ -14325,12 +14632,18 @@ msgstr ""
msgid "PackageRegistry|NuGet Command"
msgstr ""
+msgid "PackageRegistry|Pip Command"
+msgstr ""
+
msgid "PackageRegistry|Pipeline %{linkStart}%{linkEnd} triggered %{timestamp} by %{author}"
msgstr ""
msgid "PackageRegistry|Published to the repository at %{timestamp}"
msgstr ""
+msgid "PackageRegistry|PyPi"
+msgstr ""
+
msgid "PackageRegistry|Registry Setup"
msgstr ""
@@ -14367,6 +14680,9 @@ msgstr ""
msgid "PackageRegistry|npm"
msgstr ""
+msgid "PackageRegistry|published by %{author}"
+msgstr ""
+
msgid "PackageRegistry|yarn"
msgstr ""
@@ -14382,6 +14698,9 @@ msgstr ""
msgid "PackageType|NuGet"
msgstr ""
+msgid "PackageType|PyPi"
+msgstr ""
+
msgid "Packages"
msgstr ""
@@ -14475,6 +14794,9 @@ msgstr ""
msgid "Past due"
msgstr ""
+msgid "Paste a machine public key here. Read more about how to generate it"
+msgstr ""
+
msgid "Paste a machine public key here. Read more about how to generate it %{link_start}here%{link_end}"
msgstr ""
@@ -14490,9 +14812,6 @@ msgstr ""
msgid "Path"
msgstr ""
-msgid "Path, transfer, remove"
-msgstr ""
-
msgid "Path:"
msgstr ""
@@ -14523,7 +14842,7 @@ msgstr ""
msgid "Percentage"
msgstr ""
-msgid "Perform advanced options such as changing path, transferring, or removing the group."
+msgid "Perform advanced options such as changing path, transferring, exporting, or removing the group."
msgstr ""
msgid "Perform common operations on GitLab project"
@@ -14604,9 +14923,6 @@ msgstr ""
msgid "Pipeline minutes quota"
msgstr ""
-msgid "Pipeline quota"
-msgstr ""
-
msgid "Pipeline subscriptions"
msgstr ""
@@ -14616,6 +14932,9 @@ msgstr ""
msgid "Pipeline: %{status}"
msgstr ""
+msgid "PipelineCharts|CI / CD Analytics"
+msgstr ""
+
msgid "PipelineCharts|Failed:"
msgstr ""
@@ -14694,15 +15013,6 @@ msgstr ""
msgid "Pipelines settings for '%{project_name}' were successfully updated."
msgstr ""
-msgid "Pipelines| to purchase more minutes."
-msgstr ""
-
-msgid "Pipelines|%{namespace_name} has exceeded its pipeline minutes quota."
-msgstr ""
-
-msgid "Pipelines|%{namespace_name} has less than %{notification_level}%% of CI minutes available."
-msgstr ""
-
msgid "Pipelines|API"
msgstr ""
@@ -14724,10 +15034,13 @@ msgstr ""
msgid "Pipelines|Get started with Pipelines"
msgstr ""
-msgid "Pipelines|Loading Pipelines"
+msgid "Pipelines|Group %{namespace_name} has %{percentage}%% or less Shared Runner Pipeline minutes remaining. Once it runs out, no new jobs or pipelines in its projects will run."
msgstr ""
-msgid "Pipelines|Pipelines will not run anymore on shared Runners."
+msgid "Pipelines|Group %{namespace_name} has exceeded its pipeline minutes quota. Unless you buy additional pipeline minutes, no new jobs or pipelines in its projects will run."
+msgstr ""
+
+msgid "Pipelines|Loading Pipelines"
msgstr ""
msgid "Pipelines|Project cache successfully reset."
@@ -14958,6 +15271,9 @@ msgstr ""
msgid "Please select"
msgstr ""
+msgid "Please select a Jira project"
+msgstr ""
+
msgid "Please select a country"
msgstr ""
@@ -15003,6 +15319,9 @@ msgstr ""
msgid "Please wait while we import the repository for you. Refresh at will."
msgstr ""
+msgid "Plugins directory is deprecated and will be removed in 14.0. Please move this file into /file_hooks directory."
+msgstr ""
+
msgid "Pod does not exist"
msgstr ""
@@ -15120,7 +15439,7 @@ msgstr ""
msgid "Prevent users from changing their profile name"
msgstr ""
-msgid "Prevent users from modifing merge request approvers list"
+msgid "Prevent users from modifying merge request approvers list"
msgstr ""
msgid "Prevent users from performing write operations on GitLab while performing maintenance."
@@ -15141,10 +15460,10 @@ msgstr ""
msgid "Previous Artifacts"
msgstr ""
-msgid "Previous file in diff (MRs only)"
+msgid "Previous file in diff"
msgstr ""
-msgid "Previous unresolved discussion (MRs only)"
+msgid "Previous unresolved discussion"
msgstr ""
msgid "Primary"
@@ -15177,6 +15496,9 @@ msgstr ""
msgid "Private profile"
msgstr ""
+msgid "Private projects Minutes cost factor"
+msgstr ""
+
msgid "Private projects can be created in your personal namespace with:"
msgstr ""
@@ -16005,6 +16327,9 @@ msgstr ""
msgid "ProjectSettings|With GitLab Pages you can host your static websites on GitLab"
msgstr ""
+msgid "ProjectSettings|With Metrics Dashboard you can visualize this project performance metrics"
+msgstr ""
+
msgid "ProjectTemplates|.NET Core"
msgstr ""
@@ -16068,6 +16393,9 @@ msgstr ""
msgid "ProjectTemplates|Spring"
msgstr ""
+msgid "ProjectTemplates|Static Site Editor/Middleman"
+msgstr ""
+
msgid "ProjectTemplates|iOS (Swift)"
msgstr ""
@@ -16287,13 +16615,13 @@ msgstr ""
msgid "PrometheusService|Prometheus is being automatically managed on your clusters"
msgstr ""
-msgid "PrometheusService|These metrics will only be monitored after your first deployment to an environment"
+msgid "PrometheusService|Select the Active checkbox to override the Auto Configuration with custom settings. If unchecked, Auto Configuration settings are used."
msgstr ""
-msgid "PrometheusService|Time-series monitoring service"
+msgid "PrometheusService|These metrics will only be monitored after your first deployment to an environment"
msgstr ""
-msgid "PrometheusService|To enable manual configuration, uninstall Prometheus from your clusters"
+msgid "PrometheusService|Time-series monitoring service"
msgstr ""
msgid "PrometheusService|To enable the installation of Prometheus on your clusters, deactivate the manual configuration below"
@@ -16479,6 +16807,9 @@ msgstr ""
msgid "Protip:"
msgstr ""
+msgid "Protocol"
+msgstr ""
+
msgid "Provider"
msgstr ""
@@ -16503,6 +16834,9 @@ msgstr ""
msgid "Public pipelines"
msgstr ""
+msgid "Public projects Minutes cost factor"
+msgstr ""
+
msgid "Pull"
msgstr ""
@@ -16703,6 +17037,9 @@ msgid_plural "Refreshing in %d seconds to show the updated status..."
msgstr[0] ""
msgstr[1] ""
+msgid "Regenerate export"
+msgstr ""
+
msgid "Regenerate instance ID"
msgstr ""
@@ -17080,9 +17417,29 @@ msgstr ""
msgid "Reports|%{combinedString} and %{resolvedString}"
msgstr ""
+msgid "Reports|Accessibility scanning detected %d issue for the source branch only"
+msgid_plural "Reports|Accessibility scanning detected %d issues for the source branch only"
+msgstr[0] ""
+msgstr[1] ""
+
+msgid "Reports|Accessibility scanning detected no issues for the source branch only"
+msgstr ""
+
+msgid "Reports|Accessibility scanning failed loading results"
+msgstr ""
+
+msgid "Reports|Accessibility scanning results are being parsed"
+msgstr ""
+
msgid "Reports|Actions"
msgstr ""
+msgid "Reports|An error occured while loading report"
+msgstr ""
+
+msgid "Reports|An error occurred while loading %{name} results"
+msgstr ""
+
msgid "Reports|Class"
msgstr ""
@@ -17188,7 +17545,7 @@ msgstr ""
msgid "Requested %{time_ago}"
msgstr ""
-msgid "Requested design version does not exist"
+msgid "Requested design version does not exist."
msgstr ""
msgid "Requested states are invalid"
@@ -17372,6 +17729,9 @@ msgstr ""
msgid "Resync"
msgstr ""
+msgid "Resync all"
+msgstr ""
+
msgid "Resync all %{replicableType}"
msgstr ""
@@ -17440,6 +17800,9 @@ msgstr ""
msgid "Revoked personal access token %{personal_access_token_name}!"
msgstr ""
+msgid "Revoked project access token %{project_access_token_name}!"
+msgstr ""
+
msgid "RightSidebar|adding a"
msgstr ""
@@ -17488,6 +17851,9 @@ msgstr ""
msgid "Runner token"
msgstr ""
+msgid "Runner tokens"
+msgstr ""
+
msgid "Runner was not updated."
msgstr ""
@@ -17542,6 +17908,9 @@ msgstr ""
msgid "SAML SSO for %{group_name}"
msgstr ""
+msgid "SAML discovery tokens"
+msgstr ""
+
msgid "SAML for %{group_name}"
msgstr ""
@@ -17626,12 +17995,18 @@ msgstr ""
msgid "Scheduled"
msgstr ""
+msgid "Scheduled to merge this merge request (%{strategy})."
+msgstr ""
+
msgid "Scheduled to merge this merge request when the pipeline succeeds."
msgstr ""
msgid "Schedules"
msgstr ""
+msgid "Schedules to merge this merge request (%{strategy})."
+msgstr ""
+
msgid "Scheduling"
msgstr ""
@@ -17689,6 +18064,9 @@ msgstr ""
msgid "Search branches and tags"
msgstr ""
+msgid "Search by author"
+msgstr ""
+
msgid "Search files"
msgstr ""
@@ -17834,11 +18212,6 @@ msgid_plural "SearchResults|snippets"
msgstr[0] ""
msgstr[1] ""
-msgid "SearchResults|snippet result"
-msgid_plural "SearchResults|snippet results"
-msgstr[0] ""
-msgstr[1] ""
-
msgid "SearchResults|user"
msgid_plural "SearchResults|users"
msgstr[0] ""
@@ -17849,6 +18222,9 @@ msgid_plural "SearchResults|wiki results"
msgstr[0] ""
msgstr[1] ""
+msgid "Searching by both author and message is currently not supported."
+msgstr ""
+
msgid "Seat Link"
msgstr ""
@@ -17876,184 +18252,232 @@ msgstr ""
msgid "Security Dashboard"
msgstr ""
-msgid "Security Dashboard|Error fetching the vulnerability counts. Please check your network connection and try again."
+msgid "Security configuration help link"
+msgstr ""
+
+msgid "Security dashboard"
msgstr ""
-msgid "Security Dashboard|Error fetching the vulnerability list. Please check your network connection and try again."
+msgid "Security report is out of date. Please update your branch with the latest changes from the target branch (%{targetBranchName})"
msgstr ""
-msgid "Security Dashboard|Issue Created"
+msgid "Security report is out of date. Run %{newPipelineLinkStart}a new pipeline%{newPipelineLinkEnd} for the target branch (%{targetBranchName})"
msgstr ""
-msgid "Security Reports|Comment added to '%{vulnerabilityName}'"
+msgid "SecurityConfiguration|Configured"
msgstr ""
-msgid "Security Reports|Comment deleted on '%{vulnerabilityName}'"
+msgid "SecurityConfiguration|Feature"
msgstr ""
-msgid "Security Reports|Comment edited on '%{vulnerabilityName}'"
+msgid "SecurityConfiguration|Feature documentation for %{featureName}"
msgstr ""
-msgid "Security Reports|Create issue"
+msgid "SecurityConfiguration|Not yet configured"
msgstr ""
-msgid "Security Reports|Dismiss vulnerability"
+msgid "SecurityConfiguration|Secure features"
msgstr ""
-msgid "Security Reports|Dismissed '%{vulnerabilityName}'"
+msgid "SecurityConfiguration|Status"
msgstr ""
-msgid "Security Reports|Dismissed '%{vulnerabilityName}'. Turn off the hide dismissed toggle to view."
+msgid "SecurityReports|%{firstProject} and %{secondProject}"
msgstr ""
-msgid "Security Reports|Either you don't have permission to view this dashboard or the dashboard has not been setup. Please check your permission settings with your administrator or check your dashboard configurations to proceed."
+msgid "SecurityReports|%{firstProject}, %{secondProject}, and %{rest}"
msgstr ""
-msgid "Security Reports|Learn more about setting up your dashboard"
+msgid "SecurityReports|Add a project to your dashboard"
msgstr ""
-msgid "Security Reports|More info"
+msgid "SecurityReports|Add or remove projects from your dashboard"
msgstr ""
-msgid "Security Reports|Oops, something doesn't seem right."
+msgid "SecurityReports|Add projects"
msgstr ""
-msgid "Security Reports|Security reports can only be accessed by authorized users."
+msgid "SecurityReports|Comment added to '%{vulnerabilityName}'"
msgstr ""
-msgid "Security Reports|There was an error adding the comment."
+msgid "SecurityReports|Comment deleted on '%{vulnerabilityName}'"
msgstr ""
-msgid "Security Reports|There was an error creating the issue."
+msgid "SecurityReports|Comment edited on '%{vulnerabilityName}'"
msgstr ""
-msgid "Security Reports|There was an error creating the merge request."
+msgid "SecurityReports|Create issue"
msgstr ""
-msgid "Security Reports|There was an error deleting the comment."
+msgid "SecurityReports|Dismiss Selected"
msgstr ""
-msgid "Security Reports|There was an error dismissing the vulnerabilities."
+msgid "SecurityReports|Dismiss vulnerability"
msgstr ""
-msgid "Security Reports|There was an error dismissing the vulnerability."
+msgid "SecurityReports|Dismissed '%{vulnerabilityName}'"
msgstr ""
-msgid "Security Reports|There was an error reverting the dismissal."
+msgid "SecurityReports|Dismissed '%{vulnerabilityName}'. Turn off the hide dismissed toggle to view."
msgstr ""
-msgid "Security Reports|There was an error reverting this dismissal."
+msgid "SecurityReports|Each vulnerability now has a unique page that can be directly linked to, shared, referenced, and tracked as the single source of truth. Vulnerability occurrences also persist across scanner runs, which improves tracking and visibility and reduces duplicates between scans."
msgstr ""
-msgid "Security Reports|Undo dismiss"
+msgid "SecurityReports|Edit dashboard"
msgstr ""
-msgid "Security Reports|You do not have sufficient permissions to access this report"
+msgid "SecurityReports|Either you don't have permission to view this dashboard or the dashboard has not been setup. Please check your permission settings with your administrator or check your dashboard configurations to proceed."
msgstr ""
-msgid "Security Reports|You must sign in as an authorized user to see this report"
+msgid "SecurityReports|Error fetching the vulnerability counts. Please check your network connection and try again."
msgstr ""
-msgid "Security configuration help link"
+msgid "SecurityReports|Error fetching the vulnerability list. Please check your network connection and try again."
msgstr ""
-msgid "Security dashboard"
+msgid "SecurityReports|False positive"
msgstr ""
-msgid "Security report is out of date. Please update your branch with the latest changes from the target branch (%{targetBranchName})"
+msgid "SecurityReports|Hide dismissed"
msgstr ""
-msgid "Security report is out of date. Run %{newPipelineLinkStart}a new pipeline%{newPipelineLinkEnd} for the target branch (%{targetBranchName})"
+msgid "SecurityReports|Introducing standalone vulnerabilities"
msgstr ""
-msgid "SecurityConfiguration|Configured"
+msgid "SecurityReports|Issue Created"
msgstr ""
-msgid "SecurityConfiguration|Feature"
+msgid "SecurityReports|Learn More"
msgstr ""
-msgid "SecurityConfiguration|Feature documentation for %{featureName}"
+msgid "SecurityReports|Learn more about setting up your dashboard"
msgstr ""
-msgid "SecurityConfiguration|Not yet configured"
+msgid "SecurityReports|Load more vulnerabilities"
msgstr ""
-msgid "SecurityConfiguration|Secure features"
+msgid "SecurityReports|Monitor vulnerabilities in your code"
msgstr ""
-msgid "SecurityConfiguration|Status"
+msgid "SecurityReports|More info"
msgstr ""
-msgid "SecurityDashboard|%{firstProject} and %{secondProject}"
+msgid "SecurityReports|More information"
msgstr ""
-msgid "SecurityDashboard|%{firstProject}, %{secondProject}, and %{rest}"
+msgid "SecurityReports|No vulnerabilities found for dashboard"
msgstr ""
-msgid "SecurityDashboard|Add a project to your dashboard"
+msgid "SecurityReports|No vulnerabilities found for this group"
msgstr ""
-msgid "SecurityDashboard|Add or remove projects from your dashboard"
+msgid "SecurityReports|No vulnerabilities found for this pipeline"
msgstr ""
-msgid "SecurityDashboard|Add projects"
+msgid "SecurityReports|No vulnerabilities found for this project"
msgstr ""
-msgid "SecurityDashboard|Edit dashboard"
+msgid "SecurityReports|Oops, something doesn't seem right."
msgstr ""
-msgid "SecurityDashboard|Hide dismissed"
+msgid "SecurityReports|Pipeline %{pipelineLink} triggered %{timeago} by %{user}"
msgstr ""
-msgid "SecurityDashboard|Monitor vulnerabilities in your code"
+msgid "SecurityReports|Project"
msgstr ""
-msgid "SecurityDashboard|More information"
+msgid "SecurityReports|Projects added"
msgstr ""
-msgid "SecurityDashboard|No vulnerabilities found for dashboard"
+msgid "SecurityReports|Remove project from dashboard"
msgstr ""
-msgid "SecurityDashboard|Pipeline %{pipelineLink} triggered %{timeago} by %{user}"
+msgid "SecurityReports|Report type"
msgstr ""
-msgid "SecurityDashboard|Project"
+msgid "SecurityReports|Return to dashboard"
msgstr ""
-msgid "SecurityDashboard|Projects added"
+msgid "SecurityReports|Security Dashboard"
msgstr ""
-msgid "SecurityDashboard|Remove project from dashboard"
+msgid "SecurityReports|Security reports can only be accessed by authorized users."
msgstr ""
-msgid "SecurityDashboard|Report type"
+msgid "SecurityReports|Select a project to add by using the project search field above."
msgstr ""
-msgid "SecurityDashboard|Return to dashboard"
+msgid "SecurityReports|Select a reason"
msgstr ""
-msgid "SecurityDashboard|Security Dashboard"
+msgid "SecurityReports|Severity"
msgstr ""
-msgid "SecurityDashboard|Select a project to add by using the project search field above."
+msgid "SecurityReports|Status"
msgstr ""
-msgid "SecurityDashboard|Severity"
+msgid "SecurityReports|The security dashboard displays the latest security findings for projects you wish to monitor. Select \"Edit dashboard\" to add and remove projects."
msgstr ""
-msgid "SecurityDashboard|Status"
+msgid "SecurityReports|The security dashboard displays the latest security report. Use it to find and fix vulnerabilities."
msgstr ""
-msgid "SecurityDashboard|The security dashboard displays the latest security findings for projects you wish to monitor. Select \"Edit dashboard\" to add and remove projects."
+msgid "SecurityReports|There was an error adding the comment."
msgstr ""
-msgid "SecurityDashboard|The security dashboard displays the latest security report. Use it to find and fix vulnerabilities."
+msgid "SecurityReports|There was an error creating the issue."
msgstr ""
-msgid "SecurityDashboard|There was an error while generating the report."
+msgid "SecurityReports|There was an error creating the merge request."
msgstr ""
-msgid "SecurityDashboard|Unable to add %{invalidProjects}"
+msgid "SecurityReports|There was an error deleting the comment."
+msgstr ""
+
+msgid "SecurityReports|There was an error dismissing the vulnerabilities."
+msgstr ""
+
+msgid "SecurityReports|There was an error dismissing the vulnerability."
+msgstr ""
+
+msgid "SecurityReports|There was an error reverting the dismissal."
+msgstr ""
+
+msgid "SecurityReports|There was an error reverting this dismissal."
+msgstr ""
+
+msgid "SecurityReports|There was an error while generating the report."
+msgstr ""
+
+msgid "SecurityReports|Unable to add %{invalidProjects}"
+msgstr ""
+
+msgid "SecurityReports|Undo dismiss"
+msgstr ""
+
+msgid "SecurityReports|While it's rare to have no vulnerabilities for your group, it can happen. In any event, we ask that you double check your settings to make sure you've set up your dashboard correctly."
+msgstr ""
+
+msgid "SecurityReports|While it's rare to have no vulnerabilities for your pipeline, it can happen. In any event, we ask that you double check your settings to make sure all security scanning jobs have passed successfully."
+msgstr ""
+
+msgid "SecurityReports|While it's rare to have no vulnerabilities for your project, it can happen. In any event, we ask that you double check your settings to make sure you've set up your dashboard correctly."
+msgstr ""
+
+msgid "SecurityReports|While it's rare to have no vulnerabilities, it can happen. In any event, we ask that you please double check your settings to make sure you've set up your dashboard correctly."
+msgstr ""
+
+msgid "SecurityReports|Won't fix / Accept risk"
+msgstr ""
+
+msgid "SecurityReports|You do not have sufficient permissions to access this report"
+msgstr ""
+
+msgid "SecurityReports|You must sign in as an authorized user to see this report"
+msgstr ""
+
+msgid "SecurityReports|[No reason]"
msgstr ""
msgid "See GitLab's %{password_policy_guidelines}"
@@ -18065,6 +18489,9 @@ msgstr ""
msgid "See the affected projects in the GitLab admin panel"
msgstr ""
+msgid "See what's new at GitLab"
+msgstr ""
+
msgid "Select"
msgstr ""
@@ -18470,9 +18897,6 @@ msgstr ""
msgid "Set up Jira Integration"
msgstr ""
-msgid "Set up Jira Integration illustration"
-msgstr ""
-
msgid "Set up a %{type} Runner automatically"
msgstr ""
@@ -18557,6 +18981,9 @@ msgstr ""
msgid "Severity: %{severity}"
msgstr ""
+msgid "Shards (%{shards})"
+msgstr ""
+
msgid "Shards to synchronize"
msgstr ""
@@ -18871,6 +19298,9 @@ msgstr ""
msgid "Snowplow"
msgstr ""
+msgid "Some child epics may be hidden due to applied filters"
+msgstr ""
+
msgid "Some email servers do not support overriding the email sender name. Enable this option to include the name of the author of the issue, merge request or comment in the email body instead."
msgstr ""
@@ -19012,6 +19442,9 @@ msgstr ""
msgid "Something went wrong while updating your list settings"
msgstr ""
+msgid "Something went wrong with your automatic subscription renewal"
+msgstr ""
+
msgid "Something went wrong, unable to add %{project} to dashboard"
msgstr ""
@@ -19297,6 +19730,9 @@ msgstr ""
msgid "Stage removed"
msgstr ""
+msgid "Standard"
+msgstr ""
+
msgid "Star a label to make it a priority label. Order the prioritized labels to change their relative priority, by dragging."
msgstr ""
@@ -19420,6 +19856,9 @@ msgstr ""
msgid "Static Application Security Testing (SAST)"
msgstr ""
+msgid "StaticSiteEditor|An error occurred while submitting your changes."
+msgstr ""
+
msgid "StaticSiteEditor|Branch could not be created."
msgstr ""
@@ -19429,18 +19868,30 @@ msgstr ""
msgid "StaticSiteEditor|Could not create merge request."
msgstr ""
+msgid "StaticSiteEditor|Incompatible file content"
+msgstr ""
+
msgid "StaticSiteEditor|Return to site"
msgstr ""
+msgid "StaticSiteEditor|Static site editor"
+msgstr ""
+
msgid "StaticSiteEditor|Success!"
msgstr ""
msgid "StaticSiteEditor|Summary of changes"
msgstr ""
+msgid "StaticSiteEditor|The Static Site Editor is currently configured to only edit Markdown content on pages generated from Middleman. Visit the documentation to learn more about configuring your site to use the Static Site Editor."
+msgstr ""
+
msgid "StaticSiteEditor|Update %{sourcePath} file"
msgstr ""
+msgid "StaticSiteEditor|View documentation"
+msgstr ""
+
msgid "StaticSiteEditor|View merge request"
msgstr ""
@@ -19498,6 +19949,12 @@ msgstr ""
msgid "StatusPage|Status page"
msgstr ""
+msgid "StatusPage|Status page URL"
+msgstr ""
+
+msgid "StatusPage|Status page frontend documentation"
+msgstr ""
+
msgid "StatusPage|To publish incidents to an external status page, GitLab will store a JSON file in your Amazon S3 account in a location accessible to your external status page service. Make sure to also set up %{docsLink}"
msgstr ""
@@ -19846,6 +20303,12 @@ msgstr ""
msgid "Synced"
msgstr ""
+msgid "Synchronization disabled"
+msgstr ""
+
+msgid "Synchronization of container repositories is disabled."
+msgstr ""
+
msgid "System"
msgstr ""
@@ -19999,6 +20462,9 @@ msgstr ""
msgid "Telephone number"
msgstr ""
+msgid "Telephone number (Optional)"
+msgstr ""
+
msgid "Template"
msgstr ""
@@ -20305,6 +20771,9 @@ msgstr ""
msgid "The license was successfully uploaded and is now active. You can see the details below."
msgstr ""
+msgid "The license was successfully uploaded and will be active from %{starts_at}. You can see the details below."
+msgstr ""
+
msgid "The maximum file size allowed is %{size}."
msgstr ""
@@ -20626,6 +21095,9 @@ msgstr ""
msgid "There was an error getting the epic participants."
msgstr ""
+msgid "There was an error importing the Jira project."
+msgstr ""
+
msgid "There was an error loading users activity calendar."
msgstr ""
@@ -20641,7 +21113,7 @@ msgstr ""
msgid "There was an error resetting user pipeline minutes."
msgstr ""
-msgid "There was an error saving this Geo Node"
+msgid "There was an error saving this Geo Node."
msgstr ""
msgid "There was an error saving your changes."
@@ -20704,6 +21176,9 @@ msgstr ""
msgid "They can be managed using the %{link}."
msgstr ""
+msgid "Third Party Advisory Link"
+msgstr ""
+
msgid "Third party offers"
msgstr ""
@@ -20740,6 +21215,9 @@ msgstr ""
msgid "This application will be able to:"
msgstr ""
+msgid "This attachment has been truncated to avoid exceeding the maximum allowed attachment size of 15MB. %{written_count} of %{issues_count} issues have been included. Consider re-exporting with a narrower selection of issues."
+msgstr ""
+
msgid "This block is self-referential"
msgstr ""
@@ -20881,6 +21359,12 @@ msgstr ""
msgid "This issue is confidential"
msgstr ""
+msgid "This issue is currently blocked by the following issues: %{issues}."
+msgstr ""
+
+msgid "This issue is in a child epic of the filtered epic"
+msgstr ""
+
msgid "This issue is locked."
msgstr ""
@@ -21079,7 +21563,10 @@ msgstr ""
msgid "This user will be the author of all events in the activity feed that are the result of an update, like new branches being created or new commits being pushed to existing branches. Upon creation or when reassigning you can only assign yourself to be the mirror user."
msgstr ""
-msgid "This variable can not be masked"
+msgid "This variable can not be masked."
+msgstr ""
+
+msgid "This variable does not match the expected pattern."
msgstr ""
msgid "This will help us personalize your onboarding experience."
@@ -21109,10 +21596,10 @@ msgstr ""
msgid "ThreatMonitoring|Application firewall not detected"
msgstr ""
-msgid "ThreatMonitoring|Container Network Policy"
+msgid "ThreatMonitoring|Container Network Policies are not installed or have been disabled. To view this data, ensure your Network Policies are installed and enabled for your cluster."
msgstr ""
-msgid "ThreatMonitoring|Container NetworkPolicies are not installed or has been disabled. To view this data, ensure you NetworkPolicies are installed and enabled for your cluster."
+msgid "ThreatMonitoring|Container Network Policy"
msgstr ""
msgid "ThreatMonitoring|Container NetworkPolicies not detected"
@@ -21145,7 +21632,7 @@ msgstr ""
msgid "ThreatMonitoring|Something went wrong, unable to fetch statistics"
msgstr ""
-msgid "ThreatMonitoring|The firewall is not installed or has been disabled. To view this data, ensure you firewall is installed and enabled for your cluster."
+msgid "ThreatMonitoring|The firewall is not installed or has been disabled. To view this data, ensure the web application firewall is installed and enabled for your cluster."
msgstr ""
msgid "ThreatMonitoring|The graph below is an overview of traffic coming to your application as tracked by the Web Application Firewall (WAF). View the docs for instructions on how to access the WAF logs to see what type of malicious traffic is trying to access your app. The docs link is also accessible by clicking the \"?\" icon next to the title below."
@@ -21214,6 +21701,9 @@ msgstr ""
msgid "Time in seconds GitLab will wait for a response from the external service. When the service does not respond in time, access will be denied."
msgstr ""
+msgid "Time of import: %{importTime}"
+msgstr ""
+
msgid "Time remaining"
msgstr ""
@@ -21398,7 +21888,7 @@ msgstr ""
msgid "Title:"
msgstr ""
-msgid "Titles and Filenames"
+msgid "Titles and Descriptions"
msgstr ""
msgid "To"
@@ -21605,7 +22095,7 @@ msgstr ""
msgid "Too many projects enabled. You will need to manage them via the console or the API."
msgstr ""
-msgid "Topics"
+msgid "Topics (optional)"
msgstr ""
msgid "Total"
@@ -21767,6 +22257,9 @@ msgstr ""
msgid "Try all GitLab has to offer for 30 days."
msgstr ""
+msgid "Try changing or removing filters."
+msgstr ""
+
msgid "Try to fork again"
msgstr ""
@@ -21860,6 +22353,9 @@ msgstr ""
msgid "Unable to connect to server: %{error}"
msgstr ""
+msgid "Unable to connect to the Jira instance. Please check your Jira integration configuration."
+msgstr ""
+
msgid "Unable to convert Kubernetes logs encoding to UTF-8"
msgstr ""
@@ -22803,6 +23299,9 @@ msgstr ""
msgid "View issue"
msgstr ""
+msgid "View issues"
+msgstr ""
+
msgid "View it on GitLab"
msgstr ""
@@ -22980,6 +23479,15 @@ msgstr ""
msgid "VulnerabilityManagement|Resolved %{timeago} by %{user}"
msgstr ""
+msgid "VulnerabilityManagement|Something went wrong while trying to delete the comment. Please try again later."
+msgstr ""
+
+msgid "VulnerabilityManagement|Something went wrong while trying to retrieve the vulnerability history. Please try again later."
+msgstr ""
+
+msgid "VulnerabilityManagement|Something went wrong while trying to save the comment. Please try again later."
+msgstr ""
+
msgid "VulnerabilityManagement|Something went wrong, could not create an issue."
msgstr ""
@@ -23010,6 +23518,9 @@ msgstr ""
msgid "VulnerabilityStatusTypes|Resolved"
msgstr ""
+msgid "Vulnerability|%{scannerName} (version %{scannerVersion})"
+msgstr ""
+
msgid "Vulnerability|Class"
msgstr ""
@@ -23043,7 +23554,10 @@ msgstr ""
msgid "Vulnerability|Project"
msgstr ""
-msgid "Vulnerability|Report Type"
+msgid "Vulnerability|Scanner Provider"
+msgstr ""
+
+msgid "Vulnerability|Scanner Type"
msgstr ""
msgid "Vulnerability|Severity"
@@ -23097,9 +23611,15 @@ msgstr ""
msgid "We sent you an email with reset password instructions"
msgstr ""
+msgid "We tried to automatically renew your %{strong}%{plan_name}%{strong_close} subscription for %{strong}%{namespace_name}%{strong_close} on %{expires_on} but something went wrong so your subscription was downgraded to the free plan. Don't worry, your data is safe. We suggest you check your payment method and get in touch with our support team (%{support_link}). They'll gladly help with your subscription renewal."
+msgstr ""
+
msgid "We want to be sure it is you, please confirm you are not a robot."
msgstr ""
+msgid "We will automatically renew your %{strong}%{plan_name}%{strong_close} subscription for %{strong}%{namespace_name}%{strong_close} on %{strong}%{expires_on}%{strong_close}. There's nothing that you need to do, we'll let you know when the renewal is complete. Need more seats, a higher plan or just want to review your payment method?"
+msgstr ""
+
msgid "We've found no vulnerabilities"
msgstr ""
@@ -23204,15 +23724,6 @@ msgstr ""
msgid "When:"
msgstr ""
-msgid "While it's rare to have no vulnerabilities for your group, it can happen. In any event, we ask that you double check your settings to make sure you've set up your dashboard correctly."
-msgstr ""
-
-msgid "While it's rare to have no vulnerabilities for your pipeline, it can happen. In any event, we ask that you double check your settings to make sure all security scanning jobs have passed successfully."
-msgstr ""
-
-msgid "While it's rare to have no vulnerabilities for your project, it can happen. In any event, we ask that you double check your settings to make sure you've set up your dashboard correctly."
-msgstr ""
-
msgid "While it's rare to have no vulnerabilities, it can happen. In any event, we ask that you please double check your settings to make sure you've set up your dashboard correctly."
msgstr ""
@@ -23243,9 +23754,6 @@ msgstr ""
msgid "Wiki"
msgstr ""
-msgid "Wiki pages"
-msgstr ""
-
msgid "Wiki was successfully updated."
msgstr ""
@@ -23429,6 +23937,9 @@ msgstr ""
msgid "Yes, add it"
msgstr ""
+msgid "Yes, close issue"
+msgstr ""
+
msgid "Yes, let me map Google Code users to full names or GitLab users."
msgstr ""
@@ -23624,6 +24135,9 @@ msgstr ""
msgid "You could not create a new trigger."
msgstr ""
+msgid "You didn't renew your %{strong}%{plan_name}%{strong_close} subscription for %{strong}%{namespace_name}%{strong_close} so it was downgraded to the free plan."
+msgstr ""
+
msgid "You didn't renew your %{strong}%{plan_name}%{strong_close} subscription so it was downgraded to the GitLab Core Plan."
msgstr ""
@@ -23690,6 +24204,9 @@ msgstr ""
msgid "You have declined the invitation to join %{label}."
msgstr ""
+msgid "You have imported from this project %{numberOfPreviousImportsForProject} times before. Each new import will create duplicate issues."
+msgstr ""
+
msgid "You have no permissions"
msgstr ""
@@ -23771,6 +24288,9 @@ msgstr ""
msgid "You need to upload a Google Takeout archive."
msgstr ""
+msgid "You reached %{usage_in_percent} of %{namespace_name}'s capacity (%{used_storage} of %{storage_limit})"
+msgstr ""
+
msgid "You tried to fork %{link_to_the_project} but it failed for the following reason:"
msgstr ""
@@ -23855,9 +24375,21 @@ msgstr ""
msgid "YouTube"
msgstr ""
+msgid "Your %{strong}%{plan_name}%{strong_close} subscription for %{strong}%{namespace_name}%{strong_close} will expire on %{strong}%{expires_on}%{strong_close}. After that, you will not to be able to create issues or merge requests as well as many other features."
+msgstr ""
+
msgid "Your %{strong}%{plan_name}%{strong_close} subscription will expire on %{strong}%{expires_on}%{strong_close}. After that, you will not to be able to create issues or merge requests as well as many other features."
msgstr ""
+msgid "Your CSV export has started. It will be emailed to %{email} when complete."
+msgstr ""
+
+msgid "Your CSV export of %{issues_count} from project %{project_link} has been added to this email as an attachment."
+msgstr ""
+
+msgid "Your CSV export of %{written_count} from project %{project_name} (%{project_url}) has been added to this email as an attachment."
+msgstr ""
+
msgid "Your Commit Email will be used for web based operations, such as edits and merges."
msgstr ""
@@ -23954,6 +24486,9 @@ msgstr ""
msgid "Your comment could not be updated! Please check your network connection and try again."
msgstr ""
+msgid "Your comment will be discarded."
+msgstr ""
+
msgid "Your custom stage '%{title}' was created"
msgstr ""
@@ -23993,6 +24528,9 @@ msgstr ""
msgid "Your new personal access token has been created."
msgstr ""
+msgid "Your new project access token has been created."
+msgstr ""
+
msgid "Your password isn't required to view this page. If a password or any other personal details are requested, please contact your administrator to report abuse."
msgstr ""
@@ -24011,12 +24549,18 @@ msgstr ""
msgid "Your request for access has been queued for review."
msgstr ""
+msgid "Your search didn't match any commits."
+msgstr ""
+
msgid "Your subscription expired!"
msgstr ""
msgid "Your subscription has been downgraded"
msgstr ""
+msgid "Your subscription will automatically renew in %{remaining_days}"
+msgstr ""
+
msgid "Your subscription will expire in %{remaining_days}"
msgstr ""
@@ -24055,7 +24599,7 @@ msgstr ""
msgid "allowed to fail"
msgstr ""
-msgid "already being used for another group or project milestone."
+msgid "already being used for another group or project %{timebox_name}."
msgstr ""
msgid "already has a \"created\" issue link"
@@ -24133,6 +24677,9 @@ msgstr ""
msgid "ciReport|%{linkStartTag}Learn more about SAST %{linkEndTag}"
msgstr ""
+msgid "ciReport|%{linkStartTag}Learn more about Secret Scanning %{linkEndTag}"
+msgstr ""
+
msgid "ciReport|%{linkStartTag}Learn more about codequality reports %{linkEndTag}"
msgstr ""
@@ -24296,6 +24843,12 @@ msgstr ""
msgid "ciReport|SAST"
msgstr ""
+msgid "ciReport|Secret scanning"
+msgstr ""
+
+msgid "ciReport|Secret scanning detects secrets and credentials vulnerabilities in your source code."
+msgstr ""
+
msgid "ciReport|Security scanning"
msgstr ""
@@ -24493,6 +25046,9 @@ msgstr ""
msgid "group"
msgstr ""
+msgid "groups"
+msgstr ""
+
msgid "has already been linked to another vulnerability"
msgstr ""
@@ -24656,9 +25212,6 @@ msgstr[1] ""
msgid "merged %{time_ago}"
msgstr ""
-msgid "milestone should belong either to a project or a group."
-msgstr ""
-
msgid "missing"
msgstr ""
@@ -24995,6 +25548,9 @@ msgstr ""
msgid "no contributions"
msgstr ""
+msgid "no expiration"
+msgstr ""
+
msgid "no one can merge"
msgstr ""
@@ -25047,6 +25603,9 @@ msgstr ""
msgid "pending removal"
msgstr ""
+msgid "per day"
+msgstr ""
+
msgid "pipeline"
msgstr ""
@@ -25075,6 +25634,12 @@ msgstr[1] ""
msgid "project avatar"
msgstr ""
+msgid "projects"
+msgstr ""
+
+msgid "push to your repository, create pipelines, create issues or add comments. To reduce storage capacity, delete unused repositories, artifacts, wikis, issues, and pipelines."
+msgstr ""
+
msgid "quick actions"
msgstr ""
@@ -25221,11 +25786,6 @@ msgstr ""
msgid "this document"
msgstr ""
-msgid "thread resolved"
-msgid_plural "threads resolved"
-msgstr[0] ""
-msgstr[1] ""
-
msgid "to help your contributors communicate effectively!"
msgstr ""
@@ -25280,6 +25840,9 @@ msgstr ""
msgid "view the blob"
msgstr ""
+msgid "vulnerability|Add a comment"
+msgstr ""
+
msgid "vulnerability|Add a comment or reason for dismissal"
msgstr ""
diff --git a/locale/tr_TR/gitlab.po b/locale/tr_TR/gitlab.po
index 8349ab12f36..5473bc15651 100644
--- a/locale/tr_TR/gitlab.po
+++ b/locale/tr_TR/gitlab.po
@@ -12,7 +12,7 @@ msgstr ""
"X-Crowdin-Project: gitlab-ee\n"
"X-Crowdin-Language: tr\n"
"X-Crowdin-File: /master/locale/gitlab.pot\n"
-"PO-Revision-Date: 2020-04-15 00:28\n"
+"PO-Revision-Date: 2020-05-05 21:36\n"
msgid " %{start} to %{end}"
msgstr ""
@@ -212,6 +212,11 @@ msgid_plural "%d tags"
msgstr[0] "%d etiket"
msgstr[1] "%d etiket"
+msgid "%d unresolved thread"
+msgid_plural "%d unresolved threads"
+msgstr[0] ""
+msgstr[1] ""
+
msgid "%d vulnerability dismissed"
msgid_plural "%d vulnerabilities dismissed"
msgstr[0] ""
@@ -371,6 +376,9 @@ msgstr "%{mergeLength}/%{usersLength} birleÅŸtirebilir"
msgid "%{mrText}, this issue will be closed automatically."
msgstr "%{mrText}, bu sorun otomatik olarak kapatılacaktır."
+msgid "%{namespace_name} is now read-only. You cannot: %{base_message}"
+msgstr ""
+
msgid "%{name} contained %{resultsString}"
msgstr "%{name} %{resultsString} ifadesini içeriyor"
@@ -383,6 +391,11 @@ msgstr ""
msgid "%{name}'s avatar"
msgstr "%{name} kullanıcısının profil resmi"
+msgid "%{no_of_days} day"
+msgid_plural "%{no_of_days} days"
+msgstr[0] ""
+msgstr[1] ""
+
msgid "%{number_commits_behind} commits behind %{default_branch}, %{number_commits_ahead} commits ahead"
msgstr ""
@@ -506,6 +519,9 @@ msgstr[1] "%{text} %{files} dosyaları"
msgid "%{text} is available"
msgstr "%{text} kullanılabilir"
+msgid "%{timebox_name} should belong either to a project or a group."
+msgstr ""
+
msgid "%{title} %{operator} %{threshold}"
msgstr "%{title} %{operator} %{threshold}"
@@ -781,12 +797,18 @@ msgstr ""
msgid "<code>\"johnsmith@example.com\": \"johnsmith@example.com\"</code> will add \"By <a href=\"#\">johnsmith@example.com</a>\" to all issues and comments originally created by johnsmith@example.com. By default, the email address or username is masked to ensure the user's privacy. Use this option if you want to show the full email address."
msgstr ""
+msgid "<namespace / project>"
+msgstr ""
+
msgid "<no name set>"
msgstr "<isim belirlenmemiÅŸ>"
msgid "<no scopes selected>"
msgstr "<hiçbir kapsam seçilmedi>"
+msgid "<project name>"
+msgstr ""
+
msgid "<strong>%{group_name}</strong> group members"
msgstr "<strong>%{group_name}</strong> grup üyeleri"
@@ -820,6 +842,9 @@ msgstr ""
msgid "A basic page and serverless function that uses AWS Lambda, AWS API Gateway, and GitLab Pages"
msgstr ""
+msgid "A complete DevOps platform"
+msgstr ""
+
msgid "A default branch cannot be chosen for an empty project."
msgstr "Boş bir proje için bir varsayılan dal seçilemez."
@@ -1036,6 +1061,12 @@ msgstr ""
msgid "AccessTokens|reset it"
msgstr "sıfırla"
+msgid "AccessibilityReport|Accessibility report artifact not found"
+msgstr ""
+
+msgid "AccessibilityReport|Failed to retrieve accessibility report"
+msgstr ""
+
msgid "AccessibilityReport|Learn More"
msgstr ""
@@ -1045,6 +1076,9 @@ msgstr ""
msgid "AccessibilityReport|New"
msgstr ""
+msgid "AccessibilityReport|The accessibility scanning found an error of the following type: %{code}"
+msgstr ""
+
msgid "Account"
msgstr "Hesap"
@@ -1287,6 +1321,9 @@ msgstr ""
msgid "Added at"
msgstr "Tarihinde eklendi"
+msgid "Added for this merge request"
+msgstr ""
+
msgid "Added in this version"
msgstr "Bu sürümde eklendi"
@@ -1658,6 +1695,72 @@ msgid_plural "Alerts"
msgstr[0] "Uyarı"
msgstr[1] "Uyarı"
+msgid "Alert Details"
+msgstr ""
+
+msgid "AlertManagement|Acknowledged"
+msgstr ""
+
+msgid "AlertManagement|Alert"
+msgstr ""
+
+msgid "AlertManagement|Authorize external service"
+msgstr ""
+
+msgid "AlertManagement|Display alerts from all your monitoring tools directly within GitLab. Streamline the investigation of your alerts and the escalation of alerts to incidents."
+msgstr ""
+
+msgid "AlertManagement|End time"
+msgstr ""
+
+msgid "AlertManagement|End time:"
+msgstr ""
+
+msgid "AlertManagement|Events"
+msgstr ""
+
+msgid "AlertManagement|Events:"
+msgstr ""
+
+msgid "AlertManagement|Full Alert Details"
+msgstr ""
+
+msgid "AlertManagement|More information"
+msgstr ""
+
+msgid "AlertManagement|No alerts available to display. If you think you're seeing this message in error, refresh the page."
+msgstr ""
+
+msgid "AlertManagement|No alerts to display."
+msgstr ""
+
+msgid "AlertManagement|Overview"
+msgstr ""
+
+msgid "AlertManagement|Resolved"
+msgstr ""
+
+msgid "AlertManagement|Severity"
+msgstr ""
+
+msgid "AlertManagement|Start time"
+msgstr ""
+
+msgid "AlertManagement|Start time:"
+msgstr ""
+
+msgid "AlertManagement|Status"
+msgstr ""
+
+msgid "AlertManagement|Surface alerts in GitLab"
+msgstr ""
+
+msgid "AlertManagement|There was an error displaying the alerts. Confirm your endpoint's configuration details to ensure alerts appear."
+msgstr ""
+
+msgid "AlertManagement|Triggered"
+msgstr ""
+
msgid "AlertService|%{linkStart}Learn more%{linkEnd} about configuring this endpoint to receive alerts."
msgstr ""
@@ -1727,6 +1830,9 @@ msgstr "Tüm projeler"
msgid "All security scans are enabled because %{linkStart}Auto DevOps%{linkEnd} is enabled on this project"
msgstr ""
+msgid "All threads resolved"
+msgstr ""
+
msgid "All users"
msgstr "Tüm kullanıcılar"
@@ -1745,6 +1851,9 @@ msgstr "Grup sahiplerinin LDAP ile ilgili ayarları yönetmesine izin ver"
msgid "Allow only the selected protocols to be used for Git access."
msgstr "Git erişimi için yalnızca seçili protokollerin kullanılmasına izin ver."
+msgid "Allow owners to manage default branch protection per group"
+msgstr ""
+
msgid "Allow owners to manually add users outside of LDAP"
msgstr ""
@@ -1853,6 +1962,9 @@ msgstr "Yeni kuralın onaylayanları alınırken bir hata oluştu."
msgid "An error occurred fetching the dropdown data."
msgstr ""
+msgid "An error occurred fetching the project authors."
+msgstr ""
+
msgid "An error occurred previewing the blob"
msgstr "Blob datanın öngösteriminde, bir hata meydana geldi"
@@ -1934,6 +2046,9 @@ msgstr ""
msgid "An error occurred while fetching sidebar data"
msgstr "Kenar çubuğu verileri getirilirken bir hata oluştu"
+msgid "An error occurred while fetching terraform reports."
+msgstr ""
+
msgid "An error occurred while fetching the Service Desk address."
msgstr ""
@@ -2171,6 +2286,9 @@ msgstr "Anti-spam doÄŸrulama"
msgid "Any"
msgstr "Herhangi"
+msgid "Any Author"
+msgstr ""
+
msgid "Any Label"
msgstr "Herhangi bir etiket"
@@ -2386,10 +2504,10 @@ msgstr ""
msgid "Are you sure that you want to unarchive this project?"
msgstr ""
-msgid "Are you sure you want to cancel creating this comment?"
+msgid "Are you sure you want to cancel editing this comment?"
msgstr ""
-msgid "Are you sure you want to cancel editing this comment?"
+msgid "Are you sure you want to close this blocked issue?"
msgstr ""
msgid "Are you sure you want to delete %{name}?"
@@ -2419,6 +2537,9 @@ msgstr ""
msgid "Are you sure you want to deploy this environment?"
msgstr ""
+msgid "Are you sure you want to discard this comment?"
+msgstr ""
+
msgid "Are you sure you want to erase this build?"
msgstr "Bu yapıyı silmek istediğinizden emin misiniz?"
@@ -2595,6 +2716,9 @@ msgstr ""
msgid "At least one approval from a code owner is required to change files matching the respective CODEOWNER rules."
msgstr ""
+msgid "At least one logging option is required to be enabled"
+msgstr ""
+
msgid "At least one of group_id or project_id must be specified"
msgstr ""
@@ -2633,6 +2757,27 @@ msgstr ""
msgid "AuditEvents|Target"
msgstr "Hedef"
+msgid "AuditLogs|(removed)"
+msgstr ""
+
+msgid "AuditLogs|Action"
+msgstr ""
+
+msgid "AuditLogs|Author"
+msgstr ""
+
+msgid "AuditLogs|Date"
+msgstr ""
+
+msgid "AuditLogs|IP Address"
+msgstr ""
+
+msgid "AuditLogs|Object"
+msgstr ""
+
+msgid "AuditLogs|Target"
+msgstr ""
+
msgid "Aug"
msgstr "AÄŸustos"
@@ -2915,9 +3060,6 @@ msgstr ""
msgid "BambooService|You must set up automatic revision labeling and a repository trigger in Bamboo."
msgstr ""
-msgid "Batch operations"
-msgstr ""
-
msgid "BatchComments|Delete all pending comments"
msgstr "Bekleyen tüm yorumları sil"
@@ -3047,6 +3189,9 @@ msgstr ""
msgid "Boards"
msgstr "Panolar"
+msgid "Boards and Board Lists"
+msgstr ""
+
msgid "Boards|Collapse"
msgstr "Daralt"
@@ -3266,6 +3411,9 @@ msgstr "EE satın al"
msgid "Buy GitLab Enterprise Edition"
msgstr "Gitlab Kurumsal Sürümünü satın alın"
+msgid "Buy more Pipeline minutes"
+msgstr ""
+
msgid "By %{user_name}"
msgstr "%{user_name} tarafından"
@@ -3386,6 +3534,9 @@ msgstr ""
msgid "Can't scan the code?"
msgstr ""
+msgid "Can't update snippet: %{err}"
+msgstr ""
+
msgid "Canary"
msgstr ""
@@ -3554,6 +3705,9 @@ msgstr ""
msgid "Changing group path can have unintended side effects."
msgstr ""
+msgid "Charts"
+msgstr ""
+
msgid "Charts can't be displayed as the request for data has timed out. %{documentationLink}"
msgstr ""
@@ -3590,9 +3744,6 @@ msgstr "Etiket"
msgid "ChatMessage|and [%{count} more](%{pipeline_failed_jobs_url})"
msgstr ""
-msgid "ChatMessage|failed"
-msgstr "başarısız"
-
msgid "ChatMessage|has failed"
msgstr "başarısız oldu"
@@ -3608,9 +3759,6 @@ msgstr ""
msgid "ChatMessage|in %{project_link}"
msgstr ""
-msgid "ChatMessage|passed"
-msgstr "geçti"
-
msgid "Check again"
msgstr "Tekrar kontrol et"
@@ -4061,9 +4209,6 @@ msgstr ""
msgid "Click any <strong>project name</strong> in the project list below to navigate to the project milestone."
msgstr ""
-msgid "Click here"
-msgstr "Buraya tıklayın"
-
msgid "Click the <strong>Download</strong> button and wait for downloading to complete."
msgstr "<strong>İndirme</strong> düğmesini tıklayın ve indirme işleminin tamamlanmasını bekleyin."
@@ -4451,6 +4596,12 @@ msgstr "Projeler getiriliyor"
msgid "ClusterIntegration|Fetching zones"
msgstr "Alanlar getiriliyor"
+msgid "ClusterIntegration|Fluentd"
+msgstr ""
+
+msgid "ClusterIntegration|Fluentd is an open source data collector, which lets you unify the data collection and consumption for a better use and understanding of data. It requires at least one of the following logs to be successfully installed."
+msgstr ""
+
msgid "ClusterIntegration|GitLab Integration"
msgstr "GitLab Bütünleşmesi"
@@ -4757,6 +4908,15 @@ msgstr ""
msgid "ClusterIntegration|Request to begin uninstalling failed"
msgstr ""
+msgid "ClusterIntegration|SIEM Hostname"
+msgstr ""
+
+msgid "ClusterIntegration|SIEM Port"
+msgstr ""
+
+msgid "ClusterIntegration|SIEM Protocol"
+msgstr ""
+
msgid "ClusterIntegration|Save changes"
msgstr "DeÄŸiÅŸiklikleri kaydet"
@@ -4853,6 +5013,12 @@ msgstr "Alan seç"
msgid "ClusterIntegration|Select zone to choose machine type"
msgstr "Makine türünü seçmek için alan seç"
+msgid "ClusterIntegration|Send Cilium Logs"
+msgstr ""
+
+msgid "ClusterIntegration|Send ModSecurity Logs"
+msgstr ""
+
msgid "ClusterIntegration|Service Token"
msgstr ""
@@ -5281,18 +5447,33 @@ msgstr ""
msgid "Compliance framework (optional)"
msgstr ""
+msgid "ComplianceFramework|GDPR"
+msgstr ""
+
msgid "ComplianceFramework|GDPR - General Data Protection Regulation"
msgstr ""
+msgid "ComplianceFramework|HIPAA"
+msgstr ""
+
msgid "ComplianceFramework|HIPAA - Health Insurance Portability and Accountability Act"
msgstr ""
+msgid "ComplianceFramework|PCI-DSS"
+msgstr ""
+
msgid "ComplianceFramework|PCI-DSS - Payment Card Industry-Data Security Standard"
msgstr ""
+msgid "ComplianceFramework|SOC 2"
+msgstr ""
+
msgid "ComplianceFramework|SOC 2 - Service Organization Control 2"
msgstr ""
+msgid "ComplianceFramework|SOX"
+msgstr ""
+
msgid "ComplianceFramework|SOX - Sarbanes-Oxley"
msgstr ""
@@ -5440,9 +5621,15 @@ msgstr ""
msgid "Container repositories sync capacity"
msgstr ""
+msgid "ContainerRegistry| Please visit the %{linkStart}administration settings%{linkEnd} to enable this feature."
+msgstr ""
+
msgid "ContainerRegistry|%{imageName} tags"
msgstr ""
+msgid "ContainerRegistry|%{title} was successfully scheduled for deletion"
+msgstr ""
+
msgid "ContainerRegistry|Automatically remove extra images that aren't designed to be kept."
msgstr ""
@@ -5455,6 +5642,9 @@ msgstr ""
msgid "ContainerRegistry|Container Registry"
msgstr ""
+msgid "ContainerRegistry|Container Registry tag expiration and retention policy is disabled"
+msgstr ""
+
msgid "ContainerRegistry|Copy build command"
msgstr ""
@@ -5464,18 +5654,12 @@ msgstr ""
msgid "ContainerRegistry|Copy push command"
msgstr ""
-msgid "ContainerRegistry|Currently, the Container Registry tag expiration feature is not available for projects created before GitLab version 12.8. For updates and more information, visit Issue %{linkStart}#196124%{linkEnd}"
-msgstr ""
-
msgid "ContainerRegistry|Docker connection error"
msgstr ""
msgid "ContainerRegistry|Docker tag expiration policy is %{toggleStatus}"
msgstr ""
-msgid "ContainerRegistry|Docker tags with names matching this regex pattern will expire:"
-msgstr ""
-
msgid "ContainerRegistry|Edit Settings"
msgstr ""
@@ -5497,9 +5681,6 @@ msgstr ""
msgid "ContainerRegistry|Image ID"
msgstr ""
-msgid "ContainerRegistry|Image deleted successfully"
-msgstr ""
-
msgid "ContainerRegistry|Keep and protect the images that matter most."
msgstr ""
@@ -5515,12 +5696,24 @@ msgstr ""
msgid "ContainerRegistry|Number of tags to retain:"
msgstr ""
+msgid "ContainerRegistry|Please contact your administrator."
+msgstr ""
+
msgid "ContainerRegistry|Push an image"
msgstr ""
msgid "ContainerRegistry|Quick Start"
msgstr ""
+msgid "ContainerRegistry|Regular expressions such as %{codeStart}.*-test%{codeEnd} or %{codeStart}dev-.*%{codeEnd} are supported"
+msgstr ""
+
+msgid "ContainerRegistry|Regular expressions such as %{codeStart}.*-test%{codeEnd} or %{codeStart}dev-.*%{codeEnd} are supported. To select all tags, use %{codeStart}.*%{codeEnd}"
+msgstr ""
+
+msgid "ContainerRegistry|Remember to run %{docLinkStart}garbage collection%{docLinkEnd} to remove the stale data from storage."
+msgstr ""
+
msgid "ContainerRegistry|Remove repository"
msgstr "Depoyu kaldır"
@@ -5535,22 +5728,22 @@ msgstr[1] ""
msgid "ContainerRegistry|Retention policy has been Enabled"
msgstr ""
-msgid "ContainerRegistry|Something went wrong while deleting the image."
+msgid "ContainerRegistry|Something went wrong while fetching the expiration policy."
msgstr ""
-msgid "ContainerRegistry|Something went wrong while deleting the tag."
+msgid "ContainerRegistry|Something went wrong while fetching the repository list."
msgstr ""
-msgid "ContainerRegistry|Something went wrong while deleting the tags."
+msgid "ContainerRegistry|Something went wrong while fetching the tags list."
msgstr ""
-msgid "ContainerRegistry|Something went wrong while fetching the expiration policy."
+msgid "ContainerRegistry|Something went wrong while marking the tag for deletion."
msgstr ""
-msgid "ContainerRegistry|Something went wrong while fetching the packages list."
+msgid "ContainerRegistry|Something went wrong while marking the tags for deletion."
msgstr ""
-msgid "ContainerRegistry|Something went wrong while fetching the tags list."
+msgid "ContainerRegistry|Something went wrong while scheduling %{title} for deletion. Please try again."
msgstr ""
msgid "ContainerRegistry|Something went wrong while updating the expiration policy."
@@ -5559,16 +5752,25 @@ msgstr ""
msgid "ContainerRegistry|Tag"
msgstr "Etiket"
-msgid "ContainerRegistry|Tag deleted successfully"
-msgstr ""
-
msgid "ContainerRegistry|Tag expiration policy"
msgstr ""
msgid "ContainerRegistry|Tag expiration policy is designed to:"
msgstr ""
-msgid "ContainerRegistry|Tags deleted successfully"
+msgid "ContainerRegistry|Tag successfully marked for deletion."
+msgstr ""
+
+msgid "ContainerRegistry|Tags successfully marked for deletion."
+msgstr ""
+
+msgid "ContainerRegistry|Tags with names matching this regex pattern will %{italicStart}be preserved:%{italicEnd}"
+msgstr ""
+
+msgid "ContainerRegistry|Tags with names matching this regex pattern will %{italicStart}expire:%{italicEnd}"
+msgstr ""
+
+msgid "ContainerRegistry|The Container Registry tag expiration and retention policies for this project have not been enabled."
msgstr ""
msgid "ContainerRegistry|The last tag related to this image was recently removed. This empty image and any associated data will be automatically removed as part of the regular Garbage Collection process. If you have any questions, contact your administrator."
@@ -5589,16 +5791,16 @@ msgstr ""
msgid "ContainerRegistry|There are no container images stored for this project"
msgstr ""
-msgid "ContainerRegistry|This Registry contains deleted image tag data. Remember to run %{docLinkStart}garbage collection%{docLinkEnd} to remove the stale data from storage."
+msgid "ContainerRegistry|There was an error during the deletion of this image repository, please try again."
msgstr ""
msgid "ContainerRegistry|This image has no active tags"
msgstr ""
-msgid "ContainerRegistry|We are having trouble connecting to Docker, which could be due to an issue with your project name or path. %{docLinkStart}More Information%{docLinkEnd}"
+msgid "ContainerRegistry|This image repository is scheduled for deletion"
msgstr ""
-msgid "ContainerRegistry|Wildcards such as %{codeStart}.*-stable%{codeEnd} or %{codeStart}production/.*%{codeEnd} are supported. To select all tags, use %{codeStart}.*%{codeEnd}"
+msgid "ContainerRegistry|We are having trouble connecting to Docker, which could be due to an issue with your project name or path. %{docLinkStart}More Information%{docLinkEnd}"
msgstr ""
msgid "ContainerRegistry|With the Container Registry, every project can have its own space to store its Docker images. %{docLinkStart}More Information%{docLinkEnd}"
@@ -5853,7 +6055,7 @@ msgstr ""
msgid "Could not delete chat nickname %{chat_name}."
msgstr ""
-msgid "Could not find design"
+msgid "Could not find design."
msgstr ""
msgid "Could not remove the trigger."
@@ -5868,6 +6070,9 @@ msgstr ""
msgid "Could not revoke personal access token %{personal_access_token_name}."
msgstr ""
+msgid "Could not revoke project access token %{project_access_token_name}."
+msgstr ""
+
msgid "Could not save group ID"
msgstr ""
@@ -5925,6 +6130,9 @@ msgstr ""
msgid "Create a new branch"
msgstr "Yeni bir dal oluÅŸtur"
+msgid "Create a new deploy key for this project"
+msgstr ""
+
msgid "Create a new file as there are no files yet. Afterwards, you'll be able to commit your changes."
msgstr ""
@@ -6030,6 +6238,9 @@ msgstr "Proje etiketi oluÅŸtur"
msgid "Create requirement"
msgstr ""
+msgid "Create snippet"
+msgstr ""
+
msgid "Create wildcard: %{searchTerm}"
msgstr ""
@@ -6051,6 +6262,9 @@ msgstr "kiÅŸisel eriÅŸim belirteci oluÅŸtur"
msgid "Created"
msgstr "OluÅŸturdu:"
+msgid "Created %{timestamp}"
+msgstr ""
+
msgid "Created At"
msgstr "OluÅŸturma tarihi"
@@ -6407,6 +6621,9 @@ msgstr ""
msgid "CycleAnalytics|stage dropdown"
msgstr ""
+msgid "DAG"
+msgstr ""
+
msgid "DNS"
msgstr "DNS"
@@ -6773,6 +6990,9 @@ msgstr ""
msgid "Deploy key was successfully updated."
msgstr "Dağıtım anahtarı başarıyla güncellendi."
+msgid "Deploy keys allow read-only or read-write (if enabled) access to your repository. Deploy keys can be used for CI, staging or production servers. You can create a deploy key or add an existing one."
+msgstr ""
+
msgid "Deploy progress not found. To see pods, ensure your environment matches %{linkStart}deploy board criteria%{linkEnd}."
msgstr ""
@@ -6926,6 +7146,9 @@ msgstr ""
msgid "Deploying to"
msgstr ""
+msgid "Deployment Frequency"
+msgstr ""
+
msgid "Deployment|API"
msgstr ""
@@ -7444,9 +7667,6 @@ msgstr "Açıklamayı düzenle"
msgid "Edit environment"
msgstr "Ortamı düzenle"
-msgid "Edit epic description"
-msgstr "Epik açıklamasını düzenle"
-
msgid "Edit file"
msgstr "Dosyayı düzenle"
@@ -8068,9 +8288,6 @@ msgstr ""
msgid "Epics"
msgstr ""
-msgid "Epics (Ultimate / Gold license only)"
-msgstr ""
-
msgid "Epics Roadmap"
msgstr ""
@@ -8080,6 +8297,9 @@ msgstr ""
msgid "Epics let you manage your portfolio of projects more efficiently and with less effort"
msgstr ""
+msgid "Epics, Issues, and Merge Requests"
+msgstr ""
+
msgid "Epics|Add an epic"
msgstr ""
@@ -8131,6 +8351,9 @@ msgstr ""
msgid "Epics|Something went wrong while fetching group epics."
msgstr ""
+msgid "Epics|Something went wrong while moving item."
+msgstr ""
+
msgid "Epics|Something went wrong while ordering item."
msgstr ""
@@ -8185,6 +8408,9 @@ msgstr "Etiketler getirilirken hata oluÅŸtu."
msgid "Error fetching network graph."
msgstr ""
+msgid "Error fetching payload data."
+msgstr ""
+
msgid "Error fetching projects"
msgstr ""
@@ -8194,9 +8420,6 @@ msgstr ""
msgid "Error fetching the dependency list. Please check your network connection and try again."
msgstr ""
-msgid "Error fetching usage ping data."
-msgstr ""
-
msgid "Error loading branch data. Please try again."
msgstr "Dal verileri yüklenirken hata oluştu. Lütfen tekrar deneyin."
@@ -8338,6 +8561,9 @@ msgstr ""
msgid "Errors"
msgstr ""
+msgid "Errors:"
+msgstr ""
+
msgid "Estimated"
msgstr ""
@@ -8542,12 +8768,18 @@ msgstr "Genel grupları keşfedin"
msgid "Export as CSV"
msgstr "CSV olarak dışa aktar"
+msgid "Export group"
+msgstr ""
+
msgid "Export issues"
msgstr "Dışa aktarma sorunları"
msgid "Export project"
msgstr "Projeyi dışa aktar"
+msgid "Export this group with all related data to a new GitLab instance. Once complete, you can import the data file from the \"New Group\" page."
+msgstr ""
+
msgid "Export this project with all its related data in order to move your project to a new GitLab instance. Once the export is finished, you can import the file from the \"New Project\" page."
msgstr ""
@@ -8617,9 +8849,6 @@ msgstr ""
msgid "Failed Jobs"
msgstr ""
-msgid "Failed create wiki"
-msgstr "Viki oluşturma başarısız"
-
msgid "Failed to add a Zoom meeting"
msgstr ""
@@ -8659,6 +8888,9 @@ msgstr ""
msgid "Failed to create resources"
msgstr ""
+msgid "Failed to create wiki"
+msgstr ""
+
msgid "Failed to delete board. Please try again."
msgstr ""
@@ -8668,7 +8900,7 @@ msgstr ""
msgid "Failed to enqueue the rebase operation, possibly due to a long-lived transaction. Try again later."
msgstr ""
-msgid "Failed to find import label for jira import."
+msgid "Failed to find import label for Jira import."
msgstr ""
msgid "Failed to get ref."
@@ -8692,6 +8924,12 @@ msgstr ""
msgid "Failed to load groups & users."
msgstr ""
+msgid "Failed to load labels. Please try again."
+msgstr ""
+
+msgid "Failed to load milestones. Please try again."
+msgstr ""
+
msgid "Failed to load related branches"
msgstr ""
@@ -9067,8 +9305,11 @@ msgstr "İşlem mesajına göre süz"
msgid "Filter by milestone name"
msgstr "Dönüm noktası adına göre süz"
-msgid "Filter by name..."
-msgstr "İsme göre filtrele..."
+msgid "Filter by name"
+msgstr ""
+
+msgid "Filter by status"
+msgstr ""
msgid "Filter by two-factor authentication"
msgstr "İki adımlı kimlik doğrulamasına göre süz"
@@ -9079,12 +9320,18 @@ msgstr ""
msgid "Filter projects"
msgstr "Projeleri filtrele"
+msgid "Filter results"
+msgstr ""
+
msgid "Filter results by group"
msgstr "Sonuçları gruba göre süz"
msgid "Filter results by project"
msgstr "Projeye göre sonuçları süz"
+msgid "Filter results..."
+msgstr ""
+
msgid "Filter your projects by name"
msgstr "Projelerinizi isme göre filtreleyin"
@@ -9373,11 +9620,11 @@ msgstr ""
msgid "GeoNodes|Checksummed"
msgstr "Sağlama yapıldı"
-msgid "GeoNodes|Container repositories"
+msgid "GeoNodes|Consult Geo troubleshooting information"
msgstr ""
-msgid "GeoNodes|Data is out of date from %{timeago}"
-msgstr "Veriler, %{timeago} itibaren güncel değil"
+msgid "GeoNodes|Container repositories"
+msgstr ""
msgid "GeoNodes|Data replication lag"
msgstr "Veri çoğaltma gecikmesi"
@@ -9418,6 +9665,9 @@ msgstr ""
msgid "GeoNodes|Last event ID seen from primary"
msgstr ""
+msgid "GeoNodes|Learn more about Geo node statuses"
+msgstr ""
+
msgid "GeoNodes|Loading nodes"
msgstr "Düğümler yükleniyor"
@@ -9433,6 +9683,9 @@ msgstr ""
msgid "GeoNodes|Node was successfully removed."
msgstr ""
+msgid "GeoNodes|Node's status was updated %{timeAgo}."
+msgstr ""
+
msgid "GeoNodes|Not checksummed"
msgstr ""
@@ -9463,7 +9716,10 @@ msgstr ""
msgid "GeoNodes|Repository verification progress"
msgstr ""
-msgid "GeoNodes|Selective"
+msgid "GeoNodes|Selective (%{syncLabel})"
+msgstr ""
+
+msgid "GeoNodes|Selective synchronization"
msgstr ""
msgid "GeoNodes|Something went wrong while changing node status"
@@ -9490,6 +9746,9 @@ msgstr ""
msgid "GeoNodes|Unverified"
msgstr ""
+msgid "GeoNodes|Updated %{timeAgo}"
+msgstr ""
+
msgid "GeoNodes|Used slots"
msgstr ""
@@ -9529,16 +9788,16 @@ msgstr ""
msgid "Geo|All"
msgstr ""
+msgid "Geo|All %{replicable_type}"
+msgstr ""
+
msgid "Geo|All projects"
msgstr "Geo|Tüm projeler"
-msgid "Geo|All projects are being scheduled for re-sync"
+msgid "Geo|All projects are being scheduled for resync"
msgstr ""
-msgid "Geo|All projects are being scheduled for re-verify"
-msgstr ""
-
-msgid "Geo|Batch operations"
+msgid "Geo|All projects are being scheduled for reverify"
msgstr ""
msgid "Geo|Could not remove tracking entry for an existing project."
@@ -9550,9 +9809,15 @@ msgstr ""
msgid "Geo|Failed"
msgstr ""
+msgid "Geo|Filter by status"
+msgstr ""
+
msgid "Geo|Geo Status"
msgstr "CoÄŸrafi Durum"
+msgid "Geo|In progress"
+msgstr ""
+
msgid "Geo|In sync"
msgstr ""
@@ -9577,9 +9842,6 @@ msgstr "Sonraki senkronizasyon planlandı"
msgid "Geo|Not synced yet"
msgstr ""
-msgid "Geo|Pending"
-msgstr ""
-
msgid "Geo|Pending synchronization"
msgstr ""
@@ -9616,7 +9878,7 @@ msgstr ""
msgid "Geo|Resync"
msgstr "Yeniden senkronize et"
-msgid "Geo|Resync all projects"
+msgid "Geo|Resync all"
msgstr ""
msgid "Geo|Retry count"
@@ -9625,7 +9887,7 @@ msgstr ""
msgid "Geo|Reverify"
msgstr ""
-msgid "Geo|Reverify all projects"
+msgid "Geo|Reverify all"
msgstr ""
msgid "Geo|Status"
@@ -9967,9 +10229,6 @@ msgstr ""
msgid "Go to file"
msgstr ""
-msgid "Go to file (MRs only)"
-msgstr ""
-
msgid "Go to file permalink (while viewing a file)"
msgstr "Dosya kalıcı bağlantısına git (bir dosyayı görüntülerken)"
@@ -10414,6 +10673,9 @@ msgstr ""
msgid "GroupSettings|Disable group mentions"
msgstr ""
+msgid "GroupSettings|Export group"
+msgstr ""
+
msgid "GroupSettings|If the parent group's visibility is lower than the group current visibility, visibility levels for subgroups and projects will be changed to match the new parent group's visibility."
msgstr ""
@@ -10492,6 +10754,9 @@ msgstr "Gruplar"
msgid "Groups (%{count})"
msgstr ""
+msgid "Groups (%{groups})"
+msgstr ""
+
msgid "Groups can also be nested by creating %{subgroup_docs_link_start}subgroups%{subgroup_docs_link_end}."
msgstr ""
@@ -10757,15 +11022,12 @@ msgstr ""
msgid "ID:"
msgstr ""
-msgid "IDE|Allow live previews of JavaScript projects in the Web IDE using CodeSandbox client side evaluation."
+msgid "IDE|Allow live previews of JavaScript projects in the Web IDE using CodeSandbox Live Preview."
msgstr ""
msgid "IDE|Back"
msgstr "Geri"
-msgid "IDE|Client side evaluation"
-msgstr ""
-
msgid "IDE|Commit"
msgstr ""
@@ -10829,6 +11091,9 @@ msgstr ""
msgid "If any job surpasses this timeout threshold, it will be marked as failed. Human readable time input language is accepted like \"1 hour\". Values without specification represent seconds."
msgstr ""
+msgid "If blank, set allowable lifetime to %{instance_level_policy_in_words}, as defined by the instance admin. Once set, existing tokens for users in this group may be revoked."
+msgstr ""
+
msgid "If checked, group owners can manage LDAP group links and LDAP member overrides"
msgstr ""
@@ -10868,6 +11133,9 @@ msgstr ""
msgid "If you lose your recovery codes you can generate new ones, invalidating all previous codes."
msgstr ""
+msgid "If you reach 100%% storage capacity, you will not be able to: %{base_message}"
+msgstr ""
+
msgid "If your HTTP repository is not publicly accessible, add your credentials."
msgstr ""
@@ -10979,6 +11247,9 @@ msgstr ""
msgid "Import repository"
msgstr ""
+msgid "Import started by: %{importInitiator}"
+msgstr ""
+
msgid "Import tasks"
msgstr ""
@@ -11045,6 +11316,9 @@ msgstr ""
msgid "In order to tailor your experience with GitLab we<br>would like to know a bit more about you."
msgstr ""
+msgid "In progress"
+msgstr ""
+
msgid "In the next step, you'll be able to select the projects you want to import."
msgstr ""
@@ -11392,15 +11666,15 @@ msgstr ""
msgid "Issues"
msgstr "Sorunlar"
-msgid "Issues / Merge Requests"
-msgstr ""
-
msgid "Issues Analytics"
msgstr ""
msgid "Issues Rate Limits"
msgstr ""
+msgid "Issues and Merge Requests"
+msgstr ""
+
msgid "Issues can be bugs, tasks or ideas to be discussed. Also, issues are searchable and filterable."
msgstr ""
@@ -11422,11 +11696,11 @@ msgstr ""
msgid "IssuesAnalytics|Avg/Month:"
msgstr "Ortalama/Ay:"
-msgid "IssuesAnalytics|Issues created"
+msgid "IssuesAnalytics|Issues opened"
msgstr ""
-msgid "IssuesAnalytics|Issues created per month"
-msgstr "Aylık oluşturulan sorunlar"
+msgid "IssuesAnalytics|Issues opened per month"
+msgstr ""
msgid "IssuesAnalytics|Last 12 months"
msgstr "Son 12 ay"
@@ -11479,6 +11753,9 @@ msgstr ""
msgid "Jira integration not configured."
msgstr ""
+msgid "Jira project: %{importProject}"
+msgstr ""
+
msgid "JiraService|Events for %{noteable_model_name} are disabled."
msgstr ""
@@ -11868,6 +12145,9 @@ msgstr ""
msgid "Lead"
msgstr ""
+msgid "Lead Time"
+msgstr ""
+
msgid "Learn GitLab"
msgstr ""
@@ -11880,6 +12160,9 @@ msgstr ""
msgid "Learn how to %{no_packages_link_start}publish and share your packages%{no_packages_link_end} with GitLab."
msgstr ""
+msgid "Learn how to enable synchronization"
+msgstr ""
+
msgid "Learn more"
msgstr "Daha fazlasını öğrenin"
@@ -12211,9 +12494,6 @@ msgstr ""
msgid "Live preview"
msgstr "Canlı önizleme"
-msgid "Load more vulnerabilities"
-msgstr ""
-
msgid "Loading"
msgstr ""
@@ -12364,9 +12644,6 @@ msgstr ""
msgid "Manage"
msgstr "Yönet"
-msgid "Manage Git repositories with fine-grained access controls that keep your code secure. Perform code reviews and enhance collaboration with merge requests. Each project can also have an issue tracker and a wiki."
-msgstr ""
-
msgid "Manage Web IDE features"
msgstr ""
@@ -12397,6 +12674,9 @@ msgstr ""
msgid "Manage your license"
msgstr ""
+msgid "Managed Account"
+msgstr ""
+
msgid "Manifest"
msgstr ""
@@ -12661,6 +12941,9 @@ msgstr "BirleÅŸtirme istekleri oluÅŸturuldu"
msgid "Merge Requests in Review"
msgstr ""
+msgid "Merge automatically (%{strategy})"
+msgstr ""
+
msgid "Merge commit message"
msgstr "Birleştirme işlemi mesajı"
@@ -12814,6 +13097,12 @@ msgstr "BirleÅŸtirildi"
msgid "Merged branches are being deleted. This can take some time depending on the number of branches. Please refresh the page to see changes."
msgstr ""
+msgid "Merged this merge request."
+msgstr ""
+
+msgid "Merges this merge request immediately."
+msgstr ""
+
msgid "Merges this merge request when the pipeline succeeds."
msgstr ""
@@ -12921,12 +13210,15 @@ msgid_plural "Metrics|Edit metrics"
msgstr[0] ""
msgstr[1] ""
-msgid "Metrics|Environment"
+msgid "Metrics|Expand panel"
msgstr ""
msgid "Metrics|For grouping similar metrics"
msgstr ""
+msgid "Metrics|Go back (Esc)"
+msgstr ""
+
msgid "Metrics|Invalid time range, please verify."
msgstr ""
@@ -12957,9 +13249,6 @@ msgstr ""
msgid "Metrics|Refresh dashboard"
msgstr ""
-msgid "Metrics|Show last"
-msgstr ""
-
msgid "Metrics|There was an error creating the dashboard."
msgstr ""
@@ -13038,6 +13327,9 @@ msgstr "ör. ist/sn"
msgid "Microsoft Azure"
msgstr "Microsoft Azure"
+msgid "Middleman project with Static Site Editor support"
+msgstr ""
+
msgid "Migrated %{success_count}/%{total_count} files."
msgstr ""
@@ -13483,10 +13775,10 @@ msgstr ""
msgid "Next"
msgstr ""
-msgid "Next file in diff (MRs only)"
+msgid "Next file in diff"
msgstr ""
-msgid "Next unresolved discussion (MRs only)"
+msgid "Next unresolved discussion"
msgstr ""
msgid "Nickname"
@@ -13546,6 +13838,9 @@ msgstr ""
msgid "No changes between %{ref_start}%{source_branch}%{ref_end} and %{ref_start}%{target_branch}%{ref_end}"
msgstr ""
+msgid "No child epics match applied filters"
+msgstr ""
+
msgid "No connection could be made to a Gitaly Server, please check your logs!"
msgstr ""
@@ -13684,15 +13979,6 @@ msgstr ""
msgid "No thanks, don't show this again"
msgstr ""
-msgid "No vulnerabilities found for this group"
-msgstr ""
-
-msgid "No vulnerabilities found for this pipeline"
-msgstr ""
-
-msgid "No vulnerabilities found for this project"
-msgstr ""
-
msgid "No vulnerabilities present"
msgstr ""
@@ -13816,6 +14102,9 @@ msgstr ""
msgid "Nothing to preview."
msgstr "Ön izlenecek bir şey yok."
+msgid "Nothing to synchronize"
+msgstr ""
+
msgid "Notification events"
msgstr "Bildirim etkinlikleri"
@@ -13987,6 +14276,9 @@ msgstr ""
msgid "Once removed, the fork relationship cannot be restored and you will no longer be able to send merge requests to the source."
msgstr ""
+msgid "Once the exported file is ready you can download it from this page."
+msgstr ""
+
msgid "Once the exported file is ready, you will receive a notification email with a download link, or you can download it from this page."
msgstr "Dışa aktarılan dosya hazır olduğunda, indirme bağlantısına sahip bir bildirim e-postası alacaksınız veya bu sayfadan indirebilirsiniz."
@@ -14088,9 +14380,6 @@ msgstr "Ham olarak aç"
msgid "Open sidebar"
msgstr "Kenar çubuğunu aç"
-msgid "Open source software to collaborate on code"
-msgstr "Kod üzerinde ortak çalışmak için açık kaynaklı yazılım"
-
msgid "Open: %{openIssuesCount}"
msgstr ""
@@ -14193,6 +14482,9 @@ msgstr ""
msgid "OutdatedBrowser|You can provide feedback %{feedback_link_start}on this issue%{feedback_link_end} or via your usual support channels."
msgstr ""
+msgid "Overridden"
+msgstr ""
+
msgid "Overview"
msgstr "Genel bakış"
@@ -14244,6 +14536,9 @@ msgstr ""
msgid "PackageRegistry|Conan Command"
msgstr ""
+msgid "PackageRegistry|Copy .pypirc content"
+msgstr ""
+
msgid "PackageRegistry|Copy Conan Command"
msgstr ""
@@ -14265,6 +14560,9 @@ msgstr ""
msgid "PackageRegistry|Copy NuGet Setup Command"
msgstr ""
+msgid "PackageRegistry|Copy Pip command"
+msgstr ""
+
msgid "PackageRegistry|Copy and paste this inside your %{codeStart}pom.xml%{codeEnd} %{codeStart}dependencies%{codeEnd} block."
msgstr ""
@@ -14298,6 +14596,12 @@ msgstr ""
msgid "PackageRegistry|For more information on the NuGet registry, %{linkStart}see the documentation%{linkEnd}."
msgstr ""
+msgid "PackageRegistry|For more information on the PyPi registry, %{linkStart}see the documentation%{linkEnd}."
+msgstr ""
+
+msgid "PackageRegistry|If you haven't already done so, you will need to add the below to your %{codeStart}.pypirc%{codeEnd} file."
+msgstr ""
+
msgid "PackageRegistry|If you haven't already done so, you will need to add the below to your %{codeStart}pom.xml%{codeEnd} file."
msgstr ""
@@ -14307,6 +14611,9 @@ msgstr ""
msgid "PackageRegistry|Learn how to %{noPackagesLinkStart}publish and share your packages%{noPackagesLinkEnd} with GitLab."
msgstr ""
+msgid "PackageRegistry|Manually Published"
+msgstr ""
+
msgid "PackageRegistry|Maven"
msgstr ""
@@ -14325,12 +14632,18 @@ msgstr ""
msgid "PackageRegistry|NuGet Command"
msgstr ""
+msgid "PackageRegistry|Pip Command"
+msgstr ""
+
msgid "PackageRegistry|Pipeline %{linkStart}%{linkEnd} triggered %{timestamp} by %{author}"
msgstr ""
msgid "PackageRegistry|Published to the repository at %{timestamp}"
msgstr ""
+msgid "PackageRegistry|PyPi"
+msgstr ""
+
msgid "PackageRegistry|Registry Setup"
msgstr ""
@@ -14367,6 +14680,9 @@ msgstr ""
msgid "PackageRegistry|npm"
msgstr ""
+msgid "PackageRegistry|published by %{author}"
+msgstr ""
+
msgid "PackageRegistry|yarn"
msgstr ""
@@ -14382,6 +14698,9 @@ msgstr ""
msgid "PackageType|NuGet"
msgstr ""
+msgid "PackageType|PyPi"
+msgstr ""
+
msgid "Packages"
msgstr "Paketler"
@@ -14475,6 +14794,9 @@ msgstr ""
msgid "Past due"
msgstr ""
+msgid "Paste a machine public key here. Read more about how to generate it"
+msgstr ""
+
msgid "Paste a machine public key here. Read more about how to generate it %{link_start}here%{link_end}"
msgstr ""
@@ -14490,9 +14812,6 @@ msgstr ""
msgid "Path"
msgstr "Yol"
-msgid "Path, transfer, remove"
-msgstr ""
-
msgid "Path:"
msgstr "Yol:"
@@ -14523,8 +14842,8 @@ msgstr ""
msgid "Percentage"
msgstr ""
-msgid "Perform advanced options such as changing path, transferring, or removing the group."
-msgstr "Yolu değiştirme, grubu aktarma veya kaldırma gibi gelişmiş seçenekleri uygulayın."
+msgid "Perform advanced options such as changing path, transferring, exporting, or removing the group."
+msgstr ""
msgid "Perform common operations on GitLab project"
msgstr ""
@@ -14604,9 +14923,6 @@ msgstr ""
msgid "Pipeline minutes quota"
msgstr "İş hattı dakika kotası"
-msgid "Pipeline quota"
-msgstr "İş Hattı Kotası"
-
msgid "Pipeline subscriptions"
msgstr ""
@@ -14616,6 +14932,9 @@ msgstr ""
msgid "Pipeline: %{status}"
msgstr "İş hattı: %{status}"
+msgid "PipelineCharts|CI / CD Analytics"
+msgstr ""
+
msgid "PipelineCharts|Failed:"
msgstr ""
@@ -14694,15 +15013,6 @@ msgstr ""
msgid "Pipelines settings for '%{project_name}' were successfully updated."
msgstr "'%{project_name}' için iş hattı ayarları başarıyla güncellendi."
-msgid "Pipelines| to purchase more minutes."
-msgstr ""
-
-msgid "Pipelines|%{namespace_name} has exceeded its pipeline minutes quota."
-msgstr ""
-
-msgid "Pipelines|%{namespace_name} has less than %{notification_level}%% of CI minutes available."
-msgstr ""
-
msgid "Pipelines|API"
msgstr ""
@@ -14724,12 +15034,15 @@ msgstr ""
msgid "Pipelines|Get started with Pipelines"
msgstr ""
-msgid "Pipelines|Loading Pipelines"
-msgstr "İş Hatları Yükleniyor"
+msgid "Pipelines|Group %{namespace_name} has %{percentage}%% or less Shared Runner Pipeline minutes remaining. Once it runs out, no new jobs or pipelines in its projects will run."
+msgstr ""
-msgid "Pipelines|Pipelines will not run anymore on shared Runners."
+msgid "Pipelines|Group %{namespace_name} has exceeded its pipeline minutes quota. Unless you buy additional pipeline minutes, no new jobs or pipelines in its projects will run."
msgstr ""
+msgid "Pipelines|Loading Pipelines"
+msgstr "İş Hatları Yükleniyor"
+
msgid "Pipelines|Project cache successfully reset."
msgstr ""
@@ -14958,6 +15271,9 @@ msgstr ""
msgid "Please select"
msgstr ""
+msgid "Please select a Jira project"
+msgstr ""
+
msgid "Please select a country"
msgstr "Lütfen bir ülke seçin"
@@ -15003,6 +15319,9 @@ msgstr ""
msgid "Please wait while we import the repository for you. Refresh at will."
msgstr ""
+msgid "Plugins directory is deprecated and will be removed in 14.0. Please move this file into /file_hooks directory."
+msgstr ""
+
msgid "Pod does not exist"
msgstr ""
@@ -15120,7 +15439,7 @@ msgstr ""
msgid "Prevent users from changing their profile name"
msgstr ""
-msgid "Prevent users from modifing merge request approvers list"
+msgid "Prevent users from modifying merge request approvers list"
msgstr ""
msgid "Prevent users from performing write operations on GitLab while performing maintenance."
@@ -15141,10 +15460,10 @@ msgstr "Önizleme yükü"
msgid "Previous Artifacts"
msgstr ""
-msgid "Previous file in diff (MRs only)"
+msgid "Previous file in diff"
msgstr ""
-msgid "Previous unresolved discussion (MRs only)"
+msgid "Previous unresolved discussion"
msgstr ""
msgid "Primary"
@@ -15177,6 +15496,9 @@ msgstr ""
msgid "Private profile"
msgstr "Özel profil"
+msgid "Private projects Minutes cost factor"
+msgstr ""
+
msgid "Private projects can be created in your personal namespace with:"
msgstr ""
@@ -16005,6 +16327,9 @@ msgstr "Viki"
msgid "ProjectSettings|With GitLab Pages you can host your static websites on GitLab"
msgstr "GitLab Pages ile statik web sitelerinizi GitLab üzerinde barındırabilirsiniz"
+msgid "ProjectSettings|With Metrics Dashboard you can visualize this project performance metrics"
+msgstr ""
+
msgid "ProjectTemplates|.NET Core"
msgstr ""
@@ -16068,6 +16393,9 @@ msgstr ""
msgid "ProjectTemplates|Spring"
msgstr ""
+msgid "ProjectTemplates|Static Site Editor/Middleman"
+msgstr ""
+
msgid "ProjectTemplates|iOS (Swift)"
msgstr ""
@@ -16287,13 +16615,13 @@ msgstr ""
msgid "PrometheusService|Prometheus is being automatically managed on your clusters"
msgstr ""
-msgid "PrometheusService|These metrics will only be monitored after your first deployment to an environment"
+msgid "PrometheusService|Select the Active checkbox to override the Auto Configuration with custom settings. If unchecked, Auto Configuration settings are used."
msgstr ""
-msgid "PrometheusService|Time-series monitoring service"
+msgid "PrometheusService|These metrics will only be monitored after your first deployment to an environment"
msgstr ""
-msgid "PrometheusService|To enable manual configuration, uninstall Prometheus from your clusters"
+msgid "PrometheusService|Time-series monitoring service"
msgstr ""
msgid "PrometheusService|To enable the installation of Prometheus on your clusters, deactivate the manual configuration below"
@@ -16479,6 +16807,9 @@ msgstr ""
msgid "Protip:"
msgstr ""
+msgid "Protocol"
+msgstr ""
+
msgid "Provider"
msgstr ""
@@ -16503,6 +16834,9 @@ msgstr ""
msgid "Public pipelines"
msgstr ""
+msgid "Public projects Minutes cost factor"
+msgstr ""
+
msgid "Pull"
msgstr ""
@@ -16703,6 +17037,9 @@ msgid_plural "Refreshing in %d seconds to show the updated status..."
msgstr[0] "Güncellenmiş durumu göstermek için bir saniye içinde yenilenecek..."
msgstr[1] "Güncellenmiş durumu göstermek için %d saniye içinde yenilenecek..."
+msgid "Regenerate export"
+msgstr ""
+
msgid "Regenerate instance ID"
msgstr ""
@@ -17080,9 +17417,29 @@ msgstr ""
msgid "Reports|%{combinedString} and %{resolvedString}"
msgstr ""
+msgid "Reports|Accessibility scanning detected %d issue for the source branch only"
+msgid_plural "Reports|Accessibility scanning detected %d issues for the source branch only"
+msgstr[0] ""
+msgstr[1] ""
+
+msgid "Reports|Accessibility scanning detected no issues for the source branch only"
+msgstr ""
+
+msgid "Reports|Accessibility scanning failed loading results"
+msgstr ""
+
+msgid "Reports|Accessibility scanning results are being parsed"
+msgstr ""
+
msgid "Reports|Actions"
msgstr ""
+msgid "Reports|An error occured while loading report"
+msgstr ""
+
+msgid "Reports|An error occurred while loading %{name} results"
+msgstr ""
+
msgid "Reports|Class"
msgstr "Sınıf"
@@ -17188,7 +17545,7 @@ msgstr ""
msgid "Requested %{time_ago}"
msgstr ""
-msgid "Requested design version does not exist"
+msgid "Requested design version does not exist."
msgstr ""
msgid "Requested states are invalid"
@@ -17372,6 +17729,9 @@ msgstr ""
msgid "Resync"
msgstr ""
+msgid "Resync all"
+msgstr ""
+
msgid "Resync all %{replicableType}"
msgstr ""
@@ -17440,6 +17800,9 @@ msgstr ""
msgid "Revoked personal access token %{personal_access_token_name}!"
msgstr ""
+msgid "Revoked project access token %{project_access_token_name}!"
+msgstr ""
+
msgid "RightSidebar|adding a"
msgstr ""
@@ -17488,6 +17851,9 @@ msgstr ""
msgid "Runner token"
msgstr ""
+msgid "Runner tokens"
+msgstr ""
+
msgid "Runner was not updated."
msgstr "Çalıştırıcı güncellenemedi."
@@ -17542,6 +17908,9 @@ msgstr ""
msgid "SAML SSO for %{group_name}"
msgstr ""
+msgid "SAML discovery tokens"
+msgstr ""
+
msgid "SAML for %{group_name}"
msgstr ""
@@ -17626,12 +17995,18 @@ msgstr ""
msgid "Scheduled"
msgstr ""
+msgid "Scheduled to merge this merge request (%{strategy})."
+msgstr ""
+
msgid "Scheduled to merge this merge request when the pipeline succeeds."
msgstr ""
msgid "Schedules"
msgstr ""
+msgid "Schedules to merge this merge request (%{strategy})."
+msgstr ""
+
msgid "Scheduling"
msgstr ""
@@ -17689,6 +18064,9 @@ msgstr "Dalları ara"
msgid "Search branches and tags"
msgstr "Dalları ve etiketleri ara"
+msgid "Search by author"
+msgstr ""
+
msgid "Search files"
msgstr "Dosyaları ara"
@@ -17834,11 +18212,6 @@ msgid_plural "SearchResults|snippets"
msgstr[0] "parçacık"
msgstr[1] "parçacık"
-msgid "SearchResults|snippet result"
-msgid_plural "SearchResults|snippet results"
-msgstr[0] "parçacık sonucu"
-msgstr[1] "parçacık sonucu"
-
msgid "SearchResults|user"
msgid_plural "SearchResults|users"
msgstr[0] ""
@@ -17849,6 +18222,9 @@ msgid_plural "SearchResults|wiki results"
msgstr[0] "viki sonucu"
msgstr[1] "viki sonucu"
+msgid "Searching by both author and message is currently not supported."
+msgstr ""
+
msgid "Seat Link"
msgstr ""
@@ -17876,184 +18252,232 @@ msgstr "Güvenlik Yapılandırması"
msgid "Security Dashboard"
msgstr "Güvenlik Kontrol Panosu"
-msgid "Security Dashboard|Error fetching the vulnerability counts. Please check your network connection and try again."
+msgid "Security configuration help link"
msgstr ""
-msgid "Security Dashboard|Error fetching the vulnerability list. Please check your network connection and try again."
+msgid "Security dashboard"
+msgstr "Güvenlik gösterge panosu"
+
+msgid "Security report is out of date. Please update your branch with the latest changes from the target branch (%{targetBranchName})"
msgstr ""
-msgid "Security Dashboard|Issue Created"
-msgstr "Sorun oluÅŸturuldu"
+msgid "Security report is out of date. Run %{newPipelineLinkStart}a new pipeline%{newPipelineLinkEnd} for the target branch (%{targetBranchName})"
+msgstr ""
+
+msgid "SecurityConfiguration|Configured"
+msgstr "Yapılandırılan"
-msgid "Security Reports|Comment added to '%{vulnerabilityName}'"
+msgid "SecurityConfiguration|Feature"
msgstr ""
-msgid "Security Reports|Comment deleted on '%{vulnerabilityName}'"
+msgid "SecurityConfiguration|Feature documentation for %{featureName}"
msgstr ""
-msgid "Security Reports|Comment edited on '%{vulnerabilityName}'"
+msgid "SecurityConfiguration|Not yet configured"
msgstr ""
-msgid "Security Reports|Create issue"
-msgstr "Sorun oluÅŸtur"
+msgid "SecurityConfiguration|Secure features"
+msgstr ""
-msgid "Security Reports|Dismiss vulnerability"
+msgid "SecurityConfiguration|Status"
+msgstr "Durum"
+
+msgid "SecurityReports|%{firstProject} and %{secondProject}"
msgstr ""
-msgid "Security Reports|Dismissed '%{vulnerabilityName}'"
+msgid "SecurityReports|%{firstProject}, %{secondProject}, and %{rest}"
msgstr ""
-msgid "Security Reports|Dismissed '%{vulnerabilityName}'. Turn off the hide dismissed toggle to view."
+msgid "SecurityReports|Add a project to your dashboard"
msgstr ""
-msgid "Security Reports|Either you don't have permission to view this dashboard or the dashboard has not been setup. Please check your permission settings with your administrator or check your dashboard configurations to proceed."
+msgid "SecurityReports|Add or remove projects from your dashboard"
msgstr ""
-msgid "Security Reports|Learn more about setting up your dashboard"
-msgstr "Gösterge panonuzu ayarlama hakkında daha fazla bilgi edinin"
+msgid "SecurityReports|Add projects"
+msgstr ""
-msgid "Security Reports|More info"
+msgid "SecurityReports|Comment added to '%{vulnerabilityName}'"
msgstr ""
-msgid "Security Reports|Oops, something doesn't seem right."
+msgid "SecurityReports|Comment deleted on '%{vulnerabilityName}'"
msgstr ""
-msgid "Security Reports|Security reports can only be accessed by authorized users."
+msgid "SecurityReports|Comment edited on '%{vulnerabilityName}'"
msgstr ""
-msgid "Security Reports|There was an error adding the comment."
+msgid "SecurityReports|Create issue"
msgstr ""
-msgid "Security Reports|There was an error creating the issue."
+msgid "SecurityReports|Dismiss Selected"
msgstr ""
-msgid "Security Reports|There was an error creating the merge request."
+msgid "SecurityReports|Dismiss vulnerability"
msgstr ""
-msgid "Security Reports|There was an error deleting the comment."
+msgid "SecurityReports|Dismissed '%{vulnerabilityName}'"
msgstr ""
-msgid "Security Reports|There was an error dismissing the vulnerabilities."
+msgid "SecurityReports|Dismissed '%{vulnerabilityName}'. Turn off the hide dismissed toggle to view."
msgstr ""
-msgid "Security Reports|There was an error dismissing the vulnerability."
+msgid "SecurityReports|Each vulnerability now has a unique page that can be directly linked to, shared, referenced, and tracked as the single source of truth. Vulnerability occurrences also persist across scanner runs, which improves tracking and visibility and reduces duplicates between scans."
msgstr ""
-msgid "Security Reports|There was an error reverting the dismissal."
+msgid "SecurityReports|Edit dashboard"
msgstr ""
-msgid "Security Reports|There was an error reverting this dismissal."
+msgid "SecurityReports|Either you don't have permission to view this dashboard or the dashboard has not been setup. Please check your permission settings with your administrator or check your dashboard configurations to proceed."
msgstr ""
-msgid "Security Reports|Undo dismiss"
+msgid "SecurityReports|Error fetching the vulnerability counts. Please check your network connection and try again."
msgstr ""
-msgid "Security Reports|You do not have sufficient permissions to access this report"
+msgid "SecurityReports|Error fetching the vulnerability list. Please check your network connection and try again."
msgstr ""
-msgid "Security Reports|You must sign in as an authorized user to see this report"
+msgid "SecurityReports|False positive"
msgstr ""
-msgid "Security configuration help link"
+msgid "SecurityReports|Hide dismissed"
msgstr ""
-msgid "Security dashboard"
-msgstr "Güvenlik gösterge panosu"
+msgid "SecurityReports|Introducing standalone vulnerabilities"
+msgstr ""
-msgid "Security report is out of date. Please update your branch with the latest changes from the target branch (%{targetBranchName})"
+msgid "SecurityReports|Issue Created"
msgstr ""
-msgid "Security report is out of date. Run %{newPipelineLinkStart}a new pipeline%{newPipelineLinkEnd} for the target branch (%{targetBranchName})"
+msgid "SecurityReports|Learn More"
msgstr ""
-msgid "SecurityConfiguration|Configured"
-msgstr "Yapılandırılan"
+msgid "SecurityReports|Learn more about setting up your dashboard"
+msgstr ""
-msgid "SecurityConfiguration|Feature"
+msgid "SecurityReports|Load more vulnerabilities"
msgstr ""
-msgid "SecurityConfiguration|Feature documentation for %{featureName}"
+msgid "SecurityReports|Monitor vulnerabilities in your code"
msgstr ""
-msgid "SecurityConfiguration|Not yet configured"
+msgid "SecurityReports|More info"
msgstr ""
-msgid "SecurityConfiguration|Secure features"
+msgid "SecurityReports|More information"
msgstr ""
-msgid "SecurityConfiguration|Status"
-msgstr "Durum"
+msgid "SecurityReports|No vulnerabilities found for dashboard"
+msgstr ""
-msgid "SecurityDashboard|%{firstProject} and %{secondProject}"
+msgid "SecurityReports|No vulnerabilities found for this group"
msgstr ""
-msgid "SecurityDashboard|%{firstProject}, %{secondProject}, and %{rest}"
+msgid "SecurityReports|No vulnerabilities found for this pipeline"
msgstr ""
-msgid "SecurityDashboard|Add a project to your dashboard"
+msgid "SecurityReports|No vulnerabilities found for this project"
msgstr ""
-msgid "SecurityDashboard|Add or remove projects from your dashboard"
+msgid "SecurityReports|Oops, something doesn't seem right."
msgstr ""
-msgid "SecurityDashboard|Add projects"
-msgstr "Proje ekle"
+msgid "SecurityReports|Pipeline %{pipelineLink} triggered %{timeago} by %{user}"
+msgstr ""
-msgid "SecurityDashboard|Edit dashboard"
+msgid "SecurityReports|Project"
msgstr ""
-msgid "SecurityDashboard|Hide dismissed"
-msgstr "Reddedilenleri gizle"
+msgid "SecurityReports|Projects added"
+msgstr ""
-msgid "SecurityDashboard|Monitor vulnerabilities in your code"
+msgid "SecurityReports|Remove project from dashboard"
msgstr ""
-msgid "SecurityDashboard|More information"
-msgstr "Daha fazla bilgi"
+msgid "SecurityReports|Report type"
+msgstr ""
-msgid "SecurityDashboard|No vulnerabilities found for dashboard"
+msgid "SecurityReports|Return to dashboard"
msgstr ""
-msgid "SecurityDashboard|Pipeline %{pipelineLink} triggered %{timeago} by %{user}"
+msgid "SecurityReports|Security Dashboard"
msgstr ""
-msgid "SecurityDashboard|Project"
-msgstr "Proje"
+msgid "SecurityReports|Security reports can only be accessed by authorized users."
+msgstr ""
+
+msgid "SecurityReports|Select a project to add by using the project search field above."
+msgstr ""
+
+msgid "SecurityReports|Select a reason"
+msgstr ""
-msgid "SecurityDashboard|Projects added"
-msgstr "Projeler eklendi"
+msgid "SecurityReports|Severity"
+msgstr ""
-msgid "SecurityDashboard|Remove project from dashboard"
+msgid "SecurityReports|Status"
msgstr ""
-msgid "SecurityDashboard|Report type"
-msgstr "Rapor türü"
+msgid "SecurityReports|The security dashboard displays the latest security findings for projects you wish to monitor. Select \"Edit dashboard\" to add and remove projects."
+msgstr ""
-msgid "SecurityDashboard|Return to dashboard"
-msgstr "Gösterge panosuna geri dön"
+msgid "SecurityReports|The security dashboard displays the latest security report. Use it to find and fix vulnerabilities."
+msgstr ""
-msgid "SecurityDashboard|Security Dashboard"
-msgstr "Güvenlik Kontrol Panosu"
+msgid "SecurityReports|There was an error adding the comment."
+msgstr ""
+
+msgid "SecurityReports|There was an error creating the issue."
+msgstr ""
+
+msgid "SecurityReports|There was an error creating the merge request."
+msgstr ""
+
+msgid "SecurityReports|There was an error deleting the comment."
+msgstr ""
+
+msgid "SecurityReports|There was an error dismissing the vulnerabilities."
+msgstr ""
-msgid "SecurityDashboard|Select a project to add by using the project search field above."
+msgid "SecurityReports|There was an error dismissing the vulnerability."
msgstr ""
-msgid "SecurityDashboard|Severity"
-msgstr "Önem Derecesi"
+msgid "SecurityReports|There was an error reverting the dismissal."
+msgstr ""
+
+msgid "SecurityReports|There was an error reverting this dismissal."
+msgstr ""
+
+msgid "SecurityReports|There was an error while generating the report."
+msgstr ""
-msgid "SecurityDashboard|Status"
+msgid "SecurityReports|Unable to add %{invalidProjects}"
msgstr ""
-msgid "SecurityDashboard|The security dashboard displays the latest security findings for projects you wish to monitor. Select \"Edit dashboard\" to add and remove projects."
+msgid "SecurityReports|Undo dismiss"
msgstr ""
-msgid "SecurityDashboard|The security dashboard displays the latest security report. Use it to find and fix vulnerabilities."
+msgid "SecurityReports|While it's rare to have no vulnerabilities for your group, it can happen. In any event, we ask that you double check your settings to make sure you've set up your dashboard correctly."
msgstr ""
-msgid "SecurityDashboard|There was an error while generating the report."
+msgid "SecurityReports|While it's rare to have no vulnerabilities for your pipeline, it can happen. In any event, we ask that you double check your settings to make sure all security scanning jobs have passed successfully."
msgstr ""
-msgid "SecurityDashboard|Unable to add %{invalidProjects}"
+msgid "SecurityReports|While it's rare to have no vulnerabilities for your project, it can happen. In any event, we ask that you double check your settings to make sure you've set up your dashboard correctly."
+msgstr ""
+
+msgid "SecurityReports|While it's rare to have no vulnerabilities, it can happen. In any event, we ask that you please double check your settings to make sure you've set up your dashboard correctly."
+msgstr ""
+
+msgid "SecurityReports|Won't fix / Accept risk"
+msgstr ""
+
+msgid "SecurityReports|You do not have sufficient permissions to access this report"
+msgstr ""
+
+msgid "SecurityReports|You must sign in as an authorized user to see this report"
+msgstr ""
+
+msgid "SecurityReports|[No reason]"
msgstr ""
msgid "See GitLab's %{password_policy_guidelines}"
@@ -18065,6 +18489,9 @@ msgstr "Metrikleri gör"
msgid "See the affected projects in the GitLab admin panel"
msgstr ""
+msgid "See what's new at GitLab"
+msgstr ""
+
msgid "Select"
msgstr ""
@@ -18470,9 +18897,6 @@ msgstr ""
msgid "Set up Jira Integration"
msgstr ""
-msgid "Set up Jira Integration illustration"
-msgstr ""
-
msgid "Set up a %{type} Runner automatically"
msgstr ""
@@ -18557,6 +18981,9 @@ msgstr ""
msgid "Severity: %{severity}"
msgstr ""
+msgid "Shards (%{shards})"
+msgstr ""
+
msgid "Shards to synchronize"
msgstr ""
@@ -18871,6 +19298,9 @@ msgstr ""
msgid "Snowplow"
msgstr ""
+msgid "Some child epics may be hidden due to applied filters"
+msgstr ""
+
msgid "Some email servers do not support overriding the email sender name. Enable this option to include the name of the author of the issue, merge request or comment in the email body instead."
msgstr ""
@@ -19012,6 +19442,9 @@ msgstr ""
msgid "Something went wrong while updating your list settings"
msgstr ""
+msgid "Something went wrong with your automatic subscription renewal"
+msgstr ""
+
msgid "Something went wrong, unable to add %{project} to dashboard"
msgstr ""
@@ -19297,6 +19730,9 @@ msgstr ""
msgid "Stage removed"
msgstr ""
+msgid "Standard"
+msgstr ""
+
msgid "Star a label to make it a priority label. Order the prioritized labels to change their relative priority, by dragging."
msgstr ""
@@ -19420,6 +19856,9 @@ msgstr ""
msgid "Static Application Security Testing (SAST)"
msgstr ""
+msgid "StaticSiteEditor|An error occurred while submitting your changes."
+msgstr ""
+
msgid "StaticSiteEditor|Branch could not be created."
msgstr ""
@@ -19429,18 +19868,30 @@ msgstr ""
msgid "StaticSiteEditor|Could not create merge request."
msgstr ""
+msgid "StaticSiteEditor|Incompatible file content"
+msgstr ""
+
msgid "StaticSiteEditor|Return to site"
msgstr ""
+msgid "StaticSiteEditor|Static site editor"
+msgstr ""
+
msgid "StaticSiteEditor|Success!"
msgstr ""
msgid "StaticSiteEditor|Summary of changes"
msgstr ""
+msgid "StaticSiteEditor|The Static Site Editor is currently configured to only edit Markdown content on pages generated from Middleman. Visit the documentation to learn more about configuring your site to use the Static Site Editor."
+msgstr ""
+
msgid "StaticSiteEditor|Update %{sourcePath} file"
msgstr ""
+msgid "StaticSiteEditor|View documentation"
+msgstr ""
+
msgid "StaticSiteEditor|View merge request"
msgstr ""
@@ -19498,6 +19949,12 @@ msgstr ""
msgid "StatusPage|Status page"
msgstr ""
+msgid "StatusPage|Status page URL"
+msgstr ""
+
+msgid "StatusPage|Status page frontend documentation"
+msgstr ""
+
msgid "StatusPage|To publish incidents to an external status page, GitLab will store a JSON file in your Amazon S3 account in a location accessible to your external status page service. Make sure to also set up %{docsLink}"
msgstr ""
@@ -19846,6 +20303,12 @@ msgstr ""
msgid "Synced"
msgstr ""
+msgid "Synchronization disabled"
+msgstr ""
+
+msgid "Synchronization of container repositories is disabled."
+msgstr ""
+
msgid "System"
msgstr "Sistem"
@@ -19999,6 +20462,9 @@ msgstr ""
msgid "Telephone number"
msgstr "Telefon numarası"
+msgid "Telephone number (Optional)"
+msgstr ""
+
msgid "Template"
msgstr "Åžablon"
@@ -20305,6 +20771,9 @@ msgstr ""
msgid "The license was successfully uploaded and is now active. You can see the details below."
msgstr ""
+msgid "The license was successfully uploaded and will be active from %{starts_at}. You can see the details below."
+msgstr ""
+
msgid "The maximum file size allowed is %{size}."
msgstr ""
@@ -20626,6 +21095,9 @@ msgstr ""
msgid "There was an error getting the epic participants."
msgstr ""
+msgid "There was an error importing the Jira project."
+msgstr ""
+
msgid "There was an error loading users activity calendar."
msgstr "Kullanıcı etkinlik takvimi yüklenirken bir hata oluştu."
@@ -20641,7 +21113,7 @@ msgstr "Grup iş hattı dakikaları sıfırlanırken bir hata oluştu."
msgid "There was an error resetting user pipeline minutes."
msgstr "Kullanıcı iş hattı dakikaları sıfırlanırken bir hata oluştu."
-msgid "There was an error saving this Geo Node"
+msgid "There was an error saving this Geo Node."
msgstr ""
msgid "There was an error saving your changes."
@@ -20704,6 +21176,9 @@ msgstr ""
msgid "They can be managed using the %{link}."
msgstr ""
+msgid "Third Party Advisory Link"
+msgstr ""
+
msgid "Third party offers"
msgstr "Üçüncü taraf teklifleri"
@@ -20740,6 +21215,9 @@ msgstr ""
msgid "This application will be able to:"
msgstr ""
+msgid "This attachment has been truncated to avoid exceeding the maximum allowed attachment size of 15MB. %{written_count} of %{issues_count} issues have been included. Consider re-exporting with a narrower selection of issues."
+msgstr ""
+
msgid "This block is self-referential"
msgstr ""
@@ -20881,6 +21359,12 @@ msgstr ""
msgid "This issue is confidential"
msgstr "Bu sorun gizlidir"
+msgid "This issue is currently blocked by the following issues: %{issues}."
+msgstr ""
+
+msgid "This issue is in a child epic of the filtered epic"
+msgstr ""
+
msgid "This issue is locked."
msgstr "Bu sorun kilitlendi."
@@ -21079,7 +21563,10 @@ msgstr ""
msgid "This user will be the author of all events in the activity feed that are the result of an update, like new branches being created or new commits being pushed to existing branches. Upon creation or when reassigning you can only assign yourself to be the mirror user."
msgstr ""
-msgid "This variable can not be masked"
+msgid "This variable can not be masked."
+msgstr ""
+
+msgid "This variable does not match the expected pattern."
msgstr ""
msgid "This will help us personalize your onboarding experience."
@@ -21109,10 +21596,10 @@ msgstr ""
msgid "ThreatMonitoring|Application firewall not detected"
msgstr ""
-msgid "ThreatMonitoring|Container Network Policy"
+msgid "ThreatMonitoring|Container Network Policies are not installed or have been disabled. To view this data, ensure your Network Policies are installed and enabled for your cluster."
msgstr ""
-msgid "ThreatMonitoring|Container NetworkPolicies are not installed or has been disabled. To view this data, ensure you NetworkPolicies are installed and enabled for your cluster."
+msgid "ThreatMonitoring|Container Network Policy"
msgstr ""
msgid "ThreatMonitoring|Container NetworkPolicies not detected"
@@ -21145,7 +21632,7 @@ msgstr ""
msgid "ThreatMonitoring|Something went wrong, unable to fetch statistics"
msgstr ""
-msgid "ThreatMonitoring|The firewall is not installed or has been disabled. To view this data, ensure you firewall is installed and enabled for your cluster."
+msgid "ThreatMonitoring|The firewall is not installed or has been disabled. To view this data, ensure the web application firewall is installed and enabled for your cluster."
msgstr ""
msgid "ThreatMonitoring|The graph below is an overview of traffic coming to your application as tracked by the Web Application Firewall (WAF). View the docs for instructions on how to access the WAF logs to see what type of malicious traffic is trying to access your app. The docs link is also accessible by clicking the \"?\" icon next to the title below."
@@ -21214,6 +21701,9 @@ msgstr "Son işlemden birleştirmeye kadar geçen süre"
msgid "Time in seconds GitLab will wait for a response from the external service. When the service does not respond in time, access will be denied."
msgstr ""
+msgid "Time of import: %{importTime}"
+msgstr ""
+
msgid "Time remaining"
msgstr "Kalan süre"
@@ -21398,8 +21888,8 @@ msgstr "Başlık"
msgid "Title:"
msgstr ""
-msgid "Titles and Filenames"
-msgstr "Başlıklar ve dosya adları"
+msgid "Titles and Descriptions"
+msgstr ""
msgid "To"
msgstr ""
@@ -21605,8 +22095,8 @@ msgstr ""
msgid "Too many projects enabled. You will need to manage them via the console or the API."
msgstr ""
-msgid "Topics"
-msgstr "Konular"
+msgid "Topics (optional)"
+msgstr ""
msgid "Total"
msgstr ""
@@ -21767,6 +22257,9 @@ msgstr "Tekrar denensin mi?"
msgid "Try all GitLab has to offer for 30 days."
msgstr "GitLab’ın sunduğu her şeyi 30 gün boyunca deneyin."
+msgid "Try changing or removing filters."
+msgstr ""
+
msgid "Try to fork again"
msgstr "Tekrar çatallamayı dene"
@@ -21860,6 +22353,9 @@ msgstr ""
msgid "Unable to connect to server: %{error}"
msgstr ""
+msgid "Unable to connect to the Jira instance. Please check your Jira integration configuration."
+msgstr ""
+
msgid "Unable to convert Kubernetes logs encoding to UTF-8"
msgstr ""
@@ -22803,6 +23299,9 @@ msgstr ""
msgid "View issue"
msgstr ""
+msgid "View issues"
+msgstr ""
+
msgid "View it on GitLab"
msgstr "GitLab'da görüntüle"
@@ -22980,6 +23479,15 @@ msgstr ""
msgid "VulnerabilityManagement|Resolved %{timeago} by %{user}"
msgstr ""
+msgid "VulnerabilityManagement|Something went wrong while trying to delete the comment. Please try again later."
+msgstr ""
+
+msgid "VulnerabilityManagement|Something went wrong while trying to retrieve the vulnerability history. Please try again later."
+msgstr ""
+
+msgid "VulnerabilityManagement|Something went wrong while trying to save the comment. Please try again later."
+msgstr ""
+
msgid "VulnerabilityManagement|Something went wrong, could not create an issue."
msgstr ""
@@ -23010,6 +23518,9 @@ msgstr ""
msgid "VulnerabilityStatusTypes|Resolved"
msgstr ""
+msgid "Vulnerability|%{scannerName} (version %{scannerVersion})"
+msgstr ""
+
msgid "Vulnerability|Class"
msgstr ""
@@ -23043,8 +23554,11 @@ msgstr ""
msgid "Vulnerability|Project"
msgstr "Proje"
-msgid "Vulnerability|Report Type"
-msgstr "Rapor Türü"
+msgid "Vulnerability|Scanner Provider"
+msgstr ""
+
+msgid "Vulnerability|Scanner Type"
+msgstr ""
msgid "Vulnerability|Severity"
msgstr ""
@@ -23097,9 +23611,15 @@ msgstr ""
msgid "We sent you an email with reset password instructions"
msgstr ""
+msgid "We tried to automatically renew your %{strong}%{plan_name}%{strong_close} subscription for %{strong}%{namespace_name}%{strong_close} on %{expires_on} but something went wrong so your subscription was downgraded to the free plan. Don't worry, your data is safe. We suggest you check your payment method and get in touch with our support team (%{support_link}). They'll gladly help with your subscription renewal."
+msgstr ""
+
msgid "We want to be sure it is you, please confirm you are not a robot."
msgstr ""
+msgid "We will automatically renew your %{strong}%{plan_name}%{strong_close} subscription for %{strong}%{namespace_name}%{strong_close} on %{strong}%{expires_on}%{strong_close}. There's nothing that you need to do, we'll let you know when the renewal is complete. Need more seats, a higher plan or just want to review your payment method?"
+msgstr ""
+
msgid "We've found no vulnerabilities"
msgstr ""
@@ -23204,15 +23724,6 @@ msgstr ""
msgid "When:"
msgstr ""
-msgid "While it's rare to have no vulnerabilities for your group, it can happen. In any event, we ask that you double check your settings to make sure you've set up your dashboard correctly."
-msgstr ""
-
-msgid "While it's rare to have no vulnerabilities for your pipeline, it can happen. In any event, we ask that you double check your settings to make sure all security scanning jobs have passed successfully."
-msgstr ""
-
-msgid "While it's rare to have no vulnerabilities for your project, it can happen. In any event, we ask that you double check your settings to make sure you've set up your dashboard correctly."
-msgstr ""
-
msgid "While it's rare to have no vulnerabilities, it can happen. In any event, we ask that you please double check your settings to make sure you've set up your dashboard correctly."
msgstr ""
@@ -23243,9 +23754,6 @@ msgstr ""
msgid "Wiki"
msgstr "Viki"
-msgid "Wiki pages"
-msgstr "Viki sayfaları"
-
msgid "Wiki was successfully updated."
msgstr "Wiki başarıyla güncellendi."
@@ -23429,6 +23937,9 @@ msgstr ""
msgid "Yes, add it"
msgstr "Evet, bunu ekle"
+msgid "Yes, close issue"
+msgstr ""
+
msgid "Yes, let me map Google Code users to full names or GitLab users."
msgstr ""
@@ -23624,6 +24135,9 @@ msgstr "Bu salt okunur GitLab örneğine yazamazsınız."
msgid "You could not create a new trigger."
msgstr ""
+msgid "You didn't renew your %{strong}%{plan_name}%{strong_close} subscription for %{strong}%{namespace_name}%{strong_close} so it was downgraded to the free plan."
+msgstr ""
+
msgid "You didn't renew your %{strong}%{plan_name}%{strong_close} subscription so it was downgraded to the GitLab Core Plan."
msgstr ""
@@ -23690,6 +24204,9 @@ msgstr ""
msgid "You have declined the invitation to join %{label}."
msgstr ""
+msgid "You have imported from this project %{numberOfPreviousImportsForProject} times before. Each new import will create duplicate issues."
+msgstr ""
+
msgid "You have no permissions"
msgstr "Ä°zinleriniz yok"
@@ -23771,6 +24288,9 @@ msgstr ""
msgid "You need to upload a Google Takeout archive."
msgstr ""
+msgid "You reached %{usage_in_percent} of %{namespace_name}'s capacity (%{used_storage} of %{storage_limit})"
+msgstr ""
+
msgid "You tried to fork %{link_to_the_project} but it failed for the following reason:"
msgstr ""
@@ -23855,9 +24375,21 @@ msgstr ""
msgid "YouTube"
msgstr "YouTube"
+msgid "Your %{strong}%{plan_name}%{strong_close} subscription for %{strong}%{namespace_name}%{strong_close} will expire on %{strong}%{expires_on}%{strong_close}. After that, you will not to be able to create issues or merge requests as well as many other features."
+msgstr ""
+
msgid "Your %{strong}%{plan_name}%{strong_close} subscription will expire on %{strong}%{expires_on}%{strong_close}. After that, you will not to be able to create issues or merge requests as well as many other features."
msgstr ""
+msgid "Your CSV export has started. It will be emailed to %{email} when complete."
+msgstr ""
+
+msgid "Your CSV export of %{issues_count} from project %{project_link} has been added to this email as an attachment."
+msgstr ""
+
+msgid "Your CSV export of %{written_count} from project %{project_name} (%{project_url}) has been added to this email as an attachment."
+msgstr ""
+
msgid "Your Commit Email will be used for web based operations, such as edits and merges."
msgstr "İşlem e-postanız, düzenlemeler ve birleştirmeler gibi web tabanlı işlemler için kullanılacaktır."
@@ -23954,6 +24486,9 @@ msgstr ""
msgid "Your comment could not be updated! Please check your network connection and try again."
msgstr ""
+msgid "Your comment will be discarded."
+msgstr ""
+
msgid "Your custom stage '%{title}' was created"
msgstr ""
@@ -23993,6 +24528,9 @@ msgstr ""
msgid "Your new personal access token has been created."
msgstr ""
+msgid "Your new project access token has been created."
+msgstr ""
+
msgid "Your password isn't required to view this page. If a password or any other personal details are requested, please contact your administrator to report abuse."
msgstr ""
@@ -24011,12 +24549,18 @@ msgstr "Projeleriniz"
msgid "Your request for access has been queued for review."
msgstr ""
+msgid "Your search didn't match any commits."
+msgstr ""
+
msgid "Your subscription expired!"
msgstr ""
msgid "Your subscription has been downgraded"
msgstr ""
+msgid "Your subscription will automatically renew in %{remaining_days}"
+msgstr ""
+
msgid "Your subscription will expire in %{remaining_days}"
msgstr ""
@@ -24055,7 +24599,7 @@ msgstr "önce"
msgid "allowed to fail"
msgstr ""
-msgid "already being used for another group or project milestone."
+msgid "already being used for another group or project %{timebox_name}."
msgstr ""
msgid "already has a \"created\" issue link"
@@ -24133,6 +24677,9 @@ msgstr ""
msgid "ciReport|%{linkStartTag}Learn more about SAST %{linkEndTag}"
msgstr ""
+msgid "ciReport|%{linkStartTag}Learn more about Secret Scanning %{linkEndTag}"
+msgstr ""
+
msgid "ciReport|%{linkStartTag}Learn more about codequality reports %{linkEndTag}"
msgstr ""
@@ -24296,6 +24843,12 @@ msgstr ""
msgid "ciReport|SAST"
msgstr ""
+msgid "ciReport|Secret scanning"
+msgstr ""
+
+msgid "ciReport|Secret scanning detects secrets and credentials vulnerabilities in your source code."
+msgstr ""
+
msgid "ciReport|Security scanning"
msgstr "Güvenlik taraması"
@@ -24493,6 +25046,9 @@ msgstr ""
msgid "group"
msgstr "grup"
+msgid "groups"
+msgstr ""
+
msgid "has already been linked to another vulnerability"
msgstr ""
@@ -24656,9 +25212,6 @@ msgstr[1] "birleÅŸtirme isteÄŸi"
msgid "merged %{time_ago}"
msgstr ""
-msgid "milestone should belong either to a project or a group."
-msgstr ""
-
msgid "missing"
msgstr ""
@@ -24995,6 +25548,9 @@ msgstr "yeni birleÅŸtirme isteÄŸi"
msgid "no contributions"
msgstr ""
+msgid "no expiration"
+msgstr ""
+
msgid "no one can merge"
msgstr ""
@@ -25047,6 +25603,9 @@ msgstr ""
msgid "pending removal"
msgstr ""
+msgid "per day"
+msgstr ""
+
msgid "pipeline"
msgstr ""
@@ -25075,6 +25634,12 @@ msgstr[1] "proje"
msgid "project avatar"
msgstr "proje profil resmi"
+msgid "projects"
+msgstr ""
+
+msgid "push to your repository, create pipelines, create issues or add comments. To reduce storage capacity, delete unused repositories, artifacts, wikis, issues, and pipelines."
+msgstr ""
+
msgid "quick actions"
msgstr ""
@@ -25221,11 +25786,6 @@ msgstr ""
msgid "this document"
msgstr "bu belge"
-msgid "thread resolved"
-msgid_plural "threads resolved"
-msgstr[0] ""
-msgstr[1] ""
-
msgid "to help your contributors communicate effectively!"
msgstr "katkıda bulunanlara yardım etmek için etkili şekilde iletişim kurun!"
@@ -25280,6 +25840,9 @@ msgstr "GitLab'da görüntüle"
msgid "view the blob"
msgstr ""
+msgid "vulnerability|Add a comment"
+msgstr ""
+
msgid "vulnerability|Add a comment or reason for dismissal"
msgstr ""
diff --git a/locale/uk/gitlab.po b/locale/uk/gitlab.po
index 65f23335450..e09df16fe38 100644
--- a/locale/uk/gitlab.po
+++ b/locale/uk/gitlab.po
@@ -12,10 +12,10 @@ msgstr ""
"X-Crowdin-Project: gitlab-ee\n"
"X-Crowdin-Language: uk\n"
"X-Crowdin-File: /master/locale/gitlab.pot\n"
-"PO-Revision-Date: 2020-04-15 01:23\n"
+"PO-Revision-Date: 2020-05-05 21:36\n"
msgid " %{start} to %{end}"
-msgstr ""
+msgstr "%{start} до %{end}"
msgid " (from %{timeoutSource})"
msgstr " (з %{timeoutSource})"
@@ -82,10 +82,10 @@ msgstr[3] ""
msgid "%d changed file"
msgid_plural "%d changed files"
-msgstr[0] ""
-msgstr[1] ""
-msgstr[2] ""
-msgstr[3] ""
+msgstr[0] "%d змінений файл"
+msgstr[1] "%d змінених файли"
+msgstr[2] "%d змінених файлів"
+msgstr[3] "%d змінених файлів"
msgid "%d child epic"
msgid_plural "%d child epics"
@@ -141,10 +141,10 @@ msgstr[3] "%d внеÑків"
msgid "%d error"
msgid_plural "%d errors"
-msgstr[0] ""
-msgstr[1] ""
-msgstr[2] ""
-msgstr[3] ""
+msgstr[0] "%d помилка"
+msgstr[1] "%d помилки"
+msgstr[2] "%d помилок"
+msgstr[3] "%d помилок"
msgid "%d exporter"
msgid_plural "%d exporters"
@@ -272,13 +272,20 @@ msgstr[1] "%d теги"
msgstr[2] "%d тегів"
msgstr[3] "%d тегів"
-msgid "%d vulnerability dismissed"
-msgid_plural "%d vulnerabilities dismissed"
+msgid "%d unresolved thread"
+msgid_plural "%d unresolved threads"
msgstr[0] ""
msgstr[1] ""
msgstr[2] ""
msgstr[3] ""
+msgid "%d vulnerability dismissed"
+msgid_plural "%d vulnerabilities dismissed"
+msgstr[0] "%d вразливіÑÑ‚ÑŒ відхилено"
+msgstr[1] "%d вразливоÑÑ‚Ñ– відхилено"
+msgstr[2] "%d вразливоÑтей відхилено"
+msgstr[3] "%d вразливоÑтей відхилено"
+
msgid "%s additional commit has been omitted to prevent performance issues."
msgid_plural "%s additional commits have been omitted to prevent performance issues."
msgstr[0] "%s доданий коміт був виключений Ð´Ð»Ñ Ð·Ð°Ð¿Ð¾Ð±Ñ–Ð³Ð°Ð½Ð½Ñ Ð¿Ñ€Ð¾Ð±Ð»ÐµÐ¼ із продуктивніÑÑ‚ÑŽ."
@@ -443,6 +450,9 @@ msgstr "%{mergeLength}/%{usersLength} можуть виконувати злит
msgid "%{mrText}, this issue will be closed automatically."
msgstr "%{mrText}, Ñ†Ñ Ð·Ð°Ð´Ð°Ñ‡Ð° буде закрита автоматично."
+msgid "%{namespace_name} is now read-only. You cannot: %{base_message}"
+msgstr ""
+
msgid "%{name} contained %{resultsString}"
msgstr "%{name} міÑтить %{resultsString}"
@@ -455,6 +465,13 @@ msgstr ""
msgid "%{name}'s avatar"
msgstr "Ðватар %{name}"
+msgid "%{no_of_days} day"
+msgid_plural "%{no_of_days} days"
+msgstr[0] ""
+msgstr[1] ""
+msgstr[2] ""
+msgstr[3] ""
+
msgid "%{number_commits_behind} commits behind %{default_branch}, %{number_commits_ahead} commits ahead"
msgstr "на %{number_commits_behind} комітів позаду %{default_branch}, на %{number_commits_ahead} комітів попереду"
@@ -477,7 +494,7 @@ msgid "%{percent}%{percentSymbol} complete"
msgstr "%{percent}%{percentSymbol} завершено"
msgid "%{placeholder} is not a valid color scheme"
-msgstr ""
+msgstr "%{placeholder} не Ñ” дійÑною кольоровою Ñхемою"
msgid "%{placeholder} is not a valid theme"
msgstr ""
@@ -549,7 +566,7 @@ msgid "%{strong_start}%{human_size}%{strong_end} Files"
msgstr "%{strong_start}%{human_size}%{strong_end} Файлів"
msgid "%{strong_start}%{human_size}%{strong_end} Storage"
-msgstr ""
+msgstr "%{strong_start}%{human_size}%{strong_end} Cховище"
msgid "%{strong_start}%{release_count}%{strong_end} Release"
msgid_plural "%{strong_start}%{release_count}%{strong_end} Releases"
@@ -575,7 +592,7 @@ msgid "%{tags} tags per image name"
msgstr "%{tags} теги на кожне Ñ–Ð¼â€™Ñ Ð¾Ð±Ñ€Ð°Ð·Ñƒ"
msgid "%{tag}-%{evidence}-%{filename}"
-msgstr ""
+msgstr "%{tag}-%{evidence}-%{filename}"
msgid "%{template_project_id} is unknown or invalid"
msgstr "%{template_project_id} невідомий або неправильний"
@@ -590,6 +607,9 @@ msgstr[3] "%{text} %{files} файлів"
msgid "%{text} is available"
msgstr "%{text} доÑтупний"
+msgid "%{timebox_name} should belong either to a project or a group."
+msgstr ""
+
msgid "%{title} %{operator} %{threshold}"
msgstr "%{title} %{operator} %{threshold}"
@@ -597,7 +617,7 @@ msgid "%{title} changes"
msgstr "%{title} зміни"
msgid "%{token}..."
-msgstr ""
+msgstr "%{token}..."
msgid "%{totalWeight} total weight"
msgstr "%{totalWeight} загальна вага"
@@ -670,7 +690,7 @@ msgid "(revoked)"
msgstr ""
msgid "*"
-msgstr ""
+msgstr "*"
msgid "+ %{amount} more"
msgstr "+ %{amount} більше"
@@ -695,7 +715,7 @@ msgid "+%{approvers} more approvers"
msgstr ""
msgid "+%{tags} more"
-msgstr ""
+msgstr "+%{tags} більше"
msgid ", or "
msgstr ", або "
@@ -800,7 +820,7 @@ msgstr[2] "%d хвилин"
msgstr[3] "%d хвилин"
msgid "1 month"
-msgstr ""
+msgstr "1 міÑÑць"
msgid "1 open issue"
msgid_plural "%{issues} open issues"
@@ -903,12 +923,18 @@ msgstr "<code>\"johnsmith@example.com\": \"johnsm...@example.com\"</code> дод
msgid "<code>\"johnsmith@example.com\": \"johnsmith@example.com\"</code> will add \"By <a href=\"#\">johnsmith@example.com</a>\" to all issues and comments originally created by johnsmith@example.com. By default, the email address or username is masked to ensure the user's privacy. Use this option if you want to show the full email address."
msgstr "<code>\"johnsmith@example.com\": \"johnsmith@example.com\"</code> додаÑÑ‚ÑŒ \"<a href=\"#\">johnsmith@example.com</a>\" до вÑÑ–Ñ… задач та коментарів, Ñкі були Ñтворені johnsmith@example.com. За замовчуваннÑм Ñ–Ð¼â€™Ñ ÐºÐ¾Ñ€Ð¸Ñтувача та його електронна адреÑа заблоковані Ð´Ð»Ñ Ð·Ð°Ð±ÐµÐ·Ð¿ÐµÑ‡ÐµÐ½Ð½Ñ ÐºÐ¾Ð½Ñ„Ñ–Ð´ÐµÐ½Ñ†Ñ–Ð¹Ð½Ð¾ÑÑ‚Ñ–. ВикориÑтовуйте цю опцію, Ñкщо ви хочете показувати електронну адреÑу повніÑÑ‚ÑŽ."
+msgid "<namespace / project>"
+msgstr ""
+
msgid "<no name set>"
msgstr "<Ñ–Ð¼â€™Ñ Ð½Ðµ задане>"
msgid "<no scopes selected>"
msgstr "<облаÑÑ‚ÑŒ дії не вибрано>"
+msgid "<project name>"
+msgstr ""
+
msgid "<strong>%{group_name}</strong> group members"
msgstr "<strong>%{group_name}</strong> кориÑтувачі групи"
@@ -942,6 +968,9 @@ msgstr "Обліковий Ð·Ð°Ð¿Ð¸Ñ Let's Encrypt буде налаштоваÐ
msgid "A basic page and serverless function that uses AWS Lambda, AWS API Gateway, and GitLab Pages"
msgstr "Базова Ñторінка та Serverless функціÑ, що викориÑтовує AWS Lambda, AWS API Gateway та GitLab Pages"
+msgid "A complete DevOps platform"
+msgstr ""
+
msgid "A default branch cannot be chosen for an empty project."
msgstr "Гілку за замовчуваннÑм не може бути обрано Ð´Ð»Ñ Ð¿Ð¾Ñ€Ð¾Ð¶Ð½ÑŒÐ¾Ð³Ð¾ проекту."
@@ -1158,15 +1187,24 @@ msgstr "Ваш токен Ñтатичних об’єктів викориÑÑ‚Ð
msgid "AccessTokens|reset it"
msgstr "перегенерувати його"
-msgid "AccessibilityReport|Learn More"
+msgid "AccessibilityReport|Accessibility report artifact not found"
msgstr ""
+msgid "AccessibilityReport|Failed to retrieve accessibility report"
+msgstr ""
+
+msgid "AccessibilityReport|Learn More"
+msgstr "ДізнатиÑÑ Ð±Ñ–Ð»ÑŒÑˆÐµ"
+
msgid "AccessibilityReport|Message: %{message}"
msgstr ""
msgid "AccessibilityReport|New"
msgstr ""
+msgid "AccessibilityReport|The accessibility scanning found an error of the following type: %{code}"
+msgstr ""
+
msgid "Account"
msgstr "Обліковий запиÑ"
@@ -1198,7 +1236,7 @@ msgid "Active Sessions"
msgstr "Ðктивні ÑеÑÑ–Ñ—"
msgid "Active Users:"
-msgstr ""
+msgstr "Ðктивні кориÑтувачі:"
msgid "Activity"
msgstr "ÐктивніÑÑ‚ÑŒ"
@@ -1241,7 +1279,7 @@ msgid "Add README"
msgstr "Додати інÑтрукцію (README)"
msgid "Add Variable"
-msgstr ""
+msgstr "Додати змінну"
msgid "Add Zoom meeting"
msgstr "Додати Zoom-зуÑтріч"
@@ -1295,7 +1333,7 @@ msgid "Add an issue"
msgstr "Додати задачу"
msgid "Add another link"
-msgstr ""
+msgstr "Додати ще одне поÑиланнÑ"
msgid "Add approval rule"
msgstr "Додати правило затвердженнÑ"
@@ -1310,13 +1348,13 @@ msgid "Add comment now"
msgstr "Додати коментар"
msgid "Add domain"
-msgstr ""
+msgstr "Додати домен"
msgid "Add email address"
msgstr "Додати адреÑу електронної пошти"
msgid "Add environment"
-msgstr ""
+msgstr "Додати Ñередовище"
msgid "Add header and footer to emails. Please note that color settings will only be applied within the application interface"
msgstr "Додати заголовок Ñ– футер в електронні лиÑти. Будь лаÑка, зверніть увагу, що Ð½Ð°Ð»Ð°ÑˆÑ‚ÑƒÐ²Ð°Ð½Ð½Ñ ÐºÐ¾Ð»ÑŒÐ¾Ñ€Ñƒ будуть викориÑтовуватиÑÑ Ñ‚Ñ–Ð»ÑŒÐºÐ¸ в інтерфейÑÑ– заÑтоÑунку"
@@ -1382,7 +1420,7 @@ msgid "Add users to group"
msgstr "Додати кориÑтувача до групи"
msgid "Add variable"
-msgstr ""
+msgstr "Додати змінну"
msgid "Add webhook"
msgstr "Додати вебхук"
@@ -1411,6 +1449,9 @@ msgstr "Додано задачу до епіку."
msgid "Added at"
msgstr "Додано"
+msgid "Added for this merge request"
+msgstr ""
+
msgid "Added in this version"
msgstr "Додано в цій верÑÑ–Ñ—"
@@ -1466,34 +1507,34 @@ msgid "Admin notes"
msgstr "Ðотатки адмініÑтратора"
msgid "AdminArea|Active users"
-msgstr ""
+msgstr "Ðктивні кориÑтувачі"
msgid "AdminArea|Billable users"
msgstr ""
msgid "AdminArea|Blocked users"
-msgstr ""
+msgstr "Заблоковані кориÑтувачі"
msgid "AdminArea|Bots"
-msgstr ""
+msgstr "Боти"
msgid "AdminArea|Developer"
-msgstr ""
+msgstr "Розробник"
msgid "AdminArea|Guest"
-msgstr ""
+msgstr "ГіÑÑ‚ÑŒ"
msgid "AdminArea|Included Free in license"
msgstr ""
msgid "AdminArea|Maintainer"
-msgstr ""
+msgstr "Керівник"
msgid "AdminArea|Owner"
-msgstr ""
+msgstr "ВлаÑник"
msgid "AdminArea|Reporter"
-msgstr ""
+msgstr "Репортер"
msgid "AdminArea|Stop all jobs"
msgstr "Зупинити вÑÑ– завданнÑ"
@@ -1508,16 +1549,16 @@ msgid "AdminArea|Stopping jobs failed"
msgstr "Зупинка завдань пройшла невдало"
msgid "AdminArea|Total users"
-msgstr ""
+msgstr "Загальна кількіÑÑ‚ÑŒ кориÑтувачів"
msgid "AdminArea|Users statistics"
-msgstr ""
+msgstr "СтатиÑтика кориÑтувачів"
msgid "AdminArea|Users with highest role"
-msgstr ""
+msgstr "КориÑтувачі з найвищою роллю"
msgid "AdminArea|Users without a Group and Project"
-msgstr ""
+msgstr "КориÑтувачі без групи та проєкту"
msgid "AdminArea|You’re about to stop all jobs.This will halt all current jobs that are running."
msgstr "Зараз ви зупинете вÑÑ– завданнÑ. Це обірве уÑÑ– запущені завданнÑ."
@@ -1556,7 +1597,7 @@ msgid "AdminSettings|Environment variables are protected by default"
msgstr "Змінні Ñередовища Ñ” захищеними за замовчуваннÑм"
msgid "AdminSettings|Go to General Settings"
-msgstr ""
+msgstr "Перейти до загальних налаштувань"
msgid "AdminSettings|Integrations configured here will automatically apply to all projects on this instance."
msgstr ""
@@ -1694,10 +1735,10 @@ msgid "AdminUsers|Owned groups will be left"
msgstr "Групи, що Ñ” у влаÑноÑÑ‚Ñ– буде збережено"
msgid "AdminUsers|Personal projects will be left"
-msgstr "ПерÑональні проекти буде збережено"
+msgstr ""
msgid "AdminUsers|Personal projects, group and user history will be left intact"
-msgstr "ПерÑональні проекти, групи, та Ñ–Ñторію кориÑтувача буде залишено без змін"
+msgstr ""
msgid "AdminUsers|Reactivating a user will:"
msgstr "Повторна Ð°ÐºÑ‚Ð¸Ð²Ð°Ñ†Ñ–Ñ ÐºÐ¾Ñ€Ð¸Ñтувача зробить:"
@@ -1757,13 +1798,13 @@ msgid "AdminUsers|You are about to permanently delete the user %{username}. This
msgstr "Ви збираєтеÑÑ Ð¾Ñтаточно видалити кориÑтувача %{username}. Ð’ÑÑ– пов'Ñзані з ним задачі, запити на Ð·Ð»Ð¸Ñ‚Ñ‚Ñ Ñ– групи будуть видалені. Щоб уникнути втрати даних, розглÑньте можливіÑÑ‚ÑŒ %{strong_start}Ð±Ð»Ð¾ÐºÑƒÐ²Ð°Ð½Ð½Ñ ÐºÐ¾Ñ€Ð¸Ñтувача%{strong_end} заміÑÑ‚ÑŒ видаленнÑ. ПіÑÐ»Ñ %{strong_start}Ð²Ð¸Ð´Ð°Ð»ÐµÐ½Ð½Ñ ÐºÐ¾Ñ€Ð¸Ñтувача%{strong_end}, його неможливо буде відновити."
msgid "Administration"
-msgstr ""
+msgstr "ÐдмініÑтруваннÑ"
msgid "Advanced"
msgstr "Розширений"
msgid "Advanced Settings"
-msgstr ""
+msgstr "Додаткові налаштуваннÑ"
msgid "Advanced permissions, Large File Storage and Two-Factor authentication settings."
msgstr "Додаткові дозволи, Ñховище великих файлів (LFS) Ñ– Ð½Ð°Ð»Ð°ÑˆÑ‚ÑƒÐ²Ð°Ð½Ð½Ñ Ð´Ð²Ð¾Ñ„Ð°ÐºÑ‚Ð¾Ñ€Ð½Ð¾Ñ— автентифікації."
@@ -1784,6 +1825,72 @@ msgstr[1] "ПопередженнÑ"
msgstr[2] "Попереджень"
msgstr[3] "Попереджень"
+msgid "Alert Details"
+msgstr ""
+
+msgid "AlertManagement|Acknowledged"
+msgstr ""
+
+msgid "AlertManagement|Alert"
+msgstr ""
+
+msgid "AlertManagement|Authorize external service"
+msgstr ""
+
+msgid "AlertManagement|Display alerts from all your monitoring tools directly within GitLab. Streamline the investigation of your alerts and the escalation of alerts to incidents."
+msgstr ""
+
+msgid "AlertManagement|End time"
+msgstr ""
+
+msgid "AlertManagement|End time:"
+msgstr ""
+
+msgid "AlertManagement|Events"
+msgstr ""
+
+msgid "AlertManagement|Events:"
+msgstr ""
+
+msgid "AlertManagement|Full Alert Details"
+msgstr ""
+
+msgid "AlertManagement|More information"
+msgstr ""
+
+msgid "AlertManagement|No alerts available to display. If you think you're seeing this message in error, refresh the page."
+msgstr ""
+
+msgid "AlertManagement|No alerts to display."
+msgstr ""
+
+msgid "AlertManagement|Overview"
+msgstr ""
+
+msgid "AlertManagement|Resolved"
+msgstr ""
+
+msgid "AlertManagement|Severity"
+msgstr ""
+
+msgid "AlertManagement|Start time"
+msgstr ""
+
+msgid "AlertManagement|Start time:"
+msgstr ""
+
+msgid "AlertManagement|Status"
+msgstr ""
+
+msgid "AlertManagement|Surface alerts in GitLab"
+msgstr ""
+
+msgid "AlertManagement|There was an error displaying the alerts. Confirm your endpoint's configuration details to ensure alerts appear."
+msgstr ""
+
+msgid "AlertManagement|Triggered"
+msgstr ""
+
msgid "AlertService|%{linkStart}Learn more%{linkEnd} about configuring this endpoint to receive alerts."
msgstr "%{linkStart}ДізнатиÑÑ Ð±Ñ–Ð»ÑŒÑˆÐµ%{linkEnd} про Ð½Ð°Ð»Ð°ÑˆÑ‚ÑƒÐ²Ð°Ð½Ð½Ñ Ñ†Ñ–Ñ”Ñ— кінцевої точки Ð´Ð»Ñ Ð¾Ñ‚Ñ€Ð¸Ð¼Ð°Ð½Ð½Ñ Ð¿Ð¾Ð¿ÐµÑ€ÐµÐ´Ð¶ÐµÐ½ÑŒ."
@@ -1806,7 +1913,7 @@ msgid "All %{replicableType} are being scheduled for %{action}"
msgstr ""
msgid "All (default)"
-msgstr ""
+msgstr "Ð’ÑÑ– (за замовчуваннÑм)"
msgid "All Members"
msgstr "Ð’ÑÑ– учаÑники"
@@ -1824,13 +1931,13 @@ msgid "All email addresses will be used to identify your commits."
msgstr "Ð’ÑÑ– адреÑи електронної пошти будуть викориÑтовуватиÑÑ Ð´Ð»Ñ Ñ–Ð´ÐµÐ½Ñ‚Ð¸Ñ„Ñ–ÐºÐ°Ñ†Ñ–Ñ— ваших комітів."
msgid "All environments"
-msgstr ""
+msgstr "Ð’ÑÑ– Ñередовища"
msgid "All features are enabled for blank projects, from templates, or when importing, but you can disable them afterward in the project settings."
msgstr "Ð’ÑÑ– функції Ð´Ð»Ñ Ð½Ð¾Ð²Ð¸Ñ… проектів берутьÑÑ Ñ–Ð· шаблонів або під Ñ‡Ð°Ñ Ñ–Ð¼Ð¿Ð¾Ñ€Ñ‚ÑƒÐ²Ð°Ð½Ð½Ñ, але ви можете вимикати Ñ—Ñ… пізніше в налаштуваннÑÑ… проекту."
msgid "All groups and projects"
-msgstr "Ð’ÑÑ– групи та проекти"
+msgstr ""
msgid "All issues for this milestone are closed."
msgstr "Ð’ÑÑ– задачі в цьому етапі закриті."
@@ -1848,11 +1955,14 @@ msgid "All paths are relative to the GitLab URL. Do not include %{relative_url_l
msgstr "Ð’ÑÑ– шлÑхи Ñ” відноÑними до URL-адреÑи GitLab. Ðе включайте %{relative_url_link_start}відноÑну URL-адреÑу%{relative_url_link_end}."
msgid "All projects"
-msgstr "Ð’ÑÑ– проекти"
+msgstr ""
msgid "All security scans are enabled because %{linkStart}Auto DevOps%{linkEnd} is enabled on this project"
msgstr "Ð’ÑÑ– ÑÐºÐ°Ð½ÑƒÐ²Ð°Ð½Ð½Ñ Ð±ÐµÐ·Ð¿ÐµÐºÐ¸ увімкнені, тому що %{linkStart}Auto DevOps%{linkEnd} увімкнено в цьому проєкті"
+msgid "All threads resolved"
+msgstr ""
+
msgid "All users"
msgstr "Ð’ÑÑ– кориÑтувачі"
@@ -1871,6 +1981,9 @@ msgstr "Дозволити влаÑникам груп керувати нала
msgid "Allow only the selected protocols to be used for Git access."
msgstr "Дозволити викориÑÑ‚Ð°Ð½Ð½Ñ Ñ‚Ñ–Ð»ÑŒÐºÐ¸ вибраних протоколів Ð´Ð»Ñ Ð´Ð¾Ñтупу до Git."
+msgid "Allow owners to manage default branch protection per group"
+msgstr ""
+
msgid "Allow owners to manually add users outside of LDAP"
msgstr ""
@@ -1979,6 +2092,9 @@ msgstr "Помилка при отриманні затверджуючих оÑ
msgid "An error occurred fetching the dropdown data."
msgstr "Помилка при отриманні даних Ð´Ð»Ñ Ð²Ð¸Ð¿Ð°Ð´Ð°ÑŽÑ‡Ð¾Ð³Ð¾ ÑпиÑку."
+msgid "An error occurred fetching the project authors."
+msgstr ""
+
msgid "An error occurred previewing the blob"
msgstr "СталаÑÑ Ð¿Ð¾Ð¼Ð¸Ð»ÐºÐ° під Ñ‡Ð°Ñ Ð¿Ð¾Ð¿ÐµÑ€ÐµÐ´Ð½ÑŒÐ¾Ð³Ð¾ переглÑду об'єкта"
@@ -2060,6 +2176,9 @@ msgstr "Помилка при отриманні Ð°Ð²Ñ‚Ð¾Ð´Ð¾Ð¿Ð¾Ð²Ð½ÐµÐ½Ð½Ñ Ð¿
msgid "An error occurred while fetching sidebar data"
msgstr "Виникла помилка під Ñ‡Ð°Ñ Ð·Ð°Ð²Ð°Ð½Ñ‚Ð°Ð¶ÐµÐ½Ð½Ñ Ð´Ð°Ð½Ð¸Ñ… Ð´Ð»Ñ Ð±Ñ–Ñ‡Ð½Ð¾Ñ— панелі"
+msgid "An error occurred while fetching terraform reports."
+msgstr ""
+
msgid "An error occurred while fetching the Service Desk address."
msgstr "СталаÑÑ Ð¿Ð¾Ð¼Ð¸Ð»ÐºÐ° під Ñ‡Ð°Ñ Ð¾Ñ‚Ñ€Ð¸Ð¼Ð°Ð½Ð½Ñ Ð°Ð´Ñ€ÐµÑи Служби підтримки."
@@ -2274,10 +2393,10 @@ msgid "Analyze a review version of your web application."
msgstr "Проаналізуйте верÑÑ–ÑŽ Ð´Ð»Ñ Ð¿Ð¾Ð¿ÐµÑ€ÐµÐ´Ð½ÑŒÐ¾Ð³Ð¾ оглÑду вашого веб-заÑтоÑунка."
msgid "Analyze your dependencies for known vulnerabilities."
-msgstr ""
+msgstr "Проаналізуйте ваші залежноÑÑ‚Ñ– на предмет відомих вразливоÑтей."
msgid "Analyze your source code for known vulnerabilities."
-msgstr ""
+msgstr "Проаналізуйте ваш вихідний код на предмет відомих вразливоÑтей."
msgid "Ancestors"
msgstr "Предки"
@@ -2297,6 +2416,9 @@ msgstr "Перевірка проти Ñпаму"
msgid "Any"
msgstr "Будь-Ñкий"
+msgid "Any Author"
+msgstr ""
+
msgid "Any Label"
msgstr "Будь-Ñка мітка"
@@ -2304,7 +2426,7 @@ msgid "Any Milestone"
msgstr "Будь-Ñкий етап"
msgid "Any branch"
-msgstr ""
+msgstr "Будь-Ñка гілка"
msgid "Any eligible user"
msgstr "Будь-Ñкий кориÑтувач з необхідним доÑтупом"
@@ -2470,7 +2592,7 @@ msgid "Approve the current merge request."
msgstr "Затвердити поточний запит на злиттÑ."
msgid "Approved by: "
-msgstr ""
+msgstr "Затверджено кориÑтувачем: "
msgid "Approved the current merge request."
msgstr "Затверджено поточний запит на злиттÑ."
@@ -2488,7 +2610,7 @@ msgid "April"
msgstr "квітень"
msgid "Archive"
-msgstr ""
+msgstr "Ðрхів"
msgid "Archive jobs"
msgstr "Ðрхівувати завданнÑ"
@@ -2497,7 +2619,7 @@ msgid "Archive project"
msgstr "Ðрхівувати проект"
msgid "Archived"
-msgstr ""
+msgstr "Заархівовано"
msgid "Archived project! Repository and other project resources are read only"
msgstr "Ðрхівований проєкт! Репозиторій та інші реÑурÑи проєкту доÑтупні лише Ð´Ð»Ñ Ñ‡Ð¸Ñ‚Ð°Ð½Ð½Ñ"
@@ -2506,10 +2628,10 @@ msgid "Archived project! Repository and other project resources are read-only"
msgstr "Ðрхівований проект! Репозиторій та інші реÑурÑи проекту доÑтупні лише Ð´Ð»Ñ Ñ‡Ð¸Ñ‚Ð°Ð½Ð½Ñ"
msgid "Archived projects"
-msgstr "Заархівовані проекти"
+msgstr ""
msgid "Archiving the project will make it entirely read only. It is hidden from the dashboard and doesn't show up in searches. %{strong_start}The repository cannot be committed to, and no issues, comments, or other entities can be created.%{strong_end}"
-msgstr ""
+msgstr "ÐÑ€Ñ…Ñ–Ð²ÑƒÐ²Ð°Ð½Ð½Ñ Ð¿Ñ€Ð¾Ñ”ÐºÑ‚Ñƒ зробить його доÑтупним лише Ð´Ð»Ñ Ñ‡Ð¸Ñ‚Ð°Ð½Ð½Ñ. Він буде прихований з панелі ÐºÐµÑ€ÑƒÐ²Ð°Ð½Ð½Ñ Ñ– не відображатиметьÑÑ Ð² пошуках. %{strong_start}Ð’ репозиторій не можна буде комітити, Ñ– не можливо буде Ñтворити жодних задач, коментарів чи інших об'єктів.%{strong_end}"
msgid "Are you setting up GitLab for a company?"
msgstr "Ви налаштовуєте GitLab Ð´Ð»Ñ ÐºÐ¾Ð¼Ð¿Ð°Ð½Ñ–Ñ—?"
@@ -2520,12 +2642,12 @@ msgstr "Ви впевнені, що хочете заархівувати цей
msgid "Are you sure that you want to unarchive this project?"
msgstr "Ви впевнені, що хочете розархівувати цей проект?"
-msgid "Are you sure you want to cancel creating this comment?"
-msgstr "Ви впевнені, що хочете ÑкаÑувати цей коментар?"
-
msgid "Are you sure you want to cancel editing this comment?"
msgstr "Ви впевнені, що хочете ÑкаÑувати Ñ€ÐµÐ´Ð°Ð³ÑƒÐ²Ð°Ð½Ð½Ñ Ñ†ÑŒÐ¾Ð³Ð¾ коментарÑ?"
+msgid "Are you sure you want to close this blocked issue?"
+msgstr ""
+
msgid "Are you sure you want to delete %{name}?"
msgstr ""
@@ -2553,6 +2675,9 @@ msgstr ""
msgid "Are you sure you want to deploy this environment?"
msgstr ""
+msgid "Are you sure you want to discard this comment?"
+msgstr ""
+
msgid "Are you sure you want to erase this build?"
msgstr "Ви впевнені, що хочете видалити цей білд?"
@@ -2731,6 +2856,9 @@ msgstr "Призначає %{assignee_users_sentence}."
msgid "At least one approval from a code owner is required to change files matching the respective CODEOWNER rules."
msgstr "Потрібне щонайменше одне Ð·Ð°Ñ‚Ð²ÐµÑ€Ð´Ð¶ÐµÐ½Ð½Ñ Ð²Ñ–Ð´ влаÑника коду Ð´Ð»Ñ Ð·Ð¼Ñ–Ð½Ð¸ файлів, що задовільнÑÑŽÑ‚ÑŒ відповідним правилам CODEOWNER."
+msgid "At least one logging option is required to be enabled"
+msgstr ""
+
msgid "At least one of group_id or project_id must be specified"
msgstr "Треба зазначити принаймні group_id або project_id"
@@ -2771,6 +2899,27 @@ msgstr ""
msgid "AuditEvents|Target"
msgstr "Ціль"
+msgid "AuditLogs|(removed)"
+msgstr ""
+
+msgid "AuditLogs|Action"
+msgstr ""
+
+msgid "AuditLogs|Author"
+msgstr ""
+
+msgid "AuditLogs|Date"
+msgstr ""
+
+msgid "AuditLogs|IP Address"
+msgstr ""
+
+msgid "AuditLogs|Object"
+msgstr ""
+
+msgid "AuditLogs|Target"
+msgstr ""
+
msgid "Aug"
msgstr "Ñерп."
@@ -3053,9 +3202,6 @@ msgstr "Коренева URL-адреÑа Bamboo, наприклад https://bam
msgid "BambooService|You must set up automatic revision labeling and a repository trigger in Bamboo."
msgstr "Ви повинні налаштувати автоматичне вÑÑ‚Ð°Ð½Ð¾Ð²Ð»ÐµÐ½Ð½Ñ Ð¼Ñ–Ñ‚Ð¾Ðº на ревізії, а також тригер репозиторію в Bamboo."
-msgid "Batch operations"
-msgstr "Пакетні операції"
-
msgid "BatchComments|Delete all pending comments"
msgstr "Видалити вÑÑ– коментарі в очікуванні"
@@ -3150,7 +3296,7 @@ msgid "Blocked"
msgstr "Заблокований"
msgid "Blocked issue"
-msgstr ""
+msgstr "Заблокована задача"
msgid "Blocks"
msgstr ""
@@ -3185,6 +3331,9 @@ msgstr "Початок роботи з типовим набором ÑпиÑкÑ
msgid "Boards"
msgstr "Дошки"
+msgid "Boards and Board Lists"
+msgstr ""
+
msgid "Boards|Collapse"
msgstr "Згорнути"
@@ -3404,6 +3553,9 @@ msgstr "Купити ЕЕ"
msgid "Buy GitLab Enterprise Edition"
msgstr "Купити GitLab Enterprise Edition"
+msgid "Buy more Pipeline minutes"
+msgstr ""
+
msgid "By %{user_name}"
msgstr "Від %{user_name}"
@@ -3411,7 +3563,7 @@ msgid "By default GitLab sends emails in HTML and plain text formats so mail cli
msgstr "За замовчуваннÑм GitLab відправлÑÑ” електронні лиÑти в форматі HTML та у звичайному текÑтовому форматі, щоб поштові клієнти могли вибирати Ñкий з форматів викориÑтовувати. Вимкніть це налаштуваннÑ, Ñкщо ви хочете відправлÑти лиÑти лише у звичайному текÑтовому форматі."
msgid "By default, all projects and groups will use the global notifications setting."
-msgstr "За замовчуваннÑм уÑÑ– проекти та групи будуть викориÑтовувати глобальні Ð½Ð°Ð»Ð°ÑˆÑ‚ÑƒÐ²Ð°Ð½Ð½Ñ Ñповіщень."
+msgstr ""
msgid "ByAuthor|by"
msgstr "від"
@@ -3524,6 +3676,9 @@ msgstr "Ðеможливо видалити учаÑників групи без
msgid "Can't scan the code?"
msgstr "Ðеможливо Ñканувати код?"
+msgid "Can't update snippet: %{err}"
+msgstr ""
+
msgid "Canary"
msgstr "Canary"
@@ -3692,6 +3847,9 @@ msgstr "Зміна тегу релізу підтримуєтьÑÑ Ð»Ð¸ÑˆÐµ чÐ
msgid "Changing group path can have unintended side effects."
msgstr "Зміна шлÑху групи може мати небажані побічні ефекти."
+msgid "Charts"
+msgstr ""
+
msgid "Charts can't be displayed as the request for data has timed out. %{documentationLink}"
msgstr "Графіки не можуть відображатиÑÑ, оÑкільки вичерпано Ñ‡Ð°Ñ Ð·Ð°Ð¿Ð¸Ñ‚Ñƒ даних. %{documentationLink}"
@@ -3728,9 +3886,6 @@ msgstr "Тег"
msgid "ChatMessage|and [%{count} more](%{pipeline_failed_jobs_url})"
msgstr "і [%{count} більше](%{pipeline_failed_jobs_url})"
-msgid "ChatMessage|failed"
-msgstr "провалено"
-
msgid "ChatMessage|has failed"
msgstr "провалено"
@@ -3746,9 +3901,6 @@ msgstr "за %{duration}"
msgid "ChatMessage|in %{project_link}"
msgstr "в %{project_link}"
-msgid "ChatMessage|passed"
-msgstr "уÑпішно пройдено"
-
msgid "Check again"
msgstr "Перевірити знову"
@@ -3762,7 +3914,7 @@ msgid "Check your .gitlab-ci.yml"
msgstr "Перевірте Ñвій .gitlab-ci.yml"
msgid "Check your Docker images for known vulnerabilities."
-msgstr ""
+msgstr "Перевірте Ñвої образи Docker на наÑвніÑÑ‚ÑŒ відомих вразливоÑтей."
msgid "Checking %{text} availability…"
msgstr "Перевірка доÑтупноÑÑ‚Ñ– %{text}…"
@@ -3816,13 +3968,13 @@ msgid "Checkout|Checkout"
msgstr "ÐžÑ„Ð¾Ñ€Ð¼Ð»ÐµÐ½Ð½Ñ Ð·Ð°Ð¼Ð¾Ð²Ð»ÐµÐ½Ð½Ñ"
msgid "Checkout|City"
-msgstr ""
+msgstr "МіÑто"
msgid "Checkout|Confirm purchase"
msgstr ""
msgid "Checkout|Confirming..."
-msgstr ""
+msgstr "ПідтвердженнÑ..."
msgid "Checkout|Continue to billing"
msgstr "Перейти до оплати"
@@ -3831,10 +3983,10 @@ msgid "Checkout|Continue to payment"
msgstr ""
msgid "Checkout|Country"
-msgstr ""
+msgstr "Країна"
msgid "Checkout|Create a new group"
-msgstr ""
+msgstr "Створити нову групу"
msgid "Checkout|Credit card form failed to load. Please try again."
msgstr ""
@@ -3864,7 +4016,7 @@ msgid "Checkout|Failed to register credit card. Please try again."
msgstr ""
msgid "Checkout|GitLab group"
-msgstr ""
+msgstr "Група GitLab"
msgid "Checkout|GitLab plan"
msgstr "GitLab план"
@@ -3882,7 +4034,7 @@ msgid "Checkout|Number of users"
msgstr "КількіÑÑ‚ÑŒ кориÑтувачів"
msgid "Checkout|Payment method"
-msgstr ""
+msgstr "СпоÑіб оплати"
msgid "Checkout|Please select a country"
msgstr ""
@@ -3891,7 +4043,7 @@ msgid "Checkout|Please select a state"
msgstr ""
msgid "Checkout|Select"
-msgstr ""
+msgstr "Вибрати"
msgid "Checkout|State"
msgstr ""
@@ -3927,7 +4079,7 @@ msgid "Checkout|Your subscription will be applied to this group"
msgstr ""
msgid "Checkout|Zip code"
-msgstr ""
+msgstr "Поштовий індекÑ"
msgid "Checkout|company or team"
msgstr "ÐºÐ¾Ð¼Ð¿Ð°Ð½Ñ–Ñ Ð°Ð±Ð¾ команда"
@@ -3939,7 +4091,7 @@ msgid "Cherry-pick this merge request"
msgstr "Вибрати (cherry-pick) цей запит на злиттÑ"
msgid "Child"
-msgstr ""
+msgstr "Дочірний"
msgid "Child epic does not exist."
msgstr "Дочірній епік не Ñ–Ñнує."
@@ -4083,7 +4235,7 @@ msgid "CiVariables|Cannot use Masked Variable with current value"
msgstr "Ðеможливо викориÑтовувати приховану змінну із поточним значеннÑм"
msgid "CiVariables|Environments"
-msgstr ""
+msgstr "Середовища"
msgid "CiVariables|Input variable key"
msgstr "Ключ вхідної змінної"
@@ -4098,7 +4250,7 @@ msgid "CiVariables|Masked"
msgstr "Приховано"
msgid "CiVariables|Protected"
-msgstr ""
+msgstr "Захищений"
msgid "CiVariables|Remove variable row"
msgstr "Видалити Ñ€Ñдок змінних"
@@ -4158,7 +4310,7 @@ msgid "Class"
msgstr "КлаÑ"
msgid "Class:"
-msgstr ""
+msgstr "КлаÑ:"
msgid "Classification Label (optional)"
msgstr "Мітка клаÑифікації (необов'Ñзково)"
@@ -4170,7 +4322,7 @@ msgid "Clear"
msgstr "ОчиÑтити"
msgid "Clear chart filters"
-msgstr ""
+msgstr "ОчиÑтити фільтри графіків"
msgid "Clear input"
msgstr "ОчиÑтити ввід"
@@ -4199,9 +4351,6 @@ msgstr "Очищає вагу."
msgid "Click any <strong>project name</strong> in the project list below to navigate to the project milestone."
msgstr "Клікніть по будь-Ñкому <strong>імені проекту</strong> зі ÑпиÑку нижче Ð´Ð»Ñ Ñ‚Ð¾Ð³Ð¾, щоб перейти до етапу проекту."
-msgid "Click here"
-msgstr "ÐатиÑніть тут"
-
msgid "Click the <strong>Download</strong> button and wait for downloading to complete."
msgstr "ÐатиÑніть кнопку <strong>ЗавантаженнÑ</strong> Ñ– зачекайте поки Ð·Ð°Ð²Ð°Ð½Ñ‚Ð°Ð¶ÐµÐ½Ð½Ñ Ð½Ðµ завершитьÑÑ."
@@ -4287,7 +4436,7 @@ msgid "Closes this %{quick_action_target}."
msgstr "Закриває %{quick_action_target}."
msgid "Cluster"
-msgstr ""
+msgstr "КлаÑтер"
msgid "Cluster Health"
msgstr "Стан клаÑтера"
@@ -4299,7 +4448,7 @@ msgid "Cluster does not exist"
msgstr ""
msgid "Cluster level"
-msgstr ""
+msgstr "Рівень клаÑтера"
msgid "ClusterIntegration| %{custom_domain_start}More information%{custom_domain_end}."
msgstr "%{custom_domain_start}Детальніше%{custom_domain_end}."
@@ -4317,7 +4466,7 @@ msgid "ClusterIntegration|%{appList} was successfully installed on your Kubernet
msgstr "%{appList} були уÑпішно вÑтановлені на ваш Kubernetes-клаÑтер"
msgid "ClusterIntegration|%{external_ip}.nip.io"
-msgstr ""
+msgstr "%{external_ip}.nip.io"
msgid "ClusterIntegration|%{title} uninstalled successfully."
msgstr "%{title} уÑпішно видалено."
@@ -4434,7 +4583,7 @@ msgid "ClusterIntegration|Choose which of your environments will use this cluste
msgstr "Виберіть, Ñке із ваших Ñередовищ буде викориÑтовувати цей клаÑтер."
msgid "ClusterIntegration|Clear cluster cache"
-msgstr ""
+msgstr "ОчиÑтити кеш клаÑтера"
msgid "ClusterIntegration|Clear the local cache of namespace and service accounts. This is necessary if your integration has become out of sync. The cache is repopulated during the next CI job that requires namespace and service accounts."
msgstr ""
@@ -4551,7 +4700,7 @@ msgid "ClusterIntegration|Enable this setting if using role-based access control
msgstr "Увімкніть цей параметр, Ñкщо викориÑтовуєтьÑÑ Ñ€Ð¾Ð»ÑŒÐ¾Ð²Ðµ ÐºÐµÑ€ÑƒÐ²Ð°Ð½Ð½Ñ Ð´Ð¾Ñтупом (RBAC)."
msgid "ClusterIntegration|Enabled stack"
-msgstr ""
+msgstr "Увімкнений Ñтек"
msgid "ClusterIntegration|Enter the details for your Amazon EKS Kubernetes cluster"
msgstr "Введіть параметри вашого Amazon EKS Kubernetes-клаÑтера"
@@ -4589,6 +4738,12 @@ msgstr "ÐžÑ‚Ñ€Ð¸Ð¼Ð°Ð½Ð½Ñ Ð¿Ñ€Ð¾ÐµÐºÑ‚Ñ–Ð²"
msgid "ClusterIntegration|Fetching zones"
msgstr "ÐžÑ‚Ñ€Ð¸Ð¼Ð°Ð½Ð½Ñ Ð·Ð¾Ð½"
+msgid "ClusterIntegration|Fluentd"
+msgstr ""
+
+msgid "ClusterIntegration|Fluentd is an open source data collector, which lets you unify the data collection and consumption for a better use and understanding of data. It requires at least one of the following logs to be successfully installed."
+msgstr ""
+
msgid "ClusterIntegration|GitLab Integration"
msgstr "Ð†Ð½Ñ‚ÐµÐ³Ñ€Ð°Ñ†Ñ–Ñ Ð· GitLab"
@@ -4602,7 +4757,7 @@ msgid "ClusterIntegration|GitLab-managed cluster"
msgstr "КлаÑтер, що керуєтьÑÑ GitLab"
msgid "ClusterIntegration|Gitlab Integration"
-msgstr ""
+msgstr "Ð†Ð½Ñ‚ÐµÐ³Ñ€Ð°Ñ†Ñ–Ñ Ð· Gitlab"
msgid "ClusterIntegration|Global default"
msgstr ""
@@ -4755,7 +4910,7 @@ msgid "ClusterIntegration|Loading instance types"
msgstr "Ð—Ð°Ð²Ð°Ð½Ñ‚Ð°Ð¶ÐµÐ½Ð½Ñ Ñ‚Ð¸Ð¿Ñ–Ð² інÑтанÑів"
msgid "ClusterIntegration|Loading networks"
-msgstr ""
+msgstr "Ð—Ð°Ð²Ð°Ð½Ñ‚Ð°Ð¶ÐµÐ½Ð½Ñ Ð¼ÐµÑ€ÐµÐ¶"
msgid "ClusterIntegration|Loading security groups"
msgstr "Ð—Ð°Ð²Ð°Ð½Ñ‚Ð°Ð¶ÐµÐ½Ð½Ñ Ð³Ñ€ÑƒÐ¿ безпеки"
@@ -4895,6 +5050,15 @@ msgstr "Запит про початок вÑÑ‚Ð°Ð½Ð¾Ð²Ð»ÐµÐ½Ð½Ñ Ð½Ðµ викоÐ
msgid "ClusterIntegration|Request to begin uninstalling failed"
msgstr "Запит про початок Ð²Ð¸Ð´Ð°Ð»ÐµÐ½Ð½Ñ Ð½Ðµ виконано"
+msgid "ClusterIntegration|SIEM Hostname"
+msgstr ""
+
+msgid "ClusterIntegration|SIEM Port"
+msgstr ""
+
+msgid "ClusterIntegration|SIEM Protocol"
+msgstr ""
+
msgid "ClusterIntegration|Save changes"
msgstr "Зберегти зміни"
@@ -4991,6 +5155,12 @@ msgstr "Вибрати зону"
msgid "ClusterIntegration|Select zone to choose machine type"
msgstr "Виберіть зону, щоб вибрати тип машин"
+msgid "ClusterIntegration|Send Cilium Logs"
+msgstr ""
+
+msgid "ClusterIntegration|Send ModSecurity Logs"
+msgstr ""
+
msgid "ClusterIntegration|Service Token"
msgstr "Токен СервіÑа"
@@ -5106,7 +5276,7 @@ msgid "ClusterIntegration|We could not verify that one of your projects on GCP h
msgstr "Ми не змогли перевірити, що один із ваших проектів в GCP має ввімкнений білінг. Будь лаÑка, Ñпробуйте ще раз."
msgid "ClusterIntegration|With a Kubernetes cluster associated to this project, you can use review apps, deploy your applications, run your pipelines, and much more in an easy way."
-msgstr "За допомогою підключеного до цього проекту Kubernetes-клаÑтера, ви можете викориÑтовувати Review Apps, розгортати ваші проекти, запуÑкати конвеєри збірки тощо."
+msgstr ""
msgid "ClusterIntegration|You are about to remove your cluster integration and all GitLab-created resources associated with this cluster."
msgstr "Ви збираєтеÑÑ Ð²Ð¸Ð´Ð°Ð»Ð¸Ñ‚Ð¸ інтеграцію вашого клаÑтера та вÑÑ– реÑурÑи пов'Ñзані з цим клаÑтером, Ñкі були Ñтворені GitLab."
@@ -5136,7 +5306,7 @@ msgid "ClusterIntegration|Your cluster API is unreachable. Please ensure your AP
msgstr "API вашого клаÑтера недоÑтупний. ПереконайтеÑÑ, що ваш API URL правильний."
msgid "ClusterIntegration|Your service role is distinct from the provision role used when authenticating. It will allow Amazon EKS and the Kubernetes control plane to manage AWS resources on your behalf. To use a new role, first create one on %{startLink}Amazon Web Services %{externalLinkIcon} %{endLink}."
-msgstr ""
+msgstr "Ваша Ñлужбова роль відмінна від ролі Ð´Ð»Ñ Ð¿Ñ€Ð¾Ð²Ñ–Ð·Ñ–ÑŽÐ²Ð°Ð½Ð½Ñ, Ñка викориÑтовувалаÑÑ Ð¿Ñ€Ð¸ автентифікації. Це дозволить Amazon EKS та панелі ÑƒÐ¿Ñ€Ð°Ð²Ð»Ñ–Ð½Ð½Ñ Kubernetes керувати реÑурÑами AWS від вашого імені. Ð”Ð»Ñ Ð²Ð¸ÐºÐ¾Ñ€Ð¸ÑÑ‚Ð°Ð½Ð½Ñ Ð½Ð¾Ð²Ð¾Ð³Ð¾ імені ролі, Ñпочатку Ñтворіть Ñ—Ñ— в %{startLink}Amazon Web Services %{externalLinkIcon} %{endLink}."
msgid "ClusterIntegration|Zone"
msgstr "Зона"
@@ -5196,7 +5366,7 @@ msgid "Code Owners to the merge request changes."
msgstr ""
msgid "Code Quality"
-msgstr ""
+msgstr "ЯкіÑÑ‚ÑŒ коду"
msgid "Code Review"
msgstr ""
@@ -5241,7 +5411,7 @@ msgid "Collapse approvers"
msgstr "Згорнути ÑпиÑок затверджуючих оÑіб"
msgid "Collapse child epics"
-msgstr ""
+msgstr "Згорнути дочірні епіки"
msgid "Collapse sidebar"
msgstr "Згорнути панель"
@@ -5421,18 +5591,33 @@ msgstr ""
msgid "Compliance framework (optional)"
msgstr ""
+msgid "ComplianceFramework|GDPR"
+msgstr ""
+
msgid "ComplianceFramework|GDPR - General Data Protection Regulation"
msgstr ""
+msgid "ComplianceFramework|HIPAA"
+msgstr ""
+
msgid "ComplianceFramework|HIPAA - Health Insurance Portability and Accountability Act"
msgstr ""
+msgid "ComplianceFramework|PCI-DSS"
+msgstr ""
+
msgid "ComplianceFramework|PCI-DSS - Payment Card Industry-Data Security Standard"
msgstr ""
+msgid "ComplianceFramework|SOC 2"
+msgstr ""
+
msgid "ComplianceFramework|SOC 2 - Service Organization Control 2"
msgstr ""
+msgid "ComplianceFramework|SOX"
+msgstr ""
+
msgid "ComplianceFramework|SOX - Sarbanes-Oxley"
msgstr ""
@@ -5527,7 +5712,7 @@ msgid "Connect your external repositories, and CI/CD pipelines will run for new
msgstr "Підключіть ваші зовнішні репозиторії, Ñ– CI/CD конвеєри будуть запуÑкатиÑÑ Ð´Ð»Ñ Ð½Ð¾Ð²Ð¸Ñ… комітів. Створений GitLab-проект буде мати лише CI/CD фунції."
msgid "Connected"
-msgstr ""
+msgstr "Підключено"
msgid "Connecting"
msgstr "З'єднаннÑ"
@@ -5563,7 +5748,7 @@ msgid "Container Registry"
msgstr "РеєÑÑ‚Ñ€ Контейнерів"
msgid "Container Registry tag expiration policy"
-msgstr ""
+msgstr "Політика Ð·Ð°ÐºÑ–Ð½Ñ‡ÐµÐ½Ð½Ñ Ñ‚ÐµÑ€Ð¼Ñ–Ð½Ñƒ дії тегу контейнерів"
msgid "Container Scanning"
msgstr "Ð¡ÐºÐ°Ð½ÑƒÐ²Ð°Ð½Ð½Ñ ÐºÐ¾Ð½Ñ‚ÐµÐ¹Ð½ÐµÑ€Ð°"
@@ -5580,9 +5765,15 @@ msgstr ""
msgid "Container repositories sync capacity"
msgstr ""
+msgid "ContainerRegistry| Please visit the %{linkStart}administration settings%{linkEnd} to enable this feature."
+msgstr ""
+
msgid "ContainerRegistry|%{imageName} tags"
msgstr ""
+msgid "ContainerRegistry|%{title} was successfully scheduled for deletion"
+msgstr ""
+
msgid "ContainerRegistry|Automatically remove extra images that aren't designed to be kept."
msgstr ""
@@ -5595,6 +5786,9 @@ msgstr ""
msgid "ContainerRegistry|Container Registry"
msgstr "РеєÑÑ‚Ñ€ контейнерів"
+msgid "ContainerRegistry|Container Registry tag expiration and retention policy is disabled"
+msgstr ""
+
msgid "ContainerRegistry|Copy build command"
msgstr "Скопіювати команду збірки"
@@ -5604,18 +5798,12 @@ msgstr "Скопіювати команду входу"
msgid "ContainerRegistry|Copy push command"
msgstr "Скопіювати команду відправленнÑ"
-msgid "ContainerRegistry|Currently, the Container Registry tag expiration feature is not available for projects created before GitLab version 12.8. For updates and more information, visit Issue %{linkStart}#196124%{linkEnd}"
-msgstr ""
-
msgid "ContainerRegistry|Docker connection error"
msgstr "Помилка Ð·â€™Ñ”Ð´Ð½Ð°Ð½Ð½Ñ Docker"
msgid "ContainerRegistry|Docker tag expiration policy is %{toggleStatus}"
msgstr ""
-msgid "ContainerRegistry|Docker tags with names matching this regex pattern will expire:"
-msgstr ""
-
msgid "ContainerRegistry|Edit Settings"
msgstr ""
@@ -5623,10 +5811,10 @@ msgid "ContainerRegistry|Expiration interval:"
msgstr ""
msgid "ContainerRegistry|Expiration policy successfully saved."
-msgstr ""
+msgstr "Політика щодо терміну дії уÑпішно збережена."
msgid "ContainerRegistry|Expiration policy:"
-msgstr ""
+msgstr "Політика завершеннÑ:"
msgid "ContainerRegistry|Expiration schedule:"
msgstr ""
@@ -5637,9 +5825,6 @@ msgstr "Якщо ви ще не виконали вхід вам необхідÐ
msgid "ContainerRegistry|Image ID"
msgstr "Ідентифікатор образу"
-msgid "ContainerRegistry|Image deleted successfully"
-msgstr ""
-
msgid "ContainerRegistry|Keep and protect the images that matter most."
msgstr ""
@@ -5647,7 +5832,7 @@ msgid "ContainerRegistry|Last Updated"
msgstr "ВоÑтаннє оновлено"
msgid "ContainerRegistry|Login"
-msgstr ""
+msgstr "Увійти"
msgid "ContainerRegistry|Missing or insufficient permission, delete button disabled"
msgstr ""
@@ -5655,12 +5840,24 @@ msgstr ""
msgid "ContainerRegistry|Number of tags to retain:"
msgstr ""
+msgid "ContainerRegistry|Please contact your administrator."
+msgstr ""
+
msgid "ContainerRegistry|Push an image"
msgstr ""
msgid "ContainerRegistry|Quick Start"
msgstr "Швидкий Ñтарт"
+msgid "ContainerRegistry|Regular expressions such as %{codeStart}.*-test%{codeEnd} or %{codeStart}dev-.*%{codeEnd} are supported"
+msgstr ""
+
+msgid "ContainerRegistry|Regular expressions such as %{codeStart}.*-test%{codeEnd} or %{codeStart}dev-.*%{codeEnd} are supported. To select all tags, use %{codeStart}.*%{codeEnd}"
+msgstr ""
+
+msgid "ContainerRegistry|Remember to run %{docLinkStart}garbage collection%{docLinkEnd} to remove the stale data from storage."
+msgstr ""
+
msgid "ContainerRegistry|Remove repository"
msgstr "Видалити репозиторій"
@@ -5677,22 +5874,22 @@ msgstr[3] "Видалити тегів"
msgid "ContainerRegistry|Retention policy has been Enabled"
msgstr ""
-msgid "ContainerRegistry|Something went wrong while deleting the image."
+msgid "ContainerRegistry|Something went wrong while fetching the expiration policy."
msgstr ""
-msgid "ContainerRegistry|Something went wrong while deleting the tag."
+msgid "ContainerRegistry|Something went wrong while fetching the repository list."
msgstr ""
-msgid "ContainerRegistry|Something went wrong while deleting the tags."
+msgid "ContainerRegistry|Something went wrong while fetching the tags list."
msgstr ""
-msgid "ContainerRegistry|Something went wrong while fetching the expiration policy."
+msgid "ContainerRegistry|Something went wrong while marking the tag for deletion."
msgstr ""
-msgid "ContainerRegistry|Something went wrong while fetching the packages list."
+msgid "ContainerRegistry|Something went wrong while marking the tags for deletion."
msgstr ""
-msgid "ContainerRegistry|Something went wrong while fetching the tags list."
+msgid "ContainerRegistry|Something went wrong while scheduling %{title} for deletion. Please try again."
msgstr ""
msgid "ContainerRegistry|Something went wrong while updating the expiration policy."
@@ -5701,16 +5898,25 @@ msgstr ""
msgid "ContainerRegistry|Tag"
msgstr "Тег"
-msgid "ContainerRegistry|Tag deleted successfully"
+msgid "ContainerRegistry|Tag expiration policy"
+msgstr "Політика Ð·Ð°ÐºÑ–Ð½Ñ‡ÐµÐ½Ð½Ñ Ñ‚ÐµÑ€Ð¼Ñ–Ð½Ñƒ дії тегу"
+
+msgid "ContainerRegistry|Tag expiration policy is designed to:"
msgstr ""
-msgid "ContainerRegistry|Tag expiration policy"
+msgid "ContainerRegistry|Tag successfully marked for deletion."
msgstr ""
-msgid "ContainerRegistry|Tag expiration policy is designed to:"
+msgid "ContainerRegistry|Tags successfully marked for deletion."
+msgstr ""
+
+msgid "ContainerRegistry|Tags with names matching this regex pattern will %{italicStart}be preserved:%{italicEnd}"
msgstr ""
-msgid "ContainerRegistry|Tags deleted successfully"
+msgid "ContainerRegistry|Tags with names matching this regex pattern will %{italicStart}expire:%{italicEnd}"
+msgstr ""
+
+msgid "ContainerRegistry|The Container Registry tag expiration and retention policies for this project have not been enabled."
msgstr ""
msgid "ContainerRegistry|The last tag related to this image was recently removed. This empty image and any associated data will be automatically removed as part of the regular Garbage Collection process. If you have any questions, contact your administrator."
@@ -5731,18 +5937,18 @@ msgstr "В цій групі немає образів контейнерів"
msgid "ContainerRegistry|There are no container images stored for this project"
msgstr "Ð”Ð»Ñ Ñ†ÑŒÐ¾Ð³Ð¾ проекту не збережено жодного образу контейнерів"
-msgid "ContainerRegistry|This Registry contains deleted image tag data. Remember to run %{docLinkStart}garbage collection%{docLinkEnd} to remove the stale data from storage."
+msgid "ContainerRegistry|There was an error during the deletion of this image repository, please try again."
msgstr ""
msgid "ContainerRegistry|This image has no active tags"
msgstr "Образ не має активних тегів"
+msgid "ContainerRegistry|This image repository is scheduled for deletion"
+msgstr ""
+
msgid "ContainerRegistry|We are having trouble connecting to Docker, which could be due to an issue with your project name or path. %{docLinkStart}More Information%{docLinkEnd}"
msgstr "У Ð½Ð°Ñ Ð²Ð¸Ð½Ð¸ÐºÐ»Ð¸ проблеми при з’єднанні з Docker, що може бути Ñпричинено помилкою пов’Ñзаною із іменем проекту або шлÑхом. %{docLinkStart}ДізнатиÑÑ Ð±Ñ–Ð»ÑŒÑˆÐµ%{docLinkEnd}"
-msgid "ContainerRegistry|Wildcards such as %{codeStart}.*-stable%{codeEnd} or %{codeStart}production/.*%{codeEnd} are supported. To select all tags, use %{codeStart}.*%{codeEnd}"
-msgstr ""
-
msgid "ContainerRegistry|With the Container Registry, every project can have its own space to store its Docker images. %{docLinkStart}More Information%{docLinkEnd}"
msgstr "За допомогою РеєÑтру контейнерів, кожен проект може мати влаÑний проÑÑ‚Ñ–Ñ€ Ð´Ð»Ñ Ñ€Ð¾Ð·Ð¼Ñ–Ñ‰ÐµÐ½Ð½Ñ Ð¾Ð±Ñ€Ð°Ð·Ñ–Ð² Docker. %{docLinkStart}ДізнатиÑÑ Ð±Ñ–Ð»ÑŒÑˆÐµ%{docLinkEnd}"
@@ -5804,16 +6010,16 @@ msgid "ContributionAnalytics|Issues"
msgstr "Задачі"
msgid "ContributionAnalytics|Last 3 months"
-msgstr ""
+msgstr "ОÑтанні 3 міÑÑці"
msgid "ContributionAnalytics|Last month"
-msgstr ""
+msgstr "ОÑтанній міÑÑць"
msgid "ContributionAnalytics|Last week"
-msgstr ""
+msgstr "ОÑтанній тиждень"
msgid "ContributionAnalytics|Merge Requests"
-msgstr ""
+msgstr "Запити на злиттÑ"
msgid "ContributionAnalytics|No issues for the selected time period."
msgstr ""
@@ -5912,7 +6118,7 @@ msgid "Copy commit SHA"
msgstr "Скопіювати SHA коміту"
msgid "Copy environment"
-msgstr ""
+msgstr "Копіювати Ñередовище"
msgid "Copy evidence SHA"
msgstr ""
@@ -5927,7 +6133,7 @@ msgid "Copy impersonation token"
msgstr "Скопіювати токен імітуваннÑ"
msgid "Copy key"
-msgstr ""
+msgstr "Копіювати ключ"
msgid "Copy labels and milestone from %{source_issuable_reference}."
msgstr "Скопіювати мітки та етап із %{source_issuable_reference}."
@@ -5957,7 +6163,7 @@ msgid "Copy trigger token"
msgstr "Скопіювати токен тригера"
msgid "Copy value"
-msgstr ""
+msgstr "Копіювати значеннÑ"
msgid "Could not add admins as members"
msgstr "Ðе вдалоÑÑ Ð´Ð¾Ð´Ð°Ñ‚Ð¸ адмініÑтраторів Ñк учаÑників"
@@ -5995,8 +6201,8 @@ msgstr "Ðе вдалоÑÑ Ð²Ð¸Ð´Ð°Ð»Ð¸Ñ‚Ð¸ %{design}. Будь лаÑка, ÑÐ
msgid "Could not delete chat nickname %{chat_name}."
msgstr "Ðе вдалоÑÑ Ð²Ð¸Ð´Ð°Ð»Ð¸Ñ‚Ð¸ пÑевдонім Ð´Ð»Ñ Ñ‡Ð°Ñ‚Ñƒ %{chat_name}."
-msgid "Could not find design"
-msgstr "Ðе вдалоÑÑ Ð·Ð½Ð°Ð¹Ñ‚Ð¸ дизайн"
+msgid "Could not find design."
+msgstr ""
msgid "Could not remove the trigger."
msgstr "Ðе вдалоÑÑ Ð²Ð¸Ð´Ð°Ð»Ð¸Ñ‚Ð¸ тригер."
@@ -6010,6 +6216,9 @@ msgstr "Ðеможливо відкликати токен імітуваннÑ
msgid "Could not revoke personal access token %{personal_access_token_name}."
msgstr "Ðеможливо відкликати перÑональний токен доÑтупу %{personal_access_token_name}."
+msgid "Could not revoke project access token %{project_access_token_name}."
+msgstr ""
+
msgid "Could not save group ID"
msgstr ""
@@ -6067,6 +6276,9 @@ msgstr "Створити запит на злиттÑ"
msgid "Create a new branch"
msgstr "Створити нову гілку"
+msgid "Create a new deploy key for this project"
+msgstr ""
+
msgid "Create a new file as there are no files yet. Afterwards, you'll be able to commit your changes."
msgstr "Створіть новий файл, тому що наразі немає жодного файлу. Пізніше ви зможете закомітити ваші зміни."
@@ -6172,6 +6384,9 @@ msgstr "Створити мітку проекту"
msgid "Create requirement"
msgstr ""
+msgid "Create snippet"
+msgstr ""
+
msgid "Create wildcard: %{searchTerm}"
msgstr ""
@@ -6193,11 +6408,14 @@ msgstr "Створити токен Ð´Ð»Ñ Ð¾ÑобиÑтого доÑтупу"
msgid "Created"
msgstr "Створено"
+msgid "Created %{timestamp}"
+msgstr ""
+
msgid "Created At"
msgstr "Створено в"
msgid "Created On"
-msgstr ""
+msgstr "Створено"
msgid "Created a branch and a merge request to resolve this issue."
msgstr "Створено гілку та запит на Ð·Ð»Ð¸Ñ‚Ñ‚Ñ Ð´Ð»Ñ Ð²Ð¸Ñ€Ñ–ÑˆÐµÐ½Ð½Ñ Ñ†Ñ–Ñ”Ñ— задачі."
@@ -6242,7 +6460,7 @@ msgid "Creation date"
msgstr "Дата ÑтвореннÑ"
msgid "Credentials"
-msgstr ""
+msgstr "Облікові дані"
msgid "CredentialsInventory|No credentials found"
msgstr ""
@@ -6510,7 +6728,7 @@ msgstr[2] ""
msgstr[3] ""
msgid "CycleAnalytics|Select labels"
-msgstr ""
+msgstr "Вибрати мітки"
msgid "CycleAnalytics|Show"
msgstr "Показати"
@@ -6537,7 +6755,7 @@ msgid "CycleAnalytics|Total days to completion"
msgstr "Загальна кількіÑÑ‚ÑŒ днів до завершеннÑ"
msgid "CycleAnalytics|Type of work"
-msgstr ""
+msgstr "Тип роботи"
msgid "CycleAnalytics|group dropdown filter"
msgstr "випадаючий фільтр груп"
@@ -6551,6 +6769,9 @@ msgstr "випадаючий ÑпиÑок проєктів"
msgid "CycleAnalytics|stage dropdown"
msgstr "Випадаючий ÑпиÑок Ñтадій"
+msgid "DAG"
+msgstr ""
+
msgid "DNS"
msgstr "DNS"
@@ -6591,7 +6812,7 @@ msgid "Date range cannot exceed %{maxDateRange} days."
msgstr ""
msgid "Day of month"
-msgstr ""
+msgstr "День міÑÑцÑ"
msgid "DayTitle|F"
msgstr "Пт"
@@ -6822,10 +7043,10 @@ msgstr[3] "%d вразливоÑтей"
msgid "Dependencies|%d vulnerability detected"
msgid_plural "Dependencies|%d vulnerabilities detected"
-msgstr[0] ""
-msgstr[1] ""
-msgstr[2] ""
-msgstr[3] ""
+msgstr[0] "виÑвлено %d вразливіÑÑ‚ÑŒ"
+msgstr[1] "виÑвлено %d вразливоÑÑ‚Ñ–"
+msgstr[2] "виÑвлено %d вразливоÑтей"
+msgstr[3] "виÑвлено %d вразливоÑтей"
msgid "Dependencies|%{remainingLicensesCount} more"
msgstr "ще %{remainingLicensesCount}"
@@ -6867,7 +7088,7 @@ msgid "Dependencies|The %{codeStartTag}dependency_scanning%{codeEndTag} job has
msgstr "Ð—Ð°Ð²Ð´Ð°Ð½Ð½Ñ %{codeStartTag}dependency_scanning%{codeEndTag} закінчилоÑÑ Ð½ÐµÑƒÑпішно Ñ– неможе згенерувати ÑпиÑок. Будь лаÑка, переконайтеÑÑ, що Ð·Ð°Ð²Ð´Ð°Ð½Ð½Ñ Ð²Ð¸ÐºÐ¾Ñ€ÑƒÑ”Ñ‚ÑŒÑÑ Ð½Ð°Ð»ÐµÐ¶Ð½Ð¸Ð¼ чином Ñ– перезапуÑÑ‚Ñ–Ñ‚ÑŒ конвеєр."
msgid "Dependencies|Toggle vulnerability list"
-msgstr ""
+msgstr "Увімкнути/вимкнути ÑпиÑок вразливоÑтей"
msgid "Dependencies|Unsupported file(s) detected"
msgstr "ВиÑвлено непідтимувані файли"
@@ -6929,6 +7150,9 @@ msgstr "Ключі Ð´Ð»Ñ Ñ€Ð¾Ð·Ð³Ð¾Ñ€Ñ‚ÑƒÐ²Ð°Ð½Ð½Ñ"
msgid "Deploy key was successfully updated."
msgstr "Ключ Ñ€Ð¾Ð·Ð³Ð¾Ñ€Ñ‚Ð°Ð½Ð½Ñ ÑƒÑпішно оновлено."
+msgid "Deploy keys allow read-only or read-write (if enabled) access to your repository. Deploy keys can be used for CI, staging or production servers. You can create a deploy key or add an existing one."
+msgstr ""
+
msgid "Deploy progress not found. To see pods, ensure your environment matches %{linkStart}deploy board criteria%{linkEnd}."
msgstr "Хід Ñ€Ð¾Ð·Ð³Ð¾Ñ€Ñ‚Ð°Ð½Ð½Ñ Ð½Ðµ знайдено. Щоб побачити pod'и, переконайтеÑÑŒ, що ваше Ñередовище відповідає %{linkStart}критеріÑм дошки розгортань%{linkEnd}."
@@ -7082,6 +7306,9 @@ msgstr "Розгорнуто на"
msgid "Deploying to"
msgstr "Ð Ð¾Ð·Ð³Ð¾Ñ€Ñ‚Ð°Ð½Ð½Ñ Ð´Ð¾"
+msgid "Deployment Frequency"
+msgstr ""
+
msgid "Deployment|API"
msgstr "API"
@@ -7155,7 +7382,7 @@ msgid "DesignManagement|Are you sure you want to delete the selected designs?"
msgstr "Ви впевнені, що хочете видалити вибрані дизайни?"
msgid "DesignManagement|Cancel comment confirmation"
-msgstr ""
+msgstr "СкаÑувати Ð¿Ñ–Ð´Ñ‚Ð²ÐµÑ€Ð´Ð¶ÐµÐ½Ð½Ñ ÐºÐ¾Ð¼ÐµÐ½Ñ‚Ð°Ñ€Ñ"
msgid "DesignManagement|Could not add a new comment. Please try again."
msgstr ""
@@ -7179,7 +7406,7 @@ msgid "DesignManagement|Deselect all"
msgstr "ЗнÑти Ð²Ð¸Ð´Ñ–Ð»ÐµÐ½Ð½Ñ Ð· уÑÑ–Ñ…"
msgid "DesignManagement|Discard comment"
-msgstr ""
+msgstr "Відхилити коментар"
msgid "DesignManagement|Error uploading a new design. Please try again."
msgstr ""
@@ -7194,7 +7421,7 @@ msgid "DesignManagement|Go to previous design"
msgstr "Перейти до попереднього дизайну"
msgid "DesignManagement|Keep comment"
-msgstr ""
+msgstr "Залишити коментар"
msgid "DesignManagement|Requested design version does not exist. Showing latest version instead"
msgstr "Бажана верÑÑ–Ñ Ð´Ð¸Ð·Ð°Ð¹Ð½Ñƒ не Ñ–Ñнує. ЗаміÑÑ‚ÑŒ неї відображаєтьÑÑ Ð¾ÑтаннÑ"
@@ -7290,7 +7517,7 @@ msgid "Discard all changes"
msgstr "Відхилити вÑÑ– зміни"
msgid "Discard all changes?"
-msgstr ""
+msgstr "СкаÑувати вÑÑ– зміни?"
msgid "Discard changes"
msgstr "Відхилити зміни"
@@ -7317,7 +7544,7 @@ msgid "Discover projects, groups and snippets. Share your projects with others"
msgstr "Відкрийте Ð´Ð»Ñ Ñебе групи, проекти та фрагменти коду. ПоділітьÑÑ Ñвоїми проектами з іншими"
msgid "Discover|Check your application for security vulnerabilities that may lead to unauthorized access, data leaks, and denial of services."
-msgstr ""
+msgstr "Перевірте Ñвій заÑтоÑунок на наÑвніÑÑ‚ÑŒ вразливоÑтей безпеки, що можуть привеÑти до неавторизованого доÑтупу, витоку даних, або відмови ÑевіÑів."
msgid "Discover|For code that's already live in production, our dashboards give you an easy way to prioritize any issues that are found, empowering your team to ship quickly and securely."
msgstr ""
@@ -7338,7 +7565,7 @@ msgid "Discover|Start a free trial"
msgstr ""
msgid "Discover|Upgrade now"
-msgstr ""
+msgstr "Оновити зараз"
msgid "Discuss a specific suggestion or question"
msgstr "Обговорити конкретну пропозицію чи питаннÑ"
@@ -7366,10 +7593,10 @@ msgstr "Відхилити"
msgid "Dismiss %d selected vulnerability as"
msgid_plural "Dismiss %d selected vulnerabilities as"
-msgstr[0] ""
-msgstr[1] ""
-msgstr[2] ""
-msgstr[3] ""
+msgstr[0] "Відхилити %d виділену вразливіÑÑ‚ÑŒ Ñк"
+msgstr[1] "Відхилити %d виділені вразливоÑÑ‚Ñ– Ñк"
+msgstr[2] "Відхилити %d виділених вразливоÑтей Ñк"
+msgstr[3] "Відхилити %d виділених вразливоÑтей Ñк"
msgid "Dismiss DevOps Score introduction"
msgstr ""
@@ -7558,7 +7785,7 @@ msgid "Edit Deploy Key"
msgstr "Редагувати ключ Ð´Ð»Ñ Ñ€Ð¾Ð·Ð³Ð¾Ñ€Ñ‚ÑƒÐ²Ð°Ð½Ð½Ñ"
msgid "Edit Geo Node"
-msgstr ""
+msgstr "Редагувати Гео-вузол"
msgid "Edit Group Hook"
msgstr "Редагувати хук групи"
@@ -7602,9 +7829,6 @@ msgstr "Редагувати опиÑ"
msgid "Edit environment"
msgstr "Редагувати Ñередовище"
-msgid "Edit epic description"
-msgstr "Редагувати Ð¾Ð¿Ð¸Ñ ÐµÐ¿Ñ–ÐºÑƒ"
-
msgid "Edit file"
msgstr "Редагувати файл"
@@ -8026,7 +8250,7 @@ msgid "EnvironmentsDashboard|More actions"
msgstr "Додаткові дії"
msgid "EnvironmentsDashboard|Read more."
-msgstr ""
+msgstr "Читати більше."
msgid "EnvironmentsDashboard|Remove"
msgstr "Видалити"
@@ -8077,10 +8301,10 @@ msgid "Environments|Currently showing all results."
msgstr ""
msgid "Environments|Delete"
-msgstr ""
+msgstr "Видалити"
msgid "Environments|Delete environment"
-msgstr ""
+msgstr "Видалити Ñередовище"
msgid "Environments|Deleting the '%{environmentName}' environment cannot be undone. Do you want to delete it anyway?"
msgstr ""
@@ -8226,9 +8450,6 @@ msgstr "Події епіків"
msgid "Epics"
msgstr "Епіки"
-msgid "Epics (Ultimate / Gold license only)"
-msgstr "Епіки (лише Ultimate / Gold ліцензіÑ)"
-
msgid "Epics Roadmap"
msgstr "План-графік епіків"
@@ -8238,6 +8459,9 @@ msgstr "Епіки та задачі"
msgid "Epics let you manage your portfolio of projects more efficiently and with less effort"
msgstr "Епіки дозволÑÑŽÑ‚ÑŒ керувати вашим портфелем проектів ефективніше та з меншими зуÑиллÑми"
+msgid "Epics, Issues, and Merge Requests"
+msgstr ""
+
msgid "Epics|Add an epic"
msgstr "Додати епік"
@@ -8289,6 +8513,9 @@ msgstr "ЩоÑÑŒ пішло не так при отриманні дочірні
msgid "Epics|Something went wrong while fetching group epics."
msgstr "Помилка при отриманні епіків групи."
+msgid "Epics|Something went wrong while moving item."
+msgstr ""
+
msgid "Epics|Something went wrong while ordering item."
msgstr "Помилка при вÑтановленні порÑдку елементу."
@@ -8343,6 +8570,9 @@ msgstr "Помилка Ð·Ð°Ð²Ð°Ð½Ñ‚Ð°Ð¶ÐµÐ½Ð½Ñ Ð¼Ñ–Ñ‚Ð¾Ðº."
msgid "Error fetching network graph."
msgstr "Помилка при отриманні графа мережі."
+msgid "Error fetching payload data."
+msgstr ""
+
msgid "Error fetching projects"
msgstr "Помилка Ð¾Ñ‚Ñ€Ð¸Ð¼Ð°Ð½Ð½Ñ Ð¿Ñ€Ð¾ÐµÐºÑ‚Ñ–Ð²"
@@ -8352,9 +8582,6 @@ msgstr "Помилка Ð·Ð°Ð²Ð°Ð½Ñ‚Ð°Ð¶ÐµÐ½Ð½Ñ refs"
msgid "Error fetching the dependency list. Please check your network connection and try again."
msgstr "Помилка при отриманні ÑпиÑку залежноÑтей. Будь лаÑка, перевірте ваше Ð·â€™Ñ”Ð´Ð½Ð°Ð½Ð½Ñ Ð· мережею."
-msgid "Error fetching usage ping data."
-msgstr "Помилка при отриманні данних про викориÑтаннÑ."
-
msgid "Error loading branch data. Please try again."
msgstr "Помилка при завантаженні даних про гілку. Будь лаÑка, Ñпробуйте знову."
@@ -8496,6 +8723,9 @@ msgstr "Ð”Ð»Ñ Ð¼Ð¾Ð¶Ð»Ð¸Ð²Ð¾ÑÑ‚Ñ– вибору проектів введіть
msgid "Errors"
msgstr "Помилки"
+msgid "Errors:"
+msgstr ""
+
msgid "Estimated"
msgstr "За оцінками"
@@ -8700,12 +8930,18 @@ msgstr "ПереглÑнути публічні групи"
msgid "Export as CSV"
msgstr "ЕкÑпортувати Ñк CSV"
+msgid "Export group"
+msgstr ""
+
msgid "Export issues"
msgstr "ЕкÑпортувати задачі"
msgid "Export project"
msgstr "ЕкÑпорт проекту"
+msgid "Export this group with all related data to a new GitLab instance. Once complete, you can import the data file from the \"New Group\" page."
+msgstr ""
+
msgid "Export this project with all its related data in order to move your project to a new GitLab instance. Once the export is finished, you can import the file from the \"New Project\" page."
msgstr "ЕкÑпортувати цей проект з уÑіма пов'Ñзаними даними Ð´Ð»Ñ Ñ‚Ð¾Ð³Ð¾, щоб переміÑтити його на новий інÑÑ‚Ð°Ð½Ñ GitLab. ПіÑÐ»Ñ Ð·Ð°Ð²ÐµÑ€ÑˆÐµÐ½Ð½Ñ ÐµÐºÑпорту ви зможете імпортувати файл на Ñторінці \"Ðовий проект\"."
@@ -8775,9 +9011,6 @@ msgstr "Ðевдало"
msgid "Failed Jobs"
msgstr "Провалені завданнÑ"
-msgid "Failed create wiki"
-msgstr "Ðе вдалоÑÑ Ñтворити вікі"
-
msgid "Failed to add a Zoom meeting"
msgstr "Ðе вдалоÑÑ Ð´Ð¾Ð´Ð°Ñ‚Ð¸ Zoom-зуÑтріч"
@@ -8817,6 +9050,9 @@ msgstr ""
msgid "Failed to create resources"
msgstr "Ðе вдалоÑÑ Ñтворити реÑурÑ"
+msgid "Failed to create wiki"
+msgstr ""
+
msgid "Failed to delete board. Please try again."
msgstr "Ðе вдалоÑÑ Ð²Ð¸Ð´Ð°Ð»Ð¸Ñ‚Ð¸ дошку. Будь лаÑка, Ñпробуйте знову."
@@ -8826,7 +9062,7 @@ msgstr "Ðе вдалоÑÑ Ñ€Ð¾Ð·Ð³Ð¾Ñ€Ð½ÑƒÑ‚Ð¸ до"
msgid "Failed to enqueue the rebase operation, possibly due to a long-lived transaction. Try again later."
msgstr "Ðе вдалоÑÑ Ð²ÐºÐ»ÑŽÑ‡Ð¸Ñ‚Ð¸ rebase в чергу, можливо через довгоживучу транзакцію. Спробуйте знову через деÑкий чаÑ."
-msgid "Failed to find import label for jira import."
+msgid "Failed to find import label for Jira import."
msgstr ""
msgid "Failed to get ref."
@@ -8850,6 +9086,12 @@ msgstr ""
msgid "Failed to load groups & users."
msgstr "Ðе вдалоÑÑ Ð·Ð°Ð²Ð°Ð½Ñ‚Ð°Ð¶Ð¸Ñ‚Ð¸ групи та кориÑтувачів."
+msgid "Failed to load labels. Please try again."
+msgstr ""
+
+msgid "Failed to load milestones. Please try again."
+msgstr ""
+
msgid "Failed to load related branches"
msgstr "Ðе вдалоÑÑ Ð·Ð°Ð²Ð°Ð½Ñ‚Ð°Ð¶Ð¸Ñ‚Ð¸ пов’Ñзані гілки"
@@ -9154,7 +9396,7 @@ msgid "Fetching licenses failed. You are not permitted to perform this action."
msgstr "Ðе вдалоÑÑ Ð¾Ñ‚Ñ€Ð¸Ð¼Ð°Ñ‚Ð¸ ліцензії. У Ð²Ð°Ñ Ð½ÐµÐ¼Ð°Ñ” дозовлів виконувати цю дію."
msgid "File"
-msgstr ""
+msgstr "Файл"
msgid "File Hooks"
msgstr ""
@@ -9193,7 +9435,7 @@ msgid "File upload error."
msgstr "Помилка Ð·Ð°Ð²Ð°Ð½Ñ‚Ð°Ð¶ÐµÐ½Ð½Ñ Ñ„Ð°Ð¹Ð»Ñƒ."
msgid "File:"
-msgstr ""
+msgstr "Файл:"
msgid "Files"
msgstr "Файли"
@@ -9225,8 +9467,11 @@ msgstr "Фільтрувати за повідомленнÑм Ð´Ð»Ñ ÐºÐ¾Ð¼Ñ–Ñ‚
msgid "Filter by milestone name"
msgstr "Фільтрувати за назвою етапу"
-msgid "Filter by name..."
-msgstr "Фільтр по імені..."
+msgid "Filter by name"
+msgstr ""
+
+msgid "Filter by status"
+msgstr ""
msgid "Filter by two-factor authentication"
msgstr "Фільтрувати за двофакторною автентифікацією"
@@ -9235,7 +9480,10 @@ msgid "Filter by user"
msgstr "Сортувати по кориÑтувачах"
msgid "Filter projects"
-msgstr "Фільтрувати проекти"
+msgstr ""
+
+msgid "Filter results"
+msgstr ""
msgid "Filter results by group"
msgstr "Фільтрувати результати за групою"
@@ -9243,6 +9491,9 @@ msgstr "Фільтрувати результати за групою"
msgid "Filter results by project"
msgstr "Фільтрувати результати за проектом"
+msgid "Filter results..."
+msgstr ""
+
msgid "Filter your projects by name"
msgstr "Відфільтрувати ваші проекти за іменами"
@@ -9310,7 +9561,7 @@ msgid "Fixed:"
msgstr "Виправлено:"
msgid "Flags"
-msgstr ""
+msgstr "Прапори"
msgid "FlowdockService|Flowdock Git source token"
msgstr "Токен джерела Git Ð´Ð»Ñ Flowdock"
@@ -9531,12 +9782,12 @@ msgstr "ВкладеннÑ"
msgid "GeoNodes|Checksummed"
msgstr "Із контрольною Ñумою"
+msgid "GeoNodes|Consult Geo troubleshooting information"
+msgstr ""
+
msgid "GeoNodes|Container repositories"
msgstr "Репозиторії контейнерів"
-msgid "GeoNodes|Data is out of date from %{timeago}"
-msgstr "Дані заÑтаріли на %{timeago}"
-
msgid "GeoNodes|Data replication lag"
msgstr "Затримка реплікації даних"
@@ -9576,6 +9827,9 @@ msgstr "ОÑтанній ідентифікатор події, оброблен
msgid "GeoNodes|Last event ID seen from primary"
msgstr "ОÑтанній ідентифікатор події, видимий з оÑновного"
+msgid "GeoNodes|Learn more about Geo node statuses"
+msgstr ""
+
msgid "GeoNodes|Loading nodes"
msgstr "Ð—Ð°Ð²Ð°Ð½Ñ‚Ð°Ð¶ÐµÐ½Ð½Ñ Ð²ÑƒÐ·Ð»Ñ–Ð²"
@@ -9591,6 +9845,9 @@ msgstr ""
msgid "GeoNodes|Node was successfully removed."
msgstr "Вузол уÑпішно видалено."
+msgid "GeoNodes|Node's status was updated %{timeAgo}."
+msgstr ""
+
msgid "GeoNodes|Not checksummed"
msgstr "Без контрольної Ñуми"
@@ -9621,8 +9878,11 @@ msgstr "Стан обчиÑÐ»ÐµÐ½Ð½Ñ ÐºÐ¾Ð½Ñ‚Ñ€Ð¾Ð»ÑŒÐ½Ð¾Ñ— Ñуми репоз
msgid "GeoNodes|Repository verification progress"
msgstr "Стан перевірки репозиторію"
-msgid "GeoNodes|Selective"
-msgstr "Вибіркові"
+msgid "GeoNodes|Selective (%{syncLabel})"
+msgstr ""
+
+msgid "GeoNodes|Selective synchronization"
+msgstr ""
msgid "GeoNodes|Something went wrong while changing node status"
msgstr "Проблема при зміні ÑтатуÑа вузла"
@@ -9648,6 +9908,9 @@ msgstr "ÐевикориÑтані Ñлоти"
msgid "GeoNodes|Unverified"
msgstr "Ðепідтверджені"
+msgid "GeoNodes|Updated %{timeAgo}"
+msgstr ""
+
msgid "GeoNodes|Used slots"
msgstr "ВикориÑтані Ñлоти"
@@ -9687,17 +9950,17 @@ msgstr "%{name} заплановано Ð´Ð»Ñ Ð¿Ð¾Ð²Ñ‚Ð¾Ñ€Ð½Ð¾Ñ— перевірÐ
msgid "Geo|All"
msgstr "Ð’ÑÑ–"
-msgid "Geo|All projects"
-msgstr "Ð’ÑÑ– проекти"
+msgid "Geo|All %{replicable_type}"
+msgstr ""
-msgid "Geo|All projects are being scheduled for re-sync"
-msgstr "Ð’ÑÑ– проекти плануютьÑÑ Ð´Ð»Ñ Ð¿Ð¾Ð²Ñ‚Ð¾Ñ€Ð½Ð¾Ñ— Ñинхронізації"
+msgid "Geo|All projects"
+msgstr ""
-msgid "Geo|All projects are being scheduled for re-verify"
-msgstr "Ð’ÑÑ– проекти заплановано Ð´Ð»Ñ Ð¿Ð¾Ð²Ñ‚Ð¾Ñ€Ð½Ð¾Ñ— перевірки"
+msgid "Geo|All projects are being scheduled for resync"
+msgstr ""
-msgid "Geo|Batch operations"
-msgstr "Групові операції"
+msgid "Geo|All projects are being scheduled for reverify"
+msgstr ""
msgid "Geo|Could not remove tracking entry for an existing project."
msgstr "Ðе вдалоÑÑ Ð²Ð¸Ð´Ð°Ð»Ð¸Ñ‚Ð¸ Ð·Ð°Ð¿Ð¸Ñ Ð²Ñ–Ð´ÑÑ‚ÐµÐ¶ÐµÐ½Ð½Ñ Ð´Ð»Ñ Ñ–Ñнуючого проекту."
@@ -9708,9 +9971,15 @@ msgstr "Ðе вдалоÑÑ Ð²Ð¸Ð´Ð°Ð»Ð¸Ñ‚Ð¸ Ð·Ð°Ð¿Ð¸Ñ Ð²Ñ–Ð´ÑÑ‚ÐµÐ¶ÐµÐ½Ð½Ñ Ð
msgid "Geo|Failed"
msgstr "Ðевдало"
+msgid "Geo|Filter by status"
+msgstr ""
+
msgid "Geo|Geo Status"
msgstr "Geo ÑтатуÑ"
+msgid "Geo|In progress"
+msgstr ""
+
msgid "Geo|In sync"
msgstr "Синхронізовано"
@@ -9735,9 +10004,6 @@ msgstr "ÐаÑтупна ÑÐ¸Ð½Ñ…Ñ€Ð¾Ð½Ñ–Ð·Ð°Ñ†Ñ–Ñ Ð·Ð°Ð¿Ð»Ð°Ð½Ð¾Ð²Ð°Ð½Ð° на"
msgid "Geo|Not synced yet"
msgstr "Ðе Ñинхронізовано"
-msgid "Geo|Pending"
-msgstr "В очікуванні"
-
msgid "Geo|Pending synchronization"
msgstr "ÐžÑ‡Ñ–ÐºÑƒÐ²Ð°Ð½Ð½Ñ Ñинхронізації"
@@ -9774,8 +10040,8 @@ msgstr ""
msgid "Geo|Resync"
msgstr "Повторна ÑинхронізаціÑ"
-msgid "Geo|Resync all projects"
-msgstr "Повторна ÑÐ¸Ð½Ñ…Ñ€Ð¾Ð½Ñ–Ð·Ð°Ñ†Ñ–Ñ Ð²ÑÑ–Ñ… проектів"
+msgid "Geo|Resync all"
+msgstr ""
msgid "Geo|Retry count"
msgstr "КількіÑÑ‚ÑŒ Ñпроб"
@@ -9783,8 +10049,8 @@ msgstr "КількіÑÑ‚ÑŒ Ñпроб"
msgid "Geo|Reverify"
msgstr "Повторно перевірити"
-msgid "Geo|Reverify all projects"
-msgstr "Повторно перевірити уÑÑ– проекти"
+msgid "Geo|Reverify all"
+msgstr ""
msgid "Geo|Status"
msgstr "СтатуÑ"
@@ -9889,7 +10155,7 @@ msgid "GitHub import"
msgstr "GitHub-імпорт"
msgid "GitLab / Unsubscribe"
-msgstr ""
+msgstr "GitLab / СкаÑувати підпиÑку"
msgid "GitLab Enterprise Edition %{plan}"
msgstr "GitLab Enterprise Edition %{plan}"
@@ -10108,7 +10374,7 @@ msgid "Go to %{link_to_google_takeout}."
msgstr "Перейти до %{link_to_google_takeout}."
msgid "Go to Pipelines"
-msgstr ""
+msgstr "Перейти до конвеєрів"
msgid "Go to Webhooks"
msgstr ""
@@ -10125,9 +10391,6 @@ msgstr "Перейти до Ñередовищ"
msgid "Go to file"
msgstr "Перейти до файлу"
-msgid "Go to file (MRs only)"
-msgstr "Перейти до файлу (лише запити на злиттÑ)"
-
msgid "Go to file permalink (while viewing a file)"
msgstr "Перейти до поÑтійного поÑÐ¸Ð»Ð°Ð½Ð½Ñ Ð½Ð° файл (при переглÑді)"
@@ -10213,7 +10476,7 @@ msgid "Golden Tanuki"
msgstr "Золотий Танукі"
msgid "Google Cloud Platform"
-msgstr ""
+msgstr "Google Cloud Platform"
msgid "Google Code import"
msgstr "Імпорт з Google Code"
@@ -10339,7 +10602,7 @@ msgid "Group maintainers can register group runners in the %{link}"
msgstr "Керівники групи можуть зареєÑтрувати групові runner'и через %{link}"
msgid "Group members"
-msgstr ""
+msgstr "КориÑтувачі групи"
msgid "Group name"
msgstr "Ðазва групи"
@@ -10462,7 +10725,7 @@ msgid "GroupSAML|Manage your group’s membership while adding another level of
msgstr "Керуйте членÑтвом у вашій групі додаючи ще один рівень безпеки із SAML."
msgid "GroupSAML|Members"
-msgstr ""
+msgstr "УчаÑники"
msgid "GroupSAML|Members will be forwarded here when signing in to your group. Get this from your identity provider, where it can also be called \"SSO Service Location\", \"SAML Token Issuance Endpoint\", or \"SAML 2.0/W-Federation URL\"."
msgstr "УчаÑники будуть перенаправлені Ñюди, коли будуть заходити до вашої групи. Отримайте його від Ñвого провайдера ідентифікації, де вона також може називатиÑÑ \"SSO Service Location\", \"SAML Token Issuance Endpoint\", або \"SAML 2.0/W-Federation URL\"."
@@ -10572,6 +10835,9 @@ msgstr "Вимкнути ÑÐ¿Ð¾Ð²Ñ–Ñ‰ÐµÐ½Ð½Ñ ÐµÐ»ÐµÐºÑ‚Ñ€Ð¾Ð½Ð½Ð¾ÑŽ поштоÑ
msgid "GroupSettings|Disable group mentions"
msgstr ""
+msgid "GroupSettings|Export group"
+msgstr ""
+
msgid "GroupSettings|If the parent group's visibility is lower than the group current visibility, visibility levels for subgroups and projects will be changed to match the new parent group's visibility."
msgstr "Якщо видиміÑÑ‚ÑŒ батьківÑької групи нижча, за поточну видиміÑÑ‚ÑŒ групи, тоді рівні видимоÑÑ‚Ñ– Ð´Ð»Ñ Ð¿Ñ–Ð´Ð³Ñ€ÑƒÐ¿ та проектів будуть змінені, щоб відповідати новій видимоÑÑ‚Ñ– батьківÑької групи."
@@ -10650,6 +10916,9 @@ msgstr "Групи"
msgid "Groups (%{count})"
msgstr "Групи (%{count})"
+msgid "Groups (%{groups})"
+msgstr ""
+
msgid "Groups can also be nested by creating %{subgroup_docs_link_start}subgroups%{subgroup_docs_link_end}."
msgstr "Групи також можуть бути вкладеними при викориÑтанні %{subgroup_docs_link_start}підгруп%{subgroup_docs_link_end}."
@@ -10747,7 +11016,7 @@ msgid "Header message"
msgstr "ÐŸÐ¾Ð²Ñ–Ð´Ð¾Ð¼Ð»ÐµÐ½Ð½Ñ Ð·Ð°Ð³Ð¾Ð»Ð¾Ð²ÐºÐ°"
msgid "Health"
-msgstr ""
+msgstr "Здоров'Ñ"
msgid "Health Check"
msgstr "Перевірка ПрацездатноÑÑ‚Ñ–"
@@ -10842,7 +11111,7 @@ msgid "Hiding all labels"
msgstr "ÐŸÑ€Ð¸Ñ…Ð¾Ð²ÑƒÐ²Ð°Ð½Ð½Ñ Ð²ÑÑ–Ñ… міток"
msgid "High or unknown vulnerabilities present"
-msgstr ""
+msgstr "ПриÑутні вразливоÑÑ‚Ñ– виÑокого або невідомого рівнÑ"
msgid "Highest number of requests per minute for each raw path, default to 300. To disable throttling set to 0."
msgstr "Ðайвища кількіÑÑ‚ÑŒ запитів за хвилину Ð´Ð»Ñ ÐºÐ¾Ð¶Ð½Ð¾Ð³Ð¾ необробленого шлÑÑ…, за замовчуваннÑм Ñкладає 300. Ð”Ð»Ñ Ð²Ð¸Ð¼ÐºÐ½ÐµÐ½Ð½Ñ Ð¾Ð±Ð¼ÐµÐ¶ÐµÐ½Ð½Ñ Ð²Ñтановіть в 0."
@@ -10919,15 +11188,12 @@ msgstr "ID"
msgid "ID:"
msgstr "ID:"
-msgid "IDE|Allow live previews of JavaScript projects in the Web IDE using CodeSandbox client side evaluation."
-msgstr "Дозволити попередній переглÑд JavaScript-проектів у веб-IDE за допомогою Ð²Ð¸ÐºÐ¾Ð½Ð°Ð½Ð½Ñ Ð½Ð° клієнті CodeSandbox."
+msgid "IDE|Allow live previews of JavaScript projects in the Web IDE using CodeSandbox Live Preview."
+msgstr ""
msgid "IDE|Back"
msgstr "Ðазад"
-msgid "IDE|Client side evaluation"
-msgstr "Ð’Ð¸ÐºÐ¾Ð½Ð°Ð½Ð½Ñ Ð½Ð° клієнті"
-
msgid "IDE|Commit"
msgstr "Коміт"
@@ -10991,6 +11257,9 @@ msgstr ""
msgid "If any job surpasses this timeout threshold, it will be marked as failed. Human readable time input language is accepted like \"1 hour\". Values without specification represent seconds."
msgstr "Якщо будь-Ñке Ð·Ð°Ð²Ð´Ð°Ð½Ð½Ñ Ð¿ÐµÑ€ÐµÐ²Ð¸Ñ‰Ð¸Ñ‚ÑŒ цей чаÑовий поріг, воно буде відмічене Ñк невдале. Ð”Ð»Ñ Ð²Ð²Ð¾Ð´Ñƒ чаÑу підтримуєтьÑÑ \"людÑкий\" формат, наприклад \"1 hour\" (1 година). Ð—Ð½Ð°Ñ‡ÐµÐ½Ð½Ñ Ð±ÐµÐ· одиниць виміру предÑтавлÑÑŽÑ‚ÑŒ Ñекунди."
+msgid "If blank, set allowable lifetime to %{instance_level_policy_in_words}, as defined by the instance admin. Once set, existing tokens for users in this group may be revoked."
+msgstr ""
+
msgid "If checked, group owners can manage LDAP group links and LDAP member overrides"
msgstr "Якщо увімкнено, тоді влаÑники груп зможуть керувати зв'Ñзками груп LDAP та перевизначеннÑм членів LDAP"
@@ -11030,6 +11299,9 @@ msgstr ""
msgid "If you lose your recovery codes you can generate new ones, invalidating all previous codes."
msgstr "Якщо ви втратите коди відновленнÑ, ви можете Ñтворити нові, Ñ– вÑÑ– попередні коди Ð²Ñ–Ð´Ð½Ð¾Ð²Ð»ÐµÐ½Ð½Ñ Ð±ÑƒÐ´ÑƒÑ‚ÑŒ недійÑними."
+msgid "If you reach 100%% storage capacity, you will not be able to: %{base_message}"
+msgstr ""
+
msgid "If your HTTP repository is not publicly accessible, add your credentials."
msgstr "Якщо ваш HTTP-репозиторій не Ñ” загальнодоÑтупним, додайте Ñвої облікові дані."
@@ -11043,7 +11315,7 @@ msgid "Ignored"
msgstr ""
msgid "Image: %{image}"
-msgstr ""
+msgstr "Образ: %{image}"
msgid "ImageDiffViewer|2-up"
msgstr "2 поруч"
@@ -11141,6 +11413,9 @@ msgstr "Імпорт репозиторіїв з GitHub"
msgid "Import repository"
msgstr "Імпорт репозиторію"
+msgid "Import started by: %{importInitiator}"
+msgstr ""
+
msgid "Import tasks"
msgstr "Імпорт завдань"
@@ -11207,6 +11482,9 @@ msgstr ""
msgid "In order to tailor your experience with GitLab we<br>would like to know a bit more about you."
msgstr "Ð”Ð»Ñ Ñ‚Ð¾Ð³Ð¾, щоб адаптувати GitLab до Ð²Ð°Ñ Ð½Ð°Ð¼<br>потрібно більше про Ð²Ð°Ñ Ð´Ñ–Ð·Ð½Ð°Ñ‚Ð¸ÑÑ."
+msgid "In progress"
+msgstr ""
+
msgid "In the next step, you'll be able to select the projects you want to import."
msgstr "Ðа наÑтупному кроці ви зможете вибрати проекти, Ñкі хочете імпортувати."
@@ -11332,7 +11610,7 @@ msgid "Instance license"
msgstr "Ð›Ñ–Ñ†ÐµÐ½Ð·Ñ–Ñ Ñ–Ð½ÑтанÑу"
msgid "Integration"
-msgstr ""
+msgstr "ІнтеграціÑ"
msgid "Integration Settings"
msgstr ""
@@ -11413,7 +11691,7 @@ msgid "Invalid input, please avoid emojis"
msgstr "Ðекорректний ввід, будь лаÑка, уникайте Ñмайликів"
msgid "Invalid login or password"
-msgstr ""
+msgstr "Ðеправильний логін або пароль"
msgid "Invalid pin code"
msgstr "Ðеправильний PIN-код"
@@ -11556,15 +11834,15 @@ msgstr "РеєÑÑ‚Ñ€ задач YouTrack"
msgid "Issues"
msgstr "Задачі"
-msgid "Issues / Merge Requests"
-msgstr "Задачі / запити на злиттÑ"
-
msgid "Issues Analytics"
msgstr ""
msgid "Issues Rate Limits"
msgstr ""
+msgid "Issues and Merge Requests"
+msgstr ""
+
msgid "Issues can be bugs, tasks or ideas to be discussed. Also, issues are searchable and filterable."
msgstr "Задачі можуть бути помилками, нагадуваннÑми чи ідеÑми Ð´Ð»Ñ Ð¾Ð±Ð³Ð¾Ð²Ð¾Ñ€ÐµÐ½Ð½Ñ. Крім того, задачі доÑтупні Ð´Ð»Ñ Ð¿Ð¾ÑˆÑƒÐºÑƒ та фільтруваннÑ."
@@ -11586,11 +11864,11 @@ msgstr "ПіÑÐ»Ñ ÑÑ‚Ð²Ð¾Ñ€ÐµÐ½Ð½Ñ Ð·Ð°Ð´Ð°Ñ‡ Ð´Ð»Ñ Ð²Ð°ÑˆÐ¸Ñ… проектÑ
msgid "IssuesAnalytics|Avg/Month:"
msgstr "Ð’ Ñередньому за міÑÑць:"
-msgid "IssuesAnalytics|Issues created"
-msgstr "Створені задачі"
+msgid "IssuesAnalytics|Issues opened"
+msgstr ""
-msgid "IssuesAnalytics|Issues created per month"
-msgstr "Створені задачі за міÑÑць"
+msgid "IssuesAnalytics|Issues opened per month"
+msgstr ""
msgid "IssuesAnalytics|Last 12 months"
msgstr "ОÑтанні 12 міÑÑців"
@@ -11643,6 +11921,9 @@ msgstr ""
msgid "Jira integration not configured."
msgstr ""
+msgid "Jira project: %{importProject}"
+msgstr ""
+
msgid "JiraService|Events for %{noteable_model_name} are disabled."
msgstr "Події Ð´Ð»Ñ %{noteable_model_name} вимкнені."
@@ -12034,6 +12315,9 @@ msgstr "ОÑтанній конвеєр Ð´Ð»Ñ Ð¾Ñтаннього коміту
msgid "Lead"
msgstr "Керувати"
+msgid "Lead Time"
+msgstr ""
+
msgid "Learn GitLab"
msgstr "ДізнатиÑÑ Ð¿Ñ€Ð¾ GitLab"
@@ -12046,6 +12330,9 @@ msgstr "ДізнайтеÑÑ Ñк %{link_start}зробити внеÑок до
msgid "Learn how to %{no_packages_link_start}publish and share your packages%{no_packages_link_end} with GitLab."
msgstr "ДізнайтеÑÑ Ñк %{no_packages_link_start}публікувати та ділитиÑÑ Ð²Ð°ÑˆÐ¸Ð¼Ð¸ пакетами%{no_packages_link_end} через GitLab."
+msgid "Learn how to enable synchronization"
+msgstr ""
+
msgid "Learn more"
msgstr "ДізнатиÑÑ Ð±Ñ–Ð»ÑŒÑˆÐµ"
@@ -12387,11 +12674,8 @@ msgstr "СпиÑко репозиторіїв вашого Bitbucket Server"
msgid "Live preview"
msgstr "Попередній переглÑд"
-msgid "Load more vulnerabilities"
-msgstr ""
-
msgid "Loading"
-msgstr ""
+msgstr "ЗавантаженнÑ"
msgid "Loading contribution stats for group members"
msgstr "Ð—Ð°Ð²Ð°Ð½Ñ‚Ð°Ð¶ÐµÐ½Ð½Ñ ÑтатиÑтики учаÑників групи"
@@ -12478,7 +12762,7 @@ msgid "Logs|To see the logs, deploy your code to an environment."
msgstr ""
msgid "Low vulnerabilities present"
-msgstr ""
+msgstr "ПриÑутні вразливоÑÑ‚Ñ– низького рівнÑ"
msgid "MB"
msgstr "МБ"
@@ -12493,7 +12777,7 @@ msgid "MR widget|Take a look at our %{beginnerLinkStart}Beginner's Guide to Cont
msgstr ""
msgid "MR widget|The pipeline will now run automatically every time you commit code. Pipelines are useful for deploying static web pages, detecting vulnerabilities in dependencies, static or dynamic application security testing (SAST and DAST), and so much more!"
-msgstr ""
+msgstr "Цей конвеєр тепер буде запуÑкатиÑÑ ÐºÐ¾Ð¶Ð½Ð¾Ð³Ð¾ разу при коміті коду. Конвеєри можуть бути кориÑними Ð´Ð»Ñ Ñ€Ð¾Ð·Ð³Ð¾Ñ€Ñ‚Ð°Ð½Ð½Ñ Ñтатичних вебÑторінок, виÑÐ²Ð»ÐµÐ½Ð½Ñ Ð²Ñ€Ð°Ð·Ð»Ð¸Ð²Ð¾Ñтей в залежноÑÑ‚ÑÑ…, Ñтатичному або динамічному теÑтуванні заÑтоÑунків (SAST та DAST), Ñ– багато чого іншого!"
msgid "MRApprovals|Approvals"
msgstr ""
@@ -12514,7 +12798,7 @@ msgid "Made this issue confidential."
msgstr "Цю задачу зроблено конфіденційною."
msgid "Maintenance mode"
-msgstr ""
+msgstr "Режим обÑлуговуваннÑ"
msgid "Make and review changes in the browser with the Web IDE"
msgstr "ЗдійÑнити та переглÑнути зміни в браузері за допомогою веб-IDE"
@@ -12540,9 +12824,6 @@ msgstr ""
msgid "Manage"
msgstr "Керувати"
-msgid "Manage Git repositories with fine-grained access controls that keep your code secure. Perform code reviews and enhance collaboration with merge requests. Each project can also have an issue tracker and a wiki."
-msgstr "ÐšÐµÑ€ÑƒÐ²Ð°Ð½Ð½Ñ Ñ€ÐµÐ¿Ð¾Ð·Ð¸Ñ‚Ð¾Ñ€Ñ–Ñми Git за допомогою детального контролю доÑтупу збереже ваш код в безпеці. Виконуйте переглÑд коду та покращуйте Ñпівпрацю за допомогою запитів на злиттÑ. Кожен проект може також мати трекер задач та вікі."
-
msgid "Manage Web IDE features"
msgstr "ÐšÐµÑ€ÑƒÐ²Ð°Ð½Ð½Ñ Ñ„ÑƒÐ½ÐºÑ†Ñ–Ñми веб-IDE"
@@ -12573,6 +12854,9 @@ msgstr "ÐšÐµÑ€ÑƒÐ²Ð°Ð½Ð½Ñ Ð´Ð²Ð¾Ñ„Ð°ÐºÑ‚Ð¾Ñ€Ð½Ð¾ÑŽ автентифікаціє
msgid "Manage your license"
msgstr ""
+msgid "Managed Account"
+msgstr ""
+
msgid "Manifest"
msgstr "МаніфеÑÑ‚"
@@ -12787,7 +13071,7 @@ msgid "Median"
msgstr "Медіана"
msgid "Medium vulnerabilities present"
-msgstr ""
+msgstr "ПриÑутні вразливоÑÑ‚Ñ– низького рівнÑ"
msgid "Member lock"
msgstr "Ð‘Ð»Ð¾ÐºÑƒÐ²Ð°Ð½Ð½Ñ ÑƒÑ‡Ð°Ñників"
@@ -12837,6 +13121,9 @@ msgstr "Запит на Ð·Ð»Ð¸Ñ‚Ñ‚Ñ Ð±ÑƒÐ»Ð¾ Ñтворено"
msgid "Merge Requests in Review"
msgstr ""
+msgid "Merge automatically (%{strategy})"
+msgstr ""
+
msgid "Merge commit message"
msgstr "ÐŸÐ¾Ð²Ñ–Ð´Ð¾Ð¼Ð»ÐµÐ½Ð½Ñ Ð´Ð»Ñ ÐºÐ¾Ð¼Ñ–Ñ‚Ñƒ-злиттÑ"
@@ -12990,6 +13277,12 @@ msgstr "Злито"
msgid "Merged branches are being deleted. This can take some time depending on the number of branches. Please refresh the page to see changes."
msgstr "Злиті гілки в процеÑÑ– видаленнÑ. Це може зайнÑти деÑкий Ñ‡Ð°Ñ Ð² залежноÑÑ‚Ñ– від Ñ—Ñ… кількоÑÑ‚Ñ–. Будь лаÑка, оновіть Ñторінку, щоб побачити зміни."
+msgid "Merged this merge request."
+msgstr ""
+
+msgid "Merges this merge request immediately."
+msgstr ""
+
msgid "Merges this merge request when the pipeline succeeds."
msgstr "Зливає цей запит на злиттÑ, коли конвеєр завершуєтьÑÑ ÑƒÑпішно."
@@ -13003,7 +13296,7 @@ msgid "Method"
msgstr "Метод"
msgid "Method:"
-msgstr ""
+msgstr "Метод:"
msgid "Metric was successfully added."
msgstr "Метрику уÑпішно додано."
@@ -13012,7 +13305,7 @@ msgid "Metric was successfully updated."
msgstr "Метрику уÑпішно оновлено."
msgid "Metric:"
-msgstr ""
+msgstr "Метрика:"
msgid "MetricChart|Please select a metric"
msgstr "Будь лаÑка, виберіть метрику"
@@ -13099,12 +13392,15 @@ msgstr[1] ""
msgstr[2] ""
msgstr[3] ""
-msgid "Metrics|Environment"
-msgstr "Середовище"
+msgid "Metrics|Expand panel"
+msgstr ""
msgid "Metrics|For grouping similar metrics"
msgstr "Ð”Ð»Ñ Ð³Ñ€ÑƒÐ¿ÑƒÐ²Ð°Ð½Ð½Ñ Ð¿Ð¾Ð´Ñ–Ð±Ð½Ð¸Ñ… метрик"
+msgid "Metrics|Go back (Esc)"
+msgstr ""
+
msgid "Metrics|Invalid time range, please verify."
msgstr ""
@@ -13135,9 +13431,6 @@ msgstr "Ð”Ð¾ÐºÑƒÐ¼ÐµÐ½Ñ‚Ð°Ñ†Ñ–Ñ Ð¿Ð¾ запитам Prometheus"
msgid "Metrics|Refresh dashboard"
msgstr ""
-msgid "Metrics|Show last"
-msgstr "Показати оÑтанні"
-
msgid "Metrics|There was an error creating the dashboard."
msgstr ""
@@ -13184,7 +13477,7 @@ msgid "Metrics|Validating query"
msgstr ""
msgid "Metrics|Values"
-msgstr ""
+msgstr "ЗначеннÑ"
msgid "Metrics|View logs"
msgstr ""
@@ -13216,6 +13509,9 @@ msgstr "напр. зап/Ñек"
msgid "Microsoft Azure"
msgstr "Microsoft Azure"
+msgid "Middleman project with Static Site Editor support"
+msgstr ""
+
msgid "Migrated %{success_count}/%{total_count} files."
msgstr "ПеренеÑено %{success_count}/%{total_count} файлів."
@@ -13627,7 +13923,7 @@ msgid "New project"
msgstr "Ðовий проект"
msgid "New release"
-msgstr ""
+msgstr "Ðовий реліз"
msgid "New requirement"
msgstr ""
@@ -13665,11 +13961,11 @@ msgstr "За замовчуваннÑм щойно зареєÑтровані к
msgid "Next"
msgstr "Далі"
-msgid "Next file in diff (MRs only)"
-msgstr "ÐаÑтупний файл у різниці (лише Ð´Ð»Ñ Ð·Ð°Ð¿Ð¸Ñ‚Ñ–Ð² на злиттÑ)"
+msgid "Next file in diff"
+msgstr ""
-msgid "Next unresolved discussion (MRs only)"
-msgstr "ÐаÑтупне незакрите Ð¾Ð±Ð³Ð¾Ð²Ð¾Ñ€ÐµÐ½Ð½Ñ (лише в запитах на злиттÑ)"
+msgid "Next unresolved discussion"
+msgstr ""
msgid "Nickname"
msgstr "ПÑевдонім"
@@ -13728,6 +14024,9 @@ msgstr "Ðемає змін"
msgid "No changes between %{ref_start}%{source_branch}%{ref_end} and %{ref_start}%{target_branch}%{ref_end}"
msgstr "Ðемає змін між %{ref_start}%{source_branch}%{ref_end} та %{ref_start}%{target_branch}%{ref_end}"
+msgid "No child epics match applied filters"
+msgstr ""
+
msgid "No connection could be made to a Gitaly Server, please check your logs!"
msgstr "Ðеможливо з'єднатиÑÑŒ із Ñервером Gitaly, будь лаÑка, перевірте логи!"
@@ -13866,17 +14165,8 @@ msgstr ""
msgid "No thanks, don't show this again"
msgstr ""
-msgid "No vulnerabilities found for this group"
-msgstr ""
-
-msgid "No vulnerabilities found for this pipeline"
-msgstr ""
-
-msgid "No vulnerabilities found for this project"
-msgstr "Ð’ цьому проєкті не знайдено вразливоÑтей"
-
msgid "No vulnerabilities present"
-msgstr ""
+msgstr "ВразливоÑÑ‚Ñ– відÑутні"
msgid "No webhooks found, add one in the form above."
msgstr "Ðе знайдено жодного вебхука, додайте його у формі вище."
@@ -13998,6 +14288,9 @@ msgstr ""
msgid "Nothing to preview."
msgstr "Дані Ð´Ð»Ñ Ð¿ÐµÑ€ÐµÐ³Ð»Ñду відÑутні."
+msgid "Nothing to synchronize"
+msgstr ""
+
msgid "Notification events"
msgstr "ÐŸÐ¾Ð²Ñ–Ð´Ð¾Ð¼Ð»ÐµÐ½Ð½Ñ Ð¿Ñ€Ð¾ події"
@@ -14169,6 +14462,9 @@ msgstr "ПіÑÐ»Ñ Ñ–Ð¼Ð¿Ð¾Ñ€Ñ‚Ñƒ, репозиторії можуть бути
msgid "Once removed, the fork relationship cannot be restored and you will no longer be able to send merge requests to the source."
msgstr "ПіÑÐ»Ñ Ð²Ð¸Ð´Ð°Ð»ÐµÐ½Ð½Ñ Ð·Ð²â€™Ñзок форку не може бути відновлений Ñ– ви більше не зможете відправлÑти запити на Ð·Ð»Ð¸Ñ‚Ñ‚Ñ Ñƒ вихідний проект."
+msgid "Once the exported file is ready you can download it from this page."
+msgstr ""
+
msgid "Once the exported file is ready, you will receive a notification email with a download link, or you can download it from this page."
msgstr "ПіÑÐ»Ñ Ñ‚Ð¾Ð³Ð¾, Ñк екÑпортований файл буде готовий, ви отримаєте ÑÐ¿Ð¾Ð²Ñ–Ñ‰ÐµÐ½Ð½Ñ ÐµÐ»ÐµÐºÑ‚Ñ€Ð¾Ð½Ð½Ð¾ÑŽ поштою із поÑиланнÑм Ð´Ð»Ñ Ð·Ð°Ð²Ð°Ð½Ñ‚Ð°Ð¶ÐµÐ½Ð½Ñ Ð°Ð±Ð¾ можете завантажити його з цієї Ñторінки."
@@ -14272,9 +14568,6 @@ msgstr "Відкрити в неформатованому виглÑді"
msgid "Open sidebar"
msgstr "Розгорніть бічну панель"
-msgid "Open source software to collaborate on code"
-msgstr "Відкрите програмне Ð·Ð°Ð±ÐµÐ·Ð¿ÐµÑ‡ÐµÐ½Ð½Ñ Ð´Ð»Ñ Ñпільної роботи над кодом"
-
msgid "Open: %{openIssuesCount}"
msgstr ""
@@ -14377,6 +14670,9 @@ msgstr ""
msgid "OutdatedBrowser|You can provide feedback %{feedback_link_start}on this issue%{feedback_link_end} or via your usual support channels."
msgstr ""
+msgid "Overridden"
+msgstr ""
+
msgid "Overview"
msgstr "ОглÑд"
@@ -14428,6 +14724,9 @@ msgstr ""
msgid "PackageRegistry|Conan Command"
msgstr ""
+msgid "PackageRegistry|Copy .pypirc content"
+msgstr ""
+
msgid "PackageRegistry|Copy Conan Command"
msgstr ""
@@ -14449,6 +14748,9 @@ msgstr ""
msgid "PackageRegistry|Copy NuGet Setup Command"
msgstr ""
+msgid "PackageRegistry|Copy Pip command"
+msgstr ""
+
msgid "PackageRegistry|Copy and paste this inside your %{codeStart}pom.xml%{codeEnd} %{codeStart}dependencies%{codeEnd} block."
msgstr ""
@@ -14482,6 +14784,12 @@ msgstr ""
msgid "PackageRegistry|For more information on the NuGet registry, %{linkStart}see the documentation%{linkEnd}."
msgstr ""
+msgid "PackageRegistry|For more information on the PyPi registry, %{linkStart}see the documentation%{linkEnd}."
+msgstr ""
+
+msgid "PackageRegistry|If you haven't already done so, you will need to add the below to your %{codeStart}.pypirc%{codeEnd} file."
+msgstr ""
+
msgid "PackageRegistry|If you haven't already done so, you will need to add the below to your %{codeStart}pom.xml%{codeEnd} file."
msgstr ""
@@ -14491,6 +14799,9 @@ msgstr "Ð’ÑтановленнÑ"
msgid "PackageRegistry|Learn how to %{noPackagesLinkStart}publish and share your packages%{noPackagesLinkEnd} with GitLab."
msgstr "ДізнайтеÑÑ Ñк %{noPackagesLinkStart}публікувати та ділитиÑÑ Ð²Ð°ÑˆÐ¸Ð¼Ð¸ пакетами%{noPackagesLinkEnd} через GitLab."
+msgid "PackageRegistry|Manually Published"
+msgstr ""
+
msgid "PackageRegistry|Maven"
msgstr ""
@@ -14509,12 +14820,18 @@ msgstr ""
msgid "PackageRegistry|NuGet Command"
msgstr ""
+msgid "PackageRegistry|Pip Command"
+msgstr ""
+
msgid "PackageRegistry|Pipeline %{linkStart}%{linkEnd} triggered %{timestamp} by %{author}"
msgstr ""
msgid "PackageRegistry|Published to the repository at %{timestamp}"
msgstr ""
+msgid "PackageRegistry|PyPi"
+msgstr ""
+
msgid "PackageRegistry|Registry Setup"
msgstr "ÐÐ°Ð»Ð°ÑˆÑ‚ÑƒÐ²Ð°Ð½Ð½Ñ Ñ€ÐµÑ”Ñтру"
@@ -14551,19 +14868,25 @@ msgstr ""
msgid "PackageRegistry|npm"
msgstr "npm"
+msgid "PackageRegistry|published by %{author}"
+msgstr ""
+
msgid "PackageRegistry|yarn"
msgstr "yarn"
msgid "PackageType|Conan"
-msgstr ""
+msgstr "Conan"
msgid "PackageType|Maven"
-msgstr ""
+msgstr "Maven"
msgid "PackageType|NPM"
-msgstr ""
+msgstr "NPM"
msgid "PackageType|NuGet"
+msgstr "NuGet"
+
+msgid "PackageType|PyPi"
msgstr ""
msgid "Packages"
@@ -14600,10 +14923,10 @@ msgid "Pagination|Last »"
msgstr "ОÑÑ‚Ð°Ð½Ð½Ñ Â»"
msgid "Pagination|Next"
-msgstr ""
+msgstr "ÐаÑтупна"
msgid "Pagination|Prev"
-msgstr ""
+msgstr "ПопереднÑ"
msgid "Pagination|« First"
msgstr "« Перша"
@@ -14615,7 +14938,7 @@ msgid "Parameter \"job_id\" cannot exceed length of %{job_id_max_size}"
msgstr ""
msgid "Parent"
-msgstr ""
+msgstr "Джерело"
msgid "Parent epic doesn't exist."
msgstr "БатьківÑький епік не Ñ–Ñнує."
@@ -14659,6 +14982,9 @@ msgstr "Пароль уÑпішно оновлено. Будь лаÑка, увÑ
msgid "Past due"
msgstr "ПроÑтрочені"
+msgid "Paste a machine public key here. Read more about how to generate it"
+msgstr ""
+
msgid "Paste a machine public key here. Read more about how to generate it %{link_start}here%{link_end}"
msgstr "Ð’Ñтавте Ñюди відкритий ключ машини. ДізнайтеÑÑ Ð±Ñ–Ð»ÑŒÑˆÐµ про те, Ñк його згенерувати %{link_start}тут%{link_end}"
@@ -14674,9 +15000,6 @@ msgstr "Ð’Ñтавте Ñвій публічний ключ SSH, Ñкий заз
msgid "Path"
msgstr "ШлÑÑ…"
-msgid "Path, transfer, remove"
-msgstr "ШлÑÑ…, перенеÑеннÑ, видаленнÑ"
-
msgid "Path:"
msgstr "ШлÑÑ…:"
@@ -14707,8 +15030,8 @@ msgstr ""
msgid "Percentage"
msgstr ""
-msgid "Perform advanced options such as changing path, transferring, or removing the group."
-msgstr "Виконуйте такі розширені операції, Ñк зміна шлÑху, перенеÑÐµÐ½Ð½Ñ Ñ‡Ð¸ Ð²Ð¸Ð´Ð°Ð»ÐµÐ½Ð½Ñ Ð³Ñ€ÑƒÐ¿Ð¸."
+msgid "Perform advanced options such as changing path, transferring, exporting, or removing the group."
+msgstr ""
msgid "Perform common operations on GitLab project"
msgstr "Виконати звичайні операції на проекті GitLab"
@@ -14788,9 +15111,6 @@ msgstr "Розклади Конвеєрів"
msgid "Pipeline minutes quota"
msgstr "Квота хвилин Ð´Ð»Ñ ÐºÐ¾Ð½Ð²ÐµÑ”Ñ€Ñ–Ð²"
-msgid "Pipeline quota"
-msgstr "Квота на конвеєри"
-
msgid "Pipeline subscriptions"
msgstr ""
@@ -14800,6 +15120,9 @@ msgstr "Тригери конвеєру"
msgid "Pipeline: %{status}"
msgstr "Конвеєр: %{status}"
+msgid "PipelineCharts|CI / CD Analytics"
+msgstr ""
+
msgid "PipelineCharts|Failed:"
msgstr "Ðевдалі:"
@@ -14878,15 +15201,6 @@ msgstr "Конвеєри Ð´Ð»Ñ Ð·Ð°Ð¿Ð¸Ñ‚Ñ–Ð² на Ð·Ð»Ð¸Ñ‚Ñ‚Ñ Ð½Ð°Ð»Ð°ÑˆÑ‚Ð¾Ð
msgid "Pipelines settings for '%{project_name}' were successfully updated."
msgstr "ÐÐ°Ð»Ð°ÑˆÑ‚ÑƒÐ²Ð°Ð½Ð½Ñ ÐºÐ¾Ð½Ð²ÐµÑ”Ñ€Ñ–Ð² Ð´Ð»Ñ \"%{project_name}\" уÑпішно оновлено."
-msgid "Pipelines| to purchase more minutes."
-msgstr " Ð´Ð»Ñ Ð¿Ñ€Ð¸Ð´Ð±Ð°Ð½Ð½Ñ Ð´Ð¾Ð´Ð°Ñ‚ÐºÐ¾Ð²Ð¸Ñ… хвилин."
-
-msgid "Pipelines|%{namespace_name} has exceeded its pipeline minutes quota."
-msgstr "%{namespace_name} перевищив Ñвою квоту хвилин Ð´Ð»Ñ ÐºÐ¾Ð½Ð²ÐµÑ”Ñ€Ñ–Ð²."
-
-msgid "Pipelines|%{namespace_name} has less than %{notification_level}%% of CI minutes available."
-msgstr "%{namespace_name} має менш ніж %{notification_level}%% доÑтупних хвилин Ð´Ð»Ñ ÐºÐ¾Ð½Ð²ÐµÑ”Ñ€Ñ–Ð²."
-
msgid "Pipelines|API"
msgstr "API"
@@ -14908,12 +15222,15 @@ msgstr "Безперервно Ñ–Ð½Ñ‚ÐµÐ³Ñ€Ð°Ñ†Ñ–Ñ Ð´Ð¾Ð¿Ð¾Ð¼Ð°Ð³Ð°Ñ” знахÐ
msgid "Pipelines|Get started with Pipelines"
msgstr "Розпочати роботу з Конвеєрами"
+msgid "Pipelines|Group %{namespace_name} has %{percentage}%% or less Shared Runner Pipeline minutes remaining. Once it runs out, no new jobs or pipelines in its projects will run."
+msgstr ""
+
+msgid "Pipelines|Group %{namespace_name} has exceeded its pipeline minutes quota. Unless you buy additional pipeline minutes, no new jobs or pipelines in its projects will run."
+msgstr ""
+
msgid "Pipelines|Loading Pipelines"
msgstr "Ð—Ð°Ð²Ð°Ð½Ñ‚Ð°Ð¶ÐµÐ½Ð½Ñ ÐºÐ¾Ð½Ð²ÐµÑ”Ñ€Ñ–Ð²"
-msgid "Pipelines|Pipelines will not run anymore on shared Runners."
-msgstr "Конвеєри більше не будуть запуÑкатиÑÑ Ð½Ð° загальних runner'ах."
-
msgid "Pipelines|Project cache successfully reset."
msgstr "Кеш проекту уÑпішно очищено."
@@ -14951,7 +15268,7 @@ msgid "Pipeline|Coverage"
msgstr "ПокриттÑ"
msgid "Pipeline|Date"
-msgstr ""
+msgstr "Дата"
msgid "Pipeline|Detached merge request pipeline"
msgstr "Відділений конвеєр запиту на злиттÑ"
@@ -14975,7 +15292,7 @@ msgid "Pipeline|Pipeline"
msgstr "Конвеєр"
msgid "Pipeline|Pipelines"
-msgstr ""
+msgstr "Конвеєр"
msgid "Pipeline|Run Pipeline"
msgstr "ЗапуÑтити Конвеєр"
@@ -15142,6 +15459,9 @@ msgstr "Будь лаÑка, повторно введіть адреÑу еле
msgid "Please select"
msgstr "Будь лаÑка, виберіть"
+msgid "Please select a Jira project"
+msgstr ""
+
msgid "Please select a country"
msgstr "Будь лаÑка, виберіть країну"
@@ -15187,6 +15507,9 @@ msgstr "Будь лаÑка, почекайте поки ми з’єднуємÐ
msgid "Please wait while we import the repository for you. Refresh at will."
msgstr "Будь лаÑка, почекайте поки ми імпортуємо ваш репозиторій. Оновлюйте Ñторінку за бажаннÑм."
+msgid "Plugins directory is deprecated and will be removed in 14.0. Please move this file into /file_hooks directory."
+msgstr ""
+
msgid "Pod does not exist"
msgstr ""
@@ -15304,7 +15627,7 @@ msgstr ""
msgid "Prevent users from changing their profile name"
msgstr ""
-msgid "Prevent users from modifing merge request approvers list"
+msgid "Prevent users from modifying merge request approvers list"
msgstr ""
msgid "Prevent users from performing write operations on GitLab while performing maintenance."
@@ -15325,11 +15648,11 @@ msgstr "Попередній переглÑд кориÑного навантаÐ
msgid "Previous Artifacts"
msgstr "Попередні артефакти"
-msgid "Previous file in diff (MRs only)"
-msgstr "Попередній файл у різниці (лише запити на злиттÑ)"
+msgid "Previous file in diff"
+msgstr ""
-msgid "Previous unresolved discussion (MRs only)"
-msgstr "Попереднє незакрите Ð¾Ð±Ð³Ð¾Ð²Ð¾Ñ€ÐµÐ½Ð½Ñ (лише в запитах на злиттÑ)"
+msgid "Previous unresolved discussion"
+msgstr ""
msgid "Primary"
msgstr "Головний"
@@ -15361,6 +15684,9 @@ msgstr "Приватна група(и)"
msgid "Private profile"
msgstr "Приватний профіль"
+msgid "Private projects Minutes cost factor"
+msgstr ""
+
msgid "Private projects can be created in your personal namespace with:"
msgstr "Приватні проекти можуть бути Ñтворені у вашому перÑональному проÑторі імен з:"
@@ -15368,7 +15694,7 @@ msgid "Proceed"
msgstr "Продовжити"
msgid "Productivity"
-msgstr ""
+msgstr "ПродуктивніÑÑ‚ÑŒ"
msgid "Productivity Analytics"
msgstr "Ðналітика ПродуктивноÑÑ‚Ñ–"
@@ -16189,6 +16515,9 @@ msgstr "Вікі"
msgid "ProjectSettings|With GitLab Pages you can host your static websites on GitLab"
msgstr "За допомогою GitLab Pages ви можете розміщувати Ñвої Ñтатичні веб-Ñайти в GitLab"
+msgid "ProjectSettings|With Metrics Dashboard you can visualize this project performance metrics"
+msgstr ""
+
msgid "ProjectTemplates|.NET Core"
msgstr ".NET Core"
@@ -16252,14 +16581,17 @@ msgstr "Serverless Framework/JS"
msgid "ProjectTemplates|Spring"
msgstr "Spring"
+msgid "ProjectTemplates|Static Site Editor/Middleman"
+msgstr ""
+
msgid "ProjectTemplates|iOS (Swift)"
msgstr "iOS (Swift)"
msgid "Projects"
-msgstr "Проекти"
+msgstr "Проєкти"
msgid "Projects (%{count})"
-msgstr "Проекти (%{count})"
+msgstr "Проєкти (%{count})"
msgid "Projects Successfully Retrieved"
msgstr "Проекти уÑпішно отримано"
@@ -16471,15 +16803,15 @@ msgstr "Базова адреÑа Prometheus API, наприклад http://prom
msgid "PrometheusService|Prometheus is being automatically managed on your clusters"
msgstr "Prometheus автоматично налаштований на ваших клаÑтерах"
+msgid "PrometheusService|Select the Active checkbox to override the Auto Configuration with custom settings. If unchecked, Auto Configuration settings are used."
+msgstr ""
+
msgid "PrometheusService|These metrics will only be monitored after your first deployment to an environment"
msgstr "Ці метрики будуть збиратиÑÑ Ñ‚Ñ–Ð»ÑŒÐºÐ¸ піÑÐ»Ñ Ð¿ÐµÑ€ÑˆÐ¾Ð³Ð¾ Ñ€Ð¾Ð·Ð³Ð¾Ñ€Ñ‚Ð°Ð½Ð½Ñ Ð² ÑкомуÑÑŒ Ñеридовищі"
msgid "PrometheusService|Time-series monitoring service"
msgstr "Ð¡ÐµÑ€Ð²Ñ–Ñ Ð¼Ð¾Ð½Ñ–Ñ‚Ð¾Ñ€Ð¸Ð½Ð³Ñƒ чаÑових Ñ€Ñдів"
-msgid "PrometheusService|To enable manual configuration, uninstall Prometheus from your clusters"
-msgstr "Ð”Ð»Ñ Ð¼Ð¾Ð¶Ð»Ð¸Ð²Ð¾ÑÑ‚Ñ– ручного налаштуваннÑ, видаліть Prometheus із ваших клаÑтерів"
-
msgid "PrometheusService|To enable the installation of Prometheus on your clusters, deactivate the manual configuration below"
msgstr "Ð”Ð»Ñ Ð¼Ð¾Ð¶Ð»Ð¸Ð²Ð¾ÑÑ‚Ñ– вÑÑ‚Ð°Ð½Ð¾Ð²Ð»ÐµÐ½Ð½Ñ Prometheus на ваші клаÑтери, деактивуйте ручні Ð½Ð°Ð»Ð°ÑˆÑ‚ÑƒÐ²Ð°Ð½Ð½Ñ Ð½Ð¸Ð¶Ñ‡Ðµ"
@@ -16663,6 +16995,9 @@ msgstr "ЗахиÑÑ‚ із вашого Ñередовища було знÑто"
msgid "Protip:"
msgstr "Підказка:"
+msgid "Protocol"
+msgstr ""
+
msgid "Provider"
msgstr "ПоÑтачальник"
@@ -16687,6 +17022,9 @@ msgstr "Публічні ключі Ñ€Ð¾Ð·Ð³Ð¾Ñ€Ñ‚Ð°Ð½Ð½Ñ (%{deploy_keys_count}
msgid "Public pipelines"
msgstr "Публічні конвеєри"
+msgid "Public projects Minutes cost factor"
+msgstr ""
+
msgid "Pull"
msgstr "Отримати (pull)"
@@ -16889,6 +17227,9 @@ msgstr[1] "ÐžÐ½Ð¾Ð²Ð»ÐµÐ½Ð½Ñ Ñ‡ÐµÑ€ÐµÐ· %d Ñекунди Ð´Ð»Ñ Ð²Ñ–Ð´Ð¾Ð±Ñ€Ð°
msgstr[2] "ÐžÐ½Ð¾Ð²Ð»ÐµÐ½Ð½Ñ Ñ‡ÐµÑ€ÐµÐ· %d Ñекунд Ð´Ð»Ñ Ð²Ñ–Ð´Ð¾Ð±Ñ€Ð°Ð¶ÐµÐ½Ð½Ñ Ð°ÐºÑ‚ÑƒÐ°Ð»ÑŒÐ½Ð¾Ð³Ð¾ Ñтану..."
msgstr[3] "ÐžÐ½Ð¾Ð²Ð»ÐµÐ½Ð½Ñ Ñ‡ÐµÑ€ÐµÐ· %d Ñекунд Ð´Ð»Ñ Ð²Ñ–Ð´Ð¾Ð±Ñ€Ð°Ð¶ÐµÐ½Ð½Ñ Ð°ÐºÑ‚ÑƒÐ°Ð»ÑŒÐ½Ð¾Ð³Ð¾ Ñтану..."
+msgid "Regenerate export"
+msgstr ""
+
msgid "Regenerate instance ID"
msgstr "Перегенерувати ідентифікатор інÑтанÑу"
@@ -17197,10 +17538,10 @@ msgid "Rename folder"
msgstr "Перейменувати папку"
msgid "Rename/Move"
-msgstr ""
+msgstr "Перейменувати/ПереміÑтити"
msgid "Reopen"
-msgstr ""
+msgstr "Повторне відкриттÑ"
msgid "Reopen epic"
msgstr "Повторне Ð²Ñ–Ð´ÐºÑ€Ð¸Ñ‚Ñ‚Ñ ÐµÐ¿Ñ–ÐºÑƒ"
@@ -17260,7 +17601,7 @@ msgid "Reported Resource Changes: %{addNum} to add, %{changeNum} to change, %{de
msgstr ""
msgid "Reporter"
-msgstr ""
+msgstr "Репортер"
msgid "Reporting"
msgstr "ЗвітуваннÑ"
@@ -17268,9 +17609,31 @@ msgstr "ЗвітуваннÑ"
msgid "Reports|%{combinedString} and %{resolvedString}"
msgstr ""
+msgid "Reports|Accessibility scanning detected %d issue for the source branch only"
+msgid_plural "Reports|Accessibility scanning detected %d issues for the source branch only"
+msgstr[0] ""
+msgstr[1] ""
+msgstr[2] ""
+msgstr[3] ""
+
+msgid "Reports|Accessibility scanning detected no issues for the source branch only"
+msgstr ""
+
+msgid "Reports|Accessibility scanning failed loading results"
+msgstr ""
+
+msgid "Reports|Accessibility scanning results are being parsed"
+msgstr ""
+
msgid "Reports|Actions"
msgstr "Дії"
+msgid "Reports|An error occured while loading report"
+msgstr ""
+
+msgid "Reports|An error occurred while loading %{name} results"
+msgstr ""
+
msgid "Reports|Class"
msgstr "КлаÑ"
@@ -17376,7 +17739,7 @@ msgstr "Запит на зв’ÑÐ·ÑƒÐ²Ð°Ð½Ð½Ñ Ð¾Ð±Ð»Ñ–ÐºÐ¾Ð²Ð¾Ð³Ð¾ запиÑÑ
msgid "Requested %{time_ago}"
msgstr "Відправлено запит %{time_ago}"
-msgid "Requested design version does not exist"
+msgid "Requested design version does not exist."
msgstr ""
msgid "Requested states are invalid"
@@ -17535,7 +17898,7 @@ msgid "Restart Terminal"
msgstr "ПерезапуÑтити термінал"
msgid "Restore group"
-msgstr ""
+msgstr "Відновити групу"
msgid "Restore project"
msgstr "Відновити проєкт"
@@ -17564,6 +17927,9 @@ msgstr "Відновити реплікацію"
msgid "Resync"
msgstr ""
+msgid "Resync all"
+msgstr ""
+
msgid "Resync all %{replicableType}"
msgstr ""
@@ -17634,6 +18000,9 @@ msgstr "Відкликано токен Ñ–Ð¼Ñ–Ñ‚ÑƒÐ²Ð°Ð½Ð½Ñ %{token_name}!"
msgid "Revoked personal access token %{personal_access_token_name}!"
msgstr "Відкликано перÑональний токен доÑтупу %{personal_access_token_name}!"
+msgid "Revoked project access token %{project_access_token_name}!"
+msgstr ""
+
msgid "RightSidebar|adding a"
msgstr "додаваннÑ"
@@ -17650,7 +18019,7 @@ msgid "Rollback"
msgstr "Відкотити"
msgid "Rook"
-msgstr ""
+msgstr "Rook"
msgid "Rules that define what git pushes are accepted for a project. All newly created projects will use these settings."
msgstr ""
@@ -17682,6 +18051,9 @@ msgstr "Runner виконує завданні із призначених прÐ
msgid "Runner token"
msgstr "Токен Runner'а"
+msgid "Runner tokens"
+msgstr ""
+
msgid "Runner was not updated."
msgstr "Runner не було оновлено."
@@ -17736,6 +18108,9 @@ msgstr "Єдиний вхід SAML"
msgid "SAML SSO for %{group_name}"
msgstr "Єдиний вхід SAML Ð´Ð»Ñ %{group_name}"
+msgid "SAML discovery tokens"
+msgstr ""
+
msgid "SAML for %{group_name}"
msgstr "SAML Ð´Ð»Ñ %{group_name}"
@@ -17794,7 +18169,7 @@ msgid "Save comment"
msgstr "Зберегти коментар"
msgid "Save expiration policy"
-msgstr ""
+msgstr "Зберегти політику Ð·Ð°ÐºÑ–Ð½Ñ‡ÐµÐ½Ð½Ñ Ñ‚ÐµÑ€Ð¼Ñ–Ð½Ñƒ дії"
msgid "Save password"
msgstr "Зберегти пароль"
@@ -17820,12 +18195,18 @@ msgstr "Розклад нового конвеєра"
msgid "Scheduled"
msgstr "Заплановано"
+msgid "Scheduled to merge this merge request (%{strategy})."
+msgstr ""
+
msgid "Scheduled to merge this merge request when the pipeline succeeds."
msgstr "Заплановано Ð·Ð»Ð¸Ð²Ð°Ð½Ð½Ñ Ñ†ÑŒÐ¾Ð³Ð¾ запиту на злиттÑ, коли конвеєр завершитьÑÑ ÑƒÑпішно."
msgid "Schedules"
msgstr "Розклади"
+msgid "Schedules to merge this merge request (%{strategy})."
+msgstr ""
+
msgid "Scheduling"
msgstr "ПлануваннÑ"
@@ -17883,6 +18264,9 @@ msgstr "Пошук у гілках"
msgid "Search branches and tags"
msgstr "Пошук гілок та тегів"
+msgid "Search by author"
+msgstr ""
+
msgid "Search files"
msgstr "Пошук файлів"
@@ -18044,13 +18428,6 @@ msgstr[1] "Ñніпети"
msgstr[2] "Ñніпетів"
msgstr[3] "Ñніпетів"
-msgid "SearchResults|snippet result"
-msgid_plural "SearchResults|snippet results"
-msgstr[0] "результат в Ñніпетах"
-msgstr[1] "результати в Ñніпетах"
-msgstr[2] "результатів в Ñніпетах"
-msgstr[3] "результатів в Ñніпетах"
-
msgid "SearchResults|user"
msgid_plural "SearchResults|users"
msgstr[0] "кориÑтувач"
@@ -18065,6 +18442,9 @@ msgstr[1] "результати у вікі"
msgstr[2] "результатів у вікі"
msgstr[3] "результатів у вікі"
+msgid "Searching by both author and message is currently not supported."
+msgstr ""
+
msgid "Seat Link"
msgstr ""
@@ -18092,185 +18472,233 @@ msgstr ""
msgid "Security Dashboard"
msgstr "Панель безпеки"
-msgid "Security Dashboard|Error fetching the vulnerability counts. Please check your network connection and try again."
-msgstr "Помилка під Ñ‡Ð°Ñ Ð¾Ñ‚Ñ€Ð¸Ð¼Ð°Ð½Ð½Ñ ÐºÑ–Ð»ÑŒÐºÐ¾ÑÑ‚Ñ– вразливоÑтей. Будь лаÑка, перевірте ваше з'Ñ”Ð´Ð½Ð°Ð½Ð½Ñ Ð· мережею Ñ– Ñпробуйте знову."
+msgid "Security configuration help link"
+msgstr "ПоÑÐ¸Ð»Ð°Ð½Ð½Ñ Ð½Ð° довідку Ð´Ð»Ñ Ð½Ð°Ð»Ð°ÑˆÑ‚ÑƒÐ²Ð°Ð½ÑŒ безпеки"
-msgid "Security Dashboard|Error fetching the vulnerability list. Please check your network connection and try again."
-msgstr "Помилка під Ñ‡Ð°Ñ Ð¾Ñ‚Ñ€Ð¸Ð¼Ð°Ð½Ð½Ñ ÑпиÑку вразливоÑтей. Будь лаÑка, перевірте ваше з'Ñ”Ð´Ð½Ð°Ð½Ð½Ñ Ð· мережею Ñ– Ñпробуйте знову."
+msgid "Security dashboard"
+msgstr "Панель безпеки"
-msgid "Security Dashboard|Issue Created"
-msgstr "Створено задачу"
+msgid "Security report is out of date. Please update your branch with the latest changes from the target branch (%{targetBranchName})"
+msgstr ""
-msgid "Security Reports|Comment added to '%{vulnerabilityName}'"
-msgstr "Коментар додано до '%{vulnerabilityName}'"
+msgid "Security report is out of date. Run %{newPipelineLinkStart}a new pipeline%{newPipelineLinkEnd} for the target branch (%{targetBranchName})"
+msgstr ""
-msgid "Security Reports|Comment deleted on '%{vulnerabilityName}'"
-msgstr "Коментар видалено в \"%{vulnerabilityName}\""
+msgid "SecurityConfiguration|Configured"
+msgstr "Ðалаштовано"
-msgid "Security Reports|Comment edited on '%{vulnerabilityName}'"
-msgstr "Коментар відредаговано в \"%{vulnerabilityName}\""
+msgid "SecurityConfiguration|Feature"
+msgstr "ФункціÑ"
-msgid "Security Reports|Create issue"
-msgstr "Створити задачу"
+msgid "SecurityConfiguration|Feature documentation for %{featureName}"
+msgstr ""
-msgid "Security Reports|Dismiss vulnerability"
-msgstr "Відхилити вразливіÑÑ‚ÑŒ"
+msgid "SecurityConfiguration|Not yet configured"
+msgstr "Ще не налаштовано"
-msgid "Security Reports|Dismissed '%{vulnerabilityName}'"
-msgstr "Відхилено '%{vulnerabilityName}'"
+msgid "SecurityConfiguration|Secure features"
+msgstr "Безпечні функції"
-msgid "Security Reports|Dismissed '%{vulnerabilityName}'. Turn off the hide dismissed toggle to view."
-msgstr "Відхилено \"%{vulnerabilityName}\". Ð”Ð»Ñ Ð¼Ð¾Ð¶Ð»Ð¸Ð²Ð¾ÑÑ‚Ñ– переглÑду вимкніть перемикач \"приховати відхилені\"."
+msgid "SecurityConfiguration|Status"
+msgstr "Стан"
-msgid "Security Reports|Either you don't have permission to view this dashboard or the dashboard has not been setup. Please check your permission settings with your administrator or check your dashboard configurations to proceed."
-msgstr "У Ð²Ð°Ñ Ð½ÐµÐ¼Ð°Ñ” доÑтупу до цієї панелі ÑƒÐ¿Ñ€Ð°Ð²Ð»Ñ–Ð½Ð½Ñ Ð°Ð±Ð¾ вона не Ñконфігурована. Ð”Ð»Ñ Ð¿Ñ€Ð¾Ð´Ð¾Ð²Ð¶ÐµÐ½Ð½Ñ, будь-лаÑка, перевірте Ð½Ð°Ð»Ð°ÑˆÑ‚ÑƒÐ²Ð°Ð½Ð½Ñ Ð´Ð¾Ñтупу за допомогою вашого адмініÑтратора, або перевірте конфігурації панелей."
+msgid "SecurityReports|%{firstProject} and %{secondProject}"
+msgstr ""
-msgid "Security Reports|Learn more about setting up your dashboard"
-msgstr "ДізнайтеÑÑ Ð±Ñ–Ð»ÑŒÑˆÐµ про Ð½Ð°Ð»Ð°ÑˆÑ‚ÑƒÐ²Ð°Ð½Ð½Ñ Ð²Ð°ÑˆÐ¾Ñ— панелі"
+msgid "SecurityReports|%{firstProject}, %{secondProject}, and %{rest}"
+msgstr ""
-msgid "Security Reports|More info"
-msgstr "Детальніше"
+msgid "SecurityReports|Add a project to your dashboard"
+msgstr ""
-msgid "Security Reports|Oops, something doesn't seem right."
-msgstr "Ой, щоÑÑŒ пішло не так."
+msgid "SecurityReports|Add or remove projects from your dashboard"
+msgstr ""
-msgid "Security Reports|Security reports can only be accessed by authorized users."
+msgid "SecurityReports|Add projects"
msgstr ""
-msgid "Security Reports|There was an error adding the comment."
-msgstr "Помилка під Ñ‡Ð°Ñ Ð´Ð¾Ð´Ð°Ð²Ð°Ð½Ð½Ñ ÐºÐ¾Ð¼ÐµÐ½Ñ‚Ð°Ñ€Ñ."
+msgid "SecurityReports|Comment added to '%{vulnerabilityName}'"
+msgstr ""
-msgid "Security Reports|There was an error creating the issue."
-msgstr "Помилка при Ñтворенні задачі."
+msgid "SecurityReports|Comment deleted on '%{vulnerabilityName}'"
+msgstr ""
-msgid "Security Reports|There was an error creating the merge request."
-msgstr "Помилка при Ñтворенні запиту на злиттÑ."
+msgid "SecurityReports|Comment edited on '%{vulnerabilityName}'"
+msgstr ""
-msgid "Security Reports|There was an error deleting the comment."
-msgstr "Під Ñ‡Ð°Ñ Ð²Ð¸Ð´Ð°Ð»ÐµÐ½Ð½Ñ ÐºÐ¾Ð¼ÐµÐ½Ñ‚Ð°Ñ€Ñ ÑталаÑÑ Ð¿Ð¾Ð¼Ð¸Ð»ÐºÐ°."
+msgid "SecurityReports|Create issue"
+msgstr ""
-msgid "Security Reports|There was an error dismissing the vulnerabilities."
+msgid "SecurityReports|Dismiss Selected"
msgstr ""
-msgid "Security Reports|There was an error dismissing the vulnerability."
-msgstr "Помилка при відхиленні вразливоÑÑ‚Ñ–."
+msgid "SecurityReports|Dismiss vulnerability"
+msgstr ""
-msgid "Security Reports|There was an error reverting the dismissal."
-msgstr "Помилка при анулюванні відхиленнÑ."
+msgid "SecurityReports|Dismissed '%{vulnerabilityName}'"
+msgstr ""
-msgid "Security Reports|There was an error reverting this dismissal."
-msgstr "Помилка при анулюванні цього відхиленнÑ."
+msgid "SecurityReports|Dismissed '%{vulnerabilityName}'. Turn off the hide dismissed toggle to view."
+msgstr ""
-msgid "Security Reports|Undo dismiss"
-msgstr "Відмінити відхиленнÑ"
+msgid "SecurityReports|Each vulnerability now has a unique page that can be directly linked to, shared, referenced, and tracked as the single source of truth. Vulnerability occurrences also persist across scanner runs, which improves tracking and visibility and reduces duplicates between scans."
+msgstr ""
-msgid "Security Reports|You do not have sufficient permissions to access this report"
+msgid "SecurityReports|Edit dashboard"
msgstr ""
-msgid "Security Reports|You must sign in as an authorized user to see this report"
+msgid "SecurityReports|Either you don't have permission to view this dashboard or the dashboard has not been setup. Please check your permission settings with your administrator or check your dashboard configurations to proceed."
msgstr ""
-msgid "Security configuration help link"
-msgstr "ПоÑÐ¸Ð»Ð°Ð½Ð½Ñ Ð½Ð° довідку Ð´Ð»Ñ Ð½Ð°Ð»Ð°ÑˆÑ‚ÑƒÐ²Ð°Ð½ÑŒ безпеки"
+msgid "SecurityReports|Error fetching the vulnerability counts. Please check your network connection and try again."
+msgstr ""
-msgid "Security dashboard"
-msgstr "Панель безпеки"
+msgid "SecurityReports|Error fetching the vulnerability list. Please check your network connection and try again."
+msgstr ""
-msgid "Security report is out of date. Please update your branch with the latest changes from the target branch (%{targetBranchName})"
+msgid "SecurityReports|False positive"
msgstr ""
-msgid "Security report is out of date. Run %{newPipelineLinkStart}a new pipeline%{newPipelineLinkEnd} for the target branch (%{targetBranchName})"
+msgid "SecurityReports|Hide dismissed"
msgstr ""
-msgid "SecurityConfiguration|Configured"
-msgstr "Ðалаштовано"
+msgid "SecurityReports|Introducing standalone vulnerabilities"
+msgstr ""
-msgid "SecurityConfiguration|Feature"
-msgstr "ФункціÑ"
+msgid "SecurityReports|Issue Created"
+msgstr ""
-msgid "SecurityConfiguration|Feature documentation for %{featureName}"
+msgid "SecurityReports|Learn More"
msgstr ""
-msgid "SecurityConfiguration|Not yet configured"
-msgstr "Ще не налаштовано"
+msgid "SecurityReports|Learn more about setting up your dashboard"
+msgstr ""
-msgid "SecurityConfiguration|Secure features"
-msgstr "Безпечні функції"
+msgid "SecurityReports|Load more vulnerabilities"
+msgstr ""
-msgid "SecurityConfiguration|Status"
-msgstr "Стан"
+msgid "SecurityReports|Monitor vulnerabilities in your code"
+msgstr ""
-msgid "SecurityDashboard|%{firstProject} and %{secondProject}"
-msgstr "%{firstProject} та %{secondProject}"
+msgid "SecurityReports|More info"
+msgstr ""
-msgid "SecurityDashboard|%{firstProject}, %{secondProject}, and %{rest}"
-msgstr "%{firstProject}, %{secondProject} та %{rest}"
+msgid "SecurityReports|More information"
+msgstr ""
-msgid "SecurityDashboard|Add a project to your dashboard"
-msgstr "Додати проект до панелі"
+msgid "SecurityReports|No vulnerabilities found for dashboard"
+msgstr ""
-msgid "SecurityDashboard|Add or remove projects from your dashboard"
-msgstr "Додати або видалити проекти із панелі"
+msgid "SecurityReports|No vulnerabilities found for this group"
+msgstr ""
-msgid "SecurityDashboard|Add projects"
-msgstr "Додати проекти"
+msgid "SecurityReports|No vulnerabilities found for this pipeline"
+msgstr ""
-msgid "SecurityDashboard|Edit dashboard"
-msgstr "Редагувати панель"
+msgid "SecurityReports|No vulnerabilities found for this project"
+msgstr ""
-msgid "SecurityDashboard|Hide dismissed"
-msgstr "Приховати відхилені"
+msgid "SecurityReports|Oops, something doesn't seem right."
+msgstr ""
-msgid "SecurityDashboard|Monitor vulnerabilities in your code"
-msgstr "Моніторинг вразливоÑтей у вашому коді"
+msgid "SecurityReports|Pipeline %{pipelineLink} triggered %{timeago} by %{user}"
+msgstr ""
-msgid "SecurityDashboard|More information"
-msgstr "Детальніше"
+msgid "SecurityReports|Project"
+msgstr ""
-msgid "SecurityDashboard|No vulnerabilities found for dashboard"
+msgid "SecurityReports|Projects added"
msgstr ""
-msgid "SecurityDashboard|Pipeline %{pipelineLink} triggered %{timeago} by %{user}"
+msgid "SecurityReports|Remove project from dashboard"
msgstr ""
-msgid "SecurityDashboard|Project"
-msgstr "Проект"
+msgid "SecurityReports|Report type"
+msgstr ""
-msgid "SecurityDashboard|Projects added"
-msgstr "Додані проекти"
+msgid "SecurityReports|Return to dashboard"
+msgstr ""
-msgid "SecurityDashboard|Remove project from dashboard"
-msgstr "Видалити проект із панелі"
+msgid "SecurityReports|Security Dashboard"
+msgstr ""
-msgid "SecurityDashboard|Report type"
-msgstr "Тип звіту"
+msgid "SecurityReports|Security reports can only be accessed by authorized users."
+msgstr ""
-msgid "SecurityDashboard|Return to dashboard"
-msgstr "ПовернутиÑÑ Ð´Ð¾ панелі"
+msgid "SecurityReports|Select a project to add by using the project search field above."
+msgstr ""
-msgid "SecurityDashboard|Security Dashboard"
-msgstr "Панель безпеки"
+msgid "SecurityReports|Select a reason"
+msgstr ""
-msgid "SecurityDashboard|Select a project to add by using the project search field above."
-msgstr "Виберіть проект вище за допомогою Ð¿Ð¾Ð»Ñ Ð¿Ð¾ÑˆÑƒÐºÑƒ проекту."
+msgid "SecurityReports|Severity"
+msgstr ""
-msgid "SecurityDashboard|Severity"
-msgstr "Рівень"
+msgid "SecurityReports|Status"
+msgstr ""
-msgid "SecurityDashboard|Status"
+msgid "SecurityReports|The security dashboard displays the latest security findings for projects you wish to monitor. Select \"Edit dashboard\" to add and remove projects."
msgstr ""
-msgid "SecurityDashboard|The security dashboard displays the latest security findings for projects you wish to monitor. Select \"Edit dashboard\" to add and remove projects."
-msgstr "Панель безпеки відображає оÑтанню інфорацію Ð´Ð»Ñ Ð¿Ñ€Ð¾ÐµÐºÑ‚Ñ–Ð², Ñкі ви відÑлідковуєте. Виберіть \"Редагувати панель\" Ð´Ð»Ñ Ð´Ð¾Ð´Ð°Ð²Ð°Ð½Ð½Ñ Ñ‚Ð° Ð²Ð¸Ð´Ð°Ð»ÐµÐ½Ð½Ñ Ð¿Ñ€Ð¾ÐµÐºÑ‚Ñ–Ð²."
+msgid "SecurityReports|The security dashboard displays the latest security report. Use it to find and fix vulnerabilities."
+msgstr ""
-msgid "SecurityDashboard|The security dashboard displays the latest security report. Use it to find and fix vulnerabilities."
+msgid "SecurityReports|There was an error adding the comment."
msgstr ""
-msgid "SecurityDashboard|There was an error while generating the report."
+msgid "SecurityReports|There was an error creating the issue."
msgstr ""
-msgid "SecurityDashboard|Unable to add %{invalidProjects}"
-msgstr "Ðе вдалоÑÑ Ð´Ð¾Ð´Ð°Ñ‚Ð¸ %{invalidProjects}"
+msgid "SecurityReports|There was an error creating the merge request."
+msgstr ""
+
+msgid "SecurityReports|There was an error deleting the comment."
+msgstr ""
+
+msgid "SecurityReports|There was an error dismissing the vulnerabilities."
+msgstr ""
+
+msgid "SecurityReports|There was an error dismissing the vulnerability."
+msgstr ""
+
+msgid "SecurityReports|There was an error reverting the dismissal."
+msgstr ""
+
+msgid "SecurityReports|There was an error reverting this dismissal."
+msgstr ""
+
+msgid "SecurityReports|There was an error while generating the report."
+msgstr ""
+
+msgid "SecurityReports|Unable to add %{invalidProjects}"
+msgstr ""
+
+msgid "SecurityReports|Undo dismiss"
+msgstr ""
+
+msgid "SecurityReports|While it's rare to have no vulnerabilities for your group, it can happen. In any event, we ask that you double check your settings to make sure you've set up your dashboard correctly."
+msgstr ""
+
+msgid "SecurityReports|While it's rare to have no vulnerabilities for your pipeline, it can happen. In any event, we ask that you double check your settings to make sure all security scanning jobs have passed successfully."
+msgstr ""
+
+msgid "SecurityReports|While it's rare to have no vulnerabilities for your project, it can happen. In any event, we ask that you double check your settings to make sure you've set up your dashboard correctly."
+msgstr ""
+
+msgid "SecurityReports|While it's rare to have no vulnerabilities, it can happen. In any event, we ask that you please double check your settings to make sure you've set up your dashboard correctly."
+msgstr ""
+
+msgid "SecurityReports|Won't fix / Accept risk"
+msgstr ""
+
+msgid "SecurityReports|You do not have sufficient permissions to access this report"
+msgstr ""
+
+msgid "SecurityReports|You must sign in as an authorized user to see this report"
+msgstr ""
+
+msgid "SecurityReports|[No reason]"
+msgstr ""
msgid "See GitLab's %{password_policy_guidelines}"
msgstr ""
@@ -18281,6 +18709,9 @@ msgstr "ПереглÑнути метрики"
msgid "See the affected projects in the GitLab admin panel"
msgstr "ПереглÑнути уÑÑ– уражені проекти на панелі адмініÑтратора GitLab"
+msgid "See what's new at GitLab"
+msgstr ""
+
msgid "Select"
msgstr "Вибрати"
@@ -18686,9 +19117,6 @@ msgstr "ÐÐ°Ð»Ð°ÑˆÑ‚ÑƒÐ²Ð°Ð½Ð½Ñ CI/CD"
msgid "Set up Jira Integration"
msgstr ""
-msgid "Set up Jira Integration illustration"
-msgstr ""
-
msgid "Set up a %{type} Runner automatically"
msgstr "Ðвтоматично налаштувати %{type} Runner"
@@ -18773,6 +19201,9 @@ msgstr ""
msgid "Severity: %{severity}"
msgstr "Рівень: %{severity}"
+msgid "Shards (%{shards})"
+msgstr ""
+
msgid "Shards to synchronize"
msgstr ""
@@ -19075,7 +19506,7 @@ msgid "Snippets|Description (optional)"
msgstr ""
msgid "Snippets|File"
-msgstr ""
+msgstr "Файл"
msgid "Snippets|Give your file a name to add code highlighting, e.g. example.rb for Ruby"
msgstr ""
@@ -19089,6 +19520,9 @@ msgstr ""
msgid "Snowplow"
msgstr "Snowplow"
+msgid "Some child epics may be hidden due to applied filters"
+msgstr ""
+
msgid "Some email servers do not support overriding the email sender name. Enable this option to include the name of the author of the issue, merge request or comment in the email body instead."
msgstr "ДеÑкі Ñервери електронної пошти не підтримують Ð¿ÐµÑ€ÐµÐ²Ð¸Ð·Ð½Ð°Ñ‡ÐµÐ½Ð½Ñ Ñ–Ð¼ÐµÐ½Ñ– відправника. Увімкніть це налаштуваннÑ, щоб включати Ñ–Ð¼â€™Ñ Ð°Ð²Ñ‚Ð¾Ñ€Ð° задачі, запиту на Ð·Ð»Ð¸Ñ‚Ñ‚Ñ Ð°Ð±Ð¾ ÐºÐ¾Ð¼ÐµÐ½Ñ‚Ð°Ñ€Ñ Ð² тіло повідомленнÑ."
@@ -19230,6 +19664,9 @@ msgstr ""
msgid "Something went wrong while updating your list settings"
msgstr ""
+msgid "Something went wrong with your automatic subscription renewal"
+msgstr ""
+
msgid "Something went wrong, unable to add %{project} to dashboard"
msgstr "Проблема, не вдалоÑÑ Ð´Ð¾Ð´Ð°Ñ‚Ð¸ %{project} до панелі керуваннÑ"
@@ -19515,6 +19952,9 @@ msgstr ""
msgid "Stage removed"
msgstr ""
+msgid "Standard"
+msgstr ""
+
msgid "Star a label to make it a priority label. Order the prioritized labels to change their relative priority, by dragging."
msgstr "Позначте мітку, щоб зробити Ñ—Ñ— пріоритетною. ПеретÑгуйте пріоритетні мітки Ð´Ð»Ñ Ð·Ð¼Ñ–Ð½Ð¸ Ñ—Ñ… відноÑного пріоритету."
@@ -19638,6 +20078,9 @@ msgstr "Залиште Ñвоє Ð¿Ð¾Ð²Ñ–Ð´Ð¾Ð¼Ð»ÐµÐ½Ð½Ñ Ð´Ð»Ñ Ð°ÐºÑ‚Ð¸Ð²Ð°Ñ†Ñ–
msgid "Static Application Security Testing (SAST)"
msgstr ""
+msgid "StaticSiteEditor|An error occurred while submitting your changes."
+msgstr ""
+
msgid "StaticSiteEditor|Branch could not be created."
msgstr ""
@@ -19647,18 +20090,30 @@ msgstr ""
msgid "StaticSiteEditor|Could not create merge request."
msgstr ""
+msgid "StaticSiteEditor|Incompatible file content"
+msgstr ""
+
msgid "StaticSiteEditor|Return to site"
msgstr ""
-msgid "StaticSiteEditor|Success!"
+msgid "StaticSiteEditor|Static site editor"
msgstr ""
+msgid "StaticSiteEditor|Success!"
+msgstr "УÑпіх!"
+
msgid "StaticSiteEditor|Summary of changes"
msgstr ""
+msgid "StaticSiteEditor|The Static Site Editor is currently configured to only edit Markdown content on pages generated from Middleman. Visit the documentation to learn more about configuring your site to use the Static Site Editor."
+msgstr ""
+
msgid "StaticSiteEditor|Update %{sourcePath} file"
msgstr ""
+msgid "StaticSiteEditor|View documentation"
+msgstr ""
+
msgid "StaticSiteEditor|View merge request"
msgstr ""
@@ -19699,7 +20154,7 @@ msgid "StatusPage|AWS region"
msgstr ""
msgid "StatusPage|Active"
-msgstr ""
+msgstr "Ðктивний"
msgid "StatusPage|Bucket %{docsLink}"
msgstr ""
@@ -19716,6 +20171,12 @@ msgstr ""
msgid "StatusPage|Status page"
msgstr ""
+msgid "StatusPage|Status page URL"
+msgstr ""
+
+msgid "StatusPage|Status page frontend documentation"
+msgstr ""
+
msgid "StatusPage|To publish incidents to an external status page, GitLab will store a JSON file in your Amazon S3 account in a location accessible to your external status page service. Make sure to also set up %{docsLink}"
msgstr ""
@@ -20064,6 +20525,12 @@ msgstr "Ð†Ð½Ñ„Ð¾Ñ€Ð¼Ð°Ñ†Ñ–Ñ Ð¿Ñ€Ð¾ Ñинхронізацію"
msgid "Synced"
msgstr ""
+msgid "Synchronization disabled"
+msgstr ""
+
+msgid "Synchronization of container repositories is disabled."
+msgstr ""
+
msgid "System"
msgstr "СиÑтемні"
@@ -20217,6 +20684,9 @@ msgstr "Домен команди"
msgid "Telephone number"
msgstr "Ðомер телефону"
+msgid "Telephone number (Optional)"
+msgstr ""
+
msgid "Template"
msgstr "Шаблон"
@@ -20529,6 +20999,9 @@ msgstr "Ліцензію видалено. Ðаразі GitLab не моє жоÐ
msgid "The license was successfully uploaded and is now active. You can see the details below."
msgstr "Ліцензію уÑпішно завантажено Ñ– вона активна. Подробиці можна переглÑнути нижче."
+msgid "The license was successfully uploaded and will be active from %{starts_at}. You can see the details below."
+msgstr ""
+
msgid "The maximum file size allowed is %{size}."
msgstr "МакÑимальний розмір файлу — %{size}."
@@ -20704,10 +21177,10 @@ msgid "The value lying at the midpoint of a series of observed values. E.g., bet
msgstr "Середнє Ð·Ð½Ð°Ñ‡ÐµÐ½Ð½Ñ Ð² Ñ€Ñдку. Приклад: між 3, 5, 9, Ñередніми 5, між 3, 5, 7, 8, Ñередніми (5 + 7) / 2 = 6."
msgid "The vulnerability is no longer detected. Verify the vulnerability has been fixed or removed before changing its status."
-msgstr ""
+msgstr "ВразливіÑÑ‚ÑŒ більше не виÑвлÑєтьÑÑ. Перевірте, що цю вразливіÑÑ‚ÑŒ виправлено або видалено перед тим, Ñк змінювати ÑтатуÑ."
msgid "The vulnerability is no longer detected. Verify the vulnerability has been remediated before changing its status."
-msgstr ""
+msgstr "ВразливіÑÑ‚ÑŒ більше не виÑвлÑєтьÑÑ. Перевірте, що цю вразливіÑÑ‚ÑŒ виправлено перед тим, Ñк змінювати ÑтатуÑ."
msgid "There are no GPG keys associated with this account."
msgstr "Ðемає GPG ключів, пов’Ñзаних із цим обліковим запиÑом."
@@ -20850,6 +21323,9 @@ msgstr "Помилка при отриманні даних Ð´Ð»Ñ Ð´Ñ–Ð°Ð³Ñ€Ð°Ð
msgid "There was an error getting the epic participants."
msgstr ""
+msgid "There was an error importing the Jira project."
+msgstr ""
+
msgid "There was an error loading users activity calendar."
msgstr "Помилка при завантаженні ÐºÐ°Ð»ÐµÐ½Ð´Ð°Ñ€Ñ Ð°ÐºÑ‚Ð¸Ð²Ð½Ð¾ÑÑ‚Ñ– кориÑтувачів."
@@ -20865,7 +21341,7 @@ msgstr "Помилка при Ñкиданні групових хвилин дÐ
msgid "There was an error resetting user pipeline minutes."
msgstr "Помилка при Ñкиданні хвилин Ð´Ð»Ñ ÐºÐ¾Ð½Ð²ÐµÑ”Ñ€Ñ–Ð² кориÑтувача."
-msgid "There was an error saving this Geo Node"
+msgid "There was an error saving this Geo Node."
msgstr ""
msgid "There was an error saving your changes."
@@ -20928,6 +21404,9 @@ msgstr "Ці змінні вÑтановлені в налаштуваннÑÑ… Ð
msgid "They can be managed using the %{link}."
msgstr "Ðими можна керувати за допомогою %{link}."
+msgid "Third Party Advisory Link"
+msgstr ""
+
msgid "Third party offers"
msgstr "Сторонні пропозиції"
@@ -20964,6 +21443,9 @@ msgstr "Цей заÑтоÑунок було Ñтворено %{link_to_owner}."
msgid "This application will be able to:"
msgstr "Ð¦Ñ Ð¿Ñ€Ð¾Ð³Ñ€Ð°Ð¼Ð° зможе:"
+msgid "This attachment has been truncated to avoid exceeding the maximum allowed attachment size of 15MB. %{written_count} of %{issues_count} issues have been included. Consider re-exporting with a narrower selection of issues."
+msgstr ""
+
msgid "This block is self-referential"
msgstr "Цей блок поÑилаєтьÑÑ Ñам на Ñебе"
@@ -21105,6 +21587,12 @@ msgstr "Ð¦Ñ Ð·Ð°Ð´Ð°Ñ‡Ð° Ñ” %{confidentialLinkStart}конфіденційноÑ
msgid "This issue is confidential"
msgstr "Ð¦Ñ Ð·Ð°Ð´Ð°Ñ‡Ð° Ñ” конфіденційною"
+msgid "This issue is currently blocked by the following issues: %{issues}."
+msgstr ""
+
+msgid "This issue is in a child epic of the filtered epic"
+msgstr ""
+
msgid "This issue is locked."
msgstr "Ð¦Ñ Ð·Ð°Ð´Ð°Ñ‡Ð° заблокована."
@@ -21303,7 +21791,10 @@ msgstr "Цей кориÑтувач буде автором вÑÑ–Ñ… подій
msgid "This user will be the author of all events in the activity feed that are the result of an update, like new branches being created or new commits being pushed to existing branches. Upon creation or when reassigning you can only assign yourself to be the mirror user."
msgstr "Цей кориÑтувач буде автором вÑÑ–Ñ… подій в каналі активноÑÑ‚Ñ–, Ñкі Ñ” результатом оновленнÑ, наприклад ÑÑ‚Ð²Ð¾Ñ€ÐµÐ½Ð½Ñ Ð½Ð¾Ð²Ð¸Ñ… гілок або Ð²Ñ–Ð´Ð¿Ñ€Ð°Ð²Ð»ÐµÐ½Ð½Ñ Ð½Ð¾Ð²Ð¸Ñ… комітів до Ñ–Ñнуючих гілок. При Ñтворенні або перепризначенні ви зможете призначити лише Ñебе кориÑтувачем Ð´Ð»Ñ Ð²Ñ–Ð´Ð´Ð·ÐµÑ€ÐºÐ°Ð»ÐµÐ½Ð½Ñ."
-msgid "This variable can not be masked"
+msgid "This variable can not be masked."
+msgstr ""
+
+msgid "This variable does not match the expected pattern."
msgstr ""
msgid "This will help us personalize your onboarding experience."
@@ -21333,10 +21824,10 @@ msgstr ""
msgid "ThreatMonitoring|Application firewall not detected"
msgstr ""
-msgid "ThreatMonitoring|Container Network Policy"
+msgid "ThreatMonitoring|Container Network Policies are not installed or have been disabled. To view this data, ensure your Network Policies are installed and enabled for your cluster."
msgstr ""
-msgid "ThreatMonitoring|Container NetworkPolicies are not installed or has been disabled. To view this data, ensure you NetworkPolicies are installed and enabled for your cluster."
+msgid "ThreatMonitoring|Container Network Policy"
msgstr ""
msgid "ThreatMonitoring|Container NetworkPolicies not detected"
@@ -21361,7 +21852,7 @@ msgid "ThreatMonitoring|Requests"
msgstr ""
msgid "ThreatMonitoring|Show last"
-msgstr ""
+msgstr "Показати оÑтанній"
msgid "ThreatMonitoring|Something went wrong, unable to fetch environments"
msgstr ""
@@ -21369,7 +21860,7 @@ msgstr ""
msgid "ThreatMonitoring|Something went wrong, unable to fetch statistics"
msgstr ""
-msgid "ThreatMonitoring|The firewall is not installed or has been disabled. To view this data, ensure you firewall is installed and enabled for your cluster."
+msgid "ThreatMonitoring|The firewall is not installed or has been disabled. To view this data, ensure the web application firewall is installed and enabled for your cluster."
msgstr ""
msgid "ThreatMonitoring|The graph below is an overview of traffic coming to your application as tracked by the Web Application Firewall (WAF). View the docs for instructions on how to access the WAF logs to see what type of malicious traffic is trying to access your app. The docs link is also accessible by clicking the \"?\" icon next to the title below."
@@ -21382,7 +21873,7 @@ msgid "ThreatMonitoring|Threat Monitoring help page link"
msgstr ""
msgid "ThreatMonitoring|Time"
-msgstr ""
+msgstr "ЧаÑ"
msgid "ThreatMonitoring|To view this data, ensure you have configured an environment for this project and that at least one threat monitoring feature is enabled."
msgstr ""
@@ -21391,7 +21882,7 @@ msgid "ThreatMonitoring|Total Packets"
msgstr ""
msgid "ThreatMonitoring|Total Requests"
-msgstr ""
+msgstr "Загальна кількіÑÑ‚ÑŒ запитів"
msgid "ThreatMonitoring|View documentation"
msgstr ""
@@ -21438,6 +21929,9 @@ msgstr "Ð§Ð°Ñ Ð²Ñ–Ð´ оÑтаннього коміту до злиттÑ"
msgid "Time in seconds GitLab will wait for a response from the external service. When the service does not respond in time, access will be denied."
msgstr "Ð§Ð°Ñ Ð² Ñекундах, протÑгом Ñкого GitLab чекатиме відповіді від зовнішньої Ñлужби. Якщо вона не відповіÑÑ‚ÑŒ вчаÑно, доÑтуп буде заборонений."
+msgid "Time of import: %{importTime}"
+msgstr ""
+
msgid "Time remaining"
msgstr "ЗалишилоÑÑ Ñ‡Ð°Ñу"
@@ -21626,8 +22120,8 @@ msgstr "Заголовок"
msgid "Title:"
msgstr "Заголовок:"
-msgid "Titles and Filenames"
-msgstr "Заголовки та імена файлів"
+msgid "Titles and Descriptions"
+msgstr ""
msgid "To"
msgstr "До"
@@ -21833,8 +22327,8 @@ msgstr ""
msgid "Too many projects enabled. You will need to manage them via the console or the API."
msgstr ""
-msgid "Topics"
-msgstr "Теми"
+msgid "Topics (optional)"
+msgstr ""
msgid "Total"
msgstr "Ð’Ñього"
@@ -21846,13 +22340,13 @@ msgid "Total artifacts size: %{total_size}"
msgstr "Загальний розмір артефактів: %{total_size}"
msgid "Total cores (vCPUs)"
-msgstr ""
+msgstr "Ð’Ñього Ñдер (vCPU)"
msgid "Total issues"
msgstr "Ð’Ñього задач"
msgid "Total memory (GB)"
-msgstr ""
+msgstr "Ð’Ñього пам'ÑÑ‚Ñ– (ГБ)"
msgid "Total test time for all commits/merges"
msgstr "Загальний чаÑ, щоб перевірити вÑÑ– коміти/злиттÑ"
@@ -21939,7 +22433,7 @@ msgid "Trending"
msgstr "ПопулÑрні"
msgid "Trials|Go back to GitLab"
-msgstr ""
+msgstr "ПовернутиÑÑ Ð´Ð¾ GitLab"
msgid "Trials|Skip Trial (Continue with Free Account)"
msgstr ""
@@ -21951,7 +22445,7 @@ msgid "Trials|You won't get a free trial right now but you can always resume thi
msgstr ""
msgid "Trigger"
-msgstr ""
+msgstr "Тригер"
msgid "Trigger pipelines for mirror updates"
msgstr "ЗапуÑкати конвеєри Ð´Ð»Ñ Ð²Ñ–Ð´Ð´Ð·ÐµÑ€ÐºÐ°Ð»ÐµÐ½Ð¸Ñ… змін"
@@ -21995,6 +22489,9 @@ msgstr "Спробуйте ще раз?"
msgid "Try all GitLab has to offer for 30 days."
msgstr "Спробуйте вÑÑ– функції GitLab протÑгом 30 днів."
+msgid "Try changing or removing filters."
+msgstr ""
+
msgid "Try to fork again"
msgstr "Спробувати зробити форк знову"
@@ -22088,6 +22585,9 @@ msgstr "Ðеможливо з’єднатиÑÑŒ з Prometheus Ñервером"
msgid "Unable to connect to server: %{error}"
msgstr "Ðе вдалоÑÑ Ð·Ð²â€™ÑзатиÑÑ Ñ–Ð· Ñервером: %{error}"
+msgid "Unable to connect to the Jira instance. Please check your Jira integration configuration."
+msgstr ""
+
msgid "Unable to convert Kubernetes logs encoding to UTF-8"
msgstr ""
@@ -22509,7 +23009,7 @@ msgid "Use an one time password authenticator on your mobile device or computer
msgstr "ВикориÑтовуйте одноразовий пароль на вашому мобільному приÑтрої або комп’ютері Ð´Ð»Ñ Ð°ÐºÑ‚Ð¸Ð²Ð°Ñ†Ñ–Ñ— двофакторної автентифікації (2FA)."
msgid "Use custom color #FF0000"
-msgstr ""
+msgstr "ВикориÑтовувати влаÑний колір #FF0000"
msgid "Use group milestones to manage issues from multiple projects in the same milestone."
msgstr "ВикориÑтовуйте групові етапи, щоб керувати у одному етапі задачі з різних проектів."
@@ -23035,6 +23535,9 @@ msgstr ""
msgid "View issue"
msgstr "ПереглÑнути задачу"
+msgid "View issues"
+msgstr ""
+
msgid "View it on GitLab"
msgstr "ПереглÑнути це на GitLab"
@@ -23129,7 +23632,7 @@ msgid "VisualReviewApp|%{stepStart}Step 4%{stepEnd}. Leave feedback in the Revie
msgstr ""
msgid "VisualReviewApp|Cancel"
-msgstr ""
+msgstr "СкаÑувати"
msgid "VisualReviewApp|Copy merge request ID"
msgstr "Скопіювати ID запиту на злиттÑ"
@@ -23162,16 +23665,16 @@ msgid "Vulnerabilities over time"
msgstr "ВразливоÑÑ‚Ñ– в чаÑÑ–"
msgid "Vulnerability List"
-msgstr ""
+msgstr "СпиÑок вразливоÑтей"
msgid "Vulnerability remediated. Review before resolving."
-msgstr ""
+msgstr "ВразливіÑÑ‚ÑŒ виправлено. ПереглÑньте перед вирішеннÑм."
msgid "Vulnerability resolved in %{branch}"
-msgstr ""
+msgstr "ВразливіÑÑ‚ÑŒ вирішено в %{branch}"
msgid "Vulnerability resolved in the default branch"
-msgstr ""
+msgstr "ВразливіÑÑ‚ÑŒ вирішено у гілці за замовчуваннÑм"
msgid "Vulnerability-Check"
msgstr "Vulnerability-Check"
@@ -23186,60 +23689,72 @@ msgid "VulnerabilityManagement|A true-positive and will fix"
msgstr ""
msgid "VulnerabilityManagement|Change status"
-msgstr ""
+msgstr "Змінити ÑтатуÑ"
msgid "VulnerabilityManagement|Confirm"
-msgstr ""
+msgstr "Підтвердити"
msgid "VulnerabilityManagement|Confirmed %{timeago} by %{user}"
-msgstr ""
+msgstr "Підтверджено %{timeago} кориÑтувачем %{user}"
msgid "VulnerabilityManagement|Create issue"
-msgstr ""
+msgstr "Створити задачу"
msgid "VulnerabilityManagement|Detected %{timeago} in pipeline %{pipelineLink}"
-msgstr ""
+msgstr "ВиÑвлено %{timeago} в контейнері %{pipelineLink}"
msgid "VulnerabilityManagement|Dismiss"
-msgstr ""
+msgstr "Відхилити"
msgid "VulnerabilityManagement|Dismissed %{timeago} by %{user}"
-msgstr ""
+msgstr "Відхилено %{timeago} кориÑтувачем %{user}"
msgid "VulnerabilityManagement|Resolved"
-msgstr ""
+msgstr "Вирішено"
msgid "VulnerabilityManagement|Resolved %{timeago} by %{user}"
+msgstr "Вирішено %{timeago} кориÑтувачем %{user}"
+
+msgid "VulnerabilityManagement|Something went wrong while trying to delete the comment. Please try again later."
msgstr ""
-msgid "VulnerabilityManagement|Something went wrong, could not create an issue."
+msgid "VulnerabilityManagement|Something went wrong while trying to retrieve the vulnerability history. Please try again later."
msgstr ""
-msgid "VulnerabilityManagement|Something went wrong, could not get user."
+msgid "VulnerabilityManagement|Something went wrong while trying to save the comment. Please try again later."
msgstr ""
+msgid "VulnerabilityManagement|Something went wrong, could not create an issue."
+msgstr "ЩоÑÑŒ пішло не так, не вдалоÑÑ Ñтворити задачу."
+
+msgid "VulnerabilityManagement|Something went wrong, could not get user."
+msgstr "ЩоÑÑŒ пішло не так, не вдалоÑÑ Ð¾Ñ‚Ñ€Ð¸Ð¼Ð°Ñ‚Ð¸ кориÑтувача."
+
msgid "VulnerabilityManagement|Something went wrong, could not update vulnerability state."
-msgstr ""
+msgstr "ЩоÑÑŒ пішло не так, не вдалоÑÑ Ð¾Ð½Ð¾Ð²Ð¸Ñ‚Ð¸ Ñтан вразливоÑÑ‚Ñ–."
msgid "VulnerabilityManagement|Verified as fixed or mitigated"
-msgstr ""
+msgstr "Підтверджено і вирішено"
msgid "VulnerabilityManagement|Will not fix or a false-positive"
msgstr ""
msgid "VulnerabilityStatusTypes|All"
-msgstr ""
+msgstr "Ð’ÑÑ–"
msgid "VulnerabilityStatusTypes|Confirmed"
-msgstr ""
+msgstr "Підтверджені"
msgid "VulnerabilityStatusTypes|Detected"
-msgstr ""
+msgstr "ВиÑвлені"
msgid "VulnerabilityStatusTypes|Dismissed"
-msgstr ""
+msgstr "Відхилені"
msgid "VulnerabilityStatusTypes|Resolved"
+msgstr "Вирішені"
+
+msgid "Vulnerability|%{scannerName} (version %{scannerVersion})"
msgstr ""
msgid "Vulnerability|Class"
@@ -23267,7 +23782,7 @@ msgid "Vulnerability|Links"
msgstr "ПоÑиланнÑ"
msgid "Vulnerability|Method"
-msgstr ""
+msgstr "Метод"
msgid "Vulnerability|Namespace"
msgstr "ПроÑÑ‚Ñ–Ñ€ імен"
@@ -23275,14 +23790,17 @@ msgstr "ПроÑÑ‚Ñ–Ñ€ імен"
msgid "Vulnerability|Project"
msgstr "Проект"
-msgid "Vulnerability|Report Type"
-msgstr "Тип звіту"
+msgid "Vulnerability|Scanner Provider"
+msgstr ""
+
+msgid "Vulnerability|Scanner Type"
+msgstr ""
msgid "Vulnerability|Severity"
msgstr "Рівень"
msgid "Vulnerability|Status"
-msgstr ""
+msgstr "СтатуÑ"
msgid "WIP"
msgstr "WIP (в процеÑÑ–)"
@@ -23329,9 +23847,15 @@ msgstr "Ми отримали відповідь від вашого приÑÑ‚Ñ
msgid "We sent you an email with reset password instructions"
msgstr "Ми надіÑлали вам Ð¿Ð¾Ð²Ñ–Ð´Ð¾Ð¼Ð»ÐµÐ½Ð½Ñ Ð¿Ð¾ електронній пошті з інÑтрукціÑми по відновленню паролю"
+msgid "We tried to automatically renew your %{strong}%{plan_name}%{strong_close} subscription for %{strong}%{namespace_name}%{strong_close} on %{expires_on} but something went wrong so your subscription was downgraded to the free plan. Don't worry, your data is safe. We suggest you check your payment method and get in touch with our support team (%{support_link}). They'll gladly help with your subscription renewal."
+msgstr ""
+
msgid "We want to be sure it is you, please confirm you are not a robot."
msgstr "Ми хочемо бути впевнені, що це ви, будь лаÑка, підтвердіть, що ви не робот."
+msgid "We will automatically renew your %{strong}%{plan_name}%{strong_close} subscription for %{strong}%{namespace_name}%{strong_close} on %{strong}%{expires_on}%{strong_close}. There's nothing that you need to do, we'll let you know when the renewal is complete. Need more seats, a higher plan or just want to review your payment method?"
+msgstr ""
+
msgid "We've found no vulnerabilities"
msgstr "Ми не виÑвили вразливоÑтей"
@@ -23348,7 +23872,7 @@ msgid "WebIDE|Merge request"
msgstr ""
msgid "Webhook"
-msgstr ""
+msgstr "Вебхук"
msgid "Webhook Logs"
msgstr ""
@@ -23438,17 +23962,8 @@ msgstr "При викориÑтанні протоколів <code>http://</code
msgid "When:"
msgstr "Коли:"
-msgid "While it's rare to have no vulnerabilities for your group, it can happen. In any event, we ask that you double check your settings to make sure you've set up your dashboard correctly."
-msgstr ""
-
-msgid "While it's rare to have no vulnerabilities for your pipeline, it can happen. In any event, we ask that you double check your settings to make sure all security scanning jobs have passed successfully."
-msgstr ""
-
-msgid "While it's rare to have no vulnerabilities for your project, it can happen. In any event, we ask that you double check your settings to make sure you've set up your dashboard correctly."
-msgstr ""
-
msgid "While it's rare to have no vulnerabilities, it can happen. In any event, we ask that you please double check your settings to make sure you've set up your dashboard correctly."
-msgstr ""
+msgstr "Хоча й рідко, але можливо не мати вразливоÑтей. Ð’ будь-Ñкому разі, ми проÑимо Ð²Ð°Ñ Ð¿ÐµÑ€ÐµÐ²Ñ–Ñ€Ð¸Ñ‚Ð¸ ваші налаштуваннÑ, щоб впевнитиÑÑ, що ваша панель налаштована правильно."
msgid "White helpers give contextual information."
msgstr "Білі вказівники дають контекÑтну інформацію."
@@ -23477,9 +23992,6 @@ msgstr ""
msgid "Wiki"
msgstr "Вікі"
-msgid "Wiki pages"
-msgstr "Сторінки Вікі"
-
msgid "Wiki was successfully updated."
msgstr "Вікі уÑпішно оновлено."
@@ -23663,6 +24175,9 @@ msgstr "Так або ні"
msgid "Yes, add it"
msgstr "Так, додати це"
+msgid "Yes, close issue"
+msgstr ""
+
msgid "Yes, let me map Google Code users to full names or GitLab users."
msgstr "Так, дозволити мені зв’Ñзати кориÑтувачів Google Code із повними іменами кориÑтувачів GitLab."
@@ -23858,6 +24373,9 @@ msgstr "Ви не можете запиÑувати на цей \"тільки Ð
msgid "You could not create a new trigger."
msgstr "Ви не змогли Ñтворити новий тригер."
+msgid "You didn't renew your %{strong}%{plan_name}%{strong_close} subscription for %{strong}%{namespace_name}%{strong_close} so it was downgraded to the free plan."
+msgstr ""
+
msgid "You didn't renew your %{strong}%{plan_name}%{strong_close} subscription so it was downgraded to the GitLab Core Plan."
msgstr ""
@@ -23924,6 +24442,9 @@ msgstr "Ви відпиÑані від цього обговореннÑ."
msgid "You have declined the invitation to join %{label}."
msgstr "Ви відхилили Ð·Ð°Ð¿Ñ€Ð¾ÑˆÐµÐ½Ð½Ñ Ð¿Ñ€Ð¸Ñ”Ð´Ð½Ð°Ñ‚Ð¸ÑÑ Ð´Ð¾ %{label}."
+msgid "You have imported from this project %{numberOfPreviousImportsForProject} times before. Each new import will create duplicate issues."
+msgstr ""
+
msgid "You have no permissions"
msgstr "У Ð²Ð°Ñ Ð½ÐµÐ¼Ð°Ñ” прав доÑтупу"
@@ -24005,6 +24526,9 @@ msgstr "Ви повинні завантажити екÑпортований а
msgid "You need to upload a Google Takeout archive."
msgstr "Вам потрібно завантажити архів Google Takeout."
+msgid "You reached %{usage_in_percent} of %{namespace_name}'s capacity (%{used_storage} of %{storage_limit})"
+msgstr ""
+
msgid "You tried to fork %{link_to_the_project} but it failed for the following reason:"
msgstr "Ви намагалиÑÑ Ñтворити Ð²Ñ–Ð´Ð³Ð°Ð»ÑƒÐ¶ÐµÐ½Ð½Ñ (форк) %{link_to_the_project}, але воно не вдалоÑÑ Ð· наÑтупної причини:"
@@ -24089,9 +24613,21 @@ msgstr "Ви уже увімкнули двофакторну автентифі
msgid "YouTube"
msgstr "YouTube"
+msgid "Your %{strong}%{plan_name}%{strong_close} subscription for %{strong}%{namespace_name}%{strong_close} will expire on %{strong}%{expires_on}%{strong_close}. After that, you will not to be able to create issues or merge requests as well as many other features."
+msgstr ""
+
msgid "Your %{strong}%{plan_name}%{strong_close} subscription will expire on %{strong}%{expires_on}%{strong_close}. After that, you will not to be able to create issues or merge requests as well as many other features."
msgstr ""
+msgid "Your CSV export has started. It will be emailed to %{email} when complete."
+msgstr ""
+
+msgid "Your CSV export of %{issues_count} from project %{project_link} has been added to this email as an attachment."
+msgstr ""
+
+msgid "Your CSV export of %{written_count} from project %{project_name} (%{project_url}) has been added to this email as an attachment."
+msgstr ""
+
msgid "Your Commit Email will be used for web based operations, such as edits and merges."
msgstr "Ваша адреÑа електронної пошти Ð´Ð»Ñ ÐºÐ¾Ð¼Ñ–Ñ‚Ñ–Ð² буде викориÑтовуватиÑÑ Ð´Ð»Ñ Ð±Ñ€Ð°ÑƒÐ·ÐµÑ€Ð½Ð¸Ñ… операцій, таких Ñк Ñ€ÐµÐ´Ð°Ð³ÑƒÐ²Ð°Ð½Ð½Ñ Ñ‚Ð° злиттÑ."
@@ -24188,6 +24724,9 @@ msgstr "Ðе вдалоÑÑ Ð½Ð°Ð´Ñ–Ñлати ваш коментар! Пере
msgid "Your comment could not be updated! Please check your network connection and try again."
msgstr "Ðе вдалоÑÑ Ð¾Ð½Ð¾Ð²Ð¸Ñ‚Ð¸ ваш коментар! Перевірте Ð¿Ñ–Ð´ÐºÐ»ÑŽÑ‡ÐµÐ½Ð½Ñ Ð´Ð¾ мережі та повторіть Ñпробу."
+msgid "Your comment will be discarded."
+msgstr ""
+
msgid "Your custom stage '%{title}' was created"
msgstr ""
@@ -24227,6 +24766,9 @@ msgstr "Ваш новий токен SCIM"
msgid "Your new personal access token has been created."
msgstr "Ваш новий перÑональний токен доÑтупу Ñтворено."
+msgid "Your new project access token has been created."
+msgstr ""
+
msgid "Your password isn't required to view this page. If a password or any other personal details are requested, please contact your administrator to report abuse."
msgstr "Ваш пароль не потрібен Ð´Ð»Ñ Ð¿ÐµÑ€ÐµÐ³Ð»Ñду цієї Ñторінки. Якщо вимагаєтьÑÑ Ð²Ð²ÐµÑти пароль або будь-Ñкі інші оÑобиÑÑ‚Ñ– дані, зв’ÑжітьÑÑ Ð·Ñ– Ñвоїм адмініÑтратором, щоб повідомити про порушеннÑ."
@@ -24234,7 +24776,7 @@ msgid "Your password reset token has expired."
msgstr "Ваш токен Ð´Ð»Ñ ÑÐºÐ¸Ð´Ð°Ð½Ð½Ñ Ð¿Ð°Ñ€Ð¾Ð»ÑŽ заÑтарів."
msgid "Your profile"
-msgstr ""
+msgstr "Ваш профіль"
msgid "Your project limit is %{limit} projects! Please contact your administrator to increase it"
msgstr "Ваш ліміт проектів Ñкладає %{limit}! Будь лаÑка, звернітьÑÑ Ð´Ð¾ адмініÑтратора, щоб його збільшити"
@@ -24245,12 +24787,18 @@ msgstr "Ваші проекти"
msgid "Your request for access has been queued for review."
msgstr "Ваш запит на Ð¾Ñ‚Ñ€Ð¸Ð¼Ð°Ð½Ð½Ñ Ð´Ð¾Ñтупу поÑтавлено в чергу Ð´Ð»Ñ Ð¿ÐµÑ€ÐµÐ²Ñ–Ñ€ÐºÐ¸."
+msgid "Your search didn't match any commits."
+msgstr ""
+
msgid "Your subscription expired!"
msgstr ""
msgid "Your subscription has been downgraded"
msgstr ""
+msgid "Your subscription will automatically renew in %{remaining_days}"
+msgstr ""
+
msgid "Your subscription will expire in %{remaining_days}"
msgstr ""
@@ -24271,10 +24819,10 @@ msgstr "дизайн"
msgid "about 1 hour"
msgid_plural "about %d hours"
-msgstr[0] ""
-msgstr[1] ""
-msgstr[2] ""
-msgstr[3] ""
+msgstr[0] "близько 1 години"
+msgstr[1] "близько %d годин"
+msgstr[2] "близько %d годин"
+msgstr[3] "близько %d годин"
msgid "activated"
msgstr ""
@@ -24291,8 +24839,8 @@ msgstr "тому"
msgid "allowed to fail"
msgstr "невдача дозволена"
-msgid "already being used for another group or project milestone."
-msgstr "вже викориÑтовуєтьÑÑ Ð´Ð»Ñ Ñ–Ð½ÑˆÐ¾Ð³Ð¾ проектного або групового етапу."
+msgid "already being used for another group or project %{timebox_name}."
+msgstr ""
msgid "already has a \"created\" issue link"
msgstr ""
@@ -24310,7 +24858,7 @@ msgid "any-approver for the project already exists"
msgstr ""
msgid "archived"
-msgstr ""
+msgstr "заархівовано"
msgid "assign yourself"
msgstr "призначити Ñамому Ñобі"
@@ -24369,6 +24917,9 @@ msgstr "%{linkStartTag}ДізнайтеÑÑ Ð±Ñ–Ð»ÑŒÑˆÐµ про Ñкануван
msgid "ciReport|%{linkStartTag}Learn more about SAST %{linkEndTag}"
msgstr "%{linkStartTag}ДізнайтеÑÑ Ð±Ñ–Ð»ÑŒÑˆÐµ про SAST%{linkEndTag}"
+msgid "ciReport|%{linkStartTag}Learn more about Secret Scanning %{linkEndTag}"
+msgstr ""
+
msgid "ciReport|%{linkStartTag}Learn more about codequality reports %{linkEndTag}"
msgstr "%{linkStartTag}ДізнатиÑÑ Ð±Ñ–Ð»ÑŒÑˆÐµ про звіти про ÑкіÑÑ‚ÑŒ коду %{linkEndTag}"
@@ -24542,6 +25093,12 @@ msgstr "Вирішити за допомогою запиту на злиттÑ"
msgid "ciReport|SAST"
msgstr "SAST"
+msgid "ciReport|Secret scanning"
+msgstr ""
+
+msgid "ciReport|Secret scanning detects secrets and credentials vulnerabilities in your source code."
+msgstr ""
+
msgid "ciReport|Security scanning"
msgstr "Перевірка безпеки"
@@ -24712,10 +25269,10 @@ msgstr "не вдалоÑÑ Ð²Ñ–Ð´Ñ…Ð¸Ð»Ð¸Ñ‚Ð¸ пов’Ñзану знахідÐ
msgid "file"
msgid_plural "files"
-msgstr[0] ""
-msgstr[1] ""
-msgstr[2] ""
-msgstr[3] ""
+msgstr[0] "файл"
+msgstr[1] "файли"
+msgstr[2] "файлів"
+msgstr[3] "файлів"
msgid "finding is not found or is already attached to a vulnerability"
msgstr "знахідку втрачено або вже закріплено за вразливіÑÑ‚ÑŽ"
@@ -24747,9 +25304,12 @@ msgstr ""
msgid "group"
msgstr "група"
-msgid "has already been linked to another vulnerability"
+msgid "groups"
msgstr ""
+msgid "has already been linked to another vulnerability"
+msgstr "було прив’Ñзано до іншої вразливоÑÑ‚Ñ–"
+
msgid "has already been taken"
msgstr "уже викориÑтовуєтьÑÑ"
@@ -24791,7 +25351,7 @@ msgid "invalid milestone state `%{state}`"
msgstr "некоректний Ñтан етапу '%{state}'"
msgid "is"
-msgstr ""
+msgstr "Ñ”"
msgid "is already associated to a GitLab Issue. New issue will not be associated."
msgstr ""
@@ -24914,9 +25474,6 @@ msgstr[3] "запитів на злиттÑ"
msgid "merged %{time_ago}"
msgstr ""
-msgid "milestone should belong either to a project or a group."
-msgstr "етап повинен належати до проекту або до групи."
-
msgid "missing"
msgstr "відÑутні"
@@ -25253,6 +25810,9 @@ msgstr "Ðовий запит на злиттÑ"
msgid "no contributions"
msgstr "немає внеÑків"
+msgid "no expiration"
+msgstr ""
+
msgid "no one can merge"
msgstr "ніхто не може виконати злиттÑ"
@@ -25309,6 +25869,9 @@ msgstr "коментар в очікуванні"
msgid "pending removal"
msgstr "очікуєтьÑÑ Ð²Ð¸Ð´Ð°Ð»ÐµÐ½Ð½Ñ"
+msgid "per day"
+msgstr ""
+
msgid "pipeline"
msgstr "конвеєр"
@@ -25341,6 +25904,12 @@ msgstr[3] "проектів"
msgid "project avatar"
msgstr "аватар проекту"
+msgid "projects"
+msgstr ""
+
+msgid "push to your repository, create pipelines, create issues or add comments. To reduce storage capacity, delete unused repositories, artifacts, wikis, issues, and pipelines."
+msgstr ""
+
msgid "quick actions"
msgstr "швидкі дії"
@@ -25489,13 +26058,6 @@ msgstr ""
msgid "this document"
msgstr "цей документ"
-msgid "thread resolved"
-msgid_plural "threads resolved"
-msgstr[0] "Ð¾Ð±Ð³Ð¾Ð²Ð¾Ñ€ÐµÐ½Ð½Ñ Ð·Ð°ÐºÑ€Ð¸Ñ‚Ð¾"
-msgstr[1] "Ð¾Ð±Ð³Ð¾Ð²Ð¾Ñ€ÐµÐ½Ð½Ñ Ð·Ð°ÐºÑ€Ð¸Ñ‚Ñ–"
-msgstr[2] "обговорень закрито"
-msgstr[3] "обговорень закрито"
-
msgid "to help your contributors communicate effectively!"
msgstr "щоб допомогти вашим контриб’юторам ефективно ÑпілкуватиÑÑ!"
@@ -25550,6 +26112,9 @@ msgstr "переглÑнути це на GitLab"
msgid "view the blob"
msgstr "переглÑнути бінарні дані"
+msgid "vulnerability|Add a comment"
+msgstr ""
+
msgid "vulnerability|Add a comment or reason for dismissal"
msgstr "Додати коментар або причину Ð´Ð»Ñ Ð²Ñ–Ð´Ñ…Ð¸Ð»ÐµÐ½Ð½Ñ"
@@ -25572,7 +26137,7 @@ msgid "vulnerability|dismissed"
msgstr "відхилено"
msgid "wiki page"
-msgstr ""
+msgstr "вікі-Ñторінка"
msgid "with %{additions} additions, %{deletions} deletions."
msgstr "з %{additions} додаваннÑми Ñ– %{deletions} видаленнÑми."
diff --git a/locale/ur_PK/gitlab.po b/locale/ur_PK/gitlab.po
index 380eb562032..40304c3ba3b 100644
--- a/locale/ur_PK/gitlab.po
+++ b/locale/ur_PK/gitlab.po
@@ -12,7 +12,7 @@ msgstr ""
"X-Crowdin-Project: gitlab-ee\n"
"X-Crowdin-Language: ur-PK\n"
"X-Crowdin-File: /master/locale/gitlab.pot\n"
-"PO-Revision-Date: 2020-04-15 00:28\n"
+"PO-Revision-Date: 2020-05-05 21:37\n"
msgid " %{start} to %{end}"
msgstr ""
@@ -212,6 +212,11 @@ msgid_plural "%d tags"
msgstr[0] ""
msgstr[1] ""
+msgid "%d unresolved thread"
+msgid_plural "%d unresolved threads"
+msgstr[0] ""
+msgstr[1] ""
+
msgid "%d vulnerability dismissed"
msgid_plural "%d vulnerabilities dismissed"
msgstr[0] ""
@@ -371,6 +376,9 @@ msgstr ""
msgid "%{mrText}, this issue will be closed automatically."
msgstr ""
+msgid "%{namespace_name} is now read-only. You cannot: %{base_message}"
+msgstr ""
+
msgid "%{name} contained %{resultsString}"
msgstr ""
@@ -383,6 +391,11 @@ msgstr ""
msgid "%{name}'s avatar"
msgstr ""
+msgid "%{no_of_days} day"
+msgid_plural "%{no_of_days} days"
+msgstr[0] ""
+msgstr[1] ""
+
msgid "%{number_commits_behind} commits behind %{default_branch}, %{number_commits_ahead} commits ahead"
msgstr ""
@@ -506,6 +519,9 @@ msgstr[1] ""
msgid "%{text} is available"
msgstr ""
+msgid "%{timebox_name} should belong either to a project or a group."
+msgstr ""
+
msgid "%{title} %{operator} %{threshold}"
msgstr ""
@@ -781,12 +797,18 @@ msgstr ""
msgid "<code>\"johnsmith@example.com\": \"johnsmith@example.com\"</code> will add \"By <a href=\"#\">johnsmith@example.com</a>\" to all issues and comments originally created by johnsmith@example.com. By default, the email address or username is masked to ensure the user's privacy. Use this option if you want to show the full email address."
msgstr ""
+msgid "<namespace / project>"
+msgstr ""
+
msgid "<no name set>"
msgstr ""
msgid "<no scopes selected>"
msgstr ""
+msgid "<project name>"
+msgstr ""
+
msgid "<strong>%{group_name}</strong> group members"
msgstr ""
@@ -820,6 +842,9 @@ msgstr ""
msgid "A basic page and serverless function that uses AWS Lambda, AWS API Gateway, and GitLab Pages"
msgstr ""
+msgid "A complete DevOps platform"
+msgstr ""
+
msgid "A default branch cannot be chosen for an empty project."
msgstr ""
@@ -1036,6 +1061,12 @@ msgstr ""
msgid "AccessTokens|reset it"
msgstr ""
+msgid "AccessibilityReport|Accessibility report artifact not found"
+msgstr ""
+
+msgid "AccessibilityReport|Failed to retrieve accessibility report"
+msgstr ""
+
msgid "AccessibilityReport|Learn More"
msgstr ""
@@ -1045,6 +1076,9 @@ msgstr ""
msgid "AccessibilityReport|New"
msgstr ""
+msgid "AccessibilityReport|The accessibility scanning found an error of the following type: %{code}"
+msgstr ""
+
msgid "Account"
msgstr ""
@@ -1287,6 +1321,9 @@ msgstr ""
msgid "Added at"
msgstr ""
+msgid "Added for this merge request"
+msgstr ""
+
msgid "Added in this version"
msgstr ""
@@ -1658,6 +1695,72 @@ msgid_plural "Alerts"
msgstr[0] ""
msgstr[1] ""
+msgid "Alert Details"
+msgstr ""
+
+msgid "AlertManagement|Acknowledged"
+msgstr ""
+
+msgid "AlertManagement|Alert"
+msgstr ""
+
+msgid "AlertManagement|Authorize external service"
+msgstr ""
+
+msgid "AlertManagement|Display alerts from all your monitoring tools directly within GitLab. Streamline the investigation of your alerts and the escalation of alerts to incidents."
+msgstr ""
+
+msgid "AlertManagement|End time"
+msgstr ""
+
+msgid "AlertManagement|End time:"
+msgstr ""
+
+msgid "AlertManagement|Events"
+msgstr ""
+
+msgid "AlertManagement|Events:"
+msgstr ""
+
+msgid "AlertManagement|Full Alert Details"
+msgstr ""
+
+msgid "AlertManagement|More information"
+msgstr ""
+
+msgid "AlertManagement|No alerts available to display. If you think you're seeing this message in error, refresh the page."
+msgstr ""
+
+msgid "AlertManagement|No alerts to display."
+msgstr ""
+
+msgid "AlertManagement|Overview"
+msgstr ""
+
+msgid "AlertManagement|Resolved"
+msgstr ""
+
+msgid "AlertManagement|Severity"
+msgstr ""
+
+msgid "AlertManagement|Start time"
+msgstr ""
+
+msgid "AlertManagement|Start time:"
+msgstr ""
+
+msgid "AlertManagement|Status"
+msgstr ""
+
+msgid "AlertManagement|Surface alerts in GitLab"
+msgstr ""
+
+msgid "AlertManagement|There was an error displaying the alerts. Confirm your endpoint's configuration details to ensure alerts appear."
+msgstr ""
+
+msgid "AlertManagement|Triggered"
+msgstr ""
+
msgid "AlertService|%{linkStart}Learn more%{linkEnd} about configuring this endpoint to receive alerts."
msgstr ""
@@ -1727,6 +1830,9 @@ msgstr ""
msgid "All security scans are enabled because %{linkStart}Auto DevOps%{linkEnd} is enabled on this project"
msgstr ""
+msgid "All threads resolved"
+msgstr ""
+
msgid "All users"
msgstr ""
@@ -1745,6 +1851,9 @@ msgstr ""
msgid "Allow only the selected protocols to be used for Git access."
msgstr ""
+msgid "Allow owners to manage default branch protection per group"
+msgstr ""
+
msgid "Allow owners to manually add users outside of LDAP"
msgstr ""
@@ -1853,6 +1962,9 @@ msgstr ""
msgid "An error occurred fetching the dropdown data."
msgstr ""
+msgid "An error occurred fetching the project authors."
+msgstr ""
+
msgid "An error occurred previewing the blob"
msgstr ""
@@ -1934,6 +2046,9 @@ msgstr ""
msgid "An error occurred while fetching sidebar data"
msgstr ""
+msgid "An error occurred while fetching terraform reports."
+msgstr ""
+
msgid "An error occurred while fetching the Service Desk address."
msgstr ""
@@ -2171,6 +2286,9 @@ msgstr ""
msgid "Any"
msgstr ""
+msgid "Any Author"
+msgstr ""
+
msgid "Any Label"
msgstr ""
@@ -2386,10 +2504,10 @@ msgstr ""
msgid "Are you sure that you want to unarchive this project?"
msgstr ""
-msgid "Are you sure you want to cancel creating this comment?"
+msgid "Are you sure you want to cancel editing this comment?"
msgstr ""
-msgid "Are you sure you want to cancel editing this comment?"
+msgid "Are you sure you want to close this blocked issue?"
msgstr ""
msgid "Are you sure you want to delete %{name}?"
@@ -2419,6 +2537,9 @@ msgstr ""
msgid "Are you sure you want to deploy this environment?"
msgstr ""
+msgid "Are you sure you want to discard this comment?"
+msgstr ""
+
msgid "Are you sure you want to erase this build?"
msgstr ""
@@ -2595,6 +2716,9 @@ msgstr ""
msgid "At least one approval from a code owner is required to change files matching the respective CODEOWNER rules."
msgstr ""
+msgid "At least one logging option is required to be enabled"
+msgstr ""
+
msgid "At least one of group_id or project_id must be specified"
msgstr ""
@@ -2633,6 +2757,27 @@ msgstr ""
msgid "AuditEvents|Target"
msgstr ""
+msgid "AuditLogs|(removed)"
+msgstr ""
+
+msgid "AuditLogs|Action"
+msgstr ""
+
+msgid "AuditLogs|Author"
+msgstr ""
+
+msgid "AuditLogs|Date"
+msgstr ""
+
+msgid "AuditLogs|IP Address"
+msgstr ""
+
+msgid "AuditLogs|Object"
+msgstr ""
+
+msgid "AuditLogs|Target"
+msgstr ""
+
msgid "Aug"
msgstr ""
@@ -2915,9 +3060,6 @@ msgstr ""
msgid "BambooService|You must set up automatic revision labeling and a repository trigger in Bamboo."
msgstr ""
-msgid "Batch operations"
-msgstr ""
-
msgid "BatchComments|Delete all pending comments"
msgstr ""
@@ -3047,6 +3189,9 @@ msgstr ""
msgid "Boards"
msgstr ""
+msgid "Boards and Board Lists"
+msgstr ""
+
msgid "Boards|Collapse"
msgstr ""
@@ -3266,6 +3411,9 @@ msgstr ""
msgid "Buy GitLab Enterprise Edition"
msgstr ""
+msgid "Buy more Pipeline minutes"
+msgstr ""
+
msgid "By %{user_name}"
msgstr ""
@@ -3386,6 +3534,9 @@ msgstr ""
msgid "Can't scan the code?"
msgstr ""
+msgid "Can't update snippet: %{err}"
+msgstr ""
+
msgid "Canary"
msgstr ""
@@ -3554,6 +3705,9 @@ msgstr ""
msgid "Changing group path can have unintended side effects."
msgstr ""
+msgid "Charts"
+msgstr ""
+
msgid "Charts can't be displayed as the request for data has timed out. %{documentationLink}"
msgstr ""
@@ -3590,9 +3744,6 @@ msgstr ""
msgid "ChatMessage|and [%{count} more](%{pipeline_failed_jobs_url})"
msgstr ""
-msgid "ChatMessage|failed"
-msgstr ""
-
msgid "ChatMessage|has failed"
msgstr ""
@@ -3608,9 +3759,6 @@ msgstr ""
msgid "ChatMessage|in %{project_link}"
msgstr ""
-msgid "ChatMessage|passed"
-msgstr ""
-
msgid "Check again"
msgstr ""
@@ -4061,9 +4209,6 @@ msgstr ""
msgid "Click any <strong>project name</strong> in the project list below to navigate to the project milestone."
msgstr ""
-msgid "Click here"
-msgstr ""
-
msgid "Click the <strong>Download</strong> button and wait for downloading to complete."
msgstr ""
@@ -4451,6 +4596,12 @@ msgstr ""
msgid "ClusterIntegration|Fetching zones"
msgstr ""
+msgid "ClusterIntegration|Fluentd"
+msgstr ""
+
+msgid "ClusterIntegration|Fluentd is an open source data collector, which lets you unify the data collection and consumption for a better use and understanding of data. It requires at least one of the following logs to be successfully installed."
+msgstr ""
+
msgid "ClusterIntegration|GitLab Integration"
msgstr ""
@@ -4757,6 +4908,15 @@ msgstr ""
msgid "ClusterIntegration|Request to begin uninstalling failed"
msgstr ""
+msgid "ClusterIntegration|SIEM Hostname"
+msgstr ""
+
+msgid "ClusterIntegration|SIEM Port"
+msgstr ""
+
+msgid "ClusterIntegration|SIEM Protocol"
+msgstr ""
+
msgid "ClusterIntegration|Save changes"
msgstr ""
@@ -4853,6 +5013,12 @@ msgstr ""
msgid "ClusterIntegration|Select zone to choose machine type"
msgstr ""
+msgid "ClusterIntegration|Send Cilium Logs"
+msgstr ""
+
+msgid "ClusterIntegration|Send ModSecurity Logs"
+msgstr ""
+
msgid "ClusterIntegration|Service Token"
msgstr ""
@@ -5281,18 +5447,33 @@ msgstr ""
msgid "Compliance framework (optional)"
msgstr ""
+msgid "ComplianceFramework|GDPR"
+msgstr ""
+
msgid "ComplianceFramework|GDPR - General Data Protection Regulation"
msgstr ""
+msgid "ComplianceFramework|HIPAA"
+msgstr ""
+
msgid "ComplianceFramework|HIPAA - Health Insurance Portability and Accountability Act"
msgstr ""
+msgid "ComplianceFramework|PCI-DSS"
+msgstr ""
+
msgid "ComplianceFramework|PCI-DSS - Payment Card Industry-Data Security Standard"
msgstr ""
+msgid "ComplianceFramework|SOC 2"
+msgstr ""
+
msgid "ComplianceFramework|SOC 2 - Service Organization Control 2"
msgstr ""
+msgid "ComplianceFramework|SOX"
+msgstr ""
+
msgid "ComplianceFramework|SOX - Sarbanes-Oxley"
msgstr ""
@@ -5440,9 +5621,15 @@ msgstr ""
msgid "Container repositories sync capacity"
msgstr ""
+msgid "ContainerRegistry| Please visit the %{linkStart}administration settings%{linkEnd} to enable this feature."
+msgstr ""
+
msgid "ContainerRegistry|%{imageName} tags"
msgstr ""
+msgid "ContainerRegistry|%{title} was successfully scheduled for deletion"
+msgstr ""
+
msgid "ContainerRegistry|Automatically remove extra images that aren't designed to be kept."
msgstr ""
@@ -5455,6 +5642,9 @@ msgstr ""
msgid "ContainerRegistry|Container Registry"
msgstr ""
+msgid "ContainerRegistry|Container Registry tag expiration and retention policy is disabled"
+msgstr ""
+
msgid "ContainerRegistry|Copy build command"
msgstr ""
@@ -5464,18 +5654,12 @@ msgstr ""
msgid "ContainerRegistry|Copy push command"
msgstr ""
-msgid "ContainerRegistry|Currently, the Container Registry tag expiration feature is not available for projects created before GitLab version 12.8. For updates and more information, visit Issue %{linkStart}#196124%{linkEnd}"
-msgstr ""
-
msgid "ContainerRegistry|Docker connection error"
msgstr ""
msgid "ContainerRegistry|Docker tag expiration policy is %{toggleStatus}"
msgstr ""
-msgid "ContainerRegistry|Docker tags with names matching this regex pattern will expire:"
-msgstr ""
-
msgid "ContainerRegistry|Edit Settings"
msgstr ""
@@ -5497,9 +5681,6 @@ msgstr ""
msgid "ContainerRegistry|Image ID"
msgstr ""
-msgid "ContainerRegistry|Image deleted successfully"
-msgstr ""
-
msgid "ContainerRegistry|Keep and protect the images that matter most."
msgstr ""
@@ -5515,12 +5696,24 @@ msgstr ""
msgid "ContainerRegistry|Number of tags to retain:"
msgstr ""
+msgid "ContainerRegistry|Please contact your administrator."
+msgstr ""
+
msgid "ContainerRegistry|Push an image"
msgstr ""
msgid "ContainerRegistry|Quick Start"
msgstr ""
+msgid "ContainerRegistry|Regular expressions such as %{codeStart}.*-test%{codeEnd} or %{codeStart}dev-.*%{codeEnd} are supported"
+msgstr ""
+
+msgid "ContainerRegistry|Regular expressions such as %{codeStart}.*-test%{codeEnd} or %{codeStart}dev-.*%{codeEnd} are supported. To select all tags, use %{codeStart}.*%{codeEnd}"
+msgstr ""
+
+msgid "ContainerRegistry|Remember to run %{docLinkStart}garbage collection%{docLinkEnd} to remove the stale data from storage."
+msgstr ""
+
msgid "ContainerRegistry|Remove repository"
msgstr ""
@@ -5535,22 +5728,22 @@ msgstr[1] ""
msgid "ContainerRegistry|Retention policy has been Enabled"
msgstr ""
-msgid "ContainerRegistry|Something went wrong while deleting the image."
+msgid "ContainerRegistry|Something went wrong while fetching the expiration policy."
msgstr ""
-msgid "ContainerRegistry|Something went wrong while deleting the tag."
+msgid "ContainerRegistry|Something went wrong while fetching the repository list."
msgstr ""
-msgid "ContainerRegistry|Something went wrong while deleting the tags."
+msgid "ContainerRegistry|Something went wrong while fetching the tags list."
msgstr ""
-msgid "ContainerRegistry|Something went wrong while fetching the expiration policy."
+msgid "ContainerRegistry|Something went wrong while marking the tag for deletion."
msgstr ""
-msgid "ContainerRegistry|Something went wrong while fetching the packages list."
+msgid "ContainerRegistry|Something went wrong while marking the tags for deletion."
msgstr ""
-msgid "ContainerRegistry|Something went wrong while fetching the tags list."
+msgid "ContainerRegistry|Something went wrong while scheduling %{title} for deletion. Please try again."
msgstr ""
msgid "ContainerRegistry|Something went wrong while updating the expiration policy."
@@ -5559,16 +5752,25 @@ msgstr ""
msgid "ContainerRegistry|Tag"
msgstr ""
-msgid "ContainerRegistry|Tag deleted successfully"
-msgstr ""
-
msgid "ContainerRegistry|Tag expiration policy"
msgstr ""
msgid "ContainerRegistry|Tag expiration policy is designed to:"
msgstr ""
-msgid "ContainerRegistry|Tags deleted successfully"
+msgid "ContainerRegistry|Tag successfully marked for deletion."
+msgstr ""
+
+msgid "ContainerRegistry|Tags successfully marked for deletion."
+msgstr ""
+
+msgid "ContainerRegistry|Tags with names matching this regex pattern will %{italicStart}be preserved:%{italicEnd}"
+msgstr ""
+
+msgid "ContainerRegistry|Tags with names matching this regex pattern will %{italicStart}expire:%{italicEnd}"
+msgstr ""
+
+msgid "ContainerRegistry|The Container Registry tag expiration and retention policies for this project have not been enabled."
msgstr ""
msgid "ContainerRegistry|The last tag related to this image was recently removed. This empty image and any associated data will be automatically removed as part of the regular Garbage Collection process. If you have any questions, contact your administrator."
@@ -5589,16 +5791,16 @@ msgstr ""
msgid "ContainerRegistry|There are no container images stored for this project"
msgstr ""
-msgid "ContainerRegistry|This Registry contains deleted image tag data. Remember to run %{docLinkStart}garbage collection%{docLinkEnd} to remove the stale data from storage."
+msgid "ContainerRegistry|There was an error during the deletion of this image repository, please try again."
msgstr ""
msgid "ContainerRegistry|This image has no active tags"
msgstr ""
-msgid "ContainerRegistry|We are having trouble connecting to Docker, which could be due to an issue with your project name or path. %{docLinkStart}More Information%{docLinkEnd}"
+msgid "ContainerRegistry|This image repository is scheduled for deletion"
msgstr ""
-msgid "ContainerRegistry|Wildcards such as %{codeStart}.*-stable%{codeEnd} or %{codeStart}production/.*%{codeEnd} are supported. To select all tags, use %{codeStart}.*%{codeEnd}"
+msgid "ContainerRegistry|We are having trouble connecting to Docker, which could be due to an issue with your project name or path. %{docLinkStart}More Information%{docLinkEnd}"
msgstr ""
msgid "ContainerRegistry|With the Container Registry, every project can have its own space to store its Docker images. %{docLinkStart}More Information%{docLinkEnd}"
@@ -5853,7 +6055,7 @@ msgstr ""
msgid "Could not delete chat nickname %{chat_name}."
msgstr ""
-msgid "Could not find design"
+msgid "Could not find design."
msgstr ""
msgid "Could not remove the trigger."
@@ -5868,6 +6070,9 @@ msgstr ""
msgid "Could not revoke personal access token %{personal_access_token_name}."
msgstr ""
+msgid "Could not revoke project access token %{project_access_token_name}."
+msgstr ""
+
msgid "Could not save group ID"
msgstr ""
@@ -5925,6 +6130,9 @@ msgstr ""
msgid "Create a new branch"
msgstr ""
+msgid "Create a new deploy key for this project"
+msgstr ""
+
msgid "Create a new file as there are no files yet. Afterwards, you'll be able to commit your changes."
msgstr ""
@@ -6030,6 +6238,9 @@ msgstr ""
msgid "Create requirement"
msgstr ""
+msgid "Create snippet"
+msgstr ""
+
msgid "Create wildcard: %{searchTerm}"
msgstr ""
@@ -6051,6 +6262,9 @@ msgstr ""
msgid "Created"
msgstr ""
+msgid "Created %{timestamp}"
+msgstr ""
+
msgid "Created At"
msgstr ""
@@ -6407,6 +6621,9 @@ msgstr ""
msgid "CycleAnalytics|stage dropdown"
msgstr ""
+msgid "DAG"
+msgstr ""
+
msgid "DNS"
msgstr ""
@@ -6773,6 +6990,9 @@ msgstr ""
msgid "Deploy key was successfully updated."
msgstr ""
+msgid "Deploy keys allow read-only or read-write (if enabled) access to your repository. Deploy keys can be used for CI, staging or production servers. You can create a deploy key or add an existing one."
+msgstr ""
+
msgid "Deploy progress not found. To see pods, ensure your environment matches %{linkStart}deploy board criteria%{linkEnd}."
msgstr ""
@@ -6926,6 +7146,9 @@ msgstr ""
msgid "Deploying to"
msgstr ""
+msgid "Deployment Frequency"
+msgstr ""
+
msgid "Deployment|API"
msgstr ""
@@ -7444,9 +7667,6 @@ msgstr ""
msgid "Edit environment"
msgstr ""
-msgid "Edit epic description"
-msgstr ""
-
msgid "Edit file"
msgstr ""
@@ -8068,9 +8288,6 @@ msgstr ""
msgid "Epics"
msgstr ""
-msgid "Epics (Ultimate / Gold license only)"
-msgstr ""
-
msgid "Epics Roadmap"
msgstr ""
@@ -8080,6 +8297,9 @@ msgstr ""
msgid "Epics let you manage your portfolio of projects more efficiently and with less effort"
msgstr ""
+msgid "Epics, Issues, and Merge Requests"
+msgstr ""
+
msgid "Epics|Add an epic"
msgstr ""
@@ -8131,6 +8351,9 @@ msgstr ""
msgid "Epics|Something went wrong while fetching group epics."
msgstr ""
+msgid "Epics|Something went wrong while moving item."
+msgstr ""
+
msgid "Epics|Something went wrong while ordering item."
msgstr ""
@@ -8185,6 +8408,9 @@ msgstr ""
msgid "Error fetching network graph."
msgstr ""
+msgid "Error fetching payload data."
+msgstr ""
+
msgid "Error fetching projects"
msgstr ""
@@ -8194,9 +8420,6 @@ msgstr ""
msgid "Error fetching the dependency list. Please check your network connection and try again."
msgstr ""
-msgid "Error fetching usage ping data."
-msgstr ""
-
msgid "Error loading branch data. Please try again."
msgstr ""
@@ -8338,6 +8561,9 @@ msgstr ""
msgid "Errors"
msgstr ""
+msgid "Errors:"
+msgstr ""
+
msgid "Estimated"
msgstr ""
@@ -8542,12 +8768,18 @@ msgstr ""
msgid "Export as CSV"
msgstr ""
+msgid "Export group"
+msgstr ""
+
msgid "Export issues"
msgstr ""
msgid "Export project"
msgstr ""
+msgid "Export this group with all related data to a new GitLab instance. Once complete, you can import the data file from the \"New Group\" page."
+msgstr ""
+
msgid "Export this project with all its related data in order to move your project to a new GitLab instance. Once the export is finished, you can import the file from the \"New Project\" page."
msgstr ""
@@ -8617,9 +8849,6 @@ msgstr ""
msgid "Failed Jobs"
msgstr ""
-msgid "Failed create wiki"
-msgstr ""
-
msgid "Failed to add a Zoom meeting"
msgstr ""
@@ -8659,6 +8888,9 @@ msgstr ""
msgid "Failed to create resources"
msgstr ""
+msgid "Failed to create wiki"
+msgstr ""
+
msgid "Failed to delete board. Please try again."
msgstr ""
@@ -8668,7 +8900,7 @@ msgstr ""
msgid "Failed to enqueue the rebase operation, possibly due to a long-lived transaction. Try again later."
msgstr ""
-msgid "Failed to find import label for jira import."
+msgid "Failed to find import label for Jira import."
msgstr ""
msgid "Failed to get ref."
@@ -8692,6 +8924,12 @@ msgstr ""
msgid "Failed to load groups & users."
msgstr ""
+msgid "Failed to load labels. Please try again."
+msgstr ""
+
+msgid "Failed to load milestones. Please try again."
+msgstr ""
+
msgid "Failed to load related branches"
msgstr ""
@@ -9067,7 +9305,10 @@ msgstr ""
msgid "Filter by milestone name"
msgstr ""
-msgid "Filter by name..."
+msgid "Filter by name"
+msgstr ""
+
+msgid "Filter by status"
msgstr ""
msgid "Filter by two-factor authentication"
@@ -9079,12 +9320,18 @@ msgstr ""
msgid "Filter projects"
msgstr ""
+msgid "Filter results"
+msgstr ""
+
msgid "Filter results by group"
msgstr ""
msgid "Filter results by project"
msgstr ""
+msgid "Filter results..."
+msgstr ""
+
msgid "Filter your projects by name"
msgstr ""
@@ -9373,10 +9620,10 @@ msgstr ""
msgid "GeoNodes|Checksummed"
msgstr ""
-msgid "GeoNodes|Container repositories"
+msgid "GeoNodes|Consult Geo troubleshooting information"
msgstr ""
-msgid "GeoNodes|Data is out of date from %{timeago}"
+msgid "GeoNodes|Container repositories"
msgstr ""
msgid "GeoNodes|Data replication lag"
@@ -9418,6 +9665,9 @@ msgstr ""
msgid "GeoNodes|Last event ID seen from primary"
msgstr ""
+msgid "GeoNodes|Learn more about Geo node statuses"
+msgstr ""
+
msgid "GeoNodes|Loading nodes"
msgstr ""
@@ -9433,6 +9683,9 @@ msgstr ""
msgid "GeoNodes|Node was successfully removed."
msgstr ""
+msgid "GeoNodes|Node's status was updated %{timeAgo}."
+msgstr ""
+
msgid "GeoNodes|Not checksummed"
msgstr ""
@@ -9463,7 +9716,10 @@ msgstr ""
msgid "GeoNodes|Repository verification progress"
msgstr ""
-msgid "GeoNodes|Selective"
+msgid "GeoNodes|Selective (%{syncLabel})"
+msgstr ""
+
+msgid "GeoNodes|Selective synchronization"
msgstr ""
msgid "GeoNodes|Something went wrong while changing node status"
@@ -9490,6 +9746,9 @@ msgstr ""
msgid "GeoNodes|Unverified"
msgstr ""
+msgid "GeoNodes|Updated %{timeAgo}"
+msgstr ""
+
msgid "GeoNodes|Used slots"
msgstr ""
@@ -9529,16 +9788,16 @@ msgstr ""
msgid "Geo|All"
msgstr ""
-msgid "Geo|All projects"
+msgid "Geo|All %{replicable_type}"
msgstr ""
-msgid "Geo|All projects are being scheduled for re-sync"
+msgid "Geo|All projects"
msgstr ""
-msgid "Geo|All projects are being scheduled for re-verify"
+msgid "Geo|All projects are being scheduled for resync"
msgstr ""
-msgid "Geo|Batch operations"
+msgid "Geo|All projects are being scheduled for reverify"
msgstr ""
msgid "Geo|Could not remove tracking entry for an existing project."
@@ -9550,9 +9809,15 @@ msgstr ""
msgid "Geo|Failed"
msgstr ""
+msgid "Geo|Filter by status"
+msgstr ""
+
msgid "Geo|Geo Status"
msgstr ""
+msgid "Geo|In progress"
+msgstr ""
+
msgid "Geo|In sync"
msgstr ""
@@ -9577,9 +9842,6 @@ msgstr ""
msgid "Geo|Not synced yet"
msgstr ""
-msgid "Geo|Pending"
-msgstr ""
-
msgid "Geo|Pending synchronization"
msgstr ""
@@ -9616,7 +9878,7 @@ msgstr ""
msgid "Geo|Resync"
msgstr ""
-msgid "Geo|Resync all projects"
+msgid "Geo|Resync all"
msgstr ""
msgid "Geo|Retry count"
@@ -9625,7 +9887,7 @@ msgstr ""
msgid "Geo|Reverify"
msgstr ""
-msgid "Geo|Reverify all projects"
+msgid "Geo|Reverify all"
msgstr ""
msgid "Geo|Status"
@@ -9967,9 +10229,6 @@ msgstr ""
msgid "Go to file"
msgstr ""
-msgid "Go to file (MRs only)"
-msgstr ""
-
msgid "Go to file permalink (while viewing a file)"
msgstr ""
@@ -10414,6 +10673,9 @@ msgstr ""
msgid "GroupSettings|Disable group mentions"
msgstr ""
+msgid "GroupSettings|Export group"
+msgstr ""
+
msgid "GroupSettings|If the parent group's visibility is lower than the group current visibility, visibility levels for subgroups and projects will be changed to match the new parent group's visibility."
msgstr ""
@@ -10492,6 +10754,9 @@ msgstr ""
msgid "Groups (%{count})"
msgstr ""
+msgid "Groups (%{groups})"
+msgstr ""
+
msgid "Groups can also be nested by creating %{subgroup_docs_link_start}subgroups%{subgroup_docs_link_end}."
msgstr ""
@@ -10757,15 +11022,12 @@ msgstr ""
msgid "ID:"
msgstr ""
-msgid "IDE|Allow live previews of JavaScript projects in the Web IDE using CodeSandbox client side evaluation."
+msgid "IDE|Allow live previews of JavaScript projects in the Web IDE using CodeSandbox Live Preview."
msgstr ""
msgid "IDE|Back"
msgstr ""
-msgid "IDE|Client side evaluation"
-msgstr ""
-
msgid "IDE|Commit"
msgstr ""
@@ -10829,6 +11091,9 @@ msgstr ""
msgid "If any job surpasses this timeout threshold, it will be marked as failed. Human readable time input language is accepted like \"1 hour\". Values without specification represent seconds."
msgstr ""
+msgid "If blank, set allowable lifetime to %{instance_level_policy_in_words}, as defined by the instance admin. Once set, existing tokens for users in this group may be revoked."
+msgstr ""
+
msgid "If checked, group owners can manage LDAP group links and LDAP member overrides"
msgstr ""
@@ -10868,6 +11133,9 @@ msgstr ""
msgid "If you lose your recovery codes you can generate new ones, invalidating all previous codes."
msgstr ""
+msgid "If you reach 100%% storage capacity, you will not be able to: %{base_message}"
+msgstr ""
+
msgid "If your HTTP repository is not publicly accessible, add your credentials."
msgstr ""
@@ -10979,6 +11247,9 @@ msgstr ""
msgid "Import repository"
msgstr ""
+msgid "Import started by: %{importInitiator}"
+msgstr ""
+
msgid "Import tasks"
msgstr ""
@@ -11045,6 +11316,9 @@ msgstr ""
msgid "In order to tailor your experience with GitLab we<br>would like to know a bit more about you."
msgstr ""
+msgid "In progress"
+msgstr ""
+
msgid "In the next step, you'll be able to select the projects you want to import."
msgstr ""
@@ -11392,15 +11666,15 @@ msgstr ""
msgid "Issues"
msgstr ""
-msgid "Issues / Merge Requests"
-msgstr ""
-
msgid "Issues Analytics"
msgstr ""
msgid "Issues Rate Limits"
msgstr ""
+msgid "Issues and Merge Requests"
+msgstr ""
+
msgid "Issues can be bugs, tasks or ideas to be discussed. Also, issues are searchable and filterable."
msgstr ""
@@ -11422,10 +11696,10 @@ msgstr ""
msgid "IssuesAnalytics|Avg/Month:"
msgstr ""
-msgid "IssuesAnalytics|Issues created"
+msgid "IssuesAnalytics|Issues opened"
msgstr ""
-msgid "IssuesAnalytics|Issues created per month"
+msgid "IssuesAnalytics|Issues opened per month"
msgstr ""
msgid "IssuesAnalytics|Last 12 months"
@@ -11479,6 +11753,9 @@ msgstr ""
msgid "Jira integration not configured."
msgstr ""
+msgid "Jira project: %{importProject}"
+msgstr ""
+
msgid "JiraService|Events for %{noteable_model_name} are disabled."
msgstr ""
@@ -11868,6 +12145,9 @@ msgstr ""
msgid "Lead"
msgstr ""
+msgid "Lead Time"
+msgstr ""
+
msgid "Learn GitLab"
msgstr ""
@@ -11880,6 +12160,9 @@ msgstr ""
msgid "Learn how to %{no_packages_link_start}publish and share your packages%{no_packages_link_end} with GitLab."
msgstr ""
+msgid "Learn how to enable synchronization"
+msgstr ""
+
msgid "Learn more"
msgstr ""
@@ -12211,9 +12494,6 @@ msgstr ""
msgid "Live preview"
msgstr ""
-msgid "Load more vulnerabilities"
-msgstr ""
-
msgid "Loading"
msgstr ""
@@ -12364,9 +12644,6 @@ msgstr ""
msgid "Manage"
msgstr ""
-msgid "Manage Git repositories with fine-grained access controls that keep your code secure. Perform code reviews and enhance collaboration with merge requests. Each project can also have an issue tracker and a wiki."
-msgstr ""
-
msgid "Manage Web IDE features"
msgstr ""
@@ -12397,6 +12674,9 @@ msgstr ""
msgid "Manage your license"
msgstr ""
+msgid "Managed Account"
+msgstr ""
+
msgid "Manifest"
msgstr ""
@@ -12661,6 +12941,9 @@ msgstr ""
msgid "Merge Requests in Review"
msgstr ""
+msgid "Merge automatically (%{strategy})"
+msgstr ""
+
msgid "Merge commit message"
msgstr ""
@@ -12814,6 +13097,12 @@ msgstr ""
msgid "Merged branches are being deleted. This can take some time depending on the number of branches. Please refresh the page to see changes."
msgstr ""
+msgid "Merged this merge request."
+msgstr ""
+
+msgid "Merges this merge request immediately."
+msgstr ""
+
msgid "Merges this merge request when the pipeline succeeds."
msgstr ""
@@ -12921,12 +13210,15 @@ msgid_plural "Metrics|Edit metrics"
msgstr[0] ""
msgstr[1] ""
-msgid "Metrics|Environment"
+msgid "Metrics|Expand panel"
msgstr ""
msgid "Metrics|For grouping similar metrics"
msgstr ""
+msgid "Metrics|Go back (Esc)"
+msgstr ""
+
msgid "Metrics|Invalid time range, please verify."
msgstr ""
@@ -12957,9 +13249,6 @@ msgstr ""
msgid "Metrics|Refresh dashboard"
msgstr ""
-msgid "Metrics|Show last"
-msgstr ""
-
msgid "Metrics|There was an error creating the dashboard."
msgstr ""
@@ -13038,6 +13327,9 @@ msgstr ""
msgid "Microsoft Azure"
msgstr ""
+msgid "Middleman project with Static Site Editor support"
+msgstr ""
+
msgid "Migrated %{success_count}/%{total_count} files."
msgstr ""
@@ -13483,10 +13775,10 @@ msgstr ""
msgid "Next"
msgstr ""
-msgid "Next file in diff (MRs only)"
+msgid "Next file in diff"
msgstr ""
-msgid "Next unresolved discussion (MRs only)"
+msgid "Next unresolved discussion"
msgstr ""
msgid "Nickname"
@@ -13546,6 +13838,9 @@ msgstr ""
msgid "No changes between %{ref_start}%{source_branch}%{ref_end} and %{ref_start}%{target_branch}%{ref_end}"
msgstr ""
+msgid "No child epics match applied filters"
+msgstr ""
+
msgid "No connection could be made to a Gitaly Server, please check your logs!"
msgstr ""
@@ -13684,15 +13979,6 @@ msgstr ""
msgid "No thanks, don't show this again"
msgstr ""
-msgid "No vulnerabilities found for this group"
-msgstr ""
-
-msgid "No vulnerabilities found for this pipeline"
-msgstr ""
-
-msgid "No vulnerabilities found for this project"
-msgstr ""
-
msgid "No vulnerabilities present"
msgstr ""
@@ -13816,6 +14102,9 @@ msgstr ""
msgid "Nothing to preview."
msgstr ""
+msgid "Nothing to synchronize"
+msgstr ""
+
msgid "Notification events"
msgstr ""
@@ -13987,6 +14276,9 @@ msgstr ""
msgid "Once removed, the fork relationship cannot be restored and you will no longer be able to send merge requests to the source."
msgstr ""
+msgid "Once the exported file is ready you can download it from this page."
+msgstr ""
+
msgid "Once the exported file is ready, you will receive a notification email with a download link, or you can download it from this page."
msgstr ""
@@ -14088,9 +14380,6 @@ msgstr ""
msgid "Open sidebar"
msgstr ""
-msgid "Open source software to collaborate on code"
-msgstr ""
-
msgid "Open: %{openIssuesCount}"
msgstr ""
@@ -14193,6 +14482,9 @@ msgstr ""
msgid "OutdatedBrowser|You can provide feedback %{feedback_link_start}on this issue%{feedback_link_end} or via your usual support channels."
msgstr ""
+msgid "Overridden"
+msgstr ""
+
msgid "Overview"
msgstr ""
@@ -14244,6 +14536,9 @@ msgstr ""
msgid "PackageRegistry|Conan Command"
msgstr ""
+msgid "PackageRegistry|Copy .pypirc content"
+msgstr ""
+
msgid "PackageRegistry|Copy Conan Command"
msgstr ""
@@ -14265,6 +14560,9 @@ msgstr ""
msgid "PackageRegistry|Copy NuGet Setup Command"
msgstr ""
+msgid "PackageRegistry|Copy Pip command"
+msgstr ""
+
msgid "PackageRegistry|Copy and paste this inside your %{codeStart}pom.xml%{codeEnd} %{codeStart}dependencies%{codeEnd} block."
msgstr ""
@@ -14298,6 +14596,12 @@ msgstr ""
msgid "PackageRegistry|For more information on the NuGet registry, %{linkStart}see the documentation%{linkEnd}."
msgstr ""
+msgid "PackageRegistry|For more information on the PyPi registry, %{linkStart}see the documentation%{linkEnd}."
+msgstr ""
+
+msgid "PackageRegistry|If you haven't already done so, you will need to add the below to your %{codeStart}.pypirc%{codeEnd} file."
+msgstr ""
+
msgid "PackageRegistry|If you haven't already done so, you will need to add the below to your %{codeStart}pom.xml%{codeEnd} file."
msgstr ""
@@ -14307,6 +14611,9 @@ msgstr ""
msgid "PackageRegistry|Learn how to %{noPackagesLinkStart}publish and share your packages%{noPackagesLinkEnd} with GitLab."
msgstr ""
+msgid "PackageRegistry|Manually Published"
+msgstr ""
+
msgid "PackageRegistry|Maven"
msgstr ""
@@ -14325,12 +14632,18 @@ msgstr ""
msgid "PackageRegistry|NuGet Command"
msgstr ""
+msgid "PackageRegistry|Pip Command"
+msgstr ""
+
msgid "PackageRegistry|Pipeline %{linkStart}%{linkEnd} triggered %{timestamp} by %{author}"
msgstr ""
msgid "PackageRegistry|Published to the repository at %{timestamp}"
msgstr ""
+msgid "PackageRegistry|PyPi"
+msgstr ""
+
msgid "PackageRegistry|Registry Setup"
msgstr ""
@@ -14367,6 +14680,9 @@ msgstr ""
msgid "PackageRegistry|npm"
msgstr ""
+msgid "PackageRegistry|published by %{author}"
+msgstr ""
+
msgid "PackageRegistry|yarn"
msgstr ""
@@ -14382,6 +14698,9 @@ msgstr ""
msgid "PackageType|NuGet"
msgstr ""
+msgid "PackageType|PyPi"
+msgstr ""
+
msgid "Packages"
msgstr ""
@@ -14475,6 +14794,9 @@ msgstr ""
msgid "Past due"
msgstr ""
+msgid "Paste a machine public key here. Read more about how to generate it"
+msgstr ""
+
msgid "Paste a machine public key here. Read more about how to generate it %{link_start}here%{link_end}"
msgstr ""
@@ -14490,9 +14812,6 @@ msgstr ""
msgid "Path"
msgstr ""
-msgid "Path, transfer, remove"
-msgstr ""
-
msgid "Path:"
msgstr ""
@@ -14523,7 +14842,7 @@ msgstr ""
msgid "Percentage"
msgstr ""
-msgid "Perform advanced options such as changing path, transferring, or removing the group."
+msgid "Perform advanced options such as changing path, transferring, exporting, or removing the group."
msgstr ""
msgid "Perform common operations on GitLab project"
@@ -14604,9 +14923,6 @@ msgstr ""
msgid "Pipeline minutes quota"
msgstr ""
-msgid "Pipeline quota"
-msgstr ""
-
msgid "Pipeline subscriptions"
msgstr ""
@@ -14616,6 +14932,9 @@ msgstr ""
msgid "Pipeline: %{status}"
msgstr ""
+msgid "PipelineCharts|CI / CD Analytics"
+msgstr ""
+
msgid "PipelineCharts|Failed:"
msgstr ""
@@ -14694,15 +15013,6 @@ msgstr ""
msgid "Pipelines settings for '%{project_name}' were successfully updated."
msgstr ""
-msgid "Pipelines| to purchase more minutes."
-msgstr ""
-
-msgid "Pipelines|%{namespace_name} has exceeded its pipeline minutes quota."
-msgstr ""
-
-msgid "Pipelines|%{namespace_name} has less than %{notification_level}%% of CI minutes available."
-msgstr ""
-
msgid "Pipelines|API"
msgstr ""
@@ -14724,10 +15034,13 @@ msgstr ""
msgid "Pipelines|Get started with Pipelines"
msgstr ""
-msgid "Pipelines|Loading Pipelines"
+msgid "Pipelines|Group %{namespace_name} has %{percentage}%% or less Shared Runner Pipeline minutes remaining. Once it runs out, no new jobs or pipelines in its projects will run."
msgstr ""
-msgid "Pipelines|Pipelines will not run anymore on shared Runners."
+msgid "Pipelines|Group %{namespace_name} has exceeded its pipeline minutes quota. Unless you buy additional pipeline minutes, no new jobs or pipelines in its projects will run."
+msgstr ""
+
+msgid "Pipelines|Loading Pipelines"
msgstr ""
msgid "Pipelines|Project cache successfully reset."
@@ -14958,6 +15271,9 @@ msgstr ""
msgid "Please select"
msgstr ""
+msgid "Please select a Jira project"
+msgstr ""
+
msgid "Please select a country"
msgstr ""
@@ -15003,6 +15319,9 @@ msgstr ""
msgid "Please wait while we import the repository for you. Refresh at will."
msgstr ""
+msgid "Plugins directory is deprecated and will be removed in 14.0. Please move this file into /file_hooks directory."
+msgstr ""
+
msgid "Pod does not exist"
msgstr ""
@@ -15120,7 +15439,7 @@ msgstr ""
msgid "Prevent users from changing their profile name"
msgstr ""
-msgid "Prevent users from modifing merge request approvers list"
+msgid "Prevent users from modifying merge request approvers list"
msgstr ""
msgid "Prevent users from performing write operations on GitLab while performing maintenance."
@@ -15141,10 +15460,10 @@ msgstr ""
msgid "Previous Artifacts"
msgstr ""
-msgid "Previous file in diff (MRs only)"
+msgid "Previous file in diff"
msgstr ""
-msgid "Previous unresolved discussion (MRs only)"
+msgid "Previous unresolved discussion"
msgstr ""
msgid "Primary"
@@ -15177,6 +15496,9 @@ msgstr ""
msgid "Private profile"
msgstr ""
+msgid "Private projects Minutes cost factor"
+msgstr ""
+
msgid "Private projects can be created in your personal namespace with:"
msgstr ""
@@ -16005,6 +16327,9 @@ msgstr ""
msgid "ProjectSettings|With GitLab Pages you can host your static websites on GitLab"
msgstr ""
+msgid "ProjectSettings|With Metrics Dashboard you can visualize this project performance metrics"
+msgstr ""
+
msgid "ProjectTemplates|.NET Core"
msgstr ""
@@ -16068,6 +16393,9 @@ msgstr ""
msgid "ProjectTemplates|Spring"
msgstr ""
+msgid "ProjectTemplates|Static Site Editor/Middleman"
+msgstr ""
+
msgid "ProjectTemplates|iOS (Swift)"
msgstr ""
@@ -16287,13 +16615,13 @@ msgstr ""
msgid "PrometheusService|Prometheus is being automatically managed on your clusters"
msgstr ""
-msgid "PrometheusService|These metrics will only be monitored after your first deployment to an environment"
+msgid "PrometheusService|Select the Active checkbox to override the Auto Configuration with custom settings. If unchecked, Auto Configuration settings are used."
msgstr ""
-msgid "PrometheusService|Time-series monitoring service"
+msgid "PrometheusService|These metrics will only be monitored after your first deployment to an environment"
msgstr ""
-msgid "PrometheusService|To enable manual configuration, uninstall Prometheus from your clusters"
+msgid "PrometheusService|Time-series monitoring service"
msgstr ""
msgid "PrometheusService|To enable the installation of Prometheus on your clusters, deactivate the manual configuration below"
@@ -16479,6 +16807,9 @@ msgstr ""
msgid "Protip:"
msgstr ""
+msgid "Protocol"
+msgstr ""
+
msgid "Provider"
msgstr ""
@@ -16503,6 +16834,9 @@ msgstr ""
msgid "Public pipelines"
msgstr ""
+msgid "Public projects Minutes cost factor"
+msgstr ""
+
msgid "Pull"
msgstr ""
@@ -16703,6 +17037,9 @@ msgid_plural "Refreshing in %d seconds to show the updated status..."
msgstr[0] ""
msgstr[1] ""
+msgid "Regenerate export"
+msgstr ""
+
msgid "Regenerate instance ID"
msgstr ""
@@ -17080,9 +17417,29 @@ msgstr ""
msgid "Reports|%{combinedString} and %{resolvedString}"
msgstr ""
+msgid "Reports|Accessibility scanning detected %d issue for the source branch only"
+msgid_plural "Reports|Accessibility scanning detected %d issues for the source branch only"
+msgstr[0] ""
+msgstr[1] ""
+
+msgid "Reports|Accessibility scanning detected no issues for the source branch only"
+msgstr ""
+
+msgid "Reports|Accessibility scanning failed loading results"
+msgstr ""
+
+msgid "Reports|Accessibility scanning results are being parsed"
+msgstr ""
+
msgid "Reports|Actions"
msgstr ""
+msgid "Reports|An error occured while loading report"
+msgstr ""
+
+msgid "Reports|An error occurred while loading %{name} results"
+msgstr ""
+
msgid "Reports|Class"
msgstr ""
@@ -17188,7 +17545,7 @@ msgstr ""
msgid "Requested %{time_ago}"
msgstr ""
-msgid "Requested design version does not exist"
+msgid "Requested design version does not exist."
msgstr ""
msgid "Requested states are invalid"
@@ -17372,6 +17729,9 @@ msgstr ""
msgid "Resync"
msgstr ""
+msgid "Resync all"
+msgstr ""
+
msgid "Resync all %{replicableType}"
msgstr ""
@@ -17440,6 +17800,9 @@ msgstr ""
msgid "Revoked personal access token %{personal_access_token_name}!"
msgstr ""
+msgid "Revoked project access token %{project_access_token_name}!"
+msgstr ""
+
msgid "RightSidebar|adding a"
msgstr ""
@@ -17488,6 +17851,9 @@ msgstr ""
msgid "Runner token"
msgstr ""
+msgid "Runner tokens"
+msgstr ""
+
msgid "Runner was not updated."
msgstr ""
@@ -17542,6 +17908,9 @@ msgstr ""
msgid "SAML SSO for %{group_name}"
msgstr ""
+msgid "SAML discovery tokens"
+msgstr ""
+
msgid "SAML for %{group_name}"
msgstr ""
@@ -17626,12 +17995,18 @@ msgstr ""
msgid "Scheduled"
msgstr ""
+msgid "Scheduled to merge this merge request (%{strategy})."
+msgstr ""
+
msgid "Scheduled to merge this merge request when the pipeline succeeds."
msgstr ""
msgid "Schedules"
msgstr ""
+msgid "Schedules to merge this merge request (%{strategy})."
+msgstr ""
+
msgid "Scheduling"
msgstr ""
@@ -17689,6 +18064,9 @@ msgstr ""
msgid "Search branches and tags"
msgstr ""
+msgid "Search by author"
+msgstr ""
+
msgid "Search files"
msgstr ""
@@ -17834,11 +18212,6 @@ msgid_plural "SearchResults|snippets"
msgstr[0] ""
msgstr[1] ""
-msgid "SearchResults|snippet result"
-msgid_plural "SearchResults|snippet results"
-msgstr[0] ""
-msgstr[1] ""
-
msgid "SearchResults|user"
msgid_plural "SearchResults|users"
msgstr[0] ""
@@ -17849,6 +18222,9 @@ msgid_plural "SearchResults|wiki results"
msgstr[0] ""
msgstr[1] ""
+msgid "Searching by both author and message is currently not supported."
+msgstr ""
+
msgid "Seat Link"
msgstr ""
@@ -17876,184 +18252,232 @@ msgstr ""
msgid "Security Dashboard"
msgstr ""
-msgid "Security Dashboard|Error fetching the vulnerability counts. Please check your network connection and try again."
+msgid "Security configuration help link"
+msgstr ""
+
+msgid "Security dashboard"
msgstr ""
-msgid "Security Dashboard|Error fetching the vulnerability list. Please check your network connection and try again."
+msgid "Security report is out of date. Please update your branch with the latest changes from the target branch (%{targetBranchName})"
msgstr ""
-msgid "Security Dashboard|Issue Created"
+msgid "Security report is out of date. Run %{newPipelineLinkStart}a new pipeline%{newPipelineLinkEnd} for the target branch (%{targetBranchName})"
msgstr ""
-msgid "Security Reports|Comment added to '%{vulnerabilityName}'"
+msgid "SecurityConfiguration|Configured"
msgstr ""
-msgid "Security Reports|Comment deleted on '%{vulnerabilityName}'"
+msgid "SecurityConfiguration|Feature"
msgstr ""
-msgid "Security Reports|Comment edited on '%{vulnerabilityName}'"
+msgid "SecurityConfiguration|Feature documentation for %{featureName}"
msgstr ""
-msgid "Security Reports|Create issue"
+msgid "SecurityConfiguration|Not yet configured"
msgstr ""
-msgid "Security Reports|Dismiss vulnerability"
+msgid "SecurityConfiguration|Secure features"
msgstr ""
-msgid "Security Reports|Dismissed '%{vulnerabilityName}'"
+msgid "SecurityConfiguration|Status"
msgstr ""
-msgid "Security Reports|Dismissed '%{vulnerabilityName}'. Turn off the hide dismissed toggle to view."
+msgid "SecurityReports|%{firstProject} and %{secondProject}"
msgstr ""
-msgid "Security Reports|Either you don't have permission to view this dashboard or the dashboard has not been setup. Please check your permission settings with your administrator or check your dashboard configurations to proceed."
+msgid "SecurityReports|%{firstProject}, %{secondProject}, and %{rest}"
msgstr ""
-msgid "Security Reports|Learn more about setting up your dashboard"
+msgid "SecurityReports|Add a project to your dashboard"
msgstr ""
-msgid "Security Reports|More info"
+msgid "SecurityReports|Add or remove projects from your dashboard"
msgstr ""
-msgid "Security Reports|Oops, something doesn't seem right."
+msgid "SecurityReports|Add projects"
msgstr ""
-msgid "Security Reports|Security reports can only be accessed by authorized users."
+msgid "SecurityReports|Comment added to '%{vulnerabilityName}'"
msgstr ""
-msgid "Security Reports|There was an error adding the comment."
+msgid "SecurityReports|Comment deleted on '%{vulnerabilityName}'"
msgstr ""
-msgid "Security Reports|There was an error creating the issue."
+msgid "SecurityReports|Comment edited on '%{vulnerabilityName}'"
msgstr ""
-msgid "Security Reports|There was an error creating the merge request."
+msgid "SecurityReports|Create issue"
msgstr ""
-msgid "Security Reports|There was an error deleting the comment."
+msgid "SecurityReports|Dismiss Selected"
msgstr ""
-msgid "Security Reports|There was an error dismissing the vulnerabilities."
+msgid "SecurityReports|Dismiss vulnerability"
msgstr ""
-msgid "Security Reports|There was an error dismissing the vulnerability."
+msgid "SecurityReports|Dismissed '%{vulnerabilityName}'"
msgstr ""
-msgid "Security Reports|There was an error reverting the dismissal."
+msgid "SecurityReports|Dismissed '%{vulnerabilityName}'. Turn off the hide dismissed toggle to view."
msgstr ""
-msgid "Security Reports|There was an error reverting this dismissal."
+msgid "SecurityReports|Each vulnerability now has a unique page that can be directly linked to, shared, referenced, and tracked as the single source of truth. Vulnerability occurrences also persist across scanner runs, which improves tracking and visibility and reduces duplicates between scans."
msgstr ""
-msgid "Security Reports|Undo dismiss"
+msgid "SecurityReports|Edit dashboard"
msgstr ""
-msgid "Security Reports|You do not have sufficient permissions to access this report"
+msgid "SecurityReports|Either you don't have permission to view this dashboard or the dashboard has not been setup. Please check your permission settings with your administrator or check your dashboard configurations to proceed."
msgstr ""
-msgid "Security Reports|You must sign in as an authorized user to see this report"
+msgid "SecurityReports|Error fetching the vulnerability counts. Please check your network connection and try again."
msgstr ""
-msgid "Security configuration help link"
+msgid "SecurityReports|Error fetching the vulnerability list. Please check your network connection and try again."
msgstr ""
-msgid "Security dashboard"
+msgid "SecurityReports|False positive"
msgstr ""
-msgid "Security report is out of date. Please update your branch with the latest changes from the target branch (%{targetBranchName})"
+msgid "SecurityReports|Hide dismissed"
msgstr ""
-msgid "Security report is out of date. Run %{newPipelineLinkStart}a new pipeline%{newPipelineLinkEnd} for the target branch (%{targetBranchName})"
+msgid "SecurityReports|Introducing standalone vulnerabilities"
msgstr ""
-msgid "SecurityConfiguration|Configured"
+msgid "SecurityReports|Issue Created"
msgstr ""
-msgid "SecurityConfiguration|Feature"
+msgid "SecurityReports|Learn More"
msgstr ""
-msgid "SecurityConfiguration|Feature documentation for %{featureName}"
+msgid "SecurityReports|Learn more about setting up your dashboard"
msgstr ""
-msgid "SecurityConfiguration|Not yet configured"
+msgid "SecurityReports|Load more vulnerabilities"
msgstr ""
-msgid "SecurityConfiguration|Secure features"
+msgid "SecurityReports|Monitor vulnerabilities in your code"
msgstr ""
-msgid "SecurityConfiguration|Status"
+msgid "SecurityReports|More info"
msgstr ""
-msgid "SecurityDashboard|%{firstProject} and %{secondProject}"
+msgid "SecurityReports|More information"
msgstr ""
-msgid "SecurityDashboard|%{firstProject}, %{secondProject}, and %{rest}"
+msgid "SecurityReports|No vulnerabilities found for dashboard"
msgstr ""
-msgid "SecurityDashboard|Add a project to your dashboard"
+msgid "SecurityReports|No vulnerabilities found for this group"
msgstr ""
-msgid "SecurityDashboard|Add or remove projects from your dashboard"
+msgid "SecurityReports|No vulnerabilities found for this pipeline"
msgstr ""
-msgid "SecurityDashboard|Add projects"
+msgid "SecurityReports|No vulnerabilities found for this project"
msgstr ""
-msgid "SecurityDashboard|Edit dashboard"
+msgid "SecurityReports|Oops, something doesn't seem right."
msgstr ""
-msgid "SecurityDashboard|Hide dismissed"
+msgid "SecurityReports|Pipeline %{pipelineLink} triggered %{timeago} by %{user}"
msgstr ""
-msgid "SecurityDashboard|Monitor vulnerabilities in your code"
+msgid "SecurityReports|Project"
msgstr ""
-msgid "SecurityDashboard|More information"
+msgid "SecurityReports|Projects added"
msgstr ""
-msgid "SecurityDashboard|No vulnerabilities found for dashboard"
+msgid "SecurityReports|Remove project from dashboard"
msgstr ""
-msgid "SecurityDashboard|Pipeline %{pipelineLink} triggered %{timeago} by %{user}"
+msgid "SecurityReports|Report type"
msgstr ""
-msgid "SecurityDashboard|Project"
+msgid "SecurityReports|Return to dashboard"
msgstr ""
-msgid "SecurityDashboard|Projects added"
+msgid "SecurityReports|Security Dashboard"
msgstr ""
-msgid "SecurityDashboard|Remove project from dashboard"
+msgid "SecurityReports|Security reports can only be accessed by authorized users."
msgstr ""
-msgid "SecurityDashboard|Report type"
+msgid "SecurityReports|Select a project to add by using the project search field above."
msgstr ""
-msgid "SecurityDashboard|Return to dashboard"
+msgid "SecurityReports|Select a reason"
msgstr ""
-msgid "SecurityDashboard|Security Dashboard"
+msgid "SecurityReports|Severity"
msgstr ""
-msgid "SecurityDashboard|Select a project to add by using the project search field above."
+msgid "SecurityReports|Status"
msgstr ""
-msgid "SecurityDashboard|Severity"
+msgid "SecurityReports|The security dashboard displays the latest security findings for projects you wish to monitor. Select \"Edit dashboard\" to add and remove projects."
msgstr ""
-msgid "SecurityDashboard|Status"
+msgid "SecurityReports|The security dashboard displays the latest security report. Use it to find and fix vulnerabilities."
msgstr ""
-msgid "SecurityDashboard|The security dashboard displays the latest security findings for projects you wish to monitor. Select \"Edit dashboard\" to add and remove projects."
+msgid "SecurityReports|There was an error adding the comment."
msgstr ""
-msgid "SecurityDashboard|The security dashboard displays the latest security report. Use it to find and fix vulnerabilities."
+msgid "SecurityReports|There was an error creating the issue."
msgstr ""
-msgid "SecurityDashboard|There was an error while generating the report."
+msgid "SecurityReports|There was an error creating the merge request."
msgstr ""
-msgid "SecurityDashboard|Unable to add %{invalidProjects}"
+msgid "SecurityReports|There was an error deleting the comment."
+msgstr ""
+
+msgid "SecurityReports|There was an error dismissing the vulnerabilities."
+msgstr ""
+
+msgid "SecurityReports|There was an error dismissing the vulnerability."
+msgstr ""
+
+msgid "SecurityReports|There was an error reverting the dismissal."
+msgstr ""
+
+msgid "SecurityReports|There was an error reverting this dismissal."
+msgstr ""
+
+msgid "SecurityReports|There was an error while generating the report."
+msgstr ""
+
+msgid "SecurityReports|Unable to add %{invalidProjects}"
+msgstr ""
+
+msgid "SecurityReports|Undo dismiss"
+msgstr ""
+
+msgid "SecurityReports|While it's rare to have no vulnerabilities for your group, it can happen. In any event, we ask that you double check your settings to make sure you've set up your dashboard correctly."
+msgstr ""
+
+msgid "SecurityReports|While it's rare to have no vulnerabilities for your pipeline, it can happen. In any event, we ask that you double check your settings to make sure all security scanning jobs have passed successfully."
+msgstr ""
+
+msgid "SecurityReports|While it's rare to have no vulnerabilities for your project, it can happen. In any event, we ask that you double check your settings to make sure you've set up your dashboard correctly."
+msgstr ""
+
+msgid "SecurityReports|While it's rare to have no vulnerabilities, it can happen. In any event, we ask that you please double check your settings to make sure you've set up your dashboard correctly."
+msgstr ""
+
+msgid "SecurityReports|Won't fix / Accept risk"
+msgstr ""
+
+msgid "SecurityReports|You do not have sufficient permissions to access this report"
+msgstr ""
+
+msgid "SecurityReports|You must sign in as an authorized user to see this report"
+msgstr ""
+
+msgid "SecurityReports|[No reason]"
msgstr ""
msgid "See GitLab's %{password_policy_guidelines}"
@@ -18065,6 +18489,9 @@ msgstr ""
msgid "See the affected projects in the GitLab admin panel"
msgstr ""
+msgid "See what's new at GitLab"
+msgstr ""
+
msgid "Select"
msgstr ""
@@ -18470,9 +18897,6 @@ msgstr ""
msgid "Set up Jira Integration"
msgstr ""
-msgid "Set up Jira Integration illustration"
-msgstr ""
-
msgid "Set up a %{type} Runner automatically"
msgstr ""
@@ -18557,6 +18981,9 @@ msgstr ""
msgid "Severity: %{severity}"
msgstr ""
+msgid "Shards (%{shards})"
+msgstr ""
+
msgid "Shards to synchronize"
msgstr ""
@@ -18871,6 +19298,9 @@ msgstr ""
msgid "Snowplow"
msgstr ""
+msgid "Some child epics may be hidden due to applied filters"
+msgstr ""
+
msgid "Some email servers do not support overriding the email sender name. Enable this option to include the name of the author of the issue, merge request or comment in the email body instead."
msgstr ""
@@ -19012,6 +19442,9 @@ msgstr ""
msgid "Something went wrong while updating your list settings"
msgstr ""
+msgid "Something went wrong with your automatic subscription renewal"
+msgstr ""
+
msgid "Something went wrong, unable to add %{project} to dashboard"
msgstr ""
@@ -19297,6 +19730,9 @@ msgstr ""
msgid "Stage removed"
msgstr ""
+msgid "Standard"
+msgstr ""
+
msgid "Star a label to make it a priority label. Order the prioritized labels to change their relative priority, by dragging."
msgstr ""
@@ -19420,6 +19856,9 @@ msgstr ""
msgid "Static Application Security Testing (SAST)"
msgstr ""
+msgid "StaticSiteEditor|An error occurred while submitting your changes."
+msgstr ""
+
msgid "StaticSiteEditor|Branch could not be created."
msgstr ""
@@ -19429,18 +19868,30 @@ msgstr ""
msgid "StaticSiteEditor|Could not create merge request."
msgstr ""
+msgid "StaticSiteEditor|Incompatible file content"
+msgstr ""
+
msgid "StaticSiteEditor|Return to site"
msgstr ""
+msgid "StaticSiteEditor|Static site editor"
+msgstr ""
+
msgid "StaticSiteEditor|Success!"
msgstr ""
msgid "StaticSiteEditor|Summary of changes"
msgstr ""
+msgid "StaticSiteEditor|The Static Site Editor is currently configured to only edit Markdown content on pages generated from Middleman. Visit the documentation to learn more about configuring your site to use the Static Site Editor."
+msgstr ""
+
msgid "StaticSiteEditor|Update %{sourcePath} file"
msgstr ""
+msgid "StaticSiteEditor|View documentation"
+msgstr ""
+
msgid "StaticSiteEditor|View merge request"
msgstr ""
@@ -19498,6 +19949,12 @@ msgstr ""
msgid "StatusPage|Status page"
msgstr ""
+msgid "StatusPage|Status page URL"
+msgstr ""
+
+msgid "StatusPage|Status page frontend documentation"
+msgstr ""
+
msgid "StatusPage|To publish incidents to an external status page, GitLab will store a JSON file in your Amazon S3 account in a location accessible to your external status page service. Make sure to also set up %{docsLink}"
msgstr ""
@@ -19846,6 +20303,12 @@ msgstr ""
msgid "Synced"
msgstr ""
+msgid "Synchronization disabled"
+msgstr ""
+
+msgid "Synchronization of container repositories is disabled."
+msgstr ""
+
msgid "System"
msgstr ""
@@ -19999,6 +20462,9 @@ msgstr ""
msgid "Telephone number"
msgstr ""
+msgid "Telephone number (Optional)"
+msgstr ""
+
msgid "Template"
msgstr ""
@@ -20305,6 +20771,9 @@ msgstr ""
msgid "The license was successfully uploaded and is now active. You can see the details below."
msgstr ""
+msgid "The license was successfully uploaded and will be active from %{starts_at}. You can see the details below."
+msgstr ""
+
msgid "The maximum file size allowed is %{size}."
msgstr ""
@@ -20626,6 +21095,9 @@ msgstr ""
msgid "There was an error getting the epic participants."
msgstr ""
+msgid "There was an error importing the Jira project."
+msgstr ""
+
msgid "There was an error loading users activity calendar."
msgstr ""
@@ -20641,7 +21113,7 @@ msgstr ""
msgid "There was an error resetting user pipeline minutes."
msgstr ""
-msgid "There was an error saving this Geo Node"
+msgid "There was an error saving this Geo Node."
msgstr ""
msgid "There was an error saving your changes."
@@ -20704,6 +21176,9 @@ msgstr ""
msgid "They can be managed using the %{link}."
msgstr ""
+msgid "Third Party Advisory Link"
+msgstr ""
+
msgid "Third party offers"
msgstr ""
@@ -20740,6 +21215,9 @@ msgstr ""
msgid "This application will be able to:"
msgstr ""
+msgid "This attachment has been truncated to avoid exceeding the maximum allowed attachment size of 15MB. %{written_count} of %{issues_count} issues have been included. Consider re-exporting with a narrower selection of issues."
+msgstr ""
+
msgid "This block is self-referential"
msgstr ""
@@ -20881,6 +21359,12 @@ msgstr ""
msgid "This issue is confidential"
msgstr ""
+msgid "This issue is currently blocked by the following issues: %{issues}."
+msgstr ""
+
+msgid "This issue is in a child epic of the filtered epic"
+msgstr ""
+
msgid "This issue is locked."
msgstr ""
@@ -21079,7 +21563,10 @@ msgstr ""
msgid "This user will be the author of all events in the activity feed that are the result of an update, like new branches being created or new commits being pushed to existing branches. Upon creation or when reassigning you can only assign yourself to be the mirror user."
msgstr ""
-msgid "This variable can not be masked"
+msgid "This variable can not be masked."
+msgstr ""
+
+msgid "This variable does not match the expected pattern."
msgstr ""
msgid "This will help us personalize your onboarding experience."
@@ -21109,10 +21596,10 @@ msgstr ""
msgid "ThreatMonitoring|Application firewall not detected"
msgstr ""
-msgid "ThreatMonitoring|Container Network Policy"
+msgid "ThreatMonitoring|Container Network Policies are not installed or have been disabled. To view this data, ensure your Network Policies are installed and enabled for your cluster."
msgstr ""
-msgid "ThreatMonitoring|Container NetworkPolicies are not installed or has been disabled. To view this data, ensure you NetworkPolicies are installed and enabled for your cluster."
+msgid "ThreatMonitoring|Container Network Policy"
msgstr ""
msgid "ThreatMonitoring|Container NetworkPolicies not detected"
@@ -21145,7 +21632,7 @@ msgstr ""
msgid "ThreatMonitoring|Something went wrong, unable to fetch statistics"
msgstr ""
-msgid "ThreatMonitoring|The firewall is not installed or has been disabled. To view this data, ensure you firewall is installed and enabled for your cluster."
+msgid "ThreatMonitoring|The firewall is not installed or has been disabled. To view this data, ensure the web application firewall is installed and enabled for your cluster."
msgstr ""
msgid "ThreatMonitoring|The graph below is an overview of traffic coming to your application as tracked by the Web Application Firewall (WAF). View the docs for instructions on how to access the WAF logs to see what type of malicious traffic is trying to access your app. The docs link is also accessible by clicking the \"?\" icon next to the title below."
@@ -21214,6 +21701,9 @@ msgstr ""
msgid "Time in seconds GitLab will wait for a response from the external service. When the service does not respond in time, access will be denied."
msgstr ""
+msgid "Time of import: %{importTime}"
+msgstr ""
+
msgid "Time remaining"
msgstr ""
@@ -21398,7 +21888,7 @@ msgstr ""
msgid "Title:"
msgstr ""
-msgid "Titles and Filenames"
+msgid "Titles and Descriptions"
msgstr ""
msgid "To"
@@ -21605,7 +22095,7 @@ msgstr ""
msgid "Too many projects enabled. You will need to manage them via the console or the API."
msgstr ""
-msgid "Topics"
+msgid "Topics (optional)"
msgstr ""
msgid "Total"
@@ -21767,6 +22257,9 @@ msgstr ""
msgid "Try all GitLab has to offer for 30 days."
msgstr ""
+msgid "Try changing or removing filters."
+msgstr ""
+
msgid "Try to fork again"
msgstr ""
@@ -21860,6 +22353,9 @@ msgstr ""
msgid "Unable to connect to server: %{error}"
msgstr ""
+msgid "Unable to connect to the Jira instance. Please check your Jira integration configuration."
+msgstr ""
+
msgid "Unable to convert Kubernetes logs encoding to UTF-8"
msgstr ""
@@ -22803,6 +23299,9 @@ msgstr ""
msgid "View issue"
msgstr ""
+msgid "View issues"
+msgstr ""
+
msgid "View it on GitLab"
msgstr ""
@@ -22980,6 +23479,15 @@ msgstr ""
msgid "VulnerabilityManagement|Resolved %{timeago} by %{user}"
msgstr ""
+msgid "VulnerabilityManagement|Something went wrong while trying to delete the comment. Please try again later."
+msgstr ""
+
+msgid "VulnerabilityManagement|Something went wrong while trying to retrieve the vulnerability history. Please try again later."
+msgstr ""
+
+msgid "VulnerabilityManagement|Something went wrong while trying to save the comment. Please try again later."
+msgstr ""
+
msgid "VulnerabilityManagement|Something went wrong, could not create an issue."
msgstr ""
@@ -23010,6 +23518,9 @@ msgstr ""
msgid "VulnerabilityStatusTypes|Resolved"
msgstr ""
+msgid "Vulnerability|%{scannerName} (version %{scannerVersion})"
+msgstr ""
+
msgid "Vulnerability|Class"
msgstr ""
@@ -23043,7 +23554,10 @@ msgstr ""
msgid "Vulnerability|Project"
msgstr ""
-msgid "Vulnerability|Report Type"
+msgid "Vulnerability|Scanner Provider"
+msgstr ""
+
+msgid "Vulnerability|Scanner Type"
msgstr ""
msgid "Vulnerability|Severity"
@@ -23097,9 +23611,15 @@ msgstr ""
msgid "We sent you an email with reset password instructions"
msgstr ""
+msgid "We tried to automatically renew your %{strong}%{plan_name}%{strong_close} subscription for %{strong}%{namespace_name}%{strong_close} on %{expires_on} but something went wrong so your subscription was downgraded to the free plan. Don't worry, your data is safe. We suggest you check your payment method and get in touch with our support team (%{support_link}). They'll gladly help with your subscription renewal."
+msgstr ""
+
msgid "We want to be sure it is you, please confirm you are not a robot."
msgstr ""
+msgid "We will automatically renew your %{strong}%{plan_name}%{strong_close} subscription for %{strong}%{namespace_name}%{strong_close} on %{strong}%{expires_on}%{strong_close}. There's nothing that you need to do, we'll let you know when the renewal is complete. Need more seats, a higher plan or just want to review your payment method?"
+msgstr ""
+
msgid "We've found no vulnerabilities"
msgstr ""
@@ -23204,15 +23724,6 @@ msgstr ""
msgid "When:"
msgstr ""
-msgid "While it's rare to have no vulnerabilities for your group, it can happen. In any event, we ask that you double check your settings to make sure you've set up your dashboard correctly."
-msgstr ""
-
-msgid "While it's rare to have no vulnerabilities for your pipeline, it can happen. In any event, we ask that you double check your settings to make sure all security scanning jobs have passed successfully."
-msgstr ""
-
-msgid "While it's rare to have no vulnerabilities for your project, it can happen. In any event, we ask that you double check your settings to make sure you've set up your dashboard correctly."
-msgstr ""
-
msgid "While it's rare to have no vulnerabilities, it can happen. In any event, we ask that you please double check your settings to make sure you've set up your dashboard correctly."
msgstr ""
@@ -23243,9 +23754,6 @@ msgstr ""
msgid "Wiki"
msgstr ""
-msgid "Wiki pages"
-msgstr ""
-
msgid "Wiki was successfully updated."
msgstr ""
@@ -23429,6 +23937,9 @@ msgstr ""
msgid "Yes, add it"
msgstr ""
+msgid "Yes, close issue"
+msgstr ""
+
msgid "Yes, let me map Google Code users to full names or GitLab users."
msgstr ""
@@ -23624,6 +24135,9 @@ msgstr ""
msgid "You could not create a new trigger."
msgstr ""
+msgid "You didn't renew your %{strong}%{plan_name}%{strong_close} subscription for %{strong}%{namespace_name}%{strong_close} so it was downgraded to the free plan."
+msgstr ""
+
msgid "You didn't renew your %{strong}%{plan_name}%{strong_close} subscription so it was downgraded to the GitLab Core Plan."
msgstr ""
@@ -23690,6 +24204,9 @@ msgstr ""
msgid "You have declined the invitation to join %{label}."
msgstr ""
+msgid "You have imported from this project %{numberOfPreviousImportsForProject} times before. Each new import will create duplicate issues."
+msgstr ""
+
msgid "You have no permissions"
msgstr ""
@@ -23771,6 +24288,9 @@ msgstr ""
msgid "You need to upload a Google Takeout archive."
msgstr ""
+msgid "You reached %{usage_in_percent} of %{namespace_name}'s capacity (%{used_storage} of %{storage_limit})"
+msgstr ""
+
msgid "You tried to fork %{link_to_the_project} but it failed for the following reason:"
msgstr ""
@@ -23855,9 +24375,21 @@ msgstr ""
msgid "YouTube"
msgstr ""
+msgid "Your %{strong}%{plan_name}%{strong_close} subscription for %{strong}%{namespace_name}%{strong_close} will expire on %{strong}%{expires_on}%{strong_close}. After that, you will not to be able to create issues or merge requests as well as many other features."
+msgstr ""
+
msgid "Your %{strong}%{plan_name}%{strong_close} subscription will expire on %{strong}%{expires_on}%{strong_close}. After that, you will not to be able to create issues or merge requests as well as many other features."
msgstr ""
+msgid "Your CSV export has started. It will be emailed to %{email} when complete."
+msgstr ""
+
+msgid "Your CSV export of %{issues_count} from project %{project_link} has been added to this email as an attachment."
+msgstr ""
+
+msgid "Your CSV export of %{written_count} from project %{project_name} (%{project_url}) has been added to this email as an attachment."
+msgstr ""
+
msgid "Your Commit Email will be used for web based operations, such as edits and merges."
msgstr ""
@@ -23954,6 +24486,9 @@ msgstr ""
msgid "Your comment could not be updated! Please check your network connection and try again."
msgstr ""
+msgid "Your comment will be discarded."
+msgstr ""
+
msgid "Your custom stage '%{title}' was created"
msgstr ""
@@ -23993,6 +24528,9 @@ msgstr ""
msgid "Your new personal access token has been created."
msgstr ""
+msgid "Your new project access token has been created."
+msgstr ""
+
msgid "Your password isn't required to view this page. If a password or any other personal details are requested, please contact your administrator to report abuse."
msgstr ""
@@ -24011,12 +24549,18 @@ msgstr ""
msgid "Your request for access has been queued for review."
msgstr ""
+msgid "Your search didn't match any commits."
+msgstr ""
+
msgid "Your subscription expired!"
msgstr ""
msgid "Your subscription has been downgraded"
msgstr ""
+msgid "Your subscription will automatically renew in %{remaining_days}"
+msgstr ""
+
msgid "Your subscription will expire in %{remaining_days}"
msgstr ""
@@ -24055,7 +24599,7 @@ msgstr ""
msgid "allowed to fail"
msgstr ""
-msgid "already being used for another group or project milestone."
+msgid "already being used for another group or project %{timebox_name}."
msgstr ""
msgid "already has a \"created\" issue link"
@@ -24133,6 +24677,9 @@ msgstr ""
msgid "ciReport|%{linkStartTag}Learn more about SAST %{linkEndTag}"
msgstr ""
+msgid "ciReport|%{linkStartTag}Learn more about Secret Scanning %{linkEndTag}"
+msgstr ""
+
msgid "ciReport|%{linkStartTag}Learn more about codequality reports %{linkEndTag}"
msgstr ""
@@ -24296,6 +24843,12 @@ msgstr ""
msgid "ciReport|SAST"
msgstr ""
+msgid "ciReport|Secret scanning"
+msgstr ""
+
+msgid "ciReport|Secret scanning detects secrets and credentials vulnerabilities in your source code."
+msgstr ""
+
msgid "ciReport|Security scanning"
msgstr ""
@@ -24493,6 +25046,9 @@ msgstr ""
msgid "group"
msgstr ""
+msgid "groups"
+msgstr ""
+
msgid "has already been linked to another vulnerability"
msgstr ""
@@ -24656,9 +25212,6 @@ msgstr[1] ""
msgid "merged %{time_ago}"
msgstr ""
-msgid "milestone should belong either to a project or a group."
-msgstr ""
-
msgid "missing"
msgstr ""
@@ -24995,6 +25548,9 @@ msgstr ""
msgid "no contributions"
msgstr ""
+msgid "no expiration"
+msgstr ""
+
msgid "no one can merge"
msgstr ""
@@ -25047,6 +25603,9 @@ msgstr ""
msgid "pending removal"
msgstr ""
+msgid "per day"
+msgstr ""
+
msgid "pipeline"
msgstr ""
@@ -25075,6 +25634,12 @@ msgstr[1] ""
msgid "project avatar"
msgstr ""
+msgid "projects"
+msgstr ""
+
+msgid "push to your repository, create pipelines, create issues or add comments. To reduce storage capacity, delete unused repositories, artifacts, wikis, issues, and pipelines."
+msgstr ""
+
msgid "quick actions"
msgstr ""
@@ -25221,11 +25786,6 @@ msgstr ""
msgid "this document"
msgstr ""
-msgid "thread resolved"
-msgid_plural "threads resolved"
-msgstr[0] ""
-msgstr[1] ""
-
msgid "to help your contributors communicate effectively!"
msgstr ""
@@ -25280,6 +25840,9 @@ msgstr ""
msgid "view the blob"
msgstr ""
+msgid "vulnerability|Add a comment"
+msgstr ""
+
msgid "vulnerability|Add a comment or reason for dismissal"
msgstr ""
diff --git a/locale/uz_UZ/gitlab.po b/locale/uz_UZ/gitlab.po
index b23b5af97f4..3694bf80831 100644
--- a/locale/uz_UZ/gitlab.po
+++ b/locale/uz_UZ/gitlab.po
@@ -12,7 +12,7 @@ msgstr ""
"X-Crowdin-Project: gitlab-ee\n"
"X-Crowdin-Language: uz\n"
"X-Crowdin-File: /master/locale/gitlab.pot\n"
-"PO-Revision-Date: 2020-04-15 00:28\n"
+"PO-Revision-Date: 2020-05-05 21:33\n"
msgid " %{start} to %{end}"
msgstr ""
@@ -212,6 +212,11 @@ msgid_plural "%d tags"
msgstr[0] ""
msgstr[1] ""
+msgid "%d unresolved thread"
+msgid_plural "%d unresolved threads"
+msgstr[0] ""
+msgstr[1] ""
+
msgid "%d vulnerability dismissed"
msgid_plural "%d vulnerabilities dismissed"
msgstr[0] ""
@@ -371,6 +376,9 @@ msgstr ""
msgid "%{mrText}, this issue will be closed automatically."
msgstr ""
+msgid "%{namespace_name} is now read-only. You cannot: %{base_message}"
+msgstr ""
+
msgid "%{name} contained %{resultsString}"
msgstr ""
@@ -383,6 +391,11 @@ msgstr ""
msgid "%{name}'s avatar"
msgstr ""
+msgid "%{no_of_days} day"
+msgid_plural "%{no_of_days} days"
+msgstr[0] ""
+msgstr[1] ""
+
msgid "%{number_commits_behind} commits behind %{default_branch}, %{number_commits_ahead} commits ahead"
msgstr ""
@@ -506,6 +519,9 @@ msgstr[1] ""
msgid "%{text} is available"
msgstr ""
+msgid "%{timebox_name} should belong either to a project or a group."
+msgstr ""
+
msgid "%{title} %{operator} %{threshold}"
msgstr ""
@@ -781,12 +797,18 @@ msgstr ""
msgid "<code>\"johnsmith@example.com\": \"johnsmith@example.com\"</code> will add \"By <a href=\"#\">johnsmith@example.com</a>\" to all issues and comments originally created by johnsmith@example.com. By default, the email address or username is masked to ensure the user's privacy. Use this option if you want to show the full email address."
msgstr ""
+msgid "<namespace / project>"
+msgstr ""
+
msgid "<no name set>"
msgstr ""
msgid "<no scopes selected>"
msgstr ""
+msgid "<project name>"
+msgstr ""
+
msgid "<strong>%{group_name}</strong> group members"
msgstr ""
@@ -820,6 +842,9 @@ msgstr ""
msgid "A basic page and serverless function that uses AWS Lambda, AWS API Gateway, and GitLab Pages"
msgstr ""
+msgid "A complete DevOps platform"
+msgstr ""
+
msgid "A default branch cannot be chosen for an empty project."
msgstr ""
@@ -1036,6 +1061,12 @@ msgstr ""
msgid "AccessTokens|reset it"
msgstr ""
+msgid "AccessibilityReport|Accessibility report artifact not found"
+msgstr ""
+
+msgid "AccessibilityReport|Failed to retrieve accessibility report"
+msgstr ""
+
msgid "AccessibilityReport|Learn More"
msgstr ""
@@ -1045,6 +1076,9 @@ msgstr ""
msgid "AccessibilityReport|New"
msgstr ""
+msgid "AccessibilityReport|The accessibility scanning found an error of the following type: %{code}"
+msgstr ""
+
msgid "Account"
msgstr ""
@@ -1287,6 +1321,9 @@ msgstr ""
msgid "Added at"
msgstr ""
+msgid "Added for this merge request"
+msgstr ""
+
msgid "Added in this version"
msgstr ""
@@ -1658,6 +1695,72 @@ msgid_plural "Alerts"
msgstr[0] ""
msgstr[1] ""
+msgid "Alert Details"
+msgstr ""
+
+msgid "AlertManagement|Acknowledged"
+msgstr ""
+
+msgid "AlertManagement|Alert"
+msgstr ""
+
+msgid "AlertManagement|Authorize external service"
+msgstr ""
+
+msgid "AlertManagement|Display alerts from all your monitoring tools directly within GitLab. Streamline the investigation of your alerts and the escalation of alerts to incidents."
+msgstr ""
+
+msgid "AlertManagement|End time"
+msgstr ""
+
+msgid "AlertManagement|End time:"
+msgstr ""
+
+msgid "AlertManagement|Events"
+msgstr ""
+
+msgid "AlertManagement|Events:"
+msgstr ""
+
+msgid "AlertManagement|Full Alert Details"
+msgstr ""
+
+msgid "AlertManagement|More information"
+msgstr ""
+
+msgid "AlertManagement|No alerts available to display. If you think you're seeing this message in error, refresh the page."
+msgstr ""
+
+msgid "AlertManagement|No alerts to display."
+msgstr ""
+
+msgid "AlertManagement|Overview"
+msgstr ""
+
+msgid "AlertManagement|Resolved"
+msgstr ""
+
+msgid "AlertManagement|Severity"
+msgstr ""
+
+msgid "AlertManagement|Start time"
+msgstr ""
+
+msgid "AlertManagement|Start time:"
+msgstr ""
+
+msgid "AlertManagement|Status"
+msgstr ""
+
+msgid "AlertManagement|Surface alerts in GitLab"
+msgstr ""
+
+msgid "AlertManagement|There was an error displaying the alerts. Confirm your endpoint's configuration details to ensure alerts appear."
+msgstr ""
+
+msgid "AlertManagement|Triggered"
+msgstr ""
+
msgid "AlertService|%{linkStart}Learn more%{linkEnd} about configuring this endpoint to receive alerts."
msgstr ""
@@ -1727,6 +1830,9 @@ msgstr ""
msgid "All security scans are enabled because %{linkStart}Auto DevOps%{linkEnd} is enabled on this project"
msgstr ""
+msgid "All threads resolved"
+msgstr ""
+
msgid "All users"
msgstr ""
@@ -1745,6 +1851,9 @@ msgstr ""
msgid "Allow only the selected protocols to be used for Git access."
msgstr ""
+msgid "Allow owners to manage default branch protection per group"
+msgstr ""
+
msgid "Allow owners to manually add users outside of LDAP"
msgstr ""
@@ -1853,6 +1962,9 @@ msgstr ""
msgid "An error occurred fetching the dropdown data."
msgstr ""
+msgid "An error occurred fetching the project authors."
+msgstr ""
+
msgid "An error occurred previewing the blob"
msgstr ""
@@ -1934,6 +2046,9 @@ msgstr ""
msgid "An error occurred while fetching sidebar data"
msgstr ""
+msgid "An error occurred while fetching terraform reports."
+msgstr ""
+
msgid "An error occurred while fetching the Service Desk address."
msgstr ""
@@ -2171,6 +2286,9 @@ msgstr ""
msgid "Any"
msgstr ""
+msgid "Any Author"
+msgstr ""
+
msgid "Any Label"
msgstr ""
@@ -2386,10 +2504,10 @@ msgstr ""
msgid "Are you sure that you want to unarchive this project?"
msgstr ""
-msgid "Are you sure you want to cancel creating this comment?"
+msgid "Are you sure you want to cancel editing this comment?"
msgstr ""
-msgid "Are you sure you want to cancel editing this comment?"
+msgid "Are you sure you want to close this blocked issue?"
msgstr ""
msgid "Are you sure you want to delete %{name}?"
@@ -2419,6 +2537,9 @@ msgstr ""
msgid "Are you sure you want to deploy this environment?"
msgstr ""
+msgid "Are you sure you want to discard this comment?"
+msgstr ""
+
msgid "Are you sure you want to erase this build?"
msgstr ""
@@ -2595,6 +2716,9 @@ msgstr ""
msgid "At least one approval from a code owner is required to change files matching the respective CODEOWNER rules."
msgstr ""
+msgid "At least one logging option is required to be enabled"
+msgstr ""
+
msgid "At least one of group_id or project_id must be specified"
msgstr ""
@@ -2633,6 +2757,27 @@ msgstr ""
msgid "AuditEvents|Target"
msgstr ""
+msgid "AuditLogs|(removed)"
+msgstr ""
+
+msgid "AuditLogs|Action"
+msgstr ""
+
+msgid "AuditLogs|Author"
+msgstr ""
+
+msgid "AuditLogs|Date"
+msgstr ""
+
+msgid "AuditLogs|IP Address"
+msgstr ""
+
+msgid "AuditLogs|Object"
+msgstr ""
+
+msgid "AuditLogs|Target"
+msgstr ""
+
msgid "Aug"
msgstr ""
@@ -2915,9 +3060,6 @@ msgstr ""
msgid "BambooService|You must set up automatic revision labeling and a repository trigger in Bamboo."
msgstr ""
-msgid "Batch operations"
-msgstr ""
-
msgid "BatchComments|Delete all pending comments"
msgstr ""
@@ -3047,6 +3189,9 @@ msgstr ""
msgid "Boards"
msgstr ""
+msgid "Boards and Board Lists"
+msgstr ""
+
msgid "Boards|Collapse"
msgstr ""
@@ -3266,6 +3411,9 @@ msgstr ""
msgid "Buy GitLab Enterprise Edition"
msgstr ""
+msgid "Buy more Pipeline minutes"
+msgstr ""
+
msgid "By %{user_name}"
msgstr ""
@@ -3386,6 +3534,9 @@ msgstr ""
msgid "Can't scan the code?"
msgstr ""
+msgid "Can't update snippet: %{err}"
+msgstr ""
+
msgid "Canary"
msgstr ""
@@ -3554,6 +3705,9 @@ msgstr ""
msgid "Changing group path can have unintended side effects."
msgstr ""
+msgid "Charts"
+msgstr ""
+
msgid "Charts can't be displayed as the request for data has timed out. %{documentationLink}"
msgstr ""
@@ -3590,9 +3744,6 @@ msgstr ""
msgid "ChatMessage|and [%{count} more](%{pipeline_failed_jobs_url})"
msgstr ""
-msgid "ChatMessage|failed"
-msgstr ""
-
msgid "ChatMessage|has failed"
msgstr ""
@@ -3608,9 +3759,6 @@ msgstr ""
msgid "ChatMessage|in %{project_link}"
msgstr ""
-msgid "ChatMessage|passed"
-msgstr ""
-
msgid "Check again"
msgstr ""
@@ -4061,9 +4209,6 @@ msgstr ""
msgid "Click any <strong>project name</strong> in the project list below to navigate to the project milestone."
msgstr ""
-msgid "Click here"
-msgstr ""
-
msgid "Click the <strong>Download</strong> button and wait for downloading to complete."
msgstr ""
@@ -4451,6 +4596,12 @@ msgstr ""
msgid "ClusterIntegration|Fetching zones"
msgstr ""
+msgid "ClusterIntegration|Fluentd"
+msgstr ""
+
+msgid "ClusterIntegration|Fluentd is an open source data collector, which lets you unify the data collection and consumption for a better use and understanding of data. It requires at least one of the following logs to be successfully installed."
+msgstr ""
+
msgid "ClusterIntegration|GitLab Integration"
msgstr ""
@@ -4757,6 +4908,15 @@ msgstr ""
msgid "ClusterIntegration|Request to begin uninstalling failed"
msgstr ""
+msgid "ClusterIntegration|SIEM Hostname"
+msgstr ""
+
+msgid "ClusterIntegration|SIEM Port"
+msgstr ""
+
+msgid "ClusterIntegration|SIEM Protocol"
+msgstr ""
+
msgid "ClusterIntegration|Save changes"
msgstr ""
@@ -4853,6 +5013,12 @@ msgstr ""
msgid "ClusterIntegration|Select zone to choose machine type"
msgstr ""
+msgid "ClusterIntegration|Send Cilium Logs"
+msgstr ""
+
+msgid "ClusterIntegration|Send ModSecurity Logs"
+msgstr ""
+
msgid "ClusterIntegration|Service Token"
msgstr ""
@@ -5281,18 +5447,33 @@ msgstr ""
msgid "Compliance framework (optional)"
msgstr ""
+msgid "ComplianceFramework|GDPR"
+msgstr ""
+
msgid "ComplianceFramework|GDPR - General Data Protection Regulation"
msgstr ""
+msgid "ComplianceFramework|HIPAA"
+msgstr ""
+
msgid "ComplianceFramework|HIPAA - Health Insurance Portability and Accountability Act"
msgstr ""
+msgid "ComplianceFramework|PCI-DSS"
+msgstr ""
+
msgid "ComplianceFramework|PCI-DSS - Payment Card Industry-Data Security Standard"
msgstr ""
+msgid "ComplianceFramework|SOC 2"
+msgstr ""
+
msgid "ComplianceFramework|SOC 2 - Service Organization Control 2"
msgstr ""
+msgid "ComplianceFramework|SOX"
+msgstr ""
+
msgid "ComplianceFramework|SOX - Sarbanes-Oxley"
msgstr ""
@@ -5440,9 +5621,15 @@ msgstr ""
msgid "Container repositories sync capacity"
msgstr ""
+msgid "ContainerRegistry| Please visit the %{linkStart}administration settings%{linkEnd} to enable this feature."
+msgstr ""
+
msgid "ContainerRegistry|%{imageName} tags"
msgstr ""
+msgid "ContainerRegistry|%{title} was successfully scheduled for deletion"
+msgstr ""
+
msgid "ContainerRegistry|Automatically remove extra images that aren't designed to be kept."
msgstr ""
@@ -5455,6 +5642,9 @@ msgstr ""
msgid "ContainerRegistry|Container Registry"
msgstr ""
+msgid "ContainerRegistry|Container Registry tag expiration and retention policy is disabled"
+msgstr ""
+
msgid "ContainerRegistry|Copy build command"
msgstr ""
@@ -5464,18 +5654,12 @@ msgstr ""
msgid "ContainerRegistry|Copy push command"
msgstr ""
-msgid "ContainerRegistry|Currently, the Container Registry tag expiration feature is not available for projects created before GitLab version 12.8. For updates and more information, visit Issue %{linkStart}#196124%{linkEnd}"
-msgstr ""
-
msgid "ContainerRegistry|Docker connection error"
msgstr ""
msgid "ContainerRegistry|Docker tag expiration policy is %{toggleStatus}"
msgstr ""
-msgid "ContainerRegistry|Docker tags with names matching this regex pattern will expire:"
-msgstr ""
-
msgid "ContainerRegistry|Edit Settings"
msgstr ""
@@ -5497,9 +5681,6 @@ msgstr ""
msgid "ContainerRegistry|Image ID"
msgstr ""
-msgid "ContainerRegistry|Image deleted successfully"
-msgstr ""
-
msgid "ContainerRegistry|Keep and protect the images that matter most."
msgstr ""
@@ -5515,12 +5696,24 @@ msgstr ""
msgid "ContainerRegistry|Number of tags to retain:"
msgstr ""
+msgid "ContainerRegistry|Please contact your administrator."
+msgstr ""
+
msgid "ContainerRegistry|Push an image"
msgstr ""
msgid "ContainerRegistry|Quick Start"
msgstr ""
+msgid "ContainerRegistry|Regular expressions such as %{codeStart}.*-test%{codeEnd} or %{codeStart}dev-.*%{codeEnd} are supported"
+msgstr ""
+
+msgid "ContainerRegistry|Regular expressions such as %{codeStart}.*-test%{codeEnd} or %{codeStart}dev-.*%{codeEnd} are supported. To select all tags, use %{codeStart}.*%{codeEnd}"
+msgstr ""
+
+msgid "ContainerRegistry|Remember to run %{docLinkStart}garbage collection%{docLinkEnd} to remove the stale data from storage."
+msgstr ""
+
msgid "ContainerRegistry|Remove repository"
msgstr ""
@@ -5535,22 +5728,22 @@ msgstr[1] ""
msgid "ContainerRegistry|Retention policy has been Enabled"
msgstr ""
-msgid "ContainerRegistry|Something went wrong while deleting the image."
+msgid "ContainerRegistry|Something went wrong while fetching the expiration policy."
msgstr ""
-msgid "ContainerRegistry|Something went wrong while deleting the tag."
+msgid "ContainerRegistry|Something went wrong while fetching the repository list."
msgstr ""
-msgid "ContainerRegistry|Something went wrong while deleting the tags."
+msgid "ContainerRegistry|Something went wrong while fetching the tags list."
msgstr ""
-msgid "ContainerRegistry|Something went wrong while fetching the expiration policy."
+msgid "ContainerRegistry|Something went wrong while marking the tag for deletion."
msgstr ""
-msgid "ContainerRegistry|Something went wrong while fetching the packages list."
+msgid "ContainerRegistry|Something went wrong while marking the tags for deletion."
msgstr ""
-msgid "ContainerRegistry|Something went wrong while fetching the tags list."
+msgid "ContainerRegistry|Something went wrong while scheduling %{title} for deletion. Please try again."
msgstr ""
msgid "ContainerRegistry|Something went wrong while updating the expiration policy."
@@ -5559,16 +5752,25 @@ msgstr ""
msgid "ContainerRegistry|Tag"
msgstr ""
-msgid "ContainerRegistry|Tag deleted successfully"
-msgstr ""
-
msgid "ContainerRegistry|Tag expiration policy"
msgstr ""
msgid "ContainerRegistry|Tag expiration policy is designed to:"
msgstr ""
-msgid "ContainerRegistry|Tags deleted successfully"
+msgid "ContainerRegistry|Tag successfully marked for deletion."
+msgstr ""
+
+msgid "ContainerRegistry|Tags successfully marked for deletion."
+msgstr ""
+
+msgid "ContainerRegistry|Tags with names matching this regex pattern will %{italicStart}be preserved:%{italicEnd}"
+msgstr ""
+
+msgid "ContainerRegistry|Tags with names matching this regex pattern will %{italicStart}expire:%{italicEnd}"
+msgstr ""
+
+msgid "ContainerRegistry|The Container Registry tag expiration and retention policies for this project have not been enabled."
msgstr ""
msgid "ContainerRegistry|The last tag related to this image was recently removed. This empty image and any associated data will be automatically removed as part of the regular Garbage Collection process. If you have any questions, contact your administrator."
@@ -5589,16 +5791,16 @@ msgstr ""
msgid "ContainerRegistry|There are no container images stored for this project"
msgstr ""
-msgid "ContainerRegistry|This Registry contains deleted image tag data. Remember to run %{docLinkStart}garbage collection%{docLinkEnd} to remove the stale data from storage."
+msgid "ContainerRegistry|There was an error during the deletion of this image repository, please try again."
msgstr ""
msgid "ContainerRegistry|This image has no active tags"
msgstr ""
-msgid "ContainerRegistry|We are having trouble connecting to Docker, which could be due to an issue with your project name or path. %{docLinkStart}More Information%{docLinkEnd}"
+msgid "ContainerRegistry|This image repository is scheduled for deletion"
msgstr ""
-msgid "ContainerRegistry|Wildcards such as %{codeStart}.*-stable%{codeEnd} or %{codeStart}production/.*%{codeEnd} are supported. To select all tags, use %{codeStart}.*%{codeEnd}"
+msgid "ContainerRegistry|We are having trouble connecting to Docker, which could be due to an issue with your project name or path. %{docLinkStart}More Information%{docLinkEnd}"
msgstr ""
msgid "ContainerRegistry|With the Container Registry, every project can have its own space to store its Docker images. %{docLinkStart}More Information%{docLinkEnd}"
@@ -5853,7 +6055,7 @@ msgstr ""
msgid "Could not delete chat nickname %{chat_name}."
msgstr ""
-msgid "Could not find design"
+msgid "Could not find design."
msgstr ""
msgid "Could not remove the trigger."
@@ -5868,6 +6070,9 @@ msgstr ""
msgid "Could not revoke personal access token %{personal_access_token_name}."
msgstr ""
+msgid "Could not revoke project access token %{project_access_token_name}."
+msgstr ""
+
msgid "Could not save group ID"
msgstr ""
@@ -5925,6 +6130,9 @@ msgstr ""
msgid "Create a new branch"
msgstr ""
+msgid "Create a new deploy key for this project"
+msgstr ""
+
msgid "Create a new file as there are no files yet. Afterwards, you'll be able to commit your changes."
msgstr ""
@@ -6030,6 +6238,9 @@ msgstr ""
msgid "Create requirement"
msgstr ""
+msgid "Create snippet"
+msgstr ""
+
msgid "Create wildcard: %{searchTerm}"
msgstr ""
@@ -6051,6 +6262,9 @@ msgstr ""
msgid "Created"
msgstr ""
+msgid "Created %{timestamp}"
+msgstr ""
+
msgid "Created At"
msgstr ""
@@ -6407,6 +6621,9 @@ msgstr ""
msgid "CycleAnalytics|stage dropdown"
msgstr ""
+msgid "DAG"
+msgstr ""
+
msgid "DNS"
msgstr ""
@@ -6773,6 +6990,9 @@ msgstr ""
msgid "Deploy key was successfully updated."
msgstr ""
+msgid "Deploy keys allow read-only or read-write (if enabled) access to your repository. Deploy keys can be used for CI, staging or production servers. You can create a deploy key or add an existing one."
+msgstr ""
+
msgid "Deploy progress not found. To see pods, ensure your environment matches %{linkStart}deploy board criteria%{linkEnd}."
msgstr ""
@@ -6926,6 +7146,9 @@ msgstr ""
msgid "Deploying to"
msgstr ""
+msgid "Deployment Frequency"
+msgstr ""
+
msgid "Deployment|API"
msgstr ""
@@ -7444,9 +7667,6 @@ msgstr ""
msgid "Edit environment"
msgstr ""
-msgid "Edit epic description"
-msgstr ""
-
msgid "Edit file"
msgstr ""
@@ -8068,9 +8288,6 @@ msgstr ""
msgid "Epics"
msgstr ""
-msgid "Epics (Ultimate / Gold license only)"
-msgstr ""
-
msgid "Epics Roadmap"
msgstr ""
@@ -8080,6 +8297,9 @@ msgstr ""
msgid "Epics let you manage your portfolio of projects more efficiently and with less effort"
msgstr ""
+msgid "Epics, Issues, and Merge Requests"
+msgstr ""
+
msgid "Epics|Add an epic"
msgstr ""
@@ -8131,6 +8351,9 @@ msgstr ""
msgid "Epics|Something went wrong while fetching group epics."
msgstr ""
+msgid "Epics|Something went wrong while moving item."
+msgstr ""
+
msgid "Epics|Something went wrong while ordering item."
msgstr ""
@@ -8185,6 +8408,9 @@ msgstr ""
msgid "Error fetching network graph."
msgstr ""
+msgid "Error fetching payload data."
+msgstr ""
+
msgid "Error fetching projects"
msgstr ""
@@ -8194,9 +8420,6 @@ msgstr ""
msgid "Error fetching the dependency list. Please check your network connection and try again."
msgstr ""
-msgid "Error fetching usage ping data."
-msgstr ""
-
msgid "Error loading branch data. Please try again."
msgstr ""
@@ -8338,6 +8561,9 @@ msgstr ""
msgid "Errors"
msgstr ""
+msgid "Errors:"
+msgstr ""
+
msgid "Estimated"
msgstr ""
@@ -8542,12 +8768,18 @@ msgstr ""
msgid "Export as CSV"
msgstr ""
+msgid "Export group"
+msgstr ""
+
msgid "Export issues"
msgstr ""
msgid "Export project"
msgstr ""
+msgid "Export this group with all related data to a new GitLab instance. Once complete, you can import the data file from the \"New Group\" page."
+msgstr ""
+
msgid "Export this project with all its related data in order to move your project to a new GitLab instance. Once the export is finished, you can import the file from the \"New Project\" page."
msgstr ""
@@ -8617,9 +8849,6 @@ msgstr ""
msgid "Failed Jobs"
msgstr ""
-msgid "Failed create wiki"
-msgstr ""
-
msgid "Failed to add a Zoom meeting"
msgstr ""
@@ -8659,6 +8888,9 @@ msgstr ""
msgid "Failed to create resources"
msgstr ""
+msgid "Failed to create wiki"
+msgstr ""
+
msgid "Failed to delete board. Please try again."
msgstr ""
@@ -8668,7 +8900,7 @@ msgstr ""
msgid "Failed to enqueue the rebase operation, possibly due to a long-lived transaction. Try again later."
msgstr ""
-msgid "Failed to find import label for jira import."
+msgid "Failed to find import label for Jira import."
msgstr ""
msgid "Failed to get ref."
@@ -8692,6 +8924,12 @@ msgstr ""
msgid "Failed to load groups & users."
msgstr ""
+msgid "Failed to load labels. Please try again."
+msgstr ""
+
+msgid "Failed to load milestones. Please try again."
+msgstr ""
+
msgid "Failed to load related branches"
msgstr ""
@@ -9067,7 +9305,10 @@ msgstr ""
msgid "Filter by milestone name"
msgstr ""
-msgid "Filter by name..."
+msgid "Filter by name"
+msgstr ""
+
+msgid "Filter by status"
msgstr ""
msgid "Filter by two-factor authentication"
@@ -9079,12 +9320,18 @@ msgstr ""
msgid "Filter projects"
msgstr ""
+msgid "Filter results"
+msgstr ""
+
msgid "Filter results by group"
msgstr ""
msgid "Filter results by project"
msgstr ""
+msgid "Filter results..."
+msgstr ""
+
msgid "Filter your projects by name"
msgstr ""
@@ -9373,10 +9620,10 @@ msgstr ""
msgid "GeoNodes|Checksummed"
msgstr ""
-msgid "GeoNodes|Container repositories"
+msgid "GeoNodes|Consult Geo troubleshooting information"
msgstr ""
-msgid "GeoNodes|Data is out of date from %{timeago}"
+msgid "GeoNodes|Container repositories"
msgstr ""
msgid "GeoNodes|Data replication lag"
@@ -9418,6 +9665,9 @@ msgstr ""
msgid "GeoNodes|Last event ID seen from primary"
msgstr ""
+msgid "GeoNodes|Learn more about Geo node statuses"
+msgstr ""
+
msgid "GeoNodes|Loading nodes"
msgstr ""
@@ -9433,6 +9683,9 @@ msgstr ""
msgid "GeoNodes|Node was successfully removed."
msgstr ""
+msgid "GeoNodes|Node's status was updated %{timeAgo}."
+msgstr ""
+
msgid "GeoNodes|Not checksummed"
msgstr ""
@@ -9463,7 +9716,10 @@ msgstr ""
msgid "GeoNodes|Repository verification progress"
msgstr ""
-msgid "GeoNodes|Selective"
+msgid "GeoNodes|Selective (%{syncLabel})"
+msgstr ""
+
+msgid "GeoNodes|Selective synchronization"
msgstr ""
msgid "GeoNodes|Something went wrong while changing node status"
@@ -9490,6 +9746,9 @@ msgstr ""
msgid "GeoNodes|Unverified"
msgstr ""
+msgid "GeoNodes|Updated %{timeAgo}"
+msgstr ""
+
msgid "GeoNodes|Used slots"
msgstr ""
@@ -9529,16 +9788,16 @@ msgstr ""
msgid "Geo|All"
msgstr ""
-msgid "Geo|All projects"
+msgid "Geo|All %{replicable_type}"
msgstr ""
-msgid "Geo|All projects are being scheduled for re-sync"
+msgid "Geo|All projects"
msgstr ""
-msgid "Geo|All projects are being scheduled for re-verify"
+msgid "Geo|All projects are being scheduled for resync"
msgstr ""
-msgid "Geo|Batch operations"
+msgid "Geo|All projects are being scheduled for reverify"
msgstr ""
msgid "Geo|Could not remove tracking entry for an existing project."
@@ -9550,9 +9809,15 @@ msgstr ""
msgid "Geo|Failed"
msgstr ""
+msgid "Geo|Filter by status"
+msgstr ""
+
msgid "Geo|Geo Status"
msgstr ""
+msgid "Geo|In progress"
+msgstr ""
+
msgid "Geo|In sync"
msgstr ""
@@ -9577,9 +9842,6 @@ msgstr ""
msgid "Geo|Not synced yet"
msgstr ""
-msgid "Geo|Pending"
-msgstr ""
-
msgid "Geo|Pending synchronization"
msgstr ""
@@ -9616,7 +9878,7 @@ msgstr ""
msgid "Geo|Resync"
msgstr ""
-msgid "Geo|Resync all projects"
+msgid "Geo|Resync all"
msgstr ""
msgid "Geo|Retry count"
@@ -9625,7 +9887,7 @@ msgstr ""
msgid "Geo|Reverify"
msgstr ""
-msgid "Geo|Reverify all projects"
+msgid "Geo|Reverify all"
msgstr ""
msgid "Geo|Status"
@@ -9967,9 +10229,6 @@ msgstr ""
msgid "Go to file"
msgstr ""
-msgid "Go to file (MRs only)"
-msgstr ""
-
msgid "Go to file permalink (while viewing a file)"
msgstr ""
@@ -10414,6 +10673,9 @@ msgstr ""
msgid "GroupSettings|Disable group mentions"
msgstr ""
+msgid "GroupSettings|Export group"
+msgstr ""
+
msgid "GroupSettings|If the parent group's visibility is lower than the group current visibility, visibility levels for subgroups and projects will be changed to match the new parent group's visibility."
msgstr ""
@@ -10492,6 +10754,9 @@ msgstr ""
msgid "Groups (%{count})"
msgstr ""
+msgid "Groups (%{groups})"
+msgstr ""
+
msgid "Groups can also be nested by creating %{subgroup_docs_link_start}subgroups%{subgroup_docs_link_end}."
msgstr ""
@@ -10757,15 +11022,12 @@ msgstr ""
msgid "ID:"
msgstr ""
-msgid "IDE|Allow live previews of JavaScript projects in the Web IDE using CodeSandbox client side evaluation."
+msgid "IDE|Allow live previews of JavaScript projects in the Web IDE using CodeSandbox Live Preview."
msgstr ""
msgid "IDE|Back"
msgstr ""
-msgid "IDE|Client side evaluation"
-msgstr ""
-
msgid "IDE|Commit"
msgstr ""
@@ -10829,6 +11091,9 @@ msgstr ""
msgid "If any job surpasses this timeout threshold, it will be marked as failed. Human readable time input language is accepted like \"1 hour\". Values without specification represent seconds."
msgstr ""
+msgid "If blank, set allowable lifetime to %{instance_level_policy_in_words}, as defined by the instance admin. Once set, existing tokens for users in this group may be revoked."
+msgstr ""
+
msgid "If checked, group owners can manage LDAP group links and LDAP member overrides"
msgstr ""
@@ -10868,6 +11133,9 @@ msgstr ""
msgid "If you lose your recovery codes you can generate new ones, invalidating all previous codes."
msgstr ""
+msgid "If you reach 100%% storage capacity, you will not be able to: %{base_message}"
+msgstr ""
+
msgid "If your HTTP repository is not publicly accessible, add your credentials."
msgstr ""
@@ -10979,6 +11247,9 @@ msgstr ""
msgid "Import repository"
msgstr ""
+msgid "Import started by: %{importInitiator}"
+msgstr ""
+
msgid "Import tasks"
msgstr ""
@@ -11045,6 +11316,9 @@ msgstr ""
msgid "In order to tailor your experience with GitLab we<br>would like to know a bit more about you."
msgstr ""
+msgid "In progress"
+msgstr ""
+
msgid "In the next step, you'll be able to select the projects you want to import."
msgstr ""
@@ -11392,15 +11666,15 @@ msgstr ""
msgid "Issues"
msgstr ""
-msgid "Issues / Merge Requests"
-msgstr ""
-
msgid "Issues Analytics"
msgstr ""
msgid "Issues Rate Limits"
msgstr ""
+msgid "Issues and Merge Requests"
+msgstr ""
+
msgid "Issues can be bugs, tasks or ideas to be discussed. Also, issues are searchable and filterable."
msgstr ""
@@ -11422,10 +11696,10 @@ msgstr ""
msgid "IssuesAnalytics|Avg/Month:"
msgstr ""
-msgid "IssuesAnalytics|Issues created"
+msgid "IssuesAnalytics|Issues opened"
msgstr ""
-msgid "IssuesAnalytics|Issues created per month"
+msgid "IssuesAnalytics|Issues opened per month"
msgstr ""
msgid "IssuesAnalytics|Last 12 months"
@@ -11479,6 +11753,9 @@ msgstr ""
msgid "Jira integration not configured."
msgstr ""
+msgid "Jira project: %{importProject}"
+msgstr ""
+
msgid "JiraService|Events for %{noteable_model_name} are disabled."
msgstr ""
@@ -11868,6 +12145,9 @@ msgstr ""
msgid "Lead"
msgstr ""
+msgid "Lead Time"
+msgstr ""
+
msgid "Learn GitLab"
msgstr ""
@@ -11880,6 +12160,9 @@ msgstr ""
msgid "Learn how to %{no_packages_link_start}publish and share your packages%{no_packages_link_end} with GitLab."
msgstr ""
+msgid "Learn how to enable synchronization"
+msgstr ""
+
msgid "Learn more"
msgstr ""
@@ -12211,9 +12494,6 @@ msgstr ""
msgid "Live preview"
msgstr ""
-msgid "Load more vulnerabilities"
-msgstr ""
-
msgid "Loading"
msgstr ""
@@ -12364,9 +12644,6 @@ msgstr ""
msgid "Manage"
msgstr ""
-msgid "Manage Git repositories with fine-grained access controls that keep your code secure. Perform code reviews and enhance collaboration with merge requests. Each project can also have an issue tracker and a wiki."
-msgstr ""
-
msgid "Manage Web IDE features"
msgstr ""
@@ -12397,6 +12674,9 @@ msgstr ""
msgid "Manage your license"
msgstr ""
+msgid "Managed Account"
+msgstr ""
+
msgid "Manifest"
msgstr ""
@@ -12661,6 +12941,9 @@ msgstr ""
msgid "Merge Requests in Review"
msgstr ""
+msgid "Merge automatically (%{strategy})"
+msgstr ""
+
msgid "Merge commit message"
msgstr ""
@@ -12814,6 +13097,12 @@ msgstr ""
msgid "Merged branches are being deleted. This can take some time depending on the number of branches. Please refresh the page to see changes."
msgstr ""
+msgid "Merged this merge request."
+msgstr ""
+
+msgid "Merges this merge request immediately."
+msgstr ""
+
msgid "Merges this merge request when the pipeline succeeds."
msgstr ""
@@ -12921,12 +13210,15 @@ msgid_plural "Metrics|Edit metrics"
msgstr[0] ""
msgstr[1] ""
-msgid "Metrics|Environment"
+msgid "Metrics|Expand panel"
msgstr ""
msgid "Metrics|For grouping similar metrics"
msgstr ""
+msgid "Metrics|Go back (Esc)"
+msgstr ""
+
msgid "Metrics|Invalid time range, please verify."
msgstr ""
@@ -12957,9 +13249,6 @@ msgstr ""
msgid "Metrics|Refresh dashboard"
msgstr ""
-msgid "Metrics|Show last"
-msgstr ""
-
msgid "Metrics|There was an error creating the dashboard."
msgstr ""
@@ -13038,6 +13327,9 @@ msgstr ""
msgid "Microsoft Azure"
msgstr ""
+msgid "Middleman project with Static Site Editor support"
+msgstr ""
+
msgid "Migrated %{success_count}/%{total_count} files."
msgstr ""
@@ -13483,10 +13775,10 @@ msgstr ""
msgid "Next"
msgstr ""
-msgid "Next file in diff (MRs only)"
+msgid "Next file in diff"
msgstr ""
-msgid "Next unresolved discussion (MRs only)"
+msgid "Next unresolved discussion"
msgstr ""
msgid "Nickname"
@@ -13546,6 +13838,9 @@ msgstr ""
msgid "No changes between %{ref_start}%{source_branch}%{ref_end} and %{ref_start}%{target_branch}%{ref_end}"
msgstr ""
+msgid "No child epics match applied filters"
+msgstr ""
+
msgid "No connection could be made to a Gitaly Server, please check your logs!"
msgstr ""
@@ -13684,15 +13979,6 @@ msgstr ""
msgid "No thanks, don't show this again"
msgstr ""
-msgid "No vulnerabilities found for this group"
-msgstr ""
-
-msgid "No vulnerabilities found for this pipeline"
-msgstr ""
-
-msgid "No vulnerabilities found for this project"
-msgstr ""
-
msgid "No vulnerabilities present"
msgstr ""
@@ -13816,6 +14102,9 @@ msgstr ""
msgid "Nothing to preview."
msgstr ""
+msgid "Nothing to synchronize"
+msgstr ""
+
msgid "Notification events"
msgstr ""
@@ -13987,6 +14276,9 @@ msgstr ""
msgid "Once removed, the fork relationship cannot be restored and you will no longer be able to send merge requests to the source."
msgstr ""
+msgid "Once the exported file is ready you can download it from this page."
+msgstr ""
+
msgid "Once the exported file is ready, you will receive a notification email with a download link, or you can download it from this page."
msgstr ""
@@ -14088,9 +14380,6 @@ msgstr ""
msgid "Open sidebar"
msgstr ""
-msgid "Open source software to collaborate on code"
-msgstr ""
-
msgid "Open: %{openIssuesCount}"
msgstr ""
@@ -14193,6 +14482,9 @@ msgstr ""
msgid "OutdatedBrowser|You can provide feedback %{feedback_link_start}on this issue%{feedback_link_end} or via your usual support channels."
msgstr ""
+msgid "Overridden"
+msgstr ""
+
msgid "Overview"
msgstr ""
@@ -14244,6 +14536,9 @@ msgstr ""
msgid "PackageRegistry|Conan Command"
msgstr ""
+msgid "PackageRegistry|Copy .pypirc content"
+msgstr ""
+
msgid "PackageRegistry|Copy Conan Command"
msgstr ""
@@ -14265,6 +14560,9 @@ msgstr ""
msgid "PackageRegistry|Copy NuGet Setup Command"
msgstr ""
+msgid "PackageRegistry|Copy Pip command"
+msgstr ""
+
msgid "PackageRegistry|Copy and paste this inside your %{codeStart}pom.xml%{codeEnd} %{codeStart}dependencies%{codeEnd} block."
msgstr ""
@@ -14298,6 +14596,12 @@ msgstr ""
msgid "PackageRegistry|For more information on the NuGet registry, %{linkStart}see the documentation%{linkEnd}."
msgstr ""
+msgid "PackageRegistry|For more information on the PyPi registry, %{linkStart}see the documentation%{linkEnd}."
+msgstr ""
+
+msgid "PackageRegistry|If you haven't already done so, you will need to add the below to your %{codeStart}.pypirc%{codeEnd} file."
+msgstr ""
+
msgid "PackageRegistry|If you haven't already done so, you will need to add the below to your %{codeStart}pom.xml%{codeEnd} file."
msgstr ""
@@ -14307,6 +14611,9 @@ msgstr ""
msgid "PackageRegistry|Learn how to %{noPackagesLinkStart}publish and share your packages%{noPackagesLinkEnd} with GitLab."
msgstr ""
+msgid "PackageRegistry|Manually Published"
+msgstr ""
+
msgid "PackageRegistry|Maven"
msgstr ""
@@ -14325,12 +14632,18 @@ msgstr ""
msgid "PackageRegistry|NuGet Command"
msgstr ""
+msgid "PackageRegistry|Pip Command"
+msgstr ""
+
msgid "PackageRegistry|Pipeline %{linkStart}%{linkEnd} triggered %{timestamp} by %{author}"
msgstr ""
msgid "PackageRegistry|Published to the repository at %{timestamp}"
msgstr ""
+msgid "PackageRegistry|PyPi"
+msgstr ""
+
msgid "PackageRegistry|Registry Setup"
msgstr ""
@@ -14367,6 +14680,9 @@ msgstr ""
msgid "PackageRegistry|npm"
msgstr ""
+msgid "PackageRegistry|published by %{author}"
+msgstr ""
+
msgid "PackageRegistry|yarn"
msgstr ""
@@ -14382,6 +14698,9 @@ msgstr ""
msgid "PackageType|NuGet"
msgstr ""
+msgid "PackageType|PyPi"
+msgstr ""
+
msgid "Packages"
msgstr ""
@@ -14475,6 +14794,9 @@ msgstr ""
msgid "Past due"
msgstr ""
+msgid "Paste a machine public key here. Read more about how to generate it"
+msgstr ""
+
msgid "Paste a machine public key here. Read more about how to generate it %{link_start}here%{link_end}"
msgstr ""
@@ -14490,9 +14812,6 @@ msgstr ""
msgid "Path"
msgstr ""
-msgid "Path, transfer, remove"
-msgstr ""
-
msgid "Path:"
msgstr ""
@@ -14523,7 +14842,7 @@ msgstr ""
msgid "Percentage"
msgstr ""
-msgid "Perform advanced options such as changing path, transferring, or removing the group."
+msgid "Perform advanced options such as changing path, transferring, exporting, or removing the group."
msgstr ""
msgid "Perform common operations on GitLab project"
@@ -14604,9 +14923,6 @@ msgstr ""
msgid "Pipeline minutes quota"
msgstr ""
-msgid "Pipeline quota"
-msgstr ""
-
msgid "Pipeline subscriptions"
msgstr ""
@@ -14616,6 +14932,9 @@ msgstr ""
msgid "Pipeline: %{status}"
msgstr ""
+msgid "PipelineCharts|CI / CD Analytics"
+msgstr ""
+
msgid "PipelineCharts|Failed:"
msgstr ""
@@ -14694,15 +15013,6 @@ msgstr ""
msgid "Pipelines settings for '%{project_name}' were successfully updated."
msgstr ""
-msgid "Pipelines| to purchase more minutes."
-msgstr ""
-
-msgid "Pipelines|%{namespace_name} has exceeded its pipeline minutes quota."
-msgstr ""
-
-msgid "Pipelines|%{namespace_name} has less than %{notification_level}%% of CI minutes available."
-msgstr ""
-
msgid "Pipelines|API"
msgstr ""
@@ -14724,10 +15034,13 @@ msgstr ""
msgid "Pipelines|Get started with Pipelines"
msgstr ""
-msgid "Pipelines|Loading Pipelines"
+msgid "Pipelines|Group %{namespace_name} has %{percentage}%% or less Shared Runner Pipeline minutes remaining. Once it runs out, no new jobs or pipelines in its projects will run."
msgstr ""
-msgid "Pipelines|Pipelines will not run anymore on shared Runners."
+msgid "Pipelines|Group %{namespace_name} has exceeded its pipeline minutes quota. Unless you buy additional pipeline minutes, no new jobs or pipelines in its projects will run."
+msgstr ""
+
+msgid "Pipelines|Loading Pipelines"
msgstr ""
msgid "Pipelines|Project cache successfully reset."
@@ -14958,6 +15271,9 @@ msgstr ""
msgid "Please select"
msgstr ""
+msgid "Please select a Jira project"
+msgstr ""
+
msgid "Please select a country"
msgstr ""
@@ -15003,6 +15319,9 @@ msgstr ""
msgid "Please wait while we import the repository for you. Refresh at will."
msgstr ""
+msgid "Plugins directory is deprecated and will be removed in 14.0. Please move this file into /file_hooks directory."
+msgstr ""
+
msgid "Pod does not exist"
msgstr ""
@@ -15120,7 +15439,7 @@ msgstr ""
msgid "Prevent users from changing their profile name"
msgstr ""
-msgid "Prevent users from modifing merge request approvers list"
+msgid "Prevent users from modifying merge request approvers list"
msgstr ""
msgid "Prevent users from performing write operations on GitLab while performing maintenance."
@@ -15141,10 +15460,10 @@ msgstr ""
msgid "Previous Artifacts"
msgstr ""
-msgid "Previous file in diff (MRs only)"
+msgid "Previous file in diff"
msgstr ""
-msgid "Previous unresolved discussion (MRs only)"
+msgid "Previous unresolved discussion"
msgstr ""
msgid "Primary"
@@ -15177,6 +15496,9 @@ msgstr ""
msgid "Private profile"
msgstr ""
+msgid "Private projects Minutes cost factor"
+msgstr ""
+
msgid "Private projects can be created in your personal namespace with:"
msgstr ""
@@ -16005,6 +16327,9 @@ msgstr ""
msgid "ProjectSettings|With GitLab Pages you can host your static websites on GitLab"
msgstr ""
+msgid "ProjectSettings|With Metrics Dashboard you can visualize this project performance metrics"
+msgstr ""
+
msgid "ProjectTemplates|.NET Core"
msgstr ""
@@ -16068,6 +16393,9 @@ msgstr ""
msgid "ProjectTemplates|Spring"
msgstr ""
+msgid "ProjectTemplates|Static Site Editor/Middleman"
+msgstr ""
+
msgid "ProjectTemplates|iOS (Swift)"
msgstr ""
@@ -16287,13 +16615,13 @@ msgstr ""
msgid "PrometheusService|Prometheus is being automatically managed on your clusters"
msgstr ""
-msgid "PrometheusService|These metrics will only be monitored after your first deployment to an environment"
+msgid "PrometheusService|Select the Active checkbox to override the Auto Configuration with custom settings. If unchecked, Auto Configuration settings are used."
msgstr ""
-msgid "PrometheusService|Time-series monitoring service"
+msgid "PrometheusService|These metrics will only be monitored after your first deployment to an environment"
msgstr ""
-msgid "PrometheusService|To enable manual configuration, uninstall Prometheus from your clusters"
+msgid "PrometheusService|Time-series monitoring service"
msgstr ""
msgid "PrometheusService|To enable the installation of Prometheus on your clusters, deactivate the manual configuration below"
@@ -16479,6 +16807,9 @@ msgstr ""
msgid "Protip:"
msgstr ""
+msgid "Protocol"
+msgstr ""
+
msgid "Provider"
msgstr ""
@@ -16503,6 +16834,9 @@ msgstr ""
msgid "Public pipelines"
msgstr ""
+msgid "Public projects Minutes cost factor"
+msgstr ""
+
msgid "Pull"
msgstr ""
@@ -16703,6 +17037,9 @@ msgid_plural "Refreshing in %d seconds to show the updated status..."
msgstr[0] ""
msgstr[1] ""
+msgid "Regenerate export"
+msgstr ""
+
msgid "Regenerate instance ID"
msgstr ""
@@ -17080,9 +17417,29 @@ msgstr ""
msgid "Reports|%{combinedString} and %{resolvedString}"
msgstr ""
+msgid "Reports|Accessibility scanning detected %d issue for the source branch only"
+msgid_plural "Reports|Accessibility scanning detected %d issues for the source branch only"
+msgstr[0] ""
+msgstr[1] ""
+
+msgid "Reports|Accessibility scanning detected no issues for the source branch only"
+msgstr ""
+
+msgid "Reports|Accessibility scanning failed loading results"
+msgstr ""
+
+msgid "Reports|Accessibility scanning results are being parsed"
+msgstr ""
+
msgid "Reports|Actions"
msgstr ""
+msgid "Reports|An error occured while loading report"
+msgstr ""
+
+msgid "Reports|An error occurred while loading %{name} results"
+msgstr ""
+
msgid "Reports|Class"
msgstr ""
@@ -17188,7 +17545,7 @@ msgstr ""
msgid "Requested %{time_ago}"
msgstr ""
-msgid "Requested design version does not exist"
+msgid "Requested design version does not exist."
msgstr ""
msgid "Requested states are invalid"
@@ -17372,6 +17729,9 @@ msgstr ""
msgid "Resync"
msgstr ""
+msgid "Resync all"
+msgstr ""
+
msgid "Resync all %{replicableType}"
msgstr ""
@@ -17440,6 +17800,9 @@ msgstr ""
msgid "Revoked personal access token %{personal_access_token_name}!"
msgstr ""
+msgid "Revoked project access token %{project_access_token_name}!"
+msgstr ""
+
msgid "RightSidebar|adding a"
msgstr ""
@@ -17488,6 +17851,9 @@ msgstr ""
msgid "Runner token"
msgstr ""
+msgid "Runner tokens"
+msgstr ""
+
msgid "Runner was not updated."
msgstr ""
@@ -17542,6 +17908,9 @@ msgstr ""
msgid "SAML SSO for %{group_name}"
msgstr ""
+msgid "SAML discovery tokens"
+msgstr ""
+
msgid "SAML for %{group_name}"
msgstr ""
@@ -17626,12 +17995,18 @@ msgstr ""
msgid "Scheduled"
msgstr ""
+msgid "Scheduled to merge this merge request (%{strategy})."
+msgstr ""
+
msgid "Scheduled to merge this merge request when the pipeline succeeds."
msgstr ""
msgid "Schedules"
msgstr ""
+msgid "Schedules to merge this merge request (%{strategy})."
+msgstr ""
+
msgid "Scheduling"
msgstr ""
@@ -17689,6 +18064,9 @@ msgstr ""
msgid "Search branches and tags"
msgstr ""
+msgid "Search by author"
+msgstr ""
+
msgid "Search files"
msgstr ""
@@ -17834,11 +18212,6 @@ msgid_plural "SearchResults|snippets"
msgstr[0] ""
msgstr[1] ""
-msgid "SearchResults|snippet result"
-msgid_plural "SearchResults|snippet results"
-msgstr[0] ""
-msgstr[1] ""
-
msgid "SearchResults|user"
msgid_plural "SearchResults|users"
msgstr[0] ""
@@ -17849,6 +18222,9 @@ msgid_plural "SearchResults|wiki results"
msgstr[0] ""
msgstr[1] ""
+msgid "Searching by both author and message is currently not supported."
+msgstr ""
+
msgid "Seat Link"
msgstr ""
@@ -17876,184 +18252,232 @@ msgstr ""
msgid "Security Dashboard"
msgstr ""
-msgid "Security Dashboard|Error fetching the vulnerability counts. Please check your network connection and try again."
+msgid "Security configuration help link"
+msgstr ""
+
+msgid "Security dashboard"
msgstr ""
-msgid "Security Dashboard|Error fetching the vulnerability list. Please check your network connection and try again."
+msgid "Security report is out of date. Please update your branch with the latest changes from the target branch (%{targetBranchName})"
msgstr ""
-msgid "Security Dashboard|Issue Created"
+msgid "Security report is out of date. Run %{newPipelineLinkStart}a new pipeline%{newPipelineLinkEnd} for the target branch (%{targetBranchName})"
msgstr ""
-msgid "Security Reports|Comment added to '%{vulnerabilityName}'"
+msgid "SecurityConfiguration|Configured"
msgstr ""
-msgid "Security Reports|Comment deleted on '%{vulnerabilityName}'"
+msgid "SecurityConfiguration|Feature"
msgstr ""
-msgid "Security Reports|Comment edited on '%{vulnerabilityName}'"
+msgid "SecurityConfiguration|Feature documentation for %{featureName}"
msgstr ""
-msgid "Security Reports|Create issue"
+msgid "SecurityConfiguration|Not yet configured"
msgstr ""
-msgid "Security Reports|Dismiss vulnerability"
+msgid "SecurityConfiguration|Secure features"
msgstr ""
-msgid "Security Reports|Dismissed '%{vulnerabilityName}'"
+msgid "SecurityConfiguration|Status"
msgstr ""
-msgid "Security Reports|Dismissed '%{vulnerabilityName}'. Turn off the hide dismissed toggle to view."
+msgid "SecurityReports|%{firstProject} and %{secondProject}"
msgstr ""
-msgid "Security Reports|Either you don't have permission to view this dashboard or the dashboard has not been setup. Please check your permission settings with your administrator or check your dashboard configurations to proceed."
+msgid "SecurityReports|%{firstProject}, %{secondProject}, and %{rest}"
msgstr ""
-msgid "Security Reports|Learn more about setting up your dashboard"
+msgid "SecurityReports|Add a project to your dashboard"
msgstr ""
-msgid "Security Reports|More info"
+msgid "SecurityReports|Add or remove projects from your dashboard"
msgstr ""
-msgid "Security Reports|Oops, something doesn't seem right."
+msgid "SecurityReports|Add projects"
msgstr ""
-msgid "Security Reports|Security reports can only be accessed by authorized users."
+msgid "SecurityReports|Comment added to '%{vulnerabilityName}'"
msgstr ""
-msgid "Security Reports|There was an error adding the comment."
+msgid "SecurityReports|Comment deleted on '%{vulnerabilityName}'"
msgstr ""
-msgid "Security Reports|There was an error creating the issue."
+msgid "SecurityReports|Comment edited on '%{vulnerabilityName}'"
msgstr ""
-msgid "Security Reports|There was an error creating the merge request."
+msgid "SecurityReports|Create issue"
msgstr ""
-msgid "Security Reports|There was an error deleting the comment."
+msgid "SecurityReports|Dismiss Selected"
msgstr ""
-msgid "Security Reports|There was an error dismissing the vulnerabilities."
+msgid "SecurityReports|Dismiss vulnerability"
msgstr ""
-msgid "Security Reports|There was an error dismissing the vulnerability."
+msgid "SecurityReports|Dismissed '%{vulnerabilityName}'"
msgstr ""
-msgid "Security Reports|There was an error reverting the dismissal."
+msgid "SecurityReports|Dismissed '%{vulnerabilityName}'. Turn off the hide dismissed toggle to view."
msgstr ""
-msgid "Security Reports|There was an error reverting this dismissal."
+msgid "SecurityReports|Each vulnerability now has a unique page that can be directly linked to, shared, referenced, and tracked as the single source of truth. Vulnerability occurrences also persist across scanner runs, which improves tracking and visibility and reduces duplicates between scans."
msgstr ""
-msgid "Security Reports|Undo dismiss"
+msgid "SecurityReports|Edit dashboard"
msgstr ""
-msgid "Security Reports|You do not have sufficient permissions to access this report"
+msgid "SecurityReports|Either you don't have permission to view this dashboard or the dashboard has not been setup. Please check your permission settings with your administrator or check your dashboard configurations to proceed."
msgstr ""
-msgid "Security Reports|You must sign in as an authorized user to see this report"
+msgid "SecurityReports|Error fetching the vulnerability counts. Please check your network connection and try again."
msgstr ""
-msgid "Security configuration help link"
+msgid "SecurityReports|Error fetching the vulnerability list. Please check your network connection and try again."
msgstr ""
-msgid "Security dashboard"
+msgid "SecurityReports|False positive"
msgstr ""
-msgid "Security report is out of date. Please update your branch with the latest changes from the target branch (%{targetBranchName})"
+msgid "SecurityReports|Hide dismissed"
msgstr ""
-msgid "Security report is out of date. Run %{newPipelineLinkStart}a new pipeline%{newPipelineLinkEnd} for the target branch (%{targetBranchName})"
+msgid "SecurityReports|Introducing standalone vulnerabilities"
msgstr ""
-msgid "SecurityConfiguration|Configured"
+msgid "SecurityReports|Issue Created"
msgstr ""
-msgid "SecurityConfiguration|Feature"
+msgid "SecurityReports|Learn More"
msgstr ""
-msgid "SecurityConfiguration|Feature documentation for %{featureName}"
+msgid "SecurityReports|Learn more about setting up your dashboard"
msgstr ""
-msgid "SecurityConfiguration|Not yet configured"
+msgid "SecurityReports|Load more vulnerabilities"
msgstr ""
-msgid "SecurityConfiguration|Secure features"
+msgid "SecurityReports|Monitor vulnerabilities in your code"
msgstr ""
-msgid "SecurityConfiguration|Status"
+msgid "SecurityReports|More info"
msgstr ""
-msgid "SecurityDashboard|%{firstProject} and %{secondProject}"
+msgid "SecurityReports|More information"
msgstr ""
-msgid "SecurityDashboard|%{firstProject}, %{secondProject}, and %{rest}"
+msgid "SecurityReports|No vulnerabilities found for dashboard"
msgstr ""
-msgid "SecurityDashboard|Add a project to your dashboard"
+msgid "SecurityReports|No vulnerabilities found for this group"
msgstr ""
-msgid "SecurityDashboard|Add or remove projects from your dashboard"
+msgid "SecurityReports|No vulnerabilities found for this pipeline"
msgstr ""
-msgid "SecurityDashboard|Add projects"
+msgid "SecurityReports|No vulnerabilities found for this project"
msgstr ""
-msgid "SecurityDashboard|Edit dashboard"
+msgid "SecurityReports|Oops, something doesn't seem right."
msgstr ""
-msgid "SecurityDashboard|Hide dismissed"
+msgid "SecurityReports|Pipeline %{pipelineLink} triggered %{timeago} by %{user}"
msgstr ""
-msgid "SecurityDashboard|Monitor vulnerabilities in your code"
+msgid "SecurityReports|Project"
msgstr ""
-msgid "SecurityDashboard|More information"
+msgid "SecurityReports|Projects added"
msgstr ""
-msgid "SecurityDashboard|No vulnerabilities found for dashboard"
+msgid "SecurityReports|Remove project from dashboard"
msgstr ""
-msgid "SecurityDashboard|Pipeline %{pipelineLink} triggered %{timeago} by %{user}"
+msgid "SecurityReports|Report type"
msgstr ""
-msgid "SecurityDashboard|Project"
+msgid "SecurityReports|Return to dashboard"
msgstr ""
-msgid "SecurityDashboard|Projects added"
+msgid "SecurityReports|Security Dashboard"
msgstr ""
-msgid "SecurityDashboard|Remove project from dashboard"
+msgid "SecurityReports|Security reports can only be accessed by authorized users."
msgstr ""
-msgid "SecurityDashboard|Report type"
+msgid "SecurityReports|Select a project to add by using the project search field above."
msgstr ""
-msgid "SecurityDashboard|Return to dashboard"
+msgid "SecurityReports|Select a reason"
msgstr ""
-msgid "SecurityDashboard|Security Dashboard"
+msgid "SecurityReports|Severity"
msgstr ""
-msgid "SecurityDashboard|Select a project to add by using the project search field above."
+msgid "SecurityReports|Status"
msgstr ""
-msgid "SecurityDashboard|Severity"
+msgid "SecurityReports|The security dashboard displays the latest security findings for projects you wish to monitor. Select \"Edit dashboard\" to add and remove projects."
msgstr ""
-msgid "SecurityDashboard|Status"
+msgid "SecurityReports|The security dashboard displays the latest security report. Use it to find and fix vulnerabilities."
msgstr ""
-msgid "SecurityDashboard|The security dashboard displays the latest security findings for projects you wish to monitor. Select \"Edit dashboard\" to add and remove projects."
+msgid "SecurityReports|There was an error adding the comment."
msgstr ""
-msgid "SecurityDashboard|The security dashboard displays the latest security report. Use it to find and fix vulnerabilities."
+msgid "SecurityReports|There was an error creating the issue."
msgstr ""
-msgid "SecurityDashboard|There was an error while generating the report."
+msgid "SecurityReports|There was an error creating the merge request."
msgstr ""
-msgid "SecurityDashboard|Unable to add %{invalidProjects}"
+msgid "SecurityReports|There was an error deleting the comment."
+msgstr ""
+
+msgid "SecurityReports|There was an error dismissing the vulnerabilities."
+msgstr ""
+
+msgid "SecurityReports|There was an error dismissing the vulnerability."
+msgstr ""
+
+msgid "SecurityReports|There was an error reverting the dismissal."
+msgstr ""
+
+msgid "SecurityReports|There was an error reverting this dismissal."
+msgstr ""
+
+msgid "SecurityReports|There was an error while generating the report."
+msgstr ""
+
+msgid "SecurityReports|Unable to add %{invalidProjects}"
+msgstr ""
+
+msgid "SecurityReports|Undo dismiss"
+msgstr ""
+
+msgid "SecurityReports|While it's rare to have no vulnerabilities for your group, it can happen. In any event, we ask that you double check your settings to make sure you've set up your dashboard correctly."
+msgstr ""
+
+msgid "SecurityReports|While it's rare to have no vulnerabilities for your pipeline, it can happen. In any event, we ask that you double check your settings to make sure all security scanning jobs have passed successfully."
+msgstr ""
+
+msgid "SecurityReports|While it's rare to have no vulnerabilities for your project, it can happen. In any event, we ask that you double check your settings to make sure you've set up your dashboard correctly."
+msgstr ""
+
+msgid "SecurityReports|While it's rare to have no vulnerabilities, it can happen. In any event, we ask that you please double check your settings to make sure you've set up your dashboard correctly."
+msgstr ""
+
+msgid "SecurityReports|Won't fix / Accept risk"
+msgstr ""
+
+msgid "SecurityReports|You do not have sufficient permissions to access this report"
+msgstr ""
+
+msgid "SecurityReports|You must sign in as an authorized user to see this report"
+msgstr ""
+
+msgid "SecurityReports|[No reason]"
msgstr ""
msgid "See GitLab's %{password_policy_guidelines}"
@@ -18065,6 +18489,9 @@ msgstr ""
msgid "See the affected projects in the GitLab admin panel"
msgstr ""
+msgid "See what's new at GitLab"
+msgstr ""
+
msgid "Select"
msgstr ""
@@ -18470,9 +18897,6 @@ msgstr ""
msgid "Set up Jira Integration"
msgstr ""
-msgid "Set up Jira Integration illustration"
-msgstr ""
-
msgid "Set up a %{type} Runner automatically"
msgstr ""
@@ -18557,6 +18981,9 @@ msgstr ""
msgid "Severity: %{severity}"
msgstr ""
+msgid "Shards (%{shards})"
+msgstr ""
+
msgid "Shards to synchronize"
msgstr ""
@@ -18871,6 +19298,9 @@ msgstr ""
msgid "Snowplow"
msgstr ""
+msgid "Some child epics may be hidden due to applied filters"
+msgstr ""
+
msgid "Some email servers do not support overriding the email sender name. Enable this option to include the name of the author of the issue, merge request or comment in the email body instead."
msgstr ""
@@ -19012,6 +19442,9 @@ msgstr ""
msgid "Something went wrong while updating your list settings"
msgstr ""
+msgid "Something went wrong with your automatic subscription renewal"
+msgstr ""
+
msgid "Something went wrong, unable to add %{project} to dashboard"
msgstr ""
@@ -19297,6 +19730,9 @@ msgstr ""
msgid "Stage removed"
msgstr ""
+msgid "Standard"
+msgstr ""
+
msgid "Star a label to make it a priority label. Order the prioritized labels to change their relative priority, by dragging."
msgstr ""
@@ -19420,6 +19856,9 @@ msgstr ""
msgid "Static Application Security Testing (SAST)"
msgstr ""
+msgid "StaticSiteEditor|An error occurred while submitting your changes."
+msgstr ""
+
msgid "StaticSiteEditor|Branch could not be created."
msgstr ""
@@ -19429,18 +19868,30 @@ msgstr ""
msgid "StaticSiteEditor|Could not create merge request."
msgstr ""
+msgid "StaticSiteEditor|Incompatible file content"
+msgstr ""
+
msgid "StaticSiteEditor|Return to site"
msgstr ""
+msgid "StaticSiteEditor|Static site editor"
+msgstr ""
+
msgid "StaticSiteEditor|Success!"
msgstr ""
msgid "StaticSiteEditor|Summary of changes"
msgstr ""
+msgid "StaticSiteEditor|The Static Site Editor is currently configured to only edit Markdown content on pages generated from Middleman. Visit the documentation to learn more about configuring your site to use the Static Site Editor."
+msgstr ""
+
msgid "StaticSiteEditor|Update %{sourcePath} file"
msgstr ""
+msgid "StaticSiteEditor|View documentation"
+msgstr ""
+
msgid "StaticSiteEditor|View merge request"
msgstr ""
@@ -19498,6 +19949,12 @@ msgstr ""
msgid "StatusPage|Status page"
msgstr ""
+msgid "StatusPage|Status page URL"
+msgstr ""
+
+msgid "StatusPage|Status page frontend documentation"
+msgstr ""
+
msgid "StatusPage|To publish incidents to an external status page, GitLab will store a JSON file in your Amazon S3 account in a location accessible to your external status page service. Make sure to also set up %{docsLink}"
msgstr ""
@@ -19846,6 +20303,12 @@ msgstr ""
msgid "Synced"
msgstr ""
+msgid "Synchronization disabled"
+msgstr ""
+
+msgid "Synchronization of container repositories is disabled."
+msgstr ""
+
msgid "System"
msgstr ""
@@ -19999,6 +20462,9 @@ msgstr ""
msgid "Telephone number"
msgstr ""
+msgid "Telephone number (Optional)"
+msgstr ""
+
msgid "Template"
msgstr ""
@@ -20305,6 +20771,9 @@ msgstr ""
msgid "The license was successfully uploaded and is now active. You can see the details below."
msgstr ""
+msgid "The license was successfully uploaded and will be active from %{starts_at}. You can see the details below."
+msgstr ""
+
msgid "The maximum file size allowed is %{size}."
msgstr ""
@@ -20626,6 +21095,9 @@ msgstr ""
msgid "There was an error getting the epic participants."
msgstr ""
+msgid "There was an error importing the Jira project."
+msgstr ""
+
msgid "There was an error loading users activity calendar."
msgstr ""
@@ -20641,7 +21113,7 @@ msgstr ""
msgid "There was an error resetting user pipeline minutes."
msgstr ""
-msgid "There was an error saving this Geo Node"
+msgid "There was an error saving this Geo Node."
msgstr ""
msgid "There was an error saving your changes."
@@ -20704,6 +21176,9 @@ msgstr ""
msgid "They can be managed using the %{link}."
msgstr ""
+msgid "Third Party Advisory Link"
+msgstr ""
+
msgid "Third party offers"
msgstr ""
@@ -20740,6 +21215,9 @@ msgstr ""
msgid "This application will be able to:"
msgstr ""
+msgid "This attachment has been truncated to avoid exceeding the maximum allowed attachment size of 15MB. %{written_count} of %{issues_count} issues have been included. Consider re-exporting with a narrower selection of issues."
+msgstr ""
+
msgid "This block is self-referential"
msgstr ""
@@ -20881,6 +21359,12 @@ msgstr ""
msgid "This issue is confidential"
msgstr ""
+msgid "This issue is currently blocked by the following issues: %{issues}."
+msgstr ""
+
+msgid "This issue is in a child epic of the filtered epic"
+msgstr ""
+
msgid "This issue is locked."
msgstr ""
@@ -21079,7 +21563,10 @@ msgstr ""
msgid "This user will be the author of all events in the activity feed that are the result of an update, like new branches being created or new commits being pushed to existing branches. Upon creation or when reassigning you can only assign yourself to be the mirror user."
msgstr ""
-msgid "This variable can not be masked"
+msgid "This variable can not be masked."
+msgstr ""
+
+msgid "This variable does not match the expected pattern."
msgstr ""
msgid "This will help us personalize your onboarding experience."
@@ -21109,10 +21596,10 @@ msgstr ""
msgid "ThreatMonitoring|Application firewall not detected"
msgstr ""
-msgid "ThreatMonitoring|Container Network Policy"
+msgid "ThreatMonitoring|Container Network Policies are not installed or have been disabled. To view this data, ensure your Network Policies are installed and enabled for your cluster."
msgstr ""
-msgid "ThreatMonitoring|Container NetworkPolicies are not installed or has been disabled. To view this data, ensure you NetworkPolicies are installed and enabled for your cluster."
+msgid "ThreatMonitoring|Container Network Policy"
msgstr ""
msgid "ThreatMonitoring|Container NetworkPolicies not detected"
@@ -21145,7 +21632,7 @@ msgstr ""
msgid "ThreatMonitoring|Something went wrong, unable to fetch statistics"
msgstr ""
-msgid "ThreatMonitoring|The firewall is not installed or has been disabled. To view this data, ensure you firewall is installed and enabled for your cluster."
+msgid "ThreatMonitoring|The firewall is not installed or has been disabled. To view this data, ensure the web application firewall is installed and enabled for your cluster."
msgstr ""
msgid "ThreatMonitoring|The graph below is an overview of traffic coming to your application as tracked by the Web Application Firewall (WAF). View the docs for instructions on how to access the WAF logs to see what type of malicious traffic is trying to access your app. The docs link is also accessible by clicking the \"?\" icon next to the title below."
@@ -21214,6 +21701,9 @@ msgstr ""
msgid "Time in seconds GitLab will wait for a response from the external service. When the service does not respond in time, access will be denied."
msgstr ""
+msgid "Time of import: %{importTime}"
+msgstr ""
+
msgid "Time remaining"
msgstr ""
@@ -21398,7 +21888,7 @@ msgstr ""
msgid "Title:"
msgstr ""
-msgid "Titles and Filenames"
+msgid "Titles and Descriptions"
msgstr ""
msgid "To"
@@ -21605,7 +22095,7 @@ msgstr ""
msgid "Too many projects enabled. You will need to manage them via the console or the API."
msgstr ""
-msgid "Topics"
+msgid "Topics (optional)"
msgstr ""
msgid "Total"
@@ -21767,6 +22257,9 @@ msgstr ""
msgid "Try all GitLab has to offer for 30 days."
msgstr ""
+msgid "Try changing or removing filters."
+msgstr ""
+
msgid "Try to fork again"
msgstr ""
@@ -21860,6 +22353,9 @@ msgstr ""
msgid "Unable to connect to server: %{error}"
msgstr ""
+msgid "Unable to connect to the Jira instance. Please check your Jira integration configuration."
+msgstr ""
+
msgid "Unable to convert Kubernetes logs encoding to UTF-8"
msgstr ""
@@ -22803,6 +23299,9 @@ msgstr ""
msgid "View issue"
msgstr ""
+msgid "View issues"
+msgstr ""
+
msgid "View it on GitLab"
msgstr ""
@@ -22980,6 +23479,15 @@ msgstr ""
msgid "VulnerabilityManagement|Resolved %{timeago} by %{user}"
msgstr ""
+msgid "VulnerabilityManagement|Something went wrong while trying to delete the comment. Please try again later."
+msgstr ""
+
+msgid "VulnerabilityManagement|Something went wrong while trying to retrieve the vulnerability history. Please try again later."
+msgstr ""
+
+msgid "VulnerabilityManagement|Something went wrong while trying to save the comment. Please try again later."
+msgstr ""
+
msgid "VulnerabilityManagement|Something went wrong, could not create an issue."
msgstr ""
@@ -23010,6 +23518,9 @@ msgstr ""
msgid "VulnerabilityStatusTypes|Resolved"
msgstr ""
+msgid "Vulnerability|%{scannerName} (version %{scannerVersion})"
+msgstr ""
+
msgid "Vulnerability|Class"
msgstr ""
@@ -23043,7 +23554,10 @@ msgstr ""
msgid "Vulnerability|Project"
msgstr ""
-msgid "Vulnerability|Report Type"
+msgid "Vulnerability|Scanner Provider"
+msgstr ""
+
+msgid "Vulnerability|Scanner Type"
msgstr ""
msgid "Vulnerability|Severity"
@@ -23097,9 +23611,15 @@ msgstr ""
msgid "We sent you an email with reset password instructions"
msgstr ""
+msgid "We tried to automatically renew your %{strong}%{plan_name}%{strong_close} subscription for %{strong}%{namespace_name}%{strong_close} on %{expires_on} but something went wrong so your subscription was downgraded to the free plan. Don't worry, your data is safe. We suggest you check your payment method and get in touch with our support team (%{support_link}). They'll gladly help with your subscription renewal."
+msgstr ""
+
msgid "We want to be sure it is you, please confirm you are not a robot."
msgstr ""
+msgid "We will automatically renew your %{strong}%{plan_name}%{strong_close} subscription for %{strong}%{namespace_name}%{strong_close} on %{strong}%{expires_on}%{strong_close}. There's nothing that you need to do, we'll let you know when the renewal is complete. Need more seats, a higher plan or just want to review your payment method?"
+msgstr ""
+
msgid "We've found no vulnerabilities"
msgstr ""
@@ -23204,15 +23724,6 @@ msgstr ""
msgid "When:"
msgstr ""
-msgid "While it's rare to have no vulnerabilities for your group, it can happen. In any event, we ask that you double check your settings to make sure you've set up your dashboard correctly."
-msgstr ""
-
-msgid "While it's rare to have no vulnerabilities for your pipeline, it can happen. In any event, we ask that you double check your settings to make sure all security scanning jobs have passed successfully."
-msgstr ""
-
-msgid "While it's rare to have no vulnerabilities for your project, it can happen. In any event, we ask that you double check your settings to make sure you've set up your dashboard correctly."
-msgstr ""
-
msgid "While it's rare to have no vulnerabilities, it can happen. In any event, we ask that you please double check your settings to make sure you've set up your dashboard correctly."
msgstr ""
@@ -23243,9 +23754,6 @@ msgstr ""
msgid "Wiki"
msgstr ""
-msgid "Wiki pages"
-msgstr ""
-
msgid "Wiki was successfully updated."
msgstr ""
@@ -23429,6 +23937,9 @@ msgstr ""
msgid "Yes, add it"
msgstr ""
+msgid "Yes, close issue"
+msgstr ""
+
msgid "Yes, let me map Google Code users to full names or GitLab users."
msgstr ""
@@ -23624,6 +24135,9 @@ msgstr ""
msgid "You could not create a new trigger."
msgstr ""
+msgid "You didn't renew your %{strong}%{plan_name}%{strong_close} subscription for %{strong}%{namespace_name}%{strong_close} so it was downgraded to the free plan."
+msgstr ""
+
msgid "You didn't renew your %{strong}%{plan_name}%{strong_close} subscription so it was downgraded to the GitLab Core Plan."
msgstr ""
@@ -23690,6 +24204,9 @@ msgstr ""
msgid "You have declined the invitation to join %{label}."
msgstr ""
+msgid "You have imported from this project %{numberOfPreviousImportsForProject} times before. Each new import will create duplicate issues."
+msgstr ""
+
msgid "You have no permissions"
msgstr ""
@@ -23771,6 +24288,9 @@ msgstr ""
msgid "You need to upload a Google Takeout archive."
msgstr ""
+msgid "You reached %{usage_in_percent} of %{namespace_name}'s capacity (%{used_storage} of %{storage_limit})"
+msgstr ""
+
msgid "You tried to fork %{link_to_the_project} but it failed for the following reason:"
msgstr ""
@@ -23855,9 +24375,21 @@ msgstr ""
msgid "YouTube"
msgstr ""
+msgid "Your %{strong}%{plan_name}%{strong_close} subscription for %{strong}%{namespace_name}%{strong_close} will expire on %{strong}%{expires_on}%{strong_close}. After that, you will not to be able to create issues or merge requests as well as many other features."
+msgstr ""
+
msgid "Your %{strong}%{plan_name}%{strong_close} subscription will expire on %{strong}%{expires_on}%{strong_close}. After that, you will not to be able to create issues or merge requests as well as many other features."
msgstr ""
+msgid "Your CSV export has started. It will be emailed to %{email} when complete."
+msgstr ""
+
+msgid "Your CSV export of %{issues_count} from project %{project_link} has been added to this email as an attachment."
+msgstr ""
+
+msgid "Your CSV export of %{written_count} from project %{project_name} (%{project_url}) has been added to this email as an attachment."
+msgstr ""
+
msgid "Your Commit Email will be used for web based operations, such as edits and merges."
msgstr ""
@@ -23954,6 +24486,9 @@ msgstr ""
msgid "Your comment could not be updated! Please check your network connection and try again."
msgstr ""
+msgid "Your comment will be discarded."
+msgstr ""
+
msgid "Your custom stage '%{title}' was created"
msgstr ""
@@ -23993,6 +24528,9 @@ msgstr ""
msgid "Your new personal access token has been created."
msgstr ""
+msgid "Your new project access token has been created."
+msgstr ""
+
msgid "Your password isn't required to view this page. If a password or any other personal details are requested, please contact your administrator to report abuse."
msgstr ""
@@ -24011,12 +24549,18 @@ msgstr ""
msgid "Your request for access has been queued for review."
msgstr ""
+msgid "Your search didn't match any commits."
+msgstr ""
+
msgid "Your subscription expired!"
msgstr ""
msgid "Your subscription has been downgraded"
msgstr ""
+msgid "Your subscription will automatically renew in %{remaining_days}"
+msgstr ""
+
msgid "Your subscription will expire in %{remaining_days}"
msgstr ""
@@ -24055,7 +24599,7 @@ msgstr ""
msgid "allowed to fail"
msgstr ""
-msgid "already being used for another group or project milestone."
+msgid "already being used for another group or project %{timebox_name}."
msgstr ""
msgid "already has a \"created\" issue link"
@@ -24133,6 +24677,9 @@ msgstr ""
msgid "ciReport|%{linkStartTag}Learn more about SAST %{linkEndTag}"
msgstr ""
+msgid "ciReport|%{linkStartTag}Learn more about Secret Scanning %{linkEndTag}"
+msgstr ""
+
msgid "ciReport|%{linkStartTag}Learn more about codequality reports %{linkEndTag}"
msgstr ""
@@ -24296,6 +24843,12 @@ msgstr ""
msgid "ciReport|SAST"
msgstr ""
+msgid "ciReport|Secret scanning"
+msgstr ""
+
+msgid "ciReport|Secret scanning detects secrets and credentials vulnerabilities in your source code."
+msgstr ""
+
msgid "ciReport|Security scanning"
msgstr ""
@@ -24493,6 +25046,9 @@ msgstr ""
msgid "group"
msgstr ""
+msgid "groups"
+msgstr ""
+
msgid "has already been linked to another vulnerability"
msgstr ""
@@ -24656,9 +25212,6 @@ msgstr[1] ""
msgid "merged %{time_ago}"
msgstr ""
-msgid "milestone should belong either to a project or a group."
-msgstr ""
-
msgid "missing"
msgstr ""
@@ -24995,6 +25548,9 @@ msgstr ""
msgid "no contributions"
msgstr ""
+msgid "no expiration"
+msgstr ""
+
msgid "no one can merge"
msgstr ""
@@ -25047,6 +25603,9 @@ msgstr ""
msgid "pending removal"
msgstr ""
+msgid "per day"
+msgstr ""
+
msgid "pipeline"
msgstr ""
@@ -25075,6 +25634,12 @@ msgstr[1] ""
msgid "project avatar"
msgstr ""
+msgid "projects"
+msgstr ""
+
+msgid "push to your repository, create pipelines, create issues or add comments. To reduce storage capacity, delete unused repositories, artifacts, wikis, issues, and pipelines."
+msgstr ""
+
msgid "quick actions"
msgstr ""
@@ -25221,11 +25786,6 @@ msgstr ""
msgid "this document"
msgstr ""
-msgid "thread resolved"
-msgid_plural "threads resolved"
-msgstr[0] ""
-msgstr[1] ""
-
msgid "to help your contributors communicate effectively!"
msgstr ""
@@ -25280,6 +25840,9 @@ msgstr ""
msgid "view the blob"
msgstr ""
+msgid "vulnerability|Add a comment"
+msgstr ""
+
msgid "vulnerability|Add a comment or reason for dismissal"
msgstr ""
diff --git a/locale/vi_VN/gitlab.po b/locale/vi_VN/gitlab.po
index 4e4c772d46c..a2c51f338f0 100644
--- a/locale/vi_VN/gitlab.po
+++ b/locale/vi_VN/gitlab.po
@@ -12,7 +12,7 @@ msgstr ""
"X-Crowdin-Project: gitlab-ee\n"
"X-Crowdin-Language: vi\n"
"X-Crowdin-File: /master/locale/gitlab.pot\n"
-"PO-Revision-Date: 2020-04-15 00:28\n"
+"PO-Revision-Date: 2020-05-05 21:34\n"
msgid " %{start} to %{end}"
msgstr ""
@@ -182,6 +182,10 @@ msgid "%d tag"
msgid_plural "%d tags"
msgstr[0] ""
+msgid "%d unresolved thread"
+msgid_plural "%d unresolved threads"
+msgstr[0] ""
+
msgid "%d vulnerability dismissed"
msgid_plural "%d vulnerabilities dismissed"
msgstr[0] ""
@@ -335,6 +339,9 @@ msgstr ""
msgid "%{mrText}, this issue will be closed automatically."
msgstr ""
+msgid "%{namespace_name} is now read-only. You cannot: %{base_message}"
+msgstr ""
+
msgid "%{name} contained %{resultsString}"
msgstr ""
@@ -347,6 +354,10 @@ msgstr ""
msgid "%{name}'s avatar"
msgstr ""
+msgid "%{no_of_days} day"
+msgid_plural "%{no_of_days} days"
+msgstr[0] ""
+
msgid "%{number_commits_behind} commits behind %{default_branch}, %{number_commits_ahead} commits ahead"
msgstr ""
@@ -464,6 +475,9 @@ msgstr[0] ""
msgid "%{text} is available"
msgstr ""
+msgid "%{timebox_name} should belong either to a project or a group."
+msgstr ""
+
msgid "%{title} %{operator} %{threshold}"
msgstr ""
@@ -720,12 +734,18 @@ msgstr ""
msgid "<code>\"johnsmith@example.com\": \"johnsmith@example.com\"</code> will add \"By <a href=\"#\">johnsmith@example.com</a>\" to all issues and comments originally created by johnsmith@example.com. By default, the email address or username is masked to ensure the user's privacy. Use this option if you want to show the full email address."
msgstr ""
+msgid "<namespace / project>"
+msgstr ""
+
msgid "<no name set>"
msgstr ""
msgid "<no scopes selected>"
msgstr ""
+msgid "<project name>"
+msgstr ""
+
msgid "<strong>%{group_name}</strong> group members"
msgstr ""
@@ -759,6 +779,9 @@ msgstr ""
msgid "A basic page and serverless function that uses AWS Lambda, AWS API Gateway, and GitLab Pages"
msgstr ""
+msgid "A complete DevOps platform"
+msgstr ""
+
msgid "A default branch cannot be chosen for an empty project."
msgstr ""
@@ -975,6 +998,12 @@ msgstr ""
msgid "AccessTokens|reset it"
msgstr ""
+msgid "AccessibilityReport|Accessibility report artifact not found"
+msgstr ""
+
+msgid "AccessibilityReport|Failed to retrieve accessibility report"
+msgstr ""
+
msgid "AccessibilityReport|Learn More"
msgstr ""
@@ -984,6 +1013,9 @@ msgstr ""
msgid "AccessibilityReport|New"
msgstr ""
+msgid "AccessibilityReport|The accessibility scanning found an error of the following type: %{code}"
+msgstr ""
+
msgid "Account"
msgstr ""
@@ -1225,6 +1257,9 @@ msgstr ""
msgid "Added at"
msgstr ""
+msgid "Added for this merge request"
+msgstr ""
+
msgid "Added in this version"
msgstr ""
@@ -1595,6 +1630,72 @@ msgid "Alert"
msgid_plural "Alerts"
msgstr[0] ""
+msgid "Alert Details"
+msgstr ""
+
+msgid "AlertManagement|Acknowledged"
+msgstr ""
+
+msgid "AlertManagement|Alert"
+msgstr ""
+
+msgid "AlertManagement|Authorize external service"
+msgstr ""
+
+msgid "AlertManagement|Display alerts from all your monitoring tools directly within GitLab. Streamline the investigation of your alerts and the escalation of alerts to incidents."
+msgstr ""
+
+msgid "AlertManagement|End time"
+msgstr ""
+
+msgid "AlertManagement|End time:"
+msgstr ""
+
+msgid "AlertManagement|Events"
+msgstr ""
+
+msgid "AlertManagement|Events:"
+msgstr ""
+
+msgid "AlertManagement|Full Alert Details"
+msgstr ""
+
+msgid "AlertManagement|More information"
+msgstr ""
+
+msgid "AlertManagement|No alerts available to display. If you think you're seeing this message in error, refresh the page."
+msgstr ""
+
+msgid "AlertManagement|No alerts to display."
+msgstr ""
+
+msgid "AlertManagement|Overview"
+msgstr ""
+
+msgid "AlertManagement|Resolved"
+msgstr ""
+
+msgid "AlertManagement|Severity"
+msgstr ""
+
+msgid "AlertManagement|Start time"
+msgstr ""
+
+msgid "AlertManagement|Start time:"
+msgstr ""
+
+msgid "AlertManagement|Status"
+msgstr ""
+
+msgid "AlertManagement|Surface alerts in GitLab"
+msgstr ""
+
+msgid "AlertManagement|There was an error displaying the alerts. Confirm your endpoint's configuration details to ensure alerts appear."
+msgstr ""
+
+msgid "AlertManagement|Triggered"
+msgstr ""
+
msgid "AlertService|%{linkStart}Learn more%{linkEnd} about configuring this endpoint to receive alerts."
msgstr ""
@@ -1664,6 +1765,9 @@ msgstr ""
msgid "All security scans are enabled because %{linkStart}Auto DevOps%{linkEnd} is enabled on this project"
msgstr ""
+msgid "All threads resolved"
+msgstr ""
+
msgid "All users"
msgstr ""
@@ -1682,6 +1786,9 @@ msgstr ""
msgid "Allow only the selected protocols to be used for Git access."
msgstr ""
+msgid "Allow owners to manage default branch protection per group"
+msgstr ""
+
msgid "Allow owners to manually add users outside of LDAP"
msgstr ""
@@ -1790,6 +1897,9 @@ msgstr ""
msgid "An error occurred fetching the dropdown data."
msgstr ""
+msgid "An error occurred fetching the project authors."
+msgstr ""
+
msgid "An error occurred previewing the blob"
msgstr ""
@@ -1871,6 +1981,9 @@ msgstr ""
msgid "An error occurred while fetching sidebar data"
msgstr ""
+msgid "An error occurred while fetching terraform reports."
+msgstr ""
+
msgid "An error occurred while fetching the Service Desk address."
msgstr ""
@@ -2108,6 +2221,9 @@ msgstr ""
msgid "Any"
msgstr ""
+msgid "Any Author"
+msgstr ""
+
msgid "Any Label"
msgstr ""
@@ -2319,10 +2435,10 @@ msgstr ""
msgid "Are you sure that you want to unarchive this project?"
msgstr ""
-msgid "Are you sure you want to cancel creating this comment?"
+msgid "Are you sure you want to cancel editing this comment?"
msgstr ""
-msgid "Are you sure you want to cancel editing this comment?"
+msgid "Are you sure you want to close this blocked issue?"
msgstr ""
msgid "Are you sure you want to delete %{name}?"
@@ -2352,6 +2468,9 @@ msgstr ""
msgid "Are you sure you want to deploy this environment?"
msgstr ""
+msgid "Are you sure you want to discard this comment?"
+msgstr ""
+
msgid "Are you sure you want to erase this build?"
msgstr ""
@@ -2527,6 +2646,9 @@ msgstr ""
msgid "At least one approval from a code owner is required to change files matching the respective CODEOWNER rules."
msgstr ""
+msgid "At least one logging option is required to be enabled"
+msgstr ""
+
msgid "At least one of group_id or project_id must be specified"
msgstr ""
@@ -2564,6 +2686,27 @@ msgstr ""
msgid "AuditEvents|Target"
msgstr ""
+msgid "AuditLogs|(removed)"
+msgstr ""
+
+msgid "AuditLogs|Action"
+msgstr ""
+
+msgid "AuditLogs|Author"
+msgstr ""
+
+msgid "AuditLogs|Date"
+msgstr ""
+
+msgid "AuditLogs|IP Address"
+msgstr ""
+
+msgid "AuditLogs|Object"
+msgstr ""
+
+msgid "AuditLogs|Target"
+msgstr ""
+
msgid "Aug"
msgstr ""
@@ -2846,9 +2989,6 @@ msgstr ""
msgid "BambooService|You must set up automatic revision labeling and a repository trigger in Bamboo."
msgstr ""
-msgid "Batch operations"
-msgstr ""
-
msgid "BatchComments|Delete all pending comments"
msgstr ""
@@ -2978,6 +3118,9 @@ msgstr ""
msgid "Boards"
msgstr ""
+msgid "Boards and Board Lists"
+msgstr ""
+
msgid "Boards|Collapse"
msgstr ""
@@ -3197,6 +3340,9 @@ msgstr ""
msgid "Buy GitLab Enterprise Edition"
msgstr ""
+msgid "Buy more Pipeline minutes"
+msgstr ""
+
msgid "By %{user_name}"
msgstr ""
@@ -3317,6 +3463,9 @@ msgstr ""
msgid "Can't scan the code?"
msgstr ""
+msgid "Can't update snippet: %{err}"
+msgstr ""
+
msgid "Canary"
msgstr ""
@@ -3485,6 +3634,9 @@ msgstr ""
msgid "Changing group path can have unintended side effects."
msgstr ""
+msgid "Charts"
+msgstr ""
+
msgid "Charts can't be displayed as the request for data has timed out. %{documentationLink}"
msgstr ""
@@ -3521,9 +3673,6 @@ msgstr ""
msgid "ChatMessage|and [%{count} more](%{pipeline_failed_jobs_url})"
msgstr ""
-msgid "ChatMessage|failed"
-msgstr ""
-
msgid "ChatMessage|has failed"
msgstr ""
@@ -3539,9 +3688,6 @@ msgstr ""
msgid "ChatMessage|in %{project_link}"
msgstr ""
-msgid "ChatMessage|passed"
-msgstr ""
-
msgid "Check again"
msgstr ""
@@ -3992,9 +4138,6 @@ msgstr ""
msgid "Click any <strong>project name</strong> in the project list below to navigate to the project milestone."
msgstr ""
-msgid "Click here"
-msgstr ""
-
msgid "Click the <strong>Download</strong> button and wait for downloading to complete."
msgstr ""
@@ -4382,6 +4525,12 @@ msgstr ""
msgid "ClusterIntegration|Fetching zones"
msgstr ""
+msgid "ClusterIntegration|Fluentd"
+msgstr ""
+
+msgid "ClusterIntegration|Fluentd is an open source data collector, which lets you unify the data collection and consumption for a better use and understanding of data. It requires at least one of the following logs to be successfully installed."
+msgstr ""
+
msgid "ClusterIntegration|GitLab Integration"
msgstr ""
@@ -4688,6 +4837,15 @@ msgstr ""
msgid "ClusterIntegration|Request to begin uninstalling failed"
msgstr ""
+msgid "ClusterIntegration|SIEM Hostname"
+msgstr ""
+
+msgid "ClusterIntegration|SIEM Port"
+msgstr ""
+
+msgid "ClusterIntegration|SIEM Protocol"
+msgstr ""
+
msgid "ClusterIntegration|Save changes"
msgstr ""
@@ -4784,6 +4942,12 @@ msgstr ""
msgid "ClusterIntegration|Select zone to choose machine type"
msgstr ""
+msgid "ClusterIntegration|Send Cilium Logs"
+msgstr ""
+
+msgid "ClusterIntegration|Send ModSecurity Logs"
+msgstr ""
+
msgid "ClusterIntegration|Service Token"
msgstr ""
@@ -5211,18 +5375,33 @@ msgstr ""
msgid "Compliance framework (optional)"
msgstr ""
+msgid "ComplianceFramework|GDPR"
+msgstr ""
+
msgid "ComplianceFramework|GDPR - General Data Protection Regulation"
msgstr ""
+msgid "ComplianceFramework|HIPAA"
+msgstr ""
+
msgid "ComplianceFramework|HIPAA - Health Insurance Portability and Accountability Act"
msgstr ""
+msgid "ComplianceFramework|PCI-DSS"
+msgstr ""
+
msgid "ComplianceFramework|PCI-DSS - Payment Card Industry-Data Security Standard"
msgstr ""
+msgid "ComplianceFramework|SOC 2"
+msgstr ""
+
msgid "ComplianceFramework|SOC 2 - Service Organization Control 2"
msgstr ""
+msgid "ComplianceFramework|SOX"
+msgstr ""
+
msgid "ComplianceFramework|SOX - Sarbanes-Oxley"
msgstr ""
@@ -5370,9 +5549,15 @@ msgstr ""
msgid "Container repositories sync capacity"
msgstr ""
+msgid "ContainerRegistry| Please visit the %{linkStart}administration settings%{linkEnd} to enable this feature."
+msgstr ""
+
msgid "ContainerRegistry|%{imageName} tags"
msgstr ""
+msgid "ContainerRegistry|%{title} was successfully scheduled for deletion"
+msgstr ""
+
msgid "ContainerRegistry|Automatically remove extra images that aren't designed to be kept."
msgstr ""
@@ -5385,6 +5570,9 @@ msgstr ""
msgid "ContainerRegistry|Container Registry"
msgstr ""
+msgid "ContainerRegistry|Container Registry tag expiration and retention policy is disabled"
+msgstr ""
+
msgid "ContainerRegistry|Copy build command"
msgstr ""
@@ -5394,18 +5582,12 @@ msgstr ""
msgid "ContainerRegistry|Copy push command"
msgstr ""
-msgid "ContainerRegistry|Currently, the Container Registry tag expiration feature is not available for projects created before GitLab version 12.8. For updates and more information, visit Issue %{linkStart}#196124%{linkEnd}"
-msgstr ""
-
msgid "ContainerRegistry|Docker connection error"
msgstr ""
msgid "ContainerRegistry|Docker tag expiration policy is %{toggleStatus}"
msgstr ""
-msgid "ContainerRegistry|Docker tags with names matching this regex pattern will expire:"
-msgstr ""
-
msgid "ContainerRegistry|Edit Settings"
msgstr ""
@@ -5427,9 +5609,6 @@ msgstr ""
msgid "ContainerRegistry|Image ID"
msgstr ""
-msgid "ContainerRegistry|Image deleted successfully"
-msgstr ""
-
msgid "ContainerRegistry|Keep and protect the images that matter most."
msgstr ""
@@ -5445,12 +5624,24 @@ msgstr ""
msgid "ContainerRegistry|Number of tags to retain:"
msgstr ""
+msgid "ContainerRegistry|Please contact your administrator."
+msgstr ""
+
msgid "ContainerRegistry|Push an image"
msgstr ""
msgid "ContainerRegistry|Quick Start"
msgstr ""
+msgid "ContainerRegistry|Regular expressions such as %{codeStart}.*-test%{codeEnd} or %{codeStart}dev-.*%{codeEnd} are supported"
+msgstr ""
+
+msgid "ContainerRegistry|Regular expressions such as %{codeStart}.*-test%{codeEnd} or %{codeStart}dev-.*%{codeEnd} are supported. To select all tags, use %{codeStart}.*%{codeEnd}"
+msgstr ""
+
+msgid "ContainerRegistry|Remember to run %{docLinkStart}garbage collection%{docLinkEnd} to remove the stale data from storage."
+msgstr ""
+
msgid "ContainerRegistry|Remove repository"
msgstr ""
@@ -5464,22 +5655,22 @@ msgstr[0] ""
msgid "ContainerRegistry|Retention policy has been Enabled"
msgstr ""
-msgid "ContainerRegistry|Something went wrong while deleting the image."
+msgid "ContainerRegistry|Something went wrong while fetching the expiration policy."
msgstr ""
-msgid "ContainerRegistry|Something went wrong while deleting the tag."
+msgid "ContainerRegistry|Something went wrong while fetching the repository list."
msgstr ""
-msgid "ContainerRegistry|Something went wrong while deleting the tags."
+msgid "ContainerRegistry|Something went wrong while fetching the tags list."
msgstr ""
-msgid "ContainerRegistry|Something went wrong while fetching the expiration policy."
+msgid "ContainerRegistry|Something went wrong while marking the tag for deletion."
msgstr ""
-msgid "ContainerRegistry|Something went wrong while fetching the packages list."
+msgid "ContainerRegistry|Something went wrong while marking the tags for deletion."
msgstr ""
-msgid "ContainerRegistry|Something went wrong while fetching the tags list."
+msgid "ContainerRegistry|Something went wrong while scheduling %{title} for deletion. Please try again."
msgstr ""
msgid "ContainerRegistry|Something went wrong while updating the expiration policy."
@@ -5488,16 +5679,25 @@ msgstr ""
msgid "ContainerRegistry|Tag"
msgstr ""
-msgid "ContainerRegistry|Tag deleted successfully"
-msgstr ""
-
msgid "ContainerRegistry|Tag expiration policy"
msgstr ""
msgid "ContainerRegistry|Tag expiration policy is designed to:"
msgstr ""
-msgid "ContainerRegistry|Tags deleted successfully"
+msgid "ContainerRegistry|Tag successfully marked for deletion."
+msgstr ""
+
+msgid "ContainerRegistry|Tags successfully marked for deletion."
+msgstr ""
+
+msgid "ContainerRegistry|Tags with names matching this regex pattern will %{italicStart}be preserved:%{italicEnd}"
+msgstr ""
+
+msgid "ContainerRegistry|Tags with names matching this regex pattern will %{italicStart}expire:%{italicEnd}"
+msgstr ""
+
+msgid "ContainerRegistry|The Container Registry tag expiration and retention policies for this project have not been enabled."
msgstr ""
msgid "ContainerRegistry|The last tag related to this image was recently removed. This empty image and any associated data will be automatically removed as part of the regular Garbage Collection process. If you have any questions, contact your administrator."
@@ -5518,16 +5718,16 @@ msgstr ""
msgid "ContainerRegistry|There are no container images stored for this project"
msgstr ""
-msgid "ContainerRegistry|This Registry contains deleted image tag data. Remember to run %{docLinkStart}garbage collection%{docLinkEnd} to remove the stale data from storage."
+msgid "ContainerRegistry|There was an error during the deletion of this image repository, please try again."
msgstr ""
msgid "ContainerRegistry|This image has no active tags"
msgstr ""
-msgid "ContainerRegistry|We are having trouble connecting to Docker, which could be due to an issue with your project name or path. %{docLinkStart}More Information%{docLinkEnd}"
+msgid "ContainerRegistry|This image repository is scheduled for deletion"
msgstr ""
-msgid "ContainerRegistry|Wildcards such as %{codeStart}.*-stable%{codeEnd} or %{codeStart}production/.*%{codeEnd} are supported. To select all tags, use %{codeStart}.*%{codeEnd}"
+msgid "ContainerRegistry|We are having trouble connecting to Docker, which could be due to an issue with your project name or path. %{docLinkStart}More Information%{docLinkEnd}"
msgstr ""
msgid "ContainerRegistry|With the Container Registry, every project can have its own space to store its Docker images. %{docLinkStart}More Information%{docLinkEnd}"
@@ -5782,7 +5982,7 @@ msgstr ""
msgid "Could not delete chat nickname %{chat_name}."
msgstr ""
-msgid "Could not find design"
+msgid "Could not find design."
msgstr ""
msgid "Could not remove the trigger."
@@ -5797,6 +5997,9 @@ msgstr ""
msgid "Could not revoke personal access token %{personal_access_token_name}."
msgstr ""
+msgid "Could not revoke project access token %{project_access_token_name}."
+msgstr ""
+
msgid "Could not save group ID"
msgstr ""
@@ -5854,6 +6057,9 @@ msgstr ""
msgid "Create a new branch"
msgstr ""
+msgid "Create a new deploy key for this project"
+msgstr ""
+
msgid "Create a new file as there are no files yet. Afterwards, you'll be able to commit your changes."
msgstr ""
@@ -5959,6 +6165,9 @@ msgstr ""
msgid "Create requirement"
msgstr ""
+msgid "Create snippet"
+msgstr ""
+
msgid "Create wildcard: %{searchTerm}"
msgstr ""
@@ -5980,6 +6189,9 @@ msgstr ""
msgid "Created"
msgstr ""
+msgid "Created %{timestamp}"
+msgstr ""
+
msgid "Created At"
msgstr ""
@@ -6335,6 +6547,9 @@ msgstr ""
msgid "CycleAnalytics|stage dropdown"
msgstr ""
+msgid "DAG"
+msgstr ""
+
msgid "DNS"
msgstr ""
@@ -6695,6 +6910,9 @@ msgstr ""
msgid "Deploy key was successfully updated."
msgstr ""
+msgid "Deploy keys allow read-only or read-write (if enabled) access to your repository. Deploy keys can be used for CI, staging or production servers. You can create a deploy key or add an existing one."
+msgstr ""
+
msgid "Deploy progress not found. To see pods, ensure your environment matches %{linkStart}deploy board criteria%{linkEnd}."
msgstr ""
@@ -6848,6 +7066,9 @@ msgstr ""
msgid "Deploying to"
msgstr ""
+msgid "Deployment Frequency"
+msgstr ""
+
msgid "Deployment|API"
msgstr ""
@@ -7365,9 +7586,6 @@ msgstr ""
msgid "Edit environment"
msgstr ""
-msgid "Edit epic description"
-msgstr ""
-
msgid "Edit file"
msgstr ""
@@ -7989,9 +8207,6 @@ msgstr ""
msgid "Epics"
msgstr ""
-msgid "Epics (Ultimate / Gold license only)"
-msgstr ""
-
msgid "Epics Roadmap"
msgstr ""
@@ -8001,6 +8216,9 @@ msgstr ""
msgid "Epics let you manage your portfolio of projects more efficiently and with less effort"
msgstr ""
+msgid "Epics, Issues, and Merge Requests"
+msgstr ""
+
msgid "Epics|Add an epic"
msgstr ""
@@ -8052,6 +8270,9 @@ msgstr ""
msgid "Epics|Something went wrong while fetching group epics."
msgstr ""
+msgid "Epics|Something went wrong while moving item."
+msgstr ""
+
msgid "Epics|Something went wrong while ordering item."
msgstr ""
@@ -8106,6 +8327,9 @@ msgstr ""
msgid "Error fetching network graph."
msgstr ""
+msgid "Error fetching payload data."
+msgstr ""
+
msgid "Error fetching projects"
msgstr ""
@@ -8115,9 +8339,6 @@ msgstr ""
msgid "Error fetching the dependency list. Please check your network connection and try again."
msgstr ""
-msgid "Error fetching usage ping data."
-msgstr ""
-
msgid "Error loading branch data. Please try again."
msgstr ""
@@ -8259,6 +8480,9 @@ msgstr ""
msgid "Errors"
msgstr ""
+msgid "Errors:"
+msgstr ""
+
msgid "Estimated"
msgstr ""
@@ -8463,12 +8687,18 @@ msgstr ""
msgid "Export as CSV"
msgstr ""
+msgid "Export group"
+msgstr ""
+
msgid "Export issues"
msgstr ""
msgid "Export project"
msgstr ""
+msgid "Export this group with all related data to a new GitLab instance. Once complete, you can import the data file from the \"New Group\" page."
+msgstr ""
+
msgid "Export this project with all its related data in order to move your project to a new GitLab instance. Once the export is finished, you can import the file from the \"New Project\" page."
msgstr ""
@@ -8538,9 +8768,6 @@ msgstr ""
msgid "Failed Jobs"
msgstr ""
-msgid "Failed create wiki"
-msgstr ""
-
msgid "Failed to add a Zoom meeting"
msgstr ""
@@ -8580,6 +8807,9 @@ msgstr ""
msgid "Failed to create resources"
msgstr ""
+msgid "Failed to create wiki"
+msgstr ""
+
msgid "Failed to delete board. Please try again."
msgstr ""
@@ -8589,7 +8819,7 @@ msgstr ""
msgid "Failed to enqueue the rebase operation, possibly due to a long-lived transaction. Try again later."
msgstr ""
-msgid "Failed to find import label for jira import."
+msgid "Failed to find import label for Jira import."
msgstr ""
msgid "Failed to get ref."
@@ -8613,6 +8843,12 @@ msgstr ""
msgid "Failed to load groups & users."
msgstr ""
+msgid "Failed to load labels. Please try again."
+msgstr ""
+
+msgid "Failed to load milestones. Please try again."
+msgstr ""
+
msgid "Failed to load related branches"
msgstr ""
@@ -8988,7 +9224,10 @@ msgstr ""
msgid "Filter by milestone name"
msgstr ""
-msgid "Filter by name..."
+msgid "Filter by name"
+msgstr ""
+
+msgid "Filter by status"
msgstr ""
msgid "Filter by two-factor authentication"
@@ -9000,12 +9239,18 @@ msgstr ""
msgid "Filter projects"
msgstr ""
+msgid "Filter results"
+msgstr ""
+
msgid "Filter results by group"
msgstr ""
msgid "Filter results by project"
msgstr ""
+msgid "Filter results..."
+msgstr ""
+
msgid "Filter your projects by name"
msgstr ""
@@ -9294,10 +9539,10 @@ msgstr ""
msgid "GeoNodes|Checksummed"
msgstr ""
-msgid "GeoNodes|Container repositories"
+msgid "GeoNodes|Consult Geo troubleshooting information"
msgstr ""
-msgid "GeoNodes|Data is out of date from %{timeago}"
+msgid "GeoNodes|Container repositories"
msgstr ""
msgid "GeoNodes|Data replication lag"
@@ -9339,6 +9584,9 @@ msgstr ""
msgid "GeoNodes|Last event ID seen from primary"
msgstr ""
+msgid "GeoNodes|Learn more about Geo node statuses"
+msgstr ""
+
msgid "GeoNodes|Loading nodes"
msgstr ""
@@ -9354,6 +9602,9 @@ msgstr ""
msgid "GeoNodes|Node was successfully removed."
msgstr ""
+msgid "GeoNodes|Node's status was updated %{timeAgo}."
+msgstr ""
+
msgid "GeoNodes|Not checksummed"
msgstr ""
@@ -9384,7 +9635,10 @@ msgstr ""
msgid "GeoNodes|Repository verification progress"
msgstr ""
-msgid "GeoNodes|Selective"
+msgid "GeoNodes|Selective (%{syncLabel})"
+msgstr ""
+
+msgid "GeoNodes|Selective synchronization"
msgstr ""
msgid "GeoNodes|Something went wrong while changing node status"
@@ -9411,6 +9665,9 @@ msgstr ""
msgid "GeoNodes|Unverified"
msgstr ""
+msgid "GeoNodes|Updated %{timeAgo}"
+msgstr ""
+
msgid "GeoNodes|Used slots"
msgstr ""
@@ -9450,16 +9707,16 @@ msgstr ""
msgid "Geo|All"
msgstr ""
-msgid "Geo|All projects"
+msgid "Geo|All %{replicable_type}"
msgstr ""
-msgid "Geo|All projects are being scheduled for re-sync"
+msgid "Geo|All projects"
msgstr ""
-msgid "Geo|All projects are being scheduled for re-verify"
+msgid "Geo|All projects are being scheduled for resync"
msgstr ""
-msgid "Geo|Batch operations"
+msgid "Geo|All projects are being scheduled for reverify"
msgstr ""
msgid "Geo|Could not remove tracking entry for an existing project."
@@ -9471,9 +9728,15 @@ msgstr ""
msgid "Geo|Failed"
msgstr ""
+msgid "Geo|Filter by status"
+msgstr ""
+
msgid "Geo|Geo Status"
msgstr ""
+msgid "Geo|In progress"
+msgstr ""
+
msgid "Geo|In sync"
msgstr ""
@@ -9498,9 +9761,6 @@ msgstr ""
msgid "Geo|Not synced yet"
msgstr ""
-msgid "Geo|Pending"
-msgstr ""
-
msgid "Geo|Pending synchronization"
msgstr ""
@@ -9537,7 +9797,7 @@ msgstr ""
msgid "Geo|Resync"
msgstr ""
-msgid "Geo|Resync all projects"
+msgid "Geo|Resync all"
msgstr ""
msgid "Geo|Retry count"
@@ -9546,7 +9806,7 @@ msgstr ""
msgid "Geo|Reverify"
msgstr ""
-msgid "Geo|Reverify all projects"
+msgid "Geo|Reverify all"
msgstr ""
msgid "Geo|Status"
@@ -9888,9 +10148,6 @@ msgstr ""
msgid "Go to file"
msgstr ""
-msgid "Go to file (MRs only)"
-msgstr ""
-
msgid "Go to file permalink (while viewing a file)"
msgstr ""
@@ -10335,6 +10592,9 @@ msgstr ""
msgid "GroupSettings|Disable group mentions"
msgstr ""
+msgid "GroupSettings|Export group"
+msgstr ""
+
msgid "GroupSettings|If the parent group's visibility is lower than the group current visibility, visibility levels for subgroups and projects will be changed to match the new parent group's visibility."
msgstr ""
@@ -10413,6 +10673,9 @@ msgstr ""
msgid "Groups (%{count})"
msgstr ""
+msgid "Groups (%{groups})"
+msgstr ""
+
msgid "Groups can also be nested by creating %{subgroup_docs_link_start}subgroups%{subgroup_docs_link_end}."
msgstr ""
@@ -10676,15 +10939,12 @@ msgstr ""
msgid "ID:"
msgstr ""
-msgid "IDE|Allow live previews of JavaScript projects in the Web IDE using CodeSandbox client side evaluation."
+msgid "IDE|Allow live previews of JavaScript projects in the Web IDE using CodeSandbox Live Preview."
msgstr ""
msgid "IDE|Back"
msgstr ""
-msgid "IDE|Client side evaluation"
-msgstr ""
-
msgid "IDE|Commit"
msgstr ""
@@ -10748,6 +11008,9 @@ msgstr ""
msgid "If any job surpasses this timeout threshold, it will be marked as failed. Human readable time input language is accepted like \"1 hour\". Values without specification represent seconds."
msgstr ""
+msgid "If blank, set allowable lifetime to %{instance_level_policy_in_words}, as defined by the instance admin. Once set, existing tokens for users in this group may be revoked."
+msgstr ""
+
msgid "If checked, group owners can manage LDAP group links and LDAP member overrides"
msgstr ""
@@ -10787,6 +11050,9 @@ msgstr ""
msgid "If you lose your recovery codes you can generate new ones, invalidating all previous codes."
msgstr ""
+msgid "If you reach 100%% storage capacity, you will not be able to: %{base_message}"
+msgstr ""
+
msgid "If your HTTP repository is not publicly accessible, add your credentials."
msgstr ""
@@ -10898,6 +11164,9 @@ msgstr ""
msgid "Import repository"
msgstr ""
+msgid "Import started by: %{importInitiator}"
+msgstr ""
+
msgid "Import tasks"
msgstr ""
@@ -10964,6 +11233,9 @@ msgstr ""
msgid "In order to tailor your experience with GitLab we<br>would like to know a bit more about you."
msgstr ""
+msgid "In progress"
+msgstr ""
+
msgid "In the next step, you'll be able to select the projects you want to import."
msgstr ""
@@ -11310,15 +11582,15 @@ msgstr ""
msgid "Issues"
msgstr ""
-msgid "Issues / Merge Requests"
-msgstr ""
-
msgid "Issues Analytics"
msgstr ""
msgid "Issues Rate Limits"
msgstr ""
+msgid "Issues and Merge Requests"
+msgstr ""
+
msgid "Issues can be bugs, tasks or ideas to be discussed. Also, issues are searchable and filterable."
msgstr ""
@@ -11340,10 +11612,10 @@ msgstr ""
msgid "IssuesAnalytics|Avg/Month:"
msgstr ""
-msgid "IssuesAnalytics|Issues created"
+msgid "IssuesAnalytics|Issues opened"
msgstr ""
-msgid "IssuesAnalytics|Issues created per month"
+msgid "IssuesAnalytics|Issues opened per month"
msgstr ""
msgid "IssuesAnalytics|Last 12 months"
@@ -11397,6 +11669,9 @@ msgstr ""
msgid "Jira integration not configured."
msgstr ""
+msgid "Jira project: %{importProject}"
+msgstr ""
+
msgid "JiraService|Events for %{noteable_model_name} are disabled."
msgstr ""
@@ -11785,6 +12060,9 @@ msgstr ""
msgid "Lead"
msgstr ""
+msgid "Lead Time"
+msgstr ""
+
msgid "Learn GitLab"
msgstr ""
@@ -11797,6 +12075,9 @@ msgstr ""
msgid "Learn how to %{no_packages_link_start}publish and share your packages%{no_packages_link_end} with GitLab."
msgstr ""
+msgid "Learn how to enable synchronization"
+msgstr ""
+
msgid "Learn more"
msgstr ""
@@ -12123,9 +12404,6 @@ msgstr ""
msgid "Live preview"
msgstr ""
-msgid "Load more vulnerabilities"
-msgstr ""
-
msgid "Loading"
msgstr ""
@@ -12276,9 +12554,6 @@ msgstr ""
msgid "Manage"
msgstr ""
-msgid "Manage Git repositories with fine-grained access controls that keep your code secure. Perform code reviews and enhance collaboration with merge requests. Each project can also have an issue tracker and a wiki."
-msgstr ""
-
msgid "Manage Web IDE features"
msgstr ""
@@ -12309,6 +12584,9 @@ msgstr ""
msgid "Manage your license"
msgstr ""
+msgid "Managed Account"
+msgstr ""
+
msgid "Manifest"
msgstr ""
@@ -12573,6 +12851,9 @@ msgstr ""
msgid "Merge Requests in Review"
msgstr ""
+msgid "Merge automatically (%{strategy})"
+msgstr ""
+
msgid "Merge commit message"
msgstr ""
@@ -12726,6 +13007,12 @@ msgstr ""
msgid "Merged branches are being deleted. This can take some time depending on the number of branches. Please refresh the page to see changes."
msgstr ""
+msgid "Merged this merge request."
+msgstr ""
+
+msgid "Merges this merge request immediately."
+msgstr ""
+
msgid "Merges this merge request when the pipeline succeeds."
msgstr ""
@@ -12832,12 +13119,15 @@ msgid "Metrics|Edit metric"
msgid_plural "Metrics|Edit metrics"
msgstr[0] ""
-msgid "Metrics|Environment"
+msgid "Metrics|Expand panel"
msgstr ""
msgid "Metrics|For grouping similar metrics"
msgstr ""
+msgid "Metrics|Go back (Esc)"
+msgstr ""
+
msgid "Metrics|Invalid time range, please verify."
msgstr ""
@@ -12868,9 +13158,6 @@ msgstr ""
msgid "Metrics|Refresh dashboard"
msgstr ""
-msgid "Metrics|Show last"
-msgstr ""
-
msgid "Metrics|There was an error creating the dashboard."
msgstr ""
@@ -12949,6 +13236,9 @@ msgstr ""
msgid "Microsoft Azure"
msgstr ""
+msgid "Middleman project with Static Site Editor support"
+msgstr ""
+
msgid "Migrated %{success_count}/%{total_count} files."
msgstr ""
@@ -13392,10 +13682,10 @@ msgstr ""
msgid "Next"
msgstr ""
-msgid "Next file in diff (MRs only)"
+msgid "Next file in diff"
msgstr ""
-msgid "Next unresolved discussion (MRs only)"
+msgid "Next unresolved discussion"
msgstr ""
msgid "Nickname"
@@ -13455,6 +13745,9 @@ msgstr ""
msgid "No changes between %{ref_start}%{source_branch}%{ref_end} and %{ref_start}%{target_branch}%{ref_end}"
msgstr ""
+msgid "No child epics match applied filters"
+msgstr ""
+
msgid "No connection could be made to a Gitaly Server, please check your logs!"
msgstr ""
@@ -13593,15 +13886,6 @@ msgstr ""
msgid "No thanks, don't show this again"
msgstr ""
-msgid "No vulnerabilities found for this group"
-msgstr ""
-
-msgid "No vulnerabilities found for this pipeline"
-msgstr ""
-
-msgid "No vulnerabilities found for this project"
-msgstr ""
-
msgid "No vulnerabilities present"
msgstr ""
@@ -13725,6 +14009,9 @@ msgstr ""
msgid "Nothing to preview."
msgstr ""
+msgid "Nothing to synchronize"
+msgstr ""
+
msgid "Notification events"
msgstr ""
@@ -13896,6 +14183,9 @@ msgstr ""
msgid "Once removed, the fork relationship cannot be restored and you will no longer be able to send merge requests to the source."
msgstr ""
+msgid "Once the exported file is ready you can download it from this page."
+msgstr ""
+
msgid "Once the exported file is ready, you will receive a notification email with a download link, or you can download it from this page."
msgstr ""
@@ -13996,9 +14286,6 @@ msgstr ""
msgid "Open sidebar"
msgstr ""
-msgid "Open source software to collaborate on code"
-msgstr ""
-
msgid "Open: %{openIssuesCount}"
msgstr ""
@@ -14101,6 +14388,9 @@ msgstr ""
msgid "OutdatedBrowser|You can provide feedback %{feedback_link_start}on this issue%{feedback_link_end} or via your usual support channels."
msgstr ""
+msgid "Overridden"
+msgstr ""
+
msgid "Overview"
msgstr ""
@@ -14152,6 +14442,9 @@ msgstr ""
msgid "PackageRegistry|Conan Command"
msgstr ""
+msgid "PackageRegistry|Copy .pypirc content"
+msgstr ""
+
msgid "PackageRegistry|Copy Conan Command"
msgstr ""
@@ -14173,6 +14466,9 @@ msgstr ""
msgid "PackageRegistry|Copy NuGet Setup Command"
msgstr ""
+msgid "PackageRegistry|Copy Pip command"
+msgstr ""
+
msgid "PackageRegistry|Copy and paste this inside your %{codeStart}pom.xml%{codeEnd} %{codeStart}dependencies%{codeEnd} block."
msgstr ""
@@ -14206,6 +14502,12 @@ msgstr ""
msgid "PackageRegistry|For more information on the NuGet registry, %{linkStart}see the documentation%{linkEnd}."
msgstr ""
+msgid "PackageRegistry|For more information on the PyPi registry, %{linkStart}see the documentation%{linkEnd}."
+msgstr ""
+
+msgid "PackageRegistry|If you haven't already done so, you will need to add the below to your %{codeStart}.pypirc%{codeEnd} file."
+msgstr ""
+
msgid "PackageRegistry|If you haven't already done so, you will need to add the below to your %{codeStart}pom.xml%{codeEnd} file."
msgstr ""
@@ -14215,6 +14517,9 @@ msgstr ""
msgid "PackageRegistry|Learn how to %{noPackagesLinkStart}publish and share your packages%{noPackagesLinkEnd} with GitLab."
msgstr ""
+msgid "PackageRegistry|Manually Published"
+msgstr ""
+
msgid "PackageRegistry|Maven"
msgstr ""
@@ -14233,12 +14538,18 @@ msgstr ""
msgid "PackageRegistry|NuGet Command"
msgstr ""
+msgid "PackageRegistry|Pip Command"
+msgstr ""
+
msgid "PackageRegistry|Pipeline %{linkStart}%{linkEnd} triggered %{timestamp} by %{author}"
msgstr ""
msgid "PackageRegistry|Published to the repository at %{timestamp}"
msgstr ""
+msgid "PackageRegistry|PyPi"
+msgstr ""
+
msgid "PackageRegistry|Registry Setup"
msgstr ""
@@ -14275,6 +14586,9 @@ msgstr ""
msgid "PackageRegistry|npm"
msgstr ""
+msgid "PackageRegistry|published by %{author}"
+msgstr ""
+
msgid "PackageRegistry|yarn"
msgstr ""
@@ -14290,6 +14604,9 @@ msgstr ""
msgid "PackageType|NuGet"
msgstr ""
+msgid "PackageType|PyPi"
+msgstr ""
+
msgid "Packages"
msgstr ""
@@ -14383,6 +14700,9 @@ msgstr ""
msgid "Past due"
msgstr ""
+msgid "Paste a machine public key here. Read more about how to generate it"
+msgstr ""
+
msgid "Paste a machine public key here. Read more about how to generate it %{link_start}here%{link_end}"
msgstr ""
@@ -14398,9 +14718,6 @@ msgstr ""
msgid "Path"
msgstr ""
-msgid "Path, transfer, remove"
-msgstr ""
-
msgid "Path:"
msgstr ""
@@ -14431,7 +14748,7 @@ msgstr ""
msgid "Percentage"
msgstr ""
-msgid "Perform advanced options such as changing path, transferring, or removing the group."
+msgid "Perform advanced options such as changing path, transferring, exporting, or removing the group."
msgstr ""
msgid "Perform common operations on GitLab project"
@@ -14512,9 +14829,6 @@ msgstr ""
msgid "Pipeline minutes quota"
msgstr ""
-msgid "Pipeline quota"
-msgstr ""
-
msgid "Pipeline subscriptions"
msgstr ""
@@ -14524,6 +14838,9 @@ msgstr ""
msgid "Pipeline: %{status}"
msgstr ""
+msgid "PipelineCharts|CI / CD Analytics"
+msgstr ""
+
msgid "PipelineCharts|Failed:"
msgstr ""
@@ -14602,15 +14919,6 @@ msgstr ""
msgid "Pipelines settings for '%{project_name}' were successfully updated."
msgstr ""
-msgid "Pipelines| to purchase more minutes."
-msgstr ""
-
-msgid "Pipelines|%{namespace_name} has exceeded its pipeline minutes quota."
-msgstr ""
-
-msgid "Pipelines|%{namespace_name} has less than %{notification_level}%% of CI minutes available."
-msgstr ""
-
msgid "Pipelines|API"
msgstr ""
@@ -14632,10 +14940,13 @@ msgstr ""
msgid "Pipelines|Get started with Pipelines"
msgstr ""
-msgid "Pipelines|Loading Pipelines"
+msgid "Pipelines|Group %{namespace_name} has %{percentage}%% or less Shared Runner Pipeline minutes remaining. Once it runs out, no new jobs or pipelines in its projects will run."
+msgstr ""
+
+msgid "Pipelines|Group %{namespace_name} has exceeded its pipeline minutes quota. Unless you buy additional pipeline minutes, no new jobs or pipelines in its projects will run."
msgstr ""
-msgid "Pipelines|Pipelines will not run anymore on shared Runners."
+msgid "Pipelines|Loading Pipelines"
msgstr ""
msgid "Pipelines|Project cache successfully reset."
@@ -14866,6 +15177,9 @@ msgstr ""
msgid "Please select"
msgstr ""
+msgid "Please select a Jira project"
+msgstr ""
+
msgid "Please select a country"
msgstr ""
@@ -14911,6 +15225,9 @@ msgstr ""
msgid "Please wait while we import the repository for you. Refresh at will."
msgstr ""
+msgid "Plugins directory is deprecated and will be removed in 14.0. Please move this file into /file_hooks directory."
+msgstr ""
+
msgid "Pod does not exist"
msgstr ""
@@ -15028,7 +15345,7 @@ msgstr ""
msgid "Prevent users from changing their profile name"
msgstr ""
-msgid "Prevent users from modifing merge request approvers list"
+msgid "Prevent users from modifying merge request approvers list"
msgstr ""
msgid "Prevent users from performing write operations on GitLab while performing maintenance."
@@ -15049,10 +15366,10 @@ msgstr ""
msgid "Previous Artifacts"
msgstr ""
-msgid "Previous file in diff (MRs only)"
+msgid "Previous file in diff"
msgstr ""
-msgid "Previous unresolved discussion (MRs only)"
+msgid "Previous unresolved discussion"
msgstr ""
msgid "Primary"
@@ -15085,6 +15402,9 @@ msgstr ""
msgid "Private profile"
msgstr ""
+msgid "Private projects Minutes cost factor"
+msgstr ""
+
msgid "Private projects can be created in your personal namespace with:"
msgstr ""
@@ -15913,6 +16233,9 @@ msgstr ""
msgid "ProjectSettings|With GitLab Pages you can host your static websites on GitLab"
msgstr ""
+msgid "ProjectSettings|With Metrics Dashboard you can visualize this project performance metrics"
+msgstr ""
+
msgid "ProjectTemplates|.NET Core"
msgstr ""
@@ -15976,6 +16299,9 @@ msgstr ""
msgid "ProjectTemplates|Spring"
msgstr ""
+msgid "ProjectTemplates|Static Site Editor/Middleman"
+msgstr ""
+
msgid "ProjectTemplates|iOS (Swift)"
msgstr ""
@@ -16195,13 +16521,13 @@ msgstr ""
msgid "PrometheusService|Prometheus is being automatically managed on your clusters"
msgstr ""
-msgid "PrometheusService|These metrics will only be monitored after your first deployment to an environment"
+msgid "PrometheusService|Select the Active checkbox to override the Auto Configuration with custom settings. If unchecked, Auto Configuration settings are used."
msgstr ""
-msgid "PrometheusService|Time-series monitoring service"
+msgid "PrometheusService|These metrics will only be monitored after your first deployment to an environment"
msgstr ""
-msgid "PrometheusService|To enable manual configuration, uninstall Prometheus from your clusters"
+msgid "PrometheusService|Time-series monitoring service"
msgstr ""
msgid "PrometheusService|To enable the installation of Prometheus on your clusters, deactivate the manual configuration below"
@@ -16387,6 +16713,9 @@ msgstr ""
msgid "Protip:"
msgstr ""
+msgid "Protocol"
+msgstr ""
+
msgid "Provider"
msgstr ""
@@ -16411,6 +16740,9 @@ msgstr ""
msgid "Public pipelines"
msgstr ""
+msgid "Public projects Minutes cost factor"
+msgstr ""
+
msgid "Pull"
msgstr ""
@@ -16610,6 +16942,9 @@ msgid "Refreshing in a second to show the updated status..."
msgid_plural "Refreshing in %d seconds to show the updated status..."
msgstr[0] ""
+msgid "Regenerate export"
+msgstr ""
+
msgid "Regenerate instance ID"
msgstr ""
@@ -16986,9 +17321,28 @@ msgstr ""
msgid "Reports|%{combinedString} and %{resolvedString}"
msgstr ""
+msgid "Reports|Accessibility scanning detected %d issue for the source branch only"
+msgid_plural "Reports|Accessibility scanning detected %d issues for the source branch only"
+msgstr[0] ""
+
+msgid "Reports|Accessibility scanning detected no issues for the source branch only"
+msgstr ""
+
+msgid "Reports|Accessibility scanning failed loading results"
+msgstr ""
+
+msgid "Reports|Accessibility scanning results are being parsed"
+msgstr ""
+
msgid "Reports|Actions"
msgstr ""
+msgid "Reports|An error occured while loading report"
+msgstr ""
+
+msgid "Reports|An error occurred while loading %{name} results"
+msgstr ""
+
msgid "Reports|Class"
msgstr ""
@@ -17094,7 +17448,7 @@ msgstr ""
msgid "Requested %{time_ago}"
msgstr ""
-msgid "Requested design version does not exist"
+msgid "Requested design version does not exist."
msgstr ""
msgid "Requested states are invalid"
@@ -17276,6 +17630,9 @@ msgstr ""
msgid "Resync"
msgstr ""
+msgid "Resync all"
+msgstr ""
+
msgid "Resync all %{replicableType}"
msgstr ""
@@ -17343,6 +17700,9 @@ msgstr ""
msgid "Revoked personal access token %{personal_access_token_name}!"
msgstr ""
+msgid "Revoked project access token %{project_access_token_name}!"
+msgstr ""
+
msgid "RightSidebar|adding a"
msgstr ""
@@ -17391,6 +17751,9 @@ msgstr ""
msgid "Runner token"
msgstr ""
+msgid "Runner tokens"
+msgstr ""
+
msgid "Runner was not updated."
msgstr ""
@@ -17445,6 +17808,9 @@ msgstr ""
msgid "SAML SSO for %{group_name}"
msgstr ""
+msgid "SAML discovery tokens"
+msgstr ""
+
msgid "SAML for %{group_name}"
msgstr ""
@@ -17529,12 +17895,18 @@ msgstr ""
msgid "Scheduled"
msgstr ""
+msgid "Scheduled to merge this merge request (%{strategy})."
+msgstr ""
+
msgid "Scheduled to merge this merge request when the pipeline succeeds."
msgstr ""
msgid "Schedules"
msgstr ""
+msgid "Schedules to merge this merge request (%{strategy})."
+msgstr ""
+
msgid "Scheduling"
msgstr ""
@@ -17592,6 +17964,9 @@ msgstr ""
msgid "Search branches and tags"
msgstr ""
+msgid "Search by author"
+msgstr ""
+
msgid "Search files"
msgstr ""
@@ -17729,10 +18104,6 @@ msgid "SearchResults|snippet"
msgid_plural "SearchResults|snippets"
msgstr[0] ""
-msgid "SearchResults|snippet result"
-msgid_plural "SearchResults|snippet results"
-msgstr[0] ""
-
msgid "SearchResults|user"
msgid_plural "SearchResults|users"
msgstr[0] ""
@@ -17741,6 +18112,9 @@ msgid "SearchResults|wiki result"
msgid_plural "SearchResults|wiki results"
msgstr[0] ""
+msgid "Searching by both author and message is currently not supported."
+msgstr ""
+
msgid "Seat Link"
msgstr ""
@@ -17768,184 +18142,232 @@ msgstr ""
msgid "Security Dashboard"
msgstr ""
-msgid "Security Dashboard|Error fetching the vulnerability counts. Please check your network connection and try again."
+msgid "Security configuration help link"
+msgstr ""
+
+msgid "Security dashboard"
msgstr ""
-msgid "Security Dashboard|Error fetching the vulnerability list. Please check your network connection and try again."
+msgid "Security report is out of date. Please update your branch with the latest changes from the target branch (%{targetBranchName})"
msgstr ""
-msgid "Security Dashboard|Issue Created"
+msgid "Security report is out of date. Run %{newPipelineLinkStart}a new pipeline%{newPipelineLinkEnd} for the target branch (%{targetBranchName})"
msgstr ""
-msgid "Security Reports|Comment added to '%{vulnerabilityName}'"
+msgid "SecurityConfiguration|Configured"
msgstr ""
-msgid "Security Reports|Comment deleted on '%{vulnerabilityName}'"
+msgid "SecurityConfiguration|Feature"
msgstr ""
-msgid "Security Reports|Comment edited on '%{vulnerabilityName}'"
+msgid "SecurityConfiguration|Feature documentation for %{featureName}"
msgstr ""
-msgid "Security Reports|Create issue"
+msgid "SecurityConfiguration|Not yet configured"
msgstr ""
-msgid "Security Reports|Dismiss vulnerability"
+msgid "SecurityConfiguration|Secure features"
msgstr ""
-msgid "Security Reports|Dismissed '%{vulnerabilityName}'"
+msgid "SecurityConfiguration|Status"
msgstr ""
-msgid "Security Reports|Dismissed '%{vulnerabilityName}'. Turn off the hide dismissed toggle to view."
+msgid "SecurityReports|%{firstProject} and %{secondProject}"
msgstr ""
-msgid "Security Reports|Either you don't have permission to view this dashboard or the dashboard has not been setup. Please check your permission settings with your administrator or check your dashboard configurations to proceed."
+msgid "SecurityReports|%{firstProject}, %{secondProject}, and %{rest}"
msgstr ""
-msgid "Security Reports|Learn more about setting up your dashboard"
+msgid "SecurityReports|Add a project to your dashboard"
msgstr ""
-msgid "Security Reports|More info"
+msgid "SecurityReports|Add or remove projects from your dashboard"
msgstr ""
-msgid "Security Reports|Oops, something doesn't seem right."
+msgid "SecurityReports|Add projects"
msgstr ""
-msgid "Security Reports|Security reports can only be accessed by authorized users."
+msgid "SecurityReports|Comment added to '%{vulnerabilityName}'"
msgstr ""
-msgid "Security Reports|There was an error adding the comment."
+msgid "SecurityReports|Comment deleted on '%{vulnerabilityName}'"
msgstr ""
-msgid "Security Reports|There was an error creating the issue."
+msgid "SecurityReports|Comment edited on '%{vulnerabilityName}'"
msgstr ""
-msgid "Security Reports|There was an error creating the merge request."
+msgid "SecurityReports|Create issue"
msgstr ""
-msgid "Security Reports|There was an error deleting the comment."
+msgid "SecurityReports|Dismiss Selected"
msgstr ""
-msgid "Security Reports|There was an error dismissing the vulnerabilities."
+msgid "SecurityReports|Dismiss vulnerability"
msgstr ""
-msgid "Security Reports|There was an error dismissing the vulnerability."
+msgid "SecurityReports|Dismissed '%{vulnerabilityName}'"
msgstr ""
-msgid "Security Reports|There was an error reverting the dismissal."
+msgid "SecurityReports|Dismissed '%{vulnerabilityName}'. Turn off the hide dismissed toggle to view."
msgstr ""
-msgid "Security Reports|There was an error reverting this dismissal."
+msgid "SecurityReports|Each vulnerability now has a unique page that can be directly linked to, shared, referenced, and tracked as the single source of truth. Vulnerability occurrences also persist across scanner runs, which improves tracking and visibility and reduces duplicates between scans."
msgstr ""
-msgid "Security Reports|Undo dismiss"
+msgid "SecurityReports|Edit dashboard"
msgstr ""
-msgid "Security Reports|You do not have sufficient permissions to access this report"
+msgid "SecurityReports|Either you don't have permission to view this dashboard or the dashboard has not been setup. Please check your permission settings with your administrator or check your dashboard configurations to proceed."
msgstr ""
-msgid "Security Reports|You must sign in as an authorized user to see this report"
+msgid "SecurityReports|Error fetching the vulnerability counts. Please check your network connection and try again."
msgstr ""
-msgid "Security configuration help link"
+msgid "SecurityReports|Error fetching the vulnerability list. Please check your network connection and try again."
msgstr ""
-msgid "Security dashboard"
+msgid "SecurityReports|False positive"
msgstr ""
-msgid "Security report is out of date. Please update your branch with the latest changes from the target branch (%{targetBranchName})"
+msgid "SecurityReports|Hide dismissed"
msgstr ""
-msgid "Security report is out of date. Run %{newPipelineLinkStart}a new pipeline%{newPipelineLinkEnd} for the target branch (%{targetBranchName})"
+msgid "SecurityReports|Introducing standalone vulnerabilities"
msgstr ""
-msgid "SecurityConfiguration|Configured"
+msgid "SecurityReports|Issue Created"
msgstr ""
-msgid "SecurityConfiguration|Feature"
+msgid "SecurityReports|Learn More"
msgstr ""
-msgid "SecurityConfiguration|Feature documentation for %{featureName}"
+msgid "SecurityReports|Learn more about setting up your dashboard"
msgstr ""
-msgid "SecurityConfiguration|Not yet configured"
+msgid "SecurityReports|Load more vulnerabilities"
msgstr ""
-msgid "SecurityConfiguration|Secure features"
+msgid "SecurityReports|Monitor vulnerabilities in your code"
msgstr ""
-msgid "SecurityConfiguration|Status"
+msgid "SecurityReports|More info"
msgstr ""
-msgid "SecurityDashboard|%{firstProject} and %{secondProject}"
+msgid "SecurityReports|More information"
msgstr ""
-msgid "SecurityDashboard|%{firstProject}, %{secondProject}, and %{rest}"
+msgid "SecurityReports|No vulnerabilities found for dashboard"
msgstr ""
-msgid "SecurityDashboard|Add a project to your dashboard"
+msgid "SecurityReports|No vulnerabilities found for this group"
msgstr ""
-msgid "SecurityDashboard|Add or remove projects from your dashboard"
+msgid "SecurityReports|No vulnerabilities found for this pipeline"
msgstr ""
-msgid "SecurityDashboard|Add projects"
+msgid "SecurityReports|No vulnerabilities found for this project"
msgstr ""
-msgid "SecurityDashboard|Edit dashboard"
+msgid "SecurityReports|Oops, something doesn't seem right."
msgstr ""
-msgid "SecurityDashboard|Hide dismissed"
+msgid "SecurityReports|Pipeline %{pipelineLink} triggered %{timeago} by %{user}"
msgstr ""
-msgid "SecurityDashboard|Monitor vulnerabilities in your code"
+msgid "SecurityReports|Project"
msgstr ""
-msgid "SecurityDashboard|More information"
+msgid "SecurityReports|Projects added"
msgstr ""
-msgid "SecurityDashboard|No vulnerabilities found for dashboard"
+msgid "SecurityReports|Remove project from dashboard"
msgstr ""
-msgid "SecurityDashboard|Pipeline %{pipelineLink} triggered %{timeago} by %{user}"
+msgid "SecurityReports|Report type"
msgstr ""
-msgid "SecurityDashboard|Project"
+msgid "SecurityReports|Return to dashboard"
msgstr ""
-msgid "SecurityDashboard|Projects added"
+msgid "SecurityReports|Security Dashboard"
msgstr ""
-msgid "SecurityDashboard|Remove project from dashboard"
+msgid "SecurityReports|Security reports can only be accessed by authorized users."
msgstr ""
-msgid "SecurityDashboard|Report type"
+msgid "SecurityReports|Select a project to add by using the project search field above."
msgstr ""
-msgid "SecurityDashboard|Return to dashboard"
+msgid "SecurityReports|Select a reason"
msgstr ""
-msgid "SecurityDashboard|Security Dashboard"
+msgid "SecurityReports|Severity"
msgstr ""
-msgid "SecurityDashboard|Select a project to add by using the project search field above."
+msgid "SecurityReports|Status"
msgstr ""
-msgid "SecurityDashboard|Severity"
+msgid "SecurityReports|The security dashboard displays the latest security findings for projects you wish to monitor. Select \"Edit dashboard\" to add and remove projects."
msgstr ""
-msgid "SecurityDashboard|Status"
+msgid "SecurityReports|The security dashboard displays the latest security report. Use it to find and fix vulnerabilities."
msgstr ""
-msgid "SecurityDashboard|The security dashboard displays the latest security findings for projects you wish to monitor. Select \"Edit dashboard\" to add and remove projects."
+msgid "SecurityReports|There was an error adding the comment."
msgstr ""
-msgid "SecurityDashboard|The security dashboard displays the latest security report. Use it to find and fix vulnerabilities."
+msgid "SecurityReports|There was an error creating the issue."
msgstr ""
-msgid "SecurityDashboard|There was an error while generating the report."
+msgid "SecurityReports|There was an error creating the merge request."
msgstr ""
-msgid "SecurityDashboard|Unable to add %{invalidProjects}"
+msgid "SecurityReports|There was an error deleting the comment."
+msgstr ""
+
+msgid "SecurityReports|There was an error dismissing the vulnerabilities."
+msgstr ""
+
+msgid "SecurityReports|There was an error dismissing the vulnerability."
+msgstr ""
+
+msgid "SecurityReports|There was an error reverting the dismissal."
+msgstr ""
+
+msgid "SecurityReports|There was an error reverting this dismissal."
+msgstr ""
+
+msgid "SecurityReports|There was an error while generating the report."
+msgstr ""
+
+msgid "SecurityReports|Unable to add %{invalidProjects}"
+msgstr ""
+
+msgid "SecurityReports|Undo dismiss"
+msgstr ""
+
+msgid "SecurityReports|While it's rare to have no vulnerabilities for your group, it can happen. In any event, we ask that you double check your settings to make sure you've set up your dashboard correctly."
+msgstr ""
+
+msgid "SecurityReports|While it's rare to have no vulnerabilities for your pipeline, it can happen. In any event, we ask that you double check your settings to make sure all security scanning jobs have passed successfully."
+msgstr ""
+
+msgid "SecurityReports|While it's rare to have no vulnerabilities for your project, it can happen. In any event, we ask that you double check your settings to make sure you've set up your dashboard correctly."
+msgstr ""
+
+msgid "SecurityReports|While it's rare to have no vulnerabilities, it can happen. In any event, we ask that you please double check your settings to make sure you've set up your dashboard correctly."
+msgstr ""
+
+msgid "SecurityReports|Won't fix / Accept risk"
+msgstr ""
+
+msgid "SecurityReports|You do not have sufficient permissions to access this report"
+msgstr ""
+
+msgid "SecurityReports|You must sign in as an authorized user to see this report"
+msgstr ""
+
+msgid "SecurityReports|[No reason]"
msgstr ""
msgid "See GitLab's %{password_policy_guidelines}"
@@ -17957,6 +18379,9 @@ msgstr ""
msgid "See the affected projects in the GitLab admin panel"
msgstr ""
+msgid "See what's new at GitLab"
+msgstr ""
+
msgid "Select"
msgstr ""
@@ -18362,9 +18787,6 @@ msgstr ""
msgid "Set up Jira Integration"
msgstr ""
-msgid "Set up Jira Integration illustration"
-msgstr ""
-
msgid "Set up a %{type} Runner automatically"
msgstr ""
@@ -18449,6 +18871,9 @@ msgstr ""
msgid "Severity: %{severity}"
msgstr ""
+msgid "Shards (%{shards})"
+msgstr ""
+
msgid "Shards to synchronize"
msgstr ""
@@ -18762,6 +19187,9 @@ msgstr ""
msgid "Snowplow"
msgstr ""
+msgid "Some child epics may be hidden due to applied filters"
+msgstr ""
+
msgid "Some email servers do not support overriding the email sender name. Enable this option to include the name of the author of the issue, merge request or comment in the email body instead."
msgstr ""
@@ -18903,6 +19331,9 @@ msgstr ""
msgid "Something went wrong while updating your list settings"
msgstr ""
+msgid "Something went wrong with your automatic subscription renewal"
+msgstr ""
+
msgid "Something went wrong, unable to add %{project} to dashboard"
msgstr ""
@@ -19188,6 +19619,9 @@ msgstr ""
msgid "Stage removed"
msgstr ""
+msgid "Standard"
+msgstr ""
+
msgid "Star a label to make it a priority label. Order the prioritized labels to change their relative priority, by dragging."
msgstr ""
@@ -19311,6 +19745,9 @@ msgstr ""
msgid "Static Application Security Testing (SAST)"
msgstr ""
+msgid "StaticSiteEditor|An error occurred while submitting your changes."
+msgstr ""
+
msgid "StaticSiteEditor|Branch could not be created."
msgstr ""
@@ -19320,18 +19757,30 @@ msgstr ""
msgid "StaticSiteEditor|Could not create merge request."
msgstr ""
+msgid "StaticSiteEditor|Incompatible file content"
+msgstr ""
+
msgid "StaticSiteEditor|Return to site"
msgstr ""
+msgid "StaticSiteEditor|Static site editor"
+msgstr ""
+
msgid "StaticSiteEditor|Success!"
msgstr ""
msgid "StaticSiteEditor|Summary of changes"
msgstr ""
+msgid "StaticSiteEditor|The Static Site Editor is currently configured to only edit Markdown content on pages generated from Middleman. Visit the documentation to learn more about configuring your site to use the Static Site Editor."
+msgstr ""
+
msgid "StaticSiteEditor|Update %{sourcePath} file"
msgstr ""
+msgid "StaticSiteEditor|View documentation"
+msgstr ""
+
msgid "StaticSiteEditor|View merge request"
msgstr ""
@@ -19389,6 +19838,12 @@ msgstr ""
msgid "StatusPage|Status page"
msgstr ""
+msgid "StatusPage|Status page URL"
+msgstr ""
+
+msgid "StatusPage|Status page frontend documentation"
+msgstr ""
+
msgid "StatusPage|To publish incidents to an external status page, GitLab will store a JSON file in your Amazon S3 account in a location accessible to your external status page service. Make sure to also set up %{docsLink}"
msgstr ""
@@ -19737,6 +20192,12 @@ msgstr ""
msgid "Synced"
msgstr ""
+msgid "Synchronization disabled"
+msgstr ""
+
+msgid "Synchronization of container repositories is disabled."
+msgstr ""
+
msgid "System"
msgstr ""
@@ -19890,6 +20351,9 @@ msgstr ""
msgid "Telephone number"
msgstr ""
+msgid "Telephone number (Optional)"
+msgstr ""
+
msgid "Template"
msgstr ""
@@ -20193,6 +20657,9 @@ msgstr ""
msgid "The license was successfully uploaded and is now active. You can see the details below."
msgstr ""
+msgid "The license was successfully uploaded and will be active from %{starts_at}. You can see the details below."
+msgstr ""
+
msgid "The maximum file size allowed is %{size}."
msgstr ""
@@ -20514,6 +20981,9 @@ msgstr ""
msgid "There was an error getting the epic participants."
msgstr ""
+msgid "There was an error importing the Jira project."
+msgstr ""
+
msgid "There was an error loading users activity calendar."
msgstr ""
@@ -20529,7 +20999,7 @@ msgstr ""
msgid "There was an error resetting user pipeline minutes."
msgstr ""
-msgid "There was an error saving this Geo Node"
+msgid "There was an error saving this Geo Node."
msgstr ""
msgid "There was an error saving your changes."
@@ -20592,6 +21062,9 @@ msgstr ""
msgid "They can be managed using the %{link}."
msgstr ""
+msgid "Third Party Advisory Link"
+msgstr ""
+
msgid "Third party offers"
msgstr ""
@@ -20628,6 +21101,9 @@ msgstr ""
msgid "This application will be able to:"
msgstr ""
+msgid "This attachment has been truncated to avoid exceeding the maximum allowed attachment size of 15MB. %{written_count} of %{issues_count} issues have been included. Consider re-exporting with a narrower selection of issues."
+msgstr ""
+
msgid "This block is self-referential"
msgstr ""
@@ -20769,6 +21245,12 @@ msgstr ""
msgid "This issue is confidential"
msgstr ""
+msgid "This issue is currently blocked by the following issues: %{issues}."
+msgstr ""
+
+msgid "This issue is in a child epic of the filtered epic"
+msgstr ""
+
msgid "This issue is locked."
msgstr ""
@@ -20967,7 +21449,10 @@ msgstr ""
msgid "This user will be the author of all events in the activity feed that are the result of an update, like new branches being created or new commits being pushed to existing branches. Upon creation or when reassigning you can only assign yourself to be the mirror user."
msgstr ""
-msgid "This variable can not be masked"
+msgid "This variable can not be masked."
+msgstr ""
+
+msgid "This variable does not match the expected pattern."
msgstr ""
msgid "This will help us personalize your onboarding experience."
@@ -20997,10 +21482,10 @@ msgstr ""
msgid "ThreatMonitoring|Application firewall not detected"
msgstr ""
-msgid "ThreatMonitoring|Container Network Policy"
+msgid "ThreatMonitoring|Container Network Policies are not installed or have been disabled. To view this data, ensure your Network Policies are installed and enabled for your cluster."
msgstr ""
-msgid "ThreatMonitoring|Container NetworkPolicies are not installed or has been disabled. To view this data, ensure you NetworkPolicies are installed and enabled for your cluster."
+msgid "ThreatMonitoring|Container Network Policy"
msgstr ""
msgid "ThreatMonitoring|Container NetworkPolicies not detected"
@@ -21033,7 +21518,7 @@ msgstr ""
msgid "ThreatMonitoring|Something went wrong, unable to fetch statistics"
msgstr ""
-msgid "ThreatMonitoring|The firewall is not installed or has been disabled. To view this data, ensure you firewall is installed and enabled for your cluster."
+msgid "ThreatMonitoring|The firewall is not installed or has been disabled. To view this data, ensure the web application firewall is installed and enabled for your cluster."
msgstr ""
msgid "ThreatMonitoring|The graph below is an overview of traffic coming to your application as tracked by the Web Application Firewall (WAF). View the docs for instructions on how to access the WAF logs to see what type of malicious traffic is trying to access your app. The docs link is also accessible by clicking the \"?\" icon next to the title below."
@@ -21102,6 +21587,9 @@ msgstr ""
msgid "Time in seconds GitLab will wait for a response from the external service. When the service does not respond in time, access will be denied."
msgstr ""
+msgid "Time of import: %{importTime}"
+msgstr ""
+
msgid "Time remaining"
msgstr ""
@@ -21284,7 +21772,7 @@ msgstr ""
msgid "Title:"
msgstr ""
-msgid "Titles and Filenames"
+msgid "Titles and Descriptions"
msgstr ""
msgid "To"
@@ -21491,7 +21979,7 @@ msgstr ""
msgid "Too many projects enabled. You will need to manage them via the console or the API."
msgstr ""
-msgid "Topics"
+msgid "Topics (optional)"
msgstr ""
msgid "Total"
@@ -21653,6 +22141,9 @@ msgstr ""
msgid "Try all GitLab has to offer for 30 days."
msgstr ""
+msgid "Try changing or removing filters."
+msgstr ""
+
msgid "Try to fork again"
msgstr ""
@@ -21746,6 +22237,9 @@ msgstr ""
msgid "Unable to connect to server: %{error}"
msgstr ""
+msgid "Unable to connect to the Jira instance. Please check your Jira integration configuration."
+msgstr ""
+
msgid "Unable to convert Kubernetes logs encoding to UTF-8"
msgstr ""
@@ -22687,6 +23181,9 @@ msgstr ""
msgid "View issue"
msgstr ""
+msgid "View issues"
+msgstr ""
+
msgid "View it on GitLab"
msgstr ""
@@ -22864,6 +23361,15 @@ msgstr ""
msgid "VulnerabilityManagement|Resolved %{timeago} by %{user}"
msgstr ""
+msgid "VulnerabilityManagement|Something went wrong while trying to delete the comment. Please try again later."
+msgstr ""
+
+msgid "VulnerabilityManagement|Something went wrong while trying to retrieve the vulnerability history. Please try again later."
+msgstr ""
+
+msgid "VulnerabilityManagement|Something went wrong while trying to save the comment. Please try again later."
+msgstr ""
+
msgid "VulnerabilityManagement|Something went wrong, could not create an issue."
msgstr ""
@@ -22894,6 +23400,9 @@ msgstr ""
msgid "VulnerabilityStatusTypes|Resolved"
msgstr ""
+msgid "Vulnerability|%{scannerName} (version %{scannerVersion})"
+msgstr ""
+
msgid "Vulnerability|Class"
msgstr ""
@@ -22927,7 +23436,10 @@ msgstr ""
msgid "Vulnerability|Project"
msgstr ""
-msgid "Vulnerability|Report Type"
+msgid "Vulnerability|Scanner Provider"
+msgstr ""
+
+msgid "Vulnerability|Scanner Type"
msgstr ""
msgid "Vulnerability|Severity"
@@ -22981,9 +23493,15 @@ msgstr ""
msgid "We sent you an email with reset password instructions"
msgstr ""
+msgid "We tried to automatically renew your %{strong}%{plan_name}%{strong_close} subscription for %{strong}%{namespace_name}%{strong_close} on %{expires_on} but something went wrong so your subscription was downgraded to the free plan. Don't worry, your data is safe. We suggest you check your payment method and get in touch with our support team (%{support_link}). They'll gladly help with your subscription renewal."
+msgstr ""
+
msgid "We want to be sure it is you, please confirm you are not a robot."
msgstr ""
+msgid "We will automatically renew your %{strong}%{plan_name}%{strong_close} subscription for %{strong}%{namespace_name}%{strong_close} on %{strong}%{expires_on}%{strong_close}. There's nothing that you need to do, we'll let you know when the renewal is complete. Need more seats, a higher plan or just want to review your payment method?"
+msgstr ""
+
msgid "We've found no vulnerabilities"
msgstr ""
@@ -23087,15 +23605,6 @@ msgstr ""
msgid "When:"
msgstr ""
-msgid "While it's rare to have no vulnerabilities for your group, it can happen. In any event, we ask that you double check your settings to make sure you've set up your dashboard correctly."
-msgstr ""
-
-msgid "While it's rare to have no vulnerabilities for your pipeline, it can happen. In any event, we ask that you double check your settings to make sure all security scanning jobs have passed successfully."
-msgstr ""
-
-msgid "While it's rare to have no vulnerabilities for your project, it can happen. In any event, we ask that you double check your settings to make sure you've set up your dashboard correctly."
-msgstr ""
-
msgid "While it's rare to have no vulnerabilities, it can happen. In any event, we ask that you please double check your settings to make sure you've set up your dashboard correctly."
msgstr ""
@@ -23126,9 +23635,6 @@ msgstr ""
msgid "Wiki"
msgstr ""
-msgid "Wiki pages"
-msgstr ""
-
msgid "Wiki was successfully updated."
msgstr ""
@@ -23312,6 +23818,9 @@ msgstr ""
msgid "Yes, add it"
msgstr ""
+msgid "Yes, close issue"
+msgstr ""
+
msgid "Yes, let me map Google Code users to full names or GitLab users."
msgstr ""
@@ -23507,6 +24016,9 @@ msgstr ""
msgid "You could not create a new trigger."
msgstr ""
+msgid "You didn't renew your %{strong}%{plan_name}%{strong_close} subscription for %{strong}%{namespace_name}%{strong_close} so it was downgraded to the free plan."
+msgstr ""
+
msgid "You didn't renew your %{strong}%{plan_name}%{strong_close} subscription so it was downgraded to the GitLab Core Plan."
msgstr ""
@@ -23573,6 +24085,9 @@ msgstr ""
msgid "You have declined the invitation to join %{label}."
msgstr ""
+msgid "You have imported from this project %{numberOfPreviousImportsForProject} times before. Each new import will create duplicate issues."
+msgstr ""
+
msgid "You have no permissions"
msgstr ""
@@ -23654,6 +24169,9 @@ msgstr ""
msgid "You need to upload a Google Takeout archive."
msgstr ""
+msgid "You reached %{usage_in_percent} of %{namespace_name}'s capacity (%{used_storage} of %{storage_limit})"
+msgstr ""
+
msgid "You tried to fork %{link_to_the_project} but it failed for the following reason:"
msgstr ""
@@ -23738,9 +24256,21 @@ msgstr ""
msgid "YouTube"
msgstr ""
+msgid "Your %{strong}%{plan_name}%{strong_close} subscription for %{strong}%{namespace_name}%{strong_close} will expire on %{strong}%{expires_on}%{strong_close}. After that, you will not to be able to create issues or merge requests as well as many other features."
+msgstr ""
+
msgid "Your %{strong}%{plan_name}%{strong_close} subscription will expire on %{strong}%{expires_on}%{strong_close}. After that, you will not to be able to create issues or merge requests as well as many other features."
msgstr ""
+msgid "Your CSV export has started. It will be emailed to %{email} when complete."
+msgstr ""
+
+msgid "Your CSV export of %{issues_count} from project %{project_link} has been added to this email as an attachment."
+msgstr ""
+
+msgid "Your CSV export of %{written_count} from project %{project_name} (%{project_url}) has been added to this email as an attachment."
+msgstr ""
+
msgid "Your Commit Email will be used for web based operations, such as edits and merges."
msgstr ""
@@ -23837,6 +24367,9 @@ msgstr ""
msgid "Your comment could not be updated! Please check your network connection and try again."
msgstr ""
+msgid "Your comment will be discarded."
+msgstr ""
+
msgid "Your custom stage '%{title}' was created"
msgstr ""
@@ -23876,6 +24409,9 @@ msgstr ""
msgid "Your new personal access token has been created."
msgstr ""
+msgid "Your new project access token has been created."
+msgstr ""
+
msgid "Your password isn't required to view this page. If a password or any other personal details are requested, please contact your administrator to report abuse."
msgstr ""
@@ -23894,12 +24430,18 @@ msgstr ""
msgid "Your request for access has been queued for review."
msgstr ""
+msgid "Your search didn't match any commits."
+msgstr ""
+
msgid "Your subscription expired!"
msgstr ""
msgid "Your subscription has been downgraded"
msgstr ""
+msgid "Your subscription will automatically renew in %{remaining_days}"
+msgstr ""
+
msgid "Your subscription will expire in %{remaining_days}"
msgstr ""
@@ -23937,7 +24479,7 @@ msgstr ""
msgid "allowed to fail"
msgstr ""
-msgid "already being used for another group or project milestone."
+msgid "already being used for another group or project %{timebox_name}."
msgstr ""
msgid "already has a \"created\" issue link"
@@ -24015,6 +24557,9 @@ msgstr ""
msgid "ciReport|%{linkStartTag}Learn more about SAST %{linkEndTag}"
msgstr ""
+msgid "ciReport|%{linkStartTag}Learn more about Secret Scanning %{linkEndTag}"
+msgstr ""
+
msgid "ciReport|%{linkStartTag}Learn more about codequality reports %{linkEndTag}"
msgstr ""
@@ -24173,6 +24718,12 @@ msgstr ""
msgid "ciReport|SAST"
msgstr ""
+msgid "ciReport|Secret scanning"
+msgstr ""
+
+msgid "ciReport|Secret scanning detects secrets and credentials vulnerabilities in your source code."
+msgstr ""
+
msgid "ciReport|Security scanning"
msgstr ""
@@ -24366,6 +24917,9 @@ msgstr ""
msgid "group"
msgstr ""
+msgid "groups"
+msgstr ""
+
msgid "has already been linked to another vulnerability"
msgstr ""
@@ -24527,9 +25081,6 @@ msgstr[0] ""
msgid "merged %{time_ago}"
msgstr ""
-msgid "milestone should belong either to a project or a group."
-msgstr ""
-
msgid "missing"
msgstr ""
@@ -24866,6 +25417,9 @@ msgstr ""
msgid "no contributions"
msgstr ""
+msgid "no expiration"
+msgstr ""
+
msgid "no one can merge"
msgstr ""
@@ -24916,6 +25470,9 @@ msgstr ""
msgid "pending removal"
msgstr ""
+msgid "per day"
+msgstr ""
+
msgid "pipeline"
msgstr ""
@@ -24942,6 +25499,12 @@ msgstr[0] ""
msgid "project avatar"
msgstr ""
+msgid "projects"
+msgstr ""
+
+msgid "push to your repository, create pipelines, create issues or add comments. To reduce storage capacity, delete unused repositories, artifacts, wikis, issues, and pipelines."
+msgstr ""
+
msgid "quick actions"
msgstr ""
@@ -25087,10 +25650,6 @@ msgstr ""
msgid "this document"
msgstr ""
-msgid "thread resolved"
-msgid_plural "threads resolved"
-msgstr[0] ""
-
msgid "to help your contributors communicate effectively!"
msgstr ""
@@ -25145,6 +25704,9 @@ msgstr ""
msgid "view the blob"
msgstr ""
+msgid "vulnerability|Add a comment"
+msgstr ""
+
msgid "vulnerability|Add a comment or reason for dismissal"
msgstr ""
diff --git a/locale/zh_CN/gitlab.po b/locale/zh_CN/gitlab.po
index 9b11d043e60..2e5f39c0ff4 100644
--- a/locale/zh_CN/gitlab.po
+++ b/locale/zh_CN/gitlab.po
@@ -12,7 +12,7 @@ msgstr ""
"X-Crowdin-Project: gitlab-ee\n"
"X-Crowdin-Language: zh-CN\n"
"X-Crowdin-File: /master/locale/gitlab.pot\n"
-"PO-Revision-Date: 2020-04-15 00:30\n"
+"PO-Revision-Date: 2020-05-05 21:36\n"
msgid " %{start} to %{end}"
msgstr ""
@@ -182,6 +182,10 @@ msgid "%d tag"
msgid_plural "%d tags"
msgstr[0] "%d个标签"
+msgid "%d unresolved thread"
+msgid_plural "%d unresolved threads"
+msgstr[0] ""
+
msgid "%d vulnerability dismissed"
msgid_plural "%d vulnerabilities dismissed"
msgstr[0] ""
@@ -335,6 +339,9 @@ msgstr "%{mergeLength}/%{usersLength} å¯ä»¥åˆå¹¶"
msgid "%{mrText}, this issue will be closed automatically."
msgstr "%{mrText},此议题将自动关闭。"
+msgid "%{namespace_name} is now read-only. You cannot: %{base_message}"
+msgstr ""
+
msgid "%{name} contained %{resultsString}"
msgstr "%{name} åŒ…å« %{resultsString}"
@@ -347,6 +354,10 @@ msgstr ""
msgid "%{name}'s avatar"
msgstr "%{name} 的头åƒ"
+msgid "%{no_of_days} day"
+msgid_plural "%{no_of_days} days"
+msgstr[0] ""
+
msgid "%{number_commits_behind} commits behind %{default_branch}, %{number_commits_ahead} commits ahead"
msgstr "%{number_commits_behind}个è½åŽäºŽ%{default_branch}分支的æ交, åŒæ—¶%{number_commits_ahead}个超å‰çš„æ交"
@@ -464,6 +475,9 @@ msgstr[0] "%{text}%{files}"
msgid "%{text} is available"
msgstr "%{text}å¯ç”¨"
+msgid "%{timebox_name} should belong either to a project or a group."
+msgstr ""
+
msgid "%{title} %{operator} %{threshold}"
msgstr "%{title} %{operator} %{threshold}"
@@ -720,12 +734,18 @@ msgstr "<code>\"johnsmith@example.com\": \"johnsm...@example.com\"</code> 将会
msgid "<code>\"johnsmith@example.com\": \"johnsmith@example.com\"</code> will add \"By <a href=\"#\">johnsmith@example.com</a>\" to all issues and comments originally created by johnsmith@example.com. By default, the email address or username is masked to ensure the user's privacy. Use this option if you want to show the full email address."
msgstr "<code>\"johnsmith@example.com\": \"johnsmith@example.com\"</code> 将会把“由<a href=\"#\">@johnsmith</a>â€æ·»åŠ åˆ°åŽŸæœ¬ç”±johnsmith@example.com创建的所有议题和评论中。 为ä¿æŠ¤ç”¨æˆ·çš„éšç§ï¼Œç”µå­é‚®ä»¶åœ°å€æˆ–用户å默认将被å±è”½ã€‚如需显示完整邮件地å€ï¼Œå¯ä½¿ç”¨æ­¤é€‰é¡¹ã€‚"
+msgid "<namespace / project>"
+msgstr ""
+
msgid "<no name set>"
msgstr "<未设置å称>"
msgid "<no scopes selected>"
msgstr "<未选择范围>"
+msgid "<project name>"
+msgstr ""
+
msgid "<strong>%{group_name}</strong> group members"
msgstr "<strong>%{group_name}</strong> 群组æˆå‘˜"
@@ -759,6 +779,9 @@ msgstr "将使用您的电å­é‚®ä»¶åœ°å€ä¸ºæ­¤GitLab安装并é…ç½®Let's Encryp
msgid "A basic page and serverless function that uses AWS Lambda, AWS API Gateway, and GitLab Pages"
msgstr "使用AWS Lambda,AWS API网关和GitLab Pages的基本页é¢å’Œæ— æœåŠ¡å™¨åŠŸèƒ½"
+msgid "A complete DevOps platform"
+msgstr ""
+
msgid "A default branch cannot be chosen for an empty project."
msgstr "无法为空项目选择默认分支。"
@@ -975,6 +998,12 @@ msgstr "您的é™æ€å¯¹è±¡ä»¤ç‰Œç”¨äºŽä»Žå¤–部存储存å–é™æ€å¯¹è±¡ï¼ˆä¾‹å¦‚
msgid "AccessTokens|reset it"
msgstr "é‡ç½®ä»¤ç‰Œ"
+msgid "AccessibilityReport|Accessibility report artifact not found"
+msgstr ""
+
+msgid "AccessibilityReport|Failed to retrieve accessibility report"
+msgstr ""
+
msgid "AccessibilityReport|Learn More"
msgstr ""
@@ -984,6 +1013,9 @@ msgstr ""
msgid "AccessibilityReport|New"
msgstr ""
+msgid "AccessibilityReport|The accessibility scanning found an error of the following type: %{code}"
+msgstr ""
+
msgid "Account"
msgstr "å¸å·"
@@ -1225,6 +1257,9 @@ msgstr "为å²è¯—添加了一个议题。"
msgid "Added at"
msgstr "添加时间"
+msgid "Added for this merge request"
+msgstr ""
+
msgid "Added in this version"
msgstr "此版本新增"
@@ -1595,6 +1630,72 @@ msgid "Alert"
msgid_plural "Alerts"
msgstr[0] "警报"
+msgid "Alert Details"
+msgstr ""
+
+msgid "AlertManagement|Acknowledged"
+msgstr ""
+
+msgid "AlertManagement|Alert"
+msgstr ""
+
+msgid "AlertManagement|Authorize external service"
+msgstr ""
+
+msgid "AlertManagement|Display alerts from all your monitoring tools directly within GitLab. Streamline the investigation of your alerts and the escalation of alerts to incidents."
+msgstr ""
+
+msgid "AlertManagement|End time"
+msgstr ""
+
+msgid "AlertManagement|End time:"
+msgstr ""
+
+msgid "AlertManagement|Events"
+msgstr ""
+
+msgid "AlertManagement|Events:"
+msgstr ""
+
+msgid "AlertManagement|Full Alert Details"
+msgstr ""
+
+msgid "AlertManagement|More information"
+msgstr ""
+
+msgid "AlertManagement|No alerts available to display. If you think you're seeing this message in error, refresh the page."
+msgstr ""
+
+msgid "AlertManagement|No alerts to display."
+msgstr ""
+
+msgid "AlertManagement|Overview"
+msgstr ""
+
+msgid "AlertManagement|Resolved"
+msgstr ""
+
+msgid "AlertManagement|Severity"
+msgstr ""
+
+msgid "AlertManagement|Start time"
+msgstr ""
+
+msgid "AlertManagement|Start time:"
+msgstr ""
+
+msgid "AlertManagement|Status"
+msgstr ""
+
+msgid "AlertManagement|Surface alerts in GitLab"
+msgstr ""
+
+msgid "AlertManagement|There was an error displaying the alerts. Confirm your endpoint's configuration details to ensure alerts appear."
+msgstr ""
+
+msgid "AlertManagement|Triggered"
+msgstr ""
+
msgid "AlertService|%{linkStart}Learn more%{linkEnd} about configuring this endpoint to receive alerts."
msgstr "%{linkStart}了解更多%{linkEnd}有关接收报警端点é…ç½®"
@@ -1664,6 +1765,9 @@ msgstr "所有项目"
msgid "All security scans are enabled because %{linkStart}Auto DevOps%{linkEnd} is enabled on this project"
msgstr "所有安全扫æ都已å¯ç”¨ï¼Œå› ä¸ºæ­¤é¡¹ç›®å·²å¼€å¯äº†%{linkStart}Auto DevOps%{linkEnd}"
+msgid "All threads resolved"
+msgstr ""
+
msgid "All users"
msgstr "所有用户"
@@ -1682,6 +1786,9 @@ msgstr "å…è®¸ç¾¤ç»„æ‰€æœ‰è€…ç®¡ç† LDAP 相关的设置"
msgid "Allow only the selected protocols to be used for Git access."
msgstr "ä»…å…许所选å议用于 Git 访问。"
+msgid "Allow owners to manage default branch protection per group"
+msgstr ""
+
msgid "Allow owners to manually add users outside of LDAP"
msgstr ""
@@ -1790,6 +1897,9 @@ msgstr "获å–新规则的核准人时出错。"
msgid "An error occurred fetching the dropdown data."
msgstr "获å–下拉数æ®æ—¶å‡ºé”™ã€‚"
+msgid "An error occurred fetching the project authors."
+msgstr ""
+
msgid "An error occurred previewing the blob"
msgstr "预览 blob 时出错"
@@ -1871,6 +1981,9 @@ msgstr "获å–项目自动完æˆæ—¶å‡ºé”™ã€‚"
msgid "An error occurred while fetching sidebar data"
msgstr "获å–侧边æ æ•°æ®æ—¶å‘生错误"
+msgid "An error occurred while fetching terraform reports."
+msgstr ""
+
msgid "An error occurred while fetching the Service Desk address."
msgstr "获å–æœåŠ¡å°åœ°å€æ—¶å‘生错误。"
@@ -2108,6 +2221,9 @@ msgstr "å垃圾邮件验è¯"
msgid "Any"
msgstr "任何"
+msgid "Any Author"
+msgstr ""
+
msgid "Any Label"
msgstr "任何标记"
@@ -2319,12 +2435,12 @@ msgstr "确定è¦å½’档此项目å—?"
msgid "Are you sure that you want to unarchive this project?"
msgstr "确定è¦å–消归档此项目å—?"
-msgid "Are you sure you want to cancel creating this comment?"
-msgstr "确定è¦å–消创建此评论å—?"
-
msgid "Are you sure you want to cancel editing this comment?"
msgstr "确定è¦å–消编辑此评论å—?"
+msgid "Are you sure you want to close this blocked issue?"
+msgstr ""
+
msgid "Are you sure you want to delete %{name}?"
msgstr ""
@@ -2352,6 +2468,9 @@ msgstr "确定è¦åˆ é™¤è¿™æ¡æµæ°´çº¿å—? 删除æ“作将使所有æµæ°´çº¿ç¼
msgid "Are you sure you want to deploy this environment?"
msgstr ""
+msgid "Are you sure you want to discard this comment?"
+msgstr ""
+
msgid "Are you sure you want to erase this build?"
msgstr "您确定è¦åˆ é™¤è¿™ä¸ªæž„建å—?"
@@ -2527,6 +2646,9 @@ msgstr "指派 %{assignee_users_sentence}。"
msgid "At least one approval from a code owner is required to change files matching the respective CODEOWNER rules."
msgstr "至少需è¦ä¸€ä¸ªä»£ç æ‰€æœ‰è€…批准,以便更改符åˆç›¸åº”çš„ CODEEWNER 规则的文件。"
+msgid "At least one logging option is required to be enabled"
+msgstr ""
+
msgid "At least one of group_id or project_id must be specified"
msgstr "必须指定至少一个group_id或 project_id"
@@ -2564,6 +2686,27 @@ msgstr "于"
msgid "AuditEvents|Target"
msgstr "目标"
+msgid "AuditLogs|(removed)"
+msgstr ""
+
+msgid "AuditLogs|Action"
+msgstr ""
+
+msgid "AuditLogs|Author"
+msgstr ""
+
+msgid "AuditLogs|Date"
+msgstr ""
+
+msgid "AuditLogs|IP Address"
+msgstr ""
+
+msgid "AuditLogs|Object"
+msgstr ""
+
+msgid "AuditLogs|Target"
+msgstr ""
+
msgid "Aug"
msgstr "8月"
@@ -2846,9 +2989,6 @@ msgstr "Bamboo 根地å€ï¼Œä¾‹å¦‚:https://bambo.example.com"
msgid "BambooService|You must set up automatic revision labeling and a repository trigger in Bamboo."
msgstr "您必须在Bamboo中设置自动版本标签和仓库触å‘器。"
-msgid "Batch operations"
-msgstr "批é‡æ“作"
-
msgid "BatchComments|Delete all pending comments"
msgstr "删除所有待处ç†çš„评论"
@@ -2978,6 +3118,9 @@ msgstr "使用默认列表,是您充分利用看æ¿ä½œç”¨çš„起点。"
msgid "Boards"
msgstr "看æ¿"
+msgid "Boards and Board Lists"
+msgstr ""
+
msgid "Boards|Collapse"
msgstr "收起"
@@ -3197,6 +3340,9 @@ msgstr "è´­ä¹°ä¼ä¸šç‰ˆ"
msgid "Buy GitLab Enterprise Edition"
msgstr "è´­ä¹°GitLabä¼ä¸šç‰ˆ"
+msgid "Buy more Pipeline minutes"
+msgstr ""
+
msgid "By %{user_name}"
msgstr "ç”± %{user_name}"
@@ -3317,6 +3463,9 @@ msgstr "éžç¾¤ç»„托管账户无法删除群组æˆå‘˜"
msgid "Can't scan the code?"
msgstr "无法扫æ二维ç ï¼Ÿ"
+msgid "Can't update snippet: %{err}"
+msgstr ""
+
msgid "Canary"
msgstr "金ä¸é›€"
@@ -3485,6 +3634,9 @@ msgstr "åªå¯ä»¥é€šè¿‡Releases API更改å‘布标签。 %{linkStart}更多信æ
msgid "Changing group path can have unintended side effects."
msgstr "更改群组路径å¯èƒ½ä¼šæœ‰æ„想ä¸åˆ°çš„副作用。"
+msgid "Charts"
+msgstr ""
+
msgid "Charts can't be displayed as the request for data has timed out. %{documentationLink}"
msgstr "无法显示图表,因为数æ®è¯·æ±‚已超时。 %{documentationLink}"
@@ -3521,9 +3673,6 @@ msgstr "标签"
msgid "ChatMessage|and [%{count} more](%{pipeline_failed_jobs_url})"
msgstr "以åŠ[其余%{count}项](%{pipeline_failed_jobs_url})"
-msgid "ChatMessage|failed"
-msgstr "已失败"
-
msgid "ChatMessage|has failed"
msgstr "已失败"
@@ -3539,9 +3688,6 @@ msgstr "在%{duration}åŽ"
msgid "ChatMessage|in %{project_link}"
msgstr "在%{project_link}中"
-msgid "ChatMessage|passed"
-msgstr "已通过"
-
msgid "Check again"
msgstr "å†æ¬¡æ£€æŸ¥"
@@ -3992,9 +4138,6 @@ msgstr "清除æƒé‡"
msgid "Click any <strong>project name</strong> in the project list below to navigate to the project milestone."
msgstr "å•å‡»ä¸‹é¢é¡¹ç›®åˆ—表中的任何 <strong>项目å称</strong> 跳转到项目里程碑。"
-msgid "Click here"
-msgstr "点击此处"
-
msgid "Click the <strong>Download</strong> button and wait for downloading to complete."
msgstr "点击 <strong>下载</strong> 按钮,等待下载完æˆã€‚"
@@ -4382,6 +4525,12 @@ msgstr "正在获å–项目"
msgid "ClusterIntegration|Fetching zones"
msgstr "正在获å–地域"
+msgid "ClusterIntegration|Fluentd"
+msgstr ""
+
+msgid "ClusterIntegration|Fluentd is an open source data collector, which lets you unify the data collection and consumption for a better use and understanding of data. It requires at least one of the following logs to be successfully installed."
+msgstr ""
+
msgid "ClusterIntegration|GitLab Integration"
msgstr "GitLab集æˆ"
@@ -4688,6 +4837,15 @@ msgstr "请求å¯åŠ¨å®‰è£…失败"
msgid "ClusterIntegration|Request to begin uninstalling failed"
msgstr "请求å¯åŠ¨å¸è½½å¤±è´¥"
+msgid "ClusterIntegration|SIEM Hostname"
+msgstr ""
+
+msgid "ClusterIntegration|SIEM Port"
+msgstr ""
+
+msgid "ClusterIntegration|SIEM Protocol"
+msgstr ""
+
msgid "ClusterIntegration|Save changes"
msgstr "ä¿å­˜æ›´æ”¹"
@@ -4784,6 +4942,12 @@ msgstr "选择地域"
msgid "ClusterIntegration|Select zone to choose machine type"
msgstr "按地域选择实例类型"
+msgid "ClusterIntegration|Send Cilium Logs"
+msgstr ""
+
+msgid "ClusterIntegration|Send ModSecurity Logs"
+msgstr ""
+
msgid "ClusterIntegration|Service Token"
msgstr "æœåŠ¡ä»¤ç‰Œ"
@@ -5211,18 +5375,33 @@ msgstr ""
msgid "Compliance framework (optional)"
msgstr ""
+msgid "ComplianceFramework|GDPR"
+msgstr ""
+
msgid "ComplianceFramework|GDPR - General Data Protection Regulation"
msgstr ""
+msgid "ComplianceFramework|HIPAA"
+msgstr ""
+
msgid "ComplianceFramework|HIPAA - Health Insurance Portability and Accountability Act"
msgstr ""
+msgid "ComplianceFramework|PCI-DSS"
+msgstr ""
+
msgid "ComplianceFramework|PCI-DSS - Payment Card Industry-Data Security Standard"
msgstr ""
+msgid "ComplianceFramework|SOC 2"
+msgstr ""
+
msgid "ComplianceFramework|SOC 2 - Service Organization Control 2"
msgstr ""
+msgid "ComplianceFramework|SOX"
+msgstr ""
+
msgid "ComplianceFramework|SOX - Sarbanes-Oxley"
msgstr ""
@@ -5370,9 +5549,15 @@ msgstr "æ­¤GitLab实例上尚未å¯ç”¨å®¹å™¨é•œåƒåº“。请通知管ç†å‘˜å¯ç”¨
msgid "Container repositories sync capacity"
msgstr ""
+msgid "ContainerRegistry| Please visit the %{linkStart}administration settings%{linkEnd} to enable this feature."
+msgstr ""
+
msgid "ContainerRegistry|%{imageName} tags"
msgstr ""
+msgid "ContainerRegistry|%{title} was successfully scheduled for deletion"
+msgstr ""
+
msgid "ContainerRegistry|Automatically remove extra images that aren't designed to be kept."
msgstr "自动删除计划ä¸ä¿ç•™çš„多余镜åƒã€‚"
@@ -5385,6 +5570,9 @@ msgstr ""
msgid "ContainerRegistry|Container Registry"
msgstr "容器镜åƒåº“"
+msgid "ContainerRegistry|Container Registry tag expiration and retention policy is disabled"
+msgstr ""
+
msgid "ContainerRegistry|Copy build command"
msgstr "å¤åˆ¶æž„建命令"
@@ -5394,18 +5582,12 @@ msgstr "å¤åˆ¶ç™»å½•å‘½ä»¤"
msgid "ContainerRegistry|Copy push command"
msgstr "å¤åˆ¶æŽ¨é€å‘½ä»¤"
-msgid "ContainerRegistry|Currently, the Container Registry tag expiration feature is not available for projects created before GitLab version 12.8. For updates and more information, visit Issue %{linkStart}#196124%{linkEnd}"
-msgstr ""
-
msgid "ContainerRegistry|Docker connection error"
msgstr "Docker连接错误"
msgid "ContainerRegistry|Docker tag expiration policy is %{toggleStatus}"
msgstr "Docker标签过期策略为%{toggleStatus}"
-msgid "ContainerRegistry|Docker tags with names matching this regex pattern will expire:"
-msgstr ""
-
msgid "ContainerRegistry|Edit Settings"
msgstr ""
@@ -5427,9 +5609,6 @@ msgstr "如果您尚未登录,您需è¦ä½¿ç”¨æ‚¨çš„GitLab用户å和密ç æ¥
msgid "ContainerRegistry|Image ID"
msgstr "é•œåƒID"
-msgid "ContainerRegistry|Image deleted successfully"
-msgstr ""
-
msgid "ContainerRegistry|Keep and protect the images that matter most."
msgstr "ä¿ç•™å’Œä¿æŠ¤æœ€é‡è¦çš„é•œåƒã€‚"
@@ -5445,12 +5624,24 @@ msgstr ""
msgid "ContainerRegistry|Number of tags to retain:"
msgstr "è¦ä¿ç•™çš„标签数é‡ï¼š"
+msgid "ContainerRegistry|Please contact your administrator."
+msgstr ""
+
msgid "ContainerRegistry|Push an image"
msgstr ""
msgid "ContainerRegistry|Quick Start"
msgstr "快速入门"
+msgid "ContainerRegistry|Regular expressions such as %{codeStart}.*-test%{codeEnd} or %{codeStart}dev-.*%{codeEnd} are supported"
+msgstr ""
+
+msgid "ContainerRegistry|Regular expressions such as %{codeStart}.*-test%{codeEnd} or %{codeStart}dev-.*%{codeEnd} are supported. To select all tags, use %{codeStart}.*%{codeEnd}"
+msgstr ""
+
+msgid "ContainerRegistry|Remember to run %{docLinkStart}garbage collection%{docLinkEnd} to remove the stale data from storage."
+msgstr ""
+
msgid "ContainerRegistry|Remove repository"
msgstr "删除仓库"
@@ -5464,22 +5655,22 @@ msgstr[0] "删除标签"
msgid "ContainerRegistry|Retention policy has been Enabled"
msgstr ""
-msgid "ContainerRegistry|Something went wrong while deleting the image."
-msgstr ""
+msgid "ContainerRegistry|Something went wrong while fetching the expiration policy."
+msgstr "获å–到期政策时出了错。"
-msgid "ContainerRegistry|Something went wrong while deleting the tag."
+msgid "ContainerRegistry|Something went wrong while fetching the repository list."
msgstr ""
-msgid "ContainerRegistry|Something went wrong while deleting the tags."
+msgid "ContainerRegistry|Something went wrong while fetching the tags list."
msgstr ""
-msgid "ContainerRegistry|Something went wrong while fetching the expiration policy."
-msgstr "获å–到期政策时出了错。"
+msgid "ContainerRegistry|Something went wrong while marking the tag for deletion."
+msgstr ""
-msgid "ContainerRegistry|Something went wrong while fetching the packages list."
+msgid "ContainerRegistry|Something went wrong while marking the tags for deletion."
msgstr ""
-msgid "ContainerRegistry|Something went wrong while fetching the tags list."
+msgid "ContainerRegistry|Something went wrong while scheduling %{title} for deletion. Please try again."
msgstr ""
msgid "ContainerRegistry|Something went wrong while updating the expiration policy."
@@ -5488,16 +5679,25 @@ msgstr "更新到期政策时出了错。"
msgid "ContainerRegistry|Tag"
msgstr "标签"
-msgid "ContainerRegistry|Tag deleted successfully"
-msgstr ""
-
msgid "ContainerRegistry|Tag expiration policy"
msgstr "标签过期策略"
msgid "ContainerRegistry|Tag expiration policy is designed to:"
msgstr "标签过期策略旨在:"
-msgid "ContainerRegistry|Tags deleted successfully"
+msgid "ContainerRegistry|Tag successfully marked for deletion."
+msgstr ""
+
+msgid "ContainerRegistry|Tags successfully marked for deletion."
+msgstr ""
+
+msgid "ContainerRegistry|Tags with names matching this regex pattern will %{italicStart}be preserved:%{italicEnd}"
+msgstr ""
+
+msgid "ContainerRegistry|Tags with names matching this regex pattern will %{italicStart}expire:%{italicEnd}"
+msgstr ""
+
+msgid "ContainerRegistry|The Container Registry tag expiration and retention policies for this project have not been enabled."
msgstr ""
msgid "ContainerRegistry|The last tag related to this image was recently removed. This empty image and any associated data will be automatically removed as part of the regular Garbage Collection process. If you have any questions, contact your administrator."
@@ -5518,18 +5718,18 @@ msgstr "当å‰ç¾¤ç»„没有容器镜åƒã€‚"
msgid "ContainerRegistry|There are no container images stored for this project"
msgstr "当å‰é¡¹ç›®æ²¡æœ‰å®¹å™¨é•œåƒã€‚"
-msgid "ContainerRegistry|This Registry contains deleted image tag data. Remember to run %{docLinkStart}garbage collection%{docLinkEnd} to remove the stale data from storage."
+msgid "ContainerRegistry|There was an error during the deletion of this image repository, please try again."
msgstr ""
msgid "ContainerRegistry|This image has no active tags"
msgstr "此镜åƒæ²¡æœ‰æ´»åŠ¨çš„标签"
+msgid "ContainerRegistry|This image repository is scheduled for deletion"
+msgstr ""
+
msgid "ContainerRegistry|We are having trouble connecting to Docker, which could be due to an issue with your project name or path. %{docLinkStart}More Information%{docLinkEnd}"
msgstr "当å‰æ— æ³•è¿žæŽ¥åˆ°Docker。原因å¯èƒ½æ˜¯é¡¹ç›®å称或路径的问题。 %{docLinkStart}更多信æ¯%{docLinkEnd}"
-msgid "ContainerRegistry|Wildcards such as %{codeStart}.*-stable%{codeEnd} or %{codeStart}production/.*%{codeEnd} are supported. To select all tags, use %{codeStart}.*%{codeEnd}"
-msgstr ""
-
msgid "ContainerRegistry|With the Container Registry, every project can have its own space to store its Docker images. %{docLinkStart}More Information%{docLinkEnd}"
msgstr "在容器镜åƒåº“中,æ¯ä¸ªé¡¹ç›®éƒ½æœ‰è‡ªå·±çš„空间æ¥å­˜å‚¨å®¹å™¨é•œåƒã€‚ %{docLinkStart}更多信æ¯%{docLinkEnd}"
@@ -5782,8 +5982,8 @@ msgstr "无法删除%{design}。请é‡è¯•ã€‚"
msgid "Could not delete chat nickname %{chat_name}."
msgstr "无法删除èŠå¤©æ˜µç§° %{chat_name}。"
-msgid "Could not find design"
-msgstr "未找到设计"
+msgid "Could not find design."
+msgstr ""
msgid "Could not remove the trigger."
msgstr "无法删除触å‘器。"
@@ -5797,6 +5997,9 @@ msgstr "无法撤消身份模拟令牌 %{token_name}。"
msgid "Could not revoke personal access token %{personal_access_token_name}."
msgstr "无法撤消个人访问令牌 %{personal_access_token_name}。"
+msgid "Could not revoke project access token %{project_access_token_name}."
+msgstr ""
+
msgid "Could not save group ID"
msgstr "无法ä¿å­˜ç¾¤ç»„ID"
@@ -5854,6 +6057,9 @@ msgstr "创建一个åˆå¹¶ç”³è¯·"
msgid "Create a new branch"
msgstr "创建一个新分支"
+msgid "Create a new deploy key for this project"
+msgstr ""
+
msgid "Create a new file as there are no files yet. Afterwards, you'll be able to commit your changes."
msgstr "当å‰æ— æ–‡ä»¶ã€‚请先创建一个新文件。然åŽæ‚¨å°†èƒ½å¤Ÿæ交您的更改。"
@@ -5959,6 +6165,9 @@ msgstr "创建项目标记"
msgid "Create requirement"
msgstr ""
+msgid "Create snippet"
+msgstr ""
+
msgid "Create wildcard: %{searchTerm}"
msgstr ""
@@ -5980,6 +6189,9 @@ msgstr "创建个人访问令牌"
msgid "Created"
msgstr "创建时间"
+msgid "Created %{timestamp}"
+msgstr ""
+
msgid "Created At"
msgstr "创建于"
@@ -6335,6 +6547,9 @@ msgstr "项目下拉列表过滤器"
msgid "CycleAnalytics|stage dropdown"
msgstr "阶段下拉列表"
+msgid "DAG"
+msgstr ""
+
msgid "DNS"
msgstr "DNS"
@@ -6695,6 +6910,9 @@ msgstr "部署密钥"
msgid "Deploy key was successfully updated."
msgstr "部署密钥已æˆåŠŸæ›´æ–°ã€‚"
+msgid "Deploy keys allow read-only or read-write (if enabled) access to your repository. Deploy keys can be used for CI, staging or production servers. You can create a deploy key or add an existing one."
+msgstr ""
+
msgid "Deploy progress not found. To see pods, ensure your environment matches %{linkStart}deploy board criteria%{linkEnd}."
msgstr "未找到部署进度。è¦æŸ¥çœ‹pod,请确ä¿æ‚¨çš„环境符åˆ%{linkStart}部署看æ¿æ¡ä»¶%{linkEnd}。"
@@ -6848,6 +7066,9 @@ msgstr "已部署到"
msgid "Deploying to"
msgstr "正在部署到"
+msgid "Deployment Frequency"
+msgstr ""
+
msgid "Deployment|API"
msgstr "API"
@@ -7365,9 +7586,6 @@ msgstr "编辑æè¿°ä¿¡æ¯"
msgid "Edit environment"
msgstr "编辑环境"
-msgid "Edit epic description"
-msgstr "编辑å²è¯—æè¿°"
-
msgid "Edit file"
msgstr "编辑文件"
@@ -7989,9 +8207,6 @@ msgstr "å²è¯—事件"
msgid "Epics"
msgstr "å²è¯—"
-msgid "Epics (Ultimate / Gold license only)"
-msgstr "å²è¯—(仅旗舰版/金牌许å¯)"
-
msgid "Epics Roadmap"
msgstr "å²è¯—路线图"
@@ -8001,6 +8216,9 @@ msgstr "å²è¯—和议题"
msgid "Epics let you manage your portfolio of projects more efficiently and with less effort"
msgstr "有效利用å²è¯—,您的产å“线管ç†ä¼šå˜å¾—æ›´è½»æ¾é«˜æ•ˆ"
+msgid "Epics, Issues, and Merge Requests"
+msgstr ""
+
msgid "Epics|Add an epic"
msgstr "添加一个å²è¯—"
@@ -8052,6 +8270,9 @@ msgstr "获å–å­å²è¯—时出错。"
msgid "Epics|Something went wrong while fetching group epics."
msgstr "获å–群组å²è¯—时出错。"
+msgid "Epics|Something went wrong while moving item."
+msgstr ""
+
msgid "Epics|Something went wrong while ordering item."
msgstr "排åºæ—¶å‡ºé”™ã€‚"
@@ -8106,6 +8327,9 @@ msgstr "获å–标记时出错。"
msgid "Error fetching network graph."
msgstr "获å–网络图时出错。"
+msgid "Error fetching payload data."
+msgstr ""
+
msgid "Error fetching projects"
msgstr "获å–项目时出错"
@@ -8115,9 +8339,6 @@ msgstr "获å–refs时出错。"
msgid "Error fetching the dependency list. Please check your network connection and try again."
msgstr "获å–ä¾èµ–列表时出错。请检查您的网络连接,然åŽé‡è¯•ã€‚"
-msgid "Error fetching usage ping data."
-msgstr "获å–使用情况(usage ping)æ•°æ®æ—¶å‡ºé”™ã€‚"
-
msgid "Error loading branch data. Please try again."
msgstr "加载分支数æ®å¤±è´¥ï¼Œè¯·é‡è¯•ã€‚"
@@ -8259,6 +8480,9 @@ msgstr "è¦å¯ç”¨é€‰æ‹©çš„项目,请输入有效的验è¯ä»¤ç‰Œ"
msgid "Errors"
msgstr "错误"
+msgid "Errors:"
+msgstr ""
+
msgid "Estimated"
msgstr "预计"
@@ -8463,12 +8687,18 @@ msgstr "æœç´¢å…¬å¼€ç¾¤ç»„"
msgid "Export as CSV"
msgstr "导出为 CSV"
+msgid "Export group"
+msgstr ""
+
msgid "Export issues"
msgstr "导出议题"
msgid "Export project"
msgstr "导出项目"
+msgid "Export this group with all related data to a new GitLab instance. Once complete, you can import the data file from the \"New Group\" page."
+msgstr ""
+
msgid "Export this project with all its related data in order to move your project to a new GitLab instance. Once the export is finished, you can import the file from the \"New Project\" page."
msgstr "导出此项目åŠå…¶æ‰€æœ‰ç›¸å…³æ•°æ®ï¼Œä»¥ä¾¿å°†é¡¹ç›®ç§»åŠ¨åˆ°æ–°çš„GitLab实例。导出完æˆåŽï¼Œæ‚¨å¯ä»¥ä»Žâ€œæ–°å»ºé¡¹ç›®â€é¡µé¢å¯¼å…¥æ–‡ä»¶ã€‚"
@@ -8538,9 +8768,6 @@ msgstr "已失败"
msgid "Failed Jobs"
msgstr "失败的作业"
-msgid "Failed create wiki"
-msgstr "创建 Wiki 失败"
-
msgid "Failed to add a Zoom meeting"
msgstr "无法添加Zoom会议"
@@ -8580,6 +8807,9 @@ msgstr ""
msgid "Failed to create resources"
msgstr "创建资æºå¤±è´¥"
+msgid "Failed to create wiki"
+msgstr ""
+
msgid "Failed to delete board. Please try again."
msgstr "删除看æ¿å¤±è´¥ã€‚请é‡è¯•ã€‚"
@@ -8589,7 +8819,7 @@ msgstr "无法部署到"
msgid "Failed to enqueue the rebase operation, possibly due to a long-lived transaction. Try again later."
msgstr "无法将rebaseæ“作加入队列,å¯èƒ½æ˜¯ç”±äºŽäº‹åŠ¡æ—¶é—´è¿‡é•¿ã€‚ç¨åŽå†è¯•ã€‚"
-msgid "Failed to find import label for jira import."
+msgid "Failed to find import label for Jira import."
msgstr ""
msgid "Failed to get ref."
@@ -8613,6 +8843,12 @@ msgstr ""
msgid "Failed to load groups & users."
msgstr "加载群组和用户失败。"
+msgid "Failed to load labels. Please try again."
+msgstr ""
+
+msgid "Failed to load milestones. Please try again."
+msgstr ""
+
msgid "Failed to load related branches"
msgstr "加载相关分支失败"
@@ -8988,8 +9224,11 @@ msgstr "按æ交消æ¯è¿‡æ»¤"
msgid "Filter by milestone name"
msgstr "按里程碑å称过滤"
-msgid "Filter by name..."
-msgstr "按å称过滤..."
+msgid "Filter by name"
+msgstr ""
+
+msgid "Filter by status"
+msgstr ""
msgid "Filter by two-factor authentication"
msgstr "按åŒé‡è®¤è¯è¿‡æ»¤"
@@ -9000,12 +9239,18 @@ msgstr "按用户筛选"
msgid "Filter projects"
msgstr "筛选项目"
+msgid "Filter results"
+msgstr ""
+
msgid "Filter results by group"
msgstr "按群组过滤结果"
msgid "Filter results by project"
msgstr "按项目过滤结果"
+msgid "Filter results..."
+msgstr ""
+
msgid "Filter your projects by name"
msgstr "按å称过滤您的项目"
@@ -9294,12 +9539,12 @@ msgstr "附件"
msgid "GeoNodes|Checksummed"
msgstr "已校验"
+msgid "GeoNodes|Consult Geo troubleshooting information"
+msgstr ""
+
msgid "GeoNodes|Container repositories"
msgstr "容器仓库"
-msgid "GeoNodes|Data is out of date from %{timeago}"
-msgstr "æ•°æ®ä»Ž %{timeago} 起过期"
-
msgid "GeoNodes|Data replication lag"
msgstr "æ•°æ®åº“åŒæ­¥æ»žåŽ"
@@ -9339,6 +9584,9 @@ msgstr "游标处ç†çš„最åŽäº‹ä»¶ID"
msgid "GeoNodes|Last event ID seen from primary"
msgstr "主节点中最新的事件ID"
+msgid "GeoNodes|Learn more about Geo node statuses"
+msgstr ""
+
msgid "GeoNodes|Loading nodes"
msgstr "载入节点"
@@ -9354,6 +9602,9 @@ msgstr "节点URL"
msgid "GeoNodes|Node was successfully removed."
msgstr "æˆåŠŸåˆ é™¤èŠ‚点。"
+msgid "GeoNodes|Node's status was updated %{timeAgo}."
+msgstr ""
+
msgid "GeoNodes|Not checksummed"
msgstr "未校验"
@@ -9384,8 +9635,11 @@ msgstr "仓库校验和进度"
msgid "GeoNodes|Repository verification progress"
msgstr "仓库验è¯è¿›åº¦"
-msgid "GeoNodes|Selective"
-msgstr "选择性"
+msgid "GeoNodes|Selective (%{syncLabel})"
+msgstr ""
+
+msgid "GeoNodes|Selective synchronization"
+msgstr ""
msgid "GeoNodes|Something went wrong while changing node status"
msgstr "更改节点状æ€æ—¶å‘生错误"
@@ -9411,6 +9665,9 @@ msgstr "未使用的槽"
msgid "GeoNodes|Unverified"
msgstr "未验è¯"
+msgid "GeoNodes|Updated %{timeAgo}"
+msgstr ""
+
msgid "GeoNodes|Used slots"
msgstr "已使用的槽"
@@ -9450,17 +9707,17 @@ msgstr "%{name}已计划é‡æ–°éªŒè¯"
msgid "Geo|All"
msgstr "全部"
+msgid "Geo|All %{replicable_type}"
+msgstr ""
+
msgid "Geo|All projects"
msgstr "所有项目"
-msgid "Geo|All projects are being scheduled for re-sync"
-msgstr "正在计划所有项目é‡æ–°åŒæ­¥"
-
-msgid "Geo|All projects are being scheduled for re-verify"
-msgstr "所有项目正在计划é‡æ–°éªŒè¯"
+msgid "Geo|All projects are being scheduled for resync"
+msgstr ""
-msgid "Geo|Batch operations"
-msgstr "批é‡æ“作"
+msgid "Geo|All projects are being scheduled for reverify"
+msgstr ""
msgid "Geo|Could not remove tracking entry for an existing project."
msgstr "无法删除现有项目的跟踪æ¡ç›®ã€‚"
@@ -9471,9 +9728,15 @@ msgstr "无法删除现有上传的跟踪æ¡ç›®ã€‚"
msgid "Geo|Failed"
msgstr "失败"
+msgid "Geo|Filter by status"
+msgstr ""
+
msgid "Geo|Geo Status"
msgstr "Geo状æ€"
+msgid "Geo|In progress"
+msgstr ""
+
msgid "Geo|In sync"
msgstr "å·²åŒæ­¥"
@@ -9498,9 +9761,6 @@ msgstr "下一次åŒæ­¥å®‰æŽ’在"
msgid "Geo|Not synced yet"
msgstr "尚未åŒæ­¥"
-msgid "Geo|Pending"
-msgstr "待定"
-
msgid "Geo|Pending synchronization"
msgstr "å¾…åŒæ­¥"
@@ -9537,8 +9797,8 @@ msgstr ""
msgid "Geo|Resync"
msgstr "é‡æ–°åŒæ­¥"
-msgid "Geo|Resync all projects"
-msgstr "é‡æ–°åŒæ­¥æ‰€æœ‰é¡¹ç›®"
+msgid "Geo|Resync all"
+msgstr ""
msgid "Geo|Retry count"
msgstr "é‡è¯•è®¡æ•°"
@@ -9546,8 +9806,8 @@ msgstr "é‡è¯•è®¡æ•°"
msgid "Geo|Reverify"
msgstr "é‡æ–°æ ¡éªŒ"
-msgid "Geo|Reverify all projects"
-msgstr "é‡æ–°æ ¡éªŒæ‰€æœ‰é¡¹ç›®"
+msgid "Geo|Reverify all"
+msgstr ""
msgid "Geo|Status"
msgstr "状æ€"
@@ -9888,9 +10148,6 @@ msgstr "转到环境"
msgid "Go to file"
msgstr "转到文件"
-msgid "Go to file (MRs only)"
-msgstr "转到文件 (仅 MR)"
-
msgid "Go to file permalink (while viewing a file)"
msgstr "转到文件永久链接 (åŒæ—¶æŸ¥çœ‹æ–‡ä»¶)"
@@ -10335,6 +10592,9 @@ msgstr "ç¦ç”¨ç”µå­é‚®ä»¶é€šçŸ¥"
msgid "GroupSettings|Disable group mentions"
msgstr "ç¦ç”¨ç¾¤ç»„æåŠ"
+msgid "GroupSettings|Export group"
+msgstr ""
+
msgid "GroupSettings|If the parent group's visibility is lower than the group current visibility, visibility levels for subgroups and projects will be changed to match the new parent group's visibility."
msgstr "如果新父群组的å¯è§æ€§ä½ŽäºŽå½“å‰ç¾¤ç»„çš„å¯è§æ€§ï¼Œå­ç¾¤ç»„和项目的å¯è§åº¦å°†ä¼šæ”¹å˜ï¼Œä»¥ä¾¿ä¸Žæ–°çˆ¶ç¾¤ç»„çš„å¯è§æ€§ç›¸åŒ¹é…。"
@@ -10413,6 +10673,9 @@ msgstr "群组"
msgid "Groups (%{count})"
msgstr "群组(%{count})"
+msgid "Groups (%{groups})"
+msgstr ""
+
msgid "Groups can also be nested by creating %{subgroup_docs_link_start}subgroups%{subgroup_docs_link_end}."
msgstr "也å¯ä»¥é€šè¿‡åˆ›å»º %{subgroup_docs_link_start}å­ç¾¤ç»„æ¥åµŒå¥—群组%{subgroup_docs_link_end}。"
@@ -10676,15 +10939,12 @@ msgstr "ID"
msgid "ID:"
msgstr "ID:"
-msgid "IDE|Allow live previews of JavaScript projects in the Web IDE using CodeSandbox client side evaluation."
-msgstr "å…许Web IDE中的JavaScript项目使用CodeSandbox客户端的实时预览。"
+msgid "IDE|Allow live previews of JavaScript projects in the Web IDE using CodeSandbox Live Preview."
+msgstr ""
msgid "IDE|Back"
msgstr "返回"
-msgid "IDE|Client side evaluation"
-msgstr "客户端计算"
-
msgid "IDE|Commit"
msgstr "æ交"
@@ -10748,6 +11008,9 @@ msgstr ""
msgid "If any job surpasses this timeout threshold, it will be marked as failed. Human readable time input language is accepted like \"1 hour\". Values without specification represent seconds."
msgstr "如果任何作业超过这个超时阈值,它将被标记为失败。å¯è¾“入英文语å¥ï¼Œå¦‚ “1 hourâ€ã€‚默认å•ä½ä¸ºç§’。"
+msgid "If blank, set allowable lifetime to %{instance_level_policy_in_words}, as defined by the instance admin. Once set, existing tokens for users in this group may be revoked."
+msgstr ""
+
msgid "If checked, group owners can manage LDAP group links and LDAP member overrides"
msgstr "如选中,则群组所有者å¯ä»¥ç®¡ç†LDAP群组链接和 LDAPæˆå‘˜è¦†ç›–"
@@ -10787,6 +11050,9 @@ msgstr "如果您认为这å¯èƒ½æ˜¯ä¸€ä¸ªé”™è¯¯ï¼Œè¯·å‚阅%{linkStart}GEOæ•…éšœ
msgid "If you lose your recovery codes you can generate new ones, invalidating all previous codes."
msgstr "如果您丢失了æ¢å¤ç ï¼Œæ‚¨å¯ä»¥ç”Ÿæˆæ–°çš„æ¢å¤ç ï¼Œæ‰€æœ‰ä»¥å‰çš„æ¢å¤ç å°†å¤±æ•ˆã€‚"
+msgid "If you reach 100%% storage capacity, you will not be able to: %{base_message}"
+msgstr ""
+
msgid "If your HTTP repository is not publicly accessible, add your credentials."
msgstr "如果您的HTTP仓库无法公开访问,需在地å€ä¸­æ·»åŠ å‡­æ®ã€‚"
@@ -10898,6 +11164,9 @@ msgstr "从 GitHub 导入仓库"
msgid "Import repository"
msgstr "导入仓库"
+msgid "Import started by: %{importInitiator}"
+msgstr ""
+
msgid "Import tasks"
msgstr "导入任务"
@@ -10964,6 +11233,9 @@ msgstr ""
msgid "In order to tailor your experience with GitLab we<br>would like to know a bit more about you."
msgstr "为了能é‡èº«å®šåˆ¶æ‚¨åœ¨GitLab的体验,我们<br>希望对您有更多了解。"
+msgid "In progress"
+msgstr ""
+
msgid "In the next step, you'll be able to select the projects you want to import."
msgstr "继续下一步,选择想è¦å¯¼å…¥çš„项目"
@@ -11310,15 +11582,15 @@ msgstr "YoutTrack议题跟踪器"
msgid "Issues"
msgstr "议题"
-msgid "Issues / Merge Requests"
-msgstr "议题/åˆå¹¶è¯·æ±‚"
-
msgid "Issues Analytics"
msgstr ""
msgid "Issues Rate Limits"
msgstr ""
+msgid "Issues and Merge Requests"
+msgstr ""
+
msgid "Issues can be bugs, tasks or ideas to be discussed. Also, issues are searchable and filterable."
msgstr "议题å¯ä»¥æ˜¯ç¼ºé™·ï¼Œä»»åŠ¡æˆ–è¦è®¨è®ºçš„想法。此外,å¯ä»¥é€šè¿‡æœç´¢å’Œè¿‡æ»¤æ¥æŸ¥æ‰¾è®®é¢˜ã€‚"
@@ -11340,11 +11612,11 @@ msgstr "在您为项目创建议题åŽï¼Œæˆ‘们就会开始跟踪并显示它们
msgid "IssuesAnalytics|Avg/Month:"
msgstr "月å‡ï¼š"
-msgid "IssuesAnalytics|Issues created"
-msgstr "创建的议题数"
+msgid "IssuesAnalytics|Issues opened"
+msgstr ""
-msgid "IssuesAnalytics|Issues created per month"
-msgstr "æ¯æœˆåˆ›å»ºçš„议题"
+msgid "IssuesAnalytics|Issues opened per month"
+msgstr ""
msgid "IssuesAnalytics|Last 12 months"
msgstr "最近12个月"
@@ -11397,6 +11669,9 @@ msgstr ""
msgid "Jira integration not configured."
msgstr ""
+msgid "Jira project: %{importProject}"
+msgstr ""
+
msgid "JiraService|Events for %{noteable_model_name} are disabled."
msgstr "%{noteable_model_name} 事件已ç¦ç”¨ã€‚"
@@ -11785,6 +12060,9 @@ msgstr "此分支上最近æ交的最新æµæ°´çº¿"
msgid "Lead"
msgstr "最高"
+msgid "Lead Time"
+msgstr ""
+
msgid "Learn GitLab"
msgstr "学习GitLab"
@@ -11797,6 +12075,9 @@ msgstr "了解如何 %{link_start}贡献到内置的模æ¿%{link_end}"
msgid "Learn how to %{no_packages_link_start}publish and share your packages%{no_packages_link_end} with GitLab."
msgstr "了解GitLab如何 %{no_packages_link_start}å‘布和共享您的包%{no_packages_link_end}。"
+msgid "Learn how to enable synchronization"
+msgstr ""
+
msgid "Learn more"
msgstr "进一步了解"
@@ -12123,9 +12404,6 @@ msgstr "列出您的 Bitbucket 库"
msgid "Live preview"
msgstr "实时预览"
-msgid "Load more vulnerabilities"
-msgstr ""
-
msgid "Loading"
msgstr ""
@@ -12276,9 +12554,6 @@ msgstr "字符串格å¼é”™è¯¯"
msgid "Manage"
msgstr "管ç†"
-msgid "Manage Git repositories with fine-grained access controls that keep your code secure. Perform code reviews and enhance collaboration with merge requests. Each project can also have an issue tracker and a wiki."
-msgstr "通过细粒度的访问控制æ¥ç®¡ç†Git仓库,确ä¿æ‚¨çš„代ç å®‰å…¨ã€‚执行代ç å®¡æŸ¥å¹¶é€šè¿‡åˆå¹¶è¯·æ±‚的实现更紧密的开å‘å作。æ¯ä¸ªé¡¹ç›®è¿˜å¯ä»¥é…置议题跟踪和wiki。"
-
msgid "Manage Web IDE features"
msgstr "管ç†Web IDE功能"
@@ -12309,6 +12584,9 @@ msgstr "管ç†åŒé‡è®¤è¯"
msgid "Manage your license"
msgstr ""
+msgid "Managed Account"
+msgstr ""
+
msgid "Manifest"
msgstr "Manifest"
@@ -12573,6 +12851,9 @@ msgstr "创建åˆå¹¶è¯·æ±‚"
msgid "Merge Requests in Review"
msgstr "评审中的åˆå¹¶è¯·æ±‚"
+msgid "Merge automatically (%{strategy})"
+msgstr ""
+
msgid "Merge commit message"
msgstr "åˆå¹¶æ交消æ¯"
@@ -12726,6 +13007,12 @@ msgstr "å·²åˆå¹¶"
msgid "Merged branches are being deleted. This can take some time depending on the number of branches. Please refresh the page to see changes."
msgstr "å·²åˆå¹¶åˆ†æ”¯æ­£åœ¨è¢«åˆ é™¤ã€‚该æ“作å¯èƒ½éœ€è¦ä¸€äº›æ—¶é—´ï¼Œå…·ä½“å–决于分支的数é‡ã€‚请刷新页é¢ä»¥æŸ¥çœ‹æ›´æ–°ã€‚"
+msgid "Merged this merge request."
+msgstr ""
+
+msgid "Merges this merge request immediately."
+msgstr ""
+
msgid "Merges this merge request when the pipeline succeeds."
msgstr "当æµæ°´çº¿æˆåŠŸæ—¶åˆå¹¶æ­¤åˆå¹¶è¯·æ±‚。"
@@ -12832,12 +13119,15 @@ msgid "Metrics|Edit metric"
msgid_plural "Metrics|Edit metrics"
msgstr[0] ""
-msgid "Metrics|Environment"
-msgstr "环境"
+msgid "Metrics|Expand panel"
+msgstr ""
msgid "Metrics|For grouping similar metrics"
msgstr "用于分组类似指标"
+msgid "Metrics|Go back (Esc)"
+msgstr ""
+
msgid "Metrics|Invalid time range, please verify."
msgstr ""
@@ -12868,9 +13158,6 @@ msgstr "Prometheus查询文档"
msgid "Metrics|Refresh dashboard"
msgstr ""
-msgid "Metrics|Show last"
-msgstr "显示最近"
-
msgid "Metrics|There was an error creating the dashboard."
msgstr "创建仪表æ¿æ—¶å‡ºé”™ã€‚"
@@ -12949,6 +13236,9 @@ msgstr "例如:req / sec"
msgid "Microsoft Azure"
msgstr "Microsoft Azure"
+msgid "Middleman project with Static Site Editor support"
+msgstr ""
+
msgid "Migrated %{success_count}/%{total_count} files."
msgstr "å·²è¿ç§» %{success_count}/%{total_count} 文件。"
@@ -13392,11 +13682,11 @@ msgstr "默认情况下,新注册的用户将是外部用户"
msgid "Next"
msgstr "预览版"
-msgid "Next file in diff (MRs only)"
-msgstr "差异中的下一个文件(仅MR)"
+msgid "Next file in diff"
+msgstr ""
-msgid "Next unresolved discussion (MRs only)"
-msgstr "下一个未解决的讨论(仅é™MR)"
+msgid "Next unresolved discussion"
+msgstr ""
msgid "Nickname"
msgstr "昵称"
@@ -13455,6 +13745,9 @@ msgstr "æ— å˜æ›´å†…容"
msgid "No changes between %{ref_start}%{source_branch}%{ref_end} and %{ref_start}%{target_branch}%{ref_end}"
msgstr "%{ref_start}%{source_branch}%{ref_end} å’Œ %{ref_start}%{target_branch}%{ref_end} 之间没有产生å˜åŒ–"
+msgid "No child epics match applied filters"
+msgstr ""
+
msgid "No connection could be made to a Gitaly Server, please check your logs!"
msgstr "无法连接到GitalyæœåŠ¡å™¨ï¼Œè¯·æ£€æŸ¥ç›¸å…³æ—¥å¿—ï¼"
@@ -13593,15 +13886,6 @@ msgstr ""
msgid "No thanks, don't show this again"
msgstr ""
-msgid "No vulnerabilities found for this group"
-msgstr "在此群组中未å‘现æ¼æ´ž"
-
-msgid "No vulnerabilities found for this pipeline"
-msgstr "在此æµæ°´çº¿ä¸­æœªå‘现æ¼æ´ž"
-
-msgid "No vulnerabilities found for this project"
-msgstr "在此项目中未å‘现æ¼æ´ž"
-
msgid "No vulnerabilities present"
msgstr "æ— æ¼æ´ž"
@@ -13725,6 +14009,9 @@ msgstr ""
msgid "Nothing to preview."
msgstr "没有å¯é¢„览的内容。"
+msgid "Nothing to synchronize"
+msgstr ""
+
msgid "Notification events"
msgstr "通知事件"
@@ -13896,6 +14183,9 @@ msgstr "仓库导入åŽï¼Œå¯ä»¥é€šè¿‡SSH进行镜åƒã€‚点击%{link_start}æ­¤å¤
msgid "Once removed, the fork relationship cannot be restored and you will no longer be able to send merge requests to the source."
msgstr "删除åŽå°†æ— æ³•æ¢å¤æ´¾ç”Ÿå…³ç³»ï¼Œæ‚¨å°†æ— æ³•å†å‘æºé¡¹ç›®å‘é€åˆå¹¶è¯·æ±‚。"
+msgid "Once the exported file is ready you can download it from this page."
+msgstr ""
+
msgid "Once the exported file is ready, you will receive a notification email with a download link, or you can download it from this page."
msgstr "导出的文件准备就绪åŽï¼Œæ‚¨å°†æ”¶åˆ°å¸¦æœ‰ä¸‹è½½é“¾æŽ¥çš„通知电å­é‚®ä»¶ï¼Œæˆ–者您å¯ä»¥ä»Žæ­¤é¡µé¢ä¸‹è½½ã€‚"
@@ -13996,9 +14286,6 @@ msgstr "打开原始文件"
msgid "Open sidebar"
msgstr "打开侧边æ "
-msgid "Open source software to collaborate on code"
-msgstr "用于代ç å¼€å‘å作的开æºè½¯ä»¶"
-
msgid "Open: %{openIssuesCount}"
msgstr ""
@@ -14101,6 +14388,9 @@ msgstr ""
msgid "OutdatedBrowser|You can provide feedback %{feedback_link_start}on this issue%{feedback_link_end} or via your usual support channels."
msgstr ""
+msgid "Overridden"
+msgstr ""
+
msgid "Overview"
msgstr "概览"
@@ -14152,6 +14442,9 @@ msgstr ""
msgid "PackageRegistry|Conan Command"
msgstr "Conan命令"
+msgid "PackageRegistry|Copy .pypirc content"
+msgstr ""
+
msgid "PackageRegistry|Copy Conan Command"
msgstr "å¤åˆ¶Conan命令"
@@ -14173,6 +14466,9 @@ msgstr ""
msgid "PackageRegistry|Copy NuGet Setup Command"
msgstr ""
+msgid "PackageRegistry|Copy Pip command"
+msgstr ""
+
msgid "PackageRegistry|Copy and paste this inside your %{codeStart}pom.xml%{codeEnd} %{codeStart}dependencies%{codeEnd} block."
msgstr "将其å¤åˆ¶å¹¶ç²˜è´´åˆ°æ‚¨çš„%{codeStart}pom.xml%{codeEnd}文件的%{codeStart}dependencies%{codeEnd}å—中。"
@@ -14206,6 +14502,12 @@ msgstr "有关Maven的注册表的详细信æ¯ï¼Œ%{linkStart}请å‚阅文档%{li
msgid "PackageRegistry|For more information on the NuGet registry, %{linkStart}see the documentation%{linkEnd}."
msgstr ""
+msgid "PackageRegistry|For more information on the PyPi registry, %{linkStart}see the documentation%{linkEnd}."
+msgstr ""
+
+msgid "PackageRegistry|If you haven't already done so, you will need to add the below to your %{codeStart}.pypirc%{codeEnd} file."
+msgstr ""
+
msgid "PackageRegistry|If you haven't already done so, you will need to add the below to your %{codeStart}pom.xml%{codeEnd} file."
msgstr "如果尚未é…置,需è¦å°†ä»¥ä¸‹å†…容添加到%{codeStart}pom.xml%{codeEnd}文件中。"
@@ -14215,6 +14517,9 @@ msgstr "安装"
msgid "PackageRegistry|Learn how to %{noPackagesLinkStart}publish and share your packages%{noPackagesLinkEnd} with GitLab."
msgstr "了解GitLab如何%{noPackagesLinkStart}å‘布和共享您的软件包%{noPackagesLinkEnd}。"
+msgid "PackageRegistry|Manually Published"
+msgstr ""
+
msgid "PackageRegistry|Maven"
msgstr ""
@@ -14233,12 +14538,18 @@ msgstr ""
msgid "PackageRegistry|NuGet Command"
msgstr ""
+msgid "PackageRegistry|Pip Command"
+msgstr ""
+
msgid "PackageRegistry|Pipeline %{linkStart}%{linkEnd} triggered %{timestamp} by %{author}"
msgstr ""
msgid "PackageRegistry|Published to the repository at %{timestamp}"
msgstr ""
+msgid "PackageRegistry|PyPi"
+msgstr ""
+
msgid "PackageRegistry|Registry Setup"
msgstr "é•œåƒåº“设置"
@@ -14275,6 +14586,9 @@ msgstr ""
msgid "PackageRegistry|npm"
msgstr "npm"
+msgid "PackageRegistry|published by %{author}"
+msgstr ""
+
msgid "PackageRegistry|yarn"
msgstr "yarn"
@@ -14290,6 +14604,9 @@ msgstr ""
msgid "PackageType|NuGet"
msgstr ""
+msgid "PackageType|PyPi"
+msgstr ""
+
msgid "Packages"
msgstr "软件包"
@@ -14383,6 +14700,9 @@ msgstr "密ç æ›´æ–°æˆåŠŸã€‚请登录"
msgid "Past due"
msgstr "逾期"
+msgid "Paste a machine public key here. Read more about how to generate it"
+msgstr ""
+
msgid "Paste a machine public key here. Read more about how to generate it %{link_start}here%{link_end}"
msgstr "在此处粘贴计算机公钥。在%{link_start}这里%{link_end}了解更多关于如何产生公钥"
@@ -14398,9 +14718,6 @@ msgstr "粘贴您的SSH公钥,通常包å«åœ¨æ–‡ä»¶'~/.ssh/id_ed25519.pub'或'
msgid "Path"
msgstr "路径"
-msgid "Path, transfer, remove"
-msgstr "路径,转移,删除"
-
msgid "Path:"
msgstr "路径:"
@@ -14431,8 +14748,8 @@ msgstr ""
msgid "Percentage"
msgstr ""
-msgid "Perform advanced options such as changing path, transferring, or removing the group."
-msgstr "执行高级选项,例如更改路径,移动或删除群组。"
+msgid "Perform advanced options such as changing path, transferring, exporting, or removing the group."
+msgstr ""
msgid "Perform common operations on GitLab project"
msgstr "在GitLab项目上执行常è§æ“作"
@@ -14512,9 +14829,6 @@ msgstr "æµæ°´çº¿è®¡åˆ’"
msgid "Pipeline minutes quota"
msgstr "æµæ°´çº¿åˆ†é’Ÿæ•°é…é¢"
-msgid "Pipeline quota"
-msgstr "æµæ°´çº¿é…é¢"
-
msgid "Pipeline subscriptions"
msgstr "æµæ°´çº¿è®¢é˜…"
@@ -14524,6 +14838,9 @@ msgstr "æµæ°´çº¿è§¦å‘器"
msgid "Pipeline: %{status}"
msgstr "æµæ°´çº¿: %{status}"
+msgid "PipelineCharts|CI / CD Analytics"
+msgstr ""
+
msgid "PipelineCharts|Failed:"
msgstr "失败:"
@@ -14602,15 +14919,6 @@ msgstr "åˆå¹¶è¯·æ±‚çš„æµæ°´çº¿å·²é…置。游离的æµæ°´çº¿è¿è¡ŒäºŽåˆå¹¶è¯·
msgid "Pipelines settings for '%{project_name}' were successfully updated."
msgstr "“%{project_name}â€çš„æµæ°´çº¿è®¾ç½®å·²æˆåŠŸæ›´æ–°ã€‚"
-msgid "Pipelines| to purchase more minutes."
-msgstr "以购买更多的分钟数。"
-
-msgid "Pipelines|%{namespace_name} has exceeded its pipeline minutes quota."
-msgstr "%{namespace_name}å·²ç»è¶…过其æµæ°´çº¿æ—¶é—´é…é¢ã€‚"
-
-msgid "Pipelines|%{namespace_name} has less than %{notification_level}%% of CI minutes available."
-msgstr "%{namespace_name}çš„å¯ç”¨CI分钟数已少于%{notification_level}%%。"
-
msgid "Pipelines|API"
msgstr "API"
@@ -14632,12 +14940,15 @@ msgstr "æŒç»­é›†æˆå¯ä»¥é€šè¿‡è‡ªåŠ¨è¿è¡Œæµ‹è¯•æ¥å¸®åŠ©æ£€æµ‹ä»£ç ç¼ºé™·ï¼Œ
msgid "Pipelines|Get started with Pipelines"
msgstr "æµæ°´çº¿å…¥é—¨"
+msgid "Pipelines|Group %{namespace_name} has %{percentage}%% or less Shared Runner Pipeline minutes remaining. Once it runs out, no new jobs or pipelines in its projects will run."
+msgstr ""
+
+msgid "Pipelines|Group %{namespace_name} has exceeded its pipeline minutes quota. Unless you buy additional pipeline minutes, no new jobs or pipelines in its projects will run."
+msgstr ""
+
msgid "Pipelines|Loading Pipelines"
msgstr "载入æµæ°´çº¿"
-msgid "Pipelines|Pipelines will not run anymore on shared Runners."
-msgstr "æµæ°´çº¿å°†ä¸å†åœ¨å…±äº«çš„Runners上è¿è¡Œã€‚"
-
msgid "Pipelines|Project cache successfully reset."
msgstr "项目缓存é‡ç½®æˆåŠŸã€‚"
@@ -14866,6 +15177,9 @@ msgstr "请å†æ¬¡è¾“入电å­é‚®ä»¶åœ°å€ã€‚"
msgid "Please select"
msgstr "请选择"
+msgid "Please select a Jira project"
+msgstr ""
+
msgid "Please select a country"
msgstr "请选择国家/地区"
@@ -14911,6 +15225,9 @@ msgstr "连接代ç ä»“库中,请ç¨å€™ã€‚å¯åœ¨ä»»æ„时刻刷新以获å–当
msgid "Please wait while we import the repository for you. Refresh at will."
msgstr "导入代ç ä»“库中,请ç¨å€™ã€‚å¯åœ¨ä»»æ„时刻刷新以获å–当å‰çŠ¶æ€ã€‚"
+msgid "Plugins directory is deprecated and will be removed in 14.0. Please move this file into /file_hooks directory."
+msgstr ""
+
msgid "Pod does not exist"
msgstr ""
@@ -15028,7 +15345,7 @@ msgstr "防止环境自动终止"
msgid "Prevent users from changing their profile name"
msgstr "ç¦æ­¢ç”¨æˆ·æ›´æ”¹é…置文件å称"
-msgid "Prevent users from modifing merge request approvers list"
+msgid "Prevent users from modifying merge request approvers list"
msgstr ""
msgid "Prevent users from performing write operations on GitLab while performing maintenance."
@@ -15049,11 +15366,11 @@ msgstr "预览上传数æ®"
msgid "Previous Artifacts"
msgstr "å‰ä¸€ä¸ªäº§ç‰©"
-msgid "Previous file in diff (MRs only)"
-msgstr "差异中的上一个文件(仅MR)"
+msgid "Previous file in diff"
+msgstr ""
-msgid "Previous unresolved discussion (MRs only)"
-msgstr "上一个未解决的讨论(仅é™MR)"
+msgid "Previous unresolved discussion"
+msgstr ""
msgid "Primary"
msgstr "主è¦"
@@ -15085,6 +15402,9 @@ msgstr "ç§æœ‰ç¾¤ç»„"
msgid "Private profile"
msgstr "éžå…¬å¼€èµ„æ–™"
+msgid "Private projects Minutes cost factor"
+msgstr ""
+
msgid "Private projects can be created in your personal namespace with:"
msgstr "ç§æœ‰é¡¹ç›®å¯ä»¥åœ¨ä¸ªäººå称空间中创建:"
@@ -15913,6 +16233,9 @@ msgstr "Wiki"
msgid "ProjectSettings|With GitLab Pages you can host your static websites on GitLab"
msgstr "使用GitLab Pages,您å¯ä»¥åœ¨GitLab上托管é™æ€ç½‘ç«™"
+msgid "ProjectSettings|With Metrics Dashboard you can visualize this project performance metrics"
+msgstr ""
+
msgid "ProjectTemplates|.NET Core"
msgstr ".NET Core"
@@ -15976,6 +16299,9 @@ msgstr "Serverless Framework/JS"
msgid "ProjectTemplates|Spring"
msgstr "Spring"
+msgid "ProjectTemplates|Static Site Editor/Middleman"
+msgstr ""
+
msgid "ProjectTemplates|iOS (Swift)"
msgstr "iOS (Swift)"
@@ -16195,15 +16521,15 @@ msgstr "Prometheus API 地å€ï¼Œä¾‹å¦‚ http://prometheus.example.com/"
msgid "PrometheusService|Prometheus is being automatically managed on your clusters"
msgstr "Prometheus正在被群集自动管ç†"
+msgid "PrometheusService|Select the Active checkbox to override the Auto Configuration with custom settings. If unchecked, Auto Configuration settings are used."
+msgstr ""
+
msgid "PrometheusService|These metrics will only be monitored after your first deployment to an environment"
msgstr "在首次部署到环境之åŽ, 这些指标æ‰ä¼šè¢«ç›‘控"
msgid "PrometheusService|Time-series monitoring service"
msgstr "以时间为åºçš„监控æœåŠ¡"
-msgid "PrometheusService|To enable manual configuration, uninstall Prometheus from your clusters"
-msgstr "如需å¯ç”¨æ‰‹åŠ¨é…置,请从群集中å¸è½½Prometheus"
-
msgid "PrometheusService|To enable the installation of Prometheus on your clusters, deactivate the manual configuration below"
msgstr "如需在群集上å¯ç”¨Prometheus的安装,请å–消以下的手动é…ç½®"
@@ -16387,6 +16713,9 @@ msgstr "环境已ç»ä¸è¢«ä¿æŠ¤"
msgid "Protip:"
msgstr "æ示:"
+msgid "Protocol"
+msgstr ""
+
msgid "Provider"
msgstr "æ供者"
@@ -16411,6 +16740,9 @@ msgstr "公共部署密钥(%{deploy_keys_count})"
msgid "Public pipelines"
msgstr "公开æµæ°´çº¿"
+msgid "Public projects Minutes cost factor"
+msgstr ""
+
msgid "Pull"
msgstr "拉å–"
@@ -16610,6 +16942,9 @@ msgid "Refreshing in a second to show the updated status..."
msgid_plural "Refreshing in %d seconds to show the updated status..."
msgstr[0] "%d 秒åŽåˆ·æ–°ä»¥æ˜¾ç¤ºæ›´æ–°çŠ¶æ€..."
+msgid "Regenerate export"
+msgstr ""
+
msgid "Regenerate instance ID"
msgstr "é‡æ–°ç”Ÿæˆå®žä¾‹ID"
@@ -16986,9 +17321,28 @@ msgstr "报告"
msgid "Reports|%{combinedString} and %{resolvedString}"
msgstr ""
+msgid "Reports|Accessibility scanning detected %d issue for the source branch only"
+msgid_plural "Reports|Accessibility scanning detected %d issues for the source branch only"
+msgstr[0] ""
+
+msgid "Reports|Accessibility scanning detected no issues for the source branch only"
+msgstr ""
+
+msgid "Reports|Accessibility scanning failed loading results"
+msgstr ""
+
+msgid "Reports|Accessibility scanning results are being parsed"
+msgstr ""
+
msgid "Reports|Actions"
msgstr "æ“作"
+msgid "Reports|An error occured while loading report"
+msgstr ""
+
+msgid "Reports|An error occurred while loading %{name} results"
+msgstr ""
+
msgid "Reports|Class"
msgstr "ç±»"
@@ -17094,8 +17448,8 @@ msgstr "链接SAMLå¸æˆ·çš„请求必须ç»è¿‡æŽˆæƒ"
msgid "Requested %{time_ago}"
msgstr "请求的 %{time_ago}"
-msgid "Requested design version does not exist"
-msgstr "请求的设计版本ä¸å­˜åœ¨"
+msgid "Requested design version does not exist."
+msgstr ""
msgid "Requested states are invalid"
msgstr ""
@@ -17276,6 +17630,9 @@ msgstr "æ¢å¤å¤åˆ¶"
msgid "Resync"
msgstr "é‡æ–°åŒæ­¥"
+msgid "Resync all"
+msgstr ""
+
msgid "Resync all %{replicableType}"
msgstr ""
@@ -17343,6 +17700,9 @@ msgstr "撤销身份模拟令牌 %{token_name}ï¼"
msgid "Revoked personal access token %{personal_access_token_name}!"
msgstr "撤销个人访问令牌 %{personal_access_token_name}ï¼"
+msgid "Revoked project access token %{project_access_token_name}!"
+msgstr ""
+
msgid "RightSidebar|adding a"
msgstr "添加"
@@ -17391,6 +17751,9 @@ msgstr "Runnerå°†è¿è¡ŒæŒ‡å®šé¡¹ç›®çš„作业"
msgid "Runner token"
msgstr "Runner 令牌"
+msgid "Runner tokens"
+msgstr ""
+
msgid "Runner was not updated."
msgstr "Runner未更新。"
@@ -17445,6 +17808,9 @@ msgstr "SAML SSO(å•ç‚¹ç™»å½•)"
msgid "SAML SSO for %{group_name}"
msgstr "群组%{group_name} 的 SAML SSO"
+msgid "SAML discovery tokens"
+msgstr ""
+
msgid "SAML for %{group_name}"
msgstr "%{group_name} çš„ SAML"
@@ -17529,12 +17895,18 @@ msgstr "新建æµæ°´çº¿è®¡åˆ’"
msgid "Scheduled"
msgstr "已计划"
+msgid "Scheduled to merge this merge request (%{strategy})."
+msgstr ""
+
msgid "Scheduled to merge this merge request when the pipeline succeeds."
msgstr "计划在æµæ°´çº¿æˆåŠŸæ—¶åˆå¹¶æ­¤åˆå¹¶è¯·æ±‚。"
msgid "Schedules"
msgstr "计划"
+msgid "Schedules to merge this merge request (%{strategy})."
+msgstr ""
+
msgid "Scheduling"
msgstr "计划"
@@ -17592,6 +17964,9 @@ msgstr "æœç´¢åˆ†æ”¯"
msgid "Search branches and tags"
msgstr "æœç´¢åˆ†æ”¯å’Œæ ‡ç­¾"
+msgid "Search by author"
+msgstr ""
+
msgid "Search files"
msgstr "æœç´¢æ–‡ä»¶"
@@ -17729,10 +18104,6 @@ msgid "SearchResults|snippet"
msgid_plural "SearchResults|snippets"
msgstr[0] "代ç ç‰‡æ®µ"
-msgid "SearchResults|snippet result"
-msgid_plural "SearchResults|snippet results"
-msgstr[0] "代ç ç‰‡æ®µç»“æžœ"
-
msgid "SearchResults|user"
msgid_plural "SearchResults|users"
msgstr[0] "用户"
@@ -17741,6 +18112,9 @@ msgid "SearchResults|wiki result"
msgid_plural "SearchResults|wiki results"
msgstr[0] "wiki结果"
+msgid "Searching by both author and message is currently not supported."
+msgstr ""
+
msgid "Seat Link"
msgstr ""
@@ -17768,185 +18142,233 @@ msgstr "安全é…ç½®"
msgid "Security Dashboard"
msgstr "安全仪表æ¿"
-msgid "Security Dashboard|Error fetching the vulnerability counts. Please check your network connection and try again."
-msgstr "获å–æ¼æ´žæ•°é‡æ—¶å‡ºé”™ã€‚请检查您的网络连接,然åŽé‡è¯•ã€‚"
+msgid "Security configuration help link"
+msgstr "安全é…置帮助链接"
-msgid "Security Dashboard|Error fetching the vulnerability list. Please check your network connection and try again."
-msgstr "获å–æ¼æ´žåˆ—表时出错。请检查您的网络连接,然åŽé‡è¯•ã€‚"
+msgid "Security dashboard"
+msgstr "安全仪表盘"
-msgid "Security Dashboard|Issue Created"
-msgstr "已创建议题"
+msgid "Security report is out of date. Please update your branch with the latest changes from the target branch (%{targetBranchName})"
+msgstr ""
-msgid "Security Reports|Comment added to '%{vulnerabilityName}'"
-msgstr "评论已添加到'%{vulnerabilityName}'"
+msgid "Security report is out of date. Run %{newPipelineLinkStart}a new pipeline%{newPipelineLinkEnd} for the target branch (%{targetBranchName})"
+msgstr ""
-msgid "Security Reports|Comment deleted on '%{vulnerabilityName}'"
-msgstr "评论已从'%{vulnerabilityName}' 删除"
+msgid "SecurityConfiguration|Configured"
+msgstr "å·²é…ç½®"
-msgid "Security Reports|Comment edited on '%{vulnerabilityName}'"
-msgstr "在'%{vulnerabilityName}' 上的评论被编辑"
+msgid "SecurityConfiguration|Feature"
+msgstr "功能"
-msgid "Security Reports|Create issue"
-msgstr "创建议题"
+msgid "SecurityConfiguration|Feature documentation for %{featureName}"
+msgstr ""
-msgid "Security Reports|Dismiss vulnerability"
-msgstr "忽略æ¼æ´ž"
+msgid "SecurityConfiguration|Not yet configured"
+msgstr "尚未é…ç½®"
-msgid "Security Reports|Dismissed '%{vulnerabilityName}'"
-msgstr "已忽略的'%{vulnerabilityName}'"
+msgid "SecurityConfiguration|Secure features"
+msgstr "安全功能"
-msgid "Security Reports|Dismissed '%{vulnerabilityName}'. Turn off the hide dismissed toggle to view."
-msgstr "已忽略'%{vulnerabilityName}'。请关闭éšè—忽略开关以查看。"
+msgid "SecurityConfiguration|Status"
+msgstr "状æ€"
-msgid "Security Reports|Either you don't have permission to view this dashboard or the dashboard has not been setup. Please check your permission settings with your administrator or check your dashboard configurations to proceed."
-msgstr "您无æƒæŸ¥çœ‹æ­¤ä»ªè¡¨æ¿æˆ–尚未设置仪表æ¿ã€‚请与管ç†å‘˜æ ¸å®žæ‚¨çš„æƒé™è®¾ç½®ï¼Œæˆ–检查仪表æ¿é…置以继续。"
+msgid "SecurityReports|%{firstProject} and %{secondProject}"
+msgstr ""
-msgid "Security Reports|Learn more about setting up your dashboard"
-msgstr "了解更多关于您的仪表æ¿è®¾ç½®"
+msgid "SecurityReports|%{firstProject}, %{secondProject}, and %{rest}"
+msgstr ""
-msgid "Security Reports|More info"
-msgstr "更多信æ¯"
+msgid "SecurityReports|Add a project to your dashboard"
+msgstr ""
-msgid "Security Reports|Oops, something doesn't seem right."
-msgstr "哦,似乎有些ä¸æ­£ç¡®ã€‚"
+msgid "SecurityReports|Add or remove projects from your dashboard"
+msgstr ""
-msgid "Security Reports|Security reports can only be accessed by authorized users."
+msgid "SecurityReports|Add projects"
msgstr ""
-msgid "Security Reports|There was an error adding the comment."
-msgstr "添加评论时出错。"
+msgid "SecurityReports|Comment added to '%{vulnerabilityName}'"
+msgstr ""
-msgid "Security Reports|There was an error creating the issue."
-msgstr "创建议题时出错。"
+msgid "SecurityReports|Comment deleted on '%{vulnerabilityName}'"
+msgstr ""
-msgid "Security Reports|There was an error creating the merge request."
-msgstr "创建åˆå¹¶è¯·æ±‚时出错。"
+msgid "SecurityReports|Comment edited on '%{vulnerabilityName}'"
+msgstr ""
-msgid "Security Reports|There was an error deleting the comment."
-msgstr "删除评论时出错。"
+msgid "SecurityReports|Create issue"
+msgstr ""
-msgid "Security Reports|There was an error dismissing the vulnerabilities."
+msgid "SecurityReports|Dismiss Selected"
msgstr ""
-msgid "Security Reports|There was an error dismissing the vulnerability."
-msgstr "忽略æ¼æ´žæ—¶å‡ºé”™ã€‚"
+msgid "SecurityReports|Dismiss vulnerability"
+msgstr ""
-msgid "Security Reports|There was an error reverting the dismissal."
-msgstr "å–消忽略时出错。"
+msgid "SecurityReports|Dismissed '%{vulnerabilityName}'"
+msgstr ""
-msgid "Security Reports|There was an error reverting this dismissal."
-msgstr "å–消忽略时出错。"
+msgid "SecurityReports|Dismissed '%{vulnerabilityName}'. Turn off the hide dismissed toggle to view."
+msgstr ""
-msgid "Security Reports|Undo dismiss"
-msgstr "撤消解除"
+msgid "SecurityReports|Each vulnerability now has a unique page that can be directly linked to, shared, referenced, and tracked as the single source of truth. Vulnerability occurrences also persist across scanner runs, which improves tracking and visibility and reduces duplicates between scans."
+msgstr ""
-msgid "Security Reports|You do not have sufficient permissions to access this report"
+msgid "SecurityReports|Edit dashboard"
msgstr ""
-msgid "Security Reports|You must sign in as an authorized user to see this report"
+msgid "SecurityReports|Either you don't have permission to view this dashboard or the dashboard has not been setup. Please check your permission settings with your administrator or check your dashboard configurations to proceed."
msgstr ""
-msgid "Security configuration help link"
-msgstr "安全é…置帮助链接"
+msgid "SecurityReports|Error fetching the vulnerability counts. Please check your network connection and try again."
+msgstr ""
-msgid "Security dashboard"
-msgstr "安全仪表盘"
+msgid "SecurityReports|Error fetching the vulnerability list. Please check your network connection and try again."
+msgstr ""
-msgid "Security report is out of date. Please update your branch with the latest changes from the target branch (%{targetBranchName})"
+msgid "SecurityReports|False positive"
msgstr ""
-msgid "Security report is out of date. Run %{newPipelineLinkStart}a new pipeline%{newPipelineLinkEnd} for the target branch (%{targetBranchName})"
+msgid "SecurityReports|Hide dismissed"
msgstr ""
-msgid "SecurityConfiguration|Configured"
-msgstr "å·²é…ç½®"
+msgid "SecurityReports|Introducing standalone vulnerabilities"
+msgstr ""
-msgid "SecurityConfiguration|Feature"
-msgstr "功能"
+msgid "SecurityReports|Issue Created"
+msgstr ""
-msgid "SecurityConfiguration|Feature documentation for %{featureName}"
+msgid "SecurityReports|Learn More"
msgstr ""
-msgid "SecurityConfiguration|Not yet configured"
-msgstr "尚未é…ç½®"
+msgid "SecurityReports|Learn more about setting up your dashboard"
+msgstr ""
-msgid "SecurityConfiguration|Secure features"
-msgstr "安全功能"
+msgid "SecurityReports|Load more vulnerabilities"
+msgstr ""
-msgid "SecurityConfiguration|Status"
-msgstr "状æ€"
+msgid "SecurityReports|Monitor vulnerabilities in your code"
+msgstr ""
-msgid "SecurityDashboard|%{firstProject} and %{secondProject}"
-msgstr "%{firstProject}和%{secondProject}"
+msgid "SecurityReports|More info"
+msgstr ""
-msgid "SecurityDashboard|%{firstProject}, %{secondProject}, and %{rest}"
-msgstr "%{firstProject},%{secondProject},åŠ%{rest}"
+msgid "SecurityReports|More information"
+msgstr ""
-msgid "SecurityDashboard|Add a project to your dashboard"
-msgstr "将项目添加到仪表æ¿"
+msgid "SecurityReports|No vulnerabilities found for dashboard"
+msgstr ""
-msgid "SecurityDashboard|Add or remove projects from your dashboard"
-msgstr "å‘仪表æ¿æ·»åŠ æˆ–删除项目"
+msgid "SecurityReports|No vulnerabilities found for this group"
+msgstr ""
-msgid "SecurityDashboard|Add projects"
-msgstr "添加项目"
+msgid "SecurityReports|No vulnerabilities found for this pipeline"
+msgstr ""
-msgid "SecurityDashboard|Edit dashboard"
-msgstr "编辑仪表æ¿"
+msgid "SecurityReports|No vulnerabilities found for this project"
+msgstr ""
-msgid "SecurityDashboard|Hide dismissed"
-msgstr "éšè—已忽略项"
+msgid "SecurityReports|Oops, something doesn't seem right."
+msgstr ""
-msgid "SecurityDashboard|Monitor vulnerabilities in your code"
-msgstr "监控代ç ä¸­çš„æ¼æ´ž"
+msgid "SecurityReports|Pipeline %{pipelineLink} triggered %{timeago} by %{user}"
+msgstr ""
-msgid "SecurityDashboard|More information"
-msgstr "更多信æ¯"
+msgid "SecurityReports|Project"
+msgstr ""
-msgid "SecurityDashboard|No vulnerabilities found for dashboard"
+msgid "SecurityReports|Projects added"
msgstr ""
-msgid "SecurityDashboard|Pipeline %{pipelineLink} triggered %{timeago} by %{user}"
-msgstr "æµæ°´çº¿%{pipelineLink}于%{timeago}ç”±%{user}触å‘"
+msgid "SecurityReports|Remove project from dashboard"
+msgstr ""
-msgid "SecurityDashboard|Project"
-msgstr "项目"
+msgid "SecurityReports|Report type"
+msgstr ""
-msgid "SecurityDashboard|Projects added"
-msgstr "项目已添加"
+msgid "SecurityReports|Return to dashboard"
+msgstr ""
-msgid "SecurityDashboard|Remove project from dashboard"
-msgstr "从仪表æ¿åˆ é™¤é¡¹ç›®"
+msgid "SecurityReports|Security Dashboard"
+msgstr ""
-msgid "SecurityDashboard|Report type"
-msgstr "报表类型"
+msgid "SecurityReports|Security reports can only be accessed by authorized users."
+msgstr ""
-msgid "SecurityDashboard|Return to dashboard"
-msgstr "返回仪表æ¿"
+msgid "SecurityReports|Select a project to add by using the project search field above."
+msgstr ""
-msgid "SecurityDashboard|Security Dashboard"
-msgstr "安全仪表æ¿"
+msgid "SecurityReports|Select a reason"
+msgstr ""
-msgid "SecurityDashboard|Select a project to add by using the project search field above."
-msgstr "请使用上é¢çš„项目æœç´¢å­—段æ¥é€‰æ‹©è¦æ·»åŠ çš„项目。"
+msgid "SecurityReports|Severity"
+msgstr ""
-msgid "SecurityDashboard|Severity"
-msgstr "严é‡æ€§"
+msgid "SecurityReports|Status"
+msgstr ""
-msgid "SecurityDashboard|Status"
+msgid "SecurityReports|The security dashboard displays the latest security findings for projects you wish to monitor. Select \"Edit dashboard\" to add and remove projects."
msgstr ""
-msgid "SecurityDashboard|The security dashboard displays the latest security findings for projects you wish to monitor. Select \"Edit dashboard\" to add and remove projects."
-msgstr "安全é¢æ¿æ˜¾ç¤ºæ‚¨è¦ç›‘测项目的最新安全扫æ结果。选择“编辑仪表æ¿â€æ¥æ·»åŠ å¹¶ç§»é™¤é¡¹ç›®ã€‚"
+msgid "SecurityReports|The security dashboard displays the latest security report. Use it to find and fix vulnerabilities."
+msgstr ""
+
+msgid "SecurityReports|There was an error adding the comment."
+msgstr ""
+
+msgid "SecurityReports|There was an error creating the issue."
+msgstr ""
+
+msgid "SecurityReports|There was an error creating the merge request."
+msgstr ""
+
+msgid "SecurityReports|There was an error deleting the comment."
+msgstr ""
+
+msgid "SecurityReports|There was an error dismissing the vulnerabilities."
+msgstr ""
+
+msgid "SecurityReports|There was an error dismissing the vulnerability."
+msgstr ""
+
+msgid "SecurityReports|There was an error reverting the dismissal."
+msgstr ""
-msgid "SecurityDashboard|The security dashboard displays the latest security report. Use it to find and fix vulnerabilities."
+msgid "SecurityReports|There was an error reverting this dismissal."
msgstr ""
-msgid "SecurityDashboard|There was an error while generating the report."
+msgid "SecurityReports|There was an error while generating the report."
msgstr ""
-msgid "SecurityDashboard|Unable to add %{invalidProjects}"
-msgstr "无法添加%{invalidProjects}"
+msgid "SecurityReports|Unable to add %{invalidProjects}"
+msgstr ""
+
+msgid "SecurityReports|Undo dismiss"
+msgstr ""
+
+msgid "SecurityReports|While it's rare to have no vulnerabilities for your group, it can happen. In any event, we ask that you double check your settings to make sure you've set up your dashboard correctly."
+msgstr ""
+
+msgid "SecurityReports|While it's rare to have no vulnerabilities for your pipeline, it can happen. In any event, we ask that you double check your settings to make sure all security scanning jobs have passed successfully."
+msgstr ""
+
+msgid "SecurityReports|While it's rare to have no vulnerabilities for your project, it can happen. In any event, we ask that you double check your settings to make sure you've set up your dashboard correctly."
+msgstr ""
+
+msgid "SecurityReports|While it's rare to have no vulnerabilities, it can happen. In any event, we ask that you please double check your settings to make sure you've set up your dashboard correctly."
+msgstr ""
+
+msgid "SecurityReports|Won't fix / Accept risk"
+msgstr ""
+
+msgid "SecurityReports|You do not have sufficient permissions to access this report"
+msgstr ""
+
+msgid "SecurityReports|You must sign in as an authorized user to see this report"
+msgstr ""
+
+msgid "SecurityReports|[No reason]"
+msgstr ""
msgid "See GitLab's %{password_policy_guidelines}"
msgstr "å‚è§GitLabçš„%{password_policy_guidelines}"
@@ -17957,6 +18379,9 @@ msgstr "查看指标"
msgid "See the affected projects in the GitLab admin panel"
msgstr "查看 GitLab 管ç†é¢æ¿ä¸­çš„å—å½±å“项目"
+msgid "See what's new at GitLab"
+msgstr ""
+
msgid "Select"
msgstr "选择"
@@ -18362,9 +18787,6 @@ msgstr "é…ç½® CI/CD"
msgid "Set up Jira Integration"
msgstr ""
-msgid "Set up Jira Integration illustration"
-msgstr ""
-
msgid "Set up a %{type} Runner automatically"
msgstr "自动设置一个%{type}的Runner"
@@ -18449,6 +18871,9 @@ msgstr ""
msgid "Severity: %{severity}"
msgstr "严é‡ç¨‹åº¦: %{severity}"
+msgid "Shards (%{shards})"
+msgstr ""
+
msgid "Shards to synchronize"
msgstr ""
@@ -18762,6 +19187,9 @@ msgstr ""
msgid "Snowplow"
msgstr "Snowplow"
+msgid "Some child epics may be hidden due to applied filters"
+msgstr ""
+
msgid "Some email servers do not support overriding the email sender name. Enable this option to include the name of the author of the issue, merge request or comment in the email body instead."
msgstr "æŸäº›ç”µå­é‚®ä»¶æœåŠ¡å™¨ä¸æ”¯æŒè¦†ç›–电å­é‚®ä»¶çš„å‘件人å称。å¯ç”¨æ­¤é€‰é¡¹å¯ä»¥åœ¨ç”µå­é‚®ä»¶æ­£æ–‡ä¸­åŒ…å«è®®é¢˜çš„作者姓åã€åˆå¹¶è¯·æ±‚或评论。"
@@ -18903,6 +19331,9 @@ msgstr ""
msgid "Something went wrong while updating your list settings"
msgstr "更新列表设置时出现错误"
+msgid "Something went wrong with your automatic subscription renewal"
+msgstr ""
+
msgid "Something went wrong, unable to add %{project} to dashboard"
msgstr "å°†%{project} 添加到仪表æ¿æ—¶å‡ºé”™"
@@ -19188,6 +19619,9 @@ msgstr "阶段数æ®å·²æ›´æ–°"
msgid "Stage removed"
msgstr "阶段已删除"
+msgid "Standard"
+msgstr ""
+
msgid "Star a label to make it a priority label. Order the prioritized labels to change their relative priority, by dragging."
msgstr "把一个标记加上星标,å¯å°†å…¶å˜ä¸ºä¼˜å…ˆæ ‡è®°ã€‚通过拖放æ¥è°ƒæ•´ä¼˜å…ˆæ ‡è®°çš„顺åºï¼Œå¯ä»¥æ”¹å˜ä»–们的相对优先级。"
@@ -19311,6 +19745,9 @@ msgstr "输入消æ¯ä»¥å¯ç”¨"
msgid "Static Application Security Testing (SAST)"
msgstr "é™æ€åº”用程åºå®‰å…¨æµ‹è¯•(SAST)"
+msgid "StaticSiteEditor|An error occurred while submitting your changes."
+msgstr ""
+
msgid "StaticSiteEditor|Branch could not be created."
msgstr ""
@@ -19320,18 +19757,30 @@ msgstr ""
msgid "StaticSiteEditor|Could not create merge request."
msgstr ""
+msgid "StaticSiteEditor|Incompatible file content"
+msgstr ""
+
msgid "StaticSiteEditor|Return to site"
msgstr ""
+msgid "StaticSiteEditor|Static site editor"
+msgstr ""
+
msgid "StaticSiteEditor|Success!"
msgstr ""
msgid "StaticSiteEditor|Summary of changes"
msgstr ""
+msgid "StaticSiteEditor|The Static Site Editor is currently configured to only edit Markdown content on pages generated from Middleman. Visit the documentation to learn more about configuring your site to use the Static Site Editor."
+msgstr ""
+
msgid "StaticSiteEditor|Update %{sourcePath} file"
msgstr ""
+msgid "StaticSiteEditor|View documentation"
+msgstr ""
+
msgid "StaticSiteEditor|View merge request"
msgstr ""
@@ -19389,6 +19838,12 @@ msgstr ""
msgid "StatusPage|Status page"
msgstr ""
+msgid "StatusPage|Status page URL"
+msgstr ""
+
+msgid "StatusPage|Status page frontend documentation"
+msgstr ""
+
msgid "StatusPage|To publish incidents to an external status page, GitLab will store a JSON file in your Amazon S3 account in a location accessible to your external status page service. Make sure to also set up %{docsLink}"
msgstr ""
@@ -19737,6 +20192,12 @@ msgstr "åŒæ­¥ä¿¡æ¯"
msgid "Synced"
msgstr ""
+msgid "Synchronization disabled"
+msgstr ""
+
+msgid "Synchronization of container repositories is disabled."
+msgstr ""
+
msgid "System"
msgstr "系统"
@@ -19890,6 +20351,9 @@ msgstr "团队域"
msgid "Telephone number"
msgstr "电è¯å·ç "
+msgid "Telephone number (Optional)"
+msgstr ""
+
msgid "Template"
msgstr "模æ¿"
@@ -20193,6 +20657,9 @@ msgstr "许å¯è¯å·²è¢«åˆ é™¤ã€‚ GitLab当å‰æ— æœ‰æ•ˆçš„许å¯è¯ã€‚"
msgid "The license was successfully uploaded and is now active. You can see the details below."
msgstr "许å¯è¯å·²æˆåŠŸä¸Šä¼ ï¼Œå¹¶å·²æ¿€æ´»ã€‚详细信æ¯å¦‚下。"
+msgid "The license was successfully uploaded and will be active from %{starts_at}. You can see the details below."
+msgstr ""
+
msgid "The maximum file size allowed is %{size}."
msgstr "å…许的最大文件大å°ä¸º %{size}。"
@@ -20514,6 +20981,9 @@ msgstr "收集图表数æ®æ—¶å‡ºé”™"
msgid "There was an error getting the epic participants."
msgstr ""
+msgid "There was an error importing the Jira project."
+msgstr ""
+
msgid "There was an error loading users activity calendar."
msgstr "加载用户活动日历时出错。"
@@ -20529,7 +20999,7 @@ msgstr "é‡ç½®ç¾¤ç»„æµæ°´çº¿åˆ†é’Ÿæ•°æ—¶å‡ºé”™ã€‚"
msgid "There was an error resetting user pipeline minutes."
msgstr "é‡ç½®ç”¨æˆ·æµæ°´çº¿åˆ†é’Ÿæ•°æ—¶å‡ºé”™ã€‚"
-msgid "There was an error saving this Geo Node"
+msgid "There was an error saving this Geo Node."
msgstr ""
msgid "There was an error saving your changes."
@@ -20592,6 +21062,9 @@ msgstr "这些é…置于父群组的å˜é‡ï¼Œå¯ä»¥å’Œé¡¹ç›®å˜é‡ä¸€èµ·ç”¨äºŽå½“
msgid "They can be managed using the %{link}."
msgstr "å¯ä»¥ä½¿ç”¨ %{link} 进行托管。"
+msgid "Third Party Advisory Link"
+msgstr ""
+
msgid "Third party offers"
msgstr "第三方优惠"
@@ -20628,6 +21101,9 @@ msgstr "这个应用程åºæ˜¯ç”± %{link_to_owner} 创建的。"
msgid "This application will be able to:"
msgstr "此应用程åºå°†å¯ä»¥ï¼š"
+msgid "This attachment has been truncated to avoid exceeding the maximum allowed attachment size of 15MB. %{written_count} of %{issues_count} issues have been included. Consider re-exporting with a narrower selection of issues."
+msgstr ""
+
msgid "This block is self-referential"
msgstr "该阻塞为自我引用"
@@ -20769,6 +21245,12 @@ msgstr "此问题是%{confidentialLinkStart}机密的%{linkEnd}和%{lockedLinkSt
msgid "This issue is confidential"
msgstr "当å‰è®®é¢˜ä¸ºç§å¯†è®®é¢˜"
+msgid "This issue is currently blocked by the following issues: %{issues}."
+msgstr ""
+
+msgid "This issue is in a child epic of the filtered epic"
+msgstr ""
+
msgid "This issue is locked."
msgstr "此议题已é”定。"
@@ -20967,7 +21449,10 @@ msgstr "此用户将æˆä¸ºæ´»åŠ¨æµä¸­æ‰€æœ‰äº‹ä»¶çš„作者,例如创建新分
msgid "This user will be the author of all events in the activity feed that are the result of an update, like new branches being created or new commits being pushed to existing branches. Upon creation or when reassigning you can only assign yourself to be the mirror user."
msgstr "此用户将æˆä¸ºæ´»åŠ¨æµä¸­æ‰€æœ‰äº‹ä»¶çš„作者,例如创建新分支或者推é€æ–°æ交到现有分支。在创建或é‡æ–°æŒ‡å®šæ—¶æ‚¨ä»…å¯å°†è‡ªå·±æŒ‡å®šä¸ºé•œåƒç”¨æˆ·ã€‚"
-msgid "This variable can not be masked"
+msgid "This variable can not be masked."
+msgstr ""
+
+msgid "This variable does not match the expected pattern."
msgstr ""
msgid "This will help us personalize your onboarding experience."
@@ -20997,10 +21482,10 @@ msgstr "异常请求"
msgid "ThreatMonitoring|Application firewall not detected"
msgstr ""
-msgid "ThreatMonitoring|Container Network Policy"
+msgid "ThreatMonitoring|Container Network Policies are not installed or have been disabled. To view this data, ensure your Network Policies are installed and enabled for your cluster."
msgstr ""
-msgid "ThreatMonitoring|Container NetworkPolicies are not installed or has been disabled. To view this data, ensure you NetworkPolicies are installed and enabled for your cluster."
+msgid "ThreatMonitoring|Container Network Policy"
msgstr ""
msgid "ThreatMonitoring|Container NetworkPolicies not detected"
@@ -21033,7 +21518,7 @@ msgstr "出现错误,无法获å–环境"
msgid "ThreatMonitoring|Something went wrong, unable to fetch statistics"
msgstr ""
-msgid "ThreatMonitoring|The firewall is not installed or has been disabled. To view this data, ensure you firewall is installed and enabled for your cluster."
+msgid "ThreatMonitoring|The firewall is not installed or has been disabled. To view this data, ensure the web application firewall is installed and enabled for your cluster."
msgstr ""
msgid "ThreatMonitoring|The graph below is an overview of traffic coming to your application as tracked by the Web Application Firewall (WAF). View the docs for instructions on how to access the WAF logs to see what type of malicious traffic is trying to access your app. The docs link is also accessible by clicking the \"?\" icon next to the title below."
@@ -21102,6 +21587,9 @@ msgstr "最åŽä¸€æ¬¡æ交到åˆå¹¶çš„时间"
msgid "Time in seconds GitLab will wait for a response from the external service. When the service does not respond in time, access will be denied."
msgstr "GitLab等待外部æœåŠ¡çš„å“应时间(秒)。当æœåŠ¡æ²¡æœ‰åŠæ—¶å“应时,访问将被拒ç»ã€‚"
+msgid "Time of import: %{importTime}"
+msgstr ""
+
msgid "Time remaining"
msgstr "剩余时间:"
@@ -21284,8 +21772,8 @@ msgstr "标题"
msgid "Title:"
msgstr "标题:"
-msgid "Titles and Filenames"
-msgstr "标题和文件å称"
+msgid "Titles and Descriptions"
+msgstr ""
msgid "To"
msgstr "至"
@@ -21491,8 +21979,8 @@ msgstr "å¯ç”¨äº†å¤ªå¤šçš„命å空间。您需è¦é€šè¿‡æŽ§åˆ¶å°æˆ–APIæ¥ç®¡ç†
msgid "Too many projects enabled. You will need to manage them via the console or the API."
msgstr "å¯ç”¨äº†å¤ªå¤šçš„项目。您需è¦é€šè¿‡æŽ§åˆ¶å°æˆ–APIæ¥ç®¡ç†å®ƒä»¬ã€‚"
-msgid "Topics"
-msgstr "主题"
+msgid "Topics (optional)"
+msgstr ""
msgid "Total"
msgstr "全部"
@@ -21653,6 +22141,9 @@ msgstr "é‡è¯•ï¼Ÿ"
msgid "Try all GitLab has to offer for 30 days."
msgstr "30天内体验GitLab的所有功能。"
+msgid "Try changing or removing filters."
+msgstr ""
+
msgid "Try to fork again"
msgstr "å°è¯•å†æ¬¡æ´¾ç”Ÿ"
@@ -21746,6 +22237,9 @@ msgstr "无法连接到PrometheusæœåŠ¡å™¨"
msgid "Unable to connect to server: %{error}"
msgstr "无法连接到æœåŠ¡å™¨: %{error}"
+msgid "Unable to connect to the Jira instance. Please check your Jira integration configuration."
+msgstr ""
+
msgid "Unable to convert Kubernetes logs encoding to UTF-8"
msgstr ""
@@ -22687,6 +23181,9 @@ msgstr ""
msgid "View issue"
msgstr "查看议题"
+msgid "View issues"
+msgstr ""
+
msgid "View it on GitLab"
msgstr "使用GitLab查看"
@@ -22864,6 +23361,15 @@ msgstr ""
msgid "VulnerabilityManagement|Resolved %{timeago} by %{user}"
msgstr ""
+msgid "VulnerabilityManagement|Something went wrong while trying to delete the comment. Please try again later."
+msgstr ""
+
+msgid "VulnerabilityManagement|Something went wrong while trying to retrieve the vulnerability history. Please try again later."
+msgstr ""
+
+msgid "VulnerabilityManagement|Something went wrong while trying to save the comment. Please try again later."
+msgstr ""
+
msgid "VulnerabilityManagement|Something went wrong, could not create an issue."
msgstr ""
@@ -22894,6 +23400,9 @@ msgstr ""
msgid "VulnerabilityStatusTypes|Resolved"
msgstr ""
+msgid "Vulnerability|%{scannerName} (version %{scannerVersion})"
+msgstr ""
+
msgid "Vulnerability|Class"
msgstr "类型"
@@ -22927,8 +23436,11 @@ msgstr "命å空间"
msgid "Vulnerability|Project"
msgstr "项目"
-msgid "Vulnerability|Report Type"
-msgstr "报告类型"
+msgid "Vulnerability|Scanner Provider"
+msgstr ""
+
+msgid "Vulnerability|Scanner Type"
+msgstr ""
msgid "Vulnerability|Severity"
msgstr "严é‡çº§åˆ«"
@@ -22981,9 +23493,15 @@ msgstr "我们从您的U2F设备收到了回å¤ã€‚您已通过身份验è¯ã€‚"
msgid "We sent you an email with reset password instructions"
msgstr "我们å‘é€äº†ä¸€å°å¸¦æœ‰é‡ç½®å¯†ç ä¿¡æ¯çš„电å­é‚®ä»¶"
+msgid "We tried to automatically renew your %{strong}%{plan_name}%{strong_close} subscription for %{strong}%{namespace_name}%{strong_close} on %{expires_on} but something went wrong so your subscription was downgraded to the free plan. Don't worry, your data is safe. We suggest you check your payment method and get in touch with our support team (%{support_link}). They'll gladly help with your subscription renewal."
+msgstr ""
+
msgid "We want to be sure it is you, please confirm you are not a robot."
msgstr "我们è¦ç¡®å®šæ‚¨æ˜¯ä¸æ˜¯æœºå™¨äººã€‚"
+msgid "We will automatically renew your %{strong}%{plan_name}%{strong_close} subscription for %{strong}%{namespace_name}%{strong_close} on %{strong}%{expires_on}%{strong_close}. There's nothing that you need to do, we'll let you know when the renewal is complete. Need more seats, a higher plan or just want to review your payment method?"
+msgstr ""
+
msgid "We've found no vulnerabilities"
msgstr "未å‘现安全æ¼æ´ž"
@@ -23087,15 +23605,6 @@ msgstr "使用<code>http://</code>或<code>https://</code>å议时,请æä¾›ä
msgid "When:"
msgstr "当:"
-msgid "While it's rare to have no vulnerabilities for your group, it can happen. In any event, we ask that you double check your settings to make sure you've set up your dashboard correctly."
-msgstr "虽然您的群组中没有æ¼æ´žï¼Œè¿™ç§çŽ°è±¡å¾ˆç½•è§ï¼Œä½†ä¹Ÿæ˜¯æœ‰å¯èƒ½çš„。无论如何,建议您仔细检查设置以确ä¿ä»ªè¡¨æ¿çš„é…置正确。"
-
-msgid "While it's rare to have no vulnerabilities for your pipeline, it can happen. In any event, we ask that you double check your settings to make sure all security scanning jobs have passed successfully."
-msgstr "虽然您的æµæ°´çº¿ä¸­æ²¡æœ‰æ¼æ´žï¼Œè¿™ç§çŽ°è±¡å¾ˆç½•è§ï¼Œä½†ä¹Ÿæ˜¯æœ‰å¯èƒ½çš„。无论如何,建议您仔细检查设置以确ä¿ä»ªè¡¨æ¿çš„é…置正确。"
-
-msgid "While it's rare to have no vulnerabilities for your project, it can happen. In any event, we ask that you double check your settings to make sure you've set up your dashboard correctly."
-msgstr "虽然您的项目中没有æ¼æ´žï¼Œè¿™ç§çŽ°è±¡å¾ˆç½•è§ï¼Œä½†ä¹Ÿæ˜¯æœ‰å¯èƒ½çš„。无论如何,建议您仔细检查设置以确ä¿ä»ªè¡¨æ¿çš„é…置正确。"
-
msgid "While it's rare to have no vulnerabilities, it can happen. In any event, we ask that you please double check your settings to make sure you've set up your dashboard correctly."
msgstr "虽然没有å‘现æ¼æ´žï¼Œè¿™ç§çŽ°è±¡å¾ˆç½•è§ï¼Œä½†ä¹Ÿæ˜¯æœ‰å¯èƒ½çš„。无论如何,建议您仔细检查设置以确ä¿ä»ªè¡¨æ¿çš„é…置正确。"
@@ -23126,9 +23635,6 @@ msgstr ""
msgid "Wiki"
msgstr "Wiki"
-msgid "Wiki pages"
-msgstr "Wiki页é¢"
-
msgid "Wiki was successfully updated."
msgstr "Wikiå·²æˆåŠŸæ›´æ–°ã€‚"
@@ -23312,6 +23818,9 @@ msgstr "是或å¦"
msgid "Yes, add it"
msgstr "是的,添加它"
+msgid "Yes, close issue"
+msgstr ""
+
msgid "Yes, let me map Google Code users to full names or GitLab users."
msgstr "是的,让我将Google Code用户映射到全å或GitLab用户。"
@@ -23507,6 +24016,9 @@ msgstr "您ä¸èƒ½å†™å…¥è¿™ä¸ªåªè¯»çš„ GitLab 实例。"
msgid "You could not create a new trigger."
msgstr "您无法创建新的触å‘器。"
+msgid "You didn't renew your %{strong}%{plan_name}%{strong_close} subscription for %{strong}%{namespace_name}%{strong_close} so it was downgraded to the free plan."
+msgstr ""
+
msgid "You didn't renew your %{strong}%{plan_name}%{strong_close} subscription so it was downgraded to the GitLab Core Plan."
msgstr ""
@@ -23573,6 +24085,9 @@ msgstr "您已å–消订阅该主题。"
msgid "You have declined the invitation to join %{label}."
msgstr "您已拒ç»åŠ å…¥%{label}的邀请。"
+msgid "You have imported from this project %{numberOfPreviousImportsForProject} times before. Each new import will create duplicate issues."
+msgstr ""
+
msgid "You have no permissions"
msgstr "没有æƒé™"
@@ -23654,6 +24169,9 @@ msgstr "您需è¦ä¸Šä¼ GitLab项目导出文件(以.gz结尾)."
msgid "You need to upload a Google Takeout archive."
msgstr "您需è¦ä¸Šä¼ Google Takeout文件。"
+msgid "You reached %{usage_in_percent} of %{namespace_name}'s capacity (%{used_storage} of %{storage_limit})"
+msgstr ""
+
msgid "You tried to fork %{link_to_the_project} but it failed for the following reason:"
msgstr "您å°è¯•æ´¾ç”Ÿ %{link_to_the_project} 但由于以下原因导致失败:"
@@ -23738,9 +24256,21 @@ msgstr "您已ç»ä½¿ç”¨ä¸€æ¬¡å¯†ç éªŒè¯å™¨å¯ç”¨äº†åŒé‡è®¤è¯ã€‚如果您è¦
msgid "YouTube"
msgstr "YouTube"
+msgid "Your %{strong}%{plan_name}%{strong_close} subscription for %{strong}%{namespace_name}%{strong_close} will expire on %{strong}%{expires_on}%{strong_close}. After that, you will not to be able to create issues or merge requests as well as many other features."
+msgstr ""
+
msgid "Your %{strong}%{plan_name}%{strong_close} subscription will expire on %{strong}%{expires_on}%{strong_close}. After that, you will not to be able to create issues or merge requests as well as many other features."
msgstr ""
+msgid "Your CSV export has started. It will be emailed to %{email} when complete."
+msgstr ""
+
+msgid "Your CSV export of %{issues_count} from project %{project_link} has been added to this email as an attachment."
+msgstr ""
+
+msgid "Your CSV export of %{written_count} from project %{project_name} (%{project_url}) has been added to this email as an attachment."
+msgstr ""
+
msgid "Your Commit Email will be used for web based operations, such as edits and merges."
msgstr "您的æ交电å­é‚®ä»¶å°†ç”¨äºŽåŸºäºŽwebçš„æ“作,例如编辑与åˆå¹¶ã€‚"
@@ -23837,6 +24367,9 @@ msgstr "您的评论无法æ交ï¼è¯·æ£€æŸ¥æ‚¨çš„网络连接,然åŽé‡è¯•ã€‚
msgid "Your comment could not be updated! Please check your network connection and try again."
msgstr "您的评论无法更新ï¼è¯·æ£€æŸ¥æ‚¨çš„网络连接,然åŽé‡è¯•ã€‚"
+msgid "Your comment will be discarded."
+msgstr ""
+
msgid "Your custom stage '%{title}' was created"
msgstr ""
@@ -23876,6 +24409,9 @@ msgstr "您的新 SCIM 令牌"
msgid "Your new personal access token has been created."
msgstr "您的新个人访问令牌已创建。"
+msgid "Your new project access token has been created."
+msgstr ""
+
msgid "Your password isn't required to view this page. If a password or any other personal details are requested, please contact your administrator to report abuse."
msgstr "查看此页é¢ä¸éœ€è¦å¯†ç ã€‚如果è¦æ±‚输入密ç æˆ–其他任何个人详细信æ¯ï¼Œè¯·ä¸Žç®¡ç†å‘˜è”系以举报滥用行为。"
@@ -23894,12 +24430,18 @@ msgstr "您的项目"
msgid "Your request for access has been queued for review."
msgstr "您的访问请求已进入审核队列。"
+msgid "Your search didn't match any commits."
+msgstr ""
+
msgid "Your subscription expired!"
msgstr ""
msgid "Your subscription has been downgraded"
msgstr ""
+msgid "Your subscription will automatically renew in %{remaining_days}"
+msgstr ""
+
msgid "Your subscription will expire in %{remaining_days}"
msgstr ""
@@ -23937,8 +24479,8 @@ msgstr "å‰"
msgid "allowed to fail"
msgstr "å…许失败"
-msgid "already being used for another group or project milestone."
-msgstr "å·²ç»ç”¨äºŽå¦ä¸€ç¾¤ç»„或项目里程碑。"
+msgid "already being used for another group or project %{timebox_name}."
+msgstr ""
msgid "already has a \"created\" issue link"
msgstr "å·²ç»æœ‰â€œå·²åˆ›å»ºâ€çš„议题链接"
@@ -24015,6 +24557,9 @@ msgstr "%{linkStartTag}了解更多有关ä¾èµ–项扫æçš„ä¿¡æ¯ %{linkEndTag}"
msgid "ciReport|%{linkStartTag}Learn more about SAST %{linkEndTag}"
msgstr "%{linkStartTag}了解更多有关SAST %{linkEndTag}çš„ä¿¡æ¯"
+msgid "ciReport|%{linkStartTag}Learn more about Secret Scanning %{linkEndTag}"
+msgstr ""
+
msgid "ciReport|%{linkStartTag}Learn more about codequality reports %{linkEndTag}"
msgstr "%{linkStartTag}了解有关代ç è´¨é‡æŠ¥å‘Šçš„更多信æ¯%{linkEndTag}"
@@ -24173,6 +24718,12 @@ msgstr "通过åˆå¹¶è¯·æ±‚解决"
msgid "ciReport|SAST"
msgstr "SAST"
+msgid "ciReport|Secret scanning"
+msgstr ""
+
+msgid "ciReport|Secret scanning detects secrets and credentials vulnerabilities in your source code."
+msgstr ""
+
msgid "ciReport|Security scanning"
msgstr "安全扫æ"
@@ -24366,6 +24917,9 @@ msgstr ""
msgid "group"
msgstr "群组"
+msgid "groups"
+msgstr ""
+
msgid "has already been linked to another vulnerability"
msgstr "å·²ç»é“¾æŽ¥åˆ°å¦ä¸€ä¸ªæ¼æ´ž"
@@ -24527,9 +25081,6 @@ msgstr[0] "åˆå¹¶è¯·æ±‚"
msgid "merged %{time_ago}"
msgstr ""
-msgid "milestone should belong either to a project or a group."
-msgstr "里程碑应该属于一个项目或一个群组。"
-
msgid "missing"
msgstr "丢失"
@@ -24866,6 +25417,9 @@ msgstr "新建åˆå¹¶è¯·æ±‚"
msgid "no contributions"
msgstr "无贡献"
+msgid "no expiration"
+msgstr ""
+
msgid "no one can merge"
msgstr "没有人å¯ä»¥åˆå¹¶"
@@ -24916,6 +25470,9 @@ msgstr "待处ç†çš„评论"
msgid "pending removal"
msgstr "等待中的移除"
+msgid "per day"
+msgstr ""
+
msgid "pipeline"
msgstr "æµæ°´çº¿"
@@ -24942,6 +25499,12 @@ msgstr[0] "项目"
msgid "project avatar"
msgstr "项目头åƒ"
+msgid "projects"
+msgstr ""
+
+msgid "push to your repository, create pipelines, create issues or add comments. To reduce storage capacity, delete unused repositories, artifacts, wikis, issues, and pipelines."
+msgstr ""
+
msgid "quick actions"
msgstr "å¿«æ·æ“作"
@@ -25087,10 +25650,6 @@ msgstr "下列议题"
msgid "this document"
msgstr "此文档"
-msgid "thread resolved"
-msgid_plural "threads resolved"
-msgstr[0] "è¯é¢˜å·²è§£å†³"
-
msgid "to help your contributors communicate effectively!"
msgstr "帮助您的贡献者进行有效沟通ï¼"
@@ -25145,6 +25704,9 @@ msgstr "使用GitLab查看"
msgid "view the blob"
msgstr "查看blob"
+msgid "vulnerability|Add a comment"
+msgstr ""
+
msgid "vulnerability|Add a comment or reason for dismissal"
msgstr "为忽略添加评论或原因"
diff --git a/locale/zh_HK/gitlab.po b/locale/zh_HK/gitlab.po
index d020a2c8e67..bcf3a220dea 100644
--- a/locale/zh_HK/gitlab.po
+++ b/locale/zh_HK/gitlab.po
@@ -12,7 +12,7 @@ msgstr ""
"X-Crowdin-Project: gitlab-ee\n"
"X-Crowdin-Language: zh-HK\n"
"X-Crowdin-File: /master/locale/gitlab.pot\n"
-"PO-Revision-Date: 2020-04-15 00:30\n"
+"PO-Revision-Date: 2020-05-05 21:32\n"
msgid " %{start} to %{end}"
msgstr ""
@@ -182,6 +182,10 @@ msgid "%d tag"
msgid_plural "%d tags"
msgstr[0] ""
+msgid "%d unresolved thread"
+msgid_plural "%d unresolved threads"
+msgstr[0] ""
+
msgid "%d vulnerability dismissed"
msgid_plural "%d vulnerabilities dismissed"
msgstr[0] ""
@@ -335,6 +339,9 @@ msgstr ""
msgid "%{mrText}, this issue will be closed automatically."
msgstr ""
+msgid "%{namespace_name} is now read-only. You cannot: %{base_message}"
+msgstr ""
+
msgid "%{name} contained %{resultsString}"
msgstr ""
@@ -347,6 +354,10 @@ msgstr ""
msgid "%{name}'s avatar"
msgstr "%{name} çš„é ­åƒ"
+msgid "%{no_of_days} day"
+msgid_plural "%{no_of_days} days"
+msgstr[0] ""
+
msgid "%{number_commits_behind} commits behind %{default_branch}, %{number_commits_ahead} commits ahead"
msgstr "%{number_commits_behind} 個è½å¾Œæ–¼ %{default_branch} 分支的æ交,%{number_commits_ahead} 個超å‰çš„æ交"
@@ -464,6 +475,9 @@ msgstr[0] "%{text} %{files} 個檔案"
msgid "%{text} is available"
msgstr "%{text} å¯ç”¨"
+msgid "%{timebox_name} should belong either to a project or a group."
+msgstr ""
+
msgid "%{title} %{operator} %{threshold}"
msgstr ""
@@ -720,12 +734,18 @@ msgstr "<code>\"johnsmith@example.com\": \"johnsm...@example.com\"</code> 將會
msgid "<code>\"johnsmith@example.com\": \"johnsmith@example.com\"</code> will add \"By <a href=\"#\">johnsmith@example.com</a>\" to all issues and comments originally created by johnsmith@example.com. By default, the email address or username is masked to ensure the user's privacy. Use this option if you want to show the full email address."
msgstr ""
+msgid "<namespace / project>"
+msgstr ""
+
msgid "<no name set>"
msgstr ""
msgid "<no scopes selected>"
msgstr ""
+msgid "<project name>"
+msgstr ""
+
msgid "<strong>%{group_name}</strong> group members"
msgstr "<strong>%{group_name}</strong> 群組æˆå“¡"
@@ -759,6 +779,9 @@ msgstr ""
msgid "A basic page and serverless function that uses AWS Lambda, AWS API Gateway, and GitLab Pages"
msgstr ""
+msgid "A complete DevOps platform"
+msgstr ""
+
msgid "A default branch cannot be chosen for an empty project."
msgstr ""
@@ -975,6 +998,12 @@ msgstr ""
msgid "AccessTokens|reset it"
msgstr ""
+msgid "AccessibilityReport|Accessibility report artifact not found"
+msgstr ""
+
+msgid "AccessibilityReport|Failed to retrieve accessibility report"
+msgstr ""
+
msgid "AccessibilityReport|Learn More"
msgstr ""
@@ -984,6 +1013,9 @@ msgstr ""
msgid "AccessibilityReport|New"
msgstr ""
+msgid "AccessibilityReport|The accessibility scanning found an error of the following type: %{code}"
+msgstr ""
+
msgid "Account"
msgstr "帳戶"
@@ -1225,6 +1257,9 @@ msgstr ""
msgid "Added at"
msgstr ""
+msgid "Added for this merge request"
+msgstr ""
+
msgid "Added in this version"
msgstr ""
@@ -1595,6 +1630,72 @@ msgid "Alert"
msgid_plural "Alerts"
msgstr[0] "æ示"
+msgid "Alert Details"
+msgstr ""
+
+msgid "AlertManagement|Acknowledged"
+msgstr ""
+
+msgid "AlertManagement|Alert"
+msgstr ""
+
+msgid "AlertManagement|Authorize external service"
+msgstr ""
+
+msgid "AlertManagement|Display alerts from all your monitoring tools directly within GitLab. Streamline the investigation of your alerts and the escalation of alerts to incidents."
+msgstr ""
+
+msgid "AlertManagement|End time"
+msgstr ""
+
+msgid "AlertManagement|End time:"
+msgstr ""
+
+msgid "AlertManagement|Events"
+msgstr ""
+
+msgid "AlertManagement|Events:"
+msgstr ""
+
+msgid "AlertManagement|Full Alert Details"
+msgstr ""
+
+msgid "AlertManagement|More information"
+msgstr ""
+
+msgid "AlertManagement|No alerts available to display. If you think you're seeing this message in error, refresh the page."
+msgstr ""
+
+msgid "AlertManagement|No alerts to display."
+msgstr ""
+
+msgid "AlertManagement|Overview"
+msgstr ""
+
+msgid "AlertManagement|Resolved"
+msgstr ""
+
+msgid "AlertManagement|Severity"
+msgstr ""
+
+msgid "AlertManagement|Start time"
+msgstr ""
+
+msgid "AlertManagement|Start time:"
+msgstr ""
+
+msgid "AlertManagement|Status"
+msgstr ""
+
+msgid "AlertManagement|Surface alerts in GitLab"
+msgstr ""
+
+msgid "AlertManagement|There was an error displaying the alerts. Confirm your endpoint's configuration details to ensure alerts appear."
+msgstr ""
+
+msgid "AlertManagement|Triggered"
+msgstr ""
+
msgid "AlertService|%{linkStart}Learn more%{linkEnd} about configuring this endpoint to receive alerts."
msgstr ""
@@ -1664,6 +1765,9 @@ msgstr ""
msgid "All security scans are enabled because %{linkStart}Auto DevOps%{linkEnd} is enabled on this project"
msgstr ""
+msgid "All threads resolved"
+msgstr ""
+
msgid "All users"
msgstr "所有使用者"
@@ -1682,6 +1786,9 @@ msgstr ""
msgid "Allow only the selected protocols to be used for Git access."
msgstr ""
+msgid "Allow owners to manage default branch protection per group"
+msgstr ""
+
msgid "Allow owners to manually add users outside of LDAP"
msgstr ""
@@ -1790,6 +1897,9 @@ msgstr ""
msgid "An error occurred fetching the dropdown data."
msgstr ""
+msgid "An error occurred fetching the project authors."
+msgstr ""
+
msgid "An error occurred previewing the blob"
msgstr "é è¦½ blob 檔案時發生錯誤"
@@ -1871,6 +1981,9 @@ msgstr ""
msgid "An error occurred while fetching sidebar data"
msgstr "讀å–å´é‚Šæ¬„資料時發生錯誤"
+msgid "An error occurred while fetching terraform reports."
+msgstr ""
+
msgid "An error occurred while fetching the Service Desk address."
msgstr ""
@@ -2108,6 +2221,9 @@ msgstr ""
msgid "Any"
msgstr ""
+msgid "Any Author"
+msgstr ""
+
msgid "Any Label"
msgstr ""
@@ -2319,10 +2435,10 @@ msgstr ""
msgid "Are you sure that you want to unarchive this project?"
msgstr ""
-msgid "Are you sure you want to cancel creating this comment?"
+msgid "Are you sure you want to cancel editing this comment?"
msgstr ""
-msgid "Are you sure you want to cancel editing this comment?"
+msgid "Are you sure you want to close this blocked issue?"
msgstr ""
msgid "Are you sure you want to delete %{name}?"
@@ -2352,6 +2468,9 @@ msgstr ""
msgid "Are you sure you want to deploy this environment?"
msgstr ""
+msgid "Are you sure you want to discard this comment?"
+msgstr ""
+
msgid "Are you sure you want to erase this build?"
msgstr ""
@@ -2527,6 +2646,9 @@ msgstr ""
msgid "At least one approval from a code owner is required to change files matching the respective CODEOWNER rules."
msgstr ""
+msgid "At least one logging option is required to be enabled"
+msgstr ""
+
msgid "At least one of group_id or project_id must be specified"
msgstr ""
@@ -2564,6 +2686,27 @@ msgstr ""
msgid "AuditEvents|Target"
msgstr ""
+msgid "AuditLogs|(removed)"
+msgstr ""
+
+msgid "AuditLogs|Action"
+msgstr ""
+
+msgid "AuditLogs|Author"
+msgstr ""
+
+msgid "AuditLogs|Date"
+msgstr ""
+
+msgid "AuditLogs|IP Address"
+msgstr ""
+
+msgid "AuditLogs|Object"
+msgstr ""
+
+msgid "AuditLogs|Target"
+msgstr ""
+
msgid "Aug"
msgstr "八月"
@@ -2846,9 +2989,6 @@ msgstr ""
msgid "BambooService|You must set up automatic revision labeling and a repository trigger in Bamboo."
msgstr ""
-msgid "Batch operations"
-msgstr ""
-
msgid "BatchComments|Delete all pending comments"
msgstr ""
@@ -2978,6 +3118,9 @@ msgstr ""
msgid "Boards"
msgstr ""
+msgid "Boards and Board Lists"
+msgstr ""
+
msgid "Boards|Collapse"
msgstr ""
@@ -3197,6 +3340,9 @@ msgstr ""
msgid "Buy GitLab Enterprise Edition"
msgstr ""
+msgid "Buy more Pipeline minutes"
+msgstr ""
+
msgid "By %{user_name}"
msgstr ""
@@ -3317,6 +3463,9 @@ msgstr ""
msgid "Can't scan the code?"
msgstr ""
+msgid "Can't update snippet: %{err}"
+msgstr ""
+
msgid "Canary"
msgstr ""
@@ -3485,6 +3634,9 @@ msgstr ""
msgid "Changing group path can have unintended side effects."
msgstr ""
+msgid "Charts"
+msgstr ""
+
msgid "Charts can't be displayed as the request for data has timed out. %{documentationLink}"
msgstr ""
@@ -3521,9 +3673,6 @@ msgstr ""
msgid "ChatMessage|and [%{count} more](%{pipeline_failed_jobs_url})"
msgstr ""
-msgid "ChatMessage|failed"
-msgstr ""
-
msgid "ChatMessage|has failed"
msgstr ""
@@ -3539,9 +3688,6 @@ msgstr ""
msgid "ChatMessage|in %{project_link}"
msgstr ""
-msgid "ChatMessage|passed"
-msgstr ""
-
msgid "Check again"
msgstr ""
@@ -3992,9 +4138,6 @@ msgstr ""
msgid "Click any <strong>project name</strong> in the project list below to navigate to the project milestone."
msgstr "在專案列表點擊任何<strong>專案å稱</strong>,將轉跳到專案的里程碑。"
-msgid "Click here"
-msgstr ""
-
msgid "Click the <strong>Download</strong> button and wait for downloading to complete."
msgstr ""
@@ -4382,6 +4525,12 @@ msgstr ""
msgid "ClusterIntegration|Fetching zones"
msgstr ""
+msgid "ClusterIntegration|Fluentd"
+msgstr ""
+
+msgid "ClusterIntegration|Fluentd is an open source data collector, which lets you unify the data collection and consumption for a better use and understanding of data. It requires at least one of the following logs to be successfully installed."
+msgstr ""
+
msgid "ClusterIntegration|GitLab Integration"
msgstr ""
@@ -4688,6 +4837,15 @@ msgstr ""
msgid "ClusterIntegration|Request to begin uninstalling failed"
msgstr ""
+msgid "ClusterIntegration|SIEM Hostname"
+msgstr ""
+
+msgid "ClusterIntegration|SIEM Port"
+msgstr ""
+
+msgid "ClusterIntegration|SIEM Protocol"
+msgstr ""
+
msgid "ClusterIntegration|Save changes"
msgstr ""
@@ -4784,6 +4942,12 @@ msgstr ""
msgid "ClusterIntegration|Select zone to choose machine type"
msgstr ""
+msgid "ClusterIntegration|Send Cilium Logs"
+msgstr ""
+
+msgid "ClusterIntegration|Send ModSecurity Logs"
+msgstr ""
+
msgid "ClusterIntegration|Service Token"
msgstr ""
@@ -5211,18 +5375,33 @@ msgstr ""
msgid "Compliance framework (optional)"
msgstr ""
+msgid "ComplianceFramework|GDPR"
+msgstr ""
+
msgid "ComplianceFramework|GDPR - General Data Protection Regulation"
msgstr ""
+msgid "ComplianceFramework|HIPAA"
+msgstr ""
+
msgid "ComplianceFramework|HIPAA - Health Insurance Portability and Accountability Act"
msgstr ""
+msgid "ComplianceFramework|PCI-DSS"
+msgstr ""
+
msgid "ComplianceFramework|PCI-DSS - Payment Card Industry-Data Security Standard"
msgstr ""
+msgid "ComplianceFramework|SOC 2"
+msgstr ""
+
msgid "ComplianceFramework|SOC 2 - Service Organization Control 2"
msgstr ""
+msgid "ComplianceFramework|SOX"
+msgstr ""
+
msgid "ComplianceFramework|SOX - Sarbanes-Oxley"
msgstr ""
@@ -5370,9 +5549,15 @@ msgstr ""
msgid "Container repositories sync capacity"
msgstr ""
+msgid "ContainerRegistry| Please visit the %{linkStart}administration settings%{linkEnd} to enable this feature."
+msgstr ""
+
msgid "ContainerRegistry|%{imageName} tags"
msgstr ""
+msgid "ContainerRegistry|%{title} was successfully scheduled for deletion"
+msgstr ""
+
msgid "ContainerRegistry|Automatically remove extra images that aren't designed to be kept."
msgstr ""
@@ -5385,6 +5570,9 @@ msgstr ""
msgid "ContainerRegistry|Container Registry"
msgstr ""
+msgid "ContainerRegistry|Container Registry tag expiration and retention policy is disabled"
+msgstr ""
+
msgid "ContainerRegistry|Copy build command"
msgstr ""
@@ -5394,18 +5582,12 @@ msgstr ""
msgid "ContainerRegistry|Copy push command"
msgstr ""
-msgid "ContainerRegistry|Currently, the Container Registry tag expiration feature is not available for projects created before GitLab version 12.8. For updates and more information, visit Issue %{linkStart}#196124%{linkEnd}"
-msgstr ""
-
msgid "ContainerRegistry|Docker connection error"
msgstr ""
msgid "ContainerRegistry|Docker tag expiration policy is %{toggleStatus}"
msgstr ""
-msgid "ContainerRegistry|Docker tags with names matching this regex pattern will expire:"
-msgstr ""
-
msgid "ContainerRegistry|Edit Settings"
msgstr ""
@@ -5427,9 +5609,6 @@ msgstr ""
msgid "ContainerRegistry|Image ID"
msgstr ""
-msgid "ContainerRegistry|Image deleted successfully"
-msgstr ""
-
msgid "ContainerRegistry|Keep and protect the images that matter most."
msgstr ""
@@ -5445,12 +5624,24 @@ msgstr ""
msgid "ContainerRegistry|Number of tags to retain:"
msgstr ""
+msgid "ContainerRegistry|Please contact your administrator."
+msgstr ""
+
msgid "ContainerRegistry|Push an image"
msgstr ""
msgid "ContainerRegistry|Quick Start"
msgstr ""
+msgid "ContainerRegistry|Regular expressions such as %{codeStart}.*-test%{codeEnd} or %{codeStart}dev-.*%{codeEnd} are supported"
+msgstr ""
+
+msgid "ContainerRegistry|Regular expressions such as %{codeStart}.*-test%{codeEnd} or %{codeStart}dev-.*%{codeEnd} are supported. To select all tags, use %{codeStart}.*%{codeEnd}"
+msgstr ""
+
+msgid "ContainerRegistry|Remember to run %{docLinkStart}garbage collection%{docLinkEnd} to remove the stale data from storage."
+msgstr ""
+
msgid "ContainerRegistry|Remove repository"
msgstr ""
@@ -5464,22 +5655,22 @@ msgstr[0] ""
msgid "ContainerRegistry|Retention policy has been Enabled"
msgstr ""
-msgid "ContainerRegistry|Something went wrong while deleting the image."
+msgid "ContainerRegistry|Something went wrong while fetching the expiration policy."
msgstr ""
-msgid "ContainerRegistry|Something went wrong while deleting the tag."
+msgid "ContainerRegistry|Something went wrong while fetching the repository list."
msgstr ""
-msgid "ContainerRegistry|Something went wrong while deleting the tags."
+msgid "ContainerRegistry|Something went wrong while fetching the tags list."
msgstr ""
-msgid "ContainerRegistry|Something went wrong while fetching the expiration policy."
+msgid "ContainerRegistry|Something went wrong while marking the tag for deletion."
msgstr ""
-msgid "ContainerRegistry|Something went wrong while fetching the packages list."
+msgid "ContainerRegistry|Something went wrong while marking the tags for deletion."
msgstr ""
-msgid "ContainerRegistry|Something went wrong while fetching the tags list."
+msgid "ContainerRegistry|Something went wrong while scheduling %{title} for deletion. Please try again."
msgstr ""
msgid "ContainerRegistry|Something went wrong while updating the expiration policy."
@@ -5488,16 +5679,25 @@ msgstr ""
msgid "ContainerRegistry|Tag"
msgstr ""
-msgid "ContainerRegistry|Tag deleted successfully"
-msgstr ""
-
msgid "ContainerRegistry|Tag expiration policy"
msgstr ""
msgid "ContainerRegistry|Tag expiration policy is designed to:"
msgstr ""
-msgid "ContainerRegistry|Tags deleted successfully"
+msgid "ContainerRegistry|Tag successfully marked for deletion."
+msgstr ""
+
+msgid "ContainerRegistry|Tags successfully marked for deletion."
+msgstr ""
+
+msgid "ContainerRegistry|Tags with names matching this regex pattern will %{italicStart}be preserved:%{italicEnd}"
+msgstr ""
+
+msgid "ContainerRegistry|Tags with names matching this regex pattern will %{italicStart}expire:%{italicEnd}"
+msgstr ""
+
+msgid "ContainerRegistry|The Container Registry tag expiration and retention policies for this project have not been enabled."
msgstr ""
msgid "ContainerRegistry|The last tag related to this image was recently removed. This empty image and any associated data will be automatically removed as part of the regular Garbage Collection process. If you have any questions, contact your administrator."
@@ -5518,16 +5718,16 @@ msgstr ""
msgid "ContainerRegistry|There are no container images stored for this project"
msgstr ""
-msgid "ContainerRegistry|This Registry contains deleted image tag data. Remember to run %{docLinkStart}garbage collection%{docLinkEnd} to remove the stale data from storage."
+msgid "ContainerRegistry|There was an error during the deletion of this image repository, please try again."
msgstr ""
msgid "ContainerRegistry|This image has no active tags"
msgstr ""
-msgid "ContainerRegistry|We are having trouble connecting to Docker, which could be due to an issue with your project name or path. %{docLinkStart}More Information%{docLinkEnd}"
+msgid "ContainerRegistry|This image repository is scheduled for deletion"
msgstr ""
-msgid "ContainerRegistry|Wildcards such as %{codeStart}.*-stable%{codeEnd} or %{codeStart}production/.*%{codeEnd} are supported. To select all tags, use %{codeStart}.*%{codeEnd}"
+msgid "ContainerRegistry|We are having trouble connecting to Docker, which could be due to an issue with your project name or path. %{docLinkStart}More Information%{docLinkEnd}"
msgstr ""
msgid "ContainerRegistry|With the Container Registry, every project can have its own space to store its Docker images. %{docLinkStart}More Information%{docLinkEnd}"
@@ -5782,7 +5982,7 @@ msgstr ""
msgid "Could not delete chat nickname %{chat_name}."
msgstr ""
-msgid "Could not find design"
+msgid "Could not find design."
msgstr ""
msgid "Could not remove the trigger."
@@ -5797,6 +5997,9 @@ msgstr ""
msgid "Could not revoke personal access token %{personal_access_token_name}."
msgstr ""
+msgid "Could not revoke project access token %{project_access_token_name}."
+msgstr ""
+
msgid "Could not save group ID"
msgstr ""
@@ -5854,6 +6057,9 @@ msgstr ""
msgid "Create a new branch"
msgstr ""
+msgid "Create a new deploy key for this project"
+msgstr ""
+
msgid "Create a new file as there are no files yet. Afterwards, you'll be able to commit your changes."
msgstr ""
@@ -5959,6 +6165,9 @@ msgstr "建立專案標籤"
msgid "Create requirement"
msgstr ""
+msgid "Create snippet"
+msgstr ""
+
msgid "Create wildcard: %{searchTerm}"
msgstr ""
@@ -5980,6 +6189,9 @@ msgstr "創建個人訪å•ä»¤ç‰Œ"
msgid "Created"
msgstr ""
+msgid "Created %{timestamp}"
+msgstr ""
+
msgid "Created At"
msgstr ""
@@ -6335,6 +6547,9 @@ msgstr ""
msgid "CycleAnalytics|stage dropdown"
msgstr ""
+msgid "DAG"
+msgstr ""
+
msgid "DNS"
msgstr ""
@@ -6695,6 +6910,9 @@ msgstr ""
msgid "Deploy key was successfully updated."
msgstr ""
+msgid "Deploy keys allow read-only or read-write (if enabled) access to your repository. Deploy keys can be used for CI, staging or production servers. You can create a deploy key or add an existing one."
+msgstr ""
+
msgid "Deploy progress not found. To see pods, ensure your environment matches %{linkStart}deploy board criteria%{linkEnd}."
msgstr ""
@@ -6848,6 +7066,9 @@ msgstr ""
msgid "Deploying to"
msgstr ""
+msgid "Deployment Frequency"
+msgstr ""
+
msgid "Deployment|API"
msgstr ""
@@ -7365,9 +7586,6 @@ msgstr ""
msgid "Edit environment"
msgstr ""
-msgid "Edit epic description"
-msgstr ""
-
msgid "Edit file"
msgstr ""
@@ -7989,9 +8207,6 @@ msgstr ""
msgid "Epics"
msgstr ""
-msgid "Epics (Ultimate / Gold license only)"
-msgstr ""
-
msgid "Epics Roadmap"
msgstr ""
@@ -8001,6 +8216,9 @@ msgstr ""
msgid "Epics let you manage your portfolio of projects more efficiently and with less effort"
msgstr ""
+msgid "Epics, Issues, and Merge Requests"
+msgstr ""
+
msgid "Epics|Add an epic"
msgstr ""
@@ -8052,6 +8270,9 @@ msgstr ""
msgid "Epics|Something went wrong while fetching group epics."
msgstr ""
+msgid "Epics|Something went wrong while moving item."
+msgstr ""
+
msgid "Epics|Something went wrong while ordering item."
msgstr ""
@@ -8106,6 +8327,9 @@ msgstr "讀å–標籤時發生錯誤。"
msgid "Error fetching network graph."
msgstr ""
+msgid "Error fetching payload data."
+msgstr ""
+
msgid "Error fetching projects"
msgstr ""
@@ -8115,9 +8339,6 @@ msgstr ""
msgid "Error fetching the dependency list. Please check your network connection and try again."
msgstr ""
-msgid "Error fetching usage ping data."
-msgstr ""
-
msgid "Error loading branch data. Please try again."
msgstr ""
@@ -8259,6 +8480,9 @@ msgstr ""
msgid "Errors"
msgstr ""
+msgid "Errors:"
+msgstr ""
+
msgid "Estimated"
msgstr ""
@@ -8463,12 +8687,18 @@ msgstr ""
msgid "Export as CSV"
msgstr ""
+msgid "Export group"
+msgstr ""
+
msgid "Export issues"
msgstr ""
msgid "Export project"
msgstr ""
+msgid "Export this group with all related data to a new GitLab instance. Once complete, you can import the data file from the \"New Group\" page."
+msgstr ""
+
msgid "Export this project with all its related data in order to move your project to a new GitLab instance. Once the export is finished, you can import the file from the \"New Project\" page."
msgstr ""
@@ -8538,9 +8768,6 @@ msgstr ""
msgid "Failed Jobs"
msgstr ""
-msgid "Failed create wiki"
-msgstr ""
-
msgid "Failed to add a Zoom meeting"
msgstr ""
@@ -8580,6 +8807,9 @@ msgstr ""
msgid "Failed to create resources"
msgstr ""
+msgid "Failed to create wiki"
+msgstr ""
+
msgid "Failed to delete board. Please try again."
msgstr ""
@@ -8589,7 +8819,7 @@ msgstr ""
msgid "Failed to enqueue the rebase operation, possibly due to a long-lived transaction. Try again later."
msgstr ""
-msgid "Failed to find import label for jira import."
+msgid "Failed to find import label for Jira import."
msgstr ""
msgid "Failed to get ref."
@@ -8613,6 +8843,12 @@ msgstr ""
msgid "Failed to load groups & users."
msgstr ""
+msgid "Failed to load labels. Please try again."
+msgstr ""
+
+msgid "Failed to load milestones. Please try again."
+msgstr ""
+
msgid "Failed to load related branches"
msgstr ""
@@ -8988,7 +9224,10 @@ msgstr "按æ交消æ¯éŽæ¿¾"
msgid "Filter by milestone name"
msgstr "é€éŽé‡Œç¨‹ç¢‘å稱篩é¸"
-msgid "Filter by name..."
+msgid "Filter by name"
+msgstr ""
+
+msgid "Filter by status"
msgstr ""
msgid "Filter by two-factor authentication"
@@ -9000,12 +9239,18 @@ msgstr ""
msgid "Filter projects"
msgstr ""
+msgid "Filter results"
+msgstr ""
+
msgid "Filter results by group"
msgstr ""
msgid "Filter results by project"
msgstr ""
+msgid "Filter results..."
+msgstr ""
+
msgid "Filter your projects by name"
msgstr ""
@@ -9294,10 +9539,10 @@ msgstr ""
msgid "GeoNodes|Checksummed"
msgstr ""
-msgid "GeoNodes|Container repositories"
+msgid "GeoNodes|Consult Geo troubleshooting information"
msgstr ""
-msgid "GeoNodes|Data is out of date from %{timeago}"
+msgid "GeoNodes|Container repositories"
msgstr ""
msgid "GeoNodes|Data replication lag"
@@ -9339,6 +9584,9 @@ msgstr ""
msgid "GeoNodes|Last event ID seen from primary"
msgstr ""
+msgid "GeoNodes|Learn more about Geo node statuses"
+msgstr ""
+
msgid "GeoNodes|Loading nodes"
msgstr ""
@@ -9354,6 +9602,9 @@ msgstr ""
msgid "GeoNodes|Node was successfully removed."
msgstr ""
+msgid "GeoNodes|Node's status was updated %{timeAgo}."
+msgstr ""
+
msgid "GeoNodes|Not checksummed"
msgstr ""
@@ -9384,7 +9635,10 @@ msgstr ""
msgid "GeoNodes|Repository verification progress"
msgstr ""
-msgid "GeoNodes|Selective"
+msgid "GeoNodes|Selective (%{syncLabel})"
+msgstr ""
+
+msgid "GeoNodes|Selective synchronization"
msgstr ""
msgid "GeoNodes|Something went wrong while changing node status"
@@ -9411,6 +9665,9 @@ msgstr ""
msgid "GeoNodes|Unverified"
msgstr ""
+msgid "GeoNodes|Updated %{timeAgo}"
+msgstr ""
+
msgid "GeoNodes|Used slots"
msgstr ""
@@ -9450,16 +9707,16 @@ msgstr ""
msgid "Geo|All"
msgstr ""
-msgid "Geo|All projects"
+msgid "Geo|All %{replicable_type}"
msgstr ""
-msgid "Geo|All projects are being scheduled for re-sync"
+msgid "Geo|All projects"
msgstr ""
-msgid "Geo|All projects are being scheduled for re-verify"
+msgid "Geo|All projects are being scheduled for resync"
msgstr ""
-msgid "Geo|Batch operations"
+msgid "Geo|All projects are being scheduled for reverify"
msgstr ""
msgid "Geo|Could not remove tracking entry for an existing project."
@@ -9471,9 +9728,15 @@ msgstr ""
msgid "Geo|Failed"
msgstr ""
+msgid "Geo|Filter by status"
+msgstr ""
+
msgid "Geo|Geo Status"
msgstr ""
+msgid "Geo|In progress"
+msgstr ""
+
msgid "Geo|In sync"
msgstr ""
@@ -9498,9 +9761,6 @@ msgstr ""
msgid "Geo|Not synced yet"
msgstr ""
-msgid "Geo|Pending"
-msgstr ""
-
msgid "Geo|Pending synchronization"
msgstr ""
@@ -9537,7 +9797,7 @@ msgstr ""
msgid "Geo|Resync"
msgstr ""
-msgid "Geo|Resync all projects"
+msgid "Geo|Resync all"
msgstr ""
msgid "Geo|Retry count"
@@ -9546,7 +9806,7 @@ msgstr ""
msgid "Geo|Reverify"
msgstr ""
-msgid "Geo|Reverify all projects"
+msgid "Geo|Reverify all"
msgstr ""
msgid "Geo|Status"
@@ -9888,9 +10148,6 @@ msgstr ""
msgid "Go to file"
msgstr ""
-msgid "Go to file (MRs only)"
-msgstr ""
-
msgid "Go to file permalink (while viewing a file)"
msgstr ""
@@ -10335,6 +10592,9 @@ msgstr ""
msgid "GroupSettings|Disable group mentions"
msgstr ""
+msgid "GroupSettings|Export group"
+msgstr ""
+
msgid "GroupSettings|If the parent group's visibility is lower than the group current visibility, visibility levels for subgroups and projects will be changed to match the new parent group's visibility."
msgstr ""
@@ -10413,6 +10673,9 @@ msgstr "群組"
msgid "Groups (%{count})"
msgstr ""
+msgid "Groups (%{groups})"
+msgstr ""
+
msgid "Groups can also be nested by creating %{subgroup_docs_link_start}subgroups%{subgroup_docs_link_end}."
msgstr ""
@@ -10676,15 +10939,12 @@ msgstr ""
msgid "ID:"
msgstr ""
-msgid "IDE|Allow live previews of JavaScript projects in the Web IDE using CodeSandbox client side evaluation."
+msgid "IDE|Allow live previews of JavaScript projects in the Web IDE using CodeSandbox Live Preview."
msgstr ""
msgid "IDE|Back"
msgstr ""
-msgid "IDE|Client side evaluation"
-msgstr ""
-
msgid "IDE|Commit"
msgstr ""
@@ -10748,6 +11008,9 @@ msgstr ""
msgid "If any job surpasses this timeout threshold, it will be marked as failed. Human readable time input language is accepted like \"1 hour\". Values without specification represent seconds."
msgstr ""
+msgid "If blank, set allowable lifetime to %{instance_level_policy_in_words}, as defined by the instance admin. Once set, existing tokens for users in this group may be revoked."
+msgstr ""
+
msgid "If checked, group owners can manage LDAP group links and LDAP member overrides"
msgstr ""
@@ -10787,6 +11050,9 @@ msgstr ""
msgid "If you lose your recovery codes you can generate new ones, invalidating all previous codes."
msgstr ""
+msgid "If you reach 100%% storage capacity, you will not be able to: %{base_message}"
+msgstr ""
+
msgid "If your HTTP repository is not publicly accessible, add your credentials."
msgstr ""
@@ -10898,6 +11164,9 @@ msgstr ""
msgid "Import repository"
msgstr "導入存儲庫"
+msgid "Import started by: %{importInitiator}"
+msgstr ""
+
msgid "Import tasks"
msgstr ""
@@ -10964,6 +11233,9 @@ msgstr ""
msgid "In order to tailor your experience with GitLab we<br>would like to know a bit more about you."
msgstr ""
+msgid "In progress"
+msgstr ""
+
msgid "In the next step, you'll be able to select the projects you want to import."
msgstr ""
@@ -11310,15 +11582,15 @@ msgstr ""
msgid "Issues"
msgstr "議題"
-msgid "Issues / Merge Requests"
-msgstr ""
-
msgid "Issues Analytics"
msgstr ""
msgid "Issues Rate Limits"
msgstr ""
+msgid "Issues and Merge Requests"
+msgstr ""
+
msgid "Issues can be bugs, tasks or ideas to be discussed. Also, issues are searchable and filterable."
msgstr ""
@@ -11340,10 +11612,10 @@ msgstr ""
msgid "IssuesAnalytics|Avg/Month:"
msgstr ""
-msgid "IssuesAnalytics|Issues created"
+msgid "IssuesAnalytics|Issues opened"
msgstr ""
-msgid "IssuesAnalytics|Issues created per month"
+msgid "IssuesAnalytics|Issues opened per month"
msgstr ""
msgid "IssuesAnalytics|Last 12 months"
@@ -11397,6 +11669,9 @@ msgstr ""
msgid "Jira integration not configured."
msgstr ""
+msgid "Jira project: %{importProject}"
+msgstr ""
+
msgid "JiraService|Events for %{noteable_model_name} are disabled."
msgstr ""
@@ -11785,6 +12060,9 @@ msgstr ""
msgid "Lead"
msgstr ""
+msgid "Lead Time"
+msgstr ""
+
msgid "Learn GitLab"
msgstr ""
@@ -11797,6 +12075,9 @@ msgstr ""
msgid "Learn how to %{no_packages_link_start}publish and share your packages%{no_packages_link_end} with GitLab."
msgstr ""
+msgid "Learn how to enable synchronization"
+msgstr ""
+
msgid "Learn more"
msgstr ""
@@ -12123,9 +12404,6 @@ msgstr ""
msgid "Live preview"
msgstr ""
-msgid "Load more vulnerabilities"
-msgstr ""
-
msgid "Loading"
msgstr ""
@@ -12276,9 +12554,6 @@ msgstr ""
msgid "Manage"
msgstr ""
-msgid "Manage Git repositories with fine-grained access controls that keep your code secure. Perform code reviews and enhance collaboration with merge requests. Each project can also have an issue tracker and a wiki."
-msgstr ""
-
msgid "Manage Web IDE features"
msgstr "管ç†ç¶²é  IDE 功能"
@@ -12309,6 +12584,9 @@ msgstr ""
msgid "Manage your license"
msgstr ""
+msgid "Managed Account"
+msgstr ""
+
msgid "Manifest"
msgstr ""
@@ -12573,6 +12851,9 @@ msgstr ""
msgid "Merge Requests in Review"
msgstr ""
+msgid "Merge automatically (%{strategy})"
+msgstr ""
+
msgid "Merge commit message"
msgstr ""
@@ -12726,6 +13007,12 @@ msgstr "å·²åˆä½µ"
msgid "Merged branches are being deleted. This can take some time depending on the number of branches. Please refresh the page to see changes."
msgstr ""
+msgid "Merged this merge request."
+msgstr ""
+
+msgid "Merges this merge request immediately."
+msgstr ""
+
msgid "Merges this merge request when the pipeline succeeds."
msgstr ""
@@ -12832,12 +13119,15 @@ msgid "Metrics|Edit metric"
msgid_plural "Metrics|Edit metrics"
msgstr[0] ""
-msgid "Metrics|Environment"
+msgid "Metrics|Expand panel"
msgstr ""
msgid "Metrics|For grouping similar metrics"
msgstr ""
+msgid "Metrics|Go back (Esc)"
+msgstr ""
+
msgid "Metrics|Invalid time range, please verify."
msgstr ""
@@ -12868,9 +13158,6 @@ msgstr ""
msgid "Metrics|Refresh dashboard"
msgstr ""
-msgid "Metrics|Show last"
-msgstr ""
-
msgid "Metrics|There was an error creating the dashboard."
msgstr ""
@@ -12949,6 +13236,9 @@ msgstr ""
msgid "Microsoft Azure"
msgstr ""
+msgid "Middleman project with Static Site Editor support"
+msgstr ""
+
msgid "Migrated %{success_count}/%{total_count} files."
msgstr ""
@@ -13392,10 +13682,10 @@ msgstr ""
msgid "Next"
msgstr ""
-msgid "Next file in diff (MRs only)"
+msgid "Next file in diff"
msgstr ""
-msgid "Next unresolved discussion (MRs only)"
+msgid "Next unresolved discussion"
msgstr ""
msgid "Nickname"
@@ -13455,6 +13745,9 @@ msgstr ""
msgid "No changes between %{ref_start}%{source_branch}%{ref_end} and %{ref_start}%{target_branch}%{ref_end}"
msgstr ""
+msgid "No child epics match applied filters"
+msgstr ""
+
msgid "No connection could be made to a Gitaly Server, please check your logs!"
msgstr ""
@@ -13593,15 +13886,6 @@ msgstr ""
msgid "No thanks, don't show this again"
msgstr ""
-msgid "No vulnerabilities found for this group"
-msgstr ""
-
-msgid "No vulnerabilities found for this pipeline"
-msgstr ""
-
-msgid "No vulnerabilities found for this project"
-msgstr ""
-
msgid "No vulnerabilities present"
msgstr ""
@@ -13725,6 +14009,9 @@ msgstr ""
msgid "Nothing to preview."
msgstr ""
+msgid "Nothing to synchronize"
+msgstr ""
+
msgid "Notification events"
msgstr "通知事件"
@@ -13896,6 +14183,9 @@ msgstr ""
msgid "Once removed, the fork relationship cannot be restored and you will no longer be able to send merge requests to the source."
msgstr ""
+msgid "Once the exported file is ready you can download it from this page."
+msgstr ""
+
msgid "Once the exported file is ready, you will receive a notification email with a download link, or you can download it from this page."
msgstr ""
@@ -13996,9 +14286,6 @@ msgstr "打開原文件"
msgid "Open sidebar"
msgstr ""
-msgid "Open source software to collaborate on code"
-msgstr ""
-
msgid "Open: %{openIssuesCount}"
msgstr ""
@@ -14101,6 +14388,9 @@ msgstr ""
msgid "OutdatedBrowser|You can provide feedback %{feedback_link_start}on this issue%{feedback_link_end} or via your usual support channels."
msgstr ""
+msgid "Overridden"
+msgstr ""
+
msgid "Overview"
msgstr "概覽"
@@ -14152,6 +14442,9 @@ msgstr ""
msgid "PackageRegistry|Conan Command"
msgstr ""
+msgid "PackageRegistry|Copy .pypirc content"
+msgstr ""
+
msgid "PackageRegistry|Copy Conan Command"
msgstr ""
@@ -14173,6 +14466,9 @@ msgstr ""
msgid "PackageRegistry|Copy NuGet Setup Command"
msgstr ""
+msgid "PackageRegistry|Copy Pip command"
+msgstr ""
+
msgid "PackageRegistry|Copy and paste this inside your %{codeStart}pom.xml%{codeEnd} %{codeStart}dependencies%{codeEnd} block."
msgstr ""
@@ -14206,6 +14502,12 @@ msgstr ""
msgid "PackageRegistry|For more information on the NuGet registry, %{linkStart}see the documentation%{linkEnd}."
msgstr ""
+msgid "PackageRegistry|For more information on the PyPi registry, %{linkStart}see the documentation%{linkEnd}."
+msgstr ""
+
+msgid "PackageRegistry|If you haven't already done so, you will need to add the below to your %{codeStart}.pypirc%{codeEnd} file."
+msgstr ""
+
msgid "PackageRegistry|If you haven't already done so, you will need to add the below to your %{codeStart}pom.xml%{codeEnd} file."
msgstr ""
@@ -14215,6 +14517,9 @@ msgstr ""
msgid "PackageRegistry|Learn how to %{noPackagesLinkStart}publish and share your packages%{noPackagesLinkEnd} with GitLab."
msgstr ""
+msgid "PackageRegistry|Manually Published"
+msgstr ""
+
msgid "PackageRegistry|Maven"
msgstr ""
@@ -14233,12 +14538,18 @@ msgstr ""
msgid "PackageRegistry|NuGet Command"
msgstr ""
+msgid "PackageRegistry|Pip Command"
+msgstr ""
+
msgid "PackageRegistry|Pipeline %{linkStart}%{linkEnd} triggered %{timestamp} by %{author}"
msgstr ""
msgid "PackageRegistry|Published to the repository at %{timestamp}"
msgstr ""
+msgid "PackageRegistry|PyPi"
+msgstr ""
+
msgid "PackageRegistry|Registry Setup"
msgstr ""
@@ -14275,6 +14586,9 @@ msgstr ""
msgid "PackageRegistry|npm"
msgstr ""
+msgid "PackageRegistry|published by %{author}"
+msgstr ""
+
msgid "PackageRegistry|yarn"
msgstr ""
@@ -14290,6 +14604,9 @@ msgstr ""
msgid "PackageType|NuGet"
msgstr ""
+msgid "PackageType|PyPi"
+msgstr ""
+
msgid "Packages"
msgstr ""
@@ -14383,6 +14700,9 @@ msgstr ""
msgid "Past due"
msgstr ""
+msgid "Paste a machine public key here. Read more about how to generate it"
+msgstr ""
+
msgid "Paste a machine public key here. Read more about how to generate it %{link_start}here%{link_end}"
msgstr ""
@@ -14398,9 +14718,6 @@ msgstr ""
msgid "Path"
msgstr ""
-msgid "Path, transfer, remove"
-msgstr ""
-
msgid "Path:"
msgstr ""
@@ -14431,7 +14748,7 @@ msgstr ""
msgid "Percentage"
msgstr ""
-msgid "Perform advanced options such as changing path, transferring, or removing the group."
+msgid "Perform advanced options such as changing path, transferring, exporting, or removing the group."
msgstr ""
msgid "Perform common operations on GitLab project"
@@ -14512,9 +14829,6 @@ msgstr "æµæ°´ç·šè¨ˆåŠƒ"
msgid "Pipeline minutes quota"
msgstr ""
-msgid "Pipeline quota"
-msgstr ""
-
msgid "Pipeline subscriptions"
msgstr ""
@@ -14524,6 +14838,9 @@ msgstr ""
msgid "Pipeline: %{status}"
msgstr ""
+msgid "PipelineCharts|CI / CD Analytics"
+msgstr ""
+
msgid "PipelineCharts|Failed:"
msgstr "失敗:"
@@ -14602,15 +14919,6 @@ msgstr ""
msgid "Pipelines settings for '%{project_name}' were successfully updated."
msgstr ""
-msgid "Pipelines| to purchase more minutes."
-msgstr ""
-
-msgid "Pipelines|%{namespace_name} has exceeded its pipeline minutes quota."
-msgstr ""
-
-msgid "Pipelines|%{namespace_name} has less than %{notification_level}%% of CI minutes available."
-msgstr ""
-
msgid "Pipelines|API"
msgstr ""
@@ -14632,10 +14940,13 @@ msgstr ""
msgid "Pipelines|Get started with Pipelines"
msgstr ""
-msgid "Pipelines|Loading Pipelines"
+msgid "Pipelines|Group %{namespace_name} has %{percentage}%% or less Shared Runner Pipeline minutes remaining. Once it runs out, no new jobs or pipelines in its projects will run."
+msgstr ""
+
+msgid "Pipelines|Group %{namespace_name} has exceeded its pipeline minutes quota. Unless you buy additional pipeline minutes, no new jobs or pipelines in its projects will run."
msgstr ""
-msgid "Pipelines|Pipelines will not run anymore on shared Runners."
+msgid "Pipelines|Loading Pipelines"
msgstr ""
msgid "Pipelines|Project cache successfully reset."
@@ -14866,6 +15177,9 @@ msgstr ""
msgid "Please select"
msgstr ""
+msgid "Please select a Jira project"
+msgstr ""
+
msgid "Please select a country"
msgstr ""
@@ -14911,6 +15225,9 @@ msgstr ""
msgid "Please wait while we import the repository for you. Refresh at will."
msgstr ""
+msgid "Plugins directory is deprecated and will be removed in 14.0. Please move this file into /file_hooks directory."
+msgstr ""
+
msgid "Pod does not exist"
msgstr ""
@@ -15028,7 +15345,7 @@ msgstr ""
msgid "Prevent users from changing their profile name"
msgstr ""
-msgid "Prevent users from modifing merge request approvers list"
+msgid "Prevent users from modifying merge request approvers list"
msgstr ""
msgid "Prevent users from performing write operations on GitLab while performing maintenance."
@@ -15049,10 +15366,10 @@ msgstr ""
msgid "Previous Artifacts"
msgstr ""
-msgid "Previous file in diff (MRs only)"
+msgid "Previous file in diff"
msgstr ""
-msgid "Previous unresolved discussion (MRs only)"
+msgid "Previous unresolved discussion"
msgstr ""
msgid "Primary"
@@ -15085,6 +15402,9 @@ msgstr ""
msgid "Private profile"
msgstr ""
+msgid "Private projects Minutes cost factor"
+msgstr ""
+
msgid "Private projects can be created in your personal namespace with:"
msgstr ""
@@ -15913,6 +16233,9 @@ msgstr ""
msgid "ProjectSettings|With GitLab Pages you can host your static websites on GitLab"
msgstr ""
+msgid "ProjectSettings|With Metrics Dashboard you can visualize this project performance metrics"
+msgstr ""
+
msgid "ProjectTemplates|.NET Core"
msgstr ""
@@ -15976,6 +16299,9 @@ msgstr ""
msgid "ProjectTemplates|Spring"
msgstr ""
+msgid "ProjectTemplates|Static Site Editor/Middleman"
+msgstr ""
+
msgid "ProjectTemplates|iOS (Swift)"
msgstr ""
@@ -16195,13 +16521,13 @@ msgstr ""
msgid "PrometheusService|Prometheus is being automatically managed on your clusters"
msgstr ""
-msgid "PrometheusService|These metrics will only be monitored after your first deployment to an environment"
+msgid "PrometheusService|Select the Active checkbox to override the Auto Configuration with custom settings. If unchecked, Auto Configuration settings are used."
msgstr ""
-msgid "PrometheusService|Time-series monitoring service"
+msgid "PrometheusService|These metrics will only be monitored after your first deployment to an environment"
msgstr ""
-msgid "PrometheusService|To enable manual configuration, uninstall Prometheus from your clusters"
+msgid "PrometheusService|Time-series monitoring service"
msgstr ""
msgid "PrometheusService|To enable the installation of Prometheus on your clusters, deactivate the manual configuration below"
@@ -16387,6 +16713,9 @@ msgstr ""
msgid "Protip:"
msgstr ""
+msgid "Protocol"
+msgstr ""
+
msgid "Provider"
msgstr ""
@@ -16411,6 +16740,9 @@ msgstr ""
msgid "Public pipelines"
msgstr ""
+msgid "Public projects Minutes cost factor"
+msgstr ""
+
msgid "Pull"
msgstr ""
@@ -16610,6 +16942,9 @@ msgid "Refreshing in a second to show the updated status..."
msgid_plural "Refreshing in %d seconds to show the updated status..."
msgstr[0] ""
+msgid "Regenerate export"
+msgstr ""
+
msgid "Regenerate instance ID"
msgstr ""
@@ -16986,9 +17321,28 @@ msgstr ""
msgid "Reports|%{combinedString} and %{resolvedString}"
msgstr ""
+msgid "Reports|Accessibility scanning detected %d issue for the source branch only"
+msgid_plural "Reports|Accessibility scanning detected %d issues for the source branch only"
+msgstr[0] ""
+
+msgid "Reports|Accessibility scanning detected no issues for the source branch only"
+msgstr ""
+
+msgid "Reports|Accessibility scanning failed loading results"
+msgstr ""
+
+msgid "Reports|Accessibility scanning results are being parsed"
+msgstr ""
+
msgid "Reports|Actions"
msgstr ""
+msgid "Reports|An error occured while loading report"
+msgstr ""
+
+msgid "Reports|An error occurred while loading %{name} results"
+msgstr ""
+
msgid "Reports|Class"
msgstr ""
@@ -17094,7 +17448,7 @@ msgstr ""
msgid "Requested %{time_ago}"
msgstr ""
-msgid "Requested design version does not exist"
+msgid "Requested design version does not exist."
msgstr ""
msgid "Requested states are invalid"
@@ -17276,6 +17630,9 @@ msgstr ""
msgid "Resync"
msgstr ""
+msgid "Resync all"
+msgstr ""
+
msgid "Resync all %{replicableType}"
msgstr ""
@@ -17343,6 +17700,9 @@ msgstr ""
msgid "Revoked personal access token %{personal_access_token_name}!"
msgstr ""
+msgid "Revoked project access token %{project_access_token_name}!"
+msgstr ""
+
msgid "RightSidebar|adding a"
msgstr ""
@@ -17391,6 +17751,9 @@ msgstr ""
msgid "Runner token"
msgstr ""
+msgid "Runner tokens"
+msgstr ""
+
msgid "Runner was not updated."
msgstr ""
@@ -17445,6 +17808,9 @@ msgstr ""
msgid "SAML SSO for %{group_name}"
msgstr ""
+msgid "SAML discovery tokens"
+msgstr ""
+
msgid "SAML for %{group_name}"
msgstr ""
@@ -17529,12 +17895,18 @@ msgstr "新建æµæ°´ç·šè¨ˆåŠƒ"
msgid "Scheduled"
msgstr ""
+msgid "Scheduled to merge this merge request (%{strategy})."
+msgstr ""
+
msgid "Scheduled to merge this merge request when the pipeline succeeds."
msgstr ""
msgid "Schedules"
msgstr ""
+msgid "Schedules to merge this merge request (%{strategy})."
+msgstr ""
+
msgid "Scheduling"
msgstr ""
@@ -17592,6 +17964,9 @@ msgstr ""
msgid "Search branches and tags"
msgstr "æœç´¢åˆ†æ”¯å’Œæ¨™ç±¤"
+msgid "Search by author"
+msgstr ""
+
msgid "Search files"
msgstr ""
@@ -17729,10 +18104,6 @@ msgid "SearchResults|snippet"
msgid_plural "SearchResults|snippets"
msgstr[0] ""
-msgid "SearchResults|snippet result"
-msgid_plural "SearchResults|snippet results"
-msgstr[0] ""
-
msgid "SearchResults|user"
msgid_plural "SearchResults|users"
msgstr[0] ""
@@ -17741,6 +18112,9 @@ msgid "SearchResults|wiki result"
msgid_plural "SearchResults|wiki results"
msgstr[0] ""
+msgid "Searching by both author and message is currently not supported."
+msgstr ""
+
msgid "Seat Link"
msgstr ""
@@ -17768,184 +18142,232 @@ msgstr ""
msgid "Security Dashboard"
msgstr ""
-msgid "Security Dashboard|Error fetching the vulnerability counts. Please check your network connection and try again."
+msgid "Security configuration help link"
+msgstr ""
+
+msgid "Security dashboard"
msgstr ""
-msgid "Security Dashboard|Error fetching the vulnerability list. Please check your network connection and try again."
+msgid "Security report is out of date. Please update your branch with the latest changes from the target branch (%{targetBranchName})"
msgstr ""
-msgid "Security Dashboard|Issue Created"
+msgid "Security report is out of date. Run %{newPipelineLinkStart}a new pipeline%{newPipelineLinkEnd} for the target branch (%{targetBranchName})"
msgstr ""
-msgid "Security Reports|Comment added to '%{vulnerabilityName}'"
+msgid "SecurityConfiguration|Configured"
msgstr ""
-msgid "Security Reports|Comment deleted on '%{vulnerabilityName}'"
+msgid "SecurityConfiguration|Feature"
msgstr ""
-msgid "Security Reports|Comment edited on '%{vulnerabilityName}'"
+msgid "SecurityConfiguration|Feature documentation for %{featureName}"
msgstr ""
-msgid "Security Reports|Create issue"
+msgid "SecurityConfiguration|Not yet configured"
msgstr ""
-msgid "Security Reports|Dismiss vulnerability"
+msgid "SecurityConfiguration|Secure features"
msgstr ""
-msgid "Security Reports|Dismissed '%{vulnerabilityName}'"
+msgid "SecurityConfiguration|Status"
msgstr ""
-msgid "Security Reports|Dismissed '%{vulnerabilityName}'. Turn off the hide dismissed toggle to view."
+msgid "SecurityReports|%{firstProject} and %{secondProject}"
msgstr ""
-msgid "Security Reports|Either you don't have permission to view this dashboard or the dashboard has not been setup. Please check your permission settings with your administrator or check your dashboard configurations to proceed."
+msgid "SecurityReports|%{firstProject}, %{secondProject}, and %{rest}"
msgstr ""
-msgid "Security Reports|Learn more about setting up your dashboard"
+msgid "SecurityReports|Add a project to your dashboard"
msgstr ""
-msgid "Security Reports|More info"
+msgid "SecurityReports|Add or remove projects from your dashboard"
msgstr ""
-msgid "Security Reports|Oops, something doesn't seem right."
+msgid "SecurityReports|Add projects"
msgstr ""
-msgid "Security Reports|Security reports can only be accessed by authorized users."
+msgid "SecurityReports|Comment added to '%{vulnerabilityName}'"
msgstr ""
-msgid "Security Reports|There was an error adding the comment."
+msgid "SecurityReports|Comment deleted on '%{vulnerabilityName}'"
msgstr ""
-msgid "Security Reports|There was an error creating the issue."
+msgid "SecurityReports|Comment edited on '%{vulnerabilityName}'"
msgstr ""
-msgid "Security Reports|There was an error creating the merge request."
+msgid "SecurityReports|Create issue"
msgstr ""
-msgid "Security Reports|There was an error deleting the comment."
+msgid "SecurityReports|Dismiss Selected"
msgstr ""
-msgid "Security Reports|There was an error dismissing the vulnerabilities."
+msgid "SecurityReports|Dismiss vulnerability"
msgstr ""
-msgid "Security Reports|There was an error dismissing the vulnerability."
+msgid "SecurityReports|Dismissed '%{vulnerabilityName}'"
msgstr ""
-msgid "Security Reports|There was an error reverting the dismissal."
+msgid "SecurityReports|Dismissed '%{vulnerabilityName}'. Turn off the hide dismissed toggle to view."
msgstr ""
-msgid "Security Reports|There was an error reverting this dismissal."
+msgid "SecurityReports|Each vulnerability now has a unique page that can be directly linked to, shared, referenced, and tracked as the single source of truth. Vulnerability occurrences also persist across scanner runs, which improves tracking and visibility and reduces duplicates between scans."
msgstr ""
-msgid "Security Reports|Undo dismiss"
+msgid "SecurityReports|Edit dashboard"
msgstr ""
-msgid "Security Reports|You do not have sufficient permissions to access this report"
+msgid "SecurityReports|Either you don't have permission to view this dashboard or the dashboard has not been setup. Please check your permission settings with your administrator or check your dashboard configurations to proceed."
msgstr ""
-msgid "Security Reports|You must sign in as an authorized user to see this report"
+msgid "SecurityReports|Error fetching the vulnerability counts. Please check your network connection and try again."
msgstr ""
-msgid "Security configuration help link"
+msgid "SecurityReports|Error fetching the vulnerability list. Please check your network connection and try again."
msgstr ""
-msgid "Security dashboard"
+msgid "SecurityReports|False positive"
msgstr ""
-msgid "Security report is out of date. Please update your branch with the latest changes from the target branch (%{targetBranchName})"
+msgid "SecurityReports|Hide dismissed"
msgstr ""
-msgid "Security report is out of date. Run %{newPipelineLinkStart}a new pipeline%{newPipelineLinkEnd} for the target branch (%{targetBranchName})"
+msgid "SecurityReports|Introducing standalone vulnerabilities"
msgstr ""
-msgid "SecurityConfiguration|Configured"
+msgid "SecurityReports|Issue Created"
msgstr ""
-msgid "SecurityConfiguration|Feature"
+msgid "SecurityReports|Learn More"
msgstr ""
-msgid "SecurityConfiguration|Feature documentation for %{featureName}"
+msgid "SecurityReports|Learn more about setting up your dashboard"
msgstr ""
-msgid "SecurityConfiguration|Not yet configured"
+msgid "SecurityReports|Load more vulnerabilities"
msgstr ""
-msgid "SecurityConfiguration|Secure features"
+msgid "SecurityReports|Monitor vulnerabilities in your code"
msgstr ""
-msgid "SecurityConfiguration|Status"
+msgid "SecurityReports|More info"
msgstr ""
-msgid "SecurityDashboard|%{firstProject} and %{secondProject}"
+msgid "SecurityReports|More information"
msgstr ""
-msgid "SecurityDashboard|%{firstProject}, %{secondProject}, and %{rest}"
+msgid "SecurityReports|No vulnerabilities found for dashboard"
msgstr ""
-msgid "SecurityDashboard|Add a project to your dashboard"
+msgid "SecurityReports|No vulnerabilities found for this group"
msgstr ""
-msgid "SecurityDashboard|Add or remove projects from your dashboard"
+msgid "SecurityReports|No vulnerabilities found for this pipeline"
msgstr ""
-msgid "SecurityDashboard|Add projects"
+msgid "SecurityReports|No vulnerabilities found for this project"
msgstr ""
-msgid "SecurityDashboard|Edit dashboard"
+msgid "SecurityReports|Oops, something doesn't seem right."
msgstr ""
-msgid "SecurityDashboard|Hide dismissed"
+msgid "SecurityReports|Pipeline %{pipelineLink} triggered %{timeago} by %{user}"
msgstr ""
-msgid "SecurityDashboard|Monitor vulnerabilities in your code"
+msgid "SecurityReports|Project"
msgstr ""
-msgid "SecurityDashboard|More information"
+msgid "SecurityReports|Projects added"
msgstr ""
-msgid "SecurityDashboard|No vulnerabilities found for dashboard"
+msgid "SecurityReports|Remove project from dashboard"
msgstr ""
-msgid "SecurityDashboard|Pipeline %{pipelineLink} triggered %{timeago} by %{user}"
+msgid "SecurityReports|Report type"
msgstr ""
-msgid "SecurityDashboard|Project"
+msgid "SecurityReports|Return to dashboard"
msgstr ""
-msgid "SecurityDashboard|Projects added"
+msgid "SecurityReports|Security Dashboard"
msgstr ""
-msgid "SecurityDashboard|Remove project from dashboard"
+msgid "SecurityReports|Security reports can only be accessed by authorized users."
msgstr ""
-msgid "SecurityDashboard|Report type"
+msgid "SecurityReports|Select a project to add by using the project search field above."
msgstr ""
-msgid "SecurityDashboard|Return to dashboard"
+msgid "SecurityReports|Select a reason"
msgstr ""
-msgid "SecurityDashboard|Security Dashboard"
+msgid "SecurityReports|Severity"
msgstr ""
-msgid "SecurityDashboard|Select a project to add by using the project search field above."
+msgid "SecurityReports|Status"
msgstr ""
-msgid "SecurityDashboard|Severity"
+msgid "SecurityReports|The security dashboard displays the latest security findings for projects you wish to monitor. Select \"Edit dashboard\" to add and remove projects."
msgstr ""
-msgid "SecurityDashboard|Status"
+msgid "SecurityReports|The security dashboard displays the latest security report. Use it to find and fix vulnerabilities."
msgstr ""
-msgid "SecurityDashboard|The security dashboard displays the latest security findings for projects you wish to monitor. Select \"Edit dashboard\" to add and remove projects."
+msgid "SecurityReports|There was an error adding the comment."
msgstr ""
-msgid "SecurityDashboard|The security dashboard displays the latest security report. Use it to find and fix vulnerabilities."
+msgid "SecurityReports|There was an error creating the issue."
msgstr ""
-msgid "SecurityDashboard|There was an error while generating the report."
+msgid "SecurityReports|There was an error creating the merge request."
msgstr ""
-msgid "SecurityDashboard|Unable to add %{invalidProjects}"
+msgid "SecurityReports|There was an error deleting the comment."
+msgstr ""
+
+msgid "SecurityReports|There was an error dismissing the vulnerabilities."
+msgstr ""
+
+msgid "SecurityReports|There was an error dismissing the vulnerability."
+msgstr ""
+
+msgid "SecurityReports|There was an error reverting the dismissal."
+msgstr ""
+
+msgid "SecurityReports|There was an error reverting this dismissal."
+msgstr ""
+
+msgid "SecurityReports|There was an error while generating the report."
+msgstr ""
+
+msgid "SecurityReports|Unable to add %{invalidProjects}"
+msgstr ""
+
+msgid "SecurityReports|Undo dismiss"
+msgstr ""
+
+msgid "SecurityReports|While it's rare to have no vulnerabilities for your group, it can happen. In any event, we ask that you double check your settings to make sure you've set up your dashboard correctly."
+msgstr ""
+
+msgid "SecurityReports|While it's rare to have no vulnerabilities for your pipeline, it can happen. In any event, we ask that you double check your settings to make sure all security scanning jobs have passed successfully."
+msgstr ""
+
+msgid "SecurityReports|While it's rare to have no vulnerabilities for your project, it can happen. In any event, we ask that you double check your settings to make sure you've set up your dashboard correctly."
+msgstr ""
+
+msgid "SecurityReports|While it's rare to have no vulnerabilities, it can happen. In any event, we ask that you please double check your settings to make sure you've set up your dashboard correctly."
+msgstr ""
+
+msgid "SecurityReports|Won't fix / Accept risk"
+msgstr ""
+
+msgid "SecurityReports|You do not have sufficient permissions to access this report"
+msgstr ""
+
+msgid "SecurityReports|You must sign in as an authorized user to see this report"
+msgstr ""
+
+msgid "SecurityReports|[No reason]"
msgstr ""
msgid "See GitLab's %{password_policy_guidelines}"
@@ -17957,6 +18379,9 @@ msgstr ""
msgid "See the affected projects in the GitLab admin panel"
msgstr ""
+msgid "See what's new at GitLab"
+msgstr ""
+
msgid "Select"
msgstr ""
@@ -18362,9 +18787,6 @@ msgstr ""
msgid "Set up Jira Integration"
msgstr ""
-msgid "Set up Jira Integration illustration"
-msgstr ""
-
msgid "Set up a %{type} Runner automatically"
msgstr ""
@@ -18449,6 +18871,9 @@ msgstr ""
msgid "Severity: %{severity}"
msgstr ""
+msgid "Shards (%{shards})"
+msgstr ""
+
msgid "Shards to synchronize"
msgstr ""
@@ -18762,6 +19187,9 @@ msgstr ""
msgid "Snowplow"
msgstr ""
+msgid "Some child epics may be hidden due to applied filters"
+msgstr ""
+
msgid "Some email servers do not support overriding the email sender name. Enable this option to include the name of the author of the issue, merge request or comment in the email body instead."
msgstr ""
@@ -18903,6 +19331,9 @@ msgstr ""
msgid "Something went wrong while updating your list settings"
msgstr ""
+msgid "Something went wrong with your automatic subscription renewal"
+msgstr ""
+
msgid "Something went wrong, unable to add %{project} to dashboard"
msgstr ""
@@ -19188,6 +19619,9 @@ msgstr ""
msgid "Stage removed"
msgstr ""
+msgid "Standard"
+msgstr ""
+
msgid "Star a label to make it a priority label. Order the prioritized labels to change their relative priority, by dragging."
msgstr ""
@@ -19311,6 +19745,9 @@ msgstr ""
msgid "Static Application Security Testing (SAST)"
msgstr ""
+msgid "StaticSiteEditor|An error occurred while submitting your changes."
+msgstr ""
+
msgid "StaticSiteEditor|Branch could not be created."
msgstr ""
@@ -19320,18 +19757,30 @@ msgstr ""
msgid "StaticSiteEditor|Could not create merge request."
msgstr ""
+msgid "StaticSiteEditor|Incompatible file content"
+msgstr ""
+
msgid "StaticSiteEditor|Return to site"
msgstr ""
+msgid "StaticSiteEditor|Static site editor"
+msgstr ""
+
msgid "StaticSiteEditor|Success!"
msgstr ""
msgid "StaticSiteEditor|Summary of changes"
msgstr ""
+msgid "StaticSiteEditor|The Static Site Editor is currently configured to only edit Markdown content on pages generated from Middleman. Visit the documentation to learn more about configuring your site to use the Static Site Editor."
+msgstr ""
+
msgid "StaticSiteEditor|Update %{sourcePath} file"
msgstr ""
+msgid "StaticSiteEditor|View documentation"
+msgstr ""
+
msgid "StaticSiteEditor|View merge request"
msgstr ""
@@ -19389,6 +19838,12 @@ msgstr ""
msgid "StatusPage|Status page"
msgstr ""
+msgid "StatusPage|Status page URL"
+msgstr ""
+
+msgid "StatusPage|Status page frontend documentation"
+msgstr ""
+
msgid "StatusPage|To publish incidents to an external status page, GitLab will store a JSON file in your Amazon S3 account in a location accessible to your external status page service. Make sure to also set up %{docsLink}"
msgstr ""
@@ -19737,6 +20192,12 @@ msgstr ""
msgid "Synced"
msgstr ""
+msgid "Synchronization disabled"
+msgstr ""
+
+msgid "Synchronization of container repositories is disabled."
+msgstr ""
+
msgid "System"
msgstr ""
@@ -19890,6 +20351,9 @@ msgstr ""
msgid "Telephone number"
msgstr ""
+msgid "Telephone number (Optional)"
+msgstr ""
+
msgid "Template"
msgstr ""
@@ -20193,6 +20657,9 @@ msgstr ""
msgid "The license was successfully uploaded and is now active. You can see the details below."
msgstr ""
+msgid "The license was successfully uploaded and will be active from %{starts_at}. You can see the details below."
+msgstr ""
+
msgid "The maximum file size allowed is %{size}."
msgstr ""
@@ -20514,6 +20981,9 @@ msgstr ""
msgid "There was an error getting the epic participants."
msgstr ""
+msgid "There was an error importing the Jira project."
+msgstr ""
+
msgid "There was an error loading users activity calendar."
msgstr ""
@@ -20529,7 +20999,7 @@ msgstr ""
msgid "There was an error resetting user pipeline minutes."
msgstr ""
-msgid "There was an error saving this Geo Node"
+msgid "There was an error saving this Geo Node."
msgstr ""
msgid "There was an error saving your changes."
@@ -20592,6 +21062,9 @@ msgstr ""
msgid "They can be managed using the %{link}."
msgstr ""
+msgid "Third Party Advisory Link"
+msgstr ""
+
msgid "Third party offers"
msgstr ""
@@ -20628,6 +21101,9 @@ msgstr ""
msgid "This application will be able to:"
msgstr ""
+msgid "This attachment has been truncated to avoid exceeding the maximum allowed attachment size of 15MB. %{written_count} of %{issues_count} issues have been included. Consider re-exporting with a narrower selection of issues."
+msgstr ""
+
msgid "This block is self-referential"
msgstr ""
@@ -20769,6 +21245,12 @@ msgstr ""
msgid "This issue is confidential"
msgstr "這個議題是隱密的"
+msgid "This issue is currently blocked by the following issues: %{issues}."
+msgstr ""
+
+msgid "This issue is in a child epic of the filtered epic"
+msgstr ""
+
msgid "This issue is locked."
msgstr "這個議題已被鎖定。"
@@ -20967,7 +21449,10 @@ msgstr ""
msgid "This user will be the author of all events in the activity feed that are the result of an update, like new branches being created or new commits being pushed to existing branches. Upon creation or when reassigning you can only assign yourself to be the mirror user."
msgstr ""
-msgid "This variable can not be masked"
+msgid "This variable can not be masked."
+msgstr ""
+
+msgid "This variable does not match the expected pattern."
msgstr ""
msgid "This will help us personalize your onboarding experience."
@@ -20997,10 +21482,10 @@ msgstr ""
msgid "ThreatMonitoring|Application firewall not detected"
msgstr ""
-msgid "ThreatMonitoring|Container Network Policy"
+msgid "ThreatMonitoring|Container Network Policies are not installed or have been disabled. To view this data, ensure your Network Policies are installed and enabled for your cluster."
msgstr ""
-msgid "ThreatMonitoring|Container NetworkPolicies are not installed or has been disabled. To view this data, ensure you NetworkPolicies are installed and enabled for your cluster."
+msgid "ThreatMonitoring|Container Network Policy"
msgstr ""
msgid "ThreatMonitoring|Container NetworkPolicies not detected"
@@ -21033,7 +21518,7 @@ msgstr ""
msgid "ThreatMonitoring|Something went wrong, unable to fetch statistics"
msgstr ""
-msgid "ThreatMonitoring|The firewall is not installed or has been disabled. To view this data, ensure you firewall is installed and enabled for your cluster."
+msgid "ThreatMonitoring|The firewall is not installed or has been disabled. To view this data, ensure the web application firewall is installed and enabled for your cluster."
msgstr ""
msgid "ThreatMonitoring|The graph below is an overview of traffic coming to your application as tracked by the Web Application Firewall (WAF). View the docs for instructions on how to access the WAF logs to see what type of malicious traffic is trying to access your app. The docs link is also accessible by clicking the \"?\" icon next to the title below."
@@ -21102,6 +21587,9 @@ msgstr ""
msgid "Time in seconds GitLab will wait for a response from the external service. When the service does not respond in time, access will be denied."
msgstr ""
+msgid "Time of import: %{importTime}"
+msgstr ""
+
msgid "Time remaining"
msgstr ""
@@ -21284,7 +21772,7 @@ msgstr ""
msgid "Title:"
msgstr ""
-msgid "Titles and Filenames"
+msgid "Titles and Descriptions"
msgstr ""
msgid "To"
@@ -21491,7 +21979,7 @@ msgstr ""
msgid "Too many projects enabled. You will need to manage them via the console or the API."
msgstr ""
-msgid "Topics"
+msgid "Topics (optional)"
msgstr ""
msgid "Total"
@@ -21653,6 +22141,9 @@ msgstr ""
msgid "Try all GitLab has to offer for 30 days."
msgstr ""
+msgid "Try changing or removing filters."
+msgstr ""
+
msgid "Try to fork again"
msgstr ""
@@ -21746,6 +22237,9 @@ msgstr ""
msgid "Unable to connect to server: %{error}"
msgstr ""
+msgid "Unable to connect to the Jira instance. Please check your Jira integration configuration."
+msgstr ""
+
msgid "Unable to convert Kubernetes logs encoding to UTF-8"
msgstr ""
@@ -22687,6 +23181,9 @@ msgstr ""
msgid "View issue"
msgstr ""
+msgid "View issues"
+msgstr ""
+
msgid "View it on GitLab"
msgstr ""
@@ -22864,6 +23361,15 @@ msgstr ""
msgid "VulnerabilityManagement|Resolved %{timeago} by %{user}"
msgstr ""
+msgid "VulnerabilityManagement|Something went wrong while trying to delete the comment. Please try again later."
+msgstr ""
+
+msgid "VulnerabilityManagement|Something went wrong while trying to retrieve the vulnerability history. Please try again later."
+msgstr ""
+
+msgid "VulnerabilityManagement|Something went wrong while trying to save the comment. Please try again later."
+msgstr ""
+
msgid "VulnerabilityManagement|Something went wrong, could not create an issue."
msgstr ""
@@ -22894,6 +23400,9 @@ msgstr ""
msgid "VulnerabilityStatusTypes|Resolved"
msgstr ""
+msgid "Vulnerability|%{scannerName} (version %{scannerVersion})"
+msgstr ""
+
msgid "Vulnerability|Class"
msgstr ""
@@ -22927,7 +23436,10 @@ msgstr ""
msgid "Vulnerability|Project"
msgstr ""
-msgid "Vulnerability|Report Type"
+msgid "Vulnerability|Scanner Provider"
+msgstr ""
+
+msgid "Vulnerability|Scanner Type"
msgstr ""
msgid "Vulnerability|Severity"
@@ -22981,9 +23493,15 @@ msgstr ""
msgid "We sent you an email with reset password instructions"
msgstr ""
+msgid "We tried to automatically renew your %{strong}%{plan_name}%{strong_close} subscription for %{strong}%{namespace_name}%{strong_close} on %{expires_on} but something went wrong so your subscription was downgraded to the free plan. Don't worry, your data is safe. We suggest you check your payment method and get in touch with our support team (%{support_link}). They'll gladly help with your subscription renewal."
+msgstr ""
+
msgid "We want to be sure it is you, please confirm you are not a robot."
msgstr ""
+msgid "We will automatically renew your %{strong}%{plan_name}%{strong_close} subscription for %{strong}%{namespace_name}%{strong_close} on %{strong}%{expires_on}%{strong_close}. There's nothing that you need to do, we'll let you know when the renewal is complete. Need more seats, a higher plan or just want to review your payment method?"
+msgstr ""
+
msgid "We've found no vulnerabilities"
msgstr ""
@@ -23087,15 +23605,6 @@ msgstr ""
msgid "When:"
msgstr ""
-msgid "While it's rare to have no vulnerabilities for your group, it can happen. In any event, we ask that you double check your settings to make sure you've set up your dashboard correctly."
-msgstr ""
-
-msgid "While it's rare to have no vulnerabilities for your pipeline, it can happen. In any event, we ask that you double check your settings to make sure all security scanning jobs have passed successfully."
-msgstr ""
-
-msgid "While it's rare to have no vulnerabilities for your project, it can happen. In any event, we ask that you double check your settings to make sure you've set up your dashboard correctly."
-msgstr ""
-
msgid "While it's rare to have no vulnerabilities, it can happen. In any event, we ask that you please double check your settings to make sure you've set up your dashboard correctly."
msgstr ""
@@ -23126,9 +23635,6 @@ msgstr ""
msgid "Wiki"
msgstr ""
-msgid "Wiki pages"
-msgstr ""
-
msgid "Wiki was successfully updated."
msgstr ""
@@ -23312,6 +23818,9 @@ msgstr ""
msgid "Yes, add it"
msgstr ""
+msgid "Yes, close issue"
+msgstr ""
+
msgid "Yes, let me map Google Code users to full names or GitLab users."
msgstr ""
@@ -23507,6 +24016,9 @@ msgstr ""
msgid "You could not create a new trigger."
msgstr ""
+msgid "You didn't renew your %{strong}%{plan_name}%{strong_close} subscription for %{strong}%{namespace_name}%{strong_close} so it was downgraded to the free plan."
+msgstr ""
+
msgid "You didn't renew your %{strong}%{plan_name}%{strong_close} subscription so it was downgraded to the GitLab Core Plan."
msgstr ""
@@ -23573,6 +24085,9 @@ msgstr ""
msgid "You have declined the invitation to join %{label}."
msgstr ""
+msgid "You have imported from this project %{numberOfPreviousImportsForProject} times before. Each new import will create duplicate issues."
+msgstr ""
+
msgid "You have no permissions"
msgstr ""
@@ -23654,6 +24169,9 @@ msgstr ""
msgid "You need to upload a Google Takeout archive."
msgstr ""
+msgid "You reached %{usage_in_percent} of %{namespace_name}'s capacity (%{used_storage} of %{storage_limit})"
+msgstr ""
+
msgid "You tried to fork %{link_to_the_project} but it failed for the following reason:"
msgstr ""
@@ -23738,9 +24256,21 @@ msgstr ""
msgid "YouTube"
msgstr ""
+msgid "Your %{strong}%{plan_name}%{strong_close} subscription for %{strong}%{namespace_name}%{strong_close} will expire on %{strong}%{expires_on}%{strong_close}. After that, you will not to be able to create issues or merge requests as well as many other features."
+msgstr ""
+
msgid "Your %{strong}%{plan_name}%{strong_close} subscription will expire on %{strong}%{expires_on}%{strong_close}. After that, you will not to be able to create issues or merge requests as well as many other features."
msgstr ""
+msgid "Your CSV export has started. It will be emailed to %{email} when complete."
+msgstr ""
+
+msgid "Your CSV export of %{issues_count} from project %{project_link} has been added to this email as an attachment."
+msgstr ""
+
+msgid "Your CSV export of %{written_count} from project %{project_name} (%{project_url}) has been added to this email as an attachment."
+msgstr ""
+
msgid "Your Commit Email will be used for web based operations, such as edits and merges."
msgstr ""
@@ -23837,6 +24367,9 @@ msgstr ""
msgid "Your comment could not be updated! Please check your network connection and try again."
msgstr ""
+msgid "Your comment will be discarded."
+msgstr ""
+
msgid "Your custom stage '%{title}' was created"
msgstr ""
@@ -23876,6 +24409,9 @@ msgstr ""
msgid "Your new personal access token has been created."
msgstr ""
+msgid "Your new project access token has been created."
+msgstr ""
+
msgid "Your password isn't required to view this page. If a password or any other personal details are requested, please contact your administrator to report abuse."
msgstr ""
@@ -23894,12 +24430,18 @@ msgstr ""
msgid "Your request for access has been queued for review."
msgstr ""
+msgid "Your search didn't match any commits."
+msgstr ""
+
msgid "Your subscription expired!"
msgstr ""
msgid "Your subscription has been downgraded"
msgstr ""
+msgid "Your subscription will automatically renew in %{remaining_days}"
+msgstr ""
+
msgid "Your subscription will expire in %{remaining_days}"
msgstr ""
@@ -23937,7 +24479,7 @@ msgstr ""
msgid "allowed to fail"
msgstr ""
-msgid "already being used for another group or project milestone."
+msgid "already being used for another group or project %{timebox_name}."
msgstr ""
msgid "already has a \"created\" issue link"
@@ -24015,6 +24557,9 @@ msgstr ""
msgid "ciReport|%{linkStartTag}Learn more about SAST %{linkEndTag}"
msgstr ""
+msgid "ciReport|%{linkStartTag}Learn more about Secret Scanning %{linkEndTag}"
+msgstr ""
+
msgid "ciReport|%{linkStartTag}Learn more about codequality reports %{linkEndTag}"
msgstr ""
@@ -24173,6 +24718,12 @@ msgstr ""
msgid "ciReport|SAST"
msgstr ""
+msgid "ciReport|Secret scanning"
+msgstr ""
+
+msgid "ciReport|Secret scanning detects secrets and credentials vulnerabilities in your source code."
+msgstr ""
+
msgid "ciReport|Security scanning"
msgstr ""
@@ -24366,6 +24917,9 @@ msgstr ""
msgid "group"
msgstr ""
+msgid "groups"
+msgstr ""
+
msgid "has already been linked to another vulnerability"
msgstr ""
@@ -24527,9 +25081,6 @@ msgstr[0] ""
msgid "merged %{time_ago}"
msgstr ""
-msgid "milestone should belong either to a project or a group."
-msgstr ""
-
msgid "missing"
msgstr ""
@@ -24866,6 +25417,9 @@ msgstr "æ–°åˆä½µè«‹æ±‚"
msgid "no contributions"
msgstr ""
+msgid "no expiration"
+msgstr ""
+
msgid "no one can merge"
msgstr ""
@@ -24916,6 +25470,9 @@ msgstr ""
msgid "pending removal"
msgstr ""
+msgid "per day"
+msgstr ""
+
msgid "pipeline"
msgstr ""
@@ -24942,6 +25499,12 @@ msgstr[0] ""
msgid "project avatar"
msgstr ""
+msgid "projects"
+msgstr ""
+
+msgid "push to your repository, create pipelines, create issues or add comments. To reduce storage capacity, delete unused repositories, artifacts, wikis, issues, and pipelines."
+msgstr ""
+
msgid "quick actions"
msgstr "快速æ“作"
@@ -25087,10 +25650,6 @@ msgstr ""
msgid "this document"
msgstr ""
-msgid "thread resolved"
-msgid_plural "threads resolved"
-msgstr[0] ""
-
msgid "to help your contributors communicate effectively!"
msgstr ""
@@ -25145,6 +25704,9 @@ msgstr ""
msgid "view the blob"
msgstr ""
+msgid "vulnerability|Add a comment"
+msgstr ""
+
msgid "vulnerability|Add a comment or reason for dismissal"
msgstr ""
diff --git a/locale/zh_TW/gitlab.po b/locale/zh_TW/gitlab.po
index 06e67940cc2..c5739f2f412 100644
--- a/locale/zh_TW/gitlab.po
+++ b/locale/zh_TW/gitlab.po
@@ -12,7 +12,7 @@ msgstr ""
"X-Crowdin-Project: gitlab-ee\n"
"X-Crowdin-Language: zh-TW\n"
"X-Crowdin-File: /master/locale/gitlab.pot\n"
-"PO-Revision-Date: 2020-04-15 00:30\n"
+"PO-Revision-Date: 2020-05-05 21:37\n"
msgid " %{start} to %{end}"
msgstr ""
@@ -182,6 +182,10 @@ msgid "%d tag"
msgid_plural "%d tags"
msgstr[0] "%d 個標籤"
+msgid "%d unresolved thread"
+msgid_plural "%d unresolved threads"
+msgstr[0] ""
+
msgid "%d vulnerability dismissed"
msgid_plural "%d vulnerabilities dismissed"
msgstr[0] ""
@@ -335,6 +339,9 @@ msgstr "%{mergeLength}/%{usersLength} å¯ä»¥åˆä½µ"
msgid "%{mrText}, this issue will be closed automatically."
msgstr "%{mrText},此議題將自動關閉。"
+msgid "%{namespace_name} is now read-only. You cannot: %{base_message}"
+msgstr ""
+
msgid "%{name} contained %{resultsString}"
msgstr "%{name} åŒ…å« %{resultsString}"
@@ -347,6 +354,10 @@ msgstr ""
msgid "%{name}'s avatar"
msgstr "%{name} 的大頭貼"
+msgid "%{no_of_days} day"
+msgid_plural "%{no_of_days} days"
+msgstr[0] ""
+
msgid "%{number_commits_behind} commits behind %{default_branch}, %{number_commits_ahead} commits ahead"
msgstr "%{number_commits_behind} 個è½å¾Œæ–¼ %{default_branch} 分支的æ交,%{number_commits_ahead} 個領先æ交"
@@ -464,6 +475,9 @@ msgstr[0] "%{text}%{files}"
msgid "%{text} is available"
msgstr "%{text} å¯ç”¨"
+msgid "%{timebox_name} should belong either to a project or a group."
+msgstr ""
+
msgid "%{title} %{operator} %{threshold}"
msgstr "%{title} %{operator} %{threshold}"
@@ -720,12 +734,18 @@ msgstr "<code>\"johnsmith@example.com\": \"johnsm...@example.com\"</code> 將會
msgid "<code>\"johnsmith@example.com\": \"johnsmith@example.com\"</code> will add \"By <a href=\"#\">johnsmith@example.com</a>\" to all issues and comments originally created by johnsmith@example.com. By default, the email address or username is masked to ensure the user's privacy. Use this option if you want to show the full email address."
msgstr "<code>\"johnsmith@example.com\": \"johnsmith@example.com\"</code> 將會把「由 <a href=\"#\">@johnsmith</a>ã€åŠ å…¥åˆ°åŽŸæœ¬ç”± johnsmith@example.com 建立的所有議題和留言中。為ä¿è­·ä½¿ç”¨è€…çš„éš±ç§ï¼Œé›»å­éƒµä»¶åœ°å€æˆ–使用者å稱é è¨­æœƒè¢«é®ä½ã€‚如需顯示完整郵件地å€ï¼Œå¯ä½¿ç”¨æ­¤é¸é …。"
+msgid "<namespace / project>"
+msgstr ""
+
msgid "<no name set>"
msgstr "<未設定å稱>"
msgid "<no scopes selected>"
msgstr "<未é¸æ“‡ç¯„åœ>"
+msgid "<project name>"
+msgstr ""
+
msgid "<strong>%{group_name}</strong> group members"
msgstr "<strong>%{group_name}</strong> 群組的æˆå“¡"
@@ -759,6 +779,9 @@ msgstr "將會使用您的電å­ä¿¡ç®±ä½å€è¨­å®šæ­¤ GitLab 實體的 Let's Enc
msgid "A basic page and serverless function that uses AWS Lambda, AWS API Gateway, and GitLab Pages"
msgstr "使用 AWS Lambdaã€AWS API é–˜é“åŠ GitLab Pages 的基本é é¢å’Œç„¡ä¼ºæœå™¨åŠŸèƒ½"
+msgid "A complete DevOps platform"
+msgstr ""
+
msgid "A default branch cannot be chosen for an empty project."
msgstr "無法設定空專案的é è¨­åˆ†æ”¯ã€‚"
@@ -975,6 +998,12 @@ msgstr "您的éœæ…‹ç‰©ä»¶æ¬Šæ–用來從外部儲存空間存å–éœæ…‹ç‰©ä»¶ï¼ˆ
msgid "AccessTokens|reset it"
msgstr "é‡è¨­æ¬Šæ–"
+msgid "AccessibilityReport|Accessibility report artifact not found"
+msgstr ""
+
+msgid "AccessibilityReport|Failed to retrieve accessibility report"
+msgstr ""
+
msgid "AccessibilityReport|Learn More"
msgstr ""
@@ -984,6 +1013,9 @@ msgstr ""
msgid "AccessibilityReport|New"
msgstr ""
+msgid "AccessibilityReport|The accessibility scanning found an error of the following type: %{code}"
+msgstr ""
+
msgid "Account"
msgstr "帳戶"
@@ -1225,6 +1257,9 @@ msgstr "å·²å‘å²è©©åŠ å…¥è­°é¡Œã€‚"
msgid "Added at"
msgstr "加入時間"
+msgid "Added for this merge request"
+msgstr ""
+
msgid "Added in this version"
msgstr "此版本新增"
@@ -1595,6 +1630,72 @@ msgid "Alert"
msgid_plural "Alerts"
msgstr[0] "警示"
+msgid "Alert Details"
+msgstr ""
+
+msgid "AlertManagement|Acknowledged"
+msgstr ""
+
+msgid "AlertManagement|Alert"
+msgstr ""
+
+msgid "AlertManagement|Authorize external service"
+msgstr ""
+
+msgid "AlertManagement|Display alerts from all your monitoring tools directly within GitLab. Streamline the investigation of your alerts and the escalation of alerts to incidents."
+msgstr ""
+
+msgid "AlertManagement|End time"
+msgstr ""
+
+msgid "AlertManagement|End time:"
+msgstr ""
+
+msgid "AlertManagement|Events"
+msgstr ""
+
+msgid "AlertManagement|Events:"
+msgstr ""
+
+msgid "AlertManagement|Full Alert Details"
+msgstr ""
+
+msgid "AlertManagement|More information"
+msgstr ""
+
+msgid "AlertManagement|No alerts available to display. If you think you're seeing this message in error, refresh the page."
+msgstr ""
+
+msgid "AlertManagement|No alerts to display."
+msgstr ""
+
+msgid "AlertManagement|Overview"
+msgstr ""
+
+msgid "AlertManagement|Resolved"
+msgstr ""
+
+msgid "AlertManagement|Severity"
+msgstr ""
+
+msgid "AlertManagement|Start time"
+msgstr ""
+
+msgid "AlertManagement|Start time:"
+msgstr ""
+
+msgid "AlertManagement|Status"
+msgstr ""
+
+msgid "AlertManagement|Surface alerts in GitLab"
+msgstr ""
+
+msgid "AlertManagement|There was an error displaying the alerts. Confirm your endpoint's configuration details to ensure alerts appear."
+msgstr ""
+
+msgid "AlertManagement|Triggered"
+msgstr ""
+
msgid "AlertService|%{linkStart}Learn more%{linkEnd} about configuring this endpoint to receive alerts."
msgstr "%{linkStart}了解更多%{linkEnd}有關接收警示端點設定"
@@ -1664,6 +1765,9 @@ msgstr "所有專案"
msgid "All security scans are enabled because %{linkStart}Auto DevOps%{linkEnd} is enabled on this project"
msgstr "因為這個專案已開啟 %{linkStart}Auto DevOps%{linkEnd},已啟用所有安全掃æ"
+msgid "All threads resolved"
+msgstr ""
+
msgid "All users"
msgstr "所有使用者"
@@ -1682,6 +1786,9 @@ msgstr "å…è¨±ç¾¤çµ„æ‰€æœ‰è€…ç®¡ç† LDAP 相關的設定"
msgid "Allow only the selected protocols to be used for Git access."
msgstr "僅å…許é¸å–之用於 Git å­˜å–的通訊å”定。"
+msgid "Allow owners to manage default branch protection per group"
+msgstr ""
+
msgid "Allow owners to manually add users outside of LDAP"
msgstr ""
@@ -1790,6 +1897,9 @@ msgstr "抓å–æ–°è¦å‰‡çš„核准者時發生錯誤。"
msgid "An error occurred fetching the dropdown data."
msgstr "抓å–下拉資料時發生錯誤。"
+msgid "An error occurred fetching the project authors."
+msgstr ""
+
msgid "An error occurred previewing the blob"
msgstr "é è¦½ blob 時發生錯誤"
@@ -1871,6 +1981,9 @@ msgstr "抓å–專案自動完æˆæ™‚發生錯誤。"
msgid "An error occurred while fetching sidebar data"
msgstr "抓å–å´é‚Šæ¬„資料時發生錯誤"
+msgid "An error occurred while fetching terraform reports."
+msgstr ""
+
msgid "An error occurred while fetching the Service Desk address."
msgstr "抓å–æœå‹™å°ä½å€æ™‚發生錯誤。"
@@ -2108,6 +2221,9 @@ msgstr ""
msgid "Any"
msgstr "任何"
+msgid "Any Author"
+msgstr ""
+
msgid "Any Label"
msgstr ""
@@ -2319,12 +2435,12 @@ msgstr "確定è¦æ­¸æª”此專案嗎?"
msgid "Are you sure that you want to unarchive this project?"
msgstr "確定è¦å–消歸檔此專案嗎?"
-msgid "Are you sure you want to cancel creating this comment?"
-msgstr "確定è¦å–消建立此留言嗎?"
-
msgid "Are you sure you want to cancel editing this comment?"
msgstr "確定è¦å–消編輯此留言嗎?"
+msgid "Are you sure you want to close this blocked issue?"
+msgstr ""
+
msgid "Are you sure you want to delete %{name}?"
msgstr ""
@@ -2352,6 +2468,9 @@ msgstr ""
msgid "Are you sure you want to deploy this environment?"
msgstr ""
+msgid "Are you sure you want to discard this comment?"
+msgstr ""
+
msgid "Are you sure you want to erase this build?"
msgstr "你確定è¦åˆªé™¤é€™å€‹çµ„建嗎?"
@@ -2527,6 +2646,9 @@ msgstr ""
msgid "At least one approval from a code owner is required to change files matching the respective CODEOWNER rules."
msgstr ""
+msgid "At least one logging option is required to be enabled"
+msgstr ""
+
msgid "At least one of group_id or project_id must be specified"
msgstr ""
@@ -2564,6 +2686,27 @@ msgstr ""
msgid "AuditEvents|Target"
msgstr ""
+msgid "AuditLogs|(removed)"
+msgstr ""
+
+msgid "AuditLogs|Action"
+msgstr ""
+
+msgid "AuditLogs|Author"
+msgstr ""
+
+msgid "AuditLogs|Date"
+msgstr ""
+
+msgid "AuditLogs|IP Address"
+msgstr ""
+
+msgid "AuditLogs|Object"
+msgstr ""
+
+msgid "AuditLogs|Target"
+msgstr ""
+
msgid "Aug"
msgstr "8月"
@@ -2846,9 +2989,6 @@ msgstr ""
msgid "BambooService|You must set up automatic revision labeling and a repository trigger in Bamboo."
msgstr ""
-msgid "Batch operations"
-msgstr ""
-
msgid "BatchComments|Delete all pending comments"
msgstr ""
@@ -2978,6 +3118,9 @@ msgstr ""
msgid "Boards"
msgstr ""
+msgid "Boards and Board Lists"
+msgstr ""
+
msgid "Boards|Collapse"
msgstr ""
@@ -3197,6 +3340,9 @@ msgstr ""
msgid "Buy GitLab Enterprise Edition"
msgstr ""
+msgid "Buy more Pipeline minutes"
+msgstr ""
+
msgid "By %{user_name}"
msgstr ""
@@ -3317,6 +3463,9 @@ msgstr ""
msgid "Can't scan the code?"
msgstr ""
+msgid "Can't update snippet: %{err}"
+msgstr ""
+
msgid "Canary"
msgstr ""
@@ -3485,6 +3634,9 @@ msgstr ""
msgid "Changing group path can have unintended side effects."
msgstr ""
+msgid "Charts"
+msgstr ""
+
msgid "Charts can't be displayed as the request for data has timed out. %{documentationLink}"
msgstr ""
@@ -3521,9 +3673,6 @@ msgstr ""
msgid "ChatMessage|and [%{count} more](%{pipeline_failed_jobs_url})"
msgstr ""
-msgid "ChatMessage|failed"
-msgstr ""
-
msgid "ChatMessage|has failed"
msgstr ""
@@ -3539,9 +3688,6 @@ msgstr ""
msgid "ChatMessage|in %{project_link}"
msgstr ""
-msgid "ChatMessage|passed"
-msgstr ""
-
msgid "Check again"
msgstr ""
@@ -3992,9 +4138,6 @@ msgstr ""
msgid "Click any <strong>project name</strong> in the project list below to navigate to the project milestone."
msgstr ""
-msgid "Click here"
-msgstr ""
-
msgid "Click the <strong>Download</strong> button and wait for downloading to complete."
msgstr ""
@@ -4382,6 +4525,12 @@ msgstr ""
msgid "ClusterIntegration|Fetching zones"
msgstr ""
+msgid "ClusterIntegration|Fluentd"
+msgstr ""
+
+msgid "ClusterIntegration|Fluentd is an open source data collector, which lets you unify the data collection and consumption for a better use and understanding of data. It requires at least one of the following logs to be successfully installed."
+msgstr ""
+
msgid "ClusterIntegration|GitLab Integration"
msgstr ""
@@ -4688,6 +4837,15 @@ msgstr ""
msgid "ClusterIntegration|Request to begin uninstalling failed"
msgstr ""
+msgid "ClusterIntegration|SIEM Hostname"
+msgstr ""
+
+msgid "ClusterIntegration|SIEM Port"
+msgstr ""
+
+msgid "ClusterIntegration|SIEM Protocol"
+msgstr ""
+
msgid "ClusterIntegration|Save changes"
msgstr ""
@@ -4784,6 +4942,12 @@ msgstr ""
msgid "ClusterIntegration|Select zone to choose machine type"
msgstr ""
+msgid "ClusterIntegration|Send Cilium Logs"
+msgstr ""
+
+msgid "ClusterIntegration|Send ModSecurity Logs"
+msgstr ""
+
msgid "ClusterIntegration|Service Token"
msgstr ""
@@ -5211,18 +5375,33 @@ msgstr ""
msgid "Compliance framework (optional)"
msgstr ""
+msgid "ComplianceFramework|GDPR"
+msgstr ""
+
msgid "ComplianceFramework|GDPR - General Data Protection Regulation"
msgstr ""
+msgid "ComplianceFramework|HIPAA"
+msgstr ""
+
msgid "ComplianceFramework|HIPAA - Health Insurance Portability and Accountability Act"
msgstr ""
+msgid "ComplianceFramework|PCI-DSS"
+msgstr ""
+
msgid "ComplianceFramework|PCI-DSS - Payment Card Industry-Data Security Standard"
msgstr ""
+msgid "ComplianceFramework|SOC 2"
+msgstr ""
+
msgid "ComplianceFramework|SOC 2 - Service Organization Control 2"
msgstr ""
+msgid "ComplianceFramework|SOX"
+msgstr ""
+
msgid "ComplianceFramework|SOX - Sarbanes-Oxley"
msgstr ""
@@ -5370,9 +5549,15 @@ msgstr ""
msgid "Container repositories sync capacity"
msgstr ""
+msgid "ContainerRegistry| Please visit the %{linkStart}administration settings%{linkEnd} to enable this feature."
+msgstr ""
+
msgid "ContainerRegistry|%{imageName} tags"
msgstr ""
+msgid "ContainerRegistry|%{title} was successfully scheduled for deletion"
+msgstr ""
+
msgid "ContainerRegistry|Automatically remove extra images that aren't designed to be kept."
msgstr ""
@@ -5385,6 +5570,9 @@ msgstr ""
msgid "ContainerRegistry|Container Registry"
msgstr ""
+msgid "ContainerRegistry|Container Registry tag expiration and retention policy is disabled"
+msgstr ""
+
msgid "ContainerRegistry|Copy build command"
msgstr ""
@@ -5394,18 +5582,12 @@ msgstr ""
msgid "ContainerRegistry|Copy push command"
msgstr ""
-msgid "ContainerRegistry|Currently, the Container Registry tag expiration feature is not available for projects created before GitLab version 12.8. For updates and more information, visit Issue %{linkStart}#196124%{linkEnd}"
-msgstr ""
-
msgid "ContainerRegistry|Docker connection error"
msgstr ""
msgid "ContainerRegistry|Docker tag expiration policy is %{toggleStatus}"
msgstr ""
-msgid "ContainerRegistry|Docker tags with names matching this regex pattern will expire:"
-msgstr ""
-
msgid "ContainerRegistry|Edit Settings"
msgstr ""
@@ -5427,9 +5609,6 @@ msgstr ""
msgid "ContainerRegistry|Image ID"
msgstr ""
-msgid "ContainerRegistry|Image deleted successfully"
-msgstr ""
-
msgid "ContainerRegistry|Keep and protect the images that matter most."
msgstr ""
@@ -5445,12 +5624,24 @@ msgstr ""
msgid "ContainerRegistry|Number of tags to retain:"
msgstr ""
+msgid "ContainerRegistry|Please contact your administrator."
+msgstr ""
+
msgid "ContainerRegistry|Push an image"
msgstr ""
msgid "ContainerRegistry|Quick Start"
msgstr ""
+msgid "ContainerRegistry|Regular expressions such as %{codeStart}.*-test%{codeEnd} or %{codeStart}dev-.*%{codeEnd} are supported"
+msgstr ""
+
+msgid "ContainerRegistry|Regular expressions such as %{codeStart}.*-test%{codeEnd} or %{codeStart}dev-.*%{codeEnd} are supported. To select all tags, use %{codeStart}.*%{codeEnd}"
+msgstr ""
+
+msgid "ContainerRegistry|Remember to run %{docLinkStart}garbage collection%{docLinkEnd} to remove the stale data from storage."
+msgstr ""
+
msgid "ContainerRegistry|Remove repository"
msgstr ""
@@ -5464,22 +5655,22 @@ msgstr[0] ""
msgid "ContainerRegistry|Retention policy has been Enabled"
msgstr ""
-msgid "ContainerRegistry|Something went wrong while deleting the image."
+msgid "ContainerRegistry|Something went wrong while fetching the expiration policy."
msgstr ""
-msgid "ContainerRegistry|Something went wrong while deleting the tag."
+msgid "ContainerRegistry|Something went wrong while fetching the repository list."
msgstr ""
-msgid "ContainerRegistry|Something went wrong while deleting the tags."
+msgid "ContainerRegistry|Something went wrong while fetching the tags list."
msgstr ""
-msgid "ContainerRegistry|Something went wrong while fetching the expiration policy."
+msgid "ContainerRegistry|Something went wrong while marking the tag for deletion."
msgstr ""
-msgid "ContainerRegistry|Something went wrong while fetching the packages list."
+msgid "ContainerRegistry|Something went wrong while marking the tags for deletion."
msgstr ""
-msgid "ContainerRegistry|Something went wrong while fetching the tags list."
+msgid "ContainerRegistry|Something went wrong while scheduling %{title} for deletion. Please try again."
msgstr ""
msgid "ContainerRegistry|Something went wrong while updating the expiration policy."
@@ -5488,16 +5679,25 @@ msgstr ""
msgid "ContainerRegistry|Tag"
msgstr ""
-msgid "ContainerRegistry|Tag deleted successfully"
-msgstr ""
-
msgid "ContainerRegistry|Tag expiration policy"
msgstr ""
msgid "ContainerRegistry|Tag expiration policy is designed to:"
msgstr ""
-msgid "ContainerRegistry|Tags deleted successfully"
+msgid "ContainerRegistry|Tag successfully marked for deletion."
+msgstr ""
+
+msgid "ContainerRegistry|Tags successfully marked for deletion."
+msgstr ""
+
+msgid "ContainerRegistry|Tags with names matching this regex pattern will %{italicStart}be preserved:%{italicEnd}"
+msgstr ""
+
+msgid "ContainerRegistry|Tags with names matching this regex pattern will %{italicStart}expire:%{italicEnd}"
+msgstr ""
+
+msgid "ContainerRegistry|The Container Registry tag expiration and retention policies for this project have not been enabled."
msgstr ""
msgid "ContainerRegistry|The last tag related to this image was recently removed. This empty image and any associated data will be automatically removed as part of the regular Garbage Collection process. If you have any questions, contact your administrator."
@@ -5518,16 +5718,16 @@ msgstr ""
msgid "ContainerRegistry|There are no container images stored for this project"
msgstr ""
-msgid "ContainerRegistry|This Registry contains deleted image tag data. Remember to run %{docLinkStart}garbage collection%{docLinkEnd} to remove the stale data from storage."
+msgid "ContainerRegistry|There was an error during the deletion of this image repository, please try again."
msgstr ""
msgid "ContainerRegistry|This image has no active tags"
msgstr ""
-msgid "ContainerRegistry|We are having trouble connecting to Docker, which could be due to an issue with your project name or path. %{docLinkStart}More Information%{docLinkEnd}"
+msgid "ContainerRegistry|This image repository is scheduled for deletion"
msgstr ""
-msgid "ContainerRegistry|Wildcards such as %{codeStart}.*-stable%{codeEnd} or %{codeStart}production/.*%{codeEnd} are supported. To select all tags, use %{codeStart}.*%{codeEnd}"
+msgid "ContainerRegistry|We are having trouble connecting to Docker, which could be due to an issue with your project name or path. %{docLinkStart}More Information%{docLinkEnd}"
msgstr ""
msgid "ContainerRegistry|With the Container Registry, every project can have its own space to store its Docker images. %{docLinkStart}More Information%{docLinkEnd}"
@@ -5782,7 +5982,7 @@ msgstr ""
msgid "Could not delete chat nickname %{chat_name}."
msgstr ""
-msgid "Could not find design"
+msgid "Could not find design."
msgstr ""
msgid "Could not remove the trigger."
@@ -5797,6 +5997,9 @@ msgstr ""
msgid "Could not revoke personal access token %{personal_access_token_name}."
msgstr ""
+msgid "Could not revoke project access token %{project_access_token_name}."
+msgstr ""
+
msgid "Could not save group ID"
msgstr ""
@@ -5854,6 +6057,9 @@ msgstr ""
msgid "Create a new branch"
msgstr ""
+msgid "Create a new deploy key for this project"
+msgstr ""
+
msgid "Create a new file as there are no files yet. Afterwards, you'll be able to commit your changes."
msgstr ""
@@ -5959,6 +6165,9 @@ msgstr ""
msgid "Create requirement"
msgstr ""
+msgid "Create snippet"
+msgstr ""
+
msgid "Create wildcard: %{searchTerm}"
msgstr ""
@@ -5980,6 +6189,9 @@ msgstr ""
msgid "Created"
msgstr ""
+msgid "Created %{timestamp}"
+msgstr ""
+
msgid "Created At"
msgstr ""
@@ -6335,6 +6547,9 @@ msgstr ""
msgid "CycleAnalytics|stage dropdown"
msgstr ""
+msgid "DAG"
+msgstr ""
+
msgid "DNS"
msgstr "DNS"
@@ -6695,6 +6910,9 @@ msgstr ""
msgid "Deploy key was successfully updated."
msgstr ""
+msgid "Deploy keys allow read-only or read-write (if enabled) access to your repository. Deploy keys can be used for CI, staging or production servers. You can create a deploy key or add an existing one."
+msgstr ""
+
msgid "Deploy progress not found. To see pods, ensure your environment matches %{linkStart}deploy board criteria%{linkEnd}."
msgstr ""
@@ -6848,6 +7066,9 @@ msgstr ""
msgid "Deploying to"
msgstr ""
+msgid "Deployment Frequency"
+msgstr ""
+
msgid "Deployment|API"
msgstr ""
@@ -7365,9 +7586,6 @@ msgstr ""
msgid "Edit environment"
msgstr ""
-msgid "Edit epic description"
-msgstr ""
-
msgid "Edit file"
msgstr ""
@@ -7989,9 +8207,6 @@ msgstr ""
msgid "Epics"
msgstr ""
-msgid "Epics (Ultimate / Gold license only)"
-msgstr ""
-
msgid "Epics Roadmap"
msgstr ""
@@ -8001,6 +8216,9 @@ msgstr ""
msgid "Epics let you manage your portfolio of projects more efficiently and with less effort"
msgstr ""
+msgid "Epics, Issues, and Merge Requests"
+msgstr ""
+
msgid "Epics|Add an epic"
msgstr ""
@@ -8052,6 +8270,9 @@ msgstr ""
msgid "Epics|Something went wrong while fetching group epics."
msgstr ""
+msgid "Epics|Something went wrong while moving item."
+msgstr ""
+
msgid "Epics|Something went wrong while ordering item."
msgstr ""
@@ -8106,6 +8327,9 @@ msgstr ""
msgid "Error fetching network graph."
msgstr ""
+msgid "Error fetching payload data."
+msgstr ""
+
msgid "Error fetching projects"
msgstr ""
@@ -8115,9 +8339,6 @@ msgstr ""
msgid "Error fetching the dependency list. Please check your network connection and try again."
msgstr ""
-msgid "Error fetching usage ping data."
-msgstr ""
-
msgid "Error loading branch data. Please try again."
msgstr ""
@@ -8259,6 +8480,9 @@ msgstr ""
msgid "Errors"
msgstr ""
+msgid "Errors:"
+msgstr ""
+
msgid "Estimated"
msgstr ""
@@ -8463,12 +8687,18 @@ msgstr ""
msgid "Export as CSV"
msgstr ""
+msgid "Export group"
+msgstr ""
+
msgid "Export issues"
msgstr ""
msgid "Export project"
msgstr ""
+msgid "Export this group with all related data to a new GitLab instance. Once complete, you can import the data file from the \"New Group\" page."
+msgstr ""
+
msgid "Export this project with all its related data in order to move your project to a new GitLab instance. Once the export is finished, you can import the file from the \"New Project\" page."
msgstr ""
@@ -8538,9 +8768,6 @@ msgstr ""
msgid "Failed Jobs"
msgstr ""
-msgid "Failed create wiki"
-msgstr ""
-
msgid "Failed to add a Zoom meeting"
msgstr ""
@@ -8580,6 +8807,9 @@ msgstr ""
msgid "Failed to create resources"
msgstr ""
+msgid "Failed to create wiki"
+msgstr ""
+
msgid "Failed to delete board. Please try again."
msgstr ""
@@ -8589,7 +8819,7 @@ msgstr ""
msgid "Failed to enqueue the rebase operation, possibly due to a long-lived transaction. Try again later."
msgstr ""
-msgid "Failed to find import label for jira import."
+msgid "Failed to find import label for Jira import."
msgstr ""
msgid "Failed to get ref."
@@ -8613,6 +8843,12 @@ msgstr ""
msgid "Failed to load groups & users."
msgstr ""
+msgid "Failed to load labels. Please try again."
+msgstr ""
+
+msgid "Failed to load milestones. Please try again."
+msgstr ""
+
msgid "Failed to load related branches"
msgstr ""
@@ -8988,7 +9224,10 @@ msgstr ""
msgid "Filter by milestone name"
msgstr ""
-msgid "Filter by name..."
+msgid "Filter by name"
+msgstr ""
+
+msgid "Filter by status"
msgstr ""
msgid "Filter by two-factor authentication"
@@ -9000,12 +9239,18 @@ msgstr ""
msgid "Filter projects"
msgstr ""
+msgid "Filter results"
+msgstr ""
+
msgid "Filter results by group"
msgstr ""
msgid "Filter results by project"
msgstr ""
+msgid "Filter results..."
+msgstr ""
+
msgid "Filter your projects by name"
msgstr ""
@@ -9294,10 +9539,10 @@ msgstr ""
msgid "GeoNodes|Checksummed"
msgstr ""
-msgid "GeoNodes|Container repositories"
+msgid "GeoNodes|Consult Geo troubleshooting information"
msgstr ""
-msgid "GeoNodes|Data is out of date from %{timeago}"
+msgid "GeoNodes|Container repositories"
msgstr ""
msgid "GeoNodes|Data replication lag"
@@ -9339,6 +9584,9 @@ msgstr ""
msgid "GeoNodes|Last event ID seen from primary"
msgstr ""
+msgid "GeoNodes|Learn more about Geo node statuses"
+msgstr ""
+
msgid "GeoNodes|Loading nodes"
msgstr ""
@@ -9354,6 +9602,9 @@ msgstr ""
msgid "GeoNodes|Node was successfully removed."
msgstr ""
+msgid "GeoNodes|Node's status was updated %{timeAgo}."
+msgstr ""
+
msgid "GeoNodes|Not checksummed"
msgstr ""
@@ -9384,7 +9635,10 @@ msgstr ""
msgid "GeoNodes|Repository verification progress"
msgstr ""
-msgid "GeoNodes|Selective"
+msgid "GeoNodes|Selective (%{syncLabel})"
+msgstr ""
+
+msgid "GeoNodes|Selective synchronization"
msgstr ""
msgid "GeoNodes|Something went wrong while changing node status"
@@ -9411,6 +9665,9 @@ msgstr ""
msgid "GeoNodes|Unverified"
msgstr ""
+msgid "GeoNodes|Updated %{timeAgo}"
+msgstr ""
+
msgid "GeoNodes|Used slots"
msgstr ""
@@ -9450,16 +9707,16 @@ msgstr ""
msgid "Geo|All"
msgstr ""
-msgid "Geo|All projects"
+msgid "Geo|All %{replicable_type}"
msgstr ""
-msgid "Geo|All projects are being scheduled for re-sync"
+msgid "Geo|All projects"
msgstr ""
-msgid "Geo|All projects are being scheduled for re-verify"
+msgid "Geo|All projects are being scheduled for resync"
msgstr ""
-msgid "Geo|Batch operations"
+msgid "Geo|All projects are being scheduled for reverify"
msgstr ""
msgid "Geo|Could not remove tracking entry for an existing project."
@@ -9471,9 +9728,15 @@ msgstr ""
msgid "Geo|Failed"
msgstr ""
+msgid "Geo|Filter by status"
+msgstr ""
+
msgid "Geo|Geo Status"
msgstr ""
+msgid "Geo|In progress"
+msgstr ""
+
msgid "Geo|In sync"
msgstr ""
@@ -9498,9 +9761,6 @@ msgstr ""
msgid "Geo|Not synced yet"
msgstr ""
-msgid "Geo|Pending"
-msgstr ""
-
msgid "Geo|Pending synchronization"
msgstr ""
@@ -9537,7 +9797,7 @@ msgstr ""
msgid "Geo|Resync"
msgstr ""
-msgid "Geo|Resync all projects"
+msgid "Geo|Resync all"
msgstr ""
msgid "Geo|Retry count"
@@ -9546,7 +9806,7 @@ msgstr ""
msgid "Geo|Reverify"
msgstr ""
-msgid "Geo|Reverify all projects"
+msgid "Geo|Reverify all"
msgstr ""
msgid "Geo|Status"
@@ -9888,9 +10148,6 @@ msgstr ""
msgid "Go to file"
msgstr ""
-msgid "Go to file (MRs only)"
-msgstr ""
-
msgid "Go to file permalink (while viewing a file)"
msgstr ""
@@ -10335,6 +10592,9 @@ msgstr ""
msgid "GroupSettings|Disable group mentions"
msgstr ""
+msgid "GroupSettings|Export group"
+msgstr ""
+
msgid "GroupSettings|If the parent group's visibility is lower than the group current visibility, visibility levels for subgroups and projects will be changed to match the new parent group's visibility."
msgstr ""
@@ -10413,6 +10673,9 @@ msgstr ""
msgid "Groups (%{count})"
msgstr ""
+msgid "Groups (%{groups})"
+msgstr ""
+
msgid "Groups can also be nested by creating %{subgroup_docs_link_start}subgroups%{subgroup_docs_link_end}."
msgstr ""
@@ -10676,15 +10939,12 @@ msgstr "ID"
msgid "ID:"
msgstr "ID:"
-msgid "IDE|Allow live previews of JavaScript projects in the Web IDE using CodeSandbox client side evaluation."
+msgid "IDE|Allow live previews of JavaScript projects in the Web IDE using CodeSandbox Live Preview."
msgstr ""
msgid "IDE|Back"
msgstr ""
-msgid "IDE|Client side evaluation"
-msgstr ""
-
msgid "IDE|Commit"
msgstr ""
@@ -10748,6 +11008,9 @@ msgstr ""
msgid "If any job surpasses this timeout threshold, it will be marked as failed. Human readable time input language is accepted like \"1 hour\". Values without specification represent seconds."
msgstr ""
+msgid "If blank, set allowable lifetime to %{instance_level_policy_in_words}, as defined by the instance admin. Once set, existing tokens for users in this group may be revoked."
+msgstr ""
+
msgid "If checked, group owners can manage LDAP group links and LDAP member overrides"
msgstr ""
@@ -10787,6 +11050,9 @@ msgstr ""
msgid "If you lose your recovery codes you can generate new ones, invalidating all previous codes."
msgstr ""
+msgid "If you reach 100%% storage capacity, you will not be able to: %{base_message}"
+msgstr ""
+
msgid "If your HTTP repository is not publicly accessible, add your credentials."
msgstr ""
@@ -10898,6 +11164,9 @@ msgstr ""
msgid "Import repository"
msgstr ""
+msgid "Import started by: %{importInitiator}"
+msgstr ""
+
msgid "Import tasks"
msgstr ""
@@ -10964,6 +11233,9 @@ msgstr ""
msgid "In order to tailor your experience with GitLab we<br>would like to know a bit more about you."
msgstr ""
+msgid "In progress"
+msgstr ""
+
msgid "In the next step, you'll be able to select the projects you want to import."
msgstr ""
@@ -11310,15 +11582,15 @@ msgstr ""
msgid "Issues"
msgstr ""
-msgid "Issues / Merge Requests"
-msgstr ""
-
msgid "Issues Analytics"
msgstr ""
msgid "Issues Rate Limits"
msgstr ""
+msgid "Issues and Merge Requests"
+msgstr ""
+
msgid "Issues can be bugs, tasks or ideas to be discussed. Also, issues are searchable and filterable."
msgstr ""
@@ -11340,10 +11612,10 @@ msgstr ""
msgid "IssuesAnalytics|Avg/Month:"
msgstr ""
-msgid "IssuesAnalytics|Issues created"
+msgid "IssuesAnalytics|Issues opened"
msgstr ""
-msgid "IssuesAnalytics|Issues created per month"
+msgid "IssuesAnalytics|Issues opened per month"
msgstr ""
msgid "IssuesAnalytics|Last 12 months"
@@ -11397,6 +11669,9 @@ msgstr ""
msgid "Jira integration not configured."
msgstr ""
+msgid "Jira project: %{importProject}"
+msgstr ""
+
msgid "JiraService|Events for %{noteable_model_name} are disabled."
msgstr ""
@@ -11785,6 +12060,9 @@ msgstr ""
msgid "Lead"
msgstr ""
+msgid "Lead Time"
+msgstr ""
+
msgid "Learn GitLab"
msgstr ""
@@ -11797,6 +12075,9 @@ msgstr ""
msgid "Learn how to %{no_packages_link_start}publish and share your packages%{no_packages_link_end} with GitLab."
msgstr ""
+msgid "Learn how to enable synchronization"
+msgstr ""
+
msgid "Learn more"
msgstr ""
@@ -12123,9 +12404,6 @@ msgstr ""
msgid "Live preview"
msgstr ""
-msgid "Load more vulnerabilities"
-msgstr ""
-
msgid "Loading"
msgstr ""
@@ -12276,9 +12554,6 @@ msgstr ""
msgid "Manage"
msgstr ""
-msgid "Manage Git repositories with fine-grained access controls that keep your code secure. Perform code reviews and enhance collaboration with merge requests. Each project can also have an issue tracker and a wiki."
-msgstr ""
-
msgid "Manage Web IDE features"
msgstr ""
@@ -12309,6 +12584,9 @@ msgstr ""
msgid "Manage your license"
msgstr ""
+msgid "Managed Account"
+msgstr ""
+
msgid "Manifest"
msgstr ""
@@ -12573,6 +12851,9 @@ msgstr ""
msgid "Merge Requests in Review"
msgstr ""
+msgid "Merge automatically (%{strategy})"
+msgstr ""
+
msgid "Merge commit message"
msgstr ""
@@ -12726,6 +13007,12 @@ msgstr ""
msgid "Merged branches are being deleted. This can take some time depending on the number of branches. Please refresh the page to see changes."
msgstr ""
+msgid "Merged this merge request."
+msgstr ""
+
+msgid "Merges this merge request immediately."
+msgstr ""
+
msgid "Merges this merge request when the pipeline succeeds."
msgstr ""
@@ -12832,12 +13119,15 @@ msgid "Metrics|Edit metric"
msgid_plural "Metrics|Edit metrics"
msgstr[0] ""
-msgid "Metrics|Environment"
+msgid "Metrics|Expand panel"
msgstr ""
msgid "Metrics|For grouping similar metrics"
msgstr ""
+msgid "Metrics|Go back (Esc)"
+msgstr ""
+
msgid "Metrics|Invalid time range, please verify."
msgstr ""
@@ -12868,9 +13158,6 @@ msgstr ""
msgid "Metrics|Refresh dashboard"
msgstr ""
-msgid "Metrics|Show last"
-msgstr ""
-
msgid "Metrics|There was an error creating the dashboard."
msgstr ""
@@ -12949,6 +13236,9 @@ msgstr ""
msgid "Microsoft Azure"
msgstr ""
+msgid "Middleman project with Static Site Editor support"
+msgstr ""
+
msgid "Migrated %{success_count}/%{total_count} files."
msgstr ""
@@ -13392,10 +13682,10 @@ msgstr ""
msgid "Next"
msgstr ""
-msgid "Next file in diff (MRs only)"
+msgid "Next file in diff"
msgstr ""
-msgid "Next unresolved discussion (MRs only)"
+msgid "Next unresolved discussion"
msgstr ""
msgid "Nickname"
@@ -13455,6 +13745,9 @@ msgstr ""
msgid "No changes between %{ref_start}%{source_branch}%{ref_end} and %{ref_start}%{target_branch}%{ref_end}"
msgstr ""
+msgid "No child epics match applied filters"
+msgstr ""
+
msgid "No connection could be made to a Gitaly Server, please check your logs!"
msgstr ""
@@ -13593,15 +13886,6 @@ msgstr ""
msgid "No thanks, don't show this again"
msgstr ""
-msgid "No vulnerabilities found for this group"
-msgstr ""
-
-msgid "No vulnerabilities found for this pipeline"
-msgstr ""
-
-msgid "No vulnerabilities found for this project"
-msgstr ""
-
msgid "No vulnerabilities present"
msgstr ""
@@ -13725,6 +14009,9 @@ msgstr ""
msgid "Nothing to preview."
msgstr ""
+msgid "Nothing to synchronize"
+msgstr ""
+
msgid "Notification events"
msgstr ""
@@ -13896,6 +14183,9 @@ msgstr ""
msgid "Once removed, the fork relationship cannot be restored and you will no longer be able to send merge requests to the source."
msgstr ""
+msgid "Once the exported file is ready you can download it from this page."
+msgstr ""
+
msgid "Once the exported file is ready, you will receive a notification email with a download link, or you can download it from this page."
msgstr ""
@@ -13996,9 +14286,6 @@ msgstr ""
msgid "Open sidebar"
msgstr ""
-msgid "Open source software to collaborate on code"
-msgstr ""
-
msgid "Open: %{openIssuesCount}"
msgstr ""
@@ -14101,6 +14388,9 @@ msgstr ""
msgid "OutdatedBrowser|You can provide feedback %{feedback_link_start}on this issue%{feedback_link_end} or via your usual support channels."
msgstr ""
+msgid "Overridden"
+msgstr ""
+
msgid "Overview"
msgstr ""
@@ -14152,6 +14442,9 @@ msgstr ""
msgid "PackageRegistry|Conan Command"
msgstr ""
+msgid "PackageRegistry|Copy .pypirc content"
+msgstr ""
+
msgid "PackageRegistry|Copy Conan Command"
msgstr ""
@@ -14173,6 +14466,9 @@ msgstr ""
msgid "PackageRegistry|Copy NuGet Setup Command"
msgstr ""
+msgid "PackageRegistry|Copy Pip command"
+msgstr ""
+
msgid "PackageRegistry|Copy and paste this inside your %{codeStart}pom.xml%{codeEnd} %{codeStart}dependencies%{codeEnd} block."
msgstr ""
@@ -14206,6 +14502,12 @@ msgstr ""
msgid "PackageRegistry|For more information on the NuGet registry, %{linkStart}see the documentation%{linkEnd}."
msgstr ""
+msgid "PackageRegistry|For more information on the PyPi registry, %{linkStart}see the documentation%{linkEnd}."
+msgstr ""
+
+msgid "PackageRegistry|If you haven't already done so, you will need to add the below to your %{codeStart}.pypirc%{codeEnd} file."
+msgstr ""
+
msgid "PackageRegistry|If you haven't already done so, you will need to add the below to your %{codeStart}pom.xml%{codeEnd} file."
msgstr ""
@@ -14215,6 +14517,9 @@ msgstr ""
msgid "PackageRegistry|Learn how to %{noPackagesLinkStart}publish and share your packages%{noPackagesLinkEnd} with GitLab."
msgstr ""
+msgid "PackageRegistry|Manually Published"
+msgstr ""
+
msgid "PackageRegistry|Maven"
msgstr ""
@@ -14233,12 +14538,18 @@ msgstr ""
msgid "PackageRegistry|NuGet Command"
msgstr ""
+msgid "PackageRegistry|Pip Command"
+msgstr ""
+
msgid "PackageRegistry|Pipeline %{linkStart}%{linkEnd} triggered %{timestamp} by %{author}"
msgstr ""
msgid "PackageRegistry|Published to the repository at %{timestamp}"
msgstr ""
+msgid "PackageRegistry|PyPi"
+msgstr ""
+
msgid "PackageRegistry|Registry Setup"
msgstr ""
@@ -14275,6 +14586,9 @@ msgstr ""
msgid "PackageRegistry|npm"
msgstr ""
+msgid "PackageRegistry|published by %{author}"
+msgstr ""
+
msgid "PackageRegistry|yarn"
msgstr ""
@@ -14290,6 +14604,9 @@ msgstr ""
msgid "PackageType|NuGet"
msgstr ""
+msgid "PackageType|PyPi"
+msgstr ""
+
msgid "Packages"
msgstr ""
@@ -14383,6 +14700,9 @@ msgstr ""
msgid "Past due"
msgstr ""
+msgid "Paste a machine public key here. Read more about how to generate it"
+msgstr ""
+
msgid "Paste a machine public key here. Read more about how to generate it %{link_start}here%{link_end}"
msgstr ""
@@ -14398,9 +14718,6 @@ msgstr ""
msgid "Path"
msgstr ""
-msgid "Path, transfer, remove"
-msgstr ""
-
msgid "Path:"
msgstr ""
@@ -14431,7 +14748,7 @@ msgstr ""
msgid "Percentage"
msgstr ""
-msgid "Perform advanced options such as changing path, transferring, or removing the group."
+msgid "Perform advanced options such as changing path, transferring, exporting, or removing the group."
msgstr ""
msgid "Perform common operations on GitLab project"
@@ -14512,9 +14829,6 @@ msgstr ""
msgid "Pipeline minutes quota"
msgstr ""
-msgid "Pipeline quota"
-msgstr ""
-
msgid "Pipeline subscriptions"
msgstr ""
@@ -14524,6 +14838,9 @@ msgstr ""
msgid "Pipeline: %{status}"
msgstr ""
+msgid "PipelineCharts|CI / CD Analytics"
+msgstr ""
+
msgid "PipelineCharts|Failed:"
msgstr ""
@@ -14602,15 +14919,6 @@ msgstr ""
msgid "Pipelines settings for '%{project_name}' were successfully updated."
msgstr ""
-msgid "Pipelines| to purchase more minutes."
-msgstr ""
-
-msgid "Pipelines|%{namespace_name} has exceeded its pipeline minutes quota."
-msgstr ""
-
-msgid "Pipelines|%{namespace_name} has less than %{notification_level}%% of CI minutes available."
-msgstr ""
-
msgid "Pipelines|API"
msgstr ""
@@ -14632,10 +14940,13 @@ msgstr ""
msgid "Pipelines|Get started with Pipelines"
msgstr ""
-msgid "Pipelines|Loading Pipelines"
+msgid "Pipelines|Group %{namespace_name} has %{percentage}%% or less Shared Runner Pipeline minutes remaining. Once it runs out, no new jobs or pipelines in its projects will run."
+msgstr ""
+
+msgid "Pipelines|Group %{namespace_name} has exceeded its pipeline minutes quota. Unless you buy additional pipeline minutes, no new jobs or pipelines in its projects will run."
msgstr ""
-msgid "Pipelines|Pipelines will not run anymore on shared Runners."
+msgid "Pipelines|Loading Pipelines"
msgstr ""
msgid "Pipelines|Project cache successfully reset."
@@ -14866,6 +15177,9 @@ msgstr ""
msgid "Please select"
msgstr ""
+msgid "Please select a Jira project"
+msgstr ""
+
msgid "Please select a country"
msgstr ""
@@ -14911,6 +15225,9 @@ msgstr ""
msgid "Please wait while we import the repository for you. Refresh at will."
msgstr ""
+msgid "Plugins directory is deprecated and will be removed in 14.0. Please move this file into /file_hooks directory."
+msgstr ""
+
msgid "Pod does not exist"
msgstr ""
@@ -15028,7 +15345,7 @@ msgstr ""
msgid "Prevent users from changing their profile name"
msgstr ""
-msgid "Prevent users from modifing merge request approvers list"
+msgid "Prevent users from modifying merge request approvers list"
msgstr ""
msgid "Prevent users from performing write operations on GitLab while performing maintenance."
@@ -15049,10 +15366,10 @@ msgstr ""
msgid "Previous Artifacts"
msgstr ""
-msgid "Previous file in diff (MRs only)"
+msgid "Previous file in diff"
msgstr ""
-msgid "Previous unresolved discussion (MRs only)"
+msgid "Previous unresolved discussion"
msgstr ""
msgid "Primary"
@@ -15085,6 +15402,9 @@ msgstr ""
msgid "Private profile"
msgstr ""
+msgid "Private projects Minutes cost factor"
+msgstr ""
+
msgid "Private projects can be created in your personal namespace with:"
msgstr ""
@@ -15913,6 +16233,9 @@ msgstr ""
msgid "ProjectSettings|With GitLab Pages you can host your static websites on GitLab"
msgstr ""
+msgid "ProjectSettings|With Metrics Dashboard you can visualize this project performance metrics"
+msgstr ""
+
msgid "ProjectTemplates|.NET Core"
msgstr ""
@@ -15976,6 +16299,9 @@ msgstr ""
msgid "ProjectTemplates|Spring"
msgstr ""
+msgid "ProjectTemplates|Static Site Editor/Middleman"
+msgstr ""
+
msgid "ProjectTemplates|iOS (Swift)"
msgstr ""
@@ -16195,13 +16521,13 @@ msgstr ""
msgid "PrometheusService|Prometheus is being automatically managed on your clusters"
msgstr ""
-msgid "PrometheusService|These metrics will only be monitored after your first deployment to an environment"
+msgid "PrometheusService|Select the Active checkbox to override the Auto Configuration with custom settings. If unchecked, Auto Configuration settings are used."
msgstr ""
-msgid "PrometheusService|Time-series monitoring service"
+msgid "PrometheusService|These metrics will only be monitored after your first deployment to an environment"
msgstr ""
-msgid "PrometheusService|To enable manual configuration, uninstall Prometheus from your clusters"
+msgid "PrometheusService|Time-series monitoring service"
msgstr ""
msgid "PrometheusService|To enable the installation of Prometheus on your clusters, deactivate the manual configuration below"
@@ -16387,6 +16713,9 @@ msgstr ""
msgid "Protip:"
msgstr ""
+msgid "Protocol"
+msgstr ""
+
msgid "Provider"
msgstr ""
@@ -16411,6 +16740,9 @@ msgstr ""
msgid "Public pipelines"
msgstr ""
+msgid "Public projects Minutes cost factor"
+msgstr ""
+
msgid "Pull"
msgstr ""
@@ -16610,6 +16942,9 @@ msgid "Refreshing in a second to show the updated status..."
msgid_plural "Refreshing in %d seconds to show the updated status..."
msgstr[0] ""
+msgid "Regenerate export"
+msgstr ""
+
msgid "Regenerate instance ID"
msgstr ""
@@ -16986,9 +17321,28 @@ msgstr ""
msgid "Reports|%{combinedString} and %{resolvedString}"
msgstr ""
+msgid "Reports|Accessibility scanning detected %d issue for the source branch only"
+msgid_plural "Reports|Accessibility scanning detected %d issues for the source branch only"
+msgstr[0] ""
+
+msgid "Reports|Accessibility scanning detected no issues for the source branch only"
+msgstr ""
+
+msgid "Reports|Accessibility scanning failed loading results"
+msgstr ""
+
+msgid "Reports|Accessibility scanning results are being parsed"
+msgstr ""
+
msgid "Reports|Actions"
msgstr ""
+msgid "Reports|An error occured while loading report"
+msgstr ""
+
+msgid "Reports|An error occurred while loading %{name} results"
+msgstr ""
+
msgid "Reports|Class"
msgstr ""
@@ -17094,7 +17448,7 @@ msgstr ""
msgid "Requested %{time_ago}"
msgstr ""
-msgid "Requested design version does not exist"
+msgid "Requested design version does not exist."
msgstr ""
msgid "Requested states are invalid"
@@ -17276,6 +17630,9 @@ msgstr ""
msgid "Resync"
msgstr ""
+msgid "Resync all"
+msgstr ""
+
msgid "Resync all %{replicableType}"
msgstr ""
@@ -17343,6 +17700,9 @@ msgstr ""
msgid "Revoked personal access token %{personal_access_token_name}!"
msgstr ""
+msgid "Revoked project access token %{project_access_token_name}!"
+msgstr ""
+
msgid "RightSidebar|adding a"
msgstr ""
@@ -17391,6 +17751,9 @@ msgstr ""
msgid "Runner token"
msgstr ""
+msgid "Runner tokens"
+msgstr ""
+
msgid "Runner was not updated."
msgstr ""
@@ -17445,6 +17808,9 @@ msgstr ""
msgid "SAML SSO for %{group_name}"
msgstr ""
+msgid "SAML discovery tokens"
+msgstr ""
+
msgid "SAML for %{group_name}"
msgstr ""
@@ -17529,12 +17895,18 @@ msgstr ""
msgid "Scheduled"
msgstr ""
+msgid "Scheduled to merge this merge request (%{strategy})."
+msgstr ""
+
msgid "Scheduled to merge this merge request when the pipeline succeeds."
msgstr ""
msgid "Schedules"
msgstr ""
+msgid "Schedules to merge this merge request (%{strategy})."
+msgstr ""
+
msgid "Scheduling"
msgstr ""
@@ -17592,6 +17964,9 @@ msgstr ""
msgid "Search branches and tags"
msgstr ""
+msgid "Search by author"
+msgstr ""
+
msgid "Search files"
msgstr ""
@@ -17729,10 +18104,6 @@ msgid "SearchResults|snippet"
msgid_plural "SearchResults|snippets"
msgstr[0] ""
-msgid "SearchResults|snippet result"
-msgid_plural "SearchResults|snippet results"
-msgstr[0] ""
-
msgid "SearchResults|user"
msgid_plural "SearchResults|users"
msgstr[0] ""
@@ -17741,6 +18112,9 @@ msgid "SearchResults|wiki result"
msgid_plural "SearchResults|wiki results"
msgstr[0] ""
+msgid "Searching by both author and message is currently not supported."
+msgstr ""
+
msgid "Seat Link"
msgstr ""
@@ -17768,184 +18142,232 @@ msgstr ""
msgid "Security Dashboard"
msgstr ""
-msgid "Security Dashboard|Error fetching the vulnerability counts. Please check your network connection and try again."
+msgid "Security configuration help link"
+msgstr ""
+
+msgid "Security dashboard"
msgstr ""
-msgid "Security Dashboard|Error fetching the vulnerability list. Please check your network connection and try again."
+msgid "Security report is out of date. Please update your branch with the latest changes from the target branch (%{targetBranchName})"
msgstr ""
-msgid "Security Dashboard|Issue Created"
+msgid "Security report is out of date. Run %{newPipelineLinkStart}a new pipeline%{newPipelineLinkEnd} for the target branch (%{targetBranchName})"
msgstr ""
-msgid "Security Reports|Comment added to '%{vulnerabilityName}'"
+msgid "SecurityConfiguration|Configured"
msgstr ""
-msgid "Security Reports|Comment deleted on '%{vulnerabilityName}'"
+msgid "SecurityConfiguration|Feature"
msgstr ""
-msgid "Security Reports|Comment edited on '%{vulnerabilityName}'"
+msgid "SecurityConfiguration|Feature documentation for %{featureName}"
msgstr ""
-msgid "Security Reports|Create issue"
+msgid "SecurityConfiguration|Not yet configured"
msgstr ""
-msgid "Security Reports|Dismiss vulnerability"
+msgid "SecurityConfiguration|Secure features"
msgstr ""
-msgid "Security Reports|Dismissed '%{vulnerabilityName}'"
+msgid "SecurityConfiguration|Status"
msgstr ""
-msgid "Security Reports|Dismissed '%{vulnerabilityName}'. Turn off the hide dismissed toggle to view."
+msgid "SecurityReports|%{firstProject} and %{secondProject}"
msgstr ""
-msgid "Security Reports|Either you don't have permission to view this dashboard or the dashboard has not been setup. Please check your permission settings with your administrator or check your dashboard configurations to proceed."
+msgid "SecurityReports|%{firstProject}, %{secondProject}, and %{rest}"
msgstr ""
-msgid "Security Reports|Learn more about setting up your dashboard"
+msgid "SecurityReports|Add a project to your dashboard"
msgstr ""
-msgid "Security Reports|More info"
+msgid "SecurityReports|Add or remove projects from your dashboard"
msgstr ""
-msgid "Security Reports|Oops, something doesn't seem right."
+msgid "SecurityReports|Add projects"
msgstr ""
-msgid "Security Reports|Security reports can only be accessed by authorized users."
+msgid "SecurityReports|Comment added to '%{vulnerabilityName}'"
msgstr ""
-msgid "Security Reports|There was an error adding the comment."
+msgid "SecurityReports|Comment deleted on '%{vulnerabilityName}'"
msgstr ""
-msgid "Security Reports|There was an error creating the issue."
+msgid "SecurityReports|Comment edited on '%{vulnerabilityName}'"
msgstr ""
-msgid "Security Reports|There was an error creating the merge request."
+msgid "SecurityReports|Create issue"
msgstr ""
-msgid "Security Reports|There was an error deleting the comment."
+msgid "SecurityReports|Dismiss Selected"
msgstr ""
-msgid "Security Reports|There was an error dismissing the vulnerabilities."
+msgid "SecurityReports|Dismiss vulnerability"
msgstr ""
-msgid "Security Reports|There was an error dismissing the vulnerability."
+msgid "SecurityReports|Dismissed '%{vulnerabilityName}'"
msgstr ""
-msgid "Security Reports|There was an error reverting the dismissal."
+msgid "SecurityReports|Dismissed '%{vulnerabilityName}'. Turn off the hide dismissed toggle to view."
msgstr ""
-msgid "Security Reports|There was an error reverting this dismissal."
+msgid "SecurityReports|Each vulnerability now has a unique page that can be directly linked to, shared, referenced, and tracked as the single source of truth. Vulnerability occurrences also persist across scanner runs, which improves tracking and visibility and reduces duplicates between scans."
msgstr ""
-msgid "Security Reports|Undo dismiss"
+msgid "SecurityReports|Edit dashboard"
msgstr ""
-msgid "Security Reports|You do not have sufficient permissions to access this report"
+msgid "SecurityReports|Either you don't have permission to view this dashboard or the dashboard has not been setup. Please check your permission settings with your administrator or check your dashboard configurations to proceed."
msgstr ""
-msgid "Security Reports|You must sign in as an authorized user to see this report"
+msgid "SecurityReports|Error fetching the vulnerability counts. Please check your network connection and try again."
msgstr ""
-msgid "Security configuration help link"
+msgid "SecurityReports|Error fetching the vulnerability list. Please check your network connection and try again."
msgstr ""
-msgid "Security dashboard"
+msgid "SecurityReports|False positive"
msgstr ""
-msgid "Security report is out of date. Please update your branch with the latest changes from the target branch (%{targetBranchName})"
+msgid "SecurityReports|Hide dismissed"
msgstr ""
-msgid "Security report is out of date. Run %{newPipelineLinkStart}a new pipeline%{newPipelineLinkEnd} for the target branch (%{targetBranchName})"
+msgid "SecurityReports|Introducing standalone vulnerabilities"
msgstr ""
-msgid "SecurityConfiguration|Configured"
+msgid "SecurityReports|Issue Created"
msgstr ""
-msgid "SecurityConfiguration|Feature"
+msgid "SecurityReports|Learn More"
msgstr ""
-msgid "SecurityConfiguration|Feature documentation for %{featureName}"
+msgid "SecurityReports|Learn more about setting up your dashboard"
msgstr ""
-msgid "SecurityConfiguration|Not yet configured"
+msgid "SecurityReports|Load more vulnerabilities"
msgstr ""
-msgid "SecurityConfiguration|Secure features"
+msgid "SecurityReports|Monitor vulnerabilities in your code"
msgstr ""
-msgid "SecurityConfiguration|Status"
+msgid "SecurityReports|More info"
msgstr ""
-msgid "SecurityDashboard|%{firstProject} and %{secondProject}"
+msgid "SecurityReports|More information"
msgstr ""
-msgid "SecurityDashboard|%{firstProject}, %{secondProject}, and %{rest}"
+msgid "SecurityReports|No vulnerabilities found for dashboard"
msgstr ""
-msgid "SecurityDashboard|Add a project to your dashboard"
+msgid "SecurityReports|No vulnerabilities found for this group"
msgstr ""
-msgid "SecurityDashboard|Add or remove projects from your dashboard"
+msgid "SecurityReports|No vulnerabilities found for this pipeline"
msgstr ""
-msgid "SecurityDashboard|Add projects"
+msgid "SecurityReports|No vulnerabilities found for this project"
msgstr ""
-msgid "SecurityDashboard|Edit dashboard"
+msgid "SecurityReports|Oops, something doesn't seem right."
msgstr ""
-msgid "SecurityDashboard|Hide dismissed"
+msgid "SecurityReports|Pipeline %{pipelineLink} triggered %{timeago} by %{user}"
msgstr ""
-msgid "SecurityDashboard|Monitor vulnerabilities in your code"
+msgid "SecurityReports|Project"
msgstr ""
-msgid "SecurityDashboard|More information"
+msgid "SecurityReports|Projects added"
msgstr ""
-msgid "SecurityDashboard|No vulnerabilities found for dashboard"
+msgid "SecurityReports|Remove project from dashboard"
msgstr ""
-msgid "SecurityDashboard|Pipeline %{pipelineLink} triggered %{timeago} by %{user}"
+msgid "SecurityReports|Report type"
msgstr ""
-msgid "SecurityDashboard|Project"
+msgid "SecurityReports|Return to dashboard"
msgstr ""
-msgid "SecurityDashboard|Projects added"
+msgid "SecurityReports|Security Dashboard"
msgstr ""
-msgid "SecurityDashboard|Remove project from dashboard"
+msgid "SecurityReports|Security reports can only be accessed by authorized users."
msgstr ""
-msgid "SecurityDashboard|Report type"
+msgid "SecurityReports|Select a project to add by using the project search field above."
msgstr ""
-msgid "SecurityDashboard|Return to dashboard"
+msgid "SecurityReports|Select a reason"
msgstr ""
-msgid "SecurityDashboard|Security Dashboard"
+msgid "SecurityReports|Severity"
msgstr ""
-msgid "SecurityDashboard|Select a project to add by using the project search field above."
+msgid "SecurityReports|Status"
msgstr ""
-msgid "SecurityDashboard|Severity"
+msgid "SecurityReports|The security dashboard displays the latest security findings for projects you wish to monitor. Select \"Edit dashboard\" to add and remove projects."
msgstr ""
-msgid "SecurityDashboard|Status"
+msgid "SecurityReports|The security dashboard displays the latest security report. Use it to find and fix vulnerabilities."
msgstr ""
-msgid "SecurityDashboard|The security dashboard displays the latest security findings for projects you wish to monitor. Select \"Edit dashboard\" to add and remove projects."
+msgid "SecurityReports|There was an error adding the comment."
msgstr ""
-msgid "SecurityDashboard|The security dashboard displays the latest security report. Use it to find and fix vulnerabilities."
+msgid "SecurityReports|There was an error creating the issue."
msgstr ""
-msgid "SecurityDashboard|There was an error while generating the report."
+msgid "SecurityReports|There was an error creating the merge request."
msgstr ""
-msgid "SecurityDashboard|Unable to add %{invalidProjects}"
+msgid "SecurityReports|There was an error deleting the comment."
+msgstr ""
+
+msgid "SecurityReports|There was an error dismissing the vulnerabilities."
+msgstr ""
+
+msgid "SecurityReports|There was an error dismissing the vulnerability."
+msgstr ""
+
+msgid "SecurityReports|There was an error reverting the dismissal."
+msgstr ""
+
+msgid "SecurityReports|There was an error reverting this dismissal."
+msgstr ""
+
+msgid "SecurityReports|There was an error while generating the report."
+msgstr ""
+
+msgid "SecurityReports|Unable to add %{invalidProjects}"
+msgstr ""
+
+msgid "SecurityReports|Undo dismiss"
+msgstr ""
+
+msgid "SecurityReports|While it's rare to have no vulnerabilities for your group, it can happen. In any event, we ask that you double check your settings to make sure you've set up your dashboard correctly."
+msgstr ""
+
+msgid "SecurityReports|While it's rare to have no vulnerabilities for your pipeline, it can happen. In any event, we ask that you double check your settings to make sure all security scanning jobs have passed successfully."
+msgstr ""
+
+msgid "SecurityReports|While it's rare to have no vulnerabilities for your project, it can happen. In any event, we ask that you double check your settings to make sure you've set up your dashboard correctly."
+msgstr ""
+
+msgid "SecurityReports|While it's rare to have no vulnerabilities, it can happen. In any event, we ask that you please double check your settings to make sure you've set up your dashboard correctly."
+msgstr ""
+
+msgid "SecurityReports|Won't fix / Accept risk"
+msgstr ""
+
+msgid "SecurityReports|You do not have sufficient permissions to access this report"
+msgstr ""
+
+msgid "SecurityReports|You must sign in as an authorized user to see this report"
+msgstr ""
+
+msgid "SecurityReports|[No reason]"
msgstr ""
msgid "See GitLab's %{password_policy_guidelines}"
@@ -17957,6 +18379,9 @@ msgstr ""
msgid "See the affected projects in the GitLab admin panel"
msgstr ""
+msgid "See what's new at GitLab"
+msgstr ""
+
msgid "Select"
msgstr ""
@@ -18362,9 +18787,6 @@ msgstr ""
msgid "Set up Jira Integration"
msgstr ""
-msgid "Set up Jira Integration illustration"
-msgstr ""
-
msgid "Set up a %{type} Runner automatically"
msgstr ""
@@ -18449,6 +18871,9 @@ msgstr ""
msgid "Severity: %{severity}"
msgstr ""
+msgid "Shards (%{shards})"
+msgstr ""
+
msgid "Shards to synchronize"
msgstr ""
@@ -18762,6 +19187,9 @@ msgstr ""
msgid "Snowplow"
msgstr ""
+msgid "Some child epics may be hidden due to applied filters"
+msgstr ""
+
msgid "Some email servers do not support overriding the email sender name. Enable this option to include the name of the author of the issue, merge request or comment in the email body instead."
msgstr ""
@@ -18903,6 +19331,9 @@ msgstr ""
msgid "Something went wrong while updating your list settings"
msgstr ""
+msgid "Something went wrong with your automatic subscription renewal"
+msgstr ""
+
msgid "Something went wrong, unable to add %{project} to dashboard"
msgstr ""
@@ -19188,6 +19619,9 @@ msgstr ""
msgid "Stage removed"
msgstr ""
+msgid "Standard"
+msgstr ""
+
msgid "Star a label to make it a priority label. Order the prioritized labels to change their relative priority, by dragging."
msgstr ""
@@ -19311,6 +19745,9 @@ msgstr ""
msgid "Static Application Security Testing (SAST)"
msgstr ""
+msgid "StaticSiteEditor|An error occurred while submitting your changes."
+msgstr ""
+
msgid "StaticSiteEditor|Branch could not be created."
msgstr ""
@@ -19320,18 +19757,30 @@ msgstr ""
msgid "StaticSiteEditor|Could not create merge request."
msgstr ""
+msgid "StaticSiteEditor|Incompatible file content"
+msgstr ""
+
msgid "StaticSiteEditor|Return to site"
msgstr ""
+msgid "StaticSiteEditor|Static site editor"
+msgstr ""
+
msgid "StaticSiteEditor|Success!"
msgstr ""
msgid "StaticSiteEditor|Summary of changes"
msgstr ""
+msgid "StaticSiteEditor|The Static Site Editor is currently configured to only edit Markdown content on pages generated from Middleman. Visit the documentation to learn more about configuring your site to use the Static Site Editor."
+msgstr ""
+
msgid "StaticSiteEditor|Update %{sourcePath} file"
msgstr ""
+msgid "StaticSiteEditor|View documentation"
+msgstr ""
+
msgid "StaticSiteEditor|View merge request"
msgstr ""
@@ -19389,6 +19838,12 @@ msgstr ""
msgid "StatusPage|Status page"
msgstr ""
+msgid "StatusPage|Status page URL"
+msgstr ""
+
+msgid "StatusPage|Status page frontend documentation"
+msgstr ""
+
msgid "StatusPage|To publish incidents to an external status page, GitLab will store a JSON file in your Amazon S3 account in a location accessible to your external status page service. Make sure to also set up %{docsLink}"
msgstr ""
@@ -19737,6 +20192,12 @@ msgstr ""
msgid "Synced"
msgstr ""
+msgid "Synchronization disabled"
+msgstr ""
+
+msgid "Synchronization of container repositories is disabled."
+msgstr ""
+
msgid "System"
msgstr ""
@@ -19890,6 +20351,9 @@ msgstr ""
msgid "Telephone number"
msgstr ""
+msgid "Telephone number (Optional)"
+msgstr ""
+
msgid "Template"
msgstr ""
@@ -20193,6 +20657,9 @@ msgstr ""
msgid "The license was successfully uploaded and is now active. You can see the details below."
msgstr ""
+msgid "The license was successfully uploaded and will be active from %{starts_at}. You can see the details below."
+msgstr ""
+
msgid "The maximum file size allowed is %{size}."
msgstr ""
@@ -20514,6 +20981,9 @@ msgstr ""
msgid "There was an error getting the epic participants."
msgstr ""
+msgid "There was an error importing the Jira project."
+msgstr ""
+
msgid "There was an error loading users activity calendar."
msgstr ""
@@ -20529,7 +20999,7 @@ msgstr ""
msgid "There was an error resetting user pipeline minutes."
msgstr ""
-msgid "There was an error saving this Geo Node"
+msgid "There was an error saving this Geo Node."
msgstr ""
msgid "There was an error saving your changes."
@@ -20592,6 +21062,9 @@ msgstr ""
msgid "They can be managed using the %{link}."
msgstr ""
+msgid "Third Party Advisory Link"
+msgstr ""
+
msgid "Third party offers"
msgstr ""
@@ -20628,6 +21101,9 @@ msgstr ""
msgid "This application will be able to:"
msgstr ""
+msgid "This attachment has been truncated to avoid exceeding the maximum allowed attachment size of 15MB. %{written_count} of %{issues_count} issues have been included. Consider re-exporting with a narrower selection of issues."
+msgstr ""
+
msgid "This block is self-referential"
msgstr ""
@@ -20769,6 +21245,12 @@ msgstr ""
msgid "This issue is confidential"
msgstr ""
+msgid "This issue is currently blocked by the following issues: %{issues}."
+msgstr ""
+
+msgid "This issue is in a child epic of the filtered epic"
+msgstr ""
+
msgid "This issue is locked."
msgstr ""
@@ -20967,7 +21449,10 @@ msgstr ""
msgid "This user will be the author of all events in the activity feed that are the result of an update, like new branches being created or new commits being pushed to existing branches. Upon creation or when reassigning you can only assign yourself to be the mirror user."
msgstr ""
-msgid "This variable can not be masked"
+msgid "This variable can not be masked."
+msgstr ""
+
+msgid "This variable does not match the expected pattern."
msgstr ""
msgid "This will help us personalize your onboarding experience."
@@ -20997,10 +21482,10 @@ msgstr ""
msgid "ThreatMonitoring|Application firewall not detected"
msgstr ""
-msgid "ThreatMonitoring|Container Network Policy"
+msgid "ThreatMonitoring|Container Network Policies are not installed or have been disabled. To view this data, ensure your Network Policies are installed and enabled for your cluster."
msgstr ""
-msgid "ThreatMonitoring|Container NetworkPolicies are not installed or has been disabled. To view this data, ensure you NetworkPolicies are installed and enabled for your cluster."
+msgid "ThreatMonitoring|Container Network Policy"
msgstr ""
msgid "ThreatMonitoring|Container NetworkPolicies not detected"
@@ -21033,7 +21518,7 @@ msgstr ""
msgid "ThreatMonitoring|Something went wrong, unable to fetch statistics"
msgstr ""
-msgid "ThreatMonitoring|The firewall is not installed or has been disabled. To view this data, ensure you firewall is installed and enabled for your cluster."
+msgid "ThreatMonitoring|The firewall is not installed or has been disabled. To view this data, ensure the web application firewall is installed and enabled for your cluster."
msgstr ""
msgid "ThreatMonitoring|The graph below is an overview of traffic coming to your application as tracked by the Web Application Firewall (WAF). View the docs for instructions on how to access the WAF logs to see what type of malicious traffic is trying to access your app. The docs link is also accessible by clicking the \"?\" icon next to the title below."
@@ -21102,6 +21587,9 @@ msgstr ""
msgid "Time in seconds GitLab will wait for a response from the external service. When the service does not respond in time, access will be denied."
msgstr ""
+msgid "Time of import: %{importTime}"
+msgstr ""
+
msgid "Time remaining"
msgstr ""
@@ -21284,7 +21772,7 @@ msgstr ""
msgid "Title:"
msgstr ""
-msgid "Titles and Filenames"
+msgid "Titles and Descriptions"
msgstr ""
msgid "To"
@@ -21491,7 +21979,7 @@ msgstr ""
msgid "Too many projects enabled. You will need to manage them via the console or the API."
msgstr ""
-msgid "Topics"
+msgid "Topics (optional)"
msgstr ""
msgid "Total"
@@ -21653,6 +22141,9 @@ msgstr ""
msgid "Try all GitLab has to offer for 30 days."
msgstr ""
+msgid "Try changing or removing filters."
+msgstr ""
+
msgid "Try to fork again"
msgstr ""
@@ -21746,6 +22237,9 @@ msgstr ""
msgid "Unable to connect to server: %{error}"
msgstr ""
+msgid "Unable to connect to the Jira instance. Please check your Jira integration configuration."
+msgstr ""
+
msgid "Unable to convert Kubernetes logs encoding to UTF-8"
msgstr ""
@@ -22687,6 +23181,9 @@ msgstr ""
msgid "View issue"
msgstr ""
+msgid "View issues"
+msgstr ""
+
msgid "View it on GitLab"
msgstr ""
@@ -22864,6 +23361,15 @@ msgstr ""
msgid "VulnerabilityManagement|Resolved %{timeago} by %{user}"
msgstr ""
+msgid "VulnerabilityManagement|Something went wrong while trying to delete the comment. Please try again later."
+msgstr ""
+
+msgid "VulnerabilityManagement|Something went wrong while trying to retrieve the vulnerability history. Please try again later."
+msgstr ""
+
+msgid "VulnerabilityManagement|Something went wrong while trying to save the comment. Please try again later."
+msgstr ""
+
msgid "VulnerabilityManagement|Something went wrong, could not create an issue."
msgstr ""
@@ -22894,6 +23400,9 @@ msgstr ""
msgid "VulnerabilityStatusTypes|Resolved"
msgstr ""
+msgid "Vulnerability|%{scannerName} (version %{scannerVersion})"
+msgstr ""
+
msgid "Vulnerability|Class"
msgstr ""
@@ -22927,7 +23436,10 @@ msgstr ""
msgid "Vulnerability|Project"
msgstr ""
-msgid "Vulnerability|Report Type"
+msgid "Vulnerability|Scanner Provider"
+msgstr ""
+
+msgid "Vulnerability|Scanner Type"
msgstr ""
msgid "Vulnerability|Severity"
@@ -22981,9 +23493,15 @@ msgstr ""
msgid "We sent you an email with reset password instructions"
msgstr ""
+msgid "We tried to automatically renew your %{strong}%{plan_name}%{strong_close} subscription for %{strong}%{namespace_name}%{strong_close} on %{expires_on} but something went wrong so your subscription was downgraded to the free plan. Don't worry, your data is safe. We suggest you check your payment method and get in touch with our support team (%{support_link}). They'll gladly help with your subscription renewal."
+msgstr ""
+
msgid "We want to be sure it is you, please confirm you are not a robot."
msgstr ""
+msgid "We will automatically renew your %{strong}%{plan_name}%{strong_close} subscription for %{strong}%{namespace_name}%{strong_close} on %{strong}%{expires_on}%{strong_close}. There's nothing that you need to do, we'll let you know when the renewal is complete. Need more seats, a higher plan or just want to review your payment method?"
+msgstr ""
+
msgid "We've found no vulnerabilities"
msgstr ""
@@ -23087,15 +23605,6 @@ msgstr ""
msgid "When:"
msgstr ""
-msgid "While it's rare to have no vulnerabilities for your group, it can happen. In any event, we ask that you double check your settings to make sure you've set up your dashboard correctly."
-msgstr ""
-
-msgid "While it's rare to have no vulnerabilities for your pipeline, it can happen. In any event, we ask that you double check your settings to make sure all security scanning jobs have passed successfully."
-msgstr ""
-
-msgid "While it's rare to have no vulnerabilities for your project, it can happen. In any event, we ask that you double check your settings to make sure you've set up your dashboard correctly."
-msgstr ""
-
msgid "While it's rare to have no vulnerabilities, it can happen. In any event, we ask that you please double check your settings to make sure you've set up your dashboard correctly."
msgstr ""
@@ -23126,9 +23635,6 @@ msgstr ""
msgid "Wiki"
msgstr "Wiki"
-msgid "Wiki pages"
-msgstr ""
-
msgid "Wiki was successfully updated."
msgstr ""
@@ -23312,6 +23818,9 @@ msgstr ""
msgid "Yes, add it"
msgstr ""
+msgid "Yes, close issue"
+msgstr ""
+
msgid "Yes, let me map Google Code users to full names or GitLab users."
msgstr ""
@@ -23507,6 +24016,9 @@ msgstr ""
msgid "You could not create a new trigger."
msgstr ""
+msgid "You didn't renew your %{strong}%{plan_name}%{strong_close} subscription for %{strong}%{namespace_name}%{strong_close} so it was downgraded to the free plan."
+msgstr ""
+
msgid "You didn't renew your %{strong}%{plan_name}%{strong_close} subscription so it was downgraded to the GitLab Core Plan."
msgstr ""
@@ -23573,6 +24085,9 @@ msgstr ""
msgid "You have declined the invitation to join %{label}."
msgstr ""
+msgid "You have imported from this project %{numberOfPreviousImportsForProject} times before. Each new import will create duplicate issues."
+msgstr ""
+
msgid "You have no permissions"
msgstr ""
@@ -23654,6 +24169,9 @@ msgstr ""
msgid "You need to upload a Google Takeout archive."
msgstr ""
+msgid "You reached %{usage_in_percent} of %{namespace_name}'s capacity (%{used_storage} of %{storage_limit})"
+msgstr ""
+
msgid "You tried to fork %{link_to_the_project} but it failed for the following reason:"
msgstr ""
@@ -23738,9 +24256,21 @@ msgstr ""
msgid "YouTube"
msgstr ""
+msgid "Your %{strong}%{plan_name}%{strong_close} subscription for %{strong}%{namespace_name}%{strong_close} will expire on %{strong}%{expires_on}%{strong_close}. After that, you will not to be able to create issues or merge requests as well as many other features."
+msgstr ""
+
msgid "Your %{strong}%{plan_name}%{strong_close} subscription will expire on %{strong}%{expires_on}%{strong_close}. After that, you will not to be able to create issues or merge requests as well as many other features."
msgstr ""
+msgid "Your CSV export has started. It will be emailed to %{email} when complete."
+msgstr ""
+
+msgid "Your CSV export of %{issues_count} from project %{project_link} has been added to this email as an attachment."
+msgstr ""
+
+msgid "Your CSV export of %{written_count} from project %{project_name} (%{project_url}) has been added to this email as an attachment."
+msgstr ""
+
msgid "Your Commit Email will be used for web based operations, such as edits and merges."
msgstr ""
@@ -23837,6 +24367,9 @@ msgstr ""
msgid "Your comment could not be updated! Please check your network connection and try again."
msgstr ""
+msgid "Your comment will be discarded."
+msgstr ""
+
msgid "Your custom stage '%{title}' was created"
msgstr ""
@@ -23876,6 +24409,9 @@ msgstr ""
msgid "Your new personal access token has been created."
msgstr ""
+msgid "Your new project access token has been created."
+msgstr ""
+
msgid "Your password isn't required to view this page. If a password or any other personal details are requested, please contact your administrator to report abuse."
msgstr ""
@@ -23894,12 +24430,18 @@ msgstr ""
msgid "Your request for access has been queued for review."
msgstr ""
+msgid "Your search didn't match any commits."
+msgstr ""
+
msgid "Your subscription expired!"
msgstr ""
msgid "Your subscription has been downgraded"
msgstr ""
+msgid "Your subscription will automatically renew in %{remaining_days}"
+msgstr ""
+
msgid "Your subscription will expire in %{remaining_days}"
msgstr ""
@@ -23937,7 +24479,7 @@ msgstr "å‰"
msgid "allowed to fail"
msgstr ""
-msgid "already being used for another group or project milestone."
+msgid "already being used for another group or project %{timebox_name}."
msgstr ""
msgid "already has a \"created\" issue link"
@@ -24015,6 +24557,9 @@ msgstr ""
msgid "ciReport|%{linkStartTag}Learn more about SAST %{linkEndTag}"
msgstr ""
+msgid "ciReport|%{linkStartTag}Learn more about Secret Scanning %{linkEndTag}"
+msgstr ""
+
msgid "ciReport|%{linkStartTag}Learn more about codequality reports %{linkEndTag}"
msgstr ""
@@ -24173,6 +24718,12 @@ msgstr ""
msgid "ciReport|SAST"
msgstr ""
+msgid "ciReport|Secret scanning"
+msgstr ""
+
+msgid "ciReport|Secret scanning detects secrets and credentials vulnerabilities in your source code."
+msgstr ""
+
msgid "ciReport|Security scanning"
msgstr ""
@@ -24366,6 +24917,9 @@ msgstr ""
msgid "group"
msgstr ""
+msgid "groups"
+msgstr ""
+
msgid "has already been linked to another vulnerability"
msgstr ""
@@ -24527,9 +25081,6 @@ msgstr[0] ""
msgid "merged %{time_ago}"
msgstr ""
-msgid "milestone should belong either to a project or a group."
-msgstr ""
-
msgid "missing"
msgstr ""
@@ -24866,6 +25417,9 @@ msgstr ""
msgid "no contributions"
msgstr ""
+msgid "no expiration"
+msgstr ""
+
msgid "no one can merge"
msgstr ""
@@ -24916,6 +25470,9 @@ msgstr ""
msgid "pending removal"
msgstr ""
+msgid "per day"
+msgstr ""
+
msgid "pipeline"
msgstr ""
@@ -24942,6 +25499,12 @@ msgstr[0] ""
msgid "project avatar"
msgstr ""
+msgid "projects"
+msgstr ""
+
+msgid "push to your repository, create pipelines, create issues or add comments. To reduce storage capacity, delete unused repositories, artifacts, wikis, issues, and pipelines."
+msgstr ""
+
msgid "quick actions"
msgstr ""
@@ -25087,10 +25650,6 @@ msgstr ""
msgid "this document"
msgstr ""
-msgid "thread resolved"
-msgid_plural "threads resolved"
-msgstr[0] ""
-
msgid "to help your contributors communicate effectively!"
msgstr ""
@@ -25145,6 +25704,9 @@ msgstr ""
msgid "view the blob"
msgstr ""
+msgid "vulnerability|Add a comment"
+msgstr ""
+
msgid "vulnerability|Add a comment or reason for dismissal"
msgstr ""
diff --git a/package.json b/package.json
index bd2ded48e03..82976399171 100644
--- a/package.json
+++ b/package.json
@@ -4,14 +4,15 @@
"check-dependencies": "scripts/frontend/check_dependencies.sh",
"block-dependencies": "node scripts/frontend/block_dependencies.js",
"clean": "rm -rf public/assets tmp/cache/*-loader",
- "dev-server": "NODE_OPTIONS=\"--max-old-space-size=3584\" nodemon -w 'config/webpack.config.js' --exec 'webpack-dev-server --config config/webpack.config.js'",
+ "dev-server": "NODE_OPTIONS=\"--max-old-space-size=3584\" node scripts/frontend/webpack_dev_server.js",
"eslint": "eslint --cache --max-warnings 0 --report-unused-disable-directives --ext .js,.vue .",
"eslint-fix": "eslint --cache --max-warnings 0 --report-unused-disable-directives --ext .js,.vue --fix .",
"eslint-report": "eslint --max-warnings 0 --ext .js,.vue --format html --output-file ./eslint-report.html --no-inline-config .",
"file-coverage": "scripts/frontend/file_test_coverage.js",
"prejest": "yarn check-dependencies",
- "jest": "jest",
+ "jest": "jest --config jest.config.unit.js",
"jest-debug": "node --inspect-brk node_modules/.bin/jest --runInBand",
+ "jest:integration": "jest --config jest.config.integration.js",
"jsdoc": "jsdoc -c config/jsdocs.config.js",
"prekarma": "yarn check-dependencies",
"karma": "BABEL_ENV=${BABEL_ENV:=karma} karma start --single-run true config/karma.config.js",
@@ -39,11 +40,14 @@
"@babel/plugin-syntax-import-meta": "^7.8.3",
"@babel/preset-env": "^7.8.4",
"@gitlab/at.js": "1.5.5",
- "@gitlab/svgs": "1.121.0",
- "@gitlab/ui": "12.1.0",
+ "@gitlab/svgs": "1.127.0",
+ "@gitlab/ui": "14.10.0",
"@gitlab/visual-review-tools": "1.6.1",
+ "@rails/actioncable": "^6.0.3",
"@sentry/browser": "^5.10.2",
- "@sourcegraph/code-host-integration": "0.0.36",
+ "@sourcegraph/code-host-integration": "0.0.46",
+ "@toast-ui/editor": "^2.0.1",
+ "@toast-ui/vue-editor": "^2.0.1",
"apollo-cache-inmemory": "^1.6.3",
"apollo-client": "^2.6.4",
"apollo-link": "^1.2.11",
@@ -60,12 +64,13 @@
"chart.js": "2.7.2",
"classlist-polyfill": "^1.2.0",
"clipboard": "^1.7.1",
+ "codemirror": "^5.48.4",
"codesandbox-api": "0.0.23",
"compression-webpack-plugin": "^3.0.1",
"copy-webpack-plugin": "^5.0.5",
"core-js": "^3.6.4",
"cropper": "^2.3.0",
- "css-loader": "^1.0.0",
+ "css-loader": "^2.1.1",
"d3-scale": "^2.2.2",
"d3-selection": "^1.2.0",
"dateformat": "^3.0.3",
@@ -96,6 +101,7 @@
"lodash": "^4.17.15",
"marked": "^0.3.12",
"mermaid": "^8.4.8",
+ "mitt": "^1.2.0",
"monaco-editor": "^0.18.1",
"monaco-editor-webpack-plugin": "^1.7.0",
"mousetrap": "^1.4.6",
@@ -124,12 +130,11 @@
"tiptap-commands": "^1.4.0",
"tiptap-extensions": "^1.8.0",
"tributejs": "4.1.3",
- "underscore": "^1.9.2",
"unfetch": "^4.1.0",
"url-loader": "^3.0.0",
"visibilityjs": "^1.2.4",
"vue": "^2.6.10",
- "vue-apollo": "^3.0.0-beta.28",
+ "vue-apollo": "^3.0.3",
"vue-loader": "^15.9.0",
"vue-router": "^3.0.2",
"vue-template-compiler": "^2.6.10",
@@ -145,7 +150,7 @@
},
"devDependencies": {
"@babel/plugin-transform-modules-commonjs": "^7.8.3",
- "@gitlab/eslint-plugin": "2.2.1",
+ "@gitlab/eslint-plugin": "3.1.0",
"@vue/test-utils": "^1.0.0-beta.30",
"axios-mock-adapter": "^1.15.0",
"babel-jest": "^24.1.0",
@@ -160,11 +165,10 @@
"eslint-import-resolver-jest": "^2.1.1",
"eslint-import-resolver-webpack": "^0.12.1",
"eslint-plugin-jasmine": "^4.1.0",
- "eslint-plugin-jest": "^22.3.0",
"eslint-plugin-no-jquery": "^2.3.0",
"gettext-extractor": "^3.4.3",
"gettext-extractor-vue": "^4.0.2",
- "graphql-tag": "^2.10.0",
+ "graphql-tag": "^2.10.1",
"istanbul-lib-coverage": "^3.0.0",
"istanbul-lib-report": "^3.0.0",
"istanbul-reports": "^3.0.0",
diff --git a/qa/Gemfile b/qa/Gemfile
index 86d8aa5c025..6eb8733ab41 100644
--- a/qa/Gemfile
+++ b/qa/Gemfile
@@ -1,14 +1,15 @@
source 'https://rubygems.org'
gem 'gitlab-qa'
-gem 'activesupport', '6.0.2' # This should stay in sync with the root's Gemfile
+gem 'activesupport', '~> 6.0.3' # This should stay in sync with the root's Gemfile
gem 'capybara', '~> 3.29.0'
gem 'capybara-screenshot', '~> 1.0.23'
gem 'rake', '~> 12.3.0'
gem 'rspec', '~> 3.7'
gem 'selenium-webdriver', '~> 3.12'
-gem 'airborne', '~> 0.2.13'
-gem 'nokogiri', '~> 1.10.5'
+gem 'airborne', '~> 0.3.4'
+gem 'rest-client', '~> 2.1.0'
+gem 'nokogiri', '~> 1.10.9'
gem 'rspec-retry', '~> 0.6.1'
gem 'rspec_junit_formatter', '~> 0.4.1'
gem 'faker', '~> 1.6', '>= 1.6.6'
diff --git a/qa/Gemfile.lock b/qa/Gemfile.lock
index ececdab80d5..9aeba236c96 100644
--- a/qa/Gemfile.lock
+++ b/qa/Gemfile.lock
@@ -1,20 +1,20 @@
GEM
remote: https://rubygems.org/
specs:
- activesupport (6.0.2)
+ activesupport (6.0.3)
concurrent-ruby (~> 1.0, >= 1.0.2)
i18n (>= 0.7, < 2)
minitest (~> 5.1)
tzinfo (~> 1.1)
- zeitwerk (~> 2.2)
+ zeitwerk (~> 2.2, >= 2.2.2)
addressable (2.7.0)
public_suffix (>= 2.0.2, < 5.0)
- airborne (0.2.13)
+ airborne (0.3.4)
activesupport
rack
- rack-test (~> 0.6, >= 0.6.2)
- rest-client (>= 1.7.3, < 3.0)
- rspec (~> 3.1)
+ rack-test (>= 1.1.0, < 2.0)
+ rest-client (>= 2.0.2, < 3.0)
+ rspec (~> 3.8)
byebug (9.1.0)
capybara (3.29.0)
addressable
@@ -34,11 +34,12 @@ GEM
debase-ruby_core_source (>= 0.10.2)
debase-ruby_core_source (0.10.6)
diff-lcs (1.3)
- domain_name (0.5.20170404)
+ domain_name (0.5.20190701)
unf (>= 0.0.5, < 1.0.0)
faker (1.9.3)
i18n (>= 0.7)
gitlab-qa (4.0.0)
+ http-accept (1.7.0)
http-cookie (1.0.3)
domain_name (~> 0.5)
i18n (1.8.2)
@@ -48,14 +49,14 @@ GEM
launchy (2.4.3)
addressable (~> 2.3)
method_source (0.9.0)
- mime-types (3.1)
+ mime-types (3.3.1)
mime-types-data (~> 3.2015)
- mime-types-data (3.2016.0521)
+ mime-types-data (3.2020.0425)
mini_mime (1.0.2)
mini_portile2 (2.4.0)
minitest (5.14.0)
netrc (0.11.0)
- nokogiri (1.10.5)
+ nokogiri (1.10.9)
mini_portile2 (~> 2.4.0)
parallel (1.17.0)
parallel_tests (2.29.0)
@@ -67,33 +68,34 @@ GEM
byebug (~> 9.1)
pry (~> 0.10)
public_suffix (4.0.1)
- rack (2.0.7)
- rack-test (0.8.3)
+ rack (2.2.2)
+ rack-test (1.1.0)
rack (>= 1.0, < 3)
rake (12.3.0)
regexp_parser (1.6.0)
- rest-client (2.0.2)
+ rest-client (2.1.0)
+ http-accept (>= 1.7.0, < 2.0)
http-cookie (>= 1.0.2, < 2.0)
mime-types (>= 1.16, < 4.0)
netrc (~> 0.8)
- rspec (3.7.0)
- rspec-core (~> 3.7.0)
- rspec-expectations (~> 3.7.0)
- rspec-mocks (~> 3.7.0)
- rspec-core (3.7.1)
- rspec-support (~> 3.7.0)
- rspec-expectations (3.7.0)
+ rspec (3.9.0)
+ rspec-core (~> 3.9.0)
+ rspec-expectations (~> 3.9.0)
+ rspec-mocks (~> 3.9.0)
+ rspec-core (3.9.2)
+ rspec-support (~> 3.9.3)
+ rspec-expectations (3.9.1)
diff-lcs (>= 1.2.0, < 2.0)
- rspec-support (~> 3.7.0)
- rspec-mocks (3.7.0)
+ rspec-support (~> 3.9.0)
+ rspec-mocks (3.9.1)
diff-lcs (>= 1.2.0, < 2.0)
- rspec-support (~> 3.7.0)
+ rspec-support (~> 3.9.0)
rspec-retry (0.6.1)
rspec-core (> 3.3)
- rspec-support (3.7.0)
+ rspec-support (3.9.3)
rspec_junit_formatter (0.4.1)
rspec-core (>= 2, < 4, != 2.12.0)
- ruby-debug-ide (0.7.0)
+ ruby-debug-ide (0.7.2)
rake (>= 0.8.1)
rubyzip (1.3.0)
selenium-webdriver (3.142.6)
@@ -101,11 +103,11 @@ GEM
rubyzip (>= 1.2.2)
thread_safe (0.3.6)
timecop (0.9.1)
- tzinfo (1.2.6)
+ tzinfo (1.2.7)
thread_safe (~> 0.1)
unf (0.1.4)
unf_ext
- unf_ext (0.0.7.4)
+ unf_ext (0.0.7.7)
xpath (3.2.0)
nokogiri (~> 1.8)
zeitwerk (2.3.0)
@@ -114,18 +116,19 @@ PLATFORMS
ruby
DEPENDENCIES
- activesupport (= 6.0.2)
- airborne (~> 0.2.13)
+ activesupport (~> 6.0.3)
+ airborne (~> 0.3.4)
capybara (~> 3.29.0)
capybara-screenshot (~> 1.0.23)
debase (~> 0.2.4.1)
faker (~> 1.6, >= 1.6.6)
gitlab-qa
knapsack (~> 1.17)
- nokogiri (~> 1.10.5)
+ nokogiri (~> 1.10.9)
parallel_tests (~> 2.29)
pry-byebug (~> 3.5.1)
rake (~> 12.3.0)
+ rest-client (~> 2.1.0)
rspec (~> 3.7)
rspec-retry (~> 0.6.1)
rspec_junit_formatter (~> 0.4.1)
diff --git a/qa/README.md b/qa/README.md
index c7c6e535963..8f41327fb15 100644
--- a/qa/README.md
+++ b/qa/README.md
@@ -46,7 +46,7 @@ the browser to use. You will need to have Chrome (or Chromium) and
### Writing tests
-- [Writing tests from scratch tutorial](../doc/development/testing_guide/end_to_end/quick_start_guide.md)
+- [Writing tests from scratch tutorial](../doc/development/testing_guide/end_to_end/beginners_guide.md)
- [Best practices](../doc/development/testing_guide/best_practices.md)
- [Using page objects](../doc/development/testing_guide/end_to_end/page_objects.md)
- [Guidelines](../doc/development/testing_guide/index.md)
diff --git a/qa/Rakefile b/qa/Rakefile
index 7ba8a6d68ba..9f547b92bb8 100644
--- a/qa/Rakefile
+++ b/qa/Rakefile
@@ -1,6 +1,7 @@
require_relative 'qa/tools/revoke_all_personal_access_tokens'
require_relative 'qa/tools/delete_subgroups'
require_relative 'qa/tools/generate_perf_testdata'
+require_relative 'qa/tools/delete_test_ssh_keys'
desc "Revokes all personal access tokens"
task :revoke_personal_access_tokens do
@@ -39,3 +40,14 @@ end
desc "Generate data and run load tests"
task generate_data_and_run_load_test: [:generate_perf_testdata, :run_artillery_load_tests]
+
+desc "Deletes test ssh keys a user"
+task :delete_test_ssh_keys, [:title_portion, :delete_before] do |t, args|
+ QA::Tools::DeleteTestSSHKeys.new(args).run
+end
+
+desc "Dry run of deleting test ssh keys for a user. Lists keys to be deleted"
+task :delete_test_ssh_keys_dry_run, [:title_portion, :delete_before] do |t, args|
+ args.with_defaults(dry_run: true)
+ QA::Tools::DeleteTestSSHKeys.new(args).run
+end
diff --git a/qa/qa.rb b/qa/qa.rb
index af418033252..fe22b5792c3 100644
--- a/qa/qa.rb
+++ b/qa/qa.rb
@@ -39,7 +39,6 @@ module QA
autoload :MailHog, 'qa/runtime/mail_hog'
autoload :IPAddress, 'qa/runtime/ip_address'
autoload :Search, 'qa/runtime/search'
- autoload :Project, 'qa/runtime/project'
autoload :ApplicationSettings, 'qa/runtime/application_settings'
module API
@@ -73,10 +72,10 @@ module QA
autoload :DeployKey, 'qa/resource/deploy_key'
autoload :DeployToken, 'qa/resource/deploy_token'
autoload :ProtectedBranch, 'qa/resource/protected_branch'
+ autoload :Pipeline, 'qa/resource/pipeline'
autoload :CiVariable, 'qa/resource/ci_variable'
autoload :Runner, 'qa/resource/runner'
autoload :PersonalAccessToken, 'qa/resource/personal_access_token'
- autoload :KubernetesCluster, 'qa/resource/kubernetes_cluster'
autoload :User, 'qa/resource/user'
autoload :ProjectMilestone, 'qa/resource/project_milestone'
autoload :Members, 'qa/resource/members'
@@ -88,6 +87,12 @@ module QA
autoload :Tag, 'qa/resource/tag'
autoload :ProjectMember, 'qa/resource/project_member'
autoload :UserGPG, 'qa/resource/user_gpg'
+ autoload :Visibility, 'qa/resource/visibility'
+
+ module KubernetesCluster
+ autoload :Base, 'qa/resource/kubernetes_cluster/base'
+ autoload :ProjectCluster, 'qa/resource/kubernetes_cluster/project_cluster'
+ end
module Events
autoload :Base, 'qa/resource/events/base'
@@ -138,6 +143,7 @@ module QA
autoload :Mattermost, 'qa/scenario/test/integration/mattermost'
autoload :ObjectStorage, 'qa/scenario/test/integration/object_storage'
autoload :SMTP, 'qa/scenario/test/integration/smtp'
+ autoload :GitalyHA, 'qa/scenario/test/integration/gitaly_ha'
end
module Sanity
@@ -156,6 +162,7 @@ module QA
autoload :Base, 'qa/page/base'
autoload :View, 'qa/page/view'
autoload :Element, 'qa/page/element'
+ autoload :PageConcern, 'qa/page/page_concern'
autoload :Validator, 'qa/page/validator'
autoload :Validatable, 'qa/page/validatable'
@@ -184,6 +191,7 @@ module QA
autoload :New, 'qa/page/dashboard/snippet/new'
autoload :Index, 'qa/page/dashboard/snippet/index'
autoload :Show, 'qa/page/dashboard/snippet/show'
+ autoload :Edit, 'qa/page/dashboard/snippet/edit'
end
end
@@ -242,11 +250,11 @@ module QA
end
module Settings
- autoload :Common, 'qa/page/project/settings/common'
autoload :Advanced, 'qa/page/project/settings/advanced'
autoload :Main, 'qa/page/project/settings/main'
autoload :Repository, 'qa/page/project/settings/repository'
autoload :CICD, 'qa/page/project/settings/ci_cd'
+ autoload :GeneralPipelines, 'qa/page/project/settings/general_pipelines'
autoload :AutoDevops, 'qa/page/project/settings/auto_devops'
autoload :DeployKeys, 'qa/page/project/settings/deploy_keys'
autoload :DeployTokens, 'qa/page/project/settings/deploy_tokens'
@@ -257,6 +265,13 @@ module QA
autoload :Members, 'qa/page/project/settings/members'
autoload :MirroringRepositories, 'qa/page/project/settings/mirroring_repositories'
autoload :VisibilityFeaturesPermissions, 'qa/page/project/settings/visibility_features_permissions'
+ autoload :Operations, 'qa/page/project/settings/operations'
+ autoload :Incidents, 'qa/page/project/settings/incidents'
+ autoload :Integrations, 'qa/page/project/settings/integrations'
+
+ module Services
+ autoload :Prometheus, 'qa/page/project/settings/services/prometheus'
+ end
end
module SubMenus
@@ -395,6 +410,7 @@ module QA
autoload :Breadcrumbs, 'qa/page/component/breadcrumbs'
autoload :CiBadgeLink, 'qa/page/component/ci_badge_link'
autoload :ClonePanel, 'qa/page/component/clone_panel'
+ autoload :DesignManagement, 'qa/page/component/design_management'
autoload :LazyLoader, 'qa/page/component/lazy_loader'
autoload :LegacyClonePanel, 'qa/page/component/legacy_clone_panel'
autoload :Dropzone, 'qa/page/component/dropzone'
@@ -404,6 +420,8 @@ module QA
autoload :UsersSelect, 'qa/page/component/users_select'
autoload :Note, 'qa/page/component/note'
autoload :ConfirmModal, 'qa/page/component/confirm_modal'
+ autoload :CustomMetric, 'qa/page/component/custom_metric'
+ autoload :DesignManagement, 'qa/page/component/design_management'
module Issuable
autoload :Common, 'qa/page/component/issuable/common'
@@ -412,6 +430,10 @@ module QA
module WebIDE
autoload :Alert, 'qa/page/component/web_ide/alert'
end
+
+ module Project
+ autoload :Templates, 'qa/page/component/project/templates'
+ end
end
end
@@ -431,6 +453,7 @@ module QA
autoload :Shellout, 'qa/service/shellout'
autoload :KubernetesCluster, 'qa/service/kubernetes_cluster'
autoload :Omnibus, 'qa/service/omnibus'
+ autoload :PraefectManager, 'qa/service/praefect_manager'
module ClusterProvider
autoload :Base, 'qa/service/cluster_provider/base'
diff --git a/qa/qa/fixtures/monitored_auto_devops/.gitlab-ci.yml b/qa/qa/fixtures/monitored_auto_devops/.gitlab-ci.yml
index 3e83c8f0f77..052ba1c14fb 100644
--- a/qa/qa/fixtures/monitored_auto_devops/.gitlab-ci.yml
+++ b/qa/qa/fixtures/monitored_auto_devops/.gitlab-ci.yml
@@ -22,10 +22,12 @@ variables:
stages:
- production
-# This job continuously deploys to staging/production on every push to `master`.
+# This job continuously deploys to production on every push to `master`.
production:
stage: production
+ tags:
+ - qa
script:
- check_kube_domain
- install_dependencies
@@ -34,7 +36,6 @@ production:
- initialize_tiller
- create_secret
- deploy
- - persist_environment_url
environment:
name: production
url: http://$CI_PROJECT_PATH_SLUG.$AUTO_DEVOPS_DOMAIN
diff --git a/qa/qa/git/repository.rb b/qa/qa/git/repository.rb
index 771f135a95c..bd2fbbd80cb 100644
--- a/qa/qa/git/repository.rb
+++ b/qa/qa/git/repository.rb
@@ -12,6 +12,8 @@ module QA
module Git
class Repository
include Scenario::Actable
+ include Support::Repeater
+
RepositoryCommandError = Class.new(StandardError)
attr_writer :use_lfs, :gpg_key_id
@@ -58,8 +60,8 @@ module QA
end
def clone(opts = '')
- clone_result = run("git clone #{opts} #{uri} ./")
- return clone_result.response unless clone_result.success
+ clone_result = run("git clone #{opts} #{uri} ./", max_attempts: 3)
+ return clone_result.response unless clone_result.success?
enable_lfs_result = enable_lfs if use_lfs?
@@ -92,7 +94,7 @@ module QA
if use_lfs?
git_lfs_track_result = run(%Q{git lfs track #{name} --lockable})
- return git_lfs_track_result.response unless git_lfs_track_result.success
+ return git_lfs_track_result.response unless git_lfs_track_result.success?
end
git_add_result = run(%Q{git add #{name}})
@@ -101,11 +103,11 @@ module QA
end
def delete_tag(tag_name)
- run(%Q{git push origin --delete #{tag_name}}).to_s
+ run(%Q{git push origin --delete #{tag_name}}, max_attempts: 3).to_s
end
def commit(message)
- run(%Q{git commit -m "#{message}"}).to_s
+ run(%Q{git commit -m "#{message}"}, max_attempts: 3).to_s
end
def commit_with_gpg(message)
@@ -113,13 +115,21 @@ module QA
end
def push_changes(branch = 'master')
- run("git push #{uri} #{branch}").to_s
+ run("git push #{uri} #{branch}", max_attempts: 3).to_s
end
def merge(branch)
run("git merge #{branch}")
end
+ def init_repository
+ run("git init")
+ end
+
+ def pull(repository = nil, branch = nil)
+ run(['git', 'pull', repository, branch].compact.join(' '))
+ end
+
def commits
run('git log --oneline').to_s.split("\n")
end
@@ -164,8 +174,8 @@ module QA
def fetch_supported_git_protocol
# ls-remote is one command known to respond to Git protocol v2 so we use
# it to get output including the version reported via Git tracing
- output = run("git ls-remote #{uri}", "GIT_TRACE_PACKET=1")
- output.response[/git< version (\d+)/, 1] || 'unknown'
+ result = run("git ls-remote #{uri}", env: "GIT_TRACE_PACKET=1", max_attempts: 3)
+ result.response[/git< version (\d+)/, 1] || 'unknown'
end
def try_add_credentials_to_netrc
@@ -175,6 +185,10 @@ module QA
save_netrc_content
end
+ def file_content(file)
+ run("cat #{file}").to_s
+ end
+
private
attr_reader :uri, :username, :password, :known_hosts_file,
@@ -182,9 +196,12 @@ module QA
alias_method :use_lfs?, :use_lfs
- Result = Struct.new(:success, :response) do
- alias_method :success?, :success
+ Result = Struct.new(:command, :exitstatus, :response) do
alias_method :to_s, :response
+
+ def success?
+ exitstatus.zero?
+ end
end
def add_credentials?
@@ -209,19 +226,26 @@ module QA
touch_gitconfig_result.to_s + git_lfs_install_result.to_s
end
- def run(command_str, *extra_env)
- command = [env_vars, *extra_env, command_str, '2>&1'].compact.join(' ')
- Runtime::Logger.debug "Git: pwd=[#{Dir.pwd}], command=[#{command}]"
+ def run(command_str, env: [], max_attempts: 1)
+ command = [env_vars, *env, command_str, '2>&1'].compact.join(' ')
+ result = nil
- output, status = Open3.capture2e(command)
- output.chomp!
- Runtime::Logger.debug "Git: output=[#{output}], exitstatus=[#{status.exitstatus}]"
+ repeat_until(max_attempts: max_attempts, raise_on_failure: false) do
+ Runtime::Logger.debug "Git: pwd=[#{Dir.pwd}], command=[#{command}]"
+ output, status = Open3.capture2e(command)
+ output.chomp!
+ Runtime::Logger.debug "Git: output=[#{output}], exitstatus=[#{status.exitstatus}]"
+
+ result = Result.new(command, status.exitstatus, output)
+
+ result.success?
+ end
- unless status.success?
- raise RepositoryCommandError, "The command #{command} failed (#{status.exitstatus}) with the following output:\n#{output}"
+ unless result.success?
+ raise RepositoryCommandError, "The command #{result.command} failed (#{result.exitstatus}) with the following output:\n#{result.response}"
end
- Result.new(status.exitstatus == 0, output)
+ result
end
def default_credentials
diff --git a/qa/qa/page/base.rb b/qa/qa/page/base.rb
index 42208f05c89..cb3827f8eb1 100644
--- a/qa/qa/page/base.rb
+++ b/qa/qa/page/base.rb
@@ -133,8 +133,13 @@ module QA
end
# replace with (..., page = self.class)
- def click_element(name, page = nil, text: nil, wait: Capybara.default_max_wait_time)
- find_element(name, text: text, wait: wait).click
+ def click_element(name, page = nil, **kwargs)
+ wait_for_requests
+
+ wait = kwargs.delete(:wait) || Capybara.default_max_wait_time
+ text = kwargs.delete(:text)
+
+ find(element_selector_css(name, kwargs), text: text, wait: wait).click
page.validate_elements_present! if page
end
diff --git a/qa/qa/page/component/breadcrumbs.rb b/qa/qa/page/component/breadcrumbs.rb
index 656aa380bbd..2576e376e4e 100644
--- a/qa/qa/page/component/breadcrumbs.rb
+++ b/qa/qa/page/component/breadcrumbs.rb
@@ -4,7 +4,11 @@ module QA
module Page
module Component
module Breadcrumbs
+ extend QA::Page::PageConcern
+
def self.included(base)
+ super
+
base.view 'app/views/layouts/nav/_breadcrumbs.html.haml' do
element :breadcrumb_links_content
end
diff --git a/qa/qa/page/component/ci_badge_link.rb b/qa/qa/page/component/ci_badge_link.rb
index 3db675c3a60..8629399e911 100644
--- a/qa/qa/page/component/ci_badge_link.rb
+++ b/qa/qa/page/component/ci_badge_link.rb
@@ -4,6 +4,8 @@ module QA
module Page
module Component
module CiBadgeLink
+ extend QA::Page::PageConcern
+
COMPLETED_STATUSES = %w[passed failed canceled blocked skipped manual].freeze # excludes created, pending, running
INCOMPLETE_STATUSES = %w[pending created running].freeze
@@ -27,6 +29,8 @@ module QA
end
def self.included(base)
+ super
+
base.view 'app/assets/javascripts/vue_shared/components/ci_badge_link.vue' do
element :status_badge
end
diff --git a/qa/qa/page/component/clone_panel.rb b/qa/qa/page/component/clone_panel.rb
index fbe19e5802b..a0aea6fe44d 100644
--- a/qa/qa/page/component/clone_panel.rb
+++ b/qa/qa/page/component/clone_panel.rb
@@ -4,7 +4,11 @@ module QA
module Page
module Component
module ClonePanel
+ extend QA::Page::PageConcern
+
def self.included(base)
+ super
+
base.view 'app/views/projects/buttons/_clone.html.haml' do
element :clone_dropdown
element :clone_options
diff --git a/qa/qa/page/component/confirm_modal.rb b/qa/qa/page/component/confirm_modal.rb
index 355e2783fb7..039640d207a 100644
--- a/qa/qa/page/component/confirm_modal.rb
+++ b/qa/qa/page/component/confirm_modal.rb
@@ -4,7 +4,11 @@ module QA
module Page
module Component
module ConfirmModal
+ extend QA::Page::PageConcern
+
def self.included(base)
+ super
+
base.view 'app/views/shared/_confirm_modal.html.haml' do
element :confirm_modal
element :confirm_input
diff --git a/qa/qa/page/component/custom_metric.rb b/qa/qa/page/component/custom_metric.rb
new file mode 100644
index 00000000000..094979f5e18
--- /dev/null
+++ b/qa/qa/page/component/custom_metric.rb
@@ -0,0 +1,49 @@
+# frozen_string_literal: true
+
+module QA
+ module Page
+ module Component
+ module CustomMetric
+ extend QA::Page::PageConcern
+
+ def self.included(base)
+ super
+
+ base.view 'app/assets/javascripts/custom_metrics/components/custom_metrics_form_fields.vue' do
+ element :custom_metric_prometheus_title_field
+ element :custom_metric_prometheus_query_field
+ element :custom_metric_prometheus_y_label_field
+ element :custom_metric_prometheus_unit_label_field
+ element :custom_metric_prometheus_legend_label_field
+ end
+ end
+
+ def add_custom_metric
+ fill_element :custom_metric_prometheus_title_field, 'HTTP Requests Total'
+ fill_element :custom_metric_prometheus_query_field, 'rate(http_requests_total[5m])'
+ fill_element :custom_metric_prometheus_y_label_field, 'Requests/second'
+ fill_element :custom_metric_prometheus_unit_label_field, 'req/sec'
+ fill_element :custom_metric_prometheus_legend_label_field, 'HTTP requests'
+
+ save_changes
+ end
+
+ def save_changes
+ click_button(class: 'btn-success')
+ end
+
+ def delete_custom_metric
+ click_button(class: 'btn-danger')
+ within('.modal-content') { click_button(class: 'btn-danger') }
+ end
+
+ def edit_custom_metric
+ fill_element :custom_metric_prometheus_title_field, ''
+ fill_element :custom_metric_prometheus_title_field, 'Throughput'
+
+ save_changes
+ end
+ end
+ end
+ end
+end
diff --git a/qa/qa/page/component/design_management.rb b/qa/qa/page/component/design_management.rb
new file mode 100644
index 00000000000..a8a24bd3949
--- /dev/null
+++ b/qa/qa/page/component/design_management.rb
@@ -0,0 +1,82 @@
+# frozen_string_literal: true
+
+module QA
+ module Page
+ module Component
+ module DesignManagement
+ extend QA::Page::PageConcern
+
+ def self.included(base)
+ super
+
+ base.class_eval do
+ view 'app/assets/javascripts/design_management/components/design_notes/design_discussion.vue' do
+ element :design_discussion_content
+ end
+
+ view 'app/assets/javascripts/design_management/components/design_notes/design_note.vue' do
+ element :note_content
+ end
+
+ view 'app/assets/javascripts/design_management/components/design_notes/design_reply_form.vue' do
+ element :note_textarea
+ element :save_comment_button
+ end
+
+ view 'app/assets/javascripts/design_management/components/design_overlay.vue' do
+ element :design_image_button
+ end
+
+ view 'app/assets/javascripts/design_management/components/list/item.vue' do
+ element :design_file_name
+ element :design_image
+ end
+ end
+ end
+
+ def add_annotation(note)
+ click_element(:design_image_button)
+ fill_element(:note_textarea, note)
+ click_element(:save_comment_button)
+
+ # It takes a moment for the annotation to be saved.
+ # We'll check for the annotation in a test, but here we'll at least
+ # wait for the "Save comment" button to disappear
+ saved = has_no_element?(:save_comment_button)
+
+ raise ExpectationNotMet, %q(There was a problem while adding the annotation) unless saved
+ end
+
+ def add_design(design_file_path)
+ # `attach_file` doesn't seem able to find element via data attributes.
+ # It accepts a `class:` option, but that only works for class attributes
+ # It doesn't work as a CSS selector.
+ # So instead we use the name attribute as a locator
+ page.attach_file("design_file", design_file_path, make_visible: { display: 'block' })
+
+ filename = ::File.basename(design_file_path)
+
+ found = wait_until(reload: false, sleep_interval: 1) do
+ image = find_element(:design_image)
+
+ has_element?(:design_file_name, text: filename) &&
+ image["complete"] &&
+ image["naturalWidth"].to_i > 0
+ end
+
+ raise ElementNotFound, %Q(Attempted to attach design "#{filename}" but it did not appear) unless found
+ end
+
+ def click_design(filename)
+ click_element(:design_file_name, text: filename)
+ end
+
+ def has_annotation?(note)
+ within_element_by_index(:design_discussion_content, 0) do
+ has_element?(:note_content, text: note)
+ end
+ end
+ end
+ end
+ end
+end
diff --git a/qa/qa/page/component/groups_filter.rb b/qa/qa/page/component/groups_filter.rb
index 7eb1257db71..f82bb81a3fc 100644
--- a/qa/qa/page/component/groups_filter.rb
+++ b/qa/qa/page/component/groups_filter.rb
@@ -4,7 +4,11 @@ module QA
module Page
module Component
module GroupsFilter
+ extend QA::Page::PageConcern
+
def self.included(base)
+ super
+
base.view 'app/views/shared/groups/_search_form.html.haml' do
element :groups_filter
end
diff --git a/qa/qa/page/component/issuable/common.rb b/qa/qa/page/component/issuable/common.rb
index 1155d4da036..bbab1746d7a 100644
--- a/qa/qa/page/component/issuable/common.rb
+++ b/qa/qa/page/component/issuable/common.rb
@@ -5,7 +5,11 @@ module QA
module Component
module Issuable
module Common
+ extend QA::Page::PageConcern
+
def self.included(base)
+ super
+
base.view 'app/assets/javascripts/issue_show/components/title.vue' do
element :edit_button
element :title, required: true
diff --git a/qa/qa/page/component/lazy_loader.rb b/qa/qa/page/component/lazy_loader.rb
index 6f74a4691ba..2123431fc55 100644
--- a/qa/qa/page/component/lazy_loader.rb
+++ b/qa/qa/page/component/lazy_loader.rb
@@ -4,7 +4,11 @@ module QA
module Page
module Component
module LazyLoader
+ extend QA::Page::PageConcern
+
def self.included(base)
+ super
+
base.view 'app/assets/javascripts/lazy_loader.js' do
element :js_lazy_loaded
end
diff --git a/qa/qa/page/component/legacy_clone_panel.rb b/qa/qa/page/component/legacy_clone_panel.rb
index 7b4b30623a6..ebab9fd708c 100644
--- a/qa/qa/page/component/legacy_clone_panel.rb
+++ b/qa/qa/page/component/legacy_clone_panel.rb
@@ -4,7 +4,11 @@ module QA
module Page
module Component
module LegacyClonePanel
+ extend QA::Page::PageConcern
+
def self.included(base)
+ super
+
base.view 'app/views/shared/_clone_panel.html.haml' do
element :clone_dropdown
element :clone_options_dropdown, '.clone-options-dropdown' # rubocop:disable QA/ElementWithPattern
diff --git a/qa/qa/page/component/note.rb b/qa/qa/page/component/note.rb
index 3e8ed9069ce..0e9cdd49519 100644
--- a/qa/qa/page/component/note.rb
+++ b/qa/qa/page/component/note.rb
@@ -4,7 +4,11 @@ module QA
module Page
module Component
module Note
+ extend QA::Page::PageConcern
+
def self.included(base)
+ super
+
base.view 'app/assets/javascripts/notes/components/comment_form.vue' do
element :note_dropdown
element :discussion_option
diff --git a/qa/qa/page/component/project/templates.rb b/qa/qa/page/component/project/templates.rb
new file mode 100644
index 00000000000..8baf15acdff
--- /dev/null
+++ b/qa/qa/page/component/project/templates.rb
@@ -0,0 +1,15 @@
+# frozen_string_literal: true
+
+module QA
+ module Page::Component
+ module Project
+ module Templates
+ def use_template_for_project(project_name)
+ within find_element(:template_option_row, text: project_name) do
+ click_element :use_template_button
+ end
+ end
+ end
+ end
+ end
+end
diff --git a/qa/qa/page/component/select2.rb b/qa/qa/page/component/select2.rb
index e667fad1dd3..b8beb64b6bd 100644
--- a/qa/qa/page/component/select2.rb
+++ b/qa/qa/page/component/select2.rb
@@ -18,10 +18,14 @@ module QA
end
end
- def search_and_select(item_text)
+ def search_item(item_text)
find('.select2-input').set(item_text)
wait_for_search_to_complete
+ end
+
+ def search_and_select(item_text)
+ search_item(item_text)
select_item(item_text)
end
@@ -36,7 +40,7 @@ module QA
end
def dropdown_open?
- has_css?('.select2-input')
+ find('.select2-focusser').disabled?
end
end
end
diff --git a/qa/qa/page/component/web_ide/alert.rb b/qa/qa/page/component/web_ide/alert.rb
index 0f0623d5ebf..c2903662b52 100644
--- a/qa/qa/page/component/web_ide/alert.rb
+++ b/qa/qa/page/component/web_ide/alert.rb
@@ -5,8 +5,12 @@ module QA
module Component
module WebIDE
module Alert
- def self.prepended(page)
- page.module_eval do
+ extend QA::Page::PageConcern
+
+ def self.prepended(base)
+ super
+
+ base.class_eval do
view 'app/assets/javascripts/ide/components/error_message.vue' do
element :flash_alert
end
diff --git a/qa/qa/page/dashboard/snippet/edit.rb b/qa/qa/page/dashboard/snippet/edit.rb
new file mode 100644
index 00000000000..d28b8178c99
--- /dev/null
+++ b/qa/qa/page/dashboard/snippet/edit.rb
@@ -0,0 +1,36 @@
+# frozen_string_literal: true
+
+module QA
+ module Page
+ module Dashboard
+ module Snippet
+ class Edit < Page::Base
+ view 'app/views/shared/snippets/_form.html.haml' do
+ element :submit_button
+ end
+
+ view 'app/assets/javascripts/snippets/components/edit.vue' do
+ element :submit_button
+ end
+
+ def add_to_file_content(content)
+ finished_loading?
+ text_area.set content
+ text_area.has_text?(content) # wait for changes to take effect
+ end
+
+ def save_changes
+ click_element(:submit_button)
+ wait_until { assert_no_element(:submit_button) }
+ end
+
+ private
+
+ def text_area
+ find('#editor textarea', visible: false)
+ end
+ end
+ end
+ end
+ end
+end
diff --git a/qa/qa/page/dashboard/snippet/new.rb b/qa/qa/page/dashboard/snippet/new.rb
index da5013e787e..d1a194ba1db 100644
--- a/qa/qa/page/dashboard/snippet/new.rb
+++ b/qa/qa/page/dashboard/snippet/new.rb
@@ -5,16 +5,33 @@ module QA
module Dashboard
module Snippet
class New < Page::Base
+ view 'app/assets/javascripts/snippets/components/edit.vue' do
+ element :submit_button
+ end
+
+ view 'app/assets/javascripts/snippets/components/snippet_description_edit.vue' do
+ element :snippet_description_field
+ element :description_placeholder, required: true
+ end
+
+ view 'app/assets/javascripts/snippets/components/snippet_title.vue' do
+ element :snippet_title, required: true
+ end
+
+ view 'app/assets/javascripts/snippets/components/snippet_blob_edit.vue' do
+ element :snippet_file_name
+ end
+
view 'app/views/shared/form_elements/_description.html.haml' do
element :issuable_form_description
end
view 'app/views/shared/snippets/_form.html.haml' do
- element :description_field
+ element :snippet_description_field
element :description_placeholder
element :snippet_title
element :snippet_file_name
- element :create_snippet_button
+ element :submit_button
end
view 'app/views/projects/_zen.html.haml' do
@@ -28,7 +45,7 @@ module QA
def fill_description(description)
click_element :description_placeholder
- fill_element :description_field, description
+ fill_element :snippet_description_field, description
end
def set_visibility(visibility)
@@ -46,7 +63,8 @@ module QA
end
def click_create_snippet_button
- click_element :create_snippet_button
+ wait_until(reload: false) { !find_element(:submit_button).disabled? }
+ click_element :submit_button
end
private
diff --git a/qa/qa/page/dashboard/snippet/show.rb b/qa/qa/page/dashboard/snippet/show.rb
index 88d6ef02d22..d43b64cd1d4 100644
--- a/qa/qa/page/dashboard/snippet/show.rb
+++ b/qa/qa/page/dashboard/snippet/show.rb
@@ -5,10 +5,17 @@ module QA
module Dashboard
module Snippet
class Show < Page::Base
+ view 'app/assets/javascripts/snippets/components/snippet_description_view.vue' do
+ element :snippet_description_field
+ end
+
+ view 'app/assets/javascripts/snippets/components/snippet_title.vue' do
+ element :snippet_title, required: true
+ end
+
view 'app/views/shared/snippets/_header.html.haml' do
element :snippet_title, required: true
- element :snippet_description, required: true
- element :embed_type
+ element :snippet_description_field, required: true
element :snippet_box
end
@@ -16,22 +23,38 @@ module QA
element :file_title_name
end
+ view 'app/assets/javascripts/blob/components/blob_header_filepath.vue' do
+ element :file_title_name
+ end
+
view 'app/views/shared/_file_highlight.html.haml' do
element :file_content
end
+ view 'app/assets/javascripts/vue_shared/components/blob_viewers/simple_viewer.vue' do
+ element :file_content
+ end
+
+ view 'app/assets/javascripts/snippets/components/snippet_header.vue' do
+ element :snippet_action_button
+ element :delete_snippet_button
+ end
+
+ view 'app/assets/javascripts/snippets/components/snippet_blob_view.vue' do
+ element :clone_button
+ end
+
+ view 'app/assets/javascripts/vue_shared/components/clone_dropdown.vue' do
+ element :copy_http_url_button
+ element :copy_ssh_url_button
+ end
+
def has_snippet_title?(snippet_title)
has_element? :snippet_title, text: snippet_title
end
def has_snippet_description?(snippet_description)
- has_element? :snippet_description, text: snippet_description
- end
-
- def has_embed_type?(embed_type)
- within_element(:embed_type) do
- has_text?(embed_type)
- end
+ has_element? :snippet_description_field, text: snippet_description
end
def has_visibility_type?(visibility_type)
@@ -52,6 +75,30 @@ module QA
has_text?(file_content)
end
end
+
+ def click_edit_button
+ finished_loading?
+ click_element(:snippet_action_button, action: 'Edit')
+ end
+
+ def click_delete_button
+ finished_loading?
+ click_element(:snippet_action_button, action: 'Delete')
+ click_element(:delete_snippet_button)
+ finished_loading? # wait for the page to reload after deletion
+ end
+
+ def get_repository_uri_http
+ finished_loading?
+ click_element(:clone_button)
+ Git::Location.new(find_element(:copy_http_url_button)['data-clipboard-text']).uri.to_s
+ end
+
+ def get_repository_uri_ssh
+ finished_loading?
+ click_element(:clone_button)
+ Git::Location.new(find_element(:copy_ssh_url_button)['data-clipboard-text']).uri.to_s
+ end
end
end
end
diff --git a/qa/qa/page/file/shared/commit_button.rb b/qa/qa/page/file/shared/commit_button.rb
index 9ea4f4e7818..c1c5907f27d 100644
--- a/qa/qa/page/file/shared/commit_button.rb
+++ b/qa/qa/page/file/shared/commit_button.rb
@@ -5,7 +5,11 @@ module QA
module File
module Shared
module CommitButton
+ extend QA::Page::PageConcern
+
def self.included(base)
+ super
+
base.view 'app/views/projects/_commit_button.html.haml' do
element :commit_button
end
diff --git a/qa/qa/page/file/shared/commit_message.rb b/qa/qa/page/file/shared/commit_message.rb
index ce3b1e9939c..823ce7bf7f9 100644
--- a/qa/qa/page/file/shared/commit_message.rb
+++ b/qa/qa/page/file/shared/commit_message.rb
@@ -5,7 +5,11 @@ module QA
module File
module Shared
module CommitMessage
+ extend QA::Page::PageConcern
+
def self.included(base)
+ super
+
base.view 'app/views/shared/_commit_message_container.html.haml' do
element :commit_message, "text_area_tag 'commit_message'" # rubocop:disable QA/ElementWithPattern
end
diff --git a/qa/qa/page/file/shared/editor.rb b/qa/qa/page/file/shared/editor.rb
index 448c09cfbca..ce4465d2a5c 100644
--- a/qa/qa/page/file/shared/editor.rb
+++ b/qa/qa/page/file/shared/editor.rb
@@ -5,7 +5,11 @@ module QA
module File
module Shared
module Editor
+ extend QA::Page::PageConcern
+
def self.included(base)
+ super
+
base.view 'app/views/projects/blob/_editor.html.haml' do
element :editor
end
diff --git a/qa/qa/page/group/sub_menus/common.rb b/qa/qa/page/group/sub_menus/common.rb
index 96efc8da98d..86102f70d29 100644
--- a/qa/qa/page/group/sub_menus/common.rb
+++ b/qa/qa/page/group/sub_menus/common.rb
@@ -5,9 +5,12 @@ module QA
module Group
module SubMenus
module Common
+ extend QA::Page::PageConcern
include QA::Page::SubMenus::Common
def self.included(base)
+ super
+
base.class_eval do
view 'app/views/layouts/nav/sidebar/_group.html.haml' do
element :group_sidebar
diff --git a/qa/qa/page/main/terms.rb b/qa/qa/page/main/terms.rb
index a4928f24397..a0de267fb31 100644
--- a/qa/qa/page/main/terms.rb
+++ b/qa/qa/page/main/terms.rb
@@ -1,20 +1,22 @@
# frozen_string_literal: true
module QA
- module Page::Main
- class Terms < Page::Base
- view 'app/views/layouts/terms.html.haml' do
- element :user_avatar, required: true
- end
+ module Page
+ module Main
+ class Terms < Page::Base
+ view 'app/views/layouts/terms.html.haml' do
+ element :user_avatar, required: true
+ end
- view 'app/views/users/terms/index.html.haml' do
- element :terms_content, required: true
+ view 'app/views/users/terms/index.html.haml' do
+ element :terms_content, required: true
- element :accept_terms_button
- end
+ element :accept_terms_button
+ end
- def accept_terms
- click_element :accept_terms_button, Page::Main::Menu
+ def accept_terms
+ click_element :accept_terms_button, Page::Main::Menu
+ end
end
end
end
diff --git a/qa/qa/page/page_concern.rb b/qa/qa/page/page_concern.rb
new file mode 100644
index 00000000000..6ba2d27f574
--- /dev/null
+++ b/qa/qa/page/page_concern.rb
@@ -0,0 +1,16 @@
+module QA
+ module Page
+ module PageConcern
+ def included(base)
+ unless base.is_a?(Class)
+ raise "Expected #{self} to be prepended to a class, but #{base} is a module!"
+ end
+
+ unless base.ancestors.include?(::QA::Page::Base)
+ raise "Expected #{self} to be prepended to a class that inherits from ::QA::Page::Base, but #{base} doesn't!"
+ end
+ end
+ alias_method :prepended, :included
+ end
+ end
+end
diff --git a/qa/qa/page/profile/personal_access_tokens.rb b/qa/qa/page/profile/personal_access_tokens.rb
index 7069e7d3e4f..fd191fa3e27 100644
--- a/qa/qa/page/profile/personal_access_tokens.rb
+++ b/qa/qa/page/profile/personal_access_tokens.rb
@@ -6,9 +6,9 @@ module QA
module Page
module Profile
class PersonalAccessTokens < Page::Base
- view 'app/views/shared/_personal_access_tokens_form.html.haml' do
+ view 'app/views/shared/access_tokens/_form.html.haml' do
element :expiry_date_field
- element :personal_access_token_name_field
+ element :access_token_name_field
element :create_token_button
end
@@ -16,15 +16,15 @@ module QA
element :api_radio, 'qa-#{scope}-radio' # rubocop:disable QA/ElementWithPattern, Lint/InterpolationCheck
end
- view 'app/views/shared/_personal_access_tokens_created_container.html.haml' do
- element :created_personal_access_token
+ view 'app/views/shared/access_tokens/_created_container.html.haml' do
+ element :created_access_token
end
- view 'app/views/shared/_personal_access_tokens_table.html.haml' do
+ view 'app/views/shared/access_tokens/_table.html.haml' do
element :revoke_button
end
def fill_token_name(name)
- fill_element(:personal_access_token_name_field, name)
+ fill_element(:access_token_name_field, name)
end
def check_api
@@ -36,7 +36,7 @@ module QA
end
def created_access_token
- find_element(:created_personal_access_token, wait: 30).value
+ find_element(:created_access_token, wait: 30).value
end
def fill_expiry_date(date)
diff --git a/qa/qa/page/profile/ssh_keys.rb b/qa/qa/page/profile/ssh_keys.rb
index 082202f91ca..810877e21ad 100644
--- a/qa/qa/page/profile/ssh_keys.rb
+++ b/qa/qa/page/profile/ssh_keys.rb
@@ -5,6 +5,7 @@ module QA
module Profile
class SSHKeys < Page::Base
view 'app/views/profiles/keys/_form.html.haml' do
+ element :key_expiry_date_field
element :key_title_field
element :key_public_key_field
element :add_key_button
@@ -19,17 +20,26 @@ module QA
end
def add_key(public_key, title)
- fill_element :key_public_key_field, public_key
- fill_element :key_title_field, title
+ fill_element(:key_public_key_field, public_key)
+ fill_element(:key_title_field, title)
+ # Expire in 2 days just in case the key is created just before midnight
+ fill_expiry_date(Date.today + 2)
- click_element :add_key_button
+ click_element(:add_key_button)
+ end
+
+ def fill_expiry_date(date)
+ date = date.strftime('%m/%d/%Y') if date.is_a?(Date)
+ Date.strptime(date, '%m/%d/%Y') rescue ArgumentError raise "Expiry date must be in mm/dd/yyyy format"
+
+ fill_element(:key_expiry_date_field, date)
end
def remove_key(title)
click_link(title)
accept_alert do
- click_element :delete_key_button
+ click_element(:delete_key_button)
end
end
diff --git a/qa/qa/page/project/issue/index.rb b/qa/qa/page/project/issue/index.rb
index b5ad63ab8de..ace2537fc0e 100644
--- a/qa/qa/page/project/issue/index.rb
+++ b/qa/qa/page/project/issue/index.rb
@@ -9,6 +9,15 @@ module QA
element :assignee_link
end
+ view 'app/views/projects/issues/export_csv/_button.html.haml' do
+ element :export_as_csv_button
+ end
+
+ view 'app/views/projects/issues/export_csv/_modal.html.haml' do
+ element :export_issues_button
+ element :export_issues_modal
+ end
+
view 'app/views/projects/issues/_issue.html.haml' do
element :issue
element :issue_link, 'link_to issue.title' # rubocop:disable QA/ElementWithPattern
@@ -34,6 +43,18 @@ module QA
click_element :closed_issues_link
end
+ def click_export_as_csv_button
+ click_element(:export_as_csv_button)
+ end
+
+ def click_export_issues_button
+ click_element(:export_issues_button)
+ end
+
+ def export_issues_modal
+ find_element(:export_issues_modal)
+ end
+
def has_assignee_link_count?(count)
all_elements(:assignee_link, count: count)
end
diff --git a/qa/qa/page/project/issue/show.rb b/qa/qa/page/project/issue/show.rb
index 8365ecb6348..dd74ff28763 100644
--- a/qa/qa/page/project/issue/show.rb
+++ b/qa/qa/page/project/issue/show.rb
@@ -7,6 +7,7 @@ module QA
class Show < Page::Base
include Page::Component::Issuable::Common
include Page::Component::Note
+ include Page::Component::DesignManagement
view 'app/assets/javascripts/notes/components/comment_form.vue' do
element :comment_button
@@ -56,6 +57,23 @@ module QA
element :new_note_form, 'attr: :note' # rubocop:disable QA/ElementWithPattern
end
+ view 'app/views/projects/issues/_tabs.html.haml' do
+ element :discussion_tab_link
+ element :discussion_tab_content
+ element :designs_tab_link
+ element :designs_tab_content
+ end
+
+ def click_discussion_tab
+ click_element(:discussion_tab_link)
+ active_element?(:discussion_tab_content)
+ end
+
+ def click_designs_tab
+ click_element(:designs_tab_link)
+ active_element?(:designs_tab_content)
+ end
+
def click_milestone_link
click_element(:milestone_link)
end
diff --git a/qa/qa/page/project/job/show.rb b/qa/qa/page/project/job/show.rb
index 26db2f20c1b..971b8c5e5f8 100644
--- a/qa/qa/page/project/job/show.rb
+++ b/qa/qa/page/project/job/show.rb
@@ -1,51 +1,55 @@
# frozen_string_literal: true
-module QA::Page
- module Project::Job
- class Show < QA::Page::Base
- include Component::CiBadgeLink
+module QA
+ module Page
+ module Project
+ module Job
+ class Show < QA::Page::Base
+ include Component::CiBadgeLink
- view 'app/assets/javascripts/jobs/components/log/log.vue' do
- element :job_log_content
- end
+ view 'app/assets/javascripts/jobs/components/log/log.vue' do
+ element :job_log_content
+ end
- view 'app/assets/javascripts/jobs/components/stages_dropdown.vue' do
- element :pipeline_path
- end
+ view 'app/assets/javascripts/jobs/components/stages_dropdown.vue' do
+ element :pipeline_path
+ end
- view 'app/assets/javascripts/jobs/components/sidebar.vue' do
- element :retry_button
- end
+ view 'app/assets/javascripts/jobs/components/sidebar.vue' do
+ element :retry_button
+ end
- def successful?(timeout: 60)
- raise "Timed out waiting for the build trace to load" unless loaded?
- raise "Timed out waiting for the status to be a valid completed state" unless completed?(timeout: timeout)
+ def successful?(timeout: 60)
+ raise "Timed out waiting for the build trace to load" unless loaded?
+ raise "Timed out waiting for the status to be a valid completed state" unless completed?(timeout: timeout)
- passed?
- end
+ passed?
+ end
- # Reminder: You may wish to wait for a particular job status before checking output
- def output(wait: 5)
- result = ''
+ # Reminder: You may wish to wait for a particular job status before checking output
+ def output(wait: 5)
+ result = ''
- wait_until(reload: false, max_duration: wait, sleep_interval: 1) do
- result = find_element(:job_log_content).text
+ wait_until(reload: false, max_duration: wait, sleep_interval: 1) do
+ result = find_element(:job_log_content).text
- result.include?('Job')
- end
+ result.include?('Job')
+ end
- result
- end
+ result
+ end
- def retry!
- click_element :retry_button
- end
+ def retry!
+ click_element :retry_button
+ end
- private
+ private
- def loaded?(wait: 60)
- wait_until(reload: true, max_duration: wait, sleep_interval: 1) do
- has_element?(:job_log_content, wait: 1)
+ def loaded?(wait: 60)
+ wait_until(reload: true, max_duration: wait, sleep_interval: 1) do
+ has_element?(:job_log_content, wait: 1)
+ end
+ end
end
end
end
diff --git a/qa/qa/page/project/new.rb b/qa/qa/page/project/new.rb
index 97214e22820..f6c015f64ea 100644
--- a/qa/qa/page/project/new.rb
+++ b/qa/qa/page/project/new.rb
@@ -5,6 +5,7 @@ module QA
module Project
class New < Page::Base
include Page::Component::Select2
+ include Page::Component::Project::Templates
view 'app/views/projects/new.html.haml' do
element :project_create_from_template_tab
@@ -26,6 +27,11 @@ module QA
element :import_github, "icon('github', text: 'GitHub')" # rubocop:disable QA/ElementWithPattern
end
+ view 'app/views/projects/project_templates/_built_in_templates.html.haml' do
+ element :use_template_button
+ element :template_option_row
+ end
+
def choose_test_namespace
choose_namespace(Runtime::Namespace.path)
end
diff --git a/qa/qa/page/project/operations/kubernetes/index.rb b/qa/qa/page/project/operations/kubernetes/index.rb
index 84b58e9ea5b..0c92f9a9f28 100644
--- a/qa/qa/page/project/operations/kubernetes/index.rb
+++ b/qa/qa/page/project/operations/kubernetes/index.rb
@@ -17,6 +17,10 @@ module QA
def has_cluster?(cluster)
has_element?(:cluster, cluster_name: cluster.to_s)
end
+
+ def click_on_cluster(cluster)
+ click_on cluster.cluster_name
+ end
end
end
end
diff --git a/qa/qa/page/project/operations/metrics/show.rb b/qa/qa/page/project/operations/metrics/show.rb
index 020a3a1d5f8..2228cca1d3d 100644
--- a/qa/qa/page/project/operations/metrics/show.rb
+++ b/qa/qa/page/project/operations/metrics/show.rb
@@ -14,17 +14,22 @@ module QA
element :dashboards_filter_dropdown
element :environments_dropdown
element :edit_dashboard_button
- element :show_last_dropdown
+ element :range_picker_dropdown
end
view 'app/assets/javascripts/monitoring/components/duplicate_dashboard_form.vue' do
element :duplicate_dashboard_filename_field
end
- view 'app/assets/javascripts/monitoring/components/panel_type.vue' do
+ view 'app/assets/javascripts/monitoring/components/dashboard_panel.vue' do
element :prometheus_graph_widgets
element :prometheus_widgets_dropdown
element :alert_widget_menu_item
+ element :generate_chart_link_menu_item
+ end
+
+ view 'app/assets/javascripts/vue_shared/components/date_time_picker/date_time_picker.vue' do
+ element :quick_range_item
end
def wait_for_metrics
@@ -66,9 +71,18 @@ module QA
end
def show_last(range = '8 hours')
- click_element :show_last_dropdown
- within_element :show_last_dropdown do
- click_on range
+ all_elements(:range_picker_dropdown, minimum: 1).first.click
+ click_element :quick_range_item, text: range
+ end
+
+ def copy_link_to_first_chart
+ all_elements(:prometheus_widgets_dropdown, minimum: 1).first.click
+ find_element(:generate_chart_link_menu_item)['data-clipboard-text']
+ end
+
+ def has_custom_metric?(metric)
+ within_element :prometheus_graphs do
+ has_text?(metric)
end
end
diff --git a/qa/qa/page/project/pipeline/index.rb b/qa/qa/page/project/pipeline/index.rb
index 327eedeaf91..54e4d0fb2fc 100644
--- a/qa/qa/page/project/pipeline/index.rb
+++ b/qa/qa/page/project/pipeline/index.rb
@@ -1,41 +1,45 @@
# frozen_string_literal: true
-module QA::Page
- module Project::Pipeline
- class Index < QA::Page::Base
- view 'app/assets/javascripts/pipelines/components/pipeline_url.vue' do
- element :pipeline_url_link
- end
-
- view 'app/assets/javascripts/pipelines/components/pipelines_table_row.vue' do
- element :pipeline_commit_status
- element :pipeline_retry_button
- end
-
- def click_on_latest_pipeline
- all_elements(:pipeline_url_link, minimum: 1, wait: QA::Support::Repeater::DEFAULT_MAX_WAIT_TIME).first.click
- end
-
- def wait_for_latest_pipeline_success
- wait_for_latest_pipeline_status { has_text?('passed') }
- end
-
- def wait_for_latest_pipeline_completion
- wait_for_latest_pipeline_status { has_text?('passed') || has_text?('failed') }
- end
-
- def wait_for_latest_pipeline_status
- wait_until(reload: false, max_duration: 360) do
- within_element_by_index(:pipeline_commit_status, 0) { yield }
- end
- end
-
- def wait_for_latest_pipeline_success_or_retry
- wait_for_latest_pipeline_completion
-
- if has_text?('failed')
- click_element :pipeline_retry_button
- wait_for_latest_pipeline_success
+module QA
+ module Page
+ module Project
+ module Pipeline
+ class Index < QA::Page::Base
+ view 'app/assets/javascripts/pipelines/components/pipeline_url.vue' do
+ element :pipeline_url_link
+ end
+
+ view 'app/assets/javascripts/pipelines/components/pipelines_table_row.vue' do
+ element :pipeline_commit_status
+ element :pipeline_retry_button
+ end
+
+ def click_on_latest_pipeline
+ all_elements(:pipeline_url_link, minimum: 1, wait: QA::Support::Repeater::DEFAULT_MAX_WAIT_TIME).first.click
+ end
+
+ def wait_for_latest_pipeline_success
+ wait_for_latest_pipeline_status { has_text?('passed') }
+ end
+
+ def wait_for_latest_pipeline_completion
+ wait_for_latest_pipeline_status { has_text?('passed') || has_text?('failed') }
+ end
+
+ def wait_for_latest_pipeline_status
+ wait_until(reload: false, max_duration: 360) do
+ within_element_by_index(:pipeline_commit_status, 0) { yield }
+ end
+ end
+
+ def wait_for_latest_pipeline_success_or_retry
+ wait_for_latest_pipeline_completion
+
+ if has_text?('failed')
+ click_element :pipeline_retry_button
+ wait_for_latest_pipeline_success
+ end
+ end
end
end
end
diff --git a/qa/qa/page/project/pipeline/show.rb b/qa/qa/page/project/pipeline/show.rb
index 1003b828a32..d22dfefc096 100644
--- a/qa/qa/page/project/pipeline/show.rb
+++ b/qa/qa/page/project/pipeline/show.rb
@@ -1,73 +1,77 @@
# frozen_string_literal: true
-module QA::Page
- module Project::Pipeline
- class Show < QA::Page::Base
- include Component::CiBadgeLink
-
- view 'app/assets/javascripts/vue_shared/components/header_ci_component.vue' do
- element :pipeline_header, /header class.*ci-header-container.*/ # rubocop:disable QA/ElementWithPattern
- end
+module QA
+ module Page
+ module Project
+ module Pipeline
+ class Show < QA::Page::Base
+ include Component::CiBadgeLink
+
+ view 'app/assets/javascripts/vue_shared/components/header_ci_component.vue' do
+ element :pipeline_header, /header class.*ci-header-container.*/ # rubocop:disable QA/ElementWithPattern
+ end
- view 'app/assets/javascripts/pipelines/components/graph/graph_component.vue' do
- element :pipeline_graph, /class.*pipeline-graph.*/ # rubocop:disable QA/ElementWithPattern
- end
+ view 'app/assets/javascripts/pipelines/components/graph/graph_component.vue' do
+ element :pipeline_graph, /class.*pipeline-graph.*/ # rubocop:disable QA/ElementWithPattern
+ end
- view 'app/assets/javascripts/pipelines/components/graph/job_item.vue' do
- element :job_component, /class.*ci-job-component.*/ # rubocop:disable QA/ElementWithPattern
- element :job_link
- end
+ view 'app/assets/javascripts/pipelines/components/graph/job_item.vue' do
+ element :job_component, /class.*ci-job-component.*/ # rubocop:disable QA/ElementWithPattern
+ element :job_link
+ end
- view 'app/assets/javascripts/pipelines/components/graph/linked_pipeline.vue' do
- element :linked_pipeline_button
- end
+ view 'app/assets/javascripts/pipelines/components/graph/linked_pipeline.vue' do
+ element :linked_pipeline_button
+ end
- view 'app/assets/javascripts/vue_shared/components/ci_icon.vue' do
- element :status_icon, 'ci-status-icon-${status}' # rubocop:disable QA/ElementWithPattern
- end
+ view 'app/assets/javascripts/vue_shared/components/ci_icon.vue' do
+ element :status_icon, 'ci-status-icon-${status}' # rubocop:disable QA/ElementWithPattern
+ end
- view 'app/views/projects/pipelines/_info.html.haml' do
- element :pipeline_badges
- end
+ view 'app/views/projects/pipelines/_info.html.haml' do
+ element :pipeline_badges
+ end
- def running?(wait: 0)
- within('.ci-header-container') do
- page.has_content?('running', wait: wait)
- end
- end
+ def running?(wait: 0)
+ within('.ci-header-container') do
+ page.has_content?('running', wait: wait)
+ end
+ end
- def has_build?(name, status: :success, wait: nil)
- within('.pipeline-graph') do
- within('.ci-job-component', text: name) do
- has_selector?(".ci-status-icon-#{status}", { wait: wait }.compact)
+ def has_build?(name, status: :success, wait: nil)
+ within('.pipeline-graph') do
+ within('.ci-job-component', text: name) do
+ has_selector?(".ci-status-icon-#{status}", { wait: wait }.compact)
+ end
+ end
end
- end
- end
- def has_job?(job_name)
- has_element?(:job_link, text: job_name)
- end
+ def has_job?(job_name)
+ has_element?(:job_link, text: job_name)
+ end
- def has_no_job?(job_name)
- has_no_element?(:job_link, text: job_name)
- end
+ def has_no_job?(job_name)
+ has_no_element?(:job_link, text: job_name)
+ end
- def has_tag?(tag_name)
- within_element(:pipeline_badges) do
- has_selector?('.badge', text: tag_name)
- end
- end
+ def has_tag?(tag_name)
+ within_element(:pipeline_badges) do
+ has_selector?('.badge', text: tag_name)
+ end
+ end
- def click_job(job_name)
- click_element(:job_link, text: job_name)
- end
+ def click_job(job_name)
+ click_element(:job_link, text: job_name)
+ end
- def click_linked_job(project_name)
- click_element(:linked_pipeline_button, text: /#{project_name}/)
- end
+ def click_linked_job(project_name)
+ click_element(:linked_pipeline_button, text: /#{project_name}/)
+ end
- def click_on_first_job
- first('.js-pipeline-graph-job-link', wait: QA::Support::Repeater::DEFAULT_MAX_WAIT_TIME).click
+ def click_on_first_job
+ first('.js-pipeline-graph-job-link', wait: QA::Support::Repeater::DEFAULT_MAX_WAIT_TIME).click
+ end
+ end
end
end
end
diff --git a/qa/qa/page/project/settings/advanced.rb b/qa/qa/page/project/settings/advanced.rb
index c95c47fa560..3bb5181a31c 100644
--- a/qa/qa/page/project/settings/advanced.rb
+++ b/qa/qa/page/project/settings/advanced.rb
@@ -57,6 +57,10 @@ module QA
click_element :download_export_link
end
+ def has_download_export_link?
+ has_element? :download_export_link
+ end
+
def archive_project
page.accept_alert("Are you sure that you want to archive this project?") do
click_element :archive_project_link
diff --git a/qa/qa/page/project/settings/ci_cd.rb b/qa/qa/page/project/settings/ci_cd.rb
index 46f93fad61e..aa27c030b78 100644
--- a/qa/qa/page/project/settings/ci_cd.rb
+++ b/qa/qa/page/project/settings/ci_cd.rb
@@ -5,12 +5,19 @@ module QA
module Project
module Settings
class CICD < Page::Base
- include Common
+ include QA::Page::Settings::Common
view 'app/views/projects/settings/ci_cd/show.html.haml' do
element :autodevops_settings_content
element :runners_settings_content
element :variables_settings_content
+ element :general_pipelines_settings_content
+ end
+
+ def expand_general_pipelines(&block)
+ expand_section(:general_pipelines_settings_content) do
+ Settings::GeneralPipelines.perform(&block)
+ end
end
def expand_runners_settings(&block)
diff --git a/qa/qa/page/project/settings/ci_variables.rb b/qa/qa/page/project/settings/ci_variables.rb
index 6cdf40cd1da..de268b14aa2 100644
--- a/qa/qa/page/project/settings/ci_variables.rb
+++ b/qa/qa/page/project/settings/ci_variables.rb
@@ -5,7 +5,7 @@ module QA
module Project
module Settings
class CiVariables < Page::Base
- include Common
+ include QA::Page::Settings::Common
view 'app/assets/javascripts/ci_variable_list/components/ci_variable_modal.vue' do
element :ci_variable_key_field
diff --git a/qa/qa/page/project/settings/common.rb b/qa/qa/page/project/settings/common.rb
deleted file mode 100644
index f5f22623060..00000000000
--- a/qa/qa/page/project/settings/common.rb
+++ /dev/null
@@ -1,13 +0,0 @@
-# frozen_string_literal: true
-
-module QA
- module Page
- module Project
- module Settings
- module Common
- include QA::Page::Settings::Common
- end
- end
- end
- end
-end
diff --git a/qa/qa/page/project/settings/deploy_keys.rb b/qa/qa/page/project/settings/deploy_keys.rb
index c330d090ce6..8d655b0684e 100644
--- a/qa/qa/page/project/settings/deploy_keys.rb
+++ b/qa/qa/page/project/settings/deploy_keys.rb
@@ -5,7 +5,7 @@ module QA
module Project
module Settings
class DeployKeys < Page::Base
- view 'app/views/projects/deploy_keys/_form.html.haml' do
+ view 'app/views/shared/deploy_keys/_form.html.haml' do
element :deploy_key_title, 'text_field :title' # rubocop:disable QA/ElementWithPattern
element :deploy_key_key, 'text_area :key' # rubocop:disable QA/ElementWithPattern
end
diff --git a/qa/qa/page/project/settings/general_pipelines.rb b/qa/qa/page/project/settings/general_pipelines.rb
new file mode 100644
index 00000000000..5a98849a41d
--- /dev/null
+++ b/qa/qa/page/project/settings/general_pipelines.rb
@@ -0,0 +1,23 @@
+# frozen_string_literal: true
+
+module QA
+ module Page
+ module Project
+ module Settings
+ class GeneralPipelines < Page::Base
+ include QA::Page::Settings::Common
+
+ view 'app/views/projects/settings/ci_cd/_form.html.haml' do
+ element :build_coverage_regex_field
+ element :save_general_pipelines_changes_button
+ end
+
+ def configure_coverage_regex(pattern)
+ fill_element :build_coverage_regex_field, pattern
+ click_element :save_general_pipelines_changes_button
+ end
+ end
+ end
+ end
+ end
+end
diff --git a/qa/qa/page/project/settings/incidents.rb b/qa/qa/page/project/settings/incidents.rb
new file mode 100644
index 00000000000..94d5fc369ad
--- /dev/null
+++ b/qa/qa/page/project/settings/incidents.rb
@@ -0,0 +1,37 @@
+# frozen_string_literal: true
+
+module QA
+ module Page
+ module Project
+ module Settings
+ class Incidents < Page::Base
+ view 'app/views/projects/settings/operations/_incidents.html.haml' do
+ element :create_issue_checkbox
+ element :incident_templates_dropdown
+ element :save_changes_button
+ end
+
+ def enable_issues_for_incidents
+ check_element :create_issue_checkbox
+ end
+
+ def select_issue_template(template)
+ within_element :incident_templates_dropdown do
+ find(:option, template).select_option
+ end
+ end
+
+ def save_incident_settings
+ click_element :save_changes_button
+ end
+
+ def has_template?(template)
+ within_element :incident_templates_dropdown do
+ has_text?(template)
+ end
+ end
+ end
+ end
+ end
+ end
+end
diff --git a/qa/qa/page/project/settings/integrations.rb b/qa/qa/page/project/settings/integrations.rb
new file mode 100644
index 00000000000..436a42fb093
--- /dev/null
+++ b/qa/qa/page/project/settings/integrations.rb
@@ -0,0 +1,19 @@
+# frozen_string_literal: true
+
+module QA
+ module Page
+ module Project
+ module Settings
+ class Integrations < QA::Page::Base
+ view 'app/views/shared/integrations/_index.html.haml' do
+ element :prometheus_link, '{ data: { qa_selector: "#{integration.to_param' # rubocop:disable QA/ElementWithPattern
+ end
+
+ def click_on_prometheus_integration
+ click_element :prometheus_link
+ end
+ end
+ end
+ end
+ end
+end
diff --git a/qa/qa/page/project/settings/main.rb b/qa/qa/page/project/settings/main.rb
index 18d55598d90..efae497b6ba 100644
--- a/qa/qa/page/project/settings/main.rb
+++ b/qa/qa/page/project/settings/main.rb
@@ -5,7 +5,7 @@ module QA
module Project
module Settings
class Main < Page::Base
- include Common
+ include QA::Page::Settings::Common
include Component::Select2
include SubMenus::Project
diff --git a/qa/qa/page/project/settings/merge_request.rb b/qa/qa/page/project/settings/merge_request.rb
index 7da2c9d168c..0092426b31f 100644
--- a/qa/qa/page/project/settings/merge_request.rb
+++ b/qa/qa/page/project/settings/merge_request.rb
@@ -5,7 +5,7 @@ module QA
module Project
module Settings
class MergeRequest < QA::Page::Base
- include Common
+ include QA::Page::Settings::Common
view 'app/views/projects/edit.html.haml' do
element :save_merge_request_changes
diff --git a/qa/qa/page/project/settings/operations.rb b/qa/qa/page/project/settings/operations.rb
new file mode 100644
index 00000000000..f6e005d3189
--- /dev/null
+++ b/qa/qa/page/project/settings/operations.rb
@@ -0,0 +1,23 @@
+# frozen_string_literal: true
+
+module QA
+ module Page
+ module Project
+ module Settings
+ class Operations < Page::Base
+ include QA::Page::Settings::Common
+
+ view 'app/views/projects/settings/operations/_incidents.html.haml' do
+ element :incidents_settings_content
+ end
+
+ def expand_incidents(&block)
+ expand_section(:incidents_settings_content) do
+ Settings::Incidents.perform(&block)
+ end
+ end
+ end
+ end
+ end
+ end
+end
diff --git a/qa/qa/page/project/settings/repository.rb b/qa/qa/page/project/settings/repository.rb
index 8810b971fda..8e9a24a4741 100644
--- a/qa/qa/page/project/settings/repository.rb
+++ b/qa/qa/page/project/settings/repository.rb
@@ -5,7 +5,7 @@ module QA
module Project
module Settings
class Repository < Page::Base
- include Common
+ include QA::Page::Settings::Common
view 'app/views/projects/protected_branches/shared/_index.html.haml' do
element :protected_branches_settings
@@ -19,7 +19,7 @@ module QA
element :deploy_tokens_settings
end
- view 'app/views/projects/deploy_keys/_index.html.haml' do
+ view 'app/views/shared/deploy_keys/_index.html.haml' do
element :deploy_keys_settings
end
diff --git a/qa/qa/page/project/settings/services/prometheus.rb b/qa/qa/page/project/settings/services/prometheus.rb
new file mode 100644
index 00000000000..8ae4ded535e
--- /dev/null
+++ b/qa/qa/page/project/settings/services/prometheus.rb
@@ -0,0 +1,36 @@
+# frozen_string_literal: true
+
+module QA
+ module Page
+ module Project
+ module Settings
+ module Services
+ class Prometheus < Page::Base
+ include Page::Component::CustomMetric
+
+ view 'app/views/projects/services/prometheus/_custom_metrics.html.haml' do
+ element :custom_metrics_container
+ element :new_metric_button
+ end
+
+ def click_on_custom_metric(custom_metric)
+ within_element :custom_metrics_container do
+ click_on custom_metric
+ end
+ end
+
+ def click_on_new_metric
+ click_element :new_metric_button
+ end
+
+ def has_custom_metric?(custom_metric)
+ within_element :custom_metrics_container do
+ has_text? custom_metric
+ end
+ end
+ end
+ end
+ end
+ end
+ end
+end
diff --git a/qa/qa/page/project/sub_menus/ci_cd.rb b/qa/qa/page/project/sub_menus/ci_cd.rb
index 2f0bc8b9ba6..9405ea97fff 100644
--- a/qa/qa/page/project/sub_menus/ci_cd.rb
+++ b/qa/qa/page/project/sub_menus/ci_cd.rb
@@ -5,10 +5,14 @@ module QA
module Project
module SubMenus
module CiCd
- include Page::Project::SubMenus::Common
+ extend QA::Page::PageConcern
def self.included(base)
+ super
+
base.class_eval do
+ include QA::Page::Project::SubMenus::Common
+
view 'app/views/layouts/nav/sidebar/_project.html.haml' do
element :link_pipelines
end
diff --git a/qa/qa/page/project/sub_menus/common.rb b/qa/qa/page/project/sub_menus/common.rb
index da759398cff..85bf932be4a 100644
--- a/qa/qa/page/project/sub_menus/common.rb
+++ b/qa/qa/page/project/sub_menus/common.rb
@@ -5,6 +5,7 @@ module QA
module Project
module SubMenus
module Common
+ extend QA::Page::PageConcern
include QA::Page::SubMenus::Common
private
diff --git a/qa/qa/page/project/sub_menus/issues.rb b/qa/qa/page/project/sub_menus/issues.rb
index d27a250a300..c15a8ec4cc7 100644
--- a/qa/qa/page/project/sub_menus/issues.rb
+++ b/qa/qa/page/project/sub_menus/issues.rb
@@ -5,10 +5,14 @@ module QA
module Project
module SubMenus
module Issues
- include Page::Project::SubMenus::Common
+ extend QA::Page::PageConcern
def self.included(base)
+ super
+
base.class_eval do
+ include QA::Page::Project::SubMenus::Common
+
view 'app/views/layouts/nav/sidebar/_project.html.haml' do
element :issue_boards_link
element :issues_item
diff --git a/qa/qa/page/project/sub_menus/operations.rb b/qa/qa/page/project/sub_menus/operations.rb
index bcbc1dc16d3..ff9c8a21174 100644
--- a/qa/qa/page/project/sub_menus/operations.rb
+++ b/qa/qa/page/project/sub_menus/operations.rb
@@ -5,12 +5,16 @@ module QA
module Project
module SubMenus
module Operations
- include Page::Project::SubMenus::Common
+ extend QA::Page::PageConcern
def self.included(base)
+ super
+
base.class_eval do
+ include QA::Page::Project::SubMenus::Common
+
view 'app/views/layouts/nav/sidebar/_project.html.haml' do
- element :link_operations
+ element :operations_link
element :operations_environments_link
element :operations_metrics_link
end
@@ -45,8 +49,8 @@ module QA
def hover_operations
within_sidebar do
- scroll_to_element(:link_operations)
- find_element(:link_operations).hover
+ scroll_to_element(:operations_link)
+ find_element(:operations_link).hover
yield
end
diff --git a/qa/qa/page/project/sub_menus/project.rb b/qa/qa/page/project/sub_menus/project.rb
index 6f1bc131f84..4af640301b9 100644
--- a/qa/qa/page/project/sub_menus/project.rb
+++ b/qa/qa/page/project/sub_menus/project.rb
@@ -5,10 +5,14 @@ module QA
module Project
module SubMenus
module Project
- include Common
+ extend QA::Page::PageConcern
def self.included(base)
+ super
+
base.class_eval do
+ include QA::Page::Project::SubMenus::Common
+
view 'app/views/layouts/nav/sidebar/_project.html.haml' do
element :project_link
end
diff --git a/qa/qa/page/project/sub_menus/repository.rb b/qa/qa/page/project/sub_menus/repository.rb
index 65149e631f3..38d6b8e50f4 100644
--- a/qa/qa/page/project/sub_menus/repository.rb
+++ b/qa/qa/page/project/sub_menus/repository.rb
@@ -5,10 +5,14 @@ module QA
module Project
module SubMenus
module Repository
- include Page::Project::SubMenus::Common
+ extend QA::Page::PageConcern
def self.included(base)
+ super
+
base.class_eval do
+ include QA::Page::Project::SubMenus::Common
+
view 'app/views/layouts/nav/sidebar/_project.html.haml' do
element :project_menu_repo
element :branches_link
@@ -44,5 +48,3 @@ module QA
end
end
end
-
-QA::Page::Project::SubMenus::Repository.prepend_if_ee('QA::EE::Page::Project::SubMenus::Repository')
diff --git a/qa/qa/page/project/sub_menus/settings.rb b/qa/qa/page/project/sub_menus/settings.rb
index 8be442ba35d..0dd4bd1817a 100644
--- a/qa/qa/page/project/sub_menus/settings.rb
+++ b/qa/qa/page/project/sub_menus/settings.rb
@@ -5,15 +5,20 @@ module QA
module Project
module SubMenus
module Settings
- include Page::Project::SubMenus::Common
+ extend QA::Page::PageConcern
def self.included(base)
+ super
+
base.class_eval do
+ include QA::Page::Project::SubMenus::Common
+
view 'app/views/layouts/nav/sidebar/_project.html.haml' do
element :settings_item
element :link_members_settings
element :general_settings_link
element :integrations_settings_link
+ element :operations_settings_link
end
end
end
@@ -64,6 +69,14 @@ module QA
end
end
+ def go_to_operations_settings
+ hover_settings do
+ within_submenu do
+ click_element :operations_settings_link
+ end
+ end
+ end
+
private
def hover_settings
diff --git a/qa/qa/page/search/results.rb b/qa/qa/page/search/results.rb
index 85f1d224935..55477db8804 100644
--- a/qa/qa/page/search/results.rb
+++ b/qa/qa/page/search/results.rb
@@ -1,53 +1,55 @@
# frozen_string_literal: true
-module QA::Page
- module Search
- class Results < QA::Page::Base
- view 'app/views/search/_category.html.haml' do
- element :code_tab
- element :projects_tab
- end
+module QA
+ module Page
+ module Search
+ class Results < QA::Page::Base
+ view 'app/views/search/_category.html.haml' do
+ element :code_tab
+ element :projects_tab
+ end
- view 'app/views/search/results/_blob_data.html.haml' do
- element :result_item_content
- element :file_title_content
- element :file_text_content
- end
+ view 'app/views/search/results/_blob_data.html.haml' do
+ element :result_item_content
+ element :file_title_content
+ element :file_text_content
+ end
- view 'app/views/shared/projects/_project.html.haml' do
- element :project
- end
+ view 'app/views/shared/projects/_project.html.haml' do
+ element :project
+ end
- def switch_to_code
- switch_to_tab(:code_tab)
- end
+ def switch_to_code
+ switch_to_tab(:code_tab)
+ end
- def switch_to_projects
- switch_to_tab(:projects_tab)
- end
+ def switch_to_projects
+ switch_to_tab(:projects_tab)
+ end
- def has_file_in_project?(file_name, project_name)
- has_element?(:result_item_content, text: "#{project_name}: #{file_name}")
- end
+ def has_file_in_project?(file_name, project_name)
+ has_element?(:result_item_content, text: "#{project_name}: #{file_name}")
+ end
- def has_file_with_content?(file_name, file_text)
- within_element_by_index(:result_item_content, 0) do
- break false unless has_element?(:file_title_content, text: file_name)
+ def has_file_with_content?(file_name, file_text)
+ within_element_by_index(:result_item_content, 0) do
+ break false unless has_element?(:file_title_content, text: file_name)
- has_element?(:file_text_content, text: file_text)
+ has_element?(:file_text_content, text: file_text)
+ end
end
- end
- def has_project?(project_name)
- has_element?(:project, project_name: project_name)
- end
+ def has_project?(project_name)
+ has_element?(:project, project_name: project_name)
+ end
- private
+ private
- def switch_to_tab(tab)
- retry_until do
- click_element(tab)
- has_active_element?(tab)
+ def switch_to_tab(tab)
+ retry_until do
+ click_element(tab)
+ has_active_element?(tab)
+ end
end
end
end
diff --git a/qa/qa/resource/api_fabricator.rb b/qa/qa/resource/api_fabricator.rb
index cac58c599ea..591aa449219 100644
--- a/qa/qa/resource/api_fabricator.rb
+++ b/qa/qa/resource/api_fabricator.rb
@@ -14,6 +14,7 @@ module QA
ResourceQueryError = Class.new(RuntimeError)
ResourceUpdateFailedError = Class.new(RuntimeError)
ResourceURLMissingError = Class.new(RuntimeError)
+ InternalServerError = Class.new(RuntimeError)
attr_reader :api_resource, :api_response
attr_writer :api_client
@@ -54,11 +55,23 @@ module QA
end
end
- private
-
include Support::Api
attr_writer :api_resource, :api_response
+ def api_put(body = api_put_body)
+ response = put(
+ Runtime::API::Request.new(api_client, api_put_path).url,
+ body)
+
+ unless response.code == HTTP_STATUS_OK
+ raise ResourceFabricationFailedError, "Updating #{self.class.name} using the API failed (#{response.code}) with `#{response}`."
+ end
+
+ process_api_response(parse_body(response))
+ end
+
+ private
+
def resource_web_url(resource)
resource.fetch(:web_url) do
raise ResourceURLMissingError, "API resource for #{self.class.name} does not expose a `web_url` property: `#{resource}`."
@@ -73,7 +86,9 @@ module QA
url = Runtime::API::Request.new(api_client, get_path).url
response = get(url)
- unless response.code == HTTP_STATUS_OK
+ if response.code == HTTP_STATUS_SERVER_ERROR
+ raise InternalServerError, "Failed to GET #{url} - (#{response.code}): `#{response}`."
+ elsif response.code != HTTP_STATUS_OK
raise ResourceNotFoundError, "Resource at #{url} could not be found (#{response.code}): `#{response}`."
end
@@ -92,18 +107,6 @@ module QA
process_api_response(parse_body(response))
end
- def api_put
- response = put(
- Runtime::API::Request.new(api_client, api_put_path).url,
- api_put_body)
-
- unless response.code == HTTP_STATUS_OK
- raise ResourceFabricationFailedError, "Updating #{self.class.name} using the API failed (#{response.code}) with `#{response}`."
- end
-
- process_api_response(parse_body(response))
- end
-
def api_delete
url = Runtime::API::Request.new(api_client, api_delete_path).url
response = delete(url)
diff --git a/qa/qa/resource/kubernetes_cluster.rb b/qa/qa/resource/kubernetes_cluster.rb
deleted file mode 100644
index 7306acfe2a4..00000000000
--- a/qa/qa/resource/kubernetes_cluster.rb
+++ /dev/null
@@ -1,68 +0,0 @@
-# frozen_string_literal: true
-
-require 'securerandom'
-
-module QA
- module Resource
- class KubernetesCluster < Base
- attr_writer :project, :cluster,
- :install_helm_tiller, :install_ingress, :install_prometheus, :install_runner, :domain
-
- attribute :ingress_ip do
- Page::Project::Operations::Kubernetes::Show.perform(&:ingress_ip)
- end
-
- def fabricate!
- @project.visit!
-
- Page::Project::Menu.perform(
- &:go_to_operations_kubernetes)
-
- Page::Project::Operations::Kubernetes::Index.perform(
- &:add_kubernetes_cluster)
-
- Page::Project::Operations::Kubernetes::Add.perform(
- &:add_existing_cluster)
-
- Page::Project::Operations::Kubernetes::AddExisting.perform do |cluster_page|
- cluster_page.set_cluster_name(@cluster.cluster_name)
- cluster_page.set_api_url(@cluster.api_url)
- cluster_page.set_ca_certificate(@cluster.ca_certificate)
- cluster_page.set_token(@cluster.token)
- cluster_page.uncheck_rbac! unless @cluster.rbac
- cluster_page.add_cluster!
- end
-
- if @install_helm_tiller
- Page::Project::Operations::Kubernetes::Show.perform do |show|
- # We must wait a few seconds for permissions to be set up correctly for new cluster
- sleep 10
-
- # Open applications tab
- show.open_applications
-
- # Helm must be installed before everything else
- show.install!(:helm)
- show.await_installed(:helm)
-
- show.install!(:ingress) if @install_ingress
- show.install!(:prometheus) if @install_prometheus
- show.install!(:runner) if @install_runner
-
- show.await_installed(:ingress) if @install_ingress
- show.await_installed(:prometheus) if @install_prometheus
- show.await_installed(:runner) if @install_runner
-
- if @install_ingress
- populate(:ingress_ip)
-
- show.open_details
- show.set_domain("#{ingress_ip}.nip.io")
- show.save_domain
- end
- end
- end
- end
- end
- end
-end
diff --git a/qa/qa/resource/kubernetes_cluster/base.rb b/qa/qa/resource/kubernetes_cluster/base.rb
new file mode 100644
index 00000000000..38bca48be17
--- /dev/null
+++ b/qa/qa/resource/kubernetes_cluster/base.rb
@@ -0,0 +1,40 @@
+# frozen_string_literal: true
+
+require 'securerandom'
+
+module QA
+ module Resource
+ module KubernetesCluster
+ class Base < Resource::Base
+ attr_writer :add_name_uuid
+
+ attribute :id
+ attribute :name
+ attribute :domain
+ attribute :enabled
+ attribute :managed
+ attribute :management_project_id
+
+ attribute :api_url
+ attribute :token
+ attribute :ca_cert
+ attribute :namespace
+
+ attribute :authorization_type
+ attribute :environment_scope
+
+ def initialize
+ @add_name_uuid = true
+ @enabled = true
+ @managed = true
+ @authorization_type = :rbac
+ @environment_scope = :*
+ end
+
+ def name=(new_name)
+ @name = @add_name_uuid ? "#{new_name}-#{SecureRandom.hex(5)}" : new_name
+ end
+ end
+ end
+ end
+end
diff --git a/qa/qa/resource/kubernetes_cluster/project_cluster.rb b/qa/qa/resource/kubernetes_cluster/project_cluster.rb
new file mode 100644
index 00000000000..5c61cc29236
--- /dev/null
+++ b/qa/qa/resource/kubernetes_cluster/project_cluster.rb
@@ -0,0 +1,72 @@
+# frozen_string_literal: true
+
+module QA
+ module Resource
+ module KubernetesCluster
+ class ProjectCluster < Base
+ attr_writer :cluster,
+ :install_helm_tiller, :install_ingress, :install_prometheus, :install_runner, :domain
+
+ attribute :project do
+ Resource::Project.fabricate!
+ end
+
+ attribute :ingress_ip do
+ Page::Project::Operations::Kubernetes::Show.perform(&:ingress_ip)
+ end
+
+ def fabricate!
+ project.visit!
+
+ Page::Project::Menu.perform(
+ &:go_to_operations_kubernetes)
+
+ Page::Project::Operations::Kubernetes::Index.perform(
+ &:add_kubernetes_cluster)
+
+ Page::Project::Operations::Kubernetes::Add.perform(
+ &:add_existing_cluster)
+
+ Page::Project::Operations::Kubernetes::AddExisting.perform do |cluster_page|
+ cluster_page.set_cluster_name(@cluster.cluster_name)
+ cluster_page.set_api_url(@cluster.api_url)
+ cluster_page.set_ca_certificate(@cluster.ca_certificate)
+ cluster_page.set_token(@cluster.token)
+ cluster_page.uncheck_rbac! unless @cluster.rbac
+ cluster_page.add_cluster!
+ end
+
+ if @install_helm_tiller
+ Page::Project::Operations::Kubernetes::Show.perform do |show|
+ # We must wait a few seconds for permissions to be set up correctly for new cluster
+ sleep 10
+
+ # Open applications tab
+ show.open_applications
+
+ # Helm must be installed before everything else
+ show.install!(:helm)
+ show.await_installed(:helm)
+
+ show.install!(:ingress) if @install_ingress
+ show.install!(:prometheus) if @install_prometheus
+ show.install!(:runner) if @install_runner
+
+ show.await_installed(:ingress) if @install_ingress
+ show.await_installed(:prometheus) if @install_prometheus
+ show.await_installed(:runner) if @install_runner
+
+ if @install_ingress
+ populate(:ingress_ip)
+
+ show.open_details
+ show.set_domain("#{ingress_ip}.nip.io")
+ show.save_domain
+ end
+ end
+ end
+ end
+ end
+ end
+ end
+end
diff --git a/qa/qa/resource/pipeline.rb b/qa/qa/resource/pipeline.rb
new file mode 100644
index 00000000000..a115de3e825
--- /dev/null
+++ b/qa/qa/resource/pipeline.rb
@@ -0,0 +1,53 @@
+# frozen_string_literal: true
+
+module QA
+ module Resource
+ class Pipeline < Base
+ attribute :project do
+ Resource::Project.fabricate! do |project|
+ project.name = 'project-with-pipeline'
+ end
+ end
+
+ attribute :id
+ attribute :status
+ attribute :ref
+ attribute :sha
+
+ # array in form
+ # [
+ # { key: 'UPLOAD_TO_S3', variable_type: 'file', value: true },
+ # { key: 'SOMETHING', variable_type: 'env_var', value: 'yes' }
+ # ]
+ attribute :variables
+
+ def initialize
+ @ref = 'master'
+ @variables = []
+ end
+
+ def fabricate!
+ project.visit!
+
+ Page::Project::Menu.perform(&:click_ci_cd_pipelines)
+ Page::Project::Pipeline::Index.perform(&:click_run_pipeline_button)
+ Page::Project::Pipeline::New.perform(&:click_run_pipeline_button)
+ end
+
+ def api_get_path
+ "/projects/#{project.id}/pipelines/#{id}"
+ end
+
+ def api_post_path
+ "/projects/#{project.id}/pipeline"
+ end
+
+ def api_post_body
+ {
+ ref: ref,
+ variables: variables
+ }
+ end
+ end
+ end
+end
diff --git a/qa/qa/resource/project.rb b/qa/qa/resource/project.rb
index cb047f81d02..78e2ba8a248 100644
--- a/qa/qa/resource/project.rb
+++ b/qa/qa/resource/project.rb
@@ -7,11 +7,11 @@ module QA
class Project < Base
include Events::Project
include Members
+ include Visibility
attr_accessor :repository_storage # requires admin access
attr_writer :initialize_with_readme
attr_writer :auto_devops_enabled
- attr_writer :visibility
attribute :id
attribute :name
@@ -19,6 +19,8 @@ module QA
attribute :description
attribute :standalone
attribute :runners_token
+ attribute :visibility
+ attribute :template_name
attribute :group do
Group.fabricate!
@@ -50,7 +52,8 @@ module QA
@description = 'My awesome project'
@initialize_with_readme = false
@auto_devops_enabled = false
- @visibility = 'public'
+ @visibility = :public
+ @template_name = nil
end
def name=(raw_name)
@@ -63,6 +66,13 @@ module QA
Page::Group::Show.perform(&:go_to_new_project)
end
+ if @template_name
+ Page::Project::New.perform do |new_page|
+ new_page.click_create_from_template_tab
+ new_page.use_template_for_project(@template_name)
+ end
+ end
+
Page::Project::New.perform do |new_page|
new_page.choose_test_namespace
new_page.choose_name(@name)
@@ -83,6 +93,10 @@ module QA
"/projects/#{CGI.escape(path_with_namespace)}"
end
+ def api_visibility_path
+ "/projects/#{id}"
+ end
+
def api_get_archive_path(type = 'tar.gz')
"#{api_get_path}/repository/archive.#{type}"
end
@@ -95,6 +109,10 @@ module QA
"#{api_get_path}/runners"
end
+ def api_pipelines_path
+ "#{api_get_path}/pipelines"
+ end
+
def api_put_path
"/projects/#{id}"
end
@@ -118,6 +136,7 @@ module QA
end
post_body[:repository_storage] = repository_storage if repository_storage
+ post_body[:template_name] = @template_name if @template_name
post_body
end
@@ -152,10 +171,19 @@ module QA
end
def runners(tag_list: nil)
- response = get Runtime::API::Request.new(api_client, "#{api_runners_path}?tag_list=#{tag_list.compact.join(',')}").url
+ response = if tag_list
+ get Runtime::API::Request.new(api_client, "#{api_runners_path}?tag_list=#{tag_list.compact.join(',')}").url
+ else
+ get Runtime::API::Request.new(api_client, "#{api_runners_path}").url
+ end
+
parse_body(response)
end
+ def pipelines
+ parse_body(get(Runtime::API::Request.new(api_client, api_pipelines_path).url))
+ end
+
def share_with_group(invitee, access_level = Resource::Members::AccessLevel::DEVELOPER)
post Runtime::API::Request.new(api_client, "/projects/#{id}/share").url, { group_id: invitee.id, group_access: access_level }
end
diff --git a/qa/qa/resource/runner.rb b/qa/qa/resource/runner.rb
index f1f72c9cacd..b2a36f92ffe 100644
--- a/qa/qa/resource/runner.rb
+++ b/qa/qa/resource/runner.rb
@@ -5,8 +5,8 @@ require 'securerandom'
module QA
module Resource
class Runner < Base
- attr_writer :name, :tags, :image
- attr_accessor :config, :token
+ attr_writer :name, :tags, :image, :executor, :executor_image
+ attr_accessor :config, :token, :run_untagged
attribute :id
attribute :project do
@@ -20,35 +20,42 @@ module QA
@name || "qa-runner-#{SecureRandom.hex(4)}"
end
- def tags
- @tags || %w[qa e2e]
- end
-
def image
@image || 'gitlab/gitlab-runner:alpine'
end
+ def executor
+ @executor || :shell
+ end
+
+ def executor_image
+ @executor_image || 'registry.gitlab.com/gitlab-org/gitlab-build-images:gitlab-qa-alpine-ruby-2.6'
+ end
+
def fabricate_via_api!
Service::DockerRun::GitlabRunner.new(name).tap do |runner|
runner.pull
runner.token = @token ||= project.runners_token
runner.address = Runtime::Scenario.gitlab_address
- runner.tags = tags
+ runner.tags = @tags if @tags
runner.image = image
runner.config = config if config
+ runner.executor = executor
+ runner.executor_image = executor_image if executor == :docker
+ runner.run_untagged = run_untagged if run_untagged
runner.register!
end
end
def remove_via_api!
- runners = project.runners(tag_list: tags)
+ runners = project.runners(tag_list: @tags)
unless runners && !runners.empty?
- raise "Project #{project.path_with_namespace} has no runners with tags #{tags}."
+ raise "Project #{project.path_with_namespace} has no runners#{" with tags #{@tags}." if @tags&.any?}"
end
this_runner = runners.find { |runner| runner[:description] == name }
unless this_runner
- raise "Project #{project.path_with_namespace} does not have a runner with a description matching #{name} and tags #{tags}. Runners available: #{runners}"
+ raise "Project #{project.path_with_namespace} does not have a runner with a description matching #{name} #{"or tags #{@tags}" if @tags&.any?}. Runners available: #{runners}"
end
@id = this_runner[:id]
diff --git a/qa/qa/resource/ssh_key.rb b/qa/qa/resource/ssh_key.rb
index 3e130aef9e4..b948bf3969b 100644
--- a/qa/qa/resource/ssh_key.rb
+++ b/qa/qa/resource/ssh_key.rb
@@ -5,12 +5,16 @@ module QA
class SSHKey < Base
extend Forwardable
- attr_accessor :title
+ attr_reader :title
attribute :id
def_delegators :key, :private_key, :public_key, :md5_fingerprint
+ def initialize
+ self.title = Time.now.to_f
+ end
+
def key
@key ||= Runtime::Key::RSA.new
end
@@ -28,6 +32,10 @@ module QA
api_post
end
+ def title=(title)
+ @title = "E2E test key: #{title}"
+ end
+
def api_delete
QA::Runtime::Logger.debug("Deleting SSH key with title '#{title}' and fingerprint '#{md5_fingerprint}'")
diff --git a/qa/qa/resource/visibility.rb b/qa/qa/resource/visibility.rb
new file mode 100644
index 00000000000..b31bd3fca49
--- /dev/null
+++ b/qa/qa/resource/visibility.rb
@@ -0,0 +1,17 @@
+# frozen_string_literal: true
+
+module QA
+ module Resource
+ module Visibility
+ def set_visibility(visibility)
+ put Runtime::API::Request.new(api_client, api_visibility_path).url, { visibility: visibility }
+ end
+
+ class VisibilityLevel
+ %i(public internal private).each do |level|
+ const_set(level.upcase, level)
+ end
+ end
+ end
+ end
+end
diff --git a/qa/qa/runtime/api/client.rb b/qa/qa/runtime/api/client.rb
index b9a3c9184aa..d29571df981 100644
--- a/qa/qa/runtime/api/client.rb
+++ b/qa/qa/runtime/api/client.rb
@@ -1,7 +1,5 @@
# frozen_string_literal: true
-require 'airborne'
-
module QA
module Runtime
module API
diff --git a/qa/qa/runtime/project.rb b/qa/qa/runtime/project.rb
deleted file mode 100644
index 89edfee1fbe..00000000000
--- a/qa/qa/runtime/project.rb
+++ /dev/null
@@ -1,35 +0,0 @@
-# frozen_string_literal: true
-
-module QA
- module Runtime
- module Project
- extend self
- extend Support::Api
-
- def create_project(project_name, api_client, project_description = 'default')
- project = Resource::Project.fabricate_via_api! do |project|
- project.add_name_uuid = false
- project.name = project_name
- project.description = project_description
- project.api_client = api_client
- project.visibility = 'public'
- end
- project
- end
-
- def push_file_to_project(target_project, file_name, file_content)
- Resource::Repository::ProjectPush.fabricate! do |push|
- push.project = target_project
- push.file_name = file_name
- push.file_content = file_content
- end
- end
-
- def set_project_visibility(api_client, project_id, visibility)
- request = Runtime::API::Request.new(api_client, "/projects/#{project_id}")
- response = put request.url, visibility: visibility
- response.code.equal?(QA::Support::Api::HTTP_STATUS_OK)
- end
- end
- end
-end
diff --git a/qa/qa/scenario/test/integration/gitaly_ha.rb b/qa/qa/scenario/test/integration/gitaly_ha.rb
new file mode 100644
index 00000000000..dbca1a1dd6d
--- /dev/null
+++ b/qa/qa/scenario/test/integration/gitaly_ha.rb
@@ -0,0 +1,13 @@
+# frozen_string_literal: true
+
+module QA
+ module Scenario
+ module Test
+ module Integration
+ class GitalyHA < Test::Instance::All
+ tags :gitaly_ha
+ end
+ end
+ end
+ end
+end
diff --git a/qa/qa/service/docker_run/gitlab_runner.rb b/qa/qa/service/docker_run/gitlab_runner.rb
index 6856a5a8399..834f6b430ac 100644
--- a/qa/qa/service/docker_run/gitlab_runner.rb
+++ b/qa/qa/service/docker_run/gitlab_runner.rb
@@ -6,14 +6,21 @@ module QA
module Service
module DockerRun
class GitlabRunner < Base
- attr_accessor :token, :address, :tags, :image, :run_untagged
- attr_writer :config
+ attr_reader :tags
+ attr_accessor :token, :address, :image, :run_untagged
+ attr_writer :config, :executor, :executor_image
+
+ CONFLICTING_VARIABLES_MESSAGE = <<~MSG
+ There are conflicting options preventing the runner from starting.
+ %s cannot be specified if %s is %s
+ MSG
def initialize(name)
@image = 'gitlab/gitlab-runner:alpine'
@name = name || "qa-runner-#{SecureRandom.hex(4)}"
- @tags = %w[qa test]
- @run_untagged = false
+ @run_untagged = true
+ @executor = :shell
+ @executor_image = 'registry.gitlab.com/gitlab-org/gitlab-build-images:gitlab-qa-alpine-ruby-2.6'
super()
end
@@ -32,23 +39,49 @@ module QA
shell <<~CMD.tr("\n", ' ')
docker run -d --rm --entrypoint=/bin/sh
--network #{network} --name #{@name}
- -p 8093:8093
- -e CI_SERVER_URL=#{@address}
- -e REGISTER_NON_INTERACTIVE=true
- -e REGISTRATION_TOKEN=#{@token}
- -e RUNNER_EXECUTOR=shell
- -e RUNNER_TAG_LIST=#{@tags.join(',')}
- -e RUNNER_NAME=#{@name}
+ #{'-v /var/run/docker.sock:/var/run/docker.sock' if @executor == :docker}
+ --privileged
#{@image} -c "#{register_command}"
CMD
end
+ def tags=(tags)
+ @tags = tags
+ @run_untagged = false
+ end
+
private
def register_command
- <<~CMD
+ args = []
+ args << '--non-interactive'
+ args << "--name #{@name}"
+ args << "--url #{@address}"
+ args << "--registration-token #{@token}"
+
+ args << if run_untagged
+ raise CONFLICTING_VARIABLES_MESSAGE % [:tags=, :run_untagged, run_untagged] if @tags&.any?
+
+ '--run-untagged=true'
+ else
+ raise 'You must specify tags to run!' unless @tags&.any?
+
+ "--tag-list #{@tags.join(',')}"
+ end
+
+ args << "--executor #{@executor}"
+
+ if @executor == :docker
+ args << "--docker-image #{@executor_image}"
+ args << '--docker-tlsverify=false'
+ args << '--docker-privileged=true'
+ args << "--docker-network-mode=#{network}"
+ end
+
+ <<~CMD.strip
printf '#{config.chomp.gsub(/\n/, "\\n").gsub('"', '\"')}' > /etc/gitlab-runner/config.toml &&
- gitlab-runner register --run-untagged=#{@run_untagged} &&
+ gitlab-runner register \
+ #{args.join(' ')} &&
gitlab-runner run
CMD
end
diff --git a/qa/qa/service/praefect_manager.rb b/qa/qa/service/praefect_manager.rb
new file mode 100644
index 00000000000..d8fa72456ad
--- /dev/null
+++ b/qa/qa/service/praefect_manager.rb
@@ -0,0 +1,27 @@
+# frozen_string_literal: true
+
+module QA
+ module Service
+ class PraefectManager
+ include Service::Shellout
+
+ def initialize
+ @praefect = 'praefect'
+ @first_node = 'gitaly1'
+ @second_node = 'gitaly2'
+ @primary_node = @first_node
+ @secondary_node = @second_node
+ end
+
+ def stop_primary_node
+ shell "docker stop #{@primary_node}"
+ @secondary_node, @primary_node = @primary_node, @secondary_node
+ end
+
+ def reset
+ shell "docker start #{@primary_node}"
+ shell "docker start #{@secondary_node}"
+ end
+ end
+ end
+end
diff --git a/qa/qa/specs/features/api/1_manage/rate_limits_spec.rb b/qa/qa/specs/features/api/1_manage/rate_limits_spec.rb
index 819739ac535..1bf435014af 100644
--- a/qa/qa/specs/features/api/1_manage/rate_limits_spec.rb
+++ b/qa/qa/specs/features/api/1_manage/rate_limits_spec.rb
@@ -1,5 +1,7 @@
# frozen_string_literal: true
+require 'airborne'
+
module QA
context 'Manage with IP rate limits', :requires_admin do
describe 'Users API' do
diff --git a/qa/qa/specs/features/api/1_manage/users_spec.rb b/qa/qa/specs/features/api/1_manage/users_spec.rb
index ba1ba204d24..fbc26e81b69 100644
--- a/qa/qa/specs/features/api/1_manage/users_spec.rb
+++ b/qa/qa/specs/features/api/1_manage/users_spec.rb
@@ -1,5 +1,7 @@
# frozen_string_literal: true
+require 'airborne'
+
module QA
context 'Manage' do
describe 'Users API' do
diff --git a/qa/qa/specs/features/api/2_plan/closes_issue_via_pushing_a_commit_spec.rb b/qa/qa/specs/features/api/2_plan/closes_issue_via_pushing_a_commit_spec.rb
index f14fcc5afce..58d716f759e 100644
--- a/qa/qa/specs/features/api/2_plan/closes_issue_via_pushing_a_commit_spec.rb
+++ b/qa/qa/specs/features/api/2_plan/closes_issue_via_pushing_a_commit_spec.rb
@@ -1,5 +1,7 @@
# frozen_string_literal: true
+require 'airborne'
+
module QA
context 'Plan' do
include Support::Api
diff --git a/qa/qa/specs/features/api/3_create/repository/files_spec.rb b/qa/qa/specs/features/api/3_create/repository/files_spec.rb
index dc471128dae..92858ba4107 100644
--- a/qa/qa/specs/features/api/3_create/repository/files_spec.rb
+++ b/qa/qa/specs/features/api/3_create/repository/files_spec.rb
@@ -1,5 +1,6 @@
# frozen_string_literal: true
+require 'airborne'
require 'securerandom'
module QA
diff --git a/qa/qa/specs/features/api/3_create/repository/project_archive_compare_spec.rb b/qa/qa/specs/features/api/3_create/repository/project_archive_compare_spec.rb
index 5ba434a7781..3ad56e21ad4 100644
--- a/qa/qa/specs/features/api/3_create/repository/project_archive_compare_spec.rb
+++ b/qa/qa/specs/features/api/3_create/repository/project_archive_compare_spec.rb
@@ -1,5 +1,6 @@
# frozen_string_literal: true
+require 'airborne'
require 'securerandom'
require 'digest'
diff --git a/qa/qa/specs/features/browser_ui/2_plan/email/trigger_email_notification_spec.rb b/qa/qa/specs/features/browser_ui/2_plan/email/trigger_email_notification_spec.rb
index 5f7a6981f23..0a577aa07f8 100644
--- a/qa/qa/specs/features/browser_ui/2_plan/email/trigger_email_notification_spec.rb
+++ b/qa/qa/specs/features/browser_ui/2_plan/email/trigger_email_notification_spec.rb
@@ -3,6 +3,8 @@
module QA
context 'Plan', :orchestrated, :smtp do
describe 'Email Notification' do
+ include Support::Api
+
let(:user) do
Resource::User.fabricate_or_use(Runtime::Env.gitlab_qa_username_1, Runtime::Env.gitlab_qa_password_1)
end
diff --git a/qa/qa/specs/features/browser_ui/2_plan/issue/create_issue_spec.rb b/qa/qa/specs/features/browser_ui/2_plan/issue/create_issue_spec.rb
index 2cb6a76b6b3..7b4418191a3 100644
--- a/qa/qa/specs/features/browser_ui/2_plan/issue/create_issue_spec.rb
+++ b/qa/qa/specs/features/browser_ui/2_plan/issue/create_issue_spec.rb
@@ -17,7 +17,7 @@ module QA
end
end
- context 'when using attachments in comments', :object_storage, quarantine: { issue: 'https://gitlab.com/gitlab-org/gitlab/issues/205408', type: :bug } do
+ context 'when using attachments in comments', :object_storage do
let(:gif_file_name) { 'banana_sample.gif' }
let(:file_to_attach) do
File.absolute_path(File.join('spec', 'fixtures', gif_file_name))
diff --git a/qa/qa/specs/features/browser_ui/2_plan/issue_boards/focus_mode_spec.rb b/qa/qa/specs/features/browser_ui/2_plan/issue_boards/focus_mode_spec.rb
new file mode 100644
index 00000000000..409c7d321f0
--- /dev/null
+++ b/qa/qa/specs/features/browser_ui/2_plan/issue_boards/focus_mode_spec.rb
@@ -0,0 +1,28 @@
+# frozen_string_literal: true
+
+module QA
+ context 'Plan', :reliable do
+ describe 'Issue board focus mode' do
+ let(:project) do
+ QA::Resource::Project.fabricate_via_api! do |project|
+ project.name = 'sample-project-issue-board-focus-mode'
+ end
+ end
+
+ before do
+ Flow::Login.sign_in
+ end
+
+ it 'focuses on issue board' do
+ project.visit!
+
+ Page::Project::Menu.perform(&:go_to_boards)
+ EE::Page::Component::IssueBoard::Show.perform do |show|
+ show.click_focus_mode_button
+
+ expect(show.focused_board).to be_visible
+ end
+ end
+ end
+ end
+end
diff --git a/qa/qa/specs/features/browser_ui/3_create/design_management/add_design_add_annotation.rb b/qa/qa/specs/features/browser_ui/3_create/design_management/add_design_add_annotation.rb
new file mode 100644
index 00000000000..b50edcfcf08
--- /dev/null
+++ b/qa/qa/specs/features/browser_ui/3_create/design_management/add_design_add_annotation.rb
@@ -0,0 +1,31 @@
+# frozen_string_literal: true
+
+module QA
+ context 'Create' do
+ describe 'Design management' do
+ let(:issue) { Resource::Issue.fabricate_via_api! }
+ let(:design_filename) { 'banana_sample.gif' }
+ let(:design) { File.absolute_path(File.join('spec', 'fixtures', design_filename)) }
+ let(:annotation) { "This design is great!" }
+
+ before do
+ Flow::Login.sign_in
+ end
+
+ it 'user adds a design and annotation' do
+ issue.visit!
+
+ Page::Project::Issue::Show.perform do |show|
+ show.click_designs_tab
+ show.add_design(design)
+ show.click_design(design_filename)
+ show.add_annotation(annotation)
+
+ expect(show).to have_annotation(annotation)
+
+ show.click_discussion_tab
+ end
+ end
+ end
+ end
+end
diff --git a/qa/qa/specs/features/browser_ui/3_create/gitaly/high_availability_spec.rb b/qa/qa/specs/features/browser_ui/3_create/gitaly/high_availability_spec.rb
new file mode 100644
index 00000000000..3bb03f68d51
--- /dev/null
+++ b/qa/qa/specs/features/browser_ui/3_create/gitaly/high_availability_spec.rb
@@ -0,0 +1,63 @@
+# frozen_string_literal: true
+
+module QA
+ context 'Create' do
+ context 'Gitaly' do
+ describe 'High Availability', :orchestrated, :gitaly_ha do
+ let(:project) do
+ Resource::Project.fabricate! do |project|
+ project.name = 'gitaly_high_availability'
+ end
+ end
+ let(:initial_file) { 'pushed_to_primary.txt' }
+ let(:final_file) { 'pushed_to_secondary.txt' }
+
+ before do
+ @praefect_manager = Service::PraefectManager.new
+ Flow::Login.sign_in
+ end
+
+ after do
+ @praefect_manager.reset
+ end
+
+ it 'makes sure that automatic failover is happening' do
+ Resource::Repository::ProjectPush.fabricate! do |push|
+ push.project = project
+ push.commit_message = 'pushed to primary gitaly node'
+ push.new_branch = true
+ push.file_name = initial_file
+ push.file_content = "This should exist on both nodes"
+ end
+
+ @praefect_manager.stop_primary_node
+
+ project.visit!
+
+ Page::Project::Show.perform do |show|
+ show.wait_until do
+ show.has_name?(project.name)
+ end
+ expect(show).to have_file(initial_file)
+ end
+
+ Resource::Repository::Commit.fabricate_via_api! do |commit|
+ commit.project = project
+ commit.add_files([
+ {
+ file_path: 'committed_to_primary.txt',
+ content: 'This should exist on both nodes too'
+ }
+ ])
+ end
+
+ project.visit!
+
+ Page::Project::Show.perform do |show|
+ expect(show).to have_file(final_file)
+ end
+ end
+ end
+ end
+ end
+end
diff --git a/qa/qa/specs/features/browser_ui/3_create/merge_request/merge_merge_request_from_fork_spec.rb b/qa/qa/specs/features/browser_ui/3_create/merge_request/merge_merge_request_from_fork_spec.rb
index 7d4e6b7efbc..3964ae7eada 100644
--- a/qa/qa/specs/features/browser_ui/3_create/merge_request/merge_merge_request_from_fork_spec.rb
+++ b/qa/qa/specs/features/browser_ui/3_create/merge_request/merge_merge_request_from_fork_spec.rb
@@ -10,6 +10,8 @@ module QA
merge_request.fork_branch = 'feature-branch'
end
+ merge_request.project.api_put(auto_devops_enabled: false)
+
Page::Main::Menu.perform(&:sign_out)
Page::Main::Login.perform(&:sign_in_using_credentials)
diff --git a/qa/qa/specs/features/browser_ui/3_create/repository/add_ssh_key_spec.rb b/qa/qa/specs/features/browser_ui/3_create/repository/add_ssh_key_spec.rb
index 25866e12185..68bbc1719fc 100644
--- a/qa/qa/specs/features/browser_ui/3_create/repository/add_ssh_key_spec.rb
+++ b/qa/qa/specs/features/browser_ui/3_create/repository/add_ssh_key_spec.rb
@@ -12,7 +12,7 @@ module QA
resource.title = key_title
end
- expect(page).to have_content("Title: #{key_title}")
+ expect(page).to have_content(key.title)
expect(page).to have_content(key.md5_fingerprint)
Page::Main::Menu.perform(&:click_settings_link)
diff --git a/qa/qa/specs/features/browser_ui/3_create/repository/move_project_create_fork_spec.rb b/qa/qa/specs/features/browser_ui/3_create/repository/move_project_create_fork_spec.rb
index 13fe8918f97..d0123da53bb 100644
--- a/qa/qa/specs/features/browser_ui/3_create/repository/move_project_create_fork_spec.rb
+++ b/qa/qa/specs/features/browser_ui/3_create/repository/move_project_create_fork_spec.rb
@@ -1,8 +1,8 @@
# frozen_string_literal: true
module QA
- context 'Create' do
- describe 'Gitaly repository storage', :orchestrated, :repository_storage, :requires_admin, quarantine: { type: :new } do
+ context 'Create', quarantine: { issue: 'https://gitlab.com/gitlab-org/gitlab/-/issues/217002', type: :investigating } do
+ describe 'Gitaly repository storage', :orchestrated, :repository_storage, :requires_admin do
let(:user) { Resource::User.fabricate_or_use(Runtime::Env.gitlab_qa_username_1, Runtime::Env.gitlab_qa_password_1) }
let(:parent_project) do
Resource::Project.fabricate_via_api! do |project|
diff --git a/qa/qa/specs/features/browser_ui/3_create/repository/push_over_http_file_size_spec.rb b/qa/qa/specs/features/browser_ui/3_create/repository/push_over_http_file_size_spec.rb
index 9bc4dcbca2a..9b504ad76b4 100644
--- a/qa/qa/specs/features/browser_ui/3_create/repository/push_over_http_file_size_spec.rb
+++ b/qa/qa/specs/features/browser_ui/3_create/repository/push_over_http_file_size_spec.rb
@@ -3,6 +3,8 @@
module QA
context 'Create', :requires_admin do
describe 'push after setting the file size limit via admin/application_settings' do
+ include Support::Api
+
before(:context) do
@project = Resource::Project.fabricate_via_api! do |p|
p.name = 'project-test-push-limit'
@@ -39,12 +41,10 @@ module QA
def set_file_size_limit(limit)
request = Runtime::API::Request.new(@api_client, '/application/settings')
- put request.url, receive_max_input_size: limit
+ response = put request.url, receive_max_input_size: limit
- expect_status(200)
- expect(json_body).to match(
- a_hash_including(receive_max_input_size: limit)
- )
+ expect(response.code).to eq(200)
+ expect(parse_body(response)[:receive_max_input_size]).to eq(limit)
end
def push_new_file(file_name, wait_for_push: true)
diff --git a/qa/qa/specs/features/browser_ui/3_create/snippet/clone_push_pull_personal_snippet_spec.rb b/qa/qa/specs/features/browser_ui/3_create/snippet/clone_push_pull_personal_snippet_spec.rb
new file mode 100644
index 00000000000..341ff39fdf1
--- /dev/null
+++ b/qa/qa/specs/features/browser_ui/3_create/snippet/clone_push_pull_personal_snippet_spec.rb
@@ -0,0 +1,102 @@
+# frozen_string_literal: true
+
+module QA
+ context 'Create' do
+ describe 'Version control for personal snippets' do
+ let(:new_file) { 'new_snippet_file' }
+ let(:changed_content) { 'changes' }
+ let(:commit_message) { 'Changes to snippets' }
+ let(:added_content) { 'updated ' }
+ let(:branch_name) { 'master' }
+
+ let(:snippet) do
+ Resource::Snippet.fabricate! do |snippet|
+ snippet.file_name = new_file
+ end
+ end
+
+ let(:ssh_key) do
+ Resource::SSHKey.fabricate_via_api! do |resource|
+ resource.title = "my key title #{Time.now.to_f}"
+ end
+ end
+
+ let(:repository_uri_http) do
+ snippet
+ Page::Dashboard::Snippet::Show.perform(&:get_repository_uri_http)
+ end
+
+ let(:repository_uri_ssh) do
+ ssh_key
+ snippet
+ Page::Dashboard::Snippet::Show.perform(&:get_repository_uri_ssh)
+ end
+
+ before do
+ Flow::Login.sign_in
+ end
+
+ it 'clones, pushes, and pulls a snippet over HTTP, edits via UI' do
+ Resource::Repository::Push.fabricate! do |push|
+ push.repository_http_uri = repository_uri_http
+ push.file_name = new_file
+ push.file_content = changed_content
+ push.commit_message = commit_message
+ push.new_branch = false
+ end
+
+ page.refresh
+ verify_changes_in_ui
+
+ Page::Dashboard::Snippet::Show.perform(&:click_edit_button)
+
+ Page::Dashboard::Snippet::Edit.perform do |snippet|
+ snippet.add_to_file_content(added_content)
+ snippet.save_changes
+ end
+
+ Git::Repository.perform do |repository|
+ repository.init_repository
+ repository.pull(repository_uri_http, branch_name)
+
+ expect(repository.commits.size).to eq(3)
+ expect(repository.commits.first).to include('Update snippet')
+ expect(repository.file_content(new_file)).to include("#{added_content}#{changed_content}")
+ end
+ end
+
+ it 'clones, pushes, and pulls a snippet over SSH, deletes via UI' do
+ Resource::Repository::Push.fabricate! do |push|
+ push.repository_ssh_uri = repository_uri_ssh
+ push.ssh_key = ssh_key
+ push.file_name = new_file
+ push.file_content = changed_content
+ push.commit_message = commit_message
+ push.new_branch = false
+ end
+
+ page.refresh
+ verify_changes_in_ui
+
+ Page::Dashboard::Snippet::Show.perform(&:click_delete_button)
+
+ # attempt to pull a deleted snippet, get a missing repository error
+ Git::Repository.perform do |repository|
+ repository.uri = repository_uri_ssh
+ repository.use_ssh_key(ssh_key)
+ repository.init_repository
+
+ expect { repository.pull(repository_uri_ssh, branch_name) }
+ .to raise_error(QA::Git::Repository::RepositoryCommandError, /[fatal: Could not read from remote repository.]+/)
+ end
+ end
+
+ def verify_changes_in_ui
+ Page::Dashboard::Snippet::Show.perform do |snippet|
+ expect(snippet).to have_file_name(new_file)
+ expect(snippet).to have_file_content(changed_content)
+ end
+ end
+ end
+ end
+end
diff --git a/qa/qa/specs/features/browser_ui/3_create/snippet/create_snippet_spec.rb b/qa/qa/specs/features/browser_ui/3_create/snippet/create_snippet_spec.rb
index d38b8560a38..dfcbf4b44c8 100644
--- a/qa/qa/specs/features/browser_ui/3_create/snippet/create_snippet_spec.rb
+++ b/qa/qa/specs/features/browser_ui/3_create/snippet/create_snippet_spec.rb
@@ -1,7 +1,7 @@
# frozen_string_literal: true
module QA
- context 'Create', :smoke, quarantine: { issue: 'https://gitlab.com/gitlab-org/gitlab/-/issues/215031', type: :investigating } do
+ context 'Create', :smoke do
describe 'Snippet creation' do
it 'User creates a snippet' do
Flow::Login.sign_in
diff --git a/qa/qa/specs/features/browser_ui/3_create/web_ide/review_merge_request_spec.rb b/qa/qa/specs/features/browser_ui/3_create/web_ide/review_merge_request_spec.rb
index 8ea1534492c..ed988bdf046 100644
--- a/qa/qa/specs/features/browser_ui/3_create/web_ide/review_merge_request_spec.rb
+++ b/qa/qa/specs/features/browser_ui/3_create/web_ide/review_merge_request_spec.rb
@@ -1,7 +1,7 @@
# frozen_string_literal: true
module QA
- context 'Create', quarantine: { type: :new } do
+ context 'Create' do
describe 'Review a merge request in Web IDE' do
let(:new_file) { 'awesome_new_file.txt' }
let(:original_text) { 'Text' }
diff --git a/qa/qa/specs/features/browser_ui/4_verify/runner/register_runner_spec.rb b/qa/qa/specs/features/browser_ui/4_verify/runner/register_runner_spec.rb
index 2550ee7193b..f8a589ad93b 100644
--- a/qa/qa/specs/features/browser_ui/4_verify/runner/register_runner_spec.rb
+++ b/qa/qa/specs/features/browser_ui/4_verify/runner/register_runner_spec.rb
@@ -7,6 +7,7 @@ module QA
let!(:runner) do
Resource::Runner.fabricate! do |runner|
runner.name = executor
+ runner.tags = ['e2e-test']
end
end
diff --git a/qa/qa/specs/features/browser_ui/4_verify/testing/view_code_coverage_spec.rb b/qa/qa/specs/features/browser_ui/4_verify/testing/view_code_coverage_spec.rb
new file mode 100644
index 00000000000..4d549dde858
--- /dev/null
+++ b/qa/qa/specs/features/browser_ui/4_verify/testing/view_code_coverage_spec.rb
@@ -0,0 +1,61 @@
+# frozen_string_literal: true
+
+module QA
+ context 'Verify', :docker, :runner do
+ describe 'Code coverage statistics' do
+ let(:simplecov) { '\(\d+.\d+\%\) covered' }
+ let(:executor) { "qa-runner-#{Time.now.to_i}" }
+ let(:runner) do
+ Resource::Runner.fabricate_via_api! do |runner|
+ runner.name = executor
+ runner.tags = ['e2e-test']
+ end
+ end
+
+ let(:merge_request) do
+ Resource::MergeRequest.fabricate_via_api! do |mr|
+ mr.project = runner.project
+ mr.file_name = '.gitlab-ci.yml'
+ mr.file_content = <<~EOF
+ test:
+ tags: [e2e-test]
+ script:
+ - echo '(66.67%) covered'
+ EOF
+ end
+ end
+
+ before do
+ Flow::Login.sign_in
+ end
+
+ after do
+ runner.remove_via_api!
+ end
+
+ it 'creates an MR with code coverage statistics' do
+ runner.project.visit!
+ configure_code_coverage(simplecov)
+ merge_request.visit!
+
+ Page::MergeRequest::Show.perform do |mr_widget|
+ Support::Retrier.retry_until(max_attempts: 5, sleep_interval: 5) do
+ mr_widget.has_pipeline_status?(/Pipeline #\d+ passed/)
+ end
+ expect(mr_widget).to have_content('Coverage 66.67%')
+ end
+ end
+ end
+
+ private
+
+ def configure_code_coverage(coverage_tool_pattern)
+ Page::Project::Menu.perform(&:go_to_ci_cd_settings)
+ Page::Project::Settings::CICD.perform do |settings|
+ settings.expand_general_pipelines do |coverage|
+ coverage.configure_coverage_regex(coverage_tool_pattern)
+ end
+ end
+ end
+ end
+end
diff --git a/qa/qa/specs/features/browser_ui/6_release/pipeline/parent_child_pipelines_dependent_relationship_spec.rb b/qa/qa/specs/features/browser_ui/6_release/pipeline/parent_child_pipelines_dependent_relationship_spec.rb
index e71212bcb68..b1eb26f0d63 100644
--- a/qa/qa/specs/features/browser_ui/6_release/pipeline/parent_child_pipelines_dependent_relationship_spec.rb
+++ b/qa/qa/specs/features/browser_ui/6_release/pipeline/parent_child_pipelines_dependent_relationship_spec.rb
@@ -1,7 +1,7 @@
# frozen_string_literal: true
module QA
- context 'Release', :docker, quarantine: { type: :new } do
+ context 'Release', :docker, quarantine: { issue: 'https://gitlab.com/gitlab-org/gitlab/-/issues/217250', type: :investigating } do
describe 'Parent-child pipelines dependent relationship' do
let!(:project) do
Resource::Project.fabricate_via_api! do |project|
diff --git a/qa/qa/specs/features/browser_ui/6_release/pipeline/parent_child_pipelines_independent_relationship_spec.rb b/qa/qa/specs/features/browser_ui/6_release/pipeline/parent_child_pipelines_independent_relationship_spec.rb
index 633af9c2e8a..c9a61fc6305 100644
--- a/qa/qa/specs/features/browser_ui/6_release/pipeline/parent_child_pipelines_independent_relationship_spec.rb
+++ b/qa/qa/specs/features/browser_ui/6_release/pipeline/parent_child_pipelines_independent_relationship_spec.rb
@@ -1,7 +1,7 @@
# frozen_string_literal: true
module QA
- context 'Release', :docker, quarantine: { type: :new } do
+ context 'Release', :docker, quarantine: { issue: 'https://gitlab.com/gitlab-org/gitlab/-/issues/217250', type: :investigating } do
describe 'Parent-child pipelines independent relationship' do
let!(:project) do
Resource::Project.fabricate_via_api! do |project|
diff --git a/qa/qa/specs/features/browser_ui/7_configure/auto_devops/create_project_with_auto_devops_spec.rb b/qa/qa/specs/features/browser_ui/7_configure/auto_devops/create_project_with_auto_devops_spec.rb
index 0a52b01af03..292fc40bec4 100644
--- a/qa/qa/specs/features/browser_ui/7_configure/auto_devops/create_project_with_auto_devops_spec.rb
+++ b/qa/qa/specs/features/browser_ui/7_configure/auto_devops/create_project_with_auto_devops_spec.rb
@@ -35,7 +35,7 @@ module QA
end
# Connect K8s cluster
- Resource::KubernetesCluster.fabricate! do |k8s_cluster|
+ Resource::KubernetesCluster::ProjectCluster.fabricate! do |k8s_cluster|
k8s_cluster.project = project
k8s_cluster.cluster = cluster
k8s_cluster.install_helm_tiller = true
diff --git a/qa/qa/specs/features/browser_ui/7_configure/kubernetes/kubernetes_integration_spec.rb b/qa/qa/specs/features/browser_ui/7_configure/kubernetes/kubernetes_integration_spec.rb
index 9a52109c8cb..04c68598239 100644
--- a/qa/qa/specs/features/browser_ui/7_configure/kubernetes/kubernetes_integration_spec.rb
+++ b/qa/qa/specs/features/browser_ui/7_configure/kubernetes/kubernetes_integration_spec.rb
@@ -1,8 +1,8 @@
# frozen_string_literal: true
module QA
- context 'Configure' do
- describe 'Kubernetes Cluster Integration', :orchestrated, :kubernetes, :requires_admin, quarantine: { type: :new } do
+ context 'Configure', quarantine: { issue: 'https://gitlab.com/gitlab-org/gitlab/-/issues/209085', type: :investigating } do
+ describe 'Kubernetes Cluster Integration', :orchestrated, :kubernetes, :requires_admin do
context 'Project Clusters' do
let(:cluster) { Service::KubernetesCluster.new(provider_class: Service::ClusterProvider::K3s).create! }
let(:project) do
@@ -21,12 +21,10 @@ module QA
end
it 'can create and associate a project cluster', :smoke do
- Resource::KubernetesCluster.fabricate_via_browser_ui! do |k8s_cluster|
+ Resource::KubernetesCluster::ProjectCluster.fabricate_via_browser_ui! do |k8s_cluster|
k8s_cluster.project = project
k8s_cluster.cluster = cluster
- end
-
- project.visit!
+ end.project.visit!
Page::Project::Menu.perform(&:go_to_operations_kubernetes)
diff --git a/qa/qa/specs/features/browser_ui/8_monitor/all_monitor_core_features_spec.rb b/qa/qa/specs/features/browser_ui/8_monitor/all_monitor_core_features_spec.rb
new file mode 100644
index 00000000000..45273655bb6
--- /dev/null
+++ b/qa/qa/specs/features/browser_ui/8_monitor/all_monitor_core_features_spec.rb
@@ -0,0 +1,155 @@
+# frozen_string_literal: true
+
+module QA
+ context 'Monitor', quarantine: { issue: 'https://gitlab.com/gitlab-org/gitlab/-/issues/217705', type: :flaky } do
+ describe 'with Prometheus Gitlab-managed cluster', :orchestrated, :kubernetes, :docker, :runner do
+ before :all do
+ Flow::Login.sign_in
+ @project, @runner = deploy_project_with_prometheus
+ end
+
+ before do
+ Flow::Login.sign_in_unless_signed_in
+ @project.visit!
+ end
+
+ after :all do
+ @runner.remove_via_api!
+ @cluster.remove!
+ end
+
+ it 'configures custom metrics' do
+ verify_add_custom_metric
+ verify_edit_custom_metric
+ verify_delete_custom_metric
+ end
+
+ it 'duplicates to create dashboard to custom' do
+ Page::Project::Menu.perform(&:go_to_operations_metrics)
+
+ Page::Project::Operations::Metrics::Show.perform do |dashboard|
+ dashboard.duplicate_dashboard
+
+ expect(dashboard).to have_metrics
+ expect(dashboard).to have_edit_dashboard_enabled
+ end
+ end
+
+ it 'verifies data on filtered deployed environment' do
+ Page::Project::Menu.perform(&:go_to_operations_metrics)
+
+ Page::Project::Operations::Metrics::Show.perform do |dashboard|
+ dashboard.filter_environment
+
+ expect(dashboard).to have_metrics
+ end
+ end
+
+ it 'filters using the quick range' do
+ Page::Project::Menu.perform(&:go_to_operations_metrics)
+
+ Page::Project::Operations::Metrics::Show.perform do |dashboard|
+ dashboard.show_last('30 minutes')
+ expect(dashboard).to have_metrics
+
+ dashboard.show_last('3 hours')
+ expect(dashboard).to have_metrics
+
+ dashboard.show_last('1 day')
+ expect(dashboard).to have_metrics
+ end
+ end
+
+ private
+
+ def deploy_project_with_prometheus
+ project = Resource::Project.fabricate_via_api! do |project|
+ project.name = 'cluster-with-prometheus'
+ project.description = 'Cluster with Prometheus'
+ end
+
+ runner = Resource::Runner.fabricate_via_api! do |runner|
+ runner.project = project
+ runner.name = project.name
+ end
+
+ @cluster = Service::KubernetesCluster.new.create!
+
+ cluster_props = Resource::KubernetesCluster::ProjectCluster.fabricate! do |cluster_settings|
+ cluster_settings.project = project
+ cluster_settings.cluster = @cluster
+ cluster_settings.install_helm_tiller = true
+ cluster_settings.install_ingress = true
+ cluster_settings.install_prometheus = true
+ end
+
+ Resource::CiVariable.fabricate_via_api! do |ci_variable|
+ ci_variable.project = project
+ ci_variable.key = 'AUTO_DEVOPS_DOMAIN'
+ ci_variable.value = cluster_props.ingress_ip
+ ci_variable.masked = false
+ end
+
+ Resource::Repository::ProjectPush.fabricate! do |push|
+ push.project = project
+ push.directory = Pathname
+ .new(__dir__)
+ .join('../../../../fixtures/monitored_auto_devops')
+ push.commit_message = 'Create AutoDevOps compatible Project for Monitoring'
+ end
+
+ Page::Project::Menu.perform(&:click_ci_cd_pipelines)
+ Page::Project::Pipeline::Index.perform(&:wait_for_latest_pipeline_success_or_retry)
+
+ [project, runner]
+ end
+
+ def verify_add_custom_metric
+ Page::Project::Menu.perform(&:go_to_integrations_settings)
+ Page::Project::Settings::Integrations.perform(&:click_on_prometheus_integration)
+
+ Page::Project::Settings::Services::Prometheus.perform do |metrics_panel|
+ metrics_panel.click_on_new_metric
+ metrics_panel.add_custom_metric
+ end
+
+ Page::Project::Menu.perform(&:go_to_operations_metrics)
+
+ Page::Project::Operations::Metrics::Show.perform do |dashboard|
+ expect(dashboard).to have_custom_metric('HTTP Requests Total')
+ end
+ end
+
+ def verify_edit_custom_metric
+ Page::Project::Menu.perform(&:go_to_integrations_settings)
+ Page::Project::Settings::Integrations.perform(&:click_on_prometheus_integration)
+ Page::Project::Settings::Services::Prometheus.perform do |metrics_panel|
+ metrics_panel.click_on_custom_metric('Business / HTTP Requests Total (req/sec)')
+ metrics_panel.edit_custom_metric
+ end
+
+ Page::Project::Menu.perform(&:go_to_operations_metrics)
+
+ Page::Project::Operations::Metrics::Show.perform do |dashboard|
+ expect(dashboard).to have_custom_metric('Throughput')
+ end
+ end
+
+ def verify_delete_custom_metric
+ Page::Project::Menu.perform(&:go_to_integrations_settings)
+ Page::Project::Settings::Integrations.perform(&:click_on_prometheus_integration)
+
+ Page::Project::Settings::Services::Prometheus.perform do |metrics_panel|
+ metrics_panel.click_on_custom_metric('Business / Throughput (req/sec)')
+ metrics_panel.delete_custom_metric
+ end
+
+ Page::Project::Menu.perform(&:go_to_operations_metrics)
+
+ Page::Project::Operations::Metrics::Show.perform do |dashboard|
+ expect(dashboard).not_to have_custom_metric('Throughput')
+ end
+ end
+ end
+ end
+end
diff --git a/qa/qa/specs/features/browser_ui/8_monitor/apm/dashboards_spec.rb b/qa/qa/specs/features/browser_ui/8_monitor/apm/dashboards_spec.rb
deleted file mode 100644
index f7463c69db1..00000000000
--- a/qa/qa/specs/features/browser_ui/8_monitor/apm/dashboards_spec.rb
+++ /dev/null
@@ -1,97 +0,0 @@
-# frozen_string_literal: true
-
-module QA
- context 'Monitor' do
- describe 'Dashboards', :orchestrated, :kubernetes, quarantine: { type: :new } do
- before(:all) do
- @cluster = Service::KubernetesCluster.new.create!
- Flow::Login.sign_in
- create_project_to_monitor
- wait_for_deployment
- end
-
- before do
- Flow::Login.sign_in_unless_signed_in
- @project.visit!
- end
-
- after(:all) do
- @cluster&.remove!
- end
-
- it 'duplicates to create dashboard to custom' do
- Page::Project::Menu.perform(&:go_to_operations_metrics)
-
- Page::Project::Operations::Metrics::Show.perform do |dashboard|
- dashboard.duplicate_dashboard
-
- expect(dashboard).to have_metrics
- expect(dashboard).to have_edit_dashboard_enabled
- end
- end
-
- it 'verifies data on filtered deployed environment' do
- Page::Project::Menu.perform(&:go_to_operations_metrics)
-
- Page::Project::Operations::Metrics::Show.perform do |dashboard|
- dashboard.filter_environment
-
- expect(dashboard).to have_metrics
- end
- end
-
- it 'filters using the quick range' do
- Page::Project::Menu.perform(&:go_to_operations_metrics)
-
- Page::Project::Operations::Metrics::Show.perform do |dashboard|
- dashboard.show_last('30 minutes')
- expect(dashboard).to have_metrics
-
- dashboard.show_last('3 hours')
- expect(dashboard).to have_metrics
-
- dashboard.show_last('1 day')
- expect(dashboard).to have_metrics
- end
- end
-
- private
-
- def wait_for_deployment
- Page::Project::Menu.perform(&:click_ci_cd_pipelines)
- Page::Project::Pipeline::Index.perform(&:wait_for_latest_pipeline_success_or_retry)
- Page::Project::Menu.perform(&:go_to_operations_metrics)
- end
-
- def create_project_to_monitor
- @project = Resource::Project.fabricate_via_api! do |project|
- project.name = 'cluster-with-prometheus'
- project.description = 'Cluster with Prometheus'
- end
-
- @cluster_props = Resource::KubernetesCluster.fabricate_via_browser_ui! do |cluster_settings|
- cluster_settings.project = @project
- cluster_settings.cluster = @cluster
- cluster_settings.install_helm_tiller = true
- cluster_settings.install_ingress = true
- cluster_settings.install_prometheus = true
- end
-
- Resource::CiVariable.fabricate_via_api! do |ci_variable|
- ci_variable.project = @project
- ci_variable.key = 'AUTO_DEVOPS_DOMAIN'
- ci_variable.value = @cluster_props.ingress_ip
- ci_variable.masked = false
- end
-
- Resource::Repository::ProjectPush.fabricate! do |push|
- push.project = @project
- push.directory = Pathname
- .new(__dir__)
- .join('../../../../../fixtures/monitored_auto_devops')
- push.commit_message = 'Create AutoDevOps compatible Project for Monitoring'
- end
- end
- end
- end
-end
diff --git a/qa/qa/specs/features/sanity/framework_spec.rb b/qa/qa/specs/features/sanity/framework_spec.rb
index aae0f0ade71..611c6c7b1ff 100644
--- a/qa/qa/specs/features/sanity/framework_spec.rb
+++ b/qa/qa/specs/features/sanity/framework_spec.rb
@@ -6,7 +6,7 @@ module QA
it 'succeeds' do
Runtime::Browser.visit(:gitlab, Page::Main::Login)
- expect(page).to have_text('Open source software to collaborate on code')
+ expect(page).to have_text('A complete DevOps platform')
end
end
diff --git a/qa/qa/specs/helpers/quarantine.rb b/qa/qa/specs/helpers/quarantine.rb
index 8b14184f3b7..dd3a50ac128 100644
--- a/qa/qa/specs/helpers/quarantine.rb
+++ b/qa/qa/specs/helpers/quarantine.rb
@@ -2,83 +2,87 @@
require 'rspec/core'
-module QA::Specs::Helpers
- module Quarantine
- include RSpec::Core::Pending
+module QA
+ module Specs
+ module Helpers
+ module Quarantine
+ include RSpec::Core::Pending
- extend self
+ extend self
- def configure_rspec
- RSpec.configure do |config|
- config.before(:context, :quarantine) do
- Quarantine.skip_or_run_quarantined_contexts(config.inclusion_filter.rules, self.class)
- end
+ def configure_rspec
+ RSpec.configure do |config|
+ config.before(:context, :quarantine) do
+ Quarantine.skip_or_run_quarantined_contexts(config.inclusion_filter.rules, self.class)
+ end
- config.before do |example|
- Quarantine.skip_or_run_quarantined_tests_or_contexts(config.inclusion_filter.rules, example)
+ config.before do |example|
+ Quarantine.skip_or_run_quarantined_tests_or_contexts(config.inclusion_filter.rules, example)
+ end
+ end
end
- end
- end
- # Skip the entire context if a context is quarantined. This avoids running
- # before blocks unnecessarily.
- def skip_or_run_quarantined_contexts(filters, example)
- return unless example.metadata.key?(:quarantine)
+ # Skip the entire context if a context is quarantined. This avoids running
+ # before blocks unnecessarily.
+ def skip_or_run_quarantined_contexts(filters, example)
+ return unless example.metadata.key?(:quarantine)
- skip_or_run_quarantined_tests_or_contexts(filters, example)
- end
+ skip_or_run_quarantined_tests_or_contexts(filters, example)
+ end
- # Skip tests in quarantine unless we explicitly focus on them.
- def skip_or_run_quarantined_tests_or_contexts(filters, example)
- if filters.key?(:quarantine)
- included_filters = filters_other_than_quarantine(filters)
+ # Skip tests in quarantine unless we explicitly focus on them.
+ def skip_or_run_quarantined_tests_or_contexts(filters, example)
+ if filters.key?(:quarantine)
+ included_filters = filters_other_than_quarantine(filters)
- # If :quarantine is focused, skip the test/context unless its metadata
- # includes quarantine and any other filters
- # E.g., Suppose a test is tagged :smoke and :quarantine, and another is tagged
- # :ldap and :quarantine. If we wanted to run just quarantined smoke tests
- # using `--tag quarantine --tag smoke`, without this check we'd end up
- # running that ldap test as well because of the :quarantine metadata.
- # We could use an exclusion filter, but this way the test report will list
- # the quarantined tests when they're not run so that we're aware of them
- skip("Only running tests tagged with :quarantine and any of #{included_filters.keys}") if should_skip_when_focused?(example.metadata, included_filters)
- else
- if example.metadata.key?(:quarantine)
- quarantine_message = %w(In quarantine)
- quarantine_tag = example.metadata[:quarantine]
+ # If :quarantine is focused, skip the test/context unless its metadata
+ # includes quarantine and any other filters
+ # E.g., Suppose a test is tagged :smoke and :quarantine, and another is tagged
+ # :ldap and :quarantine. If we wanted to run just quarantined smoke tests
+ # using `--tag quarantine --tag smoke`, without this check we'd end up
+ # running that ldap test as well because of the :quarantine metadata.
+ # We could use an exclusion filter, but this way the test report will list
+ # the quarantined tests when they're not run so that we're aware of them
+ skip("Only running tests tagged with :quarantine and any of #{included_filters.keys}") if should_skip_when_focused?(example.metadata, included_filters)
+ else
+ if example.metadata.key?(:quarantine)
+ quarantine_message = %w(In quarantine)
+ quarantine_tag = example.metadata[:quarantine]
- if !!quarantine_tag
- quarantine_message << case quarantine_tag
- when String
- ": #{quarantine_tag}"
- when Hash
- ": #{quarantine_tag[:issue]}"
- else
- ''
- end
- end
+ if !!quarantine_tag
+ quarantine_message << case quarantine_tag
+ when String
+ ": #{quarantine_tag}"
+ when Hash
+ ": #{quarantine_tag[:issue]}"
+ else
+ ''
+ end
+ end
- skip(quarantine_message.join(' ').strip)
+ skip(quarantine_message.join(' ').strip)
+ end
+ end
end
- end
- end
- def filters_other_than_quarantine(filter)
- filter.reject { |key, _| key == :quarantine }
- end
+ def filters_other_than_quarantine(filter)
+ filter.reject { |key, _| key == :quarantine }
+ end
- # Checks if a test or context should be skipped.
- #
- # Returns true if
- # - the metadata does not includes the :quarantine tag
- # or if
- # - the metadata includes the :quarantine tag
- # - and the filter includes other tags that aren't in the metadata
- def should_skip_when_focused?(metadata, included_filters)
- return true unless metadata.key?(:quarantine)
- return false if included_filters.empty?
+ # Checks if a test or context should be skipped.
+ #
+ # Returns true if
+ # - the metadata does not includes the :quarantine tag
+ # or if
+ # - the metadata includes the :quarantine tag
+ # - and the filter includes other tags that aren't in the metadata
+ def should_skip_when_focused?(metadata, included_filters)
+ return true unless metadata.key?(:quarantine)
+ return false if included_filters.empty?
- (metadata.keys & included_filters.keys).empty?
+ (metadata.keys & included_filters.keys).empty?
+ end
+ end
end
end
end
diff --git a/qa/qa/specs/runner.rb b/qa/qa/specs/runner.rb
index ac73cc00dbf..afeddeaa5d5 100644
--- a/qa/qa/specs/runner.rb
+++ b/qa/qa/specs/runner.rb
@@ -42,6 +42,8 @@ module QA
tags_for_rspec.push(%w[--tag ~skip_signup_disabled]) if QA::Runtime::Env.signup_disabled?
+ tags_for_rspec.push(%w[--tag ~skip_live_env]) if QA::Runtime::Env.dot_com?
+
QA::Runtime::Env.supported_features.each_key do |key|
tags_for_rspec.push(%W[--tag ~requires_#{key}]) unless QA::Runtime::Env.can_test? key
end
diff --git a/qa/qa/support/api.rb b/qa/qa/support/api.rb
index 90924ffd40e..f5e4d4e294b 100644
--- a/qa/qa/support/api.rb
+++ b/qa/qa/support/api.rb
@@ -1,5 +1,7 @@
# frozen_string_literal: true
+require 'rest-client'
+
module QA
module Support
module Api
@@ -7,6 +9,7 @@ module QA
HTTP_STATUS_CREATED = 201
HTTP_STATUS_NO_CONTENT = 204
HTTP_STATUS_ACCEPTED = 202
+ HTTP_STATUS_SERVER_ERROR = 500
def post(url, payload)
RestClient::Request.execute(
diff --git a/qa/qa/tools/delete_test_ssh_keys.rb b/qa/qa/tools/delete_test_ssh_keys.rb
new file mode 100644
index 00000000000..953e9fc63d1
--- /dev/null
+++ b/qa/qa/tools/delete_test_ssh_keys.rb
@@ -0,0 +1,80 @@
+# frozen_string_literal: true
+
+require_relative '../../qa'
+
+# This script deletes all selected test ssh keys for a specific user
+# Keys can be selected by a string matching part of the key's title and by created date
+# - Specify `title_portion` to delete only keys that include the string provided
+# - Specify `delete_before` to delete only keys that were created before the given date
+#
+# If `dry_run` is true the script will list the keys by title and indicate whether each will be deleted
+#
+# Required environment variables: GITLAB_QA_ACCESS_TOKEN and GITLAB_ADDRESS
+# - GITLAB_QA_ACCESS_TOKEN should have API access and belong to the user whose keys will be deleted
+
+module QA
+ module Tools
+ class DeleteTestSSHKeys
+ include Support::Api
+
+ ITEMS_PER_PAGE = '100'
+
+ def initialize(title_portion: 'E2E test key:', delete_before: Date.today.to_s, dry_run: false)
+ raise ArgumentError, "Please provide GITLAB_ADDRESS" unless ENV['GITLAB_ADDRESS']
+ raise ArgumentError, "Please provide GITLAB_QA_ACCESS_TOKEN" unless ENV['GITLAB_QA_ACCESS_TOKEN']
+
+ @api_client = Runtime::API::Client.new(ENV['GITLAB_ADDRESS'], personal_access_token: ENV['GITLAB_QA_ACCESS_TOKEN'])
+ @title_portion = title_portion
+ @delete_before = Date.parse(delete_before)
+ @dry_run = dry_run
+ end
+
+ def run
+ STDOUT.puts 'Running...'
+
+ keys_head_response = head Runtime::API::Request.new(@api_client, "/user/keys", per_page: ITEMS_PER_PAGE).url
+ total_pages = keys_head_response.headers[:x_total_pages]
+
+ test_ssh_key_ids = fetch_test_ssh_key_ids(total_pages)
+ STDOUT.puts "Number of test ssh keys to be deleted: #{test_ssh_key_ids.length}"
+
+ return if dry_run?
+
+ delete_ssh_keys(test_ssh_key_ids) unless test_ssh_key_ids.empty?
+ STDOUT.puts "\nDone"
+ end
+
+ private
+
+ attr_reader :dry_run
+ alias_method :dry_run?, :dry_run
+
+ def delete_ssh_keys(ssh_key_ids)
+ STDOUT.puts "Deleting #{ssh_key_ids.length} ssh keys..."
+ ssh_key_ids.each do |key_id|
+ delete_response = delete Runtime::API::Request.new(@api_client, "/user/keys/#{key_id}").url
+ dot_or_f = delete_response.code == 204 ? "\e[32m.\e[0m" : "\e[31mF\e[0m"
+ print dot_or_f
+ end
+ end
+
+ def fetch_test_ssh_key_ids(pages)
+ key_ids = []
+
+ pages.to_i.times do |page_no|
+ get_keys_response = get Runtime::API::Request.new(@api_client, "/user/keys", page: (page_no + 1).to_s, per_page: ITEMS_PER_PAGE).url
+ keys = JSON.parse(get_keys_response.body).select do |key|
+ to_delete = key['title'].include?(@title_portion) && Date.parse(key['created_at']) < @delete_before
+
+ puts "Key title: #{key['title']}\tcreated_at: #{key['created_at']}\tdelete? #{to_delete}" if dry_run?
+
+ to_delete
+ end
+ key_ids.concat(keys.map { |key| key['id'] })
+ end
+
+ key_ids.uniq
+ end
+ end
+ end
+end
diff --git a/qa/qa/vendor/jenkins/page/configure_job.rb b/qa/qa/vendor/jenkins/page/configure_job.rb
index 56a2602a003..471567ec828 100644
--- a/qa/qa/vendor/jenkins/page/configure_job.rb
+++ b/qa/qa/vendor/jenkins/page/configure_job.rb
@@ -9,16 +9,19 @@ module QA
class ConfigureJob < Page::Base
attr_accessor :job_name
- def initialize
- @path = "/job/#{@job_name}/configure"
+ def path
+ "/job/#{@job_name}/configure"
end
def configure(scm_url:)
set_git_source_code_management_url(scm_url)
click_build_when_change_is_pushed_to_gitlab
set_publish_status_to_gitlab
- click_save
- wait_for_configuration_to_save
+
+ Support::Retrier.retry_until(sleep_interval: 0.5) do
+ click_save
+ wait_for_configuration_to_save
+ end
end
private
@@ -58,8 +61,8 @@ module QA
end
def wait_for_configuration_to_save
- QA::Support::Waiter.wait_until(sleep_interval: 1.0) do
- !page.current_url.include?(@path)
+ QA::Support::Waiter.wait_until(max_duration: 10, raise_on_failure: false) do
+ !page.current_url.include?(path)
end
end
end
diff --git a/qa/qa/vendor/jenkins/page/last_job_console.rb b/qa/qa/vendor/jenkins/page/last_job_console.rb
index f41b91c2cdb..9fcbb8ab956 100644
--- a/qa/qa/vendor/jenkins/page/last_job_console.rb
+++ b/qa/qa/vendor/jenkins/page/last_job_console.rb
@@ -9,6 +9,8 @@ module QA
class LastJobConsole < Page::Base
attr_accessor :job_name
+ CONSOLE_OUTPUT_SELECTOR = '.console-output'
+
def path
"/job/#{@job_name}/lastBuild/console"
end
@@ -17,13 +19,23 @@ module QA
# Retry on errors such as:
# Selenium::WebDriver::Error::JavascriptError:
# javascript error: this.each is not a function
- Support::Retrier.retry_on_exception(reload_page: page) do
- page.has_text?('Finished: SUCCESS')
+ Support::Retrier.retry_on_exception(reload_page: page, sleep_interval: 1) do
+ has_console_output? && console_output.include?('Finished: SUCCESS')
end
end
def no_failed_status_update?
- page.has_no_text?('Failed to update Gitlab commit status')
+ !console_output.include?('Failed to update Gitlab commit status')
+ end
+
+ private
+
+ def has_console_output?
+ page.has_selector?(CONSOLE_OUTPUT_SELECTOR, wait: 1)
+ end
+
+ def console_output
+ page.find(CONSOLE_OUTPUT_SELECTOR).text
end
end
end
diff --git a/qa/spec/factory/resource/user_spec.rb b/qa/spec/factory/resource/user_spec.rb
index 820c506b715..d59ee24c758 100644
--- a/qa/spec/factory/resource/user_spec.rb
+++ b/qa/spec/factory/resource/user_spec.rb
@@ -2,7 +2,7 @@
describe QA::Resource::User do
describe "#fabricate_via_api!" do
- Response = Struct.new(:code, :body)
+ response = Struct.new(:code, :body)
it 'fetches an existing user' do
existing_users = [
@@ -13,8 +13,8 @@ describe QA::Resource::User do
web_url: ''
}
]
- users_response = Response.new('200', JSON.dump(existing_users))
- single_user_response = Response.new('200', JSON.dump(existing_users.first))
+ users_response = response.new('200', JSON.dump(existing_users))
+ single_user_response = response.new('200', JSON.dump(existing_users.first))
expect(subject).to receive(:api_get_from).with("/users?username=name").and_return(users_response)
expect(subject).to receive(:api_get_from).with("/users/0").and_return(single_user_response)
@@ -26,7 +26,7 @@ describe QA::Resource::User do
end
it 'tries to create a user if it does not exist' do
- expect(subject).to receive(:api_get_from).with("/users?username=foo").and_return(Response.new('200', '[]'))
+ expect(subject).to receive(:api_get_from).with("/users?username=foo").and_return(response.new('200', '[]'))
expect(subject).to receive(:api_post).and_return({ web_url: '' })
subject.username = 'foo'
diff --git a/qa/spec/git/repository_spec.rb b/qa/spec/git/repository_spec.rb
index 6cca9f55e11..8355c77f493 100644
--- a/qa/spec/git/repository_spec.rb
+++ b/qa/spec/git/repository_spec.rb
@@ -3,55 +3,119 @@
describe QA::Git::Repository do
include Helpers::StubENV
- shared_context 'git directory' do
- let(:repository) { described_class.new }
+ shared_context 'unresolvable git directory' do
+ let(:repo_uri) { 'http://foo/bar.git' }
+ let(:repo_uri_with_credentials) { 'http://root@foo/bar.git' }
+ let(:repository) { described_class.new.tap { |r| r.uri = repo_uri } }
let(:tmp_git_dir) { Dir.mktmpdir }
let(:tmp_netrc_dir) { Dir.mktmpdir }
before do
stub_env('GITLAB_USERNAME', 'root')
- cd_empty_temp_directory
- set_bad_uri
-
allow(repository).to receive(:tmp_home_dir).and_return(tmp_netrc_dir)
end
+ around do |example|
+ FileUtils.cd(tmp_git_dir) do
+ example.run
+ end
+ end
+
after do
- # Switch to a safe dir before deleting tmp dirs to avoid dir access errors
- FileUtils.cd __dir__
FileUtils.remove_entry_secure(tmp_git_dir, true)
FileUtils.remove_entry_secure(tmp_netrc_dir, true)
end
+ end
- def cd_empty_temp_directory
- FileUtils.cd tmp_git_dir
+ shared_examples 'command with retries' do
+ let(:extra_args) { {} }
+ let(:result_output) { +'Command successful' }
+ let(:result) { described_class::Result.new(any_args, 0, result_output) }
+ let(:command_return) { result_output }
+
+ context 'when command is successful' do
+ it 'returns the #run command Result output' do
+ expect(repository).to receive(:run).with(command, extra_args.merge(max_attempts: 3)).and_return(result)
+
+ expect(call_method).to eq(command_return)
+ end
end
- def set_bad_uri
- repository.uri = 'http://foo/bar.git'
+ context 'when command is not successful the first time' do
+ context 'and retried command is successful' do
+ it 'retries the command twice and returns the successful #run command Result output' do
+ expect(Open3).to receive(:capture2e).and_return([+'', double(exitstatus: 1)]).twice
+ expect(Open3).to receive(:capture2e).and_return([result_output, double(exitstatus: 0)])
+
+ expect(call_method).to eq(command_return)
+ end
+ end
+
+ context 'and retried command is not successful after 3 attempts' do
+ it 'raises a RepositoryCommandError exception' do
+ expect(Open3).to receive(:capture2e).and_return([+'FAILURE', double(exitstatus: 42)]).exactly(3).times
+
+ expect { call_method }.to raise_error(described_class::RepositoryCommandError, /The command .* failed \(42\) with the following output:\nFAILURE/)
+ end
+ end
end
end
context 'with default credentials' do
- include_context 'git directory' do
+ include_context 'unresolvable git directory' do
before do
repository.use_default_credentials
end
end
describe '#clone' do
- it 'is unable to resolve host' do
- expect { repository.clone }.to raise_error(described_class::RepositoryCommandError, /The command .* failed \(128\) with the following output/)
+ let(:opts) { '' }
+ let(:call_method) { repository.clone }
+ let(:command) { "git clone #{opts} #{repo_uri_with_credentials} ./" }
+
+ context 'when no opts is given' do
+ it_behaves_like 'command with retries'
+ end
+
+ context 'when opts is given' do
+ let(:opts) { '--depth 1' }
+
+ it_behaves_like 'command with retries' do
+ let(:call_method) { repository.clone(opts) }
+ end
+ end
+ end
+
+ describe '#shallow_clone' do
+ it_behaves_like 'command with retries' do
+ let(:call_method) { repository.shallow_clone }
+ let(:command) { "git clone --depth 1 #{repo_uri_with_credentials} ./" }
+ end
+ end
+
+ describe '#delete_tag' do
+ it_behaves_like 'command with retries' do
+ let(:tag_name) { 'v1.0' }
+ let(:call_method) { repository.delete_tag(tag_name) }
+ let(:command) { "git push origin --delete #{tag_name}" }
end
end
describe '#push_changes' do
- before do
- `git init` # need a repo to push from
+ let(:branch) { 'master' }
+ let(:call_method) { repository.push_changes }
+ let(:command) { "git push #{repo_uri_with_credentials} #{branch}" }
+
+ context 'when no branch is given' do
+ it_behaves_like 'command with retries'
end
- it 'fails to push changes' do
- expect { repository.push_changes }.to raise_error(described_class::RepositoryCommandError, /The command .* failed \(1\) with the following output/)
+ context 'when branch is given' do
+ let(:branch) { 'my-branch' }
+
+ it_behaves_like 'command with retries' do
+ let(:call_method) { repository.push_changes(branch) }
+ end
end
end
@@ -59,6 +123,7 @@ describe QA::Git::Repository do
[0, 1, 2].each do |version|
it "configures git to use protocol version #{version}" do
expect(repository).to receive(:run).with("git config protocol.version #{version}")
+
repository.git_protocol = version
end
end
@@ -69,21 +134,31 @@ describe QA::Git::Repository do
end
describe '#fetch_supported_git_protocol' do
- Result = Struct.new(:response)
+ let(:call_method) { repository.fetch_supported_git_protocol }
+
+ it_behaves_like 'command with retries' do
+ let(:command) { "git ls-remote #{repo_uri_with_credentials}" }
+ let(:result_output) { +'packet: git< version 2' }
+ let(:command_return) { '2' }
+ let(:extra_args) { { env: "GIT_TRACE_PACKET=1" } }
+ end
it "reports the detected version" do
- expect(repository).to receive(:run).and_return(Result.new("packet: git< version 2"))
- expect(repository.fetch_supported_git_protocol).to eq('2')
+ expect(repository).to receive(:run).and_return(described_class::Result.new(any_args, 0, "packet: git< version 2"))
+
+ expect(call_method).to eq('2')
end
it 'reports unknown if version is unknown' do
- expect(repository).to receive(:run).and_return(Result.new("packet: git< version -1"))
- expect(repository.fetch_supported_git_protocol).to eq('unknown')
+ expect(repository).to receive(:run).and_return(described_class::Result.new(any_args, 0, "packet: git< version -1"))
+
+ expect(call_method).to eq('unknown')
end
it 'reports unknown if content does not identify a version' do
- expect(repository).to receive(:run).and_return(Result.new("foo"))
- expect(repository.fetch_supported_git_protocol).to eq('unknown')
+ expect(repository).to receive(:run).and_return(described_class::Result.new(any_args, 0, "foo"))
+
+ expect(call_method).to eq('unknown')
end
end
@@ -96,7 +171,7 @@ describe QA::Git::Repository do
end
context 'with specific credentials' do
- include_context 'git directory'
+ include_context 'unresolvable git directory'
context 'before setting credentials' do
it 'does not add credentials to .netrc' do
diff --git a/qa/spec/resource/ssh_key_spec.rb b/qa/spec/resource/ssh_key_spec.rb
new file mode 100644
index 00000000000..b2b5ec070e1
--- /dev/null
+++ b/qa/spec/resource/ssh_key_spec.rb
@@ -0,0 +1,21 @@
+# frozen_string_literal: true
+
+describe QA::Resource::SSHKey do
+ describe '#key' do
+ it 'generates a default key' do
+ expect(subject.key).to be_a(QA::Runtime::Key::RSA)
+ end
+ end
+
+ describe '#title' do
+ it 'generates a default title' do
+ expect(subject.title).to match(/E2E test key: \d+/)
+ end
+
+ it 'is possible to set the title' do
+ subject.title = 'I am in a title'
+
+ expect(subject.title).to eq('E2E test key: I am in a title')
+ end
+ end
+end
diff --git a/qa/spec/service/docker_run/gitlab_runner_spec.rb b/qa/spec/service/docker_run/gitlab_runner_spec.rb
new file mode 100644
index 00000000000..db1bb74ca8f
--- /dev/null
+++ b/qa/spec/service/docker_run/gitlab_runner_spec.rb
@@ -0,0 +1,173 @@
+# frozen_string_literal: true
+
+module QA
+ describe Service::DockerRun::GitlabRunner do
+ let(:runner_name) { 'test-runner' }
+ let(:address) { 'gitlab.test' }
+ let(:token) { 'abc123' }
+
+ let(:tags) { %w[qa test] }
+
+ subject do
+ described_class.new(runner_name).tap do |runner|
+ runner.address = address
+ runner.token = token
+ end
+ end
+
+ it 'defaults to run untagged' do
+ expect(subject.run_untagged).to be(true)
+ end
+
+ describe '#register!' do
+ let(:register) { subject.send(:register!) }
+
+ before do
+ allow(subject).to receive(:shell)
+ end
+
+ context 'defaults' do
+ before do
+ register
+ end
+
+ it 'runs non-interactively' do
+ expect(subject).to have_received(:shell).with(/ --non-interactive /)
+ end
+
+ it 'sets pertinent information' do
+ expect(subject).to have_received(:shell).with(/--name #{runner_name} /)
+ expect(subject).to have_received(:shell).with(/--url #{subject.address} /)
+ expect(subject).to have_received(:shell).with(/--registration-token #{subject.token} /)
+ end
+
+ it 'runs untagged' do
+ expect(subject).to have_received(:shell).with(/--run-untagged=true /)
+ end
+
+ it 'has no tags' do
+ expect(subject.tags).to be_falsey
+ end
+
+ it 'runs daemonized' do
+ expect(subject).to have_received(:shell).with(/ -d /)
+ end
+
+ it 'cleans itself up' do
+ expect(subject).to have_received(:shell).with(/ --rm /)
+ end
+ end
+
+ context 'running untagged' do
+ before do
+ register
+ end
+
+ it 'passes --run-untagged=true' do
+ expect(subject).to have_received(:shell).with(/--run-untagged=true /)
+ end
+
+ it 'does not pass tag list' do
+ expect(subject).not_to have_received(:shell).with(/--tag-list/)
+ end
+ end
+
+ context 'running tagged' do
+ context 'with only tags set' do
+ before do
+ subject.tags = tags
+
+ register
+ end
+
+ it 'does not pass --run-untagged' do
+ expect(subject).not_to have_received(:shell).with(/--run-untagged=true/)
+ end
+
+ it 'passes the tags with comma-separation' do
+ expect(subject).to have_received(:shell).with(/--tag-list #{tags.join(',')} /)
+ end
+ end
+
+ context 'with specifying only run_untagged' do
+ before do
+ subject.run_untagged = false
+ end
+
+ it 'raises an error if tags are not specified' do
+ expect { register }.to raise_error(/must specify tags/i)
+ end
+ end
+
+ context 'when specifying contradicting variables' do
+ before do
+ subject.tags = tags
+ subject.run_untagged = true
+ end
+
+ it 'raises an error' do
+ expect { register }.to raise_error(/conflicting options/i)
+ end
+ end
+ end
+
+ context 'executors' do
+ it 'defaults to the shell executor' do
+ register
+
+ expect(subject).to have_received(:shell).with(/--executor shell /)
+ end
+
+ context 'docker' do
+ before do
+ subject.executor = :docker
+
+ register
+ end
+
+ it 'specifies the docker executor' do
+ expect(subject).to have_received(:shell).with(/--executor docker /)
+ end
+
+ it 'mounts the docker socket to the host runner' do
+ expect(subject).to have_received(:shell).with(/-v \/var\/run\/docker.sock:\/var\/run\/docker.sock /)
+ end
+
+ it 'runs in privileged mode' do
+ expect(subject).to have_received(:shell).with(/--privileged /)
+ end
+
+ it 'has a default image' do
+ expect(subject).to have_received(:shell).with(/--docker-image \b.+\b /)
+ end
+
+ it 'does not verify TLS' do
+ expect(subject).to have_received(:shell).with(/--docker-tlsverify=false /)
+ end
+
+ it 'passes privileged mode' do
+ expect(subject).to have_received(:shell).with(/--docker-privileged=true /)
+ end
+
+ it 'passes the host network' do
+ expect(subject).to have_received(:shell).with(/--docker-network-mode=#{subject.network}/)
+ end
+ end
+ end
+ end
+
+ describe '#tags=' do
+ before do
+ subject.tags = tags
+ end
+
+ it 'sets the tags' do
+ expect(subject.tags).to eq(tags)
+ end
+
+ it 'sets run_untagged' do
+ expect(subject.run_untagged).to be(false)
+ end
+ end
+ end
+end
diff --git a/qa/spec/specs/helpers/quarantine_spec.rb b/qa/spec/specs/helpers/quarantine_spec.rb
index d5c6820f0a9..1f09c3f73ac 100644
--- a/qa/spec/specs/helpers/quarantine_spec.rb
+++ b/qa/spec/specs/helpers/quarantine_spec.rb
@@ -31,14 +31,6 @@ RSpec.configure do |c|
config.color_mode = :off
- # Load airborne again to avoid "undefined method `match_expected_default?'" errors
- # that happen because a hook calls a method added via a custom RSpec setting
- # that is removed when the RSpec configuration is sandboxed.
- # If this needs to be changed (e.g., to load other libraries as well), see
- # this discussion for alternative solutions:
- # https://gitlab.com/gitlab-org/gitlab-foss/merge_requests/25223#note_143392053
- load 'airborne.rb'
-
ex.run
end
end
diff --git a/qa/spec/specs/runner_spec.rb b/qa/spec/specs/runner_spec.rb
index 1a3efe0b46c..361588fa14f 100644
--- a/qa/spec/specs/runner_spec.rb
+++ b/qa/spec/specs/runner_spec.rb
@@ -14,6 +14,8 @@ describe QA::Specs::Runner do
describe '#perform' do
before do
allow(QA::Runtime::Browser).to receive(:configure!)
+
+ QA::Runtime::Scenario.define(:gitlab_address, "http://gitlab.test")
end
it_behaves_like 'excludes orchestrated'
@@ -80,6 +82,18 @@ describe QA::Specs::Runner do
end
end
+ context 'when running against live environment' do
+ before do
+ QA::Runtime::Scenario.define(:gitlab_address, "https://staging.gitlab.com")
+ end
+
+ it 'includes default args and excludes the skip_live_env tag' do
+ expect_rspec_runner_arguments(['--tag', '~orchestrated', '--tag', '~skip_live_env', *described_class::DEFAULT_TEST_PATH_ARGS])
+
+ subject.perform
+ end
+ end
+
context 'testable features' do
shared_examples 'one supported feature' do |feature|
before do
diff --git a/rubocop/cop/avoid_keyword_arguments_in_sidekiq_workers.rb b/rubocop/cop/avoid_keyword_arguments_in_sidekiq_workers.rb
new file mode 100644
index 00000000000..eba38c1630f
--- /dev/null
+++ b/rubocop/cop/avoid_keyword_arguments_in_sidekiq_workers.rb
@@ -0,0 +1,20 @@
+module RuboCop
+ module Cop
+ # Cop that blacklists keyword arguments usage in Sidekiq workers
+ class AvoidKeywordArgumentsInSidekiqWorkers < RuboCop::Cop::Cop
+ MSG = "Do not use keyword arguments in Sidekiq workers. " \
+ "For details, check https://github.com/mperham/sidekiq/issues/2372".freeze
+ OBSERVED_METHOD = :perform
+
+ def on_def(node)
+ return if node.method_name != OBSERVED_METHOD
+
+ node.arguments.each do |argument|
+ if argument.type == :kwarg || argument.type == :kwoptarg
+ add_offense(node, location: :expression)
+ end
+ end
+ end
+ end
+ end
+end
diff --git a/rubocop/cop/gitlab/change_timzone.rb b/rubocop/cop/gitlab/change_timzone.rb
new file mode 100644
index 00000000000..63e6dd411f3
--- /dev/null
+++ b/rubocop/cop/gitlab/change_timzone.rb
@@ -0,0 +1,20 @@
+# frozen_string_literal: true
+
+module RuboCop
+ module Cop
+ module Gitlab
+ class ChangeTimezone < RuboCop::Cop::Cop
+ MSG = "Do not change timezone in the runtime (application or rspec), " \
+ "it could result in silently modifying other behavior.".freeze
+
+ def_node_matcher :changing_timezone?, <<~PATTERN
+ (send (const nil? :Time) :zone= ...)
+ PATTERN
+
+ def on_send(node)
+ changing_timezone?(node) { add_offense(node) }
+ end
+ end
+ end
+ end
+end
diff --git a/rubocop/cop/gitlab/json.rb b/rubocop/cop/gitlab/json.rb
new file mode 100644
index 00000000000..8c9027223aa
--- /dev/null
+++ b/rubocop/cop/gitlab/json.rb
@@ -0,0 +1,36 @@
+# frozen_string_literal: true
+
+module RuboCop
+ module Cop
+ module Gitlab
+ class Json < RuboCop::Cop::Cop
+ MSG_SEND = <<~EOL.freeze
+ Avoid calling `JSON` directly. Instead, use the `Gitlab::Json`
+ wrapper. This allows us to alter the JSON parser being used.
+ EOL
+
+ def_node_matcher :json_node?, <<~PATTERN
+ (send (const nil? :JSON)...)
+ PATTERN
+
+ def on_send(node)
+ add_offense(node, location: :expression, message: MSG_SEND) if json_node?(node)
+ end
+
+ def autocorrect(node)
+ autocorrect_json_node(node)
+ end
+
+ def autocorrect_json_node(node)
+ _, method_name, *arg_nodes = *node
+
+ replacement = "Gitlab::Json.#{method_name}(#{arg_nodes.map(&:source).join(', ')})"
+
+ lambda do |corrector|
+ corrector.replace(node.source_range, replacement)
+ end
+ end
+ end
+ end
+ end
+end
diff --git a/rubocop/cop/inject_enterprise_edition_module.rb b/rubocop/cop/inject_enterprise_edition_module.rb
index 6f007e667f2..7edce94eaee 100644
--- a/rubocop/cop/inject_enterprise_edition_module.rb
+++ b/rubocop/cop/inject_enterprise_edition_module.rb
@@ -17,6 +17,8 @@ module RuboCop
CHECK_LINE_METHODS =
Set.new(%i[include_if_ee extend_if_ee prepend_if_ee]).freeze
+ CHECK_LINE_METHODS_REGEXP = Regexp.union(CHECK_LINE_METHODS.map(&:to_s)).freeze
+
DISALLOW_METHODS = Set.new(%i[include extend prepend]).freeze
def ee_const?(node)
@@ -48,7 +50,13 @@ module RuboCop
# the expression is the last line _of code_.
last_line -= 1 if buffer.source.end_with?("\n")
- add_offense(node, message: INVALID_LINE) if line < last_line
+ last_line_content = buffer.source.split("\n")[-1]
+
+ if CHECK_LINE_METHODS_REGEXP.match?(last_line_content)
+ ignore_node(node)
+ elsif line < last_line
+ add_offense(node, message: INVALID_LINE)
+ end
end
def verify_argument_type(node)
diff --git a/rubocop/cop/migration/add_column.rb b/rubocop/cop/migration/add_column.rb
deleted file mode 100644
index 0af90fb7fd1..00000000000
--- a/rubocop/cop/migration/add_column.rb
+++ /dev/null
@@ -1,50 +0,0 @@
-require_relative '../../migration_helpers'
-
-module RuboCop
- module Cop
- module Migration
- # Cop that checks if columns are added in a way that doesn't require
- # downtime.
- class AddColumn < RuboCop::Cop::Cop
- include MigrationHelpers
-
- MSG = '`add_column` with a default value requires downtime, ' \
- 'use `add_column_with_default` instead'.freeze
-
- def on_send(node)
- return unless in_migration?(node)
-
- name = node.children[1]
-
- return unless name == :add_column
-
- # Ignore whitelisted tables.
- return if table_whitelisted?(node.children[2])
-
- opts = node.children.last
-
- return unless opts && opts.type == :hash
-
- opts.each_node(:pair) do |pair|
- if hash_key_type(pair) == :sym && hash_key_name(pair) == :default
- add_offense(node, location: :selector)
- end
- end
- end
-
- def table_whitelisted?(symbol)
- symbol && symbol.type == :sym &&
- WHITELISTED_TABLES.include?(symbol.children[0])
- end
-
- def hash_key_type(pair)
- pair.children[0].type
- end
-
- def hash_key_name(pair)
- pair.children[0].children[0]
- end
- end
- end
- end
-end
diff --git a/rubocop/cop/migration/add_column_with_default.rb b/rubocop/cop/migration/add_column_with_default.rb
index 383653ef5a5..355319b0dfe 100644
--- a/rubocop/cop/migration/add_column_with_default.rb
+++ b/rubocop/cop/migration/add_column_with_default.rb
@@ -5,39 +5,17 @@ require_relative '../../migration_helpers'
module RuboCop
module Cop
module Migration
- # Cop that checks if columns are added in a way that doesn't require
- # downtime.
class AddColumnWithDefault < RuboCop::Cop::Cop
include MigrationHelpers
- MSG = '`add_column_with_default` without `allow_null: true` may cause prolonged lock situations and downtime, ' \
- 'see https://gitlab.com/gitlab-org/gitlab/issues/38060'.freeze
-
- def_node_matcher :add_column_with_default?, <<~PATTERN
- (send _ :add_column_with_default $_ ... (hash $...))
- PATTERN
+ MSG = '`add_column_with_default` is deprecated, use `add_column` instead'.freeze
def on_send(node)
return unless in_migration?(node)
- add_column_with_default?(node) do |table, options|
- add_offense(node, location: :selector) if offensive?(table, options)
- end
- end
-
- private
-
- def offensive?(table, options)
- table_blacklisted?(table) && !nulls_allowed?(options)
- end
-
- def nulls_allowed?(options)
- options.find { |opt| opt.key.value == :allow_null && opt.value.true_type? }
- end
+ name = node.children[1]
- def table_blacklisted?(symbol)
- symbol && symbol.type == :sym &&
- BLACKLISTED_TABLES.include?(symbol.children[0])
+ add_offense(node, location: :selector) if name == :add_column_with_default
end
end
end
diff --git a/rubocop/cop/migration/add_columns_to_wide_tables.rb b/rubocop/cop/migration/add_columns_to_wide_tables.rb
index 4618e4ae890..2880783dc3e 100644
--- a/rubocop/cop/migration/add_columns_to_wide_tables.rb
+++ b/rubocop/cop/migration/add_columns_to_wide_tables.rb
@@ -14,7 +14,6 @@ module RuboCop
BLACKLISTED_METHODS = %i[
add_column
- add_column_with_default
add_reference
add_timestamps_with_timezone
].freeze
diff --git a/rubocop/cop/migration/add_concurrent_foreign_key.rb b/rubocop/cop/migration/add_concurrent_foreign_key.rb
index d78c7b9b043..236de6224a4 100644
--- a/rubocop/cop/migration/add_concurrent_foreign_key.rb
+++ b/rubocop/cop/migration/add_concurrent_foreign_key.rb
@@ -10,17 +10,29 @@ module RuboCop
MSG = '`add_foreign_key` requires downtime, use `add_concurrent_foreign_key` instead'.freeze
+ def_node_matcher :false_node?, <<~PATTERN
+ (false)
+ PATTERN
+
def on_send(node)
return unless in_migration?(node)
name = node.children[1]
- add_offense(node, location: :selector) if name == :add_foreign_key
+ if name == :add_foreign_key && !not_valid_fk?(node)
+ add_offense(node, location: :selector)
+ end
end
def method_name(node)
node.children.first
end
+
+ def not_valid_fk?(node)
+ node.each_node(:pair).any? do |pair|
+ pair.children[0].children[0] == :validate && false_node?(pair.children[1])
+ end
+ end
end
end
end
diff --git a/rubocop/cop/migration/add_limit_to_string_columns.rb b/rubocop/cop/migration/add_limit_to_string_columns.rb
deleted file mode 100644
index 30affcbb089..00000000000
--- a/rubocop/cop/migration/add_limit_to_string_columns.rb
+++ /dev/null
@@ -1,59 +0,0 @@
-# frozen_string_literal: true
-
-require_relative '../../migration_helpers'
-
-module RuboCop
- module Cop
- module Migration
- # Cop that enforces length constraints to string columns
- class AddLimitToStringColumns < RuboCop::Cop::Cop
- include MigrationHelpers
-
- ADD_COLUMNS_METHODS = %i(add_column add_column_with_default).freeze
-
- MSG = 'String columns should have a limit constraint. 255 is suggested'.freeze
-
- def on_def(node)
- return unless in_migration?(node)
-
- node.each_descendant(:send) do |send_node|
- next unless string_operation?(send_node)
-
- add_offense(send_node, location: :selector) unless limit_on_string_column?(send_node)
- end
- end
-
- private
-
- def string_operation?(node)
- modifier = node.children[0]
- migration_method = node.children[1]
-
- if migration_method == :string
- modifier.type == :lvar
- elsif ADD_COLUMNS_METHODS.include?(migration_method)
- modifier.nil? && string_column?(node.children[4])
- end
- end
-
- def string_column?(column_type)
- column_type.type == :sym && column_type.value == :string
- end
-
- def limit_on_string_column?(node)
- migration_method = node.children[1]
-
- if migration_method == :string
- limit_present?(node.children)
- elsif ADD_COLUMNS_METHODS.include?(migration_method)
- limit_present?(node)
- end
- end
-
- def limit_present?(statement)
- !(statement.to_s =~ /:limit/).nil?
- end
- end
- end
- end
-end
diff --git a/rubocop/cop/migration/add_limit_to_text_columns.rb b/rubocop/cop/migration/add_limit_to_text_columns.rb
new file mode 100644
index 00000000000..15c28bb9266
--- /dev/null
+++ b/rubocop/cop/migration/add_limit_to_text_columns.rb
@@ -0,0 +1,121 @@
+# frozen_string_literal: true
+
+require_relative '../../migration_helpers'
+
+module RuboCop
+ module Cop
+ module Migration
+ # Cop that enforces always adding a limit on text columns
+ class AddLimitToTextColumns < RuboCop::Cop::Cop
+ include MigrationHelpers
+
+ MSG = 'Text columns should always have a limit set (255 is suggested). ' \
+ 'You can add a limit to a `text` column by using `add_text_limit`'.freeze
+
+ def_node_matcher :reverting?, <<~PATTERN
+ (def :down ...)
+ PATTERN
+
+ def_node_matcher :add_text_limit?, <<~PATTERN
+ (send _ :add_text_limit ...)
+ PATTERN
+
+ def on_def(node)
+ return unless in_migration?(node)
+
+ # Don't enforce the rule when on down to keep consistency with existing schema
+ return if reverting?(node)
+
+ node.each_descendant(:send) do |send_node|
+ next unless text_operation?(send_node)
+
+ # We require a limit for the same table and attribute name
+ if text_limit_missing?(node, *table_and_attribute_name(send_node))
+ add_offense(send_node, location: :selector)
+ end
+ end
+ end
+
+ private
+
+ def text_operation?(node)
+ modifier = node.children[0]
+ migration_method = node.children[1]
+
+ if migration_method == :text
+ modifier.type == :lvar
+ elsif ADD_COLUMN_METHODS.include?(migration_method)
+ modifier.nil? && text_column?(node.children[4])
+ end
+ end
+
+ def text_column?(column_type)
+ column_type.type == :sym && column_type.value == :text
+ end
+
+ # For a given node, find the table and attribute this node is for
+ #
+ # Simple when we have calls to `add_column_XXX` helper methods
+ #
+ # A little bit more tricky when we have attributes defined as part of
+ # a create/change table block:
+ # - The attribute name is available on the node
+ # - Finding the table name requires to:
+ # * go up
+ # * find the first block the attribute def is part of
+ # * go back down to find the create_table node
+ # * fetch the table name from that node
+ def table_and_attribute_name(node)
+ migration_method = node.children[1]
+ table_name, attribute_name = ''
+
+ if migration_method == :text
+ # We are inside a node in a create/change table block
+ block_node = node.each_ancestor(:block).first
+ create_table_node = block_node
+ .children
+ .find { |n| TABLE_METHODS.include?(n.children[1])}
+
+ if create_table_node
+ table_name = create_table_node.children[2].value
+ else
+ # Guard against errors when a new table create/change migration
+ # helper is introduced and warn the author so that it can be
+ # added in TABLE_METHODS
+ table_name = 'unknown'
+ add_offense(block_node, message: 'Unknown table create/change helper')
+ end
+
+ attribute_name = node.children[2].value
+ else
+ # We are in a node for one of the ADD_COLUMN_METHODS
+ table_name = node.children[2].value
+ attribute_name = node.children[3].value
+ end
+
+ [table_name, attribute_name]
+ end
+
+ # Check if there is an `add_text_limit` call for the provided
+ # table and attribute name
+ def text_limit_missing?(node, table_name, attribute_name)
+ limit_found = false
+
+ node.each_descendant(:send) do |send_node|
+ next unless add_text_limit?(send_node)
+
+ limit_table = send_node.children[2].value
+ limit_attribute = send_node.children[3].value
+
+ if limit_table == table_name && limit_attribute == attribute_name
+ limit_found = true
+ break
+ end
+ end
+
+ !limit_found
+ end
+ end
+ end
+ end
+end
diff --git a/rubocop/cop/migration/prevent_strings.rb b/rubocop/cop/migration/prevent_strings.rb
new file mode 100644
index 00000000000..25c00194698
--- /dev/null
+++ b/rubocop/cop/migration/prevent_strings.rb
@@ -0,0 +1,52 @@
+# frozen_string_literal: true
+
+require_relative '../../migration_helpers'
+
+module RuboCop
+ module Cop
+ module Migration
+ # Cop that enforces using text instead of the string data type
+ class PreventStrings < RuboCop::Cop::Cop
+ include MigrationHelpers
+
+ MSG = 'Do not use the `string` data type, use `text` instead. ' \
+ 'Updating limits on strings requires downtime. This can be avoided ' \
+ 'by using `text` and adding a limit with `add_text_limit`'.freeze
+
+ def_node_matcher :reverting?, <<~PATTERN
+ (def :down ...)
+ PATTERN
+
+ def on_def(node)
+ return unless in_migration?(node)
+
+ # Don't enforce the rule when on down to keep consistency with existing schema
+ return if reverting?(node)
+
+ node.each_descendant(:send) do |send_node|
+ next unless string_operation?(send_node)
+
+ add_offense(send_node, location: :selector)
+ end
+ end
+
+ private
+
+ def string_operation?(node)
+ modifier = node.children[0]
+ migration_method = node.children[1]
+
+ if migration_method == :string
+ modifier.type == :lvar
+ elsif ADD_COLUMN_METHODS.include?(migration_method)
+ modifier.nil? && string_column?(node.children[4])
+ end
+ end
+
+ def string_column?(column_type)
+ column_type.type == :sym && column_type.value == :string
+ end
+ end
+ end
+ end
+end
diff --git a/rubocop/cop/migration/reversible_add_column_with_default.rb b/rubocop/cop/migration/reversible_add_column_with_default.rb
deleted file mode 100644
index dd49188defa..00000000000
--- a/rubocop/cop/migration/reversible_add_column_with_default.rb
+++ /dev/null
@@ -1,35 +0,0 @@
-require_relative '../../migration_helpers'
-
-module RuboCop
- module Cop
- module Migration
- # Cop that checks if `add_column_with_default` is used with `up`/`down` methods
- # and not `change`.
- class ReversibleAddColumnWithDefault < RuboCop::Cop::Cop
- include MigrationHelpers
-
- def_node_matcher :add_column_with_default?, <<~PATTERN
- (send nil? :add_column_with_default $...)
- PATTERN
-
- def_node_matcher :defines_change?, <<~PATTERN
- (def :change ...)
- PATTERN
-
- MSG = '`add_column_with_default` is not reversible so you must manually define ' \
- 'the `up` and `down` methods in your migration class, using `remove_column` in `down`'.freeze
-
- def on_send(node)
- return unless in_migration?(node)
- return unless add_column_with_default?(node)
-
- node.each_ancestor(:def) do |def_node|
- next unless defines_change?(def_node)
-
- add_offense(def_node, location: :name)
- end
- end
- end
- end
- end
-end
diff --git a/rubocop/cop/migration/with_lock_retries_disallowed_method.rb b/rubocop/cop/migration/with_lock_retries_disallowed_method.rb
new file mode 100644
index 00000000000..309ddcc9746
--- /dev/null
+++ b/rubocop/cop/migration/with_lock_retries_disallowed_method.rb
@@ -0,0 +1,58 @@
+# frozen_string_literal: true
+
+require_relative '../../migration_helpers'
+
+module RuboCop
+ module Cop
+ module Migration
+ class WithLockRetriesDisallowedMethod < RuboCop::Cop::Cop
+ include MigrationHelpers
+
+ ALLOWED_MIGRATION_METHODS = %i[
+ create_table
+ drop_table
+ add_foreign_key
+ remove_foreign_key
+ add_column
+ remove_column
+ execute
+ change_column_default
+ remove_foreign_key_if_exists
+ remove_foreign_key_without_error
+ table_exists?
+ index_exists_by_name?
+ foreign_key_exists?
+ index_exists?
+ column_exists?
+ ].sort.freeze
+
+ MSG = "The method is not allowed to be called within the `with_lock_retries` block, the only allowed methods are: #{ALLOWED_MIGRATION_METHODS.join(', ')}"
+
+ def_node_matcher :send_node?, <<~PATTERN
+ send
+ PATTERN
+
+ def on_block(node)
+ block_body = node.body
+
+ return unless in_migration?(node)
+ return unless block_body
+ return unless node.method_name == :with_lock_retries
+
+ if send_node?(block_body)
+ check_node(block_body)
+ else
+ block_body.children.each { |n| check_node(n) }
+ end
+ end
+
+ def check_node(node)
+ return unless send_node?(node)
+
+ name = node.children[1]
+ add_offense(node, location: :expression) unless ALLOWED_MIGRATION_METHODS.include?(name)
+ end
+ end
+ end
+ end
+end
diff --git a/rubocop/cop/migration/with_lock_retries_without_ddl_transaction.rb b/rubocop/cop/migration/with_lock_retries_without_ddl_transaction.rb
deleted file mode 100644
index ebd91dd5a6e..00000000000
--- a/rubocop/cop/migration/with_lock_retries_without_ddl_transaction.rb
+++ /dev/null
@@ -1,36 +0,0 @@
-# frozen_string_literal: true
-
-require_relative '../../migration_helpers'
-
-module RuboCop
- module Cop
- module Migration
- # Cop that prevents usage of `with_lock_retries` with `disable_ddl_transaction!`
- class WithLockRetriesWithoutDdlTransaction < RuboCop::Cop::Cop
- include MigrationHelpers
-
- MSG = '`with_lock_retries` cannot be used with disabled DDL transactions (`disable_ddl_transaction!`). ' \
- 'Please remove the `disable_ddl_transaction!` call from your migration.'.freeze
-
- def_node_matcher :disable_ddl_transaction?, <<~PATTERN
- (send _ :disable_ddl_transaction!)
- PATTERN
-
- def_node_matcher :with_lock_retries?, <<~PATTERN
- (send _ :with_lock_retries)
- PATTERN
-
- def on_send(node)
- return unless in_migration?(node)
- return unless with_lock_retries?(node)
-
- node.each_ancestor(:begin) do |begin_node|
- disable_ddl_transaction_node = begin_node.children.find { |n| disable_ddl_transaction?(n) }
-
- add_offense(node, location: :expression) if disable_ddl_transaction_node
- end
- end
- end
- end
- end
-end
diff --git a/rubocop/cop/performance/ar_exists_and_present_blank.rb b/rubocop/cop/performance/ar_exists_and_present_blank.rb
new file mode 100644
index 00000000000..54c2d6bf95a
--- /dev/null
+++ b/rubocop/cop/performance/ar_exists_and_present_blank.rb
@@ -0,0 +1,57 @@
+# frozen_string_literal: true
+
+module RuboCop
+ module Cop
+ module Performance
+ class ARExistsAndPresentBlank < RuboCop::Cop::Cop
+ def message_present(ivar)
+ "Avoid `#{ivar}.present?`, because it will generate database query 'Select TABLE.*' which is expensive. "\
+ "Suggest to use `#{ivar}.any?` to replace `#{ivar}.present?`"
+ end
+
+ def message_blank(ivar)
+ "Avoid `#{ivar}.blank?`, because it will generate database query 'Select TABLE.*' which is expensive. "\
+ "Suggest to use `#{ivar}.empty?` to replace `#{ivar}.blank?`"
+ end
+
+ def_node_matcher :exists_match, <<~PATTERN
+ (send (ivar $_) :exists?)
+ PATTERN
+
+ def_node_matcher :present_match, <<~PATTERN
+ (send (ivar $_) :present?)
+ PATTERN
+
+ def_node_matcher :blank_match, <<~PATTERN
+ (send (ivar $_) :blank?)
+ PATTERN
+
+ def file_name(node)
+ node.location.expression.source_buffer.name
+ end
+
+ def in_haml_file?(node)
+ file_name(node).end_with?('.haml.rb')
+ end
+
+ def on_send(node)
+ return unless in_haml_file?(node)
+
+ ivar_present = present_match(node)
+ ivar_blank = blank_match(node)
+ return unless ivar_present || ivar_blank
+
+ node.each_ancestor(:begin) do |begin_node|
+ begin_node.each_descendant do |n|
+ ivar_exists = exists_match(n)
+ next unless ivar_exists
+
+ add_offense(node, location: :expression, message: message_present(ivar_exists)) if ivar_exists == ivar_present
+ add_offense(node, location: :expression, message: message_blank(ivar_exists)) if ivar_exists == ivar_blank
+ end
+ end
+ end
+ end
+ end
+ end
+end
diff --git a/rubocop/cop/rspec/empty_line_after_shared_example.rb b/rubocop/cop/rspec/empty_line_after_shared_example.rb
new file mode 100644
index 00000000000..5d09565bd5a
--- /dev/null
+++ b/rubocop/cop/rspec/empty_line_after_shared_example.rb
@@ -0,0 +1,64 @@
+# frozen_string_literal: true
+
+require 'rubocop/rspec/final_end_location'
+require 'rubocop/rspec/blank_line_separation'
+require 'rubocop/rspec/language'
+
+module RuboCop
+ module Cop
+ module RSpec
+ # Checks if there is an empty line after shared example blocks.
+ #
+ # @example
+ # # bad
+ # RSpec.describe Foo do
+ # it_behaves_like 'do this first'
+ # it_behaves_like 'does this' do
+ # end
+ # it_behaves_like 'does that' do
+ # end
+ # it_behaves_like 'do some more'
+ # end
+ #
+ # # good
+ # RSpec.describe Foo do
+ # it_behaves_like 'do this first'
+ # it_behaves_like 'does this' do
+ # end
+ #
+ # it_behaves_like 'does that' do
+ # end
+ #
+ # it_behaves_like 'do some more'
+ # end
+ #
+ # # fair - it's ok to have non-separated without blocks
+ # RSpec.describe Foo do
+ # it_behaves_like 'do this first'
+ # it_behaves_like 'does this'
+ # end
+ #
+ class EmptyLineAfterSharedExample < RuboCop::Cop::Cop
+ include RuboCop::RSpec::BlankLineSeparation
+ include RuboCop::RSpec::Language
+
+ MSG = 'Add an empty line after `%<example>s` block.'
+
+ def_node_matcher :shared_examples,
+ (SharedGroups::ALL + Includes::ALL).block_pattern
+
+ def on_block(node)
+ shared_examples(node) do
+ break if last_child?(node)
+
+ missing_separating_line(node) do |location|
+ add_offense(node,
+ location: location,
+ message: format(MSG, example: node.method_name))
+ end
+ end
+ end
+ end
+ end
+ end
+end
diff --git a/rubocop/migration_helpers.rb b/rubocop/migration_helpers.rb
index a987ae360d3..de377b15cc8 100644
--- a/rubocop/migration_helpers.rb
+++ b/rubocop/migration_helpers.rb
@@ -54,6 +54,12 @@ module RuboCop
ci_builds
].freeze
+ # List of helpers that add new columns, either directly (ADD_COLUMN_METHODS)
+ # or through a create/alter table (TABLE_METHODS)
+ ADD_COLUMN_METHODS = %i(add_column add_column_with_default change_column_type_concurrently).freeze
+
+ TABLE_METHODS = %i(create_table create_table_if_not_exists change_table).freeze
+
# Returns true if the given node originated from the db/migrate directory.
def in_migration?(node)
dirname(node).end_with?('db/migrate', 'db/geo/migrate') || in_post_deployment_migration?(node)
diff --git a/scripts/build_assets_image b/scripts/build_assets_image
index 6354c2df798..9eb1ccd5515 100755
--- a/scripts/build_assets_image
+++ b/scripts/build_assets_image
@@ -1,5 +1,3 @@
-#!/bin/bash
-
# Exit early if we don't want to build the image
if [[ "${BUILD_ASSETS_IMAGE}" != "true" ]]
then
@@ -19,15 +17,19 @@ ASSETS_IMAGE_PATH=${CI_REGISTRY}/${CI_PROJECT_PATH}/${ASSETS_IMAGE_NAME}
mkdir -p assets_container.build/public
cp -r public/assets assets_container.build/public/
cp Dockerfile.assets assets_container.build/
-docker build -t ${ASSETS_IMAGE_PATH}:${CI_COMMIT_REF_SLUG} -f assets_container.build/Dockerfile.assets assets_container.build/
-docker tag ${ASSETS_IMAGE_PATH}:${CI_COMMIT_REF_SLUG} ${ASSETS_IMAGE_PATH}:${CI_COMMIT_SHA}
-docker login -u gitlab-ci-token -p ${CI_JOB_TOKEN} ${CI_REGISTRY}
-docker push ${ASSETS_IMAGE_PATH}:${CI_COMMIT_REF_SLUG}
-docker push ${ASSETS_IMAGE_PATH}:${CI_COMMIT_SHA}
+
+COMMIT_REF_SLUG_DESTINATION=${ASSETS_IMAGE_PATH}:${CI_COMMIT_REF_SLUG}
+COMMIT_SHA_DESTINATION=${ASSETS_IMAGE_PATH}:${CI_COMMIT_SHA}
+COMMIT_REF_NAME_DESTINATION=${ASSETS_IMAGE_PATH}:${CI_COMMIT_REF_NAME}
+
+DESTINATIONS="--destination=$COMMIT_REF_SLUG_DESTINATION --destination=$COMMIT_SHA_DESTINATION"
# Also tag the image with GitLab version, if running on a tag pipeline, so
# other projects can simply use that instead of computing the slug.
if [ -n "$CI_COMMIT_TAG" ]; then
- docker tag ${ASSETS_IMAGE_PATH}:${CI_COMMIT_REF_SLUG} ${ASSETS_IMAGE_PATH}:${CI_COMMIT_REF_NAME}
- docker push ${ASSETS_IMAGE_PATH}:${CI_COMMIT_REF_NAME}
+ DESTINATIONS="$DESTINATIONS --destination=$COMMIT_REF_NAME_DESTINATION"
fi
+
+echo "building assets image for destinations: $DESTINATIONS"
+
+/kaniko/executor --context=assets_container.build --dockerfile=assets_container.build/Dockerfile.assets $DESTINATIONS
diff --git a/scripts/clean-old-cached-assets b/scripts/clean-old-cached-assets
index 9a373439e5e..20889b7ffe6 100755
--- a/scripts/clean-old-cached-assets
+++ b/scripts/clean-old-cached-assets
@@ -1,4 +1,4 @@
-#!/bin/bash
+#!/usr/bin/env bash
# Clean up cached files that are older than 4 days
find tmp/cache/assets/sprockets/ -type f -mtime +4 -execdir rm -- "{}" \;
diff --git a/scripts/create_postgres_user.sh b/scripts/create_postgres_user.sh
index 8a049bcd7fb..4c9db68c9d3 100644
--- a/scripts/create_postgres_user.sh
+++ b/scripts/create_postgres_user.sh
@@ -1,4 +1,4 @@
-#!/bin/bash
+#!/usr/bin/env bash
psql -h postgres -U postgres postgres <<EOF
CREATE USER gitlab;
diff --git a/scripts/frontend/webpack_dev_server.js b/scripts/frontend/webpack_dev_server.js
new file mode 100755
index 00000000000..8026a8d47e2
--- /dev/null
+++ b/scripts/frontend/webpack_dev_server.js
@@ -0,0 +1,68 @@
+const nodemon = require('nodemon');
+
+const DEV_SERVER_HOST = process.env.DEV_SERVER_HOST || 'localhost';
+const DEV_SERVER_PORT = process.env.DEV_SERVER_PORT || '3808';
+const STATIC_MODE = process.env.DEV_SERVER_STATIC && process.env.DEV_SERVER_STATIC != 'false';
+const DLL_MODE = process.env.WEBPACK_VENDOR_DLL && process.env.WEBPACK_VENDOR_DLL != 'false';
+
+const baseConfig = {
+ ignoreRoot: ['.git', 'node_modules/*/'],
+ noUpdateNotifier: true,
+ signal: 'SIGTERM',
+ delay: 1000,
+};
+
+// run webpack in compile-once mode and watch for changes
+if (STATIC_MODE) {
+ nodemon({
+ exec: `rm -rf public/assets/webpack ; yarn run webpack && exec ruby -run -e httpd public/ -p ${DEV_SERVER_PORT}`,
+ watch: [
+ 'config/webpack.config.js',
+ 'app/assets/javascripts',
+ 'ee/app/assets/javascripts',
+ // ensure we refresh when running yarn install
+ 'node_modules/.yarn-integrity',
+ ],
+ ext: 'js,json,vue',
+ ...baseConfig,
+ });
+}
+
+// run webpack through webpack-dev-server, optionally compiling a DLL to reduce memory
+else {
+ let watch = ['config/webpack.config.js'];
+
+ // if utilizing the vendor DLL, we need to restart the process when dependency changes occur
+ if (DLL_MODE) {
+ watch.push(
+ 'config/webpack.vendor.config.js',
+ // ensure we refresh when running yarn install
+ 'node_modules/.yarn-integrity',
+ 'package.json',
+ 'yarn.lock',
+ );
+ }
+ nodemon({
+ exec: 'webpack-dev-server --config config/webpack.config.js',
+ watch,
+ ...baseConfig,
+ });
+}
+
+// print useful messages for nodemon events
+nodemon
+ .on('start', function() {
+ console.log(`Starting webpack webserver on http://${DEV_SERVER_HOST}:${DEV_SERVER_PORT}`);
+ if (STATIC_MODE) {
+ console.log('You are starting webpack in compile-once mode');
+ console.log('The JavaScript assets are recompiled only if they change');
+ console.log('If you change them often, you might want to unset DEV_SERVER_STATIC');
+ }
+ })
+ .on('quit', function() {
+ console.log('Shutting down webpack process');
+ process.exit();
+ })
+ .on('restart', function(files) {
+ console.log('Restarting webpack process due to: ', files);
+ });
diff --git a/scripts/gitaly_test.rb b/scripts/gitaly_test.rb
index 8db47afdd4d..e6f2c9885d9 100644
--- a/scripts/gitaly_test.rb
+++ b/scripts/gitaly_test.rb
@@ -20,7 +20,7 @@ module GitalyTest
'HOME' => File.expand_path('tmp/tests'),
'GEM_PATH' => Gem.path.join(':'),
'BUNDLE_APP_CONFIG' => File.join(File.dirname(gemfile), '.bundle/config'),
- 'BUNDLE_FLAGS' => "--jobs=4 --retry=3",
+ 'BUNDLE_FLAGS' => "--jobs=4 --retry=3 --quiet",
'BUNDLE_INSTALL_FLAGS' => nil,
'BUNDLE_GEMFILE' => gemfile,
'RUBYOPT' => nil,
diff --git a/scripts/prepare_build.sh b/scripts/prepare_build.sh
index e80d752f09f..d2cc16f4f8b 100644
--- a/scripts/prepare_build.sh
+++ b/scripts/prepare_build.sh
@@ -6,12 +6,17 @@ export BUNDLE_INSTALL_FLAGS="--without=production --jobs=$(nproc) --path=vendor
if [ "$USE_BUNDLE_INSTALL" != "false" ]; then
bundle --version
- bundle install --clean $BUNDLE_INSTALL_FLAGS && bundle check
+ run_timed_command "bundle install --clean ${BUNDLE_INSTALL_FLAGS}"
+ run_timed_command "bundle check"
+ # When we test multiple versions of PG in the same pipeline, we have a single `setup-test-env`
+ # job but the `pg` gem needs to be rebuilt since it includes extensions (https://guides.rubygems.org/gems-with-extensions).
+ # Uncomment the following line if multiple versions of PG are tested in the same pipeline.
+ # run_timed_command "bundle pristine pg"
fi
# Only install knapsack after bundle install! Otherwise oddly some native
# gems could not be found under some circumstance. No idea why, hours wasted.
-retry gem install knapsack --no-document
+run_timed_command "gem install knapsack --no-document"
cp config/gitlab.yml.example config/gitlab.yml
sed -i 's/bin_path: \/usr\/bin\/git/bin_path: \/usr\/local\/bin\/git/' config/gitlab.yml
@@ -33,6 +38,9 @@ if [ -f config/database_geo.yml ]; then
sed -i 's/username: git/username: postgres/g' config/database_geo.yml
fi
+cp config/cable.yml.example config/cable.yml
+sed -i 's|url:.*$|url: redis://redis:6379|g' config/cable.yml
+
cp config/resque.yml.example config/resque.yml
sed -i 's|url:.*$|url: redis://redis:6379|g' config/resque.yml
diff --git a/scripts/prepare_postgres_fdw.sh b/scripts/prepare_postgres_fdw.sh
index 69442d2881b..246f3acc569 100755
--- a/scripts/prepare_postgres_fdw.sh
+++ b/scripts/prepare_postgres_fdw.sh
@@ -1,4 +1,4 @@
-#!/bin/bash
+#!/usr/bin/env bash
psql -h postgres -U postgres gitlabhq_geo_test <<EOF
CREATE EXTENSION postgres_fdw;
diff --git a/scripts/regenerate-schema b/scripts/regenerate-schema
new file mode 100755
index 00000000000..cedd612f766
--- /dev/null
+++ b/scripts/regenerate-schema
@@ -0,0 +1,182 @@
+#!/usr/bin/env ruby
+
+# frozen_string_literal: true
+
+require 'open3'
+require 'uri'
+
+class SchemaRegenerator
+ ##
+ # Filename of the schema
+ #
+ # This file is being regenerated by this script.
+ FILENAME = 'db/structure.sql'
+
+ ##
+ # Directories where migrations are stored
+ #
+ # The methods +hide_migrations+ and +unhide_migrations+ will rename
+ # these to disable/enable migrations.
+ MIGRATION_DIRS = %w[db/migrate db/post_migrate].freeze
+
+ def execute
+ Dir.chdir(File.expand_path('..', __dir__)) do
+ checkout_ref
+ checkout_clean_schema
+ hide_migrations
+ reset_db
+ unhide_migrations
+ migrate
+ ensure
+ unhide_migrations
+ end
+ end
+
+ private
+
+ ##
+ # Git checkout +CI_COMMIT_SHA+.
+ #
+ # When running from CI, checkout the clean commit,
+ # not the merged result.
+ def checkout_ref
+ return unless ci?
+
+ run %Q[git checkout #{source_ref}]
+ run %q[git clean -f -- db]
+ end
+
+ ##
+ # Checkout the clean schema from the target branch
+ def checkout_clean_schema
+ remote_checkout_clean_schema || local_checkout_clean_schema
+ end
+
+ ##
+ # Get clean schema from remote servers
+ #
+ # This script might run in CI, using a shallow clone, so to checkout
+ # the file, fetch the target branch from the server.
+ def remote_checkout_clean_schema
+ return false unless project_url
+ return false unless target_project_url
+
+ run %Q[git remote add target_project #{target_project_url}.git]
+ run %Q[git fetch target_project #{target_branch}:#{target_branch}]
+
+ local_checkout_clean_schema
+ end
+
+ ##
+ # Git checkout the schema from target branch.
+ #
+ # Ask git to checkout the schema from the target branch and reset
+ # the file to unstage the changes.
+ def local_checkout_clean_schema
+ run %Q[git checkout #{merge_base} -- #{FILENAME}]
+ run %Q[git reset -- #{FILENAME}]
+ end
+
+ ##
+ # Move migrations to where Rails will not find them.
+ #
+ # To reset the database to clean schema defined in +FILENAME+, move
+ # the migrations to a path where Rails will not find them, otherwise
+ # +db:reset+ would abort. Later when the migrations should be
+ # applied, use +unhide_migrations+ to bring them back.
+ def hide_migrations
+ MIGRATION_DIRS.each do |dir|
+ File.rename(dir, "#{dir}__")
+ end
+ end
+
+ ##
+ # Undo the effect of +hide_migrations+.
+ #
+ # Place back the migrations which might be moved by
+ # +hide_migrations+.
+ def unhide_migrations
+ error = nil
+
+ MIGRATION_DIRS.each do |dir|
+ File.rename("#{dir}__", dir)
+ rescue Errno::ENOENT
+ nil
+ rescue StandardError => e
+ # Save error for later, but continue with other dirs first
+ error = e
+ end
+
+ raise error if error
+ end
+
+ ##
+ # Run rake task to reset the database.
+ def reset_db
+ run %q[bin/rails db:reset RAILS_ENV=test]
+ end
+
+ ##
+ # Run rake task to run migrations.
+ def migrate
+ run %q[bin/rails db:migrate RAILS_ENV=test]
+ end
+
+ ##
+ # Run the given +cmd+.
+ #
+ # The command is colored green, and the output of the command is
+ # colored gray.
+ # When the command failed an exception is raised.
+ def run(cmd)
+ puts "\e[32m$ #{cmd}\e[37m"
+ stdout_str, stderr_str, status = Open3.capture3(cmd)
+ puts "#{stdout_str}#{stderr_str}\e[0m"
+ raise("Command failed: #{stderr_str}") unless status.success?
+
+ stdout_str
+ end
+
+ ##
+ # Return the base commit between source and target branch.
+ def merge_base
+ @merge_base ||= run("git merge-base #{target_branch} #{source_ref}").chomp
+ end
+
+ ##
+ # Return the name of the target branch
+ #
+ # Get source ref from CI environment variable, or read the +TARGET+
+ # environment+ variable, or default to +HEAD+.
+ def target_branch
+ ENV['CI_MERGE_REQUEST_TARGET_BRANCH_NAME'] || ENV['TARGET'] || 'master'
+ end
+
+ ##
+ # Return the source ref
+ #
+ # Get source ref from CI environment variable, or default to +HEAD+.
+ def source_ref
+ ENV['CI_COMMIT_SHA'] || 'HEAD'
+ end
+
+ ##
+ # Return the source project URL from CI environment variable.
+ def project_url
+ ENV['CI_PROJECT_URL']
+ end
+
+ ##
+ # Return the target project URL from CI environment variable.
+ def target_project_url
+ ENV['CI_MERGE_REQUEST_PROJECT_URL']
+ end
+
+ ##
+ # Return whether the script is running from CI
+ def ci?
+ ENV['CI']
+ end
+end
+
+SchemaRegenerator.new.execute
diff --git a/scripts/review_apps/automated_cleanup.rb b/scripts/review_apps/automated_cleanup.rb
index e3ed7143ea2..a9659071a2f 100755
--- a/scripts/review_apps/automated_cleanup.rb
+++ b/scripts/review_apps/automated_cleanup.rb
@@ -1,7 +1,6 @@
# frozen_string_literal: true
require 'gitlab'
-require_relative File.expand_path('../../lib/quality/helm_client.rb', __dir__)
require_relative File.expand_path('../../lib/quality/helm3_client.rb', __dir__)
require_relative File.expand_path('../../lib/quality/kubernetes_client.rb', __dir__)
@@ -9,7 +8,6 @@ class AutomatedCleanup
attr_reader :project_path, :gitlab_token
DEPLOYMENTS_PER_PAGE = 100
- HELM_RELEASES_BATCH_SIZE = 5
IGNORED_HELM_ERRORS = [
'transport is closing',
'error upgrading connection',
@@ -45,18 +43,8 @@ class AutomatedCleanup
self.class.ee? ? 'review-apps-ee' : 'review-apps-ce'
end
- def helm3?
- !ENV['HELM_3'].nil?
- end
-
- def helm_client_class
- helm3? ? Quality::Helm3Client : Quality::HelmClient
- end
-
def helm
- @helm ||= helm_client_class.new(
- tiller_namespace: review_apps_namespace,
- namespace: review_apps_namespace)
+ @helm ||= Quality::Helm3Client.new(namespace: review_apps_namespace)
end
def kubernetes
@@ -88,7 +76,7 @@ class AutomatedCleanup
if deployed_at < delete_threshold
deleted_environment = delete_environment(environment, deployment)
if deleted_environment
- release = helm_client_class::Release.new(environment.slug, 1, deployed_at.to_s, nil, nil, review_apps_namespace)
+ release = Quality::Helm3Client::Release.new(environment.slug, 1, deployed_at.to_s, nil, nil, review_apps_namespace)
releases_to_delete << release
end
else
@@ -117,7 +105,7 @@ class AutomatedCleanup
# Prevents deleting `dns-gitlab-review-app` releases or other unrelated releases
next unless release.name.start_with?('review-')
- if release.status.casecmp('failed') == 0 || release.last_update < threshold
+ if release.status == 'failed' || release.last_update < threshold
releases_to_delete << release
else
print_release_state(subject: 'Release', release_name: release.name, release_date: release.last_update, action: 'leaving')
@@ -154,7 +142,6 @@ class AutomatedCleanup
def helm_releases
args = ['--all', '--date']
- args << "--max #{HELM_RELEASES_BATCH_SIZE}" unless helm3?
helm.releases(args: args)
end
@@ -170,7 +157,7 @@ class AutomatedCleanup
helm.delete(release_name: releases_names)
kubernetes.cleanup(release_name: releases_names, wait: false)
- rescue helm_client_class::CommandFailedError => ex
+ rescue Quality::Helm3Client::CommandFailedError => ex
raise ex unless ignore_exception?(ex.message, IGNORED_HELM_ERRORS)
puts "Ignoring the following Helm error:\n#{ex}\n"
diff --git a/scripts/review_apps/gcp_cleanup.sh b/scripts/review_apps/gcp_cleanup.sh
index ea6b60ed5ff..f289a50f629 100755
--- a/scripts/review_apps/gcp_cleanup.sh
+++ b/scripts/review_apps/gcp_cleanup.sh
@@ -1,7 +1,14 @@
-#!/bin/bash
+#!/usr/bin/env bash
source scripts/utils.sh
+function setup_gcp_dependencies() {
+ apk add jq
+
+ gcloud auth activate-service-account --key-file="${REVIEW_APPS_GCP_CREDENTIALS}"
+ gcloud config set project "${REVIEW_APPS_GCP_PROJECT}"
+}
+
# These scripts require the following environment variables:
# - REVIEW_APPS_GCP_REGION - e.g `us-central1`
# - KUBE_NAMESPACE - e.g `review-apps-ee`
diff --git a/scripts/review_apps/review-apps.sh b/scripts/review_apps/review-apps.sh
index 915b4f5050b..097fe9c8cca 100755
--- a/scripts/review_apps/review-apps.sh
+++ b/scripts/review_apps/review-apps.sh
@@ -95,37 +95,6 @@ function delete_failed_release() {
fi
}
-function helm2_deploy_exists() {
- local namespace="${1}"
- local release="${2}"
- local deploy_exists
-
- echoinfo "Checking if Helm 2 ${release} exists in the ${namespace} namespace..." true
-
- kubectl get cm -l OWNER=TILLER -n ${namespace} | grep ${release} 2>&1
- deploy_exists=$?
-
- echoinfo "Helm 2 release for ${release} is ${deploy_exists}"
- return $deploy_exists
-}
-
-function delete_helm2_release() {
- local namespace="${KUBE_NAMESPACE}"
- local release="${CI_ENVIRONMENT_SLUG}"
-
- if [ -z "${release}" ]; then
- echoerr "No release given, aborting the delete!"
- return
- fi
-
- if ! helm2_deploy_exists "${namespace}" "${release}"; then
- echoinfo "No Review App with ${release} is currently deployed by Helm 2."
- else
- echoinfo "Cleaning up ${release} installed by Helm 2"
- kubectl_cleanup_release "${namespace}" "${release}"
- fi
-}
-
function get_pod() {
local namespace="${KUBE_NAMESPACE}"
local release="${CI_ENVIRONMENT_SLUG}"
@@ -267,7 +236,6 @@ function base_config_changed() {
function deploy() {
local namespace="${KUBE_NAMESPACE}"
local release="${CI_ENVIRONMENT_SLUG}"
- local edition="${GITLAB_EDITION-ce}"
local base_config_file_ref="master"
if [[ "$(base_config_changed)" == "true" ]]; then base_config_file_ref="${CI_COMMIT_SHA}"; fi
local base_config_file="https://gitlab.com/gitlab-org/gitlab/raw/${base_config_file_ref}/scripts/review_apps/base-config.yaml"
@@ -275,13 +243,13 @@ function deploy() {
echoinfo "Deploying ${release}..." true
IMAGE_REPOSITORY="registry.gitlab.com/gitlab-org/build/cng-mirror"
- gitlab_migrations_image_repository="${IMAGE_REPOSITORY}/gitlab-rails-${edition}"
- gitlab_sidekiq_image_repository="${IMAGE_REPOSITORY}/gitlab-sidekiq-${edition}"
- gitlab_unicorn_image_repository="${IMAGE_REPOSITORY}/gitlab-webservice-${edition}"
- gitlab_task_runner_image_repository="${IMAGE_REPOSITORY}/gitlab-task-runner-${edition}"
+ gitlab_migrations_image_repository="${IMAGE_REPOSITORY}/gitlab-rails-ee"
+ gitlab_sidekiq_image_repository="${IMAGE_REPOSITORY}/gitlab-sidekiq-ee"
+ gitlab_unicorn_image_repository="${IMAGE_REPOSITORY}/gitlab-webservice-ee"
+ gitlab_task_runner_image_repository="${IMAGE_REPOSITORY}/gitlab-task-runner-ee"
gitlab_gitaly_image_repository="${IMAGE_REPOSITORY}/gitaly"
gitlab_shell_image_repository="${IMAGE_REPOSITORY}/gitlab-shell"
- gitlab_workhorse_image_repository="${IMAGE_REPOSITORY}/gitlab-workhorse-${edition}"
+ gitlab_workhorse_image_repository="${IMAGE_REPOSITORY}/gitlab-workhorse-ee"
create_application_secret
@@ -290,7 +258,7 @@ HELM_CMD=$(cat << EOF
--namespace="${namespace}" \
--install \
--wait \
- --timeout 900s \
+ --timeout 15m \
--set ci.branch="${CI_COMMIT_REF_NAME}" \
--set ci.commit.sha="${CI_COMMIT_SHORT_SHA}" \
--set ci.job.url="${CI_JOB_URL}" \
@@ -304,8 +272,10 @@ HELM_CMD=$(cat << EOF
--set gitlab.gitaly.image.tag="v${GITALY_VERSION}" \
--set gitlab.gitlab-shell.image.repository="${gitlab_shell_image_repository}" \
--set gitlab.gitlab-shell.image.tag="v${GITLAB_SHELL_VERSION}" \
+ --set gitlab.sidekiq.annotations.commit="${CI_COMMIT_SHORT_SHA}" \
--set gitlab.sidekiq.image.repository="${gitlab_sidekiq_image_repository}" \
--set gitlab.sidekiq.image.tag="${CI_COMMIT_REF_SLUG}" \
+ --set gitlab.unicorn.annotations.commit="${CI_COMMIT_SHORT_SHA}" \
--set gitlab.unicorn.image.repository="${gitlab_unicorn_image_repository}" \
--set gitlab.unicorn.image.tag="${CI_COMMIT_REF_SLUG}" \
--set gitlab.unicorn.workhorse.image="${gitlab_workhorse_image_repository}" \
diff --git a/scripts/rspec_helpers.sh b/scripts/rspec_helpers.sh
index 70ddb61e588..0c9d3505ff3 100644
--- a/scripts/rspec_helpers.sh
+++ b/scripts/rspec_helpers.sh
@@ -1,4 +1,4 @@
-#!/bin/bash
+#!/usr/bin/env bash
function retrieve_tests_metadata() {
mkdir -p knapsack/ rspec_flaky/ rspec_profiling/
@@ -15,9 +15,13 @@ function retrieve_tests_metadata() {
function update_tests_metadata() {
echo "{}" > "${KNAPSACK_RSPEC_SUITE_REPORT_PATH}"
- scripts/merge-reports "${KNAPSACK_RSPEC_SUITE_REPORT_PATH}" knapsack/rspec*_pg9_*.json
+ scripts/merge-reports "${KNAPSACK_RSPEC_SUITE_REPORT_PATH}" knapsack/rspec*.json
if [[ -n "${TESTS_METADATA_S3_BUCKET}" ]]; then
- scripts/sync-reports put "${TESTS_METADATA_S3_BUCKET}" "${KNAPSACK_RSPEC_SUITE_REPORT_PATH}"
+ if [[ "$CI_PIPELINE_SOURCE" == "schedule" ]]; then
+ scripts/sync-reports put "${TESTS_METADATA_S3_BUCKET}" "${KNAPSACK_RSPEC_SUITE_REPORT_PATH}"
+ else
+ echo "Not uplaoding report to S3 as the pipeline is not a scheduled one."
+ fi
fi
rm -f knapsack/rspec*.json
@@ -28,12 +32,20 @@ function update_tests_metadata() {
scripts/flaky_examples/prune-old-flaky-examples "${FLAKY_RSPEC_SUITE_REPORT_PATH}"
if [[ -n ${TESTS_METADATA_S3_BUCKET} ]]; then
- scripts/sync-reports put "${TESTS_METADATA_S3_BUCKET}" "${FLAKY_RSPEC_SUITE_REPORT_PATH}"
+ if [[ "$CI_PIPELINE_SOURCE" == "schedule" ]]; then
+ scripts/sync-reports put "${TESTS_METADATA_S3_BUCKET}" "${FLAKY_RSPEC_SUITE_REPORT_PATH}"
+ else
+ echo "Not uploading report to S3 as the pipeline is not a scheduled one."
+ fi
fi
rm -f rspec_flaky/all_*.json rspec_flaky/new_*.json
- scripts/insert-rspec-profiling-data
+ if [[ "$CI_PIPELINE_SOURCE" == "schedule" ]]; then
+ scripts/insert-rspec-profiling-data
+ else
+ echo "Not inserting profiling data as the pipeline is not a scheduled one."
+ fi
}
function rspec_simple_job() {
@@ -41,16 +53,14 @@ function rspec_simple_job() {
export NO_KNAPSACK="1"
- scripts/gitaly-test-spawn
-
bin/rspec --color --format documentation --format RspecJunitFormatter --out junit_rspec.xml ${rspec_opts}
}
function rspec_paralellized_job() {
- read -ra job_name <<< "$CI_JOB_NAME"
+ read -ra job_name <<< "${CI_JOB_NAME}"
local test_tool="${job_name[0]}"
local test_level="${job_name[1]}"
- local database="${job_name[2]}"
+ local report_name=$(echo "${CI_JOB_NAME}" | sed -E 's|[/ ]|_|g') # e.g. 'rspec unit pg11 1/24' would become 'rspec_unit_pg11_1_24'
local rspec_opts="${1}"
local spec_folder_prefix=""
@@ -59,7 +69,13 @@ function rspec_paralellized_job() {
fi
export KNAPSACK_LOG_LEVEL="debug"
- export KNAPSACK_REPORT_PATH="knapsack/${test_tool}_${test_level}_${database}_${CI_NODE_INDEX}_${CI_NODE_TOTAL}_report.json"
+ export KNAPSACK_REPORT_PATH="knapsack/${report_name}_report.json"
+
+ # There's a bug where artifacts are sometimes not downloaded. Since specs can run without the Knapsack report, we can
+ # handle the missing artifact gracefully here. See https://gitlab.com/gitlab-org/gitlab/-/issues/212349.
+ if [[ ! -f "${KNAPSACK_RSPEC_SUITE_REPORT_PATH}" ]]; then
+ echo "{}" > "${KNAPSACK_RSPEC_SUITE_REPORT_PATH}"
+ fi
cp "${KNAPSACK_RSPEC_SUITE_REPORT_PATH}" "${KNAPSACK_REPORT_PATH}"
@@ -74,8 +90,8 @@ function rspec_paralellized_job() {
export KNAPSACK_GENERATE_REPORT="true"
export FLAKY_RSPEC_GENERATE_REPORT="true"
export SUITE_FLAKY_RSPEC_REPORT_PATH="${FLAKY_RSPEC_SUITE_REPORT_PATH}"
- export FLAKY_RSPEC_REPORT_PATH="rspec_flaky/all_${test_tool}_${CI_NODE_INDEX}_${CI_NODE_TOTAL}_report.json"
- export NEW_FLAKY_RSPEC_REPORT_PATH="rspec_flaky/new_${test_tool}_${CI_NODE_INDEX}_${CI_NODE_TOTAL}_report.json"
+ export FLAKY_RSPEC_REPORT_PATH="rspec_flaky/all_${report_name}_report.json"
+ export NEW_FLAKY_RSPEC_REPORT_PATH="rspec_flaky/new_${report_name}_report.json"
if [[ ! -f $FLAKY_RSPEC_REPORT_PATH ]]; then
echo "{}" > "${FLAKY_RSPEC_REPORT_PATH}"
@@ -86,11 +102,9 @@ function rspec_paralellized_job() {
fi
fi
- scripts/gitaly-test-spawn
-
mkdir -p tmp/memory_test
- export MEMORY_TEST_PATH="tmp/memory_test/${test_tool}_${test_level}_${database}_${CI_NODE_INDEX}_${CI_NODE_TOTAL}_memory.csv"
+ export MEMORY_TEST_PATH="tmp/memory_test/${report_name}_memory.csv"
knapsack rspec "-Ispec --color --format documentation --format RspecJunitFormatter --out junit_rspec.xml ${rspec_opts}"
diff --git a/scripts/schema_changed.sh b/scripts/schema_changed.sh
index e8c120e92e1..427e0128df7 100755
--- a/scripts/schema_changed.sh
+++ b/scripts/schema_changed.sh
@@ -2,13 +2,13 @@
schema_changed() {
if [ ! -z "$(git diff --name-only -- db/structure.sql)" ]; then
- printf "db/structure.sql after rake db:migrate:reset is different from one in the repository"
+ printf "Schema changes are not cleanly committed to db/structure.sql\n"
printf "The diff is as follows:\n"
diff=$(git diff -p --binary -- db/structure.sql)
printf "%s" "$diff"
exit 1
else
- printf "db/structure.sql after rake db:migrate:reset matches one in the repository"
+ printf "Schema changes are correctly applied to db/structure.sql\n"
fi
}
diff --git a/scripts/security-harness b/scripts/security-harness
index c101cd03454..b9492e16066 100755
--- a/scripts/security-harness
+++ b/scripts/security-harness
@@ -19,7 +19,7 @@ end
HOOK_PATH = File.expand_path("../.git/hooks/pre-push", __dir__)
HOOK_DATA = <<~HOOK
- #!/bin/bash
+ #!/usr/bin/env bash
set -e
diff --git a/scripts/utils.sh b/scripts/utils.sh
index d1f98bb3f62..897f8d5a8b8 100644
--- a/scripts/utils.sh
+++ b/scripts/utils.sh
@@ -18,11 +18,8 @@ function setup_db_user_only() {
}
function setup_db() {
- setup_db_user_only
-
- bundle exec rake db:drop db:create db:structure:load db:migrate
-
- bundle exec rake gitlab:db:setup_ee
+ run_timed_command "setup_db_user_only"
+ run_timed_command "bundle exec rake db:drop db:create db:structure:load db:migrate gitlab:db:setup_ee"
}
function install_api_client_dependencies_with_apk() {
@@ -38,6 +35,24 @@ function install_gitlab_gem() {
gem install gitlab --no-document --version 4.13.0
}
+function run_timed_command() {
+ local cmd="${1}"
+ local start=$(date +%s)
+ echosuccess "\$ ${cmd}"
+ eval "${cmd}"
+ local ret=$?
+ local end=$(date +%s)
+ local runtime=$((end-start))
+
+ if [[ $ret -eq 0 ]]; then
+ echosuccess "==> '${cmd}' succeeded in ${runtime} seconds."
+ return 0
+ else
+ echoerr "==> '${cmd}' failed (${ret}) in ${runtime} seconds."
+ return $ret
+ fi
+}
+
function echoerr() {
local header="${2}"
@@ -58,6 +73,16 @@ function echoinfo() {
fi
}
+function echosuccess() {
+ local header="${2}"
+
+ if [ -n "${header}" ]; then
+ printf "\n\033[0;32m** %s **\n\033[0m" "${1}" >&2;
+ else
+ printf "\033[0;32m%s\n\033[0m" "${1}" >&2;
+ fi
+}
+
function get_job_id() {
local job_name="${1}"
local query_string="${2:+&${2}}"
diff --git a/spec/channels/application_cable/connection_spec.rb b/spec/channels/application_cable/connection_spec.rb
new file mode 100644
index 00000000000..f3d67133528
--- /dev/null
+++ b/spec/channels/application_cable/connection_spec.rb
@@ -0,0 +1,47 @@
+# frozen_string_literal: true
+
+require 'spec_helper'
+
+describe ApplicationCable::Connection, :clean_gitlab_redis_shared_state do
+ let(:session_id) { Rack::Session::SessionId.new('6919a6f1bb119dd7396fadc38fd18d0d') }
+
+ before do
+ Gitlab::Redis::SharedState.with do |redis|
+ redis.set("session:gitlab:#{session_id.private_id}", Marshal.dump(session_hash))
+ end
+
+ cookies[Gitlab::Application.config.session_options[:key]] = session_id.public_id
+ end
+
+ context 'when user is logged in' do
+ let(:user) { create(:user) }
+ let(:session_hash) { { 'warden.user.user.key' => [[user.id], user.encrypted_password[0, 29]] } }
+
+ it 'sets current_user' do
+ connect
+
+ expect(connection.current_user).to eq(user)
+ end
+
+ context 'with a stale password' do
+ let(:partial_password_hash) { build(:user, password: 'some_old_password').encrypted_password[0, 29] }
+ let(:session_hash) { { 'warden.user.user.key' => [[user.id], partial_password_hash] } }
+
+ it 'sets current_user to nil' do
+ connect
+
+ expect(connection.current_user).to be_nil
+ end
+ end
+ end
+
+ context 'when user is not logged in' do
+ let(:session_hash) { {} }
+
+ it 'sets current_user to nil' do
+ connect
+
+ expect(connection.current_user).to be_nil
+ end
+ end
+end
diff --git a/spec/channels/issues_channel_spec.rb b/spec/channels/issues_channel_spec.rb
new file mode 100644
index 00000000000..1c88cc73456
--- /dev/null
+++ b/spec/channels/issues_channel_spec.rb
@@ -0,0 +1,36 @@
+# frozen_string_literal: true
+
+require 'spec_helper'
+
+describe IssuesChannel do
+ let_it_be(:issue) { create(:issue) }
+
+ it 'rejects when project path is invalid' do
+ subscribe(project_path: 'invalid_project_path', iid: issue.iid)
+
+ expect(subscription).to be_rejected
+ end
+
+ it 'rejects when iid is invalid' do
+ subscribe(project_path: issue.project.full_path, iid: non_existing_record_iid)
+
+ expect(subscription).to be_rejected
+ end
+
+ it 'rejects when the user does not have access' do
+ stub_connection current_user: nil
+
+ subscribe(project_path: issue.project.full_path, iid: issue.iid)
+
+ expect(subscription).to be_rejected
+ end
+
+ it 'subscribes to a stream when the user has access' do
+ stub_connection current_user: issue.author
+
+ subscribe(project_path: issue.project.full_path, iid: issue.iid)
+
+ expect(subscription).to be_confirmed
+ expect(subscription).to have_stream_for(issue)
+ end
+end
diff --git a/spec/config/application_spec.rb b/spec/config/application_spec.rb
index 994cea4c84f..e6b8da690a2 100644
--- a/spec/config/application_spec.rb
+++ b/spec/config/application_spec.rb
@@ -5,7 +5,7 @@ require 'spec_helper'
describe Gitlab::Application do # rubocop:disable RSpec/FilePath
using RSpec::Parameterized::TableSyntax
- FILTERED_PARAM = ActiveSupport::ParameterFilter::FILTERED
+ filtered_param = ActiveSupport::ParameterFilter::FILTERED
context 'when parameters are logged' do
describe 'rails does not leak confidential parameters' do
@@ -19,11 +19,11 @@ describe Gitlab::Application do # rubocop:disable RSpec/FilePath
where(:input_url, :output_query) do
'/' | {}
'/?safe=1' | { 'safe' => '1' }
- '/?private_token=secret' | { 'private_token' => FILTERED_PARAM }
- '/?mixed=1&private_token=secret' | { 'mixed' => '1', 'private_token' => FILTERED_PARAM }
- '/?note=secret&noteable=1&prefix_note=2' | { 'note' => FILTERED_PARAM, 'noteable' => '1', 'prefix_note' => '2' }
- '/?note[note]=secret&target_type=1' | { 'note' => FILTERED_PARAM, 'target_type' => '1' }
- '/?safe[note]=secret&target_type=1' | { 'safe' => { 'note' => FILTERED_PARAM }, 'target_type' => '1' }
+ '/?private_token=secret' | { 'private_token' => filtered_param }
+ '/?mixed=1&private_token=secret' | { 'mixed' => '1', 'private_token' => filtered_param }
+ '/?note=secret&noteable=1&prefix_note=2' | { 'note' => filtered_param, 'noteable' => '1', 'prefix_note' => '2' }
+ '/?note[note]=secret&target_type=1' | { 'note' => filtered_param, 'target_type' => '1' }
+ '/?safe[note]=secret&target_type=1' | { 'safe' => { 'note' => filtered_param }, 'target_type' => '1' }
end
with_them do
diff --git a/spec/config/mail_room_spec.rb b/spec/config/mail_room_spec.rb
index fcef4e7a9b0..bd8269fb2c5 100644
--- a/spec/config/mail_room_spec.rb
+++ b/spec/config/mail_room_spec.rb
@@ -53,7 +53,8 @@ describe 'mail_room.yml' do
email: 'gitlab-incoming@gmail.com',
password: '[REDACTED]',
name: 'inbox',
- idle_timeout: 60
+ idle_timeout: 60,
+ expunge_deleted: true
}
expected_options = {
redis_url: gitlab_redis_queues.url,
diff --git a/spec/config/smime_signature_settings_spec.rb b/spec/config/smime_signature_settings_spec.rb
index 4f076a92b16..7e7b42b129a 100644
--- a/spec/config/smime_signature_settings_spec.rb
+++ b/spec/config/smime_signature_settings_spec.rb
@@ -6,6 +6,7 @@ describe SmimeSignatureSettings do
describe '.parse' do
let(:default_smime_key) { Rails.root.join('.gitlab_smime_key') }
let(:default_smime_cert) { Rails.root.join('.gitlab_smime_cert') }
+ let(:default_smime_ca_certs) { nil }
it 'sets correct default values to disabled' do
parsed_settings = described_class.parse(nil)
@@ -13,6 +14,7 @@ describe SmimeSignatureSettings do
expect(parsed_settings['enabled']).to be(false)
expect(parsed_settings['key_file']).to eq(default_smime_key)
expect(parsed_settings['cert_file']).to eq(default_smime_cert)
+ expect(parsed_settings['ca_certs_file']).to eq(default_smime_ca_certs)
end
context 'when providing custom values' do
@@ -24,6 +26,7 @@ describe SmimeSignatureSettings do
expect(parsed_settings['enabled']).to be(false)
expect(parsed_settings['key_file']).to eq(default_smime_key)
expect(parsed_settings['cert_file']).to eq(default_smime_cert)
+ expect(parsed_settings['ca_certs_file']).to eq(default_smime_ca_certs)
end
it 'enables smime with default key and cert' do
@@ -36,15 +39,18 @@ describe SmimeSignatureSettings do
expect(parsed_settings['enabled']).to be(true)
expect(parsed_settings['key_file']).to eq(default_smime_key)
expect(parsed_settings['cert_file']).to eq(default_smime_cert)
+ expect(parsed_settings['ca_certs_file']).to eq(default_smime_ca_certs)
end
it 'enables smime with custom key and cert' do
custom_key = '/custom/key'
custom_cert = '/custom/cert'
+ custom_ca_certs = '/custom/ca_certs'
custom_settings = Settingslogic.new({
'enabled' => true,
'key_file' => custom_key,
- 'cert_file' => custom_cert
+ 'cert_file' => custom_cert,
+ 'ca_certs_file' => custom_ca_certs
})
parsed_settings = described_class.parse(custom_settings)
@@ -52,6 +58,7 @@ describe SmimeSignatureSettings do
expect(parsed_settings['enabled']).to be(true)
expect(parsed_settings['key_file']).to eq(custom_key)
expect(parsed_settings['cert_file']).to eq(custom_cert)
+ expect(parsed_settings['ca_certs_file']).to eq(custom_ca_certs)
end
end
end
diff --git a/spec/controllers/admin/ci/variables_controller_spec.rb b/spec/controllers/admin/ci/variables_controller_spec.rb
new file mode 100644
index 00000000000..57f2dd21f39
--- /dev/null
+++ b/spec/controllers/admin/ci/variables_controller_spec.rb
@@ -0,0 +1,70 @@
+# frozen_string_literal: true
+
+require 'spec_helper'
+
+describe Admin::Ci::VariablesController do
+ let_it_be(:variable) { create(:ci_instance_variable) }
+
+ before do
+ sign_in(user)
+ end
+
+ describe 'GET #show' do
+ subject do
+ get :show, params: {}, format: :json
+ end
+
+ context 'when signed in as admin' do
+ let(:user) { create(:admin) }
+
+ include_examples 'GET #show lists all variables'
+ end
+
+ context 'when signed in as regular user' do
+ let(:user) { create(:user) }
+
+ it 'returns 404' do
+ subject
+
+ expect(response).to have_gitlab_http_status(:not_found)
+ end
+ end
+ end
+
+ describe 'PATCH #update' do
+ subject do
+ patch :update,
+ params: {
+ variables_attributes: variables_attributes
+ },
+ format: :json
+ end
+
+ context 'when signed in as admin' do
+ let(:user) { create(:admin) }
+
+ include_examples 'PATCH #update updates variables' do
+ let(:variables_scope) { Ci::InstanceVariable.all }
+ let(:file_variables_scope) { variables_scope.file }
+ end
+ end
+
+ context 'when signed in as regular user' do
+ let(:user) { create(:user) }
+
+ let(:variables_attributes) do
+ [{
+ id: variable.id,
+ key: variable.key,
+ secret_value: 'new value'
+ }]
+ end
+
+ it 'returns 404' do
+ subject
+
+ expect(response).to have_gitlab_http_status(:not_found)
+ end
+ end
+ end
+end
diff --git a/spec/controllers/admin/clusters_controller_spec.rb b/spec/controllers/admin/clusters_controller_spec.rb
index bd6d5614ccd..d4a12e0dc52 100644
--- a/spec/controllers/admin/clusters_controller_spec.rb
+++ b/spec/controllers/admin/clusters_controller_spec.rb
@@ -27,7 +27,7 @@ describe Admin::ClustersController do
create(:cluster, :disabled, :provided_by_gcp, :production_environment, :instance)
end
- it 'lists available clusters' do
+ it 'lists available clusters and displays html' do
get_index
expect(response).to have_gitlab_http_status(:ok)
@@ -35,20 +35,39 @@ describe Admin::ClustersController do
expect(assigns(:clusters)).to match_array([enabled_cluster, disabled_cluster])
end
+ it 'lists available clusters and renders json serializer' do
+ get_index(format: :json)
+
+ expect(response).to have_gitlab_http_status(:ok)
+ expect(response).to match_response_schema('cluster_list')
+ end
+
context 'when page is specified' do
let(:last_page) { Clusters::Cluster.instance_type.page.total_pages }
+ let(:total_count) { Clusters::Cluster.instance_type.page.total_count }
before do
- allow(Clusters::Cluster).to receive(:paginates_per).and_return(1)
- create_list(:cluster, 2, :provided_by_gcp, :production_environment, :instance)
+ create_list(:cluster, 30, :provided_by_gcp, :production_environment, :instance)
end
it 'redirects to the page' do
+ expect(last_page).to be > 1
+
get_index(page: last_page)
expect(response).to have_gitlab_http_status(:ok)
expect(assigns(:clusters).current_page).to eq(last_page)
end
+
+ it 'displays cluster list for associated page' do
+ expect(last_page).to be > 1
+
+ get_index(page: last_page, format: :json)
+
+ expect(response).to have_gitlab_http_status(:ok)
+ expect(response.headers['X-Page'].to_i).to eq(last_page)
+ expect(response.headers['X-Total'].to_i).to eq(total_count)
+ end
end
end
diff --git a/spec/controllers/admin/requests_profiles_controller_spec.rb b/spec/controllers/admin/requests_profiles_controller_spec.rb
index 13123c8e486..629233b04e7 100644
--- a/spec/controllers/admin/requests_profiles_controller_spec.rb
+++ b/spec/controllers/admin/requests_profiles_controller_spec.rb
@@ -27,7 +27,7 @@ describe Admin::RequestsProfilesController do
end
context 'when loading HTML profile' do
- let(:basename) { "profile_#{Time.now.to_i}_execution.html" }
+ let(:basename) { "profile_#{Time.current.to_i}_execution.html" }
let(:sample_data) do
'<html> <body> <h1>Heading</h1> <p>paragraph.</p> </body> </html>'
@@ -42,7 +42,7 @@ describe Admin::RequestsProfilesController do
end
context 'when loading TXT profile' do
- let(:basename) { "profile_#{Time.now.to_i}_memory.txt" }
+ let(:basename) { "profile_#{Time.current.to_i}_memory.txt" }
let(:sample_data) do
<<~TXT
@@ -60,7 +60,7 @@ describe Admin::RequestsProfilesController do
end
context 'when loading PDF profile' do
- let(:basename) { "profile_#{Time.now.to_i}_anything.pdf" }
+ let(:basename) { "profile_#{Time.current.to_i}_anything.pdf" }
let(:sample_data) { 'mocked pdf content' }
diff --git a/spec/controllers/admin/users_controller_spec.rb b/spec/controllers/admin/users_controller_spec.rb
index 387fc0407b6..7a7201a6454 100644
--- a/spec/controllers/admin/users_controller_spec.rb
+++ b/spec/controllers/admin/users_controller_spec.rb
@@ -296,7 +296,7 @@ describe Admin::UsersController do
it 'sets the new password to expire immediately' do
expect { update_password(user, 'AValidPassword1') }
- .to change { user.reload.password_expires_at }.to be_within(2.seconds).of(Time.now)
+ .to change { user.reload.password_expires_at }.to be_within(2.seconds).of(Time.current)
end
end
diff --git a/spec/controllers/application_controller_spec.rb b/spec/controllers/application_controller_spec.rb
index 2a913069acc..ed2e61d6cf6 100644
--- a/spec/controllers/application_controller_spec.rb
+++ b/spec/controllers/application_controller_spec.rb
@@ -14,7 +14,7 @@ describe ApplicationController do
end
it 'redirects if the user is over their password expiry' do
- user.password_expires_at = Time.new(2002)
+ user.password_expires_at = Time.zone.local(2002)
expect(user.ldap_user?).to be_falsey
allow(controller).to receive(:current_user).and_return(user)
@@ -25,7 +25,7 @@ describe ApplicationController do
end
it 'does not redirect if the user is under their password expiry' do
- user.password_expires_at = Time.now + 20010101
+ user.password_expires_at = Time.current + 20010101
expect(user.ldap_user?).to be_falsey
allow(controller).to receive(:current_user).and_return(user)
@@ -35,7 +35,7 @@ describe ApplicationController do
end
it 'does not redirect if the user is over their password expiry but they are an ldap user' do
- user.password_expires_at = Time.new(2002)
+ user.password_expires_at = Time.zone.local(2002)
allow(user).to receive(:ldap_user?).and_return(true)
allow(controller).to receive(:current_user).and_return(user)
@@ -47,7 +47,7 @@ describe ApplicationController do
it 'does not redirect if the user is over their password expiry but password authentication is disabled for the web interface' do
stub_application_setting(password_authentication_enabled_for_web: false)
stub_application_setting(password_authentication_enabled_for_git: false)
- user.password_expires_at = Time.new(2002)
+ user.password_expires_at = Time.zone.local(2002)
allow(controller).to receive(:current_user).and_return(user)
expect(controller).not_to receive(:redirect_to)
@@ -530,6 +530,14 @@ describe ApplicationController do
expect(controller.last_payload).to include('correlation_id' => 'new-id')
end
+
+ it 'adds context metadata to the payload' do
+ sign_in user
+
+ get :index
+
+ expect(controller.last_payload[:metadata]).to include('meta.user' => user.username)
+ end
end
describe '#access_denied' do
@@ -891,7 +899,7 @@ describe ApplicationController do
end
it 'sets the group if it was available' do
- group = build(:group)
+ group = build_stubbed(:group)
controller.instance_variable_set(:@group, group)
get :index, format: :json
@@ -900,7 +908,7 @@ describe ApplicationController do
end
it 'sets the project if one was available' do
- project = build(:project)
+ project = build_stubbed(:project)
controller.instance_variable_set(:@project, project)
get :index, format: :json
@@ -913,5 +921,58 @@ describe ApplicationController do
expect(json_response['meta.caller_id']).to eq('AnonymousController#index')
end
+
+ it 'assigns the context to a variable for logging' do
+ get :index, format: :json
+
+ expect(assigns(:current_context)).to include('meta.user' => user.username)
+ end
+
+ it 'assigns the context when the action caused an error' do
+ allow(controller).to receive(:index) { raise 'Broken' }
+
+ expect { get :index, format: :json }.to raise_error('Broken')
+
+ expect(assigns(:current_context)).to include('meta.user' => user.username)
+ end
+ end
+
+ describe '#current_user' do
+ controller(described_class) do
+ def index; end
+ end
+
+ let_it_be(:impersonator) { create(:user) }
+ let_it_be(:user) { create(:user) }
+
+ before do
+ sign_in(user)
+ end
+
+ context 'when being impersonated' do
+ before do
+ allow(controller).to receive(:session).and_return({ impersonator_id: impersonator.id })
+ end
+
+ it 'returns a User with impersonator', :aggregate_failures do
+ get :index
+
+ expect(controller.current_user).to be_a(User)
+ expect(controller.current_user.impersonator).to eq(impersonator)
+ end
+ end
+
+ context 'when not being impersonated' do
+ before do
+ allow(controller).to receive(:session).and_return({})
+ end
+
+ it 'returns a User', :aggregate_failures do
+ get :index
+
+ expect(controller.current_user).to be_a(User)
+ expect(controller.current_user.impersonator).to be_nil
+ end
+ end
end
end
diff --git a/spec/controllers/concerns/issuable_actions_spec.rb b/spec/controllers/concerns/issuable_actions_spec.rb
index 7b0b4497f3f..2ab46992b99 100644
--- a/spec/controllers/concerns/issuable_actions_spec.rb
+++ b/spec/controllers/concerns/issuable_actions_spec.rb
@@ -14,7 +14,7 @@ describe IssuableActions do
klass = Class.new do
attr_reader :current_user, :project, :issuable
- def self.before_action(action, params = nil)
+ def self.before_action(action = nil, params = nil)
end
include IssuableActions
diff --git a/spec/controllers/concerns/metrics_dashboard_spec.rb b/spec/controllers/concerns/metrics_dashboard_spec.rb
index 4e42171e3d3..e2fa03670d9 100644
--- a/spec/controllers/concerns/metrics_dashboard_spec.rb
+++ b/spec/controllers/concerns/metrics_dashboard_spec.rb
@@ -45,7 +45,7 @@ describe MetricsDashboard do
it 'returns the specified dashboard' do
expect(json_response['dashboard']['dashboard']).to eq('Environment metrics')
expect(json_response).not_to have_key('all_dashboards')
- expect(json_response).not_to have_key('metrics_data')
+ expect(json_response).to have_key('metrics_data')
end
context 'when the params are in an alternate format' do
@@ -54,7 +54,7 @@ describe MetricsDashboard do
it 'returns the specified dashboard' do
expect(json_response['dashboard']['dashboard']).to eq('Environment metrics')
expect(json_response).not_to have_key('all_dashboards')
- expect(json_response).not_to have_key('metrics_data')
+ expect(json_response).to have_key('metrics_data')
end
end
@@ -114,6 +114,35 @@ describe MetricsDashboard do
end
end
end
+
+ context 'starred dashboards' do
+ let_it_be(:dashboard_yml) { fixture_file('lib/gitlab/metrics/dashboard/sample_dashboard.yml') }
+ let_it_be(:dashboards) do
+ {
+ '.gitlab/dashboards/test.yml' => dashboard_yml,
+ '.gitlab/dashboards/anomaly.yml' => dashboard_yml,
+ '.gitlab/dashboards/errors.yml' => dashboard_yml
+ }
+ end
+ let_it_be(:project) { create(:project, :custom_repo, files: dashboards) }
+
+ before do
+ create(:metrics_users_starred_dashboard, user: user, project: project, dashboard_path: '.gitlab/dashboards/errors.yml')
+ create(:metrics_users_starred_dashboard, user: user, project: project, dashboard_path: '.gitlab/dashboards/test.yml')
+ end
+
+ it 'adds starred dashboard information and sorts the list' do
+ all_dashboards = json_response['all_dashboards'].map { |dashboard| dashboard.slice('display_name', 'starred', 'user_starred_path') }
+ expected_response = [
+ { "display_name" => "Default", "starred" => false, 'user_starred_path' => api_v4_projects_metrics_user_starred_dashboards_path(id: project.id, params: { dashboard_path: 'config/prometheus/common_metrics.yml' }) },
+ { "display_name" => "anomaly.yml", "starred" => false, 'user_starred_path' => api_v4_projects_metrics_user_starred_dashboards_path(id: project.id, params: { dashboard_path: '.gitlab/dashboards/anomaly.yml' }) },
+ { "display_name" => "errors.yml", "starred" => true, 'user_starred_path' => api_v4_projects_metrics_user_starred_dashboards_path(id: project.id, params: { dashboard_path: '.gitlab/dashboards/errors.yml' }) },
+ { "display_name" => "test.yml", "starred" => true, 'user_starred_path' => api_v4_projects_metrics_user_starred_dashboards_path(id: project.id, params: { dashboard_path: '.gitlab/dashboards/test.yml' }) }
+ ]
+
+ expect(all_dashboards).to eql expected_response
+ end
+ end
end
end
end
diff --git a/spec/controllers/dashboard/projects_controller_spec.rb b/spec/controllers/dashboard/projects_controller_spec.rb
index a13b56deb23..eeac696c3f2 100644
--- a/spec/controllers/dashboard/projects_controller_spec.rb
+++ b/spec/controllers/dashboard/projects_controller_spec.rb
@@ -86,11 +86,58 @@ describe Dashboard::ProjectsController do
end
describe 'GET /starred.json' do
+ subject { get :starred, format: :json }
+
+ let(:projects) { create_list(:project, 2, creator: user) }
+
before do
- get :starred, format: :json
+ allow(Kaminari.config).to receive(:default_per_page).and_return(1)
+
+ projects.each do |project|
+ project.add_developer(user)
+ create(:users_star_project, project_id: project.id, user_id: user.id)
+ end
end
- it { is_expected.to respond_with(:success) }
+ it 'returns success' do
+ subject
+
+ expect(response).to have_gitlab_http_status(:ok)
+ end
+
+ it 'paginates the records' do
+ subject
+
+ expect(assigns(:projects).count).to eq(1)
+ end
+ end
+ end
+
+ context 'atom requests' do
+ let(:user) { create(:user) }
+
+ before do
+ sign_in(user)
+ end
+
+ describe '#index' do
+ context 'project pagination' do
+ let(:projects) { create_list(:project, 2, creator: user) }
+
+ before do
+ allow(Kaminari.config).to receive(:default_per_page).and_return(1)
+
+ projects.each do |project|
+ project.add_developer(user)
+ end
+ end
+
+ it 'does not paginate projects, even if normally restricted by pagination' do
+ get :index, format: :atom
+
+ expect(assigns(:events).count).to eq(2)
+ end
+ end
end
end
end
diff --git a/spec/controllers/google_api/authorizations_controller_spec.rb b/spec/controllers/google_api/authorizations_controller_spec.rb
index 58bda2bd4e8..9d0e0d92978 100644
--- a/spec/controllers/google_api/authorizations_controller_spec.rb
+++ b/spec/controllers/google_api/authorizations_controller_spec.rb
@@ -12,10 +12,6 @@ describe GoogleApi::AuthorizationsController do
before do
sign_in(user)
-
- allow_next_instance_of(GoogleApi::CloudPlatform::Client) do |instance|
- allow(instance).to receive(:get_token).and_return([token, expires_at])
- end
end
shared_examples_for 'access denied' do
@@ -38,6 +34,12 @@ describe GoogleApi::AuthorizationsController do
context 'session key matches state param' do
let(:state) { session_key }
+ before do
+ allow_next_instance_of(GoogleApi::CloudPlatform::Client) do |instance|
+ allow(instance).to receive(:get_token).and_return([token, expires_at])
+ end
+ end
+
it 'sets token and expires_at in session' do
subject
@@ -63,6 +65,22 @@ describe GoogleApi::AuthorizationsController do
it_behaves_like 'access denied'
end
+
+ context 'when a Faraday exception occurs' do
+ let(:state) { session_key }
+
+ [::Faraday::TimeoutError, ::Faraday::ConnectionFailed].each do |error|
+ it "sets a flash alert on #{error}" do
+ allow_next_instance_of(GoogleApi::CloudPlatform::Client) do |instance|
+ allow(instance).to receive(:get_token).and_raise(error.new(nil))
+ end
+
+ subject
+
+ expect(flash[:alert]).to eq('Timeout connecting to the Google API. Please try again.')
+ end
+ end
+ end
end
context 'state param is present, but session key is blank' do
diff --git a/spec/controllers/graphql_controller_spec.rb b/spec/controllers/graphql_controller_spec.rb
index 06a949471a7..68150504fe3 100644
--- a/spec/controllers/graphql_controller_spec.rb
+++ b/spec/controllers/graphql_controller_spec.rb
@@ -3,6 +3,8 @@
require 'spec_helper'
describe GraphqlController do
+ include GraphqlHelpers
+
before do
stub_feature_flags(graphql: true)
end
@@ -30,7 +32,7 @@ describe GraphqlController do
describe 'POST #execute' do
context 'when user is logged in' do
- let(:user) { create(:user) }
+ let(:user) { create(:user, last_activity_on: Date.yesterday) }
before do
sign_in(user)
@@ -54,6 +56,19 @@ describe GraphqlController do
expect(response).to have_gitlab_http_status(:forbidden)
expect(response).to render_template('errors/access_denied')
end
+
+ it 'updates the users last_activity_on field' do
+ expect { post :execute }.to change { user.reload.last_activity_on }
+ end
+ end
+
+ context 'when user uses an API token' do
+ let(:user) { create(:user, last_activity_on: Date.yesterday) }
+ let(:token) { create(:personal_access_token, user: user, scopes: [:api]) }
+
+ it 'updates the users last_activity_on field' do
+ expect { post :execute, params: { access_token: token.token } }.to change { user.reload.last_activity_on }
+ end
end
context 'when user is not logged in' do
@@ -64,4 +79,52 @@ describe GraphqlController do
end
end
end
+
+ describe 'Admin Mode' do
+ let(:admin) { create(:admin) }
+ let(:project) { create(:project) }
+ let(:graphql_query) { graphql_query_for('project', { 'fullPath' => project.full_path }, %w(id name)) }
+
+ before do
+ sign_in(admin)
+ end
+
+ context 'when admin mode enabled' do
+ before do
+ Gitlab::Session.with_session(controller.session) do
+ controller.current_user_mode.request_admin_mode!
+ controller.current_user_mode.enable_admin_mode!(password: admin.password)
+ end
+ end
+
+ it 'can query project data' do
+ post :execute, params: { query: graphql_query }
+
+ expect(controller.current_user_mode.admin_mode?).to be(true)
+ expect(json_response['data']['project']['name']).to eq(project.name)
+ end
+ end
+
+ context 'when admin mode disabled' do
+ it 'cannot query project data' do
+ post :execute, params: { query: graphql_query }
+
+ expect(controller.current_user_mode.admin_mode?).to be(false)
+ expect(json_response['data']['project']).to be_nil
+ end
+
+ context 'when admin is member of the project' do
+ before do
+ project.add_developer(admin)
+ end
+
+ it 'can query project data' do
+ post :execute, params: { query: graphql_query }
+
+ expect(controller.current_user_mode.admin_mode?).to be(false)
+ expect(json_response['data']['project']['name']).to eq(project.name)
+ end
+ end
+ end
+ end
end
diff --git a/spec/controllers/groups/clusters_controller_spec.rb b/spec/controllers/groups/clusters_controller_spec.rb
index 28a174560dd..1f2f6bd811b 100644
--- a/spec/controllers/groups/clusters_controller_spec.rb
+++ b/spec/controllers/groups/clusters_controller_spec.rb
@@ -32,7 +32,7 @@ describe Groups::ClustersController do
create(:cluster, :disabled, :provided_by_gcp, :production_environment, cluster_type: :group_type, groups: [group])
end
- it 'lists available clusters' do
+ it 'lists available clusters and renders html' do
go
expect(response).to have_gitlab_http_status(:ok)
@@ -40,20 +40,39 @@ describe Groups::ClustersController do
expect(assigns(:clusters)).to match_array([enabled_cluster, disabled_cluster])
end
+ it 'lists available clusters with json serializer' do
+ go(format: :json)
+
+ expect(response).to have_gitlab_http_status(:ok)
+ expect(response).to match_response_schema('cluster_list')
+ end
+
context 'when page is specified' do
let(:last_page) { group.clusters.page.total_pages }
+ let(:total_count) { group.clusters.page.total_count }
before do
- allow(Clusters::Cluster).to receive(:paginates_per).and_return(1)
- create_list(:cluster, 2, :provided_by_gcp, :production_environment, cluster_type: :group_type, groups: [group])
+ create_list(:cluster, 30, :provided_by_gcp, :production_environment, cluster_type: :group_type, groups: [group])
end
it 'redirects to the page' do
+ expect(last_page).to be > 1
+
go(page: last_page)
expect(response).to have_gitlab_http_status(:ok)
expect(assigns(:clusters).current_page).to eq(last_page)
end
+
+ it 'displays cluster list for associated page' do
+ expect(last_page).to be > 1
+
+ go(page: last_page, format: :json)
+
+ expect(response).to have_gitlab_http_status(:ok)
+ expect(response.headers['X-Page'].to_i).to eq(last_page)
+ expect(response.headers['X-Total'].to_i).to eq(total_count)
+ end
end
end
diff --git a/spec/controllers/groups/group_links_controller_spec.rb b/spec/controllers/groups/group_links_controller_spec.rb
index 21169188386..ca430414d17 100644
--- a/spec/controllers/groups/group_links_controller_spec.rb
+++ b/spec/controllers/groups/group_links_controller_spec.rb
@@ -99,18 +99,6 @@ describe Groups::GroupLinksController do
expect(flash[:alert]).to eq('error')
end
end
-
- context 'when feature flag is disabled' do
- before do
- stub_feature_flags(share_group_with_group: false)
- end
-
- it 'renders 404' do
- subject
-
- expect(response).to have_gitlab_http_status(:not_found)
- end
- end
end
context 'when user does not have access to the group' do
@@ -184,18 +172,6 @@ describe Groups::GroupLinksController do
expect(response).to have_gitlab_http_status(:not_found)
end
end
-
- context 'when feature flag is disabled' do
- before do
- stub_feature_flags(share_group_with_group: false)
- end
-
- it 'renders 404' do
- subject
-
- expect(response).to have_gitlab_http_status(:not_found)
- end
- end
end
describe '#destroy' do
@@ -231,17 +207,5 @@ describe Groups::GroupLinksController do
expect(response).to have_gitlab_http_status(:not_found)
end
end
-
- context 'when feature flag is disabled' do
- before do
- stub_feature_flags(share_group_with_group: false)
- end
-
- it 'renders 404' do
- subject
-
- expect(response).to have_gitlab_http_status(:not_found)
- end
- end
end
end
diff --git a/spec/controllers/groups/registry/repositories_controller_spec.rb b/spec/controllers/groups/registry/repositories_controller_spec.rb
index a84664c6c04..7b78aeadbd8 100644
--- a/spec/controllers/groups/registry/repositories_controller_spec.rb
+++ b/spec/controllers/groups/registry/repositories_controller_spec.rb
@@ -6,12 +6,13 @@ describe Groups::Registry::RepositoriesController do
let_it_be(:user) { create(:user) }
let_it_be(:guest) { create(:user) }
let_it_be(:group, reload: true) { create(:group) }
+ let(:additional_parameters) { {} }
subject do
- get :index, params: {
+ get :index, params: additional_parameters.merge({
group_id: group,
format: format
- }
+ })
end
before do
@@ -36,6 +37,25 @@ describe Groups::Registry::RepositoriesController do
end
end
+ shared_examples 'with name parameter' do
+ let_it_be(:project) { create(:project, group: test_group) }
+ let_it_be(:repo) { create(:container_repository, project: project, name: 'my_searched_image') }
+ let_it_be(:another_repo) { create(:container_repository, project: project, name: 'bar') }
+
+ let(:additional_parameters) { { name: 'my_searched_image' } }
+
+ it 'returns the searched repo' do
+ subject
+
+ expect(response).to have_gitlab_http_status(:ok)
+ expect(json_response.length).to eq 1
+ expect(json_response.first).to include(
+ 'id' => repo.id,
+ 'name' => repo.name
+ )
+ end
+ end
+
shared_examples 'renders correctly' do
context 'when user has access to registry' do
let_it_be(:test_group) { group }
@@ -64,6 +84,8 @@ describe Groups::Registry::RepositoriesController do
it_behaves_like 'renders a list of repositories'
+ it_behaves_like 'with name parameter'
+
it_behaves_like 'a gitlab tracking event', described_class.name, 'list_repositories'
context 'with project in subgroup' do
@@ -71,6 +93,8 @@ describe Groups::Registry::RepositoriesController do
it_behaves_like 'renders a list of repositories'
+ it_behaves_like 'with name parameter'
+
context 'with project in subgroup and group' do
let_it_be(:repo_in_test_group) { create_project_with_repo(test_group) }
let_it_be(:repo_in_group) { create_project_with_repo(group) }
@@ -81,6 +105,8 @@ describe Groups::Registry::RepositoriesController do
expect(json_response).to be_kind_of(Array)
expect(json_response.length).to eq 2
end
+
+ it_behaves_like 'with name parameter'
end
end
end
diff --git a/spec/controllers/groups/settings/integrations_controller_spec.rb b/spec/controllers/groups/settings/integrations_controller_spec.rb
index 76cd74de183..29c93c621bd 100644
--- a/spec/controllers/groups/settings/integrations_controller_spec.rb
+++ b/spec/controllers/groups/settings/integrations_controller_spec.rb
@@ -27,7 +27,7 @@ describe Groups::Settings::IntegrationsController do
context 'when group_level_integrations not enabled' do
it 'returns not_found' do
- stub_feature_flags(group_level_integrations: { enabled: false, thing: group })
+ stub_feature_flags(group_level_integrations: false)
get :index, params: { group_id: group }
@@ -60,7 +60,7 @@ describe Groups::Settings::IntegrationsController do
context 'when group_level_integrations not enabled' do
it 'returns not_found' do
- stub_feature_flags(group_level_integrations: { enabled: false, thing: group })
+ stub_feature_flags(group_level_integrations: false)
get :edit, params: { group_id: group, id: Service.available_services_names.sample }
diff --git a/spec/controllers/groups/settings/repository_controller_spec.rb b/spec/controllers/groups/settings/repository_controller_spec.rb
index 20070fb17a0..9523d404538 100644
--- a/spec/controllers/groups/settings/repository_controller_spec.rb
+++ b/spec/controllers/groups/settings/repository_controller_spec.rb
@@ -15,7 +15,7 @@ describe Groups::Settings::RepositoryController do
describe 'POST create_deploy_token' do
context 'when ajax_new_deploy_token feature flag is disabled for the project' do
before do
- stub_feature_flags(ajax_new_deploy_token: { enabled: false, thing: group })
+ stub_feature_flags(ajax_new_deploy_token: false)
entity.add_owner(user)
end
@@ -56,7 +56,7 @@ describe Groups::Settings::RepositoryController do
'id' => be_a(Integer),
'name' => deploy_token_params[:name],
'username' => deploy_token_params[:username],
- 'expires_at' => Time.parse(deploy_token_params[:expires_at]),
+ 'expires_at' => Time.zone.parse(deploy_token_params[:expires_at]),
'token' => be_a(String),
'scopes' => deploy_token_params.inject([]) do |scopes, kv|
key, value = kv
diff --git a/spec/controllers/groups_controller_spec.rb b/spec/controllers/groups_controller_spec.rb
index 93478bbff1d..354c9e047c8 100644
--- a/spec/controllers/groups_controller_spec.rb
+++ b/spec/controllers/groups_controller_spec.rb
@@ -270,6 +270,37 @@ describe GroupsController do
it { expect(subject).to render_template(:new) }
end
+
+ context 'when creating a group with `default_branch_protection` attribute' do
+ before do
+ sign_in(user)
+ end
+
+ subject do
+ post :create, params: { group: { name: 'new_group', path: 'new_group', default_branch_protection: Gitlab::Access::PROTECTION_NONE } }
+ end
+
+ context 'for users who have the ability to create a group with `default_branch_protection`' do
+ it 'creates group with the specified branch protection level' do
+ subject
+
+ expect(response).to have_gitlab_http_status(:found)
+ expect(Group.last.default_branch_protection).to eq(Gitlab::Access::PROTECTION_NONE)
+ end
+ end
+
+ context 'for users who do not have the ability to create a group with `default_branch_protection`' do
+ it 'does not create the group with the specified branch protection level' do
+ allow(Ability).to receive(:allowed?).and_call_original
+ allow(Ability).to receive(:allowed?).with(user, :create_group_with_default_branch_protection) { false }
+
+ subject
+
+ expect(response).to have_gitlab_http_status(:found)
+ expect(Group.last.default_branch_protection).not_to eq(Gitlab::Access::PROTECTION_NONE)
+ end
+ end
+ end
end
describe 'GET #index' do
@@ -423,11 +454,31 @@ describe GroupsController do
expect(group.reload.project_creation_level).to eq(::Gitlab::Access::MAINTAINER_PROJECT_ACCESS)
end
- it 'updates the default_branch_protection successfully' do
- post :update, params: { id: group.to_param, group: { default_branch_protection: ::Gitlab::Access::PROTECTION_DEV_CAN_MERGE } }
+ context 'updating default_branch_protection' do
+ subject do
+ put :update, params: { id: group.to_param, group: { default_branch_protection: ::Gitlab::Access::PROTECTION_DEV_CAN_MERGE } }
+ end
+
+ context 'for users who have the ability to update default_branch_protection' do
+ it 'updates the attribute' do
+ subject
- expect(response).to have_gitlab_http_status(:found)
- expect(group.reload.default_branch_protection).to eq(::Gitlab::Access::PROTECTION_DEV_CAN_MERGE)
+ expect(response).to have_gitlab_http_status(:found)
+ expect(group.reload.default_branch_protection).to eq(::Gitlab::Access::PROTECTION_DEV_CAN_MERGE)
+ end
+ end
+
+ context 'for users who do not have the ability to update default_branch_protection' do
+ it 'does not update the attribute' do
+ allow(Ability).to receive(:allowed?).and_call_original
+ allow(Ability).to receive(:allowed?).with(user, :update_default_branch_protection, group) { false }
+
+ subject
+
+ expect(response).to have_gitlab_http_status(:found)
+ expect(group.reload.default_branch_protection).not_to eq(::Gitlab::Access::PROTECTION_DEV_CAN_MERGE)
+ end
+ end
end
context 'when a project inside the group has container repositories' do
diff --git a/spec/controllers/help_controller_spec.rb b/spec/controllers/help_controller_spec.rb
index f03fee8d3ae..fafbe6bffe1 100644
--- a/spec/controllers/help_controller_spec.rb
+++ b/spec/controllers/help_controller_spec.rb
@@ -99,6 +99,7 @@ describe HelpController do
context 'for Markdown formats' do
context 'when requested file exists' do
before do
+ expect(File).to receive(:read).and_return(fixture_file('blockquote_fence_after.md'))
get :show, params: { path: 'ssh/README' }, format: :md
end
@@ -108,7 +109,7 @@ describe HelpController do
it 'renders HTML' do
expect(response).to render_template('show.html.haml')
- expect(response.content_type).to eq 'text/html'
+ expect(response.media_type).to eq 'text/html'
end
end
@@ -129,7 +130,7 @@ describe HelpController do
},
format: :png
expect(response).to be_successful
- expect(response.content_type).to eq 'image/png'
+ expect(response.media_type).to eq 'image/png'
expect(response.headers['Content-Disposition']).to match(/^inline;/)
end
end
@@ -168,6 +169,6 @@ describe HelpController do
end
def stub_readme(content)
- allow(File).to receive(:read).and_return(content)
+ expect(File).to receive(:read).and_return(content)
end
end
diff --git a/spec/controllers/import/bitbucket_controller_spec.rb b/spec/controllers/import/bitbucket_controller_spec.rb
index ab4f6d5054c..d44edb63635 100644
--- a/spec/controllers/import/bitbucket_controller_spec.rb
+++ b/spec/controllers/import/bitbucket_controller_spec.rb
@@ -27,7 +27,7 @@ describe Import::BitbucketController do
end
it "updates access token" do
- expires_at = Time.now + 1.day
+ expires_at = Time.current + 1.day
expires_in = 1.day
access_token = double(token: token,
secret: secret,
diff --git a/spec/controllers/ldap/omniauth_callbacks_controller_spec.rb b/spec/controllers/ldap/omniauth_callbacks_controller_spec.rb
index ceab9754617..0242a91ac60 100644
--- a/spec/controllers/ldap/omniauth_callbacks_controller_spec.rb
+++ b/spec/controllers/ldap/omniauth_callbacks_controller_spec.rb
@@ -65,4 +65,55 @@ describe Ldap::OmniauthCallbacksController do
expect(request.env['warden']).to be_authenticated
end
end
+
+ describe 'enable admin mode' do
+ include_context 'custom session'
+
+ before do
+ sign_in user
+ end
+
+ context 'with a regular user' do
+ it 'cannot be enabled' do
+ reauthenticate_and_check_admin_mode(expected_admin_mode: false)
+
+ expect(response).to redirect_to(root_path)
+ end
+ end
+
+ context 'with an admin user' do
+ let(:user) { create(:omniauth_user, :admin, extern_uid: uid, provider: provider) }
+
+ context 'when requested first' do
+ before do
+ subject.current_user_mode.request_admin_mode!
+ end
+
+ it 'can be enabled' do
+ reauthenticate_and_check_admin_mode(expected_admin_mode: true)
+
+ expect(response).to redirect_to(admin_root_path)
+ end
+ end
+
+ context 'when not requested first' do
+ it 'cannot be enabled' do
+ reauthenticate_and_check_admin_mode(expected_admin_mode: false)
+
+ expect(response).to redirect_to(root_path)
+ end
+ end
+ end
+ end
+
+ def reauthenticate_and_check_admin_mode(expected_admin_mode:)
+ # Initially admin mode disabled
+ expect(subject.current_user_mode.admin_mode?).to be(false)
+
+ # Trigger OmniAuth admin mode flow and expect admin mode status
+ post provider
+
+ expect(request.env['warden']).to be_authenticated
+ expect(subject.current_user_mode.admin_mode?).to be(expected_admin_mode)
+ end
end
diff --git a/spec/controllers/oauth/token_info_controller_spec.rb b/spec/controllers/oauth/token_info_controller_spec.rb
index 4b3539879df..4658c2702ca 100644
--- a/spec/controllers/oauth/token_info_controller_spec.rb
+++ b/spec/controllers/oauth/token_info_controller_spec.rb
@@ -9,7 +9,7 @@ RSpec.describe Oauth::TokenInfoController do
get :show
expect(response).to have_gitlab_http_status(:unauthorized)
- expect(JSON.parse(response.body)).to include('error' => 'invalid_request')
+ expect(Gitlab::Json.parse(response.body)).to include('error' => 'invalid_request')
end
end
@@ -23,7 +23,7 @@ RSpec.describe Oauth::TokenInfoController do
get :show, params: { access_token: access_token.token }
expect(response).to have_gitlab_http_status(:ok)
- expect(JSON.parse(response.body)).to eq(
+ expect(Gitlab::Json.parse(response.body)).to eq(
'scope' => %w[api],
'scopes' => %w[api],
'created_at' => access_token.created_at.to_i,
@@ -40,7 +40,7 @@ RSpec.describe Oauth::TokenInfoController do
get :show, params: { access_token: 'unknown_token' }
expect(response).to have_gitlab_http_status(:unauthorized)
- expect(JSON.parse(response.body)).to include('error' => 'invalid_request')
+ expect(Gitlab::Json.parse(response.body)).to include('error' => 'invalid_request')
end
end
@@ -53,7 +53,7 @@ RSpec.describe Oauth::TokenInfoController do
get :show, params: { access_token: access_token.token }
expect(response).to have_gitlab_http_status(:unauthorized)
- expect(JSON.parse(response.body)).to include('error' => 'invalid_request')
+ expect(Gitlab::Json.parse(response.body)).to include('error' => 'invalid_request')
end
end
@@ -64,7 +64,7 @@ RSpec.describe Oauth::TokenInfoController do
get :show, params: { access_token: access_token.token }
expect(response).to have_gitlab_http_status(:unauthorized)
- expect(JSON.parse(response.body)).to include('error' => 'invalid_request')
+ expect(Gitlab::Json.parse(response.body)).to include('error' => 'invalid_request')
end
end
end
diff --git a/spec/controllers/omniauth_callbacks_controller_spec.rb b/spec/controllers/omniauth_callbacks_controller_spec.rb
index 9537ff62f8b..0d8a6827afe 100644
--- a/spec/controllers/omniauth_callbacks_controller_spec.rb
+++ b/spec/controllers/omniauth_callbacks_controller_spec.rb
@@ -2,7 +2,7 @@
require 'spec_helper'
-describe OmniauthCallbacksController, type: :controller, do_not_mock_admin_mode: true do
+describe OmniauthCallbacksController, type: :controller do
include LoginHelpers
describe 'omniauth' do
@@ -144,6 +144,10 @@ describe OmniauthCallbacksController, type: :controller, do_not_mock_admin_mode:
let(:extern_uid) { 'my-uid' }
let(:provider) { :github }
+ it_behaves_like 'known sign in' do
+ let(:post_action) { post provider }
+ end
+
it 'allows sign in' do
post provider
@@ -287,6 +291,11 @@ describe OmniauthCallbacksController, type: :controller, do_not_mock_admin_mode:
request.env['omniauth.auth'] = Rails.application.env_config['omniauth.auth']
end
+ it_behaves_like 'known sign in' do
+ let(:user) { create(:omniauth_user, extern_uid: 'my-uid', provider: 'saml') }
+ let(:post_action) { post :saml, params: { SAMLResponse: mock_saml_response } }
+ end
+
context 'sign up' do
before do
user.destroy
diff --git a/spec/controllers/profiles/emails_controller_spec.rb b/spec/controllers/profiles/emails_controller_spec.rb
index 7c6b1863202..ffec43fea2c 100644
--- a/spec/controllers/profiles/emails_controller_spec.rb
+++ b/spec/controllers/profiles/emails_controller_spec.rb
@@ -9,13 +9,27 @@ describe Profiles::EmailsController do
sign_in(user)
end
+ around do |example|
+ perform_enqueued_jobs do
+ example.run
+ end
+ end
+
describe '#create' do
- let(:email_params) { { email: "add_email@example.com" } }
+ context 'when email address is valid' do
+ let(:email_params) { { email: "add_email@example.com" } }
- it 'sends an email confirmation' do
- expect { post(:create, params: { email: email_params }) }.to change { ActionMailer::Base.deliveries.size }
- expect(ActionMailer::Base.deliveries.last.to).to eq [email_params[:email]]
- expect(ActionMailer::Base.deliveries.last.subject).to match "Confirmation instructions"
+ it 'sends an email confirmation' do
+ expect { post(:create, params: { email: email_params }) }.to change { ActionMailer::Base.deliveries.size }
+ end
+ end
+
+ context 'when email address is invalid' do
+ let(:email_params) { { email: "test.@example.com" } }
+
+ it 'does not send an email confirmation' do
+ expect { post(:create, params: { email: email_params }) }.not_to change { ActionMailer::Base.deliveries.size }
+ end
end
end
diff --git a/spec/controllers/projects/alert_management_controller_spec.rb b/spec/controllers/projects/alert_management_controller_spec.rb
new file mode 100644
index 00000000000..b84376db33d
--- /dev/null
+++ b/spec/controllers/projects/alert_management_controller_spec.rb
@@ -0,0 +1,59 @@
+# frozen_string_literal: true
+
+require 'spec_helper'
+
+describe Projects::AlertManagementController do
+ let_it_be(:project) { create(:project) }
+ let_it_be(:role) { :developer }
+ let_it_be(:user) { create(:user) }
+ let_it_be(:id) { 1 }
+
+ before do
+ project.add_role(user, role)
+ sign_in(user)
+ end
+
+ describe 'GET #index' do
+ it 'shows the page' do
+ get :index, params: { namespace_id: project.namespace, project_id: project }
+
+ expect(response).to have_gitlab_http_status(:ok)
+ end
+
+ context 'when user is unauthorized' do
+ let(:role) { :reporter }
+
+ it 'shows 404' do
+ get :index, params: { namespace_id: project.namespace, project_id: project }
+
+ expect(response).to have_gitlab_http_status(:not_found)
+ end
+ end
+ end
+
+ describe 'GET #details' do
+ it 'shows the page' do
+ get :details, params: { namespace_id: project.namespace, project_id: project, id: id }
+
+ expect(response).to have_gitlab_http_status(:ok)
+ end
+
+ context 'when user is unauthorized' do
+ let(:role) { :reporter }
+
+ it 'shows 404' do
+ get :index, params: { namespace_id: project.namespace, project_id: project }
+
+ expect(response).to have_gitlab_http_status(:not_found)
+ end
+ end
+ end
+
+ describe 'set_alert_id' do
+ it 'sets alert id from the route' do
+ get :details, params: { namespace_id: project.namespace, project_id: project, id: id }
+
+ expect(assigns(:alert_id)).to eq(id.to_s)
+ end
+ end
+end
diff --git a/spec/controllers/projects/artifacts_controller_spec.rb b/spec/controllers/projects/artifacts_controller_spec.rb
index c59983d5138..be616b566dd 100644
--- a/spec/controllers/projects/artifacts_controller_spec.rb
+++ b/spec/controllers/projects/artifacts_controller_spec.rb
@@ -308,10 +308,13 @@ describe Projects::ArtifactsController do
end
describe 'GET raw' do
- subject { get(:raw, params: { namespace_id: project.namespace, project_id: project, job_id: job, path: path }) }
+ let(:query_params) { { namespace_id: project.namespace, project_id: project, job_id: job, path: path } }
+
+ subject { get(:raw, params: query_params) }
context 'when the file exists' do
let(:path) { 'ci_artifacts.txt' }
+ let(:archive_matcher) { /build_artifacts.zip(\?[^?]+)?$/ }
shared_examples 'a valid file' do
it 'serves the file using workhorse' do
@@ -323,8 +326,8 @@ describe Projects::ArtifactsController do
expect(params.keys).to eq(%w(Archive Entry))
expect(params['Archive']).to start_with(archive_path)
# On object storage, the URL can end with a query string
- expect(params['Archive']).to match(/build_artifacts.zip(\?[^?]+)?$/)
- expect(params['Entry']).to eq(Base64.encode64('ci_artifacts.txt'))
+ expect(params['Archive']).to match(archive_matcher)
+ expect(params['Entry']).to eq(Base64.encode64(path))
end
def send_data
@@ -334,7 +337,7 @@ describe Projects::ArtifactsController do
def params
@params ||= begin
base64_params = send_data.sub(/\Aartifacts\-entry:/, '')
- JSON.parse(Base64.urlsafe_decode64(base64_params))
+ Gitlab::Json.parse(Base64.urlsafe_decode64(base64_params))
end
end
end
@@ -359,6 +362,37 @@ describe Projects::ArtifactsController do
let(:archive_path) { 'https://' }
end
end
+
+ context 'fetching an artifact of different type' do
+ before do
+ job.job_artifacts.each(&:destroy)
+ end
+
+ context 'when the artifact is zip' do
+ let!(:artifact) { create(:ci_job_artifact, :lsif, job: job, file_path: Rails.root.join("spec/fixtures/#{file_name}")) }
+ let(:path) { 'lsif/main.go.json' }
+ let(:file_name) { 'lsif.json.zip' }
+ let(:archive_matcher) { file_name }
+ let(:query_params) { super().merge(file_type: :lsif, path: path) }
+
+ it_behaves_like 'a valid file' do
+ let(:store) { ObjectStorage::Store::LOCAL }
+ let(:archive_path) { JobArtifactUploader.root }
+ end
+ end
+
+ context 'when the artifact is not zip' do
+ let(:query_params) { super().merge(file_type: :junit, path: '') }
+
+ it 'responds with not found' do
+ create(:ci_job_artifact, :junit, job: job)
+
+ subject
+
+ expect(response).to have_gitlab_http_status(:not_found)
+ end
+ end
+ end
end
end
diff --git a/spec/controllers/projects/branches_controller_spec.rb b/spec/controllers/projects/branches_controller_spec.rb
index 85d3044993e..174d8904481 100644
--- a/spec/controllers/projects/branches_controller_spec.rb
+++ b/spec/controllers/projects/branches_controller_spec.rb
@@ -124,57 +124,39 @@ describe Projects::BranchesController do
)
end
- context 'create_confidential_merge_request feature is enabled' do
+ context 'user cannot update issue' do
+ let(:issue) { create(:issue, project: confidential_issue_project) }
+
+ it 'does not post a system note' do
+ expect(SystemNoteService).not_to receive(:new_issue_branch)
+
+ create_branch_with_confidential_issue_project
+ end
+ end
+
+ context 'user can update issue' do
before do
- stub_feature_flags(create_confidential_merge_request: true)
+ confidential_issue_project.add_reporter(user)
end
- context 'user cannot update issue' do
+ context 'issue is under the specified project' do
let(:issue) { create(:issue, project: confidential_issue_project) }
- it 'does not post a system note' do
- expect(SystemNoteService).not_to receive(:new_issue_branch)
+ it 'posts a system note' do
+ expect(SystemNoteService).to receive(:new_issue_branch).with(issue, confidential_issue_project, user, "1-feature-branch", branch_project: project)
create_branch_with_confidential_issue_project
end
end
- context 'user can update issue' do
- before do
- confidential_issue_project.add_reporter(user)
- end
-
- context 'issue is under the specified project' do
- let(:issue) { create(:issue, project: confidential_issue_project) }
-
- it 'posts a system note' do
- expect(SystemNoteService).to receive(:new_issue_branch).with(issue, confidential_issue_project, user, "1-feature-branch", branch_project: project)
-
- create_branch_with_confidential_issue_project
- end
- end
-
- context 'issue is not under the specified project' do
- it 'does not post a system note' do
- expect(SystemNoteService).not_to receive(:new_issue_branch)
+ context 'issue is not under the specified project' do
+ it 'does not post a system note' do
+ expect(SystemNoteService).not_to receive(:new_issue_branch)
- create_branch_with_confidential_issue_project
- end
+ create_branch_with_confidential_issue_project
end
end
end
-
- context 'create_confidential_merge_request feature is disabled' do
- before do
- stub_feature_flags(create_confidential_merge_request: false)
- end
-
- it 'posts a system note on project' do
- expect(SystemNoteService).to receive(:new_issue_branch).with(issue, project, user, "1-feature-branch", branch_project: project)
-
- create_branch_with_confidential_issue_project
- end
- end
end
context 'repository-less project' do
diff --git a/spec/controllers/projects/ci/daily_build_group_report_results_controller_spec.rb b/spec/controllers/projects/ci/daily_build_group_report_results_controller_spec.rb
new file mode 100644
index 00000000000..ac31045678f
--- /dev/null
+++ b/spec/controllers/projects/ci/daily_build_group_report_results_controller_spec.rb
@@ -0,0 +1,80 @@
+# frozen_string_literal: true
+
+require 'spec_helper'
+
+describe Projects::Ci::DailyBuildGroupReportResultsController do
+ describe 'GET index' do
+ let(:project) { create(:project, :public, :repository) }
+ let(:ref_path) { 'refs/heads/master' }
+ let(:param_type) { 'coverage' }
+ let(:start_date) { '2019-12-10' }
+ let(:end_date) { '2020-03-09' }
+
+ def create_daily_coverage(group_name, coverage, date)
+ create(
+ :ci_daily_build_group_report_result,
+ project: project,
+ ref_path: ref_path,
+ group_name: group_name,
+ data: { 'coverage' => coverage },
+ date: date
+ )
+ end
+
+ def csv_response
+ CSV.parse(response.body)
+ end
+
+ before do
+ create_daily_coverage('rspec', 79.0, '2020-03-09')
+ create_daily_coverage('karma', 81.0, '2019-12-10')
+ create_daily_coverage('rspec', 67.0, '2019-12-09')
+ create_daily_coverage('karma', 71.0, '2019-12-09')
+
+ get :index, params: {
+ namespace_id: project.namespace,
+ project_id: project,
+ ref_path: ref_path,
+ param_type: param_type,
+ start_date: start_date,
+ end_date: end_date,
+ format: :csv
+ }
+ end
+
+ it 'serves the results in CSV' do
+ expect(response).to have_gitlab_http_status(:ok)
+ expect(response.headers['Content-Type']).to eq('text/csv; charset=utf-8')
+
+ expect(csv_response).to eq([
+ %w[date group_name coverage],
+ ['2020-03-09', 'rspec', '79.0'],
+ ['2019-12-10', 'karma', '81.0']
+ ])
+ end
+
+ context 'when given date range spans more than 90 days' do
+ let(:start_date) { '2019-12-09' }
+ let(:end_date) { '2020-03-09' }
+
+ it 'limits the result to 90 days from the given start_date' do
+ expect(response).to have_gitlab_http_status(:ok)
+ expect(response.headers['Content-Type']).to eq('text/csv; charset=utf-8')
+
+ expect(csv_response).to eq([
+ %w[date group_name coverage],
+ ['2020-03-09', 'rspec', '79.0'],
+ ['2019-12-10', 'karma', '81.0']
+ ])
+ end
+ end
+
+ context 'when given param_type is invalid' do
+ let(:param_type) { 'something_else' }
+
+ it 'responds with 422 error' do
+ expect(response).to have_gitlab_http_status(:unprocessable_entity)
+ end
+ end
+ end
+end
diff --git a/spec/controllers/projects/clusters_controller_spec.rb b/spec/controllers/projects/clusters_controller_spec.rb
index 07733ec30d9..698a3773d59 100644
--- a/spec/controllers/projects/clusters_controller_spec.rb
+++ b/spec/controllers/projects/clusters_controller_spec.rb
@@ -26,7 +26,7 @@ describe Projects::ClustersController do
let!(:enabled_cluster) { create(:cluster, :provided_by_gcp, projects: [project]) }
let!(:disabled_cluster) { create(:cluster, :disabled, :provided_by_gcp, :production_environment, projects: [project]) }
- it 'lists available clusters' do
+ it 'lists available clusters and renders html' do
go
expect(response).to have_gitlab_http_status(:ok)
@@ -34,20 +34,39 @@ describe Projects::ClustersController do
expect(assigns(:clusters)).to match_array([enabled_cluster, disabled_cluster])
end
+ it 'lists available clusters with json serializer' do
+ go(format: :json)
+
+ expect(response).to have_gitlab_http_status(:ok)
+ expect(response).to match_response_schema('cluster_list')
+ end
+
context 'when page is specified' do
let(:last_page) { project.clusters.page.total_pages }
+ let(:total_count) { project.clusters.page.total_count }
before do
- allow(Clusters::Cluster).to receive(:paginates_per).and_return(1)
- create_list(:cluster, 2, :provided_by_gcp, :production_environment, projects: [project])
+ create_list(:cluster, 30, :provided_by_gcp, :production_environment, projects: [project])
end
it 'redirects to the page' do
+ expect(last_page).to be > 1
+
go(page: last_page)
expect(response).to have_gitlab_http_status(:ok)
expect(assigns(:clusters).current_page).to eq(last_page)
end
+
+ it 'displays cluster list for associated page' do
+ expect(last_page).to be > 1
+
+ go(page: last_page, format: :json)
+
+ expect(response).to have_gitlab_http_status(:ok)
+ expect(response.headers['X-Page'].to_i).to eq(last_page)
+ expect(response.headers['X-Total'].to_i).to eq(total_count)
+ end
end
end
@@ -68,9 +87,11 @@ describe Projects::ClustersController do
it 'is allowed for admin when admin mode enabled', :enable_admin_mode do
expect { go }.to be_allowed_for(:admin)
end
+
it 'is disabled for admin when admin mode disabled' do
expect { go }.to be_denied_for(:admin)
end
+
it { expect { go }.to be_allowed_for(:owner).of(project) }
it { expect { go }.to be_allowed_for(:maintainer).of(project) }
it { expect { go }.to be_denied_for(:developer).of(project) }
diff --git a/spec/controllers/projects/cycle_analytics/events_controller_spec.rb b/spec/controllers/projects/cycle_analytics/events_controller_spec.rb
index b828c678d0c..942e095d669 100644
--- a/spec/controllers/projects/cycle_analytics/events_controller_spec.rb
+++ b/spec/controllers/projects/cycle_analytics/events_controller_spec.rb
@@ -17,7 +17,7 @@ describe Projects::CycleAnalytics::EventsController do
get_issue
expect(response).to be_successful
- expect(JSON.parse(response.body)['events']).to be_empty
+ expect(Gitlab::Json.parse(response.body)['events']).to be_empty
end
end
@@ -38,7 +38,7 @@ describe Projects::CycleAnalytics::EventsController do
it 'contains event detais' do
get_issue
- events = JSON.parse(response.body)['events']
+ events = Gitlab::Json.parse(response.body)['events']
expect(events).not_to be_empty
expect(events.first).to include('title', 'author', 'iid', 'total_time', 'created_at', 'url')
@@ -51,7 +51,7 @@ describe Projects::CycleAnalytics::EventsController do
expect(response).to be_successful
- expect(JSON.parse(response.body)['events']).to be_empty
+ expect(Gitlab::Json.parse(response.body)['events']).to be_empty
end
end
end
diff --git a/spec/controllers/projects/design_management/designs/raw_images_controller_spec.rb b/spec/controllers/projects/design_management/designs/raw_images_controller_spec.rb
new file mode 100644
index 00000000000..30d2b79a92f
--- /dev/null
+++ b/spec/controllers/projects/design_management/designs/raw_images_controller_spec.rb
@@ -0,0 +1,153 @@
+# frozen_string_literal: true
+
+require 'spec_helper'
+
+describe Projects::DesignManagement::Designs::RawImagesController do
+ include DesignManagementTestHelpers
+
+ let_it_be(:project) { create(:project, :private) }
+ let_it_be(:issue) { create(:issue, project: project) }
+ let_it_be(:viewer) { issue.author }
+ let(:design_id) { design.id }
+ let(:sha) { design.versions.first.sha }
+ let(:filename) { design.filename }
+
+ before do
+ enable_design_management
+ end
+
+ describe 'GET #show' do
+ subject do
+ get(:show,
+ params: {
+ namespace_id: project.namespace,
+ project_id: project,
+ design_id: design_id,
+ sha: sha
+ })
+ end
+
+ before do
+ sign_in(viewer)
+ end
+
+ context 'when the design is not an LFS file' do
+ let_it_be(:design) { create(:design, :with_file, issue: issue, versions_count: 2) }
+
+ # For security, .svg images should only ever be served with Content-Disposition: attachment.
+ # If this specs ever fails we must assess whether we should be serving svg images.
+ # See https://gitlab.com/gitlab-org/gitlab/issues/12771
+ it 'serves files with `Content-Disposition: attachment`' do
+ subject
+
+ expect(response.header['Content-Disposition']).to eq('attachment')
+ expect(response).to have_gitlab_http_status(:ok)
+ end
+
+ it 'serves files with Workhorse' do
+ subject
+
+ expect(response.header[Gitlab::Workhorse::DETECT_HEADER]).to eq "true"
+ expect(response.header[Gitlab::Workhorse::SEND_DATA_HEADER]).to start_with('git-blob:')
+ expect(response).to have_gitlab_http_status(:ok)
+ end
+
+ it_behaves_like 'project cache control headers'
+
+ context 'when the user does not have permission' do
+ let_it_be(:viewer) { create(:user) }
+
+ specify do
+ subject
+
+ expect(response).to have_gitlab_http_status(:not_found)
+ end
+ end
+
+ context 'when design does not exist' do
+ let(:design_id) { 'foo' }
+
+ specify do
+ subject
+
+ expect(response).to have_gitlab_http_status(:not_found)
+ end
+ end
+
+ describe 'sha param' do
+ let(:newest_version) { design.versions.ordered.first }
+ let(:oldest_version) { design.versions.ordered.last }
+
+ shared_examples 'a successful request for sha' do
+ it do
+ expect_next_instance_of(DesignManagement::Repository) do |repository|
+ expect(repository).to receive(:blob_at).with(expected_ref, design.full_path).and_call_original
+ end
+
+ subject
+
+ expect(response).to have_gitlab_http_status(:ok)
+ end
+ end
+
+ specify { expect(newest_version.sha).not_to eq(oldest_version.sha) }
+
+ context 'when sha is the newest version sha' do
+ let(:sha) { newest_version.sha }
+ let(:expected_ref) { sha }
+
+ it_behaves_like 'a successful request for sha'
+ end
+
+ context 'when sha is the oldest version sha' do
+ let(:sha) { oldest_version.sha }
+ let(:expected_ref) { sha }
+
+ it_behaves_like 'a successful request for sha'
+ end
+
+ context 'when sha is nil' do
+ let(:sha) { nil }
+ let(:expected_ref) { 'master' }
+
+ it_behaves_like 'a successful request for sha'
+ end
+ end
+ end
+
+ context 'when the design is an LFS file' do
+ let_it_be(:design) { create(:design, :with_lfs_file, issue: issue) }
+
+ # For security, .svg images should only ever be served with Content-Disposition: attachment.
+ # If this specs ever fails we must assess whether we should be serving svg images.
+ # See https://gitlab.com/gitlab-org/gitlab/issues/12771
+ it 'serves files with `Content-Disposition: attachment`' do
+ subject
+
+ expect(response.header['Content-Disposition']).to eq(%Q(attachment; filename=\"#{filename}\"; filename*=UTF-8''#{filename}))
+ end
+
+ it 'sets appropriate caching headers' do
+ subject
+
+ expect(response.header['ETag']).to be_present
+ expect(response.header['Cache-Control']).to eq("max-age=60, private")
+ end
+ end
+
+ # Pass `skip_lfs_disabled_tests: true` to this shared example to disable
+ # the test scenarios for when LFS is disabled globally.
+ #
+ # When LFS is disabled then the design management feature also becomes disabled.
+ # When the feature is disabled, the `authorize :read_design` check within the
+ # controller will never authorize the user. Therefore #show will return a 403 and
+ # we cannot test the data that it serves.
+ it_behaves_like 'a controller that can serve LFS files', skip_lfs_disabled_tests: true do
+ let(:file) { fixture_file_upload('spec/fixtures/dk.png', '`/png') }
+ let(:lfs_pointer) { Gitlab::Git::LfsPointerFile.new(file.read) }
+ let(:design) { create(:design, :with_lfs_file, file: lfs_pointer.pointer, issue: issue) }
+ let(:lfs_oid) { project.design_repository.blob_at('HEAD', design.full_path).lfs_oid }
+ let(:filepath) { design.full_path }
+ end
+ end
+end
diff --git a/spec/controllers/projects/design_management/designs/resized_image_controller_spec.rb b/spec/controllers/projects/design_management/designs/resized_image_controller_spec.rb
new file mode 100644
index 00000000000..6bfec1b314e
--- /dev/null
+++ b/spec/controllers/projects/design_management/designs/resized_image_controller_spec.rb
@@ -0,0 +1,148 @@
+# frozen_string_literal: true
+
+require 'spec_helper'
+
+describe Projects::DesignManagement::Designs::ResizedImageController do
+ include DesignManagementTestHelpers
+
+ let_it_be(:project) { create(:project, :private) }
+ let_it_be(:issue) { create(:issue, project: project) }
+ let_it_be(:viewer) { issue.author }
+ let_it_be(:size) { :v432x230 }
+ let(:design) { create(:design, :with_smaller_image_versions, issue: issue, versions_count: 2) }
+ let(:design_id) { design.id }
+ let(:sha) { design.versions.first.sha }
+
+ before do
+ enable_design_management
+ end
+
+ describe 'GET #show' do
+ subject do
+ get(:show,
+ params: {
+ namespace_id: project.namespace,
+ project_id: project,
+ design_id: design_id,
+ sha: sha,
+ id: size
+ })
+ end
+
+ before do
+ sign_in(viewer)
+ subject
+ end
+
+ context 'when the user does not have permission' do
+ let_it_be(:viewer) { create(:user) }
+
+ specify do
+ expect(response).to have_gitlab_http_status(:not_found)
+ end
+ end
+
+ describe 'Response headers' do
+ it 'completes the request successfully' do
+ expect(response).to have_gitlab_http_status(:ok)
+ end
+
+ it 'sets Content-Disposition as attachment' do
+ filename = design.filename
+
+ expect(response.header['Content-Disposition']).to eq(%Q(attachment; filename=\"#{filename}\"; filename*=UTF-8''#{filename}))
+ end
+
+ it 'serves files with Workhorse' do
+ expect(response.header[Gitlab::Workhorse::DETECT_HEADER]).to eq 'true'
+ end
+
+ it 'sets appropriate caching headers' do
+ expect(response.header['Cache-Control']).to eq('private')
+ expect(response.header['ETag']).to be_present
+ end
+ end
+
+ context 'when design does not exist' do
+ let(:design_id) { 'foo' }
+
+ specify do
+ subject
+
+ expect(response).to have_gitlab_http_status(:not_found)
+ end
+ end
+
+ context 'when size is invalid' do
+ let_it_be(:size) { :foo }
+
+ it 'returns a 404' do
+ expect(response).to have_gitlab_http_status(:not_found)
+ end
+ end
+
+ describe 'sha param' do
+ let(:newest_version) { design.versions.ordered.first }
+ let(:oldest_version) { design.versions.ordered.last }
+
+ # The design images generated by Factorybot are identical, so
+ # refer to the `ETag` header, which is uniquely generated from the Action
+ # (the record that represents the design at a specific version), to
+ # verify that the correct file is being returned.
+ def etag(action)
+ ActionDispatch::TestResponse.new.send(:generate_weak_etag, [action.cache_key, ''])
+ end
+
+ specify { expect(newest_version.sha).not_to eq(oldest_version.sha) }
+
+ context 'when sha is the newest version sha' do
+ let(:sha) { newest_version.sha }
+
+ it 'serves the newest image' do
+ action = newest_version.actions.first
+
+ expect(response.header['ETag']).to eq(etag(action))
+ expect(response).to have_gitlab_http_status(:ok)
+ end
+ end
+
+ context 'when sha is the oldest version sha' do
+ let(:sha) { oldest_version.sha }
+
+ it 'serves the oldest image' do
+ action = oldest_version.actions.first
+
+ expect(response.header['ETag']).to eq(etag(action))
+ expect(response).to have_gitlab_http_status(:ok)
+ end
+ end
+
+ context 'when sha is nil' do
+ let(:sha) { nil }
+
+ it 'serves the newest image' do
+ action = newest_version.actions.first
+
+ expect(response.header['ETag']).to eq(etag(action))
+ expect(response).to have_gitlab_http_status(:ok)
+ end
+ end
+
+ context 'when sha is not a valid version sha' do
+ let(:sha) { '570e7b2abdd848b95f2f578043fc23bd6f6fd24d' }
+
+ it 'returns a 404' do
+ expect(response).to have_gitlab_http_status(:not_found)
+ end
+ end
+ end
+
+ context 'when design does not have a smaller image size available' do
+ let(:design) { create(:design, :with_file, issue: issue) }
+
+ it 'returns a 404' do
+ expect(response).to have_gitlab_http_status(:not_found)
+ end
+ end
+ end
+end
diff --git a/spec/controllers/projects/environments/prometheus_api_controller_spec.rb b/spec/controllers/projects/environments/prometheus_api_controller_spec.rb
index 793c10f0b21..64f90e44bb6 100644
--- a/spec/controllers/projects/environments/prometheus_api_controller_spec.rb
+++ b/spec/controllers/projects/environments/prometheus_api_controller_spec.rb
@@ -38,7 +38,7 @@ describe Projects::Environments::PrometheusApiController do
context 'with success result' do
let(:service_result) { { status: :success, body: prometheus_body } }
let(:prometheus_body) { '{"status":"success"}' }
- let(:prometheus_json_body) { JSON.parse(prometheus_body) }
+ let(:prometheus_json_body) { Gitlab::Json.parse(prometheus_body) }
it 'returns prometheus response' do
get :proxy, params: environment_params
@@ -55,7 +55,7 @@ describe Projects::Environments::PrometheusApiController do
end
it 'replaces variables with values' do
- get :proxy, params: environment_params.merge(query: 'up{environment="%{ci_environment_slug}"}')
+ get :proxy, params: environment_params.merge(query: 'up{environment="{{ci_environment_slug}}"}')
expect(Prometheus::ProxyService).to have_received(:new)
.with(environment, 'GET', 'query', expected_params)
diff --git a/spec/controllers/projects/environments_controller_spec.rb b/spec/controllers/projects/environments_controller_spec.rb
index 3b035eea7d5..56fff2771ec 100644
--- a/spec/controllers/projects/environments_controller_spec.rb
+++ b/spec/controllers/projects/environments_controller_spec.rb
@@ -410,6 +410,18 @@ describe Projects::EnvironmentsController do
expect(json_response['last_update']).to eq(42)
end
end
+
+ context 'permissions' do
+ before do
+ allow(controller).to receive(:can?).and_return true
+ end
+
+ it 'checks :metrics_dashboard ability' do
+ expect(controller).to receive(:can?).with(anything, :metrics_dashboard, anything)
+
+ get :metrics, params: environment_params
+ end
+ end
end
describe 'GET #additional_metrics' do
@@ -473,6 +485,18 @@ describe Projects::EnvironmentsController do
.to raise_error(ActionController::ParameterMissing)
end
end
+
+ context 'permissions' do
+ before do
+ allow(controller).to receive(:can?).and_return true
+ end
+
+ it 'checks :metrics_dashboard ability' do
+ expect(controller).to receive(:can?).with(anything, :metrics_dashboard, anything)
+
+ get :metrics, params: environment_params
+ end
+ end
end
describe 'GET #metrics_dashboard' do
@@ -648,6 +672,18 @@ describe Projects::EnvironmentsController do
it_behaves_like 'the default dashboard'
it_behaves_like 'dashboard can be specified'
it_behaves_like 'dashboard can be embedded'
+
+ context 'permissions' do
+ before do
+ allow(controller).to receive(:can?).and_return true
+ end
+
+ it 'checks :metrics_dashboard ability' do
+ expect(controller).to receive(:can?).with(anything, :metrics_dashboard, anything)
+
+ get :metrics, params: environment_params
+ end
+ end
end
describe 'GET #search' do
diff --git a/spec/controllers/projects/grafana_api_controller_spec.rb b/spec/controllers/projects/grafana_api_controller_spec.rb
index c62baa30fde..8502bd1ab0a 100644
--- a/spec/controllers/projects/grafana_api_controller_spec.rb
+++ b/spec/controllers/projects/grafana_api_controller_spec.rb
@@ -131,10 +131,11 @@ describe Projects::GrafanaApiController do
get :metrics_dashboard, params: params
expect(response).to have_gitlab_http_status(:ok)
- expect(json_response).to eq({
+ expect(json_response).to include({
'dashboard' => '{}',
'status' => 'success'
})
+ expect(json_response).to include('metrics_data')
end
end
diff --git a/spec/controllers/projects/graphs_controller_spec.rb b/spec/controllers/projects/graphs_controller_spec.rb
index b5248c7f0c8..e589815c45d 100644
--- a/spec/controllers/projects/graphs_controller_spec.rb
+++ b/spec/controllers/projects/graphs_controller_spec.rb
@@ -41,6 +41,26 @@ describe Projects::GraphsController do
expect(response).to have_gitlab_http_status(:ok)
expect(response).to render_template(:charts)
end
+
+ it 'sets the daily coverage options' do
+ Timecop.freeze do
+ get(:charts, params: { namespace_id: project.namespace.path, project_id: project.path, id: 'master' })
+
+ expect(assigns[:daily_coverage_options]).to eq(
+ base_params: {
+ start_date: Time.current.to_date - 90.days,
+ end_date: Time.current.to_date,
+ ref_path: project.repository.expand_ref('master'),
+ param_type: 'coverage'
+ },
+ download_path: namespace_project_ci_daily_build_group_report_results_path(
+ namespace_id: project.namespace,
+ project_id: project,
+ format: :csv
+ )
+ )
+ end
+ end
end
context 'when languages were previously detected' do
diff --git a/spec/controllers/projects/import/jira_controller_spec.rb b/spec/controllers/projects/import/jira_controller_spec.rb
index 4629aab65dd..d1b0a086576 100644
--- a/spec/controllers/projects/import/jira_controller_spec.rb
+++ b/spec/controllers/projects/import/jira_controller_spec.rb
@@ -3,6 +3,8 @@
require 'spec_helper'
describe Projects::Import::JiraController do
+ include JiraServiceHelper
+
let_it_be(:user) { create(:user) }
let_it_be(:project) { create(:project) }
let_it_be(:jira_project_key) { 'Test' }
@@ -61,9 +63,10 @@ describe Projects::Import::JiraController do
before do
stub_feature_flags(jira_issue_import: true)
stub_feature_flags(jira_issue_import_vue: false)
+ stub_jira_service_test
end
- context 'when jira service is enabled for the project' do
+ context 'when Jira service is enabled for the project' do
let_it_be(:jira_service) { create(:jira_service, project: project) }
context 'when user is developer' do
@@ -79,7 +82,7 @@ describe Projects::Import::JiraController do
get :show, params: { namespace_id: project.namespace.to_param, project_id: project }
end
- it 'does not query jira service' do
+ it 'does not query Jira service' do
expect(project).not_to receive(:jira_service)
end
@@ -118,7 +121,7 @@ describe Projects::Import::JiraController do
end
end
- context 'when running jira import first time' do
+ context 'when running Jira import first time' do
context 'get show' do
before do
allow(JIRA::Resource::Project).to receive(:all).and_return(jira_projects)
@@ -147,12 +150,12 @@ describe Projects::Import::JiraController do
end
context 'post import' do
- context 'when jira project key is empty' do
+ context 'when Jira project key is empty' do
it 'redirects back to show with an error' do
post :import, params: { namespace_id: project.namespace, project_id: project, jira_project_key: '' }
expect(response).to redirect_to(project_import_jira_path(project))
- expect(flash[:alert]).to eq('No jira project key has been provided.')
+ expect(flash[:alert]).to eq('No Jira project key has been provided.')
end
end
@@ -197,7 +200,7 @@ describe Projects::Import::JiraController do
end
end
- context 'when jira import ran before' do
+ context 'when Jira import ran before' do
let_it_be(:jira_import_state) { create(:jira_import_state, :finished, project: project, jira_project_key: jira_project_key) }
context 'get show' do
diff --git a/spec/controllers/projects/issues_controller_spec.rb b/spec/controllers/projects/issues_controller_spec.rb
index 862a4bd3559..96f11f11dc4 100644
--- a/spec/controllers/projects/issues_controller_spec.rb
+++ b/spec/controllers/projects/issues_controller_spec.rb
@@ -4,6 +4,7 @@ require 'spec_helper'
describe Projects::IssuesController do
include ProjectForksHelper
+ include_context 'includes Spam constants'
let(:project) { create(:project) }
let(:user) { create(:user) }
@@ -186,6 +187,33 @@ describe Projects::IssuesController do
expect(assigns(:issue)).to be_a_new(Issue)
end
+ where(:conf_value, :conf_result) do
+ [
+ [true, true],
+ ['true', true],
+ ['TRUE', true],
+ [false, false],
+ ['false', false],
+ ['FALSE', false]
+ ]
+ end
+
+ with_them do
+ it 'sets the confidential flag to the expected value' do
+ get :new, params: {
+ namespace_id: project.namespace,
+ project_id: project,
+ issue: {
+ confidential: conf_value
+ }
+ }
+
+ assigned_issue = assigns(:issue)
+ expect(assigned_issue).to be_a_new(Issue)
+ expect(assigned_issue.confidential).to eq conf_result
+ end
+ end
+
it 'fills in an issue for a merge request' do
project_with_repository = create(:project, :repository)
project_with_repository.add_developer(user)
@@ -242,6 +270,91 @@ describe Projects::IssuesController do
end
end
+ describe '#related_branches' do
+ subject { get :related_branches, params: params, format: :json }
+
+ before do
+ sign_in(user)
+ project.add_developer(developer)
+ end
+
+ let(:developer) { user }
+ let(:params) do
+ {
+ namespace_id: project.namespace,
+ project_id: project,
+ id: issue.iid
+ }
+ end
+
+ context 'the current user cannot download code' do
+ it 'prevents access' do
+ allow(controller).to receive(:can?).with(any_args).and_return(true)
+ allow(controller).to receive(:can?).with(user, :download_code, project).and_return(false)
+
+ subject
+
+ expect(response).to have_gitlab_http_status(:not_found)
+ end
+ end
+
+ context 'there are no related branches' do
+ it 'assigns empty arrays', :aggregate_failures do
+ subject
+
+ expect(response).to have_gitlab_http_status(:ok)
+ expect(assigns(:related_branches)).to be_empty
+ expect(response).to render_template('projects/issues/_related_branches')
+ expect(json_response).to eq('html' => '')
+ end
+ end
+
+ context 'there are related branches' do
+ let(:missing_branch) { "#{issue.to_branch_name}-missing" }
+ let(:unreadable_branch) { "#{issue.to_branch_name}-unreadable" }
+ let(:pipeline) { build(:ci_pipeline, :success, project: project) }
+ let(:master_branch) { 'master' }
+
+ let(:related_branches) do
+ [
+ branch_info(issue.to_branch_name, pipeline.detailed_status(user)),
+ branch_info(missing_branch, nil),
+ branch_info(unreadable_branch, nil)
+ ]
+ end
+
+ def branch_info(name, status)
+ {
+ name: name,
+ link: controller.project_compare_path(project, from: master_branch, to: name),
+ pipeline_status: status
+ }
+ end
+
+ before do
+ allow(controller).to receive(:find_routable!)
+ .with(Project, project.full_path, any_args).and_return(project)
+ allow(project).to receive(:default_branch).and_return(master_branch)
+ allow_next_instance_of(Issues::RelatedBranchesService) do |service|
+ allow(service).to receive(:execute).and_return(related_branches)
+ end
+ end
+
+ it 'finds and assigns the appropriate branch information', :aggregate_failures do
+ subject
+
+ expect(response).to have_gitlab_http_status(:ok)
+ expect(assigns(:related_branches)).to contain_exactly(
+ branch_info(issue.to_branch_name, an_instance_of(Gitlab::Ci::Status::Success)),
+ branch_info(missing_branch, be_nil),
+ branch_info(unreadable_branch, be_nil)
+ )
+ expect(response).to render_template('projects/issues/_related_branches')
+ expect(json_response).to match('html' => String)
+ end
+ end
+ end
+
# This spec runs as a request-style spec in order to invoke the
# Rails router. A controller-style spec matches the wrong route, and
# session['user_return_to'] becomes incorrect.
@@ -419,11 +532,11 @@ describe Projects::IssuesController do
expect(issue.reload.title).to eq('New title')
end
- context 'when Akismet is enabled and the issue is identified as spam' do
+ context 'when the SpamVerdictService disallows' do
before do
stub_application_setting(recaptcha_enabled: true)
- expect_next_instance_of(Spam::AkismetService) do |akismet_service|
- expect(akismet_service).to receive_messages(spam?: true)
+ expect_next_instance_of(Spam::SpamVerdictService) do |verdict_service|
+ expect(verdict_service).to receive(:execute).and_return(REQUIRE_RECAPTCHA)
end
end
@@ -496,7 +609,7 @@ describe Projects::IssuesController do
before do
project.add_developer(user)
- issue.update!(last_edited_by: deleted_user, last_edited_at: Time.now)
+ issue.update!(last_edited_by: deleted_user, last_edited_at: Time.current)
deleted_user.destroy
sign_in(user)
@@ -712,20 +825,20 @@ describe Projects::IssuesController do
update_issue(issue_params: { assignee_ids: [assignee.id] })
expect(json_response['assignees'].first.keys)
- .to match_array(%w(id name username avatar_url state web_url))
+ .to include(*%w(id name username avatar_url state web_url))
end
end
- context 'Akismet is enabled' do
+ context 'Recaptcha is enabled' do
before do
project.update!(visibility_level: Gitlab::VisibilityLevel::PUBLIC)
stub_application_setting(recaptcha_enabled: true)
end
- context 'when an issue is not identified as spam' do
+ context 'when SpamVerdictService allows the issue' do
before do
- expect_next_instance_of(Spam::AkismetService) do |akismet_service|
- expect(akismet_service).to receive_messages(spam?: false)
+ expect_next_instance_of(Spam::SpamVerdictService) do |verdict_service|
+ expect(verdict_service).to receive(:execute).and_return(ALLOW)
end
end
@@ -735,10 +848,10 @@ describe Projects::IssuesController do
end
context 'when an issue is identified as spam' do
- context 'when captcha is not verified' do
+ context 'when recaptcha is not verified' do
before do
- expect_next_instance_of(Spam::AkismetService) do |akismet_service|
- expect(akismet_service).to receive_messages(spam?: true)
+ expect_next_instance_of(Spam::SpamVerdictService) do |verdict_service|
+ expect(verdict_service).to receive(:execute).and_return(REQUIRE_RECAPTCHA)
end
end
@@ -751,7 +864,7 @@ describe Projects::IssuesController do
expect { update_issue }.not_to change { issue.reload.title }
end
- it 'rejects an issue recognized as a spam when recaptcha disabled' do
+ it 'rejects an issue recognized as a spam when reCAPTCHA disabled' do
stub_application_setting(recaptcha_enabled: false)
expect { update_issue }.not_to change { issue.reload.title }
@@ -796,7 +909,7 @@ describe Projects::IssuesController do
end
end
- context 'when captcha is verified' do
+ context 'when recaptcha is verified' do
let(:spammy_title) { 'Whatever' }
let!(:spam_logs) { create_list(:spam_log, 2, user: user, title: spammy_title) }
@@ -810,7 +923,7 @@ describe Projects::IssuesController do
expect(response).to have_gitlab_http_status(:ok)
end
- it 'accepts an issue after recaptcha is verified' do
+ it 'accepts an issue after reCAPTCHA is verified' do
expect { update_verified_issue }.to change { issue.reload.title }.to(spammy_title)
end
@@ -967,17 +1080,17 @@ describe Projects::IssuesController do
end
end
- context 'Akismet is enabled' do
+ context 'Recaptcha is enabled' do
before do
stub_application_setting(recaptcha_enabled: true)
end
- context 'when an issue is not identified as spam' do
+ context 'when SpamVerdictService allows the issue' do
before do
stub_feature_flags(allow_possible_spam: false)
- expect_next_instance_of(Spam::AkismetService) do |akismet_service|
- expect(akismet_service).to receive_messages(spam?: false)
+ expect_next_instance_of(Spam::SpamVerdictService) do |verdict_service|
+ expect(verdict_service).to receive(:execute).and_return(ALLOW)
end
end
@@ -986,18 +1099,18 @@ describe Projects::IssuesController do
end
end
- context 'when an issue is identified as spam' do
+ context 'when SpamVerdictService requires recaptcha' do
context 'when captcha is not verified' do
- def post_spam_issue
- post_new_issue(title: 'Spam Title', description: 'Spam lives here')
- end
-
before do
- expect_next_instance_of(Spam::AkismetService) do |akismet_service|
- expect(akismet_service).to receive_messages(spam?: true)
+ expect_next_instance_of(Spam::SpamVerdictService) do |verdict_service|
+ expect(verdict_service).to receive(:execute).and_return(REQUIRE_RECAPTCHA)
end
end
+ def post_spam_issue
+ post_new_issue(title: 'Spam Title', description: 'Spam lives here')
+ end
+
context 'when allow_possible_spam feature flag is false' do
before do
stub_feature_flags(allow_possible_spam: false)
@@ -1016,7 +1129,7 @@ describe Projects::IssuesController do
expect { post_new_issue(title: '') }.not_to change(Issue, :count)
end
- it 'does not create an issue when recaptcha is not enabled' do
+ it 'does not create an issue when reCAPTCHA is not enabled' do
stub_application_setting(recaptcha_enabled: false)
expect { post_spam_issue }.not_to change(Issue, :count)
@@ -1039,30 +1152,31 @@ describe Projects::IssuesController do
end
end
- context 'when captcha is verified' do
+ context 'when Recaptcha is verified' do
let!(:spam_logs) { create_list(:spam_log, 2, user: user, title: 'Title') }
+ let!(:last_spam_log) { spam_logs.last }
def post_verified_issue
- post_new_issue({}, { spam_log_id: spam_logs.last.id, recaptcha_verification: true } )
+ post_new_issue({}, { spam_log_id: last_spam_log.id, recaptcha_verification: true } )
end
before do
expect(controller).to receive_messages(verify_recaptcha: true)
end
- it 'accepts an issue after recaptcha is verified' do
+ it 'accepts an issue after reCAPTCHA is verified' do
expect { post_verified_issue }.to change(Issue, :count)
end
it 'marks spam log as recaptcha_verified' do
- expect { post_verified_issue }.to change { SpamLog.last.recaptcha_verified }.from(false).to(true)
+ expect { post_verified_issue }.to change { last_spam_log.reload.recaptcha_verified }.from(false).to(true)
end
it 'does not mark spam log as recaptcha_verified when it does not belong to current_user' do
spam_log = create(:spam_log)
expect { post_new_issue({}, { spam_log_id: spam_log.id, recaptcha_verification: true } ) }
- .not_to change { SpamLog.last.recaptcha_verified }
+ .not_to change { last_spam_log.recaptcha_verified }
end
end
end
@@ -1294,6 +1408,7 @@ describe Projects::IssuesController do
it 'render merge request as json' do
create_merge_request
+ expect(response).to have_gitlab_http_status(:ok)
expect(response).to match_response_schema('merge_request')
end
@@ -1337,24 +1452,8 @@ describe Projects::IssuesController do
let(:target_project) { fork_project(project, user, repository: true) }
let(:target_project_id) { target_project.id }
- context 'create_confidential_merge_request feature is enabled' do
- before do
- stub_feature_flags(create_confidential_merge_request: true)
- end
-
- it 'creates a new merge request', :sidekiq_might_not_need_inline do
- expect { create_merge_request }.to change(target_project.merge_requests, :count).by(1)
- end
- end
-
- context 'create_confidential_merge_request feature is disabled' do
- before do
- stub_feature_flags(create_confidential_merge_request: false)
- end
-
- it 'creates a new merge request' do
- expect { create_merge_request }.to change(project.merge_requests, :count).by(1)
- end
+ it 'creates a new merge request', :sidekiq_might_not_need_inline do
+ expect { create_merge_request }.to change(target_project.merge_requests, :count).by(1)
end
end
@@ -1513,61 +1612,6 @@ describe Projects::IssuesController do
expect(note_json['author']['status_tooltip_html']).to be_present
end
- context 'is_gitlab_employee attribute' do
- subject { get :discussions, params: { namespace_id: project.namespace, project_id: project, id: issue.iid } }
-
- before do
- allow(Gitlab).to receive(:com?).and_return(true)
- note_user = discussion.author
- note_user.update(email: email)
- note_user.confirm
- end
-
- shared_examples 'non inclusion of gitlab employee badge' do
- it 'does not render the is_gitlab_employee attribute' do
- subject
-
- note_json = json_response.first['notes'].first
-
- expect(note_json['author']['is_gitlab_employee']).to be nil
- end
- end
-
- context 'when user is a gitlab employee' do
- let(:email) { 'test@gitlab.com' }
-
- it 'renders the is_gitlab_employee attribute' do
- subject
-
- note_json = json_response.first['notes'].first
-
- expect(note_json['author']['is_gitlab_employee']).to be true
- end
-
- context 'when feature flag is disabled' do
- before do
- stub_feature_flags(gitlab_employee_badge: false)
- end
-
- it_behaves_like 'non inclusion of gitlab employee badge'
- end
- end
-
- context 'when user is not a gitlab employee' do
- let(:email) { 'test@example.com' }
-
- it_behaves_like 'non inclusion of gitlab employee badge'
-
- context 'when feature flag is disabled' do
- before do
- stub_feature_flags(gitlab_employee_badge: false)
- end
-
- it_behaves_like 'non inclusion of gitlab employee badge'
- end
- end
- end
-
it 'does not cause an extra query for the status' do
control = ActiveRecord::QueryRecorder.new do
get :discussions, params: { namespace_id: project.namespace, project_id: project, id: issue.iid }
@@ -1660,6 +1704,33 @@ describe Projects::IssuesController do
end
end
+ describe 'GET #designs' do
+ context 'when project has moved' do
+ let(:new_project) { create(:project) }
+ let(:issue) { create(:issue, project: new_project) }
+
+ before do
+ sign_in(user)
+
+ project.route.destroy
+ new_project.redirect_routes.create!(path: project.full_path)
+ new_project.add_developer(user)
+ end
+
+ it 'redirects from an old issue/designs correctly' do
+ get :designs,
+ params: {
+ namespace_id: project.namespace,
+ project_id: project,
+ id: issue
+ }
+
+ expect(response).to redirect_to(designs_project_issue_path(new_project, issue))
+ expect(response).to have_gitlab_http_status(:found)
+ end
+ end
+ end
+
context 'private project with token authentication' do
let(:private_project) { create(:project, :private) }
diff --git a/spec/controllers/projects/logs_controller_spec.rb b/spec/controllers/projects/logs_controller_spec.rb
index ea71dbe45aa..e86a42b03c8 100644
--- a/spec/controllers/projects/logs_controller_spec.rb
+++ b/spec/controllers/projects/logs_controller_spec.rb
@@ -16,16 +16,23 @@ describe Projects::LogsController do
let(:container) { 'container-1' }
before do
- project.add_maintainer(user)
-
sign_in(user)
end
describe 'GET #index' do
let(:empty_project) { create(:project) }
+ it 'returns 404 with developer access' do
+ project.add_developer(user)
+
+ get :index, params: environment_params
+
+ expect(response).to have_gitlab_http_status(:not_found)
+ end
+
it 'renders empty logs page if no environment exists' do
empty_project.add_maintainer(user)
+
get :index, params: { namespace_id: empty_project.namespace, project_id: empty_project }
expect(response).to be_ok
@@ -33,6 +40,8 @@ describe Projects::LogsController do
end
it 'renders index template' do
+ project.add_maintainer(user)
+
get :index, params: environment_params
expect(response).to be_ok
@@ -50,7 +59,7 @@ describe Projects::LogsController do
container_name: container
}
end
- let(:service_result_json) { JSON.parse(service_result.to_json) }
+ let(:service_result_json) { Gitlab::Json.parse(service_result.to_json) }
let_it_be(:cluster) { create(:cluster, :provided_by_gcp, environment_scope: '*', projects: [project]) }
@@ -60,70 +69,84 @@ describe Projects::LogsController do
end
end
- it 'returns the service result' do
+ it 'returns 404 with developer access' do
+ project.add_developer(user)
+
get endpoint, params: environment_params(pod_name: pod_name, format: :json)
- expect(response).to have_gitlab_http_status(:success)
- expect(json_response).to eq(service_result_json)
+ expect(response).to have_gitlab_http_status(:not_found)
end
- it 'registers a usage of the endpoint' do
- expect(::Gitlab::UsageCounters::PodLogs).to receive(:increment).with(project.id)
+ context 'with maintainer access' do
+ before do
+ project.add_maintainer(user)
+ end
- get endpoint, params: environment_params(pod_name: pod_name, format: :json)
+ it 'returns the service result' do
+ get endpoint, params: environment_params(pod_name: pod_name, format: :json)
- expect(response).to have_gitlab_http_status(:success)
- end
+ expect(response).to have_gitlab_http_status(:success)
+ expect(json_response).to eq(service_result_json)
+ end
- it 'sets the polling header' do
- get endpoint, params: environment_params(pod_name: pod_name, format: :json)
+ it 'registers a usage of the endpoint' do
+ expect(::Gitlab::UsageCounters::PodLogs).to receive(:increment).with(project.id)
- expect(response).to have_gitlab_http_status(:success)
- expect(response.headers['Poll-Interval']).to eq('3000')
- end
+ get endpoint, params: environment_params(pod_name: pod_name, format: :json)
- context 'when service is processing' do
- let(:service_result) { nil }
+ expect(response).to have_gitlab_http_status(:success)
+ end
- it 'returns a 202' do
+ it 'sets the polling header' do
get endpoint, params: environment_params(pod_name: pod_name, format: :json)
- expect(response).to have_gitlab_http_status(:accepted)
+ expect(response).to have_gitlab_http_status(:success)
+ expect(response.headers['Poll-Interval']).to eq('3000')
end
- end
- shared_examples 'unsuccessful execution response' do |message|
- let(:service_result) do
- {
- status: :error,
- message: message
- }
- end
+ context 'when service is processing' do
+ let(:service_result) { nil }
- it 'returns the error' do
- get endpoint, params: environment_params(pod_name: pod_name, format: :json)
+ it 'returns a 202' do
+ get endpoint, params: environment_params(pod_name: pod_name, format: :json)
- expect(response).to have_gitlab_http_status(:bad_request)
- expect(json_response).to eq(service_result_json)
+ expect(response).to have_gitlab_http_status(:accepted)
+ end
end
- end
- context 'when service is failing' do
- it_behaves_like 'unsuccessful execution response', 'some error'
- end
+ shared_examples 'unsuccessful execution response' do |message|
+ let(:service_result) do
+ {
+ status: :error,
+ message: message
+ }
+ end
- context 'when cluster is nil' do
- let!(:cluster) { nil }
+ it 'returns the error' do
+ get endpoint, params: environment_params(pod_name: pod_name, format: :json)
- it_behaves_like 'unsuccessful execution response', 'Environment does not have deployments'
- end
+ expect(response).to have_gitlab_http_status(:bad_request)
+ expect(json_response).to eq(service_result_json)
+ end
+ end
- context 'when namespace is empty' do
- before do
- allow(environment).to receive(:deployment_namespace).and_return('')
+ context 'when service is failing' do
+ it_behaves_like 'unsuccessful execution response', 'some error'
+ end
+
+ context 'when cluster is nil' do
+ let!(:cluster) { nil }
+
+ it_behaves_like 'unsuccessful execution response', 'Environment does not have deployments'
end
- it_behaves_like 'unsuccessful execution response', 'Environment does not have deployments'
+ context 'when namespace is empty' do
+ before do
+ allow(environment).to receive(:deployment_namespace).and_return('')
+ end
+
+ it_behaves_like 'unsuccessful execution response', 'Environment does not have deployments'
+ end
end
end
diff --git a/spec/controllers/projects/merge_requests_controller_spec.rb b/spec/controllers/projects/merge_requests_controller_spec.rb
index aaeaf53d100..7d9e42fcc2d 100644
--- a/spec/controllers/projects/merge_requests_controller_spec.rb
+++ b/spec/controllers/projects/merge_requests_controller_spec.rb
@@ -935,34 +935,6 @@ describe Projects::MergeRequestsController do
}])
end
end
-
- context 'when feature flag :ci_expose_arbitrary_artifacts_in_mr is disabled' do
- let(:job_options) do
- {
- artifacts: {
- paths: ['ci_artifacts.txt'],
- expose_as: 'Exposed artifact'
- }
- }
- end
- let(:report) { double }
-
- before do
- stub_feature_flags(ci_expose_arbitrary_artifacts_in_mr: false)
- end
-
- it 'does not send polling interval' do
- expect(Gitlab::PollingInterval).not_to receive(:set_header)
-
- subject
- end
-
- it 'returns 204 HTTP status' do
- subject
-
- expect(response).to have_gitlab_http_status(:no_content)
- end
- end
end
context 'when pipeline does not have jobs with exposed artifacts' do
@@ -1114,6 +1086,150 @@ describe Projects::MergeRequestsController do
end
end
+ describe 'GET terraform_reports' do
+ let(:merge_request) do
+ create(:merge_request,
+ :with_merge_request_pipeline,
+ target_project: project,
+ source_project: project)
+ end
+
+ let(:pipeline) do
+ create(:ci_pipeline,
+ :success,
+ :with_terraform_reports,
+ project: merge_request.source_project,
+ ref: merge_request.source_branch,
+ sha: merge_request.diff_head_sha)
+ end
+
+ before do
+ allow_any_instance_of(MergeRequest)
+ .to receive(:find_terraform_reports)
+ .and_return(report)
+
+ allow_any_instance_of(MergeRequest)
+ .to receive(:actual_head_pipeline)
+ .and_return(pipeline)
+ end
+
+ subject do
+ get :terraform_reports, params: {
+ namespace_id: project.namespace.to_param,
+ project_id: project,
+ id: merge_request.iid
+ },
+ format: :json
+ end
+
+ describe 'permissions on a public project with private CI/CD' do
+ let(:project) { create :project, :repository, :public, :builds_private }
+ let(:report) { { status: :parsed, data: [] } }
+
+ context 'while signed out' do
+ before do
+ sign_out(user)
+ end
+
+ it 'responds with a 404' do
+ subject
+
+ expect(response).to have_gitlab_http_status(:not_found)
+ expect(response.body).to be_blank
+ end
+ end
+
+ context 'while signed in as an unrelated user' do
+ before do
+ sign_in(create(:user))
+ end
+
+ it 'responds with a 404' do
+ subject
+
+ expect(response).to have_gitlab_http_status(:not_found)
+ expect(response.body).to be_blank
+ end
+ end
+ end
+
+ context 'when pipeline has jobs with terraform reports' do
+ before do
+ allow_next_instance_of(MergeRequest) do |merge_request|
+ allow(merge_request).to receive(:has_terraform_reports?).and_return(true)
+ end
+ end
+
+ context 'when processing terraform reports is in progress' do
+ let(:report) { { status: :parsing } }
+
+ it 'sends polling interval' do
+ expect(Gitlab::PollingInterval).to receive(:set_header)
+
+ subject
+ end
+
+ it 'returns 204 HTTP status' do
+ subject
+
+ expect(response).to have_gitlab_http_status(:no_content)
+ end
+ end
+
+ context 'when processing terraform reports is completed' do
+ let(:report) { { status: :parsed, data: pipeline.terraform_reports.plans } }
+
+ it 'returns terraform reports' do
+ subject
+
+ expect(response).to have_gitlab_http_status(:ok)
+ expect(json_response).to match(
+ a_hash_including(
+ 'tfplan.json' => hash_including(
+ 'create' => 0,
+ 'delete' => 0,
+ 'update' => 1
+ )
+ )
+ )
+ end
+ end
+
+ context 'when user created corrupted terraform reports' do
+ let(:report) { { status: :error, status_reason: 'Failed to parse terraform reports' } }
+
+ it 'does not send polling interval' do
+ expect(Gitlab::PollingInterval).not_to receive(:set_header)
+
+ subject
+ end
+
+ it 'returns 400 HTTP status' do
+ subject
+
+ expect(response).to have_gitlab_http_status(:bad_request)
+ expect(json_response).to eq({ 'status_reason' => 'Failed to parse terraform reports' })
+ end
+ end
+ end
+
+ context 'when pipeline does not have jobs with terraform reports' do
+ before do
+ allow_next_instance_of(MergeRequest) do |merge_request|
+ allow(merge_request).to receive(:has_terraform_reports?).and_return(false)
+ end
+ end
+
+ let(:report) { { status: :error } }
+
+ it 'returns error' do
+ subject
+
+ expect(response).to have_gitlab_http_status(:bad_request)
+ end
+ end
+ end
+
describe 'GET test_reports' do
let(:merge_request) do
create(:merge_request,
@@ -1225,6 +1341,141 @@ describe Projects::MergeRequestsController do
end
end
+ describe 'GET accessibility_reports' do
+ let(:merge_request) do
+ create(:merge_request,
+ :with_diffs,
+ :with_merge_request_pipeline,
+ target_project: project,
+ source_project: project
+ )
+ end
+
+ let(:pipeline) do
+ create(:ci_pipeline,
+ :success,
+ project: merge_request.source_project,
+ ref: merge_request.source_branch,
+ sha: merge_request.diff_head_sha)
+ end
+
+ before do
+ allow_any_instance_of(MergeRequest)
+ .to receive(:compare_accessibility_reports)
+ .and_return(accessibility_comparison)
+
+ allow_any_instance_of(MergeRequest)
+ .to receive(:actual_head_pipeline)
+ .and_return(pipeline)
+ end
+
+ subject do
+ get :accessibility_reports, params: {
+ namespace_id: project.namespace.to_param,
+ project_id: project,
+ id: merge_request.iid
+ },
+ format: :json
+ end
+
+ context 'permissions on a public project with private CI/CD' do
+ let(:project) { create(:project, :repository, :public, :builds_private) }
+ let(:accessibility_comparison) { { status: :parsed, data: { summary: 1 } } }
+
+ context 'while signed out' do
+ before do
+ sign_out(user)
+ end
+
+ it 'responds with a 404' do
+ subject
+
+ expect(response).to have_gitlab_http_status(:not_found)
+ expect(response.body).to be_blank
+ end
+ end
+
+ context 'while signed in as an unrelated user' do
+ before do
+ sign_in(create(:user))
+ end
+
+ it 'responds with a 404' do
+ subject
+
+ expect(response).to have_gitlab_http_status(:not_found)
+ expect(response.body).to be_blank
+ end
+ end
+ end
+
+ context 'when feature flag is disabled' do
+ let(:accessibility_comparison) { { status: :parsed, data: { summary: 1 } } }
+
+ before do
+ stub_feature_flags(accessibility_report_view: false)
+ end
+
+ it 'returns 204 HTTP status' do
+ subject
+
+ expect(response).to have_gitlab_http_status(:no_content)
+ end
+ end
+
+ context 'when pipeline has jobs with accessibility reports' do
+ before do
+ allow_any_instance_of(MergeRequest)
+ .to receive(:has_accessibility_reports?)
+ .and_return(true)
+ end
+
+ context 'when processing accessibility reports is in progress' do
+ let(:accessibility_comparison) { { status: :parsing } }
+
+ it 'sends polling interval' do
+ expect(Gitlab::PollingInterval).to receive(:set_header)
+
+ subject
+ end
+
+ it 'returns 204 HTTP status' do
+ subject
+
+ expect(response).to have_gitlab_http_status(:no_content)
+ end
+ end
+
+ context 'when processing accessibility reports is completed' do
+ let(:accessibility_comparison) { { status: :parsed, data: { summary: 1 } } }
+
+ it 'returns accessibility reports' do
+ subject
+
+ expect(response).to have_gitlab_http_status(:ok)
+ expect(json_response).to eq({ 'summary' => 1 })
+ end
+ end
+
+ context 'when user created corrupted accessibility reports' do
+ let(:accessibility_comparison) { { status: :error, status_reason: 'This merge request does not have accessibility reports' } }
+
+ it 'does not send polling interval' do
+ expect(Gitlab::PollingInterval).not_to receive(:set_header)
+
+ subject
+ end
+
+ it 'returns 400 HTTP status' do
+ subject
+
+ expect(response).to have_gitlab_http_status(:bad_request)
+ expect(json_response).to eq({ 'status_reason' => 'This merge request does not have accessibility reports' })
+ end
+ end
+ end
+ end
+
describe 'POST remove_wip' do
before do
merge_request.title = merge_request.wip_title
diff --git a/spec/controllers/projects/mirrors_controller_spec.rb b/spec/controllers/projects/mirrors_controller_spec.rb
index faeade0d737..8cd940978c0 100644
--- a/spec/controllers/projects/mirrors_controller_spec.rb
+++ b/spec/controllers/projects/mirrors_controller_spec.rb
@@ -189,7 +189,7 @@ describe Projects::MirrorsController do
context 'no data in cache' do
it 'requests the cache to be filled and returns a 204 response' do
- expect(ReactiveCachingWorker).to receive(:perform_async).with(cache.class, cache.id).at_least(:once)
+ expect(ExternalServiceReactiveCachingWorker).to receive(:perform_async).with(cache.class, cache.id).at_least(:once)
do_get(project)
diff --git a/spec/controllers/projects/pipelines_controller_spec.rb b/spec/controllers/projects/pipelines_controller_spec.rb
index 9d243bf5a7f..b3d8fb94fb3 100644
--- a/spec/controllers/projects/pipelines_controller_spec.rb
+++ b/spec/controllers/projects/pipelines_controller_spec.rb
@@ -145,11 +145,81 @@ describe Projects::PipelinesController do
end
end
- def get_pipelines_index_json
+ context 'filter by scope' do
+ it 'returns matched pipelines' do
+ get_pipelines_index_json(scope: 'running')
+
+ check_pipeline_response(returned: 2, all: 6, running: 2, pending: 1, finished: 3)
+ end
+
+ context 'scope is branches or tags' do
+ before do
+ create(:ci_pipeline, :failed, project: project, ref: 'v1.0.0', tag: true)
+ end
+
+ context 'when scope is branches' do
+ it 'returns matched pipelines' do
+ get_pipelines_index_json(scope: 'branches')
+
+ check_pipeline_response(returned: 1, all: 7, running: 2, pending: 1, finished: 4)
+ end
+ end
+
+ context 'when scope is tags' do
+ it 'returns matched pipelines' do
+ get_pipelines_index_json(scope: 'tags')
+
+ check_pipeline_response(returned: 1, all: 7, running: 2, pending: 1, finished: 4)
+ end
+ end
+ end
+ end
+
+ context 'filter by username' do
+ let!(:pipeline) { create(:ci_pipeline, :running, project: project, user: user) }
+
+ context 'when username exists' do
+ it 'returns matched pipelines' do
+ get_pipelines_index_json(username: user.username)
+
+ check_pipeline_response(returned: 1, all: 1, running: 1, pending: 0, finished: 0)
+ end
+ end
+
+ context 'when username does not exist' do
+ it 'returns empty' do
+ get_pipelines_index_json(username: 'invalid-username')
+
+ check_pipeline_response(returned: 0, all: 0, running: 0, pending: 0, finished: 0)
+ end
+ end
+ end
+
+ context 'filter by ref' do
+ let!(:pipeline) { create(:ci_pipeline, :running, project: project, ref: 'branch-1') }
+
+ context 'when pipelines with the ref exists' do
+ it 'returns matched pipelines' do
+ get_pipelines_index_json(ref: 'branch-1')
+
+ check_pipeline_response(returned: 1, all: 1, running: 1, pending: 0, finished: 0)
+ end
+ end
+
+ context 'when no pipeline with the ref exists' do
+ it 'returns empty list' do
+ get_pipelines_index_json(ref: 'invalid-ref')
+
+ check_pipeline_response(returned: 0, all: 0, running: 0, pending: 0, finished: 0)
+ end
+ end
+ end
+
+ def get_pipelines_index_json(params = {})
get :index, params: {
namespace_id: project.namespace,
project_id: project
- },
+ }.merge(params),
format: :json
end
@@ -199,6 +269,18 @@ describe Projects::PipelinesController do
user: user
)
end
+
+ def check_pipeline_response(returned:, all:, running:, pending:, finished:)
+ aggregate_failures do
+ expect(response).to match_response_schema('pipeline')
+
+ expect(json_response['pipelines'].count).to eq returned
+ expect(json_response['count']['all'].to_i).to eq all
+ expect(json_response['count']['running'].to_i).to eq running
+ expect(json_response['count']['pending'].to_i).to eq pending
+ expect(json_response['count']['finished'].to_i).to eq finished
+ end
+ end
end
describe 'GET show.json' do
@@ -748,12 +830,10 @@ describe Projects::PipelinesController do
context 'when feature is enabled' do
before do
- stub_feature_flags(junit_pipeline_view: true)
+ stub_feature_flags(junit_pipeline_view: project)
end
context 'when pipeline does not have a test report' do
- let(:pipeline) { create(:ci_pipeline, project: project) }
-
it 'renders an empty test report' do
get_test_report_json
@@ -763,7 +843,11 @@ describe Projects::PipelinesController do
end
context 'when pipeline has a test report' do
- let(:pipeline) { create(:ci_pipeline, :with_test_reports, project: project) }
+ before do
+ create(:ci_build, name: 'rspec', pipeline: pipeline).tap do |build|
+ create(:ci_job_artifact, :junit, job: build)
+ end
+ end
it 'renders the test report' do
get_test_report_json
@@ -773,25 +857,28 @@ describe Projects::PipelinesController do
end
end
- context 'when pipeline has corrupt test reports' do
- let(:pipeline) { create(:ci_pipeline, project: project) }
-
+ context 'when pipeline has a corrupt test report artifact' do
before do
- job = create(:ci_build, pipeline: pipeline)
- create(:ci_job_artifact, :junit_with_corrupted_data, job: job, project: project)
- end
+ create(:ci_build, name: 'rspec', pipeline: pipeline).tap do |build|
+ create(:ci_job_artifact, :junit_with_corrupted_data, job: build)
+ end
- it 'renders the test reports' do
get_test_report_json
+ end
+ it 'renders the test reports' do
expect(response).to have_gitlab_http_status(:ok)
- expect(json_response['status']).to eq('error_parsing_report')
+ expect(json_response['test_suites'].count).to eq(1)
+ end
+
+ it 'returns a suite_error on the suite with corrupted XML' do
+ expect(json_response['test_suites'].first['suite_error']).to eq('JUnit XML parsing failed: 1:1: FATAL: Document is empty')
end
end
context 'when junit_pipeline_screenshots_view is enabled' do
before do
- stub_feature_flags(junit_pipeline_screenshots_view: { enabled: true, thing: project })
+ stub_feature_flags(junit_pipeline_screenshots_view: project)
end
context 'when test_report contains attachment and scope is with_attachment as a URL param' do
@@ -820,7 +907,7 @@ describe Projects::PipelinesController do
context 'when junit_pipeline_screenshots_view is disabled' do
before do
- stub_feature_flags(junit_pipeline_screenshots_view: { enabled: false, thing: project })
+ stub_feature_flags(junit_pipeline_screenshots_view: false)
end
context 'when test_report contains attachment and scope is with_attachment as a URL param' do
diff --git a/spec/controllers/projects/prometheus/alerts_controller_spec.rb b/spec/controllers/projects/prometheus/alerts_controller_spec.rb
index 451834e0962..e936cb5916e 100644
--- a/spec/controllers/projects/prometheus/alerts_controller_spec.rb
+++ b/spec/controllers/projects/prometheus/alerts_controller_spec.rb
@@ -352,7 +352,7 @@ describe Projects::Prometheus::AlertsController do
get :metrics_dashboard, params: request_params(id: metric.id, environment_id: alert.environment.id), format: :json
expect(response).to have_gitlab_http_status(:ok)
- expect(json_response.keys).to contain_exactly('dashboard', 'status')
+ expect(json_response.keys).to contain_exactly('dashboard', 'status', 'metrics_data')
end
it 'is the correct embed' do
diff --git a/spec/controllers/projects/refs_controller_spec.rb b/spec/controllers/projects/refs_controller_spec.rb
index 646c7a7db7c..b043e7f2538 100644
--- a/spec/controllers/projects/refs_controller_spec.rb
+++ b/spec/controllers/projects/refs_controller_spec.rb
@@ -12,25 +12,27 @@ describe Projects::RefsController do
end
describe 'GET #logs_tree' do
+ let(:path) { 'foo/bar/baz.html' }
+
def default_get(format = :html)
get :logs_tree,
params: {
namespace_id: project.namespace.to_param,
project_id: project,
id: 'master',
- path: 'foo/bar/baz.html'
+ path: path
},
format: format
end
- def xhr_get(format = :html)
+ def xhr_get(format = :html, params = {})
get :logs_tree, params: {
namespace_id: project.namespace.to_param,
project_id: project,
id: 'master',
- path: 'foo/bar/baz.html',
+ path: path,
format: format
- }, xhr: true
+ }.merge(params), xhr: true
end
it 'never throws MissingTemplate' do
@@ -52,13 +54,27 @@ describe Projects::RefsController do
expect(response).to be_successful
end
- it 'renders JSON' do
- expect(::Gitlab::GitalyClient).to receive(:allow_ref_name_caching).and_call_original
+ context 'when json is requested' do
+ it 'renders JSON' do
+ expect(::Gitlab::GitalyClient).to receive(:allow_ref_name_caching).and_call_original
- xhr_get(:json)
+ xhr_get(:json)
- expect(response).to be_successful
- expect(json_response).to be_kind_of(Array)
+ expect(response).to be_successful
+ expect(json_response).to be_kind_of(Array)
+ end
+
+ it 'caches tree summary data', :use_clean_rails_memory_store_caching do
+ expect_next_instance_of(::Gitlab::TreeSummary) do |instance|
+ expect(instance).to receive_messages(summarize: ['logs'], next_offset: 50, more?: true)
+ end
+
+ xhr_get(:json, offset: 25)
+
+ cache_key = "projects/#{project.id}/logs/#{project.commit.id}/#{path}/25"
+ expect(Rails.cache.fetch(cache_key)).to eq(['logs', 50])
+ expect(response.headers['More-Logs-Offset']).to eq(50)
+ end
end
end
end
diff --git a/spec/controllers/projects/registry/repositories_controller_spec.rb b/spec/controllers/projects/registry/repositories_controller_spec.rb
index c641a45a216..badb84f9b50 100644
--- a/spec/controllers/projects/registry/repositories_controller_spec.rb
+++ b/spec/controllers/projects/registry/repositories_controller_spec.rb
@@ -3,8 +3,8 @@
require 'spec_helper'
describe Projects::Registry::RepositoriesController do
- let(:user) { create(:user) }
- let(:project) { create(:project, :private) }
+ let_it_be(:user) { create(:user) }
+ let_it_be(:project) { create(:project, :private) }
before do
sign_in(user)
@@ -16,6 +16,22 @@ describe Projects::Registry::RepositoriesController do
project.add_developer(user)
end
+ shared_examples 'with name parameter' do
+ let_it_be(:repo) { create(:container_repository, project: project, name: 'my_searched_image') }
+ let_it_be(:another_repo) { create(:container_repository, project: project, name: 'bar') }
+
+ it 'returns the searched repo' do
+ go_to_index(format: :json, params: { name: 'my_searched_image' })
+
+ expect(response).to have_gitlab_http_status(:ok)
+ expect(json_response.length).to eq 1
+ expect(json_response.first).to include(
+ 'id' => repo.id,
+ 'name' => repo.name
+ )
+ end
+ end
+
shared_examples 'renders a list of repositories' do
context 'when root container repository exists' do
before do
@@ -60,6 +76,8 @@ describe Projects::Registry::RepositoriesController do
expect(response).to match_response_schema('registry/repositories')
expect(response).to include_pagination_headers
end
+
+ it_behaves_like 'with name parameter'
end
context 'when there are no tags for this repository' do
@@ -138,11 +156,11 @@ describe Projects::Registry::RepositoriesController do
end
end
- def go_to_index(format: :html)
- get :index, params: {
+ def go_to_index(format: :html, params: {} )
+ get :index, params: params.merge({
namespace_id: project.namespace,
project_id: project
- },
+ }),
format: format
end
diff --git a/spec/controllers/projects/service_hook_logs_controller_spec.rb b/spec/controllers/projects/service_hook_logs_controller_spec.rb
index ca57b0579a8..a5130cd6e32 100644
--- a/spec/controllers/projects/service_hook_logs_controller_spec.rb
+++ b/spec/controllers/projects/service_hook_logs_controller_spec.rb
@@ -24,7 +24,7 @@ describe Projects::ServiceHookLogsController do
describe 'GET #show' do
subject { get :show, params: log_params }
- it do
+ specify do
expect(response).to be_successful
end
end
diff --git a/spec/controllers/projects/settings/access_tokens_controller_spec.rb b/spec/controllers/projects/settings/access_tokens_controller_spec.rb
new file mode 100644
index 00000000000..884a5bc2836
--- /dev/null
+++ b/spec/controllers/projects/settings/access_tokens_controller_spec.rb
@@ -0,0 +1,190 @@
+# frozen_string_literal: true
+
+require('spec_helper')
+
+describe Projects::Settings::AccessTokensController do
+ let_it_be(:user) { create(:user) }
+ let_it_be(:project) { create(:project) }
+
+ before_all do
+ project.add_maintainer(user)
+ end
+
+ before do
+ sign_in(user)
+ end
+
+ shared_examples 'feature unavailability' do
+ context 'when flag is disabled' do
+ before do
+ stub_feature_flags(resource_access_token: false)
+ end
+
+ it { is_expected.to have_gitlab_http_status(:not_found) }
+ end
+
+ context 'when environment is Gitlab.com' do
+ before do
+ allow(Gitlab).to receive(:com?).and_return(true)
+ end
+
+ it { is_expected.to have_gitlab_http_status(:not_found) }
+ end
+ end
+
+ describe '#index' do
+ subject { get :index, params: { namespace_id: project.namespace, project_id: project } }
+
+ it_behaves_like 'feature unavailability'
+
+ context 'when feature is available' do
+ let_it_be(:bot_user) { create(:user, :project_bot) }
+ let_it_be(:active_project_access_token) { create(:personal_access_token, user: bot_user) }
+ let_it_be(:inactive_project_access_token) { create(:personal_access_token, :revoked, user: bot_user) }
+
+ before_all do
+ project.add_maintainer(bot_user)
+ end
+
+ before do
+ enable_feature
+ end
+
+ it 'retrieves active project access tokens' do
+ subject
+
+ expect(assigns(:active_project_access_tokens)).to contain_exactly(active_project_access_token)
+ end
+
+ it 'retrieves inactive project access tokens' do
+ subject
+
+ expect(assigns(:inactive_project_access_tokens)).to contain_exactly(inactive_project_access_token)
+ end
+
+ it 'lists all available scopes' do
+ subject
+
+ expect(assigns(:scopes)).to eq(Gitlab::Auth.resource_bot_scopes)
+ end
+
+ it 'retrieves newly created personal access token value' do
+ token_value = 'random-value'
+ allow(PersonalAccessToken).to receive(:redis_getdel).with("#{user.id}:#{project.id}").and_return(token_value)
+
+ subject
+
+ expect(assigns(:new_project_access_token)).to eq(token_value)
+ end
+ end
+ end
+
+ describe '#create', :clean_gitlab_redis_shared_state do
+ subject { post :create, params: { namespace_id: project.namespace, project_id: project }.merge(project_access_token: access_token_params) }
+
+ let_it_be(:access_token_params) { {} }
+
+ it_behaves_like 'feature unavailability'
+
+ context 'when feature is available' do
+ let_it_be(:access_token_params) { { name: 'Nerd bot', scopes: ["api"], expires_at: 1.month.since.to_date } }
+
+ before do
+ enable_feature
+ end
+
+ def created_token
+ PersonalAccessToken.order(:created_at).last
+ end
+
+ it 'returns success message' do
+ subject
+
+ expect(response.flash[:notice]).to match(/\AYour new project access token has been created./i)
+ end
+
+ it 'creates project access token' do
+ subject
+
+ expect(created_token.name).to eq(access_token_params[:name])
+ expect(created_token.scopes).to eq(access_token_params[:scopes])
+ expect(created_token.expires_at).to eq(access_token_params[:expires_at])
+ end
+
+ it 'creates project bot user' do
+ subject
+
+ expect(created_token.user).to be_project_bot
+ end
+
+ it 'stores newly created token redis store' do
+ expect(PersonalAccessToken).to receive(:redis_store!)
+
+ subject
+ end
+
+ it { expect { subject }.to change { User.count }.by(1) }
+ it { expect { subject }.to change { PersonalAccessToken.count }.by(1) }
+
+ context 'when unsuccessful' do
+ before do
+ allow_next_instance_of(ResourceAccessTokens::CreateService) do |service|
+ allow(service).to receive(:execute).and_return ServiceResponse.error(message: 'Failed!')
+ end
+ end
+
+ it { expect(subject).to render_template(:index) }
+ end
+ end
+ end
+
+ describe '#revoke' do
+ subject { put :revoke, params: { namespace_id: project.namespace, project_id: project, id: project_access_token } }
+
+ let_it_be(:bot_user) { create(:user, :project_bot) }
+ let_it_be(:project_access_token) { create(:personal_access_token, user: bot_user) }
+
+ before_all do
+ project.add_maintainer(bot_user)
+ end
+
+ it_behaves_like 'feature unavailability'
+
+ context 'when feature is available' do
+ before do
+ enable_feature
+ end
+
+ it 'revokes token access' do
+ subject
+
+ expect(project_access_token.reload.revoked?).to be true
+ end
+
+ it 'removed membership of bot user' do
+ subject
+
+ expect(project.reload.bots).not_to include(bot_user)
+ end
+
+ it 'blocks project bot user' do
+ subject
+
+ expect(bot_user.reload.blocked?).to be true
+ end
+
+ it 'converts issuables of the bot user to ghost user' do
+ issue = create(:issue, author: bot_user)
+
+ subject
+
+ expect(issue.reload.author.ghost?).to be true
+ end
+ end
+ end
+
+ def enable_feature
+ allow(Gitlab).to receive(:com?).and_return(false)
+ stub_feature_flags(resource_access_token: true)
+ end
+end
diff --git a/spec/controllers/projects/settings/repository_controller_spec.rb b/spec/controllers/projects/settings/repository_controller_spec.rb
index 847c80e8917..fb9cdd860dc 100644
--- a/spec/controllers/projects/settings/repository_controller_spec.rb
+++ b/spec/controllers/projects/settings/repository_controller_spec.rb
@@ -36,7 +36,7 @@ describe Projects::Settings::RepositoryController do
describe 'POST create_deploy_token' do
context 'when ajax_new_deploy_token feature flag is disabled for the project' do
before do
- stub_feature_flags(ajax_new_deploy_token: { enabled: false, thing: project })
+ stub_feature_flags(ajax_new_deploy_token: false)
end
it_behaves_like 'a created deploy token' do
@@ -73,7 +73,7 @@ describe Projects::Settings::RepositoryController do
'id' => be_a(Integer),
'name' => deploy_token_params[:name],
'username' => deploy_token_params[:username],
- 'expires_at' => Time.parse(deploy_token_params[:expires_at]),
+ 'expires_at' => Time.zone.parse(deploy_token_params[:expires_at]),
'token' => be_a(String),
'scopes' => deploy_token_params.inject([]) do |scopes, kv|
key, value = kv
diff --git a/spec/controllers/projects/snippets_controller_spec.rb b/spec/controllers/projects/snippets_controller_spec.rb
index 284789305e2..b5f4929d8ce 100644
--- a/spec/controllers/projects/snippets_controller_spec.rb
+++ b/spec/controllers/projects/snippets_controller_spec.rb
@@ -127,7 +127,7 @@ describe Projects::SnippetsController do
.to log_spam(title: 'Title', user_id: user.id, noteable_type: 'ProjectSnippet')
end
- it 'renders :new with recaptcha disabled' do
+ it 'renders :new with reCAPTCHA disabled' do
stub_application_setting(recaptcha_enabled: false)
create_snippet(project, visibility_level: Snippet::PUBLIC)
@@ -135,18 +135,18 @@ describe Projects::SnippetsController do
expect(response).to render_template(:new)
end
- context 'recaptcha enabled' do
+ context 'reCAPTCHA enabled' do
before do
stub_application_setting(recaptcha_enabled: true)
end
- it 'renders :verify with recaptcha enabled' do
+ it 'renders :verify with reCAPTCHA enabled' do
create_snippet(project, visibility_level: Snippet::PUBLIC)
expect(response).to render_template(:verify)
end
- it 'renders snippet page when recaptcha verified' do
+ it 'renders snippet page when reCAPTCHA verified' do
spammy_title = 'Whatever'
spam_logs = create_list(:spam_log, 2, user: user, title: spammy_title)
@@ -223,7 +223,7 @@ describe Projects::SnippetsController do
.to log_spam(title: 'Foo', user_id: user.id, noteable_type: 'ProjectSnippet')
end
- it 'renders :edit with recaptcha disabled' do
+ it 'renders :edit with reCAPTCHA disabled' do
stub_application_setting(recaptcha_enabled: false)
update_snippet(title: 'Foo')
@@ -231,18 +231,18 @@ describe Projects::SnippetsController do
expect(response).to render_template(:edit)
end
- context 'recaptcha enabled' do
+ context 'reCAPTCHA enabled' do
before do
stub_application_setting(recaptcha_enabled: true)
end
- it 'renders :verify with recaptcha enabled' do
+ it 'renders :verify with reCAPTCHA enabled' do
update_snippet(title: 'Foo')
expect(response).to render_template(:verify)
end
- it 'renders snippet page when recaptcha verified' do
+ it 'renders snippet page when reCAPTCHA verified' do
spammy_title = 'Whatever'
spam_logs = create_list(:spam_log, 2, user: user, title: spammy_title)
@@ -268,7 +268,7 @@ describe Projects::SnippetsController do
.to log_spam(title: 'Foo', user_id: user.id, noteable_type: 'ProjectSnippet')
end
- it 'renders :edit with recaptcha disabled' do
+ it 'renders :edit with reCAPTCHA disabled' do
stub_application_setting(recaptcha_enabled: false)
update_snippet(title: 'Foo', visibility_level: Snippet::PUBLIC)
@@ -276,18 +276,18 @@ describe Projects::SnippetsController do
expect(response).to render_template(:edit)
end
- context 'recaptcha enabled' do
+ context 'reCAPTCHA enabled' do
before do
stub_application_setting(recaptcha_enabled: true)
end
- it 'renders :verify with recaptcha enabled' do
+ it 'renders :verify' do
update_snippet(title: 'Foo', visibility_level: Snippet::PUBLIC)
expect(response).to render_template(:verify)
end
- it 'renders snippet page when recaptcha verified' do
+ it 'renders snippet page' do
spammy_title = 'Whatever'
spam_logs = create_list(:spam_log, 2, user: user, title: spammy_title)
@@ -346,20 +346,6 @@ describe Projects::SnippetsController do
expect(assigns(:blob)).to eq(project_snippet.blobs.first)
end
-
- context 'when feature flag version_snippets is disabled' do
- before do
- stub_feature_flags(version_snippets: false)
- end
-
- it 'returns the snippet database content' do
- subject
-
- blob = assigns(:blob)
-
- expect(blob.data).to eq(project_snippet.content)
- end
- end
end
%w[show raw].each do |action|
diff --git a/spec/controllers/projects/static_site_editor_controller_spec.rb b/spec/controllers/projects/static_site_editor_controller_spec.rb
index f7c8848b8cf..7b470254de1 100644
--- a/spec/controllers/projects/static_site_editor_controller_spec.rb
+++ b/spec/controllers/projects/static_site_editor_controller_spec.rb
@@ -3,7 +3,8 @@
require 'spec_helper'
describe Projects::StaticSiteEditorController do
- let(:project) { create(:project, :public, :repository) }
+ let_it_be(:project) { create(:project, :public, :repository) }
+ let_it_be(:user) { create(:user) }
describe 'GET show' do
let(:default_params) do
@@ -27,8 +28,6 @@ describe Projects::StaticSiteEditorController do
end
context 'as guest' do
- let(:user) { create(:user) }
-
before do
project.add_guest(user)
sign_in(user)
@@ -42,10 +41,11 @@ describe Projects::StaticSiteEditorController do
%w[developer maintainer].each do |role|
context "as #{role}" do
- let(:user) { create(:user) }
+ before_all do
+ project.add_role(user, role)
+ end
before do
- project.add_role(user, role)
sign_in(user)
get :show, params: default_params
end
@@ -54,8 +54,10 @@ describe Projects::StaticSiteEditorController do
expect(response).to render_template(:show)
end
- it 'assigns a config variable' do
+ it 'assigns a required variables' do
expect(assigns(:config)).to be_a(Gitlab::StaticSiteEditor::Config)
+ expect(assigns(:ref)).to eq('master')
+ expect(assigns(:path)).to eq('README.md')
end
context 'when combination of ref and file path is incorrect' do
diff --git a/spec/controllers/projects/usage_ping_controller_spec.rb b/spec/controllers/projects/usage_ping_controller_spec.rb
index 284db93d7a8..a68967c228f 100644
--- a/spec/controllers/projects/usage_ping_controller_spec.rb
+++ b/spec/controllers/projects/usage_ping_controller_spec.rb
@@ -6,45 +6,52 @@ describe Projects::UsagePingController do
let_it_be(:project) { create(:project) }
let_it_be(:user) { create(:user) }
- describe 'POST #web_ide_clientside_preview' do
- subject { post :web_ide_clientside_preview, params: { namespace_id: project.namespace, project_id: project } }
+ before do
+ sign_in(user) if user
+ end
- before do
- sign_in(user) if user
- end
+ shared_examples 'counter is not increased' do
+ context 'when the user is not authenticated' do
+ let(:user) { nil }
- context 'when web ide clientside preview is enabled' do
- before do
- stub_application_setting(web_ide_clientside_preview_enabled: true)
- end
+ it 'returns 302' do
+ subject
- context 'when the user is not authenticated' do
- let(:user) { nil }
+ expect(response).to have_gitlab_http_status(:found)
+ end
+ end
- it 'returns 302' do
- subject
+ context 'when the user does not have access to the project' do
+ it 'returns 404' do
+ subject
- expect(response).to have_gitlab_http_status(:found)
- end
+ expect(response).to have_gitlab_http_status(:not_found)
end
+ end
+ end
- context 'when the user does not have access to the project' do
- it 'returns 404' do
- subject
+ shared_examples 'counter is increased' do |counter|
+ context 'when the authenticated user has access to the project' do
+ let(:user) { project.owner }
- expect(response).to have_gitlab_http_status(:not_found)
- end
+ it 'increments the usage counter' do
+ expect do
+ subject
+ end.to change { Gitlab::UsageDataCounters::WebIdeCounter.total_count(counter) }.by(1)
end
+ end
+ end
- context 'when the user has access to the project' do
- let(:user) { project.owner }
+ describe 'POST #web_ide_clientside_preview' do
+ subject { post :web_ide_clientside_preview, params: { namespace_id: project.namespace, project_id: project } }
- it 'increments the counter' do
- expect do
- subject
- end.to change { Gitlab::UsageDataCounters::WebIdeCounter.total_previews_count }.by(1)
- end
+ context 'when web ide clientside preview is enabled' do
+ before do
+ stub_application_setting(web_ide_clientside_preview_enabled: true)
end
+
+ it_behaves_like 'counter is not increased'
+ it_behaves_like 'counter is increased', 'WEB_IDE_PREVIEWS_COUNT'
end
context 'when web ide clientside preview is not enabled' do
@@ -61,4 +68,11 @@ describe Projects::UsagePingController do
end
end
end
+
+ describe 'POST #web_ide_pipelines_count' do
+ subject { post :web_ide_pipelines_count, params: { namespace_id: project.namespace, project_id: project } }
+
+ it_behaves_like 'counter is not increased'
+ it_behaves_like 'counter is increased', 'WEB_IDE_PIPELINES_COUNT'
+ end
end
diff --git a/spec/controllers/projects/wikis_controller_spec.rb b/spec/controllers/projects/wikis_controller_spec.rb
index 99d14298cd1..b4bbf76ce18 100644
--- a/spec/controllers/projects/wikis_controller_spec.rb
+++ b/spec/controllers/projects/wikis_controller_spec.rb
@@ -98,13 +98,12 @@ describe Projects::WikisController do
let(:id) { wiki_title }
it 'limits the retrieved pages for the sidebar' do
- expect(controller).to receive(:load_wiki).and_return(project_wiki)
- expect(project_wiki).to receive(:list_pages).with(limit: 15).and_call_original
-
subject
expect(response).to have_gitlab_http_status(:ok)
expect(assigns(:page).title).to eq(wiki_title)
+ expect(assigns(:sidebar_wiki_entries)).to contain_exactly(an_instance_of(WikiPage))
+ expect(assigns(:sidebar_limited)).to be(false)
end
context 'when page content encoding is invalid' do
@@ -200,7 +199,20 @@ describe Projects::WikisController do
subject
- expect(response).to redirect_to(project_wiki_path(project, project_wiki.list_pages.first))
+ expect(response).to redirect_to_wiki(project, project_wiki.list_pages.first)
+ end
+ end
+
+ context 'when the page has nil content' do
+ let(:page) { create(:wiki_page) }
+
+ it 'redirects to show' do
+ allow(page).to receive(:content).and_return(nil)
+ allow(controller).to receive(:find_page).and_return(page)
+
+ subject
+
+ expect(response).to redirect_to_wiki(project, page)
end
end
@@ -235,7 +247,7 @@ describe Projects::WikisController do
allow(controller).to receive(:valid_encoding?).and_return(false)
subject
- expect(response).to redirect_to(project_wiki_path(project, project_wiki.list_pages.first))
+ expect(response).to redirect_to_wiki(project, project_wiki.list_pages.first)
end
end
@@ -265,4 +277,8 @@ describe Projects::WikisController do
page = wiki.page(title: title, dir: dir)
project_wiki.delete_page(page, "test commit")
end
+
+ def redirect_to_wiki(project, page)
+ redirect_to(controller.project_wiki_path(project, page))
+ end
end
diff --git a/spec/controllers/projects_controller_spec.rb b/spec/controllers/projects_controller_spec.rb
index fc3efc8e805..6c00dad8bb7 100644
--- a/spec/controllers/projects_controller_spec.rb
+++ b/spec/controllers/projects_controller_spec.rb
@@ -1020,6 +1020,32 @@ describe ProjectsController do
expect(json_response['body']).to include(expanded_path)
end
end
+
+ context 'when path and ref parameters are provided' do
+ let(:project_with_repo) { create(:project, :repository) }
+ let(:preview_markdown_params) do
+ {
+ namespace_id: project_with_repo.namespace,
+ id: project_with_repo,
+ text: "![](./logo-white.png)\n",
+ ref: 'other_branch',
+ path: 'files/images/README.md'
+ }
+ end
+
+ before do
+ project_with_repo.add_maintainer(user)
+ project_with_repo.repository.create_branch('other_branch')
+ end
+
+ it 'renders JSON body with image links expanded' do
+ expanded_path = "/#{project_with_repo.full_path}/-/raw/other_branch/files/images/logo-white.png"
+
+ post :preview_markdown, params: preview_markdown_params
+
+ expect(json_response['body']).to include(expanded_path)
+ end
+ end
end
describe '#ensure_canonical_path' do
diff --git a/spec/controllers/registrations_controller_spec.rb b/spec/controllers/registrations_controller_spec.rb
index 0b4ecb68cf7..01a9647a763 100644
--- a/spec/controllers/registrations_controller_spec.rb
+++ b/spec/controllers/registrations_controller_spec.rb
@@ -139,21 +139,11 @@ describe RegistrationsController do
expect(flash[:alert]).to eq(_('There was an error with the reCAPTCHA. Please solve the reCAPTCHA again.'))
end
- it 'redirects to the dashboard when the recaptcha is solved' do
+ it 'redirects to the dashboard when the reCAPTCHA is solved' do
post(:create, params: user_params)
expect(flash[:notice]).to eq(I18n.t('devise.registrations.signed_up'))
end
-
- it 'does not require reCAPTCHA if disabled by feature flag' do
- stub_feature_flags(registrations_recaptcha: false)
-
- post(:create, params: user_params)
-
- expect(controller).not_to receive(:verify_recaptcha)
- expect(flash[:alert]).to be_nil
- expect(flash[:notice]).to eq(I18n.t('devise.registrations.signed_up'))
- end
end
context 'when invisible captcha is enabled' do
@@ -294,8 +284,6 @@ describe RegistrationsController do
end
it "logs a 'User Created' message" do
- stub_feature_flags(registrations_recaptcha: false)
-
expect(Gitlab::AppLogger).to receive(:info).with(/\AUser Created: username=new_username email=new@user.com.+\z/).and_call_original
post(:create, params: user_params)
@@ -419,24 +407,34 @@ describe RegistrationsController do
describe '#welcome' do
subject { get :welcome }
- before do
- sign_in(create(:user))
- end
-
context 'signup_flow experiment enabled' do
before do
stub_experiment_for_user(signup_flow: true)
end
it 'renders the devise_experimental_separate_sign_up_flow layout' do
+ sign_in(create(:user))
+
expected_layout = Gitlab.ee? ? :checkout : :devise_experimental_separate_sign_up_flow
expect(subject).to render_template(expected_layout)
end
+
+ context '2FA is required from group' do
+ before do
+ user = create(:user, require_two_factor_authentication_from_group: true)
+ sign_in(user)
+ end
+
+ it 'does not perform a redirect' do
+ expect(subject).not_to redirect_to(profile_two_factor_auth_path)
+ end
+ end
end
context 'signup_flow experiment disabled' do
before do
+ sign_in(create(:user))
stub_experiment_for_user(signup_flow: false)
end
diff --git a/spec/controllers/repositories/git_http_controller_spec.rb b/spec/controllers/repositories/git_http_controller_spec.rb
index 59455d90c25..1a2eee5d3a9 100644
--- a/spec/controllers/repositories/git_http_controller_spec.rb
+++ b/spec/controllers/repositories/git_http_controller_spec.rb
@@ -165,48 +165,11 @@ describe Repositories::GitHttpController do
end
end
- shared_examples 'snippet feature flag disabled behavior' do
- before do
- stub_feature_flags(version_snippets: false)
-
- request.headers.merge! auth_env(user.username, user.password, nil)
- end
-
- describe 'GET #info_refs' do
- let(:params) { container_params.merge(service: 'git-upload-pack') }
-
- it 'returns 403' do
- expect(controller).not_to receive(:access_check)
-
- get :info_refs, params: params
-
- expect(response).to have_gitlab_http_status(:forbidden)
- expect(response.body).to eq 'Snippet git access is disabled.'
- end
- end
-
- describe 'POST #git_upload_pack' do
- before do
- allow(controller).to receive(:authenticate_user).and_return(true)
- allow(controller).to receive(:verify_workhorse_api!).and_return(true)
- allow(controller).to receive(:access_check).and_return(nil)
- end
-
- it 'returns 403' do
- expect(controller).not_to receive(:access_check)
-
- post :git_upload_pack, params: params
-
- expect(response).to have_gitlab_http_status(:forbidden)
- expect(response.body).to eq 'Snippet git access is disabled.'
- end
- end
- end
-
context 'when repository container is a project' do
it_behaves_like 'info_refs behavior' do
let(:user) { project.owner }
end
+
it_behaves_like 'git_upload_pack behavior', true
it_behaves_like 'access checker class' do
let(:expected_class) { Gitlab::GitAccess }
@@ -221,14 +184,12 @@ describe Repositories::GitHttpController do
it_behaves_like 'info_refs behavior' do
let(:user) { personal_snippet.author }
end
+
it_behaves_like 'git_upload_pack behavior', false
it_behaves_like 'access checker class' do
let(:expected_class) { Gitlab::GitAccessSnippet }
let(:expected_object) { personal_snippet }
end
- it_behaves_like 'snippet feature flag disabled behavior' do
- let(:user) { personal_snippet.author }
- end
end
context 'when repository container is a project snippet' do
@@ -238,13 +199,11 @@ describe Repositories::GitHttpController do
it_behaves_like 'info_refs behavior' do
let(:user) { project_snippet.author }
end
+
it_behaves_like 'git_upload_pack behavior', false
it_behaves_like 'access checker class' do
let(:expected_class) { Gitlab::GitAccessSnippet }
let(:expected_object) { project_snippet }
end
- it_behaves_like 'snippet feature flag disabled behavior' do
- let(:user) { project_snippet.author }
- end
end
end
diff --git a/spec/controllers/search_controller_spec.rb b/spec/controllers/search_controller_spec.rb
index 1fe313452fe..79ffa297da3 100644
--- a/spec/controllers/search_controller_spec.rb
+++ b/spec/controllers/search_controller_spec.rb
@@ -84,7 +84,7 @@ describe SearchController do
with_them do
it do
project_wiki = create(:project_wiki, project: project, user: user)
- create(:wiki_page, wiki: project_wiki, attrs: { title: 'merge', content: 'merge' })
+ create(:wiki_page, wiki: project_wiki, title: 'merge', content: 'merge')
expect(subject).to render_template("search/results/#{partial}")
end
@@ -140,14 +140,6 @@ describe SearchController do
end
end
- context 'snippet search' do
- it 'forces title search' do
- get :show, params: { scope: 'snippet_blobs', snippets: 'true', search: 'foo' }
-
- expect(assigns[:scope]).to eq('snippet_titles')
- end
- end
-
it 'finds issue comments' do
project = create(:project, :public)
note = create(:note_on_issue, project: project)
diff --git a/spec/controllers/sessions_controller_spec.rb b/spec/controllers/sessions_controller_spec.rb
index af2e452c0ca..a65698a5b56 100644
--- a/spec/controllers/sessions_controller_spec.rb
+++ b/spec/controllers/sessions_controller_spec.rb
@@ -41,10 +41,10 @@ describe SessionsController do
stub_ldap_setting(enabled: true)
end
- it 'assigns ldap_servers' do
+ it 'ldap_servers available in helper' do
get(:new)
- expect(assigns[:ldap_servers].first.to_h).to include('label' => 'ldap', 'provider_name' => 'ldapmain')
+ expect(subject.ldap_servers.first.to_h).to include('label' => 'ldap', 'provider_name' => 'ldapmain')
end
context 'with sign_in disabled' do
@@ -52,10 +52,10 @@ describe SessionsController do
stub_ldap_setting(prevent_ldap_sign_in: true)
end
- it 'assigns no ldap_servers' do
+ it 'no ldap_servers available in helper' do
get(:new)
- expect(assigns[:ldap_servers]).to eq []
+ expect(subject.ldap_servers).to eq []
end
end
end
@@ -99,6 +99,11 @@ describe SessionsController do
set_devise_mapping(context: @request)
end
+ it_behaves_like 'known sign in' do
+ let(:user) { create(:user) }
+ let(:post_action) { post(:create, params: { user: { login: user.username, password: user.password } }) }
+ end
+
context 'when using standard authentications' do
context 'invalid password' do
it 'does not authenticate user' do
diff --git a/spec/controllers/snippets_controller_spec.rb b/spec/controllers/snippets_controller_spec.rb
index 05c48fb190c..046ee40cec2 100644
--- a/spec/controllers/snippets_controller_spec.rb
+++ b/spec/controllers/snippets_controller_spec.rb
@@ -86,20 +86,6 @@ describe SnippetsController do
expect(assigns(:blob)).to eq(personal_snippet.blobs.first)
end
-
- context 'when feature flag version_snippets is disabled' do
- before do
- stub_feature_flags(version_snippets: false)
- end
-
- it 'returns the snippet database content' do
- subject
-
- blob = assigns(:blob)
-
- expect(blob.data).to eq(personal_snippet.content)
- end
- end
end
context 'when the personal snippet is private' do
@@ -257,39 +243,13 @@ describe SnippetsController do
end
end
- context 'when the snippet description contains a file' do
- include FileMoverHelpers
-
- let(:picture_secret) { SecureRandom.hex }
- let(:text_secret) { SecureRandom.hex }
- let(:picture_file) { "/-/system/user/#{user.id}/#{picture_secret}/picture.jpg" }
- let(:text_file) { "/-/system/user/#{user.id}/#{text_secret}/text.txt" }
- let(:description) do
- "Description with picture: ![picture](/uploads#{picture_file}) and "\
- "text: [text.txt](/uploads#{text_file})"
- end
-
- before do
- allow(FileUtils).to receive(:mkdir_p)
- allow(FileUtils).to receive(:move)
- stub_file_mover(text_file)
- stub_file_mover(picture_file)
- end
-
- subject { create_snippet({ description: description }, { files: [picture_file, text_file] }) }
-
- it 'creates the snippet' do
- expect { subject }.to change { Snippet.count }.by(1)
- end
-
- it 'stores the snippet description correctly' do
- snippet = subject
+ context 'when the controller receives the files param' do
+ let(:files) { %w(foo bar) }
- expected_description = "Description with picture: "\
- "![picture](/uploads/-/system/personal_snippet/#{snippet.id}/#{picture_secret}/picture.jpg) and "\
- "text: [text.txt](/uploads/-/system/personal_snippet/#{snippet.id}/#{text_secret}/text.txt)"
+ it 'passes the files param to the snippet create service' do
+ expect(Snippets::CreateService).to receive(:new).with(nil, user, hash_including(files: files)).and_call_original
- expect(snippet.description).to eq(expected_description)
+ create_snippet({ title: nil }, { files: files })
end
end
@@ -318,7 +278,7 @@ describe SnippetsController do
.to log_spam(title: 'Title', user: user, noteable_type: 'PersonalSnippet')
end
- it 'renders :new with recaptcha disabled' do
+ it 'renders :new with reCAPTCHA disabled' do
stub_application_setting(recaptcha_enabled: false)
create_snippet(visibility_level: Snippet::PUBLIC)
@@ -326,18 +286,18 @@ describe SnippetsController do
expect(response).to render_template(:new)
end
- context 'recaptcha enabled' do
+ context 'reCAPTCHA enabled' do
before do
stub_application_setting(recaptcha_enabled: true)
end
- it 'renders :verify with recaptcha enabled' do
+ it 'renders :verify' do
create_snippet(visibility_level: Snippet::PUBLIC)
expect(response).to render_template(:verify)
end
- it 'renders snippet page when recaptcha verified' do
+ it 'renders snippet page' do
spammy_title = 'Whatever'
spam_logs = create_list(:spam_log, 2, user: user, title: spammy_title)
@@ -403,7 +363,7 @@ describe SnippetsController do
.to log_spam(title: 'Foo', user: user, noteable_type: 'PersonalSnippet')
end
- it 'renders :edit with recaptcha disabled' do
+ it 'renders :edit with reCAPTCHA disabled' do
stub_application_setting(recaptcha_enabled: false)
update_snippet(title: 'Foo', visibility_level: Snippet::PUBLIC)
@@ -411,18 +371,18 @@ describe SnippetsController do
expect(response).to render_template(:edit)
end
- context 'recaptcha enabled' do
+ context 'reCAPTCHA enabled' do
before do
stub_application_setting(recaptcha_enabled: true)
end
- it 'renders :verify with recaptcha enabled' do
+ it 'renders :verify' do
update_snippet(title: 'Foo', visibility_level: Snippet::PUBLIC)
expect(response).to render_template(:verify)
end
- it 'renders snippet page when recaptcha verified' do
+ it 'renders snippet page when reCAPTCHA verified' do
spammy_title = 'Whatever'
spam_logs = create_list(:spam_log, 2, user: user, title: spammy_title)
@@ -446,7 +406,7 @@ describe SnippetsController do
.to log_spam(title: 'Foo', user: user, noteable_type: 'PersonalSnippet')
end
- it 'renders :edit with recaptcha disabled' do
+ it 'renders :edit with reCAPTCHA disabled' do
stub_application_setting(recaptcha_enabled: false)
update_snippet(title: 'Foo')
@@ -459,13 +419,13 @@ describe SnippetsController do
stub_application_setting(recaptcha_enabled: true)
end
- it 'renders :verify with recaptcha enabled' do
+ it 'renders :verify' do
update_snippet(title: 'Foo')
expect(response).to render_template(:verify)
end
- it 'renders snippet page when recaptcha verified' do
+ it 'renders snippet page when reCAPTCHA verified' do
spammy_title = 'Whatever'
spam_logs = create_list(:spam_log, 2, user: user, title: spammy_title)
@@ -572,24 +532,6 @@ describe SnippetsController do
expect(response.cache_control[:public]).to eq snippet.public?
end
- context 'when feature flag version_snippets is disabled' do
- before do
- stub_feature_flags(version_snippets: false)
- end
-
- it_behaves_like '200 status'
- it_behaves_like 'CRLF line ending'
-
- it 'returns snippet database content' do
- subject
-
- expect(response.body).to eq snippet.content
- expect(response.header['Content-Type']).to eq('text/plain; charset=utf-8')
- end
-
- it_behaves_like 'content disposition headers'
- end
-
context 'when snippet repository is empty' do
before do
allow_any_instance_of(Repository).to receive(:empty?).and_return(true)
diff --git a/spec/db/schema_spec.rb b/spec/db/schema_spec.rb
index 2e22e67f4e2..eac9eb7aa47 100644
--- a/spec/db/schema_spec.rb
+++ b/spec/db/schema_spec.rb
@@ -14,9 +14,9 @@ describe 'Database schema' do
IGNORED_FK_COLUMNS = {
abuse_reports: %w[reporter_id user_id],
application_settings: %w[performance_bar_allowed_group_id slack_app_id snowplow_app_id eks_account_id eks_access_key_id],
- approvers: %w[target_id user_id],
approvals: %w[user_id],
approver_groups: %w[target_id],
+ approvers: %w[target_id user_id],
audit_events: %w[author_id entity_id],
award_emoji: %w[awardable_id user_id],
aws_roles: %w[role_external_id],
@@ -29,12 +29,13 @@ describe 'Database schema' do
ci_trigger_requests: %w[commit_id],
cluster_providers_aws: %w[security_group_id vpc_id access_key_id],
cluster_providers_gcp: %w[gcp_project_id operation_id],
+ commit_user_mentions: %w[commit_id],
deploy_keys_projects: %w[deploy_key_id],
deployments: %w[deployable_id environment_id user_id],
draft_notes: %w[discussion_id commit_id],
emails: %w[user_id],
- events: %w[target_id],
epics: %w[updated_by_id last_edited_by_id state_id],
+ events: %w[target_id],
forked_project_links: %w[forked_from_project_id],
geo_event_log: %w[hashed_storage_attachments_event_id],
geo_job_artifact_deleted_events: %w[job_artifact_id],
@@ -44,14 +45,14 @@ describe 'Database schema' do
geo_repository_deleted_events: %w[project_id],
geo_upload_deleted_events: %w[upload_id model_id],
gitlab_subscription_histories: %w[gitlab_subscription_id hosted_plan_id namespace_id],
- import_failures: %w[project_id],
identities: %w[user_id],
+ import_failures: %w[project_id],
issues: %w[last_edited_by_id state_id],
jira_tracker_data: %w[jira_issue_transition_id],
keys: %w[user_id],
label_links: %w[target_id],
- lfs_objects_projects: %w[lfs_object_id project_id],
ldap_group_links: %w[group_id],
+ lfs_objects_projects: %w[lfs_object_id project_id],
members: %w[source_id created_by_id],
merge_requests: %w[last_edited_by_id state_id],
namespaces: %w[owner_id parent_id],
@@ -63,15 +64,16 @@ describe 'Database schema' do
open_project_tracker_data: %w[closed_status_id],
project_group_links: %w[group_id],
project_statistics: %w[namespace_id],
- projects: %w[creator_id namespace_id ci_id mirror_user_id],
+ projects: %w[creator_id ci_id mirror_user_id],
redirect_routes: %w[source_id],
repository_languages: %w[programming_language_id],
routes: %w[source_id],
sent_notifications: %w[project_id noteable_id recipient_id commit_id in_reply_to_discussion_id],
+ slack_integrations: %w[team_id user_id],
snippets: %w[author_id],
spam_logs: %w[user_id],
subscriptions: %w[user_id subscribable_id],
- slack_integrations: %w[team_id user_id],
+ suggestions: %w[commit_id],
taggings: %w[tag_id taggable_id tagger_id],
timelogs: %w[user_id],
todos: %w[target_id commit_id],
@@ -81,9 +83,7 @@ describe 'Database schema' do
users_star_projects: %w[user_id],
vulnerability_identifiers: %w[external_id],
vulnerability_scanners: %w[external_id],
- web_hooks: %w[service_id group_id],
- suggestions: %w[commit_id],
- commit_user_mentions: %w[commit_id]
+ web_hooks: %w[service_id group_id]
}.with_indifferent_access.freeze
context 'for table' do
diff --git a/spec/factories/alert_management/alerts.rb b/spec/factories/alert_management/alerts.rb
new file mode 100644
index 00000000000..01f40a7a465
--- /dev/null
+++ b/spec/factories/alert_management/alerts.rb
@@ -0,0 +1,81 @@
+# frozen_string_literal: true
+require 'ffaker'
+
+FactoryBot.define do
+ factory :alert_management_alert, class: 'AlertManagement::Alert' do
+ triggered
+ project
+ title { FFaker::Lorem.sentence }
+ started_at { Time.current }
+
+ trait :with_issue do
+ issue
+ end
+
+ trait :with_fingerprint do
+ fingerprint { SecureRandom.hex }
+ end
+
+ trait :with_service do
+ service { FFaker::Product.product_name }
+ end
+
+ trait :with_monitoring_tool do
+ monitoring_tool { FFaker::AWS.product_description }
+ end
+
+ trait :with_description do
+ description { FFaker::Lorem.sentence }
+ end
+
+ trait :with_host do
+ hosts { [FFaker::Internet.ip_v4_address] }
+ end
+
+ trait :with_ended_at do
+ ended_at { Time.current }
+ end
+
+ trait :without_ended_at do
+ ended_at { nil }
+ end
+
+ trait :triggered do
+ status { AlertManagement::Alert::STATUSES[:triggered] }
+ without_ended_at
+ end
+
+ trait :acknowledged do
+ status { AlertManagement::Alert::STATUSES[:acknowledged] }
+ without_ended_at
+ end
+
+ trait :resolved do
+ status { AlertManagement::Alert::STATUSES[:resolved] }
+ with_ended_at
+ end
+
+ trait :ignored do
+ status { AlertManagement::Alert::STATUSES[:ignored] }
+ without_ended_at
+ end
+
+ trait :low_severity do
+ severity { 'low' }
+ end
+
+ trait :prometheus do
+ monitoring_tool { Gitlab::AlertManagement::AlertParams::MONITORING_TOOLS[:prometheus] }
+ end
+
+ trait :all_fields do
+ with_issue
+ with_fingerprint
+ with_service
+ with_monitoring_tool
+ with_host
+ with_description
+ low_severity
+ end
+ end
+end
diff --git a/spec/factories/appearances.rb b/spec/factories/appearances.rb
index e2922662ea4..8101cd8d8bf 100644
--- a/spec/factories/appearances.rb
+++ b/spec/factories/appearances.rb
@@ -7,6 +7,7 @@ FactoryBot.define do
title { "GitLab Community Edition" }
description { "Open source software to collaborate on code" }
new_project_guidelines { "Custom project guidelines" }
+ profile_image_guidelines { "Custom profile image guidelines" }
end
trait :with_logo do
diff --git a/spec/factories/ci/builds.rb b/spec/factories/ci/builds.rb
index fb3c163dff1..26786aab12c 100644
--- a/spec/factories/ci/builds.rb
+++ b/spec/factories/ci/builds.rb
@@ -314,12 +314,30 @@ FactoryBot.define do
end
end
+ trait :broken_test_reports do
+ after(:build) do |build|
+ build.job_artifacts << create(:ci_job_artifact, :junit_with_corrupted_data, job: build)
+ end
+ end
+
+ trait :accessibility_reports do
+ after(:build) do |build|
+ build.job_artifacts << create(:ci_job_artifact, :accessibility, job: build)
+ end
+ end
+
trait :coverage_reports do
after(:build) do |build|
build.job_artifacts << create(:ci_job_artifact, :cobertura, job: build)
end
end
+ trait :terraform_reports do
+ after(:build) do |build|
+ build.job_artifacts << create(:ci_job_artifact, :terraform, job: build)
+ end
+ end
+
trait :expired do
artifacts_expire_at { 1.minute.ago }
end
diff --git a/spec/factories/ci/daily_build_group_report_results.rb b/spec/factories/ci/daily_build_group_report_results.rb
new file mode 100644
index 00000000000..8653316b51a
--- /dev/null
+++ b/spec/factories/ci/daily_build_group_report_results.rb
@@ -0,0 +1,14 @@
+# frozen_string_literal: true
+
+FactoryBot.define do
+ factory :ci_daily_build_group_report_result, class: 'Ci::DailyBuildGroupReportResult' do
+ ref_path { Gitlab::Git::BRANCH_REF_PREFIX + 'master' }
+ date { Time.zone.now.to_date }
+ project
+ last_pipeline factory: :ci_pipeline
+ group_name { 'rspec' }
+ data do
+ { 'coverage' => 77.0 }
+ end
+ end
+end
diff --git a/spec/factories/ci/daily_report_results.rb b/spec/factories/ci/daily_report_results.rb
deleted file mode 100644
index e2255e8a134..00000000000
--- a/spec/factories/ci/daily_report_results.rb
+++ /dev/null
@@ -1,13 +0,0 @@
-# frozen_string_literal: true
-
-FactoryBot.define do
- factory :ci_daily_report_result, class: 'Ci::DailyReportResult' do
- ref_path { Gitlab::Git::BRANCH_REF_PREFIX + 'master' }
- date { Time.zone.now.to_date }
- project
- last_pipeline factory: :ci_pipeline
- param_type { Ci::DailyReportResult.param_types[:coverage] }
- title { 'rspec' }
- value { 77.0 }
- end
-end
diff --git a/spec/factories/ci/freeze_periods.rb b/spec/factories/ci/freeze_periods.rb
new file mode 100644
index 00000000000..de48c2076c8
--- /dev/null
+++ b/spec/factories/ci/freeze_periods.rb
@@ -0,0 +1,10 @@
+# frozen_string_literal: true
+
+FactoryBot.define do
+ factory :ci_freeze_period, class: 'Ci::FreezePeriod' do
+ project
+ freeze_start { '0 23 * * 5' }
+ freeze_end { '0 7 * * 1' }
+ cron_timezone { 'UTC' }
+ end
+end
diff --git a/spec/factories/ci/instance_variables.rb b/spec/factories/ci/instance_variables.rb
new file mode 100644
index 00000000000..5a3551d3561
--- /dev/null
+++ b/spec/factories/ci/instance_variables.rb
@@ -0,0 +1,13 @@
+# frozen_string_literal: true
+
+FactoryBot.define do
+ factory :ci_instance_variable, class: 'Ci::InstanceVariable' do
+ sequence(:key) { |n| "VARIABLE_#{n}" }
+ value { 'VARIABLE_VALUE' }
+ masked { false }
+
+ trait(:protected) do
+ add_attribute(:protected) { true }
+ end
+ end
+end
diff --git a/spec/factories/ci/job_artifacts.rb b/spec/factories/ci/job_artifacts.rb
index 82383cfa2b0..26c09795a0b 100644
--- a/spec/factories/ci/job_artifacts.rb
+++ b/spec/factories/ci/job_artifacts.rb
@@ -139,6 +139,36 @@ FactoryBot.define do
end
end
+ trait :accessibility do
+ file_type { :accessibility }
+ file_format { :raw }
+
+ after(:build) do |artifact, _evaluator|
+ artifact.file = fixture_file_upload(
+ Rails.root.join('spec/fixtures/accessibility/pa11y_with_errors.json'), 'application/json')
+ end
+ end
+
+ trait :accessibility_with_invalid_url do
+ file_type { :accessibility }
+ file_format { :raw }
+
+ after(:build) do |artifact, _evaluator|
+ artifact.file = fixture_file_upload(
+ Rails.root.join('spec/fixtures/accessibility/pa11y_with_invalid_url.json'), 'application/json')
+ end
+ end
+
+ trait :accessibility_without_errors do
+ file_type { :accessibility }
+ file_format { :raw }
+
+ after(:build) do |artifact, _evaluator|
+ artifact.file = fixture_file_upload(
+ Rails.root.join('spec/fixtures/accessibility/pa11y_without_errors.json'), 'application/json')
+ end
+ end
+
trait :cobertura do
file_type { :cobertura }
file_format { :gzip }
@@ -149,6 +179,26 @@ FactoryBot.define do
end
end
+ trait :terraform do
+ file_type { :terraform }
+ file_format { :raw }
+
+ after(:build) do |artifact, evaluator|
+ artifact.file = fixture_file_upload(
+ Rails.root.join('spec/fixtures/terraform/tfplan.json'), 'application/json')
+ end
+ end
+
+ trait :terraform_with_corrupted_data do
+ file_type { :terraform }
+ file_format { :raw }
+
+ after(:build) do |artifact, evaluator|
+ artifact.file = fixture_file_upload(
+ Rails.root.join('spec/fixtures/terraform/tfplan_with_corrupted_data.json'), 'application/json')
+ end
+ end
+
trait :coverage_gocov_xml do
file_type { :cobertura }
file_format { :gzip }
@@ -181,11 +231,14 @@ FactoryBot.define do
trait :lsif do
file_type { :lsif }
- file_format { :gzip }
+ file_format { :zip }
+
+ transient do
+ file_path { Rails.root.join('spec/fixtures/lsif.json.gz') }
+ end
after(:build) do |artifact, evaluator|
- artifact.file = fixture_file_upload(
- Rails.root.join('spec/fixtures/lsif.json.gz'), 'application/x-gzip')
+ artifact.file = fixture_file_upload(evaluator.file_path, 'application/x-gzip')
end
end
@@ -199,6 +252,21 @@ FactoryBot.define do
end
end
+ trait :cluster_applications do
+ file_type { :cluster_applications }
+ file_format { :gzip }
+
+ transient do
+ file do
+ fixture_file_upload(Rails.root.join('spec/fixtures/helm/helm_list_v2_prometheus_missing.json.gz'), 'application/x-gzip')
+ end
+ end
+
+ after(:build) do |artifact, evaluator|
+ artifact.file = evaluator.file
+ end
+ end
+
trait :correct_checksum do
after(:build) do |artifact, evaluator|
artifact.file_sha256 = Digest::SHA256.file(artifact.file.path).hexdigest
diff --git a/spec/factories/ci/pipelines.rb b/spec/factories/ci/pipelines.rb
index 257dd3337ba..0b3653a01ed 100644
--- a/spec/factories/ci/pipelines.rb
+++ b/spec/factories/ci/pipelines.rb
@@ -75,6 +75,22 @@ FactoryBot.define do
end
end
+ trait :with_broken_test_reports do
+ status { :success }
+
+ after(:build) do |pipeline, _evaluator|
+ pipeline.builds << build(:ci_build, :broken_test_reports, pipeline: pipeline, project: pipeline.project)
+ end
+ end
+
+ trait :with_accessibility_reports do
+ status { :success }
+
+ after(:build) do |pipeline, evaluator|
+ pipeline.builds << build(:ci_build, :accessibility_reports, pipeline: pipeline, project: pipeline.project)
+ end
+ end
+
trait :with_coverage_reports do
status { :success }
@@ -83,6 +99,14 @@ FactoryBot.define do
end
end
+ trait :with_terraform_reports do
+ status { :success }
+
+ after(:build) do |pipeline, evaluator|
+ pipeline.builds << build(:ci_build, :terraform_reports, pipeline: pipeline, project: pipeline.project)
+ end
+ end
+
trait :with_exposed_artifacts do
status { :success }
diff --git a/spec/factories/ci/test_case.rb b/spec/factories/ci/test_case.rb
index bb1508c0d75..0639aac566a 100644
--- a/spec/factories/ci/test_case.rb
+++ b/spec/factories/ci/test_case.rb
@@ -16,7 +16,7 @@ FactoryBot.define do
system_output { "Failure/Error: is_expected.to eq(300) expected: 300 got: -100" }
end
- trait :with_attachment do
+ trait :failed_with_attachment do
status { Gitlab::Ci::Reports::TestCase::STATUS_FAILED }
attachment { "some/path.png" }
end
diff --git a/spec/factories/clusters/applications/helm.rb b/spec/factories/clusters/applications/helm.rb
index 728c83e01b4..c49c26f06e5 100644
--- a/spec/factories/clusters/applications/helm.rb
+++ b/spec/factories/clusters/applications/helm.rb
@@ -65,6 +65,10 @@ FactoryBot.define do
status_reason { 'something went wrong' }
end
+ trait :uninstalled do
+ status { 10 }
+ end
+
trait :timed_out do
installing
updated_at { ClusterWaitForAppInstallationWorker::TIMEOUT.ago }
@@ -77,6 +81,24 @@ FactoryBot.define do
trait :no_helm_installed do
cluster factory: %i(cluster provided_by_gcp)
end
+
+ trait :modsecurity_blocking do
+ modsecurity_enabled { true }
+ modsecurity_mode { :blocking }
+ end
+
+ trait :modsecurity_logging do
+ modsecurity_enabled { true }
+ modsecurity_mode { :logging }
+ end
+
+ trait :modsecurity_disabled do
+ modsecurity_enabled { false }
+ end
+
+ trait :modsecurity_not_installed do
+ modsecurity_enabled { nil }
+ end
end
factory :clusters_applications_cert_manager, class: 'Clusters::Applications::CertManager' do
@@ -142,6 +164,8 @@ FactoryBot.define do
factory :clusters_applications_fluentd, class: 'Clusters::Applications::Fluentd' do
host { 'example.com' }
+ waf_log_enabled { true }
+ cilium_log_enabled { true }
cluster factory: %i(cluster with_installed_helm provided_by_gcp)
trait :no_helm_installed do
diff --git a/spec/factories/deploy_tokens.rb b/spec/factories/deploy_tokens.rb
index 657915f9976..d4127f78ebf 100644
--- a/spec/factories/deploy_tokens.rb
+++ b/spec/factories/deploy_tokens.rb
@@ -8,6 +8,8 @@ FactoryBot.define do
read_repository { true }
read_registry { true }
write_registry { false }
+ read_package_registry { false }
+ write_package_registry { false }
revoked { false }
expires_at { 5.days.from_now }
deploy_token_type { DeployToken.deploy_token_types[:project_type] }
@@ -31,5 +33,11 @@ FactoryBot.define do
trait :project do
deploy_token_type { DeployToken.deploy_token_types[:project_type] }
end
+
+ trait :all_scopes do
+ write_registry { true}
+ read_package_registry { true }
+ write_package_registry { true }
+ end
end
end
diff --git a/spec/factories/design_management/actions.rb b/spec/factories/design_management/actions.rb
new file mode 100644
index 00000000000..e2561f98f52
--- /dev/null
+++ b/spec/factories/design_management/actions.rb
@@ -0,0 +1,13 @@
+# frozen_string_literal: true
+
+FactoryBot.define do
+ factory :design_action, class: 'DesignManagement::Action' do
+ design
+ association :version, factory: :design_version
+ event { :creation }
+
+ trait :with_image_v432x230 do
+ image_v432x230 { fixture_file_upload('spec/fixtures/dk.png') }
+ end
+ end
+end
diff --git a/spec/factories/design_management/design_at_version.rb b/spec/factories/design_management/design_at_version.rb
new file mode 100644
index 00000000000..b73df71595c
--- /dev/null
+++ b/spec/factories/design_management/design_at_version.rb
@@ -0,0 +1,23 @@
+# frozen_string_literal: true
+
+FactoryBot.define do
+ factory :design_at_version, class: 'DesignManagement::DesignAtVersion' do
+ skip_create # This is not an Active::Record model.
+
+ design { nil }
+
+ version { nil }
+
+ transient do
+ issue { design&.issue || version&.issue || create(:issue) }
+ end
+
+ initialize_with do
+ attrs = attributes.dup
+ attrs[:design] ||= create(:design, issue: issue)
+ attrs[:version] ||= create(:design_version, issue: issue)
+
+ new(attrs)
+ end
+ end
+end
diff --git a/spec/factories/design_management/designs.rb b/spec/factories/design_management/designs.rb
new file mode 100644
index 00000000000..59d4cc56f95
--- /dev/null
+++ b/spec/factories/design_management/designs.rb
@@ -0,0 +1,128 @@
+# frozen_string_literal: true
+
+FactoryBot.define do
+ factory :design, class: 'DesignManagement::Design' do
+ issue { create(:issue) }
+ project { issue&.project || create(:project) }
+ sequence(:filename) { |n| "homescreen-#{n}.jpg" }
+
+ transient do
+ author { issue.author }
+ end
+
+ trait :importing do
+ issue { nil }
+
+ importing { true }
+ imported { false }
+ end
+
+ trait :imported do
+ importing { false }
+ imported { true }
+ end
+
+ create_versions = ->(design, evaluator, commit_version) do
+ unless evaluator.versions_count.zero?
+ project = design.project
+ issue = design.issue
+ repository = project.design_repository
+ repository.create_if_not_exists
+ dv_table_name = DesignManagement::Action.table_name
+ updates = [0, evaluator.versions_count - (evaluator.deleted ? 2 : 1)].max
+
+ run_action = ->(action) do
+ sha = commit_version[action]
+ version = DesignManagement::Version.new(sha: sha, issue: issue, author: evaluator.author)
+ version.save(validate: false) # We need it to have an ID, validate later
+ Gitlab::Database.bulk_insert(dv_table_name, [action.row_attrs(version)])
+ end
+
+ # always a creation
+ run_action[DesignManagement::DesignAction.new(design, :create, evaluator.file)]
+
+ # 0 or more updates
+ updates.times do
+ run_action[DesignManagement::DesignAction.new(design, :update, evaluator.file)]
+ end
+
+ # and maybe a deletion
+ run_action[DesignManagement::DesignAction.new(design, :delete)] if evaluator.deleted
+ end
+
+ design.clear_version_cache
+ end
+
+ # Use this trait to build designs that are backed by Git LFS, committed
+ # to the repository, and with an LfsObject correctly created for it.
+ trait :with_lfs_file do
+ with_file
+
+ transient do
+ raw_file { fixture_file_upload('spec/fixtures/dk.png', 'image/png') }
+ lfs_pointer { Gitlab::Git::LfsPointerFile.new(SecureRandom.random_bytes) }
+ file { lfs_pointer.pointer }
+ end
+
+ after :create do |design, evaluator|
+ lfs_object = create(:lfs_object, file: evaluator.raw_file, oid: evaluator.lfs_pointer.sha256, size: evaluator.lfs_pointer.size)
+ create(:lfs_objects_project, project: design.project, lfs_object: lfs_object, repository_type: :design)
+ end
+ end
+
+ # Use this trait if you want versions in a particular history, but don't
+ # want to pay for gitlay calls.
+ trait :with_versions do
+ transient do
+ deleted { false }
+ versions_count { 1 }
+ sequence(:file) { |n| "some-file-content-#{n}" }
+ end
+
+ after :create do |design, evaluator|
+ counter = (1..).lazy
+
+ # Just produce a SHA by hashing the action and a monotonic counter
+ commit_version = ->(action) do
+ Digest::SHA1.hexdigest("#{action.gitaly_action}.#{counter.next}")
+ end
+
+ create_versions[design, evaluator, commit_version]
+ end
+ end
+
+ # Use this trait to build designs that have commits in the repository
+ # and files that can be retrieved.
+ trait :with_file do
+ transient do
+ deleted { false }
+ versions_count { 1 }
+ file { File.join(Rails.root, 'spec/fixtures/dk.png') }
+ end
+
+ after :create do |design, evaluator|
+ project = design.project
+ repository = project.design_repository
+
+ commit_version = ->(action) do
+ repository.multi_action(
+ evaluator.author,
+ branch_name: 'master',
+ message: "#{action.action} for #{design.filename}",
+ actions: [action.gitaly_action]
+ )
+ end
+
+ create_versions[design, evaluator, commit_version]
+ end
+ end
+
+ trait :with_smaller_image_versions do
+ with_lfs_file
+
+ after :create do |design|
+ design.versions.each { |v| DesignManagement::GenerateImageVersionsService.new(v).execute }
+ end
+ end
+ end
+end
diff --git a/spec/factories/design_management/versions.rb b/spec/factories/design_management/versions.rb
new file mode 100644
index 00000000000..e6d17ba691c
--- /dev/null
+++ b/spec/factories/design_management/versions.rb
@@ -0,0 +1,142 @@
+# frozen_string_literal: true
+
+FactoryBot.define do
+ factory :design_version, class: 'DesignManagement::Version' do
+ sha
+ issue { designs.first&.issue || create(:issue) }
+ author { issue&.author || create(:user) }
+
+ transient do
+ designs_count { 1 }
+ created_designs { [] }
+ modified_designs { [] }
+ deleted_designs { [] }
+ end
+
+ # Warning: this will intentionally result in an invalid version!
+ trait :empty do
+ designs_count { 0 }
+ end
+
+ trait :importing do
+ issue { nil }
+
+ designs_count { 0 }
+ importing { true }
+ imported { false }
+ end
+
+ trait :imported do
+ importing { false }
+ imported { true }
+ end
+
+ after(:build) do |version, evaluator|
+ # By default all designs are created_designs, so just add them.
+ specific_designs = [].concat(
+ evaluator.created_designs,
+ evaluator.modified_designs,
+ evaluator.deleted_designs
+ )
+ version.designs += specific_designs
+
+ unless evaluator.designs_count.zero? || version.designs.present?
+ version.designs << create(:design, issue: version.issue)
+ end
+ end
+
+ after :create do |version, evaluator|
+ # FactoryBot does not like methods, so we use lambdas instead
+ events = DesignManagement::Action.events
+
+ version.actions
+ .where(design_id: evaluator.modified_designs.map(&:id))
+ .update_all(event: events[:modification])
+
+ version.actions
+ .where(design_id: evaluator.deleted_designs.map(&:id))
+ .update_all(event: events[:deletion])
+
+ version.designs.reload
+ # Ensure version.issue == design.issue for all version.designs
+ version.designs.update_all(issue_id: version.issue_id)
+
+ needed = evaluator.designs_count
+ have = version.designs.size
+
+ create_list(:design, [0, needed - have].max, issue: version.issue).each do |d|
+ version.designs << d
+ end
+
+ version.actions.reset
+ end
+
+ # Use this trait to build versions with designs that are backed by Git LFS, committed
+ # to the repository, and with an LfsObject correctly created for it.
+ trait :with_lfs_file do
+ committed
+
+ transient do
+ raw_file { fixture_file_upload('spec/fixtures/dk.png', 'image/png') }
+ lfs_pointer { Gitlab::Git::LfsPointerFile.new(SecureRandom.random_bytes) }
+ file { lfs_pointer.pointer }
+ end
+
+ after :create do |version, evaluator|
+ lfs_object = create(:lfs_object, file: evaluator.raw_file, oid: evaluator.lfs_pointer.sha256, size: evaluator.lfs_pointer.size)
+ create(:lfs_objects_project, project: version.project, lfs_object: lfs_object, repository_type: :design)
+ end
+ end
+
+ # This trait is for versions that must be present in the git repository.
+ trait :committed do
+ transient do
+ file { File.join(Rails.root, 'spec/fixtures/dk.png') }
+ end
+
+ after :create do |version, evaluator|
+ project = version.issue.project
+ repository = project.design_repository
+ repository.create_if_not_exists
+
+ designs = version.designs_by_event
+ base_change = { content: evaluator.file }
+
+ actions = %w[modification deletion].flat_map { |k| designs.fetch(k, []) }.map do |design|
+ base_change.merge(action: :create, file_path: design.full_path)
+ end
+
+ if actions.present?
+ repository.multi_action(
+ evaluator.author,
+ branch_name: 'master',
+ message: "created #{actions.size} files",
+ actions: actions
+ )
+ end
+
+ mapping = {
+ 'creation' => :create,
+ 'modification' => :update,
+ 'deletion' => :delete
+ }
+
+ version_actions = designs.flat_map do |(event, designs)|
+ base = event == 'deletion' ? {} : base_change
+ designs.map do |design|
+ base.merge(action: mapping[event], file_path: design.full_path)
+ end
+ end
+
+ sha = repository.multi_action(
+ evaluator.author,
+ branch_name: 'master',
+ message: "edited #{version_actions.size} files",
+ actions: version_actions
+ )
+
+ version.update(sha: sha)
+ end
+ end
+ end
+end
diff --git a/spec/factories/events.rb b/spec/factories/events.rb
index 5b456bb58ff..ed6cb3505f4 100644
--- a/spec/factories/events.rb
+++ b/spec/factories/events.rb
@@ -25,13 +25,23 @@ FactoryBot.define do
factory :wiki_page_event do
action { Event::CREATED }
- project { @overrides[:wiki_page]&.project || create(:project, :wiki_repo) }
+ project { @overrides[:wiki_page]&.container || create(:project, :wiki_repo) }
target { create(:wiki_page_meta, :for_wiki_page, wiki_page: wiki_page) }
transient do
- wiki_page { create(:wiki_page, project: project) }
+ wiki_page { create(:wiki_page, container: project) }
end
end
+
+ trait :for_design do
+ transient do
+ design { create(:design, issue: create(:issue, project: project)) }
+ note { create(:note, author: author, project: project, noteable: design) }
+ end
+
+ action { Event::COMMENTED }
+ target { note }
+ end
end
factory :push_event, class: 'PushEvent' do
diff --git a/spec/factories/git_wiki_commit_details.rb b/spec/factories/git_wiki_commit_details.rb
new file mode 100644
index 00000000000..b35f102fd4d
--- /dev/null
+++ b/spec/factories/git_wiki_commit_details.rb
@@ -0,0 +1,15 @@
+# frozen_string_literal: true
+
+FactoryBot.define do
+ factory :git_wiki_commit_details, class: 'Gitlab::Git::Wiki::CommitDetails' do
+ skip_create
+
+ transient do
+ author { create(:user) }
+ end
+
+ sequence(:message) { |n| "Commit message #{n}" }
+
+ initialize_with { new(author.id, author.username, author.name, author.email, message) }
+ end
+end
diff --git a/spec/factories/groups.rb b/spec/factories/groups.rb
index 4b6c1756d1e..d51c437f83a 100644
--- a/spec/factories/groups.rb
+++ b/spec/factories/groups.rb
@@ -6,7 +6,7 @@ FactoryBot.define do
path { name.downcase.gsub(/\s/, '_') }
type { 'Group' }
owner { nil }
- project_creation_level { ::Gitlab::Access::MAINTAINER_PROJECT_ACCESS}
+ project_creation_level { ::Gitlab::Access::MAINTAINER_PROJECT_ACCESS }
after(:create) do |group|
if group.owner
@@ -17,15 +17,15 @@ FactoryBot.define do
end
trait :public do
- visibility_level { Gitlab::VisibilityLevel::PUBLIC}
+ visibility_level { Gitlab::VisibilityLevel::PUBLIC }
end
trait :internal do
- visibility_level {Gitlab::VisibilityLevel::INTERNAL}
+ visibility_level {Gitlab::VisibilityLevel::INTERNAL }
end
trait :private do
- visibility_level { Gitlab::VisibilityLevel::PRIVATE}
+ visibility_level { Gitlab::VisibilityLevel::PRIVATE }
end
trait :with_avatar do
@@ -49,7 +49,7 @@ FactoryBot.define do
end
trait :owner_subgroup_creation_only do
- subgroup_creation_level { ::Gitlab::Access::OWNER_SUBGROUP_ACCESS}
+ subgroup_creation_level { ::Gitlab::Access::OWNER_SUBGROUP_ACCESS }
end
end
end
diff --git a/spec/factories/identities.rb b/spec/factories/identities.rb
index a2615ce30c3..fda4bfa589b 100644
--- a/spec/factories/identities.rb
+++ b/spec/factories/identities.rb
@@ -3,6 +3,6 @@
FactoryBot.define do
factory :identity do
provider { 'ldapmain' }
- extern_uid { 'my-ldap-id' }
+ sequence(:extern_uid) { |n| "my-ldap-id-#{n}" }
end
end
diff --git a/spec/factories/iterations.rb b/spec/factories/iterations.rb
new file mode 100644
index 00000000000..f6be1d9d752
--- /dev/null
+++ b/spec/factories/iterations.rb
@@ -0,0 +1,60 @@
+# frozen_string_literal: true
+
+FactoryBot.define do
+ sequence(:sequential_date) do |n|
+ n.days.from_now
+ end
+
+ factory :iteration do
+ title
+ start_date { generate(:sequential_date) }
+ due_date { generate(:sequential_date) }
+
+ transient do
+ project { nil }
+ group { nil }
+ project_id { nil }
+ group_id { nil }
+ resource_parent { nil }
+ end
+
+ trait :upcoming do
+ state_enum { Iteration::STATE_ENUM_MAP[:upcoming] }
+ end
+
+ trait :started do
+ state_enum { Iteration::STATE_ENUM_MAP[:started] }
+ end
+
+ trait :closed do
+ state_enum { Iteration::STATE_ENUM_MAP[:closed] }
+ end
+
+ trait(:skip_future_date_validation) do
+ after(:stub, :build) do |iteration|
+ iteration.skip_future_date_validation = true
+ end
+ end
+
+ after(:build, :stub) do |iteration, evaluator|
+ if evaluator.group
+ iteration.group = evaluator.group
+ elsif evaluator.group_id
+ iteration.group_id = evaluator.group_id
+ elsif evaluator.project
+ iteration.project = evaluator.project
+ elsif evaluator.project_id
+ iteration.project_id = evaluator.project_id
+ elsif evaluator.resource_parent
+ id = evaluator.resource_parent.id
+ evaluator.resource_parent.is_a?(Group) ? evaluator.group_id = id : evaluator.project_id = id
+ else
+ iteration.project = create(:project)
+ end
+ end
+
+ factory :upcoming_iteration, traits: [:upcoming]
+ factory :started_iteration, traits: [:started]
+ factory :closed_iteration, traits: [:closed]
+ end
+end
diff --git a/spec/factories/merge_requests.rb b/spec/factories/merge_requests.rb
index abccd775c8a..b10c04a37f7 100644
--- a/spec/factories/merge_requests.rb
+++ b/spec/factories/merge_requests.rb
@@ -121,6 +121,18 @@ FactoryBot.define do
end
end
+ trait :with_accessibility_reports do
+ after(:build) do |merge_request|
+ merge_request.head_pipeline = build(
+ :ci_pipeline,
+ :success,
+ :with_accessibility_reports,
+ project: merge_request.source_project,
+ ref: merge_request.source_branch,
+ sha: merge_request.diff_head_sha)
+ end
+ end
+
trait :with_coverage_reports do
after(:build) do |merge_request|
merge_request.head_pipeline = build(
@@ -133,6 +145,18 @@ FactoryBot.define do
end
end
+ trait :with_terraform_reports do
+ after(:build) do |merge_request|
+ merge_request.head_pipeline = build(
+ :ci_pipeline,
+ :success,
+ :with_terraform_reports,
+ project: merge_request.source_project,
+ ref: merge_request.source_branch,
+ sha: merge_request.diff_head_sha)
+ end
+ end
+
trait :with_exposed_artifacts do
after(:build) do |merge_request|
merge_request.head_pipeline = build(
diff --git a/spec/factories/metrics/users_starred_dasboards.rb b/spec/factories/metrics/users_starred_dasboards.rb
new file mode 100644
index 00000000000..06fe7735e9a
--- /dev/null
+++ b/spec/factories/metrics/users_starred_dasboards.rb
@@ -0,0 +1,9 @@
+# frozen_string_literal: true
+
+FactoryBot.define do
+ factory :metrics_users_starred_dashboard, class: '::Metrics::UsersStarredDashboard' do
+ dashboard_path { "custom_dashboard.yml" }
+ user
+ project
+ end
+end
diff --git a/spec/factories/notes.rb b/spec/factories/notes.rb
index fdd1a9a18b2..7c3ba122b5a 100644
--- a/spec/factories/notes.rb
+++ b/spec/factories/notes.rb
@@ -16,6 +16,7 @@ FactoryBot.define do
factory :note_on_merge_request, traits: [:on_merge_request]
factory :note_on_project_snippet, traits: [:on_project_snippet]
factory :note_on_personal_snippet, traits: [:on_personal_snippet]
+ factory :note_on_design, traits: [:on_design]
factory :system_note, traits: [:system]
factory :discussion_note, class: 'DiscussionNote'
@@ -107,6 +108,10 @@ FactoryBot.define do
end
end
+ factory :diff_note_on_design, parent: :note, traits: [:on_design], class: 'DiffNote' do
+ position { build(:image_diff_position, file: noteable.full_path, diff_refs: noteable.diff_refs) }
+ end
+
trait :on_commit do
association :project, :repository
noteable { nil }
@@ -136,6 +141,20 @@ FactoryBot.define do
project { nil }
end
+ trait :on_design do
+ transient do
+ issue { association(:issue, project: project) }
+ end
+ noteable { association(:design, :with_file, issue: issue) }
+
+ after(:build) do |note|
+ next if note.project == note.noteable.project
+
+ # note validations require consistency between these two objects
+ note.project = note.noteable.project
+ end
+ end
+
trait :system do
system { true }
end
diff --git a/spec/factories/plan_limits.rb b/spec/factories/plan_limits.rb
new file mode 100644
index 00000000000..4aea09618d0
--- /dev/null
+++ b/spec/factories/plan_limits.rb
@@ -0,0 +1,11 @@
+# frozen_string_literal: true
+
+FactoryBot.define do
+ factory :plan_limits do
+ plan
+
+ trait :default_plan do
+ plan factory: :default_plan
+ end
+ end
+end
diff --git a/spec/factories/plans.rb b/spec/factories/plans.rb
new file mode 100644
index 00000000000..81506edcf16
--- /dev/null
+++ b/spec/factories/plans.rb
@@ -0,0 +1,13 @@
+# frozen_string_literal: true
+
+FactoryBot.define do
+ factory :plan do
+ Plan.all_plans.each do |plan|
+ factory :"#{plan}_plan" do
+ name { plan }
+ title { name.titleize }
+ initialize_with { Plan.find_or_create_by(name: plan) }
+ end
+ end
+ end
+end
diff --git a/spec/factories/project_repository_storage_moves.rb b/spec/factories/project_repository_storage_moves.rb
new file mode 100644
index 00000000000..aa8576834eb
--- /dev/null
+++ b/spec/factories/project_repository_storage_moves.rb
@@ -0,0 +1,14 @@
+# frozen_string_literal: true
+
+FactoryBot.define do
+ factory :project_repository_storage_move, class: 'ProjectRepositoryStorageMove' do
+ project
+
+ source_storage_name { 'default' }
+ destination_storage_name { 'default' }
+
+ trait :scheduled do
+ state { ProjectRepositoryStorageMove.state_machines[:state].states[:scheduled].value }
+ end
+ end
+end
diff --git a/spec/factories/project_wikis.rb b/spec/factories/project_wikis.rb
deleted file mode 100644
index 401402614f4..00000000000
--- a/spec/factories/project_wikis.rb
+++ /dev/null
@@ -1,11 +0,0 @@
-# frozen_string_literal: true
-
-FactoryBot.define do
- factory :project_wiki do
- skip_create
-
- association :project, :wiki_repo
- user { project.creator }
- initialize_with { new(project, user) }
- end
-end
diff --git a/spec/factories/projects.rb b/spec/factories/projects.rb
index 64321c9f319..45caa7a2b6a 100644
--- a/spec/factories/projects.rb
+++ b/spec/factories/projects.rb
@@ -215,6 +215,12 @@ FactoryBot.define do
end
end
+ trait :design_repo do
+ after(:create) do |project|
+ raise 'Failed to create design repository!' unless project.design_repository.create_if_not_exists
+ end
+ end
+
trait :remote_mirror do
transient do
remote_name { "remote_mirror_#{SecureRandom.hex}" }
diff --git a/spec/factories/remote_mirrors.rb b/spec/factories/remote_mirrors.rb
index 124c0510cab..aa0ace30d90 100644
--- a/spec/factories/remote_mirrors.rb
+++ b/spec/factories/remote_mirrors.rb
@@ -4,5 +4,10 @@ FactoryBot.define do
factory :remote_mirror, class: 'RemoteMirror' do
association :project, :repository
url { "http://foo:bar@test.com" }
+
+ trait :ssh do
+ url { 'ssh://git@test.com:foo/bar.git' }
+ auth_method { 'ssh_public_key' }
+ end
end
end
diff --git a/spec/factories/resource_state_event.rb b/spec/factories/resource_state_event.rb
new file mode 100644
index 00000000000..e3de462b797
--- /dev/null
+++ b/spec/factories/resource_state_event.rb
@@ -0,0 +1,10 @@
+# frozen_string_literal: true
+
+FactoryBot.define do
+ factory :resource_state_event do
+ issue { merge_request.nil? ? create(:issue) : nil }
+ merge_request { nil }
+ state { :opened }
+ user { issue&.author || merge_request&.author || create(:user) }
+ end
+end
diff --git a/spec/factories/sequences.rb b/spec/factories/sequences.rb
index cdc64a8502e..ca0804965df 100644
--- a/spec/factories/sequences.rb
+++ b/spec/factories/sequences.rb
@@ -12,4 +12,5 @@ FactoryBot.define do
sequence(:branch) { |n| "my-branch-#{n}" }
sequence(:past_time) { |n| 4.hours.ago + (2 * n).seconds }
sequence(:iid)
+ sequence(:sha) { |n| Digest::SHA1.hexdigest("commit-like-#{n}") }
end
diff --git a/spec/factories/services.rb b/spec/factories/services.rb
index c011a9e3bb4..b6696769da9 100644
--- a/spec/factories/services.rb
+++ b/spec/factories/services.rb
@@ -158,6 +158,13 @@ FactoryBot.define do
token { 'test_token' }
end
+ factory :slack_service do
+ project
+ active { true }
+ webhook { 'https://slack.service.url' }
+ type { 'SlackService' }
+ end
+
# this is for testing storing values inside properties, which is deprecated and will be removed in
# https://gitlab.com/gitlab-org/gitlab/issues/29404
trait :without_properties_callback do
diff --git a/spec/factories/uploads.rb b/spec/factories/uploads.rb
index a060cd7d6f8..b19af277cc3 100644
--- a/spec/factories/uploads.rb
+++ b/spec/factories/uploads.rb
@@ -65,5 +65,11 @@ FactoryBot.define do
model { create(:note) }
uploader { "AttachmentUploader" }
end
+
+ trait :design_action_image_v432x230_upload do
+ mount_point { :image_v432x230 }
+ model { create(:design_action) }
+ uploader { ::DesignManagement::DesignV432x230Uploader.name }
+ end
end
end
diff --git a/spec/factories/usage_data.rb b/spec/factories/usage_data.rb
index b633038b83b..8fe0018b5a6 100644
--- a/spec/factories/usage_data.rb
+++ b/spec/factories/usage_data.rb
@@ -12,6 +12,11 @@ FactoryBot.define do
create(:jira_service, :jira_cloud_service, project: projects[2])
create(:jira_service, :without_properties_callback, project: projects[3],
properties: { url: 'https://mysite.atlassian.net' })
+ jira_label = create(:label, project: projects[0])
+ create(:jira_import_state, :finished, project: projects[0], label: jira_label, failed_to_import_count: 2, imported_issues_count: 7, total_issue_count: 9)
+ create(:jira_import_state, :finished, project: projects[1], label: jira_label, imported_issues_count: 3, total_issue_count: 3)
+ create(:jira_import_state, :finished, project: projects[1], label: jira_label, imported_issues_count: 3)
+ create(:jira_import_state, :scheduled, project: projects[1], label: jira_label)
create(:prometheus_service, project: projects[1])
create(:service, project: projects[0], type: 'SlackSlashCommandsService', active: true)
create(:service, project: projects[1], type: 'SlackService', active: true)
@@ -23,11 +28,10 @@ FactoryBot.define do
create(:project_error_tracking_setting, project: projects[1], enabled: false)
create(:alerts_service, project: projects[0])
create(:alerts_service, :inactive, project: projects[1])
- create_list(:issue, 2, project: projects[0], author: User.alert_bot)
+ alert_bot_issues = create_list(:issue, 2, project: projects[0], author: User.alert_bot)
create_list(:issue, 2, project: projects[1], author: User.alert_bot)
- create_list(:issue, 4, project: projects[0])
- create(:prometheus_alert, project: projects[0])
- create(:prometheus_alert, project: projects[0])
+ issues = create_list(:issue, 4, project: projects[0])
+ create_list(:prometheus_alert, 2, project: projects[0])
create(:prometheus_alert, project: projects[1])
create(:zoom_meeting, project: projects[0], issue: projects[0].issues[0], issue_status: :added)
create_list(:zoom_meeting, 2, project: projects[0], issue: projects[0].issues[1], issue_status: :removed)
@@ -35,6 +39,20 @@ FactoryBot.define do
create_list(:zoom_meeting, 2, project: projects[0], issue: projects[0].issues[2], issue_status: :removed)
create(:sentry_issue, issue: projects[0].issues[0])
+ # Incident Labeled Issues
+ incident_label_attrs = IncidentManagement::CreateIssueService::INCIDENT_LABEL
+ incident_label = create(:label, project: projects[0], **incident_label_attrs)
+ create(:labeled_issue, project: projects[0], labels: [incident_label])
+ incident_group = create(:group)
+ incident_label_scoped_to_project = create(:label, project: projects[1], **incident_label_attrs)
+ incident_label_scoped_to_group = create(:group_label, group: incident_group, **incident_label_attrs)
+ create(:labeled_issue, project: projects[1], labels: [incident_label_scoped_to_project])
+ create(:labeled_issue, project: projects[1], labels: [incident_label_scoped_to_group])
+
+ # Alert Issues
+ create(:alert_management_alert, issue: issues[0], project: projects[0])
+ create(:alert_management_alert, issue: alert_bot_issues[0], project: projects[0])
+
# Enabled clusters
gcp_cluster = create(:cluster_provider_gcp, :created).cluster
create(:cluster_provider_aws, :created)
diff --git a/spec/factories/users.rb b/spec/factories/users.rb
index f274503f0e7..2f5cc404143 100644
--- a/spec/factories/users.rb
+++ b/spec/factories/users.rb
@@ -31,6 +31,10 @@ FactoryBot.define do
user_type { :project_bot }
end
+ trait :migration_bot do
+ user_type { :migration_bot }
+ end
+
trait :external do
external { true }
end
@@ -40,7 +44,7 @@ FactoryBot.define do
end
trait :ghost do
- ghost { true }
+ user_type { :ghost }
after(:build) { |user, _| user.block! }
end
diff --git a/spec/factories/wiki_pages.rb b/spec/factories/wiki_pages.rb
index 8eb7a12a928..e7fcc19bbfe 100644
--- a/spec/factories/wiki_pages.rb
+++ b/spec/factories/wiki_pages.rb
@@ -7,10 +7,17 @@ FactoryBot.define do
transient do
title { generate(:wiki_page_title) }
content { 'Content for wiki page' }
- format { 'markdown' }
- project { create(:project) }
- attrs do
- {
+ format { :markdown }
+ message { nil }
+ project { association(:project, :wiki_repo) }
+ container { project }
+ wiki { association(:wiki, container: container) }
+ page { OpenStruct.new(url_path: title) }
+ end
+
+ initialize_with do
+ new(wiki, page).tap do |page|
+ page.attributes = {
title: title,
content: content,
format: format
@@ -18,27 +25,13 @@ FactoryBot.define do
end
end
- page { OpenStruct.new(url_path: 'some-name') }
- wiki { build(:project_wiki, project: project) }
-
- initialize_with { new(wiki, page) }
-
- before(:create) do |page, evaluator|
- page.attributes = evaluator.attrs
- end
-
- to_create do |page|
- page.create
+ # Clear our default @page, except when using build_stubbed
+ after(:build) do |page|
+ page.instance_variable_set('@page', nil)
end
- trait :with_real_page do
- project { create(:project, :repository) }
-
- page do
- wiki.create_page(title, content)
- page_title, page_dir = wiki.page_title_and_dir(title)
- wiki.wiki.page(title: page_title, dir: page_dir, version: nil)
- end
+ to_create do |page, evaluator|
+ page.create(message: evaluator.message)
end
end
@@ -48,10 +41,10 @@ FactoryBot.define do
trait :for_wiki_page do
transient do
- wiki_page { create(:wiki_page, project: project) }
+ wiki_page { create(:wiki_page, container: project) }
end
- project { @overrides[:wiki_page]&.project || create(:project) }
+ project { @overrides[:wiki_page]&.container || create(:project) }
title { wiki_page.title }
initialize_with do
@@ -73,5 +66,6 @@ FactoryBot.define do
end
sequence(:wiki_page_title) { |n| "Page #{n}" }
+ sequence(:wiki_filename) { |n| "Page_#{n}.md" }
sequence(:sluggified_title) { |n| "slug-#{n}" }
end
diff --git a/spec/factories/wikis.rb b/spec/factories/wikis.rb
new file mode 100644
index 00000000000..96578fdcee6
--- /dev/null
+++ b/spec/factories/wikis.rb
@@ -0,0 +1,21 @@
+# frozen_string_literal: true
+
+FactoryBot.define do
+ factory :wiki do
+ transient do
+ container { association(:project, :wiki_repo) }
+ user { association(:user) }
+ end
+
+ initialize_with { Wiki.for_container(container, user) }
+ skip_create
+
+ factory :project_wiki do
+ transient do
+ project { association(:project, :wiki_repo) }
+ end
+
+ container { project }
+ end
+ end
+end
diff --git a/spec/features/admin/admin_appearance_spec.rb b/spec/features/admin/admin_appearance_spec.rb
index f6c498f7a4c..e711ee7d40e 100644
--- a/spec/features/admin/admin_appearance_spec.rb
+++ b/spec/features/admin/admin_appearance_spec.rb
@@ -12,6 +12,7 @@ describe 'Admin Appearance' do
fill_in 'appearance_title', with: 'MyCompany'
fill_in 'appearance_description', with: 'dev server'
fill_in 'appearance_new_project_guidelines', with: 'Custom project guidelines'
+ fill_in 'appearance_profile_image_guidelines', with: 'Custom profile image guidelines'
click_button 'Update appearance settings'
expect(current_path).to eq admin_appearances_path
@@ -20,6 +21,7 @@ describe 'Admin Appearance' do
expect(page).to have_field('appearance_title', with: 'MyCompany')
expect(page).to have_field('appearance_description', with: 'dev server')
expect(page).to have_field('appearance_new_project_guidelines', with: 'Custom project guidelines')
+ expect(page).to have_field('appearance_profile_image_guidelines', with: 'Custom profile image guidelines')
expect(page).to have_content 'Last edit'
end
@@ -86,6 +88,22 @@ describe 'Admin Appearance' do
expect_custom_new_project_appearance(appearance)
end
+ context 'Profile page with custom profile image guidelines' do
+ before do
+ sign_in(create(:admin))
+ visit admin_appearances_path
+ fill_in 'appearance_profile_image_guidelines', with: 'Custom profile image guidelines, please :smile:!'
+ click_button 'Update appearance settings'
+ end
+
+ it 'renders guidelines when set' do
+ sign_in create(:user)
+ visit profile_path
+
+ expect(page).to have_content 'Custom profile image guidelines, please 😄!'
+ end
+ end
+
it 'Appearance logo' do
sign_in(create(:admin))
visit admin_appearances_path
diff --git a/spec/features/admin/admin_browses_logs_spec.rb b/spec/features/admin/admin_browses_logs_spec.rb
deleted file mode 100644
index 45e860e1536..00000000000
--- a/spec/features/admin/admin_browses_logs_spec.rb
+++ /dev/null
@@ -1,20 +0,0 @@
-# frozen_string_literal: true
-
-require 'spec_helper'
-
-describe 'Admin browses logs' do
- before do
- sign_in(create(:admin))
- end
-
- it 'shows available log files' do
- visit admin_logs_path
-
- expect(page).to have_link 'application_json.log'
- expect(page).to have_link 'git_json.log'
- expect(page).to have_link 'test.log'
- expect(page).to have_link 'sidekiq.log'
- expect(page).to have_link 'repocheck.log'
- expect(page).to have_link 'kubernetes.log'
- end
-end
diff --git a/spec/features/admin/admin_hooks_spec.rb b/spec/features/admin/admin_hooks_spec.rb
index 64326f3be32..40bcf4a31e4 100644
--- a/spec/features/admin/admin_hooks_spec.rb
+++ b/spec/features/admin/admin_hooks_spec.rb
@@ -36,6 +36,24 @@ describe 'Admin::Hooks' do
expect(page).to have_content('foo.rb')
expect(page).to have_content('bar.clj')
end
+
+ context 'deprecation warning' do
+ it 'shows warning for plugins directory' do
+ allow(Gitlab::FileHook).to receive(:files).and_return(['plugins/foo.rb'])
+
+ visit admin_hooks_path
+
+ expect(page).to have_content('Plugins directory is deprecated and will be removed in 14.0')
+ end
+
+ it 'does not show warning for file_hooks directory' do
+ allow(Gitlab::FileHook).to receive(:files).and_return(['file_hooks/foo.rb'])
+
+ visit admin_hooks_path
+
+ expect(page).not_to have_content('Plugins directory is deprecated and will be removed in 14.0')
+ end
+ end
end
describe 'New Hook' do
diff --git a/spec/features/admin/admin_mode/login_spec.rb b/spec/features/admin/admin_mode/login_spec.rb
index b8a910d3a40..afc6f2ddb56 100644
--- a/spec/features/admin/admin_mode/login_spec.rb
+++ b/spec/features/admin/admin_mode/login_spec.rb
@@ -5,6 +5,7 @@ require 'spec_helper'
describe 'Admin Mode Login', :clean_gitlab_redis_shared_state, :do_not_mock_admin_mode do
include TermsHelper
include UserLoginHelper
+ include LdapHelpers
describe 'with two-factor authentication', :js do
def enter_code(code)
@@ -179,6 +180,82 @@ describe 'Admin Mode Login', :clean_gitlab_redis_shared_state, :do_not_mock_admi
gitlab_enable_admin_mode_sign_in_via('saml', user, 'my-uid', mock_saml_response)
end
end
+
+ context 'when logging in via ldap' do
+ let(:uid) { 'my-uid' }
+ let(:provider_label) { 'Main LDAP' }
+ let(:provider_name) { 'main' }
+ let(:provider) { "ldap#{provider_name}" }
+ let(:ldap_server_config) do
+ {
+ 'label' => provider_label,
+ 'provider_name' => provider,
+ 'attributes' => {},
+ 'encryption' => 'plain',
+ 'uid' => 'uid',
+ 'base' => 'dc=example,dc=com'
+ }
+ end
+ let(:user) { create(:omniauth_user, :admin, :two_factor, extern_uid: uid, provider: provider) }
+
+ before do
+ setup_ldap(provider, user, uid, ldap_server_config)
+ end
+
+ context 'when two factor authentication is required' do
+ it 'shows 2FA prompt after ldap login' do
+ sign_in_using_ldap!(user, provider_label)
+
+ expect(page).to have_content('Two-Factor Authentication')
+
+ enter_code(user.current_otp)
+ enable_admin_mode_using_ldap!(user)
+
+ expect(page).to have_content('Two-Factor Authentication')
+
+ # Cannot reuse the TOTP
+ Timecop.travel(30.seconds.from_now) do
+ enter_code(user.current_otp)
+
+ expect(current_path).to eq admin_root_path
+ expect(page).to have_content('Admin mode enabled')
+ end
+ end
+ end
+
+ def setup_ldap(provider, user, uid, ldap_server_config)
+ stub_ldap_setting(enabled: true)
+
+ allow(::Gitlab::Auth::Ldap::Config).to receive_messages(enabled: true, servers: [ldap_server_config])
+ allow(Gitlab::Auth::OAuth::Provider).to receive_messages(providers: [provider.to_sym])
+
+ Ldap::OmniauthCallbacksController.define_providers!
+ Rails.application.reload_routes!
+
+ mock_auth_hash(provider, uid, user.email)
+ allow(Gitlab::Auth::Ldap::Access).to receive(:allowed?).with(user).and_return(true)
+
+ allow_any_instance_of(ActionDispatch::Routing::RoutesProxy)
+ .to receive(:"user_#{provider}_omniauth_callback_path")
+ .and_return("/users/auth/#{provider}/callback")
+ end
+
+ def sign_in_using_ldap!(user, provider_label)
+ visit new_user_session_path
+ click_link provider_label
+ fill_in 'username', with: user.username
+ fill_in 'password', with: user.password
+ click_button 'Sign in'
+ end
+
+ def enable_admin_mode_using_ldap!(user)
+ visit new_admin_session_path
+ click_link provider_label
+ fill_in 'username', with: user.username
+ fill_in 'password', with: user.password
+ click_button 'Enter Admin Mode'
+ end
+ end
end
end
end
diff --git a/spec/features/admin/admin_settings_spec.rb b/spec/features/admin/admin_settings_spec.rb
index 1a3da8cb373..7ec3c2abb51 100644
--- a/spec/features/admin/admin_settings_spec.rb
+++ b/spec/features/admin/admin_settings_spec.rb
@@ -212,12 +212,12 @@ describe 'Admin updates settings', :clean_gitlab_redis_shared_state, :do_not_moc
expect(current_settings.hide_third_party_offers).to be true
end
- it 'Change Slack Notifications Service template settings' do
+ it 'Change Slack Notifications Service template settings', :js do
first(:link, 'Service Templates').click
click_link 'Slack notifications'
fill_in 'Webhook', with: 'http://localhost'
fill_in 'Username', with: 'test_user'
- fill_in 'service_push_channel', with: '#test_channel'
+ fill_in 'service[push_channel]', with: '#test_channel'
page.check('Notify only broken pipelines')
page.select 'All branches', from: 'Branches to be notified'
@@ -231,10 +231,10 @@ describe 'Admin updates settings', :clean_gitlab_redis_shared_state, :do_not_moc
expect(page.all('input[type=checkbox]')).to all(be_checked)
expect(find_field('Webhook').value).to eq 'http://localhost'
expect(find_field('Username').value).to eq 'test_user'
- expect(find('#service_push_channel').value).to eq '#test_channel'
+ expect(find('[name="service[push_channel]"]').value).to eq '#test_channel'
end
- it 'defaults Deployment events to false for chat notification template settings' do
+ it 'defaults Deployment events to false for chat notification template settings', :js do
first(:link, 'Service Templates').click
click_link 'Slack notifications'
@@ -302,16 +302,6 @@ describe 'Admin updates settings', :clean_gitlab_redis_shared_state, :do_not_moc
visit metrics_and_profiling_admin_application_settings_path
end
- it 'Change Influx settings' do
- page.within('.as-influx') do
- check 'Enable InfluxDB Metrics'
- click_button 'Save changes'
- end
-
- expect(current_settings.metrics_enabled?).to be true
- expect(page).to have_content "Application settings saved successfully"
- end
-
it 'Change Prometheus settings' do
page.within('.as-prometheus') do
check 'Enable Prometheus Metrics'
@@ -382,6 +372,18 @@ describe 'Admin updates settings', :clean_gitlab_redis_shared_state, :do_not_moc
expect(current_settings.allow_local_requests_from_system_hooks).to be false
expect(current_settings.dns_rebinding_protection_enabled).to be false
end
+
+ it 'Changes Issues rate limits settings' do
+ visit network_admin_application_settings_path
+
+ page.within('.as-issue-limits') do
+ fill_in 'Max requests per second per user', with: 0
+ click_button 'Save changes'
+ end
+
+ expect(page).to have_content "Application settings saved successfully"
+ expect(current_settings.issues_create_limit).to eq(0)
+ end
end
context 'Preferences page' do
@@ -498,13 +500,13 @@ describe 'Admin updates settings', :clean_gitlab_redis_shared_state, :do_not_moc
def check_all_events
page.check('Push')
page.check('Issue')
- page.check('Confidential issue')
- page.check('Merge request')
+ page.check('Confidential Issue')
+ page.check('Merge Request')
page.check('Note')
- page.check('Confidential note')
- page.check('Tag push')
+ page.check('Confidential Note')
+ page.check('Tag Push')
page.check('Pipeline')
- page.check('Wiki page')
+ page.check('Wiki Page')
page.check('Deployment')
end
diff --git a/spec/features/admin/admin_users_impersonation_tokens_spec.rb b/spec/features/admin/admin_users_impersonation_tokens_spec.rb
index 27f2436108c..b9de858e3b9 100644
--- a/spec/features/admin/admin_users_impersonation_tokens_spec.rb
+++ b/spec/features/admin/admin_users_impersonation_tokens_spec.rb
@@ -70,7 +70,7 @@ describe 'Admin > Users > Impersonation Tokens', :js do
accept_confirm { click_on "Revoke" }
expect(page).to have_selector(".settings-message")
- expect(no_personal_access_tokens_message).to have_text("This user has no active Impersonation Tokens.")
+ expect(no_personal_access_tokens_message).to have_text("This user has no active impersonation tokens.")
end
it "removes expired tokens from 'active' section" do
@@ -79,7 +79,7 @@ describe 'Admin > Users > Impersonation Tokens', :js do
visit admin_user_impersonation_tokens_path(user_id: user.username)
expect(page).to have_selector(".settings-message")
- expect(no_personal_access_tokens_message).to have_text("This user has no active Impersonation Tokens.")
+ expect(no_personal_access_tokens_message).to have_text("This user has no active impersonation tokens.")
end
end
end
diff --git a/spec/features/boards/boards_spec.rb b/spec/features/boards/boards_spec.rb
index 0ac8e7c5fc8..e82b1be4310 100644
--- a/spec/features/boards/boards_spec.rb
+++ b/spec/features/boards/boards_spec.rb
@@ -137,6 +137,49 @@ describe 'Issue Boards', :js do
expect(find('.board:nth-child(4)')).to have_selector('.board-card', count: 0)
end
+ context 'search list negation queries' do
+ context 'with the NOT queries feature flag disabled' do
+ before do
+ stub_feature_flags(not_issuable_queries: false)
+ visit project_board_path(project, board)
+ end
+
+ it 'does not have the != option' do
+ find('.filtered-search').set('label:')
+
+ wait_for_requests
+ within('#js-dropdown-operator') do
+ tokens = all(:css, 'li.filter-dropdown-item')
+ expect(tokens.count).to eq(1)
+ button = tokens[0].find('button')
+ expect(button).to have_content('=')
+ expect(button).not_to have_content('!=')
+ end
+ end
+ end
+
+ context 'with the NOT queries feature flag enabled' do
+ before do
+ stub_feature_flags(not_issuable_queries: true)
+ visit project_board_path(project, board)
+ end
+
+ it 'does not have the != option' do
+ find('.filtered-search').set('label:')
+
+ wait_for_requests
+ within('#js-dropdown-operator') do
+ tokens = all(:css, 'li.filter-dropdown-item')
+ expect(tokens.count).to eq(2)
+ button = tokens[0].find('button')
+ expect(button).to have_content('=')
+ button = tokens[1].find('button')
+ expect(button).to have_content('!=')
+ end
+ end
+ end
+ end
+
it 'allows user to delete board' do
page.within(find('.board:nth-child(2)')) do
accept_confirm { find('.board-delete').click }
@@ -549,6 +592,17 @@ describe 'Issue Boards', :js do
end
end
+ context 'issue board focus mode' do
+ before do
+ visit project_board_path(project, board)
+ wait_for_requests
+ end
+
+ it 'shows the button' do
+ expect(page).to have_link('Toggle focus mode')
+ end
+ end
+
context 'keyboard shortcuts' do
before do
visit project_board_path(project, board)
diff --git a/spec/features/boards/focus_mode_spec.rb b/spec/features/boards/focus_mode_spec.rb
new file mode 100644
index 00000000000..fff3cce3c1a
--- /dev/null
+++ b/spec/features/boards/focus_mode_spec.rb
@@ -0,0 +1,17 @@
+# frozen_string_literal: true
+
+require 'spec_helper'
+
+describe 'Issue Boards focus mode', :js do
+ let(:project) { create(:project, :public) }
+
+ before do
+ visit project_boards_path(project)
+
+ wait_for_requests
+ end
+
+ it 'shows focus mode button to guest users' do
+ expect(page).to have_selector('.board-extra-actions .js-focus-mode-btn')
+ end
+end
diff --git a/spec/features/boards/sidebar_spec.rb b/spec/features/boards/sidebar_spec.rb
index f30689240c5..d05709b7e2f 100644
--- a/spec/features/boards/sidebar_spec.rb
+++ b/spec/features/boards/sidebar_spec.rb
@@ -217,7 +217,7 @@ describe 'Issue Boards', :js do
wait_for_requests
- click_link "No Milestone"
+ click_link "No milestone"
wait_for_requests
diff --git a/spec/features/commits/user_view_commits_spec.rb b/spec/features/commits/user_view_commits_spec.rb
new file mode 100644
index 00000000000..133baca8b1c
--- /dev/null
+++ b/spec/features/commits/user_view_commits_spec.rb
@@ -0,0 +1,22 @@
+# frozen_string_literal: true
+
+require 'spec_helper'
+
+describe 'Commit > User view commits' do
+ let_it_be(:project) { create(:project, :public, :repository) }
+ let_it_be(:user) { project.creator }
+
+ before do
+ visit project_commits_path(project)
+ end
+
+ describe 'Commits List' do
+ it 'displays the correct number of commits per day in the header' do
+ expect(first('.js-commit-header').find('.commits-count').text).to eq('1 commit')
+ end
+
+ it 'lists the correct number of commits' do
+ expect(page).to have_selector('#commits-list > li:nth-child(2) > ul', count: 1)
+ end
+ end
+end
diff --git a/spec/features/dashboard/help_spec.rb b/spec/features/dashboard/help_spec.rb
deleted file mode 100644
index 3f425001447..00000000000
--- a/spec/features/dashboard/help_spec.rb
+++ /dev/null
@@ -1,21 +0,0 @@
-# frozen_string_literal: true
-
-require 'spec_helper'
-
-RSpec.describe 'Dashboard Help' do
- before do
- sign_in(create(:user))
- end
-
- context 'documentation' do
- it 'renders correctly markdown' do
- visit help_page_path("administration/raketasks/maintenance")
-
- expect(page).to have_content('Gather information about GitLab and the system it runs on')
-
- node = find('.documentation h2 a#user-content-check-gitlab-configuration')
- expect(node[:href]).to eq '#check-gitlab-configuration'
- expect(find(:xpath, "#{node.path}/..").text).to eq 'Check GitLab configuration'
- end
- end
-end
diff --git a/spec/features/dashboard/issues_spec.rb b/spec/features/dashboard/issues_spec.rb
index ff661014fb9..0b2811618b5 100644
--- a/spec/features/dashboard/issues_spec.rb
+++ b/spec/features/dashboard/issues_spec.rb
@@ -73,7 +73,7 @@ RSpec.describe 'Dashboard Issues' do
find('.new-project-item-link').click
- expect(page).to have_current_path("#{project_path}/issues/new")
+ expect(page).to have_current_path("#{project_path}/-/issues/new")
page.within('#content-body') do
expect(page).to have_selector('.issue-form')
diff --git a/spec/features/dashboard/snippets_spec.rb b/spec/features/dashboard/snippets_spec.rb
index 287af7e7b11..94aef03e093 100644
--- a/spec/features/dashboard/snippets_spec.rb
+++ b/spec/features/dashboard/snippets_spec.rb
@@ -35,7 +35,7 @@ describe 'Dashboard snippets' do
element = page.find('.row.empty-state')
expect(element).to have_content("Code snippets")
- expect(element.find('.svg-content img')['src']).to have_content('illustrations/snippets_empty')
+ expect(element.find('.svg-content img.js-lazy-loaded')['src']).to have_content('illustrations/snippets_empty')
end
it 'shows new snippet button in main content area' do
@@ -49,47 +49,6 @@ describe 'Dashboard snippets' do
end
end
- context 'rendering file names' do
- let_it_be(:snippet) { create(:personal_snippet, :public, author: user, file_name: 'foo.txt') }
- let_it_be(:versioned_snippet) { create(:personal_snippet, :repository, :public, author: user, file_name: 'bar.txt') }
-
- before do
- sign_in(user)
- end
-
- context 'when feature flag :version_snippets is disabled' do
- before do
- stub_feature_flags(version_snippets: false)
-
- visit dashboard_snippets_path
- end
-
- it 'contains the snippet file names from the DB' do
- aggregate_failures do
- expect(page).to have_content 'foo.txt'
- expect(page).to have_content('bar.txt')
- expect(page).not_to have_content('.gitattributes')
- end
- end
- end
-
- context 'when feature flag :version_snippets is enabled' do
- before do
- stub_feature_flags(version_snippets: true)
-
- visit dashboard_snippets_path
- end
-
- it 'contains both the versioned and non-versioned filenames' do
- aggregate_failures do
- expect(page).to have_content 'foo.txt'
- expect(page).to have_content('.gitattributes')
- expect(page).not_to have_content('bar.txt')
- end
- end
- end
- end
-
context 'filtering by visibility' do
let_it_be(:snippets) do
[
diff --git a/spec/features/dashboard/todos/todos_spec.rb b/spec/features/dashboard/todos/todos_spec.rb
index 867281da1e6..63867d5796a 100644
--- a/spec/features/dashboard/todos/todos_spec.rb
+++ b/spec/features/dashboard/todos/todos_spec.rb
@@ -3,10 +3,10 @@
require 'spec_helper'
describe 'Dashboard Todos' do
- let(:user) { create(:user, username: 'john') }
- let(:author) { create(:user) }
- let(:project) { create(:project, :public) }
- let(:issue) { create(:issue, due_date: Date.today, title: "Fix bug") }
+ let_it_be(:user) { create(:user, username: 'john') }
+ let_it_be(:author) { create(:user) }
+ let_it_be(:project) { create(:project, :public) }
+ let_it_be(:issue) { create(:issue, due_date: Date.today, title: "Fix bug") }
context 'User does not have todos' do
before do
@@ -357,4 +357,38 @@ describe 'Dashboard Todos' do
expect(page).to have_link "merge request #{todo.target.to_reference}", href: href
end
end
+
+ context 'User has a todo regarding a design' do
+ let_it_be(:target) { create(:design, issue: issue, project: project) }
+ let_it_be(:note) { create(:note, project: project, note: 'I am note, hear me roar') }
+ let_it_be(:todo) do
+ create(:todo, :mentioned,
+ user: user,
+ project: project,
+ target: target,
+ author: author,
+ note: note)
+ end
+
+ before do
+ project.add_developer(user)
+ sign_in(user)
+
+ visit dashboard_todos_path
+ end
+
+ it 'has todo present' do
+ expect(page).to have_selector('.todos-list .todo', count: 1)
+ end
+
+ it 'has a link that will take me to the design page' do
+ click_link "design #{target.to_reference}"
+
+ expectation = Gitlab::Routing.url_helpers.designs_project_issue_path(
+ target.project, target.issue, target.filename
+ )
+
+ expect(current_path).to eq(expectation)
+ end
+ end
end
diff --git a/spec/features/error_tracking/user_filters_errors_by_status_spec.rb b/spec/features/error_tracking/user_filters_errors_by_status_spec.rb
index 51e29e2a5ec..4b5bc16c4db 100644
--- a/spec/features/error_tracking/user_filters_errors_by_status_spec.rb
+++ b/spec/features/error_tracking/user_filters_errors_by_status_spec.rb
@@ -6,7 +6,7 @@ describe 'When a user filters Sentry errors by status', :js, :use_clean_rails_me
include_context 'sentry error tracking context feature'
let_it_be(:issues_response_body) { fixture_file('sentry/issues_sample_response.json') }
- let_it_be(:filtered_errors_by_status_response) { JSON.parse(issues_response_body).filter { |error| error['status'] == 'ignored' }.to_json }
+ let_it_be(:filtered_errors_by_status_response) { Gitlab::Json.parse(issues_response_body).filter { |error| error['status'] == 'ignored' }.to_json }
let(:issues_api_url) { "#{sentry_api_urls.issues_url}?limit=20&query=is:unresolved" }
let(:issues_api_url_filter) { "#{sentry_api_urls.issues_url}?limit=20&query=is:ignored" }
let(:auth_token) {{ 'Authorization' => 'Bearer access_token_123' }}
diff --git a/spec/features/error_tracking/user_sees_error_index_spec.rb b/spec/features/error_tracking/user_sees_error_index_spec.rb
index 842e4a2e8b5..34a3a4b5a49 100644
--- a/spec/features/error_tracking/user_sees_error_index_spec.rb
+++ b/spec/features/error_tracking/user_sees_error_index_spec.rb
@@ -6,7 +6,7 @@ describe 'View error index page', :js, :use_clean_rails_memory_store_caching, :s
include_context 'sentry error tracking context feature'
let_it_be(:issues_response_body) { fixture_file('sentry/issues_sample_response.json') }
- let_it_be(:issues_response) { JSON.parse(issues_response_body) }
+ let_it_be(:issues_response) { Gitlab::Json.parse(issues_response_body) }
let(:issues_api_url) { "#{sentry_api_urls.issues_url}?limit=20&query=is:unresolved" }
before do
diff --git a/spec/features/explore/groups_spec.rb b/spec/features/explore/groups_spec.rb
index 50ec44580d2..aee0a7c5573 100644
--- a/spec/features/explore/groups_spec.rb
+++ b/spec/features/explore/groups_spec.rb
@@ -27,7 +27,7 @@ describe 'Explore Groups', :js do
end
before do
- stub_feature_flags({ vue_issuables_list: { enabled: false, thing: group } })
+ stub_feature_flags(vue_issuables_list: false)
end
shared_examples 'renders public and internal projects' do
diff --git a/spec/features/global_search_spec.rb b/spec/features/global_search_spec.rb
index c499fac6bc0..a7c8c29517e 100644
--- a/spec/features/global_search_spec.rb
+++ b/spec/features/global_search_spec.rb
@@ -21,7 +21,7 @@ describe 'Global search' do
describe 'I search through the issues and I see pagination' do
before do
- allow_next_instance_of(Gitlab::SearchResults) do |instance|
+ allow_next_instance_of(SearchService) do |instance|
allow(instance).to receive(:per_page).and_return(1)
end
create_list(:issue, 2, project: project, title: 'initial')
diff --git a/spec/features/groups/import_export/export_file_spec.rb b/spec/features/groups/import_export/export_file_spec.rb
new file mode 100644
index 00000000000..5829e659722
--- /dev/null
+++ b/spec/features/groups/import_export/export_file_spec.rb
@@ -0,0 +1,59 @@
+# frozen_string_literal: true
+
+require 'spec_helper'
+
+describe 'Group Export', :js do
+ include ExportFileHelper
+
+ let_it_be(:user) { create(:user) }
+ let_it_be(:group) { create(:group) }
+
+ context 'when the signed in user has the required permission level' do
+ before do
+ group.add_owner(user)
+ sign_in(user)
+ end
+
+ it 'allows the user to export the group', :sidekiq_inline do
+ visit edit_group_path(group)
+
+ expect(page).to have_content('Export group')
+
+ click_link('Export group')
+ expect(page).to have_content('Group export started')
+
+ expect(page).to have_content('Download export')
+ end
+ end
+
+ context 'when the group import/export FF is disabled' do
+ before do
+ stub_feature_flags(group_import_export: false)
+
+ group.add_owner(user)
+ sign_in(user)
+ end
+
+ it 'does not show the group export options' do
+ visit edit_group_path(group)
+
+ expect(page).to have_content('Advanced')
+ expect(page).not_to have_content('Export group')
+ end
+ end
+
+ context 'when the signed in user does not have the required permission level' do
+ before do
+ group.add_guest(user)
+
+ sign_in(user)
+ end
+
+ it 'does not let the user export the group' do
+ visit edit_group_path(group)
+
+ expect(page).to have_content('Page Not Found')
+ expect(page).not_to have_content('Export group')
+ end
+ end
+end
diff --git a/spec/features/groups/issues_spec.rb b/spec/features/groups/issues_spec.rb
index e03d7b6d1f7..1cefcd18989 100644
--- a/spec/features/groups/issues_spec.rb
+++ b/spec/features/groups/issues_spec.rb
@@ -12,7 +12,7 @@ describe 'Group issues page' do
let(:path) { issues_group_path(group) }
before do
- stub_feature_flags({ vue_issuables_list: { enabled: false, thing: group } })
+ stub_feature_flags(vue_issuables_list: false)
end
context 'with shared examples' do
@@ -186,4 +186,25 @@ describe 'Group issues page' do
end
end
end
+
+ context 'issues pagination' do
+ let(:user_in_group) { create(:group_member, :maintainer, user: create(:user), group: group ).user }
+
+ let!(:issues) do
+ (1..25).to_a.map { |index| create(:issue, project: project, title: "Issue #{index}") }
+ end
+
+ before do
+ sign_in(user_in_group)
+ visit issues_group_path(group)
+ end
+
+ it 'shows the pagination' do
+ expect(page).to have_selector('.gl-pagination')
+ end
+
+ it 'first pagination item is active' do
+ expect(page).to have_css(".js-first-button a.page-link.active")
+ end
+ end
end
diff --git a/spec/features/groups/members/leave_group_spec.rb b/spec/features/groups/members/leave_group_spec.rb
index df15f995be4..5c7c83aea6d 100644
--- a/spec/features/groups/members/leave_group_spec.rb
+++ b/spec/features/groups/members/leave_group_spec.rb
@@ -31,8 +31,6 @@ describe 'Groups > Members > Leave group' do
page.accept_confirm
- expect(find('.flash-notice')).to have_content "You left the \"#{group.full_name}\" group"
- expect(page).to have_content left_group_message(group)
expect(current_path).to eq(dashboard_groups_path)
expect(group.users).not_to include(user)
end
diff --git a/spec/features/groups/members/manage_groups_spec.rb b/spec/features/groups/members/manage_groups_spec.rb
index 55f9418521f..593c450c6d6 100644
--- a/spec/features/groups/members/manage_groups_spec.rb
+++ b/spec/features/groups/members/manage_groups_spec.rb
@@ -15,75 +15,56 @@ describe 'Groups > Members > Manage groups', :js do
sign_in(user)
end
- context 'with share groups with groups feature flag' do
- before do
- stub_feature_flags(shared_with_group: true)
- end
-
- it 'add group to group' do
- visit group_group_members_path(shared_group)
+ it 'add group to group' do
+ visit group_group_members_path(shared_group)
- add_group(shared_with_group.id, 'Reporter')
+ add_group(shared_with_group.id, 'Reporter')
- page.within(first_row) do
- expect(page).to have_content(shared_with_group.name)
- expect(page).to have_content('Reporter')
- end
+ page.within(first_row) do
+ expect(page).to have_content(shared_with_group.name)
+ expect(page).to have_content('Reporter')
end
+ end
- it 'remove user from group' do
- create(:group_group_link, shared_group: shared_group,
- shared_with_group: shared_with_group, group_access: ::Gitlab::Access::DEVELOPER)
-
- visit group_group_members_path(shared_group)
-
- expect(page).to have_content(shared_with_group.name)
+ it 'remove user from group' do
+ create(:group_group_link, shared_group: shared_group,
+ shared_with_group: shared_with_group, group_access: ::Gitlab::Access::DEVELOPER)
- accept_confirm do
- find(:css, '#existing_shares li', text: shared_with_group.name).find(:css, 'a.btn-remove').click
- end
+ visit group_group_members_path(shared_group)
- wait_for_requests
+ expect(page).to have_content(shared_with_group.name)
- expect(page).not_to have_content(shared_with_group.name)
+ accept_confirm do
+ find(:css, '#existing_shares li', text: shared_with_group.name).find(:css, 'a.btn-remove').click
end
- it 'update group to owner level' do
- create(:group_group_link, shared_group: shared_group,
- shared_with_group: shared_with_group, group_access: ::Gitlab::Access::DEVELOPER)
+ wait_for_requests
- visit group_group_members_path(shared_group)
+ expect(page).not_to have_content(shared_with_group.name)
+ end
- page.within(first_row) do
- click_button('Developer')
- click_link('Maintainer')
+ it 'update group to owner level' do
+ create(:group_group_link, shared_group: shared_group,
+ shared_with_group: shared_with_group, group_access: ::Gitlab::Access::DEVELOPER)
- wait_for_requests
+ visit group_group_members_path(shared_group)
- expect(page).to have_button('Maintainer')
- end
- end
+ page.within(first_row) do
+ click_button('Developer')
+ click_link('Maintainer')
- def add_group(id, role)
- page.click_link 'Invite group'
- page.within ".invite-group-form" do
- select2(id, from: "#shared_with_group_id")
- select(role, from: "shared_group_access")
- click_button "Invite"
- end
- end
- end
+ wait_for_requests
- context 'without share groups with groups feature flag' do
- before do
- stub_feature_flags(share_group_with_group: false)
+ expect(page).to have_button('Maintainer')
end
+ end
- it 'does not render invitation form and tabs' do
- visit group_group_members_path(shared_group)
-
- expect(page).not_to have_link('Invite member')
- expect(page).not_to have_link('Invite group')
+ def add_group(id, role)
+ page.click_link 'Invite group'
+ page.within ".invite-group-form" do
+ select2(id, from: "#shared_with_group_id")
+ select(role, from: "shared_group_access")
+ click_button "Invite"
end
end
end
diff --git a/spec/features/groups/members/master_adds_member_with_expiration_date_spec.rb b/spec/features/groups/members/master_adds_member_with_expiration_date_spec.rb
new file mode 100644
index 00000000000..491937ce4ab
--- /dev/null
+++ b/spec/features/groups/members/master_adds_member_with_expiration_date_spec.rb
@@ -0,0 +1,68 @@
+# frozen_string_literal: true
+
+require 'spec_helper'
+
+describe 'Groups > Members > Owner adds member with expiration date', :js do
+ include Select2Helper
+ include ActiveSupport::Testing::TimeHelpers
+
+ let(:user1) { create(:user, name: 'John Doe') }
+ let!(:new_member) { create(:user, name: 'Mary Jane') }
+ let(:group) { create(:group) }
+
+ before do
+ group.add_owner(user1)
+ sign_in(user1)
+ end
+
+ it 'expiration date is displayed in the members list' do
+ travel_to Time.zone.parse('2016-08-06 08:00') do
+ date = 4.days.from_now
+ visit group_group_members_path(group)
+
+ page.within '.invite-users-form' do
+ select2(new_member.id, from: '#user_ids', multiple: true)
+ fill_in 'expires_at', with: date.to_s(:medium) + "\n"
+ click_on 'Invite'
+ end
+
+ page.within "#group_member_#{group_member_id(new_member)}" do
+ expect(page).to have_content('Expires in 4 days')
+ end
+ end
+ end
+
+ it 'change expiration date' do
+ travel_to Time.zone.parse('2016-08-06 08:00') do
+ date = 3.days.from_now
+ group.add_developer(new_member)
+
+ visit group_group_members_path(group)
+
+ page.within "#group_member_#{group_member_id(new_member)}" do
+ find('.js-access-expiration-date').set date.to_s(:medium) + "\n"
+ wait_for_requests
+ expect(page).to have_content('Expires in 3 days')
+ end
+ end
+ end
+
+ it 'remove expiration date' do
+ travel_to Time.zone.parse('2016-08-06 08:00') do
+ date = 3.days.from_now
+ group_member = create(:group_member, :developer, user: new_member, group: group, expires_at: date.to_s(:medium))
+
+ visit group_group_members_path(group)
+
+ page.within "#group_member_#{group_member.id}" do
+ find('.js-clear-input').click
+ wait_for_requests
+ expect(page).not_to have_content('Expires in 3 days')
+ end
+ end
+ end
+
+ def group_member_id(user)
+ group.members.find_by(user_id: new_member).id
+ end
+end
diff --git a/spec/features/groups/navbar_spec.rb b/spec/features/groups/navbar_spec.rb
index 0cdc2aa88f4..fd5b4ec9345 100644
--- a/spec/features/groups/navbar_spec.rb
+++ b/spec/features/groups/navbar_spec.rb
@@ -10,7 +10,42 @@ describe 'Group navbar' do
let_it_be(:user) { create(:user) }
let_it_be(:group) { create(:group) }
+ let(:structure) do
+ [
+ {
+ nav_item: _('Group overview'),
+ nav_sub_items: [
+ _('Details'),
+ _('Activity')
+ ]
+ },
+ {
+ nav_item: _('Issues'),
+ nav_sub_items: [
+ _('List'),
+ _('Board'),
+ _('Labels'),
+ _('Milestones')
+ ]
+ },
+ {
+ nav_item: _('Merge Requests'),
+ nav_sub_items: []
+ },
+ {
+ nav_item: _('Kubernetes'),
+ nav_sub_items: []
+ },
+ (analytics_nav_item if Gitlab.ee?),
+ {
+ nav_item: _('Members'),
+ nav_sub_items: []
+ }
+ ]
+ end
+
before do
+ stub_feature_flags(group_push_rules: false)
group.add_maintainer(user)
sign_in(user)
end
@@ -20,4 +55,21 @@ describe 'Group navbar' do
visit group_path(group)
end
end
+
+ context 'when container registry is available' do
+ before do
+ stub_config(registry: { enabled: true })
+
+ insert_after_nav_item(
+ _('Kubernetes'),
+ new_nav_item: {
+ nav_item: _('Packages & Registries'),
+ nav_sub_items: [_('Container Registry')]
+ }
+ )
+ visit group_path(group)
+ end
+
+ it_behaves_like 'verified navigation bar'
+ end
end
diff --git a/spec/features/groups_spec.rb b/spec/features/groups_spec.rb
index d2e65c02e37..c1cb0b4951e 100644
--- a/spec/features/groups_spec.rb
+++ b/spec/features/groups_spec.rb
@@ -262,6 +262,42 @@ describe 'Group' do
end
end
+ describe 'new subgroup / project button' do
+ let(:group) { create(:group, project_creation_level: Gitlab::Access::NO_ONE_PROJECT_ACCESS, subgroup_creation_level: Gitlab::Access::OWNER_SUBGROUP_ACCESS) }
+
+ it 'new subgroup button is displayed without project creation permission' do
+ visit group_path(group)
+
+ page.within '.group-buttons' do
+ expect(page).to have_link('New subgroup')
+ end
+ end
+
+ it 'new subgroup button is displayed together with new project button when having project creation permission' do
+ group.update!(project_creation_level: Gitlab::Access::MAINTAINER_PROJECT_ACCESS)
+ visit group_path(group)
+
+ page.within '.group-buttons' do
+ expect(page).to have_css("li[data-text='New subgroup']", visible: false)
+ expect(page).to have_css("li[data-text='New project']", visible: false)
+ end
+ end
+
+ it 'new project button is displayed without subgroup creation permission' do
+ group.update!(project_creation_level: Gitlab::Access::MAINTAINER_PROJECT_ACCESS)
+ user = create(:user)
+
+ group.add_maintainer(user)
+ sign_out(:user)
+ sign_in(user)
+
+ visit group_path(group)
+ page.within '.group-buttons' do
+ expect(page).to have_link('New project')
+ end
+ end
+ end
+
def remove_with_confirm(button_text, confirm_with)
click_button button_text
fill_in 'confirm_name_input', with: confirm_with
diff --git a/spec/features/help_pages_spec.rb b/spec/features/help_pages_spec.rb
index 88a7aa51326..1ba3849fe2c 100644
--- a/spec/features/help_pages_spec.rb
+++ b/spec/features/help_pages_spec.rb
@@ -4,35 +4,9 @@ require 'spec_helper'
describe 'Help Pages' do
describe 'Get the main help page' do
- shared_examples_for 'help page' do |prefix: ''|
- it 'prefixes links correctly' do
- expect(page).to have_selector(%(div.documentation-index > table tbody tr td a[href="#{prefix}/help/api/README.md"]))
- end
- end
-
- context 'without a trailing slash' do
- before do
- visit help_path
- end
-
- it_behaves_like 'help page'
- end
-
- context 'with a trailing slash' do
- before do
- visit help_path + '/'
- end
-
- it_behaves_like 'help page'
- end
-
- context 'with a relative installation' do
- before do
- stub_config_setting(relative_url_root: '/gitlab')
- visit help_path
- end
-
- it_behaves_like 'help page', prefix: '/gitlab'
+ before do
+ allow(File).to receive(:read).and_call_original
+ allow(File).to receive(:read).with(Rails.root.join('doc', 'README.md')).and_return(fixture_file('sample_doc.md'))
end
context 'quick link shortcuts', :js do
diff --git a/spec/features/issues/bulk_assignment_labels_spec.rb b/spec/features/issues/bulk_assignment_labels_spec.rb
index d036fde5657..fc9176715c3 100644
--- a/spec/features/issues/bulk_assignment_labels_spec.rb
+++ b/spec/features/issues/bulk_assignment_labels_spec.rb
@@ -274,7 +274,7 @@ describe 'Issues > Labels bulk assignment' do
expect(find("#issue_#{issue2.id}")).to have_content 'First Release'
check 'check-all-issues'
- open_milestone_dropdown(['No Milestone'])
+ open_milestone_dropdown(['No milestone'])
update_issues
expect(find("#issue_#{issue1.id}")).to have_content 'bug'
diff --git a/spec/features/issues/filtered_search/filter_issues_spec.rb b/spec/features/issues/filtered_search/filter_issues_spec.rb
index 5e2b5921e06..3ee5840e1b9 100644
--- a/spec/features/issues/filtered_search/filter_issues_spec.rb
+++ b/spec/features/issues/filtered_search/filter_issues_spec.rb
@@ -81,6 +81,26 @@ describe 'Filter issues', :js do
expect_filtered_search_input(search_term)
end
+ context 'with the NOT queries feature flag disabled' do
+ before do
+ stub_feature_flags(not_issuable_queries: false)
+ visit project_issues_path(project)
+ end
+
+ it 'does not have the != option' do
+ input_filtered_search("label:", submit: false)
+
+ wait_for_requests
+ within('#js-dropdown-operator') do
+ tokens = all(:css, 'li.filter-dropdown-item')
+ expect(tokens.count).to eq(1)
+ button = tokens[0].find('button')
+ expect(button).to have_content('=')
+ expect(button).not_to have_content('!=')
+ end
+ end
+ end
+
describe 'filter issues by author' do
context 'only author' do
it 'filters issues by searched author' do
@@ -153,16 +173,16 @@ describe 'Filter issues', :js do
expect_filtered_search_input_empty
end
- it 'filters issues by no label' do
- input_filtered_search('label:=none')
+ it 'filters issues by any label' do
+ input_filtered_search('label:=any')
- expect_tokens([label_token('None', false)])
+ expect_tokens([label_token('Any', false)])
expect_issues_list_count(4)
expect_filtered_search_input_empty
end
it 'filters issues by no label' do
- input_filtered_search('label:!=none')
+ input_filtered_search('label:=none')
expect_tokens([label_token('None', false)])
expect_issues_list_count(4)
@@ -351,14 +371,6 @@ describe 'Filter issues', :js do
expect_filtered_search_input_empty
end
- it 'filters issues by negation of no milestone' do
- input_filtered_search("milestone:!=none ")
-
- expect_tokens([milestone_token('None', false, '!=')])
- expect_issues_list_count(5)
- expect_filtered_search_input_empty
- end
-
it 'filters issues by upcoming milestones' do
create(:milestone, project: project, due_date: 1.month.from_now) do |future_milestone|
create(:issue, project: project, milestone: future_milestone, author: user)
@@ -376,10 +388,14 @@ describe 'Filter issues', :js do
create(:issue, project: project, milestone: future_milestone, author: user)
end
+ create(:milestone, project: project, due_date: 3.days.ago) do |past_milestone|
+ create(:issue, project: project, milestone: past_milestone, author: user)
+ end
+
input_filtered_search("milestone:!=upcoming")
expect_tokens([milestone_token('Upcoming', false, '!=')])
- expect_issues_list_count(8)
+ expect_issues_list_count(1)
expect_filtered_search_input_empty
end
@@ -392,10 +408,13 @@ describe 'Filter issues', :js do
end
it 'filters issues by negation of started milestones' do
+ milestone2 = create(:milestone, title: "9", project: project, start_date: 2.weeks.from_now)
+ create(:issue, project: project, author: user, title: "something else", milestone: milestone2)
+
input_filtered_search("milestone:!=started")
expect_tokens([milestone_token('Started', false, '!=')])
- expect_issues_list_count(3)
+ expect_issues_list_count(1)
expect_filtered_search_input_empty
end
diff --git a/spec/features/issues/filtered_search/visual_tokens_spec.rb b/spec/features/issues/filtered_search/visual_tokens_spec.rb
index 3c50cb4c997..d34253b3c5e 100644
--- a/spec/features/issues/filtered_search/visual_tokens_spec.rb
+++ b/spec/features/issues/filtered_search/visual_tokens_spec.rb
@@ -113,7 +113,7 @@ describe 'Visual tokens', :js do
describe 'add new token after editing existing token' do
before do
input_filtered_search('author:=@root assignee:=none', submit: false)
- first('.tokens-container .filtered-search-token').double_click
+ first('.tokens-container .filtered-search-token').click
filtered_search.send_keys(' ')
end
@@ -175,4 +175,20 @@ describe 'Visual tokens', :js do
expect(token.find('.name').text).to eq('Label')
expect(token.find('.operator').text).to eq('=')
end
+
+ describe 'Any/None option' do
+ it 'hidden when NOT operator is selected' do
+ input_filtered_search('milestone:!=', extra_space: false, submit: false)
+
+ expect(page).not_to have_selector("#js-dropdown-milestone", text: 'Any')
+ expect(page).not_to have_selector("#js-dropdown-milestone", text: 'None')
+ end
+
+ it 'shown when EQUAL operator is selected' do
+ input_filtered_search('milestone:=', extra_space: false, submit: false)
+
+ expect(page).to have_selector("#js-dropdown-milestone", text: 'Any')
+ expect(page).to have_selector("#js-dropdown-milestone", text: 'None')
+ end
+ end
end
diff --git a/spec/features/issues/spam_issues_spec.rb b/spec/features/issues/spam_issues_spec.rb
index b59cd2d632a..47e7022011d 100644
--- a/spec/features/issues/spam_issues_spec.rb
+++ b/spec/features/issues/spam_issues_spec.rb
@@ -23,9 +23,13 @@ describe 'New issue', :js do
sign_in(user)
end
- context 'when identified as spam' do
+ context 'when SpamVerdictService disallows' do
+ include_context 'includes Spam constants'
+
before do
- WebMock.stub_request(:any, /.*akismet.com.*/).to_return(body: "true", status: 200)
+ allow_next_instance_of(Spam::SpamVerdictService) do |verdict_service|
+ allow(verdict_service).to receive(:execute).and_return(DISALLOW)
+ end
visit new_project_issue_path(project)
end
@@ -33,23 +37,22 @@ describe 'New issue', :js do
context 'when allow_possible_spam feature flag is false' do
before do
stub_feature_flags(allow_possible_spam: false)
- end
- it 'creates an issue after solving reCaptcha' do
fill_in 'issue_title', with: 'issue title'
fill_in 'issue_description', with: 'issue description'
+ end
+ it 'rejects issue creation' do
click_button 'Submit issue'
- # it is impossible to test recaptcha automatically and there is no possibility to fill in recaptcha
- # recaptcha verification is skipped in test environment and it always returns true
+ expect(page).to have_content('discarded')
+ expect(page).not_to have_content('potential spam')
expect(page).not_to have_content('issue title')
- expect(page).to have_css('.recaptcha')
-
- click_button 'Submit issue'
+ end
- expect(page.find('.issue-details h2.title')).to have_content('issue title')
- expect(page.find('.issue-details .description')).to have_content('issue description')
+ it 'creates a spam log record' do
+ expect { click_button 'Submit issue' }
+ .to log_spam(title: 'issue title', description: 'issue description', user_id: user.id, noteable_type: 'Issue')
end
end
@@ -59,10 +62,9 @@ describe 'New issue', :js do
fill_in 'issue_description', with: 'issue description'
end
- it 'creates an issue without a need to solve reCaptcha' do
+ it 'allows issue creation' do
click_button 'Submit issue'
- expect(page).not_to have_css('.recaptcha')
expect(page.find('.issue-details h2.title')).to have_content('issue title')
expect(page.find('.issue-details .description')).to have_content('issue description')
end
@@ -74,9 +76,98 @@ describe 'New issue', :js do
end
end
- context 'when not identified as spam' do
+ context 'when SpamVerdictService requires recaptcha' do
+ include_context 'includes Spam constants'
+
before do
- WebMock.stub_request(:any, /.*akismet.com.*/).to_return(body: 'false', status: 200)
+ allow_next_instance_of(Spam::SpamVerdictService) do |verdict_service|
+ allow(verdict_service).to receive(:execute).and_return(REQUIRE_RECAPTCHA)
+ end
+
+ visit new_project_issue_path(project)
+ end
+
+ context 'when recaptcha is enabled' do
+ before do
+ stub_application_setting(recaptcha_enabled: true)
+ end
+
+ context 'when allow_possible_spam feature flag is false' do
+ before do
+ stub_feature_flags(allow_possible_spam: false)
+ end
+
+ it 'creates an issue after solving reCaptcha' do
+ fill_in 'issue_title', with: 'issue title'
+ fill_in 'issue_description', with: 'issue description'
+
+ click_button 'Submit issue'
+
+ # it is impossible to test reCAPTCHA automatically and there is no possibility to fill in recaptcha
+ # reCAPTCHA verification is skipped in test environment and it always returns true
+ expect(page).not_to have_content('issue title')
+ expect(page).to have_css('.recaptcha')
+
+ click_button 'Submit issue'
+
+ expect(page.find('.issue-details h2.title')).to have_content('issue title')
+ expect(page.find('.issue-details .description')).to have_content('issue description')
+ end
+ end
+
+ context 'when allow_possible_spam feature flag is true' do
+ before do
+ fill_in 'issue_title', with: 'issue title'
+ fill_in 'issue_description', with: 'issue description'
+ end
+
+ it 'creates an issue without a need to solve reCAPTCHA' do
+ click_button 'Submit issue'
+
+ expect(page).not_to have_css('.recaptcha')
+ expect(page.find('.issue-details h2.title')).to have_content('issue title')
+ expect(page.find('.issue-details .description')).to have_content('issue description')
+ end
+
+ it 'creates a spam log record' do
+ expect { click_button 'Submit issue' }
+ .to log_spam(title: 'issue title', description: 'issue description', user_id: user.id, noteable_type: 'Issue')
+ end
+ end
+ end
+
+ context 'when reCAPTCHA is not enabled' do
+ before do
+ stub_application_setting(recaptcha_enabled: false)
+ end
+
+ context 'when allow_possible_spam feature flag is true' do
+ before do
+ fill_in 'issue_title', with: 'issue title'
+ fill_in 'issue_description', with: 'issue description'
+ end
+
+ it 'creates an issue without a need to solve reCaptcha' do
+ click_button 'Submit issue'
+
+ expect(page).not_to have_css('.recaptcha')
+ expect(page.find('.issue-details h2.title')).to have_content('issue title')
+ expect(page.find('.issue-details .description')).to have_content('issue description')
+ end
+
+ it 'creates a spam log record' do
+ expect { click_button 'Submit issue' }
+ .to log_spam(title: 'issue title', description: 'issue description', user_id: user.id, noteable_type: 'Issue')
+ end
+ end
+ end
+ end
+
+ context 'when the SpamVerdictService allows' do
+ before do
+ allow_next_instance_of(Spam::SpamVerdictService) do |verdict_service|
+ allow(verdict_service).to receive(:execute).and_return(ALLOW)
+ end
visit new_project_issue_path(project)
end
diff --git a/spec/features/issues/update_issues_spec.rb b/spec/features/issues/update_issues_spec.rb
index 45a0b1932a2..98f70df1c8b 100644
--- a/spec/features/issues/update_issues_spec.rb
+++ b/spec/features/issues/update_issues_spec.rb
@@ -95,7 +95,7 @@ describe 'Multiple issue updating from issues#index', :js do
find('#check-all-issues').click
find('.issues-bulk-update .js-milestone-select').click
- find('.dropdown-menu-milestone a', text: "No Milestone").click
+ find('.dropdown-menu-milestone a', text: "No milestone").click
click_update_issues_button
expect(find('.issue:first-child')).not_to have_content milestone.title
diff --git a/spec/features/issues/user_creates_branch_and_merge_request_spec.rb b/spec/features/issues/user_creates_branch_and_merge_request_spec.rb
index 7eecfd1ccf4..848dbbb85a6 100644
--- a/spec/features/issues/user_creates_branch_and_merge_request_spec.rb
+++ b/spec/features/issues/user_creates_branch_and_merge_request_spec.rb
@@ -164,17 +164,7 @@ describe 'User creates branch and merge request on issue page', :js do
context 'when issue is confidential' do
let(:issue) { create(:issue, :confidential, project: project) }
- it 'disables the create branch button' do
- stub_feature_flags(create_confidential_merge_request: false)
-
- visit project_issue_path(project, issue)
-
- expect(page).not_to have_css('.create-mr-dropdown-wrap')
- end
-
- it 'enables the create branch button when feature flag is enabled' do
- stub_feature_flags(create_confidential_merge_request: true)
-
+ it 'enables the create branch button' do
visit project_issue_path(project, issue)
expect(page).to have_css('.create-mr-dropdown-wrap')
diff --git a/spec/features/issues/user_sees_sidebar_updates_in_realtime_spec.rb b/spec/features/issues/user_sees_sidebar_updates_in_realtime_spec.rb
new file mode 100644
index 00000000000..c3f17227701
--- /dev/null
+++ b/spec/features/issues/user_sees_sidebar_updates_in_realtime_spec.rb
@@ -0,0 +1,32 @@
+# frozen_string_literal: true
+
+require 'spec_helper'
+
+describe 'Issues > Real-time sidebar', :js do
+ let_it_be(:project) { create(:project, :public) }
+ let_it_be(:issue) { create(:issue, project: project) }
+ let_it_be(:user) { create(:user) }
+
+ before_all do
+ project.add_developer(user)
+ end
+
+ it 'updates the assignee in real-time' do
+ Capybara::Session.new(:other_session)
+
+ using_session :other_session do
+ visit project_issue_path(project, issue)
+ expect(page.find('.assignee')).to have_content 'None'
+ end
+
+ gitlab_sign_in(user)
+ visit project_issue_path(project, issue)
+ expect(page.find('.assignee')).to have_content 'None'
+
+ click_button 'assign yourself'
+
+ using_session :other_session do
+ expect(page.find('.assignee')).to have_content user.name
+ end
+ end
+end
diff --git a/spec/features/markdown/copy_as_gfm_spec.rb b/spec/features/markdown/copy_as_gfm_spec.rb
index f85acc28645..d40c2b8bafd 100644
--- a/spec/features/markdown/copy_as_gfm_spec.rb
+++ b/spec/features/markdown/copy_as_gfm_spec.rb
@@ -173,6 +173,12 @@ describe 'Copy as GFM', :js do
)
verify_media_with_partial_path(
+ '[test.txt](/uploads/a123/image.txt)',
+
+ project_media_uri(@project, '/uploads/a123/image.txt')
+ )
+
+ verify_media_with_partial_path(
'![Image](/uploads/a123/image.png)',
project_media_uri(@project, '/uploads/a123/image.png')
diff --git a/spec/features/markdown/metrics_spec.rb b/spec/features/markdown/metrics_spec.rb
index dadb9571c54..7b0eb8959a5 100644
--- a/spec/features/markdown/metrics_spec.rb
+++ b/spec/features/markdown/metrics_spec.rb
@@ -137,7 +137,7 @@ describe 'Metrics rendering', :js, :use_clean_rails_memory_store_caching, :sidek
end
context 'transient metrics embeds' do
- let(:metrics_url) { urls.metrics_project_environment_url(project, environment, embed_json: embed_json) }
+ let(:metrics_url) { urls.metrics_dashboard_project_environment_url(project, environment, embed_json: embed_json) }
let(:title) { 'Important Metrics' }
let(:embed_json) do
{
diff --git a/spec/features/merge_request/maintainer_edits_fork_spec.rb b/spec/features/merge_request/maintainer_edits_fork_spec.rb
index f1ee6aaa897..17ff494a6fa 100644
--- a/spec/features/merge_request/maintainer_edits_fork_spec.rb
+++ b/spec/features/merge_request/maintainer_edits_fork_spec.rb
@@ -20,7 +20,7 @@ describe 'a maintainer edits files on a source-branch of an MR from a fork', :js
end
before do
- stub_feature_flags(web_ide_default: false, single_mr_diff_view: { enabled: false, thing: target_project }, code_navigation: false)
+ stub_feature_flags(web_ide_default: false, single_mr_diff_view: false, code_navigation: false)
target_project.add_maintainer(user)
sign_in(user)
diff --git a/spec/features/merge_request/user_interacts_with_batched_mr_diffs_spec.rb b/spec/features/merge_request/user_interacts_with_batched_mr_diffs_spec.rb
index 8633d67f875..2a4192374bd 100644
--- a/spec/features/merge_request/user_interacts_with_batched_mr_diffs_spec.rb
+++ b/spec/features/merge_request/user_interacts_with_batched_mr_diffs_spec.rb
@@ -10,7 +10,7 @@ describe 'Batch diffs', :js do
let(:merge_request) { create(:merge_request, source_project: project, source_branch: 'master', target_branch: 'empty-branch') }
before do
- stub_feature_flags(single_mr_diff_view: { enabled: true, thing: project })
+ stub_feature_flags(single_mr_diff_view: project)
stub_feature_flags(diffs_batch_load: true)
sign_in(project.owner)
diff --git a/spec/features/merge_request/user_posts_diff_notes_spec.rb b/spec/features/merge_request/user_posts_diff_notes_spec.rb
index 19f82058be2..ebfb5ce796f 100644
--- a/spec/features/merge_request/user_posts_diff_notes_spec.rb
+++ b/spec/features/merge_request/user_posts_diff_notes_spec.rb
@@ -235,7 +235,9 @@ describe 'Merge request > User posts diff notes', :js do
def should_allow_dismissing_a_comment(line_holder, diff_side = nil)
write_comment_on_line(line_holder, diff_side)
- find('.js-close-discussion-note-form').click
+ accept_confirm do
+ find('.js-close-discussion-note-form').click
+ end
assert_comment_dismissal(line_holder)
end
diff --git a/spec/features/merge_request/user_posts_notes_spec.rb b/spec/features/merge_request/user_posts_notes_spec.rb
index b22f5a6c211..0548d958322 100644
--- a/spec/features/merge_request/user_posts_notes_spec.rb
+++ b/spec/features/merge_request/user_posts_notes_spec.rb
@@ -147,7 +147,10 @@ describe 'Merge request > User posts notes', :js do
it 'resets the edit note form textarea with the original content of the note if cancelled' do
within('.current-note-edit-form') do
fill_in 'note[note]', with: 'Some new content'
- find('.btn-cancel').click
+
+ accept_confirm do
+ find('.btn-cancel').click
+ end
end
expect(find('.js-note-text').text).to eq ''
end
diff --git a/spec/features/merge_request/user_resolves_conflicts_spec.rb b/spec/features/merge_request/user_resolves_conflicts_spec.rb
index 5fc65f020d3..41a7456aed5 100644
--- a/spec/features/merge_request/user_resolves_conflicts_spec.rb
+++ b/spec/features/merge_request/user_resolves_conflicts_spec.rb
@@ -183,14 +183,14 @@ describe 'Merge request > User resolves conflicts', :js do
end
end
- UNRESOLVABLE_CONFLICTS = {
+ unresolvable_conflicts = {
'conflict-too-large' => 'when the conflicts contain a large file',
'conflict-binary-file' => 'when the conflicts contain a binary file',
'conflict-missing-side' => 'when the conflicts contain a file edited in one branch and deleted in another',
'conflict-non-utf8' => 'when the conflicts contain a non-UTF-8 file'
}.freeze
- UNRESOLVABLE_CONFLICTS.each do |source_branch, description|
+ unresolvable_conflicts.each do |source_branch, description|
context description do
let(:merge_request) { create_merge_request(source_branch) }
diff --git a/spec/features/merge_request/user_resolves_diff_notes_and_discussions_resolve_spec.rb b/spec/features/merge_request/user_resolves_diff_notes_and_discussions_resolve_spec.rb
index b8a5a4036a5..0e30df518d7 100644
--- a/spec/features/merge_request/user_resolves_diff_notes_and_discussions_resolve_spec.rb
+++ b/spec/features/merge_request/user_resolves_diff_notes_and_discussions_resolve_spec.rb
@@ -43,7 +43,7 @@ describe 'Merge request > User resolves diff notes and threads', :js do
context 'single thread' do
it 'shows text with how many threads' do
page.within '.line-resolve-all-container' do
- expect(page).to have_content('0/1 thread resolved')
+ expect(page).to have_content('1 unresolved thread')
end
end
@@ -60,7 +60,7 @@ describe 'Merge request > User resolves diff notes and threads', :js do
end
page.within '.line-resolve-all-container' do
- expect(page).to have_content('1/1 thread resolved')
+ expect(page).to have_content('All threads resolved')
expect(page).to have_selector('.line-resolve-btn.is-active')
end
end
@@ -77,7 +77,7 @@ describe 'Merge request > User resolves diff notes and threads', :js do
end
page.within '.line-resolve-all-container' do
- expect(page).to have_content('1/1 thread resolved')
+ expect(page).to have_content('All threads resolved')
expect(page).to have_selector('.line-resolve-btn.is-active')
end
end
@@ -89,7 +89,7 @@ describe 'Merge request > User resolves diff notes and threads', :js do
end
page.within '.line-resolve-all-container' do
- expect(page).to have_content('0/1 thread resolved')
+ expect(page).to have_content('1 unresolved thread')
end
end
@@ -162,7 +162,7 @@ describe 'Merge request > User resolves diff notes and threads', :js do
end
page.within '.line-resolve-all-container' do
- expect(page).to have_content('1/1 thread resolved')
+ expect(page).to have_content('All threads resolved')
end
end
@@ -174,7 +174,7 @@ describe 'Merge request > User resolves diff notes and threads', :js do
end
page.within '.line-resolve-all-container' do
- expect(page).to have_content('0/1 thread resolved')
+ expect(page).to have_content('1 unresolved thread')
expect(page).not_to have_selector('.line-resolve-btn.is-active')
end
end
@@ -189,7 +189,7 @@ describe 'Merge request > User resolves diff notes and threads', :js do
end
page.within '.line-resolve-all-container' do
- expect(page).to have_content('0/1 thread resolved')
+ expect(page).to have_content('1 unresolved thread')
end
end
end
@@ -203,7 +203,7 @@ describe 'Merge request > User resolves diff notes and threads', :js do
end
page.within '.line-resolve-all-container' do
- expect(page).to have_content('1/1 thread resolved')
+ expect(page).to have_content('All threads resolved')
expect(page).to have_selector('.line-resolve-btn.is-active')
end
end
@@ -218,7 +218,7 @@ describe 'Merge request > User resolves diff notes and threads', :js do
end
page.within '.line-resolve-all-container' do
- expect(page).to have_content('1/1 thread resolved')
+ expect(page).to have_content('All threads resolved')
expect(page).to have_selector('.line-resolve-btn.is-active')
end
end
@@ -275,7 +275,7 @@ describe 'Merge request > User resolves diff notes and threads', :js do
expect(page).to have_content('Last updated')
page.within '.line-resolve-all-container' do
- expect(page).to have_content('0/1 thread resolved')
+ expect(page).to have_content('1 unresolved thread')
end
end
@@ -292,7 +292,7 @@ describe 'Merge request > User resolves diff notes and threads', :js do
end
page.within '.line-resolve-all-container' do
- expect(page).to have_content('1/1 thread resolved')
+ expect(page).to have_content('All threads resolved')
end
end
end
@@ -305,7 +305,7 @@ describe 'Merge request > User resolves diff notes and threads', :js do
it 'shows text with how many threads' do
page.within '.line-resolve-all-container' do
- expect(page).to have_content('0/2 threads resolved')
+ expect(page).to have_content('2 unresolved threads')
end
end
@@ -313,7 +313,7 @@ describe 'Merge request > User resolves diff notes and threads', :js do
click_button('Resolve thread', match: :first)
page.within '.line-resolve-all-container' do
- expect(page).to have_content('1/2 threads resolved')
+ expect(page).to have_content('1 unresolved thread')
end
end
@@ -323,7 +323,7 @@ describe 'Merge request > User resolves diff notes and threads', :js do
end
page.within '.line-resolve-all-container' do
- expect(page).to have_content('2/2 threads resolved')
+ expect(page).to have_content('All threads resolved')
expect(page).to have_selector('.line-resolve-btn.is-active')
end
end
@@ -336,7 +336,7 @@ describe 'Merge request > User resolves diff notes and threads', :js do
end
page.within '.line-resolve-all-container' do
- expect(page).to have_content('2/2 threads resolved')
+ expect(page).to have_content('All threads resolved')
expect(page).to have_selector('.line-resolve-btn.is-active')
end
end
@@ -392,7 +392,7 @@ describe 'Merge request > User resolves diff notes and threads', :js do
context 'changes tab' do
it 'shows text with how many threads' do
page.within '.line-resolve-all-container' do
- expect(page).to have_content('0/1 thread resolved')
+ expect(page).to have_content('1 unresolved thread')
end
end
@@ -408,7 +408,7 @@ describe 'Merge request > User resolves diff notes and threads', :js do
end
page.within '.line-resolve-all-container' do
- expect(page).to have_content('1/1 thread resolved')
+ expect(page).to have_content('All threads resolved')
expect(page).to have_selector('.line-resolve-btn.is-active')
end
end
@@ -423,7 +423,7 @@ describe 'Merge request > User resolves diff notes and threads', :js do
end
page.within '.line-resolve-all-container' do
- expect(page).to have_content('1/1 thread resolved')
+ expect(page).to have_content('All threads resolved')
expect(page).to have_selector('.line-resolve-btn.is-active')
end
end
@@ -435,7 +435,7 @@ describe 'Merge request > User resolves diff notes and threads', :js do
end
page.within '.line-resolve-all-container' do
- expect(page).to have_content('0/1 thread resolved')
+ expect(page).to have_content('1 unresolved thread')
end
end
@@ -449,7 +449,7 @@ describe 'Merge request > User resolves diff notes and threads', :js do
end
page.within '.line-resolve-all-container' do
- expect(page).to have_content('1/1 thread resolved')
+ expect(page).to have_content('All threads resolved')
expect(page).to have_selector('.line-resolve-btn.is-active')
end
end
@@ -466,7 +466,7 @@ describe 'Merge request > User resolves diff notes and threads', :js do
end
page.within '.line-resolve-all-container' do
- expect(page).to have_content('0/1 thread resolved')
+ expect(page).to have_content('1 unresolved thread')
end
end
end
@@ -489,7 +489,7 @@ describe 'Merge request > User resolves diff notes and threads', :js do
end
page.within '.line-resolve-all-container' do
- expect(page).to have_content('0/1 thread resolved')
+ expect(page).to have_content('1 unresolved thread')
end
end
@@ -519,7 +519,7 @@ describe 'Merge request > User resolves diff notes and threads', :js do
end
page.within '.line-resolve-all-container' do
- expect(page).to have_content('1/1 thread resolved')
+ expect(page).to have_content('All threads resolved')
expect(page).to have_selector('.line-resolve-btn.is-active')
end
end
@@ -538,7 +538,7 @@ describe 'Merge request > User resolves diff notes and threads', :js do
end
page.within '.line-resolve-all-container' do
- expect(page).to have_content('0/1 thread resolved')
+ expect(page).to have_content('1 unresolved thread')
end
end
end
@@ -550,17 +550,17 @@ describe 'Merge request > User resolves diff notes and threads', :js do
end
it 'shows resolved icon' do
- expect(page).to have_content '1/1 thread resolved'
+ expect(page).to have_content 'All threads resolved'
click_button 'Toggle thread'
expect(page).to have_selector('.line-resolve-btn.is-active')
end
it 'does not allow user to click resolve button' do
- expect(page).to have_selector('.line-resolve-btn.is-disabled')
+ expect(page).to have_selector('.line-resolve-btn.is-active')
click_button 'Toggle thread'
- expect(page).to have_selector('.line-resolve-btn.is-disabled')
+ expect(page).to have_selector('.line-resolve-btn.is-active')
end
end
end
diff --git a/spec/features/merge_request/user_sees_notes_from_forked_project_spec.rb b/spec/features/merge_request/user_sees_notes_from_forked_project_spec.rb
index 9c9e0dacb87..029f55c2cd6 100644
--- a/spec/features/merge_request/user_sees_notes_from_forked_project_spec.rb
+++ b/spec/features/merge_request/user_sees_notes_from_forked_project_spec.rb
@@ -28,6 +28,7 @@ describe 'Merge request > User sees notes from forked project', :js do
page.within('.discussion-notes') do
find('.btn-text-field').click
+ scroll_to(page.find('#note_note', visible: false))
find('#note_note').send_keys('A reply comment')
find('.js-comment-button').click
end
diff --git a/spec/features/merge_requests/user_mass_updates_spec.rb b/spec/features/merge_requests/user_mass_updates_spec.rb
index 4d3461bf1ae..fa951dd50d3 100644
--- a/spec/features/merge_requests/user_mass_updates_spec.rb
+++ b/spec/features/merge_requests/user_mass_updates_spec.rb
@@ -91,7 +91,7 @@ describe 'Merge requests > User mass updates', :js do
end
it 'removes milestone from the merge request' do
- change_milestone("No Milestone")
+ change_milestone("No milestone")
expect(find('.merge-request')).not_to have_content milestone.title
end
diff --git a/spec/features/milestones/user_creates_milestone_spec.rb b/spec/features/milestones/user_creates_milestone_spec.rb
index 368a2ddecdf..12cb27b0062 100644
--- a/spec/features/milestones/user_creates_milestone_spec.rb
+++ b/spec/features/milestones/user_creates_milestone_spec.rb
@@ -14,13 +14,13 @@ describe "User creates milestone", :js do
end
it "creates milestone" do
- TITLE = "v2.3".freeze
+ title = "v2.3".freeze
- fill_in("Title", with: TITLE)
+ fill_in("Title", with: title)
fill_in("Description", with: "# Description header")
click_button("Create milestone")
- expect(page).to have_content(TITLE)
+ expect(page).to have_content(title)
.and have_content("Issues")
.and have_header_with_correct_id_and_link(1, "Description header", "description-header")
diff --git a/spec/features/milestones/user_views_milestone_spec.rb b/spec/features/milestones/user_views_milestone_spec.rb
index d8bb4902087..ca13e226432 100644
--- a/spec/features/milestones/user_views_milestone_spec.rb
+++ b/spec/features/milestones/user_views_milestone_spec.rb
@@ -14,13 +14,13 @@ describe "User views milestone" do
end
it "avoids N+1 database queries" do
- ISSUE_PARAMS = { project: project, assignees: [user], author: user, milestone: milestone, labels: labels }.freeze
+ issue_params = { project: project, assignees: [user], author: user, milestone: milestone, labels: labels }.freeze
- create(:labeled_issue, ISSUE_PARAMS)
+ create(:labeled_issue, issue_params)
control = ActiveRecord::QueryRecorder.new { visit_milestone }
- create(:labeled_issue, ISSUE_PARAMS)
+ create(:labeled_issue, issue_params)
expect { visit_milestone }.not_to exceed_query_limit(control)
end
diff --git a/spec/features/profiles/emails_spec.rb b/spec/features/profiles/emails_spec.rb
index 4d2cd0f8b56..a41ef9e86ae 100644
--- a/spec/features/profiles/emails_spec.rb
+++ b/spec/features/profiles/emails_spec.rb
@@ -31,6 +31,15 @@ describe 'Profile > Emails' do
expect(email).to be_nil
expect(page).to have_content('Email has already been taken')
end
+
+ it 'does not add an invalid email' do
+ fill_in('Email', with: 'test.@example.com')
+ click_button('Add email address')
+
+ email = user.emails.find_by(email: email)
+ expect(email).to be_nil
+ expect(page).to have_content('Email is invalid')
+ end
end
it 'User removes email' do
@@ -58,7 +67,7 @@ describe 'Profile > Emails' do
email = user.emails.create(email: 'my@email.com')
visit profile_emails_path
- expect { click_link("Resend confirmation email") }.to change { ActionMailer::Base.deliveries.size }
+ expect { click_link("Resend confirmation email") }.to have_enqueued_job.on_queue('mailers')
expect(page).to have_content("Confirmation email sent to #{email.email}")
end
diff --git a/spec/features/profiles/personal_access_tokens_spec.rb b/spec/features/profiles/personal_access_tokens_spec.rb
index 22f9c8d8afc..1fb61eeeb5a 100644
--- a/spec/features/profiles/personal_access_tokens_spec.rb
+++ b/spec/features/profiles/personal_access_tokens_spec.rb
@@ -86,7 +86,7 @@ describe 'Profile > Personal Access Tokens', :js do
accept_confirm { click_on "Revoke" }
expect(page).to have_selector(".settings-message")
- expect(no_personal_access_tokens_message).to have_text("This user has no active Personal Access Tokens.")
+ expect(no_personal_access_tokens_message).to have_text("This user has no active personal access tokens.")
end
it "removes expired tokens from 'active' section" do
@@ -94,7 +94,7 @@ describe 'Profile > Personal Access Tokens', :js do
visit profile_personal_access_tokens_path
expect(page).to have_selector(".settings-message")
- expect(no_personal_access_tokens_message).to have_text("This user has no active Personal Access Tokens.")
+ expect(no_personal_access_tokens_message).to have_text("This user has no active personal access tokens.")
end
context "when revocation fails" do
diff --git a/spec/features/projects/activity/user_sees_design_comment_spec.rb b/spec/features/projects/activity/user_sees_design_comment_spec.rb
new file mode 100644
index 00000000000..9864e9ce29f
--- /dev/null
+++ b/spec/features/projects/activity/user_sees_design_comment_spec.rb
@@ -0,0 +1,51 @@
+# frozen_string_literal: true
+
+require 'spec_helper'
+
+describe 'Projects > Activity > User sees design comment', :js do
+ include DesignManagementTestHelpers
+
+ let_it_be(:project) { create(:project, :repository, :public) }
+ let_it_be(:user) { project.creator }
+ let_it_be(:commenter) { create(:user) }
+ let_it_be(:issue) { create(:closed_issue, project: project) }
+ let_it_be(:design) { create(:design, issue: issue) }
+
+ let(:design_activity) do
+ "#{commenter.name} #{commenter.to_reference} commented on design"
+ end
+
+ let(:issue_activity) do
+ "#{user.name} #{user.to_reference} closed issue #{issue.to_reference}"
+ end
+
+ before_all do
+ project.add_developer(commenter)
+ create(:event, :for_design, project: project, author: commenter, design: design)
+ create(:closed_issue_event, project: project, author: user, target: issue)
+ end
+
+ before do
+ enable_design_management
+ end
+
+ it 'shows the design comment action in the activity page' do
+ visit activity_project_path(project)
+
+ expect(page).to have_content(design_activity)
+ end
+
+ it 'allows to filter out the design event with the "event_filter=issue" URL param', :aggregate_failures do
+ visit activity_project_path(project, event_filter: EventFilter::ISSUE)
+
+ expect(page).not_to have_content(design_activity)
+ expect(page).to have_content(issue_activity)
+ end
+
+ it 'allows to filter in the event with the "event_filter=comments" URL param', :aggregate_failures do
+ visit activity_project_path(project, event_filter: EventFilter::COMMENTS)
+
+ expect(page).to have_content(design_activity)
+ expect(page).not_to have_content(issue_activity)
+ end
+end
diff --git a/spec/features/projects/branches/user_creates_branch_spec.rb b/spec/features/projects/branches/user_creates_branch_spec.rb
index 156edb973cd..8aac188160b 100644
--- a/spec/features/projects/branches/user_creates_branch_spec.rb
+++ b/spec/features/projects/branches/user_creates_branch_spec.rb
@@ -16,18 +16,18 @@ describe "User creates branch", :js do
end
it "creates new branch" do
- BRANCH_NAME = "deploy_keys".freeze
+ branch_name = "deploy_keys".freeze
- create_branch(BRANCH_NAME)
+ create_branch(branch_name)
- expect(page).to have_content(BRANCH_NAME)
+ expect(page).to have_content(branch_name)
end
context "when branch name is invalid" do
it "does not create new branch" do
- INVALID_BRANCH_NAME = "1.0 stable".freeze
+ invalid_branch_name = "1.0 stable".freeze
- fill_in("branch_name", with: INVALID_BRANCH_NAME)
+ fill_in("branch_name", with: invalid_branch_name)
page.find("body").click # defocus the branch_name input
select_branch("master")
diff --git a/spec/features/projects/commit/comments/user_edits_comments_spec.rb b/spec/features/projects/commit/comments/user_edits_comments_spec.rb
index 0fa2b2ff232..29132173674 100644
--- a/spec/features/projects/commit/comments/user_edits_comments_spec.rb
+++ b/spec/features/projects/commit/comments/user_edits_comments_spec.rb
@@ -19,7 +19,7 @@ describe "User edits a comment on a commit", :js do
end
it "edits comment" do
- NEW_COMMENT_TEXT = "+1 Awesome!".freeze
+ new_comment_text = "+1 Awesome!".freeze
page.within(".main-notes-list") do
note = find(".note")
@@ -31,14 +31,14 @@ describe "User edits a comment on a commit", :js do
page.find(".current-note-edit-form textarea")
page.within(".current-note-edit-form") do
- fill_in("note[note]", with: NEW_COMMENT_TEXT)
+ fill_in("note[note]", with: new_comment_text)
click_button("Save comment")
end
wait_for_requests
page.within(".note") do
- expect(page).to have_content(NEW_COMMENT_TEXT)
+ expect(page).to have_content(new_comment_text)
end
end
end
diff --git a/spec/features/projects/environments_pod_logs_spec.rb b/spec/features/projects/environments_pod_logs_spec.rb
index a51f121bf59..32eaf690950 100644
--- a/spec/features/projects/environments_pod_logs_spec.rb
+++ b/spec/features/projects/environments_pod_logs_spec.rb
@@ -2,7 +2,7 @@
require 'spec_helper'
-describe 'Environment > Pod Logs', :js do
+describe 'Environment > Pod Logs', :js, :kubeclient do
include KubernetesHelpers
let(:pod_names) { %w(kube-pod) }
diff --git a/spec/features/projects/files/user_browses_files_spec.rb b/spec/features/projects/files/user_browses_files_spec.rb
index 5c52abaeb62..7e3d8e5c1c5 100644
--- a/spec/features/projects/files/user_browses_files_spec.rb
+++ b/spec/features/projects/files/user_browses_files_spec.rb
@@ -171,6 +171,18 @@ describe "User browses files" do
end
end
+ context 'when commit message has markdown', :js do
+ before do
+ project.repository.create_file(user, 'index', 'test', message: ':star: testing', branch_name: 'master')
+
+ visit(project_tree_path(project, "master"))
+ end
+
+ it 'renders emojis' do
+ expect(page).to have_selector('gl-emoji', count: 2)
+ end
+ end
+
context "when browsing a `improve/awesome` branch", :js do
before do
visit(project_tree_path(project, "improve/awesome"))
@@ -197,6 +209,33 @@ describe "User browses files" do
end
end
+ context "when browsing a `Ääh-test-utf-8` branch", :js do
+ before do
+ project.repository.create_branch('Ääh-test-utf-8', project.repository.root_ref)
+ visit(project_tree_path(project, "Ääh-test-utf-8"))
+ end
+
+ it "shows files from a repository" do
+ expect(page).to have_content("VERSION")
+ .and have_content(".gitignore")
+ .and have_content("LICENSE")
+
+ click_link("files")
+
+ page.within('.repo-breadcrumb') do
+ expect(page).to have_link('files')
+ end
+
+ click_link("html")
+
+ page.within('.repo-breadcrumb') do
+ expect(page).to have_link('html')
+ end
+
+ expect(page).to have_link('500.html')
+ end
+ end
+
context "when browsing a `test-#` branch", :js do
before do
project.repository.create_branch('test-#', project.repository.root_ref)
diff --git a/spec/features/projects/graph_spec.rb b/spec/features/projects/graph_spec.rb
index 6b2a9a6b852..25efb7b28a7 100644
--- a/spec/features/projects/graph_spec.rb
+++ b/spec/features/projects/graph_spec.rb
@@ -59,7 +59,7 @@ describe 'Project Graph', :js do
it 'HTML escapes branch name' do
expect(page.body).to include("Commit statistics for <strong>#{ERB::Util.html_escape(branch_name)}</strong>")
- expect(page.body).not_to include(branch_name)
+ expect(page.find('.dropdown-toggle-text')['innerHTML']).to eq(ERB::Util.html_escape(branch_name))
end
end
diff --git a/spec/features/projects/import_export/export_file_spec.rb b/spec/features/projects/import_export/export_file_spec.rb
index 7ee8f42e6ef..1d6d5ae1b4d 100644
--- a/spec/features/projects/import_export/export_file_spec.rb
+++ b/spec/features/projects/import_export/export_file_spec.rb
@@ -52,7 +52,7 @@ describe 'Import/Export - project export integration test', :js do
project_json_path = File.join(tmpdir, 'project.json')
expect(File).to exist(project_json_path)
- project_hash = JSON.parse(IO.read(project_json_path))
+ project_hash = Gitlab::Json.parse(IO.read(project_json_path))
sensitive_words.each do |sensitive_word|
found = find_sensitive_attributes(sensitive_word, project_hash)
@@ -78,7 +78,7 @@ describe 'Import/Export - project export integration test', :js do
expect(File).to exist(project_json_path)
relations = []
- relations << JSON.parse(IO.read(project_json_path))
+ relations << Gitlab::Json.parse(IO.read(project_json_path))
Dir.glob(File.join(tmpdir, 'tree/project', '*.ndjson')) do |rb_filename|
File.foreach(rb_filename) do |line|
json = ActiveSupport::JSON.decode(line)
diff --git a/spec/features/projects/issues/design_management/user_paginates_designs_spec.rb b/spec/features/projects/issues/design_management/user_paginates_designs_spec.rb
new file mode 100644
index 00000000000..d9a72f2d5c5
--- /dev/null
+++ b/spec/features/projects/issues/design_management/user_paginates_designs_spec.rb
@@ -0,0 +1,40 @@
+# frozen_string_literal: true
+
+require 'spec_helper'
+
+describe 'User paginates issue designs', :js do
+ include DesignManagementTestHelpers
+
+ let(:project) { create(:project_empty_repo, :public) }
+ let(:issue) { create(:issue, project: project) }
+
+ before do
+ enable_design_management
+
+ create_list(:design, 2, :with_file, issue: issue)
+
+ visit project_issue_path(project, issue)
+
+ click_link 'Designs'
+
+ wait_for_requests
+
+ find('.js-design-list-item', match: :first).click
+ end
+
+ it 'paginates to next design' do
+ expect(find('.js-previous-design')[:disabled]).to eq('true')
+
+ page.within(find('.js-design-header')) do
+ expect(page).to have_content('1 of 2')
+ end
+
+ find('.js-next-design').click
+
+ expect(find('.js-previous-design')[:disabled]).not_to eq('true')
+
+ page.within(find('.js-design-header')) do
+ expect(page).to have_content('2 of 2')
+ end
+ end
+end
diff --git a/spec/features/projects/issues/design_management/user_permissions_upload_spec.rb b/spec/features/projects/issues/design_management/user_permissions_upload_spec.rb
new file mode 100644
index 00000000000..2238e86a47f
--- /dev/null
+++ b/spec/features/projects/issues/design_management/user_permissions_upload_spec.rb
@@ -0,0 +1,24 @@
+# frozen_string_literal: true
+
+require 'spec_helper'
+
+describe 'User design permissions', :js do
+ include DesignManagementTestHelpers
+
+ let(:project) { create(:project_empty_repo, :public) }
+ let(:issue) { create(:issue, project: project) }
+
+ before do
+ enable_design_management
+
+ visit project_issue_path(project, issue)
+
+ click_link 'Designs'
+
+ wait_for_requests
+ end
+
+ it 'user does not have permissions to upload design' do
+ expect(page).not_to have_field('design_file')
+ end
+end
diff --git a/spec/features/projects/issues/design_management/user_uploads_designs_spec.rb b/spec/features/projects/issues/design_management/user_uploads_designs_spec.rb
new file mode 100644
index 00000000000..d160ab95a65
--- /dev/null
+++ b/spec/features/projects/issues/design_management/user_uploads_designs_spec.rb
@@ -0,0 +1,63 @@
+# frozen_string_literal: true
+
+require 'spec_helper'
+
+describe 'User uploads new design', :js do
+ include DesignManagementTestHelpers
+
+ let_it_be(:project) { create(:project_empty_repo, :public) }
+ let_it_be(:user) { project.owner }
+ let_it_be(:issue) { create(:issue, project: project) }
+
+ before do
+ sign_in(user)
+ end
+
+ context "when the feature is available" do
+ before do
+ enable_design_management
+
+ visit project_issue_path(project, issue)
+
+ click_link 'Designs'
+
+ wait_for_requests
+ end
+
+ it 'uploads designs' do
+ attach_file(:design_file, logo_fixture, make_visible: true)
+
+ expect(page).to have_selector('.js-design-list-item', count: 1)
+
+ within first('#designs-tab .js-design-list-item') do
+ expect(page).to have_content('dk.png')
+ end
+
+ attach_file(:design_file, gif_fixture, make_visible: true)
+
+ expect(page).to have_selector('.js-design-list-item', count: 2)
+ end
+ end
+
+ context 'when the feature is not available' do
+ before do
+ visit project_issue_path(project, issue)
+
+ click_link 'Designs'
+
+ wait_for_requests
+ end
+
+ it 'shows the message about requirements' do
+ expect(page).to have_content("To enable design management, you'll need to meet the requirements.")
+ end
+ end
+
+ def logo_fixture
+ Rails.root.join('spec', 'fixtures', 'dk.png')
+ end
+
+ def gif_fixture
+ Rails.root.join('spec', 'fixtures', 'banana_sample.gif')
+ end
+end
diff --git a/spec/features/projects/issues/design_management/user_views_design_images_spec.rb b/spec/features/projects/issues/design_management/user_views_design_images_spec.rb
new file mode 100644
index 00000000000..3d0f4df55c4
--- /dev/null
+++ b/spec/features/projects/issues/design_management/user_views_design_images_spec.rb
@@ -0,0 +1,41 @@
+# frozen_string_literal: true
+
+require 'spec_helper'
+
+describe 'Users views raw design image files' do
+ include DesignManagementTestHelpers
+
+ let_it_be(:project) { create(:project, :public) }
+ let_it_be(:issue) { create(:issue, project: project) }
+ let_it_be(:design) { create(:design, :with_file, issue: issue, versions_count: 2) }
+ let(:newest_version) { design.versions.ordered.first }
+ let(:oldest_version) { design.versions.ordered.last }
+
+ before do
+ enable_design_management
+ end
+
+ it 'serves the latest design version when no ref is given' do
+ visit project_design_management_designs_raw_image_path(design.project, design)
+
+ expect(response_headers[Gitlab::Workhorse::SEND_DATA_HEADER]).to eq(
+ workhorse_data_header_for_version(oldest_version.sha)
+ )
+ end
+
+ it 'serves the correct design version when a ref is given' do
+ visit project_design_management_designs_raw_image_path(design.project, design, oldest_version.sha)
+
+ expect(response_headers[Gitlab::Workhorse::SEND_DATA_HEADER]).to eq(
+ workhorse_data_header_for_version(oldest_version.sha)
+ )
+ end
+
+ private
+
+ def workhorse_data_header_for_version(ref)
+ blob = project.design_repository.blob_at(ref, design.full_path)
+
+ Gitlab::Workhorse.send_git_blob(project.design_repository, blob).last
+ end
+end
diff --git a/spec/features/projects/issues/design_management/user_views_design_spec.rb b/spec/features/projects/issues/design_management/user_views_design_spec.rb
new file mode 100644
index 00000000000..707049b0068
--- /dev/null
+++ b/spec/features/projects/issues/design_management/user_views_design_spec.rb
@@ -0,0 +1,29 @@
+# frozen_string_literal: true
+
+require 'spec_helper'
+
+describe 'User views issue designs', :js do
+ include DesignManagementTestHelpers
+
+ let_it_be(:project) { create(:project_empty_repo, :public) }
+ let_it_be(:issue) { create(:issue, project: project) }
+ let_it_be(:design) { create(:design, :with_file, issue: issue) }
+
+ before do
+ enable_design_management
+
+ visit project_issue_path(project, issue)
+
+ click_link 'Designs'
+ end
+
+ it 'opens design detail' do
+ click_link design.filename
+
+ page.within(find('.js-design-header')) do
+ expect(page).to have_content(design.filename)
+ end
+
+ expect(page).to have_selector('.js-design-image')
+ end
+end
diff --git a/spec/features/projects/issues/design_management/user_views_designs_spec.rb b/spec/features/projects/issues/design_management/user_views_designs_spec.rb
new file mode 100644
index 00000000000..a4fb7456922
--- /dev/null
+++ b/spec/features/projects/issues/design_management/user_views_designs_spec.rb
@@ -0,0 +1,47 @@
+# frozen_string_literal: true
+
+require 'spec_helper'
+
+describe 'User views issue designs', :js do
+ include DesignManagementTestHelpers
+
+ let_it_be(:project) { create(:project_empty_repo, :public) }
+ let_it_be(:issue) { create(:issue, project: project) }
+ let_it_be(:design) { create(:design, :with_file, issue: issue) }
+
+ before do
+ enable_design_management
+ end
+
+ context 'navigates from the issue view' do
+ before do
+ visit project_issue_path(project, issue)
+ click_link 'Designs'
+ wait_for_requests
+ end
+
+ it 'fetches list of designs' do
+ expect(page).to have_selector('.js-design-list-item', count: 1)
+ end
+ end
+
+ context 'navigates directly to the design collection view' do
+ before do
+ visit designs_project_issue_path(project, issue)
+ end
+
+ it 'expands the sidebar' do
+ expect(page).to have_selector('.layout-page.right-sidebar-expanded')
+ end
+ end
+
+ context 'navigates directly to the individual design view' do
+ before do
+ visit designs_project_issue_path(project, issue, vueroute: design.filename)
+ end
+
+ it 'sees the design' do
+ expect(page).to have_selector('.js-design-detail')
+ end
+ end
+end
diff --git a/spec/features/projects/issues/design_management/user_views_designs_with_svg_xss_spec.rb b/spec/features/projects/issues/design_management/user_views_designs_with_svg_xss_spec.rb
new file mode 100644
index 00000000000..a9e4aa899a7
--- /dev/null
+++ b/spec/features/projects/issues/design_management/user_views_designs_with_svg_xss_spec.rb
@@ -0,0 +1,55 @@
+# frozen_string_literal: true
+
+require 'spec_helper'
+
+describe 'User views an SVG design that contains XSS', :js do
+ include DesignManagementTestHelpers
+
+ let(:project) { create(:project_empty_repo, :public) }
+ let(:issue) { create(:issue, project: project) }
+ let(:file) { Rails.root.join('spec', 'fixtures', 'logo_sample.svg') }
+ let(:design) { create(:design, :with_file, filename: 'xss.svg', file: file, issue: issue) }
+
+ before do
+ enable_design_management
+
+ visit designs_project_issue_path(
+ project,
+ issue,
+ { vueroute: design.filename }
+ )
+
+ wait_for_requests
+ end
+
+ it 'has XSS within the SVG file' do
+ file_content = File.read(file)
+
+ expect(file_content).to include("<script>alert('FAIL')</script>")
+ end
+
+ it 'displays the SVG' do
+ expect(page).to have_selector("img.design-img[alt='xss.svg']", count: 1, visible: false)
+ end
+
+ it 'does not execute the JavaScript within the SVG' do
+ # The expectation is that we can call the capybara `page.dismiss_prompt`
+ # method to close a JavaScript alert prompt without a `Capybara::ModalNotFound`
+ # being raised.
+ run_expectation = -> {
+ page.dismiss_prompt(wait: 1)
+ }
+
+ # With the page loaded, there should be no alert modal
+ expect(run_expectation).to raise_error(
+ Capybara::ModalNotFound,
+ 'Unable to find modal dialog'
+ )
+
+ # Perform a negative control test of the above expectation.
+ # With an alert modal displaying, the modal should be dismissable.
+ execute_script('alert(true)')
+
+ expect(run_expectation).not_to raise_error
+ end
+end
diff --git a/spec/features/projects/members/list_spec.rb b/spec/features/projects/members/list_spec.rb
index 84000ef73ce..f404699b2f6 100644
--- a/spec/features/projects/members/list_spec.rb
+++ b/spec/features/projects/members/list_spec.rb
@@ -34,7 +34,7 @@ describe 'Project members list' do
expect(second_row).to be_blank
end
- it 'update user acess level', :js do
+ it 'update user access level', :js do
project.add_developer(user2)
visit_members_page
@@ -86,6 +86,23 @@ describe 'Project members list' do
end
end
+ context 'project bots' do
+ let(:project_bot) { create(:user, :project_bot, name: 'project_bot') }
+
+ before do
+ project.add_maintainer(project_bot)
+ end
+
+ it 'does not show form used to change roles and "Expiration date" or the remove user button' do
+ project_member = project.project_members.find_by(user_id: project_bot.id)
+
+ visit_members_page
+
+ expect(page).not_to have_selector("#edit_project_member_#{project_member.id}")
+ expect(page).not_to have_selector("#project_member_#{project_member.id} .btn-remove")
+ end
+ end
+
def add_user(id, role)
page.within ".invite-users-form" do
select2(id, from: "#user_ids", multiple: true)
diff --git a/spec/features/projects/navbar_spec.rb b/spec/features/projects/navbar_spec.rb
index 9dfdaf54a2f..1797ca8aa7d 100644
--- a/spec/features/projects/navbar_spec.rb
+++ b/spec/features/projects/navbar_spec.rb
@@ -12,6 +12,8 @@ describe 'Project navbar' do
let_it_be(:project) { create(:project, :repository) }
before do
+ stub_licensed_features(service_desk: false)
+
project.add_maintainer(user)
sign_in(user)
end
@@ -40,7 +42,7 @@ describe 'Project navbar' do
context 'when pages are available' do
before do
- allow(Gitlab.config.pages).to receive(:enabled).and_return(true)
+ stub_config(pages: { enabled: true })
insert_after_sub_nav_item(
_('Operations'),
@@ -53,4 +55,21 @@ describe 'Project navbar' do
it_behaves_like 'verified navigation bar'
end
+
+ context 'when container registry is available' do
+ before do
+ stub_config(registry: { enabled: true })
+
+ insert_after_nav_item(
+ _('Operations'),
+ new_nav_item: {
+ nav_item: _('Packages & Registries'),
+ nav_sub_items: [_('Container Registry')]
+ }
+ )
+ visit project_path(project)
+ end
+
+ it_behaves_like 'verified navigation bar'
+ end
end
diff --git a/spec/features/projects/pipelines/pipeline_spec.rb b/spec/features/projects/pipelines/pipeline_spec.rb
index e8846b5b617..de81547887b 100644
--- a/spec/features/projects/pipelines/pipeline_spec.rb
+++ b/spec/features/projects/pipelines/pipeline_spec.rb
@@ -19,32 +19,32 @@ describe 'Pipeline', :js do
shared_context 'pipeline builds' do
let!(:build_passed) do
create(:ci_build, :success,
- pipeline: pipeline, stage: 'build', name: 'build')
+ pipeline: pipeline, stage: 'build', stage_idx: 0, name: 'build')
end
let!(:build_failed) do
create(:ci_build, :failed,
- pipeline: pipeline, stage: 'test', name: 'test')
+ pipeline: pipeline, stage: 'test', stage_idx: 1, name: 'test')
end
let!(:build_preparing) do
create(:ci_build, :preparing,
- pipeline: pipeline, stage: 'deploy', name: 'prepare')
+ pipeline: pipeline, stage: 'deploy', stage_idx: 2, name: 'prepare')
end
let!(:build_running) do
create(:ci_build, :running,
- pipeline: pipeline, stage: 'deploy', name: 'deploy')
+ pipeline: pipeline, stage: 'deploy', stage_idx: 3, name: 'deploy')
end
let!(:build_manual) do
create(:ci_build, :manual,
- pipeline: pipeline, stage: 'deploy', name: 'manual-build')
+ pipeline: pipeline, stage: 'deploy', stage_idx: 3, name: 'manual-build')
end
let!(:build_scheduled) do
create(:ci_build, :scheduled,
- pipeline: pipeline, stage: 'deploy', name: 'delayed-job')
+ pipeline: pipeline, stage: 'deploy', stage_idx: 3, name: 'delayed-job')
end
let!(:build_external) do
@@ -307,9 +307,12 @@ describe 'Pipeline', :js do
context 'when the pipeline has manual stage' do
before do
- create(:ci_build, :manual, pipeline: pipeline, stage: 'publish', name: 'CentOS')
- create(:ci_build, :manual, pipeline: pipeline, stage: 'publish', name: 'Debian')
- create(:ci_build, :manual, pipeline: pipeline, stage: 'publish', name: 'OpenSUDE')
+ create(:ci_build, :manual, pipeline: pipeline, stage_idx: 10, stage: 'publish', name: 'CentOS')
+ create(:ci_build, :manual, pipeline: pipeline, stage_idx: 10, stage: 'publish', name: 'Debian')
+ create(:ci_build, :manual, pipeline: pipeline, stage_idx: 10, stage: 'publish', name: 'OpenSUDE')
+
+ # force to update stages statuses
+ Ci::ProcessPipelineService.new(pipeline).execute
visit_pipeline
end
@@ -324,9 +327,10 @@ describe 'Pipeline', :js do
visit_pipeline
end
- it 'shows Pipeline, Jobs and Failed Jobs tabs with link' do
+ it 'shows Pipeline, Jobs, DAG and Failed Jobs tabs with link' do
expect(page).to have_link('Pipeline')
expect(page).to have_link('Jobs')
+ expect(page).to have_link('DAG')
expect(page).to have_link('Failed Jobs')
end
@@ -611,6 +615,20 @@ describe 'Pipeline', :js do
end
end
end
+
+ context 'when FF dag_pipeline_tab is disabled' do
+ before do
+ stub_feature_flags(dag_pipeline_tab: false)
+ visit_pipeline
+ end
+
+ it 'does not show DAG link' do
+ expect(page).to have_link('Pipeline')
+ expect(page).to have_link('Jobs')
+ expect(page).not_to have_link('DAG')
+ expect(page).to have_link('Failed Jobs')
+ end
+ end
end
context 'when user does not have access to read jobs' do
@@ -862,9 +880,10 @@ describe 'Pipeline', :js do
end
context 'page tabs' do
- it 'shows Pipeline and Jobs tabs with link' do
+ it 'shows Pipeline, Jobs and DAG tabs with link' do
expect(page).to have_link('Pipeline')
expect(page).to have_link('Jobs')
+ expect(page).to have_link('DAG')
end
it 'shows counter in Jobs tab' do
@@ -1054,6 +1073,37 @@ describe 'Pipeline', :js do
end
end
+ describe 'GET /:project/pipelines/:id/dag' do
+ include_context 'pipeline builds'
+
+ let(:project) { create(:project, :repository) }
+ let(:pipeline) { create(:ci_pipeline, project: project, ref: 'master', sha: project.commit.id) }
+
+ before do
+ visit dag_project_pipeline_path(project, pipeline)
+ end
+
+ it 'shows DAG tab pane as active' do
+ expect(page).to have_css('#js-tab-dag.active', visible: false)
+ end
+
+ context 'page tabs' do
+ it 'shows Pipeline, Jobs and DAG tabs with link' do
+ expect(page).to have_link('Pipeline')
+ expect(page).to have_link('Jobs')
+ expect(page).to have_link('DAG')
+ end
+
+ it 'shows counter in Jobs tab' do
+ expect(page.find('.js-builds-counter').text).to eq(pipeline.total_size.to_s)
+ end
+
+ it 'shows DAG tab as active' do
+ expect(page).to have_css('li.js-dag-tab-link .active')
+ end
+ end
+ end
+
context 'when user sees pipeline flags in a pipeline detail page' do
let(:project) { create(:project, :repository) }
diff --git a/spec/features/projects/serverless/functions_spec.rb b/spec/features/projects/serverless/functions_spec.rb
index e494a0e9626..4c89af29339 100644
--- a/spec/features/projects/serverless/functions_spec.rb
+++ b/spec/features/projects/serverless/functions_spec.rb
@@ -40,7 +40,7 @@ describe 'Functions', :js do
it_behaves_like "it's missing knative installation"
end
- context 'when the user has a cluster and knative installed and visits the serverless page' do
+ context 'when the user has a cluster and knative installed and visits the serverless page', :kubeclient do
let(:cluster) { create(:cluster, :project, :provided_by_gcp, projects: [project]) }
let(:service) { cluster.platform_kubernetes }
let(:environment) { create(:environment, project: project) }
diff --git a/spec/features/projects/services/disable_triggers_spec.rb b/spec/features/projects/services/disable_triggers_spec.rb
index 2785f74bee2..d07abb94208 100644
--- a/spec/features/projects/services/disable_triggers_spec.rb
+++ b/spec/features/projects/services/disable_triggers_spec.rb
@@ -3,16 +3,12 @@
require 'spec_helper'
describe 'Disable individual triggers' do
- let(:project) { create(:project) }
- let(:user) { project.owner }
+ include_context 'project service activation'
+
let(:checkbox_selector) { 'input[type=checkbox][id$=_events]' }
before do
- sign_in(user)
-
- visit(project_settings_integrations_path(project))
-
- click_link(service_name)
+ visit_project_integration(service_name)
end
context 'service has multiple supported events' do
diff --git a/spec/features/projects/services/prometheus_external_alerts_spec.rb b/spec/features/projects/services/prometheus_external_alerts_spec.rb
index e33b2d9a75e..1a706f20352 100644
--- a/spec/features/projects/services/prometheus_external_alerts_spec.rb
+++ b/spec/features/projects/services/prometheus_external_alerts_spec.rb
@@ -3,26 +3,18 @@
require 'spec_helper'
describe 'Prometheus external alerts', :js do
- let(:project) { create(:project) }
- let(:user) { create(:user) }
+ include_context 'project service activation'
let(:alerts_section_selector) { '.js-prometheus-alerts' }
let(:alerts_section) { page.find(alerts_section_selector) }
- before do
- sign_in(user)
- project.add_maintainer(user)
-
- visit_edit_service
- end
-
context 'with manual configuration' do
before do
create(:prometheus_service, project: project, api_url: 'http://prometheus.example.com', manual_configuration: '1', active: true)
end
it 'shows the Alerts section' do
- visit_edit_service
+ visit_project_integration('Prometheus')
expect(alerts_section).to have_content('Alerts')
expect(alerts_section).to have_content('Receive alerts from manually configured Prometheus servers.')
@@ -33,16 +25,10 @@ describe 'Prometheus external alerts', :js do
context 'with no configuration' do
it 'does not show the Alerts section' do
+ visit_project_integration('Prometheus')
wait_for_requests
expect(page).not_to have_css(alerts_section_selector)
end
end
-
- private
-
- def visit_edit_service
- visit(project_settings_integrations_path(project))
- click_link('Prometheus')
- end
end
diff --git a/spec/features/projects/services/user_activates_issue_tracker_spec.rb b/spec/features/projects/services/user_activates_issue_tracker_spec.rb
index 4f3fb6ac3bf..3c5005d0c0c 100644
--- a/spec/features/projects/services/user_activates_issue_tracker_spec.rb
+++ b/spec/features/projects/services/user_activates_issue_tracker_spec.rb
@@ -3,29 +3,17 @@
require 'spec_helper'
describe 'User activates issue tracker', :js do
- let(:user) { create(:user) }
- let(:project) { create(:project) }
+ include_context 'project service activation'
let(:url) { 'http://tracker.example.com' }
- def fill_short_form(disabled: false)
- find('input[name="service[active]"] + button').click if disabled
+ def fill_form(disable: false, skip_new_issue_url: false)
+ click_active_toggle if disable
fill_in 'service_project_url', with: url
fill_in 'service_issues_url', with: "#{url}/:id"
- end
-
- def fill_full_form(disabled: false)
- fill_short_form(disabled: disabled)
- fill_in 'service_new_issue_url', with: url
- end
-
- before do
- project.add_maintainer(user)
- sign_in(user)
-
- visit project_settings_integrations_path(project)
+ fill_in 'service_new_issue_url', with: url unless skip_new_issue_url
end
shared_examples 'external issue tracker activation' do |tracker:, skip_new_issue_url: false|
@@ -34,16 +22,10 @@ describe 'User activates issue tracker', :js do
before do
stub_request(:head, url).to_return(headers: { 'Content-Type' => 'application/json' })
- click_link(tracker)
-
- if skip_new_issue_url
- fill_short_form
- else
- fill_full_form
- end
+ visit_project_integration(tracker)
+ fill_form(skip_new_issue_url: skip_new_issue_url)
- click_button('Test settings and save changes')
- wait_for_requests
+ click_test_integration
end
it 'activates the service' do
@@ -62,22 +44,10 @@ describe 'User activates issue tracker', :js do
it 'activates the service' do
stub_request(:head, url).to_raise(Gitlab::HTTP::Error)
- click_link(tracker)
+ visit_project_integration(tracker)
+ fill_form(skip_new_issue_url: skip_new_issue_url)
- if skip_new_issue_url
- fill_short_form
- else
- fill_full_form
- end
-
- click_button('Test settings and save changes')
- wait_for_requests
-
- expect(find('.flash-container-page')).to have_content 'Test failed.'
- expect(find('.flash-container-page')).to have_content 'Save anyway'
-
- find('.flash-alert .flash-action').click
- wait_for_requests
+ click_test_then_save_integration
expect(page).to have_content("#{tracker} activated.")
expect(current_path).to eq(project_settings_integrations_path(project))
@@ -87,13 +57,8 @@ describe 'User activates issue tracker', :js do
describe 'user disables the service' do
before do
- click_link(tracker)
-
- if skip_new_issue_url
- fill_short_form(disabled: true)
- else
- fill_full_form(disabled: true)
- end
+ visit_project_integration(tracker)
+ fill_form(disable: true, skip_new_issue_url: skip_new_issue_url)
click_button('Save changes')
end
diff --git a/spec/features/projects/services/user_activates_jira_spec.rb b/spec/features/projects/services/user_activates_jira_spec.rb
index fb9628032b2..a14dbf9c14d 100644
--- a/spec/features/projects/services/user_activates_jira_spec.rb
+++ b/spec/features/projects/services/user_activates_jira_spec.rb
@@ -3,14 +3,13 @@
require 'spec_helper'
describe 'User activates Jira', :js do
- let(:user) { create(:user) }
- let(:project) { create(:project) }
+ include_context 'project service activation'
let(:url) { 'http://jira.example.com' }
let(:test_url) { 'http://jira.example.com/rest/api/2/serverInfo' }
- def fill_form(disabled: false)
- find('input[name="service[active]"] + button').click if disabled
+ def fill_form(disable: false)
+ click_active_toggle if disable
fill_in 'service_url', with: url
fill_in 'service_username', with: 'username'
@@ -18,23 +17,15 @@ describe 'User activates Jira', :js do
fill_in 'service_jira_issue_transition_id', with: '25'
end
- before do
- project.add_maintainer(user)
- sign_in(user)
-
- visit project_settings_integrations_path(project)
- end
-
describe 'user sets and activates Jira Service' do
context 'when Jira connection test succeeds' do
before do
server_info = { key: 'value' }.to_json
- WebMock.stub_request(:get, test_url).with(basic_auth: %w(username password)).to_return(body: server_info)
+ stub_request(:get, test_url).with(basic_auth: %w(username password)).to_return(body: server_info)
- click_link('Jira')
+ visit_project_integration('Jira')
fill_form
- click_button('Test settings and save changes')
- wait_for_requests
+ click_test_integration
end
it 'activates the Jira service' do
@@ -51,10 +42,10 @@ describe 'User activates Jira', :js do
context 'when Jira connection test fails' do
it 'shows errors when some required fields are not filled in' do
- click_link('Jira')
+ visit_project_integration('Jira')
fill_in 'service_password', with: 'password'
- click_button('Test settings and save changes')
+ click_test_integration
page.within('.service-settings') do
expect(page).to have_content('This field is required.')
@@ -62,19 +53,12 @@ describe 'User activates Jira', :js do
end
it 'activates the Jira service' do
- WebMock.stub_request(:get, test_url).with(basic_auth: %w(username password))
+ stub_request(:get, test_url).with(basic_auth: %w(username password))
.to_raise(JIRA::HTTPError.new(double(message: 'message')))
- click_link('Jira')
+ visit_project_integration('Jira')
fill_form
- click_button('Test settings and save changes')
- wait_for_requests
-
- expect(find('.flash-container-page')).to have_content 'Test failed. message'
- expect(find('.flash-container-page')).to have_content 'Save anyway'
-
- find('.flash-alert .flash-action').click
- wait_for_requests
+ click_test_then_save_integration
expect(page).to have_content('Jira activated.')
expect(current_path).to eq(project_settings_integrations_path(project))
@@ -84,8 +68,8 @@ describe 'User activates Jira', :js do
describe 'user disables the Jira Service' do
before do
- click_link('Jira')
- fill_form(disabled: true)
+ visit_project_integration('Jira')
+ fill_form(disable: true)
click_button('Save changes')
end
diff --git a/spec/features/projects/services/user_activates_mattermost_slash_command_spec.rb b/spec/features/projects/services/user_activates_mattermost_slash_command_spec.rb
index ac9cb00be84..c6825ee663a 100644
--- a/spec/features/projects/services/user_activates_mattermost_slash_command_spec.rb
+++ b/spec/features/projects/services/user_activates_mattermost_slash_command_spec.rb
@@ -3,158 +3,158 @@
require 'spec_helper'
describe 'Set up Mattermost slash commands', :js do
- let(:user) { create(:user) }
- let(:project) { create(:project) }
- let(:mattermost_enabled) { true }
-
- before do
- stub_mattermost_setting(enabled: mattermost_enabled)
- project.add_maintainer(user)
- sign_in(user)
- visit edit_project_service_path(project, :mattermost_slash_commands)
- end
-
describe 'user visits the mattermost slash command config page' do
- it 'shows a help message' do
- expect(page).to have_content("This service allows users to perform common")
+ include_context 'project service activation'
+
+ before do
+ stub_mattermost_setting(enabled: mattermost_enabled)
+ visit_project_integration('Mattermost slash commands')
end
- it 'shows a token placeholder' do
- token_placeholder = find_field('service_token')['placeholder']
+ context 'mattermost service is enabled' do
+ let(:mattermost_enabled) { true }
- expect(token_placeholder).to eq('XXxxXXxxXXxxXXxxXXxxXXxx')
- end
+ it 'shows a help message' do
+ expect(page).to have_content("This service allows users to perform common")
+ end
- it 'redirects to the integrations page after saving but not activating' do
- token = ('a'..'z').to_a.join
+ it 'shows a token placeholder' do
+ token_placeholder = find_field('service_token')['placeholder']
- fill_in 'service_token', with: token
- find('input[name="service[active]"] + button').click
- click_on 'Save changes'
+ expect(token_placeholder).to eq('XXxxXXxxXXxxXXxxXXxxXXxx')
+ end
- expect(current_path).to eq(project_settings_integrations_path(project))
- expect(page).to have_content('Mattermost slash commands settings saved, but not activated.')
- end
+ it 'redirects to the integrations page after saving but not activating' do
+ token = ('a'..'z').to_a.join
- it 'redirects to the integrations page after activating' do
- token = ('a'..'z').to_a.join
+ fill_in 'service_token', with: token
+ click_active_toggle
+ click_on 'Save changes'
- fill_in 'service_token', with: token
- click_on 'Save changes'
+ expect(current_path).to eq(project_settings_integrations_path(project))
+ expect(page).to have_content('Mattermost slash commands settings saved, but not activated.')
+ end
- expect(current_path).to eq(project_settings_integrations_path(project))
- expect(page).to have_content('Mattermost slash commands activated.')
- end
+ it 'redirects to the integrations page after activating' do
+ token = ('a'..'z').to_a.join
- it 'shows the add to mattermost button' do
- expect(page).to have_link('Add to Mattermost')
- end
+ fill_in 'service_token', with: token
+ click_on 'Save changes'
- it 'shows an explanation if user is a member of no teams' do
- stub_teams(count: 0)
+ expect(current_path).to eq(project_settings_integrations_path(project))
+ expect(page).to have_content('Mattermost slash commands activated.')
+ end
- click_link 'Add to Mattermost'
+ it 'shows the add to mattermost button' do
+ expect(page).to have_link('Add to Mattermost')
+ end
- expect(page).to have_content('You aren’t a member of any team on the Mattermost instance')
- expect(page).to have_link('join a team', href: "#{Gitlab.config.mattermost.host}/select_team")
- end
+ it 'shows an explanation if user is a member of no teams' do
+ stub_teams(count: 0)
- it 'shows an explanation if user is a member of 1 team' do
- stub_teams(count: 1)
+ click_link 'Add to Mattermost'
- click_link 'Add to Mattermost'
+ expect(page).to have_content('You aren’t a member of any team on the Mattermost instance')
+ expect(page).to have_link('join a team', href: "#{Gitlab.config.mattermost.host}/select_team")
+ end
- expect(page).to have_content('The team where the slash commands will be used in')
- expect(page).to have_content('This is the only available team that you are a member of.')
- end
+ it 'shows an explanation if user is a member of 1 team' do
+ stub_teams(count: 1)
- it 'shows a disabled prefilled select if user is a member of 1 team' do
- teams = stub_teams(count: 1)
+ click_link 'Add to Mattermost'
- click_link 'Add to Mattermost'
+ expect(page).to have_content('The team where the slash commands will be used in')
+ expect(page).to have_content('This is the only available team that you are a member of.')
+ end
- team_name = teams.first['display_name']
- select_element = find('#mattermost_team_id')
- selected_option = select_element.find('option[selected]')
+ it 'shows a disabled prefilled select if user is a member of 1 team' do
+ teams = stub_teams(count: 1)
- expect(select_element['disabled']).to eq("true")
- expect(selected_option).to have_content(team_name.to_s)
- end
+ click_link 'Add to Mattermost'
- it 'has a hidden input for the prefilled value if user is a member of 1 team' do
- teams = stub_teams(count: 1)
+ team_name = teams.first['display_name']
+ select_element = find('#mattermost_team_id')
+ selected_option = select_element.find('option[selected]')
- click_link 'Add to Mattermost'
+ expect(select_element['disabled']).to eq("true")
+ expect(selected_option).to have_content(team_name.to_s)
+ end
- expect(find('input#mattermost_team_id', visible: false).value).to eq(teams.first['id'])
- end
+ it 'has a hidden input for the prefilled value if user is a member of 1 team' do
+ teams = stub_teams(count: 1)
- it 'shows an explanation user is a member of multiple teams' do
- stub_teams(count: 2)
+ click_link 'Add to Mattermost'
- click_link 'Add to Mattermost'
+ expect(find('input#mattermost_team_id', visible: false).value).to eq(teams.first['id'])
+ end
- expect(page).to have_content('Select the team where the slash commands will be used in')
- expect(page).to have_content('The list shows all available teams that you are a member of.')
- end
+ it 'shows an explanation user is a member of multiple teams' do
+ stub_teams(count: 2)
- it 'shows a select with team options user is a member of multiple teams' do
- stub_teams(count: 2)
+ click_link 'Add to Mattermost'
- click_link 'Add to Mattermost'
+ expect(page).to have_content('Select the team where the slash commands will be used in')
+ expect(page).to have_content('The list shows all available teams that you are a member of.')
+ end
- select_element = find('#mattermost_team_id')
+ it 'shows a select with team options user is a member of multiple teams' do
+ stub_teams(count: 2)
- expect(select_element['disabled']).to be_falsey
- expect(select_element.all('option').count).to eq(3)
- end
+ click_link 'Add to Mattermost'
- it 'shows an error alert with the error message if there is an error requesting teams' do
- allow_any_instance_of(MattermostSlashCommandsService).to receive(:list_teams) { [[], 'test mattermost error message'] }
+ select_element = find('#mattermost_team_id')
- click_link 'Add to Mattermost'
+ expect(select_element['disabled']).to be_falsey
+ expect(select_element.all('option').count).to eq(3)
+ end
- expect(page).to have_selector('.alert')
- expect(page).to have_content('test mattermost error message')
- end
+ it 'shows an error alert with the error message if there is an error requesting teams' do
+ allow_any_instance_of(MattermostSlashCommandsService).to receive(:list_teams) { [[], 'test mattermost error message'] }
- it 'enables the submit button if the required fields are provided', :js do
- stub_teams(count: 1)
+ click_link 'Add to Mattermost'
- click_link 'Add to Mattermost'
+ expect(page).to have_selector('.alert')
+ expect(page).to have_content('test mattermost error message')
+ end
- expect(find('input[type="submit"]')['disabled']).not_to eq("true")
- end
+ it 'enables the submit button if the required fields are provided', :js do
+ stub_teams(count: 1)
- it 'disables the submit button if the required fields are not provided', :js do
- stub_teams(count: 1)
+ click_link 'Add to Mattermost'
- click_link 'Add to Mattermost'
+ expect(find('input[type="submit"]')['disabled']).not_to eq("true")
+ end
- fill_in('mattermost_trigger', with: '')
+ it 'disables the submit button if the required fields are not provided', :js do
+ stub_teams(count: 1)
- expect(find('input[type="submit"]')['disabled']).to eq("true")
- end
+ click_link 'Add to Mattermost'
- def stub_teams(count: 0)
- teams = create_teams(count)
+ fill_in('mattermost_trigger', with: '')
- allow_any_instance_of(MattermostSlashCommandsService).to receive(:list_teams) { [teams, nil] }
+ expect(find('input[type="submit"]')['disabled']).to eq("true")
+ end
- teams
- end
+ def stub_teams(count: 0)
+ teams = create_teams(count)
- def create_teams(count = 0)
- teams = []
+ allow_any_instance_of(MattermostSlashCommandsService).to receive(:list_teams) { [teams, nil] }
- count.times do |i|
- teams.push({ "id" => "x#{i}", "display_name" => "x#{i}-name" })
+ teams
end
- teams
+ def create_teams(count = 0)
+ teams = []
+
+ count.times do |i|
+ teams.push({ "id" => "x#{i}", "display_name" => "x#{i}-name" })
+ end
+
+ teams
+ end
end
- describe 'mattermost service is not enabled' do
+ context 'mattermost service is not enabled' do
let(:mattermost_enabled) { false }
it 'shows the correct trigger url' do
diff --git a/spec/features/projects/services/user_activates_slack_slash_command_spec.rb b/spec/features/projects/services/user_activates_slack_slash_command_spec.rb
index 4ce1acd9377..05f1a0c6b17 100644
--- a/spec/features/projects/services/user_activates_slack_slash_command_spec.rb
+++ b/spec/features/projects/services/user_activates_slack_slash_command_spec.rb
@@ -3,13 +3,10 @@
require 'spec_helper'
describe 'Slack slash commands' do
- let(:user) { create(:user) }
- let(:project) { create(:project) }
+ include_context 'project service activation'
before do
- project.add_maintainer(user)
- sign_in(user)
- visit edit_project_service_path(project, :slack_slash_commands)
+ visit_project_integration('Slack slash commands')
end
it 'shows a token placeholder' do
@@ -24,7 +21,7 @@ describe 'Slack slash commands' do
it 'redirects to the integrations page after saving but not activating', :js do
fill_in 'service_token', with: 'token'
- find('input[name="service[active]"] + button').click
+ click_active_toggle
click_on 'Save'
expect(current_path).to eq(project_settings_integrations_path(project))
diff --git a/spec/features/projects/services/user_activates_youtrack_spec.rb b/spec/features/projects/services/user_activates_youtrack_spec.rb
deleted file mode 100644
index 26734766ff0..00000000000
--- a/spec/features/projects/services/user_activates_youtrack_spec.rb
+++ /dev/null
@@ -1,91 +0,0 @@
-# frozen_string_literal: true
-
-require 'spec_helper'
-
-describe 'User activates issue tracker', :js do
- let(:user) { create(:user) }
- let(:project) { create(:project) }
-
- let(:url) { 'http://tracker.example.com' }
-
- def fill_form(disabled: false)
- find('input[name="service[active]"] + button').click if disabled
-
- fill_in 'service_project_url', with: url
- fill_in 'service_issues_url', with: "#{url}/:id"
- end
-
- before do
- project.add_maintainer(user)
- sign_in(user)
-
- visit project_settings_integrations_path(project)
- end
-
- shared_examples 'external issue tracker activation' do |tracker:|
- describe 'user sets and activates the Service' do
- context 'when the connection test succeeds' do
- before do
- stub_request(:head, url).to_return(headers: { 'Content-Type' => 'application/json' })
-
- click_link(tracker)
- fill_form
- click_button('Test settings and save changes')
- wait_for_requests
- end
-
- it 'activates the service' do
- expect(page).to have_content("#{tracker} activated.")
- expect(current_path).to eq(project_settings_integrations_path(project))
- end
-
- it 'shows the link in the menu' do
- page.within('.nav-sidebar') do
- expect(page).to have_link(tracker, href: url)
- end
- end
- end
-
- context 'when the connection test fails' do
- it 'activates the service' do
- stub_request(:head, url).to_raise(Gitlab::HTTP::Error)
-
- click_link(tracker)
- fill_form
- click_button('Test settings and save changes')
- wait_for_requests
-
- expect(find('.flash-container-page')).to have_content 'Test failed.'
- expect(find('.flash-container-page')).to have_content 'Save anyway'
-
- find('.flash-alert .flash-action').click
- wait_for_requests
-
- expect(page).to have_content("#{tracker} activated.")
- expect(current_path).to eq(project_settings_integrations_path(project))
- end
- end
- end
-
- describe 'user disables the service' do
- before do
- click_link(tracker)
- fill_form(disabled: true)
- click_button('Save changes')
- end
-
- it 'saves but does not activate the service' do
- expect(page).to have_content("#{tracker} settings saved, but not activated.")
- expect(current_path).to eq(project_settings_integrations_path(project))
- end
-
- it 'does not show the external tracker link in the menu' do
- page.within('.nav-sidebar') do
- expect(page).not_to have_link(tracker, href: url)
- end
- end
- end
- end
-
- it_behaves_like 'external issue tracker activation', tracker: 'YouTrack'
-end
diff --git a/spec/features/projects/settings/access_tokens_spec.rb b/spec/features/projects/settings/access_tokens_spec.rb
new file mode 100644
index 00000000000..9a8a8e38164
--- /dev/null
+++ b/spec/features/projects/settings/access_tokens_spec.rb
@@ -0,0 +1,93 @@
+# frozen_string_literal: true
+
+require 'spec_helper'
+
+describe 'Project > Settings > Access Tokens', :js do
+ let_it_be(:user) { create(:user) }
+ let_it_be(:bot_user) { create(:user, :project_bot) }
+ let_it_be(:project) { create(:project) }
+
+ before_all do
+ project.add_maintainer(user)
+ end
+
+ before do
+ sign_in(user)
+ end
+
+ def create_project_access_token
+ project.add_maintainer(bot_user)
+
+ create(:personal_access_token, user: bot_user)
+ end
+
+ def active_project_access_tokens
+ find('.table.active-tokens')
+ end
+
+ def no_project_access_tokens_message
+ find('.settings-message')
+ end
+
+ def created_project_access_token
+ find('#created-personal-access-token').value
+ end
+
+ describe 'token creation' do
+ it 'allows creation of a project access token' do
+ name = 'My project access token'
+
+ visit project_settings_access_tokens_path(project)
+ fill_in 'Name', with: name
+
+ # Set date to 1st of next month
+ find_field('Expires at').click
+ find('.pika-next').click
+ click_on '1'
+
+ # Scopes
+ check 'api'
+ check 'read_api'
+
+ click_on 'Create project access token'
+
+ expect(active_project_access_tokens).to have_text(name)
+ expect(active_project_access_tokens).to have_text('In')
+ expect(active_project_access_tokens).to have_text('api')
+ expect(active_project_access_tokens).to have_text('read_api')
+ expect(created_project_access_token).not_to be_empty
+ end
+ end
+
+ describe 'active tokens' do
+ let!(:project_access_token) { create_project_access_token }
+
+ it 'shows active project access tokens' do
+ visit project_settings_access_tokens_path(project)
+
+ expect(active_project_access_tokens).to have_text(project_access_token.name)
+ end
+ end
+
+ describe 'inactive tokens' do
+ let!(:project_access_token) { create_project_access_token }
+
+ no_active_tokens_text = 'This project has no active access tokens.'
+
+ it 'allows revocation of an active token' do
+ visit project_settings_access_tokens_path(project)
+ accept_confirm { click_on 'Revoke' }
+
+ expect(page).to have_selector('.settings-message')
+ expect(no_project_access_tokens_message).to have_text(no_active_tokens_text)
+ end
+
+ it 'removes expired tokens from active section' do
+ project_access_token.update(expires_at: 5.days.ago)
+ visit project_settings_access_tokens_path(project)
+
+ expect(page).to have_selector('.settings-message')
+ expect(no_project_access_tokens_message).to have_text(no_active_tokens_text)
+ end
+ end
+end
diff --git a/spec/features/projects/settings/operations_settings_spec.rb b/spec/features/projects/settings/operations_settings_spec.rb
index 3c9102431e8..752353cf2f5 100644
--- a/spec/features/projects/settings/operations_settings_spec.rb
+++ b/spec/features/projects/settings/operations_settings_spec.rb
@@ -76,7 +76,7 @@ describe 'Projects > Settings > For a forked project', :js do
context 'success path' do
let(:projects_sample_response) do
Gitlab::Utils.deep_indifferent_access(
- JSON.parse(fixture_file('sentry/list_projects_sample_response.json'))
+ Gitlab::Json.parse(fixture_file('sentry/list_projects_sample_response.json'))
)
end
diff --git a/spec/features/projects/settings/project_settings_spec.rb b/spec/features/projects/settings/project_settings_spec.rb
index 9fc91550667..171c7920878 100644
--- a/spec/features/projects/settings/project_settings_spec.rb
+++ b/spec/features/projects/settings/project_settings_spec.rb
@@ -54,6 +54,36 @@ describe 'Projects settings' do
end
end
+ context 'default award emojis', :js do
+ it 'shows award emojis by default' do
+ visit edit_project_path(project)
+
+ default_award_emojis_input = find('input[name="project[project_setting_attributes][show_default_award_emojis]"]', visible: :hidden)
+
+ expect(default_award_emojis_input.value).to eq('true')
+ end
+
+ it 'disables award emojis when the checkbox is toggled off' do
+ visit edit_project_path(project)
+
+ default_award_emojis_input = find('input[name="project[project_setting_attributes][show_default_award_emojis]"]', visible: :hidden)
+ default_award_emojis_checkbox = find('input[name="project[project_setting_attributes][show_default_award_emojis]"][type=checkbox]')
+
+ expect(default_award_emojis_input.value).to eq('true')
+
+ default_award_emojis_checkbox.click
+
+ expect(default_award_emojis_input.value).to eq('false')
+
+ page.within('.sharing-permissions') do
+ find('input[value="Save changes"]').click
+ end
+ wait_for_requests
+
+ expect(default_award_emojis_input.value).to eq('false')
+ end
+ end
+
def expect_toggle_state(state)
is_collapsed = state == :collapsed
diff --git a/spec/features/projects/settings/registry_settings_spec.rb b/spec/features/projects/settings/registry_settings_spec.rb
index 74d3544ce92..ba92e8bc516 100644
--- a/spec/features/projects/settings/registry_settings_spec.rb
+++ b/spec/features/projects/settings/registry_settings_spec.rb
@@ -29,7 +29,7 @@ describe 'Project > Settings > CI/CD > Container registry tag expiration policy'
select('7 days until tags are automatically removed', from: 'Expiration interval:')
select('Every day', from: 'Expiration schedule:')
select('50 tags per image name', from: 'Number of tags to retain:')
- fill_in('Docker tags with names matching this regex pattern will expire:', with: '*-production')
+ fill_in('Tags with names matching this regex pattern will expire:', with: '*-production')
end
submit_button = find('.card-footer .btn.btn-success')
expect(submit_button).not_to be_disabled
diff --git a/spec/features/projects/settings/repository_settings_spec.rb b/spec/features/projects/settings/repository_settings_spec.rb
index 2fb6c71384f..b8baaa3e963 100644
--- a/spec/features/projects/settings/repository_settings_spec.rb
+++ b/spec/features/projects/settings/repository_settings_spec.rb
@@ -30,7 +30,7 @@ describe 'Projects > Settings > Repository settings' do
before do
stub_container_registry_config(enabled: true)
- stub_feature_flags(ajax_new_deploy_token: { enabled: false, thing: project })
+ stub_feature_flags(ajax_new_deploy_token: project)
visit project_settings_repository_path(project)
end
@@ -222,20 +222,6 @@ describe 'Projects > Settings > Repository settings' do
end
end
- # Removal: https://gitlab.com/gitlab-org/gitlab/-/issues/208828
- context 'with the `keep_divergent_refs` feature flag disabled' do
- before do
- stub_feature_flags(keep_divergent_refs: { enabled: false, thing: project })
- end
-
- it 'hides the "Keep divergent refs" option' do
- visit project_settings_repository_path(project)
-
- expect(page).not_to have_selector('#keep_divergent_refs')
- expect(page).not_to have_text('Keep divergent refs')
- end
- end
-
context 'repository cleanup settings' do
let(:object_map_file) { Rails.root.join('spec', 'fixtures', 'bfg_object_map.txt') }
diff --git a/spec/features/projects/settings/user_interacts_with_deploy_keys_spec.rb b/spec/features/projects/settings/user_interacts_with_deploy_keys_spec.rb
index cd9299150b2..45a16fda2cb 100644
--- a/spec/features/projects/settings/user_interacts_with_deploy_keys_spec.rb
+++ b/spec/features/projects/settings/user_interacts_with_deploy_keys_spec.rb
@@ -88,18 +88,18 @@ describe "User interacts with deploy keys", :js do
end
it "adds new key" do
- DEPLOY_KEY_TITLE = attributes_for(:key)[:title]
- DEPLOY_KEY_BODY = attributes_for(:key)[:key]
+ deploy_key_title = attributes_for(:key)[:title]
+ deploy_key_body = attributes_for(:key)[:key]
- fill_in("deploy_key_title", with: DEPLOY_KEY_TITLE)
- fill_in("deploy_key_key", with: DEPLOY_KEY_BODY)
+ fill_in("deploy_key_title", with: deploy_key_title)
+ fill_in("deploy_key_key", with: deploy_key_body)
click_button("Add key")
expect(current_path).to eq(project_settings_repository_path(project))
page.within(".deploy-keys") do
- expect(page).to have_content(DEPLOY_KEY_TITLE)
+ expect(page).to have_content(deploy_key_title)
end
end
end
diff --git a/spec/features/projects/settings/user_sees_revoke_deploy_token_modal_spec.rb b/spec/features/projects/settings/user_sees_revoke_deploy_token_modal_spec.rb
index a77240c5c33..0abc4b41a2b 100644
--- a/spec/features/projects/settings/user_sees_revoke_deploy_token_modal_spec.rb
+++ b/spec/features/projects/settings/user_sees_revoke_deploy_token_modal_spec.rb
@@ -11,7 +11,7 @@ describe 'Repository Settings > User sees revoke deploy token modal', :js do
before do
project.add_role(user, role)
sign_in(user)
- stub_feature_flags(ajax_new_deploy_token: { enabled: false, thing: project })
+ stub_feature_flags(ajax_new_deploy_token: project)
visit(project_settings_repository_path(project))
click_link('Revoke')
end
diff --git a/spec/features/projects/snippets/create_snippet_spec.rb b/spec/features/projects/snippets/create_snippet_spec.rb
index d883a1fc39c..1e8f9fa0875 100644
--- a/spec/features/projects/snippets/create_snippet_spec.rb
+++ b/spec/features/projects/snippets/create_snippet_spec.rb
@@ -5,7 +5,6 @@ require 'spec_helper'
shared_examples_for 'snippet editor' do
before do
stub_feature_flags(snippets_edit_vue: false)
- stub_feature_flags(monaco_snippets: flag)
end
def description_field
@@ -20,7 +19,7 @@ shared_examples_for 'snippet editor' do
fill_in 'project_snippet_description', with: 'My Snippet **Description**'
page.within('.file-editor') do
- el = flag == true ? find('.inputarea') : find('.ace_text-input', visible: false)
+ el = find('.inputarea')
el.send_keys 'Hello World!'
end
end
@@ -100,7 +99,7 @@ shared_examples_for 'snippet editor' do
end
context 'when the git operation fails' do
- let(:error) { 'This is a git error' }
+ let(:error) { 'Error creating the snippet' }
before do
allow_next_instance_of(Snippets::CreateService) do |instance|
@@ -145,15 +144,5 @@ describe 'Projects > Snippets > Create Snippet', :js do
let_it_be(:user) { create(:user) }
let_it_be(:project) { create(:project, :public) }
- context 'when using Monaco' do
- it_behaves_like "snippet editor" do
- let(:flag) { true }
- end
- end
-
- context 'when using ACE' do
- it_behaves_like "snippet editor" do
- let(:flag) { false }
- end
- end
+ it_behaves_like "snippet editor"
end
diff --git a/spec/features/projects/snippets/user_updates_snippet_spec.rb b/spec/features/projects/snippets/user_updates_snippet_spec.rb
index cf501e55e23..d19fe9e8d38 100644
--- a/spec/features/projects/snippets/user_updates_snippet_spec.rb
+++ b/spec/features/projects/snippets/user_updates_snippet_spec.rb
@@ -7,12 +7,9 @@ describe 'Projects > Snippets > User updates a snippet', :js do
let_it_be(:project) { create(:project, namespace: user.namespace) }
let_it_be(:snippet, reload: true) { create(:project_snippet, :repository, project: project, author: user) }
- let(:version_snippet_enabled) { true }
-
before do
stub_feature_flags(snippets_vue: false)
stub_feature_flags(snippets_edit_vue: false)
- stub_feature_flags(version_snippets: version_snippet_enabled)
project.add_maintainer(user)
sign_in(user)
@@ -35,18 +32,6 @@ describe 'Projects > Snippets > User updates a snippet', :js do
end
end
- context 'when feature flag :version_snippets is disabled' do
- let(:version_snippet_enabled) { false }
-
- it 'displays the snippet file_name and content' do
- aggregate_failures do
- expect(page.find_field('project_snippet_file_name').value).to eq snippet.file_name
- expect(page.find('.file-content')).to have_content(snippet.content)
- expect(page.find('.snippet-file-content', visible: false).value).to eq snippet.content
- end
- end
- end
-
it 'updates a snippet' do
fill_in('project_snippet_title', with: 'Snippet new title')
click_button('Save')
@@ -57,16 +42,17 @@ describe 'Projects > Snippets > User updates a snippet', :js do
context 'when the git operation fails' do
before do
allow_next_instance_of(Snippets::UpdateService) do |instance|
- allow(instance).to receive(:create_commit).and_raise(StandardError)
+ allow(instance).to receive(:create_commit).and_raise(StandardError, 'Error Message')
end
fill_in('project_snippet_title', with: 'Snippet new title')
+ fill_in('project_snippet_file_name', with: 'new_file_name')
click_button('Save')
end
it 'renders edit page and displays the error' do
- expect(page.find('.flash-container span').text).to eq('Error updating the snippet')
+ expect(page.find('.flash-container span').text).to eq('Error updating the snippet - Error Message')
expect(page).to have_content('Edit Snippet')
end
end
diff --git a/spec/features/projects/user_sees_user_popover_spec.rb b/spec/features/projects/user_sees_user_popover_spec.rb
index fafb3773866..6197460776d 100644
--- a/spec/features/projects/user_sees_user_popover_spec.rb
+++ b/spec/features/projects/user_sees_user_popover_spec.rb
@@ -3,6 +3,8 @@
require 'spec_helper'
describe 'User sees user popover', :js do
+ include Spec::Support::Helpers::Features::NotesHelpers
+
let_it_be(:project) { create(:project, :repository) }
let(:user) { project.creator }
let(:merge_request) do
@@ -17,13 +19,13 @@ describe 'User sees user popover', :js do
subject { page }
describe 'hovering over a user link in a merge request' do
+ let(:popover_selector) { '.user-popover' }
+
before do
visit project_merge_request_path(project, merge_request)
end
it 'displays user popover' do
- popover_selector = '.user-popover'
-
find('.js-user-link').hover
expect(page).to have_css(popover_selector, visible: true)
@@ -32,5 +34,17 @@ describe 'User sees user popover', :js do
expect(page).to have_content(user.name)
end
end
+
+ it "displays user popover in system note" do
+ add_note("/assign @#{user.username}")
+
+ wait_for_requests
+
+ find('.system-note-message .js-user-link').hover
+
+ page.within(popover_selector) do
+ expect(page).to have_content(user.name)
+ end
+ end
end
end
diff --git a/spec/features/projects/wiki/markdown_preview_spec.rb b/spec/features/projects/wiki/markdown_preview_spec.rb
index 7d18c0f7a14..bc567d4db42 100644
--- a/spec/features/projects/wiki/markdown_preview_spec.rb
+++ b/spec/features/projects/wiki/markdown_preview_spec.rb
@@ -5,7 +5,7 @@ require 'spec_helper'
describe 'Projects > Wiki > User previews markdown changes', :js do
let_it_be(:user) { create(:user) }
let(:project) { create(:project, :wiki_repo, namespace: user.namespace) }
- let(:wiki_page) { create(:wiki_page, wiki: project.wiki, attrs: { title: 'home', content: '[some link](other-page)' }) }
+ let(:wiki_page) { create(:wiki_page, wiki: project.wiki, title: 'home', content: '[some link](other-page)') }
let(:wiki_content) do
<<-HEREDOC
[regular link](regular)
diff --git a/spec/features/projects/wiki/shortcuts_spec.rb b/spec/features/projects/wiki/shortcuts_spec.rb
index 806d2f28bb9..c51af2526c9 100644
--- a/spec/features/projects/wiki/shortcuts_spec.rb
+++ b/spec/features/projects/wiki/shortcuts_spec.rb
@@ -5,7 +5,7 @@ require 'spec_helper'
describe 'Wiki shortcuts', :js do
let(:user) { create(:user) }
let(:project) { create(:project, :wiki_repo, namespace: user.namespace) }
- let(:wiki_page) { create(:wiki_page, wiki: project.wiki, attrs: { title: 'home', content: 'Home page' }) }
+ let(:wiki_page) { create(:wiki_page, wiki: project.wiki, title: 'home', content: 'Home page') }
before do
sign_in(user)
diff --git a/spec/features/projects/wiki/user_creates_wiki_page_spec.rb b/spec/features/projects/wiki/user_creates_wiki_page_spec.rb
index 67996cc3e5d..5678ebcb72a 100644
--- a/spec/features/projects/wiki/user_creates_wiki_page_spec.rb
+++ b/spec/features/projects/wiki/user_creates_wiki_page_spec.rb
@@ -195,7 +195,7 @@ describe "User creates wiki page" do
context "when wiki is not empty", :js do
before do
- create(:wiki_page, wiki: wiki, attrs: { title: 'home', content: 'Home page' })
+ create(:wiki_page, wiki: wiki, title: 'home', content: 'Home page')
visit(project_wikis_path(project))
end
@@ -304,19 +304,20 @@ describe "User creates wiki page" do
describe 'sidebar feature' do
context 'when there are some existing pages' do
before do
- create(:wiki_page, wiki: wiki, attrs: { title: 'home', content: 'home' })
- create(:wiki_page, wiki: wiki, attrs: { title: 'another', content: 'another' })
+ create(:wiki_page, wiki: wiki, title: 'home', content: 'home')
+ create(:wiki_page, wiki: wiki, title: 'another', content: 'another')
end
it 'renders a default sidebar when there is no customized sidebar' do
visit(project_wikis_path(project))
expect(page).to have_content('another')
+ expect(page).not_to have_link('View All Pages')
end
context 'when there is a customized sidebar' do
before do
- create(:wiki_page, wiki: wiki, attrs: { title: '_sidebar', content: 'My customized sidebar' })
+ create(:wiki_page, wiki: wiki, title: '_sidebar', content: 'My customized sidebar')
end
it 'renders my customized sidebar instead of the default one' do
@@ -328,17 +329,31 @@ describe "User creates wiki page" do
end
end
- context 'when there are more than 15 existing pages' do
+ context 'when there are 15 existing pages' do
before do
- create(:wiki_page, wiki: wiki, attrs: { title: 'home', content: 'home' })
- (1..14).each { |i| create(:wiki_page, wiki: wiki, attrs: { title: "page-#{i}", content: "page #{i}" }) }
+ (1..5).each { |i| create(:wiki_page, wiki: wiki, title: "my page #{i}") }
+ (6..10).each { |i| create(:wiki_page, wiki: wiki, title: "parent/my page #{i}") }
+ (11..15).each { |i| create(:wiki_page, wiki: wiki, title: "grandparent/parent/my page #{i}") }
end
- it 'renders a default sidebar when there is no customized sidebar' do
+ it 'shows all pages in the sidebar' do
visit(project_wikis_path(project))
- expect(page).to have_content('View All Pages')
- expect(page).to have_content('page 1')
+ (1..15).each { |i| expect(page).to have_content("my page #{i}") }
+ expect(page).not_to have_link('View All Pages')
+ end
+
+ context 'when there are more than 15 existing pages' do
+ before do
+ create(:wiki_page, wiki: wiki, title: 'my page 16')
+ end
+
+ it 'shows the first 15 pages in the sidebar' do
+ visit(project_wikis_path(project))
+
+ expect(page).to have_text('my page', count: 15)
+ expect(page).to have_link('View All Pages')
+ end
end
end
end
diff --git a/spec/features/projects/wiki/user_git_access_wiki_page_spec.rb b/spec/features/projects/wiki/user_git_access_wiki_page_spec.rb
index ab3d912dd15..6c6af1c41d2 100644
--- a/spec/features/projects/wiki/user_git_access_wiki_page_spec.rb
+++ b/spec/features/projects/wiki/user_git_access_wiki_page_spec.rb
@@ -5,7 +5,7 @@ require 'spec_helper'
describe 'Projects > Wiki > User views Git access wiki page' do
let(:user) { create(:user) }
let(:project) { create(:project, :wiki_repo, :public) }
- let(:wiki_page) { create(:wiki_page, wiki: project.wiki, attrs: { title: 'home', content: '[some link](other-page)' }) }
+ let(:wiki_page) { create(:wiki_page, wiki: project.wiki, title: 'home', content: '[some link](other-page)') }
before do
sign_in(user)
diff --git a/spec/features/projects/wiki/user_updates_wiki_page_spec.rb b/spec/features/projects/wiki/user_updates_wiki_page_spec.rb
index 9d9c83331fb..55509ddfa10 100644
--- a/spec/features/projects/wiki/user_updates_wiki_page_spec.rb
+++ b/spec/features/projects/wiki/user_updates_wiki_page_spec.rb
@@ -64,7 +64,7 @@ describe 'User updates wiki page' do
context 'when wiki is not empty' do
let(:project_wiki) { create(:project_wiki, project: project, user: project.creator) }
- let!(:wiki_page) { create(:wiki_page, wiki: project_wiki, attrs: { title: 'home', content: 'Home page' }) }
+ let!(:wiki_page) { create(:wiki_page, wiki: project_wiki, title: 'home', content: 'Home page') }
before do
visit(project_wikis_path(project))
@@ -168,7 +168,7 @@ describe 'User updates wiki page' do
let(:project_wiki) { create(:project_wiki, project: project, user: project.creator) }
let(:page_name) { 'page_name' }
let(:page_dir) { "foo/bar/#{page_name}" }
- let!(:wiki_page) { create(:wiki_page, wiki: project_wiki, attrs: { title: page_dir, content: 'Home page' }) }
+ let!(:wiki_page) { create(:wiki_page, wiki: project_wiki, title: page_dir, content: 'Home page') }
before do
visit(project_wiki_edit_path(project, wiki_page))
diff --git a/spec/features/projects/wiki/user_views_wiki_in_project_page_spec.rb b/spec/features/projects/wiki/user_views_wiki_in_project_page_spec.rb
index 471e80b27dc..cb425e8b704 100644
--- a/spec/features/projects/wiki/user_views_wiki_in_project_page_spec.rb
+++ b/spec/features/projects/wiki/user_views_wiki_in_project_page_spec.rb
@@ -21,7 +21,7 @@ describe 'Projects > Wiki > User views wiki in project page' do
context 'when wiki homepage contains a link' do
before do
- create(:wiki_page, wiki: project.wiki, attrs: { title: 'home', content: '[some link](other-page)' })
+ create(:wiki_page, wiki: project.wiki, title: 'home', content: '[some link](other-page)')
end
it 'displays the correct URL for the link' do
diff --git a/spec/features/projects/wiki/user_views_wiki_page_spec.rb b/spec/features/projects/wiki/user_views_wiki_page_spec.rb
index 8a338756323..e379e7466db 100644
--- a/spec/features/projects/wiki/user_views_wiki_page_spec.rb
+++ b/spec/features/projects/wiki/user_views_wiki_page_spec.rb
@@ -11,7 +11,7 @@ describe 'User views a wiki page' do
let(:wiki_page) do
create(:wiki_page,
wiki: project.wiki,
- attrs: { title: 'home', content: "Look at this [image](#{path})\n\n ![alt text](#{path})" })
+ title: 'home', content: "Look at this [image](#{path})\n\n ![alt text](#{path})")
end
before do
diff --git a/spec/features/projects/wiki/user_views_wiki_pages_spec.rb b/spec/features/projects/wiki/user_views_wiki_pages_spec.rb
index 6740df1d4ed..584b2a76143 100644
--- a/spec/features/projects/wiki/user_views_wiki_pages_spec.rb
+++ b/spec/features/projects/wiki/user_views_wiki_pages_spec.rb
@@ -9,13 +9,13 @@ describe 'User views wiki pages' do
let(:project) { create(:project, :wiki_repo, namespace: user.namespace) }
let!(:wiki_page1) do
- create(:wiki_page, wiki: project.wiki, attrs: { title: '3 home', content: '3' })
+ create(:wiki_page, wiki: project.wiki, title: '3 home', content: '3')
end
let!(:wiki_page2) do
- create(:wiki_page, wiki: project.wiki, attrs: { title: '1 home', content: '1' })
+ create(:wiki_page, wiki: project.wiki, title: '1 home', content: '1')
end
let!(:wiki_page3) do
- create(:wiki_page, wiki: project.wiki, attrs: { title: '2 home', content: '2' })
+ create(:wiki_page, wiki: project.wiki, title: '2 home', content: '2')
end
let(:pages) do
diff --git a/spec/features/projects/wiki/users_views_asciidoc_page_with_includes_spec.rb b/spec/features/projects/wiki/users_views_asciidoc_page_with_includes_spec.rb
index 08eea14c438..014b63fa154 100644
--- a/spec/features/projects/wiki/users_views_asciidoc_page_with_includes_spec.rb
+++ b/spec/features/projects/wiki/users_views_asciidoc_page_with_includes_spec.rb
@@ -16,7 +16,7 @@ describe 'User views AsciiDoc page with includes', :js do
format: :asciidoc
}
- create(:wiki_page, wiki: project.wiki, attrs: attrs)
+ create(:wiki_page, wiki: project.wiki, **attrs)
end
before do
diff --git a/spec/features/search/user_searches_for_code_spec.rb b/spec/features/search/user_searches_for_code_spec.rb
index 9949595fddf..0fdc7346535 100644
--- a/spec/features/search/user_searches_for_code_spec.rb
+++ b/spec/features/search/user_searches_for_code_spec.rb
@@ -40,6 +40,9 @@ describe 'User searches for code' do
find('.btn-search').click
expect(page).to have_selector('.results', text: 'Update capybara, rspec-rails, poltergeist to recent versions')
+
+ find("#L3").click
+ expect(current_url).to match(/master\/.gitignore#L3/)
end
it 'search mutiple words with refs switching' do
@@ -57,6 +60,7 @@ describe 'User searches for code' do
expect(page).to have_selector('.results', text: expected_result)
expect(find_field('dashboard_search').value).to eq(search)
+ expect(find("#L1502")[:href]).to match(/v1.0.0\/files\/markdown\/ruby-style-guide.md#L1502/)
end
end
diff --git a/spec/features/search/user_searches_for_wiki_pages_spec.rb b/spec/features/search/user_searches_for_wiki_pages_spec.rb
index 1ae37447bdc..10c3032da8b 100644
--- a/spec/features/search/user_searches_for_wiki_pages_spec.rb
+++ b/spec/features/search/user_searches_for_wiki_pages_spec.rb
@@ -5,7 +5,7 @@ require 'spec_helper'
describe 'User searches for wiki pages', :js do
let(:user) { create(:user) }
let(:project) { create(:project, :repository, :wiki_repo, namespace: user.namespace) }
- let!(:wiki_page) { create(:wiki_page, wiki: project.wiki, attrs: { title: 'directory/title', content: 'Some Wiki content' }) }
+ let!(:wiki_page) { create(:wiki_page, wiki: project.wiki, title: 'directory/title', content: 'Some Wiki content') }
before do
project.add_maintainer(user)
diff --git a/spec/features/security/project/internal_access_spec.rb b/spec/features/security/project/internal_access_spec.rb
index 45b57b5cb1b..f29aa8de928 100644
--- a/spec/features/security/project/internal_access_spec.rb
+++ b/spec/features/security/project/internal_access_spec.rb
@@ -464,9 +464,9 @@ describe "Internal Project Access" do
it { is_expected.to be_allowed_for(:owner).of(project) }
it { is_expected.to be_allowed_for(:maintainer).of(project) }
it { is_expected.to be_allowed_for(:developer).of(project) }
- it { is_expected.to be_denied_for(:reporter).of(project) }
- it { is_expected.to be_denied_for(:guest).of(project) }
- it { is_expected.to be_denied_for(:user) }
+ it { is_expected.to be_allowed_for(:reporter).of(project) }
+ it { is_expected.to be_allowed_for(:guest).of(project) }
+ it { is_expected.to be_allowed_for(:user) }
it { is_expected.to be_denied_for(:external) }
it { is_expected.to be_denied_for(:visitor) }
end
diff --git a/spec/features/security/project/private_access_spec.rb b/spec/features/security/project/private_access_spec.rb
index 9aeb3ffbd43..ac8596d89bc 100644
--- a/spec/features/security/project/private_access_spec.rb
+++ b/spec/features/security/project/private_access_spec.rb
@@ -499,7 +499,7 @@ describe "Private Project Access" do
it { is_expected.to be_allowed_for(:owner).of(project) }
it { is_expected.to be_allowed_for(:maintainer).of(project) }
it { is_expected.to be_allowed_for(:developer).of(project) }
- it { is_expected.to be_denied_for(:reporter).of(project) }
+ it { is_expected.to be_allowed_for(:reporter).of(project) }
it { is_expected.to be_denied_for(:guest).of(project) }
it { is_expected.to be_denied_for(:user) }
it { is_expected.to be_denied_for(:external) }
diff --git a/spec/features/security/project/public_access_spec.rb b/spec/features/security/project/public_access_spec.rb
index 4d8c2c7822c..11e9bff10a1 100644
--- a/spec/features/security/project/public_access_spec.rb
+++ b/spec/features/security/project/public_access_spec.rb
@@ -278,11 +278,11 @@ describe "Public Project Access" do
it { is_expected.to be_allowed_for(:owner).of(project) }
it { is_expected.to be_allowed_for(:maintainer).of(project) }
it { is_expected.to be_allowed_for(:developer).of(project) }
- it { is_expected.to be_denied_for(:reporter).of(project) }
- it { is_expected.to be_denied_for(:guest).of(project) }
- it { is_expected.to be_denied_for(:user) }
- it { is_expected.to be_denied_for(:external) }
- it { is_expected.to be_denied_for(:visitor) }
+ it { is_expected.to be_allowed_for(:reporter).of(project) }
+ it { is_expected.to be_allowed_for(:guest).of(project) }
+ it { is_expected.to be_allowed_for(:user) }
+ it { is_expected.to be_allowed_for(:external) }
+ it { is_expected.to be_allowed_for(:visitor) }
end
describe "GET /:project_path/-/environments" do
diff --git a/spec/features/snippets/search_snippets_spec.rb b/spec/features/snippets/search_snippets_spec.rb
index 691716d3576..d3e02d43813 100644
--- a/spec/features/snippets/search_snippets_spec.rb
+++ b/spec/features/snippets/search_snippets_spec.rb
@@ -11,7 +11,7 @@ describe 'Search Snippets' do
visit dashboard_snippets_path
submit_search('Middle')
- select_search_scope('Titles and Filenames')
+ select_search_scope('Titles and Descriptions')
expect(page).to have_link(public_snippet.title)
expect(page).to have_link(private_snippet.title)
diff --git a/spec/features/snippets/spam_snippets_spec.rb b/spec/features/snippets/spam_snippets_spec.rb
index 69e3f190725..d7b181dc678 100644
--- a/spec/features/snippets/spam_snippets_spec.rb
+++ b/spec/features/snippets/spam_snippets_spec.rb
@@ -3,6 +3,8 @@
require 'spec_helper'
shared_examples_for 'snippet editor' do
+ include_context 'includes Spam constants'
+
def description_field
find('.js-description-input').find('input,textarea')
end
@@ -11,7 +13,6 @@ shared_examples_for 'snippet editor' do
stub_feature_flags(allow_possible_spam: false)
stub_feature_flags(snippets_vue: false)
stub_feature_flags(snippets_edit_vue: false)
- stub_feature_flags(monaco_snippets: flag)
stub_env('IN_MEMORY_APPLICATION_SETTINGS', 'false')
Gitlab::CurrentSettings.update!(
@@ -33,18 +34,18 @@ shared_examples_for 'snippet editor' do
find('#personal_snippet_visibility_level_20').set(true)
page.within('.file-editor') do
- el = flag == true ? find('.inputarea') : find('.ace_text-input', visible: false)
+ el = find('.inputarea')
el.send_keys 'Hello World!'
end
end
- shared_examples 'solve recaptcha' do
- it 'creates a snippet after solving reCaptcha' do
+ shared_examples 'solve reCAPTCHA' do
+ it 'creates a snippet after solving reCAPTCHA' do
click_button('Create snippet')
wait_for_requests
- # it is impossible to test recaptcha automatically and there is no possibility to fill in recaptcha
- # recaptcha verification is skipped in test environment and it always returns true
+ # it is impossible to test reCAPTCHA automatically and there is no possibility to fill in recaptcha
+ # reCAPTCHA verification is skipped in test environment and it always returns true
expect(page).not_to have_content('My Snippet Title')
expect(page).to have_css('.recaptcha')
click_button('Submit personal snippet')
@@ -53,23 +54,62 @@ shared_examples_for 'snippet editor' do
end
end
- context 'when identified as spam' do
+ shared_examples 'does not allow creation' do
+ it 'rejects creation of the snippet' do
+ click_button('Create snippet')
+ wait_for_requests
+
+ expect(page).to have_content('discarded')
+ expect(page).not_to have_content('My Snippet Title')
+ expect(page).not_to have_css('.recaptcha')
+ end
+ end
+
+ context 'when SpamVerdictService requires recaptcha' do
+ before do
+ expect_next_instance_of(Spam::SpamVerdictService) do |verdict_service|
+ expect(verdict_service).to receive(:execute).and_return(REQUIRE_RECAPTCHA)
+ end
+ end
+
+ context 'when allow_possible_spam feature flag is false' do
+ before do
+ stub_application_setting(recaptcha_enabled: false)
+ end
+
+ it_behaves_like 'does not allow creation'
+ end
+
+ context 'when allow_possible_spam feature flag is true' do
+ it_behaves_like 'solve reCAPTCHA'
+ end
+ end
+
+ context 'when SpamVerdictService disallows' do
before do
- WebMock.stub_request(:any, /.*akismet.com.*/).to_return(body: "true", status: 200)
+ expect_next_instance_of(Spam::SpamVerdictService) do |verdict_service|
+ expect(verdict_service).to receive(:execute).and_return(DISALLOW)
+ end
end
context 'when allow_possible_spam feature flag is false' do
- it_behaves_like 'solve recaptcha'
+ before do
+ stub_application_setting(recaptcha_enabled: false)
+ end
+
+ it_behaves_like 'does not allow creation'
end
context 'when allow_possible_spam feature flag is true' do
- it_behaves_like 'solve recaptcha'
+ it_behaves_like 'does not allow creation'
end
end
- context 'when not identified as spam' do
+ context 'when SpamVerdictService allows' do
before do
- WebMock.stub_request(:any, /.*akismet.com.*/).to_return(body: "false", status: 200)
+ expect_next_instance_of(Spam::SpamVerdictService) do |verdict_service|
+ expect(verdict_service).to receive(:execute).and_return(ALLOW)
+ end
end
it 'creates a snippet' do
@@ -85,15 +125,5 @@ end
describe 'User creates snippet', :js do
let_it_be(:user) { create(:user) }
- context 'when using Monaco' do
- it_behaves_like "snippet editor" do
- let(:flag) { true }
- end
- end
-
- context 'when using ACE' do
- it_behaves_like "snippet editor" do
- let(:flag) { false }
- end
- end
+ it_behaves_like "snippet editor"
end
diff --git a/spec/features/snippets/user_creates_snippet_spec.rb b/spec/features/snippets/user_creates_snippet_spec.rb
index 5d3a84dd7bc..62054c1f491 100644
--- a/spec/features/snippets/user_creates_snippet_spec.rb
+++ b/spec/features/snippets/user_creates_snippet_spec.rb
@@ -6,7 +6,6 @@ shared_examples_for 'snippet editor' do
before do
stub_feature_flags(snippets_vue: false)
stub_feature_flags(snippets_edit_vue: false)
- stub_feature_flags(monaco_snippets: flag)
sign_in(user)
visit new_snippet_path
end
@@ -23,7 +22,7 @@ shared_examples_for 'snippet editor' do
fill_in 'personal_snippet_description', with: 'My Snippet **Description**'
page.within('.file-editor') do
- el = flag == true ? find('.inputarea') : find('.ace_text-input', visible: false)
+ el = find('.inputarea')
el.send_keys 'Hello World!'
end
end
@@ -80,7 +79,7 @@ shared_examples_for 'snippet editor' do
end
context 'when the git operation fails' do
- let(:error) { 'This is a git error' }
+ let(:error) { 'Error creating the snippet' }
before do
allow_next_instance_of(Snippets::CreateService) do |instance|
@@ -136,7 +135,7 @@ shared_examples_for 'snippet editor' do
fill_in 'personal_snippet_title', with: 'My Snippet Title'
page.within('.file-editor') do
find(:xpath, "//input[@id='personal_snippet_file_name']").set 'snippet+file+name'
- el = flag == true ? find('.inputarea') : find('.ace_text-input', visible: false)
+ el = find('.inputarea')
el.send_keys 'Hello World!'
end
@@ -154,15 +153,5 @@ describe 'User creates snippet', :js do
let_it_be(:user) { create(:user) }
- context 'when using Monaco' do
- it_behaves_like "snippet editor" do
- let(:flag) { true }
- end
- end
-
- context 'when using ACE' do
- it_behaves_like "snippet editor" do
- let(:flag) { false }
- end
- end
+ it_behaves_like "snippet editor"
end
diff --git a/spec/features/snippets/user_edits_snippet_spec.rb b/spec/features/snippets/user_edits_snippet_spec.rb
index b4f8fbfa47e..40b0113cf39 100644
--- a/spec/features/snippets/user_edits_snippet_spec.rb
+++ b/spec/features/snippets/user_edits_snippet_spec.rb
@@ -10,12 +10,9 @@ describe 'User edits snippet', :js do
let_it_be(:user) { create(:user) }
let_it_be(:snippet, reload: true) { create(:personal_snippet, :repository, :public, file_name: file_name, content: content, author: user) }
- let(:version_snippet_enabled) { true }
-
before do
stub_feature_flags(snippets_vue: false)
stub_feature_flags(snippets_edit_vue: false)
- stub_feature_flags(version_snippets: version_snippet_enabled)
sign_in(user)
@@ -33,18 +30,6 @@ describe 'User edits snippet', :js do
end
end
- context 'when feature flag :version_snippets is disabled' do
- let(:version_snippet_enabled) { false }
-
- it 'displays the snippet file_name and content' do
- aggregate_failures do
- expect(page.find_field('personal_snippet_file_name').value).to eq file_name
- expect(page.find('.file-content')).to have_content(content)
- expect(page.find('.snippet-file-content', visible: false).value).to eq content
- end
- end
- end
-
it 'updates the snippet' do
fill_in 'personal_snippet_title', with: 'New Snippet Title'
@@ -88,16 +73,17 @@ describe 'User edits snippet', :js do
context 'when the git operation fails' do
before do
allow_next_instance_of(Snippets::UpdateService) do |instance|
- allow(instance).to receive(:create_commit).and_raise(StandardError)
+ allow(instance).to receive(:create_commit).and_raise(StandardError, 'Error Message')
end
fill_in 'personal_snippet_title', with: 'New Snippet Title'
+ fill_in 'personal_snippet_file_name', with: 'new_file_name'
click_button('Save changes')
end
it 'renders edit page and displays the error' do
- expect(page.find('.flash-container span').text).to eq('Error updating the snippet')
+ expect(page.find('.flash-container span').text).to eq('Error updating the snippet - Error Message')
expect(page).to have_content('Edit Snippet')
end
end
diff --git a/spec/features/static_site_editor_spec.rb b/spec/features/static_site_editor_spec.rb
index c457002f888..de000ee2b9f 100644
--- a/spec/features/static_site_editor_spec.rb
+++ b/spec/features/static_site_editor_spec.rb
@@ -3,8 +3,8 @@
require 'spec_helper'
describe 'Static Site Editor' do
- let(:user) { create(:user) }
- let(:project) { create(:project, :public, :repository) }
+ let_it_be(:user) { create(:user) }
+ let_it_be(:project) { create(:project, :public, :repository) }
before do
project.add_maintainer(user)
diff --git a/spec/features/users/signup_spec.rb b/spec/features/users/signup_spec.rb
index daa987ea389..0ef86dde030 100644
--- a/spec/features/users/signup_spec.rb
+++ b/spec/features/users/signup_spec.rb
@@ -470,8 +470,8 @@ end
describe 'With experimental flow' do
before do
- stub_experiment(signup_flow: true, paid_signup_flow: false)
- stub_experiment_for_user(signup_flow: true, paid_signup_flow: false)
+ stub_experiment(signup_flow: true)
+ stub_experiment_for_user(signup_flow: true)
end
it_behaves_like 'Signup'
diff --git a/spec/finders/alert_management/alerts_finder_spec.rb b/spec/finders/alert_management/alerts_finder_spec.rb
new file mode 100644
index 00000000000..c6d2d0ad4ef
--- /dev/null
+++ b/spec/finders/alert_management/alerts_finder_spec.rb
@@ -0,0 +1,298 @@
+# frozen_string_literal: true
+
+require 'spec_helper'
+
+describe AlertManagement::AlertsFinder, '#execute' do
+ let_it_be(:current_user) { create(:user) }
+ let_it_be(:project) { create(:project) }
+ let_it_be(:alert_1) { create(:alert_management_alert, :all_fields, :resolved, project: project, ended_at: 1.year.ago, events: 2, severity: :high) }
+ let_it_be(:alert_2) { create(:alert_management_alert, :all_fields, :ignored, project: project, events: 1, severity: :critical) }
+ let_it_be(:alert_3) { create(:alert_management_alert, :all_fields) }
+ let(:params) { {} }
+
+ describe '#execute' do
+ subject { described_class.new(current_user, project, params).execute }
+
+ context 'user is not a developer or above' do
+ it { is_expected.to be_empty }
+ end
+
+ context 'user is developer' do
+ before do
+ project.add_developer(current_user)
+ end
+
+ context 'empty params' do
+ it { is_expected.to contain_exactly(alert_1, alert_2) }
+ end
+
+ context 'iid given' do
+ let(:params) { { iid: alert_1.iid } }
+
+ it { is_expected.to match_array(alert_1) }
+
+ context 'unknown iid' do
+ let(:params) { { iid: 'unknown' } }
+
+ it { is_expected.to be_empty }
+ end
+ end
+
+ context 'status given' do
+ let(:params) { { status: AlertManagement::Alert::STATUSES[:resolved] } }
+
+ it { is_expected.to match_array(alert_1) }
+
+ context 'with an array of statuses' do
+ let(:alert_3) { create(:alert_management_alert) }
+ let(:params) { { status: [AlertManagement::Alert::STATUSES[:resolved]] } }
+
+ it { is_expected.to match_array(alert_1) }
+ end
+
+ context 'with no alerts of status' do
+ let(:params) { { status: AlertManagement::Alert::STATUSES[:acknowledged] } }
+
+ it { is_expected.to be_empty }
+ end
+
+ context 'with an empty status array' do
+ let(:params) { { status: [] } }
+
+ it { is_expected.to match_array([alert_1, alert_2]) }
+ end
+
+ context 'with an nil status' do
+ let(:params) { { status: nil } }
+
+ it { is_expected.to match_array([alert_1, alert_2]) }
+ end
+ end
+
+ describe 'sorting' do
+ context 'when sorting by created' do
+ context 'sorts alerts ascending' do
+ let(:params) { { sort: 'created_asc' } }
+
+ it { is_expected.to eq [alert_1, alert_2] }
+ end
+
+ context 'sorts alerts descending' do
+ let(:params) { { sort: 'created_desc' } }
+
+ it { is_expected.to eq [alert_2, alert_1] }
+ end
+ end
+
+ context 'when sorting by updated' do
+ context 'sorts alerts ascending' do
+ let(:params) { { sort: 'updated_asc' } }
+
+ it { is_expected.to eq [alert_1, alert_2] }
+ end
+
+ context 'sorts alerts descending' do
+ let(:params) { { sort: 'updated_desc' } }
+
+ it { is_expected.to eq [alert_2, alert_1] }
+ end
+ end
+
+ context 'when sorting by start time' do
+ context 'sorts alerts ascending' do
+ let(:params) { { sort: 'start_time_asc' } }
+
+ it { is_expected.to eq [alert_1, alert_2] }
+ end
+
+ context 'sorts alerts descending' do
+ let(:params) { { sort: 'start_time_desc' } }
+
+ it { is_expected.to eq [alert_2, alert_1] }
+ end
+ end
+
+ context 'when sorting by end time' do
+ context 'sorts alerts ascending' do
+ let(:params) { { sort: 'end_time_asc' } }
+
+ it { is_expected.to eq [alert_1, alert_2] }
+ end
+
+ context 'sorts alerts descending' do
+ let(:params) { { sort: 'end_time_desc' } }
+
+ it { is_expected.to eq [alert_2, alert_1] }
+ end
+ end
+
+ context 'when sorting by events count' do
+ let_it_be(:alert_count_6) { create(:alert_management_alert, project: project, events: 6) }
+ let_it_be(:alert_count_3) { create(:alert_management_alert, project: project, events: 3) }
+
+ context 'sorts alerts ascending' do
+ let(:params) { { sort: 'events_count_asc' } }
+
+ it { is_expected.to eq [alert_2, alert_1, alert_count_3, alert_count_6] }
+ end
+
+ context 'sorts alerts descending' do
+ let(:params) { { sort: 'events_count_desc' } }
+
+ it { is_expected.to eq [alert_count_6, alert_count_3, alert_1, alert_2] }
+ end
+ end
+
+ context 'when sorting by severity' do
+ let_it_be(:alert_critical) { create(:alert_management_alert, project: project, severity: :critical) }
+ let_it_be(:alert_high) { create(:alert_management_alert, project: project, severity: :high) }
+ let_it_be(:alert_medium) { create(:alert_management_alert, project: project, severity: :medium) }
+ let_it_be(:alert_low) { create(:alert_management_alert, project: project, severity: :low) }
+ let_it_be(:alert_info) { create(:alert_management_alert, project: project, severity: :info) }
+ let_it_be(:alert_unknown) { create(:alert_management_alert, project: project, severity: :unknown) }
+
+ context 'sorts alerts ascending' do
+ let(:params) { { sort: 'severity_asc' } }
+
+ it do
+ is_expected.to eq [
+ alert_2,
+ alert_critical,
+ alert_1,
+ alert_high,
+ alert_medium,
+ alert_low,
+ alert_info,
+ alert_unknown
+ ]
+ end
+ end
+
+ context 'sorts alerts descending' do
+ let(:params) { { sort: 'severity_desc' } }
+
+ it do
+ is_expected.to eq [
+ alert_unknown,
+ alert_info,
+ alert_low,
+ alert_medium,
+ alert_1,
+ alert_high,
+ alert_critical,
+ alert_2
+ ]
+ end
+ end
+ end
+
+ context 'when sorting by status' do
+ let_it_be(:alert_triggered) { create(:alert_management_alert, project: project) }
+ let_it_be(:alert_acknowledged) { create(:alert_management_alert, :acknowledged, project: project) }
+ let_it_be(:alert_resolved) { create(:alert_management_alert, :resolved, project: project) }
+ let_it_be(:alert_ignored) { create(:alert_management_alert, :ignored, project: project) }
+
+ context 'sorts alerts ascending' do
+ let(:params) { { sort: 'status_asc' } }
+
+ it do
+ is_expected.to eq [
+ alert_triggered,
+ alert_acknowledged,
+ alert_1,
+ alert_resolved,
+ alert_2,
+ alert_ignored
+ ]
+ end
+ end
+
+ context 'sorts alerts descending' do
+ let(:params) { { sort: 'status_desc' } }
+
+ it do
+ is_expected.to eq [
+ alert_2,
+ alert_ignored,
+ alert_1,
+ alert_resolved,
+ alert_acknowledged,
+ alert_triggered
+ ]
+ end
+ end
+ end
+ end
+ end
+
+ context 'search query given' do
+ let_it_be(:alert) do
+ create(:alert_management_alert,
+ :with_fingerprint,
+ title: 'Title',
+ description: 'Desc',
+ service: 'Service',
+ monitoring_tool: 'Monitor'
+ )
+ end
+
+ before do
+ alert.project.add_developer(current_user)
+ end
+
+ subject { described_class.new(current_user, alert.project, params).execute }
+
+ context 'searching title' do
+ let(:params) { { search: alert.title } }
+
+ it { is_expected.to match_array([alert]) }
+ end
+
+ context 'searching description' do
+ let(:params) { { search: alert.description } }
+
+ it { is_expected.to match_array([alert]) }
+ end
+
+ context 'searching service' do
+ let(:params) { { search: alert.service } }
+
+ it { is_expected.to match_array([alert]) }
+ end
+
+ context 'searching monitoring tool' do
+ let(:params) { { search: alert.monitoring_tool } }
+
+ it { is_expected.to match_array([alert]) }
+ end
+
+ context 'searching something else' do
+ let(:params) { { search: alert.fingerprint } }
+
+ it { is_expected.to be_empty }
+ end
+
+ context 'empty search' do
+ let(:params) { { search: ' ' } }
+
+ it { is_expected.to match_array([alert]) }
+ end
+ end
+ end
+
+ describe '.counts_by_status' do
+ subject { described_class.counts_by_status(current_user, project, params) }
+
+ before do
+ project.add_developer(current_user)
+ end
+
+ it { is_expected.to match({ 2 => 1, 3 => 1 }) } # one resolved and one ignored
+
+ context 'when filtering params are included' do
+ let(:params) { { status: AlertManagement::Alert::STATUSES[:resolved] } }
+
+ it { is_expected.to match({ 2 => 1 }) } # one resolved
+ end
+ end
+end
diff --git a/spec/finders/artifacts_finder_spec.rb b/spec/finders/artifacts_finder_spec.rb
deleted file mode 100644
index b956e2c9515..00000000000
--- a/spec/finders/artifacts_finder_spec.rb
+++ /dev/null
@@ -1,31 +0,0 @@
-# frozen_string_literal: true
-
-require 'spec_helper'
-
-describe ArtifactsFinder do
- let(:project) { create(:project) }
-
- describe '#execute' do
- before do
- create(:ci_build, :artifacts, project: project)
- end
-
- subject { described_class.new(project, params).execute }
-
- context 'with empty params' do
- let(:params) { {} }
-
- it 'returns all artifacts belonging to the project' do
- expect(subject).to contain_exactly(*project.job_artifacts)
- end
- end
-
- context 'with sort param' do
- let(:params) { { sort: 'size_desc' } }
-
- it 'sorts the artifacts' do
- expect(subject).to eq(project.job_artifacts.order_by('size_desc'))
- end
- end
- end
-end
diff --git a/spec/finders/ci/daily_build_group_report_results_finder_spec.rb b/spec/finders/ci/daily_build_group_report_results_finder_spec.rb
new file mode 100644
index 00000000000..3000ef650d3
--- /dev/null
+++ b/spec/finders/ci/daily_build_group_report_results_finder_spec.rb
@@ -0,0 +1,72 @@
+# frozen_string_literal: true
+
+require 'spec_helper'
+
+describe Ci::DailyBuildGroupReportResultsFinder do
+ describe '#execute' do
+ let(:project) { create(:project, :private) }
+ let(:ref_path) { 'refs/heads/master' }
+ let(:limit) { nil }
+
+ def create_daily_coverage(group_name, coverage, date)
+ create(
+ :ci_daily_build_group_report_result,
+ project: project,
+ ref_path: ref_path,
+ group_name: group_name,
+ data: { 'coverage' => coverage },
+ date: date
+ )
+ end
+
+ let!(:rspec_coverage_1) { create_daily_coverage('rspec', 79.0, '2020-03-09') }
+ let!(:karma_coverage_1) { create_daily_coverage('karma', 89.0, '2020-03-09') }
+ let!(:rspec_coverage_2) { create_daily_coverage('rspec', 95.0, '2020-03-10') }
+ let!(:karma_coverage_2) { create_daily_coverage('karma', 92.0, '2020-03-10') }
+ let!(:rspec_coverage_3) { create_daily_coverage('rspec', 97.0, '2020-03-11') }
+ let!(:karma_coverage_3) { create_daily_coverage('karma', 99.0, '2020-03-11') }
+
+ subject do
+ described_class.new(
+ current_user: current_user,
+ project: project,
+ ref_path: ref_path,
+ start_date: '2020-03-09',
+ end_date: '2020-03-10',
+ limit: limit
+ ).execute
+ end
+
+ context 'when current user is allowed to download project code' do
+ let(:current_user) { project.owner }
+
+ it 'returns all matching results within the given date range' do
+ expect(subject).to match_array([
+ karma_coverage_2,
+ rspec_coverage_2,
+ karma_coverage_1,
+ rspec_coverage_1
+ ])
+ end
+
+ context 'and limit is specified' do
+ let(:limit) { 2 }
+
+ it 'returns limited number of matching results within the given date range' do
+ expect(subject).to match_array([
+ karma_coverage_2,
+ rspec_coverage_2
+ ])
+ end
+ end
+ end
+
+ context 'when current user is not allowed to download project code' do
+ let(:current_user) { create(:user) }
+
+ it 'returns an empty result' do
+ expect(subject).to be_empty
+ end
+ end
+ end
+end
diff --git a/spec/finders/ci/job_artifacts_finder_spec.rb b/spec/finders/ci/job_artifacts_finder_spec.rb
new file mode 100644
index 00000000000..3e701ba87fa
--- /dev/null
+++ b/spec/finders/ci/job_artifacts_finder_spec.rb
@@ -0,0 +1,31 @@
+# frozen_string_literal: true
+
+require 'spec_helper'
+
+describe Ci::JobArtifactsFinder do
+ let(:project) { create(:project) }
+
+ describe '#execute' do
+ before do
+ create(:ci_build, :artifacts, project: project)
+ end
+
+ subject { described_class.new(project, params).execute }
+
+ context 'with empty params' do
+ let(:params) { {} }
+
+ it 'returns all artifacts belonging to the project' do
+ expect(subject).to contain_exactly(*project.job_artifacts)
+ end
+ end
+
+ context 'with sort param' do
+ let(:params) { { sort: 'size_desc' } }
+
+ it 'sorts the artifacts' do
+ expect(subject).to eq(project.job_artifacts.order_by('size_desc'))
+ end
+ end
+ end
+end
diff --git a/spec/finders/container_repositories_finder_spec.rb b/spec/finders/container_repositories_finder_spec.rb
index 08c241186d6..d0c91a8f734 100644
--- a/spec/finders/container_repositories_finder_spec.rb
+++ b/spec/finders/container_repositories_finder_spec.rb
@@ -6,18 +6,35 @@ describe ContainerRepositoriesFinder do
let_it_be(:reporter) { create(:user) }
let_it_be(:guest) { create(:user) }
- let(:group) { create(:group) }
- let(:project) { create(:project, group: group) }
- let!(:project_repository) { create(:container_repository, project: project) }
+ let_it_be(:group) { create(:group) }
+ let_it_be(:project) { create(:project, group: group) }
+ let_it_be(:project_repository) { create(:container_repository, name: 'my_image', project: project) }
+ let(:params) { {} }
before do
group.add_reporter(reporter)
project.add_reporter(reporter)
end
+ shared_examples 'with name search' do
+ let_it_be(:not_searched_repository) do
+ create(:container_repository, name: 'foo_bar_baz', project: project)
+ end
+
+ %w[my_image my_imag _image _imag].each do |name|
+ context "with name set to #{name}" do
+ let(:params) { { name: name } }
+
+ it { is_expected.to contain_exactly(project_repository)}
+
+ it { is_expected.not_to include(not_searched_repository)}
+ end
+ end
+ end
+
describe '#execute' do
context 'with authorized user' do
- subject { described_class.new(user: reporter, subject: subject_object).execute }
+ subject { described_class.new(user: reporter, subject: subject_object, params: params).execute }
context 'when subject_type is group' do
let(:subject_object) { group }
@@ -28,12 +45,16 @@ describe ContainerRepositoriesFinder do
end
it { is_expected.to match_array([project_repository, other_repository]) }
+
+ it_behaves_like 'with name search'
end
context 'when subject_type is project' do
let(:subject_object) { project }
it { is_expected.to match_array([project_repository]) }
+
+ it_behaves_like 'with name search'
end
context 'with invalid subject_type' do
diff --git a/spec/finders/design_management/designs_finder_spec.rb b/spec/finders/design_management/designs_finder_spec.rb
new file mode 100644
index 00000000000..04bd0ad0a45
--- /dev/null
+++ b/spec/finders/design_management/designs_finder_spec.rb
@@ -0,0 +1,105 @@
+# frozen_string_literal: true
+
+require 'spec_helper'
+
+describe DesignManagement::DesignsFinder do
+ include DesignManagementTestHelpers
+
+ let_it_be(:user) { create(:user) }
+ let_it_be(:project) { create(:project, :private) }
+ let_it_be(:issue) { create(:issue, project: project) }
+ let_it_be(:design1) { create(:design, :with_file, issue: issue, versions_count: 1) }
+ let_it_be(:design2) { create(:design, :with_file, issue: issue, versions_count: 1) }
+ let_it_be(:design3) { create(:design, :with_file, issue: issue, versions_count: 1) }
+ let(:params) { {} }
+
+ subject(:designs) { described_class.new(issue, user, params).execute }
+
+ describe '#execute' do
+ context 'when user can not read designs of an issue' do
+ it 'returns no results' do
+ is_expected.to be_empty
+ end
+ end
+
+ context 'when user can read designs of an issue' do
+ before do
+ project.add_developer(user)
+ end
+
+ context 'when design management feature is disabled' do
+ it 'returns no results' do
+ is_expected.to be_empty
+ end
+ end
+
+ context 'when design management feature is enabled' do
+ before do
+ enable_design_management
+ end
+
+ it 'returns the designs' do
+ is_expected.to contain_exactly(design1, design2, design3)
+ end
+
+ context 'when argument is the ids of designs' do
+ let(:params) { { ids: [design1.id] } }
+
+ it { is_expected.to eq([design1]) }
+ end
+
+ context 'when argument is the filenames of designs' do
+ let(:params) { { filenames: [design2.filename] } }
+
+ it { is_expected.to eq([design2]) }
+ end
+
+ context 'when passed empty array' do
+ context 'for filenames' do
+ let(:params) { { filenames: [] } }
+
+ it { is_expected.to be_empty }
+ end
+
+ context "for ids" do
+ let(:params) { { ids: [] } }
+
+ it { is_expected.to be_empty }
+ end
+ end
+
+ describe 'returning designs that existed at a particular given version' do
+ let(:all_versions) { issue.design_collection.versions.ordered }
+ let(:first_version) { all_versions.last }
+ let(:second_version) { all_versions.second }
+
+ context 'when argument is the first version' do
+ let(:params) { { visible_at_version: first_version } }
+
+ it { is_expected.to eq([design1]) }
+ end
+
+ context 'when arguments are version and id' do
+ context 'when id is absent at version' do
+ let(:params) { { visible_at_version: first_version, ids: [design2.id] } }
+
+ it { is_expected.to eq([]) }
+ end
+
+ context 'when id is present at version' do
+ let(:params) { { visible_at_version: second_version, ids: [design2.id] } }
+
+ it { is_expected.to eq([design2]) }
+ end
+ end
+
+ context 'when argument is the second version' do
+ let(:params) { { visible_at_version: second_version } }
+
+ it { is_expected.to contain_exactly(design1, design2) }
+ end
+ end
+ end
+ end
+ end
+end
diff --git a/spec/finders/design_management/versions_finder_spec.rb b/spec/finders/design_management/versions_finder_spec.rb
new file mode 100644
index 00000000000..11d53d0d630
--- /dev/null
+++ b/spec/finders/design_management/versions_finder_spec.rb
@@ -0,0 +1,129 @@
+# frozen_string_literal: true
+
+require 'spec_helper'
+
+describe DesignManagement::VersionsFinder do
+ include DesignManagementTestHelpers
+
+ let_it_be(:user) { create(:user) }
+ let_it_be(:project) { create(:project, :private) }
+ let_it_be(:issue) { create(:issue, project: project) }
+ let_it_be(:design_1) { create(:design, :with_file, issue: issue, versions_count: 1) }
+ let_it_be(:design_2) { create(:design, :with_file, issue: issue, versions_count: 1) }
+ let(:version_1) { design_1.versions.first }
+ let(:version_2) { design_2.versions.first }
+ let(:design_or_collection) { issue.design_collection }
+ let(:params) { {} }
+
+ let(:finder) { described_class.new(design_or_collection, user, params) }
+
+ subject(:versions) { finder.execute }
+
+ describe '#execute' do
+ shared_examples 'returns no results' do
+ it 'returns no results when passed a DesignCollection' do
+ expect(design_or_collection).is_a?(DesignManagement::DesignCollection)
+ is_expected.to be_empty
+ end
+
+ context 'when passed a Design' do
+ let(:design_or_collection) { design_1 }
+
+ it 'returns no results when passed a Design' do
+ is_expected.to be_empty
+ end
+ end
+ end
+
+ context 'when user cannot read designs of an issue' do
+ include_examples 'returns no results'
+ end
+
+ context 'when user can read designs of an issue' do
+ before do
+ project.add_developer(user)
+ end
+
+ context 'when design management feature is disabled' do
+ include_examples 'returns no results'
+ end
+
+ context 'when design management feature is enabled' do
+ before do
+ enable_design_management
+ end
+
+ describe 'passing a DesignCollection or a Design for the initial scoping' do
+ it 'returns the versions scoped to the DesignCollection' do
+ expect(design_or_collection).is_a?(DesignManagement::DesignCollection)
+ is_expected.to eq(issue.design_collection.versions.ordered)
+ end
+
+ context 'when passed a Design' do
+ let(:design_or_collection) { design_1 }
+
+ it 'returns the versions scoped to the Design' do
+ is_expected.to eq(design_1.versions)
+ end
+ end
+ end
+
+ describe 'returning versions earlier or equal to a version' do
+ context 'when argument is the first version' do
+ let(:params) { { earlier_or_equal_to: version_1 }}
+
+ it { is_expected.to eq([version_1]) }
+ end
+
+ context 'when argument is the second version' do
+ let(:params) { { earlier_or_equal_to: version_2 }}
+
+ it { is_expected.to contain_exactly(version_1, version_2) }
+ end
+ end
+
+ describe 'returning versions by SHA' do
+ context 'when argument is the first version' do
+ let(:params) { { sha: version_1.sha } }
+
+ it { is_expected.to contain_exactly(version_1) }
+ end
+
+ context 'when argument is the second version' do
+ let(:params) { { sha: version_2.sha } }
+
+ it { is_expected.to contain_exactly(version_2) }
+ end
+ end
+
+ describe 'returning versions by ID' do
+ context 'when argument is the first version' do
+ let(:params) { { version_id: version_1.id } }
+
+ it { is_expected.to contain_exactly(version_1) }
+ end
+
+ context 'when argument is the second version' do
+ let(:params) { { version_id: version_2.id } }
+
+ it { is_expected.to contain_exactly(version_2) }
+ end
+ end
+
+ describe 'mixing id and sha' do
+ context 'when arguments are consistent' do
+ let(:params) { { version_id: version_1.id, sha: version_1.sha } }
+
+ it { is_expected.to contain_exactly(version_1) }
+ end
+
+ context 'when arguments are in-consistent' do
+ let(:params) { { version_id: version_1.id, sha: version_2.sha } }
+
+ it { is_expected.to be_empty }
+ end
+ end
+ end
+ end
+ end
+end
diff --git a/spec/finders/fork_projects_finder_spec.rb b/spec/finders/fork_projects_finder_spec.rb
index 2fba53a74a0..02ce17ac907 100644
--- a/spec/finders/fork_projects_finder_spec.rb
+++ b/spec/finders/fork_projects_finder_spec.rb
@@ -14,7 +14,7 @@ describe ForkProjectsFinder do
let(:private_fork_member) { create(:user) }
before do
- stub_feature_flags(object_pools: { enabled: false, thing: source_project })
+ stub_feature_flags(object_pools: source_project)
private_fork.update!(visibility_level: Gitlab::VisibilityLevel::PRIVATE)
private_fork.add_developer(private_fork_member)
diff --git a/spec/finders/freeze_periods_finder_spec.rb b/spec/finders/freeze_periods_finder_spec.rb
new file mode 100644
index 00000000000..4ff356b85b7
--- /dev/null
+++ b/spec/finders/freeze_periods_finder_spec.rb
@@ -0,0 +1,59 @@
+# frozen_string_literal: true
+
+require 'spec_helper'
+
+describe FreezePeriodsFinder do
+ subject(:finder) { described_class.new(project, user).execute }
+
+ let(:project) { create(:project, :private) }
+ let(:user) { create(:user) }
+ let!(:freeze_period_1) { create(:ci_freeze_period, project: project, created_at: 2.days.ago) }
+ let!(:freeze_period_2) { create(:ci_freeze_period, project: project, created_at: 1.day.ago) }
+
+ shared_examples_for 'returns nothing' do
+ specify do
+ is_expected.to be_empty
+ end
+ end
+
+ shared_examples_for 'returns freeze_periods ordered by created_at asc' do
+ it 'returns freeze_periods ordered by created_at' do
+ expect(subject.count).to eq(2)
+ expect(subject.pluck('id')).to eq([freeze_period_1.id, freeze_period_2.id])
+ end
+ end
+
+ context 'when user is a maintainer' do
+ before do
+ project.add_maintainer(user)
+ end
+
+ it_behaves_like 'returns freeze_periods ordered by created_at asc'
+ end
+
+ context 'when user is a guest' do
+ before do
+ project.add_guest(user)
+ end
+
+ it_behaves_like 'returns nothing'
+ end
+
+ context 'when user is a developer' do
+ before do
+ project.add_developer(user)
+ end
+
+ it_behaves_like 'returns nothing'
+ end
+
+ context 'when user is not a project member' do
+ it_behaves_like 'returns nothing'
+
+ context 'when project is public' do
+ let(:project) { create(:project, :public) }
+
+ it_behaves_like 'returns nothing'
+ end
+ end
+end
diff --git a/spec/finders/issues_finder_spec.rb b/spec/finders/issues_finder_spec.rb
index baf40861a6e..7493fafb5cc 100644
--- a/spec/finders/issues_finder_spec.rb
+++ b/spec/finders/issues_finder_spec.rb
@@ -132,26 +132,6 @@ describe IssuesFinder do
end
end
- context 'filtering by NOT group_id' do
- let(:params) { { not: { group_id: group.id } } }
-
- context 'when include_subgroup param not set' do
- it 'returns all other group issues' do
- expect(issues).to contain_exactly(issue2, issue3, issue4)
- end
- end
-
- context 'when include_subgroup param is true', :nested_groups do
- before do
- params[:include_subgroups] = true
- end
-
- it 'returns all other group and subgroup issues' do
- expect(issues).to contain_exactly(issue2, issue3)
- end
- end
- end
-
context 'filtering by author ID' do
let(:params) { { author_id: user2.id } }
@@ -292,12 +272,12 @@ describe IssuesFinder do
context 'using NOT' do
let(:params) { { not: { milestone_title: Milestone::Upcoming.name } } }
- it 'returns issues not in upcoming milestones for each project or group' do
- target_issues = @created_issues.reject do |issue|
- issue.milestone&.due_date && issue.milestone.due_date > Date.current
- end + @created_issues.select { |issue| issue.milestone&.title == '8.9' }
+ it 'returns issues not in upcoming milestones for each project or group, but must have a due date' do
+ target_issues = @created_issues.select do |issue|
+ issue.milestone&.due_date && issue.milestone.due_date <= Date.current
+ end
- expect(issues).to contain_exactly(issue1, issue2, issue3, issue4, *target_issues)
+ expect(issues).to contain_exactly(*target_issues)
end
end
end
@@ -343,9 +323,9 @@ describe IssuesFinder do
let(:params) { { not: { milestone_title: Milestone::Started.name } } }
it 'returns issues not in the started milestones for each project' do
- target_issues = Issue.where.not(milestone: Milestone.started)
+ target_issues = Issue.where(milestone: Milestone.not_started)
- expect(issues).to contain_exactly(issue2, issue3, issue4, *target_issues)
+ expect(issues).to contain_exactly(*target_issues)
end
end
end
@@ -452,14 +432,6 @@ describe IssuesFinder do
it 'returns issues with title and description match for search term' do
expect(issues).to contain_exactly(issue1, issue2)
end
-
- context 'using NOT' do
- let(:params) { { not: { search: 'git' } } }
-
- it 'returns issues with no title and description match for search term' do
- expect(issues).to contain_exactly(issue3, issue4)
- end
- end
end
context 'filtering by issue term in title' do
@@ -468,14 +440,6 @@ describe IssuesFinder do
it 'returns issues with title match for search term' do
expect(issues).to contain_exactly(issue1)
end
-
- context 'using NOT' do
- let(:params) { { not: { search: 'git', in: 'title' } } }
-
- it 'returns issues with no title match for search term' do
- expect(issues).to contain_exactly(issue2, issue3, issue4)
- end
- end
end
context 'filtering by issues iids' do
diff --git a/spec/finders/members_finder_spec.rb b/spec/finders/members_finder_spec.rb
index f6df727f7db..d77548c6fd0 100644
--- a/spec/finders/members_finder_spec.rb
+++ b/spec/finders/members_finder_spec.rb
@@ -110,7 +110,7 @@ describe MembersFinder, '#execute' do
project.add_maintainer(user3)
member3 = project.add_maintainer(user4)
- result = described_class.new(project, user2).execute(params: { search: user4.name })
+ result = described_class.new(project, user2, params: { search: user4.name }).execute
expect(result).to contain_exactly(member3)
end
@@ -120,7 +120,7 @@ describe MembersFinder, '#execute' do
member2 = project.add_maintainer(user3)
member3 = project.add_maintainer(user4)
- result = described_class.new(project, user2).execute(params: { sort: 'id_desc' })
+ result = described_class.new(project, user2, params: { sort: 'id_desc' }).execute
expect(result).to eq([member3, member2, member1])
end
diff --git a/spec/finders/merge_requests_finder_spec.rb b/spec/finders/merge_requests_finder_spec.rb
index 42211f7ac9d..b6f2c7bb992 100644
--- a/spec/finders/merge_requests_finder_spec.rb
+++ b/spec/finders/merge_requests_finder_spec.rb
@@ -174,15 +174,16 @@ describe MergeRequestsFinder do
deployment1 = create(
:deployment,
project: project_with_repo,
- sha: project_with_repo.commit.id,
- merge_requests: [merge_request1, merge_request2]
+ sha: project_with_repo.commit.id
)
- create(
+ deployment2 = create(
:deployment,
project: project_with_repo,
- sha: project_with_repo.commit.id,
- merge_requests: [merge_request3]
+ sha: project_with_repo.commit.id
)
+ deployment1.link_merge_requests(MergeRequest.where(id: [merge_request1.id, merge_request2.id]))
+ deployment2.link_merge_requests(MergeRequest.where(id: merge_request3.id))
+
params = { deployment_id: deployment1.id }
merge_requests = described_class.new(user, params).execute
diff --git a/spec/finders/metrics/users_starred_dashboards_finder_spec.rb b/spec/finders/metrics/users_starred_dashboards_finder_spec.rb
new file mode 100644
index 00000000000..c32b8c2d335
--- /dev/null
+++ b/spec/finders/metrics/users_starred_dashboards_finder_spec.rb
@@ -0,0 +1,55 @@
+# frozen_string_literal: true
+
+require 'spec_helper'
+
+describe Metrics::UsersStarredDashboardsFinder do
+ describe '#execute' do
+ subject(:starred_dashboards) { described_class.new(user: user, project: project, params: params).execute }
+
+ let_it_be(:user) { create(:user) }
+ let(:project) { create(:project) }
+ let(:dashboard_path) { 'config/prometheus/common_metrics.yml' }
+ let(:params) { {} }
+
+ context 'there are no starred dashboard records' do
+ it 'returns empty array' do
+ expect(starred_dashboards).to be_empty
+ end
+ end
+
+ context 'with annotation records' do
+ let!(:starred_dashboard_1) { create(:metrics_users_starred_dashboard, user: user, project: project) }
+ let!(:starred_dashboard_2) { create(:metrics_users_starred_dashboard, user: user, project: project, dashboard_path: dashboard_path) }
+ let!(:other_project_dashboard) { create(:metrics_users_starred_dashboard, user: user, dashboard_path: dashboard_path) }
+ let!(:other_user_dashboard) { create(:metrics_users_starred_dashboard, project: project, dashboard_path: dashboard_path) }
+
+ context 'user without read access to project' do
+ it 'returns empty relation' do
+ expect(starred_dashboards).to be_empty
+ end
+ end
+
+ context 'user with read access to project' do
+ before do
+ project.add_reporter(user)
+ end
+
+ it 'loads starred dashboards' do
+ expect(starred_dashboards).to contain_exactly starred_dashboard_1, starred_dashboard_2
+ end
+
+ context 'when the dashboard_path filter is present' do
+ let(:params) do
+ {
+ dashboard_path: dashboard_path
+ }
+ end
+
+ it 'loads filtered starred dashboards' do
+ expect(starred_dashboards).to contain_exactly starred_dashboard_2
+ end
+ end
+ end
+ end
+ end
+end
diff --git a/spec/finders/projects/serverless/functions_finder_spec.rb b/spec/finders/projects/serverless/functions_finder_spec.rb
index 4e9f3d371ce..1f0e3cd2eda 100644
--- a/spec/finders/projects/serverless/functions_finder_spec.rb
+++ b/spec/finders/projects/serverless/functions_finder_spec.rb
@@ -48,6 +48,7 @@ describe Projects::Serverless::FunctionsFinder do
expect(function_finder.knative_installed).to be false
end
end
+
context 'when project level cluster is present and enabled' do
it_behaves_like 'before first deployment' do
let(:cluster) { create(:cluster, :project, :provided_by_gcp, enabled: true) }
diff --git a/spec/finders/releases_finder_spec.rb b/spec/finders/releases_finder_spec.rb
index 3da5ee47b6b..cb4e5fed816 100644
--- a/spec/finders/releases_finder_spec.rb
+++ b/spec/finders/releases_finder_spec.rb
@@ -5,10 +5,11 @@ require 'spec_helper'
describe ReleasesFinder do
let(:user) { create(:user) }
let(:project) { create(:project, :repository) }
+ let(:params) { {} }
let(:repository) { project.repository }
let(:v1_0_0) { create(:release, project: project, tag: 'v1.0.0') }
let(:v1_1_0) { create(:release, project: project, tag: 'v1.1.0') }
- let(:finder) { described_class.new(project, user) }
+ let(:finder) { described_class.new(project, user, params) }
before do
v1_0_0.update_attribute(:released_at, 2.days.ago)
@@ -64,6 +65,14 @@ describe ReleasesFinder do
expect(subject).to eq([v1_1_0])
end
end
+
+ context 'when a tag parameter is passed' do
+ let(:params) { { tag: 'v1.0.0' } }
+
+ it 'only returns the release with the matching tag' do
+ expect(subject).to eq([v1_0_0])
+ end
+ end
end
end
end
diff --git a/spec/finders/todos_finder_spec.rb b/spec/finders/todos_finder_spec.rb
index a35c3a954e7..87650835b05 100644
--- a/spec/finders/todos_finder_spec.rb
+++ b/spec/finders/todos_finder_spec.rb
@@ -260,9 +260,9 @@ describe TodosFinder do
it 'returns the expected types' do
expected_result =
if Gitlab.ee?
- %w[Epic Issue MergeRequest]
+ %w[Epic Issue MergeRequest DesignManagement::Design]
else
- %w[Issue MergeRequest]
+ %w[Issue MergeRequest DesignManagement::Design]
end
expect(described_class.todo_types).to contain_exactly(*expected_result)
diff --git a/spec/fixtures/accessibility/pa11y_with_errors.json b/spec/fixtures/accessibility/pa11y_with_errors.json
new file mode 100644
index 00000000000..35537f6cdd8
--- /dev/null
+++ b/spec/fixtures/accessibility/pa11y_with_errors.json
@@ -0,0 +1,109 @@
+{
+ "total": 1,
+ "passes": 0,
+ "errors": 10,
+ "results": {
+ "https://about.gitlab.com/": [
+ {
+ "code": "WCAG2AA.Principle4.Guideline4_1.4_1_2.H91.A.NoContent",
+ "type": "error",
+ "typeCode": 1,
+ "message": "Anchor element found with a valid href attribute, but no link content has been supplied.",
+ "context": "<a href=\"/\" class=\"navbar-brand animated\"><svg height=\"36\" viewBox=\"0 0 1...</a>",
+ "selector": "#main-nav > div:nth-child(1) > a",
+ "runner": "htmlcs",
+ "runnerExtras": {}
+ },
+ {
+ "code": "WCAG2AA.Principle1.Guideline1_4.1_4_3.G18.Fail",
+ "type": "error",
+ "typeCode": 1,
+ "message": "This element has insufficient contrast at this conformance level. Expected a contrast ratio of at least 4.5:1, but text in this element has a contrast ratio of 1.05:1. Recommendation: change background to #767676.",
+ "context": "<a href=\"/stages-devops-lifecycle/\" class=\"main-nav-link\">Product</a>",
+ "selector": "#main-nav > div:nth-child(2) > ul > li:nth-child(1) > a",
+ "runner": "htmlcs",
+ "runnerExtras": {}
+ },
+ {
+ "code": "WCAG2AA.Principle1.Guideline1_4.1_4_3.G18.Fail",
+ "type": "error",
+ "typeCode": 1,
+ "message": "This element has insufficient contrast at this conformance level. Expected a contrast ratio of at least 4.5:1, but text in this element has a contrast ratio of 1.05:1. Recommendation: change background to #767676.",
+ "context": "<a href=\"/pricing/\" class=\"main-nav-link\">Pricing</a>",
+ "selector": "#main-nav > div:nth-child(2) > ul > li:nth-child(2) > a",
+ "runner": "htmlcs",
+ "runnerExtras": {}
+ },
+ {
+ "code": "WCAG2AA.Principle1.Guideline1_4.1_4_3.G18.Fail",
+ "type": "error",
+ "typeCode": 1,
+ "message": "This element has insufficient contrast at this conformance level. Expected a contrast ratio of at least 4.5:1, but text in this element has a contrast ratio of 1.05:1. Recommendation: change background to #767676.",
+ "context": "<a href=\"/resources/\" class=\"main-nav-link\">Resources</a>",
+ "selector": "#main-nav > div:nth-child(2) > ul > li:nth-child(3) > a",
+ "runner": "htmlcs",
+ "runnerExtras": {}
+ },
+ {
+ "code": "WCAG2AA.Principle1.Guideline1_4.1_4_3.G18.Fail",
+ "type": "error",
+ "typeCode": 1,
+ "message": "This element has insufficient contrast at this conformance level. Expected a contrast ratio of at least 4.5:1, but text in this element has a contrast ratio of 1.05:1. Recommendation: change background to #767676.",
+ "context": "<a href=\"/blog/\" class=\"main-nav-link\">Blog</a>",
+ "selector": "#main-nav > div:nth-child(2) > ul > li:nth-child(4) > a",
+ "runner": "htmlcs",
+ "runnerExtras": {}
+ },
+ {
+ "code": "WCAG2AA.Principle1.Guideline1_4.1_4_3.G18.Fail",
+ "type": "error",
+ "typeCode": 1,
+ "message": "This element has insufficient contrast at this conformance level. Expected a contrast ratio of at least 4.5:1, but text in this element has a contrast ratio of 1.05:1. Recommendation: change background to #767676.",
+ "context": "<a href=\"/support/\" class=\"main-nav-link\">Support</a>",
+ "selector": "#main-nav > div:nth-child(2) > ul > li:nth-child(5) > a",
+ "runner": "htmlcs",
+ "runnerExtras": {}
+ },
+ {
+ "code": "WCAG2AA.Principle1.Guideline1_4.1_4_3.G18.Fail",
+ "type": "error",
+ "typeCode": 1,
+ "message": "This element has insufficient contrast at this conformance level. Expected a contrast ratio of at least 4.5:1, but text in this element has a contrast ratio of 1.05:1. Recommendation: change background to #767676.",
+ "context": "<a href=\"/jobs/\" class=\"main-nav-link\">Jobs</a>",
+ "selector": "#main-nav > div:nth-child(2) > ul > li:nth-child(6) > a",
+ "runner": "htmlcs",
+ "runnerExtras": {}
+ },
+ {
+ "code": "WCAG2AA.Principle1.Guideline1_4.1_4_3.G18.Fail",
+ "type": "error",
+ "typeCode": 1,
+ "message": "This element has insufficient contrast at this conformance level. Expected a contrast ratio of at least 4.5:1, but text in this element has a contrast ratio of 2.82:1. Recommendation: change background to #d1470c.",
+ "context": "<a class=\"btn btn-nav-cta btn-nav-link-cta\" href=\"/free-trial\">\nGet free trial\n</a>",
+ "selector": "#main-nav > div:nth-child(2) > ul > div:nth-child(8) > a",
+ "runner": "htmlcs",
+ "runnerExtras": {}
+ },
+ {
+ "code": "WCAG2AA.Principle1.Guideline1_4.1_4_3.G18.Fail",
+ "type": "error",
+ "typeCode": 1,
+ "message": "This element has insufficient contrast at this conformance level. Expected a contrast ratio of at least 4.5:1, but text in this element has a contrast ratio of 1.05:1. Recommendation: change background to #767676.",
+ "context": "<a class=\"main-nav-link sign-up\" href=\"https://gitlab.com/explore\" target=\"_blank\">\nExplore\n</a>",
+ "selector": "#main-nav > div:nth-child(2) > ul > div:nth-child(9) > li:nth-child(1) > a",
+ "runner": "htmlcs",
+ "runnerExtras": {}
+ },
+ {
+ "code": "WCAG2AA.Principle1.Guideline1_4.1_4_3.G18.Fail",
+ "type": "error",
+ "typeCode": 1,
+ "message": "This element has insufficient contrast at this conformance level. Expected a contrast ratio of at least 4.5:1, but text in this element has a contrast ratio of 1.05:1. Recommendation: change background to #767676.",
+ "context": "<a class=\"main-nav-link sign-up\" href=\"https://gitlab.com/users/sign_in\">\nSign in\n</a>",
+ "selector": "#main-nav > div:nth-child(2) > ul > div:nth-child(9) > li:nth-child(2) > a",
+ "runner": "htmlcs",
+ "runnerExtras": {}
+ }
+ ]
+ }
+}
diff --git a/spec/fixtures/accessibility/pa11y_with_invalid_url.json b/spec/fixtures/accessibility/pa11y_with_invalid_url.json
new file mode 100644
index 00000000000..aa4a663f944
--- /dev/null
+++ b/spec/fixtures/accessibility/pa11y_with_invalid_url.json
@@ -0,0 +1,12 @@
+{
+ "total": 1,
+ "passes": 0,
+ "errors": 0,
+ "results": {
+ "": [
+ {
+ "message": "Protocol error (Page.navigate): Cannot navigate to invalid URL"
+ }
+ ]
+ }
+}
diff --git a/spec/fixtures/accessibility/pa11y_without_errors.json b/spec/fixtures/accessibility/pa11y_without_errors.json
new file mode 100644
index 00000000000..c8720f6f052
--- /dev/null
+++ b/spec/fixtures/accessibility/pa11y_without_errors.json
@@ -0,0 +1,8 @@
+{
+ "total": 1,
+ "passes": 1,
+ "errors": 0,
+ "results": {
+ "https://pa11y.org/": []
+ }
+}
diff --git a/spec/fixtures/api/schemas/cluster_list.json b/spec/fixtures/api/schemas/cluster_list.json
new file mode 100644
index 00000000000..ece9542eb79
--- /dev/null
+++ b/spec/fixtures/api/schemas/cluster_list.json
@@ -0,0 +1,14 @@
+{
+ "clusters": {
+ "type": "array",
+ "items": {
+ "cluster_type": "string",
+ "enabled": "boolean",
+ "environment_scope": "string",
+ "name": "string",
+ "path": "string",
+ "status": "string"
+ }
+ },
+ "has_ancestor_clusters": { "type": ["boolean", "false"] }
+}
diff --git a/spec/fixtures/api/schemas/cluster_status.json b/spec/fixtures/api/schemas/cluster_status.json
index ce62655648b..f6db336fe65 100644
--- a/spec/fixtures/api/schemas/cluster_status.json
+++ b/spec/fixtures/api/schemas/cluster_status.json
@@ -42,6 +42,8 @@
"host": {"type": ["string", "null"]},
"port": {"type": ["integer", "514"]},
"protocol": {"type": ["integer", "0"]},
+ "waf_log_enabled": {"type": ["boolean", "true"]},
+ "cilium_log_enabled": {"type": ["boolean", "true"]},
"update_available": { "type": ["boolean", "null"] },
"can_uninstall": { "type": "boolean" },
"available_domains": {
diff --git a/spec/fixtures/api/schemas/entities/accessibility_error.json b/spec/fixtures/api/schemas/entities/accessibility_error.json
new file mode 100644
index 00000000000..3ea84835505
--- /dev/null
+++ b/spec/fixtures/api/schemas/entities/accessibility_error.json
@@ -0,0 +1,40 @@
+{
+ "type": "object",
+ "required": [
+ "code",
+ "type",
+ "type_code",
+ "message",
+ "context",
+ "selector",
+ "runner",
+ "runner_extras"
+ ],
+ "properties": {
+ "code": {
+ "type": "string"
+ },
+ "type": {
+ "type": "string"
+ },
+ "type_code": {
+ "type": "integer"
+ },
+ "message": {
+ "type": "string"
+ },
+ "context": {
+ "type": "string"
+ },
+ "selector": {
+ "type": "string"
+ },
+ "runner": {
+ "type": "string"
+ },
+ "runner_extras": {
+ "type": ["object", "null"]
+ }
+ },
+ "additionalProperties": false
+}
diff --git a/spec/fixtures/api/schemas/entities/accessibility_reports_comparer.json b/spec/fixtures/api/schemas/entities/accessibility_reports_comparer.json
new file mode 100644
index 00000000000..ec243354eab
--- /dev/null
+++ b/spec/fixtures/api/schemas/entities/accessibility_reports_comparer.json
@@ -0,0 +1,43 @@
+{
+ "type": "object",
+ "required": ["status", "summary", "new_errors", "resolved_errors", "existing_errors"],
+ "properties": {
+ "status": {
+ "type": "string"
+ },
+ "summary": {
+ "type": "object",
+ "properties": {
+ "total": {
+ "type": "integer"
+ },
+ "resolved": {
+ "type": "integer"
+ },
+ "errored": {
+ "type": "integer"
+ }
+ },
+ "required": ["total", "resolved", "errored"]
+ },
+ "new_errors": {
+ "type": "array",
+ "items": {
+ "$ref": "accessibility_error.json"
+ }
+ },
+ "resolved_errors": {
+ "type": "array",
+ "items": {
+ "$ref": "accessibility_error.json"
+ }
+ },
+ "existing_errors": {
+ "type": "array",
+ "items": {
+ "$ref": "accessibility_error.json"
+ }
+ }
+ },
+ "additionalProperties": false
+}
diff --git a/spec/fixtures/api/schemas/entities/discussion.json b/spec/fixtures/api/schemas/entities/discussion.json
index 9d7ca62435e..21d8efe0b2b 100644
--- a/spec/fixtures/api/schemas/entities/discussion.json
+++ b/spec/fixtures/api/schemas/entities/discussion.json
@@ -29,8 +29,15 @@
"web_url": { "type": "uri" },
"status_tooltip_html": { "type": ["string", "null"] },
"path": { "type": "string" }
- },
- "additionalProperties": false
+ },
+ "required": [
+ "id",
+ "state",
+ "avatar_url",
+ "path",
+ "name",
+ "username"
+ ]
},
"created_at": { "type": "date" },
"updated_at": { "type": "date" },
diff --git a/spec/fixtures/api/schemas/entities/note_user_entity.json b/spec/fixtures/api/schemas/entities/note_user_entity.json
index 9b838054563..4a27d885cdc 100644
--- a/spec/fixtures/api/schemas/entities/note_user_entity.json
+++ b/spec/fixtures/api/schemas/entities/note_user_entity.json
@@ -16,6 +16,5 @@
"name": { "type": "string" },
"username": { "type": "string" },
"status_tooltip_html": { "$ref": "../types/nullable_string.json" }
- },
- "additionalProperties": false
+ }
}
diff --git a/spec/fixtures/api/schemas/entities/user.json b/spec/fixtures/api/schemas/entities/user.json
index 82d80b75cef..3252a37c82a 100644
--- a/spec/fixtures/api/schemas/entities/user.json
+++ b/spec/fixtures/api/schemas/entities/user.json
@@ -18,6 +18,5 @@
"name": { "type": "string" },
"username": { "type": "string" },
"status_tooltip_html": { "$ref": "../types/nullable_string.json" }
- },
- "additionalProperties": false
+ }
}
diff --git a/spec/fixtures/api/schemas/pipeline_schedule.json b/spec/fixtures/api/schemas/pipeline_schedule.json
index 690c4a7d4e8..d01801a15fa 100644
--- a/spec/fixtures/api/schemas/pipeline_schedule.json
+++ b/spec/fixtures/api/schemas/pipeline_schedule.json
@@ -30,7 +30,9 @@
"avatar_url": { "type": "uri" },
"web_url": { "type": "uri" }
},
- "additionalProperties": false
+ "required": [
+ "id", "name", "username", "state", "avatar_url", "web_url"
+ ]
},
"variables": {
"type": "array",
diff --git a/spec/fixtures/api/schemas/public_api/v4/branch.json b/spec/fixtures/api/schemas/public_api/v4/branch.json
index 3b0f010bc4f..0073a6d89fc 100644
--- a/spec/fixtures/api/schemas/public_api/v4/branch.json
+++ b/spec/fixtures/api/schemas/public_api/v4/branch.json
@@ -7,7 +7,8 @@
"protected",
"default",
"developers_can_push",
- "developers_can_merge"
+ "developers_can_merge",
+ "web_url"
],
"properties" : {
"name": { "type": "string" },
@@ -17,7 +18,8 @@
"default": { "type": "boolean" },
"developers_can_push": { "type": "boolean" },
"developers_can_merge": { "type": "boolean" },
- "can_push": { "type": "boolean" }
+ "can_push": { "type": "boolean" },
+ "web_url": { "type": "uri" }
},
"additionalProperties": false
}
diff --git a/spec/fixtures/api/schemas/public_api/v4/freeze_period.json b/spec/fixtures/api/schemas/public_api/v4/freeze_period.json
new file mode 100644
index 00000000000..b0187aee647
--- /dev/null
+++ b/spec/fixtures/api/schemas/public_api/v4/freeze_period.json
@@ -0,0 +1,20 @@
+{
+ "type": "object",
+ "required": [
+ "id",
+ "freeze_start",
+ "freeze_end",
+ "cron_timezone",
+ "created_at",
+ "updated_at"
+ ],
+ "properties": {
+ "id": { "type": "integer" },
+ "freeze_start": { "type": "string" },
+ "freeze_end": { "type": "string"},
+ "cron_timezone": { "type": "string" },
+ "created_at": { "type": "string" },
+ "updated_at": { "type": "string" }
+ },
+ "additionalProperties": false
+}
diff --git a/spec/fixtures/api/schemas/public_api/v4/freeze_periods.json b/spec/fixtures/api/schemas/public_api/v4/freeze_periods.json
new file mode 100644
index 00000000000..1e1c29a3b64
--- /dev/null
+++ b/spec/fixtures/api/schemas/public_api/v4/freeze_periods.json
@@ -0,0 +1,5 @@
+{
+ "type": "array",
+ "items": { "$ref": "freeze_period.json" }
+}
+
diff --git a/spec/fixtures/api/schemas/public_api/v4/issue.json b/spec/fixtures/api/schemas/public_api/v4/issue.json
index bf1b4a06f0b..69ecba8b6f3 100644
--- a/spec/fixtures/api/schemas/public_api/v4/issue.json
+++ b/spec/fixtures/api/schemas/public_api/v4/issue.json
@@ -71,7 +71,14 @@
"avatar_url": { "type": "uri" },
"web_url": { "type": "uri" }
},
- "additionalProperties": false
+ "required": [
+ "id",
+ "state",
+ "avatar_url",
+ "name",
+ "username",
+ "web_url"
+ ]
},
"user_notes_count": { "type": "integer" },
"upvotes": { "type": "integer" },
diff --git a/spec/fixtures/api/schemas/public_api/v4/members.json b/spec/fixtures/api/schemas/public_api/v4/members.json
index 38ad64ad061..695f00b0040 100644
--- a/spec/fixtures/api/schemas/public_api/v4/members.json
+++ b/spec/fixtures/api/schemas/public_api/v4/members.json
@@ -15,8 +15,7 @@
},
"required": [
"id", "name", "username", "state",
- "web_url", "access_level", "expires_at"
- ],
- "additionalProperties": false
+ "web_url", "access_level", "expires_at", "avatar_url"
+ ]
}
}
diff --git a/spec/fixtures/api/schemas/public_api/v4/notes.json b/spec/fixtures/api/schemas/public_api/v4/notes.json
index d15d2e90b05..683dcb19836 100644
--- a/spec/fixtures/api/schemas/public_api/v4/notes.json
+++ b/spec/fixtures/api/schemas/public_api/v4/notes.json
@@ -17,7 +17,9 @@
"avatar_url": { "type": "uri" },
"web_url": { "type": "uri" }
},
- "additionalProperties": false
+ "required" : [
+ "id", "name", "username", "state", "avatar_url", "web_url"
+ ]
},
"commands_changes": { "type": "object", "additionalProperties": true },
"created_at": { "type": "date" },
diff --git a/spec/fixtures/api/schemas/public_api/v4/project_repository_storage_move.json b/spec/fixtures/api/schemas/public_api/v4/project_repository_storage_move.json
new file mode 100644
index 00000000000..6f8a2ff58e5
--- /dev/null
+++ b/spec/fixtures/api/schemas/public_api/v4/project_repository_storage_move.json
@@ -0,0 +1,20 @@
+{
+ "type": "object",
+ "required": [
+ "id",
+ "created_at",
+ "state",
+ "source_storage_name",
+ "destination_storage_name",
+ "project"
+ ],
+ "properties" : {
+ "id": { "type": "integer" },
+ "created_at": { "type": "date" },
+ "state": { "type": "string" },
+ "source_storage_name": { "type": "string" },
+ "destination_storage_name": { "type": "string" },
+ "project": { "type": "object" }
+ },
+ "additionalProperties": false
+}
diff --git a/spec/fixtures/api/schemas/public_api/v4/project_repository_storage_moves.json b/spec/fixtures/api/schemas/public_api/v4/project_repository_storage_moves.json
new file mode 100644
index 00000000000..b2de185fbfe
--- /dev/null
+++ b/spec/fixtures/api/schemas/public_api/v4/project_repository_storage_moves.json
@@ -0,0 +1,6 @@
+{
+ "type": "array",
+ "items": {
+ "$ref": "./project_repository_storage_move.json"
+ }
+}
diff --git a/spec/fixtures/api/schemas/public_api/v4/snippets.json b/spec/fixtures/api/schemas/public_api/v4/snippets.json
index d13d703e063..7baa24a6f1f 100644
--- a/spec/fixtures/api/schemas/public_api/v4/snippets.json
+++ b/spec/fixtures/api/schemas/public_api/v4/snippets.json
@@ -10,6 +10,7 @@
"description": { "type": ["string", "null"] },
"visibility": { "type": "string" },
"web_url": { "type": "string" },
+ "raw_url": { "type": "string" },
"created_at": { "type": "date" },
"updated_at": { "type": "date" },
"author": {
@@ -22,12 +23,14 @@
"avatar_url": { "type": "uri" },
"web_url": { "type": "uri" }
},
- "additionalProperties": false
+ "required" : [
+ "id", "name", "username", "state", "avatar_url", "web_url"
+ ]
}
},
"required": [
"id", "title", "file_name", "description", "web_url",
- "created_at", "updated_at", "author"
+ "created_at", "updated_at", "author", "raw_url"
],
"additionalProperties": false
}
diff --git a/spec/fixtures/config/mail_room_enabled.yml b/spec/fixtures/config/mail_room_enabled.yml
index e1f4c2f44de..535528b32ca 100644
--- a/spec/fixtures/config/mail_room_enabled.yml
+++ b/spec/fixtures/config/mail_room_enabled.yml
@@ -9,6 +9,7 @@ test:
ssl: true
start_tls: false
mailbox: "inbox"
+ expunge_deleted: true
service_desk_email:
enabled: true
@@ -20,3 +21,4 @@ test:
ssl: true
start_tls: false
mailbox: "inbox"
+ expunge_deleted: true
diff --git a/spec/fixtures/config/redis_cache_new_format_host.yml b/spec/fixtures/config/redis_cache_new_format_host.yml
index a24f3716391..02b9e7384ac 100644
--- a/spec/fixtures/config/redis_cache_new_format_host.yml
+++ b/spec/fixtures/config/redis_cache_new_format_host.yml
@@ -7,7 +7,7 @@ development:
host: localhost
port: 26380 # point to sentinel, not to redis port
-
- host: slave2
+ host: replica2
port: 26380 # point to sentinel, not to redis port
test:
url: redis://:mynewpassword@localhost:6380/10
@@ -16,14 +16,14 @@ test:
host: localhost
port: 26380 # point to sentinel, not to redis port
-
- host: slave2
+ host: replica2
port: 26380 # point to sentinel, not to redis port
production:
url: redis://:mynewpassword@localhost:6380/10
sentinels:
-
- host: slave1
+ host: replica1
port: 26380 # point to sentinel, not to redis port
-
- host: slave2
+ host: replica2
port: 26380 # point to sentinel, not to redis port
diff --git a/spec/fixtures/config/redis_new_format_host.yml b/spec/fixtures/config/redis_new_format_host.yml
index 8d134d467e9..dc8d74c63fa 100644
--- a/spec/fixtures/config/redis_new_format_host.yml
+++ b/spec/fixtures/config/redis_new_format_host.yml
@@ -7,7 +7,7 @@ development:
host: localhost
port: 26379 # point to sentinel, not to redis port
-
- host: slave2
+ host: replica2
port: 26379 # point to sentinel, not to redis port
test:
url: redis://:mynewpassword@localhost:6379/99
@@ -16,14 +16,14 @@ test:
host: localhost
port: 26379 # point to sentinel, not to redis port
-
- host: slave2
+ host: replica2
port: 26379 # point to sentinel, not to redis port
production:
url: redis://:mynewpassword@localhost:6379/99
sentinels:
-
- host: slave1
+ host: replica1
port: 26379 # point to sentinel, not to redis port
-
- host: slave2
+ host: replica2
port: 26379 # point to sentinel, not to redis port
diff --git a/spec/fixtures/config/redis_queues_new_format_host.yml b/spec/fixtures/config/redis_queues_new_format_host.yml
index 1535584d779..bd0d82a5066 100644
--- a/spec/fixtures/config/redis_queues_new_format_host.yml
+++ b/spec/fixtures/config/redis_queues_new_format_host.yml
@@ -7,7 +7,7 @@ development:
host: localhost
port: 26381 # point to sentinel, not to redis port
-
- host: slave2
+ host: replica2
port: 26381 # point to sentinel, not to redis port
test:
url: redis://:mynewpassword@localhost:6381/11
@@ -16,14 +16,14 @@ test:
host: localhost
port: 26381 # point to sentinel, not to redis port
-
- host: slave2
+ host: replica2
port: 26381 # point to sentinel, not to redis port
production:
url: redis://:mynewpassword@localhost:6381/11
sentinels:
-
- host: slave1
+ host: replica1
port: 26381 # point to sentinel, not to redis port
-
- host: slave2
+ host: replica2
port: 26381 # point to sentinel, not to redis port
diff --git a/spec/fixtures/config/redis_shared_state_new_format_host.yml b/spec/fixtures/config/redis_shared_state_new_format_host.yml
index 1180b2b4a82..1c690567ae9 100644
--- a/spec/fixtures/config/redis_shared_state_new_format_host.yml
+++ b/spec/fixtures/config/redis_shared_state_new_format_host.yml
@@ -7,7 +7,7 @@ development:
host: localhost
port: 26382 # point to sentinel, not to redis port
-
- host: slave2
+ host: replica2
port: 26382 # point to sentinel, not to redis port
test:
url: redis://:mynewpassword@localhost:6382/12
@@ -16,14 +16,14 @@ test:
host: localhost
port: 26382 # point to sentinel, not to redis port
-
- host: slave2
+ host: replica2
port: 26382 # point to sentinel, not to redis port
production:
url: redis://:mynewpassword@localhost:6382/12
sentinels:
-
- host: slave1
+ host: replica1
port: 26382 # point to sentinel, not to redis port
-
- host: slave2
+ host: replica2
port: 26382 # point to sentinel, not to redis port
diff --git a/spec/fixtures/group_export.tar.gz b/spec/fixtures/group_export.tar.gz
index 5f5fd989f75..c8f3869ce51 100644
--- a/spec/fixtures/group_export.tar.gz
+++ b/spec/fixtures/group_export.tar.gz
Binary files differ
diff --git a/spec/fixtures/group_export_invalid_subrelations.tar.gz b/spec/fixtures/group_export_invalid_subrelations.tar.gz
index 6844d166260..e895e8ad9a2 100644
--- a/spec/fixtures/group_export_invalid_subrelations.tar.gz
+++ b/spec/fixtures/group_export_invalid_subrelations.tar.gz
Binary files differ
diff --git a/spec/fixtures/helm/helm_list_v2_prometheus_deployed.json.gz b/spec/fixtures/helm/helm_list_v2_prometheus_deployed.json.gz
new file mode 100644
index 00000000000..bcbbba8dc00
--- /dev/null
+++ b/spec/fixtures/helm/helm_list_v2_prometheus_deployed.json.gz
Binary files differ
diff --git a/spec/fixtures/helm/helm_list_v2_prometheus_failed.json.gz b/spec/fixtures/helm/helm_list_v2_prometheus_failed.json.gz
new file mode 100644
index 00000000000..0b39b42bdfa
--- /dev/null
+++ b/spec/fixtures/helm/helm_list_v2_prometheus_failed.json.gz
Binary files differ
diff --git a/spec/fixtures/helm/helm_list_v2_prometheus_missing.json.gz b/spec/fixtures/helm/helm_list_v2_prometheus_missing.json.gz
new file mode 100644
index 00000000000..20cac36287b
--- /dev/null
+++ b/spec/fixtures/helm/helm_list_v2_prometheus_missing.json.gz
Binary files differ
diff --git a/spec/fixtures/legacy_group_export.tar.gz b/spec/fixtures/legacy_group_export.tar.gz
new file mode 100644
index 00000000000..5f5fd989f75
--- /dev/null
+++ b/spec/fixtures/legacy_group_export.tar.gz
Binary files differ
diff --git a/spec/fixtures/legacy_group_export_invalid_subrelations.tar.gz b/spec/fixtures/legacy_group_export_invalid_subrelations.tar.gz
new file mode 100644
index 00000000000..6844d166260
--- /dev/null
+++ b/spec/fixtures/legacy_group_export_invalid_subrelations.tar.gz
Binary files differ
diff --git a/spec/fixtures/legacy_symlink_export.tar.gz b/spec/fixtures/legacy_symlink_export.tar.gz
new file mode 100644
index 00000000000..f295f69c56c
--- /dev/null
+++ b/spec/fixtures/legacy_symlink_export.tar.gz
Binary files differ
diff --git a/spec/fixtures/lib/elasticsearch/query.json b/spec/fixtures/lib/elasticsearch/query.json
index 75164a7439f..86431bac572 100644
--- a/spec/fixtures/lib/elasticsearch/query.json
+++ b/spec/fixtures/lib/elasticsearch/query.json
@@ -26,7 +26,7 @@
}
},
{
- "offset": {
+ "log.offset": {
"order": "desc"
}
}
diff --git a/spec/fixtures/lib/elasticsearch/query_with_container.json b/spec/fixtures/lib/elasticsearch/query_with_container.json
index 11bc653441c..3cbe2e814b1 100644
--- a/spec/fixtures/lib/elasticsearch/query_with_container.json
+++ b/spec/fixtures/lib/elasticsearch/query_with_container.json
@@ -33,7 +33,7 @@
}
},
{
- "offset": {
+ "log.offset": {
"order": "desc"
}
}
diff --git a/spec/fixtures/lib/elasticsearch/query_with_cursor.json b/spec/fixtures/lib/elasticsearch/query_with_cursor.json
index c5b81e97d3c..da697b0b081 100644
--- a/spec/fixtures/lib/elasticsearch/query_with_cursor.json
+++ b/spec/fixtures/lib/elasticsearch/query_with_cursor.json
@@ -26,7 +26,7 @@
}
},
{
- "offset": {
+ "log.offset": {
"order": "desc"
}
}
diff --git a/spec/fixtures/lib/elasticsearch/query_with_end_time.json b/spec/fixtures/lib/elasticsearch/query_with_end_time.json
index 226e0f115e7..dca08382cd8 100644
--- a/spec/fixtures/lib/elasticsearch/query_with_end_time.json
+++ b/spec/fixtures/lib/elasticsearch/query_with_end_time.json
@@ -35,7 +35,7 @@
}
},
{
- "offset": {
+ "log.offset": {
"order": "desc"
}
}
diff --git a/spec/fixtures/lib/elasticsearch/query_with_filebeat_6.json b/spec/fixtures/lib/elasticsearch/query_with_filebeat_6.json
new file mode 100644
index 00000000000..75164a7439f
--- /dev/null
+++ b/spec/fixtures/lib/elasticsearch/query_with_filebeat_6.json
@@ -0,0 +1,40 @@
+{
+ "query": {
+ "bool": {
+ "must": [
+ {
+ "match_phrase": {
+ "kubernetes.pod.name": {
+ "query": "production-6866bc8974-m4sk4"
+ }
+ }
+ },
+ {
+ "match_phrase": {
+ "kubernetes.namespace": {
+ "query": "autodevops-deploy-9-production"
+ }
+ }
+ }
+ ]
+ }
+ },
+ "sort": [
+ {
+ "@timestamp": {
+ "order": "desc"
+ }
+ },
+ {
+ "offset": {
+ "order": "desc"
+ }
+ }
+ ],
+ "_source": [
+ "@timestamp",
+ "message",
+ "kubernetes.pod.name"
+ ],
+ "size": 500
+}
diff --git a/spec/fixtures/lib/elasticsearch/query_with_search.json b/spec/fixtures/lib/elasticsearch/query_with_search.json
index ca63c12f3b8..ab5c0ef13c2 100644
--- a/spec/fixtures/lib/elasticsearch/query_with_search.json
+++ b/spec/fixtures/lib/elasticsearch/query_with_search.json
@@ -35,7 +35,7 @@
}
},
{
- "offset": {
+ "log.offset": {
"order": "desc"
}
}
diff --git a/spec/fixtures/lib/elasticsearch/query_with_start_time.json b/spec/fixtures/lib/elasticsearch/query_with_start_time.json
index cb3e37de8a7..479e4b74cdf 100644
--- a/spec/fixtures/lib/elasticsearch/query_with_start_time.json
+++ b/spec/fixtures/lib/elasticsearch/query_with_start_time.json
@@ -35,7 +35,7 @@
}
},
{
- "offset": {
+ "log.offset": {
"order": "desc"
}
}
diff --git a/spec/fixtures/lib/elasticsearch/query_with_times.json b/spec/fixtures/lib/elasticsearch/query_with_times.json
index 91d28b28842..8bb0109a053 100644
--- a/spec/fixtures/lib/elasticsearch/query_with_times.json
+++ b/spec/fixtures/lib/elasticsearch/query_with_times.json
@@ -36,7 +36,7 @@
}
},
{
- "offset": {
+ "log.offset": {
"order": "desc"
}
}
diff --git a/spec/fixtures/lib/gitlab/import_export/complex/project.json b/spec/fixtures/lib/gitlab/import_export/complex/project.json
index 4bf72fe6912..0785da9c1bf 100644
--- a/spec/fixtures/lib/gitlab/import_export/complex/project.json
+++ b/spec/fixtures/lib/gitlab/import_export/complex/project.json
@@ -7394,26 +7394,6 @@
"category": "common",
"default": false,
"wiki_page_events": true
- },
- {
- "id": 101,
- "title": "JenkinsDeprecated",
- "project_id": 5,
- "created_at": "2016-06-14T15:01:51.031Z",
- "updated_at": "2016-06-14T15:01:51.031Z",
- "active": false,
- "properties": {},
- "template": false,
- "push_events": true,
- "issues_events": true,
- "merge_requests_events": true,
- "tag_push_events": true,
- "note_events": true,
- "job_events": true,
- "category": "common",
- "default": false,
- "wiki_page_events": true,
- "type": "JenkinsDeprecatedService"
}
],
"hooks": [],
@@ -7444,6 +7424,26 @@
]
}
],
+ "protected_environments": [
+ {
+ "id": 1,
+ "project_id": 9,
+ "created_at": "2017-10-19T15:36:23.466Z",
+ "updated_at": "2017-10-19T15:36:23.466Z",
+ "name": "production",
+ "deploy_access_levels": [
+ {
+ "id": 1,
+ "protected_environment_id": 1,
+ "created_at": "2017-10-19T15:36:23.466Z",
+ "updated_at": "2017-10-19T15:36:23.466Z",
+ "access_level": 40,
+ "user_id": 1,
+ "group_id": null
+ }
+ ]
+ }
+ ],
"protected_tags": [
{
"id": 1,
diff --git a/spec/fixtures/lib/gitlab/import_export/complex/tree.tar.gz b/spec/fixtures/lib/gitlab/import_export/complex/tree.tar.gz
deleted file mode 100644
index feb1a70a89e..00000000000
--- a/spec/fixtures/lib/gitlab/import_export/complex/tree.tar.gz
+++ /dev/null
Binary files differ
diff --git a/spec/fixtures/lib/gitlab/import_export/complex/tree/project.json b/spec/fixtures/lib/gitlab/import_export/complex/tree/project.json
new file mode 100644
index 00000000000..203b0264f9e
--- /dev/null
+++ b/spec/fixtures/lib/gitlab/import_export/complex/tree/project.json
@@ -0,0 +1 @@
+{"description":"Nisi et repellendus ut enim quo accusamus vel magnam.","import_type":"gitlab_project","creator_id":123,"visibility_level":10,"archived":false,"deploy_keys":[],"hooks":[]}
diff --git a/spec/fixtures/lib/gitlab/import_export/complex/tree/project/auto_devops.ndjson b/spec/fixtures/lib/gitlab/import_export/complex/tree/project/auto_devops.ndjson
new file mode 100644
index 00000000000..85ae0843ce6
--- /dev/null
+++ b/spec/fixtures/lib/gitlab/import_export/complex/tree/project/auto_devops.ndjson
@@ -0,0 +1 @@
+{"id":1,"created_at":"2017-10-19T15:36:23.466Z","updated_at":"2017-10-19T15:36:23.466Z","enabled":null,"deploy_strategy":"continuous"}
diff --git a/spec/fixtures/lib/gitlab/import_export/complex/tree/project/boards.ndjson b/spec/fixtures/lib/gitlab/import_export/complex/tree/project/boards.ndjson
new file mode 100644
index 00000000000..ef18af69c9b
--- /dev/null
+++ b/spec/fixtures/lib/gitlab/import_export/complex/tree/project/boards.ndjson
@@ -0,0 +1 @@
+{"id":29,"project_id":49,"created_at":"2019-06-06T14:01:06.204Z","updated_at":"2019-06-06T14:22:37.045Z","name":"TestBoardABC","milestone_id":null,"group_id":null,"weight":null,"lists":[{"id":59,"board_id":29,"label_id":null,"list_type":"backlog","position":null,"created_at":"2019-06-06T14:01:06.214Z","updated_at":"2019-06-06T14:01:06.214Z","user_id":null,"milestone_id":null},{"id":61,"board_id":29,"label_id":20,"list_type":"label","position":0,"created_at":"2019-06-06T14:01:43.197Z","updated_at":"2019-06-06T14:01:43.197Z","user_id":null,"milestone_id":null,"label":{"id":20,"title":"testlabel","color":"#0033CC","project_id":49,"created_at":"2019-06-06T14:01:19.698Z","updated_at":"2019-06-06T14:01:19.698Z","template":false,"description":null,"group_id":null,"type":"ProjectLabel","priorities":[]}},{"id":60,"board_id":29,"label_id":null,"list_type":"closed","position":null,"created_at":"2019-06-06T14:01:06.221Z","updated_at":"2019-06-06T14:01:06.221Z","user_id":null,"milestone_id":null}]}
diff --git a/spec/fixtures/lib/gitlab/import_export/complex/tree/project/ci_cd_settings.ndjson b/spec/fixtures/lib/gitlab/import_export/complex/tree/project/ci_cd_settings.ndjson
new file mode 100644
index 00000000000..bf4165fe729
--- /dev/null
+++ b/spec/fixtures/lib/gitlab/import_export/complex/tree/project/ci_cd_settings.ndjson
@@ -0,0 +1 @@
+{"group_runners_enabled":false}
diff --git a/spec/fixtures/lib/gitlab/import_export/complex/tree/project/ci_pipelines.ndjson b/spec/fixtures/lib/gitlab/import_export/complex/tree/project/ci_pipelines.ndjson
new file mode 100644
index 00000000000..a9d04ec5d6d
--- /dev/null
+++ b/spec/fixtures/lib/gitlab/import_export/complex/tree/project/ci_pipelines.ndjson
@@ -0,0 +1,7 @@
+{"id":19,"project_id":5,"ref":"master","sha":"2ea1f3dec713d940208fb5ce4a38765ecb5d3f73","before_sha":null,"push_data":null,"created_at":"2016-03-22T15:20:35.763Z","updated_at":"2016-03-22T15:20:35.763Z","tag":null,"yaml_errors":null,"committed_at":null,"status":"failed","started_at":null,"finished_at":null,"duration":null,"stages":[{"id":24,"project_id":5,"pipeline_id":40,"name":"test","status":1,"created_at":"2016-03-22T15:44:44.772Z","updated_at":"2016-03-29T06:44:44.634Z","statuses":[{"id":79,"project_id":5,"status":"failed","finished_at":"2016-03-29T06:28:12.695Z","trace":"Sed culpa est et facere saepe vel id ab. Quas temporibus aut similique dolorem consequatur corporis aut praesentium. Cum officia molestiae sit earum excepturi.\n\nSint possimus aut ratione quia. Quis nesciunt ratione itaque illo. Tenetur est dolor assumenda possimus voluptatem quia minima. Accusamus reprehenderit ut et itaque non reiciendis incidunt.\n\nRerum suscipit quibusdam dolore nam omnis. Consequatur ipsa nihil ut enim blanditiis delectus. Nulla quis hic occaecati mollitia qui placeat. Quo rerum sed perferendis a accusantium consequatur commodi ut. Sit quae et cumque vel eius tempora nostrum.\n\nUllam dolorem et itaque sint est. Ea molestias quia provident dolorem vitae error et et. Ea expedita officiis iste non. Qui vitae odit saepe illum. Dolores enim ratione deserunt tempore expedita amet non neque.\n\nEligendi asperiores voluptatibus omnis repudiandae expedita distinctio qui aliquid. Autem aut doloremque distinctio ab. Nostrum sapiente repudiandae aspernatur ea et quae voluptas. Officiis perspiciatis nisi laudantium asperiores error eligendi ab. Eius quia amet magni omnis exercitationem voluptatum et.\n\nVoluptatem ullam labore quas dicta est ex voluptas. Pariatur ea modi voluptas consequatur dolores perspiciatis similique. Numquam in distinctio perspiciatis ut qui earum. Quidem omnis mollitia facere aut beatae. Ea est iure et voluptatem.","created_at":"2016-03-22T15:20:35.950Z","updated_at":"2016-03-29T06:28:12.696Z","started_at":null,"runner_id":null,"coverage":null,"commit_id":40,"commands":"$ build command","job_id":null,"name":"test build 1","deploy":false,"options":null,"allow_failure":false,"stage":"test","trigger_request_id":null,"stage_idx":1,"tag":null,"ref":"master","user_id":null,"target_url":null,"description":null,"erased_by_id":null,"erased_at":null},{"id":80,"project_id":5,"status":"success","finished_at":null,"trace":"Impedit et optio nemo ipsa. Non ad non quis ut sequi laudantium omnis velit. Corporis a enim illo eos. Quia totam tempore inventore ad est.\n\nNihil recusandae cupiditate eaque voluptatem molestias sint. Consequatur id voluptatem cupiditate harum. Consequuntur iusto quaerat reiciendis aut autem libero est. Quisquam dolores veritatis rerum et sint maxime ullam libero. Id quas porro ut perspiciatis rem amet vitae.\n\nNemo inventore minus blanditiis magnam. Modi consequuntur nostrum aut voluptatem ex. Sunt rerum rem optio mollitia qui aliquam officiis officia. Aliquid eos et id aut minus beatae reiciendis.\n\nDolores non in temporibus dicta. Fugiat voluptatem est aspernatur expedita voluptatum nam qui. Quia et eligendi sit quae sint tempore exercitationem eos. Est sapiente corrupti quidem at. Qui magni odio repudiandae saepe tenetur optio dolore.\n\nEos placeat soluta at dolorem adipisci provident. Quo commodi id reprehenderit possimus quo tenetur. Ipsum et quae eligendi laborum. Et qui nesciunt at quasi quidem voluptatem cum rerum. Excepturi non facilis aut sunt vero sed.\n\nQui explicabo ratione ut eligendi recusandae. Quis quasi quas molestiae consequatur voluptatem et voluptatem. Ex repellat saepe occaecati aperiam ea eveniet dignissimos facilis.","created_at":"2016-03-22T15:20:35.966Z","updated_at":"2016-03-22T15:20:35.966Z","started_at":null,"runner_id":null,"coverage":null,"commit_id":40,"commands":"$ build command","job_id":null,"name":"test build 2","deploy":false,"options":null,"allow_failure":false,"stage":"test","trigger_request_id":null,"stage_idx":1,"tag":null,"ref":"master","user_id":null,"target_url":null,"description":null,"erased_by_id":null,"erased_at":null}]}]}
+{"id":20,"project_id":5,"ref":"master","sha":"ce84140e8b878ce6e7c4d298c7202ff38170e3ac","before_sha":null,"push_data":null,"created_at":"2016-03-22T15:20:35.763Z","updated_at":"2016-03-22T15:20:35.763Z","tag":false,"yaml_errors":null,"committed_at":null,"status":"failed","started_at":null,"finished_at":null,"duration":null,"stages":[],"source":"external_pull_request_event","external_pull_request":{"id":3,"pull_request_iid":4,"source_branch":"feature","target_branch":"master","source_repository":"the-repository","target_repository":"the-repository","source_sha":"ce84140e8b878ce6e7c4d298c7202ff38170e3ac","target_sha":"a09386439ca39abe575675ffd4b89ae824fec22f","status":"open","created_at":"2016-03-22T15:20:35.763Z","updated_at":"2016-03-22T15:20:35.763Z"}}
+{"id":26,"project_id":5,"ref":"master","sha":"048721d90c449b244b7b4c53a9186b04330174ec","before_sha":null,"push_data":null,"created_at":"2016-03-22T15:20:35.757Z","updated_at":"2016-03-22T15:20:35.757Z","tag":false,"yaml_errors":null,"committed_at":null,"status":"failed","started_at":null,"finished_at":null,"duration":null,"source":"merge_request_event","merge_request_id":27,"stages":[{"id":21,"project_id":5,"pipeline_id":37,"name":"test","status":1,"created_at":"2016-03-22T15:44:44.772Z","updated_at":"2016-03-29T06:44:44.634Z","statuses":[{"id":74,"project_id":5,"status":"success","finished_at":null,"trace":"Ad ut quod repudiandae iste dolor doloribus. Adipisci consequuntur deserunt omnis quasi eveniet et sed fugit. Aut nemo omnis molestiae impedit ex consequatur ducimus. Voluptatum exercitationem quia aut est et hic dolorem.\n\nQuasi repellendus et eaque magni eum facilis. Dolorem aperiam nam nihil pariatur praesentium ad aliquam. Commodi enim et eos tenetur. Odio voluptatibus laboriosam mollitia rerum exercitationem magnam consequuntur. Tenetur ea vel eum corporis.\n\nVoluptatibus optio in aliquid est voluptates. Ad a ut ab placeat vero blanditiis. Earum aspernatur quia beatae expedita voluptatem dignissimos provident. Quis minima id nemo ut aut est veritatis provident.\n\nRerum voluptatem quidem eius maiores magnam veniam. Voluptatem aperiam aut voluptate et nulla deserunt voluptas. Quaerat aut accusantium laborum est dolorem architecto reiciendis. Aliquam asperiores doloribus omnis maxime enim nesciunt. Eum aut rerum repellendus debitis et ut eius.\n\nQuaerat assumenda ea sit consequatur autem in. Cum eligendi voluptatem quo sed. Ut fuga iusto cupiditate autem sint.\n\nOfficia totam officiis architecto corporis molestiae amet ut. Tempora sed dolorum rerum omnis voluptatem accusantium sit eum. Quia debitis ipsum quidem aliquam inventore sunt consequatur qui.","created_at":"2016-03-22T15:20:35.846Z","updated_at":"2016-03-22T15:20:35.846Z","started_at":null,"runner_id":null,"coverage":null,"commit_id":37,"commands":"$ build command","job_id":null,"name":"test build 2","deploy":false,"options":null,"allow_failure":false,"stage":"test","trigger_request_id":null,"stage_idx":1,"tag":null,"ref":"master","user_id":null,"target_url":null,"description":null,"erased_by_id":null,"erased_at":null},{"id":73,"project_id":5,"status":"canceled","finished_at":null,"trace":null,"created_at":"2016-03-22T15:20:35.842Z","updated_at":"2016-03-22T15:20:35.842Z","started_at":null,"runner_id":null,"coverage":null,"commit_id":37,"commands":"$ build command","job_id":null,"name":"test build 1","deploy":false,"options":null,"allow_failure":false,"stage":"test","trigger_request_id":null,"stage_idx":1,"tag":null,"ref":"master","user_id":null,"target_url":null,"description":null,"erased_by_id":null,"erased_at":null}]}],"merge_request":{"id":27,"target_branch":"feature","source_branch":"feature_conflict","source_project_id":2147483547,"author_id":1,"assignee_id":null,"title":"MR1","created_at":"2016-06-14T15:02:36.568Z","updated_at":"2016-06-14T15:02:56.815Z","state":"opened","merge_status":"unchecked","target_project_id":5,"iid":9,"description":null,"position":0,"updated_by_id":null,"merge_error":null,"diff_head_sha":"HEAD","source_branch_sha":"ABCD","target_branch_sha":"DCBA","merge_params":{"force_remove_source_branch":null}}}
+{"id":36,"project_id":5,"ref":null,"sha":"sha-notes","before_sha":null,"push_data":null,"created_at":"2016-03-22T15:20:35.755Z","updated_at":"2016-03-22T15:20:35.755Z","tag":null,"yaml_errors":null,"committed_at":null,"status":"failed","started_at":null,"finished_at":null,"user_id":2147483547,"duration":null,"source":"push","merge_request_id":null,"notes":[{"id":2147483547,"note":"Natus rerum qui dolorem dolorum voluptas.","noteable_type":"Commit","author_id":1,"created_at":"2016-03-22T15:19:59.469Z","updated_at":"2016-03-22T15:19:59.469Z","project_id":5,"attachment":{"url":null},"line_code":null,"commit_id":"be93687618e4b132087f430a4d8fc3a609c9b77c","noteable_id":36,"system":false,"st_diff":null,"updated_by_id":null,"author":{"name":"Administrator"}}],"stages":[{"id":11,"project_id":5,"pipeline_id":36,"name":"test","status":1,"created_at":"2016-03-22T15:44:44.772Z","updated_at":"2016-03-29T06:44:44.634Z","statuses":[{"id":71,"project_id":5,"status":"failed","finished_at":"2016-03-29T06:28:12.630Z","trace":null,"created_at":"2016-03-22T15:20:35.772Z","updated_at":"2016-03-29T06:28:12.634Z","started_at":null,"runner_id":null,"coverage":null,"commit_id":36,"commands":"$ build command","job_id":null,"name":"test build 1","deploy":false,"options":{"image":"busybox:latest"},"allow_failure":false,"stage":"test","trigger_request_id":null,"stage_idx":1,"stage_id":11,"tag":null,"ref":"master","user_id":null,"target_url":null,"description":null,"erased_by_id":null,"erased_at":null,"type":"Ci::Build","token":"abcd","artifacts_file_store":1,"artifacts_metadata_store":1,"artifacts_size":10},{"id":72,"project_id":5,"status":"success","finished_at":null,"trace":"Porro ea qui ut dolores. Labore ab nemo explicabo aspernatur quis voluptates corporis. Et quasi delectus est sit aperiam perspiciatis asperiores. Repudiandae cum aut consectetur accusantium officia sunt.\n\nQuidem dolore iusto quaerat ut aut inventore et molestiae. Libero voluptates atque nemo qui. Nulla temporibus ipsa similique facere.\n\nAliquam ipsam perferendis qui fugit accusantium omnis id voluptatum. Dignissimos aliquid dicta eos voluptatem assumenda quia. Sed autem natus unde dolor et non nisi et. Consequuntur nihil consequatur rerum est.\n\nSimilique neque est iste ducimus qui fuga cupiditate. Libero autem est aut fuga. Consectetur natus quis non ducimus ut dolore. Magni voluptatibus eius et maxime aut.\n\nAd officiis tempore voluptate vitae corrupti explicabo labore est. Consequatur expedita et sunt nihil aut. Deleniti porro iusto molestiae et beatae.\n\nDeleniti modi nulla qui et labore sequi corrupti. Qui voluptatem assumenda eum cupiditate et. Nesciunt ipsam ut ea possimus eum. Consectetur quidem suscipit atque dolore itaque voluptatibus et cupiditate.","created_at":"2016-03-22T15:20:35.777Z","updated_at":"2016-03-22T15:20:35.777Z","started_at":null,"runner_id":null,"coverage":null,"commit_id":36,"commands":"$ deploy command","job_id":null,"name":"test build 2","deploy":false,"options":null,"allow_failure":false,"stage":"deploy","trigger_request_id":null,"stage_idx":1,"stage_id":12,"tag":null,"ref":"master","user_id":null,"target_url":null,"description":null,"erased_by_id":null,"erased_at":null}]},{"id":12,"project_id":5,"pipeline_id":36,"name":"deploy","status":2,"created_at":"2016-03-22T15:45:45.772Z","updated_at":"2016-03-29T06:45:45.634Z"}]}
+{"id":38,"iid":1,"project_id":5,"ref":"master","sha":"5f923865dde3436854e9ceb9cdb7815618d4e849","before_sha":null,"push_data":null,"created_at":"2016-03-22T15:20:35.759Z","updated_at":"2016-03-22T15:20:35.759Z","tag":null,"yaml_errors":null,"committed_at":null,"status":"failed","started_at":null,"finished_at":null,"duration":null,"stages":[{"id":22,"project_id":5,"pipeline_id":38,"name":"test","status":1,"created_at":"2016-03-22T15:44:44.772Z","updated_at":"2016-03-29T06:44:44.634Z","statuses":[{"id":76,"project_id":5,"status":"success","finished_at":null,"trace":"Et rerum quia ea cumque ut modi non. Libero eaque ipsam architecto maiores expedita deleniti. Ratione quia qui est id.\n\nQuod sit officiis sed unde inventore veniam quisquam velit. Ea harum cum quibusdam quisquam minima quo possimus non. Temporibus itaque aliquam aut rerum veritatis at.\n\nMagnam ipsum eius recusandae qui quis sit maiores eum. Et animi iusto aut itaque. Doloribus harum deleniti nobis accusantium et libero.\n\nRerum fuga perferendis magni commodi officiis id repudiandae. Consequatur ratione consequatur suscipit facilis sunt iure est dicta. Qui unde quasi facilis et quae nesciunt. Magnam iste et nobis officiis tenetur. Aspernatur quo et temporibus non in.\n\nNisi rerum velit est ad enim sint molestiae consequuntur. Quaerat nisi nesciunt quasi officiis. Possimus non blanditiis laborum quos.\n\nRerum laudantium facere animi qui. Ipsa est iusto magnam nihil. Enim omnis occaecati non dignissimos ut recusandae eum quasi. Qui maxime dolor et nemo voluptates incidunt quia.","created_at":"2016-03-22T15:20:35.882Z","updated_at":"2016-03-22T15:20:35.882Z","started_at":null,"runner_id":null,"coverage":null,"commit_id":38,"commands":"$ build command","job_id":null,"name":"test build 2","deploy":false,"options":null,"allow_failure":false,"stage":"test","trigger_request_id":null,"stage_idx":1,"tag":null,"ref":"master","user_id":null,"target_url":null,"description":null,"erased_by_id":null,"erased_at":null},{"id":75,"project_id":5,"status":"failed","finished_at":null,"trace":"Sed et iste recusandae dicta corporis. Sunt alias porro fugit sunt. Fugiat omnis nihil dignissimos aperiam explicabo doloremque sit aut. Harum fugit expedita quia rerum ut consequatur laboriosam aliquam.\n\nNatus libero ut ut tenetur earum. Tempora omnis autem omnis et libero dolores illum autem. Deleniti eos sunt mollitia ipsam. Cum dolor repellendus dolorum sequi officia. Ullam sunt in aut pariatur excepturi.\n\nDolor nihil debitis et est eos. Cumque eos eum saepe ducimus autem. Alias architecto consequatur aut pariatur possimus. Aut quos aut incidunt quam velit et. Quas voluptatum ad dolorum dignissimos.\n\nUt voluptates consectetur illo et. Est commodi accusantium vel quo. Eos qui fugiat soluta porro.\n\nRatione possimus alias vel maxime sint totam est repellat. Ipsum corporis eos sint voluptatem eos odit. Temporibus libero nulla harum eligendi labore similique ratione magnam. Suscipit sequi in omnis neque.\n\nLaudantium dolor amet omnis placeat mollitia aut molestiae. Aut rerum similique ipsum quod illo quas unde. Sunt aut veritatis eos omnis porro. Rem veritatis mollitia praesentium dolorem. Consequatur sequi ad cumque earum omnis quia necessitatibus.","created_at":"2016-03-22T15:20:35.864Z","updated_at":"2016-03-22T15:20:35.864Z","started_at":null,"runner_id":null,"coverage":null,"commit_id":38,"commands":"$ build command","job_id":null,"name":"test build 1","deploy":false,"options":null,"allow_failure":false,"stage":"test","trigger_request_id":null,"stage_idx":1,"tag":null,"ref":"master","user_id":null,"target_url":null,"description":null,"erased_by_id":null,"erased_at":null}]}]}
+{"id":39,"project_id":5,"ref":"master","sha":"d2d430676773caa88cdaf7c55944073b2fd5561a","before_sha":null,"push_data":null,"created_at":"2016-03-22T15:20:35.761Z","updated_at":"2016-03-22T15:20:35.761Z","tag":null,"yaml_errors":null,"committed_at":null,"status":"failed","started_at":null,"finished_at":null,"duration":null,"stages":[{"id":23,"project_id":5,"pipeline_id":39,"name":"test","status":1,"created_at":"2016-03-22T15:44:44.772Z","updated_at":"2016-03-29T06:44:44.634Z","statuses":[{"id":78,"project_id":5,"status":"success","finished_at":null,"trace":"Dolorem deserunt quas quia error hic quo cum vel. Natus voluptatem cumque expedita numquam odit. Eos expedita nostrum corporis consequatur est recusandae.\n\nCulpa blanditiis rerum repudiandae alias voluptatem. Velit iusto est ullam consequatur doloribus porro. Corporis voluptas consectetur est veniam et quia quae.\n\nEt aut magni fuga nesciunt officiis molestias. Quaerat et nam necessitatibus qui rerum. Architecto quia officiis voluptatem laborum est recusandae. Quasi ducimus soluta odit necessitatibus labore numquam dignissimos. Quia facere sint temporibus inventore sunt nihil saepe dolorum.\n\nFacere dolores quis dolores a. Est minus nostrum nihil harum. Earum laborum et ipsum unde neque sit nemo. Corrupti est consequatur minima fugit. Illum voluptatem illo error ducimus officia qui debitis.\n\nDignissimos porro a autem harum aut. Aut id reprehenderit et exercitationem. Est et quisquam ipsa temporibus molestiae. Architecto natus dolore qui fugiat incidunt. Autem odit veniam excepturi et voluptatibus culpa ipsum eos.\n\nAmet quo quisquam dignissimos soluta modi dolores. Sint omnis eius optio corporis dolor. Eligendi animi porro quia placeat ut.","created_at":"2016-03-22T15:20:35.927Z","updated_at":"2016-03-22T15:20:35.927Z","started_at":null,"runner_id":null,"coverage":null,"commit_id":39,"commands":"$ build command","job_id":null,"name":"test build 2","deploy":false,"options":null,"allow_failure":false,"stage":"test","trigger_request_id":null,"stage_idx":1,"tag":null,"ref":"master","user_id":null,"target_url":null,"description":null,"erased_by_id":null,"erased_at":null},{"id":77,"project_id":5,"status":"failed","finished_at":null,"trace":"Rerum ut et suscipit est perspiciatis. Inventore debitis cum eius vitae. Ex incidunt id velit aut quo nisi. Laboriosam repellat deserunt eius reiciendis architecto et. Est harum quos nesciunt nisi consectetur.\n\nAlias esse omnis sint officia est consequatur in nobis. Dignissimos dolorum vel eligendi nesciunt dolores sit. Veniam mollitia ducimus et exercitationem molestiae libero sed. Atque omnis debitis laudantium voluptatibus qui. Repellendus tempore est commodi pariatur.\n\nExpedita voluptate illum est alias non. Modi nesciunt ab assumenda laborum nulla consequatur molestias doloremque. Magnam quod officia vel explicabo accusamus ut voluptatem incidunt. Rerum ut aliquid ullam saepe. Est eligendi debitis beatae blanditiis reiciendis.\n\nQui fuga sit dolores libero maiores et suscipit. Consectetur asperiores omnis minima impedit eos fugiat. Similique omnis nisi sed vero inventore ipsum aliquam exercitationem.\n\nBlanditiis magni iure dolorum omnis ratione delectus molestiae. Atque officia dolor voluptatem culpa quod. Incidunt suscipit quidem possimus veritatis non vel. Iusto aliquid et id quia quasi.\n\nVel facere velit blanditiis incidunt cupiditate sed maiores consequuntur. Quasi quia dicta consequuntur et quia voluptatem iste id. Incidunt et rerum fuga esse sint.","created_at":"2016-03-22T15:20:35.905Z","updated_at":"2016-03-22T15:20:35.905Z","started_at":null,"runner_id":null,"coverage":null,"commit_id":39,"commands":"$ build command","job_id":null,"name":"test build 1","deploy":false,"options":null,"allow_failure":false,"stage":"test","trigger_request_id":null,"stage_idx":1,"tag":null,"ref":"master","user_id":null,"target_url":null,"description":null,"erased_by_id":null,"erased_at":null}]}]}
+{"id":41,"project_id":5,"ref":"master","sha":"2ea1f3dec713d940208fb5ce4a38765ecb5d3f73","before_sha":null,"push_data":null,"created_at":"2016-03-22T15:20:35.763Z","updated_at":"2016-03-22T15:20:35.763Z","tag":null,"yaml_errors":null,"committed_at":null,"status":"failed","started_at":null,"finished_at":null,"duration":null,"stages":[]}
diff --git a/spec/fixtures/lib/gitlab/import_export/complex/tree/project/container_expiration_policy.ndjson b/spec/fixtures/lib/gitlab/import_export/complex/tree/project/container_expiration_policy.ndjson
new file mode 100644
index 00000000000..033eee9751c
--- /dev/null
+++ b/spec/fixtures/lib/gitlab/import_export/complex/tree/project/container_expiration_policy.ndjson
@@ -0,0 +1 @@
+{"created_at":"2019-12-13 13:45:04 UTC","updated_at":"2019-12-13 13:45:04 UTC","next_run_at":null,"project_id":5,"name_regex":null,"cadence":"3month","older_than":null,"keep_n":100,"enabled":false}
diff --git a/spec/fixtures/lib/gitlab/import_export/complex/tree/project/custom_attributes.ndjson b/spec/fixtures/lib/gitlab/import_export/complex/tree/project/custom_attributes.ndjson
new file mode 100644
index 00000000000..cf232f80c9b
--- /dev/null
+++ b/spec/fixtures/lib/gitlab/import_export/complex/tree/project/custom_attributes.ndjson
@@ -0,0 +1,2 @@
+{"id":1,"created_at":"2017-10-19T15:36:23.466Z","updated_at":"2017-10-19T15:36:23.466Z","project_id":5,"key":"foo","value":"foo"}
+{"id":2,"created_at":"2017-10-19T15:37:21.904Z","updated_at":"2017-10-19T15:37:21.904Z","project_id":5,"key":"bar","value":"bar"}
diff --git a/spec/fixtures/lib/gitlab/import_export/complex/tree/project/error_tracking_setting.ndjson b/spec/fixtures/lib/gitlab/import_export/complex/tree/project/error_tracking_setting.ndjson
new file mode 100644
index 00000000000..c95db1c86a7
--- /dev/null
+++ b/spec/fixtures/lib/gitlab/import_export/complex/tree/project/error_tracking_setting.ndjson
@@ -0,0 +1 @@
+{"api_url":"https://gitlab.example.com/api/0/projects/sentry-org/sentry-project","project_name":"Sentry Project","organization_name":"Sentry Org"}
diff --git a/spec/fixtures/lib/gitlab/import_export/complex/tree/project/external_pull_requests.ndjson b/spec/fixtures/lib/gitlab/import_export/complex/tree/project/external_pull_requests.ndjson
new file mode 100644
index 00000000000..718678ef5cd
--- /dev/null
+++ b/spec/fixtures/lib/gitlab/import_export/complex/tree/project/external_pull_requests.ndjson
@@ -0,0 +1 @@
+{"id":3,"pull_request_iid":4,"source_branch":"feature","target_branch":"master","source_repository":"the-repository","target_repository":"the-repository","source_sha":"ce84140e8b878ce6e7c4d298c7202ff38170e3ac","target_sha":"a09386439ca39abe575675ffd4b89ae824fec22f","status":"open","created_at":"2019-12-24T14:04:50.053Z","updated_at":"2019-12-24T14:05:18.138Z"}
diff --git a/spec/fixtures/lib/gitlab/import_export/complex/tree/project/issues.ndjson b/spec/fixtures/lib/gitlab/import_export/complex/tree/project/issues.ndjson
new file mode 100644
index 00000000000..2ebd1a78783
--- /dev/null
+++ b/spec/fixtures/lib/gitlab/import_export/complex/tree/project/issues.ndjson
@@ -0,0 +1,10 @@
+{"id":40,"title":"Voluptatem","author_id":22,"project_id":5,"created_at":"2016-06-14T15:02:08.340Z","updated_at":"2016-06-14T15:02:47.967Z","position":0,"branch_name":null,"description":"Aliquam enim illo et possimus.","state":"opened","iid":10,"updated_by_id":null,"confidential":false,"due_date":null,"moved_to_id":null,"test_ee_field":"test","issue_assignees":[{"user_id":1,"issue_id":40},{"user_id":15,"issue_id":40},{"user_id":16,"issue_id":40},{"user_id":16,"issue_id":40},{"user_id":6,"issue_id":40}],"award_emoji":[{"id":1,"name":"musical_keyboard","user_id":1,"awardable_type":"Issue","awardable_id":40,"created_at":"2020-01-07T11:55:22.234Z","updated_at":"2020-01-07T11:55:22.234Z"}],"zoom_meetings":[{"id":1,"project_id":5,"issue_id":40,"url":"https://zoom.us/j/123456789","issue_status":1,"created_at":"2016-06-14T15:02:04.418Z","updated_at":"2016-06-14T15:02:04.418Z"}],"milestone":{"id":1,"title":"test milestone","project_id":8,"description":"test milestone","due_date":null,"created_at":"2016-06-14T15:02:04.415Z","updated_at":"2016-06-14T15:02:04.415Z","state":"active","iid":1,"events":[{"id":487,"target_type":"Milestone","target_id":1,"project_id":46,"created_at":"2016-06-14T15:02:04.418Z","updated_at":"2016-06-14T15:02:04.418Z","action":1,"author_id":18}]},"label_links":[{"id":2,"label_id":2,"target_id":40,"target_type":"Issue","created_at":"2016-07-22T08:57:02.840Z","updated_at":"2016-07-22T08:57:02.840Z","label":{"id":2,"title":"test2","color":"#428bca","project_id":8,"created_at":"2016-07-22T08:55:44.161Z","updated_at":"2016-07-22T08:55:44.161Z","template":false,"description":"","type":"ProjectLabel"}},{"id":3,"label_id":3,"target_id":40,"target_type":"Issue","created_at":"2016-07-22T08:57:02.841Z","updated_at":"2016-07-22T08:57:02.841Z","label":{"id":3,"title":"test3","color":"#428bca","group_id":8,"created_at":"2016-07-22T08:55:44.161Z","updated_at":"2016-07-22T08:55:44.161Z","template":false,"description":"","project_id":null,"type":"GroupLabel","priorities":[{"id":1,"project_id":5,"label_id":1,"priority":1,"created_at":"2016-10-18T09:35:43.338Z","updated_at":"2016-10-18T09:35:43.338Z"}]}}],"notes":[{"id":351,"note":"Quo reprehenderit aliquam qui dicta impedit cupiditate eligendi.","note_html":"<p>something else entirely</p>","cached_markdown_version":917504,"noteable_type":"Issue","author_id":26,"created_at":"2016-06-14T15:02:47.770Z","updated_at":"2016-06-14T15:02:47.770Z","project_id":5,"attachment":{"url":null},"line_code":null,"commit_id":null,"noteable_id":40,"system":false,"st_diff":null,"updated_by_id":null,"author":{"name":"User 4"},"events":[],"award_emoji":[{"id":1,"name":"clapper","user_id":1,"awardable_type":"Note","awardable_id":351,"created_at":"2020-01-07T11:55:22.234Z","updated_at":"2020-01-07T11:55:22.234Z"}]},{"id":352,"note":"Est reprehenderit quas aut aspernatur autem recusandae voluptatem.","noteable_type":"Issue","author_id":25,"created_at":"2016-06-14T15:02:47.795Z","updated_at":"2016-06-14T15:02:47.795Z","project_id":5,"attachment":{"url":null},"line_code":null,"commit_id":null,"noteable_id":40,"system":false,"st_diff":null,"updated_by_id":null,"author":{"name":"User 3"},"events":[]},{"id":353,"note":"Perspiciatis suscipit voluptates in eius nihil.","noteable_type":"Issue","author_id":22,"created_at":"2016-06-14T15:02:47.823Z","updated_at":"2016-06-14T15:02:47.823Z","project_id":5,"attachment":{"url":null},"line_code":null,"commit_id":null,"noteable_id":40,"system":false,"st_diff":null,"updated_by_id":null,"author":{"name":"User 0"},"events":[]},{"id":354,"note":"Aut vel voluptas corrupti nisi provident laboriosam magnam aut.","noteable_type":"Issue","author_id":20,"created_at":"2016-06-14T15:02:47.850Z","updated_at":"2016-06-14T15:02:47.850Z","project_id":5,"attachment":{"url":null},"line_code":null,"commit_id":null,"noteable_id":40,"system":false,"st_diff":null,"updated_by_id":null,"author":{"name":"Ottis Schuster II"},"events":[]},{"id":355,"note":"Officia dolore consequatur in saepe cum magni.","noteable_type":"Issue","author_id":16,"created_at":"2016-06-14T15:02:47.876Z","updated_at":"2016-06-14T15:02:47.876Z","project_id":5,"attachment":{"url":null},"line_code":null,"commit_id":null,"noteable_id":40,"system":false,"st_diff":null,"updated_by_id":null,"author":{"name":"Rhett Emmerich IV"},"events":[]},{"id":356,"note":"Cum ipsum rem voluptas eaque et ea.","noteable_type":"Issue","author_id":15,"created_at":"2016-06-14T15:02:47.908Z","updated_at":"2016-06-14T15:02:47.908Z","project_id":5,"attachment":{"url":null},"line_code":null,"commit_id":null,"noteable_id":40,"system":false,"st_diff":null,"updated_by_id":null,"author":{"name":"Burdette Bernier"},"events":[]},{"id":357,"note":"Recusandae excepturi asperiores suscipit autem nostrum.","noteable_type":"Issue","author_id":6,"created_at":"2016-06-14T15:02:47.937Z","updated_at":"2016-06-14T15:02:47.937Z","project_id":5,"attachment":{"url":null},"line_code":null,"commit_id":null,"noteable_id":40,"system":false,"st_diff":null,"updated_by_id":null,"author":{"name":"Ari Wintheiser"},"events":[]},{"id":358,"note":"Et hic est id similique et non nesciunt voluptate.","noteable_type":"Issue","author_id":1,"created_at":"2016-06-14T15:02:47.965Z","updated_at":"2016-06-14T15:02:47.965Z","project_id":5,"attachment":{"url":null},"line_code":null,"commit_id":null,"noteable_id":40,"system":false,"st_diff":null,"updated_by_id":null,"author":{"name":"Administrator"},"events":[]}],"resource_label_events":[{"id":244,"action":"remove","issue_id":40,"merge_request_id":null,"label_id":2,"user_id":1,"created_at":"2018-08-28T08:24:00.494Z","label":{"id":2,"title":"test2","color":"#428bca","project_id":8,"created_at":"2016-07-22T08:55:44.161Z","updated_at":"2016-07-22T08:55:44.161Z","template":false,"description":"","type":"ProjectLabel"}}],"sentry_issue":{"id":1,"issue_id":40,"sentry_issue_identifier":1234567891}}
+{"id":39,"title":"Issue without assignees","author_id":22,"project_id":5,"created_at":"2016-06-14T15:02:08.233Z","updated_at":"2016-06-14T15:02:48.194Z","position":0,"branch_name":null,"description":"Voluptate vel reprehenderit facilis omnis voluptas magnam tenetur.","state":"opened","iid":9,"updated_by_id":null,"confidential":false,"due_date":null,"moved_to_id":null,"issue_assignees":[],"milestone":{"id":1,"title":"test milestone","project_id":8,"description":"test milestone","due_date":null,"created_at":"2016-06-14T15:02:04.415Z","updated_at":"2016-06-14T15:02:04.415Z","state":"active","iid":1,"events":[{"id":487,"target_type":"Milestone","target_id":1,"project_id":46,"created_at":"2016-06-14T15:02:04.418Z","updated_at":"2016-06-14T15:02:04.418Z","action":1,"author_id":18}]},"notes":[{"id":359,"note":"Quo eius velit quia et id quam.","noteable_type":"Issue","author_id":26,"created_at":"2016-06-14T15:02:48.009Z","updated_at":"2016-06-14T15:02:48.009Z","project_id":5,"attachment":{"url":null},"line_code":null,"commit_id":null,"noteable_id":39,"system":false,"st_diff":null,"updated_by_id":null,"author":{"name":"User 4"},"events":[]},{"id":360,"note":"Nulla commodi ratione cumque id autem.","noteable_type":"Issue","author_id":25,"created_at":"2016-06-14T15:02:48.032Z","updated_at":"2016-06-14T15:02:48.032Z","project_id":5,"attachment":{"url":null},"line_code":null,"commit_id":null,"noteable_id":39,"system":false,"st_diff":null,"updated_by_id":null,"author":{"name":"User 3"},"events":[]},{"id":361,"note":"Illum non ea sed dolores corrupti.","noteable_type":"Issue","author_id":22,"created_at":"2016-06-14T15:02:48.056Z","updated_at":"2016-06-14T15:02:48.056Z","project_id":5,"attachment":{"url":null},"line_code":null,"commit_id":null,"noteable_id":39,"system":false,"st_diff":null,"updated_by_id":null,"author":{"name":"User 0"},"events":[]},{"id":362,"note":"Facere dolores ipsum dolorum maiores omnis occaecati ab.","noteable_type":"Issue","author_id":20,"created_at":"2016-06-14T15:02:48.082Z","updated_at":"2016-06-14T15:02:48.082Z","project_id":5,"attachment":{"url":null},"line_code":null,"commit_id":null,"noteable_id":39,"system":false,"st_diff":null,"updated_by_id":null,"author":{"name":"Ottis Schuster II"},"events":[]},{"id":363,"note":"Quod laudantium similique sint aut est ducimus.","noteable_type":"Issue","author_id":16,"created_at":"2016-06-14T15:02:48.113Z","updated_at":"2016-06-14T15:02:48.113Z","project_id":5,"attachment":{"url":null},"line_code":null,"commit_id":null,"noteable_id":39,"system":false,"st_diff":null,"updated_by_id":null,"author":{"name":"Rhett Emmerich IV"},"events":[]},{"id":364,"note":"Aut omnis eos esse incidunt vero reiciendis.","noteable_type":"Issue","author_id":15,"created_at":"2016-06-14T15:02:48.139Z","updated_at":"2016-06-14T15:02:48.139Z","project_id":5,"attachment":{"url":null},"line_code":null,"commit_id":null,"noteable_id":39,"system":false,"st_diff":null,"updated_by_id":null,"author":{"name":"Burdette Bernier"},"events":[]},{"id":365,"note":"Beatae dolore et doloremque asperiores sunt.","noteable_type":"Issue","author_id":6,"created_at":"2016-06-14T15:02:48.162Z","updated_at":"2016-06-14T15:02:48.162Z","project_id":5,"attachment":{"url":null},"line_code":null,"commit_id":null,"noteable_id":39,"system":false,"st_diff":null,"updated_by_id":null,"author":{"name":"Ari Wintheiser"},"events":[]},{"id":366,"note":"Doloribus ipsam ex delectus rerum libero recusandae modi repellendus.","noteable_type":"Issue","author_id":1,"created_at":"2016-06-14T15:02:48.192Z","updated_at":"2016-06-14T15:02:48.192Z","project_id":5,"attachment":{"url":null},"line_code":null,"commit_id":null,"noteable_id":39,"system":false,"st_diff":null,"updated_by_id":null,"author":{"name":"Administrator"},"events":[]}]}
+{"id":38,"title":"Quasi adipisci non cupiditate dolorem quo qui earum sed.","author_id":6,"project_id":5,"created_at":"2016-06-14T15:02:08.154Z","updated_at":"2016-06-14T15:02:48.614Z","position":0,"branch_name":null,"description":"Ea recusandae neque autem tempora.","state":"closed","iid":8,"updated_by_id":null,"confidential":false,"due_date":null,"moved_to_id":null,"label_links":[{"id":99,"label_id":2,"target_id":38,"target_type":"Issue","created_at":"2016-07-22T08:57:02.840Z","updated_at":"2016-07-22T08:57:02.840Z","label":{"id":2,"title":"test2","color":"#428bca","project_id":8,"created_at":"2016-07-22T08:55:44.161Z","updated_at":"2016-07-22T08:55:44.161Z","template":false,"description":"","type":"ProjectLabel"}}],"notes":[{"id":367,"note":"Accusantium fugiat et eaque quisquam esse corporis.","noteable_type":"Issue","author_id":26,"created_at":"2016-06-14T15:02:48.235Z","updated_at":"2016-06-14T15:02:48.235Z","project_id":5,"attachment":{"url":null},"line_code":null,"commit_id":null,"noteable_id":38,"system":false,"st_diff":null,"updated_by_id":null,"author":{"name":"User 4"},"events":[]},{"id":368,"note":"Ea labore eum nam qui laboriosam.","noteable_type":"Issue","author_id":25,"created_at":"2016-06-14T15:02:48.261Z","updated_at":"2016-06-14T15:02:48.261Z","project_id":5,"attachment":{"url":null},"line_code":null,"commit_id":null,"noteable_id":38,"system":false,"st_diff":null,"updated_by_id":null,"author":{"name":"User 3"},"events":[]},{"id":369,"note":"Accusantium quis sed molestiae et.","noteable_type":"Issue","author_id":22,"created_at":"2016-06-14T15:02:48.294Z","updated_at":"2016-06-14T15:02:48.294Z","project_id":5,"attachment":{"url":null},"line_code":null,"commit_id":null,"noteable_id":38,"system":false,"st_diff":null,"updated_by_id":null,"author":{"name":"User 0"},"events":[]},{"id":370,"note":"Corporis numquam a voluptatem pariatur asperiores dolorem delectus autem.","noteable_type":"Issue","author_id":20,"created_at":"2016-06-14T15:02:48.523Z","updated_at":"2016-06-14T15:02:48.523Z","project_id":5,"attachment":{"url":null},"line_code":null,"commit_id":null,"noteable_id":38,"system":false,"st_diff":null,"updated_by_id":null,"author":{"name":"Ottis Schuster II"},"events":[]},{"id":371,"note":"Ea accusantium maxime voluptas rerum.","noteable_type":"Issue","author_id":16,"created_at":"2016-06-14T15:02:48.546Z","updated_at":"2016-06-14T15:02:48.546Z","project_id":5,"attachment":{"url":null},"line_code":null,"commit_id":null,"noteable_id":38,"system":false,"st_diff":null,"updated_by_id":null,"author":{"name":"Rhett Emmerich IV"},"events":[]},{"id":372,"note":"Pariatur iusto et et excepturi similique ipsam eum.","noteable_type":"Issue","author_id":15,"created_at":"2016-06-14T15:02:48.569Z","updated_at":"2016-06-14T15:02:48.569Z","project_id":5,"attachment":{"url":null},"line_code":null,"commit_id":null,"noteable_id":38,"system":false,"st_diff":null,"updated_by_id":null,"author":{"name":"Burdette Bernier"},"events":[]},{"id":373,"note":"Aliquam et culpa officia iste eius.","noteable_type":"Issue","author_id":6,"created_at":"2016-06-14T15:02:48.591Z","updated_at":"2016-06-14T15:02:48.591Z","project_id":5,"attachment":{"url":null},"line_code":null,"commit_id":null,"noteable_id":38,"system":false,"st_diff":null,"updated_by_id":null,"author":{"name":"Ari Wintheiser"},"events":[]},{"id":374,"note":"Ab id velit id unde laborum.","noteable_type":"Issue","author_id":1,"created_at":"2016-06-14T15:02:48.613Z","updated_at":"2016-06-14T15:02:48.613Z","project_id":5,"attachment":{"url":null},"line_code":null,"commit_id":null,"noteable_id":38,"system":false,"st_diff":null,"updated_by_id":null,"author":{"name":"Administrator"},"events":[]}]}
+{"id":37,"title":"Cupiditate quo aut ducimus minima molestiae vero numquam possimus.","author_id":20,"project_id":5,"created_at":"2016-06-14T15:02:08.051Z","updated_at":"2016-06-14T15:02:48.854Z","position":0,"branch_name":null,"description":"Maiores architecto quos in dolorem.","state":"opened","iid":7,"updated_by_id":null,"confidential":false,"due_date":null,"moved_to_id":null,"notes":[{"id":375,"note":"Quasi fugit qui sed eligendi aut quia.","noteable_type":"Issue","author_id":26,"created_at":"2016-06-14T15:02:48.647Z","updated_at":"2016-06-14T15:02:48.647Z","project_id":5,"attachment":{"url":null},"line_code":null,"commit_id":null,"noteable_id":37,"system":false,"st_diff":null,"updated_by_id":null,"author":{"name":"User 4"},"events":[]},{"id":376,"note":"Esse nesciunt voluptatem ex vero est consequatur.","noteable_type":"Issue","author_id":25,"created_at":"2016-06-14T15:02:48.674Z","updated_at":"2016-06-14T15:02:48.674Z","project_id":5,"attachment":{"url":null},"line_code":null,"commit_id":null,"noteable_id":37,"system":false,"st_diff":null,"updated_by_id":null,"author":{"name":"User 3"},"events":[]},{"id":377,"note":"Similique qui quas non aut et velit sequi in.","noteable_type":"Issue","author_id":22,"created_at":"2016-06-14T15:02:48.696Z","updated_at":"2016-06-14T15:02:48.696Z","project_id":5,"attachment":{"url":null},"line_code":null,"commit_id":null,"noteable_id":37,"system":false,"st_diff":null,"updated_by_id":null,"author":{"name":"User 0"},"events":[]},{"id":378,"note":"Eveniet ut cupiditate repellendus numquam in esse eius.","noteable_type":"Issue","author_id":20,"created_at":"2016-06-14T15:02:48.720Z","updated_at":"2016-06-14T15:02:48.720Z","project_id":5,"attachment":{"url":null},"line_code":null,"commit_id":null,"noteable_id":37,"system":false,"st_diff":null,"updated_by_id":null,"author":{"name":"Ottis Schuster II"},"events":[]},{"id":379,"note":"Velit est dolorem adipisci rerum sed iure.","noteable_type":"Issue","author_id":16,"created_at":"2016-06-14T15:02:48.755Z","updated_at":"2016-06-14T15:02:48.755Z","project_id":5,"attachment":{"url":null},"line_code":null,"commit_id":null,"noteable_id":37,"system":false,"st_diff":null,"updated_by_id":null,"author":{"name":"Rhett Emmerich IV"},"events":[]},{"id":380,"note":"Voluptatem ullam ab ut illo ut quo.","noteable_type":"Issue","author_id":15,"created_at":"2016-06-14T15:02:48.793Z","updated_at":"2016-06-14T15:02:48.793Z","project_id":5,"attachment":{"url":null},"line_code":null,"commit_id":null,"noteable_id":37,"system":false,"st_diff":null,"updated_by_id":null,"author":{"name":"Burdette Bernier"},"events":[]},{"id":381,"note":"Voluptatem impedit beatae quasi ipsa earum consectetur.","noteable_type":"Issue","author_id":6,"created_at":"2016-06-14T15:02:48.823Z","updated_at":"2016-06-14T15:02:48.823Z","project_id":5,"attachment":{"url":null},"line_code":null,"commit_id":null,"noteable_id":37,"system":false,"st_diff":null,"updated_by_id":null,"author":{"name":"Ari Wintheiser"},"events":[]},{"id":382,"note":"Nihil officiis eaque incidunt sunt voluptatum excepturi.","noteable_type":"Issue","author_id":1,"created_at":"2016-06-14T15:02:48.852Z","updated_at":"2016-06-14T15:02:48.852Z","project_id":5,"attachment":{"url":null},"line_code":null,"commit_id":null,"noteable_id":37,"system":false,"st_diff":null,"updated_by_id":null,"author":{"name":"Administrator"},"events":[]}]}
+{"id":36,"title":"Necessitatibus dolor est enim quia rem suscipit quidem voluptas ullam.","author_id":16,"project_id":5,"created_at":"2016-06-14T15:02:07.958Z","updated_at":"2016-06-14T15:02:49.044Z","position":0,"branch_name":null,"description":"Ut aut ut et tenetur velit aut id modi.","state":"opened","iid":6,"updated_by_id":null,"confidential":false,"due_date":null,"moved_to_id":null,"notes":[{"id":383,"note":"Excepturi deleniti sunt rerum nesciunt vero fugiat possimus.","noteable_type":"Issue","author_id":26,"created_at":"2016-06-14T15:02:48.885Z","updated_at":"2016-06-14T15:02:48.885Z","project_id":5,"attachment":{"url":null},"line_code":null,"commit_id":null,"noteable_id":36,"system":false,"st_diff":null,"updated_by_id":null,"author":{"name":"User 4"},"events":[]},{"id":384,"note":"Et est nemo sed nam sed.","noteable_type":"Issue","author_id":25,"created_at":"2016-06-14T15:02:48.910Z","updated_at":"2016-06-14T15:02:48.910Z","project_id":5,"attachment":{"url":null},"line_code":null,"commit_id":null,"noteable_id":36,"system":false,"st_diff":null,"updated_by_id":null,"author":{"name":"User 3"},"events":[]},{"id":385,"note":"Animi mollitia nulla facere amet aut quaerat.","noteable_type":"Issue","author_id":22,"created_at":"2016-06-14T15:02:48.934Z","updated_at":"2016-06-14T15:02:48.934Z","project_id":5,"attachment":{"url":null},"line_code":null,"commit_id":null,"noteable_id":36,"system":false,"st_diff":null,"updated_by_id":null,"author":{"name":"User 0"},"events":[]},{"id":386,"note":"Excepturi id voluptas ut odio officiis omnis.","noteable_type":"Issue","author_id":20,"created_at":"2016-06-14T15:02:48.955Z","updated_at":"2016-06-14T15:02:48.955Z","project_id":5,"attachment":{"url":null},"line_code":null,"commit_id":null,"noteable_id":36,"system":false,"st_diff":null,"updated_by_id":null,"author":{"name":"Ottis Schuster II"},"events":[]},{"id":387,"note":"Molestiae labore officiis magni et eligendi quasi maxime.","noteable_type":"Issue","author_id":16,"created_at":"2016-06-14T15:02:48.978Z","updated_at":"2016-06-14T15:02:48.978Z","project_id":5,"attachment":{"url":null},"line_code":null,"commit_id":null,"noteable_id":36,"system":false,"st_diff":null,"updated_by_id":null,"author":{"name":"Rhett Emmerich IV"},"events":[]},{"id":388,"note":"Officia tenetur praesentium rem nam non.","noteable_type":"Issue","author_id":15,"created_at":"2016-06-14T15:02:49.001Z","updated_at":"2016-06-14T15:02:49.001Z","project_id":5,"attachment":{"url":null},"line_code":null,"commit_id":null,"noteable_id":36,"system":false,"st_diff":null,"updated_by_id":null,"author":{"name":"Burdette Bernier"},"events":[]},{"id":389,"note":"Et et et molestiae reprehenderit.","noteable_type":"Issue","author_id":6,"created_at":"2016-06-14T15:02:49.022Z","updated_at":"2016-06-14T15:02:49.022Z","project_id":5,"attachment":{"url":null},"line_code":null,"commit_id":null,"noteable_id":36,"system":false,"st_diff":null,"updated_by_id":null,"author":{"name":"Ari Wintheiser"},"events":[]},{"id":390,"note":"Aperiam in consequatur est sunt cum quia.","noteable_type":"Issue","author_id":1,"created_at":"2016-06-14T15:02:49.043Z","updated_at":"2016-06-14T15:02:49.043Z","project_id":5,"attachment":{"url":null},"line_code":null,"commit_id":null,"noteable_id":36,"system":false,"st_diff":null,"updated_by_id":null,"author":{"name":"Administrator"},"events":[]}]}
+{"id":35,"title":"Repellat praesentium deserunt maxime incidunt harum porro qui.","author_id":20,"project_id":5,"created_at":"2016-06-14T15:02:07.832Z","updated_at":"2016-06-14T15:02:49.226Z","position":0,"branch_name":null,"description":"Dicta nisi nihil non ipsa velit.","state":"closed","iid":5,"updated_by_id":null,"confidential":false,"due_date":null,"moved_to_id":null,"notes":[{"id":391,"note":"Qui magnam et assumenda quod id dicta necessitatibus.","noteable_type":"Issue","author_id":26,"created_at":"2016-06-14T15:02:49.075Z","updated_at":"2016-06-14T15:02:49.075Z","project_id":5,"attachment":{"url":null},"line_code":null,"commit_id":null,"noteable_id":35,"system":false,"st_diff":null,"updated_by_id":null,"author":{"name":"User 4"},"events":[]},{"id":392,"note":"Consectetur deserunt possimus dolor est odio.","noteable_type":"Issue","author_id":25,"created_at":"2016-06-14T15:02:49.095Z","updated_at":"2016-06-14T15:02:49.095Z","project_id":5,"attachment":{"url":null},"line_code":null,"commit_id":null,"noteable_id":35,"system":false,"st_diff":null,"updated_by_id":null,"author":{"name":"User 3"},"events":[]},{"id":393,"note":"Labore nisi quo cumque voluptas consequatur aut qui.","noteable_type":"Issue","author_id":22,"created_at":"2016-06-14T15:02:49.117Z","updated_at":"2016-06-14T15:02:49.117Z","project_id":5,"attachment":{"url":null},"line_code":null,"commit_id":null,"noteable_id":35,"system":false,"st_diff":null,"updated_by_id":null,"author":{"name":"User 0"},"events":[]},{"id":394,"note":"Et totam facilis voluptas et enim.","noteable_type":"Issue","author_id":20,"created_at":"2016-06-14T15:02:49.138Z","updated_at":"2016-06-14T15:02:49.138Z","project_id":5,"attachment":{"url":null},"line_code":null,"commit_id":null,"noteable_id":35,"system":false,"st_diff":null,"updated_by_id":null,"author":{"name":"Ottis Schuster II"},"events":[]},{"id":395,"note":"Ratione sint pariatur sed omnis eligendi quo libero exercitationem.","noteable_type":"Issue","author_id":16,"created_at":"2016-06-14T15:02:49.160Z","updated_at":"2016-06-14T15:02:49.160Z","project_id":5,"attachment":{"url":null},"line_code":null,"commit_id":null,"noteable_id":35,"system":false,"st_diff":null,"updated_by_id":null,"author":{"name":"Rhett Emmerich IV"},"events":[]},{"id":396,"note":"Iure hic autem id voluptatem.","noteable_type":"Issue","author_id":15,"created_at":"2016-06-14T15:02:49.182Z","updated_at":"2016-06-14T15:02:49.182Z","project_id":5,"attachment":{"url":null},"line_code":null,"commit_id":null,"noteable_id":35,"system":false,"st_diff":null,"updated_by_id":null,"author":{"name":"Burdette Bernier"},"events":[]},{"id":397,"note":"Excepturi eum laboriosam delectus repellendus odio nisi et voluptatem.","noteable_type":"Issue","author_id":6,"created_at":"2016-06-14T15:02:49.205Z","updated_at":"2016-06-14T15:02:49.205Z","project_id":5,"attachment":{"url":null},"line_code":null,"commit_id":null,"noteable_id":35,"system":false,"st_diff":null,"updated_by_id":null,"author":{"name":"Ari Wintheiser"},"events":[]},{"id":398,"note":"Ut quis ex soluta consequatur et blanditiis.","noteable_type":"Issue","author_id":1,"created_at":"2016-06-14T15:02:49.225Z","updated_at":"2016-06-14T15:02:49.225Z","project_id":5,"attachment":{"url":null},"line_code":null,"commit_id":null,"noteable_id":35,"system":false,"st_diff":null,"updated_by_id":null,"author":{"name":"Administrator"},"events":[]}]}
+{"id":34,"title":"Ullam expedita deserunt libero consequatur quia dolor harum perferendis facere quidem.","author_id":1,"project_id":5,"created_at":"2016-06-14T15:02:07.717Z","updated_at":"2016-06-14T15:02:49.416Z","position":0,"branch_name":null,"description":"Ut et explicabo vel voluptatem consequuntur ut sed.","state":"closed","iid":4,"updated_by_id":null,"confidential":false,"due_date":null,"moved_to_id":null,"notes":[{"id":399,"note":"Dolor iste tempora tenetur non vitae maiores voluptatibus.","noteable_type":"Issue","author_id":26,"created_at":"2016-06-14T15:02:49.256Z","updated_at":"2016-06-14T15:02:49.256Z","project_id":5,"attachment":{"url":null},"line_code":null,"commit_id":null,"noteable_id":34,"system":false,"st_diff":null,"updated_by_id":null,"author":{"name":"User 4"},"events":[]},{"id":400,"note":"Aut sit quidem qui adipisci maxime excepturi iusto.","noteable_type":"Issue","author_id":25,"created_at":"2016-06-14T15:02:49.284Z","updated_at":"2016-06-14T15:02:49.284Z","project_id":5,"attachment":{"url":null},"line_code":null,"commit_id":null,"noteable_id":34,"system":false,"st_diff":null,"updated_by_id":null,"author":{"name":"User 3"},"events":[]},{"id":401,"note":"Et a necessitatibus autem quidem animi sunt voluptatum rerum.","noteable_type":"Issue","author_id":22,"created_at":"2016-06-14T15:02:49.305Z","updated_at":"2016-06-14T15:02:49.305Z","project_id":5,"attachment":{"url":null},"line_code":null,"commit_id":null,"noteable_id":34,"system":false,"st_diff":null,"updated_by_id":null,"author":{"name":"User 0"},"events":[]},{"id":402,"note":"Esse laboriosam quo voluptatem quis molestiae.","noteable_type":"Issue","author_id":20,"created_at":"2016-06-14T15:02:49.328Z","updated_at":"2016-06-14T15:02:49.328Z","project_id":5,"attachment":{"url":null},"line_code":null,"commit_id":null,"noteable_id":34,"system":false,"st_diff":null,"updated_by_id":null,"author":{"name":"Ottis Schuster II"},"events":[]},{"id":403,"note":"Nemo magnam distinctio est ut voluptate ea.","noteable_type":"Issue","author_id":16,"created_at":"2016-06-14T15:02:49.350Z","updated_at":"2016-06-14T15:02:49.350Z","project_id":5,"attachment":{"url":null},"line_code":null,"commit_id":null,"noteable_id":34,"system":false,"st_diff":null,"updated_by_id":null,"author":{"name":"Rhett Emmerich IV"},"events":[]},{"id":404,"note":"Omnis sed rerum neque rerum quae quam nulla officiis.","noteable_type":"Issue","author_id":15,"created_at":"2016-06-14T15:02:49.372Z","updated_at":"2016-06-14T15:02:49.372Z","project_id":5,"attachment":{"url":null},"line_code":null,"commit_id":null,"noteable_id":34,"system":false,"st_diff":null,"updated_by_id":null,"author":{"name":"Burdette Bernier"},"events":[]},{"id":405,"note":"Quo soluta dolorem vitae ad consequatur qui aut dicta.","noteable_type":"Issue","author_id":6,"created_at":"2016-06-14T15:02:49.394Z","updated_at":"2016-06-14T15:02:49.394Z","project_id":5,"attachment":{"url":null},"line_code":null,"commit_id":null,"noteable_id":34,"system":false,"st_diff":null,"updated_by_id":null,"author":{"name":"Ari Wintheiser"},"events":[]},{"id":406,"note":"Magni minus est aut aut totam ut.","noteable_type":"Issue","author_id":1,"created_at":"2016-06-14T15:02:49.414Z","updated_at":"2016-06-14T15:02:49.414Z","project_id":5,"attachment":{"url":null},"line_code":null,"commit_id":null,"noteable_id":34,"system":false,"st_diff":null,"updated_by_id":null,"author":{"name":"Administrator"},"events":[]}]}
+{"id":33,"title":"Numquam accusamus eos iste exercitationem magni non inventore.","author_id":26,"project_id":5,"created_at":"2016-06-14T15:02:07.611Z","updated_at":"2016-06-14T15:02:49.661Z","position":0,"branch_name":null,"description":"Non asperiores velit accusantium voluptate.","state":"closed","iid":3,"updated_by_id":null,"confidential":false,"due_date":null,"moved_to_id":null,"notes":[{"id":407,"note":"Quod ea et possimus architecto.","noteable_type":"Issue","author_id":26,"created_at":"2016-06-14T15:02:49.450Z","updated_at":"2016-06-14T15:02:49.450Z","project_id":5,"attachment":{"url":null},"line_code":null,"commit_id":null,"noteable_id":33,"system":false,"st_diff":null,"updated_by_id":null,"author":{"name":"User 4"},"events":[]},{"id":408,"note":"Reiciendis est et unde perferendis dicta ut praesentium quasi.","noteable_type":"Issue","author_id":25,"created_at":"2016-06-14T15:02:49.503Z","updated_at":"2016-06-14T15:02:49.503Z","project_id":5,"attachment":{"url":null},"line_code":null,"commit_id":null,"noteable_id":33,"system":false,"st_diff":null,"updated_by_id":null,"author":{"name":"User 3"},"events":[]},{"id":409,"note":"Magni quia odio blanditiis pariatur voluptas.","noteable_type":"Issue","author_id":22,"created_at":"2016-06-14T15:02:49.527Z","updated_at":"2016-06-14T15:02:49.527Z","project_id":5,"attachment":{"url":null},"line_code":null,"commit_id":null,"noteable_id":33,"system":false,"st_diff":null,"updated_by_id":null,"author":{"name":"User 0"},"events":[]},{"id":410,"note":"Enim quam ut et et et.","noteable_type":"Issue","author_id":20,"created_at":"2016-06-14T15:02:49.551Z","updated_at":"2016-06-14T15:02:49.551Z","project_id":5,"attachment":{"url":null},"line_code":null,"commit_id":null,"noteable_id":33,"system":false,"st_diff":null,"updated_by_id":null,"author":{"name":"Ottis Schuster II"},"events":[]},{"id":411,"note":"Fugit voluptatem ratione maxime expedita.","noteable_type":"Issue","author_id":16,"created_at":"2016-06-14T15:02:49.578Z","updated_at":"2016-06-14T15:02:49.578Z","project_id":5,"attachment":{"url":null},"line_code":null,"commit_id":null,"noteable_id":33,"system":false,"st_diff":null,"updated_by_id":null,"author":{"name":"Rhett Emmerich IV"},"events":[]},{"id":412,"note":"Voluptatem enim aut ipsa et et ducimus.","noteable_type":"Issue","author_id":15,"created_at":"2016-06-14T15:02:49.604Z","updated_at":"2016-06-14T15:02:49.604Z","project_id":5,"attachment":{"url":null},"line_code":null,"commit_id":null,"noteable_id":33,"system":false,"st_diff":null,"updated_by_id":null,"author":{"name":"Burdette Bernier"},"events":[]},{"id":413,"note":"Quia repellat fugiat consectetur quidem.","noteable_type":"Issue","author_id":6,"created_at":"2016-06-14T15:02:49.631Z","updated_at":"2016-06-14T15:02:49.631Z","project_id":5,"attachment":{"url":null},"line_code":null,"commit_id":null,"noteable_id":33,"system":false,"st_diff":null,"updated_by_id":null,"author":{"name":"Ari Wintheiser"},"events":[]},{"id":414,"note":"Corporis ipsum et ea necessitatibus quod assumenda repudiandae quam.","noteable_type":"Issue","author_id":1,"created_at":"2016-06-14T15:02:49.659Z","updated_at":"2016-06-14T15:02:49.659Z","project_id":5,"attachment":{"url":null},"line_code":null,"commit_id":null,"noteable_id":33,"system":false,"st_diff":null,"updated_by_id":null,"author":{"name":"Administrator"},"events":[]}]}
+{"id":32,"title":"Necessitatibus magnam qui at velit consequatur perspiciatis.","author_id":15,"project_id":5,"created_at":"2016-06-14T15:02:07.431Z","updated_at":"2016-06-14T15:02:49.884Z","position":0,"branch_name":null,"description":"Molestiae corporis magnam et fugit aliquid nulla quia.","state":"closed","iid":2,"updated_by_id":null,"confidential":false,"due_date":null,"moved_to_id":null,"notes":[{"id":415,"note":"Nemo consequatur sed blanditiis qui id iure dolores.","noteable_type":"Issue","author_id":26,"created_at":"2016-06-14T15:02:49.694Z","updated_at":"2016-06-14T15:02:49.694Z","project_id":5,"attachment":{"url":null},"line_code":null,"commit_id":null,"noteable_id":32,"system":false,"st_diff":null,"updated_by_id":null,"author":{"name":"User 4"},"events":[]},{"id":416,"note":"Voluptas ab accusantium dicta in.","noteable_type":"Issue","author_id":25,"created_at":"2016-06-14T15:02:49.718Z","updated_at":"2016-06-14T15:02:49.718Z","project_id":5,"attachment":{"url":null},"line_code":null,"commit_id":null,"noteable_id":32,"system":false,"st_diff":null,"updated_by_id":null,"author":{"name":"User 3"},"events":[]},{"id":417,"note":"Esse odit qui a et eum ducimus.","noteable_type":"Issue","author_id":22,"created_at":"2016-06-14T15:02:49.741Z","updated_at":"2016-06-14T15:02:49.741Z","project_id":5,"attachment":{"url":null},"line_code":null,"commit_id":null,"noteable_id":32,"system":false,"st_diff":null,"updated_by_id":null,"author":{"name":"User 0"},"events":[]},{"id":418,"note":"Sequi dolor doloribus ratione placeat repellendus.","noteable_type":"Issue","author_id":20,"created_at":"2016-06-14T15:02:49.767Z","updated_at":"2016-06-14T15:02:49.767Z","project_id":5,"attachment":{"url":null},"line_code":null,"commit_id":null,"noteable_id":32,"system":false,"st_diff":null,"updated_by_id":null,"author":{"name":"Ottis Schuster II"},"events":[]},{"id":419,"note":"Quae aspernatur rem est similique.","noteable_type":"Issue","author_id":16,"created_at":"2016-06-14T15:02:49.796Z","updated_at":"2016-06-14T15:02:49.796Z","project_id":5,"attachment":{"url":null},"line_code":null,"commit_id":null,"noteable_id":32,"system":false,"st_diff":null,"updated_by_id":null,"author":{"name":"Rhett Emmerich IV"},"events":[]},{"id":420,"note":"Voluptate omnis et id rerum non nesciunt laudantium assumenda.","noteable_type":"Issue","author_id":15,"created_at":"2016-06-14T15:02:49.825Z","updated_at":"2016-06-14T15:02:49.825Z","project_id":5,"attachment":{"url":null},"line_code":null,"commit_id":null,"noteable_id":32,"system":false,"st_diff":null,"updated_by_id":null,"author":{"name":"Burdette Bernier"},"events":[]},{"id":421,"note":"Quia enim ab et eligendi.","noteable_type":"Issue","author_id":6,"created_at":"2016-06-14T15:02:49.853Z","updated_at":"2016-06-14T15:02:49.853Z","project_id":5,"attachment":{"url":null},"line_code":null,"commit_id":null,"noteable_id":32,"system":false,"st_diff":null,"updated_by_id":null,"author":{"name":"Ari Wintheiser"},"events":[]},{"id":422,"note":"In fugiat rerum voluptas quas officia.","noteable_type":"Issue","author_id":1,"created_at":"2016-06-14T15:02:49.881Z","updated_at":"2016-06-14T15:02:49.881Z","project_id":5,"attachment":{"url":null},"line_code":null,"commit_id":null,"noteable_id":32,"system":false,"st_diff":null,"updated_by_id":null,"author":{"name":"Administrator"},"events":[]}]}
+{"id":31,"title":"issue_with_timelogs","author_id":16,"project_id":5,"created_at":"2016-06-14T15:02:07.280Z","updated_at":"2016-06-14T15:02:50.134Z","position":0,"branch_name":null,"description":"Quod ad architecto qui est sed quia.","state":"closed","iid":1,"updated_by_id":null,"confidential":false,"due_date":null,"moved_to_id":null,"timelogs":[{"id":1,"time_spent":72000,"user_id":1,"created_at":"2019-12-27T09:15:22.302Z","updated_at":"2019-12-27T09:15:22.302Z","spent_at":"2019-12-27T00:00:00.000Z"}],"notes":[{"id":423,"note":"A mollitia qui iste consequatur eaque iure omnis sunt.","noteable_type":"Issue","author_id":26,"created_at":"2016-06-14T15:02:49.933Z","updated_at":"2016-06-14T15:02:49.933Z","project_id":5,"attachment":{"url":null},"line_code":null,"commit_id":null,"noteable_id":31,"system":false,"st_diff":null,"updated_by_id":null,"author":{"name":"User 4"},"events":[]},{"id":424,"note":"Eveniet est et blanditiis sequi alias.","noteable_type":"Issue","author_id":25,"created_at":"2016-06-14T15:02:49.965Z","updated_at":"2016-06-14T15:02:49.965Z","project_id":5,"attachment":{"url":null},"line_code":null,"commit_id":null,"noteable_id":31,"system":false,"st_diff":null,"updated_by_id":null,"author":{"name":"User 3"},"events":[]},{"id":425,"note":"Commodi tempore voluptas doloremque est.","noteable_type":"Issue","author_id":22,"created_at":"2016-06-14T15:02:49.996Z","updated_at":"2016-06-14T15:02:49.996Z","project_id":5,"attachment":{"url":null},"line_code":null,"commit_id":null,"noteable_id":31,"system":false,"st_diff":null,"updated_by_id":null,"author":{"name":"User 0"},"events":[]},{"id":426,"note":"Quo libero impedit odio debitis rerum aspernatur.","noteable_type":"Issue","author_id":20,"created_at":"2016-06-14T15:02:50.024Z","updated_at":"2016-06-14T15:02:50.024Z","project_id":5,"attachment":{"url":null},"line_code":null,"commit_id":null,"noteable_id":31,"system":false,"st_diff":null,"updated_by_id":null,"author":{"name":"Ottis Schuster II"},"events":[]},{"id":427,"note":"Dolorem voluptatem qui labore deserunt.","noteable_type":"Issue","author_id":16,"created_at":"2016-06-14T15:02:50.049Z","updated_at":"2016-06-14T15:02:50.049Z","project_id":5,"attachment":{"url":null},"line_code":null,"commit_id":null,"noteable_id":31,"system":false,"st_diff":null,"updated_by_id":null,"author":{"name":"Rhett Emmerich IV"},"events":[]},{"id":428,"note":"Est blanditiis laboriosam enim ipsam.","noteable_type":"Issue","author_id":15,"created_at":"2016-06-14T15:02:50.077Z","updated_at":"2016-06-14T15:02:50.077Z","project_id":5,"attachment":{"url":null},"line_code":null,"commit_id":null,"noteable_id":31,"system":false,"st_diff":null,"updated_by_id":null,"author":{"name":"Burdette Bernier"},"events":[]},{"id":429,"note":"Et in voluptatem animi dolorem eos.","noteable_type":"Issue","author_id":6,"created_at":"2016-06-14T15:02:50.107Z","updated_at":"2016-06-14T15:02:50.107Z","project_id":5,"attachment":{"url":null},"line_code":null,"commit_id":null,"noteable_id":31,"system":false,"st_diff":null,"updated_by_id":null,"author":{"name":"Ari Wintheiser"},"events":[]},{"id":430,"note":"Unde culpa voluptate qui sint quos.","noteable_type":"Issue","author_id":1,"created_at":"2016-06-14T15:02:50.132Z","updated_at":"2016-06-14T15:02:50.132Z","project_id":5,"attachment":{"url":null},"line_code":null,"commit_id":null,"noteable_id":31,"system":false,"st_diff":null,"updated_by_id":null,"author":{"name":"Administrator"},"events":[]}]}
diff --git a/spec/fixtures/lib/gitlab/import_export/complex/tree/project/labels.ndjson b/spec/fixtures/lib/gitlab/import_export/complex/tree/project/labels.ndjson
new file mode 100644
index 00000000000..c36b6970e83
--- /dev/null
+++ b/spec/fixtures/lib/gitlab/import_export/complex/tree/project/labels.ndjson
@@ -0,0 +1,2 @@
+{"id":2,"title":"test2","color":"#428bca","project_id":8,"created_at":"2016-07-22T08:55:44.161Z","updated_at":"2016-07-22T08:55:44.161Z","template":false,"description":"","type":"ProjectLabel","priorities":[]}
+{"id":3,"title":"test3","color":"#428bca","group_id":8,"created_at":"2016-07-22T08:55:44.161Z","updated_at":"2016-07-22T08:55:44.161Z","template":false,"description":"","project_id":null,"type":"GroupLabel","priorities":[{"id":1,"project_id":5,"label_id":1,"priority":1,"created_at":"2016-10-18T09:35:43.338Z","updated_at":"2016-10-18T09:35:43.338Z"}]}
diff --git a/spec/fixtures/lib/gitlab/import_export/complex/tree/project/merge_requests.ndjson b/spec/fixtures/lib/gitlab/import_export/complex/tree/project/merge_requests.ndjson
new file mode 100644
index 00000000000..3687c005b96
--- /dev/null
+++ b/spec/fixtures/lib/gitlab/import_export/complex/tree/project/merge_requests.ndjson
@@ -0,0 +1,9 @@
+{"id":27,"target_branch":"feature","source_branch":"feature_conflict","source_project_id":2147483547,"author_id":1,"assignee_id":null,"title":"MR1","created_at":"2016-06-14T15:02:36.568Z","updated_at":"2016-06-14T15:02:56.815Z","state":"opened","merge_status":"unchecked","target_project_id":5,"iid":9,"description":null,"position":0,"updated_by_id":null,"merge_error":null,"diff_head_sha":"HEAD","source_branch_sha":"ABCD","target_branch_sha":"DCBA","merge_params":{"force_remove_source_branch":null},"merge_when_pipeline_succeeds":true,"merge_user_id":null,"merge_commit_sha":null,"notes":[{"id":669,"note":"added 3 commits\n\n<ul><li>16ea4e20...074a2a32 - 2 commits from branch <code>master</code></li><li>ca223a02 - readme: fix typos</li></ul>\n\n[Compare with previous version](/group/project/merge_requests/1/diffs?diff_id=1189&start_sha=16ea4e207fb258fe4e9c73185a725207c9a4f3e1)","noteable_type":"MergeRequest","author_id":26,"created_at":"2020-03-28T12:47:33.461Z","updated_at":"2020-03-28T12:47:33.461Z","project_id":5,"attachment":{"url":null},"line_code":null,"commit_id":null,"system":true,"st_diff":null,"updated_by_id":null,"position":null,"original_position":null,"resolved_at":null,"resolved_by_id":null,"discussion_id":null,"change_position":null,"resolved_by_push":null,"confidential":null,"type":null,"author":{"name":"User 4"},"award_emoji":[],"system_note_metadata":{"id":4789,"commit_count":3,"action":"commit","created_at":"2020-03-28T12:47:33.461Z","updated_at":"2020-03-28T12:47:33.461Z"},"events":[],"suggestions":[]},{"id":670,"note":"unmarked as a **Work In Progress**","noteable_type":"MergeRequest","author_id":26,"created_at":"2020-03-28T12:48:36.951Z","updated_at":"2020-03-28T12:48:36.951Z","project_id":5,"attachment":{"url":null},"line_code":null,"commit_id":null,"system":true,"st_diff":null,"updated_by_id":null,"position":null,"original_position":null,"resolved_at":null,"resolved_by_id":null,"discussion_id":null,"change_position":null,"resolved_by_push":null,"confidential":null,"type":null,"author":{"name":"User 4"},"award_emoji":[],"system_note_metadata":{"id":4790,"commit_count":null,"action":"title","created_at":"2020-03-28T12:48:36.951Z","updated_at":"2020-03-28T12:48:36.951Z"},"events":[],"suggestions":[]},{"id":671,"note":"Sit voluptatibus eveniet architecto quidem.","note_html":"<p>something else entirely</p>","cached_markdown_version":917504,"noteable_type":"MergeRequest","author_id":26,"created_at":"2016-06-14T15:02:56.632Z","updated_at":"2016-06-14T15:02:56.632Z","project_id":5,"attachment":{"url":null},"line_code":null,"commit_id":null,"noteable_id":27,"system":false,"st_diff":null,"updated_by_id":null,"author":{"name":"User 4"},"events":[],"award_emoji":[{"id":1,"name":"tada","user_id":1,"awardable_type":"Note","awardable_id":1,"created_at":"2019-11-05T15:37:21.287Z","updated_at":"2019-11-05T15:37:21.287Z"}]},{"id":672,"note":"Odio maxime ratione voluptatibus sed.","noteable_type":"MergeRequest","author_id":25,"created_at":"2016-06-14T15:02:56.656Z","updated_at":"2016-06-14T15:02:56.656Z","project_id":5,"attachment":{"url":null},"line_code":null,"commit_id":null,"noteable_id":27,"system":false,"st_diff":null,"updated_by_id":null,"author":{"name":"User 3"},"events":[]},{"id":673,"note":"Et deserunt et omnis nihil excepturi accusantium.","noteable_type":"MergeRequest","author_id":22,"created_at":"2016-06-14T15:02:56.679Z","updated_at":"2016-06-14T15:02:56.679Z","project_id":5,"attachment":{"url":null},"line_code":null,"commit_id":null,"noteable_id":27,"system":false,"st_diff":null,"updated_by_id":null,"author":{"name":"User 0"},"events":[]},{"id":674,"note":"Saepe asperiores exercitationem non dignissimos laborum reiciendis et ipsum.","noteable_type":"MergeRequest","author_id":20,"created_at":"2016-06-14T15:02:56.700Z","updated_at":"2016-06-14T15:02:56.700Z","project_id":5,"attachment":{"url":null},"line_code":null,"commit_id":null,"noteable_id":27,"system":false,"st_diff":null,"updated_by_id":null,"author":{"name":"Ottis Schuster II"},"events":[],"suggestions":[{"id":1,"note_id":674,"relative_order":0,"applied":false,"commit_id":null,"from_content":"Original line\n","to_content":"New line\n","lines_above":0,"lines_below":0,"outdated":false}]},{"id":675,"note":"Numquam est at dolor quo et sed eligendi similique.","noteable_type":"MergeRequest","author_id":16,"created_at":"2016-06-14T15:02:56.720Z","updated_at":"2016-06-14T15:02:56.720Z","project_id":5,"attachment":{"url":null},"line_code":null,"commit_id":null,"noteable_id":27,"system":false,"st_diff":null,"updated_by_id":null,"author":{"name":"Rhett Emmerich IV"},"events":[]},{"id":676,"note":"Et perferendis aliquam sunt nisi labore delectus.","noteable_type":"MergeRequest","author_id":15,"created_at":"2016-06-14T15:02:56.742Z","updated_at":"2016-06-14T15:02:56.742Z","project_id":5,"attachment":{"url":null},"line_code":null,"commit_id":null,"noteable_id":27,"system":false,"st_diff":null,"updated_by_id":null,"author":{"name":"Burdette Bernier"},"events":[]},{"id":677,"note":"Aut ex rerum et in.","noteable_type":"MergeRequest","author_id":6,"created_at":"2016-06-14T15:02:56.791Z","updated_at":"2016-06-14T15:02:56.791Z","project_id":5,"attachment":{"url":null},"line_code":null,"commit_id":null,"noteable_id":27,"system":false,"st_diff":null,"updated_by_id":null,"author":{"name":"Ari Wintheiser"},"events":[]},{"id":678,"note":"Dolor laborum earum ut exercitationem.","noteable_type":"MergeRequest","author_id":1,"created_at":"2016-06-14T15:02:56.814Z","updated_at":"2016-06-14T15:02:56.814Z","project_id":5,"attachment":{"url":null},"line_code":null,"commit_id":null,"noteable_id":27,"system":false,"st_diff":null,"updated_by_id":null,"author":{"name":"Administrator"},"events":[]}],"resource_label_events":[{"id":243,"action":"add","issue_id":null,"merge_request_id":27,"label_id":null,"user_id":1,"created_at":"2018-08-28T08:24:00.494Z"}],"merge_request_diff":{"id":27,"state":"collected","merge_request_diff_commits":[{"merge_request_diff_id":27,"relative_order":0,"sha":"bb5206fee213d983da88c47f9cf4cc6caf9c66dc","message":"Feature conflict added\n\nSigned-off-by: Dmitriy Zaporozhets <dmitriy.zaporozhets@gmail.com>\n","authored_date":"2014-08-06T08:35:52.000+02:00","author_name":"Dmitriy Zaporozhets","author_email":"dmitriy.zaporozhets@gmail.com","committed_date":"2014-08-06T08:35:52.000+02:00","committer_name":"Dmitriy Zaporozhets","committer_email":"dmitriy.zaporozhets@gmail.com"},{"merge_request_diff_id":27,"relative_order":1,"sha":"5937ac0a7beb003549fc5fd26fc247adbce4a52e","message":"Add submodule from gitlab.com\n\nSigned-off-by: Dmitriy Zaporozhets <dmitriy.zaporozhets@gmail.com>\n","authored_date":"2014-02-27T10:01:38.000+01:00","author_name":"Dmitriy Zaporozhets","author_email":"dmitriy.zaporozhets@gmail.com","committed_date":"2014-02-27T10:01:38.000+01:00","committer_name":"Dmitriy Zaporozhets","committer_email":"dmitriy.zaporozhets@gmail.com"},{"merge_request_diff_id":27,"relative_order":2,"sha":"570e7b2abdd848b95f2f578043fc23bd6f6fd24d","message":"Change some files\n\nSigned-off-by: Dmitriy Zaporozhets <dmitriy.zaporozhets@gmail.com>\n","authored_date":"2014-02-27T09:57:31.000+01:00","author_name":"Dmitriy Zaporozhets","author_email":"dmitriy.zaporozhets@gmail.com","committed_date":"2014-02-27T09:57:31.000+01:00","committer_name":"Dmitriy Zaporozhets","committer_email":"dmitriy.zaporozhets@gmail.com"},{"merge_request_diff_id":27,"relative_order":3,"sha":"6f6d7e7ed97bb5f0054f2b1df789b39ca89b6ff9","message":"More submodules\n\nSigned-off-by: Dmitriy Zaporozhets <dmitriy.zaporozhets@gmail.com>\n","authored_date":"2014-02-27T09:54:21.000+01:00","author_name":"Dmitriy Zaporozhets","author_email":"dmitriy.zaporozhets@gmail.com","committed_date":"2014-02-27T09:54:21.000+01:00","committer_name":"Dmitriy Zaporozhets","committer_email":"dmitriy.zaporozhets@gmail.com"},{"merge_request_diff_id":27,"relative_order":4,"sha":"d14d6c0abdd253381df51a723d58691b2ee1ab08","message":"Remove ds_store files\n\nSigned-off-by: Dmitriy Zaporozhets <dmitriy.zaporozhets@gmail.com>\n","authored_date":"2014-02-27T09:49:50.000+01:00","author_name":"Dmitriy Zaporozhets","author_email":"dmitriy.zaporozhets@gmail.com","committed_date":"2014-02-27T09:49:50.000+01:00","committer_name":"Dmitriy Zaporozhets","committer_email":"dmitriy.zaporozhets@gmail.com"},{"merge_request_diff_id":27,"relative_order":5,"sha":"c1acaa58bbcbc3eafe538cb8274ba387047b69f8","message":"Ignore DS files\n\nSigned-off-by: Dmitriy Zaporozhets <dmitriy.zaporozhets@gmail.com>\n","authored_date":"2014-02-27T09:48:32.000+01:00","author_name":"Dmitriy Zaporozhets","author_email":"dmitriy.zaporozhets@gmail.com","committed_date":"2014-02-27T09:48:32.000+01:00","committer_name":"Dmitriy Zaporozhets","committer_email":"dmitriy.zaporozhets@gmail.com"}],"merge_request_diff_files":[{"merge_request_diff_id":27,"relative_order":0,"utf8_diff":"Binary files a/.DS_Store and /dev/null differ\n","new_path":".DS_Store","old_path":".DS_Store","a_mode":"100644","b_mode":"0","new_file":false,"renamed_file":false,"deleted_file":true,"too_large":false},{"merge_request_diff_id":27,"relative_order":1,"utf8_diff":"--- a/.gitignore\n+++ b/.gitignore\n@@ -17,3 +17,4 @@ rerun.txt\n pickle-email-*.html\n .project\n config/initializers/secret_token.rb\n+.DS_Store\n","new_path":".gitignore","old_path":".gitignore","a_mode":"100644","b_mode":"100644","new_file":false,"renamed_file":false,"deleted_file":false,"too_large":false},{"merge_request_diff_id":27,"relative_order":2,"utf8_diff":"--- a/.gitmodules\n+++ b/.gitmodules\n@@ -1,3 +1,9 @@\n [submodule \"six\"]\n \tpath = six\n \turl = git://github.com/randx/six.git\n+[submodule \"gitlab-shell\"]\n+\tpath = gitlab-shell\n+\turl = https://github.com/gitlabhq/gitlab-shell.git\n+[submodule \"gitlab-grack\"]\n+\tpath = gitlab-grack\n+\turl = https://gitlab.com/gitlab-org/gitlab-grack.git\n","new_path":".gitmodules","old_path":".gitmodules","a_mode":"100644","b_mode":"100644","new_file":false,"renamed_file":false,"deleted_file":false,"too_large":false},{"merge_request_diff_id":27,"relative_order":3,"utf8_diff":"Binary files a/files/.DS_Store and /dev/null differ\n","new_path":"files/.DS_Store","old_path":"files/.DS_Store","a_mode":"100644","b_mode":"0","new_file":false,"renamed_file":false,"deleted_file":true,"too_large":false},{"merge_request_diff_id":27,"relative_order":4,"utf8_diff":"--- /dev/null\n+++ b/files/ruby/feature.rb\n@@ -0,0 +1,4 @@\n+# This file was changed in feature branch\n+# We put different code here to make merge conflict\n+class Conflict\n+end\n","new_path":"files/ruby/feature.rb","old_path":"files/ruby/feature.rb","a_mode":"0","b_mode":"100644","new_file":true,"renamed_file":false,"deleted_file":false,"too_large":false},{"merge_request_diff_id":27,"relative_order":5,"utf8_diff":"--- a/files/ruby/popen.rb\n+++ b/files/ruby/popen.rb\n@@ -6,12 +6,18 @@ module Popen\n \n def popen(cmd, path=nil)\n unless cmd.is_a?(Array)\n- raise \"System commands must be given as an array of strings\"\n+ raise RuntimeError, \"System commands must be given as an array of strings\"\n end\n \n path ||= Dir.pwd\n- vars = { \"PWD\" => path }\n- options = { chdir: path }\n+\n+ vars = {\n+ \"PWD\" => path\n+ }\n+\n+ options = {\n+ chdir: path\n+ }\n \n unless File.directory?(path)\n FileUtils.mkdir_p(path)\n@@ -19,6 +25,7 @@ module Popen\n \n @cmd_output = \"\"\n @cmd_status = 0\n+\n Open3.popen3(vars, *cmd, options) do |stdin, stdout, stderr, wait_thr|\n @cmd_output << stdout.read\n @cmd_output << stderr.read\n","new_path":"files/ruby/popen.rb","old_path":"files/ruby/popen.rb","a_mode":"100644","b_mode":"100644","new_file":false,"renamed_file":false,"deleted_file":false,"too_large":false},{"merge_request_diff_id":27,"relative_order":6,"utf8_diff":"--- a/files/ruby/regex.rb\n+++ b/files/ruby/regex.rb\n@@ -19,14 +19,12 @@ module Gitlab\n end\n \n def archive_formats_regex\n- #|zip|tar| tar.gz | tar.bz2 |\n- /(zip|tar|tar\\.gz|tgz|gz|tar\\.bz2|tbz|tbz2|tb2|bz2)/\n+ /(zip|tar|7z|tar\\.gz|tgz|gz|tar\\.bz2|tbz|tbz2|tb2|bz2)/\n end\n \n def git_reference_regex\n # Valid git ref regex, see:\n # https://www.kernel.org/pub/software/scm/git/docs/git-check-ref-format.html\n-\n %r{\n (?!\n (?# doesn't begins with)\n","new_path":"files/ruby/regex.rb","old_path":"files/ruby/regex.rb","a_mode":"100644","b_mode":"100644","new_file":false,"renamed_file":false,"deleted_file":false,"too_large":false},{"merge_request_diff_id":27,"relative_order":7,"utf8_diff":"--- /dev/null\n+++ b/gitlab-grack\n@@ -0,0 +1 @@\n+Subproject commit 645f6c4c82fd3f5e06f67134450a570b795e55a6\n","new_path":"gitlab-grack","old_path":"gitlab-grack","a_mode":"0","b_mode":"160000","new_file":true,"renamed_file":false,"deleted_file":false,"too_large":false},{"merge_request_diff_id":27,"relative_order":8,"utf8_diff":"--- /dev/null\n+++ b/gitlab-shell\n@@ -0,0 +1 @@\n+Subproject commit 79bceae69cb5750d6567b223597999bfa91cb3b9\n","new_path":"gitlab-shell","old_path":"gitlab-shell","a_mode":"0","b_mode":"160000","new_file":true,"renamed_file":false,"deleted_file":false,"too_large":false}],"merge_request_id":27,"created_at":"2016-06-14T15:02:36.572Z","updated_at":"2016-06-14T15:02:36.658Z","base_commit_sha":"ae73cb07c9eeaf35924a10f713b364d32b2dd34f","real_size":"9"},"events":[{"id":221,"target_type":"MergeRequest","target_id":27,"project_id":36,"created_at":"2016-06-14T15:02:36.703Z","updated_at":"2016-06-14T15:02:36.703Z","action":1,"author_id":1},{"id":187,"target_type":"MergeRequest","target_id":27,"project_id":5,"created_at":"2016-06-14T15:02:36.703Z","updated_at":"2016-06-14T15:02:36.703Z","action":1,"author_id":1}],"approvals_before_merge":1,"award_emoji":[{"id":1,"name":"thumbsup","user_id":1,"awardable_type":"MergeRequest","awardable_id":27,"created_at":"2020-01-07T11:21:21.235Z","updated_at":"2020-01-07T11:21:21.235Z"},{"id":2,"name":"drum","user_id":1,"awardable_type":"MergeRequest","awardable_id":27,"created_at":"2020-01-07T11:21:21.235Z","updated_at":"2020-01-07T11:21:21.235Z"}]}
+{"id":26,"target_branch":"master","source_branch":"feature","source_project_id":4,"author_id":1,"assignee_id":null,"title":"MR2","created_at":"2016-06-14T15:02:36.418Z","updated_at":"2016-06-14T15:02:57.013Z","state":"opened","merge_status":"unchecked","target_project_id":5,"iid":8,"description":null,"position":0,"updated_by_id":null,"merge_error":null,"merge_params":{"force_remove_source_branch":null},"merge_when_pipeline_succeeds":false,"merge_user_id":null,"merge_commit_sha":null,"notes":[{"id":679,"note":"Qui rerum totam nisi est.","noteable_type":"MergeRequest","author_id":26,"created_at":"2016-06-14T15:02:56.848Z","updated_at":"2016-06-14T15:02:56.848Z","project_id":5,"attachment":{"url":null},"line_code":null,"commit_id":null,"noteable_id":26,"system":false,"st_diff":null,"updated_by_id":null,"author":{"name":"User 4"},"events":[]},{"id":680,"note":"Pariatur magni corrupti consequatur debitis minima error beatae voluptatem.","noteable_type":"MergeRequest","author_id":25,"created_at":"2016-06-14T15:02:56.871Z","updated_at":"2016-06-14T15:02:56.871Z","project_id":5,"attachment":{"url":null},"line_code":null,"commit_id":null,"noteable_id":26,"system":false,"st_diff":null,"updated_by_id":null,"author":{"name":"User 3"},"events":[]},{"id":681,"note":"Qui quis ut modi eos rerum ratione.","noteable_type":"MergeRequest","author_id":22,"created_at":"2016-06-14T15:02:56.895Z","updated_at":"2016-06-14T15:02:56.895Z","project_id":5,"attachment":{"url":null},"line_code":null,"commit_id":null,"noteable_id":26,"system":false,"st_diff":null,"updated_by_id":null,"author":{"name":"User 0"},"events":[]},{"id":682,"note":"Illum quidem expedita mollitia fugit.","noteable_type":"MergeRequest","author_id":20,"created_at":"2016-06-14T15:02:56.918Z","updated_at":"2016-06-14T15:02:56.918Z","project_id":5,"attachment":{"url":null},"line_code":null,"commit_id":null,"noteable_id":26,"system":false,"st_diff":null,"updated_by_id":null,"author":{"name":"Ottis Schuster II"},"events":[]},{"id":683,"note":"Consectetur voluptate sit sint possimus veritatis quod.","noteable_type":"MergeRequest","author_id":16,"created_at":"2016-06-14T15:02:56.942Z","updated_at":"2016-06-14T15:02:56.942Z","project_id":5,"attachment":{"url":null},"line_code":null,"commit_id":null,"noteable_id":26,"system":false,"st_diff":null,"updated_by_id":null,"author":{"name":"Rhett Emmerich IV"},"events":[]},{"id":684,"note":"Natus libero quibusdam rem assumenda deleniti accusamus sed earum.","noteable_type":"MergeRequest","author_id":15,"created_at":"2016-06-14T15:02:56.966Z","updated_at":"2016-06-14T15:02:56.966Z","project_id":5,"attachment":{"url":null},"line_code":null,"commit_id":null,"noteable_id":26,"system":false,"st_diff":null,"updated_by_id":null,"author":{"name":"Burdette Bernier"},"events":[]},{"id":685,"note":"Tenetur autem nihil rerum odit.","noteable_type":"MergeRequest","author_id":6,"created_at":"2016-06-14T15:02:56.989Z","updated_at":"2016-06-14T15:02:56.989Z","project_id":5,"attachment":{"url":null},"line_code":null,"commit_id":null,"noteable_id":26,"system":false,"st_diff":null,"updated_by_id":null,"author":{"name":"Ari Wintheiser"},"events":[]},{"id":686,"note":"Quia maiores et odio sed.","noteable_type":"MergeRequest","author_id":1,"created_at":"2016-06-14T15:02:57.012Z","updated_at":"2016-06-14T15:02:57.012Z","project_id":5,"attachment":{"url":null},"line_code":null,"commit_id":null,"noteable_id":26,"system":false,"st_diff":null,"updated_by_id":null,"author":{"name":"Administrator"},"events":[]}],"merge_request_diff":{"id":26,"state":"collected","merge_request_diff_commits":[{"merge_request_diff_id":26,"sha":"0b4bc9a49b562e85de7cc9e834518ea6828729b9","relative_order":0,"message":"Feature added\n\nSigned-off-by: Dmitriy Zaporozhets <dmitriy.zaporozhets@gmail.com>\n","authored_date":"2014-02-27T09:26:01.000+01:00","author_name":"Dmitriy Zaporozhets","author_email":"dmitriy.zaporozhets@gmail.com","committed_date":"2014-02-27T09:26:01.000+01:00","committer_name":"Dmitriy Zaporozhets","committer_email":"dmitriy.zaporozhets@gmail.com"}],"merge_request_diff_files":[{"merge_request_diff_id":26,"relative_order":0,"utf8_diff":"--- /dev/null\n+++ b/files/ruby/feature.rb\n@@ -0,0 +1,5 @@\n+class Feature\n+ def foo\n+ puts 'bar'\n+ end\n+end\n","new_path":"files/ruby/feature.rb","old_path":"files/ruby/feature.rb","a_mode":"0","b_mode":"100644","new_file":true,"renamed_file":false,"deleted_file":false,"too_large":false}],"merge_request_id":26,"created_at":"2016-06-14T15:02:36.421Z","updated_at":"2016-06-14T15:02:36.474Z","base_commit_sha":"ae73cb07c9eeaf35924a10f713b364d32b2dd34f","real_size":"1"},"events":[{"id":222,"target_type":"MergeRequest","target_id":26,"project_id":36,"created_at":"2016-06-14T15:02:36.496Z","updated_at":"2016-06-14T15:02:36.496Z","action":1,"author_id":1},{"id":186,"target_type":"MergeRequest","target_id":26,"project_id":5,"created_at":"2016-06-14T15:02:36.496Z","updated_at":"2016-06-14T15:02:36.496Z","action":1,"author_id":1}]}
+{"id":15,"target_branch":"test-7","source_branch":"test-1","source_project_id":5,"author_id":22,"assignee_id":16,"title":"Qui accusantium et inventore facilis doloribus occaecati officiis.","created_at":"2016-06-14T15:02:25.168Z","updated_at":"2016-06-14T15:02:59.521Z","state":"opened","merge_status":"unchecked","target_project_id":5,"iid":7,"description":"Et commodi deserunt aspernatur vero rerum. Ut non dolorum alias in odit est libero. Voluptatibus eos in et vitae repudiandae facilis ex mollitia.","position":0,"updated_by_id":null,"merge_error":null,"merge_params":{"force_remove_source_branch":null},"merge_when_pipeline_succeeds":false,"merge_user_id":null,"merge_commit_sha":null,"notes":[{"id":777,"note":"Pariatur voluptas placeat aspernatur culpa suscipit soluta.","noteable_type":"MergeRequest","author_id":26,"created_at":"2016-06-14T15:02:59.348Z","updated_at":"2016-06-14T15:02:59.348Z","project_id":5,"attachment":{"url":null},"line_code":null,"commit_id":null,"noteable_id":15,"system":false,"st_diff":null,"updated_by_id":null,"author":{"name":"User 4"},"events":[]},{"id":778,"note":"Alias et iure mollitia suscipit molestiae voluptatum nostrum asperiores.","noteable_type":"MergeRequest","author_id":25,"created_at":"2016-06-14T15:02:59.372Z","updated_at":"2016-06-14T15:02:59.372Z","project_id":5,"attachment":{"url":null},"line_code":null,"commit_id":null,"noteable_id":15,"system":false,"st_diff":null,"updated_by_id":null,"author":{"name":"User 3"},"events":[]},{"id":779,"note":"Laudantium qui eum qui sunt.","noteable_type":"MergeRequest","author_id":22,"created_at":"2016-06-14T15:02:59.395Z","updated_at":"2016-06-14T15:02:59.395Z","project_id":5,"attachment":{"url":null},"line_code":null,"commit_id":null,"noteable_id":15,"system":false,"st_diff":null,"updated_by_id":null,"author":{"name":"User 0"},"events":[]},{"id":780,"note":"Quas rem est iusto ut delectus fugiat recusandae mollitia.","noteable_type":"MergeRequest","author_id":20,"created_at":"2016-06-14T15:02:59.418Z","updated_at":"2016-06-14T15:02:59.418Z","project_id":5,"attachment":{"url":null},"line_code":null,"commit_id":null,"noteable_id":15,"system":false,"st_diff":null,"updated_by_id":null,"author":{"name":"Ottis Schuster II"},"events":[]},{"id":781,"note":"Repellendus ab et qui nesciunt.","noteable_type":"MergeRequest","author_id":16,"created_at":"2016-06-14T15:02:59.444Z","updated_at":"2016-06-14T15:02:59.444Z","project_id":5,"attachment":{"url":null},"line_code":null,"commit_id":null,"noteable_id":15,"system":false,"st_diff":null,"updated_by_id":null,"author":{"name":"Rhett Emmerich IV"},"events":[]},{"id":782,"note":"Non possimus voluptatum odio qui ut.","noteable_type":"MergeRequest","author_id":15,"created_at":"2016-06-14T15:02:59.469Z","updated_at":"2016-06-14T15:02:59.469Z","project_id":5,"attachment":{"url":null},"line_code":null,"commit_id":null,"noteable_id":15,"system":false,"st_diff":null,"updated_by_id":null,"author":{"name":"Burdette Bernier"},"events":[]},{"id":783,"note":"Dolores repellendus eum ducimus quam ab dolorem quia.","noteable_type":"MergeRequest","author_id":6,"created_at":"2016-06-14T15:02:59.494Z","updated_at":"2016-06-14T15:02:59.494Z","project_id":5,"attachment":{"url":null},"line_code":null,"commit_id":null,"noteable_id":15,"system":false,"st_diff":null,"updated_by_id":null,"author":{"name":"Ari Wintheiser"},"events":[]},{"id":784,"note":"Facilis dolorem aut corrupti id ratione occaecati.","noteable_type":"MergeRequest","author_id":1,"created_at":"2016-06-14T15:02:59.520Z","updated_at":"2016-06-14T15:02:59.520Z","project_id":5,"attachment":{"url":null},"line_code":null,"commit_id":null,"noteable_id":15,"system":false,"st_diff":null,"updated_by_id":null,"author":{"name":"Administrator"},"events":[]}],"merge_request_diff":{"id":15,"state":"collected","merge_request_diff_commits":[{"merge_request_diff_id":15,"relative_order":0,"sha":"94b8d581c48d894b86661718582fecbc5e3ed2eb","message":"fixes #10\n","authored_date":"2016-01-19T13:22:56.000+01:00","author_name":"James Lopez","author_email":"james@jameslopez.es","committed_date":"2016-01-19T13:22:56.000+01:00","committer_name":"James Lopez","committer_email":"james@jameslopez.es"}],"merge_request_diff_files":[{"merge_request_diff_id":15,"relative_order":0,"utf8_diff":"--- /dev/null\n+++ b/test\n","new_path":"test","old_path":"test","a_mode":"0","b_mode":"100644","new_file":true,"renamed_file":false,"deleted_file":false,"too_large":false}],"merge_request_id":15,"created_at":"2016-06-14T15:02:25.171Z","updated_at":"2016-06-14T15:02:25.230Z","base_commit_sha":"be93687618e4b132087f430a4d8fc3a609c9b77c","real_size":"1"},"events":[{"id":223,"target_type":"MergeRequest","target_id":15,"project_id":36,"created_at":"2016-06-14T15:02:25.262Z","updated_at":"2016-06-14T15:02:25.262Z","action":1,"author_id":1},{"id":175,"target_type":"MergeRequest","target_id":15,"project_id":5,"created_at":"2016-06-14T15:02:25.262Z","updated_at":"2016-06-14T15:02:25.262Z","action":1,"author_id":22}]}
+{"id":14,"target_branch":"fix","source_branch":"test-3","source_project_id":5,"author_id":20,"assignee_id":20,"title":"In voluptas aut sequi voluptatem ullam vel corporis illum consequatur.","created_at":"2016-06-14T15:02:24.760Z","updated_at":"2016-06-14T15:02:59.749Z","state":"opened","merge_status":"unchecked","target_project_id":5,"iid":6,"description":"Dicta magnam non voluptates nam dignissimos nostrum deserunt. Dolorum et suscipit iure quae doloremque. Necessitatibus saepe aut labore sed.","position":0,"updated_by_id":null,"merge_error":null,"merge_params":{"force_remove_source_branch":null},"merge_when_pipeline_succeeds":false,"merge_user_id":null,"merge_commit_sha":null,"notes":[{"id":785,"note":"Atque cupiditate necessitatibus deserunt minus natus odit.","noteable_type":"MergeRequest","author_id":26,"created_at":"2016-06-14T15:02:59.559Z","updated_at":"2016-06-14T15:02:59.559Z","project_id":5,"attachment":{"url":null},"line_code":null,"commit_id":null,"noteable_id":14,"system":false,"st_diff":null,"updated_by_id":null,"author":{"name":"User 4"},"events":[]},{"id":786,"note":"Non dolorem provident mollitia nesciunt optio ex eveniet.","noteable_type":"MergeRequest","author_id":25,"created_at":"2016-06-14T15:02:59.587Z","updated_at":"2016-06-14T15:02:59.587Z","project_id":5,"attachment":{"url":null},"line_code":null,"commit_id":null,"noteable_id":14,"system":false,"st_diff":null,"updated_by_id":null,"author":{"name":"User 3"},"events":[]},{"id":787,"note":"Similique officia nemo quasi commodi accusantium quae qui.","noteable_type":"MergeRequest","author_id":22,"created_at":"2016-06-14T15:02:59.621Z","updated_at":"2016-06-14T15:02:59.621Z","project_id":5,"attachment":{"url":null},"line_code":null,"commit_id":null,"noteable_id":14,"system":false,"st_diff":null,"updated_by_id":null,"author":{"name":"User 0"},"events":[]},{"id":788,"note":"Et est et alias ad dolor qui.","noteable_type":"MergeRequest","author_id":20,"created_at":"2016-06-14T15:02:59.650Z","updated_at":"2016-06-14T15:02:59.650Z","project_id":5,"attachment":{"url":null},"line_code":null,"commit_id":null,"noteable_id":14,"system":false,"st_diff":null,"updated_by_id":null,"author":{"name":"Ottis Schuster II"},"events":[]},{"id":789,"note":"Numquam temporibus ratione voluptatibus aliquid.","noteable_type":"MergeRequest","author_id":16,"created_at":"2016-06-14T15:02:59.675Z","updated_at":"2016-06-14T15:02:59.675Z","project_id":5,"attachment":{"url":null},"line_code":null,"commit_id":null,"noteable_id":14,"system":false,"st_diff":null,"updated_by_id":null,"author":{"name":"Rhett Emmerich IV"},"events":[]},{"id":790,"note":"Ut ex aliquam consectetur perferendis est hic aut quia.","noteable_type":"MergeRequest","author_id":15,"created_at":"2016-06-14T15:02:59.703Z","updated_at":"2016-06-14T15:02:59.703Z","project_id":5,"attachment":{"url":null},"line_code":null,"commit_id":null,"noteable_id":14,"system":false,"st_diff":null,"updated_by_id":null,"author":{"name":"Burdette Bernier"},"events":[]},{"id":791,"note":"Esse eos quam quaerat aut ut asperiores officiis.","noteable_type":"MergeRequest","author_id":6,"created_at":"2016-06-14T15:02:59.726Z","updated_at":"2016-06-14T15:02:59.726Z","project_id":5,"attachment":{"url":null},"line_code":null,"commit_id":null,"noteable_id":14,"system":false,"st_diff":null,"updated_by_id":null,"author":{"name":"Ari Wintheiser"},"events":[]},{"id":792,"note":"Sint facilis accusantium iure blanditiis.","noteable_type":"MergeRequest","author_id":1,"created_at":"2016-06-14T15:02:59.748Z","updated_at":"2016-06-14T15:02:59.748Z","project_id":5,"attachment":{"url":null},"line_code":null,"commit_id":null,"noteable_id":14,"system":false,"st_diff":null,"updated_by_id":null,"author":{"name":"Administrator"},"events":[]}],"merge_request_diff":{"id":14,"state":"collected","merge_request_diff_commits":[{"merge_request_diff_id":14,"relative_order":0,"sha":"ddd4ff416a931589c695eb4f5b23f844426f6928","message":"fixes #10\n","authored_date":"2016-01-19T14:14:43.000+01:00","author_name":"James Lopez","author_email":"james@jameslopez.es","committed_date":"2016-01-19T14:14:43.000+01:00","committer_name":"James Lopez","committer_email":"james@jameslopez.es"},{"merge_request_diff_id":14,"relative_order":1,"sha":"be93687618e4b132087f430a4d8fc3a609c9b77c","message":"Merge branch 'master' into 'master'\r\n\r\nLFS object pointer.\r\n\r\n\r\n\r\nSee merge request !6","authored_date":"2015-12-07T12:52:12.000+01:00","author_name":"Marin Jankovski","author_email":"marin@gitlab.com","committed_date":"2015-12-07T12:52:12.000+01:00","committer_name":"Marin Jankovski","committer_email":"marin@gitlab.com"},{"merge_request_diff_id":14,"relative_order":2,"sha":"048721d90c449b244b7b4c53a9186b04330174ec","message":"LFS object pointer.\n","authored_date":"2015-12-07T11:54:28.000+01:00","author_name":"Marin Jankovski","author_email":"maxlazio@gmail.com","committed_date":"2015-12-07T11:54:28.000+01:00","committer_name":"Marin Jankovski","committer_email":"maxlazio@gmail.com"},{"merge_request_diff_id":14,"relative_order":3,"sha":"5f923865dde3436854e9ceb9cdb7815618d4e849","message":"GitLab currently doesn't support patches that involve a merge commit: add a commit here\n","authored_date":"2015-11-13T16:27:12.000+01:00","author_name":"Stan Hu","author_email":"stanhu@gmail.com","committed_date":"2015-11-13T16:27:12.000+01:00","committer_name":"Stan Hu","committer_email":"stanhu@gmail.com"},{"merge_request_diff_id":14,"relative_order":4,"sha":"d2d430676773caa88cdaf7c55944073b2fd5561a","message":"Merge branch 'add-svg' into 'master'\r\n\r\nAdd GitLab SVG\r\n\r\nAdded to test preview of sanitized SVG images\r\n\r\nSee merge request !5","authored_date":"2015-11-13T08:50:17.000+01:00","author_name":"Stan Hu","author_email":"stanhu@gmail.com","committed_date":"2015-11-13T08:50:17.000+01:00","committer_name":"Stan Hu","committer_email":"stanhu@gmail.com"},{"merge_request_diff_id":14,"relative_order":5,"sha":"2ea1f3dec713d940208fb5ce4a38765ecb5d3f73","message":"Add GitLab SVG\n","authored_date":"2015-11-13T08:39:43.000+01:00","author_name":"Stan Hu","author_email":"stanhu@gmail.com","committed_date":"2015-11-13T08:39:43.000+01:00","committer_name":"Stan Hu","committer_email":"stanhu@gmail.com"},{"merge_request_diff_id":14,"relative_order":6,"sha":"59e29889be61e6e0e5e223bfa9ac2721d31605b8","message":"Merge branch 'whitespace' into 'master'\r\n\r\nadd whitespace test file\r\n\r\nSorry, I did a mistake.\r\nGit ignore empty files.\r\nSo I add a new whitespace test file.\r\n\r\nSee merge request !4","authored_date":"2015-11-13T07:21:40.000+01:00","author_name":"Stan Hu","author_email":"stanhu@gmail.com","committed_date":"2015-11-13T07:21:40.000+01:00","committer_name":"Stan Hu","committer_email":"stanhu@gmail.com"},{"merge_request_diff_id":14,"relative_order":7,"sha":"66eceea0db202bb39c4e445e8ca28689645366c5","message":"add spaces in whitespace file\n","authored_date":"2015-11-13T06:01:27.000+01:00","author_name":"윤민ì‹","author_email":"minsik.yoon@samsung.com","committed_date":"2015-11-13T06:01:27.000+01:00","committer_name":"윤민ì‹","committer_email":"minsik.yoon@samsung.com"},{"merge_request_diff_id":14,"relative_order":8,"sha":"08f22f255f082689c0d7d39d19205085311542bc","message":"remove empty file.(beacase git ignore empty file)\nadd whitespace test file.\n","authored_date":"2015-11-13T06:00:16.000+01:00","author_name":"윤민ì‹","author_email":"minsik.yoon@samsung.com","committed_date":"2015-11-13T06:00:16.000+01:00","committer_name":"윤민ì‹","committer_email":"minsik.yoon@samsung.com"},{"merge_request_diff_id":14,"relative_order":9,"sha":"19e2e9b4ef76b422ce1154af39a91323ccc57434","message":"Merge branch 'whitespace' into 'master'\r\n\r\nadd spaces\r\n\r\nTo test this pull request.(https://github.com/gitlabhq/gitlabhq/pull/9757)\r\nJust add whitespaces.\r\n\r\nSee merge request !3","authored_date":"2015-11-13T05:23:14.000+01:00","author_name":"Stan Hu","author_email":"stanhu@gmail.com","committed_date":"2015-11-13T05:23:14.000+01:00","committer_name":"Stan Hu","committer_email":"stanhu@gmail.com"},{"merge_request_diff_id":14,"relative_order":10,"sha":"c642fe9b8b9f28f9225d7ea953fe14e74748d53b","message":"add whitespace in empty\n","authored_date":"2015-11-13T05:08:45.000+01:00","author_name":"윤민ì‹","author_email":"minsik.yoon@samsung.com","committed_date":"2015-11-13T05:08:45.000+01:00","committer_name":"윤민ì‹","committer_email":"minsik.yoon@samsung.com"},{"merge_request_diff_id":14,"relative_order":11,"sha":"9a944d90955aaf45f6d0c88f30e27f8d2c41cec0","message":"add empty file\n","authored_date":"2015-11-13T05:08:04.000+01:00","author_name":"윤민ì‹","author_email":"minsik.yoon@samsung.com","committed_date":"2015-11-13T05:08:04.000+01:00","committer_name":"윤민ì‹","committer_email":"minsik.yoon@samsung.com"},{"merge_request_diff_id":14,"relative_order":12,"sha":"c7fbe50c7c7419d9701eebe64b1fdacc3df5b9dd","message":"Add ISO-8859 test file\n","authored_date":"2015-08-25T17:53:12.000+02:00","author_name":"Stan Hu","author_email":"stanhu@packetzoom.com","committed_date":"2015-08-25T17:53:12.000+02:00","committer_name":"Stan Hu","committer_email":"stanhu@packetzoom.com"},{"merge_request_diff_id":14,"relative_order":13,"sha":"e56497bb5f03a90a51293fc6d516788730953899","message":"Merge branch 'tree_helper_spec' into 'master'\n\nAdd directory structure for tree_helper spec\n\nThis directory structure is needed for a testing the method flatten_tree(tree) in the TreeHelper module\n\nSee [merge request #275](https://gitlab.com/gitlab-org/gitlab-foss/merge_requests/275#note_732774)\n\nSee merge request !2\n","authored_date":"2015-01-10T22:23:29.000+01:00","author_name":"Sytse Sijbrandij","author_email":"sytse@gitlab.com","committed_date":"2015-01-10T22:23:29.000+01:00","committer_name":"Sytse Sijbrandij","committer_email":"sytse@gitlab.com"},{"merge_request_diff_id":14,"relative_order":14,"sha":"4cd80ccab63c82b4bad16faa5193fbd2aa06df40","message":"add directory structure for tree_helper spec\n","authored_date":"2015-01-10T21:28:18.000+01:00","author_name":"marmis85","author_email":"marmis85@gmail.com","committed_date":"2015-01-10T21:28:18.000+01:00","committer_name":"marmis85","committer_email":"marmis85@gmail.com"},{"merge_request_diff_id":14,"relative_order":15,"sha":"5937ac0a7beb003549fc5fd26fc247adbce4a52e","message":"Add submodule from gitlab.com\n\nSigned-off-by: Dmitriy Zaporozhets <dmitriy.zaporozhets@gmail.com>\n","authored_date":"2014-02-27T10:01:38.000+01:00","author_name":"Dmitriy Zaporozhets","author_email":"dmitriy.zaporozhets@gmail.com","committed_date":"2014-02-27T10:01:38.000+01:00","committer_name":"Dmitriy Zaporozhets","committer_email":"dmitriy.zaporozhets@gmail.com"},{"merge_request_diff_id":14,"relative_order":16,"sha":"570e7b2abdd848b95f2f578043fc23bd6f6fd24d","message":"Change some files\n\nSigned-off-by: Dmitriy Zaporozhets <dmitriy.zaporozhets@gmail.com>\n","authored_date":"2014-02-27T09:57:31.000+01:00","author_name":"Dmitriy Zaporozhets","author_email":"dmitriy.zaporozhets@gmail.com","committed_date":"2014-02-27T09:57:31.000+01:00","committer_name":"Dmitriy Zaporozhets","committer_email":"dmitriy.zaporozhets@gmail.com"},{"merge_request_diff_id":14,"relative_order":17,"sha":"6f6d7e7ed97bb5f0054f2b1df789b39ca89b6ff9","message":"More submodules\n\nSigned-off-by: Dmitriy Zaporozhets <dmitriy.zaporozhets@gmail.com>\n","authored_date":"2014-02-27T09:54:21.000+01:00","author_name":"Dmitriy Zaporozhets","author_email":"dmitriy.zaporozhets@gmail.com","committed_date":"2014-02-27T09:54:21.000+01:00","committer_name":"Dmitriy Zaporozhets","committer_email":"dmitriy.zaporozhets@gmail.com"},{"merge_request_diff_id":14,"relative_order":18,"sha":"d14d6c0abdd253381df51a723d58691b2ee1ab08","message":"Remove ds_store files\n\nSigned-off-by: Dmitriy Zaporozhets <dmitriy.zaporozhets@gmail.com>\n","authored_date":"2014-02-27T09:49:50.000+01:00","author_name":"Dmitriy Zaporozhets","author_email":"dmitriy.zaporozhets@gmail.com","committed_date":"2014-02-27T09:49:50.000+01:00","committer_name":"Dmitriy Zaporozhets","committer_email":"dmitriy.zaporozhets@gmail.com"},{"merge_request_diff_id":14,"relative_order":19,"sha":"c1acaa58bbcbc3eafe538cb8274ba387047b69f8","message":"Ignore DS files\n\nSigned-off-by: Dmitriy Zaporozhets <dmitriy.zaporozhets@gmail.com>\n","authored_date":"2014-02-27T09:48:32.000+01:00","author_name":"Dmitriy Zaporozhets","author_email":"dmitriy.zaporozhets@gmail.com","committed_date":"2014-02-27T09:48:32.000+01:00","committer_name":"Dmitriy Zaporozhets","committer_email":"dmitriy.zaporozhets@gmail.com"}],"merge_request_diff_files":[{"merge_request_diff_id":14,"relative_order":0,"utf8_diff":"Binary files a/.DS_Store and /dev/null differ\n","new_path":".DS_Store","old_path":".DS_Store","a_mode":"100644","b_mode":"0","new_file":false,"renamed_file":false,"deleted_file":true,"too_large":false},{"merge_request_diff_id":14,"relative_order":1,"utf8_diff":"--- a/.gitignore\n+++ b/.gitignore\n@@ -17,3 +17,4 @@ rerun.txt\n pickle-email-*.html\n .project\n config/initializers/secret_token.rb\n+.DS_Store\n","new_path":".gitignore","old_path":".gitignore","a_mode":"100644","b_mode":"100644","new_file":false,"renamed_file":false,"deleted_file":false,"too_large":false},{"merge_request_diff_id":14,"relative_order":2,"utf8_diff":"--- a/.gitmodules\n+++ b/.gitmodules\n@@ -1,3 +1,9 @@\n [submodule \"six\"]\n \tpath = six\n \turl = git://github.com/randx/six.git\n+[submodule \"gitlab-shell\"]\n+\tpath = gitlab-shell\n+\turl = https://github.com/gitlabhq/gitlab-shell.git\n+[submodule \"gitlab-grack\"]\n+\tpath = gitlab-grack\n+\turl = https://gitlab.com/gitlab-org/gitlab-grack.git\n","new_path":".gitmodules","old_path":".gitmodules","a_mode":"100644","b_mode":"100644","new_file":false,"renamed_file":false,"deleted_file":false,"too_large":false},{"merge_request_diff_id":14,"relative_order":3,"utf8_diff":"--- a/CHANGELOG\n+++ b/CHANGELOG\n@@ -1,4 +1,6 @@\n-v 6.7.0\n+v6.8.0\n+\n+v6.7.0\n - Add support for Gemnasium as a Project Service (Olivier Gonzalez)\n - Add edit file button to MergeRequest diff\n - Public groups (Jason Hollingsworth)\n","new_path":"CHANGELOG","old_path":"CHANGELOG","a_mode":"100644","b_mode":"100644","new_file":false,"renamed_file":false,"deleted_file":false,"too_large":false},{"merge_request_diff_id":14,"relative_order":4,"utf8_diff":"--- /dev/null\n+++ b/encoding/iso8859.txt\n@@ -0,0 +1 @@\n+Äü\n","new_path":"encoding/iso8859.txt","old_path":"encoding/iso8859.txt","a_mode":"0","b_mode":"100644","new_file":true,"renamed_file":false,"deleted_file":false,"too_large":false},{"merge_request_diff_id":14,"relative_order":5,"utf8_diff":"Binary files a/files/.DS_Store and /dev/null differ\n","new_path":"files/.DS_Store","old_path":"files/.DS_Store","a_mode":"100644","b_mode":"0","new_file":false,"renamed_file":false,"deleted_file":true,"too_large":false},{"merge_request_diff_id":14,"relative_order":6,"utf8_diff":"--- /dev/null\n+++ b/files/images/wm.svg\n@@ -0,0 +1,78 @@\n+<?xml version=\"1.0\" encoding=\"UTF-8\" standalone=\"no\"?>\n+<svg width=\"1300px\" height=\"680px\" viewBox=\"0 0 1300 680\" version=\"1.1\" xmlns=\"http://www.w3.org/2000/svg\" xmlns:xlink=\"http://www.w3.org/1999/xlink\" xmlns:sketch=\"http://www.bohemiancoding.com/sketch/ns\">\n+ <!-- Generator: Sketch 3.2.2 (9983) - http://www.bohemiancoding.com/sketch -->\n+ <title>wm</title>\n+ <desc>Created with Sketch.</desc>\n+ <defs>\n+ <path id=\"path-1\" d=\"M-69.8,1023.54607 L1675.19996,1023.54607 L1675.19996,0 L-69.8,0 L-69.8,1023.54607 L-69.8,1023.54607 Z\"></path>\n+ </defs>\n+ <g id=\"Page-1\" stroke=\"none\" stroke-width=\"1\" fill=\"none\" fill-rule=\"evenodd\" sketch:type=\"MSPage\">\n+ <path d=\"M1300,680 L0,680 L0,0 L1300,0 L1300,680 L1300,680 Z\" id=\"bg\" fill=\"#30353E\" sketch:type=\"MSShapeGroup\"></path>\n+ <g id=\"gitlab_logo\" sketch:type=\"MSLayerGroup\" transform=\"translate(-262.000000, -172.000000)\">\n+ <g id=\"g10\" transform=\"translate(872.500000, 512.354581) scale(1, -1) translate(-872.500000, -512.354581) translate(0.000000, 0.290751)\">\n+ <g id=\"g12\" transform=\"translate(1218.022652, 440.744871)\" fill=\"#8C929D\" sketch:type=\"MSShapeGroup\">\n+ <path d=\"M-50.0233338,141.900706 L-69.07059,141.900706 L-69.0100967,0.155858152 L8.04444805,0.155858152 L8.04444805,17.6840847 L-49.9628405,17.6840847 L-50.0233338,141.900706 L-50.0233338,141.900706 Z\" id=\"path14\"></path>\n+ </g>\n+ <g id=\"g16\">\n+ <g id=\"g18-Clipped\">\n+ <mask id=\"mask-2\" sketch:name=\"path22\" fill=\"white\">\n+ <use xlink:href=\"#path-1\"></use>\n+ </mask>\n+ <g id=\"path22\"></g>\n+ <g id=\"g18\" mask=\"url(#mask-2)\">\n+ <g transform=\"translate(382.736659, 312.879425)\">\n+ <g id=\"g24\" stroke-width=\"1\" fill=\"none\" sketch:type=\"MSLayerGroup\" transform=\"translate(852.718192, 124.992771)\">\n+ <path d=\"M63.9833317,27.9148929 C59.2218085,22.9379001 51.2134221,17.9597442 40.3909323,17.9597442 C25.8888194,17.9597442 20.0453962,25.1013043 20.0453962,34.4074318 C20.0453962,48.4730484 29.7848226,55.1819277 50.5642821,55.1819277 C54.4602853,55.1819277 60.7364685,54.7492469 63.9833317,54.1002256 L63.9833317,27.9148929 L63.9833317,27.9148929 Z M44.2869356,113.827628 C28.9053426,113.827628 14.7975996,108.376082 3.78897657,99.301416 L10.5211864,87.6422957 C18.3131929,92.1866076 27.8374026,96.7320827 41.4728323,96.7320827 C57.0568452,96.7320827 63.9833317,88.7239978 63.9833317,75.3074024 L63.9833317,68.3821827 C60.9528485,69.0312039 54.6766653,69.4650479 50.7806621,69.4650479 C17.4476729,69.4650479 0.565379986,57.7791759 0.565379986,33.3245665 C0.565379986,11.4683685 13.9844297,0.43151772 34.3299658,0.43151772 C48.0351955,0.43151772 61.1692285,6.70771614 65.7143717,16.8780421 L69.1776149,3.02876588 L82.5978279,3.02876588 L82.5978279,75.5237428 C82.5978279,98.462806 72.6408582,113.827628 44.2869356,113.827628 L44.2869356,113.827628 Z\" id=\"path26\" fill=\"#8C929D\" sketch:type=\"MSShapeGroup\"></path>\n+ </g>\n+ <g id=\"g28\" stroke-width=\"1\" fill=\"none\" sketch:type=\"MSLayerGroup\" transform=\"translate(959.546624, 124.857151)\">\n+ <path d=\"M37.2266657,17.4468081 C30.0837992,17.4468081 23.8064527,18.3121698 19.0449295,20.4767371 L19.0449295,79.2306079 L19.0449295,86.0464943 C25.538656,91.457331 33.5470425,95.3526217 43.7203922,95.3526217 C62.1173451,95.3526217 69.2602116,82.3687072 69.2602116,61.3767077 C69.2602116,31.5135879 57.7885819,17.4468081 37.2266657,17.4468081 M45.2315622,113.963713 C28.208506,113.963713 19.0449295,102.384849 19.0449295,102.384849 L19.0449295,120.67143 L18.9844362,144.908535 L10.3967097,144.908535 L0.371103324,144.908535 L0.431596656,6.62629771 C9.73826309,2.73100702 22.5081728,0.567602823 36.3611458,0.567602823 C71.8579349,0.567602823 88.9566078,23.2891625 88.9566078,62.4584098 C88.9566078,93.4043948 73.1527248,113.963713 45.2315622,113.963713\" id=\"path30\" fill=\"#8C929D\" sketch:type=\"MSShapeGroup\"></path>\n+ </g>\n+ <g id=\"g32\" stroke-width=\"1\" fill=\"none\" sketch:type=\"MSLayerGroup\" transform=\"translate(509.576747, 125.294950)\">\n+ <path d=\"M68.636665,129.10638 C85.5189579,129.10638 96.3414476,123.480366 103.484314,117.853189 L111.669527,132.029302 C100.513161,141.811145 85.5073245,147.06845 69.5021849,147.06845 C29.0274926,147.06845 0.673569983,122.3975 0.673569983,72.6252464 C0.673569983,20.4709215 31.2622559,0.12910638 66.2553217,0.12910638 C83.7879179,0.12910638 98.7227909,4.24073748 108.462217,8.35236859 L108.063194,64.0763105 L108.063194,70.6502677 L108.063194,81.6057001 L56.1168719,81.6057001 L56.1168719,64.0763105 L89.2323178,64.0763105 L89.6313411,21.7701271 C85.3025779,19.6055598 77.7269514,17.8748364 67.554765,17.8748364 C39.4172223,17.8748364 20.5863462,35.5717154 20.5863462,72.8415868 C20.5863462,110.711628 40.0663623,129.10638 68.636665,129.10638\" id=\"path34\" fill=\"#8C929D\" sketch:type=\"MSShapeGroup\"></path>\n+ </g>\n+ <g id=\"g36\" stroke-width=\"1\" fill=\"none\" sketch:type=\"MSLayerGroup\" transform=\"translate(692.388992, 124.376085)\">\n+ <path d=\"M19.7766662,145.390067 L1.16216997,145.390067 L1.2226633,121.585642 L1.2226633,111.846834 L1.2226633,106.170806 L1.2226633,96.2656714 L1.2226633,39.5681976 L1.2226633,39.3518572 C1.2226633,16.4127939 11.1796331,1.04797161 39.5335557,1.04797161 C43.4504989,1.04797161 47.2836822,1.40388649 51.0051854,2.07965952 L51.0051854,18.7925385 C48.3109055,18.3796307 45.4351455,18.1446804 42.3476589,18.1446804 C26.763646,18.1446804 19.8371595,26.1516022 19.8371595,39.5681976 L19.8371595,96.2656714 L51.0051854,96.2656714 L51.0051854,111.846834 L19.8371595,111.846834 L19.7766662,145.390067 L19.7766662,145.390067 Z\" id=\"path38\" fill=\"#8C929D\" sketch:type=\"MSShapeGroup\"></path>\n+ </g>\n+ <path d=\"M646.318899,128.021188 L664.933395,128.021188 L664.933395,236.223966 L646.318899,236.223966 L646.318899,128.021188 L646.318899,128.021188 Z\" id=\"path40\" fill=\"#8C929D\" sketch:type=\"MSShapeGroup\"></path>\n+ <path d=\"M646.318899,251.154944 L664.933395,251.154944 L664.933395,269.766036 L646.318899,269.766036 L646.318899,251.154944 L646.318899,251.154944 Z\" id=\"path42\" fill=\"#8C929D\" sketch:type=\"MSShapeGroup\"></path>\n+ <g id=\"g44\" stroke-width=\"1\" fill=\"none\" sketch:type=\"MSLayerGroup\" transform=\"translate(0.464170, 0.676006)\">\n+ <path d=\"M429.269989,169.815599 L405.225053,243.802859 L357.571431,390.440955 C355.120288,397.984955 344.444378,397.984955 341.992071,390.440955 L294.337286,243.802859 L136.094873,243.802859 L88.4389245,390.440955 C85.9877812,397.984955 75.3118715,397.984955 72.8595648,390.440955 L25.2059427,243.802859 L1.16216997,169.815599 C-1.03187664,163.067173 1.37156997,155.674379 7.11261982,151.503429 L215.215498,0.336141836 L423.319539,151.503429 C429.060589,155.674379 431.462873,163.067173 429.269989,169.815599\" id=\"path46\" fill=\"#FC6D26\" sketch:type=\"MSShapeGroup\"></path>\n+ </g>\n+ <g id=\"g48\" stroke-width=\"1\" fill=\"none\" sketch:type=\"MSLayerGroup\" transform=\"translate(135.410135, 1.012147)\">\n+ <path d=\"M80.269998,0 L80.269998,0 L159.391786,243.466717 L1.14820997,243.466717 L80.269998,0 L80.269998,0 Z\" id=\"path50\" fill=\"#E24329\" sketch:type=\"MSShapeGroup\"></path>\n+ </g>\n+ <g id=\"g52\" stroke-width=\"1\" fill=\"none\" sketch:type=\"MSLayerGroup\" transform=\"translate(215.680133, 1.012147)\">\n+ <g id=\"path54\"></g>\n+ </g>\n+ <g id=\"g56\" stroke-width=\"1\" fill=\"none\" sketch:type=\"MSLayerGroup\" transform=\"translate(24.893471, 1.012613)\">\n+ <path d=\"M190.786662,0 L111.664874,243.465554 L0.777106647,243.465554 L190.786662,0 L190.786662,0 Z\" id=\"path58\" fill=\"#FC6D26\" sketch:type=\"MSShapeGroup\"></path>\n+ </g>\n+ <g id=\"g60\" stroke-width=\"1\" fill=\"none\" sketch:type=\"MSLayerGroup\" transform=\"translate(215.680133, 1.012613)\">\n+ <g id=\"path62\"></g>\n+ </g>\n+ <g id=\"g64\" stroke-width=\"1\" fill=\"none\" sketch:type=\"MSLayerGroup\" transform=\"translate(0.077245, 0.223203)\">\n+ <path d=\"M25.5933327,244.255313 L25.5933327,244.255313 L1.54839663,170.268052 C-0.644486651,163.519627 1.75779662,156.126833 7.50000981,151.957046 L215.602888,0.789758846 L25.5933327,244.255313 L25.5933327,244.255313 Z\" id=\"path66\" fill=\"#FCA326\" sketch:type=\"MSShapeGroup\"></path>\n+ </g>\n+ <g id=\"g68\" stroke-width=\"1\" fill=\"none\" sketch:type=\"MSLayerGroup\" transform=\"translate(215.680133, 1.012147)\">\n+ <g id=\"path70\"></g>\n+ </g>\n+ <g id=\"g72\" stroke-width=\"1\" fill=\"none\" sketch:type=\"MSLayerGroup\" transform=\"translate(25.670578, 244.478283)\">\n+ <path d=\"M0,0 L110.887767,0 L63.2329818,146.638096 C60.7806751,154.183259 50.1047654,154.183259 47.6536221,146.638096 L0,0 L0,0 Z\" id=\"path74\" fill=\"#E24329\" sketch:type=\"MSShapeGroup\"></path>\n+ </g>\n+ <g id=\"g76\" stroke-width=\"1\" fill=\"none\" sketch:type=\"MSLayerGroup\" transform=\"translate(215.680133, 1.012613)\">\n+ <path d=\"M0,0 L79.121788,243.465554 L190.009555,243.465554 L0,0 L0,0 Z\" id=\"path78\" fill=\"#FC6D26\" sketch:type=\"MSShapeGroup\"></path>\n+ </g>\n+ <g id=\"g80\" stroke-width=\"1\" fill=\"none\" sketch:type=\"MSLayerGroup\" transform=\"translate(214.902910, 0.223203)\">\n+ <path d=\"M190.786662,244.255313 L190.786662,244.255313 L214.831598,170.268052 C217.024481,163.519627 214.622198,156.126833 208.879985,151.957046 L0.777106647,0.789758846 L190.786662,244.255313 L190.786662,244.255313 Z\" id=\"path82\" fill=\"#FCA326\" sketch:type=\"MSShapeGroup\"></path>\n+ </g>\n+ <g id=\"g84\" stroke-width=\"1\" fill=\"none\" sketch:type=\"MSLayerGroup\" transform=\"translate(294.009575, 244.478283)\">\n+ <path d=\"M111.679997,0 L0.79222998,0 L48.4470155,146.638096 C50.8993221,154.183259 61.5752318,154.183259 64.0263751,146.638096 L111.679997,0 L111.679997,0 Z\" id=\"path86\" fill=\"#E24329\" sketch:type=\"MSShapeGroup\"></path>\n+ </g>\n+ </g>\n+ </g>\n+ </g>\n+ </g>\n+ </g>\n+ </g>\n+ </g>\n+</svg>\n\\ No newline at end of file\n","new_path":"files/images/wm.svg","old_path":"files/images/wm.svg","a_mode":"0","b_mode":"100644","new_file":true,"renamed_file":false,"deleted_file":false,"too_large":false},{"merge_request_diff_id":14,"relative_order":7,"utf8_diff":"--- /dev/null\n+++ b/files/lfs/lfs_object.iso\n@@ -0,0 +1,4 @@\n+version https://git-lfs.github.com/spec/v1\n+oid sha256:91eff75a492a3ed0dfcb544d7f31326bc4014c8551849c192fd1e48d4dd2c897\n+size 1575078\n+\n","new_path":"files/lfs/lfs_object.iso","old_path":"files/lfs/lfs_object.iso","a_mode":"0","b_mode":"100644","new_file":true,"renamed_file":false,"deleted_file":false,"too_large":false},{"merge_request_diff_id":14,"relative_order":8,"utf8_diff":"--- a/files/ruby/popen.rb\n+++ b/files/ruby/popen.rb\n@@ -6,12 +6,18 @@ module Popen\n \n def popen(cmd, path=nil)\n unless cmd.is_a?(Array)\n- raise \"System commands must be given as an array of strings\"\n+ raise RuntimeError, \"System commands must be given as an array of strings\"\n end\n \n path ||= Dir.pwd\n- vars = { \"PWD\" => path }\n- options = { chdir: path }\n+\n+ vars = {\n+ \"PWD\" => path\n+ }\n+\n+ options = {\n+ chdir: path\n+ }\n \n unless File.directory?(path)\n FileUtils.mkdir_p(path)\n@@ -19,6 +25,7 @@ module Popen\n \n @cmd_output = \"\"\n @cmd_status = 0\n+\n Open3.popen3(vars, *cmd, options) do |stdin, stdout, stderr, wait_thr|\n @cmd_output << stdout.read\n @cmd_output << stderr.read\n","new_path":"files/ruby/popen.rb","old_path":"files/ruby/popen.rb","a_mode":"100644","b_mode":"100644","new_file":false,"renamed_file":false,"deleted_file":false,"too_large":false},{"merge_request_diff_id":14,"relative_order":9,"utf8_diff":"--- a/files/ruby/regex.rb\n+++ b/files/ruby/regex.rb\n@@ -19,14 +19,12 @@ module Gitlab\n end\n \n def archive_formats_regex\n- #|zip|tar| tar.gz | tar.bz2 |\n- /(zip|tar|tar\\.gz|tgz|gz|tar\\.bz2|tbz|tbz2|tb2|bz2)/\n+ /(zip|tar|7z|tar\\.gz|tgz|gz|tar\\.bz2|tbz|tbz2|tb2|bz2)/\n end\n \n def git_reference_regex\n # Valid git ref regex, see:\n # https://www.kernel.org/pub/software/scm/git/docs/git-check-ref-format.html\n-\n %r{\n (?!\n (?# doesn't begins with)\n","new_path":"files/ruby/regex.rb","old_path":"files/ruby/regex.rb","a_mode":"100644","b_mode":"100644","new_file":false,"renamed_file":false,"deleted_file":false,"too_large":false},{"merge_request_diff_id":14,"relative_order":10,"utf8_diff":"--- /dev/null\n+++ b/files/whitespace\n@@ -0,0 +1 @@\n+test \n","new_path":"files/whitespace","old_path":"files/whitespace","a_mode":"0","b_mode":"100644","new_file":true,"renamed_file":false,"deleted_file":false,"too_large":false},{"merge_request_diff_id":14,"relative_order":11,"utf8_diff":"--- /dev/null\n+++ b/foo/bar/.gitkeep\n","new_path":"foo/bar/.gitkeep","old_path":"foo/bar/.gitkeep","a_mode":"0","b_mode":"100644","new_file":true,"renamed_file":false,"deleted_file":false,"too_large":false},{"merge_request_diff_id":14,"relative_order":12,"utf8_diff":"--- /dev/null\n+++ b/gitlab-grack\n@@ -0,0 +1 @@\n+Subproject commit 645f6c4c82fd3f5e06f67134450a570b795e55a6\n","new_path":"gitlab-grack","old_path":"gitlab-grack","a_mode":"0","b_mode":"160000","new_file":true,"renamed_file":false,"deleted_file":false,"too_large":false},{"merge_request_diff_id":14,"relative_order":13,"utf8_diff":"--- /dev/null\n+++ b/gitlab-shell\n@@ -0,0 +1 @@\n+Subproject commit 79bceae69cb5750d6567b223597999bfa91cb3b9\n","new_path":"gitlab-shell","old_path":"gitlab-shell","a_mode":"0","b_mode":"160000","new_file":true,"renamed_file":false,"deleted_file":false,"too_large":false},{"merge_request_diff_id":14,"relative_order":14,"utf8_diff":"--- /dev/null\n+++ b/test\n","new_path":"test","old_path":"test","a_mode":"0","b_mode":"100644","new_file":true,"renamed_file":false,"deleted_file":false,"too_large":false}],"merge_request_id":14,"created_at":"2016-06-14T15:02:24.770Z","updated_at":"2016-06-14T15:02:25.007Z","base_commit_sha":"ae73cb07c9eeaf35924a10f713b364d32b2dd34f","real_size":"15"},"events":[{"id":224,"target_type":"MergeRequest","target_id":14,"project_id":36,"created_at":"2016-06-14T15:02:25.113Z","updated_at":"2016-06-14T15:02:25.113Z","action":1,"author_id":1},{"id":174,"target_type":"MergeRequest","target_id":14,"project_id":5,"created_at":"2016-06-14T15:02:25.113Z","updated_at":"2016-06-14T15:02:25.113Z","action":1,"author_id":20}]}
+{"id":13,"target_branch":"improve/awesome","source_branch":"test-8","source_project_id":5,"author_id":16,"assignee_id":25,"title":"Voluptates consequatur eius nemo amet libero animi illum delectus tempore.","created_at":"2016-06-14T15:02:24.415Z","updated_at":"2016-06-14T15:02:59.958Z","state":"opened","merge_status":"unchecked","target_project_id":5,"iid":5,"description":"Est eaque quasi qui qui. Similique voluptatem impedit iusto ratione reprehenderit. Itaque est illum ut nulla aut.","position":0,"updated_by_id":null,"merge_error":null,"merge_params":{"force_remove_source_branch":null},"merge_when_pipeline_succeeds":false,"merge_user_id":null,"merge_commit_sha":null,"notes":[{"id":793,"note":"In illum maxime aperiam nulla est aspernatur.","noteable_type":"MergeRequest","author_id":26,"created_at":"2016-06-14T15:02:59.782Z","updated_at":"2016-06-14T15:02:59.782Z","project_id":5,"attachment":{"url":null},"line_code":null,"commit_id":null,"noteable_id":13,"system":false,"st_diff":null,"updated_by_id":null,"author":{"name":"User 4"},"events":[{"merge_request_diff_id":14,"id":529,"target_type":"Note","target_id":793,"project_id":4,"created_at":"2016-07-07T14:35:12.128Z","updated_at":"2016-07-07T14:35:12.128Z","action":6,"author_id":1}]},{"id":794,"note":"Enim quia perferendis cum distinctio tenetur optio voluptas veniam.","noteable_type":"MergeRequest","author_id":25,"created_at":"2016-06-14T15:02:59.807Z","updated_at":"2016-06-14T15:02:59.807Z","project_id":5,"attachment":{"url":null},"line_code":null,"commit_id":null,"noteable_id":13,"system":false,"st_diff":null,"updated_by_id":null,"author":{"name":"User 3"},"events":[]},{"id":795,"note":"Dolor ad quia quis pariatur ducimus.","noteable_type":"MergeRequest","author_id":22,"created_at":"2016-06-14T15:02:59.831Z","updated_at":"2016-06-14T15:02:59.831Z","project_id":5,"attachment":{"url":null},"line_code":null,"commit_id":null,"noteable_id":13,"system":false,"st_diff":null,"updated_by_id":null,"author":{"name":"User 0"},"events":[]},{"id":796,"note":"Et a odio voluptate aut.","noteable_type":"MergeRequest","author_id":20,"created_at":"2016-06-14T15:02:59.854Z","updated_at":"2016-06-14T15:02:59.854Z","project_id":5,"attachment":{"url":null},"line_code":null,"commit_id":null,"noteable_id":13,"system":false,"st_diff":null,"updated_by_id":null,"author":{"name":"Ottis Schuster II"},"events":[]},{"id":797,"note":"Quis nihil temporibus voluptatum modi minima a ut.","noteable_type":"MergeRequest","author_id":16,"created_at":"2016-06-14T15:02:59.879Z","updated_at":"2016-06-14T15:02:59.879Z","project_id":5,"attachment":{"url":null},"line_code":null,"commit_id":null,"noteable_id":13,"system":false,"st_diff":null,"updated_by_id":null,"author":{"name":"Rhett Emmerich IV"},"events":[]},{"id":798,"note":"Ut alias consequatur in nostrum.","noteable_type":"MergeRequest","author_id":15,"created_at":"2016-06-14T15:02:59.904Z","updated_at":"2016-06-14T15:02:59.904Z","project_id":5,"attachment":{"url":null},"line_code":null,"commit_id":null,"noteable_id":13,"system":false,"st_diff":null,"updated_by_id":null,"author":{"name":"Burdette Bernier"},"events":[]},{"id":799,"note":"Voluptatibus aperiam assumenda et neque sint libero.","noteable_type":"MergeRequest","author_id":6,"created_at":"2016-06-14T15:02:59.926Z","updated_at":"2016-06-14T15:02:59.926Z","project_id":5,"attachment":{"url":null},"line_code":null,"commit_id":null,"noteable_id":13,"system":false,"st_diff":null,"updated_by_id":null,"author":{"name":"Ari Wintheiser"},"events":[]},{"id":800,"note":"Veritatis voluptatem dolor dolores magni quo ut ipsa fuga.","noteable_type":"MergeRequest","author_id":1,"created_at":"2016-06-14T15:02:59.956Z","updated_at":"2016-06-14T15:02:59.956Z","project_id":5,"attachment":{"url":null},"line_code":null,"commit_id":null,"noteable_id":13,"system":false,"st_diff":null,"updated_by_id":null,"author":{"name":"Administrator"},"events":[]}],"merge_request_diff":{"id":13,"state":"collected","merge_request_diff_commits":[{"merge_request_diff_id":13,"relative_order":0,"sha":"0bfedc29d30280c7e8564e19f654584b459e5868","message":"fixes #10\n","authored_date":"2016-01-19T15:25:23.000+01:00","author_name":"James Lopez","author_email":"james@jameslopez.es","committed_date":"2016-01-19T15:25:23.000+01:00","committer_name":"James Lopez","committer_email":"james@jameslopez.es"},{"merge_request_diff_id":13,"relative_order":1,"sha":"be93687618e4b132087f430a4d8fc3a609c9b77c","message":"Merge branch 'master' into 'master'\r\n\r\nLFS object pointer.\r\n\r\n\r\n\r\nSee merge request !6","authored_date":"2015-12-07T12:52:12.000+01:00","author_name":"Marin Jankovski","author_email":"marin@gitlab.com","committed_date":"2015-12-07T12:52:12.000+01:00","committer_name":"Marin Jankovski","committer_email":"marin@gitlab.com"},{"merge_request_diff_id":13,"relative_order":2,"sha":"048721d90c449b244b7b4c53a9186b04330174ec","message":"LFS object pointer.\n","authored_date":"2015-12-07T11:54:28.000+01:00","author_name":"Marin Jankovski","author_email":"maxlazio@gmail.com","committed_date":"2015-12-07T11:54:28.000+01:00","committer_name":"Marin Jankovski","committer_email":"maxlazio@gmail.com"},{"merge_request_diff_id":13,"relative_order":3,"sha":"5f923865dde3436854e9ceb9cdb7815618d4e849","message":"GitLab currently doesn't support patches that involve a merge commit: add a commit here\n","authored_date":"2015-11-13T16:27:12.000+01:00","author_name":"Stan Hu","author_email":"stanhu@gmail.com","committed_date":"2015-11-13T16:27:12.000+01:00","committer_name":"Stan Hu","committer_email":"stanhu@gmail.com"},{"merge_request_diff_id":13,"relative_order":4,"sha":"d2d430676773caa88cdaf7c55944073b2fd5561a","message":"Merge branch 'add-svg' into 'master'\r\n\r\nAdd GitLab SVG\r\n\r\nAdded to test preview of sanitized SVG images\r\n\r\nSee merge request !5","authored_date":"2015-11-13T08:50:17.000+01:00","author_name":"Stan Hu","author_email":"stanhu@gmail.com","committed_date":"2015-11-13T08:50:17.000+01:00","committer_name":"Stan Hu","committer_email":"stanhu@gmail.com"},{"merge_request_diff_id":13,"relative_order":5,"sha":"2ea1f3dec713d940208fb5ce4a38765ecb5d3f73","message":"Add GitLab SVG\n","authored_date":"2015-11-13T08:39:43.000+01:00","author_name":"Stan Hu","author_email":"stanhu@gmail.com","committed_date":"2015-11-13T08:39:43.000+01:00","committer_name":"Stan Hu","committer_email":"stanhu@gmail.com"},{"merge_request_diff_id":13,"relative_order":6,"sha":"59e29889be61e6e0e5e223bfa9ac2721d31605b8","message":"Merge branch 'whitespace' into 'master'\r\n\r\nadd whitespace test file\r\n\r\nSorry, I did a mistake.\r\nGit ignore empty files.\r\nSo I add a new whitespace test file.\r\n\r\nSee merge request !4","authored_date":"2015-11-13T07:21:40.000+01:00","author_name":"Stan Hu","author_email":"stanhu@gmail.com","committed_date":"2015-11-13T07:21:40.000+01:00","committer_name":"Stan Hu","committer_email":"stanhu@gmail.com"},{"merge_request_diff_id":13,"relative_order":7,"sha":"66eceea0db202bb39c4e445e8ca28689645366c5","message":"add spaces in whitespace file\n","authored_date":"2015-11-13T06:01:27.000+01:00","author_name":"윤민ì‹","author_email":"minsik.yoon@samsung.com","committed_date":"2015-11-13T06:01:27.000+01:00","committer_name":"윤민ì‹","committer_email":"minsik.yoon@samsung.com"},{"merge_request_diff_id":13,"relative_order":8,"sha":"08f22f255f082689c0d7d39d19205085311542bc","message":"remove empty file.(beacase git ignore empty file)\nadd whitespace test file.\n","authored_date":"2015-11-13T06:00:16.000+01:00","author_name":"윤민ì‹","author_email":"minsik.yoon@samsung.com","committed_date":"2015-11-13T06:00:16.000+01:00","committer_name":"윤민ì‹","committer_email":"minsik.yoon@samsung.com"},{"merge_request_diff_id":13,"relative_order":9,"sha":"19e2e9b4ef76b422ce1154af39a91323ccc57434","message":"Merge branch 'whitespace' into 'master'\r\n\r\nadd spaces\r\n\r\nTo test this pull request.(https://github.com/gitlabhq/gitlabhq/pull/9757)\r\nJust add whitespaces.\r\n\r\nSee merge request !3","authored_date":"2015-11-13T05:23:14.000+01:00","author_name":"Stan Hu","author_email":"stanhu@gmail.com","committed_date":"2015-11-13T05:23:14.000+01:00","committer_name":"Stan Hu","committer_email":"stanhu@gmail.com"},{"merge_request_diff_id":13,"relative_order":10,"sha":"c642fe9b8b9f28f9225d7ea953fe14e74748d53b","message":"add whitespace in empty\n","authored_date":"2015-11-13T05:08:45.000+01:00","author_name":"윤민ì‹","author_email":"minsik.yoon@samsung.com","committed_date":"2015-11-13T05:08:45.000+01:00","committer_name":"윤민ì‹","committer_email":"minsik.yoon@samsung.com"},{"merge_request_diff_id":13,"relative_order":11,"sha":"9a944d90955aaf45f6d0c88f30e27f8d2c41cec0","message":"add empty file\n","authored_date":"2015-11-13T05:08:04.000+01:00","author_name":"윤민ì‹","author_email":"minsik.yoon@samsung.com","committed_date":"2015-11-13T05:08:04.000+01:00","committer_name":"윤민ì‹","committer_email":"minsik.yoon@samsung.com"},{"merge_request_diff_id":13,"relative_order":12,"sha":"c7fbe50c7c7419d9701eebe64b1fdacc3df5b9dd","message":"Add ISO-8859 test file\n","authored_date":"2015-08-25T17:53:12.000+02:00","author_name":"Stan Hu","author_email":"stanhu@packetzoom.com","committed_date":"2015-08-25T17:53:12.000+02:00","committer_name":"Stan Hu","committer_email":"stanhu@packetzoom.com"},{"merge_request_diff_id":13,"relative_order":13,"sha":"e56497bb5f03a90a51293fc6d516788730953899","message":"Merge branch 'tree_helper_spec' into 'master'\n\nAdd directory structure for tree_helper spec\n\nThis directory structure is needed for a testing the method flatten_tree(tree) in the TreeHelper module\n\nSee [merge request #275](https://gitlab.com/gitlab-org/gitlab-foss/merge_requests/275#note_732774)\n\nSee merge request !2\n","authored_date":"2015-01-10T22:23:29.000+01:00","author_name":"Sytse Sijbrandij","author_email":"sytse@gitlab.com","committed_date":"2015-01-10T22:23:29.000+01:00","committer_name":"Sytse Sijbrandij","committer_email":"sytse@gitlab.com"},{"merge_request_diff_id":13,"relative_order":14,"sha":"4cd80ccab63c82b4bad16faa5193fbd2aa06df40","message":"add directory structure for tree_helper spec\n","authored_date":"2015-01-10T21:28:18.000+01:00","author_name":"marmis85","author_email":"marmis85@gmail.com","committed_date":"2015-01-10T21:28:18.000+01:00","committer_name":"marmis85","committer_email":"marmis85@gmail.com"}],"merge_request_diff_files":[{"merge_request_diff_id":13,"relative_order":0,"utf8_diff":"--- a/CHANGELOG\n+++ b/CHANGELOG\n@@ -1,4 +1,6 @@\n-v 6.7.0\n+v6.8.0\n+\n+v6.7.0\n - Add support for Gemnasium as a Project Service (Olivier Gonzalez)\n - Add edit file button to MergeRequest diff\n - Public groups (Jason Hollingsworth)\n","new_path":"CHANGELOG","old_path":"CHANGELOG","a_mode":"100644","b_mode":"100644","new_file":false,"renamed_file":false,"deleted_file":false,"too_large":false},{"merge_request_diff_id":13,"relative_order":1,"utf8_diff":"--- /dev/null\n+++ b/encoding/iso8859.txt\n@@ -0,0 +1 @@\n+Äü\n","new_path":"encoding/iso8859.txt","old_path":"encoding/iso8859.txt","a_mode":"0","b_mode":"100644","new_file":true,"renamed_file":false,"deleted_file":false,"too_large":false},{"merge_request_diff_id":13,"relative_order":2,"utf8_diff":"--- /dev/null\n+++ b/files/images/wm.svg\n@@ -0,0 +1,78 @@\n+<?xml version=\"1.0\" encoding=\"UTF-8\" standalone=\"no\"?>\n+<svg width=\"1300px\" height=\"680px\" viewBox=\"0 0 1300 680\" version=\"1.1\" xmlns=\"http://www.w3.org/2000/svg\" xmlns:xlink=\"http://www.w3.org/1999/xlink\" xmlns:sketch=\"http://www.bohemiancoding.com/sketch/ns\">\n+ <!-- Generator: Sketch 3.2.2 (9983) - http://www.bohemiancoding.com/sketch -->\n+ <title>wm</title>\n+ <desc>Created with Sketch.</desc>\n+ <defs>\n+ <path id=\"path-1\" d=\"M-69.8,1023.54607 L1675.19996,1023.54607 L1675.19996,0 L-69.8,0 L-69.8,1023.54607 L-69.8,1023.54607 Z\"></path>\n+ </defs>\n+ <g id=\"Page-1\" stroke=\"none\" stroke-width=\"1\" fill=\"none\" fill-rule=\"evenodd\" sketch:type=\"MSPage\">\n+ <path d=\"M1300,680 L0,680 L0,0 L1300,0 L1300,680 L1300,680 Z\" id=\"bg\" fill=\"#30353E\" sketch:type=\"MSShapeGroup\"></path>\n+ <g id=\"gitlab_logo\" sketch:type=\"MSLayerGroup\" transform=\"translate(-262.000000, -172.000000)\">\n+ <g id=\"g10\" transform=\"translate(872.500000, 512.354581) scale(1, -1) translate(-872.500000, -512.354581) translate(0.000000, 0.290751)\">\n+ <g id=\"g12\" transform=\"translate(1218.022652, 440.744871)\" fill=\"#8C929D\" sketch:type=\"MSShapeGroup\">\n+ <path d=\"M-50.0233338,141.900706 L-69.07059,141.900706 L-69.0100967,0.155858152 L8.04444805,0.155858152 L8.04444805,17.6840847 L-49.9628405,17.6840847 L-50.0233338,141.900706 L-50.0233338,141.900706 Z\" id=\"path14\"></path>\n+ </g>\n+ <g id=\"g16\">\n+ <g id=\"g18-Clipped\">\n+ <mask id=\"mask-2\" sketch:name=\"path22\" fill=\"white\">\n+ <use xlink:href=\"#path-1\"></use>\n+ </mask>\n+ <g id=\"path22\"></g>\n+ <g id=\"g18\" mask=\"url(#mask-2)\">\n+ <g transform=\"translate(382.736659, 312.879425)\">\n+ <g id=\"g24\" stroke-width=\"1\" fill=\"none\" sketch:type=\"MSLayerGroup\" transform=\"translate(852.718192, 124.992771)\">\n+ <path d=\"M63.9833317,27.9148929 C59.2218085,22.9379001 51.2134221,17.9597442 40.3909323,17.9597442 C25.8888194,17.9597442 20.0453962,25.1013043 20.0453962,34.4074318 C20.0453962,48.4730484 29.7848226,55.1819277 50.5642821,55.1819277 C54.4602853,55.1819277 60.7364685,54.7492469 63.9833317,54.1002256 L63.9833317,27.9148929 L63.9833317,27.9148929 Z M44.2869356,113.827628 C28.9053426,113.827628 14.7975996,108.376082 3.78897657,99.301416 L10.5211864,87.6422957 C18.3131929,92.1866076 27.8374026,96.7320827 41.4728323,96.7320827 C57.0568452,96.7320827 63.9833317,88.7239978 63.9833317,75.3074024 L63.9833317,68.3821827 C60.9528485,69.0312039 54.6766653,69.4650479 50.7806621,69.4650479 C17.4476729,69.4650479 0.565379986,57.7791759 0.565379986,33.3245665 C0.565379986,11.4683685 13.9844297,0.43151772 34.3299658,0.43151772 C48.0351955,0.43151772 61.1692285,6.70771614 65.7143717,16.8780421 L69.1776149,3.02876588 L82.5978279,3.02876588 L82.5978279,75.5237428 C82.5978279,98.462806 72.6408582,113.827628 44.2869356,113.827628 L44.2869356,113.827628 Z\" id=\"path26\" fill=\"#8C929D\" sketch:type=\"MSShapeGroup\"></path>\n+ </g>\n+ <g id=\"g28\" stroke-width=\"1\" fill=\"none\" sketch:type=\"MSLayerGroup\" transform=\"translate(959.546624, 124.857151)\">\n+ <path d=\"M37.2266657,17.4468081 C30.0837992,17.4468081 23.8064527,18.3121698 19.0449295,20.4767371 L19.0449295,79.2306079 L19.0449295,86.0464943 C25.538656,91.457331 33.5470425,95.3526217 43.7203922,95.3526217 C62.1173451,95.3526217 69.2602116,82.3687072 69.2602116,61.3767077 C69.2602116,31.5135879 57.7885819,17.4468081 37.2266657,17.4468081 M45.2315622,113.963713 C28.208506,113.963713 19.0449295,102.384849 19.0449295,102.384849 L19.0449295,120.67143 L18.9844362,144.908535 L10.3967097,144.908535 L0.371103324,144.908535 L0.431596656,6.62629771 C9.73826309,2.73100702 22.5081728,0.567602823 36.3611458,0.567602823 C71.8579349,0.567602823 88.9566078,23.2891625 88.9566078,62.4584098 C88.9566078,93.4043948 73.1527248,113.963713 45.2315622,113.963713\" id=\"path30\" fill=\"#8C929D\" sketch:type=\"MSShapeGroup\"></path>\n+ </g>\n+ <g id=\"g32\" stroke-width=\"1\" fill=\"none\" sketch:type=\"MSLayerGroup\" transform=\"translate(509.576747, 125.294950)\">\n+ <path d=\"M68.636665,129.10638 C85.5189579,129.10638 96.3414476,123.480366 103.484314,117.853189 L111.669527,132.029302 C100.513161,141.811145 85.5073245,147.06845 69.5021849,147.06845 C29.0274926,147.06845 0.673569983,122.3975 0.673569983,72.6252464 C0.673569983,20.4709215 31.2622559,0.12910638 66.2553217,0.12910638 C83.7879179,0.12910638 98.7227909,4.24073748 108.462217,8.35236859 L108.063194,64.0763105 L108.063194,70.6502677 L108.063194,81.6057001 L56.1168719,81.6057001 L56.1168719,64.0763105 L89.2323178,64.0763105 L89.6313411,21.7701271 C85.3025779,19.6055598 77.7269514,17.8748364 67.554765,17.8748364 C39.4172223,17.8748364 20.5863462,35.5717154 20.5863462,72.8415868 C20.5863462,110.711628 40.0663623,129.10638 68.636665,129.10638\" id=\"path34\" fill=\"#8C929D\" sketch:type=\"MSShapeGroup\"></path>\n+ </g>\n+ <g id=\"g36\" stroke-width=\"1\" fill=\"none\" sketch:type=\"MSLayerGroup\" transform=\"translate(692.388992, 124.376085)\">\n+ <path d=\"M19.7766662,145.390067 L1.16216997,145.390067 L1.2226633,121.585642 L1.2226633,111.846834 L1.2226633,106.170806 L1.2226633,96.2656714 L1.2226633,39.5681976 L1.2226633,39.3518572 C1.2226633,16.4127939 11.1796331,1.04797161 39.5335557,1.04797161 C43.4504989,1.04797161 47.2836822,1.40388649 51.0051854,2.07965952 L51.0051854,18.7925385 C48.3109055,18.3796307 45.4351455,18.1446804 42.3476589,18.1446804 C26.763646,18.1446804 19.8371595,26.1516022 19.8371595,39.5681976 L19.8371595,96.2656714 L51.0051854,96.2656714 L51.0051854,111.846834 L19.8371595,111.846834 L19.7766662,145.390067 L19.7766662,145.390067 Z\" id=\"path38\" fill=\"#8C929D\" sketch:type=\"MSShapeGroup\"></path>\n+ </g>\n+ <path d=\"M646.318899,128.021188 L664.933395,128.021188 L664.933395,236.223966 L646.318899,236.223966 L646.318899,128.021188 L646.318899,128.021188 Z\" id=\"path40\" fill=\"#8C929D\" sketch:type=\"MSShapeGroup\"></path>\n+ <path d=\"M646.318899,251.154944 L664.933395,251.154944 L664.933395,269.766036 L646.318899,269.766036 L646.318899,251.154944 L646.318899,251.154944 Z\" id=\"path42\" fill=\"#8C929D\" sketch:type=\"MSShapeGroup\"></path>\n+ <g id=\"g44\" stroke-width=\"1\" fill=\"none\" sketch:type=\"MSLayerGroup\" transform=\"translate(0.464170, 0.676006)\">\n+ <path d=\"M429.269989,169.815599 L405.225053,243.802859 L357.571431,390.440955 C355.120288,397.984955 344.444378,397.984955 341.992071,390.440955 L294.337286,243.802859 L136.094873,243.802859 L88.4389245,390.440955 C85.9877812,397.984955 75.3118715,397.984955 72.8595648,390.440955 L25.2059427,243.802859 L1.16216997,169.815599 C-1.03187664,163.067173 1.37156997,155.674379 7.11261982,151.503429 L215.215498,0.336141836 L423.319539,151.503429 C429.060589,155.674379 431.462873,163.067173 429.269989,169.815599\" id=\"path46\" fill=\"#FC6D26\" sketch:type=\"MSShapeGroup\"></path>\n+ </g>\n+ <g id=\"g48\" stroke-width=\"1\" fill=\"none\" sketch:type=\"MSLayerGroup\" transform=\"translate(135.410135, 1.012147)\">\n+ <path d=\"M80.269998,0 L80.269998,0 L159.391786,243.466717 L1.14820997,243.466717 L80.269998,0 L80.269998,0 Z\" id=\"path50\" fill=\"#E24329\" sketch:type=\"MSShapeGroup\"></path>\n+ </g>\n+ <g id=\"g52\" stroke-width=\"1\" fill=\"none\" sketch:type=\"MSLayerGroup\" transform=\"translate(215.680133, 1.012147)\">\n+ <g id=\"path54\"></g>\n+ </g>\n+ <g id=\"g56\" stroke-width=\"1\" fill=\"none\" sketch:type=\"MSLayerGroup\" transform=\"translate(24.893471, 1.012613)\">\n+ <path d=\"M190.786662,0 L111.664874,243.465554 L0.777106647,243.465554 L190.786662,0 L190.786662,0 Z\" id=\"path58\" fill=\"#FC6D26\" sketch:type=\"MSShapeGroup\"></path>\n+ </g>\n+ <g id=\"g60\" stroke-width=\"1\" fill=\"none\" sketch:type=\"MSLayerGroup\" transform=\"translate(215.680133, 1.012613)\">\n+ <g id=\"path62\"></g>\n+ </g>\n+ <g id=\"g64\" stroke-width=\"1\" fill=\"none\" sketch:type=\"MSLayerGroup\" transform=\"translate(0.077245, 0.223203)\">\n+ <path d=\"M25.5933327,244.255313 L25.5933327,244.255313 L1.54839663,170.268052 C-0.644486651,163.519627 1.75779662,156.126833 7.50000981,151.957046 L215.602888,0.789758846 L25.5933327,244.255313 L25.5933327,244.255313 Z\" id=\"path66\" fill=\"#FCA326\" sketch:type=\"MSShapeGroup\"></path>\n+ </g>\n+ <g id=\"g68\" stroke-width=\"1\" fill=\"none\" sketch:type=\"MSLayerGroup\" transform=\"translate(215.680133, 1.012147)\">\n+ <g id=\"path70\"></g>\n+ </g>\n+ <g id=\"g72\" stroke-width=\"1\" fill=\"none\" sketch:type=\"MSLayerGroup\" transform=\"translate(25.670578, 244.478283)\">\n+ <path d=\"M0,0 L110.887767,0 L63.2329818,146.638096 C60.7806751,154.183259 50.1047654,154.183259 47.6536221,146.638096 L0,0 L0,0 Z\" id=\"path74\" fill=\"#E24329\" sketch:type=\"MSShapeGroup\"></path>\n+ </g>\n+ <g id=\"g76\" stroke-width=\"1\" fill=\"none\" sketch:type=\"MSLayerGroup\" transform=\"translate(215.680133, 1.012613)\">\n+ <path d=\"M0,0 L79.121788,243.465554 L190.009555,243.465554 L0,0 L0,0 Z\" id=\"path78\" fill=\"#FC6D26\" sketch:type=\"MSShapeGroup\"></path>\n+ </g>\n+ <g id=\"g80\" stroke-width=\"1\" fill=\"none\" sketch:type=\"MSLayerGroup\" transform=\"translate(214.902910, 0.223203)\">\n+ <path d=\"M190.786662,244.255313 L190.786662,244.255313 L214.831598,170.268052 C217.024481,163.519627 214.622198,156.126833 208.879985,151.957046 L0.777106647,0.789758846 L190.786662,244.255313 L190.786662,244.255313 Z\" id=\"path82\" fill=\"#FCA326\" sketch:type=\"MSShapeGroup\"></path>\n+ </g>\n+ <g id=\"g84\" stroke-width=\"1\" fill=\"none\" sketch:type=\"MSLayerGroup\" transform=\"translate(294.009575, 244.478283)\">\n+ <path d=\"M111.679997,0 L0.79222998,0 L48.4470155,146.638096 C50.8993221,154.183259 61.5752318,154.183259 64.0263751,146.638096 L111.679997,0 L111.679997,0 Z\" id=\"path86\" fill=\"#E24329\" sketch:type=\"MSShapeGroup\"></path>\n+ </g>\n+ </g>\n+ </g>\n+ </g>\n+ </g>\n+ </g>\n+ </g>\n+ </g>\n+</svg>\n\\ No newline at end of file\n","new_path":"files/images/wm.svg","old_path":"files/images/wm.svg","a_mode":"0","b_mode":"100644","new_file":true,"renamed_file":false,"deleted_file":false,"too_large":false},{"merge_request_diff_id":13,"relative_order":3,"utf8_diff":"--- /dev/null\n+++ b/files/lfs/lfs_object.iso\n@@ -0,0 +1,4 @@\n+version https://git-lfs.github.com/spec/v1\n+oid sha256:91eff75a492a3ed0dfcb544d7f31326bc4014c8551849c192fd1e48d4dd2c897\n+size 1575078\n+\n","new_path":"files/lfs/lfs_object.iso","old_path":"files/lfs/lfs_object.iso","a_mode":"0","b_mode":"100644","new_file":true,"renamed_file":false,"deleted_file":false,"too_large":false},{"merge_request_diff_id":13,"relative_order":4,"utf8_diff":"--- /dev/null\n+++ b/files/whitespace\n@@ -0,0 +1 @@\n+test \n","new_path":"files/whitespace","old_path":"files/whitespace","a_mode":"0","b_mode":"100644","new_file":true,"renamed_file":false,"deleted_file":false,"too_large":false},{"merge_request_diff_id":13,"relative_order":5,"utf8_diff":"--- /dev/null\n+++ b/foo/bar/.gitkeep\n","new_path":"foo/bar/.gitkeep","old_path":"foo/bar/.gitkeep","a_mode":"0","b_mode":"100644","new_file":true,"renamed_file":false,"deleted_file":false,"too_large":false},{"merge_request_diff_id":13,"relative_order":6,"utf8_diff":"--- /dev/null\n+++ b/test\n","new_path":"test","old_path":"test","a_mode":"0","b_mode":"100644","new_file":true,"renamed_file":false,"deleted_file":false,"too_large":false}],"merge_request_id":13,"created_at":"2016-06-14T15:02:24.420Z","updated_at":"2016-06-14T15:02:24.561Z","base_commit_sha":"5937ac0a7beb003549fc5fd26fc247adbce4a52e","real_size":"7"},"events":[{"id":225,"target_type":"MergeRequest","target_id":13,"project_id":36,"created_at":"2016-06-14T15:02:24.636Z","updated_at":"2016-06-14T15:02:24.636Z","action":1,"author_id":16},{"id":173,"target_type":"MergeRequest","target_id":13,"project_id":5,"created_at":"2016-06-14T15:02:24.636Z","updated_at":"2016-06-14T15:02:24.636Z","action":1,"author_id":16}]}
+{"id":12,"target_branch":"flatten-dirs","source_branch":"test-2","source_project_id":5,"author_id":1,"assignee_id":22,"title":"In a rerum harum nihil accusamus aut quia nobis non.","created_at":"2016-06-14T15:02:24.000Z","updated_at":"2016-06-14T15:03:00.225Z","state":"opened","merge_status":"unchecked","target_project_id":5,"iid":4,"description":"Nam magnam odit velit rerum. Sapiente dolore sunt saepe debitis. Culpa maiores ut ad dolores dolorem et.","position":0,"updated_by_id":null,"merge_error":null,"merge_params":{"force_remove_source_branch":null},"merge_when_pipeline_succeeds":false,"merge_user_id":null,"merge_commit_sha":null,"notes":[{"id":801,"note":"Nihil dicta molestias expedita atque.","noteable_type":"MergeRequest","author_id":26,"created_at":"2016-06-14T15:03:00.001Z","updated_at":"2016-06-14T15:03:00.001Z","project_id":5,"attachment":{"url":null},"line_code":null,"commit_id":null,"noteable_id":12,"system":false,"st_diff":null,"updated_by_id":null,"author":{"name":"User 4"},"events":[]},{"id":802,"note":"Illum culpa voluptas enim accusantium deserunt.","noteable_type":"MergeRequest","author_id":25,"created_at":"2016-06-14T15:03:00.034Z","updated_at":"2016-06-14T15:03:00.034Z","project_id":5,"attachment":{"url":null},"line_code":null,"commit_id":null,"noteable_id":12,"system":false,"st_diff":null,"updated_by_id":null,"author":{"name":"User 3"},"events":[]},{"id":803,"note":"Dicta esse aliquam laboriosam unde alias.","noteable_type":"MergeRequest","author_id":22,"created_at":"2016-06-14T15:03:00.065Z","updated_at":"2016-06-14T15:03:00.065Z","project_id":5,"attachment":{"url":null},"line_code":null,"commit_id":null,"noteable_id":12,"system":false,"st_diff":null,"updated_by_id":null,"author":{"name":"User 0"},"events":[]},{"id":804,"note":"Dicta autem et sed molestiae ut quae.","noteable_type":"MergeRequest","author_id":20,"created_at":"2016-06-14T15:03:00.097Z","updated_at":"2016-06-14T15:03:00.097Z","project_id":5,"attachment":{"url":null},"line_code":null,"commit_id":null,"noteable_id":12,"system":false,"st_diff":null,"updated_by_id":null,"author":{"name":"Ottis Schuster II"},"events":[]},{"id":805,"note":"Ut ut temporibus voluptas dolore quia velit.","noteable_type":"MergeRequest","author_id":16,"created_at":"2016-06-14T15:03:00.129Z","updated_at":"2016-06-14T15:03:00.129Z","project_id":5,"attachment":{"url":null},"line_code":null,"commit_id":null,"noteable_id":12,"system":false,"st_diff":null,"updated_by_id":null,"author":{"name":"Rhett Emmerich IV"},"events":[]},{"id":806,"note":"Dolores similique sint pariatur error id quia fugit aut.","noteable_type":"MergeRequest","author_id":15,"created_at":"2016-06-14T15:03:00.162Z","updated_at":"2016-06-14T15:03:00.162Z","project_id":5,"attachment":{"url":null},"line_code":null,"commit_id":null,"noteable_id":12,"system":false,"st_diff":null,"updated_by_id":null,"author":{"name":"Burdette Bernier"},"events":[]},{"id":807,"note":"Quisquam provident nihil aperiam voluptatem.","noteable_type":"MergeRequest","author_id":6,"created_at":"2016-06-14T15:03:00.193Z","updated_at":"2016-06-14T15:03:00.193Z","project_id":5,"attachment":{"url":null},"line_code":null,"commit_id":null,"noteable_id":12,"system":false,"st_diff":null,"updated_by_id":null,"author":{"name":"Ari Wintheiser"},"events":[]},{"id":808,"note":"Similique quo vero expedita deserunt ipsam earum.","noteable_type":"MergeRequest","author_id":1,"created_at":"2016-06-14T15:03:00.224Z","updated_at":"2016-06-14T15:03:00.224Z","project_id":5,"attachment":{"url":null},"line_code":null,"commit_id":null,"noteable_id":12,"system":false,"st_diff":null,"updated_by_id":null,"author":{"name":"Administrator"},"events":[]}],"merge_request_diff":{"id":12,"state":"collected","merge_request_diff_commits":[{"merge_request_diff_id":12,"relative_order":0,"sha":"97a0df9696e2aebf10c31b3016f40214e0e8f243","message":"fixes #10\n","authored_date":"2016-01-19T14:08:21.000+01:00","author_name":"James Lopez","author_email":"james@jameslopez.es","committed_date":"2016-01-19T14:08:21.000+01:00","committer_name":"James Lopez","committer_email":"james@jameslopez.es"},{"merge_request_diff_id":12,"relative_order":1,"sha":"be93687618e4b132087f430a4d8fc3a609c9b77c","message":"Merge branch 'master' into 'master'\r\n\r\nLFS object pointer.\r\n\r\n\r\n\r\nSee merge request !6","authored_date":"2015-12-07T12:52:12.000+01:00","author_name":"Marin Jankovski","author_email":"marin@gitlab.com","committed_date":"2015-12-07T12:52:12.000+01:00","committer_name":"Marin Jankovski","committer_email":"marin@gitlab.com"},{"merge_request_diff_id":12,"relative_order":2,"sha":"048721d90c449b244b7b4c53a9186b04330174ec","message":"LFS object pointer.\n","authored_date":"2015-12-07T11:54:28.000+01:00","author_name":"Marin Jankovski","author_email":"maxlazio@gmail.com","committed_date":"2015-12-07T11:54:28.000+01:00","committer_name":"Marin Jankovski","committer_email":"maxlazio@gmail.com"},{"merge_request_diff_id":12,"relative_order":3,"sha":"5f923865dde3436854e9ceb9cdb7815618d4e849","message":"GitLab currently doesn't support patches that involve a merge commit: add a commit here\n","authored_date":"2015-11-13T16:27:12.000+01:00","author_name":"Stan Hu","author_email":"stanhu@gmail.com","committed_date":"2015-11-13T16:27:12.000+01:00","committer_name":"Stan Hu","committer_email":"stanhu@gmail.com"},{"merge_request_diff_id":12,"relative_order":4,"sha":"d2d430676773caa88cdaf7c55944073b2fd5561a","message":"Merge branch 'add-svg' into 'master'\r\n\r\nAdd GitLab SVG\r\n\r\nAdded to test preview of sanitized SVG images\r\n\r\nSee merge request !5","authored_date":"2015-11-13T08:50:17.000+01:00","author_name":"Stan Hu","author_email":"stanhu@gmail.com","committed_date":"2015-11-13T08:50:17.000+01:00","committer_name":"Stan Hu","committer_email":"stanhu@gmail.com"},{"merge_request_diff_id":12,"relative_order":5,"sha":"2ea1f3dec713d940208fb5ce4a38765ecb5d3f73","message":"Add GitLab SVG\n","authored_date":"2015-11-13T08:39:43.000+01:00","author_name":"Stan Hu","author_email":"stanhu@gmail.com","committed_date":"2015-11-13T08:39:43.000+01:00","committer_name":"Stan Hu","committer_email":"stanhu@gmail.com"},{"merge_request_diff_id":12,"relative_order":6,"sha":"59e29889be61e6e0e5e223bfa9ac2721d31605b8","message":"Merge branch 'whitespace' into 'master'\r\n\r\nadd whitespace test file\r\n\r\nSorry, I did a mistake.\r\nGit ignore empty files.\r\nSo I add a new whitespace test file.\r\n\r\nSee merge request !4","authored_date":"2015-11-13T07:21:40.000+01:00","author_name":"Stan Hu","author_email":"stanhu@gmail.com","committed_date":"2015-11-13T07:21:40.000+01:00","committer_name":"Stan Hu","committer_email":"stanhu@gmail.com"},{"merge_request_diff_id":12,"relative_order":7,"sha":"66eceea0db202bb39c4e445e8ca28689645366c5","message":"add spaces in whitespace file\n","authored_date":"2015-11-13T06:01:27.000+01:00","author_name":"윤민ì‹","author_email":"minsik.yoon@samsung.com","committed_date":"2015-11-13T06:01:27.000+01:00","committer_name":"윤민ì‹","committer_email":"minsik.yoon@samsung.com"},{"merge_request_diff_id":12,"relative_order":8,"sha":"08f22f255f082689c0d7d39d19205085311542bc","message":"remove empty file.(beacase git ignore empty file)\nadd whitespace test file.\n","authored_date":"2015-11-13T06:00:16.000+01:00","author_name":"윤민ì‹","author_email":"minsik.yoon@samsung.com","committed_date":"2015-11-13T06:00:16.000+01:00","committer_name":"윤민ì‹","committer_email":"minsik.yoon@samsung.com"},{"merge_request_diff_id":12,"relative_order":9,"sha":"19e2e9b4ef76b422ce1154af39a91323ccc57434","message":"Merge branch 'whitespace' into 'master'\r\n\r\nadd spaces\r\n\r\nTo test this pull request.(https://github.com/gitlabhq/gitlabhq/pull/9757)\r\nJust add whitespaces.\r\n\r\nSee merge request !3","authored_date":"2015-11-13T05:23:14.000+01:00","author_name":"Stan Hu","author_email":"stanhu@gmail.com","committed_date":"2015-11-13T05:23:14.000+01:00","committer_name":"Stan Hu","committer_email":"stanhu@gmail.com"},{"merge_request_diff_id":12,"relative_order":10,"sha":"c642fe9b8b9f28f9225d7ea953fe14e74748d53b","message":"add whitespace in empty\n","authored_date":"2015-11-13T05:08:45.000+01:00","author_name":"윤민ì‹","author_email":"minsik.yoon@samsung.com","committed_date":"2015-11-13T05:08:45.000+01:00","committer_name":"윤민ì‹","committer_email":"minsik.yoon@samsung.com"},{"merge_request_diff_id":12,"relative_order":11,"sha":"9a944d90955aaf45f6d0c88f30e27f8d2c41cec0","message":"add empty file\n","authored_date":"2015-11-13T05:08:04.000+01:00","author_name":"윤민ì‹","author_email":"minsik.yoon@samsung.com","committed_date":"2015-11-13T05:08:04.000+01:00","committer_name":"윤민ì‹","committer_email":"minsik.yoon@samsung.com"},{"merge_request_diff_id":12,"relative_order":12,"sha":"c7fbe50c7c7419d9701eebe64b1fdacc3df5b9dd","message":"Add ISO-8859 test file\n","authored_date":"2015-08-25T17:53:12.000+02:00","author_name":"Stan Hu","author_email":"stanhu@packetzoom.com","committed_date":"2015-08-25T17:53:12.000+02:00","committer_name":"Stan Hu","committer_email":"stanhu@packetzoom.com"}],"merge_request_diff_files":[{"merge_request_diff_id":12,"relative_order":0,"utf8_diff":"--- a/CHANGELOG\n+++ b/CHANGELOG\n@@ -1,4 +1,6 @@\n-v 6.7.0\n+v6.8.0\n+\n+v6.7.0\n - Add support for Gemnasium as a Project Service (Olivier Gonzalez)\n - Add edit file button to MergeRequest diff\n - Public groups (Jason Hollingsworth)\n","new_path":"CHANGELOG","old_path":"CHANGELOG","a_mode":"100644","b_mode":"100644","new_file":false,"renamed_file":false,"deleted_file":false,"too_large":false},{"merge_request_diff_id":12,"relative_order":1,"utf8_diff":"--- /dev/null\n+++ b/encoding/iso8859.txt\n@@ -0,0 +1 @@\n+Äü\n","new_path":"encoding/iso8859.txt","old_path":"encoding/iso8859.txt","a_mode":"0","b_mode":"100644","new_file":true,"renamed_file":false,"deleted_file":false,"too_large":false},{"merge_request_diff_id":12,"relative_order":2,"utf8_diff":"--- /dev/null\n+++ b/files/images/wm.svg\n@@ -0,0 +1,78 @@\n+<?xml version=\"1.0\" encoding=\"UTF-8\" standalone=\"no\"?>\n+<svg width=\"1300px\" height=\"680px\" viewBox=\"0 0 1300 680\" version=\"1.1\" xmlns=\"http://www.w3.org/2000/svg\" xmlns:xlink=\"http://www.w3.org/1999/xlink\" xmlns:sketch=\"http://www.bohemiancoding.com/sketch/ns\">\n+ <!-- Generator: Sketch 3.2.2 (9983) - http://www.bohemiancoding.com/sketch -->\n+ <title>wm</title>\n+ <desc>Created with Sketch.</desc>\n+ <defs>\n+ <path id=\"path-1\" d=\"M-69.8,1023.54607 L1675.19996,1023.54607 L1675.19996,0 L-69.8,0 L-69.8,1023.54607 L-69.8,1023.54607 Z\"></path>\n+ </defs>\n+ <g id=\"Page-1\" stroke=\"none\" stroke-width=\"1\" fill=\"none\" fill-rule=\"evenodd\" sketch:type=\"MSPage\">\n+ <path d=\"M1300,680 L0,680 L0,0 L1300,0 L1300,680 L1300,680 Z\" id=\"bg\" fill=\"#30353E\" sketch:type=\"MSShapeGroup\"></path>\n+ <g id=\"gitlab_logo\" sketch:type=\"MSLayerGroup\" transform=\"translate(-262.000000, -172.000000)\">\n+ <g id=\"g10\" transform=\"translate(872.500000, 512.354581) scale(1, -1) translate(-872.500000, -512.354581) translate(0.000000, 0.290751)\">\n+ <g id=\"g12\" transform=\"translate(1218.022652, 440.744871)\" fill=\"#8C929D\" sketch:type=\"MSShapeGroup\">\n+ <path d=\"M-50.0233338,141.900706 L-69.07059,141.900706 L-69.0100967,0.155858152 L8.04444805,0.155858152 L8.04444805,17.6840847 L-49.9628405,17.6840847 L-50.0233338,141.900706 L-50.0233338,141.900706 Z\" id=\"path14\"></path>\n+ </g>\n+ <g id=\"g16\">\n+ <g id=\"g18-Clipped\">\n+ <mask id=\"mask-2\" sketch:name=\"path22\" fill=\"white\">\n+ <use xlink:href=\"#path-1\"></use>\n+ </mask>\n+ <g id=\"path22\"></g>\n+ <g id=\"g18\" mask=\"url(#mask-2)\">\n+ <g transform=\"translate(382.736659, 312.879425)\">\n+ <g id=\"g24\" stroke-width=\"1\" fill=\"none\" sketch:type=\"MSLayerGroup\" transform=\"translate(852.718192, 124.992771)\">\n+ <path d=\"M63.9833317,27.9148929 C59.2218085,22.9379001 51.2134221,17.9597442 40.3909323,17.9597442 C25.8888194,17.9597442 20.0453962,25.1013043 20.0453962,34.4074318 C20.0453962,48.4730484 29.7848226,55.1819277 50.5642821,55.1819277 C54.4602853,55.1819277 60.7364685,54.7492469 63.9833317,54.1002256 L63.9833317,27.9148929 L63.9833317,27.9148929 Z M44.2869356,113.827628 C28.9053426,113.827628 14.7975996,108.376082 3.78897657,99.301416 L10.5211864,87.6422957 C18.3131929,92.1866076 27.8374026,96.7320827 41.4728323,96.7320827 C57.0568452,96.7320827 63.9833317,88.7239978 63.9833317,75.3074024 L63.9833317,68.3821827 C60.9528485,69.0312039 54.6766653,69.4650479 50.7806621,69.4650479 C17.4476729,69.4650479 0.565379986,57.7791759 0.565379986,33.3245665 C0.565379986,11.4683685 13.9844297,0.43151772 34.3299658,0.43151772 C48.0351955,0.43151772 61.1692285,6.70771614 65.7143717,16.8780421 L69.1776149,3.02876588 L82.5978279,3.02876588 L82.5978279,75.5237428 C82.5978279,98.462806 72.6408582,113.827628 44.2869356,113.827628 L44.2869356,113.827628 Z\" id=\"path26\" fill=\"#8C929D\" sketch:type=\"MSShapeGroup\"></path>\n+ </g>\n+ <g id=\"g28\" stroke-width=\"1\" fill=\"none\" sketch:type=\"MSLayerGroup\" transform=\"translate(959.546624, 124.857151)\">\n+ <path d=\"M37.2266657,17.4468081 C30.0837992,17.4468081 23.8064527,18.3121698 19.0449295,20.4767371 L19.0449295,79.2306079 L19.0449295,86.0464943 C25.538656,91.457331 33.5470425,95.3526217 43.7203922,95.3526217 C62.1173451,95.3526217 69.2602116,82.3687072 69.2602116,61.3767077 C69.2602116,31.5135879 57.7885819,17.4468081 37.2266657,17.4468081 M45.2315622,113.963713 C28.208506,113.963713 19.0449295,102.384849 19.0449295,102.384849 L19.0449295,120.67143 L18.9844362,144.908535 L10.3967097,144.908535 L0.371103324,144.908535 L0.431596656,6.62629771 C9.73826309,2.73100702 22.5081728,0.567602823 36.3611458,0.567602823 C71.8579349,0.567602823 88.9566078,23.2891625 88.9566078,62.4584098 C88.9566078,93.4043948 73.1527248,113.963713 45.2315622,113.963713\" id=\"path30\" fill=\"#8C929D\" sketch:type=\"MSShapeGroup\"></path>\n+ </g>\n+ <g id=\"g32\" stroke-width=\"1\" fill=\"none\" sketch:type=\"MSLayerGroup\" transform=\"translate(509.576747, 125.294950)\">\n+ <path d=\"M68.636665,129.10638 C85.5189579,129.10638 96.3414476,123.480366 103.484314,117.853189 L111.669527,132.029302 C100.513161,141.811145 85.5073245,147.06845 69.5021849,147.06845 C29.0274926,147.06845 0.673569983,122.3975 0.673569983,72.6252464 C0.673569983,20.4709215 31.2622559,0.12910638 66.2553217,0.12910638 C83.7879179,0.12910638 98.7227909,4.24073748 108.462217,8.35236859 L108.063194,64.0763105 L108.063194,70.6502677 L108.063194,81.6057001 L56.1168719,81.6057001 L56.1168719,64.0763105 L89.2323178,64.0763105 L89.6313411,21.7701271 C85.3025779,19.6055598 77.7269514,17.8748364 67.554765,17.8748364 C39.4172223,17.8748364 20.5863462,35.5717154 20.5863462,72.8415868 C20.5863462,110.711628 40.0663623,129.10638 68.636665,129.10638\" id=\"path34\" fill=\"#8C929D\" sketch:type=\"MSShapeGroup\"></path>\n+ </g>\n+ <g id=\"g36\" stroke-width=\"1\" fill=\"none\" sketch:type=\"MSLayerGroup\" transform=\"translate(692.388992, 124.376085)\">\n+ <path d=\"M19.7766662,145.390067 L1.16216997,145.390067 L1.2226633,121.585642 L1.2226633,111.846834 L1.2226633,106.170806 L1.2226633,96.2656714 L1.2226633,39.5681976 L1.2226633,39.3518572 C1.2226633,16.4127939 11.1796331,1.04797161 39.5335557,1.04797161 C43.4504989,1.04797161 47.2836822,1.40388649 51.0051854,2.07965952 L51.0051854,18.7925385 C48.3109055,18.3796307 45.4351455,18.1446804 42.3476589,18.1446804 C26.763646,18.1446804 19.8371595,26.1516022 19.8371595,39.5681976 L19.8371595,96.2656714 L51.0051854,96.2656714 L51.0051854,111.846834 L19.8371595,111.846834 L19.7766662,145.390067 L19.7766662,145.390067 Z\" id=\"path38\" fill=\"#8C929D\" sketch:type=\"MSShapeGroup\"></path>\n+ </g>\n+ <path d=\"M646.318899,128.021188 L664.933395,128.021188 L664.933395,236.223966 L646.318899,236.223966 L646.318899,128.021188 L646.318899,128.021188 Z\" id=\"path40\" fill=\"#8C929D\" sketch:type=\"MSShapeGroup\"></path>\n+ <path d=\"M646.318899,251.154944 L664.933395,251.154944 L664.933395,269.766036 L646.318899,269.766036 L646.318899,251.154944 L646.318899,251.154944 Z\" id=\"path42\" fill=\"#8C929D\" sketch:type=\"MSShapeGroup\"></path>\n+ <g id=\"g44\" stroke-width=\"1\" fill=\"none\" sketch:type=\"MSLayerGroup\" transform=\"translate(0.464170, 0.676006)\">\n+ <path d=\"M429.269989,169.815599 L405.225053,243.802859 L357.571431,390.440955 C355.120288,397.984955 344.444378,397.984955 341.992071,390.440955 L294.337286,243.802859 L136.094873,243.802859 L88.4389245,390.440955 C85.9877812,397.984955 75.3118715,397.984955 72.8595648,390.440955 L25.2059427,243.802859 L1.16216997,169.815599 C-1.03187664,163.067173 1.37156997,155.674379 7.11261982,151.503429 L215.215498,0.336141836 L423.319539,151.503429 C429.060589,155.674379 431.462873,163.067173 429.269989,169.815599\" id=\"path46\" fill=\"#FC6D26\" sketch:type=\"MSShapeGroup\"></path>\n+ </g>\n+ <g id=\"g48\" stroke-width=\"1\" fill=\"none\" sketch:type=\"MSLayerGroup\" transform=\"translate(135.410135, 1.012147)\">\n+ <path d=\"M80.269998,0 L80.269998,0 L159.391786,243.466717 L1.14820997,243.466717 L80.269998,0 L80.269998,0 Z\" id=\"path50\" fill=\"#E24329\" sketch:type=\"MSShapeGroup\"></path>\n+ </g>\n+ <g id=\"g52\" stroke-width=\"1\" fill=\"none\" sketch:type=\"MSLayerGroup\" transform=\"translate(215.680133, 1.012147)\">\n+ <g id=\"path54\"></g>\n+ </g>\n+ <g id=\"g56\" stroke-width=\"1\" fill=\"none\" sketch:type=\"MSLayerGroup\" transform=\"translate(24.893471, 1.012613)\">\n+ <path d=\"M190.786662,0 L111.664874,243.465554 L0.777106647,243.465554 L190.786662,0 L190.786662,0 Z\" id=\"path58\" fill=\"#FC6D26\" sketch:type=\"MSShapeGroup\"></path>\n+ </g>\n+ <g id=\"g60\" stroke-width=\"1\" fill=\"none\" sketch:type=\"MSLayerGroup\" transform=\"translate(215.680133, 1.012613)\">\n+ <g id=\"path62\"></g>\n+ </g>\n+ <g id=\"g64\" stroke-width=\"1\" fill=\"none\" sketch:type=\"MSLayerGroup\" transform=\"translate(0.077245, 0.223203)\">\n+ <path d=\"M25.5933327,244.255313 L25.5933327,244.255313 L1.54839663,170.268052 C-0.644486651,163.519627 1.75779662,156.126833 7.50000981,151.957046 L215.602888,0.789758846 L25.5933327,244.255313 L25.5933327,244.255313 Z\" id=\"path66\" fill=\"#FCA326\" sketch:type=\"MSShapeGroup\"></path>\n+ </g>\n+ <g id=\"g68\" stroke-width=\"1\" fill=\"none\" sketch:type=\"MSLayerGroup\" transform=\"translate(215.680133, 1.012147)\">\n+ <g id=\"path70\"></g>\n+ </g>\n+ <g id=\"g72\" stroke-width=\"1\" fill=\"none\" sketch:type=\"MSLayerGroup\" transform=\"translate(25.670578, 244.478283)\">\n+ <path d=\"M0,0 L110.887767,0 L63.2329818,146.638096 C60.7806751,154.183259 50.1047654,154.183259 47.6536221,146.638096 L0,0 L0,0 Z\" id=\"path74\" fill=\"#E24329\" sketch:type=\"MSShapeGroup\"></path>\n+ </g>\n+ <g id=\"g76\" stroke-width=\"1\" fill=\"none\" sketch:type=\"MSLayerGroup\" transform=\"translate(215.680133, 1.012613)\">\n+ <path d=\"M0,0 L79.121788,243.465554 L190.009555,243.465554 L0,0 L0,0 Z\" id=\"path78\" fill=\"#FC6D26\" sketch:type=\"MSShapeGroup\"></path>\n+ </g>\n+ <g id=\"g80\" stroke-width=\"1\" fill=\"none\" sketch:type=\"MSLayerGroup\" transform=\"translate(214.902910, 0.223203)\">\n+ <path d=\"M190.786662,244.255313 L190.786662,244.255313 L214.831598,170.268052 C217.024481,163.519627 214.622198,156.126833 208.879985,151.957046 L0.777106647,0.789758846 L190.786662,244.255313 L190.786662,244.255313 Z\" id=\"path82\" fill=\"#FCA326\" sketch:type=\"MSShapeGroup\"></path>\n+ </g>\n+ <g id=\"g84\" stroke-width=\"1\" fill=\"none\" sketch:type=\"MSLayerGroup\" transform=\"translate(294.009575, 244.478283)\">\n+ <path d=\"M111.679997,0 L0.79222998,0 L48.4470155,146.638096 C50.8993221,154.183259 61.5752318,154.183259 64.0263751,146.638096 L111.679997,0 L111.679997,0 Z\" id=\"path86\" fill=\"#E24329\" sketch:type=\"MSShapeGroup\"></path>\n+ </g>\n+ </g>\n+ </g>\n+ </g>\n+ </g>\n+ </g>\n+ </g>\n+ </g>\n+</svg>\n\\ No newline at end of file\n","new_path":"files/images/wm.svg","old_path":"files/images/wm.svg","a_mode":"0","b_mode":"100644","new_file":true,"renamed_file":false,"deleted_file":false,"too_large":false},{"merge_request_diff_id":12,"relative_order":3,"utf8_diff":"--- /dev/null\n+++ b/files/lfs/lfs_object.iso\n@@ -0,0 +1,4 @@\n+version https://git-lfs.github.com/spec/v1\n+oid sha256:91eff75a492a3ed0dfcb544d7f31326bc4014c8551849c192fd1e48d4dd2c897\n+size 1575078\n+\n","new_path":"files/lfs/lfs_object.iso","old_path":"files/lfs/lfs_object.iso","a_mode":"0","b_mode":"100644","new_file":true,"renamed_file":false,"deleted_file":false,"too_large":false},{"merge_request_diff_id":12,"relative_order":4,"utf8_diff":"--- /dev/null\n+++ b/files/whitespace\n@@ -0,0 +1 @@\n+test \n","new_path":"files/whitespace","old_path":"files/whitespace","a_mode":"0","b_mode":"100644","new_file":true,"renamed_file":false,"deleted_file":false,"too_large":false},{"merge_request_diff_id":12,"relative_order":5,"utf8_diff":"--- /dev/null\n+++ b/test\n","new_path":"test","old_path":"test","a_mode":"0","b_mode":"100644","new_file":true,"renamed_file":false,"deleted_file":false,"too_large":false}],"merge_request_id":12,"created_at":"2016-06-14T15:02:24.006Z","updated_at":"2016-06-14T15:02:24.169Z","base_commit_sha":"e56497bb5f03a90a51293fc6d516788730953899","real_size":"6"},"events":[{"id":226,"target_type":"MergeRequest","target_id":12,"project_id":36,"created_at":"2016-06-14T15:02:24.253Z","updated_at":"2016-06-14T15:02:24.253Z","action":1,"author_id":1},{"id":172,"target_type":"MergeRequest","target_id":12,"project_id":5,"created_at":"2016-06-14T15:02:24.253Z","updated_at":"2016-06-14T15:02:24.253Z","action":1,"author_id":1}]}
+{"id":11,"target_branch":"test-15","source_branch":"'test'","source_project_id":5,"author_id":16,"assignee_id":16,"title":"Corporis provident similique perspiciatis dolores eos animi.","created_at":"2016-06-14T15:02:23.767Z","updated_at":"2016-06-14T15:03:00.475Z","state":"opened","merge_status":"unchecked","target_project_id":5,"iid":3,"description":"Libero nesciunt mollitia quis odit eos vero quasi. Iure voluptatem ut sint pariatur voluptates ut aut. Laborum possimus unde illum ipsum eum.","position":0,"updated_by_id":null,"merge_error":null,"merge_params":{"force_remove_source_branch":null},"merge_when_pipeline_succeeds":false,"merge_user_id":null,"merge_commit_sha":null,"notes":[{"id":809,"note":"Omnis ratione laboriosam dolores qui.","noteable_type":"MergeRequest","author_id":26,"created_at":"2016-06-14T15:03:00.260Z","updated_at":"2016-06-14T15:03:00.260Z","project_id":5,"attachment":{"url":null},"line_code":null,"commit_id":null,"noteable_id":11,"system":false,"st_diff":null,"updated_by_id":null,"author":{"name":"User 4"},"events":[]},{"id":810,"note":"Voluptas voluptates pariatur dolores maxime est voluptas.","noteable_type":"MergeRequest","author_id":25,"created_at":"2016-06-14T15:03:00.290Z","updated_at":"2016-06-14T15:03:00.290Z","project_id":5,"attachment":{"url":null},"line_code":null,"commit_id":null,"noteable_id":11,"system":false,"st_diff":null,"updated_by_id":null,"author":{"name":"User 3"},"events":[]},{"id":811,"note":"Sit perspiciatis facilis ipsum consequatur.","noteable_type":"MergeRequest","author_id":22,"created_at":"2016-06-14T15:03:00.323Z","updated_at":"2016-06-14T15:03:00.323Z","project_id":5,"attachment":{"url":null},"line_code":null,"commit_id":null,"noteable_id":11,"system":false,"st_diff":null,"updated_by_id":null,"author":{"name":"User 0"},"events":[]},{"id":812,"note":"Ut neque aliquam nam et est.","noteable_type":"MergeRequest","author_id":20,"created_at":"2016-06-14T15:03:00.349Z","updated_at":"2016-06-14T15:03:00.349Z","project_id":5,"attachment":{"url":null},"line_code":null,"commit_id":null,"noteable_id":11,"system":false,"st_diff":null,"updated_by_id":null,"author":{"name":"Ottis Schuster II"},"events":[]},{"id":813,"note":"Et debitis rerum minima sit aut dolorem.","noteable_type":"MergeRequest","author_id":16,"created_at":"2016-06-14T15:03:00.374Z","updated_at":"2016-06-14T15:03:00.374Z","project_id":5,"attachment":{"url":null},"line_code":null,"commit_id":null,"noteable_id":11,"system":false,"st_diff":null,"updated_by_id":null,"author":{"name":"Rhett Emmerich IV"},"events":[]},{"id":814,"note":"Ea nisi earum fugit iste aperiam consequatur.","noteable_type":"MergeRequest","author_id":15,"created_at":"2016-06-14T15:03:00.397Z","updated_at":"2016-06-14T15:03:00.397Z","project_id":5,"attachment":{"url":null},"line_code":null,"commit_id":null,"noteable_id":11,"system":false,"st_diff":null,"updated_by_id":null,"author":{"name":"Burdette Bernier"},"events":[]},{"id":815,"note":"Amet ratione consequatur laudantium rerum voluptas est nobis.","noteable_type":"MergeRequest","author_id":6,"created_at":"2016-06-14T15:03:00.450Z","updated_at":"2016-06-14T15:03:00.450Z","project_id":5,"attachment":{"url":null},"line_code":null,"commit_id":null,"noteable_id":11,"system":false,"st_diff":null,"updated_by_id":null,"author":{"name":"Ari Wintheiser"},"events":[]},{"id":816,"note":"Ab ducimus cumque quia dolorem vitae sint beatae rerum.","noteable_type":"MergeRequest","author_id":1,"created_at":"2016-06-14T15:03:00.474Z","updated_at":"2016-06-14T15:03:00.474Z","project_id":5,"attachment":{"url":null},"line_code":null,"commit_id":null,"noteable_id":11,"system":false,"st_diff":null,"updated_by_id":null,"author":{"name":"Administrator"},"events":[]}],"merge_request_diff":{"id":11,"state":"empty","merge_request_diff_commits":[],"merge_request_diff_files":[],"merge_request_id":11,"created_at":"2016-06-14T15:02:23.772Z","updated_at":"2016-06-14T15:02:23.833Z","base_commit_sha":"e56497bb5f03a90a51293fc6d516788730953899","real_size":null},"events":[{"id":227,"target_type":"MergeRequest","target_id":11,"project_id":36,"created_at":"2016-06-14T15:02:23.865Z","updated_at":"2016-06-14T15:02:23.865Z","action":1,"author_id":16},{"id":171,"target_type":"MergeRequest","target_id":11,"project_id":5,"created_at":"2016-06-14T15:02:23.865Z","updated_at":"2016-06-14T15:02:23.865Z","action":1,"author_id":16}]}
+{"id":10,"target_branch":"feature","source_branch":"test-5","source_project_id":5,"author_id":20,"assignee_id":25,"title":"Eligendi reprehenderit doloribus quia et sit id.","created_at":"2016-06-14T15:02:23.014Z","updated_at":"2016-06-14T15:03:00.685Z","state":"opened","merge_status":"unchecked","target_project_id":5,"iid":2,"description":"Ut dolor quia aliquid dolore et nisi. Est minus suscipit enim quaerat sapiente consequatur rerum. Eveniet provident consequatur dolor accusantium reiciendis.","position":0,"updated_by_id":null,"merge_error":null,"merge_params":{"force_remove_source_branch":null},"merge_when_pipeline_succeeds":false,"merge_user_id":null,"merge_commit_sha":null,"notes":[{"id":817,"note":"Recusandae et voluptas enim qui et.","noteable_type":"MergeRequest","author_id":26,"created_at":"2016-06-14T15:03:00.510Z","updated_at":"2016-06-14T15:03:00.510Z","project_id":5,"attachment":{"url":null},"line_code":null,"commit_id":null,"noteable_id":10,"system":false,"st_diff":null,"updated_by_id":null,"author":{"name":"User 4"},"events":[]},{"id":818,"note":"Asperiores dolorem rerum ipsum totam.","noteable_type":"MergeRequest","author_id":25,"created_at":"2016-06-14T15:03:00.538Z","updated_at":"2016-06-14T15:03:00.538Z","project_id":5,"attachment":{"url":null},"line_code":null,"commit_id":null,"noteable_id":10,"system":false,"st_diff":null,"updated_by_id":null,"author":{"name":"User 3"},"events":[]},{"id":819,"note":"Qui quam et iure quasi provident cumque itaque sequi.","noteable_type":"MergeRequest","author_id":22,"created_at":"2016-06-14T15:03:00.562Z","updated_at":"2016-06-14T15:03:00.562Z","project_id":5,"attachment":{"url":null},"line_code":null,"commit_id":null,"noteable_id":10,"system":false,"st_diff":null,"updated_by_id":null,"author":{"name":"User 0"},"events":[]},{"id":820,"note":"Sint accusantium aliquid iste qui iusto minus vel.","noteable_type":"MergeRequest","author_id":20,"created_at":"2016-06-14T15:03:00.585Z","updated_at":"2016-06-14T15:03:00.585Z","project_id":5,"attachment":{"url":null},"line_code":null,"commit_id":null,"noteable_id":10,"system":false,"st_diff":null,"updated_by_id":null,"author":{"name":"Ottis Schuster II"},"events":[]},{"id":821,"note":"Dolor corrupti dolorem blanditiis voluptas.","noteable_type":"MergeRequest","author_id":16,"created_at":"2016-06-14T15:03:00.610Z","updated_at":"2016-06-14T15:03:00.610Z","project_id":5,"attachment":{"url":null},"line_code":null,"commit_id":null,"noteable_id":10,"system":false,"st_diff":null,"updated_by_id":null,"author":{"name":"Rhett Emmerich IV"},"events":[]},{"id":822,"note":"Est perferendis assumenda aliquam aliquid sit ipsum ullam aut.","noteable_type":"MergeRequest","author_id":15,"created_at":"2016-06-14T15:03:00.635Z","updated_at":"2016-06-14T15:03:00.635Z","project_id":5,"attachment":{"url":null},"line_code":null,"commit_id":null,"noteable_id":10,"system":false,"st_diff":null,"updated_by_id":null,"author":{"name":"Burdette Bernier"},"events":[]},{"id":823,"note":"Hic neque reiciendis quaerat maiores.","noteable_type":"MergeRequest","author_id":6,"created_at":"2016-06-14T15:03:00.659Z","updated_at":"2016-06-14T15:03:00.659Z","project_id":5,"attachment":{"url":null},"line_code":null,"commit_id":null,"noteable_id":10,"system":false,"st_diff":null,"updated_by_id":null,"author":{"name":"Ari Wintheiser"},"events":[]},{"id":824,"note":"Sequi architecto doloribus ut vel autem.","noteable_type":"MergeRequest","author_id":1,"created_at":"2016-06-14T15:03:00.683Z","updated_at":"2016-06-14T15:03:00.683Z","project_id":5,"attachment":{"url":null},"line_code":null,"commit_id":null,"noteable_id":10,"system":false,"st_diff":null,"updated_by_id":null,"author":{"name":"Administrator"},"events":[]}],"merge_request_diff":{"id":10,"state":"collected","merge_request_diff_commits":[{"merge_request_diff_id":10,"relative_order":0,"sha":"f998ac87ac9244f15e9c15109a6f4e62a54b779d","message":"fixes #10\n","authored_date":"2016-01-19T14:43:23.000+01:00","author_name":"James Lopez","author_email":"james@jameslopez.es","committed_date":"2016-01-19T14:43:23.000+01:00","committer_name":"James Lopez","committer_email":"james@jameslopez.es"},{"merge_request_diff_id":10,"relative_order":1,"sha":"be93687618e4b132087f430a4d8fc3a609c9b77c","message":"Merge branch 'master' into 'master'\r\n\r\nLFS object pointer.\r\n\r\n\r\n\r\nSee merge request !6","authored_date":"2015-12-07T12:52:12.000+01:00","author_name":"Marin Jankovski","author_email":"marin@gitlab.com","committed_date":"2015-12-07T12:52:12.000+01:00","committer_name":"Marin Jankovski","committer_email":"marin@gitlab.com"},{"merge_request_diff_id":10,"relative_order":2,"sha":"048721d90c449b244b7b4c53a9186b04330174ec","message":"LFS object pointer.\n","authored_date":"2015-12-07T11:54:28.000+01:00","author_name":"Marin Jankovski","author_email":"maxlazio@gmail.com","committed_date":"2015-12-07T11:54:28.000+01:00","committer_name":"Marin Jankovski","committer_email":"maxlazio@gmail.com"},{"merge_request_diff_id":10,"relative_order":3,"sha":"5f923865dde3436854e9ceb9cdb7815618d4e849","message":"GitLab currently doesn't support patches that involve a merge commit: add a commit here\n","authored_date":"2015-11-13T16:27:12.000+01:00","author_name":"Stan Hu","author_email":"stanhu@gmail.com","committed_date":"2015-11-13T16:27:12.000+01:00","committer_name":"Stan Hu","committer_email":"stanhu@gmail.com"},{"merge_request_diff_id":10,"relative_order":4,"sha":"d2d430676773caa88cdaf7c55944073b2fd5561a","message":"Merge branch 'add-svg' into 'master'\r\n\r\nAdd GitLab SVG\r\n\r\nAdded to test preview of sanitized SVG images\r\n\r\nSee merge request !5","authored_date":"2015-11-13T08:50:17.000+01:00","author_name":"Stan Hu","author_email":"stanhu@gmail.com","committed_date":"2015-11-13T08:50:17.000+01:00","committer_name":"Stan Hu","committer_email":"stanhu@gmail.com"},{"merge_request_diff_id":10,"relative_order":5,"sha":"2ea1f3dec713d940208fb5ce4a38765ecb5d3f73","message":"Add GitLab SVG\n","authored_date":"2015-11-13T08:39:43.000+01:00","author_name":"Stan Hu","author_email":"stanhu@gmail.com","committed_date":"2015-11-13T08:39:43.000+01:00","committer_name":"Stan Hu","committer_email":"stanhu@gmail.com"},{"merge_request_diff_id":10,"relative_order":6,"sha":"59e29889be61e6e0e5e223bfa9ac2721d31605b8","message":"Merge branch 'whitespace' into 'master'\r\n\r\nadd whitespace test file\r\n\r\nSorry, I did a mistake.\r\nGit ignore empty files.\r\nSo I add a new whitespace test file.\r\n\r\nSee merge request !4","authored_date":"2015-11-13T07:21:40.000+01:00","author_name":"Stan Hu","author_email":"stanhu@gmail.com","committed_date":"2015-11-13T07:21:40.000+01:00","committer_name":"Stan Hu","committer_email":"stanhu@gmail.com"},{"merge_request_diff_id":10,"relative_order":7,"sha":"66eceea0db202bb39c4e445e8ca28689645366c5","message":"add spaces in whitespace file\n","authored_date":"2015-11-13T06:01:27.000+01:00","author_name":"윤민ì‹","author_email":"minsik.yoon@samsung.com","committed_date":"2015-11-13T06:01:27.000+01:00","committer_name":"윤민ì‹","committer_email":"minsik.yoon@samsung.com"},{"merge_request_diff_id":10,"relative_order":8,"sha":"08f22f255f082689c0d7d39d19205085311542bc","message":"remove empty file.(beacase git ignore empty file)\nadd whitespace test file.\n","authored_date":"2015-11-13T06:00:16.000+01:00","author_name":"윤민ì‹","author_email":"minsik.yoon@samsung.com","committed_date":"2015-11-13T06:00:16.000+01:00","committer_name":"윤민ì‹","committer_email":"minsik.yoon@samsung.com"},{"merge_request_diff_id":10,"relative_order":9,"sha":"19e2e9b4ef76b422ce1154af39a91323ccc57434","message":"Merge branch 'whitespace' into 'master'\r\n\r\nadd spaces\r\n\r\nTo test this pull request.(https://github.com/gitlabhq/gitlabhq/pull/9757)\r\nJust add whitespaces.\r\n\r\nSee merge request !3","authored_date":"2015-11-13T05:23:14.000+01:00","author_name":"Stan Hu","author_email":"stanhu@gmail.com","committed_date":"2015-11-13T05:23:14.000+01:00","committer_name":"Stan Hu","committer_email":"stanhu@gmail.com"},{"merge_request_diff_id":10,"relative_order":10,"sha":"c642fe9b8b9f28f9225d7ea953fe14e74748d53b","message":"add whitespace in empty\n","authored_date":"2015-11-13T05:08:45.000+01:00","author_name":"윤민ì‹","author_email":"minsik.yoon@samsung.com","committed_date":"2015-11-13T05:08:45.000+01:00","committer_name":"윤민ì‹","committer_email":"minsik.yoon@samsung.com"},{"merge_request_diff_id":10,"relative_order":11,"sha":"9a944d90955aaf45f6d0c88f30e27f8d2c41cec0","message":"add empty file\n","authored_date":"2015-11-13T05:08:04.000+01:00","author_name":"윤민ì‹","author_email":"minsik.yoon@samsung.com","committed_date":"2015-11-13T05:08:04.000+01:00","committer_name":"윤민ì‹","committer_email":"minsik.yoon@samsung.com"},{"merge_request_diff_id":10,"relative_order":12,"sha":"c7fbe50c7c7419d9701eebe64b1fdacc3df5b9dd","message":"Add ISO-8859 test file\n","authored_date":"2015-08-25T17:53:12.000+02:00","author_name":"Stan Hu","author_email":"stanhu@packetzoom.com","committed_date":"2015-08-25T17:53:12.000+02:00","committer_name":"Stan Hu","committer_email":"stanhu@packetzoom.com"},{"merge_request_diff_id":10,"relative_order":13,"sha":"e56497bb5f03a90a51293fc6d516788730953899","message":"Merge branch 'tree_helper_spec' into 'master'\n\nAdd directory structure for tree_helper spec\n\nThis directory structure is needed for a testing the method flatten_tree(tree) in the TreeHelper module\n\nSee [merge request #275](https://gitlab.com/gitlab-org/gitlab-foss/merge_requests/275#note_732774)\n\nSee merge request !2\n","authored_date":"2015-01-10T22:23:29.000+01:00","author_name":"Sytse Sijbrandij","author_email":"sytse@gitlab.com","committed_date":"2015-01-10T22:23:29.000+01:00","committer_name":"Sytse Sijbrandij","committer_email":"sytse@gitlab.com"},{"merge_request_diff_id":10,"relative_order":14,"sha":"4cd80ccab63c82b4bad16faa5193fbd2aa06df40","message":"add directory structure for tree_helper spec\n","authored_date":"2015-01-10T21:28:18.000+01:00","author_name":"marmis85","author_email":"marmis85@gmail.com","committed_date":"2015-01-10T21:28:18.000+01:00","committer_name":"marmis85","committer_email":"marmis85@gmail.com"},{"merge_request_diff_id":10,"relative_order":16,"sha":"5937ac0a7beb003549fc5fd26fc247adbce4a52e","message":"Add submodule from gitlab.com\n\nSigned-off-by: Dmitriy Zaporozhets <dmitriy.zaporozhets@gmail.com>\n","authored_date":"2014-02-27T10:01:38.000+01:00","author_name":"Dmitriy Zaporozhets","author_email":"dmitriy.zaporozhets@gmail.com","committed_date":"2014-02-27T10:01:38.000+01:00","committer_name":"Dmitriy Zaporozhets","committer_email":"dmitriy.zaporozhets@gmail.com"},{"merge_request_diff_id":10,"relative_order":17,"sha":"570e7b2abdd848b95f2f578043fc23bd6f6fd24d","message":"Change some files\n\nSigned-off-by: Dmitriy Zaporozhets <dmitriy.zaporozhets@gmail.com>\n","authored_date":"2014-02-27T09:57:31.000+01:00","author_name":"Dmitriy Zaporozhets","author_email":"dmitriy.zaporozhets@gmail.com","committed_date":"2014-02-27T09:57:31.000+01:00","committer_name":"Dmitriy Zaporozhets","committer_email":"dmitriy.zaporozhets@gmail.com"},{"merge_request_diff_id":10,"relative_order":18,"sha":"6f6d7e7ed97bb5f0054f2b1df789b39ca89b6ff9","message":"More submodules\n\nSigned-off-by: Dmitriy Zaporozhets <dmitriy.zaporozhets@gmail.com>\n","authored_date":"2014-02-27T09:54:21.000+01:00","author_name":"Dmitriy Zaporozhets","author_email":"dmitriy.zaporozhets@gmail.com","committed_date":"2014-02-27T09:54:21.000+01:00","committer_name":"Dmitriy Zaporozhets","committer_email":"dmitriy.zaporozhets@gmail.com"},{"merge_request_diff_id":10,"relative_order":19,"sha":"d14d6c0abdd253381df51a723d58691b2ee1ab08","message":"Remove ds_store files\n\nSigned-off-by: Dmitriy Zaporozhets <dmitriy.zaporozhets@gmail.com>\n","authored_date":"2014-02-27T09:49:50.000+01:00","author_name":"Dmitriy Zaporozhets","author_email":"dmitriy.zaporozhets@gmail.com","committed_date":"2014-02-27T09:49:50.000+01:00","committer_name":"Dmitriy Zaporozhets","committer_email":"dmitriy.zaporozhets@gmail.com"},{"merge_request_diff_id":10,"relative_order":20,"sha":"c1acaa58bbcbc3eafe538cb8274ba387047b69f8","message":"Ignore DS files\n\nSigned-off-by: Dmitriy Zaporozhets <dmitriy.zaporozhets@gmail.com>\n","authored_date":"2014-02-27T09:48:32.000+01:00","author_name":"Dmitriy Zaporozhets","author_email":"dmitriy.zaporozhets@gmail.com","committed_date":"2014-02-27T09:48:32.000+01:00","committer_name":"Dmitriy Zaporozhets","committer_email":"dmitriy.zaporozhets@gmail.com"}],"merge_request_diff_files":[{"merge_request_diff_id":10,"relative_order":0,"utf8_diff":"Binary files a/.DS_Store and /dev/null differ\n","new_path":".DS_Store","old_path":".DS_Store","a_mode":"100644","b_mode":"0","new_file":false,"renamed_file":false,"deleted_file":true,"too_large":false},{"merge_request_diff_id":10,"relative_order":1,"utf8_diff":"--- a/.gitignore\n+++ b/.gitignore\n@@ -17,3 +17,4 @@ rerun.txt\n pickle-email-*.html\n .project\n config/initializers/secret_token.rb\n+.DS_Store\n","new_path":".gitignore","old_path":".gitignore","a_mode":"100644","b_mode":"100644","new_file":false,"renamed_file":false,"deleted_file":false,"too_large":false},{"merge_request_diff_id":10,"relative_order":2,"utf8_diff":"--- a/.gitmodules\n+++ b/.gitmodules\n@@ -1,3 +1,9 @@\n [submodule \"six\"]\n \tpath = six\n \turl = git://github.com/randx/six.git\n+[submodule \"gitlab-shell\"]\n+\tpath = gitlab-shell\n+\turl = https://github.com/gitlabhq/gitlab-shell.git\n+[submodule \"gitlab-grack\"]\n+\tpath = gitlab-grack\n+\turl = https://gitlab.com/gitlab-org/gitlab-grack.git\n","new_path":".gitmodules","old_path":".gitmodules","a_mode":"100644","b_mode":"100644","new_file":false,"renamed_file":false,"deleted_file":false,"too_large":false},{"merge_request_diff_id":10,"relative_order":3,"utf8_diff":"--- a/CHANGELOG\n+++ b/CHANGELOG\n@@ -1,4 +1,6 @@\n-v 6.7.0\n+v6.8.0\n+\n+v6.7.0\n - Add support for Gemnasium as a Project Service (Olivier Gonzalez)\n - Add edit file button to MergeRequest diff\n - Public groups (Jason Hollingsworth)\n","new_path":"CHANGELOG","old_path":"CHANGELOG","a_mode":"100644","b_mode":"100644","new_file":false,"renamed_file":false,"deleted_file":false,"too_large":false},{"merge_request_diff_id":10,"relative_order":4,"utf8_diff":"--- /dev/null\n+++ b/encoding/iso8859.txt\n@@ -0,0 +1 @@\n+Äü\n","new_path":"encoding/iso8859.txt","old_path":"encoding/iso8859.txt","a_mode":"0","b_mode":"100644","new_file":true,"renamed_file":false,"deleted_file":false,"too_large":false},{"merge_request_diff_id":10,"relative_order":5,"utf8_diff":"Binary files a/files/.DS_Store and /dev/null differ\n","new_path":"files/.DS_Store","old_path":"files/.DS_Store","a_mode":"100644","b_mode":"0","new_file":false,"renamed_file":false,"deleted_file":true,"too_large":false},{"merge_request_diff_id":10,"relative_order":6,"utf8_diff":"--- /dev/null\n+++ b/files/images/wm.svg\n@@ -0,0 +1,78 @@\n+<?xml version=\"1.0\" encoding=\"UTF-8\" standalone=\"no\"?>\n+<svg width=\"1300px\" height=\"680px\" viewBox=\"0 0 1300 680\" version=\"1.1\" xmlns=\"http://www.w3.org/2000/svg\" xmlns:xlink=\"http://www.w3.org/1999/xlink\" xmlns:sketch=\"http://www.bohemiancoding.com/sketch/ns\">\n+ <!-- Generator: Sketch 3.2.2 (9983) - http://www.bohemiancoding.com/sketch -->\n+ <title>wm</title>\n+ <desc>Created with Sketch.</desc>\n+ <defs>\n+ <path id=\"path-1\" d=\"M-69.8,1023.54607 L1675.19996,1023.54607 L1675.19996,0 L-69.8,0 L-69.8,1023.54607 L-69.8,1023.54607 Z\"></path>\n+ </defs>\n+ <g id=\"Page-1\" stroke=\"none\" stroke-width=\"1\" fill=\"none\" fill-rule=\"evenodd\" sketch:type=\"MSPage\">\n+ <path d=\"M1300,680 L0,680 L0,0 L1300,0 L1300,680 L1300,680 Z\" id=\"bg\" fill=\"#30353E\" sketch:type=\"MSShapeGroup\"></path>\n+ <g id=\"gitlab_logo\" sketch:type=\"MSLayerGroup\" transform=\"translate(-262.000000, -172.000000)\">\n+ <g id=\"g10\" transform=\"translate(872.500000, 512.354581) scale(1, -1) translate(-872.500000, -512.354581) translate(0.000000, 0.290751)\">\n+ <g id=\"g12\" transform=\"translate(1218.022652, 440.744871)\" fill=\"#8C929D\" sketch:type=\"MSShapeGroup\">\n+ <path d=\"M-50.0233338,141.900706 L-69.07059,141.900706 L-69.0100967,0.155858152 L8.04444805,0.155858152 L8.04444805,17.6840847 L-49.9628405,17.6840847 L-50.0233338,141.900706 L-50.0233338,141.900706 Z\" id=\"path14\"></path>\n+ </g>\n+ <g id=\"g16\">\n+ <g id=\"g18-Clipped\">\n+ <mask id=\"mask-2\" sketch:name=\"path22\" fill=\"white\">\n+ <use xlink:href=\"#path-1\"></use>\n+ </mask>\n+ <g id=\"path22\"></g>\n+ <g id=\"g18\" mask=\"url(#mask-2)\">\n+ <g transform=\"translate(382.736659, 312.879425)\">\n+ <g id=\"g24\" stroke-width=\"1\" fill=\"none\" sketch:type=\"MSLayerGroup\" transform=\"translate(852.718192, 124.992771)\">\n+ <path d=\"M63.9833317,27.9148929 C59.2218085,22.9379001 51.2134221,17.9597442 40.3909323,17.9597442 C25.8888194,17.9597442 20.0453962,25.1013043 20.0453962,34.4074318 C20.0453962,48.4730484 29.7848226,55.1819277 50.5642821,55.1819277 C54.4602853,55.1819277 60.7364685,54.7492469 63.9833317,54.1002256 L63.9833317,27.9148929 L63.9833317,27.9148929 Z M44.2869356,113.827628 C28.9053426,113.827628 14.7975996,108.376082 3.78897657,99.301416 L10.5211864,87.6422957 C18.3131929,92.1866076 27.8374026,96.7320827 41.4728323,96.7320827 C57.0568452,96.7320827 63.9833317,88.7239978 63.9833317,75.3074024 L63.9833317,68.3821827 C60.9528485,69.0312039 54.6766653,69.4650479 50.7806621,69.4650479 C17.4476729,69.4650479 0.565379986,57.7791759 0.565379986,33.3245665 C0.565379986,11.4683685 13.9844297,0.43151772 34.3299658,0.43151772 C48.0351955,0.43151772 61.1692285,6.70771614 65.7143717,16.8780421 L69.1776149,3.02876588 L82.5978279,3.02876588 L82.5978279,75.5237428 C82.5978279,98.462806 72.6408582,113.827628 44.2869356,113.827628 L44.2869356,113.827628 Z\" id=\"path26\" fill=\"#8C929D\" sketch:type=\"MSShapeGroup\"></path>\n+ </g>\n+ <g id=\"g28\" stroke-width=\"1\" fill=\"none\" sketch:type=\"MSLayerGroup\" transform=\"translate(959.546624, 124.857151)\">\n+ <path d=\"M37.2266657,17.4468081 C30.0837992,17.4468081 23.8064527,18.3121698 19.0449295,20.4767371 L19.0449295,79.2306079 L19.0449295,86.0464943 C25.538656,91.457331 33.5470425,95.3526217 43.7203922,95.3526217 C62.1173451,95.3526217 69.2602116,82.3687072 69.2602116,61.3767077 C69.2602116,31.5135879 57.7885819,17.4468081 37.2266657,17.4468081 M45.2315622,113.963713 C28.208506,113.963713 19.0449295,102.384849 19.0449295,102.384849 L19.0449295,120.67143 L18.9844362,144.908535 L10.3967097,144.908535 L0.371103324,144.908535 L0.431596656,6.62629771 C9.73826309,2.73100702 22.5081728,0.567602823 36.3611458,0.567602823 C71.8579349,0.567602823 88.9566078,23.2891625 88.9566078,62.4584098 C88.9566078,93.4043948 73.1527248,113.963713 45.2315622,113.963713\" id=\"path30\" fill=\"#8C929D\" sketch:type=\"MSShapeGroup\"></path>\n+ </g>\n+ <g id=\"g32\" stroke-width=\"1\" fill=\"none\" sketch:type=\"MSLayerGroup\" transform=\"translate(509.576747, 125.294950)\">\n+ <path d=\"M68.636665,129.10638 C85.5189579,129.10638 96.3414476,123.480366 103.484314,117.853189 L111.669527,132.029302 C100.513161,141.811145 85.5073245,147.06845 69.5021849,147.06845 C29.0274926,147.06845 0.673569983,122.3975 0.673569983,72.6252464 C0.673569983,20.4709215 31.2622559,0.12910638 66.2553217,0.12910638 C83.7879179,0.12910638 98.7227909,4.24073748 108.462217,8.35236859 L108.063194,64.0763105 L108.063194,70.6502677 L108.063194,81.6057001 L56.1168719,81.6057001 L56.1168719,64.0763105 L89.2323178,64.0763105 L89.6313411,21.7701271 C85.3025779,19.6055598 77.7269514,17.8748364 67.554765,17.8748364 C39.4172223,17.8748364 20.5863462,35.5717154 20.5863462,72.8415868 C20.5863462,110.711628 40.0663623,129.10638 68.636665,129.10638\" id=\"path34\" fill=\"#8C929D\" sketch:type=\"MSShapeGroup\"></path>\n+ </g>\n+ <g id=\"g36\" stroke-width=\"1\" fill=\"none\" sketch:type=\"MSLayerGroup\" transform=\"translate(692.388992, 124.376085)\">\n+ <path d=\"M19.7766662,145.390067 L1.16216997,145.390067 L1.2226633,121.585642 L1.2226633,111.846834 L1.2226633,106.170806 L1.2226633,96.2656714 L1.2226633,39.5681976 L1.2226633,39.3518572 C1.2226633,16.4127939 11.1796331,1.04797161 39.5335557,1.04797161 C43.4504989,1.04797161 47.2836822,1.40388649 51.0051854,2.07965952 L51.0051854,18.7925385 C48.3109055,18.3796307 45.4351455,18.1446804 42.3476589,18.1446804 C26.763646,18.1446804 19.8371595,26.1516022 19.8371595,39.5681976 L19.8371595,96.2656714 L51.0051854,96.2656714 L51.0051854,111.846834 L19.8371595,111.846834 L19.7766662,145.390067 L19.7766662,145.390067 Z\" id=\"path38\" fill=\"#8C929D\" sketch:type=\"MSShapeGroup\"></path>\n+ </g>\n+ <path d=\"M646.318899,128.021188 L664.933395,128.021188 L664.933395,236.223966 L646.318899,236.223966 L646.318899,128.021188 L646.318899,128.021188 Z\" id=\"path40\" fill=\"#8C929D\" sketch:type=\"MSShapeGroup\"></path>\n+ <path d=\"M646.318899,251.154944 L664.933395,251.154944 L664.933395,269.766036 L646.318899,269.766036 L646.318899,251.154944 L646.318899,251.154944 Z\" id=\"path42\" fill=\"#8C929D\" sketch:type=\"MSShapeGroup\"></path>\n+ <g id=\"g44\" stroke-width=\"1\" fill=\"none\" sketch:type=\"MSLayerGroup\" transform=\"translate(0.464170, 0.676006)\">\n+ <path d=\"M429.269989,169.815599 L405.225053,243.802859 L357.571431,390.440955 C355.120288,397.984955 344.444378,397.984955 341.992071,390.440955 L294.337286,243.802859 L136.094873,243.802859 L88.4389245,390.440955 C85.9877812,397.984955 75.3118715,397.984955 72.8595648,390.440955 L25.2059427,243.802859 L1.16216997,169.815599 C-1.03187664,163.067173 1.37156997,155.674379 7.11261982,151.503429 L215.215498,0.336141836 L423.319539,151.503429 C429.060589,155.674379 431.462873,163.067173 429.269989,169.815599\" id=\"path46\" fill=\"#FC6D26\" sketch:type=\"MSShapeGroup\"></path>\n+ </g>\n+ <g id=\"g48\" stroke-width=\"1\" fill=\"none\" sketch:type=\"MSLayerGroup\" transform=\"translate(135.410135, 1.012147)\">\n+ <path d=\"M80.269998,0 L80.269998,0 L159.391786,243.466717 L1.14820997,243.466717 L80.269998,0 L80.269998,0 Z\" id=\"path50\" fill=\"#E24329\" sketch:type=\"MSShapeGroup\"></path>\n+ </g>\n+ <g id=\"g52\" stroke-width=\"1\" fill=\"none\" sketch:type=\"MSLayerGroup\" transform=\"translate(215.680133, 1.012147)\">\n+ <g id=\"path54\"></g>\n+ </g>\n+ <g id=\"g56\" stroke-width=\"1\" fill=\"none\" sketch:type=\"MSLayerGroup\" transform=\"translate(24.893471, 1.012613)\">\n+ <path d=\"M190.786662,0 L111.664874,243.465554 L0.777106647,243.465554 L190.786662,0 L190.786662,0 Z\" id=\"path58\" fill=\"#FC6D26\" sketch:type=\"MSShapeGroup\"></path>\n+ </g>\n+ <g id=\"g60\" stroke-width=\"1\" fill=\"none\" sketch:type=\"MSLayerGroup\" transform=\"translate(215.680133, 1.012613)\">\n+ <g id=\"path62\"></g>\n+ </g>\n+ <g id=\"g64\" stroke-width=\"1\" fill=\"none\" sketch:type=\"MSLayerGroup\" transform=\"translate(0.077245, 0.223203)\">\n+ <path d=\"M25.5933327,244.255313 L25.5933327,244.255313 L1.54839663,170.268052 C-0.644486651,163.519627 1.75779662,156.126833 7.50000981,151.957046 L215.602888,0.789758846 L25.5933327,244.255313 L25.5933327,244.255313 Z\" id=\"path66\" fill=\"#FCA326\" sketch:type=\"MSShapeGroup\"></path>\n+ </g>\n+ <g id=\"g68\" stroke-width=\"1\" fill=\"none\" sketch:type=\"MSLayerGroup\" transform=\"translate(215.680133, 1.012147)\">\n+ <g id=\"path70\"></g>\n+ </g>\n+ <g id=\"g72\" stroke-width=\"1\" fill=\"none\" sketch:type=\"MSLayerGroup\" transform=\"translate(25.670578, 244.478283)\">\n+ <path d=\"M0,0 L110.887767,0 L63.2329818,146.638096 C60.7806751,154.183259 50.1047654,154.183259 47.6536221,146.638096 L0,0 L0,0 Z\" id=\"path74\" fill=\"#E24329\" sketch:type=\"MSShapeGroup\"></path>\n+ </g>\n+ <g id=\"g76\" stroke-width=\"1\" fill=\"none\" sketch:type=\"MSLayerGroup\" transform=\"translate(215.680133, 1.012613)\">\n+ <path d=\"M0,0 L79.121788,243.465554 L190.009555,243.465554 L0,0 L0,0 Z\" id=\"path78\" fill=\"#FC6D26\" sketch:type=\"MSShapeGroup\"></path>\n+ </g>\n+ <g id=\"g80\" stroke-width=\"1\" fill=\"none\" sketch:type=\"MSLayerGroup\" transform=\"translate(214.902910, 0.223203)\">\n+ <path d=\"M190.786662,244.255313 L190.786662,244.255313 L214.831598,170.268052 C217.024481,163.519627 214.622198,156.126833 208.879985,151.957046 L0.777106647,0.789758846 L190.786662,244.255313 L190.786662,244.255313 Z\" id=\"path82\" fill=\"#FCA326\" sketch:type=\"MSShapeGroup\"></path>\n+ </g>\n+ <g id=\"g84\" stroke-width=\"1\" fill=\"none\" sketch:type=\"MSLayerGroup\" transform=\"translate(294.009575, 244.478283)\">\n+ <path d=\"M111.679997,0 L0.79222998,0 L48.4470155,146.638096 C50.8993221,154.183259 61.5752318,154.183259 64.0263751,146.638096 L111.679997,0 L111.679997,0 Z\" id=\"path86\" fill=\"#E24329\" sketch:type=\"MSShapeGroup\"></path>\n+ </g>\n+ </g>\n+ </g>\n+ </g>\n+ </g>\n+ </g>\n+ </g>\n+ </g>\n+</svg>\n\\ No newline at end of file\n","new_path":"files/images/wm.svg","old_path":"files/images/wm.svg","a_mode":"0","b_mode":"100644","new_file":true,"renamed_file":false,"deleted_file":false,"too_large":false},{"merge_request_diff_id":10,"relative_order":7,"utf8_diff":"--- /dev/null\n+++ b/files/lfs/lfs_object.iso\n@@ -0,0 +1,4 @@\n+version https://git-lfs.github.com/spec/v1\n+oid sha256:91eff75a492a3ed0dfcb544d7f31326bc4014c8551849c192fd1e48d4dd2c897\n+size 1575078\n+\n","new_path":"files/lfs/lfs_object.iso","old_path":"files/lfs/lfs_object.iso","a_mode":"0","b_mode":"100644","new_file":true,"renamed_file":false,"deleted_file":false,"too_large":false},{"merge_request_diff_id":10,"relative_order":8,"utf8_diff":"--- a/files/ruby/popen.rb\n+++ b/files/ruby/popen.rb\n@@ -6,12 +6,18 @@ module Popen\n \n def popen(cmd, path=nil)\n unless cmd.is_a?(Array)\n- raise \"System commands must be given as an array of strings\"\n+ raise RuntimeError, \"System commands must be given as an array of strings\"\n end\n \n path ||= Dir.pwd\n- vars = { \"PWD\" => path }\n- options = { chdir: path }\n+\n+ vars = {\n+ \"PWD\" => path\n+ }\n+\n+ options = {\n+ chdir: path\n+ }\n \n unless File.directory?(path)\n FileUtils.mkdir_p(path)\n@@ -19,6 +25,7 @@ module Popen\n \n @cmd_output = \"\"\n @cmd_status = 0\n+\n Open3.popen3(vars, *cmd, options) do |stdin, stdout, stderr, wait_thr|\n @cmd_output << stdout.read\n @cmd_output << stderr.read\n","new_path":"files/ruby/popen.rb","old_path":"files/ruby/popen.rb","a_mode":"100644","b_mode":"100644","new_file":false,"renamed_file":false,"deleted_file":false,"too_large":false},{"merge_request_diff_id":10,"relative_order":9,"utf8_diff":"--- a/files/ruby/regex.rb\n+++ b/files/ruby/regex.rb\n@@ -19,14 +19,12 @@ module Gitlab\n end\n \n def archive_formats_regex\n- #|zip|tar| tar.gz | tar.bz2 |\n- /(zip|tar|tar\\.gz|tgz|gz|tar\\.bz2|tbz|tbz2|tb2|bz2)/\n+ /(zip|tar|7z|tar\\.gz|tgz|gz|tar\\.bz2|tbz|tbz2|tb2|bz2)/\n end\n \n def git_reference_regex\n # Valid git ref regex, see:\n # https://www.kernel.org/pub/software/scm/git/docs/git-check-ref-format.html\n-\n %r{\n (?!\n (?# doesn't begins with)\n","new_path":"files/ruby/regex.rb","old_path":"files/ruby/regex.rb","a_mode":"100644","b_mode":"100644","new_file":false,"renamed_file":false,"deleted_file":false,"too_large":false},{"merge_request_diff_id":10,"relative_order":10,"utf8_diff":"--- /dev/null\n+++ b/files/whitespace\n@@ -0,0 +1 @@\n+test \n","new_path":"files/whitespace","old_path":"files/whitespace","a_mode":"0","b_mode":"100644","new_file":true,"renamed_file":false,"deleted_file":false,"too_large":false},{"merge_request_diff_id":10,"relative_order":11,"utf8_diff":"--- /dev/null\n+++ b/foo/bar/.gitkeep\n","new_path":"foo/bar/.gitkeep","old_path":"foo/bar/.gitkeep","a_mode":"0","b_mode":"100644","new_file":true,"renamed_file":false,"deleted_file":false,"too_large":false},{"merge_request_diff_id":10,"relative_order":12,"utf8_diff":"--- /dev/null\n+++ b/gitlab-grack\n@@ -0,0 +1 @@\n+Subproject commit 645f6c4c82fd3f5e06f67134450a570b795e55a6\n","new_path":"gitlab-grack","old_path":"gitlab-grack","a_mode":"0","b_mode":"160000","new_file":true,"renamed_file":false,"deleted_file":false,"too_large":false},{"merge_request_diff_id":10,"relative_order":13,"utf8_diff":"--- /dev/null\n+++ b/gitlab-shell\n@@ -0,0 +1 @@\n+Subproject commit 79bceae69cb5750d6567b223597999bfa91cb3b9\n","new_path":"gitlab-shell","old_path":"gitlab-shell","a_mode":"0","b_mode":"160000","new_file":true,"renamed_file":false,"deleted_file":false,"too_large":false},{"merge_request_diff_id":10,"relative_order":14,"utf8_diff":"--- /dev/null\n+++ b/test\n","new_path":"test","old_path":"test","a_mode":"0","b_mode":"100644","new_file":true,"renamed_file":false,"deleted_file":false,"too_large":false}],"merge_request_id":10,"created_at":"2016-06-14T15:02:23.019Z","updated_at":"2016-06-14T15:02:23.493Z","base_commit_sha":"ae73cb07c9eeaf35924a10f713b364d32b2dd34f","real_size":"15"},"events":[{"id":228,"target_type":"MergeRequest","target_id":10,"project_id":36,"created_at":"2016-06-14T15:02:23.660Z","updated_at":"2016-06-14T15:02:23.660Z","action":1,"author_id":1},{"id":170,"target_type":"MergeRequest","target_id":10,"project_id":5,"created_at":"2016-06-14T15:02:23.660Z","updated_at":"2016-06-14T15:02:23.660Z","action":1,"author_id":20}]}
+{"id":9,"target_branch":"test-6","source_branch":"test-12","source_project_id":5,"author_id":16,"assignee_id":6,"title":"Et ipsam voluptas velit sequi illum ut.","created_at":"2016-06-14T15:02:22.825Z","updated_at":"2016-06-14T15:03:00.904Z","state":"opened","merge_status":"unchecked","target_project_id":5,"iid":1,"description":"Eveniet nihil ratione veniam similique qui aut sapiente tempora. Sed praesentium iusto dignissimos possimus id repudiandae quo nihil. Qui doloremque autem et iure fugit.","position":0,"updated_by_id":null,"merge_error":null,"merge_params":{"force_remove_source_branch":null},"merge_when_pipeline_succeeds":false,"merge_user_id":null,"merge_commit_sha":null,"notes":[{"id":825,"note":"Aliquid voluptatem consequatur voluptas ex perspiciatis.","noteable_type":"MergeRequest","author_id":26,"created_at":"2016-06-14T15:03:00.722Z","updated_at":"2016-06-14T15:03:00.722Z","project_id":5,"attachment":{"url":null},"line_code":null,"commit_id":null,"noteable_id":9,"system":false,"st_diff":null,"updated_by_id":null,"author":{"name":"User 4"},"events":[]},{"id":826,"note":"Itaque optio voluptatem praesentium voluptas.","noteable_type":"MergeRequest","author_id":25,"created_at":"2016-06-14T15:03:00.745Z","updated_at":"2016-06-14T15:03:00.745Z","project_id":5,"attachment":{"url":null},"line_code":null,"commit_id":null,"noteable_id":9,"system":false,"st_diff":null,"updated_by_id":null,"author":{"name":"User 3"},"events":[]},{"id":827,"note":"Ut est corporis fuga asperiores delectus excepturi aperiam.","noteable_type":"MergeRequest","author_id":22,"created_at":"2016-06-14T15:03:00.771Z","updated_at":"2016-06-14T15:03:00.771Z","project_id":5,"attachment":{"url":null},"line_code":null,"commit_id":null,"noteable_id":9,"system":false,"st_diff":null,"updated_by_id":null,"author":{"name":"User 0"},"events":[]},{"id":828,"note":"Similique ea dolore officiis temporibus.","noteable_type":"MergeRequest","author_id":20,"created_at":"2016-06-14T15:03:00.798Z","updated_at":"2016-06-14T15:03:00.798Z","project_id":5,"attachment":{"url":null},"line_code":null,"commit_id":null,"noteable_id":9,"system":false,"st_diff":null,"updated_by_id":null,"author":{"name":"Ottis Schuster II"},"events":[]},{"id":829,"note":"Qui laudantium qui quae quis.","noteable_type":"MergeRequest","author_id":16,"created_at":"2016-06-14T15:03:00.828Z","updated_at":"2016-06-14T15:03:00.828Z","project_id":5,"attachment":{"url":null},"line_code":null,"commit_id":null,"noteable_id":9,"system":false,"st_diff":null,"updated_by_id":null,"author":{"name":"Rhett Emmerich IV"},"events":[]},{"id":830,"note":"Et vel voluptas amet laborum qui soluta.","noteable_type":"MergeRequest","author_id":15,"created_at":"2016-06-14T15:03:00.850Z","updated_at":"2016-06-14T15:03:00.850Z","project_id":5,"attachment":{"url":null},"line_code":null,"commit_id":null,"noteable_id":9,"system":false,"st_diff":null,"updated_by_id":null,"author":{"name":"Burdette Bernier"},"events":[]},{"id":831,"note":"Enim ad consequuntur assumenda provident voluptatem similique deleniti.","noteable_type":"MergeRequest","author_id":6,"created_at":"2016-06-14T15:03:00.876Z","updated_at":"2016-06-14T15:03:00.876Z","project_id":5,"attachment":{"url":null},"line_code":null,"commit_id":null,"noteable_id":9,"system":false,"st_diff":null,"updated_by_id":null,"author":{"name":"Ari Wintheiser"},"events":[]},{"id":832,"note":"Officiis sequi commodi pariatur totam fugiat voluptas corporis dignissimos.","noteable_type":"MergeRequest","author_id":1,"created_at":"2016-06-14T15:03:00.902Z","updated_at":"2016-06-14T15:03:00.902Z","project_id":5,"attachment":{"url":null},"line_code":null,"commit_id":null,"noteable_id":9,"system":false,"st_diff":null,"updated_by_id":null,"author":{"name":"Administrator"},"events":[]}],"merge_request_diff":{"id":9,"state":"collected","merge_request_diff_commits":[{"merge_request_diff_id":9,"relative_order":0,"sha":"a4e5dfebf42e34596526acb8611bc7ed80e4eb3f","message":"fixes #10\n","authored_date":"2016-01-19T15:44:02.000+01:00","author_name":"James Lopez","author_email":"james@jameslopez.es","committed_date":"2016-01-19T15:44:02.000+01:00","committer_name":"James Lopez","committer_email":"james@jameslopez.es"}],"merge_request_diff_files":[{"merge_request_diff_id":9,"relative_order":0,"utf8_diff":"--- /dev/null\n+++ b/test\n","new_path":"test","old_path":"test","a_mode":"0","b_mode":"100644","new_file":true,"renamed_file":false,"deleted_file":false,"too_large":false}],"merge_request_id":9,"created_at":"2016-06-14T15:02:22.829Z","updated_at":"2016-06-14T15:02:22.900Z","base_commit_sha":"be93687618e4b132087f430a4d8fc3a609c9b77c","real_size":"1"},"events":[{"id":229,"target_type":"MergeRequest","target_id":9,"project_id":36,"created_at":"2016-06-14T15:02:22.927Z","updated_at":"2016-06-14T15:02:22.927Z","action":1,"author_id":16},{"id":169,"target_type":"MergeRequest","target_id":9,"project_id":5,"created_at":"2016-06-14T15:02:22.927Z","updated_at":"2016-06-14T15:02:22.927Z","action":1,"author_id":16}]}
diff --git a/spec/fixtures/lib/gitlab/import_export/complex/tree/project/milestones.ndjson b/spec/fixtures/lib/gitlab/import_export/complex/tree/project/milestones.ndjson
new file mode 100644
index 00000000000..2c9a5b00eb4
--- /dev/null
+++ b/spec/fixtures/lib/gitlab/import_export/complex/tree/project/milestones.ndjson
@@ -0,0 +1,3 @@
+{"id":1,"title":"test milestone","project_id":8,"description":"test milestone","due_date":null,"created_at":"2016-06-14T15:02:04.415Z","updated_at":"2016-06-14T15:02:04.415Z","state":"active","iid":1,"events":[{"id":487,"target_type":"Milestone","target_id":1,"project_id":46,"created_at":"2016-06-14T15:02:04.418Z","updated_at":"2016-06-14T15:02:04.418Z","action":1,"author_id":18}]}
+{"id":20,"title":"v4.0","project_id":5,"description":"Totam quam laborum id magnam natus eaque aspernatur.","due_date":null,"created_at":"2016-06-14T15:02:04.590Z","updated_at":"2016-06-14T15:02:04.590Z","state":"active","iid":5,"events":[{"id":240,"target_type":"Milestone","target_id":20,"project_id":36,"created_at":"2016-06-14T15:02:04.593Z","updated_at":"2016-06-14T15:02:04.593Z","action":1,"author_id":1},{"id":60,"target_type":"Milestone","target_id":20,"project_id":5,"created_at":"2016-06-14T15:02:04.593Z","updated_at":"2016-06-14T15:02:04.593Z","action":1,"author_id":20}]}
+{"id":19,"title":"v3.0","project_id":5,"description":"Rerum at autem exercitationem ea voluptates harum quam placeat.","due_date":null,"created_at":"2016-06-14T15:02:04.583Z","updated_at":"2016-06-14T15:02:04.583Z","state":"active","iid":4,"events":[{"id":241,"target_type":"Milestone","target_id":19,"project_id":36,"created_at":"2016-06-14T15:02:04.585Z","updated_at":"2016-06-14T15:02:04.585Z","action":1,"author_id":1},{"id":59,"target_type":"Milestone","target_id":19,"project_id":5,"created_at":"2016-06-14T15:02:04.585Z","updated_at":"2016-06-14T15:02:04.585Z","action":1,"author_id":25}]}
diff --git a/spec/fixtures/lib/gitlab/import_export/complex/tree/project/pipeline_schedules.ndjson b/spec/fixtures/lib/gitlab/import_export/complex/tree/project/pipeline_schedules.ndjson
new file mode 100644
index 00000000000..6d429be9f51
--- /dev/null
+++ b/spec/fixtures/lib/gitlab/import_export/complex/tree/project/pipeline_schedules.ndjson
@@ -0,0 +1 @@
+{"id":1,"description":"Schedule Description","ref":"master","cron":"0 4 * * 0","cron_timezone":"UTC","next_run_at":"2019-12-29T04:19:00.000Z","project_id":5,"owner_id":1,"active":true,"created_at":"2019-12-26T10:14:57.778Z","updated_at":"2019-12-26T10:14:57.778Z"}
diff --git a/spec/fixtures/lib/gitlab/import_export/complex/tree/project/project_badges.ndjson b/spec/fixtures/lib/gitlab/import_export/complex/tree/project/project_badges.ndjson
new file mode 100644
index 00000000000..f84305f3c0c
--- /dev/null
+++ b/spec/fixtures/lib/gitlab/import_export/complex/tree/project/project_badges.ndjson
@@ -0,0 +1,2 @@
+{"id":1,"created_at":"2017-10-19T15:36:23.466Z","updated_at":"2017-10-19T15:36:23.466Z","project_id":5,"type":"ProjectBadge","link_url":"http://www.example.com","image_url":"http://www.example.com"}
+{"id":2,"created_at":"2017-10-19T15:36:23.466Z","updated_at":"2017-10-19T15:36:23.466Z","project_id":5,"type":"ProjectBadge","link_url":"http://www.example.com","image_url":"http://www.example.com"}
diff --git a/spec/fixtures/lib/gitlab/import_export/complex/tree/project/project_feature.ndjson b/spec/fixtures/lib/gitlab/import_export/complex/tree/project/project_feature.ndjson
new file mode 100644
index 00000000000..a349dc6722e
--- /dev/null
+++ b/spec/fixtures/lib/gitlab/import_export/complex/tree/project/project_feature.ndjson
@@ -0,0 +1 @@
+{"builds_access_level":10,"created_at":"2014-12-26T09:26:45.000Z","id":2,"issues_access_level":10,"merge_requests_access_level":10,"project_id":4,"snippets_access_level":10,"updated_at":"2016-09-23T11:58:28.000Z","wiki_access_level":10}
diff --git a/spec/fixtures/lib/gitlab/import_export/complex/tree/project/project_members.ndjson b/spec/fixtures/lib/gitlab/import_export/complex/tree/project/project_members.ndjson
new file mode 100644
index 00000000000..d8be7b5d164
--- /dev/null
+++ b/spec/fixtures/lib/gitlab/import_export/complex/tree/project/project_members.ndjson
@@ -0,0 +1,4 @@
+{"id":36,"access_level":40,"source_id":5,"source_type":"Project","user_id":16,"notification_level":3,"created_at":"2016-06-14T15:02:03.834Z","updated_at":"2016-06-14T15:02:03.834Z","created_by_id":null,"invite_email":null,"invite_token":null,"invite_accepted_at":null,"requested_at":null,"user":{"id":16,"email":"bernard_willms@gitlabexample.com","username":"bernard_willms"}}
+{"id":35,"access_level":10,"source_id":5,"source_type":"Project","user_id":6,"notification_level":3,"created_at":"2016-06-14T15:02:03.811Z","updated_at":"2016-06-14T15:02:03.811Z","created_by_id":null,"invite_email":null,"invite_token":null,"invite_accepted_at":null,"requested_at":null,"user":{"id":6,"email":"saul_will@gitlabexample.com","username":"saul_will"}}
+{"id":34,"access_level":20,"source_id":5,"source_type":"Project","user_id":15,"notification_level":3,"created_at":"2016-06-14T15:02:03.776Z","updated_at":"2016-06-14T15:02:03.776Z","created_by_id":null,"invite_email":null,"invite_token":null,"invite_accepted_at":null,"requested_at":null,"user":{"id":15,"email":"breanna_sanford@wolf.com","username":"emmet.schamberger"}}
+{"id":33,"access_level":20,"source_id":5,"source_type":"Project","user_id":26,"notification_level":3,"created_at":"2016-06-14T15:02:03.742Z","updated_at":"2016-06-14T15:02:03.742Z","created_by_id":null,"invite_email":null,"invite_token":null,"invite_accepted_at":null,"requested_at":null,"user":{"id":26,"email":"user4@example.com","username":"user4"}}
diff --git a/spec/fixtures/lib/gitlab/import_export/complex/tree/project/protected_branches.ndjson b/spec/fixtures/lib/gitlab/import_export/complex/tree/project/protected_branches.ndjson
new file mode 100644
index 00000000000..abd2b40cf7b
--- /dev/null
+++ b/spec/fixtures/lib/gitlab/import_export/complex/tree/project/protected_branches.ndjson
@@ -0,0 +1 @@
+{"id":1,"project_id":9,"name":"master","created_at":"2016-08-30T07:32:52.426Z","updated_at":"2016-08-30T07:32:52.426Z","merge_access_levels":[{"id":1,"protected_branch_id":1,"access_level":40,"created_at":"2016-08-30T07:32:52.458Z","updated_at":"2016-08-30T07:32:52.458Z"}],"push_access_levels":[{"id":1,"protected_branch_id":1,"access_level":40,"created_at":"2016-08-30T07:32:52.490Z","updated_at":"2016-08-30T07:32:52.490Z"}]}
diff --git a/spec/fixtures/lib/gitlab/import_export/complex/tree/project/protected_environments.ndjson b/spec/fixtures/lib/gitlab/import_export/complex/tree/project/protected_environments.ndjson
new file mode 100644
index 00000000000..55afaa8bcf6
--- /dev/null
+++ b/spec/fixtures/lib/gitlab/import_export/complex/tree/project/protected_environments.ndjson
@@ -0,0 +1 @@
+{ "id": 1, "project_id": 9, "created_at": "2017-10-19T15:36:23.466Z", "updated_at": "2017-10-19T15:36:23.466Z", "name": "production", "deploy_access_levels": [ { "id": 1, "protected_environment_id": 1, "created_at": "2017-10-19T15:36:23.466Z", "updated_at": "2017-10-19T15:36:23.466Z", "access_level": 40, "user_id": 1, "group_id": null } ] }
diff --git a/spec/fixtures/lib/gitlab/import_export/complex/tree/project/protected_tags.ndjson b/spec/fixtures/lib/gitlab/import_export/complex/tree/project/protected_tags.ndjson
new file mode 100644
index 00000000000..441c7c3737f
--- /dev/null
+++ b/spec/fixtures/lib/gitlab/import_export/complex/tree/project/protected_tags.ndjson
@@ -0,0 +1 @@
+{"id":1,"project_id":9,"name":"v*","created_at":"2017-04-04T13:48:13.426Z","updated_at":"2017-04-04T13:48:13.426Z","create_access_levels":[{"id":1,"protected_tag_id":1,"access_level":40,"created_at":"2017-04-04T13:48:13.458Z","updated_at":"2017-04-04T13:48:13.458Z"}]}
diff --git a/spec/fixtures/lib/gitlab/import_export/complex/tree/project/releases.ndjson b/spec/fixtures/lib/gitlab/import_export/complex/tree/project/releases.ndjson
new file mode 100644
index 00000000000..0c14c023378
--- /dev/null
+++ b/spec/fixtures/lib/gitlab/import_export/complex/tree/project/releases.ndjson
@@ -0,0 +1 @@
+{"id":1,"tag":"release-1.1","description":"Some release notes","project_id":5,"created_at":"2019-12-26T10:17:14.621Z","updated_at":"2019-12-26T10:17:14.621Z","author_id":1,"name":"release-1.1","sha":"901de3a8bd5573f4a049b1457d28bc1592ba6bf9","released_at":"2019-12-26T10:17:14.615Z","links":[{"id":1,"release_id":1,"url":"http://localhost/namespace6/project6/-/jobs/140463678/artifacts/download","name":"release-1.1.dmg","created_at":"2019-12-26T10:17:14.621Z","updated_at":"2019-12-26T10:17:14.621Z"}]}
diff --git a/spec/fixtures/lib/gitlab/import_export/complex/tree/project/services.ndjson b/spec/fixtures/lib/gitlab/import_export/complex/tree/project/services.ndjson
new file mode 100644
index 00000000000..6d6afd3af0b
--- /dev/null
+++ b/spec/fixtures/lib/gitlab/import_export/complex/tree/project/services.ndjson
@@ -0,0 +1,19 @@
+{"id":101,"title":"YouTrack","project_id":5,"created_at":"2016-06-14T15:01:51.327Z","updated_at":"2016-06-14T15:01:51.327Z","active":false,"properties":{},"template":false,"push_events":true,"issues_events":true,"merge_requests_events":true,"tag_push_events":true,"note_events":true,"job_events":true,"type":"YoutrackService","category":"issue_tracker","default":false,"wiki_page_events":true}
+{"id":100,"title":"JetBrains TeamCity CI","project_id":5,"created_at":"2016-06-14T15:01:51.315Z","updated_at":"2016-06-14T15:01:51.315Z","active":false,"properties":{},"template":false,"push_events":true,"issues_events":true,"merge_requests_events":true,"tag_push_events":true,"note_events":true,"job_events":true,"type":"TeamcityService","category":"ci","default":false,"wiki_page_events":true}
+{"id":99,"title":"Slack","project_id":5,"created_at":"2016-06-14T15:01:51.303Z","updated_at":"2016-06-14T15:01:51.303Z","active":false,"properties":{"notify_only_broken_pipelines":true},"template":false,"push_events":true,"issues_events":true,"merge_requests_events":true,"tag_push_events":true,"note_events":true,"pipeline_events":true,"type":"SlackService","category":"common","default":false,"wiki_page_events":true}
+{"id":98,"title":"Redmine","project_id":5,"created_at":"2016-06-14T15:01:51.289Z","updated_at":"2016-06-14T15:01:51.289Z","active":false,"properties":{},"template":false,"push_events":true,"issues_events":true,"merge_requests_events":true,"tag_push_events":true,"note_events":true,"job_events":true,"type":"RedmineService","category":"issue_tracker","default":false,"wiki_page_events":true}
+{"id":97,"title":"Pushover","project_id":5,"created_at":"2016-06-14T15:01:51.277Z","updated_at":"2016-06-14T15:01:51.277Z","active":false,"properties":{},"template":false,"push_events":true,"issues_events":true,"merge_requests_events":true,"tag_push_events":true,"note_events":true,"job_events":true,"type":"PushoverService","category":"common","default":false,"wiki_page_events":true}
+{"id":96,"title":"PivotalTracker","project_id":5,"created_at":"2016-06-14T15:01:51.267Z","updated_at":"2016-06-14T15:01:51.267Z","active":false,"properties":{},"template":false,"push_events":true,"issues_events":true,"merge_requests_events":true,"tag_push_events":true,"note_events":true,"job_events":true,"type":"PivotalTrackerService","category":"common","default":false,"wiki_page_events":true}
+{"id":95,"title":"Jira","project_id":5,"created_at":"2016-06-14T15:01:51.255Z","updated_at":"2016-06-14T15:01:51.255Z","active":false,"properties":{"api_url":"","jira_issue_transition_id":"2"},"template":false,"push_events":true,"issues_events":true,"merge_requests_events":true,"tag_push_events":true,"note_events":true,"job_events":true,"type":"JiraService","category":"issue_tracker","default":false,"wiki_page_events":true}
+{"id":94,"title":"Irker (IRC gateway)","project_id":5,"created_at":"2016-06-14T15:01:51.232Z","updated_at":"2016-06-14T15:01:51.232Z","active":true,"properties":{},"template":false,"push_events":true,"issues_events":true,"merge_requests_events":true,"tag_push_events":true,"note_events":true,"job_events":true,"type":"IrkerService","category":"common","default":false,"wiki_page_events":true}
+{"id":93,"title":"HipChat","project_id":5,"created_at":"2016-06-14T15:01:51.219Z","updated_at":"2016-06-14T15:01:51.219Z","active":false,"properties":{"notify_only_broken_pipelines":true},"template":false,"push_events":true,"issues_events":true,"merge_requests_events":true,"tag_push_events":true,"note_events":true,"pipeline_events":true,"type":"HipchatService","category":"common","default":false,"wiki_page_events":true}
+{"id":91,"title":"Flowdock","project_id":5,"created_at":"2016-06-14T15:01:51.182Z","updated_at":"2016-06-14T15:01:51.182Z","active":false,"properties":{},"template":false,"push_events":true,"issues_events":true,"merge_requests_events":true,"tag_push_events":true,"note_events":true,"job_events":true,"type":"FlowdockService","category":"common","default":false,"wiki_page_events":true}
+{"id":90,"title":"External Wiki","project_id":5,"created_at":"2016-06-14T15:01:51.166Z","updated_at":"2016-06-14T15:01:51.166Z","active":false,"properties":{},"template":false,"push_events":true,"issues_events":true,"merge_requests_events":true,"tag_push_events":true,"note_events":true,"job_events":true,"type":"ExternalWikiService","category":"common","default":false,"wiki_page_events":true}
+{"id":89,"title":"Emails on push","project_id":5,"created_at":"2016-06-14T15:01:51.153Z","updated_at":"2016-06-14T15:01:51.153Z","active":false,"properties":{},"template":false,"push_events":true,"issues_events":true,"merge_requests_events":true,"tag_push_events":true,"note_events":true,"job_events":true,"type":"EmailsOnPushService","category":"common","default":false,"wiki_page_events":true}
+{"id":88,"title":"Drone CI","project_id":5,"created_at":"2016-06-14T15:01:51.139Z","updated_at":"2016-06-14T15:01:51.139Z","active":false,"properties":{},"template":false,"push_events":true,"issues_events":true,"merge_requests_events":true,"tag_push_events":true,"note_events":true,"job_events":true,"type":"DroneCiService","category":"ci","default":false,"wiki_page_events":true}
+{"id":87,"title":"Custom Issue Tracker","project_id":5,"created_at":"2016-06-14T15:01:51.125Z","updated_at":"2016-06-14T15:01:51.125Z","active":false,"properties":{},"template":false,"push_events":true,"issues_events":true,"merge_requests_events":true,"tag_push_events":true,"note_events":true,"job_events":true,"type":"CustomIssueTrackerService","category":"issue_tracker","default":false,"wiki_page_events":true}
+{"id":86,"title":"Campfire","project_id":5,"created_at":"2016-06-14T15:01:51.113Z","updated_at":"2016-06-14T15:01:51.113Z","active":false,"properties":{},"template":false,"push_events":true,"issues_events":true,"merge_requests_events":true,"tag_push_events":true,"note_events":true,"job_events":true,"type":"CampfireService","category":"common","default":false,"wiki_page_events":true}
+{"id":84,"title":"Buildkite","project_id":5,"created_at":"2016-06-14T15:01:51.080Z","updated_at":"2016-06-14T15:01:51.080Z","active":false,"properties":{},"template":false,"push_events":true,"issues_events":true,"merge_requests_events":true,"tag_push_events":true,"note_events":true,"job_events":true,"type":"BuildkiteService","category":"ci","default":false,"wiki_page_events":true}
+{"id":83,"title":"Atlassian Bamboo CI","project_id":5,"created_at":"2016-06-14T15:01:51.067Z","updated_at":"2016-06-14T15:01:51.067Z","active":false,"properties":{},"template":false,"push_events":true,"issues_events":true,"merge_requests_events":true,"tag_push_events":true,"note_events":true,"job_events":true,"type":"BambooService","category":"ci","default":false,"wiki_page_events":true}
+{"id":82,"title":"Assembla","project_id":5,"created_at":"2016-06-14T15:01:51.047Z","updated_at":"2016-06-14T15:01:51.047Z","active":false,"properties":{},"template":false,"push_events":true,"issues_events":true,"merge_requests_events":true,"tag_push_events":true,"note_events":true,"job_events":true,"type":"AssemblaService","category":"common","default":false,"wiki_page_events":true}
+{"id":81,"title":"Asana","project_id":5,"created_at":"2016-06-14T15:01:51.031Z","updated_at":"2016-06-14T15:01:51.031Z","active":false,"properties":{},"template":false,"push_events":true,"issues_events":true,"merge_requests_events":true,"tag_push_events":true,"note_events":true,"job_events":true,"type":"AsanaService","category":"common","default":false,"wiki_page_events":true}
diff --git a/spec/fixtures/lib/gitlab/import_export/complex/tree/project/snippets.ndjson b/spec/fixtures/lib/gitlab/import_export/complex/tree/project/snippets.ndjson
new file mode 100644
index 00000000000..7d626090aa4
--- /dev/null
+++ b/spec/fixtures/lib/gitlab/import_export/complex/tree/project/snippets.ndjson
@@ -0,0 +1 @@
+{"id":1,"title":"Test snippet title","content":"x = 1","author_id":1,"project_id":1,"created_at":"2019-11-05T15:06:06.579Z","updated_at":"2019-11-05T15:06:06.579Z","file_name":"","visibility_level":20,"description":"Test snippet description","award_emoji":[{"id":1,"name":"thumbsup","user_id":1,"awardable_type":"Snippet","awardable_id":1,"created_at":"2019-11-05T15:37:21.287Z","updated_at":"2019-11-05T15:37:21.287Z"},{"id":2,"name":"coffee","user_id":1,"awardable_type":"Snippet","awardable_id":1,"created_at":"2019-11-05T15:37:24.645Z","updated_at":"2019-11-05T15:37:24.645Z"}],"notes":[{"id":872,"note":"This is a test note","noteable_type":"Snippet","author_id":1,"created_at":"2019-11-05T15:37:24.645Z","updated_at":"2019-11-05T15:37:24.645Z","noteable_id":1,"author":{"name":"Random name"},"events":[],"award_emoji":[{"id":12,"name":"thumbsup","user_id":1,"awardable_type":"Note","awardable_id":872,"created_at":"2019-11-05T15:37:21.287Z","updated_at":"2019-11-05T15:37:21.287Z"}]}]}
diff --git a/spec/fixtures/lib/gitlab/import_export/complex/tree/project/triggers.ndjson b/spec/fixtures/lib/gitlab/import_export/complex/tree/project/triggers.ndjson
new file mode 100644
index 00000000000..93619f4fb44
--- /dev/null
+++ b/spec/fixtures/lib/gitlab/import_export/complex/tree/project/triggers.ndjson
@@ -0,0 +1,2 @@
+{"id":123,"token":"cdbfasdf44a5958c83654733449e585","project_id":5,"owner_id":1,"created_at":"2017-01-16T15:25:28.637Z","updated_at":"2017-01-16T15:25:28.637Z"}
+{"id":456,"token":"33a66349b5ad01fc00174af87804e40","project_id":5,"created_at":"2017-01-16T15:25:29.637Z","updated_at":"2017-01-16T15:25:29.637Z"}
diff --git a/spec/fixtures/lib/gitlab/import_export/designs/project.json b/spec/fixtures/lib/gitlab/import_export/designs/project.json
new file mode 100644
index 00000000000..28eaa38d387
--- /dev/null
+++ b/spec/fixtures/lib/gitlab/import_export/designs/project.json
@@ -0,0 +1,502 @@
+{
+ "description":"",
+ "visibility_level":0,
+ "archived":false,
+ "merge_requests_template":null,
+ "merge_requests_rebase_enabled":false,
+ "approvals_before_merge":0,
+ "reset_approvals_on_push":true,
+ "merge_requests_ff_only_enabled":false,
+ "issues_template":null,
+ "shared_runners_enabled":true,
+ "build_coverage_regex":null,
+ "build_allow_git_fetch":true,
+ "build_timeout":3600,
+ "pending_delete":false,
+ "public_builds":true,
+ "last_repository_check_failed":null,
+ "container_registry_enabled":true,
+ "only_allow_merge_if_pipeline_succeeds":false,
+ "has_external_issue_tracker":false,
+ "request_access_enabled":false,
+ "has_external_wiki":false,
+ "ci_config_path":null,
+ "only_allow_merge_if_all_discussions_are_resolved":false,
+ "repository_size_limit":null,
+ "printing_merge_request_link_enabled":true,
+ "auto_cancel_pending_pipelines":"enabled",
+ "service_desk_enabled":null,
+ "delete_error":null,
+ "disable_overriding_approvers_per_merge_request":null,
+ "resolve_outdated_diff_discussions":false,
+ "jobs_cache_index":null,
+ "external_authorization_classification_label":null,
+ "pages_https_only":false,
+ "external_webhook_token":null,
+ "merge_requests_author_approval":null,
+ "merge_requests_disable_committers_approval":null,
+ "require_password_to_approve":null,
+ "labels":[
+
+ ],
+ "milestones":[
+
+ ],
+ "issues":[
+ {
+ "id":469,
+ "title":"issue 1",
+ "author_id":1,
+ "project_id":30,
+ "created_at":"2019-08-07T03:57:55.007Z",
+ "updated_at":"2019-08-07T03:57:55.007Z",
+ "description":"",
+ "state":"opened",
+ "iid":1,
+ "updated_by_id":null,
+ "weight":null,
+ "confidential":false,
+ "due_date":null,
+ "moved_to_id":null,
+ "lock_version":0,
+ "time_estimate":0,
+ "relative_position":1073742323,
+ "service_desk_reply_to":null,
+ "last_edited_at":null,
+ "last_edited_by_id":null,
+ "discussion_locked":null,
+ "closed_at":null,
+ "closed_by_id":null,
+ "state_id":1,
+ "events":[
+ {
+ "id":1775,
+ "project_id":30,
+ "author_id":1,
+ "target_id":469,
+ "created_at":"2019-08-07T03:57:55.158Z",
+ "updated_at":"2019-08-07T03:57:55.158Z",
+ "target_type":"Issue",
+ "action":1
+ }
+ ],
+ "timelogs":[
+
+ ],
+ "notes":[
+
+ ],
+ "label_links":[
+
+ ],
+ "resource_label_events":[
+
+ ],
+ "issue_assignees":[
+
+ ],
+ "designs":[
+ {
+ "id":38,
+ "project_id":30,
+ "issue_id":469,
+ "filename":"chirrido3.jpg",
+ "notes":[
+
+ ]
+ },
+ {
+ "id":39,
+ "project_id":30,
+ "issue_id":469,
+ "filename":"jonathan_richman.jpg",
+ "notes":[
+
+ ]
+ },
+ {
+ "id":40,
+ "project_id":30,
+ "issue_id":469,
+ "filename":"mariavontrap.jpeg",
+ "notes":[
+
+ ]
+ }
+ ],
+ "design_versions":[
+ {
+ "id":24,
+ "sha":"9358d1bac8ff300d3d2597adaa2572a20f7f8703",
+ "issue_id":469,
+ "author_id":1,
+ "actions":[
+ {
+ "design_id":38,
+ "version_id":24,
+ "event":0,
+ "design":{
+ "id":38,
+ "project_id":30,
+ "issue_id":469,
+ "filename":"chirrido3.jpg"
+ }
+ }
+ ]
+ },
+ {
+ "id":25,
+ "sha":"e1a4a501bcb42f291f84e5d04c8f927821542fb6",
+ "issue_id":469,
+ "author_id":2,
+ "actions":[
+ {
+ "design_id":38,
+ "version_id":25,
+ "event":1,
+ "design":{
+ "id":38,
+ "project_id":30,
+ "issue_id":469,
+ "filename":"chirrido3.jpg"
+ }
+ },
+ {
+ "design_id":39,
+ "version_id":25,
+ "event":0,
+ "design":{
+ "id":39,
+ "project_id":30,
+ "issue_id":469,
+ "filename":"jonathan_richman.jpg"
+ }
+ }
+ ]
+ },
+ {
+ "id":26,
+ "sha":"27702d08f5ee021ae938737f84e8fe7c38599e85",
+ "issue_id":469,
+ "author_id":1,
+ "actions":[
+ {
+ "design_id":38,
+ "version_id":26,
+ "event":1,
+ "design":{
+ "id":38,
+ "project_id":30,
+ "issue_id":469,
+ "filename":"chirrido3.jpg"
+ }
+ },
+ {
+ "design_id":39,
+ "version_id":26,
+ "event":2,
+ "design":{
+ "id":39,
+ "project_id":30,
+ "issue_id":469,
+ "filename":"jonathan_richman.jpg"
+ }
+ },
+ {
+ "design_id":40,
+ "version_id":26,
+ "event":0,
+ "design":{
+ "id":40,
+ "project_id":30,
+ "issue_id":469,
+ "filename":"mariavontrap.jpeg"
+ }
+ }
+ ]
+ }
+ ]
+ },
+ {
+ "id":470,
+ "title":"issue 2",
+ "author_id":1,
+ "project_id":30,
+ "created_at":"2019-08-07T04:15:57.607Z",
+ "updated_at":"2019-08-07T04:15:57.607Z",
+ "description":"",
+ "state":"opened",
+ "iid":2,
+ "updated_by_id":null,
+ "weight":null,
+ "confidential":false,
+ "due_date":null,
+ "moved_to_id":null,
+ "lock_version":0,
+ "time_estimate":0,
+ "relative_position":1073742823,
+ "service_desk_reply_to":null,
+ "last_edited_at":null,
+ "last_edited_by_id":null,
+ "discussion_locked":null,
+ "closed_at":null,
+ "closed_by_id":null,
+ "state_id":1,
+ "events":[
+ {
+ "id":1776,
+ "project_id":30,
+ "author_id":1,
+ "target_id":470,
+ "created_at":"2019-08-07T04:15:57.789Z",
+ "updated_at":"2019-08-07T04:15:57.789Z",
+ "target_type":"Issue",
+ "action":1
+ }
+ ],
+ "timelogs":[
+
+ ],
+ "notes":[
+
+ ],
+ "label_links":[
+
+ ],
+ "resource_label_events":[
+
+ ],
+ "issue_assignees":[
+
+ ],
+ "designs":[
+ {
+ "id":42,
+ "project_id":30,
+ "issue_id":470,
+ "filename":"1 (1).jpeg",
+ "notes":[
+
+ ]
+ },
+ {
+ "id":43,
+ "project_id":30,
+ "issue_id":470,
+ "filename":"2099743.jpg",
+ "notes":[
+
+ ]
+ },
+ {
+ "id":44,
+ "project_id":30,
+ "issue_id":470,
+ "filename":"a screenshot (1).jpg",
+ "notes":[
+
+ ]
+ },
+ {
+ "id":41,
+ "project_id":30,
+ "issue_id":470,
+ "filename":"chirrido3.jpg",
+ "notes":[
+
+ ]
+ }
+ ],
+ "design_versions":[
+ {
+ "id":27,
+ "sha":"8587e78ab6bda3bc820a9f014c3be4a21ad4fcc8",
+ "issue_id":470,
+ "author_id":1,
+ "actions":[
+ {
+ "design_id":41,
+ "version_id":27,
+ "event":0,
+ "design":{
+ "id":41,
+ "project_id":30,
+ "issue_id":470,
+ "filename":"chirrido3.jpg"
+ }
+ }
+ ]
+ },
+ {
+ "id":28,
+ "sha":"73f871b4c8c1d65c62c460635e023179fb53abc4",
+ "issue_id":470,
+ "author_id":2,
+ "actions":[
+ {
+ "design_id":42,
+ "version_id":28,
+ "event":0,
+ "design":{
+ "id":42,
+ "project_id":30,
+ "issue_id":470,
+ "filename":"1 (1).jpeg"
+ }
+ },
+ {
+ "design_id":43,
+ "version_id":28,
+ "event":0,
+ "design":{
+ "id":43,
+ "project_id":30,
+ "issue_id":470,
+ "filename":"2099743.jpg"
+ }
+ }
+ ]
+ },
+ {
+ "id":29,
+ "sha":"c9b5f067f3e892122a4b12b0a25a8089192f3ac8",
+ "issue_id":470,
+ "author_id":2,
+ "actions":[
+ {
+ "design_id":42,
+ "version_id":29,
+ "event":1,
+ "design":{
+ "id":42,
+ "project_id":30,
+ "issue_id":470,
+ "filename":"1 (1).jpeg"
+ }
+ },
+ {
+ "design_id":44,
+ "version_id":29,
+ "event":0,
+ "design":{
+ "id":44,
+ "project_id":30,
+ "issue_id":470,
+ "filename":"a screenshot (1).jpg"
+ }
+ }
+ ]
+ }
+ ]
+ }
+ ],
+ "snippets":[
+
+ ],
+ "releases":[
+
+ ],
+ "project_members":[
+ {
+ "id":95,
+ "access_level":40,
+ "source_id":30,
+ "source_type":"Project",
+ "user_id":1,
+ "notification_level":3,
+ "created_at":"2019-08-07T03:57:32.825Z",
+ "updated_at":"2019-08-07T03:57:32.825Z",
+ "created_by_id":1,
+ "invite_email":null,
+ "invite_token":null,
+ "invite_accepted_at":null,
+ "requested_at":null,
+ "expires_at":null,
+ "ldap":false,
+ "override":false,
+ "user":{
+ "id":1,
+ "email":"admin@example.com",
+ "username":"root"
+ }
+ },
+ {
+ "id":96,
+ "access_level":40,
+ "source_id":30,
+ "source_type":"Project",
+ "user_id":2,
+ "notification_level":3,
+ "created_at":"2019-08-07T03:57:32.825Z",
+ "updated_at":"2019-08-07T03:57:32.825Z",
+ "created_by_id":null,
+ "invite_email":null,
+ "invite_token":null,
+ "invite_accepted_at":null,
+ "requested_at":null,
+ "expires_at":null,
+ "ldap":false,
+ "override":false,
+ "user":{
+ "id":2,
+ "email":"user_2@gitlabexample.com",
+ "username":"user_2"
+ }
+ }
+ ],
+ "merge_requests":[
+
+ ],
+ "ci_pipelines":[
+
+ ],
+ "triggers":[
+
+ ],
+ "pipeline_schedules":[
+
+ ],
+ "services":[
+
+ ],
+ "protected_branches":[
+
+ ],
+ "protected_environments": [
+
+ ],
+ "protected_tags":[
+
+ ],
+ "project_feature":{
+ "id":30,
+ "project_id":30,
+ "merge_requests_access_level":20,
+ "issues_access_level":20,
+ "wiki_access_level":20,
+ "snippets_access_level":20,
+ "builds_access_level":20,
+ "created_at":"2019-08-07T03:57:32.485Z",
+ "updated_at":"2019-08-07T03:57:32.485Z",
+ "repository_access_level":20,
+ "pages_access_level":10
+ },
+ "custom_attributes":[
+
+ ],
+ "prometheus_metrics":[
+
+ ],
+ "project_badges":[
+
+ ],
+ "ci_cd_settings":{
+ "group_runners_enabled":true
+ },
+ "boards":[
+
+ ],
+ "pipelines":[
+
+ ]
+}
diff --git a/spec/fixtures/lib/gitlab/import_export/group/tree.tar.gz b/spec/fixtures/lib/gitlab/import_export/group/tree.tar.gz
deleted file mode 100644
index 0788ca18fb3..00000000000
--- a/spec/fixtures/lib/gitlab/import_export/group/tree.tar.gz
+++ /dev/null
Binary files differ
diff --git a/spec/fixtures/lib/gitlab/import_export/group/tree/project.json b/spec/fixtures/lib/gitlab/import_export/group/tree/project.json
new file mode 100644
index 00000000000..df38e1746e5
--- /dev/null
+++ b/spec/fixtures/lib/gitlab/import_export/group/tree/project.json
@@ -0,0 +1 @@
+{"id":5,"description":"Nisi et repellendus ut enim quo accusamus vel magnam.","visibility_level":10,"archived":false,"hooks":[]}
diff --git a/spec/fixtures/lib/gitlab/import_export/group/tree/project/issues.ndjson b/spec/fixtures/lib/gitlab/import_export/group/tree/project/issues.ndjson
new file mode 100644
index 00000000000..4759e97228f
--- /dev/null
+++ b/spec/fixtures/lib/gitlab/import_export/group/tree/project/issues.ndjson
@@ -0,0 +1,3 @@
+{"id":1,"title":"Fugiat est minima quae maxime non similique.","assignee_id":null,"project_id":8,"author_id":1,"created_at":"2017-07-07T18:13:01.138Z","updated_at":"2017-08-15T18:37:40.807Z","branch_name":null,"description":"Quam totam fuga numquam in eveniet.","state":"opened","iid":1,"updated_by_id":1,"confidential":false,"due_date":null,"moved_to_id":null,"lock_version":null,"time_estimate":0,"closed_at":null,"last_edited_at":null,"last_edited_by_id":null,"group_milestone_id":null,"milestone":{"id":1,"title":"Project milestone","project_id":8,"description":"Project-level milestone","due_date":null,"created_at":"2016-06-14T15:02:04.415Z","updated_at":"2016-06-14T15:02:04.415Z","state":"active","iid":1,"group_id":null},"label_links":[{"id":11,"label_id":6,"target_id":1,"target_type":"Issue","created_at":"2017-08-15T18:37:40.795Z","updated_at":"2017-08-15T18:37:40.795Z","label":{"id":6,"title":"group label","color":"#A8D695","project_id":null,"created_at":"2017-08-15T18:37:19.698Z","updated_at":"2017-08-15T18:37:19.698Z","template":false,"description":"","group_id":5,"type":"GroupLabel","priorities":[]}},{"id":11,"label_id":2,"target_id":1,"target_type":"Issue","created_at":"2017-08-15T18:37:40.795Z","updated_at":"2017-08-15T18:37:40.795Z","label":{"id":6,"title":"A project label","color":"#A8D695","project_id":null,"created_at":"2017-08-15T18:37:19.698Z","updated_at":"2017-08-15T18:37:19.698Z","template":false,"description":"","group_id":5,"type":"ProjectLabel","priorities":[]}}]}
+{"id":2,"title":"Fugiat est minima quae maxime non similique.","assignee_id":null,"project_id":8,"author_id":1,"created_at":"2017-07-07T18:13:01.138Z","updated_at":"2017-08-15T18:37:40.807Z","branch_name":null,"description":"Quam totam fuga numquam in eveniet.","state":"closed","iid":2,"updated_by_id":1,"confidential":false,"due_date":null,"moved_to_id":null,"lock_version":null,"time_estimate":0,"closed_at":null,"last_edited_at":null,"last_edited_by_id":null,"group_milestone_id":null,"milestone":{"id":2,"title":"A group milestone","description":"Group-level milestone","due_date":null,"created_at":"2016-06-14T15:02:04.415Z","updated_at":"2016-06-14T15:02:04.415Z","state":"active","iid":1,"group_id":100},"label_links":[{"id":11,"label_id":2,"target_id":1,"target_type":"Issue","created_at":"2017-08-15T18:37:40.795Z","updated_at":"2017-08-15T18:37:40.795Z","label":{"id":2,"title":"A project label","color":"#A8D695","project_id":null,"created_at":"2017-08-15T18:37:19.698Z","updated_at":"2017-08-15T18:37:19.698Z","template":false,"description":"","group_id":5,"type":"ProjectLabel","priorities":[]}}]}
+{"id":3,"title":"Issue with Epic","author_id":1,"project_id":8,"created_at":"2019-12-08T19:41:11.233Z","updated_at":"2019-12-08T19:41:53.194Z","position":0,"branch_name":null,"description":"Donec at nulla vitae sem molestie rutrum ut at sem.","state":"opened","iid":3,"updated_by_id":null,"confidential":false,"due_date":null,"moved_to_id":null,"issue_assignees":[],"notes":[],"milestone":{"id":2,"title":"A group milestone","description":"Group-level milestone","due_date":null,"created_at":"2016-06-14T15:02:04.415Z","updated_at":"2016-06-14T15:02:04.415Z","state":"active","iid":1,"group_id":100},"epic_issue":{"id":78,"relative_position":1073740323,"epic":{"id":1,"group_id":5,"author_id":1,"assignee_id":null,"iid":1,"updated_by_id":null,"last_edited_by_id":null,"lock_version":0,"start_date":null,"end_date":null,"last_edited_at":null,"created_at":"2019-12-08T19:37:07.098Z","updated_at":"2019-12-08T19:43:11.568Z","title":"An epic","description":null,"start_date_sourcing_milestone_id":null,"due_date_sourcing_milestone_id":null,"start_date_fixed":null,"due_date_fixed":null,"start_date_is_fixed":null,"due_date_is_fixed":null,"closed_by_id":null,"closed_at":null,"parent_id":null,"relative_position":null,"state_id":"opened","start_date_sourcing_epic_id":null,"due_date_sourcing_epic_id":null,"milestone_id":null}}}
diff --git a/spec/fixtures/lib/gitlab/import_export/group/tree/project/labels.ndjson b/spec/fixtures/lib/gitlab/import_export/group/tree/project/labels.ndjson
new file mode 100644
index 00000000000..28894a8a404
--- /dev/null
+++ b/spec/fixtures/lib/gitlab/import_export/group/tree/project/labels.ndjson
@@ -0,0 +1 @@
+{"id":2,"title":"A project label","color":"#428bca","project_id":8,"created_at":"2016-07-22T08:55:44.161Z","updated_at":"2016-07-22T08:55:44.161Z","template":false,"description":"","type":"ProjectLabel","priorities":[{"id":1,"project_id":5,"label_id":1,"priority":1,"created_at":"2016-10-18T09:35:43.338Z","updated_at":"2016-10-18T09:35:43.338Z"}]}
diff --git a/spec/fixtures/lib/gitlab/import_export/group/tree/project/milestones.ndjson b/spec/fixtures/lib/gitlab/import_export/group/tree/project/milestones.ndjson
new file mode 100644
index 00000000000..29166527a9d
--- /dev/null
+++ b/spec/fixtures/lib/gitlab/import_export/group/tree/project/milestones.ndjson
@@ -0,0 +1 @@
+{"id":1,"title":"Project milestone","project_id":8,"description":"Project-level milestone","due_date":null,"created_at":"2016-06-14T15:02:04.415Z","updated_at":"2016-06-14T15:02:04.415Z","state":"active","iid":1,"group_id":null}
diff --git a/spec/fixtures/lib/gitlab/import_export/group_exports/child_with_no_parent/tree/groups/4351.json b/spec/fixtures/lib/gitlab/import_export/group_exports/child_with_no_parent/tree/groups/4351.json
new file mode 100644
index 00000000000..ce657ded7b0
--- /dev/null
+++ b/spec/fixtures/lib/gitlab/import_export/group_exports/child_with_no_parent/tree/groups/4351.json
@@ -0,0 +1 @@
+{"name":"ymg09t5704clnxnqfgaj2h098gz4r7gyx4wc3fzmlqj1en24zf","path":"ymg09t5704clnxnqfgaj2h098gz4r7gyx4wc3fzmlqj1en24zf","owner_id":123,"created_at":"2019-11-20 17:01:53 UTC","updated_at":"2019-11-20 17:05:44 UTC","description":"Group Description","avatar":{"url":null},"membership_lock":false,"share_with_group_lock":false,"visibility_level":0,"request_access_enabled":true,"ldap_sync_status":"ready","ldap_sync_error":null,"ldap_sync_last_update_at":null,"ldap_sync_last_successful_update_at":null,"ldap_sync_last_sync_at":null,"lfs_enabled":null,"parent_id":null,"shared_runners_minutes_limit":null,"repository_size_limit":null,"require_two_factor_authentication":false,"two_factor_grace_period":48,"plan_id":null,"project_creation_level":2,"trial_ends_on":null,"file_template_project_id":null,"saml_discovery_token":"rBKx3ioz","custom_project_templates_group_id":null,"auto_devops_enabled":null,"extra_shared_runners_minutes_limit":null,"last_ci_minutes_notification_at":null,"last_ci_minutes_usage_notification_level":null,"runners_token":"token","runners_token_encrypted":"encrypted","subgroup_creation_level":1,"emails_disabled":null,"max_pages_size":null,"max_artifacts_size":null,"id":4351} \ No newline at end of file
diff --git a/spec/fixtures/lib/gitlab/import_export/group_exports/child_with_no_parent/tree/groups/4352.json b/spec/fixtures/lib/gitlab/import_export/group_exports/child_with_no_parent/tree/groups/4352.json
new file mode 100644
index 00000000000..b67f863fcea
--- /dev/null
+++ b/spec/fixtures/lib/gitlab/import_export/group_exports/child_with_no_parent/tree/groups/4352.json
@@ -0,0 +1 @@
+{"name":"pwip17beq7vl4nuwz9ie7bk8navpxj1w04zylmmjveab5bargr","path":"pwip17beq7vl4nuwz9ie7bk8navpxj1w04zylmmjveab5bargr","owner_id":null,"created_at":"2019-11-20 17:01:53 UTC","updated_at":"2019-11-20 17:05:44 UTC","description":"","avatar":{"url":null},"membership_lock":false,"share_with_group_lock":false,"visibility_level":0,"request_access_enabled":true,"ldap_sync_status":"ready","ldap_sync_error":null,"ldap_sync_last_update_at":null,"ldap_sync_last_successful_update_at":null,"ldap_sync_last_sync_at":null,"lfs_enabled":null,"parent_id":null,"shared_runners_minutes_limit":null,"repository_size_limit":null,"require_two_factor_authentication":false,"two_factor_grace_period":48,"plan_id":null,"project_creation_level":2,"trial_ends_on":null,"file_template_project_id":null,"saml_discovery_token":"ki3Xnjw3","custom_project_templates_group_id":null,"auto_devops_enabled":null,"extra_shared_runners_minutes_limit":null,"last_ci_minutes_notification_at":null,"last_ci_minutes_usage_notification_level":null,"subgroup_creation_level":1,"emails_disabled":null,"max_pages_size":null,"max_artifacts_size":null,"id":4352}
diff --git a/spec/fixtures/lib/gitlab/import_export/group_exports/child_with_no_parent/tree/groups/_all.ndjson b/spec/fixtures/lib/gitlab/import_export/group_exports/child_with_no_parent/tree/groups/_all.ndjson
new file mode 100644
index 00000000000..078909d30fb
--- /dev/null
+++ b/spec/fixtures/lib/gitlab/import_export/group_exports/child_with_no_parent/tree/groups/_all.ndjson
@@ -0,0 +1,2 @@
+4351
+4352
diff --git a/spec/fixtures/lib/gitlab/import_export/group_exports/complex/tree/groups/4351.json b/spec/fixtures/lib/gitlab/import_export/group_exports/complex/tree/groups/4351.json
new file mode 100644
index 00000000000..ce657ded7b0
--- /dev/null
+++ b/spec/fixtures/lib/gitlab/import_export/group_exports/complex/tree/groups/4351.json
@@ -0,0 +1 @@
+{"name":"ymg09t5704clnxnqfgaj2h098gz4r7gyx4wc3fzmlqj1en24zf","path":"ymg09t5704clnxnqfgaj2h098gz4r7gyx4wc3fzmlqj1en24zf","owner_id":123,"created_at":"2019-11-20 17:01:53 UTC","updated_at":"2019-11-20 17:05:44 UTC","description":"Group Description","avatar":{"url":null},"membership_lock":false,"share_with_group_lock":false,"visibility_level":0,"request_access_enabled":true,"ldap_sync_status":"ready","ldap_sync_error":null,"ldap_sync_last_update_at":null,"ldap_sync_last_successful_update_at":null,"ldap_sync_last_sync_at":null,"lfs_enabled":null,"parent_id":null,"shared_runners_minutes_limit":null,"repository_size_limit":null,"require_two_factor_authentication":false,"two_factor_grace_period":48,"plan_id":null,"project_creation_level":2,"trial_ends_on":null,"file_template_project_id":null,"saml_discovery_token":"rBKx3ioz","custom_project_templates_group_id":null,"auto_devops_enabled":null,"extra_shared_runners_minutes_limit":null,"last_ci_minutes_notification_at":null,"last_ci_minutes_usage_notification_level":null,"runners_token":"token","runners_token_encrypted":"encrypted","subgroup_creation_level":1,"emails_disabled":null,"max_pages_size":null,"max_artifacts_size":null,"id":4351} \ No newline at end of file
diff --git a/spec/fixtures/lib/gitlab/import_export/group_exports/complex/tree/groups/4351/badges.ndjson b/spec/fixtures/lib/gitlab/import_export/group_exports/complex/tree/groups/4351/badges.ndjson
new file mode 100644
index 00000000000..3c04897594e
--- /dev/null
+++ b/spec/fixtures/lib/gitlab/import_export/group_exports/complex/tree/groups/4351/badges.ndjson
@@ -0,0 +1 @@
+{"id":10,"link_url":"https://localhost:3443/%{default_branch}","image_url":"https://badge_image.png","project_id":null,"group_id":4351,"created_at":"2019-11-20T17:27:02.047Z","updated_at":"2019-11-20T17:27:02.047Z","type":"GroupBadge"}
diff --git a/spec/fixtures/lib/gitlab/import_export/group_exports/complex/tree/groups/4351/boards.ndjson b/spec/fixtures/lib/gitlab/import_export/group_exports/complex/tree/groups/4351/boards.ndjson
new file mode 100644
index 00000000000..a3e28584ff5
--- /dev/null
+++ b/spec/fixtures/lib/gitlab/import_export/group_exports/complex/tree/groups/4351/boards.ndjson
@@ -0,0 +1 @@
+{"id":173,"project_id":null,"created_at":"2020-02-11T14:35:51.561Z","updated_at":"2020-02-11T14:35:51.561Z","name":"first board","milestone_id":null,"group_id":4351,"weight":null,"lists":[{"id":189,"board_id":173,"label_id":271,"list_type":"label","position":0,"created_at":"2020-02-11T14:35:57.131Z","updated_at":"2020-02-11T14:35:57.131Z","user_id":null,"milestone_id":null,"max_issue_count":0,"max_issue_weight":0,"label":{"id":271,"title":"TSL","color":"#58796f","project_id":null,"created_at":"2019-11-20T17:02:20.541Z","updated_at":"2020-02-06T15:44:52.048Z","template":false,"description":null,"group_id":4351,"type":"GroupLabel","priorities":[]},"board":{"id":173,"project_id":null,"created_at":"2020-02-11T14:35:51.561Z","updated_at":"2020-02-11T14:35:51.561Z","name":"hi","milestone_id":null,"group_id":4351,"weight":null}},{"id":190,"board_id":173,"label_id":272,"list_type":"label","position":1,"created_at":"2020-02-11T14:35:57.868Z","updated_at":"2020-02-11T14:35:57.868Z","user_id":null,"milestone_id":null,"max_issue_count":0,"max_issue_weight":0,"label":{"id":272,"title":"Sosync","color":"#110320","project_id":null,"created_at":"2019-11-20T17:02:20.532Z","updated_at":"2020-02-06T15:44:52.057Z","template":false,"description":null,"group_id":4351,"type":"GroupLabel","priorities":[]},"board":{"id":173,"project_id":null,"created_at":"2020-02-11T14:35:51.561Z","updated_at":"2020-02-11T14:35:51.561Z","name":"hi","milestone_id":null,"group_id":4351,"weight":null}},{"id":188,"board_id":173,"label_id":null,"list_type":"closed","position":null,"created_at":"2020-02-11T14:35:51.593Z","updated_at":"2020-02-11T14:35:51.593Z","user_id":null,"milestone_id":null,"max_issue_count":0,"max_issue_weight":0}],"labels":[]}
diff --git a/spec/fixtures/lib/gitlab/import_export/group_exports/complex/tree/groups/4351/epics.ndjson b/spec/fixtures/lib/gitlab/import_export/group_exports/complex/tree/groups/4351/epics.ndjson
new file mode 100644
index 00000000000..e461cbb537e
--- /dev/null
+++ b/spec/fixtures/lib/gitlab/import_export/group_exports/complex/tree/groups/4351/epics.ndjson
@@ -0,0 +1,5 @@
+{"id":13622,"milestone_id":null,"group_id":4351,"author_id":1,"assignee_id":null,"iid":1,"updated_by_id":null,"last_edited_by_id":null,"lock_version":0,"start_date":null,"end_date":null,"last_edited_at":null,"created_at":"2019-11-20T17:02:09.754Z","updated_at":"2019-11-20T18:38:40.054Z","title":"Provident neque consequatur numquam ad laboriosam voluptatem magnam.","description":"Fugit nisi est ut numquam quia rerum vitae qui. Et in est aliquid voluptas et ut vitae. In distinctio voluptates ut deleniti iste.\n\nReiciendis eum sunt vero blanditiis at quia. Voluptate eum facilis illum ea distinctio maiores. Doloribus aut nemo ea distinctio.\n\nNihil cum distinctio voluptates quam. Laboriosam distinctio ea accusantium soluta perspiciatis nesciunt impedit. Id qui natus quis minima voluptatum velit ut reprehenderit. Molestiae quia est harum sapiente rem error architecto id. Et minus ipsa et ut ut.","start_date_sourcing_milestone_id":null,"due_date_sourcing_milestone_id":null,"start_date_fixed":null,"due_date_fixed":null,"start_date_is_fixed":null,"due_date_is_fixed":null,"closed_by_id":null,"closed_at":null,"parent_id":null,"relative_position":null,"state_id":"opened","start_date_sourcing_epic_id":null,"due_date_sourcing_epic_id":null,"notes":[{"id":44170,"note":"added epic &5 as child epic","noteable_type":"Epic","author_id":1,"created_at":"2019-11-20T18:38:40.031Z","updated_at":"2019-11-20T18:38:40.035Z","project_id":null,"attachment":{"url":null},"line_code":null,"commit_id":null,"noteable_id":13622,"system":true,"st_diff":null,"updated_by_id":null,"position":null,"original_position":null,"resolved_at":null,"resolved_by_id":null,"discussion_id":"ba005d8dd59cd37a4f32406d46e759b08fd15510","change_position":null,"resolved_by_push":null,"review_id":null,"type":null,"author":{"name":"Administrator"},"award_emoji":[{"id":12,"name":"drum","user_id":1,"awardable_type":"Note","awardable_id":44170,"created_at":"2019-11-05T15:32:21.287Z","updated_at":"2019-11-05T15:32:21.287Z"}]},{"id":44168,"note":"added epic &4 as child epic","noteable_type":"Epic","author_id":1,"created_at":"2019-11-20T18:38:35.669Z","updated_at":"2019-11-20T18:38:35.673Z","project_id":null,"attachment":{"url":null},"line_code":null,"commit_id":null,"noteable_id":13622,"system":true,"st_diff":null,"updated_by_id":null,"position":null,"original_position":null,"resolved_at":null,"resolved_by_id":null,"discussion_id":"9b49d3b017aadc1876d477b960e6f8efb99ce29f","change_position":null,"resolved_by_push":null,"review_id":null,"type":null,"author":{"name":"Administrator"}},{"id":44166,"note":"added epic &3 as child epic","noteable_type":"Epic","author_id":1,"created_at":"2019-11-20T18:38:30.944Z","updated_at":"2019-11-20T18:38:30.948Z","project_id":null,"attachment":{"url":null},"line_code":null,"commit_id":null,"noteable_id":13622,"system":true,"st_diff":null,"updated_by_id":null,"position":null,"original_position":null,"resolved_at":null,"resolved_by_id":null,"discussion_id":"cccfe967f48e699a466c87a55a9f8acb00fec1a1","change_position":null,"resolved_by_push":null,"review_id":null,"type":null,"author":{"name":"Administrator"}},{"id":44164,"note":"added epic &2 as child epic","noteable_type":"Epic","author_id":1,"created_at":"2019-11-20T18:38:26.689Z","updated_at":"2019-11-20T18:38:26.724Z","project_id":null,"attachment":{"url":null},"line_code":null,"commit_id":null,"noteable_id":13622,"system":true,"st_diff":null,"updated_by_id":null,"position":null,"original_position":null,"resolved_at":null,"resolved_by_id":null,"discussion_id":"133f0c3001860fa8d2031e398a65db74477378c4","change_position":null,"resolved_by_push":null,"review_id":null,"type":null,"author":{"name":"Administrator"}}],"award_emoji":[{"id":12,"name":"thumbsup","user_id":1,"awardable_type":"Epic","awardable_id":13622,"created_at":"2019-11-05T15:37:21.287Z","updated_at":"2019-11-05T15:37:21.287Z"}]}
+{"id":13623,"milestone_id":null,"group_id":4351,"author_id":1,"assignee_id":null,"iid":2,"updated_by_id":null,"last_edited_by_id":null,"lock_version":0,"start_date":null,"end_date":null,"last_edited_at":null,"created_at":"2019-11-20T17:02:09.769Z","updated_at":"2019-11-20T18:38:26.851Z","title":"Omnis accusantium commodi voluptas odio illo eum ut.","description":"Eius vero et iste amet est voluptatem modi. Doloribus ipsam beatae et ut autem ut animi. Dolor culpa dolor omnis delectus est tempora inventore ab. Optio labore tenetur libero quia provident et quis. Blanditiis architecto sint possimus cum aut adipisci.\n\nDolores quisquam sunt cupiditate unde qui vitae nemo. Odio quas omnis ut nobis. Possimus fugit deserunt quia sed ab numquam veritatis nihil.\n\nQui nemo adipisci magnam perferendis voluptatem modi. Eius enim iure dolores consequuntur eum nobis adipisci. Consequatur architecto et quas deleniti hic id laborum officiis. Enim perferendis quis quasi totam delectus rerum deleniti.","start_date_sourcing_milestone_id":null,"due_date_sourcing_milestone_id":null,"start_date_fixed":null,"due_date_fixed":null,"start_date_is_fixed":null,"due_date_is_fixed":null,"closed_by_id":null,"closed_at":null,"parent_id":13622,"relative_position":1073741323,"state_id":"opened","start_date_sourcing_epic_id":null,"due_date_sourcing_epic_id":null,"parent":{"id":13622,"milestone_id":null,"group_id":4351,"author_id":1,"assignee_id":null,"iid":1,"updated_by_id":null,"last_edited_by_id":null,"lock_version":0,"start_date":null,"end_date":null,"last_edited_at":null,"created_at":"2019-11-20T17:02:09.754Z","updated_at":"2019-11-20T18:38:40.054Z","title":"Provident neque consequatur numquam ad laboriosam voluptatem magnam.","description":"Fugit nisi est ut numquam quia rerum vitae qui. Et in est aliquid voluptas et ut vitae. In distinctio voluptates ut deleniti iste.\n\nReiciendis eum sunt vero blanditiis at quia. Voluptate eum facilis illum ea distinctio maiores. Doloribus aut nemo ea distinctio.\n\nNihil cum distinctio voluptates quam. Laboriosam distinctio ea accusantium soluta perspiciatis nesciunt impedit. Id qui natus quis minima voluptatum velit ut reprehenderit. Molestiae quia est harum sapiente rem error architecto id. Et minus ipsa et ut ut.","start_date_sourcing_milestone_id":null,"due_date_sourcing_milestone_id":null,"start_date_fixed":null,"due_date_fixed":null,"start_date_is_fixed":null,"due_date_is_fixed":null,"closed_by_id":null,"closed_at":null,"parent_id":null,"relative_position":null,"state_id":"opened","start_date_sourcing_epic_id":null,"due_date_sourcing_epic_id":null},"notes":[{"id":44165,"note":"added epic &1 as parent epic","noteable_type":"Epic","author_id":1,"created_at":"2019-11-20T18:38:26.822Z","updated_at":"2019-11-20T18:38:26.826Z","project_id":null,"attachment":{"url":null},"line_code":null,"commit_id":null,"noteable_id":13623,"system":true,"st_diff":null,"updated_by_id":null,"position":null,"original_position":null,"resolved_at":null,"resolved_by_id":null,"discussion_id":"15f0a7f4ed16a07bc78841e122524bb867edcf86","change_position":null,"resolved_by_push":null,"review_id":null,"type":null,"author":{"name":"Administrator"}}]}
+{"id":13624,"milestone_id":null,"group_id":4351,"author_id":1,"assignee_id":null,"iid":3,"updated_by_id":null,"last_edited_by_id":null,"lock_version":0,"start_date":null,"end_date":null,"last_edited_at":null,"created_at":"2019-11-20T17:02:09.783Z","updated_at":"2019-11-20T18:38:31.018Z","title":"Quis dolore velit possimus eaque aut amet esse voluptate aliquam.","description":"Ab veritatis reprehenderit nulla laboriosam et sed asperiores corporis. Est accusantium maxime perferendis et. Omnis a qui voluptates non excepturi.\n\nAdipisci labore maiores dicta sed magnam aut. Veritatis delectus dolorum qui id. Dolorum tenetur quo iure amet. Eveniet reprehenderit dolor ipsam quia ratione quo. Facilis voluptatem vel repellat id illum.\n\nAut et magnam aut minus aspernatur. Fuga quo necessitatibus mollitia maxime quasi. Qui aspernatur quia accusamus est quod. Qui assumenda veritatis dolor non eveniet quibusdam quos qui.","start_date_sourcing_milestone_id":null,"due_date_sourcing_milestone_id":null,"start_date_fixed":null,"due_date_fixed":null,"start_date_is_fixed":null,"due_date_is_fixed":null,"closed_by_id":null,"closed_at":null,"parent_id":13622,"relative_position":1073740823,"state_id":"opened","start_date_sourcing_epic_id":null,"due_date_sourcing_epic_id":null,"parent":{"id":13622,"milestone_id":null,"group_id":4351,"author_id":1,"assignee_id":null,"iid":1,"updated_by_id":null,"last_edited_by_id":null,"lock_version":0,"start_date":null,"end_date":null,"last_edited_at":null,"created_at":"2019-11-20T17:02:09.754Z","updated_at":"2019-11-20T18:38:40.054Z","title":"Provident neque consequatur numquam ad laboriosam voluptatem magnam.","description":"Fugit nisi est ut numquam quia rerum vitae qui. Et in est aliquid voluptas et ut vitae. In distinctio voluptates ut deleniti iste.\n\nReiciendis eum sunt vero blanditiis at quia. Voluptate eum facilis illum ea distinctio maiores. Doloribus aut nemo ea distinctio.\n\nNihil cum distinctio voluptates quam. Laboriosam distinctio ea accusantium soluta perspiciatis nesciunt impedit. Id qui natus quis minima voluptatum velit ut reprehenderit. Molestiae quia est harum sapiente rem error architecto id. Et minus ipsa et ut ut.","start_date_sourcing_milestone_id":null,"due_date_sourcing_milestone_id":null,"start_date_fixed":null,"due_date_fixed":null,"start_date_is_fixed":null,"due_date_is_fixed":null,"closed_by_id":null,"closed_at":null,"parent_id":null,"relative_position":null,"state_id":"opened","start_date_sourcing_epic_id":null,"due_date_sourcing_epic_id":null},"notes":[{"id":44167,"note":"added epic &1 as parent epic","noteable_type":"Epic","author_id":1,"created_at":"2019-11-20T18:38:30.989Z","updated_at":"2019-11-20T18:38:30.993Z","project_id":null,"attachment":{"url":null},"line_code":null,"commit_id":null,"noteable_id":13624,"system":true,"st_diff":null,"updated_by_id":null,"position":null,"original_position":null,"resolved_at":null,"resolved_by_id":null,"discussion_id":"423ffec14a3ce148c11a802eb1f2613fa8ca9a94","change_position":null,"resolved_by_push":null,"review_id":null,"type":null,"author":{"name":"Administrator"}}]}
+{"id":13625,"milestone_id":null,"group_id":4351,"author_id":1,"assignee_id":null,"iid":4,"updated_by_id":null,"last_edited_by_id":null,"lock_version":0,"start_date":null,"end_date":null,"last_edited_at":null,"created_at":"2019-11-20T17:02:09.798Z","updated_at":"2019-11-20T18:38:35.765Z","title":"Possimus et ut iste temporibus earum cupiditate voluptatem esse assumenda amet.","description":"Et at corporis sed id rerum ullam dolore. Odio magnam corporis excepturi neque est. Est accusamus nostrum qui rerum.\n\nEt aut dolores eaque quibusdam aut quas explicabo id. Est necessitatibus praesentium omnis et vero laboriosam et. Sunt in saepe qui laudantium et voluptas.\n\nVelit sunt odit eum omnis beatae eius aut. Dolores commodi qui impedit deleniti et magnam pariatur. Aut odit amet ipsum ea atque. Itaque est ut sunt ullam eum nam.","start_date_sourcing_milestone_id":null,"due_date_sourcing_milestone_id":null,"start_date_fixed":null,"due_date_fixed":null,"start_date_is_fixed":null,"due_date_is_fixed":null,"closed_by_id":null,"closed_at":null,"parent_id":13622,"relative_position":1073740323,"state_id":"opened","start_date_sourcing_epic_id":null,"due_date_sourcing_epic_id":null,"parent":{"id":13622,"milestone_id":null,"group_id":4351,"author_id":1,"assignee_id":null,"iid":1,"updated_by_id":null,"last_edited_by_id":null,"lock_version":0,"start_date":null,"end_date":null,"last_edited_at":null,"created_at":"2019-11-20T17:02:09.754Z","updated_at":"2019-11-20T18:38:40.054Z","title":"Provident neque consequatur numquam ad laboriosam voluptatem magnam.","description":"Fugit nisi est ut numquam quia rerum vitae qui. Et in est aliquid voluptas et ut vitae. In distinctio voluptates ut deleniti iste.\n\nReiciendis eum sunt vero blanditiis at quia. Voluptate eum facilis illum ea distinctio maiores. Doloribus aut nemo ea distinctio.\n\nNihil cum distinctio voluptates quam. Laboriosam distinctio ea accusantium soluta perspiciatis nesciunt impedit. Id qui natus quis minima voluptatum velit ut reprehenderit. Molestiae quia est harum sapiente rem error architecto id. Et minus ipsa et ut ut.","start_date_sourcing_milestone_id":null,"due_date_sourcing_milestone_id":null,"start_date_fixed":null,"due_date_fixed":null,"start_date_is_fixed":null,"due_date_is_fixed":null,"closed_by_id":null,"closed_at":null,"parent_id":null,"relative_position":null,"state_id":"opened","start_date_sourcing_epic_id":null,"due_date_sourcing_epic_id":null},"notes":[{"id":44169,"note":"added epic &1 as parent epic","noteable_type":"Epic","author_id":1,"created_at":"2019-11-20T18:38:35.737Z","updated_at":"2019-11-20T18:38:35.741Z","project_id":null,"attachment":{"url":null},"line_code":null,"commit_id":null,"noteable_id":13625,"system":true,"st_diff":null,"updated_by_id":null,"position":null,"original_position":null,"resolved_at":null,"resolved_by_id":null,"discussion_id":"5bc3e30d508affafc61de2b4e1d9f21039505cc3","change_position":null,"resolved_by_push":null,"review_id":null,"type":null,"author":{"name":"Administrator"}}]}
+{"id":13626,"milestone_id":null,"group_id":4351,"author_id":1,"assignee_id":null,"iid":5,"updated_by_id":null,"last_edited_by_id":null,"lock_version":0,"start_date":null,"end_date":null,"last_edited_at":null,"created_at":"2019-11-20T17:02:09.812Z","updated_at":"2019-11-20T18:38:40.101Z","title":"Ab deleniti ipsum voluptatem dolor qui quos saepe repellat quo.","description":"Sunt minus sunt reiciendis culpa sed excepturi. Aperiam sed quod nemo nesciunt et quia molestias incidunt. Ipsum nam magnam labore eos a molestiae rerum possimus. Sequi autem asperiores voluptas assumenda.\n\nRerum ipsa quia cum ab corrupti omnis. Velit libero et nihil ipsa aut quo rem ipsam. Architecto omnis distinctio sed doloribus perspiciatis consequatur aut et. Fugit consequuntur est minima reiciendis reprehenderit et.\n\nConsequatur distinctio et ut blanditiis perferendis officiis inventore. Alias aut voluptatem in facere. Ut perferendis dolorum hic dolores. Ipsa dolorem soluta at mollitia. Placeat et ea numquam dicta molestias.","start_date_sourcing_milestone_id":null,"due_date_sourcing_milestone_id":null,"start_date_fixed":null,"due_date_fixed":null,"start_date_is_fixed":null,"due_date_is_fixed":null,"closed_by_id":null,"closed_at":null,"parent_id":13622,"relative_position":1073739823,"state_id":"opened","start_date_sourcing_epic_id":null,"due_date_sourcing_epic_id":null,"parent":{"id":13622,"milestone_id":null,"group_id":4351,"author_id":1,"assignee_id":null,"iid":1,"updated_by_id":null,"last_edited_by_id":null,"lock_version":0,"start_date":null,"end_date":null,"last_edited_at":null,"created_at":"2019-11-20T17:02:09.754Z","updated_at":"2019-11-20T18:38:40.054Z","title":"Provident neque consequatur numquam ad laboriosam voluptatem magnam.","description":"Fugit nisi est ut numquam quia rerum vitae qui. Et in est aliquid voluptas et ut vitae. In distinctio voluptates ut deleniti iste.\n\nReiciendis eum sunt vero blanditiis at quia. Voluptate eum facilis illum ea distinctio maiores. Doloribus aut nemo ea distinctio.\n\nNihil cum distinctio voluptates quam. Laboriosam distinctio ea accusantium soluta perspiciatis nesciunt impedit. Id qui natus quis minima voluptatum velit ut reprehenderit. Molestiae quia est harum sapiente rem error architecto id. Et minus ipsa et ut ut.","start_date_sourcing_milestone_id":null,"due_date_sourcing_milestone_id":null,"start_date_fixed":null,"due_date_fixed":null,"start_date_is_fixed":null,"due_date_is_fixed":null,"closed_by_id":null,"closed_at":null,"parent_id":null,"relative_position":null,"state_id":"opened","start_date_sourcing_epic_id":null,"due_date_sourcing_epic_id":null},"notes":[{"id":44171,"note":"added epic &1 as parent epic","noteable_type":"Epic","author_id":1,"created_at":"2019-11-20T18:38:40.074Z","updated_at":"2019-11-20T18:38:40.077Z","project_id":null,"attachment":{"url":null},"line_code":null,"commit_id":null,"noteable_id":13626,"system":true,"st_diff":null,"updated_by_id":null,"position":null,"original_position":null,"resolved_at":null,"resolved_by_id":null,"discussion_id":"a6231acdaef5f4d2e569dfb604f1baf85c49e1a0","change_position":null,"resolved_by_push":null,"review_id":null,"type":null,"author":{"name":"Administrator"}}]}
diff --git a/spec/fixtures/lib/gitlab/import_export/group_exports/complex/tree/groups/4351/labels.ndjson b/spec/fixtures/lib/gitlab/import_export/group_exports/complex/tree/groups/4351/labels.ndjson
new file mode 100644
index 00000000000..8508c4347c2
--- /dev/null
+++ b/spec/fixtures/lib/gitlab/import_export/group_exports/complex/tree/groups/4351/labels.ndjson
@@ -0,0 +1,10 @@
+{"id":23452,"title":"Bruffefunc","color":"#1d2da4","project_id":null,"created_at":"2019-11-20T17:02:20.546Z","updated_at":"2019-11-20T17:02:20.546Z","template":false,"description":null,"group_id":4351,"type":"GroupLabel","priorities":[],"textColor":"#FFFFFF"}
+{"id":23446,"title":"Cafunc","color":"#73ed5b","project_id":null,"created_at":"2019-11-20T17:02:20.526Z","updated_at":"2019-11-20T17:02:20.526Z","template":false,"description":null,"group_id":4351,"type":"GroupLabel","priorities":[],"textColor":"#FFFFFF"}
+{"id":23451,"title":"Casche","color":"#649a75","project_id":null,"created_at":"2019-11-20T17:02:20.544Z","updated_at":"2019-11-20T17:02:20.544Z","template":false,"description":null,"group_id":4351,"type":"GroupLabel","priorities":[],"textColor":"#FFFFFF"}
+{"id":23444,"title":"Cocell","color":"#1b365c","project_id":null,"created_at":"2019-11-20T17:02:20.521Z","updated_at":"2019-11-20T17:02:20.521Z","template":false,"description":null,"group_id":4351,"type":"GroupLabel","priorities":[],"textColor":"#FFFFFF"}
+{"id":23449,"title":"Packfunc","color":"#e33bba","project_id":null,"created_at":"2019-11-20T17:02:20.537Z","updated_at":"2019-11-20T17:02:20.537Z","template":false,"description":null,"group_id":4351,"type":"GroupLabel","priorities":[],"textColor":"#FFFFFF"}
+{"id":23443,"title":"Panabalt","color":"#84f708","project_id":null,"created_at":"2019-11-20T17:02:20.518Z","updated_at":"2019-11-20T17:02:20.518Z","template":false,"description":null,"group_id":4351,"type":"GroupLabel","priorities":[],"textColor":"#FFFFFF"}
+{"id":23447,"title":"Phierefunc","color":"#4ab4a8","project_id":null,"created_at":"2019-11-20T17:02:20.530Z","updated_at":"2019-11-20T17:02:20.530Z","template":false,"description":null,"group_id":4351,"type":"GroupLabel","priorities":[],"textColor":"#FFFFFF"}
+{"id":23445,"title":"Pons","color":"#47f440","project_id":null,"created_at":"2019-11-20T17:02:20.523Z","updated_at":"2019-11-20T17:02:20.523Z","template":false,"description":null,"group_id":4351,"type":"GroupLabel","priorities":[],"textColor":"#FFFFFF"}
+{"id":23448,"title":"Sosync","color":"#110320","project_id":null,"created_at":"2019-11-20T17:02:20.532Z","updated_at":"2019-11-20T17:02:20.532Z","template":false,"description":null,"group_id":4351,"type":"GroupLabel","priorities":[],"textColor":"#FFFFFF"}
+{"id":23450,"title":"TSL","color":"#58796f","project_id":null,"created_at":"2019-11-20T17:02:20.541Z","updated_at":"2019-11-20T17:02:20.541Z","template":false,"description":null,"group_id":4351,"type":"GroupLabel","priorities":[],"textColor":"#FFFFFF"}
diff --git a/spec/fixtures/lib/gitlab/import_export/group_exports/complex/tree/groups/4351/members.ndjson b/spec/fixtures/lib/gitlab/import_export/group_exports/complex/tree/groups/4351/members.ndjson
new file mode 100644
index 00000000000..ec3733a2b2b
--- /dev/null
+++ b/spec/fixtures/lib/gitlab/import_export/group_exports/complex/tree/groups/4351/members.ndjson
@@ -0,0 +1,6 @@
+{"id":13766,"access_level":30,"source_id":4351,"source_type":"Namespace","user_id":42,"notification_level":3,"created_at":"2019-11-20T17:04:36.184Z","updated_at":"2019-11-20T17:04:36.184Z","created_by_id":null,"invite_email":null,"invite_token":null,"invite_accepted_at":null,"requested_at":null,"expires_at":null,"ldap":false,"override":false,"user":{"id":42,"email":"moriah@collinsmurphy.com","username":"reported_user_15"}}
+{"id":13765,"access_level":40,"source_id":4351,"source_type":"Namespace","user_id":271,"notification_level":3,"created_at":"2019-11-20T17:04:36.044Z","updated_at":"2019-11-20T17:04:36.044Z","created_by_id":null,"invite_email":null,"invite_token":null,"invite_accepted_at":null,"requested_at":null,"expires_at":null,"ldap":false,"override":false,"user":{"id":271,"email":"garret@connellystark.ca","username":"charlesetta"}}
+{"id":13764,"access_level":30,"source_id":4351,"source_type":"Namespace","user_id":206,"notification_level":3,"created_at":"2019-11-20T17:04:35.840Z","updated_at":"2019-11-20T17:04:35.840Z","created_by_id":null,"invite_email":null,"invite_token":null,"invite_accepted_at":null,"requested_at":null,"expires_at":null,"ldap":false,"override":false,"user":{"id":206,"email":"gwendolyn_robel@gitlabexample.com","username":"gwendolyn_robel"}}
+{"id":13763,"access_level":10,"source_id":4351,"source_type":"Namespace","user_id":39,"notification_level":3,"created_at":"2019-11-20T17:04:35.704Z","updated_at":"2019-11-20T17:04:35.704Z","created_by_id":null,"invite_email":null,"invite_token":null,"invite_accepted_at":null,"requested_at":null,"expires_at":null,"ldap":false,"override":false,"user":{"id":39,"email":"alexis_berge@kerlukeklein.us","username":"reported_user_12"}}
+{"id":13762,"access_level":20,"source_id":4351,"source_type":"Namespace","user_id":1624,"notification_level":3,"created_at":"2019-11-20T17:04:35.566Z","updated_at":"2019-11-20T17:04:35.566Z","created_by_id":null,"invite_email":null,"invite_token":null,"invite_accepted_at":null,"requested_at":null,"expires_at":null,"ldap":false,"override":false,"user":{"id":1624,"email":"adriene.mcclure@gitlabexample.com","username":"adriene.mcclure"}}
+{"id":12920,"access_level":50,"source_id":4351,"source_type":"Namespace","user_id":1,"notification_level":3,"created_at":"2019-11-20T17:01:53.505Z","updated_at":"2019-11-20T17:01:53.505Z","created_by_id":null,"invite_email":null,"invite_token":null,"invite_accepted_at":null,"requested_at":null,"expires_at":null,"ldap":false,"override":false,"user":{"id":1,"email":"admin@example.com","username":"root"}}
diff --git a/spec/fixtures/lib/gitlab/import_export/group_exports/complex/tree/groups/4351/milestones.ndjson b/spec/fixtures/lib/gitlab/import_export/group_exports/complex/tree/groups/4351/milestones.ndjson
new file mode 100644
index 00000000000..40523f276e7
--- /dev/null
+++ b/spec/fixtures/lib/gitlab/import_export/group_exports/complex/tree/groups/4351/milestones.ndjson
@@ -0,0 +1,5 @@
+{"id":7642,"title":"v4.0","project_id":null,"description":"Et laudantium enim omnis ea reprehenderit iure.","due_date":null,"created_at":"2019-11-20T17:02:14.336Z","updated_at":"2019-11-20T17:02:14.336Z","state":"closed","iid":5,"start_date":null,"group_id":4351}
+{"id":7641,"title":"v3.0","project_id":null,"description":"Et repellat culpa nemo consequatur ut reprehenderit.","due_date":null,"created_at":"2019-11-20T17:02:14.323Z","updated_at":"2019-11-20T17:02:14.323Z","state":"active","iid":4,"start_date":null,"group_id":4351}
+{"id":7640,"title":"v2.0","project_id":null,"description":"Velit cupiditate est neque voluptates iste rem sunt.","due_date":null,"created_at":"2019-11-20T17:02:14.309Z","updated_at":"2019-11-20T17:02:14.309Z","state":"active","iid":3,"start_date":null,"group_id":4351}
+{"id":7639,"title":"v1.0","project_id":null,"description":"Amet velit repellat ut rerum aut cum.","due_date":null,"created_at":"2019-11-20T17:02:14.296Z","updated_at":"2019-11-20T17:02:14.296Z","state":"active","iid":2,"start_date":null,"group_id":4351}
+{"id":7638,"title":"v0.0","project_id":null,"description":"Ea quia asperiores ut modi dolorem sunt non numquam.","due_date":null,"created_at":"2019-11-20T17:02:14.282Z","updated_at":"2019-11-20T17:02:14.282Z","state":"active","iid":1,"start_date":null,"group_id":4351}
diff --git a/spec/fixtures/lib/gitlab/import_export/group_exports/complex/tree/groups/4352.json b/spec/fixtures/lib/gitlab/import_export/group_exports/complex/tree/groups/4352.json
new file mode 100644
index 00000000000..45e561dcda1
--- /dev/null
+++ b/spec/fixtures/lib/gitlab/import_export/group_exports/complex/tree/groups/4352.json
@@ -0,0 +1 @@
+{"name":"pwip17beq7vl4nuwz9ie7bk8navpxj1w04zylmmjveab5bargr","path":"pwip17beq7vl4nuwz9ie7bk8navpxj1w04zylmmjveab5bargr","owner_id":null,"created_at":"2019-11-20 17:01:53 UTC","updated_at":"2019-11-20 17:05:44 UTC","description":"","avatar":{"url":null},"membership_lock":false,"share_with_group_lock":false,"visibility_level":0,"request_access_enabled":true,"ldap_sync_status":"ready","ldap_sync_error":null,"ldap_sync_last_update_at":null,"ldap_sync_last_successful_update_at":null,"ldap_sync_last_sync_at":null,"lfs_enabled":null,"parent_id":4351,"shared_runners_minutes_limit":null,"repository_size_limit":null,"require_two_factor_authentication":false,"two_factor_grace_period":48,"plan_id":null,"project_creation_level":2,"trial_ends_on":null,"file_template_project_id":null,"saml_discovery_token":"ki3Xnjw3","custom_project_templates_group_id":null,"auto_devops_enabled":null,"extra_shared_runners_minutes_limit":null,"last_ci_minutes_notification_at":null,"last_ci_minutes_usage_notification_level":null,"subgroup_creation_level":1,"emails_disabled":null,"max_pages_size":null,"max_artifacts_size":null,"id":4352} \ No newline at end of file
diff --git a/spec/fixtures/lib/gitlab/import_export/group_exports/complex/tree/groups/4352/badges.ndjson b/spec/fixtures/lib/gitlab/import_export/group_exports/complex/tree/groups/4352/badges.ndjson
new file mode 100644
index 00000000000..5fc58de6d44
--- /dev/null
+++ b/spec/fixtures/lib/gitlab/import_export/group_exports/complex/tree/groups/4352/badges.ndjson
@@ -0,0 +1 @@
+{"id":14,"link_url":"https://localhost:3443/%{default_branch}","image_url":"https://badge_image.png","project_id":null,"group_id":4352,"created_at":"2019-11-20T17:29:36.656Z","updated_at":"2019-11-20T17:29:36.656Z","type":"GroupBadge"}
diff --git a/spec/fixtures/lib/gitlab/import_export/group_exports/complex/tree/groups/4352/boards.ndjson b/spec/fixtures/lib/gitlab/import_export/group_exports/complex/tree/groups/4352/boards.ndjson
new file mode 100644
index 00000000000..51d6750e412
--- /dev/null
+++ b/spec/fixtures/lib/gitlab/import_export/group_exports/complex/tree/groups/4352/boards.ndjson
@@ -0,0 +1,2 @@
+{"id":64,"project_id":null,"created_at":"2019-11-20T17:29:39.872Z","updated_at":"2019-11-20T17:29:39.872Z","name":"Development","milestone_id":null,"group_id":4352,"weight":null,"labels":[]}
+{"id":65,"project_id":null,"created_at":"2019-11-20T17:29:47.304Z","updated_at":"2019-11-20T17:29:47.304Z","name":"Sub Board 4","milestone_id":null,"group_id":4352,"weight":null,"labels":[]}
diff --git a/spec/fixtures/lib/gitlab/import_export/group_exports/complex/tree/groups/4352/epics.ndjson b/spec/fixtures/lib/gitlab/import_export/group_exports/complex/tree/groups/4352/epics.ndjson
new file mode 100644
index 00000000000..3ec50b72629
--- /dev/null
+++ b/spec/fixtures/lib/gitlab/import_export/group_exports/complex/tree/groups/4352/epics.ndjson
@@ -0,0 +1,5 @@
+{"id":13627,"milestone_id":null,"group_id":4352,"author_id":1,"assignee_id":null,"iid":1,"updated_by_id":null,"last_edited_by_id":null,"lock_version":0,"start_date":null,"end_date":null,"last_edited_at":null,"created_at":"2019-11-20T17:02:09.848Z","updated_at":"2019-11-20T17:02:09.848Z","title":"Nobis omnis occaecati veritatis quia eveniet sed ut cupiditate ut a.","description":"Provident iusto ipsam fuga vero. Aut mollitia earum iusto doloremque recusandae enim nam et. Quas maxime sint libero dolorum aut cumque molestias quam. Iure voluptas voluptatum similique voluptatem dolorem.\n\nAnimi aliquid praesentium sint voluptatum fuga voluptates molestias. Non hic sit modi minus a. Illum asperiores sed eius dolor impedit animi. Dolor vel fugit voluptas quia voluptatem aut minus.\n\nVelit voluptatum deleniti illo quos omnis deserunt. Omnis consequatur omnis nulla et et. Praesentium dolores rem consequatur laboriosam harum quae. Aut id aliquam nihil consequuntur.","start_date_sourcing_milestone_id":null,"due_date_sourcing_milestone_id":null,"start_date_fixed":null,"due_date_fixed":null,"start_date_is_fixed":null,"due_date_is_fixed":null,"closed_by_id":null,"closed_at":null,"parent_id":null,"relative_position":null,"state":"opened","start_date_sourcing_epic_id":null,"due_date_sourcing_epic_id":null,"notes":[]}
+{"id":13628,"milestone_id":null,"group_id":4352,"author_id":1,"assignee_id":null,"iid":2,"updated_by_id":null,"last_edited_by_id":null,"lock_version":0,"start_date":null,"end_date":null,"last_edited_at":null,"created_at":"2019-11-20T17:02:09.863Z","updated_at":"2019-11-20T17:02:09.863Z","title":"Assumenda possimus sed nostrum consequatur ut sint nihil fugiat.","description":"Culpa fugiat voluptas ut voluptas quo laborum eius. Earum qui dolore temporibus consequatur ratione minima architecto accusantium. Corporis accusantium et consequatur est mollitia sint fugiat aliquam. Est aut quia blanditiis et sint reiciendis. Eveniet accusamus quod molestiae vero hic a ipsum.\n\nNon numquam eum repellendus ipsa tempore necessitatibus. Delectus aut doloremque quis saepe nam ut aut a. Qui corrupti eum animi ipsam. Voluptatem distinctio consequatur accusantium blanditiis.\n\nQuis voluptatum facere inventore itaque quae. Quis quae dolorum autem qui labore. Laboriosam asperiores laborum aperiam voluptatibus error ut quos similique. Deleniti fugit ut eveniet ab quae.","start_date_sourcing_milestone_id":null,"due_date_sourcing_milestone_id":null,"start_date_fixed":null,"due_date_fixed":null,"start_date_is_fixed":null,"due_date_is_fixed":null,"closed_by_id":null,"closed_at":null,"parent_id":null,"relative_position":null,"state":"closed","start_date_sourcing_epic_id":null,"due_date_sourcing_epic_id":null,"notes":[]}
+{"id":13629,"milestone_id":null,"group_id":4352,"author_id":1,"assignee_id":null,"iid":3,"updated_by_id":null,"last_edited_by_id":null,"lock_version":0,"start_date":null,"end_date":null,"last_edited_at":null,"created_at":"2019-11-20T17:02:09.879Z","updated_at":"2019-11-20T17:02:09.879Z","title":"Ut dolore eos molestiae perferendis quibusdam accusamus.","description":"Possimus vel adipisci consequatur asperiores. Et aspernatur quis ipsum aut natus tempora. Recusandae voluptatibus officiis praesentium et. Nostrum beatae laboriosam dolor nihil ut deserunt ad. Exercitationem iure hic minus deleniti assumenda quis rem.\n\nVoluptate optio et impedit sapiente dignissimos deleniti sit ea. Neque modi voluptates accusamus non non officia sit quis. Qui nihil dolores aut nostrum quia sed dolore perspiciatis. Vero necessitatibus inventore eligendi est aliquid dolorum.\n\nNulla et autem aut fugit aut aut expedita. Molestiae beatae eligendi reiciendis temporibus mollitia aut reprehenderit. Autem maiores rerum dolorum cupiditate. Cum est quasi ab et. Ratione doloribus quas perspiciatis alias voluptates et.","start_date_sourcing_milestone_id":null,"due_date_sourcing_milestone_id":null,"start_date_fixed":null,"due_date_fixed":null,"start_date_is_fixed":null,"due_date_is_fixed":null,"closed_by_id":null,"closed_at":null,"parent_id":null,"relative_position":null,"state":"opened","start_date_sourcing_epic_id":null,"due_date_sourcing_epic_id":null,"notes":[]}
+{"id":13630,"milestone_id":null,"group_id":4352,"author_id":1,"assignee_id":null,"iid":4,"updated_by_id":null,"last_edited_by_id":null,"lock_version":0,"start_date":null,"end_date":null,"last_edited_at":null,"created_at":"2019-11-20T17:02:09.894Z","updated_at":"2019-11-20T17:02:09.894Z","title":"Molestias numquam ut veritatis suscipit eum vitae minima et consequatur sit.","description":"Ad omnis tempore blanditiis vero possimus. Quis quidem et quo cumque pariatur. Nihil eaque inventore natus delectus est qui voluptate. Officiis illo voluptatum aut modi. Inventore voluptate est voluptatem deserunt aut esse.\n\nOdit deserunt ut expedita sit ut. Nam est aut alias quibusdam. Est delectus ratione expedita hic eaque est. Delectus est voluptatibus quo aut dolorem. Libero saepe alias aspernatur itaque et qui.\n\nOmnis voluptas nemo nostrum accusantium. Perspiciatis cupiditate quia quo asperiores. Voluptas perspiciatis nihil officia consectetur recusandae. Libero sed eum laborum expedita quisquam soluta incidunt odit.","start_date_sourcing_milestone_id":null,"due_date_sourcing_milestone_id":null,"start_date_fixed":null,"due_date_fixed":null,"start_date_is_fixed":null,"due_date_is_fixed":null,"closed_by_id":null,"closed_at":null,"parent_id":null,"relative_position":null,"state":"closed","start_date_sourcing_epic_id":null,"due_date_sourcing_epic_id":null,"notes":[]}
+{"id":13631,"milestone_id":null,"group_id":4352,"author_id":1,"assignee_id":null,"iid":5,"updated_by_id":null,"last_edited_by_id":null,"lock_version":0,"start_date":null,"end_date":null,"last_edited_at":null,"created_at":"2019-11-20T17:02:09.908Z","updated_at":"2019-11-20T17:02:09.908Z","title":"Labore quas voluptas delectus fugiat aut nihil vero.","description":"Necessitatibus aspernatur sunt repellat non animi reprehenderit. Dolor harum ad tempore nesciunt aperiam tenetur. Tempore in est sed quo. Aliquam eaque ullam est consequuntur porro rerum minima aspernatur. Ullam cupiditate illum dicta praesentium assumenda.\n\nEnim impedit ab dolorem libero maiores. Non consectetur ut molestiae quo atque quae necessitatibus. Placeat eveniet minus occaecati magni.\n\nConsequuntur laboriosam quisquam quo eligendi et quia. Sunt ipsam unde adipisci ad praesentium. Odit quia eius quia harum dolor nobis.","start_date_sourcing_milestone_id":null,"due_date_sourcing_milestone_id":null,"start_date_fixed":null,"due_date_fixed":null,"start_date_is_fixed":null,"due_date_is_fixed":null,"closed_by_id":null,"closed_at":null,"parent_id":null,"relative_position":null,"state":"opened","start_date_sourcing_epic_id":null,"due_date_sourcing_epic_id":null,"notes":[]}
diff --git a/spec/fixtures/lib/gitlab/import_export/group_exports/complex/tree/groups/4352/labels.ndjson b/spec/fixtures/lib/gitlab/import_export/group_exports/complex/tree/groups/4352/labels.ndjson
new file mode 100644
index 00000000000..a0a63c14ed7
--- /dev/null
+++ b/spec/fixtures/lib/gitlab/import_export/group_exports/complex/tree/groups/4352/labels.ndjson
@@ -0,0 +1,9 @@
+{"id":23453,"title":"Brire","color":"#d68d9d","project_id":null,"created_at":"2019-11-20T17:02:20.549Z","updated_at":"2019-11-20T17:02:20.549Z","template":false,"description":null,"group_id":4352,"type":"GroupLabel","priorities":[],"textColor":"#333333"}
+{"id":23461,"title":"Cygfunc","color":"#a0695d","project_id":null,"created_at":"2019-11-20T17:02:20.575Z","updated_at":"2019-11-20T17:02:20.575Z","template":false,"description":null,"group_id":4352,"type":"GroupLabel","priorities":[],"textColor":"#FFFFFF"}
+{"id":23459,"title":"Cygnix","color":"#691678","project_id":null,"created_at":"2019-11-20T17:02:20.569Z","updated_at":"2019-11-20T17:02:20.569Z","template":false,"description":null,"group_id":4352,"type":"GroupLabel","priorities":[],"textColor":"#FFFFFF"}
+{"id":23456,"title":"Genbalt","color":"#7f800c","project_id":null,"created_at":"2019-11-20T17:02:20.560Z","updated_at":"2019-11-20T17:02:20.560Z","template":false,"description":null,"group_id":4352,"type":"GroupLabel","priorities":[],"textColor":"#FFFFFF"}
+{"id":23457,"title":"NBP","color":"#e19356","project_id":null,"created_at":"2019-11-20T17:02:20.564Z","updated_at":"2019-11-20T17:02:20.564Z","template":false,"description":null,"group_id":4352,"type":"GroupLabel","priorities":[],"textColor":"#FFFFFF"}
+{"id":23455,"title":"Pionce","color":"#65c1b1","project_id":null,"created_at":"2019-11-20T17:02:20.555Z","updated_at":"2019-11-20T17:02:20.555Z","template":false,"description":null,"group_id":4352,"type":"GroupLabel","priorities":[],"textColor":"#FFFFFF"}
+{"id":23458,"title":"Pist","color":"#f62da4","project_id":null,"created_at":"2019-11-20T17:02:20.566Z","updated_at":"2019-11-20T17:02:20.566Z","template":false,"description":null,"group_id":4352,"type":"GroupLabel","priorities":[],"textColor":"#FFFFFF"}
+{"id":23454,"title":"Poffe","color":"#4f03bc","project_id":null,"created_at":"2019-11-20T17:02:20.552Z","updated_at":"2019-11-20T17:02:20.552Z","template":false,"description":null,"group_id":4352,"type":"GroupLabel","priorities":[],"textColor":"#FFFFFF"}
+{"id":23460,"title":"Poune","color":"#036637","project_id":null,"created_at":"2019-11-20T17:02:20.572Z","updated_at":"2019-11-20T17:02:20.572Z","template":false,"description":null,"group_id":4352,"type":"GroupLabel","priorities":[],"textColor":"#FFFFFF"}
diff --git a/spec/fixtures/lib/gitlab/import_export/group_exports/complex/tree/groups/4352/members.ndjson b/spec/fixtures/lib/gitlab/import_export/group_exports/complex/tree/groups/4352/members.ndjson
new file mode 100644
index 00000000000..8f9dd22804a
--- /dev/null
+++ b/spec/fixtures/lib/gitlab/import_export/group_exports/complex/tree/groups/4352/members.ndjson
@@ -0,0 +1,6 @@
+{"id":13771,"access_level":30,"source_id":4352,"source_type":"Namespace","user_id":1087,"notification_level":3,"created_at":"2019-11-20T17:04:36.968Z","updated_at":"2019-11-20T17:04:36.968Z","created_by_id":null,"invite_email":null,"invite_token":null,"invite_accepted_at":null,"requested_at":null,"expires_at":null,"ldap":false,"override":false,"user":{"id":1087,"email":"paige@blanda.info","username":"billi_auer"}}
+{"id":13770,"access_level":20,"source_id":4352,"source_type":"Namespace","user_id":171,"notification_level":3,"created_at":"2019-11-20T17:04:36.821Z","updated_at":"2019-11-20T17:04:36.821Z","created_by_id":null,"invite_email":null,"invite_token":null,"invite_accepted_at":null,"requested_at":null,"expires_at":null,"ldap":false,"override":false,"user":{"id":171,"email":"heidi@bosco.co.uk","username":"gerard.cruickshank"}}
+{"id":13769,"access_level":30,"source_id":4352,"source_type":"Namespace","user_id":1157,"notification_level":3,"created_at":"2019-11-20T17:04:36.606Z","updated_at":"2019-11-20T17:04:36.606Z","created_by_id":null,"invite_email":null,"invite_token":null,"invite_accepted_at":null,"requested_at":null,"expires_at":null,"ldap":false,"override":false,"user":{"id":1157,"email":"larisa.bruen@carroll.biz","username":"milagros.reynolds"}}
+{"id":13768,"access_level":40,"source_id":4352,"source_type":"Namespace","user_id":14,"notification_level":3,"created_at":"2019-11-20T17:04:36.465Z","updated_at":"2019-11-20T17:04:36.465Z","created_by_id":null,"invite_email":null,"invite_token":null,"invite_accepted_at":null,"requested_at":null,"expires_at":null,"ldap":false,"override":false,"user":{"id":14,"email":"madlyn_kovacek@wiza.ca","username":"monique.gusikowski"}}
+{"id":13767,"access_level":10,"source_id":4352,"source_type":"Namespace","user_id":1167,"notification_level":3,"created_at":"2019-11-20T17:04:36.324Z","updated_at":"2019-11-20T17:04:36.324Z","created_by_id":null,"invite_email":null,"invite_token":null,"invite_accepted_at":null,"requested_at":null,"expires_at":null,"ldap":false,"override":false,"user":{"id":1167,"email":"mirella@koepp.ca","username":"eileen"}}
+{"id":12921,"access_level":50,"source_id":4352,"source_type":"Namespace","user_id":1,"notification_level":3,"created_at":"2019-11-20T17:01:53.953Z","updated_at":"2019-11-20T17:01:53.953Z","created_by_id":null,"invite_email":null,"invite_token":null,"invite_accepted_at":null,"requested_at":null,"expires_at":null,"ldap":false,"override":false,"user":{"id":1,"email":"admin@example.com","username":"root"}}
diff --git a/spec/fixtures/lib/gitlab/import_export/group_exports/complex/tree/groups/4352/milestones.ndjson b/spec/fixtures/lib/gitlab/import_export/group_exports/complex/tree/groups/4352/milestones.ndjson
new file mode 100644
index 00000000000..35aa59dc54a
--- /dev/null
+++ b/spec/fixtures/lib/gitlab/import_export/group_exports/complex/tree/groups/4352/milestones.ndjson
@@ -0,0 +1,5 @@
+{"id":7647,"title":"v4.0","project_id":null,"description":"Magnam accusantium fuga quo dolorum.","due_date":null,"created_at":"2019-11-20T17:02:14.511Z","updated_at":"2019-11-20T17:02:14.511Z","state":"active","iid":5,"start_date":null,"group_id":4352}
+{"id":7646,"title":"v3.0","project_id":null,"description":"Quasi ut beatae quo vel.","due_date":null,"created_at":"2019-11-20T17:02:14.392Z","updated_at":"2019-11-20T17:02:14.392Z","state":"active","iid":4,"start_date":null,"group_id":4352}
+{"id":7645,"title":"v2.0","project_id":null,"description":"Voluptates et rerum maxime sint cum.","due_date":null,"created_at":"2019-11-20T17:02:14.380Z","updated_at":"2019-11-20T17:02:14.380Z","state":"closed","iid":3,"start_date":null,"group_id":4352}
+{"id":7644,"title":"v1.0","project_id":null,"description":"Qui dolores et facilis corporis dolores.","due_date":null,"created_at":"2019-11-20T17:02:14.364Z","updated_at":"2019-11-20T17:02:14.364Z","state":"active","iid":2,"start_date":null,"group_id":4352}
+{"id":7643,"title":"v0.0","project_id":null,"description":"Et dolor nam rerum culpa nisi doloremque ex.","due_date":null,"created_at":"2019-11-20T17:02:14.351Z","updated_at":"2019-11-20T17:02:14.351Z","state":"active","iid":1,"start_date":null,"group_id":4352}
diff --git a/spec/fixtures/lib/gitlab/import_export/group_exports/complex/tree/groups/4353.json b/spec/fixtures/lib/gitlab/import_export/group_exports/complex/tree/groups/4353.json
new file mode 100644
index 00000000000..d0dea3a9ca9
--- /dev/null
+++ b/spec/fixtures/lib/gitlab/import_export/group_exports/complex/tree/groups/4353.json
@@ -0,0 +1 @@
+{"name":"4n1db5ghlicx3ioddnwftxygq65nxb96dafkf89qp7p9sjqi3p","path":"4n1db5ghlicx3ioddnwftxygq65nxb96dafkf89qp7p9sjqi3p","owner_id":null,"created_at":"2019-11-20 17:01:54 UTC","updated_at":"2019-11-20 17:05:44 UTC","description":"","avatar":{"url":null},"membership_lock":false,"share_with_group_lock":false,"visibility_level":0,"request_access_enabled":true,"ldap_sync_status":"ready","ldap_sync_error":null,"ldap_sync_last_update_at":null,"ldap_sync_last_successful_update_at":null,"ldap_sync_last_sync_at":null,"lfs_enabled":null,"parent_id":4351,"shared_runners_minutes_limit":null,"repository_size_limit":null,"require_two_factor_authentication":false,"two_factor_grace_period":48,"plan_id":null,"project_creation_level":2,"trial_ends_on":null,"file_template_project_id":null,"saml_discovery_token":"m7cx4AZi","custom_project_templates_group_id":null,"auto_devops_enabled":null,"extra_shared_runners_minutes_limit":null,"last_ci_minutes_notification_at":null,"last_ci_minutes_usage_notification_level":null,"subgroup_creation_level":1,"emails_disabled":null,"max_pages_size":null,"max_artifacts_size":null,"id":4353} \ No newline at end of file
diff --git a/spec/fixtures/lib/gitlab/import_export/group_exports/complex/tree/groups/4353/badges.ndjson b/spec/fixtures/lib/gitlab/import_export/group_exports/complex/tree/groups/4353/badges.ndjson
new file mode 100644
index 00000000000..7e0db9da567
--- /dev/null
+++ b/spec/fixtures/lib/gitlab/import_export/group_exports/complex/tree/groups/4353/badges.ndjson
@@ -0,0 +1 @@
+{"id":11,"link_url":"https://localhost:3443/%{default_branch}","image_url":"https://badge_image.png","project_id":null,"group_id":4355,"created_at":"2019-11-20T17:28:11.883Z","updated_at":"2019-11-20T17:28:11.883Z","type":"GroupBadge"}
diff --git a/spec/fixtures/lib/gitlab/import_export/group_exports/complex/tree/groups/4353/boards.ndjson b/spec/fixtures/lib/gitlab/import_export/group_exports/complex/tree/groups/4353/boards.ndjson
new file mode 100644
index 00000000000..e58ab48806e
--- /dev/null
+++ b/spec/fixtures/lib/gitlab/import_export/group_exports/complex/tree/groups/4353/boards.ndjson
@@ -0,0 +1,2 @@
+{"id":58,"project_id":null,"created_at":"2019-11-20T17:28:15.616Z","updated_at":"2019-11-20T17:28:15.616Z","name":"Development","milestone_id":null,"group_id":4355,"weight":null,"labels":[]}
+{"id":59,"project_id":null,"created_at":"2019-11-20T17:28:25.289Z","updated_at":"2019-11-20T17:28:25.289Z","name":"Sub Board 1","milestone_id":null,"group_id":4355,"weight":null,"labels":[]}
diff --git a/spec/fixtures/lib/gitlab/import_export/group_exports/complex/tree/groups/4353/epics.ndjson b/spec/fixtures/lib/gitlab/import_export/group_exports/complex/tree/groups/4353/epics.ndjson
new file mode 100644
index 00000000000..0d0a676cf6f
--- /dev/null
+++ b/spec/fixtures/lib/gitlab/import_export/group_exports/complex/tree/groups/4353/epics.ndjson
@@ -0,0 +1,5 @@
+{"id":13642,"milestone_id":null,"group_id":4355,"author_id":1,"assignee_id":null,"iid":1,"updated_by_id":null,"last_edited_by_id":null,"lock_version":0,"start_date":null,"end_date":null,"last_edited_at":null,"created_at":"2019-11-20T17:02:10.151Z","updated_at":"2019-11-20T17:02:10.151Z","title":"Iste qui ratione dolores nisi vel dolor ea totam omnis aut.","description":"Voluptas dolore tenetur repudiandae repellendus maiores beatae quia et. Nisi mollitia exercitationem ut dolores tempore repellat similique. Nesciunt sit occaecati fugiat voluptates qui. Provident quod qui nulla atque dignissimos.\n\nAd veritatis nihil illum nisi est accusamus recusandae. Eos dolore autem ab corporis consectetur officiis ipsum. Consequatur non quis dolor rerum et hic consectetur dicta. Sed aut consectetur mollitia est.\n\nQuia sed dolore culpa error omnis quae quaerat. Magni quos quod illo tempore et eligendi enim. Autem reprehenderit esse vitae aut ipsum consectetur quis.","start_date_sourcing_milestone_id":null,"due_date_sourcing_milestone_id":null,"start_date_fixed":null,"due_date_fixed":null,"start_date_is_fixed":null,"due_date_is_fixed":null,"closed_by_id":null,"closed_at":null,"parent_id":null,"relative_position":null,"state_id":"opened","start_date_sourcing_epic_id":null,"due_date_sourcing_epic_id":null,"notes":[]}
+{"id":13643,"milestone_id":null,"group_id":4355,"author_id":1,"assignee_id":null,"iid":2,"updated_by_id":null,"last_edited_by_id":null,"lock_version":0,"start_date":null,"end_date":null,"last_edited_at":null,"created_at":"2019-11-20T17:02:10.166Z","updated_at":"2019-11-20T17:02:10.166Z","title":"Corporis placeat ut totam impedit ex qui debitis atque et provident.","description":"Quam aut in distinctio ut accusamus aliquam dolor sit. Aliquid quod corporis voluptas aliquam voluptate blanditiis distinctio dolore. Qui quis et qui non sunt deleniti iusto consequatur. Quasi quos omnis nobis et tenetur.\n\nCorrupti eius quod molestias et magnam laboriosam quia quis. Architecto aut eius est voluptas mollitia. Suscipit amet consequatur recusandae natus. Consectetur error quisquam est quas et qui.\n\nRerum earum fugit dolore sunt inventore. Vitae odit tempore autem adipisci voluptate esse placeat nobis. Debitis necessitatibus harum molestiae ex minima tempore consequuntur nihil.","start_date_sourcing_milestone_id":null,"due_date_sourcing_milestone_id":null,"start_date_fixed":null,"due_date_fixed":null,"start_date_is_fixed":null,"due_date_is_fixed":null,"closed_by_id":null,"closed_at":null,"parent_id":null,"relative_position":null,"state_id":"opened","start_date_sourcing_epic_id":null,"due_date_sourcing_epic_id":null,"notes":[]}
+{"id":13644,"milestone_id":null,"group_id":4355,"author_id":1,"assignee_id":null,"iid":3,"updated_by_id":null,"last_edited_by_id":null,"lock_version":0,"start_date":null,"end_date":null,"last_edited_at":null,"created_at":"2019-11-20T17:02:10.180Z","updated_at":"2019-11-20T17:02:10.180Z","title":"Voluptatem incidunt soluta fuga doloribus dolores nisi reiciendis impedit.","description":"Ipsa qui enim deleniti voluptas. Quasi nihil est blanditiis voluptas laudantium cum sequi consequatur. Id quo et atque error et possimus.\n\nUllam ea soluta ipsam sunt veritatis. Et incidunt natus consequatur repellat. Quam molestias magni consequatur soluta aut nobis. Maxime natus aperiam unde recusandae. A in dolorum facilis veniam est.\n\nEx repellendus tempore rem voluptatibus ad culpa consequatur. Consequatur quo quo dolore dicta nostrum necessitatibus tenetur. A voluptatem harum corporis qui quod molestiae culpa.","start_date_sourcing_milestone_id":null,"due_date_sourcing_milestone_id":null,"start_date_fixed":null,"due_date_fixed":null,"start_date_is_fixed":null,"due_date_is_fixed":null,"closed_by_id":null,"closed_at":null,"parent_id":null,"relative_position":null,"state_id":"opened","start_date_sourcing_epic_id":null,"due_date_sourcing_epic_id":null,"notes":[]}
+{"id":13645,"milestone_id":null,"group_id":4355,"author_id":1,"assignee_id":null,"iid":4,"updated_by_id":null,"last_edited_by_id":null,"lock_version":0,"start_date":null,"end_date":null,"last_edited_at":null,"created_at":"2019-11-20T17:02:10.194Z","updated_at":"2019-11-20T17:02:10.194Z","title":"Aut quo veniam soluta veritatis autem doloremque totam qui quia.","description":"Dolor itaque sunt perspiciatis quas natus et praesentium. A sit sapiente dolores ut et dolorum nihil omnis. Dolor quis dolores aut et perferendis.\n\nConsequatur molestiae laboriosam eum consequatur recusandae maxime deleniti commodi. Voluptas voluptatem eaque dicta animi aliquam rerum veritatis. Fugiat consequatur est sit et voluptatem.\n\nSequi tenetur itaque est vero eligendi quia laudantium et. Modi assumenda odio explicabo est non et. Voluptatem et enim minus sit at dicta est.","start_date_sourcing_milestone_id":null,"due_date_sourcing_milestone_id":null,"start_date_fixed":null,"due_date_fixed":null,"start_date_is_fixed":null,"due_date_is_fixed":null,"closed_by_id":null,"closed_at":null,"parent_id":null,"relative_position":null,"state_id":"opened","start_date_sourcing_epic_id":null,"due_date_sourcing_epic_id":null,"notes":[]}
+{"id":13646,"milestone_id":null,"group_id":4355,"author_id":1,"assignee_id":null,"iid":5,"updated_by_id":null,"last_edited_by_id":null,"lock_version":0,"start_date":null,"end_date":null,"last_edited_at":null,"created_at":"2019-11-20T17:02:10.208Z","updated_at":"2019-11-20T17:02:10.208Z","title":"Reprehenderit molestias incidunt non odio laudantium minima eum debitis ipsum.","description":"Quas velit omnis architecto quis eius. Vitae unde velit veniam dolor. Dolores facilis vel repellat et placeat ea rerum ratione. Rem fugit ab assumenda provident vel voluptas harum.\n\nQuia molestias similique illum delectus modi officiis. Aut modi sit ut qui. Est sequi corrupti laudantium ut optio eveniet ut. Corrupti quo provident natus aut omnis nam.\n\nVoluptas facilis repudiandae est quam. Mollitia fugit sint voluptatem aut. Quam quo eligendi id ad perferendis quis magnam. Corrupti sequi vel deleniti odit qui fugit.","start_date_sourcing_milestone_id":null,"due_date_sourcing_milestone_id":null,"start_date_fixed":null,"due_date_fixed":null,"start_date_is_fixed":null,"due_date_is_fixed":null,"closed_by_id":null,"closed_at":null,"parent_id":null,"relative_position":null,"state_id":"opened","start_date_sourcing_epic_id":null,"due_date_sourcing_epic_id":null,"notes":[]}
diff --git a/spec/fixtures/lib/gitlab/import_export/group_exports/complex/tree/groups/4353/labels.ndjson b/spec/fixtures/lib/gitlab/import_export/group_exports/complex/tree/groups/4353/labels.ndjson
new file mode 100644
index 00000000000..e5ee4a7f2ca
--- /dev/null
+++ b/spec/fixtures/lib/gitlab/import_export/group_exports/complex/tree/groups/4353/labels.ndjson
@@ -0,0 +1,9 @@
+{"id":23488,"title":"Brisync","color":"#66ac54","project_id":null,"created_at":"2019-11-20T17:02:20.654Z","updated_at":"2019-11-20T17:02:20.654Z","template":false,"description":null,"group_id":4355,"type":"GroupLabel","priorities":[],"textColor":"#FFFFFF"}
+{"id":23486,"title":"Casync","color":"#2f494d","project_id":null,"created_at":"2019-11-20T17:02:20.648Z","updated_at":"2019-11-20T17:02:20.648Z","template":false,"description":null,"group_id":4355,"type":"GroupLabel","priorities":[],"textColor":"#FFFFFF"}
+{"id":23485,"title":"Cygnix","color":"#691678","project_id":null,"created_at":"2019-11-20T17:02:20.646Z","updated_at":"2019-11-20T17:02:20.646Z","template":false,"description":null,"group_id":4355,"type":"GroupLabel","priorities":[],"textColor":"#FFFFFF"}
+{"id":23484,"title":"Pynce","color":"#117075","project_id":null,"created_at":"2019-11-20T17:02:20.643Z","updated_at":"2019-11-20T17:02:20.643Z","template":false,"description":null,"group_id":4355,"type":"GroupLabel","priorities":[],"textColor":"#FFFFFF"}
+{"id":23490,"title":"Pynswood","color":"#67314e","project_id":null,"created_at":"2019-11-20T17:02:20.663Z","updated_at":"2019-11-20T17:02:20.663Z","template":false,"description":null,"group_id":4355,"type":"GroupLabel","priorities":[],"textColor":"#FFFFFF"}
+{"id":23483,"title":"Triffe","color":"#3bf49a","project_id":null,"created_at":"2019-11-20T17:02:20.640Z","updated_at":"2019-11-20T17:02:20.640Z","template":false,"description":null,"group_id":4355,"type":"GroupLabel","priorities":[],"textColor":"#FFFFFF"}
+{"id":23489,"title":"Trintforge","color":"#cdab1a","project_id":null,"created_at":"2019-11-20T17:02:20.657Z","updated_at":"2019-11-20T17:02:20.657Z","template":false,"description":null,"group_id":4355,"type":"GroupLabel","priorities":[],"textColor":"#FFFFFF"}
+{"id":23482,"title":"Trouffeforge","color":"#db06cb","project_id":null,"created_at":"2019-11-20T17:02:20.637Z","updated_at":"2019-11-20T17:02:20.637Z","template":false,"description":null,"group_id":4355,"type":"GroupLabel","priorities":[],"textColor":"#FFFFFF"}
+{"id":23487,"title":"Tryre","color":"#d00c41","project_id":null,"created_at":"2019-11-20T17:02:20.651Z","updated_at":"2019-11-20T17:02:20.651Z","template":false,"description":null,"group_id":4355,"type":"GroupLabel","priorities":[],"textColor":"#FFFFFF"}
diff --git a/spec/fixtures/lib/gitlab/import_export/group_exports/complex/tree/groups/4353/members.ndjson b/spec/fixtures/lib/gitlab/import_export/group_exports/complex/tree/groups/4353/members.ndjson
new file mode 100644
index 00000000000..7a36a035c09
--- /dev/null
+++ b/spec/fixtures/lib/gitlab/import_export/group_exports/complex/tree/groups/4353/members.ndjson
@@ -0,0 +1,6 @@
+{"id":13786,"access_level":30,"source_id":4355,"source_type":"Namespace","user_id":1533,"notification_level":3,"created_at":"2019-11-20T17:04:39.405Z","updated_at":"2019-11-20T17:04:39.405Z","created_by_id":null,"invite_email":null,"invite_token":null,"invite_accepted_at":null,"requested_at":null,"expires_at":null,"ldap":false,"override":false,"user":{"id":1533,"email":"jose@cassin.ca","username":"buster"}}
+{"id":13785,"access_level":10,"source_id":4355,"source_type":"Namespace","user_id":1586,"notification_level":3,"created_at":"2019-11-20T17:04:39.269Z","updated_at":"2019-11-20T17:04:39.269Z","created_by_id":null,"invite_email":null,"invite_token":null,"invite_accepted_at":null,"requested_at":null,"expires_at":null,"ldap":false,"override":false,"user":{"id":1586,"email":"carie@gleichner.us","username":"dominque"}}
+{"id":13784,"access_level":30,"source_id":4355,"source_type":"Namespace","user_id":190,"notification_level":3,"created_at":"2019-11-20T17:04:39.127Z","updated_at":"2019-11-20T17:04:39.127Z","created_by_id":null,"invite_email":null,"invite_token":null,"invite_accepted_at":null,"requested_at":null,"expires_at":null,"ldap":false,"override":false,"user":{"id":190,"email":"delois@funk.biz","username":"kittie"}}
+{"id":13783,"access_level":20,"source_id":4355,"source_type":"Namespace","user_id":254,"notification_level":3,"created_at":"2019-11-20T17:04:38.971Z","updated_at":"2019-11-20T17:04:38.971Z","created_by_id":null,"invite_email":null,"invite_token":null,"invite_accepted_at":null,"requested_at":null,"expires_at":null,"ldap":false,"override":false,"user":{"id":254,"email":"tyra.lowe@whitemckenzie.co.uk","username":"kassie"}}
+{"id":13782,"access_level":40,"source_id":4355,"source_type":"Namespace","user_id":503,"notification_level":3,"created_at":"2019-11-20T17:04:38.743Z","updated_at":"2019-11-20T17:04:38.743Z","created_by_id":null,"invite_email":null,"invite_token":null,"invite_accepted_at":null,"requested_at":null,"expires_at":null,"ldap":false,"override":false,"user":{"id":503,"email":"tyesha.brakus@bruen.ca","username":"charise"}}
+{"id":12924,"access_level":50,"source_id":4355,"source_type":"Namespace","user_id":1,"notification_level":3,"created_at":"2019-11-20T17:01:54.145Z","updated_at":"2019-11-20T17:01:54.145Z","created_by_id":null,"invite_email":null,"invite_token":null,"invite_accepted_at":null,"requested_at":null,"expires_at":null,"ldap":false,"override":false,"user":{"id":1,"email":"admin@example.com","username":"root"}}
diff --git a/spec/fixtures/lib/gitlab/import_export/group_exports/complex/tree/groups/4353/milestones.ndjson b/spec/fixtures/lib/gitlab/import_export/group_exports/complex/tree/groups/4353/milestones.ndjson
new file mode 100644
index 00000000000..cad8ff88d43
--- /dev/null
+++ b/spec/fixtures/lib/gitlab/import_export/group_exports/complex/tree/groups/4353/milestones.ndjson
@@ -0,0 +1,5 @@
+{"id":7662,"title":"v4.0","project_id":null,"description":"Consequatur quaerat aut voluptas repudiandae.","due_date":null,"created_at":"2019-11-20T17:02:14.746Z","updated_at":"2019-11-20T17:02:14.746Z","state":"active","iid":5,"start_date":null,"group_id":4355}
+{"id":7661,"title":"v3.0","project_id":null,"description":"In cupiditate aspernatur non ipsa enim consequatur.","due_date":null,"created_at":"2019-11-20T17:02:14.731Z","updated_at":"2019-11-20T17:02:14.731Z","state":"active","iid":4,"start_date":null,"group_id":4355}
+{"id":7660,"title":"v2.0","project_id":null,"description":"Dolor non rem omnis atque.","due_date":null,"created_at":"2019-11-20T17:02:14.716Z","updated_at":"2019-11-20T17:02:14.716Z","state":"closed","iid":3,"start_date":null,"group_id":4355}
+{"id":7659,"title":"v1.0","project_id":null,"description":"Nihil consectetur et quibusdam esse quae.","due_date":null,"created_at":"2019-11-20T17:02:14.701Z","updated_at":"2019-11-20T17:02:14.701Z","state":"closed","iid":2,"start_date":null,"group_id":4355}
+{"id":7658,"title":"v0.0","project_id":null,"description":"Suscipit dolor id magnam reprehenderit.","due_date":null,"created_at":"2019-11-20T17:02:14.686Z","updated_at":"2019-11-20T17:02:14.686Z","state":"active","iid":1,"start_date":null,"group_id":4355}
diff --git a/spec/fixtures/lib/gitlab/import_export/group_exports/complex/tree/groups/_all.ndjson b/spec/fixtures/lib/gitlab/import_export/group_exports/complex/tree/groups/_all.ndjson
new file mode 100644
index 00000000000..88195502464
--- /dev/null
+++ b/spec/fixtures/lib/gitlab/import_export/group_exports/complex/tree/groups/_all.ndjson
@@ -0,0 +1,3 @@
+4351
+4352
+4353
diff --git a/spec/fixtures/lib/gitlab/import_export/group_exports/no_children/tree/groups/4353.json b/spec/fixtures/lib/gitlab/import_export/group_exports/no_children/tree/groups/4353.json
new file mode 100644
index 00000000000..26368307160
--- /dev/null
+++ b/spec/fixtures/lib/gitlab/import_export/group_exports/no_children/tree/groups/4353.json
@@ -0,0 +1,41 @@
+{
+ "id": 4353,
+ "name": "group",
+ "path": "group",
+ "owner_id": null,
+ "created_at": "2019-11-20 17:01:53 UTC",
+ "updated_at": "2019-11-20 17:05:44 UTC",
+ "description": "Group Description",
+ "avatar": {
+ "url": null
+ },
+ "membership_lock": false,
+ "share_with_group_lock": false,
+ "visibility_level": 0,
+ "request_access_enabled": true,
+ "ldap_sync_status": "ready",
+ "ldap_sync_error": null,
+ "ldap_sync_last_update_at": null,
+ "ldap_sync_last_successful_update_at": null,
+ "ldap_sync_last_sync_at": null,
+ "lfs_enabled": null,
+ "parent_id": null,
+ "shared_runners_minutes_limit": null,
+ "repository_size_limit": null,
+ "require_two_factor_authentication": false,
+ "two_factor_grace_period": 48,
+ "plan_id": null,
+ "project_creation_level": 2,
+ "trial_ends_on": null,
+ "file_template_project_id": null,
+ "saml_discovery_token": "rBKx3ioz",
+ "custom_project_templates_group_id": null,
+ "auto_devops_enabled": null,
+ "extra_shared_runners_minutes_limit": null,
+ "last_ci_minutes_notification_at": null,
+ "last_ci_minutes_usage_notification_level": null,
+ "subgroup_creation_level": 1,
+ "emails_disabled": null,
+ "max_pages_size": null,
+ "max_artifacts_size": null
+}
diff --git a/spec/fixtures/lib/gitlab/import_export/group_exports/no_children/tree/groups/4353/badges.ndjson b/spec/fixtures/lib/gitlab/import_export/group_exports/no_children/tree/groups/4353/badges.ndjson
new file mode 100644
index 00000000000..3c04897594e
--- /dev/null
+++ b/spec/fixtures/lib/gitlab/import_export/group_exports/no_children/tree/groups/4353/badges.ndjson
@@ -0,0 +1 @@
+{"id":10,"link_url":"https://localhost:3443/%{default_branch}","image_url":"https://badge_image.png","project_id":null,"group_id":4351,"created_at":"2019-11-20T17:27:02.047Z","updated_at":"2019-11-20T17:27:02.047Z","type":"GroupBadge"}
diff --git a/spec/fixtures/lib/gitlab/import_export/group_exports/no_children/tree/groups/4353/boards.ndjson b/spec/fixtures/lib/gitlab/import_export/group_exports/no_children/tree/groups/4353/boards.ndjson
new file mode 100644
index 00000000000..c3581b5c375
--- /dev/null
+++ b/spec/fixtures/lib/gitlab/import_export/group_exports/no_children/tree/groups/4353/boards.ndjson
@@ -0,0 +1,2 @@
+{"id":56,"project_id":null,"created_at":"2019-11-20T17:27:16.808Z","updated_at":"2019-11-20T17:27:16.808Z","name":"Development","milestone_id":null,"group_id":4351,"weight":null,"labels":[]}
+{"id":57,"project_id":null,"created_at":"2019-11-20T17:27:41.118Z","updated_at":"2019-11-20T17:27:41.118Z","name":"Board!","milestone_id":7638,"group_id":4351,"weight":null,"labels":[]}
diff --git a/spec/fixtures/lib/gitlab/import_export/group_exports/no_children/tree/groups/4353/epics.ndjson b/spec/fixtures/lib/gitlab/import_export/group_exports/no_children/tree/groups/4353/epics.ndjson
new file mode 100644
index 00000000000..39ac8bbd06c
--- /dev/null
+++ b/spec/fixtures/lib/gitlab/import_export/group_exports/no_children/tree/groups/4353/epics.ndjson
@@ -0,0 +1,5 @@
+{"id":13622,"milestone_id":null,"group_id":4351,"author_id":1,"assignee_id":null,"iid":1,"updated_by_id":null,"last_edited_by_id":null,"lock_version":0,"start_date":null,"end_date":null,"last_edited_at":null,"created_at":"2019-11-20T17:02:09.754Z","updated_at":"2019-11-20T18:38:40.054Z","title":"Provident neque consequatur numquam ad laboriosam voluptatem magnam.","description":"Fugit nisi est ut numquam quia rerum vitae qui. Et in est aliquid voluptas et ut vitae. In distinctio voluptates ut deleniti iste.\n\nReiciendis eum sunt vero blanditiis at quia. Voluptate eum facilis illum ea distinctio maiores. Doloribus aut nemo ea distinctio.\n\nNihil cum distinctio voluptates quam. Laboriosam distinctio ea accusantium soluta perspiciatis nesciunt impedit. Id qui natus quis minima voluptatum velit ut reprehenderit. Molestiae quia est harum sapiente rem error architecto id. Et minus ipsa et ut ut.","start_date_sourcing_milestone_id":null,"due_date_sourcing_milestone_id":null,"start_date_fixed":null,"due_date_fixed":null,"start_date_is_fixed":null,"due_date_is_fixed":null,"closed_by_id":null,"closed_at":null,"parent_id":null,"relative_position":null,"state_id":"opened","start_date_sourcing_epic_id":null,"due_date_sourcing_epic_id":null,"notes":[{"id":44170,"note":"added epic &5 as child epic","noteable_type":"Epic","author_id":1,"created_at":"2019-11-20T18:38:40.031Z","updated_at":"2019-11-20T18:38:40.035Z","project_id":null,"attachment":{"url":null},"line_code":null,"commit_id":null,"noteable_id":13622,"system":true,"st_diff":null,"updated_by_id":null,"position":null,"original_position":null,"resolved_at":null,"resolved_by_id":null,"discussion_id":"ba005d8dd59cd37a4f32406d46e759b08fd15510","change_position":null,"resolved_by_push":null,"review_id":null,"type":null,"author":{"name":"Administrator"}},{"id":44168,"note":"added epic &4 as child epic","noteable_type":"Epic","author_id":1,"created_at":"2019-11-20T18:38:35.669Z","updated_at":"2019-11-20T18:38:35.673Z","project_id":null,"attachment":{"url":null},"line_code":null,"commit_id":null,"noteable_id":13622,"system":true,"st_diff":null,"updated_by_id":null,"position":null,"original_position":null,"resolved_at":null,"resolved_by_id":null,"discussion_id":"9b49d3b017aadc1876d477b960e6f8efb99ce29f","change_position":null,"resolved_by_push":null,"review_id":null,"type":null,"author":{"name":"Administrator"}},{"id":44166,"note":"added epic &3 as child epic","noteable_type":"Epic","author_id":1,"created_at":"2019-11-20T18:38:30.944Z","updated_at":"2019-11-20T18:38:30.948Z","project_id":null,"attachment":{"url":null},"line_code":null,"commit_id":null,"noteable_id":13622,"system":true,"st_diff":null,"updated_by_id":null,"position":null,"original_position":null,"resolved_at":null,"resolved_by_id":null,"discussion_id":"cccfe967f48e699a466c87a55a9f8acb00fec1a1","change_position":null,"resolved_by_push":null,"review_id":null,"type":null,"author":{"name":"Administrator"}},{"id":44164,"note":"added epic &2 as child epic","noteable_type":"Epic","author_id":1,"created_at":"2019-11-20T18:38:26.689Z","updated_at":"2019-11-20T18:38:26.724Z","project_id":null,"attachment":{"url":null},"line_code":null,"commit_id":null,"noteable_id":13622,"system":true,"st_diff":null,"updated_by_id":null,"position":null,"original_position":null,"resolved_at":null,"resolved_by_id":null,"discussion_id":"133f0c3001860fa8d2031e398a65db74477378c4","change_position":null,"resolved_by_push":null,"review_id":null,"type":null,"author":{"name":"Administrator"}}]}
+{"id":13623,"milestone_id":null,"group_id":4351,"author_id":1,"assignee_id":null,"iid":2,"updated_by_id":null,"last_edited_by_id":null,"lock_version":0,"start_date":null,"end_date":null,"last_edited_at":null,"created_at":"2019-11-20T17:02:09.769Z","updated_at":"2019-11-20T18:38:26.851Z","title":"Omnis accusantium commodi voluptas odio illo eum ut.","description":"Eius vero et iste amet est voluptatem modi. Doloribus ipsam beatae et ut autem ut animi. Dolor culpa dolor omnis delectus est tempora inventore ab. Optio labore tenetur libero quia provident et quis. Blanditiis architecto sint possimus cum aut adipisci.\n\nDolores quisquam sunt cupiditate unde qui vitae nemo. Odio quas omnis ut nobis. Possimus fugit deserunt quia sed ab numquam veritatis nihil.\n\nQui nemo adipisci magnam perferendis voluptatem modi. Eius enim iure dolores consequuntur eum nobis adipisci. Consequatur architecto et quas deleniti hic id laborum officiis. Enim perferendis quis quasi totam delectus rerum deleniti.","start_date_sourcing_milestone_id":null,"due_date_sourcing_milestone_id":null,"start_date_fixed":null,"due_date_fixed":null,"start_date_is_fixed":null,"due_date_is_fixed":null,"closed_by_id":null,"closed_at":null,"parent_id":13622,"relative_position":1073741323,"state_id":"opened","start_date_sourcing_epic_id":null,"due_date_sourcing_epic_id":null,"parent":{"id":13622,"milestone_id":null,"group_id":4351,"author_id":1,"assignee_id":null,"iid":1,"updated_by_id":null,"last_edited_by_id":null,"lock_version":0,"start_date":null,"end_date":null,"last_edited_at":null,"created_at":"2019-11-20T17:02:09.754Z","updated_at":"2019-11-20T18:38:40.054Z","title":"Provident neque consequatur numquam ad laboriosam voluptatem magnam.","description":"Fugit nisi est ut numquam quia rerum vitae qui. Et in est aliquid voluptas et ut vitae. In distinctio voluptates ut deleniti iste.\n\nReiciendis eum sunt vero blanditiis at quia. Voluptate eum facilis illum ea distinctio maiores. Doloribus aut nemo ea distinctio.\n\nNihil cum distinctio voluptates quam. Laboriosam distinctio ea accusantium soluta perspiciatis nesciunt impedit. Id qui natus quis minima voluptatum velit ut reprehenderit. Molestiae quia est harum sapiente rem error architecto id. Et minus ipsa et ut ut.","start_date_sourcing_milestone_id":null,"due_date_sourcing_milestone_id":null,"start_date_fixed":null,"due_date_fixed":null,"start_date_is_fixed":null,"due_date_is_fixed":null,"closed_by_id":null,"closed_at":null,"parent_id":null,"relative_position":null,"state_id":"opened","start_date_sourcing_epic_id":null,"due_date_sourcing_epic_id":null},"notes":[{"id":44165,"note":"added epic &1 as parent epic","noteable_type":"Epic","author_id":1,"created_at":"2019-11-20T18:38:26.822Z","updated_at":"2019-11-20T18:38:26.826Z","project_id":null,"attachment":{"url":null},"line_code":null,"commit_id":null,"noteable_id":13623,"system":true,"st_diff":null,"updated_by_id":null,"position":null,"original_position":null,"resolved_at":null,"resolved_by_id":null,"discussion_id":"15f0a7f4ed16a07bc78841e122524bb867edcf86","change_position":null,"resolved_by_push":null,"review_id":null,"type":null,"author":{"name":"Administrator"}}]}
+{"id":13624,"milestone_id":null,"group_id":4351,"author_id":1,"assignee_id":null,"iid":3,"updated_by_id":null,"last_edited_by_id":null,"lock_version":0,"start_date":null,"end_date":null,"last_edited_at":null,"created_at":"2019-11-20T17:02:09.783Z","updated_at":"2019-11-20T18:38:31.018Z","title":"Quis dolore velit possimus eaque aut amet esse voluptate aliquam.","description":"Ab veritatis reprehenderit nulla laboriosam et sed asperiores corporis. Est accusantium maxime perferendis et. Omnis a qui voluptates non excepturi.\n\nAdipisci labore maiores dicta sed magnam aut. Veritatis delectus dolorum qui id. Dolorum tenetur quo iure amet. Eveniet reprehenderit dolor ipsam quia ratione quo. Facilis voluptatem vel repellat id illum.\n\nAut et magnam aut minus aspernatur. Fuga quo necessitatibus mollitia maxime quasi. Qui aspernatur quia accusamus est quod. Qui assumenda veritatis dolor non eveniet quibusdam quos qui.","start_date_sourcing_milestone_id":null,"due_date_sourcing_milestone_id":null,"start_date_fixed":null,"due_date_fixed":null,"start_date_is_fixed":null,"due_date_is_fixed":null,"closed_by_id":null,"closed_at":null,"parent_id":13622,"relative_position":1073740823,"state_id":"opened","start_date_sourcing_epic_id":null,"due_date_sourcing_epic_id":null,"parent":{"id":13622,"milestone_id":null,"group_id":4351,"author_id":1,"assignee_id":null,"iid":1,"updated_by_id":null,"last_edited_by_id":null,"lock_version":0,"start_date":null,"end_date":null,"last_edited_at":null,"created_at":"2019-11-20T17:02:09.754Z","updated_at":"2019-11-20T18:38:40.054Z","title":"Provident neque consequatur numquam ad laboriosam voluptatem magnam.","description":"Fugit nisi est ut numquam quia rerum vitae qui. Et in est aliquid voluptas et ut vitae. In distinctio voluptates ut deleniti iste.\n\nReiciendis eum sunt vero blanditiis at quia. Voluptate eum facilis illum ea distinctio maiores. Doloribus aut nemo ea distinctio.\n\nNihil cum distinctio voluptates quam. Laboriosam distinctio ea accusantium soluta perspiciatis nesciunt impedit. Id qui natus quis minima voluptatum velit ut reprehenderit. Molestiae quia est harum sapiente rem error architecto id. Et minus ipsa et ut ut.","start_date_sourcing_milestone_id":null,"due_date_sourcing_milestone_id":null,"start_date_fixed":null,"due_date_fixed":null,"start_date_is_fixed":null,"due_date_is_fixed":null,"closed_by_id":null,"closed_at":null,"parent_id":null,"relative_position":null,"state_id":"opened","start_date_sourcing_epic_id":null,"due_date_sourcing_epic_id":null},"notes":[{"id":44167,"note":"added epic &1 as parent epic","noteable_type":"Epic","author_id":1,"created_at":"2019-11-20T18:38:30.989Z","updated_at":"2019-11-20T18:38:30.993Z","project_id":null,"attachment":{"url":null},"line_code":null,"commit_id":null,"noteable_id":13624,"system":true,"st_diff":null,"updated_by_id":null,"position":null,"original_position":null,"resolved_at":null,"resolved_by_id":null,"discussion_id":"423ffec14a3ce148c11a802eb1f2613fa8ca9a94","change_position":null,"resolved_by_push":null,"review_id":null,"type":null,"author":{"name":"Administrator"}}]}
+{"id":13625,"milestone_id":null,"group_id":4351,"author_id":1,"assignee_id":null,"iid":4,"updated_by_id":null,"last_edited_by_id":null,"lock_version":0,"start_date":null,"end_date":null,"last_edited_at":null,"created_at":"2019-11-20T17:02:09.798Z","updated_at":"2019-11-20T18:38:35.765Z","title":"Possimus et ut iste temporibus earum cupiditate voluptatem esse assumenda amet.","description":"Et at corporis sed id rerum ullam dolore. Odio magnam corporis excepturi neque est. Est accusamus nostrum qui rerum.\n\nEt aut dolores eaque quibusdam aut quas explicabo id. Est necessitatibus praesentium omnis et vero laboriosam et. Sunt in saepe qui laudantium et voluptas.\n\nVelit sunt odit eum omnis beatae eius aut. Dolores commodi qui impedit deleniti et magnam pariatur. Aut odit amet ipsum ea atque. Itaque est ut sunt ullam eum nam.","start_date_sourcing_milestone_id":null,"due_date_sourcing_milestone_id":null,"start_date_fixed":null,"due_date_fixed":null,"start_date_is_fixed":null,"due_date_is_fixed":null,"closed_by_id":null,"closed_at":null,"parent_id":13622,"relative_position":1073740323,"state_id":"opened","start_date_sourcing_epic_id":null,"due_date_sourcing_epic_id":null,"parent":{"id":13622,"milestone_id":null,"group_id":4351,"author_id":1,"assignee_id":null,"iid":1,"updated_by_id":null,"last_edited_by_id":null,"lock_version":0,"start_date":null,"end_date":null,"last_edited_at":null,"created_at":"2019-11-20T17:02:09.754Z","updated_at":"2019-11-20T18:38:40.054Z","title":"Provident neque consequatur numquam ad laboriosam voluptatem magnam.","description":"Fugit nisi est ut numquam quia rerum vitae qui. Et in est aliquid voluptas et ut vitae. In distinctio voluptates ut deleniti iste.\n\nReiciendis eum sunt vero blanditiis at quia. Voluptate eum facilis illum ea distinctio maiores. Doloribus aut nemo ea distinctio.\n\nNihil cum distinctio voluptates quam. Laboriosam distinctio ea accusantium soluta perspiciatis nesciunt impedit. Id qui natus quis minima voluptatum velit ut reprehenderit. Molestiae quia est harum sapiente rem error architecto id. Et minus ipsa et ut ut.","start_date_sourcing_milestone_id":null,"due_date_sourcing_milestone_id":null,"start_date_fixed":null,"due_date_fixed":null,"start_date_is_fixed":null,"due_date_is_fixed":null,"closed_by_id":null,"closed_at":null,"parent_id":null,"relative_position":null,"state_id":"opened","start_date_sourcing_epic_id":null,"due_date_sourcing_epic_id":null},"notes":[{"id":44169,"note":"added epic &1 as parent epic","noteable_type":"Epic","author_id":1,"created_at":"2019-11-20T18:38:35.737Z","updated_at":"2019-11-20T18:38:35.741Z","project_id":null,"attachment":{"url":null},"line_code":null,"commit_id":null,"noteable_id":13625,"system":true,"st_diff":null,"updated_by_id":null,"position":null,"original_position":null,"resolved_at":null,"resolved_by_id":null,"discussion_id":"5bc3e30d508affafc61de2b4e1d9f21039505cc3","change_position":null,"resolved_by_push":null,"review_id":null,"type":null,"author":{"name":"Administrator"}}]}
+{"id":13626,"milestone_id":null,"group_id":4351,"author_id":1,"assignee_id":null,"iid":5,"updated_by_id":null,"last_edited_by_id":null,"lock_version":0,"start_date":null,"end_date":null,"last_edited_at":null,"created_at":"2019-11-20T17:02:09.812Z","updated_at":"2019-11-20T18:38:40.101Z","title":"Ab deleniti ipsum voluptatem dolor qui quos saepe repellat quo.","description":"Sunt minus sunt reiciendis culpa sed excepturi. Aperiam sed quod nemo nesciunt et quia molestias incidunt. Ipsum nam magnam labore eos a molestiae rerum possimus. Sequi autem asperiores voluptas assumenda.\n\nRerum ipsa quia cum ab corrupti omnis. Velit libero et nihil ipsa aut quo rem ipsam. Architecto omnis distinctio sed doloribus perspiciatis consequatur aut et. Fugit consequuntur est minima reiciendis reprehenderit et.\n\nConsequatur distinctio et ut blanditiis perferendis officiis inventore. Alias aut voluptatem in facere. Ut perferendis dolorum hic dolores. Ipsa dolorem soluta at mollitia. Placeat et ea numquam dicta molestias.","start_date_sourcing_milestone_id":null,"due_date_sourcing_milestone_id":null,"start_date_fixed":null,"due_date_fixed":null,"start_date_is_fixed":null,"due_date_is_fixed":null,"closed_by_id":null,"closed_at":null,"parent_id":13622,"relative_position":1073739823,"state_id":"opened","start_date_sourcing_epic_id":null,"due_date_sourcing_epic_id":null,"parent":{"id":13622,"milestone_id":null,"group_id":4351,"author_id":1,"assignee_id":null,"iid":1,"updated_by_id":null,"last_edited_by_id":null,"lock_version":0,"start_date":null,"end_date":null,"last_edited_at":null,"created_at":"2019-11-20T17:02:09.754Z","updated_at":"2019-11-20T18:38:40.054Z","title":"Provident neque consequatur numquam ad laboriosam voluptatem magnam.","description":"Fugit nisi est ut numquam quia rerum vitae qui. Et in est aliquid voluptas et ut vitae. In distinctio voluptates ut deleniti iste.\n\nReiciendis eum sunt vero blanditiis at quia. Voluptate eum facilis illum ea distinctio maiores. Doloribus aut nemo ea distinctio.\n\nNihil cum distinctio voluptates quam. Laboriosam distinctio ea accusantium soluta perspiciatis nesciunt impedit. Id qui natus quis minima voluptatum velit ut reprehenderit. Molestiae quia est harum sapiente rem error architecto id. Et minus ipsa et ut ut.","start_date_sourcing_milestone_id":null,"due_date_sourcing_milestone_id":null,"start_date_fixed":null,"due_date_fixed":null,"start_date_is_fixed":null,"due_date_is_fixed":null,"closed_by_id":null,"closed_at":null,"parent_id":null,"relative_position":null,"state_id":"opened","start_date_sourcing_epic_id":null,"due_date_sourcing_epic_id":null},"notes":[{"id":44171,"note":"added epic &1 as parent epic","noteable_type":"Epic","author_id":1,"created_at":"2019-11-20T18:38:40.074Z","updated_at":"2019-11-20T18:38:40.077Z","project_id":null,"attachment":{"url":null},"line_code":null,"commit_id":null,"noteable_id":13626,"system":true,"st_diff":null,"updated_by_id":null,"position":null,"original_position":null,"resolved_at":null,"resolved_by_id":null,"discussion_id":"a6231acdaef5f4d2e569dfb604f1baf85c49e1a0","change_position":null,"resolved_by_push":null,"review_id":null,"type":null,"author":{"name":"Administrator"}}]}
diff --git a/spec/fixtures/lib/gitlab/import_export/group_exports/no_children/tree/groups/4353/labels.ndjson b/spec/fixtures/lib/gitlab/import_export/group_exports/no_children/tree/groups/4353/labels.ndjson
new file mode 100644
index 00000000000..8508c4347c2
--- /dev/null
+++ b/spec/fixtures/lib/gitlab/import_export/group_exports/no_children/tree/groups/4353/labels.ndjson
@@ -0,0 +1,10 @@
+{"id":23452,"title":"Bruffefunc","color":"#1d2da4","project_id":null,"created_at":"2019-11-20T17:02:20.546Z","updated_at":"2019-11-20T17:02:20.546Z","template":false,"description":null,"group_id":4351,"type":"GroupLabel","priorities":[],"textColor":"#FFFFFF"}
+{"id":23446,"title":"Cafunc","color":"#73ed5b","project_id":null,"created_at":"2019-11-20T17:02:20.526Z","updated_at":"2019-11-20T17:02:20.526Z","template":false,"description":null,"group_id":4351,"type":"GroupLabel","priorities":[],"textColor":"#FFFFFF"}
+{"id":23451,"title":"Casche","color":"#649a75","project_id":null,"created_at":"2019-11-20T17:02:20.544Z","updated_at":"2019-11-20T17:02:20.544Z","template":false,"description":null,"group_id":4351,"type":"GroupLabel","priorities":[],"textColor":"#FFFFFF"}
+{"id":23444,"title":"Cocell","color":"#1b365c","project_id":null,"created_at":"2019-11-20T17:02:20.521Z","updated_at":"2019-11-20T17:02:20.521Z","template":false,"description":null,"group_id":4351,"type":"GroupLabel","priorities":[],"textColor":"#FFFFFF"}
+{"id":23449,"title":"Packfunc","color":"#e33bba","project_id":null,"created_at":"2019-11-20T17:02:20.537Z","updated_at":"2019-11-20T17:02:20.537Z","template":false,"description":null,"group_id":4351,"type":"GroupLabel","priorities":[],"textColor":"#FFFFFF"}
+{"id":23443,"title":"Panabalt","color":"#84f708","project_id":null,"created_at":"2019-11-20T17:02:20.518Z","updated_at":"2019-11-20T17:02:20.518Z","template":false,"description":null,"group_id":4351,"type":"GroupLabel","priorities":[],"textColor":"#FFFFFF"}
+{"id":23447,"title":"Phierefunc","color":"#4ab4a8","project_id":null,"created_at":"2019-11-20T17:02:20.530Z","updated_at":"2019-11-20T17:02:20.530Z","template":false,"description":null,"group_id":4351,"type":"GroupLabel","priorities":[],"textColor":"#FFFFFF"}
+{"id":23445,"title":"Pons","color":"#47f440","project_id":null,"created_at":"2019-11-20T17:02:20.523Z","updated_at":"2019-11-20T17:02:20.523Z","template":false,"description":null,"group_id":4351,"type":"GroupLabel","priorities":[],"textColor":"#FFFFFF"}
+{"id":23448,"title":"Sosync","color":"#110320","project_id":null,"created_at":"2019-11-20T17:02:20.532Z","updated_at":"2019-11-20T17:02:20.532Z","template":false,"description":null,"group_id":4351,"type":"GroupLabel","priorities":[],"textColor":"#FFFFFF"}
+{"id":23450,"title":"TSL","color":"#58796f","project_id":null,"created_at":"2019-11-20T17:02:20.541Z","updated_at":"2019-11-20T17:02:20.541Z","template":false,"description":null,"group_id":4351,"type":"GroupLabel","priorities":[],"textColor":"#FFFFFF"}
diff --git a/spec/fixtures/lib/gitlab/import_export/group_exports/no_children/tree/groups/4353/members.ndjson b/spec/fixtures/lib/gitlab/import_export/group_exports/no_children/tree/groups/4353/members.ndjson
new file mode 100644
index 00000000000..740c724ac5d
--- /dev/null
+++ b/spec/fixtures/lib/gitlab/import_export/group_exports/no_children/tree/groups/4353/members.ndjson
@@ -0,0 +1,6 @@
+{"id":13766,"access_level":30,"source_id":4351,"source_type":"Namespace","user_id":42,"notification_level":3,"created_at":"2019-11-20T17:04:36.184Z","updated_at":"2019-11-20T17:04:36.184Z","created_by_id":null,"invite_email":null,"invite_token":null,"invite_accepted_at":null,"requested_at":null,"expires_at":null,"ldap":false,"override":false,"user":{"id":42,"email":"moriah@collinsmurphy.com","username":"reported_user_15"}}
+{"id":13765,"access_level":40,"source_id":4351,"source_type":"Namespace","user_id":271,"notification_level":3,"created_at":"2019-11-20T17:04:36.044Z","updated_at":"2019-11-20T17:04:36.044Z","created_by_id":null,"invite_email":null,"invite_token":null,"invite_accepted_at":null,"requested_at":null,"expires_at":null,"ldap":false,"override":false,"user":{"id":271,"email":"garret@connellystark.ca","username":"charlesetta"}}
+{"id":13764,"access_level":30,"source_id":4351,"source_type":"Namespace","user_id":206,"notification_level":3,"created_at":"2019-11-20T17:04:35.840Z","updated_at":"2019-11-20T17:04:35.840Z","created_by_id":null,"invite_email":null,"invite_token":null,"invite_accepted_at":null,"requested_at":null,"expires_at":null,"ldap":false,"override":false,"user":{"id":206,"email":"margaret.bergnaum@reynolds.us","username":"gwendolyn_robel"}}
+{"id":13763,"access_level":10,"source_id":4351,"source_type":"Namespace","user_id":39,"notification_level":3,"created_at":"2019-11-20T17:04:35.704Z","updated_at":"2019-11-20T17:04:35.704Z","created_by_id":null,"invite_email":null,"invite_token":null,"invite_accepted_at":null,"requested_at":null,"expires_at":null,"ldap":false,"override":false,"user":{"id":39,"email":"alexis_berge@kerlukeklein.us","username":"reported_user_12"}}
+{"id":13762,"access_level":20,"source_id":4351,"source_type":"Namespace","user_id":1624,"notification_level":3,"created_at":"2019-11-20T17:04:35.566Z","updated_at":"2019-11-20T17:04:35.566Z","created_by_id":null,"invite_email":null,"invite_token":null,"invite_accepted_at":null,"requested_at":null,"expires_at":null,"ldap":false,"override":false,"user":{"id":1624,"email":"nakesha.herzog@powlowski.com","username":"adriene.mcclure"}}
+{"id":12920,"access_level":50,"source_id":4351,"source_type":"Namespace","user_id":1,"notification_level":3,"created_at":"2019-11-20T17:01:53.505Z","updated_at":"2019-11-20T17:01:53.505Z","created_by_id":null,"invite_email":null,"invite_token":null,"invite_accepted_at":null,"requested_at":null,"expires_at":null,"ldap":false,"override":false,"user":{"id":1,"email":"admin@example.com","username":"root"}}
diff --git a/spec/fixtures/lib/gitlab/import_export/group_exports/no_children/tree/groups/4353/milestones.ndjson b/spec/fixtures/lib/gitlab/import_export/group_exports/no_children/tree/groups/4353/milestones.ndjson
new file mode 100644
index 00000000000..40523f276e7
--- /dev/null
+++ b/spec/fixtures/lib/gitlab/import_export/group_exports/no_children/tree/groups/4353/milestones.ndjson
@@ -0,0 +1,5 @@
+{"id":7642,"title":"v4.0","project_id":null,"description":"Et laudantium enim omnis ea reprehenderit iure.","due_date":null,"created_at":"2019-11-20T17:02:14.336Z","updated_at":"2019-11-20T17:02:14.336Z","state":"closed","iid":5,"start_date":null,"group_id":4351}
+{"id":7641,"title":"v3.0","project_id":null,"description":"Et repellat culpa nemo consequatur ut reprehenderit.","due_date":null,"created_at":"2019-11-20T17:02:14.323Z","updated_at":"2019-11-20T17:02:14.323Z","state":"active","iid":4,"start_date":null,"group_id":4351}
+{"id":7640,"title":"v2.0","project_id":null,"description":"Velit cupiditate est neque voluptates iste rem sunt.","due_date":null,"created_at":"2019-11-20T17:02:14.309Z","updated_at":"2019-11-20T17:02:14.309Z","state":"active","iid":3,"start_date":null,"group_id":4351}
+{"id":7639,"title":"v1.0","project_id":null,"description":"Amet velit repellat ut rerum aut cum.","due_date":null,"created_at":"2019-11-20T17:02:14.296Z","updated_at":"2019-11-20T17:02:14.296Z","state":"active","iid":2,"start_date":null,"group_id":4351}
+{"id":7638,"title":"v0.0","project_id":null,"description":"Ea quia asperiores ut modi dolorem sunt non numquam.","due_date":null,"created_at":"2019-11-20T17:02:14.282Z","updated_at":"2019-11-20T17:02:14.282Z","state":"active","iid":1,"start_date":null,"group_id":4351}
diff --git a/spec/fixtures/lib/gitlab/import_export/group_exports/no_children/tree/groups/_all.ndjson b/spec/fixtures/lib/gitlab/import_export/group_exports/no_children/tree/groups/_all.ndjson
new file mode 100644
index 00000000000..64b4254d985
--- /dev/null
+++ b/spec/fixtures/lib/gitlab/import_export/group_exports/no_children/tree/groups/_all.ndjson
@@ -0,0 +1 @@
+4353
diff --git a/spec/fixtures/lib/gitlab/import_export/group_exports/visibility_levels/internal/tree/groups/283.json b/spec/fixtures/lib/gitlab/import_export/group_exports/visibility_levels/internal/tree/groups/283.json
new file mode 100644
index 00000000000..43fba82e87e
--- /dev/null
+++ b/spec/fixtures/lib/gitlab/import_export/group_exports/visibility_levels/internal/tree/groups/283.json
@@ -0,0 +1 @@
+{"id":283,"name":"internal","path":"internal","owner_id":null,"created_at":"2020-02-12T16:56:34.924Z","updated_at":"2020-02-12T16:56:38.710Z","description":"","avatar":{"url":null},"membership_lock":false,"share_with_group_lock":false,"visibility_level":10,"request_access_enabled":true,"ldap_sync_status":"ready","ldap_sync_error":null,"ldap_sync_last_update_at":null,"ldap_sync_last_successful_update_at":null,"ldap_sync_last_sync_at":null,"lfs_enabled":null,"parent_id":null,"shared_runners_minutes_limit":null,"repository_size_limit":null,"require_two_factor_authentication":false,"two_factor_grace_period":48,"plan_id":null,"project_creation_level":2,"trial_ends_on":null,"file_template_project_id":null,"custom_project_templates_group_id":null,"auto_devops_enabled":null,"extra_shared_runners_minutes_limit":null,"last_ci_minutes_notification_at":null,"last_ci_minutes_usage_notification_level":null,"subgroup_creation_level":1,"emails_disabled":null,"max_pages_size":null,"max_artifacts_size":null,"mentions_disabled":null}
diff --git a/spec/fixtures/lib/gitlab/import_export/group_exports/visibility_levels/internal/tree/groups/284.json b/spec/fixtures/lib/gitlab/import_export/group_exports/visibility_levels/internal/tree/groups/284.json
new file mode 100644
index 00000000000..376258fb835
--- /dev/null
+++ b/spec/fixtures/lib/gitlab/import_export/group_exports/visibility_levels/internal/tree/groups/284.json
@@ -0,0 +1 @@
+{"id":284,"name":"public","path":"public","owner_id":null,"created_at":"2020-02-12T17:33:00.575Z","updated_at":"2020-02-12T17:33:00.575Z","description":"","avatar":{"url":null},"membership_lock":false,"share_with_group_lock":false,"visibility_level":20,"request_access_enabled":true,"ldap_sync_status":"ready","ldap_sync_error":null,"ldap_sync_last_update_at":null,"ldap_sync_last_successful_update_at":null,"ldap_sync_last_sync_at":null,"lfs_enabled":null,"parent_id":283,"shared_runners_minutes_limit":null,"repository_size_limit":null,"require_two_factor_authentication":false,"two_factor_grace_period":48,"plan_id":null,"project_creation_level":2,"trial_ends_on":null,"file_template_project_id":null,"custom_project_templates_group_id":null,"auto_devops_enabled":null,"extra_shared_runners_minutes_limit":null,"last_ci_minutes_notification_at":null,"last_ci_minutes_usage_notification_level":null,"subgroup_creation_level":1,"emails_disabled":null,"max_pages_size":null,"max_artifacts_size":null,"mentions_disabled":null}
diff --git a/spec/fixtures/lib/gitlab/import_export/group_exports/visibility_levels/internal/tree/groups/285.json b/spec/fixtures/lib/gitlab/import_export/group_exports/visibility_levels/internal/tree/groups/285.json
new file mode 100644
index 00000000000..d0539d9d490
--- /dev/null
+++ b/spec/fixtures/lib/gitlab/import_export/group_exports/visibility_levels/internal/tree/groups/285.json
@@ -0,0 +1 @@
+{"id":285,"name":"internal","path":"internal","owner_id":null,"created_at":"2020-02-12T17:33:00.575Z","updated_at":"2020-02-12T17:33:00.575Z","description":"","avatar":{"url":null},"membership_lock":false,"share_with_group_lock":false,"visibility_level":10,"request_access_enabled":true,"ldap_sync_status":"ready","ldap_sync_error":null,"ldap_sync_last_update_at":null,"ldap_sync_last_successful_update_at":null,"ldap_sync_last_sync_at":null,"lfs_enabled":null,"parent_id":283,"shared_runners_minutes_limit":null,"repository_size_limit":null,"require_two_factor_authentication":false,"two_factor_grace_period":48,"plan_id":null,"project_creation_level":2,"trial_ends_on":null,"file_template_project_id":null,"custom_project_templates_group_id":null,"auto_devops_enabled":null,"extra_shared_runners_minutes_limit":null,"last_ci_minutes_notification_at":null,"last_ci_minutes_usage_notification_level":null,"subgroup_creation_level":1,"emails_disabled":null,"max_pages_size":null,"max_artifacts_size":null,"mentions_disabled":null}
diff --git a/spec/fixtures/lib/gitlab/import_export/group_exports/visibility_levels/internal/tree/groups/286.json b/spec/fixtures/lib/gitlab/import_export/group_exports/visibility_levels/internal/tree/groups/286.json
new file mode 100644
index 00000000000..aee3de23380
--- /dev/null
+++ b/spec/fixtures/lib/gitlab/import_export/group_exports/visibility_levels/internal/tree/groups/286.json
@@ -0,0 +1 @@
+{"id":286,"name":"private","path":"private","owner_id":null,"created_at":"2020-02-12T17:33:00.575Z","updated_at":"2020-02-12T17:33:00.575Z","description":"","avatar":{"url":null},"membership_lock":false,"share_with_group_lock":false,"visibility_level":0,"request_access_enabled":true,"ldap_sync_status":"ready","ldap_sync_error":null,"ldap_sync_last_update_at":null,"ldap_sync_last_successful_update_at":null,"ldap_sync_last_sync_at":null,"lfs_enabled":null,"parent_id":283,"shared_runners_minutes_limit":null,"repository_size_limit":null,"require_two_factor_authentication":false,"two_factor_grace_period":48,"plan_id":null,"project_creation_level":2,"trial_ends_on":null,"file_template_project_id":null,"custom_project_templates_group_id":null,"auto_devops_enabled":null,"extra_shared_runners_minutes_limit":null,"last_ci_minutes_notification_at":null,"last_ci_minutes_usage_notification_level":null,"subgroup_creation_level":1,"emails_disabled":null,"max_pages_size":null,"max_artifacts_size":null,"mentions_disabled":null}
diff --git a/spec/fixtures/lib/gitlab/import_export/group_exports/visibility_levels/internal/tree/groups/_all.ndjson b/spec/fixtures/lib/gitlab/import_export/group_exports/visibility_levels/internal/tree/groups/_all.ndjson
new file mode 100644
index 00000000000..f289581d271
--- /dev/null
+++ b/spec/fixtures/lib/gitlab/import_export/group_exports/visibility_levels/internal/tree/groups/_all.ndjson
@@ -0,0 +1,4 @@
+283
+284
+285
+286
diff --git a/spec/fixtures/lib/gitlab/import_export/group_exports/visibility_levels/private/tree/groups/283.json b/spec/fixtures/lib/gitlab/import_export/group_exports/visibility_levels/private/tree/groups/283.json
new file mode 100644
index 00000000000..7843315d217
--- /dev/null
+++ b/spec/fixtures/lib/gitlab/import_export/group_exports/visibility_levels/private/tree/groups/283.json
@@ -0,0 +1 @@
+{"id":283,"name":"private","path":"private","owner_id":null,"created_at":"2020-02-12T16:56:34.924Z","updated_at":"2020-02-12T16:56:38.710Z","description":"","avatar":{"url":null},"membership_lock":false,"share_with_group_lock":false,"visibility_level":0,"request_access_enabled":true,"ldap_sync_status":"ready","ldap_sync_error":null,"ldap_sync_last_update_at":null,"ldap_sync_last_successful_update_at":null,"ldap_sync_last_sync_at":null,"lfs_enabled":null,"parent_id":null,"shared_runners_minutes_limit":null,"repository_size_limit":null,"require_two_factor_authentication":false,"two_factor_grace_period":48,"plan_id":null,"project_creation_level":2,"trial_ends_on":null,"file_template_project_id":null,"custom_project_templates_group_id":null,"auto_devops_enabled":null,"extra_shared_runners_minutes_limit":null,"last_ci_minutes_notification_at":null,"last_ci_minutes_usage_notification_level":null,"subgroup_creation_level":1,"emails_disabled":null,"max_pages_size":null,"max_artifacts_size":null,"mentions_disabled":null}
diff --git a/spec/fixtures/lib/gitlab/import_export/group_exports/visibility_levels/private/tree/groups/284.json b/spec/fixtures/lib/gitlab/import_export/group_exports/visibility_levels/private/tree/groups/284.json
new file mode 100644
index 00000000000..376258fb835
--- /dev/null
+++ b/spec/fixtures/lib/gitlab/import_export/group_exports/visibility_levels/private/tree/groups/284.json
@@ -0,0 +1 @@
+{"id":284,"name":"public","path":"public","owner_id":null,"created_at":"2020-02-12T17:33:00.575Z","updated_at":"2020-02-12T17:33:00.575Z","description":"","avatar":{"url":null},"membership_lock":false,"share_with_group_lock":false,"visibility_level":20,"request_access_enabled":true,"ldap_sync_status":"ready","ldap_sync_error":null,"ldap_sync_last_update_at":null,"ldap_sync_last_successful_update_at":null,"ldap_sync_last_sync_at":null,"lfs_enabled":null,"parent_id":283,"shared_runners_minutes_limit":null,"repository_size_limit":null,"require_two_factor_authentication":false,"two_factor_grace_period":48,"plan_id":null,"project_creation_level":2,"trial_ends_on":null,"file_template_project_id":null,"custom_project_templates_group_id":null,"auto_devops_enabled":null,"extra_shared_runners_minutes_limit":null,"last_ci_minutes_notification_at":null,"last_ci_minutes_usage_notification_level":null,"subgroup_creation_level":1,"emails_disabled":null,"max_pages_size":null,"max_artifacts_size":null,"mentions_disabled":null}
diff --git a/spec/fixtures/lib/gitlab/import_export/group_exports/visibility_levels/private/tree/groups/285.json b/spec/fixtures/lib/gitlab/import_export/group_exports/visibility_levels/private/tree/groups/285.json
new file mode 100644
index 00000000000..d0539d9d490
--- /dev/null
+++ b/spec/fixtures/lib/gitlab/import_export/group_exports/visibility_levels/private/tree/groups/285.json
@@ -0,0 +1 @@
+{"id":285,"name":"internal","path":"internal","owner_id":null,"created_at":"2020-02-12T17:33:00.575Z","updated_at":"2020-02-12T17:33:00.575Z","description":"","avatar":{"url":null},"membership_lock":false,"share_with_group_lock":false,"visibility_level":10,"request_access_enabled":true,"ldap_sync_status":"ready","ldap_sync_error":null,"ldap_sync_last_update_at":null,"ldap_sync_last_successful_update_at":null,"ldap_sync_last_sync_at":null,"lfs_enabled":null,"parent_id":283,"shared_runners_minutes_limit":null,"repository_size_limit":null,"require_two_factor_authentication":false,"two_factor_grace_period":48,"plan_id":null,"project_creation_level":2,"trial_ends_on":null,"file_template_project_id":null,"custom_project_templates_group_id":null,"auto_devops_enabled":null,"extra_shared_runners_minutes_limit":null,"last_ci_minutes_notification_at":null,"last_ci_minutes_usage_notification_level":null,"subgroup_creation_level":1,"emails_disabled":null,"max_pages_size":null,"max_artifacts_size":null,"mentions_disabled":null}
diff --git a/spec/fixtures/lib/gitlab/import_export/group_exports/visibility_levels/private/tree/groups/286.json b/spec/fixtures/lib/gitlab/import_export/group_exports/visibility_levels/private/tree/groups/286.json
new file mode 100644
index 00000000000..aee3de23380
--- /dev/null
+++ b/spec/fixtures/lib/gitlab/import_export/group_exports/visibility_levels/private/tree/groups/286.json
@@ -0,0 +1 @@
+{"id":286,"name":"private","path":"private","owner_id":null,"created_at":"2020-02-12T17:33:00.575Z","updated_at":"2020-02-12T17:33:00.575Z","description":"","avatar":{"url":null},"membership_lock":false,"share_with_group_lock":false,"visibility_level":0,"request_access_enabled":true,"ldap_sync_status":"ready","ldap_sync_error":null,"ldap_sync_last_update_at":null,"ldap_sync_last_successful_update_at":null,"ldap_sync_last_sync_at":null,"lfs_enabled":null,"parent_id":283,"shared_runners_minutes_limit":null,"repository_size_limit":null,"require_two_factor_authentication":false,"two_factor_grace_period":48,"plan_id":null,"project_creation_level":2,"trial_ends_on":null,"file_template_project_id":null,"custom_project_templates_group_id":null,"auto_devops_enabled":null,"extra_shared_runners_minutes_limit":null,"last_ci_minutes_notification_at":null,"last_ci_minutes_usage_notification_level":null,"subgroup_creation_level":1,"emails_disabled":null,"max_pages_size":null,"max_artifacts_size":null,"mentions_disabled":null}
diff --git a/spec/fixtures/lib/gitlab/import_export/group_exports/visibility_levels/private/tree/groups/_all.ndjson b/spec/fixtures/lib/gitlab/import_export/group_exports/visibility_levels/private/tree/groups/_all.ndjson
new file mode 100644
index 00000000000..f289581d271
--- /dev/null
+++ b/spec/fixtures/lib/gitlab/import_export/group_exports/visibility_levels/private/tree/groups/_all.ndjson
@@ -0,0 +1,4 @@
+283
+284
+285
+286
diff --git a/spec/fixtures/lib/gitlab/import_export/group_exports/visibility_levels/public/tree/groups/283.json b/spec/fixtures/lib/gitlab/import_export/group_exports/visibility_levels/public/tree/groups/283.json
new file mode 100644
index 00000000000..1148be71cd1
--- /dev/null
+++ b/spec/fixtures/lib/gitlab/import_export/group_exports/visibility_levels/public/tree/groups/283.json
@@ -0,0 +1 @@
+{"id":283,"name":"public","path":"public","owner_id":null,"created_at":"2020-02-12T16:56:34.924Z","updated_at":"2020-02-12T16:56:38.710Z","description":"","avatar":{"url":null},"membership_lock":false,"share_with_group_lock":false,"visibility_level":20,"request_access_enabled":true,"ldap_sync_status":"ready","ldap_sync_error":null,"ldap_sync_last_update_at":null,"ldap_sync_last_successful_update_at":null,"ldap_sync_last_sync_at":null,"lfs_enabled":null,"parent_id":null,"shared_runners_minutes_limit":null,"repository_size_limit":null,"require_two_factor_authentication":false,"two_factor_grace_period":48,"plan_id":null,"project_creation_level":2,"trial_ends_on":null,"file_template_project_id":null,"custom_project_templates_group_id":null,"auto_devops_enabled":null,"extra_shared_runners_minutes_limit":null,"last_ci_minutes_notification_at":null,"last_ci_minutes_usage_notification_level":null,"subgroup_creation_level":1,"emails_disabled":null,"max_pages_size":null,"max_artifacts_size":null,"mentions_disabled":null}
diff --git a/spec/fixtures/lib/gitlab/import_export/group_exports/visibility_levels/public/tree/groups/284.json b/spec/fixtures/lib/gitlab/import_export/group_exports/visibility_levels/public/tree/groups/284.json
new file mode 100644
index 00000000000..376258fb835
--- /dev/null
+++ b/spec/fixtures/lib/gitlab/import_export/group_exports/visibility_levels/public/tree/groups/284.json
@@ -0,0 +1 @@
+{"id":284,"name":"public","path":"public","owner_id":null,"created_at":"2020-02-12T17:33:00.575Z","updated_at":"2020-02-12T17:33:00.575Z","description":"","avatar":{"url":null},"membership_lock":false,"share_with_group_lock":false,"visibility_level":20,"request_access_enabled":true,"ldap_sync_status":"ready","ldap_sync_error":null,"ldap_sync_last_update_at":null,"ldap_sync_last_successful_update_at":null,"ldap_sync_last_sync_at":null,"lfs_enabled":null,"parent_id":283,"shared_runners_minutes_limit":null,"repository_size_limit":null,"require_two_factor_authentication":false,"two_factor_grace_period":48,"plan_id":null,"project_creation_level":2,"trial_ends_on":null,"file_template_project_id":null,"custom_project_templates_group_id":null,"auto_devops_enabled":null,"extra_shared_runners_minutes_limit":null,"last_ci_minutes_notification_at":null,"last_ci_minutes_usage_notification_level":null,"subgroup_creation_level":1,"emails_disabled":null,"max_pages_size":null,"max_artifacts_size":null,"mentions_disabled":null}
diff --git a/spec/fixtures/lib/gitlab/import_export/group_exports/visibility_levels/public/tree/groups/285.json b/spec/fixtures/lib/gitlab/import_export/group_exports/visibility_levels/public/tree/groups/285.json
new file mode 100644
index 00000000000..d0539d9d490
--- /dev/null
+++ b/spec/fixtures/lib/gitlab/import_export/group_exports/visibility_levels/public/tree/groups/285.json
@@ -0,0 +1 @@
+{"id":285,"name":"internal","path":"internal","owner_id":null,"created_at":"2020-02-12T17:33:00.575Z","updated_at":"2020-02-12T17:33:00.575Z","description":"","avatar":{"url":null},"membership_lock":false,"share_with_group_lock":false,"visibility_level":10,"request_access_enabled":true,"ldap_sync_status":"ready","ldap_sync_error":null,"ldap_sync_last_update_at":null,"ldap_sync_last_successful_update_at":null,"ldap_sync_last_sync_at":null,"lfs_enabled":null,"parent_id":283,"shared_runners_minutes_limit":null,"repository_size_limit":null,"require_two_factor_authentication":false,"two_factor_grace_period":48,"plan_id":null,"project_creation_level":2,"trial_ends_on":null,"file_template_project_id":null,"custom_project_templates_group_id":null,"auto_devops_enabled":null,"extra_shared_runners_minutes_limit":null,"last_ci_minutes_notification_at":null,"last_ci_minutes_usage_notification_level":null,"subgroup_creation_level":1,"emails_disabled":null,"max_pages_size":null,"max_artifacts_size":null,"mentions_disabled":null}
diff --git a/spec/fixtures/lib/gitlab/import_export/group_exports/visibility_levels/public/tree/groups/286.json b/spec/fixtures/lib/gitlab/import_export/group_exports/visibility_levels/public/tree/groups/286.json
new file mode 100644
index 00000000000..aee3de23380
--- /dev/null
+++ b/spec/fixtures/lib/gitlab/import_export/group_exports/visibility_levels/public/tree/groups/286.json
@@ -0,0 +1 @@
+{"id":286,"name":"private","path":"private","owner_id":null,"created_at":"2020-02-12T17:33:00.575Z","updated_at":"2020-02-12T17:33:00.575Z","description":"","avatar":{"url":null},"membership_lock":false,"share_with_group_lock":false,"visibility_level":0,"request_access_enabled":true,"ldap_sync_status":"ready","ldap_sync_error":null,"ldap_sync_last_update_at":null,"ldap_sync_last_successful_update_at":null,"ldap_sync_last_sync_at":null,"lfs_enabled":null,"parent_id":283,"shared_runners_minutes_limit":null,"repository_size_limit":null,"require_two_factor_authentication":false,"two_factor_grace_period":48,"plan_id":null,"project_creation_level":2,"trial_ends_on":null,"file_template_project_id":null,"custom_project_templates_group_id":null,"auto_devops_enabled":null,"extra_shared_runners_minutes_limit":null,"last_ci_minutes_notification_at":null,"last_ci_minutes_usage_notification_level":null,"subgroup_creation_level":1,"emails_disabled":null,"max_pages_size":null,"max_artifacts_size":null,"mentions_disabled":null}
diff --git a/spec/fixtures/lib/gitlab/import_export/group_exports/visibility_levels/public/tree/groups/_all.ndjson b/spec/fixtures/lib/gitlab/import_export/group_exports/visibility_levels/public/tree/groups/_all.ndjson
new file mode 100644
index 00000000000..f289581d271
--- /dev/null
+++ b/spec/fixtures/lib/gitlab/import_export/group_exports/visibility_levels/public/tree/groups/_all.ndjson
@@ -0,0 +1,4 @@
+283
+284
+285
+286
diff --git a/spec/fixtures/lib/gitlab/import_export/invalid_json/tree.tar.gz b/spec/fixtures/lib/gitlab/import_export/invalid_json/tree.tar.gz
deleted file mode 100644
index 6524ed5042c..00000000000
--- a/spec/fixtures/lib/gitlab/import_export/invalid_json/tree.tar.gz
+++ /dev/null
Binary files differ
diff --git a/spec/fixtures/lib/gitlab/import_export/invalid_json/tree/project.json b/spec/fixtures/lib/gitlab/import_export/invalid_json/tree/project.json
new file mode 100644
index 00000000000..a5349c5eb85
--- /dev/null
+++ b/spec/fixtures/lib/gitlab/import_export/invalid_json/tree/project.json
@@ -0,0 +1 @@
+{"invalid" json}
diff --git a/spec/fixtures/lib/gitlab/import_export/light/tree.tar.gz b/spec/fixtures/lib/gitlab/import_export/light/tree.tar.gz
deleted file mode 100644
index eac19c23b44..00000000000
--- a/spec/fixtures/lib/gitlab/import_export/light/tree.tar.gz
+++ /dev/null
Binary files differ
diff --git a/spec/fixtures/lib/gitlab/import_export/light/tree/project.json b/spec/fixtures/lib/gitlab/import_export/light/tree/project.json
new file mode 100644
index 00000000000..12136c6df3b
--- /dev/null
+++ b/spec/fixtures/lib/gitlab/import_export/light/tree/project.json
@@ -0,0 +1 @@
+{"description":"Nisi et repellendus ut enim quo accusamus vel magnam.","import_type":"gitlab_project","creator_id":2147483547,"visibility_level":10,"archived":false,"hooks":[]}
diff --git a/spec/fixtures/lib/gitlab/import_export/light/tree/project/custom_attributes.ndjson b/spec/fixtures/lib/gitlab/import_export/light/tree/project/custom_attributes.ndjson
new file mode 100644
index 00000000000..c1bf6550321
--- /dev/null
+++ b/spec/fixtures/lib/gitlab/import_export/light/tree/project/custom_attributes.ndjson
@@ -0,0 +1,2 @@
+{"id":201,"project_id":5,"created_at":"2016-06-14T15:01:51.315Z","updated_at":"2016-06-14T15:01:51.315Z","key":"color","value":"red"}
+{"id":202,"project_id":5,"created_at":"2016-06-14T15:01:51.315Z","updated_at":"2016-06-14T15:01:51.315Z","key":"size","value":"small"}
diff --git a/spec/fixtures/lib/gitlab/import_export/light/tree/project/issues.ndjson b/spec/fixtures/lib/gitlab/import_export/light/tree/project/issues.ndjson
new file mode 100644
index 00000000000..51154e820e6
--- /dev/null
+++ b/spec/fixtures/lib/gitlab/import_export/light/tree/project/issues.ndjson
@@ -0,0 +1 @@
+{"id":1,"title":"Fugiat est minima quae maxime non similique.","assignee_id":null,"project_id":8,"author_id":1,"created_at":"2017-07-07T18:13:01.138Z","updated_at":"2017-08-15T18:37:40.807Z","branch_name":null,"description":"Quam totam fuga numquam in eveniet.","state":"opened","iid":20,"updated_by_id":1,"confidential":false,"due_date":null,"moved_to_id":null,"lock_version":null,"time_estimate":0,"closed_at":null,"last_edited_at":null,"last_edited_by_id":null,"group_milestone_id":null,"milestone":{"id":1,"title":"A milestone","group_id":null,"description":"Project-level milestone","due_date":null,"created_at":"2016-06-14T15:02:04.415Z","updated_at":"2016-06-14T15:02:04.415Z","state":"active","iid":1},"label_links":[{"id":11,"label_id":2,"target_id":1,"target_type":"Issue","created_at":"2017-08-15T18:37:40.795Z","updated_at":"2017-08-15T18:37:40.795Z","label":{"id":6,"title":"Another label","color":"#A8D695","project_id":null,"created_at":"2017-08-15T18:37:19.698Z","updated_at":"2017-08-15T18:37:19.698Z","template":false,"description":"","group_id":null,"type":"ProjectLabel","priorities":[]}}],"notes":[{"id":20,"note":"created merge request !1 to address this issue","noteable_type":"Issue","author_id":1,"created_at":"2020-03-28T01:37:42.307Z","updated_at":"2020-03-28T01:37:42.307Z","project_id":8,"attachment":{"url":null},"line_code":null,"commit_id":null,"system":true,"st_diff":null,"updated_by_id":null,"position":null,"original_position":null,"resolved_at":null,"resolved_by_id":null,"discussion_id":null,"change_position":null,"resolved_by_push":null,"confidential":null,"type":null,"author":{"name":"Author"},"award_emoji":[],"system_note_metadata":{"id":21,"commit_count":null,"action":"merge","created_at":"2020-03-28T01:37:42.307Z","updated_at":"2020-03-28T01:37:42.307Z"},"events":[]}]}
diff --git a/spec/fixtures/lib/gitlab/import_export/light/tree/project/labels.ndjson b/spec/fixtures/lib/gitlab/import_export/light/tree/project/labels.ndjson
new file mode 100644
index 00000000000..28894a8a404
--- /dev/null
+++ b/spec/fixtures/lib/gitlab/import_export/light/tree/project/labels.ndjson
@@ -0,0 +1 @@
+{"id":2,"title":"A project label","color":"#428bca","project_id":8,"created_at":"2016-07-22T08:55:44.161Z","updated_at":"2016-07-22T08:55:44.161Z","template":false,"description":"","type":"ProjectLabel","priorities":[{"id":1,"project_id":5,"label_id":1,"priority":1,"created_at":"2016-10-18T09:35:43.338Z","updated_at":"2016-10-18T09:35:43.338Z"}]}
diff --git a/spec/fixtures/lib/gitlab/import_export/light/tree/project/milestones.ndjson b/spec/fixtures/lib/gitlab/import_export/light/tree/project/milestones.ndjson
new file mode 100644
index 00000000000..5158c81db7c
--- /dev/null
+++ b/spec/fixtures/lib/gitlab/import_export/light/tree/project/milestones.ndjson
@@ -0,0 +1 @@
+{"id":1,"title":"A milestone","project_id":8,"description":"Project-level milestone","due_date":null,"created_at":"2016-06-14T15:02:04.415Z","updated_at":"2016-06-14T15:02:04.415Z","state":"active","iid":1,"group_id":null}
diff --git a/spec/fixtures/lib/gitlab/import_export/light/tree/project/services.ndjson b/spec/fixtures/lib/gitlab/import_export/light/tree/project/services.ndjson
new file mode 100644
index 00000000000..c5ae6bf4b04
--- /dev/null
+++ b/spec/fixtures/lib/gitlab/import_export/light/tree/project/services.ndjson
@@ -0,0 +1,2 @@
+{"id":100,"title":"JetBrains TeamCity CI","project_id":5,"created_at":"2016-06-14T15:01:51.315Z","updated_at":"2016-06-14T15:01:51.315Z","active":false,"properties":{},"template":true,"instance":false,"push_events":true,"issues_events":true,"merge_requests_events":true,"tag_push_events":true,"note_events":true,"job_events":true,"type":"TeamcityService","category":"ci","default":false,"wiki_page_events":true}
+{"id":101,"title":"Jira","project_id":5,"created_at":"2016-06-14T15:01:51.315Z","updated_at":"2016-06-14T15:01:51.315Z","active":false,"properties":{},"template":false,"instance":true,"push_events":true,"issues_events":true,"merge_requests_events":true,"tag_push_events":true,"note_events":true,"job_events":true,"type":"JiraService","category":"ci","default":false,"wiki_page_events":true}
diff --git a/spec/fixtures/lib/gitlab/import_export/milestone-iid/tree.tar.gz b/spec/fixtures/lib/gitlab/import_export/milestone-iid/tree.tar.gz
deleted file mode 100644
index 726afa0bfa4..00000000000
--- a/spec/fixtures/lib/gitlab/import_export/milestone-iid/tree.tar.gz
+++ /dev/null
Binary files differ
diff --git a/spec/fixtures/lib/gitlab/import_export/milestone-iid/tree/project.json b/spec/fixtures/lib/gitlab/import_export/milestone-iid/tree/project.json
new file mode 100644
index 00000000000..1fed9cc4d2a
--- /dev/null
+++ b/spec/fixtures/lib/gitlab/import_export/milestone-iid/tree/project.json
@@ -0,0 +1 @@
+{"id":5,"description":"Nisi et repellendus ut enim quo accusamus vel magnam.","import_type":"gitlab_project","creator_id":123,"visibility_level":10,"archived":false,"hooks":[]}
diff --git a/spec/fixtures/lib/gitlab/import_export/milestone-iid/tree/project/issues.ndjson b/spec/fixtures/lib/gitlab/import_export/milestone-iid/tree/project/issues.ndjson
new file mode 100644
index 00000000000..ea74eb8b379
--- /dev/null
+++ b/spec/fixtures/lib/gitlab/import_export/milestone-iid/tree/project/issues.ndjson
@@ -0,0 +1,2 @@
+{"id":1,"title":"Fugiat est minima quae maxime non similique.","assignee_id":null,"project_id":8,"author_id":1,"created_at":"2017-07-07T18:13:01.138Z","updated_at":"2017-08-15T18:37:40.807Z","branch_name":null,"description":"Quam totam fuga numquam in eveniet.","state":"opened","iid":20,"updated_by_id":1,"confidential":false,"due_date":null,"moved_to_id":null,"lock_version":null,"time_estimate":0,"closed_at":null,"last_edited_at":null,"last_edited_by_id":null,"group_milestone_id":null,"milestone":{"id":1,"title":"Group-level milestone","description":"Group-level milestone","due_date":null,"created_at":"2016-06-14T15:02:04.415Z","updated_at":"2016-06-14T15:02:04.415Z","state":"active","iid":1,"group_id":8}}
+{"id":2,"title":"est minima quae maxime non similique.","assignee_id":null,"project_id":8,"author_id":1,"created_at":"2017-07-07T18:13:01.138Z","updated_at":"2017-08-15T18:37:40.807Z","branch_name":null,"description":"Quam totam fuga numquam in eveniet.","state":"opened","iid":21,"updated_by_id":1,"confidential":false,"due_date":null,"moved_to_id":null,"lock_version":null,"time_estimate":0,"closed_at":null,"last_edited_at":null,"last_edited_by_id":null,"group_milestone_id":null,"milestone":{"id":2,"title":"Another milestone","project_id":8,"description":"milestone","due_date":null,"created_at":"2016-06-14T15:02:04.415Z","updated_at":"2016-06-14T15:02:04.415Z","state":"active","iid":1,"group_id":null}}
diff --git a/spec/fixtures/lib/gitlab/import_export/multi_pipeline_ref_one_external_pr/tree.tar.gz b/spec/fixtures/lib/gitlab/import_export/multi_pipeline_ref_one_external_pr/tree.tar.gz
deleted file mode 100644
index 13f3d3c6791..00000000000
--- a/spec/fixtures/lib/gitlab/import_export/multi_pipeline_ref_one_external_pr/tree.tar.gz
+++ /dev/null
Binary files differ
diff --git a/spec/fixtures/lib/gitlab/import_export/multi_pipeline_ref_one_external_pr/tree/project.json b/spec/fixtures/lib/gitlab/import_export/multi_pipeline_ref_one_external_pr/tree/project.json
new file mode 100644
index 00000000000..5f7cf8128bc
--- /dev/null
+++ b/spec/fixtures/lib/gitlab/import_export/multi_pipeline_ref_one_external_pr/tree/project.json
@@ -0,0 +1 @@
+{"id":5,"approvals_before_merge":0,"archived":false,"auto_cancel_pending_pipelines":"enabled","autoclose_referenced_issues":true,"build_allow_git_fetch":true,"build_coverage_regex":null,"build_timeout":3600,"ci_config_path":null,"delete_error":null,"description":"Vim, Tmux and others","disable_overriding_approvers_per_merge_request":null,"external_authorization_classification_label":"","external_webhook_token":"D3mVYFzZkgZ5kMfcW_wx","public_builds":true,"shared_runners_enabled":true,"visibility_level":20}
diff --git a/spec/fixtures/lib/gitlab/import_export/multi_pipeline_ref_one_external_pr/tree/project/ci_cd_settings.ndjson b/spec/fixtures/lib/gitlab/import_export/multi_pipeline_ref_one_external_pr/tree/project/ci_cd_settings.ndjson
new file mode 100644
index 00000000000..ab06e07d48d
--- /dev/null
+++ b/spec/fixtures/lib/gitlab/import_export/multi_pipeline_ref_one_external_pr/tree/project/ci_cd_settings.ndjson
@@ -0,0 +1 @@
+{"group_runners_enabled":true}
diff --git a/spec/fixtures/lib/gitlab/import_export/multi_pipeline_ref_one_external_pr/tree/project/ci_pipelines.ndjson b/spec/fixtures/lib/gitlab/import_export/multi_pipeline_ref_one_external_pr/tree/project/ci_pipelines.ndjson
new file mode 100644
index 00000000000..0c93a83d50d
--- /dev/null
+++ b/spec/fixtures/lib/gitlab/import_export/multi_pipeline_ref_one_external_pr/tree/project/ci_pipelines.ndjson
@@ -0,0 +1,2 @@
+{"before_sha":"0000000000000000000000000000000000000000","committed_at":null,"config_source":"repository_source","created_at":"2020-02-25T12:08:40.615Z","duration":61,"external_pull_request":{"created_at":"2020-02-25T12:08:40.478Z","id":59023,"project_id":17121868,"pull_request_iid":4,"source_branch":"new-branch","source_repository":"liptonshmidt/dotfiles","source_sha":"122bc4bbad5b6448089cacbe16d0bdc3534e7eda","status":"open","target_branch":"master","target_repository":"liptonshmidt/dotfiles","target_sha":"86ebe754fa12216e5c0d9d95890936e2fcc62392","updated_at":"2020-02-25T12:08:40.478Z"},"failure_reason":null,"finished_at":"2020-02-25T12:09:44.464Z","id":120842687,"iid":8,"lock_version":3,"notes":[],"project_id":17121868,"protected":false,"ref":"new-branch","sha":"122bc4bbad5b6448089cacbe16d0bdc3534e7eda","source":"external_pull_request_event","source_sha":"122bc4bbad5b6448089cacbe16d0bdc3534e7eda","stages":[],"started_at":"2020-02-25T12:08:42.511Z","status":"success","tag":false,"target_sha":"86ebe754fa12216e5c0d9d95890936e2fcc62392","updated_at":"2020-02-25T12:09:44.473Z","user_id":4087087,"yaml_errors":null}
+{"before_sha":"86ebe754fa12216e5c0d9d95890936e2fcc62392","committed_at":null,"config_source":"repository_source","created_at":"2020-02-25T12:08:37.434Z","duration":57,"external_pull_request":{"created_at":"2020-02-25T12:08:40.478Z","id":59023,"project_id":17121868,"pull_request_iid":4,"source_branch":"new-branch","source_repository":"liptonshmidt/dotfiles","source_sha":"122bc4bbad5b6448089cacbe16d0bdc3534e7eda","status":"open","target_branch":"master","target_repository":"liptonshmidt/dotfiles","target_sha":"86ebe754fa12216e5c0d9d95890936e2fcc62392","updated_at":"2020-02-25T12:08:40.478Z"},"failure_reason":null,"finished_at":"2020-02-25T12:09:36.557Z","id":120842675,"iid":7,"lock_version":3,"notes":[],"project_id":17121868,"protected":false,"ref":"new-branch","sha":"122bc4bbad5b6448089cacbe16d0bdc3534e7eda","source":"external_pull_request_event","source_sha":"122bc4bbad5b6448089cacbe16d0bdc3534e7eda","stages":[],"started_at":"2020-02-25T12:08:38.682Z","status":"success","tag":false,"target_sha":"86ebe754fa12216e5c0d9d95890936e2fcc62392","updated_at":"2020-02-25T12:09:36.565Z","user_id":4087087,"yaml_errors":null}
diff --git a/spec/fixtures/lib/gitlab/import_export/multi_pipeline_ref_one_external_pr/tree/project/external_pull_requests.ndjson b/spec/fixtures/lib/gitlab/import_export/multi_pipeline_ref_one_external_pr/tree/project/external_pull_requests.ndjson
new file mode 100644
index 00000000000..421ac662dac
--- /dev/null
+++ b/spec/fixtures/lib/gitlab/import_export/multi_pipeline_ref_one_external_pr/tree/project/external_pull_requests.ndjson
@@ -0,0 +1 @@
+{"created_at":"2020-02-25T12:08:40.478Z","id":59023,"project_id":17121868,"pull_request_iid":4,"source_branch":"new-branch","source_repository":"liptonshmidt/dotfiles","source_sha":"122bc4bbad5b6448089cacbe16d0bdc3534e7eda","status":"open","target_branch":"master","target_repository":"liptonshmidt/dotfiles","target_sha":"86ebe754fa12216e5c0d9d95890936e2fcc62392","updated_at":"2020-02-25T12:08:40.478Z"}
diff --git a/spec/fixtures/lib/gitlab/import_export/multi_pipeline_ref_one_external_pr/tree/project/project_feature.ndjson b/spec/fixtures/lib/gitlab/import_export/multi_pipeline_ref_one_external_pr/tree/project/project_feature.ndjson
new file mode 100644
index 00000000000..51f4b53b742
--- /dev/null
+++ b/spec/fixtures/lib/gitlab/import_export/multi_pipeline_ref_one_external_pr/tree/project/project_feature.ndjson
@@ -0,0 +1 @@
+{"builds_access_level":20,"created_at":"2020-02-25T11:20:09.925Z","forking_access_level":20,"id":17494715,"issues_access_level":0,"merge_requests_access_level":0,"pages_access_level":20,"project_id":17121868,"repository_access_level":20,"snippets_access_level":0,"updated_at":"2020-02-25T11:20:10.376Z","wiki_access_level":0}
diff --git a/spec/fixtures/lib/gitlab/import_export/with_invalid_records/tree.tar.gz b/spec/fixtures/lib/gitlab/import_export/with_invalid_records/tree.tar.gz
deleted file mode 100644
index 24c51e72d7d..00000000000
--- a/spec/fixtures/lib/gitlab/import_export/with_invalid_records/tree.tar.gz
+++ /dev/null
Binary files differ
diff --git a/spec/fixtures/lib/gitlab/import_export/with_invalid_records/tree/project.json b/spec/fixtures/lib/gitlab/import_export/with_invalid_records/tree/project.json
new file mode 100644
index 00000000000..c767a8733b4
--- /dev/null
+++ b/spec/fixtures/lib/gitlab/import_export/with_invalid_records/tree/project.json
@@ -0,0 +1 @@
+{"id":5,"description":"Nisi et repellendus ut enim quo accusamus vel magnam.","import_type":"gitlab_project","creator_id":999,"visibility_level":10,"archived":false,"hooks":[]}
diff --git a/spec/fixtures/lib/gitlab/import_export/with_invalid_records/tree/project/milestones.ndjson b/spec/fixtures/lib/gitlab/import_export/with_invalid_records/tree/project/milestones.ndjson
new file mode 100644
index 00000000000..28e737fa43c
--- /dev/null
+++ b/spec/fixtures/lib/gitlab/import_export/with_invalid_records/tree/project/milestones.ndjson
@@ -0,0 +1,2 @@
+{"id":1,"title":null,"project_id":8,"description":123,"due_date":null,"created_at":"NOT A DATE","updated_at":"NOT A DATE","state":"active","iid":1,"group_id":null}
+{"id":42,"title":"A valid milestone","project_id":8,"description":"Project-level milestone","due_date":null,"created_at":"2016-06-14T15:02:04.415Z","updated_at":"2016-06-14T15:02:04.415Z","state":"active","iid":1,"group_id":null}
diff --git a/spec/fixtures/lib/gitlab/metrics/dashboard/development_metrics.yml b/spec/fixtures/lib/gitlab/metrics/dashboard/development_metrics.yml
new file mode 100644
index 00000000000..083261a5574
--- /dev/null
+++ b/spec/fixtures/lib/gitlab/metrics/dashboard/development_metrics.yml
@@ -0,0 +1,39 @@
+panel_groups:
+ - group: 'Usage Variation'
+ panels:
+ - type: anomaly-chart
+ title: "Memory Usage Rate Anomalies"
+ y_label: "Memory Usage Rate"
+ metrics:
+ - id: container_memory_usage_bytes
+ query_range: avg(sum(rate(container_memory_usage_bytes[15m]))) /1024
+ label: "Memory Usage Rate"
+ unit: "kB"
+ - id: container_memory_usage_bytes_upper
+ query_range: 80000
+ label: "Memory Usage Rate Lower Limit"
+ unit: "kB"
+ - id: container_memory_usage_bytes_lower
+ query_range: 50000
+ label: "Memory Usage Rate Upper Limit"
+ unit: "kB"
+ - group: System metrics (Kubernetes)
+ panels:
+ - title: 'Container CPU Usage by Environment (seconds)'
+ type: 'heatmap'
+ metrics:
+ - id: container_cpu_usage_by_env
+ query_range: 'sum(rate(container_cpu_usage_seconds_total{environment=~"coredns|production|kube|kube-controller"}[1h])) by (environment)'
+ step: 3600
+ - title: 'Number of GitLab Runner requests by status'
+ type: 'heatmap'
+ metrics:
+ - id: number_of_runner_requests_by_status
+ query_range: 'sum(rate(gitlab_runner_api_request_statuses_total[60m])) by (status)'
+ step: 3600
+ - title: '95 percentile of request durations per handler (seconds)'
+ type: 'heatmap'
+ metrics:
+ - id: 95_percentile_of_request_durations_per_handler
+ query_range: 'histogram_quantile(0.95, sum(rate(prometheus_http_request_duration_seconds_bucket[1h])) by (handler,le))'
+ step: 3600
diff --git a/spec/fixtures/lsif.json.zip b/spec/fixtures/lsif.json.zip
new file mode 100644
index 00000000000..a65457664b9
--- /dev/null
+++ b/spec/fixtures/lsif.json.zip
Binary files differ
diff --git a/spec/fixtures/sample_doc.md b/spec/fixtures/sample_doc.md
new file mode 100644
index 00000000000..84080dd1089
--- /dev/null
+++ b/spec/fixtures/sample_doc.md
@@ -0,0 +1 @@
+[GitLab API](api/README.md)
diff --git a/spec/fixtures/terraform/tfplan.json b/spec/fixtures/terraform/tfplan.json
new file mode 100644
index 00000000000..0ab4891e63a
--- /dev/null
+++ b/spec/fixtures/terraform/tfplan.json
@@ -0,0 +1 @@
+{"create": 0, "update": 1, "delete": 0}
diff --git a/spec/fixtures/terraform/tfplan_with_corrupted_data.json b/spec/fixtures/terraform/tfplan_with_corrupted_data.json
new file mode 100644
index 00000000000..b83f5e172bb
--- /dev/null
+++ b/spec/fixtures/terraform/tfplan_with_corrupted_data.json
@@ -0,0 +1 @@
+Exited code 1
diff --git a/spec/fixtures/trace/sample_trace b/spec/fixtures/trace/sample_trace
index e76712782be..e9d1e79fc71 100644
--- a/spec/fixtures/trace/sample_trace
+++ b/spec/fixtures/trace/sample_trace
@@ -2768,10 +2768,6 @@ Service
updates the has_external_issue_tracker boolean
on update
updates the has_external_issue_tracker boolean
- #deprecated?
- should return false by default
- #deprecation_message
- should be empty by default
#api_field_names
filters out sensitive fields
diff --git a/spec/fixtures/x509/ZZZZZZA6.crl b/spec/fixtures/x509/ZZZZZZA6.crl
new file mode 100644
index 00000000000..eb6b9d5d71a
--- /dev/null
+++ b/spec/fixtures/x509/ZZZZZZA6.crl
Binary files differ
diff --git a/spec/frontend/.eslintrc.yml b/spec/frontend/.eslintrc.yml
index c8aacca5ef2..b9159191114 100644
--- a/spec/frontend/.eslintrc.yml
+++ b/spec/frontend/.eslintrc.yml
@@ -1,10 +1,6 @@
---
-env:
- jest/globals: true
-plugins:
- - jest
extends:
- - 'plugin:jest/recommended'
+ - 'plugin:@gitlab/jest'
settings:
# We have to teach eslint-plugin-import what node modules we use
# otherwise there is an error when it tries to resolve them
@@ -14,9 +10,18 @@ settings:
- path
import/resolver:
jest:
- jestConfigFile: 'jest.config.js'
+ jestConfigFile: 'jest.config.unit.js'
globals:
getJSONFixture: false
loadFixtures: false
preloadFixtures: false
setFixtures: false
+rules:
+ jest/expect-expect:
+ - off
+ - assertFunctionNames:
+ - 'expect*'
+ - 'assert*'
+ - 'testAction'
+ jest/no-test-callback:
+ - off
diff --git a/spec/frontend/__mocks__/@toast-ui/vue-editor/index.js b/spec/frontend/__mocks__/@toast-ui/vue-editor/index.js
new file mode 100644
index 00000000000..726ed0fa030
--- /dev/null
+++ b/spec/frontend/__mocks__/@toast-ui/vue-editor/index.js
@@ -0,0 +1,29 @@
+export const Editor = {
+ props: {
+ initialValue: {
+ type: String,
+ required: true,
+ },
+ options: {
+ type: Object,
+ },
+ initialEditType: {
+ type: String,
+ },
+ height: {
+ type: String,
+ },
+ previewStyle: {
+ type: String,
+ },
+ },
+ render(h) {
+ return h('div');
+ },
+};
+
+export const Viewer = {
+ render(h) {
+ return h('div');
+ },
+};
diff --git a/spec/frontend/ajax_loading_spinner_spec.js b/spec/frontend/ajax_loading_spinner_spec.js
new file mode 100644
index 00000000000..8ed2ee49ff8
--- /dev/null
+++ b/spec/frontend/ajax_loading_spinner_spec.js
@@ -0,0 +1,57 @@
+import $ from 'jquery';
+import AjaxLoadingSpinner from '~/ajax_loading_spinner';
+
+describe('Ajax Loading Spinner', () => {
+ const fixtureTemplate = 'static/ajax_loading_spinner.html';
+ preloadFixtures(fixtureTemplate);
+
+ beforeEach(() => {
+ loadFixtures(fixtureTemplate);
+ AjaxLoadingSpinner.init();
+ });
+
+ it('change current icon with spinner icon and disable link while waiting ajax response', done => {
+ jest.spyOn($, 'ajax').mockImplementation(req => {
+ const xhr = new XMLHttpRequest();
+ const ajaxLoadingSpinner = document.querySelector('.js-ajax-loading-spinner');
+ const icon = ajaxLoadingSpinner.querySelector('i');
+
+ req.beforeSend(xhr, { dataType: 'text/html' });
+
+ expect(icon).not.toHaveClass('fa-trash-o');
+ expect(icon).toHaveClass('fa-spinner');
+ expect(icon).toHaveClass('fa-spin');
+ expect(icon.dataset.icon).toEqual('fa-trash-o');
+ expect(ajaxLoadingSpinner.getAttribute('disabled')).toEqual('');
+
+ req.complete({});
+
+ done();
+ const deferred = $.Deferred();
+ return deferred.promise();
+ });
+ document.querySelector('.js-ajax-loading-spinner').click();
+ });
+
+ it('use original icon again and enabled the link after complete the ajax request', done => {
+ jest.spyOn($, 'ajax').mockImplementation(req => {
+ const xhr = new XMLHttpRequest();
+ const ajaxLoadingSpinner = document.querySelector('.js-ajax-loading-spinner');
+
+ req.beforeSend(xhr, { dataType: 'text/html' });
+ req.complete({});
+
+ const icon = ajaxLoadingSpinner.querySelector('i');
+
+ expect(icon).toHaveClass('fa-trash-o');
+ expect(icon).not.toHaveClass('fa-spinner');
+ expect(icon).not.toHaveClass('fa-spin');
+ expect(ajaxLoadingSpinner.getAttribute('disabled')).toEqual(null);
+
+ done();
+ const deferred = $.Deferred();
+ return deferred.promise();
+ });
+ document.querySelector('.js-ajax-loading-spinner').click();
+ });
+});
diff --git a/spec/frontend/alert_management/components/alert_management_detail_spec.js b/spec/frontend/alert_management/components/alert_management_detail_spec.js
new file mode 100644
index 00000000000..1e4c2e24ccb
--- /dev/null
+++ b/spec/frontend/alert_management/components/alert_management_detail_spec.js
@@ -0,0 +1,242 @@
+import { mount, shallowMount } from '@vue/test-utils';
+import { GlAlert, GlLoadingIcon, GlDropdownItem, GlTable } from '@gitlab/ui';
+import AlertDetails from '~/alert_management/components/alert_details.vue';
+import updateAlertStatus from '~/alert_management/graphql/mutations/update_alert_status.graphql';
+import createFlash from '~/flash';
+
+import mockAlerts from '../mocks/alerts.json';
+
+const mockAlert = mockAlerts[0];
+jest.mock('~/flash');
+
+describe('AlertDetails', () => {
+ let wrapper;
+ const newIssuePath = 'root/alerts/-/issues/new';
+ const findStatusDropdownItem = () => wrapper.find(GlDropdownItem);
+ const findDetailsTable = () => wrapper.find(GlTable);
+
+ function mountComponent({
+ data,
+ createIssueFromAlertEnabled = false,
+ loading = false,
+ mountMethod = shallowMount,
+ stubs = {},
+ } = {}) {
+ wrapper = mountMethod(AlertDetails, {
+ propsData: {
+ alertId: 'alertId',
+ projectPath: 'projectPath',
+ newIssuePath,
+ },
+ data() {
+ return { alert: { ...mockAlert }, ...data };
+ },
+ provide: {
+ glFeatures: { createIssueFromAlertEnabled },
+ },
+ mocks: {
+ $apollo: {
+ mutate: jest.fn(),
+ queries: {
+ alert: {
+ loading,
+ },
+ },
+ },
+ },
+ stubs,
+ });
+ }
+
+ afterEach(() => {
+ if (wrapper) {
+ wrapper.destroy();
+ }
+ });
+
+ const findCreatedIssueBtn = () => wrapper.find('[data-testid="createIssueBtn"]');
+
+ describe('Alert details', () => {
+ describe('when alert is null', () => {
+ beforeEach(() => {
+ mountComponent({ data: { alert: null } });
+ });
+
+ it('shows an empty state', () => {
+ expect(wrapper.find('[data-testid="alertDetailsTabs"]').exists()).toBe(false);
+ });
+ });
+
+ describe('when alert is present', () => {
+ beforeEach(() => {
+ mountComponent({ data: { alert: mockAlert } });
+ });
+
+ it('renders a tab with overview information', () => {
+ expect(wrapper.find('[data-testid="overviewTab"]').exists()).toBe(true);
+ });
+
+ it('renders a tab with full alert information', () => {
+ expect(wrapper.find('[data-testid="fullDetailsTab"]').exists()).toBe(true);
+ });
+
+ it('renders a title', () => {
+ expect(wrapper.find('[data-testid="title"]').text()).toBe(mockAlert.title);
+ });
+
+ it('renders a start time', () => {
+ expect(wrapper.find('[data-testid="startTimeItem"]').exists()).toBe(true);
+ expect(wrapper.find('[data-testid="startTimeItem"]').props().time).toBe(
+ mockAlert.startedAt,
+ );
+ });
+ });
+
+ describe('individual alert fields', () => {
+ describe.each`
+ field | data | isShown
+ ${'eventCount'} | ${1} | ${true}
+ ${'eventCount'} | ${undefined} | ${false}
+ ${'monitoringTool'} | ${'New Relic'} | ${true}
+ ${'monitoringTool'} | ${undefined} | ${false}
+ ${'service'} | ${'Prometheus'} | ${true}
+ ${'service'} | ${undefined} | ${false}
+ `(`$desc`, ({ field, data, isShown }) => {
+ beforeEach(() => {
+ mountComponent({ data: { alert: { ...mockAlert, [field]: data } } });
+ });
+
+ it(`${field} is ${isShown ? 'displayed' : 'hidden'} correctly`, () => {
+ if (isShown) {
+ expect(wrapper.find(`[data-testid="${field}"]`).text()).toBe(data.toString());
+ } else {
+ expect(wrapper.find(`[data-testid="${field}"]`).exists()).toBe(false);
+ }
+ });
+ });
+ });
+
+ describe('Create issue from alert', () => {
+ describe('createIssueFromAlertEnabled feature flag enabled', () => {
+ it('should display a button that links to new issue page', () => {
+ mountComponent({ createIssueFromAlertEnabled: true });
+ expect(findCreatedIssueBtn().exists()).toBe(true);
+ expect(findCreatedIssueBtn().attributes('href')).toBe(newIssuePath);
+ });
+ });
+
+ describe('createIssueFromAlertEnabled feature flag disabled', () => {
+ it('should display a button that links to a new issue page', () => {
+ mountComponent({ createIssueFromAlertEnabled: false });
+ expect(findCreatedIssueBtn().exists()).toBe(false);
+ });
+ });
+ });
+
+ describe('View full alert details', () => {
+ beforeEach(() => {
+ mountComponent({ data: { alert: mockAlert } });
+ });
+ it('should display a table of raw alert details data', () => {
+ wrapper.find('[data-testid="fullDetailsTab"]').trigger('click');
+ expect(findDetailsTable().exists()).toBe(true);
+ });
+ });
+
+ describe('loading state', () => {
+ beforeEach(() => {
+ mountComponent({ loading: true });
+ });
+
+ it('displays a loading state when loading', () => {
+ expect(wrapper.find(GlLoadingIcon).exists()).toBe(true);
+ });
+ });
+
+ describe('error state', () => {
+ it('displays a error state correctly', () => {
+ mountComponent({ data: { errored: true } });
+ expect(wrapper.find(GlAlert).exists()).toBe(true);
+ });
+
+ it('does not display an error when dismissed', () => {
+ mountComponent({ data: { errored: true, isErrorDismissed: true } });
+ expect(wrapper.find(GlAlert).exists()).toBe(false);
+ });
+ });
+
+ describe('header', () => {
+ const findHeader = () => wrapper.find('[data-testid="alert-header"]');
+ const stubs = { TimeAgoTooltip: '<span>now</span>' };
+
+ describe('individual header fields', () => {
+ describe.each`
+ severity | createdAt | monitoringTool | result
+ ${'MEDIUM'} | ${'2020-04-17T23:18:14.996Z'} | ${null} | ${'Medium • Reported now'}
+ ${'INFO'} | ${'2020-04-17T23:18:14.996Z'} | ${'Datadog'} | ${'Info • Reported now by Datadog'}
+ `(
+ `When severity=$severity, createdAt=$createdAt, monitoringTool=$monitoringTool`,
+ ({ severity, createdAt, monitoringTool, result }) => {
+ beforeEach(() => {
+ mountComponent({
+ data: { alert: { ...mockAlert, severity, createdAt, monitoringTool } },
+ mountMethod: mount,
+ stubs,
+ });
+ });
+
+ it('header text is shown correctly', () => {
+ expect(findHeader().text()).toBe(result);
+ });
+ },
+ );
+ });
+ });
+ });
+
+ describe('updating the alert status', () => {
+ const mockUpdatedMutationResult = {
+ data: {
+ updateAlertStatus: {
+ errors: [],
+ alert: {
+ status: 'acknowledged',
+ },
+ },
+ },
+ };
+
+ beforeEach(() => {
+ mountComponent({
+ props: { alertManagementEnabled: true, userCanEnableAlertManagement: true },
+ data: { alert: mockAlert },
+ loading: false,
+ });
+ });
+
+ it('calls `$apollo.mutate` with `updateAlertStatus` mutation and variables containing `iid`, `status`, & `projectPath`', () => {
+ jest.spyOn(wrapper.vm.$apollo, 'mutate').mockResolvedValue(mockUpdatedMutationResult);
+ findStatusDropdownItem().vm.$emit('click');
+
+ expect(wrapper.vm.$apollo.mutate).toHaveBeenCalledWith({
+ mutation: updateAlertStatus,
+ variables: {
+ iid: 'alertId',
+ status: 'TRIGGERED',
+ projectPath: 'projectPath',
+ },
+ });
+ });
+
+ it('calls `createFlash` when request fails', () => {
+ jest.spyOn(wrapper.vm.$apollo, 'mutate').mockReturnValue(Promise.reject(new Error()));
+ findStatusDropdownItem().vm.$emit('click');
+
+ setImmediate(() => {
+ expect(createFlash).toHaveBeenCalledWith(
+ 'There was an error while updating the status of the alert. Please try again.',
+ );
+ });
+ });
+ });
+});
diff --git a/spec/frontend/alert_management/components/alert_management_list_spec.js b/spec/frontend/alert_management/components/alert_management_list_spec.js
new file mode 100644
index 00000000000..c4630ac57fe
--- /dev/null
+++ b/spec/frontend/alert_management/components/alert_management_list_spec.js
@@ -0,0 +1,325 @@
+import { mount } from '@vue/test-utils';
+import {
+ GlEmptyState,
+ GlTable,
+ GlAlert,
+ GlLoadingIcon,
+ GlDropdown,
+ GlDropdownItem,
+ GlIcon,
+ GlTab,
+} from '@gitlab/ui';
+import { visitUrl } from '~/lib/utils/url_utility';
+import TimeAgo from '~/vue_shared/components/time_ago_tooltip.vue';
+import createFlash from '~/flash';
+import AlertManagementList from '~/alert_management/components/alert_management_list.vue';
+import { ALERTS_STATUS_TABS } from '../../../../app/assets/javascripts/alert_management/constants';
+import updateAlertStatus from '~/alert_management/graphql/mutations/update_alert_status.graphql';
+import mockAlerts from '../mocks/alerts.json';
+
+jest.mock('~/flash');
+
+jest.mock('~/lib/utils/url_utility', () => ({
+ visitUrl: jest.fn().mockName('visitUrlMock'),
+ joinPaths: jest.requireActual('~/lib/utils/url_utility').joinPaths,
+}));
+
+describe('AlertManagementList', () => {
+ let wrapper;
+
+ const findAlertsTable = () => wrapper.find(GlTable);
+ const findAlerts = () => wrapper.findAll('table tbody tr');
+ const findAlert = () => wrapper.find(GlAlert);
+ const findLoader = () => wrapper.find(GlLoadingIcon);
+ const findStatusDropdown = () => wrapper.find(GlDropdown);
+ const findStatusFilterTabs = () => wrapper.findAll(GlTab);
+ const findDateFields = () => wrapper.findAll(TimeAgo);
+ const findFirstStatusOption = () => findStatusDropdown().find(GlDropdownItem);
+ const findSeverityFields = () => wrapper.findAll('[data-testid="severityField"]');
+
+ function mountComponent({
+ props = {
+ alertManagementEnabled: false,
+ userCanEnableAlertManagement: false,
+ },
+ data = {},
+ loading = false,
+ alertListStatusFilteringEnabled = false,
+ stubs = {},
+ } = {}) {
+ wrapper = mount(AlertManagementList, {
+ propsData: {
+ projectPath: 'gitlab-org/gitlab',
+ enableAlertManagementPath: '/link',
+ emptyAlertSvgPath: 'illustration/path',
+ ...props,
+ },
+ provide: {
+ glFeatures: {
+ alertListStatusFilteringEnabled,
+ },
+ },
+ data() {
+ return data;
+ },
+ mocks: {
+ $apollo: {
+ mutate: jest.fn(),
+ queries: {
+ alerts: {
+ loading,
+ },
+ },
+ },
+ },
+ stubs,
+ });
+ }
+
+ beforeEach(() => {
+ mountComponent();
+ });
+
+ afterEach(() => {
+ if (wrapper) {
+ wrapper.destroy();
+ }
+ });
+
+ describe('alert management feature renders empty state', () => {
+ it('shows empty state', () => {
+ expect(wrapper.find(GlEmptyState).exists()).toBe(true);
+ });
+ });
+
+ describe('Status Filter Tabs', () => {
+ describe('alertListStatusFilteringEnabled feature flag enabled', () => {
+ beforeEach(() => {
+ mountComponent({
+ props: { alertManagementEnabled: true, userCanEnableAlertManagement: true },
+ data: { alerts: mockAlerts },
+ loading: false,
+ alertListStatusFilteringEnabled: true,
+ stubs: {
+ GlTab: true,
+ },
+ });
+ });
+
+ it('should display filter tabs for all statuses', () => {
+ const tabs = findStatusFilterTabs().wrappers;
+ tabs.forEach((tab, i) => {
+ expect(tab.text()).toContain(ALERTS_STATUS_TABS[i].title);
+ });
+ });
+ });
+
+ describe('alertListStatusFilteringEnabled feature flag disabled', () => {
+ beforeEach(() => {
+ mountComponent({
+ props: { alertManagementEnabled: true, userCanEnableAlertManagement: true },
+ data: { alerts: mockAlerts },
+ loading: false,
+ alertListStatusFilteringEnabled: false,
+ stubs: {
+ GlTab: true,
+ },
+ });
+ });
+
+ it('should NOT display tabs', () => {
+ expect(findStatusFilterTabs()).not.toExist();
+ });
+ });
+ });
+
+ describe('Alerts table', () => {
+ it('loading state', () => {
+ mountComponent({
+ props: { alertManagementEnabled: true, userCanEnableAlertManagement: true },
+ data: { alerts: null },
+ loading: true,
+ });
+ expect(findAlertsTable().exists()).toBe(true);
+ expect(findLoader().exists()).toBe(true);
+ });
+
+ it('error state', () => {
+ mountComponent({
+ props: { alertManagementEnabled: true, userCanEnableAlertManagement: true },
+ data: { alerts: null, errored: true },
+ loading: false,
+ });
+ expect(findAlertsTable().exists()).toBe(true);
+ expect(findAlertsTable().text()).toContain('No alerts to display');
+ expect(findLoader().exists()).toBe(false);
+ expect(findAlert().props().variant).toBe('danger');
+ });
+
+ it('empty state', () => {
+ mountComponent({
+ props: { alertManagementEnabled: true, userCanEnableAlertManagement: true },
+ data: { alerts: [], errored: false },
+ loading: false,
+ });
+ expect(findAlertsTable().exists()).toBe(true);
+ expect(findAlertsTable().text()).toContain('No alerts to display');
+ expect(findLoader().exists()).toBe(false);
+ expect(findAlert().props().variant).toBe('info');
+ });
+
+ it('has data state', () => {
+ mountComponent({
+ props: { alertManagementEnabled: true, userCanEnableAlertManagement: true },
+ data: { alerts: mockAlerts, errored: false },
+ loading: false,
+ });
+ expect(findLoader().exists()).toBe(false);
+ expect(findAlertsTable().exists()).toBe(true);
+ expect(findAlerts()).toHaveLength(mockAlerts.length);
+ });
+
+ it('displays status dropdown', () => {
+ mountComponent({
+ props: { alertManagementEnabled: true, userCanEnableAlertManagement: true },
+ data: { alerts: mockAlerts, errored: false },
+ loading: false,
+ });
+ expect(findStatusDropdown().exists()).toBe(true);
+ });
+
+ it('shows correct severity icons', () => {
+ mountComponent({
+ props: { alertManagementEnabled: true, userCanEnableAlertManagement: true },
+ data: { alerts: mockAlerts, errored: false },
+ loading: false,
+ });
+
+ return wrapper.vm.$nextTick().then(() => {
+ expect(wrapper.find(GlTable).exists()).toBe(true);
+ expect(
+ findAlertsTable()
+ .find(GlIcon)
+ .classes('icon-critical'),
+ ).toBe(true);
+ });
+ });
+
+ it('renders severity text', () => {
+ mountComponent({
+ props: { alertManagementEnabled: true, userCanEnableAlertManagement: true },
+ data: { alerts: mockAlerts, errored: false },
+ loading: false,
+ });
+
+ expect(
+ findSeverityFields()
+ .at(0)
+ .text(),
+ ).toBe('Critical');
+ });
+
+ it('navigates to the detail page when alert row is clicked', () => {
+ mountComponent({
+ props: { alertManagementEnabled: true, userCanEnableAlertManagement: true },
+ data: { alerts: mockAlerts, errored: false },
+ loading: false,
+ });
+
+ findAlerts()
+ .at(0)
+ .trigger('click');
+ expect(visitUrl).toHaveBeenCalledWith('/1527542/details');
+ });
+
+ describe('handle date fields', () => {
+ it('should display time ago dates when values provided', () => {
+ mountComponent({
+ props: { alertManagementEnabled: true, userCanEnableAlertManagement: true },
+ data: {
+ alerts: [
+ {
+ iid: 1,
+ status: 'acknowledged',
+ startedAt: '2020-03-17T23:18:14.996Z',
+ endedAt: '2020-04-17T23:18:14.996Z',
+ severity: 'high',
+ },
+ ],
+ errored: false,
+ },
+ loading: false,
+ });
+ expect(findDateFields().length).toBe(2);
+ });
+
+ it('should not display time ago dates when values not provided', () => {
+ mountComponent({
+ props: { alertManagementEnabled: true, userCanEnableAlertManagement: true },
+ data: {
+ alerts: [
+ {
+ iid: 1,
+ status: 'acknowledged',
+ startedAt: null,
+ endedAt: null,
+ severity: 'high',
+ },
+ ],
+ errored: false,
+ },
+ loading: false,
+ });
+ expect(findDateFields().exists()).toBe(false);
+ });
+ });
+ });
+
+ describe('updating the alert status', () => {
+ const iid = '1527542';
+ const mockUpdatedMutationResult = {
+ data: {
+ updateAlertStatus: {
+ errors: [],
+ alert: {
+ iid,
+ status: 'acknowledged',
+ },
+ },
+ },
+ };
+
+ beforeEach(() => {
+ mountComponent({
+ props: { alertManagementEnabled: true, userCanEnableAlertManagement: true },
+ data: { alerts: mockAlerts, errored: false },
+ loading: false,
+ });
+ });
+
+ it('calls `$apollo.mutate` with `updateAlertStatus` mutation and variables containing `iid`, `status`, & `projectPath`', () => {
+ jest.spyOn(wrapper.vm.$apollo, 'mutate').mockResolvedValue(mockUpdatedMutationResult);
+ findFirstStatusOption().vm.$emit('click');
+
+ expect(wrapper.vm.$apollo.mutate).toHaveBeenCalledWith({
+ mutation: updateAlertStatus,
+ variables: {
+ iid,
+ status: 'TRIGGERED',
+ projectPath: 'gitlab-org/gitlab',
+ },
+ });
+ });
+
+ it('calls `createFlash` when request fails', () => {
+ jest.spyOn(wrapper.vm.$apollo, 'mutate').mockReturnValue(Promise.reject(new Error()));
+ findFirstStatusOption().vm.$emit('click');
+
+ setImmediate(() => {
+ expect(createFlash).toHaveBeenCalledWith(
+ 'There was an error while updating the status of the alert. Please try again.',
+ );
+ });
+ });
+ });
+});
diff --git a/spec/frontend/alert_management/mocks/alerts.json b/spec/frontend/alert_management/mocks/alerts.json
new file mode 100644
index 00000000000..b67e2cfc52e
--- /dev/null
+++ b/spec/frontend/alert_management/mocks/alerts.json
@@ -0,0 +1,29 @@
+[
+ {
+ "iid": "1527542",
+ "title": "SyntaxError: Invalid or unexpected token",
+ "severity": "CRITICAL",
+ "eventCount": 7,
+ "startedAt": "2020-04-17T23:18:14.996Z",
+ "endedAt": "2020-04-17T23:18:14.996Z",
+ "status": "TRIGGERED"
+ },
+ {
+ "iid": "1527543",
+ "title": "Some other alert Some other alert Some other alert Some other alert Some other alert Some other alert",
+ "severity": "MEDIUM",
+ "eventCount": 1,
+ "startedAt": "2020-04-17T23:18:14.996Z",
+ "endedAt": "2020-04-17T23:18:14.996Z",
+ "status": "ACKNOWLEDGED"
+ },
+ {
+ "iid": "1527544",
+ "title": "SyntaxError: Invalid or unexpected token",
+ "severity": "LOW",
+ "eventCount": 4,
+ "startedAt": "2020-04-17T23:18:14.996Z",
+ "endedAt": "2020-04-17T23:18:14.996Z",
+ "status": "RESOLVED"
+ }
+ ]
diff --git a/spec/frontend/api_spec.js b/spec/frontend/api_spec.js
index f34c2fb69eb..d365048ab0b 100644
--- a/spec/frontend/api_spec.js
+++ b/spec/frontend/api_spec.js
@@ -15,7 +15,7 @@ describe('Api', () => {
beforeEach(() => {
mock = new MockAdapter(axios);
originalGon = window.gon;
- window.gon = Object.assign({}, dummyGon);
+ window.gon = { ...dummyGon };
});
afterEach(() => {
diff --git a/spec/frontend/autosave_spec.js b/spec/frontend/autosave_spec.js
index 3119477f385..bbdf3c6f91d 100644
--- a/spec/frontend/autosave_spec.js
+++ b/spec/frontend/autosave_spec.js
@@ -10,6 +10,8 @@ describe('Autosave', () => {
const field = $('<textarea></textarea>');
const key = 'key';
const fallbackKey = 'fallbackKey';
+ const lockVersionKey = 'lockVersionKey';
+ const lockVersion = 1;
describe('class constructor', () => {
beforeEach(() => {
@@ -30,6 +32,13 @@ describe('Autosave', () => {
expect(AccessorUtilities.isLocalStorageAccessSafe).toHaveBeenCalled();
expect(autosave.isLocalStorageAvailable).toBe(true);
});
+
+ it('should set .isLocalStorageAvailable if lockVersion is passed', () => {
+ autosave = new Autosave(field, key, null, lockVersion);
+
+ expect(AccessorUtilities.isLocalStorageAccessSafe).toHaveBeenCalled();
+ expect(autosave.isLocalStorageAvailable).toBe(true);
+ });
});
describe('restore', () => {
@@ -96,6 +105,40 @@ describe('Autosave', () => {
});
});
+ describe('getSavedLockVersion', () => {
+ beforeEach(() => {
+ autosave = {
+ field,
+ key,
+ lockVersionKey,
+ };
+ });
+
+ describe('if .isLocalStorageAvailable is `false`', () => {
+ beforeEach(() => {
+ autosave.isLocalStorageAvailable = false;
+
+ Autosave.prototype.getSavedLockVersion.call(autosave);
+ });
+
+ it('should not call .getItem', () => {
+ expect(window.localStorage.getItem).not.toHaveBeenCalled();
+ });
+ });
+
+ describe('if .isLocalStorageAvailable is `true`', () => {
+ beforeEach(() => {
+ autosave.isLocalStorageAvailable = true;
+ });
+
+ it('should call .getItem', () => {
+ Autosave.prototype.getSavedLockVersion.call(autosave);
+
+ expect(window.localStorage.getItem).toHaveBeenCalledWith(lockVersionKey);
+ });
+ });
+ });
+
describe('save', () => {
beforeEach(() => {
autosave = { reset: jest.fn() };
@@ -128,10 +171,51 @@ describe('Autosave', () => {
});
});
+ describe('save with lockVersion', () => {
+ beforeEach(() => {
+ autosave = {
+ field,
+ key,
+ lockVersionKey,
+ lockVersion,
+ isLocalStorageAvailable: true,
+ };
+ });
+
+ describe('lockVersion is valid', () => {
+ it('should call .setItem', () => {
+ Autosave.prototype.save.call(autosave);
+ expect(window.localStorage.setItem).toHaveBeenCalledWith(lockVersionKey, lockVersion);
+ });
+
+ it('should call .setItem when version is 0', () => {
+ autosave.lockVersion = 0;
+ Autosave.prototype.save.call(autosave);
+ expect(window.localStorage.setItem).toHaveBeenCalledWith(
+ lockVersionKey,
+ autosave.lockVersion,
+ );
+ });
+ });
+
+ describe('lockVersion is invalid', () => {
+ it('should not call .setItem with lockVersion', () => {
+ delete autosave.lockVersion;
+ Autosave.prototype.save.call(autosave);
+
+ expect(window.localStorage.setItem).not.toHaveBeenCalledWith(
+ lockVersionKey,
+ autosave.lockVersion,
+ );
+ });
+ });
+ });
+
describe('reset', () => {
beforeEach(() => {
autosave = {
key,
+ lockVersionKey,
};
});
@@ -156,6 +240,7 @@ describe('Autosave', () => {
it('should call .removeItem', () => {
expect(window.localStorage.removeItem).toHaveBeenCalledWith(key);
+ expect(window.localStorage.removeItem).toHaveBeenCalledWith(lockVersionKey);
});
});
});
@@ -166,8 +251,8 @@ describe('Autosave', () => {
field,
key,
fallbackKey,
+ isLocalStorageAvailable: true,
};
- autosave.isLocalStorageAvailable = true;
});
it('should call .getItem', () => {
@@ -185,7 +270,8 @@ describe('Autosave', () => {
it('should call .removeItem for key and fallbackKey', () => {
Autosave.prototype.reset.call(autosave);
- expect(window.localStorage.removeItem).toHaveBeenCalledTimes(2);
+ expect(window.localStorage.removeItem).toHaveBeenCalledWith(fallbackKey);
+ expect(window.localStorage.removeItem).toHaveBeenCalledWith(key);
});
});
});
diff --git a/spec/frontend/avatar_helper_spec.js b/spec/frontend/avatar_helper_spec.js
new file mode 100644
index 00000000000..c4da7189751
--- /dev/null
+++ b/spec/frontend/avatar_helper_spec.js
@@ -0,0 +1,110 @@
+import { TEST_HOST } from 'spec/test_constants';
+import { getFirstCharacterCapitalized } from '~/lib/utils/text_utility';
+import {
+ DEFAULT_SIZE_CLASS,
+ IDENTICON_BG_COUNT,
+ renderAvatar,
+ renderIdenticon,
+ getIdenticonBackgroundClass,
+ getIdenticonTitle,
+} from '~/helpers/avatar_helper';
+
+function matchAll(str) {
+ return new RegExp(`^${str}$`);
+}
+
+describe('avatar_helper', () => {
+ describe('getIdenticonBackgroundClass', () => {
+ it('returns identicon bg class from id that is a number', () => {
+ expect(getIdenticonBackgroundClass(1)).toEqual('bg2');
+ });
+
+ it('returns identicon bg class from id that is a string', () => {
+ expect(getIdenticonBackgroundClass('1')).toEqual('bg2');
+ });
+
+ it('returns identicon bg class from id that is a GraphQL string id', () => {
+ expect(getIdenticonBackgroundClass('gid://gitlab/Project/1')).toEqual('bg2');
+ });
+
+ it('returns identicon bg class from unparsable string', () => {
+ expect(getIdenticonBackgroundClass('gid://gitlab/')).toEqual('bg1');
+ });
+
+ it(`wraps around if id is bigger than ${IDENTICON_BG_COUNT}`, () => {
+ expect(getIdenticonBackgroundClass(IDENTICON_BG_COUNT + 4)).toEqual('bg5');
+ expect(getIdenticonBackgroundClass(IDENTICON_BG_COUNT * 5 + 6)).toEqual('bg7');
+ });
+ });
+
+ describe('getIdenticonTitle', () => {
+ it('returns identicon title from name', () => {
+ expect(getIdenticonTitle('Lorem')).toEqual('L');
+ expect(getIdenticonTitle('dolar-sit-amit')).toEqual('D');
+ expect(getIdenticonTitle('%-with-special-chars')).toEqual('%');
+ });
+
+ it('returns space if name is falsey', () => {
+ expect(getIdenticonTitle('')).toEqual(' ');
+ expect(getIdenticonTitle(null)).toEqual(' ');
+ });
+ });
+
+ describe('renderIdenticon', () => {
+ it('renders with the first letter as title and bg based on id', () => {
+ const entity = {
+ id: IDENTICON_BG_COUNT + 3,
+ name: 'Xavior',
+ };
+ const options = {
+ sizeClass: 's32',
+ };
+
+ const result = renderIdenticon(entity, options);
+
+ expect(result).toHaveClass(`identicon ${options.sizeClass} bg4`);
+ expect(result).toHaveText(matchAll(getFirstCharacterCapitalized(entity.name)));
+ });
+
+ it('renders with defaults, if no options are given', () => {
+ const entity = {
+ id: 1,
+ name: 'tanuki',
+ };
+
+ const result = renderIdenticon(entity);
+
+ expect(result).toHaveClass(`identicon ${DEFAULT_SIZE_CLASS} bg2`);
+ expect(result).toHaveText(matchAll(getFirstCharacterCapitalized(entity.name)));
+ });
+ });
+
+ describe('renderAvatar', () => {
+ it('renders an image with the avatarUrl', () => {
+ const avatarUrl = `${TEST_HOST}/not-real-assets/test.png`;
+
+ const result = renderAvatar({
+ avatar_url: avatarUrl,
+ });
+
+ expect(result).toBeMatchedBy('img');
+ expect(result).toHaveAttr('src', avatarUrl);
+ expect(result).toHaveClass(DEFAULT_SIZE_CLASS);
+ });
+
+ it('renders an identicon if no avatarUrl', () => {
+ const entity = {
+ id: 1,
+ name: 'walrus',
+ };
+ const options = {
+ sizeClass: 's16',
+ };
+
+ const result = renderAvatar(entity, options);
+
+ expect(result).toHaveClass(`identicon ${options.sizeClass} bg2`);
+ expect(result).toHaveText(matchAll(getFirstCharacterCapitalized(entity.name)));
+ });
+ });
+});
diff --git a/spec/frontend/behaviors/markdown/paste_markdown_table_spec.js b/spec/frontend/behaviors/markdown/paste_markdown_table_spec.js
index a98919e2113..eab805382bd 100644
--- a/spec/frontend/behaviors/markdown/paste_markdown_table_spec.js
+++ b/spec/frontend/behaviors/markdown/paste_markdown_table_spec.js
@@ -57,6 +57,18 @@ describe('PasteMarkdownTable', () => {
expect(new PasteMarkdownTable(data).isTable()).toBe(false);
});
+
+ it('returns false when the table copy comes from a diff', () => {
+ data.types = ['text/html', 'text/plain'];
+ data.getData = jest.fn().mockImplementation(mimeType => {
+ if (mimeType === 'text/html') {
+ return '<table class="diff-wrap-lines"><tr><td>First</td><td>Second</td></tr></table>';
+ }
+ return 'First\tSecond';
+ });
+
+ expect(new PasteMarkdownTable(data).isTable()).toBe(false);
+ });
});
describe('convertToTableMarkdown', () => {
diff --git a/spec/frontend/behaviors/markdown/render_metrics_spec.js b/spec/frontend/behaviors/markdown/render_metrics_spec.js
index 3f7beeb817b..ab81ed6b8f0 100644
--- a/spec/frontend/behaviors/markdown/render_metrics_spec.js
+++ b/spec/frontend/behaviors/markdown/render_metrics_spec.js
@@ -11,20 +11,20 @@ const getElements = () => Array.from(document.getElementsByClassName('js-render-
describe('Render metrics for Gitlab Flavoured Markdown', () => {
it('does nothing when no elements are found', () => {
- renderMetrics([]);
-
- expect(mockEmbedGroup).not.toHaveBeenCalled();
+ return renderMetrics([]).then(() => {
+ expect(mockEmbedGroup).not.toHaveBeenCalled();
+ });
});
it('renders a vue component when elements are found', () => {
document.body.innerHTML = `<div class="js-render-metrics" data-dashboard-url="${TEST_HOST}"></div>`;
- renderMetrics(getElements());
-
- expect(mockEmbedGroup).toHaveBeenCalledTimes(1);
- expect(mockEmbedGroup).toHaveBeenCalledWith(
- expect.objectContaining({ propsData: { urls: [`${TEST_HOST}`] } }),
- );
+ return renderMetrics(getElements()).then(() => {
+ expect(mockEmbedGroup).toHaveBeenCalledTimes(1);
+ expect(mockEmbedGroup).toHaveBeenCalledWith(
+ expect.objectContaining({ propsData: { urls: [`${TEST_HOST}`] } }),
+ );
+ });
});
it('takes sibling metrics and groups them under a shared parent', () => {
@@ -36,14 +36,14 @@ describe('Render metrics for Gitlab Flavoured Markdown', () => {
<div class="js-render-metrics" data-dashboard-url="${TEST_HOST}/3"></div>
`;
- renderMetrics(getElements());
-
- expect(mockEmbedGroup).toHaveBeenCalledTimes(2);
- expect(mockEmbedGroup).toHaveBeenCalledWith(
- expect.objectContaining({ propsData: { urls: [`${TEST_HOST}/1`, `${TEST_HOST}/2`] } }),
- );
- expect(mockEmbedGroup).toHaveBeenCalledWith(
- expect.objectContaining({ propsData: { urls: [`${TEST_HOST}/3`] } }),
- );
+ return renderMetrics(getElements()).then(() => {
+ expect(mockEmbedGroup).toHaveBeenCalledTimes(2);
+ expect(mockEmbedGroup).toHaveBeenCalledWith(
+ expect.objectContaining({ propsData: { urls: [`${TEST_HOST}/1`, `${TEST_HOST}/2`] } }),
+ );
+ expect(mockEmbedGroup).toHaveBeenCalledWith(
+ expect.objectContaining({ propsData: { urls: [`${TEST_HOST}/3`] } }),
+ );
+ });
});
});
diff --git a/spec/frontend/blob/components/__snapshots__/blob_edit_header_spec.js.snap b/spec/frontend/blob/components/__snapshots__/blob_edit_header_spec.js.snap
index e47a7dcfa2a..1e639f91797 100644
--- a/spec/frontend/blob/components/__snapshots__/blob_edit_header_spec.js.snap
+++ b/spec/frontend/blob/components/__snapshots__/blob_edit_header_spec.js.snap
@@ -5,7 +5,7 @@ exports[`Blob Header Editing rendering matches the snapshot 1`] = `
class="js-file-title file-title-flex-parent"
>
<gl-form-input-stub
- class="form-control js-snippet-file-name qa-snippet-file-name"
+ class="form-control js-snippet-file-name"
id="snippet_file_name"
name="snippet_file_name"
placeholder="Give your file a name to add code highlighting, e.g. example.rb for Ruby"
diff --git a/spec/frontend/blob/components/__snapshots__/blob_header_filepath_spec.js.snap b/spec/frontend/blob/components/__snapshots__/blob_header_filepath_spec.js.snap
index 7382a3a4cf7..2ac6e0d5d24 100644
--- a/spec/frontend/blob/components/__snapshots__/blob_header_filepath_spec.js.snap
+++ b/spec/frontend/blob/components/__snapshots__/blob_header_filepath_spec.js.snap
@@ -8,14 +8,15 @@ exports[`Blob Header Filepath rendering matches the snapshot 1`] = `
<file-icon-stub
aria-hidden="true"
cssclasses="mr-2"
- filename="dummy.md"
+ filename="foo/bar/dummy.md"
size="18"
/>
<strong
- class="file-title-name qa-file-title-name mr-1 js-blob-header-filepath"
+ class="file-title-name mr-1 js-blob-header-filepath"
+ data-qa-selector="file_title_name"
>
- dummy.md
+ foo/bar/dummy.md
</strong>
<small
@@ -26,8 +27,8 @@ exports[`Blob Header Filepath rendering matches the snapshot 1`] = `
<clipboard-button-stub
cssclass="btn-clipboard btn-transparent lh-100 position-static"
- gfm="\`dummy.md\`"
- text="dummy.md"
+ gfm="\`foo/bar/dummy.md\`"
+ text="foo/bar/dummy.md"
title="Copy file path"
tooltipplacement="top"
/>
diff --git a/spec/frontend/blob/components/__snapshots__/blob_header_spec.js.snap b/spec/frontend/blob/components/__snapshots__/blob_header_spec.js.snap
index 2878ad492a4..7d868625956 100644
--- a/spec/frontend/blob/components/__snapshots__/blob_header_spec.js.snap
+++ b/spec/frontend/blob/components/__snapshots__/blob_header_spec.js.snap
@@ -9,7 +9,7 @@ exports[`Blob Header Default Actions rendering matches the snapshot 1`] = `
/>
<div
- class="file-actions d-none d-sm-block"
+ class="file-actions d-none d-sm-flex"
>
<viewer-switcher-stub
value="simple"
diff --git a/spec/frontend/blob/components/blob_content_error_spec.js b/spec/frontend/blob/components/blob_content_error_spec.js
index 58a9ee761df..6eb5cfb71aa 100644
--- a/spec/frontend/blob/components/blob_content_error_spec.js
+++ b/spec/frontend/blob/components/blob_content_error_spec.js
@@ -1,27 +1,60 @@
import { shallowMount } from '@vue/test-utils';
import BlobContentError from '~/blob/components/blob_content_error.vue';
+import { GlSprintf } from '@gitlab/ui';
+
+import { BLOB_RENDER_ERRORS } from '~/blob/components/constants';
describe('Blob Content Error component', () => {
let wrapper;
- const viewerError = '<h1 id="error">Foo Error</h1>';
- function createComponent() {
+ function createComponent(props = {}) {
wrapper = shallowMount(BlobContentError, {
propsData: {
- viewerError,
+ ...props,
+ },
+ stubs: {
+ GlSprintf,
},
});
}
- beforeEach(() => {
- createComponent();
- });
-
afterEach(() => {
wrapper.destroy();
});
- it('renders the passed error without transformations', () => {
- expect(wrapper.html()).toContain(viewerError);
+ describe('collapsed and too large blobs', () => {
+ it.each`
+ error | reason | options
+ ${BLOB_RENDER_ERRORS.REASONS.COLLAPSED} | ${'it is larger than 1.00 MiB'} | ${[BLOB_RENDER_ERRORS.OPTIONS.LOAD.text, BLOB_RENDER_ERRORS.OPTIONS.DOWNLOAD.text]}
+ ${BLOB_RENDER_ERRORS.REASONS.TOO_LARGE} | ${'it is larger than 100.00 MiB'} | ${[BLOB_RENDER_ERRORS.OPTIONS.DOWNLOAD.text]}
+ `('renders correct reason for $error.id', ({ error, reason, options }) => {
+ createComponent({
+ viewerError: error.id,
+ });
+ expect(wrapper.text()).toContain(reason);
+ options.forEach(option => {
+ expect(wrapper.text()).toContain(option);
+ });
+ });
+ });
+
+ describe('external blob', () => {
+ it.each`
+ storageType | reason | options
+ ${'lfs'} | ${BLOB_RENDER_ERRORS.REASONS.EXTERNAL.text.lfs} | ${[BLOB_RENDER_ERRORS.OPTIONS.DOWNLOAD.text]}
+ ${'build_artifact'} | ${BLOB_RENDER_ERRORS.REASONS.EXTERNAL.text.build_artifact} | ${[BLOB_RENDER_ERRORS.OPTIONS.DOWNLOAD.text]}
+ ${'default'} | ${BLOB_RENDER_ERRORS.REASONS.EXTERNAL.text.default} | ${[BLOB_RENDER_ERRORS.OPTIONS.DOWNLOAD.text]}
+ `('renders correct reason for $storageType blob', ({ storageType, reason, options }) => {
+ createComponent({
+ viewerError: BLOB_RENDER_ERRORS.REASONS.EXTERNAL.id,
+ blob: {
+ externalStorage: storageType,
+ },
+ });
+ expect(wrapper.text()).toContain(reason);
+ options.forEach(option => {
+ expect(wrapper.text()).toContain(option);
+ });
+ });
});
});
diff --git a/spec/frontend/blob/components/blob_content_spec.js b/spec/frontend/blob/components/blob_content_spec.js
index 6a130c9c43d..244ed41869d 100644
--- a/spec/frontend/blob/components/blob_content_spec.js
+++ b/spec/frontend/blob/components/blob_content_spec.js
@@ -2,6 +2,12 @@ import { shallowMount } from '@vue/test-utils';
import BlobContent from '~/blob/components/blob_content.vue';
import BlobContentError from '~/blob/components/blob_content_error.vue';
import {
+ BLOB_RENDER_EVENT_LOAD,
+ BLOB_RENDER_EVENT_SHOW_SOURCE,
+ BLOB_RENDER_ERRORS,
+} from '~/blob/components/constants';
+import {
+ Blob,
RichViewerMock,
SimpleViewerMock,
RichBlobContentMock,
@@ -38,7 +44,7 @@ describe('Blob Content component', () => {
it('renders error if there is any in the viewer', () => {
const renderError = 'Oops';
- const viewer = Object.assign({}, SimpleViewerMock, { renderError });
+ const viewer = { ...SimpleViewerMock, renderError };
createComponent({}, viewer);
expect(wrapper.contains(GlLoadingIcon)).toBe(false);
expect(wrapper.contains(BlobContentError)).toBe(true);
@@ -67,4 +73,32 @@ describe('Blob Content component', () => {
expect(wrapper.find(viewer).html()).toContain(content);
});
});
+
+ describe('functionality', () => {
+ describe('render error', () => {
+ const findErrorEl = () => wrapper.find(BlobContentError);
+ const renderError = BLOB_RENDER_ERRORS.REASONS.COLLAPSED.id;
+ const viewer = { ...SimpleViewerMock, renderError };
+
+ beforeEach(() => {
+ createComponent({ blob: Blob }, viewer);
+ });
+
+ it('correctly sets blob on the blob-content-error component', () => {
+ expect(findErrorEl().props('blob')).toEqual(Blob);
+ });
+
+ it(`properly proxies ${BLOB_RENDER_EVENT_LOAD} event`, () => {
+ expect(wrapper.emitted(BLOB_RENDER_EVENT_LOAD)).toBeUndefined();
+ findErrorEl().vm.$emit(BLOB_RENDER_EVENT_LOAD);
+ expect(wrapper.emitted(BLOB_RENDER_EVENT_LOAD)).toBeTruthy();
+ });
+
+ it(`properly proxies ${BLOB_RENDER_EVENT_SHOW_SOURCE} event`, () => {
+ expect(wrapper.emitted(BLOB_RENDER_EVENT_SHOW_SOURCE)).toBeUndefined();
+ findErrorEl().vm.$emit(BLOB_RENDER_EVENT_SHOW_SOURCE);
+ expect(wrapper.emitted(BLOB_RENDER_EVENT_SHOW_SOURCE)).toBeTruthy();
+ });
+ });
+ });
});
diff --git a/spec/frontend/blob/components/blob_header_filepath_spec.js b/spec/frontend/blob/components/blob_header_filepath_spec.js
index d029ba2a7a4..3a53208f357 100644
--- a/spec/frontend/blob/components/blob_header_filepath_spec.js
+++ b/spec/frontend/blob/components/blob_header_filepath_spec.js
@@ -15,7 +15,7 @@ describe('Blob Header Filepath', () => {
function createComponent(blobProps = {}, options = {}) {
wrapper = shallowMount(BlobHeaderFilepath, {
propsData: {
- blob: Object.assign({}, MockBlob, blobProps),
+ blob: { ...MockBlob, ...blobProps },
},
...options,
});
@@ -38,12 +38,12 @@ describe('Blob Header Filepath', () => {
.find('.js-blob-header-filepath')
.text()
.trim(),
- ).toBe(MockBlob.name);
+ ).toBe(MockBlob.path);
});
it('does not fail if the name is empty', () => {
- const emptyName = '';
- createComponent({ name: emptyName });
+ const emptyPath = '';
+ createComponent({ path: emptyPath });
expect(wrapper.find('.js-blob-header-filepath').exists()).toBe(false);
});
@@ -84,7 +84,7 @@ describe('Blob Header Filepath', () => {
describe('functionality', () => {
it('sets gfm value correctly on the clipboard-button', () => {
createComponent();
- expect(wrapper.vm.gfmCopyText).toBe('`dummy.md`');
+ expect(wrapper.vm.gfmCopyText).toBe(`\`${MockBlob.path}\``);
});
});
});
diff --git a/spec/frontend/blob/components/blob_header_spec.js b/spec/frontend/blob/components/blob_header_spec.js
index d410ef10fc9..0e7d2f6516a 100644
--- a/spec/frontend/blob/components/blob_header_spec.js
+++ b/spec/frontend/blob/components/blob_header_spec.js
@@ -13,7 +13,7 @@ describe('Blob Header Default Actions', () => {
const method = shouldMount ? mount : shallowMount;
wrapper = method.call(this, BlobHeader, {
propsData: {
- blob: Object.assign({}, Blob, blobProps),
+ blob: { ...Blob, ...blobProps },
...propsData,
},
...options,
diff --git a/spec/frontend/blob/components/mock_data.js b/spec/frontend/blob/components/mock_data.js
index bfcca14324f..0f7193846ff 100644
--- a/spec/frontend/blob/components/mock_data.js
+++ b/spec/frontend/blob/components/mock_data.js
@@ -21,7 +21,7 @@ export const RichViewerMock = {
export const Blob = {
binary: false,
name: 'dummy.md',
- path: 'dummy.md',
+ path: 'foo/bar/dummy.md',
rawPath: '/flightjs/flight/snippets/51/raw',
size: 75,
simpleViewer: {
diff --git a/spec/frontend/blob/pipeline_tour_success_modal_spec.js b/spec/frontend/blob/pipeline_tour_success_modal_spec.js
index 99940225652..6d4e5e46cb8 100644
--- a/spec/frontend/blob/pipeline_tour_success_modal_spec.js
+++ b/spec/frontend/blob/pipeline_tour_success_modal_spec.js
@@ -12,8 +12,8 @@ describe('PipelineTourSuccessModal', () => {
beforeEach(() => {
document.body.dataset.page = 'projects:blob:show';
-
trackingSpy = mockTracking('_category_', undefined, jest.spyOn);
+
wrapper = shallowMount(pipelineTourSuccess, {
propsData: modalProps,
stubs: {
diff --git a/spec/frontend/blob/suggest_gitlab_ci_yml/components/popover_spec.js b/spec/frontend/blob/suggest_gitlab_ci_yml/components/popover_spec.js
index fb0964a3f32..3c03e6f04ab 100644
--- a/spec/frontend/blob/suggest_gitlab_ci_yml/components/popover_spec.js
+++ b/spec/frontend/blob/suggest_gitlab_ci_yml/components/popover_spec.js
@@ -69,8 +69,10 @@ describe('Suggest gitlab-ci.yml Popover', () => {
let trackingSpy;
beforeEach(() => {
+ document.body.dataset.page = 'projects:blob:new';
+ trackingSpy = mockTracking('_category_', undefined, jest.spyOn);
+
createWrapper(commitTrackLabel);
- trackingSpy = mockTracking('_category_', wrapper.element, jest.spyOn);
});
afterEach(() => {
@@ -83,10 +85,6 @@ describe('Suggest gitlab-ci.yml Popover', () => {
const expectedLabel = 'suggest_commit_first_project_gitlab_ci_yml';
const expectedProperty = 'owner';
- document.body.dataset.page = 'projects:blob:new';
-
- wrapper.vm.trackOnShow();
-
expect(trackingSpy).toHaveBeenCalledWith(expectedCategory, expectedAction, {
label: expectedLabel,
property: expectedProperty,
@@ -99,6 +97,7 @@ describe('Suggest gitlab-ci.yml Popover', () => {
const expectedProperty = 'owner';
const expectedValue = '10';
const dismissButton = wrapper.find(GlDeprecatedButton);
+ trackingSpy = mockTracking('_category_', wrapper.element, jest.spyOn);
triggerEvent(dismissButton.element);
diff --git a/spec/frontend/blob/utils_spec.js b/spec/frontend/blob/utils_spec.js
index 39a73aae444..119ed2dfe7a 100644
--- a/spec/frontend/blob/utils_spec.js
+++ b/spec/frontend/blob/utils_spec.js
@@ -8,11 +8,6 @@ jest.mock('~/editor/editor_lite', () => {
});
});
-const mockCreateAceInstance = jest.fn();
-global.ace = {
- edit: mockCreateAceInstance,
-};
-
describe('Blob utilities', () => {
beforeEach(() => {
Editor.mockClear();
@@ -29,21 +24,6 @@ describe('Blob utilities', () => {
});
describe('Monaco editor', () => {
- let origProp;
-
- beforeEach(() => {
- origProp = window.gon;
- window.gon = {
- features: {
- monacoSnippets: true,
- },
- };
- });
-
- afterEach(() => {
- window.gon = origProp;
- });
-
it('initializes the Editor Lite', () => {
utils.initEditorLite({ el: editorEl });
expect(Editor).toHaveBeenCalled();
@@ -69,27 +49,5 @@ describe('Blob utilities', () => {
]);
});
});
- describe('ACE editor', () => {
- let origProp;
-
- beforeEach(() => {
- origProp = window.gon;
- window.gon = {
- features: {
- monacoSnippets: false,
- },
- };
- });
-
- afterEach(() => {
- window.gon = origProp;
- });
-
- it('does not initialize the Editor Lite', () => {
- utils.initEditorLite({ el: editorEl });
- expect(Editor).not.toHaveBeenCalled();
- expect(mockCreateAceInstance).toHaveBeenCalledWith(editorEl);
- });
- });
});
});
diff --git a/spec/frontend/boards/board_list_spec.js b/spec/frontend/boards/board_list_spec.js
index 882310030f8..fa21053e2de 100644
--- a/spec/frontend/boards/board_list_spec.js
+++ b/spec/frontend/boards/board_list_spec.js
@@ -64,7 +64,7 @@ describe('Board list component', () => {
let getIssues;
function generateIssues(compWrapper) {
for (let i = 1; i < 20; i += 1) {
- const issue = Object.assign({}, compWrapper.list.issues[0]);
+ const issue = { ...compWrapper.list.issues[0] };
issue.id += i;
compWrapper.list.issues.push(issue);
}
diff --git a/spec/frontend/boards/boards_store_spec.js b/spec/frontend/boards/boards_store_spec.js
index 5c5315fd465..29cc8f981bd 100644
--- a/spec/frontend/boards/boards_store_spec.js
+++ b/spec/frontend/boards/boards_store_spec.js
@@ -214,6 +214,22 @@ describe('boardsStore', () => {
});
});
+ describe('getListIssues', () => {
+ let list;
+
+ beforeEach(() => {
+ list = new List(listObj);
+ setupDefaultResponses();
+ });
+
+ it('makes a request to get issues', () => {
+ const expectedResponse = expect.objectContaining({ issues: [createTestIssue()] });
+ expect(list.issues).toEqual([]);
+
+ return expect(boardsStore.getListIssues(list, true)).resolves.toEqual(expectedResponse);
+ });
+ });
+
describe('getIssuesForList', () => {
const id = 'TOO-MUCH';
const url = `${endpoints.listsEndpoint}/${id}/issues?id=${id}`;
@@ -1040,5 +1056,126 @@ describe('boardsStore', () => {
});
});
});
+
+ describe('addListIssue', () => {
+ let list;
+ const issue1 = new ListIssue({
+ title: 'Testing',
+ id: 2,
+ iid: 2,
+ confidential: false,
+ labels: [
+ {
+ color: '#ff0000',
+ description: 'testing;',
+ id: 5000,
+ priority: undefined,
+ textColor: 'white',
+ title: 'Test',
+ },
+ ],
+ assignees: [],
+ });
+ const issue2 = new ListIssue({
+ title: 'Testing',
+ id: 1,
+ iid: 1,
+ confidential: false,
+ labels: [
+ {
+ id: 1,
+ title: 'test',
+ color: 'red',
+ description: 'testing',
+ },
+ ],
+ assignees: [
+ {
+ id: 1,
+ name: 'name',
+ username: 'username',
+ avatar_url: 'http://avatar_url',
+ },
+ ],
+ real_path: 'path/to/issue',
+ });
+
+ beforeEach(() => {
+ list = new List(listObj);
+ list.addIssue(issue1);
+ setupDefaultResponses();
+ });
+
+ it('adds issues that are not already on the list', () => {
+ expect(list.findIssue(issue2.id)).toBe(undefined);
+ expect(list.issues).toEqual([issue1]);
+
+ boardsStore.addListIssue(list, issue2);
+ expect(list.findIssue(issue2.id)).toBe(issue2);
+ expect(list.issues.length).toBe(2);
+ expect(list.issues).toEqual([issue1, issue2]);
+ });
+ });
+
+ describe('updateIssue', () => {
+ let issue;
+ let patchSpy;
+
+ beforeEach(() => {
+ issue = new ListIssue({
+ title: 'Testing',
+ id: 1,
+ iid: 1,
+ confidential: false,
+ labels: [
+ {
+ id: 1,
+ title: 'test',
+ color: 'red',
+ description: 'testing',
+ },
+ ],
+ assignees: [
+ {
+ id: 1,
+ name: 'name',
+ username: 'username',
+ avatar_url: 'http://avatar_url',
+ },
+ ],
+ real_path: 'path/to/issue',
+ });
+
+ patchSpy = jest.fn().mockReturnValue([200, { labels: [] }]);
+ axiosMock.onPatch(`path/to/issue.json`).reply(({ data }) => patchSpy(JSON.parse(data)));
+ });
+
+ it('passes assignee ids when there are assignees', () => {
+ boardsStore.updateIssue(issue);
+ return boardsStore.updateIssue(issue).then(() => {
+ expect(patchSpy).toHaveBeenCalledWith({
+ issue: {
+ milestone_id: null,
+ assignee_ids: [1],
+ label_ids: [1],
+ },
+ });
+ });
+ });
+
+ it('passes assignee ids of [0] when there are no assignees', () => {
+ issue.removeAllAssignees();
+
+ return boardsStore.updateIssue(issue).then(() => {
+ expect(patchSpy).toHaveBeenCalledWith({
+ issue: {
+ milestone_id: null,
+ assignee_ids: [0],
+ label_ids: [1],
+ },
+ });
+ });
+ });
+ });
});
});
diff --git a/spec/frontend/boards/issue_spec.js b/spec/frontend/boards/issue_spec.js
index ff72edaa695..412f20684f5 100644
--- a/spec/frontend/boards/issue_spec.js
+++ b/spec/frontend/boards/issue_spec.js
@@ -1,6 +1,5 @@
/* global ListIssue */
-import axios from '~/lib/utils/axios_utils';
import '~/boards/models/label';
import '~/boards/models/assignee';
import '~/boards/models/issue';
@@ -173,25 +172,12 @@ describe('Issue model', () => {
});
describe('update', () => {
- it('passes assignee ids when there are assignees', done => {
- jest.spyOn(axios, 'patch').mockImplementation((url, data) => {
- expect(data.issue.assignee_ids).toEqual([1]);
- done();
- return Promise.resolve();
- });
-
- issue.update('url');
- });
+ it('passes update to boardsStore', () => {
+ jest.spyOn(boardsStore, 'updateIssue').mockImplementation();
- it('passes assignee ids of [0] when there are no assignees', done => {
- jest.spyOn(axios, 'patch').mockImplementation((url, data) => {
- expect(data.issue.assignee_ids).toEqual([0]);
- done();
- return Promise.resolve();
- });
+ issue.update();
- issue.removeAllAssignees();
- issue.update('url');
+ expect(boardsStore.updateIssue).toHaveBeenCalledWith(issue);
});
});
});
diff --git a/spec/frontend/bootstrap_linked_tabs_spec.js b/spec/frontend/bootstrap_linked_tabs_spec.js
new file mode 100644
index 00000000000..2d8939e6480
--- /dev/null
+++ b/spec/frontend/bootstrap_linked_tabs_spec.js
@@ -0,0 +1,67 @@
+import LinkedTabs from '~/lib/utils/bootstrap_linked_tabs';
+
+describe('Linked Tabs', () => {
+ preloadFixtures('static/linked_tabs.html');
+
+ beforeEach(() => {
+ loadFixtures('static/linked_tabs.html');
+ });
+
+ describe('when is initialized', () => {
+ beforeEach(() => {
+ jest.spyOn(window.history, 'replaceState').mockImplementation(() => {});
+ });
+
+ it('should activate the tab correspondent to the given action', () => {
+ // eslint-disable-next-line no-new
+ new LinkedTabs({
+ action: 'tab1',
+ defaultAction: 'tab1',
+ parentEl: '.linked-tabs',
+ });
+
+ expect(document.querySelector('#tab1').classList).toContain('active');
+ });
+
+ it('should active the default tab action when the action is show', () => {
+ // eslint-disable-next-line no-new
+ new LinkedTabs({
+ action: 'show',
+ defaultAction: 'tab1',
+ parentEl: '.linked-tabs',
+ });
+
+ expect(document.querySelector('#tab1').classList).toContain('active');
+ });
+ });
+
+ describe('on click', () => {
+ it('should change the url according to the clicked tab', () => {
+ const historySpy = jest.spyOn(window.history, 'replaceState').mockImplementation(() => {});
+
+ const linkedTabs = new LinkedTabs({
+ action: 'show',
+ defaultAction: 'tab1',
+ parentEl: '.linked-tabs',
+ });
+
+ const secondTab = document.querySelector('.linked-tabs li:nth-child(2) a');
+ const newState =
+ secondTab.getAttribute('href') +
+ linkedTabs.currentLocation.search +
+ linkedTabs.currentLocation.hash;
+
+ secondTab.click();
+
+ if (historySpy) {
+ expect(historySpy).toHaveBeenCalledWith(
+ {
+ url: newState,
+ },
+ document.title,
+ newState,
+ );
+ }
+ });
+ });
+});
diff --git a/spec/frontend/broadcast_notification_spec.js b/spec/frontend/broadcast_notification_spec.js
new file mode 100644
index 00000000000..8d433946632
--- /dev/null
+++ b/spec/frontend/broadcast_notification_spec.js
@@ -0,0 +1,35 @@
+import Cookies from 'js-cookie';
+import initBroadcastNotifications from '~/broadcast_notification';
+
+describe('broadcast message on dismiss', () => {
+ const dismiss = () => {
+ const button = document.querySelector('.js-dismiss-current-broadcast-notification');
+ button.click();
+ };
+ const endsAt = '2020-01-01T00:00:00Z';
+
+ beforeEach(() => {
+ setFixtures(`
+ <div class="js-broadcast-notification-1">
+ <button class="js-dismiss-current-broadcast-notification" data-id="1" data-expire-date="${endsAt}"></button>
+ </div>
+ `);
+
+ initBroadcastNotifications();
+ });
+
+ it('removes broadcast message', () => {
+ dismiss();
+
+ expect(document.querySelector('.js-broadcast-notification-1')).toBeNull();
+ });
+
+ it('calls Cookies.set', () => {
+ jest.spyOn(Cookies, 'set');
+ dismiss();
+
+ expect(Cookies.set).toHaveBeenCalledWith('hide_broadcast_message_1', true, {
+ expires: new Date(endsAt),
+ });
+ });
+});
diff --git a/spec/frontend/ci_variable_list/ci_variable_list/ajax_variable_list_spec.js b/spec/frontend/ci_variable_list/ci_variable_list/ajax_variable_list_spec.js
new file mode 100644
index 00000000000..93b185bd242
--- /dev/null
+++ b/spec/frontend/ci_variable_list/ci_variable_list/ajax_variable_list_spec.js
@@ -0,0 +1,203 @@
+import $ from 'jquery';
+import MockAdapter from 'axios-mock-adapter';
+import axios from '~/lib/utils/axios_utils';
+import AjaxFormVariableList from '~/ci_variable_list/ajax_variable_list';
+
+const VARIABLE_PATCH_ENDPOINT = 'http://test.host/frontend-fixtures/builds-project/-/variables';
+const HIDE_CLASS = 'hide';
+
+describe('AjaxFormVariableList', () => {
+ preloadFixtures('projects/ci_cd_settings.html');
+ preloadFixtures('projects/ci_cd_settings_with_variables.html');
+
+ let container;
+ let saveButton;
+ let errorBox;
+
+ let mock;
+ let ajaxVariableList;
+
+ beforeEach(() => {
+ loadFixtures('projects/ci_cd_settings.html');
+ container = document.querySelector('.js-ci-variable-list-section');
+
+ mock = new MockAdapter(axios);
+
+ const ajaxVariableListEl = document.querySelector('.js-ci-variable-list-section');
+ saveButton = ajaxVariableListEl.querySelector('.js-ci-variables-save-button');
+ errorBox = container.querySelector('.js-ci-variable-error-box');
+ ajaxVariableList = new AjaxFormVariableList({
+ container,
+ formField: 'variables',
+ saveButton,
+ errorBox,
+ saveEndpoint: container.dataset.saveEndpoint,
+ maskableRegex: container.dataset.maskableRegex,
+ });
+
+ jest.spyOn(ajaxVariableList, 'updateRowsWithPersistedVariables');
+ jest.spyOn(ajaxVariableList.variableList, 'toggleEnableRow');
+ });
+
+ afterEach(() => {
+ mock.restore();
+ });
+
+ describe('onSaveClicked', () => {
+ it('shows loading spinner while waiting for the request', () => {
+ const loadingIcon = saveButton.querySelector('.js-ci-variables-save-loading-icon');
+
+ mock.onPatch(VARIABLE_PATCH_ENDPOINT).reply(() => {
+ expect(loadingIcon.classList.contains(HIDE_CLASS)).toEqual(false);
+
+ return [200, {}];
+ });
+
+ expect(loadingIcon.classList.contains(HIDE_CLASS)).toEqual(true);
+
+ return ajaxVariableList.onSaveClicked().then(() => {
+ expect(loadingIcon.classList.contains(HIDE_CLASS)).toEqual(true);
+ });
+ });
+
+ it('calls `updateRowsWithPersistedVariables` with the persisted variables', () => {
+ const variablesResponse = [{ id: 1, key: 'foo', value: 'bar' }];
+ mock.onPatch(VARIABLE_PATCH_ENDPOINT).reply(200, {
+ variables: variablesResponse,
+ });
+
+ return ajaxVariableList.onSaveClicked().then(() => {
+ expect(ajaxVariableList.updateRowsWithPersistedVariables).toHaveBeenCalledWith(
+ variablesResponse,
+ );
+ });
+ });
+
+ it('hides any previous error box', () => {
+ mock.onPatch(VARIABLE_PATCH_ENDPOINT).reply(200);
+
+ expect(errorBox.classList.contains(HIDE_CLASS)).toEqual(true);
+
+ return ajaxVariableList.onSaveClicked().then(() => {
+ expect(errorBox.classList.contains(HIDE_CLASS)).toEqual(true);
+ });
+ });
+
+ it('disables remove buttons while waiting for the request', () => {
+ mock.onPatch(VARIABLE_PATCH_ENDPOINT).reply(() => {
+ expect(ajaxVariableList.variableList.toggleEnableRow).toHaveBeenCalledWith(false);
+
+ return [200, {}];
+ });
+
+ return ajaxVariableList.onSaveClicked().then(() => {
+ expect(ajaxVariableList.variableList.toggleEnableRow).toHaveBeenCalledWith(true);
+ });
+ });
+
+ it('hides secret values', () => {
+ mock.onPatch(VARIABLE_PATCH_ENDPOINT).reply(200, {});
+
+ const row = container.querySelector('.js-row');
+ const valueInput = row.querySelector('.js-ci-variable-input-value');
+ const valuePlaceholder = row.querySelector('.js-secret-value-placeholder');
+
+ valueInput.value = 'bar';
+ $(valueInput).trigger('input');
+
+ expect(valuePlaceholder.classList.contains(HIDE_CLASS)).toBe(true);
+ expect(valueInput.classList.contains(HIDE_CLASS)).toBe(false);
+
+ return ajaxVariableList.onSaveClicked().then(() => {
+ expect(valuePlaceholder.classList.contains(HIDE_CLASS)).toBe(false);
+ expect(valueInput.classList.contains(HIDE_CLASS)).toBe(true);
+ });
+ });
+
+ it('shows error box with validation errors', () => {
+ const validationError = 'some validation error';
+ mock.onPatch(VARIABLE_PATCH_ENDPOINT).reply(400, [validationError]);
+
+ expect(errorBox.classList.contains(HIDE_CLASS)).toEqual(true);
+
+ return ajaxVariableList.onSaveClicked().then(() => {
+ expect(errorBox.classList.contains(HIDE_CLASS)).toEqual(false);
+ expect(errorBox.textContent.trim().replace(/\n+\s+/m, ' ')).toEqual(
+ `Validation failed ${validationError}`,
+ );
+ });
+ });
+
+ it('shows flash message when request fails', () => {
+ mock.onPatch(VARIABLE_PATCH_ENDPOINT).reply(500);
+
+ expect(errorBox.classList.contains(HIDE_CLASS)).toEqual(true);
+
+ return ajaxVariableList.onSaveClicked().then(() => {
+ expect(errorBox.classList.contains(HIDE_CLASS)).toEqual(true);
+ });
+ });
+ });
+
+ describe('updateRowsWithPersistedVariables', () => {
+ beforeEach(() => {
+ loadFixtures('projects/ci_cd_settings_with_variables.html');
+ container = document.querySelector('.js-ci-variable-list-section');
+
+ const ajaxVariableListEl = document.querySelector('.js-ci-variable-list-section');
+ saveButton = ajaxVariableListEl.querySelector('.js-ci-variables-save-button');
+ errorBox = container.querySelector('.js-ci-variable-error-box');
+ ajaxVariableList = new AjaxFormVariableList({
+ container,
+ formField: 'variables',
+ saveButton,
+ errorBox,
+ saveEndpoint: container.dataset.saveEndpoint,
+ });
+ });
+
+ it('removes variable that was removed', () => {
+ expect(container.querySelectorAll('.js-row').length).toBe(3);
+
+ container.querySelector('.js-row-remove-button').click();
+
+ expect(container.querySelectorAll('.js-row').length).toBe(3);
+
+ ajaxVariableList.updateRowsWithPersistedVariables([]);
+
+ expect(container.querySelectorAll('.js-row').length).toBe(2);
+ });
+
+ it('updates new variable row with persisted ID', () => {
+ const row = container.querySelector('.js-row:last-child');
+ const idInput = row.querySelector('.js-ci-variable-input-id');
+ const keyInput = row.querySelector('.js-ci-variable-input-key');
+ const valueInput = row.querySelector('.js-ci-variable-input-value');
+
+ keyInput.value = 'foo';
+ $(keyInput).trigger('input');
+ valueInput.value = 'bar';
+ $(valueInput).trigger('input');
+
+ expect(idInput.value).toEqual('');
+
+ ajaxVariableList.updateRowsWithPersistedVariables([
+ {
+ id: 3,
+ key: 'foo',
+ value: 'bar',
+ },
+ ]);
+
+ expect(idInput.value).toEqual('3');
+ expect(row.dataset.isPersisted).toEqual('true');
+ });
+ });
+
+ describe('maskableRegex', () => {
+ it('takes in the regex provided by the data attribute', () => {
+ expect(container.dataset.maskableRegex).toBe('^[a-zA-Z0-9_+=/@:.-]{8,}$');
+ expect(ajaxVariableList.maskableRegex).toBe(container.dataset.maskableRegex);
+ });
+ });
+});
diff --git a/spec/frontend/ci_variable_list/ci_variable_list/ci_variable_list_spec.js b/spec/frontend/ci_variable_list/ci_variable_list/ci_variable_list_spec.js
new file mode 100644
index 00000000000..9508203e5c2
--- /dev/null
+++ b/spec/frontend/ci_variable_list/ci_variable_list/ci_variable_list_spec.js
@@ -0,0 +1,282 @@
+import $ from 'jquery';
+import waitForPromises from 'helpers/wait_for_promises';
+import VariableList from '~/ci_variable_list/ci_variable_list';
+
+const HIDE_CLASS = 'hide';
+
+describe('VariableList', () => {
+ preloadFixtures('pipeline_schedules/edit.html');
+ preloadFixtures('pipeline_schedules/edit_with_variables.html');
+ preloadFixtures('projects/ci_cd_settings.html');
+
+ let $wrapper;
+ let variableList;
+
+ describe('with only key/value inputs', () => {
+ describe('with no variables', () => {
+ beforeEach(() => {
+ loadFixtures('pipeline_schedules/edit.html');
+ $wrapper = $('.js-ci-variable-list-section');
+
+ variableList = new VariableList({
+ container: $wrapper,
+ formField: 'schedule',
+ });
+ variableList.init();
+ });
+
+ it('should remove the row when clicking the remove button', () => {
+ $wrapper.find('.js-row-remove-button').trigger('click');
+
+ expect($wrapper.find('.js-row').length).toBe(0);
+ });
+
+ it('should add another row when editing the last rows key input', () => {
+ const $row = $wrapper.find('.js-row');
+ $row
+ .find('.js-ci-variable-input-key')
+ .val('foo')
+ .trigger('input');
+
+ expect($wrapper.find('.js-row').length).toBe(2);
+
+ // Check for the correct default in the new row
+ const $keyInput = $wrapper.find('.js-row:last-child').find('.js-ci-variable-input-key');
+
+ expect($keyInput.val()).toBe('');
+ });
+
+ it('should add another row when editing the last rows value textarea', () => {
+ const $row = $wrapper.find('.js-row');
+ $row
+ .find('.js-ci-variable-input-value')
+ .val('foo')
+ .trigger('input');
+
+ expect($wrapper.find('.js-row').length).toBe(2);
+
+ // Check for the correct default in the new row
+ const $valueInput = $wrapper.find('.js-row:last-child').find('.js-ci-variable-input-key');
+
+ expect($valueInput.val()).toBe('');
+ });
+
+ it('should remove empty row after blurring', () => {
+ const $row = $wrapper.find('.js-row');
+ $row
+ .find('.js-ci-variable-input-key')
+ .val('foo')
+ .trigger('input');
+
+ expect($wrapper.find('.js-row').length).toBe(2);
+
+ $row
+ .find('.js-ci-variable-input-key')
+ .val('')
+ .trigger('input')
+ .trigger('blur');
+
+ expect($wrapper.find('.js-row').length).toBe(1);
+ });
+ });
+
+ describe('with persisted variables', () => {
+ beforeEach(() => {
+ loadFixtures('pipeline_schedules/edit_with_variables.html');
+ $wrapper = $('.js-ci-variable-list-section');
+
+ variableList = new VariableList({
+ container: $wrapper,
+ formField: 'schedule',
+ });
+ variableList.init();
+ });
+
+ it('should have "Reveal values" button initially when there are already variables', () => {
+ expect($wrapper.find('.js-secret-value-reveal-button').text()).toBe('Reveal values');
+ });
+
+ it('should reveal hidden values', () => {
+ const $row = $wrapper.find('.js-row:first-child');
+ const $inputValue = $row.find('.js-ci-variable-input-value');
+ const $placeholder = $row.find('.js-secret-value-placeholder');
+
+ expect($placeholder.hasClass(HIDE_CLASS)).toBe(false);
+ expect($inputValue.hasClass(HIDE_CLASS)).toBe(true);
+
+ // Reveal values
+ $wrapper.find('.js-secret-value-reveal-button').click();
+
+ expect($placeholder.hasClass(HIDE_CLASS)).toBe(true);
+ expect($inputValue.hasClass(HIDE_CLASS)).toBe(false);
+ });
+ });
+ });
+
+ describe('with all inputs(key, value, protected)', () => {
+ beforeEach(() => {
+ loadFixtures('projects/ci_cd_settings.html');
+ $wrapper = $('.js-ci-variable-list-section');
+
+ $wrapper.find('.js-ci-variable-input-protected').attr('data-default', 'false');
+
+ variableList = new VariableList({
+ container: $wrapper,
+ formField: 'variables',
+ });
+ variableList.init();
+ });
+
+ it('should not add another row when editing the last rows protected checkbox', () => {
+ const $row = $wrapper.find('.js-row:last-child');
+ $row.find('.ci-variable-protected-item .js-project-feature-toggle').click();
+
+ return waitForPromises().then(() => {
+ expect($wrapper.find('.js-row').length).toBe(1);
+ });
+ });
+
+ it('should not add another row when editing the last rows masked checkbox', () => {
+ jest.spyOn(variableList, 'checkIfRowTouched');
+ const $row = $wrapper.find('.js-row:last-child');
+ $row.find('.ci-variable-masked-item .js-project-feature-toggle').click();
+
+ return waitForPromises().then(() => {
+ // This validates that we are checking after the event listener has run
+ expect(variableList.checkIfRowTouched).toHaveBeenCalled();
+ expect($wrapper.find('.js-row').length).toBe(1);
+ });
+ });
+
+ describe('validateMaskability', () => {
+ let $row;
+
+ const maskingErrorElement = '.js-row:last-child .masking-validation-error';
+ const clickToggle = () =>
+ $row.find('.ci-variable-masked-item .js-project-feature-toggle').click();
+
+ beforeEach(() => {
+ $row = $wrapper.find('.js-row:last-child');
+ });
+
+ it('has a regex provided via a data attribute', () => {
+ clickToggle();
+
+ expect($wrapper.attr('data-maskable-regex')).toBe('^[a-zA-Z0-9_+=/@:.-]{8,}$');
+ });
+
+ it('allows values that are 8 characters long', () => {
+ $row.find('.js-ci-variable-input-value').val('looooong');
+
+ clickToggle();
+
+ expect($wrapper.find(maskingErrorElement)).toHaveClass('hide');
+ });
+
+ it('rejects values that are shorter than 8 characters', () => {
+ $row.find('.js-ci-variable-input-value').val('short');
+
+ clickToggle();
+
+ expect($wrapper.find(maskingErrorElement)).toBeVisible();
+ });
+
+ it('allows values with base 64 characters', () => {
+ $row.find('.js-ci-variable-input-value').val('abcABC123_+=/-');
+
+ clickToggle();
+
+ expect($wrapper.find(maskingErrorElement)).toHaveClass('hide');
+ });
+
+ it('rejects values with other special characters', () => {
+ $row.find('.js-ci-variable-input-value').val('1234567$');
+
+ clickToggle();
+
+ expect($wrapper.find(maskingErrorElement)).toBeVisible();
+ });
+ });
+ });
+
+ describe('toggleEnableRow method', () => {
+ beforeEach(() => {
+ loadFixtures('pipeline_schedules/edit_with_variables.html');
+ $wrapper = $('.js-ci-variable-list-section');
+
+ variableList = new VariableList({
+ container: $wrapper,
+ formField: 'variables',
+ });
+ variableList.init();
+ });
+
+ it('should disable all key inputs', () => {
+ expect($wrapper.find('.js-ci-variable-input-key:not([disabled])').length).toBe(3);
+
+ variableList.toggleEnableRow(false);
+
+ expect($wrapper.find('.js-ci-variable-input-key[disabled]').length).toBe(3);
+ });
+
+ it('should disable all remove buttons', () => {
+ expect($wrapper.find('.js-row-remove-button:not([disabled])').length).toBe(3);
+
+ variableList.toggleEnableRow(false);
+
+ expect($wrapper.find('.js-row-remove-button[disabled]').length).toBe(3);
+ });
+
+ it('should enable all remove buttons', () => {
+ variableList.toggleEnableRow(false);
+
+ expect($wrapper.find('.js-row-remove-button[disabled]').length).toBe(3);
+
+ variableList.toggleEnableRow(true);
+
+ expect($wrapper.find('.js-row-remove-button:not([disabled])').length).toBe(3);
+ });
+
+ it('should enable all key inputs', () => {
+ variableList.toggleEnableRow(false);
+
+ expect($wrapper.find('.js-ci-variable-input-key[disabled]').length).toBe(3);
+
+ variableList.toggleEnableRow(true);
+
+ expect($wrapper.find('.js-ci-variable-input-key:not([disabled])').length).toBe(3);
+ });
+ });
+
+ describe('hideValues', () => {
+ beforeEach(() => {
+ loadFixtures('projects/ci_cd_settings.html');
+ $wrapper = $('.js-ci-variable-list-section');
+
+ variableList = new VariableList({
+ container: $wrapper,
+ formField: 'variables',
+ });
+ variableList.init();
+ });
+
+ it('should hide value input and show placeholder stars', () => {
+ const $row = $wrapper.find('.js-row');
+ const $inputValue = $row.find('.js-ci-variable-input-value');
+ const $placeholder = $row.find('.js-secret-value-placeholder');
+
+ $row
+ .find('.js-ci-variable-input-value')
+ .val('foo')
+ .trigger('input');
+
+ expect($placeholder.hasClass(HIDE_CLASS)).toBe(true);
+ expect($inputValue.hasClass(HIDE_CLASS)).toBe(false);
+
+ variableList.hideValues();
+
+ expect($placeholder.hasClass(HIDE_CLASS)).toBe(false);
+ expect($inputValue.hasClass(HIDE_CLASS)).toBe(true);
+ });
+ });
+});
diff --git a/spec/javascripts/ci_variable_list/native_form_variable_list_spec.js b/spec/frontend/ci_variable_list/ci_variable_list/native_form_variable_list_spec.js
index 4982b68fa81..4982b68fa81 100644
--- a/spec/javascripts/ci_variable_list/native_form_variable_list_spec.js
+++ b/spec/frontend/ci_variable_list/ci_variable_list/native_form_variable_list_spec.js
diff --git a/spec/frontend/ci_variable_list/components/ci_variable_modal_spec.js b/spec/frontend/ci_variable_list/components/ci_variable_modal_spec.js
index 7b8d69df35e..9179302f786 100644
--- a/spec/frontend/ci_variable_list/components/ci_variable_modal_spec.js
+++ b/spec/frontend/ci_variable_list/components/ci_variable_modal_spec.js
@@ -96,6 +96,13 @@ describe('Ci variable modal', () => {
findModal().vm.$emit('hidden');
expect(store.dispatch).toHaveBeenCalledWith('clearModal');
});
+
+ it('should dispatch setVariableProtected when admin settings are configured to protect variables', () => {
+ store.state.isProtectedByDefault = true;
+ findModal().vm.$emit('shown');
+
+ expect(store.dispatch).toHaveBeenCalledWith('setVariableProtected');
+ });
});
describe('Editing a variable', () => {
diff --git a/spec/frontend/ci_variable_list/services/mock_data.js b/spec/frontend/ci_variable_list/services/mock_data.js
index 09c6cd9de21..7dab33050d9 100644
--- a/spec/frontend/ci_variable_list/services/mock_data.js
+++ b/spec/frontend/ci_variable_list/services/mock_data.js
@@ -8,7 +8,7 @@ export default {
protected: false,
secret_value: 'test_val',
value: 'test_val',
- variable_type: 'Var',
+ variable_type: 'Variable',
},
],
@@ -44,7 +44,7 @@ export default {
protected: false,
secret_value: 'test_val',
value: 'test_val',
- variable_type: 'Var',
+ variable_type: 'Variable',
},
{
environment_scope: 'All (default)',
@@ -104,7 +104,7 @@ export default {
id: 28,
key: 'goku_var',
value: 'goku_val',
- variable_type: 'Var',
+ variable_type: 'Variable',
protected: true,
masked: true,
environment_scope: 'staging',
@@ -114,7 +114,7 @@ export default {
id: 25,
key: 'test_var_4',
value: 'test_val_4',
- variable_type: 'Var',
+ variable_type: 'Variable',
protected: false,
masked: false,
environment_scope: 'production',
@@ -134,7 +134,7 @@ export default {
id: 24,
key: 'test_var_3',
value: 'test_val_3',
- variable_type: 'Var',
+ variable_type: 'Variable',
protected: false,
masked: false,
environment_scope: 'All (default)',
@@ -144,7 +144,7 @@ export default {
id: 26,
key: 'test_var_5',
value: 'test_val_5',
- variable_type: 'Var',
+ variable_type: 'Variable',
protected: false,
masked: false,
environment_scope: 'production',
diff --git a/spec/frontend/ci_variable_list/store/actions_spec.js b/spec/frontend/ci_variable_list/store/actions_spec.js
index 84455612f0c..12b4311d0f5 100644
--- a/spec/frontend/ci_variable_list/store/actions_spec.js
+++ b/spec/frontend/ci_variable_list/store/actions_spec.js
@@ -75,6 +75,16 @@ describe('CI variable list store actions', () => {
});
});
+ describe('setVariableProtected', () => {
+ it('commits SET_VARIABLE_PROTECTED mutation', () => {
+ testAction(actions.setVariableProtected, {}, {}, [
+ {
+ type: types.SET_VARIABLE_PROTECTED,
+ },
+ ]);
+ });
+ });
+
describe('deleteVariable', () => {
it('dispatch correct actions on successful deleted variable', done => {
mock.onPatch(state.endpoint).reply(200);
diff --git a/spec/frontend/ci_variable_list/store/mutations_spec.js b/spec/frontend/ci_variable_list/store/mutations_spec.js
index 8652359f3df..1934d108957 100644
--- a/spec/frontend/ci_variable_list/store/mutations_spec.js
+++ b/spec/frontend/ci_variable_list/store/mutations_spec.js
@@ -47,7 +47,7 @@ describe('CI variable list mutations', () => {
describe('CLEAR_MODAL', () => {
it('should clear modal state ', () => {
const modalState = {
- variable_type: 'Var',
+ variable_type: 'Variable',
key: '',
secret_value: '',
protected: false,
@@ -97,4 +97,12 @@ describe('CI variable list mutations', () => {
expect(stateCopy.environments).toEqual(['dev', 'production', 'staging']);
});
});
+
+ describe('SET_VARIABLE_PROTECTED', () => {
+ it('should set protected value to true', () => {
+ mutations[types.SET_VARIABLE_PROTECTED](stateCopy);
+
+ expect(stateCopy.variable.protected).toBe(true);
+ });
+ });
});
diff --git a/spec/frontend/close_reopen_report_toggle_spec.js b/spec/frontend/close_reopen_report_toggle_spec.js
new file mode 100644
index 00000000000..f6b5e4bed87
--- /dev/null
+++ b/spec/frontend/close_reopen_report_toggle_spec.js
@@ -0,0 +1,288 @@
+import CloseReopenReportToggle from '~/close_reopen_report_toggle';
+import DropLab from '~/droplab/drop_lab';
+
+describe('CloseReopenReportToggle', () => {
+ describe('class constructor', () => {
+ const dropdownTrigger = {};
+ const dropdownList = {};
+ const button = {};
+ let commentTypeToggle;
+
+ beforeEach(() => {
+ commentTypeToggle = new CloseReopenReportToggle({
+ dropdownTrigger,
+ dropdownList,
+ button,
+ });
+ });
+
+ it('sets .dropdownTrigger', () => {
+ expect(commentTypeToggle.dropdownTrigger).toBe(dropdownTrigger);
+ });
+
+ it('sets .dropdownList', () => {
+ expect(commentTypeToggle.dropdownList).toBe(dropdownList);
+ });
+
+ it('sets .button', () => {
+ expect(commentTypeToggle.button).toBe(button);
+ });
+ });
+
+ describe('initDroplab', () => {
+ let closeReopenReportToggle;
+ const dropdownList = {
+ querySelector: jest.fn(),
+ };
+ const dropdownTrigger = {};
+ const button = {};
+ const reopenItem = {};
+ const closeItem = {};
+ const config = {};
+
+ beforeEach(() => {
+ jest.spyOn(DropLab.prototype, 'init').mockImplementation(() => {});
+ dropdownList.querySelector.mockReturnValueOnce(reopenItem).mockReturnValueOnce(closeItem);
+
+ closeReopenReportToggle = new CloseReopenReportToggle({
+ dropdownTrigger,
+ dropdownList,
+ button,
+ });
+
+ jest.spyOn(closeReopenReportToggle, 'setConfig').mockReturnValue(config);
+
+ closeReopenReportToggle.initDroplab();
+ });
+
+ it('sets .reopenItem and .closeItem', () => {
+ expect(dropdownList.querySelector).toHaveBeenCalledWith('.reopen-item');
+ expect(dropdownList.querySelector).toHaveBeenCalledWith('.close-item');
+ expect(closeReopenReportToggle.reopenItem).toBe(reopenItem);
+ expect(closeReopenReportToggle.closeItem).toBe(closeItem);
+ });
+
+ it('sets .droplab', () => {
+ expect(closeReopenReportToggle.droplab).toEqual(expect.any(Object));
+ });
+
+ it('calls .setConfig', () => {
+ expect(closeReopenReportToggle.setConfig).toHaveBeenCalled();
+ });
+
+ it('calls droplab.init', () => {
+ expect(DropLab.prototype.init).toHaveBeenCalledWith(
+ dropdownTrigger,
+ dropdownList,
+ expect.any(Array),
+ config,
+ );
+ });
+ });
+
+ describe('updateButton', () => {
+ let closeReopenReportToggle;
+ const dropdownList = {};
+ const dropdownTrigger = {};
+ const button = {
+ blur: jest.fn(),
+ };
+ const isClosed = true;
+
+ beforeEach(() => {
+ closeReopenReportToggle = new CloseReopenReportToggle({
+ dropdownTrigger,
+ dropdownList,
+ button,
+ });
+
+ jest.spyOn(closeReopenReportToggle, 'toggleButtonType').mockImplementation(() => {});
+
+ closeReopenReportToggle.updateButton(isClosed);
+ });
+
+ it('calls .toggleButtonType', () => {
+ expect(closeReopenReportToggle.toggleButtonType).toHaveBeenCalledWith(isClosed);
+ });
+
+ it('calls .button.blur', () => {
+ expect(closeReopenReportToggle.button.blur).toHaveBeenCalled();
+ });
+ });
+
+ describe('toggleButtonType', () => {
+ let closeReopenReportToggle;
+ const dropdownList = {};
+ const dropdownTrigger = {};
+ const button = {};
+ const isClosed = true;
+ const showItem = {
+ click: jest.fn(),
+ };
+ const hideItem = {};
+ showItem.classList = {
+ add: jest.fn(),
+ remove: jest.fn(),
+ };
+ hideItem.classList = {
+ add: jest.fn(),
+ remove: jest.fn(),
+ };
+
+ beforeEach(() => {
+ closeReopenReportToggle = new CloseReopenReportToggle({
+ dropdownTrigger,
+ dropdownList,
+ button,
+ });
+
+ jest.spyOn(closeReopenReportToggle, 'getButtonTypes').mockReturnValue([showItem, hideItem]);
+
+ closeReopenReportToggle.toggleButtonType(isClosed);
+ });
+
+ it('calls .getButtonTypes', () => {
+ expect(closeReopenReportToggle.getButtonTypes).toHaveBeenCalledWith(isClosed);
+ });
+
+ it('removes hide class and add selected class to showItem, opposite for hideItem', () => {
+ expect(showItem.classList.remove).toHaveBeenCalledWith('hidden');
+ expect(showItem.classList.add).toHaveBeenCalledWith('droplab-item-selected');
+ expect(hideItem.classList.add).toHaveBeenCalledWith('hidden');
+ expect(hideItem.classList.remove).toHaveBeenCalledWith('droplab-item-selected');
+ });
+
+ it('clicks the showItem', () => {
+ expect(showItem.click).toHaveBeenCalled();
+ });
+ });
+
+ describe('getButtonTypes', () => {
+ let closeReopenReportToggle;
+ const dropdownList = {};
+ const dropdownTrigger = {};
+ const button = {};
+ const reopenItem = {};
+ const closeItem = {};
+
+ beforeEach(() => {
+ closeReopenReportToggle = new CloseReopenReportToggle({
+ dropdownTrigger,
+ dropdownList,
+ button,
+ });
+
+ closeReopenReportToggle.reopenItem = reopenItem;
+ closeReopenReportToggle.closeItem = closeItem;
+ });
+
+ it('returns reopenItem, closeItem if isClosed is true', () => {
+ const buttonTypes = closeReopenReportToggle.getButtonTypes(true);
+
+ expect(buttonTypes).toEqual([reopenItem, closeItem]);
+ });
+
+ it('returns closeItem, reopenItem if isClosed is false', () => {
+ const buttonTypes = closeReopenReportToggle.getButtonTypes(false);
+
+ expect(buttonTypes).toEqual([closeItem, reopenItem]);
+ });
+ });
+
+ describe('setDisable', () => {
+ let closeReopenReportToggle;
+ const dropdownList = {};
+ const dropdownTrigger = {
+ setAttribute: jest.fn(),
+ removeAttribute: jest.fn(),
+ };
+ const button = {
+ setAttribute: jest.fn(),
+ removeAttribute: jest.fn(),
+ };
+
+ beforeEach(() => {
+ closeReopenReportToggle = new CloseReopenReportToggle({
+ dropdownTrigger,
+ dropdownList,
+ button,
+ });
+ });
+
+ it('disable .button and .dropdownTrigger if shouldDisable is true', () => {
+ closeReopenReportToggle.setDisable(true);
+
+ expect(button.setAttribute).toHaveBeenCalledWith('disabled', 'true');
+ expect(dropdownTrigger.setAttribute).toHaveBeenCalledWith('disabled', 'true');
+ });
+
+ it('disable .button and .dropdownTrigger if shouldDisable is undefined', () => {
+ closeReopenReportToggle.setDisable();
+
+ expect(button.setAttribute).toHaveBeenCalledWith('disabled', 'true');
+ expect(dropdownTrigger.setAttribute).toHaveBeenCalledWith('disabled', 'true');
+ });
+
+ it('enable .button and .dropdownTrigger if shouldDisable is false', () => {
+ closeReopenReportToggle.setDisable(false);
+
+ expect(button.removeAttribute).toHaveBeenCalledWith('disabled');
+ expect(dropdownTrigger.removeAttribute).toHaveBeenCalledWith('disabled');
+ });
+ });
+
+ describe('setConfig', () => {
+ let closeReopenReportToggle;
+ const dropdownList = {};
+ const dropdownTrigger = {};
+ const button = {};
+ let config;
+
+ beforeEach(() => {
+ closeReopenReportToggle = new CloseReopenReportToggle({
+ dropdownTrigger,
+ dropdownList,
+ button,
+ });
+
+ config = closeReopenReportToggle.setConfig();
+ });
+
+ it('returns a config object', () => {
+ expect(config).toEqual({
+ InputSetter: [
+ {
+ input: button,
+ valueAttribute: 'data-text',
+ inputAttribute: 'data-value',
+ },
+ {
+ input: button,
+ valueAttribute: 'data-text',
+ inputAttribute: 'title',
+ },
+ {
+ input: button,
+ valueAttribute: 'data-button-class',
+ inputAttribute: 'class',
+ },
+ {
+ input: dropdownTrigger,
+ valueAttribute: 'data-toggle-class',
+ inputAttribute: 'class',
+ },
+ {
+ input: button,
+ valueAttribute: 'data-url',
+ inputAttribute: 'href',
+ },
+ {
+ input: button,
+ valueAttribute: 'data-method',
+ inputAttribute: 'data-method',
+ },
+ ],
+ });
+ });
+ });
+});
diff --git a/spec/frontend/clusters/components/applications_spec.js b/spec/frontend/clusters/components/applications_spec.js
index 782e5215ad8..33b30891d5e 100644
--- a/spec/frontend/clusters/components/applications_spec.js
+++ b/spec/frontend/clusters/components/applications_spec.js
@@ -8,6 +8,7 @@ import eventHub from '~/clusters/event_hub';
import KnativeDomainEditor from '~/clusters/components/knative_domain_editor.vue';
import CrossplaneProviderStack from '~/clusters/components/crossplane_provider_stack.vue';
import IngressModsecuritySettings from '~/clusters/components/ingress_modsecurity_settings.vue';
+import FluentdOutputSettings from '~/clusters/components/fluentd_output_settings.vue';
describe('Applications', () => {
let vm;
@@ -67,6 +68,10 @@ describe('Applications', () => {
it('renders a row for Elastic Stack', () => {
expect(vm.$el.querySelector('.js-cluster-application-row-elastic_stack')).not.toBeNull();
});
+
+ it('renders a row for Fluentd', () => {
+ expect(vm.$el.querySelector('.js-cluster-application-row-fluentd')).not.toBeNull();
+ });
});
describe('Group cluster applications', () => {
@@ -112,6 +117,10 @@ describe('Applications', () => {
it('renders a row for Elastic Stack', () => {
expect(vm.$el.querySelector('.js-cluster-application-row-elastic_stack')).not.toBeNull();
});
+
+ it('renders a row for Fluentd', () => {
+ expect(vm.$el.querySelector('.js-cluster-application-row-fluentd')).not.toBeNull();
+ });
});
describe('Instance cluster applications', () => {
@@ -157,6 +166,10 @@ describe('Applications', () => {
it('renders a row for Elastic Stack', () => {
expect(vm.$el.querySelector('.js-cluster-application-row-elastic_stack')).not.toBeNull();
});
+
+ it('renders a row for Fluentd', () => {
+ expect(vm.$el.querySelector('.js-cluster-application-row-fluentd')).not.toBeNull();
+ });
});
describe('Helm application', () => {
@@ -240,6 +253,7 @@ describe('Applications', () => {
jupyter: { title: 'JupyterHub', hostname: '' },
knative: { title: 'Knative', hostname: '' },
elastic_stack: { title: 'Elastic Stack' },
+ fluentd: { title: 'Fluentd' },
},
});
@@ -539,4 +553,23 @@ describe('Applications', () => {
});
});
});
+
+ describe('Fluentd application', () => {
+ const propsData = {
+ applications: {
+ ...APPLICATIONS_MOCK_STATE,
+ },
+ };
+
+ let wrapper;
+ beforeEach(() => {
+ wrapper = shallowMount(Applications, { propsData });
+ });
+ afterEach(() => {
+ wrapper.destroy();
+ });
+ it('renders the correct Component', () => {
+ expect(wrapper.contains(FluentdOutputSettings)).toBe(true);
+ });
+ });
});
diff --git a/spec/frontend/clusters/components/fluentd_output_settings_spec.js b/spec/frontend/clusters/components/fluentd_output_settings_spec.js
new file mode 100644
index 00000000000..5e27cc49049
--- /dev/null
+++ b/spec/frontend/clusters/components/fluentd_output_settings_spec.js
@@ -0,0 +1,186 @@
+import { shallowMount } from '@vue/test-utils';
+import FluentdOutputSettings from '~/clusters/components/fluentd_output_settings.vue';
+import { APPLICATION_STATUS, FLUENTD } from '~/clusters/constants';
+import { GlAlert, GlDropdown, GlFormCheckbox } from '@gitlab/ui';
+import eventHub from '~/clusters/event_hub';
+
+const { UPDATING } = APPLICATION_STATUS;
+
+describe('FluentdOutputSettings', () => {
+ let wrapper;
+
+ const defaultSettings = {
+ protocol: 'tcp',
+ host: '127.0.0.1',
+ port: 514,
+ wafLogEnabled: true,
+ ciliumLogEnabled: false,
+ };
+ const defaultProps = {
+ status: 'installable',
+ updateFailed: false,
+ ...defaultSettings,
+ };
+
+ const createComponent = (props = {}) => {
+ wrapper = shallowMount(FluentdOutputSettings, {
+ propsData: {
+ ...defaultProps,
+ ...props,
+ },
+ });
+ };
+ const updateComponentPropsFromEvent = () => {
+ const { isEditingSettings, ...props } = eventHub.$emit.mock.calls[0][1];
+ wrapper.setProps(props);
+ };
+ const findSaveButton = () => wrapper.find({ ref: 'saveBtn' });
+ const findCancelButton = () => wrapper.find({ ref: 'cancelBtn' });
+ const findProtocolDropdown = () => wrapper.find(GlDropdown);
+ const findCheckbox = name =>
+ wrapper.findAll(GlFormCheckbox).wrappers.find(x => x.text() === name);
+ const findHost = () => wrapper.find('#fluentd-host');
+ const findPort = () => wrapper.find('#fluentd-port');
+ const changeCheckbox = checkbox => {
+ const currentValue = checkbox.attributes('checked')?.toString() === 'true';
+ checkbox.vm.$emit('input', !currentValue);
+ };
+ const changeInput = ({ element }, val) => {
+ element.value = val;
+ element.dispatchEvent(new Event('input'));
+ };
+ const changePort = val => changeInput(findPort(), val);
+ const changeHost = val => changeInput(findHost(), val);
+ const changeProtocol = idx => findProtocolDropdown().vm.$children[idx].$emit('click');
+ const toApplicationSettings = ({ wafLogEnabled, ciliumLogEnabled, ...settings }) => ({
+ ...settings,
+ waf_log_enabled: wafLogEnabled,
+ cilium_log_enabled: ciliumLogEnabled,
+ });
+
+ describe('when fluentd is installed', () => {
+ beforeEach(() => {
+ createComponent({ status: 'installed' });
+ jest.spyOn(eventHub, '$emit');
+ });
+
+ it('does not render save and cancel buttons', () => {
+ expect(findSaveButton().exists()).toBe(false);
+ expect(findCancelButton().exists()).toBe(false);
+ });
+
+ describe.each`
+ desc | changeFn | key | value
+ ${'when protocol dropdown is triggered'} | ${() => changeProtocol(1)} | ${'protocol'} | ${'udp'}
+ ${'when host is changed'} | ${() => changeHost('test-host')} | ${'host'} | ${'test-host'}
+ ${'when port is changed'} | ${() => changePort(123)} | ${'port'} | ${123}
+ ${'when wafLogEnabled changes'} | ${() => changeCheckbox(findCheckbox('Send ModSecurity Logs'))} | ${'wafLogEnabled'} | ${!defaultSettings.wafLogEnabled}
+ ${'when ciliumLogEnabled changes'} | ${() => changeCheckbox(findCheckbox('Send Cilium Logs'))} | ${'ciliumLogEnabled'} | ${!defaultSettings.ciliumLogEnabled}
+ `('$desc', ({ changeFn, key, value }) => {
+ beforeEach(() => {
+ changeFn();
+ });
+
+ it('triggers set event to be propagated with the current value', () => {
+ expect(eventHub.$emit).toHaveBeenCalledWith('setFluentdSettings', {
+ [key]: value,
+ isEditingSettings: true,
+ });
+ });
+
+ describe('when value is updated from store', () => {
+ beforeEach(() => {
+ updateComponentPropsFromEvent();
+ });
+
+ it('enables save and cancel buttons', () => {
+ expect(findSaveButton().exists()).toBe(true);
+ expect(findSaveButton().attributes().disabled).toBeUndefined();
+ expect(findCancelButton().exists()).toBe(true);
+ expect(findCancelButton().attributes().disabled).toBeUndefined();
+ });
+
+ describe('and the save changes button is clicked', () => {
+ beforeEach(() => {
+ eventHub.$emit.mockClear();
+ findSaveButton().vm.$emit('click');
+ });
+
+ it('triggers save event and pass current values', () => {
+ expect(eventHub.$emit).toHaveBeenCalledWith('updateApplication', {
+ id: FLUENTD,
+ params: toApplicationSettings({
+ ...defaultSettings,
+ [key]: value,
+ }),
+ });
+ });
+ });
+
+ describe('and the cancel button is clicked', () => {
+ beforeEach(() => {
+ eventHub.$emit.mockClear();
+ findCancelButton().vm.$emit('click');
+ });
+
+ it('triggers reset event', () => {
+ expect(eventHub.$emit).toHaveBeenCalledWith('setFluentdSettings', {
+ ...defaultSettings,
+ isEditingSettings: false,
+ });
+ });
+
+ describe('when value is updated from store', () => {
+ beforeEach(() => {
+ updateComponentPropsFromEvent();
+ });
+
+ it('does not render save and cancel buttons', () => {
+ expect(findSaveButton().exists()).toBe(false);
+ expect(findCancelButton().exists()).toBe(false);
+ });
+ });
+ });
+ });
+ });
+
+ describe(`when fluentd status is ${UPDATING}`, () => {
+ beforeEach(() => {
+ createComponent({ installed: true, status: UPDATING });
+ });
+
+ it('renders loading spinner in save button', () => {
+ expect(findSaveButton().props('loading')).toBe(true);
+ });
+
+ it('renders disabled save button', () => {
+ expect(findSaveButton().props('disabled')).toBe(true);
+ });
+
+ it('renders save button with "Saving" label', () => {
+ expect(findSaveButton().text()).toBe('Saving');
+ });
+ });
+
+ describe('when fluentd fails to update', () => {
+ beforeEach(() => {
+ createComponent({ updateFailed: true });
+ });
+
+ it('displays a error message', () => {
+ expect(wrapper.contains(GlAlert)).toBe(true);
+ });
+ });
+ });
+
+ describe('when fluentd is not installed', () => {
+ beforeEach(() => {
+ createComponent();
+ });
+
+ it('does not render the save button', () => {
+ expect(findSaveButton().exists()).toBe(false);
+ expect(findCancelButton().exists()).toBe(false);
+ });
+ });
+});
diff --git a/spec/frontend/clusters/components/knative_domain_editor_spec.js b/spec/frontend/clusters/components/knative_domain_editor_spec.js
index 2de04f7da1f..73d08661199 100644
--- a/spec/frontend/clusters/components/knative_domain_editor_spec.js
+++ b/spec/frontend/clusters/components/knative_domain_editor_spec.js
@@ -93,7 +93,7 @@ describe('KnativeDomainEditor', () => {
it('displays toast indicating a successful update', () => {
wrapper.vm.$toast = { show: jest.fn() };
- wrapper.setProps({ knative: Object.assign({ updateSuccessful: true }, knative) });
+ wrapper.setProps({ knative: { updateSuccessful: true, ...knative } });
return wrapper.vm.$nextTick(() => {
expect(wrapper.vm.$toast.show).toHaveBeenCalledWith(
diff --git a/spec/frontend/clusters/services/mock_data.js b/spec/frontend/clusters/services/mock_data.js
index 52d78ea1176..c5ec3f6e6a8 100644
--- a/spec/frontend/clusters/services/mock_data.js
+++ b/spec/frontend/clusters/services/mock_data.js
@@ -159,6 +159,7 @@ const APPLICATIONS_MOCK_STATE = {
jupyter: { title: 'JupyterHub', status: 'installable', hostname: '' },
knative: { title: 'Knative ', status: 'installable', hostname: '' },
elastic_stack: { title: 'Elastic Stack', status: 'installable' },
+ fluentd: { title: 'Fluentd', status: 'installable' },
};
export { CLUSTERS_MOCK_DATA, DEFAULT_APPLICATION_STATE, APPLICATIONS_MOCK_STATE };
diff --git a/spec/frontend/clusters/stores/clusters_store_spec.js b/spec/frontend/clusters/stores/clusters_store_spec.js
index 9fafc688af9..36e99c37be5 100644
--- a/spec/frontend/clusters/stores/clusters_store_spec.js
+++ b/spec/frontend/clusters/stores/clusters_store_spec.js
@@ -121,6 +121,24 @@ describe('Clusters Store', () => {
uninstallFailed: false,
validationError: null,
},
+ fluentd: {
+ title: 'Fluentd',
+ status: null,
+ statusReason: null,
+ requestReason: null,
+ port: null,
+ ciliumLogEnabled: null,
+ host: null,
+ protocol: null,
+ installed: false,
+ isEditingSettings: false,
+ installFailed: false,
+ uninstallable: false,
+ uninstallSuccessful: false,
+ uninstallFailed: false,
+ validationError: null,
+ wafLogEnabled: null,
+ },
jupyter: {
title: 'JupyterHub',
status: mockResponseData.applications[4].status,
diff --git a/spec/frontend/clusters_list/components/clusters_spec.js b/spec/frontend/clusters_list/components/clusters_spec.js
index 85c86b2c0a9..e2d2e4b73b3 100644
--- a/spec/frontend/clusters_list/components/clusters_spec.js
+++ b/spec/frontend/clusters_list/components/clusters_spec.js
@@ -1,46 +1,68 @@
-import Vuex from 'vuex';
-import { createLocalVue, mount } from '@vue/test-utils';
-import { GlTable, GlLoadingIcon } from '@gitlab/ui';
+import axios from '~/lib/utils/axios_utils';
import Clusters from '~/clusters_list/components/clusters.vue';
-import mockData from '../mock_data';
-
-const localVue = createLocalVue();
-localVue.use(Vuex);
+import ClusterStore from '~/clusters_list/store';
+import MockAdapter from 'axios-mock-adapter';
+import { apiData } from '../mock_data';
+import { mount } from '@vue/test-utils';
+import { GlLoadingIcon, GlTable, GlPagination } from '@gitlab/ui';
describe('Clusters', () => {
+ let mock;
+ let store;
let wrapper;
- const findTable = () => wrapper.find(GlTable);
+ const endpoint = 'some/endpoint';
+
const findLoader = () => wrapper.find(GlLoadingIcon);
+ const findPaginatedButtons = () => wrapper.find(GlPagination);
+ const findTable = () => wrapper.find(GlTable);
const findStatuses = () => findTable().findAll('.js-status');
- const mountComponent = _state => {
- const state = { clusters: mockData, endpoint: 'some/endpoint', ..._state };
- const store = new Vuex.Store({
- state,
- });
+ const mockPollingApi = (response, body, header) => {
+ mock.onGet(`${endpoint}?page=${header['x-page']}`).reply(response, body, header);
+ };
- wrapper = mount(Clusters, { localVue, store });
+ const mountWrapper = () => {
+ store = ClusterStore({ endpoint });
+ wrapper = mount(Clusters, { store });
+ return axios.waitForAll();
};
beforeEach(() => {
- mountComponent({ loading: false });
+ mock = new MockAdapter(axios);
+ mockPollingApi(200, apiData, {
+ 'x-total': apiData.clusters.length,
+ 'x-per-page': 20,
+ 'x-page': 1,
+ });
+
+ return mountWrapper();
+ });
+
+ afterEach(() => {
+ wrapper.destroy();
+ mock.restore();
});
describe('clusters table', () => {
- it('displays a loader instead of the table while loading', () => {
- mountComponent({ loading: true });
- expect(findLoader().exists()).toBe(true);
- expect(findTable().exists()).toBe(false);
+ describe('when data is loading', () => {
+ beforeEach(() => {
+ wrapper.vm.$store.state.loading = true;
+ return wrapper.vm.$nextTick();
+ });
+
+ it('displays a loader instead of the table while loading', () => {
+ expect(findLoader().exists()).toBe(true);
+ expect(findTable().exists()).toBe(false);
+ });
});
it('displays a table component', () => {
expect(findTable().exists()).toBe(true);
- expect(findTable().exists()).toBe(true);
});
it('renders the correct table headers', () => {
- const tableHeaders = wrapper.vm.$options.fields;
+ const tableHeaders = wrapper.vm.fields;
const headers = findTable().findAll('th');
expect(headers.length).toBe(tableHeaders.length);
@@ -62,7 +84,8 @@ describe('Clusters', () => {
${'unreachable'} | ${'bg-danger'} | ${1}
${'authentication_failure'} | ${'bg-warning'} | ${2}
${'deleting'} | ${null} | ${3}
- ${'connected'} | ${'bg-success'} | ${4}
+ ${'created'} | ${'bg-success'} | ${4}
+ ${'default'} | ${'bg-white'} | ${5}
`('renders a status for each cluster', ({ statusName, className, lineNumber }) => {
const statuses = findStatuses();
const status = statuses.at(lineNumber);
@@ -75,4 +98,47 @@ describe('Clusters', () => {
}
});
});
+
+ describe('pagination', () => {
+ const perPage = apiData.clusters.length;
+ const totalFirstPage = 100;
+ const totalSecondPage = 500;
+
+ beforeEach(() => {
+ mockPollingApi(200, apiData, {
+ 'x-total': totalFirstPage,
+ 'x-per-page': perPage,
+ 'x-page': 1,
+ });
+ return mountWrapper();
+ });
+
+ it('should load to page 1 with header values', () => {
+ const buttons = findPaginatedButtons();
+
+ expect(buttons.props('perPage')).toBe(perPage);
+ expect(buttons.props('totalItems')).toBe(totalFirstPage);
+ expect(buttons.props('value')).toBe(1);
+ });
+
+ describe('when updating currentPage', () => {
+ beforeEach(() => {
+ mockPollingApi(200, apiData, {
+ 'x-total': totalSecondPage,
+ 'x-per-page': perPage,
+ 'x-page': 2,
+ });
+ wrapper.setData({ currentPage: 2 });
+ return axios.waitForAll();
+ });
+
+ it('should change pagination when currentPage changes', () => {
+ const buttons = findPaginatedButtons();
+
+ expect(buttons.props('perPage')).toBe(perPage);
+ expect(buttons.props('totalItems')).toBe(totalSecondPage);
+ expect(buttons.props('value')).toBe(2);
+ });
+ });
+ });
});
diff --git a/spec/frontend/clusters_list/mock_data.js b/spec/frontend/clusters_list/mock_data.js
index 5398975d81c..9a90a378f31 100644
--- a/spec/frontend/clusters_list/mock_data.js
+++ b/spec/frontend/clusters_list/mock_data.js
@@ -1,4 +1,4 @@
-export default [
+export const clusterList = [
{
name: 'My Cluster 1',
environmentScope: '*',
@@ -40,8 +40,22 @@ export default [
environmentScope: 'development',
size: '12',
clusterType: 'project_type',
- status: 'connected',
+ status: 'created',
+ cpu: '6 (100% free)',
+ memory: '20.12 (35% free)',
+ },
+ {
+ name: 'My Cluster 6',
+ environmentScope: '*',
+ size: '1',
+ clusterType: 'project_type',
+ status: 'cleanup_ongoing',
cpu: '6 (100% free)',
memory: '20.12 (35% free)',
},
];
+
+export const apiData = {
+ clusters: clusterList,
+ has_ancestor_clusters: false,
+};
diff --git a/spec/frontend/clusters_list/store/actions_spec.js b/spec/frontend/clusters_list/store/actions_spec.js
index e903200bf1d..70766af3ec4 100644
--- a/spec/frontend/clusters_list/store/actions_spec.js
+++ b/spec/frontend/clusters_list/store/actions_spec.js
@@ -2,6 +2,7 @@ import MockAdapter from 'axios-mock-adapter';
import flashError from '~/flash';
import testAction from 'helpers/vuex_action_helper';
import axios from '~/lib/utils/axios_utils';
+import { apiData } from '../mock_data';
import * as types from '~/clusters_list/store/mutation_types';
import * as actions from '~/clusters_list/store/actions';
@@ -10,8 +11,6 @@ jest.mock('~/flash.js');
describe('Clusters store actions', () => {
describe('fetchClusters', () => {
let mock;
- const endpoint = '/clusters';
- const clusters = [{ name: 'test' }];
beforeEach(() => {
mock = new MockAdapter(axios);
@@ -20,14 +19,29 @@ describe('Clusters store actions', () => {
afterEach(() => mock.restore());
it('should commit SET_CLUSTERS_DATA with received response', done => {
- mock.onGet().reply(200, clusters);
+ const headers = {
+ 'x-total': apiData.clusters.length,
+ 'x-per-page': 20,
+ 'x-page': 1,
+ };
+
+ const paginationInformation = {
+ nextPage: NaN,
+ page: 1,
+ perPage: 20,
+ previousPage: NaN,
+ total: apiData.clusters.length,
+ totalPages: NaN,
+ };
+
+ mock.onGet().reply(200, apiData, headers);
testAction(
actions.fetchClusters,
- { endpoint },
+ { endpoint: apiData.endpoint },
{},
[
- { type: types.SET_CLUSTERS_DATA, payload: clusters },
+ { type: types.SET_CLUSTERS_DATA, payload: { data: apiData, paginationInformation } },
{ type: types.SET_LOADING_STATE, payload: false },
],
[],
@@ -38,13 +52,10 @@ describe('Clusters store actions', () => {
it('should show flash on API error', done => {
mock.onGet().reply(400, 'Not Found');
- testAction(actions.fetchClusters, { endpoint }, {}, [], [], () => {
+ testAction(actions.fetchClusters, { endpoint: apiData.endpoint }, {}, [], [], () => {
expect(flashError).toHaveBeenCalledWith(expect.stringMatching('error'));
done();
});
});
});
});
-
-// prevent babel-plugin-rewire from generating an invalid default during karma tests
-export default () => {};
diff --git a/spec/frontend/code_navigation/components/__snapshots__/popover_spec.js.snap b/spec/frontend/code_navigation/components/__snapshots__/popover_spec.js.snap
index c1534022242..c9fdd388585 100644
--- a/spec/frontend/code_navigation/components/__snapshots__/popover_spec.js.snap
+++ b/spec/frontend/code_navigation/components/__snapshots__/popover_spec.js.snap
@@ -23,17 +23,20 @@ exports[`Code navigation popover component renders popover 1`] = `
<div
class="popover-body"
>
- <gl-deprecated-button-stub
+ <gl-button-stub
+ category="tertiary"
class="w-100"
- href="http://test.com"
- size="md"
+ data-testid="go-to-definition-btn"
+ href="http://gitlab.com/test.js#L20"
+ icon=""
+ size="medium"
target="_blank"
variant="default"
>
Go to definition
- </gl-deprecated-button-stub>
+ </gl-button-stub>
</div>
</div>
`;
diff --git a/spec/frontend/code_navigation/components/app_spec.js b/spec/frontend/code_navigation/components/app_spec.js
index d5693cc4173..6dfc81dcc40 100644
--- a/spec/frontend/code_navigation/components/app_spec.js
+++ b/spec/frontend/code_navigation/components/app_spec.js
@@ -48,6 +48,7 @@ describe('Code navigation app component', () => {
factory({
currentDefinition: { hover: 'console' },
currentDefinitionPosition: { x: 0 },
+ currentBlobPath: 'index.js',
});
expect(wrapper.find(Popover).exists()).toBe(true);
diff --git a/spec/frontend/code_navigation/components/popover_spec.js b/spec/frontend/code_navigation/components/popover_spec.js
index df3bbc7c1c6..858e94cf155 100644
--- a/spec/frontend/code_navigation/components/popover_spec.js
+++ b/spec/frontend/code_navigation/components/popover_spec.js
@@ -1,7 +1,7 @@
import { shallowMount } from '@vue/test-utils';
import Popover from '~/code_navigation/components/popover.vue';
-const DEFINITION_PATH_PREFIX = 'http:/';
+const DEFINITION_PATH_PREFIX = 'http://gitlab.com';
const MOCK_CODE_DATA = Object.freeze({
hover: [
@@ -10,7 +10,7 @@ const MOCK_CODE_DATA = Object.freeze({
value: 'console.log',
},
],
- definition_path: 'test.com',
+ definition_path: 'test.js#L20',
});
const MOCK_DOCS_DATA = Object.freeze({
@@ -20,13 +20,15 @@ const MOCK_DOCS_DATA = Object.freeze({
value: 'console.log',
},
],
- definition_path: 'test.com',
+ definition_path: 'test.js#L20',
});
let wrapper;
-function factory(position, data, definitionPathPrefix) {
- wrapper = shallowMount(Popover, { propsData: { position, data, definitionPathPrefix } });
+function factory({ position, data, definitionPathPrefix, blobPath = 'index.js' }) {
+ wrapper = shallowMount(Popover, {
+ propsData: { position, data, definitionPathPrefix, blobPath },
+ });
}
describe('Code navigation popover component', () => {
@@ -35,14 +37,33 @@ describe('Code navigation popover component', () => {
});
it('renders popover', () => {
- factory({ x: 0, y: 0, height: 0 }, MOCK_CODE_DATA, DEFINITION_PATH_PREFIX);
+ factory({
+ position: { x: 0, y: 0, height: 0 },
+ data: MOCK_CODE_DATA,
+ definitionPathPrefix: DEFINITION_PATH_PREFIX,
+ });
expect(wrapper.element).toMatchSnapshot();
});
+ it('renders link with hash to current file', () => {
+ factory({
+ position: { x: 0, y: 0, height: 0 },
+ data: MOCK_CODE_DATA,
+ definitionPathPrefix: DEFINITION_PATH_PREFIX,
+ blobPath: 'test.js',
+ });
+
+ expect(wrapper.find('[data-testid="go-to-definition-btn"]').attributes('href')).toBe('#L20');
+ });
+
describe('code output', () => {
it('renders code output', () => {
- factory({ x: 0, y: 0, height: 0 }, MOCK_CODE_DATA, DEFINITION_PATH_PREFIX);
+ factory({
+ position: { x: 0, y: 0, height: 0 },
+ data: MOCK_CODE_DATA,
+ definitionPathPrefix: DEFINITION_PATH_PREFIX,
+ });
expect(wrapper.find({ ref: 'code-output' }).exists()).toBe(true);
expect(wrapper.find({ ref: 'doc-output' }).exists()).toBe(false);
@@ -51,7 +72,11 @@ describe('Code navigation popover component', () => {
describe('documentation output', () => {
it('renders code output', () => {
- factory({ x: 0, y: 0, height: 0 }, MOCK_DOCS_DATA, DEFINITION_PATH_PREFIX);
+ factory({
+ position: { x: 0, y: 0, height: 0 },
+ data: MOCK_DOCS_DATA,
+ definitionPathPrefix: DEFINITION_PATH_PREFIX,
+ });
expect(wrapper.find({ ref: 'code-output' }).exists()).toBe(false);
expect(wrapper.find({ ref: 'doc-output' }).exists()).toBe(true);
diff --git a/spec/frontend/code_navigation/store/actions_spec.js b/spec/frontend/code_navigation/store/actions_spec.js
index 6d2ede6dda7..4cf77ed1be5 100644
--- a/spec/frontend/code_navigation/store/actions_spec.js
+++ b/spec/frontend/code_navigation/store/actions_spec.js
@@ -143,6 +143,16 @@ describe('Code navigation actions', () => {
expect(addInteractionClass.mock.calls[0]).toEqual(['index.js', 'test']);
expect(addInteractionClass.mock.calls[1]).toEqual(['index.js', 'console.log']);
});
+
+ it('does not call addInteractionClass when no data exists', () => {
+ const state = {
+ data: null,
+ };
+
+ actions.showBlobInteractionZones({ state }, 'index.js');
+
+ expect(addInteractionClass).not.toHaveBeenCalled();
+ });
});
describe('showDefinition', () => {
@@ -173,7 +183,11 @@ describe('Code navigation actions', () => {
[
{
type: 'SET_CURRENT_DEFINITION',
- payload: { definition: { hover: 'test' }, position: { height: 0, x: 0, y: 0 } },
+ payload: {
+ blobPath: 'index.js',
+ definition: { hover: 'test' },
+ position: { height: 0, x: 0, y: 0 },
+ },
},
],
[],
@@ -193,7 +207,11 @@ describe('Code navigation actions', () => {
[
{
type: 'SET_CURRENT_DEFINITION',
- payload: { definition: { hover: 'test' }, position: { height: 0, x: 0, y: 0 } },
+ payload: {
+ blobPath: 'index.js',
+ definition: { hover: 'test' },
+ position: { height: 0, x: 0, y: 0 },
+ },
},
],
[],
@@ -214,7 +232,11 @@ describe('Code navigation actions', () => {
[
{
type: 'SET_CURRENT_DEFINITION',
- payload: { definition: { hover: 'test' }, position: { height: 0, x: 0, y: 0 } },
+ payload: {
+ blobPath: 'index.js',
+ definition: { hover: 'test' },
+ position: { height: 0, x: 0, y: 0 },
+ },
},
],
[],
diff --git a/spec/frontend/commit/pipelines/pipelines_spec.js b/spec/frontend/commit/pipelines/pipelines_spec.js
index b88cba90b87..86ae207e7b7 100644
--- a/spec/frontend/commit/pipelines/pipelines_spec.js
+++ b/spec/frontend/commit/pipelines/pipelines_spec.js
@@ -118,7 +118,7 @@ describe('Pipelines table in Commits and Merge requests', () => {
let pipelineCopy;
beforeEach(() => {
- pipelineCopy = Object.assign({}, pipeline);
+ pipelineCopy = { ...pipeline };
});
describe('when latest pipeline has detached flag and canRunPipeline is true', () => {
@@ -128,12 +128,7 @@ describe('Pipelines table in Commits and Merge requests', () => {
mock.onGet('endpoint.json').reply(200, [pipelineCopy]);
- vm = mountComponent(
- PipelinesTable,
- Object.assign({}, props, {
- canRunPipeline: true,
- }),
- );
+ vm = mountComponent(PipelinesTable, { ...props, canRunPipeline: true });
setImmediate(() => {
expect(vm.$el.querySelector('.js-run-mr-pipeline')).not.toBeNull();
@@ -149,12 +144,7 @@ describe('Pipelines table in Commits and Merge requests', () => {
mock.onGet('endpoint.json').reply(200, [pipelineCopy]);
- vm = mountComponent(
- PipelinesTable,
- Object.assign({}, props, {
- canRunPipeline: false,
- }),
- );
+ vm = mountComponent(PipelinesTable, { ...props, canRunPipeline: false });
setImmediate(() => {
expect(vm.$el.querySelector('.js-run-mr-pipeline')).toBeNull();
@@ -170,12 +160,7 @@ describe('Pipelines table in Commits and Merge requests', () => {
mock.onGet('endpoint.json').reply(200, [pipelineCopy]);
- vm = mountComponent(
- PipelinesTable,
- Object.assign({}, props, {
- canRunPipeline: true,
- }),
- );
+ vm = mountComponent(PipelinesTable, { ...props, canRunPipeline: true });
setImmediate(() => {
expect(vm.$el.querySelector('.js-run-mr-pipeline')).toBeNull();
@@ -191,12 +176,7 @@ describe('Pipelines table in Commits and Merge requests', () => {
mock.onGet('endpoint.json').reply(200, [pipelineCopy]);
- vm = mountComponent(
- PipelinesTable,
- Object.assign({}, props, {
- canRunPipeline: false,
- }),
- );
+ vm = mountComponent(PipelinesTable, { ...props, canRunPipeline: false });
setImmediate(() => {
expect(vm.$el.querySelector('.js-run-mr-pipeline')).toBeNull();
@@ -211,14 +191,12 @@ describe('Pipelines table in Commits and Merge requests', () => {
mock.onGet('endpoint.json').reply(200, [pipelineCopy]);
- vm = mountComponent(
- PipelinesTable,
- Object.assign({}, props, {
- canRunPipeline: true,
- projectId: '5',
- mergeRequestId: 3,
- }),
- );
+ vm = mountComponent(PipelinesTable, {
+ ...props,
+ canRunPipeline: true,
+ projectId: '5',
+ mergeRequestId: 3,
+ });
});
it('updates the loading state', done => {
diff --git a/spec/javascripts/commit_merge_requests_spec.js b/spec/frontend/commit_merge_requests_spec.js
index 82968e028d1..82968e028d1 100644
--- a/spec/javascripts/commit_merge_requests_spec.js
+++ b/spec/frontend/commit_merge_requests_spec.js
diff --git a/spec/frontend/commits_spec.js b/spec/frontend/commits_spec.js
new file mode 100644
index 00000000000..42bd37570b1
--- /dev/null
+++ b/spec/frontend/commits_spec.js
@@ -0,0 +1,98 @@
+import $ from 'jquery';
+import 'vendor/jquery.endless-scroll';
+import MockAdapter from 'axios-mock-adapter';
+import axios from '~/lib/utils/axios_utils';
+import CommitsList from '~/commits';
+import Pager from '~/pager';
+
+describe('Commits List', () => {
+ let commitsList;
+
+ beforeEach(() => {
+ setFixtures(`
+ <form class="commits-search-form" action="/h5bp/html5-boilerplate/commits/master">
+ <input id="commits-search">
+ </form>
+ <ol id="commits-list"></ol>
+ `);
+ jest.spyOn(Pager, 'init').mockImplementation(() => {});
+ commitsList = new CommitsList(25);
+ });
+
+ it('should be defined', () => {
+ expect(CommitsList).toBeDefined();
+ });
+
+ describe('processCommits', () => {
+ it('should join commit headers', () => {
+ commitsList.$contentList = $(`
+ <div>
+ <li class="commit-header" data-day="2016-09-20">
+ <span class="day">20 Sep, 2016</span>
+ <span class="commits-count">1 commit</span>
+ </li>
+ <li class="commit"></li>
+ </div>
+ `);
+
+ const data = `
+ <li class="commit-header" data-day="2016-09-20">
+ <span class="day">20 Sep, 2016</span>
+ <span class="commits-count">1 commit</span>
+ </li>
+ <li class="commit"></li>
+ `;
+
+ // The last commit header should be removed
+ // since the previous one has the same data-day value.
+ expect(commitsList.processCommits(data).find('li.commit-header').length).toBe(0);
+ });
+ });
+
+ describe('on entering input', () => {
+ let ajaxSpy;
+ let mock;
+
+ beforeEach(() => {
+ commitsList.searchField.val('');
+
+ jest.spyOn(window.history, 'replaceState').mockImplementation(() => {});
+ mock = new MockAdapter(axios);
+
+ mock.onGet('/h5bp/html5-boilerplate/commits/master').reply(200, {
+ html: '<li>Result</li>',
+ });
+
+ ajaxSpy = jest.spyOn(axios, 'get');
+ });
+
+ afterEach(() => {
+ mock.restore();
+ });
+
+ it('should save the last search string', done => {
+ commitsList.searchField.val('GitLab');
+ commitsList
+ .filterResults()
+ .then(() => {
+ expect(ajaxSpy).toHaveBeenCalled();
+ expect(commitsList.lastSearch).toEqual('GitLab');
+
+ done();
+ })
+ .catch(done.fail);
+ });
+
+ it('should not make ajax call if the input does not change', done => {
+ commitsList
+ .filterResults()
+ .then(() => {
+ expect(ajaxSpy).not.toHaveBeenCalled();
+ expect(commitsList.lastSearch).toEqual('');
+
+ done();
+ })
+ .catch(done.fail);
+ });
+ });
+});
diff --git a/spec/frontend/contributors/store/actions_spec.js b/spec/frontend/contributors/store/actions_spec.js
index fe3e2132d9d..55437da837c 100644
--- a/spec/frontend/contributors/store/actions_spec.js
+++ b/spec/frontend/contributors/store/actions_spec.js
@@ -55,6 +55,3 @@ describe('Contributors store actions', () => {
});
});
});
-
-// prevent babel-plugin-rewire from generating an invalid default during karma tests
-export default () => {};
diff --git a/spec/frontend/contributors/store/getters_spec.js b/spec/frontend/contributors/store/getters_spec.js
index e6342a669b7..a4202e0ef4b 100644
--- a/spec/frontend/contributors/store/getters_spec.js
+++ b/spec/frontend/contributors/store/getters_spec.js
@@ -74,6 +74,3 @@ describe('Contributors Store Getters', () => {
});
});
});
-
-// prevent babel-plugin-rewire from generating an invalid default during karma tests
-export default () => {};
diff --git a/spec/frontend/create_cluster/eks_cluster/services/aws_services_facade_spec.js b/spec/frontend/create_cluster/eks_cluster/services/aws_services_facade_spec.js
index 490a2775b67..0ef09b4b87e 100644
--- a/spec/frontend/create_cluster/eks_cluster/services/aws_services_facade_spec.js
+++ b/spec/frontend/create_cluster/eks_cluster/services/aws_services_facade_spec.js
@@ -75,7 +75,7 @@ describe('awsServicesFacade', () => {
});
it('return list of regions where each item has a name and value', () => {
- expect(fetchRoles()).resolves.toEqual(rolesOutput);
+ return expect(fetchRoles()).resolves.toEqual(rolesOutput);
});
});
@@ -91,7 +91,7 @@ describe('awsServicesFacade', () => {
});
it('return list of roles where each item has a name and value', () => {
- expect(fetchRegions()).resolves.toEqual(regionsOutput);
+ return expect(fetchRegions()).resolves.toEqual(regionsOutput);
});
});
@@ -112,7 +112,7 @@ describe('awsServicesFacade', () => {
});
it('return list of key pairs where each item has a name and value', () => {
- expect(fetchKeyPairs({ region })).resolves.toEqual(keyPairsOutput);
+ return expect(fetchKeyPairs({ region })).resolves.toEqual(keyPairsOutput);
});
});
@@ -133,7 +133,7 @@ describe('awsServicesFacade', () => {
});
it('return list of vpcs where each item has a name and value', () => {
- expect(fetchVpcs({ region })).resolves.toEqual(vpcsOutput);
+ return expect(fetchVpcs({ region })).resolves.toEqual(vpcsOutput);
});
});
@@ -151,7 +151,7 @@ describe('awsServicesFacade', () => {
});
it('uses name tag value as the vpc name', () => {
- expect(fetchVpcs({ region })).resolves.toEqual(vpcsOutput);
+ return expect(fetchVpcs({ region })).resolves.toEqual(vpcsOutput);
});
});
@@ -167,7 +167,7 @@ describe('awsServicesFacade', () => {
});
it('return list of subnets where each item has a name and value', () => {
- expect(fetchSubnets({ region, vpc })).resolves.toEqual(subnetsOutput);
+ return expect(fetchSubnets({ region, vpc })).resolves.toEqual(subnetsOutput);
});
});
@@ -189,7 +189,7 @@ describe('awsServicesFacade', () => {
});
it('return list of security groups where each item has a name and value', () => {
- expect(fetchSecurityGroups({ region, vpc })).resolves.toEqual(securityGroupsOutput);
+ return expect(fetchSecurityGroups({ region, vpc })).resolves.toEqual(securityGroupsOutput);
});
});
});
diff --git a/spec/javascripts/create_item_dropdown_spec.js b/spec/frontend/create_item_dropdown_spec.js
index a814952faab..a814952faab 100644
--- a/spec/javascripts/create_item_dropdown_spec.js
+++ b/spec/frontend/create_item_dropdown_spec.js
diff --git a/spec/frontend/custom_metrics/components/custom_metrics_form_fields_spec.js b/spec/frontend/custom_metrics/components/custom_metrics_form_fields_spec.js
index 61cbef0c557..79c37293fe5 100644
--- a/spec/frontend/custom_metrics/components/custom_metrics_form_fields_spec.js
+++ b/spec/frontend/custom_metrics/components/custom_metrics_form_fields_spec.js
@@ -152,7 +152,6 @@ describe('custom metrics form fields component', () => {
describe('when query validation is in flight', () => {
beforeEach(() => {
- jest.useFakeTimers();
mountComponent(
{ metricPersisted: true, ...makeFormData({ query: 'validQuery' }) },
{
diff --git a/spec/frontend/deploy_keys/components/action_btn_spec.js b/spec/frontend/deploy_keys/components/action_btn_spec.js
new file mode 100644
index 00000000000..b8211b02464
--- /dev/null
+++ b/spec/frontend/deploy_keys/components/action_btn_spec.js
@@ -0,0 +1,54 @@
+import { shallowMount } from '@vue/test-utils';
+import { GlLoadingIcon } from '@gitlab/ui';
+import eventHub from '~/deploy_keys/eventhub';
+import actionBtn from '~/deploy_keys/components/action_btn.vue';
+
+describe('Deploy keys action btn', () => {
+ const data = getJSONFixture('deploy_keys/keys.json');
+ const deployKey = data.enabled_keys[0];
+ let wrapper;
+
+ const findLoadingIcon = () => wrapper.find(GlLoadingIcon);
+
+ beforeEach(() => {
+ wrapper = shallowMount(actionBtn, {
+ propsData: {
+ deployKey,
+ type: 'enable',
+ },
+ slots: {
+ default: 'Enable',
+ },
+ });
+ });
+
+ it('renders the default slot', () => {
+ expect(wrapper.text()).toBe('Enable');
+ });
+
+ it('sends eventHub event with btn type', () => {
+ jest.spyOn(eventHub, '$emit').mockImplementation(() => {});
+
+ wrapper.trigger('click');
+
+ return wrapper.vm.$nextTick().then(() => {
+ expect(eventHub.$emit).toHaveBeenCalledWith('enable.key', deployKey, expect.anything());
+ });
+ });
+
+ it('shows loading spinner after click', () => {
+ wrapper.trigger('click');
+
+ return wrapper.vm.$nextTick().then(() => {
+ expect(findLoadingIcon().exists()).toBe(true);
+ });
+ });
+
+ it('disables button after click', () => {
+ wrapper.trigger('click');
+
+ return wrapper.vm.$nextTick().then(() => {
+ expect(wrapper.attributes('disabled')).toBe('disabled');
+ });
+ });
+});
diff --git a/spec/frontend/deploy_keys/components/app_spec.js b/spec/frontend/deploy_keys/components/app_spec.js
new file mode 100644
index 00000000000..291502c9ed7
--- /dev/null
+++ b/spec/frontend/deploy_keys/components/app_spec.js
@@ -0,0 +1,142 @@
+import { mount } from '@vue/test-utils';
+import MockAdapter from 'axios-mock-adapter';
+import { TEST_HOST } from 'spec/test_constants';
+import waitForPromises from 'helpers/wait_for_promises';
+import axios from '~/lib/utils/axios_utils';
+import eventHub from '~/deploy_keys/eventhub';
+import deployKeysApp from '~/deploy_keys/components/app.vue';
+
+const TEST_ENDPOINT = `${TEST_HOST}/dummy/`;
+
+describe('Deploy keys app component', () => {
+ const data = getJSONFixture('deploy_keys/keys.json');
+ let wrapper;
+ let mock;
+
+ const mountComponent = () => {
+ wrapper = mount(deployKeysApp, {
+ propsData: {
+ endpoint: TEST_ENDPOINT,
+ projectId: '8',
+ },
+ });
+
+ return waitForPromises();
+ };
+
+ beforeEach(() => {
+ mock = new MockAdapter(axios);
+ mock.onGet(TEST_ENDPOINT).reply(200, data);
+ });
+
+ afterEach(() => {
+ wrapper.destroy();
+ mock.restore();
+ });
+
+ const findLoadingIcon = () => wrapper.find('.gl-spinner');
+ const findKeyPanels = () => wrapper.findAll('.deploy-keys .nav-links li');
+
+ it('renders loading icon while waiting for request', () => {
+ mock.onGet(TEST_ENDPOINT).reply(() => new Promise());
+
+ mountComponent();
+
+ return wrapper.vm.$nextTick().then(() => {
+ expect(findLoadingIcon().exists()).toBe(true);
+ });
+ });
+
+ it('renders keys panels', () => {
+ return mountComponent().then(() => {
+ expect(findKeyPanels().length).toBe(3);
+ });
+ });
+
+ it.each`
+ selector | label | count
+ ${'.js-deployKeys-tab-enabled_keys'} | ${'Enabled deploy keys'} | ${1}
+ ${'.js-deployKeys-tab-available_project_keys'} | ${'Privately accessible deploy keys'} | ${0}
+ ${'.js-deployKeys-tab-public_keys'} | ${'Publicly accessible deploy keys'} | ${1}
+ `('$selector title is $label with keys count equal to $count', ({ selector, label, count }) => {
+ return mountComponent().then(() => {
+ const element = wrapper.find(selector);
+ expect(element.exists()).toBe(true);
+ expect(element.text().trim()).toContain(label);
+
+ expect(
+ element
+ .find('.badge')
+ .text()
+ .trim(),
+ ).toBe(count.toString());
+ });
+ });
+
+ it('does not render key panels when keys object is empty', () => {
+ mock.onGet(TEST_ENDPOINT).reply(200, []);
+
+ return mountComponent().then(() => {
+ expect(findKeyPanels().length).toBe(0);
+ });
+ });
+
+ it('re-fetches deploy keys when enabling a key', () => {
+ const key = data.public_keys[0];
+ return mountComponent()
+ .then(() => {
+ jest.spyOn(wrapper.vm.service, 'getKeys').mockImplementation(() => {});
+ jest.spyOn(wrapper.vm.service, 'enableKey').mockImplementation(() => Promise.resolve());
+
+ eventHub.$emit('enable.key', key);
+
+ return wrapper.vm.$nextTick();
+ })
+ .then(() => {
+ expect(wrapper.vm.service.enableKey).toHaveBeenCalledWith(key.id);
+ expect(wrapper.vm.service.getKeys).toHaveBeenCalled();
+ });
+ });
+
+ it('re-fetches deploy keys when disabling a key', () => {
+ const key = data.public_keys[0];
+ return mountComponent()
+ .then(() => {
+ jest.spyOn(window, 'confirm').mockReturnValue(true);
+ jest.spyOn(wrapper.vm.service, 'getKeys').mockImplementation(() => {});
+ jest.spyOn(wrapper.vm.service, 'disableKey').mockImplementation(() => Promise.resolve());
+
+ eventHub.$emit('disable.key', key);
+
+ return wrapper.vm.$nextTick();
+ })
+ .then(() => {
+ expect(wrapper.vm.service.disableKey).toHaveBeenCalledWith(key.id);
+ expect(wrapper.vm.service.getKeys).toHaveBeenCalled();
+ });
+ });
+
+ it('calls disableKey when removing a key', () => {
+ const key = data.public_keys[0];
+ return mountComponent()
+ .then(() => {
+ jest.spyOn(window, 'confirm').mockReturnValue(true);
+ jest.spyOn(wrapper.vm.service, 'getKeys').mockImplementation(() => {});
+ jest.spyOn(wrapper.vm.service, 'disableKey').mockImplementation(() => Promise.resolve());
+
+ eventHub.$emit('remove.key', key);
+
+ return wrapper.vm.$nextTick();
+ })
+ .then(() => {
+ expect(wrapper.vm.service.disableKey).toHaveBeenCalledWith(key.id);
+ expect(wrapper.vm.service.getKeys).toHaveBeenCalled();
+ });
+ });
+
+ it('hasKeys returns true when there are keys', () => {
+ return mountComponent().then(() => {
+ expect(wrapper.vm.hasKeys).toEqual(3);
+ });
+ });
+});
diff --git a/spec/frontend/deploy_keys/components/key_spec.js b/spec/frontend/deploy_keys/components/key_spec.js
new file mode 100644
index 00000000000..7d942d969bb
--- /dev/null
+++ b/spec/frontend/deploy_keys/components/key_spec.js
@@ -0,0 +1,161 @@
+import { mount } from '@vue/test-utils';
+import DeployKeysStore from '~/deploy_keys/store';
+import key from '~/deploy_keys/components/key.vue';
+import { getTimeago } from '~/lib/utils/datetime_utility';
+
+describe('Deploy keys key', () => {
+ let wrapper;
+ let store;
+
+ const data = getJSONFixture('deploy_keys/keys.json');
+
+ const findTextAndTrim = selector =>
+ wrapper
+ .find(selector)
+ .text()
+ .trim();
+
+ const createComponent = propsData => {
+ wrapper = mount(key, {
+ propsData: {
+ store,
+ endpoint: 'https://test.host/dummy/endpoint',
+ ...propsData,
+ },
+ });
+ };
+
+ beforeEach(() => {
+ store = new DeployKeysStore();
+ store.keys = data;
+ });
+
+ afterEach(() => {
+ wrapper.destroy();
+ wrapper = null;
+ });
+
+ describe('enabled key', () => {
+ const deployKey = data.enabled_keys[0];
+
+ it('renders the keys title', () => {
+ createComponent({ deployKey });
+
+ expect(findTextAndTrim('.title')).toContain('My title');
+ });
+
+ it('renders human friendly formatted created date', () => {
+ createComponent({ deployKey });
+
+ expect(findTextAndTrim('.key-created-at')).toBe(
+ `${getTimeago().format(deployKey.created_at)}`,
+ );
+ });
+
+ it('shows pencil button for editing', () => {
+ createComponent({ deployKey });
+
+ expect(wrapper.find('.btn .ic-pencil')).toExist();
+ });
+
+ it('shows disable button when the project is not deletable', () => {
+ createComponent({ deployKey });
+
+ expect(wrapper.find('.btn .ic-cancel')).toExist();
+ });
+
+ it('shows remove button when the project is deletable', () => {
+ createComponent({
+ deployKey: { ...deployKey, destroyed_when_orphaned: true, almost_orphaned: true },
+ });
+ expect(wrapper.find('.btn .ic-remove')).toExist();
+ });
+ });
+
+ describe('deploy key labels', () => {
+ const deployKey = data.enabled_keys[0];
+ const deployKeysProjects = [...deployKey.deploy_keys_projects];
+ it('shows write access title when key has write access', () => {
+ deployKeysProjects[0] = { ...deployKeysProjects[0], can_push: true };
+ createComponent({ deployKey: { ...deployKey, deploy_keys_projects: deployKeysProjects } });
+
+ expect(wrapper.find('.deploy-project-label').attributes('data-original-title')).toBe(
+ 'Write access allowed',
+ );
+ });
+
+ it('does not show write access title when key has write access', () => {
+ deployKeysProjects[0] = { ...deployKeysProjects[0], can_push: false };
+ createComponent({ deployKey: { ...deployKey, deploy_keys_projects: deployKeysProjects } });
+
+ expect(wrapper.find('.deploy-project-label').attributes('data-original-title')).toBe(
+ 'Read access only',
+ );
+ });
+
+ it('shows expandable button if more than two projects', () => {
+ createComponent({ deployKey });
+ const labels = wrapper.findAll('.deploy-project-label');
+
+ expect(labels.length).toBe(2);
+ expect(labels.at(1).text()).toContain('others');
+ expect(labels.at(1).attributes('data-original-title')).toContain('Expand');
+ });
+
+ it('expands all project labels after click', () => {
+ createComponent({ deployKey });
+ const { length } = deployKey.deploy_keys_projects;
+ wrapper
+ .findAll('.deploy-project-label')
+ .at(1)
+ .trigger('click');
+
+ return wrapper.vm.$nextTick().then(() => {
+ const labels = wrapper.findAll('.deploy-project-label');
+
+ expect(labels.length).toBe(length);
+ expect(labels.at(1).text()).not.toContain(`+${length} others`);
+ expect(labels.at(1).attributes('data-original-title')).not.toContain('Expand');
+ });
+ });
+
+ it('shows two projects', () => {
+ createComponent({
+ deployKey: { ...deployKey, deploy_keys_projects: [...deployKeysProjects].slice(0, 2) },
+ });
+
+ const labels = wrapper.findAll('.deploy-project-label');
+
+ expect(labels.length).toBe(2);
+ expect(labels.at(1).text()).toContain(deployKey.deploy_keys_projects[1].project.full_name);
+ });
+ });
+
+ describe('public keys', () => {
+ const deployKey = data.public_keys[0];
+
+ it('renders deploy keys without any enabled projects', () => {
+ createComponent({ deployKey: { ...deployKey, deploy_keys_projects: [] } });
+
+ expect(findTextAndTrim('.deploy-project-list')).toBe('None');
+ });
+
+ it('shows enable button', () => {
+ createComponent({ deployKey });
+ expect(findTextAndTrim('.btn')).toBe('Enable');
+ });
+
+ it('shows pencil button for editing', () => {
+ createComponent({ deployKey });
+ expect(wrapper.find('.btn .ic-pencil')).toExist();
+ });
+
+ it('shows disable button when key is enabled', () => {
+ store.keys.enabled_keys.push(deployKey);
+
+ createComponent({ deployKey });
+
+ expect(wrapper.find('.btn .ic-cancel')).toExist();
+ });
+ });
+});
diff --git a/spec/frontend/deploy_keys/components/keys_panel_spec.js b/spec/frontend/deploy_keys/components/keys_panel_spec.js
new file mode 100644
index 00000000000..53c8ba073bc
--- /dev/null
+++ b/spec/frontend/deploy_keys/components/keys_panel_spec.js
@@ -0,0 +1,63 @@
+import { mount } from '@vue/test-utils';
+import DeployKeysStore from '~/deploy_keys/store';
+import deployKeysPanel from '~/deploy_keys/components/keys_panel.vue';
+
+describe('Deploy keys panel', () => {
+ const data = getJSONFixture('deploy_keys/keys.json');
+ let wrapper;
+
+ const findTableRowHeader = () => wrapper.find('.table-row-header');
+
+ const mountComponent = props => {
+ const store = new DeployKeysStore();
+ store.keys = data;
+ wrapper = mount(deployKeysPanel, {
+ propsData: {
+ title: 'test',
+ keys: data.enabled_keys,
+ showHelpBox: true,
+ store,
+ endpoint: 'https://test.host/dummy/endpoint',
+ ...props,
+ },
+ });
+ };
+
+ afterEach(() => {
+ wrapper.destroy();
+ wrapper = null;
+ });
+
+ it('renders list of keys', () => {
+ mountComponent();
+ expect(wrapper.findAll('.deploy-key').length).toBe(wrapper.vm.keys.length);
+ });
+
+ it('renders table header', () => {
+ mountComponent();
+ const tableHeader = findTableRowHeader();
+
+ expect(tableHeader).toExist();
+ expect(tableHeader.text()).toContain('Deploy key');
+ expect(tableHeader.text()).toContain('Project usage');
+ expect(tableHeader.text()).toContain('Created');
+ });
+
+ it('renders help box if keys are empty', () => {
+ mountComponent({ keys: [] });
+
+ expect(wrapper.find('.settings-message').exists()).toBe(true);
+
+ expect(
+ wrapper
+ .find('.settings-message')
+ .text()
+ .trim(),
+ ).toBe('No deploy keys found. Create one with the form above.');
+ });
+
+ it('renders no table header if keys are empty', () => {
+ mountComponent({ keys: [] });
+ expect(findTableRowHeader().exists()).toBe(false);
+ });
+});
diff --git a/spec/frontend/design_management/components/__snapshots__/design_note_pin_spec.js.snap b/spec/frontend/design_management/components/__snapshots__/design_note_pin_spec.js.snap
new file mode 100644
index 00000000000..4828e8cb3c2
--- /dev/null
+++ b/spec/frontend/design_management/components/__snapshots__/design_note_pin_spec.js.snap
@@ -0,0 +1,42 @@
+// Jest Snapshot v1, https://goo.gl/fbAQLP
+
+exports[`Design discussions component should match the snapshot of note when repositioning 1`] = `
+<button
+ aria-label="Comment form position"
+ class="position-absolute btn-transparent comment-indicator"
+ style="left: 10px; top: 10px; cursor: move;"
+ type="button"
+>
+ <icon-stub
+ name="image-comment-dark"
+ size="16"
+ />
+</button>
+`;
+
+exports[`Design discussions component should match the snapshot of note with index 1`] = `
+<button
+ aria-label="Comment '1' position"
+ class="position-absolute js-image-badge badge badge-pill"
+ style="left: 10px; top: 10px;"
+ type="button"
+>
+
+ 1
+
+</button>
+`;
+
+exports[`Design discussions component should match the snapshot of note without index 1`] = `
+<button
+ aria-label="Comment form position"
+ class="position-absolute btn-transparent comment-indicator"
+ style="left: 10px; top: 10px;"
+ type="button"
+>
+ <icon-stub
+ name="image-comment-dark"
+ size="16"
+ />
+</button>
+`;
diff --git a/spec/frontend/design_management/components/__snapshots__/design_presentation_spec.js.snap b/spec/frontend/design_management/components/__snapshots__/design_presentation_spec.js.snap
new file mode 100644
index 00000000000..189962c5b2e
--- /dev/null
+++ b/spec/frontend/design_management/components/__snapshots__/design_presentation_spec.js.snap
@@ -0,0 +1,104 @@
+// Jest Snapshot v1, https://goo.gl/fbAQLP
+
+exports[`Design management design presentation component currentCommentForm is equal to current annotation position when isAnnotating is true 1`] = `
+<div
+ class="h-100 w-100 p-3 overflow-auto position-relative"
+>
+ <div
+ class="h-100 w-100 d-flex align-items-center position-relative"
+ >
+ <design-image-stub
+ image="test.jpg"
+ name="test"
+ scale="1"
+ />
+
+ <design-overlay-stub
+ currentcommentform="[object Object]"
+ dimensions="[object Object]"
+ notes=""
+ position="[object Object]"
+ />
+ </div>
+</div>
+`;
+
+exports[`Design management design presentation component currentCommentForm is null when isAnnotating is false 1`] = `
+<div
+ class="h-100 w-100 p-3 overflow-auto position-relative"
+>
+ <div
+ class="h-100 w-100 d-flex align-items-center position-relative"
+ >
+ <design-image-stub
+ image="test.jpg"
+ name="test"
+ scale="1"
+ />
+
+ <design-overlay-stub
+ dimensions="[object Object]"
+ notes=""
+ position="[object Object]"
+ />
+ </div>
+</div>
+`;
+
+exports[`Design management design presentation component currentCommentForm is null when isAnnotating is true but annotation position is falsey 1`] = `
+<div
+ class="h-100 w-100 p-3 overflow-auto position-relative"
+>
+ <div
+ class="h-100 w-100 d-flex align-items-center position-relative"
+ >
+ <design-image-stub
+ image="test.jpg"
+ name="test"
+ scale="1"
+ />
+
+ <design-overlay-stub
+ dimensions="[object Object]"
+ notes=""
+ position="[object Object]"
+ />
+ </div>
+</div>
+`;
+
+exports[`Design management design presentation component renders empty state when no image provided 1`] = `
+<div
+ class="h-100 w-100 p-3 overflow-auto position-relative"
+>
+ <div
+ class="h-100 w-100 d-flex align-items-center position-relative"
+ >
+ <!---->
+
+ <!---->
+ </div>
+</div>
+`;
+
+exports[`Design management design presentation component renders image and overlay when image provided 1`] = `
+<div
+ class="h-100 w-100 p-3 overflow-auto position-relative"
+>
+ <div
+ class="h-100 w-100 d-flex align-items-center position-relative"
+ >
+ <design-image-stub
+ image="test.jpg"
+ name="test"
+ scale="1"
+ />
+
+ <design-overlay-stub
+ dimensions="[object Object]"
+ notes=""
+ position="[object Object]"
+ />
+ </div>
+</div>
+`;
diff --git a/spec/frontend/design_management/components/__snapshots__/design_scaler_spec.js.snap b/spec/frontend/design_management/components/__snapshots__/design_scaler_spec.js.snap
new file mode 100644
index 00000000000..cb4575cbd11
--- /dev/null
+++ b/spec/frontend/design_management/components/__snapshots__/design_scaler_spec.js.snap
@@ -0,0 +1,115 @@
+// Jest Snapshot v1, https://goo.gl/fbAQLP
+
+exports[`Design management design scaler component minus and reset buttons are disabled when scale === 1 1`] = `
+<div
+ class="design-scaler btn-group"
+ role="group"
+>
+ <button
+ class="btn"
+ disabled="disabled"
+ >
+ <span
+ class="d-flex-center gl-icon s16"
+ >
+
+ –
+
+ </span>
+ </button>
+
+ <button
+ class="btn"
+ disabled="disabled"
+ >
+ <gl-icon-stub
+ name="redo"
+ size="16"
+ />
+ </button>
+
+ <button
+ class="btn"
+ >
+ <gl-icon-stub
+ name="plus"
+ size="16"
+ />
+ </button>
+</div>
+`;
+
+exports[`Design management design scaler component minus and reset buttons are enabled when scale > 1 1`] = `
+<div
+ class="design-scaler btn-group"
+ role="group"
+>
+ <button
+ class="btn"
+ >
+ <span
+ class="d-flex-center gl-icon s16"
+ >
+
+ –
+
+ </span>
+ </button>
+
+ <button
+ class="btn"
+ >
+ <gl-icon-stub
+ name="redo"
+ size="16"
+ />
+ </button>
+
+ <button
+ class="btn"
+ >
+ <gl-icon-stub
+ name="plus"
+ size="16"
+ />
+ </button>
+</div>
+`;
+
+exports[`Design management design scaler component plus button is disabled when scale === 2 1`] = `
+<div
+ class="design-scaler btn-group"
+ role="group"
+>
+ <button
+ class="btn"
+ >
+ <span
+ class="d-flex-center gl-icon s16"
+ >
+
+ –
+
+ </span>
+ </button>
+
+ <button
+ class="btn"
+ >
+ <gl-icon-stub
+ name="redo"
+ size="16"
+ />
+ </button>
+
+ <button
+ class="btn"
+ disabled="disabled"
+ >
+ <gl-icon-stub
+ name="plus"
+ size="16"
+ />
+ </button>
+</div>
+`;
diff --git a/spec/frontend/design_management/components/__snapshots__/image_spec.js.snap b/spec/frontend/design_management/components/__snapshots__/image_spec.js.snap
new file mode 100644
index 00000000000..acaa62b11eb
--- /dev/null
+++ b/spec/frontend/design_management/components/__snapshots__/image_spec.js.snap
@@ -0,0 +1,68 @@
+// Jest Snapshot v1, https://goo.gl/fbAQLP
+
+exports[`Design management large image component renders image 1`] = `
+<div
+ class="m-auto js-design-image"
+>
+ <!---->
+
+ <img
+ alt="test"
+ class="mh-100 img-fluid"
+ src="test.jpg"
+ />
+</div>
+`;
+
+exports[`Design management large image component renders loading state 1`] = `
+<div
+ class="m-auto js-design-image"
+ isloading="true"
+>
+ <!---->
+
+ <img
+ alt=""
+ class="mh-100 img-fluid"
+ src=""
+ />
+</div>
+`;
+
+exports[`Design management large image component renders media broken icon on error 1`] = `
+<gl-icon-stub
+ class="text-secondary-100"
+ name="media-broken"
+ size="48"
+/>
+`;
+
+exports[`Design management large image component sets correct classes and styles if imageStyle is set 1`] = `
+<div
+ class="m-auto js-design-image"
+>
+ <!---->
+
+ <img
+ alt="test"
+ class="mh-100"
+ src="test.jpg"
+ style="width: 100px; height: 100px;"
+ />
+</div>
+`;
+
+exports[`Design management large image component zoom sets image style when zoomed 1`] = `
+<div
+ class="m-auto js-design-image"
+>
+ <!---->
+
+ <img
+ alt="test"
+ class="mh-100"
+ src="test.jpg"
+ style="width: 200px; height: 200px;"
+ />
+</div>
+`;
diff --git a/spec/frontend/design_management/components/delete_button_spec.js b/spec/frontend/design_management/components/delete_button_spec.js
new file mode 100644
index 00000000000..9d3bcd98e44
--- /dev/null
+++ b/spec/frontend/design_management/components/delete_button_spec.js
@@ -0,0 +1,51 @@
+import { shallowMount } from '@vue/test-utils';
+import { GlDeprecatedButton, GlModal, GlModalDirective } from '@gitlab/ui';
+import BatchDeleteButton from '~/design_management/components/delete_button.vue';
+
+describe('Batch delete button component', () => {
+ let wrapper;
+
+ const findButton = () => wrapper.find(GlDeprecatedButton);
+ const findModal = () => wrapper.find(GlModal);
+
+ function createComponent(isDeleting = false) {
+ wrapper = shallowMount(BatchDeleteButton, {
+ propsData: {
+ isDeleting,
+ },
+ directives: {
+ GlModalDirective,
+ },
+ });
+ }
+
+ afterEach(() => {
+ wrapper.destroy();
+ });
+
+ it('renders non-disabled button by default', () => {
+ createComponent();
+
+ expect(findButton().exists()).toBe(true);
+ expect(findButton().attributes('disabled')).toBeFalsy();
+ });
+
+ it('renders disabled button when design is deleting', () => {
+ createComponent(true);
+ expect(findButton().attributes('disabled')).toBeTruthy();
+ });
+
+ it('emits `deleteSelectedDesigns` event on modal ok click', () => {
+ createComponent();
+ findButton().vm.$emit('click');
+ return wrapper.vm
+ .$nextTick()
+ .then(() => {
+ findModal().vm.$emit('ok');
+ return wrapper.vm.$nextTick();
+ })
+ .then(() => {
+ expect(wrapper.emitted().deleteSelectedDesigns).toBeTruthy();
+ });
+ });
+});
diff --git a/spec/frontend/design_management/components/design_note_pin_spec.js b/spec/frontend/design_management/components/design_note_pin_spec.js
new file mode 100644
index 00000000000..4f7260b1363
--- /dev/null
+++ b/spec/frontend/design_management/components/design_note_pin_spec.js
@@ -0,0 +1,49 @@
+import { shallowMount } from '@vue/test-utils';
+import DesignNotePin from '~/design_management/components/design_note_pin.vue';
+
+describe('Design discussions component', () => {
+ let wrapper;
+
+ function createComponent(propsData = {}) {
+ wrapper = shallowMount(DesignNotePin, {
+ propsData: {
+ position: {
+ left: '10px',
+ top: '10px',
+ },
+ ...propsData,
+ },
+ });
+ }
+
+ afterEach(() => {
+ wrapper.destroy();
+ });
+
+ it('should match the snapshot of note without index', () => {
+ createComponent();
+ expect(wrapper.element).toMatchSnapshot();
+ });
+
+ it('should match the snapshot of note with index', () => {
+ createComponent({ label: '1' });
+ expect(wrapper.element).toMatchSnapshot();
+ });
+
+ it('should match the snapshot of note when repositioning', () => {
+ createComponent({ repositioning: true });
+ expect(wrapper.element).toMatchSnapshot();
+ });
+
+ describe('pinStyle', () => {
+ it('sets cursor to `move` when repositioning = true', () => {
+ createComponent({ repositioning: true });
+ expect(wrapper.vm.pinStyle.cursor).toBe('move');
+ });
+
+ it('does not set cursor when repositioning = false', () => {
+ createComponent();
+ expect(wrapper.vm.pinStyle.cursor).toBe(undefined);
+ });
+ });
+});
diff --git a/spec/frontend/design_management/components/design_notes/__snapshots__/design_note_spec.js.snap b/spec/frontend/design_management/components/design_notes/__snapshots__/design_note_spec.js.snap
new file mode 100644
index 00000000000..e071274cc81
--- /dev/null
+++ b/spec/frontend/design_management/components/design_notes/__snapshots__/design_note_spec.js.snap
@@ -0,0 +1,61 @@
+// Jest Snapshot v1, https://goo.gl/fbAQLP
+
+exports[`Design note component should match the snapshot 1`] = `
+<timeline-entry-item-stub
+ class="design-note note-form"
+ id="note_123"
+>
+ <user-avatar-link-stub
+ imgalt=""
+ imgcssclasses=""
+ imgsize="40"
+ imgsrc=""
+ linkhref=""
+ tooltipplacement="top"
+ tooltiptext=""
+ username=""
+ />
+
+ <div
+ class="d-flex justify-content-between"
+ >
+ <div>
+ <a
+ class="js-user-link"
+ data-user-id="author-id"
+ >
+ <span
+ class="note-header-author-name bold"
+ >
+
+ </span>
+
+ <!---->
+
+ <span
+ class="note-headline-light"
+ >
+ @
+ </span>
+ </a>
+
+ <span
+ class="note-headline-light note-headline-meta"
+ >
+ <span
+ class="system-note-message"
+ />
+
+ <!---->
+ </span>
+ </div>
+
+ <!---->
+ </div>
+
+ <div
+ class="note-text js-note-text md"
+ data-qa-selector="note_content"
+ />
+</timeline-entry-item-stub>
+`;
diff --git a/spec/frontend/design_management/components/design_notes/__snapshots__/design_reply_form_spec.js.snap b/spec/frontend/design_management/components/design_notes/__snapshots__/design_reply_form_spec.js.snap
new file mode 100644
index 00000000000..e01c79e3520
--- /dev/null
+++ b/spec/frontend/design_management/components/design_notes/__snapshots__/design_reply_form_spec.js.snap
@@ -0,0 +1,15 @@
+// Jest Snapshot v1, https://goo.gl/fbAQLP
+
+exports[`Design reply form component renders button text as "Comment" when creating a comment 1`] = `
+"<button data-track-event=\\"click_button\\" data-qa-selector=\\"save_comment_button\\" type=\\"submit\\" disabled=\\"disabled\\" class=\\"btn btn-success btn-md disabled\\">
+ <!---->
+ Comment
+</button>"
+`;
+
+exports[`Design reply form component renders button text as "Save comment" when creating a comment 1`] = `
+"<button data-track-event=\\"click_button\\" data-qa-selector=\\"save_comment_button\\" type=\\"submit\\" disabled=\\"disabled\\" class=\\"btn btn-success btn-md disabled\\">
+ <!---->
+ Save comment
+</button>"
+`;
diff --git a/spec/frontend/design_management/components/design_notes/design_discussion_spec.js b/spec/frontend/design_management/components/design_notes/design_discussion_spec.js
new file mode 100644
index 00000000000..b16b26ff82f
--- /dev/null
+++ b/spec/frontend/design_management/components/design_notes/design_discussion_spec.js
@@ -0,0 +1,133 @@
+import { shallowMount } from '@vue/test-utils';
+import { ApolloMutation } from 'vue-apollo';
+import DesignDiscussion from '~/design_management/components/design_notes/design_discussion.vue';
+import DesignNote from '~/design_management/components/design_notes/design_note.vue';
+import DesignReplyForm from '~/design_management/components/design_notes/design_reply_form.vue';
+import createNoteMutation from '~/design_management/graphql/mutations/createNote.mutation.graphql';
+import ReplyPlaceholder from '~/notes/components/discussion_reply_placeholder.vue';
+
+describe('Design discussions component', () => {
+ let wrapper;
+
+ const findReplyPlaceholder = () => wrapper.find(ReplyPlaceholder);
+ const findReplyForm = () => wrapper.find(DesignReplyForm);
+
+ const mutationVariables = {
+ mutation: createNoteMutation,
+ update: expect.anything(),
+ variables: {
+ input: {
+ noteableId: 'noteable-id',
+ body: 'test',
+ discussionId: '0',
+ },
+ },
+ };
+ const mutate = jest.fn(() => Promise.resolve());
+ const $apollo = {
+ mutate,
+ };
+
+ function createComponent(props = {}, data = {}) {
+ wrapper = shallowMount(DesignDiscussion, {
+ propsData: {
+ discussion: {
+ id: '0',
+ notes: [
+ {
+ id: '1',
+ },
+ {
+ id: '2',
+ },
+ ],
+ },
+ noteableId: 'noteable-id',
+ designId: 'design-id',
+ discussionIndex: 1,
+ ...props,
+ },
+ data() {
+ return {
+ ...data,
+ };
+ },
+ stubs: {
+ ReplyPlaceholder,
+ ApolloMutation,
+ },
+ mocks: { $apollo },
+ });
+ }
+
+ afterEach(() => {
+ wrapper.destroy();
+ });
+
+ it('renders correct amount of discussion notes', () => {
+ createComponent();
+ expect(wrapper.findAll(DesignNote)).toHaveLength(2);
+ });
+
+ it('renders reply placeholder by default', () => {
+ createComponent();
+ expect(findReplyPlaceholder().exists()).toBe(true);
+ });
+
+ it('hides reply placeholder and opens form on placeholder click', () => {
+ createComponent();
+ findReplyPlaceholder().trigger('click');
+
+ return wrapper.vm.$nextTick().then(() => {
+ expect(findReplyPlaceholder().exists()).toBe(false);
+ expect(findReplyForm().exists()).toBe(true);
+ });
+ });
+
+ it('calls mutation on submitting form and closes the form', () => {
+ createComponent({}, { discussionComment: 'test', isFormRendered: true });
+
+ findReplyForm().vm.$emit('submitForm');
+ expect(mutate).toHaveBeenCalledWith(mutationVariables);
+
+ return mutate()
+ .then(() => {
+ return wrapper.vm.$nextTick();
+ })
+ .then(() => {
+ expect(findReplyForm().exists()).toBe(false);
+ });
+ });
+
+ it('clears the discussion comment on closing comment form', () => {
+ createComponent({}, { discussionComment: 'test', isFormRendered: true });
+
+ return wrapper.vm
+ .$nextTick()
+ .then(() => {
+ findReplyForm().vm.$emit('cancelForm');
+
+ expect(wrapper.vm.discussionComment).toBe('');
+ return wrapper.vm.$nextTick();
+ })
+ .then(() => {
+ expect(findReplyForm().exists()).toBe(false);
+ });
+ });
+
+ it('applies correct class to design notes when discussion is highlighted', () => {
+ createComponent(
+ {},
+ {
+ activeDiscussion: {
+ id: '1',
+ source: 'pin',
+ },
+ },
+ );
+
+ expect(wrapper.findAll(DesignNote).wrappers.every(note => note.classes('gl-bg-blue-50'))).toBe(
+ true,
+ );
+ });
+});
diff --git a/spec/frontend/design_management/components/design_notes/design_note_spec.js b/spec/frontend/design_management/components/design_notes/design_note_spec.js
new file mode 100644
index 00000000000..8b32d3022ee
--- /dev/null
+++ b/spec/frontend/design_management/components/design_notes/design_note_spec.js
@@ -0,0 +1,170 @@
+import { shallowMount } from '@vue/test-utils';
+import { ApolloMutation } from 'vue-apollo';
+import DesignNote from '~/design_management/components/design_notes/design_note.vue';
+import UserAvatarLink from '~/vue_shared/components/user_avatar/user_avatar_link.vue';
+import TimeAgoTooltip from '~/vue_shared/components/time_ago_tooltip.vue';
+import DesignReplyForm from '~/design_management/components/design_notes/design_reply_form.vue';
+
+const scrollIntoViewMock = jest.fn();
+const note = {
+ id: 'gid://gitlab/DiffNote/123',
+ author: {
+ id: 'author-id',
+ },
+ body: 'test',
+ userPermissions: {
+ adminNote: false,
+ },
+};
+HTMLElement.prototype.scrollIntoView = scrollIntoViewMock;
+
+const $route = {
+ hash: '#note_123',
+};
+
+const mutate = jest.fn().mockResolvedValue({ data: { updateNote: {} } });
+
+describe('Design note component', () => {
+ let wrapper;
+
+ const findUserAvatar = () => wrapper.find(UserAvatarLink);
+ const findUserLink = () => wrapper.find('.js-user-link');
+ const findReplyForm = () => wrapper.find(DesignReplyForm);
+ const findEditButton = () => wrapper.find('.js-note-edit');
+ const findNoteContent = () => wrapper.find('.js-note-text');
+
+ function createComponent(props = {}, data = { isEditing: false }) {
+ wrapper = shallowMount(DesignNote, {
+ propsData: {
+ note: {},
+ ...props,
+ },
+ data() {
+ return {
+ ...data,
+ };
+ },
+ mocks: {
+ $route,
+ $apollo: {
+ mutate,
+ },
+ },
+ stubs: {
+ ApolloMutation,
+ },
+ });
+ }
+
+ afterEach(() => {
+ wrapper.destroy();
+ });
+
+ it('should match the snapshot', () => {
+ createComponent({
+ note,
+ });
+
+ expect(wrapper.element).toMatchSnapshot();
+ });
+
+ it('should render an author', () => {
+ createComponent({
+ note,
+ });
+
+ expect(findUserAvatar().exists()).toBe(true);
+ expect(findUserLink().exists()).toBe(true);
+ });
+
+ it('should render a time ago tooltip if note has createdAt property', () => {
+ createComponent({
+ note: {
+ ...note,
+ createdAt: '2019-07-26T15:02:20Z',
+ },
+ });
+
+ expect(wrapper.find(TimeAgoTooltip).exists()).toBe(true);
+ });
+
+ it('should trigger a scrollIntoView method', () => {
+ createComponent({
+ note,
+ });
+
+ expect(scrollIntoViewMock).toHaveBeenCalled();
+ });
+
+ it('should not render edit icon when user does not have a permission', () => {
+ createComponent({
+ note,
+ });
+
+ expect(findEditButton().exists()).toBe(false);
+ });
+
+ describe('when user has a permission to edit note', () => {
+ it('should open an edit form on edit button click', () => {
+ createComponent({
+ note: {
+ ...note,
+ userPermissions: {
+ adminNote: true,
+ },
+ },
+ });
+
+ findEditButton().trigger('click');
+
+ return wrapper.vm.$nextTick().then(() => {
+ expect(findReplyForm().exists()).toBe(true);
+ expect(findNoteContent().exists()).toBe(false);
+ });
+ });
+
+ describe('when edit form is rendered', () => {
+ beforeEach(() => {
+ createComponent(
+ {
+ note: {
+ ...note,
+ userPermissions: {
+ adminNote: true,
+ },
+ },
+ },
+ { isEditing: true },
+ );
+ });
+
+ it('should not render note content and should render reply form', () => {
+ expect(findNoteContent().exists()).toBe(false);
+ expect(findReplyForm().exists()).toBe(true);
+ });
+
+ it('hides the form on hideForm event', () => {
+ findReplyForm().vm.$emit('cancelForm');
+
+ return wrapper.vm.$nextTick().then(() => {
+ expect(findReplyForm().exists()).toBe(false);
+ expect(findNoteContent().exists()).toBe(true);
+ });
+ });
+
+ it('calls a mutation on submitForm event and hides a form', () => {
+ findReplyForm().vm.$emit('submitForm');
+ expect(mutate).toHaveBeenCalled();
+
+ return mutate()
+ .then(() => {
+ return wrapper.vm.$nextTick();
+ })
+ .then(() => {
+ expect(findReplyForm().exists()).toBe(false);
+ expect(findNoteContent().exists()).toBe(true);
+ });
+ });
+ });
+ });
+});
diff --git a/spec/frontend/design_management/components/design_notes/design_reply_form_spec.js b/spec/frontend/design_management/components/design_notes/design_reply_form_spec.js
new file mode 100644
index 00000000000..34b8f1f9fa8
--- /dev/null
+++ b/spec/frontend/design_management/components/design_notes/design_reply_form_spec.js
@@ -0,0 +1,182 @@
+import { mount } from '@vue/test-utils';
+import DesignReplyForm from '~/design_management/components/design_notes/design_reply_form.vue';
+
+const showModal = jest.fn();
+
+const GlModal = {
+ template: '<div><slot name="modal-title"></slot><slot></slot><slot name="modal-ok"></slot></div>',
+ methods: {
+ show: showModal,
+ },
+};
+
+describe('Design reply form component', () => {
+ let wrapper;
+
+ const findTextarea = () => wrapper.find('textarea');
+ const findSubmitButton = () => wrapper.find({ ref: 'submitButton' });
+ const findCancelButton = () => wrapper.find({ ref: 'cancelButton' });
+ const findModal = () => wrapper.find({ ref: 'cancelCommentModal' });
+
+ function createComponent(props = {}) {
+ wrapper = mount(DesignReplyForm, {
+ propsData: {
+ value: '',
+ isSaving: false,
+ ...props,
+ },
+ stubs: { GlModal },
+ });
+ }
+
+ afterEach(() => {
+ wrapper.destroy();
+ });
+
+ it('textarea has focus after component mount', () => {
+ createComponent();
+
+ expect(findTextarea().element).toEqual(document.activeElement);
+ });
+
+ it('renders button text as "Comment" when creating a comment', () => {
+ createComponent();
+
+ expect(findSubmitButton().html()).toMatchSnapshot();
+ });
+
+ it('renders button text as "Save comment" when creating a comment', () => {
+ createComponent({ isNewComment: false });
+
+ expect(findSubmitButton().html()).toMatchSnapshot();
+ });
+
+ describe('when form has no text', () => {
+ beforeEach(() => {
+ createComponent({
+ value: '',
+ });
+ });
+
+ it('submit button is disabled', () => {
+ expect(findSubmitButton().attributes().disabled).toBeTruthy();
+ });
+
+ it('does not emit submitForm event on textarea ctrl+enter keydown', () => {
+ findTextarea().trigger('keydown.enter', {
+ ctrlKey: true,
+ });
+
+ return wrapper.vm.$nextTick().then(() => {
+ expect(wrapper.emitted('submitForm')).toBeFalsy();
+ });
+ });
+
+ it('does not emit submitForm event on textarea meta+enter keydown', () => {
+ findTextarea().trigger('keydown.enter', {
+ metaKey: true,
+ });
+
+ return wrapper.vm.$nextTick().then(() => {
+ expect(wrapper.emitted('submitForm')).toBeFalsy();
+ });
+ });
+
+ it('emits cancelForm event on pressing escape button on textarea', () => {
+ findTextarea().trigger('keyup.esc');
+
+ expect(wrapper.emitted('cancelForm')).toBeTruthy();
+ });
+
+ it('emits cancelForm event on clicking Cancel button', () => {
+ findCancelButton().vm.$emit('click');
+
+ expect(wrapper.emitted('cancelForm')).toHaveLength(1);
+ });
+ });
+
+ describe('when form has text', () => {
+ beforeEach(() => {
+ createComponent({
+ value: 'test',
+ });
+ });
+
+ it('submit button is enabled', () => {
+ expect(findSubmitButton().attributes().disabled).toBeFalsy();
+ });
+
+ it('emits submitForm event on Comment button click', () => {
+ findSubmitButton().vm.$emit('click');
+
+ return wrapper.vm.$nextTick().then(() => {
+ expect(wrapper.emitted('submitForm')).toBeTruthy();
+ });
+ });
+
+ it('emits submitForm event on textarea ctrl+enter keydown', () => {
+ findTextarea().trigger('keydown.enter', {
+ ctrlKey: true,
+ });
+
+ return wrapper.vm.$nextTick().then(() => {
+ expect(wrapper.emitted('submitForm')).toBeTruthy();
+ });
+ });
+
+ it('emits submitForm event on textarea meta+enter keydown', () => {
+ findTextarea().trigger('keydown.enter', {
+ metaKey: true,
+ });
+
+ return wrapper.vm.$nextTick().then(() => {
+ expect(wrapper.emitted('submitForm')).toBeTruthy();
+ });
+ });
+
+ it('emits input event on changing textarea content', () => {
+ findTextarea().setValue('test2');
+
+ return wrapper.vm.$nextTick().then(() => {
+ expect(wrapper.emitted('input')).toBeTruthy();
+ });
+ });
+
+ it('emits cancelForm event on Escape key if text was not changed', () => {
+ findTextarea().trigger('keyup.esc');
+
+ expect(wrapper.emitted('cancelForm')).toBeTruthy();
+ });
+
+ it('opens confirmation modal on Escape key when text has changed', () => {
+ wrapper.setProps({ value: 'test2' });
+
+ return wrapper.vm.$nextTick().then(() => {
+ findTextarea().trigger('keyup.esc');
+ expect(showModal).toHaveBeenCalled();
+ });
+ });
+
+ it('emits cancelForm event on Cancel button click if text was not changed', () => {
+ findCancelButton().trigger('click');
+
+ expect(wrapper.emitted('cancelForm')).toBeTruthy();
+ });
+
+ it('opens confirmation modal on Cancel button click when text has changed', () => {
+ wrapper.setProps({ value: 'test2' });
+
+ return wrapper.vm.$nextTick().then(() => {
+ findCancelButton().trigger('click');
+ expect(showModal).toHaveBeenCalled();
+ });
+ });
+
+ it('emits cancelForm event on modal Ok button click', () => {
+ findTextarea().trigger('keyup.esc');
+ findModal().vm.$emit('ok');
+
+ expect(wrapper.emitted('cancelForm')).toBeTruthy();
+ });
+ });
+});
diff --git a/spec/frontend/design_management/components/design_overlay_spec.js b/spec/frontend/design_management/components/design_overlay_spec.js
new file mode 100644
index 00000000000..1c9b130aca6
--- /dev/null
+++ b/spec/frontend/design_management/components/design_overlay_spec.js
@@ -0,0 +1,393 @@
+import { mount } from '@vue/test-utils';
+import DesignOverlay from '~/design_management/components/design_overlay.vue';
+import updateActiveDiscussion from '~/design_management/graphql/mutations/update_active_discussion.mutation.graphql';
+import notes from '../mock_data/notes';
+import { ACTIVE_DISCUSSION_SOURCE_TYPES } from '~/design_management/constants';
+
+const mutate = jest.fn(() => Promise.resolve());
+
+describe('Design overlay component', () => {
+ let wrapper;
+
+ const mockDimensions = { width: 100, height: 100 };
+ const mockNoteNotAuthorised = {
+ id: 'note-not-authorised',
+ discussion: { id: 'discussion-not-authorised' },
+ position: {
+ x: 1,
+ y: 80,
+ ...mockDimensions,
+ },
+ userPermissions: {},
+ };
+
+ const findOverlay = () => wrapper.find('.image-diff-overlay');
+ const findAllNotes = () => wrapper.findAll('.js-image-badge');
+ const findCommentBadge = () => wrapper.find('.comment-indicator');
+ const findFirstBadge = () => findAllNotes().at(0);
+ const findSecondBadge = () => findAllNotes().at(1);
+
+ const clickAndDragBadge = (elem, fromPoint, toPoint) => {
+ elem.trigger('mousedown', { clientX: fromPoint.x, clientY: fromPoint.y });
+ return wrapper.vm.$nextTick().then(() => {
+ elem.trigger('mousemove', { clientX: toPoint.x, clientY: toPoint.y });
+ return wrapper.vm.$nextTick();
+ });
+ };
+
+ function createComponent(props = {}, data = {}) {
+ wrapper = mount(DesignOverlay, {
+ propsData: {
+ dimensions: mockDimensions,
+ position: {
+ top: '0',
+ left: '0',
+ },
+ ...props,
+ },
+ data() {
+ return {
+ activeDiscussion: {
+ id: null,
+ source: null,
+ },
+ ...data,
+ };
+ },
+ mocks: {
+ $apollo: {
+ mutate,
+ },
+ },
+ });
+ }
+
+ it('should have correct inline style', () => {
+ createComponent();
+
+ expect(wrapper.find('.image-diff-overlay').attributes().style).toBe(
+ 'width: 100px; height: 100px; top: 0px; left: 0px;',
+ );
+ });
+
+ it('should emit `openCommentForm` when clicking on overlay', () => {
+ createComponent();
+ const newCoordinates = {
+ x: 10,
+ y: 10,
+ };
+
+ wrapper
+ .find('.image-diff-overlay-add-comment')
+ .trigger('mouseup', { offsetX: newCoordinates.x, offsetY: newCoordinates.y });
+ return wrapper.vm.$nextTick().then(() => {
+ expect(wrapper.emitted('openCommentForm')).toEqual([
+ [{ x: newCoordinates.x, y: newCoordinates.y }],
+ ]);
+ });
+ });
+
+ describe('with notes', () => {
+ beforeEach(() => {
+ createComponent({
+ notes,
+ });
+ });
+
+ it('should render a correct amount of notes', () => {
+ expect(findAllNotes()).toHaveLength(notes.length);
+ });
+
+ it('should have a correct style for each note badge', () => {
+ expect(findFirstBadge().attributes().style).toBe('left: 10px; top: 15px;');
+ expect(findSecondBadge().attributes().style).toBe('left: 50px; top: 50px;');
+ });
+
+ it('should recalculate badges positions on window resize', () => {
+ createComponent({
+ notes,
+ dimensions: {
+ width: 400,
+ height: 400,
+ },
+ });
+
+ expect(findFirstBadge().attributes().style).toBe('left: 40px; top: 60px;');
+
+ wrapper.setProps({
+ dimensions: {
+ width: 200,
+ height: 200,
+ },
+ });
+
+ return wrapper.vm.$nextTick().then(() => {
+ expect(findFirstBadge().attributes().style).toBe('left: 20px; top: 30px;');
+ });
+ });
+
+ it('should call an update active discussion mutation when clicking a note without moving it', () => {
+ const note = notes[0];
+ const { position } = note;
+ const mutationVariables = {
+ mutation: updateActiveDiscussion,
+ variables: {
+ id: note.id,
+ source: ACTIVE_DISCUSSION_SOURCE_TYPES.pin,
+ },
+ };
+
+ findFirstBadge().trigger('mousedown', { clientX: position.x, clientY: position.y });
+
+ return wrapper.vm.$nextTick().then(() => {
+ findFirstBadge().trigger('mouseup', { clientX: position.x, clientY: position.y });
+ expect(mutate).toHaveBeenCalledWith(mutationVariables);
+ });
+ });
+
+ it('when there is an active discussion, should apply inactive class to all pins besides the active one', () => {
+ wrapper.setData({
+ activeDiscussion: {
+ id: notes[0].id,
+ source: 'discussion',
+ },
+ });
+
+ return wrapper.vm.$nextTick().then(() => {
+ expect(findSecondBadge().classes()).toContain('inactive');
+ });
+ });
+ });
+
+ describe('when moving notes', () => {
+ it('should update badge style when note is being moved', () => {
+ createComponent({
+ notes,
+ });
+
+ const { position } = notes[0];
+
+ return clickAndDragBadge(
+ findFirstBadge(),
+ { x: position.x, y: position.y },
+ { x: 20, y: 20 },
+ ).then(() => {
+ expect(findFirstBadge().attributes().style).toBe('left: 20px; top: 20px; cursor: move;');
+ });
+ });
+
+ it('should emit `moveNote` event when note-moving action ends', () => {
+ createComponent({ notes });
+ const note = notes[0];
+ const { position } = note;
+ const newCoordinates = { x: 20, y: 20 };
+
+ wrapper.setData({
+ movingNoteNewPosition: {
+ ...position,
+ ...newCoordinates,
+ },
+ movingNoteStartPosition: {
+ noteId: notes[0].id,
+ discussionId: notes[0].discussion.id,
+ ...position,
+ },
+ });
+
+ const badge = findFirstBadge();
+ return clickAndDragBadge(badge, { x: position.x, y: position.y }, newCoordinates)
+ .then(() => {
+ badge.trigger('mouseup');
+ return wrapper.vm.$nextTick();
+ })
+ .then(() => {
+ expect(wrapper.emitted('moveNote')).toEqual([
+ [
+ {
+ noteId: notes[0].id,
+ discussionId: notes[0].discussion.id,
+ coordinates: newCoordinates,
+ },
+ ],
+ ]);
+ });
+ });
+
+ it('should do nothing if [adminNote] permission is not present', () => {
+ createComponent({
+ dimensions: mockDimensions,
+ notes: [mockNoteNotAuthorised],
+ });
+
+ const badge = findAllNotes().at(0);
+ return clickAndDragBadge(
+ badge,
+ { x: mockNoteNotAuthorised.x, y: mockNoteNotAuthorised.y },
+ { x: 20, y: 20 },
+ ).then(() => {
+ expect(wrapper.vm.movingNoteStartPosition).toBeNull();
+ expect(findFirstBadge().attributes().style).toBe('left: 1px; top: 80px;');
+ });
+ });
+ });
+
+ describe('with a new form', () => {
+ it('should render a new comment badge', () => {
+ createComponent({
+ currentCommentForm: {
+ ...notes[0].position,
+ },
+ });
+
+ expect(findCommentBadge().exists()).toBe(true);
+ expect(findCommentBadge().attributes().style).toBe('left: 10px; top: 15px;');
+ });
+
+ describe('when moving the comment badge', () => {
+ it('should update badge style to reflect new position', () => {
+ const { position } = notes[0];
+
+ createComponent({
+ currentCommentForm: {
+ ...position,
+ },
+ });
+
+ return clickAndDragBadge(
+ findCommentBadge(),
+ { x: position.x, y: position.y },
+ { x: 20, y: 20 },
+ ).then(() => {
+ expect(findCommentBadge().attributes().style).toBe(
+ 'left: 20px; top: 20px; cursor: move;',
+ );
+ });
+ });
+
+ it('should update badge style when note-moving action ends', () => {
+ const { position } = notes[0];
+ createComponent({
+ currentCommentForm: {
+ ...position,
+ },
+ });
+
+ const commentBadge = findCommentBadge();
+ const toPoint = { x: 20, y: 20 };
+
+ return clickAndDragBadge(commentBadge, { x: position.x, y: position.y }, toPoint)
+ .then(() => {
+ commentBadge.trigger('mouseup');
+ // simulates the currentCommentForm being updated in index.vue component, and
+ // propagated back down to this prop
+ wrapper.setProps({
+ currentCommentForm: { height: position.height, width: position.width, ...toPoint },
+ });
+ return wrapper.vm.$nextTick();
+ })
+ .then(() => {
+ expect(commentBadge.attributes().style).toBe('left: 20px; top: 20px;');
+ });
+ });
+
+ it.each`
+ element | getElementFunc | event
+ ${'overlay'} | ${findOverlay} | ${'mouseleave'}
+ ${'comment badge'} | ${findCommentBadge} | ${'mouseup'}
+ `(
+ 'should emit `openCommentForm` event when $event fired on $element element',
+ ({ getElementFunc, event }) => {
+ createComponent({
+ notes,
+ currentCommentForm: {
+ ...notes[0].position,
+ },
+ });
+
+ const newCoordinates = { x: 20, y: 20 };
+ wrapper.setData({
+ movingNoteStartPosition: {
+ ...notes[0].position,
+ },
+ movingNoteNewPosition: {
+ ...notes[0].position,
+ ...newCoordinates,
+ },
+ });
+
+ getElementFunc().trigger(event);
+ return wrapper.vm.$nextTick().then(() => {
+ expect(wrapper.emitted('openCommentForm')).toEqual([[newCoordinates]]);
+ });
+ },
+ );
+ });
+ });
+
+ describe('getMovingNotePositionDelta', () => {
+ it('should calculate delta correctly from state', () => {
+ createComponent();
+
+ wrapper.setData({
+ movingNoteStartPosition: {
+ clientX: 10,
+ clientY: 20,
+ },
+ });
+
+ const mockMouseEvent = {
+ clientX: 30,
+ clientY: 10,
+ };
+
+ expect(wrapper.vm.getMovingNotePositionDelta(mockMouseEvent)).toEqual({
+ deltaX: 20,
+ deltaY: -10,
+ });
+ });
+ });
+
+ describe('isPositionInOverlay', () => {
+ createComponent({ dimensions: mockDimensions });
+
+ it.each`
+ test | coordinates | expectedResult
+ ${'within overlay bounds'} | ${{ x: 50, y: 50 }} | ${true}
+ ${'outside overlay bounds'} | ${{ x: 101, y: 101 }} | ${false}
+ `('returns [$expectedResult] when position is $test', ({ coordinates, expectedResult }) => {
+ const position = { ...mockDimensions, ...coordinates };
+
+ expect(wrapper.vm.isPositionInOverlay(position)).toBe(expectedResult);
+ });
+ });
+
+ describe('getNoteRelativePosition', () => {
+ it('calculates position correctly', () => {
+ createComponent({ dimensions: mockDimensions });
+ const position = { x: 50, y: 50, width: 200, height: 200 };
+
+ expect(wrapper.vm.getNoteRelativePosition(position)).toEqual({ left: 25, top: 25 });
+ });
+ });
+
+ describe('canMoveNote', () => {
+ it.each`
+ adminNotePermission | canMoveNoteResult
+ ${true} | ${true}
+ ${false} | ${false}
+ ${undefined} | ${false}
+ `(
+ 'returns [$canMoveNoteResult] when [adminNote permission] is [$adminNotePermission]',
+ ({ adminNotePermission, canMoveNoteResult }) => {
+ createComponent();
+
+ const note = {
+ userPermissions: {
+ adminNote: adminNotePermission,
+ },
+ };
+ expect(wrapper.vm.canMoveNote(note)).toBe(canMoveNoteResult);
+ },
+ );
+ });
+});
diff --git a/spec/frontend/design_management/components/design_presentation_spec.js b/spec/frontend/design_management/components/design_presentation_spec.js
new file mode 100644
index 00000000000..8a709393d92
--- /dev/null
+++ b/spec/frontend/design_management/components/design_presentation_spec.js
@@ -0,0 +1,546 @@
+import { shallowMount } from '@vue/test-utils';
+import DesignPresentation from '~/design_management/components/design_presentation.vue';
+import DesignOverlay from '~/design_management/components/design_overlay.vue';
+
+const mockOverlayData = {
+ overlayDimensions: {
+ width: 100,
+ height: 100,
+ },
+ overlayPosition: {
+ top: '0',
+ left: '0',
+ },
+};
+
+describe('Design management design presentation component', () => {
+ let wrapper;
+
+ function createComponent(
+ { image, imageName, discussions = [], isAnnotating = false } = {},
+ data = {},
+ stubs = {},
+ ) {
+ wrapper = shallowMount(DesignPresentation, {
+ propsData: {
+ image,
+ imageName,
+ discussions,
+ isAnnotating,
+ },
+ stubs,
+ });
+
+ wrapper.setData(data);
+ wrapper.element.scrollTo = jest.fn();
+ }
+
+ const findOverlayCommentButton = () => wrapper.find('.image-diff-overlay-add-comment');
+
+ /**
+ * Spy on $refs and mock given values
+ * @param {Object} viewportDimensions {width, height}
+ * @param {Object} childDimensions {width, height}
+ * @param {Float} scrollTopPerc 0 < x < 1
+ * @param {Float} scrollLeftPerc 0 < x < 1
+ */
+ function mockRefDimensions(
+ ref,
+ viewportDimensions,
+ childDimensions,
+ scrollTopPerc,
+ scrollLeftPerc,
+ ) {
+ jest.spyOn(ref, 'scrollWidth', 'get').mockReturnValue(childDimensions.width);
+ jest.spyOn(ref, 'scrollHeight', 'get').mockReturnValue(childDimensions.height);
+ jest.spyOn(ref, 'offsetWidth', 'get').mockReturnValue(viewportDimensions.width);
+ jest.spyOn(ref, 'offsetHeight', 'get').mockReturnValue(viewportDimensions.height);
+ jest
+ .spyOn(ref, 'scrollLeft', 'get')
+ .mockReturnValue((childDimensions.width - viewportDimensions.width) * scrollLeftPerc);
+ jest
+ .spyOn(ref, 'scrollTop', 'get')
+ .mockReturnValue((childDimensions.height - viewportDimensions.height) * scrollTopPerc);
+ }
+
+ function clickDragExplore(startCoords, endCoords, { useTouchEvents, mouseup } = {}) {
+ const event = useTouchEvents
+ ? {
+ mousedown: 'touchstart',
+ mousemove: 'touchmove',
+ mouseup: 'touchend',
+ }
+ : {
+ mousedown: 'mousedown',
+ mousemove: 'mousemove',
+ mouseup: 'mouseup',
+ };
+
+ const addCommentOverlay = findOverlayCommentButton();
+
+ // triggering mouse events on this element best simulates
+ // reality, as it is the lowest-level node that needs to
+ // respond to mouse events
+ addCommentOverlay.trigger(event.mousedown, {
+ clientX: startCoords.clientX,
+ clientY: startCoords.clientY,
+ });
+ return wrapper.vm
+ .$nextTick()
+ .then(() => {
+ addCommentOverlay.trigger(event.mousemove, {
+ clientX: endCoords.clientX,
+ clientY: endCoords.clientY,
+ });
+
+ return wrapper.vm.$nextTick();
+ })
+ .then(() => {
+ if (mouseup) {
+ addCommentOverlay.trigger(event.mouseup);
+ return wrapper.vm.$nextTick();
+ }
+
+ return undefined;
+ });
+ }
+
+ afterEach(() => {
+ wrapper.destroy();
+ });
+
+ it('renders image and overlay when image provided', () => {
+ createComponent(
+ {
+ image: 'test.jpg',
+ imageName: 'test',
+ },
+ mockOverlayData,
+ );
+
+ return wrapper.vm.$nextTick().then(() => {
+ expect(wrapper.element).toMatchSnapshot();
+ });
+ });
+
+ it('renders empty state when no image provided', () => {
+ createComponent();
+
+ return wrapper.vm.$nextTick().then(() => {
+ expect(wrapper.element).toMatchSnapshot();
+ });
+ });
+
+ it('openCommentForm event emits correct data', () => {
+ createComponent(
+ {
+ image: 'test.jpg',
+ imageName: 'test',
+ },
+ mockOverlayData,
+ );
+
+ wrapper.vm.openCommentForm({ x: 1, y: 1 });
+
+ return wrapper.vm.$nextTick().then(() => {
+ expect(wrapper.emitted('openCommentForm')).toEqual([
+ [{ ...mockOverlayData.overlayDimensions, x: 1, y: 1 }],
+ ]);
+ });
+ });
+
+ describe('currentCommentForm', () => {
+ it('is null when isAnnotating is false', () => {
+ createComponent(
+ {
+ image: 'test.jpg',
+ imageName: 'test',
+ },
+ mockOverlayData,
+ );
+
+ return wrapper.vm.$nextTick().then(() => {
+ expect(wrapper.vm.currentCommentForm).toBeNull();
+ expect(wrapper.element).toMatchSnapshot();
+ });
+ });
+
+ it('is null when isAnnotating is true but annotation position is falsey', () => {
+ createComponent(
+ {
+ image: 'test.jpg',
+ imageName: 'test',
+ isAnnotating: true,
+ },
+ mockOverlayData,
+ );
+
+ return wrapper.vm.$nextTick().then(() => {
+ expect(wrapper.vm.currentCommentForm).toBeNull();
+ expect(wrapper.element).toMatchSnapshot();
+ });
+ });
+
+ it('is equal to current annotation position when isAnnotating is true', () => {
+ createComponent(
+ {
+ image: 'test.jpg',
+ imageName: 'test',
+ isAnnotating: true,
+ },
+ {
+ ...mockOverlayData,
+ currentAnnotationPosition: {
+ x: 1,
+ y: 1,
+ width: 100,
+ height: 100,
+ },
+ },
+ );
+
+ return wrapper.vm.$nextTick().then(() => {
+ expect(wrapper.vm.currentCommentForm).toEqual({
+ x: 1,
+ y: 1,
+ width: 100,
+ height: 100,
+ });
+ expect(wrapper.element).toMatchSnapshot();
+ });
+ });
+ });
+
+ describe('setOverlayPosition', () => {
+ beforeEach(() => {
+ createComponent(
+ {
+ image: 'test.jpg',
+ imageName: 'test',
+ },
+ mockOverlayData,
+ );
+ });
+
+ afterEach(() => {
+ jest.clearAllMocks();
+ });
+
+ it('sets overlay position correctly when overlay is smaller than viewport', () => {
+ jest.spyOn(wrapper.vm.$refs.presentationViewport, 'offsetWidth', 'get').mockReturnValue(200);
+ jest.spyOn(wrapper.vm.$refs.presentationViewport, 'offsetHeight', 'get').mockReturnValue(200);
+
+ wrapper.vm.setOverlayPosition();
+ expect(wrapper.vm.overlayPosition).toEqual({
+ left: `calc(50% - ${mockOverlayData.overlayDimensions.width / 2}px)`,
+ top: `calc(50% - ${mockOverlayData.overlayDimensions.height / 2}px)`,
+ });
+ });
+
+ it('sets overlay position correctly when overlay width is larger than viewports', () => {
+ jest.spyOn(wrapper.vm.$refs.presentationViewport, 'offsetWidth', 'get').mockReturnValue(50);
+ jest.spyOn(wrapper.vm.$refs.presentationViewport, 'offsetHeight', 'get').mockReturnValue(200);
+
+ wrapper.vm.setOverlayPosition();
+ expect(wrapper.vm.overlayPosition).toEqual({
+ left: '0',
+ top: `calc(50% - ${mockOverlayData.overlayDimensions.height / 2}px)`,
+ });
+ });
+
+ it('sets overlay position correctly when overlay height is larger than viewports', () => {
+ jest.spyOn(wrapper.vm.$refs.presentationViewport, 'offsetWidth', 'get').mockReturnValue(200);
+ jest.spyOn(wrapper.vm.$refs.presentationViewport, 'offsetHeight', 'get').mockReturnValue(50);
+
+ wrapper.vm.setOverlayPosition();
+ expect(wrapper.vm.overlayPosition).toEqual({
+ left: `calc(50% - ${mockOverlayData.overlayDimensions.width / 2}px)`,
+ top: '0',
+ });
+ });
+ });
+
+ describe('getViewportCenter', () => {
+ beforeEach(() => {
+ createComponent(
+ {
+ image: 'test.jpg',
+ imageName: 'test',
+ },
+ mockOverlayData,
+ );
+ });
+
+ it('calculate center correctly with no scroll', () => {
+ mockRefDimensions(
+ wrapper.vm.$refs.presentationViewport,
+ { width: 10, height: 10 },
+ { width: 20, height: 20 },
+ 0,
+ 0,
+ );
+
+ expect(wrapper.vm.getViewportCenter()).toEqual({
+ x: 5,
+ y: 5,
+ });
+ });
+
+ it('calculate center correctly with some scroll', () => {
+ mockRefDimensions(
+ wrapper.vm.$refs.presentationViewport,
+ { width: 10, height: 10 },
+ { width: 20, height: 20 },
+ 0.5,
+ 0.5,
+ );
+
+ expect(wrapper.vm.getViewportCenter()).toEqual({
+ x: 10,
+ y: 10,
+ });
+ });
+
+ it('Returns default case if no overflow (scrollWidth==offsetWidth, etc.)', () => {
+ mockRefDimensions(
+ wrapper.vm.$refs.presentationViewport,
+ { width: 20, height: 20 },
+ { width: 20, height: 20 },
+ 0.5,
+ 0.5,
+ );
+
+ expect(wrapper.vm.getViewportCenter()).toEqual({
+ x: 10,
+ y: 10,
+ });
+ });
+ });
+
+ describe('scaleZoomFocalPoint', () => {
+ it('scales focal point correctly when zooming in', () => {
+ createComponent(
+ {
+ image: 'test.jpg',
+ imageName: 'test',
+ },
+ {
+ ...mockOverlayData,
+ zoomFocalPoint: {
+ x: 5,
+ y: 5,
+ width: 50,
+ height: 50,
+ },
+ },
+ );
+
+ wrapper.vm.scaleZoomFocalPoint();
+ expect(wrapper.vm.zoomFocalPoint).toEqual({
+ x: 10,
+ y: 10,
+ width: 100,
+ height: 100,
+ });
+ });
+
+ it('scales focal point correctly when zooming out', () => {
+ createComponent(
+ {
+ image: 'test.jpg',
+ imageName: 'test',
+ },
+ {
+ ...mockOverlayData,
+ zoomFocalPoint: {
+ x: 10,
+ y: 10,
+ width: 200,
+ height: 200,
+ },
+ },
+ );
+
+ wrapper.vm.scaleZoomFocalPoint();
+ expect(wrapper.vm.zoomFocalPoint).toEqual({
+ x: 5,
+ y: 5,
+ width: 100,
+ height: 100,
+ });
+ });
+ });
+
+ describe('onImageResize', () => {
+ it('sets zoom focal point on initial load', () => {
+ createComponent(
+ {
+ image: 'test.jpg',
+ imageName: 'test',
+ },
+ mockOverlayData,
+ );
+
+ wrapper.setMethods({
+ shiftZoomFocalPoint: jest.fn(),
+ scaleZoomFocalPoint: jest.fn(),
+ scrollToFocalPoint: jest.fn(),
+ });
+
+ wrapper.vm.onImageResize({ width: 10, height: 10 });
+ return wrapper.vm.$nextTick().then(() => {
+ expect(wrapper.vm.shiftZoomFocalPoint).toHaveBeenCalled();
+ expect(wrapper.vm.initialLoad).toBe(false);
+ });
+ });
+
+ it('calls scaleZoomFocalPoint and scrollToFocalPoint after initial load', () => {
+ wrapper.vm.onImageResize({ width: 10, height: 10 });
+ return wrapper.vm.$nextTick().then(() => {
+ expect(wrapper.vm.scaleZoomFocalPoint).toHaveBeenCalled();
+ expect(wrapper.vm.scrollToFocalPoint).toHaveBeenCalled();
+ });
+ });
+ });
+
+ describe('onPresentationMousedown', () => {
+ it.each`
+ scenario | width | height
+ ${'width overflows'} | ${101} | ${100}
+ ${'height overflows'} | ${100} | ${101}
+ ${'width and height overflows'} | ${200} | ${200}
+ `('sets lastDragPosition when design $scenario', ({ width, height }) => {
+ createComponent();
+ mockRefDimensions(
+ wrapper.vm.$refs.presentationViewport,
+ { width: 100, height: 100 },
+ { width, height },
+ );
+
+ const newLastDragPosition = { x: 2, y: 2 };
+ wrapper.vm.onPresentationMousedown({
+ clientX: newLastDragPosition.x,
+ clientY: newLastDragPosition.y,
+ });
+
+ expect(wrapper.vm.lastDragPosition).toStrictEqual(newLastDragPosition);
+ });
+
+ it('does not set lastDragPosition if design does not overflow', () => {
+ const lastDragPosition = { x: 1, y: 1 };
+
+ createComponent({}, { lastDragPosition });
+ mockRefDimensions(
+ wrapper.vm.$refs.presentationViewport,
+ { width: 100, height: 100 },
+ { width: 50, height: 50 },
+ );
+
+ wrapper.vm.onPresentationMousedown({ clientX: 2, clientY: 2 });
+
+ // check lastDragPosition is unchanged
+ expect(wrapper.vm.lastDragPosition).toStrictEqual(lastDragPosition);
+ });
+ });
+
+ describe('getAnnotationPositon', () => {
+ it.each`
+ coordinates | overlayDimensions | position
+ ${{ x: 100, y: 100 }} | ${{ width: 50, height: 50 }} | ${{ x: 100, y: 100, width: 50, height: 50 }}
+ ${{ x: 100.2, y: 100.5 }} | ${{ width: 50.6, height: 50.0 }} | ${{ x: 100, y: 101, width: 51, height: 50 }}
+ `('returns correct annotation position', ({ coordinates, overlayDimensions, position }) => {
+ createComponent(undefined, {
+ overlayDimensions: {
+ width: overlayDimensions.width,
+ height: overlayDimensions.height,
+ },
+ });
+
+ expect(wrapper.vm.getAnnotationPositon(coordinates)).toStrictEqual(position);
+ });
+ });
+
+ describe('when design is overflowing', () => {
+ beforeEach(() => {
+ createComponent(
+ {
+ image: 'test.jpg',
+ imageName: 'test',
+ },
+ mockOverlayData,
+ {
+ 'design-overlay': DesignOverlay,
+ },
+ );
+
+ // mock a design that overflows
+ mockRefDimensions(
+ wrapper.vm.$refs.presentationViewport,
+ { width: 10, height: 10 },
+ { width: 20, height: 20 },
+ 0,
+ 0,
+ );
+ });
+
+ it('opens a comment form if design was not dragged', () => {
+ const addCommentOverlay = findOverlayCommentButton();
+ const startCoords = {
+ clientX: 1,
+ clientY: 1,
+ };
+
+ addCommentOverlay.trigger('mousedown', {
+ clientX: startCoords.clientX,
+ clientY: startCoords.clientY,
+ });
+
+ return wrapper.vm
+ .$nextTick()
+ .then(() => {
+ addCommentOverlay.trigger('mouseup');
+ return wrapper.vm.$nextTick();
+ })
+ .then(() => {
+ expect(wrapper.emitted('openCommentForm')).toBeDefined();
+ });
+ });
+
+ describe('when clicking and dragging', () => {
+ it.each`
+ description | useTouchEvents
+ ${'with touch events'} | ${true}
+ ${'without touch events'} | ${false}
+ `('calls scrollTo with correct arguments $description', ({ useTouchEvents }) => {
+ return clickDragExplore(
+ { clientX: 0, clientY: 0 },
+ { clientX: 10, clientY: 10 },
+ { useTouchEvents },
+ ).then(() => {
+ expect(wrapper.element.scrollTo).toHaveBeenCalledTimes(1);
+ expect(wrapper.element.scrollTo).toHaveBeenCalledWith(-10, -10);
+ });
+ });
+
+ it('does not open a comment form when drag position exceeds buffer', () => {
+ return clickDragExplore(
+ { clientX: 0, clientY: 0 },
+ { clientX: 10, clientY: 10 },
+ { mouseup: true },
+ ).then(() => {
+ expect(wrapper.emitted('openCommentForm')).toBeFalsy();
+ });
+ });
+
+ it('opens a comment form when drag position is within buffer', () => {
+ return clickDragExplore(
+ { clientX: 0, clientY: 0 },
+ { clientX: 1, clientY: 0 },
+ { mouseup: true },
+ ).then(() => {
+ expect(wrapper.emitted('openCommentForm')).toBeDefined();
+ });
+ });
+ });
+ });
+});
diff --git a/spec/frontend/design_management/components/design_scaler_spec.js b/spec/frontend/design_management/components/design_scaler_spec.js
new file mode 100644
index 00000000000..b06d2f924df
--- /dev/null
+++ b/spec/frontend/design_management/components/design_scaler_spec.js
@@ -0,0 +1,67 @@
+import { shallowMount } from '@vue/test-utils';
+import DesignScaler from '~/design_management/components/design_scaler.vue';
+
+describe('Design management design scaler component', () => {
+ let wrapper;
+
+ function createComponent(propsData, data = {}) {
+ wrapper = shallowMount(DesignScaler, {
+ propsData,
+ });
+ wrapper.setData(data);
+ }
+
+ afterEach(() => {
+ wrapper.destroy();
+ });
+
+ const getButton = type => {
+ const buttonTypeOrder = ['minus', 'reset', 'plus'];
+ const buttons = wrapper.findAll('button');
+ return buttons.at(buttonTypeOrder.indexOf(type));
+ };
+
+ it('emits @scale event when "plus" button clicked', () => {
+ createComponent();
+
+ getButton('plus').trigger('click');
+ expect(wrapper.emitted('scale')).toEqual([[1.2]]);
+ });
+
+ it('emits @scale event when "reset" button clicked (scale > 1)', () => {
+ createComponent({}, { scale: 1.6 });
+ return wrapper.vm.$nextTick().then(() => {
+ getButton('reset').trigger('click');
+ expect(wrapper.emitted('scale')).toEqual([[1]]);
+ });
+ });
+
+ it('emits @scale event when "minus" button clicked (scale > 1)', () => {
+ createComponent({}, { scale: 1.6 });
+
+ return wrapper.vm.$nextTick().then(() => {
+ getButton('minus').trigger('click');
+ expect(wrapper.emitted('scale')).toEqual([[1.4]]);
+ });
+ });
+
+ it('minus and reset buttons are disabled when scale === 1', () => {
+ createComponent();
+
+ expect(wrapper.element).toMatchSnapshot();
+ });
+
+ it('minus and reset buttons are enabled when scale > 1', () => {
+ createComponent({}, { scale: 1.2 });
+ return wrapper.vm.$nextTick().then(() => {
+ expect(wrapper.element).toMatchSnapshot();
+ });
+ });
+
+ it('plus button is disabled when scale === 2', () => {
+ createComponent({}, { scale: 2 });
+ return wrapper.vm.$nextTick().then(() => {
+ expect(wrapper.element).toMatchSnapshot();
+ });
+ });
+});
diff --git a/spec/frontend/design_management/components/image_spec.js b/spec/frontend/design_management/components/image_spec.js
new file mode 100644
index 00000000000..52d60b04a8a
--- /dev/null
+++ b/spec/frontend/design_management/components/image_spec.js
@@ -0,0 +1,133 @@
+import { shallowMount } from '@vue/test-utils';
+import { GlIcon } from '@gitlab/ui';
+import DesignImage from '~/design_management/components/image.vue';
+
+describe('Design management large image component', () => {
+ let wrapper;
+
+ function createComponent(propsData, data = {}) {
+ wrapper = shallowMount(DesignImage, {
+ propsData,
+ });
+ wrapper.setData(data);
+ }
+
+ afterEach(() => {
+ wrapper.destroy();
+ });
+
+ it('renders loading state', () => {
+ createComponent({
+ isLoading: true,
+ });
+
+ expect(wrapper.element).toMatchSnapshot();
+ });
+
+ it('renders image', () => {
+ createComponent({
+ isLoading: false,
+ image: 'test.jpg',
+ name: 'test',
+ });
+
+ expect(wrapper.element).toMatchSnapshot();
+ });
+
+ it('sets correct classes and styles if imageStyle is set', () => {
+ createComponent(
+ {
+ isLoading: false,
+ image: 'test.jpg',
+ name: 'test',
+ },
+ {
+ imageStyle: {
+ width: '100px',
+ height: '100px',
+ },
+ },
+ );
+ return wrapper.vm.$nextTick().then(() => {
+ expect(wrapper.element).toMatchSnapshot();
+ });
+ });
+
+ it('renders media broken icon on error', () => {
+ createComponent({
+ isLoading: false,
+ image: 'test.jpg',
+ name: 'test',
+ });
+
+ const image = wrapper.find('img');
+ image.trigger('error');
+ return wrapper.vm.$nextTick().then(() => {
+ expect(image.isVisible()).toBe(false);
+ expect(wrapper.find(GlIcon).element).toMatchSnapshot();
+ });
+ });
+
+ describe('zoom', () => {
+ const baseImageWidth = 100;
+ const baseImageHeight = 100;
+
+ beforeEach(() => {
+ createComponent(
+ {
+ isLoading: false,
+ image: 'test.jpg',
+ name: 'test',
+ },
+ {
+ imageStyle: {
+ width: `${baseImageWidth}px`,
+ height: `${baseImageHeight}px`,
+ },
+ baseImageSize: {
+ width: baseImageWidth,
+ height: baseImageHeight,
+ },
+ },
+ );
+
+ jest.spyOn(wrapper.vm.$refs.contentImg, 'offsetWidth', 'get').mockReturnValue(baseImageWidth);
+ jest
+ .spyOn(wrapper.vm.$refs.contentImg, 'offsetHeight', 'get')
+ .mockReturnValue(baseImageHeight);
+ });
+
+ it('emits @resize event on zoom', () => {
+ const zoomAmount = 2;
+ wrapper.vm.zoom(zoomAmount);
+
+ return wrapper.vm.$nextTick().then(() => {
+ expect(wrapper.emitted('resize')).toEqual([
+ [{ width: baseImageWidth * zoomAmount, height: baseImageHeight * zoomAmount }],
+ ]);
+ });
+ });
+
+ it('emits @resize event with base image size when scale=1', () => {
+ wrapper.vm.zoom(1);
+
+ return wrapper.vm.$nextTick().then(() => {
+ expect(wrapper.emitted('resize')).toEqual([
+ [{ width: baseImageWidth, height: baseImageHeight }],
+ ]);
+ });
+ });
+
+ it('sets image style when zoomed', () => {
+ const zoomAmount = 2;
+ wrapper.vm.zoom(zoomAmount);
+ expect(wrapper.vm.imageStyle).toEqual({
+ width: `${baseImageWidth * zoomAmount}px`,
+ height: `${baseImageHeight * zoomAmount}px`,
+ });
+ return wrapper.vm.$nextTick().then(() => {
+ expect(wrapper.element).toMatchSnapshot();
+ });
+ });
+ });
+});
diff --git a/spec/frontend/design_management/components/list/__snapshots__/item_spec.js.snap b/spec/frontend/design_management/components/list/__snapshots__/item_spec.js.snap
new file mode 100644
index 00000000000..9cd427f6aae
--- /dev/null
+++ b/spec/frontend/design_management/components/list/__snapshots__/item_spec.js.snap
@@ -0,0 +1,472 @@
+// Jest Snapshot v1, https://goo.gl/fbAQLP
+
+exports[`Design management list item component when item appears in view after image is loaded renders media broken icon when image onerror triggered 1`] = `
+<gl-icon-stub
+ class="text-secondary"
+ name="media-broken"
+ size="32"
+/>
+`;
+
+exports[`Design management list item component with no notes renders item with correct status icon for creation event 1`] = `
+<router-link-stub
+ class="card cursor-pointer text-plain js-design-list-item design-list-item"
+ to="[object Object]"
+>
+ <div
+ class="card-body p-0 d-flex-center overflow-hidden position-relative"
+ >
+ <div
+ class="design-event position-absolute"
+ >
+ <span
+ aria-label="Added in this version"
+ title="Added in this version"
+ >
+ <icon-stub
+ class="text-success-500"
+ name="file-addition-solid"
+ size="18"
+ />
+ </span>
+ </div>
+
+ <gl-intersection-observer-stub
+ options="[object Object]"
+ >
+ <!---->
+
+ <img
+ alt="test"
+ class="block mx-auto mw-100 mh-100 design-img"
+ data-qa-selector="design_image"
+ src=""
+ />
+ </gl-intersection-observer-stub>
+ </div>
+
+ <div
+ class="card-footer d-flex w-100"
+ >
+ <div
+ class="d-flex flex-column str-truncated-100"
+ >
+ <span
+ class="bold str-truncated-100"
+ data-qa-selector="design_file_name"
+ >
+ test
+ </span>
+
+ <span
+ class="str-truncated-100"
+ >
+
+ Updated
+ <timeago-stub
+ cssclass=""
+ time="01-01-2019"
+ tooltipplacement="bottom"
+ />
+ </span>
+ </div>
+
+ <!---->
+ </div>
+</router-link-stub>
+`;
+
+exports[`Design management list item component with no notes renders item with correct status icon for deletion event 1`] = `
+<router-link-stub
+ class="card cursor-pointer text-plain js-design-list-item design-list-item"
+ to="[object Object]"
+>
+ <div
+ class="card-body p-0 d-flex-center overflow-hidden position-relative"
+ >
+ <div
+ class="design-event position-absolute"
+ >
+ <span
+ aria-label="Deleted in this version"
+ title="Deleted in this version"
+ >
+ <icon-stub
+ class="text-danger-500"
+ name="file-deletion-solid"
+ size="18"
+ />
+ </span>
+ </div>
+
+ <gl-intersection-observer-stub
+ options="[object Object]"
+ >
+ <!---->
+
+ <img
+ alt="test"
+ class="block mx-auto mw-100 mh-100 design-img"
+ data-qa-selector="design_image"
+ src=""
+ />
+ </gl-intersection-observer-stub>
+ </div>
+
+ <div
+ class="card-footer d-flex w-100"
+ >
+ <div
+ class="d-flex flex-column str-truncated-100"
+ >
+ <span
+ class="bold str-truncated-100"
+ data-qa-selector="design_file_name"
+ >
+ test
+ </span>
+
+ <span
+ class="str-truncated-100"
+ >
+
+ Updated
+ <timeago-stub
+ cssclass=""
+ time="01-01-2019"
+ tooltipplacement="bottom"
+ />
+ </span>
+ </div>
+
+ <!---->
+ </div>
+</router-link-stub>
+`;
+
+exports[`Design management list item component with no notes renders item with correct status icon for modification event 1`] = `
+<router-link-stub
+ class="card cursor-pointer text-plain js-design-list-item design-list-item"
+ to="[object Object]"
+>
+ <div
+ class="card-body p-0 d-flex-center overflow-hidden position-relative"
+ >
+ <div
+ class="design-event position-absolute"
+ >
+ <span
+ aria-label="Modified in this version"
+ title="Modified in this version"
+ >
+ <icon-stub
+ class="text-primary-500"
+ name="file-modified-solid"
+ size="18"
+ />
+ </span>
+ </div>
+
+ <gl-intersection-observer-stub
+ options="[object Object]"
+ >
+ <!---->
+
+ <img
+ alt="test"
+ class="block mx-auto mw-100 mh-100 design-img"
+ data-qa-selector="design_image"
+ src=""
+ />
+ </gl-intersection-observer-stub>
+ </div>
+
+ <div
+ class="card-footer d-flex w-100"
+ >
+ <div
+ class="d-flex flex-column str-truncated-100"
+ >
+ <span
+ class="bold str-truncated-100"
+ data-qa-selector="design_file_name"
+ >
+ test
+ </span>
+
+ <span
+ class="str-truncated-100"
+ >
+
+ Updated
+ <timeago-stub
+ cssclass=""
+ time="01-01-2019"
+ tooltipplacement="bottom"
+ />
+ </span>
+ </div>
+
+ <!---->
+ </div>
+</router-link-stub>
+`;
+
+exports[`Design management list item component with no notes renders item with no status icon for none event 1`] = `
+<router-link-stub
+ class="card cursor-pointer text-plain js-design-list-item design-list-item"
+ to="[object Object]"
+>
+ <div
+ class="card-body p-0 d-flex-center overflow-hidden position-relative"
+ >
+ <!---->
+
+ <gl-intersection-observer-stub
+ options="[object Object]"
+ >
+ <!---->
+
+ <img
+ alt="test"
+ class="block mx-auto mw-100 mh-100 design-img"
+ data-qa-selector="design_image"
+ src=""
+ />
+ </gl-intersection-observer-stub>
+ </div>
+
+ <div
+ class="card-footer d-flex w-100"
+ >
+ <div
+ class="d-flex flex-column str-truncated-100"
+ >
+ <span
+ class="bold str-truncated-100"
+ data-qa-selector="design_file_name"
+ >
+ test
+ </span>
+
+ <span
+ class="str-truncated-100"
+ >
+
+ Updated
+ <timeago-stub
+ cssclass=""
+ time="01-01-2019"
+ tooltipplacement="bottom"
+ />
+ </span>
+ </div>
+
+ <!---->
+ </div>
+</router-link-stub>
+`;
+
+exports[`Design management list item component with no notes renders loading spinner when isUploading is true 1`] = `
+<router-link-stub
+ class="card cursor-pointer text-plain js-design-list-item design-list-item"
+ to="[object Object]"
+>
+ <div
+ class="card-body p-0 d-flex-center overflow-hidden position-relative"
+ >
+ <!---->
+
+ <gl-intersection-observer-stub
+ options="[object Object]"
+ >
+ <gl-loading-icon-stub
+ color="orange"
+ label="Loading"
+ size="md"
+ />
+
+ <img
+ alt="test"
+ class="block mx-auto mw-100 mh-100 design-img"
+ data-qa-selector="design_image"
+ src=""
+ style="display: none;"
+ />
+ </gl-intersection-observer-stub>
+ </div>
+
+ <div
+ class="card-footer d-flex w-100"
+ >
+ <div
+ class="d-flex flex-column str-truncated-100"
+ >
+ <span
+ class="bold str-truncated-100"
+ data-qa-selector="design_file_name"
+ >
+ test
+ </span>
+
+ <span
+ class="str-truncated-100"
+ >
+
+ Updated
+ <timeago-stub
+ cssclass=""
+ time="01-01-2019"
+ tooltipplacement="bottom"
+ />
+ </span>
+ </div>
+
+ <!---->
+ </div>
+</router-link-stub>
+`;
+
+exports[`Design management list item component with notes renders item with multiple comments 1`] = `
+<router-link-stub
+ class="card cursor-pointer text-plain js-design-list-item design-list-item"
+ to="[object Object]"
+>
+ <div
+ class="card-body p-0 d-flex-center overflow-hidden position-relative"
+ >
+ <!---->
+
+ <gl-intersection-observer-stub
+ options="[object Object]"
+ >
+ <!---->
+
+ <img
+ alt="test"
+ class="block mx-auto mw-100 mh-100 design-img"
+ data-qa-selector="design_image"
+ src=""
+ />
+ </gl-intersection-observer-stub>
+ </div>
+
+ <div
+ class="card-footer d-flex w-100"
+ >
+ <div
+ class="d-flex flex-column str-truncated-100"
+ >
+ <span
+ class="bold str-truncated-100"
+ data-qa-selector="design_file_name"
+ >
+ test
+ </span>
+
+ <span
+ class="str-truncated-100"
+ >
+
+ Updated
+ <timeago-stub
+ cssclass=""
+ time="01-01-2019"
+ tooltipplacement="bottom"
+ />
+ </span>
+ </div>
+
+ <div
+ class="ml-auto d-flex align-items-center text-secondary"
+ >
+ <icon-stub
+ class="ml-1"
+ name="comments"
+ size="16"
+ />
+
+ <span
+ aria-label="2 comments"
+ class="ml-1"
+ >
+
+ 2
+
+ </span>
+ </div>
+ </div>
+</router-link-stub>
+`;
+
+exports[`Design management list item component with notes renders item with single comment 1`] = `
+<router-link-stub
+ class="card cursor-pointer text-plain js-design-list-item design-list-item"
+ to="[object Object]"
+>
+ <div
+ class="card-body p-0 d-flex-center overflow-hidden position-relative"
+ >
+ <!---->
+
+ <gl-intersection-observer-stub
+ options="[object Object]"
+ >
+ <!---->
+
+ <img
+ alt="test"
+ class="block mx-auto mw-100 mh-100 design-img"
+ data-qa-selector="design_image"
+ src=""
+ />
+ </gl-intersection-observer-stub>
+ </div>
+
+ <div
+ class="card-footer d-flex w-100"
+ >
+ <div
+ class="d-flex flex-column str-truncated-100"
+ >
+ <span
+ class="bold str-truncated-100"
+ data-qa-selector="design_file_name"
+ >
+ test
+ </span>
+
+ <span
+ class="str-truncated-100"
+ >
+
+ Updated
+ <timeago-stub
+ cssclass=""
+ time="01-01-2019"
+ tooltipplacement="bottom"
+ />
+ </span>
+ </div>
+
+ <div
+ class="ml-auto d-flex align-items-center text-secondary"
+ >
+ <icon-stub
+ class="ml-1"
+ name="comments"
+ size="16"
+ />
+
+ <span
+ aria-label="1 comment"
+ class="ml-1"
+ >
+
+ 1
+
+ </span>
+ </div>
+ </div>
+</router-link-stub>
+`;
diff --git a/spec/frontend/design_management/components/list/item_spec.js b/spec/frontend/design_management/components/list/item_spec.js
new file mode 100644
index 00000000000..705b532454f
--- /dev/null
+++ b/spec/frontend/design_management/components/list/item_spec.js
@@ -0,0 +1,168 @@
+import { createLocalVue, shallowMount } from '@vue/test-utils';
+import { GlIcon, GlLoadingIcon, GlIntersectionObserver } from '@gitlab/ui';
+import VueRouter from 'vue-router';
+import Item from '~/design_management/components/list/item.vue';
+
+const localVue = createLocalVue();
+localVue.use(VueRouter);
+const router = new VueRouter();
+
+// Referenced from: doc/api/graphql/reference/gitlab_schema.graphql:DesignVersionEvent
+const DESIGN_VERSION_EVENT = {
+ CREATION: 'CREATION',
+ DELETION: 'DELETION',
+ MODIFICATION: 'MODIFICATION',
+ NO_CHANGE: 'NONE',
+};
+
+describe('Design management list item component', () => {
+ let wrapper;
+
+ function createComponent({
+ notesCount = 0,
+ event = DESIGN_VERSION_EVENT.NO_CHANGE,
+ isUploading = false,
+ imageLoading = false,
+ } = {}) {
+ wrapper = shallowMount(Item, {
+ localVue,
+ router,
+ propsData: {
+ id: 1,
+ filename: 'test',
+ image: 'http://via.placeholder.com/300',
+ isUploading,
+ event,
+ notesCount,
+ updatedAt: '01-01-2019',
+ },
+ data() {
+ return {
+ imageLoading,
+ };
+ },
+ stubs: ['router-link'],
+ });
+ }
+
+ afterEach(() => {
+ wrapper.destroy();
+ });
+
+ describe('when item is not in view', () => {
+ it('image is not rendered', () => {
+ createComponent();
+
+ const image = wrapper.find('img');
+ expect(image.attributes('src')).toBe('');
+ });
+ });
+
+ describe('when item appears in view', () => {
+ let image;
+ let glIntersectionObserver;
+
+ beforeEach(() => {
+ createComponent();
+ image = wrapper.find('img');
+ glIntersectionObserver = wrapper.find(GlIntersectionObserver);
+
+ glIntersectionObserver.vm.$emit('appear');
+ return wrapper.vm.$nextTick();
+ });
+
+ describe('before image is loaded', () => {
+ it('renders loading spinner', () => {
+ expect(wrapper.find(GlLoadingIcon)).toExist();
+ });
+ });
+
+ describe('after image is loaded', () => {
+ beforeEach(() => {
+ image.trigger('load');
+ return wrapper.vm.$nextTick();
+ });
+
+ it('renders an image', () => {
+ expect(image.attributes('src')).toBe('http://via.placeholder.com/300');
+ expect(image.isVisible()).toBe(true);
+ });
+
+ it('renders media broken icon when image onerror triggered', () => {
+ image.trigger('error');
+ return wrapper.vm.$nextTick().then(() => {
+ expect(image.isVisible()).toBe(false);
+ expect(wrapper.find(GlIcon).element).toMatchSnapshot();
+ });
+ });
+
+ describe('when imageV432x230 and image provided', () => {
+ it('renders imageV432x230 image', () => {
+ const mockSrc = 'mock-imageV432x230-url';
+ wrapper.setProps({ imageV432x230: mockSrc });
+
+ return wrapper.vm.$nextTick().then(() => {
+ expect(image.attributes('src')).toBe(mockSrc);
+ });
+ });
+ });
+
+ describe('when image disappears from view and then reappears', () => {
+ beforeEach(() => {
+ glIntersectionObserver.vm.$emit('appear');
+ return wrapper.vm.$nextTick();
+ });
+
+ it('renders an image', () => {
+ expect(image.isVisible()).toBe(true);
+ });
+ });
+ });
+ });
+
+ describe('with notes', () => {
+ it('renders item with single comment', () => {
+ createComponent({ notesCount: 1 });
+
+ expect(wrapper.element).toMatchSnapshot();
+ });
+
+ it('renders item with multiple comments', () => {
+ createComponent({ notesCount: 2 });
+
+ expect(wrapper.element).toMatchSnapshot();
+ });
+ });
+
+ describe('with no notes', () => {
+ it('renders item with no status icon for none event', () => {
+ createComponent();
+
+ expect(wrapper.element).toMatchSnapshot();
+ });
+
+ it('renders item with correct status icon for modification event', () => {
+ createComponent({ event: DESIGN_VERSION_EVENT.MODIFICATION });
+
+ expect(wrapper.element).toMatchSnapshot();
+ });
+
+ it('renders item with correct status icon for deletion event', () => {
+ createComponent({ event: DESIGN_VERSION_EVENT.DELETION });
+
+ expect(wrapper.element).toMatchSnapshot();
+ });
+
+ it('renders item with correct status icon for creation event', () => {
+ createComponent({ event: DESIGN_VERSION_EVENT.CREATION });
+
+ expect(wrapper.element).toMatchSnapshot();
+ });
+
+ it('renders loading spinner when isUploading is true', () => {
+ createComponent({ isUploading: true });
+
+ expect(wrapper.element).toMatchSnapshot();
+ });
+ });
+});
diff --git a/spec/frontend/design_management/components/toolbar/__snapshots__/index_spec.js.snap b/spec/frontend/design_management/components/toolbar/__snapshots__/index_spec.js.snap
new file mode 100644
index 00000000000..e55cff8de3d
--- /dev/null
+++ b/spec/frontend/design_management/components/toolbar/__snapshots__/index_spec.js.snap
@@ -0,0 +1,61 @@
+// Jest Snapshot v1, https://goo.gl/fbAQLP
+
+exports[`Design management toolbar component renders design and updated data 1`] = `
+<header
+ class="d-flex p-2 bg-white align-items-center js-design-header"
+>
+ <a
+ aria-label="Go back to designs"
+ class="mr-3 text-plain d-flex justify-content-center align-items-center"
+ >
+ <icon-stub
+ name="close"
+ size="18"
+ />
+ </a>
+
+ <div
+ class="overflow-hidden d-flex align-items-center"
+ >
+ <h2
+ class="m-0 str-truncated-100 gl-font-base"
+ >
+ test.jpg
+ </h2>
+
+ <small
+ class="text-secondary"
+ >
+ Updated 1 hour ago by Test Name
+ </small>
+ </div>
+
+ <pagination-stub
+ class="ml-auto flex-shrink-0"
+ id="1"
+ />
+
+ <gl-deprecated-button-stub
+ class="mr-2"
+ href="/-/designs/306/7f747adcd4693afadbe968d7ba7d983349b9012d"
+ size="md"
+ variant="secondary"
+ >
+ <icon-stub
+ name="download"
+ size="18"
+ />
+ </gl-deprecated-button-stub>
+
+ <delete-button-stub
+ buttonclass=""
+ buttonvariant="danger"
+ hasselecteddesigns="true"
+ >
+ <icon-stub
+ name="remove"
+ size="18"
+ />
+ </delete-button-stub>
+</header>
+`;
diff --git a/spec/frontend/design_management/components/toolbar/__snapshots__/pagination_button_spec.js.snap b/spec/frontend/design_management/components/toolbar/__snapshots__/pagination_button_spec.js.snap
new file mode 100644
index 00000000000..08662a04f15
--- /dev/null
+++ b/spec/frontend/design_management/components/toolbar/__snapshots__/pagination_button_spec.js.snap
@@ -0,0 +1,28 @@
+// Jest Snapshot v1, https://goo.gl/fbAQLP
+
+exports[`Design management pagination button component disables button when no design is passed 1`] = `
+<router-link-stub
+ aria-label="Test title"
+ class="btn btn-default disabled"
+ disabled="true"
+ to="[object Object]"
+>
+ <icon-stub
+ name="angle-right"
+ size="16"
+ />
+</router-link-stub>
+`;
+
+exports[`Design management pagination button component renders router-link 1`] = `
+<router-link-stub
+ aria-label="Test title"
+ class="btn btn-default"
+ to="[object Object]"
+>
+ <icon-stub
+ name="angle-right"
+ size="16"
+ />
+</router-link-stub>
+`;
diff --git a/spec/frontend/design_management/components/toolbar/__snapshots__/pagination_spec.js.snap b/spec/frontend/design_management/components/toolbar/__snapshots__/pagination_spec.js.snap
new file mode 100644
index 00000000000..0197b4bff79
--- /dev/null
+++ b/spec/frontend/design_management/components/toolbar/__snapshots__/pagination_spec.js.snap
@@ -0,0 +1,29 @@
+// Jest Snapshot v1, https://goo.gl/fbAQLP
+
+exports[`Design management pagination component hides components when designs are empty 1`] = `<!---->`;
+
+exports[`Design management pagination component renders pagination buttons 1`] = `
+<div
+ class="d-flex align-items-center"
+>
+
+ 0 of 2
+
+ <div
+ class="btn-group ml-3 mr-3"
+ >
+ <pagination-button-stub
+ class="js-previous-design"
+ iconname="angle-left"
+ title="Go to previous design"
+ />
+
+ <pagination-button-stub
+ class="js-next-design"
+ design="[object Object]"
+ iconname="angle-right"
+ title="Go to next design"
+ />
+ </div>
+</div>
+`;
diff --git a/spec/frontend/design_management/components/toolbar/index_spec.js b/spec/frontend/design_management/components/toolbar/index_spec.js
new file mode 100644
index 00000000000..2910b2f62ba
--- /dev/null
+++ b/spec/frontend/design_management/components/toolbar/index_spec.js
@@ -0,0 +1,123 @@
+import { createLocalVue, shallowMount } from '@vue/test-utils';
+import VueRouter from 'vue-router';
+import Toolbar from '~/design_management/components/toolbar/index.vue';
+import DeleteButton from '~/design_management/components/delete_button.vue';
+import { DESIGNS_ROUTE_NAME } from '~/design_management/router/constants';
+import { GlDeprecatedButton } from '@gitlab/ui';
+
+const localVue = createLocalVue();
+localVue.use(VueRouter);
+const router = new VueRouter();
+
+const RouterLinkStub = {
+ props: {
+ to: {
+ type: Object,
+ },
+ },
+ render(createElement) {
+ return createElement('a', {}, this.$slots.default);
+ },
+};
+
+describe('Design management toolbar component', () => {
+ let wrapper;
+
+ function createComponent(isLoading = false, createDesign = true, props) {
+ const updatedAt = new Date();
+ updatedAt.setHours(updatedAt.getHours() - 1);
+
+ wrapper = shallowMount(Toolbar, {
+ localVue,
+ router,
+ propsData: {
+ id: '1',
+ isLatestVersion: true,
+ isLoading,
+ isDeleting: false,
+ filename: 'test.jpg',
+ updatedAt: updatedAt.toString(),
+ updatedBy: {
+ name: 'Test Name',
+ },
+ image: '/-/designs/306/7f747adcd4693afadbe968d7ba7d983349b9012d',
+ ...props,
+ },
+ stubs: {
+ 'router-link': RouterLinkStub,
+ },
+ });
+
+ wrapper.setData({
+ permissions: {
+ createDesign,
+ },
+ });
+ }
+
+ afterEach(() => {
+ wrapper.destroy();
+ });
+
+ it('renders design and updated data', () => {
+ createComponent();
+
+ return wrapper.vm.$nextTick().then(() => {
+ expect(wrapper.element).toMatchSnapshot();
+ });
+ });
+
+ it('links back to designs list', () => {
+ createComponent();
+
+ return wrapper.vm.$nextTick().then(() => {
+ const link = wrapper.find('a');
+
+ expect(link.props('to')).toEqual({
+ name: DESIGNS_ROUTE_NAME,
+ query: {
+ version: undefined,
+ },
+ });
+ });
+ });
+
+ it('renders delete button on latest designs version with logged in user', () => {
+ createComponent();
+
+ return wrapper.vm.$nextTick().then(() => {
+ expect(wrapper.find(DeleteButton).exists()).toBe(true);
+ });
+ });
+
+ it('does not render delete button on non-latest version', () => {
+ createComponent(false, true, { isLatestVersion: false });
+
+ return wrapper.vm.$nextTick().then(() => {
+ expect(wrapper.find(DeleteButton).exists()).toBe(false);
+ });
+ });
+
+ it('does not render delete button when user is not logged in', () => {
+ createComponent(false, false);
+
+ return wrapper.vm.$nextTick().then(() => {
+ expect(wrapper.find(DeleteButton).exists()).toBe(false);
+ });
+ });
+
+ it('emits `delete` event on deleteButton `deleteSelectedDesigns` event', () => {
+ createComponent();
+
+ return wrapper.vm.$nextTick().then(() => {
+ wrapper.find(DeleteButton).vm.$emit('deleteSelectedDesigns');
+ expect(wrapper.emitted().delete).toBeTruthy();
+ });
+ });
+
+ it('renders download button with correct link', () => {
+ expect(wrapper.find(GlDeprecatedButton).attributes('href')).toBe(
+ '/-/designs/306/7f747adcd4693afadbe968d7ba7d983349b9012d',
+ );
+ });
+});
diff --git a/spec/frontend/design_management/components/toolbar/pagination_button_spec.js b/spec/frontend/design_management/components/toolbar/pagination_button_spec.js
new file mode 100644
index 00000000000..b7df201795b
--- /dev/null
+++ b/spec/frontend/design_management/components/toolbar/pagination_button_spec.js
@@ -0,0 +1,61 @@
+import { createLocalVue, shallowMount } from '@vue/test-utils';
+import VueRouter from 'vue-router';
+import PaginationButton from '~/design_management/components/toolbar/pagination_button.vue';
+import { DESIGN_ROUTE_NAME } from '~/design_management/router/constants';
+
+const localVue = createLocalVue();
+localVue.use(VueRouter);
+const router = new VueRouter();
+
+describe('Design management pagination button component', () => {
+ let wrapper;
+
+ function createComponent(design = null) {
+ wrapper = shallowMount(PaginationButton, {
+ localVue,
+ router,
+ propsData: {
+ design,
+ title: 'Test title',
+ iconName: 'angle-right',
+ },
+ stubs: ['router-link'],
+ });
+ }
+
+ afterEach(() => {
+ wrapper.destroy();
+ });
+
+ it('disables button when no design is passed', () => {
+ createComponent();
+
+ expect(wrapper.element).toMatchSnapshot();
+ });
+
+ it('renders router-link', () => {
+ createComponent({ id: '2' });
+
+ expect(wrapper.element).toMatchSnapshot();
+ });
+
+ describe('designLink', () => {
+ it('returns empty link when design is null', () => {
+ createComponent();
+
+ expect(wrapper.vm.designLink).toEqual({});
+ });
+
+ it('returns design link', () => {
+ createComponent({ id: '2', filename: 'test' });
+
+ wrapper.vm.$router.replace('/root/test-project/issues/1/designs/test?version=1');
+
+ expect(wrapper.vm.designLink).toEqual({
+ name: DESIGN_ROUTE_NAME,
+ params: { id: 'test' },
+ query: { version: '1' },
+ });
+ });
+ });
+});
diff --git a/spec/frontend/design_management/components/toolbar/pagination_spec.js b/spec/frontend/design_management/components/toolbar/pagination_spec.js
new file mode 100644
index 00000000000..db5a36dadf6
--- /dev/null
+++ b/spec/frontend/design_management/components/toolbar/pagination_spec.js
@@ -0,0 +1,79 @@
+/* global Mousetrap */
+import 'mousetrap';
+import { shallowMount } from '@vue/test-utils';
+import Pagination from '~/design_management/components/toolbar/pagination.vue';
+import { DESIGN_ROUTE_NAME } from '~/design_management/router/constants';
+
+const push = jest.fn();
+const $router = {
+ push,
+};
+
+const $route = {
+ path: '/designs/design-2',
+ query: {},
+};
+
+describe('Design management pagination component', () => {
+ let wrapper;
+
+ function createComponent() {
+ wrapper = shallowMount(Pagination, {
+ propsData: {
+ id: '2',
+ },
+ mocks: {
+ $router,
+ $route,
+ },
+ });
+ }
+
+ beforeEach(() => {
+ createComponent();
+ });
+
+ afterEach(() => {
+ wrapper.destroy();
+ });
+
+ it('hides components when designs are empty', () => {
+ expect(wrapper.element).toMatchSnapshot();
+ });
+
+ it('renders pagination buttons', () => {
+ wrapper.setData({
+ designs: [{ id: '1' }, { id: '2' }],
+ });
+
+ return wrapper.vm.$nextTick().then(() => {
+ expect(wrapper.element).toMatchSnapshot();
+ });
+ });
+
+ describe('keyboard buttons navigation', () => {
+ beforeEach(() => {
+ wrapper.setData({
+ designs: [{ filename: '1' }, { filename: '2' }, { filename: '3' }],
+ });
+ });
+
+ it('routes to previous design on Left button', () => {
+ Mousetrap.trigger('left');
+ expect(push).toHaveBeenCalledWith({
+ name: DESIGN_ROUTE_NAME,
+ params: { id: '1' },
+ query: {},
+ });
+ });
+
+ it('routes to next design on Right button', () => {
+ Mousetrap.trigger('right');
+ expect(push).toHaveBeenCalledWith({
+ name: DESIGN_ROUTE_NAME,
+ params: { id: '3' },
+ query: {},
+ });
+ });
+ });
+});
diff --git a/spec/frontend/design_management/components/upload/__snapshots__/button_spec.js.snap b/spec/frontend/design_management/components/upload/__snapshots__/button_spec.js.snap
new file mode 100644
index 00000000000..185bf4a48f7
--- /dev/null
+++ b/spec/frontend/design_management/components/upload/__snapshots__/button_spec.js.snap
@@ -0,0 +1,79 @@
+// Jest Snapshot v1, https://goo.gl/fbAQLP
+
+exports[`Design management upload button component renders inverted upload design button 1`] = `
+<div
+ isinverted="true"
+>
+ <gl-deprecated-button-stub
+ size="md"
+ title="Adding a design with the same filename replaces the file in a new version."
+ variant="success"
+ >
+
+ Add designs
+
+ <!---->
+ </gl-deprecated-button-stub>
+
+ <input
+ accept="image/*"
+ class="hide"
+ multiple="multiple"
+ name="design_file"
+ type="file"
+ />
+</div>
+`;
+
+exports[`Design management upload button component renders loading icon 1`] = `
+<div>
+ <gl-deprecated-button-stub
+ disabled="true"
+ size="md"
+ title="Adding a design with the same filename replaces the file in a new version."
+ variant="success"
+ >
+
+ Add designs
+
+ <gl-loading-icon-stub
+ class="ml-1"
+ color="orange"
+ inline="true"
+ label="Loading"
+ size="sm"
+ />
+ </gl-deprecated-button-stub>
+
+ <input
+ accept="image/*"
+ class="hide"
+ multiple="multiple"
+ name="design_file"
+ type="file"
+ />
+</div>
+`;
+
+exports[`Design management upload button component renders upload design button 1`] = `
+<div>
+ <gl-deprecated-button-stub
+ size="md"
+ title="Adding a design with the same filename replaces the file in a new version."
+ variant="success"
+ >
+
+ Add designs
+
+ <!---->
+ </gl-deprecated-button-stub>
+
+ <input
+ accept="image/*"
+ class="hide"
+ multiple="multiple"
+ name="design_file"
+ type="file"
+ />
+</div>
+`;
diff --git a/spec/frontend/design_management/components/upload/__snapshots__/design_dropzone_spec.js.snap b/spec/frontend/design_management/components/upload/__snapshots__/design_dropzone_spec.js.snap
new file mode 100644
index 00000000000..0737b9729a2
--- /dev/null
+++ b/spec/frontend/design_management/components/upload/__snapshots__/design_dropzone_spec.js.snap
@@ -0,0 +1,455 @@
+// Jest Snapshot v1, https://goo.gl/fbAQLP
+
+exports[`Design management dropzone component when dragging renders correct template when drag event contains files 1`] = `
+<div
+ class="w-100 position-relative"
+>
+ <button
+ class="card design-dropzone-card design-dropzone-border w-100 h-100 d-flex-center p-3"
+ >
+ <div
+ class="d-flex-center flex-column text-center"
+ >
+ <gl-icon-stub
+ class="mb-4"
+ name="doc-new"
+ size="48"
+ />
+
+ <p>
+ <gl-sprintf-stub
+ message="%{lineOneStart}Drag and drop to upload your designs%{lineOneEnd} or %{linkStart}click to upload%{linkEnd}."
+ />
+ </p>
+ </div>
+ </button>
+
+ <input
+ accept="image/*"
+ class="hide"
+ multiple="multiple"
+ name="design_file"
+ type="file"
+ />
+
+ <transition-stub
+ name="design-dropzone-fade"
+ >
+ <div
+ class="card design-dropzone-border design-dropzone-overlay w-100 h-100 position-absolute d-flex-center p-3 bg-white"
+ style=""
+ >
+ <div
+ class="mw-50 text-center"
+ style="display: none;"
+ >
+ <h3>
+ Oh no!
+ </h3>
+
+ <span>
+ You are trying to upload something other than an image. Please upload a .png, .jpg, .jpeg, .gif, .bmp, .tiff or .ico.
+ </span>
+ </div>
+
+ <div
+ class="mw-50 text-center"
+ style=""
+ >
+ <h3>
+ Incoming!
+ </h3>
+
+ <span>
+ Drop your designs to start your upload.
+ </span>
+ </div>
+ </div>
+ </transition-stub>
+</div>
+`;
+
+exports[`Design management dropzone component when dragging renders correct template when drag event contains files and text 1`] = `
+<div
+ class="w-100 position-relative"
+>
+ <button
+ class="card design-dropzone-card design-dropzone-border w-100 h-100 d-flex-center p-3"
+ >
+ <div
+ class="d-flex-center flex-column text-center"
+ >
+ <gl-icon-stub
+ class="mb-4"
+ name="doc-new"
+ size="48"
+ />
+
+ <p>
+ <gl-sprintf-stub
+ message="%{lineOneStart}Drag and drop to upload your designs%{lineOneEnd} or %{linkStart}click to upload%{linkEnd}."
+ />
+ </p>
+ </div>
+ </button>
+
+ <input
+ accept="image/*"
+ class="hide"
+ multiple="multiple"
+ name="design_file"
+ type="file"
+ />
+
+ <transition-stub
+ name="design-dropzone-fade"
+ >
+ <div
+ class="card design-dropzone-border design-dropzone-overlay w-100 h-100 position-absolute d-flex-center p-3 bg-white"
+ style=""
+ >
+ <div
+ class="mw-50 text-center"
+ style="display: none;"
+ >
+ <h3>
+ Oh no!
+ </h3>
+
+ <span>
+ You are trying to upload something other than an image. Please upload a .png, .jpg, .jpeg, .gif, .bmp, .tiff or .ico.
+ </span>
+ </div>
+
+ <div
+ class="mw-50 text-center"
+ style=""
+ >
+ <h3>
+ Incoming!
+ </h3>
+
+ <span>
+ Drop your designs to start your upload.
+ </span>
+ </div>
+ </div>
+ </transition-stub>
+</div>
+`;
+
+exports[`Design management dropzone component when dragging renders correct template when drag event contains text 1`] = `
+<div
+ class="w-100 position-relative"
+>
+ <button
+ class="card design-dropzone-card design-dropzone-border w-100 h-100 d-flex-center p-3"
+ >
+ <div
+ class="d-flex-center flex-column text-center"
+ >
+ <gl-icon-stub
+ class="mb-4"
+ name="doc-new"
+ size="48"
+ />
+
+ <p>
+ <gl-sprintf-stub
+ message="%{lineOneStart}Drag and drop to upload your designs%{lineOneEnd} or %{linkStart}click to upload%{linkEnd}."
+ />
+ </p>
+ </div>
+ </button>
+
+ <input
+ accept="image/*"
+ class="hide"
+ multiple="multiple"
+ name="design_file"
+ type="file"
+ />
+
+ <transition-stub
+ name="design-dropzone-fade"
+ >
+ <div
+ class="card design-dropzone-border design-dropzone-overlay w-100 h-100 position-absolute d-flex-center p-3 bg-white"
+ style=""
+ >
+ <div
+ class="mw-50 text-center"
+ >
+ <h3>
+ Oh no!
+ </h3>
+
+ <span>
+ You are trying to upload something other than an image. Please upload a .png, .jpg, .jpeg, .gif, .bmp, .tiff or .ico.
+ </span>
+ </div>
+
+ <div
+ class="mw-50 text-center"
+ style="display: none;"
+ >
+ <h3>
+ Incoming!
+ </h3>
+
+ <span>
+ Drop your designs to start your upload.
+ </span>
+ </div>
+ </div>
+ </transition-stub>
+</div>
+`;
+
+exports[`Design management dropzone component when dragging renders correct template when drag event is empty 1`] = `
+<div
+ class="w-100 position-relative"
+>
+ <button
+ class="card design-dropzone-card design-dropzone-border w-100 h-100 d-flex-center p-3"
+ >
+ <div
+ class="d-flex-center flex-column text-center"
+ >
+ <gl-icon-stub
+ class="mb-4"
+ name="doc-new"
+ size="48"
+ />
+
+ <p>
+ <gl-sprintf-stub
+ message="%{lineOneStart}Drag and drop to upload your designs%{lineOneEnd} or %{linkStart}click to upload%{linkEnd}."
+ />
+ </p>
+ </div>
+ </button>
+
+ <input
+ accept="image/*"
+ class="hide"
+ multiple="multiple"
+ name="design_file"
+ type="file"
+ />
+
+ <transition-stub
+ name="design-dropzone-fade"
+ >
+ <div
+ class="card design-dropzone-border design-dropzone-overlay w-100 h-100 position-absolute d-flex-center p-3 bg-white"
+ style=""
+ >
+ <div
+ class="mw-50 text-center"
+ >
+ <h3>
+ Oh no!
+ </h3>
+
+ <span>
+ You are trying to upload something other than an image. Please upload a .png, .jpg, .jpeg, .gif, .bmp, .tiff or .ico.
+ </span>
+ </div>
+
+ <div
+ class="mw-50 text-center"
+ style="display: none;"
+ >
+ <h3>
+ Incoming!
+ </h3>
+
+ <span>
+ Drop your designs to start your upload.
+ </span>
+ </div>
+ </div>
+ </transition-stub>
+</div>
+`;
+
+exports[`Design management dropzone component when dragging renders correct template when dragging stops 1`] = `
+<div
+ class="w-100 position-relative"
+>
+ <button
+ class="card design-dropzone-card design-dropzone-border w-100 h-100 d-flex-center p-3"
+ >
+ <div
+ class="d-flex-center flex-column text-center"
+ >
+ <gl-icon-stub
+ class="mb-4"
+ name="doc-new"
+ size="48"
+ />
+
+ <p>
+ <gl-sprintf-stub
+ message="%{lineOneStart}Drag and drop to upload your designs%{lineOneEnd} or %{linkStart}click to upload%{linkEnd}."
+ />
+ </p>
+ </div>
+ </button>
+
+ <input
+ accept="image/*"
+ class="hide"
+ multiple="multiple"
+ name="design_file"
+ type="file"
+ />
+
+ <transition-stub
+ name="design-dropzone-fade"
+ >
+ <div
+ class="card design-dropzone-border design-dropzone-overlay w-100 h-100 position-absolute d-flex-center p-3 bg-white"
+ style="display: none;"
+ >
+ <div
+ class="mw-50 text-center"
+ >
+ <h3>
+ Oh no!
+ </h3>
+
+ <span>
+ You are trying to upload something other than an image. Please upload a .png, .jpg, .jpeg, .gif, .bmp, .tiff or .ico.
+ </span>
+ </div>
+
+ <div
+ class="mw-50 text-center"
+ style="display: none;"
+ >
+ <h3>
+ Incoming!
+ </h3>
+
+ <span>
+ Drop your designs to start your upload.
+ </span>
+ </div>
+ </div>
+ </transition-stub>
+</div>
+`;
+
+exports[`Design management dropzone component when no slot provided renders default dropzone card 1`] = `
+<div
+ class="w-100 position-relative"
+>
+ <button
+ class="card design-dropzone-card design-dropzone-border w-100 h-100 d-flex-center p-3"
+ >
+ <div
+ class="d-flex-center flex-column text-center"
+ >
+ <gl-icon-stub
+ class="mb-4"
+ name="doc-new"
+ size="48"
+ />
+
+ <p>
+ <gl-sprintf-stub
+ message="%{lineOneStart}Drag and drop to upload your designs%{lineOneEnd} or %{linkStart}click to upload%{linkEnd}."
+ />
+ </p>
+ </div>
+ </button>
+
+ <input
+ accept="image/*"
+ class="hide"
+ multiple="multiple"
+ name="design_file"
+ type="file"
+ />
+
+ <transition-stub
+ name="design-dropzone-fade"
+ >
+ <div
+ class="card design-dropzone-border design-dropzone-overlay w-100 h-100 position-absolute d-flex-center p-3 bg-white"
+ style="display: none;"
+ >
+ <div
+ class="mw-50 text-center"
+ >
+ <h3>
+ Oh no!
+ </h3>
+
+ <span>
+ You are trying to upload something other than an image. Please upload a .png, .jpg, .jpeg, .gif, .bmp, .tiff or .ico.
+ </span>
+ </div>
+
+ <div
+ class="mw-50 text-center"
+ style="display: none;"
+ >
+ <h3>
+ Incoming!
+ </h3>
+
+ <span>
+ Drop your designs to start your upload.
+ </span>
+ </div>
+ </div>
+ </transition-stub>
+</div>
+`;
+
+exports[`Design management dropzone component when slot provided renders dropzone with slot content 1`] = `
+<div
+ class="w-100 position-relative"
+>
+ <div>
+ dropzone slot
+ </div>
+
+ <transition-stub
+ name="design-dropzone-fade"
+ >
+ <div
+ class="card design-dropzone-border design-dropzone-overlay w-100 h-100 position-absolute d-flex-center p-3 bg-white"
+ style="display: none;"
+ >
+ <div
+ class="mw-50 text-center"
+ >
+ <h3>
+ Oh no!
+ </h3>
+
+ <span>
+ You are trying to upload something other than an image. Please upload a .png, .jpg, .jpeg, .gif, .bmp, .tiff or .ico.
+ </span>
+ </div>
+
+ <div
+ class="mw-50 text-center"
+ style="display: none;"
+ >
+ <h3>
+ Incoming!
+ </h3>
+
+ <span>
+ Drop your designs to start your upload.
+ </span>
+ </div>
+ </div>
+ </transition-stub>
+</div>
+`;
diff --git a/spec/frontend/design_management/components/upload/__snapshots__/design_version_dropdown_spec.js.snap b/spec/frontend/design_management/components/upload/__snapshots__/design_version_dropdown_spec.js.snap
new file mode 100644
index 00000000000..00f1a40dfb2
--- /dev/null
+++ b/spec/frontend/design_management/components/upload/__snapshots__/design_version_dropdown_spec.js.snap
@@ -0,0 +1,111 @@
+// Jest Snapshot v1, https://goo.gl/fbAQLP
+
+exports[`Design management design version dropdown component renders design version dropdown button 1`] = `
+<gl-dropdown-stub
+ class="design-version-dropdown"
+ issueiid=""
+ projectpath=""
+ text="Showing Latest Version"
+ variant="link"
+>
+ <gl-dropdown-item-stub>
+ <router-link-stub
+ class="d-flex js-version-link"
+ to="[object Object]"
+ >
+ <div
+ class="flex-grow-1 ml-2"
+ >
+ <div>
+ <strong>
+ Version 2
+
+ <span>
+ (latest)
+ </span>
+ </strong>
+ </div>
+ </div>
+
+ <i
+ class="fa fa-check pull-right"
+ />
+ </router-link-stub>
+ </gl-dropdown-item-stub>
+ <gl-dropdown-item-stub>
+ <router-link-stub
+ class="d-flex js-version-link"
+ to="[object Object]"
+ >
+ <div
+ class="flex-grow-1 ml-2"
+ >
+ <div>
+ <strong>
+ Version 1
+
+ <!---->
+ </strong>
+ </div>
+ </div>
+
+ <!---->
+ </router-link-stub>
+ </gl-dropdown-item-stub>
+</gl-dropdown-stub>
+`;
+
+exports[`Design management design version dropdown component renders design version list 1`] = `
+<gl-dropdown-stub
+ class="design-version-dropdown"
+ issueiid=""
+ projectpath=""
+ text="Showing Latest Version"
+ variant="link"
+>
+ <gl-dropdown-item-stub>
+ <router-link-stub
+ class="d-flex js-version-link"
+ to="[object Object]"
+ >
+ <div
+ class="flex-grow-1 ml-2"
+ >
+ <div>
+ <strong>
+ Version 2
+
+ <span>
+ (latest)
+ </span>
+ </strong>
+ </div>
+ </div>
+
+ <i
+ class="fa fa-check pull-right"
+ />
+ </router-link-stub>
+ </gl-dropdown-item-stub>
+ <gl-dropdown-item-stub>
+ <router-link-stub
+ class="d-flex js-version-link"
+ to="[object Object]"
+ >
+ <div
+ class="flex-grow-1 ml-2"
+ >
+ <div>
+ <strong>
+ Version 1
+
+ <!---->
+ </strong>
+ </div>
+ </div>
+
+ <!---->
+ </router-link-stub>
+ </gl-dropdown-item-stub>
+</gl-dropdown-stub>
+`;
diff --git a/spec/frontend/design_management/components/upload/button_spec.js b/spec/frontend/design_management/components/upload/button_spec.js
new file mode 100644
index 00000000000..c0a9693dc37
--- /dev/null
+++ b/spec/frontend/design_management/components/upload/button_spec.js
@@ -0,0 +1,59 @@
+import { shallowMount } from '@vue/test-utils';
+import UploadButton from '~/design_management/components/upload/button.vue';
+
+describe('Design management upload button component', () => {
+ let wrapper;
+
+ function createComponent(isSaving = false, isInverted = false) {
+ wrapper = shallowMount(UploadButton, {
+ propsData: {
+ isSaving,
+ isInverted,
+ },
+ });
+ }
+
+ afterEach(() => {
+ wrapper.destroy();
+ });
+
+ it('renders upload design button', () => {
+ createComponent();
+
+ expect(wrapper.element).toMatchSnapshot();
+ });
+
+ it('renders inverted upload design button', () => {
+ createComponent(false, true);
+
+ expect(wrapper.element).toMatchSnapshot();
+ });
+
+ it('renders loading icon', () => {
+ createComponent(true);
+
+ expect(wrapper.element).toMatchSnapshot();
+ });
+
+ describe('onFileUploadChange', () => {
+ it('emits upload event', () => {
+ createComponent();
+
+ wrapper.vm.onFileUploadChange({ target: { files: 'test' } });
+
+ expect(wrapper.emitted().upload[0]).toEqual(['test']);
+ });
+ });
+
+ describe('openFileUpload', () => {
+ it('triggers click on input', () => {
+ createComponent();
+
+ const clickSpy = jest.spyOn(wrapper.find('input').element, 'click');
+
+ wrapper.vm.openFileUpload();
+
+ expect(clickSpy).toHaveBeenCalled();
+ });
+ });
+});
diff --git a/spec/frontend/design_management/components/upload/design_dropzone_spec.js b/spec/frontend/design_management/components/upload/design_dropzone_spec.js
new file mode 100644
index 00000000000..9b86b5b2878
--- /dev/null
+++ b/spec/frontend/design_management/components/upload/design_dropzone_spec.js
@@ -0,0 +1,132 @@
+import { shallowMount } from '@vue/test-utils';
+import DesignDropzone from '~/design_management/components/upload/design_dropzone.vue';
+import createFlash from '~/flash';
+
+jest.mock('~/flash');
+
+describe('Design management dropzone component', () => {
+ let wrapper;
+
+ const mockDragEvent = ({ types = ['Files'], files = [] }) => {
+ return { dataTransfer: { types, files } };
+ };
+
+ const findDropzoneCard = () => wrapper.find('.design-dropzone-card');
+
+ function createComponent({ slots = {}, data = {} } = {}) {
+ wrapper = shallowMount(DesignDropzone, {
+ slots,
+ data() {
+ return data;
+ },
+ });
+ }
+
+ afterEach(() => {
+ wrapper.destroy();
+ });
+
+ describe('when slot provided', () => {
+ it('renders dropzone with slot content', () => {
+ createComponent({
+ slots: {
+ default: ['<div>dropzone slot</div>'],
+ },
+ });
+
+ expect(wrapper.element).toMatchSnapshot();
+ });
+ });
+
+ describe('when no slot provided', () => {
+ it('renders default dropzone card', () => {
+ createComponent();
+
+ expect(wrapper.element).toMatchSnapshot();
+ });
+
+ it('triggers click event on file input element when clicked', () => {
+ createComponent();
+ const clickSpy = jest.spyOn(wrapper.find('input').element, 'click');
+
+ findDropzoneCard().trigger('click');
+ expect(clickSpy).toHaveBeenCalled();
+ });
+ });
+
+ describe('when dragging', () => {
+ it.each`
+ description | eventPayload
+ ${'is empty'} | ${{}}
+ ${'contains text'} | ${mockDragEvent({ types: ['text'] })}
+ ${'contains files and text'} | ${mockDragEvent({ types: ['Files', 'text'] })}
+ ${'contains files'} | ${mockDragEvent({ types: ['Files'] })}
+ `('renders correct template when drag event $description', ({ eventPayload }) => {
+ createComponent();
+
+ wrapper.trigger('dragenter', eventPayload);
+ return wrapper.vm.$nextTick().then(() => {
+ expect(wrapper.element).toMatchSnapshot();
+ });
+ });
+
+ it('renders correct template when dragging stops', () => {
+ createComponent();
+
+ wrapper.trigger('dragenter');
+ return wrapper.vm
+ .$nextTick()
+ .then(() => {
+ wrapper.trigger('dragleave');
+ return wrapper.vm.$nextTick();
+ })
+ .then(() => {
+ expect(wrapper.element).toMatchSnapshot();
+ });
+ });
+ });
+
+ describe('when dropping', () => {
+ it('emits upload event', () => {
+ createComponent();
+ const mockFile = { name: 'test', type: 'image/jpg' };
+ const mockEvent = mockDragEvent({ files: [mockFile] });
+
+ wrapper.trigger('dragenter', mockEvent);
+ return wrapper.vm
+ .$nextTick()
+ .then(() => {
+ wrapper.trigger('drop', mockEvent);
+ return wrapper.vm.$nextTick();
+ })
+ .then(() => {
+ expect(wrapper.emitted().change[0]).toEqual([[mockFile]]);
+ });
+ });
+ });
+
+ describe('ondrop', () => {
+ const mockData = { dragCounter: 1, isDragDataValid: true };
+
+ describe('when drag data is valid', () => {
+ it('emits upload event for valid files', () => {
+ createComponent({ data: mockData });
+
+ const mockFile = { type: 'image/jpg' };
+ const mockEvent = mockDragEvent({ files: [mockFile] });
+
+ wrapper.vm.ondrop(mockEvent);
+ expect(wrapper.emitted().change[0]).toEqual([[mockFile]]);
+ });
+
+ it('calls createFlash when files are invalid', () => {
+ createComponent({ data: mockData });
+
+ const mockEvent = mockDragEvent({ files: [{ type: 'audio/midi' }] });
+
+ wrapper.vm.ondrop(mockEvent);
+ expect(createFlash).toHaveBeenCalledTimes(1);
+ });
+ });
+ });
+});
diff --git a/spec/frontend/design_management/components/upload/design_version_dropdown_spec.js b/spec/frontend/design_management/components/upload/design_version_dropdown_spec.js
new file mode 100644
index 00000000000..7521b9fad2a
--- /dev/null
+++ b/spec/frontend/design_management/components/upload/design_version_dropdown_spec.js
@@ -0,0 +1,114 @@
+import { shallowMount } from '@vue/test-utils';
+import DesignVersionDropdown from '~/design_management/components/upload/design_version_dropdown.vue';
+import { GlDropdown, GlDropdownItem } from '@gitlab/ui';
+import mockAllVersions from './mock_data/all_versions';
+
+const LATEST_VERSION_ID = 3;
+const PREVIOUS_VERSION_ID = 2;
+
+const designRouteFactory = versionId => ({
+ path: `/designs?version=${versionId}`,
+ query: {
+ version: `${versionId}`,
+ },
+});
+
+const MOCK_ROUTE = {
+ path: '/designs',
+ query: {},
+};
+
+describe('Design management design version dropdown component', () => {
+ let wrapper;
+
+ function createComponent({ maxVersions = -1, $route = MOCK_ROUTE } = {}) {
+ wrapper = shallowMount(DesignVersionDropdown, {
+ propsData: {
+ projectPath: '',
+ issueIid: '',
+ },
+ mocks: {
+ $route,
+ },
+ stubs: ['router-link'],
+ });
+
+ wrapper.setData({
+ allVersions: maxVersions > -1 ? mockAllVersions.slice(0, maxVersions) : mockAllVersions,
+ });
+ }
+
+ afterEach(() => {
+ wrapper.destroy();
+ });
+
+ const findVersionLink = index => wrapper.findAll('.js-version-link').at(index);
+
+ it('renders design version dropdown button', () => {
+ createComponent();
+
+ return wrapper.vm.$nextTick().then(() => {
+ expect(wrapper.element).toMatchSnapshot();
+ });
+ });
+
+ it('renders design version list', () => {
+ createComponent();
+
+ return wrapper.vm.$nextTick().then(() => {
+ expect(wrapper.element).toMatchSnapshot();
+ });
+ });
+
+ describe('selected version name', () => {
+ it('has "latest" on most recent version item', () => {
+ createComponent();
+
+ return wrapper.vm.$nextTick().then(() => {
+ expect(findVersionLink(0).text()).toContain('latest');
+ });
+ });
+ });
+
+ describe('versions list', () => {
+ it('displays latest version text by default', () => {
+ createComponent();
+
+ return wrapper.vm.$nextTick().then(() => {
+ expect(wrapper.find(GlDropdown).attributes('text')).toBe('Showing Latest Version');
+ });
+ });
+
+ it('displays latest version text when only 1 version is present', () => {
+ createComponent({ maxVersions: 1 });
+
+ return wrapper.vm.$nextTick().then(() => {
+ expect(wrapper.find(GlDropdown).attributes('text')).toBe('Showing Latest Version');
+ });
+ });
+
+ it('displays version text when the current version is not the latest', () => {
+ createComponent({ $route: designRouteFactory(PREVIOUS_VERSION_ID) });
+
+ return wrapper.vm.$nextTick().then(() => {
+ expect(wrapper.find(GlDropdown).attributes('text')).toBe(`Showing Version #1`);
+ });
+ });
+
+ it('displays latest version text when the current version is the latest', () => {
+ createComponent({ $route: designRouteFactory(LATEST_VERSION_ID) });
+
+ return wrapper.vm.$nextTick().then(() => {
+ expect(wrapper.find(GlDropdown).attributes('text')).toBe('Showing Latest Version');
+ });
+ });
+
+ it('should have the same length as apollo query', () => {
+ createComponent();
+
+ return wrapper.vm.$nextTick().then(() => {
+ expect(wrapper.findAll(GlDropdownItem)).toHaveLength(wrapper.vm.allVersions.length);
+ });
+ });
+ });
+});
diff --git a/spec/frontend/design_management/components/upload/mock_data/all_versions.js b/spec/frontend/design_management/components/upload/mock_data/all_versions.js
new file mode 100644
index 00000000000..e76bbd261bd
--- /dev/null
+++ b/spec/frontend/design_management/components/upload/mock_data/all_versions.js
@@ -0,0 +1,14 @@
+export default [
+ {
+ node: {
+ id: 'gid://gitlab/DesignManagement::Version/3',
+ sha: '0945756378e0b1588b9dd40d5a6b99e8b7198f55',
+ },
+ },
+ {
+ node: {
+ id: 'gid://gitlab/DesignManagement::Version/2',
+ sha: '5b063fef0cd7213b312db65b30e24f057df21b20',
+ },
+ },
+];
diff --git a/spec/frontend/design_management/mock_data/all_versions.js b/spec/frontend/design_management/mock_data/all_versions.js
new file mode 100644
index 00000000000..c389fdb8747
--- /dev/null
+++ b/spec/frontend/design_management/mock_data/all_versions.js
@@ -0,0 +1,8 @@
+export default [
+ {
+ node: {
+ id: 'gid://gitlab/DesignManagement::Version/1',
+ sha: 'b389071a06c153509e11da1f582005b316667001',
+ },
+ },
+];
diff --git a/spec/frontend/design_management/mock_data/design.js b/spec/frontend/design_management/mock_data/design.js
new file mode 100644
index 00000000000..34e3077f4a2
--- /dev/null
+++ b/spec/frontend/design_management/mock_data/design.js
@@ -0,0 +1,54 @@
+export default {
+ id: 'design-id',
+ filename: 'test.jpg',
+ fullPath: 'full-design-path',
+ image: 'test.jpg',
+ updatedAt: '01-01-2019',
+ updatedBy: {
+ name: 'test',
+ },
+ issue: {
+ title: 'My precious issue',
+ webPath: 'full-issue-path',
+ webUrl: 'full-issue-url',
+ participants: {
+ edges: [
+ {
+ node: {
+ name: 'Administrator',
+ username: 'root',
+ webUrl: 'link-to-author',
+ avatarUrl: 'link-to-avatar',
+ },
+ },
+ ],
+ },
+ },
+ discussions: {
+ nodes: [
+ {
+ id: 'discussion-id',
+ replyId: 'discussion-reply-id',
+ notes: {
+ nodes: [
+ {
+ id: 'note-id',
+ body: '123',
+ author: {
+ name: 'Administrator',
+ username: 'root',
+ webUrl: 'link-to-author',
+ avatarUrl: 'link-to-avatar',
+ },
+ },
+ ],
+ },
+ },
+ ],
+ },
+ diffRefs: {
+ headSha: 'headSha',
+ baseSha: 'baseSha',
+ startSha: 'startSha',
+ },
+};
diff --git a/spec/frontend/design_management/mock_data/designs.js b/spec/frontend/design_management/mock_data/designs.js
new file mode 100644
index 00000000000..07f5c1b7457
--- /dev/null
+++ b/spec/frontend/design_management/mock_data/designs.js
@@ -0,0 +1,17 @@
+import design from './design';
+
+export default {
+ project: {
+ issue: {
+ designCollection: {
+ designs: {
+ edges: [
+ {
+ node: design,
+ },
+ ],
+ },
+ },
+ },
+ },
+};
diff --git a/spec/frontend/design_management/mock_data/no_designs.js b/spec/frontend/design_management/mock_data/no_designs.js
new file mode 100644
index 00000000000..9db0ffcade2
--- /dev/null
+++ b/spec/frontend/design_management/mock_data/no_designs.js
@@ -0,0 +1,11 @@
+export default {
+ project: {
+ issue: {
+ designCollection: {
+ designs: {
+ edges: [],
+ },
+ },
+ },
+ },
+};
diff --git a/spec/frontend/design_management/mock_data/notes.js b/spec/frontend/design_management/mock_data/notes.js
new file mode 100644
index 00000000000..db4624c8524
--- /dev/null
+++ b/spec/frontend/design_management/mock_data/notes.js
@@ -0,0 +1,32 @@
+export default [
+ {
+ id: 'note-id-1',
+ position: {
+ height: 100,
+ width: 100,
+ x: 10,
+ y: 15,
+ },
+ userPermissions: {
+ adminNote: true,
+ },
+ discussion: {
+ id: 'discussion-id-1',
+ },
+ },
+ {
+ id: 'note-id-2',
+ position: {
+ height: 50,
+ width: 50,
+ x: 25,
+ y: 25,
+ },
+ userPermissions: {
+ adminNote: true,
+ },
+ discussion: {
+ id: 'discussion-id-2',
+ },
+ },
+];
diff --git a/spec/frontend/design_management/pages/__snapshots__/index_spec.js.snap b/spec/frontend/design_management/pages/__snapshots__/index_spec.js.snap
new file mode 100644
index 00000000000..3ba63fd14f0
--- /dev/null
+++ b/spec/frontend/design_management/pages/__snapshots__/index_spec.js.snap
@@ -0,0 +1,263 @@
+// Jest Snapshot v1, https://goo.gl/fbAQLP
+
+exports[`Design management index page designs does not render toolbar when there is no permission 1`] = `
+<div>
+ <!---->
+
+ <div
+ class="mt-4"
+ >
+ <ol
+ class="list-unstyled row"
+ >
+ <li
+ class="col-md-6 col-lg-4 mb-3"
+ >
+ <design-dropzone-stub
+ class="design-list-item"
+ />
+ </li>
+
+ <li
+ class="col-md-6 col-lg-4 mb-3"
+ >
+ <design-dropzone-stub>
+ <design-stub
+ event="NONE"
+ filename="design-1-name"
+ id="design-1"
+ image="design-1-image"
+ notescount="0"
+ />
+ </design-dropzone-stub>
+
+ <!---->
+ </li>
+ <li
+ class="col-md-6 col-lg-4 mb-3"
+ >
+ <design-dropzone-stub>
+ <design-stub
+ event="NONE"
+ filename="design-2-name"
+ id="design-2"
+ image="design-2-image"
+ notescount="1"
+ />
+ </design-dropzone-stub>
+
+ <!---->
+ </li>
+ <li
+ class="col-md-6 col-lg-4 mb-3"
+ >
+ <design-dropzone-stub>
+ <design-stub
+ event="NONE"
+ filename="design-3-name"
+ id="design-3"
+ image="design-3-image"
+ notescount="0"
+ />
+ </design-dropzone-stub>
+
+ <!---->
+ </li>
+ </ol>
+ </div>
+
+ <router-view-stub
+ name="default"
+ />
+</div>
+`;
+
+exports[`Design management index page designs renders designs list and header with upload button 1`] = `
+<div>
+ <header
+ class="row-content-block border-top-0 p-2 d-flex"
+ >
+ <div
+ class="d-flex justify-content-between align-items-center w-100"
+ >
+ <design-version-dropdown-stub />
+
+ <div
+ class="qa-selector-toolbar d-flex"
+ >
+ <gl-deprecated-button-stub
+ class="mr-2 js-select-all"
+ size="md"
+ variant="link"
+ >
+ Select all
+ </gl-deprecated-button-stub>
+
+ <div>
+ <delete-button-stub
+ buttonclass="btn-danger btn-inverted mr-2"
+ buttonvariant=""
+ >
+
+ Delete selected
+
+ <!---->
+ </delete-button-stub>
+ </div>
+
+ <upload-button-stub />
+ </div>
+ </div>
+ </header>
+
+ <div
+ class="mt-4"
+ >
+ <ol
+ class="list-unstyled row"
+ >
+ <li
+ class="col-md-6 col-lg-4 mb-3"
+ >
+ <design-dropzone-stub
+ class="design-list-item"
+ />
+ </li>
+
+ <li
+ class="col-md-6 col-lg-4 mb-3"
+ >
+ <design-dropzone-stub>
+ <design-stub
+ event="NONE"
+ filename="design-1-name"
+ id="design-1"
+ image="design-1-image"
+ notescount="0"
+ />
+ </design-dropzone-stub>
+
+ <input
+ class="design-checkbox"
+ type="checkbox"
+ />
+ </li>
+ <li
+ class="col-md-6 col-lg-4 mb-3"
+ >
+ <design-dropzone-stub>
+ <design-stub
+ event="NONE"
+ filename="design-2-name"
+ id="design-2"
+ image="design-2-image"
+ notescount="1"
+ />
+ </design-dropzone-stub>
+
+ <input
+ class="design-checkbox"
+ type="checkbox"
+ />
+ </li>
+ <li
+ class="col-md-6 col-lg-4 mb-3"
+ >
+ <design-dropzone-stub>
+ <design-stub
+ event="NONE"
+ filename="design-3-name"
+ id="design-3"
+ image="design-3-image"
+ notescount="0"
+ />
+ </design-dropzone-stub>
+
+ <input
+ class="design-checkbox"
+ type="checkbox"
+ />
+ </li>
+ </ol>
+ </div>
+
+ <router-view-stub
+ name="default"
+ />
+</div>
+`;
+
+exports[`Design management index page designs renders error 1`] = `
+<div>
+ <!---->
+
+ <div
+ class="mt-4"
+ >
+ <gl-alert-stub
+ dismisslabel="Dismiss"
+ primarybuttonlink=""
+ primarybuttontext=""
+ secondarybuttonlink=""
+ secondarybuttontext=""
+ title=""
+ variant="danger"
+ >
+
+ An error occurred while loading designs. Please try again.
+
+ </gl-alert-stub>
+ </div>
+
+ <router-view-stub
+ name="default"
+ />
+</div>
+`;
+
+exports[`Design management index page designs renders loading icon 1`] = `
+<div>
+ <!---->
+
+ <div
+ class="mt-4"
+ >
+ <gl-loading-icon-stub
+ color="orange"
+ label="Loading"
+ size="md"
+ />
+ </div>
+
+ <router-view-stub
+ name="default"
+ />
+</div>
+`;
+
+exports[`Design management index page when has no designs renders empty text 1`] = `
+<div>
+ <!---->
+
+ <div
+ class="mt-4"
+ >
+ <ol
+ class="list-unstyled row"
+ >
+ <li
+ class="col-md-6 col-lg-4 mb-3"
+ >
+ <design-dropzone-stub
+ class="design-list-item"
+ />
+ </li>
+
+ </ol>
+ </div>
+
+ <router-view-stub
+ name="default"
+ />
+</div>
+`;
diff --git a/spec/frontend/design_management/pages/design/__snapshots__/index_spec.js.snap b/spec/frontend/design_management/pages/design/__snapshots__/index_spec.js.snap
new file mode 100644
index 00000000000..76e481ee518
--- /dev/null
+++ b/spec/frontend/design_management/pages/design/__snapshots__/index_spec.js.snap
@@ -0,0 +1,184 @@
+// Jest Snapshot v1, https://goo.gl/fbAQLP
+
+exports[`Design management design index page renders design index 1`] = `
+<div
+ class="design-detail js-design-detail fixed-top w-100 position-bottom-0 d-flex justify-content-center flex-column flex-lg-row"
+>
+ <div
+ class="d-flex overflow-hidden flex-grow-1 flex-column position-relative"
+ >
+ <design-destroyer-stub
+ filenames="test.jpg"
+ iid="1"
+ projectpath=""
+ />
+
+ <!---->
+
+ <design-presentation-stub
+ discussions="[object Object]"
+ image="test.jpg"
+ imagename="test.jpg"
+ scale="1"
+ />
+
+ <div
+ class="design-scaler-wrapper position-absolute mb-4 d-flex-center"
+ >
+ <design-scaler-stub />
+ </div>
+ </div>
+
+ <div
+ class="image-notes"
+ >
+ <h2
+ class="gl-font-size-20-deprecated-no-really-do-not-use-me font-weight-bold mt-0"
+ >
+
+ My precious issue
+
+ </h2>
+
+ <a
+ class="text-tertiary text-decoration-none mb-3 d-block"
+ href="full-issue-url"
+ >
+ ull-issue-path
+ </a>
+
+ <participants-stub
+ class="mb-4"
+ numberoflessparticipants="7"
+ participants="[object Object]"
+ />
+
+ <div
+ class="design-discussion-wrapper"
+ >
+ <div
+ class="badge badge-pill"
+ type="button"
+ >
+ 1
+ </div>
+
+ <div
+ class="design-discussion bordered-box position-relative"
+ data-qa-selector="design_discussion_content"
+ >
+ <design-note-stub
+ class=""
+ markdownpreviewpath="//preview_markdown?target_type=Issue"
+ note="[object Object]"
+ />
+
+ <div
+ class="reply-wrapper"
+ >
+ <reply-placeholder-stub
+ buttontext="Reply..."
+ class="qa-discussion-reply"
+ />
+ </div>
+ </div>
+ </div>
+
+ <!---->
+ </div>
+</div>
+`;
+
+exports[`Design management design index page sets loading state 1`] = `
+<div
+ class="design-detail js-design-detail fixed-top w-100 position-bottom-0 d-flex justify-content-center flex-column flex-lg-row"
+>
+ <gl-loading-icon-stub
+ class="align-self-center"
+ color="orange"
+ label="Loading"
+ size="xl"
+ />
+</div>
+`;
+
+exports[`Design management design index page with error GlAlert is rendered in correct position with correct content 1`] = `
+<div
+ class="design-detail js-design-detail fixed-top w-100 position-bottom-0 d-flex justify-content-center flex-column flex-lg-row"
+>
+ <div
+ class="d-flex overflow-hidden flex-grow-1 flex-column position-relative"
+ >
+ <design-destroyer-stub
+ filenames="test.jpg"
+ iid="1"
+ projectpath=""
+ />
+
+ <div
+ class="p-3"
+ >
+ <gl-alert-stub
+ dismissible="true"
+ dismisslabel="Dismiss"
+ primarybuttonlink=""
+ primarybuttontext=""
+ secondarybuttonlink=""
+ secondarybuttontext=""
+ title=""
+ variant="danger"
+ >
+
+ woops
+
+ </gl-alert-stub>
+ </div>
+
+ <design-presentation-stub
+ discussions=""
+ image="test.jpg"
+ imagename="test.jpg"
+ scale="1"
+ />
+
+ <div
+ class="design-scaler-wrapper position-absolute mb-4 d-flex-center"
+ >
+ <design-scaler-stub />
+ </div>
+ </div>
+
+ <div
+ class="image-notes"
+ >
+ <h2
+ class="gl-font-size-20-deprecated-no-really-do-not-use-me font-weight-bold mt-0"
+ >
+
+ My precious issue
+
+ </h2>
+
+ <a
+ class="text-tertiary text-decoration-none mb-3 d-block"
+ href="full-issue-url"
+ >
+ ull-issue-path
+ </a>
+
+ <participants-stub
+ class="mb-4"
+ numberoflessparticipants="7"
+ participants="[object Object]"
+ />
+
+ <h2
+ class="new-discussion-disclaimer gl-font-base m-0"
+ >
+
+ Click the image where you'd like to start a new discussion
+
+ </h2>
+ </div>
+</div>
+`;
diff --git a/spec/frontend/design_management/pages/design/index_spec.js b/spec/frontend/design_management/pages/design/index_spec.js
new file mode 100644
index 00000000000..9e2f071a983
--- /dev/null
+++ b/spec/frontend/design_management/pages/design/index_spec.js
@@ -0,0 +1,301 @@
+import { shallowMount } from '@vue/test-utils';
+import { GlAlert } from '@gitlab/ui';
+import { ApolloMutation } from 'vue-apollo';
+import createFlash from '~/flash';
+import DesignIndex from '~/design_management/pages/design/index.vue';
+import DesignDiscussion from '~/design_management/components/design_notes/design_discussion.vue';
+import DesignReplyForm from '~/design_management/components/design_notes/design_reply_form.vue';
+import Participants from '~/sidebar/components/participants/participants.vue';
+import createImageDiffNoteMutation from '~/design_management/graphql/mutations/createImageDiffNote.mutation.graphql';
+import updateActiveDiscussionMutation from '~/design_management/graphql/mutations/update_active_discussion.mutation.graphql';
+import design from '../../mock_data/design';
+import mockResponseWithDesigns from '../../mock_data/designs';
+import mockResponseNoDesigns from '../../mock_data/no_designs';
+import mockAllVersions from '../../mock_data/all_versions';
+import {
+ DESIGN_NOT_FOUND_ERROR,
+ DESIGN_VERSION_NOT_EXIST_ERROR,
+} from '~/design_management/utils/error_messages';
+import { DESIGNS_ROUTE_NAME } from '~/design_management/router/constants';
+
+jest.mock('~/flash');
+jest.mock('mousetrap', () => ({
+ bind: jest.fn(),
+ unbind: jest.fn(),
+}));
+
+describe('Design management design index page', () => {
+ let wrapper;
+ const newComment = 'new comment';
+ const annotationCoordinates = {
+ x: 10,
+ y: 10,
+ width: 100,
+ height: 100,
+ };
+ const createDiscussionMutationVariables = {
+ mutation: createImageDiffNoteMutation,
+ update: expect.anything(),
+ variables: {
+ input: {
+ body: newComment,
+ noteableId: design.id,
+ position: {
+ headSha: 'headSha',
+ baseSha: 'baseSha',
+ startSha: 'startSha',
+ paths: {
+ newPath: 'full-design-path',
+ },
+ ...annotationCoordinates,
+ },
+ },
+ },
+ };
+
+ const updateActiveDiscussionMutationVariables = {
+ mutation: updateActiveDiscussionMutation,
+ variables: {
+ id: design.discussions.nodes[0].notes.nodes[0].id,
+ source: 'discussion',
+ },
+ };
+
+ const mutate = jest.fn().mockResolvedValue();
+ const routerPush = jest.fn();
+
+ const findDiscussions = () => wrapper.findAll(DesignDiscussion);
+ const findDiscussionForm = () => wrapper.find(DesignReplyForm);
+ const findParticipants = () => wrapper.find(Participants);
+ const findDiscussionsWrapper = () => wrapper.find('.image-notes');
+
+ function createComponent(loading = false, data = {}, { routeQuery = {} } = {}) {
+ const $apollo = {
+ queries: {
+ design: {
+ loading,
+ },
+ },
+ mutate,
+ };
+
+ const $router = {
+ push: routerPush,
+ };
+
+ const $route = {
+ query: routeQuery,
+ };
+
+ wrapper = shallowMount(DesignIndex, {
+ propsData: { id: '1' },
+ mocks: { $apollo, $router, $route },
+ stubs: {
+ ApolloMutation,
+ DesignDiscussion,
+ },
+ data() {
+ return {
+ issueIid: '1',
+ activeDiscussion: {
+ id: null,
+ source: null,
+ },
+ ...data,
+ };
+ },
+ });
+ }
+
+ afterEach(() => {
+ wrapper.destroy();
+ });
+
+ it('sets loading state', () => {
+ createComponent(true);
+
+ expect(wrapper.element).toMatchSnapshot();
+ });
+
+ it('renders design index', () => {
+ createComponent(false, { design });
+
+ expect(wrapper.element).toMatchSnapshot();
+ expect(wrapper.find(GlAlert).exists()).toBe(false);
+ });
+
+ it('renders participants', () => {
+ createComponent(false, { design });
+
+ expect(findParticipants().exists()).toBe(true);
+ });
+
+ it('passes the correct amount of participants to the Participants component', () => {
+ createComponent(false, { design });
+
+ expect(findParticipants().props('participants')).toHaveLength(1);
+ });
+
+ describe('when has no discussions', () => {
+ beforeEach(() => {
+ createComponent(false, {
+ design: {
+ ...design,
+ discussions: {
+ nodes: [],
+ },
+ },
+ });
+ });
+
+ it('does not render discussions', () => {
+ expect(findDiscussions().exists()).toBe(false);
+ });
+
+ it('renders a message about possibility to create a new discussion', () => {
+ expect(wrapper.find('.new-discussion-disclaimer').exists()).toBe(true);
+ });
+ });
+
+ describe('when has discussions', () => {
+ beforeEach(() => {
+ createComponent(false, { design });
+ });
+
+ it('renders correct amount of discussions', () => {
+ expect(findDiscussions()).toHaveLength(1);
+ });
+
+ it('sends a mutation to set an active discussion when clicking on a discussion', () => {
+ findDiscussions()
+ .at(0)
+ .trigger('click');
+
+ expect(mutate).toHaveBeenCalledWith(updateActiveDiscussionMutationVariables);
+ });
+
+ it('sends a mutation to reset an active discussion when clicking outside of discussion', () => {
+ findDiscussionsWrapper().trigger('click');
+
+ expect(mutate).toHaveBeenCalledWith({
+ ...updateActiveDiscussionMutationVariables,
+ variables: { id: undefined, source: 'discussion' },
+ });
+ });
+ });
+
+ it('opens a new discussion form', () => {
+ createComponent(false, {
+ design: {
+ ...design,
+ discussions: {
+ nodes: [],
+ },
+ },
+ });
+
+ wrapper.vm.openCommentForm({ x: 0, y: 0 });
+
+ return wrapper.vm.$nextTick().then(() => {
+ expect(findDiscussionForm().exists()).toBe(true);
+ });
+ });
+
+ it('sends a mutation on submitting form and closes form', () => {
+ createComponent(false, {
+ design: {
+ ...design,
+ discussions: {
+ nodes: [],
+ },
+ },
+ annotationCoordinates,
+ comment: newComment,
+ });
+
+ findDiscussionForm().vm.$emit('submitForm');
+ expect(mutate).toHaveBeenCalledWith(createDiscussionMutationVariables);
+
+ return wrapper.vm
+ .$nextTick()
+ .then(() => {
+ return mutate({ variables: createDiscussionMutationVariables });
+ })
+ .then(() => {
+ expect(findDiscussionForm().exists()).toBe(false);
+ });
+ });
+
+ it('closes the form and clears the comment on canceling form', () => {
+ createComponent(false, {
+ design: {
+ ...design,
+ discussions: {
+ nodes: [],
+ },
+ },
+ annotationCoordinates,
+ comment: newComment,
+ });
+
+ findDiscussionForm().vm.$emit('cancelForm');
+
+ expect(wrapper.vm.comment).toBe('');
+
+ return wrapper.vm.$nextTick().then(() => {
+ expect(findDiscussionForm().exists()).toBe(false);
+ });
+ });
+
+ describe('with error', () => {
+ beforeEach(() => {
+ createComponent(false, {
+ design: {
+ ...design,
+ discussions: {
+ nodes: [],
+ },
+ },
+ errorMessage: 'woops',
+ });
+ });
+
+ it('GlAlert is rendered in correct position with correct content', () => {
+ expect(wrapper.element).toMatchSnapshot();
+ });
+ });
+
+ describe('onDesignQueryResult', () => {
+ describe('with no designs', () => {
+ it('redirects to /designs', () => {
+ createComponent(true);
+
+ wrapper.vm.onDesignQueryResult({ data: mockResponseNoDesigns, loading: false });
+ return wrapper.vm.$nextTick().then(() => {
+ expect(createFlash).toHaveBeenCalledTimes(1);
+ expect(createFlash).toHaveBeenCalledWith(DESIGN_NOT_FOUND_ERROR);
+ expect(routerPush).toHaveBeenCalledTimes(1);
+ expect(routerPush).toHaveBeenCalledWith({ name: DESIGNS_ROUTE_NAME });
+ });
+ });
+ });
+
+ describe('when no design exists for given version', () => {
+ it('redirects to /designs', () => {
+ // attempt to query for a version of the design that doesn't exist
+ createComponent(true, {}, { routeQuery: { version: '999' } });
+ wrapper.setData({
+ allVersions: mockAllVersions,
+ });
+
+ wrapper.vm.onDesignQueryResult({ data: mockResponseWithDesigns, loading: false });
+ return wrapper.vm.$nextTick().then(() => {
+ expect(createFlash).toHaveBeenCalledTimes(1);
+ expect(createFlash).toHaveBeenCalledWith(DESIGN_VERSION_NOT_EXIST_ERROR);
+ expect(routerPush).toHaveBeenCalledTimes(1);
+ expect(routerPush).toHaveBeenCalledWith({ name: DESIGNS_ROUTE_NAME });
+ });
+ });
+ });
+ });
+});
diff --git a/spec/frontend/design_management/pages/index_spec.js b/spec/frontend/design_management/pages/index_spec.js
new file mode 100644
index 00000000000..2299b858da9
--- /dev/null
+++ b/spec/frontend/design_management/pages/index_spec.js
@@ -0,0 +1,533 @@
+import { createLocalVue, shallowMount } from '@vue/test-utils';
+import { ApolloMutation } from 'vue-apollo';
+import VueRouter from 'vue-router';
+import { GlEmptyState } from '@gitlab/ui';
+
+import Index from '~/design_management/pages/index.vue';
+import uploadDesignQuery from '~/design_management/graphql/mutations/uploadDesign.mutation.graphql';
+import DesignDestroyer from '~/design_management/components/design_destroyer.vue';
+import DesignDropzone from '~/design_management/components/upload/design_dropzone.vue';
+import DeleteButton from '~/design_management/components/delete_button.vue';
+import { DESIGNS_ROUTE_NAME } from '~/design_management/router/constants';
+import {
+ EXISTING_DESIGN_DROP_MANY_FILES_MESSAGE,
+ EXISTING_DESIGN_DROP_INVALID_FILENAME_MESSAGE,
+} from '~/design_management/utils/error_messages';
+import createFlash from '~/flash';
+
+const localVue = createLocalVue();
+localVue.use(VueRouter);
+const router = new VueRouter({
+ routes: [
+ {
+ name: DESIGNS_ROUTE_NAME,
+ path: '/designs',
+ component: Index,
+ },
+ ],
+});
+
+jest.mock('~/flash.js');
+
+const mockDesigns = [
+ {
+ id: 'design-1',
+ image: 'design-1-image',
+ filename: 'design-1-name',
+ event: 'NONE',
+ notesCount: 0,
+ },
+ {
+ id: 'design-2',
+ image: 'design-2-image',
+ filename: 'design-2-name',
+ event: 'NONE',
+ notesCount: 1,
+ },
+ {
+ id: 'design-3',
+ image: 'design-3-image',
+ filename: 'design-3-name',
+ event: 'NONE',
+ notesCount: 0,
+ },
+];
+
+const mockVersion = {
+ node: {
+ id: 'gid://gitlab/DesignManagement::Version/1',
+ },
+};
+
+describe('Design management index page', () => {
+ let mutate;
+ let wrapper;
+
+ const findDesignCheckboxes = () => wrapper.findAll('.design-checkbox');
+ const findSelectAllButton = () => wrapper.find('.js-select-all');
+ const findToolbar = () => wrapper.find('.qa-selector-toolbar');
+ const findDeleteButton = () => wrapper.find(DeleteButton);
+ const findDropzone = () => wrapper.findAll(DesignDropzone).at(0);
+ const findFirstDropzoneWithDesign = () => wrapper.findAll(DesignDropzone).at(1);
+
+ function createComponent({
+ loading = false,
+ designs = [],
+ allVersions = [],
+ createDesign = true,
+ stubs = {},
+ mockMutate = jest.fn().mockResolvedValue(),
+ } = {}) {
+ mutate = mockMutate;
+ const $apollo = {
+ queries: {
+ designs: {
+ loading,
+ },
+ permissions: {
+ loading,
+ },
+ },
+ mutate,
+ };
+
+ wrapper = shallowMount(Index, {
+ mocks: { $apollo },
+ localVue,
+ router,
+ stubs: { DesignDestroyer, ApolloMutation, ...stubs },
+ attachToDocument: true,
+ });
+
+ wrapper.setData({
+ designs,
+ allVersions,
+ issueIid: '1',
+ permissions: {
+ createDesign,
+ },
+ });
+ }
+
+ afterEach(() => {
+ wrapper.destroy();
+ });
+
+ describe('designs', () => {
+ it('renders loading icon', () => {
+ createComponent({ loading: true });
+
+ return wrapper.vm.$nextTick().then(() => {
+ expect(wrapper.element).toMatchSnapshot();
+ });
+ });
+
+ it('renders error', () => {
+ createComponent();
+
+ wrapper.setData({ error: true });
+
+ return wrapper.vm.$nextTick().then(() => {
+ expect(wrapper.element).toMatchSnapshot();
+ });
+ });
+
+ it('renders a toolbar with buttons when there are designs', () => {
+ createComponent({ designs: mockDesigns, allVersions: [mockVersion] });
+
+ return wrapper.vm.$nextTick().then(() => {
+ expect(findToolbar().exists()).toBe(true);
+ });
+ });
+
+ it('renders designs list and header with upload button', () => {
+ createComponent({ designs: mockDesigns, allVersions: [mockVersion] });
+
+ return wrapper.vm.$nextTick().then(() => {
+ expect(wrapper.element).toMatchSnapshot();
+ });
+ });
+
+ it('does not render toolbar when there is no permission', () => {
+ createComponent({ designs: mockDesigns, allVersions: [mockVersion], createDesign: false });
+
+ return wrapper.vm.$nextTick().then(() => {
+ expect(wrapper.element).toMatchSnapshot();
+ });
+ });
+ });
+
+ describe('when has no designs', () => {
+ beforeEach(() => {
+ createComponent();
+ });
+
+ it('renders empty text', () =>
+ wrapper.vm.$nextTick().then(() => {
+ expect(wrapper.element).toMatchSnapshot();
+ }));
+
+ it('does not render a toolbar with buttons', () =>
+ wrapper.vm.$nextTick().then(() => {
+ expect(findToolbar().exists()).toBe(false);
+ }));
+ });
+
+ describe('uploading designs', () => {
+ it('calls mutation on upload', () => {
+ createComponent({ stubs: { GlEmptyState } });
+
+ const mutationVariables = {
+ update: expect.anything(),
+ context: {
+ hasUpload: true,
+ },
+ mutation: uploadDesignQuery,
+ variables: {
+ files: [{ name: 'test' }],
+ projectPath: '',
+ iid: '1',
+ },
+ optimisticResponse: {
+ __typename: 'Mutation',
+ designManagementUpload: {
+ __typename: 'DesignManagementUploadPayload',
+ designs: [
+ {
+ __typename: 'Design',
+ id: expect.anything(),
+ image: '',
+ imageV432x230: '',
+ filename: 'test',
+ fullPath: '',
+ event: 'NONE',
+ notesCount: 0,
+ diffRefs: {
+ __typename: 'DiffRefs',
+ baseSha: '',
+ startSha: '',
+ headSha: '',
+ },
+ discussions: {
+ __typename: 'DesignDiscussion',
+ nodes: [],
+ },
+ versions: {
+ __typename: 'DesignVersionConnection',
+ edges: {
+ __typename: 'DesignVersionEdge',
+ node: {
+ __typename: 'DesignVersion',
+ id: expect.anything(),
+ sha: expect.anything(),
+ },
+ },
+ },
+ },
+ ],
+ skippedDesigns: [],
+ errors: [],
+ },
+ },
+ };
+
+ return wrapper.vm.$nextTick().then(() => {
+ findDropzone().vm.$emit('change', [{ name: 'test' }]);
+ expect(mutate).toHaveBeenCalledWith(mutationVariables);
+ expect(wrapper.vm.filesToBeSaved).toEqual([{ name: 'test' }]);
+ expect(wrapper.vm.isSaving).toBeTruthy();
+ });
+ });
+
+ it('sets isSaving', () => {
+ createComponent();
+
+ const uploadDesign = wrapper.vm.onUploadDesign([
+ {
+ name: 'test',
+ },
+ ]);
+
+ expect(wrapper.vm.isSaving).toBe(true);
+
+ return uploadDesign.then(() => {
+ expect(wrapper.vm.isSaving).toBe(false);
+ });
+ });
+
+ it('updates state appropriately after upload complete', () => {
+ createComponent({ stubs: { GlEmptyState } });
+ wrapper.setData({ filesToBeSaved: [{ name: 'test' }] });
+
+ wrapper.vm.onUploadDesignDone();
+ return wrapper.vm.$nextTick().then(() => {
+ expect(wrapper.vm.filesToBeSaved).toEqual([]);
+ expect(wrapper.vm.isSaving).toBeFalsy();
+ expect(wrapper.vm.isLatestVersion).toBe(true);
+ });
+ });
+
+ it('updates state appropriately after upload error', () => {
+ createComponent({ stubs: { GlEmptyState } });
+ wrapper.setData({ filesToBeSaved: [{ name: 'test' }] });
+
+ wrapper.vm.onUploadDesignError();
+ return wrapper.vm.$nextTick().then(() => {
+ expect(wrapper.vm.filesToBeSaved).toEqual([]);
+ expect(wrapper.vm.isSaving).toBeFalsy();
+ expect(createFlash).toHaveBeenCalled();
+
+ createFlash.mockReset();
+ });
+ });
+
+ it('does not call mutation if createDesign is false', () => {
+ createComponent({ createDesign: false });
+
+ wrapper.vm.onUploadDesign([]);
+
+ expect(mutate).not.toHaveBeenCalled();
+ });
+
+ describe('upload count limit', () => {
+ const MAXIMUM_FILE_UPLOAD_LIMIT = 10;
+
+ afterEach(() => {
+ createFlash.mockReset();
+ });
+
+ it('does not warn when the max files are uploaded', () => {
+ createComponent();
+
+ wrapper.vm.onUploadDesign(new Array(MAXIMUM_FILE_UPLOAD_LIMIT).fill(mockDesigns[0]));
+
+ expect(createFlash).not.toHaveBeenCalled();
+ });
+
+ it('warns when too many files are uploaded', () => {
+ createComponent();
+
+ wrapper.vm.onUploadDesign(new Array(MAXIMUM_FILE_UPLOAD_LIMIT + 1).fill(mockDesigns[0]));
+
+ expect(createFlash).toHaveBeenCalled();
+ });
+ });
+
+ it('flashes warning if designs are skipped', () => {
+ createComponent({
+ mockMutate: () =>
+ Promise.resolve({
+ data: { designManagementUpload: { skippedDesigns: [{ filename: 'test.jpg' }] } },
+ }),
+ });
+
+ const uploadDesign = wrapper.vm.onUploadDesign([
+ {
+ name: 'test',
+ },
+ ]);
+
+ return uploadDesign.then(() => {
+ expect(createFlash).toHaveBeenCalledTimes(1);
+ expect(createFlash).toHaveBeenCalledWith(
+ 'Upload skipped. test.jpg did not change.',
+ 'warning',
+ );
+ });
+ });
+
+ describe('dragging onto an existing design', () => {
+ beforeEach(() => {
+ createComponent({ designs: mockDesigns, allVersions: [mockVersion] });
+ });
+
+ it('calls onUploadDesign with valid upload', () => {
+ wrapper.setMethods({
+ onUploadDesign: jest.fn(),
+ });
+
+ const mockUploadPayload = [
+ {
+ name: mockDesigns[0].filename,
+ },
+ ];
+
+ const designDropzone = findFirstDropzoneWithDesign();
+ designDropzone.vm.$emit('change', mockUploadPayload);
+
+ expect(wrapper.vm.onUploadDesign).toHaveBeenCalledTimes(1);
+ expect(wrapper.vm.onUploadDesign).toHaveBeenCalledWith(mockUploadPayload);
+ });
+
+ it.each`
+ description | eventPayload | message
+ ${'> 1 file'} | ${[{ name: 'test' }, { name: 'test-2' }]} | ${EXISTING_DESIGN_DROP_MANY_FILES_MESSAGE}
+ ${'different filename'} | ${[{ name: 'wrong-name' }]} | ${EXISTING_DESIGN_DROP_INVALID_FILENAME_MESSAGE}
+ `('calls createFlash when upload has $description', ({ eventPayload, message }) => {
+ const designDropzone = findFirstDropzoneWithDesign();
+ designDropzone.vm.$emit('change', eventPayload);
+
+ expect(createFlash).toHaveBeenCalledTimes(1);
+ expect(createFlash).toHaveBeenCalledWith(message);
+ });
+ });
+ });
+
+ describe('on latest version when has designs', () => {
+ beforeEach(() => {
+ createComponent({ designs: mockDesigns, allVersions: [mockVersion] });
+ });
+
+ it('renders design checkboxes', () => {
+ expect(findDesignCheckboxes()).toHaveLength(mockDesigns.length);
+ });
+
+ it('renders toolbar buttons', () => {
+ expect(findToolbar().exists()).toBe(true);
+ expect(findToolbar().classes()).toContain('d-flex');
+ expect(findToolbar().classes()).not.toContain('d-none');
+ });
+
+ it('adds two designs to selected designs when their checkboxes are checked', () => {
+ findDesignCheckboxes()
+ .at(0)
+ .trigger('click');
+
+ return wrapper.vm
+ .$nextTick()
+ .then(() => {
+ findDesignCheckboxes()
+ .at(1)
+ .trigger('click');
+
+ return wrapper.vm.$nextTick();
+ })
+ .then(() => {
+ expect(findDeleteButton().exists()).toBe(true);
+ expect(findSelectAllButton().text()).toBe('Deselect all');
+ findDeleteButton().vm.$emit('deleteSelectedDesigns');
+ const [{ variables }] = mutate.mock.calls[0];
+ expect(variables.filenames).toStrictEqual([
+ mockDesigns[0].filename,
+ mockDesigns[1].filename,
+ ]);
+ });
+ });
+
+ it('adds all designs to selected designs when Select All button is clicked', () => {
+ findSelectAllButton().vm.$emit('click');
+
+ return wrapper.vm.$nextTick().then(() => {
+ expect(findDeleteButton().props().hasSelectedDesigns).toBe(true);
+ expect(findSelectAllButton().text()).toBe('Deselect all');
+ expect(wrapper.vm.selectedDesigns).toEqual(mockDesigns.map(design => design.filename));
+ });
+ });
+
+ it('removes all designs from selected designs when at least one design was selected', () => {
+ findDesignCheckboxes()
+ .at(0)
+ .trigger('click');
+
+ return wrapper.vm
+ .$nextTick()
+ .then(() => {
+ findSelectAllButton().vm.$emit('click');
+ })
+ .then(() => {
+ expect(findDeleteButton().props().hasSelectedDesigns).toBe(false);
+ expect(findSelectAllButton().text()).toBe('Select all');
+ expect(wrapper.vm.selectedDesigns).toEqual([]);
+ });
+ });
+ });
+
+ it('on latest version when has no designs does not render toolbar buttons', () => {
+ createComponent({ designs: [], allVersions: [mockVersion] });
+ expect(findToolbar().exists()).toBe(false);
+ });
+
+ describe('on non-latest version', () => {
+ beforeEach(() => {
+ createComponent({ designs: mockDesigns, allVersions: [mockVersion] });
+
+ router.replace({
+ name: DESIGNS_ROUTE_NAME,
+ query: {
+ version: '2',
+ },
+ });
+ });
+
+ it('does not render design checkboxes', () => {
+ expect(findDesignCheckboxes()).toHaveLength(0);
+ });
+
+ it('does not render Delete selected button', () => {
+ expect(findDeleteButton().exists()).toBe(false);
+ });
+
+ it('does not render Select All button', () => {
+ expect(findSelectAllButton().exists()).toBe(false);
+ });
+ });
+
+ describe('pasting a design', () => {
+ let event;
+ beforeEach(() => {
+ createComponent({ designs: mockDesigns, allVersions: [mockVersion] });
+
+ wrapper.setMethods({
+ onUploadDesign: jest.fn(),
+ });
+
+ event = new Event('paste');
+
+ router.replace({
+ name: DESIGNS_ROUTE_NAME,
+ query: {
+ version: '2',
+ },
+ });
+ });
+
+ it('calls onUploadDesign with valid paste', () => {
+ event.clipboardData = {
+ files: [{ name: 'image.png', type: 'image/png' }],
+ getData: () => 'test.png',
+ };
+
+ document.dispatchEvent(event);
+
+ expect(wrapper.vm.onUploadDesign).toHaveBeenCalledTimes(1);
+ expect(wrapper.vm.onUploadDesign).toHaveBeenCalledWith([
+ new File([{ name: 'image.png' }], 'test.png'),
+ ]);
+ });
+
+ it('renames a design if it has an image.png filename', () => {
+ event.clipboardData = {
+ files: [{ name: 'image.png', type: 'image/png' }],
+ getData: () => 'image.png',
+ };
+
+ document.dispatchEvent(event);
+
+ expect(wrapper.vm.onUploadDesign).toHaveBeenCalledTimes(1);
+ expect(wrapper.vm.onUploadDesign).toHaveBeenCalledWith([
+ new File([{ name: 'image.png' }], `design_${Date.now()}.png`),
+ ]);
+ });
+
+ it('does not call onUploadDesign with invalid paste', () => {
+ event.clipboardData = {
+ items: [{ type: 'text/plain' }, { type: 'text' }],
+ files: [],
+ };
+
+ document.dispatchEvent(event);
+
+ expect(wrapper.vm.onUploadDesign).not.toHaveBeenCalled();
+ });
+ });
+});
diff --git a/spec/frontend/design_management/router_spec.js b/spec/frontend/design_management/router_spec.js
new file mode 100644
index 00000000000..0f4afa5e288
--- /dev/null
+++ b/spec/frontend/design_management/router_spec.js
@@ -0,0 +1,81 @@
+import { mount, createLocalVue } from '@vue/test-utils';
+import { nextTick } from 'vue';
+import VueRouter from 'vue-router';
+import App from '~/design_management/components/app.vue';
+import Designs from '~/design_management/pages/index.vue';
+import DesignDetail from '~/design_management/pages/design/index.vue';
+import createRouter from '~/design_management/router';
+import {
+ ROOT_ROUTE_NAME,
+ DESIGNS_ROUTE_NAME,
+ DESIGN_ROUTE_NAME,
+} from '~/design_management/router/constants';
+import '~/commons/bootstrap';
+
+function factory(routeArg) {
+ const localVue = createLocalVue();
+ localVue.use(VueRouter);
+
+ window.gon = { sprite_icons: '' };
+
+ const router = createRouter('/');
+ if (routeArg !== undefined) {
+ router.push(routeArg);
+ }
+
+ return mount(App, {
+ localVue,
+ router,
+ mocks: {
+ $apollo: {
+ queries: {
+ designs: { loading: true },
+ design: { loading: true },
+ permissions: { loading: true },
+ },
+ },
+ },
+ });
+}
+
+jest.mock('mousetrap', () => ({
+ bind: jest.fn(),
+ unbind: jest.fn(),
+}));
+
+describe('Design management router', () => {
+ afterEach(() => {
+ window.location.hash = '';
+ });
+
+ describe.each([['/'], [{ name: ROOT_ROUTE_NAME }]])('root route', routeArg => {
+ it('pushes home component', () => {
+ const wrapper = factory(routeArg);
+
+ expect(wrapper.find(Designs).exists()).toBe(true);
+ });
+ });
+
+ describe.each([['/designs'], [{ name: DESIGNS_ROUTE_NAME }]])('designs route', routeArg => {
+ it('pushes designs root component', () => {
+ const wrapper = factory(routeArg);
+
+ expect(wrapper.find(Designs).exists()).toBe(true);
+ });
+ });
+
+ describe.each([['/designs/1'], [{ name: DESIGN_ROUTE_NAME, params: { id: '1' } }]])(
+ 'designs detail route',
+ routeArg => {
+ it('pushes designs detail component', () => {
+ const wrapper = factory(routeArg);
+
+ return nextTick().then(() => {
+ const detail = wrapper.find(DesignDetail);
+ expect(detail.exists()).toBe(true);
+ expect(detail.props('id')).toEqual('1');
+ });
+ });
+ },
+ );
+});
diff --git a/spec/frontend/design_management/utils/cache_update_spec.js b/spec/frontend/design_management/utils/cache_update_spec.js
new file mode 100644
index 00000000000..641d35ff9ff
--- /dev/null
+++ b/spec/frontend/design_management/utils/cache_update_spec.js
@@ -0,0 +1,44 @@
+import { InMemoryCache } from 'apollo-cache-inmemory';
+import {
+ updateStoreAfterDesignsDelete,
+ updateStoreAfterAddDiscussionComment,
+ updateStoreAfterAddImageDiffNote,
+ updateStoreAfterUploadDesign,
+ updateStoreAfterUpdateImageDiffNote,
+} from '~/design_management/utils/cache_update';
+import {
+ designDeletionError,
+ ADD_DISCUSSION_COMMENT_ERROR,
+ ADD_IMAGE_DIFF_NOTE_ERROR,
+ UPDATE_IMAGE_DIFF_NOTE_ERROR,
+} from '~/design_management/utils/error_messages';
+import design from '../mock_data/design';
+import createFlash from '~/flash';
+
+jest.mock('~/flash.js');
+
+describe('Design Management cache update', () => {
+ const mockErrors = ['code red!'];
+
+ let mockStore;
+
+ beforeEach(() => {
+ mockStore = new InMemoryCache();
+ });
+
+ describe('error handling', () => {
+ it.each`
+ fnName | subject | errorMessage | extraArgs
+ ${'updateStoreAfterDesignsDelete'} | ${updateStoreAfterDesignsDelete} | ${designDeletionError({ singular: true })} | ${[[design]]}
+ ${'updateStoreAfterAddDiscussionComment'} | ${updateStoreAfterAddDiscussionComment} | ${ADD_DISCUSSION_COMMENT_ERROR} | ${[]}
+ ${'updateStoreAfterAddImageDiffNote'} | ${updateStoreAfterAddImageDiffNote} | ${ADD_IMAGE_DIFF_NOTE_ERROR} | ${[]}
+ ${'updateStoreAfterUploadDesign'} | ${updateStoreAfterUploadDesign} | ${mockErrors[0]} | ${[]}
+ ${'updateStoreAfterUpdateImageDiffNote'} | ${updateStoreAfterUpdateImageDiffNote} | ${UPDATE_IMAGE_DIFF_NOTE_ERROR} | ${[]}
+ `('$fnName handles errors in response', ({ subject, extraArgs, errorMessage }) => {
+ expect(createFlash).not.toHaveBeenCalled();
+ expect(() => subject(mockStore, { errors: mockErrors }, {}, ...extraArgs)).toThrow();
+ expect(createFlash).toHaveBeenCalledTimes(1);
+ expect(createFlash).toHaveBeenCalledWith(errorMessage);
+ });
+ });
+});
diff --git a/spec/frontend/design_management/utils/design_management_utils_spec.js b/spec/frontend/design_management/utils/design_management_utils_spec.js
new file mode 100644
index 00000000000..af631073df6
--- /dev/null
+++ b/spec/frontend/design_management/utils/design_management_utils_spec.js
@@ -0,0 +1,176 @@
+import {
+ extractCurrentDiscussion,
+ extractDiscussions,
+ findVersionId,
+ designUploadOptimisticResponse,
+ updateImageDiffNoteOptimisticResponse,
+ isValidDesignFile,
+ extractDesign,
+} from '~/design_management/utils/design_management_utils';
+import mockResponseNoDesigns from '../mock_data/no_designs';
+import mockResponseWithDesigns from '../mock_data/designs';
+import mockDesign from '../mock_data/design';
+
+jest.mock('lodash/uniqueId', () => () => 1);
+
+describe('extractCurrentDiscussion', () => {
+ let discussions;
+
+ beforeEach(() => {
+ discussions = {
+ nodes: [
+ { id: 101, payload: 'w' },
+ { id: 102, payload: 'x' },
+ { id: 103, payload: 'y' },
+ { id: 104, payload: 'z' },
+ ],
+ };
+ });
+
+ it('finds the relevant discussion if it exists', () => {
+ const id = 103;
+ expect(extractCurrentDiscussion(discussions, id)).toEqual({ id, payload: 'y' });
+ });
+
+ it('returns null if the relevant discussion does not exist', () => {
+ expect(extractCurrentDiscussion(discussions, 0)).not.toBeDefined();
+ });
+});
+
+describe('extractDiscussions', () => {
+ let discussions;
+
+ beforeEach(() => {
+ discussions = {
+ nodes: [
+ { id: 1, notes: { nodes: ['a'] } },
+ { id: 2, notes: { nodes: ['b'] } },
+ { id: 3, notes: { nodes: ['c'] } },
+ { id: 4, notes: { nodes: ['d'] } },
+ ],
+ };
+ });
+
+ it('discards the edges.node artifacts of GraphQL', () => {
+ expect(extractDiscussions(discussions)).toEqual([
+ { id: 1, notes: ['a'] },
+ { id: 2, notes: ['b'] },
+ { id: 3, notes: ['c'] },
+ { id: 4, notes: ['d'] },
+ ]);
+ });
+});
+
+describe('version parser', () => {
+ it('correctly extracts version ID from a valid version string', () => {
+ const testVersionId = '123';
+ const testVersionString = `gid://gitlab/DesignManagement::Version/${testVersionId}`;
+
+ expect(findVersionId(testVersionString)).toEqual(testVersionId);
+ });
+
+ it('fails to extract version ID from an invalid version string', () => {
+ const testInvalidVersionString = `gid://gitlab/DesignManagement::Version`;
+
+ expect(findVersionId(testInvalidVersionString)).toBeUndefined();
+ });
+});
+
+describe('optimistic responses', () => {
+ it('correctly generated for designManagementUpload', () => {
+ const expectedResponse = {
+ __typename: 'Mutation',
+ designManagementUpload: {
+ __typename: 'DesignManagementUploadPayload',
+ designs: [
+ {
+ __typename: 'Design',
+ id: -1,
+ image: '',
+ imageV432x230: '',
+ filename: 'test',
+ fullPath: '',
+ notesCount: 0,
+ event: 'NONE',
+ diffRefs: { __typename: 'DiffRefs', baseSha: '', startSha: '', headSha: '' },
+ discussions: { __typename: 'DesignDiscussion', nodes: [] },
+ versions: {
+ __typename: 'DesignVersionConnection',
+ edges: {
+ __typename: 'DesignVersionEdge',
+ node: { __typename: 'DesignVersion', id: -1, sha: -1 },
+ },
+ },
+ },
+ ],
+ errors: [],
+ skippedDesigns: [],
+ },
+ };
+ expect(designUploadOptimisticResponse([{ name: 'test' }])).toEqual(expectedResponse);
+ });
+
+ it('correctly generated for updateImageDiffNoteOptimisticResponse', () => {
+ const mockNote = {
+ id: 'test-note-id',
+ };
+
+ const mockPosition = {
+ x: 10,
+ y: 10,
+ width: 10,
+ height: 10,
+ };
+
+ const expectedResponse = {
+ __typename: 'Mutation',
+ updateImageDiffNote: {
+ __typename: 'UpdateImageDiffNotePayload',
+ note: {
+ ...mockNote,
+ position: mockPosition,
+ },
+ errors: [],
+ },
+ };
+ expect(updateImageDiffNoteOptimisticResponse(mockNote, { position: mockPosition })).toEqual(
+ expectedResponse,
+ );
+ });
+});
+
+describe('isValidDesignFile', () => {
+ // test every filetype that Design Management supports
+ // https://docs.gitlab.com/ee/user/project/issues/design_management.html#limitations
+ it.each`
+ mimetype | isValid
+ ${'image/svg'} | ${true}
+ ${'image/png'} | ${true}
+ ${'image/jpg'} | ${true}
+ ${'image/jpeg'} | ${true}
+ ${'image/gif'} | ${true}
+ ${'image/bmp'} | ${true}
+ ${'image/tiff'} | ${true}
+ ${'image/ico'} | ${true}
+ ${'image/svg'} | ${true}
+ ${'video/mpeg'} | ${false}
+ ${'audio/midi'} | ${false}
+ ${'application/octet-stream'} | ${false}
+ `('returns $isValid for file type $mimetype', ({ mimetype, isValid }) => {
+ expect(isValidDesignFile({ type: mimetype })).toBe(isValid);
+ });
+});
+
+describe('extractDesign', () => {
+ describe('with no designs', () => {
+ it('returns undefined', () => {
+ expect(extractDesign(mockResponseNoDesigns)).toBeUndefined();
+ });
+ });
+
+ describe('with designs', () => {
+ it('returns the first design available', () => {
+ expect(extractDesign(mockResponseWithDesigns)).toEqual(mockDesign);
+ });
+ });
+});
diff --git a/spec/frontend/design_management/utils/error_messages_spec.js b/spec/frontend/design_management/utils/error_messages_spec.js
new file mode 100644
index 00000000000..635ff931d7d
--- /dev/null
+++ b/spec/frontend/design_management/utils/error_messages_spec.js
@@ -0,0 +1,62 @@
+import {
+ designDeletionError,
+ designUploadSkippedWarning,
+} from '~/design_management/utils/error_messages';
+
+const mockFilenames = n =>
+ Array(n)
+ .fill(0)
+ .map((_, i) => ({ filename: `${i + 1}.jpg` }));
+
+describe('Error message', () => {
+ describe('designDeletionError', () => {
+ const singularMsg = 'Could not delete a design. Please try again.';
+ const pluralMsg = 'Could not delete designs. Please try again.';
+
+ describe('when [singular=true]', () => {
+ it.each([[undefined], [true]])('uses singular grammar', singularOption => {
+ expect(designDeletionError({ singular: singularOption })).toEqual(singularMsg);
+ });
+ });
+
+ describe('when [singular=false]', () => {
+ it('uses plural grammar', () => {
+ expect(designDeletionError({ singular: false })).toEqual(pluralMsg);
+ });
+ });
+ });
+
+ describe.each([
+ [[], [], null],
+ [mockFilenames(1), mockFilenames(1), 'Upload skipped. 1.jpg did not change.'],
+ [
+ mockFilenames(2),
+ mockFilenames(2),
+ 'Upload skipped. The designs you tried uploading did not change.',
+ ],
+ [
+ mockFilenames(2),
+ mockFilenames(1),
+ 'Upload skipped. Some of the designs you tried uploading did not change: 1.jpg.',
+ ],
+ [
+ mockFilenames(6),
+ mockFilenames(5),
+ 'Upload skipped. Some of the designs you tried uploading did not change: 1.jpg, 2.jpg, 3.jpg, 4.jpg, 5.jpg.',
+ ],
+ [
+ mockFilenames(7),
+ mockFilenames(6),
+ 'Upload skipped. Some of the designs you tried uploading did not change: 1.jpg, 2.jpg, 3.jpg, 4.jpg, 5.jpg, and 1 more.',
+ ],
+ [
+ mockFilenames(8),
+ mockFilenames(7),
+ 'Upload skipped. Some of the designs you tried uploading did not change: 1.jpg, 2.jpg, 3.jpg, 4.jpg, 5.jpg, and 2 more.',
+ ],
+ ])('designUploadSkippedWarning', (uploadedFiles, skippedFiles, expected) => {
+ test('returns expected warning message', () => {
+ expect(designUploadSkippedWarning(uploadedFiles, skippedFiles)).toBe(expected);
+ });
+ });
+});
diff --git a/spec/frontend/design_management/utils/tracking_spec.js b/spec/frontend/design_management/utils/tracking_spec.js
new file mode 100644
index 00000000000..9fa5eae55b3
--- /dev/null
+++ b/spec/frontend/design_management/utils/tracking_spec.js
@@ -0,0 +1,53 @@
+import { mockTracking } from 'helpers/tracking_helper';
+import { trackDesignDetailView } from '~/design_management/utils/tracking';
+
+function getTrackingSpy(key) {
+ return mockTracking(key, undefined, jest.spyOn);
+}
+
+describe('Tracking Events', () => {
+ describe('trackDesignDetailView', () => {
+ const eventKey = 'projects:issues:design';
+ const eventName = 'design_viewed';
+
+ it('trackDesignDetailView fires a tracking event when called', () => {
+ const trackingSpy = getTrackingSpy(eventKey);
+
+ trackDesignDetailView();
+
+ expect(trackingSpy).toHaveBeenCalledWith(
+ eventKey,
+ eventName,
+ expect.objectContaining({
+ label: eventName,
+ value: {
+ 'internal-object-refrerer': '',
+ 'design-collection-owner': '',
+ 'design-version-number': 1,
+ 'design-is-current-version': false,
+ },
+ }),
+ );
+ });
+
+ it('trackDesignDetailView allows to customize the value payload', () => {
+ const trackingSpy = getTrackingSpy(eventKey);
+
+ trackDesignDetailView('from-a-test', 'test', 100, true);
+
+ expect(trackingSpy).toHaveBeenCalledWith(
+ eventKey,
+ eventName,
+ expect.objectContaining({
+ label: eventName,
+ value: {
+ 'internal-object-refrerer': 'from-a-test',
+ 'design-collection-owner': 'test',
+ 'design-version-number': 100,
+ 'design-is-current-version': true,
+ },
+ }),
+ );
+ });
+ });
+});
diff --git a/spec/frontend/diff_comments_store_spec.js b/spec/frontend/diff_comments_store_spec.js
new file mode 100644
index 00000000000..6f25c9dd3bc
--- /dev/null
+++ b/spec/frontend/diff_comments_store_spec.js
@@ -0,0 +1,136 @@
+/* global CommentsStore */
+
+import '~/diff_notes/models/discussion';
+import '~/diff_notes/models/note';
+import '~/diff_notes/stores/comments';
+
+function createDiscussion(noteId = 1, resolved = true) {
+ CommentsStore.create({
+ discussionId: 'a',
+ noteId,
+ canResolve: true,
+ resolved,
+ resolvedBy: 'test',
+ authorName: 'test',
+ authorAvatar: 'test',
+ noteTruncated: 'test...',
+ });
+}
+
+beforeEach(() => {
+ CommentsStore.state = {};
+});
+
+describe('New discussion', () => {
+ it('creates new discussion', () => {
+ expect(Object.keys(CommentsStore.state).length).toBe(0);
+ createDiscussion();
+
+ expect(Object.keys(CommentsStore.state).length).toBe(1);
+ });
+
+ it('creates new note in discussion', () => {
+ createDiscussion();
+ createDiscussion(2);
+
+ const discussion = CommentsStore.state.a;
+
+ expect(Object.keys(discussion.notes).length).toBe(2);
+ });
+});
+
+describe('Get note', () => {
+ beforeEach(() => {
+ createDiscussion();
+ });
+
+ it('gets note by ID', () => {
+ const note = CommentsStore.get('a', 1);
+
+ expect(note).toBeDefined();
+ expect(note.id).toBe(1);
+ });
+});
+
+describe('Delete discussion', () => {
+ beforeEach(() => {
+ createDiscussion();
+ });
+
+ it('deletes discussion by ID', () => {
+ CommentsStore.delete('a', 1);
+
+ expect(Object.keys(CommentsStore.state).length).toBe(0);
+ });
+
+ it('deletes discussion when no more notes', () => {
+ createDiscussion();
+ createDiscussion(2);
+
+ expect(Object.keys(CommentsStore.state).length).toBe(1);
+ expect(Object.keys(CommentsStore.state.a.notes).length).toBe(2);
+
+ CommentsStore.delete('a', 1);
+ CommentsStore.delete('a', 2);
+
+ expect(Object.keys(CommentsStore.state).length).toBe(0);
+ });
+});
+
+describe('Update note', () => {
+ beforeEach(() => {
+ createDiscussion();
+ });
+
+ it('updates note to be unresolved', () => {
+ CommentsStore.update('a', 1, false, 'test');
+
+ const note = CommentsStore.get('a', 1);
+
+ expect(note.resolved).toBe(false);
+ });
+});
+
+describe('Discussion resolved', () => {
+ beforeEach(() => {
+ createDiscussion();
+ });
+
+ it('is resolved with single note', () => {
+ const discussion = CommentsStore.state.a;
+
+ expect(discussion.isResolved()).toBe(true);
+ });
+
+ it('is unresolved with 2 notes', () => {
+ const discussion = CommentsStore.state.a;
+ createDiscussion(2, false);
+
+ expect(discussion.isResolved()).toBe(false);
+ });
+
+ it('is resolved with 2 notes', () => {
+ const discussion = CommentsStore.state.a;
+ createDiscussion(2);
+
+ expect(discussion.isResolved()).toBe(true);
+ });
+
+ it('resolve all notes', () => {
+ const discussion = CommentsStore.state.a;
+ createDiscussion(2, false);
+
+ discussion.resolveAllNotes();
+
+ expect(discussion.isResolved()).toBe(true);
+ });
+
+ it('unresolve all notes', () => {
+ const discussion = CommentsStore.state.a;
+ createDiscussion(2);
+
+ discussion.unResolveAllNotes();
+
+ expect(discussion.isResolved()).toBe(false);
+ });
+});
diff --git a/spec/frontend/diffs/components/app_spec.js b/spec/frontend/diffs/components/app_spec.js
index 3a0354205f8..57e3a93c6f4 100644
--- a/spec/frontend/diffs/components/app_spec.js
+++ b/spec/frontend/diffs/components/app_spec.js
@@ -14,10 +14,13 @@ import TreeList from '~/diffs/components/tree_list.vue';
import { INLINE_DIFF_VIEW_TYPE, PARALLEL_DIFF_VIEW_TYPE } from '~/diffs/constants';
import createDiffsStore from '../create_diffs_store';
import axios from '~/lib/utils/axios_utils';
+import * as urlUtils from '~/lib/utils/url_utility';
import diffsMockData from '../mock_data/merge_request_diffs';
const mergeRequestDiff = { version_index: 1 };
const TEST_ENDPOINT = `${TEST_HOST}/diff/endpoint`;
+const COMMIT_URL = '[BASE URL]/OLD';
+const UPDATED_COMMIT_URL = '[BASE URL]/NEW';
describe('diffs/components/app', () => {
const oldMrTabs = window.mrTabs;
@@ -25,8 +28,14 @@ describe('diffs/components/app', () => {
let wrapper;
let mock;
- function createComponent(props = {}, extendStore = () => {}) {
+ function createComponent(props = {}, extendStore = () => {}, provisions = {}) {
const localVue = createLocalVue();
+ const provide = {
+ ...provisions,
+ glFeatures: {
+ ...(provisions.glFeatures || {}),
+ },
+ };
localVue.use(Vuex);
@@ -49,6 +58,7 @@ describe('diffs/components/app', () => {
showSuggestPopover: true,
...props,
},
+ provide,
store,
methods: {
isLatestVersion() {
@@ -79,7 +89,10 @@ describe('diffs/components/app', () => {
window.mrTabs = oldMrTabs;
// reset component
- wrapper.destroy();
+ if (wrapper) {
+ wrapper.destroy();
+ wrapper = null;
+ }
mock.restore();
});
@@ -452,76 +465,109 @@ describe('diffs/components/app', () => {
});
describe('keyboard shortcut navigation', () => {
- const mappings = {
- '[': -1,
- k: -1,
- ']': +1,
- j: +1,
- };
- let spy;
+ let spies = [];
+ let jumpSpy;
+ let moveSpy;
+
+ function setup(componentProps, featureFlags) {
+ createComponent(
+ componentProps,
+ ({ state }) => {
+ state.diffs.commit = { id: 'SHA123' };
+ },
+ { glFeatures: { mrCommitNeighborNav: true, ...featureFlags } },
+ );
+
+ moveSpy = jest.spyOn(wrapper.vm, 'moveToNeighboringCommit').mockImplementation(() => {});
+ jumpSpy = jest.fn();
+ spies = [jumpSpy, moveSpy];
+ wrapper.setMethods({
+ jumpToFile: jumpSpy,
+ });
+ }
describe('visible app', () => {
- beforeEach(() => {
- spy = jest.fn();
+ it.each`
+ key | name | spy | args | featureFlags
+ ${'['} | ${'jumpToFile'} | ${0} | ${[-1]} | ${{}}
+ ${'k'} | ${'jumpToFile'} | ${0} | ${[-1]} | ${{}}
+ ${']'} | ${'jumpToFile'} | ${0} | ${[+1]} | ${{}}
+ ${'j'} | ${'jumpToFile'} | ${0} | ${[+1]} | ${{}}
+ ${'x'} | ${'moveToNeighboringCommit'} | ${1} | ${[{ direction: 'previous' }]} | ${{ mrCommitNeighborNav: true }}
+ ${'c'} | ${'moveToNeighboringCommit'} | ${1} | ${[{ direction: 'next' }]} | ${{ mrCommitNeighborNav: true }}
+ `(
+ 'calls `$name()` with correct parameters whenever the "$key" key is pressed',
+ ({ key, spy, args, featureFlags }) => {
+ setup({ shouldShow: true }, featureFlags);
- createComponent({
- shouldShow: true,
- });
- wrapper.setMethods({
- jumpToFile: spy,
- });
- });
+ return wrapper.vm.$nextTick().then(() => {
+ expect(spies[spy]).not.toHaveBeenCalled();
+
+ Mousetrap.trigger(key);
+
+ expect(spies[spy]).toHaveBeenCalledWith(...args);
+ });
+ },
+ );
+
+ it.each`
+ key | name | spy | featureFlags
+ ${'x'} | ${'moveToNeighboringCommit'} | ${1} | ${{ mrCommitNeighborNav: false }}
+ ${'c'} | ${'moveToNeighboringCommit'} | ${1} | ${{ mrCommitNeighborNav: false }}
+ `(
+ 'does not call `$name()` even when the correct key is pressed if the feature flag is disabled',
+ ({ key, spy, featureFlags }) => {
+ setup({ shouldShow: true }, featureFlags);
- it.each(Object.keys(mappings))(
- 'calls `jumpToFile()` with correct parameter whenever pre-defined %s is pressed',
- key => {
return wrapper.vm.$nextTick().then(() => {
- expect(spy).not.toHaveBeenCalled();
+ expect(spies[spy]).not.toHaveBeenCalled();
Mousetrap.trigger(key);
- expect(spy).toHaveBeenCalledWith(mappings[key]);
+ expect(spies[spy]).not.toHaveBeenCalled();
});
},
);
- it('does not call `jumpToFile()` when unknown key is pressed', done => {
- wrapper.vm
- .$nextTick()
- .then(() => {
- Mousetrap.trigger('d');
+ it.each`
+ key | name | spy | allowed
+ ${'d'} | ${'jumpToFile'} | ${0} | ${['[', ']', 'j', 'k']}
+ ${'r'} | ${'moveToNeighboringCommit'} | ${1} | ${['x', 'c']}
+ `(
+ `does not call \`$name()\` when a key that is not one of \`$allowed\` is pressed`,
+ ({ key, spy }) => {
+ setup({ shouldShow: true }, { mrCommitNeighborNav: true });
- expect(spy).not.toHaveBeenCalled();
- })
- .then(done)
- .catch(done.fail);
- });
+ return wrapper.vm.$nextTick().then(() => {
+ Mousetrap.trigger(key);
+
+ expect(spies[spy]).not.toHaveBeenCalled();
+ });
+ },
+ );
});
- describe('hideen app', () => {
+ describe('hidden app', () => {
beforeEach(() => {
- spy = jest.fn();
+ setup({ shouldShow: false }, { mrCommitNeighborNav: true });
- createComponent({
- shouldShow: false,
- });
- wrapper.setMethods({
- jumpToFile: spy,
+ return wrapper.vm.$nextTick().then(() => {
+ Mousetrap.reset();
});
});
- it('stops calling `jumpToFile()` when application is hidden', done => {
- wrapper.vm
- .$nextTick()
- .then(() => {
- Object.keys(mappings).forEach(key => {
- Mousetrap.trigger(key);
+ it.each`
+ key | name | spy
+ ${'['} | ${'jumpToFile'} | ${0}
+ ${'k'} | ${'jumpToFile'} | ${0}
+ ${']'} | ${'jumpToFile'} | ${0}
+ ${'j'} | ${'jumpToFile'} | ${0}
+ ${'x'} | ${'moveToNeighboringCommit'} | ${1}
+ ${'c'} | ${'moveToNeighboringCommit'} | ${1}
+ `('stops calling `$name()` when the app is hidden', ({ key, spy }) => {
+ Mousetrap.trigger(key);
- expect(spy).not.toHaveBeenCalled();
- });
- })
- .then(done)
- .catch(done.fail);
+ expect(spies[spy]).not.toHaveBeenCalled();
});
});
});
@@ -602,6 +648,70 @@ describe('diffs/components/app', () => {
});
});
+ describe('commit watcher', () => {
+ const spy = () => {
+ jest.spyOn(wrapper.vm, 'refetchDiffData').mockImplementation(() => {});
+ jest.spyOn(wrapper.vm, 'adjustView').mockImplementation(() => {});
+ };
+ let location;
+
+ beforeAll(() => {
+ location = window.location;
+ delete window.location;
+ window.location = COMMIT_URL;
+ document.title = 'My Title';
+ });
+
+ beforeEach(() => {
+ jest.spyOn(urlUtils, 'updateHistory');
+ });
+
+ afterAll(() => {
+ window.location = location;
+ });
+
+ it('when the commit changes and the app is not loading it should update the history, refetch the diff data, and update the view', () => {
+ createComponent({}, ({ state }) => {
+ state.diffs.commit = { ...state.diffs.commit, id: 'OLD' };
+ });
+ spy();
+
+ store.state.diffs.commit = { id: 'NEW' };
+
+ return wrapper.vm.$nextTick().then(() => {
+ expect(urlUtils.updateHistory).toHaveBeenCalledWith({
+ title: document.title,
+ url: UPDATED_COMMIT_URL,
+ });
+ expect(wrapper.vm.refetchDiffData).toHaveBeenCalled();
+ expect(wrapper.vm.adjustView).toHaveBeenCalled();
+ });
+ });
+
+ it.each`
+ isLoading | oldSha | newSha
+ ${true} | ${'OLD'} | ${'NEW'}
+ ${false} | ${'NEW'} | ${'NEW'}
+ `(
+ 'given `{ "isLoading": $isLoading, "oldSha": "$oldSha", "newSha": "$newSha" }`, nothing should happen',
+ ({ isLoading, oldSha, newSha }) => {
+ createComponent({}, ({ state }) => {
+ state.diffs.isLoading = isLoading;
+ state.diffs.commit = { ...state.diffs.commit, id: oldSha };
+ });
+ spy();
+
+ store.state.diffs.commit = { id: newSha };
+
+ return wrapper.vm.$nextTick().then(() => {
+ expect(urlUtils.updateHistory).not.toHaveBeenCalled();
+ expect(wrapper.vm.refetchDiffData).not.toHaveBeenCalled();
+ expect(wrapper.vm.adjustView).not.toHaveBeenCalled();
+ });
+ },
+ );
+ });
+
describe('diffs', () => {
it('should render compare versions component', () => {
createComponent({}, ({ state }) => {
diff --git a/spec/frontend/diffs/components/commit_item_spec.js b/spec/frontend/diffs/components/commit_item_spec.js
index 6bb3a0dcf21..0df951d43a7 100644
--- a/spec/frontend/diffs/components/commit_item_spec.js
+++ b/spec/frontend/diffs/components/commit_item_spec.js
@@ -13,6 +13,8 @@ const TEST_AUTHOR_EMAIL = 'test+test@gitlab.com';
const TEST_AUTHOR_GRAVATAR = `${TEST_HOST}/avatar/test?s=40`;
const TEST_SIGNATURE_HTML = '<a>Legit commit</a>';
const TEST_PIPELINE_STATUS_PATH = `${TEST_HOST}/pipeline/status`;
+const NEXT_COMMIT_URL = `${TEST_HOST}/?commit_id=next`;
+const PREV_COMMIT_URL = `${TEST_HOST}/?commit_id=prev`;
describe('diffs/components/commit_item', () => {
let wrapper;
@@ -30,12 +32,24 @@ describe('diffs/components/commit_item', () => {
const getCommitActionsElement = () => wrapper.find('.commit-actions');
const getCommitPipelineStatus = () => wrapper.find(CommitPipelineStatus);
- const mountComponent = propsData => {
+ const getCommitNavButtonsElement = () => wrapper.find('.commit-nav-buttons');
+ const getNextCommitNavElement = () =>
+ getCommitNavButtonsElement().find('.btn-group > *:last-child');
+ const getPrevCommitNavElement = () =>
+ getCommitNavButtonsElement().find('.btn-group > *:first-child');
+
+ const mountComponent = (propsData, featureFlags = {}) => {
wrapper = mount(Component, {
propsData: {
commit,
...propsData,
},
+ provide: {
+ glFeatures: {
+ mrCommitNeighborNav: true,
+ ...featureFlags,
+ },
+ },
stubs: {
CommitPipelineStatus: true,
},
@@ -173,4 +187,132 @@ describe('diffs/components/commit_item', () => {
expect(getCommitPipelineStatus().exists()).toBe(true);
});
});
+
+ describe('without neighbor commits', () => {
+ beforeEach(() => {
+ mountComponent({ commit: { ...commit, prev_commit_id: null, next_commit_id: null } });
+ });
+
+ it('does not render any navigation buttons', () => {
+ expect(getCommitNavButtonsElement().exists()).toEqual(false);
+ });
+ });
+
+ describe('with neighbor commits', () => {
+ let mrCommit;
+
+ beforeEach(() => {
+ mrCommit = {
+ ...commit,
+ next_commit_id: 'next',
+ prev_commit_id: 'prev',
+ };
+
+ mountComponent({ commit: mrCommit });
+ });
+
+ it('renders the commit navigation buttons', () => {
+ expect(getCommitNavButtonsElement().exists()).toEqual(true);
+
+ mountComponent({
+ commit: { ...mrCommit, next_commit_id: null },
+ });
+ expect(getCommitNavButtonsElement().exists()).toEqual(true);
+
+ mountComponent({
+ commit: { ...mrCommit, prev_commit_id: null },
+ });
+ expect(getCommitNavButtonsElement().exists()).toEqual(true);
+ });
+
+ it('does not render the commit navigation buttons if the `mrCommitNeighborNav` feature flag is disabled', () => {
+ mountComponent({ commit: mrCommit }, { mrCommitNeighborNav: false });
+
+ expect(getCommitNavButtonsElement().exists()).toEqual(false);
+ });
+
+ describe('prev commit', () => {
+ const { location } = window;
+
+ beforeAll(() => {
+ delete window.location;
+ window.location = { href: `${TEST_HOST}?commit_id=${mrCommit.id}` };
+ });
+
+ beforeEach(() => {
+ jest.spyOn(wrapper.vm, 'moveToNeighboringCommit').mockImplementation(() => {});
+ });
+
+ afterAll(() => {
+ window.location = location;
+ });
+
+ it('uses the correct href', () => {
+ const link = getPrevCommitNavElement();
+
+ expect(link.element.getAttribute('href')).toEqual(PREV_COMMIT_URL);
+ });
+
+ it('triggers the correct Vuex action on click', () => {
+ const link = getPrevCommitNavElement();
+
+ link.trigger('click');
+ return wrapper.vm.$nextTick().then(() => {
+ expect(wrapper.vm.moveToNeighboringCommit).toHaveBeenCalledWith({
+ direction: 'previous',
+ });
+ });
+ });
+
+ it('renders a disabled button when there is no prev commit', () => {
+ mountComponent({ commit: { ...mrCommit, prev_commit_id: null } });
+
+ const button = getPrevCommitNavElement();
+
+ expect(button.element.tagName).toEqual('BUTTON');
+ expect(button.element.hasAttribute('disabled')).toEqual(true);
+ });
+ });
+
+ describe('next commit', () => {
+ const { location } = window;
+
+ beforeAll(() => {
+ delete window.location;
+ window.location = { href: `${TEST_HOST}?commit_id=${mrCommit.id}` };
+ });
+
+ beforeEach(() => {
+ jest.spyOn(wrapper.vm, 'moveToNeighboringCommit').mockImplementation(() => {});
+ });
+
+ afterAll(() => {
+ window.location = location;
+ });
+
+ it('uses the correct href', () => {
+ const link = getNextCommitNavElement();
+
+ expect(link.element.getAttribute('href')).toEqual(NEXT_COMMIT_URL);
+ });
+
+ it('triggers the correct Vuex action on click', () => {
+ const link = getNextCommitNavElement();
+
+ link.trigger('click');
+ return wrapper.vm.$nextTick().then(() => {
+ expect(wrapper.vm.moveToNeighboringCommit).toHaveBeenCalledWith({ direction: 'next' });
+ });
+ });
+
+ it('renders a disabled button when there is no next commit', () => {
+ mountComponent({ commit: { ...mrCommit, next_commit_id: null } });
+
+ const button = getNextCommitNavElement();
+
+ expect(button.element.tagName).toEqual('BUTTON');
+ expect(button.element.hasAttribute('disabled')).toEqual(true);
+ });
+ });
+ });
});
diff --git a/spec/frontend/diffs/components/diff_content_spec.js b/spec/frontend/diffs/components/diff_content_spec.js
index 979c67787f7..b78895f9e55 100644
--- a/spec/frontend/diffs/components/diff_content_spec.js
+++ b/spec/frontend/diffs/components/diff_content_spec.js
@@ -10,7 +10,7 @@ import ImageDiffOverlay from '~/diffs/components/image_diff_overlay.vue';
import NoteForm from '~/notes/components/note_form.vue';
import DiffDiscussions from '~/diffs/components/diff_discussions.vue';
import { IMAGE_DIFF_POSITION_TYPE } from '~/diffs/constants';
-import diffFileMockData from '../../../javascripts/diffs/mock_data/diff_file';
+import diffFileMockData from '../mock_data/diff_file';
import { diffViewerModes } from '~/ide/constants';
const localVue = createLocalVue();
diff --git a/spec/frontend/diffs/components/diff_discussions_spec.js b/spec/frontend/diffs/components/diff_discussions_spec.js
index ba5a4f96204..83becc7a20a 100644
--- a/spec/frontend/diffs/components/diff_discussions_spec.js
+++ b/spec/frontend/diffs/components/diff_discussions_spec.js
@@ -13,7 +13,7 @@ const localVue = createLocalVue();
describe('DiffDiscussions', () => {
let store;
let wrapper;
- const getDiscussionsMockData = () => [Object.assign({}, discussionsMockData)];
+ const getDiscussionsMockData = () => [{ ...discussionsMockData }];
const createComponent = props => {
store = createStore();
diff --git a/spec/frontend/diffs/components/diff_expansion_cell_spec.js b/spec/frontend/diffs/components/diff_expansion_cell_spec.js
index 31c6a4d5b60..0504f3933e0 100644
--- a/spec/frontend/diffs/components/diff_expansion_cell_spec.js
+++ b/spec/frontend/diffs/components/diff_expansion_cell_spec.js
@@ -81,7 +81,7 @@ describe('DiffExpansionCell', () => {
isTop: false,
isBottom: false,
};
- const props = Object.assign({}, defaults, options);
+ const props = { ...defaults, ...options };
vm = createComponentWithStore(cmp, store, props).$mount();
};
diff --git a/spec/frontend/diffs/components/diff_gutter_avatars_spec.js b/spec/frontend/diffs/components/diff_gutter_avatars_spec.js
index 4d8345d494d..da18d8e7894 100644
--- a/spec/frontend/diffs/components/diff_gutter_avatars_spec.js
+++ b/spec/frontend/diffs/components/diff_gutter_avatars_spec.js
@@ -2,7 +2,7 @@ import { shallowMount } from '@vue/test-utils';
import DiffGutterAvatars from '~/diffs/components/diff_gutter_avatars.vue';
import discussionsMockData from '../mock_data/diff_discussions';
-const getDiscussionsMockData = () => [Object.assign({}, discussionsMockData)];
+const getDiscussionsMockData = () => [{ ...discussionsMockData }];
describe('DiffGutterAvatars', () => {
let wrapper;
diff --git a/spec/frontend/diffs/components/diff_line_note_form_spec.js b/spec/frontend/diffs/components/diff_line_note_form_spec.js
index 9b032d10fdc..3e0acd0dace 100644
--- a/spec/frontend/diffs/components/diff_line_note_form_spec.js
+++ b/spec/frontend/diffs/components/diff_line_note_form_spec.js
@@ -9,7 +9,7 @@ describe('DiffLineNoteForm', () => {
let wrapper;
let diffFile;
let diffLines;
- const getDiffFileMock = () => Object.assign({}, diffFileMockData);
+ const getDiffFileMock = () => ({ ...diffFileMockData });
beforeEach(() => {
diffFile = getDiffFileMock();
diff --git a/spec/frontend/diffs/components/edit_button_spec.js b/spec/frontend/diffs/components/edit_button_spec.js
index f9a1d4a84a8..71512c1c4af 100644
--- a/spec/frontend/diffs/components/edit_button_spec.js
+++ b/spec/frontend/diffs/components/edit_button_spec.js
@@ -1,4 +1,5 @@
import { shallowMount } from '@vue/test-utils';
+import { GlDeprecatedButton } from '@gitlab/ui';
import EditButton from '~/diffs/components/edit_button.vue';
const editPath = 'test-path';
@@ -22,7 +23,7 @@ describe('EditButton', () => {
canCurrentUserFork: false,
});
- expect(wrapper.attributes('href')).toBe(editPath);
+ expect(wrapper.find(GlDeprecatedButton).attributes('href')).toBe(editPath);
});
it('emits a show fork message event if current user can fork', () => {
@@ -30,7 +31,7 @@ describe('EditButton', () => {
editPath,
canCurrentUserFork: true,
});
- wrapper.trigger('click');
+ wrapper.find(GlDeprecatedButton).trigger('click');
return wrapper.vm.$nextTick().then(() => {
expect(wrapper.emitted('showForkMessage')).toBeTruthy();
@@ -42,7 +43,7 @@ describe('EditButton', () => {
editPath,
canCurrentUserFork: false,
});
- wrapper.trigger('click');
+ wrapper.find(GlDeprecatedButton).trigger('click');
return wrapper.vm.$nextTick().then(() => {
expect(wrapper.emitted('showForkMessage')).toBeFalsy();
@@ -55,10 +56,20 @@ describe('EditButton', () => {
canCurrentUserFork: true,
canModifyBlob: true,
});
- wrapper.trigger('click');
+ wrapper.find(GlDeprecatedButton).trigger('click');
return wrapper.vm.$nextTick().then(() => {
expect(wrapper.emitted('showForkMessage')).toBeFalsy();
});
});
+
+ it('disables button if editPath is empty', () => {
+ createComponent({
+ editPath: '',
+ canCurrentUserFork: true,
+ canModifyBlob: true,
+ });
+
+ expect(wrapper.find(GlDeprecatedButton).attributes('disabled')).toBe('true');
+ });
});
diff --git a/spec/frontend/diffs/components/inline_diff_expansion_row_spec.js b/spec/frontend/diffs/components/inline_diff_expansion_row_spec.js
index f423c3b111e..90f012fbafe 100644
--- a/spec/frontend/diffs/components/inline_diff_expansion_row_spec.js
+++ b/spec/frontend/diffs/components/inline_diff_expansion_row_spec.js
@@ -16,7 +16,7 @@ describe('InlineDiffExpansionRow', () => {
isTop: false,
isBottom: false,
};
- const props = Object.assign({}, defaults, options);
+ const props = { ...defaults, ...options };
return createComponentWithStore(cmp, createStore(), props).$mount();
};
diff --git a/spec/frontend/diffs/components/inline_diff_view_spec.js b/spec/frontend/diffs/components/inline_diff_view_spec.js
index a63c13fb271..9b0cf6a84d9 100644
--- a/spec/frontend/diffs/components/inline_diff_view_spec.js
+++ b/spec/frontend/diffs/components/inline_diff_view_spec.js
@@ -8,8 +8,8 @@ import discussionsMockData from '../mock_data/diff_discussions';
describe('InlineDiffView', () => {
let component;
- const getDiffFileMock = () => Object.assign({}, diffFileMockData);
- const getDiscussionsMockData = () => [Object.assign({}, discussionsMockData)];
+ const getDiffFileMock = () => ({ ...diffFileMockData });
+ const getDiscussionsMockData = () => [{ ...discussionsMockData }];
const notesLength = getDiscussionsMockData()[0].notes.length;
beforeEach(done => {
diff --git a/spec/frontend/diffs/components/parallel_diff_expansion_row_spec.js b/spec/frontend/diffs/components/parallel_diff_expansion_row_spec.js
index 15b2a824697..38112445e8d 100644
--- a/spec/frontend/diffs/components/parallel_diff_expansion_row_spec.js
+++ b/spec/frontend/diffs/components/parallel_diff_expansion_row_spec.js
@@ -16,7 +16,7 @@ describe('ParallelDiffExpansionRow', () => {
isTop: false,
isBottom: false,
};
- const props = Object.assign({}, defaults, options);
+ const props = { ...defaults, ...options };
return createComponentWithStore(cmp, createStore(), props).$mount();
};
diff --git a/spec/frontend/diffs/components/parallel_diff_view_spec.js b/spec/frontend/diffs/components/parallel_diff_view_spec.js
index 0eefbc7ec08..03cf1b72b62 100644
--- a/spec/frontend/diffs/components/parallel_diff_view_spec.js
+++ b/spec/frontend/diffs/components/parallel_diff_view_spec.js
@@ -7,7 +7,7 @@ import diffFileMockData from '../mock_data/diff_file';
describe('ParallelDiffView', () => {
let component;
- const getDiffFileMock = () => Object.assign({}, diffFileMockData);
+ const getDiffFileMock = () => ({ ...diffFileMockData });
beforeEach(() => {
const diffFile = getDiffFileMock();
diff --git a/spec/frontend/diffs/store/actions_spec.js b/spec/frontend/diffs/store/actions_spec.js
index ceccce6312f..3fba661da44 100644
--- a/spec/frontend/diffs/store/actions_spec.js
+++ b/spec/frontend/diffs/store/actions_spec.js
@@ -40,9 +40,12 @@ import {
receiveFullDiffError,
fetchFullDiff,
toggleFullDiff,
+ switchToFullDiffFromRenamedFile,
setFileCollapsed,
setExpandedDiffLines,
setSuggestPopoverDismissed,
+ changeCurrentCommit,
+ moveToNeighboringCommit,
} from '~/diffs/store/actions';
import eventHub from '~/notes/event_hub';
import * as types from '~/diffs/store/mutation_types';
@@ -312,7 +315,7 @@ describe('DiffsStoreActions', () => {
describe('fetchDiffFilesMeta', () => {
it('should fetch diff meta information', done => {
- const endpointMetadata = '/fetch/diffs_meta?';
+ const endpointMetadata = '/fetch/diffs_meta';
const mock = new MockAdapter(axios);
const data = { diff_files: [] };
const res = { data };
@@ -1250,6 +1253,87 @@ describe('DiffsStoreActions', () => {
});
});
+ describe('switchToFullDiffFromRenamedFile', () => {
+ const SUCCESS_URL = 'fakehost/context.success';
+ const ERROR_URL = 'fakehost/context.error';
+ const testFilePath = 'testpath';
+ const updatedViewerName = 'testviewer';
+ const preparedLine = { prepared: 'in-a-test' };
+ const testFile = {
+ file_path: testFilePath,
+ file_hash: 'testhash',
+ alternate_viewer: { name: updatedViewerName },
+ };
+ const updatedViewer = { name: updatedViewerName, collapsed: false };
+ const testData = [{ rich_text: 'test' }, { rich_text: 'file2' }];
+ let renamedFile;
+ let mock;
+
+ beforeEach(() => {
+ mock = new MockAdapter(axios);
+ jest.spyOn(utils, 'prepareLineForRenamedFile').mockImplementation(() => preparedLine);
+ });
+
+ afterEach(() => {
+ renamedFile = null;
+ mock.restore();
+ });
+
+ describe('success', () => {
+ beforeEach(() => {
+ renamedFile = { ...testFile, context_lines_path: SUCCESS_URL };
+ mock.onGet(SUCCESS_URL).replyOnce(200, testData);
+ });
+
+ it.each`
+ diffViewType
+ ${INLINE_DIFF_VIEW_TYPE}
+ ${PARALLEL_DIFF_VIEW_TYPE}
+ `(
+ 'performs the correct mutations and starts a render queue for view type $diffViewType',
+ ({ diffViewType }) => {
+ return testAction(
+ switchToFullDiffFromRenamedFile,
+ { diffFile: renamedFile },
+ { diffViewType },
+ [
+ {
+ type: types.SET_DIFF_FILE_VIEWER,
+ payload: { filePath: testFilePath, viewer: updatedViewer },
+ },
+ {
+ type: types.SET_CURRENT_VIEW_DIFF_FILE_LINES,
+ payload: { filePath: testFilePath, lines: [preparedLine, preparedLine] },
+ },
+ ],
+ [{ type: 'startRenderDiffsQueue' }],
+ );
+ },
+ );
+ });
+
+ describe('error', () => {
+ beforeEach(() => {
+ renamedFile = { ...testFile, context_lines_path: ERROR_URL };
+ mock.onGet(ERROR_URL).reply(500);
+ });
+
+ it('dispatches the error handling action', () => {
+ const rejected = testAction(
+ switchToFullDiffFromRenamedFile,
+ { diffFile: renamedFile },
+ null,
+ [],
+ [{ type: 'receiveFullDiffError', payload: testFilePath }],
+ );
+
+ return rejected.catch(error =>
+ expect(error).toEqual(new Error('Request failed with status code 500')),
+ );
+ });
+ });
+ });
+
describe('setFileCollapsed', () => {
it('commits SET_FILE_COLLAPSED', done => {
testAction(
@@ -1347,4 +1431,102 @@ describe('DiffsStoreActions', () => {
);
});
});
+
+ describe('changeCurrentCommit', () => {
+ it('commits the new commit information and re-requests the diff metadata for the commit', () => {
+ return testAction(
+ changeCurrentCommit,
+ { commitId: 'NEW' },
+ {
+ commit: {
+ id: 'OLD',
+ },
+ endpoint: 'URL/OLD',
+ endpointBatch: 'URL/OLD',
+ endpointMetadata: 'URL/OLD',
+ },
+ [
+ { type: types.SET_DIFF_FILES, payload: [] },
+ {
+ type: types.SET_BASE_CONFIG,
+ payload: {
+ commit: {
+ id: 'OLD', // Not a typo: the action fired next will overwrite all of the `commit` in state
+ },
+ endpoint: 'URL/NEW',
+ endpointBatch: 'URL/NEW',
+ endpointMetadata: 'URL/NEW',
+ },
+ },
+ ],
+ [{ type: 'fetchDiffFilesMeta' }],
+ );
+ });
+
+ it.each`
+ commitId | commit | msg
+ ${undefined} | ${{ id: 'OLD' }} | ${'`commitId` is a required argument'}
+ ${'NEW'} | ${null} | ${'`state` must already contain a valid `commit`'}
+ ${undefined} | ${null} | ${'`commitId` is a required argument'}
+ `(
+ 'returns a rejected promise with the error message $msg given `{ "commitId": $commitId, "state.commit": $commit }`',
+ ({ commitId, commit, msg }) => {
+ const err = new Error(msg);
+ const actionReturn = testAction(
+ changeCurrentCommit,
+ { commitId },
+ {
+ endpoint: 'URL/OLD',
+ endpointBatch: 'URL/OLD',
+ endpointMetadata: 'URL/OLD',
+ commit,
+ },
+ [],
+ [],
+ );
+
+ return expect(actionReturn).rejects.toStrictEqual(err);
+ },
+ );
+ });
+
+ describe('moveToNeighboringCommit', () => {
+ it.each`
+ direction | expected | currentCommit
+ ${'next'} | ${'NEXTSHA'} | ${{ next_commit_id: 'NEXTSHA' }}
+ ${'previous'} | ${'PREVIOUSSHA'} | ${{ prev_commit_id: 'PREVIOUSSHA' }}
+ `(
+ 'for the direction "$direction", dispatches the action to move to the SHA "$expected"',
+ ({ direction, expected, currentCommit }) => {
+ return testAction(
+ moveToNeighboringCommit,
+ { direction },
+ { commit: currentCommit },
+ [],
+ [{ type: 'changeCurrentCommit', payload: { commitId: expected } }],
+ );
+ },
+ );
+
+ it.each`
+ direction | diffsAreLoading | currentCommit
+ ${'next'} | ${false} | ${{ prev_commit_id: 'PREVIOUSSHA' }}
+ ${'next'} | ${true} | ${{ prev_commit_id: 'PREVIOUSSHA' }}
+ ${'next'} | ${false} | ${undefined}
+ ${'previous'} | ${false} | ${{ next_commit_id: 'NEXTSHA' }}
+ ${'previous'} | ${true} | ${{ next_commit_id: 'NEXTSHA' }}
+ ${'previous'} | ${false} | ${undefined}
+ `(
+ 'given `{ "isloading": $diffsAreLoading, "commit": $currentCommit }` in state, no actions are dispatched',
+ ({ direction, diffsAreLoading, currentCommit }) => {
+ return testAction(
+ moveToNeighboringCommit,
+ { direction },
+ { commit: currentCommit, isLoading: diffsAreLoading },
+ [],
+ [],
+ );
+ },
+ );
+ });
});
diff --git a/spec/frontend/diffs/store/getters_spec.js b/spec/frontend/diffs/store/getters_spec.js
index ca47f51cb15..dac5be2d656 100644
--- a/spec/frontend/diffs/store/getters_spec.js
+++ b/spec/frontend/diffs/store/getters_spec.js
@@ -14,10 +14,10 @@ describe('Diffs Module Getters', () => {
beforeEach(() => {
localState = state();
- discussionMock = Object.assign({}, discussion);
+ discussionMock = { ...discussion };
discussionMock.diff_file.file_hash = diffFileMock.fileHash;
- discussionMock1 = Object.assign({}, discussion);
+ discussionMock1 = { ...discussion };
discussionMock1.diff_file.file_hash = diffFileMock.fileHash;
});
diff --git a/spec/frontend/diffs/store/getters_versions_dropdowns_spec.js b/spec/frontend/diffs/store/getters_versions_dropdowns_spec.js
index eb0f2364a50..0343ef75732 100644
--- a/spec/frontend/diffs/store/getters_versions_dropdowns_spec.js
+++ b/spec/frontend/diffs/store/getters_versions_dropdowns_spec.js
@@ -18,7 +18,6 @@ describe('Compare diff version dropdowns', () => {
};
localState.targetBranchName = 'baseVersion';
localState.mergeRequestDiffs = diffsMockData;
- gon.features = { diffCompareWithHead: true };
});
describe('selectedTargetIndex', () => {
@@ -129,14 +128,6 @@ describe('Compare diff version dropdowns', () => {
});
assertVersions(targetVersions);
});
-
- it('does not list head version if feature flag is not enabled', () => {
- gon.features = { diffCompareWithHead: false };
- setupTest();
- const targetVersions = getters.diffCompareDropdownTargetVersions(localState, getters);
-
- expect(targetVersions.find(version => version.isHead)).toBeUndefined();
- });
});
it('diffCompareDropdownSourceVersions', () => {
diff --git a/spec/frontend/diffs/store/mutations_spec.js b/spec/frontend/diffs/store/mutations_spec.js
index 858ab5be167..c24d406fef3 100644
--- a/spec/frontend/diffs/store/mutations_spec.js
+++ b/spec/frontend/diffs/store/mutations_spec.js
@@ -1041,6 +1041,36 @@ describe('DiffsStoreMutations', () => {
});
});
+ describe('SET_DIFF_FILE_VIEWER', () => {
+ it("should update the correct diffFile's viewer property", () => {
+ const state = {
+ diffFiles: [
+ { file_path: 'SearchString', viewer: 'OLD VIEWER' },
+ { file_path: 'OtherSearchString' },
+ { file_path: 'SomeOtherString' },
+ ],
+ };
+
+ mutations[types.SET_DIFF_FILE_VIEWER](state, {
+ filePath: 'SearchString',
+ viewer: 'NEW VIEWER',
+ });
+
+ expect(state.diffFiles[0].viewer).toEqual('NEW VIEWER');
+ expect(state.diffFiles[1].viewer).not.toBeDefined();
+ expect(state.diffFiles[2].viewer).not.toBeDefined();
+
+ mutations[types.SET_DIFF_FILE_VIEWER](state, {
+ filePath: 'OtherSearchString',
+ viewer: 'NEW VIEWER',
+ });
+
+ expect(state.diffFiles[0].viewer).toEqual('NEW VIEWER');
+ expect(state.diffFiles[1].viewer).toEqual('NEW VIEWER');
+ expect(state.diffFiles[2].viewer).not.toBeDefined();
+ });
+ });
+
describe('SET_SHOW_SUGGEST_POPOVER', () => {
it('sets showSuggestPopover to false', () => {
const state = { showSuggestPopover: true };
diff --git a/spec/frontend/diffs/store/utils_spec.js b/spec/frontend/diffs/store/utils_spec.js
index 1adcdab272a..641373e666f 100644
--- a/spec/frontend/diffs/store/utils_spec.js
+++ b/spec/frontend/diffs/store/utils_spec.js
@@ -361,6 +361,72 @@ describe('DiffsStoreUtils', () => {
});
});
+ describe('prepareLineForRenamedFile', () => {
+ const diffFile = {
+ file_hash: 'file-hash',
+ };
+ const lineIndex = 4;
+ const sourceLine = {
+ foo: 'test',
+ rich_text: ' <p>rich</p>', // Note the leading space
+ };
+ const correctLine = {
+ foo: 'test',
+ line_code: 'file-hash_5_5',
+ old_line: 5,
+ new_line: 5,
+ rich_text: '<p>rich</p>', // Note no leading space
+ discussionsExpanded: true,
+ discussions: [],
+ hasForm: false,
+ text: undefined,
+ alreadyPrepared: true,
+ };
+ let preppedLine;
+
+ beforeEach(() => {
+ preppedLine = utils.prepareLineForRenamedFile({
+ diffViewType: INLINE_DIFF_VIEW_TYPE,
+ line: sourceLine,
+ index: lineIndex,
+ diffFile,
+ });
+ });
+
+ it('copies over the original line object to the new prepared line', () => {
+ expect(preppedLine).toEqual(
+ expect.objectContaining({
+ foo: correctLine.foo,
+ rich_text: correctLine.rich_text,
+ }),
+ );
+ });
+
+ it('correctly sets the old and new lines, plus a line code', () => {
+ expect(preppedLine.old_line).toEqual(correctLine.old_line);
+ expect(preppedLine.new_line).toEqual(correctLine.new_line);
+ expect(preppedLine.line_code).toEqual(correctLine.line_code);
+ });
+
+ it('returns a single object with the correct structure for `inline` lines', () => {
+ expect(preppedLine).toEqual(correctLine);
+ });
+
+ it('returns a nested object with "left" and "right" lines + the line code for `parallel` lines', () => {
+ preppedLine = utils.prepareLineForRenamedFile({
+ diffViewType: PARALLEL_DIFF_VIEW_TYPE,
+ line: sourceLine,
+ index: lineIndex,
+ diffFile,
+ });
+
+ expect(Object.keys(preppedLine)).toEqual(['left', 'right', 'line_code']);
+ expect(preppedLine.left).toEqual(correctLine);
+ expect(preppedLine.right).toEqual(correctLine);
+ expect(preppedLine.line_code).toEqual(correctLine.line_code);
+ });
+ });
+
describe('prepareDiffData', () => {
let mock;
let preparedDiff;
@@ -372,13 +438,13 @@ describe('DiffsStoreUtils', () => {
mock = getDiffFileMock();
preparedDiff = { diff_files: [mock] };
splitInlineDiff = {
- diff_files: [Object.assign({}, mock, { parallel_diff_lines: undefined })],
+ diff_files: [{ ...mock, parallel_diff_lines: undefined }],
};
splitParallelDiff = {
- diff_files: [Object.assign({}, mock, { highlighted_diff_lines: undefined })],
+ diff_files: [{ ...mock, highlighted_diff_lines: undefined }],
};
completedDiff = {
- diff_files: [Object.assign({}, mock, { highlighted_diff_lines: undefined })],
+ diff_files: [{ ...mock, highlighted_diff_lines: undefined }],
};
preparedDiff.diff_files = utils.prepareDiffData(preparedDiff);
@@ -503,11 +569,16 @@ describe('DiffsStoreUtils', () => {
},
};
+ // When multi line comments are fully implemented `line_code` will be
+ // included in all requests. Until then we need to ensure the logic does
+ // not change when it is included only in the "comparison" argument.
+ const lineRange = { start_line_code: 'abc_1_1', end_line_code: 'abc_1_2' };
+
it('returns true when the discussion is up to date', () => {
expect(
utils.isDiscussionApplicableToLine({
discussion: discussions.upToDateDiscussion1,
- diffPosition,
+ diffPosition: { ...diffPosition, line_range: lineRange },
latestDiff: true,
}),
).toBe(true);
@@ -517,7 +588,7 @@ describe('DiffsStoreUtils', () => {
expect(
utils.isDiscussionApplicableToLine({
discussion: discussions.outDatedDiscussion1,
- diffPosition,
+ diffPosition: { ...diffPosition, line_range: lineRange },
latestDiff: true,
}),
).toBe(false);
@@ -534,6 +605,7 @@ describe('DiffsStoreUtils', () => {
diffPosition: {
...diffPosition,
lineCode: 'ABC_1',
+ line_range: lineRange,
},
latestDiff: true,
}),
@@ -551,6 +623,7 @@ describe('DiffsStoreUtils', () => {
diffPosition: {
...diffPosition,
line_code: 'ABC_1',
+ line_range: lineRange,
},
latestDiff: true,
}),
@@ -568,6 +641,7 @@ describe('DiffsStoreUtils', () => {
diffPosition: {
...diffPosition,
lineCode: 'ABC_1',
+ line_range: lineRange,
},
latestDiff: false,
}),
diff --git a/spec/frontend/dirty_submit/dirty_submit_collection_spec.js b/spec/frontend/dirty_submit/dirty_submit_collection_spec.js
new file mode 100644
index 00000000000..170d581be23
--- /dev/null
+++ b/spec/frontend/dirty_submit/dirty_submit_collection_spec.js
@@ -0,0 +1,22 @@
+import DirtySubmitCollection from '~/dirty_submit/dirty_submit_collection';
+import { setInputValue, createForm } from './helper';
+
+jest.mock('lodash/throttle', () => jest.fn(fn => fn));
+
+describe('DirtySubmitCollection', () => {
+ const testElementsCollection = [createForm(), createForm()];
+ const forms = testElementsCollection.map(testElements => testElements.form);
+
+ new DirtySubmitCollection(forms); // eslint-disable-line no-new
+
+ it.each(testElementsCollection)('disables submits until there are changes', testElements => {
+ const { input, submit } = testElements;
+ const originalValue = input.value;
+
+ expect(submit.disabled).toBe(true);
+ setInputValue(input, `${originalValue} changes`);
+ expect(submit.disabled).toBe(false);
+ setInputValue(input, originalValue);
+ expect(submit.disabled).toBe(true);
+ });
+});
diff --git a/spec/javascripts/dirty_submit/dirty_submit_factory_spec.js b/spec/frontend/dirty_submit/dirty_submit_factory_spec.js
index 40843a68582..40843a68582 100644
--- a/spec/javascripts/dirty_submit/dirty_submit_factory_spec.js
+++ b/spec/frontend/dirty_submit/dirty_submit_factory_spec.js
diff --git a/spec/frontend/dirty_submit/dirty_submit_form_spec.js b/spec/frontend/dirty_submit/dirty_submit_form_spec.js
new file mode 100644
index 00000000000..d7f690df1f3
--- /dev/null
+++ b/spec/frontend/dirty_submit/dirty_submit_form_spec.js
@@ -0,0 +1,97 @@
+import { range as rge, throttle } from 'lodash';
+import DirtySubmitForm from '~/dirty_submit/dirty_submit_form';
+import { getInputValue, setInputValue, createForm } from './helper';
+
+jest.mock('lodash/throttle', () => jest.fn(fn => fn));
+const lodash = jest.requireActual('lodash');
+
+function expectToToggleDisableOnDirtyUpdate(submit, input) {
+ const originalValue = getInputValue(input);
+
+ expect(submit.disabled).toBe(true);
+
+ setInputValue(input, `${originalValue} changes`);
+ expect(submit.disabled).toBe(false);
+ setInputValue(input, originalValue);
+ expect(submit.disabled).toBe(true);
+}
+
+describe('DirtySubmitForm', () => {
+ describe('submit button tests', () => {
+ it('disables submit until there are changes', () => {
+ const { form, input, submit } = createForm();
+
+ new DirtySubmitForm(form); // eslint-disable-line no-new
+
+ expectToToggleDisableOnDirtyUpdate(submit, input);
+ });
+
+ it('disables submit until there are changes when initializing with a falsy value', () => {
+ const { form, input, submit } = createForm();
+ input.value = '';
+
+ new DirtySubmitForm(form); // eslint-disable-line no-new
+
+ expectToToggleDisableOnDirtyUpdate(submit, input);
+ });
+
+ it('disables submit until there are changes for radio inputs', () => {
+ const { form, input, submit } = createForm('radio');
+
+ new DirtySubmitForm(form); // eslint-disable-line no-new
+
+ expectToToggleDisableOnDirtyUpdate(submit, input);
+ });
+
+ it('disables submit until there are changes for checkbox inputs', () => {
+ const { form, input, submit } = createForm('checkbox');
+
+ new DirtySubmitForm(form); // eslint-disable-line no-new
+
+ expectToToggleDisableOnDirtyUpdate(submit, input);
+ });
+ });
+
+ describe('throttling tests', () => {
+ beforeEach(() => {
+ throttle.mockImplementation(lodash.throttle);
+ jest.useFakeTimers();
+ });
+
+ afterEach(() => {
+ throttle.mockReset();
+ });
+
+ it('throttles updates when rapid changes are made to a single form element', () => {
+ const { form, input } = createForm();
+ const updateDirtyInputSpy = jest.spyOn(new DirtySubmitForm(form), 'updateDirtyInput');
+
+ rge(10).forEach(i => {
+ setInputValue(input, `change ${i}`, false);
+ });
+
+ jest.runOnlyPendingTimers();
+
+ expect(updateDirtyInputSpy).toHaveBeenCalledTimes(1);
+ });
+
+ it('does not throttle updates when rapid changes are made to different form elements', () => {
+ const form = document.createElement('form');
+ const range = rge(10);
+ range.forEach(i => {
+ form.innerHTML += `<input type="text" name="input-${i}" class="js-input-${i}"/>`;
+ });
+
+ const updateDirtyInputSpy = jest.spyOn(new DirtySubmitForm(form), 'updateDirtyInput');
+
+ range.forEach(i => {
+ const input = form.querySelector(`.js-input-${i}`);
+ setInputValue(input, `change`, false);
+ });
+
+ jest.runOnlyPendingTimers();
+
+ expect(updateDirtyInputSpy).toHaveBeenCalledTimes(range.length);
+ });
+ });
+});
diff --git a/spec/frontend/dirty_submit/helper.js b/spec/frontend/dirty_submit/helper.js
new file mode 100644
index 00000000000..c02512b7671
--- /dev/null
+++ b/spec/frontend/dirty_submit/helper.js
@@ -0,0 +1,43 @@
+function isCheckableType(type) {
+ return /^(radio|checkbox)$/.test(type);
+}
+
+export function setInputValue(element, value) {
+ const { type } = element;
+ let eventType;
+
+ if (isCheckableType(type)) {
+ element.checked = !element.checked;
+ eventType = 'change';
+ } else {
+ element.value = value;
+ eventType = 'input';
+ }
+
+ element.dispatchEvent(
+ new Event(eventType, {
+ bubbles: true,
+ }),
+ );
+}
+
+export function getInputValue(input) {
+ return isCheckableType(input.type) ? input.checked : input.value;
+}
+
+export function createForm(type = 'text') {
+ const form = document.createElement('form');
+ form.innerHTML = `
+ <input type="${type}" name="${type}" class="js-input"/>
+ <button type="submit" class="js-dirty-submit"></button>
+ `;
+
+ const input = form.querySelector('.js-input');
+ const submit = form.querySelector('.js-dirty-submit');
+
+ return {
+ form,
+ input,
+ submit,
+ };
+}
diff --git a/spec/frontend/editor/editor_lite_spec.js b/spec/frontend/editor/editor_lite_spec.js
new file mode 100644
index 00000000000..cb07bcf8f28
--- /dev/null
+++ b/spec/frontend/editor/editor_lite_spec.js
@@ -0,0 +1,177 @@
+import { editor as monacoEditor, languages as monacoLanguages, Uri } from 'monaco-editor';
+import Editor from '~/editor/editor_lite';
+import { DEFAULT_THEME, themes } from '~/ide/lib/themes';
+
+describe('Base editor', () => {
+ let editorEl;
+ let editor;
+ const blobContent = 'Foo Bar';
+ const blobPath = 'test.md';
+ const uri = new Uri('gitlab', false, blobPath);
+ const fakeModel = { foo: 'bar' };
+
+ beforeEach(() => {
+ setFixtures('<div id="editor" data-editor-loading></div>');
+ editorEl = document.getElementById('editor');
+ editor = new Editor();
+ });
+
+ afterEach(() => {
+ editor.dispose();
+ editorEl.remove();
+ });
+
+ it('initializes Editor with basic properties', () => {
+ expect(editor).toBeDefined();
+ expect(editor.editorEl).toBe(null);
+ expect(editor.blobContent).toEqual('');
+ expect(editor.blobPath).toEqual('');
+ });
+
+ it('removes `editor-loading` data attribute from the target DOM element', () => {
+ editor.createInstance({ el: editorEl });
+
+ expect(editorEl.dataset.editorLoading).toBeUndefined();
+ });
+
+ describe('instance of the Editor', () => {
+ let modelSpy;
+ let instanceSpy;
+ let setModel;
+ let dispose;
+
+ beforeEach(() => {
+ setModel = jest.fn();
+ dispose = jest.fn();
+ modelSpy = jest.spyOn(monacoEditor, 'createModel').mockImplementation(() => fakeModel);
+ instanceSpy = jest.spyOn(monacoEditor, 'create').mockImplementation(() => ({
+ setModel,
+ dispose,
+ }));
+ });
+
+ it('does nothing if no dom element is supplied', () => {
+ editor.createInstance();
+
+ expect(editor.editorEl).toBe(null);
+ expect(editor.blobContent).toEqual('');
+ expect(editor.blobPath).toEqual('');
+
+ expect(modelSpy).not.toHaveBeenCalled();
+ expect(instanceSpy).not.toHaveBeenCalled();
+ expect(setModel).not.toHaveBeenCalled();
+ });
+
+ it('creates model to be supplied to Monaco editor', () => {
+ editor.createInstance({ el: editorEl, blobPath, blobContent });
+
+ expect(modelSpy).toHaveBeenCalledWith(blobContent, undefined, uri);
+ expect(setModel).toHaveBeenCalledWith(fakeModel);
+ });
+
+ it('initializes the instance on a supplied DOM node', () => {
+ editor.createInstance({ el: editorEl });
+
+ expect(editor.editorEl).not.toBe(null);
+ expect(instanceSpy).toHaveBeenCalledWith(editorEl, expect.anything());
+ });
+ });
+
+ describe('implementation', () => {
+ beforeEach(() => {
+ editor.createInstance({ el: editorEl, blobPath, blobContent });
+ });
+
+ afterEach(() => {
+ editor.model.dispose();
+ });
+
+ it('correctly proxies value from the model', () => {
+ expect(editor.getValue()).toEqual(blobContent);
+ });
+
+ it('is capable of changing the language of the model', () => {
+ // ignore warnings and errors Monaco posts during setup
+ // (due to being called from Jest/Node.js environment)
+ jest.spyOn(console, 'warn').mockImplementation(() => {});
+ jest.spyOn(console, 'error').mockImplementation(() => {});
+
+ const blobRenamedPath = 'test.js';
+
+ expect(editor.model.getLanguageIdentifier().language).toEqual('markdown');
+ editor.updateModelLanguage(blobRenamedPath);
+
+ expect(editor.model.getLanguageIdentifier().language).toEqual('javascript');
+ });
+
+ it('falls back to plaintext if there is no language associated with an extension', () => {
+ const blobRenamedPath = 'test.myext';
+ const spy = jest.spyOn(console, 'error').mockImplementation(() => {});
+
+ editor.updateModelLanguage(blobRenamedPath);
+
+ expect(spy).not.toHaveBeenCalled();
+ expect(editor.model.getLanguageIdentifier().language).toEqual('plaintext');
+ });
+ });
+
+ describe('languages', () => {
+ it('registers custom languages defined with Monaco', () => {
+ expect(monacoLanguages.getLanguages()).toEqual(
+ expect.arrayContaining([
+ expect.objectContaining({
+ id: 'vue',
+ }),
+ ]),
+ );
+ });
+ });
+
+ describe('syntax highlighting theme', () => {
+ let themeDefineSpy;
+ let themeSetSpy;
+ let defaultScheme;
+
+ beforeEach(() => {
+ themeDefineSpy = jest.spyOn(monacoEditor, 'defineTheme').mockImplementation(() => {});
+ themeSetSpy = jest.spyOn(monacoEditor, 'setTheme').mockImplementation(() => {});
+ defaultScheme = window.gon.user_color_scheme;
+ });
+
+ afterEach(() => {
+ window.gon.user_color_scheme = defaultScheme;
+ });
+
+ it('sets default syntax highlighting theme', () => {
+ const expectedTheme = themes.find(t => t.name === DEFAULT_THEME);
+
+ editor = new Editor();
+
+ expect(themeDefineSpy).toHaveBeenCalledWith(DEFAULT_THEME, expectedTheme.data);
+ expect(themeSetSpy).toHaveBeenCalledWith(DEFAULT_THEME);
+ });
+
+ it('sets correct theme if it is set in users preferences', () => {
+ const expectedTheme = themes.find(t => t.name !== DEFAULT_THEME);
+
+ expect(expectedTheme.name).not.toBe(DEFAULT_THEME);
+
+ window.gon.user_color_scheme = expectedTheme.name;
+ editor = new Editor();
+
+ expect(themeDefineSpy).toHaveBeenCalledWith(expectedTheme.name, expectedTheme.data);
+ expect(themeSetSpy).toHaveBeenCalledWith(expectedTheme.name);
+ });
+
+ it('falls back to default theme if a selected one is not supported yet', () => {
+ const name = 'non-existent-theme';
+ const nonExistentTheme = { name };
+
+ window.gon.user_color_scheme = nonExistentTheme.name;
+ editor = new Editor();
+
+ expect(themeDefineSpy).not.toHaveBeenCalled();
+ expect(themeSetSpy).toHaveBeenCalledWith(DEFAULT_THEME);
+ });
+ });
+});
diff --git a/spec/frontend/emoji_spec.js b/spec/frontend/emoji_spec.js
new file mode 100644
index 00000000000..25bc95e0dd6
--- /dev/null
+++ b/spec/frontend/emoji_spec.js
@@ -0,0 +1,485 @@
+import { glEmojiTag } from '~/emoji';
+import isEmojiUnicodeSupported, {
+ isFlagEmoji,
+ isRainbowFlagEmoji,
+ isKeycapEmoji,
+ isSkinToneComboEmoji,
+ isHorceRacingSkinToneComboEmoji,
+ isPersonZwjEmoji,
+} from '~/emoji/support/is_emoji_unicode_supported';
+
+const emptySupportMap = {
+ personZwj: false,
+ horseRacing: false,
+ flag: false,
+ skinToneModifier: false,
+ '9.0': false,
+ '8.0': false,
+ '7.0': false,
+ 6.1: false,
+ '6.0': false,
+ 5.2: false,
+ 5.1: false,
+ 4.1: false,
+ '4.0': false,
+ 3.2: false,
+ '3.0': false,
+ 1.1: false,
+};
+
+const emojiFixtureMap = {
+ bomb: {
+ name: 'bomb',
+ moji: '💣',
+ unicodeVersion: '6.0',
+ },
+ construction_worker_tone5: {
+ name: 'construction_worker_tone5',
+ moji: '👷ðŸ¿',
+ unicodeVersion: '8.0',
+ },
+ five: {
+ name: 'five',
+ moji: '5ï¸âƒ£',
+ unicodeVersion: '3.0',
+ },
+ grey_question: {
+ name: 'grey_question',
+ moji: 'â”',
+ unicodeVersion: '6.0',
+ },
+};
+
+function markupToDomElement(markup) {
+ const div = document.createElement('div');
+ div.innerHTML = markup;
+ return div.firstElementChild;
+}
+
+function testGlEmojiImageFallback(element, name, src) {
+ expect(element.tagName.toLowerCase()).toBe('img');
+ expect(element.getAttribute('src')).toBe(src);
+ expect(element.getAttribute('title')).toBe(`:${name}:`);
+ expect(element.getAttribute('alt')).toBe(`:${name}:`);
+}
+
+const defaults = {
+ forceFallback: false,
+ sprite: false,
+};
+
+function testGlEmojiElement(element, name, unicodeVersion, unicodeMoji, options = {}) {
+ const opts = { ...defaults, ...options };
+ expect(element.tagName.toLowerCase()).toBe('gl-emoji');
+ expect(element.dataset.name).toBe(name);
+ expect(element.dataset.fallbackSrc.length).toBeGreaterThan(0);
+ expect(element.dataset.unicodeVersion).toBe(unicodeVersion);
+
+ const fallbackSpriteClass = `emoji-${name}`;
+ if (opts.sprite) {
+ expect(element.dataset.fallbackSpriteClass).toBe(fallbackSpriteClass);
+ }
+
+ if (opts.forceFallback && opts.sprite) {
+ expect(element.getAttribute('class')).toBe(`emoji-icon ${fallbackSpriteClass}`);
+ }
+
+ if (opts.forceFallback && !opts.sprite) {
+ // Check for image fallback
+ testGlEmojiImageFallback(element.firstElementChild, name, element.dataset.fallbackSrc);
+ } else {
+ // Otherwise make sure things are still unicode text
+ expect(element.textContent.trim()).toBe(unicodeMoji);
+ }
+}
+
+describe('gl_emoji', () => {
+ describe('glEmojiTag', () => {
+ it('bomb emoji', () => {
+ const emojiKey = 'bomb';
+ const markup = glEmojiTag(emojiFixtureMap[emojiKey].name);
+ const glEmojiElement = markupToDomElement(markup);
+ testGlEmojiElement(
+ glEmojiElement,
+ emojiFixtureMap[emojiKey].name,
+ emojiFixtureMap[emojiKey].unicodeVersion,
+ emojiFixtureMap[emojiKey].moji,
+ );
+ });
+
+ it('bomb emoji with image fallback', () => {
+ const emojiKey = 'bomb';
+ const markup = glEmojiTag(emojiFixtureMap[emojiKey].name, {
+ forceFallback: true,
+ });
+ const glEmojiElement = markupToDomElement(markup);
+ testGlEmojiElement(
+ glEmojiElement,
+ emojiFixtureMap[emojiKey].name,
+ emojiFixtureMap[emojiKey].unicodeVersion,
+ emojiFixtureMap[emojiKey].moji,
+ {
+ forceFallback: true,
+ },
+ );
+ });
+
+ it('bomb emoji with sprite fallback readiness', () => {
+ const emojiKey = 'bomb';
+ const markup = glEmojiTag(emojiFixtureMap[emojiKey].name, {
+ sprite: true,
+ });
+ const glEmojiElement = markupToDomElement(markup);
+ testGlEmojiElement(
+ glEmojiElement,
+ emojiFixtureMap[emojiKey].name,
+ emojiFixtureMap[emojiKey].unicodeVersion,
+ emojiFixtureMap[emojiKey].moji,
+ {
+ sprite: true,
+ },
+ );
+ });
+
+ it('bomb emoji with sprite fallback', () => {
+ const emojiKey = 'bomb';
+ const markup = glEmojiTag(emojiFixtureMap[emojiKey].name, {
+ forceFallback: true,
+ sprite: true,
+ });
+ const glEmojiElement = markupToDomElement(markup);
+ testGlEmojiElement(
+ glEmojiElement,
+ emojiFixtureMap[emojiKey].name,
+ emojiFixtureMap[emojiKey].unicodeVersion,
+ emojiFixtureMap[emojiKey].moji,
+ {
+ forceFallback: true,
+ sprite: true,
+ },
+ );
+ });
+
+ it('question mark when invalid emoji name given', () => {
+ const name = 'invalid_emoji';
+ const emojiKey = 'grey_question';
+ const markup = glEmojiTag(name);
+ const glEmojiElement = markupToDomElement(markup);
+ testGlEmojiElement(
+ glEmojiElement,
+ emojiFixtureMap[emojiKey].name,
+ emojiFixtureMap[emojiKey].unicodeVersion,
+ emojiFixtureMap[emojiKey].moji,
+ );
+ });
+
+ it('question mark with image fallback when invalid emoji name given', () => {
+ const name = 'invalid_emoji';
+ const emojiKey = 'grey_question';
+ const markup = glEmojiTag(name, {
+ forceFallback: true,
+ });
+ const glEmojiElement = markupToDomElement(markup);
+ testGlEmojiElement(
+ glEmojiElement,
+ emojiFixtureMap[emojiKey].name,
+ emojiFixtureMap[emojiKey].unicodeVersion,
+ emojiFixtureMap[emojiKey].moji,
+ {
+ forceFallback: true,
+ },
+ );
+ });
+ });
+
+ describe('isFlagEmoji', () => {
+ it('should gracefully handle empty string', () => {
+ expect(isFlagEmoji('')).toBeFalsy();
+ });
+
+ it('should detect flag_ac', () => {
+ expect(isFlagEmoji('🇦🇨')).toBeTruthy();
+ });
+
+ it('should detect flag_us', () => {
+ expect(isFlagEmoji('🇺🇸')).toBeTruthy();
+ });
+
+ it('should detect flag_zw', () => {
+ expect(isFlagEmoji('🇿🇼')).toBeTruthy();
+ });
+
+ it('should not detect flags', () => {
+ expect(isFlagEmoji('ðŸŽ')).toBeFalsy();
+ });
+
+ it('should not detect triangular_flag_on_post', () => {
+ expect(isFlagEmoji('🚩')).toBeFalsy();
+ });
+
+ it('should not detect single letter', () => {
+ expect(isFlagEmoji('🇦')).toBeFalsy();
+ });
+
+ it('should not detect >2 letters', () => {
+ expect(isFlagEmoji('🇦🇧🇨')).toBeFalsy();
+ });
+ });
+
+ describe('isRainbowFlagEmoji', () => {
+ it('should gracefully handle empty string', () => {
+ expect(isRainbowFlagEmoji('')).toBeFalsy();
+ });
+
+ it('should detect rainbow_flag', () => {
+ expect(isRainbowFlagEmoji('ðŸ³ðŸŒˆ')).toBeTruthy();
+ });
+
+ it("should not detect flag_white on its' own", () => {
+ expect(isRainbowFlagEmoji('ðŸ³')).toBeFalsy();
+ });
+
+ it("should not detect rainbow on its' own", () => {
+ expect(isRainbowFlagEmoji('🌈')).toBeFalsy();
+ });
+
+ it('should not detect flag_white with something else', () => {
+ expect(isRainbowFlagEmoji('ðŸ³ðŸ”µ')).toBeFalsy();
+ });
+ });
+
+ describe('isKeycapEmoji', () => {
+ it('should gracefully handle empty string', () => {
+ expect(isKeycapEmoji('')).toBeFalsy();
+ });
+
+ it('should detect one(keycap)', () => {
+ expect(isKeycapEmoji('1ï¸âƒ£')).toBeTruthy();
+ });
+
+ it('should detect nine(keycap)', () => {
+ expect(isKeycapEmoji('9ï¸âƒ£')).toBeTruthy();
+ });
+
+ it('should not detect ten(keycap)', () => {
+ expect(isKeycapEmoji('🔟')).toBeFalsy();
+ });
+
+ it('should not detect hash(keycap)', () => {
+ expect(isKeycapEmoji('#⃣')).toBeFalsy();
+ });
+ });
+
+ describe('isSkinToneComboEmoji', () => {
+ it('should gracefully handle empty string', () => {
+ expect(isSkinToneComboEmoji('')).toBeFalsy();
+ });
+
+ it('should detect hand_splayed_tone5', () => {
+ expect(isSkinToneComboEmoji('ðŸ–ðŸ¿')).toBeTruthy();
+ });
+
+ it('should not detect hand_splayed', () => {
+ expect(isSkinToneComboEmoji('ðŸ–')).toBeFalsy();
+ });
+
+ it('should detect lifter_tone1', () => {
+ expect(isSkinToneComboEmoji('ðŸ‹ðŸ»')).toBeTruthy();
+ });
+
+ it('should not detect lifter', () => {
+ expect(isSkinToneComboEmoji('ðŸ‹')).toBeFalsy();
+ });
+
+ it('should detect rowboat_tone4', () => {
+ expect(isSkinToneComboEmoji('🚣ðŸ¾')).toBeTruthy();
+ });
+
+ it('should not detect rowboat', () => {
+ expect(isSkinToneComboEmoji('🚣')).toBeFalsy();
+ });
+
+ it('should not detect individual tone emoji', () => {
+ expect(isSkinToneComboEmoji('ðŸ»')).toBeFalsy();
+ });
+ });
+
+ describe('isHorceRacingSkinToneComboEmoji', () => {
+ it('should gracefully handle empty string', () => {
+ expect(isHorceRacingSkinToneComboEmoji('')).toBeFalsy();
+ });
+
+ it('should detect horse_racing_tone2', () => {
+ expect(isHorceRacingSkinToneComboEmoji('ðŸ‡ðŸ¼')).toBeTruthy();
+ });
+
+ it('should not detect horse_racing', () => {
+ expect(isHorceRacingSkinToneComboEmoji('ðŸ‡')).toBeFalsy();
+ });
+ });
+
+ describe('isPersonZwjEmoji', () => {
+ it('should gracefully handle empty string', () => {
+ expect(isPersonZwjEmoji('')).toBeFalsy();
+ });
+
+ it('should detect couple_mm', () => {
+ expect(isPersonZwjEmoji('👨â€â¤ï¸â€ðŸ‘¨')).toBeTruthy();
+ });
+
+ it('should not detect couple_with_heart', () => {
+ expect(isPersonZwjEmoji('💑')).toBeFalsy();
+ });
+
+ it('should not detect couplekiss', () => {
+ expect(isPersonZwjEmoji('ðŸ’')).toBeFalsy();
+ });
+
+ it('should detect family_mmb', () => {
+ expect(isPersonZwjEmoji('👨â€ðŸ‘¨â€ðŸ‘¦')).toBeTruthy();
+ });
+
+ it('should detect family_mwgb', () => {
+ expect(isPersonZwjEmoji('👨â€ðŸ‘©â€ðŸ‘§â€ðŸ‘¦')).toBeTruthy();
+ });
+
+ it('should not detect family', () => {
+ expect(isPersonZwjEmoji('👪')).toBeFalsy();
+ });
+
+ it('should detect kiss_ww', () => {
+ expect(isPersonZwjEmoji('👩â€â¤ï¸â€ðŸ’‹â€ðŸ‘©')).toBeTruthy();
+ });
+
+ it('should not detect girl', () => {
+ expect(isPersonZwjEmoji('👧')).toBeFalsy();
+ });
+
+ it('should not detect girl_tone5', () => {
+ expect(isPersonZwjEmoji('👧ðŸ¿')).toBeFalsy();
+ });
+
+ it('should not detect man', () => {
+ expect(isPersonZwjEmoji('👨')).toBeFalsy();
+ });
+
+ it('should not detect woman', () => {
+ expect(isPersonZwjEmoji('👩')).toBeFalsy();
+ });
+ });
+
+ describe('isEmojiUnicodeSupported', () => {
+ it('should gracefully handle empty string with unicode support', () => {
+ const isSupported = isEmojiUnicodeSupported({ '1.0': true }, '', '1.0');
+
+ expect(isSupported).toBeTruthy();
+ });
+
+ it('should gracefully handle empty string without unicode support', () => {
+ const isSupported = isEmojiUnicodeSupported({}, '', '1.0');
+
+ expect(isSupported).toBeFalsy();
+ });
+
+ it('bomb(6.0) with 6.0 support', () => {
+ const emojiKey = 'bomb';
+ const unicodeSupportMap = { ...emptySupportMap, '6.0': true };
+ const isSupported = isEmojiUnicodeSupported(
+ unicodeSupportMap,
+ emojiFixtureMap[emojiKey].moji,
+ emojiFixtureMap[emojiKey].unicodeVersion,
+ );
+
+ expect(isSupported).toBeTruthy();
+ });
+
+ it('bomb(6.0) without 6.0 support', () => {
+ const emojiKey = 'bomb';
+ const unicodeSupportMap = emptySupportMap;
+ const isSupported = isEmojiUnicodeSupported(
+ unicodeSupportMap,
+ emojiFixtureMap[emojiKey].moji,
+ emojiFixtureMap[emojiKey].unicodeVersion,
+ );
+
+ expect(isSupported).toBeFalsy();
+ });
+
+ it('bomb(6.0) without 6.0 but with 9.0 support', () => {
+ const emojiKey = 'bomb';
+ const unicodeSupportMap = { ...emptySupportMap, '9.0': true };
+ const isSupported = isEmojiUnicodeSupported(
+ unicodeSupportMap,
+ emojiFixtureMap[emojiKey].moji,
+ emojiFixtureMap[emojiKey].unicodeVersion,
+ );
+
+ expect(isSupported).toBeFalsy();
+ });
+
+ it('construction_worker_tone5(8.0) without skin tone modifier support', () => {
+ const emojiKey = 'construction_worker_tone5';
+ const unicodeSupportMap = {
+ ...emptySupportMap,
+ skinToneModifier: false,
+ '9.0': true,
+ '8.0': true,
+ '7.0': true,
+ 6.1: true,
+ '6.0': true,
+ 5.2: true,
+ 5.1: true,
+ 4.1: true,
+ '4.0': true,
+ 3.2: true,
+ '3.0': true,
+ 1.1: true,
+ };
+ const isSupported = isEmojiUnicodeSupported(
+ unicodeSupportMap,
+ emojiFixtureMap[emojiKey].moji,
+ emojiFixtureMap[emojiKey].unicodeVersion,
+ );
+
+ expect(isSupported).toBeFalsy();
+ });
+
+ it('use native keycap on >=57 chrome', () => {
+ const emojiKey = 'five';
+ const unicodeSupportMap = {
+ ...emptySupportMap,
+ '3.0': true,
+ meta: {
+ isChrome: true,
+ chromeVersion: 57,
+ },
+ };
+ const isSupported = isEmojiUnicodeSupported(
+ unicodeSupportMap,
+ emojiFixtureMap[emojiKey].moji,
+ emojiFixtureMap[emojiKey].unicodeVersion,
+ );
+
+ expect(isSupported).toBeTruthy();
+ });
+
+ it('fallback keycap on <57 chrome', () => {
+ const emojiKey = 'five';
+ const unicodeSupportMap = {
+ ...emptySupportMap,
+ '3.0': true,
+ meta: {
+ isChrome: true,
+ chromeVersion: 50,
+ },
+ };
+ const isSupported = isEmojiUnicodeSupported(
+ unicodeSupportMap,
+ emojiFixtureMap[emojiKey].moji,
+ emojiFixtureMap[emojiKey].unicodeVersion,
+ );
+
+ expect(isSupported).toBeFalsy();
+ });
+ });
+});
diff --git a/spec/frontend/feature_highlight/feature_highlight_helper_spec.js b/spec/frontend/feature_highlight/feature_highlight_helper_spec.js
new file mode 100644
index 00000000000..2c3c3e3267a
--- /dev/null
+++ b/spec/frontend/feature_highlight/feature_highlight_helper_spec.js
@@ -0,0 +1,62 @@
+import $ from 'jquery';
+import axios from '~/lib/utils/axios_utils';
+import { getSelector, dismiss, inserted } from '~/feature_highlight/feature_highlight_helper';
+import { togglePopover } from '~/shared/popover';
+
+describe('feature highlight helper', () => {
+ describe('getSelector', () => {
+ it('returns js-feature-highlight selector', () => {
+ const highlightId = 'highlightId';
+
+ expect(getSelector(highlightId)).toEqual(
+ `.js-feature-highlight[data-highlight=${highlightId}]`,
+ );
+ });
+ });
+
+ describe('dismiss', () => {
+ const context = {
+ hide: () => {},
+ attr: () => '/-/callouts/dismiss',
+ };
+
+ beforeEach(() => {
+ jest.spyOn(axios, 'post').mockResolvedValue();
+ jest.spyOn(togglePopover, 'call').mockImplementation(() => {});
+ jest.spyOn(context, 'hide').mockImplementation(() => {});
+ dismiss.call(context);
+ });
+
+ it('calls persistent dismissal endpoint', () => {
+ expect(axios.post).toHaveBeenCalledWith(
+ '/-/callouts/dismiss',
+ expect.objectContaining({ feature_name: undefined }),
+ );
+ });
+
+ it('calls hide popover', () => {
+ expect(togglePopover.call).toHaveBeenCalledWith(context, false);
+ });
+
+ it('calls hide', () => {
+ expect(context.hide).toHaveBeenCalled();
+ });
+ });
+
+ describe('inserted', () => {
+ it('registers click event callback', done => {
+ const context = {
+ getAttribute: () => 'popoverId',
+ dataset: {
+ highlight: 'some-feature',
+ },
+ };
+
+ jest.spyOn($.fn, 'on').mockImplementation(event => {
+ expect(event).toEqual('click');
+ done();
+ });
+ inserted.call(context);
+ });
+ });
+});
diff --git a/spec/frontend/feature_highlight/feature_highlight_options_spec.js b/spec/frontend/feature_highlight/feature_highlight_options_spec.js
index 8b75c46fd4c..f82f984cb7f 100644
--- a/spec/frontend/feature_highlight/feature_highlight_options_spec.js
+++ b/spec/frontend/feature_highlight/feature_highlight_options_spec.js
@@ -3,34 +3,20 @@ import domContentLoaded from '~/feature_highlight/feature_highlight_options';
describe('feature highlight options', () => {
describe('domContentLoaded', () => {
- it('should not call highlightFeatures when breakpoint is xs', () => {
- jest.spyOn(bp, 'getBreakpointSize').mockReturnValue('xs');
-
- expect(domContentLoaded()).toBe(false);
- });
-
- it('should not call highlightFeatures when breakpoint is sm', () => {
- jest.spyOn(bp, 'getBreakpointSize').mockReturnValue('sm');
-
- expect(domContentLoaded()).toBe(false);
- });
-
- it('should not call highlightFeatures when breakpoint is md', () => {
- jest.spyOn(bp, 'getBreakpointSize').mockReturnValue('md');
-
- expect(domContentLoaded()).toBe(false);
- });
-
- it('should not call highlightFeatures when breakpoint is not xl', () => {
- jest.spyOn(bp, 'getBreakpointSize').mockReturnValue('lg');
-
- expect(domContentLoaded()).toBe(false);
- });
-
- it('should call highlightFeatures when breakpoint is xl', () => {
- jest.spyOn(bp, 'getBreakpointSize').mockReturnValue('xl');
-
- expect(domContentLoaded()).toBe(true);
- });
+ it.each`
+ breakPoint | shouldCall
+ ${'xs'} | ${false}
+ ${'sm'} | ${false}
+ ${'md'} | ${false}
+ ${'lg'} | ${false}
+ ${'xl'} | ${true}
+ `(
+ 'when breakpoint is $breakPoint should call highlightFeatures is $shouldCall',
+ ({ breakPoint, shouldCall }) => {
+ jest.spyOn(bp, 'getBreakpointSize').mockReturnValue(breakPoint);
+
+ expect(domContentLoaded()).toBe(shouldCall);
+ },
+ );
});
});
diff --git a/spec/frontend/feature_highlight/feature_highlight_spec.js b/spec/frontend/feature_highlight/feature_highlight_spec.js
new file mode 100644
index 00000000000..79c4050c8c4
--- /dev/null
+++ b/spec/frontend/feature_highlight/feature_highlight_spec.js
@@ -0,0 +1,120 @@
+import $ from 'jquery';
+import MockAdapter from 'axios-mock-adapter';
+import * as featureHighlight from '~/feature_highlight/feature_highlight';
+import * as popover from '~/shared/popover';
+import axios from '~/lib/utils/axios_utils';
+
+jest.mock('~/shared/popover');
+
+describe('feature highlight', () => {
+ beforeEach(() => {
+ setFixtures(`
+ <div>
+ <div class="js-feature-highlight" data-highlight="test" data-highlight-priority="10" data-dismiss-endpoint="/test" disabled>
+ Trigger
+ </div>
+ </div>
+ <div class="feature-highlight-popover-content">
+ Content
+ <div class="dismiss-feature-highlight">
+ Dismiss
+ </div>
+ </div>
+ `);
+ });
+
+ describe('setupFeatureHighlightPopover', () => {
+ let mock;
+ const selector = '.js-feature-highlight[data-highlight=test]';
+
+ beforeEach(() => {
+ mock = new MockAdapter(axios);
+ mock.onGet('/test').reply(200);
+ jest.spyOn(window, 'addEventListener').mockImplementation(() => {});
+ featureHighlight.setupFeatureHighlightPopover('test', 0);
+ });
+
+ afterEach(() => {
+ mock.restore();
+ });
+
+ it('setup popover content', () => {
+ const $popoverContent = $('.feature-highlight-popover-content');
+ const outerHTML = $popoverContent.prop('outerHTML');
+
+ expect($(selector).data('content')).toEqual(outerHTML);
+ });
+
+ it('setup mouseenter', () => {
+ $(selector).trigger('mouseenter');
+
+ expect(popover.mouseenter).toHaveBeenCalledWith(expect.any(Object));
+ });
+
+ it('setup debounced mouseleave', () => {
+ $(selector).trigger('mouseleave');
+
+ expect(popover.debouncedMouseleave).toHaveBeenCalled();
+ });
+
+ it('setup show.bs.popover', () => {
+ $(selector).trigger('show.bs.popover');
+
+ expect(window.addEventListener).toHaveBeenCalledWith('scroll', expect.any(Function), {
+ once: true,
+ });
+ });
+
+ it('removes disabled attribute', () => {
+ expect($('.js-feature-highlight').is(':disabled')).toEqual(false);
+ });
+ });
+
+ describe('findHighestPriorityFeature', () => {
+ beforeEach(() => {
+ setFixtures(`
+ <div class="js-feature-highlight" data-highlight="test" data-highlight-priority="10" disabled></div>
+ <div class="js-feature-highlight" data-highlight="test-high-priority" data-highlight-priority="20" disabled></div>
+ <div class="js-feature-highlight" data-highlight="test-low-priority" data-highlight-priority="0" disabled></div>
+ `);
+ });
+
+ it('should pick the highest priority feature highlight', () => {
+ setFixtures(`
+ <div class="js-feature-highlight" data-highlight="test" data-highlight-priority="10" disabled></div>
+ <div class="js-feature-highlight" data-highlight="test-high-priority" data-highlight-priority="20" disabled></div>
+ <div class="js-feature-highlight" data-highlight="test-low-priority" data-highlight-priority="0" disabled></div>
+ `);
+
+ expect($('.js-feature-highlight').length).toBeGreaterThan(1);
+ expect(featureHighlight.findHighestPriorityFeature()).toEqual('test-high-priority');
+ });
+
+ it('should work when no priority is set', () => {
+ setFixtures(`
+ <div class="js-feature-highlight" data-highlight="test" disabled></div>
+ `);
+
+ expect(featureHighlight.findHighestPriorityFeature()).toEqual('test');
+ });
+
+ it('should pick the highest priority feature highlight when some have no priority set', () => {
+ setFixtures(`
+ <div class="js-feature-highlight" data-highlight="test-no-priority1" disabled></div>
+ <div class="js-feature-highlight" data-highlight="test" data-highlight-priority="10" disabled></div>
+ <div class="js-feature-highlight" data-highlight="test-no-priority2" disabled></div>
+ <div class="js-feature-highlight" data-highlight="test-high-priority" data-highlight-priority="20" disabled></div>
+ <div class="js-feature-highlight" data-highlight="test-low-priority" data-highlight-priority="0" disabled></div>
+ `);
+
+ expect($('.js-feature-highlight').length).toBeGreaterThan(1);
+ expect(featureHighlight.findHighestPriorityFeature()).toEqual('test-high-priority');
+ });
+ });
+
+ describe('highlightFeatures', () => {
+ it('calls setupFeatureHighlightPopover', () => {
+ expect(featureHighlight.highlightFeatures()).toEqual('test');
+ });
+ });
+});
diff --git a/spec/frontend/filtered_search/dropdown_utils_spec.js b/spec/frontend/filtered_search/dropdown_utils_spec.js
new file mode 100644
index 00000000000..3320b6b0942
--- /dev/null
+++ b/spec/frontend/filtered_search/dropdown_utils_spec.js
@@ -0,0 +1,374 @@
+import DropdownUtils from '~/filtered_search/dropdown_utils';
+import FilteredSearchDropdownManager from '~/filtered_search/filtered_search_dropdown_manager';
+import IssuableFilteredSearchTokenKeys from '~/filtered_search/issuable_filtered_search_token_keys';
+import FilteredSearchSpecHelper from '../helpers/filtered_search_spec_helper';
+
+describe('Dropdown Utils', () => {
+ const issueListFixture = 'issues/issue_list.html';
+ preloadFixtures(issueListFixture);
+
+ describe('getEscapedText', () => {
+ it('should return same word when it has no space', () => {
+ const escaped = DropdownUtils.getEscapedText('textWithoutSpace');
+
+ expect(escaped).toBe('textWithoutSpace');
+ });
+
+ it('should escape with double quotes', () => {
+ let escaped = DropdownUtils.getEscapedText('text with space');
+
+ expect(escaped).toBe('"text with space"');
+
+ escaped = DropdownUtils.getEscapedText("won't fix");
+
+ expect(escaped).toBe('"won\'t fix"');
+ });
+
+ it('should escape with single quotes', () => {
+ const escaped = DropdownUtils.getEscapedText('won"t fix');
+
+ expect(escaped).toBe("'won\"t fix'");
+ });
+
+ it('should escape with single quotes by default', () => {
+ const escaped = DropdownUtils.getEscapedText('won"t\' fix');
+
+ expect(escaped).toBe("'won\"t' fix'");
+ });
+ });
+
+ describe('filterWithSymbol', () => {
+ let input;
+ const item = {
+ title: '@root',
+ };
+
+ beforeEach(() => {
+ setFixtures(`
+ <input type="text" id="test" />
+ `);
+
+ input = document.getElementById('test');
+ });
+
+ it('should filter without symbol', () => {
+ input.value = 'roo';
+
+ const updatedItem = DropdownUtils.filterWithSymbol('@', input, item);
+
+ expect(updatedItem.droplab_hidden).toBe(false);
+ });
+
+ it('should filter with symbol', () => {
+ input.value = '@roo';
+
+ const updatedItem = DropdownUtils.filterWithSymbol('@', input, item);
+
+ expect(updatedItem.droplab_hidden).toBe(false);
+ });
+
+ describe('filters multiple word title', () => {
+ const multipleWordItem = {
+ title: 'Community Contributions',
+ };
+
+ it('should filter with double quote', () => {
+ input.value = '"';
+
+ const updatedItem = DropdownUtils.filterWithSymbol('~', input, multipleWordItem);
+
+ expect(updatedItem.droplab_hidden).toBe(false);
+ });
+
+ it('should filter with double quote and symbol', () => {
+ input.value = '~"';
+
+ const updatedItem = DropdownUtils.filterWithSymbol('~', input, multipleWordItem);
+
+ expect(updatedItem.droplab_hidden).toBe(false);
+ });
+
+ it('should filter with double quote and multiple words', () => {
+ input.value = '"community con';
+
+ const updatedItem = DropdownUtils.filterWithSymbol('~', input, multipleWordItem);
+
+ expect(updatedItem.droplab_hidden).toBe(false);
+ });
+
+ it('should filter with double quote, symbol and multiple words', () => {
+ input.value = '~"community con';
+
+ const updatedItem = DropdownUtils.filterWithSymbol('~', input, multipleWordItem);
+
+ expect(updatedItem.droplab_hidden).toBe(false);
+ });
+
+ it('should filter with single quote', () => {
+ input.value = "'";
+
+ const updatedItem = DropdownUtils.filterWithSymbol('~', input, multipleWordItem);
+
+ expect(updatedItem.droplab_hidden).toBe(false);
+ });
+
+ it('should filter with single quote and symbol', () => {
+ input.value = "~'";
+
+ const updatedItem = DropdownUtils.filterWithSymbol('~', input, multipleWordItem);
+
+ expect(updatedItem.droplab_hidden).toBe(false);
+ });
+
+ it('should filter with single quote and multiple words', () => {
+ input.value = "'community con";
+
+ const updatedItem = DropdownUtils.filterWithSymbol('~', input, multipleWordItem);
+
+ expect(updatedItem.droplab_hidden).toBe(false);
+ });
+
+ it('should filter with single quote, symbol and multiple words', () => {
+ input.value = "~'community con";
+
+ const updatedItem = DropdownUtils.filterWithSymbol('~', input, multipleWordItem);
+
+ expect(updatedItem.droplab_hidden).toBe(false);
+ });
+ });
+ });
+
+ describe('filterHint', () => {
+ let input;
+ let allowedKeys;
+
+ beforeEach(() => {
+ setFixtures(`
+ <ul class="tokens-container">
+ <li class="input-token">
+ <input class="filtered-search" type="text" id="test" />
+ </li>
+ </ul>
+ `);
+
+ input = document.getElementById('test');
+ allowedKeys = IssuableFilteredSearchTokenKeys.getKeys();
+ });
+
+ function config() {
+ return {
+ input,
+ allowedKeys,
+ };
+ }
+
+ it('should filter', () => {
+ input.value = 'l';
+ let updatedItem = DropdownUtils.filterHint(config(), {
+ hint: 'label',
+ });
+
+ expect(updatedItem.droplab_hidden).toBe(false);
+
+ input.value = 'o';
+ updatedItem = DropdownUtils.filterHint(config(), {
+ hint: 'label',
+ });
+
+ expect(updatedItem.droplab_hidden).toBe(true);
+ });
+
+ it('should return droplab_hidden false when item has no hint', () => {
+ const updatedItem = DropdownUtils.filterHint(config(), {}, '');
+
+ expect(updatedItem.droplab_hidden).toBe(false);
+ });
+
+ it('should allow multiple if item.type is array', () => {
+ input.value = 'label:~first la';
+ const updatedItem = DropdownUtils.filterHint(config(), {
+ hint: 'label',
+ type: 'array',
+ });
+
+ expect(updatedItem.droplab_hidden).toBe(false);
+ });
+
+ it('should prevent multiple if item.type is not array', () => {
+ input.value = 'milestone:~first mile';
+ let updatedItem = DropdownUtils.filterHint(config(), {
+ hint: 'milestone',
+ });
+
+ expect(updatedItem.droplab_hidden).toBe(true);
+
+ updatedItem = DropdownUtils.filterHint(config(), {
+ hint: 'milestone',
+ type: 'string',
+ });
+
+ expect(updatedItem.droplab_hidden).toBe(true);
+ });
+ });
+
+ describe('setDataValueIfSelected', () => {
+ beforeEach(() => {
+ jest.spyOn(FilteredSearchDropdownManager, 'addWordToInput').mockImplementation(() => {});
+ });
+
+ it('calls addWordToInput when dataValue exists', () => {
+ const selected = {
+ getAttribute: () => 'value',
+ hasAttribute: () => false,
+ };
+
+ DropdownUtils.setDataValueIfSelected(null, '=', selected);
+
+ expect(FilteredSearchDropdownManager.addWordToInput.mock.calls.length).toEqual(1);
+ });
+
+ it('returns true when dataValue exists', () => {
+ const selected = {
+ getAttribute: () => 'value',
+ hasAttribute: () => false,
+ };
+
+ const result = DropdownUtils.setDataValueIfSelected(null, '=', selected);
+ const result2 = DropdownUtils.setDataValueIfSelected(null, '!=', selected);
+
+ expect(result).toBe(true);
+ expect(result2).toBe(true);
+ });
+
+ it('returns false when dataValue does not exist', () => {
+ const selected = {
+ getAttribute: () => null,
+ };
+
+ const result = DropdownUtils.setDataValueIfSelected(null, '=', selected);
+ const result2 = DropdownUtils.setDataValueIfSelected(null, '!=', selected);
+
+ expect(result).toBe(false);
+ expect(result2).toBe(false);
+ });
+ });
+
+ describe('getInputSelectionPosition', () => {
+ describe('word with trailing spaces', () => {
+ const value = 'label:none ';
+
+ it('should return selectionStart when cursor is at the trailing space', () => {
+ const { left, right } = DropdownUtils.getInputSelectionPosition({
+ selectionStart: 11,
+ value,
+ });
+
+ expect(left).toBe(11);
+ expect(right).toBe(11);
+ });
+
+ it('should return input when cursor is at the start of input', () => {
+ const { left, right } = DropdownUtils.getInputSelectionPosition({
+ selectionStart: 0,
+ value,
+ });
+
+ expect(left).toBe(0);
+ expect(right).toBe(10);
+ });
+
+ it('should return input when cursor is at the middle of input', () => {
+ const { left, right } = DropdownUtils.getInputSelectionPosition({
+ selectionStart: 7,
+ value,
+ });
+
+ expect(left).toBe(0);
+ expect(right).toBe(10);
+ });
+
+ it('should return input when cursor is at the end of input', () => {
+ const { left, right } = DropdownUtils.getInputSelectionPosition({
+ selectionStart: 10,
+ value,
+ });
+
+ expect(left).toBe(0);
+ expect(right).toBe(10);
+ });
+ });
+
+ describe('multiple words', () => {
+ const value = 'label:~"Community Contribution"';
+
+ it('should return input when cursor is after the first word', () => {
+ const { left, right } = DropdownUtils.getInputSelectionPosition({
+ selectionStart: 17,
+ value,
+ });
+
+ expect(left).toBe(0);
+ expect(right).toBe(31);
+ });
+
+ it('should return input when cursor is before the second word', () => {
+ const { left, right } = DropdownUtils.getInputSelectionPosition({
+ selectionStart: 18,
+ value,
+ });
+
+ expect(left).toBe(0);
+ expect(right).toBe(31);
+ });
+ });
+
+ describe('incomplete multiple words', () => {
+ const value = 'label:~"Community Contribution';
+
+ it('should return entire input when cursor is at the start of input', () => {
+ const { left, right } = DropdownUtils.getInputSelectionPosition({
+ selectionStart: 0,
+ value,
+ });
+
+ expect(left).toBe(0);
+ expect(right).toBe(30);
+ });
+
+ it('should return entire input when cursor is at the end of input', () => {
+ const { left, right } = DropdownUtils.getInputSelectionPosition({
+ selectionStart: 30,
+ value,
+ });
+
+ expect(left).toBe(0);
+ expect(right).toBe(30);
+ });
+ });
+ });
+
+ describe('getSearchQuery', () => {
+ let authorToken;
+
+ beforeEach(() => {
+ loadFixtures(issueListFixture);
+
+ authorToken = FilteredSearchSpecHelper.createFilterVisualToken('author', '=', '@user');
+ const searchTermToken = FilteredSearchSpecHelper.createSearchVisualToken('search term');
+
+ const tokensContainer = document.querySelector('.tokens-container');
+ tokensContainer.appendChild(searchTermToken);
+ tokensContainer.appendChild(authorToken);
+ });
+
+ it('uses original value if present', () => {
+ const originalValue = 'original dance';
+ const valueContainer = authorToken.querySelector('.value-container');
+ valueContainer.dataset.originalValue = originalValue;
+
+ const searchQuery = DropdownUtils.getSearchQuery();
+
+ expect(searchQuery).toBe(' search term author:=original dance');
+ });
+ });
+});
diff --git a/spec/frontend/filtered_search/filtered_search_manager_spec.js b/spec/frontend/filtered_search/filtered_search_manager_spec.js
new file mode 100644
index 00000000000..ef87662a1ef
--- /dev/null
+++ b/spec/frontend/filtered_search/filtered_search_manager_spec.js
@@ -0,0 +1,587 @@
+import RecentSearchesService from '~/filtered_search/services/recent_searches_service';
+import RecentSearchesServiceError from '~/filtered_search/services/recent_searches_service_error';
+import RecentSearchesRoot from '~/filtered_search/recent_searches_root';
+import IssuableFilteredSearchTokenKeys from '~/filtered_search/issuable_filtered_search_token_keys';
+import '~/lib/utils/common_utils';
+import DropdownUtils from '~/filtered_search/dropdown_utils';
+import FilteredSearchVisualTokens from '~/filtered_search/filtered_search_visual_tokens';
+import FilteredSearchDropdownManager from '~/filtered_search/filtered_search_dropdown_manager';
+import FilteredSearchManager from '~/filtered_search/filtered_search_manager';
+import FilteredSearchSpecHelper from '../helpers/filtered_search_spec_helper';
+import { BACKSPACE_KEY_CODE, DELETE_KEY_CODE } from '~/lib/utils/keycodes';
+import { visitUrl } from '~/lib/utils/url_utility';
+
+jest.mock('~/lib/utils/url_utility', () => ({
+ ...jest.requireActual('~/lib/utils/url_utility'),
+ visitUrl: jest.fn(),
+}));
+
+describe('Filtered Search Manager', () => {
+ let input;
+ let manager;
+ let tokensContainer;
+ const page = 'issues';
+ const placeholder = 'Search or filter results...';
+
+ function dispatchBackspaceEvent(element, eventType) {
+ const event = new Event(eventType);
+ event.keyCode = BACKSPACE_KEY_CODE;
+ element.dispatchEvent(event);
+ }
+
+ function dispatchDeleteEvent(element, eventType) {
+ const event = new Event(eventType);
+ event.keyCode = DELETE_KEY_CODE;
+ element.dispatchEvent(event);
+ }
+
+ function dispatchAltBackspaceEvent(element, eventType) {
+ const event = new Event(eventType);
+ event.altKey = true;
+ event.keyCode = BACKSPACE_KEY_CODE;
+ element.dispatchEvent(event);
+ }
+
+ function dispatchCtrlBackspaceEvent(element, eventType) {
+ const event = new Event(eventType);
+ event.ctrlKey = true;
+ event.keyCode = BACKSPACE_KEY_CODE;
+ element.dispatchEvent(event);
+ }
+
+ function dispatchMetaBackspaceEvent(element, eventType) {
+ const event = new Event(eventType);
+ event.metaKey = true;
+ event.keyCode = BACKSPACE_KEY_CODE;
+ element.dispatchEvent(event);
+ }
+
+ function getVisualTokens() {
+ return tokensContainer.querySelectorAll('.js-visual-token');
+ }
+
+ beforeEach(() => {
+ setFixtures(`
+ <div class="filtered-search-box">
+ <form>
+ <ul class="tokens-container list-unstyled">
+ ${FilteredSearchSpecHelper.createInputHTML(placeholder)}
+ </ul>
+ <button class="clear-search" type="button">
+ <i class="fa fa-times"></i>
+ </button>
+ </form>
+ </div>
+ `);
+
+ jest.spyOn(FilteredSearchDropdownManager.prototype, 'setDropdown').mockImplementation();
+ });
+
+ const initializeManager = () => {
+ jest.spyOn(FilteredSearchManager.prototype, 'loadSearchParamsFromURL').mockImplementation();
+ jest.spyOn(FilteredSearchManager.prototype, 'tokenChange').mockImplementation();
+ jest
+ .spyOn(FilteredSearchDropdownManager.prototype, 'updateDropdownOffset')
+ .mockImplementation();
+ jest.spyOn(gl.utils, 'getParameterByName').mockReturnValue(null);
+ jest.spyOn(FilteredSearchVisualTokens, 'unselectTokens');
+
+ input = document.querySelector('.filtered-search');
+ tokensContainer = document.querySelector('.tokens-container');
+ manager = new FilteredSearchManager({ page });
+ manager.setup();
+ };
+
+ afterEach(() => {
+ manager.cleanup();
+ });
+
+ describe('class constructor', () => {
+ const isLocalStorageAvailable = 'isLocalStorageAvailable';
+
+ beforeEach(() => {
+ jest.spyOn(RecentSearchesService, 'isAvailable').mockReturnValue(isLocalStorageAvailable);
+ jest.spyOn(RecentSearchesRoot.prototype, 'render').mockImplementation();
+ });
+
+ it('should instantiate RecentSearchesStore with isLocalStorageAvailable', () => {
+ manager = new FilteredSearchManager({ page });
+
+ expect(RecentSearchesService.isAvailable).toHaveBeenCalled();
+ expect(manager.recentSearchesStore.state).toEqual(
+ expect.objectContaining({
+ isLocalStorageAvailable,
+ allowedKeys: IssuableFilteredSearchTokenKeys.getKeys(),
+ }),
+ );
+ });
+ });
+
+ describe('setup', () => {
+ beforeEach(() => {
+ manager = new FilteredSearchManager({ page });
+ });
+
+ it('should not instantiate Flash if an RecentSearchesServiceError is caught', () => {
+ jest
+ .spyOn(RecentSearchesService.prototype, 'fetch')
+ .mockImplementation(() => Promise.reject(new RecentSearchesServiceError()));
+ jest.spyOn(window, 'Flash').mockImplementation();
+
+ manager.setup();
+
+ expect(window.Flash).not.toHaveBeenCalled();
+ });
+ });
+
+ describe('searchState', () => {
+ beforeEach(() => {
+ jest.spyOn(FilteredSearchManager.prototype, 'search').mockImplementation();
+ initializeManager();
+ });
+
+ it('should blur button', () => {
+ const e = {
+ preventDefault: () => {},
+ currentTarget: {
+ blur: () => {},
+ },
+ };
+ jest.spyOn(e.currentTarget, 'blur');
+ manager.searchState(e);
+
+ expect(e.currentTarget.blur).toHaveBeenCalled();
+ });
+
+ it('should not call search if there is no state', () => {
+ const e = {
+ preventDefault: () => {},
+ currentTarget: {
+ blur: () => {},
+ },
+ };
+
+ manager.searchState(e);
+
+ expect(FilteredSearchManager.prototype.search).not.toHaveBeenCalled();
+ });
+
+ it('should call search when there is state', () => {
+ const e = {
+ preventDefault: () => {},
+ currentTarget: {
+ blur: () => {},
+ dataset: {
+ state: 'opened',
+ },
+ },
+ };
+
+ manager.searchState(e);
+
+ expect(FilteredSearchManager.prototype.search).toHaveBeenCalledWith('opened');
+ });
+ });
+
+ describe('search', () => {
+ const defaultParams = '?scope=all&utf8=%E2%9C%93&state=opened';
+
+ beforeEach(() => {
+ initializeManager();
+ });
+
+ it('should search with a single word', done => {
+ input.value = 'searchTerm';
+
+ visitUrl.mockImplementation(url => {
+ expect(url).toEqual(`${defaultParams}&search=searchTerm`);
+ done();
+ });
+
+ manager.search();
+ });
+
+ it('should search with multiple words', done => {
+ input.value = 'awesome search terms';
+
+ visitUrl.mockImplementation(url => {
+ expect(url).toEqual(`${defaultParams}&search=awesome+search+terms`);
+ done();
+ });
+
+ manager.search();
+ });
+
+ it('should search with special characters', done => {
+ input.value = '~!@#$%^&*()_+{}:<>,.?/';
+
+ visitUrl.mockImplementation(url => {
+ expect(url).toEqual(
+ `${defaultParams}&search=~!%40%23%24%25%5E%26*()_%2B%7B%7D%3A%3C%3E%2C.%3F%2F`,
+ );
+ done();
+ });
+
+ manager.search();
+ });
+
+ it('removes duplicated tokens', done => {
+ tokensContainer.innerHTML = FilteredSearchSpecHelper.createTokensContainerHTML(`
+ ${FilteredSearchSpecHelper.createFilterVisualTokenHTML('label', '=', '~bug')}
+ ${FilteredSearchSpecHelper.createFilterVisualTokenHTML('label', '=', '~bug')}
+ `);
+
+ visitUrl.mockImplementation(url => {
+ expect(url).toEqual(`${defaultParams}&label_name[]=bug`);
+ done();
+ });
+
+ manager.search();
+ });
+ });
+
+ describe('handleInputPlaceholder', () => {
+ beforeEach(() => {
+ initializeManager();
+ });
+
+ it('should render placeholder when there is no input', () => {
+ expect(input.placeholder).toEqual(placeholder);
+ });
+
+ it('should not render placeholder when there is input', () => {
+ input.value = 'test words';
+
+ const event = new Event('input');
+ input.dispatchEvent(event);
+
+ expect(input.placeholder).toEqual('');
+ });
+
+ it('should not render placeholder when there are tokens and no input', () => {
+ tokensContainer.innerHTML = FilteredSearchSpecHelper.createTokensContainerHTML(
+ FilteredSearchSpecHelper.createFilterVisualTokenHTML('label', '=', '~bug'),
+ );
+
+ const event = new Event('input');
+ input.dispatchEvent(event);
+
+ expect(input.placeholder).toEqual('');
+ });
+ });
+
+ describe('checkForBackspace', () => {
+ beforeEach(() => {
+ initializeManager();
+ });
+
+ describe('tokens and no input', () => {
+ beforeEach(() => {
+ tokensContainer.innerHTML = FilteredSearchSpecHelper.createTokensContainerHTML(
+ FilteredSearchSpecHelper.createFilterVisualTokenHTML('label', '=', '~bug'),
+ );
+ });
+
+ it('removes last token', () => {
+ jest.spyOn(FilteredSearchVisualTokens, 'removeLastTokenPartial');
+ dispatchBackspaceEvent(input, 'keyup');
+ dispatchBackspaceEvent(input, 'keyup');
+
+ expect(FilteredSearchVisualTokens.removeLastTokenPartial).toHaveBeenCalled();
+ });
+
+ it('sets the input', () => {
+ jest.spyOn(FilteredSearchVisualTokens, 'getLastTokenPartial');
+ dispatchDeleteEvent(input, 'keyup');
+ dispatchDeleteEvent(input, 'keyup');
+
+ expect(FilteredSearchVisualTokens.getLastTokenPartial).toHaveBeenCalled();
+ expect(input.value).toEqual('~bug');
+ });
+ });
+
+ it('does not remove token or change input when there is existing input', () => {
+ jest.spyOn(FilteredSearchVisualTokens, 'removeLastTokenPartial');
+ jest.spyOn(FilteredSearchVisualTokens, 'getLastTokenPartial');
+
+ input.value = 'text';
+ dispatchDeleteEvent(input, 'keyup');
+
+ expect(FilteredSearchVisualTokens.removeLastTokenPartial).not.toHaveBeenCalled();
+ expect(FilteredSearchVisualTokens.getLastTokenPartial).not.toHaveBeenCalled();
+ expect(input.value).toEqual('text');
+ });
+
+ it('does not remove previous token on single backspace press', () => {
+ jest.spyOn(FilteredSearchVisualTokens, 'removeLastTokenPartial');
+ jest.spyOn(FilteredSearchVisualTokens, 'getLastTokenPartial');
+
+ input.value = 't';
+ dispatchDeleteEvent(input, 'keyup');
+
+ expect(FilteredSearchVisualTokens.removeLastTokenPartial).not.toHaveBeenCalled();
+ expect(FilteredSearchVisualTokens.getLastTokenPartial).not.toHaveBeenCalled();
+ expect(input.value).toEqual('t');
+ });
+ });
+
+ describe('checkForAltOrCtrlBackspace', () => {
+ beforeEach(() => {
+ initializeManager();
+ jest.spyOn(FilteredSearchVisualTokens, 'removeLastTokenPartial');
+ });
+
+ describe('tokens and no input', () => {
+ beforeEach(() => {
+ tokensContainer.innerHTML = FilteredSearchSpecHelper.createTokensContainerHTML(
+ FilteredSearchSpecHelper.createFilterVisualTokenHTML('label', '=', '~bug'),
+ );
+ });
+
+ it('removes last token via alt-backspace', () => {
+ dispatchAltBackspaceEvent(input, 'keydown');
+
+ expect(FilteredSearchVisualTokens.removeLastTokenPartial).toHaveBeenCalled();
+ });
+
+ it('removes last token via ctrl-backspace', () => {
+ dispatchCtrlBackspaceEvent(input, 'keydown');
+
+ expect(FilteredSearchVisualTokens.removeLastTokenPartial).toHaveBeenCalled();
+ });
+ });
+
+ describe('tokens and input', () => {
+ beforeEach(() => {
+ tokensContainer.innerHTML = FilteredSearchSpecHelper.createTokensContainerHTML(
+ FilteredSearchSpecHelper.createFilterVisualTokenHTML('label', '=', '~bug'),
+ );
+ });
+
+ it('does not remove token or change input via alt-backspace when there is existing input', () => {
+ input = manager.filteredSearchInput;
+ input.value = 'text';
+ dispatchAltBackspaceEvent(input, 'keydown');
+
+ expect(FilteredSearchVisualTokens.removeLastTokenPartial).not.toHaveBeenCalled();
+ expect(input.value).toEqual('text');
+ });
+
+ it('does not remove token or change input via ctrl-backspace when there is existing input', () => {
+ input = manager.filteredSearchInput;
+ input.value = 'text';
+ dispatchCtrlBackspaceEvent(input, 'keydown');
+
+ expect(FilteredSearchVisualTokens.removeLastTokenPartial).not.toHaveBeenCalled();
+ expect(input.value).toEqual('text');
+ });
+ });
+ });
+
+ describe('checkForMetaBackspace', () => {
+ beforeEach(() => {
+ initializeManager();
+ });
+
+ beforeEach(() => {
+ tokensContainer.innerHTML = FilteredSearchSpecHelper.createTokensContainerHTML(
+ FilteredSearchSpecHelper.createFilterVisualTokenHTML('label', '=', '~bug'),
+ );
+ });
+
+ it('removes all tokens and input', () => {
+ jest.spyOn(FilteredSearchManager.prototype, 'clearSearch');
+ dispatchMetaBackspaceEvent(input, 'keydown');
+
+ expect(manager.clearSearch).toHaveBeenCalled();
+ expect(manager.filteredSearchInput.value).toEqual('');
+ expect(DropdownUtils.getSearchQuery()).toEqual('');
+ });
+ });
+
+ describe('removeToken', () => {
+ beforeEach(() => {
+ initializeManager();
+ });
+
+ it('removes token even when it is already selected', () => {
+ tokensContainer.innerHTML = FilteredSearchSpecHelper.createTokensContainerHTML(
+ FilteredSearchSpecHelper.createFilterVisualTokenHTML('milestone', '=', 'none', true),
+ );
+
+ tokensContainer.querySelector('.js-visual-token .remove-token').click();
+
+ expect(tokensContainer.querySelector('.js-visual-token')).toEqual(null);
+ });
+
+ describe('unselected token', () => {
+ beforeEach(() => {
+ jest.spyOn(FilteredSearchManager.prototype, 'removeSelectedToken');
+
+ tokensContainer.innerHTML = FilteredSearchSpecHelper.createTokensContainerHTML(
+ FilteredSearchSpecHelper.createFilterVisualTokenHTML('milestone', '=', 'none'),
+ );
+ tokensContainer.querySelector('.js-visual-token .remove-token').click();
+ });
+
+ it('removes token when remove button is selected', () => {
+ expect(tokensContainer.querySelector('.js-visual-token')).toEqual(null);
+ });
+
+ it('calls removeSelectedToken', () => {
+ expect(manager.removeSelectedToken).toHaveBeenCalled();
+ });
+ });
+ });
+
+ describe('removeSelectedTokenKeydown', () => {
+ beforeEach(() => {
+ initializeManager();
+ tokensContainer.innerHTML = FilteredSearchSpecHelper.createTokensContainerHTML(
+ FilteredSearchSpecHelper.createFilterVisualTokenHTML('milestone', '=', 'none', true),
+ );
+ });
+
+ it('removes selected token when the backspace key is pressed', () => {
+ expect(getVisualTokens().length).toEqual(1);
+
+ dispatchBackspaceEvent(document, 'keydown');
+
+ expect(getVisualTokens().length).toEqual(0);
+ });
+
+ it('removes selected token when the delete key is pressed', () => {
+ expect(getVisualTokens().length).toEqual(1);
+
+ dispatchDeleteEvent(document, 'keydown');
+
+ expect(getVisualTokens().length).toEqual(0);
+ });
+
+ it('updates the input placeholder after removal', () => {
+ manager.handleInputPlaceholder();
+
+ expect(input.placeholder).toEqual('');
+ expect(getVisualTokens().length).toEqual(1);
+
+ dispatchBackspaceEvent(document, 'keydown');
+
+ expect(input.placeholder).not.toEqual('');
+ expect(getVisualTokens().length).toEqual(0);
+ });
+
+ it('updates the clear button after removal', () => {
+ manager.toggleClearSearchButton();
+
+ const clearButton = document.querySelector('.clear-search');
+
+ expect(clearButton.classList.contains('hidden')).toEqual(false);
+ expect(getVisualTokens().length).toEqual(1);
+
+ dispatchBackspaceEvent(document, 'keydown');
+
+ expect(clearButton.classList.contains('hidden')).toEqual(true);
+ expect(getVisualTokens().length).toEqual(0);
+ });
+ });
+
+ describe('removeSelectedToken', () => {
+ beforeEach(() => {
+ jest.spyOn(FilteredSearchVisualTokens, 'removeSelectedToken');
+ jest.spyOn(FilteredSearchManager.prototype, 'handleInputPlaceholder');
+ jest.spyOn(FilteredSearchManager.prototype, 'toggleClearSearchButton');
+ initializeManager();
+ });
+
+ it('calls FilteredSearchVisualTokens.removeSelectedToken', () => {
+ manager.removeSelectedToken();
+
+ expect(FilteredSearchVisualTokens.removeSelectedToken).toHaveBeenCalled();
+ });
+
+ it('calls handleInputPlaceholder', () => {
+ manager.removeSelectedToken();
+
+ expect(manager.handleInputPlaceholder).toHaveBeenCalled();
+ });
+
+ it('calls toggleClearSearchButton', () => {
+ manager.removeSelectedToken();
+
+ expect(manager.toggleClearSearchButton).toHaveBeenCalled();
+ });
+
+ it('calls update dropdown offset', () => {
+ manager.removeSelectedToken();
+
+ expect(manager.dropdownManager.updateDropdownOffset).toHaveBeenCalled();
+ });
+ });
+
+ describe('Clearing search', () => {
+ beforeEach(() => {
+ initializeManager();
+ });
+
+ it('Clicking the "x" clear button, clears the input', () => {
+ const inputValue = 'label:=~bug';
+ manager.filteredSearchInput.value = inputValue;
+ manager.filteredSearchInput.dispatchEvent(new Event('input'));
+
+ expect(DropdownUtils.getSearchQuery()).toEqual(inputValue);
+
+ manager.clearSearchButton.click();
+
+ expect(manager.filteredSearchInput.value).toEqual('');
+ expect(DropdownUtils.getSearchQuery()).toEqual('');
+ });
+ });
+
+ describe('toggleInputContainerFocus', () => {
+ beforeEach(() => {
+ initializeManager();
+ });
+
+ it('toggles on focus', () => {
+ input.focus();
+
+ expect(document.querySelector('.filtered-search-box').classList.contains('focus')).toEqual(
+ true,
+ );
+ });
+
+ it('toggles on blur', () => {
+ input.blur();
+
+ expect(document.querySelector('.filtered-search-box').classList.contains('focus')).toEqual(
+ false,
+ );
+ });
+ });
+
+ describe('getAllParams', () => {
+ let paramsArr;
+ beforeEach(() => {
+ paramsArr = ['key=value', 'otherkey=othervalue'];
+
+ initializeManager();
+ });
+
+ it('correctly modifies params when custom modifier is passed', () => {
+ const modifedParams = manager.getAllParams.call(
+ {
+ modifyUrlParams: params => params.reverse(),
+ },
+ [].concat(paramsArr),
+ );
+
+ expect(modifedParams[0]).toBe(paramsArr[1]);
+ });
+
+ it('does not modify params when no custom modifier is passed', () => {
+ const modifedParams = manager.getAllParams.call({}, paramsArr);
+
+ expect(modifedParams[1]).toBe(paramsArr[1]);
+ });
+ });
+});
diff --git a/spec/javascripts/filtered_search/filtered_search_tokenizer_spec.js b/spec/frontend/filtered_search/filtered_search_tokenizer_spec.js
index dec03e5ab93..dec03e5ab93 100644
--- a/spec/javascripts/filtered_search/filtered_search_tokenizer_spec.js
+++ b/spec/frontend/filtered_search/filtered_search_tokenizer_spec.js
diff --git a/spec/javascripts/filtered_search/issues_filtered_search_token_keys_spec.js b/spec/frontend/filtered_search/issues_filtered_search_token_keys_spec.js
index c7be900ba2c..c7be900ba2c 100644
--- a/spec/javascripts/filtered_search/issues_filtered_search_token_keys_spec.js
+++ b/spec/frontend/filtered_search/issues_filtered_search_token_keys_spec.js
diff --git a/spec/frontend/filtered_search/recent_searches_root_spec.js b/spec/frontend/filtered_search/recent_searches_root_spec.js
new file mode 100644
index 00000000000..281d406e013
--- /dev/null
+++ b/spec/frontend/filtered_search/recent_searches_root_spec.js
@@ -0,0 +1,32 @@
+import Vue from 'vue';
+import RecentSearchesRoot from '~/filtered_search/recent_searches_root';
+
+jest.mock('vue');
+
+describe('RecentSearchesRoot', () => {
+ describe('render', () => {
+ let recentSearchesRoot;
+ let data;
+ let template;
+
+ beforeEach(() => {
+ recentSearchesRoot = {
+ store: {
+ state: 'state',
+ },
+ };
+
+ Vue.mockImplementation(options => {
+ ({ data, template } = options);
+ });
+
+ RecentSearchesRoot.prototype.render.call(recentSearchesRoot);
+ });
+
+ it('should instantiate Vue', () => {
+ expect(Vue).toHaveBeenCalled();
+ expect(data()).toBe(recentSearchesRoot.store.state);
+ expect(template).toContain(':is-local-storage-available="isLocalStorageAvailable"');
+ });
+ });
+});
diff --git a/spec/frontend/filtered_search/services/recent_searches_service_spec.js b/spec/frontend/filtered_search/services/recent_searches_service_spec.js
new file mode 100644
index 00000000000..a89d38b7a20
--- /dev/null
+++ b/spec/frontend/filtered_search/services/recent_searches_service_spec.js
@@ -0,0 +1,161 @@
+import RecentSearchesService from '~/filtered_search/services/recent_searches_service';
+import RecentSearchesServiceError from '~/filtered_search/services/recent_searches_service_error';
+import AccessorUtilities from '~/lib/utils/accessor';
+import { useLocalStorageSpy } from 'helpers/local_storage_helper';
+
+useLocalStorageSpy();
+
+describe('RecentSearchesService', () => {
+ let service;
+
+ beforeEach(() => {
+ service = new RecentSearchesService();
+ localStorage.removeItem(service.localStorageKey);
+ });
+
+ describe('fetch', () => {
+ beforeEach(() => {
+ jest.spyOn(RecentSearchesService, 'isAvailable').mockReturnValue(true);
+ });
+
+ it('should default to empty array', done => {
+ const fetchItemsPromise = service.fetch();
+
+ fetchItemsPromise
+ .then(items => {
+ expect(items).toEqual([]);
+ })
+ .then(done)
+ .catch(done.fail);
+ });
+
+ it('should reject when unable to parse', done => {
+ jest.spyOn(localStorage, 'getItem').mockReturnValue('fail');
+ const fetchItemsPromise = service.fetch();
+
+ fetchItemsPromise
+ .then(done.fail)
+ .catch(error => {
+ expect(error).toEqual(expect.any(SyntaxError));
+ })
+ .then(done)
+ .catch(done.fail);
+ });
+
+ it('should reject when service is unavailable', done => {
+ RecentSearchesService.isAvailable.mockReturnValue(false);
+
+ service
+ .fetch()
+ .then(done.fail)
+ .catch(error => {
+ expect(error).toEqual(expect.any(Error));
+ })
+ .then(done)
+ .catch(done.fail);
+ });
+
+ it('should return items from localStorage', done => {
+ jest.spyOn(localStorage, 'getItem').mockReturnValue('["foo", "bar"]');
+ const fetchItemsPromise = service.fetch();
+
+ fetchItemsPromise
+ .then(items => {
+ expect(items).toEqual(['foo', 'bar']);
+ })
+ .then(done)
+ .catch(done.fail);
+ });
+
+ describe('if .isAvailable returns `false`', () => {
+ beforeEach(() => {
+ RecentSearchesService.isAvailable.mockReturnValue(false);
+
+ jest.spyOn(Storage.prototype, 'getItem').mockImplementation(() => {});
+ });
+
+ it('should not call .getItem', done => {
+ RecentSearchesService.prototype
+ .fetch()
+ .then(done.fail)
+ .catch(err => {
+ expect(err).toEqual(new RecentSearchesServiceError());
+ expect(localStorage.getItem).not.toHaveBeenCalled();
+ })
+ .then(done)
+ .catch(done.fail);
+ });
+ });
+ });
+
+ describe('setRecentSearches', () => {
+ beforeEach(() => {
+ jest.spyOn(RecentSearchesService, 'isAvailable').mockReturnValue(true);
+ });
+
+ it('should save things in localStorage', () => {
+ jest.spyOn(localStorage, 'setItem');
+ const items = ['foo', 'bar'];
+ service.save(items);
+
+ expect(localStorage.setItem).toHaveBeenCalledWith(expect.any(String), JSON.stringify(items));
+ });
+ });
+
+ describe('save', () => {
+ beforeEach(() => {
+ jest.spyOn(localStorage, 'setItem');
+ jest.spyOn(RecentSearchesService, 'isAvailable').mockImplementation(() => {});
+ });
+
+ describe('if .isAvailable returns `true`', () => {
+ const searchesString = 'searchesString';
+ const localStorageKey = 'localStorageKey';
+ const recentSearchesService = {
+ localStorageKey,
+ };
+
+ beforeEach(() => {
+ RecentSearchesService.isAvailable.mockReturnValue(true);
+
+ jest.spyOn(JSON, 'stringify').mockReturnValue(searchesString);
+ });
+
+ it('should call .setItem', () => {
+ RecentSearchesService.prototype.save.call(recentSearchesService);
+
+ expect(localStorage.setItem).toHaveBeenCalledWith(localStorageKey, searchesString);
+ });
+ });
+
+ describe('if .isAvailable returns `false`', () => {
+ beforeEach(() => {
+ RecentSearchesService.isAvailable.mockReturnValue(false);
+ });
+
+ it('should not call .setItem', () => {
+ RecentSearchesService.prototype.save();
+
+ expect(localStorage.setItem).not.toHaveBeenCalled();
+ });
+ });
+ });
+
+ describe('isAvailable', () => {
+ let isAvailable;
+
+ beforeEach(() => {
+ jest.spyOn(AccessorUtilities, 'isLocalStorageAccessSafe');
+
+ isAvailable = RecentSearchesService.isAvailable();
+ });
+
+ it('should call .isLocalStorageAccessSafe', () => {
+ expect(AccessorUtilities.isLocalStorageAccessSafe).toHaveBeenCalled();
+ });
+
+ it('should return a boolean', () => {
+ expect(typeof isAvailable).toBe('boolean');
+ });
+ });
+});
diff --git a/spec/frontend/filtered_search/visual_token_value_spec.js b/spec/frontend/filtered_search/visual_token_value_spec.js
new file mode 100644
index 00000000000..ea501423403
--- /dev/null
+++ b/spec/frontend/filtered_search/visual_token_value_spec.js
@@ -0,0 +1,389 @@
+import { escape } from 'lodash';
+import VisualTokenValue from '~/filtered_search/visual_token_value';
+import AjaxCache from '~/lib/utils/ajax_cache';
+import UsersCache from '~/lib/utils/users_cache';
+import DropdownUtils from '~/filtered_search//dropdown_utils';
+import FilteredSearchSpecHelper from '../helpers/filtered_search_spec_helper';
+
+describe('Filtered Search Visual Tokens', () => {
+ const findElements = tokenElement => {
+ const tokenNameElement = tokenElement.querySelector('.name');
+ const tokenValueContainer = tokenElement.querySelector('.value-container');
+ const tokenValueElement = tokenValueContainer.querySelector('.value');
+ const tokenOperatorElement = tokenElement.querySelector('.operator');
+ const tokenType = tokenNameElement.innerText.toLowerCase();
+ const tokenValue = tokenValueElement.innerText;
+ const tokenOperator = tokenOperatorElement.innerText;
+ const subject = new VisualTokenValue(tokenValue, tokenType, tokenOperator);
+ return { subject, tokenValueContainer, tokenValueElement };
+ };
+
+ let tokensContainer;
+ let authorToken;
+ let bugLabelToken;
+
+ beforeEach(() => {
+ setFixtures(`
+ <ul class="tokens-container">
+ ${FilteredSearchSpecHelper.createInputHTML()}
+ </ul>
+ `);
+ tokensContainer = document.querySelector('.tokens-container');
+
+ authorToken = FilteredSearchSpecHelper.createFilterVisualToken('author', '=', '@user');
+ bugLabelToken = FilteredSearchSpecHelper.createFilterVisualToken('label', '=', '~bug');
+ });
+
+ describe('updateUserTokenAppearance', () => {
+ let usersCacheSpy;
+
+ beforeEach(() => {
+ jest.spyOn(UsersCache, 'retrieve').mockImplementation(username => usersCacheSpy(username));
+ });
+
+ it('ignores error if UsersCache throws', done => {
+ jest.spyOn(window, 'Flash').mockImplementation(() => {});
+ const dummyError = new Error('Earth rotated backwards');
+ const { subject, tokenValueContainer, tokenValueElement } = findElements(authorToken);
+ const tokenValue = tokenValueElement.innerText;
+ usersCacheSpy = username => {
+ expect(`@${username}`).toBe(tokenValue);
+ return Promise.reject(dummyError);
+ };
+
+ subject
+ .updateUserTokenAppearance(tokenValueContainer, tokenValueElement, tokenValue)
+ .then(() => {
+ expect(window.Flash.mock.calls.length).toBe(0);
+ })
+ .then(done)
+ .catch(done.fail);
+ });
+
+ it('does nothing if user cannot be found', done => {
+ const { subject, tokenValueContainer, tokenValueElement } = findElements(authorToken);
+ const tokenValue = tokenValueElement.innerText;
+ usersCacheSpy = username => {
+ expect(`@${username}`).toBe(tokenValue);
+ return Promise.resolve(undefined);
+ };
+
+ subject
+ .updateUserTokenAppearance(tokenValueContainer, tokenValueElement, tokenValue)
+ .then(() => {
+ expect(tokenValueElement.innerText).toBe(tokenValue);
+ })
+ .then(done)
+ .catch(done.fail);
+ });
+
+ it('replaces author token with avatar and display name', done => {
+ const dummyUser = {
+ name: 'Important Person',
+ avatar_url: 'https://host.invalid/mypics/avatar.png',
+ };
+ const { subject, tokenValueContainer, tokenValueElement } = findElements(authorToken);
+ const tokenValue = tokenValueElement.innerText;
+ usersCacheSpy = username => {
+ expect(`@${username}`).toBe(tokenValue);
+ return Promise.resolve(dummyUser);
+ };
+
+ subject
+ .updateUserTokenAppearance(tokenValueContainer, tokenValueElement, tokenValue)
+ .then(() => {
+ expect(tokenValueContainer.dataset.originalValue).toBe(tokenValue);
+ expect(tokenValueElement.innerText.trim()).toBe(dummyUser.name);
+ const avatar = tokenValueElement.querySelector('img.avatar');
+
+ expect(avatar.getAttribute('src')).toBe(dummyUser.avatar_url);
+ expect(avatar.getAttribute('alt')).toBe('');
+ })
+ .then(done)
+ .catch(done.fail);
+ });
+
+ it('escapes user name when creating token', done => {
+ const dummyUser = {
+ name: '<script>',
+ avatar_url: `${gl.TEST_HOST}/mypics/avatar.png`,
+ };
+ const { subject, tokenValueContainer, tokenValueElement } = findElements(authorToken);
+ const tokenValue = tokenValueElement.innerText;
+ usersCacheSpy = username => {
+ expect(`@${username}`).toBe(tokenValue);
+ return Promise.resolve(dummyUser);
+ };
+
+ subject
+ .updateUserTokenAppearance(tokenValueContainer, tokenValueElement, tokenValue)
+ .then(() => {
+ expect(tokenValueElement.innerText.trim()).toBe(dummyUser.name);
+ tokenValueElement.querySelector('.avatar').remove();
+
+ expect(tokenValueElement.innerHTML.trim()).toBe(escape(dummyUser.name));
+ })
+ .then(done)
+ .catch(done.fail);
+ });
+ });
+
+ describe('updateLabelTokenColor', () => {
+ const jsonFixtureName = 'labels/project_labels.json';
+ const dummyEndpoint = '/dummy/endpoint';
+
+ preloadFixtures(jsonFixtureName);
+
+ let labelData;
+
+ beforeAll(() => {
+ labelData = getJSONFixture(jsonFixtureName);
+ });
+
+ const missingLabelToken = FilteredSearchSpecHelper.createFilterVisualToken(
+ 'label',
+ '=',
+ '~doesnotexist',
+ );
+ const spaceLabelToken = FilteredSearchSpecHelper.createFilterVisualToken(
+ 'label',
+ '=',
+ '~"some space"',
+ );
+
+ beforeEach(() => {
+ tokensContainer.innerHTML = FilteredSearchSpecHelper.createTokensContainerHTML(`
+ ${bugLabelToken.outerHTML}
+ ${missingLabelToken.outerHTML}
+ ${spaceLabelToken.outerHTML}
+ `);
+
+ const filteredSearchInput = document.querySelector('.filtered-search');
+ filteredSearchInput.dataset.runnerTagsEndpoint = `${dummyEndpoint}/admin/runners/tag_list`;
+ filteredSearchInput.dataset.labelsEndpoint = `${dummyEndpoint}/-/labels`;
+ filteredSearchInput.dataset.milestonesEndpoint = `${dummyEndpoint}/-/milestones`;
+
+ AjaxCache.internalStorage = {};
+ AjaxCache.internalStorage[`${filteredSearchInput.dataset.labelsEndpoint}.json`] = labelData;
+ });
+
+ const parseColor = color => {
+ const dummyElement = document.createElement('div');
+ dummyElement.style.color = color;
+ return dummyElement.style.color;
+ };
+
+ const expectValueContainerStyle = (tokenValueContainer, label) => {
+ expect(tokenValueContainer.getAttribute('style')).not.toBe(null);
+ expect(tokenValueContainer.style.backgroundColor).toBe(parseColor(label.color));
+ expect(tokenValueContainer.style.color).toBe(parseColor(label.text_color));
+ };
+
+ const findLabel = tokenValue =>
+ labelData.find(label => tokenValue === `~${DropdownUtils.getEscapedText(label.title)}`);
+
+ it('updates the color of a label token', done => {
+ const { subject, tokenValueContainer, tokenValueElement } = findElements(bugLabelToken);
+ const tokenValue = tokenValueElement.innerText;
+ const matchingLabel = findLabel(tokenValue);
+
+ subject
+ .updateLabelTokenColor(tokenValueContainer, tokenValue)
+ .then(() => {
+ expectValueContainerStyle(tokenValueContainer, matchingLabel);
+ })
+ .then(done)
+ .catch(done.fail);
+ });
+
+ it('updates the color of a label token with spaces', done => {
+ const { subject, tokenValueContainer, tokenValueElement } = findElements(spaceLabelToken);
+ const tokenValue = tokenValueElement.innerText;
+ const matchingLabel = findLabel(tokenValue);
+
+ subject
+ .updateLabelTokenColor(tokenValueContainer, tokenValue)
+ .then(() => {
+ expectValueContainerStyle(tokenValueContainer, matchingLabel);
+ })
+ .then(done)
+ .catch(done.fail);
+ });
+
+ it('does not change color of a missing label', done => {
+ const { subject, tokenValueContainer, tokenValueElement } = findElements(missingLabelToken);
+ const tokenValue = tokenValueElement.innerText;
+ const matchingLabel = findLabel(tokenValue);
+
+ expect(matchingLabel).toBe(undefined);
+
+ subject
+ .updateLabelTokenColor(tokenValueContainer, tokenValue)
+ .then(() => {
+ expect(tokenValueContainer.getAttribute('style')).toBe(null);
+ })
+ .then(done)
+ .catch(done.fail);
+ });
+ });
+
+ describe('setTokenStyle', () => {
+ let originalTextColor;
+
+ beforeEach(() => {
+ originalTextColor = bugLabelToken.style.color;
+ });
+
+ it('should set backgroundColor', () => {
+ const originalBackgroundColor = bugLabelToken.style.backgroundColor;
+ const token = VisualTokenValue.setTokenStyle(bugLabelToken, 'blue', 'white');
+
+ expect(token.style.backgroundColor).toEqual('blue');
+ expect(token.style.backgroundColor).not.toEqual(originalBackgroundColor);
+ });
+
+ it('should set textColor', () => {
+ const token = VisualTokenValue.setTokenStyle(bugLabelToken, 'white', 'black');
+
+ expect(token.style.color).toEqual('black');
+ expect(token.style.color).not.toEqual(originalTextColor);
+ });
+
+ it('should add inverted class when textColor is #FFFFFF', () => {
+ const token = VisualTokenValue.setTokenStyle(bugLabelToken, 'black', '#FFFFFF');
+
+ expect(token.style.color).toEqual('rgb(255, 255, 255)');
+ expect(token.style.color).not.toEqual(originalTextColor);
+ expect(token.querySelector('.remove-token').classList.contains('inverted')).toEqual(true);
+ });
+ });
+
+ describe('render', () => {
+ const setupSpies = subject => {
+ jest.spyOn(subject, 'updateLabelTokenColor').mockImplementation(() => {});
+ const updateLabelTokenColorSpy = subject.updateLabelTokenColor;
+
+ jest.spyOn(subject, 'updateUserTokenAppearance').mockImplementation(() => {});
+ const updateUserTokenAppearanceSpy = subject.updateUserTokenAppearance;
+
+ return { updateLabelTokenColorSpy, updateUserTokenAppearanceSpy };
+ };
+
+ const keywordToken = FilteredSearchSpecHelper.createFilterVisualToken('search');
+ const milestoneToken = FilteredSearchSpecHelper.createFilterVisualToken(
+ 'milestone',
+ 'upcoming',
+ );
+
+ beforeEach(() => {
+ tokensContainer.innerHTML = FilteredSearchSpecHelper.createTokensContainerHTML(`
+ ${authorToken.outerHTML}
+ ${bugLabelToken.outerHTML}
+ ${keywordToken.outerHTML}
+ ${milestoneToken.outerHTML}
+ `);
+ });
+
+ it('renders a author token value element', () => {
+ const { subject, tokenValueContainer, tokenValueElement } = findElements(authorToken);
+
+ const { updateLabelTokenColorSpy, updateUserTokenAppearanceSpy } = setupSpies(subject);
+ subject.render(tokenValueContainer, tokenValueElement);
+
+ expect(updateUserTokenAppearanceSpy.mock.calls.length).toBe(1);
+ const expectedArgs = [tokenValueContainer, tokenValueElement];
+
+ expect(updateUserTokenAppearanceSpy.mock.calls[0]).toEqual(expectedArgs);
+ expect(updateLabelTokenColorSpy.mock.calls.length).toBe(0);
+ });
+
+ it('renders a label token value element', () => {
+ const { subject, tokenValueContainer, tokenValueElement } = findElements(bugLabelToken);
+
+ const { updateLabelTokenColorSpy, updateUserTokenAppearanceSpy } = setupSpies(subject);
+ subject.render(tokenValueContainer, tokenValueElement);
+
+ expect(updateLabelTokenColorSpy.mock.calls.length).toBe(1);
+ const expectedArgs = [tokenValueContainer];
+
+ expect(updateLabelTokenColorSpy.mock.calls[0]).toEqual(expectedArgs);
+ expect(updateUserTokenAppearanceSpy.mock.calls.length).toBe(0);
+ });
+
+ it('renders a milestone token value element', () => {
+ const { subject, tokenValueContainer, tokenValueElement } = findElements(milestoneToken);
+
+ const { updateLabelTokenColorSpy, updateUserTokenAppearanceSpy } = setupSpies(subject);
+ subject.render(tokenValueContainer, tokenValueElement);
+
+ expect(updateLabelTokenColorSpy.mock.calls.length).toBe(0);
+ expect(updateUserTokenAppearanceSpy.mock.calls.length).toBe(0);
+ });
+
+ it('does not update user token appearance for `none` filter', () => {
+ const { subject, tokenValueContainer, tokenValueElement } = findElements(authorToken);
+
+ subject.tokenValue = 'none';
+
+ const { updateUserTokenAppearanceSpy } = setupSpies(subject);
+ subject.render(tokenValueContainer, tokenValueElement);
+
+ expect(updateUserTokenAppearanceSpy.mock.calls.length).toBe(0);
+ });
+
+ it('does not update user token appearance for `None` filter', () => {
+ const { subject, tokenValueContainer, tokenValueElement } = findElements(authorToken);
+
+ subject.tokenValue = 'None';
+
+ const { updateUserTokenAppearanceSpy } = setupSpies(subject);
+ subject.render(tokenValueContainer, tokenValueElement);
+
+ expect(updateUserTokenAppearanceSpy.mock.calls.length).toBe(0);
+ });
+
+ it('does not update user token appearance for `any` filter', () => {
+ const { subject, tokenValueContainer, tokenValueElement } = findElements(authorToken);
+
+ subject.tokenValue = 'any';
+
+ const { updateUserTokenAppearanceSpy } = setupSpies(subject);
+ subject.render(tokenValueContainer, tokenValueElement);
+
+ expect(updateUserTokenAppearanceSpy.mock.calls.length).toBe(0);
+ });
+
+ it('does not update label token color for `None` filter', () => {
+ const { subject, tokenValueContainer, tokenValueElement } = findElements(bugLabelToken);
+
+ subject.tokenValue = 'None';
+
+ const { updateLabelTokenColorSpy } = setupSpies(subject);
+ subject.render(tokenValueContainer, tokenValueElement);
+
+ expect(updateLabelTokenColorSpy.mock.calls.length).toBe(0);
+ });
+
+ it('does not update label token color for `none` filter', () => {
+ const { subject, tokenValueContainer, tokenValueElement } = findElements(bugLabelToken);
+
+ subject.tokenValue = 'none';
+
+ const { updateLabelTokenColorSpy } = setupSpies(subject);
+ subject.render(tokenValueContainer, tokenValueElement);
+
+ expect(updateLabelTokenColorSpy.mock.calls.length).toBe(0);
+ });
+
+ it('does not update label token color for `any` filter', () => {
+ const { subject, tokenValueContainer, tokenValueElement } = findElements(bugLabelToken);
+
+ subject.tokenValue = 'any';
+
+ const { updateLabelTokenColorSpy } = setupSpies(subject);
+ subject.render(tokenValueContainer, tokenValueElement);
+
+ expect(updateLabelTokenColorSpy.mock.calls.length).toBe(0);
+ });
+ });
+});
diff --git a/spec/frontend/fixtures/test_report.rb b/spec/frontend/fixtures/test_report.rb
index d26bba9b9d0..d0ecaf11994 100644
--- a/spec/frontend/fixtures/test_report.rb
+++ b/spec/frontend/fixtures/test_report.rb
@@ -15,7 +15,7 @@ describe Projects::PipelinesController, "(JavaScript fixtures)", type: :controll
before do
sign_in(user)
- stub_feature_flags(junit_pipeline_view: true)
+ stub_feature_flags(junit_pipeline_view: project)
end
it "pipelines/test_report.json" do
diff --git a/spec/frontend/flash_spec.js b/spec/frontend/flash_spec.js
new file mode 100644
index 00000000000..fa7c1904339
--- /dev/null
+++ b/spec/frontend/flash_spec.js
@@ -0,0 +1,233 @@
+import flash, { createFlashEl, createAction, hideFlash, removeFlashClickListener } from '~/flash';
+
+describe('Flash', () => {
+ describe('createFlashEl', () => {
+ let el;
+
+ beforeEach(() => {
+ el = document.createElement('div');
+ });
+
+ afterEach(() => {
+ el.innerHTML = '';
+ });
+
+ it('creates flash element with type', () => {
+ el.innerHTML = createFlashEl('testing', 'alert');
+
+ expect(el.querySelector('.flash-alert')).not.toBeNull();
+ });
+
+ it('escapes text', () => {
+ el.innerHTML = createFlashEl('<script>alert("a");</script>', 'alert');
+
+ expect(el.querySelector('.flash-text').textContent.trim()).toBe(
+ '<script>alert("a");</script>',
+ );
+ });
+ });
+
+ describe('hideFlash', () => {
+ let el;
+
+ beforeEach(() => {
+ el = document.createElement('div');
+ el.className = 'js-testing';
+ });
+
+ it('sets transition style', () => {
+ hideFlash(el);
+
+ expect(el.style.transition).toBe('opacity 0.15s');
+ });
+
+ it('sets opacity style', () => {
+ hideFlash(el);
+
+ expect(el.style.opacity).toBe('0');
+ });
+
+ it('does not set styles when fadeTransition is false', () => {
+ hideFlash(el, false);
+
+ expect(el.style.opacity).toBe('');
+ expect(el.style.transition).toBeFalsy();
+ });
+
+ it('removes element after transitionend', () => {
+ document.body.appendChild(el);
+
+ hideFlash(el);
+ el.dispatchEvent(new Event('transitionend'));
+
+ expect(document.querySelector('.js-testing')).toBeNull();
+ });
+
+ it('calls event listener callback once', () => {
+ jest.spyOn(el, 'remove');
+ document.body.appendChild(el);
+
+ hideFlash(el);
+
+ el.dispatchEvent(new Event('transitionend'));
+ el.dispatchEvent(new Event('transitionend'));
+
+ expect(el.remove.mock.calls.length).toBe(1);
+ });
+ });
+
+ describe('createAction', () => {
+ let el;
+
+ beforeEach(() => {
+ el = document.createElement('div');
+ });
+
+ it('creates link with href', () => {
+ el.innerHTML = createAction({
+ href: 'testing',
+ title: 'test',
+ });
+
+ expect(el.querySelector('.flash-action').href).toContain('testing');
+ });
+
+ it('uses hash as href when no href is present', () => {
+ el.innerHTML = createAction({
+ title: 'test',
+ });
+
+ expect(el.querySelector('.flash-action').href).toContain('#');
+ });
+
+ it('adds role when no href is present', () => {
+ el.innerHTML = createAction({
+ title: 'test',
+ });
+
+ expect(el.querySelector('.flash-action').getAttribute('role')).toBe('button');
+ });
+
+ it('escapes the title text', () => {
+ el.innerHTML = createAction({
+ title: '<script>alert("a")</script>',
+ });
+
+ expect(el.querySelector('.flash-action').textContent.trim()).toBe(
+ '<script>alert("a")</script>',
+ );
+ });
+ });
+
+ describe('createFlash', () => {
+ describe('no flash-container', () => {
+ it('does not add to the DOM', () => {
+ const flashEl = flash('testing');
+
+ expect(flashEl).toBeNull();
+
+ expect(document.querySelector('.flash-alert')).toBeNull();
+ });
+ });
+
+ describe('with flash-container', () => {
+ beforeEach(() => {
+ document.body.innerHTML += `
+ <div class="content-wrapper js-content-wrapper">
+ <div class="flash-container"></div>
+ </div>
+ `;
+ });
+
+ afterEach(() => {
+ document.querySelector('.js-content-wrapper').remove();
+ });
+
+ it('adds flash element into container', () => {
+ flash('test', 'alert', document, null, false, true);
+
+ expect(document.querySelector('.flash-alert')).not.toBeNull();
+
+ expect(document.body.className).toContain('flash-shown');
+ });
+
+ it('adds flash into specified parent', () => {
+ flash('test', 'alert', document.querySelector('.content-wrapper'));
+
+ expect(document.querySelector('.content-wrapper .flash-alert')).not.toBeNull();
+ });
+
+ it('adds container classes when inside content-wrapper', () => {
+ flash('test');
+
+ expect(document.querySelector('.flash-text').className).toBe('flash-text');
+ });
+
+ it('does not add container when outside of content-wrapper', () => {
+ document.querySelector('.content-wrapper').className = 'js-content-wrapper';
+ flash('test');
+
+ expect(document.querySelector('.flash-text').className.trim()).toContain('flash-text');
+ });
+
+ it('removes element after clicking', () => {
+ flash('test', 'alert', document, null, false, true);
+
+ document.querySelector('.flash-alert .js-close-icon').click();
+
+ expect(document.querySelector('.flash-alert')).toBeNull();
+
+ expect(document.body.className).not.toContain('flash-shown');
+ });
+
+ describe('with actionConfig', () => {
+ it('adds action link', () => {
+ flash('test', 'alert', document, {
+ title: 'test',
+ });
+
+ expect(document.querySelector('.flash-action')).not.toBeNull();
+ });
+
+ it('calls actionConfig clickHandler on click', () => {
+ const actionConfig = {
+ title: 'test',
+ clickHandler: jest.fn(),
+ };
+
+ flash('test', 'alert', document, actionConfig);
+
+ document.querySelector('.flash-action').click();
+
+ expect(actionConfig.clickHandler).toHaveBeenCalled();
+ });
+ });
+ });
+ });
+
+ describe('removeFlashClickListener', () => {
+ beforeEach(() => {
+ document.body.innerHTML += `
+ <div class="flash-container">
+ <div class="flash">
+ <div class="close-icon js-close-icon"></div>
+ </div>
+ </div>
+ `;
+ });
+
+ it('removes global flash on click', done => {
+ const flashEl = document.querySelector('.flash');
+
+ removeFlashClickListener(flashEl, false);
+
+ flashEl.querySelector('.js-close-icon').click();
+
+ setImmediate(() => {
+ expect(document.querySelector('.flash')).toBeNull();
+
+ done();
+ });
+ });
+ });
+});
diff --git a/spec/frontend/frequent_items/components/app_spec.js b/spec/frontend/frequent_items/components/app_spec.js
new file mode 100644
index 00000000000..7c54a48aa41
--- /dev/null
+++ b/spec/frontend/frequent_items/components/app_spec.js
@@ -0,0 +1,251 @@
+import MockAdapter from 'axios-mock-adapter';
+import Vue from 'vue';
+import { mountComponentWithStore } from 'helpers/vue_mount_component_helper';
+import axios from '~/lib/utils/axios_utils';
+import appComponent from '~/frequent_items/components/app.vue';
+import eventHub from '~/frequent_items/event_hub';
+import store from '~/frequent_items/store';
+import { FREQUENT_ITEMS, HOUR_IN_MS } from '~/frequent_items/constants';
+import { getTopFrequentItems } from '~/frequent_items/utils';
+import { currentSession, mockFrequentProjects, mockSearchedProjects } from '../mock_data';
+import { useLocalStorageSpy } from 'helpers/local_storage_helper';
+import waitForPromises from 'helpers/wait_for_promises';
+
+useLocalStorageSpy();
+
+let session;
+const createComponentWithStore = (namespace = 'projects') => {
+ session = currentSession[namespace];
+ gon.api_version = session.apiVersion;
+ const Component = Vue.extend(appComponent);
+
+ return mountComponentWithStore(Component, {
+ store,
+ props: {
+ namespace,
+ currentUserName: session.username,
+ currentItem: session.project || session.group,
+ },
+ });
+};
+
+describe('Frequent Items App Component', () => {
+ let vm;
+ let mock;
+
+ beforeEach(() => {
+ mock = new MockAdapter(axios);
+ vm = createComponentWithStore();
+ });
+
+ afterEach(() => {
+ mock.restore();
+ vm.$destroy();
+ });
+
+ describe('methods', () => {
+ describe('dropdownOpenHandler', () => {
+ it('should fetch frequent items when no search has been previously made on desktop', () => {
+ jest.spyOn(vm, 'fetchFrequentItems').mockImplementation(() => {});
+
+ vm.dropdownOpenHandler();
+
+ expect(vm.fetchFrequentItems).toHaveBeenCalledWith();
+ });
+ });
+
+ describe('logItemAccess', () => {
+ let storage;
+
+ beforeEach(() => {
+ storage = {};
+
+ localStorage.setItem.mockImplementation((storageKey, value) => {
+ storage[storageKey] = value;
+ });
+
+ localStorage.getItem.mockImplementation(storageKey => {
+ if (storage[storageKey]) {
+ return storage[storageKey];
+ }
+
+ return null;
+ });
+ });
+
+ it('should create a project store if it does not exist and adds a project', () => {
+ vm.logItemAccess(session.storageKey, session.project);
+
+ const projects = JSON.parse(storage[session.storageKey]);
+
+ expect(projects.length).toBe(1);
+ expect(projects[0].frequency).toBe(1);
+ expect(projects[0].lastAccessedOn).toBeDefined();
+ });
+
+ it('should prevent inserting same report multiple times into store', () => {
+ vm.logItemAccess(session.storageKey, session.project);
+ vm.logItemAccess(session.storageKey, session.project);
+
+ const projects = JSON.parse(storage[session.storageKey]);
+
+ expect(projects.length).toBe(1);
+ });
+
+ it('should increase frequency of report if it was logged multiple times over the course of an hour', () => {
+ let projects;
+ const newTimestamp = Date.now() + HOUR_IN_MS + 1;
+
+ vm.logItemAccess(session.storageKey, session.project);
+ projects = JSON.parse(storage[session.storageKey]);
+
+ expect(projects[0].frequency).toBe(1);
+
+ vm.logItemAccess(session.storageKey, {
+ ...session.project,
+ lastAccessedOn: newTimestamp,
+ });
+ projects = JSON.parse(storage[session.storageKey]);
+
+ expect(projects[0].frequency).toBe(2);
+ expect(projects[0].lastAccessedOn).not.toBe(session.project.lastAccessedOn);
+ });
+
+ it('should always update project metadata', () => {
+ let projects;
+ const oldProject = {
+ ...session.project,
+ };
+
+ const newProject = {
+ ...session.project,
+ name: 'New Name',
+ avatarUrl: 'new/avatar.png',
+ namespace: 'New / Namespace',
+ webUrl: 'http://localhost/new/web/url',
+ };
+
+ vm.logItemAccess(session.storageKey, oldProject);
+ projects = JSON.parse(storage[session.storageKey]);
+
+ expect(projects[0].name).toBe(oldProject.name);
+ expect(projects[0].avatarUrl).toBe(oldProject.avatarUrl);
+ expect(projects[0].namespace).toBe(oldProject.namespace);
+ expect(projects[0].webUrl).toBe(oldProject.webUrl);
+
+ vm.logItemAccess(session.storageKey, newProject);
+ projects = JSON.parse(storage[session.storageKey]);
+
+ expect(projects[0].name).toBe(newProject.name);
+ expect(projects[0].avatarUrl).toBe(newProject.avatarUrl);
+ expect(projects[0].namespace).toBe(newProject.namespace);
+ expect(projects[0].webUrl).toBe(newProject.webUrl);
+ });
+
+ it('should not add more than 20 projects in store', () => {
+ for (let id = 0; id < FREQUENT_ITEMS.MAX_COUNT; id += 1) {
+ const project = {
+ ...session.project,
+ id,
+ };
+ vm.logItemAccess(session.storageKey, project);
+ }
+
+ const projects = JSON.parse(storage[session.storageKey]);
+
+ expect(projects.length).toBe(FREQUENT_ITEMS.MAX_COUNT);
+ });
+ });
+ });
+
+ describe('created', () => {
+ it('should bind event listeners on eventHub', done => {
+ jest.spyOn(eventHub, '$on').mockImplementation(() => {});
+
+ createComponentWithStore().$mount();
+
+ Vue.nextTick(() => {
+ expect(eventHub.$on).toHaveBeenCalledWith('projects-dropdownOpen', expect.any(Function));
+ done();
+ });
+ });
+ });
+
+ describe('beforeDestroy', () => {
+ it('should unbind event listeners on eventHub', done => {
+ jest.spyOn(eventHub, '$off').mockImplementation(() => {});
+
+ vm.$mount();
+ vm.$destroy();
+
+ Vue.nextTick(() => {
+ expect(eventHub.$off).toHaveBeenCalledWith('projects-dropdownOpen', expect.any(Function));
+ done();
+ });
+ });
+ });
+
+ describe('template', () => {
+ it('should render search input', () => {
+ expect(vm.$el.querySelector('.search-input-container')).toBeDefined();
+ });
+
+ it('should render loading animation', done => {
+ vm.$store.dispatch('fetchSearchedItems');
+
+ Vue.nextTick(() => {
+ const loadingEl = vm.$el.querySelector('.loading-animation');
+
+ expect(loadingEl).toBeDefined();
+ expect(loadingEl.classList.contains('prepend-top-20')).toBe(true);
+ expect(loadingEl.querySelector('span').getAttribute('aria-label')).toBe('Loading projects');
+ done();
+ });
+ });
+
+ it('should render frequent projects list header', done => {
+ Vue.nextTick(() => {
+ const sectionHeaderEl = vm.$el.querySelector('.section-header');
+
+ expect(sectionHeaderEl).toBeDefined();
+ expect(sectionHeaderEl.innerText.trim()).toBe('Frequently visited');
+ done();
+ });
+ });
+
+ it('should render frequent projects list', done => {
+ const expectedResult = getTopFrequentItems(mockFrequentProjects);
+ localStorage.getItem.mockImplementation(() => JSON.stringify(mockFrequentProjects));
+
+ expect(vm.$el.querySelectorAll('.frequent-items-list-container li').length).toBe(1);
+
+ vm.fetchFrequentItems();
+ Vue.nextTick(() => {
+ expect(vm.$el.querySelectorAll('.frequent-items-list-container li').length).toBe(
+ expectedResult.length,
+ );
+ done();
+ });
+ });
+
+ it('should render searched projects list', done => {
+ mock.onGet(/\/api\/v4\/projects.json(.*)$/).replyOnce(200, mockSearchedProjects);
+
+ expect(vm.$el.querySelectorAll('.frequent-items-list-container li').length).toBe(1);
+
+ vm.$store.dispatch('setSearchQuery', 'gitlab');
+ vm.$nextTick()
+ .then(() => {
+ expect(vm.$el.querySelector('.loading-animation')).toBeDefined();
+ })
+ .then(waitForPromises)
+ .then(() => {
+ expect(vm.$el.querySelectorAll('.frequent-items-list-container li').length).toBe(
+ mockSearchedProjects.data.length,
+ );
+ })
+ .then(done)
+ .catch(done.fail);
+ });
+ });
+});
diff --git a/spec/frontend/frequent_items/mock_data.js b/spec/frontend/frequent_items/mock_data.js
index 5cd4cddd877..8c3c66f67ff 100644
--- a/spec/frontend/frequent_items/mock_data.js
+++ b/spec/frontend/frequent_items/mock_data.js
@@ -1,5 +1,94 @@
import { TEST_HOST } from 'helpers/test_constants';
+export const currentSession = {
+ groups: {
+ username: 'root',
+ storageKey: 'root/frequent-groups',
+ apiVersion: 'v4',
+ group: {
+ id: 1,
+ name: 'dummy-group',
+ full_name: 'dummy-parent-group',
+ webUrl: `${TEST_HOST}/dummy-group`,
+ avatarUrl: null,
+ lastAccessedOn: Date.now(),
+ },
+ },
+ projects: {
+ username: 'root',
+ storageKey: 'root/frequent-projects',
+ apiVersion: 'v4',
+ project: {
+ id: 1,
+ name: 'dummy-project',
+ namespace: 'SampleGroup / Dummy-Project',
+ webUrl: `${TEST_HOST}/samplegroup/dummy-project`,
+ avatarUrl: null,
+ lastAccessedOn: Date.now(),
+ },
+ },
+};
+
+export const mockNamespace = 'projects';
+
+export const mockStorageKey = 'test-user/frequent-projects';
+
+export const mockGroup = {
+ id: 1,
+ name: 'Sub451',
+ namespace: 'Commit451 / Sub451',
+ webUrl: `${TEST_HOST}/Commit451/Sub451`,
+ avatarUrl: null,
+};
+
+export const mockRawGroup = {
+ id: 1,
+ name: 'Sub451',
+ full_name: 'Commit451 / Sub451',
+ web_url: `${TEST_HOST}/Commit451/Sub451`,
+ avatar_url: null,
+};
+
+export const mockFrequentGroups = [
+ {
+ id: 3,
+ name: 'Subgroup451',
+ full_name: 'Commit451 / Subgroup451',
+ webUrl: '/Commit451/Subgroup451',
+ avatarUrl: null,
+ frequency: 7,
+ lastAccessedOn: 1497979281815,
+ },
+ {
+ id: 1,
+ name: 'Commit451',
+ full_name: 'Commit451',
+ webUrl: '/Commit451',
+ avatarUrl: null,
+ frequency: 3,
+ lastAccessedOn: 1497979281815,
+ },
+];
+
+export const mockSearchedGroups = [mockRawGroup];
+export const mockProcessedSearchedGroups = [mockGroup];
+
+export const mockProject = {
+ id: 1,
+ name: 'GitLab Community Edition',
+ namespace: 'gitlab-org / gitlab-ce',
+ webUrl: `${TEST_HOST}/gitlab-org/gitlab-foss`,
+ avatarUrl: null,
+};
+
+export const mockRawProject = {
+ id: 1,
+ name: 'GitLab Community Edition',
+ name_with_namespace: 'gitlab-org / gitlab-ce',
+ web_url: `${TEST_HOST}/gitlab-org/gitlab-foss`,
+ avatar_url: null,
+};
+
export const mockFrequentProjects = [
{
id: 1,
@@ -48,10 +137,34 @@ export const mockFrequentProjects = [
},
];
-export const mockProject = {
- id: 1,
- name: 'GitLab Community Edition',
- namespace: 'gitlab-org / gitlab-ce',
- webUrl: `${TEST_HOST}/gitlab-org/gitlab-foss`,
- avatarUrl: null,
-};
+export const mockSearchedProjects = { data: [mockRawProject] };
+export const mockProcessedSearchedProjects = [mockProject];
+
+export const unsortedFrequentItems = [
+ { id: 1, frequency: 12, lastAccessedOn: 1491400843391 },
+ { id: 2, frequency: 14, lastAccessedOn: 1488240890738 },
+ { id: 3, frequency: 44, lastAccessedOn: 1497675908472 },
+ { id: 4, frequency: 8, lastAccessedOn: 1497979281815 },
+ { id: 5, frequency: 34, lastAccessedOn: 1488089211943 },
+ { id: 6, frequency: 14, lastAccessedOn: 1493517292488 },
+ { id: 7, frequency: 42, lastAccessedOn: 1486815299875 },
+ { id: 8, frequency: 33, lastAccessedOn: 1500762279114 },
+ { id: 10, frequency: 46, lastAccessedOn: 1483251641543 },
+];
+
+/**
+ * This const has a specific order which tests authenticity
+ * of `getTopFrequentItems` method so
+ * DO NOT change order of items in this const.
+ */
+export const sortedFrequentItems = [
+ { id: 10, frequency: 46, lastAccessedOn: 1483251641543 },
+ { id: 3, frequency: 44, lastAccessedOn: 1497675908472 },
+ { id: 7, frequency: 42, lastAccessedOn: 1486815299875 },
+ { id: 5, frequency: 34, lastAccessedOn: 1488089211943 },
+ { id: 8, frequency: 33, lastAccessedOn: 1500762279114 },
+ { id: 6, frequency: 14, lastAccessedOn: 1493517292488 },
+ { id: 2, frequency: 14, lastAccessedOn: 1488240890738 },
+ { id: 1, frequency: 12, lastAccessedOn: 1491400843391 },
+ { id: 4, frequency: 8, lastAccessedOn: 1497979281815 },
+];
diff --git a/spec/frontend/frequent_items/store/actions_spec.js b/spec/frontend/frequent_items/store/actions_spec.js
new file mode 100644
index 00000000000..304098e85f1
--- /dev/null
+++ b/spec/frontend/frequent_items/store/actions_spec.js
@@ -0,0 +1,228 @@
+import testAction from 'helpers/vuex_action_helper';
+import MockAdapter from 'axios-mock-adapter';
+import axios from '~/lib/utils/axios_utils';
+import AccessorUtilities from '~/lib/utils/accessor';
+import * as actions from '~/frequent_items/store/actions';
+import * as types from '~/frequent_items/store/mutation_types';
+import state from '~/frequent_items/store/state';
+import {
+ mockNamespace,
+ mockStorageKey,
+ mockFrequentProjects,
+ mockSearchedProjects,
+} from '../mock_data';
+
+describe('Frequent Items Dropdown Store Actions', () => {
+ let mockedState;
+ let mock;
+
+ beforeEach(() => {
+ mockedState = state();
+ mock = new MockAdapter(axios);
+
+ mockedState.namespace = mockNamespace;
+ mockedState.storageKey = mockStorageKey;
+ });
+
+ afterEach(() => {
+ mock.restore();
+ });
+
+ describe('setNamespace', () => {
+ it('should set namespace', done => {
+ testAction(
+ actions.setNamespace,
+ mockNamespace,
+ mockedState,
+ [{ type: types.SET_NAMESPACE, payload: mockNamespace }],
+ [],
+ done,
+ );
+ });
+ });
+
+ describe('setStorageKey', () => {
+ it('should set storage key', done => {
+ testAction(
+ actions.setStorageKey,
+ mockStorageKey,
+ mockedState,
+ [{ type: types.SET_STORAGE_KEY, payload: mockStorageKey }],
+ [],
+ done,
+ );
+ });
+ });
+
+ describe('requestFrequentItems', () => {
+ it('should request frequent items', done => {
+ testAction(
+ actions.requestFrequentItems,
+ null,
+ mockedState,
+ [{ type: types.REQUEST_FREQUENT_ITEMS }],
+ [],
+ done,
+ );
+ });
+ });
+
+ describe('receiveFrequentItemsSuccess', () => {
+ it('should set frequent items', done => {
+ testAction(
+ actions.receiveFrequentItemsSuccess,
+ mockFrequentProjects,
+ mockedState,
+ [{ type: types.RECEIVE_FREQUENT_ITEMS_SUCCESS, payload: mockFrequentProjects }],
+ [],
+ done,
+ );
+ });
+ });
+
+ describe('receiveFrequentItemsError', () => {
+ it('should set frequent items error state', done => {
+ testAction(
+ actions.receiveFrequentItemsError,
+ null,
+ mockedState,
+ [{ type: types.RECEIVE_FREQUENT_ITEMS_ERROR }],
+ [],
+ done,
+ );
+ });
+ });
+
+ describe('fetchFrequentItems', () => {
+ it('should dispatch `receiveFrequentItemsSuccess`', done => {
+ mockedState.namespace = mockNamespace;
+ mockedState.storageKey = mockStorageKey;
+
+ testAction(
+ actions.fetchFrequentItems,
+ null,
+ mockedState,
+ [],
+ [{ type: 'requestFrequentItems' }, { type: 'receiveFrequentItemsSuccess', payload: [] }],
+ done,
+ );
+ });
+
+ it('should dispatch `receiveFrequentItemsError`', done => {
+ jest.spyOn(AccessorUtilities, 'isLocalStorageAccessSafe').mockReturnValue(false);
+ mockedState.namespace = mockNamespace;
+ mockedState.storageKey = mockStorageKey;
+
+ testAction(
+ actions.fetchFrequentItems,
+ null,
+ mockedState,
+ [],
+ [{ type: 'requestFrequentItems' }, { type: 'receiveFrequentItemsError' }],
+ done,
+ );
+ });
+ });
+
+ describe('requestSearchedItems', () => {
+ it('should request searched items', done => {
+ testAction(
+ actions.requestSearchedItems,
+ null,
+ mockedState,
+ [{ type: types.REQUEST_SEARCHED_ITEMS }],
+ [],
+ done,
+ );
+ });
+ });
+
+ describe('receiveSearchedItemsSuccess', () => {
+ it('should set searched items', done => {
+ testAction(
+ actions.receiveSearchedItemsSuccess,
+ mockSearchedProjects,
+ mockedState,
+ [{ type: types.RECEIVE_SEARCHED_ITEMS_SUCCESS, payload: mockSearchedProjects }],
+ [],
+ done,
+ );
+ });
+ });
+
+ describe('receiveSearchedItemsError', () => {
+ it('should set searched items error state', done => {
+ testAction(
+ actions.receiveSearchedItemsError,
+ null,
+ mockedState,
+ [{ type: types.RECEIVE_SEARCHED_ITEMS_ERROR }],
+ [],
+ done,
+ );
+ });
+ });
+
+ describe('fetchSearchedItems', () => {
+ beforeEach(() => {
+ gon.api_version = 'v4';
+ });
+
+ it('should dispatch `receiveSearchedItemsSuccess`', done => {
+ mock.onGet(/\/api\/v4\/projects.json(.*)$/).replyOnce(200, mockSearchedProjects, {});
+
+ testAction(
+ actions.fetchSearchedItems,
+ null,
+ mockedState,
+ [],
+ [
+ { type: 'requestSearchedItems' },
+ {
+ type: 'receiveSearchedItemsSuccess',
+ payload: { data: mockSearchedProjects, headers: {} },
+ },
+ ],
+ done,
+ );
+ });
+
+ it('should dispatch `receiveSearchedItemsError`', done => {
+ gon.api_version = 'v4';
+ mock.onGet(/\/api\/v4\/projects.json(.*)$/).replyOnce(500);
+
+ testAction(
+ actions.fetchSearchedItems,
+ null,
+ mockedState,
+ [],
+ [{ type: 'requestSearchedItems' }, { type: 'receiveSearchedItemsError' }],
+ done,
+ );
+ });
+ });
+
+ describe('setSearchQuery', () => {
+ it('should commit query and dispatch `fetchSearchedItems` when query is present', done => {
+ testAction(
+ actions.setSearchQuery,
+ { query: 'test' },
+ mockedState,
+ [{ type: types.SET_SEARCH_QUERY, payload: { query: 'test' } }],
+ [{ type: 'fetchSearchedItems', payload: { query: 'test' } }],
+ done,
+ );
+ });
+
+ it('should commit query and dispatch `fetchFrequentItems` when query is empty', done => {
+ testAction(
+ actions.setSearchQuery,
+ null,
+ mockedState,
+ [{ type: types.SET_SEARCH_QUERY, payload: null }],
+ [{ type: 'fetchFrequentItems' }],
+ done,
+ );
+ });
+ });
+});
diff --git a/spec/javascripts/frequent_items/store/mutations_spec.js b/spec/frontend/frequent_items/store/mutations_spec.js
index d36964b2600..d36964b2600 100644
--- a/spec/javascripts/frequent_items/store/mutations_spec.js
+++ b/spec/frontend/frequent_items/store/mutations_spec.js
diff --git a/spec/frontend/frequent_items/utils_spec.js b/spec/frontend/frequent_items/utils_spec.js
new file mode 100644
index 00000000000..181dd9268dc
--- /dev/null
+++ b/spec/frontend/frequent_items/utils_spec.js
@@ -0,0 +1,130 @@
+import { GlBreakpointInstance as bp } from '@gitlab/ui/dist/utils';
+import {
+ isMobile,
+ getTopFrequentItems,
+ updateExistingFrequentItem,
+ sanitizeItem,
+} from '~/frequent_items/utils';
+import { HOUR_IN_MS, FREQUENT_ITEMS } from '~/frequent_items/constants';
+import { mockProject, unsortedFrequentItems, sortedFrequentItems } from './mock_data';
+
+describe('Frequent Items utils spec', () => {
+ describe('isMobile', () => {
+ it('returns true when the screen is medium ', () => {
+ jest.spyOn(bp, 'getBreakpointSize').mockReturnValue('md');
+
+ expect(isMobile()).toBe(true);
+ });
+
+ it('returns true when the screen is small ', () => {
+ jest.spyOn(bp, 'getBreakpointSize').mockReturnValue('sm');
+
+ expect(isMobile()).toBe(true);
+ });
+
+ it('returns true when the screen is extra-small ', () => {
+ jest.spyOn(bp, 'getBreakpointSize').mockReturnValue('xs');
+
+ expect(isMobile()).toBe(true);
+ });
+
+ it('returns false when the screen is larger than medium ', () => {
+ jest.spyOn(bp, 'getBreakpointSize').mockReturnValue('lg');
+
+ expect(isMobile()).toBe(false);
+ });
+ });
+
+ describe('getTopFrequentItems', () => {
+ it('returns empty array if no items provided', () => {
+ const result = getTopFrequentItems();
+
+ expect(result.length).toBe(0);
+ });
+
+ it('returns correct amount of items for mobile', () => {
+ jest.spyOn(bp, 'getBreakpointSize').mockReturnValue('md');
+ const result = getTopFrequentItems(unsortedFrequentItems);
+
+ expect(result.length).toBe(FREQUENT_ITEMS.LIST_COUNT_MOBILE);
+ });
+
+ it('returns correct amount of items for desktop', () => {
+ jest.spyOn(bp, 'getBreakpointSize').mockReturnValue('xl');
+ const result = getTopFrequentItems(unsortedFrequentItems);
+
+ expect(result.length).toBe(FREQUENT_ITEMS.LIST_COUNT_DESKTOP);
+ });
+
+ it('sorts frequent items in order of frequency and lastAccessedOn', () => {
+ jest.spyOn(bp, 'getBreakpointSize').mockReturnValue('xl');
+ const result = getTopFrequentItems(unsortedFrequentItems);
+ const expectedResult = sortedFrequentItems.slice(0, FREQUENT_ITEMS.LIST_COUNT_DESKTOP);
+
+ expect(result).toEqual(expectedResult);
+ });
+ });
+
+ describe('updateExistingFrequentItem', () => {
+ let mockedProject;
+
+ beforeEach(() => {
+ mockedProject = {
+ ...mockProject,
+ frequency: 1,
+ lastAccessedOn: 1497979281815,
+ };
+ });
+
+ it('updates item if accessed over an hour ago', () => {
+ const newTimestamp = Date.now() + HOUR_IN_MS + 1;
+ const newItem = {
+ ...mockedProject,
+ lastAccessedOn: newTimestamp,
+ };
+ const result = updateExistingFrequentItem(mockedProject, newItem);
+
+ expect(result.frequency).toBe(mockedProject.frequency + 1);
+ });
+
+ it('does not update item if accessed within the hour', () => {
+ const newItem = {
+ ...mockedProject,
+ lastAccessedOn: mockedProject.lastAccessedOn + HOUR_IN_MS,
+ };
+ const result = updateExistingFrequentItem(mockedProject, newItem);
+
+ expect(result.frequency).toBe(mockedProject.frequency);
+ });
+ });
+
+ describe('sanitizeItem', () => {
+ it('strips HTML tags for name and namespace', () => {
+ const input = {
+ name: '<br><b>test</b>',
+ namespace: '<br>test',
+ id: 1,
+ };
+
+ expect(sanitizeItem(input)).toEqual({ name: 'test', namespace: 'test', id: 1 });
+ });
+
+ it("skips `name` key if it doesn't exist on the item", () => {
+ const input = {
+ namespace: '<br>test',
+ id: 1,
+ };
+
+ expect(sanitizeItem(input)).toEqual({ namespace: 'test', id: 1 });
+ });
+
+ it("skips `namespace` key if it doesn't exist on the item", () => {
+ const input = {
+ name: '<br><b>test</b>',
+ id: 1,
+ };
+
+ expect(sanitizeItem(input)).toEqual({ name: 'test', id: 1 });
+ });
+ });
+});
diff --git a/spec/frontend/groups/components/app_spec.js b/spec/frontend/groups/components/app_spec.js
new file mode 100644
index 00000000000..35eda21e047
--- /dev/null
+++ b/spec/frontend/groups/components/app_spec.js
@@ -0,0 +1,507 @@
+import '~/flash';
+import $ from 'jquery';
+import Vue from 'vue';
+import AxiosMockAdapter from 'axios-mock-adapter';
+import axios from '~/lib/utils/axios_utils';
+import waitForPromises from 'helpers/wait_for_promises';
+import appComponent from '~/groups/components/app.vue';
+import groupFolderComponent from '~/groups/components/group_folder.vue';
+import groupItemComponent from '~/groups/components/group_item.vue';
+import eventHub from '~/groups/event_hub';
+import GroupsStore from '~/groups/store/groups_store';
+import GroupsService from '~/groups/service/groups_service';
+import * as urlUtilities from '~/lib/utils/url_utility';
+
+import {
+ mockEndpoint,
+ mockGroups,
+ mockSearchedGroups,
+ mockRawPageInfo,
+ mockParentGroupItem,
+ mockRawChildren,
+ mockChildren,
+ mockPageInfo,
+} from '../mock_data';
+
+const createComponent = (hideProjects = false) => {
+ const Component = Vue.extend(appComponent);
+ const store = new GroupsStore(false);
+ const service = new GroupsService(mockEndpoint);
+
+ store.state.pageInfo = mockPageInfo;
+
+ return new Component({
+ propsData: {
+ store,
+ service,
+ hideProjects,
+ },
+ });
+};
+
+describe('AppComponent', () => {
+ let vm;
+ let mock;
+ let getGroupsSpy;
+
+ beforeEach(() => {
+ mock = new AxiosMockAdapter(axios);
+ mock.onGet('/dashboard/groups.json').reply(200, mockGroups);
+ Vue.component('group-folder', groupFolderComponent);
+ Vue.component('group-item', groupItemComponent);
+
+ vm = createComponent();
+ getGroupsSpy = jest.spyOn(vm.service, 'getGroups');
+ return vm.$nextTick();
+ });
+
+ describe('computed', () => {
+ beforeEach(() => {
+ vm.$mount();
+ });
+
+ afterEach(() => {
+ vm.$destroy();
+ });
+
+ describe('groups', () => {
+ it('should return list of groups from store', () => {
+ jest.spyOn(vm.store, 'getGroups').mockImplementation(() => {});
+
+ const { groups } = vm;
+
+ expect(vm.store.getGroups).toHaveBeenCalled();
+ expect(groups).not.toBeDefined();
+ });
+ });
+
+ describe('pageInfo', () => {
+ it('should return pagination info from store', () => {
+ jest.spyOn(vm.store, 'getPaginationInfo').mockImplementation(() => {});
+
+ const { pageInfo } = vm;
+
+ expect(vm.store.getPaginationInfo).toHaveBeenCalled();
+ expect(pageInfo).not.toBeDefined();
+ });
+ });
+ });
+
+ describe('methods', () => {
+ beforeEach(() => {
+ vm.$mount();
+ });
+
+ afterEach(() => {
+ vm.$destroy();
+ });
+
+ describe('fetchGroups', () => {
+ it('should call `getGroups` with all the params provided', () => {
+ return vm
+ .fetchGroups({
+ parentId: 1,
+ page: 2,
+ filterGroupsBy: 'git',
+ sortBy: 'created_desc',
+ archived: true,
+ })
+ .then(() => {
+ expect(getGroupsSpy).toHaveBeenCalledWith(1, 2, 'git', 'created_desc', true);
+ });
+ });
+
+ it('should set headers to store for building pagination info when called with `updatePagination`', () => {
+ mock.onGet('/dashboard/groups.json').reply(200, { headers: mockRawPageInfo });
+
+ jest.spyOn(vm, 'updatePagination').mockImplementation(() => {});
+
+ return vm.fetchGroups({ updatePagination: true }).then(() => {
+ expect(getGroupsSpy).toHaveBeenCalled();
+ expect(vm.updatePagination).toHaveBeenCalled();
+ });
+ });
+
+ it('should show flash error when request fails', () => {
+ mock.onGet('/dashboard/groups.json').reply(400);
+
+ jest.spyOn($, 'scrollTo').mockImplementation(() => {});
+ jest.spyOn(window, 'Flash').mockImplementation(() => {});
+
+ return vm.fetchGroups({}).then(() => {
+ expect(vm.isLoading).toBe(false);
+ expect($.scrollTo).toHaveBeenCalledWith(0);
+ expect(window.Flash).toHaveBeenCalledWith('An error occurred. Please try again.');
+ });
+ });
+ });
+
+ describe('fetchAllGroups', () => {
+ beforeEach(() => {
+ jest.spyOn(vm, 'fetchGroups');
+ jest.spyOn(vm, 'updateGroups');
+ });
+
+ it('should fetch default set of groups', () => {
+ jest.spyOn(vm, 'updatePagination');
+
+ const fetchPromise = vm.fetchAllGroups();
+
+ expect(vm.isLoading).toBe(true);
+
+ return fetchPromise.then(() => {
+ expect(vm.isLoading).toBe(false);
+ expect(vm.updateGroups).toHaveBeenCalled();
+ });
+ });
+
+ it('should fetch matching set of groups when app is loaded with search query', () => {
+ mock.onGet('/dashboard/groups.json').reply(200, mockSearchedGroups);
+
+ const fetchPromise = vm.fetchAllGroups();
+
+ expect(vm.fetchGroups).toHaveBeenCalledWith({
+ page: null,
+ filterGroupsBy: null,
+ sortBy: null,
+ updatePagination: true,
+ archived: null,
+ });
+ return fetchPromise.then(() => {
+ expect(vm.updateGroups).toHaveBeenCalled();
+ });
+ });
+ });
+
+ describe('fetchPage', () => {
+ beforeEach(() => {
+ jest.spyOn(vm, 'fetchGroups');
+ jest.spyOn(vm, 'updateGroups');
+ });
+
+ it('should fetch groups for provided page details and update window state', () => {
+ jest.spyOn(urlUtilities, 'mergeUrlParams');
+ jest.spyOn(window.history, 'replaceState').mockImplementation(() => {});
+ jest.spyOn($, 'scrollTo').mockImplementation(() => {});
+
+ const fetchPagePromise = vm.fetchPage(2, null, null, true);
+
+ expect(vm.isLoading).toBe(true);
+ expect(vm.fetchGroups).toHaveBeenCalledWith({
+ page: 2,
+ filterGroupsBy: null,
+ sortBy: null,
+ updatePagination: true,
+ archived: true,
+ });
+
+ return fetchPagePromise.then(() => {
+ expect(vm.isLoading).toBe(false);
+ expect($.scrollTo).toHaveBeenCalledWith(0);
+ expect(urlUtilities.mergeUrlParams).toHaveBeenCalledWith({ page: 2 }, expect.any(String));
+ expect(window.history.replaceState).toHaveBeenCalledWith(
+ {
+ page: expect.any(String),
+ },
+ expect.any(String),
+ expect.any(String),
+ );
+
+ expect(vm.updateGroups).toHaveBeenCalled();
+ });
+ });
+ });
+
+ describe('toggleChildren', () => {
+ let groupItem;
+
+ beforeEach(() => {
+ groupItem = { ...mockParentGroupItem };
+ groupItem.isOpen = false;
+ groupItem.isChildrenLoading = false;
+ });
+
+ it('should fetch children of given group and expand it if group is collapsed and children are not loaded', () => {
+ mock.onGet('/dashboard/groups.json').reply(200, mockRawChildren);
+ jest.spyOn(vm, 'fetchGroups');
+ jest.spyOn(vm.store, 'setGroupChildren').mockImplementation(() => {});
+
+ vm.toggleChildren(groupItem);
+
+ expect(groupItem.isChildrenLoading).toBe(true);
+ expect(vm.fetchGroups).toHaveBeenCalledWith({
+ parentId: groupItem.id,
+ });
+ return waitForPromises().then(() => {
+ expect(vm.store.setGroupChildren).toHaveBeenCalled();
+ });
+ });
+
+ it('should skip network request while expanding group if children are already loaded', () => {
+ jest.spyOn(vm, 'fetchGroups');
+ groupItem.children = mockRawChildren;
+
+ vm.toggleChildren(groupItem);
+
+ expect(vm.fetchGroups).not.toHaveBeenCalled();
+ expect(groupItem.isOpen).toBe(true);
+ });
+
+ it('should collapse group if it is already expanded', () => {
+ jest.spyOn(vm, 'fetchGroups');
+ groupItem.isOpen = true;
+
+ vm.toggleChildren(groupItem);
+
+ expect(vm.fetchGroups).not.toHaveBeenCalled();
+ expect(groupItem.isOpen).toBe(false);
+ });
+
+ it('should set `isChildrenLoading` back to `false` if load request fails', () => {
+ mock.onGet('/dashboard/groups.json').reply(400);
+
+ vm.toggleChildren(groupItem);
+
+ expect(groupItem.isChildrenLoading).toBe(true);
+ return waitForPromises().then(() => {
+ expect(groupItem.isChildrenLoading).toBe(false);
+ });
+ });
+ });
+
+ describe('showLeaveGroupModal', () => {
+ it('caches candidate group (as props) which is to be left', () => {
+ const group = { ...mockParentGroupItem };
+
+ expect(vm.targetGroup).toBe(null);
+ expect(vm.targetParentGroup).toBe(null);
+ vm.showLeaveGroupModal(group, mockParentGroupItem);
+
+ expect(vm.targetGroup).not.toBe(null);
+ expect(vm.targetParentGroup).not.toBe(null);
+ });
+
+ it('updates props which show modal confirmation dialog', () => {
+ const group = { ...mockParentGroupItem };
+
+ expect(vm.showModal).toBe(false);
+ expect(vm.groupLeaveConfirmationMessage).toBe('');
+ vm.showLeaveGroupModal(group, mockParentGroupItem);
+
+ expect(vm.showModal).toBe(true);
+ expect(vm.groupLeaveConfirmationMessage).toBe(
+ `Are you sure you want to leave the "${group.fullName}" group?`,
+ );
+ });
+ });
+
+ describe('hideLeaveGroupModal', () => {
+ it('hides modal confirmation which is shown before leaving the group', () => {
+ const group = { ...mockParentGroupItem };
+ vm.showLeaveGroupModal(group, mockParentGroupItem);
+
+ expect(vm.showModal).toBe(true);
+ vm.hideLeaveGroupModal();
+
+ expect(vm.showModal).toBe(false);
+ });
+ });
+
+ describe('leaveGroup', () => {
+ let groupItem;
+ let childGroupItem;
+
+ beforeEach(() => {
+ groupItem = { ...mockParentGroupItem };
+ groupItem.children = mockChildren;
+ [childGroupItem] = groupItem.children;
+ groupItem.isChildrenLoading = false;
+ vm.targetGroup = childGroupItem;
+ vm.targetParentGroup = groupItem;
+ });
+
+ it('hides modal confirmation leave group and remove group item from tree', () => {
+ const notice = `You left the "${childGroupItem.fullName}" group.`;
+ jest.spyOn(vm.service, 'leaveGroup').mockResolvedValue({ data: { notice } });
+ jest.spyOn(vm.store, 'removeGroup');
+ jest.spyOn(window, 'Flash').mockImplementation(() => {});
+ jest.spyOn($, 'scrollTo').mockImplementation(() => {});
+
+ vm.leaveGroup();
+
+ expect(vm.showModal).toBe(false);
+ expect(vm.targetGroup.isBeingRemoved).toBe(true);
+ expect(vm.service.leaveGroup).toHaveBeenCalledWith(vm.targetGroup.leavePath);
+ return waitForPromises().then(() => {
+ expect($.scrollTo).toHaveBeenCalledWith(0);
+ expect(vm.store.removeGroup).toHaveBeenCalledWith(vm.targetGroup, vm.targetParentGroup);
+ expect(window.Flash).toHaveBeenCalledWith(notice, 'notice');
+ });
+ });
+
+ it('should show error flash message if request failed to leave group', () => {
+ const message = 'An error occurred. Please try again.';
+ jest.spyOn(vm.service, 'leaveGroup').mockRejectedValue({ status: 500 });
+ jest.spyOn(vm.store, 'removeGroup');
+ jest.spyOn(window, 'Flash').mockImplementation(() => {});
+
+ vm.leaveGroup();
+
+ expect(vm.targetGroup.isBeingRemoved).toBe(true);
+ expect(vm.service.leaveGroup).toHaveBeenCalledWith(childGroupItem.leavePath);
+ return waitForPromises().then(() => {
+ expect(vm.store.removeGroup).not.toHaveBeenCalled();
+ expect(window.Flash).toHaveBeenCalledWith(message);
+ expect(vm.targetGroup.isBeingRemoved).toBe(false);
+ });
+ });
+
+ it('should show appropriate error flash message if request forbids to leave group', () => {
+ const message = 'Failed to leave the group. Please make sure you are not the only owner.';
+ jest.spyOn(vm.service, 'leaveGroup').mockRejectedValue({ status: 403 });
+ jest.spyOn(vm.store, 'removeGroup');
+ jest.spyOn(window, 'Flash').mockImplementation(() => {});
+
+ vm.leaveGroup(childGroupItem, groupItem);
+
+ expect(vm.targetGroup.isBeingRemoved).toBe(true);
+ expect(vm.service.leaveGroup).toHaveBeenCalledWith(childGroupItem.leavePath);
+ return waitForPromises().then(() => {
+ expect(vm.store.removeGroup).not.toHaveBeenCalled();
+ expect(window.Flash).toHaveBeenCalledWith(message);
+ expect(vm.targetGroup.isBeingRemoved).toBe(false);
+ });
+ });
+ });
+
+ describe('updatePagination', () => {
+ it('should set pagination info to store from provided headers', () => {
+ jest.spyOn(vm.store, 'setPaginationInfo').mockImplementation(() => {});
+
+ vm.updatePagination(mockRawPageInfo);
+
+ expect(vm.store.setPaginationInfo).toHaveBeenCalledWith(mockRawPageInfo);
+ });
+ });
+
+ describe('updateGroups', () => {
+ it('should call setGroups on store if method was called directly', () => {
+ jest.spyOn(vm.store, 'setGroups').mockImplementation(() => {});
+
+ vm.updateGroups(mockGroups);
+
+ expect(vm.store.setGroups).toHaveBeenCalledWith(mockGroups);
+ });
+
+ it('should call setSearchedGroups on store if method was called with fromSearch param', () => {
+ jest.spyOn(vm.store, 'setSearchedGroups').mockImplementation(() => {});
+
+ vm.updateGroups(mockGroups, true);
+
+ expect(vm.store.setSearchedGroups).toHaveBeenCalledWith(mockGroups);
+ });
+
+ it('should set `isSearchEmpty` prop based on groups count', () => {
+ vm.updateGroups(mockGroups);
+
+ expect(vm.isSearchEmpty).toBe(false);
+
+ vm.updateGroups([]);
+
+ expect(vm.isSearchEmpty).toBe(true);
+ });
+ });
+ });
+
+ describe('created', () => {
+ it('should bind event listeners on eventHub', () => {
+ jest.spyOn(eventHub, '$on').mockImplementation(() => {});
+
+ const newVm = createComponent();
+ newVm.$mount();
+
+ return vm.$nextTick().then(() => {
+ expect(eventHub.$on).toHaveBeenCalledWith('fetchPage', expect.any(Function));
+ expect(eventHub.$on).toHaveBeenCalledWith('toggleChildren', expect.any(Function));
+ expect(eventHub.$on).toHaveBeenCalledWith('showLeaveGroupModal', expect.any(Function));
+ expect(eventHub.$on).toHaveBeenCalledWith('updatePagination', expect.any(Function));
+ expect(eventHub.$on).toHaveBeenCalledWith('updateGroups', expect.any(Function));
+ newVm.$destroy();
+ });
+ });
+
+ it('should initialize `searchEmptyMessage` prop with correct string when `hideProjects` is `false`', () => {
+ const newVm = createComponent();
+ newVm.$mount();
+ return vm.$nextTick().then(() => {
+ expect(newVm.searchEmptyMessage).toBe('No groups or projects matched your search');
+ newVm.$destroy();
+ });
+ });
+
+ it('should initialize `searchEmptyMessage` prop with correct string when `hideProjects` is `true`', () => {
+ const newVm = createComponent(true);
+ newVm.$mount();
+ return vm.$nextTick().then(() => {
+ expect(newVm.searchEmptyMessage).toBe('No groups matched your search');
+ newVm.$destroy();
+ });
+ });
+ });
+
+ describe('beforeDestroy', () => {
+ it('should unbind event listeners on eventHub', () => {
+ jest.spyOn(eventHub, '$off').mockImplementation(() => {});
+
+ const newVm = createComponent();
+ newVm.$mount();
+ newVm.$destroy();
+
+ return vm.$nextTick().then(() => {
+ expect(eventHub.$off).toHaveBeenCalledWith('fetchPage', expect.any(Function));
+ expect(eventHub.$off).toHaveBeenCalledWith('toggleChildren', expect.any(Function));
+ expect(eventHub.$off).toHaveBeenCalledWith('showLeaveGroupModal', expect.any(Function));
+ expect(eventHub.$off).toHaveBeenCalledWith('updatePagination', expect.any(Function));
+ expect(eventHub.$off).toHaveBeenCalledWith('updateGroups', expect.any(Function));
+ });
+ });
+ });
+
+ describe('template', () => {
+ beforeEach(() => {
+ vm.$mount();
+ });
+
+ afterEach(() => {
+ vm.$destroy();
+ });
+
+ it('should render loading icon', () => {
+ vm.isLoading = true;
+ return vm.$nextTick().then(() => {
+ expect(vm.$el.querySelector('.loading-animation')).toBeDefined();
+ expect(vm.$el.querySelector('span').getAttribute('aria-label')).toBe('Loading groups');
+ });
+ });
+
+ it('should render groups tree', () => {
+ vm.store.state.groups = [mockParentGroupItem];
+ vm.isLoading = false;
+ return vm.$nextTick().then(() => {
+ expect(vm.$el.querySelector('.groups-list-tree-container')).toBeDefined();
+ });
+ });
+
+ it('renders modal confirmation dialog', () => {
+ vm.groupLeaveConfirmationMessage = 'Are you sure you want to leave the "foo" group?';
+ vm.showModal = true;
+ return vm.$nextTick().then(() => {
+ const modalDialogEl = vm.$el.querySelector('.modal');
+
+ expect(modalDialogEl).not.toBe(null);
+ expect(modalDialogEl.querySelector('.modal-title').innerText.trim()).toBe('Are you sure?');
+ expect(modalDialogEl.querySelector('.btn.btn-warning').innerText.trim()).toBe('Leave');
+ });
+ });
+ });
+});
diff --git a/spec/frontend/groups/components/group_folder_spec.js b/spec/frontend/groups/components/group_folder_spec.js
new file mode 100644
index 00000000000..a40fa9bece8
--- /dev/null
+++ b/spec/frontend/groups/components/group_folder_spec.js
@@ -0,0 +1,65 @@
+import Vue from 'vue';
+
+import groupFolderComponent from '~/groups/components/group_folder.vue';
+import groupItemComponent from '~/groups/components/group_item.vue';
+import { mockGroups, mockParentGroupItem } from '../mock_data';
+
+const createComponent = (groups = mockGroups, parentGroup = mockParentGroupItem) => {
+ const Component = Vue.extend(groupFolderComponent);
+
+ return new Component({
+ propsData: {
+ groups,
+ parentGroup,
+ },
+ });
+};
+
+describe('GroupFolderComponent', () => {
+ let vm;
+
+ beforeEach(() => {
+ Vue.component('group-item', groupItemComponent);
+
+ vm = createComponent();
+ vm.$mount();
+
+ return Vue.nextTick();
+ });
+
+ afterEach(() => {
+ vm.$destroy();
+ });
+
+ describe('computed', () => {
+ describe('hasMoreChildren', () => {
+ it('should return false when childrenCount of group is less than MAX_CHILDREN_COUNT', () => {
+ expect(vm.hasMoreChildren).toBeFalsy();
+ });
+ });
+
+ describe('moreChildrenStats', () => {
+ it('should return message with count of excess children over MAX_CHILDREN_COUNT limit', () => {
+ expect(vm.moreChildrenStats).toBe('3 more items');
+ });
+ });
+ });
+
+ describe('template', () => {
+ it('should render component template correctly', () => {
+ expect(vm.$el.classList.contains('group-list-tree')).toBeTruthy();
+ expect(vm.$el.querySelectorAll('li.group-row').length).toBe(7);
+ });
+
+ it('should render more children link when groups list has children over MAX_CHILDREN_COUNT limit', () => {
+ const parentGroup = { ...mockParentGroupItem };
+ parentGroup.childrenCount = 21;
+
+ const newVm = createComponent(mockGroups, parentGroup);
+ newVm.$mount();
+
+ expect(newVm.$el.querySelector('li.group-row a.has-more-items')).toBeDefined();
+ newVm.$destroy();
+ });
+ });
+});
diff --git a/spec/frontend/groups/components/group_item_spec.js b/spec/frontend/groups/components/group_item_spec.js
new file mode 100644
index 00000000000..7eb1c54ddb2
--- /dev/null
+++ b/spec/frontend/groups/components/group_item_spec.js
@@ -0,0 +1,215 @@
+import Vue from 'vue';
+import mountComponent from 'helpers/vue_mount_component_helper';
+import groupItemComponent from '~/groups/components/group_item.vue';
+import groupFolderComponent from '~/groups/components/group_folder.vue';
+import eventHub from '~/groups/event_hub';
+import * as urlUtilities from '~/lib/utils/url_utility';
+import { mockParentGroupItem, mockChildren } from '../mock_data';
+
+const createComponent = (group = mockParentGroupItem, parentGroup = mockChildren[0]) => {
+ const Component = Vue.extend(groupItemComponent);
+
+ return mountComponent(Component, {
+ group,
+ parentGroup,
+ });
+};
+
+describe('GroupItemComponent', () => {
+ let vm;
+
+ beforeEach(() => {
+ Vue.component('group-folder', groupFolderComponent);
+
+ vm = createComponent();
+
+ return Vue.nextTick();
+ });
+
+ afterEach(() => {
+ vm.$destroy();
+ });
+
+ describe('computed', () => {
+ describe('groupDomId', () => {
+ it('should return ID string suffixed with group ID', () => {
+ expect(vm.groupDomId).toBe('group-55');
+ });
+ });
+
+ describe('rowClass', () => {
+ it('should return map of classes based on group details', () => {
+ const classes = ['is-open', 'has-children', 'has-description', 'being-removed'];
+ const { rowClass } = vm;
+
+ expect(Object.keys(rowClass).length).toBe(classes.length);
+ Object.keys(rowClass).forEach(className => {
+ expect(classes.indexOf(className)).toBeGreaterThan(-1);
+ });
+ });
+ });
+
+ describe('hasChildren', () => {
+ it('should return boolean value representing if group has any children present', () => {
+ let newVm;
+ const group = { ...mockParentGroupItem };
+
+ group.childrenCount = 5;
+ newVm = createComponent(group);
+
+ expect(newVm.hasChildren).toBeTruthy();
+ newVm.$destroy();
+
+ group.childrenCount = 0;
+ newVm = createComponent(group);
+
+ expect(newVm.hasChildren).toBeFalsy();
+ newVm.$destroy();
+ });
+ });
+
+ describe('hasAvatar', () => {
+ it('should return boolean value representing if group has any avatar present', () => {
+ let newVm;
+ const group = { ...mockParentGroupItem };
+
+ group.avatarUrl = null;
+ newVm = createComponent(group);
+
+ expect(newVm.hasAvatar).toBeFalsy();
+ newVm.$destroy();
+
+ group.avatarUrl = '/uploads/group_avatar.png';
+ newVm = createComponent(group);
+
+ expect(newVm.hasAvatar).toBeTruthy();
+ newVm.$destroy();
+ });
+ });
+
+ describe('isGroup', () => {
+ it('should return boolean value representing if group item is of type `group` or not', () => {
+ let newVm;
+ const group = { ...mockParentGroupItem };
+
+ group.type = 'group';
+ newVm = createComponent(group);
+
+ expect(newVm.isGroup).toBeTruthy();
+ newVm.$destroy();
+
+ group.type = 'project';
+ newVm = createComponent(group);
+
+ expect(newVm.isGroup).toBeFalsy();
+ newVm.$destroy();
+ });
+ });
+ });
+
+ describe('methods', () => {
+ describe('onClickRowGroup', () => {
+ let event;
+
+ beforeEach(() => {
+ const classList = {
+ contains() {
+ return false;
+ },
+ };
+
+ event = {
+ target: {
+ classList,
+ parentElement: {
+ classList,
+ },
+ },
+ };
+ });
+
+ it('should emit `toggleChildren` event when expand is clicked on a group and it has children present', () => {
+ jest.spyOn(eventHub, '$emit').mockImplementation(() => {});
+
+ vm.onClickRowGroup(event);
+
+ expect(eventHub.$emit).toHaveBeenCalledWith('toggleChildren', vm.group);
+ });
+
+ it('should navigate page to group homepage if group does not have any children present', () => {
+ jest.spyOn(urlUtilities, 'visitUrl').mockImplementation();
+ const group = { ...mockParentGroupItem };
+ group.childrenCount = 0;
+ const newVm = createComponent(group);
+ jest.spyOn(eventHub, '$emit').mockImplementation(() => {});
+
+ newVm.onClickRowGroup(event);
+
+ expect(eventHub.$emit).not.toHaveBeenCalled();
+ expect(urlUtilities.visitUrl).toHaveBeenCalledWith(newVm.group.relativePath);
+ });
+ });
+ });
+
+ describe('template', () => {
+ let group = null;
+
+ describe('for a group pending deletion', () => {
+ beforeEach(() => {
+ group = { ...mockParentGroupItem, pendingRemoval: true };
+ vm = createComponent(group);
+ });
+
+ it('renders the group pending removal badge', () => {
+ const badgeEl = vm.$el.querySelector('.badge-warning');
+
+ expect(badgeEl).toBeDefined();
+ expect(badgeEl.innerHTML).toContain('pending removal');
+ });
+ });
+
+ describe('for a group not scheduled for deletion', () => {
+ beforeEach(() => {
+ group = { ...mockParentGroupItem, pendingRemoval: false };
+ vm = createComponent(group);
+ });
+
+ it('does not render the group pending removal badge', () => {
+ const groupTextContainer = vm.$el.querySelector('.group-text-container');
+
+ expect(groupTextContainer).not.toContain('pending removal');
+ });
+ });
+
+ it('should render component template correctly', () => {
+ const visibilityIconEl = vm.$el.querySelector('.item-visibility');
+
+ expect(vm.$el.getAttribute('id')).toBe('group-55');
+ expect(vm.$el.classList.contains('group-row')).toBeTruthy();
+
+ expect(vm.$el.querySelector('.group-row-contents')).toBeDefined();
+ expect(vm.$el.querySelector('.group-row-contents .controls')).toBeDefined();
+ expect(vm.$el.querySelector('.group-row-contents .stats')).toBeDefined();
+
+ expect(vm.$el.querySelector('.folder-toggle-wrap')).toBeDefined();
+ expect(vm.$el.querySelector('.folder-toggle-wrap .folder-caret')).toBeDefined();
+ expect(vm.$el.querySelector('.folder-toggle-wrap .item-type-icon')).toBeDefined();
+
+ expect(vm.$el.querySelector('.avatar-container')).toBeDefined();
+ expect(vm.$el.querySelector('.avatar-container a.no-expand')).toBeDefined();
+ expect(vm.$el.querySelector('.avatar-container .avatar')).toBeDefined();
+
+ expect(vm.$el.querySelector('.title')).toBeDefined();
+ expect(vm.$el.querySelector('.title a.no-expand')).toBeDefined();
+
+ expect(visibilityIconEl).not.toBe(null);
+ expect(visibilityIconEl.dataset.originalTitle).toBe(vm.visibilityTooltip);
+ expect(visibilityIconEl.querySelectorAll('svg').length).toBeGreaterThan(0);
+
+ expect(vm.$el.querySelector('.access-type')).toBeDefined();
+ expect(vm.$el.querySelector('.description')).toBeDefined();
+
+ expect(vm.$el.querySelector('.group-list-tree')).toBeDefined();
+ });
+ });
+});
diff --git a/spec/frontend/groups/components/groups_spec.js b/spec/frontend/groups/components/groups_spec.js
new file mode 100644
index 00000000000..6205400eb03
--- /dev/null
+++ b/spec/frontend/groups/components/groups_spec.js
@@ -0,0 +1,72 @@
+import Vue from 'vue';
+
+import mountComponent from 'helpers/vue_mount_component_helper';
+import groupsComponent from '~/groups/components/groups.vue';
+import groupFolderComponent from '~/groups/components/group_folder.vue';
+import groupItemComponent from '~/groups/components/group_item.vue';
+import eventHub from '~/groups/event_hub';
+import { mockGroups, mockPageInfo } from '../mock_data';
+
+const createComponent = (searchEmpty = false) => {
+ const Component = Vue.extend(groupsComponent);
+
+ return mountComponent(Component, {
+ groups: mockGroups,
+ pageInfo: mockPageInfo,
+ searchEmptyMessage: 'No matching results',
+ searchEmpty,
+ });
+};
+
+describe('GroupsComponent', () => {
+ let vm;
+
+ beforeEach(() => {
+ Vue.component('group-folder', groupFolderComponent);
+ Vue.component('group-item', groupItemComponent);
+
+ vm = createComponent();
+
+ return vm.$nextTick();
+ });
+
+ afterEach(() => {
+ vm.$destroy();
+ });
+
+ describe('methods', () => {
+ describe('change', () => {
+ it('should emit `fetchPage` event when page is changed via pagination', () => {
+ jest.spyOn(eventHub, '$emit').mockImplementation();
+
+ vm.change(2);
+
+ expect(eventHub.$emit).toHaveBeenCalledWith(
+ 'fetchPage',
+ 2,
+ expect.any(Object),
+ expect.any(Object),
+ expect.any(Object),
+ );
+ });
+ });
+ });
+
+ describe('template', () => {
+ it('should render component template correctly', () => {
+ return vm.$nextTick().then(() => {
+ expect(vm.$el.querySelector('.groups-list-tree-container')).toBeDefined();
+ expect(vm.$el.querySelector('.group-list-tree')).toBeDefined();
+ expect(vm.$el.querySelector('.gl-pagination')).toBeDefined();
+ expect(vm.$el.querySelectorAll('.has-no-search-results').length).toBe(0);
+ });
+ });
+
+ it('should render empty search message when `searchEmpty` is `true`', () => {
+ vm.searchEmpty = true;
+ return vm.$nextTick().then(() => {
+ expect(vm.$el.querySelector('.has-no-search-results')).toBeDefined();
+ });
+ });
+ });
+});
diff --git a/spec/frontend/groups/components/item_actions_spec.js b/spec/frontend/groups/components/item_actions_spec.js
new file mode 100644
index 00000000000..c0dc1a816e6
--- /dev/null
+++ b/spec/frontend/groups/components/item_actions_spec.js
@@ -0,0 +1,84 @@
+import Vue from 'vue';
+
+import mountComponent from 'helpers/vue_mount_component_helper';
+import itemActionsComponent from '~/groups/components/item_actions.vue';
+import eventHub from '~/groups/event_hub';
+import { mockParentGroupItem, mockChildren } from '../mock_data';
+
+const createComponent = (group = mockParentGroupItem, parentGroup = mockChildren[0]) => {
+ const Component = Vue.extend(itemActionsComponent);
+
+ return mountComponent(Component, {
+ group,
+ parentGroup,
+ });
+};
+
+describe('ItemActionsComponent', () => {
+ let vm;
+
+ beforeEach(() => {
+ vm = createComponent();
+ });
+
+ afterEach(() => {
+ vm.$destroy();
+ });
+
+ describe('methods', () => {
+ describe('onLeaveGroup', () => {
+ it('emits `showLeaveGroupModal` event with `group` and `parentGroup` props', () => {
+ jest.spyOn(eventHub, '$emit').mockImplementation(() => {});
+ vm.onLeaveGroup();
+
+ expect(eventHub.$emit).toHaveBeenCalledWith(
+ 'showLeaveGroupModal',
+ vm.group,
+ vm.parentGroup,
+ );
+ });
+ });
+ });
+
+ describe('template', () => {
+ it('should render component template correctly', () => {
+ expect(vm.$el.classList.contains('controls')).toBeTruthy();
+ });
+
+ it('should render Edit Group button with correct attribute values', () => {
+ const group = { ...mockParentGroupItem };
+ group.canEdit = true;
+ const newVm = createComponent(group);
+
+ const editBtn = newVm.$el.querySelector('a.edit-group');
+
+ expect(editBtn).toBeDefined();
+ expect(editBtn.classList.contains('no-expand')).toBeTruthy();
+ expect(editBtn.getAttribute('href')).toBe(group.editPath);
+ expect(editBtn.getAttribute('aria-label')).toBe('Edit group');
+ expect(editBtn.dataset.originalTitle).toBe('Edit group');
+ expect(editBtn.querySelectorAll('svg use').length).not.toBe(0);
+ expect(editBtn.querySelector('svg use').getAttribute('xlink:href')).toContain('#settings');
+
+ newVm.$destroy();
+ });
+
+ it('should render Leave Group button with correct attribute values', () => {
+ const group = { ...mockParentGroupItem };
+ group.canLeave = true;
+ const newVm = createComponent(group);
+
+ const leaveBtn = newVm.$el.querySelector('a.leave-group');
+
+ expect(leaveBtn).toBeDefined();
+ expect(leaveBtn.classList.contains('no-expand')).toBeTruthy();
+ expect(leaveBtn.getAttribute('href')).toBe(group.leavePath);
+ expect(leaveBtn.getAttribute('aria-label')).toBe('Leave this group');
+ expect(leaveBtn.dataset.originalTitle).toBe('Leave this group');
+ expect(leaveBtn.querySelectorAll('svg use').length).not.toBe(0);
+ expect(leaveBtn.querySelector('svg use').getAttribute('xlink:href')).toContain('#leave');
+
+ newVm.$destroy();
+ });
+ });
+});
diff --git a/spec/frontend/groups/components/item_caret_spec.js b/spec/frontend/groups/components/item_caret_spec.js
new file mode 100644
index 00000000000..bfe27be9b51
--- /dev/null
+++ b/spec/frontend/groups/components/item_caret_spec.js
@@ -0,0 +1,38 @@
+import Vue from 'vue';
+
+import mountComponent from 'helpers/vue_mount_component_helper';
+import itemCaretComponent from '~/groups/components/item_caret.vue';
+
+const createComponent = (isGroupOpen = false) => {
+ const Component = Vue.extend(itemCaretComponent);
+
+ return mountComponent(Component, {
+ isGroupOpen,
+ });
+};
+
+describe('ItemCaretComponent', () => {
+ let vm;
+
+ afterEach(() => {
+ vm.$destroy();
+ });
+
+ describe('template', () => {
+ it('should render component template correctly', () => {
+ vm = createComponent();
+ expect(vm.$el.classList.contains('folder-caret')).toBeTruthy();
+ expect(vm.$el.querySelectorAll('svg').length).toBe(1);
+ });
+
+ it('should render caret down icon if `isGroupOpen` prop is `true`', () => {
+ vm = createComponent(true);
+ expect(vm.$el.querySelector('svg use').getAttribute('xlink:href')).toContain('angle-down');
+ });
+
+ it('should render caret right icon if `isGroupOpen` prop is `false`', () => {
+ vm = createComponent();
+ expect(vm.$el.querySelector('svg use').getAttribute('xlink:href')).toContain('angle-right');
+ });
+ });
+});
diff --git a/spec/frontend/groups/components/item_stats_spec.js b/spec/frontend/groups/components/item_stats_spec.js
new file mode 100644
index 00000000000..771643609ec
--- /dev/null
+++ b/spec/frontend/groups/components/item_stats_spec.js
@@ -0,0 +1,119 @@
+import Vue from 'vue';
+
+import mountComponent from 'helpers/vue_mount_component_helper';
+import itemStatsComponent from '~/groups/components/item_stats.vue';
+import {
+ mockParentGroupItem,
+ ITEM_TYPE,
+ VISIBILITY_TYPE_ICON,
+ GROUP_VISIBILITY_TYPE,
+ PROJECT_VISIBILITY_TYPE,
+} from '../mock_data';
+
+const createComponent = (item = mockParentGroupItem) => {
+ const Component = Vue.extend(itemStatsComponent);
+
+ return mountComponent(Component, {
+ item,
+ });
+};
+
+describe('ItemStatsComponent', () => {
+ describe('computed', () => {
+ describe('visibilityIcon', () => {
+ it('should return icon class based on `item.visibility` value', () => {
+ Object.keys(VISIBILITY_TYPE_ICON).forEach(visibility => {
+ const item = { ...mockParentGroupItem, visibility };
+ const vm = createComponent(item);
+
+ expect(vm.visibilityIcon).toBe(VISIBILITY_TYPE_ICON[visibility]);
+ vm.$destroy();
+ });
+ });
+ });
+
+ describe('visibilityTooltip', () => {
+ it('should return tooltip string for Group based on `item.visibility` value', () => {
+ Object.keys(GROUP_VISIBILITY_TYPE).forEach(visibility => {
+ const item = { ...mockParentGroupItem, visibility, type: ITEM_TYPE.GROUP };
+ const vm = createComponent(item);
+
+ expect(vm.visibilityTooltip).toBe(GROUP_VISIBILITY_TYPE[visibility]);
+ vm.$destroy();
+ });
+ });
+
+ it('should return tooltip string for Project based on `item.visibility` value', () => {
+ Object.keys(PROJECT_VISIBILITY_TYPE).forEach(visibility => {
+ const item = { ...mockParentGroupItem, visibility, type: ITEM_TYPE.PROJECT };
+ const vm = createComponent(item);
+
+ expect(vm.visibilityTooltip).toBe(PROJECT_VISIBILITY_TYPE[visibility]);
+ vm.$destroy();
+ });
+ });
+ });
+
+ describe('isProject', () => {
+ it('should return boolean value representing whether `item.type` is Project or not', () => {
+ let item;
+ let vm;
+
+ item = { ...mockParentGroupItem, type: ITEM_TYPE.PROJECT };
+ vm = createComponent(item);
+
+ expect(vm.isProject).toBeTruthy();
+ vm.$destroy();
+
+ item = { ...mockParentGroupItem, type: ITEM_TYPE.GROUP };
+ vm = createComponent(item);
+
+ expect(vm.isProject).toBeFalsy();
+ vm.$destroy();
+ });
+ });
+
+ describe('isGroup', () => {
+ it('should return boolean value representing whether `item.type` is Group or not', () => {
+ let item;
+ let vm;
+
+ item = { ...mockParentGroupItem, type: ITEM_TYPE.GROUP };
+ vm = createComponent(item);
+
+ expect(vm.isGroup).toBeTruthy();
+ vm.$destroy();
+
+ item = { ...mockParentGroupItem, type: ITEM_TYPE.PROJECT };
+ vm = createComponent(item);
+
+ expect(vm.isGroup).toBeFalsy();
+ vm.$destroy();
+ });
+ });
+ });
+
+ describe('template', () => {
+ it('renders component container element correctly', () => {
+ const vm = createComponent();
+
+ expect(vm.$el.classList.contains('stats')).toBeTruthy();
+
+ vm.$destroy();
+ });
+
+ it('renders start count and last updated information for project item correctly', () => {
+ const item = { ...mockParentGroupItem, type: ITEM_TYPE.PROJECT, starCount: 4 };
+ const vm = createComponent(item);
+
+ const projectStarIconEl = vm.$el.querySelector('.project-stars');
+
+ expect(projectStarIconEl).not.toBeNull();
+ expect(projectStarIconEl.querySelectorAll('svg').length).toBeGreaterThan(0);
+ expect(projectStarIconEl.querySelectorAll('.stat-value').length).toBeGreaterThan(0);
+ expect(vm.$el.querySelectorAll('.last-updated').length).toBeGreaterThan(0);
+
+ vm.$destroy();
+ });
+ });
+});
diff --git a/spec/frontend/groups/components/item_stats_value_spec.js b/spec/frontend/groups/components/item_stats_value_spec.js
new file mode 100644
index 00000000000..da6f145fa19
--- /dev/null
+++ b/spec/frontend/groups/components/item_stats_value_spec.js
@@ -0,0 +1,82 @@
+import Vue from 'vue';
+
+import mountComponent from 'helpers/vue_mount_component_helper';
+import itemStatsValueComponent from '~/groups/components/item_stats_value.vue';
+
+const createComponent = ({ title, cssClass, iconName, tooltipPlacement, value }) => {
+ const Component = Vue.extend(itemStatsValueComponent);
+
+ return mountComponent(Component, {
+ title,
+ cssClass,
+ iconName,
+ tooltipPlacement,
+ value,
+ });
+};
+
+describe('ItemStatsValueComponent', () => {
+ describe('computed', () => {
+ let vm;
+ const itemConfig = {
+ title: 'Subgroups',
+ cssClass: 'number-subgroups',
+ iconName: 'folder',
+ tooltipPlacement: 'left',
+ };
+
+ describe('isValuePresent', () => {
+ it('returns true if non-empty `value` is present', () => {
+ vm = createComponent({ ...itemConfig, value: 10 });
+
+ expect(vm.isValuePresent).toBeTruthy();
+ });
+
+ it('returns false if empty `value` is present', () => {
+ vm = createComponent(itemConfig);
+
+ expect(vm.isValuePresent).toBeFalsy();
+ });
+
+ afterEach(() => {
+ vm.$destroy();
+ });
+ });
+ });
+
+ describe('template', () => {
+ let vm;
+ beforeEach(() => {
+ vm = createComponent({
+ title: 'Subgroups',
+ cssClass: 'number-subgroups',
+ iconName: 'folder',
+ tooltipPlacement: 'left',
+ value: 10,
+ });
+ });
+
+ afterEach(() => {
+ vm.$destroy();
+ });
+
+ it('renders component element correctly', () => {
+ expect(vm.$el.classList.contains('number-subgroups')).toBeTruthy();
+ expect(vm.$el.querySelectorAll('svg').length).toBeGreaterThan(0);
+ expect(vm.$el.querySelectorAll('.stat-value').length).toBeGreaterThan(0);
+ });
+
+ it('renders element tooltip correctly', () => {
+ expect(vm.$el.dataset.originalTitle).toBe('Subgroups');
+ expect(vm.$el.dataset.placement).toBe('left');
+ });
+
+ it('renders element icon correctly', () => {
+ expect(vm.$el.querySelector('svg use').getAttribute('xlink:href')).toContain('folder');
+ });
+
+ it('renders value count correctly', () => {
+ expect(vm.$el.querySelector('.stat-value').innerText.trim()).toContain('10');
+ });
+ });
+});
diff --git a/spec/frontend/groups/components/item_type_icon_spec.js b/spec/frontend/groups/components/item_type_icon_spec.js
new file mode 100644
index 00000000000..251b5b5ff4c
--- /dev/null
+++ b/spec/frontend/groups/components/item_type_icon_spec.js
@@ -0,0 +1,53 @@
+import Vue from 'vue';
+
+import mountComponent from 'helpers/vue_mount_component_helper';
+import itemTypeIconComponent from '~/groups/components/item_type_icon.vue';
+import { ITEM_TYPE } from '../mock_data';
+
+const createComponent = (itemType = ITEM_TYPE.GROUP, isGroupOpen = false) => {
+ const Component = Vue.extend(itemTypeIconComponent);
+
+ return mountComponent(Component, {
+ itemType,
+ isGroupOpen,
+ });
+};
+
+describe('ItemTypeIconComponent', () => {
+ describe('template', () => {
+ it('should render component template correctly', () => {
+ const vm = createComponent();
+
+ expect(vm.$el.classList.contains('item-type-icon')).toBeTruthy();
+ vm.$destroy();
+ });
+
+ it('should render folder open or close icon based `isGroupOpen` prop value', () => {
+ let vm;
+
+ vm = createComponent(ITEM_TYPE.GROUP, true);
+
+ expect(vm.$el.querySelector('use').getAttribute('xlink:href')).toContain('folder-open');
+ vm.$destroy();
+
+ vm = createComponent(ITEM_TYPE.GROUP);
+
+ expect(vm.$el.querySelector('use').getAttribute('xlink:href')).toContain('folder');
+ vm.$destroy();
+ });
+
+ it('should render bookmark icon based on `isProject` prop value', () => {
+ let vm;
+
+ vm = createComponent(ITEM_TYPE.PROJECT);
+
+ expect(vm.$el.querySelector('use').getAttribute('xlink:href')).toContain('bookmark');
+ vm.$destroy();
+
+ vm = createComponent(ITEM_TYPE.GROUP);
+
+ expect(vm.$el.querySelector('use').getAttribute('xlink:href')).not.toContain('bookmark');
+ vm.$destroy();
+ });
+ });
+});
diff --git a/spec/javascripts/groups/mock_data.js b/spec/frontend/groups/mock_data.js
index 380dda9f7b1..380dda9f7b1 100644
--- a/spec/javascripts/groups/mock_data.js
+++ b/spec/frontend/groups/mock_data.js
diff --git a/spec/frontend/groups/service/groups_service_spec.js b/spec/frontend/groups/service/groups_service_spec.js
new file mode 100644
index 00000000000..38a565eba01
--- /dev/null
+++ b/spec/frontend/groups/service/groups_service_spec.js
@@ -0,0 +1,42 @@
+import axios from '~/lib/utils/axios_utils';
+
+import GroupsService from '~/groups/service/groups_service';
+import { mockEndpoint, mockParentGroupItem } from '../mock_data';
+
+describe('GroupsService', () => {
+ let service;
+
+ beforeEach(() => {
+ service = new GroupsService(mockEndpoint);
+ });
+
+ describe('getGroups', () => {
+ it('should return promise for `GET` request on provided endpoint', () => {
+ jest.spyOn(axios, 'get').mockResolvedValue();
+ const params = {
+ page: 2,
+ filter: 'git',
+ sort: 'created_asc',
+ archived: true,
+ };
+
+ service.getGroups(55, 2, 'git', 'created_asc', true);
+
+ expect(axios.get).toHaveBeenCalledWith(mockEndpoint, { params: { parent_id: 55 } });
+
+ service.getGroups(null, 2, 'git', 'created_asc', true);
+
+ expect(axios.get).toHaveBeenCalledWith(mockEndpoint, { params });
+ });
+ });
+
+ describe('leaveGroup', () => {
+ it('should return promise for `DELETE` request on provided endpoint', () => {
+ jest.spyOn(axios, 'delete').mockResolvedValue();
+
+ service.leaveGroup(mockParentGroupItem.leavePath);
+
+ expect(axios.delete).toHaveBeenCalledWith(mockParentGroupItem.leavePath);
+ });
+ });
+});
diff --git a/spec/frontend/groups/store/groups_store_spec.js b/spec/frontend/groups/store/groups_store_spec.js
new file mode 100644
index 00000000000..7d12f73d270
--- /dev/null
+++ b/spec/frontend/groups/store/groups_store_spec.js
@@ -0,0 +1,123 @@
+import GroupsStore from '~/groups/store/groups_store';
+import {
+ mockGroups,
+ mockSearchedGroups,
+ mockParentGroupItem,
+ mockRawChildren,
+ mockRawPageInfo,
+} from '../mock_data';
+
+describe('ProjectsStore', () => {
+ describe('constructor', () => {
+ it('should initialize default state', () => {
+ let store;
+
+ store = new GroupsStore();
+
+ expect(Object.keys(store.state).length).toBe(2);
+ expect(Array.isArray(store.state.groups)).toBeTruthy();
+ expect(Object.keys(store.state.pageInfo).length).toBe(0);
+ expect(store.hideProjects).not.toBeDefined();
+
+ store = new GroupsStore(true);
+
+ expect(store.hideProjects).toBeTruthy();
+ });
+ });
+
+ describe('setGroups', () => {
+ it('should set groups to state', () => {
+ const store = new GroupsStore();
+ jest.spyOn(store, 'formatGroupItem');
+
+ store.setGroups(mockGroups);
+
+ expect(store.state.groups.length).toBe(mockGroups.length);
+ expect(store.formatGroupItem).toHaveBeenCalledWith(expect.any(Object));
+ expect(Object.keys(store.state.groups[0]).indexOf('fullName')).toBeGreaterThan(-1);
+ });
+ });
+
+ describe('setSearchedGroups', () => {
+ it('should set searched groups to state', () => {
+ const store = new GroupsStore();
+ jest.spyOn(store, 'formatGroupItem');
+
+ store.setSearchedGroups(mockSearchedGroups);
+
+ expect(store.state.groups.length).toBe(mockSearchedGroups.length);
+ expect(store.formatGroupItem).toHaveBeenCalledWith(expect.any(Object));
+ expect(Object.keys(store.state.groups[0]).indexOf('fullName')).toBeGreaterThan(-1);
+ expect(Object.keys(store.state.groups[0].children[0]).indexOf('fullName')).toBeGreaterThan(
+ -1,
+ );
+ });
+ });
+
+ describe('setGroupChildren', () => {
+ it('should set children to group item in state', () => {
+ const store = new GroupsStore();
+ jest.spyOn(store, 'formatGroupItem');
+
+ store.setGroupChildren(mockParentGroupItem, mockRawChildren);
+
+ expect(store.formatGroupItem).toHaveBeenCalledWith(expect.any(Object));
+ expect(mockParentGroupItem.children.length).toBe(1);
+ expect(Object.keys(mockParentGroupItem.children[0]).indexOf('fullName')).toBeGreaterThan(-1);
+ expect(mockParentGroupItem.isOpen).toBeTruthy();
+ expect(mockParentGroupItem.isChildrenLoading).toBeFalsy();
+ });
+ });
+
+ describe('setPaginationInfo', () => {
+ it('should parse and set pagination info in state', () => {
+ const store = new GroupsStore();
+
+ store.setPaginationInfo(mockRawPageInfo);
+
+ expect(store.state.pageInfo.perPage).toBe(10);
+ expect(store.state.pageInfo.page).toBe(10);
+ expect(store.state.pageInfo.total).toBe(10);
+ expect(store.state.pageInfo.totalPages).toBe(10);
+ expect(store.state.pageInfo.nextPage).toBe(10);
+ expect(store.state.pageInfo.previousPage).toBe(10);
+ });
+ });
+
+ describe('formatGroupItem', () => {
+ it('should parse group item object and return updated object', () => {
+ let store;
+ let updatedGroupItem;
+
+ store = new GroupsStore();
+ updatedGroupItem = store.formatGroupItem(mockRawChildren[0]);
+
+ expect(Object.keys(updatedGroupItem).indexOf('fullName')).toBeGreaterThan(-1);
+ expect(updatedGroupItem.childrenCount).toBe(mockRawChildren[0].children_count);
+ expect(updatedGroupItem.isChildrenLoading).toBe(false);
+ expect(updatedGroupItem.isBeingRemoved).toBe(false);
+
+ store = new GroupsStore(true);
+ updatedGroupItem = store.formatGroupItem(mockRawChildren[0]);
+
+ expect(Object.keys(updatedGroupItem).indexOf('fullName')).toBeGreaterThan(-1);
+ expect(updatedGroupItem.childrenCount).toBe(mockRawChildren[0].subgroup_count);
+ });
+ });
+
+ describe('removeGroup', () => {
+ it('should remove children from group item in state', () => {
+ const store = new GroupsStore();
+ const rawParentGroup = { ...mockGroups[0] };
+ const rawChildGroup = { ...mockGroups[1] };
+
+ store.setGroups([rawParentGroup]);
+ store.setGroupChildren(store.state.groups[0], [rawChildGroup]);
+ const childItem = store.state.groups[0].children[0];
+
+ store.removeGroup(childItem, store.state.groups[0]);
+
+ expect(store.state.groups[0].children.length).toBe(0);
+ });
+ });
+});
diff --git a/spec/frontend/header_spec.js b/spec/frontend/header_spec.js
index 0a74799283a..6d2d7976196 100644
--- a/spec/frontend/header_spec.js
+++ b/spec/frontend/header_spec.js
@@ -60,8 +60,8 @@ describe('Header', () => {
beforeEach(() => {
setFixtures(`
<li class="js-nav-user-dropdown">
- <a class="js-buy-ci-minutes-link" data-track-event="click_buy_ci_minutes" data-track-label="free" data-track-property="user_dropdown">Buy CI minutes
- </a>
+ <a class="js-buy-ci-minutes-link" data-track-event="click_buy_ci_minutes" data-track-label="free" data-track-property="user_dropdown">Buy CI minutes</a>
+ <a class="js-upgrade-plan-link" data-track-event="click_upgrade_link" data-track-label="free" data-track-property="user_dropdown">Upgrade</a>
</li>`);
trackingSpy = mockTracking('_category_', $('.js-nav-user-dropdown').element, jest.spyOn);
@@ -77,8 +77,16 @@ describe('Header', () => {
it('sends a tracking event when the dropdown is opened and contains Buy CI minutes link', () => {
$('.js-nav-user-dropdown').trigger('shown.bs.dropdown');
- expect(trackingSpy).toHaveBeenCalledTimes(1);
- expect(trackingSpy).toHaveBeenCalledWith(undefined, 'show_buy_ci_minutes', {
+ expect(trackingSpy).toHaveBeenCalledWith('some:page', 'show_buy_ci_minutes', {
+ label: 'free',
+ property: 'user_dropdown',
+ });
+ });
+
+ it('sends a tracking event when the dropdown is opened and contains Upgrade link', () => {
+ $('.js-nav-user-dropdown').trigger('shown.bs.dropdown');
+
+ expect(trackingSpy).toHaveBeenCalledWith('some:page', 'show_upgrade_link', {
label: 'free',
property: 'user_dropdown',
});
diff --git a/spec/frontend/helpers/class_spec_helper.js b/spec/frontend/helpers/class_spec_helper.js
index 7a60d33b471..b26f087f0c5 100644
--- a/spec/frontend/helpers/class_spec_helper.js
+++ b/spec/frontend/helpers/class_spec_helper.js
@@ -1,3 +1,4 @@
+// eslint-disable-next-line jest/no-export
export default class ClassSpecHelper {
static itShouldBeAStaticMethod(base, method) {
return it('should be a static method', () => {
diff --git a/spec/frontend/helpers/event_hub_factory_spec.js b/spec/frontend/helpers/event_hub_factory_spec.js
new file mode 100644
index 00000000000..dcfec6b836a
--- /dev/null
+++ b/spec/frontend/helpers/event_hub_factory_spec.js
@@ -0,0 +1,94 @@
+import createEventHub from '~/helpers/event_hub_factory';
+
+describe('event bus factory', () => {
+ let eventBus;
+
+ beforeEach(() => {
+ eventBus = createEventHub();
+ });
+
+ afterEach(() => {
+ eventBus = null;
+ });
+
+ describe('underlying module', () => {
+ let mitt;
+
+ beforeEach(() => {
+ jest.resetModules();
+ jest.mock('mitt');
+
+ // eslint-disable-next-line global-require
+ mitt = require('mitt');
+ mitt.mockReturnValue(() => ({}));
+
+ const createEventHubActual = jest.requireActual('~/helpers/event_hub_factory').default;
+ eventBus = createEventHubActual();
+ });
+
+ it('creates an emitter', () => {
+ expect(mitt).toHaveBeenCalled();
+ });
+ });
+
+ describe('instance', () => {
+ it.each`
+ method
+ ${'on'}
+ ${'once'}
+ ${'off'}
+ ${'emit'}
+ `('binds $$method to $method ', ({ method }) => {
+ expect(typeof eventBus[method]).toBe('function');
+ expect(eventBus[method]).toBe(eventBus[`$${method}`]);
+ });
+ });
+
+ describe('once', () => {
+ const event = 'foobar';
+ let handler;
+
+ beforeEach(() => {
+ jest.spyOn(eventBus, 'on');
+ jest.spyOn(eventBus, 'off');
+ handler = jest.fn();
+ eventBus.once(event, handler);
+ });
+
+ it('calls on internally', () => {
+ expect(eventBus.on).toHaveBeenCalled();
+ });
+
+ it('calls handler when event is emitted', () => {
+ eventBus.emit(event);
+ expect(handler).toHaveBeenCalled();
+ });
+
+ it('calls off when event is emitted', () => {
+ eventBus.emit(event);
+ expect(eventBus.off).toHaveBeenCalled();
+ });
+
+ it('calls the handler only once when event is emitted multiple times', () => {
+ eventBus.emit(event);
+ eventBus.emit(event);
+ expect(handler).toHaveBeenCalledTimes(1);
+ });
+
+ describe('when the handler thows an error', () => {
+ beforeEach(() => {
+ handler = jest.fn().mockImplementation(() => {
+ throw new Error();
+ });
+ eventBus.once(event, handler);
+ });
+
+ it('calls off when event is emitted', () => {
+ expect(() => {
+ eventBus.emit(event);
+ }).toThrow();
+ expect(eventBus.off).toHaveBeenCalled();
+ });
+ });
+ });
+});
diff --git a/spec/frontend/helpers/filtered_search_spec_helper.js b/spec/frontend/helpers/filtered_search_spec_helper.js
new file mode 100644
index 00000000000..ceb7982bbc3
--- /dev/null
+++ b/spec/frontend/helpers/filtered_search_spec_helper.js
@@ -0,0 +1,69 @@
+export default class FilteredSearchSpecHelper {
+ static createFilterVisualTokenHTML(name, operator, value, isSelected) {
+ return FilteredSearchSpecHelper.createFilterVisualToken(name, operator, value, isSelected)
+ .outerHTML;
+ }
+
+ static createFilterVisualToken(name, operator, value, isSelected = false) {
+ const li = document.createElement('li');
+ li.classList.add('js-visual-token', 'filtered-search-token', `search-token-${name}`);
+
+ li.innerHTML = `
+ <div class="selectable ${isSelected ? 'selected' : ''}" role="button">
+ <div class="name">${name}</div>
+ <div class="operator">${operator}</div>
+ <div class="value-container">
+ <div class="value">${value}</div>
+ <div class="remove-token" role="button">
+ <i class="fa fa-close"></i>
+ </div>
+ </div>
+ </div>
+ `;
+
+ return li;
+ }
+
+ static createNameFilterVisualTokenHTML(name) {
+ return `
+ <li class="js-visual-token filtered-search-token">
+ <div class="name">${name}</div>
+ </li>
+ `;
+ }
+
+ static createNameOperatorFilterVisualTokenHTML(name, operator) {
+ return `
+ <li class="js-visual-token filtered-search-token">
+ <div class="name">${name}</div>
+ <div class="operator">${operator}</div>
+ </li>
+ `;
+ }
+
+ static createSearchVisualToken(name) {
+ const li = document.createElement('li');
+ li.classList.add('js-visual-token', 'filtered-search-term');
+ li.innerHTML = `<div class="name">${name}</div>`;
+ return li;
+ }
+
+ static createSearchVisualTokenHTML(name) {
+ return FilteredSearchSpecHelper.createSearchVisualToken(name).outerHTML;
+ }
+
+ static createInputHTML(placeholder = '', value = '') {
+ return `
+ <li class="input-token">
+ <input type='text' class='filtered-search' placeholder='${placeholder}' value='${value}'/>
+ </li>
+ `;
+ }
+
+ static createTokensContainerHTML(html, inputPlaceholder) {
+ return `
+ ${html}
+ ${FilteredSearchSpecHelper.createInputHTML(inputPlaceholder)}
+ `;
+ }
+}
diff --git a/spec/frontend/helpers/fixtures.js b/spec/frontend/helpers/fixtures.js
index 778196843db..a89ceab3f8e 100644
--- a/spec/frontend/helpers/fixtures.js
+++ b/spec/frontend/helpers/fixtures.js
@@ -23,11 +23,12 @@ Did you run bin/rake frontend:fixtures?`,
export const getJSONFixture = relativePath => JSON.parse(getFixture(relativePath));
export const resetHTMLFixture = () => {
- document.body.textContent = '';
+ document.head.innerHTML = '';
+ document.body.innerHTML = '';
};
export const setHTMLFixture = (htmlContent, resetHook = afterEach) => {
- document.body.outerHTML = htmlContent;
+ document.body.innerHTML = htmlContent;
resetHook(resetHTMLFixture);
};
diff --git a/spec/frontend/helpers/set_window_location_helper.js b/spec/frontend/helpers/set_window_location_helper.js
new file mode 100644
index 00000000000..a94e73762c9
--- /dev/null
+++ b/spec/frontend/helpers/set_window_location_helper.js
@@ -0,0 +1,40 @@
+/**
+ * setWindowLocation allows for setting `window.location`
+ * (doing so directly is causing an error in jsdom)
+ *
+ * Example usage:
+ * assert(window.location.hash === undefined);
+ * setWindowLocation('http://example.com#foo')
+ * assert(window.location.hash === '#foo');
+ *
+ * More information:
+ * https://github.com/facebook/jest/issues/890
+ *
+ * @param url
+ */
+export default function setWindowLocation(url) {
+ const parsedUrl = new URL(url);
+
+ const newLocationValue = [
+ 'hash',
+ 'host',
+ 'hostname',
+ 'href',
+ 'origin',
+ 'pathname',
+ 'port',
+ 'protocol',
+ 'search',
+ ].reduce(
+ (location, prop) => ({
+ ...location,
+ [prop]: parsedUrl[prop],
+ }),
+ {},
+ );
+
+ Object.defineProperty(window, 'location', {
+ value: newLocationValue,
+ writable: true,
+ });
+}
diff --git a/spec/frontend/helpers/set_window_location_helper_spec.js b/spec/frontend/helpers/set_window_location_helper_spec.js
new file mode 100644
index 00000000000..2a2c024c824
--- /dev/null
+++ b/spec/frontend/helpers/set_window_location_helper_spec.js
@@ -0,0 +1,40 @@
+import setWindowLocation from './set_window_location_helper';
+
+describe('setWindowLocation', () => {
+ const originalLocation = window.location;
+
+ afterEach(() => {
+ window.location = originalLocation;
+ });
+
+ it.each`
+ url | property | value
+ ${'https://gitlab.com#foo'} | ${'hash'} | ${'#foo'}
+ ${'http://gitlab.com'} | ${'host'} | ${'gitlab.com'}
+ ${'http://gitlab.org'} | ${'hostname'} | ${'gitlab.org'}
+ ${'http://gitlab.org/foo#bar'} | ${'href'} | ${'http://gitlab.org/foo#bar'}
+ ${'http://gitlab.com'} | ${'origin'} | ${'http://gitlab.com'}
+ ${'http://gitlab.com/foo/bar/baz'} | ${'pathname'} | ${'/foo/bar/baz'}
+ ${'https://gitlab.com'} | ${'protocol'} | ${'https:'}
+ ${'http://gitlab.com#foo'} | ${'protocol'} | ${'http:'}
+ ${'http://gitlab.com:8080'} | ${'port'} | ${'8080'}
+ ${'http://gitlab.com?foo=bar&bar=foo'} | ${'search'} | ${'?foo=bar&bar=foo'}
+ `(
+ 'sets "window.location.$property" to be "$value" when called with: "$url"',
+ ({ url, property, value }) => {
+ expect(window.location).toBe(originalLocation);
+
+ setWindowLocation(url);
+
+ expect(window.location[property]).toBe(value);
+ },
+ );
+
+ it.each([null, 1, undefined, false, '', 'gitlab.com'])(
+ 'throws an error when called with an invalid url: "%s"',
+ invalidUrl => {
+ expect(() => setWindowLocation(invalidUrl)).toThrow(new TypeError('Invalid URL'));
+ expect(window.location).toBe(originalLocation);
+ },
+ );
+});
diff --git a/spec/frontend/helpers/vue_mount_component_helper.js b/spec/frontend/helpers/vue_mount_component_helper.js
index 6848c95d95d..615ff69a01c 100644
--- a/spec/frontend/helpers/vue_mount_component_helper.js
+++ b/spec/frontend/helpers/vue_mount_component_helper.js
@@ -1,22 +1,38 @@
import Vue from 'vue';
+/**
+ * Deprecated. Please do not use.
+ * Please see https://gitlab.com/groups/gitlab-org/-/epics/2445
+ */
const mountComponent = (Component, props = {}, el = null) =>
new Component({
propsData: props,
}).$mount(el);
+/**
+ * Deprecated. Please do not use.
+ * Please see https://gitlab.com/groups/gitlab-org/-/epics/2445
+ */
export const createComponentWithStore = (Component, store, propsData = {}) =>
new Component({
store,
propsData,
});
+/**
+ * Deprecated. Please do not use.
+ * Please see https://gitlab.com/groups/gitlab-org/-/epics/2445
+ */
export const mountComponentWithStore = (Component, { el, props, store }) =>
new Component({
store,
propsData: props || {},
}).$mount(el);
+/**
+ * Deprecated. Please do not use.
+ * Please see https://gitlab.com/groups/gitlab-org/-/epics/2445
+ */
export const mountComponentWithSlots = (Component, { props, slots }) => {
const component = new Component({
propsData: props || {},
@@ -30,9 +46,18 @@ export const mountComponentWithSlots = (Component, { props, slots }) => {
/**
* Mount a component with the given render method.
*
+ * -----------------------------
+ * Deprecated. Please do not use.
+ * Please see https://gitlab.com/groups/gitlab-org/-/epics/2445
+ * -----------------------------
+ *
* This helps with inserting slots that need to be compiled.
*/
export const mountComponentWithRender = (render, el = null) =>
mountComponent(Vue.extend({ render }), {}, el);
+/**
+ * Deprecated. Please do not use.
+ * Please see https://gitlab.com/groups/gitlab-org/-/epics/2445
+ */
export default mountComponent;
diff --git a/spec/frontend/helpers/web_worker_mock.js b/spec/frontend/helpers/web_worker_mock.js
new file mode 100644
index 00000000000..2b4a391e1d2
--- /dev/null
+++ b/spec/frontend/helpers/web_worker_mock.js
@@ -0,0 +1,10 @@
+/* eslint-disable class-methods-use-this */
+export default class WebWorkerMock {
+ addEventListener() {}
+
+ removeEventListener() {}
+
+ terminate() {}
+
+ postMessage() {}
+}
diff --git a/spec/frontend/ide/components/activity_bar_spec.js b/spec/frontend/ide/components/activity_bar_spec.js
new file mode 100644
index 00000000000..8b3853d4535
--- /dev/null
+++ b/spec/frontend/ide/components/activity_bar_spec.js
@@ -0,0 +1,72 @@
+import Vue from 'vue';
+import store from '~/ide/stores';
+import { leftSidebarViews } from '~/ide/constants';
+import ActivityBar from '~/ide/components/activity_bar.vue';
+import { createComponentWithStore } from '../../helpers/vue_mount_component_helper';
+import { resetStore } from '../helpers';
+
+describe('IDE activity bar', () => {
+ const Component = Vue.extend(ActivityBar);
+ let vm;
+
+ beforeEach(() => {
+ Vue.set(store.state.projects, 'abcproject', {
+ web_url: 'testing',
+ });
+ Vue.set(store.state, 'currentProjectId', 'abcproject');
+
+ vm = createComponentWithStore(Component, store);
+ });
+
+ afterEach(() => {
+ vm.$destroy();
+
+ resetStore(vm.$store);
+ });
+
+ describe('updateActivityBarView', () => {
+ beforeEach(() => {
+ jest.spyOn(vm, 'updateActivityBarView').mockImplementation(() => {});
+
+ vm.$mount();
+ });
+
+ it('calls updateActivityBarView with edit value on click', () => {
+ vm.$el.querySelector('.js-ide-edit-mode').click();
+
+ expect(vm.updateActivityBarView).toHaveBeenCalledWith(leftSidebarViews.edit.name);
+ });
+
+ it('calls updateActivityBarView with commit value on click', () => {
+ vm.$el.querySelector('.js-ide-commit-mode').click();
+
+ expect(vm.updateActivityBarView).toHaveBeenCalledWith(leftSidebarViews.commit.name);
+ });
+
+ it('calls updateActivityBarView with review value on click', () => {
+ vm.$el.querySelector('.js-ide-review-mode').click();
+
+ expect(vm.updateActivityBarView).toHaveBeenCalledWith(leftSidebarViews.review.name);
+ });
+ });
+
+ describe('active item', () => {
+ beforeEach(() => {
+ vm.$mount();
+ });
+
+ it('sets edit item active', () => {
+ expect(vm.$el.querySelector('.js-ide-edit-mode').classList).toContain('active');
+ });
+
+ it('sets commit item active', done => {
+ vm.$store.state.currentActivityView = leftSidebarViews.commit.name;
+
+ vm.$nextTick(() => {
+ expect(vm.$el.querySelector('.js-ide-commit-mode').classList).toContain('active');
+
+ done();
+ });
+ });
+ });
+});
diff --git a/spec/frontend/ide/components/commit_sidebar/editor_header_spec.js b/spec/frontend/ide/components/commit_sidebar/editor_header_spec.js
index a25aba61516..ff780939026 100644
--- a/spec/frontend/ide/components/commit_sidebar/editor_header_spec.js
+++ b/spec/frontend/ide/components/commit_sidebar/editor_header_spec.js
@@ -7,27 +7,32 @@ import { file } from '../../helpers';
const localVue = createLocalVue();
localVue.use(Vuex);
+const TEST_FILE_PATH = 'test/file/path';
+
describe('IDE commit editor header', () => {
let wrapper;
- let f;
let store;
- const findDiscardModal = () => wrapper.find({ ref: 'discardModal' });
- const findDiscardButton = () => wrapper.find({ ref: 'discardButton' });
-
- beforeEach(() => {
- f = file('file');
- store = createStore();
-
+ const createComponent = (fileProps = {}) => {
wrapper = mount(EditorHeader, {
store,
localVue,
propsData: {
- activeFile: f,
+ activeFile: {
+ ...file(TEST_FILE_PATH),
+ staged: true,
+ ...fileProps,
+ },
},
});
+ };
- jest.spyOn(wrapper.vm, 'discardChanges').mockImplementation();
+ const findDiscardModal = () => wrapper.find({ ref: 'discardModal' });
+ const findDiscardButton = () => wrapper.find({ ref: 'discardButton' });
+
+ beforeEach(() => {
+ store = createStore();
+ jest.spyOn(store, 'dispatch').mockImplementation();
});
afterEach(() => {
@@ -35,29 +40,38 @@ describe('IDE commit editor header', () => {
wrapper = null;
});
- it('renders button to discard', () => {
- expect(wrapper.vm.$el.querySelectorAll('.btn')).toHaveLength(1);
+ it.each`
+ fileProps | shouldExist
+ ${{ staged: false, changed: false }} | ${false}
+ ${{ staged: true, changed: false }} | ${true}
+ ${{ staged: false, changed: true }} | ${true}
+ ${{ staged: true, changed: true }} | ${true}
+ `('with $fileProps, show discard button is $shouldExist', ({ fileProps, shouldExist }) => {
+ createComponent(fileProps);
+
+ expect(findDiscardButton().exists()).toBe(shouldExist);
});
describe('discard button', () => {
- let modal;
-
beforeEach(() => {
- modal = findDiscardModal();
+ createComponent();
+ const modal = findDiscardModal();
jest.spyOn(modal.vm, 'show');
findDiscardButton().trigger('click');
});
it('opens a dialog confirming discard', () => {
- expect(modal.vm.show).toHaveBeenCalled();
+ expect(findDiscardModal().vm.show).toHaveBeenCalled();
});
it('calls discardFileChanges if dialog result is confirmed', () => {
- modal.vm.$emit('ok');
+ expect(store.dispatch).not.toHaveBeenCalled();
+
+ findDiscardModal().vm.$emit('ok');
- expect(wrapper.vm.discardChanges).toHaveBeenCalledWith(f.path);
+ expect(store.dispatch).toHaveBeenCalledWith('discardFileChanges', TEST_FILE_PATH);
});
});
});
diff --git a/spec/frontend/ide/components/commit_sidebar/form_spec.js b/spec/frontend/ide/components/commit_sidebar/form_spec.js
index dfde69ab2df..129180bb46e 100644
--- a/spec/frontend/ide/components/commit_sidebar/form_spec.js
+++ b/spec/frontend/ide/components/commit_sidebar/form_spec.js
@@ -1,6 +1,5 @@
import Vue from 'vue';
import { createComponentWithStore } from 'helpers/vue_mount_component_helper';
-import waitForPromises from 'helpers/wait_for_promises';
import { projectData } from 'jest/ide/mock_data';
import store from '~/ide/stores';
import CommitForm from '~/ide/components/commit_sidebar/form.vue';
@@ -31,10 +30,10 @@ describe('IDE commit form', () => {
});
describe('compact', () => {
- beforeEach(done => {
+ beforeEach(() => {
vm.isCompact = true;
- vm.$nextTick(done);
+ return vm.$nextTick();
});
it('renders commit button in compact mode', () => {
@@ -46,95 +45,84 @@ describe('IDE commit form', () => {
expect(vm.$el.querySelector('form')).toBeNull();
});
- it('renders overview text', done => {
+ it('renders overview text', () => {
vm.$store.state.stagedFiles.push('test');
- vm.$nextTick(() => {
+ return vm.$nextTick(() => {
expect(vm.$el.querySelector('p').textContent).toContain('1 changed file');
- done();
});
});
- it('shows form when clicking commit button', done => {
+ it('shows form when clicking commit button', () => {
vm.$el.querySelector('.btn-primary').click();
- vm.$nextTick(() => {
+ return vm.$nextTick(() => {
expect(vm.$el.querySelector('form')).not.toBeNull();
-
- done();
});
});
- it('toggles activity bar view when clicking commit button', done => {
+ it('toggles activity bar view when clicking commit button', () => {
vm.$el.querySelector('.btn-primary').click();
- vm.$nextTick(() => {
+ return vm.$nextTick(() => {
expect(store.state.currentActivityView).toBe(leftSidebarViews.commit.name);
-
- done();
});
});
- it('collapses if lastCommitMsg is set to empty and current view is not commit view', done => {
+ it('collapses if lastCommitMsg is set to empty and current view is not commit view', () => {
store.state.lastCommitMsg = 'abc';
store.state.currentActivityView = leftSidebarViews.edit.name;
- vm.$nextTick(() => {
- // if commit message is set, form is uncollapsed
- expect(vm.isCompact).toBe(false);
+ return vm
+ .$nextTick()
+ .then(() => {
+ // if commit message is set, form is uncollapsed
+ expect(vm.isCompact).toBe(false);
- store.state.lastCommitMsg = '';
+ store.state.lastCommitMsg = '';
- vm.$nextTick(() => {
+ return vm.$nextTick();
+ })
+ .then(() => {
// collapsed when set to empty
expect(vm.isCompact).toBe(true);
-
- done();
});
- });
});
});
describe('full', () => {
- beforeEach(done => {
+ beforeEach(() => {
vm.isCompact = false;
- vm.$nextTick(done);
+ return vm.$nextTick();
});
- it('updates commitMessage in store on input', done => {
+ it('updates commitMessage in store on input', () => {
const textarea = vm.$el.querySelector('textarea');
textarea.value = 'testing commit message';
textarea.dispatchEvent(new Event('input'));
- waitForPromises()
- .then(() => {
- expect(vm.$store.state.commit.commitMessage).toBe('testing commit message');
- })
- .then(done)
- .catch(done.fail);
+ return vm.$nextTick().then(() => {
+ expect(vm.$store.state.commit.commitMessage).toBe('testing commit message');
+ });
});
- it('updating currentActivityView not to commit view sets compact mode', done => {
+ it('updating currentActivityView not to commit view sets compact mode', () => {
store.state.currentActivityView = 'a';
- vm.$nextTick(() => {
+ return vm.$nextTick(() => {
expect(vm.isCompact).toBe(true);
-
- done();
});
});
- it('always opens itself in full view current activity view is not commit view when clicking commit button', done => {
+ it('always opens itself in full view current activity view is not commit view when clicking commit button', () => {
vm.$el.querySelector('.btn-primary').click();
- vm.$nextTick(() => {
+ return vm.$nextTick(() => {
expect(store.state.currentActivityView).toBe(leftSidebarViews.commit.name);
expect(vm.isCompact).toBe(false);
-
- done();
});
});
@@ -143,41 +131,54 @@ describe('IDE commit form', () => {
expect(vm.$el.querySelector('.btn-default').textContent).toContain('Collapse');
});
- it('resets commitMessage when clicking discard button', done => {
+ it('resets commitMessage when clicking discard button', () => {
vm.$store.state.commit.commitMessage = 'testing commit message';
- waitForPromises()
+ return vm
+ .$nextTick()
.then(() => {
vm.$el.querySelector('.btn-default').click();
})
- .then(Vue.nextTick)
+ .then(() => vm.$nextTick())
.then(() => {
expect(vm.$store.state.commit.commitMessage).not.toBe('testing commit message');
- })
- .then(done)
- .catch(done.fail);
+ });
});
});
describe('when submitting', () => {
beforeEach(() => {
- jest.spyOn(vm, 'commitChanges').mockImplementation(() => {});
+ jest.spyOn(vm, 'commitChanges');
+
vm.$store.state.stagedFiles.push('test');
+ vm.$store.state.commit.commitMessage = 'testing commit message';
});
- it('calls commitChanges', done => {
- vm.$store.state.commit.commitMessage = 'testing commit message';
+ it('calls commitChanges', () => {
+ vm.commitChanges.mockResolvedValue({ success: true });
+
+ return vm.$nextTick().then(() => {
+ vm.$el.querySelector('.btn-success').click();
+
+ expect(vm.commitChanges).toHaveBeenCalled();
+ });
+ });
+
+ it('opens new branch modal if commitChanges throws an error', () => {
+ vm.commitChanges.mockRejectedValue({ success: false });
- waitForPromises()
+ jest.spyOn(vm.$refs.createBranchModal, 'show').mockImplementation();
+
+ return vm
+ .$nextTick()
.then(() => {
vm.$el.querySelector('.btn-success').click();
+
+ return vm.$nextTick();
})
- .then(Vue.nextTick)
.then(() => {
- expect(vm.commitChanges).toHaveBeenCalled();
- })
- .then(done)
- .catch(done.fail);
+ expect(vm.$refs.createBranchModal.show).toHaveBeenCalled();
+ });
});
});
});
diff --git a/spec/frontend/ide/components/commit_sidebar/list_spec.js b/spec/frontend/ide/components/commit_sidebar/list_spec.js
index ee209487665..2b5664ffc4e 100644
--- a/spec/frontend/ide/components/commit_sidebar/list_spec.js
+++ b/spec/frontend/ide/components/commit_sidebar/list_spec.js
@@ -21,8 +21,6 @@ describe('Multi-file editor commit sidebar list', () => {
keyPrefix: 'staged',
});
- vm.$store.state.rightPanelCollapsed = false;
-
vm.$mount();
});
diff --git a/spec/frontend/ide/components/commit_sidebar/radio_group_spec.js b/spec/frontend/ide/components/commit_sidebar/radio_group_spec.js
new file mode 100644
index 00000000000..ac80ba58056
--- /dev/null
+++ b/spec/frontend/ide/components/commit_sidebar/radio_group_spec.js
@@ -0,0 +1,134 @@
+import Vue from 'vue';
+import { createComponentWithStore } from 'helpers/vue_mount_component_helper';
+import { resetStore } from 'jest/ide/helpers';
+import store from '~/ide/stores';
+import radioGroup from '~/ide/components/commit_sidebar/radio_group.vue';
+
+describe('IDE commit sidebar radio group', () => {
+ let vm;
+
+ beforeEach(done => {
+ const Component = Vue.extend(radioGroup);
+
+ store.state.commit.commitAction = '2';
+
+ vm = createComponentWithStore(Component, store, {
+ value: '1',
+ label: 'test',
+ checked: true,
+ });
+
+ vm.$mount();
+
+ Vue.nextTick(done);
+ });
+
+ afterEach(() => {
+ vm.$destroy();
+
+ resetStore(vm.$store);
+ });
+
+ it('uses label if present', () => {
+ expect(vm.$el.textContent).toContain('test');
+ });
+
+ it('uses slot if label is not present', done => {
+ vm.$destroy();
+
+ vm = new Vue({
+ components: {
+ radioGroup,
+ },
+ store,
+ render: createElement =>
+ createElement('radio-group', { props: { value: '1' } }, 'Testing slot'),
+ });
+
+ vm.$mount();
+
+ Vue.nextTick(() => {
+ expect(vm.$el.textContent).toContain('Testing slot');
+
+ done();
+ });
+ });
+
+ it('updates store when changing radio button', done => {
+ vm.$el.querySelector('input').dispatchEvent(new Event('change'));
+
+ Vue.nextTick(() => {
+ expect(store.state.commit.commitAction).toBe('1');
+
+ done();
+ });
+ });
+
+ describe('with input', () => {
+ beforeEach(done => {
+ vm.$destroy();
+
+ const Component = Vue.extend(radioGroup);
+
+ store.state.commit.commitAction = '1';
+ store.state.commit.newBranchName = 'test-123';
+
+ vm = createComponentWithStore(Component, store, {
+ value: '1',
+ label: 'test',
+ checked: true,
+ showInput: true,
+ });
+
+ vm.$mount();
+
+ Vue.nextTick(done);
+ });
+
+ it('renders input box when commitAction matches value', () => {
+ expect(vm.$el.querySelector('.form-control')).not.toBeNull();
+ });
+
+ it('hides input when commitAction doesnt match value', done => {
+ store.state.commit.commitAction = '2';
+
+ Vue.nextTick(() => {
+ expect(vm.$el.querySelector('.form-control')).toBeNull();
+ done();
+ });
+ });
+
+ it('updates branch name in store on input', done => {
+ const input = vm.$el.querySelector('.form-control');
+ input.value = 'testing-123';
+ input.dispatchEvent(new Event('input'));
+
+ Vue.nextTick(() => {
+ expect(store.state.commit.newBranchName).toBe('testing-123');
+
+ done();
+ });
+ });
+
+ it('renders newBranchName if present', () => {
+ const input = vm.$el.querySelector('.form-control');
+
+ expect(input.value).toBe('test-123');
+ });
+ });
+
+ describe('tooltipTitle', () => {
+ it('returns title when disabled', () => {
+ vm.title = 'test title';
+ vm.disabled = true;
+
+ expect(vm.tooltipTitle).toBe('test title');
+ });
+
+ it('returns blank when not disabled', () => {
+ vm.title = 'test title';
+
+ expect(vm.tooltipTitle).not.toBe('test title');
+ });
+ });
+});
diff --git a/spec/frontend/ide/components/file_row_extra_spec.js b/spec/frontend/ide/components/file_row_extra_spec.js
new file mode 100644
index 00000000000..e78bacadebb
--- /dev/null
+++ b/spec/frontend/ide/components/file_row_extra_spec.js
@@ -0,0 +1,170 @@
+import Vue from 'vue';
+import { createComponentWithStore } from 'helpers/vue_mount_component_helper';
+import { createStore } from '~/ide/stores';
+import FileRowExtra from '~/ide/components/file_row_extra.vue';
+import { file, resetStore } from '../helpers';
+
+describe('IDE extra file row component', () => {
+ let Component;
+ let vm;
+ let unstagedFilesCount = 0;
+ let stagedFilesCount = 0;
+ let changesCount = 0;
+
+ beforeAll(() => {
+ Component = Vue.extend(FileRowExtra);
+ });
+
+ beforeEach(() => {
+ vm = createComponentWithStore(Component, createStore(), {
+ file: {
+ ...file('test'),
+ },
+ dropdownOpen: false,
+ });
+
+ jest.spyOn(vm, 'getUnstagedFilesCountForPath', 'get').mockReturnValue(() => unstagedFilesCount);
+ jest.spyOn(vm, 'getStagedFilesCountForPath', 'get').mockReturnValue(() => stagedFilesCount);
+ jest.spyOn(vm, 'getChangesInFolder', 'get').mockReturnValue(() => changesCount);
+
+ vm.$mount();
+ });
+
+ afterEach(() => {
+ vm.$destroy();
+ resetStore(vm.$store);
+
+ stagedFilesCount = 0;
+ unstagedFilesCount = 0;
+ changesCount = 0;
+ });
+
+ describe('folderChangesTooltip', () => {
+ it('returns undefined when changes count is 0', () => {
+ changesCount = 0;
+
+ expect(vm.folderChangesTooltip).toBe(undefined);
+ });
+
+ [{ input: 1, output: '1 changed file' }, { input: 2, output: '2 changed files' }].forEach(
+ ({ input, output }) => {
+ it('returns changed files count if changes count is not 0', () => {
+ changesCount = input;
+
+ expect(vm.folderChangesTooltip).toBe(output);
+ });
+ },
+ );
+ });
+
+ describe('show tree changes count', () => {
+ it('does not show for blobs', () => {
+ vm.file.type = 'blob';
+
+ expect(vm.$el.querySelector('.ide-tree-changes')).toBe(null);
+ });
+
+ it('does not show when changes count is 0', () => {
+ vm.file.type = 'tree';
+
+ expect(vm.$el.querySelector('.ide-tree-changes')).toBe(null);
+ });
+
+ it('does not show when tree is open', done => {
+ vm.file.type = 'tree';
+ vm.file.opened = true;
+ changesCount = 1;
+
+ vm.$nextTick(() => {
+ expect(vm.$el.querySelector('.ide-tree-changes')).toBe(null);
+
+ done();
+ });
+ });
+
+ it('shows for trees with changes', done => {
+ vm.file.type = 'tree';
+ vm.file.opened = false;
+ changesCount = 1;
+
+ vm.$nextTick(() => {
+ expect(vm.$el.querySelector('.ide-tree-changes')).not.toBe(null);
+
+ done();
+ });
+ });
+ });
+
+ describe('changes file icon', () => {
+ it('hides when file is not changed', () => {
+ expect(vm.$el.querySelector('.file-changed-icon')).toBe(null);
+ });
+
+ it('shows when file is changed', done => {
+ vm.file.changed = true;
+
+ vm.$nextTick(() => {
+ expect(vm.$el.querySelector('.file-changed-icon')).not.toBe(null);
+
+ done();
+ });
+ });
+
+ it('shows when file is staged', done => {
+ vm.file.staged = true;
+
+ vm.$nextTick(() => {
+ expect(vm.$el.querySelector('.file-changed-icon')).not.toBe(null);
+
+ done();
+ });
+ });
+
+ it('shows when file is a tempFile', done => {
+ vm.file.tempFile = true;
+
+ vm.$nextTick(() => {
+ expect(vm.$el.querySelector('.file-changed-icon')).not.toBe(null);
+
+ done();
+ });
+ });
+
+ it('shows when file is renamed', done => {
+ vm.file.prevPath = 'original-file';
+
+ vm.$nextTick(() => {
+ expect(vm.$el.querySelector('.file-changed-icon')).not.toBe(null);
+
+ done();
+ });
+ });
+
+ it('hides when file is renamed', done => {
+ vm.file.prevPath = 'original-file';
+ vm.file.type = 'tree';
+
+ vm.$nextTick(() => {
+ expect(vm.$el.querySelector('.file-changed-icon')).toBe(null);
+
+ done();
+ });
+ });
+ });
+
+ describe('merge request icon', () => {
+ it('hides when not a merge request change', () => {
+ expect(vm.$el.querySelector('.ic-git-merge')).toBe(null);
+ });
+
+ it('shows when a merge request change', done => {
+ vm.file.mrChange = true;
+
+ vm.$nextTick(() => {
+ expect(vm.$el.querySelector('.ic-git-merge')).not.toBe(null);
+
+ done();
+ });
+ });
+ });
+});
diff --git a/spec/frontend/ide/components/file_templates/bar_spec.js b/spec/frontend/ide/components/file_templates/bar_spec.js
new file mode 100644
index 00000000000..21dbe18a223
--- /dev/null
+++ b/spec/frontend/ide/components/file_templates/bar_spec.js
@@ -0,0 +1,117 @@
+import Vue from 'vue';
+import { mountComponentWithStore } from 'helpers/vue_mount_component_helper';
+import { createStore } from '~/ide/stores';
+import Bar from '~/ide/components/file_templates/bar.vue';
+import { resetStore, file } from '../../helpers';
+
+describe('IDE file templates bar component', () => {
+ let Component;
+ let vm;
+
+ beforeAll(() => {
+ Component = Vue.extend(Bar);
+ });
+
+ beforeEach(() => {
+ const store = createStore();
+
+ store.state.openFiles.push({
+ ...file('file'),
+ opened: true,
+ active: true,
+ });
+
+ vm = mountComponentWithStore(Component, { store });
+ });
+
+ afterEach(() => {
+ vm.$destroy();
+ resetStore(vm.$store);
+ });
+
+ describe('template type dropdown', () => {
+ it('renders dropdown component', () => {
+ expect(vm.$el.querySelector('.dropdown').textContent).toContain('Choose a type');
+ });
+
+ it('calls setSelectedTemplateType when clicking item', () => {
+ jest.spyOn(vm, 'setSelectedTemplateType').mockImplementation();
+
+ vm.$el.querySelector('.dropdown-content button').click();
+
+ expect(vm.setSelectedTemplateType).toHaveBeenCalledWith({
+ name: '.gitlab-ci.yml',
+ key: 'gitlab_ci_ymls',
+ });
+ });
+ });
+
+ describe('template dropdown', () => {
+ beforeEach(done => {
+ vm.$store.state.fileTemplates.templates = [
+ {
+ name: 'test',
+ },
+ ];
+ vm.$store.state.fileTemplates.selectedTemplateType = {
+ name: '.gitlab-ci.yml',
+ key: 'gitlab_ci_ymls',
+ };
+
+ vm.$nextTick(done);
+ });
+
+ it('renders dropdown component', () => {
+ expect(vm.$el.querySelectorAll('.dropdown')[1].textContent).toContain('Choose a template');
+ });
+
+ it('calls fetchTemplate on click', () => {
+ jest.spyOn(vm, 'fetchTemplate').mockImplementation();
+
+ vm.$el
+ .querySelectorAll('.dropdown-content')[1]
+ .querySelector('button')
+ .click();
+
+ expect(vm.fetchTemplate).toHaveBeenCalledWith({
+ name: 'test',
+ });
+ });
+ });
+
+ it('shows undo button if updateSuccess is true', done => {
+ vm.$store.state.fileTemplates.updateSuccess = true;
+
+ vm.$nextTick(() => {
+ expect(vm.$el.querySelector('.btn-default').style.display).not.toBe('none');
+
+ done();
+ });
+ });
+
+ it('calls undoFileTemplate when clicking undo button', () => {
+ jest.spyOn(vm, 'undoFileTemplate').mockImplementation();
+
+ vm.$el.querySelector('.btn-default').click();
+
+ expect(vm.undoFileTemplate).toHaveBeenCalled();
+ });
+
+ it('calls setSelectedTemplateType if activeFile name matches a template', done => {
+ const fileName = '.gitlab-ci.yml';
+
+ jest.spyOn(vm, 'setSelectedTemplateType').mockImplementation(() => {});
+ vm.$store.state.openFiles[0].name = fileName;
+
+ vm.setInitialType();
+
+ vm.$nextTick(() => {
+ expect(vm.setSelectedTemplateType).toHaveBeenCalledWith({
+ name: fileName,
+ key: 'gitlab_ci_ymls',
+ });
+
+ done();
+ });
+ });
+});
diff --git a/spec/frontend/ide/components/ide_review_spec.js b/spec/frontend/ide/components/ide_review_spec.js
new file mode 100644
index 00000000000..b56957e1f6d
--- /dev/null
+++ b/spec/frontend/ide/components/ide_review_spec.js
@@ -0,0 +1,73 @@
+import Vue from 'vue';
+import IdeReview from '~/ide/components/ide_review.vue';
+import { createStore } from '~/ide/stores';
+import { createComponentWithStore } from '../../helpers/vue_mount_component_helper';
+import { trimText } from '../../helpers/text_helper';
+import { resetStore, file } from '../helpers';
+import { projectData } from '../mock_data';
+
+describe('IDE review mode', () => {
+ const Component = Vue.extend(IdeReview);
+ let vm;
+ let store;
+
+ beforeEach(() => {
+ store = createStore();
+ store.state.currentProjectId = 'abcproject';
+ store.state.currentBranchId = 'master';
+ store.state.projects.abcproject = { ...projectData };
+ Vue.set(store.state.trees, 'abcproject/master', {
+ tree: [file('fileName')],
+ loading: false,
+ });
+
+ vm = createComponentWithStore(Component, store).$mount();
+ });
+
+ afterEach(() => {
+ vm.$destroy();
+
+ resetStore(vm.$store);
+ });
+
+ it('renders list of files', () => {
+ expect(vm.$el.textContent).toContain('fileName');
+ });
+
+ describe('merge request', () => {
+ beforeEach(() => {
+ store.state.currentMergeRequestId = '1';
+ store.state.projects.abcproject.mergeRequests['1'] = {
+ iid: 123,
+ web_url: 'testing123',
+ };
+
+ return vm.$nextTick();
+ });
+
+ it('renders edit dropdown', () => {
+ expect(vm.$el.querySelector('.btn')).not.toBe(null);
+ });
+
+ it('renders merge request link & IID', () => {
+ store.state.viewer = 'mrdiff';
+
+ return vm.$nextTick(() => {
+ const link = vm.$el.querySelector('.ide-review-sub-header');
+
+ expect(link.querySelector('a').getAttribute('href')).toBe('testing123');
+ expect(trimText(link.textContent)).toBe('Merge request (!123)');
+ });
+ });
+
+ it('changes text to latest changes when viewer is not mrdiff', () => {
+ store.state.viewer = 'diff';
+
+ return vm.$nextTick(() => {
+ expect(trimText(vm.$el.querySelector('.ide-review-sub-header').textContent)).toBe(
+ 'Latest changes',
+ );
+ });
+ });
+ });
+});
diff --git a/spec/frontend/ide/components/ide_side_bar_spec.js b/spec/frontend/ide/components/ide_side_bar_spec.js
new file mode 100644
index 00000000000..65cad2e7eb0
--- /dev/null
+++ b/spec/frontend/ide/components/ide_side_bar_spec.js
@@ -0,0 +1,57 @@
+import Vue from 'vue';
+import { createComponentWithStore } from 'helpers/vue_mount_component_helper';
+import store from '~/ide/stores';
+import ideSidebar from '~/ide/components/ide_side_bar.vue';
+import { leftSidebarViews } from '~/ide/constants';
+import { resetStore } from '../helpers';
+import { projectData } from '../mock_data';
+
+describe('IdeSidebar', () => {
+ let vm;
+
+ beforeEach(() => {
+ const Component = Vue.extend(ideSidebar);
+
+ store.state.currentProjectId = 'abcproject';
+ store.state.projects.abcproject = projectData;
+
+ vm = createComponentWithStore(Component, store).$mount();
+ });
+
+ afterEach(() => {
+ vm.$destroy();
+
+ resetStore(vm.$store);
+ });
+
+ it('renders a sidebar', () => {
+ expect(vm.$el.querySelector('.multi-file-commit-panel-inner')).not.toBeNull();
+ });
+
+ it('renders loading icon component', done => {
+ vm.$store.state.loading = true;
+
+ vm.$nextTick(() => {
+ expect(vm.$el.querySelector('.multi-file-loading-container')).not.toBeNull();
+ expect(vm.$el.querySelectorAll('.multi-file-loading-container').length).toBe(3);
+
+ done();
+ });
+ });
+
+ describe('activityBarComponent', () => {
+ it('renders tree component', () => {
+ expect(vm.$el.querySelector('.ide-file-list')).not.toBeNull();
+ });
+
+ it('renders commit component', done => {
+ vm.$store.state.currentActivityView = leftSidebarViews.commit.name;
+
+ vm.$nextTick(() => {
+ expect(vm.$el.querySelector('.multi-file-commit-panel-section')).not.toBeNull();
+
+ done();
+ });
+ });
+ });
+});
diff --git a/spec/frontend/ide/components/ide_spec.js b/spec/frontend/ide/components/ide_spec.js
new file mode 100644
index 00000000000..78a280e6304
--- /dev/null
+++ b/spec/frontend/ide/components/ide_spec.js
@@ -0,0 +1,125 @@
+import Vue from 'vue';
+import { createComponentWithStore } from 'helpers/vue_mount_component_helper';
+import store from '~/ide/stores';
+import ide from '~/ide/components/ide.vue';
+import { file, resetStore } from '../helpers';
+import { projectData } from '../mock_data';
+
+function bootstrap(projData) {
+ const Component = Vue.extend(ide);
+
+ store.state.currentProjectId = 'abcproject';
+ store.state.currentBranchId = 'master';
+ store.state.projects.abcproject = { ...projData };
+ Vue.set(store.state.trees, 'abcproject/master', {
+ tree: [],
+ loading: false,
+ });
+
+ return createComponentWithStore(Component, store, {
+ emptyStateSvgPath: 'svg',
+ noChangesStateSvgPath: 'svg',
+ committedStateSvgPath: 'svg',
+ });
+}
+
+describe('ide component, empty repo', () => {
+ let vm;
+
+ beforeEach(() => {
+ const emptyProjData = { ...projectData, empty_repo: true, branches: {} };
+ vm = bootstrap(emptyProjData);
+ vm.$mount();
+ });
+
+ afterEach(() => {
+ vm.$destroy();
+
+ resetStore(vm.$store);
+ });
+
+ it('renders "New file" button in empty repo', done => {
+ vm.$nextTick(() => {
+ expect(vm.$el.querySelector('.ide-empty-state button[title="New file"]')).not.toBeNull();
+ done();
+ });
+ });
+});
+
+describe('ide component, non-empty repo', () => {
+ let vm;
+
+ beforeEach(() => {
+ vm = bootstrap(projectData);
+ vm.$mount();
+ });
+
+ afterEach(() => {
+ vm.$destroy();
+
+ resetStore(vm.$store);
+ });
+
+ it('shows error message when set', done => {
+ expect(vm.$el.querySelector('.gl-alert')).toBe(null);
+
+ vm.$store.state.errorMessage = {
+ text: 'error',
+ };
+
+ vm.$nextTick(() => {
+ expect(vm.$el.querySelector('.gl-alert')).not.toBe(null);
+
+ done();
+ });
+ });
+
+ describe('onBeforeUnload', () => {
+ it('returns undefined when no staged files or changed files', () => {
+ expect(vm.onBeforeUnload()).toBe(undefined);
+ });
+
+ it('returns warning text when their are changed files', () => {
+ vm.$store.state.changedFiles.push(file());
+
+ expect(vm.onBeforeUnload()).toBe('Are you sure you want to lose unsaved changes?');
+ });
+
+ it('returns warning text when their are staged files', () => {
+ vm.$store.state.stagedFiles.push(file());
+
+ expect(vm.onBeforeUnload()).toBe('Are you sure you want to lose unsaved changes?');
+ });
+
+ it('updates event object', () => {
+ const event = {};
+ vm.$store.state.stagedFiles.push(file());
+
+ vm.onBeforeUnload(event);
+
+ expect(event.returnValue).toBe('Are you sure you want to lose unsaved changes?');
+ });
+ });
+
+ describe('non-existent branch', () => {
+ it('does not render "New file" button for non-existent branch when repo is not empty', done => {
+ vm.$nextTick(() => {
+ expect(vm.$el.querySelector('.ide-empty-state button[title="New file"]')).toBeNull();
+ done();
+ });
+ });
+ });
+
+ describe('branch with files', () => {
+ beforeEach(() => {
+ store.state.trees['abcproject/master'].tree = [file()];
+ });
+
+ it('does not render "New file" button', done => {
+ vm.$nextTick(() => {
+ expect(vm.$el.querySelector('.ide-empty-state button[title="New file"]')).toBeNull();
+ done();
+ });
+ });
+ });
+});
diff --git a/spec/frontend/ide/components/ide_status_bar_spec.js b/spec/frontend/ide/components/ide_status_bar_spec.js
new file mode 100644
index 00000000000..bc8144f544c
--- /dev/null
+++ b/spec/frontend/ide/components/ide_status_bar_spec.js
@@ -0,0 +1,127 @@
+import Vue from 'vue';
+import _ from 'lodash';
+import { createComponentWithStore } from 'helpers/vue_mount_component_helper';
+import { TEST_HOST } from '../../helpers/test_constants';
+import { createStore } from '~/ide/stores';
+import IdeStatusBar from '~/ide/components/ide_status_bar.vue';
+import { rightSidebarViews } from '~/ide/constants';
+import { projectData } from '../mock_data';
+
+const TEST_PROJECT_ID = 'abcproject';
+const TEST_MERGE_REQUEST_ID = '9001';
+const TEST_MERGE_REQUEST_URL = `${TEST_HOST}merge-requests/${TEST_MERGE_REQUEST_ID}`;
+
+describe('ideStatusBar', () => {
+ let store;
+ let vm;
+
+ const createComponent = () => {
+ vm = createComponentWithStore(Vue.extend(IdeStatusBar), store).$mount();
+ };
+ const findMRStatus = () => vm.$el.querySelector('.js-ide-status-mr');
+
+ beforeEach(() => {
+ store = createStore();
+ store.state.currentProjectId = TEST_PROJECT_ID;
+ store.state.projects[TEST_PROJECT_ID] = _.clone(projectData);
+ store.state.currentBranchId = 'master';
+ });
+
+ afterEach(() => {
+ vm.$destroy();
+ });
+
+ describe('default', () => {
+ beforeEach(() => {
+ createComponent();
+ });
+
+ it('triggers a setInterval', () => {
+ expect(vm.intervalId).not.toBe(null);
+ });
+
+ it('renders the statusbar', () => {
+ expect(vm.$el.className).toBe('ide-status-bar');
+ });
+
+ describe('commitAgeUpdate', () => {
+ beforeEach(() => {
+ jest.spyOn(vm, 'commitAgeUpdate').mockImplementation(() => {});
+ });
+
+ afterEach(() => {
+ jest.clearAllTimers();
+ });
+
+ it('gets called every second', () => {
+ expect(vm.commitAgeUpdate).not.toHaveBeenCalled();
+
+ jest.advanceTimersByTime(1000);
+
+ expect(vm.commitAgeUpdate.mock.calls.length).toEqual(1);
+
+ jest.advanceTimersByTime(1000);
+
+ expect(vm.commitAgeUpdate.mock.calls.length).toEqual(2);
+ });
+ });
+
+ describe('getCommitPath', () => {
+ it('returns the path to the commit details', () => {
+ expect(vm.getCommitPath('abc123de')).toBe('/commit/abc123de');
+ });
+ });
+
+ describe('pipeline status', () => {
+ it('opens right sidebar on clicking icon', done => {
+ jest.spyOn(vm, 'openRightPane').mockImplementation(() => {});
+ Vue.set(vm.$store.state.pipelines, 'latestPipeline', {
+ details: {
+ status: {
+ text: 'success',
+ details_path: 'test',
+ icon: 'status_success',
+ },
+ },
+ commit: {
+ author_gravatar_url: 'www',
+ },
+ });
+
+ vm.$nextTick()
+ .then(() => {
+ vm.$el.querySelector('.ide-status-pipeline button').click();
+
+ expect(vm.openRightPane).toHaveBeenCalledWith(rightSidebarViews.pipelines);
+ })
+ .then(done)
+ .catch(done.fail);
+ });
+ });
+
+ it('does not show merge request status', () => {
+ expect(findMRStatus()).toBe(null);
+ });
+ });
+
+ describe('with merge request in store', () => {
+ beforeEach(() => {
+ store.state.projects[TEST_PROJECT_ID].mergeRequests = {
+ [TEST_MERGE_REQUEST_ID]: {
+ web_url: TEST_MERGE_REQUEST_URL,
+ references: {
+ short: `!${TEST_MERGE_REQUEST_ID}`,
+ },
+ },
+ };
+ store.state.currentMergeRequestId = TEST_MERGE_REQUEST_ID;
+
+ createComponent();
+ });
+
+ it('shows merge request status', () => {
+ expect(findMRStatus().textContent.trim()).toEqual(`Merge request !${TEST_MERGE_REQUEST_ID}`);
+ expect(findMRStatus().querySelector('a').href).toEqual(TEST_MERGE_REQUEST_URL);
+ });
+ });
+});
diff --git a/spec/frontend/ide/components/ide_tree_list_spec.js b/spec/frontend/ide/components/ide_tree_list_spec.js
new file mode 100644
index 00000000000..30f11db3153
--- /dev/null
+++ b/spec/frontend/ide/components/ide_tree_list_spec.js
@@ -0,0 +1,77 @@
+import Vue from 'vue';
+import IdeTreeList from '~/ide/components/ide_tree_list.vue';
+import store from '~/ide/stores';
+import { createComponentWithStore } from '../../helpers/vue_mount_component_helper';
+import { resetStore, file } from '../helpers';
+import { projectData } from '../mock_data';
+
+describe('IDE tree list', () => {
+ const Component = Vue.extend(IdeTreeList);
+ const normalBranchTree = [file('fileName')];
+ const emptyBranchTree = [];
+ let vm;
+
+ const bootstrapWithTree = (tree = normalBranchTree) => {
+ store.state.currentProjectId = 'abcproject';
+ store.state.currentBranchId = 'master';
+ store.state.projects.abcproject = { ...projectData };
+ Vue.set(store.state.trees, 'abcproject/master', {
+ tree,
+ loading: false,
+ });
+
+ vm = createComponentWithStore(Component, store, {
+ viewerType: 'edit',
+ });
+ };
+
+ afterEach(() => {
+ vm.$destroy();
+
+ resetStore(vm.$store);
+ });
+
+ describe('normal branch', () => {
+ beforeEach(() => {
+ bootstrapWithTree();
+
+ jest.spyOn(vm, 'updateViewer');
+
+ vm.$mount();
+ });
+
+ it('updates viewer on mount', () => {
+ expect(vm.updateViewer).toHaveBeenCalledWith('edit');
+ });
+
+ it('renders loading indicator', done => {
+ store.state.trees['abcproject/master'].loading = true;
+
+ vm.$nextTick(() => {
+ expect(vm.$el.querySelector('.multi-file-loading-container')).not.toBeNull();
+ expect(vm.$el.querySelectorAll('.multi-file-loading-container').length).toBe(3);
+
+ done();
+ });
+ });
+
+ it('renders list of files', () => {
+ expect(vm.$el.textContent).toContain('fileName');
+ });
+ });
+
+ describe('empty-branch state', () => {
+ beforeEach(() => {
+ bootstrapWithTree(emptyBranchTree);
+
+ jest.spyOn(vm, 'updateViewer');
+
+ vm.$mount();
+ });
+
+ it('does not load files if the branch is empty', () => {
+ expect(vm.$el.textContent).not.toContain('fileName');
+ expect(vm.$el.textContent).toContain('No files');
+ });
+ });
+});
diff --git a/spec/frontend/ide/components/ide_tree_spec.js b/spec/frontend/ide/components/ide_tree_spec.js
new file mode 100644
index 00000000000..01f007f09c3
--- /dev/null
+++ b/spec/frontend/ide/components/ide_tree_spec.js
@@ -0,0 +1,34 @@
+import Vue from 'vue';
+import IdeTree from '~/ide/components/ide_tree.vue';
+import store from '~/ide/stores';
+import { createComponentWithStore } from '../../helpers/vue_mount_component_helper';
+import { resetStore, file } from '../helpers';
+import { projectData } from '../mock_data';
+
+describe('IdeRepoTree', () => {
+ let vm;
+
+ beforeEach(() => {
+ const IdeRepoTree = Vue.extend(IdeTree);
+
+ store.state.currentProjectId = 'abcproject';
+ store.state.currentBranchId = 'master';
+ store.state.projects.abcproject = { ...projectData };
+ Vue.set(store.state.trees, 'abcproject/master', {
+ tree: [file('fileName')],
+ loading: false,
+ });
+
+ vm = createComponentWithStore(IdeRepoTree, store).$mount();
+ });
+
+ afterEach(() => {
+ vm.$destroy();
+
+ resetStore(vm.$store);
+ });
+
+ it('renders list of files', () => {
+ expect(vm.$el.textContent).toContain('fileName');
+ });
+});
diff --git a/spec/javascripts/ide/components/jobs/detail/description_spec.js b/spec/frontend/ide/components/jobs/detail/description_spec.js
index babae00d2f7..babae00d2f7 100644
--- a/spec/javascripts/ide/components/jobs/detail/description_spec.js
+++ b/spec/frontend/ide/components/jobs/detail/description_spec.js
diff --git a/spec/javascripts/ide/components/jobs/item_spec.js b/spec/frontend/ide/components/jobs/item_spec.js
index 2f97d39e98e..2f97d39e98e 100644
--- a/spec/javascripts/ide/components/jobs/item_spec.js
+++ b/spec/frontend/ide/components/jobs/item_spec.js
diff --git a/spec/frontend/ide/components/merge_requests/item_spec.js b/spec/frontend/ide/components/merge_requests/item_spec.js
new file mode 100644
index 00000000000..6a2451ad263
--- /dev/null
+++ b/spec/frontend/ide/components/merge_requests/item_spec.js
@@ -0,0 +1,63 @@
+import Vue from 'vue';
+import router from '~/ide/ide_router';
+import Item from '~/ide/components/merge_requests/item.vue';
+import mountCompontent from '../../../helpers/vue_mount_component_helper';
+
+describe('IDE merge request item', () => {
+ const Component = Vue.extend(Item);
+ let vm;
+
+ beforeEach(() => {
+ vm = mountCompontent(Component, {
+ item: {
+ iid: 1,
+ projectPathWithNamespace: 'gitlab-org/gitlab-ce',
+ title: 'Merge request title',
+ },
+ currentId: '1',
+ currentProjectId: 'gitlab-org/gitlab-ce',
+ });
+ });
+
+ afterEach(() => {
+ vm.$destroy();
+ });
+
+ it('renders merge requests data', () => {
+ expect(vm.$el.textContent).toContain('Merge request title');
+ expect(vm.$el.textContent).toContain('gitlab-org/gitlab-ce!1');
+ });
+
+ it('renders link with href', () => {
+ const expectedHref = router.resolve(
+ `/project/${vm.item.projectPathWithNamespace}/merge_requests/${vm.item.iid}`,
+ ).href;
+
+ expect(vm.$el.tagName.toLowerCase()).toBe('a');
+ expect(vm.$el).toHaveAttr('href', expectedHref);
+ });
+
+ it('renders icon if ID matches currentId', () => {
+ expect(vm.$el.querySelector('.ic-mobile-issue-close')).not.toBe(null);
+ });
+
+ it('does not render icon if ID does not match currentId', done => {
+ vm.currentId = '2';
+
+ vm.$nextTick(() => {
+ expect(vm.$el.querySelector('.ic-mobile-issue-close')).toBe(null);
+
+ done();
+ });
+ });
+
+ it('does not render icon if project ID does not match', done => {
+ vm.currentProjectId = 'test/test';
+
+ vm.$nextTick(() => {
+ expect(vm.$el.querySelector('.ic-mobile-issue-close')).toBe(null);
+
+ done();
+ });
+ });
+});
diff --git a/spec/frontend/ide/components/nav_dropdown_button_spec.js b/spec/frontend/ide/components/nav_dropdown_button_spec.js
new file mode 100644
index 00000000000..2aa3992a6d8
--- /dev/null
+++ b/spec/frontend/ide/components/nav_dropdown_button_spec.js
@@ -0,0 +1,93 @@
+import Vue from 'vue';
+import { trimText } from 'helpers/text_helper';
+import { mountComponentWithStore } from 'helpers/vue_mount_component_helper';
+import NavDropdownButton from '~/ide/components/nav_dropdown_button.vue';
+import { createStore } from '~/ide/stores';
+
+describe('NavDropdown', () => {
+ const TEST_BRANCH_ID = 'lorem-ipsum-dolar';
+ const TEST_MR_ID = '12345';
+ let store;
+ let vm;
+
+ beforeEach(() => {
+ store = createStore();
+ });
+
+ afterEach(() => {
+ vm.$destroy();
+ });
+
+ const createComponent = (props = {}) => {
+ vm = mountComponentWithStore(Vue.extend(NavDropdownButton), { props, store });
+ vm.$mount();
+ };
+
+ const findIcon = name => vm.$el.querySelector(`.ic-${name}`);
+ const findMRIcon = () => findIcon('merge-request');
+ const findBranchIcon = () => findIcon('branch');
+
+ describe('normal', () => {
+ beforeEach(() => {
+ createComponent();
+ });
+
+ it('renders empty placeholders, if state is falsey', () => {
+ expect(trimText(vm.$el.textContent)).toEqual('- -');
+ });
+
+ it('renders branch name, if state has currentBranchId', done => {
+ vm.$store.state.currentBranchId = TEST_BRANCH_ID;
+
+ vm.$nextTick()
+ .then(() => {
+ expect(trimText(vm.$el.textContent)).toEqual(`${TEST_BRANCH_ID} -`);
+ })
+ .then(done)
+ .catch(done.fail);
+ });
+
+ it('renders mr id, if state has currentMergeRequestId', done => {
+ vm.$store.state.currentMergeRequestId = TEST_MR_ID;
+
+ vm.$nextTick()
+ .then(() => {
+ expect(trimText(vm.$el.textContent)).toEqual(`- !${TEST_MR_ID}`);
+ })
+ .then(done)
+ .catch(done.fail);
+ });
+
+ it('renders branch and mr, if state has both', done => {
+ vm.$store.state.currentBranchId = TEST_BRANCH_ID;
+ vm.$store.state.currentMergeRequestId = TEST_MR_ID;
+
+ vm.$nextTick()
+ .then(() => {
+ expect(trimText(vm.$el.textContent)).toEqual(`${TEST_BRANCH_ID} !${TEST_MR_ID}`);
+ })
+ .then(done)
+ .catch(done.fail);
+ });
+
+ it('shows icons', () => {
+ expect(findBranchIcon()).toBeTruthy();
+ expect(findMRIcon()).toBeTruthy();
+ });
+ });
+
+ describe('with showMergeRequests false', () => {
+ beforeEach(() => {
+ createComponent({ showMergeRequests: false });
+ });
+
+ it('shows single empty placeholder, if state is falsey', () => {
+ expect(trimText(vm.$el.textContent)).toEqual('-');
+ });
+
+ it('shows only branch icon', () => {
+ expect(findBranchIcon()).toBeTruthy();
+ expect(findMRIcon()).toBe(null);
+ });
+ });
+});
diff --git a/spec/frontend/ide/components/nav_dropdown_spec.js b/spec/frontend/ide/components/nav_dropdown_spec.js
new file mode 100644
index 00000000000..ce123d925c8
--- /dev/null
+++ b/spec/frontend/ide/components/nav_dropdown_spec.js
@@ -0,0 +1,102 @@
+import $ from 'jquery';
+import { mount } from '@vue/test-utils';
+import { createStore } from '~/ide/stores';
+import NavDropdown from '~/ide/components/nav_dropdown.vue';
+import { PERMISSION_READ_MR } from '~/ide/constants';
+
+const TEST_PROJECT_ID = 'lorem-ipsum';
+
+describe('IDE NavDropdown', () => {
+ let store;
+ let wrapper;
+
+ beforeEach(() => {
+ store = createStore();
+ Object.assign(store.state, {
+ currentProjectId: TEST_PROJECT_ID,
+ currentBranchId: 'master',
+ projects: {
+ [TEST_PROJECT_ID]: {
+ userPermissions: {
+ [PERMISSION_READ_MR]: true,
+ },
+ branches: {
+ master: { id: 'master' },
+ },
+ },
+ },
+ });
+ jest.spyOn(store, 'dispatch').mockImplementation(() => {});
+ });
+
+ afterEach(() => {
+ wrapper.destroy();
+ });
+
+ const createComponent = () => {
+ wrapper = mount(NavDropdown, {
+ store,
+ });
+ };
+
+ const findIcon = name => wrapper.find(`.ic-${name}`);
+ const findMRIcon = () => findIcon('merge-request');
+ const findNavForm = () => wrapper.find('.ide-nav-form');
+ const showDropdown = () => {
+ $(wrapper.vm.$el).trigger('show.bs.dropdown');
+ };
+ const hideDropdown = () => {
+ $(wrapper.vm.$el).trigger('hide.bs.dropdown');
+ };
+
+ describe('default', () => {
+ beforeEach(() => {
+ createComponent();
+ });
+
+ it('renders nothing initially', () => {
+ expect(findNavForm().exists()).toBe(false);
+ });
+
+ it('renders nav form when show.bs.dropdown', done => {
+ showDropdown();
+
+ wrapper.vm
+ .$nextTick()
+ .then(() => {
+ expect(findNavForm().exists()).toBe(true);
+ })
+ .then(done)
+ .catch(done.fail);
+ });
+
+ it('destroys nav form when closed', done => {
+ showDropdown();
+ hideDropdown();
+
+ wrapper.vm
+ .$nextTick()
+ .then(() => {
+ expect(findNavForm().exists()).toBe(false);
+ })
+ .then(done)
+ .catch(done.fail);
+ });
+
+ it('renders merge request icon', () => {
+ expect(findMRIcon().exists()).toBe(true);
+ });
+ });
+
+ describe('when user cannot read merge requests', () => {
+ beforeEach(() => {
+ store.state.projects[TEST_PROJECT_ID].userPermissions = {};
+
+ createComponent();
+ });
+
+ it('does not render merge requests', () => {
+ expect(findMRIcon().exists()).toBe(false);
+ });
+ });
+});
diff --git a/spec/frontend/ide/components/new_dropdown/button_spec.js b/spec/frontend/ide/components/new_dropdown/button_spec.js
new file mode 100644
index 00000000000..3c611b7de8f
--- /dev/null
+++ b/spec/frontend/ide/components/new_dropdown/button_spec.js
@@ -0,0 +1,65 @@
+import Vue from 'vue';
+import mountComponent from 'helpers/vue_mount_component_helper';
+import Button from '~/ide/components/new_dropdown/button.vue';
+
+describe('IDE new entry dropdown button component', () => {
+ let Component;
+ let vm;
+
+ beforeAll(() => {
+ Component = Vue.extend(Button);
+ });
+
+ beforeEach(() => {
+ vm = mountComponent(Component, {
+ label: 'Testing',
+ icon: 'doc-new',
+ });
+
+ jest.spyOn(vm, '$emit').mockImplementation(() => {});
+ });
+
+ afterEach(() => {
+ vm.$destroy();
+ });
+
+ it('renders button with label', () => {
+ expect(vm.$el.textContent).toContain('Testing');
+ });
+
+ it('renders icon', () => {
+ expect(vm.$el.querySelector('.ic-doc-new')).not.toBe(null);
+ });
+
+ it('emits click event', () => {
+ vm.$el.click();
+
+ expect(vm.$emit).toHaveBeenCalledWith('click');
+ });
+
+ it('hides label if showLabel is false', done => {
+ vm.showLabel = false;
+
+ vm.$nextTick(() => {
+ expect(vm.$el.textContent).not.toContain('Testing');
+
+ done();
+ });
+ });
+
+ describe('tooltipTitle', () => {
+ it('returns empty string when showLabel is true', () => {
+ expect(vm.tooltipTitle).toBe('');
+ });
+
+ it('returns label', done => {
+ vm.showLabel = false;
+
+ vm.$nextTick(() => {
+ expect(vm.tooltipTitle).toBe('Testing');
+
+ done();
+ });
+ });
+ });
+});
diff --git a/spec/frontend/ide/components/new_dropdown/index_spec.js b/spec/frontend/ide/components/new_dropdown/index_spec.js
new file mode 100644
index 00000000000..00781c16609
--- /dev/null
+++ b/spec/frontend/ide/components/new_dropdown/index_spec.js
@@ -0,0 +1,84 @@
+import Vue from 'vue';
+import { createComponentWithStore } from 'helpers/vue_mount_component_helper';
+import store from '~/ide/stores';
+import newDropdown from '~/ide/components/new_dropdown/index.vue';
+import { resetStore } from '../../helpers';
+
+describe('new dropdown component', () => {
+ let vm;
+
+ beforeEach(() => {
+ const component = Vue.extend(newDropdown);
+
+ vm = createComponentWithStore(component, store, {
+ branch: 'master',
+ path: '',
+ mouseOver: false,
+ type: 'tree',
+ });
+
+ vm.$store.state.currentProjectId = 'abcproject';
+ vm.$store.state.path = '';
+ vm.$store.state.trees['abcproject/mybranch'] = {
+ tree: [],
+ };
+
+ vm.$mount();
+
+ jest.spyOn(vm.$refs.newModal, 'open').mockImplementation(() => {});
+ });
+
+ afterEach(() => {
+ vm.$destroy();
+
+ resetStore(vm.$store);
+ });
+
+ it('renders new file, upload and new directory links', () => {
+ const buttons = vm.$el.querySelectorAll('.dropdown-menu button');
+
+ expect(buttons[0].textContent.trim()).toBe('New file');
+ expect(buttons[1].textContent.trim()).toBe('Upload file');
+ expect(buttons[2].textContent.trim()).toBe('New directory');
+ });
+
+ describe('createNewItem', () => {
+ it('opens modal for a blob when new file is clicked', () => {
+ vm.$el.querySelectorAll('.dropdown-menu button')[0].click();
+
+ expect(vm.$refs.newModal.open).toHaveBeenCalledWith('blob', '');
+ });
+
+ it('opens modal for a tree when new directory is clicked', () => {
+ vm.$el.querySelectorAll('.dropdown-menu button')[2].click();
+
+ expect(vm.$refs.newModal.open).toHaveBeenCalledWith('tree', '');
+ });
+ });
+
+ describe('isOpen', () => {
+ it('scrolls dropdown into view', done => {
+ jest.spyOn(vm.$refs.dropdownMenu, 'scrollIntoView').mockImplementation(() => {});
+
+ vm.isOpen = true;
+
+ setImmediate(() => {
+ expect(vm.$refs.dropdownMenu.scrollIntoView).toHaveBeenCalledWith({
+ block: 'nearest',
+ });
+
+ done();
+ });
+ });
+ });
+
+ describe('delete entry', () => {
+ it('calls delete action', () => {
+ jest.spyOn(vm, 'deleteEntry').mockImplementation(() => {});
+
+ vm.$el.querySelectorAll('.dropdown-menu button')[4].click();
+
+ expect(vm.deleteEntry).toHaveBeenCalledWith('');
+ });
+ });
+});
diff --git a/spec/frontend/ide/components/new_dropdown/modal_spec.js b/spec/frontend/ide/components/new_dropdown/modal_spec.js
new file mode 100644
index 00000000000..62a59a76bf4
--- /dev/null
+++ b/spec/frontend/ide/components/new_dropdown/modal_spec.js
@@ -0,0 +1,175 @@
+import Vue from 'vue';
+import { createComponentWithStore } from 'helpers/vue_mount_component_helper';
+import { createStore } from '~/ide/stores';
+import modal from '~/ide/components/new_dropdown/modal.vue';
+import createFlash from '~/flash';
+
+jest.mock('~/flash');
+
+describe('new file modal component', () => {
+ const Component = Vue.extend(modal);
+ let vm;
+
+ afterEach(() => {
+ vm.$destroy();
+ });
+
+ describe.each`
+ entryType | modalTitle | btnTitle | showsFileTemplates
+ ${'tree'} | ${'Create new directory'} | ${'Create directory'} | ${false}
+ ${'blob'} | ${'Create new file'} | ${'Create file'} | ${true}
+ `('$entryType', ({ entryType, modalTitle, btnTitle, showsFileTemplates }) => {
+ beforeEach(done => {
+ const store = createStore();
+
+ vm = createComponentWithStore(Component, store).$mount();
+ vm.open(entryType);
+ vm.name = 'testing';
+
+ vm.$nextTick(done);
+ });
+
+ afterEach(() => {
+ vm.close();
+ });
+
+ it(`sets modal title as ${entryType}`, () => {
+ expect(document.querySelector('.modal-title').textContent.trim()).toBe(modalTitle);
+ });
+
+ it(`sets button label as ${entryType}`, () => {
+ expect(document.querySelector('.btn-success').textContent.trim()).toBe(btnTitle);
+ });
+
+ it(`sets form label as ${entryType}`, () => {
+ expect(document.querySelector('.label-bold').textContent.trim()).toBe('Name');
+ });
+
+ it(`shows file templates: ${showsFileTemplates}`, () => {
+ const templateFilesEl = document.querySelector('.file-templates');
+ expect(Boolean(templateFilesEl)).toBe(showsFileTemplates);
+ });
+ });
+
+ describe('rename entry', () => {
+ beforeEach(() => {
+ const store = createStore();
+ store.state.entries = {
+ 'test-path': {
+ name: 'test',
+ type: 'blob',
+ path: 'test-path',
+ },
+ };
+
+ vm = createComponentWithStore(Component, store).$mount();
+ });
+
+ it.each`
+ entryType | modalTitle | btnTitle
+ ${'tree'} | ${'Rename folder'} | ${'Rename folder'}
+ ${'blob'} | ${'Rename file'} | ${'Rename file'}
+ `(
+ 'renders title and button for renaming $entryType',
+ ({ entryType, modalTitle, btnTitle }, done) => {
+ vm.$store.state.entries['test-path'].type = entryType;
+ vm.open('rename', 'test-path');
+
+ vm.$nextTick(() => {
+ expect(document.querySelector('.modal-title').textContent.trim()).toBe(modalTitle);
+ expect(document.querySelector('.btn-success').textContent.trim()).toBe(btnTitle);
+
+ done();
+ });
+ },
+ );
+
+ describe('entryName', () => {
+ it('returns entries name', () => {
+ vm.open('rename', 'test-path');
+
+ expect(vm.entryName).toBe('test-path');
+ });
+
+ it('does not reset entryName to its old value if empty', () => {
+ vm.entryName = 'hello';
+ vm.entryName = '';
+
+ expect(vm.entryName).toBe('');
+ });
+ });
+
+ describe('open', () => {
+ it('sets entryName to path provided if modalType is rename', () => {
+ vm.open('rename', 'test-path');
+
+ expect(vm.entryName).toBe('test-path');
+ });
+
+ it("appends '/' to the path if modalType isn't rename", () => {
+ vm.open('blob', 'test-path');
+
+ expect(vm.entryName).toBe('test-path/');
+ });
+
+ it('leaves entryName blank if no path is provided', () => {
+ vm.open('blob');
+
+ expect(vm.entryName).toBe('');
+ });
+ });
+ });
+
+ describe('submitForm', () => {
+ let store;
+
+ beforeEach(() => {
+ store = createStore();
+ store.state.entries = {
+ 'test-path/test': {
+ name: 'test',
+ deleted: false,
+ },
+ };
+
+ vm = createComponentWithStore(Component, store).$mount();
+ });
+
+ it('throws an error when target entry exists', () => {
+ vm.open('rename', 'test-path/test');
+
+ expect(createFlash).not.toHaveBeenCalled();
+
+ vm.submitForm();
+
+ expect(createFlash).toHaveBeenCalledWith(
+ 'The name "test-path/test" is already taken in this directory.',
+ 'alert',
+ expect.anything(),
+ null,
+ false,
+ true,
+ );
+ });
+
+ it('does not throw error when target entry does not exist', () => {
+ jest.spyOn(vm, 'renameEntry').mockImplementation();
+
+ vm.open('rename', 'test-path/test');
+ vm.entryName = 'test-path/test2';
+ vm.submitForm();
+
+ expect(createFlash).not.toHaveBeenCalled();
+ });
+
+ it('removes leading/trailing found in the new name', () => {
+ vm.open('rename', 'test-path/test');
+
+ vm.entryName = 'test-path /test';
+
+ vm.submitForm();
+
+ expect(vm.entryName).toBe('test-path/test');
+ });
+ });
+});
diff --git a/spec/frontend/ide/components/new_dropdown/upload_spec.js b/spec/frontend/ide/components/new_dropdown/upload_spec.js
new file mode 100644
index 00000000000..a418fdeb572
--- /dev/null
+++ b/spec/frontend/ide/components/new_dropdown/upload_spec.js
@@ -0,0 +1,112 @@
+import Vue from 'vue';
+import createComponent from 'helpers/vue_mount_component_helper';
+import upload from '~/ide/components/new_dropdown/upload.vue';
+
+describe('new dropdown upload', () => {
+ let vm;
+
+ beforeEach(() => {
+ const Component = Vue.extend(upload);
+
+ vm = createComponent(Component, {
+ path: '',
+ });
+
+ vm.entryName = 'testing';
+
+ jest.spyOn(vm, '$emit');
+ });
+
+ afterEach(() => {
+ vm.$destroy();
+ });
+
+ describe('openFile', () => {
+ it('calls for each file', () => {
+ const files = ['test', 'test2', 'test3'];
+
+ jest.spyOn(vm, 'readFile').mockImplementation(() => {});
+ jest.spyOn(vm.$refs.fileUpload, 'files', 'get').mockReturnValue(files);
+
+ vm.openFile();
+
+ expect(vm.readFile.mock.calls.length).toBe(3);
+
+ files.forEach((file, i) => {
+ expect(vm.readFile.mock.calls[i]).toEqual([file]);
+ });
+ });
+ });
+
+ describe('readFile', () => {
+ beforeEach(() => {
+ jest.spyOn(FileReader.prototype, 'readAsDataURL').mockImplementation(() => {});
+ });
+
+ it('calls readAsDataURL for all files', () => {
+ const file = {
+ type: 'images/png',
+ };
+
+ vm.readFile(file);
+
+ expect(FileReader.prototype.readAsDataURL).toHaveBeenCalledWith(file);
+ });
+ });
+
+ describe('createFile', () => {
+ const textTarget = {
+ result: 'base64,cGxhaW4gdGV4dA==',
+ };
+ const binaryTarget = {
+ result: 'base64,w4I=',
+ };
+ const textFile = new File(['plain text'], 'textFile');
+
+ const binaryFile = {
+ name: 'binaryFile',
+ type: 'image/png',
+ };
+
+ beforeEach(() => {
+ jest.spyOn(FileReader.prototype, 'readAsText');
+ });
+
+ it('calls readAsText and creates file in plain text (without encoding) if the file content is plain text', done => {
+ const waitForCreate = new Promise(resolve => vm.$on('create', resolve));
+
+ vm.createFile(textTarget, textFile);
+
+ expect(FileReader.prototype.readAsText).toHaveBeenCalledWith(textFile);
+
+ waitForCreate
+ .then(() => {
+ expect(vm.$emit).toHaveBeenCalledWith('create', {
+ name: textFile.name,
+ type: 'blob',
+ content: 'plain text',
+ base64: false,
+ binary: false,
+ rawPath: '',
+ });
+ })
+ .then(done)
+ .catch(done.fail);
+ });
+
+ it('splits content on base64 if binary', () => {
+ vm.createFile(binaryTarget, binaryFile);
+
+ expect(FileReader.prototype.readAsText).not.toHaveBeenCalledWith(textFile);
+
+ expect(vm.$emit).toHaveBeenCalledWith('create', {
+ name: binaryFile.name,
+ type: 'blob',
+ content: binaryTarget.result.split('base64,')[1],
+ base64: true,
+ binary: true,
+ rawPath: binaryTarget.result,
+ });
+ });
+ });
+});
diff --git a/spec/frontend/ide/components/pipelines/list_spec.js b/spec/frontend/ide/components/pipelines/list_spec.js
index 11e672b6685..d909a5e478e 100644
--- a/spec/frontend/ide/components/pipelines/list_spec.js
+++ b/spec/frontend/ide/components/pipelines/list_spec.js
@@ -7,10 +7,15 @@ import JobsList from '~/ide/components/jobs/list.vue';
import Tab from '~/vue_shared/components/tabs/tab.vue';
import CiIcon from '~/vue_shared/components/ci_icon.vue';
import { pipelines } from '../../../../javascripts/ide/mock_data';
+import IDEServices from '~/ide/services';
const localVue = createLocalVue();
localVue.use(Vuex);
+jest.mock('~/ide/services', () => ({
+ pingUsage: jest.fn(),
+}));
+
describe('IDE pipelines list', () => {
let wrapper;
@@ -25,14 +30,18 @@ describe('IDE pipelines list', () => {
};
const fetchLatestPipelineMock = jest.fn();
+ const pingUsageMock = jest.fn();
const failedStagesGetterMock = jest.fn().mockReturnValue([]);
+ const fakeProjectPath = 'alpha/beta';
const createComponent = (state = {}) => {
const { pipelines: pipelinesState, ...restOfState } = state;
const { defaultPipelines, ...defaultRestOfState } = defaultState;
const fakeStore = new Vuex.Store({
- getters: { currentProject: () => ({ web_url: 'some/url ' }) },
+ getters: {
+ currentProject: () => ({ web_url: 'some/url ', path_with_namespace: fakeProjectPath }),
+ },
state: {
...defaultRestOfState,
...restOfState,
@@ -46,6 +55,7 @@ describe('IDE pipelines list', () => {
},
actions: {
fetchLatestPipeline: fetchLatestPipelineMock,
+ pingUsage: pingUsageMock,
},
getters: {
jobsCount: () => 1,
@@ -77,6 +87,11 @@ describe('IDE pipelines list', () => {
expect(fetchLatestPipelineMock).toHaveBeenCalled();
});
+ it('pings pipeline usage', () => {
+ createComponent();
+ expect(IDEServices.pingUsage).toHaveBeenCalledWith(fakeProjectPath);
+ });
+
describe('when loading', () => {
let defaultPipelinesLoadingState;
beforeAll(() => {
diff --git a/spec/frontend/ide/components/preview/clientside_spec.js b/spec/frontend/ide/components/preview/clientside_spec.js
index 0cde6fb6060..7b2025f5e9f 100644
--- a/spec/frontend/ide/components/preview/clientside_spec.js
+++ b/spec/frontend/ide/components/preview/clientside_spec.js
@@ -70,14 +70,6 @@ describe('IDE clientside preview', () => {
});
};
- beforeAll(() => {
- jest.useFakeTimers();
- });
-
- afterAll(() => {
- jest.useRealTimers();
- });
-
afterEach(() => {
wrapper.destroy();
});
diff --git a/spec/frontend/ide/components/repo_commit_section_spec.js b/spec/frontend/ide/components/repo_commit_section_spec.js
index 5ea03eb1593..237be018807 100644
--- a/spec/frontend/ide/components/repo_commit_section_spec.js
+++ b/spec/frontend/ide/components/repo_commit_section_spec.js
@@ -36,7 +36,6 @@ describe('RepoCommitSection', () => {
}),
);
- store.state.rightPanelCollapsed = false;
store.state.currentBranch = 'master';
store.state.changedFiles = [];
store.state.stagedFiles = [{ ...files[0] }, { ...files[1] }];
diff --git a/spec/frontend/ide/components/repo_tab_spec.js b/spec/frontend/ide/components/repo_tab_spec.js
new file mode 100644
index 00000000000..82ea73ffbb1
--- /dev/null
+++ b/spec/frontend/ide/components/repo_tab_spec.js
@@ -0,0 +1,185 @@
+import Vue from 'vue';
+import store from '~/ide/stores';
+import repoTab from '~/ide/components/repo_tab.vue';
+import router from '~/ide/ide_router';
+import { file, resetStore } from '../helpers';
+
+describe('RepoTab', () => {
+ let vm;
+
+ function createComponent(propsData) {
+ const RepoTab = Vue.extend(repoTab);
+
+ return new RepoTab({
+ store,
+ propsData,
+ }).$mount();
+ }
+
+ beforeEach(() => {
+ jest.spyOn(router, 'push').mockImplementation(() => {});
+ });
+
+ afterEach(() => {
+ vm.$destroy();
+
+ resetStore(vm.$store);
+ });
+
+ it('renders a close link and a name link', () => {
+ vm = createComponent({
+ tab: file(),
+ });
+ vm.$store.state.openFiles.push(vm.tab);
+ const close = vm.$el.querySelector('.multi-file-tab-close');
+ const name = vm.$el.querySelector(`[title="${vm.tab.url}"]`);
+
+ expect(close.innerHTML).toContain('#close');
+ expect(name.textContent.trim()).toEqual(vm.tab.name);
+ });
+
+ it('does not call openPendingTab when tab is active', done => {
+ vm = createComponent({
+ tab: {
+ ...file(),
+ pending: true,
+ active: true,
+ },
+ });
+
+ jest.spyOn(vm, 'openPendingTab').mockImplementation(() => {});
+
+ vm.$el.click();
+
+ vm.$nextTick(() => {
+ expect(vm.openPendingTab).not.toHaveBeenCalled();
+
+ done();
+ });
+ });
+
+ it('fires clickFile when the link is clicked', () => {
+ vm = createComponent({
+ tab: file(),
+ });
+
+ jest.spyOn(vm, 'clickFile').mockImplementation(() => {});
+
+ vm.$el.click();
+
+ expect(vm.clickFile).toHaveBeenCalledWith(vm.tab);
+ });
+
+ it('calls closeFile when clicking close button', () => {
+ vm = createComponent({
+ tab: file(),
+ });
+
+ jest.spyOn(vm, 'closeFile').mockImplementation(() => {});
+
+ vm.$el.querySelector('.multi-file-tab-close').click();
+
+ expect(vm.closeFile).toHaveBeenCalledWith(vm.tab);
+ });
+
+ it('changes icon on hover', done => {
+ const tab = file();
+ tab.changed = true;
+ vm = createComponent({
+ tab,
+ });
+
+ vm.$el.dispatchEvent(new Event('mouseover'));
+
+ Vue.nextTick()
+ .then(() => {
+ expect(vm.$el.querySelector('.file-modified')).toBeNull();
+
+ vm.$el.dispatchEvent(new Event('mouseout'));
+ })
+ .then(Vue.nextTick)
+ .then(() => {
+ expect(vm.$el.querySelector('.file-modified')).not.toBeNull();
+
+ done();
+ })
+ .catch(done.fail);
+ });
+
+ describe('locked file', () => {
+ let f;
+
+ beforeEach(() => {
+ f = file('locked file');
+ f.file_lock = {
+ user: {
+ name: 'testuser',
+ updated_at: new Date(),
+ },
+ };
+
+ vm = createComponent({
+ tab: f,
+ });
+ });
+
+ afterEach(() => {
+ vm.$destroy();
+ });
+
+ it('renders lock icon', () => {
+ expect(vm.$el.querySelector('.file-status-icon')).not.toBeNull();
+ });
+
+ it('renders a tooltip', () => {
+ expect(vm.$el.querySelector('span:nth-child(2)').dataset.originalTitle).toContain(
+ 'Locked by testuser',
+ );
+ });
+ });
+
+ describe('methods', () => {
+ describe('closeTab', () => {
+ it('closes tab if file has changed', done => {
+ const tab = file();
+ tab.changed = true;
+ tab.opened = true;
+ vm = createComponent({
+ tab,
+ });
+ vm.$store.state.openFiles.push(tab);
+ vm.$store.state.changedFiles.push(tab);
+ vm.$store.state.entries[tab.path] = tab;
+ vm.$store.dispatch('setFileActive', tab.path);
+
+ vm.$el.querySelector('.multi-file-tab-close').click();
+
+ vm.$nextTick(() => {
+ expect(tab.opened).toBeFalsy();
+ expect(vm.$store.state.changedFiles.length).toBe(1);
+
+ done();
+ });
+ });
+
+ it('closes tab when clicking close btn', done => {
+ const tab = file('lose');
+ tab.opened = true;
+ vm = createComponent({
+ tab,
+ });
+ vm.$store.state.openFiles.push(tab);
+ vm.$store.state.entries[tab.path] = tab;
+ vm.$store.dispatch('setFileActive', tab.path);
+
+ vm.$el.querySelector('.multi-file-tab-close').click();
+
+ vm.$nextTick(() => {
+ expect(tab.opened).toBeFalsy();
+
+ done();
+ });
+ });
+ });
+ });
+});
diff --git a/spec/javascripts/ide/components/repo_tabs_spec.js b/spec/frontend/ide/components/repo_tabs_spec.js
index 583f71e6121..583f71e6121 100644
--- a/spec/javascripts/ide/components/repo_tabs_spec.js
+++ b/spec/frontend/ide/components/repo_tabs_spec.js
diff --git a/spec/frontend/ide/components/shared/tokened_input_spec.js b/spec/frontend/ide/components/shared/tokened_input_spec.js
new file mode 100644
index 00000000000..e687216bd06
--- /dev/null
+++ b/spec/frontend/ide/components/shared/tokened_input_spec.js
@@ -0,0 +1,133 @@
+import Vue from 'vue';
+import mountComponent from 'helpers/vue_mount_component_helper';
+import TokenedInput from '~/ide/components/shared/tokened_input.vue';
+
+const TEST_PLACEHOLDER = 'Searching in test';
+const TEST_TOKENS = [
+ { label: 'lorem', id: 1 },
+ { label: 'ipsum', id: 2 },
+ { label: 'dolar', id: 3 },
+];
+const TEST_VALUE = 'lorem';
+
+function getTokenElements(vm) {
+ return Array.from(vm.$el.querySelectorAll('.filtered-search-token button'));
+}
+
+function createBackspaceEvent() {
+ const e = new Event('keyup');
+ e.keyCode = 8;
+ e.which = e.keyCode;
+ e.altKey = false;
+ e.ctrlKey = true;
+ e.shiftKey = false;
+ e.metaKey = false;
+ return e;
+}
+
+describe('IDE shared/TokenedInput', () => {
+ const Component = Vue.extend(TokenedInput);
+ let vm;
+
+ beforeEach(() => {
+ vm = mountComponent(Component, {
+ tokens: TEST_TOKENS,
+ placeholder: TEST_PLACEHOLDER,
+ value: TEST_VALUE,
+ });
+
+ jest.spyOn(vm, '$emit').mockImplementation(() => {});
+ });
+
+ afterEach(() => {
+ vm.$destroy();
+ });
+
+ it('renders tokens', () => {
+ const renderedTokens = getTokenElements(vm).map(x => x.textContent.trim());
+
+ expect(renderedTokens).toEqual(TEST_TOKENS.map(x => x.label));
+ });
+
+ it('renders input', () => {
+ expect(vm.$refs.input).toBeTruthy();
+ expect(vm.$refs.input).toHaveValue(TEST_VALUE);
+ });
+
+ it('renders placeholder, when tokens are empty', done => {
+ vm.tokens = [];
+
+ vm.$nextTick()
+ .then(() => {
+ expect(vm.$refs.input).toHaveAttr('placeholder', TEST_PLACEHOLDER);
+ })
+ .then(done)
+ .catch(done.fail);
+ });
+
+ it('triggers "removeToken" on token click', () => {
+ getTokenElements(vm)[0].click();
+
+ expect(vm.$emit).toHaveBeenCalledWith('removeToken', TEST_TOKENS[0]);
+ });
+
+ it('when input triggers backspace event, it calls "onBackspace"', () => {
+ jest.spyOn(vm, 'onBackspace').mockImplementation(() => {});
+
+ vm.$refs.input.dispatchEvent(createBackspaceEvent());
+ vm.$refs.input.dispatchEvent(createBackspaceEvent());
+
+ expect(vm.onBackspace).toHaveBeenCalledTimes(2);
+ });
+
+ it('triggers "removeToken" on backspaces when value is empty', () => {
+ vm.value = '';
+
+ vm.onBackspace();
+
+ expect(vm.$emit).not.toHaveBeenCalled();
+ expect(vm.backspaceCount).toEqual(1);
+
+ vm.onBackspace();
+
+ expect(vm.$emit).toHaveBeenCalledWith('removeToken', TEST_TOKENS[TEST_TOKENS.length - 1]);
+ expect(vm.backspaceCount).toEqual(0);
+ });
+
+ it('does not trigger "removeToken" on backspaces when value is not empty', () => {
+ vm.onBackspace();
+ vm.onBackspace();
+
+ expect(vm.backspaceCount).toEqual(0);
+ expect(vm.$emit).not.toHaveBeenCalled();
+ });
+
+ it('does not trigger "removeToken" on backspaces when tokens are empty', () => {
+ vm.tokens = [];
+
+ vm.onBackspace();
+ vm.onBackspace();
+
+ expect(vm.backspaceCount).toEqual(0);
+ expect(vm.$emit).not.toHaveBeenCalled();
+ });
+
+ it('triggers "focus" on input focus', () => {
+ vm.$refs.input.dispatchEvent(new Event('focus'));
+
+ expect(vm.$emit).toHaveBeenCalledWith('focus');
+ });
+
+ it('triggers "blur" on input blur', () => {
+ vm.$refs.input.dispatchEvent(new Event('blur'));
+
+ expect(vm.$emit).toHaveBeenCalledWith('blur');
+ });
+
+ it('triggers "input" with value on input change', () => {
+ vm.$refs.input.value = 'something-else';
+ vm.$refs.input.dispatchEvent(new Event('input'));
+
+ expect(vm.$emit).toHaveBeenCalledWith('input', 'something-else');
+ });
+});
diff --git a/spec/frontend/ide/lib/common/model_manager_spec.js b/spec/frontend/ide/lib/common/model_manager_spec.js
new file mode 100644
index 00000000000..08e4ab0f113
--- /dev/null
+++ b/spec/frontend/ide/lib/common/model_manager_spec.js
@@ -0,0 +1,126 @@
+import eventHub from '~/ide/eventhub';
+import ModelManager from '~/ide/lib/common/model_manager';
+import { file } from '../../helpers';
+
+describe('Multi-file editor library model manager', () => {
+ let instance;
+
+ beforeEach(() => {
+ instance = new ModelManager();
+ });
+
+ afterEach(() => {
+ instance.dispose();
+ });
+
+ describe('addModel', () => {
+ it('caches model', () => {
+ instance.addModel(file());
+
+ expect(instance.models.size).toBe(1);
+ });
+
+ it('caches model by file path', () => {
+ const f = file('path-name');
+ instance.addModel(f);
+
+ expect(instance.models.keys().next().value).toBe(f.key);
+ });
+
+ it('adds model into disposable', () => {
+ jest.spyOn(instance.disposable, 'add');
+
+ instance.addModel(file());
+
+ expect(instance.disposable.add).toHaveBeenCalled();
+ });
+
+ it('returns cached model', () => {
+ jest.spyOn(instance.models, 'get');
+
+ instance.addModel(file());
+ instance.addModel(file());
+
+ expect(instance.models.get).toHaveBeenCalled();
+ });
+
+ it('adds eventHub listener', () => {
+ const f = file();
+ jest.spyOn(eventHub, '$on');
+
+ instance.addModel(f);
+
+ expect(eventHub.$on).toHaveBeenCalledWith(
+ `editor.update.model.dispose.${f.key}`,
+ expect.anything(),
+ );
+ });
+ });
+
+ describe('hasCachedModel', () => {
+ it('returns false when no models exist', () => {
+ expect(instance.hasCachedModel('path')).toBeFalsy();
+ });
+
+ it('returns true when model exists', () => {
+ const f = file('path-name');
+
+ instance.addModel(f);
+
+ expect(instance.hasCachedModel(f.key)).toBeTruthy();
+ });
+ });
+
+ describe('getModel', () => {
+ it('returns cached model', () => {
+ instance.addModel(file('path-name'));
+
+ expect(instance.getModel('path-name')).not.toBeNull();
+ });
+ });
+
+ describe('removeCachedModel', () => {
+ let f;
+
+ beforeEach(() => {
+ f = file();
+
+ instance.addModel(f);
+ });
+
+ it('clears cached model', () => {
+ instance.removeCachedModel(f);
+
+ expect(instance.models.size).toBe(0);
+ });
+
+ it('removes eventHub listener', () => {
+ jest.spyOn(eventHub, '$off');
+
+ instance.removeCachedModel(f);
+
+ expect(eventHub.$off).toHaveBeenCalledWith(
+ `editor.update.model.dispose.${f.key}`,
+ expect.anything(),
+ );
+ });
+ });
+
+ describe('dispose', () => {
+ it('clears cached models', () => {
+ instance.addModel(file());
+
+ instance.dispose();
+
+ expect(instance.models.size).toBe(0);
+ });
+
+ it('calls disposable dispose', () => {
+ jest.spyOn(instance.disposable, 'dispose');
+
+ instance.dispose();
+
+ expect(instance.disposable.dispose).toHaveBeenCalled();
+ });
+ });
+});
diff --git a/spec/frontend/ide/lib/common/model_spec.js b/spec/frontend/ide/lib/common/model_spec.js
new file mode 100644
index 00000000000..2ef2f0da6da
--- /dev/null
+++ b/spec/frontend/ide/lib/common/model_spec.js
@@ -0,0 +1,137 @@
+import eventHub from '~/ide/eventhub';
+import Model from '~/ide/lib/common/model';
+import { file } from '../../helpers';
+
+describe('Multi-file editor library model', () => {
+ let model;
+
+ beforeEach(() => {
+ jest.spyOn(eventHub, '$on');
+
+ const f = file('path');
+ f.mrChange = { diff: 'ABC' };
+ f.baseRaw = 'test';
+ model = new Model(f);
+ });
+
+ afterEach(() => {
+ model.dispose();
+ });
+
+ it('creates original model & base model & new model', () => {
+ expect(model.originalModel).not.toBeNull();
+ expect(model.model).not.toBeNull();
+ expect(model.baseModel).not.toBeNull();
+
+ expect(model.originalModel.uri.path).toBe('original/path--path');
+ expect(model.model.uri.path).toBe('path--path');
+ expect(model.baseModel.uri.path).toBe('target/path--path');
+ });
+
+ it('creates model with head file to compare against', () => {
+ const f = file('path');
+ model.dispose();
+
+ model = new Model(f, {
+ ...f,
+ content: '123 testing',
+ });
+
+ expect(model.head).not.toBeNull();
+ expect(model.getOriginalModel().getValue()).toBe('123 testing');
+ });
+
+ it('adds eventHub listener', () => {
+ expect(eventHub.$on).toHaveBeenCalledWith(
+ `editor.update.model.dispose.${model.file.key}`,
+ expect.anything(),
+ );
+ });
+
+ describe('path', () => {
+ it('returns file path', () => {
+ expect(model.path).toBe(model.file.key);
+ });
+ });
+
+ describe('getModel', () => {
+ it('returns model', () => {
+ expect(model.getModel()).toBe(model.model);
+ });
+ });
+
+ describe('getOriginalModel', () => {
+ it('returns original model', () => {
+ expect(model.getOriginalModel()).toBe(model.originalModel);
+ });
+ });
+
+ describe('getBaseModel', () => {
+ it('returns base model', () => {
+ expect(model.getBaseModel()).toBe(model.baseModel);
+ });
+ });
+
+ describe('setValue', () => {
+ it('updates models value', () => {
+ model.setValue('testing 123');
+
+ expect(model.getModel().getValue()).toBe('testing 123');
+ });
+ });
+
+ describe('onChange', () => {
+ it('calls callback on change', done => {
+ const spy = jest.fn();
+ model.onChange(spy);
+
+ model.getModel().setValue('123');
+
+ setImmediate(() => {
+ expect(spy).toHaveBeenCalledWith(model, expect.anything());
+ done();
+ });
+ });
+ });
+
+ describe('dispose', () => {
+ it('calls disposable dispose', () => {
+ jest.spyOn(model.disposable, 'dispose');
+
+ model.dispose();
+
+ expect(model.disposable.dispose).toHaveBeenCalled();
+ });
+
+ it('clears events', () => {
+ model.onChange(() => {});
+
+ expect(model.events.size).toBe(1);
+
+ model.dispose();
+
+ expect(model.events.size).toBe(0);
+ });
+
+ it('removes eventHub listener', () => {
+ jest.spyOn(eventHub, '$off');
+
+ model.dispose();
+
+ expect(eventHub.$off).toHaveBeenCalledWith(
+ `editor.update.model.dispose.${model.file.key}`,
+ expect.anything(),
+ );
+ });
+
+ it('calls onDispose callback', () => {
+ const disposeSpy = jest.fn();
+
+ model.onDispose(disposeSpy);
+
+ model.dispose();
+
+ expect(disposeSpy).toHaveBeenCalled();
+ });
+ });
+});
diff --git a/spec/frontend/ide/lib/decorations/controller_spec.js b/spec/frontend/ide/lib/decorations/controller_spec.js
new file mode 100644
index 00000000000..4556fc9d646
--- /dev/null
+++ b/spec/frontend/ide/lib/decorations/controller_spec.js
@@ -0,0 +1,143 @@
+import Editor from '~/ide/lib/editor';
+import DecorationsController from '~/ide/lib/decorations/controller';
+import Model from '~/ide/lib/common/model';
+import { file } from '../../helpers';
+
+describe('Multi-file editor library decorations controller', () => {
+ let editorInstance;
+ let controller;
+ let model;
+
+ beforeEach(() => {
+ editorInstance = Editor.create();
+ editorInstance.createInstance(document.createElement('div'));
+
+ controller = new DecorationsController(editorInstance);
+ model = new Model(file('path'));
+ });
+
+ afterEach(() => {
+ model.dispose();
+ editorInstance.dispose();
+ controller.dispose();
+ });
+
+ describe('getAllDecorationsForModel', () => {
+ it('returns empty array when no decorations exist for model', () => {
+ const decorations = controller.getAllDecorationsForModel(model);
+
+ expect(decorations).toEqual([]);
+ });
+
+ it('returns decorations by model URL', () => {
+ controller.addDecorations(model, 'key', [{ decoration: 'decorationValue' }]);
+
+ const decorations = controller.getAllDecorationsForModel(model);
+
+ expect(decorations[0]).toEqual({ decoration: 'decorationValue' });
+ });
+ });
+
+ describe('addDecorations', () => {
+ it('caches decorations in a new map', () => {
+ controller.addDecorations(model, 'key', [{ decoration: 'decorationValue' }]);
+
+ expect(controller.decorations.size).toBe(1);
+ });
+
+ it('does not create new cache model', () => {
+ controller.addDecorations(model, 'key', [{ decoration: 'decorationValue' }]);
+ controller.addDecorations(model, 'key', [{ decoration: 'decorationValue2' }]);
+
+ expect(controller.decorations.size).toBe(1);
+ });
+
+ it('caches decorations by model URL', () => {
+ controller.addDecorations(model, 'key', [{ decoration: 'decorationValue' }]);
+
+ expect(controller.decorations.size).toBe(1);
+ expect(controller.decorations.keys().next().value).toBe('gitlab:path--path');
+ });
+
+ it('calls decorate method', () => {
+ jest.spyOn(controller, 'decorate').mockImplementation(() => {});
+
+ controller.addDecorations(model, 'key', [{ decoration: 'decorationValue' }]);
+
+ expect(controller.decorate).toHaveBeenCalled();
+ });
+ });
+
+ describe('decorate', () => {
+ it('sets decorations on editor instance', () => {
+ jest.spyOn(controller.editor.instance, 'deltaDecorations').mockImplementation(() => {});
+
+ controller.decorate(model);
+
+ expect(controller.editor.instance.deltaDecorations).toHaveBeenCalledWith([], []);
+ });
+
+ it('caches decorations', () => {
+ jest.spyOn(controller.editor.instance, 'deltaDecorations').mockReturnValue([]);
+
+ controller.decorate(model);
+
+ expect(controller.editorDecorations.size).toBe(1);
+ });
+
+ it('caches decorations by model URL', () => {
+ jest.spyOn(controller.editor.instance, 'deltaDecorations').mockReturnValue([]);
+
+ controller.decorate(model);
+
+ expect(controller.editorDecorations.keys().next().value).toBe('gitlab:path--path');
+ });
+ });
+
+ describe('dispose', () => {
+ it('clears cached decorations', () => {
+ controller.addDecorations(model, 'key', [{ decoration: 'decorationValue' }]);
+
+ controller.dispose();
+
+ expect(controller.decorations.size).toBe(0);
+ });
+
+ it('clears cached editorDecorations', () => {
+ controller.addDecorations(model, 'key', [{ decoration: 'decorationValue' }]);
+
+ controller.dispose();
+
+ expect(controller.editorDecorations.size).toBe(0);
+ });
+ });
+
+ describe('hasDecorations', () => {
+ it('returns true when decorations are cached', () => {
+ controller.addDecorations(model, 'key', [{ decoration: 'decorationValue' }]);
+
+ expect(controller.hasDecorations(model)).toBe(true);
+ });
+
+ it('returns false when no model decorations exist', () => {
+ expect(controller.hasDecorations(model)).toBe(false);
+ });
+ });
+
+ describe('removeDecorations', () => {
+ beforeEach(() => {
+ controller.addDecorations(model, 'key', [{ decoration: 'decorationValue' }]);
+ controller.decorate(model);
+ });
+
+ it('removes cached decorations', () => {
+ expect(controller.decorations.size).not.toBe(0);
+ expect(controller.editorDecorations.size).not.toBe(0);
+
+ controller.removeDecorations(model);
+
+ expect(controller.decorations.size).toBe(0);
+ expect(controller.editorDecorations.size).toBe(0);
+ });
+ });
+});
diff --git a/spec/frontend/ide/lib/diff/controller_spec.js b/spec/frontend/ide/lib/diff/controller_spec.js
new file mode 100644
index 00000000000..0b33a4c6ad6
--- /dev/null
+++ b/spec/frontend/ide/lib/diff/controller_spec.js
@@ -0,0 +1,215 @@
+import { Range } from 'monaco-editor';
+import Editor from '~/ide/lib/editor';
+import ModelManager from '~/ide/lib/common/model_manager';
+import DecorationsController from '~/ide/lib/decorations/controller';
+import DirtyDiffController, { getDiffChangeType, getDecorator } from '~/ide/lib/diff/controller';
+import { computeDiff } from '~/ide/lib/diff/diff';
+import { file } from '../../helpers';
+
+describe('Multi-file editor library dirty diff controller', () => {
+ let editorInstance;
+ let controller;
+ let modelManager;
+ let decorationsController;
+ let model;
+
+ beforeEach(() => {
+ editorInstance = Editor.create();
+ editorInstance.createInstance(document.createElement('div'));
+
+ modelManager = new ModelManager();
+ decorationsController = new DecorationsController(editorInstance);
+
+ model = modelManager.addModel(file('path'));
+
+ controller = new DirtyDiffController(modelManager, decorationsController);
+ });
+
+ afterEach(() => {
+ controller.dispose();
+ model.dispose();
+ decorationsController.dispose();
+ editorInstance.dispose();
+ });
+
+ describe('getDiffChangeType', () => {
+ ['added', 'removed', 'modified'].forEach(type => {
+ it(`returns ${type}`, () => {
+ const change = {
+ [type]: true,
+ };
+
+ expect(getDiffChangeType(change)).toBe(type);
+ });
+ });
+ });
+
+ describe('getDecorator', () => {
+ ['added', 'removed', 'modified'].forEach(type => {
+ it(`returns with linesDecorationsClassName for ${type}`, () => {
+ const change = {
+ [type]: true,
+ };
+
+ expect(getDecorator(change).options.linesDecorationsClassName).toBe(
+ `dirty-diff dirty-diff-${type}`,
+ );
+ });
+
+ it('returns with line numbers', () => {
+ const change = {
+ lineNumber: 1,
+ endLineNumber: 2,
+ [type]: true,
+ };
+
+ const { range } = getDecorator(change);
+
+ expect(range.startLineNumber).toBe(1);
+ expect(range.endLineNumber).toBe(2);
+ expect(range.startColumn).toBe(1);
+ expect(range.endColumn).toBe(1);
+ });
+ });
+ });
+
+ describe('attachModel', () => {
+ it('adds change event callback', () => {
+ jest.spyOn(model, 'onChange').mockImplementation(() => {});
+
+ controller.attachModel(model);
+
+ expect(model.onChange).toHaveBeenCalled();
+ });
+
+ it('adds dispose event callback', () => {
+ jest.spyOn(model, 'onDispose').mockImplementation(() => {});
+
+ controller.attachModel(model);
+
+ expect(model.onDispose).toHaveBeenCalled();
+ });
+
+ it('calls throttledComputeDiff on change', () => {
+ jest.spyOn(controller, 'throttledComputeDiff').mockImplementation(() => {});
+
+ controller.attachModel(model);
+
+ model.getModel().setValue('123');
+
+ expect(controller.throttledComputeDiff).toHaveBeenCalled();
+ });
+
+ it('caches model', () => {
+ controller.attachModel(model);
+
+ expect(controller.models.has(model.url)).toBe(true);
+ });
+ });
+
+ describe('computeDiff', () => {
+ it('posts to worker', () => {
+ jest.spyOn(controller.dirtyDiffWorker, 'postMessage').mockImplementation(() => {});
+
+ controller.computeDiff(model);
+
+ expect(controller.dirtyDiffWorker.postMessage).toHaveBeenCalledWith({
+ path: model.path,
+ originalContent: '',
+ newContent: '',
+ });
+ });
+ });
+
+ describe('reDecorate', () => {
+ it('calls computeDiff when no decorations are cached', () => {
+ jest.spyOn(controller, 'computeDiff').mockImplementation(() => {});
+
+ controller.reDecorate(model);
+
+ expect(controller.computeDiff).toHaveBeenCalledWith(model);
+ });
+
+ it('calls decorate when decorations are cached', () => {
+ jest.spyOn(controller.decorationsController, 'decorate').mockImplementation(() => {});
+
+ controller.decorationsController.decorations.set(model.url, 'test');
+
+ controller.reDecorate(model);
+
+ expect(controller.decorationsController.decorate).toHaveBeenCalledWith(model);
+ });
+ });
+
+ describe('decorate', () => {
+ it('adds decorations into decorations controller', () => {
+ jest.spyOn(controller.decorationsController, 'addDecorations').mockImplementation(() => {});
+
+ controller.decorate({ data: { changes: [], path: model.path } });
+
+ expect(controller.decorationsController.addDecorations).toHaveBeenCalledWith(
+ model,
+ 'dirtyDiff',
+ expect.anything(),
+ );
+ });
+
+ it('adds decorations into editor', () => {
+ const spy = jest.spyOn(controller.decorationsController.editor.instance, 'deltaDecorations');
+
+ controller.decorate({
+ data: { changes: computeDiff('123', '1234'), path: model.path },
+ });
+
+ expect(spy).toHaveBeenCalledWith(
+ [],
+ [
+ {
+ range: new Range(1, 1, 1, 1),
+ options: {
+ isWholeLine: true,
+ linesDecorationsClassName: 'dirty-diff dirty-diff-modified',
+ },
+ },
+ ],
+ );
+ });
+ });
+
+ describe('dispose', () => {
+ it('calls disposable dispose', () => {
+ jest.spyOn(controller.disposable, 'dispose');
+
+ controller.dispose();
+
+ expect(controller.disposable.dispose).toHaveBeenCalled();
+ });
+
+ it('terminates worker', () => {
+ jest.spyOn(controller.dirtyDiffWorker, 'terminate');
+
+ controller.dispose();
+
+ expect(controller.dirtyDiffWorker.terminate).toHaveBeenCalled();
+ });
+
+ it('removes worker event listener', () => {
+ jest.spyOn(controller.dirtyDiffWorker, 'removeEventListener');
+
+ controller.dispose();
+
+ expect(controller.dirtyDiffWorker.removeEventListener).toHaveBeenCalledWith(
+ 'message',
+ expect.anything(),
+ );
+ });
+
+ it('clears cached models', () => {
+ controller.attachModel(model);
+
+ model.dispose();
+
+ expect(controller.models.size).toBe(0);
+ });
+ });
+});
diff --git a/spec/frontend/ide/lib/editor_spec.js b/spec/frontend/ide/lib/editor_spec.js
new file mode 100644
index 00000000000..36d4c3c26ee
--- /dev/null
+++ b/spec/frontend/ide/lib/editor_spec.js
@@ -0,0 +1,302 @@
+import { editor as monacoEditor, languages as monacoLanguages } from 'monaco-editor';
+import Editor from '~/ide/lib/editor';
+import { defaultEditorOptions } from '~/ide/lib/editor_options';
+import { file } from '../helpers';
+
+describe('Multi-file editor library', () => {
+ let instance;
+ let el;
+ let holder;
+
+ const setNodeOffsetWidth = val => {
+ Object.defineProperty(instance.instance.getDomNode(), 'offsetWidth', {
+ get() {
+ return val;
+ },
+ });
+ };
+
+ beforeEach(() => {
+ el = document.createElement('div');
+ holder = document.createElement('div');
+ el.appendChild(holder);
+
+ document.body.appendChild(el);
+
+ instance = Editor.create();
+ });
+
+ afterEach(() => {
+ instance.modelManager.dispose();
+ instance.dispose();
+ Editor.editorInstance = null;
+
+ el.remove();
+ });
+
+ it('creates instance of editor', () => {
+ expect(Editor.editorInstance).not.toBeNull();
+ });
+
+ it('creates instance returns cached instance', () => {
+ expect(Editor.create()).toEqual(instance);
+ });
+
+ describe('createInstance', () => {
+ it('creates editor instance', () => {
+ jest.spyOn(monacoEditor, 'create');
+
+ instance.createInstance(holder);
+
+ expect(monacoEditor.create).toHaveBeenCalled();
+ });
+
+ it('creates dirty diff controller', () => {
+ instance.createInstance(holder);
+
+ expect(instance.dirtyDiffController).not.toBeNull();
+ });
+
+ it('creates model manager', () => {
+ instance.createInstance(holder);
+
+ expect(instance.modelManager).not.toBeNull();
+ });
+ });
+
+ describe('createDiffInstance', () => {
+ it('creates editor instance', () => {
+ jest.spyOn(monacoEditor, 'createDiffEditor');
+
+ instance.createDiffInstance(holder);
+
+ expect(monacoEditor.createDiffEditor).toHaveBeenCalledWith(holder, {
+ ...defaultEditorOptions,
+ quickSuggestions: false,
+ occurrencesHighlight: false,
+ renderSideBySide: false,
+ readOnly: true,
+ renderLineHighlight: 'all',
+ hideCursorInOverviewRuler: false,
+ });
+ });
+ });
+
+ describe('createModel', () => {
+ it('calls model manager addModel', () => {
+ jest.spyOn(instance.modelManager, 'addModel').mockImplementation(() => {});
+
+ instance.createModel('FILE');
+
+ expect(instance.modelManager.addModel).toHaveBeenCalledWith('FILE', null);
+ });
+ });
+
+ describe('attachModel', () => {
+ let model;
+
+ beforeEach(() => {
+ instance.createInstance(document.createElement('div'));
+
+ model = instance.createModel(file());
+ });
+
+ it('sets the current model on the instance', () => {
+ instance.attachModel(model);
+
+ expect(instance.currentModel).toBe(model);
+ });
+
+ it('attaches the model to the current instance', () => {
+ jest.spyOn(instance.instance, 'setModel').mockImplementation(() => {});
+
+ instance.attachModel(model);
+
+ expect(instance.instance.setModel).toHaveBeenCalledWith(model.getModel());
+ });
+
+ it('sets original & modified when diff editor', () => {
+ jest.spyOn(instance.instance, 'getEditorType').mockReturnValue('vs.editor.IDiffEditor');
+ jest.spyOn(instance.instance, 'setModel').mockImplementation(() => {});
+
+ instance.attachModel(model);
+
+ expect(instance.instance.setModel).toHaveBeenCalledWith({
+ original: model.getOriginalModel(),
+ modified: model.getModel(),
+ });
+ });
+
+ it('attaches the model to the dirty diff controller', () => {
+ jest.spyOn(instance.dirtyDiffController, 'attachModel').mockImplementation(() => {});
+
+ instance.attachModel(model);
+
+ expect(instance.dirtyDiffController.attachModel).toHaveBeenCalledWith(model);
+ });
+
+ it('re-decorates with the dirty diff controller', () => {
+ jest.spyOn(instance.dirtyDiffController, 'reDecorate').mockImplementation(() => {});
+
+ instance.attachModel(model);
+
+ expect(instance.dirtyDiffController.reDecorate).toHaveBeenCalledWith(model);
+ });
+ });
+
+ describe('attachMergeRequestModel', () => {
+ let model;
+
+ beforeEach(() => {
+ instance.createDiffInstance(document.createElement('div'));
+
+ const f = file();
+ f.mrChanges = { diff: 'ABC' };
+ f.baseRaw = 'testing';
+
+ model = instance.createModel(f);
+ });
+
+ it('sets original & modified', () => {
+ jest.spyOn(instance.instance, 'setModel').mockImplementation(() => {});
+
+ instance.attachMergeRequestModel(model);
+
+ expect(instance.instance.setModel).toHaveBeenCalledWith({
+ original: model.getBaseModel(),
+ modified: model.getModel(),
+ });
+ });
+ });
+
+ describe('clearEditor', () => {
+ it('resets the editor model', () => {
+ instance.createInstance(document.createElement('div'));
+
+ jest.spyOn(instance.instance, 'setModel').mockImplementation(() => {});
+
+ instance.clearEditor();
+
+ expect(instance.instance.setModel).toHaveBeenCalledWith(null);
+ });
+ });
+
+ describe('languages', () => {
+ it('registers custom languages defined with Monaco', () => {
+ expect(monacoLanguages.getLanguages()).toEqual(
+ expect.arrayContaining([
+ expect.objectContaining({
+ id: 'vue',
+ }),
+ ]),
+ );
+ });
+ });
+
+ describe('dispose', () => {
+ it('calls disposble dispose method', () => {
+ jest.spyOn(instance.disposable, 'dispose');
+
+ instance.dispose();
+
+ expect(instance.disposable.dispose).toHaveBeenCalled();
+ });
+
+ it('resets instance', () => {
+ instance.createInstance(document.createElement('div'));
+
+ expect(instance.instance).not.toBeNull();
+
+ instance.dispose();
+
+ expect(instance.instance).toBeNull();
+ });
+
+ it('does not dispose modelManager', () => {
+ jest.spyOn(instance.modelManager, 'dispose').mockImplementation(() => {});
+
+ instance.dispose();
+
+ expect(instance.modelManager.dispose).not.toHaveBeenCalled();
+ });
+
+ it('does not dispose decorationsController', () => {
+ jest.spyOn(instance.decorationsController, 'dispose').mockImplementation(() => {});
+
+ instance.dispose();
+
+ expect(instance.decorationsController.dispose).not.toHaveBeenCalled();
+ });
+ });
+
+ describe('updateDiffView', () => {
+ describe('edit mode', () => {
+ it('does not update options', () => {
+ instance.createInstance(holder);
+
+ jest.spyOn(instance.instance, 'updateOptions').mockImplementation(() => {});
+
+ instance.updateDiffView();
+
+ expect(instance.instance.updateOptions).not.toHaveBeenCalled();
+ });
+ });
+
+ describe('diff mode', () => {
+ beforeEach(() => {
+ instance.createDiffInstance(holder);
+
+ jest.spyOn(instance.instance, 'updateOptions');
+ });
+
+ it('sets renderSideBySide to false if el is less than 700 pixels', () => {
+ setNodeOffsetWidth(600);
+
+ expect(instance.instance.updateOptions).not.toHaveBeenCalledWith({
+ renderSideBySide: false,
+ });
+ });
+
+ it('sets renderSideBySide to false if el is more than 700 pixels', () => {
+ setNodeOffsetWidth(800);
+
+ expect(instance.instance.updateOptions).not.toHaveBeenCalledWith({
+ renderSideBySide: true,
+ });
+ });
+ });
+ });
+
+ describe('isDiffEditorType', () => {
+ it('returns true when diff editor', () => {
+ instance.createDiffInstance(holder);
+
+ expect(instance.isDiffEditorType).toBe(true);
+ });
+
+ it('returns false when not diff editor', () => {
+ instance.createInstance(holder);
+
+ expect(instance.isDiffEditorType).toBe(false);
+ });
+ });
+
+ it('sets quickSuggestions to false when language is markdown', () => {
+ instance.createInstance(holder);
+
+ jest.spyOn(instance.instance, 'updateOptions');
+
+ const model = instance.createModel({
+ ...file(),
+ key: 'index.md',
+ path: 'index.md',
+ });
+
+ instance.attachModel(model);
+
+ expect(instance.instance.updateOptions).toHaveBeenCalledWith({
+ readOnly: false,
+ quickSuggestions: false,
+ });
+ });
+});
diff --git a/spec/frontend/ide/lib/languages/vue_spec.js b/spec/frontend/ide/lib/languages/vue_spec.js
new file mode 100644
index 00000000000..3d8784c1436
--- /dev/null
+++ b/spec/frontend/ide/lib/languages/vue_spec.js
@@ -0,0 +1,92 @@
+import { editor } from 'monaco-editor';
+import { registerLanguages } from '~/ide/utils';
+import vue from '~/ide/lib/languages/vue';
+
+// This file only tests syntax specific to vue. This does not test existing syntaxes
+// of html, javascript, css and handlebars, which vue files extend.
+describe('tokenization for .vue files', () => {
+ beforeEach(() => {
+ registerLanguages(vue);
+ });
+
+ test.each([
+ [
+ '<div v-if="something">content</div>',
+ [
+ [
+ { language: 'vue', offset: 0, type: 'delimiter.html' },
+ { language: 'vue', offset: 1, type: 'tag.html' },
+ { language: 'vue', offset: 4, type: '' },
+ { language: 'vue', offset: 5, type: 'variable' },
+ { language: 'vue', offset: 21, type: 'delimiter.html' },
+ { language: 'vue', offset: 22, type: '' },
+ { language: 'vue', offset: 29, type: 'delimiter.html' },
+ { language: 'vue', offset: 31, type: 'tag.html' },
+ { language: 'vue', offset: 34, type: 'delimiter.html' },
+ ],
+ ],
+ ],
+ [
+ '<input :placeholder="placeholder">',
+ [
+ [
+ { language: 'vue', offset: 0, type: 'delimiter.html' },
+ { language: 'vue', offset: 1, type: 'tag.html' },
+ { language: 'vue', offset: 6, type: '' },
+ { language: 'vue', offset: 7, type: 'variable' },
+ { language: 'vue', offset: 33, type: 'delimiter.html' },
+ ],
+ ],
+ ],
+ [
+ '<gl-modal @ok="submitForm()"></gl-modal>',
+ [
+ [
+ { language: 'vue', offset: 0, type: 'delimiter.html' },
+ { language: 'vue', offset: 1, type: 'tag.html' },
+ { language: 'vue', offset: 3, type: 'attribute.name' },
+ { language: 'vue', offset: 9, type: '' },
+ { language: 'vue', offset: 10, type: 'variable' },
+ { language: 'vue', offset: 28, type: 'delimiter.html' },
+ { language: 'vue', offset: 31, type: 'tag.html' },
+ { language: 'vue', offset: 33, type: 'attribute.name' },
+ { language: 'vue', offset: 39, type: 'delimiter.html' },
+ ],
+ ],
+ ],
+ [
+ '<a v-on:click.stop="doSomething">...</a>',
+ [
+ [
+ { language: 'vue', offset: 0, type: 'delimiter.html' },
+ { language: 'vue', offset: 1, type: 'tag.html' },
+ { language: 'vue', offset: 2, type: '' },
+ { language: 'vue', offset: 3, type: 'variable' },
+ { language: 'vue', offset: 32, type: 'delimiter.html' },
+ { language: 'vue', offset: 33, type: '' },
+ { language: 'vue', offset: 36, type: 'delimiter.html' },
+ { language: 'vue', offset: 38, type: 'tag.html' },
+ { language: 'vue', offset: 39, type: 'delimiter.html' },
+ ],
+ ],
+ ],
+ [
+ '<a @[event]="doSomething">...</a>',
+ [
+ [
+ { language: 'vue', offset: 0, type: 'delimiter.html' },
+ { language: 'vue', offset: 1, type: 'tag.html' },
+ { language: 'vue', offset: 2, type: '' },
+ { language: 'vue', offset: 3, type: 'variable' },
+ { language: 'vue', offset: 25, type: 'delimiter.html' },
+ { language: 'vue', offset: 26, type: '' },
+ { language: 'vue', offset: 29, type: 'delimiter.html' },
+ { language: 'vue', offset: 31, type: 'tag.html' },
+ { language: 'vue', offset: 32, type: 'delimiter.html' },
+ ],
+ ],
+ ],
+ ])('%s', (string, tokens) => {
+ expect(editor.tokenize(string, 'vue')).toEqual(tokens);
+ });
+});
diff --git a/spec/frontend/ide/services/index_spec.js b/spec/frontend/ide/services/index_spec.js
index 658ad37d7f2..3cb6e064aa2 100644
--- a/spec/frontend/ide/services/index_spec.js
+++ b/spec/frontend/ide/services/index_spec.js
@@ -221,4 +221,67 @@ describe('IDE services', () => {
});
});
});
+
+ describe('getFiles', () => {
+ let mock;
+ let relativeUrlRoot;
+ const TEST_RELATIVE_URL_ROOT = 'blah-blah';
+
+ beforeEach(() => {
+ jest.spyOn(axios, 'get');
+ relativeUrlRoot = gon.relative_url_root;
+ gon.relative_url_root = TEST_RELATIVE_URL_ROOT;
+
+ mock = new MockAdapter(axios);
+
+ mock
+ .onGet(`${TEST_RELATIVE_URL_ROOT}/${TEST_PROJECT_ID}/-/files/${TEST_COMMIT_SHA}`)
+ .reply(200, [TEST_FILE_PATH]);
+ });
+
+ afterEach(() => {
+ mock.restore();
+ gon.relative_url_root = relativeUrlRoot;
+ });
+
+ it('initates the api call based on the passed path and commit hash', () => {
+ return services.getFiles(TEST_PROJECT_ID, TEST_COMMIT_SHA).then(({ data }) => {
+ expect(axios.get).toHaveBeenCalledWith(
+ `${gon.relative_url_root}/${TEST_PROJECT_ID}/-/files/${TEST_COMMIT_SHA}`,
+ expect.any(Object),
+ );
+ expect(data).toEqual([TEST_FILE_PATH]);
+ });
+ });
+ });
+
+ describe('pingUsage', () => {
+ let mock;
+ let relativeUrlRoot;
+ const TEST_RELATIVE_URL_ROOT = 'blah-blah';
+
+ beforeEach(() => {
+ jest.spyOn(axios, 'post');
+ relativeUrlRoot = gon.relative_url_root;
+ gon.relative_url_root = TEST_RELATIVE_URL_ROOT;
+
+ mock = new MockAdapter(axios);
+ });
+
+ afterEach(() => {
+ mock.restore();
+ gon.relative_url_root = relativeUrlRoot;
+ });
+
+ it('posts to usage endpoint', () => {
+ const TEST_PROJECT_PATH = 'foo/bar';
+ const axiosURL = `${TEST_RELATIVE_URL_ROOT}/${TEST_PROJECT_PATH}/usage_ping/web_ide_pipelines_count`;
+
+ mock.onPost(axiosURL).reply(200);
+
+ return services.pingUsage(TEST_PROJECT_PATH).then(() => {
+ expect(axios.post).toHaveBeenCalledWith(axiosURL);
+ });
+ });
+ });
});
diff --git a/spec/frontend/ide/stores/mutations_spec.js b/spec/frontend/ide/stores/mutations_spec.js
index 5d0fe35a10e..2eca9acb8d8 100644
--- a/spec/frontend/ide/stores/mutations_spec.js
+++ b/spec/frontend/ide/stores/mutations_spec.js
@@ -55,30 +55,6 @@ describe('Multi-file store mutations', () => {
});
});
- describe('SET_LEFT_PANEL_COLLAPSED', () => {
- it('sets left panel collapsed', () => {
- mutations.SET_LEFT_PANEL_COLLAPSED(localState, true);
-
- expect(localState.leftPanelCollapsed).toBeTruthy();
-
- mutations.SET_LEFT_PANEL_COLLAPSED(localState, false);
-
- expect(localState.leftPanelCollapsed).toBeFalsy();
- });
- });
-
- describe('SET_RIGHT_PANEL_COLLAPSED', () => {
- it('sets right panel collapsed', () => {
- mutations.SET_RIGHT_PANEL_COLLAPSED(localState, true);
-
- expect(localState.rightPanelCollapsed).toBeTruthy();
-
- mutations.SET_RIGHT_PANEL_COLLAPSED(localState, false);
-
- expect(localState.rightPanelCollapsed).toBeFalsy();
- });
- });
-
describe('CLEAR_STAGED_CHANGES', () => {
it('clears stagedFiles array', () => {
localState.stagedFiles.push('a');
@@ -339,23 +315,6 @@ describe('Multi-file store mutations', () => {
});
});
- describe('OPEN_NEW_ENTRY_MODAL', () => {
- it('sets entryModal', () => {
- localState.entries.testPath = file();
-
- mutations.OPEN_NEW_ENTRY_MODAL(localState, {
- type: 'test',
- path: 'testPath',
- });
-
- expect(localState.entryModal).toEqual({
- type: 'test',
- path: 'testPath',
- entry: localState.entries.testPath,
- });
- });
- });
-
describe('RENAME_ENTRY', () => {
beforeEach(() => {
localState.trees = {
diff --git a/spec/frontend/ide/stores/utils_spec.js b/spec/frontend/ide/stores/utils_spec.js
index 90f2644de62..b87f6c1f05a 100644
--- a/spec/frontend/ide/stores/utils_spec.js
+++ b/spec/frontend/ide/stores/utils_spec.js
@@ -685,4 +685,75 @@ describe('Multi-file store utils', () => {
});
});
});
+
+ describe('extractMarkdownImagesFromEntries', () => {
+ let mdFile;
+ let entries;
+
+ beforeEach(() => {
+ const img = { content: '/base64/encoded/image+' };
+ mdFile = { path: 'path/to/some/directory/myfile.md' };
+ entries = {
+ // invalid (or lack of) extensions are also supported as long as there's
+ // a real image inside and can go into an <img> tag's `src` and the browser
+ // can render it
+ img,
+ 'img.js': img,
+ 'img.png': img,
+ 'img.with.many.dots.png': img,
+ 'path/to/img.gif': img,
+ 'path/to/some/img.jpg': img,
+ 'path/to/some/img 1/img.png': img,
+ 'path/to/some/directory/img.png': img,
+ 'path/to/some/directory/img 1.png': img,
+ };
+ });
+
+ it.each`
+ markdownBefore | ext | imgAlt | imgTitle
+ ${'* ![img](/img)'} | ${'jpeg'} | ${'img'} | ${undefined}
+ ${'* ![img](/img.js)'} | ${'js'} | ${'img'} | ${undefined}
+ ${'* ![img](img.png)'} | ${'png'} | ${'img'} | ${undefined}
+ ${'* ![img](./img.png)'} | ${'png'} | ${'img'} | ${undefined}
+ ${'* ![with spaces](../img 1/img.png)'} | ${'png'} | ${'with spaces'} | ${undefined}
+ ${'* ![img](../../img.gif " title ")'} | ${'gif'} | ${'img'} | ${' title '}
+ ${'* ![img](../img.jpg)'} | ${'jpg'} | ${'img'} | ${undefined}
+ ${'* ![img](/img.png "title")'} | ${'png'} | ${'img'} | ${'title'}
+ ${'* ![img](/img.with.many.dots.png)'} | ${'png'} | ${'img'} | ${undefined}
+ ${'* ![img](img 1.png)'} | ${'png'} | ${'img'} | ${undefined}
+ ${'* ![img](img.png "title here")'} | ${'png'} | ${'img'} | ${'title here'}
+ `(
+ 'correctly transforms markdown with uncommitted images: $markdownBefore',
+ ({ markdownBefore, ext, imgAlt, imgTitle }) => {
+ mdFile.content = markdownBefore;
+
+ expect(utils.extractMarkdownImagesFromEntries(mdFile, entries)).toEqual({
+ content: '* {{gl_md_img_1}}',
+ images: {
+ '{{gl_md_img_1}}': {
+ src: `data:image/${ext};base64,/base64/encoded/image+`,
+ alt: imgAlt,
+ title: imgTitle,
+ },
+ },
+ });
+ },
+ );
+
+ it.each`
+ markdown
+ ${'* ![img](i.png)'}
+ ${'* ![img](img.png invalid title)'}
+ ${'* ![img](img.png "incorrect" "markdown")'}
+ ${'* ![img](https://gitlab.com/logo.png)'}
+ ${'* ![img](https://gitlab.com/some/deep/nested/path/logo.png)'}
+ `("doesn't touch invalid or non-existant images in markdown: $markdown", ({ markdown }) => {
+ mdFile.content = markdown;
+
+ expect(utils.extractMarkdownImagesFromEntries(mdFile, entries)).toEqual({
+ content: markdown,
+ images: {},
+ });
+ });
+ });
});
diff --git a/spec/frontend/ide/utils_spec.js b/spec/frontend/ide/utils_spec.js
index 44eae7eacbe..ea975500e8d 100644
--- a/spec/frontend/ide/utils_spec.js
+++ b/spec/frontend/ide/utils_spec.js
@@ -1,6 +1,7 @@
import { commitItemIconMap } from '~/ide/constants';
-import { getCommitIconMap, isTextFile } from '~/ide/utils';
+import { getCommitIconMap, isTextFile, registerLanguages, trimPathComponents } from '~/ide/utils';
import { decorateData } from '~/ide/stores/utils';
+import { languages } from 'monaco-editor';
describe('WebIDE utils', () => {
describe('isTextFile', () => {
@@ -102,4 +103,93 @@ describe('WebIDE utils', () => {
expect(getCommitIconMap(entry)).toEqual(commitItemIconMap.modified);
});
});
+
+ describe('trimPathComponents', () => {
+ it.each`
+ input | output
+ ${'example path '} | ${'example path'}
+ ${'p/somefile '} | ${'p/somefile'}
+ ${'p /somefile '} | ${'p/somefile'}
+ ${'p/ somefile '} | ${'p/somefile'}
+ ${' p/somefile '} | ${'p/somefile'}
+ ${'p/somefile .md'} | ${'p/somefile .md'}
+ ${'path / to / some/file.doc '} | ${'path/to/some/file.doc'}
+ `('trims all path components in path: "$input"', ({ input, output }) => {
+ expect(trimPathComponents(input)).toEqual(output);
+ });
+ });
+
+ describe('registerLanguages', () => {
+ let langs;
+
+ beforeEach(() => {
+ langs = [
+ {
+ id: 'html',
+ extensions: ['.html'],
+ conf: { comments: { blockComment: ['<!--', '-->'] } },
+ language: { tokenizer: {} },
+ },
+ {
+ id: 'css',
+ extensions: ['.css'],
+ conf: { comments: { blockComment: ['/*', '*/'] } },
+ language: { tokenizer: {} },
+ },
+ {
+ id: 'js',
+ extensions: ['.js'],
+ conf: { comments: { blockComment: ['/*', '*/'] } },
+ language: { tokenizer: {} },
+ },
+ ];
+
+ jest.spyOn(languages, 'register').mockImplementation(() => {});
+ jest.spyOn(languages, 'setMonarchTokensProvider').mockImplementation(() => {});
+ jest.spyOn(languages, 'setLanguageConfiguration').mockImplementation(() => {});
+ });
+
+ it('registers all the passed languages with Monaco', () => {
+ registerLanguages(...langs);
+
+ expect(languages.register.mock.calls).toEqual([
+ [
+ {
+ conf: { comments: { blockComment: ['/*', '*/'] } },
+ extensions: ['.css'],
+ id: 'css',
+ language: { tokenizer: {} },
+ },
+ ],
+ [
+ {
+ conf: { comments: { blockComment: ['/*', '*/'] } },
+ extensions: ['.js'],
+ id: 'js',
+ language: { tokenizer: {} },
+ },
+ ],
+ [
+ {
+ conf: { comments: { blockComment: ['<!--', '-->'] } },
+ extensions: ['.html'],
+ id: 'html',
+ language: { tokenizer: {} },
+ },
+ ],
+ ]);
+
+ expect(languages.setMonarchTokensProvider.mock.calls).toEqual([
+ ['css', { tokenizer: {} }],
+ ['js', { tokenizer: {} }],
+ ['html', { tokenizer: {} }],
+ ]);
+
+ expect(languages.setLanguageConfiguration.mock.calls).toEqual([
+ ['css', { comments: { blockComment: ['/*', '*/'] } }],
+ ['js', { comments: { blockComment: ['/*', '*/'] } }],
+ ['html', { comments: { blockComment: ['<!--', '-->'] } }],
+ ]);
+ });
+ });
});
diff --git a/spec/frontend/image_diff/helpers/badge_helper_spec.js b/spec/frontend/image_diff/helpers/badge_helper_spec.js
new file mode 100644
index 00000000000..c970ccc535d
--- /dev/null
+++ b/spec/frontend/image_diff/helpers/badge_helper_spec.js
@@ -0,0 +1,130 @@
+import * as badgeHelper from '~/image_diff/helpers/badge_helper';
+import * as mockData from '../mock_data';
+
+describe('badge helper', () => {
+ const { coordinate, noteId, badgeText, badgeNumber } = mockData;
+ let containerEl;
+ let buttonEl;
+
+ beforeEach(() => {
+ containerEl = document.createElement('div');
+ });
+
+ describe('createImageBadge', () => {
+ beforeEach(() => {
+ buttonEl = badgeHelper.createImageBadge(noteId, coordinate);
+ });
+
+ it('should create button', () => {
+ expect(buttonEl.tagName).toEqual('BUTTON');
+ expect(buttonEl.getAttribute('type')).toEqual('button');
+ });
+
+ it('should set disabled attribute', () => {
+ expect(buttonEl.hasAttribute('disabled')).toEqual(true);
+ });
+
+ it('should set noteId', () => {
+ expect(buttonEl.dataset.noteId).toEqual(noteId);
+ });
+
+ it('should set coordinate', () => {
+ expect(buttonEl.style.left).toEqual(`${coordinate.x}px`);
+ expect(buttonEl.style.top).toEqual(`${coordinate.y}px`);
+ });
+
+ describe('classNames', () => {
+ it('should set .js-image-badge by default', () => {
+ expect(buttonEl.className).toEqual('js-image-badge');
+ });
+
+ it('should add additional class names if parameter is passed', () => {
+ const classNames = ['first-class', 'second-class'];
+ buttonEl = badgeHelper.createImageBadge(noteId, coordinate, classNames);
+
+ expect(buttonEl.className).toEqual(classNames.concat('js-image-badge').join(' '));
+ });
+ });
+ });
+
+ describe('addImageBadge', () => {
+ beforeEach(() => {
+ badgeHelper.addImageBadge(containerEl, {
+ coordinate,
+ badgeText,
+ noteId,
+ });
+ buttonEl = containerEl.querySelector('button');
+ });
+
+ it('should appends button to container', () => {
+ expect(buttonEl).toBeDefined();
+ });
+
+ it('should add badge classes', () => {
+ expect(buttonEl.className).toContain('badge badge-pill');
+ });
+
+ it('should set the badge text', () => {
+ expect(buttonEl.textContent).toEqual(badgeText);
+ });
+
+ it('should set the button coordinates', () => {
+ expect(buttonEl.style.left).toEqual(`${coordinate.x}px`);
+ expect(buttonEl.style.top).toEqual(`${coordinate.y}px`);
+ });
+
+ it('should set the button noteId', () => {
+ expect(buttonEl.dataset.noteId).toEqual(noteId);
+ });
+ });
+
+ describe('addImageCommentBadge', () => {
+ beforeEach(() => {
+ badgeHelper.addImageCommentBadge(containerEl, {
+ coordinate,
+ noteId,
+ });
+ buttonEl = containerEl.querySelector('button');
+ });
+
+ it('should append icon button to container', () => {
+ expect(buttonEl).toBeDefined();
+ });
+
+ it('should create icon comment button', () => {
+ const iconEl = buttonEl.querySelector('svg');
+
+ expect(iconEl).toBeDefined();
+ });
+ });
+
+ describe('addAvatarBadge', () => {
+ let avatarBadgeEl;
+
+ beforeEach(() => {
+ containerEl.innerHTML = `
+ <div id="${noteId}">
+ <div class="badge hidden">
+ </div>
+ </div>
+ `;
+
+ badgeHelper.addAvatarBadge(containerEl, {
+ detail: {
+ noteId,
+ badgeNumber,
+ },
+ });
+ avatarBadgeEl = containerEl.querySelector(`#${noteId} .badge`);
+ });
+
+ it('should update badge number', () => {
+ expect(avatarBadgeEl.textContent).toEqual(badgeNumber.toString());
+ });
+
+ it('should remove hidden class', () => {
+ expect(avatarBadgeEl.classList.contains('hidden')).toEqual(false);
+ });
+ });
+});
diff --git a/spec/frontend/image_diff/helpers/comment_indicator_helper_spec.js b/spec/frontend/image_diff/helpers/comment_indicator_helper_spec.js
new file mode 100644
index 00000000000..395bb7de362
--- /dev/null
+++ b/spec/frontend/image_diff/helpers/comment_indicator_helper_spec.js
@@ -0,0 +1,144 @@
+import * as commentIndicatorHelper from '~/image_diff/helpers/comment_indicator_helper';
+import * as mockData from '../mock_data';
+
+describe('commentIndicatorHelper', () => {
+ const { coordinate } = mockData;
+ let containerEl;
+
+ beforeEach(() => {
+ containerEl = document.createElement('div');
+ });
+
+ describe('addCommentIndicator', () => {
+ let buttonEl;
+
+ beforeEach(() => {
+ commentIndicatorHelper.addCommentIndicator(containerEl, coordinate);
+ buttonEl = containerEl.querySelector('button');
+ });
+
+ it('should append button to container', () => {
+ expect(buttonEl).toBeDefined();
+ });
+
+ describe('button', () => {
+ it('should set coordinate', () => {
+ expect(buttonEl.style.left).toEqual(`${coordinate.x}px`);
+ expect(buttonEl.style.top).toEqual(`${coordinate.y}px`);
+ });
+
+ it('should contain image-comment-dark svg', () => {
+ const svgEl = buttonEl.querySelector('svg');
+
+ expect(svgEl).toBeDefined();
+
+ const svgLink = svgEl.querySelector('use').getAttribute('xlink:href');
+
+ expect(svgLink.indexOf('image-comment-dark')).not.toBe(-1);
+ });
+ });
+ });
+
+ describe('removeCommentIndicator', () => {
+ it('should return removed false if there is no comment-indicator', () => {
+ const result = commentIndicatorHelper.removeCommentIndicator(containerEl);
+
+ expect(result.removed).toEqual(false);
+ });
+
+ describe('has comment indicator', () => {
+ let result;
+
+ beforeEach(() => {
+ containerEl.innerHTML = `
+ <div class="comment-indicator" style="left:${coordinate.x}px; top: ${coordinate.y}px;">
+ <img src="${gl.TEST_HOST}/image.png">
+ </div>
+ `;
+ result = commentIndicatorHelper.removeCommentIndicator(containerEl);
+ });
+
+ it('should remove comment indicator', () => {
+ expect(containerEl.querySelector('.comment-indicator')).toBeNull();
+ });
+
+ it('should return removed true', () => {
+ expect(result.removed).toEqual(true);
+ });
+
+ it('should return indicator meta', () => {
+ expect(result.x).toEqual(coordinate.x);
+ expect(result.y).toEqual(coordinate.y);
+ expect(result.image).toBeDefined();
+ expect(result.image.width).toBeDefined();
+ expect(result.image.height).toBeDefined();
+ });
+ });
+ });
+
+ describe('showCommentIndicator', () => {
+ describe('commentIndicator exists', () => {
+ beforeEach(() => {
+ containerEl.innerHTML = `
+ <button class="comment-indicator"></button>
+ `;
+ commentIndicatorHelper.showCommentIndicator(containerEl, coordinate);
+ });
+
+ it('should set commentIndicator coordinates', () => {
+ const commentIndicatorEl = containerEl.querySelector('.comment-indicator');
+
+ expect(commentIndicatorEl.style.left).toEqual(`${coordinate.x}px`);
+ expect(commentIndicatorEl.style.top).toEqual(`${coordinate.y}px`);
+ });
+ });
+
+ describe('commentIndicator does not exist', () => {
+ beforeEach(() => {
+ commentIndicatorHelper.showCommentIndicator(containerEl, coordinate);
+ });
+
+ it('should addCommentIndicator', () => {
+ const buttonEl = containerEl.querySelector('.comment-indicator');
+
+ expect(buttonEl).toBeDefined();
+ expect(buttonEl.style.left).toEqual(`${coordinate.x}px`);
+ expect(buttonEl.style.top).toEqual(`${coordinate.y}px`);
+ });
+ });
+ });
+
+ describe('commentIndicatorOnClick', () => {
+ let event;
+ let textAreaEl;
+
+ beforeEach(() => {
+ containerEl.innerHTML = `
+ <div class="diff-viewer">
+ <button></button>
+ <div class="note-container">
+ <textarea class="note-textarea"></textarea>
+ </div>
+ </div>
+ `;
+ textAreaEl = containerEl.querySelector('textarea');
+
+ event = {
+ stopPropagation: () => {},
+ currentTarget: containerEl.querySelector('button'),
+ };
+
+ jest.spyOn(event, 'stopPropagation').mockImplementation(() => {});
+ jest.spyOn(textAreaEl, 'focus').mockImplementation(() => {});
+ commentIndicatorHelper.commentIndicatorOnClick(event);
+ });
+
+ it('should stopPropagation', () => {
+ expect(event.stopPropagation).toHaveBeenCalled();
+ });
+
+ it('should focus textAreaEl', () => {
+ expect(textAreaEl.focus).toHaveBeenCalled();
+ });
+ });
+});
diff --git a/spec/frontend/image_diff/helpers/dom_helper_spec.js b/spec/frontend/image_diff/helpers/dom_helper_spec.js
new file mode 100644
index 00000000000..9357d626bbe
--- /dev/null
+++ b/spec/frontend/image_diff/helpers/dom_helper_spec.js
@@ -0,0 +1,120 @@
+import * as domHelper from '~/image_diff/helpers/dom_helper';
+import * as mockData from '../mock_data';
+
+describe('domHelper', () => {
+ const { imageMeta, badgeNumber } = mockData;
+
+ describe('setPositionDataAttribute', () => {
+ let containerEl;
+ let attributeAfterCall;
+ const position = {
+ myProperty: 'myProperty',
+ };
+
+ beforeEach(() => {
+ containerEl = document.createElement('div');
+ containerEl.dataset.position = JSON.stringify(position);
+ domHelper.setPositionDataAttribute(containerEl, imageMeta);
+ attributeAfterCall = JSON.parse(containerEl.dataset.position);
+ });
+
+ it('should set x, y, width, height', () => {
+ expect(attributeAfterCall.x).toEqual(imageMeta.x);
+ expect(attributeAfterCall.y).toEqual(imageMeta.y);
+ expect(attributeAfterCall.width).toEqual(imageMeta.width);
+ expect(attributeAfterCall.height).toEqual(imageMeta.height);
+ });
+
+ it('should not override other properties', () => {
+ expect(attributeAfterCall.myProperty).toEqual('myProperty');
+ });
+ });
+
+ describe('updateDiscussionAvatarBadgeNumber', () => {
+ let discussionEl;
+
+ beforeEach(() => {
+ discussionEl = document.createElement('div');
+ discussionEl.innerHTML = `
+ <a href="#" class="image-diff-avatar-link">
+ <div class="badge"></div>
+ </a>
+ `;
+ domHelper.updateDiscussionAvatarBadgeNumber(discussionEl, badgeNumber);
+ });
+
+ it('should update avatar badge number', () => {
+ expect(discussionEl.querySelector('.badge').textContent).toEqual(badgeNumber.toString());
+ });
+ });
+
+ describe('updateDiscussionBadgeNumber', () => {
+ let discussionEl;
+
+ beforeEach(() => {
+ discussionEl = document.createElement('div');
+ discussionEl.innerHTML = `
+ <div class="badge"></div>
+ `;
+ domHelper.updateDiscussionBadgeNumber(discussionEl, badgeNumber);
+ });
+
+ it('should update discussion badge number', () => {
+ expect(discussionEl.querySelector('.badge').textContent).toEqual(badgeNumber.toString());
+ });
+ });
+
+ describe('toggleCollapsed', () => {
+ let element;
+ let discussionNotesEl;
+
+ beforeEach(() => {
+ element = document.createElement('div');
+ element.innerHTML = `
+ <div class="discussion-notes">
+ <button></button>
+ <form class="discussion-form"></form>
+ </div>
+ `;
+ discussionNotesEl = element.querySelector('.discussion-notes');
+ });
+
+ describe('not collapsed', () => {
+ beforeEach(() => {
+ domHelper.toggleCollapsed({
+ currentTarget: element.querySelector('button'),
+ });
+ });
+
+ it('should add collapsed class', () => {
+ expect(discussionNotesEl.classList.contains('collapsed')).toEqual(true);
+ });
+
+ it('should force formEl to display none', () => {
+ const formEl = element.querySelector('.discussion-form');
+
+ expect(formEl.style.display).toEqual('none');
+ });
+ });
+
+ describe('collapsed', () => {
+ beforeEach(() => {
+ discussionNotesEl.classList.add('collapsed');
+
+ domHelper.toggleCollapsed({
+ currentTarget: element.querySelector('button'),
+ });
+ });
+
+ it('should remove collapsed class', () => {
+ expect(discussionNotesEl.classList.contains('collapsed')).toEqual(false);
+ });
+
+ it('should force formEl to display block', () => {
+ const formEl = element.querySelector('.discussion-form');
+
+ expect(formEl.style.display).toEqual('block');
+ });
+ });
+ });
+});
diff --git a/spec/javascripts/image_diff/helpers/utils_helper_spec.js b/spec/frontend/image_diff/helpers/utils_helper_spec.js
index 3b6378be883..3b6378be883 100644
--- a/spec/javascripts/image_diff/helpers/utils_helper_spec.js
+++ b/spec/frontend/image_diff/helpers/utils_helper_spec.js
diff --git a/spec/frontend/image_diff/image_badge_spec.js b/spec/frontend/image_diff/image_badge_spec.js
new file mode 100644
index 00000000000..a11b50ead47
--- /dev/null
+++ b/spec/frontend/image_diff/image_badge_spec.js
@@ -0,0 +1,84 @@
+import ImageBadge from '~/image_diff/image_badge';
+import imageDiffHelper from '~/image_diff/helpers/index';
+import * as mockData from './mock_data';
+
+describe('ImageBadge', () => {
+ const { noteId, discussionId, imageMeta } = mockData;
+ const options = {
+ noteId,
+ discussionId,
+ };
+
+ it('should save actual property', () => {
+ const imageBadge = new ImageBadge({ ...options, actual: imageMeta });
+
+ const { actual } = imageBadge;
+
+ expect(actual.x).toEqual(imageMeta.x);
+ expect(actual.y).toEqual(imageMeta.y);
+ expect(actual.width).toEqual(imageMeta.width);
+ expect(actual.height).toEqual(imageMeta.height);
+ });
+
+ it('should save browser property', () => {
+ const imageBadge = new ImageBadge({ ...options, browser: imageMeta });
+
+ const { browser } = imageBadge;
+
+ expect(browser.x).toEqual(imageMeta.x);
+ expect(browser.y).toEqual(imageMeta.y);
+ expect(browser.width).toEqual(imageMeta.width);
+ expect(browser.height).toEqual(imageMeta.height);
+ });
+
+ it('should save noteId', () => {
+ const imageBadge = new ImageBadge(options);
+
+ expect(imageBadge.noteId).toEqual(noteId);
+ });
+
+ it('should save discussionId', () => {
+ const imageBadge = new ImageBadge(options);
+
+ expect(imageBadge.discussionId).toEqual(discussionId);
+ });
+
+ describe('default values', () => {
+ let imageBadge;
+
+ beforeEach(() => {
+ imageBadge = new ImageBadge(options);
+ });
+
+ it('should return defaultimageMeta if actual property is not provided', () => {
+ const { actual } = imageBadge;
+
+ expect(actual.x).toEqual(0);
+ expect(actual.y).toEqual(0);
+ expect(actual.width).toEqual(0);
+ expect(actual.height).toEqual(0);
+ });
+
+ it('should return defaultimageMeta if browser property is not provided', () => {
+ const { browser } = imageBadge;
+
+ expect(browser.x).toEqual(0);
+ expect(browser.y).toEqual(0);
+ expect(browser.width).toEqual(0);
+ expect(browser.height).toEqual(0);
+ });
+ });
+
+ describe('imageEl property is provided and not browser property', () => {
+ beforeEach(() => {
+ jest.spyOn(imageDiffHelper, 'resizeCoordinatesToImageElement').mockReturnValue(true);
+ });
+
+ it('should generate browser property', () => {
+ const imageBadge = new ImageBadge({ ...options, imageEl: document.createElement('img') });
+
+ expect(imageDiffHelper.resizeCoordinatesToImageElement).toHaveBeenCalled();
+ expect(imageBadge.browser).toEqual(true);
+ });
+ });
+});
diff --git a/spec/frontend/image_diff/image_diff_spec.js b/spec/frontend/image_diff/image_diff_spec.js
new file mode 100644
index 00000000000..c15718b5106
--- /dev/null
+++ b/spec/frontend/image_diff/image_diff_spec.js
@@ -0,0 +1,361 @@
+import ImageDiff from '~/image_diff/image_diff';
+import * as imageUtility from '~/lib/utils/image_utility';
+import imageDiffHelper from '~/image_diff/helpers/index';
+import * as mockData from './mock_data';
+
+describe('ImageDiff', () => {
+ let element;
+ let imageDiff;
+
+ beforeEach(() => {
+ setFixtures(`
+ <div id="element">
+ <div class="diff-file">
+ <div class="js-image-frame">
+ <img src="${gl.TEST_HOST}/image.png">
+ <div class="comment-indicator"></div>
+ <div id="badge-1" class="badge">1</div>
+ <div id="badge-2" class="badge">2</div>
+ <div id="badge-3" class="badge">3</div>
+ </div>
+ <div class="note-container">
+ <div class="discussion-notes">
+ <div class="js-diff-notes-toggle"></div>
+ <div class="notes"></div>
+ </div>
+ <div class="discussion-notes">
+ <div class="js-diff-notes-toggle"></div>
+ <div class="notes"></div>
+ </div>
+ </div>
+ </div>
+ </div>
+ `);
+ element = document.getElementById('element');
+ });
+
+ describe('constructor', () => {
+ beforeEach(() => {
+ imageDiff = new ImageDiff(element, {
+ canCreateNote: true,
+ renderCommentBadge: true,
+ });
+ });
+
+ it('should set el', () => {
+ expect(imageDiff.el).toEqual(element);
+ });
+
+ it('should set canCreateNote', () => {
+ expect(imageDiff.canCreateNote).toEqual(true);
+ });
+
+ it('should set renderCommentBadge', () => {
+ expect(imageDiff.renderCommentBadge).toEqual(true);
+ });
+
+ it('should set $noteContainer', () => {
+ expect(imageDiff.$noteContainer[0]).toEqual(element.querySelector('.note-container'));
+ });
+
+ describe('default', () => {
+ beforeEach(() => {
+ imageDiff = new ImageDiff(element);
+ });
+
+ it('should set canCreateNote as false', () => {
+ expect(imageDiff.canCreateNote).toEqual(false);
+ });
+
+ it('should set renderCommentBadge as false', () => {
+ expect(imageDiff.renderCommentBadge).toEqual(false);
+ });
+ });
+ });
+
+ describe('init', () => {
+ beforeEach(() => {
+ jest.spyOn(ImageDiff.prototype, 'bindEvents').mockImplementation(() => {});
+ imageDiff = new ImageDiff(element);
+ imageDiff.init();
+ });
+
+ it('should set imageFrameEl', () => {
+ expect(imageDiff.imageFrameEl).toEqual(element.querySelector('.diff-file .js-image-frame'));
+ });
+
+ it('should set imageEl', () => {
+ expect(imageDiff.imageEl).toEqual(element.querySelector('.diff-file .js-image-frame img'));
+ });
+
+ it('should call bindEvents', () => {
+ expect(imageDiff.bindEvents).toHaveBeenCalled();
+ });
+ });
+
+ describe('bindEvents', () => {
+ let imageEl;
+
+ beforeEach(() => {
+ jest.spyOn(imageDiffHelper, 'toggleCollapsed').mockImplementation(() => {});
+ jest.spyOn(imageDiffHelper, 'commentIndicatorOnClick').mockImplementation(() => {});
+ jest.spyOn(imageDiffHelper, 'removeCommentIndicator').mockImplementation(() => {});
+ jest.spyOn(ImageDiff.prototype, 'imageClicked').mockImplementation(() => {});
+ jest.spyOn(ImageDiff.prototype, 'addBadge').mockImplementation(() => {});
+ jest.spyOn(ImageDiff.prototype, 'removeBadge').mockImplementation(() => {});
+ jest.spyOn(ImageDiff.prototype, 'renderBadges').mockImplementation(() => {});
+ imageEl = element.querySelector('.diff-file .js-image-frame img');
+ });
+
+ describe('default', () => {
+ beforeEach(() => {
+ jest.spyOn(imageUtility, 'isImageLoaded').mockReturnValue(false);
+ imageDiff = new ImageDiff(element);
+ imageDiff.imageEl = imageEl;
+ imageDiff.bindEvents();
+ });
+
+ it('should register click event delegation to js-diff-notes-toggle', () => {
+ element.querySelector('.js-diff-notes-toggle').click();
+
+ expect(imageDiffHelper.toggleCollapsed).toHaveBeenCalled();
+ });
+
+ it('should register click event delegation to comment-indicator', () => {
+ element.querySelector('.comment-indicator').click();
+
+ expect(imageDiffHelper.commentIndicatorOnClick).toHaveBeenCalled();
+ });
+ });
+
+ describe('image not loaded', () => {
+ beforeEach(() => {
+ jest.spyOn(imageUtility, 'isImageLoaded').mockReturnValue(false);
+ imageDiff = new ImageDiff(element);
+ imageDiff.imageEl = imageEl;
+ imageDiff.bindEvents();
+ });
+
+ it('should registers load eventListener', () => {
+ const loadEvent = new Event('load');
+ imageEl.dispatchEvent(loadEvent);
+
+ expect(imageDiff.renderBadges).toHaveBeenCalled();
+ });
+ });
+
+ describe('canCreateNote', () => {
+ beforeEach(() => {
+ jest.spyOn(imageUtility, 'isImageLoaded').mockReturnValue(false);
+ imageDiff = new ImageDiff(element, {
+ canCreateNote: true,
+ });
+ imageDiff.imageEl = imageEl;
+ imageDiff.bindEvents();
+ });
+
+ it('should register click.imageDiff event', () => {
+ const event = new CustomEvent('click.imageDiff');
+ element.dispatchEvent(event);
+
+ expect(imageDiff.imageClicked).toHaveBeenCalled();
+ });
+
+ it('should register blur.imageDiff event', () => {
+ const event = new CustomEvent('blur.imageDiff');
+ element.dispatchEvent(event);
+
+ expect(imageDiffHelper.removeCommentIndicator).toHaveBeenCalled();
+ });
+
+ it('should register addBadge.imageDiff event', () => {
+ const event = new CustomEvent('addBadge.imageDiff');
+ element.dispatchEvent(event);
+
+ expect(imageDiff.addBadge).toHaveBeenCalled();
+ });
+
+ it('should register removeBadge.imageDiff event', () => {
+ const event = new CustomEvent('removeBadge.imageDiff');
+ element.dispatchEvent(event);
+
+ expect(imageDiff.removeBadge).toHaveBeenCalled();
+ });
+ });
+
+ describe('canCreateNote is false', () => {
+ beforeEach(() => {
+ jest.spyOn(imageUtility, 'isImageLoaded').mockReturnValue(false);
+ imageDiff = new ImageDiff(element);
+ imageDiff.imageEl = imageEl;
+ imageDiff.bindEvents();
+ });
+
+ it('should not register click.imageDiff event', () => {
+ const event = new CustomEvent('click.imageDiff');
+ element.dispatchEvent(event);
+
+ expect(imageDiff.imageClicked).not.toHaveBeenCalled();
+ });
+ });
+ });
+
+ describe('imageClicked', () => {
+ beforeEach(() => {
+ jest.spyOn(imageDiffHelper, 'getTargetSelection').mockReturnValue({
+ actual: {},
+ browser: {},
+ });
+ jest.spyOn(imageDiffHelper, 'setPositionDataAttribute').mockImplementation(() => {});
+ jest.spyOn(imageDiffHelper, 'showCommentIndicator').mockImplementation(() => {});
+ imageDiff = new ImageDiff(element);
+ imageDiff.imageClicked({
+ detail: {
+ currentTarget: {},
+ },
+ });
+ });
+
+ it('should call getTargetSelection', () => {
+ expect(imageDiffHelper.getTargetSelection).toHaveBeenCalled();
+ });
+
+ it('should call setPositionDataAttribute', () => {
+ expect(imageDiffHelper.setPositionDataAttribute).toHaveBeenCalled();
+ });
+
+ it('should call showCommentIndicator', () => {
+ expect(imageDiffHelper.showCommentIndicator).toHaveBeenCalled();
+ });
+ });
+
+ describe('renderBadges', () => {
+ beforeEach(() => {
+ jest.spyOn(ImageDiff.prototype, 'renderBadge').mockImplementation(() => {});
+ imageDiff = new ImageDiff(element);
+ imageDiff.renderBadges();
+ });
+
+ it('should call renderBadge for each discussionEl', () => {
+ const discussionEls = element.querySelectorAll('.note-container .discussion-notes .notes');
+
+ expect(imageDiff.renderBadge.mock.calls.length).toEqual(discussionEls.length);
+ });
+ });
+
+ describe('renderBadge', () => {
+ let discussionEls;
+
+ beforeEach(() => {
+ jest.spyOn(imageDiffHelper, 'addImageBadge').mockImplementation(() => {});
+ jest.spyOn(imageDiffHelper, 'addImageCommentBadge').mockImplementation(() => {});
+ jest.spyOn(imageDiffHelper, 'generateBadgeFromDiscussionDOM').mockReturnValue({
+ browser: {},
+ noteId: 'noteId',
+ });
+ discussionEls = element.querySelectorAll('.note-container .discussion-notes .notes');
+ imageDiff = new ImageDiff(element);
+ imageDiff.renderBadge(discussionEls[0], 0);
+ });
+
+ it('should populate imageBadges', () => {
+ expect(imageDiff.imageBadges.length).toEqual(1);
+ });
+
+ describe('renderCommentBadge', () => {
+ beforeEach(() => {
+ imageDiff.renderCommentBadge = true;
+ imageDiff.renderBadge(discussionEls[0], 0);
+ });
+
+ it('should call addImageCommentBadge', () => {
+ expect(imageDiffHelper.addImageCommentBadge).toHaveBeenCalled();
+ });
+ });
+
+ describe('renderCommentBadge is false', () => {
+ it('should call addImageBadge', () => {
+ expect(imageDiffHelper.addImageBadge).toHaveBeenCalled();
+ });
+ });
+ });
+
+ describe('addBadge', () => {
+ beforeEach(() => {
+ jest.spyOn(imageDiffHelper, 'addImageBadge').mockImplementation(() => {});
+ jest.spyOn(imageDiffHelper, 'addAvatarBadge').mockImplementation(() => {});
+ jest.spyOn(imageDiffHelper, 'updateDiscussionBadgeNumber').mockImplementation(() => {});
+ imageDiff = new ImageDiff(element);
+ imageDiff.imageFrameEl = element.querySelector('.diff-file .js-image-frame');
+ imageDiff.addBadge({
+ detail: {
+ x: 0,
+ y: 1,
+ width: 25,
+ height: 50,
+ noteId: 'noteId',
+ discussionId: 'discussionId',
+ },
+ });
+ });
+
+ it('should add imageBadge to imageBadges', () => {
+ expect(imageDiff.imageBadges.length).toEqual(1);
+ });
+
+ it('should call addImageBadge', () => {
+ expect(imageDiffHelper.addImageBadge).toHaveBeenCalled();
+ });
+
+ it('should call addAvatarBadge', () => {
+ expect(imageDiffHelper.addAvatarBadge).toHaveBeenCalled();
+ });
+
+ it('should call updateDiscussionBadgeNumber', () => {
+ expect(imageDiffHelper.updateDiscussionBadgeNumber).toHaveBeenCalled();
+ });
+ });
+
+ describe('removeBadge', () => {
+ beforeEach(() => {
+ const { imageMeta } = mockData;
+
+ jest.spyOn(imageDiffHelper, 'updateDiscussionBadgeNumber').mockImplementation(() => {});
+ jest.spyOn(imageDiffHelper, 'updateDiscussionAvatarBadgeNumber').mockImplementation(() => {});
+ imageDiff = new ImageDiff(element);
+ imageDiff.imageBadges = [imageMeta, imageMeta, imageMeta];
+ imageDiff.imageFrameEl = element.querySelector('.diff-file .js-image-frame');
+ imageDiff.removeBadge({
+ detail: {
+ badgeNumber: 2,
+ },
+ });
+ });
+
+ describe('cascade badge count', () => {
+ it('should update next imageBadgeEl value', () => {
+ const imageBadgeEls = imageDiff.imageFrameEl.querySelectorAll('.badge');
+
+ expect(imageBadgeEls[0].textContent).toEqual('1');
+ expect(imageBadgeEls[1].textContent).toEqual('2');
+ expect(imageBadgeEls.length).toEqual(2);
+ });
+
+ it('should call updateDiscussionBadgeNumber', () => {
+ expect(imageDiffHelper.updateDiscussionBadgeNumber).toHaveBeenCalled();
+ });
+
+ it('should call updateDiscussionAvatarBadgeNumber', () => {
+ expect(imageDiffHelper.updateDiscussionAvatarBadgeNumber).toHaveBeenCalled();
+ });
+ });
+
+ it('should remove badge from imageBadges', () => {
+ expect(imageDiff.imageBadges.length).toEqual(2);
+ });
+
+ it('should remove imageBadgeEl', () => {
+ expect(imageDiff.imageFrameEl.querySelector('#badge-2')).toBeNull();
+ });
+ });
+});
diff --git a/spec/javascripts/image_diff/mock_data.js b/spec/frontend/image_diff/mock_data.js
index a0d1732dd0a..a0d1732dd0a 100644
--- a/spec/javascripts/image_diff/mock_data.js
+++ b/spec/frontend/image_diff/mock_data.js
diff --git a/spec/frontend/image_diff/replaced_image_diff_spec.js b/spec/frontend/image_diff/replaced_image_diff_spec.js
new file mode 100644
index 00000000000..f2a7b7f8406
--- /dev/null
+++ b/spec/frontend/image_diff/replaced_image_diff_spec.js
@@ -0,0 +1,356 @@
+import ReplacedImageDiff from '~/image_diff/replaced_image_diff';
+import ImageDiff from '~/image_diff/image_diff';
+import { viewTypes } from '~/image_diff/view_types';
+import imageDiffHelper from '~/image_diff/helpers/index';
+
+describe('ReplacedImageDiff', () => {
+ let element;
+ let replacedImageDiff;
+
+ beforeEach(() => {
+ setFixtures(`
+ <div id="element">
+ <div class="two-up">
+ <div class="js-image-frame">
+ <img src="${gl.TEST_HOST}/image.png">
+ </div>
+ </div>
+ <div class="swipe">
+ <div class="js-image-frame">
+ <img src="${gl.TEST_HOST}/image.png">
+ </div>
+ </div>
+ <div class="onion-skin">
+ <div class="js-image-frame">
+ <img src="${gl.TEST_HOST}/image.png">
+ </div>
+ </div>
+ <div class="view-modes-menu">
+ <div class="two-up">2-up</div>
+ <div class="swipe">Swipe</div>
+ <div class="onion-skin">Onion skin</div>
+ </div>
+ </div>
+ `);
+ element = document.getElementById('element');
+ });
+
+ function setupImageFrameEls() {
+ replacedImageDiff.imageFrameEls = [];
+ replacedImageDiff.imageFrameEls[viewTypes.TWO_UP] = element.querySelector(
+ '.two-up .js-image-frame',
+ );
+ replacedImageDiff.imageFrameEls[viewTypes.SWIPE] = element.querySelector(
+ '.swipe .js-image-frame',
+ );
+ replacedImageDiff.imageFrameEls[viewTypes.ONION_SKIN] = element.querySelector(
+ '.onion-skin .js-image-frame',
+ );
+ }
+
+ function setupViewModesEls() {
+ replacedImageDiff.viewModesEls = [];
+ replacedImageDiff.viewModesEls[viewTypes.TWO_UP] = element.querySelector(
+ '.view-modes-menu .two-up',
+ );
+ replacedImageDiff.viewModesEls[viewTypes.SWIPE] = element.querySelector(
+ '.view-modes-menu .swipe',
+ );
+ replacedImageDiff.viewModesEls[viewTypes.ONION_SKIN] = element.querySelector(
+ '.view-modes-menu .onion-skin',
+ );
+ }
+
+ function setupImageEls() {
+ replacedImageDiff.imageEls = [];
+ replacedImageDiff.imageEls[viewTypes.TWO_UP] = element.querySelector('.two-up img');
+ replacedImageDiff.imageEls[viewTypes.SWIPE] = element.querySelector('.swipe img');
+ replacedImageDiff.imageEls[viewTypes.ONION_SKIN] = element.querySelector('.onion-skin img');
+ }
+
+ it('should extend ImageDiff', () => {
+ replacedImageDiff = new ReplacedImageDiff(element);
+
+ expect(replacedImageDiff instanceof ImageDiff).toEqual(true);
+ });
+
+ describe('init', () => {
+ beforeEach(() => {
+ jest.spyOn(ReplacedImageDiff.prototype, 'bindEvents').mockImplementation(() => {});
+ jest.spyOn(ReplacedImageDiff.prototype, 'generateImageEls').mockImplementation(() => {});
+
+ replacedImageDiff = new ReplacedImageDiff(element);
+ replacedImageDiff.init();
+ });
+
+ it('should set imageFrameEls', () => {
+ const { imageFrameEls } = replacedImageDiff;
+
+ expect(imageFrameEls).toBeDefined();
+ expect(imageFrameEls[viewTypes.TWO_UP]).toEqual(
+ element.querySelector('.two-up .js-image-frame'),
+ );
+
+ expect(imageFrameEls[viewTypes.SWIPE]).toEqual(
+ element.querySelector('.swipe .js-image-frame'),
+ );
+
+ expect(imageFrameEls[viewTypes.ONION_SKIN]).toEqual(
+ element.querySelector('.onion-skin .js-image-frame'),
+ );
+ });
+
+ it('should set viewModesEls', () => {
+ const { viewModesEls } = replacedImageDiff;
+
+ expect(viewModesEls).toBeDefined();
+ expect(viewModesEls[viewTypes.TWO_UP]).toEqual(
+ element.querySelector('.view-modes-menu .two-up'),
+ );
+
+ expect(viewModesEls[viewTypes.SWIPE]).toEqual(
+ element.querySelector('.view-modes-menu .swipe'),
+ );
+
+ expect(viewModesEls[viewTypes.ONION_SKIN]).toEqual(
+ element.querySelector('.view-modes-menu .onion-skin'),
+ );
+ });
+
+ it('should generateImageEls', () => {
+ expect(ReplacedImageDiff.prototype.generateImageEls).toHaveBeenCalled();
+ });
+
+ it('should bindEvents', () => {
+ expect(ReplacedImageDiff.prototype.bindEvents).toHaveBeenCalled();
+ });
+
+ describe('currentView', () => {
+ it('should set currentView', () => {
+ replacedImageDiff.init(viewTypes.ONION_SKIN);
+
+ expect(replacedImageDiff.currentView).toEqual(viewTypes.ONION_SKIN);
+ });
+
+ it('should default to viewTypes.TWO_UP', () => {
+ expect(replacedImageDiff.currentView).toEqual(viewTypes.TWO_UP);
+ });
+ });
+ });
+
+ describe('generateImageEls', () => {
+ beforeEach(() => {
+ jest.spyOn(ReplacedImageDiff.prototype, 'bindEvents').mockImplementation(() => {});
+
+ replacedImageDiff = new ReplacedImageDiff(element, {
+ canCreateNote: false,
+ renderCommentBadge: false,
+ });
+
+ setupImageFrameEls();
+ });
+
+ it('should set imageEls', () => {
+ replacedImageDiff.generateImageEls();
+ const { imageEls } = replacedImageDiff;
+
+ expect(imageEls).toBeDefined();
+ expect(imageEls[viewTypes.TWO_UP]).toEqual(element.querySelector('.two-up img'));
+ expect(imageEls[viewTypes.SWIPE]).toEqual(element.querySelector('.swipe img'));
+ expect(imageEls[viewTypes.ONION_SKIN]).toEqual(element.querySelector('.onion-skin img'));
+ });
+ });
+
+ describe('bindEvents', () => {
+ beforeEach(() => {
+ jest.spyOn(ImageDiff.prototype, 'bindEvents').mockImplementation(() => {});
+ replacedImageDiff = new ReplacedImageDiff(element);
+
+ setupViewModesEls();
+ });
+
+ it('should call super.bindEvents', () => {
+ replacedImageDiff.bindEvents();
+
+ expect(ImageDiff.prototype.bindEvents).toHaveBeenCalled();
+ });
+
+ it('should register click eventlistener to 2-up view mode', done => {
+ jest.spyOn(ReplacedImageDiff.prototype, 'changeView').mockImplementation(viewMode => {
+ expect(viewMode).toEqual(viewTypes.TWO_UP);
+ done();
+ });
+
+ replacedImageDiff.bindEvents();
+ replacedImageDiff.viewModesEls[viewTypes.TWO_UP].click();
+ });
+
+ it('should register click eventlistener to swipe view mode', done => {
+ jest.spyOn(ReplacedImageDiff.prototype, 'changeView').mockImplementation(viewMode => {
+ expect(viewMode).toEqual(viewTypes.SWIPE);
+ done();
+ });
+
+ replacedImageDiff.bindEvents();
+ replacedImageDiff.viewModesEls[viewTypes.SWIPE].click();
+ });
+
+ it('should register click eventlistener to onion skin view mode', done => {
+ jest.spyOn(ReplacedImageDiff.prototype, 'changeView').mockImplementation(viewMode => {
+ expect(viewMode).toEqual(viewTypes.SWIPE);
+ done();
+ });
+
+ replacedImageDiff.bindEvents();
+ replacedImageDiff.viewModesEls[viewTypes.SWIPE].click();
+ });
+ });
+
+ describe('getters', () => {
+ describe('imageEl', () => {
+ beforeEach(() => {
+ replacedImageDiff = new ReplacedImageDiff(element);
+ replacedImageDiff.currentView = viewTypes.TWO_UP;
+ setupImageEls();
+ });
+
+ it('should return imageEl based on currentView', () => {
+ expect(replacedImageDiff.imageEl).toEqual(element.querySelector('.two-up img'));
+
+ replacedImageDiff.currentView = viewTypes.SWIPE;
+
+ expect(replacedImageDiff.imageEl).toEqual(element.querySelector('.swipe img'));
+ });
+ });
+
+ describe('imageFrameEl', () => {
+ beforeEach(() => {
+ replacedImageDiff = new ReplacedImageDiff(element);
+ replacedImageDiff.currentView = viewTypes.TWO_UP;
+ setupImageFrameEls();
+ });
+
+ it('should return imageFrameEl based on currentView', () => {
+ expect(replacedImageDiff.imageFrameEl).toEqual(
+ element.querySelector('.two-up .js-image-frame'),
+ );
+
+ replacedImageDiff.currentView = viewTypes.ONION_SKIN;
+
+ expect(replacedImageDiff.imageFrameEl).toEqual(
+ element.querySelector('.onion-skin .js-image-frame'),
+ );
+ });
+ });
+ });
+
+ describe('changeView', () => {
+ beforeEach(() => {
+ replacedImageDiff = new ReplacedImageDiff(element);
+ jest.spyOn(imageDiffHelper, 'removeCommentIndicator').mockReturnValue({
+ removed: false,
+ });
+ setupImageFrameEls();
+ });
+
+ describe('invalid viewType', () => {
+ beforeEach(() => {
+ replacedImageDiff.changeView('some-view-name');
+ });
+
+ it('should not call removeCommentIndicator', () => {
+ expect(imageDiffHelper.removeCommentIndicator).not.toHaveBeenCalled();
+ });
+ });
+
+ describe('valid viewType', () => {
+ beforeEach(() => {
+ jest.spyOn(ReplacedImageDiff.prototype, 'renderNewView').mockImplementation(() => {});
+ replacedImageDiff.changeView(viewTypes.ONION_SKIN);
+ });
+
+ afterEach(() => {
+ jest.clearAllTimers();
+ });
+
+ it('should call removeCommentIndicator', () => {
+ expect(imageDiffHelper.removeCommentIndicator).toHaveBeenCalled();
+ });
+
+ it('should update currentView to newView', () => {
+ expect(replacedImageDiff.currentView).toEqual(viewTypes.ONION_SKIN);
+ });
+
+ it('should clear imageBadges', () => {
+ expect(replacedImageDiff.imageBadges.length).toEqual(0);
+ });
+
+ it('should call renderNewView', () => {
+ jest.advanceTimersByTime(251);
+
+ expect(replacedImageDiff.renderNewView).toHaveBeenCalled();
+ });
+ });
+ });
+
+ describe('renderNewView', () => {
+ beforeEach(() => {
+ replacedImageDiff = new ReplacedImageDiff(element);
+ });
+
+ it('should call renderBadges', () => {
+ jest.spyOn(ReplacedImageDiff.prototype, 'renderBadges').mockImplementation(() => {});
+
+ replacedImageDiff.renderNewView({
+ removed: false,
+ });
+
+ expect(replacedImageDiff.renderBadges).toHaveBeenCalled();
+ });
+
+ describe('removeIndicator', () => {
+ const indicator = {
+ removed: true,
+ x: 0,
+ y: 1,
+ image: {
+ width: 50,
+ height: 100,
+ },
+ };
+
+ beforeEach(() => {
+ setupImageEls();
+ setupImageFrameEls();
+ });
+
+ it('should pass showCommentIndicator normalized indicator values', done => {
+ jest.spyOn(imageDiffHelper, 'showCommentIndicator').mockImplementation(() => {});
+ jest
+ .spyOn(imageDiffHelper, 'resizeCoordinatesToImageElement')
+ .mockImplementation((imageEl, meta) => {
+ expect(meta.x).toEqual(indicator.x);
+ expect(meta.y).toEqual(indicator.y);
+ expect(meta.width).toEqual(indicator.image.width);
+ expect(meta.height).toEqual(indicator.image.height);
+ done();
+ });
+ replacedImageDiff.renderNewView(indicator);
+ });
+
+ it('should call showCommentIndicator', done => {
+ const normalized = {
+ normalized: true,
+ };
+ jest.spyOn(imageDiffHelper, 'resizeCoordinatesToImageElement').mockReturnValue(normalized);
+ jest
+ .spyOn(imageDiffHelper, 'showCommentIndicator')
+ .mockImplementation((imageFrameEl, normalizedIndicator) => {
+ expect(normalizedIndicator).toEqual(normalized);
+ done();
+ });
+ replacedImageDiff.renderNewView(indicator);
+ });
+ });
+ });
+});
diff --git a/spec/frontend/import_projects/components/import_projects_table_spec.js b/spec/frontend/import_projects/components/import_projects_table_spec.js
index 8f60823ee72..9491b52c888 100644
--- a/spec/frontend/import_projects/components/import_projects_table_spec.js
+++ b/spec/frontend/import_projects/components/import_projects_table_spec.js
@@ -17,11 +17,12 @@ describe('ImportProjectsTable', () => {
};
function initStore() {
- const stubbedActions = Object.assign({}, actions, {
+ const stubbedActions = {
+ ...actions,
fetchJobs: jest.fn(),
fetchRepos: jest.fn(actions.requestRepos),
fetchImport: jest.fn(actions.requestImport),
- });
+ };
const store = new Vuex.Store({
state: state(),
diff --git a/spec/frontend/import_projects/components/provider_repo_table_row_spec.js b/spec/frontend/import_projects/components/provider_repo_table_row_spec.js
index 8efd526e360..8be645c496f 100644
--- a/spec/frontend/import_projects/components/provider_repo_table_row_spec.js
+++ b/spec/frontend/import_projects/components/provider_repo_table_row_spec.js
@@ -18,9 +18,7 @@ describe('ProviderRepoTableRow', () => {
};
function initStore() {
- const stubbedActions = Object.assign({}, actions, {
- fetchImport,
- });
+ const stubbedActions = { ...actions, fetchImport };
const store = new Vuex.Store({
state: state(),
diff --git a/spec/frontend/integrations/edit/components/active_toggle_spec.js b/spec/frontend/integrations/edit/components/active_toggle_spec.js
index 8a11c200c15..5469b45f708 100644
--- a/spec/frontend/integrations/edit/components/active_toggle_spec.js
+++ b/spec/frontend/integrations/edit/components/active_toggle_spec.js
@@ -9,17 +9,19 @@ describe('ActiveToggle', () => {
const defaultProps = {
initialActivated: true,
- disabled: false,
};
const createComponent = props => {
wrapper = mount(ActiveToggle, {
- propsData: Object.assign({}, defaultProps, props),
+ propsData: { ...defaultProps, ...props },
});
};
afterEach(() => {
- if (wrapper) wrapper.destroy();
+ if (wrapper) {
+ wrapper.destroy();
+ wrapper = null;
+ }
});
const findGlToggle = () => wrapper.find(GlToggle);
diff --git a/spec/frontend/integrations/edit/components/integration_form_spec.js b/spec/frontend/integrations/edit/components/integration_form_spec.js
new file mode 100644
index 00000000000..c93f63b11d0
--- /dev/null
+++ b/spec/frontend/integrations/edit/components/integration_form_spec.js
@@ -0,0 +1,99 @@
+import { shallowMount } from '@vue/test-utils';
+import IntegrationForm from '~/integrations/edit/components/integration_form.vue';
+import ActiveToggle from '~/integrations/edit/components/active_toggle.vue';
+import JiraTriggerFields from '~/integrations/edit/components/jira_trigger_fields.vue';
+import TriggerFields from '~/integrations/edit/components/trigger_fields.vue';
+
+describe('IntegrationForm', () => {
+ let wrapper;
+
+ const defaultProps = {
+ activeToggleProps: {
+ initialActivated: true,
+ },
+ showActive: true,
+ triggerFieldsProps: {
+ initialTriggerCommit: false,
+ initialTriggerMergeRequest: false,
+ initialEnableComments: false,
+ },
+ type: '',
+ };
+
+ const createComponent = props => {
+ wrapper = shallowMount(IntegrationForm, {
+ propsData: { ...defaultProps, ...props },
+ stubs: {
+ ActiveToggle,
+ JiraTriggerFields,
+ },
+ });
+ };
+
+ afterEach(() => {
+ if (wrapper) {
+ wrapper.destroy();
+ wrapper = null;
+ }
+ });
+
+ const findActiveToggle = () => wrapper.find(ActiveToggle);
+ const findJiraTriggerFields = () => wrapper.find(JiraTriggerFields);
+ const findTriggerFields = () => wrapper.find(TriggerFields);
+
+ describe('template', () => {
+ describe('showActive is true', () => {
+ it('renders ActiveToggle', () => {
+ createComponent();
+
+ expect(findActiveToggle().exists()).toBe(true);
+ });
+ });
+
+ describe('showActive is false', () => {
+ it('does not render ActiveToggle', () => {
+ createComponent({
+ showActive: false,
+ });
+
+ expect(findActiveToggle().exists()).toBe(false);
+ });
+ });
+
+ describe('type is "slack"', () => {
+ it('does not render JiraTriggerFields', () => {
+ createComponent({
+ type: 'slack',
+ });
+
+ expect(findJiraTriggerFields().exists()).toBe(false);
+ });
+ });
+
+ describe('type is "jira"', () => {
+ it('renders JiraTriggerFields', () => {
+ createComponent({
+ type: 'jira',
+ });
+
+ expect(findJiraTriggerFields().exists()).toBe(true);
+ });
+ });
+
+ describe('triggerEvents is present', () => {
+ it('renders TriggerFields', () => {
+ const events = [{ title: 'push' }];
+ const type = 'slack';
+
+ createComponent({
+ triggerEvents: events,
+ type,
+ });
+
+ expect(findTriggerFields().exists()).toBe(true);
+ expect(findTriggerFields().props('events')).toBe(events);
+ expect(findTriggerFields().props('type')).toBe(type);
+ });
+ });
+ });
+});
diff --git a/spec/frontend/integrations/edit/components/jira_trigger_fields_spec.js b/spec/frontend/integrations/edit/components/jira_trigger_fields_spec.js
new file mode 100644
index 00000000000..e4c2a0be6a3
--- /dev/null
+++ b/spec/frontend/integrations/edit/components/jira_trigger_fields_spec.js
@@ -0,0 +1,97 @@
+import { mount } from '@vue/test-utils';
+import JiraTriggerFields from '~/integrations/edit/components/jira_trigger_fields.vue';
+import { GlFormCheckbox } from '@gitlab/ui';
+
+describe('JiraTriggerFields', () => {
+ let wrapper;
+
+ const defaultProps = {
+ initialTriggerCommit: false,
+ initialTriggerMergeRequest: false,
+ initialEnableComments: false,
+ };
+
+ const createComponent = props => {
+ wrapper = mount(JiraTriggerFields, {
+ propsData: { ...defaultProps, ...props },
+ });
+ };
+
+ afterEach(() => {
+ if (wrapper) {
+ wrapper.destroy();
+ wrapper = null;
+ }
+ });
+
+ const findCommentSettings = () => wrapper.find('[data-testid="comment-settings"]');
+ const findCommentDetail = () => wrapper.find('[data-testid="comment-detail"]');
+ const findCommentSettingsCheckbox = () => findCommentSettings().find(GlFormCheckbox);
+
+ describe('template', () => {
+ describe('initialTriggerCommit and initialTriggerMergeRequest are false', () => {
+ it('does not show comment settings', () => {
+ createComponent();
+
+ expect(findCommentSettings().isVisible()).toBe(false);
+ expect(findCommentDetail().isVisible()).toBe(false);
+ });
+ });
+
+ describe('initialTriggerCommit is true', () => {
+ beforeEach(() => {
+ createComponent({
+ initialTriggerCommit: true,
+ });
+ });
+
+ it('shows comment settings', () => {
+ expect(findCommentSettings().isVisible()).toBe(true);
+ expect(findCommentDetail().isVisible()).toBe(false);
+ });
+
+ // As per https://vuejs.org/v2/guide/forms.html#Checkbox-1,
+ // browsers don't include unchecked boxes in form submissions.
+ it('includes comment settings as false even if unchecked', () => {
+ expect(
+ findCommentSettings()
+ .find('input[name="service[comment_on_event_enabled]"]')
+ .exists(),
+ ).toBe(true);
+ });
+
+ describe('on enable comments', () => {
+ it('shows comment detail', () => {
+ findCommentSettingsCheckbox().vm.$emit('input', true);
+
+ return wrapper.vm.$nextTick().then(() => {
+ expect(findCommentDetail().isVisible()).toBe(true);
+ });
+ });
+ });
+ });
+
+ describe('initialTriggerMergeRequest is true', () => {
+ it('shows comment settings', () => {
+ createComponent({
+ initialTriggerMergeRequest: true,
+ });
+
+ expect(findCommentSettings().isVisible()).toBe(true);
+ expect(findCommentDetail().isVisible()).toBe(false);
+ });
+ });
+
+ describe('initialTriggerCommit is true, initialEnableComments is true', () => {
+ it('shows comment settings and comment detail', () => {
+ createComponent({
+ initialTriggerCommit: true,
+ initialEnableComments: true,
+ });
+
+ expect(findCommentSettings().isVisible()).toBe(true);
+ expect(findCommentDetail().isVisible()).toBe(true);
+ });
+ });
+ });
+});
diff --git a/spec/frontend/integrations/edit/components/trigger_fields_spec.js b/spec/frontend/integrations/edit/components/trigger_fields_spec.js
new file mode 100644
index 00000000000..337876c6d16
--- /dev/null
+++ b/spec/frontend/integrations/edit/components/trigger_fields_spec.js
@@ -0,0 +1,136 @@
+import { mount } from '@vue/test-utils';
+import TriggerFields from '~/integrations/edit/components/trigger_fields.vue';
+import { GlFormGroup, GlFormCheckbox, GlFormInput } from '@gitlab/ui';
+
+describe('TriggerFields', () => {
+ let wrapper;
+
+ const defaultProps = {
+ type: 'slack',
+ };
+
+ const createComponent = props => {
+ wrapper = mount(TriggerFields, {
+ propsData: { ...defaultProps, ...props },
+ });
+ };
+
+ afterEach(() => {
+ if (wrapper) {
+ wrapper.destroy();
+ wrapper = null;
+ }
+ });
+
+ const findAllGlFormCheckboxes = () => wrapper.findAll(GlFormCheckbox);
+ const findAllGlFormInputs = () => wrapper.findAll(GlFormInput);
+
+ describe('template', () => {
+ it('renders a label with text "Trigger"', () => {
+ createComponent();
+
+ const triggerLabel = wrapper.find('[data-testid="trigger-fields-group"]').find('label');
+ expect(triggerLabel.exists()).toBe(true);
+ expect(triggerLabel.text()).toBe('Trigger');
+ });
+
+ describe('events without field property', () => {
+ const events = [
+ {
+ title: 'push',
+ name: 'push_event',
+ description: 'Event on push',
+ value: true,
+ },
+ {
+ title: 'merge_request',
+ name: 'merge_requests_event',
+ description: 'Event on merge_request',
+ value: false,
+ },
+ ];
+
+ beforeEach(() => {
+ createComponent({
+ events,
+ });
+ });
+
+ it('does not render GlFormInput for each event', () => {
+ expect(findAllGlFormInputs().exists()).toBe(false);
+ });
+
+ it('renders GlFormInput with description for each event', () => {
+ const groups = wrapper.find('#trigger-fields').findAll(GlFormGroup);
+
+ expect(groups).toHaveLength(2);
+ groups.wrappers.forEach((group, index) => {
+ expect(group.find('small').text()).toBe(events[index].description);
+ });
+ });
+
+ it('renders GlFormCheckbox for each event', () => {
+ const checkboxes = findAllGlFormCheckboxes();
+ const expectedResults = [
+ { labelText: 'Push', inputName: 'service[push_event]' },
+ { labelText: 'Merge Request', inputName: 'service[merge_requests_event]' },
+ ];
+ expect(checkboxes).toHaveLength(2);
+
+ checkboxes.wrappers.forEach((checkbox, index) => {
+ expect(checkbox.find('label').text()).toBe(expectedResults[index].labelText);
+ expect(checkbox.find('input').attributes('name')).toBe(expectedResults[index].inputName);
+ expect(checkbox.vm.$attrs.checked).toBe(events[index].value);
+ });
+ });
+ });
+
+ describe('events with field property', () => {
+ const events = [
+ {
+ field: {
+ name: 'push_channel',
+ value: '',
+ },
+ },
+ {
+ field: {
+ name: 'merge_request_channel',
+ value: 'gitlab-development',
+ },
+ },
+ ];
+
+ beforeEach(() => {
+ createComponent({
+ events,
+ });
+ });
+
+ it('renders GlFormCheckbox for each event', () => {
+ expect(findAllGlFormCheckboxes()).toHaveLength(2);
+ });
+
+ it('renders GlFormInput for each event', () => {
+ const fields = findAllGlFormInputs();
+ const expectedResults = [
+ {
+ name: 'service[push_channel]',
+ placeholder: 'Slack channels (e.g. general, development)',
+ },
+ {
+ name: 'service[merge_request_channel]',
+ placeholder: 'Slack channels (e.g. general, development)',
+ },
+ ];
+
+ expect(fields).toHaveLength(2);
+
+ fields.wrappers.forEach((field, index) => {
+ expect(field.attributes()).toMatchObject(expectedResults[index]);
+ expect(field.vm.$attrs.value).toBe(events[index].field.value);
+ });
+ });
+ });
+ });
+});
diff --git a/spec/frontend/integrations/integration_settings_form_spec.js b/spec/frontend/integrations/integration_settings_form_spec.js
new file mode 100644
index 00000000000..c117a37ff2f
--- /dev/null
+++ b/spec/frontend/integrations/integration_settings_form_spec.js
@@ -0,0 +1,268 @@
+import $ from 'jquery';
+import MockAdaptor from 'axios-mock-adapter';
+import axios from '~/lib/utils/axios_utils';
+import IntegrationSettingsForm from '~/integrations/integration_settings_form';
+
+describe('IntegrationSettingsForm', () => {
+ const FIXTURE = 'services/edit_service.html';
+ preloadFixtures(FIXTURE);
+
+ beforeEach(() => {
+ loadFixtures(FIXTURE);
+ });
+
+ describe('contructor', () => {
+ let integrationSettingsForm;
+
+ beforeEach(() => {
+ integrationSettingsForm = new IntegrationSettingsForm('.js-integration-settings-form');
+ jest.spyOn(integrationSettingsForm, 'init').mockImplementation(() => {});
+ });
+
+ it('should initialize form element refs on class object', () => {
+ // Form Reference
+ expect(integrationSettingsForm.$form).toBeDefined();
+ expect(integrationSettingsForm.$form.prop('nodeName')).toEqual('FORM');
+ expect(integrationSettingsForm.formActive).toBeDefined();
+
+ // Form Child Elements
+ expect(integrationSettingsForm.$submitBtn).toBeDefined();
+ expect(integrationSettingsForm.$submitBtnLoader).toBeDefined();
+ expect(integrationSettingsForm.$submitBtnLabel).toBeDefined();
+ });
+
+ it('should initialize form metadata on class object', () => {
+ expect(integrationSettingsForm.testEndPoint).toBeDefined();
+ expect(integrationSettingsForm.canTestService).toBeDefined();
+ });
+ });
+
+ describe('toggleServiceState', () => {
+ let integrationSettingsForm;
+
+ beforeEach(() => {
+ integrationSettingsForm = new IntegrationSettingsForm('.js-integration-settings-form');
+ });
+
+ it('should remove `novalidate` attribute to form when called with `true`', () => {
+ integrationSettingsForm.formActive = true;
+ integrationSettingsForm.toggleServiceState();
+
+ expect(integrationSettingsForm.$form.attr('novalidate')).not.toBeDefined();
+ });
+
+ it('should set `novalidate` attribute to form when called with `false`', () => {
+ integrationSettingsForm.formActive = false;
+ integrationSettingsForm.toggleServiceState();
+
+ expect(integrationSettingsForm.$form.attr('novalidate')).toBeDefined();
+ });
+ });
+
+ describe('toggleSubmitBtnLabel', () => {
+ let integrationSettingsForm;
+
+ beforeEach(() => {
+ integrationSettingsForm = new IntegrationSettingsForm('.js-integration-settings-form');
+ });
+
+ it('should set Save button label to "Test settings and save changes" when serviceActive & canTestService are `true`', () => {
+ integrationSettingsForm.canTestService = true;
+ integrationSettingsForm.formActive = true;
+
+ integrationSettingsForm.toggleSubmitBtnLabel();
+
+ expect(integrationSettingsForm.$submitBtnLabel.text()).toEqual(
+ 'Test settings and save changes',
+ );
+ });
+
+ it('should set Save button label to "Save changes" when either serviceActive or canTestService (or both) is `false`', () => {
+ integrationSettingsForm.canTestService = false;
+ integrationSettingsForm.formActive = false;
+
+ integrationSettingsForm.toggleSubmitBtnLabel();
+
+ expect(integrationSettingsForm.$submitBtnLabel.text()).toEqual('Save changes');
+
+ integrationSettingsForm.formActive = true;
+
+ integrationSettingsForm.toggleSubmitBtnLabel();
+
+ expect(integrationSettingsForm.$submitBtnLabel.text()).toEqual('Save changes');
+
+ integrationSettingsForm.canTestService = true;
+ integrationSettingsForm.formActive = false;
+
+ integrationSettingsForm.toggleSubmitBtnLabel();
+
+ expect(integrationSettingsForm.$submitBtnLabel.text()).toEqual('Save changes');
+ });
+ });
+
+ describe('toggleSubmitBtnState', () => {
+ let integrationSettingsForm;
+
+ beforeEach(() => {
+ integrationSettingsForm = new IntegrationSettingsForm('.js-integration-settings-form');
+ });
+
+ it('should disable Save button and show loader animation when called with `true`', () => {
+ integrationSettingsForm.toggleSubmitBtnState(true);
+
+ expect(integrationSettingsForm.$submitBtn.is(':disabled')).toBeTruthy();
+ expect(integrationSettingsForm.$submitBtnLoader.hasClass('hidden')).toBeFalsy();
+ });
+
+ it('should enable Save button and hide loader animation when called with `false`', () => {
+ integrationSettingsForm.toggleSubmitBtnState(false);
+
+ expect(integrationSettingsForm.$submitBtn.is(':disabled')).toBeFalsy();
+ expect(integrationSettingsForm.$submitBtnLoader.hasClass('hidden')).toBeTruthy();
+ });
+ });
+
+ describe('testSettings', () => {
+ let integrationSettingsForm;
+ let formData;
+ let mock;
+
+ beforeEach(() => {
+ mock = new MockAdaptor(axios);
+
+ jest.spyOn(axios, 'put');
+
+ integrationSettingsForm = new IntegrationSettingsForm('.js-integration-settings-form');
+ // eslint-disable-next-line no-jquery/no-serialize
+ formData = integrationSettingsForm.$form.serialize();
+ });
+
+ afterEach(() => {
+ mock.restore();
+ });
+
+ it('should make an ajax request with provided `formData`', () => {
+ return integrationSettingsForm.testSettings(formData).then(() => {
+ expect(axios.put).toHaveBeenCalledWith(integrationSettingsForm.testEndPoint, formData);
+ });
+ });
+
+ it('should show error Flash with `Save anyway` action if ajax request responds with error in test', () => {
+ const errorMessage = 'Test failed.';
+ mock.onPut(integrationSettingsForm.testEndPoint).reply(200, {
+ error: true,
+ message: errorMessage,
+ service_response: 'some error',
+ test_failed: true,
+ });
+
+ return integrationSettingsForm.testSettings(formData).then(() => {
+ const $flashContainer = $('.flash-container');
+
+ expect(
+ $flashContainer
+ .find('.flash-text')
+ .text()
+ .trim(),
+ ).toEqual('Test failed. some error');
+
+ expect($flashContainer.find('.flash-action')).toBeDefined();
+ expect(
+ $flashContainer
+ .find('.flash-action')
+ .text()
+ .trim(),
+ ).toEqual('Save anyway');
+ });
+ });
+
+ it('should not show error Flash with `Save anyway` action if ajax request responds with error in validation', () => {
+ const errorMessage = 'Validations failed.';
+ mock.onPut(integrationSettingsForm.testEndPoint).reply(200, {
+ error: true,
+ message: errorMessage,
+ service_response: 'some error',
+ test_failed: false,
+ });
+
+ return integrationSettingsForm.testSettings(formData).then(() => {
+ const $flashContainer = $('.flash-container');
+
+ expect(
+ $flashContainer
+ .find('.flash-text')
+ .text()
+ .trim(),
+ ).toEqual('Validations failed. some error');
+
+ expect($flashContainer.find('.flash-action')).toBeDefined();
+ expect(
+ $flashContainer
+ .find('.flash-action')
+ .text()
+ .trim(),
+ ).toEqual('');
+ });
+ });
+
+ it('should submit form if ajax request responds without any error in test', () => {
+ jest.spyOn(integrationSettingsForm.$form, 'submit').mockImplementation(() => {});
+
+ mock.onPut(integrationSettingsForm.testEndPoint).reply(200, {
+ error: false,
+ });
+
+ return integrationSettingsForm.testSettings(formData).then(() => {
+ expect(integrationSettingsForm.$form.submit).toHaveBeenCalled();
+ });
+ });
+
+ it('should submit form when clicked on `Save anyway` action of error Flash', () => {
+ jest.spyOn(integrationSettingsForm.$form, 'submit').mockImplementation(() => {});
+
+ const errorMessage = 'Test failed.';
+ mock.onPut(integrationSettingsForm.testEndPoint).reply(200, {
+ error: true,
+ message: errorMessage,
+ test_failed: true,
+ });
+
+ return integrationSettingsForm
+ .testSettings(formData)
+ .then(() => {
+ const $flashAction = $('.flash-container .flash-action');
+
+ expect($flashAction).toBeDefined();
+
+ $flashAction.get(0).click();
+ })
+ .then(() => {
+ expect(integrationSettingsForm.$form.submit).toHaveBeenCalled();
+ });
+ });
+
+ it('should show error Flash if ajax request failed', () => {
+ const errorMessage = 'Something went wrong on our end.';
+
+ mock.onPut(integrationSettingsForm.testEndPoint).networkError();
+
+ return integrationSettingsForm.testSettings(formData).then(() => {
+ expect(
+ $('.flash-container .flash-text')
+ .text()
+ .trim(),
+ ).toEqual(errorMessage);
+ });
+ });
+
+ it('should always call `toggleSubmitBtnState` with `false` once request is completed', () => {
+ mock.onPut(integrationSettingsForm.testEndPoint).networkError();
+
+ jest.spyOn(integrationSettingsForm, 'toggleSubmitBtnState').mockImplementation(() => {});
+
+ return integrationSettingsForm.testSettings(formData).then(() => {
+ expect(integrationSettingsForm.toggleSubmitBtnState).toHaveBeenCalledWith(false);
+ });
+ });
+ });
+});
diff --git a/spec/frontend/issuable_spec.js b/spec/frontend/issuable_spec.js
new file mode 100644
index 00000000000..63c1fda2fb4
--- /dev/null
+++ b/spec/frontend/issuable_spec.js
@@ -0,0 +1,64 @@
+import $ from 'jquery';
+import MockAdaptor from 'axios-mock-adapter';
+import axios from '~/lib/utils/axios_utils';
+import IssuableIndex from '~/issuable_index';
+import issuableInitBulkUpdateSidebar from '~/issuable_init_bulk_update_sidebar';
+
+describe('Issuable', () => {
+ describe('initBulkUpdate', () => {
+ it('should not set bulkUpdateSidebar', () => {
+ new IssuableIndex('issue_'); // eslint-disable-line no-new
+
+ expect(issuableInitBulkUpdateSidebar.bulkUpdateSidebar).toBeNull();
+ });
+
+ it('should set bulkUpdateSidebar', () => {
+ const element = document.createElement('div');
+ element.classList.add('issues-bulk-update');
+ document.body.appendChild(element);
+
+ new IssuableIndex('issue_'); // eslint-disable-line no-new
+
+ expect(issuableInitBulkUpdateSidebar.bulkUpdateSidebar).toBeDefined();
+ });
+ });
+
+ describe('resetIncomingEmailToken', () => {
+ let mock;
+
+ beforeEach(() => {
+ const element = document.createElement('a');
+ element.classList.add('incoming-email-token-reset');
+ element.setAttribute('href', 'foo');
+ document.body.appendChild(element);
+
+ const input = document.createElement('input');
+ input.setAttribute('id', 'issuable_email');
+ document.body.appendChild(input);
+
+ new IssuableIndex('issue_'); // eslint-disable-line no-new
+
+ mock = new MockAdaptor(axios);
+
+ mock.onPut('foo').reply(200, {
+ new_address: 'testing123',
+ });
+ });
+
+ afterEach(() => {
+ mock.restore();
+ });
+
+ it('should send request to reset email token', done => {
+ jest.spyOn(axios, 'put');
+ document.querySelector('.incoming-email-token-reset').click();
+
+ setImmediate(() => {
+ expect(axios.put).toHaveBeenCalledWith('foo');
+ expect($('#issuable_email').val()).toBe('testing123');
+
+ done();
+ });
+ });
+ });
+});
diff --git a/spec/frontend/issuables_list/components/issuable_list_root_app_spec.js b/spec/frontend/issuables_list/components/issuable_list_root_app_spec.js
new file mode 100644
index 00000000000..899010bdb0f
--- /dev/null
+++ b/spec/frontend/issuables_list/components/issuable_list_root_app_spec.js
@@ -0,0 +1,121 @@
+import { GlAlert, GlLabel } from '@gitlab/ui';
+import { shallowMount } from '@vue/test-utils';
+import Vue from 'vue';
+import IssuableListRootApp from '~/issuables_list/components/issuable_list_root_app.vue';
+
+describe('IssuableListRootApp', () => {
+ const issuesPath = 'gitlab-org/gitlab-test/-/issues';
+ const label = {
+ color: '#333',
+ title: 'jira-import::MTG-3',
+ };
+ let wrapper;
+
+ const findAlert = () => wrapper.find(GlAlert);
+
+ const findAlertLabel = () => wrapper.find(GlAlert).find(GlLabel);
+
+ const mountComponent = ({
+ isFinishedAlertShowing = false,
+ isInProgressAlertShowing = false,
+ isInProgress = false,
+ isFinished = false,
+ } = {}) =>
+ shallowMount(IssuableListRootApp, {
+ propsData: {
+ canEdit: true,
+ isJiraConfigured: true,
+ issuesPath,
+ projectPath: 'gitlab-org/gitlab-test',
+ },
+ data() {
+ return {
+ isFinishedAlertShowing,
+ isInProgressAlertShowing,
+ jiraImport: {
+ isInProgress,
+ isFinished,
+ label,
+ },
+ };
+ },
+ });
+
+ afterEach(() => {
+ wrapper.destroy();
+ wrapper = null;
+ });
+
+ describe('when Jira import is not in progress', () => {
+ it('does not show an alert', () => {
+ wrapper = mountComponent();
+
+ expect(wrapper.contains(GlAlert)).toBe(false);
+ });
+ });
+
+ describe('when Jira import is in progress', () => {
+ it('shows an alert that tells the user a Jira import is in progress', () => {
+ wrapper = mountComponent({
+ isInProgressAlertShowing: true,
+ isInProgress: true,
+ });
+
+ expect(findAlert().text()).toBe(
+ 'Import in progress. Refresh page to see newly added issues.',
+ );
+ });
+ });
+
+ describe('when Jira import has finished', () => {
+ beforeEach(() => {
+ wrapper = mountComponent({
+ isFinishedAlertShowing: true,
+ isFinished: true,
+ });
+ });
+
+ describe('shows an alert', () => {
+ it('tells the user the Jira import has finished', () => {
+ expect(findAlert().text()).toBe('Issues successfully imported with the label');
+ });
+
+ it('contains the label title associated with the Jira import', () => {
+ const alertLabelTitle = findAlertLabel().props('title');
+
+ expect(alertLabelTitle).toBe(label.title);
+ });
+
+ it('contains the correct label color', () => {
+ const alertLabelTitle = findAlertLabel().props('backgroundColor');
+
+ expect(alertLabelTitle).toBe(label.color);
+ });
+
+ it('contains a link within the label', () => {
+ const alertLabelTarget = findAlertLabel().props('target');
+
+ expect(alertLabelTarget).toBe(
+ `${issuesPath}?label_name[]=${encodeURIComponent(label.title)}`,
+ );
+ });
+ });
+ });
+
+ describe('alert message', () => {
+ it('is hidden when dismissed', () => {
+ wrapper = mountComponent({
+ isInProgressAlertShowing: true,
+ isInProgress: true,
+ });
+
+ expect(wrapper.contains(GlAlert)).toBe(true);
+
+ findAlert().vm.$emit('dismiss');
+
+ return Vue.nextTick(() => {
+ expect(wrapper.contains(GlAlert)).toBe(false);
+ });
+ });
+ });
+});
diff --git a/spec/frontend/issue_show/components/app_spec.js b/spec/frontend/issue_show/components/app_spec.js
new file mode 100644
index 00000000000..a59d6d35ded
--- /dev/null
+++ b/spec/frontend/issue_show/components/app_spec.js
@@ -0,0 +1,497 @@
+import Vue from 'vue';
+import MockAdapter from 'axios-mock-adapter';
+import { TEST_HOST } from 'helpers/test_constants';
+import axios from '~/lib/utils/axios_utils';
+import { visitUrl } from '~/lib/utils/url_utility';
+import '~/behaviors/markdown/render_gfm';
+import issuableApp from '~/issue_show/components/app.vue';
+import eventHub from '~/issue_show/event_hub';
+import { initialRequest, secondRequest } from '../mock_data';
+
+function formatText(text) {
+ return text.trim().replace(/\s\s+/g, ' ');
+}
+
+jest.mock('~/lib/utils/url_utility');
+jest.mock('~/issue_show/event_hub');
+
+const REALTIME_REQUEST_STACK = [initialRequest, secondRequest];
+
+describe('Issuable output', () => {
+ let mock;
+ let realtimeRequestCount = 0;
+ let vm;
+
+ beforeEach(() => {
+ setFixtures(`
+ <div>
+ <title>Title</title>
+ <div class="detail-page-description content-block">
+ <details open>
+ <summary>One</summary>
+ </details>
+ <details>
+ <summary>Two</summary>
+ </details>
+ </div>
+ <div class="flash-container"></div>
+ <span id="task_status"></span>
+ </div>
+ `);
+
+ const IssuableDescriptionComponent = Vue.extend(issuableApp);
+
+ mock = new MockAdapter(axios);
+ mock
+ .onGet('/gitlab-org/gitlab-shell/-/issues/9/realtime_changes/realtime_changes')
+ .reply(() => {
+ const res = Promise.resolve([200, REALTIME_REQUEST_STACK[realtimeRequestCount]]);
+ realtimeRequestCount += 1;
+ return res;
+ });
+
+ vm = new IssuableDescriptionComponent({
+ propsData: {
+ canUpdate: true,
+ canDestroy: true,
+ endpoint: '/gitlab-org/gitlab-shell/-/issues/9/realtime_changes',
+ updateEndpoint: TEST_HOST,
+ issuableRef: '#1',
+ initialTitleHtml: '',
+ initialTitleText: '',
+ initialDescriptionHtml: 'test',
+ initialDescriptionText: 'test',
+ lockVersion: 1,
+ markdownPreviewPath: '/',
+ markdownDocsPath: '/',
+ projectNamespace: '/',
+ projectPath: '/',
+ issuableTemplateNamesPath: '/issuable-templates-path',
+ },
+ }).$mount();
+ });
+
+ afterEach(() => {
+ mock.restore();
+ realtimeRequestCount = 0;
+
+ vm.poll.stop();
+ vm.$destroy();
+ });
+
+ it('should render a title/description/edited and update title/description/edited on update', () => {
+ let editedText;
+ return axios
+ .waitForAll()
+ .then(() => {
+ editedText = vm.$el.querySelector('.edited-text');
+ })
+ .then(() => {
+ expect(document.querySelector('title').innerText).toContain('this is a title (#1)');
+ expect(vm.$el.querySelector('.title').innerHTML).toContain('<p>this is a title</p>');
+ expect(vm.$el.querySelector('.md').innerHTML).toContain('<p>this is a description!</p>');
+ expect(vm.$el.querySelector('.js-task-list-field').value).toContain(
+ 'this is a description',
+ );
+
+ expect(formatText(editedText.innerText)).toMatch(/Edited[\s\S]+?by Some User/);
+ expect(editedText.querySelector('.author-link').href).toMatch(/\/some_user$/);
+ expect(editedText.querySelector('time')).toBeTruthy();
+ expect(vm.state.lock_version).toEqual(1);
+ })
+ .then(() => {
+ vm.poll.makeRequest();
+ return axios.waitForAll();
+ })
+ .then(() => {
+ expect(document.querySelector('title').innerText).toContain('2 (#1)');
+ expect(vm.$el.querySelector('.title').innerHTML).toContain('<p>2</p>');
+ expect(vm.$el.querySelector('.md').innerHTML).toContain('<p>42</p>');
+ expect(vm.$el.querySelector('.js-task-list-field').value).toContain('42');
+ expect(vm.$el.querySelector('.edited-text')).toBeTruthy();
+ expect(formatText(vm.$el.querySelector('.edited-text').innerText)).toMatch(
+ /Edited[\s\S]+?by Other User/,
+ );
+
+ expect(editedText.querySelector('.author-link').href).toMatch(/\/other_user$/);
+ expect(editedText.querySelector('time')).toBeTruthy();
+ expect(vm.state.lock_version).toEqual(2);
+ });
+ });
+
+ it('shows actions if permissions are correct', () => {
+ vm.showForm = true;
+
+ return vm.$nextTick().then(() => {
+ expect(vm.$el.querySelector('.btn')).not.toBeNull();
+ });
+ });
+
+ it('does not show actions if permissions are incorrect', () => {
+ vm.showForm = true;
+ vm.canUpdate = false;
+
+ return vm.$nextTick().then(() => {
+ expect(vm.$el.querySelector('.btn')).toBeNull();
+ });
+ });
+
+ it('does not update formState if form is already open', () => {
+ vm.updateAndShowForm();
+
+ vm.state.titleText = 'testing 123';
+
+ vm.updateAndShowForm();
+
+ return vm.$nextTick().then(() => {
+ expect(vm.store.formState.title).not.toBe('testing 123');
+ });
+ });
+
+ it('opens reCAPTCHA modal if update rejected as spam', () => {
+ let modal;
+
+ jest.spyOn(vm.service, 'updateIssuable').mockResolvedValue({
+ data: {
+ recaptcha_html: '<div class="g-recaptcha">recaptcha_html</div>',
+ },
+ });
+
+ vm.canUpdate = true;
+ vm.showForm = true;
+
+ return vm
+ .$nextTick()
+ .then(() => {
+ vm.$refs.recaptchaModal.scriptSrc = '//scriptsrc';
+ return vm.updateIssuable();
+ })
+ .then(() => {
+ modal = vm.$el.querySelector('.js-recaptcha-modal');
+
+ expect(modal.style.display).not.toEqual('none');
+ expect(modal.querySelector('.g-recaptcha').textContent).toEqual('recaptcha_html');
+ expect(document.body.querySelector('.js-recaptcha-script').src).toMatch('//scriptsrc');
+ })
+ .then(() => {
+ modal.querySelector('.close').click();
+ return vm.$nextTick();
+ })
+ .then(() => {
+ expect(modal.style.display).toEqual('none');
+ expect(document.body.querySelector('.js-recaptcha-script')).toBeNull();
+ });
+ });
+
+ describe('updateIssuable', () => {
+ it('fetches new data after update', () => {
+ const updateStoreSpy = jest.spyOn(vm, 'updateStoreState');
+ const getDataSpy = jest.spyOn(vm.service, 'getData');
+ jest.spyOn(vm.service, 'updateIssuable').mockResolvedValue({
+ data: { web_url: window.location.pathname },
+ });
+
+ return vm.updateIssuable().then(() => {
+ expect(updateStoreSpy).toHaveBeenCalled();
+ expect(getDataSpy).toHaveBeenCalled();
+ });
+ });
+
+ it('correctly updates issuable data', () => {
+ const spy = jest.spyOn(vm.service, 'updateIssuable').mockResolvedValue({
+ data: { web_url: window.location.pathname },
+ });
+
+ return vm.updateIssuable().then(() => {
+ expect(spy).toHaveBeenCalledWith(vm.formState);
+ expect(eventHub.$emit).toHaveBeenCalledWith('close.form');
+ });
+ });
+
+ it('does not redirect if issue has not moved', () => {
+ jest.spyOn(vm.service, 'updateIssuable').mockResolvedValue({
+ data: {
+ web_url: window.location.pathname,
+ confidential: vm.isConfidential,
+ },
+ });
+
+ return vm.updateIssuable().then(() => {
+ expect(visitUrl).not.toHaveBeenCalled();
+ });
+ });
+
+ it('does not redirect if issue has not moved and user has switched tabs', () => {
+ jest.spyOn(vm.service, 'updateIssuable').mockResolvedValue({
+ data: {
+ web_url: '',
+ confidential: vm.isConfidential,
+ },
+ });
+
+ return vm.updateIssuable().then(() => {
+ expect(visitUrl).not.toHaveBeenCalled();
+ });
+ });
+
+ it('redirects if returned web_url has changed', () => {
+ jest.spyOn(vm.service, 'updateIssuable').mockResolvedValue({
+ data: {
+ web_url: '/testing-issue-move',
+ confidential: vm.isConfidential,
+ },
+ });
+
+ vm.updateIssuable();
+
+ return vm.updateIssuable().then(() => {
+ expect(visitUrl).toHaveBeenCalledWith('/testing-issue-move');
+ });
+ });
+
+ describe('shows dialog when issue has unsaved changed', () => {
+ it('confirms on title change', () => {
+ vm.showForm = true;
+ vm.state.titleText = 'title has changed';
+ const e = { returnValue: null };
+ vm.handleBeforeUnloadEvent(e);
+ return vm.$nextTick().then(() => {
+ expect(e.returnValue).not.toBeNull();
+ });
+ });
+
+ it('confirms on description change', () => {
+ vm.showForm = true;
+ vm.state.descriptionText = 'description has changed';
+ const e = { returnValue: null };
+ vm.handleBeforeUnloadEvent(e);
+ return vm.$nextTick().then(() => {
+ expect(e.returnValue).not.toBeNull();
+ });
+ });
+
+ it('does nothing when nothing has changed', () => {
+ const e = { returnValue: null };
+ vm.handleBeforeUnloadEvent(e);
+ return vm.$nextTick().then(() => {
+ expect(e.returnValue).toBeNull();
+ });
+ });
+ });
+
+ describe('error when updating', () => {
+ it('closes form on error', () => {
+ jest.spyOn(vm.service, 'updateIssuable').mockRejectedValue();
+ return vm.updateIssuable().then(() => {
+ expect(eventHub.$emit).not.toHaveBeenCalledWith('close.form');
+ expect(document.querySelector('.flash-container .flash-text').innerText.trim()).toBe(
+ `Error updating issue`,
+ );
+ });
+ });
+
+ it('returns the correct error message for issuableType', () => {
+ jest.spyOn(vm.service, 'updateIssuable').mockRejectedValue();
+ vm.issuableType = 'merge request';
+
+ return vm
+ .$nextTick()
+ .then(vm.updateIssuable)
+ .then(() => {
+ expect(eventHub.$emit).not.toHaveBeenCalledWith('close.form');
+ expect(document.querySelector('.flash-container .flash-text').innerText.trim()).toBe(
+ `Error updating merge request`,
+ );
+ });
+ });
+
+ it('shows error message from backend if exists', () => {
+ const msg = 'Custom error message from backend';
+ jest
+ .spyOn(vm.service, 'updateIssuable')
+ .mockRejectedValue({ response: { data: { errors: [msg] } } });
+
+ return vm.updateIssuable().then(() => {
+ expect(document.querySelector('.flash-container .flash-text').innerText.trim()).toBe(
+ `${vm.defaultErrorMessage}. ${msg}`,
+ );
+ });
+ });
+ });
+ });
+
+ describe('deleteIssuable', () => {
+ it('changes URL when deleted', () => {
+ jest.spyOn(vm.service, 'deleteIssuable').mockResolvedValue({
+ data: {
+ web_url: '/test',
+ },
+ });
+
+ return vm.deleteIssuable().then(() => {
+ expect(visitUrl).toHaveBeenCalledWith('/test');
+ });
+ });
+
+ it('stops polling when deleting', () => {
+ const spy = jest.spyOn(vm.poll, 'stop');
+ jest.spyOn(vm.service, 'deleteIssuable').mockResolvedValue({
+ data: {
+ web_url: '/test',
+ },
+ });
+
+ return vm.deleteIssuable().then(() => {
+ expect(spy).toHaveBeenCalledWith();
+ });
+ });
+
+ it('closes form on error', () => {
+ jest.spyOn(vm.service, 'deleteIssuable').mockRejectedValue();
+
+ return vm.deleteIssuable().then(() => {
+ expect(eventHub.$emit).not.toHaveBeenCalledWith('close.form');
+ expect(document.querySelector('.flash-container .flash-text').innerText.trim()).toBe(
+ 'Error deleting issue',
+ );
+ });
+ });
+ });
+
+ describe('updateAndShowForm', () => {
+ it('shows locked warning if form is open & data is different', () => {
+ return vm
+ .$nextTick()
+ .then(() => {
+ vm.updateAndShowForm();
+
+ vm.poll.makeRequest();
+
+ return new Promise(resolve => {
+ vm.$watch('formState.lockedWarningVisible', value => {
+ if (value) resolve();
+ });
+ });
+ })
+ .then(() => {
+ expect(vm.formState.lockedWarningVisible).toEqual(true);
+ expect(vm.formState.lock_version).toEqual(1);
+ expect(vm.$el.querySelector('.alert')).not.toBeNull();
+ });
+ });
+ });
+
+ describe('requestTemplatesAndShowForm', () => {
+ let formSpy;
+
+ beforeEach(() => {
+ formSpy = jest.spyOn(vm, 'updateAndShowForm');
+ });
+
+ it('shows the form if template names request is successful', () => {
+ const mockData = [{ name: 'Bug' }];
+ mock.onGet('/issuable-templates-path').reply(() => Promise.resolve([200, mockData]));
+
+ return vm.requestTemplatesAndShowForm().then(() => {
+ expect(formSpy).toHaveBeenCalledWith(mockData);
+ });
+ });
+
+ it('shows the form if template names request failed', () => {
+ mock
+ .onGet('/issuable-templates-path')
+ .reply(() => Promise.reject(new Error('something went wrong')));
+
+ return vm.requestTemplatesAndShowForm().then(() => {
+ expect(document.querySelector('.flash-container .flash-text').textContent).toContain(
+ 'Error updating issue',
+ );
+
+ expect(formSpy).toHaveBeenCalledWith();
+ });
+ });
+ });
+
+ describe('show inline edit button', () => {
+ it('should not render by default', () => {
+ expect(vm.$el.querySelector('.title-container .note-action-button')).toBeDefined();
+ });
+
+ it('should render if showInlineEditButton', () => {
+ vm.showInlineEditButton = true;
+
+ expect(vm.$el.querySelector('.title-container .note-action-button')).toBeDefined();
+ });
+ });
+
+ describe('updateStoreState', () => {
+ it('should make a request and update the state of the store', () => {
+ const data = { foo: 1 };
+ const getDataSpy = jest.spyOn(vm.service, 'getData').mockResolvedValue({ data });
+ const updateStateSpy = jest.spyOn(vm.store, 'updateState').mockImplementation(jest.fn);
+
+ return vm.updateStoreState().then(() => {
+ expect(getDataSpy).toHaveBeenCalled();
+ expect(updateStateSpy).toHaveBeenCalledWith(data);
+ });
+ });
+
+ it('should show error message if store update fails', () => {
+ jest.spyOn(vm.service, 'getData').mockRejectedValue();
+ vm.issuableType = 'merge request';
+
+ return vm.updateStoreState().then(() => {
+ expect(document.querySelector('.flash-container .flash-text').innerText.trim()).toBe(
+ `Error updating ${vm.issuableType}`,
+ );
+ });
+ });
+ });
+
+ describe('issueChanged', () => {
+ beforeEach(() => {
+ vm.store.formState.title = '';
+ vm.store.formState.description = '';
+ vm.initialDescriptionText = '';
+ vm.initialTitleText = '';
+ });
+
+ it('returns true when title is changed', () => {
+ vm.store.formState.title = 'RandomText';
+
+ expect(vm.issueChanged).toBe(true);
+ });
+
+ it('returns false when title is empty null', () => {
+ vm.store.formState.title = null;
+
+ expect(vm.issueChanged).toBe(false);
+ });
+
+ it('returns false when `initialTitleText` is null and `formState.title` is empty string', () => {
+ vm.store.formState.title = '';
+ vm.initialTitleText = null;
+
+ expect(vm.issueChanged).toBe(false);
+ });
+
+ it('returns true when description is changed', () => {
+ vm.store.formState.description = 'RandomText';
+
+ expect(vm.issueChanged).toBe(true);
+ });
+
+ it('returns false when description is empty null', () => {
+ vm.store.formState.title = null;
+
+ expect(vm.issueChanged).toBe(false);
+ });
+
+ it('returns false when `initialDescriptionText` is null and `formState.description` is empty string', () => {
+ vm.store.formState.description = '';
+ vm.initialDescriptionText = null;
+
+ expect(vm.issueChanged).toBe(false);
+ });
+ });
+});
diff --git a/spec/frontend/issue_show/components/description_spec.js b/spec/frontend/issue_show/components/description_spec.js
new file mode 100644
index 00000000000..0053475dd13
--- /dev/null
+++ b/spec/frontend/issue_show/components/description_spec.js
@@ -0,0 +1,188 @@
+import $ from 'jquery';
+import Vue from 'vue';
+import '~/behaviors/markdown/render_gfm';
+import mountComponent from 'helpers/vue_mount_component_helper';
+import { TEST_HOST } from 'helpers/test_constants';
+import Description from '~/issue_show/components/description.vue';
+import TaskList from '~/task_list';
+
+jest.mock('~/task_list');
+
+describe('Description component', () => {
+ let vm;
+ let DescriptionComponent;
+ const props = {
+ canUpdate: true,
+ descriptionHtml: 'test',
+ descriptionText: 'test',
+ updatedAt: new Date().toString(),
+ taskStatus: '',
+ updateUrl: TEST_HOST,
+ };
+
+ beforeEach(() => {
+ DescriptionComponent = Vue.extend(Description);
+
+ if (!document.querySelector('.issuable-meta')) {
+ const metaData = document.createElement('div');
+ metaData.classList.add('issuable-meta');
+ metaData.innerHTML =
+ '<div class="flash-container"></div><span id="task_status"></span><span id="task_status_short"></span>';
+
+ document.body.appendChild(metaData);
+ }
+
+ vm = mountComponent(DescriptionComponent, props);
+ });
+
+ afterEach(() => {
+ vm.$destroy();
+ });
+
+ afterAll(() => {
+ $('.issuable-meta .flash-container').remove();
+ });
+
+ it('animates description changes', () => {
+ vm.descriptionHtml = 'changed';
+
+ return vm
+ .$nextTick()
+ .then(() => {
+ expect(
+ vm.$el.querySelector('.md').classList.contains('issue-realtime-pre-pulse'),
+ ).toBeTruthy();
+ jest.runAllTimers();
+ return vm.$nextTick();
+ })
+ .then(() => {
+ expect(
+ vm.$el.querySelector('.md').classList.contains('issue-realtime-trigger-pulse'),
+ ).toBeTruthy();
+ });
+ });
+
+ it('opens reCAPTCHA dialog if update rejected as spam', () => {
+ let modal;
+ const recaptchaChild = vm.$children.find(
+ // eslint-disable-next-line no-underscore-dangle
+ child => child.$options._componentTag === 'recaptcha-modal',
+ );
+
+ recaptchaChild.scriptSrc = '//scriptsrc';
+
+ vm.taskListUpdateSuccess({
+ recaptcha_html: '<div class="g-recaptcha">recaptcha_html</div>',
+ });
+
+ return vm
+ .$nextTick()
+ .then(() => {
+ modal = vm.$el.querySelector('.js-recaptcha-modal');
+
+ expect(modal.style.display).not.toEqual('none');
+ expect(modal.querySelector('.g-recaptcha').textContent).toEqual('recaptcha_html');
+ expect(document.body.querySelector('.js-recaptcha-script').src).toMatch('//scriptsrc');
+ })
+ .then(() => modal.querySelector('.close').click())
+ .then(() => vm.$nextTick())
+ .then(() => {
+ expect(modal.style.display).toEqual('none');
+ expect(document.body.querySelector('.js-recaptcha-script')).toBeNull();
+ });
+ });
+
+ it('applies syntax highlighting and math when description changed', () => {
+ const vmSpy = jest.spyOn(vm, 'renderGFM');
+ const prototypeSpy = jest.spyOn($.prototype, 'renderGFM');
+ vm.descriptionHtml = 'changed';
+
+ return vm.$nextTick().then(() => {
+ expect(vm.$refs['gfm-content']).toBeDefined();
+ expect(vmSpy).toHaveBeenCalled();
+ expect(prototypeSpy).toHaveBeenCalled();
+ expect($.prototype.renderGFM).toHaveBeenCalled();
+ });
+ });
+
+ it('sets data-update-url', () => {
+ expect(vm.$el.querySelector('textarea').dataset.updateUrl).toEqual(TEST_HOST);
+ });
+
+ describe('TaskList', () => {
+ beforeEach(() => {
+ vm.$destroy();
+ TaskList.mockClear();
+ vm = mountComponent(DescriptionComponent, { ...props, issuableType: 'issuableType' });
+ });
+
+ it('re-inits the TaskList when description changed', () => {
+ vm.descriptionHtml = 'changed';
+
+ expect(TaskList).toHaveBeenCalled();
+ });
+
+ it('does not re-init the TaskList when canUpdate is false', () => {
+ vm.canUpdate = false;
+ vm.descriptionHtml = 'changed';
+
+ expect(TaskList).toHaveBeenCalledTimes(1);
+ });
+
+ it('calls with issuableType dataType', () => {
+ vm.descriptionHtml = 'changed';
+
+ expect(TaskList).toHaveBeenCalledWith({
+ dataType: 'issuableType',
+ fieldName: 'description',
+ selector: '.detail-page-description',
+ onSuccess: expect.any(Function),
+ onError: expect.any(Function),
+ lockVersion: 0,
+ });
+ });
+ });
+
+ describe('taskStatus', () => {
+ it('adds full taskStatus', () => {
+ vm.taskStatus = '1 of 1';
+
+ return vm.$nextTick().then(() => {
+ expect(document.querySelector('.issuable-meta #task_status').textContent.trim()).toBe(
+ '1 of 1',
+ );
+ });
+ });
+
+ it('adds short taskStatus', () => {
+ vm.taskStatus = '1 of 1';
+
+ return vm.$nextTick().then(() => {
+ expect(document.querySelector('.issuable-meta #task_status_short').textContent.trim()).toBe(
+ '1/1 task',
+ );
+ });
+ });
+
+ it('clears task status text when no tasks are present', () => {
+ vm.taskStatus = '0 of 0';
+
+ return vm.$nextTick().then(() => {
+ expect(document.querySelector('.issuable-meta #task_status').textContent.trim()).toBe('');
+ });
+ });
+ });
+
+ describe('taskListUpdateError', () => {
+ it('should create flash notification and emit an event to parent', () => {
+ const msg =
+ 'Someone edited this issue at the same time you did. The description has been updated and you will need to make your changes again.';
+ const spy = jest.spyOn(vm, '$emit');
+
+ vm.taskListUpdateError();
+
+ expect(document.querySelector('.flash-container .flash-text').innerText.trim()).toBe(msg);
+ expect(spy).toHaveBeenCalledWith('taskListUpdateFailed');
+ });
+ });
+});
diff --git a/spec/javascripts/issue_show/components/edited_spec.js b/spec/frontend/issue_show/components/edited_spec.js
index a1683f060c0..a1683f060c0 100644
--- a/spec/javascripts/issue_show/components/edited_spec.js
+++ b/spec/frontend/issue_show/components/edited_spec.js
diff --git a/spec/frontend/issue_show/components/fields/description_template_spec.js b/spec/frontend/issue_show/components/fields/description_template_spec.js
new file mode 100644
index 00000000000..9ebab31f1ad
--- /dev/null
+++ b/spec/frontend/issue_show/components/fields/description_template_spec.js
@@ -0,0 +1,41 @@
+import Vue from 'vue';
+import descriptionTemplate from '~/issue_show/components/fields/description_template.vue';
+
+describe('Issue description template component', () => {
+ let vm;
+ let formState;
+
+ beforeEach(() => {
+ const Component = Vue.extend(descriptionTemplate);
+ formState = {
+ description: 'test',
+ };
+
+ vm = new Component({
+ propsData: {
+ formState,
+ issuableTemplates: [{ name: 'test' }],
+ projectPath: '/',
+ projectNamespace: '/',
+ },
+ }).$mount();
+ });
+
+ it('renders templates as JSON array in data attribute', () => {
+ expect(vm.$el.querySelector('.js-issuable-selector').getAttribute('data-data')).toBe(
+ '[{"name":"test"}]',
+ );
+ });
+
+ it('updates formState when changing template', () => {
+ vm.issuableTemplate.editor.setValue('test new template');
+
+ expect(formState.description).toBe('test new template');
+ });
+
+ it('returns formState description with editor getValue', () => {
+ formState.description = 'testing new template';
+
+ expect(vm.issuableTemplate.editor.getValue()).toBe('testing new template');
+ });
+});
diff --git a/spec/frontend/issue_show/components/form_spec.js b/spec/frontend/issue_show/components/form_spec.js
new file mode 100644
index 00000000000..b06a3a89d3b
--- /dev/null
+++ b/spec/frontend/issue_show/components/form_spec.js
@@ -0,0 +1,99 @@
+import Vue from 'vue';
+import mountComponent from 'helpers/vue_mount_component_helper';
+import formComponent from '~/issue_show/components/form.vue';
+import Autosave from '~/autosave';
+import eventHub from '~/issue_show/event_hub';
+
+jest.mock('~/autosave');
+
+describe('Inline edit form component', () => {
+ let vm;
+ const defaultProps = {
+ canDestroy: true,
+ formState: {
+ title: 'b',
+ description: 'a',
+ lockedWarningVisible: false,
+ },
+ issuableType: 'issue',
+ markdownPreviewPath: '/',
+ markdownDocsPath: '/',
+ projectPath: '/',
+ projectNamespace: '/',
+ };
+
+ afterEach(() => {
+ vm.$destroy();
+ });
+
+ const createComponent = props => {
+ const Component = Vue.extend(formComponent);
+
+ vm = mountComponent(Component, {
+ ...defaultProps,
+ ...props,
+ });
+ };
+
+ it('does not render template selector if no templates exist', () => {
+ createComponent();
+
+ expect(vm.$el.querySelector('.js-issuable-selector-wrap')).toBeNull();
+ });
+
+ it('renders template selector when templates exists', () => {
+ createComponent({ issuableTemplates: ['test'] });
+
+ expect(vm.$el.querySelector('.js-issuable-selector-wrap')).not.toBeNull();
+ });
+
+ it('hides locked warning by default', () => {
+ createComponent();
+
+ expect(vm.$el.querySelector('.alert')).toBeNull();
+ });
+
+ it('shows locked warning if formState is different', () => {
+ createComponent({ formState: { ...defaultProps.formState, lockedWarningVisible: true } });
+
+ expect(vm.$el.querySelector('.alert')).not.toBeNull();
+ });
+
+ it('hides locked warning when currently saving', () => {
+ createComponent({
+ formState: { ...defaultProps.formState, updateLoading: true, lockedWarningVisible: true },
+ });
+
+ expect(vm.$el.querySelector('.alert')).toBeNull();
+ });
+
+ describe('autosave', () => {
+ let spy;
+
+ beforeEach(() => {
+ spy = jest.spyOn(Autosave.prototype, 'reset');
+ });
+
+ it('initialized Autosave on mount', () => {
+ createComponent();
+
+ expect(Autosave).toHaveBeenCalledTimes(2);
+ });
+
+ it('calls reset on autosave when eventHub emits appropriate events', () => {
+ createComponent();
+
+ eventHub.$emit('close.form');
+
+ expect(spy).toHaveBeenCalledTimes(2);
+
+ eventHub.$emit('delete.issuable');
+
+ expect(spy).toHaveBeenCalledTimes(4);
+
+ eventHub.$emit('update.issuable');
+
+ expect(spy).toHaveBeenCalledTimes(6);
+ });
+ });
+});
diff --git a/spec/frontend/issue_show/components/title_spec.js b/spec/frontend/issue_show/components/title_spec.js
new file mode 100644
index 00000000000..c274048fdd5
--- /dev/null
+++ b/spec/frontend/issue_show/components/title_spec.js
@@ -0,0 +1,95 @@
+import Vue from 'vue';
+import Store from '~/issue_show/stores';
+import titleComponent from '~/issue_show/components/title.vue';
+import eventHub from '~/issue_show/event_hub';
+
+describe('Title component', () => {
+ let vm;
+ beforeEach(() => {
+ setFixtures(`<title />`);
+
+ const Component = Vue.extend(titleComponent);
+ const store = new Store({
+ titleHtml: '',
+ descriptionHtml: '',
+ issuableRef: '',
+ });
+ vm = new Component({
+ propsData: {
+ issuableRef: '#1',
+ titleHtml: 'Testing <img />',
+ titleText: 'Testing',
+ showForm: false,
+ formState: store.formState,
+ },
+ }).$mount();
+ });
+
+ it('renders title HTML', () => {
+ expect(vm.$el.querySelector('.title').innerHTML.trim()).toBe('Testing <img>');
+ });
+
+ it('updates page title when changing titleHtml', () => {
+ const spy = jest.spyOn(vm, 'setPageTitle');
+ vm.titleHtml = 'test';
+
+ return vm.$nextTick().then(() => {
+ expect(spy).toHaveBeenCalled();
+ });
+ });
+
+ it('animates title changes', () => {
+ vm.titleHtml = 'test';
+ return vm
+ .$nextTick()
+ .then(() => {
+ expect(vm.$el.querySelector('.title').classList).toContain('issue-realtime-pre-pulse');
+ jest.runAllTimers();
+ return vm.$nextTick();
+ })
+ .then(() => {
+ expect(vm.$el.querySelector('.title').classList).toContain('issue-realtime-trigger-pulse');
+ });
+ });
+
+ it('updates page title after changing title', () => {
+ vm.titleHtml = 'changed';
+ vm.titleText = 'changed';
+
+ return vm.$nextTick().then(() => {
+ expect(document.querySelector('title').textContent.trim()).toContain('changed');
+ });
+ });
+
+ describe('inline edit button', () => {
+ it('should not show by default', () => {
+ expect(vm.$el.querySelector('.btn-edit')).toBeNull();
+ });
+
+ it('should not show if canUpdate is false', () => {
+ vm.showInlineEditButton = true;
+ vm.canUpdate = false;
+
+ expect(vm.$el.querySelector('.btn-edit')).toBeNull();
+ });
+
+ it('should show if showInlineEditButton and canUpdate', () => {
+ vm.showInlineEditButton = true;
+ vm.canUpdate = true;
+
+ expect(vm.$el.querySelector('.btn-edit')).toBeDefined();
+ });
+
+ it('should trigger open.form event when clicked', () => {
+ jest.spyOn(eventHub, '$emit').mockImplementation(() => {});
+ vm.showInlineEditButton = true;
+ vm.canUpdate = true;
+
+ Vue.nextTick(() => {
+ vm.$el.querySelector('.btn-edit').click();
+
+ expect(eventHub.$emit).toHaveBeenCalledWith('open.form');
+ });
+ });
+ });
+});
diff --git a/spec/frontend/jira_import/components/jira_import_app_spec.js b/spec/frontend/jira_import/components/jira_import_app_spec.js
index ce32559d5c9..0040e71c192 100644
--- a/spec/frontend/jira_import/components/jira_import_app_spec.js
+++ b/spec/frontend/jira_import/components/jira_import_app_spec.js
@@ -1,5 +1,5 @@
import { GlAlert, GlLoadingIcon } from '@gitlab/ui';
-import { shallowMount } from '@vue/test-utils';
+import { mount, shallowMount } from '@vue/test-utils';
import Vue from 'vue';
import JiraImportApp from '~/jira_import/components/jira_import_app.vue';
import JiraImportForm from '~/jira_import/components/jira_import_form.vue';
@@ -11,12 +11,16 @@ import { IMPORT_STATE } from '~/jira_import/utils';
const mountComponent = ({
isJiraConfigured = true,
errorMessage = '',
- showAlert = true,
+ selectedProject = 'MTG',
+ showAlert = false,
status = IMPORT_STATE.NONE,
loading = false,
mutate = jest.fn(() => Promise.resolve()),
-} = {}) =>
- shallowMount(JiraImportApp, {
+ mountType,
+} = {}) => {
+ const mountFunction = mountType === 'mount' ? mount : shallowMount;
+
+ return mountFunction(JiraImportApp, {
propsData: {
isJiraConfigured,
inProgressIllustration: 'in-progress-illustration.svg',
@@ -26,6 +30,7 @@ const mountComponent = ({
['My Second Jira Project', 'MSJP'],
['Migrate to GitLab', 'MTG'],
],
+ jiraIntegrationPath: 'gitlab-org/gitlab-test/-/services/jira/edit',
projectPath: 'gitlab-org/gitlab-test',
setupIllustration: 'setup-illustration.svg',
},
@@ -33,15 +38,32 @@ const mountComponent = ({
return {
errorMessage,
showAlert,
+ selectedProject,
jiraImportDetails: {
status,
- import: {
- jiraProjectKey: 'MTG',
- scheduledAt: '2020-04-08T12:17:25+00:00',
- scheduledBy: {
- name: 'Jane Doe',
+ imports: [
+ {
+ jiraProjectKey: 'MTG',
+ scheduledAt: '2020-04-08T10:11:12+00:00',
+ scheduledBy: {
+ name: 'John Doe',
+ },
},
- },
+ {
+ jiraProjectKey: 'MSJP',
+ scheduledAt: '2020-04-09T13:14:15+00:00',
+ scheduledBy: {
+ name: 'Jimmy Doe',
+ },
+ },
+ {
+ jiraProjectKey: 'MTG',
+ scheduledAt: '2020-04-09T16:17:18+00:00',
+ scheduledBy: {
+ name: 'Jane Doe',
+ },
+ },
+ ],
},
};
},
@@ -52,6 +74,7 @@ const mountComponent = ({
},
},
});
+};
describe('JiraImportApp', () => {
let wrapper;
@@ -159,6 +182,64 @@ describe('JiraImportApp', () => {
});
});
+ describe('import in progress screen', () => {
+ beforeEach(() => {
+ wrapper = mountComponent({ status: IMPORT_STATE.SCHEDULED });
+ });
+
+ it('shows the illustration', () => {
+ expect(getProgressComponent().props('illustration')).toBe('in-progress-illustration.svg');
+ });
+
+ it('shows the name of the most recent import initiator', () => {
+ expect(getProgressComponent().props('importInitiator')).toBe('Jane Doe');
+ });
+
+ it('shows the name of the most recent imported project', () => {
+ expect(getProgressComponent().props('importProject')).toBe('MTG');
+ });
+
+ it('shows the time of the most recent import', () => {
+ expect(getProgressComponent().props('importTime')).toBe('2020-04-09T16:17:18+00:00');
+ });
+
+ it('has the path to the issues page', () => {
+ expect(getProgressComponent().props('issuesPath')).toBe('gitlab-org/gitlab-test/-/issues');
+ });
+ });
+
+ describe('jira import form screen', () => {
+ describe('when selected project has been imported before', () => {
+ it('shows jira-import::MTG-3 label since project MTG has been imported 2 time before', () => {
+ wrapper = mountComponent();
+
+ expect(getFormComponent().props('importLabel')).toBe('jira-import::MTG-3');
+ });
+
+ it('shows warning alert to explain project MTG has been imported 2 times before', () => {
+ wrapper = mountComponent({ mountType: 'mount' });
+
+ expect(getAlert().text()).toBe(
+ 'You have imported from this project 2 times before. Each new import will create duplicate issues.',
+ );
+ });
+ });
+
+ describe('when selected project has not been imported before', () => {
+ beforeEach(() => {
+ wrapper = mountComponent({ selectedProject: 'MJP' });
+ });
+
+ it('shows jira-import::MJP-1 label since project MJP has not been imported before', () => {
+ expect(getFormComponent().props('importLabel')).toBe('jira-import::MJP-1');
+ });
+
+ it('does not show warning alert since project MJP has not been imported before', () => {
+ expect(getAlert().exists()).toBe(false);
+ });
+ });
+ });
+
describe('initiating a Jira import', () => {
it('calls the mutation with the expected arguments', () => {
const mutate = jest.fn(() => Promise.resolve());
@@ -200,6 +281,7 @@ describe('JiraImportApp', () => {
wrapper = mountComponent({
errorMessage: 'There was an error importing the Jira project.',
showAlert: true,
+ selectedProject: null,
});
expect(getAlert().exists()).toBe(true);
diff --git a/spec/frontend/jira_import/components/jira_import_form_spec.js b/spec/frontend/jira_import/components/jira_import_form_spec.js
index 0987eb11693..dea94e7bf1f 100644
--- a/spec/frontend/jira_import/components/jira_import_form_spec.js
+++ b/spec/frontend/jira_import/components/jira_import_form_spec.js
@@ -2,11 +2,15 @@ import { GlAvatar, GlButton, GlFormSelect, GlLabel } from '@gitlab/ui';
import { mount, shallowMount } from '@vue/test-utils';
import JiraImportForm from '~/jira_import/components/jira_import_form.vue';
+const importLabel = 'jira-import::MTG-1';
+const value = 'MTG';
+
const mountComponent = ({ mountType } = {}) => {
const mountFunction = mountType === 'mount' ? mount : shallowMount;
return mountFunction(JiraImportForm, {
propsData: {
+ importLabel,
issuesPath: 'gitlab-org/gitlab-test/-/issues',
jiraProjects: [
{
@@ -22,6 +26,7 @@ const mountComponent = ({ mountType } = {}) => {
value: 'MTG',
},
],
+ value,
},
});
};
@@ -29,6 +34,8 @@ const mountComponent = ({ mountType } = {}) => {
describe('JiraImportForm', () => {
let wrapper;
+ const getSelectDropdown = () => wrapper.find(GlFormSelect);
+
const getCancelButton = () => wrapper.findAll(GlButton).at(1);
afterEach(() => {
@@ -40,7 +47,7 @@ describe('JiraImportForm', () => {
it('is shown', () => {
wrapper = mountComponent();
- expect(wrapper.find(GlFormSelect).exists()).toBe(true);
+ expect(wrapper.contains(GlFormSelect)).toBe(true);
});
it('contains a list of Jira projects to select from', () => {
@@ -48,8 +55,7 @@ describe('JiraImportForm', () => {
const optionItems = ['My Jira Project', 'My Second Jira Project', 'Migrate to GitLab'];
- wrapper
- .find(GlFormSelect)
+ getSelectDropdown()
.findAll('option')
.wrappers.forEach((optionEl, index) => {
expect(optionEl.text()).toBe(optionItems[index]);
@@ -63,7 +69,7 @@ describe('JiraImportForm', () => {
});
it('shows a label which will be applied to imported Jira projects', () => {
- expect(wrapper.find(GlLabel).attributes('title')).toBe('jira-import::KEY-1');
+ expect(wrapper.find(GlLabel).props('title')).toBe(importLabel);
});
it('shows information to the user', () => {
@@ -77,7 +83,7 @@ describe('JiraImportForm', () => {
});
it('shows an avatar for the Reporter', () => {
- expect(wrapper.find(GlAvatar).exists()).toBe(true);
+ expect(wrapper.contains(GlAvatar)).toBe(true);
});
it('shows jira.issue.description.content for the Description', () => {
@@ -111,16 +117,19 @@ describe('JiraImportForm', () => {
});
});
- it('emits an "initiateJiraImport" event with the selected dropdown value when submitted', () => {
- const selectedOption = 'MTG';
+ it('emits an "input" event when the input select value changes', () => {
+ wrapper = mountComponent({ mountType: 'mount' });
+
+ getSelectDropdown().vm.$emit('change', value);
+ expect(wrapper.emitted('input')[0]).toEqual([value]);
+ });
+
+ it('emits an "initiateJiraImport" event with the selected dropdown value when submitted', () => {
wrapper = mountComponent();
- wrapper.setData({
- selectedOption,
- });
wrapper.find('form').trigger('submit');
- expect(wrapper.emitted('initiateJiraImport')[0]).toEqual([selectedOption]);
+ expect(wrapper.emitted('initiateJiraImport')[0]).toEqual([value]);
});
});
diff --git a/spec/frontend/jira_import/components/jira_import_progress_spec.js b/spec/frontend/jira_import/components/jira_import_progress_spec.js
index 9a6fc3b5925..3ccf14554e1 100644
--- a/spec/frontend/jira_import/components/jira_import_progress_spec.js
+++ b/spec/frontend/jira_import/components/jira_import_progress_spec.js
@@ -2,10 +2,14 @@ import { GlEmptyState } from '@gitlab/ui';
import { mount, shallowMount } from '@vue/test-utils';
import JiraImportProgress from '~/jira_import/components/jira_import_progress.vue';
+const illustration = 'illustration.svg';
+const importProject = 'JIRAPROJECT';
+const issuesPath = 'gitlab-org/gitlab-test/-/issues';
+
describe('JiraImportProgress', () => {
let wrapper;
- const getGlEmptyStateAttribute = attribute => wrapper.find(GlEmptyState).attributes(attribute);
+ const getGlEmptyStateProp = attribute => wrapper.find(GlEmptyState).props(attribute);
const getParagraphText = () => wrapper.find('p').text();
@@ -13,11 +17,11 @@ describe('JiraImportProgress', () => {
const mountFunction = mountType === 'shallowMount' ? shallowMount : mount;
return mountFunction(JiraImportProgress, {
propsData: {
- illustration: 'illustration.svg',
+ illustration,
importInitiator: 'Jane Doe',
- importProject: 'JIRAPROJECT',
+ importProject,
importTime: '2020-04-08T12:17:25+00:00',
- issuesPath: 'gitlab-org/gitlab-test/-/issues',
+ issuesPath,
},
});
};
@@ -33,20 +37,21 @@ describe('JiraImportProgress', () => {
});
it('contains illustration', () => {
- expect(getGlEmptyStateAttribute('svgpath')).toBe('illustration.svg');
+ expect(getGlEmptyStateProp('svgPath')).toBe(illustration);
});
it('contains a title', () => {
const title = 'Import in progress';
- expect(getGlEmptyStateAttribute('title')).toBe(title);
+ expect(getGlEmptyStateProp('title')).toBe(title);
});
it('contains button text', () => {
- expect(getGlEmptyStateAttribute('primarybuttontext')).toBe('View issues');
+ expect(getGlEmptyStateProp('primaryButtonText')).toBe('View issues');
});
it('contains button url', () => {
- expect(getGlEmptyStateAttribute('primarybuttonlink')).toBe('gitlab-org/gitlab-test/-/issues');
+ const expected = `${issuesPath}?search=${importProject}`;
+ expect(getGlEmptyStateProp('primaryButtonLink')).toBe(expected);
});
});
diff --git a/spec/frontend/jira_import/components/jira_import_setup_spec.js b/spec/frontend/jira_import/components/jira_import_setup_spec.js
index 834c14b512e..aa94dc4f503 100644
--- a/spec/frontend/jira_import/components/jira_import_setup_spec.js
+++ b/spec/frontend/jira_import/components/jira_import_setup_spec.js
@@ -2,15 +2,19 @@ import { GlEmptyState } from '@gitlab/ui';
import { shallowMount } from '@vue/test-utils';
import JiraImportSetup from '~/jira_import/components/jira_import_setup.vue';
+const illustration = 'illustration.svg';
+const jiraIntegrationPath = 'gitlab-org/gitlab-test/-/services/jira/edit';
+
describe('JiraImportSetup', () => {
let wrapper;
- const getGlEmptyStateAttribute = attribute => wrapper.find(GlEmptyState).attributes(attribute);
+ const getGlEmptyStateProp = attribute => wrapper.find(GlEmptyState).props(attribute);
beforeEach(() => {
wrapper = shallowMount(JiraImportSetup, {
propsData: {
- illustration: 'illustration.svg',
+ illustration,
+ jiraIntegrationPath,
},
});
});
@@ -21,15 +25,19 @@ describe('JiraImportSetup', () => {
});
it('contains illustration', () => {
- expect(getGlEmptyStateAttribute('svgpath')).toBe('illustration.svg');
+ expect(getGlEmptyStateProp('svgPath')).toBe(illustration);
});
it('contains a description', () => {
const description = 'You will first need to set up Jira Integration to use this feature.';
- expect(getGlEmptyStateAttribute('description')).toBe(description);
+ expect(getGlEmptyStateProp('description')).toBe(description);
});
it('contains button text', () => {
- expect(getGlEmptyStateAttribute('primarybuttontext')).toBe('Set up Jira Integration');
+ expect(getGlEmptyStateProp('primaryButtonText')).toBe('Set up Jira Integration');
+ });
+
+ it('contains button link', () => {
+ expect(getGlEmptyStateProp('primaryButtonLink')).toBe(jiraIntegrationPath);
});
});
diff --git a/spec/frontend/jira_import/utils_spec.js b/spec/frontend/jira_import/utils_spec.js
index a14db104229..0b1edd6550a 100644
--- a/spec/frontend/jira_import/utils_spec.js
+++ b/spec/frontend/jira_import/utils_spec.js
@@ -1,27 +1,62 @@
-import { IMPORT_STATE, isInProgress } from '~/jira_import/utils';
+import {
+ calculateJiraImportLabel,
+ IMPORT_STATE,
+ isFinished,
+ isInProgress,
+} from '~/jira_import/utils';
describe('isInProgress', () => {
- it('returns true when state is IMPORT_STATE.SCHEDULED', () => {
- expect(isInProgress(IMPORT_STATE.SCHEDULED)).toBe(true);
+ it.each`
+ state | result
+ ${IMPORT_STATE.SCHEDULED} | ${true}
+ ${IMPORT_STATE.STARTED} | ${true}
+ ${IMPORT_STATE.FAILED} | ${false}
+ ${IMPORT_STATE.FINISHED} | ${false}
+ ${IMPORT_STATE.NONE} | ${false}
+ ${undefined} | ${false}
+ `('returns $result when state is $state', ({ state, result }) => {
+ expect(isInProgress(state)).toBe(result);
});
+});
- it('returns true when state is IMPORT_STATE.STARTED', () => {
- expect(isInProgress(IMPORT_STATE.STARTED)).toBe(true);
+describe('isFinished', () => {
+ it.each`
+ state | result
+ ${IMPORT_STATE.SCHEDULED} | ${false}
+ ${IMPORT_STATE.STARTED} | ${false}
+ ${IMPORT_STATE.FAILED} | ${false}
+ ${IMPORT_STATE.FINISHED} | ${true}
+ ${IMPORT_STATE.NONE} | ${false}
+ ${undefined} | ${false}
+ `('returns $result when state is $state', ({ state, result }) => {
+ expect(isFinished(state)).toBe(result);
});
+});
- it('returns false when state is IMPORT_STATE.FAILED', () => {
- expect(isInProgress(IMPORT_STATE.FAILED)).toBe(false);
- });
+describe('calculateJiraImportLabel', () => {
+ const jiraImports = [
+ { jiraProjectKey: 'MTG' },
+ { jiraProjectKey: 'MJP' },
+ { jiraProjectKey: 'MTG' },
+ { jiraProjectKey: 'MSJP' },
+ { jiraProjectKey: 'MTG' },
+ ];
- it('returns false when state is IMPORT_STATE.FINISHED', () => {
- expect(isInProgress(IMPORT_STATE.FINISHED)).toBe(false);
- });
+ const labels = [
+ { color: '#111', title: 'jira-import::MTG-1' },
+ { color: '#222', title: 'jira-import::MTG-2' },
+ { color: '#333', title: 'jira-import::MTG-3' },
+ ];
+
+ it('returns a label with the Jira project key and correct import count in the title', () => {
+ const label = calculateJiraImportLabel(jiraImports, labels);
- it('returns false when state is IMPORT_STATE.NONE', () => {
- expect(isInProgress(IMPORT_STATE.NONE)).toBe(false);
+ expect(label.title).toBe('jira-import::MTG-3');
});
- it('returns false when state is undefined', () => {
- expect(isInProgress()).toBe(false);
+ it('returns a label with the correct color', () => {
+ const label = calculateJiraImportLabel(jiraImports, labels);
+
+ expect(label.color).toBe('#333');
});
});
diff --git a/spec/javascripts/jobs/components/artifacts_block_spec.js b/spec/frontend/jobs/components/artifacts_block_spec.js
index 9cb56737f3e..9cb56737f3e 100644
--- a/spec/javascripts/jobs/components/artifacts_block_spec.js
+++ b/spec/frontend/jobs/components/artifacts_block_spec.js
diff --git a/spec/frontend/jobs/components/commit_block_spec.js b/spec/frontend/jobs/components/commit_block_spec.js
new file mode 100644
index 00000000000..4e2d0053831
--- /dev/null
+++ b/spec/frontend/jobs/components/commit_block_spec.js
@@ -0,0 +1,89 @@
+import Vue from 'vue';
+import component from '~/jobs/components/commit_block.vue';
+import mountComponent from '../../helpers/vue_mount_component_helper';
+
+describe('Commit block', () => {
+ const Component = Vue.extend(component);
+ let vm;
+
+ const props = {
+ commit: {
+ short_id: '1f0fb84f',
+ id: '1f0fb84fb6770d74d97eee58118fd3909cd4f48c',
+ commit_path: 'commit/1f0fb84fb6770d74d97eee58118fd3909cd4f48c',
+ title: 'Update README.md',
+ },
+ mergeRequest: {
+ iid: '!21244',
+ path: 'merge_requests/21244',
+ },
+ isLastBlock: true,
+ };
+
+ afterEach(() => {
+ vm.$destroy();
+ });
+
+ describe('pipeline short sha', () => {
+ beforeEach(() => {
+ vm = mountComponent(Component, {
+ ...props,
+ });
+ });
+
+ it('renders pipeline short sha link', () => {
+ expect(vm.$el.querySelector('.js-commit-sha').getAttribute('href')).toEqual(
+ props.commit.commit_path,
+ );
+
+ expect(vm.$el.querySelector('.js-commit-sha').textContent.trim()).toEqual(
+ props.commit.short_id,
+ );
+ });
+
+ it('renders clipboard button', () => {
+ expect(vm.$el.querySelector('button').getAttribute('data-clipboard-text')).toEqual(
+ props.commit.id,
+ );
+ });
+ });
+
+ describe('with merge request', () => {
+ it('renders merge request link and reference', () => {
+ vm = mountComponent(Component, {
+ ...props,
+ });
+
+ expect(vm.$el.querySelector('.js-link-commit').getAttribute('href')).toEqual(
+ props.mergeRequest.path,
+ );
+
+ expect(vm.$el.querySelector('.js-link-commit').textContent.trim()).toEqual(
+ `!${props.mergeRequest.iid}`,
+ );
+ });
+ });
+
+ describe('without merge request', () => {
+ it('does not render merge request', () => {
+ const copyProps = { ...props };
+ delete copyProps.mergeRequest;
+
+ vm = mountComponent(Component, {
+ ...copyProps,
+ });
+
+ expect(vm.$el.querySelector('.js-link-commit')).toBeNull();
+ });
+ });
+
+ describe('git commit title', () => {
+ it('renders git commit title', () => {
+ vm = mountComponent(Component, {
+ ...props,
+ });
+
+ expect(vm.$el.textContent).toContain(props.commit.title);
+ });
+ });
+});
diff --git a/spec/javascripts/jobs/components/empty_state_spec.js b/spec/frontend/jobs/components/empty_state_spec.js
index c6eac4e27b3..c6eac4e27b3 100644
--- a/spec/javascripts/jobs/components/empty_state_spec.js
+++ b/spec/frontend/jobs/components/empty_state_spec.js
diff --git a/spec/javascripts/jobs/components/environments_block_spec.js b/spec/frontend/jobs/components/environments_block_spec.js
index 4f2359e83b6..4f2359e83b6 100644
--- a/spec/javascripts/jobs/components/environments_block_spec.js
+++ b/spec/frontend/jobs/components/environments_block_spec.js
diff --git a/spec/frontend/jobs/components/job_container_item_spec.js b/spec/frontend/jobs/components/job_container_item_spec.js
new file mode 100644
index 00000000000..9019504d22d
--- /dev/null
+++ b/spec/frontend/jobs/components/job_container_item_spec.js
@@ -0,0 +1,101 @@
+import Vue from 'vue';
+import mountComponent from 'helpers/vue_mount_component_helper';
+import JobContainerItem from '~/jobs/components/job_container_item.vue';
+import job from '../mock_data';
+
+describe('JobContainerItem', () => {
+ const delayedJobFixture = getJSONFixture('jobs/delayed.json');
+ const Component = Vue.extend(JobContainerItem);
+ let vm;
+
+ afterEach(() => {
+ vm.$destroy();
+ });
+
+ const sharedTests = () => {
+ it('displays a status icon', () => {
+ expect(vm.$el).toHaveSpriteIcon(job.status.icon);
+ });
+
+ it('displays the job name', () => {
+ expect(vm.$el.innerText).toContain(job.name);
+ });
+
+ it('displays a link to the job', () => {
+ const link = vm.$el.querySelector('.js-job-link');
+
+ expect(link.href).toBe(job.status.details_path);
+ });
+ };
+
+ describe('when a job is not active and not retied', () => {
+ beforeEach(() => {
+ vm = mountComponent(Component, {
+ job,
+ isActive: false,
+ });
+ });
+
+ sharedTests();
+ });
+
+ describe('when a job is active', () => {
+ beforeEach(() => {
+ vm = mountComponent(Component, {
+ job,
+ isActive: true,
+ });
+ });
+
+ sharedTests();
+
+ it('displays an arrow', () => {
+ expect(vm.$el).toHaveSpriteIcon('arrow-right');
+ });
+ });
+
+ describe('when a job is retried', () => {
+ beforeEach(() => {
+ vm = mountComponent(Component, {
+ job: {
+ ...job,
+ retried: true,
+ },
+ isActive: false,
+ });
+ });
+
+ sharedTests();
+
+ it('displays an icon', () => {
+ expect(vm.$el).toHaveSpriteIcon('retry');
+ });
+ });
+
+ describe('for delayed job', () => {
+ beforeEach(() => {
+ const remainingMilliseconds = 1337000;
+ jest
+ .spyOn(Date, 'now')
+ .mockImplementation(
+ () => new Date(delayedJobFixture.scheduled_at).getTime() - remainingMilliseconds,
+ );
+ });
+
+ it('displays remaining time in tooltip', done => {
+ vm = mountComponent(Component, {
+ job: delayedJobFixture,
+ isActive: false,
+ });
+
+ Vue.nextTick()
+ .then(() => {
+ expect(vm.$el.querySelector('.js-job-link').getAttribute('data-original-title')).toEqual(
+ 'delayed job - delayed manual action (00:22:17)',
+ );
+ })
+ .then(done)
+ .catch(done.fail);
+ });
+ });
+});
diff --git a/spec/frontend/jobs/components/job_log_spec.js b/spec/frontend/jobs/components/job_log_spec.js
new file mode 100644
index 00000000000..2bb1e0af3a2
--- /dev/null
+++ b/spec/frontend/jobs/components/job_log_spec.js
@@ -0,0 +1,65 @@
+import Vue from 'vue';
+import { mountComponentWithStore } from 'helpers/vue_mount_component_helper';
+import component from '~/jobs/components/job_log.vue';
+import createStore from '~/jobs/store';
+import { resetStore } from '../store/helpers';
+
+describe('Job Log', () => {
+ const Component = Vue.extend(component);
+ let store;
+ let vm;
+
+ const trace =
+ '<span>Running with gitlab-runner 12.1.0 (de7731dd)<br/></span><span> on docker-auto-scale-com d5ae8d25<br/></span><div class="append-right-8" data-timestamp="1565502765" data-section="prepare-executor" role="button"></div><span class="section section-header js-s-prepare-executor">Using Docker executor with image ruby:2.6 ...<br/></span>';
+
+ beforeEach(() => {
+ store = createStore();
+ });
+
+ afterEach(() => {
+ resetStore(store);
+ vm.$destroy();
+ });
+
+ it('renders provided trace', () => {
+ vm = mountComponentWithStore(Component, {
+ props: {
+ trace,
+ isComplete: true,
+ },
+ store,
+ });
+
+ expect(vm.$el.querySelector('code').textContent).toContain(
+ 'Running with gitlab-runner 12.1.0 (de7731dd)',
+ );
+ });
+
+ describe('while receiving trace', () => {
+ it('renders animation', () => {
+ vm = mountComponentWithStore(Component, {
+ props: {
+ trace,
+ isComplete: false,
+ },
+ store,
+ });
+
+ expect(vm.$el.querySelector('.js-log-animation')).not.toBeNull();
+ });
+ });
+
+ describe('when build trace has finishes', () => {
+ it('does not render animation', () => {
+ vm = mountComponentWithStore(Component, {
+ props: {
+ trace,
+ isComplete: true,
+ },
+ store,
+ });
+
+ expect(vm.$el.querySelector('.js-log-animation')).toBeNull();
+ });
+ });
+});
diff --git a/spec/javascripts/jobs/components/jobs_container_spec.js b/spec/frontend/jobs/components/jobs_container_spec.js
index 119b18b7557..119b18b7557 100644
--- a/spec/javascripts/jobs/components/jobs_container_spec.js
+++ b/spec/frontend/jobs/components/jobs_container_spec.js
diff --git a/spec/frontend/jobs/components/log/line_header_spec.js b/spec/frontend/jobs/components/log/line_header_spec.js
index f2e202674ee..5ce69221dab 100644
--- a/spec/frontend/jobs/components/log/line_header_spec.js
+++ b/spec/frontend/jobs/components/log/line_header_spec.js
@@ -86,7 +86,7 @@ describe('Job Log Header Line', () => {
describe('with duration', () => {
beforeEach(() => {
- createComponent(Object.assign({}, data, { duration: '00:10' }));
+ createComponent({ ...data, duration: '00:10' });
});
it('renders the duration badge', () => {
diff --git a/spec/javascripts/jobs/components/manual_variables_form_spec.js b/spec/frontend/jobs/components/manual_variables_form_spec.js
index 82fd73ef033..82fd73ef033 100644
--- a/spec/javascripts/jobs/components/manual_variables_form_spec.js
+++ b/spec/frontend/jobs/components/manual_variables_form_spec.js
diff --git a/spec/frontend/jobs/components/sidebar_spec.js b/spec/frontend/jobs/components/sidebar_spec.js
new file mode 100644
index 00000000000..0c8e2dc3aef
--- /dev/null
+++ b/spec/frontend/jobs/components/sidebar_spec.js
@@ -0,0 +1,166 @@
+import Vue from 'vue';
+import sidebarDetailsBlock from '~/jobs/components/sidebar.vue';
+import createStore from '~/jobs/store';
+import job, { jobsInStage } from '../mock_data';
+import { mountComponentWithStore } from '../../helpers/vue_mount_component_helper';
+import { trimText } from '../../helpers/text_helper';
+
+describe('Sidebar details block', () => {
+ const SidebarComponent = Vue.extend(sidebarDetailsBlock);
+ let vm;
+ let store;
+
+ beforeEach(() => {
+ store = createStore();
+ });
+
+ afterEach(() => {
+ vm.$destroy();
+ });
+
+ describe('when there is no retry path retry', () => {
+ it('should not render a retry button', () => {
+ const copy = { ...job };
+ delete copy.retry_path;
+
+ store.dispatch('receiveJobSuccess', copy);
+ vm = mountComponentWithStore(SidebarComponent, {
+ store,
+ });
+
+ expect(vm.$el.querySelector('.js-retry-button')).toBeNull();
+ });
+ });
+
+ describe('without terminal path', () => {
+ it('does not render terminal link', () => {
+ store.dispatch('receiveJobSuccess', job);
+ vm = mountComponentWithStore(SidebarComponent, { store });
+
+ expect(vm.$el.querySelector('.js-terminal-link')).toBeNull();
+ });
+ });
+
+ describe('with terminal path', () => {
+ it('renders terminal link', () => {
+ store.dispatch('receiveJobSuccess', { ...job, terminal_path: 'job/43123/terminal' });
+ vm = mountComponentWithStore(SidebarComponent, {
+ store,
+ });
+
+ expect(vm.$el.querySelector('.js-terminal-link')).not.toBeNull();
+ });
+ });
+
+ beforeEach(() => {
+ store.dispatch('receiveJobSuccess', job);
+ vm = mountComponentWithStore(SidebarComponent, { store });
+ });
+
+ describe('actions', () => {
+ it('should render link to new issue', () => {
+ expect(vm.$el.querySelector('.js-new-issue').getAttribute('href')).toEqual(
+ job.new_issue_path,
+ );
+
+ expect(vm.$el.querySelector('.js-new-issue').textContent.trim()).toEqual('New issue');
+ });
+
+ it('should render link to retry job', () => {
+ expect(vm.$el.querySelector('.js-retry-button').getAttribute('href')).toEqual(job.retry_path);
+ });
+
+ it('should render link to cancel job', () => {
+ expect(vm.$el.querySelector('.js-cancel-job').getAttribute('href')).toEqual(job.cancel_path);
+ });
+ });
+
+ describe('information', () => {
+ it('should render job duration', () => {
+ expect(trimText(vm.$el.querySelector('.js-job-duration').textContent)).toEqual(
+ 'Duration: 6 seconds',
+ );
+ });
+
+ it('should render erased date', () => {
+ expect(trimText(vm.$el.querySelector('.js-job-erased').textContent)).toEqual(
+ 'Erased: 3 weeks ago',
+ );
+ });
+
+ it('should render finished date', () => {
+ expect(trimText(vm.$el.querySelector('.js-job-finished').textContent)).toEqual(
+ 'Finished: 3 weeks ago',
+ );
+ });
+
+ it('should render queued date', () => {
+ expect(trimText(vm.$el.querySelector('.js-job-queued').textContent)).toEqual(
+ 'Queued: 9 seconds',
+ );
+ });
+
+ it('should render runner ID', () => {
+ expect(trimText(vm.$el.querySelector('.js-job-runner').textContent)).toEqual(
+ 'Runner: local ci runner (#1)',
+ );
+ });
+
+ it('should render timeout information', () => {
+ expect(trimText(vm.$el.querySelector('.js-job-timeout').textContent)).toEqual(
+ 'Timeout: 1m 40s (from runner)',
+ );
+ });
+
+ it('should render coverage', () => {
+ expect(trimText(vm.$el.querySelector('.js-job-coverage').textContent)).toEqual(
+ 'Coverage: 20%',
+ );
+ });
+
+ it('should render tags', () => {
+ expect(trimText(vm.$el.querySelector('.js-job-tags').textContent)).toEqual('Tags: tag');
+ });
+ });
+
+ describe('stages dropdown', () => {
+ beforeEach(() => {
+ store.dispatch('receiveJobSuccess', job);
+ });
+
+ describe('with stages', () => {
+ beforeEach(() => {
+ vm = mountComponentWithStore(SidebarComponent, { store });
+ });
+
+ it('renders value provided as selectedStage as selected', () => {
+ expect(vm.$el.querySelector('.js-selected-stage').textContent.trim()).toEqual(
+ vm.selectedStage,
+ );
+ });
+ });
+
+ describe('without jobs for stages', () => {
+ beforeEach(() => {
+ store.dispatch('receiveJobSuccess', job);
+ vm = mountComponentWithStore(SidebarComponent, { store });
+ });
+
+ it('does not render job container', () => {
+ expect(vm.$el.querySelector('.js-jobs-container')).toBeNull();
+ });
+ });
+
+ describe('with jobs for stages', () => {
+ beforeEach(() => {
+ store.dispatch('receiveJobSuccess', job);
+ store.dispatch('receiveJobsForStageSuccess', jobsInStage.latest_statuses);
+ vm = mountComponentWithStore(SidebarComponent, { store });
+ });
+
+ it('renders list of jobs', () => {
+ expect(vm.$el.querySelector('.js-jobs-container')).not.toBeNull();
+ });
+ });
+ });
+});
diff --git a/spec/frontend/jobs/components/stages_dropdown_spec.js b/spec/frontend/jobs/components/stages_dropdown_spec.js
new file mode 100644
index 00000000000..e8fa6094c25
--- /dev/null
+++ b/spec/frontend/jobs/components/stages_dropdown_spec.js
@@ -0,0 +1,163 @@
+import Vue from 'vue';
+import { trimText } from 'helpers/text_helper';
+import component from '~/jobs/components/stages_dropdown.vue';
+import mountComponent from '../../helpers/vue_mount_component_helper';
+
+describe('Stages Dropdown', () => {
+ const Component = Vue.extend(component);
+ let vm;
+
+ const mockPipelineData = {
+ id: 28029444,
+ details: {
+ status: {
+ details_path: '/gitlab-org/gitlab-foss/pipelines/28029444',
+ group: 'success',
+ has_details: true,
+ icon: 'status_success',
+ label: 'passed',
+ text: 'passed',
+ tooltip: 'passed',
+ },
+ },
+ path: 'pipeline/28029444',
+ flags: {
+ merge_request_pipeline: true,
+ detached_merge_request_pipeline: false,
+ },
+ merge_request: {
+ iid: 1234,
+ path: '/root/detached-merge-request-pipelines/-/merge_requests/1',
+ title: 'Update README.md',
+ source_branch: 'feature-1234',
+ source_branch_path: '/root/detached-merge-request-pipelines/branches/feature-1234',
+ target_branch: 'master',
+ target_branch_path: '/root/detached-merge-request-pipelines/branches/master',
+ },
+ ref: {
+ name: 'test-branch',
+ },
+ };
+
+ describe('without a merge request pipeline', () => {
+ let pipeline;
+
+ beforeEach(() => {
+ pipeline = JSON.parse(JSON.stringify(mockPipelineData));
+ delete pipeline.merge_request;
+ delete pipeline.flags.merge_request_pipeline;
+ delete pipeline.flags.detached_merge_request_pipeline;
+
+ vm = mountComponent(Component, {
+ pipeline,
+ stages: [{ name: 'build' }, { name: 'test' }],
+ selectedStage: 'deploy',
+ });
+ });
+
+ afterEach(() => {
+ vm.$destroy();
+ });
+
+ it('renders pipeline status', () => {
+ expect(vm.$el.querySelector('.js-ci-status-icon-success')).not.toBeNull();
+ });
+
+ it('renders pipeline link', () => {
+ expect(vm.$el.querySelector('.js-pipeline-path').getAttribute('href')).toEqual(
+ 'pipeline/28029444',
+ );
+ });
+
+ it('renders dropdown with stages', () => {
+ expect(vm.$el.querySelector('.dropdown .js-stage-item').textContent).toContain('build');
+ });
+
+ it('rendes selected stage', () => {
+ expect(vm.$el.querySelector('.dropdown .js-selected-stage').textContent).toContain('deploy');
+ });
+
+ it(`renders the pipeline info text like "Pipeline #123 for source_branch"`, () => {
+ const expected = `Pipeline #${pipeline.id} for ${pipeline.ref.name}`;
+ const actual = trimText(vm.$el.querySelector('.js-pipeline-info').innerText);
+
+ expect(actual).toBe(expected);
+ });
+ });
+
+ describe('with an "attached" merge request pipeline', () => {
+ let pipeline;
+
+ beforeEach(() => {
+ pipeline = JSON.parse(JSON.stringify(mockPipelineData));
+ pipeline.flags.merge_request_pipeline = true;
+ pipeline.flags.detached_merge_request_pipeline = false;
+
+ vm = mountComponent(Component, {
+ pipeline,
+ stages: [],
+ selectedStage: 'deploy',
+ });
+ });
+
+ it(`renders the pipeline info text like "Pipeline #123 for !456 with source_branch into target_branch"`, () => {
+ const expected = `Pipeline #${pipeline.id} for !${pipeline.merge_request.iid} with ${pipeline.merge_request.source_branch} into ${pipeline.merge_request.target_branch}`;
+ const actual = trimText(vm.$el.querySelector('.js-pipeline-info').innerText);
+
+ expect(actual).toBe(expected);
+ });
+
+ it(`renders the correct merge request link`, () => {
+ const actual = vm.$el.querySelector('.js-mr-link').href;
+
+ expect(actual).toContain(pipeline.merge_request.path);
+ });
+
+ it(`renders the correct source branch link`, () => {
+ const actual = vm.$el.querySelector('.js-source-branch-link').href;
+
+ expect(actual).toContain(pipeline.merge_request.source_branch_path);
+ });
+
+ it(`renders the correct target branch link`, () => {
+ const actual = vm.$el.querySelector('.js-target-branch-link').href;
+
+ expect(actual).toContain(pipeline.merge_request.target_branch_path);
+ });
+ });
+
+ describe('with a detached merge request pipeline', () => {
+ let pipeline;
+
+ beforeEach(() => {
+ pipeline = JSON.parse(JSON.stringify(mockPipelineData));
+ pipeline.flags.merge_request_pipeline = false;
+ pipeline.flags.detached_merge_request_pipeline = true;
+
+ vm = mountComponent(Component, {
+ pipeline,
+ stages: [],
+ selectedStage: 'deploy',
+ });
+ });
+
+ it(`renders the pipeline info like "Pipeline #123 for !456 with source_branch"`, () => {
+ const expected = `Pipeline #${pipeline.id} for !${pipeline.merge_request.iid} with ${pipeline.merge_request.source_branch}`;
+ const actual = trimText(vm.$el.querySelector('.js-pipeline-info').innerText);
+
+ expect(actual).toBe(expected);
+ });
+
+ it(`renders the correct merge request link`, () => {
+ const actual = vm.$el.querySelector('.js-mr-link').href;
+
+ expect(actual).toContain(pipeline.merge_request.path);
+ });
+
+ it(`renders the correct source branch link`, () => {
+ const actual = vm.$el.querySelector('.js-source-branch-link').href;
+
+ expect(actual).toContain(pipeline.merge_request.source_branch_path);
+ });
+ });
+});
diff --git a/spec/javascripts/jobs/components/trigger_block_spec.js b/spec/frontend/jobs/components/trigger_block_spec.js
index 448197b82c0..448197b82c0 100644
--- a/spec/javascripts/jobs/components/trigger_block_spec.js
+++ b/spec/frontend/jobs/components/trigger_block_spec.js
diff --git a/spec/javascripts/jobs/components/unmet_prerequisites_block_spec.js b/spec/frontend/jobs/components/unmet_prerequisites_block_spec.js
index 68fcb321214..68fcb321214 100644
--- a/spec/javascripts/jobs/components/unmet_prerequisites_block_spec.js
+++ b/spec/frontend/jobs/components/unmet_prerequisites_block_spec.js
diff --git a/spec/frontend/jobs/mixins/delayed_job_mixin_spec.js b/spec/frontend/jobs/mixins/delayed_job_mixin_spec.js
new file mode 100644
index 00000000000..2f7a6030650
--- /dev/null
+++ b/spec/frontend/jobs/mixins/delayed_job_mixin_spec.js
@@ -0,0 +1,79 @@
+import Vue from 'vue';
+import mountComponent from 'helpers/vue_mount_component_helper';
+import delayedJobMixin from '~/jobs/mixins/delayed_job_mixin';
+
+describe('DelayedJobMixin', () => {
+ const delayedJobFixture = getJSONFixture('jobs/delayed.json');
+ const dummyComponent = Vue.extend({
+ mixins: [delayedJobMixin],
+ props: {
+ job: {
+ type: Object,
+ required: true,
+ },
+ },
+ render(createElement) {
+ return createElement('div', this.remainingTime);
+ },
+ });
+
+ let vm;
+
+ afterEach(() => {
+ vm.$destroy();
+ jest.clearAllTimers();
+ });
+
+ describe('if job is empty object', () => {
+ beforeEach(() => {
+ vm = mountComponent(dummyComponent, {
+ job: {},
+ });
+ });
+
+ it('sets remaining time to 00:00:00', () => {
+ expect(vm.$el.innerText).toBe('00:00:00');
+ });
+
+ describe('after mounting', () => {
+ beforeEach(() => vm.$nextTick());
+
+ it('does not update remaining time', () => {
+ expect(vm.$el.innerText).toBe('00:00:00');
+ });
+ });
+ });
+
+ describe('if job is delayed job', () => {
+ let remainingTimeInMilliseconds = 42000;
+
+ beforeEach(() => {
+ jest
+ .spyOn(Date, 'now')
+ .mockImplementation(
+ () => new Date(delayedJobFixture.scheduled_at).getTime() - remainingTimeInMilliseconds,
+ );
+
+ vm = mountComponent(dummyComponent, {
+ job: delayedJobFixture,
+ });
+ });
+
+ describe('after mounting', () => {
+ beforeEach(() => vm.$nextTick());
+
+ it('sets remaining time', () => {
+ expect(vm.$el.innerText).toBe('00:00:42');
+ });
+
+ it('updates remaining time', () => {
+ remainingTimeInMilliseconds = 41000;
+ jest.advanceTimersByTime(1000);
+
+ return vm.$nextTick().then(() => {
+ expect(vm.$el.innerText).toBe('00:00:41');
+ });
+ });
+ });
+ });
+});
diff --git a/spec/frontend/jobs/store/actions_spec.js b/spec/frontend/jobs/store/actions_spec.js
new file mode 100644
index 00000000000..91bd5521f70
--- /dev/null
+++ b/spec/frontend/jobs/store/actions_spec.js
@@ -0,0 +1,512 @@
+import MockAdapter from 'axios-mock-adapter';
+import testAction from 'helpers/vuex_action_helper';
+import { TEST_HOST } from '../../helpers/test_constants';
+import axios from '~/lib/utils/axios_utils';
+import {
+ setJobEndpoint,
+ setTraceOptions,
+ clearEtagPoll,
+ stopPolling,
+ requestJob,
+ fetchJob,
+ receiveJobSuccess,
+ receiveJobError,
+ scrollTop,
+ scrollBottom,
+ requestTrace,
+ fetchTrace,
+ startPollingTrace,
+ stopPollingTrace,
+ receiveTraceSuccess,
+ receiveTraceError,
+ toggleCollapsibleLine,
+ requestJobsForStage,
+ fetchJobsForStage,
+ receiveJobsForStageSuccess,
+ receiveJobsForStageError,
+ hideSidebar,
+ showSidebar,
+ toggleSidebar,
+} from '~/jobs/store/actions';
+import state from '~/jobs/store/state';
+import * as types from '~/jobs/store/mutation_types';
+
+describe('Job State actions', () => {
+ let mockedState;
+
+ beforeEach(() => {
+ mockedState = state();
+ });
+
+ describe('setJobEndpoint', () => {
+ it('should commit SET_JOB_ENDPOINT mutation', done => {
+ testAction(
+ setJobEndpoint,
+ 'job/872324.json',
+ mockedState,
+ [{ type: types.SET_JOB_ENDPOINT, payload: 'job/872324.json' }],
+ [],
+ done,
+ );
+ });
+ });
+
+ describe('setTraceOptions', () => {
+ it('should commit SET_TRACE_OPTIONS mutation', done => {
+ testAction(
+ setTraceOptions,
+ { pagePath: 'job/872324/trace.json' },
+ mockedState,
+ [{ type: types.SET_TRACE_OPTIONS, payload: { pagePath: 'job/872324/trace.json' } }],
+ [],
+ done,
+ );
+ });
+ });
+
+ describe('hideSidebar', () => {
+ it('should commit HIDE_SIDEBAR mutation', done => {
+ testAction(hideSidebar, null, mockedState, [{ type: types.HIDE_SIDEBAR }], [], done);
+ });
+ });
+
+ describe('showSidebar', () => {
+ it('should commit HIDE_SIDEBAR mutation', done => {
+ testAction(showSidebar, null, mockedState, [{ type: types.SHOW_SIDEBAR }], [], done);
+ });
+ });
+
+ describe('toggleSidebar', () => {
+ describe('when isSidebarOpen is true', () => {
+ it('should dispatch hideSidebar', done => {
+ testAction(toggleSidebar, null, mockedState, [], [{ type: 'hideSidebar' }], done);
+ });
+ });
+
+ describe('when isSidebarOpen is false', () => {
+ it('should dispatch showSidebar', done => {
+ mockedState.isSidebarOpen = false;
+
+ testAction(toggleSidebar, null, mockedState, [], [{ type: 'showSidebar' }], done);
+ });
+ });
+ });
+
+ describe('requestJob', () => {
+ it('should commit REQUEST_JOB mutation', done => {
+ testAction(requestJob, null, mockedState, [{ type: types.REQUEST_JOB }], [], done);
+ });
+ });
+
+ describe('fetchJob', () => {
+ let mock;
+
+ beforeEach(() => {
+ mockedState.jobEndpoint = `${TEST_HOST}/endpoint.json`;
+ mock = new MockAdapter(axios);
+ });
+
+ afterEach(() => {
+ mock.restore();
+ stopPolling();
+ clearEtagPoll();
+ });
+
+ describe('success', () => {
+ it('dispatches requestJob and receiveJobSuccess ', done => {
+ mock.onGet(`${TEST_HOST}/endpoint.json`).replyOnce(200, { id: 121212, name: 'karma' });
+
+ testAction(
+ fetchJob,
+ null,
+ mockedState,
+ [],
+ [
+ {
+ type: 'requestJob',
+ },
+ {
+ payload: { id: 121212, name: 'karma' },
+ type: 'receiveJobSuccess',
+ },
+ ],
+ done,
+ );
+ });
+ });
+
+ describe('error', () => {
+ beforeEach(() => {
+ mock.onGet(`${TEST_HOST}/endpoint.json`).reply(500);
+ });
+
+ it('dispatches requestJob and receiveJobError ', done => {
+ testAction(
+ fetchJob,
+ null,
+ mockedState,
+ [],
+ [
+ {
+ type: 'requestJob',
+ },
+ {
+ type: 'receiveJobError',
+ },
+ ],
+ done,
+ );
+ });
+ });
+ });
+
+ describe('receiveJobSuccess', () => {
+ it('should commit RECEIVE_JOB_SUCCESS mutation', done => {
+ testAction(
+ receiveJobSuccess,
+ { id: 121232132 },
+ mockedState,
+ [{ type: types.RECEIVE_JOB_SUCCESS, payload: { id: 121232132 } }],
+ [],
+ done,
+ );
+ });
+ });
+
+ describe('receiveJobError', () => {
+ it('should commit RECEIVE_JOB_ERROR mutation', done => {
+ testAction(receiveJobError, null, mockedState, [{ type: types.RECEIVE_JOB_ERROR }], [], done);
+ });
+ });
+
+ describe('scrollTop', () => {
+ it('should dispatch toggleScrollButtons action', done => {
+ testAction(scrollTop, null, mockedState, [], [{ type: 'toggleScrollButtons' }], done);
+ });
+ });
+
+ describe('scrollBottom', () => {
+ it('should dispatch toggleScrollButtons action', done => {
+ testAction(scrollBottom, null, mockedState, [], [{ type: 'toggleScrollButtons' }], done);
+ });
+ });
+
+ describe('requestTrace', () => {
+ it('should commit REQUEST_TRACE mutation', done => {
+ testAction(requestTrace, null, mockedState, [{ type: types.REQUEST_TRACE }], [], done);
+ });
+ });
+
+ describe('fetchTrace', () => {
+ let mock;
+
+ beforeEach(() => {
+ mockedState.traceEndpoint = `${TEST_HOST}/endpoint`;
+ mock = new MockAdapter(axios);
+ });
+
+ afterEach(() => {
+ mock.restore();
+ stopPolling();
+ clearEtagPoll();
+ });
+
+ describe('success', () => {
+ it('dispatches requestTrace, receiveTraceSuccess and stopPollingTrace when job is complete', done => {
+ mock.onGet(`${TEST_HOST}/endpoint/trace.json`).replyOnce(200, {
+ html: 'I, [2018-08-17T22:57:45.707325 #1841] INFO -- :',
+ complete: true,
+ });
+
+ testAction(
+ fetchTrace,
+ null,
+ mockedState,
+ [],
+ [
+ {
+ type: 'toggleScrollisInBottom',
+ payload: true,
+ },
+ {
+ payload: {
+ html: 'I, [2018-08-17T22:57:45.707325 #1841] INFO -- :',
+ complete: true,
+ },
+ type: 'receiveTraceSuccess',
+ },
+ {
+ type: 'stopPollingTrace',
+ },
+ ],
+ done,
+ );
+ });
+
+ describe('when job is incomplete', () => {
+ let tracePayload;
+
+ beforeEach(() => {
+ tracePayload = {
+ html: 'I, [2018-08-17T22:57:45.707325 #1841] INFO -- :',
+ complete: false,
+ };
+
+ mock.onGet(`${TEST_HOST}/endpoint/trace.json`).replyOnce(200, tracePayload);
+ });
+
+ it('dispatches startPollingTrace', done => {
+ testAction(
+ fetchTrace,
+ null,
+ mockedState,
+ [],
+ [
+ { type: 'toggleScrollisInBottom', payload: true },
+ { type: 'receiveTraceSuccess', payload: tracePayload },
+ { type: 'startPollingTrace' },
+ ],
+ done,
+ );
+ });
+
+ it('does not dispatch startPollingTrace when timeout is non-empty', done => {
+ mockedState.traceTimeout = 1;
+
+ testAction(
+ fetchTrace,
+ null,
+ mockedState,
+ [],
+ [
+ { type: 'toggleScrollisInBottom', payload: true },
+ { type: 'receiveTraceSuccess', payload: tracePayload },
+ ],
+ done,
+ );
+ });
+ });
+ });
+
+ describe('error', () => {
+ beforeEach(() => {
+ mock.onGet(`${TEST_HOST}/endpoint/trace.json`).reply(500);
+ });
+
+ it('dispatches requestTrace and receiveTraceError ', done => {
+ testAction(
+ fetchTrace,
+ null,
+ mockedState,
+ [],
+ [
+ {
+ type: 'receiveTraceError',
+ },
+ ],
+ done,
+ );
+ });
+ });
+ });
+
+ describe('startPollingTrace', () => {
+ let dispatch;
+ let commit;
+
+ beforeEach(() => {
+ dispatch = jest.fn();
+ commit = jest.fn();
+
+ startPollingTrace({ dispatch, commit });
+ });
+
+ afterEach(() => {
+ jest.clearAllTimers();
+ });
+
+ it('should save the timeout id but not call fetchTrace', () => {
+ expect(commit).toHaveBeenCalledWith(types.SET_TRACE_TIMEOUT, expect.any(Number));
+ expect(commit.mock.calls[0][1]).toBeGreaterThan(0);
+
+ expect(dispatch).not.toHaveBeenCalledWith('fetchTrace');
+ });
+
+ describe('after timeout has passed', () => {
+ beforeEach(() => {
+ jest.advanceTimersByTime(4000);
+ });
+
+ it('should clear the timeout id and fetchTrace', () => {
+ expect(commit).toHaveBeenCalledWith(types.SET_TRACE_TIMEOUT, 0);
+ expect(dispatch).toHaveBeenCalledWith('fetchTrace');
+ });
+ });
+ });
+
+ describe('stopPollingTrace', () => {
+ let origTimeout;
+
+ beforeEach(() => {
+ // Can't use spyOn(window, 'clearTimeout') because this caused unrelated specs to timeout
+ // https://gitlab.com/gitlab-org/gitlab/-/merge_requests/23838#note_280277727
+ origTimeout = window.clearTimeout;
+ window.clearTimeout = jest.fn();
+ });
+
+ afterEach(() => {
+ window.clearTimeout = origTimeout;
+ });
+
+ it('should commit STOP_POLLING_TRACE mutation ', done => {
+ const traceTimeout = 7;
+
+ testAction(
+ stopPollingTrace,
+ null,
+ { ...mockedState, traceTimeout },
+ [{ type: types.SET_TRACE_TIMEOUT, payload: 0 }, { type: types.STOP_POLLING_TRACE }],
+ [],
+ )
+ .then(() => {
+ expect(window.clearTimeout).toHaveBeenCalledWith(traceTimeout);
+ })
+ .then(done)
+ .catch(done.fail);
+ });
+ });
+
+ describe('receiveTraceSuccess', () => {
+ it('should commit RECEIVE_TRACE_SUCCESS mutation ', done => {
+ testAction(
+ receiveTraceSuccess,
+ 'hello world',
+ mockedState,
+ [{ type: types.RECEIVE_TRACE_SUCCESS, payload: 'hello world' }],
+ [],
+ done,
+ );
+ });
+ });
+
+ describe('receiveTraceError', () => {
+ it('should commit stop polling trace', done => {
+ testAction(receiveTraceError, null, mockedState, [], [{ type: 'stopPollingTrace' }], done);
+ });
+ });
+
+ describe('toggleCollapsibleLine', () => {
+ it('should commit TOGGLE_COLLAPSIBLE_LINE mutation ', done => {
+ testAction(
+ toggleCollapsibleLine,
+ { isClosed: true },
+ mockedState,
+ [{ type: types.TOGGLE_COLLAPSIBLE_LINE, payload: { isClosed: true } }],
+ [],
+ done,
+ );
+ });
+ });
+
+ describe('requestJobsForStage', () => {
+ it('should commit REQUEST_JOBS_FOR_STAGE mutation ', done => {
+ testAction(
+ requestJobsForStage,
+ { name: 'deploy' },
+ mockedState,
+ [{ type: types.REQUEST_JOBS_FOR_STAGE, payload: { name: 'deploy' } }],
+ [],
+ done,
+ );
+ });
+ });
+
+ describe('fetchJobsForStage', () => {
+ let mock;
+
+ beforeEach(() => {
+ mock = new MockAdapter(axios);
+ });
+
+ afterEach(() => {
+ mock.restore();
+ });
+
+ describe('success', () => {
+ it('dispatches requestJobsForStage and receiveJobsForStageSuccess ', done => {
+ mock
+ .onGet(`${TEST_HOST}/jobs.json`)
+ .replyOnce(200, { latest_statuses: [{ id: 121212, name: 'build' }], retried: [] });
+
+ testAction(
+ fetchJobsForStage,
+ { dropdown_path: `${TEST_HOST}/jobs.json` },
+ mockedState,
+ [],
+ [
+ {
+ type: 'requestJobsForStage',
+ payload: { dropdown_path: `${TEST_HOST}/jobs.json` },
+ },
+ {
+ payload: [{ id: 121212, name: 'build' }],
+ type: 'receiveJobsForStageSuccess',
+ },
+ ],
+ done,
+ );
+ });
+ });
+
+ describe('error', () => {
+ beforeEach(() => {
+ mock.onGet(`${TEST_HOST}/jobs.json`).reply(500);
+ });
+
+ it('dispatches requestJobsForStage and receiveJobsForStageError', done => {
+ testAction(
+ fetchJobsForStage,
+ { dropdown_path: `${TEST_HOST}/jobs.json` },
+ mockedState,
+ [],
+ [
+ {
+ type: 'requestJobsForStage',
+ payload: { dropdown_path: `${TEST_HOST}/jobs.json` },
+ },
+ {
+ type: 'receiveJobsForStageError',
+ },
+ ],
+ done,
+ );
+ });
+ });
+ });
+
+ describe('receiveJobsForStageSuccess', () => {
+ it('should commit RECEIVE_JOBS_FOR_STAGE_SUCCESS mutation ', done => {
+ testAction(
+ receiveJobsForStageSuccess,
+ [{ id: 121212, name: 'karma' }],
+ mockedState,
+ [{ type: types.RECEIVE_JOBS_FOR_STAGE_SUCCESS, payload: [{ id: 121212, name: 'karma' }] }],
+ [],
+ done,
+ );
+ });
+ });
+
+ describe('receiveJobsForStageError', () => {
+ it('should commit RECEIVE_JOBS_FOR_STAGE_ERROR mutation ', done => {
+ testAction(
+ receiveJobsForStageError,
+ null,
+ mockedState,
+ [{ type: types.RECEIVE_JOBS_FOR_STAGE_ERROR }],
+ [],
+ done,
+ );
+ });
+ });
+});
diff --git a/spec/javascripts/jobs/store/helpers.js b/spec/frontend/jobs/store/helpers.js
index 81a769b4a6e..81a769b4a6e 100644
--- a/spec/javascripts/jobs/store/helpers.js
+++ b/spec/frontend/jobs/store/helpers.js
diff --git a/spec/frontend/jobs/store/mutations_spec.js b/spec/frontend/jobs/store/mutations_spec.js
index d77690ffac0..3557d3b94b6 100644
--- a/spec/frontend/jobs/store/mutations_spec.js
+++ b/spec/frontend/jobs/store/mutations_spec.js
@@ -59,7 +59,7 @@ describe('Jobs Store Mutations', () => {
describe('when traceSize is bigger than the total size', () => {
it('sets isTraceSizeVisible to false', () => {
- const copy = Object.assign({}, stateCopy, { traceSize: 5118460, size: 2321312 });
+ const copy = { ...stateCopy, traceSize: 5118460, size: 2321312 };
mutations[types.RECEIVE_TRACE_SUCCESS](copy, { total: 511846 });
diff --git a/spec/frontend/labels_select_spec.js b/spec/frontend/labels_select_spec.js
index 5f48bad4970..8b08eb9e124 100644
--- a/spec/frontend/labels_select_spec.js
+++ b/spec/frontend/labels_select_spec.js
@@ -45,7 +45,6 @@ describe('LabelsSelect', () => {
labels: mockLabels,
issueUpdateURL: mockUrl,
enableScopedLabels: true,
- scopedLabelsDocumentationLink: 'docs-link',
}),
);
});
@@ -71,10 +70,6 @@ describe('LabelsSelect', () => {
it('generated label item has a gl-label-text class', () => {
expect($labelEl.find('span').hasClass('gl-label-text')).toEqual(true);
});
-
- it('generated label item template does not have gl-label-icon class', () => {
- expect($labelEl.find('.gl-label-icon')).toHaveLength(0);
- });
});
describe('when scoped label is present', () => {
@@ -87,7 +82,6 @@ describe('LabelsSelect', () => {
labels: mockScopedLabels,
issueUpdateURL: mockUrl,
enableScopedLabels: true,
- scopedLabelsDocumentationLink: 'docs-link',
}),
);
});
@@ -106,14 +100,6 @@ describe('LabelsSelect', () => {
expect($labelEl.find('a').attr('data-html')).toBe('true');
});
- it('generated label item template has question icon', () => {
- expect($labelEl.find('i.fa-question-circle')).toHaveLength(1);
- });
-
- it('generated label item template has gl-label-icon class', () => {
- expect($labelEl.find('.gl-label-icon')).toHaveLength(1);
- });
-
it('generated label item template has correct label styles', () => {
expect($labelEl.find('span.gl-label-text').attr('style')).toBe(
`background-color: ${label.color}; color: ${label.text_color};`,
@@ -141,7 +127,6 @@ describe('LabelsSelect', () => {
labels: mockScopedLabels2,
issueUpdateURL: mockUrl,
enableScopedLabels: true,
- scopedLabelsDocumentationLink: 'docs-link',
}),
);
});
diff --git a/spec/frontend/landing_spec.js b/spec/frontend/landing_spec.js
new file mode 100644
index 00000000000..448d8ee2e81
--- /dev/null
+++ b/spec/frontend/landing_spec.js
@@ -0,0 +1,184 @@
+import Cookies from 'js-cookie';
+import Landing from '~/landing';
+
+describe('Landing', () => {
+ const test = {};
+
+ describe('class constructor', () => {
+ beforeEach(() => {
+ test.landingElement = {};
+ test.dismissButton = {};
+ test.cookieName = 'cookie_name';
+
+ test.landing = new Landing(test.landingElement, test.dismissButton, test.cookieName);
+ });
+
+ it('should set .landing', () => {
+ expect(test.landing.landingElement).toBe(test.landingElement);
+ });
+
+ it('should set .cookieName', () => {
+ expect(test.landing.cookieName).toBe(test.cookieName);
+ });
+
+ it('should set .dismissButton', () => {
+ expect(test.landing.dismissButton).toBe(test.dismissButton);
+ });
+
+ it('should set .eventWrapper', () => {
+ expect(test.landing.eventWrapper).toEqual({});
+ });
+ });
+
+ describe('toggle', () => {
+ beforeEach(() => {
+ test.isDismissed = false;
+ test.landingElement = {
+ classList: {
+ toggle: jest.fn(),
+ },
+ };
+ test.landing = {
+ isDismissed: () => {},
+ addEvents: () => {},
+ landingElement: test.landingElement,
+ };
+
+ jest.spyOn(test.landing, 'isDismissed').mockReturnValue(test.isDismissed);
+ jest.spyOn(test.landing, 'addEvents').mockImplementation(() => {});
+
+ Landing.prototype.toggle.call(test.landing);
+ });
+
+ it('should call .isDismissed', () => {
+ expect(test.landing.isDismissed).toHaveBeenCalled();
+ });
+
+ it('should call .classList.toggle', () => {
+ expect(test.landingElement.classList.toggle).toHaveBeenCalledWith('hidden', test.isDismissed);
+ });
+
+ it('should call .addEvents', () => {
+ expect(test.landing.addEvents).toHaveBeenCalled();
+ });
+
+ describe('if isDismissed is true', () => {
+ beforeEach(() => {
+ test.isDismissed = true;
+ test.landingElement = {
+ classList: {
+ toggle: jest.fn(),
+ },
+ };
+ test.landing = {
+ isDismissed: () => {},
+ addEvents: () => {},
+ landingElement: test.landingElement,
+ };
+
+ jest.spyOn(test.landing, 'isDismissed').mockReturnValue(test.isDismissed);
+ jest.spyOn(test.landing, 'addEvents').mockImplementation(() => {});
+
+ test.landing.isDismissed.mockClear();
+
+ Landing.prototype.toggle.call(test.landing);
+ });
+
+ it('should not call .addEvents', () => {
+ expect(test.landing.addEvents).not.toHaveBeenCalled();
+ });
+ });
+ });
+
+ describe('addEvents', () => {
+ beforeEach(() => {
+ test.dismissButton = {
+ addEventListener: jest.fn(),
+ };
+ test.eventWrapper = {};
+ test.landing = {
+ eventWrapper: test.eventWrapper,
+ dismissButton: test.dismissButton,
+ dismissLanding: () => {},
+ };
+
+ Landing.prototype.addEvents.call(test.landing);
+ });
+
+ it('should set .eventWrapper.dismissLanding', () => {
+ expect(test.eventWrapper.dismissLanding).toEqual(expect.any(Function));
+ });
+
+ it('should call .addEventListener', () => {
+ expect(test.dismissButton.addEventListener).toHaveBeenCalledWith(
+ 'click',
+ test.eventWrapper.dismissLanding,
+ );
+ });
+ });
+
+ describe('removeEvents', () => {
+ beforeEach(() => {
+ test.dismissButton = {
+ removeEventListener: jest.fn(),
+ };
+ test.eventWrapper = { dismissLanding: () => {} };
+ test.landing = {
+ eventWrapper: test.eventWrapper,
+ dismissButton: test.dismissButton,
+ };
+
+ Landing.prototype.removeEvents.call(test.landing);
+ });
+
+ it('should call .removeEventListener', () => {
+ expect(test.dismissButton.removeEventListener).toHaveBeenCalledWith(
+ 'click',
+ test.eventWrapper.dismissLanding,
+ );
+ });
+ });
+
+ describe('dismissLanding', () => {
+ beforeEach(() => {
+ test.landingElement = {
+ classList: {
+ add: jest.fn(),
+ },
+ };
+ test.cookieName = 'cookie_name';
+ test.landing = { landingElement: test.landingElement, cookieName: test.cookieName };
+
+ jest.spyOn(Cookies, 'set').mockImplementation(() => {});
+
+ Landing.prototype.dismissLanding.call(test.landing);
+ });
+
+ it('should call .classList.add', () => {
+ expect(test.landingElement.classList.add).toHaveBeenCalledWith('hidden');
+ });
+
+ it('should call Cookies.set', () => {
+ expect(Cookies.set).toHaveBeenCalledWith(test.cookieName, 'true', { expires: 365 });
+ });
+ });
+
+ describe('isDismissed', () => {
+ beforeEach(() => {
+ test.cookieName = 'cookie_name';
+ test.landing = { cookieName: test.cookieName };
+
+ jest.spyOn(Cookies, 'get').mockReturnValue('true');
+
+ test.isDismissed = Landing.prototype.isDismissed.call(test.landing);
+ });
+
+ it('should call Cookies.get', () => {
+ expect(Cookies.get).toHaveBeenCalledWith(test.cookieName);
+ });
+
+ it('should return a boolean', () => {
+ expect(typeof test.isDismissed).toEqual('boolean');
+ });
+ });
+});
diff --git a/spec/frontend/lib/utils/axios_utils_spec.js b/spec/frontend/lib/utils/axios_utils_spec.js
index d5c39567f06..1585a38ae86 100644
--- a/spec/frontend/lib/utils/axios_utils_spec.js
+++ b/spec/frontend/lib/utils/axios_utils_spec.js
@@ -11,6 +11,7 @@ describe('axios_utils', () => {
mock = new AxiosMockAdapter(axios);
mock.onAny('/ok').reply(200);
mock.onAny('/err').reply(500);
+ // eslint-disable-next-line jest/no-standalone-expect
expect(axios.countActiveRequests()).toBe(0);
});
diff --git a/spec/frontend/lib/utils/common_utils_spec.js b/spec/frontend/lib/utils/common_utils_spec.js
index 1edfda30fec..c8dc90c9ace 100644
--- a/spec/frontend/lib/utils/common_utils_spec.js
+++ b/spec/frontend/lib/utils/common_utils_spec.js
@@ -503,7 +503,7 @@ describe('common_utils', () => {
beforeEach(() => {
window.gon = window.gon || {};
- beforeGon = Object.assign({}, window.gon);
+ beforeGon = { ...window.gon };
window.gon.sprite_icons = 'icons.svg';
});
diff --git a/spec/frontend/lib/utils/csrf_token_spec.js b/spec/frontend/lib/utils/csrf_token_spec.js
new file mode 100644
index 00000000000..1b98ef126e9
--- /dev/null
+++ b/spec/frontend/lib/utils/csrf_token_spec.js
@@ -0,0 +1,57 @@
+import csrf from '~/lib/utils/csrf';
+import { setHTMLFixture } from 'helpers/fixtures';
+
+describe('csrf', () => {
+ let testContext;
+
+ beforeEach(() => {
+ testContext = {};
+ });
+
+ beforeEach(() => {
+ testContext.tokenKey = 'X-CSRF-Token';
+ testContext.token =
+ 'pH1cvjnP9grx2oKlhWEDvUZnJ8x2eXsIs1qzyHkF3DugSG5yTxR76CWeEZRhML2D1IeVB7NEW0t5l/axE4iJpQ==';
+ });
+
+ it('returns the correct headerKey', () => {
+ expect(csrf.headerKey).toBe(testContext.tokenKey);
+ });
+
+ describe('when csrf token is in the DOM', () => {
+ beforeEach(() => {
+ setHTMLFixture(`
+ <meta name="csrf-token" content="${testContext.token}">
+ `);
+
+ csrf.init();
+ });
+
+ it('returns the csrf token', () => {
+ expect(csrf.token).toBe(testContext.token);
+ });
+
+ it('returns the csrf headers object', () => {
+ expect(csrf.headers[testContext.tokenKey]).toBe(testContext.token);
+ });
+ });
+
+ describe('when csrf token is not in the DOM', () => {
+ beforeEach(() => {
+ setHTMLFixture(`
+ <meta name="some-other-token">
+ `);
+
+ csrf.init();
+ });
+
+ it('returns null for token', () => {
+ expect(csrf.token).toBeNull();
+ });
+
+ it('returns empty object for headers', () => {
+ expect(typeof csrf.headers).toBe('object');
+ expect(Object.keys(csrf.headers).length).toBe(0);
+ });
+ });
+});
diff --git a/spec/frontend/lib/utils/downloader_spec.js b/spec/frontend/lib/utils/downloader_spec.js
new file mode 100644
index 00000000000..c14cba3a62b
--- /dev/null
+++ b/spec/frontend/lib/utils/downloader_spec.js
@@ -0,0 +1,40 @@
+import downloader from '~/lib/utils/downloader';
+
+describe('Downloader', () => {
+ let a;
+
+ beforeEach(() => {
+ a = { click: jest.fn() };
+ jest.spyOn(document, 'createElement').mockImplementation(() => a);
+ });
+
+ afterEach(() => {
+ jest.clearAllMocks();
+ });
+
+ describe('when inline file content is provided', () => {
+ const fileData = 'inline content';
+ const fileName = 'test.csv';
+
+ it('uses the data urls to download the file', () => {
+ downloader({ fileName, fileData });
+ expect(document.createElement).toHaveBeenCalledWith('a');
+ expect(a.download).toBe(fileName);
+ expect(a.href).toBe(`data:text/plain;base64,${fileData}`);
+ expect(a.click).toHaveBeenCalledTimes(1);
+ });
+ });
+
+ describe('when an endpoint is provided', () => {
+ const url = 'https://gitlab.com/test.csv';
+ const fileName = 'test.csv';
+
+ it('uses the endpoint to download the file', () => {
+ downloader({ fileName, url });
+ expect(document.createElement).toHaveBeenCalledWith('a');
+ expect(a.download).toBe(fileName);
+ expect(a.href).toBe(url);
+ expect(a.click).toHaveBeenCalledTimes(1);
+ });
+ });
+});
diff --git a/spec/frontend/lib/utils/navigation_utility_spec.js b/spec/frontend/lib/utils/navigation_utility_spec.js
new file mode 100644
index 00000000000..88172f38894
--- /dev/null
+++ b/spec/frontend/lib/utils/navigation_utility_spec.js
@@ -0,0 +1,23 @@
+import findAndFollowLink from '~/lib/utils/navigation_utility';
+import { visitUrl } from '~/lib/utils/url_utility';
+
+jest.mock('~/lib/utils/url_utility');
+
+describe('findAndFollowLink', () => {
+ it('visits a link when the selector exists', () => {
+ const href = '/some/path';
+
+ setFixtures(`<a class="my-shortcut" href="${href}">link</a>`);
+
+ findAndFollowLink('.my-shortcut');
+
+ expect(visitUrl).toHaveBeenCalledWith(href);
+ });
+
+ it('does not throw an exception when the selector does not exist', () => {
+ // this should not throw an exception
+ findAndFollowLink('.this-selector-does-not-exist');
+
+ expect(visitUrl).not.toHaveBeenCalled();
+ });
+});
diff --git a/spec/frontend/lib/utils/poll_spec.js b/spec/frontend/lib/utils/poll_spec.js
new file mode 100644
index 00000000000..5ee9738ebf3
--- /dev/null
+++ b/spec/frontend/lib/utils/poll_spec.js
@@ -0,0 +1,225 @@
+import Poll from '~/lib/utils/poll';
+import { successCodes } from '~/lib/utils/http_status';
+import waitForPromises from 'helpers/wait_for_promises';
+
+describe('Poll', () => {
+ let callbacks;
+ let service;
+
+ function setup() {
+ return new Poll({
+ resource: service,
+ method: 'fetch',
+ successCallback: callbacks.success,
+ errorCallback: callbacks.error,
+ notificationCallback: callbacks.notification,
+ }).makeRequest();
+ }
+
+ const mockServiceCall = (response, shouldFail = false) => {
+ const value = {
+ ...response,
+ header: response.header || {},
+ };
+
+ if (shouldFail) {
+ service.fetch.mockRejectedValue(value);
+ } else {
+ service.fetch.mockResolvedValue(value);
+ }
+ };
+
+ const waitForAllCallsToFinish = (waitForCount, successCallback) => {
+ if (!waitForCount) {
+ return Promise.resolve().then(successCallback());
+ }
+
+ jest.runOnlyPendingTimers();
+
+ return waitForPromises().then(() => waitForAllCallsToFinish(waitForCount - 1, successCallback));
+ };
+
+ beforeEach(() => {
+ service = {
+ fetch: jest.fn(),
+ };
+ callbacks = {
+ success: jest.fn(),
+ error: jest.fn(),
+ notification: jest.fn(),
+ };
+ });
+
+ it('calls the success callback when no header for interval is provided', done => {
+ mockServiceCall({ status: 200 });
+ setup();
+
+ waitForAllCallsToFinish(1, () => {
+ expect(callbacks.success).toHaveBeenCalled();
+ expect(callbacks.error).not.toHaveBeenCalled();
+
+ done();
+ });
+ });
+
+ it('calls the error callback when the http request returns an error', done => {
+ mockServiceCall({ status: 500 }, true);
+ setup();
+
+ waitForAllCallsToFinish(1, () => {
+ expect(callbacks.success).not.toHaveBeenCalled();
+ expect(callbacks.error).toHaveBeenCalled();
+
+ done();
+ });
+ });
+
+ it('skips the error callback when request is aborted', done => {
+ mockServiceCall({ status: 0 }, true);
+ setup();
+
+ waitForAllCallsToFinish(1, () => {
+ expect(callbacks.success).not.toHaveBeenCalled();
+ expect(callbacks.error).not.toHaveBeenCalled();
+ expect(callbacks.notification).toHaveBeenCalled();
+
+ done();
+ });
+ });
+
+ it('should call the success callback when the interval header is -1', done => {
+ mockServiceCall({ status: 200, headers: { 'poll-interval': -1 } });
+ setup()
+ .then(() => {
+ expect(callbacks.success).toHaveBeenCalled();
+ expect(callbacks.error).not.toHaveBeenCalled();
+
+ done();
+ })
+ .catch(done.fail);
+ });
+
+ describe('for 2xx status code', () => {
+ successCodes.forEach(httpCode => {
+ it(`starts polling when http status is ${httpCode} and interval header is provided`, done => {
+ mockServiceCall({ status: httpCode, headers: { 'poll-interval': 1 } });
+
+ const Polling = new Poll({
+ resource: service,
+ method: 'fetch',
+ data: { page: 1 },
+ successCallback: callbacks.success,
+ errorCallback: callbacks.error,
+ });
+
+ Polling.makeRequest();
+
+ waitForAllCallsToFinish(2, () => {
+ Polling.stop();
+
+ expect(service.fetch.mock.calls).toHaveLength(2);
+ expect(service.fetch).toHaveBeenCalledWith({ page: 1 });
+ expect(callbacks.success).toHaveBeenCalled();
+ expect(callbacks.error).not.toHaveBeenCalled();
+
+ done();
+ });
+ });
+ });
+ });
+
+ describe('stop', () => {
+ it('stops polling when method is called', done => {
+ mockServiceCall({ status: 200, headers: { 'poll-interval': 1 } });
+
+ const Polling = new Poll({
+ resource: service,
+ method: 'fetch',
+ data: { page: 1 },
+ successCallback: () => {
+ Polling.stop();
+ },
+ errorCallback: callbacks.error,
+ });
+
+ jest.spyOn(Polling, 'stop');
+
+ Polling.makeRequest();
+
+ waitForAllCallsToFinish(1, () => {
+ expect(service.fetch.mock.calls).toHaveLength(1);
+ expect(service.fetch).toHaveBeenCalledWith({ page: 1 });
+ expect(Polling.stop).toHaveBeenCalled();
+
+ done();
+ });
+ });
+ });
+
+ describe('enable', () => {
+ it('should enable polling upon a response', done => {
+ mockServiceCall({ status: 200 });
+ const Polling = new Poll({
+ resource: service,
+ method: 'fetch',
+ data: { page: 1 },
+ successCallback: () => {},
+ });
+
+ Polling.enable({
+ data: { page: 4 },
+ response: { status: 200, headers: { 'poll-interval': 1 } },
+ });
+
+ waitForAllCallsToFinish(1, () => {
+ Polling.stop();
+
+ expect(service.fetch.mock.calls).toHaveLength(1);
+ expect(service.fetch).toHaveBeenCalledWith({ page: 4 });
+ expect(Polling.options.data).toEqual({ page: 4 });
+ done();
+ });
+ });
+ });
+
+ describe('restart', () => {
+ it('should restart polling when its called', done => {
+ mockServiceCall({ status: 200, headers: { 'poll-interval': 1 } });
+
+ const Polling = new Poll({
+ resource: service,
+ method: 'fetch',
+ data: { page: 1 },
+ successCallback: () => {
+ Polling.stop();
+
+ // Let's pretend that we asynchronously restart this.
+ // setTimeout is mocked but this will actually get triggered
+ // in waitForAllCalssToFinish.
+ setTimeout(() => {
+ Polling.restart({ data: { page: 4 } });
+ }, 1);
+ },
+ errorCallback: callbacks.error,
+ });
+
+ jest.spyOn(Polling, 'stop');
+ jest.spyOn(Polling, 'enable');
+ jest.spyOn(Polling, 'restart');
+
+ Polling.makeRequest();
+
+ waitForAllCallsToFinish(2, () => {
+ Polling.stop();
+
+ expect(service.fetch.mock.calls).toHaveLength(2);
+ expect(service.fetch).toHaveBeenCalledWith({ page: 4 });
+ expect(Polling.stop).toHaveBeenCalled();
+ expect(Polling.enable).toHaveBeenCalled();
+ expect(Polling.restart).toHaveBeenCalled();
+ expect(Polling.options.data).toEqual({ page: 4 });
+ done();
+ });
+ });
+ });
+});
diff --git a/spec/frontend/lib/utils/sticky_spec.js b/spec/frontend/lib/utils/sticky_spec.js
new file mode 100644
index 00000000000..4ad68cc9ff6
--- /dev/null
+++ b/spec/frontend/lib/utils/sticky_spec.js
@@ -0,0 +1,77 @@
+import { isSticky } from '~/lib/utils/sticky';
+import { setHTMLFixture } from 'helpers/fixtures';
+
+const TEST_OFFSET_TOP = 500;
+
+describe('sticky', () => {
+ let el;
+ let offsetTop;
+
+ beforeEach(() => {
+ setHTMLFixture(
+ `
+ <div class="parent">
+ <div id="js-sticky"></div>
+ </div>
+ `,
+ );
+
+ offsetTop = TEST_OFFSET_TOP;
+ el = document.getElementById('js-sticky');
+ Object.defineProperty(el, 'offsetTop', {
+ get() {
+ return offsetTop;
+ },
+ });
+ });
+
+ afterEach(() => {
+ el = null;
+ });
+
+ describe('when stuck', () => {
+ it('does not remove is-stuck class', () => {
+ isSticky(el, 0, el.offsetTop);
+ isSticky(el, 0, el.offsetTop);
+
+ expect(el.classList.contains('is-stuck')).toBeTruthy();
+ });
+
+ it('adds is-stuck class', () => {
+ isSticky(el, 0, el.offsetTop);
+
+ expect(el.classList.contains('is-stuck')).toBeTruthy();
+ });
+
+ it('inserts placeholder element', () => {
+ isSticky(el, 0, el.offsetTop, true);
+
+ expect(document.querySelector('.sticky-placeholder')).not.toBeNull();
+ });
+ });
+
+ describe('when not stuck', () => {
+ it('removes is-stuck class', () => {
+ jest.spyOn(el.classList, 'remove');
+
+ isSticky(el, 0, el.offsetTop);
+ isSticky(el, 0, 0);
+
+ expect(el.classList.remove).toHaveBeenCalledWith('is-stuck');
+ expect(el.classList.contains('is-stuck')).toBe(false);
+ });
+
+ it('does not add is-stuck class', () => {
+ isSticky(el, 0, 0);
+
+ expect(el.classList.contains('is-stuck')).toBeFalsy();
+ });
+
+ it('removes placeholder', () => {
+ isSticky(el, 0, el.offsetTop, true);
+ isSticky(el, 0, 0, true);
+
+ expect(document.querySelector('.sticky-placeholder')).toBeNull();
+ });
+ });
+});
diff --git a/spec/frontend/lib/utils/text_markdown_spec.js b/spec/frontend/lib/utils/text_markdown_spec.js
index ba3e4020e66..1d616a7da0b 100644
--- a/spec/frontend/lib/utils/text_markdown_spec.js
+++ b/spec/frontend/lib/utils/text_markdown_spec.js
@@ -25,7 +25,7 @@ describe('init markdown', () => {
insertMarkdownText({
textArea,
text: textArea.value,
- tag: '*',
+ tag: '* ',
blockTag: null,
selected: '',
wrap: false,
@@ -43,7 +43,7 @@ describe('init markdown', () => {
insertMarkdownText({
textArea,
text: textArea.value,
- tag: '*',
+ tag: '* ',
blockTag: null,
selected: '',
wrap: false,
@@ -61,7 +61,7 @@ describe('init markdown', () => {
insertMarkdownText({
textArea,
text: textArea.value,
- tag: '*',
+ tag: '* ',
blockTag: null,
selected: '',
wrap: false,
@@ -79,7 +79,7 @@ describe('init markdown', () => {
insertMarkdownText({
textArea,
text: textArea.value,
- tag: '*',
+ tag: '* ',
blockTag: null,
selected: '',
wrap: false,
diff --git a/spec/frontend/lib/utils/url_utility_spec.js b/spec/frontend/lib/utils/url_utility_spec.js
index 4960895890f..c494033badd 100644
--- a/spec/frontend/lib/utils/url_utility_spec.js
+++ b/spec/frontend/lib/utils/url_utility_spec.js
@@ -91,36 +91,75 @@ describe('URL utility', () => {
});
describe('mergeUrlParams', () => {
+ const { mergeUrlParams } = urlUtils;
+
it('adds w', () => {
- expect(urlUtils.mergeUrlParams({ w: 1 }, '#frag')).toBe('?w=1#frag');
- expect(urlUtils.mergeUrlParams({ w: 1 }, '/path#frag')).toBe('/path?w=1#frag');
- expect(urlUtils.mergeUrlParams({ w: 1 }, 'https://host/path')).toBe('https://host/path?w=1');
- expect(urlUtils.mergeUrlParams({ w: 1 }, 'https://host/path#frag')).toBe(
- 'https://host/path?w=1#frag',
- );
+ expect(mergeUrlParams({ w: 1 }, '#frag')).toBe('?w=1#frag');
+ expect(mergeUrlParams({ w: 1 }, '')).toBe('?w=1');
+ expect(mergeUrlParams({ w: 1 }, '/path#frag')).toBe('/path?w=1#frag');
+ expect(mergeUrlParams({ w: 1 }, 'https://host/path')).toBe('https://host/path?w=1');
+ expect(mergeUrlParams({ w: 1 }, 'https://host/path#frag')).toBe('https://host/path?w=1#frag');
+ expect(mergeUrlParams({ w: 1 }, 'https://h/p?k1=v1#frag')).toBe('https://h/p?k1=v1&w=1#frag');
+ expect(mergeUrlParams({ w: 'null' }, '')).toBe('?w=null');
+ });
- expect(urlUtils.mergeUrlParams({ w: 1 }, 'https://h/p?k1=v1#frag')).toBe(
- 'https://h/p?k1=v1&w=1#frag',
- );
+ it('adds multiple params', () => {
+ expect(mergeUrlParams({ a: 1, b: 2, c: 3 }, '#frag')).toBe('?a=1&b=2&c=3#frag');
});
it('updates w', () => {
- expect(urlUtils.mergeUrlParams({ w: 1 }, '?k1=v1&w=0#frag')).toBe('?k1=v1&w=1#frag');
+ expect(mergeUrlParams({ w: 2 }, '/path?w=1#frag')).toBe('/path?w=2#frag');
+ expect(mergeUrlParams({ w: 2 }, 'https://host/path?w=1')).toBe('https://host/path?w=2');
});
- it('adds multiple params', () => {
- expect(urlUtils.mergeUrlParams({ a: 1, b: 2, c: 3 }, '#frag')).toBe('?a=1&b=2&c=3#frag');
+ it('removes null w', () => {
+ expect(mergeUrlParams({ w: null }, '?w=1#frag')).toBe('#frag');
+ expect(mergeUrlParams({ w: null }, '/path?w=1#frag')).toBe('/path#frag');
+ expect(mergeUrlParams({ w: null }, 'https://host/path?w=1')).toBe('https://host/path');
+ expect(mergeUrlParams({ w: null }, 'https://host/path?w=1#frag')).toBe(
+ 'https://host/path#frag',
+ );
+ expect(mergeUrlParams({ w: null }, 'https://h/p?k1=v1&w=1#frag')).toBe(
+ 'https://h/p?k1=v1#frag',
+ );
});
- it('adds and updates encoded params', () => {
- expect(urlUtils.mergeUrlParams({ a: '&', q: '?' }, '?a=%23#frag')).toBe('?a=%26&q=%3F#frag');
+ it('adds and updates encoded param values', () => {
+ expect(mergeUrlParams({ foo: '&', q: '?' }, '?foo=%23#frag')).toBe('?foo=%26&q=%3F#frag');
+ expect(mergeUrlParams({ foo: 'a value' }, '')).toBe('?foo=a%20value');
+ expect(mergeUrlParams({ foo: 'a value' }, '?foo=1')).toBe('?foo=a%20value');
+ });
+
+ it('adds and updates encoded param names', () => {
+ expect(mergeUrlParams({ 'a name': 1 }, '')).toBe('?a%20name=1');
+ expect(mergeUrlParams({ 'a name': 2 }, '?a%20name=1')).toBe('?a%20name=2');
+ expect(mergeUrlParams({ 'a name': null }, '?a%20name=1')).toBe('');
});
it('treats "+" as "%20"', () => {
- expect(urlUtils.mergeUrlParams({ ref: 'bogus' }, '?a=lorem+ipsum&ref=charlie')).toBe(
+ expect(mergeUrlParams({ ref: 'bogus' }, '?a=lorem+ipsum&ref=charlie')).toBe(
'?a=lorem%20ipsum&ref=bogus',
);
});
+
+ it('treats question marks and slashes as part of the query', () => {
+ expect(mergeUrlParams({ ending: '!' }, '?ending=?&foo=bar')).toBe('?ending=!&foo=bar');
+ expect(mergeUrlParams({ ending: '!' }, 'https://host/path?ending=?&foo=bar')).toBe(
+ 'https://host/path?ending=!&foo=bar',
+ );
+ expect(mergeUrlParams({ ending: '?' }, '?ending=!&foo=bar')).toBe('?ending=%3F&foo=bar');
+ expect(mergeUrlParams({ ending: '?' }, 'https://host/path?ending=!&foo=bar')).toBe(
+ 'https://host/path?ending=%3F&foo=bar',
+ );
+ expect(mergeUrlParams({ ending: '!', op: '+' }, '?ending=?&op=/')).toBe('?ending=!&op=%2B');
+ expect(mergeUrlParams({ ending: '!', op: '+' }, 'https://host/path?ending=?&op=/')).toBe(
+ 'https://host/path?ending=!&op=%2B',
+ );
+ expect(mergeUrlParams({ op: '+' }, '?op=/&foo=bar')).toBe('?op=%2B&foo=bar');
+ expect(mergeUrlParams({ op: '+' }, 'https://host/path?op=/&foo=bar')).toBe(
+ 'https://host/path?op=%2B&foo=bar',
+ );
+ });
});
describe('removeParams', () => {
@@ -284,20 +323,76 @@ describe('URL utility', () => {
});
});
- describe('isAbsoluteOrRootRelative', () => {
- const validUrls = ['https://gitlab.com/', 'http://gitlab.com/', '/users/sign_in'];
-
- const invalidUrls = [' https://gitlab.com/', './file/path', 'notanurl', '<a></a>'];
+ describe('isAbsolute', () => {
+ it.each`
+ url | valid
+ ${'https://gitlab.com/'} | ${true}
+ ${'http://gitlab.com/'} | ${true}
+ ${'/users/sign_in'} | ${false}
+ ${' https://gitlab.com'} | ${false}
+ ${'somepath.php?url=https://gitlab.com'} | ${false}
+ ${'notaurl'} | ${false}
+ ${'../relative_url'} | ${false}
+ ${'<a></a>'} | ${false}
+ `('returns $valid for $url', ({ url, valid }) => {
+ expect(urlUtils.isAbsolute(url)).toBe(valid);
+ });
+ });
- it.each(validUrls)(`returns true for %s`, url => {
- expect(urlUtils.isAbsoluteOrRootRelative(url)).toBe(true);
+ describe('isRootRelative', () => {
+ it.each`
+ url | valid
+ ${'https://gitlab.com/'} | ${false}
+ ${'http://gitlab.com/'} | ${false}
+ ${'/users/sign_in'} | ${true}
+ ${' https://gitlab.com'} | ${false}
+ ${'/somepath.php?url=https://gitlab.com'} | ${true}
+ ${'notaurl'} | ${false}
+ ${'../relative_url'} | ${false}
+ ${'<a></a>'} | ${false}
+ `('returns $valid for $url', ({ url, valid }) => {
+ expect(urlUtils.isRootRelative(url)).toBe(valid);
});
+ });
- it.each(invalidUrls)(`returns false for %s`, url => {
- expect(urlUtils.isAbsoluteOrRootRelative(url)).toBe(false);
+ describe('isAbsoluteOrRootRelative', () => {
+ it.each`
+ url | valid
+ ${'https://gitlab.com/'} | ${true}
+ ${'http://gitlab.com/'} | ${true}
+ ${'/users/sign_in'} | ${true}
+ ${' https://gitlab.com'} | ${false}
+ ${'/somepath.php?url=https://gitlab.com'} | ${true}
+ ${'notaurl'} | ${false}
+ ${'../relative_url'} | ${false}
+ ${'<a></a>'} | ${false}
+ `('returns $valid for $url', ({ url, valid }) => {
+ expect(urlUtils.isAbsoluteOrRootRelative(url)).toBe(valid);
});
});
+ describe('relativePathToAbsolute', () => {
+ it.each`
+ path | base | result
+ ${'./foo'} | ${'bar/'} | ${'/bar/foo'}
+ ${'../john.md'} | ${'bar/baz/foo.php'} | ${'/bar/john.md'}
+ ${'../images/img.png'} | ${'bar/baz/foo.php'} | ${'/bar/images/img.png'}
+ ${'../images/Image 1.png'} | ${'bar/baz/foo.php'} | ${'/bar/images/Image 1.png'}
+ ${'/images/img.png'} | ${'bar/baz/foo.php'} | ${'/images/img.png'}
+ ${'/images/img.png'} | ${'/bar/baz/foo.php'} | ${'/images/img.png'}
+ ${'../john.md'} | ${'/bar/baz/foo.php'} | ${'/bar/john.md'}
+ ${'../john.md'} | ${'///bar/baz/foo.php'} | ${'/bar/john.md'}
+ ${'/images/img.png'} | ${'https://gitlab.com/user/project/'} | ${'https://gitlab.com/images/img.png'}
+ ${'../images/img.png'} | ${'https://gitlab.com/user/project/'} | ${'https://gitlab.com/user/images/img.png'}
+ ${'../images/Image 1.png'} | ${'https://gitlab.com/user/project/'} | ${'https://gitlab.com/user/images/Image%201.png'}
+ `(
+ 'converts relative path "$path" with base "$base" to absolute path => "expected"',
+ ({ path, base, result }) => {
+ expect(urlUtils.relativePathToAbsolute(path, base)).toBe(result);
+ },
+ );
+ });
+
describe('isSafeUrl', () => {
const absoluteUrls = [
'http://example.org',
@@ -386,6 +481,12 @@ describe('URL utility', () => {
expect(urlUtils.queryToObject(searchQuery)).toEqual({ one: '1', two: '2' });
});
+
+ it('removes undefined values from the search query', () => {
+ const searchQuery = '?one=1&two=2&three';
+
+ expect(urlUtils.queryToObject(searchQuery)).toEqual({ one: '1', two: '2' });
+ });
});
describe('objectToQuery', () => {
diff --git a/spec/frontend/milestones/mock_data.js b/spec/frontend/milestones/mock_data.js
new file mode 100644
index 00000000000..c64eeeba663
--- /dev/null
+++ b/spec/frontend/milestones/mock_data.js
@@ -0,0 +1,82 @@
+export const milestones = [
+ {
+ id: 41,
+ iid: 6,
+ project_id: 8,
+ title: 'v0.1',
+ description: '',
+ state: 'active',
+ created_at: '2020-04-04T01:30:40.051Z',
+ updated_at: '2020-04-04T01:30:40.051Z',
+ due_date: null,
+ start_date: null,
+ web_url: 'http://127.0.0.1:3000/h5bp/html5-boilerplate/-/milestones/6',
+ },
+ {
+ id: 40,
+ iid: 5,
+ project_id: 8,
+ title: 'v4.0',
+ description: 'Laboriosam nisi sapiente dolores et magnam nobis ad earum.',
+ state: 'closed',
+ created_at: '2020-01-13T19:39:15.191Z',
+ updated_at: '2020-01-13T19:39:15.191Z',
+ due_date: null,
+ start_date: null,
+ web_url: 'http://127.0.0.1:3000/h5bp/html5-boilerplate/-/milestones/5',
+ },
+ {
+ id: 39,
+ iid: 4,
+ project_id: 8,
+ title: 'v3.0',
+ description: 'Necessitatibus illo alias et repellat dolorum assumenda ut.',
+ state: 'closed',
+ created_at: '2020-01-13T19:39:15.176Z',
+ updated_at: '2020-01-13T19:39:15.176Z',
+ due_date: null,
+ start_date: null,
+ web_url: 'http://127.0.0.1:3000/h5bp/html5-boilerplate/-/milestones/4',
+ },
+ {
+ id: 38,
+ iid: 3,
+ project_id: 8,
+ title: 'v2.0',
+ description: 'Doloribus qui repudiandae iste sit.',
+ state: 'closed',
+ created_at: '2020-01-13T19:39:15.161Z',
+ updated_at: '2020-01-13T19:39:15.161Z',
+ due_date: null,
+ start_date: null,
+ web_url: 'http://127.0.0.1:3000/h5bp/html5-boilerplate/-/milestones/3',
+ },
+ {
+ id: 37,
+ iid: 2,
+ project_id: 8,
+ title: 'v1.0',
+ description: 'Illo sint odio officia ea.',
+ state: 'closed',
+ created_at: '2020-01-13T19:39:15.146Z',
+ updated_at: '2020-01-13T19:39:15.146Z',
+ due_date: null,
+ start_date: null,
+ web_url: 'http://127.0.0.1:3000/h5bp/html5-boilerplate/-/milestones/2',
+ },
+ {
+ id: 36,
+ iid: 1,
+ project_id: 8,
+ title: 'v0.0',
+ description: 'Sed quae facilis deleniti at delectus assumenda nobis veritatis.',
+ state: 'active',
+ created_at: '2020-01-13T19:39:15.127Z',
+ updated_at: '2020-01-13T19:39:15.127Z',
+ due_date: null,
+ start_date: null,
+ web_url: 'http://127.0.0.1:3000/h5bp/html5-boilerplate/-/milestones/1',
+ },
+];
+
+export default milestones;
diff --git a/spec/frontend/milestones/project_milestone_combobox_spec.js b/spec/frontend/milestones/project_milestone_combobox_spec.js
new file mode 100644
index 00000000000..a7321d21559
--- /dev/null
+++ b/spec/frontend/milestones/project_milestone_combobox_spec.js
@@ -0,0 +1,150 @@
+import { milestones as projectMilestones } from './mock_data';
+import axios from 'axios';
+import MockAdapter from 'axios-mock-adapter';
+import { shallowMount } from '@vue/test-utils';
+import MilestoneCombobox from '~/milestones/project_milestone_combobox.vue';
+import { GlNewDropdown, GlLoadingIcon, GlSearchBoxByType } from '@gitlab/ui';
+
+const TEST_SEARCH_ENDPOINT = '/api/v4/projects/8/search';
+
+const extraLinks = [
+ { text: 'Create new', url: 'http://127.0.0.1:3000/h5bp/html5-boilerplate/-/milestones/new' },
+ { text: 'Manage milestones', url: '/h5bp/html5-boilerplate/-/milestones' },
+];
+
+const preselectedMilestones = [];
+const projectId = '8';
+
+describe('Milestone selector', () => {
+ let wrapper;
+ let mock;
+
+ const findNoResultsMessage = () => wrapper.find({ ref: 'noResults' });
+
+ const factory = (options = {}) => {
+ wrapper = shallowMount(MilestoneCombobox, {
+ ...options,
+ });
+ };
+
+ beforeEach(() => {
+ mock = new MockAdapter(axios);
+ gon.api_version = 'v4';
+
+ mock.onGet('/api/v4/projects/8/milestones').reply(200, projectMilestones);
+
+ factory({
+ propsData: {
+ projectId,
+ preselectedMilestones,
+ extraLinks,
+ },
+ });
+ });
+
+ afterEach(() => {
+ mock.restore();
+ wrapper.destroy();
+ wrapper = null;
+ });
+
+ it('renders the dropdown', () => {
+ expect(wrapper.find(GlNewDropdown)).toExist();
+ });
+
+ it('renders additional links', () => {
+ const links = wrapper.findAll('[href]');
+ links.wrappers.forEach((item, idx) => {
+ expect(item.text()).toBe(extraLinks[idx].text);
+ expect(item.attributes('href')).toBe(extraLinks[idx].url);
+ });
+ });
+
+ describe('before results', () => {
+ it('should show a loading icon', () => {
+ const request = mock.onGet(TEST_SEARCH_ENDPOINT, {
+ params: { search: 'TEST_SEARCH', scope: 'milestones' },
+ });
+
+ expect(wrapper.find(GlLoadingIcon).exists()).toBe(true);
+
+ return wrapper.vm.$nextTick().then(() => {
+ request.reply(200, []);
+ });
+ });
+
+ it('should not show any dropdown items', () => {
+ expect(wrapper.findAll('[role="milestone option"]')).toHaveLength(0);
+ });
+
+ it('should have "No milestone" as the button text', () => {
+ expect(wrapper.find({ ref: 'buttonText' }).text()).toBe('No milestone');
+ });
+ });
+
+ describe('with empty results', () => {
+ beforeEach(() => {
+ mock
+ .onGet(TEST_SEARCH_ENDPOINT, { params: { search: 'TEST_SEARCH', scope: 'milestones' } })
+ .reply(200, []);
+ wrapper.find(GlSearchBoxByType).vm.$emit('input', 'TEST_SEARCH');
+ return axios.waitForAll();
+ });
+
+ it('should display that no matching items are found', () => {
+ expect(findNoResultsMessage().exists()).toBe(true);
+ });
+ });
+
+ describe('with results', () => {
+ let items;
+ beforeEach(() => {
+ mock
+ .onGet(TEST_SEARCH_ENDPOINT, { params: { search: 'v0.1', scope: 'milestones' } })
+ .reply(200, [
+ {
+ id: 41,
+ iid: 6,
+ project_id: 8,
+ title: 'v0.1',
+ description: '',
+ state: 'active',
+ created_at: '2020-04-04T01:30:40.051Z',
+ updated_at: '2020-04-04T01:30:40.051Z',
+ due_date: null,
+ start_date: null,
+ web_url: 'http://127.0.0.1:3000/h5bp/html5-boilerplate/-/milestones/6',
+ },
+ ]);
+ wrapper.find(GlSearchBoxByType).vm.$emit('input', 'v0.1');
+ return axios.waitForAll().then(() => {
+ items = wrapper.findAll('[role="milestone option"]');
+ });
+ });
+
+ it('should display one item per result', () => {
+ expect(items).toHaveLength(1);
+ });
+
+ it('should emit a change if an item is clicked', () => {
+ items.at(0).vm.$emit('click');
+ expect(wrapper.emitted().change.length).toBe(1);
+ expect(wrapper.emitted().change[0]).toEqual([[{ title: 'v0.1' }]]);
+ });
+
+ it('should not have a selecton icon on any item', () => {
+ items.wrappers.forEach(item => {
+ expect(item.find('.selected-item').exists()).toBe(false);
+ });
+ });
+
+ it('should have a selecton icon if an item is clicked', () => {
+ items.at(0).vm.$emit('click');
+ expect(wrapper.find('.selected-item').exists()).toBe(true);
+ });
+
+ it('should not display a message about no results', () => {
+ expect(findNoResultsMessage().exists()).toBe(false);
+ });
+ });
+});
diff --git a/spec/frontend/mocks/ce/diffs/workers/tree_worker.js b/spec/frontend/mocks/ce/diffs/workers/tree_worker.js
index a33ddbbfe63..5532a22f8e6 100644
--- a/spec/frontend/mocks/ce/diffs/workers/tree_worker.js
+++ b/spec/frontend/mocks/ce/diffs/workers/tree_worker.js
@@ -1,8 +1 @@
-/* eslint-disable class-methods-use-this */
-export default class TreeWorkerMock {
- addEventListener() {}
-
- terminate() {}
-
- postMessage() {}
-}
+export { default } from 'helpers/web_worker_mock';
diff --git a/spec/frontend/mocks/ce/ide/lib/diff/diff_worker.js b/spec/frontend/mocks/ce/ide/lib/diff/diff_worker.js
new file mode 100644
index 00000000000..5532a22f8e6
--- /dev/null
+++ b/spec/frontend/mocks/ce/ide/lib/diff/diff_worker.js
@@ -0,0 +1 @@
+export { default } from 'helpers/web_worker_mock';
diff --git a/spec/frontend/mocks_spec.js b/spec/frontend/mocks_spec.js
index a4a1fdea396..110c418e579 100644
--- a/spec/frontend/mocks_spec.js
+++ b/spec/frontend/mocks_spec.js
@@ -8,12 +8,13 @@ describe('Mock auto-injection', () => {
failMock = jest.spyOn(global, 'fail').mockImplementation();
});
- it('~/lib/utils/axios_utils', done => {
- expect(axios.get('http://gitlab.com')).rejects.toThrow('Unexpected unmocked request');
- setImmediate(() => {
- expect(failMock).toHaveBeenCalledTimes(1);
- done();
- });
+ it('~/lib/utils/axios_utils', () => {
+ return Promise.all([
+ expect(axios.get('http://gitlab.com')).rejects.toThrow('Unexpected unmocked request'),
+ setImmediate(() => {
+ expect(failMock).toHaveBeenCalledTimes(1);
+ }),
+ ]);
});
it('jQuery.ajax()', () => {
diff --git a/spec/frontend/monitoring/__snapshots__/alert_widget_spec.js.snap b/spec/frontend/monitoring/__snapshots__/alert_widget_spec.js.snap
new file mode 100644
index 00000000000..2179e7b4ab5
--- /dev/null
+++ b/spec/frontend/monitoring/__snapshots__/alert_widget_spec.js.snap
@@ -0,0 +1,43 @@
+// Jest Snapshot v1, https://goo.gl/fbAQLP
+
+exports[`AlertWidget Alert firing displays a warning icon and matches snapshot 1`] = `
+<gl-badge-stub
+ class="d-flex-center text-truncate"
+ pill=""
+ variant="danger"
+>
+ <gl-icon-stub
+ class="flex-shrink-0"
+ name="warning"
+ size="16"
+ />
+
+ <span
+ class="text-truncate gl-pl-1-deprecated-no-really-do-not-use-me"
+ >
+ Firing:
+ alert-label &gt; 42
+
+ </span>
+</gl-badge-stub>
+`;
+
+exports[`AlertWidget Alert not firing displays a warning icon and matches snapshot 1`] = `
+<gl-badge-stub
+ class="d-flex-center text-truncate"
+ pill=""
+ variant="secondary"
+>
+ <gl-icon-stub
+ class="flex-shrink-0"
+ name="warning"
+ size="16"
+ />
+
+ <span
+ class="text-truncate gl-pl-1-deprecated-no-really-do-not-use-me"
+ >
+ alert-label &gt; 42
+ </span>
+</gl-badge-stub>
+`;
diff --git a/spec/frontend/monitoring/alert_widget_spec.js b/spec/frontend/monitoring/alert_widget_spec.js
new file mode 100644
index 00000000000..f0355dfa01b
--- /dev/null
+++ b/spec/frontend/monitoring/alert_widget_spec.js
@@ -0,0 +1,422 @@
+import { shallowMount } from '@vue/test-utils';
+import { GlLoadingIcon, GlTooltip, GlSprintf, GlBadge } from '@gitlab/ui';
+import AlertWidget from '~/monitoring/components/alert_widget.vue';
+import waitForPromises from 'helpers/wait_for_promises';
+import createFlash from '~/flash';
+
+const mockReadAlert = jest.fn();
+const mockCreateAlert = jest.fn();
+const mockUpdateAlert = jest.fn();
+const mockDeleteAlert = jest.fn();
+
+jest.mock('~/flash');
+jest.mock(
+ '~/monitoring/services/alerts_service',
+ () =>
+ function AlertsServiceMock() {
+ return {
+ readAlert: mockReadAlert,
+ createAlert: mockCreateAlert,
+ updateAlert: mockUpdateAlert,
+ deleteAlert: mockDeleteAlert,
+ };
+ },
+);
+
+describe('AlertWidget', () => {
+ let wrapper;
+
+ const nonFiringAlertResult = [
+ {
+ values: [[0, 1], [1, 42], [2, 41]],
+ },
+ ];
+ const firingAlertResult = [
+ {
+ values: [[0, 42], [1, 43], [2, 44]],
+ },
+ ];
+ const metricId = '5';
+ const alertPath = 'my/alert.json';
+
+ const relevantQueries = [
+ {
+ metricId,
+ label: 'alert-label',
+ alert_path: alertPath,
+ result: nonFiringAlertResult,
+ },
+ ];
+
+ const firingRelevantQueries = [
+ {
+ metricId,
+ label: 'alert-label',
+ alert_path: alertPath,
+ result: firingAlertResult,
+ },
+ ];
+
+ const defaultProps = {
+ alertsEndpoint: '',
+ relevantQueries,
+ alertsToManage: {},
+ modalId: 'alert-modal-1',
+ };
+
+ const propsWithAlert = {
+ relevantQueries,
+ };
+
+ const propsWithAlertData = {
+ relevantQueries,
+ alertsToManage: {
+ [alertPath]: { operator: '>', threshold: 42, alert_path: alertPath, metricId },
+ },
+ };
+
+ const createComponent = propsData => {
+ wrapper = shallowMount(AlertWidget, {
+ stubs: { GlTooltip, GlSprintf },
+ propsData: {
+ ...defaultProps,
+ ...propsData,
+ },
+ });
+ };
+ const hasLoadingIcon = () => wrapper.contains(GlLoadingIcon);
+ const findWidgetForm = () => wrapper.find({ ref: 'widgetForm' });
+ const findAlertErrorMessage = () => wrapper.find({ ref: 'alertErrorMessage' });
+ const findCurrentSettingsText = () =>
+ wrapper
+ .find({ ref: 'alertCurrentSetting' })
+ .text()
+ .replace(/\s\s+/g, ' ');
+ const findBadge = () => wrapper.find(GlBadge);
+ const findTooltip = () => wrapper.find(GlTooltip);
+
+ afterEach(() => {
+ wrapper.destroy();
+ wrapper = null;
+ });
+
+ it('displays a loading spinner and disables form when fetching alerts', () => {
+ let resolveReadAlert;
+ mockReadAlert.mockReturnValue(
+ new Promise(resolve => {
+ resolveReadAlert = resolve;
+ }),
+ );
+ createComponent(defaultProps);
+ return wrapper.vm
+ .$nextTick()
+ .then(() => {
+ expect(hasLoadingIcon()).toBe(true);
+ expect(findWidgetForm().props('disabled')).toBe(true);
+
+ resolveReadAlert({ operator: '==', threshold: 42 });
+ })
+ .then(() => waitForPromises())
+ .then(() => {
+ expect(hasLoadingIcon()).toBe(false);
+ expect(findWidgetForm().props('disabled')).toBe(false);
+ });
+ });
+
+ it('does not render loading spinner if showLoadingState is false', () => {
+ let resolveReadAlert;
+ mockReadAlert.mockReturnValue(
+ new Promise(resolve => {
+ resolveReadAlert = resolve;
+ }),
+ );
+ createComponent({
+ ...defaultProps,
+ showLoadingState: false,
+ });
+ return wrapper.vm
+ .$nextTick()
+ .then(() => {
+ expect(wrapper.find(GlLoadingIcon).exists()).toBe(false);
+
+ resolveReadAlert({ operator: '==', threshold: 42 });
+ })
+ .then(() => waitForPromises())
+ .then(() => {
+ expect(wrapper.find(GlLoadingIcon).exists()).toBe(false);
+ });
+ });
+
+ it('displays an error message when fetch fails', () => {
+ mockReadAlert.mockRejectedValue();
+ createComponent(propsWithAlert);
+ expect(hasLoadingIcon()).toBe(true);
+
+ return waitForPromises().then(() => {
+ expect(createFlash).toHaveBeenCalled();
+ expect(hasLoadingIcon()).toBe(false);
+ });
+ });
+
+ describe('Alert not firing', () => {
+ it('displays a warning icon and matches snapshot', () => {
+ mockReadAlert.mockResolvedValue({ operator: '>', threshold: 42 });
+ createComponent(propsWithAlertData);
+
+ return waitForPromises().then(() => {
+ expect(findBadge().element).toMatchSnapshot();
+ });
+ });
+
+ it('displays an alert summary when there is a single alert', () => {
+ mockReadAlert.mockResolvedValue({ operator: '>', threshold: 42 });
+ createComponent(propsWithAlertData);
+ return waitForPromises().then(() => {
+ expect(findCurrentSettingsText()).toEqual('alert-label > 42');
+ });
+ });
+
+ it('displays a combined alert summary when there are multiple alerts', () => {
+ mockReadAlert.mockResolvedValue({ operator: '>', threshold: 42 });
+ const propsWithManyAlerts = {
+ relevantQueries: [
+ ...relevantQueries,
+ ...[
+ {
+ metricId: '6',
+ alert_path: 'my/alert2.json',
+ label: 'alert-label2',
+ result: [{ values: [] }],
+ },
+ ],
+ ],
+ alertsToManage: {
+ 'my/alert.json': {
+ operator: '>',
+ threshold: 42,
+ alert_path: alertPath,
+ metricId,
+ },
+ 'my/alert2.json': {
+ operator: '==',
+ threshold: 900,
+ alert_path: 'my/alert2.json',
+ metricId: '6',
+ },
+ },
+ };
+ createComponent(propsWithManyAlerts);
+ return waitForPromises().then(() => {
+ expect(findCurrentSettingsText()).toContain('2 alerts applied');
+ });
+ });
+ });
+
+ describe('Alert firing', () => {
+ it('displays a warning icon and matches snapshot', () => {
+ mockReadAlert.mockResolvedValue({ operator: '>', threshold: 42 });
+ propsWithAlertData.relevantQueries = firingRelevantQueries;
+ createComponent(propsWithAlertData);
+
+ return waitForPromises().then(() => {
+ expect(findBadge().element).toMatchSnapshot();
+ });
+ });
+
+ it('displays an alert summary when there is a single alert', () => {
+ mockReadAlert.mockResolvedValue({ operator: '>', threshold: 42 });
+ propsWithAlertData.relevantQueries = firingRelevantQueries;
+ createComponent(propsWithAlertData);
+ return waitForPromises().then(() => {
+ expect(findCurrentSettingsText()).toEqual('Firing: alert-label > 42');
+ });
+ });
+
+ it('displays a combined alert summary when there are multiple alerts', () => {
+ mockReadAlert.mockResolvedValue({ operator: '>', threshold: 42 });
+ const propsWithManyAlerts = {
+ relevantQueries: [
+ ...firingRelevantQueries,
+ ...[
+ {
+ metricId: '6',
+ alert_path: 'my/alert2.json',
+ label: 'alert-label2',
+ result: [{ values: [] }],
+ },
+ ],
+ ],
+ alertsToManage: {
+ 'my/alert.json': {
+ operator: '>',
+ threshold: 42,
+ alert_path: alertPath,
+ metricId,
+ },
+ 'my/alert2.json': {
+ operator: '==',
+ threshold: 900,
+ alert_path: 'my/alert2.json',
+ metricId: '6',
+ },
+ },
+ };
+ createComponent(propsWithManyAlerts);
+
+ return waitForPromises().then(() => {
+ expect(findCurrentSettingsText()).toContain('2 alerts applied, 1 firing');
+ });
+ });
+
+ it('should display tooltip with thresholds summary', () => {
+ mockReadAlert.mockResolvedValue({ operator: '>', threshold: 42 });
+ const propsWithManyAlerts = {
+ relevantQueries: [
+ ...firingRelevantQueries,
+ ...[
+ {
+ metricId: '6',
+ alert_path: 'my/alert2.json',
+ label: 'alert-label2',
+ result: [{ values: [] }],
+ },
+ ],
+ ],
+ alertsToManage: {
+ 'my/alert.json': {
+ operator: '>',
+ threshold: 42,
+ alert_path: alertPath,
+ metricId,
+ },
+ 'my/alert2.json': {
+ operator: '==',
+ threshold: 900,
+ alert_path: 'my/alert2.json',
+ metricId: '6',
+ },
+ },
+ };
+ createComponent(propsWithManyAlerts);
+
+ return waitForPromises().then(() => {
+ expect(
+ findTooltip()
+ .text()
+ .replace(/\s\s+/g, ' '),
+ ).toEqual('Firing: alert-label > 42');
+ });
+ });
+ });
+
+ it('creates an alert with an appropriate handler', () => {
+ const alertParams = {
+ operator: '<',
+ threshold: 4,
+ prometheus_metric_id: '5',
+ };
+ mockReadAlert.mockResolvedValue({ operator: '>', threshold: 42 });
+ const fakeAlertPath = 'foo/bar';
+ mockCreateAlert.mockResolvedValue({ alert_path: fakeAlertPath, ...alertParams });
+ createComponent({
+ alertsToManage: {
+ [fakeAlertPath]: {
+ alert_path: fakeAlertPath,
+ operator: '<',
+ threshold: 4,
+ prometheus_metric_id: '5',
+ metricId: '5',
+ },
+ },
+ });
+
+ findWidgetForm().vm.$emit('create', alertParams);
+
+ expect(mockCreateAlert).toHaveBeenCalledWith(alertParams);
+ });
+
+ it('updates an alert with an appropriate handler', () => {
+ const alertParams = { operator: '<', threshold: 4, alert_path: alertPath };
+ const newAlertParams = { operator: '==', threshold: 12 };
+ mockReadAlert.mockResolvedValue(alertParams);
+ mockUpdateAlert.mockResolvedValue({ ...alertParams, ...newAlertParams });
+ createComponent({
+ ...propsWithAlertData,
+ alertsToManage: {
+ [alertPath]: {
+ alert_path: alertPath,
+ operator: '==',
+ threshold: 12,
+ metricId: '5',
+ },
+ },
+ });
+
+ findWidgetForm().vm.$emit('update', {
+ alert: alertPath,
+ ...newAlertParams,
+ prometheus_metric_id: '5',
+ });
+
+ expect(mockUpdateAlert).toHaveBeenCalledWith(alertPath, newAlertParams);
+ });
+
+ it('deletes an alert with an appropriate handler', () => {
+ const alertParams = { alert_path: alertPath, operator: '>', threshold: 42 };
+ mockReadAlert.mockResolvedValue(alertParams);
+ mockDeleteAlert.mockResolvedValue({});
+ createComponent({
+ ...propsWithAlert,
+ alertsToManage: {
+ [alertPath]: {
+ alert_path: alertPath,
+ operator: '>',
+ threshold: 42,
+ metricId: '5',
+ },
+ },
+ });
+
+ findWidgetForm().vm.$emit('delete', { alert: alertPath });
+
+ return wrapper.vm.$nextTick().then(() => {
+ expect(mockDeleteAlert).toHaveBeenCalledWith(alertPath);
+ expect(findAlertErrorMessage().exists()).toBe(false);
+ });
+ });
+
+ describe('when delete fails', () => {
+ beforeEach(() => {
+ const alertParams = { alert_path: alertPath, operator: '>', threshold: 42 };
+ mockReadAlert.mockResolvedValue(alertParams);
+ mockDeleteAlert.mockRejectedValue();
+
+ createComponent({
+ ...propsWithAlert,
+ alertsToManage: {
+ [alertPath]: {
+ alert_path: alertPath,
+ operator: '>',
+ threshold: 42,
+ metricId: '5',
+ },
+ },
+ });
+
+ findWidgetForm().vm.$emit('delete', { alert: alertPath });
+ return wrapper.vm.$nextTick();
+ });
+
+ it('shows error message', () => {
+ expect(findAlertErrorMessage().text()).toEqual('Error deleting alert');
+ });
+
+ it('dismisses error message on cancel', () => {
+ findWidgetForm().vm.$emit('cancel');
+
+ return wrapper.vm.$nextTick().then(() => {
+ expect(findAlertErrorMessage().exists()).toBe(false);
+ });
+ });
+ });
+});
diff --git a/spec/frontend/monitoring/components/__snapshots__/dashboard_template_spec.js.snap b/spec/frontend/monitoring/components/__snapshots__/dashboard_template_spec.js.snap
index 1906ad7c6ed..9be5fa72110 100644
--- a/spec/frontend/monitoring/components/__snapshots__/dashboard_template_spec.js.snap
+++ b/spec/frontend/monitoring/components/__snapshots__/dashboard_template_spec.js.snap
@@ -16,7 +16,6 @@ exports[`Dashboard template matches the default snapshot 1`] = `
data-qa-selector="dashboards_filter_dropdown"
defaultbranch="master"
id="monitor-dashboards-dropdown"
- selecteddashboard="[object Object]"
toggle-class="dropdown-menu-toggle"
/>
</div>
@@ -72,7 +71,7 @@ exports[`Dashboard template matches the default snapshot 1`] = `
<date-time-picker-stub
class="flex-grow-1 show-last-dropdown"
customenabled="true"
- data-qa-selector="show_last_dropdown"
+ data-qa-selector="range_picker_dropdown"
options="[object Object],[object Object],[object Object],[object Object],[object Object],[object Object],[object Object]"
value="[object Object]"
/>
@@ -101,6 +100,26 @@ exports[`Dashboard template matches the default snapshot 1`] = `
<div
class="d-sm-flex"
>
+ <div
+ class="mb-2 mr-2 d-flex"
+ >
+ <div
+ class="flex-grow-1"
+ title="Star dashboard"
+ >
+ <gl-deprecated-button-stub
+ class="w-100"
+ size="md"
+ variant="default"
+ >
+ <gl-icon-stub
+ name="star-o"
+ size="16"
+ />
+ </gl-deprecated-button-stub>
+ </div>
+ </div>
+
<!---->
<!---->
@@ -111,6 +130,8 @@ exports[`Dashboard template matches the default snapshot 1`] = `
</div>
</div>
+ <!---->
+
<empty-state-stub
clusterspath="/path/to/clusters"
documentationpath="/path/to/docs"
diff --git a/spec/frontend/monitoring/components/alert_widget_form_spec.js b/spec/frontend/monitoring/components/alert_widget_form_spec.js
new file mode 100644
index 00000000000..a8416216a94
--- /dev/null
+++ b/spec/frontend/monitoring/components/alert_widget_form_spec.js
@@ -0,0 +1,220 @@
+import { shallowMount } from '@vue/test-utils';
+import { GlLink } from '@gitlab/ui';
+import AlertWidgetForm from '~/monitoring/components/alert_widget_form.vue';
+import ModalStub from '../stubs/modal_stub';
+
+describe('AlertWidgetForm', () => {
+ let wrapper;
+
+ const metricId = '8';
+ const alertPath = 'alert';
+ const relevantQueries = [{ metricId, alert_path: alertPath, label: 'alert-label' }];
+ const dataTrackingOptions = {
+ create: { action: 'click_button', label: 'create_alert' },
+ delete: { action: 'click_button', label: 'delete_alert' },
+ update: { action: 'click_button', label: 'update_alert' },
+ };
+
+ const defaultProps = {
+ disabled: false,
+ relevantQueries,
+ modalId: 'alert-modal-1',
+ };
+
+ const propsWithAlertData = {
+ ...defaultProps,
+ alertsToManage: {
+ alert: { alert_path: alertPath, operator: '<', threshold: 5, metricId },
+ },
+ configuredAlert: metricId,
+ };
+
+ function createComponent(props = {}) {
+ const propsData = {
+ ...defaultProps,
+ ...props,
+ };
+
+ wrapper = shallowMount(AlertWidgetForm, {
+ propsData,
+ stubs: {
+ GlModal: ModalStub,
+ },
+ });
+ }
+
+ const modal = () => wrapper.find(ModalStub);
+ const modalTitle = () => modal().attributes('title');
+ const submitButton = () => modal().find(GlLink);
+ const submitButtonTrackingOpts = () =>
+ JSON.parse(submitButton().attributes('data-tracking-options'));
+ const e = {
+ preventDefault: jest.fn(),
+ };
+
+ beforeEach(() => {
+ e.preventDefault.mockReset();
+ });
+
+ afterEach(() => {
+ if (wrapper) wrapper.destroy();
+ });
+
+ it('disables the form when disabled prop is set', () => {
+ createComponent({ disabled: true });
+
+ expect(modal().attributes('ok-disabled')).toBe('true');
+ });
+
+ it('disables the form if no query is selected', () => {
+ createComponent();
+
+ expect(modal().attributes('ok-disabled')).toBe('true');
+ });
+
+ it('shows correct title and button text', () => {
+ expect(modalTitle()).toBe('Add alert');
+ expect(submitButton().text()).toBe('Add');
+ });
+
+ it('sets tracking options for create alert', () => {
+ expect(submitButtonTrackingOpts()).toEqual(dataTrackingOptions.create);
+ });
+
+ it('emits a "create" event when form submitted without existing alert', () => {
+ createComponent();
+
+ wrapper.vm.selectQuery('9');
+ wrapper.setData({
+ threshold: 900,
+ });
+
+ wrapper.vm.handleSubmit(e);
+
+ expect(wrapper.emitted().create[0]).toEqual([
+ {
+ alert: undefined,
+ operator: '>',
+ threshold: 900,
+ prometheus_metric_id: '9',
+ },
+ ]);
+ expect(e.preventDefault).toHaveBeenCalledTimes(1);
+ });
+
+ it('resets form when modal is dismissed (hidden)', () => {
+ createComponent();
+
+ wrapper.vm.selectQuery('9');
+ wrapper.vm.selectQuery('>');
+ wrapper.setData({
+ threshold: 800,
+ });
+
+ modal().vm.$emit('hidden');
+
+ expect(wrapper.vm.selectedAlert).toEqual({});
+ expect(wrapper.vm.operator).toBe(null);
+ expect(wrapper.vm.threshold).toBe(null);
+ expect(wrapper.vm.prometheusMetricId).toBe(null);
+ });
+
+ it('sets selectedAlert to the provided configuredAlert on modal show', () => {
+ createComponent(propsWithAlertData);
+
+ modal().vm.$emit('shown');
+
+ expect(wrapper.vm.selectedAlert).toEqual(propsWithAlertData.alertsToManage[alertPath]);
+ });
+
+ it('sets selectedAlert to the first relevantQueries if there is only one option on modal show', () => {
+ createComponent({
+ ...propsWithAlertData,
+ configuredAlert: '',
+ });
+
+ modal().vm.$emit('shown');
+
+ expect(wrapper.vm.selectedAlert).toEqual(propsWithAlertData.alertsToManage[alertPath]);
+ });
+
+ it('does not set selectedAlert to the first relevantQueries if there is more than one option on modal show', () => {
+ createComponent({
+ relevantQueries: [
+ {
+ metricId: '8',
+ alertPath: 'alert',
+ label: 'alert-label',
+ },
+ {
+ metricId: '9',
+ alertPath: 'alert',
+ label: 'alert-label',
+ },
+ ],
+ });
+
+ modal().vm.$emit('shown');
+
+ expect(wrapper.vm.selectedAlert).toEqual({});
+ });
+
+ describe('with existing alert', () => {
+ beforeEach(() => {
+ createComponent(propsWithAlertData);
+
+ wrapper.vm.selectQuery(metricId);
+ });
+
+ it('sets tracking options for delete alert', () => {
+ expect(submitButtonTrackingOpts()).toEqual(dataTrackingOptions.delete);
+ });
+
+ it('updates button text', () => {
+ expect(modalTitle()).toBe('Edit alert');
+ expect(submitButton().text()).toBe('Delete');
+ });
+
+ it('emits "delete" event when form values unchanged', () => {
+ wrapper.vm.handleSubmit(e);
+
+ expect(wrapper.emitted().delete[0]).toEqual([
+ {
+ alert: 'alert',
+ operator: '<',
+ threshold: 5,
+ prometheus_metric_id: '8',
+ },
+ ]);
+ expect(e.preventDefault).toHaveBeenCalledTimes(1);
+ });
+
+ it('emits "update" event when form changed', () => {
+ wrapper.setData({
+ threshold: 11,
+ });
+
+ wrapper.vm.handleSubmit(e);
+
+ expect(wrapper.emitted().update[0]).toEqual([
+ {
+ alert: 'alert',
+ operator: '<',
+ threshold: 11,
+ prometheus_metric_id: '8',
+ },
+ ]);
+ expect(e.preventDefault).toHaveBeenCalledTimes(1);
+ });
+
+ it('sets tracking options for update alert', () => {
+ wrapper.setData({
+ threshold: 11,
+ });
+
+ return wrapper.vm.$nextTick(() => {
+ expect(submitButtonTrackingOpts()).toEqual(dataTrackingOptions.update);
+ });
+ });
+ });
+});
diff --git a/spec/frontend/monitoring/components/charts/single_stat_spec.js b/spec/frontend/monitoring/components/charts/single_stat_spec.js
index fb0682d0338..9cc5970da82 100644
--- a/spec/frontend/monitoring/components/charts/single_stat_spec.js
+++ b/spec/frontend/monitoring/components/charts/single_stat_spec.js
@@ -1,6 +1,6 @@
import { shallowMount } from '@vue/test-utils';
import SingleStatChart from '~/monitoring/components/charts/single_stat.vue';
-import { graphDataPrometheusQuery } from '../../mock_data';
+import { singleStatMetricsResult } from '../../mock_data';
describe('Single Stat Chart component', () => {
let singleStatChart;
@@ -8,7 +8,7 @@ describe('Single Stat Chart component', () => {
beforeEach(() => {
singleStatChart = shallowMount(SingleStatChart, {
propsData: {
- graphData: graphDataPrometheusQuery,
+ graphData: singleStatMetricsResult,
},
});
});
@@ -26,7 +26,7 @@ describe('Single Stat Chart component', () => {
it('should change the value representation to a percentile one', () => {
singleStatChart.setProps({
graphData: {
- ...graphDataPrometheusQuery,
+ ...singleStatMetricsResult,
maxValue: 120,
},
});
@@ -37,7 +37,7 @@ describe('Single Stat Chart component', () => {
it('should display NaN for non numeric maxValue values', () => {
singleStatChart.setProps({
graphData: {
- ...graphDataPrometheusQuery,
+ ...singleStatMetricsResult,
maxValue: 'not a number',
},
});
@@ -48,13 +48,13 @@ describe('Single Stat Chart component', () => {
it('should display NaN for missing query values', () => {
singleStatChart.setProps({
graphData: {
- ...graphDataPrometheusQuery,
+ ...singleStatMetricsResult,
metrics: [
{
- ...graphDataPrometheusQuery.metrics[0],
+ ...singleStatMetricsResult.metrics[0],
result: [
{
- ...graphDataPrometheusQuery.metrics[0].result[0],
+ ...singleStatMetricsResult.metrics[0].result[0],
value: [''],
},
],
diff --git a/spec/frontend/monitoring/components/charts/time_series_spec.js b/spec/frontend/monitoring/components/charts/time_series_spec.js
index 5ac716b0c63..7d5a08bc4a1 100644
--- a/spec/frontend/monitoring/components/charts/time_series_spec.js
+++ b/spec/frontend/monitoring/components/charts/time_series_spec.js
@@ -1,4 +1,4 @@
-import { mount } from '@vue/test-utils';
+import { mount, shallowMount } from '@vue/test-utils';
import { setTestTimeout } from 'helpers/timeout';
import { GlLink } from '@gitlab/ui';
import { TEST_HOST } from 'jest/helpers/test_constants';
@@ -11,6 +11,7 @@ import {
import { cloneDeep } from 'lodash';
import { shallowWrapperContainsSlotText } from 'helpers/vue_test_utils_helper';
import { createStore } from '~/monitoring/stores';
+import { panelTypes, chartHeight } from '~/monitoring/constants';
import TimeSeries from '~/monitoring/components/charts/time_series.vue';
import * as types from '~/monitoring/stores/mutation_types';
import { deploymentData, mockProjectDir, annotationsData } from '../../mock_data';
@@ -39,10 +40,10 @@ describe('Time series component', () => {
let mockGraphData;
let store;
- const makeTimeSeriesChart = (graphData, type) =>
- mount(TimeSeries, {
+ const createWrapper = (graphData = mockGraphData, mountingMethod = shallowMount) =>
+ mountingMethod(TimeSeries, {
propsData: {
- graphData: { ...graphData, type },
+ graphData,
deploymentData: store.state.monitoringDashboard.deploymentData,
annotations: store.state.monitoringDashboard.annotations,
projectPath: `${TEST_HOST}${mockProjectDir}`,
@@ -79,9 +80,9 @@ describe('Time series component', () => {
const findChart = () => timeSeriesChart.find({ ref: 'chart' });
- beforeEach(done => {
- timeSeriesChart = makeTimeSeriesChart(mockGraphData, 'area-chart');
- timeSeriesChart.vm.$nextTick(done);
+ beforeEach(() => {
+ timeSeriesChart = createWrapper(mockGraphData, mount);
+ return timeSeriesChart.vm.$nextTick();
});
it('allows user to override max value label text using prop', () => {
@@ -100,6 +101,21 @@ describe('Time series component', () => {
});
});
+ it('chart sets a default height', () => {
+ const wrapper = createWrapper();
+ expect(wrapper.props('height')).toBe(chartHeight);
+ });
+
+ it('chart has a configurable height', () => {
+ const mockHeight = 599;
+ const wrapper = createWrapper();
+
+ wrapper.setProps({ height: mockHeight });
+ return wrapper.vm.$nextTick().then(() => {
+ expect(wrapper.props('height')).toBe(mockHeight);
+ });
+ });
+
describe('events', () => {
describe('datazoom', () => {
let eChartMock;
@@ -125,7 +141,7 @@ describe('Time series component', () => {
}),
};
- timeSeriesChart = makeTimeSeriesChart(mockGraphData);
+ timeSeriesChart = createWrapper(mockGraphData, mount);
timeSeriesChart.vm.$nextTick(() => {
findChart().vm.$emit('created', eChartMock);
done();
@@ -535,11 +551,11 @@ describe('Time series component', () => {
describe('wrapped components', () => {
const glChartComponents = [
{
- chartType: 'area-chart',
+ chartType: panelTypes.AREA_CHART,
component: GlAreaChart,
},
{
- chartType: 'line-chart',
+ chartType: panelTypes.LINE_CHART,
component: GlLineChart,
},
];
@@ -550,7 +566,10 @@ describe('Time series component', () => {
const findChartComponent = () => timeSeriesAreaChart.find(dynamicComponent.component);
beforeEach(done => {
- timeSeriesAreaChart = makeTimeSeriesChart(mockGraphData, dynamicComponent.chartType);
+ timeSeriesAreaChart = createWrapper(
+ { ...mockGraphData, type: dynamicComponent.chartType },
+ mount,
+ );
timeSeriesAreaChart.vm.$nextTick(done);
});
@@ -632,7 +651,7 @@ describe('Time series component', () => {
Object.assign(metric, { result: metricResultStatus.result }),
);
- timeSeriesChart = makeTimeSeriesChart(graphData, 'area-chart');
+ timeSeriesChart = createWrapper({ ...graphData, type: 'area-chart' }, mount);
timeSeriesChart.vm.$nextTick(done);
});
diff --git a/spec/frontend/monitoring/components/dashboard_panel_spec.js b/spec/frontend/monitoring/components/dashboard_panel_spec.js
new file mode 100644
index 00000000000..f8c9bd56721
--- /dev/null
+++ b/spec/frontend/monitoring/components/dashboard_panel_spec.js
@@ -0,0 +1,576 @@
+import Vuex from 'vuex';
+import { shallowMount } from '@vue/test-utils';
+import AxiosMockAdapter from 'axios-mock-adapter';
+import { setTestTimeout } from 'helpers/timeout';
+import invalidUrl from '~/lib/utils/invalid_url';
+import axios from '~/lib/utils/axios_utils';
+import { GlDropdownItem } from '@gitlab/ui';
+import AlertWidget from '~/monitoring/components/alert_widget.vue';
+
+import DashboardPanel from '~/monitoring/components/dashboard_panel.vue';
+import {
+ anomalyMockGraphData,
+ mockLogsHref,
+ mockLogsPath,
+ mockNamespace,
+ mockNamespacedData,
+ mockTimeRange,
+ singleStatMetricsResult,
+ graphDataPrometheusQueryRangeMultiTrack,
+ barMockData,
+ propsData,
+} from '../mock_data';
+
+import { panelTypes } from '~/monitoring/constants';
+
+import MonitorEmptyChart from '~/monitoring/components/charts/empty_chart.vue';
+import MonitorTimeSeriesChart from '~/monitoring/components/charts/time_series.vue';
+import MonitorAnomalyChart from '~/monitoring/components/charts/anomaly.vue';
+import MonitorSingleStatChart from '~/monitoring/components/charts/single_stat.vue';
+import MonitorHeatmapChart from '~/monitoring/components/charts/heatmap.vue';
+import MonitorColumnChart from '~/monitoring/components/charts/column.vue';
+import MonitorBarChart from '~/monitoring/components/charts/bar.vue';
+import MonitorStackedColumnChart from '~/monitoring/components/charts/stacked_column.vue';
+
+import { graphData, graphDataEmpty } from '../fixture_data';
+import { createStore, monitoringDashboard } from '~/monitoring/stores';
+import { createStore as createEmbedGroupStore } from '~/monitoring/stores/embed_group';
+
+global.URL.createObjectURL = jest.fn();
+
+const mocks = {
+ $toast: {
+ show: jest.fn(),
+ },
+};
+
+describe('Dashboard Panel', () => {
+ let axiosMock;
+ let store;
+ let state;
+ let wrapper;
+
+ const exampleText = 'example_text';
+
+ const findCopyLink = () => wrapper.find({ ref: 'copyChartLink' });
+ const findTimeChart = () => wrapper.find({ ref: 'timeSeriesChart' });
+ const findTitle = () => wrapper.find({ ref: 'graphTitle' });
+ const findContextualMenu = () => wrapper.find({ ref: 'contextualMenu' });
+
+ const createWrapper = (props, options) => {
+ wrapper = shallowMount(DashboardPanel, {
+ propsData: {
+ graphData,
+ settingsPath: propsData.settingsPath,
+ ...props,
+ },
+ store,
+ mocks,
+ ...options,
+ });
+ };
+
+ beforeEach(() => {
+ setTestTimeout(1000);
+
+ store = createStore();
+ state = store.state.monitoringDashboard;
+
+ axiosMock = new AxiosMockAdapter(axios);
+ });
+
+ afterEach(() => {
+ axiosMock.reset();
+ });
+
+ describe('Renders slots', () => {
+ it('renders "topLeft" slot', () => {
+ createWrapper(
+ {},
+ {
+ slots: {
+ topLeft: `<div class="top-left-content">OK</div>`,
+ },
+ },
+ );
+
+ expect(wrapper.find('.top-left-content').exists()).toBe(true);
+ expect(wrapper.find('.top-left-content').text()).toBe('OK');
+ });
+ });
+
+ describe('When no graphData is available', () => {
+ beforeEach(() => {
+ createWrapper({
+ graphData: graphDataEmpty,
+ });
+ });
+
+ afterEach(() => {
+ wrapper.destroy();
+ });
+
+ it('renders the chart title', () => {
+ expect(findTitle().text()).toBe(graphDataEmpty.title);
+ });
+
+ it('renders no download csv link', () => {
+ expect(wrapper.find({ ref: 'downloadCsvLink' }).exists()).toBe(false);
+ });
+
+ it('does not contain graph widgets', () => {
+ expect(findContextualMenu().exists()).toBe(false);
+ });
+
+ it('The Empty Chart component is rendered and is a Vue instance', () => {
+ expect(wrapper.find(MonitorEmptyChart).exists()).toBe(true);
+ expect(wrapper.find(MonitorEmptyChart).isVueInstance()).toBe(true);
+ });
+ });
+
+ describe('When graphData is null', () => {
+ beforeEach(() => {
+ createWrapper({
+ graphData: null,
+ });
+ });
+
+ afterEach(() => {
+ wrapper.destroy();
+ });
+
+ it('renders no chart title', () => {
+ expect(findTitle().text()).toBe('');
+ });
+
+ it('renders no download csv link', () => {
+ expect(wrapper.find({ ref: 'downloadCsvLink' }).exists()).toBe(false);
+ });
+
+ it('does not contain graph widgets', () => {
+ expect(findContextualMenu().exists()).toBe(false);
+ });
+
+ it('The Empty Chart component is rendered and is a Vue instance', () => {
+ expect(wrapper.find(MonitorEmptyChart).exists()).toBe(true);
+ expect(wrapper.find(MonitorEmptyChart).isVueInstance()).toBe(true);
+ });
+ });
+
+ describe('When graphData is available', () => {
+ beforeEach(() => {
+ createWrapper();
+ });
+
+ afterEach(() => {
+ wrapper.destroy();
+ });
+
+ it('renders the chart title', () => {
+ expect(findTitle().text()).toBe(graphData.title);
+ });
+
+ it('contains graph widgets', () => {
+ expect(findContextualMenu().exists()).toBe(true);
+ expect(wrapper.find({ ref: 'downloadCsvLink' }).exists()).toBe(true);
+ });
+
+ it('sets no clipboard copy link on dropdown by default', () => {
+ expect(findCopyLink().exists()).toBe(false);
+ });
+
+ it('should emit `timerange` event when a zooming in/out in a chart occcurs', () => {
+ const timeRange = {
+ start: '2020-01-01T00:00:00.000Z',
+ end: '2020-01-01T01:00:00.000Z',
+ };
+
+ jest.spyOn(wrapper.vm, '$emit');
+
+ findTimeChart().vm.$emit('datazoom', timeRange);
+
+ return wrapper.vm.$nextTick(() => {
+ expect(wrapper.vm.$emit).toHaveBeenCalledWith('timerangezoom', timeRange);
+ });
+ });
+
+ it('includes a default group id', () => {
+ expect(wrapper.vm.groupId).toBe('dashboard-panel');
+ });
+
+ describe('Supports different panel types', () => {
+ const dataWithType = type => {
+ return {
+ ...graphData,
+ type,
+ };
+ };
+
+ it('empty chart is rendered for empty results', () => {
+ createWrapper({ graphData: graphDataEmpty });
+ expect(wrapper.find(MonitorEmptyChart).exists()).toBe(true);
+ expect(wrapper.find(MonitorEmptyChart).isVueInstance()).toBe(true);
+ });
+
+ it('area chart is rendered by default', () => {
+ createWrapper();
+ expect(wrapper.find(MonitorTimeSeriesChart).exists()).toBe(true);
+ expect(wrapper.find(MonitorTimeSeriesChart).isVueInstance()).toBe(true);
+ });
+
+ it.each`
+ data | component
+ ${dataWithType(panelTypes.AREA_CHART)} | ${MonitorTimeSeriesChart}
+ ${dataWithType(panelTypes.LINE_CHART)} | ${MonitorTimeSeriesChart}
+ ${anomalyMockGraphData} | ${MonitorAnomalyChart}
+ ${dataWithType(panelTypes.COLUMN)} | ${MonitorColumnChart}
+ ${dataWithType(panelTypes.STACKED_COLUMN)} | ${MonitorStackedColumnChart}
+ ${singleStatMetricsResult} | ${MonitorSingleStatChart}
+ ${graphDataPrometheusQueryRangeMultiTrack} | ${MonitorHeatmapChart}
+ ${barMockData} | ${MonitorBarChart}
+ `('wrapps a $data.type component binding attributes', ({ data, component }) => {
+ const attrs = { attr1: 'attr1Value', attr2: 'attr2Value' };
+ createWrapper({ graphData: data }, { attrs });
+
+ expect(wrapper.find(component).exists()).toBe(true);
+ expect(wrapper.find(component).isVueInstance()).toBe(true);
+ expect(wrapper.find(component).attributes()).toMatchObject(attrs);
+ });
+ });
+ });
+
+ describe('Edit custom metric dropdown item', () => {
+ const findEditCustomMetricLink = () => wrapper.find({ ref: 'editMetricLink' });
+ const mockEditPath = '/root/kubernetes-gke-project/prometheus/metrics/23/edit';
+
+ beforeEach(() => {
+ createWrapper();
+
+ return wrapper.vm.$nextTick();
+ });
+
+ it('is not present if the panel is not a custom metric', () => {
+ expect(findEditCustomMetricLink().exists()).toBe(false);
+ });
+
+ it('is present when the panel contains an edit_path property', () => {
+ wrapper.setProps({
+ graphData: {
+ ...graphData,
+ metrics: [
+ {
+ ...graphData.metrics[0],
+ edit_path: mockEditPath,
+ },
+ ],
+ },
+ });
+
+ return wrapper.vm.$nextTick(() => {
+ expect(findEditCustomMetricLink().exists()).toBe(true);
+ expect(findEditCustomMetricLink().text()).toBe('Edit metric');
+ expect(findEditCustomMetricLink().attributes('href')).toBe(mockEditPath);
+ });
+ });
+
+ it('shows an "Edit metrics" link pointing to settingsPath for a panel with multiple metrics', () => {
+ wrapper.setProps({
+ graphData: {
+ ...graphData,
+ metrics: [
+ {
+ ...graphData.metrics[0],
+ edit_path: '/root/kubernetes-gke-project/prometheus/metrics/23/edit',
+ },
+ {
+ ...graphData.metrics[0],
+ edit_path: '/root/kubernetes-gke-project/prometheus/metrics/23/edit',
+ },
+ ],
+ },
+ });
+
+ return wrapper.vm.$nextTick(() => {
+ expect(findEditCustomMetricLink().text()).toBe('Edit metrics');
+ expect(findEditCustomMetricLink().attributes('href')).toBe(propsData.settingsPath);
+ });
+ });
+ });
+
+ describe('View Logs dropdown item', () => {
+ const findViewLogsLink = () => wrapper.find({ ref: 'viewLogsLink' });
+
+ beforeEach(() => {
+ createWrapper();
+ return wrapper.vm.$nextTick();
+ });
+
+ it('is not present by default', () =>
+ wrapper.vm.$nextTick(() => {
+ expect(findViewLogsLink().exists()).toBe(false);
+ }));
+
+ it('is not present if a time range is not set', () => {
+ state.logsPath = mockLogsPath;
+ state.timeRange = null;
+
+ return wrapper.vm.$nextTick(() => {
+ expect(findViewLogsLink().exists()).toBe(false);
+ });
+ });
+
+ it('is not present if the logs path is default', () => {
+ state.logsPath = invalidUrl;
+ state.timeRange = mockTimeRange;
+
+ return wrapper.vm.$nextTick(() => {
+ expect(findViewLogsLink().exists()).toBe(false);
+ });
+ });
+
+ it('is not present if the logs path is not set', () => {
+ state.logsPath = null;
+ state.timeRange = mockTimeRange;
+
+ return wrapper.vm.$nextTick(() => {
+ expect(findViewLogsLink().exists()).toBe(false);
+ });
+ });
+
+ it('is present when logs path and time a range is present', () => {
+ state.logsPath = mockLogsPath;
+ state.timeRange = mockTimeRange;
+
+ return wrapper.vm.$nextTick(() => {
+ expect(findViewLogsLink().attributes('href')).toMatch(mockLogsHref);
+ });
+ });
+
+ it('it is overriden when a datazoom event is received', () => {
+ state.logsPath = mockLogsPath;
+ state.timeRange = mockTimeRange;
+
+ const zoomedTimeRange = {
+ start: '2020-01-01T00:00:00.000Z',
+ end: '2020-01-01T01:00:00.000Z',
+ };
+
+ findTimeChart().vm.$emit('datazoom', zoomedTimeRange);
+
+ return wrapper.vm.$nextTick(() => {
+ const start = encodeURIComponent(zoomedTimeRange.start);
+ const end = encodeURIComponent(zoomedTimeRange.end);
+ expect(findViewLogsLink().attributes('href')).toMatch(
+ `${mockLogsPath}?start=${start}&end=${end}`,
+ );
+ });
+ });
+ });
+
+ describe('when cliboard data is available', () => {
+ const clipboardText = 'A value to copy.';
+
+ beforeEach(() => {
+ createWrapper({
+ clipboardText,
+ });
+ });
+
+ it('sets clipboard text on the dropdown', () => {
+ expect(findCopyLink().exists()).toBe(true);
+ expect(findCopyLink().element.dataset.clipboardText).toBe(clipboardText);
+ });
+
+ it('adds a copy button to the dropdown', () => {
+ expect(findCopyLink().text()).toContain('Copy link to chart');
+ });
+
+ it('opens a toast on click', () => {
+ findCopyLink().vm.$emit('click');
+
+ expect(wrapper.vm.$toast.show).toHaveBeenCalled();
+ });
+ });
+
+ describe('when cliboard data is not available', () => {
+ it('there is no "copy to clipboard" link for a null value', () => {
+ createWrapper({ clipboardText: null });
+ expect(findCopyLink().exists()).toBe(false);
+ });
+
+ it('there is no "copy to clipboard" link for an empty value', () => {
+ createWrapper({ clipboardText: '' });
+ expect(findCopyLink().exists()).toBe(false);
+ });
+ });
+
+ describe('when downloading metrics data as CSV', () => {
+ beforeEach(() => {
+ wrapper = shallowMount(DashboardPanel, {
+ propsData: {
+ clipboardText: exampleText,
+ settingsPath: propsData.settingsPath,
+ graphData: {
+ y_label: 'metric',
+ ...graphData,
+ },
+ },
+ store,
+ });
+ return wrapper.vm.$nextTick();
+ });
+
+ afterEach(() => {
+ wrapper.destroy();
+ });
+
+ describe('csvText', () => {
+ it('converts metrics data from json to csv', () => {
+ const header = `timestamp,${graphData.y_label}`;
+ const data = graphData.metrics[0].result[0].values;
+ const firstRow = `${data[0][0]},${data[0][1]}`;
+ const secondRow = `${data[1][0]},${data[1][1]}`;
+
+ expect(wrapper.vm.csvText).toMatch(`${header}\r\n${firstRow}\r\n${secondRow}\r\n`);
+ });
+ });
+
+ describe('downloadCsv', () => {
+ it('produces a link with a Blob', () => {
+ expect(global.URL.createObjectURL).toHaveBeenLastCalledWith(expect.any(Blob));
+ expect(global.URL.createObjectURL).toHaveBeenLastCalledWith(
+ expect.objectContaining({
+ size: wrapper.vm.csvText.length,
+ type: 'text/plain',
+ }),
+ );
+ });
+ });
+ });
+
+ describe('when using dynamic modules', () => {
+ const { mockDeploymentData, mockProjectPath } = mockNamespacedData;
+
+ beforeEach(() => {
+ store = createEmbedGroupStore();
+ store.registerModule(mockNamespace, monitoringDashboard);
+ store.state.embedGroup.modules.push(mockNamespace);
+
+ wrapper = shallowMount(DashboardPanel, {
+ propsData: {
+ graphData,
+ settingsPath: propsData.settingsPath,
+ namespace: mockNamespace,
+ },
+ store,
+ mocks,
+ });
+ });
+
+ it('handles namespaced time range and logs path state', () => {
+ store.state[mockNamespace].timeRange = mockTimeRange;
+ store.state[mockNamespace].logsPath = mockLogsPath;
+
+ return wrapper.vm.$nextTick().then(() => {
+ expect(wrapper.find({ ref: 'viewLogsLink' }).attributes().href).toBe(mockLogsHref);
+ });
+ });
+
+ it('handles namespaced deployment data state', () => {
+ store.state[mockNamespace].deploymentData = mockDeploymentData;
+
+ return wrapper.vm.$nextTick().then(() => {
+ expect(findTimeChart().props().deploymentData).toEqual(mockDeploymentData);
+ });
+ });
+
+ it('handles namespaced project path state', () => {
+ store.state[mockNamespace].projectPath = mockProjectPath;
+
+ return wrapper.vm.$nextTick().then(() => {
+ expect(findTimeChart().props().projectPath).toBe(mockProjectPath);
+ });
+ });
+
+ it('it renders a time series chart with no errors', () => {
+ expect(wrapper.find(MonitorTimeSeriesChart).isVueInstance()).toBe(true);
+ expect(wrapper.find(MonitorTimeSeriesChart).exists()).toBe(true);
+ });
+ });
+
+ describe('Expand to full screen', () => {
+ const findExpandBtn = () => wrapper.find({ ref: 'expandBtn' });
+
+ describe('when there is no @expand listener', () => {
+ it('does not show `View full screen` option', () => {
+ createWrapper();
+ expect(findExpandBtn().exists()).toBe(false);
+ });
+ });
+
+ describe('when there is an @expand listener', () => {
+ beforeEach(() => {
+ createWrapper({}, { listeners: { expand: () => {} } });
+ });
+
+ it('shows the `expand` option', () => {
+ expect(findExpandBtn().exists()).toBe(true);
+ });
+
+ it('emits the `expand` event', () => {
+ const preventDefault = jest.fn();
+ findExpandBtn().vm.$emit('click', { preventDefault });
+ expect(wrapper.emitted('expand')).toHaveLength(1);
+ expect(preventDefault).toHaveBeenCalled();
+ });
+ });
+ });
+
+ describe('panel alerts', () => {
+ const setMetricsSavedToDb = val =>
+ monitoringDashboard.getters.metricsSavedToDb.mockReturnValue(val);
+ const findAlertsWidget = () => wrapper.find(AlertWidget);
+ const findMenuItemAlert = () =>
+ wrapper.findAll(GlDropdownItem).filter(i => i.text() === 'Alerts');
+
+ beforeEach(() => {
+ jest.spyOn(monitoringDashboard.getters, 'metricsSavedToDb').mockReturnValue([]);
+
+ store = new Vuex.Store({
+ modules: {
+ monitoringDashboard,
+ },
+ });
+
+ createWrapper();
+ });
+
+ describe.each`
+ desc | metricsSavedToDb | props | isShown
+ ${'with permission and no metrics in db'} | ${[]} | ${{}} | ${false}
+ ${'with permission and related metrics in db'} | ${[graphData.metrics[0].metricId]} | ${{}} | ${true}
+ ${'without permission and related metrics in db'} | ${[graphData.metrics[0].metricId]} | ${{ prometheusAlertsAvailable: false }} | ${false}
+ ${'with permission and unrelated metrics in db'} | ${['another_metric_id']} | ${{}} | ${false}
+ `('$desc', ({ metricsSavedToDb, isShown, props }) => {
+ const showsDesc = isShown ? 'shows' : 'does not show';
+
+ beforeEach(() => {
+ setMetricsSavedToDb(metricsSavedToDb);
+ createWrapper({
+ alertsEndpoint: '/endpoint',
+ prometheusAlertsAvailable: true,
+ ...props,
+ });
+ return wrapper.vm.$nextTick();
+ });
+
+ it(`${showsDesc} alert widget`, () => {
+ expect(findAlertsWidget().exists()).toBe(isShown);
+ });
+
+ it(`${showsDesc} alert configuration`, () => {
+ expect(findMenuItemAlert().exists()).toBe(isShown);
+ });
+ });
+ });
+});
diff --git a/spec/frontend/monitoring/components/dashboard_spec.js b/spec/frontend/monitoring/components/dashboard_spec.js
index 8b6ee9b3bf6..b2c9fe93cde 100644
--- a/spec/frontend/monitoring/components/dashboard_spec.js
+++ b/spec/frontend/monitoring/components/dashboard_spec.js
@@ -1,6 +1,8 @@
import { shallowMount, mount } from '@vue/test-utils';
import Tracking from '~/tracking';
-import { GlModal, GlDropdownItem, GlDeprecatedButton } from '@gitlab/ui';
+import { ESC_KEY, ESC_KEY_IE11 } from '~/lib/utils/keys';
+import { GlModal, GlDropdownItem, GlDeprecatedButton, GlIcon } from '@gitlab/ui';
+import { objectToQuery } from '~/lib/utils/url_utility';
import VueDraggable from 'vuedraggable';
import MockAdapter from 'axios-mock-adapter';
import axios from '~/lib/utils/axios_utils';
@@ -11,13 +13,23 @@ import Dashboard from '~/monitoring/components/dashboard.vue';
import DateTimePicker from '~/vue_shared/components/date_time_picker/date_time_picker.vue';
import CustomMetricsFormFields from '~/custom_metrics/components/custom_metrics_form_fields.vue';
import DashboardsDropdown from '~/monitoring/components/dashboards_dropdown.vue';
+import EmptyState from '~/monitoring/components/empty_state.vue';
import GroupEmptyState from '~/monitoring/components/group_empty_state.vue';
-import PanelType from 'ee_else_ce/monitoring/components/panel_type.vue';
+import DashboardPanel from '~/monitoring/components/dashboard_panel.vue';
import { createStore } from '~/monitoring/stores';
import * as types from '~/monitoring/stores/mutation_types';
-import { setupStoreWithDashboard, setMetricResult, setupStoreWithData } from '../store_utils';
+import {
+ setupAllDashboards,
+ setupStoreWithDashboard,
+ setMetricResult,
+ setupStoreWithData,
+ setupStoreWithVariable,
+} from '../store_utils';
import { environmentData, dashboardGitResponse, propsData } from '../mock_data';
import { metricsDashboardViewModel, metricsDashboardPanelCount } from '../fixture_data';
+import createFlash from '~/flash';
+
+jest.mock('~/flash');
describe('Dashboard', () => {
let store;
@@ -27,15 +39,12 @@ describe('Dashboard', () => {
const findEnvironmentsDropdown = () => wrapper.find({ ref: 'monitorEnvironmentsDropdown' });
const findAllEnvironmentsDropdownItems = () => findEnvironmentsDropdown().findAll(GlDropdownItem);
const setSearchTerm = searchTerm => {
- wrapper.vm.$store.commit(`monitoringDashboard/${types.SET_ENVIRONMENTS_FILTER}`, searchTerm);
+ store.commit(`monitoringDashboard/${types.SET_ENVIRONMENTS_FILTER}`, searchTerm);
};
const createShallowWrapper = (props = {}, options = {}) => {
wrapper = shallowMount(Dashboard, {
propsData: { ...propsData, ...props },
- methods: {
- fetchData: jest.fn(),
- },
store,
...options,
});
@@ -44,10 +53,8 @@ describe('Dashboard', () => {
const createMountedWrapper = (props = {}, options = {}) => {
wrapper = mount(Dashboard, {
propsData: { ...propsData, ...props },
- methods: {
- fetchData: jest.fn(),
- },
store,
+ stubs: ['graph-group', 'dashboard-panel'],
...options,
});
};
@@ -55,19 +62,18 @@ describe('Dashboard', () => {
beforeEach(() => {
store = createStore();
mock = new MockAdapter(axios);
+ jest.spyOn(store, 'dispatch').mockResolvedValue();
});
afterEach(() => {
- if (wrapper) {
- wrapper.destroy();
- wrapper = null;
- }
mock.restore();
+ if (store.dispatch.mockReset) {
+ store.dispatch.mockReset();
+ }
});
describe('no metrics are available yet', () => {
beforeEach(() => {
- jest.spyOn(store, 'dispatch');
createShallowWrapper();
});
@@ -103,9 +109,7 @@ describe('Dashboard', () => {
describe('request information to the server', () => {
it('calls to set time range and fetch data', () => {
- jest.spyOn(store, 'dispatch');
-
- createShallowWrapper({ hasMetrics: true }, { methods: {} });
+ createShallowWrapper({ hasMetrics: true });
return wrapper.vm.$nextTick().then(() => {
expect(store.dispatch).toHaveBeenCalledWith(
@@ -118,20 +122,20 @@ describe('Dashboard', () => {
});
it('shows up a loading state', () => {
- createShallowWrapper({ hasMetrics: true }, { methods: {} });
+ store.state.monitoringDashboard.emptyState = 'loading';
+
+ createShallowWrapper({ hasMetrics: true });
return wrapper.vm.$nextTick().then(() => {
- expect(wrapper.vm.emptyState).toEqual('loading');
+ expect(wrapper.find(EmptyState).exists()).toBe(true);
+ expect(wrapper.find(EmptyState).props('selectedState')).toBe('loading');
});
});
it('hides the group panels when showPanels is false', () => {
- createMountedWrapper(
- { hasMetrics: true, showPanels: false },
- { stubs: ['graph-group', 'panel-type'] },
- );
+ createMountedWrapper({ hasMetrics: true, showPanels: false });
- setupStoreWithData(wrapper.vm.$store);
+ setupStoreWithData(store);
return wrapper.vm.$nextTick().then(() => {
expect(wrapper.vm.showEmptyState).toEqual(false);
@@ -142,9 +146,9 @@ describe('Dashboard', () => {
it('fetches the metrics data with proper time window', () => {
jest.spyOn(store, 'dispatch');
- createMountedWrapper({ hasMetrics: true }, { stubs: ['graph-group', 'panel-type'] });
+ createMountedWrapper({ hasMetrics: true });
- wrapper.vm.$store.commit(
+ store.commit(
`monitoringDashboard/${types.RECEIVE_ENVIRONMENTS_DATA_SUCCESS}`,
environmentData,
);
@@ -155,11 +159,176 @@ describe('Dashboard', () => {
});
});
+ describe('when the URL contains a reference to a panel', () => {
+ let location;
+
+ const setSearch = search => {
+ window.location = { ...location, search };
+ };
+
+ beforeEach(() => {
+ location = window.location;
+ delete window.location;
+ });
+
+ afterEach(() => {
+ window.location = location;
+ });
+
+ it('when the URL points to a panel it expands', () => {
+ const panelGroup = metricsDashboardViewModel.panelGroups[0];
+ const panel = panelGroup.panels[0];
+
+ setSearch(
+ objectToQuery({
+ group: panelGroup.group,
+ title: panel.title,
+ y_label: panel.y_label,
+ }),
+ );
+
+ createMountedWrapper({ hasMetrics: true });
+ setupStoreWithData(store);
+
+ return wrapper.vm.$nextTick().then(() => {
+ expect(store.dispatch).toHaveBeenCalledWith('monitoringDashboard/setExpandedPanel', {
+ group: panelGroup.group,
+ panel: expect.objectContaining({
+ title: panel.title,
+ y_label: panel.y_label,
+ }),
+ });
+ });
+ });
+
+ it('when the URL does not link to any panel, no panel is expanded', () => {
+ setSearch('');
+
+ createMountedWrapper({ hasMetrics: true });
+ setupStoreWithData(store);
+
+ return wrapper.vm.$nextTick().then(() => {
+ expect(store.dispatch).not.toHaveBeenCalledWith(
+ 'monitoringDashboard/setExpandedPanel',
+ expect.anything(),
+ );
+ });
+ });
+
+ it('when the URL points to an incorrect panel it shows an error', () => {
+ const panelGroup = metricsDashboardViewModel.panelGroups[0];
+ const panel = panelGroup.panels[0];
+
+ setSearch(
+ objectToQuery({
+ group: panelGroup.group,
+ title: 'incorrect',
+ y_label: panel.y_label,
+ }),
+ );
+
+ createMountedWrapper({ hasMetrics: true });
+ setupStoreWithData(store);
+
+ return wrapper.vm.$nextTick().then(() => {
+ expect(createFlash).toHaveBeenCalled();
+ expect(store.dispatch).not.toHaveBeenCalledWith(
+ 'monitoringDashboard/setExpandedPanel',
+ expect.anything(),
+ );
+ });
+ });
+ });
+
+ describe('when the panel is expanded', () => {
+ let group;
+ let panel;
+
+ const expandPanel = (mockGroup, mockPanel) => {
+ store.commit(`monitoringDashboard/${types.SET_EXPANDED_PANEL}`, {
+ group: mockGroup,
+ panel: mockPanel,
+ });
+ };
+
+ beforeEach(() => {
+ setupStoreWithData(store);
+
+ const { panelGroups } = store.state.monitoringDashboard.dashboard;
+ group = panelGroups[0].group;
+ [panel] = panelGroups[0].panels;
+
+ jest.spyOn(window.history, 'pushState').mockImplementation();
+ });
+
+ afterEach(() => {
+ window.history.pushState.mockRestore();
+ });
+
+ it('URL is updated with panel parameters', () => {
+ createMountedWrapper({ hasMetrics: true });
+ expandPanel(group, panel);
+
+ const expectedSearch = objectToQuery({
+ group,
+ title: panel.title,
+ y_label: panel.y_label,
+ });
+
+ return wrapper.vm.$nextTick(() => {
+ expect(window.history.pushState).toHaveBeenCalledTimes(1);
+ expect(window.history.pushState).toHaveBeenCalledWith(
+ expect.anything(), // state
+ expect.any(String), // document title
+ expect.stringContaining(`${expectedSearch}`),
+ );
+ });
+ });
+
+ it('URL is updated with panel parameters and custom dashboard', () => {
+ const dashboard = 'dashboard.yml';
+
+ createMountedWrapper({ hasMetrics: true, currentDashboard: dashboard });
+ expandPanel(group, panel);
+
+ const expectedSearch = objectToQuery({
+ dashboard,
+ group,
+ title: panel.title,
+ y_label: panel.y_label,
+ });
+
+ return wrapper.vm.$nextTick(() => {
+ expect(window.history.pushState).toHaveBeenCalledTimes(1);
+ expect(window.history.pushState).toHaveBeenCalledWith(
+ expect.anything(), // state
+ expect.any(String), // document title
+ expect.stringContaining(`${expectedSearch}`),
+ );
+ });
+ });
+
+ it('URL is updated with no parameters', () => {
+ expandPanel(group, panel);
+ createMountedWrapper({ hasMetrics: true });
+ expandPanel(null, null);
+
+ return wrapper.vm.$nextTick(() => {
+ expect(window.history.pushState).toHaveBeenCalledTimes(1);
+ expect(window.history.pushState).toHaveBeenCalledWith(
+ expect.anything(), // state
+ expect.any(String), // document title
+ expect.not.stringMatching(/group|title|y_label/), // no panel params
+ );
+ });
+ });
+ });
+
describe('when all requests have been commited by the store', () => {
beforeEach(() => {
- createMountedWrapper({ hasMetrics: true }, { stubs: ['graph-group', 'panel-type'] });
+ createMountedWrapper({ hasMetrics: true });
- setupStoreWithData(wrapper.vm.$store);
+ setupStoreWithData(store);
return wrapper.vm.$nextTick();
});
@@ -185,10 +354,89 @@ describe('Dashboard', () => {
});
});
+ describe('star dashboards', () => {
+ const findToggleStar = () => wrapper.find({ ref: 'toggleStarBtn' });
+ const findToggleStarIcon = () => findToggleStar().find(GlIcon);
+
+ beforeEach(() => {
+ createShallowWrapper();
+ setupAllDashboards(store);
+ });
+
+ it('toggle star button is shown', () => {
+ expect(findToggleStar().exists()).toBe(true);
+ expect(findToggleStar().props('disabled')).toBe(false);
+ });
+
+ it('toggle star button is disabled when starring is taking place', () => {
+ store.commit(`monitoringDashboard/${types.REQUEST_DASHBOARD_STARRING}`);
+
+ return wrapper.vm.$nextTick(() => {
+ expect(findToggleStar().exists()).toBe(true);
+ expect(findToggleStar().props('disabled')).toBe(true);
+ });
+ });
+
+ describe('when the dashboard list is loaded', () => {
+ // Tooltip element should wrap directly
+ const getToggleTooltip = () => findToggleStar().element.parentElement.getAttribute('title');
+
+ beforeEach(() => {
+ setupAllDashboards(store);
+ jest.spyOn(store, 'dispatch');
+ });
+
+ it('dispatches a toggle star action', () => {
+ findToggleStar().vm.$emit('click');
+
+ return wrapper.vm.$nextTick().then(() => {
+ expect(store.dispatch).toHaveBeenCalledWith(
+ 'monitoringDashboard/toggleStarredValue',
+ undefined,
+ );
+ });
+ });
+
+ describe('when dashboard is not starred', () => {
+ beforeEach(() => {
+ store.commit(`monitoringDashboard/${types.SET_INITIAL_STATE}`, {
+ currentDashboard: dashboardGitResponse[0].path,
+ });
+ return wrapper.vm.$nextTick();
+ });
+
+ it('toggle star button shows "Star dashboard"', () => {
+ expect(getToggleTooltip()).toBe('Star dashboard');
+ });
+
+ it('toggle star button shows an unstarred state', () => {
+ expect(findToggleStarIcon().attributes('name')).toBe('star-o');
+ });
+ });
+
+ describe('when dashboard is starred', () => {
+ beforeEach(() => {
+ store.commit(`monitoringDashboard/${types.SET_INITIAL_STATE}`, {
+ currentDashboard: dashboardGitResponse[1].path,
+ });
+ return wrapper.vm.$nextTick();
+ });
+
+ it('toggle star button shows "Star dashboard"', () => {
+ expect(getToggleTooltip()).toBe('Unstar dashboard');
+ });
+
+ it('toggle star button shows a starred state', () => {
+ expect(findToggleStarIcon().attributes('name')).toBe('star');
+ });
+ });
+ });
+ });
+
it('hides the environments dropdown list when there is no environments', () => {
- createMountedWrapper({ hasMetrics: true }, { stubs: ['graph-group', 'panel-type'] });
+ createMountedWrapper({ hasMetrics: true });
- setupStoreWithDashboard(wrapper.vm.$store);
+ setupStoreWithDashboard(store);
return wrapper.vm.$nextTick().then(() => {
expect(findAllEnvironmentsDropdownItems()).toHaveLength(0);
@@ -196,9 +444,9 @@ describe('Dashboard', () => {
});
it('renders the datetimepicker dropdown', () => {
- createMountedWrapper({ hasMetrics: true }, { stubs: ['graph-group', 'panel-type'] });
+ createMountedWrapper({ hasMetrics: true });
- setupStoreWithData(wrapper.vm.$store);
+ setupStoreWithData(store);
return wrapper.vm.$nextTick().then(() => {
expect(wrapper.find(DateTimePicker).exists()).toBe(true);
@@ -206,9 +454,9 @@ describe('Dashboard', () => {
});
it('renders the refresh dashboard button', () => {
- createMountedWrapper({ hasMetrics: true }, { stubs: ['graph-group', 'panel-type'] });
+ createMountedWrapper({ hasMetrics: true });
- setupStoreWithData(wrapper.vm.$store);
+ setupStoreWithData(store);
return wrapper.vm.$nextTick().then(() => {
const refreshBtn = wrapper.findAll({ ref: 'refreshDashboardBtn' });
@@ -218,14 +466,135 @@ describe('Dashboard', () => {
});
});
- describe('when one of the metrics is missing', () => {
+ describe('variables section', () => {
beforeEach(() => {
createShallowWrapper({ hasMetrics: true });
+ setupStoreWithData(store);
+ setupStoreWithVariable(store);
+
+ return wrapper.vm.$nextTick();
+ });
+
+ it('shows the variables section', () => {
+ expect(wrapper.vm.shouldShowVariablesSection).toBe(true);
+ });
+ });
+
+ describe('single panel expands to "full screen" mode', () => {
+ const findExpandedPanel = () => wrapper.find({ ref: 'expandedPanel' });
- const { $store } = wrapper.vm;
+ describe('when the panel is not expanded', () => {
+ beforeEach(() => {
+ createShallowWrapper({ hasMetrics: true });
+ setupStoreWithData(store);
+ return wrapper.vm.$nextTick();
+ });
+
+ it('expanded panel is not visible', () => {
+ expect(findExpandedPanel().isVisible()).toBe(false);
+ });
+
+ it('can set a panel as expanded', () => {
+ const panel = wrapper.findAll(DashboardPanel).at(1);
+
+ jest.spyOn(store, 'dispatch');
+
+ panel.vm.$emit('expand');
+
+ const groupData = metricsDashboardViewModel.panelGroups[0];
+
+ expect(store.dispatch).toHaveBeenCalledWith('monitoringDashboard/setExpandedPanel', {
+ group: groupData.group,
+ panel: expect.objectContaining({
+ id: groupData.panels[0].id,
+ }),
+ });
+ });
+ });
+
+ describe('when the panel is expanded', () => {
+ let group;
+ let panel;
+
+ const mockKeyup = key => window.dispatchEvent(new KeyboardEvent('keyup', { key }));
+
+ const MockPanel = {
+ template: `<div><slot name="topLeft"/></div>`,
+ };
+
+ beforeEach(() => {
+ createShallowWrapper({ hasMetrics: true }, { stubs: { DashboardPanel: MockPanel } });
+ setupStoreWithData(store);
+
+ const { panelGroups } = store.state.monitoringDashboard.dashboard;
+
+ group = panelGroups[0].group;
+ [panel] = panelGroups[0].panels;
+
+ store.commit(`monitoringDashboard/${types.SET_EXPANDED_PANEL}`, {
+ group,
+ panel,
+ });
+
+ jest.spyOn(store, 'dispatch');
+
+ return wrapper.vm.$nextTick();
+ });
- setupStoreWithDashboard($store);
- setMetricResult({ $store, result: [], panel: 2 });
+ it('displays a single panel and others are hidden', () => {
+ const panels = wrapper.findAll(MockPanel);
+ const visiblePanels = panels.filter(w => w.isVisible());
+
+ expect(findExpandedPanel().isVisible()).toBe(true);
+ // v-show for hiding panels is more performant than v-if
+ // check for panels to be hidden.
+ expect(panels.length).toBe(metricsDashboardPanelCount + 1);
+ expect(visiblePanels.length).toBe(1);
+ });
+
+ it('sets a link to the expanded panel', () => {
+ const searchQuery =
+ '?dashboard=config%2Fprometheus%2Fcommon_metrics.yml&group=System%20metrics%20(Kubernetes)&title=Memory%20Usage%20(Total)&y_label=Total%20Memory%20Used%20(GB)';
+
+ expect(findExpandedPanel().attributes('clipboard-text')).toEqual(
+ expect.stringContaining(searchQuery),
+ );
+ });
+
+ it('restores full dashboard by clicking `back`', () => {
+ wrapper.find({ ref: 'goBackBtn' }).vm.$emit('click');
+
+ expect(store.dispatch).toHaveBeenCalledWith(
+ 'monitoringDashboard/clearExpandedPanel',
+ undefined,
+ );
+ });
+
+ it('restores dashboard from full screen by typing the Escape key', () => {
+ mockKeyup(ESC_KEY);
+ expect(store.dispatch).toHaveBeenCalledWith(
+ `monitoringDashboard/clearExpandedPanel`,
+ undefined,
+ );
+ });
+
+ it('restores dashboard from full screen by typing the Escape key on IE11', () => {
+ mockKeyup(ESC_KEY_IE11);
+
+ expect(store.dispatch).toHaveBeenCalledWith(
+ `monitoringDashboard/clearExpandedPanel`,
+ undefined,
+ );
+ });
+ });
+ });
+
+ describe('when one of the metrics is missing', () => {
+ beforeEach(() => {
+ createShallowWrapper({ hasMetrics: true });
+
+ setupStoreWithDashboard(store);
+ setMetricResult({ store, result: [], panel: 2 });
return wrapper.vm.$nextTick();
});
@@ -249,19 +618,17 @@ describe('Dashboard', () => {
describe('searchable environments dropdown', () => {
beforeEach(() => {
- createMountedWrapper(
- { hasMetrics: true },
- {
- attachToDocument: true,
- stubs: ['graph-group', 'panel-type'],
- },
- );
+ createMountedWrapper({ hasMetrics: true }, { attachToDocument: true });
- setupStoreWithData(wrapper.vm.$store);
+ setupStoreWithData(store);
return wrapper.vm.$nextTick();
});
+ afterEach(() => {
+ wrapper.destroy();
+ });
+
it('renders a search input', () => {
expect(wrapper.find({ ref: 'monitorEnvironmentsDropdownSearch' }).exists()).toBe(true);
});
@@ -304,7 +671,7 @@ describe('Dashboard', () => {
});
it('shows loading element when environments fetch is still loading', () => {
- wrapper.vm.$store.commit(`monitoringDashboard/${types.REQUEST_ENVIRONMENTS_DATA}`);
+ store.commit(`monitoringDashboard/${types.REQUEST_ENVIRONMENTS_DATA}`);
return wrapper.vm
.$nextTick()
@@ -312,7 +679,7 @@ describe('Dashboard', () => {
expect(wrapper.find({ ref: 'monitorEnvironmentsDropdownLoading' }).exists()).toBe(true);
})
.then(() => {
- wrapper.vm.$store.commit(
+ store.commit(
`monitoringDashboard/${types.RECEIVE_ENVIRONMENTS_DATA_SUCCESS}`,
environmentData,
);
@@ -330,9 +697,11 @@ describe('Dashboard', () => {
const findRearrangeButton = () => wrapper.find('.js-rearrange-button');
beforeEach(() => {
- createShallowWrapper({ hasMetrics: true });
+ // call original dispatch
+ store.dispatch.mockRestore();
- setupStoreWithData(wrapper.vm.$store);
+ createShallowWrapper({ hasMetrics: true });
+ setupStoreWithData(store);
return wrapper.vm.$nextTick();
});
@@ -420,7 +789,7 @@ describe('Dashboard', () => {
createShallowWrapper({ hasMetrics: true, showHeader: false });
// all_dashboards is not defined in health dashboards
- wrapper.vm.$store.commit(`monitoringDashboard/${types.SET_ALL_DASHBOARDS}`, undefined);
+ store.commit(`monitoringDashboard/${types.SET_ALL_DASHBOARDS}`, undefined);
return wrapper.vm.$nextTick();
});
@@ -440,10 +809,7 @@ describe('Dashboard', () => {
beforeEach(() => {
createShallowWrapper({ hasMetrics: true });
- wrapper.vm.$store.commit(
- `monitoringDashboard/${types.SET_ALL_DASHBOARDS}`,
- dashboardGitResponse,
- );
+ setupAllDashboards(store);
return wrapper.vm.$nextTick();
});
@@ -452,10 +818,11 @@ describe('Dashboard', () => {
});
it('is present for a custom dashboard, and links to its edit_path', () => {
- const dashboard = dashboardGitResponse[1]; // non-default dashboard
- const currentDashboard = dashboard.path;
+ const dashboard = dashboardGitResponse[1];
+ store.commit(`monitoringDashboard/${types.SET_INITIAL_STATE}`, {
+ currentDashboard: dashboard.path,
+ });
- wrapper.setProps({ currentDashboard });
return wrapper.vm.$nextTick().then(() => {
expect(findEditLink().exists()).toBe(true);
expect(findEditLink().attributes('href')).toBe(dashboard.project_blob_path);
@@ -465,13 +832,8 @@ describe('Dashboard', () => {
describe('Dashboard dropdown', () => {
beforeEach(() => {
- createMountedWrapper({ hasMetrics: true }, { stubs: ['graph-group', 'panel-type'] });
-
- wrapper.vm.$store.commit(
- `monitoringDashboard/${types.SET_ALL_DASHBOARDS}`,
- dashboardGitResponse,
- );
-
+ createMountedWrapper({ hasMetrics: true });
+ setupAllDashboards(store);
return wrapper.vm.$nextTick();
});
@@ -484,15 +846,12 @@ describe('Dashboard', () => {
describe('external dashboard link', () => {
beforeEach(() => {
- createMountedWrapper(
- {
- hasMetrics: true,
- showPanels: false,
- showTimeWindowDropdown: false,
- externalDashboardUrl: '/mockUrl',
- },
- { stubs: ['graph-group', 'panel-type'] },
- );
+ createMountedWrapper({
+ hasMetrics: true,
+ showPanels: false,
+ showTimeWindowDropdown: false,
+ externalDashboardUrl: '/mockUrl',
+ });
return wrapper.vm.$nextTick();
});
@@ -507,45 +866,29 @@ describe('Dashboard', () => {
});
describe('Clipboard text in panels', () => {
- const currentDashboard = 'TEST_DASHBOARD';
+ const currentDashboard = dashboardGitResponse[1].path;
+ const panelIndex = 1; // skip expanded panel
- const getClipboardTextAt = i =>
+ const getClipboardTextFirstPanel = () =>
wrapper
- .findAll(PanelType)
- .at(i)
+ .findAll(DashboardPanel)
+ .at(panelIndex)
.props('clipboardText');
beforeEach(() => {
+ setupStoreWithData(store);
createShallowWrapper({ hasMetrics: true, currentDashboard });
- setupStoreWithData(wrapper.vm.$store);
-
return wrapper.vm.$nextTick();
});
it('contains a link to the dashboard', () => {
- expect(getClipboardTextAt(0)).toContain(`dashboard=${currentDashboard}`);
- expect(getClipboardTextAt(0)).toContain(`group=`);
- expect(getClipboardTextAt(0)).toContain(`title=`);
- expect(getClipboardTextAt(0)).toContain(`y_label=`);
- });
-
- it('strips the undefined parameter', () => {
- wrapper.setProps({ currentDashboard: undefined });
-
- return wrapper.vm.$nextTick(() => {
- expect(getClipboardTextAt(0)).not.toContain(`dashboard=`);
- expect(getClipboardTextAt(0)).toContain(`y_label=`);
- });
- });
+ const dashboardParam = `dashboard=${encodeURIComponent(currentDashboard)}`;
- it('null parameter is stripped', () => {
- wrapper.setProps({ currentDashboard: null });
-
- return wrapper.vm.$nextTick(() => {
- expect(getClipboardTextAt(0)).not.toContain(`dashboard=`);
- expect(getClipboardTextAt(0)).toContain(`y_label=`);
- });
+ expect(getClipboardTextFirstPanel()).toContain(dashboardParam);
+ expect(getClipboardTextFirstPanel()).toContain(`group=`);
+ expect(getClipboardTextFirstPanel()).toContain(`title=`);
+ expect(getClipboardTextFirstPanel()).toContain(`y_label=`);
});
});
@@ -572,7 +915,7 @@ describe('Dashboard', () => {
customMetricsPath: '/endpoint',
customMetricsAvailable: true,
});
- setupStoreWithData(wrapper.vm.$store);
+ setupStoreWithData(store);
origPage = document.body.dataset.page;
document.body.dataset.page = 'projects:environments:metrics';
diff --git a/spec/frontend/monitoring/components/dashboard_template_spec.js b/spec/frontend/monitoring/components/dashboard_template_spec.js
index d1790df4189..cc0ac348b11 100644
--- a/spec/frontend/monitoring/components/dashboard_template_spec.js
+++ b/spec/frontend/monitoring/components/dashboard_template_spec.js
@@ -3,6 +3,7 @@ import MockAdapter from 'axios-mock-adapter';
import axios from '~/lib/utils/axios_utils';
import Dashboard from '~/monitoring/components/dashboard.vue';
import { createStore } from '~/monitoring/stores';
+import { setupAllDashboards } from '../store_utils';
import { propsData } from '../mock_data';
jest.mock('~/lib/utils/url_utility');
@@ -15,24 +16,16 @@ describe('Dashboard template', () => {
beforeEach(() => {
store = createStore();
mock = new MockAdapter(axios);
+
+ setupAllDashboards(store);
});
afterEach(() => {
- if (wrapper) {
- wrapper.destroy();
- wrapper = null;
- }
mock.restore();
});
it('matches the default snapshot', () => {
- wrapper = shallowMount(Dashboard, {
- propsData: { ...propsData },
- methods: {
- fetchData: jest.fn(),
- },
- store,
- });
+ wrapper = shallowMount(Dashboard, { propsData: { ...propsData }, store });
expect(wrapper.element).toMatchSnapshot();
});
diff --git a/spec/frontend/monitoring/components/dashboard_url_time_spec.js b/spec/frontend/monitoring/components/dashboard_url_time_spec.js
index 65e9d036d1a..9bba5280007 100644
--- a/spec/frontend/monitoring/components/dashboard_url_time_spec.js
+++ b/spec/frontend/monitoring/components/dashboard_url_time_spec.js
@@ -27,7 +27,7 @@ describe('dashboard invalid url parameters', () => {
wrapper = mount(Dashboard, {
propsData: { ...propsData, ...props },
store,
- stubs: ['graph-group', 'panel-type'],
+ stubs: ['graph-group', 'dashboard-panel'],
...options,
});
};
diff --git a/spec/frontend/monitoring/components/dashboards_dropdown_spec.js b/spec/frontend/monitoring/components/dashboards_dropdown_spec.js
index 0bcfabe6415..b29d86cbc5b 100644
--- a/spec/frontend/monitoring/components/dashboards_dropdown_spec.js
+++ b/spec/frontend/monitoring/components/dashboards_dropdown_spec.js
@@ -1,5 +1,5 @@
import { shallowMount } from '@vue/test-utils';
-import { GlDropdownItem, GlModal, GlLoadingIcon, GlAlert } from '@gitlab/ui';
+import { GlDropdownItem, GlModal, GlLoadingIcon, GlAlert, GlIcon } from '@gitlab/ui';
import waitForPromises from 'helpers/wait_for_promises';
import DashboardsDropdown from '~/monitoring/components/dashboards_dropdown.vue';
@@ -9,36 +9,48 @@ import { dashboardGitResponse } from '../mock_data';
const defaultBranch = 'master';
-function createComponent(props, opts = {}) {
- const storeOpts = {
- methods: {
- duplicateSystemDashboard: jest.fn(),
- },
- computed: {
- allDashboards: () => dashboardGitResponse,
- },
- };
-
- return shallowMount(DashboardsDropdown, {
- propsData: {
- ...props,
- defaultBranch,
- },
- sync: false,
- ...storeOpts,
- ...opts,
- });
-}
+const starredDashboards = dashboardGitResponse.filter(({ starred }) => starred);
+const notStarredDashboards = dashboardGitResponse.filter(({ starred }) => !starred);
describe('DashboardsDropdown', () => {
let wrapper;
+ let mockDashboards;
+ let mockSelectedDashboard;
+
+ function createComponent(props, opts = {}) {
+ const storeOpts = {
+ methods: {
+ duplicateSystemDashboard: jest.fn(),
+ },
+ computed: {
+ allDashboards: () => mockDashboards,
+ selectedDashboard: () => mockSelectedDashboard,
+ },
+ };
+
+ return shallowMount(DashboardsDropdown, {
+ propsData: {
+ ...props,
+ defaultBranch,
+ },
+ sync: false,
+ ...storeOpts,
+ ...opts,
+ });
+ }
const findItems = () => wrapper.findAll(GlDropdownItem);
const findItemAt = i => wrapper.findAll(GlDropdownItem).at(i);
const findSearchInput = () => wrapper.find({ ref: 'monitorDashboardsDropdownSearch' });
const findNoItemsMsg = () => wrapper.find({ ref: 'monitorDashboardsDropdownMsg' });
+ const findStarredListDivider = () => wrapper.find({ ref: 'starredListDivider' });
const setSearchTerm = searchTerm => wrapper.setData({ searchTerm });
+ beforeEach(() => {
+ mockDashboards = dashboardGitResponse;
+ mockSelectedDashboard = null;
+ });
+
describe('when it receives dashboards data', () => {
beforeEach(() => {
wrapper = createComponent();
@@ -48,10 +60,14 @@ describe('DashboardsDropdown', () => {
expect(findItems().length).toEqual(dashboardGitResponse.length);
});
- it('displays items with the dashboard display name', () => {
- expect(findItemAt(0).text()).toBe(dashboardGitResponse[0].display_name);
- expect(findItemAt(1).text()).toBe(dashboardGitResponse[1].display_name);
- expect(findItemAt(2).text()).toBe(dashboardGitResponse[2].display_name);
+ it('displays items with the dashboard display name, with starred dashboards first', () => {
+ expect(findItemAt(0).text()).toBe(starredDashboards[0].display_name);
+ expect(findItemAt(1).text()).toBe(notStarredDashboards[0].display_name);
+ expect(findItemAt(2).text()).toBe(notStarredDashboards[1].display_name);
+ });
+
+ it('displays separator between starred and not starred dashboards', () => {
+ expect(findStarredListDivider().exists()).toBe(true);
});
it('displays a search input', () => {
@@ -81,18 +97,71 @@ describe('DashboardsDropdown', () => {
});
});
+ describe('when the dashboard is missing a display name', () => {
+ beforeEach(() => {
+ mockDashboards = dashboardGitResponse.map(d => ({ ...d, display_name: undefined }));
+ wrapper = createComponent();
+ });
+
+ it('displays items with the dashboard path, with starred dashboards first', () => {
+ expect(findItemAt(0).text()).toBe(starredDashboards[0].path);
+ expect(findItemAt(1).text()).toBe(notStarredDashboards[0].path);
+ expect(findItemAt(2).text()).toBe(notStarredDashboards[1].path);
+ });
+ });
+
+ describe('when it receives starred dashboards', () => {
+ beforeEach(() => {
+ mockDashboards = starredDashboards;
+ wrapper = createComponent();
+ });
+
+ it('displays an item for each dashboard', () => {
+ expect(findItems().length).toEqual(starredDashboards.length);
+ });
+
+ it('displays a star icon', () => {
+ const star = findItemAt(0).find(GlIcon);
+ expect(star.exists()).toBe(true);
+ expect(star.attributes('name')).toBe('star');
+ });
+
+ it('displays no separator between starred and not starred dashboards', () => {
+ expect(findStarredListDivider().exists()).toBe(false);
+ });
+ });
+
+ describe('when it receives only not-starred dashboards', () => {
+ beforeEach(() => {
+ mockDashboards = notStarredDashboards;
+ wrapper = createComponent();
+ });
+
+ it('displays an item for each dashboard', () => {
+ expect(findItems().length).toEqual(notStarredDashboards.length);
+ });
+
+ it('displays no star icon', () => {
+ const star = findItemAt(0).find(GlIcon);
+ expect(star.exists()).toBe(false);
+ });
+
+ it('displays no separator between starred and not starred dashboards', () => {
+ expect(findStarredListDivider().exists()).toBe(false);
+ });
+ });
+
describe('when a system dashboard is selected', () => {
let duplicateDashboardAction;
let modalDirective;
beforeEach(() => {
+ [mockSelectedDashboard] = dashboardGitResponse;
modalDirective = jest.fn();
duplicateDashboardAction = jest.fn().mockResolvedValue();
wrapper = createComponent(
- {
- selectedDashboard: dashboardGitResponse[0],
- },
+ {},
{
directives: {
GlModal: modalDirective,
@@ -260,7 +329,7 @@ describe('DashboardsDropdown', () => {
expect(wrapper.emitted().selectDashboard).toBeTruthy();
});
it('emits a "selectDashboard" event with dashboard information', () => {
- expect(wrapper.emitted().selectDashboard[0]).toEqual([dashboardGitResponse[1]]);
+ expect(wrapper.emitted().selectDashboard[0]).toEqual([dashboardGitResponse[0]]);
});
});
});
diff --git a/spec/frontend/monitoring/components/duplicate_dashboard_form_spec.js b/spec/frontend/monitoring/components/duplicate_dashboard_form_spec.js
index 10fd58f749d..216ec345552 100644
--- a/spec/frontend/monitoring/components/duplicate_dashboard_form_spec.js
+++ b/spec/frontend/monitoring/components/duplicate_dashboard_form_spec.js
@@ -81,7 +81,8 @@ describe('DuplicateDashboardForm', () => {
it('with the inital form values', () => {
expect(wrapper.emitted().change).toHaveLength(1);
- expect(lastChange()).resolves.toEqual({
+
+ return expect(lastChange()).resolves.toEqual({
branch: '',
commitMessage: expect.any(String),
dashboard: dashboardGitResponse[0].path,
@@ -92,7 +93,7 @@ describe('DuplicateDashboardForm', () => {
it('containing an inputted file name', () => {
setValue('fileName', 'my_dashboard.yml');
- expect(lastChange()).resolves.toMatchObject({
+ return expect(lastChange()).resolves.toMatchObject({
fileName: 'my_dashboard.yml',
});
});
@@ -100,7 +101,7 @@ describe('DuplicateDashboardForm', () => {
it('containing a default commit message when no message is set', () => {
setValue('commitMessage', '');
- expect(lastChange()).resolves.toMatchObject({
+ return expect(lastChange()).resolves.toMatchObject({
commitMessage: expect.stringContaining('Create custom dashboard'),
});
});
@@ -108,7 +109,7 @@ describe('DuplicateDashboardForm', () => {
it('containing an inputted commit message', () => {
setValue('commitMessage', 'My commit message');
- expect(lastChange()).resolves.toMatchObject({
+ return expect(lastChange()).resolves.toMatchObject({
commitMessage: expect.stringContaining('My commit message'),
});
});
@@ -116,7 +117,7 @@ describe('DuplicateDashboardForm', () => {
it('containing an inputted branch name', () => {
setValue('branchName', 'a-new-branch');
- expect(lastChange()).resolves.toMatchObject({
+ return expect(lastChange()).resolves.toMatchObject({
branch: 'a-new-branch',
});
});
@@ -125,13 +126,14 @@ describe('DuplicateDashboardForm', () => {
setChecked(wrapper.vm.$options.radioVals.DEFAULT);
setValue('branchName', 'a-new-branch');
- expect(lastChange()).resolves.toMatchObject({
- branch: defaultBranch,
- });
-
- return wrapper.vm.$nextTick(() => {
- expect(findByRef('branchName').isVisible()).toBe(false);
- });
+ return Promise.all([
+ expect(lastChange()).resolves.toMatchObject({
+ branch: defaultBranch,
+ }),
+ wrapper.vm.$nextTick(() => {
+ expect(findByRef('branchName').isVisible()).toBe(false);
+ }),
+ ]);
});
it('when `new` branch option is chosen, focuses on the branch name input', () => {
diff --git a/spec/frontend/monitoring/components/embeds/metric_embed_spec.js b/spec/frontend/monitoring/components/embeds/metric_embed_spec.js
index b829cd53479..f23823ccad6 100644
--- a/spec/frontend/monitoring/components/embeds/metric_embed_spec.js
+++ b/spec/frontend/monitoring/components/embeds/metric_embed_spec.js
@@ -1,6 +1,6 @@
import { createLocalVue, shallowMount } from '@vue/test-utils';
import Vuex from 'vuex';
-import PanelType from 'ee_else_ce/monitoring/components/panel_type.vue';
+import DashboardPanel from '~/monitoring/components/dashboard_panel.vue';
import { TEST_HOST } from 'helpers/test_constants';
import MetricEmbed from '~/monitoring/components/embeds/metric_embed.vue';
import { groups, initialState, metricsData, metricsWithData } from './mock_data';
@@ -62,7 +62,7 @@ describe('MetricEmbed', () => {
it('shows an empty state when no metrics are present', () => {
expect(wrapper.find('.metrics-embed').exists()).toBe(true);
- expect(wrapper.find(PanelType).exists()).toBe(false);
+ expect(wrapper.find(DashboardPanel).exists()).toBe(false);
});
});
@@ -90,12 +90,12 @@ describe('MetricEmbed', () => {
it('shows a chart when metrics are present', () => {
expect(wrapper.find('.metrics-embed').exists()).toBe(true);
- expect(wrapper.find(PanelType).exists()).toBe(true);
- expect(wrapper.findAll(PanelType).length).toBe(2);
+ expect(wrapper.find(DashboardPanel).exists()).toBe(true);
+ expect(wrapper.findAll(DashboardPanel).length).toBe(2);
});
it('includes groupId with dashboardUrl', () => {
- expect(wrapper.find(PanelType).props('groupId')).toBe(TEST_HOST);
+ expect(wrapper.find(DashboardPanel).props('groupId')).toBe(TEST_HOST);
});
});
});
diff --git a/spec/frontend/monitoring/components/panel_type_spec.js b/spec/frontend/monitoring/components/panel_type_spec.js
deleted file mode 100644
index 819b5235284..00000000000
--- a/spec/frontend/monitoring/components/panel_type_spec.js
+++ /dev/null
@@ -1,408 +0,0 @@
-import { shallowMount } from '@vue/test-utils';
-import AxiosMockAdapter from 'axios-mock-adapter';
-import { setTestTimeout } from 'helpers/timeout';
-import invalidUrl from '~/lib/utils/invalid_url';
-import axios from '~/lib/utils/axios_utils';
-
-import PanelType from '~/monitoring/components/panel_type.vue';
-import EmptyChart from '~/monitoring/components/charts/empty_chart.vue';
-import TimeSeriesChart from '~/monitoring/components/charts/time_series.vue';
-import AnomalyChart from '~/monitoring/components/charts/anomaly.vue';
-import {
- anomalyMockGraphData,
- mockLogsHref,
- mockLogsPath,
- mockNamespace,
- mockNamespacedData,
- mockTimeRange,
-} from '../mock_data';
-
-import { graphData, graphDataEmpty } from '../fixture_data';
-import { createStore, monitoringDashboard } from '~/monitoring/stores';
-import { createStore as createEmbedGroupStore } from '~/monitoring/stores/embed_group';
-
-global.URL.createObjectURL = jest.fn();
-
-const mocks = {
- $toast: {
- show: jest.fn(),
- },
-};
-
-describe('Panel Type component', () => {
- let axiosMock;
- let store;
- let state;
- let wrapper;
-
- const exampleText = 'example_text';
-
- const findCopyLink = () => wrapper.find({ ref: 'copyChartLink' });
- const findTimeChart = () => wrapper.find({ ref: 'timeChart' });
- const findTitle = () => wrapper.find({ ref: 'graphTitle' });
- const findContextualMenu = () => wrapper.find({ ref: 'contextualMenu' });
-
- const createWrapper = props => {
- wrapper = shallowMount(PanelType, {
- propsData: {
- graphData,
- ...props,
- },
- store,
- mocks,
- });
- };
-
- beforeEach(() => {
- setTestTimeout(1000);
-
- store = createStore();
- state = store.state.monitoringDashboard;
-
- axiosMock = new AxiosMockAdapter(axios);
- });
-
- afterEach(() => {
- axiosMock.reset();
- });
-
- describe('When no graphData is available', () => {
- beforeEach(() => {
- createWrapper({
- graphData: graphDataEmpty,
- });
- });
-
- afterEach(() => {
- wrapper.destroy();
- });
-
- describe('Empty Chart component', () => {
- it('renders the chart title', () => {
- expect(findTitle().text()).toBe(graphDataEmpty.title);
- });
-
- it('renders the no download csv link', () => {
- expect(wrapper.find({ ref: 'downloadCsvLink' }).exists()).toBe(false);
- });
-
- it('does not contain graph widgets', () => {
- expect(findContextualMenu().exists()).toBe(false);
- });
-
- it('is a Vue instance', () => {
- expect(wrapper.find(EmptyChart).exists()).toBe(true);
- expect(wrapper.find(EmptyChart).isVueInstance()).toBe(true);
- });
- });
- });
-
- describe('when graph data is available', () => {
- beforeEach(() => {
- createWrapper();
- });
-
- afterEach(() => {
- wrapper.destroy();
- });
-
- it('renders the chart title', () => {
- expect(findTitle().text()).toBe(graphData.title);
- });
-
- it('contains graph widgets', () => {
- expect(findContextualMenu().exists()).toBe(true);
- expect(wrapper.find({ ref: 'downloadCsvLink' }).exists()).toBe(true);
- });
-
- it('sets no clipboard copy link on dropdown by default', () => {
- expect(findCopyLink().exists()).toBe(false);
- });
-
- it('should emit `timerange` event when a zooming in/out in a chart occcurs', () => {
- const timeRange = {
- start: '2020-01-01T00:00:00.000Z',
- end: '2020-01-01T01:00:00.000Z',
- };
-
- jest.spyOn(wrapper.vm, '$emit');
-
- findTimeChart().vm.$emit('datazoom', timeRange);
-
- return wrapper.vm.$nextTick(() => {
- expect(wrapper.vm.$emit).toHaveBeenCalledWith('timerangezoom', timeRange);
- });
- });
-
- describe('Time Series Chart panel type', () => {
- it('is rendered', () => {
- expect(wrapper.find(TimeSeriesChart).isVueInstance()).toBe(true);
- expect(wrapper.find(TimeSeriesChart).exists()).toBe(true);
- });
-
- it('includes a default group id', () => {
- expect(wrapper.vm.groupId).toBe('panel-type-chart');
- });
- });
-
- describe('Anomaly Chart panel type', () => {
- beforeEach(() => {
- wrapper.setProps({
- graphData: anomalyMockGraphData,
- });
- return wrapper.vm.$nextTick();
- });
-
- it('is rendered with an anomaly chart', () => {
- expect(wrapper.find(AnomalyChart).isVueInstance()).toBe(true);
- expect(wrapper.find(AnomalyChart).exists()).toBe(true);
- });
- });
- });
-
- describe('Edit custom metric dropdown item', () => {
- const findEditCustomMetricLink = () => wrapper.find({ ref: 'editMetricLink' });
-
- beforeEach(() => {
- createWrapper();
-
- return wrapper.vm.$nextTick();
- });
-
- it('is not present if the panel is not a custom metric', () => {
- expect(findEditCustomMetricLink().exists()).toBe(false);
- });
-
- it('is present when the panel contains an edit_path property', () => {
- wrapper.setProps({
- graphData: {
- ...graphData,
- metrics: [
- {
- ...graphData.metrics[0],
- edit_path: '/root/kubernetes-gke-project/prometheus/metrics/23/edit',
- },
- ],
- },
- });
-
- return wrapper.vm.$nextTick(() => {
- expect(findEditCustomMetricLink().exists()).toBe(true);
- expect(findEditCustomMetricLink().text()).toBe('Edit metric');
- });
- });
-
- it('shows an "Edit metrics" link for a panel with multiple metrics', () => {
- wrapper.setProps({
- graphData: {
- ...graphData,
- metrics: [
- {
- ...graphData.metrics[0],
- edit_path: '/root/kubernetes-gke-project/prometheus/metrics/23/edit',
- },
- {
- ...graphData.metrics[0],
- edit_path: '/root/kubernetes-gke-project/prometheus/metrics/23/edit',
- },
- ],
- },
- });
-
- return wrapper.vm.$nextTick(() => {
- expect(findEditCustomMetricLink().text()).toBe('Edit metrics');
- });
- });
- });
-
- describe('View Logs dropdown item', () => {
- const findViewLogsLink = () => wrapper.find({ ref: 'viewLogsLink' });
-
- beforeEach(() => {
- createWrapper();
- return wrapper.vm.$nextTick();
- });
-
- it('is not present by default', () =>
- wrapper.vm.$nextTick(() => {
- expect(findViewLogsLink().exists()).toBe(false);
- }));
-
- it('is not present if a time range is not set', () => {
- state.logsPath = mockLogsPath;
- state.timeRange = null;
-
- return wrapper.vm.$nextTick(() => {
- expect(findViewLogsLink().exists()).toBe(false);
- });
- });
-
- it('is not present if the logs path is default', () => {
- state.logsPath = invalidUrl;
- state.timeRange = mockTimeRange;
-
- return wrapper.vm.$nextTick(() => {
- expect(findViewLogsLink().exists()).toBe(false);
- });
- });
-
- it('is not present if the logs path is not set', () => {
- state.logsPath = null;
- state.timeRange = mockTimeRange;
-
- return wrapper.vm.$nextTick(() => {
- expect(findViewLogsLink().exists()).toBe(false);
- });
- });
-
- it('is present when logs path and time a range is present', () => {
- state.logsPath = mockLogsPath;
- state.timeRange = mockTimeRange;
-
- return wrapper.vm.$nextTick(() => {
- expect(findViewLogsLink().attributes('href')).toMatch(mockLogsHref);
- });
- });
-
- it('it is overriden when a datazoom event is received', () => {
- state.logsPath = mockLogsPath;
- state.timeRange = mockTimeRange;
-
- const zoomedTimeRange = {
- start: '2020-01-01T00:00:00.000Z',
- end: '2020-01-01T01:00:00.000Z',
- };
-
- findTimeChart().vm.$emit('datazoom', zoomedTimeRange);
-
- return wrapper.vm.$nextTick(() => {
- const start = encodeURIComponent(zoomedTimeRange.start);
- const end = encodeURIComponent(zoomedTimeRange.end);
- expect(findViewLogsLink().attributes('href')).toMatch(
- `${mockLogsPath}?start=${start}&end=${end}`,
- );
- });
- });
- });
-
- describe('when cliboard data is available', () => {
- const clipboardText = 'A value to copy.';
-
- beforeEach(() => {
- createWrapper({
- clipboardText,
- });
- });
-
- afterEach(() => {
- wrapper.destroy();
- });
-
- it('sets clipboard text on the dropdown', () => {
- expect(findCopyLink().exists()).toBe(true);
- expect(findCopyLink().element.dataset.clipboardText).toBe(clipboardText);
- });
-
- it('adds a copy button to the dropdown', () => {
- expect(findCopyLink().text()).toContain('Copy link to chart');
- });
-
- it('opens a toast on click', () => {
- findCopyLink().vm.$emit('click');
-
- expect(wrapper.vm.$toast.show).toHaveBeenCalled();
- });
- });
-
- describe('when downloading metrics data as CSV', () => {
- beforeEach(() => {
- wrapper = shallowMount(PanelType, {
- propsData: {
- clipboardText: exampleText,
- graphData: {
- y_label: 'metric',
- ...graphData,
- },
- },
- store,
- });
- return wrapper.vm.$nextTick();
- });
-
- afterEach(() => {
- wrapper.destroy();
- });
-
- describe('csvText', () => {
- it('converts metrics data from json to csv', () => {
- const header = `timestamp,${graphData.y_label}`;
- const data = graphData.metrics[0].result[0].values;
- const firstRow = `${data[0][0]},${data[0][1]}`;
- const secondRow = `${data[1][0]},${data[1][1]}`;
-
- expect(wrapper.vm.csvText).toMatch(`${header}\r\n${firstRow}\r\n${secondRow}\r\n`);
- });
- });
-
- describe('downloadCsv', () => {
- it('produces a link with a Blob', () => {
- expect(global.URL.createObjectURL).toHaveBeenLastCalledWith(expect.any(Blob));
- expect(global.URL.createObjectURL).toHaveBeenLastCalledWith(
- expect.objectContaining({
- size: wrapper.vm.csvText.length,
- type: 'text/plain',
- }),
- );
- });
- });
- });
-
- describe('when using dynamic modules', () => {
- const { mockDeploymentData, mockProjectPath } = mockNamespacedData;
-
- beforeEach(() => {
- store = createEmbedGroupStore();
- store.registerModule(mockNamespace, monitoringDashboard);
- store.state.embedGroup.modules.push(mockNamespace);
-
- wrapper = shallowMount(PanelType, {
- propsData: {
- graphData,
- namespace: mockNamespace,
- },
- store,
- mocks,
- });
- });
-
- it('handles namespaced time range and logs path state', () => {
- store.state[mockNamespace].timeRange = mockTimeRange;
- store.state[mockNamespace].logsPath = mockLogsPath;
-
- return wrapper.vm.$nextTick().then(() => {
- expect(wrapper.find({ ref: 'viewLogsLink' }).attributes().href).toBe(mockLogsHref);
- });
- });
-
- it('handles namespaced deployment data state', () => {
- store.state[mockNamespace].deploymentData = mockDeploymentData;
-
- return wrapper.vm.$nextTick().then(() => {
- expect(findTimeChart().props().deploymentData).toEqual(mockDeploymentData);
- });
- });
-
- it('handles namespaced project path state', () => {
- store.state[mockNamespace].projectPath = mockProjectPath;
-
- return wrapper.vm.$nextTick().then(() => {
- expect(findTimeChart().props().projectPath).toBe(mockProjectPath);
- });
- });
-
- it('it renders a time series chart with no errors', () => {
- expect(wrapper.find(TimeSeriesChart).isVueInstance()).toBe(true);
- expect(wrapper.find(TimeSeriesChart).exists()).toBe(true);
- });
- });
-});
diff --git a/spec/frontend/monitoring/components/variables/custom_variable_spec.js b/spec/frontend/monitoring/components/variables/custom_variable_spec.js
new file mode 100644
index 00000000000..5a2b26219b6
--- /dev/null
+++ b/spec/frontend/monitoring/components/variables/custom_variable_spec.js
@@ -0,0 +1,52 @@
+import { shallowMount } from '@vue/test-utils';
+import { GlDropdown, GlDropdownItem } from '@gitlab/ui';
+import CustomVariable from '~/monitoring/components/variables/custom_variable.vue';
+
+describe('Custom variable component', () => {
+ let wrapper;
+ const propsData = {
+ name: 'env',
+ label: 'Select environment',
+ value: 'Production',
+ options: [{ text: 'Production', value: 'prod' }, { text: 'Canary', value: 'canary' }],
+ };
+ const createShallowWrapper = () => {
+ wrapper = shallowMount(CustomVariable, {
+ propsData,
+ });
+ };
+
+ const findDropdown = () => wrapper.find(GlDropdown);
+ const findDropdownItems = () => wrapper.findAll(GlDropdownItem);
+
+ it('renders dropdown element when all necessary props are passed', () => {
+ createShallowWrapper();
+
+ expect(findDropdown()).toExist();
+ });
+
+ it('renders dropdown element with a text', () => {
+ createShallowWrapper();
+
+ expect(findDropdown().attributes('text')).toBe(propsData.value);
+ });
+
+ it('renders all the dropdown items', () => {
+ createShallowWrapper();
+
+ expect(findDropdownItems()).toHaveLength(propsData.options.length);
+ });
+
+ it('changing dropdown items triggers update', () => {
+ createShallowWrapper();
+ jest.spyOn(wrapper.vm, '$emit');
+
+ findDropdownItems()
+ .at(1)
+ .vm.$emit('click');
+
+ return wrapper.vm.$nextTick(() => {
+ expect(wrapper.vm.$emit).toHaveBeenCalledWith('onUpdate', 'env', 'canary');
+ });
+ });
+});
diff --git a/spec/frontend/monitoring/components/variables/text_variable_spec.js b/spec/frontend/monitoring/components/variables/text_variable_spec.js
new file mode 100644
index 00000000000..f01584ae8bc
--- /dev/null
+++ b/spec/frontend/monitoring/components/variables/text_variable_spec.js
@@ -0,0 +1,59 @@
+import { shallowMount } from '@vue/test-utils';
+import { GlFormInput } from '@gitlab/ui';
+import TextVariable from '~/monitoring/components/variables/text_variable.vue';
+
+describe('Text variable component', () => {
+ let wrapper;
+ const propsData = {
+ name: 'pod',
+ label: 'Select pod',
+ value: 'test-pod',
+ };
+ const createShallowWrapper = () => {
+ wrapper = shallowMount(TextVariable, {
+ propsData,
+ });
+ };
+
+ const findInput = () => wrapper.find(GlFormInput);
+
+ it('renders a text input when all props are passed', () => {
+ createShallowWrapper();
+
+ expect(findInput()).toExist();
+ });
+
+ it('always has a default value', () => {
+ createShallowWrapper();
+
+ return wrapper.vm.$nextTick(() => {
+ expect(findInput().attributes('value')).toBe(propsData.value);
+ });
+ });
+
+ it('triggers keyup enter', () => {
+ createShallowWrapper();
+ jest.spyOn(wrapper.vm, '$emit');
+
+ findInput().element.value = 'prod-pod';
+ findInput().trigger('input');
+ findInput().trigger('keyup.enter');
+
+ return wrapper.vm.$nextTick(() => {
+ expect(wrapper.vm.$emit).toHaveBeenCalledWith('onUpdate', 'pod', 'prod-pod');
+ });
+ });
+
+ it('triggers blur enter', () => {
+ createShallowWrapper();
+ jest.spyOn(wrapper.vm, '$emit');
+
+ findInput().element.value = 'canary-pod';
+ findInput().trigger('input');
+ findInput().trigger('blur');
+
+ return wrapper.vm.$nextTick(() => {
+ expect(wrapper.vm.$emit).toHaveBeenCalledWith('onUpdate', 'pod', 'canary-pod');
+ });
+ });
+});
diff --git a/spec/frontend/monitoring/components/variables_section_spec.js b/spec/frontend/monitoring/components/variables_section_spec.js
new file mode 100644
index 00000000000..095d89c9231
--- /dev/null
+++ b/spec/frontend/monitoring/components/variables_section_spec.js
@@ -0,0 +1,126 @@
+import { shallowMount } from '@vue/test-utils';
+import Vuex from 'vuex';
+import VariablesSection from '~/monitoring/components/variables_section.vue';
+import CustomVariable from '~/monitoring/components/variables/custom_variable.vue';
+import TextVariable from '~/monitoring/components/variables/text_variable.vue';
+import { updateHistory, mergeUrlParams } from '~/lib/utils/url_utility';
+import { createStore } from '~/monitoring/stores';
+import { convertVariablesForURL } from '~/monitoring/utils';
+import * as types from '~/monitoring/stores/mutation_types';
+import { mockTemplatingDataResponses } from '../mock_data';
+
+jest.mock('~/lib/utils/url_utility', () => ({
+ updateHistory: jest.fn(),
+ mergeUrlParams: jest.fn(),
+}));
+
+describe('Metrics dashboard/variables section component', () => {
+ let store;
+ let wrapper;
+ const sampleVariables = {
+ label1: mockTemplatingDataResponses.simpleText.simpleText,
+ label2: mockTemplatingDataResponses.advText.advText,
+ label3: mockTemplatingDataResponses.simpleCustom.simpleCustom,
+ };
+
+ const createShallowWrapper = () => {
+ wrapper = shallowMount(VariablesSection, {
+ store,
+ });
+ };
+
+ const findTextInput = () => wrapper.findAll(TextVariable);
+ const findCustomInput = () => wrapper.findAll(CustomVariable);
+
+ beforeEach(() => {
+ store = createStore();
+
+ store.state.monitoringDashboard.showEmptyState = false;
+ });
+
+ it('does not show the variables section', () => {
+ createShallowWrapper();
+ const allInputs = findTextInput().length + findCustomInput().length;
+
+ expect(allInputs).toBe(0);
+ });
+
+ it('shows the variables section', () => {
+ createShallowWrapper();
+ store.commit(`monitoringDashboard/${types.SET_VARIABLES}`, sampleVariables);
+
+ return wrapper.vm.$nextTick(() => {
+ const allInputs = findTextInput().length + findCustomInput().length;
+
+ expect(allInputs).toBe(Object.keys(sampleVariables).length);
+ });
+ });
+
+ describe('when changing the variable inputs', () => {
+ const fetchDashboardData = jest.fn();
+ const updateVariableValues = jest.fn();
+
+ beforeEach(() => {
+ store = new Vuex.Store({
+ modules: {
+ monitoringDashboard: {
+ namespaced: true,
+ state: {
+ showEmptyState: false,
+ promVariables: sampleVariables,
+ },
+ actions: {
+ fetchDashboardData,
+ updateVariableValues,
+ },
+ },
+ },
+ });
+
+ createShallowWrapper();
+ });
+
+ it('merges the url params and refreshes the dashboard when a text-based variables inputs are updated', () => {
+ const firstInput = findTextInput().at(0);
+
+ firstInput.vm.$emit('onUpdate', 'label1', 'test');
+
+ return wrapper.vm.$nextTick(() => {
+ expect(updateVariableValues).toHaveBeenCalled();
+ expect(mergeUrlParams).toHaveBeenCalledWith(
+ convertVariablesForURL(sampleVariables),
+ window.location.href,
+ );
+ expect(updateHistory).toHaveBeenCalled();
+ expect(fetchDashboardData).toHaveBeenCalled();
+ });
+ });
+
+ it('merges the url params and refreshes the dashboard when a custom-based variables inputs are updated', () => {
+ const firstInput = findCustomInput().at(0);
+
+ firstInput.vm.$emit('onUpdate', 'label1', 'test');
+
+ return wrapper.vm.$nextTick(() => {
+ expect(updateVariableValues).toHaveBeenCalled();
+ expect(mergeUrlParams).toHaveBeenCalledWith(
+ convertVariablesForURL(sampleVariables),
+ window.location.href,
+ );
+ expect(updateHistory).toHaveBeenCalled();
+ expect(fetchDashboardData).toHaveBeenCalled();
+ });
+ });
+
+ it('does not merge the url params and refreshes the dashboard if the value entered is not different that is what currently stored', () => {
+ const firstInput = findTextInput().at(0);
+
+ firstInput.vm.$emit('onUpdate', 'label1', 'Simple text');
+
+ expect(updateVariableValues).not.toHaveBeenCalled();
+ expect(mergeUrlParams).not.toHaveBeenCalled();
+ expect(updateHistory).not.toHaveBeenCalled();
+ expect(fetchDashboardData).not.toHaveBeenCalled();
+ });
+ });
+});
diff --git a/spec/frontend/monitoring/mock_data.js b/spec/frontend/monitoring/mock_data.js
index 56236918c68..4611e6f1b18 100644
--- a/spec/frontend/monitoring/mock_data.js
+++ b/spec/frontend/monitoring/mock_data.js
@@ -34,6 +34,7 @@ const customDashboardsData = new Array(30).fill(null).map((_, idx) => ({
system_dashboard: false,
project_blob_path: `${mockProjectDir}/blob/master/dashboards/.gitlab/dashboards/dashboard_${idx}.yml`,
path: `.gitlab/dashboards/dashboard_${idx}.yml`,
+ starred: false,
}));
export const mockDashboardsErrorResponse = {
@@ -323,6 +324,18 @@ export const dashboardGitResponse = [
system_dashboard: true,
project_blob_path: null,
path: 'config/prometheus/common_metrics.yml',
+ starred: false,
+ user_starred_path: `${mockProjectDir}/metrics_user_starred_dashboards?dashboard_path=config/prometheus/common_metrics.yml`,
+ },
+ {
+ default: false,
+ display_name: 'dashboard.yml',
+ can_edit: true,
+ system_dashboard: false,
+ project_blob_path: `${mockProjectDir}/-/blob/master/.gitlab/dashboards/dashboard.yml`,
+ path: '.gitlab/dashboards/dashboard.yml',
+ starred: true,
+ user_starred_path: `${mockProjectDir}/metrics_user_starred_dashboards?dashboard_path=.gitlab/dashboards/dashboard.yml`,
},
...customDashboardsData,
];
@@ -341,7 +354,7 @@ export const metricsResult = [
},
];
-export const graphDataPrometheusQuery = {
+export const singleStatMetricsResult = {
title: 'Super Chart A2',
type: 'single-stat',
weight: 2,
@@ -489,7 +502,7 @@ export const stackedColumnMockedData = {
export const barMockData = {
title: 'SLA Trends - Primary Services',
- type: 'bar-chart',
+ type: 'bar',
xLabel: 'service',
y_label: 'percentile',
metrics: [
@@ -549,3 +562,217 @@ export const mockNamespacedData = {
export const mockLogsPath = '/mockLogsPath';
export const mockLogsHref = `${mockLogsPath}?duration_seconds=${mockTimeRange.duration.seconds}`;
+
+const templatingVariableTypes = {
+ text: {
+ simple: 'Simple text',
+ advanced: {
+ label: 'Variable 4',
+ type: 'text',
+ options: {
+ default_value: 'default',
+ },
+ },
+ },
+ custom: {
+ simple: ['value1', 'value2', 'value3'],
+ advanced: {
+ normal: {
+ label: 'Advanced Var',
+ type: 'custom',
+ options: {
+ values: [
+ { value: 'value1', text: 'Var 1 Option 1' },
+ {
+ value: 'value2',
+ text: 'Var 1 Option 2',
+ default: true,
+ },
+ ],
+ },
+ },
+ withoutOpts: {
+ type: 'custom',
+ options: {},
+ },
+ withoutLabel: {
+ type: 'custom',
+ options: {
+ values: [
+ { value: 'value1', text: 'Var 1 Option 1' },
+ {
+ value: 'value2',
+ text: 'Var 1 Option 2',
+ default: true,
+ },
+ ],
+ },
+ },
+ withoutType: {
+ label: 'Variable 2',
+ options: {
+ values: [
+ { value: 'value1', text: 'Var 1 Option 1' },
+ {
+ value: 'value2',
+ text: 'Var 1 Option 2',
+ default: true,
+ },
+ ],
+ },
+ },
+ },
+ },
+};
+
+const generateMockTemplatingData = data => {
+ const vars = data
+ ? {
+ variables: {
+ ...data,
+ },
+ }
+ : {};
+ return {
+ dashboard: {
+ templating: vars,
+ },
+ };
+};
+
+const responseForSimpleTextVariable = {
+ simpleText: {
+ label: 'simpleText',
+ type: 'text',
+ value: 'Simple text',
+ },
+};
+
+const responseForAdvTextVariable = {
+ advText: {
+ label: 'Variable 4',
+ type: 'text',
+ value: 'default',
+ },
+};
+
+const responseForSimpleCustomVariable = {
+ simpleCustom: {
+ label: 'simpleCustom',
+ value: 'value1',
+ options: [
+ {
+ default: false,
+ text: 'value1',
+ value: 'value1',
+ },
+ {
+ default: false,
+ text: 'value2',
+ value: 'value2',
+ },
+ {
+ default: false,
+ text: 'value3',
+ value: 'value3',
+ },
+ ],
+ type: 'custom',
+ },
+};
+
+const responseForAdvancedCustomVariableWithoutOptions = {
+ advCustomWithoutOpts: {
+ label: 'advCustomWithoutOpts',
+ options: [],
+ type: 'custom',
+ },
+};
+
+const responseForAdvancedCustomVariableWithoutLabel = {
+ advCustomWithoutLabel: {
+ label: 'advCustomWithoutLabel',
+ value: 'value2',
+ options: [
+ {
+ default: false,
+ text: 'Var 1 Option 1',
+ value: 'value1',
+ },
+ {
+ default: true,
+ text: 'Var 1 Option 2',
+ value: 'value2',
+ },
+ ],
+ type: 'custom',
+ },
+};
+
+const responseForAdvancedCustomVariable = {
+ ...responseForSimpleCustomVariable,
+ advCustomNormal: {
+ label: 'Advanced Var',
+ value: 'value2',
+ options: [
+ {
+ default: false,
+ text: 'Var 1 Option 1',
+ value: 'value1',
+ },
+ {
+ default: true,
+ text: 'Var 1 Option 2',
+ value: 'value2',
+ },
+ ],
+ type: 'custom',
+ },
+};
+
+const responsesForAllVariableTypes = {
+ ...responseForSimpleTextVariable,
+ ...responseForAdvTextVariable,
+ ...responseForSimpleCustomVariable,
+ ...responseForAdvancedCustomVariable,
+};
+
+export const mockTemplatingData = {
+ emptyTemplatingProp: generateMockTemplatingData(),
+ emptyVariablesProp: generateMockTemplatingData({}),
+ simpleText: generateMockTemplatingData({ simpleText: templatingVariableTypes.text.simple }),
+ advText: generateMockTemplatingData({ advText: templatingVariableTypes.text.advanced }),
+ simpleCustom: generateMockTemplatingData({ simpleCustom: templatingVariableTypes.custom.simple }),
+ advCustomWithoutOpts: generateMockTemplatingData({
+ advCustomWithoutOpts: templatingVariableTypes.custom.advanced.withoutOpts,
+ }),
+ advCustomWithoutType: generateMockTemplatingData({
+ advCustomWithoutType: templatingVariableTypes.custom.advanced.withoutType,
+ }),
+ advCustomWithoutLabel: generateMockTemplatingData({
+ advCustomWithoutLabel: templatingVariableTypes.custom.advanced.withoutLabel,
+ }),
+ simpleAndAdv: generateMockTemplatingData({
+ simpleCustom: templatingVariableTypes.custom.simple,
+ advCustomNormal: templatingVariableTypes.custom.advanced.normal,
+ }),
+ allVariableTypes: generateMockTemplatingData({
+ simpleText: templatingVariableTypes.text.simple,
+ advText: templatingVariableTypes.text.advanced,
+ simpleCustom: templatingVariableTypes.custom.simple,
+ advCustomNormal: templatingVariableTypes.custom.advanced.normal,
+ }),
+};
+
+export const mockTemplatingDataResponses = {
+ emptyTemplatingProp: {},
+ emptyVariablesProp: {},
+ simpleText: responseForSimpleTextVariable,
+ advText: responseForAdvTextVariable,
+ simpleCustom: responseForSimpleCustomVariable,
+ advCustomWithoutOpts: responseForAdvancedCustomVariableWithoutOptions,
+ advCustomWithoutType: {},
+ advCustomWithoutLabel: responseForAdvancedCustomVariableWithoutLabel,
+ simpleAndAdv: responseForAdvancedCustomVariable,
+ allVariableTypes: responsesForAllVariableTypes,
+};
diff --git a/spec/frontend/monitoring/store/actions_spec.js b/spec/frontend/monitoring/store/actions_spec.js
index f312aa1fd34..8914f2e66ea 100644
--- a/spec/frontend/monitoring/store/actions_spec.js
+++ b/spec/frontend/monitoring/store/actions_spec.js
@@ -11,17 +11,22 @@ import { ENVIRONMENT_AVAILABLE_STATE } from '~/monitoring/constants';
import store from '~/monitoring/stores';
import * as types from '~/monitoring/stores/mutation_types';
import {
+ fetchData,
fetchDashboard,
receiveMetricsDashboardSuccess,
fetchDeploymentsData,
fetchEnvironmentsData,
fetchDashboardData,
fetchAnnotations,
+ toggleStarredValue,
fetchPrometheusMetric,
setInitialState,
filterEnvironments,
+ setExpandedPanel,
+ clearExpandedPanel,
setGettingStartedEmptyState,
duplicateSystemDashboard,
+ updateVariableValues,
} from '~/monitoring/stores/actions';
import {
gqClient,
@@ -35,6 +40,7 @@ import {
deploymentData,
environmentData,
annotationsData,
+ mockTemplatingData,
dashboardGitResponse,
mockDashboardsErrorResponse,
} from '../mock_data';
@@ -62,9 +68,6 @@ describe('Monitoring store actions', () => {
beforeEach(() => {
mock = new MockAdapter(axios);
- // Mock `backOff` function to remove exponential algorithm delay.
- jest.useFakeTimers();
-
jest.spyOn(commonUtils, 'backOff').mockImplementation(callback => {
const q = new Promise((resolve, reject) => {
const stop = arg => (arg instanceof Error ? reject(arg) : resolve(arg));
@@ -87,6 +90,45 @@ describe('Monitoring store actions', () => {
createFlash.mockReset();
});
+ describe('fetchData', () => {
+ it('dispatches fetchEnvironmentsData and fetchEnvironmentsData', () => {
+ const { state } = store;
+
+ return testAction(
+ fetchData,
+ null,
+ state,
+ [],
+ [
+ { type: 'fetchEnvironmentsData' },
+ { type: 'fetchDashboard' },
+ { type: 'fetchAnnotations' },
+ ],
+ );
+ });
+
+ it('dispatches when feature metricsDashboardAnnotations is on', () => {
+ const origGon = window.gon;
+ window.gon = { features: { metricsDashboardAnnotations: true } };
+
+ const { state } = store;
+
+ return testAction(
+ fetchData,
+ null,
+ state,
+ [],
+ [
+ { type: 'fetchEnvironmentsData' },
+ { type: 'fetchDashboard' },
+ { type: 'fetchAnnotations' },
+ ],
+ ).then(() => {
+ window.gon = origGon;
+ });
+ });
+ });
+
describe('fetchDeploymentsData', () => {
it('dispatches receiveDeploymentsDataSuccess on success', () => {
const { state } = store;
@@ -310,6 +352,49 @@ describe('Monitoring store actions', () => {
});
});
+ describe('Toggles starred value of current dashboard', () => {
+ const { state } = store;
+ let unstarredDashboard;
+ let starredDashboard;
+
+ beforeEach(() => {
+ state.isUpdatingStarredValue = false;
+ [unstarredDashboard, starredDashboard] = dashboardGitResponse;
+ });
+
+ describe('toggleStarredValue', () => {
+ it('performs no changes if no dashboard is selected', () => {
+ return testAction(toggleStarredValue, null, state, [], []);
+ });
+
+ it('performs no changes if already changing starred value', () => {
+ state.selectedDashboard = unstarredDashboard;
+ state.isUpdatingStarredValue = true;
+ return testAction(toggleStarredValue, null, state, [], []);
+ });
+
+ it('stars dashboard if it is not starred', () => {
+ state.selectedDashboard = unstarredDashboard;
+ mock.onPost(unstarredDashboard.user_starred_path).reply(200);
+
+ return testAction(toggleStarredValue, null, state, [
+ { type: types.REQUEST_DASHBOARD_STARRING },
+ { type: types.RECEIVE_DASHBOARD_STARRING_SUCCESS, payload: true },
+ ]);
+ });
+
+ it('unstars dashboard if it is starred', () => {
+ state.selectedDashboard = starredDashboard;
+ mock.onPost(starredDashboard.user_starred_path).reply(200);
+
+ return testAction(toggleStarredValue, null, state, [
+ { type: types.REQUEST_DASHBOARD_STARRING },
+ { type: types.RECEIVE_DASHBOARD_STARRING_FAILURE },
+ ]);
+ });
+ });
+ });
+
describe('Set initial state', () => {
let mockedState;
beforeEach(() => {
@@ -357,6 +442,29 @@ describe('Monitoring store actions', () => {
);
});
});
+
+ describe('updateVariableValues', () => {
+ let mockedState;
+ beforeEach(() => {
+ mockedState = storeState();
+ });
+ it('should commit UPDATE_VARIABLE_VALUES mutation', done => {
+ testAction(
+ updateVariableValues,
+ { pod: 'POD' },
+ mockedState,
+ [
+ {
+ type: types.UPDATE_VARIABLE_VALUES,
+ payload: { pod: 'POD' },
+ },
+ ],
+ [],
+ done,
+ );
+ });
+ });
+
describe('fetchDashboard', () => {
let dispatch;
let state;
@@ -467,6 +575,33 @@ describe('Monitoring store actions', () => {
);
expect(dispatch).toHaveBeenCalledWith('fetchDashboardData');
});
+
+ it('stores templating variables', () => {
+ const response = {
+ ...metricsDashboardResponse.dashboard,
+ ...mockTemplatingData.allVariableTypes.dashboard,
+ };
+
+ receiveMetricsDashboardSuccess(
+ { state, commit, dispatch },
+ {
+ response: {
+ ...metricsDashboardResponse,
+ dashboard: {
+ ...metricsDashboardResponse.dashboard,
+ ...mockTemplatingData.allVariableTypes.dashboard,
+ },
+ },
+ },
+ );
+
+ expect(commit).toHaveBeenCalledWith(
+ types.RECEIVE_METRICS_DASHBOARD_SUCCESS,
+
+ response,
+ );
+ });
+
it('sets the dashboards loaded from the repository', () => {
const params = {};
const response = metricsDashboardResponse;
@@ -873,4 +1008,43 @@ describe('Monitoring store actions', () => {
});
});
});
+
+ describe('setExpandedPanel', () => {
+ let state;
+
+ beforeEach(() => {
+ state = storeState();
+ });
+
+ it('Sets a panel as expanded', () => {
+ const group = 'group_1';
+ const panel = { title: 'A Panel' };
+
+ return testAction(
+ setExpandedPanel,
+ { group, panel },
+ state,
+ [{ type: types.SET_EXPANDED_PANEL, payload: { group, panel } }],
+ [],
+ );
+ });
+ });
+
+ describe('clearExpandedPanel', () => {
+ let state;
+
+ beforeEach(() => {
+ state = storeState();
+ });
+
+ it('Clears a panel as expanded', () => {
+ return testAction(
+ clearExpandedPanel,
+ undefined,
+ state,
+ [{ type: types.SET_EXPANDED_PANEL, payload: { group: null, panel: null } }],
+ [],
+ );
+ });
+ });
});
diff --git a/spec/frontend/monitoring/store/getters_spec.js b/spec/frontend/monitoring/store/getters_spec.js
index f040876b832..365052e68e3 100644
--- a/spec/frontend/monitoring/store/getters_spec.js
+++ b/spec/frontend/monitoring/store/getters_spec.js
@@ -3,7 +3,12 @@ import * as getters from '~/monitoring/stores/getters';
import mutations from '~/monitoring/stores/mutations';
import * as types from '~/monitoring/stores/mutation_types';
import { metricStates } from '~/monitoring/constants';
-import { environmentData, metricsResult } from '../mock_data';
+import {
+ environmentData,
+ metricsResult,
+ dashboardGitResponse,
+ mockTemplatingDataResponses,
+} from '../mock_data';
import {
metricsDashboardPayload,
metricResultStatus,
@@ -323,4 +328,81 @@ describe('Monitoring store Getters', () => {
expect(metricsSavedToDb).toEqual([`${id1}_${metric1.id}`, `${id2}_${metric2.id}`]);
});
});
+
+ describe('getCustomVariablesArray', () => {
+ let state;
+
+ beforeEach(() => {
+ state = {
+ promVariables: {},
+ };
+ });
+
+ it('transforms the promVariables object to an array in the [variable, variable_value] format for all variable types', () => {
+ mutations[types.SET_VARIABLES](state, mockTemplatingDataResponses.allVariableTypes);
+ const variablesArray = getters.getCustomVariablesArray(state);
+
+ expect(variablesArray).toEqual([
+ 'simpleText',
+ 'Simple text',
+ 'advText',
+ 'default',
+ 'simpleCustom',
+ 'value1',
+ 'advCustomNormal',
+ 'value2',
+ ]);
+ });
+
+ it('transforms the promVariables object to an empty array when no keys are present', () => {
+ mutations[types.SET_VARIABLES](state, {});
+ const variablesArray = getters.getCustomVariablesArray(state);
+
+ expect(variablesArray).toEqual([]);
+ });
+ });
+
+ describe('selectedDashboard', () => {
+ const { selectedDashboard } = getters;
+
+ it('returns a dashboard', () => {
+ const state = {
+ allDashboards: dashboardGitResponse,
+ currentDashboard: dashboardGitResponse[0].path,
+ };
+ expect(selectedDashboard(state)).toEqual(dashboardGitResponse[0]);
+ });
+
+ it('returns a non-default dashboard', () => {
+ const state = {
+ allDashboards: dashboardGitResponse,
+ currentDashboard: dashboardGitResponse[1].path,
+ };
+ expect(selectedDashboard(state)).toEqual(dashboardGitResponse[1]);
+ });
+
+ it('returns a default dashboard when no dashboard is selected', () => {
+ const state = {
+ allDashboards: dashboardGitResponse,
+ currentDashboard: null,
+ };
+ expect(selectedDashboard(state)).toEqual(dashboardGitResponse[0]);
+ });
+
+ it('returns a default dashboard when dashboard cannot be found', () => {
+ const state = {
+ allDashboards: dashboardGitResponse,
+ currentDashboard: 'wrong_path',
+ };
+ expect(selectedDashboard(state)).toEqual(dashboardGitResponse[0]);
+ });
+
+ it('returns null when no dashboards are present', () => {
+ const state = {
+ allDashboards: [],
+ currentDashboard: dashboardGitResponse[0].path,
+ };
+ expect(selectedDashboard(state)).toEqual(null);
+ });
+ });
});
diff --git a/spec/frontend/monitoring/store/mutations_spec.js b/spec/frontend/monitoring/store/mutations_spec.js
index 1452e9bc491..4306243689a 100644
--- a/spec/frontend/monitoring/store/mutations_spec.js
+++ b/spec/frontend/monitoring/store/mutations_spec.js
@@ -72,6 +72,49 @@ describe('Monitoring mutations', () => {
});
});
+ describe('Dashboard starring mutations', () => {
+ it('REQUEST_DASHBOARD_STARRING', () => {
+ stateCopy = { isUpdatingStarredValue: false };
+ mutations[types.REQUEST_DASHBOARD_STARRING](stateCopy);
+
+ expect(stateCopy.isUpdatingStarredValue).toBe(true);
+ });
+
+ describe('RECEIVE_DASHBOARD_STARRING_SUCCESS', () => {
+ let allDashboards;
+
+ beforeEach(() => {
+ allDashboards = [...dashboardGitResponse];
+ stateCopy = {
+ allDashboards,
+ currentDashboard: allDashboards[1].path,
+ isUpdatingStarredValue: true,
+ };
+ });
+
+ it('sets a dashboard as starred', () => {
+ mutations[types.RECEIVE_DASHBOARD_STARRING_SUCCESS](stateCopy, true);
+
+ expect(stateCopy.isUpdatingStarredValue).toBe(false);
+ expect(stateCopy.allDashboards[1].starred).toBe(true);
+ });
+
+ it('sets a dashboard as unstarred', () => {
+ mutations[types.RECEIVE_DASHBOARD_STARRING_SUCCESS](stateCopy, false);
+
+ expect(stateCopy.isUpdatingStarredValue).toBe(false);
+ expect(stateCopy.allDashboards[1].starred).toBe(false);
+ });
+ });
+
+ it('RECEIVE_DASHBOARD_STARRING_FAILURE', () => {
+ stateCopy = { isUpdatingStarredValue: true };
+ mutations[types.RECEIVE_DASHBOARD_STARRING_FAILURE](stateCopy);
+
+ expect(stateCopy.isUpdatingStarredValue).toBe(false);
+ });
+ });
+
describe('RECEIVE_DEPLOYMENTS_DATA_SUCCESS', () => {
it('stores the deployment data', () => {
stateCopy.deploymentData = [];
@@ -342,4 +385,53 @@ describe('Monitoring mutations', () => {
expect(stateCopy.allDashboards).toEqual(dashboardGitResponse);
});
});
+
+ describe('SET_EXPANDED_PANEL', () => {
+ it('no expanded panel is set initally', () => {
+ expect(stateCopy.expandedPanel.panel).toEqual(null);
+ expect(stateCopy.expandedPanel.group).toEqual(null);
+ });
+
+ it('sets a panel id as the expanded panel', () => {
+ const group = 'group_1';
+ const panel = { title: 'A Panel' };
+ mutations[types.SET_EXPANDED_PANEL](stateCopy, { group, panel });
+
+ expect(stateCopy.expandedPanel).toEqual({ group, panel });
+ });
+
+ it('clears panel as the expanded panel', () => {
+ mutations[types.SET_EXPANDED_PANEL](stateCopy, { group: null, panel: null });
+
+ expect(stateCopy.expandedPanel.group).toEqual(null);
+ expect(stateCopy.expandedPanel.panel).toEqual(null);
+ });
+ });
+
+ describe('SET_VARIABLES', () => {
+ it('stores an empty variables array when no custom variables are given', () => {
+ mutations[types.SET_VARIABLES](stateCopy, {});
+
+ expect(stateCopy.promVariables).toEqual({});
+ });
+
+ it('stores variables in the key key_value format in the array', () => {
+ mutations[types.SET_VARIABLES](stateCopy, { pod: 'POD', stage: 'main ops' });
+
+ expect(stateCopy.promVariables).toEqual({ pod: 'POD', stage: 'main ops' });
+ });
+ });
+
+ describe('UPDATE_VARIABLE_VALUES', () => {
+ afterEach(() => {
+ mutations[types.SET_VARIABLES](stateCopy, {});
+ });
+
+ it('updates only the value of the variable in promVariables', () => {
+ mutations[types.SET_VARIABLES](stateCopy, { environment: { value: 'prod', type: 'text' } });
+ mutations[types.UPDATE_VARIABLE_VALUES](stateCopy, { key: 'environment', value: 'new prod' });
+
+ expect(stateCopy.promVariables).toEqual({ environment: { value: 'new prod', type: 'text' } });
+ });
+ });
});
diff --git a/spec/frontend/monitoring/store/utils_spec.js b/spec/frontend/monitoring/store/utils_spec.js
index 7ee2a16b4bd..fe5754e1216 100644
--- a/spec/frontend/monitoring/store/utils_spec.js
+++ b/spec/frontend/monitoring/store/utils_spec.js
@@ -27,6 +27,7 @@ describe('mapToDashboardViewModel', () => {
group: 'Group 1',
panels: [
{
+ id: 'ID_ABC',
title: 'Title A',
xLabel: '',
xAxis: {
@@ -49,6 +50,7 @@ describe('mapToDashboardViewModel', () => {
key: 'group-1-0',
panels: [
{
+ id: 'ID_ABC',
title: 'Title A',
type: 'chart-type',
xLabel: '',
@@ -127,11 +129,13 @@ describe('mapToDashboardViewModel', () => {
it('panel with x_label', () => {
setupWithPanel({
+ id: 'ID_123',
title: panelTitle,
x_label: 'x label',
});
expect(getMappedPanel()).toEqual({
+ id: 'ID_123',
title: panelTitle,
xLabel: 'x label',
xAxis: {
@@ -149,10 +153,12 @@ describe('mapToDashboardViewModel', () => {
it('group y_axis defaults', () => {
setupWithPanel({
+ id: 'ID_456',
title: panelTitle,
});
expect(getMappedPanel()).toEqual({
+ id: 'ID_456',
title: panelTitle,
xLabel: '',
y_label: '',
diff --git a/spec/frontend/monitoring/store/variable_mapping_spec.js b/spec/frontend/monitoring/store/variable_mapping_spec.js
new file mode 100644
index 00000000000..47681ac7c65
--- /dev/null
+++ b/spec/frontend/monitoring/store/variable_mapping_spec.js
@@ -0,0 +1,22 @@
+import { parseTemplatingVariables } from '~/monitoring/stores/variable_mapping';
+import { mockTemplatingData, mockTemplatingDataResponses } from '../mock_data';
+
+describe('parseTemplatingVariables', () => {
+ it.each`
+ case | input | expected
+ ${'Returns empty object for no dashboard input'} | ${{}} | ${{}}
+ ${'Returns empty object for empty dashboard input'} | ${{ dashboard: {} }} | ${{}}
+ ${'Returns empty object for empty templating prop'} | ${mockTemplatingData.emptyTemplatingProp} | ${{}}
+ ${'Returns empty object for empty variables prop'} | ${mockTemplatingData.emptyVariablesProp} | ${{}}
+ ${'Returns parsed object for simple text variable'} | ${mockTemplatingData.simpleText} | ${mockTemplatingDataResponses.simpleText}
+ ${'Returns parsed object for advanced text variable'} | ${mockTemplatingData.advText} | ${mockTemplatingDataResponses.advText}
+ ${'Returns parsed object for simple custom variable'} | ${mockTemplatingData.simpleCustom} | ${mockTemplatingDataResponses.simpleCustom}
+ ${'Returns parsed object for advanced custom variable without options'} | ${mockTemplatingData.advCustomWithoutOpts} | ${mockTemplatingDataResponses.advCustomWithoutOpts}
+ ${'Returns parsed object for advanced custom variable without type'} | ${mockTemplatingData.advCustomWithoutType} | ${{}}
+ ${'Returns parsed object for advanced custom variable without label'} | ${mockTemplatingData.advCustomWithoutLabel} | ${mockTemplatingDataResponses.advCustomWithoutLabel}
+ ${'Returns parsed object for simple and advanced custom variables'} | ${mockTemplatingData.simpleAndAdv} | ${mockTemplatingDataResponses.simpleAndAdv}
+ ${'Returns parsed object for all variable types'} | ${mockTemplatingData.allVariableTypes} | ${mockTemplatingDataResponses.allVariableTypes}
+ `('$case', ({ input, expected }) => {
+ expect(parseTemplatingVariables(input?.dashboard?.templating)).toEqual(expected);
+ });
+});
diff --git a/spec/frontend/monitoring/store_utils.js b/spec/frontend/monitoring/store_utils.js
index d764a79ccc3..338af79dbbe 100644
--- a/spec/frontend/monitoring/store_utils.js
+++ b/spec/frontend/monitoring/store_utils.js
@@ -1,34 +1,49 @@
import * as types from '~/monitoring/stores/mutation_types';
-import { metricsResult, environmentData } from './mock_data';
+import { metricsResult, environmentData, dashboardGitResponse } from './mock_data';
import { metricsDashboardPayload } from './fixture_data';
-export const setMetricResult = ({ $store, result, group = 0, panel = 0, metric = 0 }) => {
- const { dashboard } = $store.state.monitoringDashboard;
+export const setMetricResult = ({ store, result, group = 0, panel = 0, metric = 0 }) => {
+ const { dashboard } = store.state.monitoringDashboard;
const { metricId } = dashboard.panelGroups[group].panels[panel].metrics[metric];
- $store.commit(`monitoringDashboard/${types.RECEIVE_METRIC_RESULT_SUCCESS}`, {
+ store.commit(`monitoringDashboard/${types.RECEIVE_METRIC_RESULT_SUCCESS}`, {
metricId,
result,
});
};
-const setEnvironmentData = $store => {
- $store.commit(`monitoringDashboard/${types.RECEIVE_ENVIRONMENTS_DATA_SUCCESS}`, environmentData);
+const setEnvironmentData = store => {
+ store.commit(`monitoringDashboard/${types.RECEIVE_ENVIRONMENTS_DATA_SUCCESS}`, environmentData);
};
-export const setupStoreWithDashboard = $store => {
- $store.commit(
+export const setupAllDashboards = store => {
+ store.commit(`monitoringDashboard/${types.SET_ALL_DASHBOARDS}`, dashboardGitResponse);
+};
+
+export const setupStoreWithDashboard = store => {
+ store.commit(
+ `monitoringDashboard/${types.RECEIVE_METRICS_DASHBOARD_SUCCESS}`,
+ metricsDashboardPayload,
+ );
+ store.commit(
`monitoringDashboard/${types.RECEIVE_METRICS_DASHBOARD_SUCCESS}`,
metricsDashboardPayload,
);
};
-export const setupStoreWithData = $store => {
- setupStoreWithDashboard($store);
+export const setupStoreWithVariable = store => {
+ store.commit(`monitoringDashboard/${types.SET_VARIABLES}`, {
+ label1: 'pod',
+ });
+};
+
+export const setupStoreWithData = store => {
+ setupAllDashboards(store);
+ setupStoreWithDashboard(store);
- setMetricResult({ $store, result: [], panel: 0 });
- setMetricResult({ $store, result: metricsResult, panel: 1 });
- setMetricResult({ $store, result: metricsResult, panel: 2 });
+ setMetricResult({ store, result: [], panel: 0 });
+ setMetricResult({ store, result: metricsResult, panel: 1 });
+ setMetricResult({ store, result: metricsResult, panel: 2 });
- setEnvironmentData($store);
+ setEnvironmentData(store);
};
diff --git a/spec/frontend/monitoring/stubs/modal_stub.js b/spec/frontend/monitoring/stubs/modal_stub.js
new file mode 100644
index 00000000000..4cd0362096e
--- /dev/null
+++ b/spec/frontend/monitoring/stubs/modal_stub.js
@@ -0,0 +1,11 @@
+const ModalStub = {
+ name: 'glmodal-stub',
+ template: `
+ <div>
+ <slot></slot>
+ <slot name="modal-ok"></slot>
+ </div>
+ `,
+};
+
+export default ModalStub;
diff --git a/spec/frontend/monitoring/utils_spec.js b/spec/frontend/monitoring/utils_spec.js
index 0bb1b987b2e..aa5a4459a72 100644
--- a/spec/frontend/monitoring/utils_spec.js
+++ b/spec/frontend/monitoring/utils_spec.js
@@ -1,15 +1,13 @@
import * as monitoringUtils from '~/monitoring/utils';
-import { queryToObject, mergeUrlParams, removeParams } from '~/lib/utils/url_utility';
+import * as urlUtils from '~/lib/utils/url_utility';
import { TEST_HOST } from 'jest/helpers/test_constants';
import {
mockProjectDir,
- graphDataPrometheusQuery,
+ singleStatMetricsResult,
anomalyMockGraphData,
barMockData,
} from './mock_data';
-import { graphData } from './fixture_data';
-
-jest.mock('~/lib/utils/url_utility');
+import { metricsDashboardViewModel, graphData } from './fixture_data';
const mockPath = `${TEST_HOST}${mockProjectDir}/-/environments/29/metrics`;
@@ -27,11 +25,6 @@ const rollingRange = {
};
describe('monitoring/utils', () => {
- afterEach(() => {
- mergeUrlParams.mockReset();
- queryToObject.mockReset();
- });
-
describe('trackGenerateLinkToChartEventOptions', () => {
it('should return Cluster Monitoring options if located on Cluster Health Dashboard', () => {
document.body.dataset.page = 'groups:clusters:show';
@@ -89,7 +82,7 @@ describe('monitoring/utils', () => {
it('validates data with the query format', () => {
const validGraphData = monitoringUtils.graphDataValidatorForValues(
true,
- graphDataPrometheusQuery,
+ singleStatMetricsResult,
);
expect(validGraphData).toBe(true);
@@ -112,7 +105,7 @@ describe('monitoring/utils', () => {
let threeMetrics;
let fourMetrics;
beforeEach(() => {
- oneMetric = graphDataPrometheusQuery;
+ oneMetric = singleStatMetricsResult;
threeMetrics = anomalyMockGraphData;
const metrics = [...threeMetrics.metrics];
@@ -139,18 +132,25 @@ describe('monitoring/utils', () => {
});
describe('timeRangeFromUrl', () => {
- const { timeRangeFromUrl } = monitoringUtils;
+ beforeEach(() => {
+ jest.spyOn(urlUtils, 'queryToObject');
+ });
+
+ afterEach(() => {
+ urlUtils.queryToObject.mockRestore();
+ });
- it('returns a fixed range when query contains `start` and `end` paramters are given', () => {
- queryToObject.mockReturnValueOnce(range);
+ const { timeRangeFromUrl } = monitoringUtils;
+ it('returns a fixed range when query contains `start` and `end` parameters are given', () => {
+ urlUtils.queryToObject.mockReturnValueOnce(range);
expect(timeRangeFromUrl()).toEqual(range);
});
- it('returns a rolling range when query contains `duration_seconds` paramters are given', () => {
+ it('returns a rolling range when query contains `duration_seconds` parameters are given', () => {
const { seconds } = rollingRange.duration;
- queryToObject.mockReturnValueOnce({
+ urlUtils.queryToObject.mockReturnValueOnce({
dashboard: '.gitlab/dashboard/my_dashboard.yml',
duration_seconds: `${seconds}`,
});
@@ -158,23 +158,59 @@ describe('monitoring/utils', () => {
expect(timeRangeFromUrl()).toEqual(rollingRange);
});
- it('returns null when no time range paramters are given', () => {
- const params = {
+ it('returns null when no time range parameters are given', () => {
+ urlUtils.queryToObject.mockReturnValueOnce({
dashboard: '.gitlab/dashboards/custom_dashboard.yml',
param1: 'value1',
param2: 'value2',
- };
+ });
- expect(timeRangeFromUrl(params, mockPath)).toBe(null);
+ expect(timeRangeFromUrl()).toBe(null);
+ });
+ });
+
+ describe('getPromCustomVariablesFromUrl', () => {
+ const { getPromCustomVariablesFromUrl } = monitoringUtils;
+
+ beforeEach(() => {
+ jest.spyOn(urlUtils, 'queryToObject');
+ });
+
+ afterEach(() => {
+ urlUtils.queryToObject.mockRestore();
+ });
+
+ it('returns an object with only the custom variables', () => {
+ urlUtils.queryToObject.mockReturnValueOnce({
+ dashboard: '.gitlab/dashboards/custom_dashboard.yml',
+ y_label: 'memory usage',
+ group: 'kubernetes',
+ title: 'Kubernetes memory total',
+ start: '2020-05-06',
+ end: '2020-05-07',
+ duration_seconds: '86400',
+ direction: 'left',
+ anchor: 'top',
+ pod: 'POD',
+ 'var-pod': 'POD',
+ });
+
+ expect(getPromCustomVariablesFromUrl()).toEqual(expect.objectContaining({ pod: 'POD' }));
+ });
+
+ it('returns an empty object when no custom variables are present', () => {
+ urlUtils.queryToObject.mockReturnValueOnce({
+ dashboard: '.gitlab/dashboards/custom_dashboard.yml',
+ });
+
+ expect(getPromCustomVariablesFromUrl()).toStrictEqual({});
});
});
describe('removeTimeRangeParams', () => {
const { removeTimeRangeParams } = monitoringUtils;
- it('returns when query contains `start` and `end` paramters are given', () => {
- removeParams.mockReturnValueOnce(mockPath);
-
+ it('returns when query contains `start` and `end` parameters are given', () => {
expect(removeTimeRangeParams(`${mockPath}?start=${range.start}&end=${range.end}`)).toEqual(
mockPath,
);
@@ -184,28 +220,126 @@ describe('monitoring/utils', () => {
describe('timeRangeToUrl', () => {
const { timeRangeToUrl } = monitoringUtils;
- it('returns a fixed range when query contains `start` and `end` paramters are given', () => {
+ beforeEach(() => {
+ jest.spyOn(urlUtils, 'mergeUrlParams');
+ jest.spyOn(urlUtils, 'removeParams');
+ });
+
+ afterEach(() => {
+ urlUtils.mergeUrlParams.mockRestore();
+ urlUtils.removeParams.mockRestore();
+ });
+
+ it('returns a fixed range when query contains `start` and `end` parameters are given', () => {
const toUrl = `${mockPath}?start=${range.start}&end=${range.end}`;
const fromUrl = mockPath;
- removeParams.mockReturnValueOnce(fromUrl);
- mergeUrlParams.mockReturnValueOnce(toUrl);
+ urlUtils.removeParams.mockReturnValueOnce(fromUrl);
+ urlUtils.mergeUrlParams.mockReturnValueOnce(toUrl);
expect(timeRangeToUrl(range)).toEqual(toUrl);
- expect(mergeUrlParams).toHaveBeenCalledWith(range, fromUrl);
+ expect(urlUtils.mergeUrlParams).toHaveBeenCalledWith(range, fromUrl);
});
- it('returns a rolling range when query contains `duration_seconds` paramters are given', () => {
+ it('returns a rolling range when query contains `duration_seconds` parameters are given', () => {
const { seconds } = rollingRange.duration;
const toUrl = `${mockPath}?duration_seconds=${seconds}`;
const fromUrl = mockPath;
- removeParams.mockReturnValueOnce(fromUrl);
- mergeUrlParams.mockReturnValueOnce(toUrl);
+ urlUtils.removeParams.mockReturnValueOnce(fromUrl);
+ urlUtils.mergeUrlParams.mockReturnValueOnce(toUrl);
expect(timeRangeToUrl(rollingRange)).toEqual(toUrl);
- expect(mergeUrlParams).toHaveBeenCalledWith({ duration_seconds: `${seconds}` }, fromUrl);
+ expect(urlUtils.mergeUrlParams).toHaveBeenCalledWith(
+ { duration_seconds: `${seconds}` },
+ fromUrl,
+ );
+ });
+ });
+
+ describe('expandedPanelPayloadFromUrl', () => {
+ const { expandedPanelPayloadFromUrl } = monitoringUtils;
+ const [panelGroup] = metricsDashboardViewModel.panelGroups;
+ const [panel] = panelGroup.panels;
+
+ const { group } = panelGroup;
+ const { title, y_label: yLabel } = panel;
+
+ it('returns payload for a panel when query parameters are given', () => {
+ const search = `?group=${group}&title=${title}&y_label=${yLabel}`;
+
+ expect(expandedPanelPayloadFromUrl(metricsDashboardViewModel, search)).toEqual({
+ group: panelGroup.group,
+ panel,
+ });
+ });
+
+ it('returns null when no parameters are given', () => {
+ expect(expandedPanelPayloadFromUrl(metricsDashboardViewModel, '')).toBe(null);
+ });
+
+ it('throws an error when no group is provided', () => {
+ const search = `?title=${panel.title}&y_label=${yLabel}`;
+ expect(() => expandedPanelPayloadFromUrl(metricsDashboardViewModel, search)).toThrow();
+ });
+
+ it('throws an error when no title is provided', () => {
+ const search = `?title=${title}&y_label=${yLabel}`;
+ expect(() => expandedPanelPayloadFromUrl(metricsDashboardViewModel, search)).toThrow();
+ });
+
+ it('throws an error when no y_label group is provided', () => {
+ const search = `?group=${group}&title=${title}`;
+ expect(() => expandedPanelPayloadFromUrl(metricsDashboardViewModel, search)).toThrow();
+ });
+
+ test.each`
+ group | title | yLabel | missingField
+ ${'NOT_A_GROUP'} | ${title} | ${yLabel} | ${'group'}
+ ${group} | ${'NOT_A_TITLE'} | ${yLabel} | ${'title'}
+ ${group} | ${title} | ${'NOT_A_Y_LABEL'} | ${'y_label'}
+ `('throws an error when $missingField is incorrect', params => {
+ const search = `?group=${params.group}&title=${params.title}&y_label=${params.yLabel}`;
+ expect(() => expandedPanelPayloadFromUrl(metricsDashboardViewModel, search)).toThrow();
+ });
+ });
+
+ describe('panelToUrl', () => {
+ const { panelToUrl } = monitoringUtils;
+
+ const dashboard = 'metrics.yml';
+ const [panelGroup] = metricsDashboardViewModel.panelGroups;
+ const [panel] = panelGroup.panels;
+
+ const getUrlParams = url => urlUtils.queryToObject(url.split('?')[1]);
+
+ it('returns URL for a panel when query parameters are given', () => {
+ const params = getUrlParams(panelToUrl(dashboard, {}, panelGroup.group, panel));
+
+ expect(params).toEqual(
+ expect.objectContaining({
+ dashboard,
+ group: panelGroup.group,
+ title: panel.title,
+ y_label: panel.y_label,
+ }),
+ );
+ });
+
+ it('returns a dashboard only URL if group is missing', () => {
+ const params = getUrlParams(panelToUrl(dashboard, {}, null, panel));
+ expect(params).toEqual(expect.objectContaining({ dashboard: 'metrics.yml' }));
+ });
+
+ it('returns a dashboard only URL if panel is missing', () => {
+ const params = getUrlParams(panelToUrl(dashboard, {}, panelGroup.group, null));
+ expect(params).toEqual(expect.objectContaining({ dashboard: 'metrics.yml' }));
+ });
+
+ it('returns URL for a panel when query paramters are given including custom variables', () => {
+ const params = getUrlParams(panelToUrl(dashboard, { pod: 'pod' }, panelGroup.group, null));
+ expect(params).toEqual(expect.objectContaining({ dashboard: 'metrics.yml', pod: 'pod' }));
});
});
@@ -271,4 +405,108 @@ describe('monitoring/utils', () => {
});
});
});
+
+ describe('removePrefixFromLabel', () => {
+ it.each`
+ input | expected
+ ${undefined} | ${''}
+ ${null} | ${''}
+ ${''} | ${''}
+ ${' '} | ${' '}
+ ${'pod-1'} | ${'pod-1'}
+ ${'pod-var-1'} | ${'pod-var-1'}
+ ${'pod-1-var'} | ${'pod-1-var'}
+ ${'podvar--1'} | ${'podvar--1'}
+ ${'povar-d-1'} | ${'povar-d-1'}
+ ${'var-pod-1'} | ${'pod-1'}
+ ${'var-var-pod-1'} | ${'var-pod-1'}
+ ${'varvar-pod-1'} | ${'varvar-pod-1'}
+ ${'var-pod-1-var-'} | ${'pod-1-var-'}
+ `('removePrefixFromLabel returns $expected with input $input', ({ input, expected }) => {
+ expect(monitoringUtils.removePrefixFromLabel(input)).toEqual(expected);
+ });
+ });
+
+ describe('mergeURLVariables', () => {
+ beforeEach(() => {
+ jest.spyOn(urlUtils, 'queryToObject');
+ });
+
+ afterEach(() => {
+ urlUtils.queryToObject.mockRestore();
+ });
+
+ it('returns empty object if variables are not defined in yml or URL', () => {
+ urlUtils.queryToObject.mockReturnValueOnce({});
+
+ expect(monitoringUtils.mergeURLVariables({})).toEqual({});
+ });
+
+ it('returns empty object if variables are defined in URL but not in yml', () => {
+ urlUtils.queryToObject.mockReturnValueOnce({
+ 'var-env': 'one',
+ 'var-instance': 'localhost',
+ });
+
+ expect(monitoringUtils.mergeURLVariables({})).toEqual({});
+ });
+
+ it('returns yml variables if variables defined in yml but not in the URL', () => {
+ urlUtils.queryToObject.mockReturnValueOnce({});
+
+ const params = {
+ env: 'one',
+ instance: 'localhost',
+ };
+
+ expect(monitoringUtils.mergeURLVariables(params)).toEqual(params);
+ });
+
+ it('returns yml variables if variables defined in URL do not match with yml variables', () => {
+ const urlParams = {
+ 'var-env': 'one',
+ 'var-instance': 'localhost',
+ };
+ const ymlParams = {
+ pod: { value: 'one' },
+ service: { value: 'database' },
+ };
+ urlUtils.queryToObject.mockReturnValueOnce(urlParams);
+
+ expect(monitoringUtils.mergeURLVariables(ymlParams)).toEqual(ymlParams);
+ });
+
+ it('returns merged yml and URL variables if there is some match', () => {
+ const urlParams = {
+ 'var-env': 'one',
+ 'var-instance': 'localhost:8080',
+ };
+ const ymlParams = {
+ instance: { value: 'localhost' },
+ service: { value: 'database' },
+ };
+
+ const merged = {
+ instance: { value: 'localhost:8080' },
+ service: { value: 'database' },
+ };
+
+ urlUtils.queryToObject.mockReturnValueOnce(urlParams);
+
+ expect(monitoringUtils.mergeURLVariables(ymlParams)).toEqual(merged);
+ });
+ });
+
+ describe('convertVariablesForURL', () => {
+ it.each`
+ input | expected
+ ${undefined} | ${{}}
+ ${null} | ${{}}
+ ${{}} | ${{}}
+ ${{ env: { value: 'prod' } }} | ${{ 'var-env': 'prod' }}
+ ${{ 'var-env': { value: 'prod' } }} | ${{ 'var-var-env': 'prod' }}
+ `('convertVariablesForURL returns $expected with input $input', ({ input, expected }) => {
+ expect(monitoringUtils.convertVariablesForURL(input)).toEqual(expected);
+ });
+ });
});
diff --git a/spec/frontend/monitoring/validators_spec.js b/spec/frontend/monitoring/validators_spec.js
new file mode 100644
index 00000000000..0c3d77a7d98
--- /dev/null
+++ b/spec/frontend/monitoring/validators_spec.js
@@ -0,0 +1,80 @@
+import { alertsValidator, queriesValidator } from '~/monitoring/validators';
+
+describe('alertsValidator', () => {
+ const validAlert = {
+ alert_path: 'my/alert.json',
+ operator: '<',
+ threshold: 5,
+ metricId: '8',
+ };
+ it('requires all alerts to have an alert path', () => {
+ const { operator, threshold, metricId } = validAlert;
+ const input = {
+ [validAlert.alert_path]: {
+ operator,
+ threshold,
+ metricId,
+ },
+ };
+ expect(alertsValidator(input)).toEqual(false);
+ });
+ it('requires that the object key matches the alert path', () => {
+ const input = {
+ undefined: validAlert,
+ };
+ expect(alertsValidator(input)).toEqual(false);
+ });
+ it('requires all alerts to have a metric id', () => {
+ const input = {
+ [validAlert.alert_path]: { ...validAlert, metricId: undefined },
+ };
+ expect(alertsValidator(input)).toEqual(false);
+ });
+ it('requires the metricId to be a string', () => {
+ const input = {
+ [validAlert.alert_path]: { ...validAlert, metricId: 8 },
+ };
+ expect(alertsValidator(input)).toEqual(false);
+ });
+ it('requires all alerts to have an operator', () => {
+ const input = {
+ [validAlert.alert_path]: { ...validAlert, operator: '' },
+ };
+ expect(alertsValidator(input)).toEqual(false);
+ });
+ it('requires all alerts to have an numeric threshold', () => {
+ const input = {
+ [validAlert.alert_path]: { ...validAlert, threshold: '60' },
+ };
+ expect(alertsValidator(input)).toEqual(false);
+ });
+ it('correctly identifies a valid alerts object', () => {
+ const input = {
+ [validAlert.alert_path]: validAlert,
+ };
+ expect(alertsValidator(input)).toEqual(true);
+ });
+});
+describe('queriesValidator', () => {
+ const validQuery = {
+ metricId: '8',
+ alert_path: 'alert',
+ label: 'alert-label',
+ };
+ it('requires all alerts to have a metric id', () => {
+ const input = [{ ...validQuery, metricId: undefined }];
+ expect(queriesValidator(input)).toEqual(false);
+ });
+ it('requires the metricId to be a string', () => {
+ const input = [{ ...validQuery, metricId: 8 }];
+ expect(queriesValidator(input)).toEqual(false);
+ });
+ it('requires all queries to have a label', () => {
+ const input = [{ ...validQuery, label: undefined }];
+ expect(queriesValidator(input)).toEqual(false);
+ });
+ it('correctly identifies a valid queries array', () => {
+ const input = [validQuery];
+ expect(queriesValidator(input)).toEqual(true);
+ });
+});
diff --git a/spec/frontend/notebook/cells/code_spec.js b/spec/frontend/notebook/cells/code_spec.js
new file mode 100644
index 00000000000..33dabe2b6dc
--- /dev/null
+++ b/spec/frontend/notebook/cells/code_spec.js
@@ -0,0 +1,90 @@
+import Vue from 'vue';
+import CodeComponent from '~/notebook/cells/code.vue';
+
+const Component = Vue.extend(CodeComponent);
+
+describe('Code component', () => {
+ let vm;
+ let json;
+
+ beforeEach(() => {
+ json = getJSONFixture('blob/notebook/basic.json');
+ });
+
+ const setupComponent = cell => {
+ const comp = new Component({
+ propsData: {
+ cell,
+ },
+ });
+ comp.$mount();
+ return comp;
+ };
+
+ describe('without output', () => {
+ beforeEach(done => {
+ vm = setupComponent(json.cells[0]);
+
+ setImmediate(() => {
+ done();
+ });
+ });
+
+ it('does not render output prompt', () => {
+ expect(vm.$el.querySelectorAll('.prompt').length).toBe(1);
+ });
+ });
+
+ describe('with output', () => {
+ beforeEach(done => {
+ vm = setupComponent(json.cells[2]);
+
+ setImmediate(() => {
+ done();
+ });
+ });
+
+ it('does not render output prompt', () => {
+ expect(vm.$el.querySelectorAll('.prompt').length).toBe(2);
+ });
+
+ it('renders output cell', () => {
+ expect(vm.$el.querySelector('.output')).toBeDefined();
+ });
+ });
+
+ describe('with string for output', () => {
+ // NBFormat Version 4.1 allows outputs.text to be a string
+ beforeEach(() => {
+ const cell = json.cells[2];
+ cell.outputs[0].text = cell.outputs[0].text.join('');
+
+ vm = setupComponent(cell);
+ return vm.$nextTick();
+ });
+
+ it('does not render output prompt', () => {
+ expect(vm.$el.querySelectorAll('.prompt').length).toBe(2);
+ });
+
+ it('renders output cell', () => {
+ expect(vm.$el.querySelector('.output')).toBeDefined();
+ });
+ });
+
+ describe('with string for cell.source', () => {
+ beforeEach(() => {
+ const cell = json.cells[0];
+ cell.source = cell.source.join('');
+
+ vm = setupComponent(cell);
+ return vm.$nextTick();
+ });
+
+ it('renders the same input as when cell.source is an array', () => {
+ const expected = "console.log('test')";
+
+ expect(vm.$el.querySelector('.input').innerText).toContain(expected);
+ });
+ });
+});
diff --git a/spec/frontend/notebook/cells/markdown_spec.js b/spec/frontend/notebook/cells/markdown_spec.js
new file mode 100644
index 00000000000..ad33858da22
--- /dev/null
+++ b/spec/frontend/notebook/cells/markdown_spec.js
@@ -0,0 +1,167 @@
+import Vue from 'vue';
+import katex from 'katex';
+import MarkdownComponent from '~/notebook/cells/markdown.vue';
+
+const Component = Vue.extend(MarkdownComponent);
+
+window.katex = katex;
+
+describe('Markdown component', () => {
+ let vm;
+ let cell;
+ let json;
+
+ beforeEach(() => {
+ json = getJSONFixture('blob/notebook/basic.json');
+
+ // eslint-disable-next-line prefer-destructuring
+ cell = json.cells[1];
+
+ vm = new Component({
+ propsData: {
+ cell,
+ },
+ });
+ vm.$mount();
+
+ return vm.$nextTick();
+ });
+
+ it('does not render promot', () => {
+ expect(vm.$el.querySelector('.prompt span')).toBeNull();
+ });
+
+ it('does not render the markdown text', () => {
+ expect(vm.$el.querySelector('.markdown').innerHTML.trim()).not.toEqual(cell.source.join(''));
+ });
+
+ it('renders the markdown HTML', () => {
+ expect(vm.$el.querySelector('.markdown h1')).not.toBeNull();
+ });
+
+ it('sanitizes output', () => {
+ Object.assign(cell, {
+ source: [
+ '[XSS](data:text/html;base64,PHNjcmlwdD5hbGVydChkb2N1bWVudC5kb21haW4pPC9zY3JpcHQ+Cg==)\n',
+ ],
+ });
+
+ return vm.$nextTick().then(() => {
+ expect(vm.$el.querySelector('a').getAttribute('href')).toBeNull();
+ });
+ });
+
+ describe('katex', () => {
+ beforeEach(() => {
+ json = getJSONFixture('blob/notebook/math.json');
+ });
+
+ it('renders multi-line katex', () => {
+ vm = new Component({
+ propsData: {
+ cell: json.cells[0],
+ },
+ }).$mount();
+
+ return vm.$nextTick().then(() => {
+ expect(vm.$el.querySelector('.katex')).not.toBeNull();
+ });
+ });
+
+ it('renders inline katex', () => {
+ vm = new Component({
+ propsData: {
+ cell: json.cells[1],
+ },
+ }).$mount();
+
+ return vm.$nextTick().then(() => {
+ expect(vm.$el.querySelector('p:first-child .katex')).not.toBeNull();
+ });
+ });
+
+ it('renders multiple inline katex', () => {
+ vm = new Component({
+ propsData: {
+ cell: json.cells[1],
+ },
+ }).$mount();
+
+ return vm.$nextTick().then(() => {
+ expect(vm.$el.querySelectorAll('p:nth-child(2) .katex').length).toBe(4);
+ });
+ });
+
+ it('output cell in case of katex error', () => {
+ vm = new Component({
+ propsData: {
+ cell: {
+ cell_type: 'markdown',
+ metadata: {},
+ source: ['Some invalid $a & b$ inline formula $b & c$\n', '\n'],
+ },
+ },
+ }).$mount();
+
+ return vm.$nextTick().then(() => {
+ // expect one paragraph with no katex formula in it
+ expect(vm.$el.querySelectorAll('p').length).toBe(1);
+ expect(vm.$el.querySelectorAll('p .katex').length).toBe(0);
+ });
+ });
+
+ it('output cell and render remaining formula in case of katex error', () => {
+ vm = new Component({
+ propsData: {
+ cell: {
+ cell_type: 'markdown',
+ metadata: {},
+ source: ['An invalid $a & b$ inline formula and a vaild one $b = c$\n', '\n'],
+ },
+ },
+ }).$mount();
+
+ return vm.$nextTick().then(() => {
+ // expect one paragraph with no katex formula in it
+ expect(vm.$el.querySelectorAll('p').length).toBe(1);
+ expect(vm.$el.querySelectorAll('p .katex').length).toBe(1);
+ });
+ });
+
+ it('renders math formula in list object', () => {
+ vm = new Component({
+ propsData: {
+ cell: {
+ cell_type: 'markdown',
+ metadata: {},
+ source: ["- list with inline $a=2$ inline formula $a' + b = c$\n", '\n'],
+ },
+ },
+ }).$mount();
+
+ return vm.$nextTick().then(() => {
+ // expect one list with a katex formula in it
+ expect(vm.$el.querySelectorAll('li').length).toBe(1);
+ expect(vm.$el.querySelectorAll('li .katex').length).toBe(2);
+ });
+ });
+
+ it("renders math formula with tick ' in it", () => {
+ vm = new Component({
+ propsData: {
+ cell: {
+ cell_type: 'markdown',
+ metadata: {},
+ source: ["- list with inline $a=2$ inline formula $a' + b = c$\n", '\n'],
+ },
+ },
+ }).$mount();
+
+ return vm.$nextTick().then(() => {
+ // expect one list with a katex formula in it
+ expect(vm.$el.querySelectorAll('li').length).toBe(1);
+ expect(vm.$el.querySelectorAll('li .katex').length).toBe(2);
+ });
+ });
+ });
+});
diff --git a/spec/javascripts/notebook/cells/output/html_sanitize_tests.js b/spec/frontend/notebook/cells/output/html_sanitize_tests.js
index 74c48f04367..74c48f04367 100644
--- a/spec/javascripts/notebook/cells/output/html_sanitize_tests.js
+++ b/spec/frontend/notebook/cells/output/html_sanitize_tests.js
diff --git a/spec/javascripts/notebook/cells/output/html_spec.js b/spec/frontend/notebook/cells/output/html_spec.js
index 3ee404fb187..3ee404fb187 100644
--- a/spec/javascripts/notebook/cells/output/html_spec.js
+++ b/spec/frontend/notebook/cells/output/html_spec.js
diff --git a/spec/frontend/notebook/cells/output/index_spec.js b/spec/frontend/notebook/cells/output/index_spec.js
new file mode 100644
index 00000000000..2b1aa5317c5
--- /dev/null
+++ b/spec/frontend/notebook/cells/output/index_spec.js
@@ -0,0 +1,115 @@
+import Vue from 'vue';
+import CodeComponent from '~/notebook/cells/output/index.vue';
+
+const Component = Vue.extend(CodeComponent);
+
+describe('Output component', () => {
+ let vm;
+ let json;
+
+ const createComponent = output => {
+ vm = new Component({
+ propsData: {
+ outputs: [].concat(output),
+ count: 1,
+ },
+ });
+ vm.$mount();
+ };
+
+ beforeEach(() => {
+ json = getJSONFixture('blob/notebook/basic.json');
+ });
+
+ describe('text output', () => {
+ beforeEach(done => {
+ createComponent(json.cells[2].outputs[0]);
+
+ setImmediate(() => {
+ done();
+ });
+ });
+
+ it('renders as plain text', () => {
+ expect(vm.$el.querySelector('pre')).not.toBeNull();
+ });
+
+ it('renders promot', () => {
+ expect(vm.$el.querySelector('.prompt span')).not.toBeNull();
+ });
+ });
+
+ describe('image output', () => {
+ beforeEach(done => {
+ createComponent(json.cells[3].outputs[0]);
+
+ setImmediate(() => {
+ done();
+ });
+ });
+
+ it('renders as an image', () => {
+ expect(vm.$el.querySelector('img')).not.toBeNull();
+ });
+ });
+
+ describe('html output', () => {
+ it('renders raw HTML', () => {
+ createComponent(json.cells[4].outputs[0]);
+
+ expect(vm.$el.querySelector('p')).not.toBeNull();
+ expect(vm.$el.querySelectorAll('p').length).toBe(1);
+ expect(vm.$el.textContent.trim()).toContain('test');
+ });
+
+ it('renders multiple raw HTML outputs', () => {
+ createComponent([json.cells[4].outputs[0], json.cells[4].outputs[0]]);
+
+ expect(vm.$el.querySelectorAll('p').length).toBe(2);
+ });
+ });
+
+ describe('svg output', () => {
+ beforeEach(done => {
+ createComponent(json.cells[5].outputs[0]);
+
+ setImmediate(() => {
+ done();
+ });
+ });
+
+ it('renders as an svg', () => {
+ expect(vm.$el.querySelector('svg')).not.toBeNull();
+ });
+ });
+
+ describe('default to plain text', () => {
+ beforeEach(done => {
+ createComponent(json.cells[6].outputs[0]);
+
+ setImmediate(() => {
+ done();
+ });
+ });
+
+ it('renders as plain text', () => {
+ expect(vm.$el.querySelector('pre')).not.toBeNull();
+ expect(vm.$el.textContent.trim()).toContain('testing');
+ });
+
+ it('renders promot', () => {
+ expect(vm.$el.querySelector('.prompt span')).not.toBeNull();
+ });
+
+ it("renders as plain text when doesn't recognise other types", done => {
+ createComponent(json.cells[7].outputs[0]);
+
+ setImmediate(() => {
+ expect(vm.$el.querySelector('pre')).not.toBeNull();
+ expect(vm.$el.textContent.trim()).toContain('testing');
+
+ done();
+ });
+ });
+ });
+});
diff --git a/spec/frontend/notebook/cells/prompt_spec.js b/spec/frontend/notebook/cells/prompt_spec.js
new file mode 100644
index 00000000000..cf5a7a603c6
--- /dev/null
+++ b/spec/frontend/notebook/cells/prompt_spec.js
@@ -0,0 +1,56 @@
+import Vue from 'vue';
+import PromptComponent from '~/notebook/cells/prompt.vue';
+
+const Component = Vue.extend(PromptComponent);
+
+describe('Prompt component', () => {
+ let vm;
+
+ describe('input', () => {
+ beforeEach(done => {
+ vm = new Component({
+ propsData: {
+ type: 'In',
+ count: 1,
+ },
+ });
+ vm.$mount();
+
+ setImmediate(() => {
+ done();
+ });
+ });
+
+ it('renders in label', () => {
+ expect(vm.$el.textContent.trim()).toContain('In');
+ });
+
+ it('renders count', () => {
+ expect(vm.$el.textContent.trim()).toContain('1');
+ });
+ });
+
+ describe('output', () => {
+ beforeEach(done => {
+ vm = new Component({
+ propsData: {
+ type: 'Out',
+ count: 1,
+ },
+ });
+ vm.$mount();
+
+ setImmediate(() => {
+ done();
+ });
+ });
+
+ it('renders in label', () => {
+ expect(vm.$el.textContent.trim()).toContain('Out');
+ });
+
+ it('renders count', () => {
+ expect(vm.$el.textContent.trim()).toContain('1');
+ });
+ });
+});
diff --git a/spec/frontend/notebook/index_spec.js b/spec/frontend/notebook/index_spec.js
new file mode 100644
index 00000000000..36b092be976
--- /dev/null
+++ b/spec/frontend/notebook/index_spec.js
@@ -0,0 +1,100 @@
+import Vue from 'vue';
+import Notebook from '~/notebook/index.vue';
+
+const Component = Vue.extend(Notebook);
+
+describe('Notebook component', () => {
+ let vm;
+ let json;
+ let jsonWithWorksheet;
+
+ beforeEach(() => {
+ json = getJSONFixture('blob/notebook/basic.json');
+ jsonWithWorksheet = getJSONFixture('blob/notebook/worksheets.json');
+ });
+
+ describe('without JSON', () => {
+ beforeEach(done => {
+ vm = new Component({
+ propsData: {
+ notebook: {},
+ },
+ });
+ vm.$mount();
+
+ setImmediate(() => {
+ done();
+ });
+ });
+
+ it('does not render', () => {
+ expect(vm.$el.tagName).toBeUndefined();
+ });
+ });
+
+ describe('with JSON', () => {
+ beforeEach(done => {
+ vm = new Component({
+ propsData: {
+ notebook: json,
+ codeCssClass: 'js-code-class',
+ },
+ });
+ vm.$mount();
+
+ setImmediate(() => {
+ done();
+ });
+ });
+
+ it('renders cells', () => {
+ expect(vm.$el.querySelectorAll('.cell').length).toBe(json.cells.length);
+ });
+
+ it('renders markdown cell', () => {
+ expect(vm.$el.querySelector('.markdown')).not.toBeNull();
+ });
+
+ it('renders code cell', () => {
+ expect(vm.$el.querySelector('pre')).not.toBeNull();
+ });
+
+ it('add code class to code blocks', () => {
+ expect(vm.$el.querySelector('.js-code-class')).not.toBeNull();
+ });
+ });
+
+ describe('with worksheets', () => {
+ beforeEach(done => {
+ vm = new Component({
+ propsData: {
+ notebook: jsonWithWorksheet,
+ codeCssClass: 'js-code-class',
+ },
+ });
+ vm.$mount();
+
+ setImmediate(() => {
+ done();
+ });
+ });
+
+ it('renders cells', () => {
+ expect(vm.$el.querySelectorAll('.cell').length).toBe(
+ jsonWithWorksheet.worksheets[0].cells.length,
+ );
+ });
+
+ it('renders markdown cell', () => {
+ expect(vm.$el.querySelector('.markdown')).not.toBeNull();
+ });
+
+ it('renders code cell', () => {
+ expect(vm.$el.querySelector('pre')).not.toBeNull();
+ });
+
+ it('add code class to code blocks', () => {
+ expect(vm.$el.querySelector('.js-code-class')).not.toBeNull();
+ });
+ });
+});
diff --git a/spec/frontend/notes/components/comment_form_spec.js b/spec/frontend/notes/components/comment_form_spec.js
index a2c7f0b3767..dc68c4371aa 100644
--- a/spec/frontend/notes/components/comment_form_spec.js
+++ b/spec/frontend/notes/components/comment_form_spec.js
@@ -9,12 +9,7 @@ import CommentForm from '~/notes/components/comment_form.vue';
import * as constants from '~/notes/constants';
import { refreshUserMergeRequestCounts } from '~/commons/nav/user_merge_requests';
import { keyboardDownEvent } from '../../issue_show/helpers';
-import {
- loggedOutnoteableData,
- notesDataMock,
- userDataMock,
- noteableDataMock,
-} from '../../notes/mock_data';
+import { loggedOutnoteableData, notesDataMock, userDataMock, noteableDataMock } from '../mock_data';
jest.mock('autosize');
jest.mock('~/commons/nav/user_merge_requests');
diff --git a/spec/frontend/notes/components/discussion_actions_spec.js b/spec/frontend/notes/components/discussion_actions_spec.js
index 5101b81e3ee..44dc148933c 100644
--- a/spec/frontend/notes/components/discussion_actions_spec.js
+++ b/spec/frontend/notes/components/discussion_actions_spec.js
@@ -1,5 +1,5 @@
import { shallowMount, mount } from '@vue/test-utils';
-import { discussionMock } from '../../notes/mock_data';
+import { discussionMock } from '../mock_data';
import DiscussionActions from '~/notes/components/discussion_actions.vue';
import ReplyPlaceholder from '~/notes/components/discussion_reply_placeholder.vue';
import ResolveDiscussionButton from '~/notes/components/discussion_resolve_button.vue';
diff --git a/spec/frontend/notes/components/discussion_counter_spec.js b/spec/frontend/notes/components/discussion_counter_spec.js
index 77603c16f82..04535aa17c5 100644
--- a/spec/frontend/notes/components/discussion_counter_spec.js
+++ b/spec/frontend/notes/components/discussion_counter_spec.js
@@ -75,15 +75,14 @@ describe('DiscussionCounter component', () => {
});
it.each`
- title | resolved | isActive | icon | groupLength
- ${'not allResolved'} | ${false} | ${false} | ${'check-circle'} | ${3}
- ${'allResolved'} | ${true} | ${true} | ${'check-circle-filled'} | ${1}
- `('renders correctly if $title', ({ resolved, isActive, icon, groupLength }) => {
+ title | resolved | isActive | groupLength
+ ${'not allResolved'} | ${false} | ${false} | ${3}
+ ${'allResolved'} | ${true} | ${true} | ${1}
+ `('renders correctly if $title', ({ resolved, isActive, groupLength }) => {
updateStore({ resolvable: true, resolved });
wrapper = shallowMount(DiscussionCounter, { store, localVue });
expect(wrapper.find(`.is-active`).exists()).toBe(isActive);
- expect(wrapper.find({ name: icon }).exists()).toBe(true);
expect(wrapper.findAll('[role="group"').length).toBe(groupLength);
});
});
diff --git a/spec/frontend/notes/components/discussion_filter_spec.js b/spec/frontend/notes/components/discussion_filter_spec.js
index b8d2d721443..7f042c0e9de 100644
--- a/spec/frontend/notes/components/discussion_filter_spec.js
+++ b/spec/frontend/notes/components/discussion_filter_spec.js
@@ -1,4 +1,4 @@
-import Vue from 'vue';
+import createEventHub from '~/helpers/event_hub_factory';
import Vuex from 'vuex';
import { createLocalVue, mount } from '@vue/test-utils';
@@ -132,7 +132,7 @@ describe('DiscussionFilter component', () => {
});
describe('Merge request tabs', () => {
- eventHub = new Vue();
+ eventHub = createEventHub();
beforeEach(() => {
window.mrTabs = {
diff --git a/spec/frontend/notes/components/discussion_notes_spec.js b/spec/frontend/notes/components/discussion_notes_spec.js
index 81773752037..5a10deefd09 100644
--- a/spec/frontend/notes/components/discussion_notes_spec.js
+++ b/spec/frontend/notes/components/discussion_notes_spec.js
@@ -7,7 +7,7 @@ import PlaceholderNote from '~/vue_shared/components/notes/placeholder_note.vue'
import PlaceholderSystemNote from '~/vue_shared/components/notes/placeholder_system_note.vue';
import SystemNote from '~/vue_shared/components/notes/system_note.vue';
import createStore from '~/notes/stores';
-import { noteableDataMock, discussionMock, notesDataMock } from '../../notes/mock_data';
+import { noteableDataMock, discussionMock, notesDataMock } from '../mock_data';
describe('DiscussionNotes', () => {
let wrapper;
diff --git a/spec/frontend/notes/components/note_form_spec.js b/spec/frontend/notes/components/note_form_spec.js
index bccac03126c..8270c148fb5 100644
--- a/spec/frontend/notes/components/note_form_spec.js
+++ b/spec/frontend/notes/components/note_form_spec.js
@@ -161,18 +161,18 @@ describe('issue_note_form component', () => {
describe('actions', () => {
it('should be possible to cancel', () => {
- // TODO: do not spy on vm
- jest.spyOn(wrapper.vm, 'cancelHandler');
+ const cancelHandler = jest.fn();
wrapper.setProps({
...props,
isEditing: true,
});
+ wrapper.setMethods({ cancelHandler });
return wrapper.vm.$nextTick().then(() => {
- const cancelButton = wrapper.find('.note-edit-cancel');
+ const cancelButton = wrapper.find('[data-testid="cancel"]');
cancelButton.trigger('click');
- expect(wrapper.vm.cancelHandler).toHaveBeenCalled();
+ expect(cancelHandler).toHaveBeenCalledWith(true);
});
});
diff --git a/spec/frontend/notes/components/note_header_spec.js b/spec/frontend/notes/components/note_header_spec.js
index d477de69716..2bb08b60569 100644
--- a/spec/frontend/notes/components/note_header_spec.js
+++ b/spec/frontend/notes/components/note_header_spec.js
@@ -1,7 +1,7 @@
import { shallowMount, createLocalVue } from '@vue/test-utils';
+import { nextTick } from 'vue';
import Vuex from 'vuex';
import NoteHeader from '~/notes/components/note_header.vue';
-import GitlabTeamMemberBadge from '~/vue_shared/components/user_avatar/badges/gitlab_team_member_badge.vue';
const localVue = createLocalVue();
localVue.use(Vuex);
@@ -18,6 +18,7 @@ describe('NoteHeader component', () => {
const findActionText = () => wrapper.find({ ref: 'actionText' });
const findTimestampLink = () => wrapper.find({ ref: 'noteTimestampLink' });
const findTimestamp = () => wrapper.find({ ref: 'noteTimestamp' });
+ const findConfidentialIndicator = () => wrapper.find('[data-testid="confidentialIndicator"]');
const findSpinner = () => wrapper.find({ ref: 'spinner' });
const author = {
@@ -140,20 +141,6 @@ describe('NoteHeader component', () => {
});
});
- test.each`
- props | expected | message1 | message2
- ${{ author: { ...author, is_gitlab_employee: true } }} | ${true} | ${'renders'} | ${'true'}
- ${{ author: { ...author, is_gitlab_employee: false } }} | ${false} | ${"doesn't render"} | ${'false'}
- ${{ author }} | ${false} | ${"doesn't render"} | ${'undefined'}
- `(
- '$message1 GitLab team member badge when `is_gitlab_employee` is $message2',
- ({ props, expected }) => {
- createComponent(props);
-
- expect(wrapper.find(GitlabTeamMemberBadge).exists()).toBe(expected);
- },
- );
-
describe('loading spinner', () => {
it('shows spinner when showSpinner is true', () => {
createComponent();
@@ -179,4 +166,81 @@ describe('NoteHeader component', () => {
expect(findTimestamp().exists()).toBe(true);
});
});
+
+ describe('author username link', () => {
+ it('proxies `mouseenter` event to author name link', () => {
+ createComponent({ author });
+
+ const dispatchEvent = jest.spyOn(wrapper.vm.$refs.authorNameLink, 'dispatchEvent');
+
+ wrapper.find({ ref: 'authorUsernameLink' }).trigger('mouseenter');
+
+ expect(dispatchEvent).toHaveBeenCalledWith(new Event('mouseenter'));
+ });
+
+ it('proxies `mouseleave` event to author name link', () => {
+ createComponent({ author });
+
+ const dispatchEvent = jest.spyOn(wrapper.vm.$refs.authorNameLink, 'dispatchEvent');
+
+ wrapper.find({ ref: 'authorUsernameLink' }).trigger('mouseleave');
+
+ expect(dispatchEvent).toHaveBeenCalledWith(new Event('mouseleave'));
+ });
+ });
+
+ describe('when author status tooltip is opened', () => {
+ it('removes `title` attribute from emoji to prevent duplicate tooltips', () => {
+ createComponent({
+ author: {
+ ...author,
+ status_tooltip_html:
+ '"<span class="user-status-emoji has-tooltip" title="foo bar" data-html="true" data-placement="top"><gl-emoji title="basketball and hoop" data-name="basketball" data-unicode-version="6.0">ðŸ€</gl-emoji></span>"',
+ },
+ });
+
+ return nextTick().then(() => {
+ const authorStatus = wrapper.find({ ref: 'authorStatus' });
+ authorStatus.trigger('mouseenter');
+
+ expect(authorStatus.find('gl-emoji').attributes('title')).toBeUndefined();
+ });
+ });
+ });
+
+ describe('when author username link is hovered', () => {
+ it('toggles hover specific CSS classes on author name link', done => {
+ createComponent({ author });
+
+ const authorUsernameLink = wrapper.find({ ref: 'authorUsernameLink' });
+ const authorNameLink = wrapper.find({ ref: 'authorNameLink' });
+
+ authorUsernameLink.trigger('mouseenter');
+
+ nextTick(() => {
+ expect(authorNameLink.classes()).toContain('hover');
+ expect(authorNameLink.classes()).toContain('text-underline');
+
+ authorUsernameLink.trigger('mouseleave');
+
+ nextTick(() => {
+ expect(authorNameLink.classes()).not.toContain('hover');
+ expect(authorNameLink.classes()).not.toContain('text-underline');
+
+ done();
+ });
+ });
+ });
+ });
+
+ describe('with confidentiality indicator', () => {
+ it.each`
+ status | condition
+ ${true} | ${'shows'}
+ ${false} | ${'hides'}
+ `('$condition icon indicator when isConfidential is $status', ({ status }) => {
+ createComponent({ isConfidential: status });
+ expect(findConfidentialIndicator().exists()).toBe(status);
+ });
+ });
});
diff --git a/spec/frontend/notes/components/noteable_discussion_spec.js b/spec/frontend/notes/components/noteable_discussion_spec.js
index b91f599f158..b14ec2a65be 100644
--- a/spec/frontend/notes/components/noteable_discussion_spec.js
+++ b/spec/frontend/notes/components/noteable_discussion_spec.js
@@ -138,7 +138,7 @@ describe('noteable_discussion component', () => {
describe('signout widget', () => {
beforeEach(() => {
- originalGon = Object.assign({}, window.gon);
+ originalGon = { ...window.gon };
window.gon = window.gon || {};
});
diff --git a/spec/frontend/notes/components/notes_app_spec.js b/spec/frontend/notes/components/notes_app_spec.js
index e22dd85f221..fbfba2efb1d 100644
--- a/spec/frontend/notes/components/notes_app_spec.js
+++ b/spec/frontend/notes/components/notes_app_spec.js
@@ -10,7 +10,7 @@ import createStore from '~/notes/stores';
import * as constants from '~/notes/constants';
import '~/behaviors/markdown/render_gfm';
// TODO: use generated fixture (https://gitlab.com/gitlab-org/gitlab-foss/issues/62491)
-import * as mockData from '../../notes/mock_data';
+import * as mockData from '../mock_data';
import * as urlUtility from '~/lib/utils/url_utility';
import OrderedLayout from '~/vue_shared/components/ordered_layout.vue';
diff --git a/spec/frontend/notes/mixins/discussion_navigation_spec.js b/spec/frontend/notes/mixins/discussion_navigation_spec.js
index 4e5325b8bc3..120de023099 100644
--- a/spec/frontend/notes/mixins/discussion_navigation_spec.js
+++ b/spec/frontend/notes/mixins/discussion_navigation_spec.js
@@ -3,6 +3,7 @@ import { shallowMount, createLocalVue } from '@vue/test-utils';
import * as utils from '~/lib/utils/common_utils';
import discussionNavigation from '~/notes/mixins/discussion_navigation';
import eventHub from '~/notes/event_hub';
+import createEventHub from '~/helpers/event_hub_factory';
import notesModule from '~/notes/stores/modules';
import { setHTMLFixture } from 'helpers/fixtures';
@@ -67,8 +68,7 @@ describe('Discussion navigation mixin', () => {
describe('cycle through discussions', () => {
beforeEach(() => {
- // eslint-disable-next-line new-cap
- window.mrTabs = { eventHub: new localVue(), tabShown: jest.fn() };
+ window.mrTabs = { eventHub: createEventHub(), tabShown: jest.fn() };
});
describe.each`
diff --git a/spec/frontend/notes/mock_data.js b/spec/frontend/notes/mock_data.js
index 9ed79c61c22..980faac2b04 100644
--- a/spec/frontend/notes/mock_data.js
+++ b/spec/frontend/notes/mock_data.js
@@ -57,6 +57,7 @@ export const noteableDataMock = {
updated_by_id: 1,
web_url: '/gitlab-org/gitlab-foss/issues/26',
noteableType: 'issue',
+ blocked_by_issues: [],
};
export const lastFetchedAt = '1501862675';
diff --git a/spec/frontend/notes/old_notes_spec.js b/spec/frontend/notes/old_notes_spec.js
index 49b887b21b4..cb1d563ece7 100644
--- a/spec/frontend/notes/old_notes_spec.js
+++ b/spec/frontend/notes/old_notes_spec.js
@@ -33,7 +33,6 @@ gl.utils.disableButtonIfEmptyField = () => {};
// eslint-disable-next-line jest/no-disabled-tests
describe.skip('Old Notes (~/notes.js)', () => {
beforeEach(() => {
- jest.useFakeTimers();
loadFixtures(fixture);
// Re-declare this here so that test_setup.js#beforeEach() doesn't
@@ -194,7 +193,7 @@ describe.skip('Old Notes (~/notes.js)', () => {
$('.js-comment-button').click();
const $targetNote = $notesContainer.find(`#note_${noteEntity.id}`);
- const updatedNote = Object.assign({}, noteEntity);
+ const updatedNote = { ...noteEntity };
updatedNote.note = 'bar';
notes.updateNote(updatedNote, $targetNote);
@@ -213,13 +212,6 @@ describe.skip('Old Notes (~/notes.js)', () => {
jest.spyOn($note, 'toggleClass');
});
- afterEach(() => {
- expect(typeof urlUtility.getLocationHash.mock).toBe('object');
- urlUtility.getLocationHash.mockRestore();
- expect(urlUtility.getLocationHash.mock).toBeUndefined();
- expect(urlUtility.getLocationHash()).toBeNull();
- });
-
// urlUtility is a dependency of the notes module. Its getLocatinHash() method should be called internally.
it('sets target when hash matches', () => {
@@ -630,48 +622,6 @@ describe.skip('Old Notes (~/notes.js)', () => {
done();
});
});
-
- // This is a bad test carried over from the Karma -> Jest migration.
- // The corresponding test in the Karma suite tests for
- // elements and methods that don't actually exist, and gives a false
- // positive pass.
- /*
- it('should show flash error message when comment failed to be updated', done => {
- mockNotesPost();
- jest.spyOn(notes, 'addFlash').mockName('addFlash');
-
- $('.js-comment-button').click();
-
- deferredPromise()
- .then(() => {
- const $noteEl = $notesContainer.find(`#note_${note.id}`);
- $noteEl.find('.js-note-edit').click();
- $noteEl.find('textarea.js-note-text').val(updatedComment);
-
- mockNotesPostError();
-
- $noteEl.find('.js-comment-save-button').click();
- notes.updateComment({preventDefault: () => {}});
- })
- .then(() => deferredPromise())
- .then(() => {
- const $updatedNoteEl = $notesContainer.find(`#note_${note.id}`);
-
- expect($updatedNoteEl.hasClass('.being-posted')).toEqual(false); // Remove being-posted visuals
- expect(
- $updatedNoteEl
- .find('.note-text')
- .text()
- .trim(),
- ).toEqual(sampleComment); // See if comment reverted back to original
-
- expect(notes.addFlash).toHaveBeenCalled();
- expect(notes.flashContainer.style.display).not.toBe('none');
- done();
- })
- .catch(done.fail);
- }, 5000);
- */
});
describe('postComment with Slash commands', () => {
diff --git a/spec/frontend/notes/stores/actions_spec.js b/spec/frontend/notes/stores/actions_spec.js
index 544d482e7fc..cbfb9597159 100644
--- a/spec/frontend/notes/stores/actions_spec.js
+++ b/spec/frontend/notes/stores/actions_spec.js
@@ -34,6 +34,11 @@ describe('Actions Notes Store', () => {
dispatch = jest.fn();
state = {};
axiosMock = new AxiosMockAdapter(axios);
+
+ // This is necessary as we query Close issue button at the top of issue page when clicking bottom button
+ setFixtures(
+ '<div class="detail-page-header-actions"><button class="btn-close btn-grouped"></button></div>',
+ );
});
afterEach(() => {
@@ -242,9 +247,31 @@ describe('Actions Notes Store', () => {
});
});
- describe('poll', () => {
- jest.useFakeTimers();
+ describe('toggleBlockedIssueWarning', () => {
+ it('should set issue warning as true', done => {
+ testAction(
+ actions.toggleBlockedIssueWarning,
+ true,
+ {},
+ [{ type: 'TOGGLE_BLOCKED_ISSUE_WARNING', payload: true }],
+ [],
+ done,
+ );
+ });
+ it('should set issue warning as false', done => {
+ testAction(
+ actions.toggleBlockedIssueWarning,
+ false,
+ {},
+ [{ type: 'TOGGLE_BLOCKED_ISSUE_WARNING', payload: false }],
+ [],
+ done,
+ );
+ });
+ });
+
+ describe('poll', () => {
beforeEach(done => {
jest.spyOn(axios, 'get');
diff --git a/spec/frontend/notes/stores/collapse_utils_spec.js b/spec/frontend/notes/stores/collapse_utils_spec.js
index d3019f4b9a4..a74809eed79 100644
--- a/spec/frontend/notes/stores/collapse_utils_spec.js
+++ b/spec/frontend/notes/stores/collapse_utils_spec.js
@@ -18,9 +18,7 @@ describe('Collapse utils', () => {
});
it('returns false when a system note is not a description type', () => {
- expect(isDescriptionSystemNote(Object.assign({}, mockSystemNote, { note: 'foo' }))).toEqual(
- false,
- );
+ expect(isDescriptionSystemNote({ ...mockSystemNote, note: 'foo' })).toEqual(false);
});
it('gets the time difference between two notes', () => {
diff --git a/spec/frontend/notes/stores/mutation_spec.js b/spec/frontend/notes/stores/mutation_spec.js
index 06d2654ceca..27e3490d64b 100644
--- a/spec/frontend/notes/stores/mutation_spec.js
+++ b/spec/frontend/notes/stores/mutation_spec.js
@@ -50,7 +50,7 @@ describe('Notes Store mutations', () => {
});
describe('ADD_NEW_REPLY_TO_DISCUSSION', () => {
- const newReply = Object.assign({}, note, { discussion_id: discussionMock.id });
+ const newReply = { ...note, discussion_id: discussionMock.id };
let state;
@@ -86,7 +86,7 @@ describe('Notes Store mutations', () => {
describe('EXPAND_DISCUSSION', () => {
it('should expand a collapsed discussion', () => {
- const discussion = Object.assign({}, discussionMock, { expanded: false });
+ const discussion = { ...discussionMock, expanded: false };
const state = {
discussions: [discussion],
@@ -100,7 +100,7 @@ describe('Notes Store mutations', () => {
describe('COLLAPSE_DISCUSSION', () => {
it('should collapse an expanded discussion', () => {
- const discussion = Object.assign({}, discussionMock, { expanded: true });
+ const discussion = { ...discussionMock, expanded: true };
const state = {
discussions: [discussion],
@@ -114,7 +114,7 @@ describe('Notes Store mutations', () => {
describe('REMOVE_PLACEHOLDER_NOTES', () => {
it('should remove all placeholder notes in indivudal notes and discussion', () => {
- const placeholderNote = Object.assign({}, individualNote, { isPlaceholderNote: true });
+ const placeholderNote = { ...individualNote, isPlaceholderNote: true };
const state = { discussions: [placeholderNote] };
mutations.REMOVE_PLACEHOLDER_NOTES(state);
@@ -298,7 +298,7 @@ describe('Notes Store mutations', () => {
describe('TOGGLE_DISCUSSION', () => {
it('should open a closed discussion', () => {
- const discussion = Object.assign({}, discussionMock, { expanded: false });
+ const discussion = { ...discussionMock, expanded: false };
const state = {
discussions: [discussion],
@@ -348,8 +348,8 @@ describe('Notes Store mutations', () => {
});
it('should open all closed discussions', () => {
- const discussion1 = Object.assign({}, discussionMock, { id: 0, expanded: false });
- const discussion2 = Object.assign({}, discussionMock, { id: 1, expanded: true });
+ const discussion1 = { ...discussionMock, id: 0, expanded: false };
+ const discussion2 = { ...discussionMock, id: 1, expanded: true };
const discussionIds = [discussion1.id, discussion2.id];
const state = { discussions: [discussion1, discussion2] };
@@ -362,8 +362,8 @@ describe('Notes Store mutations', () => {
});
it('should close all opened discussions', () => {
- const discussion1 = Object.assign({}, discussionMock, { id: 0, expanded: false });
- const discussion2 = Object.assign({}, discussionMock, { id: 1, expanded: true });
+ const discussion1 = { ...discussionMock, id: 0, expanded: false };
+ const discussion2 = { ...discussionMock, id: 1, expanded: true };
const discussionIds = [discussion1.id, discussion2.id];
const state = { discussions: [discussion1, discussion2] };
@@ -382,7 +382,7 @@ describe('Notes Store mutations', () => {
discussions: [individualNote],
};
- const updated = Object.assign({}, individualNote.notes[0], { note: 'Foo' });
+ const updated = { ...individualNote.notes[0], note: 'Foo' };
mutations.UPDATE_NOTE(state, updated);
@@ -664,4 +664,40 @@ describe('Notes Store mutations', () => {
expect(state.discussionSortOrder).toBe(DESC);
});
});
+
+ describe('TOGGLE_BLOCKED_ISSUE_WARNING', () => {
+ it('should set isToggleBlockedIssueWarning as true', () => {
+ const state = {
+ discussions: [],
+ targetNoteHash: null,
+ lastFetchedAt: null,
+ isToggleStateButtonLoading: false,
+ isToggleBlockedIssueWarning: false,
+ notesData: {},
+ userData: {},
+ noteableData: {},
+ };
+
+ mutations.TOGGLE_BLOCKED_ISSUE_WARNING(state, true);
+
+ expect(state.isToggleBlockedIssueWarning).toEqual(true);
+ });
+
+ it('should set isToggleBlockedIssueWarning as false', () => {
+ const state = {
+ discussions: [],
+ targetNoteHash: null,
+ lastFetchedAt: null,
+ isToggleStateButtonLoading: false,
+ isToggleBlockedIssueWarning: true,
+ notesData: {},
+ userData: {},
+ noteableData: {},
+ };
+
+ mutations.TOGGLE_BLOCKED_ISSUE_WARNING(state, false);
+
+ expect(state.isToggleBlockedIssueWarning).toEqual(false);
+ });
+ });
});
diff --git a/spec/javascripts/oauth_remember_me_spec.js b/spec/frontend/oauth_remember_me_spec.js
index 381be82697e..381be82697e 100644
--- a/spec/javascripts/oauth_remember_me_spec.js
+++ b/spec/frontend/oauth_remember_me_spec.js
diff --git a/spec/javascripts/pages/admin/application_settings/account_and_limits_spec.js b/spec/frontend/pages/admin/application_settings/account_and_limits_spec.js
index 6a239e307e9..6a239e307e9 100644
--- a/spec/javascripts/pages/admin/application_settings/account_and_limits_spec.js
+++ b/spec/frontend/pages/admin/application_settings/account_and_limits_spec.js
diff --git a/spec/frontend/pages/admin/jobs/index/components/stop_jobs_modal_spec.js b/spec/frontend/pages/admin/jobs/index/components/stop_jobs_modal_spec.js
new file mode 100644
index 00000000000..fe17c03389c
--- /dev/null
+++ b/spec/frontend/pages/admin/jobs/index/components/stop_jobs_modal_spec.js
@@ -0,0 +1,64 @@
+import Vue from 'vue';
+import { redirectTo } from '~/lib/utils/url_utility';
+import mountComponent from 'helpers/vue_mount_component_helper';
+import axios from '~/lib/utils/axios_utils';
+import stopJobsModal from '~/pages/admin/jobs/index/components/stop_jobs_modal.vue';
+
+jest.mock('~/lib/utils/url_utility', () => ({
+ ...jest.requireActual('~/lib/utils/url_utility'),
+ redirectTo: jest.fn(),
+}));
+
+describe('stop_jobs_modal.vue', () => {
+ const props = {
+ url: `${gl.TEST_HOST}/stop_jobs_modal.vue/stopAll`,
+ };
+ let vm;
+
+ afterEach(() => {
+ vm.$destroy();
+ });
+
+ beforeEach(() => {
+ const Component = Vue.extend(stopJobsModal);
+ vm = mountComponent(Component, props);
+ });
+
+ describe('onSubmit', () => {
+ it('stops jobs and redirects to overview page', done => {
+ const responseURL = `${gl.TEST_HOST}/stop_jobs_modal.vue/jobs`;
+ jest.spyOn(axios, 'post').mockImplementation(url => {
+ expect(url).toBe(props.url);
+ return Promise.resolve({
+ request: {
+ responseURL,
+ },
+ });
+ });
+
+ vm.onSubmit()
+ .then(() => {
+ expect(redirectTo).toHaveBeenCalledWith(responseURL);
+ })
+ .then(done)
+ .catch(done.fail);
+ });
+
+ it('displays error if stopping jobs failed', done => {
+ const dummyError = new Error('stopping jobs failed');
+ jest.spyOn(axios, 'post').mockImplementation(url => {
+ expect(url).toBe(props.url);
+ return Promise.reject(dummyError);
+ });
+
+ vm.onSubmit()
+ .then(done.fail)
+ .catch(error => {
+ expect(error).toBe(dummyError);
+ expect(redirectTo).not.toHaveBeenCalled();
+ })
+ .then(done)
+ .catch(done.fail);
+ });
+ });
+});
diff --git a/spec/frontend/pages/admin/users/components/__snapshots__/delete_user_modal_spec.js.snap b/spec/frontend/pages/admin/users/components/__snapshots__/delete_user_modal_spec.js.snap
index ea3bedf59e0..82589e5147c 100644
--- a/spec/frontend/pages/admin/users/components/__snapshots__/delete_user_modal_spec.js.snap
+++ b/spec/frontend/pages/admin/users/components/__snapshots__/delete_user_modal_spec.js.snap
@@ -37,7 +37,6 @@ exports[`User Operation confirmation modal renders modal with form included 1`]
value=""
/>
</form>
-
<gl-deprecated-button-stub
size="md"
variant="secondary"
diff --git a/spec/javascripts/pages/admin/users/new/index_spec.js b/spec/frontend/pages/admin/users/new/index_spec.js
index 3896323eef7..3896323eef7 100644
--- a/spec/javascripts/pages/admin/users/new/index_spec.js
+++ b/spec/frontend/pages/admin/users/new/index_spec.js
diff --git a/spec/frontend/pages/labels/components/promote_label_modal_spec.js b/spec/frontend/pages/labels/components/promote_label_modal_spec.js
new file mode 100644
index 00000000000..9d5beca70b5
--- /dev/null
+++ b/spec/frontend/pages/labels/components/promote_label_modal_spec.js
@@ -0,0 +1,103 @@
+import Vue from 'vue';
+import mountComponent from 'helpers/vue_mount_component_helper';
+import promoteLabelModal from '~/pages/projects/labels/components/promote_label_modal.vue';
+import eventHub from '~/pages/projects/labels/event_hub';
+import axios from '~/lib/utils/axios_utils';
+
+describe('Promote label modal', () => {
+ let vm;
+ const Component = Vue.extend(promoteLabelModal);
+ const labelMockData = {
+ labelTitle: 'Documentation',
+ labelColor: '#5cb85c',
+ labelTextColor: '#ffffff',
+ url: `${gl.TEST_HOST}/dummy/promote/labels`,
+ groupName: 'group',
+ };
+
+ describe('Modal title and description', () => {
+ beforeEach(() => {
+ vm = mountComponent(Component, labelMockData);
+ });
+
+ afterEach(() => {
+ vm.$destroy();
+ });
+
+ it('contains the proper description', () => {
+ expect(vm.text).toContain(
+ `Promoting ${labelMockData.labelTitle} will make it available for all projects inside ${labelMockData.groupName}`,
+ );
+ });
+
+ it('contains a label span with the color', () => {
+ const labelFromTitle = vm.$el.querySelector('.modal-header .label.color-label');
+
+ expect(labelFromTitle.style.backgroundColor).not.toBe(null);
+ expect(labelFromTitle.textContent).toContain(vm.labelTitle);
+ });
+ });
+
+ describe('When requesting a label promotion', () => {
+ beforeEach(() => {
+ vm = mountComponent(Component, {
+ ...labelMockData,
+ });
+ jest.spyOn(eventHub, '$emit').mockImplementation(() => {});
+ });
+
+ afterEach(() => {
+ vm.$destroy();
+ });
+
+ it('redirects when a label is promoted', done => {
+ const responseURL = `${gl.TEST_HOST}/dummy/endpoint`;
+ jest.spyOn(axios, 'post').mockImplementation(url => {
+ expect(url).toBe(labelMockData.url);
+ expect(eventHub.$emit).toHaveBeenCalledWith(
+ 'promoteLabelModal.requestStarted',
+ labelMockData.url,
+ );
+ return Promise.resolve({
+ request: {
+ responseURL,
+ },
+ });
+ });
+
+ vm.onSubmit()
+ .then(() => {
+ expect(eventHub.$emit).toHaveBeenCalledWith('promoteLabelModal.requestFinished', {
+ labelUrl: labelMockData.url,
+ successful: true,
+ });
+ })
+ .then(done)
+ .catch(done.fail);
+ });
+
+ it('displays an error if promoting a label failed', done => {
+ const dummyError = new Error('promoting label failed');
+ dummyError.response = { status: 500 };
+ jest.spyOn(axios, 'post').mockImplementation(url => {
+ expect(url).toBe(labelMockData.url);
+ expect(eventHub.$emit).toHaveBeenCalledWith(
+ 'promoteLabelModal.requestStarted',
+ labelMockData.url,
+ );
+ return Promise.reject(dummyError);
+ });
+
+ vm.onSubmit()
+ .catch(error => {
+ expect(error).toBe(dummyError);
+ expect(eventHub.$emit).toHaveBeenCalledWith('promoteLabelModal.requestFinished', {
+ labelUrl: labelMockData.url,
+ successful: false,
+ });
+ })
+ .then(done)
+ .catch(done.fail);
+ });
+ });
+});
diff --git a/spec/frontend/pages/milestones/shared/components/delete_milestone_modal_spec.js b/spec/frontend/pages/milestones/shared/components/delete_milestone_modal_spec.js
new file mode 100644
index 00000000000..ff5dc6d8988
--- /dev/null
+++ b/spec/frontend/pages/milestones/shared/components/delete_milestone_modal_spec.js
@@ -0,0 +1,109 @@
+import Vue from 'vue';
+import { redirectTo } from '~/lib/utils/url_utility';
+import mountComponent from 'helpers/vue_mount_component_helper';
+import axios from '~/lib/utils/axios_utils';
+import deleteMilestoneModal from '~/pages/milestones/shared/components/delete_milestone_modal.vue';
+import eventHub from '~/pages/milestones/shared/event_hub';
+
+jest.mock('~/lib/utils/url_utility', () => ({
+ ...jest.requireActual('~/lib/utils/url_utility'),
+ redirectTo: jest.fn(),
+}));
+
+describe('delete_milestone_modal.vue', () => {
+ const Component = Vue.extend(deleteMilestoneModal);
+ const props = {
+ issueCount: 1,
+ mergeRequestCount: 2,
+ milestoneId: 3,
+ milestoneTitle: 'my milestone title',
+ milestoneUrl: `${gl.TEST_HOST}/delete_milestone_modal.vue/milestone`,
+ };
+ let vm;
+
+ afterEach(() => {
+ vm.$destroy();
+ });
+
+ describe('onSubmit', () => {
+ beforeEach(() => {
+ vm = mountComponent(Component, props);
+ jest.spyOn(eventHub, '$emit').mockImplementation(() => {});
+ });
+
+ it('deletes milestone and redirects to overview page', done => {
+ const responseURL = `${gl.TEST_HOST}/delete_milestone_modal.vue/milestoneOverview`;
+ jest.spyOn(axios, 'delete').mockImplementation(url => {
+ expect(url).toBe(props.milestoneUrl);
+ expect(eventHub.$emit).toHaveBeenCalledWith(
+ 'deleteMilestoneModal.requestStarted',
+ props.milestoneUrl,
+ );
+ eventHub.$emit.mockReset();
+ return Promise.resolve({
+ request: {
+ responseURL,
+ },
+ });
+ });
+
+ vm.onSubmit()
+ .then(() => {
+ expect(redirectTo).toHaveBeenCalledWith(responseURL);
+ expect(eventHub.$emit).toHaveBeenCalledWith('deleteMilestoneModal.requestFinished', {
+ milestoneUrl: props.milestoneUrl,
+ successful: true,
+ });
+ })
+ .then(done)
+ .catch(done.fail);
+ });
+
+ it('displays error if deleting milestone failed', done => {
+ const dummyError = new Error('deleting milestone failed');
+ dummyError.response = { status: 418 };
+ jest.spyOn(axios, 'delete').mockImplementation(url => {
+ expect(url).toBe(props.milestoneUrl);
+ expect(eventHub.$emit).toHaveBeenCalledWith(
+ 'deleteMilestoneModal.requestStarted',
+ props.milestoneUrl,
+ );
+ eventHub.$emit.mockReset();
+ return Promise.reject(dummyError);
+ });
+
+ vm.onSubmit()
+ .catch(error => {
+ expect(error).toBe(dummyError);
+ expect(redirectTo).not.toHaveBeenCalled();
+ expect(eventHub.$emit).toHaveBeenCalledWith('deleteMilestoneModal.requestFinished', {
+ milestoneUrl: props.milestoneUrl,
+ successful: false,
+ });
+ })
+ .then(done)
+ .catch(done.fail);
+ });
+ });
+
+ describe('text', () => {
+ it('contains the issue and milestone count', () => {
+ vm = mountComponent(Component, props);
+ const value = vm.text;
+
+ expect(value).toContain('remove it from 1 issue and 2 merge requests');
+ });
+
+ it('contains neither issue nor milestone count', () => {
+ vm = mountComponent(Component, {
+ ...props,
+ issueCount: 0,
+ mergeRequestCount: 0,
+ });
+
+ const value = vm.text;
+
+ expect(value).toContain('is not currently used');
+ });
+ });
+});
diff --git a/spec/frontend/pages/milestones/shared/components/promote_milestone_modal_spec.js b/spec/frontend/pages/milestones/shared/components/promote_milestone_modal_spec.js
new file mode 100644
index 00000000000..ff896354d96
--- /dev/null
+++ b/spec/frontend/pages/milestones/shared/components/promote_milestone_modal_spec.js
@@ -0,0 +1,98 @@
+import Vue from 'vue';
+import mountComponent from 'helpers/vue_mount_component_helper';
+import promoteMilestoneModal from '~/pages/milestones/shared/components/promote_milestone_modal.vue';
+import eventHub from '~/pages/milestones/shared/event_hub';
+import axios from '~/lib/utils/axios_utils';
+
+describe('Promote milestone modal', () => {
+ let vm;
+ const Component = Vue.extend(promoteMilestoneModal);
+ const milestoneMockData = {
+ milestoneTitle: 'v1.0',
+ url: `${gl.TEST_HOST}/dummy/promote/milestones`,
+ groupName: 'group',
+ };
+
+ describe('Modal title and description', () => {
+ beforeEach(() => {
+ vm = mountComponent(Component, milestoneMockData);
+ });
+
+ afterEach(() => {
+ vm.$destroy();
+ });
+
+ it('contains the proper description', () => {
+ expect(vm.text).toContain(
+ `Promoting ${milestoneMockData.milestoneTitle} will make it available for all projects inside ${milestoneMockData.groupName}.`,
+ );
+ });
+
+ it('contains the correct title', () => {
+ expect(vm.title).toEqual('Promote v1.0 to group milestone?');
+ });
+ });
+
+ describe('When requesting a milestone promotion', () => {
+ beforeEach(() => {
+ vm = mountComponent(Component, {
+ ...milestoneMockData,
+ });
+ jest.spyOn(eventHub, '$emit').mockImplementation(() => {});
+ });
+
+ afterEach(() => {
+ vm.$destroy();
+ });
+
+ it('redirects when a milestone is promoted', done => {
+ const responseURL = `${gl.TEST_HOST}/dummy/endpoint`;
+ jest.spyOn(axios, 'post').mockImplementation(url => {
+ expect(url).toBe(milestoneMockData.url);
+ expect(eventHub.$emit).toHaveBeenCalledWith(
+ 'promoteMilestoneModal.requestStarted',
+ milestoneMockData.url,
+ );
+ return Promise.resolve({
+ request: {
+ responseURL,
+ },
+ });
+ });
+
+ vm.onSubmit()
+ .then(() => {
+ expect(eventHub.$emit).toHaveBeenCalledWith('promoteMilestoneModal.requestFinished', {
+ milestoneUrl: milestoneMockData.url,
+ successful: true,
+ });
+ })
+ .then(done)
+ .catch(done.fail);
+ });
+
+ it('displays an error if promoting a milestone failed', done => {
+ const dummyError = new Error('promoting milestone failed');
+ dummyError.response = { status: 500 };
+ jest.spyOn(axios, 'post').mockImplementation(url => {
+ expect(url).toBe(milestoneMockData.url);
+ expect(eventHub.$emit).toHaveBeenCalledWith(
+ 'promoteMilestoneModal.requestStarted',
+ milestoneMockData.url,
+ );
+ return Promise.reject(dummyError);
+ });
+
+ vm.onSubmit()
+ .catch(error => {
+ expect(error).toBe(dummyError);
+ expect(eventHub.$emit).toHaveBeenCalledWith('promoteMilestoneModal.requestFinished', {
+ milestoneUrl: milestoneMockData.url,
+ successful: false,
+ });
+ })
+ .then(done)
+ .catch(done.fail);
+ });
+ });
+});
diff --git a/spec/frontend/pages/projects/pipeline_schedules/shared/components/interval_pattern_input_spec.js b/spec/frontend/pages/projects/pipeline_schedules/shared/components/interval_pattern_input_spec.js
new file mode 100644
index 00000000000..9cc1d6eeb5a
--- /dev/null
+++ b/spec/frontend/pages/projects/pipeline_schedules/shared/components/interval_pattern_input_spec.js
@@ -0,0 +1,154 @@
+import { shallowMount } from '@vue/test-utils';
+import IntervalPatternInput from '~/pages/projects/pipeline_schedules/shared/components/interval_pattern_input.vue';
+
+describe('Interval Pattern Input Component', () => {
+ let oldWindowGl;
+ let wrapper;
+
+ const mockHour = 4;
+ const mockWeekDayIndex = 1;
+ const mockDay = 1;
+
+ const cronIntervalPresets = {
+ everyDay: `0 ${mockHour} * * *`,
+ everyWeek: `0 ${mockHour} * * ${mockWeekDayIndex}`,
+ everyMonth: `0 ${mockHour} ${mockDay} * *`,
+ };
+
+ const findEveryDayRadio = () => wrapper.find('#every-day');
+ const findEveryWeekRadio = () => wrapper.find('#every-week');
+ const findEveryMonthRadio = () => wrapper.find('#every-month');
+ const findCustomRadio = () => wrapper.find('#custom');
+ const findCustomInput = () => wrapper.find('#schedule_cron');
+ const selectEveryDayRadio = () => findEveryDayRadio().setChecked();
+ const selectEveryWeekRadio = () => findEveryWeekRadio().setChecked();
+ const selectEveryMonthRadio = () => findEveryMonthRadio().setChecked();
+ const selectCustomRadio = () => findCustomRadio().trigger('click');
+
+ const createWrapper = (props = {}, data = {}) => {
+ if (wrapper) {
+ throw new Error('A wrapper already exists');
+ }
+
+ wrapper = shallowMount(IntervalPatternInput, {
+ propsData: { ...props },
+ data() {
+ return {
+ randomHour: data?.hour || mockHour,
+ randomWeekDayIndex: mockWeekDayIndex,
+ randomDay: mockDay,
+ };
+ },
+ });
+ };
+
+ beforeEach(() => {
+ oldWindowGl = window.gl;
+ window.gl = {
+ ...(window.gl || {}),
+ pipelineScheduleFieldErrors: {
+ updateFormValidityState: jest.fn(),
+ },
+ };
+ });
+
+ afterEach(() => {
+ wrapper.destroy();
+ wrapper = null;
+ window.gl = oldWindowGl;
+ });
+
+ describe('the input field defaults', () => {
+ beforeEach(() => {
+ createWrapper();
+ });
+
+ it('to a non empty string when no initial value is not passed', () => {
+ expect(findCustomInput()).not.toBe('');
+ });
+ });
+
+ describe('the input field', () => {
+ const initialCron = '0 * * * *';
+
+ beforeEach(() => {
+ createWrapper({ initialCronInterval: initialCron });
+ });
+
+ it('is equal to the prop `initialCronInterval` when passed', () => {
+ expect(findCustomInput().element.value).toBe(initialCron);
+ });
+ });
+
+ describe('The input field is enabled', () => {
+ beforeEach(() => {
+ createWrapper();
+ });
+
+ it('when a default option is selected', () => {
+ selectEveryDayRadio();
+
+ return wrapper.vm.$nextTick().then(() => {
+ expect(findCustomInput().attributes('disabled')).toBeUndefined();
+ });
+ });
+
+ it('when the custom option is selected', () => {
+ selectCustomRadio();
+
+ return wrapper.vm.$nextTick().then(() => {
+ expect(findCustomInput().attributes('disabled')).toBeUndefined();
+ });
+ });
+ });
+
+ describe('formattedTime computed property', () => {
+ it.each`
+ desc | hour | expectedValue
+ ${'returns a time in the afternoon if the value of `random time` is higher than 12'} | ${13} | ${'1:00pm'}
+ ${'returns a time in the morning if the value of `random time` is lower than 12'} | ${11} | ${'11:00am'}
+ ${'returns "12:00pm" if the value of `random time` is exactly 12'} | ${12} | ${'12:00pm'}
+ `('$desc', ({ hour, expectedValue }) => {
+ createWrapper({}, { hour });
+
+ expect(wrapper.vm.formattedTime).toBe(expectedValue);
+ });
+ });
+
+ describe('User Actions with radio buttons', () => {
+ it.each`
+ desc | initialCronInterval | act | expectedValue
+ ${'when everyday is selected, update value'} | ${'1 2 3 4 5'} | ${selectEveryDayRadio} | ${cronIntervalPresets.everyDay}
+ ${'when everyweek is selected, update value'} | ${'1 2 3 4 5'} | ${selectEveryWeekRadio} | ${cronIntervalPresets.everyWeek}
+ ${'when everymonth is selected, update value'} | ${'1 2 3 4 5'} | ${selectEveryMonthRadio} | ${cronIntervalPresets.everyMonth}
+ ${'when custom is selected, add space to value'} | ${cronIntervalPresets.everyMonth} | ${selectCustomRadio} | ${`${cronIntervalPresets.everyMonth} `}
+ `('$desc', ({ initialCronInterval, act, expectedValue }) => {
+ createWrapper({ initialCronInterval });
+
+ act();
+
+ return wrapper.vm.$nextTick().then(() => {
+ expect(findCustomInput().element.value).toBe(expectedValue);
+ });
+ });
+ });
+ describe('User actions with input field for Cron syntax', () => {
+ beforeEach(() => {
+ createWrapper();
+ });
+
+ it('when editing the cron input it selects the custom radio button', () => {
+ const newValue = '0 * * * *';
+
+ findCustomInput().setValue(newValue);
+
+ expect(wrapper.vm.cronInterval).toBe(newValue);
+ });
+
+ it('when value of input is one of the defaults, it selects the corresponding radio button', () => {
+ findCustomInput().setValue(cronIntervalPresets.everyWeek);
+
+ expect(wrapper.vm.cronInterval).toBe(cronIntervalPresets.everyWeek);
+ });
+ });
+});
diff --git a/spec/frontend/pages/projects/pipeline_schedules/shared/components/pipeline_schedule_callout_spec.js b/spec/frontend/pages/projects/pipeline_schedules/shared/components/pipeline_schedule_callout_spec.js
new file mode 100644
index 00000000000..5a61f9fca69
--- /dev/null
+++ b/spec/frontend/pages/projects/pipeline_schedules/shared/components/pipeline_schedule_callout_spec.js
@@ -0,0 +1,114 @@
+import Vue from 'vue';
+import Cookies from 'js-cookie';
+import PipelineSchedulesCallout from '~/pages/projects/pipeline_schedules/shared/components/pipeline_schedules_callout.vue';
+import '~/pages/projects/pipeline_schedules/shared/icons/intro_illustration.svg';
+
+jest.mock(
+ '~/pages/projects/pipeline_schedules/shared/icons/intro_illustration.svg',
+ () => '<svg></svg>',
+);
+
+const PipelineSchedulesCalloutComponent = Vue.extend(PipelineSchedulesCallout);
+const cookieKey = 'pipeline_schedules_callout_dismissed';
+const docsUrl = 'help/ci/scheduled_pipelines';
+
+describe('Pipeline Schedule Callout', () => {
+ let calloutComponent;
+
+ beforeEach(() => {
+ setFixtures(`
+ <div id='pipeline-schedules-callout' data-docs-url=${docsUrl}></div>
+ `);
+ });
+
+ describe('independent of cookies', () => {
+ beforeEach(() => {
+ calloutComponent = new PipelineSchedulesCalloutComponent().$mount();
+ });
+
+ it('the component can be initialized', () => {
+ expect(calloutComponent).toBeDefined();
+ });
+
+ it('correctly sets illustrationSvg', () => {
+ expect(calloutComponent.illustrationSvg).toContain('<svg');
+ });
+
+ it('correctly sets docsUrl', () => {
+ expect(calloutComponent.docsUrl).toContain(docsUrl);
+ });
+ });
+
+ describe(`when ${cookieKey} cookie is set`, () => {
+ beforeEach(() => {
+ Cookies.set(cookieKey, true);
+ calloutComponent = new PipelineSchedulesCalloutComponent().$mount();
+ });
+
+ it('correctly sets calloutDismissed to true', () => {
+ expect(calloutComponent.calloutDismissed).toBe(true);
+ });
+
+ it('does not render the callout', () => {
+ expect(calloutComponent.$el.childNodes.length).toBe(0);
+ });
+ });
+
+ describe('when cookie is not set', () => {
+ beforeEach(() => {
+ Cookies.remove(cookieKey);
+ calloutComponent = new PipelineSchedulesCalloutComponent().$mount();
+ });
+
+ it('correctly sets calloutDismissed to false', () => {
+ expect(calloutComponent.calloutDismissed).toBe(false);
+ });
+
+ it('renders the callout container', () => {
+ expect(calloutComponent.$el.querySelector('.bordered-box')).not.toBeNull();
+ });
+
+ it('renders the callout svg', () => {
+ expect(calloutComponent.$el.outerHTML).toContain('<svg');
+ });
+
+ it('renders the callout title', () => {
+ expect(calloutComponent.$el.outerHTML).toContain('Scheduling Pipelines');
+ });
+
+ it('renders the callout text', () => {
+ expect(calloutComponent.$el.outerHTML).toContain('runs pipelines in the future');
+ });
+
+ it('renders the documentation url', () => {
+ expect(calloutComponent.$el.outerHTML).toContain(docsUrl);
+ });
+
+ it('updates calloutDismissed when close button is clicked', done => {
+ calloutComponent.$el.querySelector('#dismiss-callout-btn').click();
+
+ Vue.nextTick(() => {
+ expect(calloutComponent.calloutDismissed).toBe(true);
+ done();
+ });
+ });
+
+ it('#dismissCallout updates calloutDismissed', done => {
+ calloutComponent.dismissCallout();
+
+ Vue.nextTick(() => {
+ expect(calloutComponent.calloutDismissed).toBe(true);
+ done();
+ });
+ });
+
+ it('is hidden when close button is clicked', done => {
+ calloutComponent.$el.querySelector('#dismiss-callout-btn').click();
+
+ Vue.nextTick(() => {
+ expect(calloutComponent.$el.childNodes.length).toBe(0);
+ done();
+ });
+ });
+ });
+});
diff --git a/spec/frontend/pages/projects/shared/permissions/components/settings_panel_spec.js b/spec/frontend/pages/projects/shared/permissions/components/settings_panel_spec.js
index 9c292fa0f2b..1f7eec567b8 100644
--- a/spec/frontend/pages/projects/shared/permissions/components/settings_panel_spec.js
+++ b/spec/frontend/pages/projects/shared/permissions/components/settings_panel_spec.js
@@ -23,6 +23,7 @@ const defaultProps = {
lfsEnabled: true,
emailsDisabled: false,
packagesEnabled: true,
+ showDefaultAwardEmojis: true,
},
canDisableEmails: true,
canChangeVisibilityLevel: true,
@@ -57,9 +58,6 @@ describe('Settings Panel', () => {
return mountFn(settingsPanel, {
propsData,
- provide: {
- glFeatures: { metricsDashboardVisibilitySwitchingAvailable: true },
- },
});
};
@@ -477,6 +475,18 @@ describe('Settings Panel', () => {
});
});
+ describe('Default award emojis', () => {
+ it('should show the "Show default award emojis" input', () => {
+ return wrapper.vm.$nextTick(() => {
+ expect(
+ wrapper
+ .find('input[name="project[project_setting_attributes][show_default_award_emojis]"]')
+ .exists(),
+ ).toBe(true);
+ });
+ });
+ });
+
describe('Metrics dashboard', () => {
it('should show the metrics dashboard access toggle', () => {
return wrapper.vm.$nextTick(() => {
@@ -489,15 +499,22 @@ describe('Settings Panel', () => {
.find('[name="project[project_feature_attributes][metrics_dashboard_access_level]"]')
.setValue(visibilityOptions.PUBLIC);
- expect(wrapper.vm.metricsAccessLevel).toBe(visibilityOptions.PUBLIC);
+ expect(wrapper.vm.metricsDashboardAccessLevel).toBe(visibilityOptions.PUBLIC);
});
it('should contain help text', () => {
- wrapper = overrideCurrentSettings({ visibilityLevel: visibilityOptions.PRIVATE });
-
expect(wrapper.find({ ref: 'metrics-visibility-settings' }).props().helpText).toEqual(
'With Metrics Dashboard you can visualize this project performance metrics',
);
});
+
+ it('should disable the metrics visibility dropdown when the project visibility level changes to private', () => {
+ wrapper = overrideCurrentSettings({ visibilityLevel: visibilityOptions.PRIVATE });
+
+ const metricsSettingsRow = wrapper.find({ ref: 'metrics-visibility-settings' });
+
+ expect(wrapper.vm.metricsOptionsDropdownEnabled).toBe(true);
+ expect(metricsSettingsRow.find('select').attributes('disabled')).toEqual('disabled');
+ });
});
});
diff --git a/spec/javascripts/pages/sessions/new/preserve_url_fragment_spec.js b/spec/frontend/pages/sessions/new/preserve_url_fragment_spec.js
index 1809e92e1d9..1809e92e1d9 100644
--- a/spec/javascripts/pages/sessions/new/preserve_url_fragment_spec.js
+++ b/spec/frontend/pages/sessions/new/preserve_url_fragment_spec.js
diff --git a/spec/frontend/pipelines/components/pipelines_filtered_search_spec.js b/spec/frontend/pipelines/components/pipelines_filtered_search_spec.js
new file mode 100644
index 00000000000..12c6fab9c41
--- /dev/null
+++ b/spec/frontend/pipelines/components/pipelines_filtered_search_spec.js
@@ -0,0 +1,97 @@
+import Api from '~/api';
+import { mount } from '@vue/test-utils';
+import MockAdapter from 'axios-mock-adapter';
+import axios from '~/lib/utils/axios_utils';
+import PipelinesFilteredSearch from '~/pipelines/components/pipelines_filtered_search.vue';
+import {
+ users,
+ mockSearch,
+ pipelineWithStages,
+ branches,
+ mockBranchesAfterMap,
+} from '../mock_data';
+import { GlFilteredSearch } from '@gitlab/ui';
+
+describe('Pipelines filtered search', () => {
+ let wrapper;
+ let mock;
+
+ const findFilteredSearch = () => wrapper.find(GlFilteredSearch);
+ const getSearchToken = type =>
+ findFilteredSearch()
+ .props('availableTokens')
+ .find(token => token.type === type);
+
+ const createComponent = () => {
+ wrapper = mount(PipelinesFilteredSearch, {
+ propsData: {
+ pipelines: [pipelineWithStages],
+ projectId: '21',
+ },
+ attachToDocument: true,
+ });
+ };
+
+ beforeEach(() => {
+ mock = new MockAdapter(axios);
+
+ jest.spyOn(Api, 'projectUsers').mockResolvedValue(users);
+ jest.spyOn(Api, 'branches').mockResolvedValue({ data: branches });
+
+ createComponent();
+ });
+
+ afterEach(() => {
+ mock.restore();
+ wrapper.destroy();
+ wrapper = null;
+ });
+
+ it('displays UI elements', () => {
+ expect(wrapper.isVueInstance()).toBe(true);
+ expect(wrapper.isEmpty()).toBe(false);
+
+ expect(findFilteredSearch().exists()).toBe(true);
+ });
+
+ it('displays search tokens', () => {
+ expect(getSearchToken('username')).toMatchObject({
+ type: 'username',
+ icon: 'user',
+ title: 'Trigger author',
+ unique: true,
+ triggerAuthors: users,
+ projectId: '21',
+ operators: [expect.objectContaining({ value: '=' })],
+ });
+
+ expect(getSearchToken('ref')).toMatchObject({
+ type: 'ref',
+ icon: 'branch',
+ title: 'Branch name',
+ unique: true,
+ branches: mockBranchesAfterMap,
+ projectId: '21',
+ operators: [expect.objectContaining({ value: '=' })],
+ });
+ });
+
+ it('fetches and sets project users', () => {
+ expect(Api.projectUsers).toHaveBeenCalled();
+
+ expect(wrapper.vm.projectUsers).toEqual(users);
+ });
+
+ it('fetches and sets branches', () => {
+ expect(Api.branches).toHaveBeenCalled();
+
+ expect(wrapper.vm.projectBranches).toEqual(mockBranchesAfterMap);
+ });
+
+ it('emits filterPipelines on submit with correct filter', () => {
+ findFilteredSearch().vm.$emit('submit', mockSearch);
+
+ expect(wrapper.emitted('filterPipelines')).toBeTruthy();
+ expect(wrapper.emitted('filterPipelines')[0]).toEqual([mockSearch]);
+ });
+});
diff --git a/spec/frontend/pipelines/graph/stage_column_component_spec.js b/spec/frontend/pipelines/graph/stage_column_component_spec.js
index 88e56eee1d6..d32534326c5 100644
--- a/spec/frontend/pipelines/graph/stage_column_component_spec.js
+++ b/spec/frontend/pipelines/graph/stage_column_component_spec.js
@@ -26,7 +26,7 @@ describe('stage column component', () => {
beforeEach(() => {
const mockGroups = [];
for (let i = 0; i < 3; i += 1) {
- const mockedJob = Object.assign({}, mockJob);
+ const mockedJob = { ...mockJob };
mockedJob.id += i;
mockGroups.push(mockedJob);
}
diff --git a/spec/frontend/pipelines/header_component_spec.js b/spec/frontend/pipelines/header_component_spec.js
new file mode 100644
index 00000000000..1c3a6c545a0
--- /dev/null
+++ b/spec/frontend/pipelines/header_component_spec.js
@@ -0,0 +1,116 @@
+import { shallowMount } from '@vue/test-utils';
+import HeaderComponent from '~/pipelines/components/header_component.vue';
+import CiHeader from '~/vue_shared/components/header_ci_component.vue';
+import eventHub from '~/pipelines/event_hub';
+import { GlModal } from '@gitlab/ui';
+
+describe('Pipeline details header', () => {
+ let wrapper;
+ let glModalDirective;
+
+ const threeWeeksAgo = new Date();
+ threeWeeksAgo.setDate(threeWeeksAgo.getDate() - 21);
+
+ const findDeleteModal = () => wrapper.find(GlModal);
+
+ const defaultProps = {
+ pipeline: {
+ details: {
+ status: {
+ group: 'failed',
+ icon: 'status_failed',
+ label: 'failed',
+ text: 'failed',
+ details_path: 'path',
+ },
+ },
+ id: 123,
+ created_at: threeWeeksAgo.toISOString(),
+ user: {
+ web_url: 'path',
+ name: 'Foo',
+ username: 'foobar',
+ email: 'foo@bar.com',
+ avatar_url: 'link',
+ },
+ retry_path: 'retry',
+ cancel_path: 'cancel',
+ delete_path: 'delete',
+ },
+ isLoading: false,
+ };
+
+ const createComponent = (props = {}) => {
+ glModalDirective = jest.fn();
+
+ wrapper = shallowMount(HeaderComponent, {
+ propsData: {
+ ...props,
+ },
+ directives: {
+ glModal: {
+ bind(el, { value }) {
+ glModalDirective(value);
+ },
+ },
+ },
+ });
+ };
+
+ beforeEach(() => {
+ jest.spyOn(eventHub, '$emit');
+
+ createComponent(defaultProps);
+ });
+
+ afterEach(() => {
+ eventHub.$off();
+
+ wrapper.destroy();
+ wrapper = null;
+ });
+
+ it('should render provided pipeline info', () => {
+ expect(wrapper.find(CiHeader).props()).toMatchObject({
+ status: defaultProps.pipeline.details.status,
+ itemId: defaultProps.pipeline.id,
+ time: defaultProps.pipeline.created_at,
+ user: defaultProps.pipeline.user,
+ });
+ });
+
+ describe('action buttons', () => {
+ it('should not trigger eventHub when nothing happens', () => {
+ expect(eventHub.$emit).not.toHaveBeenCalled();
+ });
+
+ it('should call postAction when retry button action is clicked', () => {
+ wrapper.find('.js-retry-button').vm.$emit('click');
+
+ expect(eventHub.$emit).toHaveBeenCalledWith('headerPostAction', 'retry');
+ });
+
+ it('should call postAction when cancel button action is clicked', () => {
+ wrapper.find('.js-btn-cancel-pipeline').vm.$emit('click');
+
+ expect(eventHub.$emit).toHaveBeenCalledWith('headerPostAction', 'cancel');
+ });
+
+ it('does not show delete modal', () => {
+ expect(findDeleteModal()).not.toBeVisible();
+ });
+
+ describe('when delete button action is clicked', () => {
+ it('displays delete modal', () => {
+ expect(findDeleteModal().props('modalId')).toBe(wrapper.vm.$options.DELETE_MODAL_ID);
+ expect(glModalDirective).toHaveBeenCalledWith(wrapper.vm.$options.DELETE_MODAL_ID);
+ });
+
+ it('should call delete when modal is submitted', () => {
+ findDeleteModal().vm.$emit('ok');
+
+ expect(eventHub.$emit).toHaveBeenCalledWith('headerDeleteAction', 'delete');
+ });
+ });
+ });
+});
diff --git a/spec/frontend/pipelines/linked_pipelines_mock.json b/spec/frontend/pipelines/linked_pipelines_mock.json
new file mode 100644
index 00000000000..8ad19ef4865
--- /dev/null
+++ b/spec/frontend/pipelines/linked_pipelines_mock.json
@@ -0,0 +1,3536 @@
+{
+ "id": 23211253,
+ "user": {
+ "id": 3585,
+ "name": "Achilleas Pipinellis",
+ "username": "axil",
+ "state": "active",
+ "avatar_url": "https://assets.gitlab-static.net/uploads/-/system/user/avatar/3585/avatar.png",
+ "web_url": "https://gitlab.com/axil",
+ "status_tooltip_html": "\u003cspan class=\"user-status-emoji has-tooltip\" title=\"I like pizza\" data-html=\"true\" data-placement=\"top\"\u003e\u003cgl-emoji title=\"slice of pizza\" data-name=\"pizza\" data-unicode-version=\"6.0\"\u003eðŸ•\u003c/gl-emoji\u003e\u003c/span\u003e",
+ "path": "/axil"
+ },
+ "active": false,
+ "coverage": null,
+ "source": "push",
+ "created_at": "2018-06-05T11:31:30.452Z",
+ "updated_at": "2018-10-31T16:35:31.305Z",
+ "path": "/gitlab-org/gitlab-runner/pipelines/23211253",
+ "flags": {
+ "latest": false,
+ "stuck": false,
+ "auto_devops": false,
+ "merge_request": false,
+ "yaml_errors": false,
+ "retryable": false,
+ "cancelable": false,
+ "failure_reason": false
+ },
+ "details": {
+ "status": {
+ "icon": "status_success",
+ "text": "passed",
+ "label": "passed",
+ "group": "success",
+ "tooltip": "passed",
+ "has_details": true,
+ "details_path": "/gitlab-org/gitlab-runner/pipelines/23211253",
+ "illustration": null,
+ "favicon": "https://gitlab.com/assets/ci_favicons/favicon_status_success-8451333011eee8ce9f2ab25dc487fe24a8758c694827a582f17f42b0a90446a2.png"
+ },
+ "duration": 53,
+ "finished_at": "2018-10-31T16:35:31.299Z",
+ "stages": [
+ {
+ "name": "prebuild",
+ "title": "prebuild: passed",
+ "groups": [
+ {
+ "name": "review-docs-deploy",
+ "size": 1,
+ "status": {
+ "icon": "status_success",
+ "text": "passed",
+ "label": "manual play action",
+ "group": "success",
+ "tooltip": "passed",
+ "has_details": true,
+ "details_path": "/gitlab-org/gitlab-runner/-/jobs/72469032",
+ "illustration": {
+ "image": "https://assets.gitlab-static.net/assets/illustrations/manual_action-2b4ca0d1bcfd92aebf33d484e36cbf7a102d007f76b5a0cfea636033a629d601.svg",
+ "size": "svg-394",
+ "title": "This job requires a manual action",
+ "content": "This job depends on a user to trigger its process. Often they are used to deploy code to production environments"
+ },
+ "favicon": "https://gitlab.com/assets/ci_favicons/favicon_status_success-8451333011eee8ce9f2ab25dc487fe24a8758c694827a582f17f42b0a90446a2.png",
+ "action": {
+ "icon": "play",
+ "title": "Play",
+ "path": "/gitlab-org/gitlab-runner/-/jobs/72469032/play",
+ "method": "post",
+ "button_title": "Trigger this manual action"
+ }
+ },
+ "jobs": [
+ {
+ "id": 72469032,
+ "name": "review-docs-deploy",
+ "started": "2018-10-31T16:34:58.778Z",
+ "archived": false,
+ "build_path": "/gitlab-org/gitlab-runner/-/jobs/72469032",
+ "retry_path": "/gitlab-org/gitlab-runner/-/jobs/72469032/retry",
+ "play_path": "/gitlab-org/gitlab-runner/-/jobs/72469032/play",
+ "playable": true,
+ "scheduled": false,
+ "created_at": "2018-06-05T11:31:30.495Z",
+ "updated_at": "2018-10-31T16:35:31.251Z",
+ "status": {
+ "icon": "status_success",
+ "text": "passed",
+ "label": "manual play action",
+ "group": "success",
+ "tooltip": "passed",
+ "has_details": true,
+ "details_path": "/gitlab-org/gitlab-runner/-/jobs/72469032",
+ "illustration": {
+ "image": "https://assets.gitlab-static.net/assets/illustrations/manual_action-2b4ca0d1bcfd92aebf33d484e36cbf7a102d007f76b5a0cfea636033a629d601.svg",
+ "size": "svg-394",
+ "title": "This job requires a manual action",
+ "content": "This job depends on a user to trigger its process. Often they are used to deploy code to production environments"
+ },
+ "favicon": "https://gitlab.com/assets/ci_favicons/favicon_status_success-8451333011eee8ce9f2ab25dc487fe24a8758c694827a582f17f42b0a90446a2.png",
+ "action": {
+ "icon": "play",
+ "title": "Play",
+ "path": "/gitlab-org/gitlab-runner/-/jobs/72469032/play",
+ "method": "post",
+ "button_title": "Trigger this manual action"
+ }
+ }
+ }
+ ]
+ }
+ ],
+ "status": {
+ "icon": "status_success",
+ "text": "passed",
+ "label": "passed",
+ "group": "success",
+ "tooltip": "passed",
+ "has_details": true,
+ "details_path": "/gitlab-org/gitlab-runner/pipelines/23211253#prebuild",
+ "illustration": null,
+ "favicon": "https://gitlab.com/assets/ci_favicons/favicon_status_success-8451333011eee8ce9f2ab25dc487fe24a8758c694827a582f17f42b0a90446a2.png"
+ },
+ "path": "/gitlab-org/gitlab-runner/pipelines/23211253#prebuild",
+ "dropdown_path": "/gitlab-org/gitlab-runner/pipelines/23211253/stage.json?stage=prebuild"
+ },
+ {
+ "name": "test",
+ "title": "test: passed",
+ "groups": [
+ {
+ "name": "docs check links",
+ "size": 1,
+ "status": {
+ "icon": "status_success",
+ "text": "passed",
+ "label": "passed",
+ "group": "success",
+ "tooltip": "passed",
+ "has_details": true,
+ "details_path": "/gitlab-org/gitlab-runner/-/jobs/72469033",
+ "illustration": {
+ "image": "https://assets.gitlab-static.net/assets/illustrations/skipped-job_empty-8b877955fbf175e42ae65b6cb95346e15282c6fc5b682756c329af3a0055225e.svg",
+ "size": "svg-430",
+ "title": "This job does not have a trace."
+ },
+ "favicon": "https://gitlab.com/assets/ci_favicons/favicon_status_success-8451333011eee8ce9f2ab25dc487fe24a8758c694827a582f17f42b0a90446a2.png",
+ "action": {
+ "icon": "retry",
+ "title": "Retry",
+ "path": "/gitlab-org/gitlab-runner/-/jobs/72469033/retry",
+ "method": "post",
+ "button_title": "Retry this job"
+ }
+ },
+ "jobs": [
+ {
+ "id": 72469033,
+ "name": "docs check links",
+ "started": "2018-06-05T11:31:33.240Z",
+ "archived": false,
+ "build_path": "/gitlab-org/gitlab-runner/-/jobs/72469033",
+ "retry_path": "/gitlab-org/gitlab-runner/-/jobs/72469033/retry",
+ "playable": false,
+ "scheduled": false,
+ "created_at": "2018-06-05T11:31:30.627Z",
+ "updated_at": "2018-06-05T11:31:54.363Z",
+ "status": {
+ "icon": "status_success",
+ "text": "passed",
+ "label": "passed",
+ "group": "success",
+ "tooltip": "passed",
+ "has_details": true,
+ "details_path": "/gitlab-org/gitlab-runner/-/jobs/72469033",
+ "illustration": {
+ "image": "https://assets.gitlab-static.net/assets/illustrations/skipped-job_empty-8b877955fbf175e42ae65b6cb95346e15282c6fc5b682756c329af3a0055225e.svg",
+ "size": "svg-430",
+ "title": "This job does not have a trace."
+ },
+ "favicon": "https://gitlab.com/assets/ci_favicons/favicon_status_success-8451333011eee8ce9f2ab25dc487fe24a8758c694827a582f17f42b0a90446a2.png",
+ "action": {
+ "icon": "retry",
+ "title": "Retry",
+ "path": "/gitlab-org/gitlab-runner/-/jobs/72469033/retry",
+ "method": "post",
+ "button_title": "Retry this job"
+ }
+ }
+ }
+ ]
+ }
+ ],
+ "status": {
+ "icon": "status_success",
+ "text": "passed",
+ "label": "passed",
+ "group": "success",
+ "tooltip": "passed",
+ "has_details": true,
+ "details_path": "/gitlab-org/gitlab-runner/pipelines/23211253#test",
+ "illustration": null,
+ "favicon": "https://gitlab.com/assets/ci_favicons/favicon_status_success-8451333011eee8ce9f2ab25dc487fe24a8758c694827a582f17f42b0a90446a2.png"
+ },
+ "path": "/gitlab-org/gitlab-runner/pipelines/23211253#test",
+ "dropdown_path": "/gitlab-org/gitlab-runner/pipelines/23211253/stage.json?stage=test"
+ },
+ {
+ "name": "cleanup",
+ "title": "cleanup: skipped",
+ "groups": [
+ {
+ "name": "review-docs-cleanup",
+ "size": 1,
+ "status": {
+ "icon": "status_manual",
+ "text": "manual",
+ "label": "manual stop action",
+ "group": "manual",
+ "tooltip": "manual action",
+ "has_details": true,
+ "details_path": "/gitlab-org/gitlab-runner/-/jobs/72469034",
+ "illustration": {
+ "image": "https://assets.gitlab-static.net/assets/illustrations/manual_action-2b4ca0d1bcfd92aebf33d484e36cbf7a102d007f76b5a0cfea636033a629d601.svg",
+ "size": "svg-394",
+ "title": "This job requires a manual action",
+ "content": "This job depends on a user to trigger its process. Often they are used to deploy code to production environments"
+ },
+ "favicon": "https://gitlab.com/assets/ci_favicons/favicon_status_manual-829a0804612cef47d9efc1618dba38325483657c847dba0546c3b9f0295bb36c.png",
+ "action": {
+ "icon": "stop",
+ "title": "Stop",
+ "path": "/gitlab-org/gitlab-runner/-/jobs/72469034/play",
+ "method": "post",
+ "button_title": "Stop this environment"
+ }
+ },
+ "jobs": [
+ {
+ "id": 72469034,
+ "name": "review-docs-cleanup",
+ "started": null,
+ "archived": false,
+ "build_path": "/gitlab-org/gitlab-runner/-/jobs/72469034",
+ "play_path": "/gitlab-org/gitlab-runner/-/jobs/72469034/play",
+ "playable": true,
+ "scheduled": false,
+ "created_at": "2018-06-05T11:31:30.760Z",
+ "updated_at": "2018-06-05T11:31:56.037Z",
+ "status": {
+ "icon": "status_manual",
+ "text": "manual",
+ "label": "manual stop action",
+ "group": "manual",
+ "tooltip": "manual action",
+ "has_details": true,
+ "details_path": "/gitlab-org/gitlab-runner/-/jobs/72469034",
+ "illustration": {
+ "image": "https://assets.gitlab-static.net/assets/illustrations/manual_action-2b4ca0d1bcfd92aebf33d484e36cbf7a102d007f76b5a0cfea636033a629d601.svg",
+ "size": "svg-394",
+ "title": "This job requires a manual action",
+ "content": "This job depends on a user to trigger its process. Often they are used to deploy code to production environments"
+ },
+ "favicon": "https://gitlab.com/assets/ci_favicons/favicon_status_manual-829a0804612cef47d9efc1618dba38325483657c847dba0546c3b9f0295bb36c.png",
+ "action": {
+ "icon": "stop",
+ "title": "Stop",
+ "path": "/gitlab-org/gitlab-runner/-/jobs/72469034/play",
+ "method": "post",
+ "button_title": "Stop this environment"
+ }
+ }
+ }
+ ]
+ }
+ ],
+ "status": {
+ "icon": "status_skipped",
+ "text": "skipped",
+ "label": "skipped",
+ "group": "skipped",
+ "tooltip": "skipped",
+ "has_details": true,
+ "details_path": "/gitlab-org/gitlab-runner/pipelines/23211253#cleanup",
+ "illustration": null,
+ "favicon": "https://gitlab.com/assets/ci_favicons/favicon_status_skipped-0b9c5e543588945e8c4ca57786bbf2d0c56631959c9f853300392d0315be829b.png"
+ },
+ "path": "/gitlab-org/gitlab-runner/pipelines/23211253#cleanup",
+ "dropdown_path": "/gitlab-org/gitlab-runner/pipelines/23211253/stage.json?stage=cleanup"
+ }
+ ],
+ "artifacts": [],
+ "manual_actions": [
+ {
+ "name": "review-docs-cleanup",
+ "path": "/gitlab-org/gitlab-runner/-/jobs/72469034/play",
+ "playable": true,
+ "scheduled": false
+ },
+ {
+ "name": "review-docs-deploy",
+ "path": "/gitlab-org/gitlab-runner/-/jobs/72469032/play",
+ "playable": true,
+ "scheduled": false
+ }
+ ],
+ "scheduled_actions": []
+ },
+ "ref": {
+ "name": "docs/add-development-guide-to-readme",
+ "path": "/gitlab-org/gitlab-runner/commits/docs/add-development-guide-to-readme",
+ "tag": false,
+ "branch": true,
+ "merge_request": false
+ },
+ "commit": {
+ "id": "8083eb0a920572214d0dccedd7981f05d535ad46",
+ "short_id": "8083eb0a",
+ "title": "Add link to development guide in readme",
+ "created_at": "2018-06-05T11:30:48.000Z",
+ "parent_ids": ["1d7cf79b5a1a2121b9474ac20d61c1b8f621289d"],
+ "message": "Add link to development guide in readme\n\nCloses https://gitlab.com/gitlab-org/gitlab-runner/issues/3122\n",
+ "author_name": "Achilleas Pipinellis",
+ "author_email": "axil@gitlab.com",
+ "authored_date": "2018-06-05T11:30:48.000Z",
+ "committer_name": "Achilleas Pipinellis",
+ "committer_email": "axil@gitlab.com",
+ "committed_date": "2018-06-05T11:30:48.000Z",
+ "author": {
+ "id": 3585,
+ "name": "Achilleas Pipinellis",
+ "username": "axil",
+ "state": "active",
+ "avatar_url": "https://assets.gitlab-static.net/uploads/-/system/user/avatar/3585/avatar.png",
+ "web_url": "https://gitlab.com/axil",
+ "status_tooltip_html": null,
+ "path": "/axil"
+ },
+ "author_gravatar_url": "https://secure.gravatar.com/avatar/1d37af00eec153a8333a4ce18e9aea41?s=80\u0026d=identicon",
+ "commit_url": "https://gitlab.com/gitlab-org/gitlab-runner/commit/8083eb0a920572214d0dccedd7981f05d535ad46",
+ "commit_path": "/gitlab-org/gitlab-runner/commit/8083eb0a920572214d0dccedd7981f05d535ad46"
+ },
+ "project": {
+ "id": 1794617
+ },
+ "triggered_by": {
+ "id": 12,
+ "user": {
+ "id": 376774,
+ "name": "Alessio Caiazza",
+ "username": "nolith",
+ "state": "active",
+ "avatar_url": "https://assets.gitlab-static.net/uploads/-/system/user/avatar/376774/avatar.png",
+ "web_url": "https://gitlab.com/nolith",
+ "status_tooltip_html": null,
+ "path": "/nolith"
+ },
+ "active": false,
+ "coverage": null,
+ "source": "pipeline",
+ "path": "/gitlab-com/gitlab-docs/pipelines/34993051",
+ "details": {
+ "status": {
+ "icon": "status_failed",
+ "text": "failed",
+ "label": "failed",
+ "group": "failed",
+ "tooltip": "failed",
+ "has_details": true,
+ "details_path": "/gitlab-com/gitlab-docs/pipelines/34993051",
+ "illustration": null,
+ "favicon": "https://gitlab.com/assets/ci_favicons/favicon_status_failed-41304d7f7e3828808b0c26771f0309e55296819a9beea3ea9fbf6689d9857c12.png"
+ },
+ "duration": 118,
+ "finished_at": "2018-10-31T16:41:40.615Z",
+ "stages": [
+ {
+ "name": "build-images",
+ "title": "build-images: skipped",
+ "groups": [
+ {
+ "name": "image:bootstrap",
+ "size": 1,
+ "status": {
+ "icon": "status_manual",
+ "text": "manual",
+ "label": "manual play action",
+ "group": "manual",
+ "tooltip": "manual action",
+ "has_details": true,
+ "details_path": "/gitlab-com/gitlab-docs/-/jobs/114982853",
+ "illustration": {
+ "image": "https://assets.gitlab-static.net/assets/illustrations/manual_action-2b4ca0d1bcfd92aebf33d484e36cbf7a102d007f76b5a0cfea636033a629d601.svg",
+ "size": "svg-394",
+ "title": "This job requires a manual action",
+ "content": "This job depends on a user to trigger its process. Often they are used to deploy code to production environments"
+ },
+ "favicon": "https://gitlab.com/assets/ci_favicons/favicon_status_manual-829a0804612cef47d9efc1618dba38325483657c847dba0546c3b9f0295bb36c.png",
+ "action": {
+ "icon": "play",
+ "title": "Play",
+ "path": "/gitlab-com/gitlab-docs/-/jobs/114982853/play",
+ "method": "post",
+ "button_title": "Trigger this manual action"
+ }
+ },
+ "jobs": [
+ {
+ "id": 11421321982853,
+ "name": "image:bootstrap",
+ "started": null,
+ "archived": false,
+ "build_path": "/gitlab-com/gitlab-docs/-/jobs/114982853",
+ "play_path": "/gitlab-com/gitlab-docs/-/jobs/114982853/play",
+ "playable": true,
+ "scheduled": false,
+ "created_at": "2018-10-31T16:35:23.704Z",
+ "updated_at": "2018-10-31T16:35:24.118Z",
+ "status": {
+ "icon": "status_manual",
+ "text": "manual",
+ "label": "manual play action",
+ "group": "manual",
+ "tooltip": "manual action",
+ "has_details": true,
+ "details_path": "/gitlab-com/gitlab-docs/-/jobs/114982853",
+ "illustration": {
+ "image": "https://assets.gitlab-static.net/assets/illustrations/manual_action-2b4ca0d1bcfd92aebf33d484e36cbf7a102d007f76b5a0cfea636033a629d601.svg",
+ "size": "svg-394",
+ "title": "This job requires a manual action",
+ "content": "This job depends on a user to trigger its process. Often they are used to deploy code to production environments"
+ },
+ "favicon": "https://gitlab.com/assets/ci_favicons/favicon_status_manual-829a0804612cef47d9efc1618dba38325483657c847dba0546c3b9f0295bb36c.png",
+ "action": {
+ "icon": "play",
+ "title": "Play",
+ "path": "/gitlab-com/gitlab-docs/-/jobs/114982853/play",
+ "method": "post",
+ "button_title": "Trigger this manual action"
+ }
+ }
+ }
+ ]
+ },
+ {
+ "name": "image:builder-onbuild",
+ "size": 1,
+ "status": {
+ "icon": "status_manual",
+ "text": "manual",
+ "label": "manual play action",
+ "group": "manual",
+ "tooltip": "manual action",
+ "has_details": true,
+ "details_path": "/gitlab-com/gitlab-docs/-/jobs/114982854",
+ "illustration": {
+ "image": "https://assets.gitlab-static.net/assets/illustrations/manual_action-2b4ca0d1bcfd92aebf33d484e36cbf7a102d007f76b5a0cfea636033a629d601.svg",
+ "size": "svg-394",
+ "title": "This job requires a manual action",
+ "content": "This job depends on a user to trigger its process. Often they are used to deploy code to production environments"
+ },
+ "favicon": "https://gitlab.com/assets/ci_favicons/favicon_status_manual-829a0804612cef47d9efc1618dba38325483657c847dba0546c3b9f0295bb36c.png",
+ "action": {
+ "icon": "play",
+ "title": "Play",
+ "path": "/gitlab-com/gitlab-docs/-/jobs/114982854/play",
+ "method": "post",
+ "button_title": "Trigger this manual action"
+ }
+ },
+ "jobs": [
+ {
+ "id": 1149822131854,
+ "name": "image:builder-onbuild",
+ "started": null,
+ "archived": false,
+ "build_path": "/gitlab-com/gitlab-docs/-/jobs/114982854",
+ "play_path": "/gitlab-com/gitlab-docs/-/jobs/114982854/play",
+ "playable": true,
+ "scheduled": false,
+ "created_at": "2018-10-31T16:35:23.728Z",
+ "updated_at": "2018-10-31T16:35:24.070Z",
+ "status": {
+ "icon": "status_manual",
+ "text": "manual",
+ "label": "manual play action",
+ "group": "manual",
+ "tooltip": "manual action",
+ "has_details": true,
+ "details_path": "/gitlab-com/gitlab-docs/-/jobs/114982854",
+ "illustration": {
+ "image": "https://assets.gitlab-static.net/assets/illustrations/manual_action-2b4ca0d1bcfd92aebf33d484e36cbf7a102d007f76b5a0cfea636033a629d601.svg",
+ "size": "svg-394",
+ "title": "This job requires a manual action",
+ "content": "This job depends on a user to trigger its process. Often they are used to deploy code to production environments"
+ },
+ "favicon": "https://gitlab.com/assets/ci_favicons/favicon_status_manual-829a0804612cef47d9efc1618dba38325483657c847dba0546c3b9f0295bb36c.png",
+ "action": {
+ "icon": "play",
+ "title": "Play",
+ "path": "/gitlab-com/gitlab-docs/-/jobs/114982854/play",
+ "method": "post",
+ "button_title": "Trigger this manual action"
+ }
+ }
+ }
+ ]
+ },
+ {
+ "name": "image:nginx-onbuild",
+ "size": 1,
+ "status": {
+ "icon": "status_manual",
+ "text": "manual",
+ "label": "manual play action",
+ "group": "manual",
+ "tooltip": "manual action",
+ "has_details": true,
+ "details_path": "/gitlab-com/gitlab-docs/-/jobs/114982855",
+ "illustration": {
+ "image": "https://assets.gitlab-static.net/assets/illustrations/manual_action-2b4ca0d1bcfd92aebf33d484e36cbf7a102d007f76b5a0cfea636033a629d601.svg",
+ "size": "svg-394",
+ "title": "This job requires a manual action",
+ "content": "This job depends on a user to trigger its process. Often they are used to deploy code to production environments"
+ },
+ "favicon": "https://gitlab.com/assets/ci_favicons/favicon_status_manual-829a0804612cef47d9efc1618dba38325483657c847dba0546c3b9f0295bb36c.png",
+ "action": {
+ "icon": "play",
+ "title": "Play",
+ "path": "/gitlab-com/gitlab-docs/-/jobs/114982855/play",
+ "method": "post",
+ "button_title": "Trigger this manual action"
+ }
+ },
+ "jobs": [
+ {
+ "id": 11498285523424,
+ "name": "image:nginx-onbuild",
+ "started": null,
+ "archived": false,
+ "build_path": "/gitlab-com/gitlab-docs/-/jobs/114982855",
+ "play_path": "/gitlab-com/gitlab-docs/-/jobs/114982855/play",
+ "playable": true,
+ "scheduled": false,
+ "created_at": "2018-10-31T16:35:23.753Z",
+ "updated_at": "2018-10-31T16:35:24.033Z",
+ "status": {
+ "icon": "status_manual",
+ "text": "manual",
+ "label": "manual play action",
+ "group": "manual",
+ "tooltip": "manual action",
+ "has_details": true,
+ "details_path": "/gitlab-com/gitlab-docs/-/jobs/114982855",
+ "illustration": {
+ "image": "https://assets.gitlab-static.net/assets/illustrations/manual_action-2b4ca0d1bcfd92aebf33d484e36cbf7a102d007f76b5a0cfea636033a629d601.svg",
+ "size": "svg-394",
+ "title": "This job requires a manual action",
+ "content": "This job depends on a user to trigger its process. Often they are used to deploy code to production environments"
+ },
+ "favicon": "https://gitlab.com/assets/ci_favicons/favicon_status_manual-829a0804612cef47d9efc1618dba38325483657c847dba0546c3b9f0295bb36c.png",
+ "action": {
+ "icon": "play",
+ "title": "Play",
+ "path": "/gitlab-com/gitlab-docs/-/jobs/114982855/play",
+ "method": "post",
+ "button_title": "Trigger this manual action"
+ }
+ }
+ }
+ ]
+ }
+ ],
+ "status": {
+ "icon": "status_skipped",
+ "text": "skipped",
+ "label": "skipped",
+ "group": "skipped",
+ "tooltip": "skipped",
+ "has_details": true,
+ "details_path": "/gitlab-com/gitlab-docs/pipelines/34993051#build-images",
+ "illustration": null,
+ "favicon": "https://gitlab.com/assets/ci_favicons/favicon_status_skipped-0b9c5e543588945e8c4ca57786bbf2d0c56631959c9f853300392d0315be829b.png"
+ },
+ "path": "/gitlab-com/gitlab-docs/pipelines/34993051#build-images",
+ "dropdown_path": "/gitlab-com/gitlab-docs/pipelines/34993051/stage.json?stage=build-images"
+ },
+ {
+ "name": "build",
+ "title": "build: failed",
+ "groups": [
+ {
+ "name": "compile_dev",
+ "size": 1,
+ "status": {
+ "icon": "status_failed",
+ "text": "failed",
+ "label": "failed",
+ "group": "failed",
+ "tooltip": "failed - (script failure)",
+ "has_details": true,
+ "details_path": "/gitlab-com/gitlab-docs/-/jobs/114984694",
+ "illustration": {
+ "image": "https://assets.gitlab-static.net/assets/illustrations/skipped-job_empty-8b877955fbf175e42ae65b6cb95346e15282c6fc5b682756c329af3a0055225e.svg",
+ "size": "svg-430",
+ "title": "This job does not have a trace."
+ },
+ "favicon": "https://gitlab.com/assets/ci_favicons/favicon_status_failed-41304d7f7e3828808b0c26771f0309e55296819a9beea3ea9fbf6689d9857c12.png",
+ "action": {
+ "icon": "retry",
+ "title": "Retry",
+ "path": "/h5bp/html5-boilerplate/-/jobs/528/retry",
+ "method": "post",
+ "button_title": "Retry this job"
+ }
+ },
+ "jobs": [
+ {
+ "id": 1149846949786,
+ "name": "compile_dev",
+ "started": "2018-10-31T16:39:41.598Z",
+ "archived": false,
+ "build_path": "/gitlab-com/gitlab-docs/-/jobs/114984694",
+ "retry_path": "/gitlab-com/gitlab-docs/-/jobs/114984694/retry",
+ "playable": false,
+ "scheduled": false,
+ "created_at": "2018-10-31T16:39:41.138Z",
+ "updated_at": "2018-10-31T16:41:40.072Z",
+ "status": {
+ "icon": "status_failed",
+ "text": "failed",
+ "label": "failed",
+ "group": "failed",
+ "tooltip": "failed - (script failure)",
+ "has_details": true,
+ "details_path": "/gitlab-com/gitlab-docs/-/jobs/114984694",
+ "illustration": {
+ "image": "https://assets.gitlab-static.net/assets/illustrations/skipped-job_empty-8b877955fbf175e42ae65b6cb95346e15282c6fc5b682756c329af3a0055225e.svg",
+ "size": "svg-430",
+ "title": "This job does not have a trace."
+ },
+ "favicon": "https://gitlab.com/assets/ci_favicons/favicon_status_failed-41304d7f7e3828808b0c26771f0309e55296819a9beea3ea9fbf6689d9857c12.png",
+ "action": {
+ "icon": "retry",
+ "title": "Retry",
+ "path": "/h5bp/html5-boilerplate/-/jobs/528/retry",
+ "method": "post",
+ "button_title": "Retry this job"
+ }
+ },
+ "recoverable": false
+ }
+ ]
+ }
+ ],
+ "status": {
+ "icon": "status_failed",
+ "text": "failed",
+ "label": "failed",
+ "group": "failed",
+ "tooltip": "failed",
+ "has_details": true,
+ "details_path": "/gitlab-com/gitlab-docs/pipelines/34993051#build",
+ "illustration": null,
+ "favicon": "https://gitlab.com/assets/ci_favicons/favicon_status_failed-41304d7f7e3828808b0c26771f0309e55296819a9beea3ea9fbf6689d9857c12.png"
+ },
+ "path": "/gitlab-com/gitlab-docs/pipelines/34993051#build",
+ "dropdown_path": "/gitlab-com/gitlab-docs/pipelines/34993051/stage.json?stage=build"
+ },
+ {
+ "name": "deploy",
+ "title": "deploy: skipped",
+ "groups": [
+ {
+ "name": "review",
+ "size": 1,
+ "status": {
+ "icon": "status_skipped",
+ "text": "skipped",
+ "label": "skipped",
+ "group": "skipped",
+ "tooltip": "skipped",
+ "has_details": true,
+ "details_path": "/gitlab-com/gitlab-docs/-/jobs/114982857",
+ "illustration": {
+ "image": "https://assets.gitlab-static.net/assets/illustrations/skipped-job_empty-8b877955fbf175e42ae65b6cb95346e15282c6fc5b682756c329af3a0055225e.svg",
+ "size": "svg-430",
+ "title": "This job has been skipped"
+ },
+ "favicon": "https://gitlab.com/assets/ci_favicons/favicon_status_skipped-0b9c5e543588945e8c4ca57786bbf2d0c56631959c9f853300392d0315be829b.png"
+ },
+ "jobs": [
+ {
+ "id": 11498282342357,
+ "name": "review",
+ "started": null,
+ "archived": false,
+ "build_path": "/gitlab-com/gitlab-docs/-/jobs/114982857",
+ "playable": false,
+ "scheduled": false,
+ "created_at": "2018-10-31T16:35:23.805Z",
+ "updated_at": "2018-10-31T16:41:40.569Z",
+ "status": {
+ "icon": "status_skipped",
+ "text": "skipped",
+ "label": "skipped",
+ "group": "skipped",
+ "tooltip": "skipped",
+ "has_details": true,
+ "details_path": "/gitlab-com/gitlab-docs/-/jobs/114982857",
+ "illustration": {
+ "image": "https://assets.gitlab-static.net/assets/illustrations/skipped-job_empty-8b877955fbf175e42ae65b6cb95346e15282c6fc5b682756c329af3a0055225e.svg",
+ "size": "svg-430",
+ "title": "This job has been skipped"
+ },
+ "favicon": "https://gitlab.com/assets/ci_favicons/favicon_status_skipped-0b9c5e543588945e8c4ca57786bbf2d0c56631959c9f853300392d0315be829b.png"
+ }
+ }
+ ]
+ },
+ {
+ "name": "review_stop",
+ "size": 1,
+ "status": {
+ "icon": "status_skipped",
+ "text": "skipped",
+ "label": "skipped",
+ "group": "skipped",
+ "tooltip": "skipped",
+ "has_details": true,
+ "details_path": "/gitlab-com/gitlab-docs/-/jobs/114982858",
+ "illustration": {
+ "image": "https://assets.gitlab-static.net/assets/illustrations/skipped-job_empty-8b877955fbf175e42ae65b6cb95346e15282c6fc5b682756c329af3a0055225e.svg",
+ "size": "svg-430",
+ "title": "This job has been skipped"
+ },
+ "favicon": "https://gitlab.com/assets/ci_favicons/favicon_status_skipped-0b9c5e543588945e8c4ca57786bbf2d0c56631959c9f853300392d0315be829b.png"
+ },
+ "jobs": [
+ {
+ "id": 114982858,
+ "name": "review_stop",
+ "started": null,
+ "archived": false,
+ "build_path": "/gitlab-com/gitlab-docs/-/jobs/114982858",
+ "playable": false,
+ "scheduled": false,
+ "created_at": "2018-10-31T16:35:23.840Z",
+ "updated_at": "2018-10-31T16:41:40.480Z",
+ "status": {
+ "icon": "status_skipped",
+ "text": "skipped",
+ "label": "skipped",
+ "group": "skipped",
+ "tooltip": "skipped",
+ "has_details": true,
+ "details_path": "/gitlab-com/gitlab-docs/-/jobs/114982858",
+ "illustration": {
+ "image": "https://assets.gitlab-static.net/assets/illustrations/skipped-job_empty-8b877955fbf175e42ae65b6cb95346e15282c6fc5b682756c329af3a0055225e.svg",
+ "size": "svg-430",
+ "title": "This job has been skipped"
+ },
+ "favicon": "https://gitlab.com/assets/ci_favicons/favicon_status_skipped-0b9c5e543588945e8c4ca57786bbf2d0c56631959c9f853300392d0315be829b.png"
+ }
+ }
+ ]
+ }
+ ],
+ "status": {
+ "icon": "status_skipped",
+ "text": "skipped",
+ "label": "skipped",
+ "group": "skipped",
+ "tooltip": "skipped",
+ "has_details": true,
+ "details_path": "/gitlab-com/gitlab-docs/pipelines/34993051#deploy",
+ "illustration": null,
+ "favicon": "https://gitlab.com/assets/ci_favicons/favicon_status_skipped-0b9c5e543588945e8c4ca57786bbf2d0c56631959c9f853300392d0315be829b.png"
+ },
+ "path": "/gitlab-com/gitlab-docs/pipelines/34993051#deploy",
+ "dropdown_path": "/gitlab-com/gitlab-docs/pipelines/34993051/stage.json?stage=deploy"
+ }
+ ],
+ "artifacts": [],
+ "manual_actions": [
+ {
+ "name": "image:bootstrap",
+ "path": "/gitlab-com/gitlab-docs/-/jobs/114982853/play",
+ "playable": true,
+ "scheduled": false
+ },
+ {
+ "name": "image:builder-onbuild",
+ "path": "/gitlab-com/gitlab-docs/-/jobs/114982854/play",
+ "playable": true,
+ "scheduled": false
+ },
+ {
+ "name": "image:nginx-onbuild",
+ "path": "/gitlab-com/gitlab-docs/-/jobs/114982855/play",
+ "playable": true,
+ "scheduled": false
+ },
+ {
+ "name": "review_stop",
+ "path": "/gitlab-com/gitlab-docs/-/jobs/114982858/play",
+ "playable": false,
+ "scheduled": false
+ }
+ ],
+ "scheduled_actions": []
+ },
+ "project": {
+ "id": 1794617,
+ "name": "Test",
+ "full_path": "/gitlab-com/gitlab-docs",
+ "full_name": "GitLab.com / GitLab Docs"
+ },
+ "triggered_by": {
+ "id": 349932310342451,
+ "user": {
+ "id": 376774,
+ "name": "Alessio Caiazza",
+ "username": "nolith",
+ "state": "active",
+ "avatar_url": "https://assets.gitlab-static.net/uploads/-/system/user/avatar/376774/avatar.png",
+ "web_url": "https://gitlab.com/nolith",
+ "status_tooltip_html": null,
+ "path": "/nolith"
+ },
+ "active": false,
+ "coverage": null,
+ "source": "pipeline",
+ "path": "/gitlab-com/gitlab-docs/pipelines/34993051",
+ "details": {
+ "status": {
+ "icon": "status_failed",
+ "text": "failed",
+ "label": "failed",
+ "group": "failed",
+ "tooltip": "failed",
+ "has_details": true,
+ "details_path": "/gitlab-com/gitlab-docs/pipelines/34993051",
+ "illustration": null,
+ "favicon": "https://gitlab.com/assets/ci_favicons/favicon_status_failed-41304d7f7e3828808b0c26771f0309e55296819a9beea3ea9fbf6689d9857c12.png"
+ },
+ "duration": 118,
+ "finished_at": "2018-10-31T16:41:40.615Z",
+ "stages": [
+ {
+ "name": "build-images",
+ "title": "build-images: skipped",
+ "groups": [
+ {
+ "name": "image:bootstrap",
+ "size": 1,
+ "status": {
+ "icon": "status_manual",
+ "text": "manual",
+ "label": "manual play action",
+ "group": "manual",
+ "tooltip": "manual action",
+ "has_details": true,
+ "details_path": "/gitlab-com/gitlab-docs/-/jobs/114982853",
+ "illustration": {
+ "image": "https://assets.gitlab-static.net/assets/illustrations/manual_action-2b4ca0d1bcfd92aebf33d484e36cbf7a102d007f76b5a0cfea636033a629d601.svg",
+ "size": "svg-394",
+ "title": "This job requires a manual action",
+ "content": "This job depends on a user to trigger its process. Often they are used to deploy code to production environments"
+ },
+ "favicon": "https://gitlab.com/assets/ci_favicons/favicon_status_manual-829a0804612cef47d9efc1618dba38325483657c847dba0546c3b9f0295bb36c.png",
+ "action": {
+ "icon": "play",
+ "title": "Play",
+ "path": "/gitlab-com/gitlab-docs/-/jobs/114982853/play",
+ "method": "post",
+ "button_title": "Trigger this manual action"
+ }
+ },
+ "jobs": [
+ {
+ "id": 11421321982853,
+ "name": "image:bootstrap",
+ "started": null,
+ "archived": false,
+ "build_path": "/gitlab-com/gitlab-docs/-/jobs/114982853",
+ "play_path": "/gitlab-com/gitlab-docs/-/jobs/114982853/play",
+ "playable": true,
+ "scheduled": false,
+ "created_at": "2018-10-31T16:35:23.704Z",
+ "updated_at": "2018-10-31T16:35:24.118Z",
+ "status": {
+ "icon": "status_manual",
+ "text": "manual",
+ "label": "manual play action",
+ "group": "manual",
+ "tooltip": "manual action",
+ "has_details": true,
+ "details_path": "/gitlab-com/gitlab-docs/-/jobs/114982853",
+ "illustration": {
+ "image": "https://assets.gitlab-static.net/assets/illustrations/manual_action-2b4ca0d1bcfd92aebf33d484e36cbf7a102d007f76b5a0cfea636033a629d601.svg",
+ "size": "svg-394",
+ "title": "This job requires a manual action",
+ "content": "This job depends on a user to trigger its process. Often they are used to deploy code to production environments"
+ },
+ "favicon": "https://gitlab.com/assets/ci_favicons/favicon_status_manual-829a0804612cef47d9efc1618dba38325483657c847dba0546c3b9f0295bb36c.png",
+ "action": {
+ "icon": "play",
+ "title": "Play",
+ "path": "/gitlab-com/gitlab-docs/-/jobs/114982853/play",
+ "method": "post",
+ "button_title": "Trigger this manual action"
+ }
+ }
+ }
+ ]
+ },
+ {
+ "name": "image:builder-onbuild",
+ "size": 1,
+ "status": {
+ "icon": "status_manual",
+ "text": "manual",
+ "label": "manual play action",
+ "group": "manual",
+ "tooltip": "manual action",
+ "has_details": true,
+ "details_path": "/gitlab-com/gitlab-docs/-/jobs/114982854",
+ "illustration": {
+ "image": "https://assets.gitlab-static.net/assets/illustrations/manual_action-2b4ca0d1bcfd92aebf33d484e36cbf7a102d007f76b5a0cfea636033a629d601.svg",
+ "size": "svg-394",
+ "title": "This job requires a manual action",
+ "content": "This job depends on a user to trigger its process. Often they are used to deploy code to production environments"
+ },
+ "favicon": "https://gitlab.com/assets/ci_favicons/favicon_status_manual-829a0804612cef47d9efc1618dba38325483657c847dba0546c3b9f0295bb36c.png",
+ "action": {
+ "icon": "play",
+ "title": "Play",
+ "path": "/gitlab-com/gitlab-docs/-/jobs/114982854/play",
+ "method": "post",
+ "button_title": "Trigger this manual action"
+ }
+ },
+ "jobs": [
+ {
+ "id": 1149822131854,
+ "name": "image:builder-onbuild",
+ "started": null,
+ "archived": false,
+ "build_path": "/gitlab-com/gitlab-docs/-/jobs/114982854",
+ "play_path": "/gitlab-com/gitlab-docs/-/jobs/114982854/play",
+ "playable": true,
+ "scheduled": false,
+ "created_at": "2018-10-31T16:35:23.728Z",
+ "updated_at": "2018-10-31T16:35:24.070Z",
+ "status": {
+ "icon": "status_manual",
+ "text": "manual",
+ "label": "manual play action",
+ "group": "manual",
+ "tooltip": "manual action",
+ "has_details": true,
+ "details_path": "/gitlab-com/gitlab-docs/-/jobs/114982854",
+ "illustration": {
+ "image": "https://assets.gitlab-static.net/assets/illustrations/manual_action-2b4ca0d1bcfd92aebf33d484e36cbf7a102d007f76b5a0cfea636033a629d601.svg",
+ "size": "svg-394",
+ "title": "This job requires a manual action",
+ "content": "This job depends on a user to trigger its process. Often they are used to deploy code to production environments"
+ },
+ "favicon": "https://gitlab.com/assets/ci_favicons/favicon_status_manual-829a0804612cef47d9efc1618dba38325483657c847dba0546c3b9f0295bb36c.png",
+ "action": {
+ "icon": "play",
+ "title": "Play",
+ "path": "/gitlab-com/gitlab-docs/-/jobs/114982854/play",
+ "method": "post",
+ "button_title": "Trigger this manual action"
+ }
+ }
+ }
+ ]
+ },
+ {
+ "name": "image:nginx-onbuild",
+ "size": 1,
+ "status": {
+ "icon": "status_manual",
+ "text": "manual",
+ "label": "manual play action",
+ "group": "manual",
+ "tooltip": "manual action",
+ "has_details": true,
+ "details_path": "/gitlab-com/gitlab-docs/-/jobs/114982855",
+ "illustration": {
+ "image": "https://assets.gitlab-static.net/assets/illustrations/manual_action-2b4ca0d1bcfd92aebf33d484e36cbf7a102d007f76b5a0cfea636033a629d601.svg",
+ "size": "svg-394",
+ "title": "This job requires a manual action",
+ "content": "This job depends on a user to trigger its process. Often they are used to deploy code to production environments"
+ },
+ "favicon": "https://gitlab.com/assets/ci_favicons/favicon_status_manual-829a0804612cef47d9efc1618dba38325483657c847dba0546c3b9f0295bb36c.png",
+ "action": {
+ "icon": "play",
+ "title": "Play",
+ "path": "/gitlab-com/gitlab-docs/-/jobs/114982855/play",
+ "method": "post",
+ "button_title": "Trigger this manual action"
+ }
+ },
+ "jobs": [
+ {
+ "id": 11498285523424,
+ "name": "image:nginx-onbuild",
+ "started": null,
+ "archived": false,
+ "build_path": "/gitlab-com/gitlab-docs/-/jobs/114982855",
+ "play_path": "/gitlab-com/gitlab-docs/-/jobs/114982855/play",
+ "playable": true,
+ "scheduled": false,
+ "created_at": "2018-10-31T16:35:23.753Z",
+ "updated_at": "2018-10-31T16:35:24.033Z",
+ "status": {
+ "icon": "status_manual",
+ "text": "manual",
+ "label": "manual play action",
+ "group": "manual",
+ "tooltip": "manual action",
+ "has_details": true,
+ "details_path": "/gitlab-com/gitlab-docs/-/jobs/114982855",
+ "illustration": {
+ "image": "https://assets.gitlab-static.net/assets/illustrations/manual_action-2b4ca0d1bcfd92aebf33d484e36cbf7a102d007f76b5a0cfea636033a629d601.svg",
+ "size": "svg-394",
+ "title": "This job requires a manual action",
+ "content": "This job depends on a user to trigger its process. Often they are used to deploy code to production environments"
+ },
+ "favicon": "https://gitlab.com/assets/ci_favicons/favicon_status_manual-829a0804612cef47d9efc1618dba38325483657c847dba0546c3b9f0295bb36c.png",
+ "action": {
+ "icon": "play",
+ "title": "Play",
+ "path": "/gitlab-com/gitlab-docs/-/jobs/114982855/play",
+ "method": "post",
+ "button_title": "Trigger this manual action"
+ }
+ }
+ }
+ ]
+ }
+ ],
+ "status": {
+ "icon": "status_skipped",
+ "text": "skipped",
+ "label": "skipped",
+ "group": "skipped",
+ "tooltip": "skipped",
+ "has_details": true,
+ "details_path": "/gitlab-com/gitlab-docs/pipelines/34993051#build-images",
+ "illustration": null,
+ "favicon": "https://gitlab.com/assets/ci_favicons/favicon_status_skipped-0b9c5e543588945e8c4ca57786bbf2d0c56631959c9f853300392d0315be829b.png"
+ },
+ "path": "/gitlab-com/gitlab-docs/pipelines/34993051#build-images",
+ "dropdown_path": "/gitlab-com/gitlab-docs/pipelines/34993051/stage.json?stage=build-images"
+ },
+ {
+ "name": "build",
+ "title": "build: failed",
+ "groups": [
+ {
+ "name": "compile_dev",
+ "size": 1,
+ "status": {
+ "icon": "status_failed",
+ "text": "failed",
+ "label": "failed",
+ "group": "failed",
+ "tooltip": "failed - (script failure)",
+ "has_details": true,
+ "details_path": "/gitlab-com/gitlab-docs/-/jobs/114984694",
+ "illustration": {
+ "image": "https://assets.gitlab-static.net/assets/illustrations/skipped-job_empty-8b877955fbf175e42ae65b6cb95346e15282c6fc5b682756c329af3a0055225e.svg",
+ "size": "svg-430",
+ "title": "This job does not have a trace."
+ },
+ "favicon": "https://gitlab.com/assets/ci_favicons/favicon_status_failed-41304d7f7e3828808b0c26771f0309e55296819a9beea3ea9fbf6689d9857c12.png",
+ "action": {
+ "icon": "retry",
+ "title": "Retry",
+ "path": "/gitlab-com/gitlab-docs/-/jobs/114984694/retry",
+ "method": "post",
+ "button_title": "Retry this job"
+ }
+ },
+ "jobs": [
+ {
+ "id": 1149846949786,
+ "name": "compile_dev",
+ "started": "2018-10-31T16:39:41.598Z",
+ "archived": false,
+ "build_path": "/gitlab-com/gitlab-docs/-/jobs/114984694",
+ "retry_path": "/gitlab-com/gitlab-docs/-/jobs/114984694/retry",
+ "playable": false,
+ "scheduled": false,
+ "created_at": "2018-10-31T16:39:41.138Z",
+ "updated_at": "2018-10-31T16:41:40.072Z",
+ "status": {
+ "icon": "status_failed",
+ "text": "failed",
+ "label": "failed",
+ "group": "failed",
+ "tooltip": "failed - (script failure)",
+ "has_details": true,
+ "details_path": "/gitlab-com/gitlab-docs/-/jobs/114984694",
+ "illustration": {
+ "image": "https://assets.gitlab-static.net/assets/illustrations/skipped-job_empty-8b877955fbf175e42ae65b6cb95346e15282c6fc5b682756c329af3a0055225e.svg",
+ "size": "svg-430",
+ "title": "This job does not have a trace."
+ },
+ "favicon": "https://gitlab.com/assets/ci_favicons/favicon_status_failed-41304d7f7e3828808b0c26771f0309e55296819a9beea3ea9fbf6689d9857c12.png",
+ "action": {
+ "icon": "retry",
+ "title": "Retry",
+ "path": "/gitlab-com/gitlab-docs/-/jobs/114984694/retry",
+ "method": "post",
+ "button_title": "Retry this job"
+ }
+ },
+ "recoverable": false
+ }
+ ]
+ }
+ ],
+ "status": {
+ "icon": "status_failed",
+ "text": "failed",
+ "label": "failed",
+ "group": "failed",
+ "tooltip": "failed",
+ "has_details": true,
+ "details_path": "/gitlab-com/gitlab-docs/pipelines/34993051#build",
+ "illustration": null,
+ "favicon": "https://gitlab.com/assets/ci_favicons/favicon_status_failed-41304d7f7e3828808b0c26771f0309e55296819a9beea3ea9fbf6689d9857c12.png"
+ },
+ "path": "/gitlab-com/gitlab-docs/pipelines/34993051#build",
+ "dropdown_path": "/gitlab-com/gitlab-docs/pipelines/34993051/stage.json?stage=build"
+ },
+ {
+ "name": "deploy",
+ "title": "deploy: skipped",
+ "groups": [
+ {
+ "name": "review",
+ "size": 1,
+ "status": {
+ "icon": "status_skipped",
+ "text": "skipped",
+ "label": "skipped",
+ "group": "skipped",
+ "tooltip": "skipped",
+ "has_details": true,
+ "details_path": "/gitlab-com/gitlab-docs/-/jobs/114982857",
+ "illustration": {
+ "image": "https://assets.gitlab-static.net/assets/illustrations/skipped-job_empty-8b877955fbf175e42ae65b6cb95346e15282c6fc5b682756c329af3a0055225e.svg",
+ "size": "svg-430",
+ "title": "This job has been skipped"
+ },
+ "favicon": "https://gitlab.com/assets/ci_favicons/favicon_status_skipped-0b9c5e543588945e8c4ca57786bbf2d0c56631959c9f853300392d0315be829b.png"
+ },
+ "jobs": [
+ {
+ "id": 11498282342357,
+ "name": "review",
+ "started": null,
+ "archived": false,
+ "build_path": "/gitlab-com/gitlab-docs/-/jobs/114982857",
+ "playable": false,
+ "scheduled": false,
+ "created_at": "2018-10-31T16:35:23.805Z",
+ "updated_at": "2018-10-31T16:41:40.569Z",
+ "status": {
+ "icon": "status_skipped",
+ "text": "skipped",
+ "label": "skipped",
+ "group": "skipped",
+ "tooltip": "skipped",
+ "has_details": true,
+ "details_path": "/gitlab-com/gitlab-docs/-/jobs/114982857",
+ "illustration": {
+ "image": "https://assets.gitlab-static.net/assets/illustrations/skipped-job_empty-8b877955fbf175e42ae65b6cb95346e15282c6fc5b682756c329af3a0055225e.svg",
+ "size": "svg-430",
+ "title": "This job has been skipped"
+ },
+ "favicon": "https://gitlab.com/assets/ci_favicons/favicon_status_skipped-0b9c5e543588945e8c4ca57786bbf2d0c56631959c9f853300392d0315be829b.png"
+ }
+ }
+ ]
+ },
+ {
+ "name": "review_stop",
+ "size": 1,
+ "status": {
+ "icon": "status_skipped",
+ "text": "skipped",
+ "label": "skipped",
+ "group": "skipped",
+ "tooltip": "skipped",
+ "has_details": true,
+ "details_path": "/gitlab-com/gitlab-docs/-/jobs/114982858",
+ "illustration": {
+ "image": "https://assets.gitlab-static.net/assets/illustrations/skipped-job_empty-8b877955fbf175e42ae65b6cb95346e15282c6fc5b682756c329af3a0055225e.svg",
+ "size": "svg-430",
+ "title": "This job has been skipped"
+ },
+ "favicon": "https://gitlab.com/assets/ci_favicons/favicon_status_skipped-0b9c5e543588945e8c4ca57786bbf2d0c56631959c9f853300392d0315be829b.png"
+ },
+ "jobs": [
+ {
+ "id": 114982858,
+ "name": "review_stop",
+ "started": null,
+ "archived": false,
+ "build_path": "/gitlab-com/gitlab-docs/-/jobs/114982858",
+ "playable": false,
+ "scheduled": false,
+ "created_at": "2018-10-31T16:35:23.840Z",
+ "updated_at": "2018-10-31T16:41:40.480Z",
+ "status": {
+ "icon": "status_skipped",
+ "text": "skipped",
+ "label": "skipped",
+ "group": "skipped",
+ "tooltip": "skipped",
+ "has_details": true,
+ "details_path": "/gitlab-com/gitlab-docs/-/jobs/114982858",
+ "illustration": {
+ "image": "https://assets.gitlab-static.net/assets/illustrations/skipped-job_empty-8b877955fbf175e42ae65b6cb95346e15282c6fc5b682756c329af3a0055225e.svg",
+ "size": "svg-430",
+ "title": "This job has been skipped"
+ },
+ "favicon": "https://gitlab.com/assets/ci_favicons/favicon_status_skipped-0b9c5e543588945e8c4ca57786bbf2d0c56631959c9f853300392d0315be829b.png"
+ }
+ }
+ ]
+ }
+ ],
+ "status": {
+ "icon": "status_skipped",
+ "text": "skipped",
+ "label": "skipped",
+ "group": "skipped",
+ "tooltip": "skipped",
+ "has_details": true,
+ "details_path": "/gitlab-com/gitlab-docs/pipelines/34993051#deploy",
+ "illustration": null,
+ "favicon": "https://gitlab.com/assets/ci_favicons/favicon_status_skipped-0b9c5e543588945e8c4ca57786bbf2d0c56631959c9f853300392d0315be829b.png"
+ },
+ "path": "/gitlab-com/gitlab-docs/pipelines/34993051#deploy",
+ "dropdown_path": "/gitlab-com/gitlab-docs/pipelines/34993051/stage.json?stage=deploy"
+ }
+ ],
+ "artifacts": [],
+ "manual_actions": [
+ {
+ "name": "image:bootstrap",
+ "path": "/gitlab-com/gitlab-docs/-/jobs/114982853/play",
+ "playable": true,
+ "scheduled": false
+ },
+ {
+ "name": "image:builder-onbuild",
+ "path": "/gitlab-com/gitlab-docs/-/jobs/114982854/play",
+ "playable": true,
+ "scheduled": false
+ },
+ {
+ "name": "image:nginx-onbuild",
+ "path": "/gitlab-com/gitlab-docs/-/jobs/114982855/play",
+ "playable": true,
+ "scheduled": false
+ },
+ {
+ "name": "review_stop",
+ "path": "/gitlab-com/gitlab-docs/-/jobs/114982858/play",
+ "playable": false,
+ "scheduled": false
+ }
+ ],
+ "scheduled_actions": []
+ },
+ "project": {
+ "id": 1794617,
+ "name": "GitLab Docs",
+ "full_path": "/gitlab-com/gitlab-docs",
+ "full_name": "GitLab.com / GitLab Docs"
+ }
+ },
+ "triggered": []
+ },
+ "triggered": [
+ {
+ "id": 34993051,
+ "user": {
+ "id": 376774,
+ "name": "Alessio Caiazza",
+ "username": "nolith",
+ "state": "active",
+ "avatar_url": "https://assets.gitlab-static.net/uploads/-/system/user/avatar/376774/avatar.png",
+ "web_url": "https://gitlab.com/nolith",
+ "status_tooltip_html": null,
+ "path": "/nolith"
+ },
+ "active": false,
+ "coverage": null,
+ "source": "pipeline",
+ "path": "/gitlab-com/gitlab-docs/pipelines/34993051",
+ "details": {
+ "status": {
+ "icon": "status_failed",
+ "text": "failed",
+ "label": "failed",
+ "group": "failed",
+ "tooltip": "failed",
+ "has_details": true,
+ "details_path": "/gitlab-com/gitlab-docs/pipelines/34993051",
+ "illustration": null,
+ "favicon": "https://gitlab.com/assets/ci_favicons/favicon_status_failed-41304d7f7e3828808b0c26771f0309e55296819a9beea3ea9fbf6689d9857c12.png"
+ },
+ "duration": 118,
+ "finished_at": "2018-10-31T16:41:40.615Z",
+ "stages": [
+ {
+ "name": "build-images",
+ "title": "build-images: skipped",
+ "groups": [
+ {
+ "name": "image:bootstrap",
+ "size": 1,
+ "status": {
+ "icon": "status_manual",
+ "text": "manual",
+ "label": "manual play action",
+ "group": "manual",
+ "tooltip": "manual action",
+ "has_details": true,
+ "details_path": "/gitlab-com/gitlab-docs/-/jobs/114982853",
+ "illustration": {
+ "image": "https://assets.gitlab-static.net/assets/illustrations/manual_action-2b4ca0d1bcfd92aebf33d484e36cbf7a102d007f76b5a0cfea636033a629d601.svg",
+ "size": "svg-394",
+ "title": "This job requires a manual action",
+ "content": "This job depends on a user to trigger its process. Often they are used to deploy code to production environments"
+ },
+ "favicon": "https://gitlab.com/assets/ci_favicons/favicon_status_manual-829a0804612cef47d9efc1618dba38325483657c847dba0546c3b9f0295bb36c.png",
+ "action": {
+ "icon": "play",
+ "title": "Play",
+ "path": "/gitlab-com/gitlab-docs/-/jobs/114982853/play",
+ "method": "post",
+ "button_title": "Trigger this manual action"
+ }
+ },
+ "jobs": [
+ {
+ "id": 114982853,
+ "name": "image:bootstrap",
+ "started": null,
+ "archived": false,
+ "build_path": "/gitlab-com/gitlab-docs/-/jobs/114982853",
+ "play_path": "/gitlab-com/gitlab-docs/-/jobs/114982853/play",
+ "playable": true,
+ "scheduled": false,
+ "created_at": "2018-10-31T16:35:23.704Z",
+ "updated_at": "2018-10-31T16:35:24.118Z",
+ "status": {
+ "icon": "status_manual",
+ "text": "manual",
+ "label": "manual play action",
+ "group": "manual",
+ "tooltip": "manual action",
+ "has_details": true,
+ "details_path": "/gitlab-com/gitlab-docs/-/jobs/114982853",
+ "illustration": {
+ "image": "https://assets.gitlab-static.net/assets/illustrations/manual_action-2b4ca0d1bcfd92aebf33d484e36cbf7a102d007f76b5a0cfea636033a629d601.svg",
+ "size": "svg-394",
+ "title": "This job requires a manual action",
+ "content": "This job depends on a user to trigger its process. Often they are used to deploy code to production environments"
+ },
+ "favicon": "https://gitlab.com/assets/ci_favicons/favicon_status_manual-829a0804612cef47d9efc1618dba38325483657c847dba0546c3b9f0295bb36c.png",
+ "action": {
+ "icon": "play",
+ "title": "Play",
+ "path": "/gitlab-com/gitlab-docs/-/jobs/114982853/play",
+ "method": "post",
+ "button_title": "Trigger this manual action"
+ }
+ }
+ }
+ ]
+ },
+ {
+ "name": "image:builder-onbuild",
+ "size": 1,
+ "status": {
+ "icon": "status_manual",
+ "text": "manual",
+ "label": "manual play action",
+ "group": "manual",
+ "tooltip": "manual action",
+ "has_details": true,
+ "details_path": "/gitlab-com/gitlab-docs/-/jobs/114982854",
+ "illustration": {
+ "image": "https://assets.gitlab-static.net/assets/illustrations/manual_action-2b4ca0d1bcfd92aebf33d484e36cbf7a102d007f76b5a0cfea636033a629d601.svg",
+ "size": "svg-394",
+ "title": "This job requires a manual action",
+ "content": "This job depends on a user to trigger its process. Often they are used to deploy code to production environments"
+ },
+ "favicon": "https://gitlab.com/assets/ci_favicons/favicon_status_manual-829a0804612cef47d9efc1618dba38325483657c847dba0546c3b9f0295bb36c.png",
+ "action": {
+ "icon": "play",
+ "title": "Play",
+ "path": "/gitlab-com/gitlab-docs/-/jobs/114982854/play",
+ "method": "post",
+ "button_title": "Trigger this manual action"
+ }
+ },
+ "jobs": [
+ {
+ "id": 114982854,
+ "name": "image:builder-onbuild",
+ "started": null,
+ "archived": false,
+ "build_path": "/gitlab-com/gitlab-docs/-/jobs/114982854",
+ "play_path": "/gitlab-com/gitlab-docs/-/jobs/114982854/play",
+ "playable": true,
+ "scheduled": false,
+ "created_at": "2018-10-31T16:35:23.728Z",
+ "updated_at": "2018-10-31T16:35:24.070Z",
+ "status": {
+ "icon": "status_manual",
+ "text": "manual",
+ "label": "manual play action",
+ "group": "manual",
+ "tooltip": "manual action",
+ "has_details": true,
+ "details_path": "/gitlab-com/gitlab-docs/-/jobs/114982854",
+ "illustration": {
+ "image": "https://assets.gitlab-static.net/assets/illustrations/manual_action-2b4ca0d1bcfd92aebf33d484e36cbf7a102d007f76b5a0cfea636033a629d601.svg",
+ "size": "svg-394",
+ "title": "This job requires a manual action",
+ "content": "This job depends on a user to trigger its process. Often they are used to deploy code to production environments"
+ },
+ "favicon": "https://gitlab.com/assets/ci_favicons/favicon_status_manual-829a0804612cef47d9efc1618dba38325483657c847dba0546c3b9f0295bb36c.png",
+ "action": {
+ "icon": "play",
+ "title": "Play",
+ "path": "/gitlab-com/gitlab-docs/-/jobs/114982854/play",
+ "method": "post",
+ "button_title": "Trigger this manual action"
+ }
+ }
+ }
+ ]
+ },
+ {
+ "name": "image:nginx-onbuild",
+ "size": 1,
+ "status": {
+ "icon": "status_manual",
+ "text": "manual",
+ "label": "manual play action",
+ "group": "manual",
+ "tooltip": "manual action",
+ "has_details": true,
+ "details_path": "/gitlab-com/gitlab-docs/-/jobs/114982855",
+ "illustration": {
+ "image": "https://assets.gitlab-static.net/assets/illustrations/manual_action-2b4ca0d1bcfd92aebf33d484e36cbf7a102d007f76b5a0cfea636033a629d601.svg",
+ "size": "svg-394",
+ "title": "This job requires a manual action",
+ "content": "This job depends on a user to trigger its process. Often they are used to deploy code to production environments"
+ },
+ "favicon": "https://gitlab.com/assets/ci_favicons/favicon_status_manual-829a0804612cef47d9efc1618dba38325483657c847dba0546c3b9f0295bb36c.png",
+ "action": {
+ "icon": "play",
+ "title": "Play",
+ "path": "/gitlab-com/gitlab-docs/-/jobs/114982855/play",
+ "method": "post",
+ "button_title": "Trigger this manual action"
+ }
+ },
+ "jobs": [
+ {
+ "id": 114982855,
+ "name": "image:nginx-onbuild",
+ "started": null,
+ "archived": false,
+ "build_path": "/gitlab-com/gitlab-docs/-/jobs/114982855",
+ "play_path": "/gitlab-com/gitlab-docs/-/jobs/114982855/play",
+ "playable": true,
+ "scheduled": false,
+ "created_at": "2018-10-31T16:35:23.753Z",
+ "updated_at": "2018-10-31T16:35:24.033Z",
+ "status": {
+ "icon": "status_manual",
+ "text": "manual",
+ "label": "manual play action",
+ "group": "manual",
+ "tooltip": "manual action",
+ "has_details": true,
+ "details_path": "/gitlab-com/gitlab-docs/-/jobs/114982855",
+ "illustration": {
+ "image": "https://assets.gitlab-static.net/assets/illustrations/manual_action-2b4ca0d1bcfd92aebf33d484e36cbf7a102d007f76b5a0cfea636033a629d601.svg",
+ "size": "svg-394",
+ "title": "This job requires a manual action",
+ "content": "This job depends on a user to trigger its process. Often they are used to deploy code to production environments"
+ },
+ "favicon": "https://gitlab.com/assets/ci_favicons/favicon_status_manual-829a0804612cef47d9efc1618dba38325483657c847dba0546c3b9f0295bb36c.png",
+ "action": {
+ "icon": "play",
+ "title": "Play",
+ "path": "/gitlab-com/gitlab-docs/-/jobs/114982855/play",
+ "method": "post",
+ "button_title": "Trigger this manual action"
+ }
+ }
+ }
+ ]
+ }
+ ],
+ "status": {
+ "icon": "status_skipped",
+ "text": "skipped",
+ "label": "skipped",
+ "group": "skipped",
+ "tooltip": "skipped",
+ "has_details": true,
+ "details_path": "/gitlab-com/gitlab-docs/pipelines/34993051#build-images",
+ "illustration": null,
+ "favicon": "https://gitlab.com/assets/ci_favicons/favicon_status_skipped-0b9c5e543588945e8c4ca57786bbf2d0c56631959c9f853300392d0315be829b.png"
+ },
+ "path": "/gitlab-com/gitlab-docs/pipelines/34993051#build-images",
+ "dropdown_path": "/gitlab-com/gitlab-docs/pipelines/34993051/stage.json?stage=build-images"
+ },
+ {
+ "name": "build",
+ "title": "build: failed",
+ "groups": [
+ {
+ "name": "compile_dev",
+ "size": 1,
+ "status": {
+ "icon": "status_failed",
+ "text": "failed",
+ "label": "failed",
+ "group": "failed",
+ "tooltip": "failed - (script failure)",
+ "has_details": true,
+ "details_path": "/gitlab-com/gitlab-docs/-/jobs/114984694",
+ "illustration": {
+ "image": "https://assets.gitlab-static.net/assets/illustrations/skipped-job_empty-8b877955fbf175e42ae65b6cb95346e15282c6fc5b682756c329af3a0055225e.svg",
+ "size": "svg-430",
+ "title": "This job does not have a trace."
+ },
+ "favicon": "https://gitlab.com/assets/ci_favicons/favicon_status_failed-41304d7f7e3828808b0c26771f0309e55296819a9beea3ea9fbf6689d9857c12.png",
+ "action": {
+ "icon": "retry",
+ "title": "Retry",
+ "path": "/h5bp/html5-boilerplate/-/jobs/528/retry",
+ "method": "post",
+ "button_title": "Retry this job"
+ }
+ },
+ "jobs": [
+ {
+ "id": 114984694,
+ "name": "compile_dev",
+ "started": "2018-10-31T16:39:41.598Z",
+ "archived": false,
+ "build_path": "/gitlab-com/gitlab-docs/-/jobs/114984694",
+ "retry_path": "/gitlab-com/gitlab-docs/-/jobs/114984694/retry",
+ "playable": false,
+ "scheduled": false,
+ "created_at": "2018-10-31T16:39:41.138Z",
+ "updated_at": "2018-10-31T16:41:40.072Z",
+ "status": {
+ "icon": "status_failed",
+ "text": "failed",
+ "label": "failed",
+ "group": "failed",
+ "tooltip": "failed - (script failure)",
+ "has_details": true,
+ "details_path": "/gitlab-com/gitlab-docs/-/jobs/114984694",
+ "illustration": {
+ "image": "https://assets.gitlab-static.net/assets/illustrations/skipped-job_empty-8b877955fbf175e42ae65b6cb95346e15282c6fc5b682756c329af3a0055225e.svg",
+ "size": "svg-430",
+ "title": "This job does not have a trace."
+ },
+ "favicon": "https://gitlab.com/assets/ci_favicons/favicon_status_failed-41304d7f7e3828808b0c26771f0309e55296819a9beea3ea9fbf6689d9857c12.png",
+ "action": {
+ "icon": "retry",
+ "title": "Retry",
+ "path": "/h5bp/html5-boilerplate/-/jobs/528/retry",
+ "method": "post",
+ "button_title": "Retry this job"
+ }
+ },
+ "recoverable": false
+ }
+ ]
+ }
+ ],
+ "status": {
+ "icon": "status_failed",
+ "text": "failed",
+ "label": "failed",
+ "group": "failed",
+ "tooltip": "failed",
+ "has_details": true,
+ "details_path": "/gitlab-com/gitlab-docs/pipelines/34993051#build",
+ "illustration": null,
+ "favicon": "https://gitlab.com/assets/ci_favicons/favicon_status_failed-41304d7f7e3828808b0c26771f0309e55296819a9beea3ea9fbf6689d9857c12.png"
+ },
+ "path": "/gitlab-com/gitlab-docs/pipelines/34993051#build",
+ "dropdown_path": "/gitlab-com/gitlab-docs/pipelines/34993051/stage.json?stage=build"
+ },
+ {
+ "name": "deploy",
+ "title": "deploy: skipped",
+ "groups": [
+ {
+ "name": "review",
+ "size": 1,
+ "status": {
+ "icon": "status_skipped",
+ "text": "skipped",
+ "label": "skipped",
+ "group": "skipped",
+ "tooltip": "skipped",
+ "has_details": true,
+ "details_path": "/gitlab-com/gitlab-docs/-/jobs/114982857",
+ "illustration": {
+ "image": "https://assets.gitlab-static.net/assets/illustrations/skipped-job_empty-8b877955fbf175e42ae65b6cb95346e15282c6fc5b682756c329af3a0055225e.svg",
+ "size": "svg-430",
+ "title": "This job has been skipped"
+ },
+ "favicon": "https://gitlab.com/assets/ci_favicons/favicon_status_skipped-0b9c5e543588945e8c4ca57786bbf2d0c56631959c9f853300392d0315be829b.png"
+ },
+ "jobs": [
+ {
+ "id": 114982857,
+ "name": "review",
+ "started": null,
+ "archived": false,
+ "build_path": "/gitlab-com/gitlab-docs/-/jobs/114982857",
+ "playable": false,
+ "scheduled": false,
+ "created_at": "2018-10-31T16:35:23.805Z",
+ "updated_at": "2018-10-31T16:41:40.569Z",
+ "status": {
+ "icon": "status_skipped",
+ "text": "skipped",
+ "label": "skipped",
+ "group": "skipped",
+ "tooltip": "skipped",
+ "has_details": true,
+ "details_path": "/gitlab-com/gitlab-docs/-/jobs/114982857",
+ "illustration": {
+ "image": "https://assets.gitlab-static.net/assets/illustrations/skipped-job_empty-8b877955fbf175e42ae65b6cb95346e15282c6fc5b682756c329af3a0055225e.svg",
+ "size": "svg-430",
+ "title": "This job has been skipped"
+ },
+ "favicon": "https://gitlab.com/assets/ci_favicons/favicon_status_skipped-0b9c5e543588945e8c4ca57786bbf2d0c56631959c9f853300392d0315be829b.png"
+ }
+ }
+ ]
+ },
+ {
+ "name": "review_stop",
+ "size": 1,
+ "status": {
+ "icon": "status_skipped",
+ "text": "skipped",
+ "label": "skipped",
+ "group": "skipped",
+ "tooltip": "skipped",
+ "has_details": true,
+ "details_path": "/gitlab-com/gitlab-docs/-/jobs/114982858",
+ "illustration": {
+ "image": "https://assets.gitlab-static.net/assets/illustrations/skipped-job_empty-8b877955fbf175e42ae65b6cb95346e15282c6fc5b682756c329af3a0055225e.svg",
+ "size": "svg-430",
+ "title": "This job has been skipped"
+ },
+ "favicon": "https://gitlab.com/assets/ci_favicons/favicon_status_skipped-0b9c5e543588945e8c4ca57786bbf2d0c56631959c9f853300392d0315be829b.png"
+ },
+ "jobs": [
+ {
+ "id": 114982858,
+ "name": "review_stop",
+ "started": null,
+ "archived": false,
+ "build_path": "/gitlab-com/gitlab-docs/-/jobs/114982858",
+ "playable": false,
+ "scheduled": false,
+ "created_at": "2018-10-31T16:35:23.840Z",
+ "updated_at": "2018-10-31T16:41:40.480Z",
+ "status": {
+ "icon": "status_skipped",
+ "text": "skipped",
+ "label": "skipped",
+ "group": "skipped",
+ "tooltip": "skipped",
+ "has_details": true,
+ "details_path": "/gitlab-com/gitlab-docs/-/jobs/114982858",
+ "illustration": {
+ "image": "https://assets.gitlab-static.net/assets/illustrations/skipped-job_empty-8b877955fbf175e42ae65b6cb95346e15282c6fc5b682756c329af3a0055225e.svg",
+ "size": "svg-430",
+ "title": "This job has been skipped"
+ },
+ "favicon": "https://gitlab.com/assets/ci_favicons/favicon_status_skipped-0b9c5e543588945e8c4ca57786bbf2d0c56631959c9f853300392d0315be829b.png"
+ }
+ }
+ ]
+ }
+ ],
+ "status": {
+ "icon": "status_skipped",
+ "text": "skipped",
+ "label": "skipped",
+ "group": "skipped",
+ "tooltip": "skipped",
+ "has_details": true,
+ "details_path": "/gitlab-com/gitlab-docs/pipelines/34993051#deploy",
+ "illustration": null,
+ "favicon": "https://gitlab.com/assets/ci_favicons/favicon_status_skipped-0b9c5e543588945e8c4ca57786bbf2d0c56631959c9f853300392d0315be829b.png"
+ },
+ "path": "/gitlab-com/gitlab-docs/pipelines/34993051#deploy",
+ "dropdown_path": "/gitlab-com/gitlab-docs/pipelines/34993051/stage.json?stage=deploy"
+ }
+ ],
+ "artifacts": [],
+ "manual_actions": [
+ {
+ "name": "image:bootstrap",
+ "path": "/gitlab-com/gitlab-docs/-/jobs/114982853/play",
+ "playable": true,
+ "scheduled": false
+ },
+ {
+ "name": "image:builder-onbuild",
+ "path": "/gitlab-com/gitlab-docs/-/jobs/114982854/play",
+ "playable": true,
+ "scheduled": false
+ },
+ {
+ "name": "image:nginx-onbuild",
+ "path": "/gitlab-com/gitlab-docs/-/jobs/114982855/play",
+ "playable": true,
+ "scheduled": false
+ },
+ {
+ "name": "review_stop",
+ "path": "/gitlab-com/gitlab-docs/-/jobs/114982858/play",
+ "playable": false,
+ "scheduled": false
+ }
+ ],
+ "scheduled_actions": []
+ },
+ "project": {
+ "id": 1794617,
+ "name": "GitLab Docs",
+ "full_path": "/gitlab-com/gitlab-docs",
+ "full_name": "GitLab.com / GitLab Docs"
+ },
+ "triggered": [{}]
+ },
+ {
+ "id": 34993052,
+ "user": {
+ "id": 376774,
+ "name": "Alessio Caiazza",
+ "username": "nolith",
+ "state": "active",
+ "avatar_url": "https://assets.gitlab-static.net/uploads/-/system/user/avatar/376774/avatar.png",
+ "web_url": "https://gitlab.com/nolith",
+ "status_tooltip_html": null,
+ "path": "/nolith"
+ },
+ "active": false,
+ "coverage": null,
+ "source": "pipeline",
+ "path": "/gitlab-com/gitlab-docs/pipelines/34993051",
+ "details": {
+ "status": {
+ "icon": "status_failed",
+ "text": "failed",
+ "label": "failed",
+ "group": "failed",
+ "tooltip": "failed",
+ "has_details": true,
+ "details_path": "/gitlab-com/gitlab-docs/pipelines/34993051",
+ "illustration": null,
+ "favicon": "https://gitlab.com/assets/ci_favicons/favicon_status_failed-41304d7f7e3828808b0c26771f0309e55296819a9beea3ea9fbf6689d9857c12.png"
+ },
+ "duration": 118,
+ "finished_at": "2018-10-31T16:41:40.615Z",
+ "stages": [
+ {
+ "name": "build-images",
+ "title": "build-images: skipped",
+ "groups": [
+ {
+ "name": "image:bootstrap",
+ "size": 1,
+ "status": {
+ "icon": "status_manual",
+ "text": "manual",
+ "label": "manual play action",
+ "group": "manual",
+ "tooltip": "manual action",
+ "has_details": true,
+ "details_path": "/gitlab-com/gitlab-docs/-/jobs/114982853",
+ "illustration": {
+ "image": "https://assets.gitlab-static.net/assets/illustrations/manual_action-2b4ca0d1bcfd92aebf33d484e36cbf7a102d007f76b5a0cfea636033a629d601.svg",
+ "size": "svg-394",
+ "title": "This job requires a manual action",
+ "content": "This job depends on a user to trigger its process. Often they are used to deploy code to production environments"
+ },
+ "favicon": "https://gitlab.com/assets/ci_favicons/favicon_status_manual-829a0804612cef47d9efc1618dba38325483657c847dba0546c3b9f0295bb36c.png",
+ "action": {
+ "icon": "play",
+ "title": "Play",
+ "path": "/gitlab-com/gitlab-docs/-/jobs/114982853/play",
+ "method": "post",
+ "button_title": "Trigger this manual action"
+ }
+ },
+ "jobs": [
+ {
+ "id": 114982853,
+ "name": "image:bootstrap",
+ "started": null,
+ "archived": false,
+ "build_path": "/gitlab-com/gitlab-docs/-/jobs/114982853",
+ "play_path": "/gitlab-com/gitlab-docs/-/jobs/114982853/play",
+ "playable": true,
+ "scheduled": false,
+ "created_at": "2018-10-31T16:35:23.704Z",
+ "updated_at": "2018-10-31T16:35:24.118Z",
+ "status": {
+ "icon": "status_manual",
+ "text": "manual",
+ "label": "manual play action",
+ "group": "manual",
+ "tooltip": "manual action",
+ "has_details": true,
+ "details_path": "/gitlab-com/gitlab-docs/-/jobs/114982853",
+ "illustration": {
+ "image": "https://assets.gitlab-static.net/assets/illustrations/manual_action-2b4ca0d1bcfd92aebf33d484e36cbf7a102d007f76b5a0cfea636033a629d601.svg",
+ "size": "svg-394",
+ "title": "This job requires a manual action",
+ "content": "This job depends on a user to trigger its process. Often they are used to deploy code to production environments"
+ },
+ "favicon": "https://gitlab.com/assets/ci_favicons/favicon_status_manual-829a0804612cef47d9efc1618dba38325483657c847dba0546c3b9f0295bb36c.png",
+ "action": {
+ "icon": "play",
+ "title": "Play",
+ "path": "/gitlab-com/gitlab-docs/-/jobs/114982853/play",
+ "method": "post",
+ "button_title": "Trigger this manual action"
+ }
+ }
+ }
+ ]
+ },
+ {
+ "name": "image:builder-onbuild",
+ "size": 1,
+ "status": {
+ "icon": "status_manual",
+ "text": "manual",
+ "label": "manual play action",
+ "group": "manual",
+ "tooltip": "manual action",
+ "has_details": true,
+ "details_path": "/gitlab-com/gitlab-docs/-/jobs/114982854",
+ "illustration": {
+ "image": "https://assets.gitlab-static.net/assets/illustrations/manual_action-2b4ca0d1bcfd92aebf33d484e36cbf7a102d007f76b5a0cfea636033a629d601.svg",
+ "size": "svg-394",
+ "title": "This job requires a manual action",
+ "content": "This job depends on a user to trigger its process. Often they are used to deploy code to production environments"
+ },
+ "favicon": "https://gitlab.com/assets/ci_favicons/favicon_status_manual-829a0804612cef47d9efc1618dba38325483657c847dba0546c3b9f0295bb36c.png",
+ "action": {
+ "icon": "play",
+ "title": "Play",
+ "path": "/gitlab-com/gitlab-docs/-/jobs/114982854/play",
+ "method": "post",
+ "button_title": "Trigger this manual action"
+ }
+ },
+ "jobs": [
+ {
+ "id": 114982854,
+ "name": "image:builder-onbuild",
+ "started": null,
+ "archived": false,
+ "build_path": "/gitlab-com/gitlab-docs/-/jobs/114982854",
+ "play_path": "/gitlab-com/gitlab-docs/-/jobs/114982854/play",
+ "playable": true,
+ "scheduled": false,
+ "created_at": "2018-10-31T16:35:23.728Z",
+ "updated_at": "2018-10-31T16:35:24.070Z",
+ "status": {
+ "icon": "status_manual",
+ "text": "manual",
+ "label": "manual play action",
+ "group": "manual",
+ "tooltip": "manual action",
+ "has_details": true,
+ "details_path": "/gitlab-com/gitlab-docs/-/jobs/114982854",
+ "illustration": {
+ "image": "https://assets.gitlab-static.net/assets/illustrations/manual_action-2b4ca0d1bcfd92aebf33d484e36cbf7a102d007f76b5a0cfea636033a629d601.svg",
+ "size": "svg-394",
+ "title": "This job requires a manual action",
+ "content": "This job depends on a user to trigger its process. Often they are used to deploy code to production environments"
+ },
+ "favicon": "https://gitlab.com/assets/ci_favicons/favicon_status_manual-829a0804612cef47d9efc1618dba38325483657c847dba0546c3b9f0295bb36c.png",
+ "action": {
+ "icon": "play",
+ "title": "Play",
+ "path": "/gitlab-com/gitlab-docs/-/jobs/114982854/play",
+ "method": "post",
+ "button_title": "Trigger this manual action"
+ }
+ }
+ }
+ ]
+ },
+ {
+ "name": "image:nginx-onbuild",
+ "size": 1,
+ "status": {
+ "icon": "status_manual",
+ "text": "manual",
+ "label": "manual play action",
+ "group": "manual",
+ "tooltip": "manual action",
+ "has_details": true,
+ "details_path": "/gitlab-com/gitlab-docs/-/jobs/114982855",
+ "illustration": {
+ "image": "https://assets.gitlab-static.net/assets/illustrations/manual_action-2b4ca0d1bcfd92aebf33d484e36cbf7a102d007f76b5a0cfea636033a629d601.svg",
+ "size": "svg-394",
+ "title": "This job requires a manual action",
+ "content": "This job depends on a user to trigger its process. Often they are used to deploy code to production environments"
+ },
+ "favicon": "https://gitlab.com/assets/ci_favicons/favicon_status_manual-829a0804612cef47d9efc1618dba38325483657c847dba0546c3b9f0295bb36c.png",
+ "action": {
+ "icon": "play",
+ "title": "Play",
+ "path": "/gitlab-com/gitlab-docs/-/jobs/114982855/play",
+ "method": "post",
+ "button_title": "Trigger this manual action"
+ }
+ },
+ "jobs": [
+ {
+ "id": 1224982855,
+ "name": "image:nginx-onbuild",
+ "started": null,
+ "archived": false,
+ "build_path": "/gitlab-com/gitlab-docs/-/jobs/114982855",
+ "play_path": "/gitlab-com/gitlab-docs/-/jobs/114982855/play",
+ "playable": true,
+ "scheduled": false,
+ "created_at": "2018-10-31T16:35:23.753Z",
+ "updated_at": "2018-10-31T16:35:24.033Z",
+ "status": {
+ "icon": "status_manual",
+ "text": "manual",
+ "label": "manual play action",
+ "group": "manual",
+ "tooltip": "manual action",
+ "has_details": true,
+ "details_path": "/gitlab-com/gitlab-docs/-/jobs/114982855",
+ "illustration": {
+ "image": "https://assets.gitlab-static.net/assets/illustrations/manual_action-2b4ca0d1bcfd92aebf33d484e36cbf7a102d007f76b5a0cfea636033a629d601.svg",
+ "size": "svg-394",
+ "title": "This job requires a manual action",
+ "content": "This job depends on a user to trigger its process. Often they are used to deploy code to production environments"
+ },
+ "favicon": "https://gitlab.com/assets/ci_favicons/favicon_status_manual-829a0804612cef47d9efc1618dba38325483657c847dba0546c3b9f0295bb36c.png",
+ "action": {
+ "icon": "play",
+ "title": "Play",
+ "path": "/gitlab-com/gitlab-docs/-/jobs/114982855/play",
+ "method": "post",
+ "button_title": "Trigger this manual action"
+ }
+ }
+ }
+ ]
+ }
+ ],
+ "status": {
+ "icon": "status_skipped",
+ "text": "skipped",
+ "label": "skipped",
+ "group": "skipped",
+ "tooltip": "skipped",
+ "has_details": true,
+ "details_path": "/gitlab-com/gitlab-docs/pipelines/34993051#build-images",
+ "illustration": null,
+ "favicon": "https://gitlab.com/assets/ci_favicons/favicon_status_skipped-0b9c5e543588945e8c4ca57786bbf2d0c56631959c9f853300392d0315be829b.png"
+ },
+ "path": "/gitlab-com/gitlab-docs/pipelines/34993051#build-images",
+ "dropdown_path": "/gitlab-com/gitlab-docs/pipelines/34993051/stage.json?stage=build-images"
+ },
+ {
+ "name": "build",
+ "title": "build: failed",
+ "groups": [
+ {
+ "name": "compile_dev",
+ "size": 1,
+ "status": {
+ "icon": "status_failed",
+ "text": "failed",
+ "label": "failed",
+ "group": "failed",
+ "tooltip": "failed - (script failure)",
+ "has_details": true,
+ "details_path": "/gitlab-com/gitlab-docs/-/jobs/114984694",
+ "illustration": {
+ "image": "https://assets.gitlab-static.net/assets/illustrations/skipped-job_empty-8b877955fbf175e42ae65b6cb95346e15282c6fc5b682756c329af3a0055225e.svg",
+ "size": "svg-430",
+ "title": "This job does not have a trace."
+ },
+ "favicon": "https://gitlab.com/assets/ci_favicons/favicon_status_failed-41304d7f7e3828808b0c26771f0309e55296819a9beea3ea9fbf6689d9857c12.png",
+ "action": {
+ "icon": "retry",
+ "title": "Retry",
+ "path": "/gitlab-com/gitlab-docs/-/jobs/114984694/retry",
+ "method": "post",
+ "button_title": "Retry this job"
+ }
+ },
+ "jobs": [
+ {
+ "id": 1123984694,
+ "name": "compile_dev",
+ "started": "2018-10-31T16:39:41.598Z",
+ "archived": false,
+ "build_path": "/gitlab-com/gitlab-docs/-/jobs/114984694",
+ "retry_path": "/gitlab-com/gitlab-docs/-/jobs/114984694/retry",
+ "playable": false,
+ "scheduled": false,
+ "created_at": "2018-10-31T16:39:41.138Z",
+ "updated_at": "2018-10-31T16:41:40.072Z",
+ "status": {
+ "icon": "status_failed",
+ "text": "failed",
+ "label": "failed",
+ "group": "failed",
+ "tooltip": "failed - (script failure)",
+ "has_details": true,
+ "details_path": "/gitlab-com/gitlab-docs/-/jobs/114984694",
+ "illustration": {
+ "image": "https://assets.gitlab-static.net/assets/illustrations/skipped-job_empty-8b877955fbf175e42ae65b6cb95346e15282c6fc5b682756c329af3a0055225e.svg",
+ "size": "svg-430",
+ "title": "This job does not have a trace."
+ },
+ "favicon": "https://gitlab.com/assets/ci_favicons/favicon_status_failed-41304d7f7e3828808b0c26771f0309e55296819a9beea3ea9fbf6689d9857c12.png",
+ "action": {
+ "icon": "retry",
+ "title": "Retry",
+ "path": "/gitlab-com/gitlab-docs/-/jobs/114984694/retry",
+ "method": "post",
+ "button_title": "Retry this job"
+ }
+ },
+ "recoverable": false
+ }
+ ]
+ }
+ ],
+ "status": {
+ "icon": "status_failed",
+ "text": "failed",
+ "label": "failed",
+ "group": "failed",
+ "tooltip": "failed",
+ "has_details": true,
+ "details_path": "/gitlab-com/gitlab-docs/pipelines/34993051#build",
+ "illustration": null,
+ "favicon": "https://gitlab.com/assets/ci_favicons/favicon_status_failed-41304d7f7e3828808b0c26771f0309e55296819a9beea3ea9fbf6689d9857c12.png"
+ },
+ "path": "/gitlab-com/gitlab-docs/pipelines/34993051#build",
+ "dropdown_path": "/gitlab-com/gitlab-docs/pipelines/34993051/stage.json?stage=build"
+ },
+ {
+ "name": "deploy",
+ "title": "deploy: skipped",
+ "groups": [
+ {
+ "name": "review",
+ "size": 1,
+ "status": {
+ "icon": "status_skipped",
+ "text": "skipped",
+ "label": "skipped",
+ "group": "skipped",
+ "tooltip": "skipped",
+ "has_details": true,
+ "details_path": "/gitlab-com/gitlab-docs/-/jobs/114982857",
+ "illustration": {
+ "image": "https://assets.gitlab-static.net/assets/illustrations/skipped-job_empty-8b877955fbf175e42ae65b6cb95346e15282c6fc5b682756c329af3a0055225e.svg",
+ "size": "svg-430",
+ "title": "This job has been skipped"
+ },
+ "favicon": "https://gitlab.com/assets/ci_favicons/favicon_status_skipped-0b9c5e543588945e8c4ca57786bbf2d0c56631959c9f853300392d0315be829b.png"
+ },
+ "jobs": [
+ {
+ "id": 1143232982857,
+ "name": "review",
+ "started": null,
+ "archived": false,
+ "build_path": "/gitlab-com/gitlab-docs/-/jobs/114982857",
+ "playable": false,
+ "scheduled": false,
+ "created_at": "2018-10-31T16:35:23.805Z",
+ "updated_at": "2018-10-31T16:41:40.569Z",
+ "status": {
+ "icon": "status_skipped",
+ "text": "skipped",
+ "label": "skipped",
+ "group": "skipped",
+ "tooltip": "skipped",
+ "has_details": true,
+ "details_path": "/gitlab-com/gitlab-docs/-/jobs/114982857",
+ "illustration": {
+ "image": "https://assets.gitlab-static.net/assets/illustrations/skipped-job_empty-8b877955fbf175e42ae65b6cb95346e15282c6fc5b682756c329af3a0055225e.svg",
+ "size": "svg-430",
+ "title": "This job has been skipped"
+ },
+ "favicon": "https://gitlab.com/assets/ci_favicons/favicon_status_skipped-0b9c5e543588945e8c4ca57786bbf2d0c56631959c9f853300392d0315be829b.png"
+ }
+ }
+ ]
+ },
+ {
+ "name": "review_stop",
+ "size": 1,
+ "status": {
+ "icon": "status_skipped",
+ "text": "skipped",
+ "label": "skipped",
+ "group": "skipped",
+ "tooltip": "skipped",
+ "has_details": true,
+ "details_path": "/gitlab-com/gitlab-docs/-/jobs/114982858",
+ "illustration": {
+ "image": "https://assets.gitlab-static.net/assets/illustrations/skipped-job_empty-8b877955fbf175e42ae65b6cb95346e15282c6fc5b682756c329af3a0055225e.svg",
+ "size": "svg-430",
+ "title": "This job has been skipped"
+ },
+ "favicon": "https://gitlab.com/assets/ci_favicons/favicon_status_skipped-0b9c5e543588945e8c4ca57786bbf2d0c56631959c9f853300392d0315be829b.png"
+ },
+ "jobs": [
+ {
+ "id": 114921313182858,
+ "name": "review_stop",
+ "started": null,
+ "archived": false,
+ "build_path": "/gitlab-com/gitlab-docs/-/jobs/114982858",
+ "playable": false,
+ "scheduled": false,
+ "created_at": "2018-10-31T16:35:23.840Z",
+ "updated_at": "2018-10-31T16:41:40.480Z",
+ "status": {
+ "icon": "status_skipped",
+ "text": "skipped",
+ "label": "skipped",
+ "group": "skipped",
+ "tooltip": "skipped",
+ "has_details": true,
+ "details_path": "/gitlab-com/gitlab-docs/-/jobs/114982858",
+ "illustration": {
+ "image": "https://assets.gitlab-static.net/assets/illustrations/skipped-job_empty-8b877955fbf175e42ae65b6cb95346e15282c6fc5b682756c329af3a0055225e.svg",
+ "size": "svg-430",
+ "title": "This job has been skipped"
+ },
+ "favicon": "https://gitlab.com/assets/ci_favicons/favicon_status_skipped-0b9c5e543588945e8c4ca57786bbf2d0c56631959c9f853300392d0315be829b.png"
+ }
+ }
+ ]
+ }
+ ],
+ "status": {
+ "icon": "status_skipped",
+ "text": "skipped",
+ "label": "skipped",
+ "group": "skipped",
+ "tooltip": "skipped",
+ "has_details": true,
+ "details_path": "/gitlab-com/gitlab-docs/pipelines/34993051#deploy",
+ "illustration": null,
+ "favicon": "https://gitlab.com/assets/ci_favicons/favicon_status_skipped-0b9c5e543588945e8c4ca57786bbf2d0c56631959c9f853300392d0315be829b.png"
+ },
+ "path": "/gitlab-com/gitlab-docs/pipelines/34993051#deploy",
+ "dropdown_path": "/gitlab-com/gitlab-docs/pipelines/34993051/stage.json?stage=deploy"
+ }
+ ],
+ "artifacts": [],
+ "manual_actions": [
+ {
+ "name": "image:bootstrap",
+ "path": "/gitlab-com/gitlab-docs/-/jobs/114982853/play",
+ "playable": true,
+ "scheduled": false
+ },
+ {
+ "name": "image:builder-onbuild",
+ "path": "/gitlab-com/gitlab-docs/-/jobs/114982854/play",
+ "playable": true,
+ "scheduled": false
+ },
+ {
+ "name": "image:nginx-onbuild",
+ "path": "/gitlab-com/gitlab-docs/-/jobs/114982855/play",
+ "playable": true,
+ "scheduled": false
+ },
+ {
+ "name": "review_stop",
+ "path": "/gitlab-com/gitlab-docs/-/jobs/114982858/play",
+ "playable": false,
+ "scheduled": false
+ }
+ ],
+ "scheduled_actions": []
+ },
+ "project": {
+ "id": 1794617,
+ "name": "GitLab Docs",
+ "full_path": "/gitlab-com/gitlab-docs",
+ "full_name": "GitLab.com / GitLab Docs"
+ },
+ "triggered": [
+ {
+ "id": 26,
+ "user": null,
+ "active": false,
+ "coverage": null,
+ "source": "push",
+ "created_at": "2019-01-06T17:48:37.599Z",
+ "updated_at": "2019-01-06T17:48:38.371Z",
+ "path": "/h5bp/html5-boilerplate/pipelines/26",
+ "flags": {
+ "latest": true,
+ "stuck": false,
+ "auto_devops": false,
+ "merge_request": false,
+ "yaml_errors": false,
+ "retryable": true,
+ "cancelable": false,
+ "failure_reason": false
+ },
+ "details": {
+ "status": {
+ "icon": "status_warning",
+ "text": "passed",
+ "label": "passed with warnings",
+ "group": "success-with-warnings",
+ "tooltip": "passed",
+ "has_details": true,
+ "details_path": "/h5bp/html5-boilerplate/pipelines/26",
+ "illustration": null,
+ "favicon": "/assets/ci_favicons/favicon_status_success-8451333011eee8ce9f2ab25dc487fe24a8758c694827a582f17f42b0a90446a2.png"
+ },
+ "duration": null,
+ "finished_at": "2019-01-06T17:48:38.370Z",
+ "stages": [
+ {
+ "name": "build",
+ "title": "build: passed",
+ "groups": [
+ {
+ "name": "build:linux",
+ "size": 1,
+ "status": {
+ "icon": "status_success",
+ "text": "passed",
+ "label": "passed",
+ "group": "success",
+ "tooltip": "passed",
+ "has_details": true,
+ "details_path": "/h5bp/html5-boilerplate/-/jobs/526",
+ "illustration": {
+ "image": "/assets/illustrations/skipped-job_empty-8b877955fbf175e42ae65b6cb95346e15282c6fc5b682756c329af3a0055225e.svg",
+ "size": "svg-430",
+ "title": "This job does not have a trace."
+ },
+ "favicon": "/assets/ci_favicons/favicon_status_success-8451333011eee8ce9f2ab25dc487fe24a8758c694827a582f17f42b0a90446a2.png",
+ "action": {
+ "icon": "retry",
+ "title": "Retry",
+ "path": "/h5bp/html5-boilerplate/-/jobs/526/retry",
+ "method": "post",
+ "button_title": "Retry this job"
+ }
+ },
+ "jobs": [
+ {
+ "id": 526,
+ "name": "build:linux",
+ "started": "2019-01-06T08:48:20.236Z",
+ "archived": false,
+ "build_path": "/h5bp/html5-boilerplate/-/jobs/526",
+ "retry_path": "/h5bp/html5-boilerplate/-/jobs/526/retry",
+ "playable": false,
+ "scheduled": false,
+ "created_at": "2019-01-06T17:48:37.806Z",
+ "updated_at": "2019-01-06T17:48:37.806Z",
+ "status": {
+ "icon": "status_success",
+ "text": "passed",
+ "label": "passed",
+ "group": "success",
+ "tooltip": "passed",
+ "has_details": true,
+ "details_path": "/h5bp/html5-boilerplate/-/jobs/526",
+ "illustration": {
+ "image": "/assets/illustrations/skipped-job_empty-8b877955fbf175e42ae65b6cb95346e15282c6fc5b682756c329af3a0055225e.svg",
+ "size": "svg-430",
+ "title": "This job does not have a trace."
+ },
+ "favicon": "/assets/ci_favicons/favicon_status_success-8451333011eee8ce9f2ab25dc487fe24a8758c694827a582f17f42b0a90446a2.png",
+ "action": {
+ "icon": "retry",
+ "title": "Retry",
+ "path": "/h5bp/html5-boilerplate/-/jobs/526/retry",
+ "method": "post",
+ "button_title": "Retry this job"
+ }
+ }
+ }
+ ]
+ },
+ {
+ "name": "build:osx",
+ "size": 1,
+ "status": {
+ "icon": "status_success",
+ "text": "passed",
+ "label": "passed",
+ "group": "success",
+ "tooltip": "passed",
+ "has_details": true,
+ "details_path": "/h5bp/html5-boilerplate/-/jobs/527",
+ "illustration": {
+ "image": "/assets/illustrations/skipped-job_empty-8b877955fbf175e42ae65b6cb95346e15282c6fc5b682756c329af3a0055225e.svg",
+ "size": "svg-430",
+ "title": "This job does not have a trace."
+ },
+ "favicon": "/assets/ci_favicons/favicon_status_success-8451333011eee8ce9f2ab25dc487fe24a8758c694827a582f17f42b0a90446a2.png",
+ "action": {
+ "icon": "retry",
+ "title": "Retry",
+ "path": "/h5bp/html5-boilerplate/-/jobs/527/retry",
+ "method": "post",
+ "button_title": "Retry this job"
+ }
+ },
+ "jobs": [
+ {
+ "id": 527,
+ "name": "build:osx",
+ "started": "2019-01-06T07:48:20.237Z",
+ "archived": false,
+ "build_path": "/h5bp/html5-boilerplate/-/jobs/527",
+ "retry_path": "/h5bp/html5-boilerplate/-/jobs/527/retry",
+ "playable": false,
+ "scheduled": false,
+ "created_at": "2019-01-06T17:48:37.846Z",
+ "updated_at": "2019-01-06T17:48:37.846Z",
+ "status": {
+ "icon": "status_success",
+ "text": "passed",
+ "label": "passed",
+ "group": "success",
+ "tooltip": "passed",
+ "has_details": true,
+ "details_path": "/h5bp/html5-boilerplate/-/jobs/527",
+ "illustration": {
+ "image": "/assets/illustrations/skipped-job_empty-8b877955fbf175e42ae65b6cb95346e15282c6fc5b682756c329af3a0055225e.svg",
+ "size": "svg-430",
+ "title": "This job does not have a trace."
+ },
+ "favicon": "/assets/ci_favicons/favicon_status_success-8451333011eee8ce9f2ab25dc487fe24a8758c694827a582f17f42b0a90446a2.png",
+ "action": {
+ "icon": "retry",
+ "title": "Retry",
+ "path": "/h5bp/html5-boilerplate/-/jobs/527/retry",
+ "method": "post",
+ "button_title": "Retry this job"
+ }
+ }
+ }
+ ]
+ }
+ ],
+ "status": {
+ "icon": "status_success",
+ "text": "passed",
+ "label": "passed",
+ "group": "success",
+ "tooltip": "passed",
+ "has_details": true,
+ "details_path": "/h5bp/html5-boilerplate/pipelines/26#build",
+ "illustration": null,
+ "favicon": "/assets/ci_favicons/favicon_status_success-8451333011eee8ce9f2ab25dc487fe24a8758c694827a582f17f42b0a90446a2.png"
+ },
+ "path": "/h5bp/html5-boilerplate/pipelines/26#build",
+ "dropdown_path": "/h5bp/html5-boilerplate/pipelines/26/stage.json?stage=build"
+ },
+ {
+ "name": "test",
+ "title": "test: passed with warnings",
+ "groups": [
+ {
+ "name": "jenkins",
+ "size": 1,
+ "status": {
+ "icon": "status_success",
+ "text": "passed",
+ "label": null,
+ "group": "success",
+ "tooltip": null,
+ "has_details": false,
+ "details_path": null,
+ "illustration": null,
+ "favicon": "/assets/ci_favicons/favicon_status_success-8451333011eee8ce9f2ab25dc487fe24a8758c694827a582f17f42b0a90446a2.png"
+ },
+ "jobs": [
+ {
+ "id": 546,
+ "name": "jenkins",
+ "started": "2019-01-06T11:48:20.237Z",
+ "archived": false,
+ "build_path": "/h5bp/html5-boilerplate/-/jobs/546",
+ "playable": false,
+ "scheduled": false,
+ "created_at": "2019-01-06T17:48:38.359Z",
+ "updated_at": "2019-01-06T17:48:38.359Z",
+ "status": {
+ "icon": "status_success",
+ "text": "passed",
+ "label": null,
+ "group": "success",
+ "tooltip": null,
+ "has_details": false,
+ "details_path": null,
+ "illustration": null,
+ "favicon": "/assets/ci_favicons/favicon_status_success-8451333011eee8ce9f2ab25dc487fe24a8758c694827a582f17f42b0a90446a2.png"
+ }
+ }
+ ]
+ },
+ {
+ "name": "rspec:linux",
+ "size": 3,
+ "status": {
+ "icon": "status_success",
+ "text": "passed",
+ "label": "passed",
+ "group": "success",
+ "tooltip": "passed",
+ "has_details": false,
+ "details_path": null,
+ "illustration": null,
+ "favicon": "/assets/ci_favicons/favicon_status_success-8451333011eee8ce9f2ab25dc487fe24a8758c694827a582f17f42b0a90446a2.png"
+ },
+ "jobs": [
+ {
+ "id": 528,
+ "name": "rspec:linux 0 3",
+ "started": "2019-01-06T09:48:20.237Z",
+ "archived": false,
+ "build_path": "/h5bp/html5-boilerplate/-/jobs/528",
+ "retry_path": "/h5bp/html5-boilerplate/-/jobs/528/retry",
+ "playable": false,
+ "scheduled": false,
+ "created_at": "2019-01-06T17:48:37.885Z",
+ "updated_at": "2019-01-06T17:48:37.885Z",
+ "status": {
+ "icon": "status_success",
+ "text": "passed",
+ "label": "passed",
+ "group": "success",
+ "tooltip": "passed",
+ "has_details": true,
+ "details_path": "/h5bp/html5-boilerplate/-/jobs/528",
+ "illustration": {
+ "image": "/assets/illustrations/skipped-job_empty-8b877955fbf175e42ae65b6cb95346e15282c6fc5b682756c329af3a0055225e.svg",
+ "size": "svg-430",
+ "title": "This job does not have a trace."
+ },
+ "favicon": "/assets/ci_favicons/favicon_status_success-8451333011eee8ce9f2ab25dc487fe24a8758c694827a582f17f42b0a90446a2.png",
+ "action": {
+ "icon": "retry",
+ "title": "Retry",
+ "path": "/h5bp/html5-boilerplate/-/jobs/528/retry",
+ "method": "post",
+ "button_title": "Retry this job"
+ }
+ }
+ },
+ {
+ "id": 529,
+ "name": "rspec:linux 1 3",
+ "started": "2019-01-06T09:48:20.237Z",
+ "archived": false,
+ "build_path": "/h5bp/html5-boilerplate/-/jobs/529",
+ "retry_path": "/h5bp/html5-boilerplate/-/jobs/529/retry",
+ "playable": false,
+ "scheduled": false,
+ "created_at": "2019-01-06T17:48:37.907Z",
+ "updated_at": "2019-01-06T17:48:37.907Z",
+ "status": {
+ "icon": "status_success",
+ "text": "passed",
+ "label": "passed",
+ "group": "success",
+ "tooltip": "passed",
+ "has_details": true,
+ "details_path": "/h5bp/html5-boilerplate/-/jobs/529",
+ "illustration": {
+ "image": "/assets/illustrations/skipped-job_empty-8b877955fbf175e42ae65b6cb95346e15282c6fc5b682756c329af3a0055225e.svg",
+ "size": "svg-430",
+ "title": "This job does not have a trace."
+ },
+ "favicon": "/assets/ci_favicons/favicon_status_success-8451333011eee8ce9f2ab25dc487fe24a8758c694827a582f17f42b0a90446a2.png",
+ "action": {
+ "icon": "retry",
+ "title": "Retry",
+ "path": "/h5bp/html5-boilerplate/-/jobs/529/retry",
+ "method": "post",
+ "button_title": "Retry this job"
+ }
+ }
+ },
+ {
+ "id": 530,
+ "name": "rspec:linux 2 3",
+ "started": "2019-01-06T09:48:20.237Z",
+ "archived": false,
+ "build_path": "/h5bp/html5-boilerplate/-/jobs/530",
+ "retry_path": "/h5bp/html5-boilerplate/-/jobs/530/retry",
+ "playable": false,
+ "scheduled": false,
+ "created_at": "2019-01-06T17:48:37.927Z",
+ "updated_at": "2019-01-06T17:48:37.927Z",
+ "status": {
+ "icon": "status_success",
+ "text": "passed",
+ "label": "passed",
+ "group": "success",
+ "tooltip": "passed",
+ "has_details": true,
+ "details_path": "/h5bp/html5-boilerplate/-/jobs/530",
+ "illustration": {
+ "image": "/assets/illustrations/skipped-job_empty-8b877955fbf175e42ae65b6cb95346e15282c6fc5b682756c329af3a0055225e.svg",
+ "size": "svg-430",
+ "title": "This job does not have a trace."
+ },
+ "favicon": "/assets/ci_favicons/favicon_status_success-8451333011eee8ce9f2ab25dc487fe24a8758c694827a582f17f42b0a90446a2.png",
+ "action": {
+ "icon": "retry",
+ "title": "Retry",
+ "path": "/h5bp/html5-boilerplate/-/jobs/530/retry",
+ "method": "post",
+ "button_title": "Retry this job"
+ }
+ }
+ }
+ ]
+ },
+ {
+ "name": "rspec:osx",
+ "size": 1,
+ "status": {
+ "icon": "status_success",
+ "text": "passed",
+ "label": "passed",
+ "group": "success",
+ "tooltip": "passed",
+ "has_details": true,
+ "details_path": "/h5bp/html5-boilerplate/-/jobs/535",
+ "illustration": {
+ "image": "/assets/illustrations/skipped-job_empty-8b877955fbf175e42ae65b6cb95346e15282c6fc5b682756c329af3a0055225e.svg",
+ "size": "svg-430",
+ "title": "This job does not have a trace."
+ },
+ "favicon": "/assets/ci_favicons/favicon_status_success-8451333011eee8ce9f2ab25dc487fe24a8758c694827a582f17f42b0a90446a2.png",
+ "action": {
+ "icon": "retry",
+ "title": "Retry",
+ "path": "/h5bp/html5-boilerplate/-/jobs/535/retry",
+ "method": "post",
+ "button_title": "Retry this job"
+ }
+ },
+ "jobs": [
+ {
+ "id": 535,
+ "name": "rspec:osx",
+ "started": "2019-01-06T09:48:20.237Z",
+ "archived": false,
+ "build_path": "/h5bp/html5-boilerplate/-/jobs/535",
+ "retry_path": "/h5bp/html5-boilerplate/-/jobs/535/retry",
+ "playable": false,
+ "scheduled": false,
+ "created_at": "2019-01-06T17:48:38.018Z",
+ "updated_at": "2019-01-06T17:48:38.018Z",
+ "status": {
+ "icon": "status_success",
+ "text": "passed",
+ "label": "passed",
+ "group": "success",
+ "tooltip": "passed",
+ "has_details": true,
+ "details_path": "/h5bp/html5-boilerplate/-/jobs/535",
+ "illustration": {
+ "image": "/assets/illustrations/skipped-job_empty-8b877955fbf175e42ae65b6cb95346e15282c6fc5b682756c329af3a0055225e.svg",
+ "size": "svg-430",
+ "title": "This job does not have a trace."
+ },
+ "favicon": "/assets/ci_favicons/favicon_status_success-8451333011eee8ce9f2ab25dc487fe24a8758c694827a582f17f42b0a90446a2.png",
+ "action": {
+ "icon": "retry",
+ "title": "Retry",
+ "path": "/h5bp/html5-boilerplate/-/jobs/535/retry",
+ "method": "post",
+ "button_title": "Retry this job"
+ }
+ }
+ }
+ ]
+ },
+ {
+ "name": "rspec:windows",
+ "size": 3,
+ "status": {
+ "icon": "status_success",
+ "text": "passed",
+ "label": "passed",
+ "group": "success",
+ "tooltip": "passed",
+ "has_details": false,
+ "details_path": null,
+ "illustration": null,
+ "favicon": "/assets/ci_favicons/favicon_status_success-8451333011eee8ce9f2ab25dc487fe24a8758c694827a582f17f42b0a90446a2.png"
+ },
+ "jobs": [
+ {
+ "id": 531,
+ "name": "rspec:windows 0 3",
+ "started": "2019-01-06T09:48:20.237Z",
+ "archived": false,
+ "build_path": "/h5bp/html5-boilerplate/-/jobs/531",
+ "retry_path": "/h5bp/html5-boilerplate/-/jobs/531/retry",
+ "playable": false,
+ "scheduled": false,
+ "created_at": "2019-01-06T17:48:37.944Z",
+ "updated_at": "2019-01-06T17:48:37.944Z",
+ "status": {
+ "icon": "status_success",
+ "text": "passed",
+ "label": "passed",
+ "group": "success",
+ "tooltip": "passed",
+ "has_details": true,
+ "details_path": "/h5bp/html5-boilerplate/-/jobs/531",
+ "illustration": {
+ "image": "/assets/illustrations/skipped-job_empty-8b877955fbf175e42ae65b6cb95346e15282c6fc5b682756c329af3a0055225e.svg",
+ "size": "svg-430",
+ "title": "This job does not have a trace."
+ },
+ "favicon": "/assets/ci_favicons/favicon_status_success-8451333011eee8ce9f2ab25dc487fe24a8758c694827a582f17f42b0a90446a2.png",
+ "action": {
+ "icon": "retry",
+ "title": "Retry",
+ "path": "/h5bp/html5-boilerplate/-/jobs/531/retry",
+ "method": "post",
+ "button_title": "Retry this job"
+ }
+ }
+ },
+ {
+ "id": 532,
+ "name": "rspec:windows 1 3",
+ "started": "2019-01-06T09:48:20.237Z",
+ "archived": false,
+ "build_path": "/h5bp/html5-boilerplate/-/jobs/532",
+ "retry_path": "/h5bp/html5-boilerplate/-/jobs/532/retry",
+ "playable": false,
+ "scheduled": false,
+ "created_at": "2019-01-06T17:48:37.962Z",
+ "updated_at": "2019-01-06T17:48:37.962Z",
+ "status": {
+ "icon": "status_success",
+ "text": "passed",
+ "label": "passed",
+ "group": "success",
+ "tooltip": "passed",
+ "has_details": true,
+ "details_path": "/h5bp/html5-boilerplate/-/jobs/532",
+ "illustration": {
+ "image": "/assets/illustrations/skipped-job_empty-8b877955fbf175e42ae65b6cb95346e15282c6fc5b682756c329af3a0055225e.svg",
+ "size": "svg-430",
+ "title": "This job does not have a trace."
+ },
+ "favicon": "/assets/ci_favicons/favicon_status_success-8451333011eee8ce9f2ab25dc487fe24a8758c694827a582f17f42b0a90446a2.png",
+ "action": {
+ "icon": "retry",
+ "title": "Retry",
+ "path": "/h5bp/html5-boilerplate/-/jobs/532/retry",
+ "method": "post",
+ "button_title": "Retry this job"
+ }
+ }
+ },
+ {
+ "id": 534,
+ "name": "rspec:windows 2 3",
+ "started": "2019-01-06T09:48:20.237Z",
+ "archived": false,
+ "build_path": "/h5bp/html5-boilerplate/-/jobs/534",
+ "retry_path": "/h5bp/html5-boilerplate/-/jobs/534/retry",
+ "playable": false,
+ "scheduled": false,
+ "created_at": "2019-01-06T17:48:37.999Z",
+ "updated_at": "2019-01-06T17:48:37.999Z",
+ "status": {
+ "icon": "status_success",
+ "text": "passed",
+ "label": "passed",
+ "group": "success",
+ "tooltip": "passed",
+ "has_details": true,
+ "details_path": "/h5bp/html5-boilerplate/-/jobs/534",
+ "illustration": {
+ "image": "/assets/illustrations/skipped-job_empty-8b877955fbf175e42ae65b6cb95346e15282c6fc5b682756c329af3a0055225e.svg",
+ "size": "svg-430",
+ "title": "This job does not have a trace."
+ },
+ "favicon": "/assets/ci_favicons/favicon_status_success-8451333011eee8ce9f2ab25dc487fe24a8758c694827a582f17f42b0a90446a2.png",
+ "action": {
+ "icon": "retry",
+ "title": "Retry",
+ "path": "/h5bp/html5-boilerplate/-/jobs/534/retry",
+ "method": "post",
+ "button_title": "Retry this job"
+ }
+ }
+ }
+ ]
+ },
+ {
+ "name": "spinach:linux",
+ "size": 1,
+ "status": {
+ "icon": "status_success",
+ "text": "passed",
+ "label": "passed",
+ "group": "success",
+ "tooltip": "passed",
+ "has_details": true,
+ "details_path": "/h5bp/html5-boilerplate/-/jobs/536",
+ "illustration": {
+ "image": "/assets/illustrations/skipped-job_empty-8b877955fbf175e42ae65b6cb95346e15282c6fc5b682756c329af3a0055225e.svg",
+ "size": "svg-430",
+ "title": "This job does not have a trace."
+ },
+ "favicon": "/assets/ci_favicons/favicon_status_success-8451333011eee8ce9f2ab25dc487fe24a8758c694827a582f17f42b0a90446a2.png",
+ "action": {
+ "icon": "retry",
+ "title": "Retry",
+ "path": "/h5bp/html5-boilerplate/-/jobs/536/retry",
+ "method": "post",
+ "button_title": "Retry this job"
+ }
+ },
+ "jobs": [
+ {
+ "id": 536,
+ "name": "spinach:linux",
+ "started": "2019-01-06T09:48:20.237Z",
+ "archived": false,
+ "build_path": "/h5bp/html5-boilerplate/-/jobs/536",
+ "retry_path": "/h5bp/html5-boilerplate/-/jobs/536/retry",
+ "playable": false,
+ "scheduled": false,
+ "created_at": "2019-01-06T17:48:38.050Z",
+ "updated_at": "2019-01-06T17:48:38.050Z",
+ "status": {
+ "icon": "status_success",
+ "text": "passed",
+ "label": "passed",
+ "group": "success",
+ "tooltip": "passed",
+ "has_details": true,
+ "details_path": "/h5bp/html5-boilerplate/-/jobs/536",
+ "illustration": {
+ "image": "/assets/illustrations/skipped-job_empty-8b877955fbf175e42ae65b6cb95346e15282c6fc5b682756c329af3a0055225e.svg",
+ "size": "svg-430",
+ "title": "This job does not have a trace."
+ },
+ "favicon": "/assets/ci_favicons/favicon_status_success-8451333011eee8ce9f2ab25dc487fe24a8758c694827a582f17f42b0a90446a2.png",
+ "action": {
+ "icon": "retry",
+ "title": "Retry",
+ "path": "/h5bp/html5-boilerplate/-/jobs/536/retry",
+ "method": "post",
+ "button_title": "Retry this job"
+ }
+ }
+ }
+ ]
+ },
+ {
+ "name": "spinach:osx",
+ "size": 1,
+ "status": {
+ "icon": "status_warning",
+ "text": "failed",
+ "label": "failed (allowed to fail)",
+ "group": "failed-with-warnings",
+ "tooltip": "failed - (unknown failure) (allowed to fail)",
+ "has_details": true,
+ "details_path": "/h5bp/html5-boilerplate/-/jobs/537",
+ "illustration": {
+ "image": "/assets/illustrations/skipped-job_empty-8b877955fbf175e42ae65b6cb95346e15282c6fc5b682756c329af3a0055225e.svg",
+ "size": "svg-430",
+ "title": "This job does not have a trace."
+ },
+ "favicon": "/assets/ci_favicons/favicon_status_failed-41304d7f7e3828808b0c26771f0309e55296819a9beea3ea9fbf6689d9857c12.png",
+ "action": {
+ "icon": "retry",
+ "title": "Retry",
+ "path": "/h5bp/html5-boilerplate/-/jobs/537/retry",
+ "method": "post",
+ "button_title": "Retry this job"
+ }
+ },
+ "jobs": [
+ {
+ "id": 537,
+ "name": "spinach:osx",
+ "started": "2019-01-06T09:48:20.237Z",
+ "archived": false,
+ "build_path": "/h5bp/html5-boilerplate/-/jobs/537",
+ "retry_path": "/h5bp/html5-boilerplate/-/jobs/537/retry",
+ "playable": false,
+ "scheduled": false,
+ "created_at": "2019-01-06T17:48:38.069Z",
+ "updated_at": "2019-01-06T17:48:38.069Z",
+ "status": {
+ "icon": "status_warning",
+ "text": "failed",
+ "label": "failed (allowed to fail)",
+ "group": "failed-with-warnings",
+ "tooltip": "failed - (unknown failure) (allowed to fail)",
+ "has_details": true,
+ "details_path": "/h5bp/html5-boilerplate/-/jobs/537",
+ "illustration": {
+ "image": "/assets/illustrations/skipped-job_empty-8b877955fbf175e42ae65b6cb95346e15282c6fc5b682756c329af3a0055225e.svg",
+ "size": "svg-430",
+ "title": "This job does not have a trace."
+ },
+ "favicon": "/assets/ci_favicons/favicon_status_failed-41304d7f7e3828808b0c26771f0309e55296819a9beea3ea9fbf6689d9857c12.png",
+ "action": {
+ "icon": "retry",
+ "title": "Retry",
+ "path": "/h5bp/html5-boilerplate/-/jobs/537/retry",
+ "method": "post",
+ "button_title": "Retry this job"
+ }
+ },
+ "callout_message": "There is an unknown failure, please try again",
+ "recoverable": true
+ }
+ ]
+ }
+ ],
+ "status": {
+ "icon": "status_warning",
+ "text": "passed",
+ "label": "passed with warnings",
+ "group": "success-with-warnings",
+ "tooltip": "passed",
+ "has_details": true,
+ "details_path": "/h5bp/html5-boilerplate/pipelines/26#test",
+ "illustration": null,
+ "favicon": "/assets/ci_favicons/favicon_status_success-8451333011eee8ce9f2ab25dc487fe24a8758c694827a582f17f42b0a90446a2.png"
+ },
+ "path": "/h5bp/html5-boilerplate/pipelines/26#test",
+ "dropdown_path": "/h5bp/html5-boilerplate/pipelines/26/stage.json?stage=test"
+ },
+ {
+ "name": "security",
+ "title": "security: passed",
+ "groups": [
+ {
+ "name": "container_scanning",
+ "size": 1,
+ "status": {
+ "icon": "status_success",
+ "text": "passed",
+ "label": "passed",
+ "group": "success",
+ "tooltip": "passed",
+ "has_details": true,
+ "details_path": "/h5bp/html5-boilerplate/-/jobs/541",
+ "illustration": {
+ "image": "/assets/illustrations/skipped-job_empty-8b877955fbf175e42ae65b6cb95346e15282c6fc5b682756c329af3a0055225e.svg",
+ "size": "svg-430",
+ "title": "This job does not have a trace."
+ },
+ "favicon": "/assets/ci_favicons/favicon_status_success-8451333011eee8ce9f2ab25dc487fe24a8758c694827a582f17f42b0a90446a2.png",
+ "action": {
+ "icon": "retry",
+ "title": "Retry",
+ "path": "/h5bp/html5-boilerplate/-/jobs/541/retry",
+ "method": "post",
+ "button_title": "Retry this job"
+ }
+ },
+ "jobs": [
+ {
+ "id": 541,
+ "name": "container_scanning",
+ "started": "2019-01-06T09:48:20.237Z",
+ "archived": false,
+ "build_path": "/h5bp/html5-boilerplate/-/jobs/541",
+ "retry_path": "/h5bp/html5-boilerplate/-/jobs/541/retry",
+ "playable": false,
+ "scheduled": false,
+ "created_at": "2019-01-06T17:48:38.186Z",
+ "updated_at": "2019-01-06T17:48:38.186Z",
+ "status": {
+ "icon": "status_success",
+ "text": "passed",
+ "label": "passed",
+ "group": "success",
+ "tooltip": "passed",
+ "has_details": true,
+ "details_path": "/h5bp/html5-boilerplate/-/jobs/541",
+ "illustration": {
+ "image": "/assets/illustrations/skipped-job_empty-8b877955fbf175e42ae65b6cb95346e15282c6fc5b682756c329af3a0055225e.svg",
+ "size": "svg-430",
+ "title": "This job does not have a trace."
+ },
+ "favicon": "/assets/ci_favicons/favicon_status_success-8451333011eee8ce9f2ab25dc487fe24a8758c694827a582f17f42b0a90446a2.png",
+ "action": {
+ "icon": "retry",
+ "title": "Retry",
+ "path": "/h5bp/html5-boilerplate/-/jobs/541/retry",
+ "method": "post",
+ "button_title": "Retry this job"
+ }
+ }
+ }
+ ]
+ },
+ {
+ "name": "dast",
+ "size": 1,
+ "status": {
+ "icon": "status_success",
+ "text": "passed",
+ "label": "passed",
+ "group": "success",
+ "tooltip": "passed",
+ "has_details": true,
+ "details_path": "/h5bp/html5-boilerplate/-/jobs/538",
+ "illustration": {
+ "image": "/assets/illustrations/skipped-job_empty-8b877955fbf175e42ae65b6cb95346e15282c6fc5b682756c329af3a0055225e.svg",
+ "size": "svg-430",
+ "title": "This job does not have a trace."
+ },
+ "favicon": "/assets/ci_favicons/favicon_status_success-8451333011eee8ce9f2ab25dc487fe24a8758c694827a582f17f42b0a90446a2.png",
+ "action": {
+ "icon": "retry",
+ "title": "Retry",
+ "path": "/h5bp/html5-boilerplate/-/jobs/538/retry",
+ "method": "post",
+ "button_title": "Retry this job"
+ }
+ },
+ "jobs": [
+ {
+ "id": 538,
+ "name": "dast",
+ "started": "2019-01-06T09:48:20.237Z",
+ "archived": false,
+ "build_path": "/h5bp/html5-boilerplate/-/jobs/538",
+ "retry_path": "/h5bp/html5-boilerplate/-/jobs/538/retry",
+ "playable": false,
+ "scheduled": false,
+ "created_at": "2019-01-06T17:48:38.087Z",
+ "updated_at": "2019-01-06T17:48:38.087Z",
+ "status": {
+ "icon": "status_success",
+ "text": "passed",
+ "label": "passed",
+ "group": "success",
+ "tooltip": "passed",
+ "has_details": true,
+ "details_path": "/h5bp/html5-boilerplate/-/jobs/538",
+ "illustration": {
+ "image": "/assets/illustrations/skipped-job_empty-8b877955fbf175e42ae65b6cb95346e15282c6fc5b682756c329af3a0055225e.svg",
+ "size": "svg-430",
+ "title": "This job does not have a trace."
+ },
+ "favicon": "/assets/ci_favicons/favicon_status_success-8451333011eee8ce9f2ab25dc487fe24a8758c694827a582f17f42b0a90446a2.png",
+ "action": {
+ "icon": "retry",
+ "title": "Retry",
+ "path": "/h5bp/html5-boilerplate/-/jobs/538/retry",
+ "method": "post",
+ "button_title": "Retry this job"
+ }
+ }
+ }
+ ]
+ },
+ {
+ "name": "dependency_scanning",
+ "size": 1,
+ "status": {
+ "icon": "status_success",
+ "text": "passed",
+ "label": "passed",
+ "group": "success",
+ "tooltip": "passed",
+ "has_details": true,
+ "details_path": "/h5bp/html5-boilerplate/-/jobs/540",
+ "illustration": {
+ "image": "/assets/illustrations/skipped-job_empty-8b877955fbf175e42ae65b6cb95346e15282c6fc5b682756c329af3a0055225e.svg",
+ "size": "svg-430",
+ "title": "This job does not have a trace."
+ },
+ "favicon": "/assets/ci_favicons/favicon_status_success-8451333011eee8ce9f2ab25dc487fe24a8758c694827a582f17f42b0a90446a2.png",
+ "action": {
+ "icon": "retry",
+ "title": "Retry",
+ "path": "/h5bp/html5-boilerplate/-/jobs/540/retry",
+ "method": "post",
+ "button_title": "Retry this job"
+ }
+ },
+ "jobs": [
+ {
+ "id": 540,
+ "name": "dependency_scanning",
+ "started": "2019-01-06T09:48:20.237Z",
+ "archived": false,
+ "build_path": "/h5bp/html5-boilerplate/-/jobs/540",
+ "retry_path": "/h5bp/html5-boilerplate/-/jobs/540/retry",
+ "playable": false,
+ "scheduled": false,
+ "created_at": "2019-01-06T17:48:38.153Z",
+ "updated_at": "2019-01-06T17:48:38.153Z",
+ "status": {
+ "icon": "status_success",
+ "text": "passed",
+ "label": "passed",
+ "group": "success",
+ "tooltip": "passed",
+ "has_details": true,
+ "details_path": "/h5bp/html5-boilerplate/-/jobs/540",
+ "illustration": {
+ "image": "/assets/illustrations/skipped-job_empty-8b877955fbf175e42ae65b6cb95346e15282c6fc5b682756c329af3a0055225e.svg",
+ "size": "svg-430",
+ "title": "This job does not have a trace."
+ },
+ "favicon": "/assets/ci_favicons/favicon_status_success-8451333011eee8ce9f2ab25dc487fe24a8758c694827a582f17f42b0a90446a2.png",
+ "action": {
+ "icon": "retry",
+ "title": "Retry",
+ "path": "/h5bp/html5-boilerplate/-/jobs/540/retry",
+ "method": "post",
+ "button_title": "Retry this job"
+ }
+ }
+ }
+ ]
+ },
+ {
+ "name": "sast",
+ "size": 1,
+ "status": {
+ "icon": "status_success",
+ "text": "passed",
+ "label": "passed",
+ "group": "success",
+ "tooltip": "passed",
+ "has_details": true,
+ "details_path": "/h5bp/html5-boilerplate/-/jobs/539",
+ "illustration": {
+ "image": "/assets/illustrations/skipped-job_empty-8b877955fbf175e42ae65b6cb95346e15282c6fc5b682756c329af3a0055225e.svg",
+ "size": "svg-430",
+ "title": "This job does not have a trace."
+ },
+ "favicon": "/assets/ci_favicons/favicon_status_success-8451333011eee8ce9f2ab25dc487fe24a8758c694827a582f17f42b0a90446a2.png",
+ "action": {
+ "icon": "retry",
+ "title": "Retry",
+ "path": "/h5bp/html5-boilerplate/-/jobs/539/retry",
+ "method": "post",
+ "button_title": "Retry this job"
+ }
+ },
+ "jobs": [
+ {
+ "id": 539,
+ "name": "sast",
+ "started": "2019-01-06T09:48:20.237Z",
+ "archived": false,
+ "build_path": "/h5bp/html5-boilerplate/-/jobs/539",
+ "retry_path": "/h5bp/html5-boilerplate/-/jobs/539/retry",
+ "playable": false,
+ "scheduled": false,
+ "created_at": "2019-01-06T17:48:38.121Z",
+ "updated_at": "2019-01-06T17:48:38.121Z",
+ "status": {
+ "icon": "status_success",
+ "text": "passed",
+ "label": "passed",
+ "group": "success",
+ "tooltip": "passed",
+ "has_details": true,
+ "details_path": "/h5bp/html5-boilerplate/-/jobs/539",
+ "illustration": {
+ "image": "/assets/illustrations/skipped-job_empty-8b877955fbf175e42ae65b6cb95346e15282c6fc5b682756c329af3a0055225e.svg",
+ "size": "svg-430",
+ "title": "This job does not have a trace."
+ },
+ "favicon": "/assets/ci_favicons/favicon_status_success-8451333011eee8ce9f2ab25dc487fe24a8758c694827a582f17f42b0a90446a2.png",
+ "action": {
+ "icon": "retry",
+ "title": "Retry",
+ "path": "/h5bp/html5-boilerplate/-/jobs/539/retry",
+ "method": "post",
+ "button_title": "Retry this job"
+ }
+ }
+ }
+ ]
+ }
+ ],
+ "status": {
+ "icon": "status_success",
+ "text": "passed",
+ "label": "passed",
+ "group": "success",
+ "tooltip": "passed",
+ "has_details": true,
+ "details_path": "/h5bp/html5-boilerplate/pipelines/26#security",
+ "illustration": null,
+ "favicon": "/assets/ci_favicons/favicon_status_success-8451333011eee8ce9f2ab25dc487fe24a8758c694827a582f17f42b0a90446a2.png"
+ },
+ "path": "/h5bp/html5-boilerplate/pipelines/26#security",
+ "dropdown_path": "/h5bp/html5-boilerplate/pipelines/26/stage.json?stage=security"
+ },
+ {
+ "name": "deploy",
+ "title": "deploy: passed",
+ "groups": [
+ {
+ "name": "production",
+ "size": 1,
+ "status": {
+ "icon": "status_skipped",
+ "text": "skipped",
+ "label": "skipped",
+ "group": "skipped",
+ "tooltip": "skipped",
+ "has_details": true,
+ "details_path": "/h5bp/html5-boilerplate/-/jobs/544",
+ "illustration": {
+ "image": "/assets/illustrations/skipped-job_empty-8b877955fbf175e42ae65b6cb95346e15282c6fc5b682756c329af3a0055225e.svg",
+ "size": "svg-430",
+ "title": "This job has been skipped"
+ },
+ "favicon": "/assets/ci_favicons/favicon_status_skipped-0b9c5e543588945e8c4ca57786bbf2d0c56631959c9f853300392d0315be829b.png"
+ },
+ "jobs": [
+ {
+ "id": 544,
+ "name": "production",
+ "started": null,
+ "archived": false,
+ "build_path": "/h5bp/html5-boilerplate/-/jobs/544",
+ "playable": false,
+ "scheduled": false,
+ "created_at": "2019-01-06T17:48:38.313Z",
+ "updated_at": "2019-01-06T17:48:38.313Z",
+ "status": {
+ "icon": "status_skipped",
+ "text": "skipped",
+ "label": "skipped",
+ "group": "skipped",
+ "tooltip": "skipped",
+ "has_details": true,
+ "details_path": "/h5bp/html5-boilerplate/-/jobs/544",
+ "illustration": {
+ "image": "/assets/illustrations/skipped-job_empty-8b877955fbf175e42ae65b6cb95346e15282c6fc5b682756c329af3a0055225e.svg",
+ "size": "svg-430",
+ "title": "This job has been skipped"
+ },
+ "favicon": "/assets/ci_favicons/favicon_status_skipped-0b9c5e543588945e8c4ca57786bbf2d0c56631959c9f853300392d0315be829b.png"
+ }
+ }
+ ]
+ },
+ {
+ "name": "staging",
+ "size": 1,
+ "status": {
+ "icon": "status_success",
+ "text": "passed",
+ "label": "passed",
+ "group": "success",
+ "tooltip": "passed",
+ "has_details": true,
+ "details_path": "/h5bp/html5-boilerplate/-/jobs/542",
+ "illustration": {
+ "image": "/assets/illustrations/skipped-job_empty-8b877955fbf175e42ae65b6cb95346e15282c6fc5b682756c329af3a0055225e.svg",
+ "size": "svg-430",
+ "title": "This job does not have a trace."
+ },
+ "favicon": "/assets/ci_favicons/favicon_status_success-8451333011eee8ce9f2ab25dc487fe24a8758c694827a582f17f42b0a90446a2.png",
+ "action": {
+ "icon": "retry",
+ "title": "Retry",
+ "path": "/h5bp/html5-boilerplate/-/jobs/542/retry",
+ "method": "post",
+ "button_title": "Retry this job"
+ }
+ },
+ "jobs": [
+ {
+ "id": 542,
+ "name": "staging",
+ "started": "2019-01-06T11:48:20.237Z",
+ "archived": false,
+ "build_path": "/h5bp/html5-boilerplate/-/jobs/542",
+ "retry_path": "/h5bp/html5-boilerplate/-/jobs/542/retry",
+ "playable": false,
+ "scheduled": false,
+ "created_at": "2019-01-06T17:48:38.219Z",
+ "updated_at": "2019-01-06T17:48:38.219Z",
+ "status": {
+ "icon": "status_success",
+ "text": "passed",
+ "label": "passed",
+ "group": "success",
+ "tooltip": "passed",
+ "has_details": true,
+ "details_path": "/h5bp/html5-boilerplate/-/jobs/542",
+ "illustration": {
+ "image": "/assets/illustrations/skipped-job_empty-8b877955fbf175e42ae65b6cb95346e15282c6fc5b682756c329af3a0055225e.svg",
+ "size": "svg-430",
+ "title": "This job does not have a trace."
+ },
+ "favicon": "/assets/ci_favicons/favicon_status_success-8451333011eee8ce9f2ab25dc487fe24a8758c694827a582f17f42b0a90446a2.png",
+ "action": {
+ "icon": "retry",
+ "title": "Retry",
+ "path": "/h5bp/html5-boilerplate/-/jobs/542/retry",
+ "method": "post",
+ "button_title": "Retry this job"
+ }
+ }
+ }
+ ]
+ },
+ {
+ "name": "stop staging",
+ "size": 1,
+ "status": {
+ "icon": "status_skipped",
+ "text": "skipped",
+ "label": "skipped",
+ "group": "skipped",
+ "tooltip": "skipped",
+ "has_details": true,
+ "details_path": "/h5bp/html5-boilerplate/-/jobs/543",
+ "illustration": {
+ "image": "/assets/illustrations/skipped-job_empty-8b877955fbf175e42ae65b6cb95346e15282c6fc5b682756c329af3a0055225e.svg",
+ "size": "svg-430",
+ "title": "This job has been skipped"
+ },
+ "favicon": "/assets/ci_favicons/favicon_status_skipped-0b9c5e543588945e8c4ca57786bbf2d0c56631959c9f853300392d0315be829b.png"
+ },
+ "jobs": [
+ {
+ "id": 543,
+ "name": "stop staging",
+ "started": null,
+ "archived": false,
+ "build_path": "/h5bp/html5-boilerplate/-/jobs/543",
+ "playable": false,
+ "scheduled": false,
+ "created_at": "2019-01-06T17:48:38.283Z",
+ "updated_at": "2019-01-06T17:48:38.283Z",
+ "status": {
+ "icon": "status_skipped",
+ "text": "skipped",
+ "label": "skipped",
+ "group": "skipped",
+ "tooltip": "skipped",
+ "has_details": true,
+ "details_path": "/h5bp/html5-boilerplate/-/jobs/543",
+ "illustration": {
+ "image": "/assets/illustrations/skipped-job_empty-8b877955fbf175e42ae65b6cb95346e15282c6fc5b682756c329af3a0055225e.svg",
+ "size": "svg-430",
+ "title": "This job has been skipped"
+ },
+ "favicon": "/assets/ci_favicons/favicon_status_skipped-0b9c5e543588945e8c4ca57786bbf2d0c56631959c9f853300392d0315be829b.png"
+ }
+ }
+ ]
+ }
+ ],
+ "status": {
+ "icon": "status_success",
+ "text": "passed",
+ "label": "passed",
+ "group": "success",
+ "tooltip": "passed",
+ "has_details": true,
+ "details_path": "/h5bp/html5-boilerplate/pipelines/26#deploy",
+ "illustration": null,
+ "favicon": "/assets/ci_favicons/favicon_status_success-8451333011eee8ce9f2ab25dc487fe24a8758c694827a582f17f42b0a90446a2.png"
+ },
+ "path": "/h5bp/html5-boilerplate/pipelines/26#deploy",
+ "dropdown_path": "/h5bp/html5-boilerplate/pipelines/26/stage.json?stage=deploy"
+ },
+ {
+ "name": "notify",
+ "title": "notify: passed",
+ "groups": [
+ {
+ "name": "slack",
+ "size": 1,
+ "status": {
+ "icon": "status_success",
+ "text": "passed",
+ "label": "manual play action",
+ "group": "success",
+ "tooltip": "passed",
+ "has_details": true,
+ "details_path": "/h5bp/html5-boilerplate/-/jobs/545",
+ "illustration": {
+ "image": "/assets/illustrations/manual_action-2b4ca0d1bcfd92aebf33d484e36cbf7a102d007f76b5a0cfea636033a629d601.svg",
+ "size": "svg-394",
+ "title": "This job requires a manual action",
+ "content": "This job depends on a user to trigger its process. Often they are used to deploy code to production environments"
+ },
+ "favicon": "/assets/ci_favicons/favicon_status_success-8451333011eee8ce9f2ab25dc487fe24a8758c694827a582f17f42b0a90446a2.png",
+ "action": {
+ "icon": "play",
+ "title": "Play",
+ "path": "/h5bp/html5-boilerplate/-/jobs/545/play",
+ "method": "post",
+ "button_title": "Trigger this manual action"
+ }
+ },
+ "jobs": [
+ {
+ "id": 545,
+ "name": "slack",
+ "started": null,
+ "archived": false,
+ "build_path": "/h5bp/html5-boilerplate/-/jobs/545",
+ "retry_path": "/h5bp/html5-boilerplate/-/jobs/545/retry",
+ "play_path": "/h5bp/html5-boilerplate/-/jobs/545/play",
+ "playable": true,
+ "scheduled": false,
+ "created_at": "2019-01-06T17:48:38.341Z",
+ "updated_at": "2019-01-06T17:48:38.341Z",
+ "status": {
+ "icon": "status_success",
+ "text": "passed",
+ "label": "manual play action",
+ "group": "success",
+ "tooltip": "passed",
+ "has_details": true,
+ "details_path": "/h5bp/html5-boilerplate/-/jobs/545",
+ "illustration": {
+ "image": "/assets/illustrations/manual_action-2b4ca0d1bcfd92aebf33d484e36cbf7a102d007f76b5a0cfea636033a629d601.svg",
+ "size": "svg-394",
+ "title": "This job requires a manual action",
+ "content": "This job depends on a user to trigger its process. Often they are used to deploy code to production environments"
+ },
+ "favicon": "/assets/ci_favicons/favicon_status_success-8451333011eee8ce9f2ab25dc487fe24a8758c694827a582f17f42b0a90446a2.png",
+ "action": {
+ "icon": "play",
+ "title": "Play",
+ "path": "/h5bp/html5-boilerplate/-/jobs/545/play",
+ "method": "post",
+ "button_title": "Trigger this manual action"
+ }
+ }
+ }
+ ]
+ }
+ ],
+ "status": {
+ "icon": "status_success",
+ "text": "passed",
+ "label": "passed",
+ "group": "success",
+ "tooltip": "passed",
+ "has_details": true,
+ "details_path": "/h5bp/html5-boilerplate/pipelines/26#notify",
+ "illustration": null,
+ "favicon": "/assets/ci_favicons/favicon_status_success-8451333011eee8ce9f2ab25dc487fe24a8758c694827a582f17f42b0a90446a2.png"
+ },
+ "path": "/h5bp/html5-boilerplate/pipelines/26#notify",
+ "dropdown_path": "/h5bp/html5-boilerplate/pipelines/26/stage.json?stage=notify"
+ }
+ ],
+ "artifacts": [
+ {
+ "name": "build:linux",
+ "expired": null,
+ "expire_at": null,
+ "path": "/h5bp/html5-boilerplate/-/jobs/526/artifacts/download",
+ "browse_path": "/h5bp/html5-boilerplate/-/jobs/526/artifacts/browse"
+ },
+ {
+ "name": "build:osx",
+ "expired": null,
+ "expire_at": null,
+ "path": "/h5bp/html5-boilerplate/-/jobs/527/artifacts/download",
+ "browse_path": "/h5bp/html5-boilerplate/-/jobs/527/artifacts/browse"
+ }
+ ],
+ "manual_actions": [
+ {
+ "name": "stop staging",
+ "path": "/h5bp/html5-boilerplate/-/jobs/543/play",
+ "playable": false,
+ "scheduled": false
+ },
+ {
+ "name": "production",
+ "path": "/h5bp/html5-boilerplate/-/jobs/544/play",
+ "playable": false,
+ "scheduled": false
+ },
+ {
+ "name": "slack",
+ "path": "/h5bp/html5-boilerplate/-/jobs/545/play",
+ "playable": true,
+ "scheduled": false
+ }
+ ],
+ "scheduled_actions": []
+ },
+ "ref": {
+ "name": "master",
+ "path": "/h5bp/html5-boilerplate/commits/master",
+ "tag": false,
+ "branch": true,
+ "merge_request": false
+ },
+ "commit": {
+ "id": "bad98c453eab56d20057f3929989251d45cd1a8b",
+ "short_id": "bad98c45",
+ "title": "remove instances of shrink-to-fit=no (#2103)",
+ "created_at": "2018-12-17T20:52:18.000Z",
+ "parent_ids": ["49130f6cfe9ff1f749015d735649a2bc6f66cf3a"],
+ "message": "remove instances of shrink-to-fit=no (#2103)\n\ncloses #2102\r\n\r\nPer my findings, the need for it as a default was rectified with the release of iOS 9.3, where the viewport no longer shrunk to accommodate overflow, as was introduced in iOS 9.",
+ "author_name": "Scott O'Hara",
+ "author_email": "scottaohara@users.noreply.github.com",
+ "authored_date": "2018-12-17T20:52:18.000Z",
+ "committer_name": "Rob Larsen",
+ "committer_email": "rob@drunkenfist.com",
+ "committed_date": "2018-12-17T20:52:18.000Z",
+ "author": null,
+ "author_gravatar_url": "https://www.gravatar.com/avatar/6d597df7cf998d16cbe00ccac063b31e?s=80\u0026d=identicon",
+ "commit_url": "http://localhost:3001/h5bp/html5-boilerplate/commit/bad98c453eab56d20057f3929989251d45cd1a8b",
+ "commit_path": "/h5bp/html5-boilerplate/commit/bad98c453eab56d20057f3929989251d45cd1a8b"
+ },
+ "retry_path": "/h5bp/html5-boilerplate/pipelines/26/retry",
+ "triggered_by": {
+ "id": 4,
+ "user": null,
+ "active": false,
+ "coverage": null,
+ "source": "push",
+ "path": "/gitlab-org/gitlab-test/pipelines/4",
+ "details": {
+ "status": {
+ "icon": "status_warning",
+ "text": "passed",
+ "label": "passed with warnings",
+ "group": "success-with-warnings",
+ "tooltip": "passed",
+ "has_details": true,
+ "details_path": "/gitlab-org/gitlab-test/pipelines/4",
+ "illustration": null,
+ "favicon": "/assets/ci_favicons/favicon_status_success-8451333011eee8ce9f2ab25dc487fe24a8758c694827a582f17f42b0a90446a2.png"
+ }
+ },
+ "project": {
+ "id": 1,
+ "name": "Gitlab Test",
+ "full_path": "/gitlab-org/gitlab-test",
+ "full_name": "Gitlab Org / Gitlab Test"
+ }
+ },
+ "triggered": [],
+ "project": {
+ "id": 1794617,
+ "name": "GitLab Docs",
+ "full_path": "/gitlab-com/gitlab-docs",
+ "full_name": "GitLab.com / GitLab Docs"
+ }
+ }
+ ]
+ }
+ ]
+}
diff --git a/spec/frontend/pipelines/mock_data.js b/spec/frontend/pipelines/mock_data.js
new file mode 100644
index 00000000000..37c1e471415
--- /dev/null
+++ b/spec/frontend/pipelines/mock_data.js
@@ -0,0 +1,568 @@
+export const pipelineWithStages = {
+ id: 20333396,
+ user: {
+ id: 128633,
+ name: 'Rémy Coutable',
+ username: 'rymai',
+ state: 'active',
+ avatar_url:
+ 'https://secure.gravatar.com/avatar/263da227929cc0035cb0eba512bcf81a?s=80\u0026d=identicon',
+ web_url: 'https://gitlab.com/rymai',
+ path: '/rymai',
+ },
+ active: true,
+ coverage: '58.24',
+ source: 'push',
+ created_at: '2018-04-11T14:04:53.881Z',
+ updated_at: '2018-04-11T14:05:00.792Z',
+ path: '/gitlab-org/gitlab/pipelines/20333396',
+ flags: {
+ latest: true,
+ stuck: false,
+ auto_devops: false,
+ yaml_errors: false,
+ retryable: false,
+ cancelable: true,
+ failure_reason: false,
+ },
+ details: {
+ status: {
+ icon: 'status_running',
+ text: 'running',
+ label: 'running',
+ group: 'running',
+ has_details: true,
+ details_path: '/gitlab-org/gitlab/pipelines/20333396',
+ favicon:
+ 'https://assets.gitlab-static.net/assets/ci_favicons/favicon_status_running-2eb56be2871937954b2ba6d6f4ee9fdf7e5e1c146ac45f7be98119ccaca1aca9.ico',
+ },
+ duration: null,
+ finished_at: null,
+ stages: [
+ {
+ name: 'build',
+ title: 'build: skipped',
+ status: {
+ icon: 'status_skipped',
+ text: 'skipped',
+ label: 'skipped',
+ group: 'skipped',
+ has_details: true,
+ details_path: '/gitlab-org/gitlab/pipelines/20333396#build',
+ favicon:
+ 'https://assets.gitlab-static.net/assets/ci_favicons/favicon_status_skipped-a2eee568a5bffdb494050c7b62dde241de9189280836288ac8923d369f16222d.ico',
+ },
+ path: '/gitlab-org/gitlab/pipelines/20333396#build',
+ dropdown_path: '/gitlab-org/gitlab/pipelines/20333396/stage.json?stage=build',
+ },
+ {
+ name: 'prepare',
+ title: 'prepare: passed',
+ status: {
+ icon: 'status_success',
+ text: 'passed',
+ label: 'passed',
+ group: 'success',
+ has_details: true,
+ details_path: '/gitlab-org/gitlab/pipelines/20333396#prepare',
+ favicon:
+ 'https://assets.gitlab-static.net/assets/ci_favicons/favicon_status_success-26f59841becbef8c6fe414e9e74471d8bfd6a91b5855c19fe7f5923a40a7da47.ico',
+ },
+ path: '/gitlab-org/gitlab/pipelines/20333396#prepare',
+ dropdown_path: '/gitlab-org/gitlab/pipelines/20333396/stage.json?stage=prepare',
+ },
+ {
+ name: 'test',
+ title: 'test: running',
+ status: {
+ icon: 'status_running',
+ text: 'running',
+ label: 'running',
+ group: 'running',
+ has_details: true,
+ details_path: '/gitlab-org/gitlab/pipelines/20333396#test',
+ favicon:
+ 'https://assets.gitlab-static.net/assets/ci_favicons/favicon_status_running-2eb56be2871937954b2ba6d6f4ee9fdf7e5e1c146ac45f7be98119ccaca1aca9.ico',
+ },
+ path: '/gitlab-org/gitlab/pipelines/20333396#test',
+ dropdown_path: '/gitlab-org/gitlab/pipelines/20333396/stage.json?stage=test',
+ },
+ {
+ name: 'post-test',
+ title: 'post-test: created',
+ status: {
+ icon: 'status_created',
+ text: 'created',
+ label: 'created',
+ group: 'created',
+ has_details: true,
+ details_path: '/gitlab-org/gitlab/pipelines/20333396#post-test',
+ favicon:
+ 'https://assets.gitlab-static.net/assets/ci_favicons/favicon_status_created-e997aa0b7db73165df8a9d6803932b18d7b7cc37d604d2d96e378fea2dba9c5f.ico',
+ },
+ path: '/gitlab-org/gitlab/pipelines/20333396#post-test',
+ dropdown_path: '/gitlab-org/gitlab/pipelines/20333396/stage.json?stage=post-test',
+ },
+ {
+ name: 'pages',
+ title: 'pages: created',
+ status: {
+ icon: 'status_created',
+ text: 'created',
+ label: 'created',
+ group: 'created',
+ has_details: true,
+ details_path: '/gitlab-org/gitlab/pipelines/20333396#pages',
+ favicon:
+ 'https://assets.gitlab-static.net/assets/ci_favicons/favicon_status_created-e997aa0b7db73165df8a9d6803932b18d7b7cc37d604d2d96e378fea2dba9c5f.ico',
+ },
+ path: '/gitlab-org/gitlab/pipelines/20333396#pages',
+ dropdown_path: '/gitlab-org/gitlab/pipelines/20333396/stage.json?stage=pages',
+ },
+ {
+ name: 'post-cleanup',
+ title: 'post-cleanup: created',
+ status: {
+ icon: 'status_created',
+ text: 'created',
+ label: 'created',
+ group: 'created',
+ has_details: true,
+ details_path: '/gitlab-org/gitlab/pipelines/20333396#post-cleanup',
+ favicon:
+ 'https://assets.gitlab-static.net/assets/ci_favicons/favicon_status_created-e997aa0b7db73165df8a9d6803932b18d7b7cc37d604d2d96e378fea2dba9c5f.ico',
+ },
+ path: '/gitlab-org/gitlab/pipelines/20333396#post-cleanup',
+ dropdown_path: '/gitlab-org/gitlab/pipelines/20333396/stage.json?stage=post-cleanup',
+ },
+ ],
+ artifacts: [
+ {
+ name: 'gitlab:assets:compile',
+ expired: false,
+ expire_at: '2018-05-12T14:22:54.730Z',
+ path: '/gitlab-org/gitlab/-/jobs/62411438/artifacts/download',
+ keep_path: '/gitlab-org/gitlab/-/jobs/62411438/artifacts/keep',
+ browse_path: '/gitlab-org/gitlab/-/jobs/62411438/artifacts/browse',
+ },
+ {
+ name: 'rspec-mysql 12 28',
+ expired: false,
+ expire_at: '2018-05-12T14:22:45.136Z',
+ path: '/gitlab-org/gitlab/-/jobs/62411397/artifacts/download',
+ keep_path: '/gitlab-org/gitlab/-/jobs/62411397/artifacts/keep',
+ browse_path: '/gitlab-org/gitlab/-/jobs/62411397/artifacts/browse',
+ },
+ {
+ name: 'rspec-mysql 6 28',
+ expired: false,
+ expire_at: '2018-05-12T14:22:41.523Z',
+ path: '/gitlab-org/gitlab/-/jobs/62411391/artifacts/download',
+ keep_path: '/gitlab-org/gitlab/-/jobs/62411391/artifacts/keep',
+ browse_path: '/gitlab-org/gitlab/-/jobs/62411391/artifacts/browse',
+ },
+ {
+ name: 'rspec-pg geo 0 1',
+ expired: false,
+ expire_at: '2018-05-12T14:22:13.287Z',
+ path: '/gitlab-org/gitlab/-/jobs/62411353/artifacts/download',
+ keep_path: '/gitlab-org/gitlab/-/jobs/62411353/artifacts/keep',
+ browse_path: '/gitlab-org/gitlab/-/jobs/62411353/artifacts/browse',
+ },
+ {
+ name: 'rspec-mysql 0 28',
+ expired: false,
+ expire_at: '2018-05-12T14:22:06.834Z',
+ path: '/gitlab-org/gitlab/-/jobs/62411385/artifacts/download',
+ keep_path: '/gitlab-org/gitlab/-/jobs/62411385/artifacts/keep',
+ browse_path: '/gitlab-org/gitlab/-/jobs/62411385/artifacts/browse',
+ },
+ {
+ name: 'spinach-mysql 0 2',
+ expired: false,
+ expire_at: '2018-05-12T14:21:51.409Z',
+ path: '/gitlab-org/gitlab/-/jobs/62411423/artifacts/download',
+ keep_path: '/gitlab-org/gitlab/-/jobs/62411423/artifacts/keep',
+ browse_path: '/gitlab-org/gitlab/-/jobs/62411423/artifacts/browse',
+ },
+ {
+ name: 'karma',
+ expired: false,
+ expire_at: '2018-05-12T14:21:20.934Z',
+ path: '/gitlab-org/gitlab/-/jobs/62411440/artifacts/download',
+ keep_path: '/gitlab-org/gitlab/-/jobs/62411440/artifacts/keep',
+ browse_path: '/gitlab-org/gitlab/-/jobs/62411440/artifacts/browse',
+ },
+ {
+ name: 'spinach-pg 0 2',
+ expired: false,
+ expire_at: '2018-05-12T14:20:01.028Z',
+ path: '/gitlab-org/gitlab/-/jobs/62411419/artifacts/download',
+ keep_path: '/gitlab-org/gitlab/-/jobs/62411419/artifacts/keep',
+ browse_path: '/gitlab-org/gitlab/-/jobs/62411419/artifacts/browse',
+ },
+ {
+ name: 'spinach-pg 1 2',
+ expired: false,
+ expire_at: '2018-05-12T14:19:04.336Z',
+ path: '/gitlab-org/gitlab/-/jobs/62411421/artifacts/download',
+ keep_path: '/gitlab-org/gitlab/-/jobs/62411421/artifacts/keep',
+ browse_path: '/gitlab-org/gitlab/-/jobs/62411421/artifacts/browse',
+ },
+ {
+ name: 'sast',
+ expired: null,
+ expire_at: null,
+ path: '/gitlab-org/gitlab/-/jobs/62411442/artifacts/download',
+ browse_path: '/gitlab-org/gitlab/-/jobs/62411442/artifacts/browse',
+ },
+ {
+ name: 'code_quality',
+ expired: false,
+ expire_at: '2018-04-18T14:16:24.484Z',
+ path: '/gitlab-org/gitlab/-/jobs/62411441/artifacts/download',
+ keep_path: '/gitlab-org/gitlab/-/jobs/62411441/artifacts/keep',
+ browse_path: '/gitlab-org/gitlab/-/jobs/62411441/artifacts/browse',
+ },
+ {
+ name: 'cache gems',
+ expired: null,
+ expire_at: null,
+ path: '/gitlab-org/gitlab/-/jobs/62411447/artifacts/download',
+ browse_path: '/gitlab-org/gitlab/-/jobs/62411447/artifacts/browse',
+ },
+ {
+ name: 'dependency_scanning',
+ expired: null,
+ expire_at: null,
+ path: '/gitlab-org/gitlab/-/jobs/62411443/artifacts/download',
+ browse_path: '/gitlab-org/gitlab/-/jobs/62411443/artifacts/browse',
+ },
+ {
+ name: 'compile-assets',
+ expired: false,
+ expire_at: '2018-04-18T14:12:07.638Z',
+ path: '/gitlab-org/gitlab/-/jobs/62411334/artifacts/download',
+ keep_path: '/gitlab-org/gitlab/-/jobs/62411334/artifacts/keep',
+ browse_path: '/gitlab-org/gitlab/-/jobs/62411334/artifacts/browse',
+ },
+ {
+ name: 'setup-test-env',
+ expired: false,
+ expire_at: '2018-04-18T14:10:27.024Z',
+ path: '/gitlab-org/gitlab/-/jobs/62411336/artifacts/download',
+ keep_path: '/gitlab-org/gitlab/-/jobs/62411336/artifacts/keep',
+ browse_path: '/gitlab-org/gitlab/-/jobs/62411336/artifacts/browse',
+ },
+ {
+ name: 'retrieve-tests-metadata',
+ expired: false,
+ expire_at: '2018-05-12T14:06:35.926Z',
+ path: '/gitlab-org/gitlab/-/jobs/62411333/artifacts/download',
+ keep_path: '/gitlab-org/gitlab/-/jobs/62411333/artifacts/keep',
+ browse_path: '/gitlab-org/gitlab/-/jobs/62411333/artifacts/browse',
+ },
+ ],
+ manual_actions: [
+ {
+ name: 'package-and-qa',
+ path: '/gitlab-org/gitlab/-/jobs/62411330/play',
+ playable: true,
+ },
+ {
+ name: 'review-docs-deploy',
+ path: '/gitlab-org/gitlab/-/jobs/62411332/play',
+ playable: true,
+ },
+ ],
+ },
+ ref: {
+ name: 'master',
+ path: '/gitlab-org/gitlab/commits/master',
+ tag: false,
+ branch: true,
+ },
+ commit: {
+ id: 'e6a2885c503825792cb8a84a8731295e361bd059',
+ short_id: 'e6a2885c',
+ title: "Merge branch 'ce-to-ee-2018-04-11' into 'master'",
+ created_at: '2018-04-11T14:04:39.000Z',
+ parent_ids: [
+ '5d9b5118f6055f72cff1a82b88133609912f2c1d',
+ '6fdc6ee76a8062fe41b1a33f7c503334a6ebdc02',
+ ],
+ message:
+ "Merge branch 'ce-to-ee-2018-04-11' into 'master'\n\nCE upstream - 2018-04-11 12:26 UTC\n\nSee merge request gitlab-org/gitlab-ee!5326",
+ author_name: 'Rémy Coutable',
+ author_email: 'remy@rymai.me',
+ authored_date: '2018-04-11T14:04:39.000Z',
+ committer_name: 'Rémy Coutable',
+ committer_email: 'remy@rymai.me',
+ committed_date: '2018-04-11T14:04:39.000Z',
+ author: {
+ id: 128633,
+ name: 'Rémy Coutable',
+ username: 'rymai',
+ state: 'active',
+ avatar_url:
+ 'https://secure.gravatar.com/avatar/263da227929cc0035cb0eba512bcf81a?s=80\u0026d=identicon',
+ web_url: 'https://gitlab.com/rymai',
+ path: '/rymai',
+ },
+ author_gravatar_url:
+ 'https://secure.gravatar.com/avatar/263da227929cc0035cb0eba512bcf81a?s=80\u0026d=identicon',
+ commit_url:
+ 'https://gitlab.com/gitlab-org/gitlab/commit/e6a2885c503825792cb8a84a8731295e361bd059',
+ commit_path: '/gitlab-org/gitlab/commit/e6a2885c503825792cb8a84a8731295e361bd059',
+ },
+ cancel_path: '/gitlab-org/gitlab/pipelines/20333396/cancel',
+ triggered_by: null,
+ triggered: [],
+};
+
+export const stageReply = {
+ name: 'deploy',
+ title: 'deploy: running',
+ latest_statuses: [
+ {
+ id: 928,
+ name: 'stop staging',
+ started: false,
+ build_path: '/twitter/flight/-/jobs/928',
+ cancel_path: '/twitter/flight/-/jobs/928/cancel',
+ playable: false,
+ created_at: '2018-04-04T20:02:02.728Z',
+ updated_at: '2018-04-04T20:02:02.766Z',
+ status: {
+ icon: 'status_pending',
+ text: 'pending',
+ label: 'pending',
+ group: 'pending',
+ tooltip: 'pending',
+ has_details: true,
+ details_path: '/twitter/flight/-/jobs/928',
+ favicon:
+ '/assets/ci_favicons/dev/favicon_status_pending-db32e1faf94b9f89530ac519790920d1f18ea8f6af6cd2e0a26cd6840cacf101.ico',
+ action: {
+ icon: 'cancel',
+ title: 'Cancel',
+ path: '/twitter/flight/-/jobs/928/cancel',
+ method: 'post',
+ },
+ },
+ },
+ {
+ id: 926,
+ name: 'production',
+ started: false,
+ build_path: '/twitter/flight/-/jobs/926',
+ retry_path: '/twitter/flight/-/jobs/926/retry',
+ play_path: '/twitter/flight/-/jobs/926/play',
+ playable: true,
+ created_at: '2018-04-04T20:00:57.202Z',
+ updated_at: '2018-04-04T20:11:13.110Z',
+ status: {
+ icon: 'status_canceled',
+ text: 'canceled',
+ label: 'manual play action',
+ group: 'canceled',
+ tooltip: 'canceled',
+ has_details: true,
+ details_path: '/twitter/flight/-/jobs/926',
+ favicon:
+ '/assets/ci_favicons/dev/favicon_status_canceled-5491840b9b6feafba0bc599cbd49ee9580321dc809683856cf1b0d51532b1af6.ico',
+ action: {
+ icon: 'play',
+ title: 'Play',
+ path: '/twitter/flight/-/jobs/926/play',
+ method: 'post',
+ },
+ },
+ },
+ {
+ id: 217,
+ name: 'staging',
+ started: '2018-03-07T08:41:46.234Z',
+ build_path: '/twitter/flight/-/jobs/217',
+ retry_path: '/twitter/flight/-/jobs/217/retry',
+ playable: false,
+ created_at: '2018-03-07T14:41:58.093Z',
+ updated_at: '2018-03-07T14:41:58.093Z',
+ status: {
+ icon: 'status_success',
+ text: 'passed',
+ label: 'passed',
+ group: 'success',
+ tooltip: 'passed',
+ has_details: true,
+ details_path: '/twitter/flight/-/jobs/217',
+ favicon:
+ '/assets/ci_favicons/dev/favicon_status_success-308b4fc054cdd1b68d0865e6cfb7b02e92e3472f201507418f8eddb74ac11a59.ico',
+ action: {
+ icon: 'retry',
+ title: 'Retry',
+ path: '/twitter/flight/-/jobs/217/retry',
+ method: 'post',
+ },
+ },
+ },
+ ],
+ status: {
+ icon: 'status_running',
+ text: 'running',
+ label: 'running',
+ group: 'running',
+ tooltip: 'running',
+ has_details: true,
+ details_path: '/twitter/flight/pipelines/13#deploy',
+ favicon:
+ '/assets/ci_favicons/dev/favicon_status_running-c3ad2fc53ea6079c174e5b6c1351ff349e99ec3af5a5622fb77b0fe53ea279c1.ico',
+ },
+ path: '/twitter/flight/pipelines/13#deploy',
+ dropdown_path: '/twitter/flight/pipelines/13/stage.json?stage=deploy',
+};
+
+export const users = [
+ {
+ id: 1,
+ name: 'Administrator',
+ username: 'root',
+ state: 'active',
+ avatar_url:
+ 'https://www.gravatar.com/avatar/e64c7d89f26bd1972efa854d13d7dd61?s=80\u0026d=identicon',
+ web_url: 'http://192.168.1.22:3000/root',
+ },
+ {
+ id: 10,
+ name: 'Angel Spinka',
+ username: 'shalonda',
+ state: 'active',
+ avatar_url:
+ 'https://www.gravatar.com/avatar/709df1b65ad06764ee2b0edf1b49fc27?s=80\u0026d=identicon',
+ web_url: 'http://192.168.1.22:3000/shalonda',
+ },
+ {
+ id: 11,
+ name: 'Art Davis',
+ username: 'deja.green',
+ state: 'active',
+ avatar_url:
+ 'https://www.gravatar.com/avatar/bb56834c061522760e7a6dd7d431a306?s=80\u0026d=identicon',
+ web_url: 'http://192.168.1.22:3000/deja.green',
+ },
+ {
+ id: 32,
+ name: 'Arnold Mante',
+ username: 'reported_user_10',
+ state: 'active',
+ avatar_url:
+ 'https://www.gravatar.com/avatar/ab558033a82466d7905179e837d7723a?s=80\u0026d=identicon',
+ web_url: 'http://192.168.1.22:3000/reported_user_10',
+ },
+ {
+ id: 38,
+ name: 'Cher Wintheiser',
+ username: 'reported_user_16',
+ state: 'active',
+ avatar_url:
+ 'https://www.gravatar.com/avatar/2640356e8b5bc4314133090994ed162b?s=80\u0026d=identicon',
+ web_url: 'http://192.168.1.22:3000/reported_user_16',
+ },
+ {
+ id: 39,
+ name: 'Bethel Wolf',
+ username: 'reported_user_17',
+ state: 'active',
+ avatar_url:
+ 'https://www.gravatar.com/avatar/4b948694fadba4b01e4acfc06b065e8e?s=80\u0026d=identicon',
+ web_url: 'http://192.168.1.22:3000/reported_user_17',
+ },
+];
+
+export const branches = [
+ {
+ name: 'branch-1',
+ commit: {
+ id: '21fb056cc47dcf706670e6de635b1b326490ebdc',
+ short_id: '21fb056c',
+ created_at: '2020-05-07T10:58:28.000-04:00',
+ parent_ids: null,
+ title: 'Add new file',
+ message: 'Add new file',
+ author_name: 'Administrator',
+ author_email: 'admin@example.com',
+ authored_date: '2020-05-07T10:58:28.000-04:00',
+ committer_name: 'Administrator',
+ committer_email: 'admin@example.com',
+ committed_date: '2020-05-07T10:58:28.000-04:00',
+ web_url:
+ 'http://192.168.1.22:3000/root/dag-pipeline/-/commit/21fb056cc47dcf706670e6de635b1b326490ebdc',
+ },
+ merged: false,
+ protected: false,
+ developers_can_push: false,
+ developers_can_merge: false,
+ can_push: true,
+ default: false,
+ web_url: 'http://192.168.1.22:3000/root/dag-pipeline/-/tree/branch-1',
+ },
+ {
+ name: 'branch-10',
+ commit: {
+ id: '66673b07efef254dab7d537f0433a40e61cf84fe',
+ short_id: '66673b07',
+ created_at: '2020-03-16T11:04:46.000-04:00',
+ parent_ids: null,
+ title: 'Update .gitlab-ci.yml',
+ message: 'Update .gitlab-ci.yml',
+ author_name: 'Administrator',
+ author_email: 'admin@example.com',
+ authored_date: '2020-03-16T11:04:46.000-04:00',
+ committer_name: 'Administrator',
+ committer_email: 'admin@example.com',
+ committed_date: '2020-03-16T11:04:46.000-04:00',
+ web_url:
+ 'http://192.168.1.22:3000/root/dag-pipeline/-/commit/66673b07efef254dab7d537f0433a40e61cf84fe',
+ },
+ merged: false,
+ protected: false,
+ developers_can_push: false,
+ developers_can_merge: false,
+ can_push: true,
+ default: false,
+ web_url: 'http://192.168.1.22:3000/root/dag-pipeline/-/tree/branch-10',
+ },
+ {
+ name: 'branch-11',
+ commit: {
+ id: '66673b07efef254dab7d537f0433a40e61cf84fe',
+ short_id: '66673b07',
+ created_at: '2020-03-16T11:04:46.000-04:00',
+ parent_ids: null,
+ title: 'Update .gitlab-ci.yml',
+ message: 'Update .gitlab-ci.yml',
+ author_name: 'Administrator',
+ author_email: 'admin@example.com',
+ authored_date: '2020-03-16T11:04:46.000-04:00',
+ committer_name: 'Administrator',
+ committer_email: 'admin@example.com',
+ committed_date: '2020-03-16T11:04:46.000-04:00',
+ web_url:
+ 'http://192.168.1.22:3000/root/dag-pipeline/-/commit/66673b07efef254dab7d537f0433a40e61cf84fe',
+ },
+ merged: false,
+ protected: false,
+ developers_can_push: false,
+ developers_can_merge: false,
+ can_push: true,
+ default: false,
+ web_url: 'http://192.168.1.22:3000/root/dag-pipeline/-/tree/branch-11',
+ },
+];
+
+export const mockSearch = [
+ { type: 'username', value: { data: 'root', operator: '=' } },
+ { type: 'ref', value: { data: 'master', operator: '=' } },
+];
+
+export const mockBranchesAfterMap = ['branch-1', 'branch-10', 'branch-11'];
diff --git a/spec/frontend/pipelines/pipeline_details_mediator_spec.js b/spec/frontend/pipelines/pipeline_details_mediator_spec.js
new file mode 100644
index 00000000000..083e97666ed
--- /dev/null
+++ b/spec/frontend/pipelines/pipeline_details_mediator_spec.js
@@ -0,0 +1,36 @@
+import MockAdapter from 'axios-mock-adapter';
+import axios from '~/lib/utils/axios_utils';
+import PipelineMediator from '~/pipelines/pipeline_details_mediator';
+import waitForPromises from 'helpers/wait_for_promises';
+
+describe('PipelineMdediator', () => {
+ let mediator;
+ let mock;
+
+ beforeEach(() => {
+ mock = new MockAdapter(axios);
+ mediator = new PipelineMediator({ endpoint: 'foo.json' });
+ });
+
+ afterEach(() => {
+ mock.restore();
+ });
+
+ it('should set defaults', () => {
+ expect(mediator.options).toEqual({ endpoint: 'foo.json' });
+ expect(mediator.state.isLoading).toEqual(false);
+ expect(mediator.store).toBeDefined();
+ expect(mediator.service).toBeDefined();
+ });
+
+ describe('request and store data', () => {
+ it('should store received data', () => {
+ mock.onGet('foo.json').reply(200, { id: '121123' });
+ mediator.fetchPipeline();
+
+ return waitForPromises().then(() => {
+ expect(mediator.store.state.pipeline).toEqual({ id: '121123' });
+ });
+ });
+ });
+});
diff --git a/spec/frontend/pipelines/pipelines_actions_spec.js b/spec/frontend/pipelines/pipelines_actions_spec.js
new file mode 100644
index 00000000000..5e8d21660de
--- /dev/null
+++ b/spec/frontend/pipelines/pipelines_actions_spec.js
@@ -0,0 +1,142 @@
+import { shallowMount } from '@vue/test-utils';
+import MockAdapter from 'axios-mock-adapter';
+import { TEST_HOST } from 'spec/test_constants';
+import axios from '~/lib/utils/axios_utils';
+import PipelinesActions from '~/pipelines/components/pipelines_actions.vue';
+import { GlDeprecatedButton } from '@gitlab/ui';
+import GlCountdown from '~/vue_shared/components/gl_countdown.vue';
+import waitForPromises from 'helpers/wait_for_promises';
+
+describe('Pipelines Actions dropdown', () => {
+ let wrapper;
+ let mock;
+
+ const createComponent = (actions = []) => {
+ wrapper = shallowMount(PipelinesActions, {
+ propsData: {
+ actions,
+ },
+ });
+ };
+
+ const findAllDropdownItems = () => wrapper.findAll(GlDeprecatedButton);
+ const findAllCountdowns = () => wrapper.findAll(GlCountdown);
+
+ beforeEach(() => {
+ mock = new MockAdapter(axios);
+ });
+
+ afterEach(() => {
+ wrapper.destroy();
+ wrapper = null;
+
+ mock.restore();
+ });
+
+ describe('manual actions', () => {
+ const mockActions = [
+ {
+ name: 'stop_review',
+ path: `${TEST_HOST}/root/review-app/builds/1893/play`,
+ },
+ {
+ name: 'foo',
+ path: `${TEST_HOST}/disabled/pipeline/action`,
+ playable: false,
+ },
+ ];
+
+ beforeEach(() => {
+ createComponent(mockActions);
+ });
+
+ it('renders a dropdown with the provided actions', () => {
+ expect(findAllDropdownItems()).toHaveLength(mockActions.length);
+ });
+
+ it("renders a disabled action when it's not playable", () => {
+ expect(
+ findAllDropdownItems()
+ .at(1)
+ .attributes('disabled'),
+ ).toBe('true');
+ });
+
+ describe('on click', () => {
+ it('makes a request and toggles the loading state', () => {
+ mock.onPost(mockActions.path).reply(200);
+
+ wrapper.find(GlDeprecatedButton).vm.$emit('click');
+
+ expect(wrapper.vm.isLoading).toBe(true);
+
+ return waitForPromises().then(() => {
+ expect(wrapper.vm.isLoading).toBe(false);
+ });
+ });
+ });
+ });
+
+ describe('scheduled jobs', () => {
+ const scheduledJobAction = {
+ name: 'scheduled action',
+ path: `${TEST_HOST}/scheduled/job/action`,
+ playable: true,
+ scheduled_at: '2063-04-05T00:42:00Z',
+ };
+ const expiredJobAction = {
+ name: 'expired action',
+ path: `${TEST_HOST}/expired/job/action`,
+ playable: true,
+ scheduled_at: '2018-10-05T08:23:00Z',
+ };
+
+ beforeEach(() => {
+ jest.spyOn(Date, 'now').mockImplementation(() => new Date('2063-04-04T00:42:00Z').getTime());
+ createComponent([scheduledJobAction, expiredJobAction]);
+ });
+
+ it('makes post request after confirming', () => {
+ mock.onPost(scheduledJobAction.path).reply(200);
+ jest.spyOn(window, 'confirm').mockReturnValue(true);
+
+ findAllDropdownItems()
+ .at(0)
+ .vm.$emit('click');
+
+ expect(window.confirm).toHaveBeenCalled();
+
+ return waitForPromises().then(() => {
+ expect(mock.history.post.length).toBe(1);
+ });
+ });
+
+ it('does not make post request if confirmation is cancelled', () => {
+ mock.onPost(scheduledJobAction.path).reply(200);
+ jest.spyOn(window, 'confirm').mockReturnValue(false);
+
+ findAllDropdownItems()
+ .at(0)
+ .vm.$emit('click');
+
+ expect(window.confirm).toHaveBeenCalled();
+ expect(mock.history.post.length).toBe(0);
+ });
+
+ it('displays the remaining time in the dropdown', () => {
+ expect(
+ findAllCountdowns()
+ .at(0)
+ .props('endDateString'),
+ ).toBe(scheduledJobAction.scheduled_at);
+ });
+
+ it('displays 00:00:00 for expired jobs in the dropdown', () => {
+ expect(
+ findAllCountdowns()
+ .at(1)
+ .props('endDateString'),
+ ).toBe(expiredJobAction.scheduled_at);
+ });
+ });
+});
diff --git a/spec/frontend/pipelines/pipelines_artifacts_spec.js b/spec/frontend/pipelines/pipelines_artifacts_spec.js
new file mode 100644
index 00000000000..a93cc8a62ab
--- /dev/null
+++ b/spec/frontend/pipelines/pipelines_artifacts_spec.js
@@ -0,0 +1,46 @@
+import { shallowMount } from '@vue/test-utils';
+import PipelineArtifacts from '~/pipelines/components/pipelines_artifacts.vue';
+import { GlLink } from '@gitlab/ui';
+
+describe('Pipelines Artifacts dropdown', () => {
+ let wrapper;
+
+ const createComponent = () => {
+ wrapper = shallowMount(PipelineArtifacts, {
+ propsData: {
+ artifacts: [
+ {
+ name: 'artifact',
+ path: '/download/path',
+ },
+ {
+ name: 'artifact two',
+ path: '/download/path-two',
+ },
+ ],
+ },
+ });
+ };
+
+ const findGlLink = () => wrapper.find(GlLink);
+ const findAllGlLinks = () => wrapper.find('.dropdown-menu').findAll(GlLink);
+
+ beforeEach(() => {
+ createComponent();
+ });
+
+ afterEach(() => {
+ wrapper.destroy();
+ wrapper = null;
+ });
+
+ it('should render a dropdown with all the provided artifacts', () => {
+ expect(findAllGlLinks()).toHaveLength(2);
+ });
+
+ it('should render a link with the provided path', () => {
+ expect(findGlLink().attributes('href')).toEqual('/download/path');
+
+ expect(findGlLink().text()).toContain('artifact');
+ });
+});
diff --git a/spec/frontend/pipelines/pipelines_spec.js b/spec/frontend/pipelines/pipelines_spec.js
new file mode 100644
index 00000000000..2ddd2116e2c
--- /dev/null
+++ b/spec/frontend/pipelines/pipelines_spec.js
@@ -0,0 +1,710 @@
+import Api from '~/api';
+import { mount } from '@vue/test-utils';
+import MockAdapter from 'axios-mock-adapter';
+import axios from '~/lib/utils/axios_utils';
+import waitForPromises from 'helpers/wait_for_promises';
+import PipelinesComponent from '~/pipelines/components/pipelines.vue';
+import Store from '~/pipelines/stores/pipelines_store';
+import { pipelineWithStages, stageReply, users, mockSearch, branches } from './mock_data';
+import { RAW_TEXT_WARNING } from '~/pipelines/constants';
+import { GlFilteredSearch } from '@gitlab/ui';
+import createFlash from '~/flash';
+
+jest.mock('~/flash', () => jest.fn());
+
+describe('Pipelines', () => {
+ const jsonFixtureName = 'pipelines/pipelines.json';
+
+ preloadFixtures(jsonFixtureName);
+
+ let pipelines;
+ let wrapper;
+ let mock;
+
+ const paths = {
+ endpoint: 'twitter/flight/pipelines.json',
+ autoDevopsPath: '/help/topics/autodevops/index.md',
+ helpPagePath: '/help/ci/quick_start/README',
+ emptyStateSvgPath: '/assets/illustrations/pipelines_empty.svg',
+ errorStateSvgPath: '/assets/illustrations/pipelines_failed.svg',
+ noPipelinesSvgPath: '/assets/illustrations/pipelines_pending.svg',
+ ciLintPath: '/ci/lint',
+ resetCachePath: '/twitter/flight/settings/ci_cd/reset_cache',
+ newPipelinePath: '/twitter/flight/pipelines/new',
+ };
+
+ const noPermissions = {
+ endpoint: 'twitter/flight/pipelines.json',
+ autoDevopsPath: '/help/topics/autodevops/index.md',
+ helpPagePath: '/help/ci/quick_start/README',
+ emptyStateSvgPath: '/assets/illustrations/pipelines_empty.svg',
+ errorStateSvgPath: '/assets/illustrations/pipelines_failed.svg',
+ noPipelinesSvgPath: '/assets/illustrations/pipelines_pending.svg',
+ };
+
+ const defaultProps = {
+ hasGitlabCi: true,
+ canCreatePipeline: true,
+ ...paths,
+ };
+
+ const findFilteredSearch = () => wrapper.find(GlFilteredSearch);
+
+ const createComponent = (props = defaultProps, methods) => {
+ wrapper = mount(PipelinesComponent, {
+ provide: { glFeatures: { filterPipelinesSearch: true } },
+ propsData: {
+ store: new Store(),
+ projectId: '21',
+ ...props,
+ },
+ methods: {
+ ...methods,
+ },
+ });
+ };
+
+ beforeEach(() => {
+ mock = new MockAdapter(axios);
+ pipelines = getJSONFixture(jsonFixtureName);
+
+ jest.spyOn(Api, 'projectUsers').mockResolvedValue(users);
+ jest.spyOn(Api, 'branches').mockResolvedValue({ data: branches });
+ });
+
+ afterEach(() => {
+ wrapper.destroy();
+ mock.restore();
+ });
+
+ describe('With permission', () => {
+ describe('With pipelines in main tab', () => {
+ beforeEach(() => {
+ mock.onGet('twitter/flight/pipelines.json').reply(200, pipelines);
+ createComponent();
+ return waitForPromises();
+ });
+
+ it('renders tabs', () => {
+ expect(wrapper.find('.js-pipelines-tab-all').text()).toContain('All');
+ });
+
+ it('renders Run Pipeline link', () => {
+ expect(wrapper.find('.js-run-pipeline').attributes('href')).toBe(paths.newPipelinePath);
+ });
+
+ it('renders CI Lint link', () => {
+ expect(wrapper.find('.js-ci-lint').attributes('href')).toBe(paths.ciLintPath);
+ });
+
+ it('renders Clear Runner Cache button', () => {
+ expect(wrapper.find('.js-clear-cache').text()).toBe('Clear Runner Caches');
+ });
+
+ it('renders pipelines table', () => {
+ expect(wrapper.findAll('.gl-responsive-table-row')).toHaveLength(
+ pipelines.pipelines.length + 1,
+ );
+ });
+ });
+
+ describe('Without pipelines on main tab with CI', () => {
+ beforeEach(() => {
+ mock.onGet('twitter/flight/pipelines.json').reply(200, {
+ pipelines: [],
+ count: {
+ all: 0,
+ pending: 0,
+ running: 0,
+ finished: 0,
+ },
+ });
+
+ createComponent();
+
+ return waitForPromises();
+ });
+
+ it('renders tabs', () => {
+ expect(wrapper.find('.js-pipelines-tab-all').text()).toContain('All');
+ });
+
+ it('renders Run Pipeline link', () => {
+ expect(wrapper.find('.js-run-pipeline').attributes('href')).toEqual(paths.newPipelinePath);
+ });
+
+ it('renders CI Lint link', () => {
+ expect(wrapper.find('.js-ci-lint').attributes('href')).toEqual(paths.ciLintPath);
+ });
+
+ it('renders Clear Runner Cache button', () => {
+ expect(wrapper.find('.js-clear-cache').text()).toEqual('Clear Runner Caches');
+ });
+
+ it('renders tab empty state', () => {
+ expect(wrapper.find('.empty-state h4').text()).toEqual('There are currently no pipelines.');
+ });
+ });
+
+ describe('Without pipelines nor CI', () => {
+ beforeEach(() => {
+ mock.onGet('twitter/flight/pipelines.json').reply(200, {
+ pipelines: [],
+ count: {
+ all: 0,
+ pending: 0,
+ running: 0,
+ finished: 0,
+ },
+ });
+
+ createComponent({ hasGitlabCi: false, canCreatePipeline: true, ...paths });
+
+ return waitForPromises();
+ });
+
+ it('renders empty state', () => {
+ expect(wrapper.find('.js-empty-state h4').text()).toEqual('Build with confidence');
+
+ expect(wrapper.find('.js-get-started-pipelines').attributes('href')).toEqual(
+ paths.helpPagePath,
+ );
+ });
+
+ it('does not render tabs nor buttons', () => {
+ expect(wrapper.find('.js-pipelines-tab-all').exists()).toBeFalsy();
+ expect(wrapper.find('.js-run-pipeline').exists()).toBeFalsy();
+ expect(wrapper.find('.js-ci-lint').exists()).toBeFalsy();
+ expect(wrapper.find('.js-clear-cache').exists()).toBeFalsy();
+ });
+ });
+
+ describe('When API returns error', () => {
+ beforeEach(() => {
+ mock.onGet('twitter/flight/pipelines.json').reply(500, {});
+ createComponent({ hasGitlabCi: false, canCreatePipeline: true, ...paths });
+
+ return waitForPromises();
+ });
+
+ it('renders tabs', () => {
+ expect(wrapper.find('.js-pipelines-tab-all').text()).toContain('All');
+ });
+
+ it('renders buttons', () => {
+ expect(wrapper.find('.js-run-pipeline').attributes('href')).toEqual(paths.newPipelinePath);
+
+ expect(wrapper.find('.js-ci-lint').attributes('href')).toEqual(paths.ciLintPath);
+ expect(wrapper.find('.js-clear-cache').text()).toEqual('Clear Runner Caches');
+ });
+
+ it('renders error state', () => {
+ expect(wrapper.find('.empty-state').text()).toContain(
+ 'There was an error fetching the pipelines.',
+ );
+ });
+ });
+ });
+
+ describe('Without permission', () => {
+ describe('With pipelines in main tab', () => {
+ beforeEach(() => {
+ mock.onGet('twitter/flight/pipelines.json').reply(200, pipelines);
+
+ createComponent({ hasGitlabCi: false, canCreatePipeline: false, ...noPermissions });
+
+ return waitForPromises();
+ });
+
+ it('renders tabs', () => {
+ expect(wrapper.find('.js-pipelines-tab-all').text()).toContain('All');
+ });
+
+ it('does not render buttons', () => {
+ expect(wrapper.find('.js-run-pipeline').exists()).toBeFalsy();
+ expect(wrapper.find('.js-ci-lint').exists()).toBeFalsy();
+ expect(wrapper.find('.js-clear-cache').exists()).toBeFalsy();
+ });
+
+ it('renders pipelines table', () => {
+ expect(wrapper.findAll('.gl-responsive-table-row')).toHaveLength(
+ pipelines.pipelines.length + 1,
+ );
+ });
+ });
+
+ describe('Without pipelines on main tab with CI', () => {
+ beforeEach(() => {
+ mock.onGet('twitter/flight/pipelines.json').reply(200, {
+ pipelines: [],
+ count: {
+ all: 0,
+ pending: 0,
+ running: 0,
+ finished: 0,
+ },
+ });
+
+ createComponent({ hasGitlabCi: true, canCreatePipeline: false, ...noPermissions });
+
+ return waitForPromises();
+ });
+
+ it('renders tabs', () => {
+ expect(wrapper.find('.js-pipelines-tab-all').text()).toContain('All');
+ });
+
+ it('does not render buttons', () => {
+ expect(wrapper.find('.js-run-pipeline').exists()).toBeFalsy();
+ expect(wrapper.find('.js-ci-lint').exists()).toBeFalsy();
+ expect(wrapper.find('.js-clear-cache').exists()).toBeFalsy();
+ });
+
+ it('renders tab empty state', () => {
+ expect(wrapper.find('.empty-state h4').text()).toEqual('There are currently no pipelines.');
+ });
+ });
+
+ describe('Without pipelines nor CI', () => {
+ beforeEach(() => {
+ mock.onGet('twitter/flight/pipelines.json').reply(200, {
+ pipelines: [],
+ count: {
+ all: 0,
+ pending: 0,
+ running: 0,
+ finished: 0,
+ },
+ });
+
+ createComponent({ hasGitlabCi: false, canCreatePipeline: false, ...noPermissions });
+
+ return waitForPromises();
+ });
+
+ it('renders empty state without button to set CI', () => {
+ expect(wrapper.find('.js-empty-state').text()).toEqual(
+ 'This project is not currently set up to run pipelines.',
+ );
+
+ expect(wrapper.find('.js-get-started-pipelines').exists()).toBeFalsy();
+ });
+
+ it('does not render tabs or buttons', () => {
+ expect(wrapper.find('.js-pipelines-tab-all').exists()).toBeFalsy();
+ expect(wrapper.find('.js-run-pipeline').exists()).toBeFalsy();
+ expect(wrapper.find('.js-ci-lint').exists()).toBeFalsy();
+ expect(wrapper.find('.js-clear-cache').exists()).toBeFalsy();
+ });
+ });
+
+ describe('When API returns error', () => {
+ beforeEach(() => {
+ mock.onGet('twitter/flight/pipelines.json').reply(500, {});
+
+ createComponent({ hasGitlabCi: false, canCreatePipeline: true, ...noPermissions });
+
+ return waitForPromises();
+ });
+
+ it('renders tabs', () => {
+ expect(wrapper.find('.js-pipelines-tab-all').text()).toContain('All');
+ });
+
+ it('does not renders buttons', () => {
+ expect(wrapper.find('.js-run-pipeline').exists()).toBeFalsy();
+ expect(wrapper.find('.js-ci-lint').exists()).toBeFalsy();
+ expect(wrapper.find('.js-clear-cache').exists()).toBeFalsy();
+ });
+
+ it('renders error state', () => {
+ expect(wrapper.find('.empty-state').text()).toContain(
+ 'There was an error fetching the pipelines.',
+ );
+ });
+ });
+ });
+
+ describe('successful request', () => {
+ describe('with pipelines', () => {
+ beforeEach(() => {
+ mock.onGet('twitter/flight/pipelines.json').reply(200, pipelines);
+
+ createComponent();
+ return waitForPromises();
+ });
+
+ it('should render table', () => {
+ expect(wrapper.find('.table-holder').exists()).toBe(true);
+ expect(wrapper.findAll('.gl-responsive-table-row')).toHaveLength(
+ pipelines.pipelines.length + 1,
+ );
+ });
+
+ it('should render navigation tabs', () => {
+ expect(wrapper.find('.js-pipelines-tab-pending').text()).toContain('Pending');
+
+ expect(wrapper.find('.js-pipelines-tab-all').text()).toContain('All');
+
+ expect(wrapper.find('.js-pipelines-tab-running').text()).toContain('Running');
+
+ expect(wrapper.find('.js-pipelines-tab-finished').text()).toContain('Finished');
+
+ expect(wrapper.find('.js-pipelines-tab-branches').text()).toContain('Branches');
+
+ expect(wrapper.find('.js-pipelines-tab-tags').text()).toContain('Tags');
+ });
+
+ it('should make an API request when using tabs', () => {
+ const updateContentMock = jest.fn(() => {});
+ createComponent(
+ { hasGitlabCi: true, canCreatePipeline: true, ...paths },
+ {
+ updateContent: updateContentMock,
+ },
+ );
+
+ return waitForPromises().then(() => {
+ wrapper.find('.js-pipelines-tab-finished').trigger('click');
+
+ expect(updateContentMock).toHaveBeenCalledWith({ scope: 'finished', page: '1' });
+ });
+ });
+
+ describe('with pagination', () => {
+ it('should make an API request when using pagination', () => {
+ const updateContentMock = jest.fn(() => {});
+ createComponent(
+ { hasGitlabCi: true, canCreatePipeline: true, ...paths },
+ {
+ updateContent: updateContentMock,
+ },
+ );
+
+ return waitForPromises()
+ .then(() => {
+ // Mock pagination
+ wrapper.vm.store.state.pageInfo = {
+ page: 1,
+ total: 10,
+ perPage: 2,
+ nextPage: 2,
+ totalPages: 5,
+ };
+
+ return wrapper.vm.$nextTick();
+ })
+ .then(() => {
+ wrapper.find('.next-page-item').trigger('click');
+
+ expect(updateContentMock).toHaveBeenCalledWith({ scope: 'all', page: '2' });
+ });
+ });
+ });
+ });
+ });
+
+ describe('methods', () => {
+ beforeEach(() => {
+ jest.spyOn(window.history, 'pushState').mockImplementation(() => null);
+ });
+
+ describe('onChangeTab', () => {
+ it('should set page to 1', () => {
+ const updateContentMock = jest.fn(() => {});
+ createComponent(
+ { hasGitlabCi: true, canCreatePipeline: true, ...paths },
+ {
+ updateContent: updateContentMock,
+ },
+ );
+
+ wrapper.vm.onChangeTab('running');
+
+ expect(updateContentMock).toHaveBeenCalledWith({ scope: 'running', page: '1' });
+ });
+ });
+
+ describe('onChangePage', () => {
+ it('should update page and keep scope', () => {
+ const updateContentMock = jest.fn(() => {});
+ createComponent(
+ { hasGitlabCi: true, canCreatePipeline: true, ...paths },
+ {
+ updateContent: updateContentMock,
+ },
+ );
+
+ wrapper.vm.onChangePage(4);
+
+ expect(updateContentMock).toHaveBeenCalledWith({ scope: wrapper.vm.scope, page: '4' });
+ });
+ });
+ });
+
+ describe('computed properties', () => {
+ beforeEach(() => {
+ createComponent();
+ });
+
+ describe('tabs', () => {
+ it('returns default tabs', () => {
+ expect(wrapper.vm.tabs).toEqual([
+ { name: 'All', scope: 'all', count: undefined, isActive: true },
+ { name: 'Pending', scope: 'pending', count: undefined, isActive: false },
+ { name: 'Running', scope: 'running', count: undefined, isActive: false },
+ { name: 'Finished', scope: 'finished', count: undefined, isActive: false },
+ { name: 'Branches', scope: 'branches', isActive: false },
+ { name: 'Tags', scope: 'tags', isActive: false },
+ ]);
+ });
+ });
+
+ describe('emptyTabMessage', () => {
+ it('returns message with scope', () => {
+ wrapper.vm.scope = 'pending';
+
+ return wrapper.vm.$nextTick().then(() => {
+ expect(wrapper.vm.emptyTabMessage).toEqual('There are currently no pending pipelines.');
+ });
+ });
+
+ it('returns message without scope when scope is `all`', () => {
+ expect(wrapper.vm.emptyTabMessage).toEqual('There are currently no pipelines.');
+ });
+ });
+
+ describe('stateToRender', () => {
+ it('returns loading state when the app is loading', () => {
+ expect(wrapper.vm.stateToRender).toEqual('loading');
+ });
+
+ it('returns error state when app has error', () => {
+ wrapper.vm.hasError = true;
+ wrapper.vm.isLoading = false;
+
+ return wrapper.vm.$nextTick().then(() => {
+ expect(wrapper.vm.stateToRender).toEqual('error');
+ });
+ });
+
+ it('returns table list when app has pipelines', () => {
+ wrapper.vm.isLoading = false;
+ wrapper.vm.hasError = false;
+ wrapper.vm.state.pipelines = pipelines.pipelines;
+
+ return wrapper.vm.$nextTick().then(() => {
+ expect(wrapper.vm.stateToRender).toEqual('tableList');
+ });
+ });
+
+ it('returns empty tab when app does not have pipelines but project has pipelines', () => {
+ wrapper.vm.state.count.all = 10;
+ wrapper.vm.isLoading = false;
+
+ return wrapper.vm.$nextTick().then(() => {
+ expect(wrapper.vm.stateToRender).toEqual('emptyTab');
+ });
+ });
+
+ it('returns empty tab when project has CI', () => {
+ wrapper.vm.isLoading = false;
+
+ return wrapper.vm.$nextTick().then(() => {
+ expect(wrapper.vm.stateToRender).toEqual('emptyTab');
+ });
+ });
+
+ it('returns empty state when project does not have pipelines nor CI', () => {
+ createComponent({ hasGitlabCi: false, canCreatePipeline: true, ...paths });
+
+ wrapper.vm.isLoading = false;
+
+ return wrapper.vm.$nextTick().then(() => {
+ expect(wrapper.vm.stateToRender).toEqual('emptyState');
+ });
+ });
+ });
+
+ describe('shouldRenderTabs', () => {
+ it('returns true when state is loading & has already made the first request', () => {
+ wrapper.vm.isLoading = true;
+ wrapper.vm.hasMadeRequest = true;
+
+ return wrapper.vm.$nextTick().then(() => {
+ expect(wrapper.vm.shouldRenderTabs).toEqual(true);
+ });
+ });
+
+ it('returns true when state is tableList & has already made the first request', () => {
+ wrapper.vm.isLoading = false;
+ wrapper.vm.state.pipelines = pipelines.pipelines;
+ wrapper.vm.hasMadeRequest = true;
+
+ return wrapper.vm.$nextTick().then(() => {
+ expect(wrapper.vm.shouldRenderTabs).toEqual(true);
+ });
+ });
+
+ it('returns true when state is error & has already made the first request', () => {
+ wrapper.vm.isLoading = false;
+ wrapper.vm.hasError = true;
+ wrapper.vm.hasMadeRequest = true;
+
+ return wrapper.vm.$nextTick().then(() => {
+ expect(wrapper.vm.shouldRenderTabs).toEqual(true);
+ });
+ });
+
+ it('returns true when state is empty tab & has already made the first request', () => {
+ wrapper.vm.isLoading = false;
+ wrapper.vm.state.count.all = 10;
+ wrapper.vm.hasMadeRequest = true;
+
+ return wrapper.vm.$nextTick().then(() => {
+ expect(wrapper.vm.shouldRenderTabs).toEqual(true);
+ });
+ });
+
+ it('returns false when has not made first request', () => {
+ wrapper.vm.hasMadeRequest = false;
+
+ return wrapper.vm.$nextTick().then(() => {
+ expect(wrapper.vm.shouldRenderTabs).toEqual(false);
+ });
+ });
+
+ it('returns false when state is empty state', () => {
+ createComponent({ hasGitlabCi: false, canCreatePipeline: true, ...paths });
+
+ wrapper.vm.isLoading = false;
+ wrapper.vm.hasMadeRequest = true;
+
+ return wrapper.vm.$nextTick().then(() => {
+ expect(wrapper.vm.shouldRenderTabs).toEqual(false);
+ });
+ });
+ });
+
+ describe('shouldRenderButtons', () => {
+ it('returns true when it has paths & has made the first request', () => {
+ wrapper.vm.hasMadeRequest = true;
+
+ return wrapper.vm.$nextTick().then(() => {
+ expect(wrapper.vm.shouldRenderButtons).toEqual(true);
+ });
+ });
+
+ it('returns false when it has not made the first request', () => {
+ wrapper.vm.hasMadeRequest = false;
+
+ return wrapper.vm.$nextTick().then(() => {
+ expect(wrapper.vm.shouldRenderButtons).toEqual(false);
+ });
+ });
+ });
+ });
+
+ describe('updates results when a staged is clicked', () => {
+ beforeEach(() => {
+ const copyPipeline = { ...pipelineWithStages };
+ copyPipeline.id += 1;
+ mock
+ .onGet('twitter/flight/pipelines.json')
+ .reply(
+ 200,
+ {
+ pipelines: [pipelineWithStages],
+ count: {
+ all: 1,
+ finished: 1,
+ pending: 0,
+ running: 0,
+ },
+ },
+ {
+ 'POLL-INTERVAL': 100,
+ },
+ )
+ .onGet(pipelineWithStages.details.stages[0].dropdown_path)
+ .reply(200, stageReply);
+
+ createComponent();
+ });
+
+ describe('when a request is being made', () => {
+ it('stops polling, cancels the request, & restarts polling', () => {
+ const stopMock = jest.spyOn(wrapper.vm.poll, 'stop');
+ const restartMock = jest.spyOn(wrapper.vm.poll, 'restart');
+ const cancelMock = jest.spyOn(wrapper.vm.service.cancelationSource, 'cancel');
+ mock.onGet('twitter/flight/pipelines.json').reply(200, pipelines);
+
+ return waitForPromises()
+ .then(() => {
+ wrapper.vm.isMakingRequest = true;
+ wrapper.find('.js-builds-dropdown-button').trigger('click');
+ })
+ .then(() => {
+ expect(cancelMock).toHaveBeenCalled();
+ expect(stopMock).toHaveBeenCalled();
+ expect(restartMock).toHaveBeenCalled();
+ });
+ });
+ });
+
+ describe('when no request is being made', () => {
+ it('stops polling & restarts polling', () => {
+ const stopMock = jest.spyOn(wrapper.vm.poll, 'stop');
+ const restartMock = jest.spyOn(wrapper.vm.poll, 'restart');
+ mock.onGet('twitter/flight/pipelines.json').reply(200, pipelines);
+
+ return waitForPromises()
+ .then(() => {
+ wrapper.find('.js-builds-dropdown-button').trigger('click');
+ expect(stopMock).toHaveBeenCalled();
+ })
+ .then(() => {
+ expect(restartMock).toHaveBeenCalled();
+ });
+ });
+ });
+ });
+
+ describe('Pipeline filters', () => {
+ let updateContentMock;
+
+ beforeEach(() => {
+ mock.onGet(paths.endpoint).reply(200, pipelines);
+ createComponent();
+
+ updateContentMock = jest.spyOn(wrapper.vm, 'updateContent');
+
+ return waitForPromises();
+ });
+
+ it('updates request data and query params on filter submit', () => {
+ const expectedQueryParams = { page: '1', scope: 'all', username: 'root', ref: 'master' };
+
+ findFilteredSearch().vm.$emit('submit', mockSearch);
+
+ expect(wrapper.vm.requestData).toEqual(expectedQueryParams);
+ expect(updateContentMock).toHaveBeenCalledWith(expectedQueryParams);
+ });
+
+ it('does not add query params if raw text search is used', () => {
+ const expectedQueryParams = { page: '1', scope: 'all' };
+
+ findFilteredSearch().vm.$emit('submit', ['rawText']);
+
+ expect(wrapper.vm.requestData).toEqual(expectedQueryParams);
+ expect(updateContentMock).toHaveBeenCalledWith(expectedQueryParams);
+ });
+
+ it('displays a warning message if raw text search is used', () => {
+ findFilteredSearch().vm.$emit('submit', ['rawText']);
+
+ expect(createFlash).toHaveBeenCalledTimes(1);
+ expect(createFlash).toHaveBeenCalledWith(RAW_TEXT_WARNING, 'warning');
+ });
+ });
+});
diff --git a/spec/frontend/pipelines/pipelines_table_row_spec.js b/spec/frontend/pipelines/pipelines_table_row_spec.js
index c43210c5350..3d564c8758c 100644
--- a/spec/frontend/pipelines/pipelines_table_row_spec.js
+++ b/spec/frontend/pipelines/pipelines_table_row_spec.js
@@ -169,7 +169,7 @@ describe('Pipelines Table Row', () => {
};
beforeEach(() => {
- const withActions = Object.assign({}, pipeline);
+ const withActions = { ...pipeline };
withActions.details.scheduled_actions = [scheduledJobAction];
withActions.flags.cancelable = true;
withActions.flags.retryable = true;
diff --git a/spec/frontend/pipelines/pipelines_table_spec.js b/spec/frontend/pipelines/pipelines_table_spec.js
new file mode 100644
index 00000000000..b0ab250dd16
--- /dev/null
+++ b/spec/frontend/pipelines/pipelines_table_spec.js
@@ -0,0 +1,66 @@
+import { mount } from '@vue/test-utils';
+import PipelinesTable from '~/pipelines/components/pipelines_table.vue';
+
+describe('Pipelines Table', () => {
+ let pipeline;
+ let wrapper;
+
+ const jsonFixtureName = 'pipelines/pipelines.json';
+
+ const defaultProps = {
+ pipelines: [],
+ autoDevopsHelpPath: 'foo',
+ viewType: 'root',
+ };
+
+ const createComponent = (props = defaultProps) => {
+ wrapper = mount(PipelinesTable, {
+ propsData: props,
+ });
+ };
+ const findRows = () => wrapper.findAll('.commit.gl-responsive-table-row');
+
+ preloadFixtures(jsonFixtureName);
+
+ beforeEach(() => {
+ const { pipelines } = getJSONFixture(jsonFixtureName);
+ pipeline = pipelines.find(p => p.user !== null && p.commit !== null);
+
+ createComponent();
+ });
+
+ afterEach(() => {
+ wrapper.destroy();
+ wrapper = null;
+ });
+
+ describe('table', () => {
+ it('should render a table', () => {
+ expect(wrapper.classes()).toContain('ci-table');
+ });
+
+ it('should render table head with correct columns', () => {
+ expect(wrapper.find('.table-section.js-pipeline-status').text()).toEqual('Status');
+
+ expect(wrapper.find('.table-section.js-pipeline-info').text()).toEqual('Pipeline');
+
+ expect(wrapper.find('.table-section.js-pipeline-commit').text()).toEqual('Commit');
+
+ expect(wrapper.find('.table-section.js-pipeline-stages').text()).toEqual('Stages');
+ });
+ });
+
+ describe('without data', () => {
+ it('should render an empty table', () => {
+ expect(findRows()).toHaveLength(0);
+ });
+ });
+
+ describe('with data', () => {
+ it('should render rows', () => {
+ createComponent({ pipelines: [pipeline], autoDevopsHelpPath: 'foo', viewType: 'root' });
+
+ expect(findRows()).toHaveLength(1);
+ });
+ });
+});
diff --git a/spec/frontend/pipelines/stage_spec.js b/spec/frontend/pipelines/stage_spec.js
new file mode 100644
index 00000000000..6aa041bcb7f
--- /dev/null
+++ b/spec/frontend/pipelines/stage_spec.js
@@ -0,0 +1,156 @@
+import { mount } from '@vue/test-utils';
+import MockAdapter from 'axios-mock-adapter';
+import axios from '~/lib/utils/axios_utils';
+import StageComponent from '~/pipelines/components/stage.vue';
+import eventHub from '~/pipelines/event_hub';
+import { stageReply } from './mock_data';
+import waitForPromises from 'helpers/wait_for_promises';
+
+describe('Pipelines stage component', () => {
+ let wrapper;
+ let mock;
+
+ const defaultProps = {
+ stage: {
+ status: {
+ group: 'success',
+ icon: 'status_success',
+ title: 'success',
+ },
+ dropdown_path: 'path.json',
+ },
+ updateDropdown: false,
+ };
+
+ const createComponent = (props = {}) => {
+ wrapper = mount(StageComponent, {
+ propsData: {
+ ...defaultProps,
+ ...props,
+ },
+ });
+ };
+
+ beforeEach(() => {
+ mock = new MockAdapter(axios);
+ });
+
+ afterEach(() => {
+ wrapper.destroy();
+ wrapper = null;
+
+ mock.restore();
+ });
+
+ describe('default', () => {
+ beforeEach(() => {
+ createComponent();
+ });
+
+ it('should render a dropdown with the status icon', () => {
+ expect(wrapper.attributes('class')).toEqual('dropdown');
+ expect(wrapper.find('svg').exists()).toBe(true);
+ expect(wrapper.find('button').attributes('data-toggle')).toEqual('dropdown');
+ });
+ });
+
+ describe('with successful request', () => {
+ beforeEach(() => {
+ mock.onGet('path.json').reply(200, stageReply);
+ createComponent();
+ });
+
+ it('should render the received data and emit `clickedDropdown` event', () => {
+ jest.spyOn(eventHub, '$emit');
+ wrapper.find('button').trigger('click');
+
+ return waitForPromises().then(() => {
+ expect(wrapper.find('.js-builds-dropdown-container ul').text()).toContain(
+ stageReply.latest_statuses[0].name,
+ );
+
+ expect(eventHub.$emit).toHaveBeenCalledWith('clickedDropdown');
+ });
+ });
+ });
+
+ describe('when request fails', () => {
+ beforeEach(() => {
+ mock.onGet('path.json').reply(500);
+ createComponent();
+ });
+
+ it('should close the dropdown', () => {
+ wrapper.setMethods({
+ closeDropdown: jest.fn(),
+ isDropdownOpen: jest.fn().mockReturnValue(false),
+ });
+
+ wrapper.find('button').trigger('click');
+
+ return waitForPromises().then(() => {
+ expect(wrapper.vm.closeDropdown).toHaveBeenCalled();
+ });
+ });
+ });
+
+ describe('update endpoint correctly', () => {
+ beforeEach(() => {
+ const copyStage = { ...stageReply };
+ copyStage.latest_statuses[0].name = 'this is the updated content';
+ mock.onGet('bar.json').reply(200, copyStage);
+ createComponent({
+ stage: {
+ status: {
+ group: 'running',
+ icon: 'status_running',
+ title: 'running',
+ },
+ dropdown_path: 'bar.json',
+ },
+ });
+ });
+
+ it('should update the stage to request the new endpoint provided', () => {
+ return wrapper.vm
+ .$nextTick()
+ .then(() => {
+ wrapper.find('button').trigger('click');
+ return waitForPromises();
+ })
+ .then(() => {
+ expect(wrapper.find('.js-builds-dropdown-container ul').text()).toContain(
+ 'this is the updated content',
+ );
+ });
+ });
+ });
+
+ describe('pipelineActionRequestComplete', () => {
+ beforeEach(() => {
+ mock.onGet('path.json').reply(200, stageReply);
+
+ mock.onPost(`${stageReply.latest_statuses[0].status.action.path}.json`).reply(200);
+
+ createComponent({ type: 'PIPELINES_TABLE' });
+ });
+
+ describe('within pipeline table', () => {
+ it('emits `refreshPipelinesTable` event when `pipelineActionRequestComplete` is triggered', () => {
+ jest.spyOn(eventHub, '$emit');
+
+ wrapper.find('button').trigger('click');
+
+ return waitForPromises()
+ .then(() => {
+ wrapper.find('.js-ci-action').trigger('click');
+
+ return waitForPromises();
+ })
+ .then(() => {
+ expect(eventHub.$emit).toHaveBeenCalledWith('refreshPipelinesTable');
+ });
+ });
+ });
+ });
+});
diff --git a/spec/frontend/pipelines/stores/pipeline_store_spec.js b/spec/frontend/pipelines/stores/pipeline_store_spec.js
new file mode 100644
index 00000000000..68d438109b3
--- /dev/null
+++ b/spec/frontend/pipelines/stores/pipeline_store_spec.js
@@ -0,0 +1,135 @@
+import PipelineStore from '~/pipelines/stores/pipeline_store';
+import LinkedPipelines from '../linked_pipelines_mock.json';
+
+describe('EE Pipeline store', () => {
+ let store;
+ let data;
+
+ beforeEach(() => {
+ store = new PipelineStore();
+ data = { ...LinkedPipelines };
+
+ store.storePipeline(data);
+ });
+
+ describe('storePipeline', () => {
+ describe('triggered_by', () => {
+ it('sets triggered_by as an array', () => {
+ expect(store.state.pipeline.triggered_by.length).toEqual(1);
+ });
+
+ it('adds isExpanding & isLoading keys set to false', () => {
+ expect(store.state.pipeline.triggered_by[0].isExpanded).toEqual(false);
+ expect(store.state.pipeline.triggered_by[0].isLoading).toEqual(false);
+ });
+
+ it('parses nested triggered_by', () => {
+ expect(store.state.pipeline.triggered_by[0].triggered_by.length).toEqual(1);
+ expect(store.state.pipeline.triggered_by[0].triggered_by[0].isExpanded).toEqual(false);
+ expect(store.state.pipeline.triggered_by[0].triggered_by[0].isLoading).toEqual(false);
+ });
+ });
+
+ describe('triggered', () => {
+ it('adds isExpanding & isLoading keys set to false for each triggered pipeline', () => {
+ store.state.pipeline.triggered.forEach(pipeline => {
+ expect(pipeline.isExpanded).toEqual(false);
+ expect(pipeline.isLoading).toEqual(false);
+ });
+ });
+
+ it('parses nested triggered pipelines', () => {
+ store.state.pipeline.triggered[1].triggered.forEach(pipeline => {
+ expect(pipeline.isExpanded).toEqual(false);
+ expect(pipeline.isLoading).toEqual(false);
+ });
+ });
+ });
+ });
+
+ describe('resetTriggeredByPipeline', () => {
+ it('closes the pipeline & nested ones', () => {
+ store.state.pipeline.triggered_by[0].isExpanded = true;
+ store.state.pipeline.triggered_by[0].triggered_by[0].isExpanded = true;
+
+ store.resetTriggeredByPipeline(store.state.pipeline, store.state.pipeline.triggered_by[0]);
+
+ expect(store.state.pipeline.triggered_by[0].isExpanded).toEqual(false);
+ expect(store.state.pipeline.triggered_by[0].triggered_by[0].isExpanded).toEqual(false);
+ });
+ });
+
+ describe('openTriggeredByPipeline', () => {
+ it('opens the given pipeline', () => {
+ store.openTriggeredByPipeline(store.state.pipeline, store.state.pipeline.triggered_by[0]);
+
+ expect(store.state.pipeline.triggered_by[0].isExpanded).toEqual(true);
+ });
+ });
+
+ describe('closeTriggeredByPipeline', () => {
+ it('closes the given pipeline', () => {
+ // open it first
+ store.openTriggeredByPipeline(store.state.pipeline, store.state.pipeline.triggered_by[0]);
+
+ store.closeTriggeredByPipeline(store.state.pipeline, store.state.pipeline.triggered_by[0]);
+
+ expect(store.state.pipeline.triggered_by[0].isExpanded).toEqual(false);
+ });
+ });
+
+ describe('resetTriggeredPipelines', () => {
+ it('closes the pipeline & nested ones', () => {
+ store.state.pipeline.triggered[0].isExpanded = true;
+ store.state.pipeline.triggered[0].triggered[0].isExpanded = true;
+
+ store.resetTriggeredPipelines(store.state.pipeline, store.state.pipeline.triggered[0]);
+
+ expect(store.state.pipeline.triggered[0].isExpanded).toEqual(false);
+ expect(store.state.pipeline.triggered[0].triggered[0].isExpanded).toEqual(false);
+ });
+ });
+
+ describe('openTriggeredPipeline', () => {
+ it('opens the given pipeline', () => {
+ store.openTriggeredPipeline(store.state.pipeline, store.state.pipeline.triggered[0]);
+
+ expect(store.state.pipeline.triggered[0].isExpanded).toEqual(true);
+ });
+ });
+
+ describe('closeTriggeredPipeline', () => {
+ it('closes the given pipeline', () => {
+ // open it first
+ store.openTriggeredPipeline(store.state.pipeline, store.state.pipeline.triggered[0]);
+
+ store.closeTriggeredPipeline(store.state.pipeline, store.state.pipeline.triggered[0]);
+
+ expect(store.state.pipeline.triggered[0].isExpanded).toEqual(false);
+ });
+ });
+
+ describe('toggleLoading', () => {
+ it('toggles the isLoading property for the given pipeline', () => {
+ store.toggleLoading(store.state.pipeline.triggered[0]);
+
+ expect(store.state.pipeline.triggered[0].isLoading).toEqual(true);
+ });
+ });
+
+ describe('addExpandedPipelineToRequestData', () => {
+ it('pushes the given id to expandedPipelines array', () => {
+ store.addExpandedPipelineToRequestData('213231');
+
+ expect(store.state.expandedPipelines).toEqual(['213231']);
+ });
+ });
+
+ describe('removeExpandedPipelineToRequestData', () => {
+ it('pushes the given id to expandedPipelines array', () => {
+ store.removeExpandedPipelineToRequestData('213231');
+
+ expect(store.state.expandedPipelines).toEqual([]);
+ });
+ });
+});
diff --git a/spec/frontend/pipelines/test_reports/stores/mutations_spec.js b/spec/frontend/pipelines/test_reports/stores/mutations_spec.js
index 9eaa563025d..a0eb93c4e6b 100644
--- a/spec/frontend/pipelines/test_reports/stores/mutations_spec.js
+++ b/spec/frontend/pipelines/test_reports/stores/mutations_spec.js
@@ -20,7 +20,7 @@ describe('Mutations TestReports Store', () => {
describe('set endpoint', () => {
it('should set endpoint', () => {
- const expectedState = Object.assign({}, mockState, { endpoint: 'foo' });
+ const expectedState = { ...mockState, endpoint: 'foo' };
mutations[types.SET_ENDPOINT](mockState, 'foo');
expect(mockState.endpoint).toEqual(expectedState.endpoint);
@@ -47,14 +47,14 @@ describe('Mutations TestReports Store', () => {
describe('toggle loading', () => {
it('should set to true', () => {
- const expectedState = Object.assign({}, mockState, { isLoading: true });
+ const expectedState = { ...mockState, isLoading: true };
mutations[types.TOGGLE_LOADING](mockState);
expect(mockState.isLoading).toEqual(expectedState.isLoading);
});
it('should toggle back to false', () => {
- const expectedState = Object.assign({}, mockState, { isLoading: false });
+ const expectedState = { ...mockState, isLoading: false };
mockState.isLoading = true;
mutations[types.TOGGLE_LOADING](mockState);
diff --git a/spec/frontend/pipelines/test_reports/test_summary_spec.js b/spec/frontend/pipelines/test_reports/test_summary_spec.js
index 160d93d2e6b..8f041e46472 100644
--- a/spec/frontend/pipelines/test_reports/test_summary_spec.js
+++ b/spec/frontend/pipelines/test_reports/test_summary_spec.js
@@ -82,17 +82,19 @@ describe('Test reports summary', () => {
describe('success percentage calculation', () => {
it.each`
- name | successCount | totalCount | result
- ${'displays 0 when there are no tests'} | ${0} | ${0} | ${'0'}
- ${'displays whole number when possible'} | ${10} | ${50} | ${'20'}
- ${'rounds to 0.01'} | ${1} | ${16604} | ${'0.01'}
- ${'correctly rounds to 50'} | ${8302} | ${16604} | ${'50'}
- ${'rounds down for large close numbers'} | ${16603} | ${16604} | ${'99.99'}
- ${'correctly displays 100'} | ${16604} | ${16604} | ${'100'}
- `('$name', ({ successCount, totalCount, result }) => {
+ name | successCount | totalCount | skippedCount | result
+ ${'displays 0 when there are no tests'} | ${0} | ${0} | ${0} | ${'0'}
+ ${'displays whole number when possible'} | ${10} | ${50} | ${0} | ${'20'}
+ ${'excludes skipped tests from total'} | ${10} | ${50} | ${5} | ${'22.22'}
+ ${'rounds to 0.01'} | ${1} | ${16604} | ${0} | ${'0.01'}
+ ${'correctly rounds to 50'} | ${8302} | ${16604} | ${0} | ${'50'}
+ ${'rounds down for large close numbers'} | ${16603} | ${16604} | ${0} | ${'99.99'}
+ ${'correctly displays 100'} | ${16604} | ${16604} | ${0} | ${'100'}
+ `('$name', ({ successCount, totalCount, skippedCount, result }) => {
createComponent({
report: {
success_count: successCount,
+ skipped_count: skippedCount,
total_count: totalCount,
},
});
diff --git a/spec/frontend/pipelines/test_reports/test_summary_table_spec.js b/spec/frontend/pipelines/test_reports/test_summary_table_spec.js
index 9146f301f66..b585536ae09 100644
--- a/spec/frontend/pipelines/test_reports/test_summary_table_spec.js
+++ b/spec/frontend/pipelines/test_reports/test_summary_table_spec.js
@@ -37,11 +37,47 @@ describe('Test reports summary table', () => {
describe('when test reports are supplied', () => {
beforeEach(() => createComponent());
+ const findErrorIcon = () => wrapper.find({ ref: 'suiteErrorIcon' });
it('renders the correct number of rows', () => {
expect(noSuitesToShow().exists()).toBe(false);
expect(allSuitesRows().length).toBe(testReports.test_suites.length);
});
+
+ describe('when there is a suite error', () => {
+ beforeEach(() => {
+ createComponent({
+ test_suites: [
+ {
+ ...testReports.test_suites[0],
+ suite_error: 'Suite Error',
+ },
+ ],
+ });
+ });
+
+ it('renders error icon', () => {
+ expect(findErrorIcon().exists()).toBe(true);
+ expect(findErrorIcon().attributes('title')).toEqual('Suite Error');
+ });
+ });
+
+ describe('when there is not a suite error', () => {
+ beforeEach(() => {
+ createComponent({
+ test_suites: [
+ {
+ ...testReports.test_suites[0],
+ suite_error: null,
+ },
+ ],
+ });
+ });
+
+ it('does not render error icon', () => {
+ expect(findErrorIcon().exists()).toBe(false);
+ });
+ });
});
describe('when there are no test suites', () => {
diff --git a/spec/frontend/pipelines/time_ago_spec.js b/spec/frontend/pipelines/time_ago_spec.js
new file mode 100644
index 00000000000..1bd16182d47
--- /dev/null
+++ b/spec/frontend/pipelines/time_ago_spec.js
@@ -0,0 +1,67 @@
+import { shallowMount } from '@vue/test-utils';
+import TimeAgo from '~/pipelines/components/time_ago.vue';
+
+describe('Timeago component', () => {
+ let wrapper;
+
+ const createComponent = (props = {}) => {
+ wrapper = shallowMount(TimeAgo, {
+ propsData: {
+ ...props,
+ },
+ data() {
+ return {
+ iconTimerSvg: `<svg></svg>`,
+ };
+ },
+ });
+ };
+
+ afterEach(() => {
+ wrapper.destroy();
+ wrapper = null;
+ });
+
+ describe('with duration', () => {
+ beforeEach(() => {
+ createComponent({ duration: 10, finishedTime: '' });
+ });
+
+ it('should render duration and timer svg', () => {
+ expect(wrapper.find('.duration').exists()).toBe(true);
+ expect(wrapper.find('.duration svg').exists()).toBe(true);
+ });
+ });
+
+ describe('without duration', () => {
+ beforeEach(() => {
+ createComponent({ duration: 0, finishedTime: '' });
+ });
+
+ it('should not render duration and timer svg', () => {
+ expect(wrapper.find('.duration').exists()).toBe(false);
+ });
+ });
+
+ describe('with finishedTime', () => {
+ beforeEach(() => {
+ createComponent({ duration: 0, finishedTime: '2017-04-26T12:40:23.277Z' });
+ });
+
+ it('should render time and calendar icon', () => {
+ expect(wrapper.find('.finished-at').exists()).toBe(true);
+ expect(wrapper.find('.finished-at i.fa-calendar').exists()).toBe(true);
+ expect(wrapper.find('.finished-at time').exists()).toBe(true);
+ });
+ });
+
+ describe('without finishedTime', () => {
+ beforeEach(() => {
+ createComponent({ duration: 0, finishedTime: '' });
+ });
+
+ it('should not render time and calendar icon', () => {
+ expect(wrapper.find('.finished-at').exists()).toBe(false);
+ });
+ });
+});
diff --git a/spec/frontend/pipelines/tokens/pipeline_branch_name_token_spec.js b/spec/frontend/pipelines/tokens/pipeline_branch_name_token_spec.js
new file mode 100644
index 00000000000..a6753600792
--- /dev/null
+++ b/spec/frontend/pipelines/tokens/pipeline_branch_name_token_spec.js
@@ -0,0 +1,89 @@
+import { GlFilteredSearchToken, GlFilteredSearchSuggestion, GlLoadingIcon } from '@gitlab/ui';
+import { shallowMount } from '@vue/test-utils';
+import PipelineBranchNameToken from '~/pipelines/components/tokens/pipeline_branch_name_token.vue';
+import { branches } from '../mock_data';
+
+describe('Pipeline Branch Name Token', () => {
+ let wrapper;
+
+ const findFilteredSearchToken = () => wrapper.find(GlFilteredSearchToken);
+ const findAllFilteredSearchSuggestions = () => wrapper.findAll(GlFilteredSearchSuggestion);
+ const findLoadingIcon = () => wrapper.find(GlLoadingIcon);
+
+ const stubs = {
+ GlFilteredSearchToken: {
+ template: `<div><slot name="suggestions"></slot></div>`,
+ },
+ };
+
+ const defaultProps = {
+ config: {
+ type: 'ref',
+ icon: 'branch',
+ title: 'Branch name',
+ dataType: 'ref',
+ unique: true,
+ branches,
+ projectId: '21',
+ },
+ value: {
+ data: '',
+ },
+ };
+
+ const createComponent = (options, data) => {
+ wrapper = shallowMount(PipelineBranchNameToken, {
+ propsData: {
+ ...defaultProps,
+ },
+ data() {
+ return {
+ ...data,
+ };
+ },
+ ...options,
+ });
+ };
+
+ beforeEach(() => {
+ createComponent();
+ });
+
+ afterEach(() => {
+ wrapper.destroy();
+ wrapper = null;
+ });
+
+ it('passes config correctly', () => {
+ expect(findFilteredSearchToken().props('config')).toEqual(defaultProps.config);
+ });
+
+ describe('displays loading icon correctly', () => {
+ it('shows loading icon', () => {
+ createComponent({ stubs }, { loading: true });
+
+ expect(findLoadingIcon().exists()).toBe(true);
+ });
+
+ it('does not show loading icon', () => {
+ createComponent({ stubs }, { loading: false });
+
+ expect(findLoadingIcon().exists()).toBe(false);
+ });
+ });
+
+ describe('shows branches correctly', () => {
+ it('renders all trigger authors', () => {
+ createComponent({ stubs }, { branches, loading: false });
+
+ expect(findAllFilteredSearchSuggestions()).toHaveLength(branches.length);
+ });
+
+ it('renders only the branch searched for', () => {
+ const mockBranches = ['master'];
+ createComponent({ stubs }, { branches: mockBranches, loading: false });
+
+ expect(findAllFilteredSearchSuggestions()).toHaveLength(mockBranches.length);
+ });
+ });
+});
diff --git a/spec/frontend/pipelines/tokens/pipeline_trigger_author_token_spec.js b/spec/frontend/pipelines/tokens/pipeline_trigger_author_token_spec.js
new file mode 100644
index 00000000000..00a9ff04e75
--- /dev/null
+++ b/spec/frontend/pipelines/tokens/pipeline_trigger_author_token_spec.js
@@ -0,0 +1,98 @@
+import { GlFilteredSearchToken, GlFilteredSearchSuggestion, GlLoadingIcon } from '@gitlab/ui';
+import { shallowMount } from '@vue/test-utils';
+import PipelineTriggerAuthorToken from '~/pipelines/components/tokens/pipeline_trigger_author_token.vue';
+import { users } from '../mock_data';
+
+describe('Pipeline Trigger Author Token', () => {
+ let wrapper;
+
+ const findFilteredSearchToken = () => wrapper.find(GlFilteredSearchToken);
+ const findAllFilteredSearchSuggestions = () => wrapper.findAll(GlFilteredSearchSuggestion);
+ const findLoadingIcon = () => wrapper.find(GlLoadingIcon);
+
+ const stubs = {
+ GlFilteredSearchToken: {
+ template: `<div><slot name="suggestions"></slot></div>`,
+ },
+ };
+
+ const defaultProps = {
+ config: {
+ type: 'username',
+ icon: 'user',
+ title: 'Trigger author',
+ dataType: 'username',
+ unique: true,
+ triggerAuthors: users,
+ },
+ value: {
+ data: '',
+ },
+ };
+
+ const createComponent = (options, data) => {
+ wrapper = shallowMount(PipelineTriggerAuthorToken, {
+ propsData: {
+ ...defaultProps,
+ },
+ data() {
+ return {
+ ...data,
+ };
+ },
+ ...options,
+ });
+ };
+
+ beforeEach(() => {
+ createComponent();
+ });
+
+ afterEach(() => {
+ wrapper.destroy();
+ wrapper = null;
+ });
+
+ it('passes config correctly', () => {
+ expect(findFilteredSearchToken().props('config')).toEqual(defaultProps.config);
+ });
+
+ describe('displays loading icon correctly', () => {
+ it('shows loading icon', () => {
+ createComponent({ stubs }, { loading: true });
+
+ expect(findLoadingIcon().exists()).toBe(true);
+ });
+
+ it('does not show loading icon', () => {
+ createComponent({ stubs }, { loading: false });
+
+ expect(findLoadingIcon().exists()).toBe(false);
+ });
+ });
+
+ describe('shows trigger authors correctly', () => {
+ beforeEach(() => {});
+
+ it('renders all trigger authors', () => {
+ createComponent({ stubs }, { users, loading: false });
+
+ // should have length of all users plus the static 'Any' option
+ expect(findAllFilteredSearchSuggestions()).toHaveLength(users.length + 1);
+ });
+
+ it('renders only the trigger author searched for', () => {
+ createComponent(
+ { stubs },
+ {
+ users: [
+ { name: 'Arnold', username: 'admin', state: 'active', avatar_url: 'avatar-link' },
+ ],
+ loading: false,
+ },
+ );
+
+ expect(findAllFilteredSearchSuggestions()).toHaveLength(2);
+ });
+ });
+});
diff --git a/spec/javascripts/pipelines_spec.js b/spec/frontend/pipelines_spec.js
index 6d4d634c575..6d4d634c575 100644
--- a/spec/javascripts/pipelines_spec.js
+++ b/spec/frontend/pipelines_spec.js
diff --git a/spec/frontend/prometheus_metrics/custom_metrics_spec.js b/spec/frontend/prometheus_metrics/custom_metrics_spec.js
index 97b8f7bd913..1244d7342ad 100644
--- a/spec/frontend/prometheus_metrics/custom_metrics_spec.js
+++ b/spec/frontend/prometheus_metrics/custom_metrics_spec.js
@@ -2,7 +2,7 @@ import MockAdapter from 'axios-mock-adapter';
import CustomMetrics from '~/prometheus_metrics/custom_metrics';
import axios from '~/lib/utils/axios_utils';
import PANEL_STATE from '~/prometheus_metrics/constants';
-import metrics from './mock_data';
+import { metrics1 as metrics } from './mock_data';
describe('PrometheusMetrics', () => {
const FIXTURE = 'services/prometheus/prometheus_service.html';
diff --git a/spec/frontend/prometheus_metrics/mock_data.js b/spec/frontend/prometheus_metrics/mock_data.js
index d5532537302..375447ac3be 100644
--- a/spec/frontend/prometheus_metrics/mock_data.js
+++ b/spec/frontend/prometheus_metrics/mock_data.js
@@ -1,4 +1,4 @@
-const metrics = [
+export const metrics1 = [
{
edit_path: '/root/prometheus-test/prometheus/metrics/3/edit',
id: 3,
@@ -19,4 +19,44 @@ const metrics = [
},
];
-export default metrics;
+export const metrics2 = [
+ {
+ group: 'Kubernetes',
+ priority: 1,
+ active_metrics: 4,
+ metrics_missing_requirements: 0,
+ },
+ {
+ group: 'HAProxy',
+ priority: 2,
+ active_metrics: 3,
+ metrics_missing_requirements: 0,
+ },
+ {
+ group: 'Apache',
+ priority: 3,
+ active_metrics: 5,
+ metrics_missing_requirements: 0,
+ },
+];
+
+export const missingVarMetrics = [
+ {
+ group: 'Kubernetes',
+ priority: 1,
+ active_metrics: 4,
+ metrics_missing_requirements: 0,
+ },
+ {
+ group: 'HAProxy',
+ priority: 2,
+ active_metrics: 3,
+ metrics_missing_requirements: 1,
+ },
+ {
+ group: 'Apache',
+ priority: 3,
+ active_metrics: 5,
+ metrics_missing_requirements: 3,
+ },
+];
diff --git a/spec/frontend/prometheus_metrics/prometheus_metrics_spec.js b/spec/frontend/prometheus_metrics/prometheus_metrics_spec.js
new file mode 100644
index 00000000000..437a2116f5c
--- /dev/null
+++ b/spec/frontend/prometheus_metrics/prometheus_metrics_spec.js
@@ -0,0 +1,178 @@
+import MockAdapter from 'axios-mock-adapter';
+import axios from '~/lib/utils/axios_utils';
+import PrometheusMetrics from '~/prometheus_metrics/prometheus_metrics';
+import PANEL_STATE from '~/prometheus_metrics/constants';
+import { metrics2 as metrics, missingVarMetrics } from './mock_data';
+
+describe('PrometheusMetrics', () => {
+ const FIXTURE = 'services/prometheus/prometheus_service.html';
+ preloadFixtures(FIXTURE);
+
+ beforeEach(() => {
+ loadFixtures(FIXTURE);
+ });
+
+ describe('constructor', () => {
+ let prometheusMetrics;
+
+ beforeEach(() => {
+ prometheusMetrics = new PrometheusMetrics('.js-prometheus-metrics-monitoring');
+ });
+
+ it('should initialize wrapper element refs on class object', () => {
+ expect(prometheusMetrics.$wrapper).toBeDefined();
+ expect(prometheusMetrics.$monitoredMetricsPanel).toBeDefined();
+ expect(prometheusMetrics.$monitoredMetricsCount).toBeDefined();
+ expect(prometheusMetrics.$monitoredMetricsLoading).toBeDefined();
+ expect(prometheusMetrics.$monitoredMetricsEmpty).toBeDefined();
+ expect(prometheusMetrics.$monitoredMetricsList).toBeDefined();
+ expect(prometheusMetrics.$missingEnvVarPanel).toBeDefined();
+ expect(prometheusMetrics.$panelToggle).toBeDefined();
+ expect(prometheusMetrics.$missingEnvVarMetricCount).toBeDefined();
+ expect(prometheusMetrics.$missingEnvVarMetricsList).toBeDefined();
+ });
+
+ it('should initialize metadata on class object', () => {
+ expect(prometheusMetrics.backOffRequestCounter).toEqual(0);
+ expect(prometheusMetrics.activeMetricsEndpoint).toContain('/test');
+ });
+ });
+
+ describe('showMonitoringMetricsPanelState', () => {
+ let prometheusMetrics;
+
+ beforeEach(() => {
+ prometheusMetrics = new PrometheusMetrics('.js-prometheus-metrics-monitoring');
+ });
+
+ it('should show loading state when called with `loading`', () => {
+ prometheusMetrics.showMonitoringMetricsPanelState(PANEL_STATE.LOADING);
+
+ expect(prometheusMetrics.$monitoredMetricsLoading.hasClass('hidden')).toBeFalsy();
+ expect(prometheusMetrics.$monitoredMetricsEmpty.hasClass('hidden')).toBeTruthy();
+ expect(prometheusMetrics.$monitoredMetricsList.hasClass('hidden')).toBeTruthy();
+ });
+
+ it('should show metrics list when called with `list`', () => {
+ prometheusMetrics.showMonitoringMetricsPanelState(PANEL_STATE.LIST);
+
+ expect(prometheusMetrics.$monitoredMetricsLoading.hasClass('hidden')).toBeTruthy();
+ expect(prometheusMetrics.$monitoredMetricsEmpty.hasClass('hidden')).toBeTruthy();
+ expect(prometheusMetrics.$monitoredMetricsList.hasClass('hidden')).toBeFalsy();
+ });
+
+ it('should show empty state when called with `empty`', () => {
+ prometheusMetrics.showMonitoringMetricsPanelState(PANEL_STATE.EMPTY);
+
+ expect(prometheusMetrics.$monitoredMetricsLoading.hasClass('hidden')).toBeTruthy();
+ expect(prometheusMetrics.$monitoredMetricsEmpty.hasClass('hidden')).toBeFalsy();
+ expect(prometheusMetrics.$monitoredMetricsList.hasClass('hidden')).toBeTruthy();
+ });
+ });
+
+ describe('populateActiveMetrics', () => {
+ let prometheusMetrics;
+
+ beforeEach(() => {
+ prometheusMetrics = new PrometheusMetrics('.js-prometheus-metrics-monitoring');
+ });
+
+ it('should show monitored metrics list', () => {
+ prometheusMetrics.populateActiveMetrics(metrics);
+
+ const $metricsListLi = prometheusMetrics.$monitoredMetricsList.find('li');
+
+ expect(prometheusMetrics.$monitoredMetricsLoading.hasClass('hidden')).toBeTruthy();
+ expect(prometheusMetrics.$monitoredMetricsList.hasClass('hidden')).toBeFalsy();
+
+ expect(prometheusMetrics.$monitoredMetricsCount.text()).toEqual(
+ '3 exporters with 12 metrics were found',
+ );
+
+ expect($metricsListLi.length).toEqual(metrics.length);
+ expect(
+ $metricsListLi
+ .first()
+ .find('.badge')
+ .text(),
+ ).toEqual(`${metrics[0].active_metrics}`);
+ });
+
+ it('should show missing environment variables list', () => {
+ prometheusMetrics.populateActiveMetrics(missingVarMetrics);
+
+ expect(prometheusMetrics.$monitoredMetricsLoading.hasClass('hidden')).toBeTruthy();
+ expect(prometheusMetrics.$missingEnvVarPanel.hasClass('hidden')).toBeFalsy();
+
+ expect(prometheusMetrics.$missingEnvVarMetricCount.text()).toEqual('2');
+ expect(prometheusMetrics.$missingEnvVarPanel.find('li').length).toEqual(2);
+ expect(prometheusMetrics.$missingEnvVarPanel.find('.flash-container')).toBeDefined();
+ });
+ });
+
+ describe('loadActiveMetrics', () => {
+ let prometheusMetrics;
+ let mock;
+
+ function mockSuccess() {
+ mock.onGet(prometheusMetrics.activeMetricsEndpoint).reply(200, {
+ data: metrics,
+ success: true,
+ });
+ }
+
+ function mockError() {
+ mock.onGet(prometheusMetrics.activeMetricsEndpoint).networkError();
+ }
+
+ beforeEach(() => {
+ jest.spyOn(axios, 'get');
+
+ prometheusMetrics = new PrometheusMetrics('.js-prometheus-metrics-monitoring');
+
+ mock = new MockAdapter(axios);
+ });
+
+ afterEach(() => {
+ mock.restore();
+ });
+
+ it('should show loader animation while response is being loaded and hide it when request is complete', done => {
+ mockSuccess();
+
+ prometheusMetrics.loadActiveMetrics();
+
+ expect(prometheusMetrics.$monitoredMetricsLoading.hasClass('hidden')).toBeFalsy();
+ expect(axios.get).toHaveBeenCalledWith(prometheusMetrics.activeMetricsEndpoint);
+
+ setImmediate(() => {
+ expect(prometheusMetrics.$monitoredMetricsLoading.hasClass('hidden')).toBeTruthy();
+ done();
+ });
+ });
+
+ it('should show empty state if response failed to load', done => {
+ mockError();
+
+ prometheusMetrics.loadActiveMetrics();
+
+ setImmediate(() => {
+ expect(prometheusMetrics.$monitoredMetricsLoading.hasClass('hidden')).toBeTruthy();
+ expect(prometheusMetrics.$monitoredMetricsEmpty.hasClass('hidden')).toBeFalsy();
+ done();
+ });
+ });
+
+ it('should populate metrics list once response is loaded', done => {
+ jest.spyOn(prometheusMetrics, 'populateActiveMetrics').mockImplementation();
+ mockSuccess();
+
+ prometheusMetrics.loadActiveMetrics();
+
+ setImmediate(() => {
+ expect(prometheusMetrics.populateActiveMetrics).toHaveBeenCalledWith(metrics);
+ done();
+ });
+ });
+ });
+});
diff --git a/spec/frontend/registry/explorer/components/image_list_spec.js b/spec/frontend/registry/explorer/components/image_list_spec.js
new file mode 100644
index 00000000000..12f0fbe0c87
--- /dev/null
+++ b/spec/frontend/registry/explorer/components/image_list_spec.js
@@ -0,0 +1,74 @@
+import { shallowMount } from '@vue/test-utils';
+import { GlPagination } from '@gitlab/ui';
+import Component from '~/registry/explorer/components/image_list.vue';
+import ClipboardButton from '~/vue_shared/components/clipboard_button.vue';
+import { RouterLink } from '../stubs';
+import { imagesListResponse, imagePagination } from '../mock_data';
+
+describe('Image List', () => {
+ let wrapper;
+
+ const firstElement = imagesListResponse.data[0];
+
+ const findDeleteBtn = () => wrapper.find('[data-testid="deleteImageButton"]');
+ const findRowItems = () => wrapper.findAll('[data-testid="rowItem"]');
+ const findDetailsLink = () => wrapper.find('[data-testid="detailsLink"]');
+ const findClipboardButton = () => wrapper.find(ClipboardButton);
+ const findPagination = () => wrapper.find(GlPagination);
+
+ const mountComponent = () => {
+ wrapper = shallowMount(Component, {
+ stubs: {
+ RouterLink,
+ },
+ propsData: {
+ images: imagesListResponse.data,
+ pagination: imagePagination,
+ },
+ });
+ };
+
+ beforeEach(() => {
+ mountComponent();
+ });
+
+ it('contains one list element for each image', () => {
+ expect(findRowItems().length).toBe(imagesListResponse.data.length);
+ });
+
+ it('contains a link to the details page', () => {
+ const link = findDetailsLink();
+ expect(link.html()).toContain(firstElement.path);
+ expect(link.props('to').name).toBe('details');
+ });
+
+ it('contains a clipboard button', () => {
+ const button = findClipboardButton();
+ expect(button.exists()).toBe(true);
+ expect(button.props('text')).toBe(firstElement.location);
+ expect(button.props('title')).toBe(firstElement.location);
+ });
+
+ it('should be possible to delete a repo', () => {
+ const deleteBtn = findDeleteBtn();
+ expect(deleteBtn.exists()).toBe(true);
+ });
+
+ describe('pagination', () => {
+ it('exists', () => {
+ expect(findPagination().exists()).toBe(true);
+ });
+
+ it('is wired to the correct pagination props', () => {
+ const pagination = findPagination();
+ expect(pagination.props('perPage')).toBe(imagePagination.perPage);
+ expect(pagination.props('totalItems')).toBe(imagePagination.total);
+ expect(pagination.props('value')).toBe(imagePagination.page);
+ });
+
+ it('emits a pageChange event when the page change', () => {
+ wrapper.setData({ currentPage: 2 });
+ expect(wrapper.emitted('pageChange')).toEqual([[2]]);
+ });
+ });
+});
diff --git a/spec/frontend/registry/explorer/mock_data.js b/spec/frontend/registry/explorer/mock_data.js
index 2d8cd4e42bc..f6beccda9b1 100644
--- a/spec/frontend/registry/explorer/mock_data.js
+++ b/spec/frontend/registry/explorer/mock_data.js
@@ -87,3 +87,11 @@ export const tagsListResponse = {
],
headers,
};
+
+export const imagePagination = {
+ perPage: 10,
+ page: 1,
+ total: 14,
+ totalPages: 2,
+ nextPage: 2,
+};
diff --git a/spec/frontend/registry/explorer/pages/details_spec.js b/spec/frontend/registry/explorer/pages/details_spec.js
index 15aa5008413..93098403a28 100644
--- a/spec/frontend/registry/explorer/pages/details_spec.js
+++ b/spec/frontend/registry/explorer/pages/details_spec.js
@@ -1,15 +1,21 @@
import { mount } from '@vue/test-utils';
-import { GlTable, GlPagination, GlSkeletonLoader } from '@gitlab/ui';
+import { GlTable, GlPagination, GlSkeletonLoader, GlAlert, GlLink } from '@gitlab/ui';
import Tracking from '~/tracking';
import stubChildren from 'helpers/stub_children';
import component from '~/registry/explorer/pages/details.vue';
-import store from '~/registry/explorer/stores/';
-import { SET_MAIN_LOADING } from '~/registry/explorer/stores/mutation_types/';
+import { createStore } from '~/registry/explorer/stores/';
+import {
+ SET_MAIN_LOADING,
+ SET_INITIAL_STATE,
+ SET_TAGS_LIST_SUCCESS,
+ SET_TAGS_PAGINATION,
+} from '~/registry/explorer/stores/mutation_types/';
import {
DELETE_TAG_SUCCESS_MESSAGE,
DELETE_TAG_ERROR_MESSAGE,
DELETE_TAGS_SUCCESS_MESSAGE,
DELETE_TAGS_ERROR_MESSAGE,
+ ADMIN_GARBAGE_COLLECTION_TIP,
} from '~/registry/explorer/constants';
import { tagsListResponse } from '../mock_data';
import { GlModal } from '../stubs';
@@ -18,6 +24,7 @@ import { $toast } from '../../shared/mocks';
describe('Details Page', () => {
let wrapper;
let dispatchSpy;
+ let store;
const findDeleteModal = () => wrapper.find(GlModal);
const findPagination = () => wrapper.find(GlPagination);
@@ -30,6 +37,8 @@ describe('Details Page', () => {
const findAllCheckboxes = () => wrapper.findAll('.js-row-checkbox');
const findCheckedCheckboxes = () => findAllCheckboxes().filter(c => c.attributes('checked'));
const findFirsTagColumn = () => wrapper.find('.js-tag-column');
+ const findFirstTagNameText = () => wrapper.find('[data-testid="rowNameText"]');
+ const findAlert = () => wrapper.find(GlAlert);
const routeId = window.btoa(JSON.stringify({ name: 'foo', tags_path: 'bar' }));
@@ -55,13 +64,17 @@ describe('Details Page', () => {
};
beforeEach(() => {
+ store = createStore();
dispatchSpy = jest.spyOn(store, 'dispatch');
- store.dispatch('receiveTagsListSuccess', tagsListResponse);
+ dispatchSpy.mockResolvedValue();
+ store.commit(SET_TAGS_LIST_SUCCESS, tagsListResponse.data);
+ store.commit(SET_TAGS_PAGINATION, tagsListResponse.headers);
jest.spyOn(Tracking, 'event');
});
afterEach(() => {
wrapper.destroy();
+ wrapper = null;
});
describe('when isLoading is true', () => {
@@ -130,10 +143,6 @@ describe('Details Page', () => {
});
describe('row checkbox', () => {
- beforeEach(() => {
- mountComponent();
- });
-
it('if selected adds item to selectedItems', () => {
findFirstRowItem('rowCheckbox').vm.$emit('change');
return wrapper.vm.$nextTick().then(() => {
@@ -240,15 +249,24 @@ describe('Details Page', () => {
});
});
- describe('tag cell', () => {
+ describe('name cell', () => {
+ it('tag column has a tooltip with the tag name', () => {
+ mountComponent();
+ expect(findFirstTagNameText().attributes('title')).toBe(tagsListResponse.data[0].name);
+ });
+
describe('on desktop viewport', () => {
beforeEach(() => {
mountComponent();
});
- it('has class w-25', () => {
+ it('table header has class w-25', () => {
expect(findFirsTagColumn().classes()).toContain('w-25');
});
+
+ it('tag column has the mw-m class', () => {
+ expect(findFirstRowItem('rowName').classes()).toContain('mw-m');
+ });
});
describe('on mobile viewport', () => {
@@ -260,9 +278,28 @@ describe('Details Page', () => {
});
});
- it('does not has class w-25', () => {
+ it('table header does not have class w-25', () => {
expect(findFirsTagColumn().classes()).not.toContain('w-25');
});
+
+ it('tag column has the gl-justify-content-end class', () => {
+ expect(findFirstRowItem('rowName').classes()).toContain('gl-justify-content-end');
+ });
+ });
+ });
+
+ describe('last updated cell', () => {
+ let timeCell;
+
+ beforeEach(() => {
+ timeCell = findFirstRowItem('rowTime');
+ });
+
+ it('displays the time in string format', () => {
+ expect(timeCell.text()).toBe('2 years ago');
+ });
+ it('has a tooltip timestamp', () => {
+ expect(timeCell.attributes('title')).toBe('Sep 19, 2017 1:45pm GMT+0000');
});
});
});
@@ -328,25 +365,9 @@ describe('Details Page', () => {
});
// itemsToBeDeleted is not represented in the DOM, is used as parking variable between selected and deleted items
expect(wrapper.vm.itemsToBeDeleted).toEqual([]);
+ expect(wrapper.vm.selectedItems).toEqual([]);
expect(findCheckedCheckboxes()).toHaveLength(0);
});
-
- it('show success toast on successful delete', () => {
- return wrapper.vm.handleSingleDelete(0).then(() => {
- expect(wrapper.vm.$toast.show).toHaveBeenCalledWith(DELETE_TAG_SUCCESS_MESSAGE, {
- type: 'success',
- });
- });
- });
-
- it('show error toast on erred delete', () => {
- dispatchSpy.mockRejectedValue();
- return wrapper.vm.handleSingleDelete(0).then(() => {
- expect(wrapper.vm.$toast.show).toHaveBeenCalledWith(DELETE_TAG_ERROR_MESSAGE, {
- type: 'error',
- });
- });
- });
});
describe('when multiple elements are selected', () => {
@@ -365,23 +386,6 @@ describe('Details Page', () => {
expect(wrapper.vm.itemsToBeDeleted).toEqual([]);
expect(findCheckedCheckboxes()).toHaveLength(0);
});
-
- it('show success toast on successful delete', () => {
- return wrapper.vm.handleMultipleDelete(0).then(() => {
- expect(wrapper.vm.$toast.show).toHaveBeenCalledWith(DELETE_TAGS_SUCCESS_MESSAGE, {
- type: 'success',
- });
- });
- });
-
- it('show error toast on erred delete', () => {
- dispatchSpy.mockRejectedValue();
- return wrapper.vm.handleMultipleDelete(0).then(() => {
- expect(wrapper.vm.$toast.show).toHaveBeenCalledWith(DELETE_TAGS_ERROR_MESSAGE, {
- type: 'error',
- });
- });
- });
});
});
@@ -395,4 +399,108 @@ describe('Details Page', () => {
});
});
});
+
+ describe('Delete alert', () => {
+ const config = {
+ garbageCollectionHelpPagePath: 'foo',
+ };
+
+ describe('when the user is an admin', () => {
+ beforeEach(() => {
+ store.commit(SET_INITIAL_STATE, { ...config, isAdmin: true });
+ });
+
+ afterEach(() => {
+ store.commit(SET_INITIAL_STATE, config);
+ });
+
+ describe.each`
+ deleteType | successTitle | errorTitle
+ ${'handleSingleDelete'} | ${DELETE_TAG_SUCCESS_MESSAGE} | ${DELETE_TAG_ERROR_MESSAGE}
+ ${'handleMultipleDelete'} | ${DELETE_TAGS_SUCCESS_MESSAGE} | ${DELETE_TAGS_ERROR_MESSAGE}
+ `('behaves correctly on $deleteType', ({ deleteType, successTitle, errorTitle }) => {
+ describe('when delete is successful', () => {
+ beforeEach(() => {
+ dispatchSpy.mockResolvedValue();
+ mountComponent();
+ return wrapper.vm[deleteType]('foo');
+ });
+
+ it('alert exists', () => {
+ expect(findAlert().exists()).toBe(true);
+ });
+
+ it('alert body contains admin tip', () => {
+ expect(
+ findAlert()
+ .text()
+ .replace(/\s\s+/gm, ' '),
+ ).toBe(ADMIN_GARBAGE_COLLECTION_TIP.replace(/%{\w+}/gm, ''));
+ });
+
+ it('alert body contains link', () => {
+ const alertLink = findAlert().find(GlLink);
+ expect(alertLink.exists()).toBe(true);
+ expect(alertLink.attributes('href')).toBe(config.garbageCollectionHelpPagePath);
+ });
+
+ it('alert title is appropriate', () => {
+ expect(findAlert().attributes('title')).toBe(successTitle);
+ });
+ });
+
+ describe('when delete is not successful', () => {
+ beforeEach(() => {
+ mountComponent();
+ dispatchSpy.mockRejectedValue();
+ return wrapper.vm[deleteType]('foo');
+ });
+
+ it('alert exist and text is appropriate', () => {
+ expect(findAlert().exists()).toBe(true);
+ expect(findAlert().text()).toBe(errorTitle);
+ });
+ });
+ });
+ });
+
+ describe.each`
+ deleteType | successTitle | errorTitle
+ ${'handleSingleDelete'} | ${DELETE_TAG_SUCCESS_MESSAGE} | ${DELETE_TAG_ERROR_MESSAGE}
+ ${'handleMultipleDelete'} | ${DELETE_TAGS_SUCCESS_MESSAGE} | ${DELETE_TAGS_ERROR_MESSAGE}
+ `(
+ 'when the user is not an admin alert behaves correctly on $deleteType',
+ ({ deleteType, successTitle, errorTitle }) => {
+ beforeEach(() => {
+ store.commit('SET_INITIAL_STATE', { ...config });
+ });
+
+ describe('when delete is successful', () => {
+ beforeEach(() => {
+ dispatchSpy.mockResolvedValue();
+ mountComponent();
+ return wrapper.vm[deleteType]('foo');
+ });
+
+ it('alert exist and text is appropriate', () => {
+ expect(findAlert().exists()).toBe(true);
+ expect(findAlert().text()).toBe(successTitle);
+ });
+ });
+
+ describe('when delete is not successful', () => {
+ beforeEach(() => {
+ mountComponent();
+ dispatchSpy.mockRejectedValue();
+ return wrapper.vm[deleteType]('foo');
+ });
+
+ it('alert exist and text is appropriate', () => {
+ expect(findAlert().exists()).toBe(true);
+ expect(findAlert().text()).toBe(errorTitle);
+ });
+ });
+ },
+ );
+ });
});
diff --git a/spec/frontend/registry/explorer/pages/index_spec.js b/spec/frontend/registry/explorer/pages/index_spec.js
index f52e7d67866..b558727ed5e 100644
--- a/spec/frontend/registry/explorer/pages/index_spec.js
+++ b/spec/frontend/registry/explorer/pages/index_spec.js
@@ -1,62 +1,26 @@
import { shallowMount } from '@vue/test-utils';
-import { GlAlert, GlSprintf, GlLink } from '@gitlab/ui';
import component from '~/registry/explorer/pages/index.vue';
import store from '~/registry/explorer/stores/';
describe('List Page', () => {
let wrapper;
- let dispatchSpy;
const findRouterView = () => wrapper.find({ ref: 'router-view' });
- const findAlert = () => wrapper.find(GlAlert);
- const findLink = () => wrapper.find(GlLink);
const mountComponent = () => {
wrapper = shallowMount(component, {
store,
stubs: {
RouterView: true,
- GlSprintf,
},
});
};
beforeEach(() => {
- dispatchSpy = jest.spyOn(store, 'dispatch');
mountComponent();
});
it('has a router view', () => {
expect(findRouterView().exists()).toBe(true);
});
-
- describe('garbageCollectionTip alert', () => {
- beforeEach(() => {
- store.dispatch('setInitialState', { isAdmin: true, garbageCollectionHelpPagePath: 'foo' });
- store.dispatch('setShowGarbageCollectionTip', true);
- });
-
- afterEach(() => {
- store.dispatch('setInitialState', {});
- store.dispatch('setShowGarbageCollectionTip', false);
- });
-
- it('is visible when the user is an admin and the user performed a delete action', () => {
- expect(findAlert().exists()).toBe(true);
- });
-
- it('on dismiss disappears ', () => {
- findAlert().vm.$emit('dismiss');
- expect(dispatchSpy).toHaveBeenCalledWith('setShowGarbageCollectionTip', false);
- return wrapper.vm.$nextTick().then(() => {
- expect(findAlert().exists()).toBe(false);
- });
- });
-
- it('contains a link to the docs', () => {
- const link = findLink();
- expect(link.exists()).toBe(true);
- expect(link.attributes('href')).toBe(store.state.config.garbageCollectionHelpPagePath);
- });
- });
});
diff --git a/spec/frontend/registry/explorer/pages/list_spec.js b/spec/frontend/registry/explorer/pages/list_spec.js
index f69b849521d..97742b9e9b3 100644
--- a/spec/frontend/registry/explorer/pages/list_spec.js
+++ b/spec/frontend/registry/explorer/pages/list_spec.js
@@ -1,47 +1,53 @@
-import VueRouter from 'vue-router';
-import { shallowMount, createLocalVue } from '@vue/test-utils';
-import { GlPagination, GlSkeletonLoader, GlSprintf, GlAlert } from '@gitlab/ui';
+import { shallowMount } from '@vue/test-utils';
+import { GlSkeletonLoader, GlSprintf, GlAlert, GlSearchBoxByClick } from '@gitlab/ui';
import Tracking from '~/tracking';
+import waitForPromises from 'helpers/wait_for_promises';
import component from '~/registry/explorer/pages/list.vue';
import QuickstartDropdown from '~/registry/explorer/components/quickstart_dropdown.vue';
import GroupEmptyState from '~/registry/explorer/components/group_empty_state.vue';
import ProjectEmptyState from '~/registry/explorer/components/project_empty_state.vue';
import ProjectPolicyAlert from '~/registry/explorer/components/project_policy_alert.vue';
-import store from '~/registry/explorer/stores/';
-import { SET_MAIN_LOADING } from '~/registry/explorer/stores/mutation_types/';
+import ImageList from '~/registry/explorer/components/image_list.vue';
+import { createStore } from '~/registry/explorer/stores/';
+import {
+ SET_MAIN_LOADING,
+ SET_IMAGES_LIST_SUCCESS,
+ SET_PAGINATION,
+ SET_INITIAL_STATE,
+} from '~/registry/explorer/stores/mutation_types/';
import {
DELETE_IMAGE_SUCCESS_MESSAGE,
DELETE_IMAGE_ERROR_MESSAGE,
+ IMAGE_REPOSITORY_LIST_LABEL,
+ SEARCH_PLACEHOLDER_TEXT,
} from '~/registry/explorer/constants';
import { imagesListResponse } from '../mock_data';
import { GlModal, GlEmptyState } from '../stubs';
import { $toast } from '../../shared/mocks';
-const localVue = createLocalVue();
-localVue.use(VueRouter);
-
describe('List Page', () => {
let wrapper;
let dispatchSpy;
+ let store;
- const findDeleteBtn = () => wrapper.find({ ref: 'deleteImageButton' });
const findDeleteModal = () => wrapper.find(GlModal);
const findSkeletonLoader = () => wrapper.find(GlSkeletonLoader);
const findImagesList = () => wrapper.find({ ref: 'imagesList' });
- const findRowItems = () => wrapper.findAll({ ref: 'rowItem' });
+
const findEmptyState = () => wrapper.find(GlEmptyState);
- const findDetailsLink = () => wrapper.find({ ref: 'detailsLink' });
- const findClipboardButton = () => wrapper.find({ ref: 'clipboardButton' });
- const findPagination = () => wrapper.find(GlPagination);
+
const findQuickStartDropdown = () => wrapper.find(QuickstartDropdown);
const findProjectEmptyState = () => wrapper.find(ProjectEmptyState);
const findGroupEmptyState = () => wrapper.find(GroupEmptyState);
const findProjectPolicyAlert = () => wrapper.find(ProjectPolicyAlert);
const findDeleteAlert = () => wrapper.find(GlAlert);
+ const findImageList = () => wrapper.find(ImageList);
+ const findListHeader = () => wrapper.find('[data-testid="listHeader"]');
+ const findSearchBox = () => wrapper.find(GlSearchBoxByClick);
+ const findEmptySearchMessage = () => wrapper.find('[data-testid="emptySearch"]');
- beforeEach(() => {
+ const mountComponent = ({ mocks } = {}) => {
wrapper = shallowMount(component, {
- localVue,
store,
stubs: {
GlModal,
@@ -50,10 +56,20 @@ describe('List Page', () => {
},
mocks: {
$toast,
+ $route: {
+ name: 'foo',
+ },
+ ...mocks,
},
});
+ };
+
+ beforeEach(() => {
+ store = createStore();
dispatchSpy = jest.spyOn(store, 'dispatch');
- store.dispatch('receiveImagesListSuccess', imagesListResponse);
+ dispatchSpy.mockResolvedValue();
+ store.commit(SET_IMAGES_LIST_SUCCESS, imagesListResponse.data);
+ store.commit(SET_PAGINATION, imagesListResponse.headers);
});
afterEach(() => {
@@ -61,17 +77,38 @@ describe('List Page', () => {
});
describe('Expiration policy notification', () => {
+ beforeEach(() => {
+ mountComponent();
+ });
it('shows up on project page', () => {
expect(findProjectPolicyAlert().exists()).toBe(true);
});
it('does show up on group page', () => {
- store.dispatch('setInitialState', { isGroupPage: true });
+ store.commit(SET_INITIAL_STATE, { isGroupPage: true });
return wrapper.vm.$nextTick().then(() => {
expect(findProjectPolicyAlert().exists()).toBe(false);
});
});
});
+ describe('API calls', () => {
+ it.each`
+ imageList | name | called
+ ${[]} | ${'foo'} | ${['requestImagesList']}
+ ${imagesListResponse.data} | ${undefined} | ${['requestImagesList']}
+ ${imagesListResponse.data} | ${'foo'} | ${undefined}
+ `(
+ 'with images equal $imageList and name $name dispatch calls $called',
+ ({ imageList, name, called }) => {
+ store.commit(SET_IMAGES_LIST_SUCCESS, imageList);
+ dispatchSpy.mockClear();
+ mountComponent({ mocks: { $route: { name } } });
+
+ expect(dispatchSpy.mock.calls[0]).toEqual(called);
+ },
+ );
+ });
+
describe('connection error', () => {
const config = {
characterError: true,
@@ -79,12 +116,13 @@ describe('List Page', () => {
helpPagePath: 'bar',
};
- beforeAll(() => {
- store.dispatch('setInitialState', config);
+ beforeEach(() => {
+ store.commit(SET_INITIAL_STATE, config);
+ mountComponent();
});
- afterAll(() => {
- store.dispatch('setInitialState', {});
+ afterEach(() => {
+ store.commit(SET_INITIAL_STATE, {});
});
it('should show an empty state', () => {
@@ -106,9 +144,12 @@ describe('List Page', () => {
});
describe('isLoading is true', () => {
- beforeAll(() => store.commit(SET_MAIN_LOADING, true));
+ beforeEach(() => {
+ store.commit(SET_MAIN_LOADING, true);
+ mountComponent();
+ });
- afterAll(() => store.commit(SET_MAIN_LOADING, false));
+ afterEach(() => store.commit(SET_MAIN_LOADING, false));
it('shows the skeleton loader', () => {
expect(findSkeletonLoader().exists()).toBe(true);
@@ -125,7 +166,9 @@ describe('List Page', () => {
describe('list is empty', () => {
beforeEach(() => {
- store.dispatch('receiveImagesListSuccess', { data: [] });
+ store.commit(SET_IMAGES_LIST_SUCCESS, []);
+ mountComponent();
+ return waitForPromises();
});
it('quick start is not visible', () => {
@@ -137,12 +180,13 @@ describe('List Page', () => {
});
describe('is group page is true', () => {
- beforeAll(() => {
- store.dispatch('setInitialState', { isGroupPage: true });
+ beforeEach(() => {
+ store.commit(SET_INITIAL_STATE, { isGroupPage: true });
+ mountComponent();
});
- afterAll(() => {
- store.dispatch('setInitialState', { isGroupPage: undefined });
+ afterEach(() => {
+ store.commit(SET_INITIAL_STATE, { isGroupPage: undefined });
});
it('group empty state is visible', () => {
@@ -152,50 +196,39 @@ describe('List Page', () => {
it('quick start is not visible', () => {
expect(findQuickStartDropdown().exists()).toBe(false);
});
+
+ it('list header is not visible', () => {
+ expect(findListHeader().exists()).toBe(false);
+ });
});
});
describe('list is not empty', () => {
- it('quick start is visible', () => {
- expect(findQuickStartDropdown().exists()).toBe(true);
- });
-
- describe('listElement', () => {
- let listElements;
- let firstElement;
-
+ describe('unfiltered state', () => {
beforeEach(() => {
- listElements = findRowItems();
- [firstElement] = store.state.images;
+ mountComponent();
});
- it('contains one list element for each image', () => {
- expect(listElements.length).toBe(store.state.images.length);
+ it('quick start is visible', () => {
+ expect(findQuickStartDropdown().exists()).toBe(true);
});
- it('contains a link to the details page', () => {
- const link = findDetailsLink();
- expect(link.html()).toContain(firstElement.path);
- expect(link.props('to').name).toBe('details');
+ it('list component is visible', () => {
+ expect(findImageList().exists()).toBe(true);
});
- it('contains a clipboard button', () => {
- const button = findClipboardButton();
- expect(button.exists()).toBe(true);
- expect(button.props('text')).toBe(firstElement.location);
- expect(button.props('title')).toBe(firstElement.location);
+ it('list header is visible', () => {
+ const header = findListHeader();
+ expect(header.exists()).toBe(true);
+ expect(header.text()).toBe(IMAGE_REPOSITORY_LIST_LABEL);
});
describe('delete image', () => {
- it('should be possible to delete a repo', () => {
- const deleteBtn = findDeleteBtn();
- expect(deleteBtn.exists()).toBe(true);
- });
-
+ const itemToDelete = { path: 'bar' };
it('should call deleteItem when confirming deletion', () => {
dispatchSpy.mockResolvedValue();
- findDeleteBtn().vm.$emit('click');
- expect(wrapper.vm.itemToDelete).not.toEqual({});
+ findImageList().vm.$emit('delete', itemToDelete);
+ expect(wrapper.vm.itemToDelete).toEqual(itemToDelete);
findDeleteModal().vm.$emit('ok');
expect(store.dispatch).toHaveBeenCalledWith(
'requestDeleteImage',
@@ -205,8 +238,8 @@ describe('List Page', () => {
it('should show a success alert when delete request is successful', () => {
dispatchSpy.mockResolvedValue();
- findDeleteBtn().vm.$emit('click');
- expect(wrapper.vm.itemToDelete).not.toEqual({});
+ findImageList().vm.$emit('delete', itemToDelete);
+ expect(wrapper.vm.itemToDelete).toEqual(itemToDelete);
return wrapper.vm.handleDeleteImage().then(() => {
const alert = findDeleteAlert();
expect(alert.exists()).toBe(true);
@@ -218,8 +251,8 @@ describe('List Page', () => {
it('should show an error alert when delete request fails', () => {
dispatchSpy.mockRejectedValue();
- findDeleteBtn().vm.$emit('click');
- expect(wrapper.vm.itemToDelete).not.toEqual({});
+ findImageList().vm.$emit('delete', itemToDelete);
+ expect(wrapper.vm.itemToDelete).toEqual(itemToDelete);
return wrapper.vm.handleDeleteImage().then(() => {
const alert = findDeleteAlert();
expect(alert.exists()).toBe(true);
@@ -229,71 +262,93 @@ describe('List Page', () => {
});
});
});
+ });
- describe('pagination', () => {
- it('exists', () => {
- expect(findPagination().exists()).toBe(true);
- });
+ describe('search', () => {
+ it('has a search box element', () => {
+ mountComponent();
+ const searchBox = findSearchBox();
+ expect(searchBox.exists()).toBe(true);
+ expect(searchBox.attributes('placeholder')).toBe(SEARCH_PLACEHOLDER_TEXT);
+ });
- it('is wired to the correct pagination props', () => {
- const pagination = findPagination();
- expect(pagination.props('perPage')).toBe(store.state.pagination.perPage);
- expect(pagination.props('totalItems')).toBe(store.state.pagination.total);
- expect(pagination.props('value')).toBe(store.state.pagination.page);
+ it('performs a search', () => {
+ mountComponent();
+ findSearchBox().vm.$emit('submit', 'foo');
+ expect(store.dispatch).toHaveBeenCalledWith('requestImagesList', {
+ name: 'foo',
});
+ });
- it('fetch the data from the API when the v-model changes', () => {
- dispatchSpy.mockReturnValue();
- wrapper.setData({ currentPage: 2 });
- return wrapper.vm.$nextTick().then(() => {
- expect(store.dispatch).toHaveBeenCalledWith('requestImagesList', { page: 2 });
- });
+ it('when search result is empty displays an empty search message', () => {
+ mountComponent();
+ store.commit(SET_IMAGES_LIST_SUCCESS, []);
+ return wrapper.vm.$nextTick().then(() => {
+ expect(findEmptySearchMessage().exists()).toBe(true);
});
});
});
- describe('modal', () => {
- it('exists', () => {
- expect(findDeleteModal().exists()).toBe(true);
- });
-
- it('contains a description with the path of the item to delete', () => {
- wrapper.setData({ itemToDelete: { path: 'foo' } });
- return wrapper.vm.$nextTick().then(() => {
- expect(findDeleteModal().html()).toContain('foo');
+ describe('pagination', () => {
+ it('pageChange event triggers the appropriate store function', () => {
+ mountComponent();
+ findImageList().vm.$emit('pageChange', 2);
+ expect(store.dispatch).toHaveBeenCalledWith('requestImagesList', {
+ pagination: { page: 2 },
+ name: wrapper.vm.search,
});
});
});
+ });
- describe('tracking', () => {
- const testTrackingCall = action => {
- expect(Tracking.event).toHaveBeenCalledWith(undefined, action, {
- label: 'registry_repository_delete',
- });
- };
+ describe('modal', () => {
+ beforeEach(() => {
+ mountComponent();
+ });
- beforeEach(() => {
- jest.spyOn(Tracking, 'event');
- dispatchSpy.mockResolvedValue();
- });
+ it('exists', () => {
+ expect(findDeleteModal().exists()).toBe(true);
+ });
- it('send an event when delete button is clicked', () => {
- const deleteBtn = findDeleteBtn();
- deleteBtn.vm.$emit('click');
- testTrackingCall('click_button');
+ it('contains a description with the path of the item to delete', () => {
+ wrapper.setData({ itemToDelete: { path: 'foo' } });
+ return wrapper.vm.$nextTick().then(() => {
+ expect(findDeleteModal().html()).toContain('foo');
});
+ });
+ });
- it('send an event when cancel is pressed on modal', () => {
- const deleteModal = findDeleteModal();
- deleteModal.vm.$emit('cancel');
- testTrackingCall('cancel_delete');
- });
+ describe('tracking', () => {
+ beforeEach(() => {
+ mountComponent();
+ });
- it('send an event when confirm is clicked on modal', () => {
- const deleteModal = findDeleteModal();
- deleteModal.vm.$emit('ok');
- testTrackingCall('confirm_delete');
+ const testTrackingCall = action => {
+ expect(Tracking.event).toHaveBeenCalledWith(undefined, action, {
+ label: 'registry_repository_delete',
});
+ };
+
+ beforeEach(() => {
+ jest.spyOn(Tracking, 'event');
+ dispatchSpy.mockResolvedValue();
+ });
+
+ it('send an event when delete button is clicked', () => {
+ findImageList().vm.$emit('delete', {});
+ testTrackingCall('click_button');
+ });
+
+ it('send an event when cancel is pressed on modal', () => {
+ const deleteModal = findDeleteModal();
+ deleteModal.vm.$emit('cancel');
+ testTrackingCall('cancel_delete');
+ });
+
+ it('send an event when confirm is clicked on modal', () => {
+ const deleteModal = findDeleteModal();
+ deleteModal.vm.$emit('ok');
+ testTrackingCall('confirm_delete');
});
});
});
diff --git a/spec/frontend/registry/explorer/stores/actions_spec.js b/spec/frontend/registry/explorer/stores/actions_spec.js
index 58f61a0e8c2..15f9db90910 100644
--- a/spec/frontend/registry/explorer/stores/actions_spec.js
+++ b/spec/frontend/registry/explorer/stores/actions_spec.js
@@ -191,7 +191,10 @@ describe('Actions RegistryExplorer Store', () => {
{
tagsPagination: {},
},
- [{ type: types.SET_MAIN_LOADING, payload: true }],
+ [
+ { type: types.SET_MAIN_LOADING, payload: true },
+ { type: types.SET_MAIN_LOADING, payload: false },
+ ],
[
{
type: 'setShowGarbageCollectionTip',
@@ -220,8 +223,7 @@ describe('Actions RegistryExplorer Store', () => {
{ type: types.SET_MAIN_LOADING, payload: false },
],
[],
- done,
- );
+ ).catch(() => done());
});
});
@@ -241,7 +243,10 @@ describe('Actions RegistryExplorer Store', () => {
{
tagsPagination: {},
},
- [{ type: types.SET_MAIN_LOADING, payload: true }],
+ [
+ { type: types.SET_MAIN_LOADING, payload: true },
+ { type: types.SET_MAIN_LOADING, payload: false },
+ ],
[
{
type: 'setShowGarbageCollectionTip',
@@ -273,8 +278,7 @@ describe('Actions RegistryExplorer Store', () => {
{ type: types.SET_MAIN_LOADING, payload: false },
],
[],
- done,
- );
+ ).catch(() => done());
});
});
@@ -311,9 +315,7 @@ describe('Actions RegistryExplorer Store', () => {
{ type: types.SET_MAIN_LOADING, payload: false },
],
[],
- ).catch(() => {
- done();
- });
+ ).catch(() => done());
});
});
});
diff --git a/spec/frontend/registry/explorer/stubs.js b/spec/frontend/registry/explorer/stubs.js
index 2c2c7587af9..0e178abfbed 100644
--- a/spec/frontend/registry/explorer/stubs.js
+++ b/spec/frontend/registry/explorer/stubs.js
@@ -9,3 +9,8 @@ export const GlEmptyState = {
template: '<div><slot name="description"></slot></div>',
name: 'GlEmptyStateSTub',
};
+
+export const RouterLink = {
+ template: `<div><slot></slot></div>`,
+ props: ['to'],
+};
diff --git a/spec/frontend/registry/settings/store/getters_spec.js b/spec/frontend/registry/settings/store/getters_spec.js
index 944057ebc9f..b781d09466c 100644
--- a/spec/frontend/registry/settings/store/getters_spec.js
+++ b/spec/frontend/registry/settings/store/getters_spec.js
@@ -4,9 +4,12 @@ import { formOptions } from '../../shared/mock_data';
describe('Getters registry settings store', () => {
const settings = {
+ enabled: true,
cadence: 'foo',
keep_n: 'bar',
older_than: 'baz',
+ name_regex: 'name-foo',
+ name_regex_keep: 'name-keep-bar',
};
describe.each`
@@ -29,6 +32,17 @@ describe('Getters registry settings store', () => {
});
});
+ describe('getSettings', () => {
+ it('returns the content of settings', () => {
+ const computedGetters = {
+ getCadence: settings.cadence,
+ getOlderThan: settings.older_than,
+ getKeepN: settings.keep_n,
+ };
+ expect(getters.getSettings({ settings }, computedGetters)).toEqual(settings);
+ });
+ });
+
describe('getIsEdited', () => {
it('returns false when original is equal to settings', () => {
const same = { foo: 'bar' };
diff --git a/spec/frontend/registry/shared/components/__snapshots__/expiration_policy_fields_spec.js.snap b/spec/frontend/registry/shared/components/__snapshots__/expiration_policy_fields_spec.js.snap
index 6e7bc0491ce..a9034b81d2f 100644
--- a/spec/frontend/registry/shared/components/__snapshots__/expiration_policy_fields_spec.js.snap
+++ b/spec/frontend/registry/shared/components/__snapshots__/expiration_policy_fields_spec.js.snap
@@ -117,11 +117,11 @@ exports[`Expiration Policy Form renders 1`] = `
<gl-form-group-stub
id="expiration-policy-name-matching-group"
invalid-feedback="The value of this input should be less than 255 characters"
- label="Docker tags with names matching this regex pattern will expire:"
label-align="right"
label-cols="3"
label-for="expiration-policy-name-matching"
>
+
<gl-form-textarea-stub
disabled="true"
id="expiration-policy-name-matching"
@@ -130,5 +130,21 @@ exports[`Expiration Policy Form renders 1`] = `
value=""
/>
</gl-form-group-stub>
+ <gl-form-group-stub
+ id="expiration-policy-keep-name-group"
+ invalid-feedback="The value of this input should be less than 255 characters"
+ label-align="right"
+ label-cols="3"
+ label-for="expiration-policy-keep-name"
+ >
+
+ <gl-form-textarea-stub
+ disabled="true"
+ id="expiration-policy-keep-name"
+ placeholder=""
+ trim=""
+ value=""
+ />
+ </gl-form-group-stub>
</div>
`;
diff --git a/spec/frontend/registry/shared/components/expiration_policy_fields_spec.js b/spec/frontend/registry/shared/components/expiration_policy_fields_spec.js
index 3782bfeaac4..4825351a6d3 100644
--- a/spec/frontend/registry/shared/components/expiration_policy_fields_spec.js
+++ b/spec/frontend/registry/shared/components/expiration_policy_fields_spec.js
@@ -40,12 +40,13 @@ describe('Expiration Policy Form', () => {
});
describe.each`
- elementName | modelName | value | disabledByToggle
- ${'toggle'} | ${'enabled'} | ${true} | ${'not disabled'}
- ${'interval'} | ${'older_than'} | ${'foo'} | ${'disabled'}
- ${'schedule'} | ${'cadence'} | ${'foo'} | ${'disabled'}
- ${'latest'} | ${'keep_n'} | ${'foo'} | ${'disabled'}
- ${'name-matching'} | ${'name_regex'} | ${'foo'} | ${'disabled'}
+ elementName | modelName | value | disabledByToggle
+ ${'toggle'} | ${'enabled'} | ${true} | ${'not disabled'}
+ ${'interval'} | ${'older_than'} | ${'foo'} | ${'disabled'}
+ ${'schedule'} | ${'cadence'} | ${'foo'} | ${'disabled'}
+ ${'latest'} | ${'keep_n'} | ${'foo'} | ${'disabled'}
+ ${'name-matching'} | ${'name_regex'} | ${'foo'} | ${'disabled'}
+ ${'keep-name'} | ${'name_regex_keep'} | ${'bar'} | ${'disabled'}
`(
`${FORM_ELEMENTS_ID_PREFIX}-$elementName form element`,
({ elementName, modelName, value, disabledByToggle }) => {
@@ -118,21 +119,26 @@ describe('Expiration Policy Form', () => {
${'schedule'}
${'latest'}
${'name-matching'}
+ ${'keep-name'}
`(`${FORM_ELEMENTS_ID_PREFIX}-$elementName is disabled`, ({ elementName }) => {
expect(findFormElements(elementName).attributes('disabled')).toBe('true');
});
});
- describe('form validation', () => {
+ describe.each`
+ modelName | elementName | stateVariable
+ ${'name_regex'} | ${'name-matching'} | ${'nameRegexState'}
+ ${'name_regex_keep'} | ${'keep-name'} | ${'nameKeepRegexState'}
+ `('regex textarea validation', ({ modelName, elementName, stateVariable }) => {
describe(`when name regex is longer than ${NAME_REGEX_LENGTH}`, () => {
const invalidString = new Array(NAME_REGEX_LENGTH + 2).join(',');
beforeEach(() => {
- mountComponent({ value: { name_regex: invalidString } });
+ mountComponent({ value: { [modelName]: invalidString } });
});
- it('nameRegexState is false', () => {
- expect(wrapper.vm.nameRegexState).toBe(false);
+ it(`${stateVariable} is false`, () => {
+ expect(wrapper.vm.textAreaState[stateVariable]).toBe(false);
});
it('emit the @invalidated event', () => {
@@ -141,17 +147,20 @@ describe('Expiration Policy Form', () => {
});
it('if the user did not type validation is null', () => {
- mountComponent({ value: { name_regex: '' } });
+ mountComponent({ value: { [modelName]: '' } });
return wrapper.vm.$nextTick().then(() => {
- expect(wrapper.vm.nameRegexState).toBe(null);
+ expect(wrapper.vm.textAreaState[stateVariable]).toBe(null);
expect(wrapper.emitted('validated')).toBeTruthy();
});
});
it(`if the user typed and is less than ${NAME_REGEX_LENGTH} state is true`, () => {
- mountComponent({ value: { name_regex: 'foo' } });
+ mountComponent({ value: { [modelName]: 'foo' } });
return wrapper.vm.$nextTick().then(() => {
- expect(wrapper.vm.nameRegexState).toBe(true);
+ const formGroup = findFormGroup(elementName);
+ const formElement = findFormElements(elementName, formGroup);
+ expect(formGroup.attributes('state')).toBeTruthy();
+ expect(formElement.attributes('state')).toBeTruthy();
});
});
});
diff --git a/spec/frontend/related_merge_requests/components/related_merge_requests_spec.js b/spec/frontend/related_merge_requests/components/related_merge_requests_spec.js
new file mode 100644
index 00000000000..1b938c93df8
--- /dev/null
+++ b/spec/frontend/related_merge_requests/components/related_merge_requests_spec.js
@@ -0,0 +1,94 @@
+import { mount, createLocalVue } from '@vue/test-utils';
+import MockAdapter from 'axios-mock-adapter';
+import axios from '~/lib/utils/axios_utils';
+import RelatedIssuableItem from '~/vue_shared/components/issue/related_issuable_item.vue';
+import RelatedMergeRequests from '~/related_merge_requests/components/related_merge_requests.vue';
+import createStore from '~/related_merge_requests/store/index';
+
+const FIXTURE_PATH = 'issues/related_merge_requests.json';
+const API_ENDPOINT = '/api/v4/projects/2/issues/33/related_merge_requests';
+const localVue = createLocalVue();
+
+describe('RelatedMergeRequests', () => {
+ let wrapper;
+ let mock;
+ let mockData;
+
+ beforeEach(done => {
+ loadFixtures(FIXTURE_PATH);
+ mockData = getJSONFixture(FIXTURE_PATH);
+
+ // put the fixture in DOM as the component expects
+ document.body.innerHTML = `<div id="js-issuable-app-initial-data">${JSON.stringify(
+ mockData,
+ )}</div>`;
+
+ mock = new MockAdapter(axios);
+ mock.onGet(`${API_ENDPOINT}?per_page=100`).reply(200, mockData, { 'x-total': 2 });
+
+ wrapper = mount(localVue.extend(RelatedMergeRequests), {
+ localVue,
+ store: createStore(),
+ propsData: {
+ endpoint: API_ENDPOINT,
+ projectNamespace: 'gitlab-org',
+ projectPath: 'gitlab-ce',
+ },
+ });
+
+ setImmediate(done);
+ });
+
+ afterEach(() => {
+ wrapper.destroy();
+ mock.restore();
+ });
+
+ describe('methods', () => {
+ describe('getAssignees', () => {
+ const assignees = [{ name: 'foo' }, { name: 'bar' }];
+
+ describe('when there is assignees array', () => {
+ it('should return assignees array', () => {
+ const mr = { assignees };
+
+ expect(wrapper.vm.getAssignees(mr)).toEqual(assignees);
+ });
+ });
+
+ it('should return an array with single assingee', () => {
+ const mr = { assignee: assignees[0] };
+
+ expect(wrapper.vm.getAssignees(mr)).toEqual([assignees[0]]);
+ });
+
+ it('should return empty array when assignee is not set', () => {
+ expect(wrapper.vm.getAssignees({})).toEqual([]);
+ expect(wrapper.vm.getAssignees({ assignee: null })).toEqual([]);
+ });
+ });
+ });
+
+ describe('template', () => {
+ it('should render related merge request items', () => {
+ expect(wrapper.find('.js-items-count').text()).toEqual('2');
+ expect(wrapper.findAll(RelatedIssuableItem).length).toEqual(2);
+
+ const props = wrapper
+ .findAll(RelatedIssuableItem)
+ .at(1)
+ .props();
+ const data = mockData[1];
+
+ expect(props.idKey).toEqual(data.id);
+ expect(props.pathIdSeparator).toEqual('!');
+ expect(props.pipelineStatus).toBe(data.head_pipeline.detailed_status);
+ expect(props.assignees).toEqual([data.assignee]);
+ expect(props.isMergeRequest).toBe(true);
+ expect(props.confidential).toEqual(false);
+ expect(props.title).toEqual(data.title);
+ expect(props.state).toEqual(data.state);
+ expect(props.createdAt).toEqual(data.created_at);
+ });
+ });
+});
diff --git a/spec/frontend/related_merge_requests/store/actions_spec.js b/spec/frontend/related_merge_requests/store/actions_spec.js
new file mode 100644
index 00000000000..26c5977cb5f
--- /dev/null
+++ b/spec/frontend/related_merge_requests/store/actions_spec.js
@@ -0,0 +1,111 @@
+import MockAdapter from 'axios-mock-adapter';
+import createFlash from '~/flash';
+import testAction from 'helpers/vuex_action_helper';
+import axios from '~/lib/utils/axios_utils';
+import * as types from '~/related_merge_requests/store/mutation_types';
+import * as actions from '~/related_merge_requests/store/actions';
+
+jest.mock('~/flash');
+
+describe('RelatedMergeRequest store actions', () => {
+ let state;
+ let mock;
+
+ beforeEach(() => {
+ state = {
+ apiEndpoint: '/api/related_merge_requests',
+ };
+ mock = new MockAdapter(axios);
+ });
+
+ afterEach(() => {
+ mock.restore();
+ });
+
+ describe('setInitialState', () => {
+ it('commits types.SET_INITIAL_STATE with given props', done => {
+ const props = { a: 1, b: 2 };
+
+ testAction(
+ actions.setInitialState,
+ props,
+ {},
+ [{ type: types.SET_INITIAL_STATE, payload: props }],
+ [],
+ done,
+ );
+ });
+ });
+
+ describe('requestData', () => {
+ it('commits types.REQUEST_DATA', done => {
+ testAction(actions.requestData, null, {}, [{ type: types.REQUEST_DATA }], [], done);
+ });
+ });
+
+ describe('receiveDataSuccess', () => {
+ it('commits types.RECEIVE_DATA_SUCCESS with data', done => {
+ const data = { a: 1, b: 2 };
+
+ testAction(
+ actions.receiveDataSuccess,
+ data,
+ {},
+ [{ type: types.RECEIVE_DATA_SUCCESS, payload: data }],
+ [],
+ done,
+ );
+ });
+ });
+
+ describe('receiveDataError', () => {
+ it('commits types.RECEIVE_DATA_ERROR', done => {
+ testAction(
+ actions.receiveDataError,
+ null,
+ {},
+ [{ type: types.RECEIVE_DATA_ERROR }],
+ [],
+ done,
+ );
+ });
+ });
+
+ describe('fetchMergeRequests', () => {
+ describe('for a successful request', () => {
+ it('should dispatch success action', done => {
+ const data = { a: 1 };
+ mock.onGet(`${state.apiEndpoint}?per_page=100`).replyOnce(200, data, { 'x-total': 2 });
+
+ testAction(
+ actions.fetchMergeRequests,
+ null,
+ state,
+ [],
+ [{ type: 'requestData' }, { type: 'receiveDataSuccess', payload: { data, total: 2 } }],
+ done,
+ );
+ });
+ });
+
+ describe('for a failing request', () => {
+ it('should dispatch error action', done => {
+ mock.onGet(`${state.apiEndpoint}?per_page=100`).replyOnce(400);
+
+ testAction(
+ actions.fetchMergeRequests,
+ null,
+ state,
+ [],
+ [{ type: 'requestData' }, { type: 'receiveDataError' }],
+ () => {
+ expect(createFlash).toHaveBeenCalledTimes(1);
+ expect(createFlash).toHaveBeenCalledWith(expect.stringMatching('Something went wrong'));
+
+ done();
+ },
+ );
+ });
+ });
+ });
+});
diff --git a/spec/javascripts/related_merge_requests/store/mutations_spec.js b/spec/frontend/related_merge_requests/store/mutations_spec.js
index 21b6e26376b..21b6e26376b 100644
--- a/spec/javascripts/related_merge_requests/store/mutations_spec.js
+++ b/spec/frontend/related_merge_requests/store/mutations_spec.js
diff --git a/spec/frontend/releases/components/app_edit_spec.js b/spec/frontend/releases/components/app_edit_spec.js
index 09bafe4aa9b..4450b047acd 100644
--- a/spec/frontend/releases/components/app_edit_spec.js
+++ b/spec/frontend/releases/components/app_edit_spec.js
@@ -1,11 +1,13 @@
import Vuex from 'vuex';
import { mount } from '@vue/test-utils';
import ReleaseEditApp from '~/releases/components/app_edit.vue';
-import { release as originalRelease } from '../mock_data';
+import { release as originalRelease, milestones as originalMilestones } from '../mock_data';
import * as commonUtils from '~/lib/utils/common_utils';
import { BACK_URL_PARAM } from '~/releases/constants';
import AssetLinksForm from '~/releases/components/asset_links_form.vue';
import { merge } from 'lodash';
+import axios from 'axios';
+import MockAdapter from 'axios-mock-adapter';
describe('Release edit component', () => {
let wrapper;
@@ -13,6 +15,7 @@ describe('Release edit component', () => {
let actions;
let getters;
let state;
+ let mock;
const factory = ({ featureFlags = {}, store: storeUpdates = {} } = {}) => {
state = {
@@ -20,6 +23,7 @@ describe('Release edit component', () => {
markdownDocsPath: 'path/to/markdown/docs',
updateReleaseApiDocsPath: 'path/to/update/release/api/docs',
releasesPagePath: 'path/to/releases/page',
+ projectId: '8',
};
actions = {
@@ -62,8 +66,11 @@ describe('Release edit component', () => {
};
beforeEach(() => {
+ mock = new MockAdapter(axios);
gon.api_version = 'v4';
+ mock.onGet('/api/v4/projects/8/milestones').reply(200, originalMilestones);
+
release = commonUtils.convertObjectPropsToCamelCase(originalRelease, { deep: true });
});
diff --git a/spec/frontend/releases/components/release_block_footer_spec.js b/spec/frontend/releases/components/release_block_footer_spec.js
index c63637c4cae..b91cfb82b65 100644
--- a/spec/frontend/releases/components/release_block_footer_spec.js
+++ b/spec/frontend/releases/components/release_block_footer_spec.js
@@ -3,13 +3,17 @@ import { GlLink } from '@gitlab/ui';
import { trimText } from 'helpers/text_helper';
import ReleaseBlockFooter from '~/releases/components/release_block_footer.vue';
import Icon from '~/vue_shared/components/icon.vue';
-import { release } from '../mock_data';
+import { release as originalRelease } from '../mock_data';
import { convertObjectPropsToCamelCase } from '~/lib/utils/common_utils';
+import { cloneDeep } from 'lodash';
+
+const mockFutureDate = new Date(9999, 0, 0).toISOString();
+let mockIsFutureRelease = false;
jest.mock('~/vue_shared/mixins/timeago', () => ({
methods: {
timeFormatted() {
- return '7 fortnights ago';
+ return mockIsFutureRelease ? 'in 1 month' : '7 fortnights ago';
},
tooltipTitle() {
return 'February 30, 2401';
@@ -19,12 +23,12 @@ jest.mock('~/vue_shared/mixins/timeago', () => ({
describe('Release block footer', () => {
let wrapper;
- let releaseClone;
+ let release;
const factory = (props = {}) => {
wrapper = mount(ReleaseBlockFooter, {
propsData: {
- ...convertObjectPropsToCamelCase(releaseClone, { deep: true }),
+ ...convertObjectPropsToCamelCase(release, { deep: true }),
...props,
},
});
@@ -33,11 +37,13 @@ describe('Release block footer', () => {
};
beforeEach(() => {
- releaseClone = JSON.parse(JSON.stringify(release));
+ release = cloneDeep(originalRelease);
});
afterEach(() => {
wrapper.destroy();
+ wrapper = null;
+ mockIsFutureRelease = false;
});
const commitInfoSection = () => wrapper.find('.js-commit-info');
@@ -60,8 +66,8 @@ describe('Release block footer', () => {
const commitLink = commitInfoSectionLink();
expect(commitLink.exists()).toBe(true);
- expect(commitLink.text()).toBe(releaseClone.commit.short_id);
- expect(commitLink.attributes('href')).toBe(releaseClone.commit_path);
+ expect(commitLink.text()).toBe(release.commit.short_id);
+ expect(commitLink.attributes('href')).toBe(release.commit_path);
});
it('renders the tag icon', () => {
@@ -75,28 +81,60 @@ describe('Release block footer', () => {
const commitLink = tagInfoSection().find(GlLink);
expect(commitLink.exists()).toBe(true);
- expect(commitLink.text()).toBe(releaseClone.tag_name);
- expect(commitLink.attributes('href')).toBe(releaseClone.tag_path);
+ expect(commitLink.text()).toBe(release.tag_name);
+ expect(commitLink.attributes('href')).toBe(release.tag_path);
});
it('renders the author and creation time info', () => {
expect(trimText(authorDateInfoSection().text())).toBe(
- `Created 7 fortnights ago by ${releaseClone.author.username}`,
+ `Created 7 fortnights ago by ${release.author.username}`,
);
});
+ describe('when the release date is in the past', () => {
+ it('prefixes the creation info with "Created"', () => {
+ expect(trimText(authorDateInfoSection().text())).toEqual(expect.stringMatching(/^Created/));
+ });
+ });
+
+ describe('renders the author and creation time info with future release date', () => {
+ beforeEach(() => {
+ mockIsFutureRelease = true;
+ factory({ releasedAt: mockFutureDate });
+ });
+
+ it('renders the release date without the author name', () => {
+ expect(trimText(authorDateInfoSection().text())).toBe(
+ `Will be created in 1 month by ${release.author.username}`,
+ );
+ });
+ });
+
+ describe('when the release date is in the future', () => {
+ beforeEach(() => {
+ mockIsFutureRelease = true;
+ factory({ releasedAt: mockFutureDate });
+ });
+
+ it('prefixes the creation info with "Will be created"', () => {
+ expect(trimText(authorDateInfoSection().text())).toEqual(
+ expect.stringMatching(/^Will be created/),
+ );
+ });
+ });
+
it("renders the author's avatar image", () => {
const avatarImg = authorDateInfoSection().find('img');
expect(avatarImg.exists()).toBe(true);
- expect(avatarImg.attributes('src')).toBe(releaseClone.author.avatar_url);
+ expect(avatarImg.attributes('src')).toBe(release.author.avatar_url);
});
it("renders a link to the author's profile", () => {
const authorLink = authorDateInfoSection().find(GlLink);
expect(authorLink.exists()).toBe(true);
- expect(authorLink.attributes('href')).toBe(releaseClone.author.web_url);
+ expect(authorLink.attributes('href')).toBe(release.author.web_url);
});
});
@@ -113,7 +151,7 @@ describe('Release block footer', () => {
it('renders the commit SHA as plain text (instead of a link)', () => {
expect(commitInfoSectionLink().exists()).toBe(false);
- expect(commitInfoSection().text()).toBe(releaseClone.commit.short_id);
+ expect(commitInfoSection().text()).toBe(release.commit.short_id);
});
});
@@ -130,7 +168,7 @@ describe('Release block footer', () => {
it('renders the tag name as plain text (instead of a link)', () => {
expect(tagInfoSectionLink().exists()).toBe(false);
- expect(tagInfoSection().text()).toBe(releaseClone.tag_name);
+ expect(tagInfoSection().text()).toBe(release.tag_name);
});
});
@@ -138,7 +176,18 @@ describe('Release block footer', () => {
beforeEach(() => factory({ author: undefined }));
it('renders the release date without the author name', () => {
- expect(trimText(authorDateInfoSection().text())).toBe('Created 7 fortnights ago');
+ expect(trimText(authorDateInfoSection().text())).toBe(`Created 7 fortnights ago`);
+ });
+ });
+
+ describe('future release without any author info', () => {
+ beforeEach(() => {
+ mockIsFutureRelease = true;
+ factory({ author: undefined, releasedAt: mockFutureDate });
+ });
+
+ it('renders the release date without the author name', () => {
+ expect(trimText(authorDateInfoSection().text())).toBe(`Will be created in 1 month`);
});
});
@@ -147,7 +196,7 @@ describe('Release block footer', () => {
it('renders the author name without the release date', () => {
expect(trimText(authorDateInfoSection().text())).toBe(
- `Created by ${releaseClone.author.username}`,
+ `Created by ${release.author.username}`,
);
});
});
diff --git a/spec/frontend/releases/components/release_block_metadata_spec.js b/spec/frontend/releases/components/release_block_metadata_spec.js
new file mode 100644
index 00000000000..cbe478bfa1f
--- /dev/null
+++ b/spec/frontend/releases/components/release_block_metadata_spec.js
@@ -0,0 +1,67 @@
+import { mount } from '@vue/test-utils';
+import { trimText } from 'helpers/text_helper';
+import ReleaseBlockMetadata from '~/releases/components/release_block_metadata.vue';
+import { release as originalRelease } from '../mock_data';
+import { convertObjectPropsToCamelCase } from '~/lib/utils/common_utils';
+import { cloneDeep } from 'lodash';
+
+const mockFutureDate = new Date(9999, 0, 0).toISOString();
+let mockIsFutureRelease = false;
+
+jest.mock('~/vue_shared/mixins/timeago', () => ({
+ methods: {
+ timeFormatted() {
+ return mockIsFutureRelease ? 'in 1 month' : '7 fortnights ago';
+ },
+ tooltipTitle() {
+ return 'February 30, 2401';
+ },
+ },
+}));
+
+describe('Release block metadata', () => {
+ let wrapper;
+ let release;
+
+ const factory = (releaseUpdates = {}) => {
+ wrapper = mount(ReleaseBlockMetadata, {
+ propsData: {
+ release: {
+ ...convertObjectPropsToCamelCase(release, { deep: true }),
+ ...releaseUpdates,
+ },
+ },
+ });
+ };
+
+ beforeEach(() => {
+ release = cloneDeep(originalRelease);
+ });
+
+ afterEach(() => {
+ wrapper.destroy();
+ wrapper = null;
+ mockIsFutureRelease = false;
+ });
+
+ const findReleaseDateInfo = () => wrapper.find('.js-release-date-info');
+
+ describe('with all props provided', () => {
+ beforeEach(() => factory());
+
+ it('renders the release time info', () => {
+ expect(trimText(findReleaseDateInfo().text())).toBe(`released 7 fortnights ago`);
+ });
+ });
+
+ describe('with a future release date', () => {
+ beforeEach(() => {
+ mockIsFutureRelease = true;
+ factory({ releasedAt: mockFutureDate });
+ });
+
+ it('renders the release date without the author name', () => {
+ expect(trimText(findReleaseDateInfo().text())).toBe(`will be released in 1 month`);
+ });
+ });
+});
diff --git a/spec/frontend/releases/components/release_block_milestone_info_spec.js b/spec/frontend/releases/components/release_block_milestone_info_spec.js
index 0b65b6cab96..0e79c45b337 100644
--- a/spec/frontend/releases/components/release_block_milestone_info_spec.js
+++ b/spec/frontend/releases/components/release_block_milestone_info_spec.js
@@ -1,5 +1,5 @@
import { mount } from '@vue/test-utils';
-import { GlProgressBar, GlLink, GlBadge, GlDeprecatedButton } from '@gitlab/ui';
+import { GlProgressBar, GlLink, GlBadge, GlButton } from '@gitlab/ui';
import { trimText } from 'helpers/text_helper';
import ReleaseBlockMilestoneInfo from '~/releases/components/release_block_milestone_info.vue';
import { milestones as originalMilestones } from '../mock_data';
@@ -106,7 +106,7 @@ describe('Release block milestone info', () => {
const clickShowMoreFewerButton = () => {
milestoneListContainer()
- .find(GlDeprecatedButton)
+ .find(GlButton)
.trigger('click');
return wrapper.vm.$nextTick();
diff --git a/spec/frontend/releases/components/release_block_spec.js b/spec/frontend/releases/components/release_block_spec.js
index 9846fcb65eb..19119d99f3c 100644
--- a/spec/frontend/releases/components/release_block_spec.js
+++ b/spec/frontend/releases/components/release_block_spec.js
@@ -1,6 +1,5 @@
import $ from 'jquery';
import { mount } from '@vue/test-utils';
-import { first } from 'underscore';
import EvidenceBlock from '~/releases/components/evidence_block.vue';
import ReleaseBlock from '~/releases/components/release_block.vue';
import ReleaseBlockFooter from '~/releases/components/release_block_footer.vue';
@@ -80,11 +79,11 @@ describe('Release block', () => {
);
expect(wrapper.find('.js-sources-dropdown li a').attributes().href).toEqual(
- first(release.assets.sources).url,
+ release.assets.sources[0].url,
);
expect(wrapper.find('.js-sources-dropdown li a').text()).toContain(
- first(release.assets.sources).format,
+ release.assets.sources[0].format,
);
});
@@ -92,12 +91,10 @@ describe('Release block', () => {
expect(wrapper.findAll('.js-assets-list li').length).toEqual(release.assets.links.length);
expect(wrapper.find('.js-assets-list li a').attributes().href).toEqual(
- first(release.assets.links).directAssetUrl,
+ release.assets.links[0].directAssetUrl,
);
- expect(wrapper.find('.js-assets-list li a').text()).toContain(
- first(release.assets.links).name,
- );
+ expect(wrapper.find('.js-assets-list li a').text()).toContain(release.assets.links[0].name);
});
it('renders author avatar', () => {
@@ -264,7 +261,7 @@ describe('Release block', () => {
});
it('renders a link to the milestone with a tooltip', () => {
- const milestone = first(release.milestones);
+ const milestone = release.milestones[0];
const milestoneLink = wrapper.find('.js-milestone-link');
expect(milestoneLink.exists()).toBe(true);
diff --git a/spec/frontend/releases/stores/modules/detail/actions_spec.js b/spec/frontend/releases/stores/modules/detail/actions_spec.js
index 4a1790adb09..854f06821be 100644
--- a/spec/frontend/releases/stores/modules/detail/actions_spec.js
+++ b/spec/frontend/releases/stores/modules/detail/actions_spec.js
@@ -130,6 +130,15 @@ describe('Release detail actions', () => {
});
});
+ describe('updateReleaseMilestones', () => {
+ it(`commits ${types.UPDATE_RELEASE_MILESTONES} with the updated release milestones`, () => {
+ const newReleaseMilestones = ['v0.0', 'v0.1'];
+ return testAction(actions.updateReleaseMilestones, newReleaseMilestones, state, [
+ { type: types.UPDATE_RELEASE_MILESTONES, payload: newReleaseMilestones },
+ ]);
+ });
+ });
+
describe('requestUpdateRelease', () => {
it(`commits ${types.REQUEST_UPDATE_RELEASE}`, () =>
testAction(actions.requestUpdateRelease, undefined, state, [
@@ -143,7 +152,7 @@ describe('Release detail actions', () => {
{ type: types.RECEIVE_UPDATE_RELEASE_SUCCESS },
]));
- describe('when the releaseShowPage feature flag is enabled', () => {
+ it('redirects to the releases page if releaseShowPage feature flag is enabled', () => {
const rootState = { featureFlags: { releaseShowPage: true } };
const updatedState = merge({}, state, {
releasesPagePath: 'path/to/releases/page',
@@ -248,6 +257,7 @@ describe('Release detail actions', () => {
{
name: state.release.name,
description: state.release.description,
+ milestones: state.release.milestones.map(milestone => milestone.title),
},
],
]);
diff --git a/spec/frontend/releases/stores/modules/detail/mutations_spec.js b/spec/frontend/releases/stores/modules/detail/mutations_spec.js
index cb5a1880b0c..f3f7ca797b4 100644
--- a/spec/frontend/releases/stores/modules/detail/mutations_spec.js
+++ b/spec/frontend/releases/stores/modules/detail/mutations_spec.js
@@ -1,10 +1,3 @@
-/* eslint-disable jest/valid-describe */
-/*
- * ESLint disable directive ↑ can be removed once
- * https://github.com/jest-community/eslint-plugin-jest/issues/203
- * is resolved
- */
-
import createState from '~/releases/stores/modules/detail/state';
import mutations from '~/releases/stores/modules/detail/mutations';
import * as types from '~/releases/stores/modules/detail/mutation_types';
@@ -27,7 +20,7 @@ describe('Release detail mutations', () => {
release = convertObjectPropsToCamelCase(originalRelease);
});
- describe(types.REQUEST_RELEASE, () => {
+ describe(`${types.REQUEST_RELEASE}`, () => {
it('set state.isFetchingRelease to true', () => {
mutations[types.REQUEST_RELEASE](state);
@@ -35,7 +28,7 @@ describe('Release detail mutations', () => {
});
});
- describe(types.RECEIVE_RELEASE_SUCCESS, () => {
+ describe(`${types.RECEIVE_RELEASE_SUCCESS}`, () => {
it('handles a successful response from the server', () => {
mutations[types.RECEIVE_RELEASE_SUCCESS](state, release);
@@ -49,7 +42,7 @@ describe('Release detail mutations', () => {
});
});
- describe(types.RECEIVE_RELEASE_ERROR, () => {
+ describe(`${types.RECEIVE_RELEASE_ERROR}`, () => {
it('handles an unsuccessful response from the server', () => {
const error = { message: 'An error occurred!' };
mutations[types.RECEIVE_RELEASE_ERROR](state, error);
@@ -62,7 +55,7 @@ describe('Release detail mutations', () => {
});
});
- describe(types.UPDATE_RELEASE_TITLE, () => {
+ describe(`${types.UPDATE_RELEASE_TITLE}`, () => {
it("updates the release's title", () => {
state.release = release;
const newTitle = 'The new release title';
@@ -72,7 +65,7 @@ describe('Release detail mutations', () => {
});
});
- describe(types.UPDATE_RELEASE_NOTES, () => {
+ describe(`${types.UPDATE_RELEASE_NOTES}`, () => {
it("updates the release's notes", () => {
state.release = release;
const newNotes = 'The new release notes';
@@ -82,7 +75,7 @@ describe('Release detail mutations', () => {
});
});
- describe(types.REQUEST_UPDATE_RELEASE, () => {
+ describe(`${types.REQUEST_UPDATE_RELEASE}`, () => {
it('set state.isUpdatingRelease to true', () => {
mutations[types.REQUEST_UPDATE_RELEASE](state);
@@ -90,7 +83,7 @@ describe('Release detail mutations', () => {
});
});
- describe(types.RECEIVE_UPDATE_RELEASE_SUCCESS, () => {
+ describe(`${types.RECEIVE_UPDATE_RELEASE_SUCCESS}`, () => {
it('handles a successful response from the server', () => {
mutations[types.RECEIVE_UPDATE_RELEASE_SUCCESS](state, release);
@@ -100,7 +93,7 @@ describe('Release detail mutations', () => {
});
});
- describe(types.RECEIVE_UPDATE_RELEASE_ERROR, () => {
+ describe(`${types.RECEIVE_UPDATE_RELEASE_ERROR}`, () => {
it('handles an unsuccessful response from the server', () => {
const error = { message: 'An error occurred!' };
mutations[types.RECEIVE_UPDATE_RELEASE_ERROR](state, error);
@@ -111,7 +104,7 @@ describe('Release detail mutations', () => {
});
});
- describe(types.ADD_EMPTY_ASSET_LINK, () => {
+ describe(`${types.ADD_EMPTY_ASSET_LINK}`, () => {
it('adds a new, empty link object to the release', () => {
state.release = release;
@@ -130,7 +123,7 @@ describe('Release detail mutations', () => {
});
});
- describe(types.UPDATE_ASSET_LINK_URL, () => {
+ describe(`${types.UPDATE_ASSET_LINK_URL}`, () => {
it('updates an asset link with a new URL', () => {
state.release = release;
@@ -145,7 +138,7 @@ describe('Release detail mutations', () => {
});
});
- describe(types.UPDATE_ASSET_LINK_NAME, () => {
+ describe(`${types.UPDATE_ASSET_LINK_NAME}`, () => {
it('updates an asset link with a new name', () => {
state.release = release;
@@ -160,7 +153,7 @@ describe('Release detail mutations', () => {
});
});
- describe(types.REMOVE_ASSET_LINK, () => {
+ describe(`${types.REMOVE_ASSET_LINK}`, () => {
it('removes an asset link from the release', () => {
state.release = release;
diff --git a/spec/frontend/reports/accessibility_report/grouped_accessibility_reports_app_spec.js b/spec/frontend/reports/accessibility_report/grouped_accessibility_reports_app_spec.js
new file mode 100644
index 00000000000..a036588596a
--- /dev/null
+++ b/spec/frontend/reports/accessibility_report/grouped_accessibility_reports_app_spec.js
@@ -0,0 +1,126 @@
+import { mount, createLocalVue } from '@vue/test-utils';
+import Vuex from 'vuex';
+import GroupedAccessibilityReportsApp from '~/reports/accessibility_report/grouped_accessibility_reports_app.vue';
+import AccessibilityIssueBody from '~/reports/accessibility_report/components/accessibility_issue_body.vue';
+import store from '~/reports/accessibility_report/store';
+import { mockReport } from './mock_data';
+
+const localVue = createLocalVue();
+localVue.use(Vuex);
+
+describe('Grouped accessibility reports app', () => {
+ const Component = localVue.extend(GroupedAccessibilityReportsApp);
+ let wrapper;
+ let mockStore;
+
+ const mountComponent = () => {
+ wrapper = mount(Component, {
+ store: mockStore,
+ localVue,
+ propsData: {
+ endpoint: 'endpoint.json',
+ },
+ methods: {
+ fetchReport: () => {},
+ },
+ });
+ };
+
+ const findHeader = () => wrapper.find('[data-testid="report-section-code-text"]');
+
+ beforeEach(() => {
+ mockStore = store();
+ mountComponent();
+ });
+
+ afterEach(() => {
+ wrapper.destroy();
+ });
+
+ describe('while loading', () => {
+ beforeEach(() => {
+ mockStore.state.isLoading = true;
+ mountComponent();
+ });
+
+ it('renders loading state', () => {
+ expect(findHeader().text()).toEqual('Accessibility scanning results are being parsed');
+ });
+ });
+
+ describe('with error', () => {
+ beforeEach(() => {
+ mockStore.state.isLoading = false;
+ mockStore.state.hasError = true;
+ mountComponent();
+ });
+
+ it('renders error state', () => {
+ expect(findHeader().text()).toEqual('Accessibility scanning failed loading results');
+ });
+ });
+
+ describe('with a report', () => {
+ describe('with no issues', () => {
+ beforeEach(() => {
+ mockStore.state.report = {
+ summary: {
+ errored: 0,
+ },
+ };
+ });
+
+ it('renders no issues header', () => {
+ expect(findHeader().text()).toContain(
+ 'Accessibility scanning detected no issues for the source branch only',
+ );
+ });
+ });
+
+ describe('with one issue', () => {
+ beforeEach(() => {
+ mockStore.state.report = {
+ summary: {
+ errored: 1,
+ },
+ };
+ });
+
+ it('renders one issue header', () => {
+ expect(findHeader().text()).toContain(
+ 'Accessibility scanning detected 1 issue for the source branch only',
+ );
+ });
+ });
+
+ describe('with multiple issues', () => {
+ beforeEach(() => {
+ mockStore.state.report = {
+ summary: {
+ errored: 2,
+ },
+ };
+ });
+
+ it('renders multiple issues header', () => {
+ expect(findHeader().text()).toContain(
+ 'Accessibility scanning detected 2 issues for the source branch only',
+ );
+ });
+ });
+
+ describe('with issues to show', () => {
+ beforeEach(() => {
+ mockStore.state.report = mockReport;
+ });
+
+ it('renders custom accessibility issue body', () => {
+ const issueBody = wrapper.find(AccessibilityIssueBody);
+
+ expect(issueBody.props('issue').code).toBe(mockReport.new_errors[0].code);
+ expect(issueBody.props('issue').message).toBe(mockReport.new_errors[0].message);
+ expect(issueBody.props('isNew')).toBe(true);
+ });
+ });
+ });
+});
diff --git a/spec/frontend/reports/accessibility_report/mock_data.js b/spec/frontend/reports/accessibility_report/mock_data.js
new file mode 100644
index 00000000000..f8e832c1ce5
--- /dev/null
+++ b/spec/frontend/reports/accessibility_report/mock_data.js
@@ -0,0 +1,55 @@
+export const mockReport = {
+ status: 'failed',
+ summary: {
+ total: 2,
+ resolved: 0,
+ errored: 2,
+ },
+ new_errors: [
+ {
+ code: 'WCAG2AA.Principle1.Guideline1_4.1_4_3.G18.Fail',
+ type: 'error',
+ typeCode: 1,
+ message:
+ 'This element has insufficient contrast at this conformance level. Expected a contrast ratio of at least 4.5:1, but text in this element has a contrast ratio of 3.84:1. Recommendation: change text colour to #767676.',
+ context: '<a href="/stages-devops-lifecycle/" class="main-nav-link">Product</a>',
+ selector: '#main-nav > div:nth-child(2) > ul > li:nth-child(1) > a',
+ runner: 'htmlcs',
+ runnerExtras: {},
+ },
+ ],
+ new_notes: [],
+ new_warnings: [],
+ resolved_errors: [
+ {
+ code: 'WCAG2AA.Principle4.Guideline4_1.4_1_2.H91.A.NoContent',
+ type: 'error',
+ typeCode: 1,
+ message:
+ 'Anchor element found with a valid href attribute, but no link content has been supplied.',
+ context: '<a href="/" class="navbar-brand animated"><svg height="36" viewBox="0 0 1...</a>',
+ selector: '#main-nav > div:nth-child(1) > a',
+ runner: 'htmlcs',
+ runnerExtras: {},
+ },
+ ],
+ resolved_notes: [],
+ resolved_warnings: [],
+ existing_errors: [
+ {
+ code: 'WCAG2AA.Principle4.Guideline4_1.4_1_2.H91.A.NoContent',
+ type: 'error',
+ typeCode: 1,
+ message:
+ 'Anchor element found with a valid href attribute, but no link content has been supplied.',
+ context: '<a href="/" class="navbar-brand animated"><svg height="36" viewBox="0 0 1...</a>',
+ selector: '#main-nav > div:nth-child(1) > a',
+ runner: 'htmlcs',
+ runnerExtras: {},
+ },
+ ],
+ existing_notes: [],
+ existing_warnings: [],
+};
+
+export default () => {};
diff --git a/spec/frontend/reports/accessibility_report/store/actions_spec.js b/spec/frontend/reports/accessibility_report/store/actions_spec.js
new file mode 100644
index 00000000000..129a5bade86
--- /dev/null
+++ b/spec/frontend/reports/accessibility_report/store/actions_spec.js
@@ -0,0 +1,121 @@
+import axios from '~/lib/utils/axios_utils';
+import MockAdapter from 'axios-mock-adapter';
+import * as actions from '~/reports/accessibility_report/store/actions';
+import * as types from '~/reports/accessibility_report/store/mutation_types';
+import createStore from '~/reports/accessibility_report/store';
+import { TEST_HOST } from 'spec/test_constants';
+import testAction from 'helpers/vuex_action_helper';
+import { mockReport } from '../mock_data';
+
+describe('Accessibility Reports actions', () => {
+ let localState;
+ let localStore;
+
+ beforeEach(() => {
+ localStore = createStore();
+ localState = localStore.state;
+ });
+
+ describe('setEndpoints', () => {
+ it('should commit SET_ENDPOINTS mutation', done => {
+ const endpoint = 'endpoint.json';
+
+ testAction(
+ actions.setEndpoint,
+ endpoint,
+ localState,
+ [{ type: types.SET_ENDPOINT, payload: endpoint }],
+ [],
+ done,
+ );
+ });
+ });
+
+ describe('fetchReport', () => {
+ let mock;
+
+ beforeEach(() => {
+ localState.endpoint = `${TEST_HOST}/endpoint.json`;
+ mock = new MockAdapter(axios);
+ });
+
+ afterEach(() => {
+ mock.restore();
+ actions.stopPolling();
+ actions.clearEtagPoll();
+ });
+
+ describe('success', () => {
+ it('should commit REQUEST_REPORT mutation and dispatch receiveReportSuccess', done => {
+ const data = { report: { summary: {} } };
+ mock.onGet(`${TEST_HOST}/endpoint.json`).reply(200, data);
+
+ testAction(
+ actions.fetchReport,
+ null,
+ localState,
+ [{ type: types.REQUEST_REPORT }],
+ [
+ {
+ payload: { status: 200, data },
+ type: 'receiveReportSuccess',
+ },
+ ],
+ done,
+ );
+ });
+ });
+
+ describe('error', () => {
+ it('should commit REQUEST_REPORT and RECEIVE_REPORT_ERROR mutations', done => {
+ mock.onGet(`${TEST_HOST}/endpoint.json`).reply(500);
+
+ testAction(
+ actions.fetchReport,
+ null,
+ localState,
+ [{ type: types.REQUEST_REPORT }],
+ [{ type: 'receiveReportError' }],
+ done,
+ );
+ });
+ });
+ });
+
+ describe('receiveReportSuccess', () => {
+ it('should commit RECEIVE_REPORT_SUCCESS mutation with 200', done => {
+ testAction(
+ actions.receiveReportSuccess,
+ { status: 200, data: mockReport },
+ localState,
+ [{ type: types.RECEIVE_REPORT_SUCCESS, payload: mockReport }],
+ [{ type: 'stopPolling' }],
+ done,
+ );
+ });
+
+ it('should not commit RECEIVE_REPORTS_SUCCESS mutation with 204', done => {
+ testAction(
+ actions.receiveReportSuccess,
+ { status: 204, data: mockReport },
+ localState,
+ [],
+ [],
+ done,
+ );
+ });
+ });
+
+ describe('receiveReportError', () => {
+ it('should commit RECEIVE_REPORT_ERROR mutation', done => {
+ testAction(
+ actions.receiveReportError,
+ null,
+ localState,
+ [{ type: types.RECEIVE_REPORT_ERROR }],
+ [{ type: 'stopPolling' }],
+ done,
+ );
+ });
+ });
+});
diff --git a/spec/frontend/reports/accessibility_report/store/getters_spec.js b/spec/frontend/reports/accessibility_report/store/getters_spec.js
new file mode 100644
index 00000000000..d74c71cfa09
--- /dev/null
+++ b/spec/frontend/reports/accessibility_report/store/getters_spec.js
@@ -0,0 +1,149 @@
+import * as getters from '~/reports/accessibility_report/store/getters';
+import createStore from '~/reports/accessibility_report/store';
+import { LOADING, ERROR, SUCCESS, STATUS_FAILED } from '~/reports/constants';
+
+describe('Accessibility reports store getters', () => {
+ let localState;
+ let localStore;
+
+ beforeEach(() => {
+ localStore = createStore();
+ localState = localStore.state;
+ });
+
+ describe('summaryStatus', () => {
+ describe('when summary is loading', () => {
+ it('returns loading status', () => {
+ localState.isLoading = true;
+
+ expect(getters.summaryStatus(localState)).toEqual(LOADING);
+ });
+ });
+
+ describe('when summary has error', () => {
+ it('returns error status', () => {
+ localState.hasError = true;
+
+ expect(getters.summaryStatus(localState)).toEqual(ERROR);
+ });
+ });
+
+ describe('when summary has failed status', () => {
+ it('returns loading status', () => {
+ localState.status = STATUS_FAILED;
+
+ expect(getters.summaryStatus(localState)).toEqual(ERROR);
+ });
+ });
+
+ describe('when summary has successfully loaded', () => {
+ it('returns loading status', () => {
+ expect(getters.summaryStatus(localState)).toEqual(SUCCESS);
+ });
+ });
+ });
+
+ describe('groupedSummaryText', () => {
+ describe('when state is loading', () => {
+ it('returns the loading summary message', () => {
+ localState.isLoading = true;
+ const result = 'Accessibility scanning results are being parsed';
+
+ expect(getters.groupedSummaryText(localState)).toEqual(result);
+ });
+ });
+
+ describe('when state has error', () => {
+ it('returns the error summary message', () => {
+ localState.hasError = true;
+ const result = 'Accessibility scanning failed loading results';
+
+ expect(getters.groupedSummaryText(localState)).toEqual(result);
+ });
+ });
+
+ describe('when state has successfully loaded', () => {
+ describe('when report has errors', () => {
+ it('returns summary message containing number of errors', () => {
+ localState.report = {
+ summary: {
+ errored: 2,
+ },
+ };
+ const result = 'Accessibility scanning detected 2 issues for the source branch only';
+
+ expect(getters.groupedSummaryText(localState)).toEqual(result);
+ });
+ });
+
+ describe('when report has no errors', () => {
+ it('returns summary message containing no errors', () => {
+ localState.report = {
+ summary: {
+ errored: 0,
+ },
+ };
+ const result = 'Accessibility scanning detected no issues for the source branch only';
+
+ expect(getters.groupedSummaryText(localState)).toEqual(result);
+ });
+ });
+ });
+ });
+
+ describe('shouldRenderIssuesList', () => {
+ describe('when has issues to render', () => {
+ it('returns true', () => {
+ localState.report = {
+ existing_errors: [{ name: 'Issue' }],
+ };
+
+ expect(getters.shouldRenderIssuesList(localState)).toEqual(true);
+ });
+ });
+
+ describe('when does not have issues to render', () => {
+ it('returns false', () => {
+ localState.report = {
+ status: 'success',
+ summary: { errored: 0 },
+ };
+
+ expect(getters.shouldRenderIssuesList(localState)).toEqual(false);
+ });
+ });
+ });
+
+ describe('unresolvedIssues', () => {
+ it('returns the array unresolved errors', () => {
+ localState.report = {
+ existing_errors: [1],
+ };
+ const result = [1];
+
+ expect(getters.unresolvedIssues(localState)).toEqual(result);
+ });
+ });
+
+ describe('resolvedIssues', () => {
+ it('returns array of resolved errors', () => {
+ localState.report = {
+ resolved_errors: [1],
+ };
+ const result = [1];
+
+ expect(getters.resolvedIssues(localState)).toEqual(result);
+ });
+ });
+
+ describe('newIssues', () => {
+ it('returns array of new errors', () => {
+ localState.report = {
+ new_errors: [1],
+ };
+ const result = [1];
+
+ expect(getters.newIssues(localState)).toEqual(result);
+ });
+ });
+});
diff --git a/spec/frontend/reports/accessibility_report/store/mutations_spec.js b/spec/frontend/reports/accessibility_report/store/mutations_spec.js
new file mode 100644
index 00000000000..a4e9571b721
--- /dev/null
+++ b/spec/frontend/reports/accessibility_report/store/mutations_spec.js
@@ -0,0 +1,64 @@
+import mutations from '~/reports/accessibility_report/store/mutations';
+import createStore from '~/reports/accessibility_report/store';
+
+describe('Accessibility Reports mutations', () => {
+ let localState;
+ let localStore;
+
+ beforeEach(() => {
+ localStore = createStore();
+ localState = localStore.state;
+ });
+
+ describe('SET_ENDPOINT', () => {
+ it('sets endpoint to given value', () => {
+ const endpoint = 'endpoint.json';
+ mutations.SET_ENDPOINT(localState, endpoint);
+
+ expect(localState.endpoint).toEqual(endpoint);
+ });
+ });
+
+ describe('REQUEST_REPORT', () => {
+ it('sets isLoading to true', () => {
+ mutations.REQUEST_REPORT(localState);
+
+ expect(localState.isLoading).toEqual(true);
+ });
+ });
+
+ describe('RECEIVE_REPORT_SUCCESS', () => {
+ it('sets isLoading to false', () => {
+ mutations.RECEIVE_REPORT_SUCCESS(localState, {});
+
+ expect(localState.isLoading).toEqual(false);
+ });
+
+ it('sets hasError to false', () => {
+ mutations.RECEIVE_REPORT_SUCCESS(localState, {});
+
+ expect(localState.hasError).toEqual(false);
+ });
+
+ it('sets report to response report', () => {
+ const report = { data: 'testing' };
+ mutations.RECEIVE_REPORT_SUCCESS(localState, report);
+
+ expect(localState.report).toEqual(report);
+ });
+ });
+
+ describe('RECEIVE_REPORT_ERROR', () => {
+ it('sets isLoading to false', () => {
+ mutations.RECEIVE_REPORT_ERROR(localState);
+
+ expect(localState.isLoading).toEqual(false);
+ });
+
+ it('sets hasError to true', () => {
+ mutations.RECEIVE_REPORT_ERROR(localState);
+
+ expect(localState.hasError).toEqual(true);
+ });
+ });
+});
diff --git a/spec/frontend/reports/components/__snapshots__/grouped_issues_list_spec.js.snap b/spec/frontend/reports/components/__snapshots__/grouped_issues_list_spec.js.snap
new file mode 100644
index 00000000000..c932379a253
--- /dev/null
+++ b/spec/frontend/reports/components/__snapshots__/grouped_issues_list_spec.js.snap
@@ -0,0 +1,25 @@
+// Jest Snapshot v1, https://goo.gl/fbAQLP
+
+exports[`Grouped Issues List renders a smart virtual list with the correct props 1`] = `
+Object {
+ "length": 4,
+ "remain": 20,
+ "rtag": "div",
+ "size": 32,
+ "wclass": "report-block-list",
+ "wtag": "ul",
+}
+`;
+
+exports[`Grouped Issues List with data renders a report item with the correct props 1`] = `
+Object {
+ "component": "TestIssueBody",
+ "isNew": false,
+ "issue": Object {
+ "name": "foo",
+ },
+ "showReportSectionStatusIcon": false,
+ "status": "none",
+ "statusIconSize": 24,
+}
+`;
diff --git a/spec/frontend/reports/components/__snapshots__/issue_status_icon_spec.js.snap b/spec/frontend/reports/components/__snapshots__/issue_status_icon_spec.js.snap
new file mode 100644
index 00000000000..70e1ff01323
--- /dev/null
+++ b/spec/frontend/reports/components/__snapshots__/issue_status_icon_spec.js.snap
@@ -0,0 +1,37 @@
+// Jest Snapshot v1, https://goo.gl/fbAQLP
+
+exports[`IssueStatusIcon renders "failed" state correctly 1`] = `
+<div
+ class="report-block-list-icon failed"
+>
+ <icon-stub
+ data-qa-selector="status_failed_icon"
+ name="status_failed_borderless"
+ size="24"
+ />
+</div>
+`;
+
+exports[`IssueStatusIcon renders "neutral" state correctly 1`] = `
+<div
+ class="report-block-list-icon neutral"
+>
+ <icon-stub
+ data-qa-selector="status_neutral_icon"
+ name="dash"
+ size="24"
+ />
+</div>
+`;
+
+exports[`IssueStatusIcon renders "success" state correctly 1`] = `
+<div
+ class="report-block-list-icon success"
+>
+ <icon-stub
+ data-qa-selector="status_success_icon"
+ name="status_success_borderless"
+ size="24"
+ />
+</div>
+`;
diff --git a/spec/frontend/reports/components/grouped_issues_list_spec.js b/spec/frontend/reports/components/grouped_issues_list_spec.js
new file mode 100644
index 00000000000..1f8f4a0e4c1
--- /dev/null
+++ b/spec/frontend/reports/components/grouped_issues_list_spec.js
@@ -0,0 +1,86 @@
+import { shallowMount } from '@vue/test-utils';
+import GroupedIssuesList from '~/reports/components/grouped_issues_list.vue';
+import ReportItem from '~/reports/components/report_item.vue';
+import SmartVirtualList from '~/vue_shared/components/smart_virtual_list.vue';
+
+describe('Grouped Issues List', () => {
+ let wrapper;
+
+ const createComponent = ({ propsData = {}, stubs = {} } = {}) => {
+ wrapper = shallowMount(GroupedIssuesList, {
+ propsData,
+ stubs,
+ });
+ };
+
+ const findHeading = groupName => wrapper.find(`[data-testid="${groupName}Heading"`);
+
+ afterEach(() => {
+ wrapper.destroy();
+ wrapper = null;
+ });
+
+ it('renders a smart virtual list with the correct props', () => {
+ createComponent({
+ propsData: {
+ resolvedIssues: [{ name: 'foo' }],
+ unresolvedIssues: [{ name: 'bar' }],
+ },
+ stubs: {
+ SmartVirtualList,
+ },
+ });
+
+ expect(wrapper.find(SmartVirtualList).props()).toMatchSnapshot();
+ });
+
+ describe('without data', () => {
+ beforeEach(createComponent);
+
+ it.each(['unresolved', 'resolved'])('does not a render a header for %s issues', issueName => {
+ expect(findHeading(issueName).exists()).toBe(false);
+ });
+
+ it.each('resolved', 'unresolved')('does not render report items for %s issues', () => {
+ expect(wrapper.contains(ReportItem)).toBe(false);
+ });
+ });
+
+ describe('with data', () => {
+ it.each`
+ givenIssues | givenHeading | groupName
+ ${[{ name: 'foo issue' }]} | ${'Foo Heading'} | ${'resolved'}
+ ${[{ name: 'bar issue' }]} | ${'Bar Heading'} | ${'unresolved'}
+ `('renders the heading for $groupName issues', ({ givenIssues, givenHeading, groupName }) => {
+ createComponent({
+ propsData: { [`${groupName}Issues`]: givenIssues, [`${groupName}Heading`]: givenHeading },
+ });
+
+ expect(findHeading(groupName).text()).toBe(givenHeading);
+ });
+
+ it.each(['resolved', 'unresolved'])('renders all %s issues', issueName => {
+ const issues = [{ name: 'foo' }, { name: 'bar' }];
+
+ createComponent({
+ propsData: { [`${issueName}Issues`]: issues },
+ });
+
+ expect(wrapper.findAll(ReportItem)).toHaveLength(issues.length);
+ });
+
+ it('renders a report item with the correct props', () => {
+ createComponent({
+ propsData: {
+ resolvedIssues: [{ name: 'foo' }],
+ component: 'TestIssueBody',
+ },
+ stubs: {
+ ReportItem,
+ },
+ });
+
+ expect(wrapper.find(ReportItem).props()).toMatchSnapshot();
+ });
+ });
+});
diff --git a/spec/frontend/reports/components/grouped_test_reports_app_spec.js b/spec/frontend/reports/components/grouped_test_reports_app_spec.js
new file mode 100644
index 00000000000..1a01db391da
--- /dev/null
+++ b/spec/frontend/reports/components/grouped_test_reports_app_spec.js
@@ -0,0 +1,260 @@
+import Vue from 'vue';
+import MockAdapter from 'axios-mock-adapter';
+import axios from '~/lib/utils/axios_utils';
+import state from '~/reports/store/state';
+import component from '~/reports/components/grouped_test_reports_app.vue';
+import mountComponent from '../../helpers/vue_mount_component_helper';
+import { failedReport } from '../mock_data/mock_data';
+import newFailedTestReports from '../mock_data/new_failures_report.json';
+import newErrorsTestReports from '../mock_data/new_errors_report.json';
+import successTestReports from '../mock_data/no_failures_report.json';
+import mixedResultsTestReports from '../mock_data/new_and_fixed_failures_report.json';
+import resolvedFailures from '../mock_data/resolved_failures.json';
+
+describe('Grouped Test Reports App', () => {
+ let vm;
+ let mock;
+ const Component = Vue.extend(component);
+
+ beforeEach(() => {
+ mock = new MockAdapter(axios);
+ });
+
+ afterEach(() => {
+ vm.$store.replaceState(state());
+ vm.$destroy();
+ mock.restore();
+ });
+
+ describe('with success result', () => {
+ beforeEach(() => {
+ mock.onGet('test_results.json').reply(200, successTestReports, {});
+ vm = mountComponent(Component, {
+ endpoint: 'test_results.json',
+ });
+ });
+
+ it('renders success summary text', done => {
+ setImmediate(() => {
+ expect(vm.$el.querySelector('.gl-spinner')).toBeNull();
+ expect(vm.$el.querySelector('.js-code-text').textContent.trim()).toEqual(
+ 'Test summary contained no changed test results out of 11 total tests',
+ );
+
+ expect(vm.$el.textContent).toContain(
+ 'rspec:pg found no changed test results out of 8 total tests',
+ );
+
+ expect(vm.$el.textContent).toContain(
+ 'java ant found no changed test results out of 3 total tests',
+ );
+ done();
+ });
+ });
+ });
+
+ describe('with 204 result', () => {
+ beforeEach(() => {
+ mock.onGet('test_results.json').reply(204, {}, {});
+ vm = mountComponent(Component, {
+ endpoint: 'test_results.json',
+ });
+ });
+
+ it('renders success summary text', done => {
+ setImmediate(() => {
+ expect(vm.$el.querySelector('.gl-spinner')).not.toBeNull();
+ expect(vm.$el.querySelector('.js-code-text').textContent.trim()).toEqual(
+ 'Test summary results are being parsed',
+ );
+
+ done();
+ });
+ });
+ });
+
+ describe('with new failed result', () => {
+ beforeEach(() => {
+ mock.onGet('test_results.json').reply(200, newFailedTestReports, {});
+ vm = mountComponent(Component, {
+ endpoint: 'test_results.json',
+ });
+ });
+
+ it('renders failed summary text + new badge', done => {
+ setImmediate(() => {
+ expect(vm.$el.querySelector('.gl-spinner')).toBeNull();
+ expect(vm.$el.querySelector('.js-code-text').textContent.trim()).toEqual(
+ 'Test summary contained 2 failed out of 11 total tests',
+ );
+
+ expect(vm.$el.textContent).toContain('rspec:pg found 2 failed out of 8 total tests');
+
+ expect(vm.$el.textContent).toContain('New');
+ expect(vm.$el.textContent).toContain(
+ 'java ant found no changed test results out of 3 total tests',
+ );
+ done();
+ });
+ });
+ });
+
+ describe('with new error result', () => {
+ beforeEach(() => {
+ mock.onGet('test_results.json').reply(200, newErrorsTestReports, {});
+ vm = mountComponent(Component, {
+ endpoint: 'test_results.json',
+ });
+ });
+
+ it('renders error summary text + new badge', done => {
+ setImmediate(() => {
+ expect(vm.$el.querySelector('.gl-spinner')).toBeNull();
+ expect(vm.$el.querySelector('.js-code-text').textContent.trim()).toEqual(
+ 'Test summary contained 2 errors out of 11 total tests',
+ );
+
+ expect(vm.$el.textContent).toContain('karma found 2 errors out of 3 total tests');
+
+ expect(vm.$el.textContent).toContain('New');
+ expect(vm.$el.textContent).toContain(
+ 'rspec:pg found no changed test results out of 8 total tests',
+ );
+ done();
+ });
+ });
+ });
+
+ describe('with mixed results', () => {
+ beforeEach(() => {
+ mock.onGet('test_results.json').reply(200, mixedResultsTestReports, {});
+ vm = mountComponent(Component, {
+ endpoint: 'test_results.json',
+ });
+ });
+
+ it('renders summary text', done => {
+ setImmediate(() => {
+ expect(vm.$el.querySelector('.gl-spinner')).toBeNull();
+ expect(vm.$el.querySelector('.js-code-text').textContent.trim()).toEqual(
+ 'Test summary contained 2 failed and 2 fixed test results out of 11 total tests',
+ );
+
+ expect(vm.$el.textContent).toContain(
+ 'rspec:pg found 1 failed and 2 fixed test results out of 8 total tests',
+ );
+
+ expect(vm.$el.textContent).toContain('New');
+ expect(vm.$el.textContent).toContain(' java ant found 1 failed out of 3 total tests');
+ done();
+ });
+ });
+ });
+
+ describe('with resolved failures and resolved errors', () => {
+ beforeEach(() => {
+ mock.onGet('test_results.json').reply(200, resolvedFailures, {});
+ vm = mountComponent(Component, {
+ endpoint: 'test_results.json',
+ });
+ });
+
+ it('renders summary text', done => {
+ setImmediate(() => {
+ expect(vm.$el.querySelector('.gl-spinner')).toBeNull();
+ expect(vm.$el.querySelector('.js-code-text').textContent.trim()).toEqual(
+ 'Test summary contained 4 fixed test results out of 11 total tests',
+ );
+
+ expect(vm.$el.textContent).toContain(
+ 'rspec:pg found 4 fixed test results out of 8 total tests',
+ );
+ done();
+ });
+ });
+
+ it('renders resolved failures', done => {
+ setImmediate(() => {
+ expect(vm.$el.querySelector('.report-block-container').textContent).toContain(
+ resolvedFailures.suites[0].resolved_failures[0].name,
+ );
+
+ expect(vm.$el.querySelector('.report-block-container').textContent).toContain(
+ resolvedFailures.suites[0].resolved_failures[1].name,
+ );
+ done();
+ });
+ });
+
+ it('renders resolved errors', done => {
+ setImmediate(() => {
+ expect(vm.$el.querySelector('.report-block-container').textContent).toContain(
+ resolvedFailures.suites[0].resolved_errors[0].name,
+ );
+
+ expect(vm.$el.querySelector('.report-block-container').textContent).toContain(
+ resolvedFailures.suites[0].resolved_errors[1].name,
+ );
+ done();
+ });
+ });
+ });
+
+ describe('with a report that failed to load', () => {
+ beforeEach(() => {
+ mock.onGet('test_results.json').reply(200, failedReport, {});
+ vm = mountComponent(Component, {
+ endpoint: 'test_results.json',
+ });
+ });
+
+ it('renders an error status for the report', done => {
+ setImmediate(() => {
+ const { name } = failedReport.suites[0];
+
+ expect(vm.$el.querySelector('.report-block-list-issue').textContent).toContain(
+ `An error occurred while loading ${name} results`,
+ );
+ done();
+ });
+ });
+ });
+
+ describe('with error', () => {
+ beforeEach(() => {
+ mock.onGet('test_results.json').reply(500, {}, {});
+ vm = mountComponent(Component, {
+ endpoint: 'test_results.json',
+ });
+ });
+
+ it('renders loading summary text with loading icon', done => {
+ setImmediate(() => {
+ expect(vm.$el.querySelector('.js-code-text').textContent.trim()).toEqual(
+ 'Test summary failed loading results',
+ );
+ done();
+ });
+ });
+ });
+
+ describe('while loading', () => {
+ beforeEach(() => {
+ mock.onGet('test_results.json').reply(200, {}, {});
+ vm = mountComponent(Component, {
+ endpoint: 'test_results.json',
+ });
+ });
+
+ it('renders loading summary text with loading icon', done => {
+ expect(vm.$el.querySelector('.gl-spinner')).not.toBeNull();
+ expect(vm.$el.querySelector('.js-code-text').textContent.trim()).toEqual(
+ 'Test summary results are being parsed',
+ );
+
+ setImmediate(() => {
+ done();
+ });
+ });
+ });
+});
diff --git a/spec/frontend/reports/components/issue_status_icon_spec.js b/spec/frontend/reports/components/issue_status_icon_spec.js
new file mode 100644
index 00000000000..3a55ff0a9e3
--- /dev/null
+++ b/spec/frontend/reports/components/issue_status_icon_spec.js
@@ -0,0 +1,29 @@
+import { shallowMount } from '@vue/test-utils';
+import ReportItem from '~/reports/components/issue_status_icon.vue';
+import { STATUS_FAILED, STATUS_NEUTRAL, STATUS_SUCCESS } from '~/reports/constants';
+
+describe('IssueStatusIcon', () => {
+ let wrapper;
+
+ const createComponent = ({ status }) => {
+ wrapper = shallowMount(ReportItem, {
+ propsData: {
+ status,
+ },
+ });
+ };
+
+ afterEach(() => {
+ wrapper.destroy();
+ wrapper = null;
+ });
+
+ it.each([STATUS_SUCCESS, STATUS_NEUTRAL, STATUS_FAILED])(
+ 'renders "%s" state correctly',
+ status => {
+ createComponent({ status });
+
+ expect(wrapper.element).toMatchSnapshot();
+ },
+ );
+});
diff --git a/spec/frontend/reports/components/modal_open_name_spec.js b/spec/frontend/reports/components/modal_open_name_spec.js
new file mode 100644
index 00000000000..d59f3571c4b
--- /dev/null
+++ b/spec/frontend/reports/components/modal_open_name_spec.js
@@ -0,0 +1,47 @@
+import Vue from 'vue';
+import Vuex from 'vuex';
+import { mountComponentWithStore } from 'helpers/vue_mount_component_helper';
+import component from '~/reports/components/modal_open_name.vue';
+
+Vue.use(Vuex);
+
+describe('Modal open name', () => {
+ const Component = Vue.extend(component);
+ let vm;
+
+ const store = new Vuex.Store({
+ actions: {
+ openModal: () => {},
+ },
+ state: {},
+ mutations: {},
+ });
+
+ beforeEach(() => {
+ vm = mountComponentWithStore(Component, {
+ store,
+ props: {
+ issue: {
+ title: 'Issue',
+ },
+ status: 'failed',
+ },
+ });
+ });
+
+ afterEach(() => {
+ vm.$destroy();
+ });
+
+ it('renders the issue name', () => {
+ expect(vm.$el.textContent.trim()).toEqual('Issue');
+ });
+
+ it('calls openModal actions when button is clicked', () => {
+ jest.spyOn(vm, 'openModal').mockImplementation(() => {});
+
+ vm.$el.click();
+
+ expect(vm.openModal).toHaveBeenCalled();
+ });
+});
diff --git a/spec/javascripts/reports/components/modal_spec.js b/spec/frontend/reports/components/modal_spec.js
index ff046e64b6e..ff046e64b6e 100644
--- a/spec/javascripts/reports/components/modal_spec.js
+++ b/spec/frontend/reports/components/modal_spec.js
diff --git a/spec/frontend/reports/components/summary_row_spec.js b/spec/frontend/reports/components/summary_row_spec.js
new file mode 100644
index 00000000000..cb0cc025e80
--- /dev/null
+++ b/spec/frontend/reports/components/summary_row_spec.js
@@ -0,0 +1,37 @@
+import Vue from 'vue';
+import mountComponent from 'helpers/vue_mount_component_helper';
+import component from '~/reports/components/summary_row.vue';
+
+describe('Summary row', () => {
+ const Component = Vue.extend(component);
+ let vm;
+
+ const props = {
+ summary: 'SAST detected 1 new vulnerability and 1 fixed vulnerability',
+ popoverOptions: {
+ title: 'Static Application Security Testing (SAST)',
+ content: '<a>Learn more about SAST</a>',
+ },
+ statusIcon: 'warning',
+ };
+
+ beforeEach(() => {
+ vm = mountComponent(Component, props);
+ });
+
+ afterEach(() => {
+ vm.$destroy();
+ });
+
+ it('renders provided summary', () => {
+ expect(
+ vm.$el.querySelector('.report-block-list-issue-description-text').textContent.trim(),
+ ).toEqual(props.summary);
+ });
+
+ it('renders provided icon', () => {
+ expect(vm.$el.querySelector('.report-block-list-icon span').classList).toContain(
+ 'js-ci-status-icon-warning',
+ );
+ });
+});
diff --git a/spec/frontend/reports/components/test_issue_body_spec.js b/spec/frontend/reports/components/test_issue_body_spec.js
new file mode 100644
index 00000000000..ff81020a4eb
--- /dev/null
+++ b/spec/frontend/reports/components/test_issue_body_spec.js
@@ -0,0 +1,72 @@
+import Vue from 'vue';
+import component from '~/reports/components/test_issue_body.vue';
+import createStore from '~/reports/store';
+import { mountComponentWithStore } from '../../helpers/vue_mount_component_helper';
+import { trimText } from '../../helpers/text_helper';
+import { issue } from '../mock_data/mock_data';
+
+describe('Test Issue body', () => {
+ let vm;
+ const Component = Vue.extend(component);
+ const store = createStore();
+
+ const commonProps = {
+ issue,
+ status: 'failed',
+ };
+
+ afterEach(() => {
+ vm.$destroy();
+ });
+
+ describe('on click', () => {
+ it('calls openModal action', () => {
+ vm = mountComponentWithStore(Component, {
+ store,
+ props: commonProps,
+ });
+
+ jest.spyOn(vm, 'openModal').mockImplementation(() => {});
+
+ vm.$el.querySelector('button').click();
+
+ expect(vm.openModal).toHaveBeenCalledWith({
+ issue: commonProps.issue,
+ });
+ });
+ });
+
+ describe('is new', () => {
+ beforeEach(() => {
+ vm = mountComponentWithStore(Component, {
+ store,
+ props: { ...commonProps, isNew: true },
+ });
+ });
+
+ it('renders issue name', () => {
+ expect(vm.$el.textContent).toContain(commonProps.issue.name);
+ });
+
+ it('renders new badge', () => {
+ expect(trimText(vm.$el.querySelector('.badge').textContent)).toEqual('New');
+ });
+ });
+
+ describe('not new', () => {
+ beforeEach(() => {
+ vm = mountComponentWithStore(Component, {
+ store,
+ props: commonProps,
+ });
+ });
+
+ it('renders issue name', () => {
+ expect(vm.$el.textContent).toContain(commonProps.issue.name);
+ });
+
+ it('does not renders new badge', () => {
+ expect(vm.$el.querySelector('.badge')).toEqual(null);
+ });
+ });
+});
diff --git a/spec/frontend/reports/mock_data/mock_data.js b/spec/frontend/reports/mock_data/mock_data.js
new file mode 100644
index 00000000000..3caaab2fd79
--- /dev/null
+++ b/spec/frontend/reports/mock_data/mock_data.js
@@ -0,0 +1,24 @@
+export const issue = {
+ result: 'failure',
+ name: 'Test#sum when a is 1 and b is 2 returns summary',
+ execution_time: 0.009411,
+ system_output:
+ "Failure/Error: is_expected.to eq(3)\n\n expected: 3\n got: -1\n\n (compared using ==)\n./spec/test_spec.rb:12:in `block (4 levels) in \u003ctop (required)\u003e'",
+};
+
+export const failedReport = {
+ summary: { total: 11, resolved: 0, errored: 2, failed: 0 },
+ suites: [
+ {
+ name: 'rspec:pg',
+ status: 'error',
+ summary: { total: 0, resolved: 0, errored: 0, failed: 0 },
+ new_failures: [],
+ resolved_failures: [],
+ existing_failures: [],
+ new_errors: [],
+ resolved_errors: [],
+ existing_errors: [],
+ },
+ ],
+};
diff --git a/spec/javascripts/reports/mock_data/new_and_fixed_failures_report.json b/spec/frontend/reports/mock_data/new_and_fixed_failures_report.json
index 6141e5433a6..6141e5433a6 100644
--- a/spec/javascripts/reports/mock_data/new_and_fixed_failures_report.json
+++ b/spec/frontend/reports/mock_data/new_and_fixed_failures_report.json
diff --git a/spec/javascripts/reports/mock_data/new_errors_report.json b/spec/frontend/reports/mock_data/new_errors_report.json
index cebf98fdb63..cebf98fdb63 100644
--- a/spec/javascripts/reports/mock_data/new_errors_report.json
+++ b/spec/frontend/reports/mock_data/new_errors_report.json
diff --git a/spec/javascripts/reports/mock_data/new_failures_report.json b/spec/frontend/reports/mock_data/new_failures_report.json
index 8b9c12c6271..8b9c12c6271 100644
--- a/spec/javascripts/reports/mock_data/new_failures_report.json
+++ b/spec/frontend/reports/mock_data/new_failures_report.json
diff --git a/spec/javascripts/reports/mock_data/no_failures_report.json b/spec/frontend/reports/mock_data/no_failures_report.json
index 7da9e0c6211..7da9e0c6211 100644
--- a/spec/javascripts/reports/mock_data/no_failures_report.json
+++ b/spec/frontend/reports/mock_data/no_failures_report.json
diff --git a/spec/javascripts/reports/mock_data/resolved_failures.json b/spec/frontend/reports/mock_data/resolved_failures.json
index 49de6aa840b..49de6aa840b 100644
--- a/spec/javascripts/reports/mock_data/resolved_failures.json
+++ b/spec/frontend/reports/mock_data/resolved_failures.json
diff --git a/spec/frontend/reports/store/actions_spec.js b/spec/frontend/reports/store/actions_spec.js
new file mode 100644
index 00000000000..3f189736922
--- /dev/null
+++ b/spec/frontend/reports/store/actions_spec.js
@@ -0,0 +1,171 @@
+import MockAdapter from 'axios-mock-adapter';
+import testAction from 'helpers/vuex_action_helper';
+import { TEST_HOST } from 'helpers/test_constants';
+import axios from '~/lib/utils/axios_utils';
+import {
+ setEndpoint,
+ requestReports,
+ fetchReports,
+ stopPolling,
+ clearEtagPoll,
+ receiveReportsSuccess,
+ receiveReportsError,
+ openModal,
+ setModalData,
+} from '~/reports/store/actions';
+import state from '~/reports/store/state';
+import * as types from '~/reports/store/mutation_types';
+
+describe('Reports Store Actions', () => {
+ let mockedState;
+
+ beforeEach(() => {
+ mockedState = state();
+ });
+
+ describe('setEndpoint', () => {
+ it('should commit SET_ENDPOINT mutation', done => {
+ testAction(
+ setEndpoint,
+ 'endpoint.json',
+ mockedState,
+ [{ type: types.SET_ENDPOINT, payload: 'endpoint.json' }],
+ [],
+ done,
+ );
+ });
+ });
+
+ describe('requestReports', () => {
+ it('should commit REQUEST_REPORTS mutation', done => {
+ testAction(requestReports, null, mockedState, [{ type: types.REQUEST_REPORTS }], [], done);
+ });
+ });
+
+ describe('fetchReports', () => {
+ let mock;
+
+ beforeEach(() => {
+ mockedState.endpoint = `${TEST_HOST}/endpoint.json`;
+ mock = new MockAdapter(axios);
+ });
+
+ afterEach(() => {
+ mock.restore();
+ stopPolling();
+ clearEtagPoll();
+ });
+
+ describe('success', () => {
+ it('dispatches requestReports and receiveReportsSuccess ', done => {
+ mock
+ .onGet(`${TEST_HOST}/endpoint.json`)
+ .replyOnce(200, { summary: {}, suites: [{ name: 'rspec' }] });
+
+ testAction(
+ fetchReports,
+ null,
+ mockedState,
+ [],
+ [
+ {
+ type: 'requestReports',
+ },
+ {
+ payload: { data: { summary: {}, suites: [{ name: 'rspec' }] }, status: 200 },
+ type: 'receiveReportsSuccess',
+ },
+ ],
+ done,
+ );
+ });
+ });
+
+ describe('error', () => {
+ beforeEach(() => {
+ mock.onGet(`${TEST_HOST}/endpoint.json`).reply(500);
+ });
+
+ it('dispatches requestReports and receiveReportsError ', done => {
+ testAction(
+ fetchReports,
+ null,
+ mockedState,
+ [],
+ [
+ {
+ type: 'requestReports',
+ },
+ {
+ type: 'receiveReportsError',
+ },
+ ],
+ done,
+ );
+ });
+ });
+ });
+
+ describe('receiveReportsSuccess', () => {
+ it('should commit RECEIVE_REPORTS_SUCCESS mutation with 200', done => {
+ testAction(
+ receiveReportsSuccess,
+ { data: { summary: {} }, status: 200 },
+ mockedState,
+ [{ type: types.RECEIVE_REPORTS_SUCCESS, payload: { summary: {} } }],
+ [],
+ done,
+ );
+ });
+
+ it('should not commit RECEIVE_REPORTS_SUCCESS mutation with 204', done => {
+ testAction(
+ receiveReportsSuccess,
+ { data: { summary: {} }, status: 204 },
+ mockedState,
+ [],
+ [],
+ done,
+ );
+ });
+ });
+
+ describe('receiveReportsError', () => {
+ it('should commit RECEIVE_REPORTS_ERROR mutation', done => {
+ testAction(
+ receiveReportsError,
+ null,
+ mockedState,
+ [{ type: types.RECEIVE_REPORTS_ERROR }],
+ [],
+ done,
+ );
+ });
+ });
+
+ describe('openModal', () => {
+ it('should dispatch setModalData', done => {
+ testAction(
+ openModal,
+ { name: 'foo' },
+ mockedState,
+ [],
+ [{ type: 'setModalData', payload: { name: 'foo' } }],
+ done,
+ );
+ });
+ });
+
+ describe('setModalData', () => {
+ it('should commit SET_ISSUE_MODAL_DATA', done => {
+ testAction(
+ setModalData,
+ { name: 'foo' },
+ mockedState,
+ [{ type: types.SET_ISSUE_MODAL_DATA, payload: { name: 'foo' } }],
+ [],
+ done,
+ );
+ });
+ });
+});
diff --git a/spec/javascripts/reports/store/mutations_spec.js b/spec/frontend/reports/store/mutations_spec.js
index 9446cd454ab..9446cd454ab 100644
--- a/spec/javascripts/reports/store/mutations_spec.js
+++ b/spec/frontend/reports/store/mutations_spec.js
diff --git a/spec/frontend/repository/components/__snapshots__/last_commit_spec.js.snap b/spec/frontend/repository/components/__snapshots__/last_commit_spec.js.snap
index 491fc20c40e..1dca65dd862 100644
--- a/spec/frontend/repository/components/__snapshots__/last_commit_spec.js.snap
+++ b/spec/frontend/repository/components/__snapshots__/last_commit_spec.js.snap
@@ -26,9 +26,7 @@ exports[`Repository last commit component renders commit widget 1`] = `
class="commit-row-message item-title"
href="https://test.com/commit/123"
>
-
- Commit title
-
+ Commit title
</gl-link-stub>
<!---->
@@ -128,9 +126,7 @@ exports[`Repository last commit component renders the signature HTML as returned
class="commit-row-message item-title"
href="https://test.com/commit/123"
>
-
- Commit title
-
+ Commit title
</gl-link-stub>
<!---->
diff --git a/spec/frontend/repository/components/last_commit_spec.js b/spec/frontend/repository/components/last_commit_spec.js
index d2576ec26b7..a5bfeb08fe4 100644
--- a/spec/frontend/repository/components/last_commit_spec.js
+++ b/spec/frontend/repository/components/last_commit_spec.js
@@ -9,6 +9,7 @@ function createCommitData(data = {}) {
const defaultData = {
sha: '123456789',
title: 'Commit title',
+ titleHtml: 'Commit title',
message: 'Commit message',
webUrl: 'https://test.com/commit/123',
authoredDate: '2019-01-01',
diff --git a/spec/frontend/repository/utils/commit_spec.js b/spec/frontend/repository/utils/commit_spec.js
index e7cc28178bf..aaaa39f739f 100644
--- a/spec/frontend/repository/utils/commit_spec.js
+++ b/spec/frontend/repository/utils/commit_spec.js
@@ -8,6 +8,7 @@ const mockData = [
committed_date: '2019-01-01',
},
commit_path: `https://test.com`,
+ commit_title_html: 'testing message',
file_name: 'index.js',
type: 'blob',
},
@@ -24,6 +25,7 @@ describe('normalizeData', () => {
fileName: 'index.js',
filePath: '/index.js',
type: 'blob',
+ titleHtml: 'testing message',
__typename: 'LogTreeCommit',
},
]);
diff --git a/spec/javascripts/settings_panels_spec.js b/spec/frontend/settings_panels_spec.js
index 2c5d91a45bc..2c5d91a45bc 100644
--- a/spec/javascripts/settings_panels_spec.js
+++ b/spec/frontend/settings_panels_spec.js
diff --git a/spec/frontend/sidebar/__snapshots__/confidential_issue_sidebar_spec.js.snap b/spec/frontend/sidebar/__snapshots__/confidential_issue_sidebar_spec.js.snap
index 1f93336e755..cf7832f3948 100644
--- a/spec/frontend/sidebar/__snapshots__/confidential_issue_sidebar_spec.js.snap
+++ b/spec/frontend/sidebar/__snapshots__/confidential_issue_sidebar_spec.js.snap
@@ -1,6 +1,6 @@
// Jest Snapshot v1, https://goo.gl/fbAQLP
-exports[`Confidential Issue Sidebar Block renders for isConfidential = false and isEditable = false 1`] = `
+exports[`Confidential Issue Sidebar Block renders for confidential = false and isEditable = false 1`] = `
<div
class="block issuable-sidebar-item confidentiality"
>
@@ -52,7 +52,7 @@ exports[`Confidential Issue Sidebar Block renders for isConfidential = false and
</div>
`;
-exports[`Confidential Issue Sidebar Block renders for isConfidential = false and isEditable = true 1`] = `
+exports[`Confidential Issue Sidebar Block renders for confidential = false and isEditable = true 1`] = `
<div
class="block issuable-sidebar-item confidentiality"
>
@@ -84,9 +84,7 @@ exports[`Confidential Issue Sidebar Block renders for isConfidential = false and
data-track-property="confidentiality"
href="#"
>
-
Edit
-
</a>
</div>
@@ -114,7 +112,7 @@ exports[`Confidential Issue Sidebar Block renders for isConfidential = false and
</div>
`;
-exports[`Confidential Issue Sidebar Block renders for isConfidential = true and isEditable = false 1`] = `
+exports[`Confidential Issue Sidebar Block renders for confidential = true and isEditable = false 1`] = `
<div
class="block issuable-sidebar-item confidentiality"
>
@@ -166,7 +164,7 @@ exports[`Confidential Issue Sidebar Block renders for isConfidential = true and
</div>
`;
-exports[`Confidential Issue Sidebar Block renders for isConfidential = true and isEditable = true 1`] = `
+exports[`Confidential Issue Sidebar Block renders for confidential = true and isEditable = true 1`] = `
<div
class="block issuable-sidebar-item confidentiality"
>
@@ -198,9 +196,7 @@ exports[`Confidential Issue Sidebar Block renders for isConfidential = true and
data-track-property="confidentiality"
href="#"
>
-
Edit
-
</a>
</div>
diff --git a/spec/frontend/sidebar/assignees_realtime_spec.js b/spec/frontend/sidebar/assignees_realtime_spec.js
new file mode 100644
index 00000000000..1c62c52dc67
--- /dev/null
+++ b/spec/frontend/sidebar/assignees_realtime_spec.js
@@ -0,0 +1,102 @@
+import { shallowMount } from '@vue/test-utils';
+import ActionCable from '@rails/actioncable';
+import AssigneesRealtime from '~/sidebar/components/assignees/assignees_realtime.vue';
+import SidebarMediator from '~/sidebar/sidebar_mediator';
+import Mock from './mock_data';
+import query from '~/issuable_sidebar/queries/issue_sidebar.query.graphql';
+
+jest.mock('@rails/actioncable', () => {
+ const mockConsumer = {
+ subscriptions: { create: jest.fn().mockReturnValue({ unsubscribe: jest.fn() }) },
+ };
+ return {
+ createConsumer: jest.fn().mockReturnValue(mockConsumer),
+ };
+});
+
+describe('Assignees Realtime', () => {
+ let wrapper;
+ let mediator;
+
+ const createComponent = () => {
+ wrapper = shallowMount(AssigneesRealtime, {
+ propsData: {
+ issuableIid: '1',
+ mediator,
+ projectPath: 'path/to/project',
+ },
+ mocks: {
+ $apollo: {
+ query,
+ queries: {
+ project: {
+ refetch: jest.fn(),
+ },
+ },
+ },
+ },
+ });
+ };
+
+ beforeEach(() => {
+ mediator = new SidebarMediator(Mock.mediator);
+ });
+
+ afterEach(() => {
+ wrapper.destroy();
+ wrapper = null;
+ SidebarMediator.singleton = null;
+ });
+
+ describe('when handleFetchResult is called from smart query', () => {
+ it('sets assignees to the store', () => {
+ const data = {
+ project: {
+ issue: {
+ assignees: {
+ nodes: [{ id: 'gid://gitlab/Environments/123', avatarUrl: 'url' }],
+ },
+ },
+ },
+ };
+ const expected = [{ id: 123, avatar_url: 'url', avatarUrl: 'url' }];
+ createComponent();
+
+ wrapper.vm.handleFetchResult({ data });
+
+ expect(mediator.store.assignees).toEqual(expected);
+ });
+ });
+
+ describe('when mounted', () => {
+ it('calls create subscription', () => {
+ const cable = ActionCable.createConsumer();
+
+ createComponent();
+
+ return wrapper.vm.$nextTick().then(() => {
+ expect(cable.subscriptions.create).toHaveBeenCalledTimes(1);
+ expect(cable.subscriptions.create).toHaveBeenCalledWith(
+ {
+ channel: 'IssuesChannel',
+ iid: wrapper.props('issuableIid'),
+ project_path: wrapper.props('projectPath'),
+ },
+ { received: wrapper.vm.received },
+ );
+ });
+ });
+ });
+
+ describe('when subscription is recieved', () => {
+ it('refetches the GraphQL project query', () => {
+ createComponent();
+
+ wrapper.vm.received({ event: 'updated' });
+
+ return wrapper.vm.$nextTick().then(() => {
+ expect(wrapper.vm.$apollo.queries.project.refetch).toHaveBeenCalledTimes(1);
+ });
+ });
+ });
+});
diff --git a/spec/frontend/sidebar/components/time_tracking/time_tracker_spec.js b/spec/frontend/sidebar/components/time_tracking/time_tracker_spec.js
new file mode 100644
index 00000000000..1f028f74423
--- /dev/null
+++ b/spec/frontend/sidebar/components/time_tracking/time_tracker_spec.js
@@ -0,0 +1,279 @@
+import Vue from 'vue';
+
+import mountComponent from 'helpers/vue_mount_component_helper';
+import TimeTracker from '~/sidebar/components/time_tracking/time_tracker.vue';
+
+describe('Issuable Time Tracker', () => {
+ let initialData;
+ let vm;
+
+ const initTimeTrackingComponent = ({
+ timeEstimate,
+ timeSpent,
+ timeEstimateHumanReadable,
+ timeSpentHumanReadable,
+ limitToHours,
+ }) => {
+ setFixtures(`
+ <div>
+ <div id="mock-container"></div>
+ </div>
+ `);
+
+ initialData = {
+ timeEstimate,
+ timeSpent,
+ humanTimeEstimate: timeEstimateHumanReadable,
+ humanTimeSpent: timeSpentHumanReadable,
+ limitToHours: Boolean(limitToHours),
+ rootPath: '/',
+ };
+
+ const TimeTrackingComponent = Vue.extend({
+ ...TimeTracker,
+ components: {
+ ...TimeTracker.components,
+ transition: {
+ // disable animations
+ render(h) {
+ return h('div', this.$slots.default);
+ },
+ },
+ },
+ });
+ vm = mountComponent(TimeTrackingComponent, initialData, '#mock-container');
+ };
+
+ afterEach(() => {
+ vm.$destroy();
+ });
+
+ describe('Initialization', () => {
+ beforeEach(() => {
+ initTimeTrackingComponent({
+ timeEstimate: 10000, // 2h 46m
+ timeSpent: 5000, // 1h 23m
+ timeEstimateHumanReadable: '2h 46m',
+ timeSpentHumanReadable: '1h 23m',
+ });
+ });
+
+ it('should return something defined', () => {
+ expect(vm).toBeDefined();
+ });
+
+ it('should correctly set timeEstimate', done => {
+ Vue.nextTick(() => {
+ expect(vm.timeEstimate).toBe(initialData.timeEstimate);
+ done();
+ });
+ });
+
+ it('should correctly set time_spent', done => {
+ Vue.nextTick(() => {
+ expect(vm.timeSpent).toBe(initialData.timeSpent);
+ done();
+ });
+ });
+ });
+
+ describe('Content Display', () => {
+ describe('Panes', () => {
+ describe('Comparison pane', () => {
+ beforeEach(() => {
+ initTimeTrackingComponent({
+ timeEstimate: 100000, // 1d 3h
+ timeSpent: 5000, // 1h 23m
+ timeEstimateHumanReadable: '1d 3h',
+ timeSpentHumanReadable: '1h 23m',
+ });
+ });
+
+ it('should show the "Comparison" pane when timeEstimate and time_spent are truthy', done => {
+ Vue.nextTick(() => {
+ expect(vm.showComparisonState).toBe(true);
+ const $comparisonPane = vm.$el.querySelector('.time-tracking-comparison-pane');
+
+ expect($comparisonPane).toBeVisible();
+ done();
+ });
+ });
+
+ it('should show full times when the sidebar is collapsed', done => {
+ Vue.nextTick(() => {
+ const timeTrackingText = vm.$el.querySelector('.time-tracking-collapsed-summary span')
+ .textContent;
+
+ expect(timeTrackingText.trim()).toBe('1h 23m / 1d 3h');
+ done();
+ });
+ });
+
+ describe('Remaining meter', () => {
+ it('should display the remaining meter with the correct width', done => {
+ Vue.nextTick(() => {
+ expect(
+ vm.$el.querySelector('.time-tracking-comparison-pane .progress[value="5"]'),
+ ).not.toBeNull();
+ done();
+ });
+ });
+
+ it('should display the remaining meter with the correct background color when within estimate', done => {
+ Vue.nextTick(() => {
+ expect(
+ vm.$el.querySelector('.time-tracking-comparison-pane .progress[variant="primary"]'),
+ ).not.toBeNull();
+ done();
+ });
+ });
+
+ it('should display the remaining meter with the correct background color when over estimate', done => {
+ vm.timeEstimate = 10000; // 2h 46m
+ vm.timeSpent = 20000000; // 231 days
+ Vue.nextTick(() => {
+ expect(
+ vm.$el.querySelector('.time-tracking-comparison-pane .progress[variant="danger"]'),
+ ).not.toBeNull();
+ done();
+ });
+ });
+ });
+ });
+
+ describe('Comparison pane when limitToHours is true', () => {
+ beforeEach(() => {
+ initTimeTrackingComponent({
+ timeEstimate: 100000, // 1d 3h
+ timeSpent: 5000, // 1h 23m
+ timeEstimateHumanReadable: '',
+ timeSpentHumanReadable: '',
+ limitToHours: true,
+ });
+ });
+
+ it('should show the correct tooltip text', done => {
+ Vue.nextTick(() => {
+ expect(vm.showComparisonState).toBe(true);
+ const $title = vm.$el.querySelector('.time-tracking-content .compare-meter').dataset
+ .originalTitle;
+
+ expect($title).toBe('Time remaining: 26h 23m');
+ done();
+ });
+ });
+ });
+
+ describe('Estimate only pane', () => {
+ beforeEach(() => {
+ initTimeTrackingComponent({
+ timeEstimate: 10000, // 2h 46m
+ timeSpent: 0,
+ timeEstimateHumanReadable: '2h 46m',
+ timeSpentHumanReadable: '',
+ });
+ });
+
+ it('should display the human readable version of time estimated', done => {
+ Vue.nextTick(() => {
+ const estimateText = vm.$el.querySelector('.time-tracking-estimate-only-pane')
+ .textContent;
+ const correctText = 'Estimated: 2h 46m';
+
+ expect(estimateText.trim()).toBe(correctText);
+ done();
+ });
+ });
+ });
+
+ describe('Spent only pane', () => {
+ beforeEach(() => {
+ initTimeTrackingComponent({
+ timeEstimate: 0,
+ timeSpent: 5000, // 1h 23m
+ timeEstimateHumanReadable: '2h 46m',
+ timeSpentHumanReadable: '1h 23m',
+ });
+ });
+
+ it('should display the human readable version of time spent', done => {
+ Vue.nextTick(() => {
+ const spentText = vm.$el.querySelector('.time-tracking-spend-only-pane').textContent;
+ const correctText = 'Spent: 1h 23m';
+
+ expect(spentText).toBe(correctText);
+ done();
+ });
+ });
+ });
+
+ describe('No time tracking pane', () => {
+ beforeEach(() => {
+ initTimeTrackingComponent({
+ timeEstimate: 0,
+ timeSpent: 0,
+ timeEstimateHumanReadable: '',
+ timeSpentHumanReadable: '',
+ });
+ });
+
+ it('should only show the "No time tracking" pane when both timeEstimate and time_spent are falsey', done => {
+ Vue.nextTick(() => {
+ const $noTrackingPane = vm.$el.querySelector('.time-tracking-no-tracking-pane');
+ const noTrackingText = $noTrackingPane.textContent;
+ const correctText = 'No estimate or time spent';
+
+ expect(vm.showNoTimeTrackingState).toBe(true);
+ expect($noTrackingPane).toBeVisible();
+ expect(noTrackingText.trim()).toBe(correctText);
+ done();
+ });
+ });
+ });
+
+ describe('Help pane', () => {
+ const helpButton = () => vm.$el.querySelector('.help-button');
+ const closeHelpButton = () => vm.$el.querySelector('.close-help-button');
+ const helpPane = () => vm.$el.querySelector('.time-tracking-help-state');
+
+ beforeEach(() => {
+ initTimeTrackingComponent({ timeEstimate: 0, timeSpent: 0 });
+
+ return vm.$nextTick();
+ });
+
+ it('should not show the "Help" pane by default', () => {
+ expect(vm.showHelpState).toBe(false);
+ expect(helpPane()).toBeNull();
+ });
+
+ it('should show the "Help" pane when help button is clicked', () => {
+ helpButton().click();
+
+ return vm.$nextTick().then(() => {
+ expect(vm.showHelpState).toBe(true);
+
+ // let animations run
+ jest.advanceTimersByTime(500);
+
+ expect(helpPane()).toBeVisible();
+ });
+ });
+
+ it('should not show the "Help" pane when help button is clicked and then closed', done => {
+ helpButton().click();
+
+ Vue.nextTick()
+ .then(() => closeHelpButton().click())
+ .then(() => Vue.nextTick())
+ .then(() => {
+ expect(vm.showHelpState).toBe(false);
+ expect(helpPane()).toBeNull();
+ })
+ .then(done)
+ .catch(done.fail);
+ });
+ });
+ });
+ });
+});
diff --git a/spec/frontend/sidebar/confidential/edit_form_buttons_spec.js b/spec/frontend/sidebar/confidential/edit_form_buttons_spec.js
new file mode 100644
index 00000000000..acdfb5139bf
--- /dev/null
+++ b/spec/frontend/sidebar/confidential/edit_form_buttons_spec.js
@@ -0,0 +1,41 @@
+import { shallowMount } from '@vue/test-utils';
+import EditFormButtons from '~/sidebar/components/confidential/edit_form_buttons.vue';
+
+describe('Edit Form Buttons', () => {
+ let wrapper;
+ const findConfidentialToggle = () => wrapper.find('[data-testid="confidential-toggle"]');
+
+ const createComponent = props => {
+ wrapper = shallowMount(EditFormButtons, {
+ propsData: {
+ updateConfidentialAttribute: () => {},
+ ...props,
+ },
+ });
+ };
+
+ afterEach(() => {
+ wrapper.destroy();
+ wrapper = null;
+ });
+
+ describe('when not confidential', () => {
+ it('renders Turn On in the ', () => {
+ createComponent({
+ isConfidential: false,
+ });
+
+ expect(findConfidentialToggle().text()).toBe('Turn On');
+ });
+ });
+
+ describe('when confidential', () => {
+ it('renders on or off text based on confidentiality', () => {
+ createComponent({
+ isConfidential: true,
+ });
+
+ expect(findConfidentialToggle().text()).toBe('Turn Off');
+ });
+ });
+});
diff --git a/spec/frontend/sidebar/confidential/edit_form_spec.js b/spec/frontend/sidebar/confidential/edit_form_spec.js
new file mode 100644
index 00000000000..137019a1e1b
--- /dev/null
+++ b/spec/frontend/sidebar/confidential/edit_form_spec.js
@@ -0,0 +1,45 @@
+import { shallowMount } from '@vue/test-utils';
+import EditForm from '~/sidebar/components/confidential/edit_form.vue';
+
+describe('Edit Form Dropdown', () => {
+ let wrapper;
+ const toggleForm = () => {};
+ const updateConfidentialAttribute = () => {};
+
+ const createComponent = props => {
+ wrapper = shallowMount(EditForm, {
+ propsData: {
+ ...props,
+ },
+ });
+ };
+
+ afterEach(() => {
+ wrapper.destroy();
+ wrapper = null;
+ });
+
+ describe('when not confidential', () => {
+ it('renders "You are going to turn off the confidentiality." in the ', () => {
+ createComponent({
+ isConfidential: false,
+ toggleForm,
+ updateConfidentialAttribute,
+ });
+
+ expect(wrapper.find('p').text()).toContain('You are going to turn on the confidentiality.');
+ });
+ });
+
+ describe('when confidential', () => {
+ it('renders on or off text based on confidentiality', () => {
+ createComponent({
+ isConfidential: true,
+ toggleForm,
+ updateConfidentialAttribute,
+ });
+
+ expect(wrapper.find('p').text()).toContain('You are going to turn off the confidentiality.');
+ });
+ });
+});
diff --git a/spec/frontend/sidebar/confidential_edit_buttons_spec.js b/spec/frontend/sidebar/confidential_edit_buttons_spec.js
deleted file mode 100644
index 32da9f83112..00000000000
--- a/spec/frontend/sidebar/confidential_edit_buttons_spec.js
+++ /dev/null
@@ -1,35 +0,0 @@
-import Vue from 'vue';
-import editFormButtons from '~/sidebar/components/confidential/edit_form_buttons.vue';
-
-describe('Edit Form Buttons', () => {
- let vm1;
- let vm2;
-
- beforeEach(() => {
- const Component = Vue.extend(editFormButtons);
- const toggleForm = () => {};
- const updateConfidentialAttribute = () => {};
-
- vm1 = new Component({
- propsData: {
- isConfidential: true,
- toggleForm,
- updateConfidentialAttribute,
- },
- }).$mount();
-
- vm2 = new Component({
- propsData: {
- isConfidential: false,
- toggleForm,
- updateConfidentialAttribute,
- },
- }).$mount();
- });
-
- it('renders on or off text based on confidentiality', () => {
- expect(vm1.$el.innerHTML.includes('Turn Off')).toBe(true);
-
- expect(vm2.$el.innerHTML.includes('Turn On')).toBe(true);
- });
-});
diff --git a/spec/frontend/sidebar/confidential_edit_form_buttons_spec.js b/spec/frontend/sidebar/confidential_edit_form_buttons_spec.js
deleted file mode 100644
index 369088cb258..00000000000
--- a/spec/frontend/sidebar/confidential_edit_form_buttons_spec.js
+++ /dev/null
@@ -1,35 +0,0 @@
-import Vue from 'vue';
-import editForm from '~/sidebar/components/confidential/edit_form.vue';
-
-describe('Edit Form Dropdown', () => {
- let vm1;
- let vm2;
-
- beforeEach(() => {
- const Component = Vue.extend(editForm);
- const toggleForm = () => {};
- const updateConfidentialAttribute = () => {};
-
- vm1 = new Component({
- propsData: {
- isConfidential: true,
- toggleForm,
- updateConfidentialAttribute,
- },
- }).$mount();
-
- vm2 = new Component({
- propsData: {
- isConfidential: false,
- toggleForm,
- updateConfidentialAttribute,
- },
- }).$mount();
- });
-
- it('renders on the appropriate warning text', () => {
- expect(vm1.$el.innerHTML.includes('You are going to turn off the confidentiality.')).toBe(true);
-
- expect(vm2.$el.innerHTML.includes('You are going to turn on the confidentiality.')).toBe(true);
- });
-});
diff --git a/spec/frontend/sidebar/confidential_issue_sidebar_spec.js b/spec/frontend/sidebar/confidential_issue_sidebar_spec.js
index 4853d9795b1..e7a64ec5ed9 100644
--- a/spec/frontend/sidebar/confidential_issue_sidebar_spec.js
+++ b/spec/frontend/sidebar/confidential_issue_sidebar_spec.js
@@ -5,6 +5,7 @@ import EditForm from '~/sidebar/components/confidential/edit_form.vue';
import SidebarService from '~/sidebar/services/sidebar_service';
import createFlash from '~/flash';
import RecaptchaModal from '~/vue_shared/components/recaptcha_modal.vue';
+import createStore from '~/notes/stores';
jest.mock('~/flash');
jest.mock('~/sidebar/services/sidebar_service');
@@ -31,8 +32,10 @@ describe('Confidential Issue Sidebar Block', () => {
};
const createComponent = propsData => {
+ const store = createStore();
const service = new SidebarService();
wrapper = shallowMount(ConfidentialIssueSidebar, {
+ store,
propsData: {
service,
...propsData,
@@ -49,29 +52,31 @@ describe('Confidential Issue Sidebar Block', () => {
});
it.each`
- isConfidential | isEditable
- ${false} | ${false}
- ${false} | ${true}
- ${true} | ${false}
- ${true} | ${true}
+ confidential | isEditable
+ ${false} | ${false}
+ ${false} | ${true}
+ ${true} | ${false}
+ ${true} | ${true}
`(
- 'renders for isConfidential = $isConfidential and isEditable = $isEditable',
- ({ isConfidential, isEditable }) => {
+ 'renders for confidential = $confidential and isEditable = $isEditable',
+ ({ confidential, isEditable }) => {
createComponent({
- isConfidential,
isEditable,
});
+ wrapper.vm.$store.state.noteableData.confidential = confidential;
- expect(wrapper.element).toMatchSnapshot();
+ return wrapper.vm.$nextTick().then(() => {
+ expect(wrapper.element).toMatchSnapshot();
+ });
},
);
describe('if editable', () => {
beforeEach(() => {
createComponent({
- isConfidential: true,
isEditable: true,
});
+ wrapper.vm.$store.state.noteableData.confidential = true;
});
it('displays the edit form when editable', () => {
diff --git a/spec/frontend/sidebar/lock/edit_form_buttons_spec.js b/spec/frontend/sidebar/lock/edit_form_buttons_spec.js
new file mode 100644
index 00000000000..66f9237ce97
--- /dev/null
+++ b/spec/frontend/sidebar/lock/edit_form_buttons_spec.js
@@ -0,0 +1,31 @@
+import { shallowMount } from '@vue/test-utils';
+import EditFormButtons from '~/sidebar/components/lock/edit_form_buttons.vue';
+
+describe('EditFormButtons', () => {
+ let wrapper;
+
+ const mountComponent = propsData => shallowMount(EditFormButtons, { propsData });
+
+ afterEach(() => {
+ wrapper.destroy();
+ wrapper = null;
+ });
+
+ it('displays "Unlock" when locked', () => {
+ wrapper = mountComponent({
+ isLocked: true,
+ updateLockedAttribute: () => {},
+ });
+
+ expect(wrapper.text()).toContain('Unlock');
+ });
+
+ it('displays "Lock" when unlocked', () => {
+ wrapper = mountComponent({
+ isLocked: false,
+ updateLockedAttribute: () => {},
+ });
+
+ expect(wrapper.text()).toContain('Lock');
+ });
+});
diff --git a/spec/frontend/sidebar/lock/lock_issue_sidebar_spec.js b/spec/frontend/sidebar/lock/lock_issue_sidebar_spec.js
new file mode 100644
index 00000000000..00997326d87
--- /dev/null
+++ b/spec/frontend/sidebar/lock/lock_issue_sidebar_spec.js
@@ -0,0 +1,99 @@
+import Vue from 'vue';
+import { mockTracking, triggerEvent } from 'helpers/tracking_helper';
+import lockIssueSidebar from '~/sidebar/components/lock/lock_issue_sidebar.vue';
+
+describe('LockIssueSidebar', () => {
+ let vm1;
+ let vm2;
+
+ beforeEach(() => {
+ const Component = Vue.extend(lockIssueSidebar);
+
+ const mediator = {
+ service: {
+ update: Promise.resolve(true),
+ },
+
+ store: {
+ isLockDialogOpen: false,
+ },
+ };
+
+ vm1 = new Component({
+ propsData: {
+ isLocked: true,
+ isEditable: true,
+ mediator,
+ issuableType: 'issue',
+ },
+ }).$mount();
+
+ vm2 = new Component({
+ propsData: {
+ isLocked: false,
+ isEditable: false,
+ mediator,
+ issuableType: 'merge_request',
+ },
+ }).$mount();
+ });
+
+ it('shows if locked and/or editable', () => {
+ expect(vm1.$el.innerHTML.includes('Edit')).toBe(true);
+
+ expect(vm1.$el.innerHTML.includes('Locked')).toBe(true);
+
+ expect(vm2.$el.innerHTML.includes('Unlocked')).toBe(true);
+ });
+
+ it('displays the edit form when editable', done => {
+ expect(vm1.isLockDialogOpen).toBe(false);
+
+ vm1.$el.querySelector('.lock-edit').click();
+
+ expect(vm1.isLockDialogOpen).toBe(true);
+
+ vm1.$nextTick(() => {
+ expect(vm1.$el.innerHTML.includes('Unlock this issue?')).toBe(true);
+
+ done();
+ });
+ });
+
+ it('tracks an event when "Edit" is clicked', () => {
+ const spy = mockTracking('_category_', vm1.$el, jest.spyOn);
+ triggerEvent('.lock-edit');
+
+ expect(spy).toHaveBeenCalledWith('_category_', 'click_edit_button', {
+ label: 'right_sidebar',
+ property: 'lock_issue',
+ });
+ });
+
+ it('displays the edit form when opened from collapsed state', done => {
+ expect(vm1.isLockDialogOpen).toBe(false);
+
+ vm1.$el.querySelector('.sidebar-collapsed-icon').click();
+
+ expect(vm1.isLockDialogOpen).toBe(true);
+
+ setImmediate(() => {
+ expect(vm1.$el.innerHTML.includes('Unlock this issue?')).toBe(true);
+
+ done();
+ });
+ });
+
+ it('does not display the edit form when opened from collapsed state if not editable', done => {
+ expect(vm2.isLockDialogOpen).toBe(false);
+
+ vm2.$el.querySelector('.sidebar-collapsed-icon').click();
+
+ Vue.nextTick()
+ .then(() => {
+ expect(vm2.isLockDialogOpen).toBe(false);
+ })
+ .then(done)
+ .catch(done.fail);
+ });
+});
diff --git a/spec/frontend/sidebar/participants_spec.js b/spec/frontend/sidebar/participants_spec.js
new file mode 100644
index 00000000000..ebe94582588
--- /dev/null
+++ b/spec/frontend/sidebar/participants_spec.js
@@ -0,0 +1,206 @@
+import { GlLoadingIcon } from '@gitlab/ui';
+import { shallowMount } from '@vue/test-utils';
+import Vue from 'vue';
+import Participants from '~/sidebar/components/participants/participants.vue';
+
+const PARTICIPANT = {
+ id: 1,
+ state: 'active',
+ username: 'marcene',
+ name: 'Allie Will',
+ web_url: 'foo.com',
+ avatar_url: 'gravatar.com/avatar/xxx',
+};
+
+const PARTICIPANT_LIST = [PARTICIPANT, { ...PARTICIPANT, id: 2 }, { ...PARTICIPANT, id: 3 }];
+
+describe('Participants', () => {
+ let wrapper;
+
+ const getMoreParticipantsButton = () => wrapper.find('button');
+
+ const getCollapsedParticipantsCount = () => wrapper.find('[data-testid="collapsed-count"]');
+
+ const mountComponent = propsData =>
+ shallowMount(Participants, {
+ propsData,
+ });
+
+ afterEach(() => {
+ wrapper.destroy();
+ wrapper = null;
+ });
+
+ describe('collapsed sidebar state', () => {
+ it('shows loading spinner when loading', () => {
+ wrapper = mountComponent({
+ loading: true,
+ });
+
+ expect(wrapper.contains(GlLoadingIcon)).toBe(true);
+ });
+
+ it('does not show loading spinner not loading', () => {
+ wrapper = mountComponent({
+ loading: false,
+ });
+
+ expect(wrapper.contains(GlLoadingIcon)).toBe(false);
+ });
+
+ it('shows participant count when given', () => {
+ wrapper = mountComponent({
+ loading: false,
+ participants: PARTICIPANT_LIST,
+ });
+
+ expect(getCollapsedParticipantsCount().text()).toBe(`${PARTICIPANT_LIST.length}`);
+ });
+
+ it('shows full participant count when there are hidden participants', () => {
+ wrapper = mountComponent({
+ loading: false,
+ participants: PARTICIPANT_LIST,
+ numberOfLessParticipants: 1,
+ });
+
+ expect(getCollapsedParticipantsCount().text()).toBe(`${PARTICIPANT_LIST.length}`);
+ });
+ });
+
+ describe('expanded sidebar state', () => {
+ it('shows loading spinner when loading', () => {
+ wrapper = mountComponent({
+ loading: true,
+ });
+
+ expect(wrapper.contains(GlLoadingIcon)).toBe(true);
+ });
+
+ it('when only showing visible participants, shows an avatar only for each participant under the limit', () => {
+ const numberOfLessParticipants = 2;
+ wrapper = mountComponent({
+ loading: false,
+ participants: PARTICIPANT_LIST,
+ numberOfLessParticipants,
+ });
+
+ wrapper.setData({
+ isShowingMoreParticipants: false,
+ });
+
+ return Vue.nextTick().then(() => {
+ expect(wrapper.findAll('.participants-author')).toHaveLength(numberOfLessParticipants);
+ });
+ });
+
+ it('when only showing all participants, each has an avatar', () => {
+ wrapper = mountComponent({
+ loading: false,
+ participants: PARTICIPANT_LIST,
+ numberOfLessParticipants: 2,
+ });
+
+ wrapper.setData({
+ isShowingMoreParticipants: true,
+ });
+
+ return Vue.nextTick().then(() => {
+ expect(wrapper.findAll('.participants-author')).toHaveLength(PARTICIPANT_LIST.length);
+ });
+ });
+
+ it('does not have more participants link when they can all be shown', () => {
+ const numberOfLessParticipants = 100;
+ wrapper = mountComponent({
+ loading: false,
+ participants: PARTICIPANT_LIST,
+ numberOfLessParticipants,
+ });
+
+ expect(PARTICIPANT_LIST.length).toBeLessThan(numberOfLessParticipants);
+ expect(getMoreParticipantsButton().exists()).toBe(false);
+ });
+
+ it('when too many participants, has more participants link to show more', () => {
+ wrapper = mountComponent({
+ loading: false,
+ participants: PARTICIPANT_LIST,
+ numberOfLessParticipants: 2,
+ });
+
+ wrapper.setData({
+ isShowingMoreParticipants: false,
+ });
+
+ return Vue.nextTick().then(() => {
+ expect(getMoreParticipantsButton().text()).toBe('+ 1 more');
+ });
+ });
+
+ it('when too many participants and already showing them, has more participants link to show less', () => {
+ wrapper = mountComponent({
+ loading: false,
+ participants: PARTICIPANT_LIST,
+ numberOfLessParticipants: 2,
+ });
+
+ wrapper.setData({
+ isShowingMoreParticipants: true,
+ });
+
+ return Vue.nextTick().then(() => {
+ expect(getMoreParticipantsButton().text()).toBe('- show less');
+ });
+ });
+
+ it('clicking more participants link emits event', () => {
+ wrapper = mountComponent({
+ loading: false,
+ participants: PARTICIPANT_LIST,
+ numberOfLessParticipants: 2,
+ });
+
+ expect(wrapper.vm.isShowingMoreParticipants).toBe(false);
+
+ getMoreParticipantsButton().trigger('click');
+
+ expect(wrapper.vm.isShowingMoreParticipants).toBe(true);
+ });
+
+ it('clicking on participants icon emits `toggleSidebar` event', () => {
+ wrapper = mountComponent({
+ loading: false,
+ participants: PARTICIPANT_LIST,
+ numberOfLessParticipants: 2,
+ });
+
+ const spy = jest.spyOn(wrapper.vm, '$emit');
+
+ wrapper.find('.sidebar-collapsed-icon').trigger('click');
+
+ return Vue.nextTick(() => {
+ expect(spy).toHaveBeenCalledWith('toggleSidebar');
+
+ spy.mockRestore();
+ });
+ });
+ });
+
+ describe('when not showing participants label', () => {
+ beforeEach(() => {
+ wrapper = mountComponent({
+ participants: PARTICIPANT_LIST,
+ showParticipantLabel: false,
+ });
+ });
+
+ it('does not show sidebar collapsed icon', () => {
+ expect(wrapper.contains('.sidebar-collapsed-icon')).toBe(false);
+ });
+
+ it('does not show participants label title', () => {
+ expect(wrapper.contains('.title')).toBe(false);
+ });
+ });
+});
diff --git a/spec/frontend/sidebar/sidebar_assignees_spec.js b/spec/frontend/sidebar/sidebar_assignees_spec.js
index c1876066a21..88e2d2c9514 100644
--- a/spec/frontend/sidebar/sidebar_assignees_spec.js
+++ b/spec/frontend/sidebar/sidebar_assignees_spec.js
@@ -3,6 +3,7 @@ import AxiosMockAdapter from 'axios-mock-adapter';
import axios from 'axios';
import SidebarAssignees from '~/sidebar/components/assignees/sidebar_assignees.vue';
import Assigness from '~/sidebar/components/assignees/assignees.vue';
+import AssigneesRealtime from '~/sidebar/components/assignees/assignees_realtime.vue';
import SidebarMediator from '~/sidebar/sidebar_mediator';
import SidebarService from '~/sidebar/services/sidebar_service';
import SidebarStore from '~/sidebar/stores/sidebar_store';
@@ -12,12 +13,19 @@ describe('sidebar assignees', () => {
let wrapper;
let mediator;
let axiosMock;
-
- const createComponent = () => {
+ const createComponent = (realTimeIssueSidebar = false, props) => {
wrapper = shallowMount(SidebarAssignees, {
propsData: {
+ issuableIid: '1',
mediator,
field: '',
+ projectPath: 'projectPath',
+ ...props,
+ },
+ provide: {
+ glFeatures: {
+ realTimeIssueSidebar,
+ },
},
// Attaching to document is required because this component emits something from the parent element :/
attachToDocument: true,
@@ -30,8 +38,6 @@ describe('sidebar assignees', () => {
jest.spyOn(mediator, 'saveAssignees');
jest.spyOn(mediator, 'assignYourself');
-
- createComponent();
});
afterEach(() => {
@@ -45,6 +51,8 @@ describe('sidebar assignees', () => {
});
it('calls the mediator when saves the assignees', () => {
+ createComponent();
+
expect(mediator.saveAssignees).not.toHaveBeenCalled();
wrapper.vm.saveAssignees();
@@ -53,6 +61,8 @@ describe('sidebar assignees', () => {
});
it('calls the mediator when "assignSelf" method is called', () => {
+ createComponent();
+
expect(mediator.assignYourself).not.toHaveBeenCalled();
expect(mediator.store.assignees.length).toBe(0);
@@ -63,6 +73,8 @@ describe('sidebar assignees', () => {
});
it('hides assignees until fetched', () => {
+ createComponent();
+
expect(wrapper.find(Assigness).exists()).toBe(false);
wrapper.vm.store.isFetching.assignees = false;
@@ -71,4 +83,30 @@ describe('sidebar assignees', () => {
expect(wrapper.find(Assigness).exists()).toBe(true);
});
});
+
+ describe('when realTimeIssueSidebar is turned on', () => {
+ describe('when issuableType is issue', () => {
+ it('finds AssigneesRealtime componeont', () => {
+ createComponent(true);
+
+ expect(wrapper.find(AssigneesRealtime).exists()).toBe(true);
+ });
+ });
+
+ describe('when issuableType is MR', () => {
+ it('does not find AssigneesRealtime componeont', () => {
+ createComponent(true, { issuableType: 'MR' });
+
+ expect(wrapper.find(AssigneesRealtime).exists()).toBe(false);
+ });
+ });
+ });
+
+ describe('when realTimeIssueSidebar is turned off', () => {
+ it('does not find AssigneesRealtime', () => {
+ createComponent(false, { issuableType: 'issue' });
+
+ expect(wrapper.find(AssigneesRealtime).exists()).toBe(false);
+ });
+ });
});
diff --git a/spec/frontend/sidebar/sidebar_mediator_spec.js b/spec/frontend/sidebar/sidebar_mediator_spec.js
new file mode 100644
index 00000000000..0892d452966
--- /dev/null
+++ b/spec/frontend/sidebar/sidebar_mediator_spec.js
@@ -0,0 +1,135 @@
+import MockAdapter from 'axios-mock-adapter';
+import axios from '~/lib/utils/axios_utils';
+import * as urlUtility from '~/lib/utils/url_utility';
+import SidebarService, { gqClient } from '~/sidebar/services/sidebar_service';
+import SidebarMediator from '~/sidebar/sidebar_mediator';
+import SidebarStore from '~/sidebar/stores/sidebar_store';
+import Mock from './mock_data';
+
+describe('Sidebar mediator', () => {
+ const { mediator: mediatorMockData } = Mock;
+ let mock;
+ let mediator;
+
+ beforeEach(() => {
+ mock = new MockAdapter(axios);
+ mediator = new SidebarMediator(mediatorMockData);
+ });
+
+ afterEach(() => {
+ SidebarService.singleton = null;
+ SidebarStore.singleton = null;
+ SidebarMediator.singleton = null;
+ mock.restore();
+ });
+
+ it('assigns yourself ', () => {
+ mediator.assignYourself();
+
+ expect(mediator.store.currentUser).toEqual(mediatorMockData.currentUser);
+ expect(mediator.store.assignees[0]).toEqual(mediatorMockData.currentUser);
+ });
+
+ it('saves assignees', () => {
+ mock.onPut(mediatorMockData.endpoint).reply(200, {});
+
+ return mediator.saveAssignees('issue[assignee_ids]').then(resp => {
+ expect(resp.status).toEqual(200);
+ });
+ });
+
+ it('fetches the data', () => {
+ const mockData = Mock.responseMap.GET[mediatorMockData.endpoint];
+ mock.onGet(mediatorMockData.endpoint).reply(200, mockData);
+
+ const mockGraphQlData = Mock.graphQlResponseData;
+ const graphQlSpy = jest.spyOn(gqClient, 'query').mockReturnValue({
+ data: mockGraphQlData,
+ });
+ const spy = jest.spyOn(mediator, 'processFetchedData').mockReturnValue(Promise.resolve());
+
+ return mediator.fetch().then(() => {
+ expect(spy).toHaveBeenCalledWith(mockData, mockGraphQlData);
+
+ spy.mockRestore();
+ graphQlSpy.mockRestore();
+ });
+ });
+
+ it('processes fetched data', () => {
+ const mockData = Mock.responseMap.GET[mediatorMockData.endpoint];
+ mediator.processFetchedData(mockData);
+
+ expect(mediator.store.assignees).toEqual(mockData.assignees);
+ expect(mediator.store.humanTimeEstimate).toEqual(mockData.human_time_estimate);
+ expect(mediator.store.humanTotalTimeSpent).toEqual(mockData.human_total_time_spent);
+ expect(mediator.store.participants).toEqual(mockData.participants);
+ expect(mediator.store.subscribed).toEqual(mockData.subscribed);
+ expect(mediator.store.timeEstimate).toEqual(mockData.time_estimate);
+ expect(mediator.store.totalTimeSpent).toEqual(mockData.total_time_spent);
+ });
+
+ it('sets moveToProjectId', () => {
+ const projectId = 7;
+ const spy = jest.spyOn(mediator.store, 'setMoveToProjectId').mockReturnValue(Promise.resolve());
+
+ mediator.setMoveToProjectId(projectId);
+
+ expect(spy).toHaveBeenCalledWith(projectId);
+
+ spy.mockRestore();
+ });
+
+ it('fetches autocomplete projects', () => {
+ const searchTerm = 'foo';
+ mock.onGet(mediatorMockData.projectsAutocompleteEndpoint).reply(200, {});
+ const getterSpy = jest
+ .spyOn(mediator.service, 'getProjectsAutocomplete')
+ .mockReturnValue(Promise.resolve({ data: {} }));
+ const setterSpy = jest
+ .spyOn(mediator.store, 'setAutocompleteProjects')
+ .mockReturnValue(Promise.resolve());
+
+ return mediator.fetchAutocompleteProjects(searchTerm).then(() => {
+ expect(getterSpy).toHaveBeenCalledWith(searchTerm);
+ expect(setterSpy).toHaveBeenCalled();
+
+ getterSpy.mockRestore();
+ setterSpy.mockRestore();
+ });
+ });
+
+ it('moves issue', () => {
+ const mockData = Mock.responseMap.POST[mediatorMockData.moveIssueEndpoint];
+ const moveToProjectId = 7;
+ mock.onPost(mediatorMockData.moveIssueEndpoint).reply(200, mockData);
+ mediator.store.setMoveToProjectId(moveToProjectId);
+ const moveIssueSpy = jest
+ .spyOn(mediator.service, 'moveIssue')
+ .mockReturnValue(Promise.resolve({ data: { web_url: mockData.web_url } }));
+ const urlSpy = jest.spyOn(urlUtility, 'visitUrl').mockReturnValue({});
+
+ return mediator.moveIssue().then(() => {
+ expect(moveIssueSpy).toHaveBeenCalledWith(moveToProjectId);
+ expect(urlSpy).toHaveBeenCalledWith(mockData.web_url);
+
+ moveIssueSpy.mockRestore();
+ urlSpy.mockRestore();
+ });
+ });
+
+ it('toggle subscription', () => {
+ mediator.store.setSubscribedState(false);
+ mock.onPost(mediatorMockData.toggleSubscriptionEndpoint).reply(200, {});
+ const spy = jest
+ .spyOn(mediator.service, 'toggleSubscription')
+ .mockReturnValue(Promise.resolve());
+
+ return mediator.toggleSubscription().then(() => {
+ expect(spy).toHaveBeenCalled();
+ expect(mediator.store.subscribed).toEqual(true);
+
+ spy.mockRestore();
+ });
+ });
+});
diff --git a/spec/frontend/sidebar/sidebar_move_issue_spec.js b/spec/frontend/sidebar/sidebar_move_issue_spec.js
new file mode 100644
index 00000000000..db0d3e06272
--- /dev/null
+++ b/spec/frontend/sidebar/sidebar_move_issue_spec.js
@@ -0,0 +1,167 @@
+import $ from 'jquery';
+import MockAdapter from 'axios-mock-adapter';
+import axios from '~/lib/utils/axios_utils';
+import SidebarMediator from '~/sidebar/sidebar_mediator';
+import SidebarStore from '~/sidebar/stores/sidebar_store';
+import SidebarService from '~/sidebar/services/sidebar_service';
+import SidebarMoveIssue from '~/sidebar/lib/sidebar_move_issue';
+import Mock from './mock_data';
+
+describe('SidebarMoveIssue', () => {
+ let mock;
+ const test = {};
+
+ beforeEach(() => {
+ mock = new MockAdapter(axios);
+ const mockData = Mock.responseMap.GET['/autocomplete/projects?project_id=15'];
+ mock.onGet('/autocomplete/projects?project_id=15').reply(200, mockData);
+ test.mediator = new SidebarMediator(Mock.mediator);
+ test.$content = $(`
+ <div class="dropdown">
+ <div class="js-toggle"></div>
+ <div class="dropdown-menu">
+ <div class="dropdown-content"></div>
+ </div>
+ <div class="js-confirm-button"></div>
+ </div>
+ `);
+ test.$toggleButton = test.$content.find('.js-toggle');
+ test.$confirmButton = test.$content.find('.js-confirm-button');
+
+ test.sidebarMoveIssue = new SidebarMoveIssue(
+ test.mediator,
+ test.$toggleButton,
+ test.$confirmButton,
+ );
+ test.sidebarMoveIssue.init();
+ });
+
+ afterEach(() => {
+ SidebarService.singleton = null;
+ SidebarStore.singleton = null;
+ SidebarMediator.singleton = null;
+
+ test.sidebarMoveIssue.destroy();
+ mock.restore();
+ });
+
+ describe('init', () => {
+ it('should initialize the dropdown and listeners', () => {
+ jest.spyOn(test.sidebarMoveIssue, 'initDropdown').mockImplementation(() => {});
+ jest.spyOn(test.sidebarMoveIssue, 'addEventListeners').mockImplementation(() => {});
+
+ test.sidebarMoveIssue.init();
+
+ expect(test.sidebarMoveIssue.initDropdown).toHaveBeenCalled();
+ expect(test.sidebarMoveIssue.addEventListeners).toHaveBeenCalled();
+ });
+ });
+
+ describe('destroy', () => {
+ it('should remove the listeners', () => {
+ jest.spyOn(test.sidebarMoveIssue, 'removeEventListeners').mockImplementation(() => {});
+
+ test.sidebarMoveIssue.destroy();
+
+ expect(test.sidebarMoveIssue.removeEventListeners).toHaveBeenCalled();
+ });
+ });
+
+ describe('initDropdown', () => {
+ it('should initialize the gl_dropdown', () => {
+ jest.spyOn($.fn, 'glDropdown').mockImplementation(() => {});
+
+ test.sidebarMoveIssue.initDropdown();
+
+ expect($.fn.glDropdown).toHaveBeenCalled();
+ });
+
+ it('escapes html from project name', done => {
+ test.$toggleButton.dropdown('toggle');
+
+ setImmediate(() => {
+ expect(test.$content.find('.js-move-issue-dropdown-item')[1].innerHTML.trim()).toEqual(
+ '&lt;img src=x onerror=alert(document.domain)&gt; foo / bar',
+ );
+ done();
+ });
+ });
+ });
+
+ describe('onConfirmClicked', () => {
+ it('should move the issue with valid project ID', () => {
+ jest.spyOn(test.mediator, 'moveIssue').mockReturnValue(Promise.resolve());
+ test.mediator.setMoveToProjectId(7);
+
+ test.sidebarMoveIssue.onConfirmClicked();
+
+ expect(test.mediator.moveIssue).toHaveBeenCalled();
+ expect(test.$confirmButton.prop('disabled')).toBeTruthy();
+ expect(test.$confirmButton.hasClass('is-loading')).toBe(true);
+ });
+
+ it('should remove loading state from confirm button on failure', done => {
+ jest.spyOn(window, 'Flash').mockImplementation(() => {});
+ jest.spyOn(test.mediator, 'moveIssue').mockReturnValue(Promise.reject());
+ test.mediator.setMoveToProjectId(7);
+
+ test.sidebarMoveIssue.onConfirmClicked();
+
+ expect(test.mediator.moveIssue).toHaveBeenCalled();
+ // Wait for the move issue request to fail
+ setImmediate(() => {
+ expect(window.Flash).toHaveBeenCalled();
+ expect(test.$confirmButton.prop('disabled')).toBeFalsy();
+ expect(test.$confirmButton.hasClass('is-loading')).toBe(false);
+ done();
+ });
+ });
+
+ it('should not move the issue with id=0', () => {
+ jest.spyOn(test.mediator, 'moveIssue').mockImplementation(() => {});
+ test.mediator.setMoveToProjectId(0);
+
+ test.sidebarMoveIssue.onConfirmClicked();
+
+ expect(test.mediator.moveIssue).not.toHaveBeenCalled();
+ });
+ });
+
+ it('should set moveToProjectId on dropdown item "No project" click', done => {
+ jest.spyOn(test.mediator, 'setMoveToProjectId').mockImplementation(() => {});
+
+ // Open the dropdown
+ test.$toggleButton.dropdown('toggle');
+
+ // Wait for the autocomplete request to finish
+ setImmediate(() => {
+ test.$content
+ .find('.js-move-issue-dropdown-item')
+ .eq(0)
+ .trigger('click');
+
+ expect(test.mediator.setMoveToProjectId).toHaveBeenCalledWith(0);
+ expect(test.$confirmButton.prop('disabled')).toBeTruthy();
+ done();
+ });
+ });
+
+ it('should set moveToProjectId on dropdown item click', done => {
+ jest.spyOn(test.mediator, 'setMoveToProjectId').mockImplementation(() => {});
+
+ // Open the dropdown
+ test.$toggleButton.dropdown('toggle');
+
+ // Wait for the autocomplete request to finish
+ setImmediate(() => {
+ test.$content
+ .find('.js-move-issue-dropdown-item')
+ .eq(1)
+ .trigger('click');
+
+ expect(test.mediator.setMoveToProjectId).toHaveBeenCalledWith(20);
+ expect(test.$confirmButton.attr('disabled')).toBe(undefined);
+ done();
+ });
+ });
+});
diff --git a/spec/frontend/sidebar/sidebar_subscriptions_spec.js b/spec/frontend/sidebar/sidebar_subscriptions_spec.js
new file mode 100644
index 00000000000..18aaeabe3dd
--- /dev/null
+++ b/spec/frontend/sidebar/sidebar_subscriptions_spec.js
@@ -0,0 +1,36 @@
+import { shallowMount } from '@vue/test-utils';
+import SidebarSubscriptions from '~/sidebar/components/subscriptions/sidebar_subscriptions.vue';
+import SidebarMediator from '~/sidebar/sidebar_mediator';
+import SidebarService from '~/sidebar/services/sidebar_service';
+import SidebarStore from '~/sidebar/stores/sidebar_store';
+import Mock from './mock_data';
+
+describe('Sidebar Subscriptions', () => {
+ let wrapper;
+ let mediator;
+
+ beforeEach(() => {
+ mediator = new SidebarMediator(Mock.mediator);
+ wrapper = shallowMount(SidebarSubscriptions, {
+ propsData: {
+ mediator,
+ },
+ });
+ });
+
+ afterEach(() => {
+ wrapper.destroy();
+ SidebarService.singleton = null;
+ SidebarStore.singleton = null;
+ SidebarMediator.singleton = null;
+ });
+
+ it('calls the mediator toggleSubscription on event', () => {
+ const spy = jest.spyOn(mediator, 'toggleSubscription').mockReturnValue(Promise.resolve());
+
+ wrapper.vm.onToggleSubscription();
+
+ expect(spy).toHaveBeenCalled();
+ spy.mockRestore();
+ });
+});
diff --git a/spec/frontend/sidebar/subscriptions_spec.js b/spec/frontend/sidebar/subscriptions_spec.js
new file mode 100644
index 00000000000..cce35666985
--- /dev/null
+++ b/spec/frontend/sidebar/subscriptions_spec.js
@@ -0,0 +1,106 @@
+import { shallowMount } from '@vue/test-utils';
+import Subscriptions from '~/sidebar/components/subscriptions/subscriptions.vue';
+import eventHub from '~/sidebar/event_hub';
+import ToggleButton from '~/vue_shared/components/toggle_button.vue';
+
+describe('Subscriptions', () => {
+ let wrapper;
+
+ const findToggleButton = () => wrapper.find(ToggleButton);
+
+ const mountComponent = propsData =>
+ shallowMount(Subscriptions, {
+ propsData,
+ });
+
+ afterEach(() => {
+ wrapper.destroy();
+ wrapper = null;
+ });
+
+ it('shows loading spinner when loading', () => {
+ wrapper = mountComponent({
+ loading: true,
+ subscribed: undefined,
+ });
+
+ expect(findToggleButton().attributes('isloading')).toBe('true');
+ });
+
+ it('is toggled "off" when currently not subscribed', () => {
+ wrapper = mountComponent({
+ subscribed: false,
+ });
+
+ expect(findToggleButton().attributes('value')).toBeFalsy();
+ });
+
+ it('is toggled "on" when currently subscribed', () => {
+ wrapper = mountComponent({
+ subscribed: true,
+ });
+
+ expect(findToggleButton().attributes('value')).toBe('true');
+ });
+
+ it('toggleSubscription method emits `toggleSubscription` event on eventHub and Component', () => {
+ const id = 42;
+ wrapper = mountComponent({ subscribed: true, id });
+ const eventHubSpy = jest.spyOn(eventHub, '$emit');
+ const wrapperEmitSpy = jest.spyOn(wrapper.vm, '$emit');
+
+ wrapper.vm.toggleSubscription();
+
+ expect(eventHubSpy).toHaveBeenCalledWith('toggleSubscription', id);
+ expect(wrapperEmitSpy).toHaveBeenCalledWith('toggleSubscription', id);
+ eventHubSpy.mockRestore();
+ wrapperEmitSpy.mockRestore();
+ });
+
+ it('tracks the event when toggled', () => {
+ wrapper = mountComponent({ subscribed: true });
+
+ const wrapperTrackSpy = jest.spyOn(wrapper.vm, 'track');
+
+ wrapper.vm.toggleSubscription();
+
+ expect(wrapperTrackSpy).toHaveBeenCalledWith('toggle_button', {
+ property: 'notifications',
+ value: 0,
+ });
+ wrapperTrackSpy.mockRestore();
+ });
+
+ it('onClickCollapsedIcon method emits `toggleSidebar` event on component', () => {
+ wrapper = mountComponent({ subscribed: true });
+ const spy = jest.spyOn(wrapper.vm, '$emit');
+
+ wrapper.vm.onClickCollapsedIcon();
+
+ expect(spy).toHaveBeenCalledWith('toggleSidebar');
+ spy.mockRestore();
+ });
+
+ describe('given project emails are disabled', () => {
+ const subscribeDisabledDescription = 'Notifications have been disabled';
+
+ beforeEach(() => {
+ wrapper = mountComponent({
+ subscribed: false,
+ projectEmailsDisabled: true,
+ subscribeDisabledDescription,
+ });
+ });
+
+ it('sets the correct display text', () => {
+ expect(wrapper.find('.issuable-header-text').text()).toContain(subscribeDisabledDescription);
+ expect(wrapper.find({ ref: 'tooltip' }).attributes('data-original-title')).toBe(
+ subscribeDisabledDescription,
+ );
+ });
+
+ it('does not render the toggle button', () => {
+ expect(wrapper.contains('.js-issuable-subscribe-button')).toBe(false);
+ });
+ });
+});
diff --git a/spec/frontend/smart_interval_spec.js b/spec/frontend/smart_interval_spec.js
index b32ac99e4e4..1a2fd7ff8f1 100644
--- a/spec/frontend/smart_interval_spec.js
+++ b/spec/frontend/smart_interval_spec.js
@@ -3,8 +3,6 @@ import { assignIn } from 'lodash';
import waitForPromises from 'helpers/wait_for_promises';
import SmartInterval from '~/smart_interval';
-jest.useFakeTimers();
-
let interval;
describe('SmartInterval', () => {
diff --git a/spec/frontend/snippet/snippet_bundle_spec.js b/spec/frontend/snippet/snippet_bundle_spec.js
index 12d20d5cd85..38d05243c65 100644
--- a/spec/frontend/snippet/snippet_bundle_spec.js
+++ b/spec/frontend/snippet/snippet_bundle_spec.js
@@ -1,94 +1,85 @@
import Editor from '~/editor/editor_lite';
-import { initEditor } from '~/snippet/snippet_bundle';
+import initEditor from '~/snippet/snippet_bundle';
import { setHTMLFixture } from 'helpers/fixtures';
jest.mock('~/editor/editor_lite', () => jest.fn());
describe('Snippet editor', () => {
- describe('Monaco editor for Snippets', () => {
- let oldGon;
- let editorEl;
- let contentEl;
- let fileNameEl;
- let form;
-
- const mockName = 'foo.bar';
- const mockContent = 'Foo Bar';
- const updatedMockContent = 'New Foo Bar';
-
- const mockEditor = {
- createInstance: jest.fn(),
- updateModelLanguage: jest.fn(),
- getValue: jest.fn().mockReturnValueOnce(updatedMockContent),
- };
- Editor.mockImplementation(() => mockEditor);
-
- function setUpFixture(name, content) {
- setHTMLFixture(`
- <div class="snippet-form-holder">
- <form>
- <input class="js-snippet-file-name" type="text" value="${name}">
- <input class="snippet-file-content" type="hidden" value="${content}">
- <pre id="editor"></pre>
- </form>
- </div>
- `);
- }
-
- function bootstrap(name = '', content = '') {
- setUpFixture(name, content);
- editorEl = document.getElementById('editor');
- contentEl = document.querySelector('.snippet-file-content');
- fileNameEl = document.querySelector('.js-snippet-file-name');
- form = document.querySelector('.snippet-form-holder form');
-
- initEditor();
- }
-
- function createEvent(name) {
- return new Event(name, {
- view: window,
- bubbles: true,
- cancelable: true,
- });
- }
-
- beforeEach(() => {
- oldGon = window.gon;
- window.gon = { features: { monacoSnippets: true } };
- bootstrap(mockName, mockContent);
+ let editorEl;
+ let contentEl;
+ let fileNameEl;
+ let form;
+
+ const mockName = 'foo.bar';
+ const mockContent = 'Foo Bar';
+ const updatedMockContent = 'New Foo Bar';
+
+ const mockEditor = {
+ createInstance: jest.fn(),
+ updateModelLanguage: jest.fn(),
+ getValue: jest.fn().mockReturnValueOnce(updatedMockContent),
+ };
+ Editor.mockImplementation(() => mockEditor);
+
+ function setUpFixture(name, content) {
+ setHTMLFixture(`
+ <div class="snippet-form-holder">
+ <form>
+ <input class="js-snippet-file-name" type="text" value="${name}">
+ <input class="snippet-file-content" type="hidden" value="${content}">
+ <pre id="editor"></pre>
+ </form>
+ </div>
+ `);
+ }
+
+ function bootstrap(name = '', content = '') {
+ setUpFixture(name, content);
+ editorEl = document.getElementById('editor');
+ contentEl = document.querySelector('.snippet-file-content');
+ fileNameEl = document.querySelector('.js-snippet-file-name');
+ form = document.querySelector('.snippet-form-holder form');
+
+ initEditor();
+ }
+
+ function createEvent(name) {
+ return new Event(name, {
+ view: window,
+ bubbles: true,
+ cancelable: true,
});
+ }
- afterEach(() => {
- window.gon = oldGon;
- });
+ beforeEach(() => {
+ bootstrap(mockName, mockContent);
+ });
- it('correctly initializes Editor', () => {
- expect(mockEditor.createInstance).toHaveBeenCalledWith({
- el: editorEl,
- blobPath: mockName,
- blobContent: mockContent,
- });
+ it('correctly initializes Editor', () => {
+ expect(mockEditor.createInstance).toHaveBeenCalledWith({
+ el: editorEl,
+ blobPath: mockName,
+ blobContent: mockContent,
});
+ });
- it('listens to file name changes and updates syntax highlighting of code', () => {
- expect(mockEditor.updateModelLanguage).not.toHaveBeenCalled();
+ it('listens to file name changes and updates syntax highlighting of code', () => {
+ expect(mockEditor.updateModelLanguage).not.toHaveBeenCalled();
- const event = createEvent('change');
+ const event = createEvent('change');
- fileNameEl.value = updatedMockContent;
- fileNameEl.dispatchEvent(event);
+ fileNameEl.value = updatedMockContent;
+ fileNameEl.dispatchEvent(event);
- expect(mockEditor.updateModelLanguage).toHaveBeenCalledWith(updatedMockContent);
- });
+ expect(mockEditor.updateModelLanguage).toHaveBeenCalledWith(updatedMockContent);
+ });
- it('listens to form submit event and populates the hidden field with most recent version of the content', () => {
- expect(contentEl.value).toBe(mockContent);
+ it('listens to form submit event and populates the hidden field with most recent version of the content', () => {
+ expect(contentEl.value).toBe(mockContent);
- const event = createEvent('submit');
+ const event = createEvent('submit');
- form.dispatchEvent(event);
- expect(contentEl.value).toBe(updatedMockContent);
- });
+ form.dispatchEvent(event);
+ expect(contentEl.value).toBe(updatedMockContent);
});
});
diff --git a/spec/frontend/snippets/components/__snapshots__/snippet_blob_edit_spec.js.snap b/spec/frontend/snippets/components/__snapshots__/snippet_blob_edit_spec.js.snap
index b1bbe2a9710..301ec5652a9 100644
--- a/spec/frontend/snippets/components/__snapshots__/snippet_blob_edit_spec.js.snap
+++ b/spec/frontend/snippets/components/__snapshots__/snippet_blob_edit_spec.js.snap
@@ -12,6 +12,7 @@ exports[`Snippet Blob Edit component rendering matches the snapshot 1`] = `
class="file-holder snippet"
>
<blob-header-edit-stub
+ data-qa-selector="snippet_file_name"
value="lorem.txt"
/>
diff --git a/spec/frontend/snippets/components/__snapshots__/snippet_description_edit_spec.js.snap b/spec/frontend/snippets/components/__snapshots__/snippet_description_edit_spec.js.snap
index 334ceaa064f..9fd4cba5b87 100644
--- a/spec/frontend/snippets/components/__snapshots__/snippet_description_edit_spec.js.snap
+++ b/spec/frontend/snippets/components/__snapshots__/snippet_description_edit_spec.js.snap
@@ -35,8 +35,8 @@ exports[`Snippet Description Edit component rendering matches the snapshot 1`] =
>
<textarea
aria-label="Description"
- class="note-textarea js-gfm-input js-autosize markdown-area
- qa-description-textarea"
+ class="note-textarea js-gfm-input js-autosize markdown-area"
+ data-qa-selector="snippet_description_field"
data-supports-quick-actions="false"
dir="auto"
placeholder="Write a comment or drag your files here…"
diff --git a/spec/frontend/snippets/components/__snapshots__/snippet_description_view_spec.js.snap b/spec/frontend/snippets/components/__snapshots__/snippet_description_view_spec.js.snap
new file mode 100644
index 00000000000..9ebc4e81baf
--- /dev/null
+++ b/spec/frontend/snippets/components/__snapshots__/snippet_description_view_spec.js.snap
@@ -0,0 +1,16 @@
+// Jest Snapshot v1, https://goo.gl/fbAQLP
+
+exports[`Snippet Description component matches the snapshot 1`] = `
+<markdown-field-view-stub
+ class="snippet-description"
+ data-qa-selector="snippet_description_field"
+>
+ <div
+ class="md js-snippet-description"
+ >
+ <h2>
+ The property of Thor
+ </h2>
+ </div>
+</markdown-field-view-stub>
+`;
diff --git a/spec/frontend/snippets/components/edit_spec.js b/spec/frontend/snippets/components/edit_spec.js
index 21a4ccf5a74..ba62a0a92ca 100644
--- a/spec/frontend/snippets/components/edit_spec.js
+++ b/spec/frontend/snippets/components/edit_spec.js
@@ -100,6 +100,7 @@ describe('Snippet Edit app', () => {
});
const findSubmitButton = () => wrapper.find('[type=submit]');
+ const findCancellButton = () => wrapper.find('[data-testid="snippet-cancel-btn"]');
describe('rendering', () => {
it('renders loader while the query is in flight', () => {
@@ -148,6 +149,21 @@ describe('Snippet Edit app', () => {
expect(isBtnDisabled).toBe(expectation);
},
);
+
+ it.each`
+ isNew | status | expectation
+ ${true} | ${`new`} | ${`/snippets`}
+ ${false} | ${`existing`} | ${newlyEditedSnippetUrl}
+ `('sets correct href for the cancel button on a $status snippet', ({ isNew, expectation }) => {
+ createComponent({
+ data: {
+ snippet: { webUrl: newlyEditedSnippetUrl },
+ newSnippet: isNew,
+ },
+ });
+
+ expect(findCancellButton().attributes('href')).toBe(expectation);
+ });
});
describe('functionality', () => {
diff --git a/spec/frontend/snippets/components/snippet_blob_view_spec.js b/spec/frontend/snippets/components/snippet_blob_view_spec.js
index 1f6038bc7f0..d06489cffa9 100644
--- a/spec/frontend/snippets/components/snippet_blob_view_spec.js
+++ b/spec/frontend/snippets/components/snippet_blob_view_spec.js
@@ -3,6 +3,7 @@ import SnippetBlobView from '~/snippets/components/snippet_blob_view.vue';
import BlobHeader from '~/blob/components/blob_header.vue';
import BlobEmbeddable from '~/blob/components/blob_embeddable.vue';
import BlobContent from '~/blob/components/blob_content.vue';
+import { BLOB_RENDER_EVENT_LOAD, BLOB_RENDER_EVENT_SHOW_SOURCE } from '~/blob/components/constants';
import { RichViewer, SimpleViewer } from '~/vue_shared/components/blob_viewers';
import {
SNIPPET_VISIBILITY_PRIVATE,
@@ -29,6 +30,8 @@ describe('Blob Embeddable', () => {
queries: {
blobContent: {
loading: contentLoading,
+ refetch: jest.fn(),
+ skip: true,
},
},
};
@@ -84,9 +87,7 @@ describe('Blob Embeddable', () => {
});
it('sets rich viewer correctly', () => {
- const data = Object.assign({}, dataMock, {
- activeViewerType: RichViewerMock.type,
- });
+ const data = { ...dataMock, activeViewerType: RichViewerMock.type };
createComponent({}, data);
expect(wrapper.find(RichViewer).exists()).toBe(true);
});
@@ -145,4 +146,35 @@ describe('Blob Embeddable', () => {
});
});
});
+
+ describe('functionality', () => {
+ describe('render error', () => {
+ const findContentEl = () => wrapper.find(BlobContent);
+
+ it('correctly sets blob on the blob-content-error component', () => {
+ createComponent();
+ expect(findContentEl().props('blob')).toEqual(BlobMock);
+ });
+
+ it(`refetches blob content on ${BLOB_RENDER_EVENT_LOAD} event`, () => {
+ createComponent();
+
+ expect(wrapper.vm.$apollo.queries.blobContent.refetch).not.toHaveBeenCalled();
+ findContentEl().vm.$emit(BLOB_RENDER_EVENT_LOAD);
+ expect(wrapper.vm.$apollo.queries.blobContent.refetch).toHaveBeenCalledTimes(1);
+ });
+
+ it(`sets '${SimpleViewerMock.type}' as active on ${BLOB_RENDER_EVENT_SHOW_SOURCE} event`, () => {
+ createComponent(
+ {},
+ {
+ activeViewerType: RichViewerMock.type,
+ },
+ );
+
+ findContentEl().vm.$emit(BLOB_RENDER_EVENT_SHOW_SOURCE);
+ expect(wrapper.vm.activeViewerType).toEqual(SimpleViewerMock.type);
+ });
+ });
+ });
});
diff --git a/spec/frontend/snippets/components/snippet_description_view_spec.js b/spec/frontend/snippets/components/snippet_description_view_spec.js
new file mode 100644
index 00000000000..46467ef311e
--- /dev/null
+++ b/spec/frontend/snippets/components/snippet_description_view_spec.js
@@ -0,0 +1,27 @@
+import SnippetDescription from '~/snippets/components/snippet_description_view.vue';
+import { shallowMount } from '@vue/test-utils';
+
+describe('Snippet Description component', () => {
+ let wrapper;
+ const description = '<h2>The property of Thor</h2>';
+
+ function createComponent() {
+ wrapper = shallowMount(SnippetDescription, {
+ propsData: {
+ description,
+ },
+ });
+ }
+
+ beforeEach(() => {
+ createComponent();
+ });
+
+ afterEach(() => {
+ wrapper.destroy();
+ });
+
+ it('matches the snapshot', () => {
+ expect(wrapper.element).toMatchSnapshot();
+ });
+});
diff --git a/spec/frontend/snippets/components/snippet_header_spec.js b/spec/frontend/snippets/components/snippet_header_spec.js
index 16a66c70d6a..5230910b6f5 100644
--- a/spec/frontend/snippets/components/snippet_header_spec.js
+++ b/spec/frontend/snippets/components/snippet_header_spec.js
@@ -7,26 +7,27 @@ import { shallowMount } from '@vue/test-utils';
describe('Snippet header component', () => {
let wrapper;
const snippet = {
- snippet: {
- id: 'gid://gitlab/PersonalSnippet/50',
- title: 'The property of Thor',
- visibilityLevel: 'private',
- webUrl: 'http://personal.dev.null/42',
- userPermissions: {
- adminSnippet: true,
- updateSnippet: true,
- reportSnippet: false,
- },
- project: null,
- author: {
- name: 'Thor Odinson',
- },
+ id: 'gid://gitlab/PersonalSnippet/50',
+ title: 'The property of Thor',
+ visibilityLevel: 'private',
+ webUrl: 'http://personal.dev.null/42',
+ userPermissions: {
+ adminSnippet: true,
+ updateSnippet: true,
+ reportSnippet: false,
+ },
+ project: null,
+ author: {
+ name: 'Thor Odinson',
+ },
+ blob: {
+ binary: false,
},
};
const mutationVariables = {
mutation: DeleteSnippetMutation,
variables: {
- id: snippet.snippet.id,
+ id: snippet.id,
},
};
const errorMsg = 'Foo bar';
@@ -46,10 +47,12 @@ describe('Snippet header component', () => {
loading = false,
permissions = {},
mutationRes = mutationTypes.RESOLVE,
+ snippetProps = {},
} = {}) {
- const defaultProps = Object.assign({}, snippet);
+ // const defaultProps = Object.assign({}, snippet, snippetProps);
+ const defaultProps = Object.assign(snippet, snippetProps);
if (permissions) {
- Object.assign(defaultProps.snippet.userPermissions, {
+ Object.assign(defaultProps.userPermissions, {
...permissions,
});
}
@@ -65,7 +68,9 @@ describe('Snippet header component', () => {
wrapper = shallowMount(SnippetHeader, {
mocks: { $apollo },
propsData: {
- ...defaultProps,
+ snippet: {
+ ...defaultProps,
+ },
},
stubs: {
ApolloMutation,
@@ -126,6 +131,17 @@ describe('Snippet header component', () => {
expect(wrapper.find(GlModal).exists()).toBe(true);
});
+ it('renders Edit button as disabled for binary snippets', () => {
+ createComponent({
+ snippetProps: {
+ blob: {
+ binary: true,
+ },
+ },
+ });
+ expect(wrapper.find('[href*="edit"]').props('disabled')).toBe(true);
+ });
+
describe('Delete mutation', () => {
const { location } = window;
@@ -156,14 +172,34 @@ describe('Snippet header component', () => {
});
});
- it('closes modal and redirects to snippets listing in case of successful mutation', () => {
- createComponent();
- wrapper.vm.closeDeleteModal = jest.fn();
+ describe('in case of successful mutation, closes modal and redirects to correct listing', () => {
+ const createDeleteSnippet = (snippetProps = {}) => {
+ createComponent({
+ snippetProps,
+ });
+ wrapper.vm.closeDeleteModal = jest.fn();
- wrapper.vm.deleteSnippet();
- return wrapper.vm.$nextTick().then(() => {
- expect(wrapper.vm.closeDeleteModal).toHaveBeenCalled();
- expect(window.location.pathname).toEqual('dashboard/snippets');
+ wrapper.vm.deleteSnippet();
+ return wrapper.vm.$nextTick();
+ };
+
+ it('redirects to dashboard/snippets for personal snippet', () => {
+ return createDeleteSnippet().then(() => {
+ expect(wrapper.vm.closeDeleteModal).toHaveBeenCalled();
+ expect(window.location.pathname).toBe('dashboard/snippets');
+ });
+ });
+
+ it('redirects to project snippets for project snippet', () => {
+ const fullPath = 'foo/bar';
+ return createDeleteSnippet({
+ project: {
+ fullPath,
+ },
+ }).then(() => {
+ expect(wrapper.vm.closeDeleteModal).toHaveBeenCalled();
+ expect(window.location.pathname).toBe(`${fullPath}/snippets`);
+ });
});
});
});
diff --git a/spec/frontend/snippets/components/snippet_title_spec.js b/spec/frontend/snippets/components/snippet_title_spec.js
index b49b2008610..88261a75f6c 100644
--- a/spec/frontend/snippets/components/snippet_title_spec.js
+++ b/spec/frontend/snippets/components/snippet_title_spec.js
@@ -1,4 +1,5 @@
import SnippetTitle from '~/snippets/components/snippet_title.vue';
+import SnippetDescription from '~/snippets/components/snippet_description_view.vue';
import { GlSprintf } from '@gitlab/ui';
import { shallowMount } from '@vue/test-utils';
@@ -16,7 +17,7 @@ describe('Snippet header component', () => {
};
function createComponent({ props = snippet } = {}) {
- const defaultProps = Object.assign({}, props);
+ const defaultProps = { ...props };
wrapper = shallowMount(SnippetTitle, {
propsData: {
@@ -36,8 +37,9 @@ describe('Snippet header component', () => {
it('renders snippets title and description', () => {
createComponent();
+
expect(wrapper.text().trim()).toContain(title);
- expect(wrapper.find('.js-snippet-description').element.innerHTML).toBe(descriptionHtml);
+ expect(wrapper.find(SnippetDescription).props('description')).toBe(descriptionHtml);
});
it('does not render recent changes time stamp if there were no updates', () => {
diff --git a/spec/frontend/static_site_editor/components/edit_area_spec.js b/spec/frontend/static_site_editor/components/edit_area_spec.js
new file mode 100644
index 00000000000..bfe41f65d6e
--- /dev/null
+++ b/spec/frontend/static_site_editor/components/edit_area_spec.js
@@ -0,0 +1,76 @@
+import { shallowMount } from '@vue/test-utils';
+
+import RichContentEditor from '~/vue_shared/components/rich_content_editor/rich_content_editor.vue';
+
+import EditArea from '~/static_site_editor/components/edit_area.vue';
+import PublishToolbar from '~/static_site_editor/components/publish_toolbar.vue';
+import EditHeader from '~/static_site_editor/components/edit_header.vue';
+
+import { sourceContentTitle as title, sourceContent as content, returnUrl } from '../mock_data';
+
+describe('~/static_site_editor/components/edit_area.vue', () => {
+ let wrapper;
+ const savingChanges = true;
+ const newContent = `new ${content}`;
+
+ const buildWrapper = (propsData = {}) => {
+ wrapper = shallowMount(EditArea, {
+ propsData: {
+ title,
+ content,
+ returnUrl,
+ savingChanges,
+ ...propsData,
+ },
+ });
+ };
+
+ const findEditHeader = () => wrapper.find(EditHeader);
+ const findRichContentEditor = () => wrapper.find(RichContentEditor);
+ const findPublishToolbar = () => wrapper.find(PublishToolbar);
+
+ beforeEach(() => {
+ buildWrapper();
+ });
+
+ afterEach(() => {
+ wrapper.destroy();
+ });
+
+ it('renders edit header', () => {
+ expect(findEditHeader().exists()).toBe(true);
+ expect(findEditHeader().props('title')).toBe(title);
+ });
+
+ it('renders rich content editor', () => {
+ expect(findRichContentEditor().exists()).toBe(true);
+ expect(findRichContentEditor().props('value')).toBe(content);
+ });
+
+ it('renders publish toolbar', () => {
+ expect(findPublishToolbar().exists()).toBe(true);
+ expect(findPublishToolbar().props('returnUrl')).toBe(returnUrl);
+ expect(findPublishToolbar().props('savingChanges')).toBe(savingChanges);
+ expect(findPublishToolbar().props('saveable')).toBe(false);
+ });
+
+ describe('when content changes', () => {
+ beforeEach(() => {
+ findRichContentEditor().vm.$emit('input', newContent);
+
+ return wrapper.vm.$nextTick();
+ });
+
+ it('sets publish toolbar as saveable when content changes', () => {
+ expect(findPublishToolbar().props('saveable')).toBe(true);
+ });
+
+ it('sets publish toolbar as not saveable when content changes are rollback', () => {
+ findRichContentEditor().vm.$emit('input', content);
+
+ return wrapper.vm.$nextTick().then(() => {
+ expect(findPublishToolbar().props('saveable')).toBe(false);
+ });
+ });
+ });
+});
diff --git a/spec/frontend/static_site_editor/components/publish_toolbar_spec.js b/spec/frontend/static_site_editor/components/publish_toolbar_spec.js
index 82eb12d4c4d..5428ed23266 100644
--- a/spec/frontend/static_site_editor/components/publish_toolbar_spec.js
+++ b/spec/frontend/static_site_editor/components/publish_toolbar_spec.js
@@ -1,5 +1,5 @@
import { shallowMount } from '@vue/test-utils';
-import { GlButton, GlLoadingIcon } from '@gitlab/ui';
+import { GlButton } from '@gitlab/ui';
import PublishToolbar from '~/static_site_editor/components/publish_toolbar.vue';
@@ -19,7 +19,6 @@ describe('Static Site Editor Toolbar', () => {
const findReturnUrlLink = () => wrapper.find({ ref: 'returnUrlLink' });
const findSaveChangesButton = () => wrapper.find(GlButton);
- const findLoadingIndicator = () => wrapper.find(GlLoadingIcon);
beforeEach(() => {
buildWrapper();
@@ -37,8 +36,8 @@ describe('Static Site Editor Toolbar', () => {
expect(findSaveChangesButton().attributes('disabled')).toBe('true');
});
- it('does not display saving changes indicator', () => {
- expect(findLoadingIndicator().classes()).toContain('invisible');
+ it('does not render the Submit Changes button with a loader', () => {
+ expect(findSaveChangesButton().props('loading')).toBe(false);
});
it('does not render returnUrl link', () => {
@@ -62,15 +61,11 @@ describe('Static Site Editor Toolbar', () => {
describe('when saving changes', () => {
beforeEach(() => {
- buildWrapper({ saveable: true, savingChanges: true });
+ buildWrapper({ savingChanges: true });
});
- it('disables Submit Changes button', () => {
- expect(findSaveChangesButton().attributes('disabled')).toBe('true');
- });
-
- it('displays saving changes indicator', () => {
- expect(findLoadingIndicator().classes()).not.toContain('invisible');
+ it('renders the Submit Changes button with a loading indicator', () => {
+ expect(findSaveChangesButton().props('loading')).toBe(true);
});
});
diff --git a/spec/frontend/static_site_editor/components/saved_changes_message_spec.js b/spec/frontend/static_site_editor/components/saved_changes_message_spec.js
index 659e9be59d2..a63c3a83395 100644
--- a/spec/frontend/static_site_editor/components/saved_changes_message_spec.js
+++ b/spec/frontend/static_site_editor/components/saved_changes_message_spec.js
@@ -46,14 +46,11 @@ describe('~/static_site_editor/components/saved_changes_message.vue', () => {
${'branch'} | ${findBranchLink} | ${props.branch}
${'commit'} | ${findCommitLink} | ${props.commit}
${'merge request'} | ${findMergeRequestLink} | ${props.mergeRequest}
- `('renders $desc link', ({ desc, findEl, prop }) => {
+ `('renders $desc link', ({ findEl, prop }) => {
const el = findEl();
expect(el.exists()).toBe(true);
expect(el.text()).toBe(prop.label);
-
- if (desc !== 'branch') {
- expect(el.attributes('href')).toBe(prop.url);
- }
+ expect(el.attributes('href')).toBe(prop.url);
});
});
diff --git a/spec/frontend/static_site_editor/components/static_site_editor_spec.js b/spec/frontend/static_site_editor/components/static_site_editor_spec.js
deleted file mode 100644
index 5d4e3758557..00000000000
--- a/spec/frontend/static_site_editor/components/static_site_editor_spec.js
+++ /dev/null
@@ -1,247 +0,0 @@
-import Vuex from 'vuex';
-import { shallowMount, createLocalVue } from '@vue/test-utils';
-import { GlSkeletonLoader } from '@gitlab/ui';
-
-import createState from '~/static_site_editor/store/state';
-
-import StaticSiteEditor from '~/static_site_editor/components/static_site_editor.vue';
-import EditArea from '~/static_site_editor/components/edit_area.vue';
-import EditHeader from '~/static_site_editor/components/edit_header.vue';
-import InvalidContentMessage from '~/static_site_editor/components/invalid_content_message.vue';
-import PublishToolbar from '~/static_site_editor/components/publish_toolbar.vue';
-import SubmitChangesError from '~/static_site_editor/components/submit_changes_error.vue';
-import SavedChangesMessage from '~/static_site_editor/components/saved_changes_message.vue';
-
-import {
- returnUrl,
- sourceContent,
- sourceContentTitle,
- savedContentMeta,
- submitChangesError,
-} from '../mock_data';
-
-const localVue = createLocalVue();
-
-localVue.use(Vuex);
-
-describe('StaticSiteEditor', () => {
- let wrapper;
- let store;
- let loadContentActionMock;
- let setContentActionMock;
- let submitChangesActionMock;
- let dismissSubmitChangesErrorActionMock;
-
- const buildStore = ({ initialState, getters } = {}) => {
- loadContentActionMock = jest.fn();
- setContentActionMock = jest.fn();
- submitChangesActionMock = jest.fn();
- dismissSubmitChangesErrorActionMock = jest.fn();
-
- store = new Vuex.Store({
- state: createState({
- isSupportedContent: true,
- ...initialState,
- }),
- getters: {
- contentChanged: () => false,
- ...getters,
- },
- actions: {
- loadContent: loadContentActionMock,
- setContent: setContentActionMock,
- submitChanges: submitChangesActionMock,
- dismissSubmitChangesError: dismissSubmitChangesErrorActionMock,
- },
- });
- };
- const buildContentLoadedStore = ({ initialState, getters } = {}) => {
- buildStore({
- initialState: {
- isContentLoaded: true,
- ...initialState,
- },
- getters: {
- ...getters,
- },
- });
- };
-
- const buildWrapper = () => {
- wrapper = shallowMount(StaticSiteEditor, {
- localVue,
- store,
- });
- };
-
- const findEditArea = () => wrapper.find(EditArea);
- const findEditHeader = () => wrapper.find(EditHeader);
- const findInvalidContentMessage = () => wrapper.find(InvalidContentMessage);
- const findPublishToolbar = () => wrapper.find(PublishToolbar);
- const findSkeletonLoader = () => wrapper.find(GlSkeletonLoader);
- const findSubmitChangesError = () => wrapper.find(SubmitChangesError);
- const findSavedChangesMessage = () => wrapper.find(SavedChangesMessage);
-
- beforeEach(() => {
- buildStore();
- buildWrapper();
- });
-
- afterEach(() => {
- wrapper.destroy();
- });
-
- it('renders the saved changes message when changes are submitted successfully', () => {
- buildStore({ initialState: { returnUrl, savedContentMeta } });
- buildWrapper();
-
- expect(findSavedChangesMessage().exists()).toBe(true);
- expect(findSavedChangesMessage().props()).toEqual({
- returnUrl,
- ...savedContentMeta,
- });
- });
-
- describe('when content is not loaded', () => {
- it('does not render edit area', () => {
- expect(findEditArea().exists()).toBe(false);
- });
-
- it('does not render edit header', () => {
- expect(findEditHeader().exists()).toBe(false);
- });
-
- it('does not render toolbar', () => {
- expect(findPublishToolbar().exists()).toBe(false);
- });
-
- it('does not render saved changes message', () => {
- expect(findSavedChangesMessage().exists()).toBe(false);
- });
- });
-
- describe('when content is loaded', () => {
- const content = sourceContent;
- const title = sourceContentTitle;
-
- beforeEach(() => {
- buildContentLoadedStore({ initialState: { content, title } });
- buildWrapper();
- });
-
- it('renders the edit area', () => {
- expect(findEditArea().exists()).toBe(true);
- });
-
- it('renders the edit header', () => {
- expect(findEditHeader().exists()).toBe(true);
- });
-
- it('does not render skeleton loader', () => {
- expect(findSkeletonLoader().exists()).toBe(false);
- });
-
- it('passes page content to edit area', () => {
- expect(findEditArea().props('value')).toBe(content);
- });
-
- it('passes page title to edit header', () => {
- expect(findEditHeader().props('title')).toBe(title);
- });
-
- it('renders toolbar', () => {
- expect(findPublishToolbar().exists()).toBe(true);
- });
- });
-
- it('sets toolbar as saveable when content changes', () => {
- buildContentLoadedStore({
- getters: {
- contentChanged: () => true,
- },
- });
- buildWrapper();
-
- expect(findPublishToolbar().props('saveable')).toBe(true);
- });
-
- it('displays skeleton loader when loading content', () => {
- buildStore({ initialState: { isLoadingContent: true } });
- buildWrapper();
-
- expect(findSkeletonLoader().exists()).toBe(true);
- });
-
- it('does not display submit changes error when an error does not exist', () => {
- buildContentLoadedStore();
- buildWrapper();
-
- expect(findSubmitChangesError().exists()).toBe(false);
- });
-
- it('sets toolbar as saving when saving changes', () => {
- buildContentLoadedStore({
- initialState: {
- isSavingChanges: true,
- },
- });
- buildWrapper();
-
- expect(findPublishToolbar().props('savingChanges')).toBe(true);
- });
-
- it('displays invalid content message when content is not supported', () => {
- buildStore({ initialState: { isSupportedContent: false } });
- buildWrapper();
-
- expect(findInvalidContentMessage().exists()).toBe(true);
- });
-
- describe('when submitting changes fail', () => {
- beforeEach(() => {
- buildContentLoadedStore({
- initialState: {
- submitChangesError,
- },
- });
- buildWrapper();
- });
-
- it('displays submit changes error message', () => {
- expect(findSubmitChangesError().exists()).toBe(true);
- });
-
- it('dispatches submitChanges action when error message emits retry event', () => {
- findSubmitChangesError().vm.$emit('retry');
-
- expect(submitChangesActionMock).toHaveBeenCalled();
- });
-
- it('dispatches dismissSubmitChangesError action when error message emits dismiss event', () => {
- findSubmitChangesError().vm.$emit('dismiss');
-
- expect(dismissSubmitChangesErrorActionMock).toHaveBeenCalled();
- });
- });
-
- it('dispatches load content action', () => {
- expect(loadContentActionMock).toHaveBeenCalled();
- });
-
- it('dispatches setContent action when edit area emits input event', () => {
- buildContentLoadedStore();
- buildWrapper();
-
- findEditArea().vm.$emit('input', sourceContent);
-
- expect(setContentActionMock).toHaveBeenCalledWith(expect.anything(), sourceContent, undefined);
- });
-
- it('dispatches submitChanges action when toolbar emits submit event', () => {
- buildContentLoadedStore();
- buildWrapper();
- findPublishToolbar().vm.$emit('submit');
-
- expect(submitChangesActionMock).toHaveBeenCalled();
- });
-});
diff --git a/spec/frontend/static_site_editor/graphql/resolvers/file_spec.js b/spec/frontend/static_site_editor/graphql/resolvers/file_spec.js
new file mode 100644
index 00000000000..8504d09e0f1
--- /dev/null
+++ b/spec/frontend/static_site_editor/graphql/resolvers/file_spec.js
@@ -0,0 +1,25 @@
+import fileResolver from '~/static_site_editor/graphql/resolvers/file';
+import loadSourceContent from '~/static_site_editor/services/load_source_content';
+
+import {
+ projectId,
+ sourcePath,
+ sourceContentTitle as title,
+ sourceContent as content,
+} from '../../mock_data';
+
+jest.mock('~/static_site_editor/services/load_source_content', () => jest.fn());
+
+describe('static_site_editor/graphql/resolvers/file', () => {
+ it('returns file content and title when fetching file successfully', () => {
+ loadSourceContent.mockResolvedValueOnce({ title, content });
+
+ return fileResolver({ fullPath: projectId }, { path: sourcePath }).then(file => {
+ expect(file).toEqual({
+ __typename: 'File',
+ title,
+ content,
+ });
+ });
+ });
+});
diff --git a/spec/frontend/static_site_editor/graphql/resolvers/submit_content_changes_spec.js b/spec/frontend/static_site_editor/graphql/resolvers/submit_content_changes_spec.js
new file mode 100644
index 00000000000..515b5394594
--- /dev/null
+++ b/spec/frontend/static_site_editor/graphql/resolvers/submit_content_changes_spec.js
@@ -0,0 +1,37 @@
+import savedContentMetaQuery from '~/static_site_editor/graphql/queries/saved_content_meta.query.graphql';
+import submitContentChanges from '~/static_site_editor/services/submit_content_changes';
+import submitContentChangesResolver from '~/static_site_editor/graphql/resolvers/submit_content_changes';
+
+import {
+ projectId as project,
+ sourcePath,
+ username,
+ sourceContent as content,
+ savedContentMeta,
+} from '../../mock_data';
+
+jest.mock('~/static_site_editor/services/submit_content_changes', () => jest.fn());
+
+describe('static_site_editor/graphql/resolvers/submit_content_changes', () => {
+ it('writes savedContentMeta query with the data returned by the submitContentChanges service', () => {
+ const cache = { writeQuery: jest.fn() };
+
+ submitContentChanges.mockResolvedValueOnce(savedContentMeta);
+
+ return submitContentChangesResolver(
+ {},
+ { input: { path: sourcePath, project, sourcePath, content, username } },
+ { cache },
+ ).then(() => {
+ expect(cache.writeQuery).toHaveBeenCalledWith({
+ query: savedContentMetaQuery,
+ data: {
+ savedContentMeta: {
+ __typename: 'SavedContentMeta',
+ ...savedContentMeta,
+ },
+ },
+ });
+ });
+ });
+});
diff --git a/spec/frontend/static_site_editor/mock_data.js b/spec/frontend/static_site_editor/mock_data.js
index 962047e6dd2..371695e913e 100644
--- a/spec/frontend/static_site_editor/mock_data.js
+++ b/spec/frontend/static_site_editor/mock_data.js
@@ -34,6 +34,9 @@ export const savedContentMeta = {
};
export const submitChangesError = 'Could not save changes';
+export const commitBranchResponse = {
+ web_url: '/tree/root-master-patch-88195',
+};
export const commitMultipleResponse = {
short_id: 'ed899a2f4b5',
web_url: '/commit/ed899a2f4b5',
@@ -42,3 +45,5 @@ export const createMergeRequestResponse = {
iid: '123',
web_url: '/merge_requests/123',
};
+
+export const trackingCategory = 'projects:static_site_editor:show';
diff --git a/spec/frontend/static_site_editor/pages/home_spec.js b/spec/frontend/static_site_editor/pages/home_spec.js
new file mode 100644
index 00000000000..8c9c54f593e
--- /dev/null
+++ b/spec/frontend/static_site_editor/pages/home_spec.js
@@ -0,0 +1,211 @@
+import Vuex from 'vuex';
+import { shallowMount, createLocalVue } from '@vue/test-utils';
+import Home from '~/static_site_editor/pages/home.vue';
+import SkeletonLoader from '~/static_site_editor/components/skeleton_loader.vue';
+import EditArea from '~/static_site_editor/components/edit_area.vue';
+import InvalidContentMessage from '~/static_site_editor/components/invalid_content_message.vue';
+import SubmitChangesError from '~/static_site_editor/components/submit_changes_error.vue';
+import submitContentChangesMutation from '~/static_site_editor/graphql/mutations/submit_content_changes.mutation.graphql';
+import { SUCCESS_ROUTE } from '~/static_site_editor/router/constants';
+
+import {
+ projectId as project,
+ returnUrl,
+ sourceContent as content,
+ sourceContentTitle as title,
+ sourcePath,
+ username,
+ savedContentMeta,
+ submitChangesError,
+} from '../mock_data';
+
+const localVue = createLocalVue();
+
+localVue.use(Vuex);
+
+describe('static_site_editor/pages/home', () => {
+ let wrapper;
+ let store;
+ let $apollo;
+ let $router;
+ let mutateMock;
+
+ const buildApollo = (queries = {}) => {
+ mutateMock = jest.fn();
+
+ $apollo = {
+ queries: {
+ sourceContent: {
+ loading: false,
+ },
+ ...queries,
+ },
+ mutate: mutateMock,
+ };
+ };
+
+ const buildRouter = () => {
+ $router = {
+ push: jest.fn(),
+ };
+ };
+
+ const buildWrapper = (data = {}) => {
+ wrapper = shallowMount(Home, {
+ localVue,
+ store,
+ mocks: {
+ $apollo,
+ $router,
+ },
+ data() {
+ return {
+ appData: { isSupportedContent: true, returnUrl, project, username, sourcePath },
+ sourceContent: { title, content },
+ ...data,
+ };
+ },
+ });
+ };
+
+ const findEditArea = () => wrapper.find(EditArea);
+ const findInvalidContentMessage = () => wrapper.find(InvalidContentMessage);
+ const findSkeletonLoader = () => wrapper.find(SkeletonLoader);
+ const findSubmitChangesError = () => wrapper.find(SubmitChangesError);
+
+ beforeEach(() => {
+ buildApollo();
+ buildRouter();
+ });
+
+ afterEach(() => {
+ wrapper.destroy();
+ wrapper = null;
+ $apollo = null;
+ });
+
+ describe('when content is loaded', () => {
+ beforeEach(() => {
+ buildWrapper();
+ });
+
+ it('renders edit area', () => {
+ expect(findEditArea().exists()).toBe(true);
+ });
+
+ it('provides source content, returnUrl, and isSavingChanges to the edit area', () => {
+ expect(findEditArea().props()).toMatchObject({
+ title,
+ content,
+ returnUrl,
+ savingChanges: false,
+ });
+ });
+ });
+
+ it('does not render edit area when content is not loaded', () => {
+ buildWrapper({ sourceContent: null });
+
+ expect(findEditArea().exists()).toBe(false);
+ });
+
+ it('renders skeleton loader when content is not loading', () => {
+ buildApollo({
+ sourceContent: {
+ loading: true,
+ },
+ });
+ buildWrapper();
+
+ expect(findSkeletonLoader().exists()).toBe(true);
+ });
+
+ it('does not render skeleton loader when content is not loading', () => {
+ buildApollo({
+ sourceContent: {
+ loading: false,
+ },
+ });
+ buildWrapper();
+
+ expect(findSkeletonLoader().exists()).toBe(false);
+ });
+
+ it('displays invalid content message when content is not supported', () => {
+ buildWrapper({ appData: { isSupportedContent: false } });
+
+ expect(findInvalidContentMessage().exists()).toBe(true);
+ });
+
+ it('does not display invalid content message when content is supported', () => {
+ buildWrapper({ appData: { isSupportedContent: true } });
+
+ expect(findInvalidContentMessage().exists()).toBe(false);
+ });
+
+ describe('when submitting changes fails', () => {
+ beforeEach(() => {
+ mutateMock.mockRejectedValue(new Error(submitChangesError));
+
+ buildWrapper();
+ findEditArea().vm.$emit('submit', { content });
+
+ return wrapper.vm.$nextTick();
+ });
+
+ it('displays submit changes error message', () => {
+ expect(findSubmitChangesError().exists()).toBe(true);
+ });
+
+ it('retries submitting changes when retry button is clicked', () => {
+ findSubmitChangesError().vm.$emit('retry');
+
+ expect(mutateMock).toHaveBeenCalled();
+ });
+
+ it('hides submit changes error message when dismiss button is clicked', () => {
+ findSubmitChangesError().vm.$emit('dismiss');
+
+ return wrapper.vm.$nextTick().then(() => {
+ expect(findSubmitChangesError().exists()).toBe(false);
+ });
+ });
+ });
+
+ it('does not display submit changes error when an error does not exist', () => {
+ buildWrapper();
+
+ expect(findSubmitChangesError().exists()).toBe(false);
+ });
+
+ describe('when submitting changes succeeds', () => {
+ const newContent = `new ${content}`;
+
+ beforeEach(() => {
+ mutateMock.mockResolvedValueOnce({ data: { submitContentChanges: savedContentMeta } });
+
+ buildWrapper();
+ findEditArea().vm.$emit('submit', { content: newContent });
+
+ return wrapper.vm.$nextTick();
+ });
+
+ it('dispatches submitContentChanges mutation', () => {
+ expect(mutateMock).toHaveBeenCalledWith({
+ mutation: submitContentChangesMutation,
+ variables: {
+ input: {
+ content: newContent,
+ project,
+ sourcePath,
+ username,
+ },
+ },
+ });
+ });
+
+ it('transitions to the SUCCESS route', () => {
+ expect($router.push).toHaveBeenCalledWith(SUCCESS_ROUTE);
+ });
+ });
+});
diff --git a/spec/frontend/static_site_editor/pages/success_spec.js b/spec/frontend/static_site_editor/pages/success_spec.js
new file mode 100644
index 00000000000..d62b67bfa83
--- /dev/null
+++ b/spec/frontend/static_site_editor/pages/success_spec.js
@@ -0,0 +1,78 @@
+import Vuex from 'vuex';
+import { shallowMount, createLocalVue } from '@vue/test-utils';
+import Success from '~/static_site_editor/pages/success.vue';
+import SavedChangesMessage from '~/static_site_editor/components/saved_changes_message.vue';
+import { savedContentMeta, returnUrl } from '../mock_data';
+import { HOME_ROUTE } from '~/static_site_editor/router/constants';
+
+const localVue = createLocalVue();
+
+localVue.use(Vuex);
+
+describe('static_site_editor/pages/success', () => {
+ let wrapper;
+ let store;
+ let router;
+
+ const buildRouter = () => {
+ router = {
+ push: jest.fn(),
+ };
+ };
+
+ const buildWrapper = (data = {}) => {
+ wrapper = shallowMount(Success, {
+ localVue,
+ store,
+ mocks: {
+ $router: router,
+ },
+ data() {
+ return {
+ savedContentMeta,
+ appData: {
+ returnUrl,
+ },
+ ...data,
+ };
+ },
+ });
+ };
+
+ const findSavedChangesMessage = () => wrapper.find(SavedChangesMessage);
+
+ beforeEach(() => {
+ buildRouter();
+ });
+
+ afterEach(() => {
+ wrapper.destroy();
+ wrapper = null;
+ });
+
+ it('renders saved changes message', () => {
+ buildWrapper();
+
+ expect(findSavedChangesMessage().exists()).toBe(true);
+ });
+
+ it('passes returnUrl to the saved changes message', () => {
+ buildWrapper();
+
+ expect(findSavedChangesMessage().props('returnUrl')).toBe(returnUrl);
+ });
+
+ it('passes saved content metadata to the saved changes message', () => {
+ buildWrapper();
+
+ expect(findSavedChangesMessage().props('branch')).toBe(savedContentMeta.branch);
+ expect(findSavedChangesMessage().props('commit')).toBe(savedContentMeta.commit);
+ expect(findSavedChangesMessage().props('mergeRequest')).toBe(savedContentMeta.mergeRequest);
+ });
+
+ it('redirects to the HOME route when content has not been submitted', () => {
+ buildWrapper({ savedContentMeta: null });
+
+ expect(router.push).toHaveBeenCalledWith(HOME_ROUTE);
+ });
+});
diff --git a/spec/frontend/static_site_editor/services/submit_content_changes_spec.js b/spec/frontend/static_site_editor/services/submit_content_changes_spec.js
index 9a0bd88b57d..a1e9ff4ec4c 100644
--- a/spec/frontend/static_site_editor/services/submit_content_changes_spec.js
+++ b/spec/frontend/static_site_editor/services/submit_content_changes_spec.js
@@ -1,11 +1,13 @@
import Api from '~/api';
import { convertObjectPropsToSnakeCase } from '~/lib/utils/common_utils';
+import { mockTracking, unmockTracking } from 'helpers/tracking_helper';
import {
DEFAULT_TARGET_BRANCH,
SUBMIT_CHANGES_BRANCH_ERROR,
SUBMIT_CHANGES_COMMIT_ERROR,
SUBMIT_CHANGES_MERGE_REQUEST_ERROR,
+ TRACKING_ACTION_CREATE_COMMIT,
} from '~/static_site_editor/constants';
import generateBranchName from '~/static_site_editor/services/generate_branch_name';
import submitContentChanges from '~/static_site_editor/services/submit_content_changes';
@@ -13,10 +15,12 @@ import submitContentChanges from '~/static_site_editor/services/submit_content_c
import {
username,
projectId,
+ commitBranchResponse,
commitMultipleResponse,
createMergeRequestResponse,
sourcePath,
sourceContent as content,
+ trackingCategory,
} from '../mock_data';
jest.mock('~/static_site_editor/services/generate_branch_name');
@@ -24,15 +28,26 @@ jest.mock('~/static_site_editor/services/generate_branch_name');
describe('submitContentChanges', () => {
const mergeRequestTitle = `Update ${sourcePath} file`;
const branch = 'branch-name';
+ let trackingSpy;
+ let origPage;
beforeEach(() => {
- jest.spyOn(Api, 'createBranch').mockResolvedValue();
+ jest.spyOn(Api, 'createBranch').mockResolvedValue({ data: commitBranchResponse });
jest.spyOn(Api, 'commitMultiple').mockResolvedValue({ data: commitMultipleResponse });
jest
.spyOn(Api, 'createProjectMergeRequest')
.mockResolvedValue({ data: createMergeRequestResponse });
generateBranchName.mockReturnValue(branch);
+
+ origPage = document.body.dataset.page;
+ document.body.dataset.page = trackingCategory;
+ trackingSpy = mockTracking(document.body.dataset.page, undefined, jest.spyOn);
+ });
+
+ afterEach(() => {
+ document.body.dataset.page = origPage;
+ unmockTracking();
});
it('creates a branch named after the username and target branch', () => {
@@ -47,7 +62,7 @@ describe('submitContentChanges', () => {
it('notifies error when branch could not be created', () => {
Api.createBranch.mockRejectedValueOnce();
- expect(submitContentChanges({ username, projectId })).rejects.toThrow(
+ return expect(submitContentChanges({ username, projectId })).rejects.toThrow(
SUBMIT_CHANGES_BRANCH_ERROR,
);
});
@@ -68,10 +83,19 @@ describe('submitContentChanges', () => {
});
});
+ it('sends the correct tracking event when committing content changes', () => {
+ return submitContentChanges({ username, projectId, sourcePath, content }).then(() => {
+ expect(trackingSpy).toHaveBeenCalledWith(
+ document.body.dataset.page,
+ TRACKING_ACTION_CREATE_COMMIT,
+ );
+ });
+ });
+
it('notifies error when content could not be committed', () => {
Api.commitMultiple.mockRejectedValueOnce();
- expect(submitContentChanges({ username, projectId })).rejects.toThrow(
+ return expect(submitContentChanges({ username, projectId })).rejects.toThrow(
SUBMIT_CHANGES_COMMIT_ERROR,
);
});
@@ -92,7 +116,7 @@ describe('submitContentChanges', () => {
it('notifies error when merge request could not be created', () => {
Api.createProjectMergeRequest.mockRejectedValueOnce();
- expect(submitContentChanges({ username, projectId })).rejects.toThrow(
+ return expect(submitContentChanges({ username, projectId })).rejects.toThrow(
SUBMIT_CHANGES_MERGE_REQUEST_ERROR,
);
});
diff --git a/spec/frontend/static_site_editor/store/actions_spec.js b/spec/frontend/static_site_editor/store/actions_spec.js
deleted file mode 100644
index 6b0b77f59b7..00000000000
--- a/spec/frontend/static_site_editor/store/actions_spec.js
+++ /dev/null
@@ -1,152 +0,0 @@
-import testAction from 'helpers/vuex_action_helper';
-import createState from '~/static_site_editor/store/state';
-import * as actions from '~/static_site_editor/store/actions';
-import * as mutationTypes from '~/static_site_editor/store/mutation_types';
-import loadSourceContent from '~/static_site_editor/services/load_source_content';
-import submitContentChanges from '~/static_site_editor/services/submit_content_changes';
-
-import createFlash from '~/flash';
-
-import {
- username,
- projectId,
- sourcePath,
- sourceContentTitle as title,
- sourceContent as content,
- savedContentMeta,
- submitChangesError,
-} from '../mock_data';
-
-jest.mock('~/flash');
-jest.mock('~/static_site_editor/services/load_source_content', () => jest.fn());
-jest.mock('~/static_site_editor/services/submit_content_changes', () => jest.fn());
-
-describe('Static Site Editor Store actions', () => {
- let state;
-
- beforeEach(() => {
- state = createState({
- projectId,
- sourcePath,
- });
- });
-
- describe('loadContent', () => {
- describe('on success', () => {
- const payload = { title, content };
-
- beforeEach(() => {
- loadSourceContent.mockResolvedValueOnce(payload);
- });
-
- it('commits receiveContentSuccess', () => {
- testAction(
- actions.loadContent,
- null,
- state,
- [
- { type: mutationTypes.LOAD_CONTENT },
- { type: mutationTypes.RECEIVE_CONTENT_SUCCESS, payload },
- ],
- [],
- );
-
- expect(loadSourceContent).toHaveBeenCalledWith({ projectId, sourcePath });
- });
- });
-
- describe('on error', () => {
- const expectedMutations = [
- { type: mutationTypes.LOAD_CONTENT },
- { type: mutationTypes.RECEIVE_CONTENT_ERROR },
- ];
-
- beforeEach(() => {
- loadSourceContent.mockRejectedValueOnce();
- });
-
- it('commits receiveContentError', () => {
- testAction(actions.loadContent, null, state, expectedMutations);
- });
-
- it('displays flash communicating error', () => {
- return testAction(actions.loadContent, null, state, expectedMutations).then(() => {
- expect(createFlash).toHaveBeenCalledWith(
- 'An error ocurred while loading your content. Please try again.',
- );
- });
- });
- });
- });
-
- describe('setContent', () => {
- it('commits setContent mutation', () => {
- testAction(actions.setContent, content, state, [
- {
- type: mutationTypes.SET_CONTENT,
- payload: content,
- },
- ]);
- });
- });
-
- describe('submitChanges', () => {
- describe('on success', () => {
- beforeEach(() => {
- state = createState({
- projectId,
- content,
- username,
- sourcePath,
- });
- submitContentChanges.mockResolvedValueOnce(savedContentMeta);
- });
-
- it('commits submitChangesSuccess mutation', () => {
- testAction(
- actions.submitChanges,
- null,
- state,
- [
- { type: mutationTypes.SUBMIT_CHANGES },
- { type: mutationTypes.SUBMIT_CHANGES_SUCCESS, payload: savedContentMeta },
- ],
- [],
- );
-
- expect(submitContentChanges).toHaveBeenCalledWith({
- username,
- projectId,
- content,
- sourcePath,
- });
- });
- });
-
- describe('on error', () => {
- const error = new Error(submitChangesError);
- const expectedMutations = [
- { type: mutationTypes.SUBMIT_CHANGES },
- { type: mutationTypes.SUBMIT_CHANGES_ERROR, payload: error.message },
- ];
-
- beforeEach(() => {
- submitContentChanges.mockRejectedValueOnce(error);
- });
-
- it('dispatches receiveContentError', () => {
- testAction(actions.submitChanges, null, state, expectedMutations);
- });
- });
- });
-
- describe('dismissSubmitChangesError', () => {
- it('commits dismissSubmitChangesError', () => {
- testAction(actions.dismissSubmitChangesError, null, state, [
- {
- type: mutationTypes.DISMISS_SUBMIT_CHANGES_ERROR,
- },
- ]);
- });
- });
-});
diff --git a/spec/frontend/static_site_editor/store/getters_spec.js b/spec/frontend/static_site_editor/store/getters_spec.js
deleted file mode 100644
index 5793e344784..00000000000
--- a/spec/frontend/static_site_editor/store/getters_spec.js
+++ /dev/null
@@ -1,19 +0,0 @@
-import createState from '~/static_site_editor/store/state';
-import { contentChanged } from '~/static_site_editor/store/getters';
-import { sourceContent as content } from '../mock_data';
-
-describe('Static Site Editor Store getters', () => {
- describe('contentChanged', () => {
- it('returns true when content and originalContent are different', () => {
- const state = createState({ content, originalContent: 'something else' });
-
- expect(contentChanged(state)).toBe(true);
- });
-
- it('returns false when content and originalContent are the same', () => {
- const state = createState({ content, originalContent: content });
-
- expect(contentChanged(state)).toBe(false);
- });
- });
-});
diff --git a/spec/frontend/static_site_editor/store/mutations_spec.js b/spec/frontend/static_site_editor/store/mutations_spec.js
deleted file mode 100644
index 2441f317d90..00000000000
--- a/spec/frontend/static_site_editor/store/mutations_spec.js
+++ /dev/null
@@ -1,54 +0,0 @@
-import createState from '~/static_site_editor/store/state';
-import mutations from '~/static_site_editor/store/mutations';
-import * as types from '~/static_site_editor/store/mutation_types';
-import {
- sourceContentTitle as title,
- sourceContent as content,
- savedContentMeta,
- submitChangesError,
-} from '../mock_data';
-
-describe('Static Site Editor Store mutations', () => {
- let state;
- const contentLoadedPayload = { title, content };
-
- beforeEach(() => {
- state = createState();
- });
-
- it.each`
- mutation | stateProperty | payload | expectedValue
- ${types.LOAD_CONTENT} | ${'isLoadingContent'} | ${undefined} | ${true}
- ${types.RECEIVE_CONTENT_SUCCESS} | ${'isLoadingContent'} | ${contentLoadedPayload} | ${false}
- ${types.RECEIVE_CONTENT_SUCCESS} | ${'isContentLoaded'} | ${contentLoadedPayload} | ${true}
- ${types.RECEIVE_CONTENT_SUCCESS} | ${'title'} | ${contentLoadedPayload} | ${title}
- ${types.RECEIVE_CONTENT_SUCCESS} | ${'content'} | ${contentLoadedPayload} | ${content}
- ${types.RECEIVE_CONTENT_SUCCESS} | ${'originalContent'} | ${contentLoadedPayload} | ${content}
- ${types.RECEIVE_CONTENT_ERROR} | ${'isLoadingContent'} | ${undefined} | ${false}
- ${types.SET_CONTENT} | ${'content'} | ${content} | ${content}
- ${types.SUBMIT_CHANGES} | ${'isSavingChanges'} | ${undefined} | ${true}
- ${types.SUBMIT_CHANGES_SUCCESS} | ${'savedContentMeta'} | ${savedContentMeta} | ${savedContentMeta}
- ${types.SUBMIT_CHANGES_SUCCESS} | ${'isSavingChanges'} | ${savedContentMeta} | ${false}
- ${types.SUBMIT_CHANGES_ERROR} | ${'isSavingChanges'} | ${undefined} | ${false}
- ${types.SUBMIT_CHANGES_ERROR} | ${'submitChangesError'} | ${submitChangesError} | ${submitChangesError}
- ${types.DISMISS_SUBMIT_CHANGES_ERROR} | ${'submitChangesError'} | ${undefined} | ${''}
- `(
- '$mutation sets $stateProperty to $expectedValue',
- ({ mutation, stateProperty, payload, expectedValue }) => {
- mutations[mutation](state, payload);
- expect(state[stateProperty]).toBe(expectedValue);
- },
- );
-
- it(`${types.SUBMIT_CHANGES_SUCCESS} sets originalContent to content current value`, () => {
- const editedContent = `${content} plus something else`;
-
- state = createState({
- originalContent: content,
- content: editedContent,
- });
- mutations[types.SUBMIT_CHANGES_SUCCESS](state);
-
- expect(state.originalContent).toBe(state.content);
- });
-});
diff --git a/spec/frontend/tracking_spec.js b/spec/frontend/tracking_spec.js
index 30a8e138df2..08a26d46618 100644
--- a/spec/frontend/tracking_spec.js
+++ b/spec/frontend/tracking_spec.js
@@ -4,6 +4,7 @@ import Tracking, { initUserTracking } from '~/tracking';
describe('Tracking', () => {
let snowplowSpy;
let bindDocumentSpy;
+ let trackLoadEventsSpy;
beforeEach(() => {
window.snowplow = window.snowplow || (() => {});
@@ -18,6 +19,7 @@ describe('Tracking', () => {
describe('initUserTracking', () => {
beforeEach(() => {
bindDocumentSpy = jest.spyOn(Tracking, 'bindDocument').mockImplementation(() => null);
+ trackLoadEventsSpy = jest.spyOn(Tracking, 'trackLoadEvents').mockImplementation(() => null);
});
it('calls through to get a new tracker with the expected options', () => {
@@ -44,10 +46,11 @@ describe('Tracking', () => {
expect(snowplowSpy).not.toHaveBeenCalledWith('enableFormTracking');
expect(snowplowSpy).not.toHaveBeenCalledWith('enableLinkClickTracking');
- window.snowplowOptions = Object.assign({}, window.snowplowOptions, {
+ window.snowplowOptions = {
+ ...window.snowplowOptions,
formTracking: true,
linkClickTracking: true,
- });
+ };
initUserTracking();
expect(snowplowSpy).toHaveBeenCalledWith('enableFormTracking');
@@ -58,6 +61,11 @@ describe('Tracking', () => {
initUserTracking();
expect(bindDocumentSpy).toHaveBeenCalled();
});
+
+ it('tracks page loaded events', () => {
+ initUserTracking();
+ expect(trackLoadEventsSpy).toHaveBeenCalled();
+ });
});
describe('.event', () => {
@@ -127,6 +135,7 @@ describe('Tracking', () => {
<input type="checkbox" data-track-event="toggle_checkbox" value="_value_" checked/>
<input class="dropdown" data-track-event="toggle_dropdown"/>
<div data-track-event="nested_event"><span class="nested"></span></div>
+ <input data-track-eventbogus="click_bogusinput" data-track-label="_label_" value="_value_"/>
`);
});
@@ -139,6 +148,12 @@ describe('Tracking', () => {
});
});
+ it('does not bind to clicks on elements without [data-track-event]', () => {
+ trigger('[data-track-eventbogus="click_bogusinput"]');
+
+ expect(eventSpy).not.toHaveBeenCalled();
+ });
+
it('allows value override with the data-track-value attribute', () => {
trigger('[data-track-event="click_input2"]');
@@ -178,6 +193,44 @@ describe('Tracking', () => {
});
});
+ describe('tracking page loaded events', () => {
+ let eventSpy;
+
+ beforeEach(() => {
+ eventSpy = jest.spyOn(Tracking, 'event');
+ setHTMLFixture(`
+ <input data-track-event="render" data-track-label="label1" value="_value_" data-track-property="_property_"/>
+ <span data-track-event="render" data-track-label="label2" data-track-value="_value_">
+ Something
+ </span>
+ <input data-track-event="_render_bogus_" data-track-label="label3" value="_value_" data-track-property="_property_"/>
+ `);
+ Tracking.trackLoadEvents('_category_'); // only happens once
+ });
+
+ it('sends tracking events when [data-track-event="render"] is on an element', () => {
+ expect(eventSpy.mock.calls).toEqual([
+ [
+ '_category_',
+ 'render',
+ {
+ label: 'label1',
+ value: '_value_',
+ property: '_property_',
+ },
+ ],
+ [
+ '_category_',
+ 'render',
+ {
+ label: 'label2',
+ value: '_value_',
+ },
+ ],
+ ]);
+ });
+ });
+
describe('tracking mixin', () => {
describe('trackingOptions', () => {
it('return the options defined on initialisation', () => {
diff --git a/spec/frontend/users_select/utils_spec.js b/spec/frontend/users_select/utils_spec.js
new file mode 100644
index 00000000000..a09935d8a04
--- /dev/null
+++ b/spec/frontend/users_select/utils_spec.js
@@ -0,0 +1,33 @@
+import $ from 'jquery';
+import { getAjaxUsersSelectOptions, getAjaxUsersSelectParams } from '~/users_select/utils';
+
+const options = {
+ fooBar: 'baz',
+ activeUserId: 1,
+};
+
+describe('getAjaxUsersSelectOptions', () => {
+ it('returns options built from select data attributes', () => {
+ const $select = $('<select />', { 'data-foo-bar': 'baz', 'data-user-id': 1 });
+
+ expect(
+ getAjaxUsersSelectOptions($select, { fooBar: 'fooBar', activeUserId: 'user-id' }),
+ ).toEqual(options);
+ });
+});
+
+describe('getAjaxUsersSelectParams', () => {
+ it('returns query parameters built from provided options', () => {
+ expect(
+ getAjaxUsersSelectParams(options, {
+ foo_bar: 'fooBar',
+ active_user_id: 'activeUserId',
+ non_existent_key: 'nonExistentKey',
+ }),
+ ).toEqual({
+ foo_bar: 'baz',
+ active_user_id: 1,
+ non_existent_key: null,
+ });
+ });
+});
diff --git a/spec/frontend/vue_mr_widget/components/mr_collapsible_extension_spec.js b/spec/frontend/vue_mr_widget/components/mr_collapsible_extension_spec.js
index a7ecb863cfb..8a604355625 100644
--- a/spec/frontend/vue_mr_widget/components/mr_collapsible_extension_spec.js
+++ b/spec/frontend/vue_mr_widget/components/mr_collapsible_extension_spec.js
@@ -61,7 +61,7 @@ describe('Merge Request Collapsible Extension', () => {
describe('while loading', () => {
beforeEach(() => {
- mountComponent(Object.assign({}, data, { isLoading: true }));
+ mountComponent({ ...data, isLoading: true });
});
it('renders the buttons disabled', () => {
@@ -86,7 +86,7 @@ describe('Merge Request Collapsible Extension', () => {
describe('with error', () => {
beforeEach(() => {
- mountComponent(Object.assign({}, data, { hasError: true }));
+ mountComponent({ ...data, hasError: true });
});
it('does not render the buttons', () => {
diff --git a/spec/frontend/vue_mr_widget/components/mr_widget_pipeline_container_spec.js b/spec/frontend/vue_mr_widget/components/mr_widget_pipeline_container_spec.js
new file mode 100644
index 00000000000..5f3a8654990
--- /dev/null
+++ b/spec/frontend/vue_mr_widget/components/mr_widget_pipeline_container_spec.js
@@ -0,0 +1,100 @@
+import { mount } from '@vue/test-utils';
+import MrWidgetPipelineContainer from '~/vue_merge_request_widget/components/mr_widget_pipeline_container.vue';
+import MrWidgetPipeline from '~/vue_merge_request_widget/components/mr_widget_pipeline.vue';
+import ArtifactsApp from '~/vue_merge_request_widget/components/artifacts_list_app.vue';
+import { mockStore } from '../mock_data';
+import MockAdapter from 'axios-mock-adapter';
+import axios from '~/lib/utils/axios_utils';
+
+describe('MrWidgetPipelineContainer', () => {
+ let wrapper;
+ let mock;
+
+ const factory = (props = {}) => {
+ wrapper = mount(MrWidgetPipelineContainer, {
+ propsData: {
+ mr: { ...mockStore },
+ ...props,
+ },
+ });
+ };
+
+ beforeEach(() => {
+ mock = new MockAdapter(axios);
+ mock.onGet().reply(200, {});
+ });
+
+ afterEach(() => {
+ wrapper.destroy();
+ });
+
+ describe('when pre merge', () => {
+ beforeEach(() => {
+ factory();
+ });
+
+ it('renders pipeline', () => {
+ expect(wrapper.find(MrWidgetPipeline).exists()).toBe(true);
+ expect(wrapper.find(MrWidgetPipeline).props()).toMatchObject({
+ pipeline: mockStore.pipeline,
+ pipelineCoverageDelta: mockStore.pipelineCoverageDelta,
+ ciStatus: mockStore.ciStatus,
+ hasCi: mockStore.hasCI,
+ sourceBranch: mockStore.sourceBranch,
+ sourceBranchLink: mockStore.sourceBranchLink,
+ });
+ });
+
+ it('renders deployments', () => {
+ const expectedProps = mockStore.deployments.map(dep =>
+ expect.objectContaining({
+ deployment: dep,
+ showMetrics: false,
+ }),
+ );
+
+ const deployments = wrapper.findAll('.mr-widget-extension .js-pre-deployment');
+
+ expect(deployments.wrappers.map(x => x.props())).toEqual(expectedProps);
+ });
+ });
+
+ describe('when post merge', () => {
+ beforeEach(() => {
+ factory({
+ isPostMerge: true,
+ });
+ });
+
+ it('renders pipeline', () => {
+ expect(wrapper.find(MrWidgetPipeline).exists()).toBe(true);
+ expect(wrapper.find(MrWidgetPipeline).props()).toMatchObject({
+ pipeline: mockStore.mergePipeline,
+ pipelineCoverageDelta: mockStore.pipelineCoverageDelta,
+ ciStatus: mockStore.ciStatus,
+ hasCi: mockStore.hasCI,
+ sourceBranch: mockStore.targetBranch,
+ sourceBranchLink: mockStore.targetBranch,
+ });
+ });
+
+ it('renders deployments', () => {
+ const expectedProps = mockStore.postMergeDeployments.map(dep =>
+ expect.objectContaining({
+ deployment: dep,
+ showMetrics: true,
+ }),
+ );
+
+ const deployments = wrapper.findAll('.mr-widget-extension .js-post-deployment');
+
+ expect(deployments.wrappers.map(x => x.props())).toEqual(expectedProps);
+ });
+ });
+
+ describe('with artifacts path', () => {
+ it('renders the artifacts app', () => {
+ expect(wrapper.find(ArtifactsApp).isVisible()).toBe(true);
+ });
+ });
+});
diff --git a/spec/frontend/vue_mr_widget/components/mr_widget_terraform_plan_spec.js b/spec/frontend/vue_mr_widget/components/mr_widget_terraform_plan_spec.js
index 1951b56587a..91e95b2bdb1 100644
--- a/spec/frontend/vue_mr_widget/components/mr_widget_terraform_plan_spec.js
+++ b/spec/frontend/vue_mr_widget/components/mr_widget_terraform_plan_spec.js
@@ -3,6 +3,7 @@ import { shallowMount } from '@vue/test-utils';
import axios from '~/lib/utils/axios_utils';
import MockAdapter from 'axios-mock-adapter';
import MrWidgetTerraformPlan from '~/vue_merge_request_widget/components/mr_widget_terraform_plan.vue';
+import Poll from '~/lib/utils/poll';
const plan = {
create: 10,
@@ -57,11 +58,23 @@ describe('MrWidgetTerraformPlan', () => {
});
describe('successful poll', () => {
+ let pollRequest;
+ let pollStop;
+
beforeEach(() => {
+ pollRequest = jest.spyOn(Poll.prototype, 'makeRequest');
+ pollStop = jest.spyOn(Poll.prototype, 'stop');
+
mockPollingApi(200, { 'tfplan.json': plan }, {});
+
return mountWrapper();
});
+ afterEach(() => {
+ pollRequest.mockRestore();
+ pollStop.mockRestore();
+ });
+
it('content change text', () => {
expect(wrapper.find(GlSprintf).exists()).toBe(true);
});
@@ -69,6 +82,11 @@ describe('MrWidgetTerraformPlan', () => {
it('renders button when url is found', () => {
expect(wrapper.find('a').text()).toContain('View full log');
});
+
+ it('does not make additional requests after poll is successful', () => {
+ expect(pollRequest).toHaveBeenCalledTimes(1);
+ expect(pollStop).toHaveBeenCalledTimes(1);
+ });
});
describe('polling fails', () => {
diff --git a/spec/frontend/vue_mr_widget/stores/artifacts_list/actions_spec.js b/spec/frontend/vue_mr_widget/stores/artifacts_list/actions_spec.js
new file mode 100644
index 00000000000..026ea0e4d0a
--- /dev/null
+++ b/spec/frontend/vue_mr_widget/stores/artifacts_list/actions_spec.js
@@ -0,0 +1,165 @@
+import MockAdapter from 'axios-mock-adapter';
+import testAction from 'helpers/vuex_action_helper';
+import { TEST_HOST } from 'helpers/test_constants';
+import axios from '~/lib/utils/axios_utils';
+import {
+ setEndpoint,
+ requestArtifacts,
+ clearEtagPoll,
+ stopPolling,
+ fetchArtifacts,
+ receiveArtifactsSuccess,
+ receiveArtifactsError,
+} from '~/vue_merge_request_widget/stores/artifacts_list/actions';
+import state from '~/vue_merge_request_widget/stores/artifacts_list/state';
+import * as types from '~/vue_merge_request_widget/stores/artifacts_list/mutation_types';
+
+describe('Artifacts App Store Actions', () => {
+ let mockedState;
+
+ beforeEach(() => {
+ mockedState = state();
+ });
+
+ describe('setEndpoint', () => {
+ it('should commit SET_ENDPOINT mutation', done => {
+ testAction(
+ setEndpoint,
+ 'endpoint.json',
+ mockedState,
+ [{ type: types.SET_ENDPOINT, payload: 'endpoint.json' }],
+ [],
+ done,
+ );
+ });
+ });
+
+ describe('requestArtifacts', () => {
+ it('should commit REQUEST_ARTIFACTS mutation', done => {
+ testAction(
+ requestArtifacts,
+ null,
+ mockedState,
+ [{ type: types.REQUEST_ARTIFACTS }],
+ [],
+ done,
+ );
+ });
+ });
+
+ describe('fetchArtifacts', () => {
+ let mock;
+
+ beforeEach(() => {
+ mockedState.endpoint = `${TEST_HOST}/endpoint.json`;
+ mock = new MockAdapter(axios);
+ });
+
+ afterEach(() => {
+ mock.restore();
+ stopPolling();
+ clearEtagPoll();
+ });
+
+ describe('success', () => {
+ it('dispatches requestArtifacts and receiveArtifactsSuccess ', done => {
+ mock.onGet(`${TEST_HOST}/endpoint.json`).replyOnce(200, [
+ {
+ text: 'result.txt',
+ url: 'asda',
+ job_name: 'generate-artifact',
+ job_path: 'asda',
+ },
+ ]);
+
+ testAction(
+ fetchArtifacts,
+ null,
+ mockedState,
+ [],
+ [
+ {
+ type: 'requestArtifacts',
+ },
+ {
+ payload: {
+ data: [
+ {
+ text: 'result.txt',
+ url: 'asda',
+ job_name: 'generate-artifact',
+ job_path: 'asda',
+ },
+ ],
+ status: 200,
+ },
+ type: 'receiveArtifactsSuccess',
+ },
+ ],
+ done,
+ );
+ });
+ });
+
+ describe('error', () => {
+ beforeEach(() => {
+ mock.onGet(`${TEST_HOST}/endpoint.json`).reply(500);
+ });
+
+ it('dispatches requestArtifacts and receiveArtifactsError ', done => {
+ testAction(
+ fetchArtifacts,
+ null,
+ mockedState,
+ [],
+ [
+ {
+ type: 'requestArtifacts',
+ },
+ {
+ type: 'receiveArtifactsError',
+ },
+ ],
+ done,
+ );
+ });
+ });
+ });
+
+ describe('receiveArtifactsSuccess', () => {
+ it('should commit RECEIVE_ARTIFACTS_SUCCESS mutation with 200', done => {
+ testAction(
+ receiveArtifactsSuccess,
+ { data: { summary: {} }, status: 200 },
+ mockedState,
+ [{ type: types.RECEIVE_ARTIFACTS_SUCCESS, payload: { summary: {} } }],
+ [],
+ done,
+ );
+ });
+
+ it('should not commit RECEIVE_ARTIFACTS_SUCCESS mutation with 204', done => {
+ testAction(
+ receiveArtifactsSuccess,
+ { data: { summary: {} }, status: 204 },
+ mockedState,
+ [],
+ [],
+ done,
+ );
+ });
+ });
+
+ describe('receiveArtifactsError', () => {
+ it('should commit RECEIVE_ARTIFACTS_ERROR mutation', done => {
+ testAction(
+ receiveArtifactsError,
+ null,
+ mockedState,
+ [{ type: types.RECEIVE_ARTIFACTS_ERROR }],
+ [],
+ done,
+ );
+ });
+ });
+});
diff --git a/spec/frontend/vue_mr_widget/stores/get_state_key_spec.js b/spec/frontend/vue_mr_widget/stores/get_state_key_spec.js
index 0f5d47b3bfe..e54cd345a37 100644
--- a/spec/frontend/vue_mr_widget/stores/get_state_key_spec.js
+++ b/spec/frontend/vue_mr_widget/stores/get_state_key_spec.js
@@ -35,10 +35,12 @@ describe('getStateKey', () => {
expect(bound()).toEqual('autoMergeEnabled');
+ context.canMerge = true;
context.isSHAMismatch = true;
expect(bound()).toEqual('shaMismatch');
+ context.canMerge = false;
context.isPipelineBlocked = true;
expect(bound()).toEqual('pipelineBlocked');
@@ -100,4 +102,26 @@ describe('getStateKey', () => {
expect(bound()).toEqual('rebase');
});
+
+ it.each`
+ canMerge | isSHAMismatch | stateKey
+ ${true} | ${true} | ${'shaMismatch'}
+ ${false} | ${true} | ${'notAllowedToMerge'}
+ ${false} | ${false} | ${'notAllowedToMerge'}
+ `(
+ 'returns $stateKey when canMerge is $canMerge and isSHAMismatch is $isSHAMismatch',
+ ({ canMerge, isSHAMismatch, stateKey }) => {
+ const bound = getStateKey.bind(
+ {
+ canMerge,
+ isSHAMismatch,
+ },
+ {
+ commits_count: 2,
+ },
+ );
+
+ expect(bound()).toEqual(stateKey);
+ },
+ );
});
diff --git a/spec/frontend/vue_mr_widget/stores/mr_widget_store_spec.js b/spec/frontend/vue_mr_widget/stores/mr_widget_store_spec.js
new file mode 100644
index 00000000000..48326eda404
--- /dev/null
+++ b/spec/frontend/vue_mr_widget/stores/mr_widget_store_spec.js
@@ -0,0 +1,112 @@
+import MergeRequestStore from '~/vue_merge_request_widget/stores/mr_widget_store';
+import { stateKey } from '~/vue_merge_request_widget/stores/state_maps';
+import mockData from '../mock_data';
+
+describe('MergeRequestStore', () => {
+ let store;
+
+ beforeEach(() => {
+ store = new MergeRequestStore(mockData);
+ });
+
+ describe('setData', () => {
+ it('should set isSHAMismatch when the diff SHA changes', () => {
+ store.setData({ ...mockData, diff_head_sha: 'a-different-string' });
+
+ expect(store.isSHAMismatch).toBe(true);
+ });
+
+ it('should not set isSHAMismatch when other data changes', () => {
+ store.setData({ ...mockData, work_in_progress: !mockData.work_in_progress });
+
+ expect(store.isSHAMismatch).toBe(false);
+ });
+
+ it('should update cached sha after rebasing', () => {
+ store.setData({ ...mockData, diff_head_sha: 'abc123' }, true);
+
+ expect(store.isSHAMismatch).toBe(false);
+ expect(store.sha).toBe('abc123');
+ });
+
+ describe('isPipelinePassing', () => {
+ it('is true when the CI status is `success`', () => {
+ store.setData({ ...mockData, ci_status: 'success' });
+
+ expect(store.isPipelinePassing).toBe(true);
+ });
+
+ it('is true when the CI status is `success-with-warnings`', () => {
+ store.setData({ ...mockData, ci_status: 'success-with-warnings' });
+
+ expect(store.isPipelinePassing).toBe(true);
+ });
+
+ it('is false when the CI status is `failed`', () => {
+ store.setData({ ...mockData, ci_status: 'failed' });
+
+ expect(store.isPipelinePassing).toBe(false);
+ });
+
+ it('is false when the CI status is anything except `success`', () => {
+ store.setData({ ...mockData, ci_status: 'foobarbaz' });
+
+ expect(store.isPipelinePassing).toBe(false);
+ });
+ });
+
+ describe('isPipelineSkipped', () => {
+ it('should set isPipelineSkipped=true when the CI status is `skipped`', () => {
+ store.setData({ ...mockData, ci_status: 'skipped' });
+
+ expect(store.isPipelineSkipped).toBe(true);
+ });
+
+ it('should set isPipelineSkipped=false when the CI status is anything except `skipped`', () => {
+ store.setData({ ...mockData, ci_status: 'foobarbaz' });
+
+ expect(store.isPipelineSkipped).toBe(false);
+ });
+ });
+
+ describe('isNothingToMergeState', () => {
+ it('returns true when nothingToMerge', () => {
+ store.state = stateKey.nothingToMerge;
+
+ expect(store.isNothingToMergeState).toBe(true);
+ });
+
+ it('returns false when not nothingToMerge', () => {
+ store.state = 'state';
+
+ expect(store.isNothingToMergeState).toBe(false);
+ });
+ });
+ });
+
+ describe('setPaths', () => {
+ it('should set the add ci config path', () => {
+ store.setData({ ...mockData });
+
+ expect(store.mergeRequestAddCiConfigPath).toBe('/group2/project2/new/pipeline');
+ });
+
+ it('should set humanAccess=Maintainer when user has that role', () => {
+ store.setData({ ...mockData });
+
+ expect(store.humanAccess).toBe('Maintainer');
+ });
+
+ it('should set pipelinesEmptySvgPath', () => {
+ store.setData({ ...mockData });
+
+ expect(store.pipelinesEmptySvgPath).toBe('/path/to/svg');
+ });
+
+ it('should set newPipelinePath', () => {
+ store.setData({ ...mockData });
+
+ expect(store.newPipelinePath).toBe('/group2/project2/pipelines/new');
+ });
+ });
+});
diff --git a/spec/frontend/vue_shared/components/__snapshots__/clone_dropdown_spec.js.snap b/spec/frontend/vue_shared/components/__snapshots__/clone_dropdown_spec.js.snap
index 4cd03a690e9..408f9d57147 100644
--- a/spec/frontend/vue_shared/components/__snapshots__/clone_dropdown_spec.js.snap
+++ b/spec/frontend/vue_shared/components/__snapshots__/clone_dropdown_spec.js.snap
@@ -24,12 +24,7 @@ exports[`Clone Dropdown Button rendering matches the snapshot 1`] = `
<b-input-group-stub
tag="div"
>
- <b-input-group-prepend-stub
- tag="div"
- >
-
- <!---->
- </b-input-group-prepend-stub>
+ <!---->
<b-form-input-stub
class="gl-form-input"
@@ -44,18 +39,14 @@ exports[`Clone Dropdown Button rendering matches the snapshot 1`] = `
>
<gl-button-stub
category="tertiary"
+ class="d-inline-flex"
data-clipboard-text="ssh://foo.bar"
- icon=""
+ data-qa-selector="copy_ssh_url_button"
+ icon="copy-to-clipboard"
size="medium"
title="Copy URL"
variant="default"
- >
- <gl-icon-stub
- name="copy-to-clipboard"
- size="16"
- title="Copy URL"
- />
- </gl-button-stub>
+ />
</b-input-group-append-stub>
</b-input-group-stub>
</div>
@@ -74,12 +65,7 @@ exports[`Clone Dropdown Button rendering matches the snapshot 1`] = `
<b-input-group-stub
tag="div"
>
- <b-input-group-prepend-stub
- tag="div"
- >
-
- <!---->
- </b-input-group-prepend-stub>
+ <!---->
<b-form-input-stub
class="gl-form-input"
@@ -94,18 +80,14 @@ exports[`Clone Dropdown Button rendering matches the snapshot 1`] = `
>
<gl-button-stub
category="tertiary"
+ class="d-inline-flex"
data-clipboard-text="http://foo.bar"
- icon=""
+ data-qa-selector="copy_http_url_button"
+ icon="copy-to-clipboard"
size="medium"
title="Copy URL"
variant="default"
- >
- <gl-icon-stub
- name="copy-to-clipboard"
- size="16"
- title="Copy URL"
- />
- </gl-button-stub>
+ />
</b-input-group-append-stub>
</b-input-group-stub>
</div>
diff --git a/spec/frontend/vue_shared/components/__snapshots__/code_block_spec.js.snap b/spec/frontend/vue_shared/components/__snapshots__/code_block_spec.js.snap
index 5347d1efc48..db174346729 100644
--- a/spec/frontend/vue_shared/components/__snapshots__/code_block_spec.js.snap
+++ b/spec/frontend/vue_shared/components/__snapshots__/code_block_spec.js.snap
@@ -1,16 +1,26 @@
// Jest Snapshot v1, https://goo.gl/fbAQLP
-exports[`Code Block matches snapshot 1`] = `
+exports[`Code Block with default props renders correctly 1`] = `
<pre
class="code-block rounded"
>
-
<code
class="d-block"
>
test-code
</code>
-
+</pre>
+`;
+exports[`Code Block with maxHeight set to "200px" renders correctly 1`] = `
+<pre
+ class="code-block rounded"
+ style="max-height: 200px; overflow-y: auto;"
+>
+ <code
+ class="d-block"
+ >
+ test-code
+ </code>
</pre>
`;
diff --git a/spec/frontend/vue_shared/components/__snapshots__/identicon_spec.js.snap b/spec/frontend/vue_shared/components/__snapshots__/identicon_spec.js.snap
index 72370cb5b52..1d8e04b83a3 100644
--- a/spec/frontend/vue_shared/components/__snapshots__/identicon_spec.js.snap
+++ b/spec/frontend/vue_shared/components/__snapshots__/identicon_spec.js.snap
@@ -1,6 +1,16 @@
// Jest Snapshot v1, https://goo.gl/fbAQLP
-exports[`Identicon matches snapshot 1`] = `
+exports[`Identicon entity id is a GraphQL id matches snapshot 1`] = `
+<div
+ class="avatar identicon s40 bg2"
+>
+
+ E
+
+</div>
+`;
+
+exports[`Identicon entity id is a number matches snapshot 1`] = `
<div
class="avatar identicon s40 bg2"
>
diff --git a/spec/frontend/vue_shared/components/awards_list_spec.js b/spec/frontend/vue_shared/components/awards_list_spec.js
index bb3e60ab9e2..0abb72ace2e 100644
--- a/spec/frontend/vue_shared/components/awards_list_spec.js
+++ b/spec/frontend/vue_shared/components/awards_list_spec.js
@@ -210,4 +210,46 @@ describe('vue_shared/components/awards_list', () => {
expect(buttons.wrappers.every(x => x.classes('disabled'))).toBe(true);
});
});
+
+ describe('with default awards', () => {
+ beforeEach(() => {
+ createComponent({
+ awards: [createAward(EMOJI_SMILE, USERS.marie), createAward(EMOJI_100, USERS.marie)],
+ canAwardEmoji: true,
+ currentUserId: USERS.root.id,
+ // Let's assert that it puts thumbsup and thumbsdown in the right order still
+ defaultAwards: [EMOJI_THUMBSDOWN, EMOJI_100, EMOJI_THUMBSUP],
+ });
+ });
+
+ it('shows awards in correct order', () => {
+ expect(findAwardsData()).toEqual([
+ {
+ classes: ['btn', 'award-control'],
+ count: 0,
+ html: matchingEmojiTag(EMOJI_THUMBSUP),
+ title: '',
+ },
+ {
+ classes: ['btn', 'award-control'],
+ count: 0,
+ html: matchingEmojiTag(EMOJI_THUMBSDOWN),
+ title: '',
+ },
+ // We expect the EMOJI_100 before the EMOJI_SMILE because it was given as a defaultAward
+ {
+ classes: ['btn', 'award-control'],
+ count: 1,
+ html: matchingEmojiTag(EMOJI_100),
+ title: 'Marie',
+ },
+ {
+ classes: ['btn', 'award-control'],
+ count: 1,
+ html: matchingEmojiTag(EMOJI_SMILE),
+ title: 'Marie',
+ },
+ ]);
+ });
+ });
});
diff --git a/spec/frontend/vue_shared/components/blob_viewers/__snapshots__/simple_viewer_spec.js.snap b/spec/frontend/vue_shared/components/blob_viewers/__snapshots__/simple_viewer_spec.js.snap
index 87f2a8f9eff..4909d2d4226 100644
--- a/spec/frontend/vue_shared/components/blob_viewers/__snapshots__/simple_viewer_spec.js.snap
+++ b/spec/frontend/vue_shared/components/blob_viewers/__snapshots__/simple_viewer_spec.js.snap
@@ -2,7 +2,8 @@
exports[`Blob Simple Viewer component rendering matches the snapshot 1`] = `
<div
- class="file-content code js-syntax-highlight qa-file-content"
+ class="file-content code js-syntax-highlight"
+ data-qa-selector="file_content"
>
<div
class="line-numbers"
diff --git a/spec/frontend/vue_shared/components/blob_viewers/rich_viewer_spec.js b/spec/frontend/vue_shared/components/blob_viewers/rich_viewer_spec.js
index ce3f289eb6e..5cf42ecdc1d 100644
--- a/spec/frontend/vue_shared/components/blob_viewers/rich_viewer_spec.js
+++ b/spec/frontend/vue_shared/components/blob_viewers/rich_viewer_spec.js
@@ -1,5 +1,6 @@
import { shallowMount } from '@vue/test-utils';
import RichViewer from '~/vue_shared/components/blob_viewers/rich_viewer.vue';
+import MarkdownFieldView from '~/vue_shared/components/markdown/field_view.vue';
import { handleBlobRichViewer } from '~/blob/viewer';
jest.mock('~/blob/viewer');
@@ -33,4 +34,8 @@ describe('Blob Rich Viewer component', () => {
it('queries for advanced viewer', () => {
expect(handleBlobRichViewer).toHaveBeenCalledWith(expect.anything(), defaultType);
});
+
+ it('is using Markdown View Field', () => {
+ expect(wrapper.contains(MarkdownFieldView)).toBe(true);
+ });
});
diff --git a/spec/frontend/vue_shared/components/ci_badge_link_spec.js b/spec/frontend/vue_shared/components/ci_badge_link_spec.js
new file mode 100644
index 00000000000..f656bb0b60d
--- /dev/null
+++ b/spec/frontend/vue_shared/components/ci_badge_link_spec.js
@@ -0,0 +1,100 @@
+import Vue from 'vue';
+import mountComponent from 'helpers/vue_mount_component_helper';
+import ciBadge from '~/vue_shared/components/ci_badge_link.vue';
+
+describe('CI Badge Link Component', () => {
+ let CIBadge;
+ let vm;
+
+ const statuses = {
+ canceled: {
+ text: 'canceled',
+ label: 'canceled',
+ group: 'canceled',
+ icon: 'status_canceled',
+ details_path: 'status/canceled',
+ },
+ created: {
+ text: 'created',
+ label: 'created',
+ group: 'created',
+ icon: 'status_created',
+ details_path: 'status/created',
+ },
+ failed: {
+ text: 'failed',
+ label: 'failed',
+ group: 'failed',
+ icon: 'status_failed',
+ details_path: 'status/failed',
+ },
+ manual: {
+ text: 'manual',
+ label: 'manual action',
+ group: 'manual',
+ icon: 'status_manual',
+ details_path: 'status/manual',
+ },
+ pending: {
+ text: 'pending',
+ label: 'pending',
+ group: 'pending',
+ icon: 'status_pending',
+ details_path: 'status/pending',
+ },
+ running: {
+ text: 'running',
+ label: 'running',
+ group: 'running',
+ icon: 'status_running',
+ details_path: 'status/running',
+ },
+ skipped: {
+ text: 'skipped',
+ label: 'skipped',
+ group: 'skipped',
+ icon: 'status_skipped',
+ details_path: 'status/skipped',
+ },
+ success_warining: {
+ text: 'passed',
+ label: 'passed',
+ group: 'success-with-warnings',
+ icon: 'status_warning',
+ details_path: 'status/warning',
+ },
+ success: {
+ text: 'passed',
+ label: 'passed',
+ group: 'passed',
+ icon: 'status_success',
+ details_path: 'status/passed',
+ },
+ };
+
+ beforeEach(() => {
+ CIBadge = Vue.extend(ciBadge);
+ });
+
+ afterEach(() => {
+ vm.$destroy();
+ });
+
+ it('should render each status badge', () => {
+ Object.keys(statuses).map(status => {
+ vm = mountComponent(CIBadge, { status: statuses[status] });
+
+ expect(vm.$el.getAttribute('href')).toEqual(statuses[status].details_path);
+ expect(vm.$el.textContent.trim()).toEqual(statuses[status].text);
+ expect(vm.$el.getAttribute('class')).toContain(`ci-status ci-${statuses[status].group}`);
+ expect(vm.$el.querySelector('svg')).toBeDefined();
+ return vm;
+ });
+ });
+
+ it('should not render label', () => {
+ vm = mountComponent(CIBadge, { status: statuses.canceled, showText: false });
+
+ expect(vm.$el.textContent.trim()).toEqual('');
+ });
+});
diff --git a/spec/frontend/vue_shared/components/ci_icon_spec.js b/spec/frontend/vue_shared/components/ci_icon_spec.js
new file mode 100644
index 00000000000..63afe631063
--- /dev/null
+++ b/spec/frontend/vue_shared/components/ci_icon_spec.js
@@ -0,0 +1,122 @@
+import Vue from 'vue';
+import mountComponent from 'helpers/vue_mount_component_helper';
+import ciIcon from '~/vue_shared/components/ci_icon.vue';
+
+describe('CI Icon component', () => {
+ const Component = Vue.extend(ciIcon);
+ let vm;
+
+ afterEach(() => {
+ vm.$destroy();
+ });
+
+ it('should render a span element with an svg', () => {
+ vm = mountComponent(Component, {
+ status: {
+ icon: 'status_success',
+ },
+ });
+
+ expect(vm.$el.tagName).toEqual('SPAN');
+ expect(vm.$el.querySelector('span > svg')).toBeDefined();
+ });
+
+ it('should render a success status', () => {
+ vm = mountComponent(Component, {
+ status: {
+ icon: 'status_success',
+ group: 'success',
+ },
+ });
+
+ expect(vm.$el.classList.contains('ci-status-icon-success')).toEqual(true);
+ });
+
+ it('should render a failed status', () => {
+ vm = mountComponent(Component, {
+ status: {
+ icon: 'status_failed',
+ group: 'failed',
+ },
+ });
+
+ expect(vm.$el.classList.contains('ci-status-icon-failed')).toEqual(true);
+ });
+
+ it('should render success with warnings status', () => {
+ vm = mountComponent(Component, {
+ status: {
+ icon: 'status_warning',
+ group: 'warning',
+ },
+ });
+
+ expect(vm.$el.classList.contains('ci-status-icon-warning')).toEqual(true);
+ });
+
+ it('should render pending status', () => {
+ vm = mountComponent(Component, {
+ status: {
+ icon: 'status_pending',
+ group: 'pending',
+ },
+ });
+
+ expect(vm.$el.classList.contains('ci-status-icon-pending')).toEqual(true);
+ });
+
+ it('should render running status', () => {
+ vm = mountComponent(Component, {
+ status: {
+ icon: 'status_running',
+ group: 'running',
+ },
+ });
+
+ expect(vm.$el.classList.contains('ci-status-icon-running')).toEqual(true);
+ });
+
+ it('should render created status', () => {
+ vm = mountComponent(Component, {
+ status: {
+ icon: 'status_created',
+ group: 'created',
+ },
+ });
+
+ expect(vm.$el.classList.contains('ci-status-icon-created')).toEqual(true);
+ });
+
+ it('should render skipped status', () => {
+ vm = mountComponent(Component, {
+ status: {
+ icon: 'status_skipped',
+ group: 'skipped',
+ },
+ });
+
+ expect(vm.$el.classList.contains('ci-status-icon-skipped')).toEqual(true);
+ });
+
+ it('should render canceled status', () => {
+ vm = mountComponent(Component, {
+ status: {
+ icon: 'status_canceled',
+ group: 'canceled',
+ },
+ });
+
+ expect(vm.$el.classList.contains('ci-status-icon-canceled')).toEqual(true);
+ });
+
+ it('should render status for manual action', () => {
+ vm = mountComponent(Component, {
+ status: {
+ icon: 'status_manual',
+ group: 'manual',
+ },
+ });
+
+ expect(vm.$el.classList.contains('ci-status-icon-manual')).toEqual(true);
+ });
+});
diff --git a/spec/frontend/vue_shared/components/code_block_spec.js b/spec/frontend/vue_shared/components/code_block_spec.js
index 0d21dd94f7c..60b0b0b566b 100644
--- a/spec/frontend/vue_shared/components/code_block_spec.js
+++ b/spec/frontend/vue_shared/components/code_block_spec.js
@@ -4,10 +4,15 @@ import CodeBlock from '~/vue_shared/components/code_block.vue';
describe('Code Block', () => {
let wrapper;
- const createComponent = () => {
+ const defaultProps = {
+ code: 'test-code',
+ };
+
+ const createComponent = (props = {}) => {
wrapper = shallowMount(CodeBlock, {
propsData: {
- code: 'test-code',
+ ...defaultProps,
+ ...props,
},
});
};
@@ -17,9 +22,23 @@ describe('Code Block', () => {
wrapper = null;
});
- it('matches snapshot', () => {
- createComponent();
+ describe('with default props', () => {
+ beforeEach(() => {
+ createComponent();
+ });
- expect(wrapper.element).toMatchSnapshot();
+ it('renders correctly', () => {
+ expect(wrapper.element).toMatchSnapshot();
+ });
+ });
+
+ describe('with maxHeight set to "200px"', () => {
+ beforeEach(() => {
+ createComponent({ maxHeight: '200px' });
+ });
+
+ it('renders correctly', () => {
+ expect(wrapper.element).toMatchSnapshot();
+ });
});
});
diff --git a/spec/frontend/vue_shared/components/content_viewer/content_viewer_spec.js b/spec/frontend/vue_shared/components/content_viewer/content_viewer_spec.js
new file mode 100644
index 00000000000..16e7e4dd5cc
--- /dev/null
+++ b/spec/frontend/vue_shared/components/content_viewer/content_viewer_spec.js
@@ -0,0 +1,21 @@
+import { mount } from '@vue/test-utils';
+import { GREEN_BOX_IMAGE_URL } from 'spec/test_constants';
+import ContentViewer from '~/vue_shared/components/content_viewer/content_viewer.vue';
+import '~/behaviors/markdown/render_gfm';
+
+describe('ContentViewer', () => {
+ let wrapper;
+
+ it.each`
+ path | type | selector | viewer
+ ${GREEN_BOX_IMAGE_URL} | ${'image'} | ${'img'} | ${'<image-viewer>'}
+ ${'myfile.md'} | ${'markdown'} | ${'.md-previewer'} | ${'<markdown-viewer>'}
+ ${'myfile.abc'} | ${undefined} | ${'[download]'} | ${'<download-viewer>'}
+ `('renders $viewer when file type="$type"', ({ path, type, selector }) => {
+ wrapper = mount(ContentViewer, {
+ propsData: { path, fileSize: 1024, type },
+ });
+
+ expect(wrapper.find(selector).element).toExist();
+ });
+});
diff --git a/spec/frontend/vue_shared/components/content_viewer/lib/viewer_utils_spec.js b/spec/frontend/vue_shared/components/content_viewer/lib/viewer_utils_spec.js
new file mode 100644
index 00000000000..facdaa86f84
--- /dev/null
+++ b/spec/frontend/vue_shared/components/content_viewer/lib/viewer_utils_spec.js
@@ -0,0 +1,20 @@
+import { viewerInformationForPath } from '~/vue_shared/components/content_viewer/lib/viewer_utils';
+
+describe('viewerInformationForPath', () => {
+ it.each`
+ path | type
+ ${'p/somefile.jpg'} | ${'image'}
+ ${'p/somefile.jpeg'} | ${'image'}
+ ${'p/somefile.bmp'} | ${'image'}
+ ${'p/somefile.ico'} | ${'image'}
+ ${'p/somefile.png'} | ${'image'}
+ ${'p/somefile.gif'} | ${'image'}
+ ${'p/somefile.md'} | ${'markdown'}
+ ${'p/md'} | ${undefined}
+ ${'p/png'} | ${undefined}
+ ${'p/md.png/a'} | ${undefined}
+ ${'p/some-file.php'} | ${undefined}
+ `('when path=$path, type=$type', ({ path, type }) => {
+ expect(viewerInformationForPath(path)?.id).toBe(type);
+ });
+});
diff --git a/spec/frontend/vue_shared/components/content_viewer/viewers/download_viewer_spec.js b/spec/frontend/vue_shared/components/content_viewer/viewers/download_viewer_spec.js
new file mode 100644
index 00000000000..b83602e7bfc
--- /dev/null
+++ b/spec/frontend/vue_shared/components/content_viewer/viewers/download_viewer_spec.js
@@ -0,0 +1,28 @@
+import { mount } from '@vue/test-utils';
+import DownloadViewer from '~/vue_shared/components/content_viewer/viewers/download_viewer.vue';
+
+describe('DownloadViewer', () => {
+ let wrapper;
+
+ it.each`
+ path | filePath | fileSize | renderedName | renderedSize
+ ${'somepath/test.abc'} | ${undefined} | ${1024} | ${'test.abc'} | ${'1.00 KiB'}
+ ${'somepath/test.abc'} | ${undefined} | ${null} | ${'test.abc'} | ${''}
+ ${'data:application/unknown;base64,U0VMRUNU'} | ${'somepath/test.abc'} | ${2048} | ${'test.abc'} | ${'2.00 KiB'}
+ `(
+ 'renders the file name as "$renderedName" and shows size as "$renderedSize"',
+ ({ path, filePath, fileSize, renderedName, renderedSize }) => {
+ wrapper = mount(DownloadViewer, {
+ propsData: { path, filePath, fileSize },
+ });
+
+ const renderedFileInfo = wrapper.find('.file-info').text();
+
+ expect(renderedFileInfo).toContain(renderedName);
+ expect(renderedFileInfo).toContain(renderedSize);
+
+ expect(wrapper.find('.btn.btn-default').text()).toContain('Download');
+ expect(wrapper.find('.btn.btn-default').element).toHaveAttr('download', 'test.abc');
+ },
+ );
+});
diff --git a/spec/frontend/vue_shared/components/content_viewer/viewers/image_viewer_spec.js b/spec/frontend/vue_shared/components/content_viewer/viewers/image_viewer_spec.js
index ef785b9f0f5..31e843297fa 100644
--- a/spec/frontend/vue_shared/components/content_viewer/viewers/image_viewer_spec.js
+++ b/spec/frontend/vue_shared/components/content_viewer/viewers/image_viewer_spec.js
@@ -1,45 +1,36 @@
-import { shallowMount } from '@vue/test-utils';
-
+import { mount } from '@vue/test-utils';
import { GREEN_BOX_IMAGE_URL } from 'spec/test_constants';
import ImageViewer from '~/vue_shared/components/content_viewer/viewers/image_viewer.vue';
describe('Image Viewer', () => {
- const requiredProps = {
- path: GREEN_BOX_IMAGE_URL,
- renderInfo: true,
- };
let wrapper;
- let imageInfo;
-
- function createElement({ props, includeRequired = true } = {}) {
- const data = includeRequired ? { ...requiredProps, ...props } : { ...props };
- wrapper = shallowMount(ImageViewer, {
- propsData: data,
+ it('renders image preview', () => {
+ wrapper = mount(ImageViewer, {
+ propsData: { path: GREEN_BOX_IMAGE_URL, fileSize: 1024 },
});
- imageInfo = wrapper.find('.image-info');
- }
-
- describe('file sizes', () => {
- it('should show the humanized file size when `renderInfo` is true and there is size info', () => {
- createElement({ props: { fileSize: 1024 } });
-
- expect(imageInfo.text()).toContain('1.00 KiB');
- });
-
- it('should not show the humanized file size when `renderInfo` is true and there is no size', () => {
- const FILESIZE_RE = /\d+(\.\d+)?\s*([KMGTP]i)*B/;
- createElement({ props: { fileSize: 0 } });
-
- // It shouldn't show any filesize info
- expect(imageInfo.text()).not.toMatch(FILESIZE_RE);
- });
-
- it('should not show any image information when `renderInfo` is false', () => {
- createElement({ props: { renderInfo: false } });
+ expect(wrapper.find('img').element).toHaveAttr('src', GREEN_BOX_IMAGE_URL);
+ });
- expect(imageInfo.exists()).toBe(false);
- });
+ describe('file sizes', () => {
+ it.each`
+ fileSize | renderInfo | elementExists | humanizedFileSize
+ ${1024} | ${true} | ${true} | ${'1.00 KiB'}
+ ${0} | ${true} | ${true} | ${''}
+ ${1024} | ${false} | ${false} | ${undefined}
+ `(
+ 'shows file size as "$humanizedFileSize", if fileSize=$fileSize and renderInfo=$renderInfo',
+ ({ fileSize, renderInfo, elementExists, humanizedFileSize }) => {
+ wrapper = mount(ImageViewer, {
+ propsData: { path: GREEN_BOX_IMAGE_URL, fileSize, renderInfo },
+ });
+
+ const imageInfo = wrapper.find('.image-info');
+
+ expect(imageInfo.exists()).toBe(elementExists);
+ expect(imageInfo.element?.textContent.trim()).toBe(humanizedFileSize);
+ },
+ );
});
});
diff --git a/spec/frontend/vue_shared/components/content_viewer/viewers/markdown_viewer_spec.js b/spec/frontend/vue_shared/components/content_viewer/viewers/markdown_viewer_spec.js
new file mode 100644
index 00000000000..8d3fcdd48d2
--- /dev/null
+++ b/spec/frontend/vue_shared/components/content_viewer/viewers/markdown_viewer_spec.js
@@ -0,0 +1,114 @@
+import $ from 'jquery';
+import axios from '~/lib/utils/axios_utils';
+import MockAdapter from 'axios-mock-adapter';
+import { mount } from '@vue/test-utils';
+import waitForPromises from 'helpers/wait_for_promises';
+import MarkdownViewer from '~/vue_shared/components/content_viewer/viewers/markdown_viewer.vue';
+
+describe('MarkdownViewer', () => {
+ let wrapper;
+ let mock;
+
+ const createComponent = props => {
+ wrapper = mount(MarkdownViewer, {
+ propsData: {
+ ...props,
+ path: 'test.md',
+ content: '* Test',
+ projectPath: 'testproject',
+ type: 'markdown',
+ },
+ });
+ };
+
+ beforeEach(() => {
+ mock = new MockAdapter(axios);
+
+ jest.spyOn(axios, 'post');
+ jest.spyOn($.fn, 'renderGFM');
+ });
+
+ afterEach(() => {
+ mock.restore();
+ });
+
+ describe('success', () => {
+ beforeEach(() => {
+ mock.onPost(`${gon.relative_url_root}/testproject/preview_markdown`).replyOnce(200, {
+ body: '<b>testing</b> {{gl_md_img_1}}',
+ });
+ });
+
+ it('renders an animation container while the markdown is loading', () => {
+ createComponent();
+
+ expect(wrapper.find('.animation-container')).toExist();
+ });
+
+ it('renders markdown preview preview renders and loads rendered markdown from server', () => {
+ createComponent();
+
+ return waitForPromises().then(() => {
+ expect(wrapper.find('.md-previewer').text()).toContain('testing');
+ });
+ });
+
+ it('receives the filePath and commitSha as a parameters and passes them on to the server', () => {
+ createComponent({ filePath: 'foo/test.md', commitSha: 'abcdef' });
+
+ expect(axios.post).toHaveBeenCalledWith(
+ `${gon.relative_url_root}/testproject/preview_markdown`,
+ { path: 'foo/test.md', text: '* Test', ref: 'abcdef' },
+ expect.any(Object),
+ );
+ });
+
+ it.each`
+ imgSrc | imgAlt
+ ${'data:image/jpeg;base64,AAAAAA+/'} | ${'my image title'}
+ ${'data:image/jpeg;base64,AAAAAA+/'} | ${'"somebody\'s image" &'}
+ ${'hack" onclick=alert(0)'} | ${'hack" onclick=alert(0)'}
+ ${'hack\\" onclick=alert(0)'} | ${'hack\\" onclick=alert(0)'}
+ ${"hack' onclick=alert(0)"} | ${"hack' onclick=alert(0)"}
+ ${"hack'><script>alert(0)</script>"} | ${"hack'><script>alert(0)</script>"}
+ `(
+ 'transforms template tags with base64 encoded images available locally',
+ ({ imgSrc, imgAlt }) => {
+ createComponent({
+ images: {
+ '{{gl_md_img_1}}': {
+ src: imgSrc,
+ alt: imgAlt,
+ title: imgAlt,
+ },
+ },
+ });
+
+ return waitForPromises().then(() => {
+ const img = wrapper.find('.md-previewer img').element;
+
+ // if the values are the same as the input, it means
+ // they were escaped correctly
+ expect(img).toHaveAttr('src', imgSrc);
+ expect(img).toHaveAttr('alt', imgAlt);
+ expect(img).toHaveAttr('title', imgAlt);
+ });
+ },
+ );
+ });
+
+ describe('error', () => {
+ beforeEach(() => {
+ mock.onPost(`${gon.relative_url_root}/testproject/preview_markdown`).replyOnce(500, {
+ body: 'Internal Server Error',
+ });
+ });
+ it('renders an error message if loading the markdown preview fails', () => {
+ createComponent();
+
+ return waitForPromises().then(() => {
+ expect(wrapper.find('.md-previewer').text()).toContain('error');
+ });
+ });
+ });
+});
diff --git a/spec/frontend/vue_shared/components/date_time_picker/date_time_picker_lib_spec.js b/spec/frontend/vue_shared/components/date_time_picker/date_time_picker_lib_spec.js
index 3a75ab2d127..98962918b49 100644
--- a/spec/frontend/vue_shared/components/date_time_picker/date_time_picker_lib_spec.js
+++ b/spec/frontend/vue_shared/components/date_time_picker/date_time_picker_lib_spec.js
@@ -56,13 +56,8 @@ describe('date time picker lib', () => {
describe('stringToISODate', () => {
['', 'null', undefined, 'abc'].forEach(input => {
- it(`throws error for invalid input like ${input}`, done => {
- try {
- dateTimePickerLib.stringToISODate(input);
- } catch (e) {
- expect(e).toBeDefined();
- done();
- }
+ it(`throws error for invalid input like ${input}`, () => {
+ expect(() => dateTimePickerLib.stringToISODate(input)).toThrow();
});
});
[
diff --git a/spec/frontend/vue_shared/components/diff_viewer/diff_viewer_spec.js b/spec/frontend/vue_shared/components/diff_viewer/diff_viewer_spec.js
new file mode 100644
index 00000000000..636508be6b6
--- /dev/null
+++ b/spec/frontend/vue_shared/components/diff_viewer/diff_viewer_spec.js
@@ -0,0 +1,98 @@
+import Vue from 'vue';
+
+import mountComponent from 'helpers/vue_mount_component_helper';
+import { GREEN_BOX_IMAGE_URL, RED_BOX_IMAGE_URL } from 'spec/test_constants';
+import diffViewer from '~/vue_shared/components/diff_viewer/diff_viewer.vue';
+
+describe('DiffViewer', () => {
+ const requiredProps = {
+ diffMode: 'replaced',
+ diffViewerMode: 'image',
+ newPath: GREEN_BOX_IMAGE_URL,
+ newSha: 'ABC',
+ oldPath: RED_BOX_IMAGE_URL,
+ oldSha: 'DEF',
+ };
+ let vm;
+
+ function createComponent(props) {
+ const DiffViewer = Vue.extend(diffViewer);
+
+ vm = mountComponent(DiffViewer, props);
+ }
+
+ afterEach(() => {
+ vm.$destroy();
+ });
+
+ it('renders image diff', done => {
+ window.gon = {
+ relative_url_root: '',
+ };
+
+ createComponent({ ...requiredProps, projectPath: '' });
+
+ setImmediate(() => {
+ expect(vm.$el.querySelector('.deleted img').getAttribute('src')).toBe(
+ `//-/raw/DEF/${RED_BOX_IMAGE_URL}`,
+ );
+
+ expect(vm.$el.querySelector('.added img').getAttribute('src')).toBe(
+ `//-/raw/ABC/${GREEN_BOX_IMAGE_URL}`,
+ );
+
+ done();
+ });
+ });
+
+ it('renders fallback download diff display', done => {
+ createComponent({
+ ...requiredProps,
+ diffViewerMode: 'added',
+ newPath: 'test.abc',
+ oldPath: 'testold.abc',
+ });
+
+ setImmediate(() => {
+ expect(vm.$el.querySelector('.deleted .file-info').textContent.trim()).toContain(
+ 'testold.abc',
+ );
+
+ expect(vm.$el.querySelector('.deleted .btn.btn-default').textContent.trim()).toContain(
+ 'Download',
+ );
+
+ expect(vm.$el.querySelector('.added .file-info').textContent.trim()).toContain('test.abc');
+ expect(vm.$el.querySelector('.added .btn.btn-default').textContent.trim()).toContain(
+ 'Download',
+ );
+
+ done();
+ });
+ });
+
+ it('renders renamed component', () => {
+ createComponent({
+ ...requiredProps,
+ diffMode: 'renamed',
+ diffViewerMode: 'renamed',
+ newPath: 'test.abc',
+ oldPath: 'testold.abc',
+ });
+
+ expect(vm.$el.textContent).toContain('File moved');
+ });
+
+ it('renders mode changed component', () => {
+ createComponent({
+ ...requiredProps,
+ diffMode: 'mode_changed',
+ newPath: 'test.abc',
+ oldPath: 'testold.abc',
+ aMode: '123',
+ bMode: '321',
+ });
+
+ expect(vm.$el.textContent).toContain('File mode changed from 123 to 321');
+ });
+});
diff --git a/spec/frontend/vue_shared/components/dropdown/dropdown_button_spec.js b/spec/frontend/vue_shared/components/dropdown/dropdown_button_spec.js
new file mode 100644
index 00000000000..892a96b76fd
--- /dev/null
+++ b/spec/frontend/vue_shared/components/dropdown/dropdown_button_spec.js
@@ -0,0 +1,81 @@
+import Vue from 'vue';
+
+import { mountComponentWithSlots } from 'helpers/vue_mount_component_helper';
+import dropdownButtonComponent from '~/vue_shared/components/dropdown/dropdown_button.vue';
+
+const defaultLabel = 'Select';
+const customLabel = 'Select project';
+
+const createComponent = (props, slots = {}) => {
+ const Component = Vue.extend(dropdownButtonComponent);
+
+ return mountComponentWithSlots(Component, { props, slots });
+};
+
+describe('DropdownButtonComponent', () => {
+ let vm;
+
+ beforeEach(() => {
+ vm = createComponent();
+ });
+
+ afterEach(() => {
+ vm.$destroy();
+ });
+
+ describe('computed', () => {
+ describe('dropdownToggleText', () => {
+ it('returns default toggle text', () => {
+ expect(vm.toggleText).toBe(defaultLabel);
+ });
+
+ it('returns custom toggle text when provided via props', () => {
+ const vmEmptyLabels = createComponent({ toggleText: customLabel });
+
+ expect(vmEmptyLabels.toggleText).toBe(customLabel);
+ vmEmptyLabels.$destroy();
+ });
+ });
+ });
+
+ describe('template', () => {
+ it('renders component container element of type `button`', () => {
+ expect(vm.$el.nodeName).toBe('BUTTON');
+ });
+
+ it('renders component container element with required data attributes', () => {
+ expect(vm.$el.dataset.abilityName).toBe(vm.abilityName);
+ expect(vm.$el.dataset.fieldName).toBe(vm.fieldName);
+ expect(vm.$el.dataset.issueUpdate).toBe(vm.updatePath);
+ expect(vm.$el.dataset.labels).toBe(vm.labelsPath);
+ expect(vm.$el.dataset.namespacePath).toBe(vm.namespace);
+ expect(vm.$el.dataset.showAny).not.toBeDefined();
+ });
+
+ it('renders dropdown toggle text element', () => {
+ const dropdownToggleTextEl = vm.$el.querySelector('.dropdown-toggle-text');
+
+ expect(dropdownToggleTextEl).not.toBeNull();
+ expect(dropdownToggleTextEl.innerText.trim()).toBe(defaultLabel);
+ });
+
+ it('renders dropdown button icon', () => {
+ const dropdownIconEl = vm.$el.querySelector('.dropdown-toggle-icon i.fa');
+
+ expect(dropdownIconEl).not.toBeNull();
+ expect(dropdownIconEl.classList.contains('fa-chevron-down')).toBe(true);
+ });
+
+ it('renders slot, if default slot exists', () => {
+ vm = createComponent(
+ {},
+ {
+ default: ['Lorem Ipsum Dolar'],
+ },
+ );
+
+ expect(vm.$el.querySelector('.dropdown-toggle-text')).toBeNull();
+ expect(vm.$el).toHaveText('Lorem Ipsum Dolar');
+ });
+ });
+});
diff --git a/spec/frontend/vue_shared/components/dropdown/dropdown_hidden_input_spec.js b/spec/frontend/vue_shared/components/dropdown/dropdown_hidden_input_spec.js
new file mode 100644
index 00000000000..30b8e869aab
--- /dev/null
+++ b/spec/frontend/vue_shared/components/dropdown/dropdown_hidden_input_spec.js
@@ -0,0 +1,36 @@
+import Vue from 'vue';
+
+import mountComponent from 'helpers/vue_mount_component_helper';
+import dropdownHiddenInputComponent from '~/vue_shared/components/dropdown/dropdown_hidden_input.vue';
+
+import { mockLabels } from './mock_data';
+
+const createComponent = (name = 'label_id[]', value = mockLabels[0].id) => {
+ const Component = Vue.extend(dropdownHiddenInputComponent);
+
+ return mountComponent(Component, {
+ name,
+ value,
+ });
+};
+
+describe('DropdownHiddenInputComponent', () => {
+ let vm;
+
+ beforeEach(() => {
+ vm = createComponent();
+ });
+
+ afterEach(() => {
+ vm.$destroy();
+ });
+
+ describe('template', () => {
+ it('renders input element of type `hidden`', () => {
+ expect(vm.$el.nodeName).toBe('INPUT');
+ expect(vm.$el.getAttribute('type')).toBe('hidden');
+ expect(vm.$el.getAttribute('name')).toBe(vm.name);
+ expect(vm.$el.getAttribute('value')).toBe(`${vm.value}`);
+ });
+ });
+});
diff --git a/spec/javascripts/vue_shared/components/dropdown/mock_data.js b/spec/frontend/vue_shared/components/dropdown/mock_data.js
index b09d42da401..b09d42da401 100644
--- a/spec/javascripts/vue_shared/components/dropdown/mock_data.js
+++ b/spec/frontend/vue_shared/components/dropdown/mock_data.js
diff --git a/spec/frontend/vue_shared/components/file_finder/item_spec.js b/spec/frontend/vue_shared/components/file_finder/item_spec.js
new file mode 100644
index 00000000000..63f2614106d
--- /dev/null
+++ b/spec/frontend/vue_shared/components/file_finder/item_spec.js
@@ -0,0 +1,140 @@
+import Vue from 'vue';
+import { file } from 'jest/ide/helpers';
+import ItemComponent from '~/vue_shared/components/file_finder/item.vue';
+import createComponent from 'helpers/vue_mount_component_helper';
+
+describe('File finder item spec', () => {
+ const Component = Vue.extend(ItemComponent);
+ let vm;
+ let localFile;
+
+ beforeEach(() => {
+ localFile = {
+ ...file(),
+ name: 'test file',
+ path: 'test/file',
+ };
+
+ vm = createComponent(Component, {
+ file: localFile,
+ focused: true,
+ searchText: '',
+ index: 0,
+ });
+ });
+
+ afterEach(() => {
+ vm.$destroy();
+ });
+
+ it('renders file name & path', () => {
+ expect(vm.$el.textContent).toContain('test file');
+ expect(vm.$el.textContent).toContain('test/file');
+ });
+
+ describe('focused', () => {
+ it('adds is-focused class', () => {
+ expect(vm.$el.classList).toContain('is-focused');
+ });
+
+ it('does not have is-focused class when not focused', done => {
+ vm.focused = false;
+
+ vm.$nextTick(() => {
+ expect(vm.$el.classList).not.toContain('is-focused');
+
+ done();
+ });
+ });
+ });
+
+ describe('changed file icon', () => {
+ it('does not render when not a changed or temp file', () => {
+ expect(vm.$el.querySelector('.diff-changed-stats')).toBe(null);
+ });
+
+ it('renders when a changed file', done => {
+ vm.file.changed = true;
+
+ vm.$nextTick(() => {
+ expect(vm.$el.querySelector('.diff-changed-stats')).not.toBe(null);
+
+ done();
+ });
+ });
+
+ it('renders when a temp file', done => {
+ vm.file.tempFile = true;
+
+ vm.$nextTick(() => {
+ expect(vm.$el.querySelector('.diff-changed-stats')).not.toBe(null);
+
+ done();
+ });
+ });
+ });
+
+ it('emits event when clicked', () => {
+ jest.spyOn(vm, '$emit').mockImplementation(() => {});
+
+ vm.$el.click();
+
+ expect(vm.$emit).toHaveBeenCalledWith('click', vm.file);
+ });
+
+ describe('path', () => {
+ let el;
+
+ beforeEach(done => {
+ vm.searchText = 'file';
+
+ el = vm.$el.querySelector('.diff-changed-file-path');
+
+ vm.$nextTick(done);
+ });
+
+ it('highlights text', () => {
+ expect(el.querySelectorAll('.highlighted').length).toBe(4);
+ });
+
+ it('adds ellipsis to long text', done => {
+ vm.file.path = new Array(70)
+ .fill()
+ .map((_, i) => `${i}-`)
+ .join('');
+
+ vm.$nextTick(() => {
+ expect(el.textContent).toBe(`...${vm.file.path.substr(vm.file.path.length - 60)}`);
+ done();
+ });
+ });
+ });
+
+ describe('name', () => {
+ let el;
+
+ beforeEach(done => {
+ vm.searchText = 'file';
+
+ el = vm.$el.querySelector('.diff-changed-file-name');
+
+ vm.$nextTick(done);
+ });
+
+ it('highlights text', () => {
+ expect(el.querySelectorAll('.highlighted').length).toBe(4);
+ });
+
+ it('does not add ellipsis to long text', done => {
+ vm.file.name = new Array(70)
+ .fill()
+ .map((_, i) => `${i}-`)
+ .join('');
+
+ vm.$nextTick(() => {
+ expect(el.textContent).not.toBe(`...${vm.file.name.substr(vm.file.name.length - 60)}`);
+ done();
+ });
+ });
+ });
+});
diff --git a/spec/frontend/vue_shared/components/file_row_spec.js b/spec/frontend/vue_shared/components/file_row_spec.js
index 732491378fa..46df2d2aaf1 100644
--- a/spec/frontend/vue_shared/components/file_row_spec.js
+++ b/spec/frontend/vue_shared/components/file_row_spec.js
@@ -91,9 +91,7 @@ describe('File row component', () => {
jest.spyOn(wrapper.vm, 'scrollIntoView');
wrapper.setProps({
- file: Object.assign({}, wrapper.props('file'), {
- active: true,
- }),
+ file: { ...wrapper.props('file'), active: true },
});
return nextTick().then(() => {
@@ -125,9 +123,7 @@ describe('File row component', () => {
it('matches the current route against encoded file URL', () => {
const fileName = 'with space';
- const rowFile = Object.assign({}, file(fileName), {
- url: `/${fileName}`,
- });
+ const rowFile = { ...file(fileName), url: `/${fileName}` };
const routerPath = `/project/${escapeFileUrl(fileName)}`;
createComponent(
{
diff --git a/spec/frontend/vue_shared/components/filtered_search_dropdown_spec.js b/spec/frontend/vue_shared/components/filtered_search_dropdown_spec.js
new file mode 100644
index 00000000000..87cafa0bb8c
--- /dev/null
+++ b/spec/frontend/vue_shared/components/filtered_search_dropdown_spec.js
@@ -0,0 +1,190 @@
+import Vue from 'vue';
+import mountComponent from 'helpers/vue_mount_component_helper';
+import component from '~/vue_shared/components/filtered_search_dropdown.vue';
+
+describe('Filtered search dropdown', () => {
+ const Component = Vue.extend(component);
+ let vm;
+
+ afterEach(() => {
+ vm.$destroy();
+ });
+
+ describe('with an empty array of items', () => {
+ beforeEach(() => {
+ vm = mountComponent(Component, {
+ items: [],
+ filterKey: '',
+ });
+ });
+
+ it('renders empty list', () => {
+ expect(vm.$el.querySelectorAll('.js-filtered-dropdown-result').length).toEqual(0);
+ });
+
+ it('renders filter input', () => {
+ expect(vm.$el.querySelector('.js-filtered-dropdown-input')).not.toBeNull();
+ });
+ });
+
+ describe('when visible numbers is less than the items length', () => {
+ beforeEach(() => {
+ vm = mountComponent(Component, {
+ items: [{ title: 'One' }, { title: 'Two' }, { title: 'Three' }],
+ visibleItems: 2,
+ filterKey: 'title',
+ });
+ });
+
+ it('it renders only the maximum number provided', () => {
+ expect(vm.$el.querySelectorAll('.js-filtered-dropdown-result').length).toEqual(2);
+ });
+ });
+
+ describe('when visible number is bigger than the items length', () => {
+ beforeEach(() => {
+ vm = mountComponent(Component, {
+ items: [{ title: 'One' }, { title: 'Two' }, { title: 'Three' }],
+ filterKey: 'title',
+ });
+ });
+
+ it('it renders the full list of items the maximum number provided', () => {
+ expect(vm.$el.querySelectorAll('.js-filtered-dropdown-result').length).toEqual(3);
+ });
+ });
+
+ describe('while filtering', () => {
+ beforeEach(() => {
+ vm = mountComponent(Component, {
+ items: [
+ { title: 'One' },
+ { title: 'Two/three' },
+ { title: 'Three four' },
+ { title: 'Five' },
+ ],
+ filterKey: 'title',
+ });
+ });
+
+ it('updates the results to match the typed value', done => {
+ vm.$el.querySelector('.js-filtered-dropdown-input').value = 'three';
+ vm.$el.querySelector('.js-filtered-dropdown-input').dispatchEvent(new Event('input'));
+ vm.$nextTick(() => {
+ expect(vm.$el.querySelectorAll('.js-filtered-dropdown-result').length).toEqual(2);
+ done();
+ });
+ });
+
+ describe('when no value matches the typed one', () => {
+ it('does not render any result', done => {
+ vm.$el.querySelector('.js-filtered-dropdown-input').value = 'six';
+ vm.$el.querySelector('.js-filtered-dropdown-input').dispatchEvent(new Event('input'));
+
+ vm.$nextTick(() => {
+ expect(vm.$el.querySelectorAll('.js-filtered-dropdown-result').length).toEqual(0);
+ done();
+ });
+ });
+ });
+ });
+
+ describe('with create mode enabled', () => {
+ describe('when there are no matches', () => {
+ beforeEach(() => {
+ vm = mountComponent(Component, {
+ items: [
+ { title: 'One' },
+ { title: 'Two/three' },
+ { title: 'Three four' },
+ { title: 'Five' },
+ ],
+ filterKey: 'title',
+ showCreateMode: true,
+ });
+
+ vm.$el.querySelector('.js-filtered-dropdown-input').value = 'eleven';
+ vm.$el.querySelector('.js-filtered-dropdown-input').dispatchEvent(new Event('input'));
+ });
+
+ it('renders a create button', done => {
+ vm.$nextTick(() => {
+ expect(vm.$el.querySelector('.js-dropdown-create-button')).not.toBeNull();
+ done();
+ });
+ });
+
+ it('renders computed button text', done => {
+ vm.$nextTick(() => {
+ expect(vm.$el.querySelector('.js-dropdown-create-button').textContent.trim()).toEqual(
+ 'Create eleven',
+ );
+ done();
+ });
+ });
+
+ describe('on click create button', () => {
+ it('emits createItem event with the filter', done => {
+ jest.spyOn(vm, '$emit').mockImplementation(() => {});
+ vm.$nextTick(() => {
+ vm.$el.querySelector('.js-dropdown-create-button').click();
+
+ expect(vm.$emit).toHaveBeenCalledWith('createItem', 'eleven');
+ done();
+ });
+ });
+ });
+ });
+
+ describe('when there are matches', () => {
+ beforeEach(() => {
+ vm = mountComponent(Component, {
+ items: [
+ { title: 'One' },
+ { title: 'Two/three' },
+ { title: 'Three four' },
+ { title: 'Five' },
+ ],
+ filterKey: 'title',
+ showCreateMode: true,
+ });
+
+ vm.$el.querySelector('.js-filtered-dropdown-input').value = 'one';
+ vm.$el.querySelector('.js-filtered-dropdown-input').dispatchEvent(new Event('input'));
+ });
+
+ it('does not render a create button', done => {
+ vm.$nextTick(() => {
+ expect(vm.$el.querySelector('.js-dropdown-create-button')).toBeNull();
+ done();
+ });
+ });
+ });
+ });
+
+ describe('with create mode disabled', () => {
+ describe('when there are no matches', () => {
+ beforeEach(() => {
+ vm = mountComponent(Component, {
+ items: [
+ { title: 'One' },
+ { title: 'Two/three' },
+ { title: 'Three four' },
+ { title: 'Five' },
+ ],
+ filterKey: 'title',
+ });
+
+ vm.$el.querySelector('.js-filtered-dropdown-input').value = 'eleven';
+ vm.$el.querySelector('.js-filtered-dropdown-input').dispatchEvent(new Event('input'));
+ });
+
+ it('does not render a create button', done => {
+ vm.$nextTick(() => {
+ expect(vm.$el.querySelector('.js-dropdown-create-button')).toBeNull();
+ done();
+ });
+ });
+ });
+ });
+});
diff --git a/spec/frontend/vue_shared/components/gl_countdown_spec.js b/spec/frontend/vue_shared/components/gl_countdown_spec.js
new file mode 100644
index 00000000000..365c9fad478
--- /dev/null
+++ b/spec/frontend/vue_shared/components/gl_countdown_spec.js
@@ -0,0 +1,83 @@
+import mountComponent from 'helpers/vue_mount_component_helper';
+import Vue from 'vue';
+import GlCountdown from '~/vue_shared/components/gl_countdown.vue';
+
+describe('GlCountdown', () => {
+ const Component = Vue.extend(GlCountdown);
+ let vm;
+ let now = '2000-01-01T00:00:00Z';
+
+ beforeEach(() => {
+ jest.spyOn(Date, 'now').mockImplementation(() => new Date(now).getTime());
+ });
+
+ afterEach(() => {
+ vm.$destroy();
+ jest.clearAllTimers();
+ });
+
+ describe('when there is time remaining', () => {
+ beforeEach(done => {
+ vm = mountComponent(Component, {
+ endDateString: '2000-01-01T01:02:03Z',
+ });
+
+ Vue.nextTick()
+ .then(done)
+ .catch(done.fail);
+ });
+
+ it('displays remaining time', () => {
+ expect(vm.$el.textContent).toContain('01:02:03');
+ });
+
+ it('updates remaining time', done => {
+ now = '2000-01-01T00:00:01Z';
+ jest.advanceTimersByTime(1000);
+
+ Vue.nextTick()
+ .then(() => {
+ expect(vm.$el.textContent).toContain('01:02:02');
+ done();
+ })
+ .catch(done.fail);
+ });
+ });
+
+ describe('when there is no time remaining', () => {
+ beforeEach(done => {
+ vm = mountComponent(Component, {
+ endDateString: '1900-01-01T00:00:00Z',
+ });
+
+ Vue.nextTick()
+ .then(done)
+ .catch(done.fail);
+ });
+
+ it('displays 00:00:00', () => {
+ expect(vm.$el.textContent).toContain('00:00:00');
+ });
+ });
+
+ describe('when an invalid date is passed', () => {
+ beforeEach(() => {
+ Vue.config.warnHandler = jest.fn();
+ });
+
+ afterEach(() => {
+ Vue.config.warnHandler = null;
+ });
+
+ it('throws a validation error', () => {
+ vm = mountComponent(Component, {
+ endDateString: 'this is invalid',
+ });
+
+ expect(Vue.config.warnHandler).toHaveBeenCalledTimes(1);
+ const [errorMessage] = Vue.config.warnHandler.mock.calls[0];
+
+ expect(errorMessage).toMatch(/^Invalid prop: .* "endDateString"/);
+ });
+ });
+});
diff --git a/spec/frontend/vue_shared/components/header_ci_component_spec.js b/spec/frontend/vue_shared/components/header_ci_component_spec.js
new file mode 100644
index 00000000000..216563165d6
--- /dev/null
+++ b/spec/frontend/vue_shared/components/header_ci_component_spec.js
@@ -0,0 +1,93 @@
+import Vue from 'vue';
+import mountComponent, { mountComponentWithSlots } from 'helpers/vue_mount_component_helper';
+import headerCi from '~/vue_shared/components/header_ci_component.vue';
+
+describe('Header CI Component', () => {
+ let HeaderCi;
+ let vm;
+ let props;
+
+ beforeEach(() => {
+ HeaderCi = Vue.extend(headerCi);
+ props = {
+ status: {
+ group: 'failed',
+ icon: 'status_failed',
+ label: 'failed',
+ text: 'failed',
+ details_path: 'path',
+ },
+ itemName: 'job',
+ itemId: 123,
+ time: '2017-05-08T14:57:39.781Z',
+ user: {
+ web_url: 'path',
+ name: 'Foo',
+ username: 'foobar',
+ email: 'foo@bar.com',
+ avatar_url: 'link',
+ },
+ hasSidebarButton: true,
+ };
+ });
+
+ afterEach(() => {
+ vm.$destroy();
+ });
+
+ const findActionButtons = () => vm.$el.querySelector('.header-action-buttons');
+
+ describe('render', () => {
+ beforeEach(() => {
+ vm = mountComponent(HeaderCi, props);
+ });
+
+ it('should render status badge', () => {
+ expect(vm.$el.querySelector('.ci-failed')).toBeDefined();
+ expect(vm.$el.querySelector('.ci-status-icon-failed svg')).toBeDefined();
+ expect(vm.$el.querySelector('.ci-failed').getAttribute('href')).toEqual(
+ props.status.details_path,
+ );
+ });
+
+ it('should render item name and id', () => {
+ expect(vm.$el.querySelector('strong').textContent.trim()).toEqual('job #123');
+ });
+
+ it('should render timeago date', () => {
+ expect(vm.$el.querySelector('time')).toBeDefined();
+ });
+
+ it('should render user icon and name', () => {
+ expect(vm.$el.querySelector('.js-user-link').innerText.trim()).toContain(props.user.name);
+ });
+
+ it('should render sidebar toggle button', () => {
+ expect(vm.$el.querySelector('.js-sidebar-build-toggle')).not.toBeNull();
+ });
+
+ it('should not render header action buttons when empty', () => {
+ expect(findActionButtons()).toBeNull();
+ });
+ });
+
+ describe('slot', () => {
+ it('should render header action buttons', () => {
+ vm = mountComponentWithSlots(HeaderCi, { props, slots: { default: 'Test Actions' } });
+
+ const buttons = findActionButtons();
+
+ expect(buttons).not.toBeNull();
+ expect(buttons.textContent).toEqual('Test Actions');
+ });
+ });
+
+ describe('shouldRenderTriggeredLabel', () => {
+ it('should rendered created keyword when the shouldRenderTriggeredLabel is false', () => {
+ vm = mountComponent(HeaderCi, { ...props, shouldRenderTriggeredLabel: false });
+
+ expect(vm.$el.textContent).toContain('created');
+ expect(vm.$el.textContent).not.toContain('triggered');
+ });
+ });
+});
diff --git a/spec/frontend/vue_shared/components/identicon_spec.js b/spec/frontend/vue_shared/components/identicon_spec.js
index 5e8b013d480..53a55dcd6bd 100644
--- a/spec/frontend/vue_shared/components/identicon_spec.js
+++ b/spec/frontend/vue_shared/components/identicon_spec.js
@@ -4,12 +4,17 @@ import IdenticonComponent from '~/vue_shared/components/identicon.vue';
describe('Identicon', () => {
let wrapper;
- const createComponent = () => {
+ const defaultProps = {
+ entityId: 1,
+ entityName: 'entity-name',
+ sizeClass: 's40',
+ };
+
+ const createComponent = (props = {}) => {
wrapper = shallowMount(IdenticonComponent, {
propsData: {
- entityId: 1,
- entityName: 'entity-name',
- sizeClass: 's40',
+ ...defaultProps,
+ ...props,
},
});
};
@@ -19,15 +24,27 @@ describe('Identicon', () => {
wrapper = null;
});
- it('matches snapshot', () => {
- createComponent();
+ describe('entity id is a number', () => {
+ beforeEach(createComponent);
+
+ it('matches snapshot', () => {
+ expect(wrapper.element).toMatchSnapshot();
+ });
- expect(wrapper.element).toMatchSnapshot();
+ it('adds a correct class to identicon', () => {
+ expect(wrapper.find({ ref: 'identicon' }).classes()).toContain('bg2');
+ });
});
- it('adds a correct class to identicon', () => {
- createComponent();
+ describe('entity id is a GraphQL id', () => {
+ beforeEach(() => createComponent({ entityId: 'gid://gitlab/Project/8' }));
+
+ it('matches snapshot', () => {
+ expect(wrapper.element).toMatchSnapshot();
+ });
- expect(wrapper.find({ ref: 'identicon' }).classes()).toContain('bg2');
+ it('adds a correct class to identicon', () => {
+ expect(wrapper.find({ ref: 'identicon' }).classes()).toContain('bg2');
+ });
});
});
diff --git a/spec/frontend/vue_shared/components/issue/issue_milestone_spec.js b/spec/frontend/vue_shared/components/issue/issue_milestone_spec.js
index 4c654e01f74..90c3fe54901 100644
--- a/spec/frontend/vue_shared/components/issue/issue_milestone_spec.js
+++ b/spec/frontend/vue_shared/components/issue/issue_milestone_spec.js
@@ -36,9 +36,7 @@ describe('IssueMilestoneComponent', () => {
describe('isMilestoneStarted', () => {
it('should return `false` when milestoneStart prop is not defined', () => {
wrapper.setProps({
- milestone: Object.assign({}, mockMilestone, {
- start_date: '',
- }),
+ milestone: { ...mockMilestone, start_date: '' },
});
expect(wrapper.vm.isMilestoneStarted).toBe(false);
@@ -46,9 +44,7 @@ describe('IssueMilestoneComponent', () => {
it('should return `true` when milestone start date is past current date', () => {
wrapper.setProps({
- milestone: Object.assign({}, mockMilestone, {
- start_date: '1990-07-22',
- }),
+ milestone: { ...mockMilestone, start_date: '1990-07-22' },
});
expect(wrapper.vm.isMilestoneStarted).toBe(true);
@@ -58,9 +54,7 @@ describe('IssueMilestoneComponent', () => {
describe('isMilestonePastDue', () => {
it('should return `false` when milestoneDue prop is not defined', () => {
wrapper.setProps({
- milestone: Object.assign({}, mockMilestone, {
- due_date: '',
- }),
+ milestone: { ...mockMilestone, due_date: '' },
});
expect(wrapper.vm.isMilestonePastDue).toBe(false);
@@ -68,9 +62,7 @@ describe('IssueMilestoneComponent', () => {
it('should return `true` when milestone due is past current date', () => {
wrapper.setProps({
- milestone: Object.assign({}, mockMilestone, {
- due_date: '1990-07-22',
- }),
+ milestone: { ...mockMilestone, due_date: '1990-07-22' },
});
expect(wrapper.vm.isMilestonePastDue).toBe(true);
@@ -84,9 +76,7 @@ describe('IssueMilestoneComponent', () => {
it('returns string containing absolute milestone start date when due date is not present', () => {
wrapper.setProps({
- milestone: Object.assign({}, mockMilestone, {
- due_date: '',
- }),
+ milestone: { ...mockMilestone, due_date: '' },
});
expect(wrapper.vm.milestoneDatesAbsolute).toBe('(January 1, 2018)');
@@ -94,10 +84,7 @@ describe('IssueMilestoneComponent', () => {
it('returns empty string when both milestone start and due dates are not present', () => {
wrapper.setProps({
- milestone: Object.assign({}, mockMilestone, {
- start_date: '',
- due_date: '',
- }),
+ milestone: { ...mockMilestone, start_date: '', due_date: '' },
});
expect(wrapper.vm.milestoneDatesAbsolute).toBe('');
@@ -107,9 +94,7 @@ describe('IssueMilestoneComponent', () => {
describe('milestoneDatesHuman', () => {
it('returns string containing milestone due date when date is yet to be due', () => {
wrapper.setProps({
- milestone: Object.assign({}, mockMilestone, {
- due_date: `${new Date().getFullYear() + 10}-01-01`,
- }),
+ milestone: { ...mockMilestone, due_date: `${new Date().getFullYear() + 10}-01-01` },
});
expect(wrapper.vm.milestoneDatesHuman).toContain('years remaining');
@@ -117,10 +102,7 @@ describe('IssueMilestoneComponent', () => {
it('returns string containing milestone start date when date has already started and due date is not present', () => {
wrapper.setProps({
- milestone: Object.assign({}, mockMilestone, {
- start_date: '1990-07-22',
- due_date: '',
- }),
+ milestone: { ...mockMilestone, start_date: '1990-07-22', due_date: '' },
});
expect(wrapper.vm.milestoneDatesHuman).toContain('Started');
@@ -128,10 +110,11 @@ describe('IssueMilestoneComponent', () => {
it('returns string containing milestone start date when date is yet to start and due date is not present', () => {
wrapper.setProps({
- milestone: Object.assign({}, mockMilestone, {
+ milestone: {
+ ...mockMilestone,
start_date: `${new Date().getFullYear() + 10}-01-01`,
due_date: '',
- }),
+ },
});
expect(wrapper.vm.milestoneDatesHuman).toContain('Starts');
@@ -139,10 +122,7 @@ describe('IssueMilestoneComponent', () => {
it('returns empty string when milestone start and due dates are not present', () => {
wrapper.setProps({
- milestone: Object.assign({}, mockMilestone, {
- start_date: '',
- due_date: '',
- }),
+ milestone: { ...mockMilestone, start_date: '', due_date: '' },
});
expect(wrapper.vm.milestoneDatesHuman).toBe('');
diff --git a/spec/frontend/vue_shared/components/issue/related_issuable_item_spec.js b/spec/frontend/vue_shared/components/issue/related_issuable_item_spec.js
index f7b1f041ef2..dd24ecf707d 100644
--- a/spec/frontend/vue_shared/components/issue/related_issuable_item_spec.js
+++ b/spec/frontend/vue_shared/components/issue/related_issuable_item_spec.js
@@ -2,10 +2,7 @@ import Vue from 'vue';
import { mount } from '@vue/test-utils';
import { formatDate } from '~/lib/utils/datetime_utility';
import RelatedIssuableItem from '~/vue_shared/components/issue/related_issuable_item.vue';
-import {
- defaultAssignees,
- defaultMilestone,
-} from '../../../../javascripts/vue_shared/components/issue/related_issuable_mock_data';
+import { defaultAssignees, defaultMilestone } from './related_issuable_mock_data';
describe('RelatedIssuableItem', () => {
let wrapper;
diff --git a/spec/frontend/vue_shared/components/markdown/field_spec.js b/spec/frontend/vue_shared/components/markdown/field_spec.js
index 46e269e5071..54ce1f47e28 100644
--- a/spec/frontend/vue_shared/components/markdown/field_spec.js
+++ b/spec/frontend/vue_shared/components/markdown/field_spec.js
@@ -9,9 +9,9 @@ const markdownPreviewPath = `${TEST_HOST}/preview`;
const markdownDocsPath = `${TEST_HOST}/docs`;
function assertMarkdownTabs(isWrite, writeLink, previewLink, wrapper) {
- expect(writeLink.element.parentNode.classList.contains('active')).toEqual(isWrite);
- expect(previewLink.element.parentNode.classList.contains('active')).toEqual(!isWrite);
- expect(wrapper.find('.md-preview-holder').element.style.display).toEqual(isWrite ? 'none' : '');
+ expect(writeLink.element.parentNode.classList.contains('active')).toBe(isWrite);
+ expect(previewLink.element.parentNode.classList.contains('active')).toBe(!isWrite);
+ expect(wrapper.find('.md-preview-holder').element.style.display).toBe(isWrite ? 'none' : '');
}
function createComponent() {
@@ -67,6 +67,10 @@ describe('Markdown field component', () => {
let previewLink;
let writeLink;
+ afterEach(() => {
+ wrapper.destroy();
+ });
+
it('renders textarea inside backdrop', () => {
wrapper = createComponent();
expect(wrapper.find('.zen-backdrop textarea').element).not.toBeNull();
@@ -92,32 +96,24 @@ describe('Markdown field component', () => {
previewLink = getPreviewLink(wrapper);
previewLink.trigger('click');
- wrapper.vm.$nextTick(() => {
+ return wrapper.vm.$nextTick(() => {
expect(wrapper.find('.md-preview-holder').element.textContent.trim()).toContain(
'Loading…',
);
});
});
- it('renders markdown preview', () => {
+ it('renders markdown preview and GFM', () => {
wrapper = createComponent();
- previewLink = getPreviewLink(wrapper);
- previewLink.trigger('click');
+ const renderGFMSpy = jest.spyOn($.fn, 'renderGFM');
- setTimeout(() => {
- expect(wrapper.find('.md-preview-holder').element.innerHTML).toContain(previewHTML);
- });
- });
-
- it('renders GFM with jQuery', () => {
- wrapper = createComponent();
previewLink = getPreviewLink(wrapper);
- jest.spyOn($.fn, 'renderGFM');
previewLink.trigger('click');
return axios.waitFor(markdownPreviewPath).then(() => {
expect(wrapper.find('.md-preview-holder').element.innerHTML).toContain(previewHTML);
+ expect(renderGFMSpy).toHaveBeenCalled();
});
});
@@ -176,7 +172,7 @@ describe('Markdown field component', () => {
const markdownButton = getMarkdownButton(wrapper);
markdownButton.trigger('click');
- wrapper.vm.$nextTick(() => {
+ return wrapper.vm.$nextTick(() => {
expect(textarea.value).toContain('**testing**');
});
});
@@ -188,8 +184,8 @@ describe('Markdown field component', () => {
const markdownButton = getAllMarkdownButtons(wrapper).wrappers[5];
markdownButton.trigger('click');
- wrapper.vm.$nextTick(() => {
- expect(textarea.value).toContain('* testing');
+ return wrapper.vm.$nextTick(() => {
+ expect(textarea.value).toContain('* testing');
});
});
@@ -200,7 +196,7 @@ describe('Markdown field component', () => {
const markdownButton = getAllMarkdownButtons(wrapper).wrappers[5];
markdownButton.trigger('click');
- wrapper.vm.$nextTick(() => {
+ return wrapper.vm.$nextTick(() => {
expect(textarea.value).toContain('* testing\n* 123');
});
});
diff --git a/spec/frontend/vue_shared/components/markdown/field_view_spec.js b/spec/frontend/vue_shared/components/markdown/field_view_spec.js
new file mode 100644
index 00000000000..80cf1f655c6
--- /dev/null
+++ b/spec/frontend/vue_shared/components/markdown/field_view_spec.js
@@ -0,0 +1,26 @@
+import $ from 'jquery';
+import { shallowMount } from '@vue/test-utils';
+
+import MarkdownFieldView from '~/vue_shared/components/markdown/field_view.vue';
+
+describe('Markdown Field View component', () => {
+ let renderGFMSpy;
+ let wrapper;
+
+ function createComponent() {
+ wrapper = shallowMount(MarkdownFieldView);
+ }
+
+ beforeEach(() => {
+ renderGFMSpy = jest.spyOn($.fn, 'renderGFM');
+ createComponent();
+ });
+
+ afterEach(() => {
+ wrapper.destroy();
+ });
+
+ it('processes rendering with GFM', () => {
+ expect(renderGFMSpy).toHaveBeenCalledTimes(1);
+ });
+});
diff --git a/spec/frontend/vue_shared/components/markdown/suggestions_spec.js b/spec/frontend/vue_shared/components/markdown/suggestions_spec.js
new file mode 100644
index 00000000000..34ccdf38b00
--- /dev/null
+++ b/spec/frontend/vue_shared/components/markdown/suggestions_spec.js
@@ -0,0 +1,102 @@
+import Vue from 'vue';
+import SuggestionsComponent from '~/vue_shared/components/markdown/suggestions.vue';
+
+const MOCK_DATA = {
+ suggestions: [
+ {
+ id: 1,
+ appliable: true,
+ applied: false,
+ current_user: {
+ can_apply: true,
+ },
+ diff_lines: [
+ {
+ can_receive_suggestion: false,
+ line_code: null,
+ meta_data: null,
+ new_line: null,
+ old_line: 5,
+ rich_text: '-test',
+ text: '-test',
+ type: 'old',
+ },
+ {
+ can_receive_suggestion: true,
+ line_code: null,
+ meta_data: null,
+ new_line: 5,
+ old_line: null,
+ rich_text: '+new test',
+ text: '+new test',
+ type: 'new',
+ },
+ ],
+ },
+ ],
+ noteHtml: `
+ <div class="suggestion">
+ <div class="line">-oldtest</div>
+ </div>
+ <div class="suggestion">
+ <div class="line">+newtest</div>
+ </div>
+ `,
+ isApplied: false,
+ helpPagePath: 'path_to_docs',
+};
+
+describe('Suggestion component', () => {
+ let vm;
+ let diffTable;
+
+ beforeEach(done => {
+ const Component = Vue.extend(SuggestionsComponent);
+
+ vm = new Component({
+ propsData: MOCK_DATA,
+ }).$mount();
+
+ diffTable = vm.generateDiff(0).$mount().$el;
+
+ jest.spyOn(vm, 'renderSuggestions').mockImplementation(() => {});
+ vm.renderSuggestions();
+ Vue.nextTick(done);
+ });
+
+ describe('mounted', () => {
+ it('renders a flash container', () => {
+ expect(vm.$el.querySelector('.js-suggestions-flash')).not.toBeNull();
+ });
+
+ it('renders a container for suggestions', () => {
+ expect(vm.$refs.container).not.toBeNull();
+ });
+
+ it('renders suggestions', () => {
+ expect(vm.renderSuggestions).toHaveBeenCalled();
+ expect(vm.$el.innerHTML.includes('oldtest')).toBe(true);
+ expect(vm.$el.innerHTML.includes('newtest')).toBe(true);
+ });
+ });
+
+ describe('generateDiff', () => {
+ it('generates a diff table', () => {
+ expect(diffTable.querySelector('.md-suggestion-diff')).not.toBeNull();
+ });
+
+ it('generates a diff table that contains contents the suggested lines', () => {
+ MOCK_DATA.suggestions[0].diff_lines.forEach(line => {
+ const text = line.text.substring(1);
+
+ expect(diffTable.innerHTML.includes(text)).toBe(true);
+ });
+ });
+
+ it('generates a diff table with the correct line number for each suggested line', () => {
+ const lines = diffTable.querySelectorAll('.old_line');
+
+ expect(parseInt([...lines][0].innerHTML, 10)).toBe(5);
+ });
+ });
+});
diff --git a/spec/frontend/vue_shared/components/markdown/toolbar_spec.js b/spec/frontend/vue_shared/components/markdown/toolbar_spec.js
new file mode 100644
index 00000000000..e7c31014bfc
--- /dev/null
+++ b/spec/frontend/vue_shared/components/markdown/toolbar_spec.js
@@ -0,0 +1,35 @@
+import Vue from 'vue';
+import mountComponent from 'helpers/vue_mount_component_helper';
+import toolbar from '~/vue_shared/components/markdown/toolbar.vue';
+
+describe('toolbar', () => {
+ let vm;
+ const Toolbar = Vue.extend(toolbar);
+ const props = {
+ markdownDocsPath: '',
+ };
+
+ afterEach(() => {
+ vm.$destroy();
+ });
+
+ describe('user can attach file', () => {
+ beforeEach(() => {
+ vm = mountComponent(Toolbar, props);
+ });
+
+ it('should render uploading-container', () => {
+ expect(vm.$el.querySelector('.uploading-container')).not.toBeNull();
+ });
+ });
+
+ describe('user cannot attach file', () => {
+ beforeEach(() => {
+ vm = mountComponent(Toolbar, { ...props, canAttachFile: false });
+ });
+
+ it('should not render uploading-container', () => {
+ expect(vm.$el.querySelector('.uploading-container')).toBeNull();
+ });
+ });
+});
diff --git a/spec/frontend/vue_shared/components/navigation_tabs_spec.js b/spec/frontend/vue_shared/components/navigation_tabs_spec.js
new file mode 100644
index 00000000000..561456d614e
--- /dev/null
+++ b/spec/frontend/vue_shared/components/navigation_tabs_spec.js
@@ -0,0 +1,64 @@
+import Vue from 'vue';
+import mountComponent from 'helpers/vue_mount_component_helper';
+import navigationTabs from '~/vue_shared/components/navigation_tabs.vue';
+
+describe('navigation tabs component', () => {
+ let vm;
+ let Component;
+ let data;
+
+ beforeEach(() => {
+ data = [
+ {
+ name: 'All',
+ scope: 'all',
+ count: 1,
+ isActive: true,
+ },
+ {
+ name: 'Pending',
+ scope: 'pending',
+ count: 0,
+ isActive: false,
+ },
+ {
+ name: 'Running',
+ scope: 'running',
+ isActive: false,
+ },
+ ];
+
+ Component = Vue.extend(navigationTabs);
+ vm = mountComponent(Component, { tabs: data, scope: 'pipelines' });
+ });
+
+ afterEach(() => {
+ vm.$destroy();
+ });
+
+ it('should render tabs', () => {
+ expect(vm.$el.querySelectorAll('li').length).toEqual(data.length);
+ });
+
+ it('should render active tab', () => {
+ expect(vm.$el.querySelector('.active .js-pipelines-tab-all')).toBeDefined();
+ });
+
+ it('should render badge', () => {
+ expect(vm.$el.querySelector('.js-pipelines-tab-all .badge').textContent.trim()).toEqual('1');
+ expect(vm.$el.querySelector('.js-pipelines-tab-pending .badge').textContent.trim()).toEqual(
+ '0',
+ );
+ });
+
+ it('should not render badge', () => {
+ expect(vm.$el.querySelector('.js-pipelines-tab-running .badge')).toEqual(null);
+ });
+
+ it('should trigger onTabClick', () => {
+ jest.spyOn(vm, '$emit').mockImplementation(() => {});
+ vm.$el.querySelector('.js-pipelines-tab-pending').click();
+
+ expect(vm.$emit).toHaveBeenCalledWith('onChangeTab', 'pending');
+ });
+});
diff --git a/spec/frontend/vue_shared/components/pikaday_spec.js b/spec/frontend/vue_shared/components/pikaday_spec.js
new file mode 100644
index 00000000000..867bf88ff50
--- /dev/null
+++ b/spec/frontend/vue_shared/components/pikaday_spec.js
@@ -0,0 +1,30 @@
+import Vue from 'vue';
+import mountComponent from 'helpers/vue_mount_component_helper';
+import datePicker from '~/vue_shared/components/pikaday.vue';
+
+describe('datePicker', () => {
+ let vm;
+ beforeEach(() => {
+ const DatePicker = Vue.extend(datePicker);
+ vm = mountComponent(DatePicker, {
+ label: 'label',
+ });
+ });
+
+ it('should render label text', () => {
+ expect(vm.$el.querySelector('.dropdown-toggle-text').innerText.trim()).toEqual('label');
+ });
+
+ it('should show calendar', () => {
+ expect(vm.$el.querySelector('.pika-single')).toBeDefined();
+ });
+
+ it('should toggle when dropdown is clicked', () => {
+ const hidePicker = jest.fn();
+ vm.$on('hidePicker', hidePicker);
+
+ vm.$el.querySelector('.dropdown-menu-toggle').click();
+
+ expect(hidePicker).toHaveBeenCalled();
+ });
+});
diff --git a/spec/frontend/vue_shared/components/project_avatar/default_spec.js b/spec/frontend/vue_shared/components/project_avatar/default_spec.js
new file mode 100644
index 00000000000..090f8b69213
--- /dev/null
+++ b/spec/frontend/vue_shared/components/project_avatar/default_spec.js
@@ -0,0 +1,58 @@
+import Vue from 'vue';
+import mountComponent from 'helpers/vue_mount_component_helper';
+import { projectData } from 'jest/ide/mock_data';
+import { TEST_HOST } from 'spec/test_constants';
+import { getFirstCharacterCapitalized } from '~/lib/utils/text_utility';
+import ProjectAvatarDefault from '~/vue_shared/components/project_avatar/default.vue';
+
+describe('ProjectAvatarDefault component', () => {
+ const Component = Vue.extend(ProjectAvatarDefault);
+ let vm;
+
+ beforeEach(() => {
+ vm = mountComponent(Component, {
+ project: projectData,
+ });
+ });
+
+ afterEach(() => {
+ vm.$destroy();
+ });
+
+ it('renders identicon if project has no avatar_url', done => {
+ const expectedText = getFirstCharacterCapitalized(projectData.name);
+
+ vm.project = {
+ ...vm.project,
+ avatar_url: null,
+ };
+
+ vm.$nextTick()
+ .then(() => {
+ const identiconEl = vm.$el.querySelector('.identicon');
+
+ expect(identiconEl).not.toBe(null);
+ expect(identiconEl.textContent.trim()).toEqual(expectedText);
+ })
+ .then(done)
+ .catch(done.fail);
+ });
+
+ it('renders avatar image if project has avatar_url', done => {
+ const avatarUrl = `${TEST_HOST}/images/home/nasa.svg`;
+
+ vm.project = {
+ ...vm.project,
+ avatar_url: avatarUrl,
+ };
+
+ vm.$nextTick()
+ .then(() => {
+ expect(vm.$el.querySelector('.avatar')).not.toBeNull();
+ expect(vm.$el.querySelector('.identicon')).toBeNull();
+ expect(vm.$el.querySelector('img')).toHaveAttr('src', avatarUrl);
+ })
+ .then(done)
+ .catch(done.fail);
+ });
+});
diff --git a/spec/frontend/vue_shared/components/project_selector/project_list_item_spec.js b/spec/frontend/vue_shared/components/project_selector/project_list_item_spec.js
new file mode 100644
index 00000000000..eb1d9e93634
--- /dev/null
+++ b/spec/frontend/vue_shared/components/project_selector/project_list_item_spec.js
@@ -0,0 +1,109 @@
+import { shallowMount, createLocalVue } from '@vue/test-utils';
+import { trimText } from 'helpers/text_helper';
+import ProjectListItem from '~/vue_shared/components/project_selector/project_list_item.vue';
+
+const localVue = createLocalVue();
+
+describe('ProjectListItem component', () => {
+ const Component = localVue.extend(ProjectListItem);
+ let wrapper;
+ let vm;
+ let options;
+
+ const project = getJSONFixture('static/projects.json')[0];
+
+ beforeEach(() => {
+ options = {
+ propsData: {
+ project,
+ selected: false,
+ },
+ localVue,
+ };
+ });
+
+ afterEach(() => {
+ wrapper.vm.$destroy();
+ });
+
+ it('does not render a check mark icon if selected === false', () => {
+ wrapper = shallowMount(Component, options);
+
+ expect(wrapper.contains('.js-selected-icon.js-unselected')).toBe(true);
+ });
+
+ it('renders a check mark icon if selected === true', () => {
+ options.propsData.selected = true;
+
+ wrapper = shallowMount(Component, options);
+
+ expect(wrapper.contains('.js-selected-icon.js-selected')).toBe(true);
+ });
+
+ it(`emits a "clicked" event when clicked`, () => {
+ wrapper = shallowMount(Component, options);
+ ({ vm } = wrapper);
+
+ jest.spyOn(vm, '$emit').mockImplementation(() => {});
+ wrapper.vm.onClick();
+
+ expect(wrapper.vm.$emit).toHaveBeenCalledWith('click');
+ });
+
+ it(`renders the project avatar`, () => {
+ wrapper = shallowMount(Component, options);
+
+ expect(wrapper.contains('.js-project-avatar')).toBe(true);
+ });
+
+ it(`renders a simple namespace name with a trailing slash`, () => {
+ options.propsData.project.name_with_namespace = 'a / b';
+
+ wrapper = shallowMount(Component, options);
+ const renderedNamespace = trimText(wrapper.find('.js-project-namespace').text());
+
+ expect(renderedNamespace).toBe('a /');
+ });
+
+ it(`renders a properly truncated namespace with a trailing slash`, () => {
+ options.propsData.project.name_with_namespace = 'a / b / c / d / e / f';
+
+ wrapper = shallowMount(Component, options);
+ const renderedNamespace = trimText(wrapper.find('.js-project-namespace').text());
+
+ expect(renderedNamespace).toBe('a / ... / e /');
+ });
+
+ it(`renders the project name`, () => {
+ options.propsData.project.name = 'my-test-project';
+
+ wrapper = shallowMount(Component, options);
+ const renderedName = trimText(wrapper.find('.js-project-name').text());
+
+ expect(renderedName).toBe('my-test-project');
+ });
+
+ it(`renders the project name with highlighting in the case of a search query match`, () => {
+ options.propsData.project.name = 'my-test-project';
+ options.propsData.matcher = 'pro';
+
+ wrapper = shallowMount(Component, options);
+ const renderedName = trimText(wrapper.find('.js-project-name').html());
+ const expected = 'my-test-<b>p</b><b>r</b><b>o</b>ject';
+
+ expect(renderedName).toContain(expected);
+ });
+
+ it('prevents search query and project name XSS', () => {
+ const alertSpy = jest.spyOn(window, 'alert');
+ options.propsData.project.name = "my-xss-pro<script>alert('XSS');</script>ject";
+ options.propsData.matcher = "pro<script>alert('XSS');</script>";
+
+ wrapper = shallowMount(Component, options);
+ const renderedName = trimText(wrapper.find('.js-project-name').html());
+ const expected = 'my-xss-project';
+
+ expect(renderedName).toContain(expected);
+ expect(alertSpy).not.toHaveBeenCalled();
+ });
+});
diff --git a/spec/frontend/vue_shared/components/project_selector/project_selector_spec.js b/spec/frontend/vue_shared/components/project_selector/project_selector_spec.js
new file mode 100644
index 00000000000..29bced394dc
--- /dev/null
+++ b/spec/frontend/vue_shared/components/project_selector/project_selector_spec.js
@@ -0,0 +1,112 @@
+import Vue from 'vue';
+import { head } from 'lodash';
+
+import { GlSearchBoxByType, GlInfiniteScroll } from '@gitlab/ui';
+import { mount, createLocalVue } from '@vue/test-utils';
+import { trimText } from 'helpers/text_helper';
+import ProjectListItem from '~/vue_shared/components/project_selector/project_list_item.vue';
+import ProjectSelector from '~/vue_shared/components/project_selector/project_selector.vue';
+
+const localVue = createLocalVue();
+
+describe('ProjectSelector component', () => {
+ let wrapper;
+ let vm;
+ const allProjects = getJSONFixture('static/projects.json');
+ const searchResults = allProjects.slice(0, 5);
+ let selected = [];
+ selected = selected.concat(allProjects.slice(0, 3)).concat(allProjects.slice(5, 8));
+
+ const findSearchInput = () => wrapper.find(GlSearchBoxByType).find('input');
+
+ beforeEach(() => {
+ wrapper = mount(Vue.extend(ProjectSelector), {
+ localVue,
+ propsData: {
+ projectSearchResults: searchResults,
+ selectedProjects: selected,
+ showNoResultsMessage: false,
+ showMinimumSearchQueryMessage: false,
+ showLoadingIndicator: false,
+ showSearchErrorMessage: false,
+ },
+ attachToDocument: true,
+ });
+
+ ({ vm } = wrapper);
+ });
+
+ afterEach(() => {
+ vm.$destroy();
+ });
+
+ it('renders the search results', () => {
+ expect(wrapper.findAll('.js-project-list-item').length).toBe(5);
+ });
+
+ it(`triggers a search when the search input value changes`, () => {
+ jest.spyOn(vm, '$emit').mockImplementation(() => {});
+ const query = 'my test query!';
+ const searchInput = findSearchInput();
+
+ searchInput.setValue(query);
+ searchInput.trigger('input');
+
+ expect(vm.$emit).toHaveBeenCalledWith('searched', query);
+ });
+
+ it(`includes a placeholder in the search box`, () => {
+ const searchInput = findSearchInput();
+
+ expect(searchInput.attributes('placeholder')).toBe('Search your projects');
+ });
+
+ it(`triggers a "bottomReached" event when user has scrolled to the bottom of the list`, () => {
+ jest.spyOn(vm, '$emit').mockImplementation(() => {});
+ wrapper.find(GlInfiniteScroll).vm.$emit('bottomReached');
+
+ expect(vm.$emit).toHaveBeenCalledWith('bottomReached');
+ });
+
+ it(`triggers a "projectClicked" event when a project is clicked`, () => {
+ jest.spyOn(vm, '$emit').mockImplementation(() => {});
+ wrapper.find(ProjectListItem).vm.$emit('click', head(searchResults));
+
+ expect(vm.$emit).toHaveBeenCalledWith('projectClicked', head(searchResults));
+ });
+
+ it(`shows a "no results" message if showNoResultsMessage === true`, () => {
+ wrapper.setProps({ showNoResultsMessage: true });
+
+ return vm.$nextTick().then(() => {
+ const noResultsEl = wrapper.find('.js-no-results-message');
+
+ expect(noResultsEl.exists()).toBe(true);
+ expect(trimText(noResultsEl.text())).toEqual('Sorry, no projects matched your search');
+ });
+ });
+
+ it(`shows a "minimum search query" message if showMinimumSearchQueryMessage === true`, () => {
+ wrapper.setProps({ showMinimumSearchQueryMessage: true });
+
+ return vm.$nextTick().then(() => {
+ const minimumSearchEl = wrapper.find('.js-minimum-search-query-message');
+
+ expect(minimumSearchEl.exists()).toBe(true);
+ expect(trimText(minimumSearchEl.text())).toEqual('Enter at least three characters to search');
+ });
+ });
+
+ it(`shows a error message if showSearchErrorMessage === true`, () => {
+ wrapper.setProps({ showSearchErrorMessage: true });
+
+ return vm.$nextTick().then(() => {
+ const errorMessageEl = wrapper.find('.js-search-error-message');
+
+ expect(errorMessageEl.exists()).toBe(true);
+ expect(trimText(errorMessageEl.text())).toEqual(
+ 'Something went wrong, unable to search projects',
+ );
+ });
+ });
+});
diff --git a/spec/frontend/vue_shared/components/rich_content_editor/rich_content_editor_spec.js b/spec/frontend/vue_shared/components/rich_content_editor/rich_content_editor_spec.js
new file mode 100644
index 00000000000..549d89171c6
--- /dev/null
+++ b/spec/frontend/vue_shared/components/rich_content_editor/rich_content_editor_spec.js
@@ -0,0 +1,59 @@
+import { shallowMount } from '@vue/test-utils';
+import RichContentEditor from '~/vue_shared/components/rich_content_editor/rich_content_editor.vue';
+import {
+ EDITOR_OPTIONS,
+ EDITOR_TYPES,
+ EDITOR_HEIGHT,
+ EDITOR_PREVIEW_STYLE,
+} from '~/vue_shared/components/rich_content_editor/constants';
+
+describe('Rich Content Editor', () => {
+ let wrapper;
+
+ const value = '## Some Markdown';
+ const findEditor = () => wrapper.find({ ref: 'editor' });
+
+ beforeEach(() => {
+ wrapper = shallowMount(RichContentEditor, {
+ propsData: { value },
+ });
+ });
+
+ describe('when content is loaded', () => {
+ it('renders an editor', () => {
+ expect(findEditor().exists()).toBe(true);
+ });
+
+ it('renders the correct content', () => {
+ expect(findEditor().props().initialValue).toBe(value);
+ });
+
+ it('provides the correct editor options', () => {
+ expect(findEditor().props().options).toEqual(EDITOR_OPTIONS);
+ });
+
+ it('has the correct preview style', () => {
+ expect(findEditor().props().previewStyle).toBe(EDITOR_PREVIEW_STYLE);
+ });
+
+ it('has the correct initial edit type', () => {
+ expect(findEditor().props().initialEditType).toBe(EDITOR_TYPES.wysiwyg);
+ });
+
+ it('has the correct height', () => {
+ expect(findEditor().props().height).toBe(EDITOR_HEIGHT);
+ });
+ });
+
+ describe('when content is changed', () => {
+ it('emits an input event with the changed content', () => {
+ const changedMarkdown = '## Changed Markdown';
+ const getMarkdownMock = jest.fn().mockReturnValueOnce(changedMarkdown);
+
+ findEditor().setMethods({ invoke: getMarkdownMock });
+ findEditor().vm.$emit('change');
+
+ expect(wrapper.emitted().input[0][0]).toBe(changedMarkdown);
+ });
+ });
+});
diff --git a/spec/frontend/vue_shared/components/rich_content_editor/toolbar_item_spec.js b/spec/frontend/vue_shared/components/rich_content_editor/toolbar_item_spec.js
new file mode 100644
index 00000000000..8545c43dc1e
--- /dev/null
+++ b/spec/frontend/vue_shared/components/rich_content_editor/toolbar_item_spec.js
@@ -0,0 +1,44 @@
+import { shallowMount } from '@vue/test-utils';
+import { GlIcon } from '@gitlab/ui';
+import ToolbarItem from '~/vue_shared/components/rich_content_editor/toolbar_item.vue';
+
+describe('Toolbar Item', () => {
+ let wrapper;
+
+ const findIcon = () => wrapper.find(GlIcon);
+ const findButton = () => wrapper.find('button');
+
+ const buildWrapper = propsData => {
+ wrapper = shallowMount(ToolbarItem, { propsData });
+ };
+
+ describe.each`
+ icon
+ ${'heading'}
+ ${'bold'}
+ ${'italic'}
+ ${'strikethrough'}
+ ${'quote'}
+ ${'link'}
+ ${'doc-code'}
+ ${'list-bulleted'}
+ ${'list-numbered'}
+ ${'list-task'}
+ ${'list-indent'}
+ ${'list-outdent'}
+ ${'dash'}
+ ${'table'}
+ ${'code'}
+ `('toolbar item component', ({ icon }) => {
+ beforeEach(() => buildWrapper({ icon }));
+
+ it('renders a toolbar button', () => {
+ expect(findButton().exists()).toBe(true);
+ });
+
+ it(`renders the ${icon} icon`, () => {
+ expect(findIcon().exists()).toBe(true);
+ expect(findIcon().props().name).toBe(icon);
+ });
+ });
+});
diff --git a/spec/frontend/vue_shared/components/rich_content_editor/toolbar_service_spec.js b/spec/frontend/vue_shared/components/rich_content_editor/toolbar_service_spec.js
new file mode 100644
index 00000000000..7605cc6a22c
--- /dev/null
+++ b/spec/frontend/vue_shared/components/rich_content_editor/toolbar_service_spec.js
@@ -0,0 +1,29 @@
+import { generateToolbarItem } from '~/vue_shared/components/rich_content_editor/toolbar_service';
+
+describe('Toolbar Service', () => {
+ const config = {
+ icon: 'bold',
+ command: 'some-command',
+ tooltip: 'Some Tooltip',
+ event: 'some-event',
+ };
+ const generatedItem = generateToolbarItem(config);
+
+ it('generates the correct command', () => {
+ expect(generatedItem.options.command).toBe(config.command);
+ });
+
+ it('generates the correct tooltip', () => {
+ expect(generatedItem.options.tooltip).toBe(config.tooltip);
+ });
+
+ it('generates the correct event', () => {
+ expect(generatedItem.options.event).toBe(config.event);
+ });
+
+ it('generates a divider when isDivider is set to true', () => {
+ const isDivider = true;
+
+ expect(generateToolbarItem({ isDivider })).toBe('divider');
+ });
+});
diff --git a/spec/frontend/vue_shared/components/sidebar/labels_select/base_spec.js b/spec/frontend/vue_shared/components/sidebar/labels_select/base_spec.js
index d90fafb6bf7..9db86fa775f 100644
--- a/spec/frontend/vue_shared/components/sidebar/labels_select/base_spec.js
+++ b/spec/frontend/vue_shared/components/sidebar/labels_select/base_spec.js
@@ -4,10 +4,7 @@ import { shallowMount } from '@vue/test-utils';
import LabelsSelect from '~/labels_select';
import BaseComponent from '~/vue_shared/components/sidebar/labels_select/base.vue';
-import {
- mockConfig,
- mockLabels,
-} from '../../../../../javascripts/vue_shared/components/sidebar/labels_select/mock_data';
+import { mockConfig, mockLabels } from './mock_data';
const createComponent = (config = mockConfig) =>
shallowMount(BaseComponent, {
diff --git a/spec/frontend/vue_shared/components/sidebar/labels_select/dropdown_button_spec.js b/spec/frontend/vue_shared/components/sidebar/labels_select/dropdown_button_spec.js
index e2e11c94c0d..d02d924bd2b 100644
--- a/spec/frontend/vue_shared/components/sidebar/labels_select/dropdown_button_spec.js
+++ b/spec/frontend/vue_shared/components/sidebar/labels_select/dropdown_button_spec.js
@@ -3,16 +3,14 @@ import Vue from 'vue';
import mountComponent from 'helpers/vue_mount_component_helper';
import dropdownButtonComponent from '~/vue_shared/components/sidebar/labels_select/dropdown_button.vue';
-import {
- mockConfig,
- mockLabels,
-} from '../../../../../javascripts/vue_shared/components/sidebar/labels_select/mock_data';
+import { mockConfig, mockLabels } from './mock_data';
-const componentConfig = Object.assign({}, mockConfig, {
+const componentConfig = {
+ ...mockConfig,
fieldName: 'label_id[]',
labels: mockLabels,
showExtraOptions: false,
-});
+};
const createComponent = (config = componentConfig) => {
const Component = Vue.extend(dropdownButtonComponent);
@@ -34,7 +32,7 @@ describe('DropdownButtonComponent', () => {
describe('computed', () => {
describe('dropdownToggleText', () => {
it('returns text as `Label` when `labels` prop is empty array', () => {
- const mockEmptyLabels = Object.assign({}, componentConfig, { labels: [] });
+ const mockEmptyLabels = { ...componentConfig, labels: [] };
const vmEmptyLabels = createComponent(mockEmptyLabels);
expect(vmEmptyLabels.dropdownToggleText).toBe('Label');
@@ -42,9 +40,7 @@ describe('DropdownButtonComponent', () => {
});
it('returns first label name with remaining label count when `labels` prop has more than one item', () => {
- const mockMoreLabels = Object.assign({}, componentConfig, {
- labels: mockLabels.concat(mockLabels),
- });
+ const mockMoreLabels = { ...componentConfig, labels: mockLabels.concat(mockLabels) };
const vmMoreLabels = createComponent(mockMoreLabels);
expect(vmMoreLabels.dropdownToggleText).toBe(
@@ -54,9 +50,7 @@ describe('DropdownButtonComponent', () => {
});
it('returns first label name when `labels` prop has only one item present', () => {
- const singleLabel = Object.assign({}, componentConfig, {
- labels: [mockLabels[0]],
- });
+ const singleLabel = { ...componentConfig, labels: [mockLabels[0]] };
const vmSingleLabel = createComponent(singleLabel);
expect(vmSingleLabel.dropdownToggleText).toBe(mockLabels[0].title);
diff --git a/spec/frontend/vue_shared/components/sidebar/labels_select/dropdown_create_label_spec.js b/spec/frontend/vue_shared/components/sidebar/labels_select/dropdown_create_label_spec.js
index d0299523137..edec3b138b3 100644
--- a/spec/frontend/vue_shared/components/sidebar/labels_select/dropdown_create_label_spec.js
+++ b/spec/frontend/vue_shared/components/sidebar/labels_select/dropdown_create_label_spec.js
@@ -3,7 +3,7 @@ import Vue from 'vue';
import mountComponent from 'helpers/vue_mount_component_helper';
import dropdownCreateLabelComponent from '~/vue_shared/components/sidebar/labels_select/dropdown_create_label.vue';
-import { mockSuggestedColors } from '../../../../../javascripts/vue_shared/components/sidebar/labels_select/mock_data';
+import { mockSuggestedColors } from './mock_data';
const createComponent = headerTitle => {
const Component = Vue.extend(dropdownCreateLabelComponent);
diff --git a/spec/frontend/vue_shared/components/sidebar/labels_select/dropdown_footer_spec.js b/spec/frontend/vue_shared/components/sidebar/labels_select/dropdown_footer_spec.js
index 784bbaf8e6a..7e9e242a4f5 100644
--- a/spec/frontend/vue_shared/components/sidebar/labels_select/dropdown_footer_spec.js
+++ b/spec/frontend/vue_shared/components/sidebar/labels_select/dropdown_footer_spec.js
@@ -3,7 +3,7 @@ import Vue from 'vue';
import mountComponent from 'helpers/vue_mount_component_helper';
import dropdownFooterComponent from '~/vue_shared/components/sidebar/labels_select/dropdown_footer.vue';
-import { mockConfig } from '../../../../../javascripts/vue_shared/components/sidebar/labels_select/mock_data';
+import { mockConfig } from './mock_data';
const createComponent = (
labelsWebUrl = mockConfig.labelsWebUrl,
diff --git a/spec/frontend/vue_shared/components/sidebar/labels_select/dropdown_value_collapsed_spec.js b/spec/frontend/vue_shared/components/sidebar/labels_select/dropdown_value_collapsed_spec.js
index 887c04268d1..e09f0006359 100644
--- a/spec/frontend/vue_shared/components/sidebar/labels_select/dropdown_value_collapsed_spec.js
+++ b/spec/frontend/vue_shared/components/sidebar/labels_select/dropdown_value_collapsed_spec.js
@@ -3,7 +3,7 @@ import Vue from 'vue';
import mountComponent from 'helpers/vue_mount_component_helper';
import dropdownValueCollapsedComponent from '~/vue_shared/components/sidebar/labels_select/dropdown_value_collapsed.vue';
-import { mockLabels } from '../../../../../javascripts/vue_shared/components/sidebar/labels_select/mock_data';
+import { mockLabels } from './mock_data';
const createComponent = (labels = mockLabels) => {
const Component = Vue.extend(dropdownValueCollapsedComponent);
diff --git a/spec/frontend/vue_shared/components/sidebar/labels_select/dropdown_value_spec.js b/spec/frontend/vue_shared/components/sidebar/labels_select/dropdown_value_spec.js
index 06355c0dd65..c33cffb421d 100644
--- a/spec/frontend/vue_shared/components/sidebar/labels_select/dropdown_value_spec.js
+++ b/spec/frontend/vue_shared/components/sidebar/labels_select/dropdown_value_spec.js
@@ -2,10 +2,7 @@ import { mount } from '@vue/test-utils';
import DropdownValueComponent from '~/vue_shared/components/sidebar/labels_select/dropdown_value.vue';
import { GlLabel } from '@gitlab/ui';
-import {
- mockConfig,
- mockLabels,
-} from '../../../../../javascripts/vue_shared/components/sidebar/labels_select/mock_data';
+import { mockConfig, mockLabels } from './mock_data';
const createComponent = (
labels = mockLabels,
diff --git a/spec/javascripts/vue_shared/components/sidebar/labels_select/mock_data.js b/spec/frontend/vue_shared/components/sidebar/labels_select/mock_data.js
index 6564c012e67..6564c012e67 100644
--- a/spec/javascripts/vue_shared/components/sidebar/labels_select/mock_data.js
+++ b/spec/frontend/vue_shared/components/sidebar/labels_select/mock_data.js
diff --git a/spec/frontend/vue_shared/components/sidebar/labels_select_vue/dropdown_button_spec.js b/spec/frontend/vue_shared/components/sidebar/labels_select_vue/dropdown_button_spec.js
index e2d31a41e82..214eb239432 100644
--- a/spec/frontend/vue_shared/components/sidebar/labels_select_vue/dropdown_button_spec.js
+++ b/spec/frontend/vue_shared/components/sidebar/labels_select_vue/dropdown_button_spec.js
@@ -33,9 +33,32 @@ describe('DropdownButton', () => {
wrapper.destroy();
});
+ describe('methods', () => {
+ describe('handleButtonClick', () => {
+ it('calls action `toggleDropdownContents` and stops event propagation when `state.variant` is "standalone"', () => {
+ const event = {
+ stopPropagation: jest.fn(),
+ };
+ wrapper = createComponent({
+ ...mockConfig,
+ variant: 'standalone',
+ });
+
+ jest.spyOn(wrapper.vm, 'toggleDropdownContents');
+
+ wrapper.vm.handleButtonClick(event);
+
+ expect(wrapper.vm.toggleDropdownContents).toHaveBeenCalled();
+ expect(event.stopPropagation).toHaveBeenCalled();
+
+ wrapper.destroy();
+ });
+ });
+ });
+
describe('template', () => {
it('renders component container element', () => {
- expect(wrapper.is('gl-deprecated-button-stub')).toBe(true);
+ expect(wrapper.is('gl-button-stub')).toBe(true);
});
it('renders button text element', () => {
diff --git a/spec/frontend/vue_shared/components/sidebar/labels_select_vue/dropdown_contents_create_view_spec.js b/spec/frontend/vue_shared/components/sidebar/labels_select_vue/dropdown_contents_create_view_spec.js
index d7ca7ce30a9..04320a72be6 100644
--- a/spec/frontend/vue_shared/components/sidebar/labels_select_vue/dropdown_contents_create_view_spec.js
+++ b/spec/frontend/vue_shared/components/sidebar/labels_select_vue/dropdown_contents_create_view_spec.js
@@ -1,7 +1,7 @@
import Vuex from 'vuex';
import { shallowMount, createLocalVue } from '@vue/test-utils';
-import { GlDeprecatedButton, GlIcon, GlFormInput, GlLink, GlLoadingIcon } from '@gitlab/ui';
+import { GlButton, GlFormInput, GlLink, GlLoadingIcon } from '@gitlab/ui';
import DropdownContentsCreateView from '~/vue_shared/components/sidebar/labels_select_vue/dropdown_contents_create_view.vue';
import labelSelectModule from '~/vue_shared/components/sidebar/labels_select_vue/store';
@@ -127,12 +127,12 @@ describe('DropdownContentsCreateView', () => {
it('renders dropdown back button element', () => {
const backBtnEl = wrapper
.find('.dropdown-title')
- .findAll(GlDeprecatedButton)
+ .findAll(GlButton)
.at(0);
expect(backBtnEl.exists()).toBe(true);
expect(backBtnEl.attributes('aria-label')).toBe('Go back');
- expect(backBtnEl.find(GlIcon).props('name')).toBe('arrow-left');
+ expect(backBtnEl.props('icon')).toBe('arrow-left');
});
it('renders dropdown title element', () => {
@@ -145,12 +145,12 @@ describe('DropdownContentsCreateView', () => {
it('renders dropdown close button element', () => {
const closeBtnEl = wrapper
.find('.dropdown-title')
- .findAll(GlDeprecatedButton)
+ .findAll(GlButton)
.at(1);
expect(closeBtnEl.exists()).toBe(true);
expect(closeBtnEl.attributes('aria-label')).toBe('Close');
- expect(closeBtnEl.find(GlIcon).props('name')).toBe('close');
+ expect(closeBtnEl.props('icon')).toBe('close');
});
it('renders label title input element', () => {
@@ -192,7 +192,7 @@ describe('DropdownContentsCreateView', () => {
it('renders create button element', () => {
const createBtnEl = wrapper
.find('.dropdown-actions')
- .findAll(GlDeprecatedButton)
+ .findAll(GlButton)
.at(0);
expect(createBtnEl.exists()).toBe(true);
@@ -213,7 +213,7 @@ describe('DropdownContentsCreateView', () => {
it('renders cancel button element', () => {
const cancelBtnEl = wrapper
.find('.dropdown-actions')
- .findAll(GlDeprecatedButton)
+ .findAll(GlButton)
.at(1);
expect(cancelBtnEl.exists()).toBe(true);
diff --git a/spec/frontend/vue_shared/components/sidebar/labels_select_vue/dropdown_contents_labels_view_spec.js b/spec/frontend/vue_shared/components/sidebar/labels_select_vue/dropdown_contents_labels_view_spec.js
index 3e6dbdb7ecb..74c769f86a3 100644
--- a/spec/frontend/vue_shared/components/sidebar/labels_select_vue/dropdown_contents_labels_view_spec.js
+++ b/spec/frontend/vue_shared/components/sidebar/labels_select_vue/dropdown_contents_labels_view_spec.js
@@ -1,9 +1,10 @@
import Vuex from 'vuex';
import { shallowMount, createLocalVue } from '@vue/test-utils';
-import { GlDeprecatedButton, GlLoadingIcon, GlIcon, GlSearchBoxByType, GlLink } from '@gitlab/ui';
+import { GlButton, GlLoadingIcon, GlSearchBoxByType, GlLink } from '@gitlab/ui';
import { UP_KEY_CODE, DOWN_KEY_CODE, ENTER_KEY_CODE, ESC_KEY_CODE } from '~/lib/utils/keycodes';
import DropdownContentsLabelsView from '~/vue_shared/components/sidebar/labels_select_vue/dropdown_contents_labels_view.vue';
+import LabelItem from '~/vue_shared/components/sidebar/labels_select_vue/label_item.vue';
import defaultState from '~/vue_shared/components/sidebar/labels_select_vue/store/state';
import mutations from '~/vue_shared/components/sidebar/labels_select_vue/store/mutations';
@@ -41,13 +42,19 @@ const createComponent = (initialState = mockConfig) => {
describe('DropdownContentsLabelsView', () => {
let wrapper;
+ let wrapperStandalone;
beforeEach(() => {
wrapper = createComponent();
+ wrapperStandalone = createComponent({
+ ...mockConfig,
+ variant: 'standalone',
+ });
});
afterEach(() => {
wrapper.destroy();
+ wrapperStandalone.destroy();
});
describe('computed', () => {
@@ -72,16 +79,6 @@ describe('DropdownContentsLabelsView', () => {
});
describe('methods', () => {
- describe('getDropdownLabelBoxStyle', () => {
- it('returns an object containing `backgroundColor` based on provided `label` param', () => {
- expect(wrapper.vm.getDropdownLabelBoxStyle(mockRegularLabel)).toEqual(
- expect.objectContaining({
- backgroundColor: mockRegularLabel.color,
- }),
- );
- });
- });
-
describe('isLabelSelected', () => {
it('returns true when provided `label` param is one of the selected labels', () => {
expect(wrapper.vm.isLabelSelected(mockRegularLabel)).toBe(true);
@@ -165,13 +162,24 @@ describe('DropdownContentsLabelsView', () => {
});
describe('handleLabelClick', () => {
- it('calls action `updateSelectedLabels` with provided `label` param', () => {
+ beforeEach(() => {
jest.spyOn(wrapper.vm, 'updateSelectedLabels').mockImplementation();
+ });
+ it('calls action `updateSelectedLabels` with provided `label` param', () => {
wrapper.vm.handleLabelClick(mockRegularLabel);
expect(wrapper.vm.updateSelectedLabels).toHaveBeenCalledWith([mockRegularLabel]);
});
+
+ it('calls action `toggleDropdownContents` when `state.allowMultiselect` is false', () => {
+ jest.spyOn(wrapper.vm, 'toggleDropdownContents');
+ wrapper.vm.$store.state.allowMultiselect = false;
+
+ wrapper.vm.handleLabelClick(mockRegularLabel);
+
+ expect(wrapper.vm.toggleDropdownContents).toHaveBeenCalled();
+ });
});
});
@@ -198,12 +206,15 @@ describe('DropdownContentsLabelsView', () => {
expect(titleEl.text()).toBe('Assign labels');
});
+ it('does not render dropdown title element when `state.variant` is "standalone"', () => {
+ expect(wrapperStandalone.find('.dropdown-title').exists()).toBe(false);
+ });
+
it('renders dropdown close button element', () => {
- const closeButtonEl = wrapper.find('.dropdown-title').find(GlDeprecatedButton);
+ const closeButtonEl = wrapper.find('.dropdown-title').find(GlButton);
expect(closeButtonEl.exists()).toBe(true);
- expect(closeButtonEl.find(GlIcon).exists()).toBe(true);
- expect(closeButtonEl.find(GlIcon).props('name')).toBe('close');
+ expect(closeButtonEl.props('icon')).toBe('close');
});
it('renders label search input element', () => {
@@ -214,16 +225,7 @@ describe('DropdownContentsLabelsView', () => {
});
it('renders label elements for all labels', () => {
- const labelsEl = wrapper.findAll('.dropdown-content li');
- const labelItemEl = labelsEl.at(0).find(GlLink);
-
- expect(labelsEl.length).toBe(mockLabels.length);
- expect(labelItemEl.exists()).toBe(true);
- expect(labelItemEl.find(GlIcon).props('name')).toBe('mobile-issue-close');
- expect(labelItemEl.find('.dropdown-label-box').attributes('style')).toBe(
- 'background-color: rgb(186, 218, 85);',
- );
- expect(labelItemEl.find(GlLink).text()).toContain(mockLabels[0].title);
+ expect(wrapper.findAll(LabelItem)).toHaveLength(mockLabels.length);
});
it('renders label element with "is-focused" when value of `currentHighlightItem` is more than -1', () => {
@@ -233,9 +235,9 @@ describe('DropdownContentsLabelsView', () => {
return wrapper.vm.$nextTick(() => {
const labelsEl = wrapper.findAll('.dropdown-content li');
- const labelItemEl = labelsEl.at(0).find(GlLink);
+ const labelItemEl = labelsEl.at(0).find(LabelItem);
- expect(labelItemEl.attributes('class')).toContain('is-focused');
+ expect(labelItemEl.props('highlight')).toBe(true);
});
});
@@ -247,19 +249,42 @@ describe('DropdownContentsLabelsView', () => {
return wrapper.vm.$nextTick(() => {
const noMatchEl = wrapper.find('.dropdown-content li');
- expect(noMatchEl.exists()).toBe(true);
+ expect(noMatchEl.isVisible()).toBe(true);
expect(noMatchEl.text()).toContain('No matching results');
});
});
it('renders footer list items', () => {
- const createLabelBtn = wrapper.find('.dropdown-footer').find(GlDeprecatedButton);
- const manageLabelsLink = wrapper.find('.dropdown-footer').find(GlLink);
-
- expect(createLabelBtn.exists()).toBe(true);
- expect(createLabelBtn.text()).toBe('Create label');
+ const createLabelLink = wrapper
+ .find('.dropdown-footer')
+ .findAll(GlLink)
+ .at(0);
+ const manageLabelsLink = wrapper
+ .find('.dropdown-footer')
+ .findAll(GlLink)
+ .at(1);
+
+ expect(createLabelLink.exists()).toBe(true);
+ expect(createLabelLink.text()).toBe('Create label');
expect(manageLabelsLink.exists()).toBe(true);
expect(manageLabelsLink.text()).toBe('Manage labels');
});
+
+ it('does not render "Create label" footer link when `state.allowLabelCreate` is `false`', () => {
+ wrapper.vm.$store.state.allowLabelCreate = false;
+
+ return wrapper.vm.$nextTick(() => {
+ const createLabelLink = wrapper
+ .find('.dropdown-footer')
+ .findAll(GlLink)
+ .at(0);
+
+ expect(createLabelLink.text()).not.toBe('Create label');
+ });
+ });
+
+ it('does not render footer list items when `state.variant` is "standalone"', () => {
+ expect(wrapperStandalone.find('.dropdown-footer').exists()).toBe(false);
+ });
});
});
diff --git a/spec/frontend/vue_shared/components/sidebar/labels_select_vue/label_item_spec.js b/spec/frontend/vue_shared/components/sidebar/labels_select_vue/label_item_spec.js
new file mode 100644
index 00000000000..401d208da5c
--- /dev/null
+++ b/spec/frontend/vue_shared/components/sidebar/labels_select_vue/label_item_spec.js
@@ -0,0 +1,111 @@
+import { shallowMount } from '@vue/test-utils';
+
+import { GlIcon, GlLink } from '@gitlab/ui';
+import LabelItem from '~/vue_shared/components/sidebar/labels_select_vue/label_item.vue';
+import { mockRegularLabel } from './mock_data';
+
+const createComponent = ({ label = mockRegularLabel, highlight = true } = {}) =>
+ shallowMount(LabelItem, {
+ propsData: {
+ label,
+ highlight,
+ },
+ });
+
+describe('LabelItem', () => {
+ let wrapper;
+
+ beforeEach(() => {
+ wrapper = createComponent();
+ });
+
+ afterEach(() => {
+ wrapper.destroy();
+ });
+
+ describe('computed', () => {
+ describe('labelBoxStyle', () => {
+ it('returns an object containing `backgroundColor` based on `label` prop', () => {
+ expect(wrapper.vm.labelBoxStyle).toEqual(
+ expect.objectContaining({
+ backgroundColor: mockRegularLabel.color,
+ }),
+ );
+ });
+ });
+ });
+
+ describe('methods', () => {
+ describe('handleClick', () => {
+ it('sets value of `isSet` data prop to opposite of its current value', () => {
+ wrapper.setData({
+ isSet: true,
+ });
+
+ wrapper.vm.handleClick();
+ expect(wrapper.vm.isSet).toBe(false);
+ wrapper.vm.handleClick();
+ expect(wrapper.vm.isSet).toBe(true);
+ });
+
+ it('emits event `clickLabel` on component with `label` prop as param', () => {
+ wrapper.vm.handleClick();
+
+ expect(wrapper.emitted('clickLabel')).toBeTruthy();
+ expect(wrapper.emitted('clickLabel')[0]).toEqual([mockRegularLabel]);
+ });
+ });
+ });
+
+ describe('template', () => {
+ it('renders gl-link component', () => {
+ expect(wrapper.find(GlLink).exists()).toBe(true);
+ });
+
+ it('renders gl-link component with class `is-focused` when `highlight` prop is true', () => {
+ wrapper.setProps({
+ highlight: true,
+ });
+
+ return wrapper.vm.$nextTick(() => {
+ expect(wrapper.find(GlLink).classes()).toContain('is-focused');
+ });
+ });
+
+ it('renders visible gl-icon component when `isSet` prop is true', () => {
+ wrapper.setData({
+ isSet: true,
+ });
+
+ return wrapper.vm.$nextTick(() => {
+ const iconEl = wrapper.find(GlIcon);
+
+ expect(iconEl.isVisible()).toBe(true);
+ expect(iconEl.props('name')).toBe('mobile-issue-close');
+ });
+ });
+
+ it('renders visible span element as placeholder instead of gl-icon when `isSet` prop is false', () => {
+ wrapper.setData({
+ isSet: false,
+ });
+
+ return wrapper.vm.$nextTick(() => {
+ const placeholderEl = wrapper.find('[data-testid="no-icon"]');
+
+ expect(placeholderEl.isVisible()).toBe(true);
+ });
+ });
+
+ it('renders label color element', () => {
+ const colorEl = wrapper.find('[data-testid="label-color-box"]');
+
+ expect(colorEl.exists()).toBe(true);
+ expect(colorEl.attributes('style')).toBe('background-color: rgb(186, 218, 85);');
+ });
+
+ it('renders label title', () => {
+ expect(wrapper.text()).toContain(mockRegularLabel.title);
+ });
+ });
+});
diff --git a/spec/frontend/vue_shared/components/sidebar/labels_select_vue/labels_select_root_spec.js b/spec/frontend/vue_shared/components/sidebar/labels_select_vue/labels_select_root_spec.js
index 126fd5438c4..ee4e9090e5d 100644
--- a/spec/frontend/vue_shared/components/sidebar/labels_select_vue/labels_select_root_spec.js
+++ b/spec/frontend/vue_shared/components/sidebar/labels_select_vue/labels_select_root_spec.js
@@ -89,6 +89,19 @@ describe('LabelsSelectRoot', () => {
expect(wrapper.attributes('class')).toContain('labels-select-wrapper position-relative');
});
+ it('renders component root element with CSS class `is-standalone` when `state.variant` is "standalone"', () => {
+ const wrapperStandalone = createComponent({
+ ...mockConfig,
+ variant: 'standalone',
+ });
+
+ return wrapperStandalone.vm.$nextTick(() => {
+ expect(wrapperStandalone.classes()).toContain('is-standalone');
+
+ wrapperStandalone.destroy();
+ });
+ });
+
it('renders `dropdown-value-collapsed` component when `allowLabelCreate` prop is `true`', () => {
expect(wrapper.find(DropdownValueCollapsed).exists()).toBe(true);
});
@@ -101,13 +114,16 @@ describe('LabelsSelectRoot', () => {
const wrapperDropdownValue = createComponent(mockConfig, {
default: 'None',
});
+ wrapperDropdownValue.vm.$store.state.showDropdownButton = false;
- const valueComp = wrapperDropdownValue.find(DropdownValue);
+ return wrapperDropdownValue.vm.$nextTick(() => {
+ const valueComp = wrapperDropdownValue.find(DropdownValue);
- expect(valueComp.exists()).toBe(true);
- expect(valueComp.text()).toBe('None');
+ expect(valueComp.exists()).toBe(true);
+ expect(valueComp.text()).toBe('None');
- wrapperDropdownValue.destroy();
+ wrapperDropdownValue.destroy();
+ });
});
it('renders `dropdown-button` component when `showDropdownButton` prop is `true`', () => {
diff --git a/spec/frontend/vue_shared/components/sidebar/labels_select_vue/mock_data.js b/spec/frontend/vue_shared/components/sidebar/labels_select_vue/mock_data.js
index a863cddbaee..e1008d13fc2 100644
--- a/spec/frontend/vue_shared/components/sidebar/labels_select_vue/mock_data.js
+++ b/spec/frontend/vue_shared/components/sidebar/labels_select_vue/mock_data.js
@@ -30,15 +30,16 @@ export const mockConfig = {
allowLabelEdit: true,
allowLabelCreate: true,
allowScopedLabels: true,
+ allowMultiselect: true,
labelsListTitle: 'Assign labels',
labelsCreateTitle: 'Create label',
+ variant: 'sidebar',
dropdownOnly: false,
selectedLabels: [mockRegularLabel, mockScopedLabel],
labelsSelectInProgress: false,
labelsFetchPath: '/gitlab-org/my-project/-/labels.json',
labelsManagePath: '/gitlab-org/my-project/-/labels',
labelsFilterBasePath: '/gitlab-org/my-project/issues',
- scopedLabelsDocumentationPath: '/help/user/project/labels.md#scoped-labels-premium',
};
export const mockSuggestedColors = {
diff --git a/spec/frontend/vue_shared/components/sidebar/labels_select_vue/store/actions_spec.js b/spec/frontend/vue_shared/components/sidebar/labels_select_vue/store/actions_spec.js
index 6e2363ba96f..072d8fe2fe2 100644
--- a/spec/frontend/vue_shared/components/sidebar/labels_select_vue/store/actions_spec.js
+++ b/spec/frontend/vue_shared/components/sidebar/labels_select_vue/store/actions_spec.js
@@ -15,7 +15,7 @@ describe('LabelsSelect Actions', () => {
};
beforeEach(() => {
- state = Object.assign({}, defaultState());
+ state = { ...defaultState() };
});
describe('setInitialState', () => {
diff --git a/spec/frontend/vue_shared/components/sidebar/labels_select_vue/store/getters_spec.js b/spec/frontend/vue_shared/components/sidebar/labels_select_vue/store/getters_spec.js
index bfceaa0828b..b866117efcf 100644
--- a/spec/frontend/vue_shared/components/sidebar/labels_select_vue/store/getters_spec.js
+++ b/spec/frontend/vue_shared/components/sidebar/labels_select_vue/store/getters_spec.js
@@ -5,19 +5,25 @@ describe('LabelsSelect Getters', () => {
it('returns string "Label" when state.labels has no selected labels', () => {
const labels = [{ id: 1 }, { id: 2 }, { id: 3 }, { id: 4 }];
- expect(getters.dropdownButtonText({ labels })).toBe('Label');
+ expect(getters.dropdownButtonText({ labels }, { isDropdownVariantSidebar: true })).toBe(
+ 'Label',
+ );
});
it('returns label title when state.labels has only 1 label', () => {
const labels = [{ id: 1, title: 'Foobar', set: true }];
- expect(getters.dropdownButtonText({ labels })).toBe('Foobar');
+ expect(getters.dropdownButtonText({ labels }, { isDropdownVariantSidebar: true })).toBe(
+ 'Foobar',
+ );
});
it('returns first label title and remaining labels count when state.labels has more than 1 label', () => {
const labels = [{ id: 1, title: 'Foo', set: true }, { id: 2, title: 'Bar', set: true }];
- expect(getters.dropdownButtonText({ labels })).toBe('Foo +1 more');
+ expect(getters.dropdownButtonText({ labels }, { isDropdownVariantSidebar: true })).toBe(
+ 'Foo +1 more',
+ );
});
});
@@ -28,4 +34,16 @@ describe('LabelsSelect Getters', () => {
expect(getters.selectedLabelsList({ selectedLabels })).toEqual([1, 2, 3, 4]);
});
});
+
+ describe('isDropdownVariantSidebar', () => {
+ it('returns `true` when `state.variant` is "sidebar"', () => {
+ expect(getters.isDropdownVariantSidebar({ variant: 'sidebar' })).toBe(true);
+ });
+ });
+
+ describe('isDropdownVariantStandalone', () => {
+ it('returns `true` when `state.variant` is "standalone"', () => {
+ expect(getters.isDropdownVariantStandalone({ variant: 'standalone' })).toBe(true);
+ });
+ });
});
diff --git a/spec/frontend/vue_shared/components/sidebar/labels_select_vue/store/mutations_spec.js b/spec/frontend/vue_shared/components/sidebar/labels_select_vue/store/mutations_spec.js
index f6ca98fcc71..8081806e314 100644
--- a/spec/frontend/vue_shared/components/sidebar/labels_select_vue/store/mutations_spec.js
+++ b/spec/frontend/vue_shared/components/sidebar/labels_select_vue/store/mutations_spec.js
@@ -29,6 +29,7 @@ describe('LabelsSelect Mutations', () => {
const state = {
dropdownOnly: false,
showDropdownButton: false,
+ variant: 'sidebar',
};
mutations[types.TOGGLE_DROPDOWN_CONTENTS](state);
@@ -155,11 +156,11 @@ describe('LabelsSelect Mutations', () => {
const labels = [{ id: 1 }, { id: 2 }, { id: 3 }, { id: 4 }];
it('updates `state.labels` to include `touched` and `set` props based on provided `labels` param', () => {
- const updatedLabelIds = [2, 4];
+ const updatedLabelIds = [2];
const state = {
labels,
};
- mutations[types.UPDATE_SELECTED_LABELS](state, { labels });
+ mutations[types.UPDATE_SELECTED_LABELS](state, { labels: [{ id: 2 }] });
state.labels.forEach(label => {
if (updatedLabelIds.includes(label.id)) {
diff --git a/spec/frontend/vue_shared/components/stacked_progress_bar_spec.js b/spec/frontend/vue_shared/components/stacked_progress_bar_spec.js
new file mode 100644
index 00000000000..bc86ee5a0c6
--- /dev/null
+++ b/spec/frontend/vue_shared/components/stacked_progress_bar_spec.js
@@ -0,0 +1,104 @@
+import Vue from 'vue';
+
+import mountComponent from 'helpers/vue_mount_component_helper';
+import stackedProgressBarComponent from '~/vue_shared/components/stacked_progress_bar.vue';
+
+const createComponent = config => {
+ const Component = Vue.extend(stackedProgressBarComponent);
+ const defaultConfig = {
+ successLabel: 'Synced',
+ failureLabel: 'Failed',
+ neutralLabel: 'Out of sync',
+ successCount: 25,
+ failureCount: 10,
+ totalCount: 5000,
+ ...config,
+ };
+
+ return mountComponent(Component, defaultConfig);
+};
+
+describe('StackedProgressBarComponent', () => {
+ let vm;
+
+ beforeEach(() => {
+ vm = createComponent();
+ });
+
+ afterEach(() => {
+ vm.$destroy();
+ });
+
+ describe('computed', () => {
+ describe('neutralCount', () => {
+ it('returns neutralCount based on totalCount, successCount and failureCount', () => {
+ expect(vm.neutralCount).toBe(4965); // 5000 - 25 - 10
+ });
+ });
+ });
+
+ describe('methods', () => {
+ describe('getPercent', () => {
+ it('returns percentage from provided count based on `totalCount`', () => {
+ expect(vm.getPercent(500)).toBe(10);
+ });
+
+ it('returns percentage with decimal place from provided count based on `totalCount`', () => {
+ expect(vm.getPercent(67)).toBe(1.3);
+ });
+
+ it('returns percentage as `< 1` from provided count based on `totalCount` when evaluated value is less than 1', () => {
+ expect(vm.getPercent(10)).toBe('< 1');
+ });
+
+ it('returns 0 if totalCount is falsy', () => {
+ vm = createComponent({ totalCount: 0 });
+
+ expect(vm.getPercent(100)).toBe(0);
+ });
+ });
+
+ describe('barStyle', () => {
+ it('returns style string based on percentage provided', () => {
+ expect(vm.barStyle(50)).toBe('width: 50%;');
+ });
+ });
+
+ describe('getTooltip', () => {
+ describe('when hideTooltips is false', () => {
+ it('returns label string based on label and count provided', () => {
+ expect(vm.getTooltip('Synced', 10)).toBe('Synced: 10');
+ });
+ });
+
+ describe('when hideTooltips is true', () => {
+ beforeEach(() => {
+ vm = createComponent({ hideTooltips: true });
+ });
+
+ it('returns an empty string', () => {
+ expect(vm.getTooltip('Synced', 10)).toBe('');
+ });
+ });
+ });
+ });
+
+ describe('template', () => {
+ it('renders container element', () => {
+ expect(vm.$el.classList.contains('stacked-progress-bar')).toBeTruthy();
+ });
+
+ it('renders empty state when count is unavailable', () => {
+ const vmX = createComponent({ totalCount: 0, successCount: 0, failureCount: 0 });
+
+ expect(vmX.$el.querySelectorAll('.status-unavailable').length).not.toBe(0);
+ vmX.$destroy();
+ });
+
+ it('renders bar elements when count is available', () => {
+ expect(vm.$el.querySelectorAll('.status-green').length).not.toBe(0);
+ expect(vm.$el.querySelectorAll('.status-neutral').length).not.toBe(0);
+ expect(vm.$el.querySelectorAll('.status-red').length).not.toBe(0);
+ });
+ });
+});
diff --git a/spec/frontend/vue_shared/components/tabs/tab_spec.js b/spec/frontend/vue_shared/components/tabs/tab_spec.js
new file mode 100644
index 00000000000..8cf07a9177c
--- /dev/null
+++ b/spec/frontend/vue_shared/components/tabs/tab_spec.js
@@ -0,0 +1,32 @@
+import Vue from 'vue';
+import mountComponent from 'helpers/vue_mount_component_helper';
+import Tab from '~/vue_shared/components/tabs/tab.vue';
+
+describe('Tab component', () => {
+ const Component = Vue.extend(Tab);
+ let vm;
+
+ beforeEach(() => {
+ vm = mountComponent(Component);
+ });
+
+ it('sets localActive to equal active', done => {
+ vm.active = true;
+
+ vm.$nextTick(() => {
+ expect(vm.localActive).toBe(true);
+
+ done();
+ });
+ });
+
+ it('sets active class', done => {
+ vm.active = true;
+
+ vm.$nextTick(() => {
+ expect(vm.$el.classList).toContain('active');
+
+ done();
+ });
+ });
+});
diff --git a/spec/frontend/vue_shared/components/tabs/tabs_spec.js b/spec/frontend/vue_shared/components/tabs/tabs_spec.js
new file mode 100644
index 00000000000..49d92094b34
--- /dev/null
+++ b/spec/frontend/vue_shared/components/tabs/tabs_spec.js
@@ -0,0 +1,61 @@
+import Vue from 'vue';
+import Tabs from '~/vue_shared/components/tabs/tabs';
+import Tab from '~/vue_shared/components/tabs/tab.vue';
+
+describe('Tabs component', () => {
+ let vm;
+
+ beforeEach(() => {
+ vm = new Vue({
+ components: {
+ Tabs,
+ Tab,
+ },
+ render(h) {
+ return h('div', [
+ h('tabs', [
+ h('tab', { attrs: { title: 'Testing', active: true } }, 'First tab'),
+ h('tab', [h('template', { slot: 'title' }, 'Test slot'), 'Second tab']),
+ ]),
+ ]);
+ },
+ }).$mount();
+
+ return vm.$nextTick();
+ });
+
+ describe('tab links', () => {
+ it('renders links for tabs', () => {
+ expect(vm.$el.querySelectorAll('a').length).toBe(2);
+ });
+
+ it('renders link titles from props', () => {
+ expect(vm.$el.querySelector('a').textContent).toContain('Testing');
+ });
+
+ it('renders link titles from slot', () => {
+ expect(vm.$el.querySelectorAll('a')[1].textContent).toContain('Test slot');
+ });
+
+ it('renders active class', () => {
+ expect(vm.$el.querySelector('a').classList).toContain('active');
+ });
+
+ it('updates active class on click', () => {
+ vm.$el.querySelectorAll('a')[1].click();
+
+ return vm.$nextTick(() => {
+ expect(vm.$el.querySelector('a').classList).not.toContain('active');
+ expect(vm.$el.querySelectorAll('a')[1].classList).toContain('active');
+ });
+ });
+ });
+
+ describe('content', () => {
+ it('renders content panes', () => {
+ expect(vm.$el.querySelectorAll('.tab-pane').length).toBe(2);
+ expect(vm.$el.querySelectorAll('.tab-pane')[0].textContent).toContain('First tab');
+ expect(vm.$el.querySelectorAll('.tab-pane')[1].textContent).toContain('Second tab');
+ });
+ });
+});
diff --git a/spec/frontend/vue_shared/components/toggle_button_spec.js b/spec/frontend/vue_shared/components/toggle_button_spec.js
new file mode 100644
index 00000000000..83bbb37a89a
--- /dev/null
+++ b/spec/frontend/vue_shared/components/toggle_button_spec.js
@@ -0,0 +1,101 @@
+import Vue from 'vue';
+import mountComponent from 'helpers/vue_mount_component_helper';
+import toggleButton from '~/vue_shared/components/toggle_button.vue';
+
+describe('Toggle Button', () => {
+ let vm;
+ let Component;
+
+ beforeEach(() => {
+ Component = Vue.extend(toggleButton);
+ });
+
+ afterEach(() => {
+ vm.$destroy();
+ });
+
+ describe('render output', () => {
+ beforeEach(() => {
+ vm = mountComponent(Component, {
+ value: true,
+ name: 'foo',
+ });
+ });
+
+ it('renders input with provided name', () => {
+ expect(vm.$el.querySelector('input').getAttribute('name')).toEqual('foo');
+ });
+
+ it('renders input with provided value', () => {
+ expect(vm.$el.querySelector('input').getAttribute('value')).toEqual('true');
+ });
+
+ it('renders input status icon', () => {
+ expect(vm.$el.querySelectorAll('span.toggle-icon').length).toEqual(1);
+ expect(vm.$el.querySelectorAll('svg.s16.toggle-icon-svg').length).toEqual(1);
+ });
+ });
+
+ describe('is-checked', () => {
+ beforeEach(() => {
+ vm = mountComponent(Component, {
+ value: true,
+ });
+
+ jest.spyOn(vm, '$emit').mockImplementation(() => {});
+ });
+
+ it('renders is checked class', () => {
+ expect(vm.$el.querySelector('button').classList.contains('is-checked')).toEqual(true);
+ });
+
+ it('sets aria-label representing toggle state', () => {
+ vm.value = true;
+
+ expect(vm.ariaLabel).toEqual('Toggle Status: ON');
+
+ vm.value = false;
+
+ expect(vm.ariaLabel).toEqual('Toggle Status: OFF');
+ });
+
+ it('emits change event when clicked', () => {
+ vm.$el.querySelector('button').click();
+
+ expect(vm.$emit).toHaveBeenCalledWith('change', false);
+ });
+ });
+
+ describe('is-disabled', () => {
+ beforeEach(() => {
+ vm = mountComponent(Component, {
+ value: true,
+ disabledInput: true,
+ });
+ jest.spyOn(vm, '$emit').mockImplementation(() => {});
+ });
+
+ it('renders disabled button', () => {
+ expect(vm.$el.querySelector('button').classList.contains('is-disabled')).toEqual(true);
+ });
+
+ it('does not emit change event when clicked', () => {
+ vm.$el.querySelector('button').click();
+
+ expect(vm.$emit).not.toHaveBeenCalled();
+ });
+ });
+
+ describe('is-loading', () => {
+ beforeEach(() => {
+ vm = mountComponent(Component, {
+ value: true,
+ isLoading: true,
+ });
+ });
+
+ it('renders loading class', () => {
+ expect(vm.$el.querySelector('button').classList.contains('is-loading')).toEqual(true);
+ });
+ });
+});
diff --git a/spec/frontend/wikis_spec.js b/spec/frontend/wikis_spec.js
index 1d17c8b0777..e5d869840aa 100644
--- a/spec/frontend/wikis_spec.js
+++ b/spec/frontend/wikis_spec.js
@@ -14,6 +14,7 @@ describe('Wikis', () => {
<option value="asciidoc">AsciiDoc</option>
<option value="org">Org</option>
</select>
+ <textarea id="wiki_content"></textarea>
<code class="js-markup-link-example">{Link title}[link:page-slug]</code>
</form>
`;
@@ -24,6 +25,10 @@ describe('Wikis', () => {
let changeFormatSelect;
let linkExample;
+ const findBeforeUnloadWarning = () => window.onbeforeunload?.();
+ const findContent = () => document.getElementById('wiki_content');
+ const findForm = () => document.querySelector('.wiki-form');
+
describe('when the wiki page is being created', () => {
const formHtmlFixture = editFormHtmlFixture({ newPage: true });
@@ -94,6 +99,27 @@ describe('Wikis', () => {
expect(linkExample.innerHTML).toBe(text);
});
+
+ it('starts with no unload warning', () => {
+ expect(findBeforeUnloadWarning()).toBeUndefined();
+ });
+
+ describe('when wiki content is updated', () => {
+ beforeEach(() => {
+ const content = findContent();
+ content.value = 'Lorem ipsum dolar sit!';
+ content.dispatchEvent(new Event('input'));
+ });
+
+ it('sets before unload warning', () => {
+ expect(findBeforeUnloadWarning()).toBe('');
+ });
+
+ it('when form submitted, unsets before unload warning', () => {
+ findForm().dispatchEvent(new Event('submit'));
+ expect(findBeforeUnloadWarning()).toBeUndefined();
+ });
+ });
});
});
});
diff --git a/spec/frontend_integration/.eslintrc.yml b/spec/frontend_integration/.eslintrc.yml
new file mode 100644
index 00000000000..26b6f935ffb
--- /dev/null
+++ b/spec/frontend_integration/.eslintrc.yml
@@ -0,0 +1,6 @@
+---
+extends: ../frontend/.eslintrc.yml
+settings:
+ import/resolver:
+ jest:
+ jestConfigFile: 'jest.config.integration.js'
diff --git a/spec/frontend_integration/README.md b/spec/frontend_integration/README.md
new file mode 100644
index 00000000000..573a385d81e
--- /dev/null
+++ b/spec/frontend_integration/README.md
@@ -0,0 +1,17 @@
+## Frontend Integration Specs
+
+This directory contains Frontend integration specs. Go to `spec/frontend` if you're looking for Frontend unit tests.
+
+Frontend integration specs:
+
+- Mock out the Backend.
+- Don't test individual components, but instead test use cases.
+- Are expected to run slower than unit tests.
+- Could end up having their own environment.
+
+As a result, they deserve their own special place.
+
+## References
+
+- https://docs.gitlab.com/ee/development/testing_guide/testing_levels.html#frontend-integration-tests
+- https://gitlab.com/gitlab-org/gitlab/-/issues/208800
diff --git a/spec/frontend_integration/ide/__snapshots__/ide_integration_spec.js.snap b/spec/frontend_integration/ide/__snapshots__/ide_integration_spec.js.snap
new file mode 100644
index 00000000000..a76f7960d03
--- /dev/null
+++ b/spec/frontend_integration/ide/__snapshots__/ide_integration_spec.js.snap
@@ -0,0 +1,136 @@
+// Jest Snapshot v1, https://goo.gl/fbAQLP
+
+exports[`WebIDE runs 1`] = `
+<div>
+ <article
+ class="ide position-relative d-flex flex-column align-items-stretch"
+ >
+ <div
+ class="ide-view flex-grow d-flex"
+ >
+ <div
+ class="file-finder-overlay"
+ style="display: none;"
+ >
+ (jest: contents hidden)
+ </div>
+ <div
+ class="multi-file-commit-panel flex-column"
+ style="width: 340px;"
+ >
+ <div
+ class="multi-file-commit-panel-inner"
+ >
+ <div
+ class="multi-file-loading-container"
+ >
+ <div
+ class="animation-container"
+ >
+ <div
+ class="skeleton-line-1"
+ />
+ <div
+ class="skeleton-line-2"
+ />
+ <div
+ class="skeleton-line-3"
+ />
+ </div>
+ </div>
+ <div
+ class="multi-file-loading-container"
+ >
+ <div
+ class="animation-container"
+ >
+ <div
+ class="skeleton-line-1"
+ />
+ <div
+ class="skeleton-line-2"
+ />
+ <div
+ class="skeleton-line-3"
+ />
+ </div>
+ </div>
+ <div
+ class="multi-file-loading-container"
+ >
+ <div
+ class="animation-container"
+ >
+ <div
+ class="skeleton-line-1"
+ />
+ <div
+ class="skeleton-line-2"
+ />
+ <div
+ class="skeleton-line-3"
+ />
+ </div>
+ </div>
+ </div>
+ <div
+ class="position-absolute position-top-0 position-bottom-0 drag-handle position-right-0"
+ size="340"
+ style="cursor: ew-resize;"
+ />
+ </div>
+ <div
+ class="multi-file-edit-pane"
+ >
+ <div
+ class="ide-empty-state"
+ >
+ <div
+ class="row js-empty-state"
+ >
+ <div
+ class="col-12"
+ >
+ <div
+ class="svg-content svg-250"
+ >
+ <img
+ src="/test/empty_state.svg"
+ />
+ </div>
+ </div>
+ <div
+ class="col-12"
+ >
+ <div
+ class="text-content text-center"
+ >
+ <h4>
+ Make and review changes in the browser with the Web IDE
+ </h4>
+ <div
+ class="gl-spinner-container"
+ >
+ <span
+ aria-hidden="true"
+ aria-label="Loading"
+ class="align-text-bottom gl-spinner gl-spinner-orange gl-spinner-md"
+ />
+ </div>
+ </div>
+ </div>
+ </div>
+ </div>
+ </div>
+ </div>
+ <footer
+ class="ide-status-bar"
+ >
+ <div
+ class="ide-status-list d-flex ml-auto"
+ >
+ </div>
+ </footer>
+ </article>
+</div>
+`;
diff --git a/spec/frontend_integration/ide/ide_integration_spec.js b/spec/frontend_integration/ide/ide_integration_spec.js
new file mode 100644
index 00000000000..7e8fb3a32ee
--- /dev/null
+++ b/spec/frontend_integration/ide/ide_integration_spec.js
@@ -0,0 +1,100 @@
+/**
+ * WARNING: WIP
+ *
+ * Please do not copy from this spec or use it as an example for anything.
+ *
+ * This is in place to iteratively set up the frontend integration testing environment
+ * and will be improved upon in a later iteration.
+ *
+ * See https://gitlab.com/gitlab-org/gitlab/-/issues/208800 for more information.
+ */
+import MockAdapter from 'axios-mock-adapter';
+import axios from '~/lib/utils/axios_utils';
+import { initIde } from '~/ide';
+
+jest.mock('~/api', () => {
+ return {
+ project: jest.fn().mockImplementation(() => new Promise(() => {})),
+ };
+});
+
+jest.mock('~/ide/services/gql', () => {
+ return {
+ query: jest.fn().mockImplementation(() => new Promise(() => {})),
+ };
+});
+
+describe('WebIDE', () => {
+ let vm;
+ let root;
+ let mock;
+ let initData;
+ let location;
+
+ beforeEach(() => {
+ root = document.createElement('div');
+ initData = {
+ emptyStateSvgPath: '/test/empty_state.svg',
+ noChangesStateSvgPath: '/test/no_changes_state.svg',
+ committedStateSvgPath: '/test/committed_state.svg',
+ pipelinesEmptyStateSvgPath: '/test/pipelines_empty_state.svg',
+ promotionSvgPath: '/test/promotion.svg',
+ ciHelpPagePath: '/test/ci_help_page',
+ webIDEHelpPagePath: '/test/web_ide_help_page',
+ clientsidePreviewEnabled: 'true',
+ renderWhitespaceInCode: 'false',
+ codesandboxBundlerUrl: 'test/codesandbox_bundler',
+ };
+
+ mock = new MockAdapter(axios);
+ mock.onAny('*').reply(() => new Promise(() => {}));
+
+ location = { pathname: '/-/ide/project/gitlab-test/test', search: '', hash: '' };
+ Object.defineProperty(window, 'location', {
+ get() {
+ return location;
+ },
+ });
+ });
+
+ afterEach(() => {
+ vm.$destroy();
+ vm = null;
+
+ mock.restore();
+ });
+
+ const createComponent = () => {
+ const el = document.createElement('div');
+ Object.assign(el.dataset, initData);
+ root.appendChild(el);
+ vm = initIde(el);
+ };
+
+ expect.addSnapshotSerializer({
+ test(value) {
+ return value instanceof HTMLElement && !value.$_hit;
+ },
+ print(element, serialize) {
+ element.$_hit = true;
+ element.querySelectorAll('[style]').forEach(el => {
+ el.$_hit = true;
+ if (el.style.display === 'none') {
+ el.textContent = '(jest: contents hidden)';
+ }
+ });
+
+ return serialize(element)
+ .replace(/^\s*<!---->$/gm, '')
+ .replace(/\n\s*\n/gm, '\n');
+ },
+ });
+
+ it('runs', () => {
+ createComponent();
+
+ return vm.$nextTick().then(() => {
+ expect(root).toMatchSnapshot();
+ });
+ });
+});
diff --git a/spec/graphql/gitlab_schema_spec.rb b/spec/graphql/gitlab_schema_spec.rb
index 0f21a55f7e9..8960ad91543 100644
--- a/spec/graphql/gitlab_schema_spec.rb
+++ b/spec/graphql/gitlab_schema_spec.rb
@@ -191,13 +191,17 @@ describe GitlabSchema do
context 'for other classes' do
# We cannot use an anonymous class here as `GlobalID` expects `.name` not
# to return `nil`
- class TestGlobalId
- include GlobalID::Identification
- attr_accessor :id
-
- def initialize(id)
- @id = id
+ before do
+ test_global_id = Class.new do
+ include GlobalID::Identification
+ attr_accessor :id
+
+ def initialize(id)
+ @id = id
+ end
end
+
+ stub_const('TestGlobalId', test_global_id)
end
it 'falls back to a regular find' do
diff --git a/spec/graphql/mutations/alert_management/create_alert_issue_spec.rb b/spec/graphql/mutations/alert_management/create_alert_issue_spec.rb
new file mode 100644
index 00000000000..1e51767cf0e
--- /dev/null
+++ b/spec/graphql/mutations/alert_management/create_alert_issue_spec.rb
@@ -0,0 +1,60 @@
+# frozen_string_literal: true
+
+require 'spec_helper'
+
+describe Mutations::AlertManagement::CreateAlertIssue do
+ let_it_be(:current_user) { create(:user) }
+ let_it_be(:project) { create(:project) }
+ let_it_be(:alert) { create(:alert_management_alert, project: project, status: 'triggered') }
+ let(:args) { { project_path: project.full_path, iid: alert.iid } }
+
+ specify { expect(described_class).to require_graphql_authorizations(:update_alert_management_alert) }
+
+ describe '#resolve' do
+ subject(:resolve) { mutation_for(project, current_user).resolve(args) }
+
+ context 'user has access to project' do
+ before do
+ project.add_developer(current_user)
+ end
+
+ context 'when CreateAlertIssueService responds with success' do
+ it 'returns the issue with no errors' do
+ expect(resolve).to eq(
+ alert: alert.reload,
+ issue: Issue.last!,
+ errors: []
+ )
+ end
+ end
+
+ context 'when CreateAlertIssue responds with an error' do
+ before do
+ allow_any_instance_of(::AlertManagement::CreateAlertIssueService)
+ .to receive(:execute)
+ .and_return(ServiceResponse.error(payload: { issue: nil }, message: 'An issue already exists'))
+ end
+
+ it 'returns errors' do
+ expect(resolve).to eq(
+ alert: alert,
+ issue: nil,
+ errors: ['An issue already exists']
+ )
+ end
+ end
+ end
+
+ context 'when resource is not accessible to the user' do
+ it 'raises an error if the resource is not accessible to the user' do
+ expect { subject }.to raise_error(Gitlab::Graphql::Errors::ResourceNotAvailable)
+ end
+ end
+ end
+
+ private
+
+ def mutation_for(project, user)
+ described_class.new(object: project, context: { current_user: user }, field: nil)
+ end
+end
diff --git a/spec/graphql/mutations/alert_management/update_alert_status_spec.rb b/spec/graphql/mutations/alert_management/update_alert_status_spec.rb
new file mode 100644
index 00000000000..8b9abd9497d
--- /dev/null
+++ b/spec/graphql/mutations/alert_management/update_alert_status_spec.rb
@@ -0,0 +1,73 @@
+# frozen_string_literal: true
+
+require 'spec_helper'
+
+describe Mutations::AlertManagement::UpdateAlertStatus do
+ let_it_be(:current_user) { create(:user) }
+ let_it_be(:alert) { create(:alert_management_alert, :triggered) }
+ let_it_be(:project) { alert.project }
+ let(:new_status) { Types::AlertManagement::StatusEnum.values['ACKNOWLEDGED'].value }
+ let(:args) { { status: new_status, project_path: project.full_path, iid: alert.iid } }
+
+ specify { expect(described_class).to require_graphql_authorizations(:update_alert_management_alert) }
+
+ describe '#resolve' do
+ subject(:resolve) { mutation_for(project, current_user).resolve(args) }
+
+ context 'user has access to project' do
+ before do
+ project.add_developer(current_user)
+ end
+
+ it 'changes the status' do
+ expect { resolve }.to change { alert.reload.acknowledged? }.to(true)
+ end
+
+ it 'returns the alert with no errors' do
+ expect(resolve).to eq(
+ alert: alert,
+ errors: []
+ )
+ end
+
+ context 'error occurs when updating' do
+ it 'returns the alert with errors' do
+ # Stub an error on the alert
+ allow_next_instance_of(Resolvers::AlertManagementAlertResolver) do |resolver|
+ allow(resolver).to receive(:resolve).and_return(alert)
+ end
+
+ allow(alert).to receive(:save).and_return(false)
+ allow(alert).to receive(:errors).and_return(
+ double(full_messages: %w(foo bar))
+ )
+ expect(resolve).to eq(
+ alert: alert,
+ errors: ['foo and bar']
+ )
+ end
+
+ context 'invalid status given' do
+ let(:new_status) { 'invalid_status' }
+
+ it 'returns the alert with errors' do
+ expect(resolve).to eq(
+ alert: alert,
+ errors: [_('Invalid status')]
+ )
+ end
+ end
+ end
+ end
+
+ it 'raises an error if the resource is not accessible to the user' do
+ expect { subject }.to raise_error(Gitlab::Graphql::Errors::ResourceNotAvailable)
+ end
+ end
+
+ private
+
+ def mutation_for(project, user)
+ described_class.new(object: project, context: { current_user: user }, field: nil)
+ end
+end
diff --git a/spec/graphql/mutations/branches/create_spec.rb b/spec/graphql/mutations/branches/create_spec.rb
new file mode 100644
index 00000000000..744f8f1f2bc
--- /dev/null
+++ b/spec/graphql/mutations/branches/create_spec.rb
@@ -0,0 +1,55 @@
+# frozen_string_literal: true
+
+require 'spec_helper'
+
+describe Mutations::Branches::Create do
+ subject(:mutation) { described_class.new(object: nil, context: context, field: nil) }
+
+ let_it_be(:project) { create(:project, :public, :repository) }
+ let_it_be(:user) { create(:user) }
+ let_it_be(:context) do
+ GraphQL::Query::Context.new(
+ query: OpenStruct.new(schema: nil),
+ values: { current_user: user },
+ object: nil
+ )
+ end
+
+ describe '#resolve' do
+ subject { mutation.resolve(project_path: project.full_path, name: branch, ref: ref) }
+
+ let(:branch) { 'new_branch' }
+ let(:ref) { 'master' }
+ let(:mutated_branch) { subject[:branch] }
+
+ it 'raises an error if the resource is not accessible to the user' do
+ expect { subject }.to raise_error(Gitlab::Graphql::Errors::ResourceNotAvailable)
+ end
+
+ context 'when the user can create a branch' do
+ before do
+ project.add_developer(user)
+
+ allow_next_instance_of(::Branches::CreateService, project, user) do |create_service|
+ allow(create_service).to receive(:execute).with(branch, ref) { service_result }
+ end
+ end
+
+ context 'when service successfully creates a new branch' do
+ let(:service_result) { { status: :success, branch: double(name: branch) } }
+
+ it 'returns a new branch' do
+ expect(mutated_branch.name).to eq(branch)
+ expect(subject[:errors]).to be_empty
+ end
+ end
+
+ context 'when service fails to create a new branch' do
+ let(:service_result) { { status: :error, message: 'Branch already exists' } }
+
+ it { expect(mutated_branch).to be_nil }
+ it { expect(subject[:errors]).to eq(['Branch already exists']) }
+ end
+ end
+ end
+end
diff --git a/spec/graphql/mutations/design_management/delete_spec.rb b/spec/graphql/mutations/design_management/delete_spec.rb
new file mode 100644
index 00000000000..60be6dad62a
--- /dev/null
+++ b/spec/graphql/mutations/design_management/delete_spec.rb
@@ -0,0 +1,145 @@
+# frozen_string_literal: true
+
+require 'spec_helper'
+
+describe Mutations::DesignManagement::Delete do
+ include DesignManagementTestHelpers
+
+ let(:issue) { create(:issue) }
+ let(:current_designs) { issue.designs.current }
+ let(:user) { issue.author }
+ let(:project) { issue.project }
+ let(:design_a) { create(:design, :with_file, issue: issue) }
+ let(:design_b) { create(:design, :with_file, issue: issue) }
+ let(:design_c) { create(:design, :with_file, issue: issue) }
+ let(:filenames) { [design_a, design_b, design_c].map(&:filename) }
+
+ let(:mutation) { described_class.new(object: nil, context: { current_user: user }, field: nil) }
+
+ before do
+ stub_const('Errors', Gitlab::Graphql::Errors, transfer_nested_constants: true)
+ end
+
+ def run_mutation
+ mutation = described_class.new(object: nil, context: { current_user: user }, field: nil)
+ mutation.resolve(project_path: project.full_path, iid: issue.iid, filenames: filenames)
+ end
+
+ describe '#resolve' do
+ let(:expected_response) do
+ { errors: [], version: DesignManagement::Version.for_issue(issue).ordered.first }
+ end
+
+ shared_examples "failures" do |error: Gitlab::Graphql::Errors::ResourceNotAvailable|
+ it "raises #{error.name}" do
+ expect { run_mutation }.to raise_error(error)
+ end
+ end
+
+ shared_examples "resource not available" do
+ it_behaves_like "failures"
+ end
+
+ context "when the feature is not available" do
+ before do
+ enable_design_management(false)
+ end
+
+ it_behaves_like "resource not available"
+ end
+
+ context "when the feature is available" do
+ before do
+ enable_design_management(true)
+ end
+
+ context "when the user is not allowed to delete designs" do
+ let(:user) { create(:user) }
+
+ it_behaves_like "resource not available"
+ end
+
+ context 'deleting an already deleted file' do
+ before do
+ run_mutation
+ end
+
+ it 'fails with an argument error' do
+ expect { run_mutation }.to raise_error(Gitlab::Graphql::Errors::ArgumentError)
+ end
+ end
+
+ context "when deleting all the designs" do
+ let(:response) { run_mutation }
+
+ it "returns a new version, and no errors" do
+ expect(response).to include(expected_response)
+ end
+
+ describe 'the current designs' do
+ before do
+ run_mutation
+ end
+
+ it 'is empty' do
+ expect(current_designs).to be_empty
+ end
+ end
+
+ it 'runs no more than 28 queries' do
+ filenames.each(&:present?) # ignore setup
+ # Queries: as of 2019-08-28
+ # -------------
+ # 01. routing query
+ # 02. find project by id
+ # 03. project.project_features
+ # 04. find namespace by id and type
+ # 05,06. project.authorizations for user (same query twice)
+ # 07. find issue by iid
+ # 08. find project by id
+ # 09. find namespace by id
+ # 10. find group namespace by id
+ # 11. project.authorizations for user (same query as 5)
+ # 12. project.project_features (same query as 3)
+ # 13. project.authorizations for user (same query as 5)
+ # 14. current designs by filename and issue
+ # 15, 16 project.authorizations for user (same query as 5)
+ # 17. find route by id and source_type
+ # ------------- our queries are below:
+ # 18. start transaction 1
+ # 19. start transaction 2
+ # 20. find version by sha and issue
+ # 21. exists version with sha and issue?
+ # 22. leave transaction 2
+ # 23. create version with sha and issue
+ # 24. create design-version links
+ # 25. validate version.actions.present?
+ # 26. validate version.issue.present?
+ # 27. validate version.sha is unique
+ # 28. leave transaction 1
+ #
+ expect { run_mutation }.not_to exceed_query_limit(28)
+ end
+ end
+
+ context "when deleting a design" do
+ let(:filenames) { [design_a.filename] }
+ let(:response) { run_mutation }
+
+ it "returns the expected response" do
+ expect(response).to include(expected_response)
+ end
+
+ describe 'the current designs' do
+ before do
+ run_mutation
+ end
+
+ it 'does contain designs b and c' do
+ expect(current_designs).to contain_exactly(design_b, design_c)
+ end
+ end
+ end
+ end
+ end
+end
diff --git a/spec/graphql/mutations/design_management/upload_spec.rb b/spec/graphql/mutations/design_management/upload_spec.rb
new file mode 100644
index 00000000000..783af70448c
--- /dev/null
+++ b/spec/graphql/mutations/design_management/upload_spec.rb
@@ -0,0 +1,136 @@
+# frozen_string_literal: true
+require 'spec_helper'
+
+describe Mutations::DesignManagement::Upload do
+ include DesignManagementTestHelpers
+ include ConcurrentHelpers
+
+ let(:issue) { create(:issue) }
+ let(:user) { issue.author }
+ let(:project) { issue.project }
+
+ subject(:mutation) do
+ described_class.new(object: nil, context: { current_user: user }, field: nil)
+ end
+
+ def run_mutation(files_to_upload = files, project_path = project.full_path, iid = issue.iid)
+ mutation = described_class.new(object: nil, context: { current_user: user }, field: nil)
+ mutation.resolve(project_path: project_path, iid: iid, files: files_to_upload)
+ end
+
+ describe "#resolve" do
+ let(:files) { [fixture_file_upload('spec/fixtures/dk.png')] }
+
+ subject(:resolve) do
+ mutation.resolve(project_path: project.full_path, iid: issue.iid, files: files)
+ end
+
+ shared_examples "resource not available" do
+ it "raises an error" do
+ expect { resolve }.to raise_error(Gitlab::Graphql::Errors::ResourceNotAvailable)
+ end
+ end
+
+ context "when the feature is not available" do
+ it_behaves_like "resource not available"
+ end
+
+ context "when the feature is available" do
+ before do
+ enable_design_management
+ end
+
+ describe 'contention in the design repo' do
+ before do
+ issue.design_collection.repository.create_if_not_exists
+ end
+
+ let(:files) do
+ ['dk.png', 'rails_sample.jpg', 'banana_sample.gif']
+ .cycle
+ .take(Concurrent.processor_count * 2)
+ .map { |f| RenameableUpload.unique_file(f) }
+ end
+
+ def creates_designs
+ prior_count = DesignManagement::Design.count
+
+ expect { yield }.not_to raise_error
+
+ expect(DesignManagement::Design.count).to eq(prior_count + files.size)
+ end
+
+ describe 'running requests in parallel' do
+ it 'does not cause errors' do
+ creates_designs do
+ run_parallel(files.map { |f| -> { run_mutation([f]) } })
+ end
+ end
+ end
+
+ describe 'running requests in parallel on different issues' do
+ it 'does not cause errors' do
+ creates_designs do
+ issues = create_list(:issue, files.size, author: user)
+ issues.each { |i| i.project.add_developer(user) }
+ blocks = files.zip(issues).map do |(f, i)|
+ -> { run_mutation([f], i.project.full_path, i.iid) }
+ end
+
+ run_parallel(blocks)
+ end
+ end
+ end
+
+ describe 'running requests in serial' do
+ it 'does not cause errors' do
+ creates_designs do
+ files.each do |f|
+ run_mutation([f])
+ end
+ end
+ end
+ end
+ end
+
+ context "when the user is not allowed to upload designs" do
+ let(:user) { create(:user) }
+
+ it_behaves_like "resource not available"
+ end
+
+ context "a valid design" do
+ it "returns the updated designs" do
+ expect(resolve[:errors]).to eq []
+ expect(resolve[:designs].map(&:filename)).to contain_exactly("dk.png")
+ end
+ end
+
+ context "context when passing an invalid project" do
+ let(:project) { build(:project) }
+
+ it_behaves_like "resource not available"
+ end
+
+ context "context when passing an invalid issue" do
+ let(:issue) { build(:issue) }
+
+ it_behaves_like "resource not available"
+ end
+
+ context "when creating designs causes errors" do
+ before do
+ fake_service = double(::DesignManagement::SaveDesignsService)
+
+ allow(fake_service).to receive(:execute).and_return(status: :error, message: "Something failed")
+ allow(::DesignManagement::SaveDesignsService).to receive(:new).and_return(fake_service)
+ end
+
+ it "wraps the errors" do
+ expect(resolve[:errors]).to eq(["Something failed"])
+ expect(resolve[:designs]).to eq([])
+ end
+ end
+ end
+ end
+end
diff --git a/spec/graphql/mutations/issues/set_confidential_spec.rb b/spec/graphql/mutations/issues/set_confidential_spec.rb
index 6031953c869..c90ce2658d6 100644
--- a/spec/graphql/mutations/issues/set_confidential_spec.rb
+++ b/spec/graphql/mutations/issues/set_confidential_spec.rb
@@ -8,6 +8,8 @@ describe Mutations::Issues::SetConfidential do
subject(:mutation) { described_class.new(object: nil, context: { current_user: user }, field: nil) }
+ specify { expect(described_class).to require_graphql_authorizations(:update_issue) }
+
describe '#resolve' do
let(:confidential) { true }
let(:mutated_issue) { subject[:issue] }
diff --git a/spec/graphql/mutations/issues/set_due_date_spec.rb b/spec/graphql/mutations/issues/set_due_date_spec.rb
index 73ba11fc551..84df6fce7c7 100644
--- a/spec/graphql/mutations/issues/set_due_date_spec.rb
+++ b/spec/graphql/mutations/issues/set_due_date_spec.rb
@@ -8,6 +8,8 @@ describe Mutations::Issues::SetDueDate do
subject(:mutation) { described_class.new(object: nil, context: { current_user: user }, field: nil) }
+ specify { expect(described_class).to require_graphql_authorizations(:update_issue) }
+
describe '#resolve' do
let(:due_date) { 2.days.since }
let(:mutated_issue) { subject[:issue] }
diff --git a/spec/graphql/mutations/issues/update_spec.rb b/spec/graphql/mutations/issues/update_spec.rb
index da286bb4092..8c3d01918fd 100644
--- a/spec/graphql/mutations/issues/update_spec.rb
+++ b/spec/graphql/mutations/issues/update_spec.rb
@@ -16,6 +16,8 @@ describe Mutations::Issues::Update do
let(:mutation) { described_class.new(object: nil, context: { current_user: user }, field: nil) }
let(:mutated_issue) { subject[:issue] }
+ specify { expect(described_class).to require_graphql_authorizations(:update_issue) }
+
describe '#resolve' do
let(:mutation_params) do
{
diff --git a/spec/graphql/mutations/merge_requests/set_labels_spec.rb b/spec/graphql/mutations/merge_requests/set_labels_spec.rb
index f58f35eb6f3..0fd2c20a5c8 100644
--- a/spec/graphql/mutations/merge_requests/set_labels_spec.rb
+++ b/spec/graphql/mutations/merge_requests/set_labels_spec.rb
@@ -8,6 +8,8 @@ describe Mutations::MergeRequests::SetLabels do
subject(:mutation) { described_class.new(object: nil, context: { current_user: user }, field: nil) }
+ specify { expect(described_class).to require_graphql_authorizations(:update_merge_request) }
+
describe '#resolve' do
let(:label) { create(:label, project: merge_request.project) }
let(:label2) { create(:label, project: merge_request.project) }
diff --git a/spec/graphql/mutations/merge_requests/set_locked_spec.rb b/spec/graphql/mutations/merge_requests/set_locked_spec.rb
index 12ae1314f22..d5219c781fd 100644
--- a/spec/graphql/mutations/merge_requests/set_locked_spec.rb
+++ b/spec/graphql/mutations/merge_requests/set_locked_spec.rb
@@ -8,6 +8,8 @@ describe Mutations::MergeRequests::SetLocked do
subject(:mutation) { described_class.new(object: nil, context: { current_user: user }, field: nil) }
+ specify { expect(described_class).to require_graphql_authorizations(:update_merge_request) }
+
describe '#resolve' do
let(:locked) { true }
let(:mutated_merge_request) { subject[:merge_request] }
diff --git a/spec/graphql/mutations/merge_requests/set_milestone_spec.rb b/spec/graphql/mutations/merge_requests/set_milestone_spec.rb
index ad7f2df0842..d77ec4de4d0 100644
--- a/spec/graphql/mutations/merge_requests/set_milestone_spec.rb
+++ b/spec/graphql/mutations/merge_requests/set_milestone_spec.rb
@@ -8,6 +8,8 @@ describe Mutations::MergeRequests::SetMilestone do
subject(:mutation) { described_class.new(object: nil, context: { current_user: user }, field: nil) }
+ specify { expect(described_class).to require_graphql_authorizations(:update_merge_request) }
+
describe '#resolve' do
let(:milestone) { create(:milestone, project: merge_request.project) }
let(:mutated_merge_request) { subject[:merge_request] }
diff --git a/spec/graphql/mutations/merge_requests/set_subscription_spec.rb b/spec/graphql/mutations/merge_requests/set_subscription_spec.rb
index a28bab363f3..cf569a74aa9 100644
--- a/spec/graphql/mutations/merge_requests/set_subscription_spec.rb
+++ b/spec/graphql/mutations/merge_requests/set_subscription_spec.rb
@@ -9,6 +9,8 @@ describe Mutations::MergeRequests::SetSubscription do
subject(:mutation) { described_class.new(object: nil, context: { current_user: user }, field: nil) }
+ specify { expect(described_class).to require_graphql_authorizations(:update_merge_request) }
+
describe '#resolve' do
let(:subscribe) { true }
let(:mutated_merge_request) { subject[:merge_request] }
diff --git a/spec/graphql/mutations/merge_requests/set_wip_spec.rb b/spec/graphql/mutations/merge_requests/set_wip_spec.rb
index 9f0adcf117a..7255d0fe7d7 100644
--- a/spec/graphql/mutations/merge_requests/set_wip_spec.rb
+++ b/spec/graphql/mutations/merge_requests/set_wip_spec.rb
@@ -8,6 +8,8 @@ describe Mutations::MergeRequests::SetWip do
subject(:mutation) { described_class.new(object: nil, context: { current_user: user }, field: nil) }
+ specify { expect(described_class).to require_graphql_authorizations(:update_merge_request) }
+
describe '#resolve' do
let(:wip) { true }
let(:mutated_merge_request) { subject[:merge_request] }
diff --git a/spec/graphql/mutations/todos/mark_all_done_spec.rb b/spec/graphql/mutations/todos/mark_all_done_spec.rb
index 98b22a3e761..4af00307969 100644
--- a/spec/graphql/mutations/todos/mark_all_done_spec.rb
+++ b/spec/graphql/mutations/todos/mark_all_done_spec.rb
@@ -17,6 +17,8 @@ describe Mutations::Todos::MarkAllDone do
let_it_be(:user3) { create(:user) }
+ specify { expect(described_class).to require_graphql_authorizations(:update_user) }
+
describe '#resolve' do
it 'marks all pending todos as done' do
updated_todo_ids = mutation_for(current_user).resolve.dig(:updated_ids)
diff --git a/spec/graphql/mutations/todos/mark_done_spec.rb b/spec/graphql/mutations/todos/mark_done_spec.rb
index 059ef3c8eee..44065f83f74 100644
--- a/spec/graphql/mutations/todos/mark_done_spec.rb
+++ b/spec/graphql/mutations/todos/mark_done_spec.rb
@@ -16,6 +16,8 @@ describe Mutations::Todos::MarkDone do
let(:mutation) { described_class.new(object: nil, context: { current_user: current_user }, field: nil) }
+ specify { expect(described_class).to require_graphql_authorizations(:update_todo) }
+
describe '#resolve' do
it 'marks a single todo as done' do
result = mark_done_mutation(todo1)
diff --git a/spec/graphql/mutations/todos/restore_spec.rb b/spec/graphql/mutations/todos/restore_spec.rb
index 1637acc2fb5..949ab6a164b 100644
--- a/spec/graphql/mutations/todos/restore_spec.rb
+++ b/spec/graphql/mutations/todos/restore_spec.rb
@@ -14,6 +14,8 @@ describe Mutations::Todos::Restore do
let(:mutation) { described_class.new(object: nil, context: { current_user: current_user }, field: nil) }
+ specify { expect(described_class).to require_graphql_authorizations(:update_todo) }
+
describe '#resolve' do
it 'restores a single todo' do
result = restore_mutation(todo1)
diff --git a/spec/graphql/resolvers/alert_management/alert_status_counts_resolver_spec.rb b/spec/graphql/resolvers/alert_management/alert_status_counts_resolver_spec.rb
new file mode 100644
index 00000000000..8eb28c8c945
--- /dev/null
+++ b/spec/graphql/resolvers/alert_management/alert_status_counts_resolver_spec.rb
@@ -0,0 +1,24 @@
+# frozen_string_literal: true
+
+require 'spec_helper'
+
+describe Resolvers::AlertManagement::AlertStatusCountsResolver do
+ include GraphqlHelpers
+
+ describe '#resolve' do
+ let_it_be(:current_user) { create(:user) }
+ let_it_be(:project) { create(:project) }
+ let(:args) { {} }
+
+ subject { resolve_alert_status_counts(args) }
+
+ it { is_expected.to be_a(Gitlab::AlertManagement::AlertStatusCounts) }
+ specify { expect(subject.project).to eq(project) }
+
+ private
+
+ def resolve_alert_status_counts(args = {}, context = { current_user: current_user })
+ resolve(described_class, obj: project, args: args, ctx: context)
+ end
+ end
+end
diff --git a/spec/graphql/resolvers/alert_management_alert_resolver_spec.rb b/spec/graphql/resolvers/alert_management_alert_resolver_spec.rb
new file mode 100644
index 00000000000..971a81a826d
--- /dev/null
+++ b/spec/graphql/resolvers/alert_management_alert_resolver_spec.rb
@@ -0,0 +1,63 @@
+# frozen_string_literal: true
+
+require 'spec_helper'
+
+describe Resolvers::AlertManagementAlertResolver do
+ include GraphqlHelpers
+
+ let_it_be(:current_user) { create(:user) }
+ let_it_be(:project) { create(:project) }
+ let_it_be(:alert_1) { create(:alert_management_alert, :resolved, project: project, ended_at: 1.year.ago, events: 2, severity: :high) }
+ let_it_be(:alert_2) { create(:alert_management_alert, :ignored, project: project, events: 1, severity: :critical) }
+ let_it_be(:alert_other_proj) { create(:alert_management_alert) }
+
+ let(:args) { {} }
+
+ subject { resolve_alerts(args) }
+
+ context 'user does not have permission' do
+ it { is_expected.to eq(AlertManagement::Alert.none) }
+ end
+
+ context 'user has permission' do
+ before do
+ project.add_developer(current_user)
+ end
+
+ it { is_expected.to contain_exactly(alert_1, alert_2) }
+
+ context 'finding by iid' do
+ let(:args) { { iid: alert_1.iid } }
+
+ it { is_expected.to contain_exactly(alert_1) }
+ end
+
+ context 'finding by status' do
+ let(:args) { { status: [Types::AlertManagement::StatusEnum.values['IGNORED'].value] } }
+
+ it { is_expected.to contain_exactly(alert_2) }
+ end
+
+ describe 'sorting' do
+ # Other sorting examples in spec/finders/alert_management/alerts_finder_spec.rb
+ context 'when sorting by events count' do
+ let_it_be(:alert_count_6) { create(:alert_management_alert, project: project, events: 6) }
+ let_it_be(:alert_count_3) { create(:alert_management_alert, project: project, events: 3) }
+
+ it 'sorts alerts ascending' do
+ expect(resolve_alerts(sort: :events_count_asc)).to eq [alert_2, alert_1, alert_count_3, alert_count_6]
+ end
+
+ it 'sorts alerts descending' do
+ expect(resolve_alerts(sort: :events_count_desc)).to eq [alert_count_6, alert_count_3, alert_1, alert_2]
+ end
+ end
+ end
+ end
+
+ private
+
+ def resolve_alerts(args = {}, context = { current_user: current_user })
+ resolve(described_class, obj: project, args: args, ctx: context)
+ end
+end
diff --git a/spec/graphql/resolvers/board_lists_resolver_spec.rb b/spec/graphql/resolvers/board_lists_resolver_spec.rb
new file mode 100644
index 00000000000..5f6c440a8ed
--- /dev/null
+++ b/spec/graphql/resolvers/board_lists_resolver_spec.rb
@@ -0,0 +1,82 @@
+# frozen_string_literal: true
+
+require 'spec_helper'
+
+describe Resolvers::BoardListsResolver do
+ include GraphqlHelpers
+
+ let_it_be(:user) { create(:user) }
+ let_it_be(:guest) { create(:user) }
+ let_it_be(:unauth_user) { create(:user) }
+ let_it_be(:project) { create(:project, creator_id: user.id, namespace: user.namespace ) }
+ let_it_be(:group) { create(:group, :private) }
+ let_it_be(:project_label) { create(:label, project: project, name: 'Development') }
+ let_it_be(:group_label) { create(:group_label, group: group, name: 'Development') }
+
+ shared_examples_for 'group and project board lists resolver' do
+ let(:board) { create(:board, resource_parent: board_parent) }
+
+ before do
+ board_parent.add_developer(user)
+ end
+
+ it 'does not create the backlog list' do
+ lists = resolve_board_lists.items
+
+ expect(lists.count).to eq 1
+ expect(lists[0].list_type).to eq 'closed'
+ end
+
+ context 'with unauthorized user' do
+ it 'raises an error' do
+ expect do
+ resolve_board_lists(current_user: unauth_user)
+ end.to raise_error(Gitlab::Graphql::Errors::ResourceNotAvailable)
+ end
+ end
+
+ context 'when authorized' do
+ let!(:label_list) { create(:list, board: board, label: label) }
+ let!(:backlog_list) { create(:backlog_list, board: board) }
+
+ it 'returns a list of board lists' do
+ lists = resolve_board_lists.items
+
+ expect(lists.count).to eq 3
+ expect(lists.map(&:list_type)).to eq %w(backlog label closed)
+ end
+
+ context 'when another user has list preferences' do
+ before do
+ board.lists.first.update_preferences_for(guest, collapsed: true)
+ end
+
+ it 'returns the complete list of board lists for this user' do
+ lists = resolve_board_lists.items
+
+ expect(lists.count).to eq 3
+ end
+ end
+ end
+ end
+
+ describe '#resolve' do
+ context 'when project boards' do
+ let(:board_parent) { project }
+ let(:label) { project_label }
+
+ it_behaves_like 'group and project board lists resolver'
+ end
+
+ context 'when group boards' do
+ let(:board_parent) { group }
+ let(:label) { group_label }
+
+ it_behaves_like 'group and project board lists resolver'
+ end
+ end
+
+ def resolve_board_lists(args: {}, current_user: user)
+ resolve(described_class, obj: board, args: args, ctx: { current_user: current_user })
+ end
+end
diff --git a/spec/graphql/resolvers/branch_commit_resolver_spec.rb b/spec/graphql/resolvers/branch_commit_resolver_spec.rb
new file mode 100644
index 00000000000..22e1de8f375
--- /dev/null
+++ b/spec/graphql/resolvers/branch_commit_resolver_spec.rb
@@ -0,0 +1,26 @@
+# frozen_string_literal: true
+
+require 'spec_helper'
+
+describe Resolvers::BranchCommitResolver do
+ include GraphqlHelpers
+
+ subject(:commit) { resolve(described_class, obj: branch) }
+
+ let_it_be(:repository) { create(:project, :repository).repository }
+ let(:branch) { repository.find_branch('master') }
+
+ describe '#resolve' do
+ it 'resolves commit' do
+ is_expected.to eq(repository.commits('master', limit: 1).last)
+ end
+
+ context 'when branch does not exist' do
+ let(:branch) { nil }
+
+ it 'returns nil' do
+ is_expected.to be_nil
+ end
+ end
+ end
+end
diff --git a/spec/graphql/resolvers/design_management/design_at_version_resolver_spec.rb b/spec/graphql/resolvers/design_management/design_at_version_resolver_spec.rb
new file mode 100644
index 00000000000..a5054ae3ebf
--- /dev/null
+++ b/spec/graphql/resolvers/design_management/design_at_version_resolver_spec.rb
@@ -0,0 +1,69 @@
+# frozen_string_literal: true
+
+require 'spec_helper'
+
+describe Resolvers::DesignManagement::DesignAtVersionResolver do
+ include GraphqlHelpers
+ include DesignManagementTestHelpers
+
+ let_it_be(:issue) { create(:issue) }
+ let_it_be(:project) { issue.project }
+ let_it_be(:user) { create(:user) }
+ let_it_be(:design_a) { create(:design, issue: issue) }
+ let_it_be(:version_a) { create(:design_version, issue: issue, created_designs: [design_a]) }
+
+ let(:current_user) { user }
+ let(:object) { issue.design_collection }
+ let(:global_id) { GitlabSchema.id_from_object(design_at_version).to_s }
+
+ let(:design_at_version) { ::DesignManagement::DesignAtVersion.new(design: design_a, version: version_a) }
+
+ let(:resource_not_available) { ::Gitlab::Graphql::Errors::ResourceNotAvailable }
+
+ before do
+ enable_design_management
+ project.add_developer(user)
+ end
+
+ describe '#resolve' do
+ context 'when the user cannot see designs' do
+ let(:current_user) { create(:user) }
+
+ it 'raises ResourceNotAvailable' do
+ expect { resolve_design }.to raise_error(resource_not_available)
+ end
+ end
+
+ it 'returns the specified design' do
+ expect(resolve_design).to eq(design_at_version)
+ end
+
+ context 'the ID belongs to a design on another issue' do
+ let(:other_dav) do
+ create(:design_at_version, issue: create(:issue, project: project))
+ end
+
+ let(:global_id) { global_id_of(other_dav) }
+
+ it 'raises ResourceNotAvailable' do
+ expect { resolve_design }.to raise_error(resource_not_available)
+ end
+
+ context 'the current object does not constrain the issue' do
+ let(:object) { nil }
+
+ it 'returns the object' do
+ expect(resolve_design).to eq(other_dav)
+ end
+ end
+ end
+ end
+
+ private
+
+ def resolve_design
+ args = { id: global_id }
+ ctx = { current_user: current_user }
+ eager_resolve(described_class, obj: object, args: args, ctx: ctx)
+ end
+end
diff --git a/spec/graphql/resolvers/design_management/design_resolver_spec.rb b/spec/graphql/resolvers/design_management/design_resolver_spec.rb
new file mode 100644
index 00000000000..857acc3d371
--- /dev/null
+++ b/spec/graphql/resolvers/design_management/design_resolver_spec.rb
@@ -0,0 +1,88 @@
+# frozen_string_literal: true
+
+require 'spec_helper'
+
+describe Resolvers::DesignManagement::DesignResolver do
+ include GraphqlHelpers
+ include DesignManagementTestHelpers
+
+ before do
+ enable_design_management
+ end
+
+ describe '#resolve' do
+ let_it_be(:issue) { create(:issue) }
+ let_it_be(:project) { issue.project }
+ let_it_be(:first_version) { create(:design_version) }
+ let_it_be(:first_design) { create(:design, issue: issue, versions: [first_version]) }
+ let_it_be(:current_user) { create(:user) }
+ let_it_be(:design_on_other_issue) do
+ create(:design, issue: create(:issue, project: project), versions: [create(:design_version)])
+ end
+
+ let(:args) { { id: GitlabSchema.id_from_object(first_design).to_s } }
+ let(:gql_context) { { current_user: current_user } }
+
+ before do
+ project.add_developer(current_user)
+ end
+
+ context 'when the user cannot see designs' do
+ let(:gql_context) { { current_user: create(:user) } }
+
+ it 'returns nothing' do
+ expect(resolve_design).to be_nil
+ end
+ end
+
+ context 'when no argument has been passed' do
+ let(:args) { {} }
+
+ it 'raises an error' do
+ expect { resolve_design }.to raise_error(::Gitlab::Graphql::Errors::ArgumentError, /must/)
+ end
+ end
+
+ context 'when both arguments have been passed' do
+ let(:args) { { filename: first_design.filename, id: GitlabSchema.id_from_object(first_design).to_s } }
+
+ it 'raises an error' do
+ expect { resolve_design }.to raise_error(::Gitlab::Graphql::Errors::ArgumentError, /may/)
+ end
+ end
+
+ context 'by ID' do
+ it 'returns the specified design' do
+ expect(resolve_design).to eq(first_design)
+ end
+
+ context 'the ID belongs to a design on another issue' do
+ let(:args) { { id: GitlabSchema.id_from_object(design_on_other_issue).to_s } }
+
+ it 'returns nothing' do
+ expect(resolve_design).to be_nil
+ end
+ end
+ end
+
+ context 'by filename' do
+ let(:args) { { filename: first_design.filename } }
+
+ it 'returns the specified design' do
+ expect(resolve_design).to eq(first_design)
+ end
+
+ context 'the filename belongs to a design on another issue' do
+ let(:args) { { filename: design_on_other_issue.filename } }
+
+ it 'returns nothing' do
+ expect(resolve_design).to be_nil
+ end
+ end
+ end
+ end
+
+ def resolve_design
+ resolve(described_class, obj: issue.design_collection, args: args, ctx: gql_context)
+ end
+end
diff --git a/spec/graphql/resolvers/design_management/designs_resolver_spec.rb b/spec/graphql/resolvers/design_management/designs_resolver_spec.rb
new file mode 100644
index 00000000000..28fc9e2151d
--- /dev/null
+++ b/spec/graphql/resolvers/design_management/designs_resolver_spec.rb
@@ -0,0 +1,93 @@
+# frozen_string_literal: true
+
+require 'spec_helper'
+
+describe Resolvers::DesignManagement::DesignsResolver do
+ include GraphqlHelpers
+ include DesignManagementTestHelpers
+
+ before do
+ enable_design_management
+ end
+
+ describe '#resolve' do
+ let_it_be(:issue) { create(:issue) }
+ let_it_be(:project) { issue.project }
+ let_it_be(:first_version) { create(:design_version) }
+ let_it_be(:first_design) { create(:design, issue: issue, versions: [first_version]) }
+ let_it_be(:current_user) { create(:user) }
+ let(:gql_context) { { current_user: current_user } }
+ let(:args) { {} }
+
+ before do
+ project.add_developer(current_user)
+ end
+
+ context 'when the user cannot see designs' do
+ let(:gql_context) { { current_user: create(:user) } }
+
+ it 'returns nothing' do
+ expect(resolve_designs).to be_empty
+ end
+ end
+
+ context 'for a design collection' do
+ context 'which contains just a single design' do
+ it 'returns just that design' do
+ expect(resolve_designs).to contain_exactly(first_design)
+ end
+ end
+
+ context 'which contains another design' do
+ it 'returns all designs' do
+ second_version = create(:design_version)
+ second_design = create(:design, issue: issue, versions: [second_version])
+
+ expect(resolve_designs).to contain_exactly(first_design, second_design)
+ end
+ end
+ end
+
+ describe 'filtering' do
+ describe 'by filename' do
+ let(:second_version) { create(:design_version) }
+ let(:second_design) { create(:design, issue: issue, versions: [second_version]) }
+ let(:args) { { filenames: [second_design.filename] } }
+
+ it 'resolves to just the relevant design, ignoring designs with the same filename on different issues' do
+ create(:design, issue: create(:issue, project: project), filename: second_design.filename)
+
+ expect(resolve_designs).to contain_exactly(second_design)
+ end
+ end
+
+ describe 'by id' do
+ let(:second_version) { create(:design_version) }
+ let(:second_design) { create(:design, issue: issue, versions: [second_version]) }
+
+ context 'the ID is on the current issue' do
+ let(:args) { { ids: [GitlabSchema.id_from_object(second_design).to_s] } }
+
+ it 'resolves to just the relevant design' do
+ expect(resolve_designs).to contain_exactly(second_design)
+ end
+ end
+
+ context 'the ID is on a different issue' do
+ let(:third_version) { create(:design_version) }
+ let(:third_design) { create(:design, issue: create(:issue, project: project), versions: [third_version]) }
+
+ let(:args) { { ids: [GitlabSchema.id_from_object(third_design).to_s] } }
+
+ it 'ignores it' do
+ expect(resolve_designs).to be_empty
+ end
+ end
+ end
+ end
+ end
+
+ def resolve_designs
+ resolve(described_class, obj: issue.design_collection, args: args, ctx: gql_context)
+ end
+end
diff --git a/spec/graphql/resolvers/design_management/version/design_at_version_resolver_spec.rb b/spec/graphql/resolvers/design_management/version/design_at_version_resolver_spec.rb
new file mode 100644
index 00000000000..cc9c0436885
--- /dev/null
+++ b/spec/graphql/resolvers/design_management/version/design_at_version_resolver_spec.rb
@@ -0,0 +1,93 @@
+# frozen_string_literal: true
+
+require 'spec_helper'
+
+describe Resolvers::DesignManagement::Version::DesignAtVersionResolver do
+ include GraphqlHelpers
+
+ include_context 'four designs in three versions'
+
+ let(:current_user) { authorized_user }
+ let(:gql_context) { { current_user: current_user } }
+
+ let(:version) { third_version }
+ let(:design) { design_a }
+
+ let(:all_singular_args) do
+ {
+ design_at_version_id: global_id_of(dav(design)),
+ design_id: global_id_of(design),
+ filename: design.filename
+ }
+ end
+
+ shared_examples 'a bad argument' do
+ let(:err_class) { ::Gitlab::Graphql::Errors::ArgumentError }
+
+ it 'raises an appropriate error' do
+ expect { resolve_objects }.to raise_error(err_class)
+ end
+ end
+
+ describe '#resolve' do
+ describe 'passing combinations of arguments' do
+ context 'passing no arguments' do
+ let(:args) { {} }
+
+ it_behaves_like 'a bad argument'
+ end
+
+ context 'passing all arguments' do
+ let(:args) { all_singular_args }
+
+ it_behaves_like 'a bad argument'
+ end
+
+ context 'passing any two arguments' do
+ let(:args) { all_singular_args.slice(*all_singular_args.keys.sample(2)) }
+
+ it_behaves_like 'a bad argument'
+ end
+ end
+
+ %i[design_at_version_id design_id filename].each do |arg|
+ describe "passing #{arg}" do
+ let(:args) { all_singular_args.slice(arg) }
+
+ it 'finds the design' do
+ expect(resolve_objects).to eq(dav(design))
+ end
+
+ context 'when the user cannot see designs' do
+ let(:current_user) { create(:user) }
+
+ it 'returns nothing' do
+ expect(resolve_objects).to be_nil
+ end
+ end
+ end
+ end
+
+ describe 'attempting to retrieve an object not visible at this version' do
+ let(:design) { design_d }
+
+ %i[design_at_version_id design_id filename].each do |arg|
+ describe "passing #{arg}" do
+ let(:args) { all_singular_args.slice(arg) }
+
+ it 'does not find the design' do
+ expect(resolve_objects).to be_nil
+ end
+ end
+ end
+ end
+ end
+
+ def resolve_objects
+ resolve(described_class, obj: version, args: args, ctx: gql_context)
+ end
+
+ def dav(design)
+ build(:design_at_version, design: design, version: version)
+ end
+end
diff --git a/spec/graphql/resolvers/design_management/version/designs_at_version_resolver_spec.rb b/spec/graphql/resolvers/design_management/version/designs_at_version_resolver_spec.rb
new file mode 100644
index 00000000000..123b26862d0
--- /dev/null
+++ b/spec/graphql/resolvers/design_management/version/designs_at_version_resolver_spec.rb
@@ -0,0 +1,86 @@
+# frozen_string_literal: true
+
+require 'spec_helper'
+
+describe Resolvers::DesignManagement::Version::DesignsAtVersionResolver do
+ include GraphqlHelpers
+
+ include_context 'four designs in three versions'
+
+ let_it_be(:current_user) { authorized_user }
+ let(:gql_context) { { current_user: current_user } }
+
+ let(:version) { third_version }
+
+ describe '.single' do
+ let(:single) { ::Resolvers::DesignManagement::Version::DesignAtVersionResolver }
+
+ it 'returns the single context resolver' do
+ expect(described_class.single).to eq(single)
+ end
+ end
+
+ describe '#resolve' do
+ let(:args) { {} }
+
+ context 'when the user cannot see designs' do
+ let(:current_user) { create(:user) }
+
+ it 'returns nothing' do
+ expect(resolve_objects).to be_empty
+ end
+ end
+
+ context 'for the current version' do
+ it 'returns all designs visible at that version' do
+ expect(resolve_objects).to contain_exactly(dav(design_a), dav(design_b), dav(design_c))
+ end
+ end
+
+ context 'for a previous version with more objects' do
+ let(:version) { second_version }
+
+ it 'returns objects that were later deleted' do
+ expect(resolve_objects).to contain_exactly(dav(design_a), dav(design_b), dav(design_c), dav(design_d))
+ end
+ end
+
+ context 'for a previous version with fewer objects' do
+ let(:version) { first_version }
+
+ it 'does not return objects that were later created' do
+ expect(resolve_objects).to contain_exactly(dav(design_a))
+ end
+ end
+
+ describe 'filtering' do
+ describe 'by filename' do
+ let(:red_herring) { create(:design, issue: create(:issue, project: project)) }
+ let(:args) { { filenames: [design_b.filename, red_herring.filename] } }
+
+ it 'resolves to just the relevant design' do
+ create(:design, issue: create(:issue, project: project), filename: design_b.filename)
+
+ expect(resolve_objects).to contain_exactly(dav(design_b))
+ end
+ end
+
+ describe 'by id' do
+ let(:red_herring) { create(:design, issue: create(:issue, project: project)) }
+ let(:args) { { ids: [design_a, red_herring].map { |x| global_id_of(x) } } }
+
+ it 'resolves to just the relevant design, ignoring objects on other issues' do
+ expect(resolve_objects).to contain_exactly(dav(design_a))
+ end
+ end
+ end
+ end
+
+ def resolve_objects
+ resolve(described_class, obj: version, args: args, ctx: gql_context)
+ end
+
+ def dav(design)
+ build(:design_at_version, design: design, version: version)
+ end
+end
diff --git a/spec/graphql/resolvers/design_management/version_in_collection_resolver_spec.rb b/spec/graphql/resolvers/design_management/version_in_collection_resolver_spec.rb
new file mode 100644
index 00000000000..ef50598d241
--- /dev/null
+++ b/spec/graphql/resolvers/design_management/version_in_collection_resolver_spec.rb
@@ -0,0 +1,64 @@
+# frozen_string_literal: true
+
+require 'spec_helper'
+
+describe Resolvers::DesignManagement::VersionInCollectionResolver do
+ include GraphqlHelpers
+ include DesignManagementTestHelpers
+
+ let(:resolver) { described_class }
+
+ describe '#resolve' do
+ let_it_be(:issue) { create(:issue) }
+ let_it_be(:current_user) { create(:user) }
+ let_it_be(:first_version) { create(:design_version, issue: issue) }
+
+ let(:project) { issue.project }
+ let(:params) { {} }
+
+ before do
+ enable_design_management
+ project.add_developer(current_user)
+ end
+
+ let(:appropriate_error) { ::Gitlab::Graphql::Errors::ArgumentError }
+
+ subject(:result) { resolve_version(issue.design_collection) }
+
+ context 'Neither id nor sha is passed as parameters' do
+ it 'raises an appropriate error' do
+ expect { result }.to raise_error(appropriate_error)
+ end
+ end
+
+ context 'we pass an id' do
+ let(:params) { { id: global_id_of(first_version) } }
+
+ it { is_expected.to eq(first_version) }
+ end
+
+ context 'we pass a sha' do
+ let(:params) { { sha: first_version.sha } }
+
+ it { is_expected.to eq(first_version) }
+ end
+
+ context 'we pass an inconsistent mixture of sha and version id' do
+ let(:params) { { sha: first_version.sha, id: global_id_of(create(:design_version)) } }
+
+ it { is_expected.to be_nil }
+ end
+
+ context 'we pass the id of something that is not a design_version' do
+ let(:params) { { id: global_id_of(project) } }
+
+ it 'raises an appropriate error' do
+ expect { result }.to raise_error(appropriate_error)
+ end
+ end
+ end
+
+ def resolve_version(obj, context = { current_user: current_user })
+ resolve(resolver, obj: obj, args: params, ctx: context)
+ end
+end
diff --git a/spec/graphql/resolvers/design_management/version_resolver_spec.rb b/spec/graphql/resolvers/design_management/version_resolver_spec.rb
new file mode 100644
index 00000000000..e7c09351204
--- /dev/null
+++ b/spec/graphql/resolvers/design_management/version_resolver_spec.rb
@@ -0,0 +1,43 @@
+# frozen_string_literal: true
+
+require 'spec_helper'
+
+describe Resolvers::DesignManagement::VersionResolver do
+ include GraphqlHelpers
+ include DesignManagementTestHelpers
+
+ let_it_be(:issue) { create(:issue) }
+ let_it_be(:current_user) { create(:user) }
+ let_it_be(:version) { create(:design_version, issue: issue) }
+ let_it_be(:developer) { create(:user) }
+
+ let(:project) { issue.project }
+ let(:params) { { id: global_id_of(version) } }
+
+ before do
+ enable_design_management
+ project.add_developer(developer)
+ end
+
+ context 'the current user is not authorized' do
+ let(:current_user) { create(:user) }
+
+ it 'raises an error on resolution' do
+ expect { resolve_version }.to raise_error(::Gitlab::Graphql::Errors::ResourceNotAvailable)
+ end
+ end
+
+ context 'the current user is authorized' do
+ let(:current_user) { developer }
+
+ context 'the id parameter is provided' do
+ it 'returns the specified version' do
+ expect(resolve_version).to eq(version)
+ end
+ end
+ end
+
+ def resolve_version
+ resolve(described_class, obj: nil, args: params, ctx: { current_user: current_user })
+ end
+end
diff --git a/spec/graphql/resolvers/design_management/versions_resolver_spec.rb b/spec/graphql/resolvers/design_management/versions_resolver_spec.rb
new file mode 100644
index 00000000000..d5bab025e45
--- /dev/null
+++ b/spec/graphql/resolvers/design_management/versions_resolver_spec.rb
@@ -0,0 +1,117 @@
+# frozen_string_literal: true
+
+require 'spec_helper'
+
+describe Resolvers::DesignManagement::VersionsResolver do
+ include GraphqlHelpers
+ include DesignManagementTestHelpers
+
+ describe '#resolve' do
+ let(:resolver) { described_class }
+ let_it_be(:issue) { create(:issue) }
+ let_it_be(:authorized_user) { create(:user) }
+ let_it_be(:first_version) { create(:design_version, issue: issue) }
+ let_it_be(:other_version) { create(:design_version, issue: issue) }
+ let_it_be(:first_design) { create(:design, issue: issue, versions: [first_version, other_version]) }
+ let_it_be(:other_design) { create(:design, :with_versions, issue: issue) }
+
+ let(:project) { issue.project }
+ let(:params) { {} }
+ let(:current_user) { authorized_user }
+ let(:parent_args) { { irrelevant: 1.2 } }
+ let(:parent) { double('Parent', parent: nil, irep_node: double(arguments: parent_args)) }
+
+ before do
+ enable_design_management
+ project.add_developer(authorized_user)
+ end
+
+ shared_examples 'a source of versions' do
+ subject(:result) { resolve_versions(object) }
+
+ let_it_be(:all_versions) { object.versions.ordered }
+
+ context 'when the user is not authorized' do
+ let(:current_user) { create(:user) }
+
+ it { is_expected.to be_empty }
+ end
+
+ context 'without constraints' do
+ it 'returns the ordered versions' do
+ expect(result).to eq(all_versions)
+ end
+ end
+
+ context 'when constrained' do
+ let_it_be(:matching) { all_versions.earlier_or_equal_to(first_version) }
+
+ shared_examples 'a query for all_versions up to the first_version' do
+ it { is_expected.to eq(matching) }
+ end
+
+ context 'by earlier_or_equal_to_id' do
+ let(:params) { { id: global_id_of(first_version) } }
+
+ it_behaves_like 'a query for all_versions up to the first_version'
+ end
+
+ context 'by earlier_or_equal_to_sha' do
+ let(:params) { { sha: first_version.sha } }
+
+ it_behaves_like 'a query for all_versions up to the first_version'
+ end
+
+ context 'by earlier_or_equal_to_sha AND earlier_or_equal_to_id' do
+ context 'and they match' do
+ # This usage is rather dumb, but so long as they match, this will
+ # return successfully
+ let(:params) do
+ {
+ sha: first_version.sha,
+ id: global_id_of(first_version)
+ }
+ end
+
+ it_behaves_like 'a query for all_versions up to the first_version'
+ end
+
+ context 'and they do not match' do
+ let(:params) do
+ {
+ sha: first_version.sha,
+ id: global_id_of(other_version)
+ }
+ end
+
+ it 'raises a suitable error' do
+ expect { result }.to raise_error(GraphQL::ExecutionError)
+ end
+ end
+ end
+
+ context 'by at_version in parent' do
+ let(:parent_args) { { atVersion: global_id_of(first_version) } }
+
+ it_behaves_like 'a query for all_versions up to the first_version'
+ end
+ end
+ end
+
+ describe 'a design collection' do
+ let_it_be(:object) { DesignManagement::DesignCollection.new(issue) }
+
+ it_behaves_like 'a source of versions'
+ end
+
+ describe 'a design' do
+ let_it_be(:object) { first_design }
+
+ it_behaves_like 'a source of versions'
+ end
+
+ def resolve_versions(obj, context = { current_user: current_user })
+ eager_resolve(resolver, obj: obj, args: params.merge(parent: parent), ctx: context)
+ end
+ end
+end
diff --git a/spec/graphql/resolvers/issues_resolver_spec.rb b/spec/graphql/resolvers/issues_resolver_spec.rb
index 53e0a9e3724..b7cc9bc6d71 100644
--- a/spec/graphql/resolvers/issues_resolver_spec.rb
+++ b/spec/graphql/resolvers/issues_resolver_spec.rb
@@ -125,12 +125,11 @@ describe Resolvers::IssuesResolver do
end
context 'when sorting by due date' do
- let(:project) { create(:project) }
-
- let!(:due_issue1) { create(:issue, project: project, due_date: 3.days.from_now) }
- let!(:due_issue2) { create(:issue, project: project, due_date: nil) }
- let!(:due_issue3) { create(:issue, project: project, due_date: 2.days.ago) }
- let!(:due_issue4) { create(:issue, project: project, due_date: nil) }
+ let_it_be(:project) { create(:project) }
+ let_it_be(:due_issue1) { create(:issue, project: project, due_date: 3.days.from_now) }
+ let_it_be(:due_issue2) { create(:issue, project: project, due_date: nil) }
+ let_it_be(:due_issue3) { create(:issue, project: project, due_date: 2.days.ago) }
+ let_it_be(:due_issue4) { create(:issue, project: project, due_date: nil) }
it 'sorts issues ascending' do
expect(resolve_issues(sort: :due_date_asc)).to eq [due_issue3, due_issue1, due_issue4, due_issue2]
@@ -142,17 +141,72 @@ describe Resolvers::IssuesResolver do
end
context 'when sorting by relative position' do
- let(:project) { create(:project) }
-
- let!(:relative_issue1) { create(:issue, project: project, relative_position: 2000) }
- let!(:relative_issue2) { create(:issue, project: project, relative_position: nil) }
- let!(:relative_issue3) { create(:issue, project: project, relative_position: 1000) }
- let!(:relative_issue4) { create(:issue, project: project, relative_position: nil) }
+ let_it_be(:project) { create(:project) }
+ let_it_be(:relative_issue1) { create(:issue, project: project, relative_position: 2000) }
+ let_it_be(:relative_issue2) { create(:issue, project: project, relative_position: nil) }
+ let_it_be(:relative_issue3) { create(:issue, project: project, relative_position: 1000) }
+ let_it_be(:relative_issue4) { create(:issue, project: project, relative_position: nil) }
it 'sorts issues ascending' do
expect(resolve_issues(sort: :relative_position_asc)).to eq [relative_issue3, relative_issue1, relative_issue4, relative_issue2]
end
end
+
+ context 'when sorting by priority' do
+ let_it_be(:project) { create(:project) }
+ let_it_be(:early_milestone) { create(:milestone, project: project, due_date: 10.days.from_now) }
+ let_it_be(:late_milestone) { create(:milestone, project: project, due_date: 30.days.from_now) }
+ let_it_be(:priority_label1) { create(:label, project: project, priority: 1) }
+ let_it_be(:priority_label2) { create(:label, project: project, priority: 5) }
+ let_it_be(:priority_issue1) { create(:issue, project: project, labels: [priority_label1], milestone: late_milestone) }
+ let_it_be(:priority_issue2) { create(:issue, project: project, labels: [priority_label2]) }
+ let_it_be(:priority_issue3) { create(:issue, project: project, milestone: early_milestone) }
+ let_it_be(:priority_issue4) { create(:issue, project: project) }
+
+ it 'sorts issues ascending' do
+ expect(resolve_issues(sort: :priority_asc).items).to eq([priority_issue3, priority_issue1, priority_issue2, priority_issue4])
+ end
+
+ it 'sorts issues descending' do
+ expect(resolve_issues(sort: :priority_desc).items).to eq([priority_issue1, priority_issue3, priority_issue2, priority_issue4])
+ end
+ end
+
+ context 'when sorting by label priority' do
+ let_it_be(:project) { create(:project) }
+ let_it_be(:label1) { create(:label, project: project, priority: 1) }
+ let_it_be(:label2) { create(:label, project: project, priority: 5) }
+ let_it_be(:label3) { create(:label, project: project, priority: 10) }
+ let_it_be(:label_issue1) { create(:issue, project: project, labels: [label1]) }
+ let_it_be(:label_issue2) { create(:issue, project: project, labels: [label2]) }
+ let_it_be(:label_issue3) { create(:issue, project: project, labels: [label1, label3]) }
+ let_it_be(:label_issue4) { create(:issue, project: project) }
+
+ it 'sorts issues ascending' do
+ expect(resolve_issues(sort: :label_priority_asc).items).to eq([label_issue3, label_issue1, label_issue2, label_issue4])
+ end
+
+ it 'sorts issues descending' do
+ expect(resolve_issues(sort: :label_priority_desc).items).to eq([label_issue2, label_issue3, label_issue1, label_issue4])
+ end
+ end
+
+ context 'when sorting by milestone due date' do
+ let_it_be(:project) { create(:project) }
+ let_it_be(:early_milestone) { create(:milestone, project: project, due_date: 10.days.from_now) }
+ let_it_be(:late_milestone) { create(:milestone, project: project, due_date: 30.days.from_now) }
+ let_it_be(:milestone_issue1) { create(:issue, project: project) }
+ let_it_be(:milestone_issue2) { create(:issue, project: project, milestone: early_milestone) }
+ let_it_be(:milestone_issue3) { create(:issue, project: project, milestone: late_milestone) }
+
+ it 'sorts issues ascending' do
+ expect(resolve_issues(sort: :milestone_due_asc).items).to eq([milestone_issue2, milestone_issue3, milestone_issue1])
+ end
+
+ it 'sorts issues descending' do
+ expect(resolve_issues(sort: :milestone_due_desc).items).to eq([milestone_issue3, milestone_issue2, milestone_issue1])
+ end
+ end
end
it 'returns issues user can see' do
diff --git a/spec/graphql/resolvers/milestone_resolver_spec.rb b/spec/graphql/resolvers/milestone_resolver_spec.rb
index 297130c2027..8e2c67fdc03 100644
--- a/spec/graphql/resolvers/milestone_resolver_spec.rb
+++ b/spec/graphql/resolvers/milestone_resolver_spec.rb
@@ -8,14 +8,14 @@ describe Resolvers::MilestoneResolver do
describe '#resolve' do
let_it_be(:current_user) { create(:user) }
+ def resolve_group_milestones(args = {}, context = { current_user: current_user })
+ resolve(described_class, obj: group, args: args, ctx: context)
+ end
+
context 'for group milestones' do
let_it_be(:now) { Time.now }
let_it_be(:group) { create(:group, :private) }
- def resolve_group_milestones(args = {}, context = { current_user: current_user })
- resolve(described_class, obj: group, args: args, ctx: context)
- end
-
before do
group.add_developer(current_user)
end
@@ -89,5 +89,25 @@ describe Resolvers::MilestoneResolver do
end
end
end
+
+ context 'when including descendant milestones in a public group' do
+ let_it_be(:group) { create(:group, :public) }
+ let(:args) { { include_descendants: true } }
+
+ it 'finds milestones only in accessible projects and groups' do
+ accessible_group = create(:group, :private, parent: group)
+ accessible_project = create(:project, group: accessible_group)
+ accessible_group.add_developer(current_user)
+ inaccessible_group = create(:group, :private, parent: group)
+ inaccessible_project = create(:project, :private, group: group)
+ milestone1 = create(:milestone, group: group)
+ milestone2 = create(:milestone, group: accessible_group)
+ milestone3 = create(:milestone, project: accessible_project)
+ create(:milestone, group: inaccessible_group)
+ create(:milestone, project: inaccessible_project)
+
+ expect(resolve_group_milestones(args)).to match_array([milestone1, milestone2, milestone3])
+ end
+ end
end
end
diff --git a/spec/graphql/resolvers/projects/jira_imports_resolver_spec.rb b/spec/graphql/resolvers/projects/jira_imports_resolver_spec.rb
index 47889126531..7146bfb441b 100644
--- a/spec/graphql/resolvers/projects/jira_imports_resolver_spec.rb
+++ b/spec/graphql/resolvers/projects/jira_imports_resolver_spec.rb
@@ -16,7 +16,7 @@ describe Resolvers::Projects::JiraImportsResolver do
context 'when anonymous user' do
let(:current_user) { nil }
- it_behaves_like 'no jira import access'
+ it_behaves_like 'no Jira import access'
end
end
@@ -25,7 +25,7 @@ describe Resolvers::Projects::JiraImportsResolver do
project.add_guest(user)
end
- it_behaves_like 'no jira import data present'
+ it_behaves_like 'no Jira import data present'
it 'does not raise access error' do
expect do
@@ -47,14 +47,14 @@ describe Resolvers::Projects::JiraImportsResolver do
stub_feature_flags(jira_issue_import: false)
end
- it_behaves_like 'no jira import access'
+ it_behaves_like 'no Jira import access'
end
context 'when user cannot read Jira imports' do
context 'when anonymous user' do
let(:current_user) { nil }
- it_behaves_like 'no jira import access'
+ it_behaves_like 'no Jira import access'
end
end
diff --git a/spec/graphql/resolvers/projects_resolver_spec.rb b/spec/graphql/resolvers/projects_resolver_spec.rb
new file mode 100644
index 00000000000..73ff99a2520
--- /dev/null
+++ b/spec/graphql/resolvers/projects_resolver_spec.rb
@@ -0,0 +1,77 @@
+# frozen_string_literal: true
+
+require 'spec_helper'
+
+describe Resolvers::ProjectsResolver do
+ include GraphqlHelpers
+
+ describe '#resolve' do
+ subject { resolve(described_class, obj: nil, args: filters, ctx: { current_user: current_user }) }
+
+ let_it_be(:project) { create(:project, :public) }
+ let_it_be(:other_project) { create(:project, :public) }
+ let_it_be(:private_project) { create(:project, :private) }
+ let_it_be(:other_private_project) { create(:project, :private) }
+
+ let_it_be(:user) { create(:user) }
+
+ let(:filters) { {} }
+
+ before_all do
+ project.add_developer(user)
+ private_project.add_developer(user)
+ end
+
+ context 'when user is not logged in' do
+ let(:current_user) { nil }
+
+ context 'when no filters are applied' do
+ it 'returns all public projects' do
+ is_expected.to contain_exactly(project, other_project)
+ end
+
+ context 'when search filter is provided' do
+ let(:filters) { { search: project.name } }
+
+ it 'returns matching project' do
+ is_expected.to contain_exactly(project)
+ end
+ end
+
+ context 'when membership filter is provided' do
+ let(:filters) { { membership: true } }
+
+ it 'returns empty list' do
+ is_expected.to be_empty
+ end
+ end
+ end
+ end
+
+ context 'when user is logged in' do
+ let(:current_user) { user }
+
+ context 'when no filters are applied' do
+ it 'returns all visible projects for the user' do
+ is_expected.to contain_exactly(project, other_project, private_project)
+ end
+
+ context 'when search filter is provided' do
+ let(:filters) { { search: project.name } }
+
+ it 'returns matching project' do
+ is_expected.to contain_exactly(project)
+ end
+ end
+
+ context 'when membership filter is provided' do
+ let(:filters) { { membership: true } }
+
+ it 'returns projects that user is member of' do
+ is_expected.to contain_exactly(project, private_project)
+ end
+ end
+ end
+ end
+ end
+end
diff --git a/spec/graphql/resolvers/release_resolver_spec.rb b/spec/graphql/resolvers/release_resolver_spec.rb
new file mode 100644
index 00000000000..71aa4bbb439
--- /dev/null
+++ b/spec/graphql/resolvers/release_resolver_spec.rb
@@ -0,0 +1,51 @@
+# frozen_string_literal: true
+
+require 'spec_helper'
+
+describe Resolvers::ReleaseResolver do
+ include GraphqlHelpers
+
+ let_it_be(:project) { create(:project, :private) }
+ let_it_be(:release) { create(:release, project: project) }
+ let_it_be(:developer) { create(:user) }
+ let_it_be(:public_user) { create(:user) }
+
+ let(:args) { { tag_name: release.tag } }
+
+ before do
+ project.add_developer(developer)
+ end
+
+ describe '#resolve' do
+ context 'when the user does not have access to the project' do
+ let(:current_user) { public_user }
+
+ it 'returns nil' do
+ expect(resolve_release).to be_nil
+ end
+ end
+
+ context "when the user has full access to the project's releases" do
+ let(:current_user) { developer }
+
+ it 'returns the release associated with the specified tag' do
+ expect(resolve_release).to eq(release)
+ end
+
+ context 'when no tag_name argument was passed' do
+ let(:args) { {} }
+
+ it 'raises an error' do
+ expect { resolve_release }.to raise_error(ArgumentError, "missing keyword: tag_name")
+ end
+ end
+ end
+ end
+
+ private
+
+ def resolve_release
+ context = { current_user: current_user }
+ resolve(described_class, obj: project, args: args, ctx: context)
+ end
+end
diff --git a/spec/graphql/resolvers/releases_resolver_spec.rb b/spec/graphql/resolvers/releases_resolver_spec.rb
new file mode 100644
index 00000000000..9de539b417a
--- /dev/null
+++ b/spec/graphql/resolvers/releases_resolver_spec.rb
@@ -0,0 +1,42 @@
+# frozen_string_literal: true
+
+require 'spec_helper'
+
+describe Resolvers::ReleasesResolver do
+ include GraphqlHelpers
+
+ let_it_be(:project) { create(:project, :private) }
+ let_it_be(:release_v1) { create(:release, project: project, tag: 'v1.0.0') }
+ let_it_be(:release_v2) { create(:release, project: project, tag: 'v2.0.0') }
+ let_it_be(:developer) { create(:user) }
+ let_it_be(:public_user) { create(:user) }
+
+ before do
+ project.add_developer(developer)
+ end
+
+ describe '#resolve' do
+ context 'when the user does not have access to the project' do
+ let(:current_user) { public_user }
+
+ it 'returns an empty array' do
+ expect(resolve_releases).to eq([])
+ end
+ end
+
+ context "when the user has full access to the project's releases" do
+ let(:current_user) { developer }
+
+ it 'returns all releases associated to the project' do
+ expect(resolve_releases).to eq([release_v1, release_v2])
+ end
+ end
+ end
+
+ private
+
+ def resolve_releases
+ context = { current_user: current_user }
+ resolve(described_class, obj: project, args: {}, ctx: context)
+ end
+end
diff --git a/spec/graphql/types/alert_management/alert_status_count_type_spec.rb b/spec/graphql/types/alert_management/alert_status_count_type_spec.rb
new file mode 100644
index 00000000000..1c56028425e
--- /dev/null
+++ b/spec/graphql/types/alert_management/alert_status_count_type_spec.rb
@@ -0,0 +1,20 @@
+# frozen_string_literal: true
+
+require 'spec_helper'
+
+describe GitlabSchema.types['AlertManagementAlertStatusCountsType'] do
+ specify { expect(described_class.graphql_name).to eq('AlertManagementAlertStatusCountsType') }
+
+ it 'exposes the expected fields' do
+ expected_fields = %i[
+ all
+ open
+ triggered
+ acknowledged
+ resolved
+ ignored
+ ]
+
+ expect(described_class).to have_graphql_fields(*expected_fields)
+ end
+end
diff --git a/spec/graphql/types/alert_management/alert_type_spec.rb b/spec/graphql/types/alert_management/alert_type_spec.rb
new file mode 100644
index 00000000000..9c326f30e3c
--- /dev/null
+++ b/spec/graphql/types/alert_management/alert_type_spec.rb
@@ -0,0 +1,31 @@
+# frozen_string_literal: true
+
+require 'spec_helper'
+
+describe GitlabSchema.types['AlertManagementAlert'] do
+ specify { expect(described_class.graphql_name).to eq('AlertManagementAlert') }
+
+ specify { expect(described_class).to require_graphql_authorizations(:read_alert_management_alert) }
+
+ it 'exposes the expected fields' do
+ expected_fields = %i[
+ iid
+ issue_iid
+ title
+ description
+ severity
+ status
+ service
+ monitoring_tool
+ hosts
+ started_at
+ ended_at
+ event_count
+ details
+ created_at
+ updated_at
+ ]
+
+ expect(described_class).to have_graphql_fields(*expected_fields)
+ end
+end
diff --git a/spec/graphql/types/alert_management/severity_enum_spec.rb b/spec/graphql/types/alert_management/severity_enum_spec.rb
new file mode 100644
index 00000000000..ca5aa826fe5
--- /dev/null
+++ b/spec/graphql/types/alert_management/severity_enum_spec.rb
@@ -0,0 +1,11 @@
+# frozen_string_literal: true
+
+require 'spec_helper'
+
+describe GitlabSchema.types['AlertManagementSeverity'] do
+ specify { expect(described_class.graphql_name).to eq('AlertManagementSeverity') }
+
+ it 'exposes all the severity values' do
+ expect(described_class.values.keys).to include(*%w[CRITICAL HIGH MEDIUM LOW INFO UNKNOWN])
+ end
+end
diff --git a/spec/graphql/types/alert_management/status_enum_spec.rb b/spec/graphql/types/alert_management/status_enum_spec.rb
new file mode 100644
index 00000000000..240d8863c97
--- /dev/null
+++ b/spec/graphql/types/alert_management/status_enum_spec.rb
@@ -0,0 +1,24 @@
+# frozen_string_literal: true
+
+require 'spec_helper'
+
+describe GitlabSchema.types['AlertManagementStatus'] do
+ specify { expect(described_class.graphql_name).to eq('AlertManagementStatus') }
+
+ describe 'statuses' do
+ using RSpec::Parameterized::TableSyntax
+
+ where(:status_name, :status_value) do
+ 'TRIGGERED' | 0
+ 'ACKNOWLEDGED' | 1
+ 'RESOLVED' | 2
+ 'IGNORED' | 3
+ end
+
+ with_them do
+ it 'exposes a status with the correct value' do
+ expect(described_class.values[status_name].value).to eq(status_value)
+ end
+ end
+ end
+end
diff --git a/spec/graphql/types/award_emojis/award_emoji_type_spec.rb b/spec/graphql/types/award_emojis/award_emoji_type_spec.rb
index de5ece3b749..4e06329506d 100644
--- a/spec/graphql/types/award_emojis/award_emoji_type_spec.rb
+++ b/spec/graphql/types/award_emojis/award_emoji_type_spec.rb
@@ -3,9 +3,9 @@
require 'spec_helper'
describe GitlabSchema.types['AwardEmoji'] do
- it { expect(described_class.graphql_name).to eq('AwardEmoji') }
+ specify { expect(described_class.graphql_name).to eq('AwardEmoji') }
- it { expect(described_class).to require_graphql_authorizations(:read_emoji) }
+ specify { expect(described_class).to require_graphql_authorizations(:read_emoji) }
- it { expect(described_class).to have_graphql_fields(:description, :unicode_version, :emoji, :name, :unicode, :user) }
+ specify { expect(described_class).to have_graphql_fields(:description, :unicode_version, :emoji, :name, :unicode, :user) }
end
diff --git a/spec/graphql/types/blob_viewers/type_enum_spec.rb b/spec/graphql/types/blob_viewers/type_enum_spec.rb
index 7bd4352f388..09664382af9 100644
--- a/spec/graphql/types/blob_viewers/type_enum_spec.rb
+++ b/spec/graphql/types/blob_viewers/type_enum_spec.rb
@@ -3,7 +3,7 @@
require 'spec_helper'
describe Types::BlobViewers::TypeEnum do
- it { expect(described_class.graphql_name).to eq('BlobViewersType') }
+ specify { expect(described_class.graphql_name).to eq('BlobViewersType') }
it 'exposes all tree entry types' do
expect(described_class.values.keys).to include(*%w[rich simple auxiliary])
diff --git a/spec/graphql/types/board_list_type_spec.rb b/spec/graphql/types/board_list_type_spec.rb
new file mode 100644
index 00000000000..69597fc9617
--- /dev/null
+++ b/spec/graphql/types/board_list_type_spec.rb
@@ -0,0 +1,13 @@
+# frozen_string_literal: true
+
+require 'spec_helper'
+
+describe GitlabSchema.types['BoardList'] do
+ specify { expect(described_class.graphql_name).to eq('BoardList') }
+
+ it 'has specific fields' do
+ expected_fields = %w[id list_type position label]
+
+ expect(described_class).to include_graphql_fields(*expected_fields)
+ end
+end
diff --git a/spec/graphql/types/board_type_spec.rb b/spec/graphql/types/board_type_spec.rb
index 1ca4bf18b57..5d87a1757b5 100644
--- a/spec/graphql/types/board_type_spec.rb
+++ b/spec/graphql/types/board_type_spec.rb
@@ -3,9 +3,9 @@
require 'spec_helper'
describe GitlabSchema.types['Board'] do
- it { expect(described_class.graphql_name).to eq('Board') }
+ specify { expect(described_class.graphql_name).to eq('Board') }
- it { expect(described_class).to require_graphql_authorizations(:read_board) }
+ specify { expect(described_class).to require_graphql_authorizations(:read_board) }
it 'has specific fields' do
expected_fields = %w[id name]
diff --git a/spec/graphql/types/branch_type_spec.rb b/spec/graphql/types/branch_type_spec.rb
new file mode 100644
index 00000000000..f58b514116d
--- /dev/null
+++ b/spec/graphql/types/branch_type_spec.rb
@@ -0,0 +1,9 @@
+# frozen_string_literal: true
+
+require 'spec_helper'
+
+describe GitlabSchema.types['Branch'] do
+ it { expect(described_class.graphql_name).to eq('Branch') }
+
+ it { expect(described_class).to have_graphql_fields(:name, :commit) }
+end
diff --git a/spec/graphql/types/ci/detailed_status_type_spec.rb b/spec/graphql/types/ci/detailed_status_type_spec.rb
index 169a03c770b..c62c8f23728 100644
--- a/spec/graphql/types/ci/detailed_status_type_spec.rb
+++ b/spec/graphql/types/ci/detailed_status_type_spec.rb
@@ -3,7 +3,7 @@
require 'spec_helper'
describe Types::Ci::DetailedStatusType do
- it { expect(described_class.graphql_name).to eq('DetailedStatus') }
+ specify { expect(described_class.graphql_name).to eq('DetailedStatus') }
it "has all fields" do
expect(described_class).to have_graphql_fields(:group, :icon, :favicon,
diff --git a/spec/graphql/types/ci/pipeline_type_spec.rb b/spec/graphql/types/ci/pipeline_type_spec.rb
index 2fafc1bc13f..d56cff12105 100644
--- a/spec/graphql/types/ci/pipeline_type_spec.rb
+++ b/spec/graphql/types/ci/pipeline_type_spec.rb
@@ -3,7 +3,7 @@
require 'spec_helper'
describe Types::Ci::PipelineType do
- it { expect(described_class.graphql_name).to eq('Pipeline') }
+ specify { expect(described_class.graphql_name).to eq('Pipeline') }
- it { expect(described_class).to expose_permissions_using(Types::PermissionTypes::Ci::Pipeline) }
+ specify { expect(described_class).to expose_permissions_using(Types::PermissionTypes::Ci::Pipeline) }
end
diff --git a/spec/graphql/types/commit_type_spec.rb b/spec/graphql/types/commit_type_spec.rb
index f5f99229f3a..88b450e3924 100644
--- a/spec/graphql/types/commit_type_spec.rb
+++ b/spec/graphql/types/commit_type_spec.rb
@@ -3,13 +3,13 @@
require 'spec_helper'
describe GitlabSchema.types['Commit'] do
- it { expect(described_class.graphql_name).to eq('Commit') }
+ specify { expect(described_class.graphql_name).to eq('Commit') }
- it { expect(described_class).to require_graphql_authorizations(:download_code) }
+ specify { expect(described_class).to require_graphql_authorizations(:download_code) }
it 'contains attributes related to commit' do
expect(described_class).to have_graphql_fields(
- :id, :sha, :title, :description, :message, :authored_date,
+ :id, :sha, :title, :description, :message, :title_html, :authored_date,
:author_name, :author_gravatar, :author, :web_url, :latest_pipeline,
:pipelines, :signature_html
)
diff --git a/spec/graphql/types/design_management/design_at_version_type_spec.rb b/spec/graphql/types/design_management/design_at_version_type_spec.rb
new file mode 100644
index 00000000000..1453d73d59c
--- /dev/null
+++ b/spec/graphql/types/design_management/design_at_version_type_spec.rb
@@ -0,0 +1,16 @@
+# frozen_string_literal: true
+
+require 'spec_helper'
+
+describe GitlabSchema.types['DesignAtVersion'] do
+ it_behaves_like 'a GraphQL type with design fields' do
+ let(:extra_design_fields) { %i[version design] }
+ let_it_be(:design) { create(:design, :with_versions) }
+ let(:object_id) do
+ version = design.versions.first
+ GitlabSchema.id_from_object(create(:design_at_version, design: design, version: version))
+ end
+ let_it_be(:object_id_b) { GitlabSchema.id_from_object(create(:design_at_version)) }
+ let(:object_type) { ::Types::DesignManagement::DesignAtVersionType }
+ end
+end
diff --git a/spec/graphql/types/design_management/design_collection_type_spec.rb b/spec/graphql/types/design_management/design_collection_type_spec.rb
new file mode 100644
index 00000000000..65150f0971d
--- /dev/null
+++ b/spec/graphql/types/design_management/design_collection_type_spec.rb
@@ -0,0 +1,13 @@
+# frozen_string_literal: true
+
+require 'spec_helper'
+
+describe GitlabSchema.types['DesignCollection'] do
+ it { expect(described_class).to require_graphql_authorizations(:read_design) }
+
+ it 'has the expected fields' do
+ expected_fields = %i[project issue designs versions version designAtVersion design]
+
+ expect(described_class).to have_graphql_fields(*expected_fields)
+ end
+end
diff --git a/spec/graphql/types/design_management/design_type_spec.rb b/spec/graphql/types/design_management/design_type_spec.rb
new file mode 100644
index 00000000000..75b4cd66d5e
--- /dev/null
+++ b/spec/graphql/types/design_management/design_type_spec.rb
@@ -0,0 +1,13 @@
+# frozen_string_literal: true
+
+require 'spec_helper'
+
+describe GitlabSchema.types['Design'] do
+ it_behaves_like 'a GraphQL type with design fields' do
+ let(:extra_design_fields) { %i[notes discussions versions] }
+ let_it_be(:design) { create(:design, :with_versions) }
+ let(:object_id) { GitlabSchema.id_from_object(design) }
+ let_it_be(:object_id_b) { GitlabSchema.id_from_object(create(:design, :with_versions)) }
+ let(:object_type) { ::Types::DesignManagement::DesignType }
+ end
+end
diff --git a/spec/graphql/types/design_management/design_version_event_enum_spec.rb b/spec/graphql/types/design_management/design_version_event_enum_spec.rb
new file mode 100644
index 00000000000..a65f1bb5990
--- /dev/null
+++ b/spec/graphql/types/design_management/design_version_event_enum_spec.rb
@@ -0,0 +1,11 @@
+# frozen_string_literal: true
+
+require 'spec_helper'
+
+describe GitlabSchema.types['DesignVersionEvent'] do
+ it { expect(described_class.graphql_name).to eq('DesignVersionEvent') }
+
+ it 'exposes the correct event states' do
+ expect(described_class.values.keys).to include(*%w[CREATION MODIFICATION DELETION NONE])
+ end
+end
diff --git a/spec/graphql/types/design_management/version_type_spec.rb b/spec/graphql/types/design_management/version_type_spec.rb
new file mode 100644
index 00000000000..3317c4c6571
--- /dev/null
+++ b/spec/graphql/types/design_management/version_type_spec.rb
@@ -0,0 +1,13 @@
+# frozen_string_literal: true
+
+require 'spec_helper'
+
+describe GitlabSchema.types['DesignVersion'] do
+ it { expect(described_class).to require_graphql_authorizations(:read_design) }
+
+ it 'has the expected fields' do
+ expected_fields = %i[id sha designs design_at_version designs_at_version]
+
+ expect(described_class).to have_graphql_fields(*expected_fields)
+ end
+end
diff --git a/spec/graphql/types/design_management_type_spec.rb b/spec/graphql/types/design_management_type_spec.rb
new file mode 100644
index 00000000000..a6204f20f23
--- /dev/null
+++ b/spec/graphql/types/design_management_type_spec.rb
@@ -0,0 +1,7 @@
+# frozen_string_literal: true
+
+require 'spec_helper'
+
+describe GitlabSchema.types['DesignManagement'] do
+ it { expect(described_class).to have_graphql_fields(:version, :design_at_version) }
+end
diff --git a/spec/graphql/types/diff_refs_type_spec.rb b/spec/graphql/types/diff_refs_type_spec.rb
index a6ead27455f..3165e642452 100644
--- a/spec/graphql/types/diff_refs_type_spec.rb
+++ b/spec/graphql/types/diff_refs_type_spec.rb
@@ -3,11 +3,11 @@
require 'spec_helper'
describe GitlabSchema.types['DiffRefs'] do
- it { expect(described_class.graphql_name).to eq('DiffRefs') }
+ specify { expect(described_class.graphql_name).to eq('DiffRefs') }
- it { expect(described_class).to have_graphql_fields(:head_sha, :base_sha, :start_sha).only }
+ specify { expect(described_class).to have_graphql_fields(:head_sha, :base_sha, :start_sha).only }
- it { expect(described_class.fields['headSha'].type).to be_non_null }
- it { expect(described_class.fields['baseSha'].type).not_to be_non_null }
- it { expect(described_class.fields['startSha'].type).to be_non_null }
+ specify { expect(described_class.fields['headSha'].type).to be_non_null }
+ specify { expect(described_class.fields['baseSha'].type).not_to be_non_null }
+ specify { expect(described_class.fields['startSha'].type).to be_non_null }
end
diff --git a/spec/graphql/types/environment_type_spec.rb b/spec/graphql/types/environment_type_spec.rb
index 24a8bddfa6a..0e5cbac05df 100644
--- a/spec/graphql/types/environment_type_spec.rb
+++ b/spec/graphql/types/environment_type_spec.rb
@@ -3,7 +3,7 @@
require 'spec_helper'
describe GitlabSchema.types['Environment'] do
- it { expect(described_class.graphql_name).to eq('Environment') }
+ specify { expect(described_class.graphql_name).to eq('Environment') }
it 'has the expected fields' do
expected_fields = %w[
@@ -13,5 +13,5 @@ describe GitlabSchema.types['Environment'] do
expect(described_class).to have_graphql_fields(*expected_fields)
end
- it { expect(described_class).to require_graphql_authorizations(:read_environment) }
+ specify { expect(described_class).to require_graphql_authorizations(:read_environment) }
end
diff --git a/spec/graphql/types/error_tracking/sentry_detailed_error_type_spec.rb b/spec/graphql/types/error_tracking/sentry_detailed_error_type_spec.rb
index 44652f831b5..0a094e9e188 100644
--- a/spec/graphql/types/error_tracking/sentry_detailed_error_type_spec.rb
+++ b/spec/graphql/types/error_tracking/sentry_detailed_error_type_spec.rb
@@ -3,9 +3,9 @@
require 'spec_helper'
describe GitlabSchema.types['SentryDetailedError'] do
- it { expect(described_class.graphql_name).to eq('SentryDetailedError') }
+ specify { expect(described_class.graphql_name).to eq('SentryDetailedError') }
- it { expect(described_class).to require_graphql_authorizations(:read_sentry_issue) }
+ specify { expect(described_class).to require_graphql_authorizations(:read_sentry_issue) }
it 'exposes the expected fields' do
expected_fields = %i[
diff --git a/spec/graphql/types/error_tracking/sentry_error_collection_type_spec.rb b/spec/graphql/types/error_tracking/sentry_error_collection_type_spec.rb
index 20ec31391d8..793da2db960 100644
--- a/spec/graphql/types/error_tracking/sentry_error_collection_type_spec.rb
+++ b/spec/graphql/types/error_tracking/sentry_error_collection_type_spec.rb
@@ -3,9 +3,9 @@
require 'spec_helper'
describe GitlabSchema.types['SentryErrorCollection'] do
- it { expect(described_class.graphql_name).to eq('SentryErrorCollection') }
+ specify { expect(described_class.graphql_name).to eq('SentryErrorCollection') }
- it { expect(described_class).to require_graphql_authorizations(:read_sentry_issue) }
+ specify { expect(described_class).to require_graphql_authorizations(:read_sentry_issue) }
it 'exposes the expected fields' do
expected_fields = %i[
diff --git a/spec/graphql/types/error_tracking/sentry_error_stack_trace_entry_type_spec.rb b/spec/graphql/types/error_tracking/sentry_error_stack_trace_entry_type_spec.rb
index 05cc2ca7612..b65398fccc9 100644
--- a/spec/graphql/types/error_tracking/sentry_error_stack_trace_entry_type_spec.rb
+++ b/spec/graphql/types/error_tracking/sentry_error_stack_trace_entry_type_spec.rb
@@ -3,7 +3,7 @@
require 'spec_helper'
describe GitlabSchema.types['SentryErrorStackTraceEntry'] do
- it { expect(described_class.graphql_name).to eq('SentryErrorStackTraceEntry') }
+ specify { expect(described_class.graphql_name).to eq('SentryErrorStackTraceEntry') }
it 'exposes the expected fields' do
expected_fields = %i[
diff --git a/spec/graphql/types/error_tracking/sentry_error_stack_trace_type_spec.rb b/spec/graphql/types/error_tracking/sentry_error_stack_trace_type_spec.rb
index 2a422228f72..2cec8865764 100644
--- a/spec/graphql/types/error_tracking/sentry_error_stack_trace_type_spec.rb
+++ b/spec/graphql/types/error_tracking/sentry_error_stack_trace_type_spec.rb
@@ -3,9 +3,9 @@
require 'spec_helper'
describe GitlabSchema.types['SentryErrorStackTrace'] do
- it { expect(described_class.graphql_name).to eq('SentryErrorStackTrace') }
+ specify { expect(described_class.graphql_name).to eq('SentryErrorStackTrace') }
- it { expect(described_class).to require_graphql_authorizations(:read_sentry_issue) }
+ specify { expect(described_class).to require_graphql_authorizations(:read_sentry_issue) }
it 'exposes the expected fields' do
expected_fields = %i[
diff --git a/spec/graphql/types/error_tracking/sentry_error_type_spec.rb b/spec/graphql/types/error_tracking/sentry_error_type_spec.rb
index 4676d91ef9c..f8cc801e35e 100644
--- a/spec/graphql/types/error_tracking/sentry_error_type_spec.rb
+++ b/spec/graphql/types/error_tracking/sentry_error_type_spec.rb
@@ -3,7 +3,7 @@
require 'spec_helper'
describe GitlabSchema.types['SentryError'] do
- it { expect(described_class.graphql_name).to eq('SentryError') }
+ specify { expect(described_class.graphql_name).to eq('SentryError') }
it 'exposes the expected fields' do
expected_fields = %i[
diff --git a/spec/graphql/types/grafana_integration_type_spec.rb b/spec/graphql/types/grafana_integration_type_spec.rb
index ac26911acbf..429b5bdffe6 100644
--- a/spec/graphql/types/grafana_integration_type_spec.rb
+++ b/spec/graphql/types/grafana_integration_type_spec.rb
@@ -14,9 +14,9 @@ describe GitlabSchema.types['GrafanaIntegration'] do
]
end
- it { expect(described_class.graphql_name).to eq('GrafanaIntegration') }
+ specify { expect(described_class.graphql_name).to eq('GrafanaIntegration') }
- it { expect(described_class).to require_graphql_authorizations(:admin_operations) }
+ specify { expect(described_class).to require_graphql_authorizations(:admin_operations) }
- it { expect(described_class).to have_graphql_fields(*expected_fields) }
+ specify { expect(described_class).to have_graphql_fields(*expected_fields) }
end
diff --git a/spec/graphql/types/group_type_spec.rb b/spec/graphql/types/group_type_spec.rb
index 532f1a4b53d..a834a9038db 100644
--- a/spec/graphql/types/group_type_spec.rb
+++ b/spec/graphql/types/group_type_spec.rb
@@ -3,11 +3,11 @@
require 'spec_helper'
describe GitlabSchema.types['Group'] do
- it { expect(described_class).to expose_permissions_using(Types::PermissionTypes::Group) }
+ specify { expect(described_class).to expose_permissions_using(Types::PermissionTypes::Group) }
- it { expect(described_class.graphql_name).to eq('Group') }
+ specify { expect(described_class.graphql_name).to eq('Group') }
- it { expect(described_class).to require_graphql_authorizations(:read_group) }
+ specify { expect(described_class).to require_graphql_authorizations(:read_group) }
it 'has the expected fields' do
expected_fields = %w[
diff --git a/spec/graphql/types/issuable_sort_enum_spec.rb b/spec/graphql/types/issuable_sort_enum_spec.rb
new file mode 100644
index 00000000000..35c42d8194c
--- /dev/null
+++ b/spec/graphql/types/issuable_sort_enum_spec.rb
@@ -0,0 +1,15 @@
+# frozen_string_literal: true
+
+require 'spec_helper'
+
+describe Types::IssuableSortEnum do
+ specify { expect(described_class.graphql_name).to eq('IssuableSort') }
+
+ it 'exposes all the existing issuable sort values' do
+ expect(described_class.values.keys).to include(
+ *%w[PRIORITY_ASC PRIORITY_DESC
+ LABEL_PRIORITY_ASC LABEL_PRIORITY_DESC
+ MILESTONE_DUE_ASC MILESTONE_DUE_DESC]
+ )
+ end
+end
diff --git a/spec/graphql/types/issuable_state_enum_spec.rb b/spec/graphql/types/issuable_state_enum_spec.rb
index 65a80fa4176..f974ed5f5fb 100644
--- a/spec/graphql/types/issuable_state_enum_spec.rb
+++ b/spec/graphql/types/issuable_state_enum_spec.rb
@@ -3,7 +3,7 @@
require 'spec_helper'
describe GitlabSchema.types['IssuableState'] do
- it { expect(described_class.graphql_name).to eq('IssuableState') }
+ specify { expect(described_class.graphql_name).to eq('IssuableState') }
it_behaves_like 'issuable state'
end
diff --git a/spec/graphql/types/issue_sort_enum_spec.rb b/spec/graphql/types/issue_sort_enum_spec.rb
index 1b6aa6d6069..c496b897cdb 100644
--- a/spec/graphql/types/issue_sort_enum_spec.rb
+++ b/spec/graphql/types/issue_sort_enum_spec.rb
@@ -3,11 +3,13 @@
require 'spec_helper'
describe GitlabSchema.types['IssueSort'] do
- it { expect(described_class.graphql_name).to eq('IssueSort') }
+ specify { expect(described_class.graphql_name).to eq('IssueSort') }
it_behaves_like 'common sort values'
it 'exposes all the existing issue sort values' do
- expect(described_class.values.keys).to include(*%w[DUE_DATE_ASC DUE_DATE_DESC RELATIVE_POSITION_ASC])
+ expect(described_class.values.keys).to include(
+ *%w[DUE_DATE_ASC DUE_DATE_DESC RELATIVE_POSITION_ASC]
+ )
end
end
diff --git a/spec/graphql/types/issue_state_enum_spec.rb b/spec/graphql/types/issue_state_enum_spec.rb
index de19e6fc505..a18c5f5d317 100644
--- a/spec/graphql/types/issue_state_enum_spec.rb
+++ b/spec/graphql/types/issue_state_enum_spec.rb
@@ -3,7 +3,7 @@
require 'spec_helper'
describe GitlabSchema.types['IssueState'] do
- it { expect(described_class.graphql_name).to eq('IssueState') }
+ specify { expect(described_class.graphql_name).to eq('IssueState') }
it_behaves_like 'issuable state'
end
diff --git a/spec/graphql/types/issue_type_spec.rb b/spec/graphql/types/issue_type_spec.rb
index ebe48c17c11..a8f7edcfe8e 100644
--- a/spec/graphql/types/issue_type_spec.rb
+++ b/spec/graphql/types/issue_type_spec.rb
@@ -3,18 +3,19 @@
require 'spec_helper'
describe GitlabSchema.types['Issue'] do
- it { expect(described_class).to expose_permissions_using(Types::PermissionTypes::Issue) }
+ specify { expect(described_class).to expose_permissions_using(Types::PermissionTypes::Issue) }
- it { expect(described_class.graphql_name).to eq('Issue') }
+ specify { expect(described_class.graphql_name).to eq('Issue') }
- it { expect(described_class).to require_graphql_authorizations(:read_issue) }
+ specify { expect(described_class).to require_graphql_authorizations(:read_issue) }
- it { expect(described_class.interfaces).to include(Types::Notes::NoteableType) }
+ specify { expect(described_class.interfaces).to include(Types::Notes::NoteableType) }
it 'has specific fields' do
fields = %i[iid title description state reference author assignees participants labels milestone due_date
confidential discussion_locked upvotes downvotes user_notes_count web_path web_url relative_position
- subscribed time_estimate total_time_spent closed_at created_at updated_at task_completion_status]
+ subscribed time_estimate total_time_spent closed_at created_at updated_at task_completion_status
+ designs design_collection]
fields.each do |field_name|
expect(described_class).to have_graphql_field(field_name)
diff --git a/spec/graphql/types/jira_import_type_spec.rb b/spec/graphql/types/jira_import_type_spec.rb
index 8448a120682..ac1aa672e30 100644
--- a/spec/graphql/types/jira_import_type_spec.rb
+++ b/spec/graphql/types/jira_import_type_spec.rb
@@ -3,9 +3,9 @@
require 'spec_helper'
describe GitlabSchema.types['JiraImport'] do
- it { expect(described_class.graphql_name).to eq('JiraImport') }
+ specify { expect(described_class.graphql_name).to eq('JiraImport') }
it 'has the expected fields' do
- expect(described_class).to have_graphql_fields(:jira_project_key, :scheduled_at, :scheduled_by)
+ expect(described_class).to have_graphql_fields(:jira_project_key, :createdAt, :scheduled_at, :scheduled_by)
end
end
diff --git a/spec/graphql/types/label_type_spec.rb b/spec/graphql/types/label_type_spec.rb
index 71b86d9b528..026c63906ef 100644
--- a/spec/graphql/types/label_type_spec.rb
+++ b/spec/graphql/types/label_type_spec.rb
@@ -8,5 +8,5 @@ describe GitlabSchema.types['Label'] do
expect(described_class).to have_graphql_fields(*expected_fields)
end
- it { expect(described_class).to require_graphql_authorizations(:read_label) }
+ specify { expect(described_class).to require_graphql_authorizations(:read_label) }
end
diff --git a/spec/graphql/types/merge_request_state_enum_spec.rb b/spec/graphql/types/merge_request_state_enum_spec.rb
index 626e33b18d3..2abc7b298b1 100644
--- a/spec/graphql/types/merge_request_state_enum_spec.rb
+++ b/spec/graphql/types/merge_request_state_enum_spec.rb
@@ -3,7 +3,7 @@
require 'spec_helper'
describe GitlabSchema.types['MergeRequestState'] do
- it { expect(described_class.graphql_name).to eq('MergeRequestState') }
+ specify { expect(described_class.graphql_name).to eq('MergeRequestState') }
it_behaves_like 'issuable state'
diff --git a/spec/graphql/types/merge_request_type_spec.rb b/spec/graphql/types/merge_request_type_spec.rb
index 0c83ebd3de9..e7ab2100084 100644
--- a/spec/graphql/types/merge_request_type_spec.rb
+++ b/spec/graphql/types/merge_request_type_spec.rb
@@ -3,11 +3,11 @@
require 'spec_helper'
describe GitlabSchema.types['MergeRequest'] do
- it { expect(described_class).to expose_permissions_using(Types::PermissionTypes::MergeRequest) }
+ specify { expect(described_class).to expose_permissions_using(Types::PermissionTypes::MergeRequest) }
- it { expect(described_class).to require_graphql_authorizations(:read_merge_request) }
+ specify { expect(described_class).to require_graphql_authorizations(:read_merge_request) }
- it { expect(described_class.interfaces).to include(Types::Notes::NoteableType) }
+ specify { expect(described_class.interfaces).to include(Types::Notes::NoteableType) }
it 'has the expected fields' do
expected_fields = %w[
diff --git a/spec/graphql/types/metadata_type_spec.rb b/spec/graphql/types/metadata_type_spec.rb
index c8270a8c2f5..75369ec9c3c 100644
--- a/spec/graphql/types/metadata_type_spec.rb
+++ b/spec/graphql/types/metadata_type_spec.rb
@@ -3,6 +3,6 @@
require 'spec_helper'
describe GitlabSchema.types['Metadata'] do
- it { expect(described_class.graphql_name).to eq('Metadata') }
- it { expect(described_class).to require_graphql_authorizations(:read_instance_metadata) }
+ specify { expect(described_class.graphql_name).to eq('Metadata') }
+ specify { expect(described_class).to require_graphql_authorizations(:read_instance_metadata) }
end
diff --git a/spec/graphql/types/metrics/dashboard_type_spec.rb b/spec/graphql/types/metrics/dashboard_type_spec.rb
index 76f2b4b8935..81219c596a7 100644
--- a/spec/graphql/types/metrics/dashboard_type_spec.rb
+++ b/spec/graphql/types/metrics/dashboard_type_spec.rb
@@ -3,7 +3,7 @@
require 'spec_helper'
describe GitlabSchema.types['MetricsDashboard'] do
- it { expect(described_class.graphql_name).to eq('MetricsDashboard') }
+ specify { expect(described_class.graphql_name).to eq('MetricsDashboard') }
it 'has the expected fields' do
expected_fields = %w[
diff --git a/spec/graphql/types/metrics/dashboards/annotation_type_spec.rb b/spec/graphql/types/metrics/dashboards/annotation_type_spec.rb
index 2956a2512eb..dbb8b04dbd7 100644
--- a/spec/graphql/types/metrics/dashboards/annotation_type_spec.rb
+++ b/spec/graphql/types/metrics/dashboards/annotation_type_spec.rb
@@ -3,7 +3,7 @@
require 'spec_helper'
describe GitlabSchema.types['MetricsDashboardAnnotation'] do
- it { expect(described_class.graphql_name).to eq('MetricsDashboardAnnotation') }
+ specify { expect(described_class.graphql_name).to eq('MetricsDashboardAnnotation') }
it 'has the expected fields' do
expected_fields = %w[
@@ -13,5 +13,5 @@ describe GitlabSchema.types['MetricsDashboardAnnotation'] do
expect(described_class).to have_graphql_fields(*expected_fields)
end
- it { expect(described_class).to require_graphql_authorizations(:read_metrics_dashboard_annotation) }
+ specify { expect(described_class).to require_graphql_authorizations(:read_metrics_dashboard_annotation) }
end
diff --git a/spec/graphql/types/milestone_type_spec.rb b/spec/graphql/types/milestone_type_spec.rb
index f7ee79eae9f..4c3d9f50a64 100644
--- a/spec/graphql/types/milestone_type_spec.rb
+++ b/spec/graphql/types/milestone_type_spec.rb
@@ -3,7 +3,7 @@
require 'spec_helper'
describe GitlabSchema.types['Milestone'] do
- it { expect(described_class.graphql_name).to eq('Milestone') }
+ specify { expect(described_class.graphql_name).to eq('Milestone') }
- it { expect(described_class).to require_graphql_authorizations(:read_milestone) }
+ specify { expect(described_class).to require_graphql_authorizations(:read_milestone) }
end
diff --git a/spec/graphql/types/namespace_type_spec.rb b/spec/graphql/types/namespace_type_spec.rb
index 6c2ba70cf4c..741698021e7 100644
--- a/spec/graphql/types/namespace_type_spec.rb
+++ b/spec/graphql/types/namespace_type_spec.rb
@@ -3,7 +3,7 @@
require 'spec_helper'
describe GitlabSchema.types['Namespace'] do
- it { expect(described_class.graphql_name).to eq('Namespace') }
+ specify { expect(described_class.graphql_name).to eq('Namespace') }
it 'has the expected fields' do
expected_fields = %w[
@@ -14,5 +14,5 @@ describe GitlabSchema.types['Namespace'] do
expect(described_class).to have_graphql_fields(*expected_fields)
end
- it { expect(described_class).to require_graphql_authorizations(:read_namespace) }
+ specify { expect(described_class).to require_graphql_authorizations(:read_namespace) }
end
diff --git a/spec/graphql/types/notes/discussion_type_spec.rb b/spec/graphql/types/notes/discussion_type_spec.rb
index 804785ba67d..44774594d17 100644
--- a/spec/graphql/types/notes/discussion_type_spec.rb
+++ b/spec/graphql/types/notes/discussion_type_spec.rb
@@ -2,7 +2,7 @@
require 'spec_helper'
describe GitlabSchema.types['Discussion'] do
- it { expect(described_class).to have_graphql_fields(:id, :created_at, :notes, :reply_id) }
+ specify { expect(described_class).to have_graphql_fields(:id, :created_at, :notes, :reply_id) }
- it { expect(described_class).to require_graphql_authorizations(:read_note) }
+ specify { expect(described_class).to require_graphql_authorizations(:read_note) }
end
diff --git a/spec/graphql/types/notes/note_type_spec.rb b/spec/graphql/types/notes/note_type_spec.rb
index 8cf84cd8dfd..019f742ee77 100644
--- a/spec/graphql/types/notes/note_type_spec.rb
+++ b/spec/graphql/types/notes/note_type_spec.rb
@@ -10,6 +10,6 @@ describe GitlabSchema.types['Note'] do
expect(described_class).to have_graphql_fields(*expected_fields)
end
- it { expect(described_class).to expose_permissions_using(Types::PermissionTypes::Note) }
- it { expect(described_class).to require_graphql_authorizations(:read_note) }
+ specify { expect(described_class).to expose_permissions_using(Types::PermissionTypes::Note) }
+ specify { expect(described_class).to require_graphql_authorizations(:read_note) }
end
diff --git a/spec/graphql/types/notes/noteable_type_spec.rb b/spec/graphql/types/notes/noteable_type_spec.rb
index a4259e160e0..4a81f45bd4e 100644
--- a/spec/graphql/types/notes/noteable_type_spec.rb
+++ b/spec/graphql/types/notes/noteable_type_spec.rb
@@ -2,12 +2,13 @@
require 'spec_helper'
describe Types::Notes::NoteableType do
- it { expect(described_class).to have_graphql_fields(:notes, :discussions) }
+ specify { expect(described_class).to have_graphql_fields(:notes, :discussions) }
describe ".resolve_type" do
it 'knows the correct type for objects' do
expect(described_class.resolve_type(build(:issue), {})).to eq(Types::IssueType)
expect(described_class.resolve_type(build(:merge_request), {})).to eq(Types::MergeRequestType)
+ expect(described_class.resolve_type(build(:design), {})).to eq(Types::DesignManagement::DesignType)
end
end
end
diff --git a/spec/graphql/types/permission_types/issue_spec.rb b/spec/graphql/types/permission_types/issue_spec.rb
index a94bc6b780e..a7a3dd00f11 100644
--- a/spec/graphql/types/permission_types/issue_spec.rb
+++ b/spec/graphql/types/permission_types/issue_spec.rb
@@ -5,8 +5,9 @@ require 'spec_helper'
describe Types::PermissionTypes::Issue do
it do
expected_permissions = [
- :read_issue, :admin_issue, :update_issue,
- :create_note, :reopen_issue
+ :read_issue, :admin_issue, :update_issue, :reopen_issue,
+ :read_design, :create_design, :destroy_design,
+ :create_note
]
expected_permissions.each do |permission|
diff --git a/spec/graphql/types/permission_types/merge_request_type_spec.rb b/spec/graphql/types/permission_types/merge_request_type_spec.rb
index 572b4ac42d0..7e9752cdc46 100644
--- a/spec/graphql/types/permission_types/merge_request_type_spec.rb
+++ b/spec/graphql/types/permission_types/merge_request_type_spec.rb
@@ -3,5 +3,5 @@
require 'spec_helper'
describe Types::MergeRequestType do
- it { expect(described_class).to expose_permissions_using(Types::PermissionTypes::MergeRequest) }
+ specify { expect(described_class).to expose_permissions_using(Types::PermissionTypes::MergeRequest) }
end
diff --git a/spec/graphql/types/permission_types/project_spec.rb b/spec/graphql/types/permission_types/project_spec.rb
index 56c4c2de4df..2789464d29c 100644
--- a/spec/graphql/types/permission_types/project_spec.rb
+++ b/spec/graphql/types/permission_types/project_spec.rb
@@ -13,7 +13,7 @@ describe Types::PermissionTypes::Project do
:create_merge_request_from, :create_wiki, :push_code, :create_deployment, :push_to_delete_protected_branch,
:admin_wiki, :admin_project, :update_pages, :admin_remote_mirror, :create_label,
:update_wiki, :destroy_wiki, :create_pages, :destroy_pages, :read_pages_content,
- :read_merge_request
+ :read_merge_request, :read_design, :create_design, :destroy_design
]
expected_permissions.each do |permission|
diff --git a/spec/graphql/types/project_type_spec.rb b/spec/graphql/types/project_type_spec.rb
index 6ea852190c9..6368f743720 100644
--- a/spec/graphql/types/project_type_spec.rb
+++ b/spec/graphql/types/project_type_spec.rb
@@ -3,11 +3,11 @@
require 'spec_helper'
describe GitlabSchema.types['Project'] do
- it { expect(described_class).to expose_permissions_using(Types::PermissionTypes::Project) }
+ specify { expect(described_class).to expose_permissions_using(Types::PermissionTypes::Project) }
- it { expect(described_class.graphql_name).to eq('Project') }
+ specify { expect(described_class.graphql_name).to eq('Project') }
- it { expect(described_class).to require_graphql_authorizations(:read_project) }
+ specify { expect(described_class).to require_graphql_authorizations(:read_project) }
it 'has the expected fields' do
expected_fields = %w[
@@ -24,7 +24,8 @@ describe GitlabSchema.types['Project'] do
namespace group statistics repository merge_requests merge_request issues
issue pipelines removeSourceBranchAfterMerge sentryDetailedError snippets
grafanaIntegration autocloseReferencedIssues suggestion_commit_message environments
- boards jira_import_status jira_imports services
+ boards jira_import_status jira_imports services releases release
+ alert_management_alerts alert_management_alert alert_management_alert_status_counts
]
expect(described_class).to include_graphql_fields(*expected_fields)
@@ -96,4 +97,18 @@ describe GitlabSchema.types['Project'] do
it { is_expected.to have_graphql_type(Types::Projects::ServiceType.connection_type) }
end
+
+ describe 'releases field' do
+ subject { described_class.fields['release'] }
+
+ it { is_expected.to have_graphql_type(Types::ReleaseType) }
+ it { is_expected.to have_graphql_resolver(Resolvers::ReleaseResolver) }
+ end
+
+ describe 'release field' do
+ subject { described_class.fields['releases'] }
+
+ it { is_expected.to have_graphql_type(Types::ReleaseType.connection_type) }
+ it { is_expected.to have_graphql_resolver(Resolvers::ReleasesResolver) }
+ end
end
diff --git a/spec/graphql/types/projects/base_service_type_spec.rb b/spec/graphql/types/projects/base_service_type_spec.rb
index bda6022bf79..4fcb9fe1a73 100644
--- a/spec/graphql/types/projects/base_service_type_spec.rb
+++ b/spec/graphql/types/projects/base_service_type_spec.rb
@@ -3,11 +3,11 @@
require 'spec_helper'
describe GitlabSchema.types['BaseService'] do
- it { expect(described_class.graphql_name).to eq('BaseService') }
+ specify { expect(described_class.graphql_name).to eq('BaseService') }
it 'has basic expected fields' do
expect(described_class).to have_graphql_fields(:type, :active)
end
- it { expect(described_class).to require_graphql_authorizations(:admin_project) }
+ specify { expect(described_class).to require_graphql_authorizations(:admin_project) }
end
diff --git a/spec/graphql/types/projects/jira_service_type_spec.rb b/spec/graphql/types/projects/jira_service_type_spec.rb
index 7f8fa6538e9..91d7e4586cb 100644
--- a/spec/graphql/types/projects/jira_service_type_spec.rb
+++ b/spec/graphql/types/projects/jira_service_type_spec.rb
@@ -3,11 +3,11 @@
require 'spec_helper'
describe GitlabSchema.types['JiraService'] do
- it { expect(described_class.graphql_name).to eq('JiraService') }
+ specify { expect(described_class.graphql_name).to eq('JiraService') }
it 'has basic expected fields' do
expect(described_class).to have_graphql_fields(:type, :active)
end
- it { expect(described_class).to require_graphql_authorizations(:admin_project) }
+ specify { expect(described_class).to require_graphql_authorizations(:admin_project) }
end
diff --git a/spec/graphql/types/projects/service_type_spec.rb b/spec/graphql/types/projects/service_type_spec.rb
index ad30a4008bc..f6758d17d18 100644
--- a/spec/graphql/types/projects/service_type_spec.rb
+++ b/spec/graphql/types/projects/service_type_spec.rb
@@ -3,7 +3,7 @@
require 'spec_helper'
describe Types::Projects::ServiceType do
- it { expect(described_class).to have_graphql_fields(:type, :active) }
+ specify { expect(described_class).to have_graphql_fields(:type, :active) }
describe ".resolve_type" do
it 'resolves the corresponding type for objects' do
diff --git a/spec/graphql/types/projects/services_enum_spec.rb b/spec/graphql/types/projects/services_enum_spec.rb
index aac4aae4f69..91e398e8d81 100644
--- a/spec/graphql/types/projects/services_enum_spec.rb
+++ b/spec/graphql/types/projects/services_enum_spec.rb
@@ -3,7 +3,7 @@
require 'spec_helper'
describe GitlabSchema.types['ServiceType'] do
- it { expect(described_class.graphql_name).to eq('ServiceType') }
+ specify { expect(described_class.graphql_name).to eq('ServiceType') }
it 'exposes all the existing project services' do
expect(described_class.values.keys).to match_array(available_services_enum)
diff --git a/spec/graphql/types/query_type_spec.rb b/spec/graphql/types/query_type_spec.rb
index ab210f2e918..1f269a80d00 100644
--- a/spec/graphql/types/query_type_spec.rb
+++ b/spec/graphql/types/query_type_spec.rb
@@ -8,7 +8,7 @@ describe GitlabSchema.types['Query'] do
end
it 'has the expected fields' do
- expected_fields = %i[project namespace group echo metadata current_user snippets]
+ expected_fields = %i[project namespace group echo metadata current_user snippets design_management]
expect(described_class).to have_graphql_fields(*expected_fields).at_least
end
diff --git a/spec/graphql/types/release_type_spec.rb b/spec/graphql/types/release_type_spec.rb
new file mode 100644
index 00000000000..d22a0b4f0fa
--- /dev/null
+++ b/spec/graphql/types/release_type_spec.rb
@@ -0,0 +1,37 @@
+# frozen_string_literal: true
+
+require 'spec_helper'
+
+describe GitlabSchema.types['Release'] do
+ it { expect(described_class).to require_graphql_authorizations(:read_release) }
+
+ it 'has the expected fields' do
+ expected_fields = %w[
+ tag_name tag_path
+ description description_html
+ name milestones author commit
+ created_at released_at
+ ]
+
+ expect(described_class).to include_graphql_fields(*expected_fields)
+ end
+
+ describe 'milestones field' do
+ subject { described_class.fields['milestones'] }
+
+ it { is_expected.to have_graphql_type(Types::MilestoneType.connection_type) }
+ end
+
+ describe 'author field' do
+ subject { described_class.fields['author'] }
+
+ it { is_expected.to have_graphql_type(Types::UserType) }
+ end
+
+ describe 'commit field' do
+ subject { described_class.fields['commit'] }
+
+ it { is_expected.to have_graphql_type(Types::CommitType) }
+ it { is_expected.to require_graphql_authorizations(:reporter_access) }
+ end
+end
diff --git a/spec/graphql/types/repository_type_spec.rb b/spec/graphql/types/repository_type_spec.rb
index f746e75b574..fb52839c712 100644
--- a/spec/graphql/types/repository_type_spec.rb
+++ b/spec/graphql/types/repository_type_spec.rb
@@ -3,11 +3,11 @@
require 'spec_helper'
describe GitlabSchema.types['Repository'] do
- it { expect(described_class.graphql_name).to eq('Repository') }
+ specify { expect(described_class.graphql_name).to eq('Repository') }
- it { expect(described_class).to require_graphql_authorizations(:download_code) }
+ specify { expect(described_class).to require_graphql_authorizations(:download_code) }
- it { expect(described_class).to have_graphql_field(:root_ref) }
+ specify { expect(described_class).to have_graphql_field(:root_ref) }
- it { expect(described_class).to have_graphql_field(:tree) }
+ specify { expect(described_class).to have_graphql_field(:tree) }
end
diff --git a/spec/graphql/types/root_storage_statistics_type_spec.rb b/spec/graphql/types/root_storage_statistics_type_spec.rb
index b796b974b82..ebaa5a18623 100644
--- a/spec/graphql/types/root_storage_statistics_type_spec.rb
+++ b/spec/graphql/types/root_storage_statistics_type_spec.rb
@@ -3,12 +3,12 @@
require 'spec_helper'
describe GitlabSchema.types['RootStorageStatistics'] do
- it { expect(described_class.graphql_name).to eq('RootStorageStatistics') }
+ specify { expect(described_class.graphql_name).to eq('RootStorageStatistics') }
it 'has all the required fields' do
expect(described_class).to have_graphql_fields(:storage_size, :repository_size, :lfs_objects_size,
:build_artifacts_size, :packages_size, :wiki_size)
end
- it { expect(described_class).to require_graphql_authorizations(:read_statistics) }
+ specify { expect(described_class).to require_graphql_authorizations(:read_statistics) }
end
diff --git a/spec/graphql/types/snippet_type_spec.rb b/spec/graphql/types/snippet_type_spec.rb
index 6e580711fda..adc13d4d651 100644
--- a/spec/graphql/types/snippet_type_spec.rb
+++ b/spec/graphql/types/snippet_type_spec.rb
@@ -17,7 +17,7 @@ describe GitlabSchema.types['Snippet'] do
end
describe 'authorizations' do
- it { expect(described_class).to require_graphql_authorizations(:read_snippet) }
+ specify { expect(described_class).to require_graphql_authorizations(:read_snippet) }
end
shared_examples 'response without repository URLs' do
@@ -35,14 +35,6 @@ describe GitlabSchema.types['Snippet'] do
expect(response['sshUrlToRepo']).to eq(snippet.ssh_url_to_repo)
expect(response['httpUrlToRepo']).to eq(snippet.http_url_to_repo)
end
-
- context 'when version_snippets feature is disabled' do
- before do
- stub_feature_flags(version_snippets: false)
- end
-
- it_behaves_like 'response without repository URLs'
- end
end
end
diff --git a/spec/graphql/types/snippets/blob_type_spec.rb b/spec/graphql/types/snippets/blob_type_spec.rb
index da36ab80f44..fb8c6896732 100644
--- a/spec/graphql/types/snippets/blob_type_spec.rb
+++ b/spec/graphql/types/snippets/blob_type_spec.rb
@@ -6,8 +6,22 @@ describe GitlabSchema.types['SnippetBlob'] do
it 'has the correct fields' do
expected_fields = [:rich_data, :plain_data,
:raw_path, :size, :binary, :name, :path,
- :simple_viewer, :rich_viewer, :mode]
+ :simple_viewer, :rich_viewer, :mode, :external_storage,
+ :rendered_as_text]
expect(described_class).to have_graphql_fields(*expected_fields)
end
+
+ specify { expect(described_class.fields['richData'].type).not_to be_non_null }
+ specify { expect(described_class.fields['plainData'].type).not_to be_non_null }
+ specify { expect(described_class.fields['rawPath'].type).to be_non_null }
+ specify { expect(described_class.fields['size'].type).to be_non_null }
+ specify { expect(described_class.fields['binary'].type).to be_non_null }
+ specify { expect(described_class.fields['name'].type).not_to be_non_null }
+ specify { expect(described_class.fields['path'].type).not_to be_non_null }
+ specify { expect(described_class.fields['simpleViewer'].type).to be_non_null }
+ specify { expect(described_class.fields['richViewer'].type).not_to be_non_null }
+ specify { expect(described_class.fields['mode'].type).not_to be_non_null }
+ specify { expect(described_class.fields['externalStorage'].type).not_to be_non_null }
+ specify { expect(described_class.fields['renderedAsText'].type).to be_non_null }
end
diff --git a/spec/graphql/types/snippets/blob_viewer_type_spec.rb b/spec/graphql/types/snippets/blob_viewer_type_spec.rb
index a51d09813ab..841e22451db 100644
--- a/spec/graphql/types/snippets/blob_viewer_type_spec.rb
+++ b/spec/graphql/types/snippets/blob_viewer_type_spec.rb
@@ -3,10 +3,91 @@
require 'spec_helper'
describe GitlabSchema.types['SnippetBlobViewer'] do
+ let_it_be(:snippet) { create(:personal_snippet, :repository) }
+ let_it_be(:blob) { snippet.repository.blob_at('HEAD', 'files/images/6049019_460s.jpg') }
+
it 'has the correct fields' do
expected_fields = [:type, :load_async, :too_large, :collapsed,
:render_error, :file_type, :loading_partial_name]
expect(described_class).to have_graphql_fields(*expected_fields)
end
+
+ it { expect(described_class.fields['type'].type).to be_non_null }
+ it { expect(described_class.fields['loadAsync'].type).to be_non_null }
+ it { expect(described_class.fields['collapsed'].type).to be_non_null }
+ it { expect(described_class.fields['tooLarge'].type).to be_non_null }
+ it { expect(described_class.fields['renderError'].type).not_to be_non_null }
+ it { expect(described_class.fields['fileType'].type).to be_non_null }
+ it { expect(described_class.fields['loadingPartialName'].type).to be_non_null }
+
+ shared_examples 'nil field converted to false' do
+ subject { GitlabSchema.execute(query, context: { current_user: snippet.author }).as_json }
+
+ before do
+ allow_next_instance_of(SnippetPresenter) do |instance|
+ allow(instance).to receive(:blob).and_return(blob)
+ end
+ end
+
+ it 'returns false' do
+ snippet_blob = subject.dig('data', 'snippets', 'edges')[0].dig('node', 'blob')
+
+ expect(snippet_blob['path']).to eq blob.path
+ expect(blob_attribute).to be_nil
+ expect(snippet_blob['simpleViewer'][attribute]).to eq false
+ end
+ end
+
+ describe 'collapsed' do
+ it_behaves_like 'nil field converted to false' do
+ let(:query) do
+ %(
+ query {
+ snippets(ids:"#{snippet.to_global_id}"){
+ edges {
+ node {
+ blob {
+ path
+ simpleViewer {
+ collapsed
+ }
+ }
+ }
+ }
+ }
+ }
+ )
+ end
+
+ let(:attribute) { 'collapsed' }
+ let(:blob_attribute) { blob.simple_viewer.collapsed? }
+ end
+ end
+
+ describe 'tooLarge' do
+ it_behaves_like 'nil field converted to false' do
+ let(:query) do
+ %(
+ query {
+ snippets(ids:"#{snippet.to_global_id}"){
+ edges {
+ node {
+ blob {
+ path
+ simpleViewer {
+ tooLarge
+ }
+ }
+ }
+ }
+ }
+ }
+ )
+ end
+
+ let(:attribute) { 'tooLarge' }
+ let(:blob_attribute) { blob.simple_viewer.too_large? }
+ end
+ end
end
diff --git a/spec/graphql/types/time_type_spec.rb b/spec/graphql/types/time_type_spec.rb
index 88a535ed3bb..3c6e191e2fb 100644
--- a/spec/graphql/types/time_type_spec.rb
+++ b/spec/graphql/types/time_type_spec.rb
@@ -6,7 +6,7 @@ describe GitlabSchema.types['Time'] do
let(:iso) { "2018-06-04T15:23:50+02:00" }
let(:time) { Time.parse(iso) }
- it { expect(described_class.graphql_name).to eq('Time') }
+ specify { expect(described_class.graphql_name).to eq('Time') }
it 'coerces Time object into ISO 8601' do
expect(described_class.coerce_isolated_result(time)).to eq(iso)
diff --git a/spec/graphql/types/todo_type_spec.rb b/spec/graphql/types/todo_type_spec.rb
index 59118259d09..87a5405f0e2 100644
--- a/spec/graphql/types/todo_type_spec.rb
+++ b/spec/graphql/types/todo_type_spec.rb
@@ -9,5 +9,5 @@ describe GitlabSchema.types['Todo'] do
expect(described_class).to have_graphql_fields(*expected_fields)
end
- it { expect(described_class).to require_graphql_authorizations(:read_todo) }
+ specify { expect(described_class).to require_graphql_authorizations(:read_todo) }
end
diff --git a/spec/graphql/types/tree/blob_type_spec.rb b/spec/graphql/types/tree/blob_type_spec.rb
index 516c862b9c6..547a03b5edf 100644
--- a/spec/graphql/types/tree/blob_type_spec.rb
+++ b/spec/graphql/types/tree/blob_type_spec.rb
@@ -3,7 +3,7 @@
require 'spec_helper'
describe Types::Tree::BlobType do
- it { expect(described_class.graphql_name).to eq('Blob') }
+ specify { expect(described_class.graphql_name).to eq('Blob') }
- it { expect(described_class).to have_graphql_fields(:id, :sha, :name, :type, :path, :flat_path, :web_url, :lfs_oid) }
+ specify { expect(described_class).to have_graphql_fields(:id, :sha, :name, :type, :path, :flat_path, :web_url, :lfs_oid) }
end
diff --git a/spec/graphql/types/tree/submodule_type_spec.rb b/spec/graphql/types/tree/submodule_type_spec.rb
index 81f7ad825a1..b5cfe8eb812 100644
--- a/spec/graphql/types/tree/submodule_type_spec.rb
+++ b/spec/graphql/types/tree/submodule_type_spec.rb
@@ -3,7 +3,7 @@
require 'spec_helper'
describe Types::Tree::SubmoduleType do
- it { expect(described_class.graphql_name).to eq('Submodule') }
+ specify { expect(described_class.graphql_name).to eq('Submodule') }
- it { expect(described_class).to have_graphql_fields(:id, :sha, :name, :type, :path, :flat_path, :web_url, :tree_url) }
+ specify { expect(described_class).to have_graphql_fields(:id, :sha, :name, :type, :path, :flat_path, :web_url, :tree_url) }
end
diff --git a/spec/graphql/types/tree/tree_entry_type_spec.rb b/spec/graphql/types/tree/tree_entry_type_spec.rb
index 228a4be0949..14826d06645 100644
--- a/spec/graphql/types/tree/tree_entry_type_spec.rb
+++ b/spec/graphql/types/tree/tree_entry_type_spec.rb
@@ -3,7 +3,7 @@
require 'spec_helper'
describe Types::Tree::TreeEntryType do
- it { expect(described_class.graphql_name).to eq('TreeEntry') }
+ specify { expect(described_class.graphql_name).to eq('TreeEntry') }
- it { expect(described_class).to have_graphql_fields(:id, :sha, :name, :type, :path, :flat_path, :web_url) }
+ specify { expect(described_class).to have_graphql_fields(:id, :sha, :name, :type, :path, :flat_path, :web_url) }
end
diff --git a/spec/graphql/types/tree/tree_type_spec.rb b/spec/graphql/types/tree/tree_type_spec.rb
index 23779d75600..93faebd3602 100644
--- a/spec/graphql/types/tree/tree_type_spec.rb
+++ b/spec/graphql/types/tree/tree_type_spec.rb
@@ -3,7 +3,7 @@
require 'spec_helper'
describe Types::Tree::TreeType do
- it { expect(described_class.graphql_name).to eq('Tree') }
+ specify { expect(described_class.graphql_name).to eq('Tree') }
- it { expect(described_class).to have_graphql_fields(:trees, :submodules, :blobs, :last_commit) }
+ specify { expect(described_class).to have_graphql_fields(:trees, :submodules, :blobs, :last_commit) }
end
diff --git a/spec/graphql/types/tree/type_enum_spec.rb b/spec/graphql/types/tree/type_enum_spec.rb
index 4caf9e1c457..dcacd6073f9 100644
--- a/spec/graphql/types/tree/type_enum_spec.rb
+++ b/spec/graphql/types/tree/type_enum_spec.rb
@@ -3,7 +3,7 @@
require 'spec_helper'
describe Types::Tree::TypeEnum do
- it { expect(described_class.graphql_name).to eq('EntryType') }
+ specify { expect(described_class.graphql_name).to eq('EntryType') }
it 'exposes all tree entry types' do
expect(described_class.values.keys).to include(*%w[tree blob commit])
diff --git a/spec/graphql/types/user_type_spec.rb b/spec/graphql/types/user_type_spec.rb
index 8c76ce43e95..cf1e91afb80 100644
--- a/spec/graphql/types/user_type_spec.rb
+++ b/spec/graphql/types/user_type_spec.rb
@@ -3,13 +3,13 @@
require 'spec_helper'
describe GitlabSchema.types['User'] do
- it { expect(described_class.graphql_name).to eq('User') }
+ specify { expect(described_class.graphql_name).to eq('User') }
- it { expect(described_class).to require_graphql_authorizations(:read_user) }
+ specify { expect(described_class).to require_graphql_authorizations(:read_user) }
it 'has the expected fields' do
expected_fields = %w[
- user_permissions snippets name username avatarUrl webUrl todos
+ id user_permissions snippets name username avatarUrl webUrl todos state
]
expect(described_class).to have_graphql_fields(*expected_fields)
diff --git a/spec/haml_lint/linter/no_plain_nodes_spec.rb b/spec/haml_lint/linter/no_plain_nodes_spec.rb
index 08deb5a4e9e..dc647467db6 100644
--- a/spec/haml_lint/linter/no_plain_nodes_spec.rb
+++ b/spec/haml_lint/linter/no_plain_nodes_spec.rb
@@ -53,4 +53,42 @@ describe HamlLint::Linter::NoPlainNodes do
it { is_expected.to report_lint count: 3 }
end
+
+ context 'does not report when a html entity' do
+ let(:haml) { '%tag &nbsp;' }
+
+ it { is_expected.not_to report_lint }
+ end
+
+ context 'does report when something that looks like a html entity' do
+ let(:haml) { '%tag &some text;' }
+
+ it { is_expected.to report_lint }
+ end
+
+ context 'does not report multiline when one or more html entities' do
+ %w(&nbsp;&gt; &#x000A9; &#187;).each do |elem|
+ let(:haml) { <<-HAML }
+ %tag
+ #{elem}
+ HAML
+
+ it elem do
+ is_expected.not_to report_lint
+ end
+ end
+ end
+
+ context 'does report multiline when one or more html entities amidst plain text' do
+ %w(&nbsp;Test Test&gt; &#x000A9;Hello &nbsp;Hello&#187;).each do |elem|
+ let(:haml) { <<-HAML }
+ %tag
+ #{elem}
+ HAML
+
+ it elem do
+ is_expected.to report_lint
+ end
+ end
+ end
end
diff --git a/spec/helpers/access_tokens_helper_spec.rb b/spec/helpers/access_tokens_helper_spec.rb
new file mode 100644
index 00000000000..1d246d3f236
--- /dev/null
+++ b/spec/helpers/access_tokens_helper_spec.rb
@@ -0,0 +1,18 @@
+# frozen_string_literal: true
+
+require "spec_helper"
+
+describe AccessTokensHelper do
+ describe "#scope_description" do
+ using RSpec::Parameterized::TableSyntax
+
+ where(:prefix, :description_location) do
+ :personal_access_token | [:doorkeeper, :scope_desc]
+ :project_access_token | [:doorkeeper, :project_access_token_scope_desc]
+ end
+
+ with_them do
+ it { expect(helper.scope_description(prefix)).to eq(description_location) }
+ end
+ end
+end
diff --git a/spec/helpers/application_helper_spec.rb b/spec/helpers/application_helper_spec.rb
index a96046735c8..05231cc6d09 100644
--- a/spec/helpers/application_helper_spec.rb
+++ b/spec/helpers/application_helper_spec.rb
@@ -71,6 +71,28 @@ describe ApplicationHelper do
end
end
+ describe '#admin_section?' do
+ context 'when controller is under the admin namespace' do
+ before do
+ allow(helper).to receive(:controller).and_return(Admin::UsersController.new)
+ end
+
+ it 'returns true' do
+ expect(helper.admin_section?).to eq(true)
+ end
+ end
+
+ context 'when controller is not under the admin namespace' do
+ before do
+ allow(helper).to receive(:controller).and_return(UsersController.new)
+ end
+
+ it 'returns true' do
+ expect(helper.admin_section?).to eq(false)
+ end
+ end
+ end
+
describe 'simple_sanitize' do
let(:a_tag) { '<a href="#">Foo</a>' }
@@ -90,8 +112,11 @@ describe ApplicationHelper do
end
describe 'time_ago_with_tooltip' do
+ around do |example|
+ Time.use_zone('UTC') { example.run }
+ end
+
def element(*arguments)
- Time.zone = 'UTC'
@time = Time.zone.parse('2015-07-02 08:23')
element = helper.time_ago_with_tooltip(@time, *arguments)
diff --git a/spec/helpers/auth_helper_spec.rb b/spec/helpers/auth_helper_spec.rb
index 1764a2bbc3c..23f3449d9a7 100644
--- a/spec/helpers/auth_helper_spec.rb
+++ b/spec/helpers/auth_helper_spec.rb
@@ -184,4 +184,40 @@ describe AuthHelper do
end
end
end
+
+ describe '#allow_admin_mode_password_authentication_for_web?' do
+ let(:user) { create(:user) }
+
+ subject { helper.allow_admin_mode_password_authentication_for_web? }
+
+ before do
+ allow(helper).to receive(:current_user).and_return(user)
+ end
+
+ it { is_expected.to be(true) }
+
+ context 'when password authentication for web is disabled' do
+ before do
+ stub_application_setting(password_authentication_enabled_for_web: false)
+ end
+
+ it { is_expected.to be(false) }
+ end
+
+ context 'when current_user is an ldap user' do
+ before do
+ allow(user).to receive(:ldap_user?).and_return(true)
+ end
+
+ it { is_expected.to be(false) }
+ end
+
+ context 'when user got password automatically set' do
+ before do
+ user.update_attribute(:password_automatically_set, true)
+ end
+
+ it { is_expected.to be(false) }
+ end
+ end
end
diff --git a/spec/helpers/boards_helper_spec.rb b/spec/helpers/boards_helper_spec.rb
index f5e5285554c..cb9be9d5fb4 100644
--- a/spec/helpers/boards_helper_spec.rb
+++ b/spec/helpers/boards_helper_spec.rb
@@ -48,6 +48,10 @@ describe BoardsHelper do
it 'returns a board_lists_path as lists_endpoint' do
expect(helper.board_data[:lists_endpoint]).to eq(board_lists_path(board))
end
+
+ it 'returns board type as parent' do
+ expect(helper.board_data[:parent]).to eq('project')
+ end
end
describe '#current_board_json' do
diff --git a/spec/helpers/clusters_helper_spec.rb b/spec/helpers/clusters_helper_spec.rb
index eec62bbb990..d40ed2248ce 100644
--- a/spec/helpers/clusters_helper_spec.rb
+++ b/spec/helpers/clusters_helper_spec.rb
@@ -59,6 +59,32 @@ describe ClustersHelper do
end
end
+ describe '#provider_icon' do
+ it 'will return GCP logo with gcp argument' do
+ logo = helper.provider_icon('gcp')
+
+ expect(logo).to match(%r(img alt="Google GKE" data-src="|/illustrations/logos/google_gke|svg))
+ end
+
+ it 'will return AWS logo with aws argument' do
+ logo = helper.provider_icon('aws')
+
+ expect(logo).to match(%r(img alt="Amazon EKS" data-src="|/illustrations/logos/amazon_eks|svg))
+ end
+
+ it 'will return default logo with unknown provider' do
+ logo = helper.provider_icon('unknown')
+
+ expect(logo).to match(%r(img alt="Kubernetes Cluster" data-src="|/illustrations/logos/kubernetes|svg))
+ end
+
+ it 'will return default logo when provider is empty' do
+ logo = helper.provider_icon
+
+ expect(logo).to match(%r(img alt="Kubernetes Cluster" data-src="|/illustrations/logos/kubernetes|svg))
+ end
+ end
+
describe '#cluster_type_label' do
subject { helper.cluster_type_label(cluster_type) }
diff --git a/spec/helpers/commits_helper_spec.rb b/spec/helpers/commits_helper_spec.rb
index dd268c2411f..e036e97f745 100644
--- a/spec/helpers/commits_helper_spec.rb
+++ b/spec/helpers/commits_helper_spec.rb
@@ -82,4 +82,32 @@ describe CommitsHelper do
expect(helper.commit_to_html(commit, ref, project)).to include('<div class="commit-content')
end
end
+
+ describe 'commit_path' do
+ it 'returns a persisted merge request commit path' do
+ project = create(:project, :repository)
+ persisted_merge_request = create(:merge_request, source_project: project, target_project: project)
+ commit = project.repository.commit
+
+ expect(helper.commit_path(persisted_merge_request.project, commit, merge_request: persisted_merge_request))
+ .to eq(diffs_project_merge_request_path(project, persisted_merge_request, commit_id: commit.id))
+ end
+
+ it 'returns a non-persisted merge request commit path which commits still reside in the source project' do
+ source_project = create(:project, :repository)
+ target_project = create(:project, :repository)
+ non_persisted_merge_request = build(:merge_request, source_project: source_project, target_project: target_project)
+ commit = source_project.repository.commit
+
+ expect(helper.commit_path(non_persisted_merge_request.project, commit, merge_request: non_persisted_merge_request))
+ .to eq(project_commit_path(source_project, commit))
+ end
+
+ it 'returns a project commit path' do
+ project = create(:project, :repository)
+ commit = project.repository.commit
+
+ expect(helper.commit_path(project, commit)).to eq(project_commit_path(project, commit))
+ end
+ end
end
diff --git a/spec/helpers/environments_helper_spec.rb b/spec/helpers/environments_helper_spec.rb
index 152e9c84ec5..0756e0162a5 100644
--- a/spec/helpers/environments_helper_spec.rb
+++ b/spec/helpers/environments_helper_spec.rb
@@ -37,10 +37,26 @@ describe EnvironmentsHelper do
'environment-state' => environment.state,
'custom-metrics-path' => project_prometheus_metrics_path(project),
'validate-query-path' => validate_query_project_prometheus_metrics_path(project),
- 'custom-metrics-available' => 'true'
+ 'custom-metrics-available' => 'true',
+ 'alerts-endpoint' => project_prometheus_alerts_path(project, environment_id: environment.id, format: :json),
+ 'prometheus-alerts-available' => 'true'
)
end
+ context 'without read_prometheus_alerts permission' do
+ before do
+ allow(helper).to receive(:can?)
+ .with(user, :read_prometheus_alerts, project)
+ .and_return(false)
+ end
+
+ it 'returns false' do
+ expect(metrics_data).to include(
+ 'prometheus-alerts-available' => 'false'
+ )
+ end
+ end
+
context 'with metrics_setting' do
before do
create(:project_metrics_setting, project: project, external_dashboard_url: 'http://gitlab.com')
diff --git a/spec/helpers/events_helper_spec.rb b/spec/helpers/events_helper_spec.rb
index ff99f76eb4d..12519390137 100644
--- a/spec/helpers/events_helper_spec.rb
+++ b/spec/helpers/events_helper_spec.rb
@@ -3,6 +3,8 @@
require 'spec_helper'
describe EventsHelper do
+ include Gitlab::Routing
+
describe '#event_commit_title' do
let(:message) { 'foo & bar ' + 'A' * 70 + '\n' + 'B' * 80 }
@@ -197,5 +199,17 @@ describe EventsHelper do
expect(subject).to eq("#{project_base_url}/-/merge_requests/#{event.note_target.iid}#note_#{event.target.id}")
end
+
+ context 'for design note events' do
+ let(:event) { create(:event, :for_design, project: project) }
+
+ it 'returns an appropriate URL' do
+ iid = event.note_target.issue.iid
+ filename = event.note_target.filename
+ note_id = event.target.id
+
+ expect(subject).to eq("#{project_base_url}/-/issues/#{iid}/designs/#{filename}#note_#{note_id}")
+ end
+ end
end
end
diff --git a/spec/helpers/export_helper_spec.rb b/spec/helpers/export_helper_spec.rb
new file mode 100644
index 00000000000..3fbda441b5d
--- /dev/null
+++ b/spec/helpers/export_helper_spec.rb
@@ -0,0 +1,11 @@
+# frozen_string_literal: true
+
+require 'spec_helper'
+
+describe ExportHelper do
+ describe '#project_export_descriptions' do
+ it 'includes design management' do
+ expect(project_export_descriptions).to include('Design Management files and data')
+ end
+ end
+end
diff --git a/spec/helpers/groups_helper_spec.rb b/spec/helpers/groups_helper_spec.rb
index ac2f028f937..5be247c5b49 100644
--- a/spec/helpers/groups_helper_spec.rb
+++ b/spec/helpers/groups_helper_spec.rb
@@ -340,4 +340,31 @@ describe GroupsHelper do
end
end
end
+
+ describe '#can_update_default_branch_protection?' do
+ let(:current_user) { create(:user) }
+ let(:group) { create(:group) }
+
+ subject { helper.can_update_default_branch_protection?(group) }
+
+ before do
+ allow(helper).to receive(:current_user) { current_user }
+ end
+
+ context 'for users who can update default branch protection of the group' do
+ before do
+ allow(helper).to receive(:can?).with(current_user, :update_default_branch_protection, group) { true }
+ end
+
+ it { is_expected.to be_truthy }
+ end
+
+ context 'for users who cannot update default branch protection of the group' do
+ before do
+ allow(helper).to receive(:can?).with(current_user, :update_default_branch_protection, group) { false }
+ end
+
+ it { is_expected.to be_falsey }
+ end
+ end
end
diff --git a/spec/helpers/issuables_helper_spec.rb b/spec/helpers/issuables_helper_spec.rb
index 7eb5d2fc08c..38ad11846d2 100644
--- a/spec/helpers/issuables_helper_spec.rb
+++ b/spec/helpers/issuables_helper_spec.rb
@@ -303,46 +303,4 @@ describe IssuablesHelper do
end
end
end
-
- describe '#gitlab_team_member_badge' do
- let(:issue) { build(:issue, author: user) }
-
- before do
- allow(Gitlab).to receive(:com?).and_return(true)
- end
-
- context 'when `:gitlab_employee_badge` feature flag is disabled' do
- let(:user) { build(:user, email: 'test@gitlab.com') }
-
- before do
- stub_feature_flags(gitlab_employee_badge: false)
- end
-
- it 'returns nil' do
- expect(helper.gitlab_team_member_badge(issue.author)).to be_nil
- end
- end
-
- context 'when issue author is not a GitLab team member' do
- let(:user) { build(:user, email: 'test@example.com') }
-
- it 'returns nil' do
- expect(helper.gitlab_team_member_badge(issue.author)).to be_nil
- end
- end
-
- context 'when issue author is a GitLab team member' do
- let(:user) { build(:user, email: 'test@gitlab.com') }
-
- it 'returns span with svg icon' do
- expect(helper.gitlab_team_member_badge(issue.author)).to have_selector('span > svg')
- end
-
- context 'when `css_class` parameter is passed' do
- it 'adds CSS classes' do
- expect(helper.gitlab_team_member_badge(issue.author, css_class: 'foo bar baz')).to have_selector('span.foo.bar.baz')
- end
- end
- end
- end
end
diff --git a/spec/helpers/markup_helper_spec.rb b/spec/helpers/markup_helper_spec.rb
index 33347f20de8..b2df543d651 100644
--- a/spec/helpers/markup_helper_spec.rb
+++ b/spec/helpers/markup_helper_spec.rb
@@ -95,12 +95,13 @@ describe MarkupHelper do
context 'when text contains a relative link to an image in the repository' do
let(:image_file) { "logo-white.png" }
let(:text_with_relative_path) { "![](./#{image_file})\n" }
- let(:generated_html) { helper.markdown(text_with_relative_path, requested_path: requested_path) }
+ let(:generated_html) { helper.markdown(text_with_relative_path, requested_path: requested_path, ref: ref) }
subject { Nokogiri::HTML.parse(generated_html) }
- context 'when requested_path is provided in the context' do
+ context 'when requested_path is provided, but ref isn\'t' do
let(:requested_path) { 'files/images/README.md' }
+ let(:ref) { nil }
it 'returns the correct HTML for the image' do
expanded_path = "/#{project.full_path}/-/raw/master/files/images/#{image_file}"
@@ -110,13 +111,43 @@ describe MarkupHelper do
end
end
- context 'when requested_path parameter is not provided' do
+ context 'when requested_path and ref parameters are both provided' do
+ let(:requested_path) { 'files/images/README.md' }
+ let(:ref) { 'other_branch' }
+
+ it 'returns the correct HTML for the image' do
+ project.repository.create_branch('other_branch')
+
+ expanded_path = "/#{project.full_path}/-/raw/#{ref}/files/images/#{image_file}"
+
+ expect(subject.css('a')[0].attr('href')).to eq(expanded_path)
+ expect(subject.css('img')[0].attr('data-src')).to eq(expanded_path)
+ end
+ end
+
+ context 'when ref is provided, but requested_path isn\'t' do
+ let(:ref) { 'other_branch' }
+ let(:requested_path) { nil }
+
+ it 'returns the correct HTML for the image' do
+ project.repository.create_branch('other_branch')
+
+ expanded_path = "/#{project.full_path}/-/blob/#{ref}/./#{image_file}"
+
+ expect(subject.css('a')[0].attr('href')).to eq(expanded_path)
+ expect(subject.css('img')[0].attr('data-src')).to eq(expanded_path)
+ end
+ end
+
+ context 'when neither requested_path, nor ref parameter is provided' do
+ let(:ref) { nil }
let(:requested_path) { nil }
- it 'returns the link to the image path as a relative path' do
+ it 'returns the correct HTML for the image' do
expanded_path = "/#{project.full_path}/-/blob/master/./#{image_file}"
expect(subject.css('a')[0].attr('href')).to eq(expanded_path)
+ expect(subject.css('img')[0].attr('data-src')).to eq(expanded_path)
end
end
end
diff --git a/spec/helpers/members_helper_spec.rb b/spec/helpers/members_helper_spec.rb
index 169c8707bf4..946ffcddae7 100644
--- a/spec/helpers/members_helper_spec.rb
+++ b/spec/helpers/members_helper_spec.rb
@@ -22,6 +22,17 @@ describe MembersHelper do
it { expect(remove_member_message(group_member_invite)).to eq "Are you sure you want to revoke the invitation for #{group_member_invite.invite_email} to join the #{group.name} group?" }
it { expect(remove_member_message(group_member_request)).to eq "Are you sure you want to deny #{requester.name}'s request to join the #{group.name} group?" }
it { expect(remove_member_message(group_member_request, user: requester)).to eq "Are you sure you want to withdraw your access request for the #{group.name} group?" }
+
+ context 'an accepted user invitation with no user associated' do
+ before do
+ group_member_invite.update(invite_email: "#{SecureRandom.hex}@example.com", invite_token: nil, user_id: nil)
+ end
+
+ it 'logs an exception and shows orphaned status' do
+ expect(Gitlab::ErrorTracking).to receive(:track_exception).with(anything, hash_including(:member_id, :invite_email, :invite_accepted_at))
+ expect(remove_member_message(group_member_invite)).to eq "Are you sure you want to remove this orphaned member from the #{group.name} group and any subresources?"
+ end
+ end
end
describe '#remove_member_title' do
diff --git a/spec/helpers/milestones_helper_spec.rb b/spec/helpers/milestones_helper_spec.rb
index 3574066e03e..4ce7143bdf0 100644
--- a/spec/helpers/milestones_helper_spec.rb
+++ b/spec/helpers/milestones_helper_spec.rb
@@ -85,4 +85,19 @@ describe MilestonesHelper do
end
end
end
+
+ describe "#group_milestone_route" do
+ let(:group) { build_stubbed(:group) }
+ let(:subgroup) { build_stubbed(:group, parent: group, name: "Test Subgrp") }
+
+ context "when in subgroup" do
+ let(:milestone) { build_stubbed(:group_milestone, group: subgroup) }
+
+ it 'generates correct url despite assigned @group' do
+ assign(:group, group)
+ milestone_path = "/groups/#{subgroup.full_path}/-/milestones/#{milestone.iid}"
+ expect(helper.group_milestone_route(milestone)).to eq(milestone_path)
+ end
+ end
+ end
end
diff --git a/spec/helpers/nav_helper_spec.rb b/spec/helpers/nav_helper_spec.rb
index f92dca11136..ac1c6c62433 100644
--- a/spec/helpers/nav_helper_spec.rb
+++ b/spec/helpers/nav_helper_spec.rb
@@ -117,4 +117,27 @@ describe NavHelper, :do_not_mock_admin_mode do
it { is_expected.to all(be_a(String)) }
end
+
+ describe '#page_has_markdown?' do
+ using RSpec::Parameterized::TableSyntax
+
+ where path: %w(
+ merge_requests#show
+ projects/merge_requests/conflicts#show
+ issues#show
+ milestones#show
+ issues#designs
+ )
+
+ with_them do
+ before do
+ allow(helper).to receive(:current_path?).and_call_original
+ allow(helper).to receive(:current_path?).with(path).and_return(true)
+ end
+
+ subject { helper.page_has_markdown? }
+
+ it { is_expected.to eq(true) }
+ end
+ end
end
diff --git a/spec/helpers/preferences_helper_spec.rb b/spec/helpers/preferences_helper_spec.rb
index c4ed99e56a0..7969cfd97b5 100644
--- a/spec/helpers/preferences_helper_spec.rb
+++ b/spec/helpers/preferences_helper_spec.rb
@@ -123,7 +123,7 @@ describe PreferencesHelper do
describe '#language_choices' do
it 'returns an array of all available languages' do
expect(helper.language_choices).to be_an(Array)
- expect(helper.language_choices.map(&:second)).to eq(Gitlab::I18n.available_locales)
+ expect(helper.language_choices.map(&:first)).to eq(Gitlab::I18n::AVAILABLE_LANGUAGES.values.sort)
end
end
diff --git a/spec/helpers/projects/alert_management_helper_spec.rb b/spec/helpers/projects/alert_management_helper_spec.rb
new file mode 100644
index 00000000000..078759de39c
--- /dev/null
+++ b/spec/helpers/projects/alert_management_helper_spec.rb
@@ -0,0 +1,82 @@
+# frozen_string_literal: true
+
+require 'spec_helper'
+
+describe Projects::AlertManagementHelper do
+ include Gitlab::Routing.url_helpers
+
+ let_it_be(:project, reload: true) { create(:project) }
+ let_it_be(:current_user) { create(:user) }
+ let_it_be(:project_path) { project.full_path }
+
+ describe '#alert_management_data' do
+ let(:user_can_enable_alert_management) { true }
+ let(:setting_path) { edit_project_service_path(project, AlertsService) }
+
+ subject(:data) { helper.alert_management_data(current_user, project) }
+
+ before do
+ allow(helper)
+ .to receive(:can?)
+ .with(current_user, :admin_project, project)
+ .and_return(user_can_enable_alert_management)
+ end
+
+ context 'without alert_managements_setting' do
+ it 'returns index page configuration' do
+ expect(helper.alert_management_data(current_user, project)).to match(
+ 'project-path' => project_path,
+ 'enable-alert-management-path' => setting_path,
+ 'empty-alert-svg-path' => match_asset_path('/assets/illustrations/alert-management-empty-state.svg'),
+ 'user-can-enable-alert-management' => 'true',
+ 'alert-management-enabled' => 'false'
+ )
+ end
+ end
+
+ context 'with alerts service' do
+ let_it_be(:alerts_service) { create(:alerts_service, project: project) }
+
+ context 'when alerts service is active' do
+ it 'enables alert management' do
+ expect(data).to include(
+ 'alert-management-enabled' => 'true'
+ )
+ end
+ end
+
+ context 'when alerts service is inactive' do
+ it 'disables alert management' do
+ alerts_service.update(active: false)
+
+ expect(data).to include(
+ 'alert-management-enabled' => 'false'
+ )
+ end
+ end
+ end
+
+ context 'when user does not have requisite enablement permissions' do
+ let(:user_can_enable_alert_management) { false }
+
+ it 'shows error tracking enablement as disabled' do
+ expect(helper.alert_management_data(current_user, project)).to include(
+ 'user-can-enable-alert-management' => 'false'
+ )
+ end
+ end
+ end
+
+ describe '#alert_management_detail_data' do
+ let(:alert_id) { 1 }
+ let(:new_issue_path) { new_project_issue_path(project) }
+
+ it 'returns detail page configuration' do
+ expect(helper.alert_management_detail_data(project, alert_id)).to eq(
+ 'alert-id' => alert_id,
+ 'project-path' => project_path,
+ 'new-issue-path' => new_issue_path
+ )
+ end
+ end
+end
diff --git a/spec/helpers/projects_helper_spec.rb b/spec/helpers/projects_helper_spec.rb
index 17e3f8f9c06..189ab1a8354 100644
--- a/spec/helpers/projects_helper_spec.rb
+++ b/spec/helpers/projects_helper_spec.rb
@@ -719,11 +719,7 @@ describe ProjectsHelper do
end
describe '#show_merge_request_count' do
- context 'when the feature flag is enabled' do
- before do
- stub_feature_flags(project_list_show_mr_count: true)
- end
-
+ context 'enabled flag' do
it 'returns true if compact mode is disabled' do
expect(helper.show_merge_request_count?).to be_truthy
end
@@ -733,22 +729,7 @@ describe ProjectsHelper do
end
end
- context 'when the feature flag is disabled' do
- before do
- stub_feature_flags(project_list_show_mr_count: false)
- end
-
- it 'always returns false' do
- expect(helper.show_merge_request_count?(disabled: false)).to be_falsy
- expect(helper.show_merge_request_count?(disabled: true)).to be_falsy
- end
- end
-
context 'disabled flag' do
- before do
- stub_feature_flags(project_list_show_mr_count: true)
- end
-
it 'returns false if disabled flag is true' do
expect(helper.show_merge_request_count?(disabled: true)).to be_falsey
end
@@ -760,11 +741,7 @@ describe ProjectsHelper do
end
describe '#show_issue_count?' do
- context 'when the feature flag is enabled' do
- before do
- stub_feature_flags(project_list_show_issue_count: true)
- end
-
+ context 'enabled flag' do
it 'returns true if compact mode is disabled' do
expect(helper.show_issue_count?).to be_truthy
end
@@ -774,22 +751,7 @@ describe ProjectsHelper do
end
end
- context 'when the feature flag is disabled' do
- before do
- stub_feature_flags(project_list_show_issue_count: false)
- end
-
- it 'always returns false' do
- expect(helper.show_issue_count?(disabled: false)).to be_falsy
- expect(helper.show_issue_count?(disabled: true)).to be_falsy
- end
- end
-
context 'disabled flag' do
- before do
- stub_feature_flags(project_list_show_issue_count: true)
- end
-
it 'returns false if disabled flag is true' do
expect(helper.show_issue_count?(disabled: true)).to be_falsey
end
diff --git a/spec/helpers/releases_helper_spec.rb b/spec/helpers/releases_helper_spec.rb
index 282758679cb..de4086e48db 100644
--- a/spec/helpers/releases_helper_spec.rb
+++ b/spec/helpers/releases_helper_spec.rb
@@ -54,7 +54,9 @@ describe ReleasesHelper do
markdown_docs_path
releases_page_path
update_release_api_docs_path
- release_assets_docs_path)
+ release_assets_docs_path
+ manage_milestones_path
+ new_milestone_path)
expect(helper.data_for_edit_release_page.keys).to eq(keys)
end
end
diff --git a/spec/helpers/search_helper_spec.rb b/spec/helpers/search_helper_spec.rb
index 18c94602596..6a06b012c6c 100644
--- a/spec/helpers/search_helper_spec.rb
+++ b/spec/helpers/search_helper_spec.rb
@@ -112,7 +112,6 @@ describe SearchHelper do
'milestones' | 'milestone'
'notes' | 'comment'
'projects' | 'project'
- 'snippet_blobs' | 'snippet result'
'snippet_titles' | 'snippet'
'users' | 'user'
'wiki_blobs' | 'wiki result'
diff --git a/spec/helpers/snippets_helper_spec.rb b/spec/helpers/snippets_helper_spec.rb
index b5b431b5818..6fdf4f5cfb4 100644
--- a/spec/helpers/snippets_helper_spec.rb
+++ b/spec/helpers/snippets_helper_spec.rb
@@ -151,35 +151,4 @@ describe SnippetsHelper do
"<input type=\"text\" readonly=\"readonly\" class=\"js-snippet-url-area snippet-embed-input form-control\" data-url=\"#{url}\" value=\"<script src=&quot;#{url}.js&quot;></script>\" autocomplete=\"off\"></input>"
end
end
-
- describe '#snippet_file_name' do
- subject { helper.snippet_file_name(snippet) }
-
- where(:snippet_type, :flag_enabled, :trait, :filename) do
- [
- [:personal_snippet, false, nil, 'foo.txt'],
- [:personal_snippet, true, nil, 'foo.txt'],
- [:personal_snippet, false, :repository, 'foo.txt'],
- [:personal_snippet, true, :repository, '.gitattributes'],
-
- [:project_snippet, false, nil, 'foo.txt'],
- [:project_snippet, true, nil, 'foo.txt'],
- [:project_snippet, false, :repository, 'foo.txt'],
- [:project_snippet, true, :repository, '.gitattributes']
- ]
- end
-
- with_them do
- let(:snippet) { create(snippet_type, trait, file_name: 'foo.txt') }
-
- before do
- allow(helper).to receive(:current_user).and_return(snippet.author)
- stub_feature_flags(version_snippets: flag_enabled)
- end
-
- it 'returns the correct filename' do
- expect(subject).to eq filename
- end
- end
- end
end
diff --git a/spec/helpers/todos_helper_spec.rb b/spec/helpers/todos_helper_spec.rb
index 7c73b990338..b09e1e2b83b 100644
--- a/spec/helpers/todos_helper_spec.rb
+++ b/spec/helpers/todos_helper_spec.rb
@@ -1,8 +1,26 @@
# frozen_string_literal: true
-require "spec_helper"
+require 'spec_helper'
describe TodosHelper do
+ let_it_be(:user) { create(:user) }
+ let_it_be(:author) { create(:user) }
+ let_it_be(:issue) { create(:issue) }
+ let_it_be(:design) { create(:design, issue: issue) }
+ let_it_be(:note) do
+ create(:note,
+ project: issue.project,
+ note: 'I am note, hear me roar')
+ end
+ let_it_be(:design_todo) do
+ create(:todo, :mentioned,
+ user: user,
+ project: issue.project,
+ target: design,
+ author: author,
+ note: note)
+ end
+
describe '#todos_count_format' do
it 'shows fuzzy count for 100 or more items' do
expect(helper.todos_count_format(100)).to eq '99+'
@@ -32,7 +50,56 @@ describe TodosHelper do
{ 'id' => projects.first.id, 'text' => projects.first.full_name }
]
- expect(JSON.parse(helper.todo_projects_options)).to match_array(expected_results)
+ expect(Gitlab::Json.parse(helper.todo_projects_options)).to match_array(expected_results)
+ end
+ end
+
+ describe '#todo_target_link' do
+ context 'when given a design' do
+ let(:todo) { design_todo }
+
+ it 'produces a good link' do
+ path = helper.todo_target_path(todo)
+ link = helper.todo_target_link(todo)
+ expected = "<a href=\"#{path}\">design #{design.to_reference}</a>"
+
+ expect(link).to eq(expected)
+ end
+ end
+ end
+
+ describe '#todo_target_path' do
+ context 'when given a design' do
+ let(:todo) { design_todo }
+
+ it 'responds with an appropriate path' do
+ path = helper.todo_target_path(todo)
+ issue_path = Gitlab::Routing.url_helpers
+ .project_issue_path(issue.project, issue)
+
+ expect(path).to eq("#{issue_path}/designs/#{design.filename}##{dom_id(design_todo.note)}")
+ end
+ end
+ end
+
+ describe '#todo_target_type_name' do
+ context 'when given a design todo' do
+ let(:todo) { design_todo }
+
+ it 'responds with an appropriate target type name' do
+ name = helper.todo_target_type_name(todo)
+
+ expect(name).to eq('design')
+ end
+ end
+ end
+
+ describe '#todo_types_options' do
+ it 'includes a match for a design todo' do
+ options = helper.todo_types_options
+ design_option = options.find { |o| o[:id] == design_todo.target_type }
+
+ expect(design_option).to include(text: 'Design')
end
end
end
diff --git a/spec/helpers/visibility_level_helper_spec.rb b/spec/helpers/visibility_level_helper_spec.rb
index debe4401308..b7a88ee5010 100644
--- a/spec/helpers/visibility_level_helper_spec.rb
+++ b/spec/helpers/visibility_level_helper_spec.rb
@@ -146,22 +146,22 @@ describe VisibilityLevelHelper do
using RSpec::Parameterized::TableSyntax
- PUBLIC = Gitlab::VisibilityLevel::PUBLIC
- INTERNAL = Gitlab::VisibilityLevel::INTERNAL
- PRIVATE = Gitlab::VisibilityLevel::PRIVATE
+ public_vis = Gitlab::VisibilityLevel::PUBLIC
+ internal_vis = Gitlab::VisibilityLevel::INTERNAL
+ private_vis = Gitlab::VisibilityLevel::PRIVATE
# This is a subset of all the permutations
where(:requested_level, :max_allowed, :global_default_level, :restricted_levels, :expected) do
- PUBLIC | PUBLIC | PUBLIC | [] | PUBLIC
- PUBLIC | PUBLIC | PUBLIC | [PUBLIC] | INTERNAL
- INTERNAL | PUBLIC | PUBLIC | [] | INTERNAL
- INTERNAL | PRIVATE | PRIVATE | [] | PRIVATE
- PRIVATE | PUBLIC | PUBLIC | [] | PRIVATE
- PUBLIC | PRIVATE | INTERNAL | [] | PRIVATE
- PUBLIC | INTERNAL | PUBLIC | [] | INTERNAL
- PUBLIC | PRIVATE | PUBLIC | [] | PRIVATE
- PUBLIC | INTERNAL | INTERNAL | [] | INTERNAL
- PUBLIC | PUBLIC | INTERNAL | [] | PUBLIC
+ public_vis | public_vis | public_vis | [] | public_vis
+ public_vis | public_vis | public_vis | [public_vis] | internal_vis
+ internal_vis | public_vis | public_vis | [] | internal_vis
+ internal_vis | private_vis | private_vis | [] | private_vis
+ private_vis | public_vis | public_vis | [] | private_vis
+ public_vis | private_vis | internal_vis | [] | private_vis
+ public_vis | internal_vis | public_vis | [] | internal_vis
+ public_vis | private_vis | public_vis | [] | private_vis
+ public_vis | internal_vis | internal_vis | [] | internal_vis
+ public_vis | public_vis | internal_vis | [] | public_vis
end
before do
diff --git a/spec/helpers/x509_helper_spec.rb b/spec/helpers/x509_helper_spec.rb
index dcdf57ce035..db3f6158195 100644
--- a/spec/helpers/x509_helper_spec.rb
+++ b/spec/helpers/x509_helper_spec.rb
@@ -57,4 +57,22 @@ describe X509Helper do
end
end
end
+
+ describe '#x509_signature?' do
+ let(:x509_signature) { create(:x509_commit_signature) }
+ let(:gpg_signature) { create(:gpg_signature) }
+
+ it 'detects a x509 signed commit' do
+ signature = Gitlab::X509::Signature.new(
+ X509Helpers::User1.signed_commit_signature,
+ X509Helpers::User1.signed_commit_base_data,
+ X509Helpers::User1.certificate_email,
+ X509Helpers::User1.signed_commit_time
+ )
+
+ expect(x509_signature?(x509_signature)).to be_truthy
+ expect(x509_signature?(signature)).to be_truthy
+ expect(x509_signature?(gpg_signature)).to be_falsey
+ end
+ end
end
diff --git a/spec/initializers/action_mailer_hooks_spec.rb b/spec/initializers/action_mailer_hooks_spec.rb
index 20f96f7e16c..03eee09f737 100644
--- a/spec/initializers/action_mailer_hooks_spec.rb
+++ b/spec/initializers/action_mailer_hooks_spec.rb
@@ -6,6 +6,10 @@ describe 'ActionMailer hooks' do
describe 'smime signature interceptor' do
before do
class_spy(ActionMailer::Base).as_stubbed_const
+
+ # rspec-rails calls ActionMailer::Base.deliveries.clear after every test
+ # https://github.com/rspec/rspec-rails/commit/71c12388e2bad78aaeea6443a393ede78341a7a3
+ allow(ActionMailer::Base).to receive_message_chain(:deliveries, :clear)
end
it 'is disabled by default' do
diff --git a/spec/initializers/lograge_spec.rb b/spec/initializers/lograge_spec.rb
index 48acdac74ac..c243217d2a2 100644
--- a/spec/initializers/lograge_spec.rb
+++ b/spec/initializers/lograge_spec.rb
@@ -123,7 +123,7 @@ describe 'lograge', type: :request do
let(:logger) do
Logger.new(log_output).tap { |logger| logger.formatter = ->(_, _, _, msg) { msg } }
end
- let(:log_data) { JSON.parse(log_output.string) }
+ let(:log_data) { Gitlab::Json.parse(log_output.string) }
before do
Lograge.logger = logger
diff --git a/spec/initializers/secret_token_spec.rb b/spec/initializers/secret_token_spec.rb
index c29f46e7779..b7979144c72 100644
--- a/spec/initializers/secret_token_spec.rb
+++ b/spec/initializers/secret_token_spec.rb
@@ -7,9 +7,8 @@ describe 'create_tokens' do
include StubENV
let(:secrets) { ActiveSupport::OrderedOptions.new }
-
- HEX_KEY = /\h{128}/.freeze
- RSA_KEY = /\A-----BEGIN RSA PRIVATE KEY-----\n.+\n-----END RSA PRIVATE KEY-----\n\Z/m.freeze
+ let(:hex_key) { /\h{128}/.freeze }
+ let(:rsa_key) { /\A-----BEGIN RSA PRIVATE KEY-----\n.+\n-----END RSA PRIVATE KEY-----\n\Z/m.freeze }
before do
allow(File).to receive(:write)
@@ -35,7 +34,7 @@ describe 'create_tokens' do
keys = secrets.values_at(:secret_key_base, :otp_key_base, :db_key_base)
expect(keys.uniq).to eq(keys)
- expect(keys).to all(match(HEX_KEY))
+ expect(keys).to all(match(hex_key))
end
it 'generates an RSA key for openid_connect_signing_key' do
@@ -44,7 +43,7 @@ describe 'create_tokens' do
keys = secrets.values_at(:openid_connect_signing_key)
expect(keys.uniq).to eq(keys)
- expect(keys).to all(match(RSA_KEY))
+ expect(keys).to all(match(rsa_key))
end
it 'warns about the secrets to add to secrets.yml' do
diff --git a/spec/initializers/zz_metrics_spec.rb b/spec/initializers/zz_metrics_spec.rb
index b9a1919ceae..f41a807f1eb 100644
--- a/spec/initializers/zz_metrics_spec.rb
+++ b/spec/initializers/zz_metrics_spec.rb
@@ -5,15 +5,11 @@ require 'spec_helper'
describe 'instrument_classes' do
let(:config) { double(:config) }
- let(:influx_sampler) { double(:influx_sampler) }
-
before do
allow(config).to receive(:instrument_method)
allow(config).to receive(:instrument_methods)
allow(config).to receive(:instrument_instance_method)
allow(config).to receive(:instrument_instance_methods)
- allow(Gitlab::Metrics::Samplers::InfluxSampler).to receive(:initialize_instance).and_return(influx_sampler)
- allow(influx_sampler).to receive(:start)
allow(Gitlab::Application).to receive(:configure)
end
diff --git a/spec/javascripts/ajax_loading_spinner_spec.js b/spec/javascripts/ajax_loading_spinner_spec.js
deleted file mode 100644
index 89195a4397f..00000000000
--- a/spec/javascripts/ajax_loading_spinner_spec.js
+++ /dev/null
@@ -1,57 +0,0 @@
-import $ from 'jquery';
-import AjaxLoadingSpinner from '~/ajax_loading_spinner';
-
-describe('Ajax Loading Spinner', () => {
- const fixtureTemplate = 'static/ajax_loading_spinner.html';
- preloadFixtures(fixtureTemplate);
-
- beforeEach(() => {
- loadFixtures(fixtureTemplate);
- AjaxLoadingSpinner.init();
- });
-
- it('change current icon with spinner icon and disable link while waiting ajax response', done => {
- spyOn($, 'ajax').and.callFake(req => {
- const xhr = new XMLHttpRequest();
- const ajaxLoadingSpinner = document.querySelector('.js-ajax-loading-spinner');
- const icon = ajaxLoadingSpinner.querySelector('i');
-
- req.beforeSend(xhr, { dataType: 'text/html' });
-
- expect(icon).not.toHaveClass('fa-trash-o');
- expect(icon).toHaveClass('fa-spinner');
- expect(icon).toHaveClass('fa-spin');
- expect(icon.dataset.icon).toEqual('fa-trash-o');
- expect(ajaxLoadingSpinner.getAttribute('disabled')).toEqual('');
-
- req.complete({});
-
- done();
- const deferred = $.Deferred();
- return deferred.promise();
- });
- document.querySelector('.js-ajax-loading-spinner').click();
- });
-
- it('use original icon again and enabled the link after complete the ajax request', done => {
- spyOn($, 'ajax').and.callFake(req => {
- const xhr = new XMLHttpRequest();
- const ajaxLoadingSpinner = document.querySelector('.js-ajax-loading-spinner');
-
- req.beforeSend(xhr, { dataType: 'text/html' });
- req.complete({});
-
- const icon = ajaxLoadingSpinner.querySelector('i');
-
- expect(icon).toHaveClass('fa-trash-o');
- expect(icon).not.toHaveClass('fa-spinner');
- expect(icon).not.toHaveClass('fa-spin');
- expect(ajaxLoadingSpinner.getAttribute('disabled')).toEqual(null);
-
- done();
- const deferred = $.Deferred();
- return deferred.promise();
- });
- document.querySelector('.js-ajax-loading-spinner').click();
- });
-});
diff --git a/spec/javascripts/avatar_helper_spec.js b/spec/javascripts/avatar_helper_spec.js
deleted file mode 100644
index c1ef08e0f1b..00000000000
--- a/spec/javascripts/avatar_helper_spec.js
+++ /dev/null
@@ -1,98 +0,0 @@
-import { TEST_HOST } from 'spec/test_constants';
-import { getFirstCharacterCapitalized } from '~/lib/utils/text_utility';
-import {
- DEFAULT_SIZE_CLASS,
- IDENTICON_BG_COUNT,
- renderAvatar,
- renderIdenticon,
- getIdenticonBackgroundClass,
- getIdenticonTitle,
-} from '~/helpers/avatar_helper';
-
-function matchAll(str) {
- return new RegExp(`^${str}$`);
-}
-
-describe('avatar_helper', () => {
- describe('getIdenticonBackgroundClass', () => {
- it('returns identicon bg class from id', () => {
- expect(getIdenticonBackgroundClass(1)).toEqual('bg2');
- });
-
- it(`wraps around if id is bigger than ${IDENTICON_BG_COUNT}`, () => {
- expect(getIdenticonBackgroundClass(IDENTICON_BG_COUNT + 4)).toEqual('bg5');
- expect(getIdenticonBackgroundClass(IDENTICON_BG_COUNT * 5 + 6)).toEqual('bg7');
- });
- });
-
- describe('getIdenticonTitle', () => {
- it('returns identicon title from name', () => {
- expect(getIdenticonTitle('Lorem')).toEqual('L');
- expect(getIdenticonTitle('dolar-sit-amit')).toEqual('D');
- expect(getIdenticonTitle('%-with-special-chars')).toEqual('%');
- });
-
- it('returns space if name is falsey', () => {
- expect(getIdenticonTitle('')).toEqual(' ');
- expect(getIdenticonTitle(null)).toEqual(' ');
- });
- });
-
- describe('renderIdenticon', () => {
- it('renders with the first letter as title and bg based on id', () => {
- const entity = {
- id: IDENTICON_BG_COUNT + 3,
- name: 'Xavior',
- };
- const options = {
- sizeClass: 's32',
- };
-
- const result = renderIdenticon(entity, options);
-
- expect(result).toHaveClass(`identicon ${options.sizeClass} bg4`);
- expect(result).toHaveText(matchAll(getFirstCharacterCapitalized(entity.name)));
- });
-
- it('renders with defaults, if no options are given', () => {
- const entity = {
- id: 1,
- name: 'tanuki',
- };
-
- const result = renderIdenticon(entity);
-
- expect(result).toHaveClass(`identicon ${DEFAULT_SIZE_CLASS} bg2`);
- expect(result).toHaveText(matchAll(getFirstCharacterCapitalized(entity.name)));
- });
- });
-
- describe('renderAvatar', () => {
- it('renders an image with the avatarUrl', () => {
- const avatarUrl = `${TEST_HOST}/not-real-assets/test.png`;
-
- const result = renderAvatar({
- avatar_url: avatarUrl,
- });
-
- expect(result).toBeMatchedBy('img');
- expect(result).toHaveAttr('src', avatarUrl);
- expect(result).toHaveClass(DEFAULT_SIZE_CLASS);
- });
-
- it('renders an identicon if no avatarUrl', () => {
- const entity = {
- id: 1,
- name: 'walrus',
- };
- const options = {
- sizeClass: 's16',
- };
-
- const result = renderAvatar(entity, options);
-
- expect(result).toHaveClass(`identicon ${options.sizeClass} bg2`);
- expect(result).toHaveText(matchAll(getFirstCharacterCapitalized(entity.name)));
- });
- });
-});
diff --git a/spec/javascripts/bootstrap_linked_tabs_spec.js b/spec/javascripts/bootstrap_linked_tabs_spec.js
deleted file mode 100644
index 1d21637ceae..00000000000
--- a/spec/javascripts/bootstrap_linked_tabs_spec.js
+++ /dev/null
@@ -1,67 +0,0 @@
-import LinkedTabs from '~/lib/utils/bootstrap_linked_tabs';
-
-describe('Linked Tabs', () => {
- preloadFixtures('static/linked_tabs.html');
-
- beforeEach(() => {
- loadFixtures('static/linked_tabs.html');
- });
-
- describe('when is initialized', () => {
- beforeEach(() => {
- spyOn(window.history, 'replaceState').and.callFake(function() {});
- });
-
- it('should activate the tab correspondent to the given action', () => {
- // eslint-disable-next-line no-new
- new LinkedTabs({
- action: 'tab1',
- defaultAction: 'tab1',
- parentEl: '.linked-tabs',
- });
-
- expect(document.querySelector('#tab1').classList).toContain('active');
- });
-
- it('should active the default tab action when the action is show', () => {
- // eslint-disable-next-line no-new
- new LinkedTabs({
- action: 'show',
- defaultAction: 'tab1',
- parentEl: '.linked-tabs',
- });
-
- expect(document.querySelector('#tab1').classList).toContain('active');
- });
- });
-
- describe('on click', () => {
- it('should change the url according to the clicked tab', () => {
- const historySpy = spyOn(window.history, 'replaceState').and.callFake(() => {});
-
- const linkedTabs = new LinkedTabs({
- action: 'show',
- defaultAction: 'tab1',
- parentEl: '.linked-tabs',
- });
-
- const secondTab = document.querySelector('.linked-tabs li:nth-child(2) a');
- const newState =
- secondTab.getAttribute('href') +
- linkedTabs.currentLocation.search +
- linkedTabs.currentLocation.hash;
-
- secondTab.click();
-
- if (historySpy) {
- expect(historySpy).toHaveBeenCalledWith(
- {
- url: newState,
- },
- document.title,
- newState,
- );
- }
- });
- });
-});
diff --git a/spec/javascripts/ci_variable_list/ajax_variable_list_spec.js b/spec/javascripts/ci_variable_list/ajax_variable_list_spec.js
deleted file mode 100644
index a1377564073..00000000000
--- a/spec/javascripts/ci_variable_list/ajax_variable_list_spec.js
+++ /dev/null
@@ -1,231 +0,0 @@
-import $ from 'jquery';
-import MockAdapter from 'axios-mock-adapter';
-import axios from '~/lib/utils/axios_utils';
-import AjaxFormVariableList from '~/ci_variable_list/ajax_variable_list';
-
-const VARIABLE_PATCH_ENDPOINT = 'http://test.host/frontend-fixtures/builds-project/-/variables';
-const HIDE_CLASS = 'hide';
-
-describe('AjaxFormVariableList', () => {
- preloadFixtures('projects/ci_cd_settings.html');
- preloadFixtures('projects/ci_cd_settings_with_variables.html');
-
- let container;
- let saveButton;
- let errorBox;
-
- let mock;
- let ajaxVariableList;
-
- beforeEach(() => {
- loadFixtures('projects/ci_cd_settings.html');
- container = document.querySelector('.js-ci-variable-list-section');
-
- mock = new MockAdapter(axios);
-
- const ajaxVariableListEl = document.querySelector('.js-ci-variable-list-section');
- saveButton = ajaxVariableListEl.querySelector('.js-ci-variables-save-button');
- errorBox = container.querySelector('.js-ci-variable-error-box');
- ajaxVariableList = new AjaxFormVariableList({
- container,
- formField: 'variables',
- saveButton,
- errorBox,
- saveEndpoint: container.dataset.saveEndpoint,
- maskableRegex: container.dataset.maskableRegex,
- });
-
- spyOn(ajaxVariableList, 'updateRowsWithPersistedVariables').and.callThrough();
- spyOn(ajaxVariableList.variableList, 'toggleEnableRow').and.callThrough();
- });
-
- afterEach(() => {
- mock.restore();
- });
-
- describe('onSaveClicked', () => {
- it('shows loading spinner while waiting for the request', done => {
- const loadingIcon = saveButton.querySelector('.js-ci-variables-save-loading-icon');
-
- mock.onPatch(VARIABLE_PATCH_ENDPOINT).reply(() => {
- expect(loadingIcon.classList.contains(HIDE_CLASS)).toEqual(false);
-
- return [200, {}];
- });
-
- expect(loadingIcon.classList.contains(HIDE_CLASS)).toEqual(true);
-
- ajaxVariableList
- .onSaveClicked()
- .then(() => {
- expect(loadingIcon.classList.contains(HIDE_CLASS)).toEqual(true);
- })
- .then(done)
- .catch(done.fail);
- });
-
- it('calls `updateRowsWithPersistedVariables` with the persisted variables', done => {
- const variablesResponse = [{ id: 1, key: 'foo', value: 'bar' }];
- mock.onPatch(VARIABLE_PATCH_ENDPOINT).reply(200, {
- variables: variablesResponse,
- });
-
- ajaxVariableList
- .onSaveClicked()
- .then(() => {
- expect(ajaxVariableList.updateRowsWithPersistedVariables).toHaveBeenCalledWith(
- variablesResponse,
- );
- })
- .then(done)
- .catch(done.fail);
- });
-
- it('hides any previous error box', done => {
- mock.onPatch(VARIABLE_PATCH_ENDPOINT).reply(200);
-
- expect(errorBox.classList.contains(HIDE_CLASS)).toEqual(true);
-
- ajaxVariableList
- .onSaveClicked()
- .then(() => {
- expect(errorBox.classList.contains(HIDE_CLASS)).toEqual(true);
- })
- .then(done)
- .catch(done.fail);
- });
-
- it('disables remove buttons while waiting for the request', done => {
- mock.onPatch(VARIABLE_PATCH_ENDPOINT).reply(() => {
- expect(ajaxVariableList.variableList.toggleEnableRow).toHaveBeenCalledWith(false);
-
- return [200, {}];
- });
-
- ajaxVariableList
- .onSaveClicked()
- .then(() => {
- expect(ajaxVariableList.variableList.toggleEnableRow).toHaveBeenCalledWith(true);
- })
- .then(done)
- .catch(done.fail);
- });
-
- it('hides secret values', done => {
- mock.onPatch(VARIABLE_PATCH_ENDPOINT).reply(200, {});
-
- const row = container.querySelector('.js-row');
- const valueInput = row.querySelector('.js-ci-variable-input-value');
- const valuePlaceholder = row.querySelector('.js-secret-value-placeholder');
-
- valueInput.value = 'bar';
- $(valueInput).trigger('input');
-
- expect(valuePlaceholder.classList.contains(HIDE_CLASS)).toBe(true);
- expect(valueInput.classList.contains(HIDE_CLASS)).toBe(false);
-
- ajaxVariableList
- .onSaveClicked()
- .then(() => {
- expect(valuePlaceholder.classList.contains(HIDE_CLASS)).toBe(false);
- expect(valueInput.classList.contains(HIDE_CLASS)).toBe(true);
- })
- .then(done)
- .catch(done.fail);
- });
-
- it('shows error box with validation errors', done => {
- const validationError = 'some validation error';
- mock.onPatch(VARIABLE_PATCH_ENDPOINT).reply(400, [validationError]);
-
- expect(errorBox.classList.contains(HIDE_CLASS)).toEqual(true);
-
- ajaxVariableList
- .onSaveClicked()
- .then(() => {
- expect(errorBox.classList.contains(HIDE_CLASS)).toEqual(false);
- expect(errorBox.textContent.trim().replace(/\n+\s+/m, ' ')).toEqual(
- `Validation failed ${validationError}`,
- );
- })
- .then(done)
- .catch(done.fail);
- });
-
- it('shows flash message when request fails', done => {
- mock.onPatch(VARIABLE_PATCH_ENDPOINT).reply(500);
-
- expect(errorBox.classList.contains(HIDE_CLASS)).toEqual(true);
-
- ajaxVariableList
- .onSaveClicked()
- .then(() => {
- expect(errorBox.classList.contains(HIDE_CLASS)).toEqual(true);
- })
- .then(done)
- .catch(done.fail);
- });
- });
-
- describe('updateRowsWithPersistedVariables', () => {
- beforeEach(() => {
- loadFixtures('projects/ci_cd_settings_with_variables.html');
- container = document.querySelector('.js-ci-variable-list-section');
-
- const ajaxVariableListEl = document.querySelector('.js-ci-variable-list-section');
- saveButton = ajaxVariableListEl.querySelector('.js-ci-variables-save-button');
- errorBox = container.querySelector('.js-ci-variable-error-box');
- ajaxVariableList = new AjaxFormVariableList({
- container,
- formField: 'variables',
- saveButton,
- errorBox,
- saveEndpoint: container.dataset.saveEndpoint,
- });
- });
-
- it('removes variable that was removed', () => {
- expect(container.querySelectorAll('.js-row').length).toBe(3);
-
- container.querySelector('.js-row-remove-button').click();
-
- expect(container.querySelectorAll('.js-row').length).toBe(3);
-
- ajaxVariableList.updateRowsWithPersistedVariables([]);
-
- expect(container.querySelectorAll('.js-row').length).toBe(2);
- });
-
- it('updates new variable row with persisted ID', () => {
- const row = container.querySelector('.js-row:last-child');
- const idInput = row.querySelector('.js-ci-variable-input-id');
- const keyInput = row.querySelector('.js-ci-variable-input-key');
- const valueInput = row.querySelector('.js-ci-variable-input-value');
-
- keyInput.value = 'foo';
- $(keyInput).trigger('input');
- valueInput.value = 'bar';
- $(valueInput).trigger('input');
-
- expect(idInput.value).toEqual('');
-
- ajaxVariableList.updateRowsWithPersistedVariables([
- {
- id: 3,
- key: 'foo',
- value: 'bar',
- },
- ]);
-
- expect(idInput.value).toEqual('3');
- expect(row.dataset.isPersisted).toEqual('true');
- });
- });
-
- describe('maskableRegex', () => {
- it('takes in the regex provided by the data attribute', () => {
- expect(container.dataset.maskableRegex).toBe('^[a-zA-Z0-9_+=/@:.-]{8,}$');
- expect(ajaxVariableList.maskableRegex).toBe(container.dataset.maskableRegex);
- });
- });
-});
diff --git a/spec/javascripts/ci_variable_list/ci_variable_list_spec.js b/spec/javascripts/ci_variable_list/ci_variable_list_spec.js
deleted file mode 100644
index c0c3a83a44b..00000000000
--- a/spec/javascripts/ci_variable_list/ci_variable_list_spec.js
+++ /dev/null
@@ -1,294 +0,0 @@
-import $ from 'jquery';
-import getSetTimeoutPromise from 'spec/helpers/set_timeout_promise_helper';
-import VariableList from '~/ci_variable_list/ci_variable_list';
-
-const HIDE_CLASS = 'hide';
-
-describe('VariableList', () => {
- preloadFixtures('pipeline_schedules/edit.html');
- preloadFixtures('pipeline_schedules/edit_with_variables.html');
- preloadFixtures('projects/ci_cd_settings.html');
-
- let $wrapper;
- let variableList;
-
- describe('with only key/value inputs', () => {
- describe('with no variables', () => {
- beforeEach(() => {
- loadFixtures('pipeline_schedules/edit.html');
- $wrapper = $('.js-ci-variable-list-section');
-
- variableList = new VariableList({
- container: $wrapper,
- formField: 'schedule',
- });
- variableList.init();
- });
-
- it('should remove the row when clicking the remove button', () => {
- $wrapper.find('.js-row-remove-button').trigger('click');
-
- expect($wrapper.find('.js-row').length).toBe(0);
- });
-
- it('should add another row when editing the last rows key input', () => {
- const $row = $wrapper.find('.js-row');
- $row
- .find('.js-ci-variable-input-key')
- .val('foo')
- .trigger('input');
-
- expect($wrapper.find('.js-row').length).toBe(2);
-
- // Check for the correct default in the new row
- const $keyInput = $wrapper.find('.js-row:last-child').find('.js-ci-variable-input-key');
-
- expect($keyInput.val()).toBe('');
- });
-
- it('should add another row when editing the last rows value textarea', () => {
- const $row = $wrapper.find('.js-row');
- $row
- .find('.js-ci-variable-input-value')
- .val('foo')
- .trigger('input');
-
- expect($wrapper.find('.js-row').length).toBe(2);
-
- // Check for the correct default in the new row
- const $valueInput = $wrapper.find('.js-row:last-child').find('.js-ci-variable-input-key');
-
- expect($valueInput.val()).toBe('');
- });
-
- it('should remove empty row after blurring', () => {
- const $row = $wrapper.find('.js-row');
- $row
- .find('.js-ci-variable-input-key')
- .val('foo')
- .trigger('input');
-
- expect($wrapper.find('.js-row').length).toBe(2);
-
- $row
- .find('.js-ci-variable-input-key')
- .val('')
- .trigger('input')
- .trigger('blur');
-
- expect($wrapper.find('.js-row').length).toBe(1);
- });
- });
-
- describe('with persisted variables', () => {
- beforeEach(() => {
- loadFixtures('pipeline_schedules/edit_with_variables.html');
- $wrapper = $('.js-ci-variable-list-section');
-
- variableList = new VariableList({
- container: $wrapper,
- formField: 'schedule',
- });
- variableList.init();
- });
-
- it('should have "Reveal values" button initially when there are already variables', () => {
- expect($wrapper.find('.js-secret-value-reveal-button').text()).toBe('Reveal values');
- });
-
- it('should reveal hidden values', () => {
- const $row = $wrapper.find('.js-row:first-child');
- const $inputValue = $row.find('.js-ci-variable-input-value');
- const $placeholder = $row.find('.js-secret-value-placeholder');
-
- expect($placeholder.hasClass(HIDE_CLASS)).toBe(false);
- expect($inputValue.hasClass(HIDE_CLASS)).toBe(true);
-
- // Reveal values
- $wrapper.find('.js-secret-value-reveal-button').click();
-
- expect($placeholder.hasClass(HIDE_CLASS)).toBe(true);
- expect($inputValue.hasClass(HIDE_CLASS)).toBe(false);
- });
- });
- });
-
- describe('with all inputs(key, value, protected)', () => {
- beforeEach(() => {
- loadFixtures('projects/ci_cd_settings.html');
- $wrapper = $('.js-ci-variable-list-section');
-
- $wrapper.find('.js-ci-variable-input-protected').attr('data-default', 'false');
-
- variableList = new VariableList({
- container: $wrapper,
- formField: 'variables',
- });
- variableList.init();
- });
-
- it('should not add another row when editing the last rows protected checkbox', done => {
- const $row = $wrapper.find('.js-row:last-child');
- $row.find('.ci-variable-protected-item .js-project-feature-toggle').click();
-
- getSetTimeoutPromise()
- .then(() => {
- expect($wrapper.find('.js-row').length).toBe(1);
- })
- .then(done)
- .catch(done.fail);
- });
-
- it('should not add another row when editing the last rows masked checkbox', done => {
- const $row = $wrapper.find('.js-row:last-child');
- $row.find('.ci-variable-masked-item .js-project-feature-toggle').click();
-
- getSetTimeoutPromise()
- .then(() => {
- expect($wrapper.find('.js-row').length).toBe(1);
- })
- .then(done)
- .catch(done.fail);
- });
-
- describe('validateMaskability', () => {
- let $row;
-
- const maskingErrorElement = '.js-row:last-child .masking-validation-error';
-
- beforeEach(() => {
- $row = $wrapper.find('.js-row:last-child');
- $row.find('.ci-variable-masked-item .js-project-feature-toggle').click();
- });
-
- it('has a regex provided via a data attribute', () => {
- expect($wrapper.attr('data-maskable-regex')).toBe('^[a-zA-Z0-9_+=/@:.-]{8,}$');
- });
-
- it('allows values that are 8 characters long', done => {
- $row.find('.js-ci-variable-input-value').val('looooong');
-
- getSetTimeoutPromise()
- .then(() => {
- expect($wrapper.find(maskingErrorElement)).toHaveClass('hide');
- })
- .then(done)
- .catch(done.fail);
- });
-
- it('rejects values that are shorter than 8 characters', done => {
- $row.find('.js-ci-variable-input-value').val('short');
-
- getSetTimeoutPromise()
- .then(() => {
- expect($wrapper.find(maskingErrorElement)).toBeVisible();
- })
- .then(done)
- .catch(done.fail);
- });
-
- it('allows values with base 64 characters', done => {
- $row.find('.js-ci-variable-input-value').val('abcABC123_+=/-');
-
- getSetTimeoutPromise()
- .then(() => {
- expect($wrapper.find(maskingErrorElement)).toHaveClass('hide');
- })
- .then(done)
- .catch(done.fail);
- });
-
- it('rejects values with other special characters', done => {
- $row.find('.js-ci-variable-input-value').val('1234567$');
-
- getSetTimeoutPromise()
- .then(() => {
- expect($wrapper.find(maskingErrorElement)).toBeVisible();
- })
- .then(done)
- .catch(done.fail);
- });
- });
- });
-
- describe('toggleEnableRow method', () => {
- beforeEach(() => {
- loadFixtures('pipeline_schedules/edit_with_variables.html');
- $wrapper = $('.js-ci-variable-list-section');
-
- variableList = new VariableList({
- container: $wrapper,
- formField: 'variables',
- });
- variableList.init();
- });
-
- it('should disable all key inputs', () => {
- expect($wrapper.find('.js-ci-variable-input-key:not([disabled])').length).toBe(3);
-
- variableList.toggleEnableRow(false);
-
- expect($wrapper.find('.js-ci-variable-input-key[disabled]').length).toBe(3);
- });
-
- it('should disable all remove buttons', () => {
- expect($wrapper.find('.js-row-remove-button:not([disabled])').length).toBe(3);
-
- variableList.toggleEnableRow(false);
-
- expect($wrapper.find('.js-row-remove-button[disabled]').length).toBe(3);
- });
-
- it('should enable all remove buttons', () => {
- variableList.toggleEnableRow(false);
-
- expect($wrapper.find('.js-row-remove-button[disabled]').length).toBe(3);
-
- variableList.toggleEnableRow(true);
-
- expect($wrapper.find('.js-row-remove-button:not([disabled])').length).toBe(3);
- });
-
- it('should enable all key inputs', () => {
- variableList.toggleEnableRow(false);
-
- expect($wrapper.find('.js-ci-variable-input-key[disabled]').length).toBe(3);
-
- variableList.toggleEnableRow(true);
-
- expect($wrapper.find('.js-ci-variable-input-key:not([disabled])').length).toBe(3);
- });
- });
-
- describe('hideValues', () => {
- beforeEach(() => {
- loadFixtures('projects/ci_cd_settings.html');
- $wrapper = $('.js-ci-variable-list-section');
-
- variableList = new VariableList({
- container: $wrapper,
- formField: 'variables',
- });
- variableList.init();
- });
-
- it('should hide value input and show placeholder stars', () => {
- const $row = $wrapper.find('.js-row');
- const $inputValue = $row.find('.js-ci-variable-input-value');
- const $placeholder = $row.find('.js-secret-value-placeholder');
-
- $row
- .find('.js-ci-variable-input-value')
- .val('foo')
- .trigger('input');
-
- expect($placeholder.hasClass(HIDE_CLASS)).toBe(true);
- expect($inputValue.hasClass(HIDE_CLASS)).toBe(false);
-
- variableList.hideValues();
-
- expect($placeholder.hasClass(HIDE_CLASS)).toBe(false);
- expect($inputValue.hasClass(HIDE_CLASS)).toBe(true);
- });
- });
-});
diff --git a/spec/javascripts/close_reopen_report_toggle_spec.js b/spec/javascripts/close_reopen_report_toggle_spec.js
deleted file mode 100644
index 04a7ae7f429..00000000000
--- a/spec/javascripts/close_reopen_report_toggle_spec.js
+++ /dev/null
@@ -1,272 +0,0 @@
-/* eslint-disable jasmine/no-unsafe-spy */
-
-import CloseReopenReportToggle from '~/close_reopen_report_toggle';
-import DropLab from '~/droplab/drop_lab';
-
-describe('CloseReopenReportToggle', () => {
- describe('class constructor', () => {
- const dropdownTrigger = {};
- const dropdownList = {};
- const button = {};
- let commentTypeToggle;
-
- beforeEach(function() {
- commentTypeToggle = new CloseReopenReportToggle({
- dropdownTrigger,
- dropdownList,
- button,
- });
- });
-
- it('sets .dropdownTrigger', function() {
- expect(commentTypeToggle.dropdownTrigger).toBe(dropdownTrigger);
- });
-
- it('sets .dropdownList', function() {
- expect(commentTypeToggle.dropdownList).toBe(dropdownList);
- });
-
- it('sets .button', function() {
- expect(commentTypeToggle.button).toBe(button);
- });
- });
-
- describe('initDroplab', () => {
- let closeReopenReportToggle;
- const dropdownList = jasmine.createSpyObj('dropdownList', ['querySelector']);
- const dropdownTrigger = {};
- const button = {};
- const reopenItem = {};
- const closeItem = {};
- const config = {};
-
- beforeEach(() => {
- spyOn(DropLab.prototype, 'init');
- dropdownList.querySelector.and.returnValues(reopenItem, closeItem);
-
- closeReopenReportToggle = new CloseReopenReportToggle({
- dropdownTrigger,
- dropdownList,
- button,
- });
-
- spyOn(closeReopenReportToggle, 'setConfig').and.returnValue(config);
-
- closeReopenReportToggle.initDroplab();
- });
-
- it('sets .reopenItem and .closeItem', () => {
- expect(dropdownList.querySelector).toHaveBeenCalledWith('.reopen-item');
- expect(dropdownList.querySelector).toHaveBeenCalledWith('.close-item');
- expect(closeReopenReportToggle.reopenItem).toBe(reopenItem);
- expect(closeReopenReportToggle.closeItem).toBe(closeItem);
- });
-
- it('sets .droplab', () => {
- expect(closeReopenReportToggle.droplab).toEqual(jasmine.any(Object));
- });
-
- it('calls .setConfig', () => {
- expect(closeReopenReportToggle.setConfig).toHaveBeenCalled();
- });
-
- it('calls droplab.init', () => {
- expect(DropLab.prototype.init).toHaveBeenCalledWith(
- dropdownTrigger,
- dropdownList,
- jasmine.any(Array),
- config,
- );
- });
- });
-
- describe('updateButton', () => {
- let closeReopenReportToggle;
- const dropdownList = {};
- const dropdownTrigger = {};
- const button = jasmine.createSpyObj('button', ['blur']);
- const isClosed = true;
-
- beforeEach(() => {
- closeReopenReportToggle = new CloseReopenReportToggle({
- dropdownTrigger,
- dropdownList,
- button,
- });
-
- spyOn(closeReopenReportToggle, 'toggleButtonType');
-
- closeReopenReportToggle.updateButton(isClosed);
- });
-
- it('calls .toggleButtonType', () => {
- expect(closeReopenReportToggle.toggleButtonType).toHaveBeenCalledWith(isClosed);
- });
-
- it('calls .button.blur', () => {
- expect(closeReopenReportToggle.button.blur).toHaveBeenCalled();
- });
- });
-
- describe('toggleButtonType', () => {
- let closeReopenReportToggle;
- const dropdownList = {};
- const dropdownTrigger = {};
- const button = {};
- const isClosed = true;
- const showItem = jasmine.createSpyObj('showItem', ['click']);
- const hideItem = {};
- showItem.classList = jasmine.createSpyObj('classList', ['add', 'remove']);
- hideItem.classList = jasmine.createSpyObj('classList', ['add', 'remove']);
-
- beforeEach(() => {
- closeReopenReportToggle = new CloseReopenReportToggle({
- dropdownTrigger,
- dropdownList,
- button,
- });
-
- spyOn(closeReopenReportToggle, 'getButtonTypes').and.returnValue([showItem, hideItem]);
-
- closeReopenReportToggle.toggleButtonType(isClosed);
- });
-
- it('calls .getButtonTypes', () => {
- expect(closeReopenReportToggle.getButtonTypes).toHaveBeenCalledWith(isClosed);
- });
-
- it('removes hide class and add selected class to showItem, opposite for hideItem', () => {
- expect(showItem.classList.remove).toHaveBeenCalledWith('hidden');
- expect(showItem.classList.add).toHaveBeenCalledWith('droplab-item-selected');
- expect(hideItem.classList.add).toHaveBeenCalledWith('hidden');
- expect(hideItem.classList.remove).toHaveBeenCalledWith('droplab-item-selected');
- });
-
- it('clicks the showItem', () => {
- expect(showItem.click).toHaveBeenCalled();
- });
- });
-
- describe('getButtonTypes', () => {
- let closeReopenReportToggle;
- const dropdownList = {};
- const dropdownTrigger = {};
- const button = {};
- const reopenItem = {};
- const closeItem = {};
-
- beforeEach(() => {
- closeReopenReportToggle = new CloseReopenReportToggle({
- dropdownTrigger,
- dropdownList,
- button,
- });
-
- closeReopenReportToggle.reopenItem = reopenItem;
- closeReopenReportToggle.closeItem = closeItem;
- });
-
- it('returns reopenItem, closeItem if isClosed is true', () => {
- const buttonTypes = closeReopenReportToggle.getButtonTypes(true);
-
- expect(buttonTypes).toEqual([reopenItem, closeItem]);
- });
-
- it('returns closeItem, reopenItem if isClosed is false', () => {
- const buttonTypes = closeReopenReportToggle.getButtonTypes(false);
-
- expect(buttonTypes).toEqual([closeItem, reopenItem]);
- });
- });
-
- describe('setDisable', () => {
- let closeReopenReportToggle;
- const dropdownList = {};
- const dropdownTrigger = jasmine.createSpyObj('button', ['setAttribute', 'removeAttribute']);
- const button = jasmine.createSpyObj('button', ['setAttribute', 'removeAttribute']);
-
- beforeEach(() => {
- closeReopenReportToggle = new CloseReopenReportToggle({
- dropdownTrigger,
- dropdownList,
- button,
- });
- });
-
- it('disable .button and .dropdownTrigger if shouldDisable is true', () => {
- closeReopenReportToggle.setDisable(true);
-
- expect(button.setAttribute).toHaveBeenCalledWith('disabled', 'true');
- expect(dropdownTrigger.setAttribute).toHaveBeenCalledWith('disabled', 'true');
- });
-
- it('disable .button and .dropdownTrigger if shouldDisable is undefined', () => {
- closeReopenReportToggle.setDisable();
-
- expect(button.setAttribute).toHaveBeenCalledWith('disabled', 'true');
- expect(dropdownTrigger.setAttribute).toHaveBeenCalledWith('disabled', 'true');
- });
-
- it('enable .button and .dropdownTrigger if shouldDisable is false', () => {
- closeReopenReportToggle.setDisable(false);
-
- expect(button.removeAttribute).toHaveBeenCalledWith('disabled');
- expect(dropdownTrigger.removeAttribute).toHaveBeenCalledWith('disabled');
- });
- });
-
- describe('setConfig', () => {
- let closeReopenReportToggle;
- const dropdownList = {};
- const dropdownTrigger = {};
- const button = {};
- let config;
-
- beforeEach(() => {
- closeReopenReportToggle = new CloseReopenReportToggle({
- dropdownTrigger,
- dropdownList,
- button,
- });
-
- config = closeReopenReportToggle.setConfig();
- });
-
- it('returns a config object', () => {
- expect(config).toEqual({
- InputSetter: [
- {
- input: button,
- valueAttribute: 'data-text',
- inputAttribute: 'data-value',
- },
- {
- input: button,
- valueAttribute: 'data-text',
- inputAttribute: 'title',
- },
- {
- input: button,
- valueAttribute: 'data-button-class',
- inputAttribute: 'class',
- },
- {
- input: dropdownTrigger,
- valueAttribute: 'data-toggle-class',
- inputAttribute: 'class',
- },
- {
- input: button,
- valueAttribute: 'data-url',
- inputAttribute: 'href',
- },
- {
- input: button,
- valueAttribute: 'data-method',
- inputAttribute: 'data-method',
- },
- ],
- });
- });
- });
-});
diff --git a/spec/javascripts/commits_spec.js b/spec/javascripts/commits_spec.js
deleted file mode 100644
index 28b89157bd3..00000000000
--- a/spec/javascripts/commits_spec.js
+++ /dev/null
@@ -1,98 +0,0 @@
-import $ from 'jquery';
-import 'vendor/jquery.endless-scroll';
-import MockAdapter from 'axios-mock-adapter';
-import axios from '~/lib/utils/axios_utils';
-import CommitsList from '~/commits';
-import Pager from '~/pager';
-
-describe('Commits List', () => {
- let commitsList;
-
- beforeEach(() => {
- setFixtures(`
- <form class="commits-search-form" action="/h5bp/html5-boilerplate/commits/master">
- <input id="commits-search">
- </form>
- <ol id="commits-list"></ol>
- `);
- spyOn(Pager, 'init').and.stub();
- commitsList = new CommitsList(25);
- });
-
- it('should be defined', () => {
- expect(CommitsList).toBeDefined();
- });
-
- describe('processCommits', () => {
- it('should join commit headers', () => {
- commitsList.$contentList = $(`
- <div>
- <li class="commit-header" data-day="2016-09-20">
- <span class="day">20 Sep, 2016</span>
- <span class="commits-count">1 commit</span>
- </li>
- <li class="commit"></li>
- </div>
- `);
-
- const data = `
- <li class="commit-header" data-day="2016-09-20">
- <span class="day">20 Sep, 2016</span>
- <span class="commits-count">1 commit</span>
- </li>
- <li class="commit"></li>
- `;
-
- // The last commit header should be removed
- // since the previous one has the same data-day value.
- expect(commitsList.processCommits(data).find('li.commit-header').length).toBe(0);
- });
- });
-
- describe('on entering input', () => {
- let ajaxSpy;
- let mock;
-
- beforeEach(() => {
- commitsList.searchField.val('');
-
- spyOn(window.history, 'replaceState').and.stub();
- mock = new MockAdapter(axios);
-
- mock.onGet('/h5bp/html5-boilerplate/commits/master').reply(200, {
- html: '<li>Result</li>',
- });
-
- ajaxSpy = spyOn(axios, 'get').and.callThrough();
- });
-
- afterEach(() => {
- mock.restore();
- });
-
- it('should save the last search string', done => {
- commitsList.searchField.val('GitLab');
- commitsList
- .filterResults()
- .then(() => {
- expect(ajaxSpy).toHaveBeenCalled();
- expect(commitsList.lastSearch).toEqual('GitLab');
-
- done();
- })
- .catch(done.fail);
- });
-
- it('should not make ajax call if the input does not change', done => {
- commitsList
- .filterResults()
- .then(() => {
- expect(ajaxSpy).not.toHaveBeenCalled();
- expect(commitsList.lastSearch).toEqual('');
-
- done();
- })
- .catch(done.fail);
- });
- });
-});
diff --git a/spec/javascripts/deploy_keys/components/action_btn_spec.js b/spec/javascripts/deploy_keys/components/action_btn_spec.js
deleted file mode 100644
index 5bf72cc0018..00000000000
--- a/spec/javascripts/deploy_keys/components/action_btn_spec.js
+++ /dev/null
@@ -1,72 +0,0 @@
-import Vue from 'vue';
-import eventHub from '~/deploy_keys/eventhub';
-import actionBtn from '~/deploy_keys/components/action_btn.vue';
-
-describe('Deploy keys action btn', () => {
- const data = getJSONFixture('deploy_keys/keys.json');
- const deployKey = data.enabled_keys[0];
- let vm;
-
- beforeEach(done => {
- const ActionBtnComponent = Vue.extend({
- components: {
- actionBtn,
- },
- data() {
- return {
- deployKey,
- };
- },
- template: `
- <action-btn
- :deploy-key="deployKey"
- type="enable">
- Enable
- </action-btn>`,
- });
-
- vm = new ActionBtnComponent().$mount();
-
- Vue.nextTick()
- .then(done)
- .catch(done.fail);
- });
-
- it('renders the default slot', () => {
- expect(vm.$el.textContent.trim()).toBe('Enable');
- });
-
- it('sends eventHub event with btn type', done => {
- spyOn(eventHub, '$emit');
-
- vm.$el.click();
-
- Vue.nextTick(() => {
- expect(eventHub.$emit).toHaveBeenCalledWith('enable.key', deployKey, jasmine.anything());
-
- done();
- });
- });
-
- it('shows loading spinner after click', done => {
- vm.$el.click();
-
- Vue.nextTick(() => {
- expect(vm.$el.querySelector('.fa')).toBeDefined();
-
- done();
- });
- });
-
- it('disables button after click', done => {
- vm.$el.click();
-
- Vue.nextTick(() => {
- expect(vm.$el.classList.contains('disabled')).toBeTruthy();
-
- expect(vm.$el.getAttribute('disabled')).toBe('disabled');
-
- done();
- });
- });
-});
diff --git a/spec/javascripts/deploy_keys/components/app_spec.js b/spec/javascripts/deploy_keys/components/app_spec.js
deleted file mode 100644
index c9a9814d122..00000000000
--- a/spec/javascripts/deploy_keys/components/app_spec.js
+++ /dev/null
@@ -1,155 +0,0 @@
-import Vue from 'vue';
-import MockAdapter from 'axios-mock-adapter';
-import { TEST_HOST } from 'spec/test_constants';
-import axios from '~/lib/utils/axios_utils';
-import eventHub from '~/deploy_keys/eventhub';
-import deployKeysApp from '~/deploy_keys/components/app.vue';
-
-describe('Deploy keys app component', () => {
- const data = getJSONFixture('deploy_keys/keys.json');
- let vm;
- let mock;
-
- beforeEach(done => {
- // set up axios mock before component
- mock = new MockAdapter(axios);
- mock.onGet(`${TEST_HOST}/dummy/`).replyOnce(200, data);
-
- const Component = Vue.extend(deployKeysApp);
-
- vm = new Component({
- propsData: {
- endpoint: `${TEST_HOST}/dummy`,
- projectId: '8',
- },
- }).$mount();
-
- setTimeout(done);
- });
-
- afterEach(() => {
- mock.restore();
- });
-
- it('renders loading icon', done => {
- vm.store.keys = {};
- vm.isLoading = false;
-
- Vue.nextTick(() => {
- expect(vm.$el.querySelectorAll('.deploy-keys .nav-links li').length).toBe(0);
-
- expect(vm.$el.querySelector('.fa-spinner')).toBeDefined();
-
- done();
- });
- });
-
- it('renders keys panels', () => {
- expect(vm.$el.querySelectorAll('.deploy-keys .nav-links li').length).toBe(3);
- });
-
- it('renders the titles with keys count', () => {
- const textContent = selector => {
- const element = vm.$el.querySelector(`${selector}`);
-
- expect(element).not.toBeNull();
- return element.textContent.trim();
- };
-
- expect(textContent('.js-deployKeys-tab-enabled_keys')).toContain('Enabled deploy keys');
- expect(textContent('.js-deployKeys-tab-available_project_keys')).toContain(
- 'Privately accessible deploy keys',
- );
-
- expect(textContent('.js-deployKeys-tab-public_keys')).toContain(
- 'Publicly accessible deploy keys',
- );
-
- expect(textContent('.js-deployKeys-tab-enabled_keys .badge')).toBe(
- `${vm.store.keys.enabled_keys.length}`,
- );
-
- expect(textContent('.js-deployKeys-tab-available_project_keys .badge')).toBe(
- `${vm.store.keys.available_project_keys.length}`,
- );
-
- expect(textContent('.js-deployKeys-tab-public_keys .badge')).toBe(
- `${vm.store.keys.public_keys.length}`,
- );
- });
-
- it('does not render key panels when keys object is empty', done => {
- vm.store.keys = {};
-
- Vue.nextTick(() => {
- expect(vm.$el.querySelectorAll('.deploy-keys .nav-links li').length).toBe(0);
-
- done();
- });
- });
-
- it('re-fetches deploy keys when enabling a key', done => {
- const key = data.public_keys[0];
-
- spyOn(vm.service, 'getKeys');
- spyOn(vm.service, 'enableKey').and.callFake(() => Promise.resolve());
-
- eventHub.$emit('enable.key', key);
-
- Vue.nextTick(() => {
- expect(vm.service.enableKey).toHaveBeenCalledWith(key.id);
- expect(vm.service.getKeys).toHaveBeenCalled();
- done();
- });
- });
-
- it('re-fetches deploy keys when disabling a key', done => {
- const key = data.public_keys[0];
-
- spyOn(window, 'confirm').and.returnValue(true);
- spyOn(vm.service, 'getKeys');
- spyOn(vm.service, 'disableKey').and.callFake(() => Promise.resolve());
-
- eventHub.$emit('disable.key', key);
-
- Vue.nextTick(() => {
- expect(vm.service.disableKey).toHaveBeenCalledWith(key.id);
- expect(vm.service.getKeys).toHaveBeenCalled();
- done();
- });
- });
-
- it('calls disableKey when removing a key', done => {
- const key = data.public_keys[0];
-
- spyOn(window, 'confirm').and.returnValue(true);
- spyOn(vm.service, 'getKeys');
- spyOn(vm.service, 'disableKey').and.callFake(() => Promise.resolve());
-
- eventHub.$emit('remove.key', key);
-
- Vue.nextTick(() => {
- expect(vm.service.disableKey).toHaveBeenCalledWith(key.id);
- expect(vm.service.getKeys).toHaveBeenCalled();
- done();
- });
- });
-
- it('hasKeys returns true when there are keys', () => {
- expect(vm.hasKeys).toEqual(3);
- });
-
- it('resets disable button loading state', done => {
- spyOn(window, 'confirm').and.returnValue(false);
-
- const btn = vm.$el.querySelector('.btn-warning');
-
- btn.click();
-
- Vue.nextTick(() => {
- expect(btn.querySelector('.btn-warning')).not.toExist();
-
- done();
- });
- });
-});
diff --git a/spec/javascripts/deploy_keys/components/key_spec.js b/spec/javascripts/deploy_keys/components/key_spec.js
deleted file mode 100644
index 7117dc4a9ee..00000000000
--- a/spec/javascripts/deploy_keys/components/key_spec.js
+++ /dev/null
@@ -1,157 +0,0 @@
-import Vue from 'vue';
-import DeployKeysStore from '~/deploy_keys/store';
-import key from '~/deploy_keys/components/key.vue';
-import { getTimeago } from '~/lib/utils/datetime_utility';
-
-describe('Deploy keys key', () => {
- let vm;
- const KeyComponent = Vue.extend(key);
- const data = getJSONFixture('deploy_keys/keys.json');
- const createComponent = deployKey => {
- const store = new DeployKeysStore();
- store.keys = data;
-
- vm = new KeyComponent({
- propsData: {
- deployKey,
- store,
- endpoint: 'https://test.host/dummy/endpoint',
- },
- }).$mount();
- };
-
- describe('enabled key', () => {
- const deployKey = data.enabled_keys[0];
-
- beforeEach(done => {
- createComponent(deployKey);
-
- setTimeout(done);
- });
-
- it('renders the keys title', () => {
- expect(vm.$el.querySelector('.title').textContent.trim()).toContain('My title');
- });
-
- it('renders human friendly formatted created date', () => {
- expect(vm.$el.querySelector('.key-created-at').textContent.trim()).toBe(
- `${getTimeago().format(deployKey.created_at)}`,
- );
- });
-
- it('shows pencil button for editing', () => {
- expect(vm.$el.querySelector('.btn .ic-pencil')).toExist();
- });
-
- it('shows disable button when the project is not deletable', () => {
- expect(vm.$el.querySelector('.btn .ic-cancel')).toExist();
- });
-
- it('shows remove button when the project is deletable', done => {
- vm.deployKey.destroyed_when_orphaned = true;
- vm.deployKey.almost_orphaned = true;
- Vue.nextTick(() => {
- expect(vm.$el.querySelector('.btn .ic-remove')).toExist();
- done();
- });
- });
- });
-
- describe('deploy key labels', () => {
- it('shows write access title when key has write access', done => {
- vm.deployKey.deploy_keys_projects[0].can_push = true;
-
- Vue.nextTick(() => {
- expect(
- vm.$el.querySelector('.deploy-project-label').getAttribute('data-original-title'),
- ).toBe('Write access allowed');
- done();
- });
- });
-
- it('does not show write access title when key has write access', done => {
- vm.deployKey.deploy_keys_projects[0].can_push = false;
-
- Vue.nextTick(() => {
- expect(
- vm.$el.querySelector('.deploy-project-label').getAttribute('data-original-title'),
- ).toBe('Read access only');
- done();
- });
- });
-
- it('shows expandable button if more than two projects', () => {
- const labels = vm.$el.querySelectorAll('.deploy-project-label');
-
- expect(labels.length).toBe(2);
- expect(labels[1].textContent).toContain('others');
- expect(labels[1].getAttribute('data-original-title')).toContain('Expand');
- });
-
- it('expands all project labels after click', done => {
- const { length } = vm.deployKey.deploy_keys_projects;
- vm.$el.querySelectorAll('.deploy-project-label')[1].click();
-
- Vue.nextTick(() => {
- const labels = vm.$el.querySelectorAll('.deploy-project-label');
-
- expect(labels.length).toBe(length);
- expect(labels[1].textContent).not.toContain(`+${length} others`);
- expect(labels[1].getAttribute('data-original-title')).not.toContain('Expand');
- done();
- });
- });
-
- it('shows two projects', done => {
- vm.deployKey.deploy_keys_projects = [...vm.deployKey.deploy_keys_projects].slice(0, 2);
-
- Vue.nextTick(() => {
- const labels = vm.$el.querySelectorAll('.deploy-project-label');
-
- expect(labels.length).toBe(2);
- expect(labels[1].textContent).toContain(
- vm.deployKey.deploy_keys_projects[1].project.full_name,
- );
- done();
- });
- });
- });
-
- describe('public keys', () => {
- const deployKey = data.public_keys[0];
-
- beforeEach(done => {
- createComponent(deployKey);
-
- setTimeout(done);
- });
-
- it('renders deploy keys without any enabled projects', done => {
- vm.deployKey.deploy_keys_projects = [];
-
- Vue.nextTick(() => {
- expect(vm.$el.querySelector('.deploy-project-list').textContent.trim()).toBe('None');
-
- done();
- });
- });
-
- it('shows enable button', () => {
- expect(vm.$el.querySelectorAll('.btn')[0].textContent.trim()).toBe('Enable');
- });
-
- it('shows pencil button for editing', () => {
- expect(vm.$el.querySelector('.btn .ic-pencil')).toExist();
- });
-
- it('shows disable button when key is enabled', done => {
- vm.store.keys.enabled_keys.push(deployKey);
-
- Vue.nextTick(() => {
- expect(vm.$el.querySelector('.btn .ic-cancel')).toExist();
-
- done();
- });
- });
- });
-});
diff --git a/spec/javascripts/deploy_keys/components/keys_panel_spec.js b/spec/javascripts/deploy_keys/components/keys_panel_spec.js
deleted file mode 100644
index f71f5ccf082..00000000000
--- a/spec/javascripts/deploy_keys/components/keys_panel_spec.js
+++ /dev/null
@@ -1,63 +0,0 @@
-import Vue from 'vue';
-import DeployKeysStore from '~/deploy_keys/store';
-import deployKeysPanel from '~/deploy_keys/components/keys_panel.vue';
-
-describe('Deploy keys panel', () => {
- const data = getJSONFixture('deploy_keys/keys.json');
- let vm;
-
- beforeEach(done => {
- const DeployKeysPanelComponent = Vue.extend(deployKeysPanel);
- const store = new DeployKeysStore();
- store.keys = data;
-
- vm = new DeployKeysPanelComponent({
- propsData: {
- title: 'test',
- keys: data.enabled_keys,
- showHelpBox: true,
- store,
- endpoint: 'https://test.host/dummy/endpoint',
- },
- }).$mount();
-
- setTimeout(done);
- });
-
- it('renders list of keys', () => {
- expect(vm.$el.querySelectorAll('.deploy-key').length).toBe(vm.keys.length);
- });
-
- it('renders table header', () => {
- const tableHeader = vm.$el.querySelector('.table-row-header');
-
- expect(tableHeader).toExist();
- expect(tableHeader.textContent).toContain('Deploy key');
- expect(tableHeader.textContent).toContain('Project usage');
- expect(tableHeader.textContent).toContain('Created');
- });
-
- it('renders help box if keys are empty', done => {
- vm.keys = [];
-
- Vue.nextTick(() => {
- expect(vm.$el.querySelector('.settings-message')).toBeDefined();
-
- expect(vm.$el.querySelector('.settings-message').textContent.trim()).toBe(
- 'No deploy keys found. Create one with the form above.',
- );
-
- done();
- });
- });
-
- it('renders no table header if keys are empty', done => {
- vm.keys = [];
-
- Vue.nextTick(() => {
- expect(vm.$el.querySelector('.table-row-header')).not.toExist();
-
- done();
- });
- });
-});
diff --git a/spec/javascripts/diff_comments_store_spec.js b/spec/javascripts/diff_comments_store_spec.js
deleted file mode 100644
index a6d363ce88e..00000000000
--- a/spec/javascripts/diff_comments_store_spec.js
+++ /dev/null
@@ -1,141 +0,0 @@
-/* eslint-disable jasmine/no-global-setup, dot-notation, jasmine/no-expect-in-setup-teardown */
-/* global CommentsStore */
-
-import '~/diff_notes/models/discussion';
-import '~/diff_notes/models/note';
-import '~/diff_notes/stores/comments';
-
-function createDiscussion(noteId = 1, resolved = true) {
- CommentsStore.create({
- discussionId: 'a',
- noteId,
- canResolve: true,
- resolved,
- resolvedBy: 'test',
- authorName: 'test',
- authorAvatar: 'test',
- noteTruncated: 'test...',
- });
-}
-
-beforeEach(() => {
- CommentsStore.state = {};
-});
-
-describe('New discussion', () => {
- it('creates new discussion', () => {
- expect(Object.keys(CommentsStore.state).length).toBe(0);
- createDiscussion();
-
- expect(Object.keys(CommentsStore.state).length).toBe(1);
- });
-
- it('creates new note in discussion', () => {
- createDiscussion();
- createDiscussion(2);
-
- const discussion = CommentsStore.state['a'];
-
- expect(Object.keys(discussion.notes).length).toBe(2);
- });
-});
-
-describe('Get note', () => {
- beforeEach(() => {
- expect(Object.keys(CommentsStore.state).length).toBe(0);
- createDiscussion();
- });
-
- it('gets note by ID', () => {
- const note = CommentsStore.get('a', 1);
-
- expect(note).toBeDefined();
- expect(note.id).toBe(1);
- });
-});
-
-describe('Delete discussion', () => {
- beforeEach(() => {
- expect(Object.keys(CommentsStore.state).length).toBe(0);
- createDiscussion();
- });
-
- it('deletes discussion by ID', () => {
- CommentsStore.delete('a', 1);
-
- expect(Object.keys(CommentsStore.state).length).toBe(0);
- });
-
- it('deletes discussion when no more notes', () => {
- createDiscussion();
- createDiscussion(2);
-
- expect(Object.keys(CommentsStore.state).length).toBe(1);
- expect(Object.keys(CommentsStore.state['a'].notes).length).toBe(2);
-
- CommentsStore.delete('a', 1);
- CommentsStore.delete('a', 2);
-
- expect(Object.keys(CommentsStore.state).length).toBe(0);
- });
-});
-
-describe('Update note', () => {
- beforeEach(() => {
- expect(Object.keys(CommentsStore.state).length).toBe(0);
- createDiscussion();
- });
-
- it('updates note to be unresolved', () => {
- CommentsStore.update('a', 1, false, 'test');
-
- const note = CommentsStore.get('a', 1);
-
- expect(note.resolved).toBe(false);
- });
-});
-
-describe('Discussion resolved', () => {
- beforeEach(() => {
- expect(Object.keys(CommentsStore.state).length).toBe(0);
- createDiscussion();
- });
-
- it('is resolved with single note', () => {
- const discussion = CommentsStore.state['a'];
-
- expect(discussion.isResolved()).toBe(true);
- });
-
- it('is unresolved with 2 notes', () => {
- const discussion = CommentsStore.state['a'];
- createDiscussion(2, false);
-
- expect(discussion.isResolved()).toBe(false);
- });
-
- it('is resolved with 2 notes', () => {
- const discussion = CommentsStore.state['a'];
- createDiscussion(2);
-
- expect(discussion.isResolved()).toBe(true);
- });
-
- it('resolve all notes', () => {
- const discussion = CommentsStore.state['a'];
- createDiscussion(2, false);
-
- discussion.resolveAllNotes();
-
- expect(discussion.isResolved()).toBe(true);
- });
-
- it('unresolve all notes', () => {
- const discussion = CommentsStore.state['a'];
- createDiscussion(2);
-
- discussion.unResolveAllNotes();
-
- expect(discussion.isResolved()).toBe(false);
- });
-});
diff --git a/spec/javascripts/diffs/create_diffs_store.js b/spec/javascripts/diffs/create_diffs_store.js
deleted file mode 100644
index 9df057dd8b2..00000000000
--- a/spec/javascripts/diffs/create_diffs_store.js
+++ /dev/null
@@ -1,5 +0,0 @@
-// No new code should be added to this file. Instead, modify the
-// file this one re-exports from. For more detail about why, see:
-// https://gitlab.com/gitlab-org/gitlab-foss/merge_requests/31349
-
-export { default } from '../../frontend/diffs/create_diffs_store';
diff --git a/spec/javascripts/diffs/mock_data/diff_discussions.js b/spec/javascripts/diffs/mock_data/diff_discussions.js
deleted file mode 100644
index 17586fddd0f..00000000000
--- a/spec/javascripts/diffs/mock_data/diff_discussions.js
+++ /dev/null
@@ -1,5 +0,0 @@
-// No new code should be added to this file. Instead, modify the
-// file this one re-exports from. For more detail about why, see:
-// https://gitlab.com/gitlab-org/gitlab-foss/merge_requests/31349
-
-export { default } from '../../../frontend/diffs/mock_data/diff_discussions';
diff --git a/spec/javascripts/diffs/mock_data/diff_file.js b/spec/javascripts/diffs/mock_data/diff_file.js
deleted file mode 100644
index 9dc365b7403..00000000000
--- a/spec/javascripts/diffs/mock_data/diff_file.js
+++ /dev/null
@@ -1,5 +0,0 @@
-// No new code should be added to this file. Instead, modify the
-// file this one re-exports from. For more detail about why, see:
-// https://gitlab.com/gitlab-org/gitlab-foss/merge_requests/31349
-
-export { default } from '../../../frontend/diffs/mock_data/diff_file';
diff --git a/spec/javascripts/diffs/mock_data/diff_file_unreadable.js b/spec/javascripts/diffs/mock_data/diff_file_unreadable.js
deleted file mode 100644
index 09a0dc61847..00000000000
--- a/spec/javascripts/diffs/mock_data/diff_file_unreadable.js
+++ /dev/null
@@ -1,5 +0,0 @@
-// No new code should be added to this file. Instead, modify the
-// file this one re-exports from. For more detail about why, see:
-// https://gitlab.com/gitlab-org/gitlab-foss/merge_requests/31349
-
-export { default } from '../../../frontend/diffs/mock_data/diff_file_unreadable';
diff --git a/spec/javascripts/diffs/mock_data/diff_with_commit.js b/spec/javascripts/diffs/mock_data/diff_with_commit.js
deleted file mode 100644
index c36b0239060..00000000000
--- a/spec/javascripts/diffs/mock_data/diff_with_commit.js
+++ /dev/null
@@ -1,7 +0,0 @@
-// No new code should be added to this file. Instead, modify the
-// file this one re-exports from. For more detail about why, see:
-// https://gitlab.com/gitlab-org/gitlab-foss/merge_requests/31349
-
-import getDiffWithCommit from '../../../frontend/diffs/mock_data/diff_with_commit';
-
-export default getDiffWithCommit;
diff --git a/spec/javascripts/diffs/mock_data/merge_request_diffs.js b/spec/javascripts/diffs/mock_data/merge_request_diffs.js
deleted file mode 100644
index de29eb7e560..00000000000
--- a/spec/javascripts/diffs/mock_data/merge_request_diffs.js
+++ /dev/null
@@ -1,7 +0,0 @@
-// No new code should be added to this file. Instead, modify the
-// file this one re-exports from. For more detail about why, see:
-// https://gitlab.com/gitlab-org/gitlab-foss/merge_requests/31349
-
-import diffsMockData from '../../../frontend/diffs/mock_data/merge_request_diffs';
-
-export default diffsMockData;
diff --git a/spec/javascripts/dirty_submit/dirty_submit_collection_spec.js b/spec/javascripts/dirty_submit/dirty_submit_collection_spec.js
deleted file mode 100644
index 47be0b3ce9d..00000000000
--- a/spec/javascripts/dirty_submit/dirty_submit_collection_spec.js
+++ /dev/null
@@ -1,29 +0,0 @@
-import DirtySubmitCollection from '~/dirty_submit/dirty_submit_collection';
-import { setInputValue, createForm } from './helper';
-
-describe('DirtySubmitCollection', () => {
- it('disables submits until there are changes', done => {
- const testElementsCollection = [createForm(), createForm()];
- const forms = testElementsCollection.map(testElements => testElements.form);
-
- new DirtySubmitCollection(forms); // eslint-disable-line no-new
-
- testElementsCollection.forEach(testElements => {
- const { input, submit } = testElements;
- const originalValue = input.value;
-
- expect(submit.disabled).toBe(true);
-
- return setInputValue(input, `${originalValue} changes`)
- .then(() => {
- expect(submit.disabled).toBe(false);
- })
- .then(() => setInputValue(input, originalValue))
- .then(() => {
- expect(submit.disabled).toBe(true);
- })
- .then(done)
- .catch(done.fail);
- });
- });
-});
diff --git a/spec/javascripts/dirty_submit/dirty_submit_form_spec.js b/spec/javascripts/dirty_submit/dirty_submit_form_spec.js
deleted file mode 100644
index 42f806fa1bf..00000000000
--- a/spec/javascripts/dirty_submit/dirty_submit_form_spec.js
+++ /dev/null
@@ -1,114 +0,0 @@
-import { range as rge } from 'lodash';
-import DirtySubmitForm from '~/dirty_submit/dirty_submit_form';
-import { getInputValue, setInputValue, createForm } from './helper';
-
-function expectToToggleDisableOnDirtyUpdate(submit, input) {
- const originalValue = getInputValue(input);
-
- expect(submit.disabled).toBe(true);
-
- return setInputValue(input, `${originalValue} changes`)
- .then(() => expect(submit.disabled).toBe(false))
- .then(() => setInputValue(input, originalValue))
- .then(() => expect(submit.disabled).toBe(true));
-}
-
-describe('DirtySubmitForm', () => {
- const originalThrottleDuration = DirtySubmitForm.THROTTLE_DURATION;
-
- describe('submit button tests', () => {
- beforeEach(() => {
- DirtySubmitForm.THROTTLE_DURATION = 0;
- });
-
- afterEach(() => {
- DirtySubmitForm.THROTTLE_DURATION = originalThrottleDuration;
- });
-
- it('disables submit until there are changes', done => {
- const { form, input, submit } = createForm();
-
- new DirtySubmitForm(form); // eslint-disable-line no-new
-
- return expectToToggleDisableOnDirtyUpdate(submit, input)
- .then(done)
- .catch(done.fail);
- });
-
- it('disables submit until there are changes when initializing with a falsy value', done => {
- const { form, input, submit } = createForm();
- input.value = '';
-
- new DirtySubmitForm(form); // eslint-disable-line no-new
-
- return expectToToggleDisableOnDirtyUpdate(submit, input)
- .then(done)
- .catch(done.fail);
- });
-
- it('disables submit until there are changes for radio inputs', done => {
- const { form, input, submit } = createForm('radio');
-
- new DirtySubmitForm(form); // eslint-disable-line no-new
-
- return expectToToggleDisableOnDirtyUpdate(submit, input)
- .then(done)
- .catch(done.fail);
- });
-
- it('disables submit until there are changes for checkbox inputs', done => {
- const { form, input, submit } = createForm('checkbox');
-
- new DirtySubmitForm(form); // eslint-disable-line no-new
-
- return expectToToggleDisableOnDirtyUpdate(submit, input)
- .then(done)
- .catch(done.fail);
- });
- });
-
- describe('throttling tests', () => {
- beforeEach(() => {
- jasmine.clock().install();
- jasmine.clock().mockDate();
- DirtySubmitForm.THROTTLE_DURATION = 100;
- });
-
- afterEach(() => {
- jasmine.clock().uninstall();
- DirtySubmitForm.THROTTLE_DURATION = originalThrottleDuration;
- });
-
- it('throttles updates when rapid changes are made to a single form element', () => {
- const { form, input } = createForm();
- const updateDirtyInputSpy = spyOn(new DirtySubmitForm(form), 'updateDirtyInput');
-
- rge(10).forEach(i => {
- setInputValue(input, `change ${i}`, false);
- });
-
- jasmine.clock().tick(101);
-
- expect(updateDirtyInputSpy).toHaveBeenCalledTimes(2);
- });
-
- it('does not throttle updates when rapid changes are made to different form elements', () => {
- const form = document.createElement('form');
- const range = rge(10);
- range.forEach(i => {
- form.innerHTML += `<input type="text" name="input-${i}" class="js-input-${i}"/>`;
- });
-
- const updateDirtyInputSpy = spyOn(new DirtySubmitForm(form), 'updateDirtyInput');
-
- range.forEach(i => {
- const input = form.querySelector(`.js-input-${i}`);
- setInputValue(input, `change`, false);
- });
-
- jasmine.clock().tick(101);
-
- expect(updateDirtyInputSpy).toHaveBeenCalledTimes(range.length);
- });
- });
-});
diff --git a/spec/javascripts/dirty_submit/helper.js b/spec/javascripts/dirty_submit/helper.js
deleted file mode 100644
index b51783cb915..00000000000
--- a/spec/javascripts/dirty_submit/helper.js
+++ /dev/null
@@ -1,48 +0,0 @@
-import DirtySubmitForm from '~/dirty_submit/dirty_submit_form';
-import setTimeoutPromiseHelper from '../helpers/set_timeout_promise_helper';
-
-function isCheckableType(type) {
- return /^(radio|checkbox)$/.test(type);
-}
-
-export function setInputValue(element, value) {
- const { type } = element;
- let eventType;
-
- if (isCheckableType(type)) {
- element.checked = !element.checked;
- eventType = 'change';
- } else {
- element.value = value;
- eventType = 'input';
- }
-
- element.dispatchEvent(
- new Event(eventType, {
- bubbles: true,
- }),
- );
-
- return setTimeoutPromiseHelper(DirtySubmitForm.THROTTLE_DURATION);
-}
-
-export function getInputValue(input) {
- return isCheckableType(input.type) ? input.checked : input.value;
-}
-
-export function createForm(type = 'text') {
- const form = document.createElement('form');
- form.innerHTML = `
- <input type="${type}" name="${type}" class="js-input"/>
- <button type="submit" class="js-dirty-submit"></button>
- `;
-
- const input = form.querySelector('.js-input');
- const submit = form.querySelector('.js-dirty-submit');
-
- return {
- form,
- input,
- submit,
- };
-}
diff --git a/spec/javascripts/editor/editor_lite_spec.js b/spec/javascripts/editor/editor_lite_spec.js
deleted file mode 100644
index 106264aa13f..00000000000
--- a/spec/javascripts/editor/editor_lite_spec.js
+++ /dev/null
@@ -1,160 +0,0 @@
-import { editor as monacoEditor, Uri } from 'monaco-editor';
-import Editor from '~/editor/editor_lite';
-import { DEFAULT_THEME, themes } from '~/ide/lib/themes';
-
-describe('Base editor', () => {
- let editorEl;
- let editor;
- const blobContent = 'Foo Bar';
- const blobPath = 'test.md';
- const uri = new Uri('gitlab', false, blobPath);
- const fakeModel = { foo: 'bar' };
-
- beforeEach(() => {
- setFixtures('<div id="editor" data-editor-loading></div>');
- editorEl = document.getElementById('editor');
- editor = new Editor();
- });
-
- afterEach(() => {
- editor.dispose();
- editorEl.remove();
- });
-
- it('initializes Editor with basic properties', () => {
- expect(editor).toBeDefined();
- expect(editor.editorEl).toBe(null);
- expect(editor.blobContent).toEqual('');
- expect(editor.blobPath).toEqual('');
- });
-
- it('removes `editor-loading` data attribute from the target DOM element', () => {
- editor.createInstance({ el: editorEl });
-
- expect(editorEl.dataset.editorLoading).toBeUndefined();
- });
-
- describe('instance of the Editor', () => {
- let modelSpy;
- let instanceSpy;
- let setModel;
- let dispose;
-
- beforeEach(() => {
- setModel = jasmine.createSpy();
- dispose = jasmine.createSpy();
- modelSpy = spyOn(monacoEditor, 'createModel').and.returnValue(fakeModel);
- instanceSpy = spyOn(monacoEditor, 'create').and.returnValue({
- setModel,
- dispose,
- });
- });
-
- it('does nothing if no dom element is supplied', () => {
- editor.createInstance();
-
- expect(editor.editorEl).toBe(null);
- expect(editor.blobContent).toEqual('');
- expect(editor.blobPath).toEqual('');
-
- expect(modelSpy).not.toHaveBeenCalled();
- expect(instanceSpy).not.toHaveBeenCalled();
- expect(setModel).not.toHaveBeenCalled();
- });
-
- it('creates model to be supplied to Monaco editor', () => {
- editor.createInstance({ el: editorEl, blobPath, blobContent });
-
- expect(modelSpy).toHaveBeenCalledWith(blobContent, undefined, uri);
- expect(setModel).toHaveBeenCalledWith(fakeModel);
- });
-
- it('initializes the instance on a supplied DOM node', () => {
- editor.createInstance({ el: editorEl });
-
- expect(editor.editorEl).not.toBe(null);
- expect(instanceSpy).toHaveBeenCalledWith(editorEl, jasmine.anything());
- });
- });
-
- describe('implementation', () => {
- beforeEach(() => {
- editor.createInstance({ el: editorEl, blobPath, blobContent });
- });
-
- afterEach(() => {
- editor.model.dispose();
- });
-
- it('correctly proxies value from the model', () => {
- expect(editor.getValue()).toEqual(blobContent);
- });
-
- it('is capable of changing the language of the model', () => {
- const blobRenamedPath = 'test.js';
-
- expect(editor.model.getLanguageIdentifier().language).toEqual('markdown');
- editor.updateModelLanguage(blobRenamedPath);
-
- expect(editor.model.getLanguageIdentifier().language).toEqual('javascript');
- });
-
- it('falls back to plaintext if there is no language associated with an extension', () => {
- const blobRenamedPath = 'test.myext';
- const spy = spyOn(console, 'error');
-
- editor.updateModelLanguage(blobRenamedPath);
-
- expect(spy).not.toHaveBeenCalled();
- expect(editor.model.getLanguageIdentifier().language).toEqual('plaintext');
- });
- });
-
- describe('syntax highlighting theme', () => {
- let themeDefineSpy;
- let themeSetSpy;
- let defaultScheme;
-
- beforeEach(() => {
- themeDefineSpy = spyOn(monacoEditor, 'defineTheme');
- themeSetSpy = spyOn(monacoEditor, 'setTheme');
- defaultScheme = window.gon.user_color_scheme;
- });
-
- afterEach(() => {
- window.gon.user_color_scheme = defaultScheme;
- });
-
- it('sets default syntax highlighting theme', () => {
- const expectedTheme = themes.find(t => t.name === DEFAULT_THEME);
-
- editor = new Editor();
-
- expect(themeDefineSpy).toHaveBeenCalledWith(DEFAULT_THEME, expectedTheme.data);
- expect(themeSetSpy).toHaveBeenCalledWith(DEFAULT_THEME);
- });
-
- it('sets correct theme if it is set in users preferences', () => {
- const expectedTheme = themes.find(t => t.name !== DEFAULT_THEME);
-
- expect(expectedTheme.name).not.toBe(DEFAULT_THEME);
-
- window.gon.user_color_scheme = expectedTheme.name;
- editor = new Editor();
-
- expect(themeDefineSpy).toHaveBeenCalledWith(expectedTheme.name, expectedTheme.data);
- expect(themeSetSpy).toHaveBeenCalledWith(expectedTheme.name);
- });
-
- it('falls back to default theme if a selected one is not supported yet', () => {
- const name = 'non-existent-theme';
- const nonExistentTheme = { name };
-
- window.gon.user_color_scheme = nonExistentTheme.name;
- editor = new Editor();
-
- expect(themeDefineSpy).not.toHaveBeenCalled();
- expect(themeSetSpy).toHaveBeenCalledWith(DEFAULT_THEME);
- });
- });
-});
diff --git a/spec/javascripts/emoji_spec.js b/spec/javascripts/emoji_spec.js
deleted file mode 100644
index 3db4d9800f1..00000000000
--- a/spec/javascripts/emoji_spec.js
+++ /dev/null
@@ -1,486 +0,0 @@
-import { glEmojiTag } from '~/emoji';
-import isEmojiUnicodeSupported, {
- isFlagEmoji,
- isRainbowFlagEmoji,
- isKeycapEmoji,
- isSkinToneComboEmoji,
- isHorceRacingSkinToneComboEmoji,
- isPersonZwjEmoji,
-} from '~/emoji/support/is_emoji_unicode_supported';
-
-const emptySupportMap = {
- personZwj: false,
- horseRacing: false,
- flag: false,
- skinToneModifier: false,
- '9.0': false,
- '8.0': false,
- '7.0': false,
- 6.1: false,
- '6.0': false,
- 5.2: false,
- 5.1: false,
- 4.1: false,
- '4.0': false,
- 3.2: false,
- '3.0': false,
- 1.1: false,
-};
-
-const emojiFixtureMap = {
- bomb: {
- name: 'bomb',
- moji: '💣',
- unicodeVersion: '6.0',
- },
- construction_worker_tone5: {
- name: 'construction_worker_tone5',
- moji: '👷ðŸ¿',
- unicodeVersion: '8.0',
- },
- five: {
- name: 'five',
- moji: '5ï¸âƒ£',
- unicodeVersion: '3.0',
- },
- grey_question: {
- name: 'grey_question',
- moji: 'â”',
- unicodeVersion: '6.0',
- },
-};
-
-function markupToDomElement(markup) {
- const div = document.createElement('div');
- div.innerHTML = markup;
- return div.firstElementChild;
-}
-
-function testGlEmojiImageFallback(element, name, src) {
- expect(element.tagName.toLowerCase()).toBe('img');
- expect(element.getAttribute('src')).toBe(src);
- expect(element.getAttribute('title')).toBe(`:${name}:`);
- expect(element.getAttribute('alt')).toBe(`:${name}:`);
-}
-
-const defaults = {
- forceFallback: false,
- sprite: false,
-};
-
-function testGlEmojiElement(element, name, unicodeVersion, unicodeMoji, options = {}) {
- const opts = Object.assign({}, defaults, options);
- expect(element.tagName.toLowerCase()).toBe('gl-emoji');
- expect(element.dataset.name).toBe(name);
- expect(element.dataset.fallbackSrc.length).toBeGreaterThan(0);
- expect(element.dataset.unicodeVersion).toBe(unicodeVersion);
-
- const fallbackSpriteClass = `emoji-${name}`;
- if (opts.sprite) {
- expect(element.dataset.fallbackSpriteClass).toBe(fallbackSpriteClass);
- }
-
- if (opts.forceFallback && opts.sprite) {
- expect(element.getAttribute('class')).toBe(`emoji-icon ${fallbackSpriteClass}`);
- }
-
- if (opts.forceFallback && !opts.sprite) {
- // Check for image fallback
- testGlEmojiImageFallback(element.firstElementChild, name, element.dataset.fallbackSrc);
- } else {
- // Otherwise make sure things are still unicode text
- expect(element.textContent.trim()).toBe(unicodeMoji);
- }
-}
-
-describe('gl_emoji', () => {
- describe('glEmojiTag', () => {
- it('bomb emoji', () => {
- const emojiKey = 'bomb';
- const markup = glEmojiTag(emojiFixtureMap[emojiKey].name);
- const glEmojiElement = markupToDomElement(markup);
- testGlEmojiElement(
- glEmojiElement,
- emojiFixtureMap[emojiKey].name,
- emojiFixtureMap[emojiKey].unicodeVersion,
- emojiFixtureMap[emojiKey].moji,
- );
- });
-
- it('bomb emoji with image fallback', () => {
- const emojiKey = 'bomb';
- const markup = glEmojiTag(emojiFixtureMap[emojiKey].name, {
- forceFallback: true,
- });
- const glEmojiElement = markupToDomElement(markup);
- testGlEmojiElement(
- glEmojiElement,
- emojiFixtureMap[emojiKey].name,
- emojiFixtureMap[emojiKey].unicodeVersion,
- emojiFixtureMap[emojiKey].moji,
- {
- forceFallback: true,
- },
- );
- });
-
- it('bomb emoji with sprite fallback readiness', () => {
- const emojiKey = 'bomb';
- const markup = glEmojiTag(emojiFixtureMap[emojiKey].name, {
- sprite: true,
- });
- const glEmojiElement = markupToDomElement(markup);
- testGlEmojiElement(
- glEmojiElement,
- emojiFixtureMap[emojiKey].name,
- emojiFixtureMap[emojiKey].unicodeVersion,
- emojiFixtureMap[emojiKey].moji,
- {
- sprite: true,
- },
- );
- });
-
- it('bomb emoji with sprite fallback', () => {
- const emojiKey = 'bomb';
- const markup = glEmojiTag(emojiFixtureMap[emojiKey].name, {
- forceFallback: true,
- sprite: true,
- });
- const glEmojiElement = markupToDomElement(markup);
- testGlEmojiElement(
- glEmojiElement,
- emojiFixtureMap[emojiKey].name,
- emojiFixtureMap[emojiKey].unicodeVersion,
- emojiFixtureMap[emojiKey].moji,
- {
- forceFallback: true,
- sprite: true,
- },
- );
- });
-
- it('question mark when invalid emoji name given', () => {
- const name = 'invalid_emoji';
- const emojiKey = 'grey_question';
- const markup = glEmojiTag(name);
- const glEmojiElement = markupToDomElement(markup);
- testGlEmojiElement(
- glEmojiElement,
- emojiFixtureMap[emojiKey].name,
- emojiFixtureMap[emojiKey].unicodeVersion,
- emojiFixtureMap[emojiKey].moji,
- );
- });
-
- it('question mark with image fallback when invalid emoji name given', () => {
- const name = 'invalid_emoji';
- const emojiKey = 'grey_question';
- const markup = glEmojiTag(name, {
- forceFallback: true,
- });
- const glEmojiElement = markupToDomElement(markup);
- testGlEmojiElement(
- glEmojiElement,
- emojiFixtureMap[emojiKey].name,
- emojiFixtureMap[emojiKey].unicodeVersion,
- emojiFixtureMap[emojiKey].moji,
- {
- forceFallback: true,
- },
- );
- });
- });
-
- describe('isFlagEmoji', () => {
- it('should gracefully handle empty string', () => {
- expect(isFlagEmoji('')).toBeFalsy();
- });
-
- it('should detect flag_ac', () => {
- expect(isFlagEmoji('🇦🇨')).toBeTruthy();
- });
-
- it('should detect flag_us', () => {
- expect(isFlagEmoji('🇺🇸')).toBeTruthy();
- });
-
- it('should detect flag_zw', () => {
- expect(isFlagEmoji('🇿🇼')).toBeTruthy();
- });
-
- it('should not detect flags', () => {
- expect(isFlagEmoji('ðŸŽ')).toBeFalsy();
- });
-
- it('should not detect triangular_flag_on_post', () => {
- expect(isFlagEmoji('🚩')).toBeFalsy();
- });
-
- it('should not detect single letter', () => {
- expect(isFlagEmoji('🇦')).toBeFalsy();
- });
-
- it('should not detect >2 letters', () => {
- expect(isFlagEmoji('🇦🇧🇨')).toBeFalsy();
- });
- });
-
- describe('isRainbowFlagEmoji', () => {
- it('should gracefully handle empty string', () => {
- expect(isRainbowFlagEmoji('')).toBeFalsy();
- });
-
- it('should detect rainbow_flag', () => {
- expect(isRainbowFlagEmoji('ðŸ³ðŸŒˆ')).toBeTruthy();
- });
-
- it("should not detect flag_white on its' own", () => {
- expect(isRainbowFlagEmoji('ðŸ³')).toBeFalsy();
- });
-
- it("should not detect rainbow on its' own", () => {
- expect(isRainbowFlagEmoji('🌈')).toBeFalsy();
- });
-
- it('should not detect flag_white with something else', () => {
- expect(isRainbowFlagEmoji('ðŸ³ðŸ”µ')).toBeFalsy();
- });
- });
-
- describe('isKeycapEmoji', () => {
- it('should gracefully handle empty string', () => {
- expect(isKeycapEmoji('')).toBeFalsy();
- });
-
- it('should detect one(keycap)', () => {
- expect(isKeycapEmoji('1ï¸âƒ£')).toBeTruthy();
- });
-
- it('should detect nine(keycap)', () => {
- expect(isKeycapEmoji('9ï¸âƒ£')).toBeTruthy();
- });
-
- it('should not detect ten(keycap)', () => {
- expect(isKeycapEmoji('🔟')).toBeFalsy();
- });
-
- it('should not detect hash(keycap)', () => {
- expect(isKeycapEmoji('#⃣')).toBeFalsy();
- });
- });
-
- describe('isSkinToneComboEmoji', () => {
- it('should gracefully handle empty string', () => {
- expect(isSkinToneComboEmoji('')).toBeFalsy();
- });
-
- it('should detect hand_splayed_tone5', () => {
- expect(isSkinToneComboEmoji('ðŸ–ðŸ¿')).toBeTruthy();
- });
-
- it('should not detect hand_splayed', () => {
- expect(isSkinToneComboEmoji('ðŸ–')).toBeFalsy();
- });
-
- it('should detect lifter_tone1', () => {
- expect(isSkinToneComboEmoji('ðŸ‹ðŸ»')).toBeTruthy();
- });
-
- it('should not detect lifter', () => {
- expect(isSkinToneComboEmoji('ðŸ‹')).toBeFalsy();
- });
-
- it('should detect rowboat_tone4', () => {
- expect(isSkinToneComboEmoji('🚣ðŸ¾')).toBeTruthy();
- });
-
- it('should not detect rowboat', () => {
- expect(isSkinToneComboEmoji('🚣')).toBeFalsy();
- });
-
- it('should not detect individual tone emoji', () => {
- expect(isSkinToneComboEmoji('ðŸ»')).toBeFalsy();
- });
- });
-
- describe('isHorceRacingSkinToneComboEmoji', () => {
- it('should gracefully handle empty string', () => {
- expect(isHorceRacingSkinToneComboEmoji('')).toBeFalsy();
- });
-
- it('should detect horse_racing_tone2', () => {
- expect(isHorceRacingSkinToneComboEmoji('ðŸ‡ðŸ¼')).toBeTruthy();
- });
-
- it('should not detect horse_racing', () => {
- expect(isHorceRacingSkinToneComboEmoji('ðŸ‡')).toBeFalsy();
- });
- });
-
- describe('isPersonZwjEmoji', () => {
- it('should gracefully handle empty string', () => {
- expect(isPersonZwjEmoji('')).toBeFalsy();
- });
-
- it('should detect couple_mm', () => {
- expect(isPersonZwjEmoji('👨â€â¤ï¸â€ðŸ‘¨')).toBeTruthy();
- });
-
- it('should not detect couple_with_heart', () => {
- expect(isPersonZwjEmoji('💑')).toBeFalsy();
- });
-
- it('should not detect couplekiss', () => {
- expect(isPersonZwjEmoji('ðŸ’')).toBeFalsy();
- });
-
- it('should detect family_mmb', () => {
- expect(isPersonZwjEmoji('👨â€ðŸ‘¨â€ðŸ‘¦')).toBeTruthy();
- });
-
- it('should detect family_mwgb', () => {
- expect(isPersonZwjEmoji('👨â€ðŸ‘©â€ðŸ‘§â€ðŸ‘¦')).toBeTruthy();
- });
-
- it('should not detect family', () => {
- expect(isPersonZwjEmoji('👪')).toBeFalsy();
- });
-
- it('should detect kiss_ww', () => {
- expect(isPersonZwjEmoji('👩â€â¤ï¸â€ðŸ’‹â€ðŸ‘©')).toBeTruthy();
- });
-
- it('should not detect girl', () => {
- expect(isPersonZwjEmoji('👧')).toBeFalsy();
- });
-
- it('should not detect girl_tone5', () => {
- expect(isPersonZwjEmoji('👧ðŸ¿')).toBeFalsy();
- });
-
- it('should not detect man', () => {
- expect(isPersonZwjEmoji('👨')).toBeFalsy();
- });
-
- it('should not detect woman', () => {
- expect(isPersonZwjEmoji('👩')).toBeFalsy();
- });
- });
-
- describe('isEmojiUnicodeSupported', () => {
- it('should gracefully handle empty string with unicode support', () => {
- const isSupported = isEmojiUnicodeSupported({ '1.0': true }, '', '1.0');
-
- expect(isSupported).toBeTruthy();
- });
-
- it('should gracefully handle empty string without unicode support', () => {
- const isSupported = isEmojiUnicodeSupported({}, '', '1.0');
-
- expect(isSupported).toBeFalsy();
- });
-
- it('bomb(6.0) with 6.0 support', () => {
- const emojiKey = 'bomb';
- const unicodeSupportMap = Object.assign({}, emptySupportMap, {
- '6.0': true,
- });
- const isSupported = isEmojiUnicodeSupported(
- unicodeSupportMap,
- emojiFixtureMap[emojiKey].moji,
- emojiFixtureMap[emojiKey].unicodeVersion,
- );
-
- expect(isSupported).toBeTruthy();
- });
-
- it('bomb(6.0) without 6.0 support', () => {
- const emojiKey = 'bomb';
- const unicodeSupportMap = emptySupportMap;
- const isSupported = isEmojiUnicodeSupported(
- unicodeSupportMap,
- emojiFixtureMap[emojiKey].moji,
- emojiFixtureMap[emojiKey].unicodeVersion,
- );
-
- expect(isSupported).toBeFalsy();
- });
-
- it('bomb(6.0) without 6.0 but with 9.0 support', () => {
- const emojiKey = 'bomb';
- const unicodeSupportMap = Object.assign({}, emptySupportMap, {
- '9.0': true,
- });
- const isSupported = isEmojiUnicodeSupported(
- unicodeSupportMap,
- emojiFixtureMap[emojiKey].moji,
- emojiFixtureMap[emojiKey].unicodeVersion,
- );
-
- expect(isSupported).toBeFalsy();
- });
-
- it('construction_worker_tone5(8.0) without skin tone modifier support', () => {
- const emojiKey = 'construction_worker_tone5';
- const unicodeSupportMap = Object.assign({}, emptySupportMap, {
- skinToneModifier: false,
- '9.0': true,
- '8.0': true,
- '7.0': true,
- 6.1: true,
- '6.0': true,
- 5.2: true,
- 5.1: true,
- 4.1: true,
- '4.0': true,
- 3.2: true,
- '3.0': true,
- 1.1: true,
- });
- const isSupported = isEmojiUnicodeSupported(
- unicodeSupportMap,
- emojiFixtureMap[emojiKey].moji,
- emojiFixtureMap[emojiKey].unicodeVersion,
- );
-
- expect(isSupported).toBeFalsy();
- });
-
- it('use native keycap on >=57 chrome', () => {
- const emojiKey = 'five';
- const unicodeSupportMap = Object.assign({}, emptySupportMap, {
- '3.0': true,
- meta: {
- isChrome: true,
- chromeVersion: 57,
- },
- });
- const isSupported = isEmojiUnicodeSupported(
- unicodeSupportMap,
- emojiFixtureMap[emojiKey].moji,
- emojiFixtureMap[emojiKey].unicodeVersion,
- );
-
- expect(isSupported).toBeTruthy();
- });
-
- it('fallback keycap on <57 chrome', () => {
- const emojiKey = 'five';
- const unicodeSupportMap = Object.assign({}, emptySupportMap, {
- '3.0': true,
- meta: {
- isChrome: true,
- chromeVersion: 50,
- },
- });
- const isSupported = isEmojiUnicodeSupported(
- unicodeSupportMap,
- emojiFixtureMap[emojiKey].moji,
- emojiFixtureMap[emojiKey].unicodeVersion,
- );
-
- expect(isSupported).toBeFalsy();
- });
- });
-});
diff --git a/spec/javascripts/feature_highlight/feature_highlight_helper_spec.js b/spec/javascripts/feature_highlight/feature_highlight_helper_spec.js
deleted file mode 100644
index ba35f7bf7c6..00000000000
--- a/spec/javascripts/feature_highlight/feature_highlight_helper_spec.js
+++ /dev/null
@@ -1,75 +0,0 @@
-import $ from 'jquery';
-import MockAdapter from 'axios-mock-adapter';
-import getSetTimeoutPromise from 'spec/helpers/set_timeout_promise_helper';
-import axios from '~/lib/utils/axios_utils';
-import { getSelector, dismiss, inserted } from '~/feature_highlight/feature_highlight_helper';
-import { togglePopover } from '~/shared/popover';
-
-describe('feature highlight helper', () => {
- describe('getSelector', () => {
- it('returns js-feature-highlight selector', () => {
- const highlightId = 'highlightId';
-
- expect(getSelector(highlightId)).toEqual(
- `.js-feature-highlight[data-highlight=${highlightId}]`,
- );
- });
- });
-
- describe('dismiss', () => {
- let mock;
- const context = {
- hide: () => {},
- attr: () => '/-/callouts/dismiss',
- };
-
- beforeEach(() => {
- mock = new MockAdapter(axios);
-
- spyOn(togglePopover, 'call').and.callFake(() => {});
- spyOn(context, 'hide').and.callFake(() => {});
- dismiss.call(context);
- });
-
- afterEach(() => {
- mock.restore();
- });
-
- it('calls persistent dismissal endpoint', done => {
- const spy = jasmine.createSpy('dismiss-endpoint-hit');
- mock.onPost('/-/callouts/dismiss').reply(spy);
-
- getSetTimeoutPromise()
- .then(() => {
- expect(spy).toHaveBeenCalled();
- })
- .then(done)
- .catch(done.fail);
- });
-
- it('calls hide popover', () => {
- expect(togglePopover.call).toHaveBeenCalledWith(context, false);
- });
-
- it('calls hide', () => {
- expect(context.hide).toHaveBeenCalled();
- });
- });
-
- describe('inserted', () => {
- it('registers click event callback', done => {
- const context = {
- getAttribute: () => 'popoverId',
- dataset: {
- highlight: 'some-feature',
- },
- };
-
- spyOn($.fn, 'on').and.callFake(event => {
- expect(event).toEqual('click');
- done();
- });
- inserted.call(context);
- });
- });
-});
diff --git a/spec/javascripts/feature_highlight/feature_highlight_spec.js b/spec/javascripts/feature_highlight/feature_highlight_spec.js
deleted file mode 100644
index 40ac4bbb6a0..00000000000
--- a/spec/javascripts/feature_highlight/feature_highlight_spec.js
+++ /dev/null
@@ -1,141 +0,0 @@
-import $ from 'jquery';
-import MockAdapter from 'axios-mock-adapter';
-import * as featureHighlight from '~/feature_highlight/feature_highlight';
-import * as popover from '~/shared/popover';
-import axios from '~/lib/utils/axios_utils';
-
-describe('feature highlight', () => {
- beforeEach(() => {
- setFixtures(`
- <div>
- <div class="js-feature-highlight" data-highlight="test" data-highlight-priority="10" data-dismiss-endpoint="/test" disabled>
- Trigger
- </div>
- </div>
- <div class="feature-highlight-popover-content">
- Content
- <div class="dismiss-feature-highlight">
- Dismiss
- </div>
- </div>
- `);
- });
-
- describe('setupFeatureHighlightPopover', () => {
- let mock;
- const selector = '.js-feature-highlight[data-highlight=test]';
-
- beforeEach(() => {
- mock = new MockAdapter(axios);
- mock.onGet('/test').reply(200);
- spyOn(window, 'addEventListener');
- featureHighlight.setupFeatureHighlightPopover('test', 0);
- });
-
- afterEach(() => {
- mock.restore();
- });
-
- it('setup popover content', () => {
- const $popoverContent = $('.feature-highlight-popover-content');
- const outerHTML = $popoverContent.prop('outerHTML');
-
- expect($(selector).data('content')).toEqual(outerHTML);
- });
-
- it('setup mouseenter', () => {
- const toggleSpy = spyOn(popover.togglePopover, 'call');
- $(selector).trigger('mouseenter');
-
- expect(toggleSpy).toHaveBeenCalledWith(jasmine.any(Object), true);
- });
-
- it('setup debounced mouseleave', done => {
- const toggleSpy = spyOn(popover.togglePopover, 'call');
- $(selector).trigger('mouseleave');
-
- // Even though we've set the debounce to 0ms, setTimeout is needed for the debounce
- setTimeout(() => {
- expect(toggleSpy).toHaveBeenCalledWith(jasmine.any(Object), false);
- done();
- }, 0);
- });
-
- it('setup show.bs.popover', () => {
- $(selector).trigger('show.bs.popover');
-
- expect(window.addEventListener).toHaveBeenCalledWith('scroll', jasmine.any(Function), {
- once: true,
- });
- });
-
- it('removes disabled attribute', () => {
- expect($('.js-feature-highlight').is(':disabled')).toEqual(false);
- });
-
- it('displays popover', () => {
- expect(document.querySelector(selector).getAttribute('aria-describedby')).toBeFalsy();
- $(selector).trigger('mouseenter');
-
- expect(document.querySelector(selector).getAttribute('aria-describedby')).toBeTruthy();
- });
-
- it('toggles when clicked', () => {
- $(selector).trigger('mouseenter');
- const popoverId = $(selector).attr('aria-describedby');
- const toggleSpy = spyOn(popover.togglePopover, 'call');
-
- $(`#${popoverId} .dismiss-feature-highlight`).click();
-
- expect(toggleSpy).toHaveBeenCalled();
- });
- });
-
- describe('findHighestPriorityFeature', () => {
- beforeEach(() => {
- setFixtures(`
- <div class="js-feature-highlight" data-highlight="test" data-highlight-priority="10" disabled></div>
- <div class="js-feature-highlight" data-highlight="test-high-priority" data-highlight-priority="20" disabled></div>
- <div class="js-feature-highlight" data-highlight="test-low-priority" data-highlight-priority="0" disabled></div>
- `);
- });
-
- it('should pick the highest priority feature highlight', () => {
- setFixtures(`
- <div class="js-feature-highlight" data-highlight="test" data-highlight-priority="10" disabled></div>
- <div class="js-feature-highlight" data-highlight="test-high-priority" data-highlight-priority="20" disabled></div>
- <div class="js-feature-highlight" data-highlight="test-low-priority" data-highlight-priority="0" disabled></div>
- `);
-
- expect($('.js-feature-highlight').length).toBeGreaterThan(1);
- expect(featureHighlight.findHighestPriorityFeature()).toEqual('test-high-priority');
- });
-
- it('should work when no priority is set', () => {
- setFixtures(`
- <div class="js-feature-highlight" data-highlight="test" disabled></div>
- `);
-
- expect(featureHighlight.findHighestPriorityFeature()).toEqual('test');
- });
-
- it('should pick the highest priority feature highlight when some have no priority set', () => {
- setFixtures(`
- <div class="js-feature-highlight" data-highlight="test-no-priority1" disabled></div>
- <div class="js-feature-highlight" data-highlight="test" data-highlight-priority="10" disabled></div>
- <div class="js-feature-highlight" data-highlight="test-no-priority2" disabled></div>
- <div class="js-feature-highlight" data-highlight="test-high-priority" data-highlight-priority="20" disabled></div>
- <div class="js-feature-highlight" data-highlight="test-low-priority" data-highlight-priority="0" disabled></div>
- `);
-
- expect($('.js-feature-highlight').length).toBeGreaterThan(1);
- expect(featureHighlight.findHighestPriorityFeature()).toEqual('test-high-priority');
- });
- });
-
- describe('highlightFeatures', () => {
- it('calls setupFeatureHighlightPopover', () => {
- expect(featureHighlight.highlightFeatures()).toEqual('test');
- });
- });
-});
diff --git a/spec/javascripts/filtered_search/dropdown_utils_spec.js b/spec/javascripts/filtered_search/dropdown_utils_spec.js
deleted file mode 100644
index 6eda4f391a4..00000000000
--- a/spec/javascripts/filtered_search/dropdown_utils_spec.js
+++ /dev/null
@@ -1,374 +0,0 @@
-import DropdownUtils from '~/filtered_search/dropdown_utils';
-import FilteredSearchDropdownManager from '~/filtered_search/filtered_search_dropdown_manager';
-import IssuableFilteredSearchTokenKeys from '~/filtered_search/issuable_filtered_search_token_keys';
-import FilteredSearchSpecHelper from '../helpers/filtered_search_spec_helper';
-
-describe('Dropdown Utils', () => {
- const issueListFixture = 'issues/issue_list.html';
- preloadFixtures(issueListFixture);
-
- describe('getEscapedText', () => {
- it('should return same word when it has no space', () => {
- const escaped = DropdownUtils.getEscapedText('textWithoutSpace');
-
- expect(escaped).toBe('textWithoutSpace');
- });
-
- it('should escape with double quotes', () => {
- let escaped = DropdownUtils.getEscapedText('text with space');
-
- expect(escaped).toBe('"text with space"');
-
- escaped = DropdownUtils.getEscapedText("won't fix");
-
- expect(escaped).toBe('"won\'t fix"');
- });
-
- it('should escape with single quotes', () => {
- const escaped = DropdownUtils.getEscapedText('won"t fix');
-
- expect(escaped).toBe("'won\"t fix'");
- });
-
- it('should escape with single quotes by default', () => {
- const escaped = DropdownUtils.getEscapedText('won"t\' fix');
-
- expect(escaped).toBe("'won\"t' fix'");
- });
- });
-
- describe('filterWithSymbol', () => {
- let input;
- const item = {
- title: '@root',
- };
-
- beforeEach(() => {
- setFixtures(`
- <input type="text" id="test" />
- `);
-
- input = document.getElementById('test');
- });
-
- it('should filter without symbol', () => {
- input.value = 'roo';
-
- const updatedItem = DropdownUtils.filterWithSymbol('@', input, item);
-
- expect(updatedItem.droplab_hidden).toBe(false);
- });
-
- it('should filter with symbol', () => {
- input.value = '@roo';
-
- const updatedItem = DropdownUtils.filterWithSymbol('@', input, item);
-
- expect(updatedItem.droplab_hidden).toBe(false);
- });
-
- describe('filters multiple word title', () => {
- const multipleWordItem = {
- title: 'Community Contributions',
- };
-
- it('should filter with double quote', () => {
- input.value = '"';
-
- const updatedItem = DropdownUtils.filterWithSymbol('~', input, multipleWordItem);
-
- expect(updatedItem.droplab_hidden).toBe(false);
- });
-
- it('should filter with double quote and symbol', () => {
- input.value = '~"';
-
- const updatedItem = DropdownUtils.filterWithSymbol('~', input, multipleWordItem);
-
- expect(updatedItem.droplab_hidden).toBe(false);
- });
-
- it('should filter with double quote and multiple words', () => {
- input.value = '"community con';
-
- const updatedItem = DropdownUtils.filterWithSymbol('~', input, multipleWordItem);
-
- expect(updatedItem.droplab_hidden).toBe(false);
- });
-
- it('should filter with double quote, symbol and multiple words', () => {
- input.value = '~"community con';
-
- const updatedItem = DropdownUtils.filterWithSymbol('~', input, multipleWordItem);
-
- expect(updatedItem.droplab_hidden).toBe(false);
- });
-
- it('should filter with single quote', () => {
- input.value = "'";
-
- const updatedItem = DropdownUtils.filterWithSymbol('~', input, multipleWordItem);
-
- expect(updatedItem.droplab_hidden).toBe(false);
- });
-
- it('should filter with single quote and symbol', () => {
- input.value = "~'";
-
- const updatedItem = DropdownUtils.filterWithSymbol('~', input, multipleWordItem);
-
- expect(updatedItem.droplab_hidden).toBe(false);
- });
-
- it('should filter with single quote and multiple words', () => {
- input.value = "'community con";
-
- const updatedItem = DropdownUtils.filterWithSymbol('~', input, multipleWordItem);
-
- expect(updatedItem.droplab_hidden).toBe(false);
- });
-
- it('should filter with single quote, symbol and multiple words', () => {
- input.value = "~'community con";
-
- const updatedItem = DropdownUtils.filterWithSymbol('~', input, multipleWordItem);
-
- expect(updatedItem.droplab_hidden).toBe(false);
- });
- });
- });
-
- describe('filterHint', () => {
- let input;
- let allowedKeys;
-
- beforeEach(() => {
- setFixtures(`
- <ul class="tokens-container">
- <li class="input-token">
- <input class="filtered-search" type="text" id="test" />
- </li>
- </ul>
- `);
-
- input = document.getElementById('test');
- allowedKeys = IssuableFilteredSearchTokenKeys.getKeys();
- });
-
- function config() {
- return {
- input,
- allowedKeys,
- };
- }
-
- it('should filter', () => {
- input.value = 'l';
- let updatedItem = DropdownUtils.filterHint(config(), {
- hint: 'label',
- });
-
- expect(updatedItem.droplab_hidden).toBe(false);
-
- input.value = 'o';
- updatedItem = DropdownUtils.filterHint(config(), {
- hint: 'label',
- });
-
- expect(updatedItem.droplab_hidden).toBe(true);
- });
-
- it('should return droplab_hidden false when item has no hint', () => {
- const updatedItem = DropdownUtils.filterHint(config(), {}, '');
-
- expect(updatedItem.droplab_hidden).toBe(false);
- });
-
- it('should allow multiple if item.type is array', () => {
- input.value = 'label:~first la';
- const updatedItem = DropdownUtils.filterHint(config(), {
- hint: 'label',
- type: 'array',
- });
-
- expect(updatedItem.droplab_hidden).toBe(false);
- });
-
- it('should prevent multiple if item.type is not array', () => {
- input.value = 'milestone:~first mile';
- let updatedItem = DropdownUtils.filterHint(config(), {
- hint: 'milestone',
- });
-
- expect(updatedItem.droplab_hidden).toBe(true);
-
- updatedItem = DropdownUtils.filterHint(config(), {
- hint: 'milestone',
- type: 'string',
- });
-
- expect(updatedItem.droplab_hidden).toBe(true);
- });
- });
-
- describe('setDataValueIfSelected', () => {
- beforeEach(() => {
- spyOn(FilteredSearchDropdownManager, 'addWordToInput').and.callFake(() => {});
- });
-
- it('calls addWordToInput when dataValue exists', () => {
- const selected = {
- getAttribute: () => 'value',
- hasAttribute: () => false,
- };
-
- DropdownUtils.setDataValueIfSelected(null, '=', selected);
-
- expect(FilteredSearchDropdownManager.addWordToInput.calls.count()).toEqual(1);
- });
-
- it('returns true when dataValue exists', () => {
- const selected = {
- getAttribute: () => 'value',
- hasAttribute: () => false,
- };
-
- const result = DropdownUtils.setDataValueIfSelected(null, '=', selected);
- const result2 = DropdownUtils.setDataValueIfSelected(null, '!=', selected);
-
- expect(result).toBe(true);
- expect(result2).toBe(true);
- });
-
- it('returns false when dataValue does not exist', () => {
- const selected = {
- getAttribute: () => null,
- };
-
- const result = DropdownUtils.setDataValueIfSelected(null, '=', selected);
- const result2 = DropdownUtils.setDataValueIfSelected(null, '!=', selected);
-
- expect(result).toBe(false);
- expect(result2).toBe(false);
- });
- });
-
- describe('getInputSelectionPosition', () => {
- describe('word with trailing spaces', () => {
- const value = 'label:none ';
-
- it('should return selectionStart when cursor is at the trailing space', () => {
- const { left, right } = DropdownUtils.getInputSelectionPosition({
- selectionStart: 11,
- value,
- });
-
- expect(left).toBe(11);
- expect(right).toBe(11);
- });
-
- it('should return input when cursor is at the start of input', () => {
- const { left, right } = DropdownUtils.getInputSelectionPosition({
- selectionStart: 0,
- value,
- });
-
- expect(left).toBe(0);
- expect(right).toBe(10);
- });
-
- it('should return input when cursor is at the middle of input', () => {
- const { left, right } = DropdownUtils.getInputSelectionPosition({
- selectionStart: 7,
- value,
- });
-
- expect(left).toBe(0);
- expect(right).toBe(10);
- });
-
- it('should return input when cursor is at the end of input', () => {
- const { left, right } = DropdownUtils.getInputSelectionPosition({
- selectionStart: 10,
- value,
- });
-
- expect(left).toBe(0);
- expect(right).toBe(10);
- });
- });
-
- describe('multiple words', () => {
- const value = 'label:~"Community Contribution"';
-
- it('should return input when cursor is after the first word', () => {
- const { left, right } = DropdownUtils.getInputSelectionPosition({
- selectionStart: 17,
- value,
- });
-
- expect(left).toBe(0);
- expect(right).toBe(31);
- });
-
- it('should return input when cursor is before the second word', () => {
- const { left, right } = DropdownUtils.getInputSelectionPosition({
- selectionStart: 18,
- value,
- });
-
- expect(left).toBe(0);
- expect(right).toBe(31);
- });
- });
-
- describe('incomplete multiple words', () => {
- const value = 'label:~"Community Contribution';
-
- it('should return entire input when cursor is at the start of input', () => {
- const { left, right } = DropdownUtils.getInputSelectionPosition({
- selectionStart: 0,
- value,
- });
-
- expect(left).toBe(0);
- expect(right).toBe(30);
- });
-
- it('should return entire input when cursor is at the end of input', () => {
- const { left, right } = DropdownUtils.getInputSelectionPosition({
- selectionStart: 30,
- value,
- });
-
- expect(left).toBe(0);
- expect(right).toBe(30);
- });
- });
- });
-
- describe('getSearchQuery', () => {
- let authorToken;
-
- beforeEach(() => {
- loadFixtures(issueListFixture);
-
- authorToken = FilteredSearchSpecHelper.createFilterVisualToken('author', '=', '@user');
- const searchTermToken = FilteredSearchSpecHelper.createSearchVisualToken('search term');
-
- const tokensContainer = document.querySelector('.tokens-container');
- tokensContainer.appendChild(searchTermToken);
- tokensContainer.appendChild(authorToken);
- });
-
- it('uses original value if present', () => {
- const originalValue = 'original dance';
- const valueContainer = authorToken.querySelector('.value-container');
- valueContainer.dataset.originalValue = originalValue;
-
- const searchQuery = DropdownUtils.getSearchQuery();
-
- expect(searchQuery).toBe(' search term author:=original dance');
- });
- });
-});
diff --git a/spec/javascripts/filtered_search/filtered_search_manager_spec.js b/spec/javascripts/filtered_search/filtered_search_manager_spec.js
deleted file mode 100644
index d0b54a16747..00000000000
--- a/spec/javascripts/filtered_search/filtered_search_manager_spec.js
+++ /dev/null
@@ -1,580 +0,0 @@
-import RecentSearchesService from '~/filtered_search/services/recent_searches_service';
-import RecentSearchesServiceError from '~/filtered_search/services/recent_searches_service_error';
-import RecentSearchesRoot from '~/filtered_search/recent_searches_root';
-import IssuableFilteredSearchTokenKeys from '~/filtered_search/issuable_filtered_search_token_keys';
-import '~/lib/utils/common_utils';
-import DropdownUtils from '~/filtered_search/dropdown_utils';
-import FilteredSearchVisualTokens from '~/filtered_search/filtered_search_visual_tokens';
-import FilteredSearchDropdownManager from '~/filtered_search/filtered_search_dropdown_manager';
-import FilteredSearchManager from '~/filtered_search/filtered_search_manager';
-import FilteredSearchSpecHelper from '../helpers/filtered_search_spec_helper';
-import { BACKSPACE_KEY_CODE, DELETE_KEY_CODE } from '~/lib/utils/keycodes';
-
-describe('Filtered Search Manager', function() {
- let input;
- let manager;
- let tokensContainer;
- const page = 'issues';
- const placeholder = 'Search or filter results...';
-
- function dispatchBackspaceEvent(element, eventType) {
- const event = new Event(eventType);
- event.keyCode = BACKSPACE_KEY_CODE;
- element.dispatchEvent(event);
- }
-
- function dispatchDeleteEvent(element, eventType) {
- const event = new Event(eventType);
- event.keyCode = DELETE_KEY_CODE;
- element.dispatchEvent(event);
- }
-
- function dispatchAltBackspaceEvent(element, eventType) {
- const event = new Event(eventType);
- event.altKey = true;
- event.keyCode = BACKSPACE_KEY_CODE;
- element.dispatchEvent(event);
- }
-
- function dispatchCtrlBackspaceEvent(element, eventType) {
- const event = new Event(eventType);
- event.ctrlKey = true;
- event.keyCode = BACKSPACE_KEY_CODE;
- element.dispatchEvent(event);
- }
-
- function dispatchMetaBackspaceEvent(element, eventType) {
- const event = new Event(eventType);
- event.metaKey = true;
- event.keyCode = BACKSPACE_KEY_CODE;
- element.dispatchEvent(event);
- }
-
- function getVisualTokens() {
- return tokensContainer.querySelectorAll('.js-visual-token');
- }
-
- beforeEach(() => {
- setFixtures(`
- <div class="filtered-search-box">
- <form>
- <ul class="tokens-container list-unstyled">
- ${FilteredSearchSpecHelper.createInputHTML(placeholder)}
- </ul>
- <button class="clear-search" type="button">
- <i class="fa fa-times"></i>
- </button>
- </form>
- </div>
- `);
-
- spyOn(FilteredSearchDropdownManager.prototype, 'setDropdown').and.callFake(() => {});
- });
-
- const initializeManager = () => {
- /* eslint-disable jasmine/no-unsafe-spy */
- spyOn(FilteredSearchManager.prototype, 'loadSearchParamsFromURL').and.callFake(() => {});
- spyOn(FilteredSearchManager.prototype, 'tokenChange').and.callFake(() => {});
- spyOn(FilteredSearchDropdownManager.prototype, 'updateDropdownOffset').and.callFake(() => {});
- spyOn(gl.utils, 'getParameterByName').and.returnValue(null);
- spyOn(FilteredSearchVisualTokens, 'unselectTokens').and.callThrough();
- /* eslint-enable jasmine/no-unsafe-spy */
-
- input = document.querySelector('.filtered-search');
- tokensContainer = document.querySelector('.tokens-container');
- manager = new FilteredSearchManager({ page });
- manager.setup();
- };
-
- afterEach(() => {
- manager.cleanup();
- });
-
- describe('class constructor', () => {
- const isLocalStorageAvailable = 'isLocalStorageAvailable';
- let RecentSearchesStoreSpy;
-
- beforeEach(() => {
- spyOn(RecentSearchesService, 'isAvailable').and.returnValue(isLocalStorageAvailable);
- spyOn(RecentSearchesRoot.prototype, 'render');
- RecentSearchesStoreSpy = spyOnDependency(FilteredSearchManager, 'RecentSearchesStore');
- });
-
- it('should instantiate RecentSearchesStore with isLocalStorageAvailable', () => {
- manager = new FilteredSearchManager({ page });
-
- expect(RecentSearchesService.isAvailable).toHaveBeenCalled();
- expect(RecentSearchesStoreSpy).toHaveBeenCalledWith({
- isLocalStorageAvailable,
- allowedKeys: IssuableFilteredSearchTokenKeys.getKeys(),
- });
- });
- });
-
- describe('setup', () => {
- beforeEach(() => {
- manager = new FilteredSearchManager({ page });
- });
-
- it('should not instantiate Flash if an RecentSearchesServiceError is caught', () => {
- spyOn(RecentSearchesService.prototype, 'fetch').and.callFake(() =>
- Promise.reject(new RecentSearchesServiceError()),
- );
- spyOn(window, 'Flash');
-
- manager.setup();
-
- expect(window.Flash).not.toHaveBeenCalled();
- });
- });
-
- describe('searchState', () => {
- beforeEach(() => {
- spyOn(FilteredSearchManager.prototype, 'search').and.callFake(() => {});
- initializeManager();
- });
-
- it('should blur button', () => {
- const e = {
- preventDefault: () => {},
- currentTarget: {
- blur: () => {},
- },
- };
- spyOn(e.currentTarget, 'blur').and.callThrough();
- manager.searchState(e);
-
- expect(e.currentTarget.blur).toHaveBeenCalled();
- });
-
- it('should not call search if there is no state', () => {
- const e = {
- preventDefault: () => {},
- currentTarget: {
- blur: () => {},
- },
- };
-
- manager.searchState(e);
-
- expect(FilteredSearchManager.prototype.search).not.toHaveBeenCalled();
- });
-
- it('should call search when there is state', () => {
- const e = {
- preventDefault: () => {},
- currentTarget: {
- blur: () => {},
- dataset: {
- state: 'opened',
- },
- },
- };
-
- manager.searchState(e);
-
- expect(FilteredSearchManager.prototype.search).toHaveBeenCalledWith('opened');
- });
- });
-
- describe('search', () => {
- const defaultParams = '?scope=all&utf8=%E2%9C%93&state=opened';
-
- beforeEach(() => {
- initializeManager();
- });
-
- it('should search with a single word', done => {
- input.value = 'searchTerm';
-
- spyOnDependency(FilteredSearchManager, 'visitUrl').and.callFake(url => {
- expect(url).toEqual(`${defaultParams}&search=searchTerm`);
- done();
- });
-
- manager.search();
- });
-
- it('should search with multiple words', done => {
- input.value = 'awesome search terms';
-
- spyOnDependency(FilteredSearchManager, 'visitUrl').and.callFake(url => {
- expect(url).toEqual(`${defaultParams}&search=awesome+search+terms`);
- done();
- });
-
- manager.search();
- });
-
- it('should search with special characters', done => {
- input.value = '~!@#$%^&*()_+{}:<>,.?/';
-
- spyOnDependency(FilteredSearchManager, 'visitUrl').and.callFake(url => {
- expect(url).toEqual(
- `${defaultParams}&search=~!%40%23%24%25%5E%26*()_%2B%7B%7D%3A%3C%3E%2C.%3F%2F`,
- );
- done();
- });
-
- manager.search();
- });
-
- it('removes duplicated tokens', done => {
- tokensContainer.innerHTML = FilteredSearchSpecHelper.createTokensContainerHTML(`
- ${FilteredSearchSpecHelper.createFilterVisualTokenHTML('label', '=', '~bug')}
- ${FilteredSearchSpecHelper.createFilterVisualTokenHTML('label', '=', '~bug')}
- `);
-
- spyOnDependency(FilteredSearchManager, 'visitUrl').and.callFake(url => {
- expect(url).toEqual(`${defaultParams}&label_name[]=bug`);
- done();
- });
-
- manager.search();
- });
- });
-
- describe('handleInputPlaceholder', () => {
- beforeEach(() => {
- initializeManager();
- });
-
- it('should render placeholder when there is no input', () => {
- expect(input.placeholder).toEqual(placeholder);
- });
-
- it('should not render placeholder when there is input', () => {
- input.value = 'test words';
-
- const event = new Event('input');
- input.dispatchEvent(event);
-
- expect(input.placeholder).toEqual('');
- });
-
- it('should not render placeholder when there are tokens and no input', () => {
- tokensContainer.innerHTML = FilteredSearchSpecHelper.createTokensContainerHTML(
- FilteredSearchSpecHelper.createFilterVisualTokenHTML('label', '=', '~bug'),
- );
-
- const event = new Event('input');
- input.dispatchEvent(event);
-
- expect(input.placeholder).toEqual('');
- });
- });
-
- describe('checkForBackspace', () => {
- beforeEach(() => {
- initializeManager();
- });
-
- describe('tokens and no input', () => {
- beforeEach(() => {
- tokensContainer.innerHTML = FilteredSearchSpecHelper.createTokensContainerHTML(
- FilteredSearchSpecHelper.createFilterVisualTokenHTML('label', '=', '~bug'),
- );
- });
-
- it('removes last token', () => {
- spyOn(FilteredSearchVisualTokens, 'removeLastTokenPartial').and.callThrough();
- dispatchBackspaceEvent(input, 'keyup');
- dispatchBackspaceEvent(input, 'keyup');
-
- expect(FilteredSearchVisualTokens.removeLastTokenPartial).toHaveBeenCalled();
- });
-
- it('sets the input', () => {
- spyOn(FilteredSearchVisualTokens, 'getLastTokenPartial').and.callThrough();
- dispatchDeleteEvent(input, 'keyup');
- dispatchDeleteEvent(input, 'keyup');
-
- expect(FilteredSearchVisualTokens.getLastTokenPartial).toHaveBeenCalled();
- expect(input.value).toEqual('~bug');
- });
- });
-
- it('does not remove token or change input when there is existing input', () => {
- spyOn(FilteredSearchVisualTokens, 'removeLastTokenPartial').and.callThrough();
- spyOn(FilteredSearchVisualTokens, 'getLastTokenPartial').and.callThrough();
-
- input.value = 'text';
- dispatchDeleteEvent(input, 'keyup');
-
- expect(FilteredSearchVisualTokens.removeLastTokenPartial).not.toHaveBeenCalled();
- expect(FilteredSearchVisualTokens.getLastTokenPartial).not.toHaveBeenCalled();
- expect(input.value).toEqual('text');
- });
-
- it('does not remove previous token on single backspace press', () => {
- spyOn(FilteredSearchVisualTokens, 'removeLastTokenPartial').and.callThrough();
- spyOn(FilteredSearchVisualTokens, 'getLastTokenPartial').and.callThrough();
-
- input.value = 't';
- dispatchDeleteEvent(input, 'keyup');
-
- expect(FilteredSearchVisualTokens.removeLastTokenPartial).not.toHaveBeenCalled();
- expect(FilteredSearchVisualTokens.getLastTokenPartial).not.toHaveBeenCalled();
- expect(input.value).toEqual('t');
- });
- });
-
- describe('checkForAltOrCtrlBackspace', () => {
- beforeEach(() => {
- initializeManager();
- spyOn(FilteredSearchVisualTokens, 'removeLastTokenPartial').and.callThrough();
- });
-
- describe('tokens and no input', () => {
- beforeEach(() => {
- tokensContainer.innerHTML = FilteredSearchSpecHelper.createTokensContainerHTML(
- FilteredSearchSpecHelper.createFilterVisualTokenHTML('label', '=', '~bug'),
- );
- });
-
- it('removes last token via alt-backspace', () => {
- dispatchAltBackspaceEvent(input, 'keydown');
-
- expect(FilteredSearchVisualTokens.removeLastTokenPartial).toHaveBeenCalled();
- });
-
- it('removes last token via ctrl-backspace', () => {
- dispatchCtrlBackspaceEvent(input, 'keydown');
-
- expect(FilteredSearchVisualTokens.removeLastTokenPartial).toHaveBeenCalled();
- });
- });
-
- describe('tokens and input', () => {
- beforeEach(() => {
- tokensContainer.innerHTML = FilteredSearchSpecHelper.createTokensContainerHTML(
- FilteredSearchSpecHelper.createFilterVisualTokenHTML('label', '=', '~bug'),
- );
- });
-
- it('does not remove token or change input via alt-backspace when there is existing input', () => {
- input = manager.filteredSearchInput;
- input.value = 'text';
- dispatchAltBackspaceEvent(input, 'keydown');
-
- expect(FilteredSearchVisualTokens.removeLastTokenPartial).not.toHaveBeenCalled();
- expect(input.value).toEqual('text');
- });
-
- it('does not remove token or change input via ctrl-backspace when there is existing input', () => {
- input = manager.filteredSearchInput;
- input.value = 'text';
- dispatchCtrlBackspaceEvent(input, 'keydown');
-
- expect(FilteredSearchVisualTokens.removeLastTokenPartial).not.toHaveBeenCalled();
- expect(input.value).toEqual('text');
- });
- });
- });
-
- describe('checkForMetaBackspace', () => {
- beforeEach(() => {
- initializeManager();
- });
-
- beforeEach(() => {
- tokensContainer.innerHTML = FilteredSearchSpecHelper.createTokensContainerHTML(
- FilteredSearchSpecHelper.createFilterVisualTokenHTML('label', '=', '~bug'),
- );
- });
-
- it('removes all tokens and input', () => {
- spyOn(FilteredSearchManager.prototype, 'clearSearch').and.callThrough();
- dispatchMetaBackspaceEvent(input, 'keydown');
-
- expect(manager.clearSearch).toHaveBeenCalled();
- expect(manager.filteredSearchInput.value).toEqual('');
- expect(DropdownUtils.getSearchQuery()).toEqual('');
- });
- });
-
- describe('removeToken', () => {
- beforeEach(() => {
- initializeManager();
- });
-
- it('removes token even when it is already selected', () => {
- tokensContainer.innerHTML = FilteredSearchSpecHelper.createTokensContainerHTML(
- FilteredSearchSpecHelper.createFilterVisualTokenHTML('milestone', '=', 'none', true),
- );
-
- tokensContainer.querySelector('.js-visual-token .remove-token').click();
-
- expect(tokensContainer.querySelector('.js-visual-token')).toEqual(null);
- });
-
- describe('unselected token', () => {
- beforeEach(() => {
- spyOn(FilteredSearchManager.prototype, 'removeSelectedToken').and.callThrough();
-
- tokensContainer.innerHTML = FilteredSearchSpecHelper.createTokensContainerHTML(
- FilteredSearchSpecHelper.createFilterVisualTokenHTML('milestone', '=', 'none'),
- );
- tokensContainer.querySelector('.js-visual-token .remove-token').click();
- });
-
- it('removes token when remove button is selected', () => {
- expect(tokensContainer.querySelector('.js-visual-token')).toEqual(null);
- });
-
- it('calls removeSelectedToken', () => {
- expect(manager.removeSelectedToken).toHaveBeenCalled();
- });
- });
- });
-
- describe('removeSelectedTokenKeydown', () => {
- beforeEach(() => {
- initializeManager();
- tokensContainer.innerHTML = FilteredSearchSpecHelper.createTokensContainerHTML(
- FilteredSearchSpecHelper.createFilterVisualTokenHTML('milestone', '=', 'none', true),
- );
- });
-
- it('removes selected token when the backspace key is pressed', () => {
- expect(getVisualTokens().length).toEqual(1);
-
- dispatchBackspaceEvent(document, 'keydown');
-
- expect(getVisualTokens().length).toEqual(0);
- });
-
- it('removes selected token when the delete key is pressed', () => {
- expect(getVisualTokens().length).toEqual(1);
-
- dispatchDeleteEvent(document, 'keydown');
-
- expect(getVisualTokens().length).toEqual(0);
- });
-
- it('updates the input placeholder after removal', () => {
- manager.handleInputPlaceholder();
-
- expect(input.placeholder).toEqual('');
- expect(getVisualTokens().length).toEqual(1);
-
- dispatchBackspaceEvent(document, 'keydown');
-
- expect(input.placeholder).not.toEqual('');
- expect(getVisualTokens().length).toEqual(0);
- });
-
- it('updates the clear button after removal', () => {
- manager.toggleClearSearchButton();
-
- const clearButton = document.querySelector('.clear-search');
-
- expect(clearButton.classList.contains('hidden')).toEqual(false);
- expect(getVisualTokens().length).toEqual(1);
-
- dispatchBackspaceEvent(document, 'keydown');
-
- expect(clearButton.classList.contains('hidden')).toEqual(true);
- expect(getVisualTokens().length).toEqual(0);
- });
- });
-
- describe('removeSelectedToken', () => {
- beforeEach(() => {
- spyOn(FilteredSearchVisualTokens, 'removeSelectedToken').and.callThrough();
- spyOn(FilteredSearchManager.prototype, 'handleInputPlaceholder').and.callThrough();
- spyOn(FilteredSearchManager.prototype, 'toggleClearSearchButton').and.callThrough();
- initializeManager();
- });
-
- it('calls FilteredSearchVisualTokens.removeSelectedToken', () => {
- manager.removeSelectedToken();
-
- expect(FilteredSearchVisualTokens.removeSelectedToken).toHaveBeenCalled();
- });
-
- it('calls handleInputPlaceholder', () => {
- manager.removeSelectedToken();
-
- expect(manager.handleInputPlaceholder).toHaveBeenCalled();
- });
-
- it('calls toggleClearSearchButton', () => {
- manager.removeSelectedToken();
-
- expect(manager.toggleClearSearchButton).toHaveBeenCalled();
- });
-
- it('calls update dropdown offset', () => {
- manager.removeSelectedToken();
-
- expect(manager.dropdownManager.updateDropdownOffset).toHaveBeenCalled();
- });
- });
-
- describe('Clearing search', () => {
- beforeEach(() => {
- initializeManager();
- });
-
- it('Clicking the "x" clear button, clears the input', () => {
- const inputValue = 'label:=~bug';
- manager.filteredSearchInput.value = inputValue;
- manager.filteredSearchInput.dispatchEvent(new Event('input'));
-
- expect(DropdownUtils.getSearchQuery()).toEqual(inputValue);
-
- manager.clearSearchButton.click();
-
- expect(manager.filteredSearchInput.value).toEqual('');
- expect(DropdownUtils.getSearchQuery()).toEqual('');
- });
- });
-
- describe('toggleInputContainerFocus', () => {
- beforeEach(() => {
- initializeManager();
- });
-
- it('toggles on focus', () => {
- input.focus();
-
- expect(document.querySelector('.filtered-search-box').classList.contains('focus')).toEqual(
- true,
- );
- });
-
- it('toggles on blur', () => {
- input.blur();
-
- expect(document.querySelector('.filtered-search-box').classList.contains('focus')).toEqual(
- false,
- );
- });
- });
-
- describe('getAllParams', () => {
- beforeEach(() => {
- this.paramsArr = ['key=value', 'otherkey=othervalue'];
-
- initializeManager();
- });
-
- it('correctly modifies params when custom modifier is passed', () => {
- const modifedParams = manager.getAllParams.call(
- {
- modifyUrlParams: paramsArr => paramsArr.reverse(),
- },
- [].concat(this.paramsArr),
- );
-
- expect(modifedParams[0]).toBe(this.paramsArr[1]);
- });
-
- it('does not modify params when no custom modifier is passed', () => {
- const modifedParams = manager.getAllParams.call({}, this.paramsArr);
-
- expect(modifedParams[1]).toBe(this.paramsArr[1]);
- });
- });
-});
diff --git a/spec/javascripts/filtered_search/recent_searches_root_spec.js b/spec/javascripts/filtered_search/recent_searches_root_spec.js
deleted file mode 100644
index 70dd4e9570d..00000000000
--- a/spec/javascripts/filtered_search/recent_searches_root_spec.js
+++ /dev/null
@@ -1,30 +0,0 @@
-import RecentSearchesRoot from '~/filtered_search/recent_searches_root';
-
-describe('RecentSearchesRoot', () => {
- describe('render', () => {
- let recentSearchesRoot;
- let data;
- let template;
- let VueSpy;
-
- beforeEach(() => {
- recentSearchesRoot = {
- store: {
- state: 'state',
- },
- };
-
- VueSpy = spyOnDependency(RecentSearchesRoot, 'Vue').and.callFake(options => {
- ({ data, template } = options);
- });
-
- RecentSearchesRoot.prototype.render.call(recentSearchesRoot);
- });
-
- it('should instantiate Vue', () => {
- expect(VueSpy).toHaveBeenCalled();
- expect(data()).toBe(recentSearchesRoot.store.state);
- expect(template).toContain(':is-local-storage-available="isLocalStorageAvailable"');
- });
- });
-});
diff --git a/spec/javascripts/filtered_search/services/recent_searches_service_spec.js b/spec/javascripts/filtered_search/services/recent_searches_service_spec.js
deleted file mode 100644
index 188f83eca16..00000000000
--- a/spec/javascripts/filtered_search/services/recent_searches_service_spec.js
+++ /dev/null
@@ -1,158 +0,0 @@
-import RecentSearchesService from '~/filtered_search/services/recent_searches_service';
-import RecentSearchesServiceError from '~/filtered_search/services/recent_searches_service_error';
-import AccessorUtilities from '~/lib/utils/accessor';
-
-describe('RecentSearchesService', () => {
- let service;
-
- beforeEach(() => {
- service = new RecentSearchesService();
- window.localStorage.removeItem(service.localStorageKey);
- });
-
- describe('fetch', () => {
- beforeEach(() => {
- spyOn(RecentSearchesService, 'isAvailable').and.returnValue(true);
- });
-
- it('should default to empty array', done => {
- const fetchItemsPromise = service.fetch();
-
- fetchItemsPromise
- .then(items => {
- expect(items).toEqual([]);
- })
- .then(done)
- .catch(done.fail);
- });
-
- it('should reject when unable to parse', done => {
- window.localStorage.setItem(service.localStorageKey, 'fail');
- const fetchItemsPromise = service.fetch();
-
- fetchItemsPromise
- .then(done.fail)
- .catch(error => {
- expect(error).toEqual(jasmine.any(SyntaxError));
- })
- .then(done)
- .catch(done.fail);
- });
-
- it('should reject when service is unavailable', done => {
- RecentSearchesService.isAvailable.and.returnValue(false);
-
- service
- .fetch()
- .then(done.fail)
- .catch(error => {
- expect(error).toEqual(jasmine.any(Error));
- })
- .then(done)
- .catch(done.fail);
- });
-
- it('should return items from localStorage', done => {
- window.localStorage.setItem(service.localStorageKey, '["foo", "bar"]');
- const fetchItemsPromise = service.fetch();
-
- fetchItemsPromise
- .then(items => {
- expect(items).toEqual(['foo', 'bar']);
- })
- .then(done)
- .catch(done.fail);
- });
-
- describe('if .isAvailable returns `false`', () => {
- beforeEach(() => {
- RecentSearchesService.isAvailable.and.returnValue(false);
-
- spyOn(window.localStorage, 'getItem');
- });
-
- it('should not call .getItem', done => {
- RecentSearchesService.prototype
- .fetch()
- .then(done.fail)
- .catch(err => {
- expect(err).toEqual(new RecentSearchesServiceError());
- expect(window.localStorage.getItem).not.toHaveBeenCalled();
- })
- .then(done)
- .catch(done.fail);
- });
- });
- });
-
- describe('setRecentSearches', () => {
- beforeEach(() => {
- spyOn(RecentSearchesService, 'isAvailable').and.returnValue(true);
- });
-
- it('should save things in localStorage', () => {
- const items = ['foo', 'bar'];
- service.save(items);
- const newLocalStorageValue = window.localStorage.getItem(service.localStorageKey);
-
- expect(JSON.parse(newLocalStorageValue)).toEqual(items);
- });
- });
-
- describe('save', () => {
- beforeEach(() => {
- spyOn(window.localStorage, 'setItem');
- spyOn(RecentSearchesService, 'isAvailable');
- });
-
- describe('if .isAvailable returns `true`', () => {
- const searchesString = 'searchesString';
- const localStorageKey = 'localStorageKey';
- const recentSearchesService = {
- localStorageKey,
- };
-
- beforeEach(() => {
- RecentSearchesService.isAvailable.and.returnValue(true);
-
- spyOn(JSON, 'stringify').and.returnValue(searchesString);
- });
-
- it('should call .setItem', () => {
- RecentSearchesService.prototype.save.call(recentSearchesService);
-
- expect(window.localStorage.setItem).toHaveBeenCalledWith(localStorageKey, searchesString);
- });
- });
-
- describe('if .isAvailable returns `false`', () => {
- beforeEach(() => {
- RecentSearchesService.isAvailable.and.returnValue(false);
- });
-
- it('should not call .setItem', () => {
- RecentSearchesService.prototype.save();
-
- expect(window.localStorage.setItem).not.toHaveBeenCalled();
- });
- });
- });
-
- describe('isAvailable', () => {
- let isAvailable;
-
- beforeEach(() => {
- spyOn(AccessorUtilities, 'isLocalStorageAccessSafe').and.callThrough();
-
- isAvailable = RecentSearchesService.isAvailable();
- });
-
- it('should call .isLocalStorageAccessSafe', () => {
- expect(AccessorUtilities.isLocalStorageAccessSafe).toHaveBeenCalled();
- });
-
- it('should return a boolean', () => {
- expect(typeof isAvailable).toBe('boolean');
- });
- });
-});
diff --git a/spec/javascripts/filtered_search/visual_token_value_spec.js b/spec/javascripts/filtered_search/visual_token_value_spec.js
deleted file mode 100644
index 4469ade1874..00000000000
--- a/spec/javascripts/filtered_search/visual_token_value_spec.js
+++ /dev/null
@@ -1,389 +0,0 @@
-import { escape as esc } from 'lodash';
-import VisualTokenValue from '~/filtered_search/visual_token_value';
-import AjaxCache from '~/lib/utils/ajax_cache';
-import UsersCache from '~/lib/utils/users_cache';
-import DropdownUtils from '~/filtered_search//dropdown_utils';
-import FilteredSearchSpecHelper from '../helpers/filtered_search_spec_helper';
-
-describe('Filtered Search Visual Tokens', () => {
- const findElements = tokenElement => {
- const tokenNameElement = tokenElement.querySelector('.name');
- const tokenValueContainer = tokenElement.querySelector('.value-container');
- const tokenValueElement = tokenValueContainer.querySelector('.value');
- const tokenOperatorElement = tokenElement.querySelector('.operator');
- const tokenType = tokenNameElement.innerText.toLowerCase();
- const tokenValue = tokenValueElement.innerText;
- const tokenOperator = tokenOperatorElement.innerText;
- const subject = new VisualTokenValue(tokenValue, tokenType, tokenOperator);
- return { subject, tokenValueContainer, tokenValueElement };
- };
-
- let tokensContainer;
- let authorToken;
- let bugLabelToken;
-
- beforeEach(() => {
- setFixtures(`
- <ul class="tokens-container">
- ${FilteredSearchSpecHelper.createInputHTML()}
- </ul>
- `);
- tokensContainer = document.querySelector('.tokens-container');
-
- authorToken = FilteredSearchSpecHelper.createFilterVisualToken('author', '=', '@user');
- bugLabelToken = FilteredSearchSpecHelper.createFilterVisualToken('label', '=', '~bug');
- });
-
- describe('updateUserTokenAppearance', () => {
- let usersCacheSpy;
-
- beforeEach(() => {
- spyOn(UsersCache, 'retrieve').and.callFake(username => usersCacheSpy(username));
- });
-
- it('ignores error if UsersCache throws', done => {
- spyOn(window, 'Flash');
- const dummyError = new Error('Earth rotated backwards');
- const { subject, tokenValueContainer, tokenValueElement } = findElements(authorToken);
- const tokenValue = tokenValueElement.innerText;
- usersCacheSpy = username => {
- expect(`@${username}`).toBe(tokenValue);
- return Promise.reject(dummyError);
- };
-
- subject
- .updateUserTokenAppearance(tokenValueContainer, tokenValueElement, tokenValue)
- .then(() => {
- expect(window.Flash.calls.count()).toBe(0);
- })
- .then(done)
- .catch(done.fail);
- });
-
- it('does nothing if user cannot be found', done => {
- const { subject, tokenValueContainer, tokenValueElement } = findElements(authorToken);
- const tokenValue = tokenValueElement.innerText;
- usersCacheSpy = username => {
- expect(`@${username}`).toBe(tokenValue);
- return Promise.resolve(undefined);
- };
-
- subject
- .updateUserTokenAppearance(tokenValueContainer, tokenValueElement, tokenValue)
- .then(() => {
- expect(tokenValueElement.innerText).toBe(tokenValue);
- })
- .then(done)
- .catch(done.fail);
- });
-
- it('replaces author token with avatar and display name', done => {
- const dummyUser = {
- name: 'Important Person',
- avatar_url: 'https://host.invalid/mypics/avatar.png',
- };
- const { subject, tokenValueContainer, tokenValueElement } = findElements(authorToken);
- const tokenValue = tokenValueElement.innerText;
- usersCacheSpy = username => {
- expect(`@${username}`).toBe(tokenValue);
- return Promise.resolve(dummyUser);
- };
-
- subject
- .updateUserTokenAppearance(tokenValueContainer, tokenValueElement, tokenValue)
- .then(() => {
- expect(tokenValueContainer.dataset.originalValue).toBe(tokenValue);
- expect(tokenValueElement.innerText.trim()).toBe(dummyUser.name);
- const avatar = tokenValueElement.querySelector('img.avatar');
-
- expect(avatar.src).toBe(dummyUser.avatar_url);
- expect(avatar.alt).toBe('');
- })
- .then(done)
- .catch(done.fail);
- });
-
- it('escapes user name when creating token', done => {
- const dummyUser = {
- name: '<script>',
- avatar_url: `${gl.TEST_HOST}/mypics/avatar.png`,
- };
- const { subject, tokenValueContainer, tokenValueElement } = findElements(authorToken);
- const tokenValue = tokenValueElement.innerText;
- usersCacheSpy = username => {
- expect(`@${username}`).toBe(tokenValue);
- return Promise.resolve(dummyUser);
- };
-
- subject
- .updateUserTokenAppearance(tokenValueContainer, tokenValueElement, tokenValue)
- .then(() => {
- expect(tokenValueElement.innerText.trim()).toBe(dummyUser.name);
- tokenValueElement.querySelector('.avatar').remove();
-
- expect(tokenValueElement.innerHTML.trim()).toBe(esc(dummyUser.name));
- })
- .then(done)
- .catch(done.fail);
- });
- });
-
- describe('updateLabelTokenColor', () => {
- const jsonFixtureName = 'labels/project_labels.json';
- const dummyEndpoint = '/dummy/endpoint';
-
- preloadFixtures(jsonFixtureName);
-
- let labelData;
-
- beforeAll(() => {
- labelData = getJSONFixture(jsonFixtureName);
- });
-
- const missingLabelToken = FilteredSearchSpecHelper.createFilterVisualToken(
- 'label',
- '=',
- '~doesnotexist',
- );
- const spaceLabelToken = FilteredSearchSpecHelper.createFilterVisualToken(
- 'label',
- '=',
- '~"some space"',
- );
-
- beforeEach(() => {
- tokensContainer.innerHTML = FilteredSearchSpecHelper.createTokensContainerHTML(`
- ${bugLabelToken.outerHTML}
- ${missingLabelToken.outerHTML}
- ${spaceLabelToken.outerHTML}
- `);
-
- const filteredSearchInput = document.querySelector('.filtered-search');
- filteredSearchInput.dataset.runnerTagsEndpoint = `${dummyEndpoint}/admin/runners/tag_list`;
- filteredSearchInput.dataset.labelsEndpoint = `${dummyEndpoint}/-/labels`;
- filteredSearchInput.dataset.milestonesEndpoint = `${dummyEndpoint}/-/milestones`;
-
- AjaxCache.internalStorage = {};
- AjaxCache.internalStorage[`${filteredSearchInput.dataset.labelsEndpoint}.json`] = labelData;
- });
-
- const parseColor = color => {
- const dummyElement = document.createElement('div');
- dummyElement.style.color = color;
- return dummyElement.style.color;
- };
-
- const expectValueContainerStyle = (tokenValueContainer, label) => {
- expect(tokenValueContainer.getAttribute('style')).not.toBe(null);
- expect(tokenValueContainer.style.backgroundColor).toBe(parseColor(label.color));
- expect(tokenValueContainer.style.color).toBe(parseColor(label.text_color));
- };
-
- const findLabel = tokenValue =>
- labelData.find(label => tokenValue === `~${DropdownUtils.getEscapedText(label.title)}`);
-
- it('updates the color of a label token', done => {
- const { subject, tokenValueContainer, tokenValueElement } = findElements(bugLabelToken);
- const tokenValue = tokenValueElement.innerText;
- const matchingLabel = findLabel(tokenValue);
-
- subject
- .updateLabelTokenColor(tokenValueContainer, tokenValue)
- .then(() => {
- expectValueContainerStyle(tokenValueContainer, matchingLabel);
- })
- .then(done)
- .catch(done.fail);
- });
-
- it('updates the color of a label token with spaces', done => {
- const { subject, tokenValueContainer, tokenValueElement } = findElements(spaceLabelToken);
- const tokenValue = tokenValueElement.innerText;
- const matchingLabel = findLabel(tokenValue);
-
- subject
- .updateLabelTokenColor(tokenValueContainer, tokenValue)
- .then(() => {
- expectValueContainerStyle(tokenValueContainer, matchingLabel);
- })
- .then(done)
- .catch(done.fail);
- });
-
- it('does not change color of a missing label', done => {
- const { subject, tokenValueContainer, tokenValueElement } = findElements(missingLabelToken);
- const tokenValue = tokenValueElement.innerText;
- const matchingLabel = findLabel(tokenValue);
-
- expect(matchingLabel).toBe(undefined);
-
- subject
- .updateLabelTokenColor(tokenValueContainer, tokenValue)
- .then(() => {
- expect(tokenValueContainer.getAttribute('style')).toBe(null);
- })
- .then(done)
- .catch(done.fail);
- });
- });
-
- describe('setTokenStyle', () => {
- let originalTextColor;
-
- beforeEach(() => {
- originalTextColor = bugLabelToken.style.color;
- });
-
- it('should set backgroundColor', () => {
- const originalBackgroundColor = bugLabelToken.style.backgroundColor;
- const token = VisualTokenValue.setTokenStyle(bugLabelToken, 'blue', 'white');
-
- expect(token.style.backgroundColor).toEqual('blue');
- expect(token.style.backgroundColor).not.toEqual(originalBackgroundColor);
- });
-
- it('should set textColor', () => {
- const token = VisualTokenValue.setTokenStyle(bugLabelToken, 'white', 'black');
-
- expect(token.style.color).toEqual('black');
- expect(token.style.color).not.toEqual(originalTextColor);
- });
-
- it('should add inverted class when textColor is #FFFFFF', () => {
- const token = VisualTokenValue.setTokenStyle(bugLabelToken, 'black', '#FFFFFF');
-
- expect(token.style.color).toEqual('rgb(255, 255, 255)');
- expect(token.style.color).not.toEqual(originalTextColor);
- expect(token.querySelector('.remove-token').classList.contains('inverted')).toEqual(true);
- });
- });
-
- describe('render', () => {
- const setupSpies = subject => {
- spyOn(subject, 'updateLabelTokenColor'); // eslint-disable-line jasmine/no-unsafe-spy
- const updateLabelTokenColorSpy = subject.updateLabelTokenColor;
-
- spyOn(subject, 'updateUserTokenAppearance'); // eslint-disable-line jasmine/no-unsafe-spy
- const updateUserTokenAppearanceSpy = subject.updateUserTokenAppearance;
-
- return { updateLabelTokenColorSpy, updateUserTokenAppearanceSpy };
- };
-
- const keywordToken = FilteredSearchSpecHelper.createFilterVisualToken('search');
- const milestoneToken = FilteredSearchSpecHelper.createFilterVisualToken(
- 'milestone',
- 'upcoming',
- );
-
- beforeEach(() => {
- tokensContainer.innerHTML = FilteredSearchSpecHelper.createTokensContainerHTML(`
- ${authorToken.outerHTML}
- ${bugLabelToken.outerHTML}
- ${keywordToken.outerHTML}
- ${milestoneToken.outerHTML}
- `);
- });
-
- it('renders a author token value element', () => {
- const { subject, tokenValueContainer, tokenValueElement } = findElements(authorToken);
-
- const { updateLabelTokenColorSpy, updateUserTokenAppearanceSpy } = setupSpies(subject);
- subject.render(tokenValueContainer, tokenValueElement);
-
- expect(updateUserTokenAppearanceSpy.calls.count()).toBe(1);
- const expectedArgs = [tokenValueContainer, tokenValueElement];
-
- expect(updateUserTokenAppearanceSpy.calls.argsFor(0)).toEqual(expectedArgs);
- expect(updateLabelTokenColorSpy.calls.count()).toBe(0);
- });
-
- it('renders a label token value element', () => {
- const { subject, tokenValueContainer, tokenValueElement } = findElements(bugLabelToken);
-
- const { updateLabelTokenColorSpy, updateUserTokenAppearanceSpy } = setupSpies(subject);
- subject.render(tokenValueContainer, tokenValueElement);
-
- expect(updateLabelTokenColorSpy.calls.count()).toBe(1);
- const expectedArgs = [tokenValueContainer];
-
- expect(updateLabelTokenColorSpy.calls.argsFor(0)).toEqual(expectedArgs);
- expect(updateUserTokenAppearanceSpy.calls.count()).toBe(0);
- });
-
- it('renders a milestone token value element', () => {
- const { subject, tokenValueContainer, tokenValueElement } = findElements(milestoneToken);
-
- const { updateLabelTokenColorSpy, updateUserTokenAppearanceSpy } = setupSpies(subject);
- subject.render(tokenValueContainer, tokenValueElement);
-
- expect(updateLabelTokenColorSpy.calls.count()).toBe(0);
- expect(updateUserTokenAppearanceSpy.calls.count()).toBe(0);
- });
-
- it('does not update user token appearance for `none` filter', () => {
- const { subject, tokenValueContainer, tokenValueElement } = findElements(authorToken);
-
- subject.tokenValue = 'none';
-
- const { updateUserTokenAppearanceSpy } = setupSpies(subject);
- subject.render(tokenValueContainer, tokenValueElement);
-
- expect(updateUserTokenAppearanceSpy.calls.count()).toBe(0);
- });
-
- it('does not update user token appearance for `None` filter', () => {
- const { subject, tokenValueContainer, tokenValueElement } = findElements(authorToken);
-
- subject.tokenValue = 'None';
-
- const { updateUserTokenAppearanceSpy } = setupSpies(subject);
- subject.render(tokenValueContainer, tokenValueElement);
-
- expect(updateUserTokenAppearanceSpy.calls.count()).toBe(0);
- });
-
- it('does not update user token appearance for `any` filter', () => {
- const { subject, tokenValueContainer, tokenValueElement } = findElements(authorToken);
-
- subject.tokenValue = 'any';
-
- const { updateUserTokenAppearanceSpy } = setupSpies(subject);
- subject.render(tokenValueContainer, tokenValueElement);
-
- expect(updateUserTokenAppearanceSpy.calls.count()).toBe(0);
- });
-
- it('does not update label token color for `None` filter', () => {
- const { subject, tokenValueContainer, tokenValueElement } = findElements(bugLabelToken);
-
- subject.tokenValue = 'None';
-
- const { updateLabelTokenColorSpy } = setupSpies(subject);
- subject.render(tokenValueContainer, tokenValueElement);
-
- expect(updateLabelTokenColorSpy.calls.count()).toBe(0);
- });
-
- it('does not update label token color for `none` filter', () => {
- const { subject, tokenValueContainer, tokenValueElement } = findElements(bugLabelToken);
-
- subject.tokenValue = 'none';
-
- const { updateLabelTokenColorSpy } = setupSpies(subject);
- subject.render(tokenValueContainer, tokenValueElement);
-
- expect(updateLabelTokenColorSpy.calls.count()).toBe(0);
- });
-
- it('does not update label token color for `any` filter', () => {
- const { subject, tokenValueContainer, tokenValueElement } = findElements(bugLabelToken);
-
- subject.tokenValue = 'any';
-
- const { updateLabelTokenColorSpy } = setupSpies(subject);
- subject.render(tokenValueContainer, tokenValueElement);
-
- expect(updateLabelTokenColorSpy.calls.count()).toBe(0);
- });
- });
-});
diff --git a/spec/javascripts/flash_spec.js b/spec/javascripts/flash_spec.js
deleted file mode 100644
index 39ca4eedb69..00000000000
--- a/spec/javascripts/flash_spec.js
+++ /dev/null
@@ -1,236 +0,0 @@
-import flash, { createFlashEl, createAction, hideFlash, removeFlashClickListener } from '~/flash';
-
-describe('Flash', () => {
- describe('createFlashEl', () => {
- let el;
-
- beforeEach(() => {
- el = document.createElement('div');
- });
-
- afterEach(() => {
- el.innerHTML = '';
- });
-
- it('creates flash element with type', () => {
- el.innerHTML = createFlashEl('testing', 'alert');
-
- expect(el.querySelector('.flash-alert')).not.toBeNull();
- });
-
- it('escapes text', () => {
- el.innerHTML = createFlashEl('<script>alert("a");</script>', 'alert');
-
- expect(el.querySelector('.flash-text').textContent.trim()).toBe(
- '<script>alert("a");</script>',
- );
- });
- });
-
- describe('hideFlash', () => {
- let el;
-
- beforeEach(() => {
- el = document.createElement('div');
- el.className = 'js-testing';
- });
-
- it('sets transition style', () => {
- hideFlash(el);
-
- expect(el.style['transition-property']).toBe('opacity');
-
- expect(el.style['transition-duration']).toBe('0.15s');
- });
-
- it('sets opacity style', () => {
- hideFlash(el);
-
- expect(el.style.opacity).toBe('0');
- });
-
- it('does not set styles when fadeTransition is false', () => {
- hideFlash(el, false);
-
- expect(el.style.opacity).toBe('');
-
- expect(el.style.transition).toBe('');
- });
-
- it('removes element after transitionend', () => {
- document.body.appendChild(el);
-
- hideFlash(el);
- el.dispatchEvent(new Event('transitionend'));
-
- expect(document.querySelector('.js-testing')).toBeNull();
- });
-
- it('calls event listener callback once', () => {
- spyOn(el, 'remove').and.callThrough();
- document.body.appendChild(el);
-
- hideFlash(el);
-
- el.dispatchEvent(new Event('transitionend'));
- el.dispatchEvent(new Event('transitionend'));
-
- expect(el.remove.calls.count()).toBe(1);
- });
- });
-
- describe('createAction', () => {
- let el;
-
- beforeEach(() => {
- el = document.createElement('div');
- });
-
- it('creates link with href', () => {
- el.innerHTML = createAction({
- href: 'testing',
- title: 'test',
- });
-
- expect(el.querySelector('.flash-action').href).toContain('testing');
- });
-
- it('uses hash as href when no href is present', () => {
- el.innerHTML = createAction({
- title: 'test',
- });
-
- expect(el.querySelector('.flash-action').href).toContain('#');
- });
-
- it('adds role when no href is present', () => {
- el.innerHTML = createAction({
- title: 'test',
- });
-
- expect(el.querySelector('.flash-action').getAttribute('role')).toBe('button');
- });
-
- it('escapes the title text', () => {
- el.innerHTML = createAction({
- title: '<script>alert("a")</script>',
- });
-
- expect(el.querySelector('.flash-action').textContent.trim()).toBe(
- '<script>alert("a")</script>',
- );
- });
- });
-
- describe('createFlash', () => {
- describe('no flash-container', () => {
- it('does not add to the DOM', () => {
- const flashEl = flash('testing');
-
- expect(flashEl).toBeNull();
-
- expect(document.querySelector('.flash-alert')).toBeNull();
- });
- });
-
- describe('with flash-container', () => {
- beforeEach(() => {
- document.body.innerHTML += `
- <div class="content-wrapper js-content-wrapper">
- <div class="flash-container"></div>
- </div>
- `;
- });
-
- afterEach(() => {
- document.querySelector('.js-content-wrapper').remove();
- });
-
- it('adds flash element into container', () => {
- flash('test', 'alert', document, null, false, true);
-
- expect(document.querySelector('.flash-alert')).not.toBeNull();
-
- expect(document.body.className).toContain('flash-shown');
- });
-
- it('adds flash into specified parent', () => {
- flash('test', 'alert', document.querySelector('.content-wrapper'));
-
- expect(document.querySelector('.content-wrapper .flash-alert')).not.toBeNull();
- });
-
- it('adds container classes when inside content-wrapper', () => {
- flash('test');
-
- expect(document.querySelector('.flash-text').className).toBe('flash-text');
- });
-
- it('does not add container when outside of content-wrapper', () => {
- document.querySelector('.content-wrapper').className = 'js-content-wrapper';
- flash('test');
-
- expect(document.querySelector('.flash-text').className.trim()).toContain('flash-text');
- });
-
- it('removes element after clicking', () => {
- flash('test', 'alert', document, null, false, true);
-
- document.querySelector('.flash-alert .js-close-icon').click();
-
- expect(document.querySelector('.flash-alert')).toBeNull();
-
- expect(document.body.className).not.toContain('flash-shown');
- });
-
- describe('with actionConfig', () => {
- it('adds action link', () => {
- flash('test', 'alert', document, {
- title: 'test',
- });
-
- expect(document.querySelector('.flash-action')).not.toBeNull();
- });
-
- it('calls actionConfig clickHandler on click', () => {
- const actionConfig = {
- title: 'test',
- clickHandler: jasmine.createSpy('actionConfig'),
- };
-
- flash('test', 'alert', document, actionConfig);
-
- document.querySelector('.flash-action').click();
-
- expect(actionConfig.clickHandler).toHaveBeenCalled();
- });
- });
- });
- });
-
- describe('removeFlashClickListener', () => {
- beforeEach(() => {
- document.body.innerHTML += `
- <div class="flash-container">
- <div class="flash">
- <div class="close-icon js-close-icon"></div>
- </div>
- </div>
- `;
- });
-
- it('removes global flash on click', done => {
- const flashEl = document.querySelector('.flash');
-
- removeFlashClickListener(flashEl, false);
-
- flashEl.querySelector('.js-close-icon').click();
-
- setTimeout(() => {
- expect(document.querySelector('.flash')).toBeNull();
-
- done();
- });
- });
- });
-});
diff --git a/spec/javascripts/frequent_items/components/app_spec.js b/spec/javascripts/frequent_items/components/app_spec.js
deleted file mode 100644
index b293ed541fd..00000000000
--- a/spec/javascripts/frequent_items/components/app_spec.js
+++ /dev/null
@@ -1,257 +0,0 @@
-import MockAdapter from 'axios-mock-adapter';
-import Vue from 'vue';
-import { mountComponentWithStore } from 'spec/helpers/vue_mount_component_helper';
-import axios from '~/lib/utils/axios_utils';
-import appComponent from '~/frequent_items/components/app.vue';
-import eventHub from '~/frequent_items/event_hub';
-import store from '~/frequent_items/store';
-import { FREQUENT_ITEMS, HOUR_IN_MS } from '~/frequent_items/constants';
-import { getTopFrequentItems } from '~/frequent_items/utils';
-import { currentSession, mockFrequentProjects, mockSearchedProjects } from '../mock_data';
-
-let session;
-const createComponentWithStore = (namespace = 'projects') => {
- session = currentSession[namespace];
- gon.api_version = session.apiVersion;
- const Component = Vue.extend(appComponent);
-
- return mountComponentWithStore(Component, {
- store,
- props: {
- namespace,
- currentUserName: session.username,
- currentItem: session.project || session.group,
- },
- });
-};
-
-describe('Frequent Items App Component', () => {
- let vm;
- let mock;
-
- beforeEach(() => {
- mock = new MockAdapter(axios);
- vm = createComponentWithStore();
- });
-
- afterEach(() => {
- mock.restore();
- vm.$destroy();
- });
-
- describe('methods', () => {
- describe('dropdownOpenHandler', () => {
- it('should fetch frequent items when no search has been previously made on desktop', () => {
- spyOn(vm, 'fetchFrequentItems');
-
- vm.dropdownOpenHandler();
-
- expect(vm.fetchFrequentItems).toHaveBeenCalledWith();
- });
- });
-
- describe('logItemAccess', () => {
- let storage;
-
- beforeEach(() => {
- storage = {};
-
- spyOn(window.localStorage, 'setItem').and.callFake((storageKey, value) => {
- storage[storageKey] = value;
- });
-
- spyOn(window.localStorage, 'getItem').and.callFake(storageKey => {
- if (storage[storageKey]) {
- return storage[storageKey];
- }
-
- return null;
- });
- });
-
- it('should create a project store if it does not exist and adds a project', () => {
- vm.logItemAccess(session.storageKey, session.project);
-
- const projects = JSON.parse(storage[session.storageKey]);
-
- expect(projects.length).toBe(1);
- expect(projects[0].frequency).toBe(1);
- expect(projects[0].lastAccessedOn).toBeDefined();
- });
-
- it('should prevent inserting same report multiple times into store', () => {
- vm.logItemAccess(session.storageKey, session.project);
- vm.logItemAccess(session.storageKey, session.project);
-
- const projects = JSON.parse(storage[session.storageKey]);
-
- expect(projects.length).toBe(1);
- });
-
- it('should increase frequency of report if it was logged multiple times over the course of an hour', () => {
- let projects;
- const newTimestamp = Date.now() + HOUR_IN_MS + 1;
-
- vm.logItemAccess(session.storageKey, session.project);
- projects = JSON.parse(storage[session.storageKey]);
-
- expect(projects[0].frequency).toBe(1);
-
- vm.logItemAccess(session.storageKey, {
- ...session.project,
- lastAccessedOn: newTimestamp,
- });
- projects = JSON.parse(storage[session.storageKey]);
-
- expect(projects[0].frequency).toBe(2);
- expect(projects[0].lastAccessedOn).not.toBe(session.project.lastAccessedOn);
- });
-
- it('should always update project metadata', () => {
- let projects;
- const oldProject = {
- ...session.project,
- };
-
- const newProject = {
- ...session.project,
- name: 'New Name',
- avatarUrl: 'new/avatar.png',
- namespace: 'New / Namespace',
- webUrl: 'http://localhost/new/web/url',
- };
-
- vm.logItemAccess(session.storageKey, oldProject);
- projects = JSON.parse(storage[session.storageKey]);
-
- expect(projects[0].name).toBe(oldProject.name);
- expect(projects[0].avatarUrl).toBe(oldProject.avatarUrl);
- expect(projects[0].namespace).toBe(oldProject.namespace);
- expect(projects[0].webUrl).toBe(oldProject.webUrl);
-
- vm.logItemAccess(session.storageKey, newProject);
- projects = JSON.parse(storage[session.storageKey]);
-
- expect(projects[0].name).toBe(newProject.name);
- expect(projects[0].avatarUrl).toBe(newProject.avatarUrl);
- expect(projects[0].namespace).toBe(newProject.namespace);
- expect(projects[0].webUrl).toBe(newProject.webUrl);
- });
-
- it('should not add more than 20 projects in store', () => {
- for (let id = 0; id < FREQUENT_ITEMS.MAX_COUNT; id += 1) {
- const project = {
- ...session.project,
- id,
- };
- vm.logItemAccess(session.storageKey, project);
- }
-
- const projects = JSON.parse(storage[session.storageKey]);
-
- expect(projects.length).toBe(FREQUENT_ITEMS.MAX_COUNT);
- });
- });
- });
-
- describe('created', () => {
- it('should bind event listeners on eventHub', done => {
- spyOn(eventHub, '$on');
-
- createComponentWithStore().$mount();
-
- Vue.nextTick(() => {
- expect(eventHub.$on).toHaveBeenCalledWith('projects-dropdownOpen', jasmine.any(Function));
- done();
- });
- });
- });
-
- describe('beforeDestroy', () => {
- it('should unbind event listeners on eventHub', done => {
- spyOn(eventHub, '$off');
-
- vm.$mount();
- vm.$destroy();
-
- Vue.nextTick(() => {
- expect(eventHub.$off).toHaveBeenCalledWith('projects-dropdownOpen', jasmine.any(Function));
- done();
- });
- });
- });
-
- describe('template', () => {
- it('should render search input', () => {
- expect(vm.$el.querySelector('.search-input-container')).toBeDefined();
- });
-
- it('should render loading animation', done => {
- vm.$store.dispatch('fetchSearchedItems');
-
- Vue.nextTick(() => {
- const loadingEl = vm.$el.querySelector('.loading-animation');
-
- expect(loadingEl).toBeDefined();
- expect(loadingEl.classList.contains('prepend-top-20')).toBe(true);
- expect(loadingEl.querySelector('span').getAttribute('aria-label')).toBe('Loading projects');
- done();
- });
- });
-
- it('should render frequent projects list header', done => {
- Vue.nextTick(() => {
- const sectionHeaderEl = vm.$el.querySelector('.section-header');
-
- expect(sectionHeaderEl).toBeDefined();
- expect(sectionHeaderEl.innerText.trim()).toBe('Frequently visited');
- done();
- });
- });
-
- it('should render frequent projects list', done => {
- const expectedResult = getTopFrequentItems(mockFrequentProjects);
- spyOn(window.localStorage, 'getItem').and.callFake(() =>
- JSON.stringify(mockFrequentProjects),
- );
-
- expect(vm.$el.querySelectorAll('.frequent-items-list-container li').length).toBe(1);
-
- vm.fetchFrequentItems();
- Vue.nextTick(() => {
- expect(vm.$el.querySelectorAll('.frequent-items-list-container li').length).toBe(
- expectedResult.length,
- );
- done();
- });
- });
-
- it('should render searched projects list', done => {
- mock.onGet(/\/api\/v4\/projects.json(.*)$/).replyOnce(200, mockSearchedProjects);
-
- expect(vm.$el.querySelectorAll('.frequent-items-list-container li').length).toBe(1);
-
- vm.$store.dispatch('setSearchQuery', 'gitlab');
- vm.$nextTick()
- .then(() => {
- expect(vm.$el.querySelector('.loading-animation')).toBeDefined();
- })
-
- // This test waits for multiple ticks in order to allow the responses to
- // propagate through each interceptor installed on the Axios instance.
- // This shouldn't be necessary; this test should be refactored to avoid this.
- // https://gitlab.com/gitlab-org/gitlab/issues/32479
- .then(vm.$nextTick)
- .then(vm.$nextTick)
- .then(vm.$nextTick)
-
- .then(() => {
- expect(vm.$el.querySelectorAll('.frequent-items-list-container li').length).toBe(
- mockSearchedProjects.data.length,
- );
- })
- .then(done)
- .catch(done.fail);
- });
- });
-});
diff --git a/spec/javascripts/frequent_items/mock_data.js b/spec/javascripts/frequent_items/mock_data.js
deleted file mode 100644
index 419f70e41af..00000000000
--- a/spec/javascripts/frequent_items/mock_data.js
+++ /dev/null
@@ -1,168 +0,0 @@
-export const currentSession = {
- groups: {
- username: 'root',
- storageKey: 'root/frequent-groups',
- apiVersion: 'v4',
- group: {
- id: 1,
- name: 'dummy-group',
- full_name: 'dummy-parent-group',
- webUrl: `${gl.TEST_HOST}/dummy-group`,
- avatarUrl: null,
- lastAccessedOn: Date.now(),
- },
- },
- projects: {
- username: 'root',
- storageKey: 'root/frequent-projects',
- apiVersion: 'v4',
- project: {
- id: 1,
- name: 'dummy-project',
- namespace: 'SampleGroup / Dummy-Project',
- webUrl: `${gl.TEST_HOST}/samplegroup/dummy-project`,
- avatarUrl: null,
- lastAccessedOn: Date.now(),
- },
- },
-};
-
-export const mockNamespace = 'projects';
-
-export const mockStorageKey = 'test-user/frequent-projects';
-
-export const mockGroup = {
- id: 1,
- name: 'Sub451',
- namespace: 'Commit451 / Sub451',
- webUrl: `${gl.TEST_HOST}/Commit451/Sub451`,
- avatarUrl: null,
-};
-
-export const mockRawGroup = {
- id: 1,
- name: 'Sub451',
- full_name: 'Commit451 / Sub451',
- web_url: `${gl.TEST_HOST}/Commit451/Sub451`,
- avatar_url: null,
-};
-
-export const mockFrequentGroups = [
- {
- id: 3,
- name: 'Subgroup451',
- full_name: 'Commit451 / Subgroup451',
- webUrl: '/Commit451/Subgroup451',
- avatarUrl: null,
- frequency: 7,
- lastAccessedOn: 1497979281815,
- },
- {
- id: 1,
- name: 'Commit451',
- full_name: 'Commit451',
- webUrl: '/Commit451',
- avatarUrl: null,
- frequency: 3,
- lastAccessedOn: 1497979281815,
- },
-];
-
-export const mockSearchedGroups = [mockRawGroup];
-export const mockProcessedSearchedGroups = [mockGroup];
-
-export const mockProject = {
- id: 1,
- name: 'GitLab Community Edition',
- namespace: 'gitlab-org / gitlab-ce',
- webUrl: `${gl.TEST_HOST}/gitlab-org/gitlab-foss`,
- avatarUrl: null,
-};
-
-export const mockRawProject = {
- id: 1,
- name: 'GitLab Community Edition',
- name_with_namespace: 'gitlab-org / gitlab-ce',
- web_url: `${gl.TEST_HOST}/gitlab-org/gitlab-foss`,
- avatar_url: null,
-};
-
-export const mockFrequentProjects = [
- {
- id: 1,
- name: 'GitLab Community Edition',
- namespace: 'gitlab-org / gitlab-ce',
- webUrl: `${gl.TEST_HOST}/gitlab-org/gitlab-foss`,
- avatarUrl: null,
- frequency: 1,
- lastAccessedOn: Date.now(),
- },
- {
- id: 2,
- name: 'GitLab CI',
- namespace: 'gitlab-org / gitlab-ci',
- webUrl: `${gl.TEST_HOST}/gitlab-org/gitlab-ci`,
- avatarUrl: null,
- frequency: 9,
- lastAccessedOn: Date.now(),
- },
- {
- id: 3,
- name: 'Typeahead.Js',
- namespace: 'twitter / typeahead-js',
- webUrl: `${gl.TEST_HOST}/twitter/typeahead-js`,
- avatarUrl: '/uploads/-/system/project/avatar/7/TWBS.png',
- frequency: 2,
- lastAccessedOn: Date.now(),
- },
- {
- id: 4,
- name: 'Intel',
- namespace: 'platform / hardware / bsp / intel',
- webUrl: `${gl.TEST_HOST}/platform/hardware/bsp/intel`,
- avatarUrl: null,
- frequency: 3,
- lastAccessedOn: Date.now(),
- },
- {
- id: 5,
- name: 'v4.4',
- namespace: 'platform / hardware / bsp / kernel / common / v4.4',
- webUrl: `${gl.TEST_HOST}/platform/hardware/bsp/kernel/common/v4.4`,
- avatarUrl: null,
- frequency: 8,
- lastAccessedOn: Date.now(),
- },
-];
-
-export const mockSearchedProjects = { data: [mockRawProject] };
-export const mockProcessedSearchedProjects = [mockProject];
-
-export const unsortedFrequentItems = [
- { id: 1, frequency: 12, lastAccessedOn: 1491400843391 },
- { id: 2, frequency: 14, lastAccessedOn: 1488240890738 },
- { id: 3, frequency: 44, lastAccessedOn: 1497675908472 },
- { id: 4, frequency: 8, lastAccessedOn: 1497979281815 },
- { id: 5, frequency: 34, lastAccessedOn: 1488089211943 },
- { id: 6, frequency: 14, lastAccessedOn: 1493517292488 },
- { id: 7, frequency: 42, lastAccessedOn: 1486815299875 },
- { id: 8, frequency: 33, lastAccessedOn: 1500762279114 },
- { id: 10, frequency: 46, lastAccessedOn: 1483251641543 },
-];
-
-/**
- * This const has a specific order which tests authenticity
- * of `getTopFrequentItems` method so
- * DO NOT change order of items in this const.
- */
-export const sortedFrequentItems = [
- { id: 10, frequency: 46, lastAccessedOn: 1483251641543 },
- { id: 3, frequency: 44, lastAccessedOn: 1497675908472 },
- { id: 7, frequency: 42, lastAccessedOn: 1486815299875 },
- { id: 5, frequency: 34, lastAccessedOn: 1488089211943 },
- { id: 8, frequency: 33, lastAccessedOn: 1500762279114 },
- { id: 6, frequency: 14, lastAccessedOn: 1493517292488 },
- { id: 2, frequency: 14, lastAccessedOn: 1488240890738 },
- { id: 1, frequency: 12, lastAccessedOn: 1491400843391 },
- { id: 4, frequency: 8, lastAccessedOn: 1497979281815 },
-];
diff --git a/spec/javascripts/frequent_items/store/actions_spec.js b/spec/javascripts/frequent_items/store/actions_spec.js
deleted file mode 100644
index 7b065b69cce..00000000000
--- a/spec/javascripts/frequent_items/store/actions_spec.js
+++ /dev/null
@@ -1,228 +0,0 @@
-import testAction from 'spec/helpers/vuex_action_helper';
-import MockAdapter from 'axios-mock-adapter';
-import axios from '~/lib/utils/axios_utils';
-import AccessorUtilities from '~/lib/utils/accessor';
-import * as actions from '~/frequent_items/store/actions';
-import * as types from '~/frequent_items/store/mutation_types';
-import state from '~/frequent_items/store/state';
-import {
- mockNamespace,
- mockStorageKey,
- mockFrequentProjects,
- mockSearchedProjects,
-} from '../mock_data';
-
-describe('Frequent Items Dropdown Store Actions', () => {
- let mockedState;
- let mock;
-
- beforeEach(() => {
- mockedState = state();
- mock = new MockAdapter(axios);
-
- mockedState.namespace = mockNamespace;
- mockedState.storageKey = mockStorageKey;
- });
-
- afterEach(() => {
- mock.restore();
- });
-
- describe('setNamespace', () => {
- it('should set namespace', done => {
- testAction(
- actions.setNamespace,
- mockNamespace,
- mockedState,
- [{ type: types.SET_NAMESPACE, payload: mockNamespace }],
- [],
- done,
- );
- });
- });
-
- describe('setStorageKey', () => {
- it('should set storage key', done => {
- testAction(
- actions.setStorageKey,
- mockStorageKey,
- mockedState,
- [{ type: types.SET_STORAGE_KEY, payload: mockStorageKey }],
- [],
- done,
- );
- });
- });
-
- describe('requestFrequentItems', () => {
- it('should request frequent items', done => {
- testAction(
- actions.requestFrequentItems,
- null,
- mockedState,
- [{ type: types.REQUEST_FREQUENT_ITEMS }],
- [],
- done,
- );
- });
- });
-
- describe('receiveFrequentItemsSuccess', () => {
- it('should set frequent items', done => {
- testAction(
- actions.receiveFrequentItemsSuccess,
- mockFrequentProjects,
- mockedState,
- [{ type: types.RECEIVE_FREQUENT_ITEMS_SUCCESS, payload: mockFrequentProjects }],
- [],
- done,
- );
- });
- });
-
- describe('receiveFrequentItemsError', () => {
- it('should set frequent items error state', done => {
- testAction(
- actions.receiveFrequentItemsError,
- null,
- mockedState,
- [{ type: types.RECEIVE_FREQUENT_ITEMS_ERROR }],
- [],
- done,
- );
- });
- });
-
- describe('fetchFrequentItems', () => {
- it('should dispatch `receiveFrequentItemsSuccess`', done => {
- mockedState.namespace = mockNamespace;
- mockedState.storageKey = mockStorageKey;
-
- testAction(
- actions.fetchFrequentItems,
- null,
- mockedState,
- [],
- [{ type: 'requestFrequentItems' }, { type: 'receiveFrequentItemsSuccess', payload: [] }],
- done,
- );
- });
-
- it('should dispatch `receiveFrequentItemsError`', done => {
- spyOn(AccessorUtilities, 'isLocalStorageAccessSafe').and.returnValue(false);
- mockedState.namespace = mockNamespace;
- mockedState.storageKey = mockStorageKey;
-
- testAction(
- actions.fetchFrequentItems,
- null,
- mockedState,
- [],
- [{ type: 'requestFrequentItems' }, { type: 'receiveFrequentItemsError' }],
- done,
- );
- });
- });
-
- describe('requestSearchedItems', () => {
- it('should request searched items', done => {
- testAction(
- actions.requestSearchedItems,
- null,
- mockedState,
- [{ type: types.REQUEST_SEARCHED_ITEMS }],
- [],
- done,
- );
- });
- });
-
- describe('receiveSearchedItemsSuccess', () => {
- it('should set searched items', done => {
- testAction(
- actions.receiveSearchedItemsSuccess,
- mockSearchedProjects,
- mockedState,
- [{ type: types.RECEIVE_SEARCHED_ITEMS_SUCCESS, payload: mockSearchedProjects }],
- [],
- done,
- );
- });
- });
-
- describe('receiveSearchedItemsError', () => {
- it('should set searched items error state', done => {
- testAction(
- actions.receiveSearchedItemsError,
- null,
- mockedState,
- [{ type: types.RECEIVE_SEARCHED_ITEMS_ERROR }],
- [],
- done,
- );
- });
- });
-
- describe('fetchSearchedItems', () => {
- beforeEach(() => {
- gon.api_version = 'v4';
- });
-
- it('should dispatch `receiveSearchedItemsSuccess`', done => {
- mock.onGet(/\/api\/v4\/projects.json(.*)$/).replyOnce(200, mockSearchedProjects, {});
-
- testAction(
- actions.fetchSearchedItems,
- null,
- mockedState,
- [],
- [
- { type: 'requestSearchedItems' },
- {
- type: 'receiveSearchedItemsSuccess',
- payload: { data: mockSearchedProjects, headers: {} },
- },
- ],
- done,
- );
- });
-
- it('should dispatch `receiveSearchedItemsError`', done => {
- gon.api_version = 'v4';
- mock.onGet(/\/api\/v4\/projects.json(.*)$/).replyOnce(500);
-
- testAction(
- actions.fetchSearchedItems,
- null,
- mockedState,
- [],
- [{ type: 'requestSearchedItems' }, { type: 'receiveSearchedItemsError' }],
- done,
- );
- });
- });
-
- describe('setSearchQuery', () => {
- it('should commit query and dispatch `fetchSearchedItems` when query is present', done => {
- testAction(
- actions.setSearchQuery,
- { query: 'test' },
- mockedState,
- [{ type: types.SET_SEARCH_QUERY, payload: { query: 'test' } }],
- [{ type: 'fetchSearchedItems', payload: { query: 'test' } }],
- done,
- );
- });
-
- it('should commit query and dispatch `fetchFrequentItems` when query is empty', done => {
- testAction(
- actions.setSearchQuery,
- null,
- mockedState,
- [{ type: types.SET_SEARCH_QUERY, payload: null }],
- [{ type: 'fetchFrequentItems' }],
- done,
- );
- });
- });
-});
diff --git a/spec/javascripts/frequent_items/utils_spec.js b/spec/javascripts/frequent_items/utils_spec.js
deleted file mode 100644
index 2939b46bc31..00000000000
--- a/spec/javascripts/frequent_items/utils_spec.js
+++ /dev/null
@@ -1,130 +0,0 @@
-import { GlBreakpointInstance as bp } from '@gitlab/ui/dist/utils';
-import {
- isMobile,
- getTopFrequentItems,
- updateExistingFrequentItem,
- sanitizeItem,
-} from '~/frequent_items/utils';
-import { HOUR_IN_MS, FREQUENT_ITEMS } from '~/frequent_items/constants';
-import { mockProject, unsortedFrequentItems, sortedFrequentItems } from './mock_data';
-
-describe('Frequent Items utils spec', () => {
- describe('isMobile', () => {
- it('returns true when the screen is medium ', () => {
- spyOn(bp, 'getBreakpointSize').and.returnValue('md');
-
- expect(isMobile()).toBe(true);
- });
-
- it('returns true when the screen is small ', () => {
- spyOn(bp, 'getBreakpointSize').and.returnValue('sm');
-
- expect(isMobile()).toBe(true);
- });
-
- it('returns true when the screen is extra-small ', () => {
- spyOn(bp, 'getBreakpointSize').and.returnValue('xs');
-
- expect(isMobile()).toBe(true);
- });
-
- it('returns false when the screen is larger than medium ', () => {
- spyOn(bp, 'getBreakpointSize').and.returnValue('lg');
-
- expect(isMobile()).toBe(false);
- });
- });
-
- describe('getTopFrequentItems', () => {
- it('returns empty array if no items provided', () => {
- const result = getTopFrequentItems();
-
- expect(result.length).toBe(0);
- });
-
- it('returns correct amount of items for mobile', () => {
- spyOn(bp, 'getBreakpointSize').and.returnValue('md');
- const result = getTopFrequentItems(unsortedFrequentItems);
-
- expect(result.length).toBe(FREQUENT_ITEMS.LIST_COUNT_MOBILE);
- });
-
- it('returns correct amount of items for desktop', () => {
- spyOn(bp, 'getBreakpointSize').and.returnValue('xl');
- const result = getTopFrequentItems(unsortedFrequentItems);
-
- expect(result.length).toBe(FREQUENT_ITEMS.LIST_COUNT_DESKTOP);
- });
-
- it('sorts frequent items in order of frequency and lastAccessedOn', () => {
- spyOn(bp, 'getBreakpointSize').and.returnValue('xl');
- const result = getTopFrequentItems(unsortedFrequentItems);
- const expectedResult = sortedFrequentItems.slice(0, FREQUENT_ITEMS.LIST_COUNT_DESKTOP);
-
- expect(result).toEqual(expectedResult);
- });
- });
-
- describe('updateExistingFrequentItem', () => {
- let mockedProject;
-
- beforeEach(() => {
- mockedProject = {
- ...mockProject,
- frequency: 1,
- lastAccessedOn: 1497979281815,
- };
- });
-
- it('updates item if accessed over an hour ago', () => {
- const newTimestamp = Date.now() + HOUR_IN_MS + 1;
- const newItem = {
- ...mockedProject,
- lastAccessedOn: newTimestamp,
- };
- const result = updateExistingFrequentItem(mockedProject, newItem);
-
- expect(result.frequency).toBe(mockedProject.frequency + 1);
- });
-
- it('does not update item if accessed within the hour', () => {
- const newItem = {
- ...mockedProject,
- lastAccessedOn: mockedProject.lastAccessedOn + HOUR_IN_MS,
- };
- const result = updateExistingFrequentItem(mockedProject, newItem);
-
- expect(result.frequency).toBe(mockedProject.frequency);
- });
- });
-
- describe('sanitizeItem', () => {
- it('strips HTML tags for name and namespace', () => {
- const input = {
- name: '<br><b>test</b>',
- namespace: '<br>test',
- id: 1,
- };
-
- expect(sanitizeItem(input)).toEqual({ name: 'test', namespace: 'test', id: 1 });
- });
-
- it("skips `name` key if it doesn't exist on the item", () => {
- const input = {
- namespace: '<br>test',
- id: 1,
- };
-
- expect(sanitizeItem(input)).toEqual({ namespace: 'test', id: 1 });
- });
-
- it("skips `namespace` key if it doesn't exist on the item", () => {
- const input = {
- name: '<br><b>test</b>',
- id: 1,
- };
-
- expect(sanitizeItem(input)).toEqual({ name: 'test', id: 1 });
- });
- });
-});
diff --git a/spec/javascripts/gl_dropdown_spec.js b/spec/javascripts/gl_dropdown_spec.js
index 00bc552bd7d..06f76c581f2 100644
--- a/spec/javascripts/gl_dropdown_spec.js
+++ b/spec/javascripts/gl_dropdown_spec.js
@@ -44,19 +44,17 @@ describe('glDropdown', function describeDropdown() {
};
function initDropDown(hasRemote, isFilterable, extraOpts = {}) {
- const options = Object.assign(
- {
- selectable: true,
- filterable: isFilterable,
- data: hasRemote ? remoteMock.bind({}, this.projectsData) : this.projectsData,
- search: {
- fields: ['name'],
- },
- text: project => project.name_with_namespace || project.name,
- id: project => project.id,
+ const options = {
+ selectable: true,
+ filterable: isFilterable,
+ data: hasRemote ? remoteMock.bind({}, this.projectsData) : this.projectsData,
+ search: {
+ fields: ['name'],
},
- extraOpts,
- );
+ text: project => project.name_with_namespace || project.name,
+ id: project => project.id,
+ ...extraOpts,
+ };
this.dropdownButtonElement = $(
'#js-project-dropdown',
this.dropdownContainerElement,
diff --git a/spec/javascripts/groups/components/app_spec.js b/spec/javascripts/groups/components/app_spec.js
deleted file mode 100644
index 23b2564d3f9..00000000000
--- a/spec/javascripts/groups/components/app_spec.js
+++ /dev/null
@@ -1,533 +0,0 @@
-import '~/flash';
-import $ from 'jquery';
-import Vue from 'vue';
-
-import appComponent from '~/groups/components/app.vue';
-import groupFolderComponent from '~/groups/components/group_folder.vue';
-import groupItemComponent from '~/groups/components/group_item.vue';
-import eventHub from '~/groups/event_hub';
-import GroupsStore from '~/groups/store/groups_store';
-import GroupsService from '~/groups/service/groups_service';
-
-import {
- mockEndpoint,
- mockGroups,
- mockSearchedGroups,
- mockRawPageInfo,
- mockParentGroupItem,
- mockRawChildren,
- mockChildren,
- mockPageInfo,
-} from '../mock_data';
-
-const createComponent = (hideProjects = false) => {
- const Component = Vue.extend(appComponent);
- const store = new GroupsStore(false);
- const service = new GroupsService(mockEndpoint);
-
- store.state.pageInfo = mockPageInfo;
-
- return new Component({
- propsData: {
- store,
- service,
- hideProjects,
- },
- });
-};
-
-const returnServicePromise = (data, failed) =>
- new Promise((resolve, reject) => {
- if (failed) {
- reject(data);
- } else {
- resolve({
- json() {
- return data;
- },
- });
- }
- });
-
-describe('AppComponent', () => {
- let vm;
-
- beforeEach(done => {
- Vue.component('group-folder', groupFolderComponent);
- Vue.component('group-item', groupItemComponent);
-
- vm = createComponent();
-
- Vue.nextTick(() => {
- done();
- });
- });
-
- describe('computed', () => {
- beforeEach(() => {
- vm.$mount();
- });
-
- afterEach(() => {
- vm.$destroy();
- });
-
- describe('groups', () => {
- it('should return list of groups from store', () => {
- spyOn(vm.store, 'getGroups');
-
- const { groups } = vm;
-
- expect(vm.store.getGroups).toHaveBeenCalled();
- expect(groups).not.toBeDefined();
- });
- });
-
- describe('pageInfo', () => {
- it('should return pagination info from store', () => {
- spyOn(vm.store, 'getPaginationInfo');
-
- const { pageInfo } = vm;
-
- expect(vm.store.getPaginationInfo).toHaveBeenCalled();
- expect(pageInfo).not.toBeDefined();
- });
- });
- });
-
- describe('methods', () => {
- beforeEach(() => {
- vm.$mount();
- });
-
- afterEach(() => {
- vm.$destroy();
- });
-
- describe('fetchGroups', () => {
- it('should call `getGroups` with all the params provided', done => {
- spyOn(vm.service, 'getGroups').and.returnValue(returnServicePromise(mockGroups));
-
- vm.fetchGroups({
- parentId: 1,
- page: 2,
- filterGroupsBy: 'git',
- sortBy: 'created_desc',
- archived: true,
- });
- setTimeout(() => {
- expect(vm.service.getGroups).toHaveBeenCalledWith(1, 2, 'git', 'created_desc', true);
- done();
- }, 0);
- });
-
- it('should set headers to store for building pagination info when called with `updatePagination`', done => {
- spyOn(vm.service, 'getGroups').and.returnValue(
- returnServicePromise({ headers: mockRawPageInfo }),
- );
- spyOn(vm, 'updatePagination');
-
- vm.fetchGroups({ updatePagination: true });
- setTimeout(() => {
- expect(vm.service.getGroups).toHaveBeenCalled();
- expect(vm.updatePagination).toHaveBeenCalled();
- done();
- }, 0);
- });
-
- it('should show flash error when request fails', done => {
- spyOn(vm.service, 'getGroups').and.returnValue(returnServicePromise(null, true));
- spyOn($, 'scrollTo');
- spyOn(window, 'Flash');
-
- vm.fetchGroups({});
- setTimeout(() => {
- expect(vm.isLoading).toBe(false);
- expect($.scrollTo).toHaveBeenCalledWith(0);
- expect(window.Flash).toHaveBeenCalledWith('An error occurred. Please try again.');
- done();
- }, 0);
- });
- });
-
- describe('fetchAllGroups', () => {
- it('should fetch default set of groups', done => {
- spyOn(vm, 'fetchGroups').and.returnValue(returnServicePromise(mockGroups));
- spyOn(vm, 'updatePagination').and.callThrough();
- spyOn(vm, 'updateGroups').and.callThrough();
-
- vm.fetchAllGroups();
-
- expect(vm.isLoading).toBe(true);
- expect(vm.fetchGroups).toHaveBeenCalled();
- setTimeout(() => {
- expect(vm.isLoading).toBe(false);
- expect(vm.updateGroups).toHaveBeenCalled();
- done();
- }, 0);
- });
-
- it('should fetch matching set of groups when app is loaded with search query', done => {
- spyOn(vm, 'fetchGroups').and.returnValue(returnServicePromise(mockSearchedGroups));
- spyOn(vm, 'updateGroups').and.callThrough();
-
- vm.fetchAllGroups();
-
- expect(vm.fetchGroups).toHaveBeenCalledWith({
- page: null,
- filterGroupsBy: null,
- sortBy: null,
- updatePagination: true,
- archived: null,
- });
- setTimeout(() => {
- expect(vm.updateGroups).toHaveBeenCalled();
- done();
- }, 0);
- });
- });
-
- describe('fetchPage', () => {
- it('should fetch groups for provided page details and update window state', done => {
- spyOn(vm, 'fetchGroups').and.returnValue(returnServicePromise(mockGroups));
- spyOn(vm, 'updateGroups').and.callThrough();
- const mergeUrlParams = spyOnDependency(appComponent, 'mergeUrlParams').and.callThrough();
- spyOn(window.history, 'replaceState');
- spyOn($, 'scrollTo');
-
- vm.fetchPage(2, null, null, true);
-
- expect(vm.isLoading).toBe(true);
- expect(vm.fetchGroups).toHaveBeenCalledWith({
- page: 2,
- filterGroupsBy: null,
- sortBy: null,
- updatePagination: true,
- archived: true,
- });
- setTimeout(() => {
- expect(vm.isLoading).toBe(false);
- expect($.scrollTo).toHaveBeenCalledWith(0);
- expect(mergeUrlParams).toHaveBeenCalledWith({ page: 2 }, jasmine.any(String));
- expect(window.history.replaceState).toHaveBeenCalledWith(
- {
- page: jasmine.any(String),
- },
- jasmine.any(String),
- jasmine.any(String),
- );
-
- expect(vm.updateGroups).toHaveBeenCalled();
- done();
- }, 0);
- });
- });
-
- describe('toggleChildren', () => {
- let groupItem;
-
- beforeEach(() => {
- groupItem = Object.assign({}, mockParentGroupItem);
- groupItem.isOpen = false;
- groupItem.isChildrenLoading = false;
- });
-
- it('should fetch children of given group and expand it if group is collapsed and children are not loaded', done => {
- spyOn(vm, 'fetchGroups').and.returnValue(returnServicePromise(mockRawChildren));
- spyOn(vm.store, 'setGroupChildren');
-
- vm.toggleChildren(groupItem);
-
- expect(groupItem.isChildrenLoading).toBe(true);
- expect(vm.fetchGroups).toHaveBeenCalledWith({
- parentId: groupItem.id,
- });
- setTimeout(() => {
- expect(vm.store.setGroupChildren).toHaveBeenCalled();
- done();
- }, 0);
- });
-
- it('should skip network request while expanding group if children are already loaded', () => {
- spyOn(vm, 'fetchGroups');
- groupItem.children = mockRawChildren;
-
- vm.toggleChildren(groupItem);
-
- expect(vm.fetchGroups).not.toHaveBeenCalled();
- expect(groupItem.isOpen).toBe(true);
- });
-
- it('should collapse group if it is already expanded', () => {
- spyOn(vm, 'fetchGroups');
- groupItem.isOpen = true;
-
- vm.toggleChildren(groupItem);
-
- expect(vm.fetchGroups).not.toHaveBeenCalled();
- expect(groupItem.isOpen).toBe(false);
- });
-
- it('should set `isChildrenLoading` back to `false` if load request fails', done => {
- spyOn(vm, 'fetchGroups').and.returnValue(returnServicePromise({}, true));
-
- vm.toggleChildren(groupItem);
-
- expect(groupItem.isChildrenLoading).toBe(true);
- setTimeout(() => {
- expect(groupItem.isChildrenLoading).toBe(false);
- done();
- }, 0);
- });
- });
-
- describe('showLeaveGroupModal', () => {
- it('caches candidate group (as props) which is to be left', () => {
- const group = Object.assign({}, mockParentGroupItem);
-
- expect(vm.targetGroup).toBe(null);
- expect(vm.targetParentGroup).toBe(null);
- vm.showLeaveGroupModal(group, mockParentGroupItem);
-
- expect(vm.targetGroup).not.toBe(null);
- expect(vm.targetParentGroup).not.toBe(null);
- });
-
- it('updates props which show modal confirmation dialog', () => {
- const group = Object.assign({}, mockParentGroupItem);
-
- expect(vm.showModal).toBe(false);
- expect(vm.groupLeaveConfirmationMessage).toBe('');
- vm.showLeaveGroupModal(group, mockParentGroupItem);
-
- expect(vm.showModal).toBe(true);
- expect(vm.groupLeaveConfirmationMessage).toBe(
- `Are you sure you want to leave the "${group.fullName}" group?`,
- );
- });
- });
-
- describe('hideLeaveGroupModal', () => {
- it('hides modal confirmation which is shown before leaving the group', () => {
- const group = Object.assign({}, mockParentGroupItem);
- vm.showLeaveGroupModal(group, mockParentGroupItem);
-
- expect(vm.showModal).toBe(true);
- vm.hideLeaveGroupModal();
-
- expect(vm.showModal).toBe(false);
- });
- });
-
- describe('leaveGroup', () => {
- let groupItem;
- let childGroupItem;
-
- beforeEach(() => {
- groupItem = Object.assign({}, mockParentGroupItem);
- groupItem.children = mockChildren;
- [childGroupItem] = groupItem.children;
- groupItem.isChildrenLoading = false;
- vm.targetGroup = childGroupItem;
- vm.targetParentGroup = groupItem;
- });
-
- it('hides modal confirmation leave group and remove group item from tree', done => {
- const notice = `You left the "${childGroupItem.fullName}" group.`;
- spyOn(vm.service, 'leaveGroup').and.returnValue(Promise.resolve({ data: { notice } }));
- spyOn(vm.store, 'removeGroup').and.callThrough();
- spyOn(window, 'Flash');
- spyOn($, 'scrollTo');
-
- vm.leaveGroup();
-
- expect(vm.showModal).toBe(false);
- expect(vm.targetGroup.isBeingRemoved).toBe(true);
- expect(vm.service.leaveGroup).toHaveBeenCalledWith(vm.targetGroup.leavePath);
- setTimeout(() => {
- expect($.scrollTo).toHaveBeenCalledWith(0);
- expect(vm.store.removeGroup).toHaveBeenCalledWith(vm.targetGroup, vm.targetParentGroup);
- expect(window.Flash).toHaveBeenCalledWith(notice, 'notice');
- done();
- }, 0);
- });
-
- it('should show error flash message if request failed to leave group', done => {
- const message = 'An error occurred. Please try again.';
- spyOn(vm.service, 'leaveGroup').and.returnValue(
- returnServicePromise({ status: 500 }, true),
- );
- spyOn(vm.store, 'removeGroup').and.callThrough();
- spyOn(window, 'Flash');
-
- vm.leaveGroup();
-
- expect(vm.targetGroup.isBeingRemoved).toBe(true);
- expect(vm.service.leaveGroup).toHaveBeenCalledWith(childGroupItem.leavePath);
- setTimeout(() => {
- expect(vm.store.removeGroup).not.toHaveBeenCalled();
- expect(window.Flash).toHaveBeenCalledWith(message);
- expect(vm.targetGroup.isBeingRemoved).toBe(false);
- done();
- }, 0);
- });
-
- it('should show appropriate error flash message if request forbids to leave group', done => {
- const message = 'Failed to leave the group. Please make sure you are not the only owner.';
- spyOn(vm.service, 'leaveGroup').and.returnValue(
- returnServicePromise({ status: 403 }, true),
- );
- spyOn(vm.store, 'removeGroup').and.callThrough();
- spyOn(window, 'Flash');
-
- vm.leaveGroup(childGroupItem, groupItem);
-
- expect(vm.targetGroup.isBeingRemoved).toBe(true);
- expect(vm.service.leaveGroup).toHaveBeenCalledWith(childGroupItem.leavePath);
- setTimeout(() => {
- expect(vm.store.removeGroup).not.toHaveBeenCalled();
- expect(window.Flash).toHaveBeenCalledWith(message);
- expect(vm.targetGroup.isBeingRemoved).toBe(false);
- done();
- }, 0);
- });
- });
-
- describe('updatePagination', () => {
- it('should set pagination info to store from provided headers', () => {
- spyOn(vm.store, 'setPaginationInfo');
-
- vm.updatePagination(mockRawPageInfo);
-
- expect(vm.store.setPaginationInfo).toHaveBeenCalledWith(mockRawPageInfo);
- });
- });
-
- describe('updateGroups', () => {
- it('should call setGroups on store if method was called directly', () => {
- spyOn(vm.store, 'setGroups');
-
- vm.updateGroups(mockGroups);
-
- expect(vm.store.setGroups).toHaveBeenCalledWith(mockGroups);
- });
-
- it('should call setSearchedGroups on store if method was called with fromSearch param', () => {
- spyOn(vm.store, 'setSearchedGroups');
-
- vm.updateGroups(mockGroups, true);
-
- expect(vm.store.setSearchedGroups).toHaveBeenCalledWith(mockGroups);
- });
-
- it('should set `isSearchEmpty` prop based on groups count', () => {
- vm.updateGroups(mockGroups);
-
- expect(vm.isSearchEmpty).toBe(false);
-
- vm.updateGroups([]);
-
- expect(vm.isSearchEmpty).toBe(true);
- });
- });
- });
-
- describe('created', () => {
- it('should bind event listeners on eventHub', done => {
- spyOn(eventHub, '$on');
-
- const newVm = createComponent();
- newVm.$mount();
-
- Vue.nextTick(() => {
- expect(eventHub.$on).toHaveBeenCalledWith('fetchPage', jasmine.any(Function));
- expect(eventHub.$on).toHaveBeenCalledWith('toggleChildren', jasmine.any(Function));
- expect(eventHub.$on).toHaveBeenCalledWith('showLeaveGroupModal', jasmine.any(Function));
- expect(eventHub.$on).toHaveBeenCalledWith('updatePagination', jasmine.any(Function));
- expect(eventHub.$on).toHaveBeenCalledWith('updateGroups', jasmine.any(Function));
- newVm.$destroy();
- done();
- });
- });
-
- it('should initialize `searchEmptyMessage` prop with correct string when `hideProjects` is `false`', done => {
- const newVm = createComponent();
- newVm.$mount();
- Vue.nextTick(() => {
- expect(newVm.searchEmptyMessage).toBe('No groups or projects matched your search');
- newVm.$destroy();
- done();
- });
- });
-
- it('should initialize `searchEmptyMessage` prop with correct string when `hideProjects` is `true`', done => {
- const newVm = createComponent(true);
- newVm.$mount();
- Vue.nextTick(() => {
- expect(newVm.searchEmptyMessage).toBe('No groups matched your search');
- newVm.$destroy();
- done();
- });
- });
- });
-
- describe('beforeDestroy', () => {
- it('should unbind event listeners on eventHub', done => {
- spyOn(eventHub, '$off');
-
- const newVm = createComponent();
- newVm.$mount();
- newVm.$destroy();
-
- Vue.nextTick(() => {
- expect(eventHub.$off).toHaveBeenCalledWith('fetchPage', jasmine.any(Function));
- expect(eventHub.$off).toHaveBeenCalledWith('toggleChildren', jasmine.any(Function));
- expect(eventHub.$off).toHaveBeenCalledWith('showLeaveGroupModal', jasmine.any(Function));
- expect(eventHub.$off).toHaveBeenCalledWith('updatePagination', jasmine.any(Function));
- expect(eventHub.$off).toHaveBeenCalledWith('updateGroups', jasmine.any(Function));
- done();
- });
- });
- });
-
- describe('template', () => {
- beforeEach(() => {
- vm.$mount();
- });
-
- afterEach(() => {
- vm.$destroy();
- });
-
- it('should render loading icon', done => {
- vm.isLoading = true;
- Vue.nextTick(() => {
- expect(vm.$el.querySelector('.loading-animation')).toBeDefined();
- expect(vm.$el.querySelector('span').getAttribute('aria-label')).toBe('Loading groups');
- done();
- });
- });
-
- it('should render groups tree', done => {
- vm.store.state.groups = [mockParentGroupItem];
- vm.isLoading = false;
- Vue.nextTick(() => {
- expect(vm.$el.querySelector('.groups-list-tree-container')).toBeDefined();
- done();
- });
- });
-
- it('renders modal confirmation dialog', done => {
- vm.groupLeaveConfirmationMessage = 'Are you sure you want to leave the "foo" group?';
- vm.showModal = true;
- Vue.nextTick(() => {
- const modalDialogEl = vm.$el.querySelector('.modal');
-
- expect(modalDialogEl).not.toBe(null);
- expect(modalDialogEl.querySelector('.modal-title').innerText.trim()).toBe('Are you sure?');
- expect(modalDialogEl.querySelector('.btn.btn-warning').innerText.trim()).toBe('Leave');
- done();
- });
- });
- });
-});
diff --git a/spec/javascripts/groups/components/group_folder_spec.js b/spec/javascripts/groups/components/group_folder_spec.js
deleted file mode 100644
index fdfd1b82bd8..00000000000
--- a/spec/javascripts/groups/components/group_folder_spec.js
+++ /dev/null
@@ -1,67 +0,0 @@
-import Vue from 'vue';
-
-import groupFolderComponent from '~/groups/components/group_folder.vue';
-import groupItemComponent from '~/groups/components/group_item.vue';
-import { mockGroups, mockParentGroupItem } from '../mock_data';
-
-const createComponent = (groups = mockGroups, parentGroup = mockParentGroupItem) => {
- const Component = Vue.extend(groupFolderComponent);
-
- return new Component({
- propsData: {
- groups,
- parentGroup,
- },
- });
-};
-
-describe('GroupFolderComponent', () => {
- let vm;
-
- beforeEach(done => {
- Vue.component('group-item', groupItemComponent);
-
- vm = createComponent();
- vm.$mount();
-
- Vue.nextTick(() => {
- done();
- });
- });
-
- afterEach(() => {
- vm.$destroy();
- });
-
- describe('computed', () => {
- describe('hasMoreChildren', () => {
- it('should return false when childrenCount of group is less than MAX_CHILDREN_COUNT', () => {
- expect(vm.hasMoreChildren).toBeFalsy();
- });
- });
-
- describe('moreChildrenStats', () => {
- it('should return message with count of excess children over MAX_CHILDREN_COUNT limit', () => {
- expect(vm.moreChildrenStats).toBe('3 more items');
- });
- });
- });
-
- describe('template', () => {
- it('should render component template correctly', () => {
- expect(vm.$el.classList.contains('group-list-tree')).toBeTruthy();
- expect(vm.$el.querySelectorAll('li.group-row').length).toBe(7);
- });
-
- it('should render more children link when groups list has children over MAX_CHILDREN_COUNT limit', () => {
- const parentGroup = Object.assign({}, mockParentGroupItem);
- parentGroup.childrenCount = 21;
-
- const newVm = createComponent(mockGroups, parentGroup);
- newVm.$mount();
-
- expect(newVm.$el.querySelector('li.group-row a.has-more-items')).toBeDefined();
- newVm.$destroy();
- });
- });
-});
diff --git a/spec/javascripts/groups/components/group_item_spec.js b/spec/javascripts/groups/components/group_item_spec.js
deleted file mode 100644
index 2889d7ae4ff..00000000000
--- a/spec/javascripts/groups/components/group_item_spec.js
+++ /dev/null
@@ -1,218 +0,0 @@
-import Vue from 'vue';
-import mountComponent from 'spec/helpers/vue_mount_component_helper';
-import groupItemComponent from '~/groups/components/group_item.vue';
-import groupFolderComponent from '~/groups/components/group_folder.vue';
-import eventHub from '~/groups/event_hub';
-import { mockParentGroupItem, mockChildren } from '../mock_data';
-
-const createComponent = (group = mockParentGroupItem, parentGroup = mockChildren[0]) => {
- const Component = Vue.extend(groupItemComponent);
-
- return mountComponent(Component, {
- group,
- parentGroup,
- });
-};
-
-describe('GroupItemComponent', () => {
- let vm;
-
- beforeEach(done => {
- Vue.component('group-folder', groupFolderComponent);
-
- vm = createComponent();
-
- Vue.nextTick(() => {
- done();
- });
- });
-
- afterEach(() => {
- vm.$destroy();
- });
-
- describe('computed', () => {
- describe('groupDomId', () => {
- it('should return ID string suffixed with group ID', () => {
- expect(vm.groupDomId).toBe('group-55');
- });
- });
-
- describe('rowClass', () => {
- it('should return map of classes based on group details', () => {
- const classes = ['is-open', 'has-children', 'has-description', 'being-removed'];
- const { rowClass } = vm;
-
- expect(Object.keys(rowClass).length).toBe(classes.length);
- Object.keys(rowClass).forEach(className => {
- expect(classes.indexOf(className)).toBeGreaterThan(-1);
- });
- });
- });
-
- describe('hasChildren', () => {
- it('should return boolean value representing if group has any children present', () => {
- let newVm;
- const group = Object.assign({}, mockParentGroupItem);
-
- group.childrenCount = 5;
- newVm = createComponent(group);
-
- expect(newVm.hasChildren).toBeTruthy();
- newVm.$destroy();
-
- group.childrenCount = 0;
- newVm = createComponent(group);
-
- expect(newVm.hasChildren).toBeFalsy();
- newVm.$destroy();
- });
- });
-
- describe('hasAvatar', () => {
- it('should return boolean value representing if group has any avatar present', () => {
- let newVm;
- const group = Object.assign({}, mockParentGroupItem);
-
- group.avatarUrl = null;
- newVm = createComponent(group);
-
- expect(newVm.hasAvatar).toBeFalsy();
- newVm.$destroy();
-
- group.avatarUrl = '/uploads/group_avatar.png';
- newVm = createComponent(group);
-
- expect(newVm.hasAvatar).toBeTruthy();
- newVm.$destroy();
- });
- });
-
- describe('isGroup', () => {
- it('should return boolean value representing if group item is of type `group` or not', () => {
- let newVm;
- const group = Object.assign({}, mockParentGroupItem);
-
- group.type = 'group';
- newVm = createComponent(group);
-
- expect(newVm.isGroup).toBeTruthy();
- newVm.$destroy();
-
- group.type = 'project';
- newVm = createComponent(group);
-
- expect(newVm.isGroup).toBeFalsy();
- newVm.$destroy();
- });
- });
- });
-
- describe('methods', () => {
- describe('onClickRowGroup', () => {
- let event;
-
- beforeEach(() => {
- const classList = {
- contains() {
- return false;
- },
- };
-
- event = {
- target: {
- classList,
- parentElement: {
- classList,
- },
- },
- };
- });
-
- it('should emit `toggleChildren` event when expand is clicked on a group and it has children present', () => {
- spyOn(eventHub, '$emit');
-
- vm.onClickRowGroup(event);
-
- expect(eventHub.$emit).toHaveBeenCalledWith('toggleChildren', vm.group);
- });
-
- it('should navigate page to group homepage if group does not have any children present', done => {
- const group = Object.assign({}, mockParentGroupItem);
- group.childrenCount = 0;
- const newVm = createComponent(group);
- const visitUrl = spyOnDependency(groupItemComponent, 'visitUrl').and.stub();
- spyOn(eventHub, '$emit');
-
- newVm.onClickRowGroup(event);
- setTimeout(() => {
- expect(eventHub.$emit).not.toHaveBeenCalled();
- expect(visitUrl).toHaveBeenCalledWith(newVm.group.relativePath);
- done();
- }, 0);
- });
- });
- });
-
- describe('template', () => {
- let group = null;
-
- describe('for a group pending deletion', () => {
- beforeEach(() => {
- group = { ...mockParentGroupItem, pendingRemoval: true };
- vm = createComponent(group);
- });
-
- it('renders the group pending removal badge', () => {
- const badgeEl = vm.$el.querySelector('.badge-warning');
-
- expect(badgeEl).toBeDefined();
- expect(badgeEl).toContainText('pending removal');
- });
- });
-
- describe('for a group not scheduled for deletion', () => {
- beforeEach(() => {
- group = { ...mockParentGroupItem, pendingRemoval: false };
- vm = createComponent(group);
- });
-
- it('does not render the group pending removal badge', () => {
- const groupTextContainer = vm.$el.querySelector('.group-text-container');
-
- expect(groupTextContainer).not.toContainText('pending removal');
- });
- });
-
- it('should render component template correctly', () => {
- const visibilityIconEl = vm.$el.querySelector('.item-visibility');
-
- expect(vm.$el.getAttribute('id')).toBe('group-55');
- expect(vm.$el.classList.contains('group-row')).toBeTruthy();
-
- expect(vm.$el.querySelector('.group-row-contents')).toBeDefined();
- expect(vm.$el.querySelector('.group-row-contents .controls')).toBeDefined();
- expect(vm.$el.querySelector('.group-row-contents .stats')).toBeDefined();
-
- expect(vm.$el.querySelector('.folder-toggle-wrap')).toBeDefined();
- expect(vm.$el.querySelector('.folder-toggle-wrap .folder-caret')).toBeDefined();
- expect(vm.$el.querySelector('.folder-toggle-wrap .item-type-icon')).toBeDefined();
-
- expect(vm.$el.querySelector('.avatar-container')).toBeDefined();
- expect(vm.$el.querySelector('.avatar-container a.no-expand')).toBeDefined();
- expect(vm.$el.querySelector('.avatar-container .avatar')).toBeDefined();
-
- expect(vm.$el.querySelector('.title')).toBeDefined();
- expect(vm.$el.querySelector('.title a.no-expand')).toBeDefined();
-
- expect(visibilityIconEl).not.toBe(null);
- expect(visibilityIconEl.dataset.originalTitle).toBe(vm.visibilityTooltip);
- expect(visibilityIconEl.querySelectorAll('svg').length).toBeGreaterThan(0);
-
- expect(vm.$el.querySelector('.access-type')).toBeDefined();
- expect(vm.$el.querySelector('.description')).toBeDefined();
-
- expect(vm.$el.querySelector('.group-list-tree')).toBeDefined();
- });
- });
-});
diff --git a/spec/javascripts/groups/components/groups_spec.js b/spec/javascripts/groups/components/groups_spec.js
deleted file mode 100644
index 8423467742e..00000000000
--- a/spec/javascripts/groups/components/groups_spec.js
+++ /dev/null
@@ -1,76 +0,0 @@
-import Vue from 'vue';
-
-import mountComponent from 'spec/helpers/vue_mount_component_helper';
-import groupsComponent from '~/groups/components/groups.vue';
-import groupFolderComponent from '~/groups/components/group_folder.vue';
-import groupItemComponent from '~/groups/components/group_item.vue';
-import eventHub from '~/groups/event_hub';
-import { mockGroups, mockPageInfo } from '../mock_data';
-
-const createComponent = (searchEmpty = false) => {
- const Component = Vue.extend(groupsComponent);
-
- return mountComponent(Component, {
- groups: mockGroups,
- pageInfo: mockPageInfo,
- searchEmptyMessage: 'No matching results',
- searchEmpty,
- });
-};
-
-describe('GroupsComponent', () => {
- let vm;
-
- beforeEach(done => {
- Vue.component('group-folder', groupFolderComponent);
- Vue.component('group-item', groupItemComponent);
-
- vm = createComponent();
-
- Vue.nextTick(() => {
- done();
- });
- });
-
- afterEach(() => {
- vm.$destroy();
- });
-
- describe('methods', () => {
- describe('change', () => {
- it('should emit `fetchPage` event when page is changed via pagination', () => {
- spyOn(eventHub, '$emit').and.stub();
-
- vm.change(2);
-
- expect(eventHub.$emit).toHaveBeenCalledWith(
- 'fetchPage',
- 2,
- jasmine.any(Object),
- jasmine.any(Object),
- jasmine.any(Object),
- );
- });
- });
- });
-
- describe('template', () => {
- it('should render component template correctly', done => {
- Vue.nextTick(() => {
- expect(vm.$el.querySelector('.groups-list-tree-container')).toBeDefined();
- expect(vm.$el.querySelector('.group-list-tree')).toBeDefined();
- expect(vm.$el.querySelector('.gl-pagination')).toBeDefined();
- expect(vm.$el.querySelectorAll('.has-no-search-results').length).toBe(0);
- done();
- });
- });
-
- it('should render empty search message when `searchEmpty` is `true`', done => {
- vm.searchEmpty = true;
- Vue.nextTick(() => {
- expect(vm.$el.querySelector('.has-no-search-results')).toBeDefined();
- done();
- });
- });
- });
-});
diff --git a/spec/javascripts/groups/components/item_actions_spec.js b/spec/javascripts/groups/components/item_actions_spec.js
deleted file mode 100644
index 9a9d6208eac..00000000000
--- a/spec/javascripts/groups/components/item_actions_spec.js
+++ /dev/null
@@ -1,84 +0,0 @@
-import Vue from 'vue';
-
-import mountComponent from 'spec/helpers/vue_mount_component_helper';
-import itemActionsComponent from '~/groups/components/item_actions.vue';
-import eventHub from '~/groups/event_hub';
-import { mockParentGroupItem, mockChildren } from '../mock_data';
-
-const createComponent = (group = mockParentGroupItem, parentGroup = mockChildren[0]) => {
- const Component = Vue.extend(itemActionsComponent);
-
- return mountComponent(Component, {
- group,
- parentGroup,
- });
-};
-
-describe('ItemActionsComponent', () => {
- let vm;
-
- beforeEach(() => {
- vm = createComponent();
- });
-
- afterEach(() => {
- vm.$destroy();
- });
-
- describe('methods', () => {
- describe('onLeaveGroup', () => {
- it('emits `showLeaveGroupModal` event with `group` and `parentGroup` props', () => {
- spyOn(eventHub, '$emit');
- vm.onLeaveGroup();
-
- expect(eventHub.$emit).toHaveBeenCalledWith(
- 'showLeaveGroupModal',
- vm.group,
- vm.parentGroup,
- );
- });
- });
- });
-
- describe('template', () => {
- it('should render component template correctly', () => {
- expect(vm.$el.classList.contains('controls')).toBeTruthy();
- });
-
- it('should render Edit Group button with correct attribute values', () => {
- const group = Object.assign({}, mockParentGroupItem);
- group.canEdit = true;
- const newVm = createComponent(group);
-
- const editBtn = newVm.$el.querySelector('a.edit-group');
-
- expect(editBtn).toBeDefined();
- expect(editBtn.classList.contains('no-expand')).toBeTruthy();
- expect(editBtn.getAttribute('href')).toBe(group.editPath);
- expect(editBtn.getAttribute('aria-label')).toBe('Edit group');
- expect(editBtn.dataset.originalTitle).toBe('Edit group');
- expect(editBtn.querySelectorAll('svg use').length).not.toBe(0);
- expect(editBtn.querySelector('svg use').getAttribute('xlink:href')).toContain('#settings');
-
- newVm.$destroy();
- });
-
- it('should render Leave Group button with correct attribute values', () => {
- const group = Object.assign({}, mockParentGroupItem);
- group.canLeave = true;
- const newVm = createComponent(group);
-
- const leaveBtn = newVm.$el.querySelector('a.leave-group');
-
- expect(leaveBtn).toBeDefined();
- expect(leaveBtn.classList.contains('no-expand')).toBeTruthy();
- expect(leaveBtn.getAttribute('href')).toBe(group.leavePath);
- expect(leaveBtn.getAttribute('aria-label')).toBe('Leave this group');
- expect(leaveBtn.dataset.originalTitle).toBe('Leave this group');
- expect(leaveBtn.querySelectorAll('svg use').length).not.toBe(0);
- expect(leaveBtn.querySelector('svg use').getAttribute('xlink:href')).toContain('#leave');
-
- newVm.$destroy();
- });
- });
-});
diff --git a/spec/javascripts/groups/components/item_caret_spec.js b/spec/javascripts/groups/components/item_caret_spec.js
deleted file mode 100644
index 0eb56abbd61..00000000000
--- a/spec/javascripts/groups/components/item_caret_spec.js
+++ /dev/null
@@ -1,38 +0,0 @@
-import Vue from 'vue';
-
-import mountComponent from 'spec/helpers/vue_mount_component_helper';
-import itemCaretComponent from '~/groups/components/item_caret.vue';
-
-const createComponent = (isGroupOpen = false) => {
- const Component = Vue.extend(itemCaretComponent);
-
- return mountComponent(Component, {
- isGroupOpen,
- });
-};
-
-describe('ItemCaretComponent', () => {
- describe('template', () => {
- it('should render component template correctly', () => {
- const vm = createComponent();
-
- expect(vm.$el.classList.contains('folder-caret')).toBeTruthy();
- expect(vm.$el.querySelectorAll('svg').length).toBe(1);
- vm.$destroy();
- });
-
- it('should render caret down icon if `isGroupOpen` prop is `true`', () => {
- const vm = createComponent(true);
-
- expect(vm.$el.querySelector('svg use').getAttribute('xlink:href')).toContain('angle-down');
- vm.$destroy();
- });
-
- it('should render caret right icon if `isGroupOpen` prop is `false`', () => {
- const vm = createComponent();
-
- expect(vm.$el.querySelector('svg use').getAttribute('xlink:href')).toContain('angle-right');
- vm.$destroy();
- });
- });
-});
diff --git a/spec/javascripts/groups/components/item_stats_spec.js b/spec/javascripts/groups/components/item_stats_spec.js
deleted file mode 100644
index 13d17b87d76..00000000000
--- a/spec/javascripts/groups/components/item_stats_spec.js
+++ /dev/null
@@ -1,128 +0,0 @@
-import Vue from 'vue';
-
-import mountComponent from 'spec/helpers/vue_mount_component_helper';
-import itemStatsComponent from '~/groups/components/item_stats.vue';
-import {
- mockParentGroupItem,
- ITEM_TYPE,
- VISIBILITY_TYPE_ICON,
- GROUP_VISIBILITY_TYPE,
- PROJECT_VISIBILITY_TYPE,
-} from '../mock_data';
-
-const createComponent = (item = mockParentGroupItem) => {
- const Component = Vue.extend(itemStatsComponent);
-
- return mountComponent(Component, {
- item,
- });
-};
-
-describe('ItemStatsComponent', () => {
- describe('computed', () => {
- describe('visibilityIcon', () => {
- it('should return icon class based on `item.visibility` value', () => {
- Object.keys(VISIBILITY_TYPE_ICON).forEach(visibility => {
- const item = Object.assign({}, mockParentGroupItem, { visibility });
- const vm = createComponent(item);
-
- expect(vm.visibilityIcon).toBe(VISIBILITY_TYPE_ICON[visibility]);
- vm.$destroy();
- });
- });
- });
-
- describe('visibilityTooltip', () => {
- it('should return tooltip string for Group based on `item.visibility` value', () => {
- Object.keys(GROUP_VISIBILITY_TYPE).forEach(visibility => {
- const item = Object.assign({}, mockParentGroupItem, {
- visibility,
- type: ITEM_TYPE.GROUP,
- });
- const vm = createComponent(item);
-
- expect(vm.visibilityTooltip).toBe(GROUP_VISIBILITY_TYPE[visibility]);
- vm.$destroy();
- });
- });
-
- it('should return tooltip string for Project based on `item.visibility` value', () => {
- Object.keys(PROJECT_VISIBILITY_TYPE).forEach(visibility => {
- const item = Object.assign({}, mockParentGroupItem, {
- visibility,
- type: ITEM_TYPE.PROJECT,
- });
- const vm = createComponent(item);
-
- expect(vm.visibilityTooltip).toBe(PROJECT_VISIBILITY_TYPE[visibility]);
- vm.$destroy();
- });
- });
- });
-
- describe('isProject', () => {
- it('should return boolean value representing whether `item.type` is Project or not', () => {
- let item;
- let vm;
-
- item = Object.assign({}, mockParentGroupItem, { type: ITEM_TYPE.PROJECT });
- vm = createComponent(item);
-
- expect(vm.isProject).toBeTruthy();
- vm.$destroy();
-
- item = Object.assign({}, mockParentGroupItem, { type: ITEM_TYPE.GROUP });
- vm = createComponent(item);
-
- expect(vm.isProject).toBeFalsy();
- vm.$destroy();
- });
- });
-
- describe('isGroup', () => {
- it('should return boolean value representing whether `item.type` is Group or not', () => {
- let item;
- let vm;
-
- item = Object.assign({}, mockParentGroupItem, { type: ITEM_TYPE.GROUP });
- vm = createComponent(item);
-
- expect(vm.isGroup).toBeTruthy();
- vm.$destroy();
-
- item = Object.assign({}, mockParentGroupItem, { type: ITEM_TYPE.PROJECT });
- vm = createComponent(item);
-
- expect(vm.isGroup).toBeFalsy();
- vm.$destroy();
- });
- });
- });
-
- describe('template', () => {
- it('renders component container element correctly', () => {
- const vm = createComponent();
-
- expect(vm.$el.classList.contains('stats')).toBeTruthy();
-
- vm.$destroy();
- });
-
- it('renders start count and last updated information for project item correctly', () => {
- const item = Object.assign({}, mockParentGroupItem, {
- type: ITEM_TYPE.PROJECT,
- starCount: 4,
- });
- const vm = createComponent(item);
-
- const projectStarIconEl = vm.$el.querySelector('.project-stars');
-
- expect(projectStarIconEl).not.toBeNull();
- expect(projectStarIconEl.querySelectorAll('svg').length).toBeGreaterThan(0);
- expect(projectStarIconEl.querySelectorAll('.stat-value').length).toBeGreaterThan(0);
- expect(vm.$el.querySelectorAll('.last-updated').length).toBeGreaterThan(0);
-
- vm.$destroy();
- });
- });
-});
diff --git a/spec/javascripts/groups/components/item_stats_value_spec.js b/spec/javascripts/groups/components/item_stats_value_spec.js
deleted file mode 100644
index ff4e781ce1a..00000000000
--- a/spec/javascripts/groups/components/item_stats_value_spec.js
+++ /dev/null
@@ -1,82 +0,0 @@
-import Vue from 'vue';
-
-import mountComponent from 'spec/helpers/vue_mount_component_helper';
-import itemStatsValueComponent from '~/groups/components/item_stats_value.vue';
-
-const createComponent = ({ title, cssClass, iconName, tooltipPlacement, value }) => {
- const Component = Vue.extend(itemStatsValueComponent);
-
- return mountComponent(Component, {
- title,
- cssClass,
- iconName,
- tooltipPlacement,
- value,
- });
-};
-
-describe('ItemStatsValueComponent', () => {
- describe('computed', () => {
- let vm;
- const itemConfig = {
- title: 'Subgroups',
- cssClass: 'number-subgroups',
- iconName: 'folder',
- tooltipPlacement: 'left',
- };
-
- describe('isValuePresent', () => {
- it('returns true if non-empty `value` is present', () => {
- vm = createComponent(Object.assign({}, itemConfig, { value: 10 }));
-
- expect(vm.isValuePresent).toBeTruthy();
- });
-
- it('returns false if empty `value` is present', () => {
- vm = createComponent(itemConfig);
-
- expect(vm.isValuePresent).toBeFalsy();
- });
-
- afterEach(() => {
- vm.$destroy();
- });
- });
- });
-
- describe('template', () => {
- let vm;
- beforeEach(() => {
- vm = createComponent({
- title: 'Subgroups',
- cssClass: 'number-subgroups',
- iconName: 'folder',
- tooltipPlacement: 'left',
- value: 10,
- });
- });
-
- it('renders component element correctly', () => {
- expect(vm.$el.classList.contains('number-subgroups')).toBeTruthy();
- expect(vm.$el.querySelectorAll('svg').length).toBeGreaterThan(0);
- expect(vm.$el.querySelectorAll('.stat-value').length).toBeGreaterThan(0);
- });
-
- it('renders element tooltip correctly', () => {
- expect(vm.$el.dataset.originalTitle).toBe('Subgroups');
- expect(vm.$el.dataset.placement).toBe('left');
- });
-
- it('renders element icon correctly', () => {
- expect(vm.$el.querySelector('svg use').getAttribute('xlink:href')).toContain('folder');
- });
-
- it('renders value count correctly', () => {
- expect(vm.$el.querySelector('.stat-value').innerText.trim()).toContain('10');
- });
-
- afterEach(() => {
- vm.$destroy();
- });
- });
-});
diff --git a/spec/javascripts/groups/components/item_type_icon_spec.js b/spec/javascripts/groups/components/item_type_icon_spec.js
deleted file mode 100644
index 321712e54a6..00000000000
--- a/spec/javascripts/groups/components/item_type_icon_spec.js
+++ /dev/null
@@ -1,58 +0,0 @@
-import Vue from 'vue';
-
-import mountComponent from 'spec/helpers/vue_mount_component_helper';
-import itemTypeIconComponent from '~/groups/components/item_type_icon.vue';
-import { ITEM_TYPE } from '../mock_data';
-
-const createComponent = (itemType = ITEM_TYPE.GROUP, isGroupOpen = false) => {
- const Component = Vue.extend(itemTypeIconComponent);
-
- return mountComponent(Component, {
- itemType,
- isGroupOpen,
- });
-};
-
-describe('ItemTypeIconComponent', () => {
- describe('template', () => {
- it('should render component template correctly', () => {
- const vm = createComponent();
- vm.$mount();
-
- expect(vm.$el.classList.contains('item-type-icon')).toBeTruthy();
- vm.$destroy();
- });
-
- it('should render folder open or close icon based `isGroupOpen` prop value', () => {
- let vm;
-
- vm = createComponent(ITEM_TYPE.GROUP, true);
- vm.$mount();
-
- expect(vm.$el.querySelector('use').getAttribute('xlink:href')).toContain('folder-open');
- vm.$destroy();
-
- vm = createComponent(ITEM_TYPE.GROUP);
- vm.$mount();
-
- expect(vm.$el.querySelector('use').getAttribute('xlink:href')).toContain('folder');
- vm.$destroy();
- });
-
- it('should render bookmark icon based on `isProject` prop value', () => {
- let vm;
-
- vm = createComponent(ITEM_TYPE.PROJECT);
- vm.$mount();
-
- expect(vm.$el.querySelector('use').getAttribute('xlink:href')).toContain('bookmark');
- vm.$destroy();
-
- vm = createComponent(ITEM_TYPE.GROUP);
- vm.$mount();
-
- expect(vm.$el.querySelector('use').getAttribute('xlink:href')).not.toContain('bookmark');
- vm.$destroy();
- });
- });
-});
diff --git a/spec/javascripts/groups/service/groups_service_spec.js b/spec/javascripts/groups/service/groups_service_spec.js
deleted file mode 100644
index 45db962a1ef..00000000000
--- a/spec/javascripts/groups/service/groups_service_spec.js
+++ /dev/null
@@ -1,42 +0,0 @@
-import axios from '~/lib/utils/axios_utils';
-
-import GroupsService from '~/groups/service/groups_service';
-import { mockEndpoint, mockParentGroupItem } from '../mock_data';
-
-describe('GroupsService', () => {
- let service;
-
- beforeEach(() => {
- service = new GroupsService(mockEndpoint);
- });
-
- describe('getGroups', () => {
- it('should return promise for `GET` request on provided endpoint', () => {
- spyOn(axios, 'get').and.stub();
- const params = {
- page: 2,
- filter: 'git',
- sort: 'created_asc',
- archived: true,
- };
-
- service.getGroups(55, 2, 'git', 'created_asc', true);
-
- expect(axios.get).toHaveBeenCalledWith(mockEndpoint, { params: { parent_id: 55 } });
-
- service.getGroups(null, 2, 'git', 'created_asc', true);
-
- expect(axios.get).toHaveBeenCalledWith(mockEndpoint, { params });
- });
- });
-
- describe('leaveGroup', () => {
- it('should return promise for `DELETE` request on provided endpoint', () => {
- spyOn(axios, 'delete').and.stub();
-
- service.leaveGroup(mockParentGroupItem.leavePath);
-
- expect(axios.delete).toHaveBeenCalledWith(mockParentGroupItem.leavePath);
- });
- });
-});
diff --git a/spec/javascripts/groups/store/groups_store_spec.js b/spec/javascripts/groups/store/groups_store_spec.js
deleted file mode 100644
index 38de4b89f31..00000000000
--- a/spec/javascripts/groups/store/groups_store_spec.js
+++ /dev/null
@@ -1,123 +0,0 @@
-import GroupsStore from '~/groups/store/groups_store';
-import {
- mockGroups,
- mockSearchedGroups,
- mockParentGroupItem,
- mockRawChildren,
- mockRawPageInfo,
-} from '../mock_data';
-
-describe('ProjectsStore', () => {
- describe('constructor', () => {
- it('should initialize default state', () => {
- let store;
-
- store = new GroupsStore();
-
- expect(Object.keys(store.state).length).toBe(2);
- expect(Array.isArray(store.state.groups)).toBeTruthy();
- expect(Object.keys(store.state.pageInfo).length).toBe(0);
- expect(store.hideProjects).not.toBeDefined();
-
- store = new GroupsStore(true);
-
- expect(store.hideProjects).toBeTruthy();
- });
- });
-
- describe('setGroups', () => {
- it('should set groups to state', () => {
- const store = new GroupsStore();
- spyOn(store, 'formatGroupItem').and.callThrough();
-
- store.setGroups(mockGroups);
-
- expect(store.state.groups.length).toBe(mockGroups.length);
- expect(store.formatGroupItem).toHaveBeenCalledWith(jasmine.any(Object));
- expect(Object.keys(store.state.groups[0]).indexOf('fullName')).toBeGreaterThan(-1);
- });
- });
-
- describe('setSearchedGroups', () => {
- it('should set searched groups to state', () => {
- const store = new GroupsStore();
- spyOn(store, 'formatGroupItem').and.callThrough();
-
- store.setSearchedGroups(mockSearchedGroups);
-
- expect(store.state.groups.length).toBe(mockSearchedGroups.length);
- expect(store.formatGroupItem).toHaveBeenCalledWith(jasmine.any(Object));
- expect(Object.keys(store.state.groups[0]).indexOf('fullName')).toBeGreaterThan(-1);
- expect(Object.keys(store.state.groups[0].children[0]).indexOf('fullName')).toBeGreaterThan(
- -1,
- );
- });
- });
-
- describe('setGroupChildren', () => {
- it('should set children to group item in state', () => {
- const store = new GroupsStore();
- spyOn(store, 'formatGroupItem').and.callThrough();
-
- store.setGroupChildren(mockParentGroupItem, mockRawChildren);
-
- expect(store.formatGroupItem).toHaveBeenCalledWith(jasmine.any(Object));
- expect(mockParentGroupItem.children.length).toBe(1);
- expect(Object.keys(mockParentGroupItem.children[0]).indexOf('fullName')).toBeGreaterThan(-1);
- expect(mockParentGroupItem.isOpen).toBeTruthy();
- expect(mockParentGroupItem.isChildrenLoading).toBeFalsy();
- });
- });
-
- describe('setPaginationInfo', () => {
- it('should parse and set pagination info in state', () => {
- const store = new GroupsStore();
-
- store.setPaginationInfo(mockRawPageInfo);
-
- expect(store.state.pageInfo.perPage).toBe(10);
- expect(store.state.pageInfo.page).toBe(10);
- expect(store.state.pageInfo.total).toBe(10);
- expect(store.state.pageInfo.totalPages).toBe(10);
- expect(store.state.pageInfo.nextPage).toBe(10);
- expect(store.state.pageInfo.previousPage).toBe(10);
- });
- });
-
- describe('formatGroupItem', () => {
- it('should parse group item object and return updated object', () => {
- let store;
- let updatedGroupItem;
-
- store = new GroupsStore();
- updatedGroupItem = store.formatGroupItem(mockRawChildren[0]);
-
- expect(Object.keys(updatedGroupItem).indexOf('fullName')).toBeGreaterThan(-1);
- expect(updatedGroupItem.childrenCount).toBe(mockRawChildren[0].children_count);
- expect(updatedGroupItem.isChildrenLoading).toBe(false);
- expect(updatedGroupItem.isBeingRemoved).toBe(false);
-
- store = new GroupsStore(true);
- updatedGroupItem = store.formatGroupItem(mockRawChildren[0]);
-
- expect(Object.keys(updatedGroupItem).indexOf('fullName')).toBeGreaterThan(-1);
- expect(updatedGroupItem.childrenCount).toBe(mockRawChildren[0].subgroup_count);
- });
- });
-
- describe('removeGroup', () => {
- it('should remove children from group item in state', () => {
- const store = new GroupsStore();
- const rawParentGroup = Object.assign({}, mockGroups[0]);
- const rawChildGroup = Object.assign({}, mockGroups[1]);
-
- store.setGroups([rawParentGroup]);
- store.setGroupChildren(store.state.groups[0], [rawChildGroup]);
- const childItem = store.state.groups[0].children[0];
-
- store.removeGroup(childItem, store.state.groups[0]);
-
- expect(store.state.groups[0].children.length).toBe(0);
- });
- });
-});
diff --git a/spec/javascripts/helpers/filtered_search_spec_helper.js b/spec/javascripts/helpers/filtered_search_spec_helper.js
index ceb7982bbc3..de17518ea51 100644
--- a/spec/javascripts/helpers/filtered_search_spec_helper.js
+++ b/spec/javascripts/helpers/filtered_search_spec_helper.js
@@ -1,69 +1 @@
-export default class FilteredSearchSpecHelper {
- static createFilterVisualTokenHTML(name, operator, value, isSelected) {
- return FilteredSearchSpecHelper.createFilterVisualToken(name, operator, value, isSelected)
- .outerHTML;
- }
-
- static createFilterVisualToken(name, operator, value, isSelected = false) {
- const li = document.createElement('li');
- li.classList.add('js-visual-token', 'filtered-search-token', `search-token-${name}`);
-
- li.innerHTML = `
- <div class="selectable ${isSelected ? 'selected' : ''}" role="button">
- <div class="name">${name}</div>
- <div class="operator">${operator}</div>
- <div class="value-container">
- <div class="value">${value}</div>
- <div class="remove-token" role="button">
- <i class="fa fa-close"></i>
- </div>
- </div>
- </div>
- `;
-
- return li;
- }
-
- static createNameFilterVisualTokenHTML(name) {
- return `
- <li class="js-visual-token filtered-search-token">
- <div class="name">${name}</div>
- </li>
- `;
- }
-
- static createNameOperatorFilterVisualTokenHTML(name, operator) {
- return `
- <li class="js-visual-token filtered-search-token">
- <div class="name">${name}</div>
- <div class="operator">${operator}</div>
- </li>
- `;
- }
-
- static createSearchVisualToken(name) {
- const li = document.createElement('li');
- li.classList.add('js-visual-token', 'filtered-search-term');
- li.innerHTML = `<div class="name">${name}</div>`;
- return li;
- }
-
- static createSearchVisualTokenHTML(name) {
- return FilteredSearchSpecHelper.createSearchVisualToken(name).outerHTML;
- }
-
- static createInputHTML(placeholder = '', value = '') {
- return `
- <li class="input-token">
- <input type='text' class='filtered-search' placeholder='${placeholder}' value='${value}'/>
- </li>
- `;
- }
-
- static createTokensContainerHTML(html, inputPlaceholder) {
- return `
- ${html}
- ${FilteredSearchSpecHelper.createInputHTML(inputPlaceholder)}
- `;
- }
-}
+export { default } from '../../frontend/helpers/filtered_search_spec_helper';
diff --git a/spec/javascripts/helpers/init_vue_mr_page_helper.js b/spec/javascripts/helpers/init_vue_mr_page_helper.js
index 04f969fcd2d..1ba08199764 100644
--- a/spec/javascripts/helpers/init_vue_mr_page_helper.js
+++ b/spec/javascripts/helpers/init_vue_mr_page_helper.js
@@ -2,7 +2,7 @@ import MockAdapter from 'axios-mock-adapter';
import initMRPage from '~/mr_notes/index';
import axios from '~/lib/utils/axios_utils';
import { userDataMock, notesDataMock, noteableDataMock } from '../../frontend/notes/mock_data';
-import diffFileMockData from '../diffs/mock_data/diff_file';
+import diffFileMockData from '../../frontend/diffs/mock_data/diff_file';
export default function initVueMRPage() {
const mrTestEl = document.createElement('div');
diff --git a/spec/javascripts/helpers/vue_mount_component_helper.js b/spec/javascripts/helpers/vue_mount_component_helper.js
index 6848c95d95d..c1857115b61 100644
--- a/spec/javascripts/helpers/vue_mount_component_helper.js
+++ b/spec/javascripts/helpers/vue_mount_component_helper.js
@@ -1,38 +1,2 @@
-import Vue from 'vue';
-
-const mountComponent = (Component, props = {}, el = null) =>
- new Component({
- propsData: props,
- }).$mount(el);
-
-export const createComponentWithStore = (Component, store, propsData = {}) =>
- new Component({
- store,
- propsData,
- });
-
-export const mountComponentWithStore = (Component, { el, props, store }) =>
- new Component({
- store,
- propsData: props || {},
- }).$mount(el);
-
-export const mountComponentWithSlots = (Component, { props, slots }) => {
- const component = new Component({
- propsData: props || {},
- });
-
- component.$slots = slots;
-
- return component.$mount();
-};
-
-/**
- * Mount a component with the given render method.
- *
- * This helps with inserting slots that need to be compiled.
- */
-export const mountComponentWithRender = (render, el = null) =>
- mountComponent(Vue.extend({ render }), {}, el);
-
-export default mountComponent;
+export { default } from '../../frontend/helpers/vue_mount_component_helper';
+export * from '../../frontend/helpers/vue_mount_component_helper';
diff --git a/spec/javascripts/ide/components/activity_bar_spec.js b/spec/javascripts/ide/components/activity_bar_spec.js
deleted file mode 100644
index 823ca29dab9..00000000000
--- a/spec/javascripts/ide/components/activity_bar_spec.js
+++ /dev/null
@@ -1,72 +0,0 @@
-import Vue from 'vue';
-import store from '~/ide/stores';
-import { leftSidebarViews } from '~/ide/constants';
-import ActivityBar from '~/ide/components/activity_bar.vue';
-import { createComponentWithStore } from '../../helpers/vue_mount_component_helper';
-import { resetStore } from '../helpers';
-
-describe('IDE activity bar', () => {
- const Component = Vue.extend(ActivityBar);
- let vm;
-
- beforeEach(() => {
- Vue.set(store.state.projects, 'abcproject', {
- web_url: 'testing',
- });
- Vue.set(store.state, 'currentProjectId', 'abcproject');
-
- vm = createComponentWithStore(Component, store);
- });
-
- afterEach(() => {
- vm.$destroy();
-
- resetStore(vm.$store);
- });
-
- describe('updateActivityBarView', () => {
- beforeEach(() => {
- spyOn(vm, 'updateActivityBarView');
-
- vm.$mount();
- });
-
- it('calls updateActivityBarView with edit value on click', () => {
- vm.$el.querySelector('.js-ide-edit-mode').click();
-
- expect(vm.updateActivityBarView).toHaveBeenCalledWith(leftSidebarViews.edit.name);
- });
-
- it('calls updateActivityBarView with commit value on click', () => {
- vm.$el.querySelector('.js-ide-commit-mode').click();
-
- expect(vm.updateActivityBarView).toHaveBeenCalledWith(leftSidebarViews.commit.name);
- });
-
- it('calls updateActivityBarView with review value on click', () => {
- vm.$el.querySelector('.js-ide-review-mode').click();
-
- expect(vm.updateActivityBarView).toHaveBeenCalledWith(leftSidebarViews.review.name);
- });
- });
-
- describe('active item', () => {
- beforeEach(() => {
- vm.$mount();
- });
-
- it('sets edit item active', () => {
- expect(vm.$el.querySelector('.js-ide-edit-mode').classList).toContain('active');
- });
-
- it('sets commit item active', done => {
- vm.$store.state.currentActivityView = leftSidebarViews.commit.name;
-
- vm.$nextTick(() => {
- expect(vm.$el.querySelector('.js-ide-commit-mode').classList).toContain('active');
-
- done();
- });
- });
- });
-});
diff --git a/spec/javascripts/ide/components/commit_sidebar/radio_group_spec.js b/spec/javascripts/ide/components/commit_sidebar/radio_group_spec.js
deleted file mode 100644
index b30f0e6822b..00000000000
--- a/spec/javascripts/ide/components/commit_sidebar/radio_group_spec.js
+++ /dev/null
@@ -1,139 +0,0 @@
-import Vue from 'vue';
-import { createComponentWithStore } from 'spec/helpers/vue_mount_component_helper';
-import { resetStore } from 'spec/ide/helpers';
-import store from '~/ide/stores';
-import radioGroup from '~/ide/components/commit_sidebar/radio_group.vue';
-
-describe('IDE commit sidebar radio group', () => {
- let vm;
-
- beforeEach(done => {
- const Component = Vue.extend(radioGroup);
-
- store.state.commit.commitAction = '2';
-
- vm = createComponentWithStore(Component, store, {
- value: '1',
- label: 'test',
- checked: true,
- });
-
- vm.$mount();
-
- Vue.nextTick(done);
- });
-
- afterEach(() => {
- vm.$destroy();
-
- resetStore(vm.$store);
- });
-
- it('uses label if present', () => {
- expect(vm.$el.textContent).toContain('test');
- });
-
- it('uses slot if label is not present', done => {
- vm.$destroy();
-
- vm = new Vue({
- components: {
- radioGroup,
- },
- store,
- template: `
- <radio-group
- value="1"
- >
- Testing slot
- </radio-group>
- `,
- });
-
- vm.$mount();
-
- Vue.nextTick(() => {
- expect(vm.$el.textContent).toContain('Testing slot');
-
- done();
- });
- });
-
- it('updates store when changing radio button', done => {
- vm.$el.querySelector('input').dispatchEvent(new Event('change'));
-
- Vue.nextTick(() => {
- expect(store.state.commit.commitAction).toBe('1');
-
- done();
- });
- });
-
- describe('with input', () => {
- beforeEach(done => {
- vm.$destroy();
-
- const Component = Vue.extend(radioGroup);
-
- store.state.commit.commitAction = '1';
- store.state.commit.newBranchName = 'test-123';
-
- vm = createComponentWithStore(Component, store, {
- value: '1',
- label: 'test',
- checked: true,
- showInput: true,
- });
-
- vm.$mount();
-
- Vue.nextTick(done);
- });
-
- it('renders input box when commitAction matches value', () => {
- expect(vm.$el.querySelector('.form-control')).not.toBeNull();
- });
-
- it('hides input when commitAction doesnt match value', done => {
- store.state.commit.commitAction = '2';
-
- Vue.nextTick(() => {
- expect(vm.$el.querySelector('.form-control')).toBeNull();
- done();
- });
- });
-
- it('updates branch name in store on input', done => {
- const input = vm.$el.querySelector('.form-control');
- input.value = 'testing-123';
- input.dispatchEvent(new Event('input'));
-
- Vue.nextTick(() => {
- expect(store.state.commit.newBranchName).toBe('testing-123');
-
- done();
- });
- });
-
- it('renders newBranchName if present', () => {
- const input = vm.$el.querySelector('.form-control');
-
- expect(input.value).toBe('test-123');
- });
- });
-
- describe('tooltipTitle', () => {
- it('returns title when disabled', () => {
- vm.title = 'test title';
- vm.disabled = true;
-
- expect(vm.tooltipTitle).toBe('test title');
- });
-
- it('returns blank when not disabled', () => {
- vm.title = 'test title';
-
- expect(vm.tooltipTitle).not.toBe('test title');
- });
- });
-});
diff --git a/spec/javascripts/ide/components/file_row_extra_spec.js b/spec/javascripts/ide/components/file_row_extra_spec.js
deleted file mode 100644
index 9fd014b50ef..00000000000
--- a/spec/javascripts/ide/components/file_row_extra_spec.js
+++ /dev/null
@@ -1,170 +0,0 @@
-import Vue from 'vue';
-import { createComponentWithStore } from 'spec/helpers/vue_mount_component_helper';
-import { createStore } from '~/ide/stores';
-import FileRowExtra from '~/ide/components/file_row_extra.vue';
-import { file, resetStore } from '../helpers';
-
-describe('IDE extra file row component', () => {
- let Component;
- let vm;
- let unstagedFilesCount = 0;
- let stagedFilesCount = 0;
- let changesCount = 0;
-
- beforeAll(() => {
- Component = Vue.extend(FileRowExtra);
- });
-
- beforeEach(() => {
- vm = createComponentWithStore(Component, createStore(), {
- file: {
- ...file('test'),
- },
- dropdownOpen: false,
- });
-
- spyOnProperty(vm, 'getUnstagedFilesCountForPath').and.returnValue(() => unstagedFilesCount);
- spyOnProperty(vm, 'getStagedFilesCountForPath').and.returnValue(() => stagedFilesCount);
- spyOnProperty(vm, 'getChangesInFolder').and.returnValue(() => changesCount);
-
- vm.$mount();
- });
-
- afterEach(() => {
- vm.$destroy();
- resetStore(vm.$store);
-
- stagedFilesCount = 0;
- unstagedFilesCount = 0;
- changesCount = 0;
- });
-
- describe('folderChangesTooltip', () => {
- it('returns undefined when changes count is 0', () => {
- changesCount = 0;
-
- expect(vm.folderChangesTooltip).toBe(undefined);
- });
-
- [{ input: 1, output: '1 changed file' }, { input: 2, output: '2 changed files' }].forEach(
- ({ input, output }) => {
- it('returns changed files count if changes count is not 0', () => {
- changesCount = input;
-
- expect(vm.folderChangesTooltip).toBe(output);
- });
- },
- );
- });
-
- describe('show tree changes count', () => {
- it('does not show for blobs', () => {
- vm.file.type = 'blob';
-
- expect(vm.$el.querySelector('.ide-tree-changes')).toBe(null);
- });
-
- it('does not show when changes count is 0', () => {
- vm.file.type = 'tree';
-
- expect(vm.$el.querySelector('.ide-tree-changes')).toBe(null);
- });
-
- it('does not show when tree is open', done => {
- vm.file.type = 'tree';
- vm.file.opened = true;
- changesCount = 1;
-
- vm.$nextTick(() => {
- expect(vm.$el.querySelector('.ide-tree-changes')).toBe(null);
-
- done();
- });
- });
-
- it('shows for trees with changes', done => {
- vm.file.type = 'tree';
- vm.file.opened = false;
- changesCount = 1;
-
- vm.$nextTick(() => {
- expect(vm.$el.querySelector('.ide-tree-changes')).not.toBe(null);
-
- done();
- });
- });
- });
-
- describe('changes file icon', () => {
- it('hides when file is not changed', () => {
- expect(vm.$el.querySelector('.file-changed-icon')).toBe(null);
- });
-
- it('shows when file is changed', done => {
- vm.file.changed = true;
-
- vm.$nextTick(() => {
- expect(vm.$el.querySelector('.file-changed-icon')).not.toBe(null);
-
- done();
- });
- });
-
- it('shows when file is staged', done => {
- vm.file.staged = true;
-
- vm.$nextTick(() => {
- expect(vm.$el.querySelector('.file-changed-icon')).not.toBe(null);
-
- done();
- });
- });
-
- it('shows when file is a tempFile', done => {
- vm.file.tempFile = true;
-
- vm.$nextTick(() => {
- expect(vm.$el.querySelector('.file-changed-icon')).not.toBe(null);
-
- done();
- });
- });
-
- it('shows when file is renamed', done => {
- vm.file.prevPath = 'original-file';
-
- vm.$nextTick(() => {
- expect(vm.$el.querySelector('.file-changed-icon')).not.toBe(null);
-
- done();
- });
- });
-
- it('hides when file is renamed', done => {
- vm.file.prevPath = 'original-file';
- vm.file.type = 'tree';
-
- vm.$nextTick(() => {
- expect(vm.$el.querySelector('.file-changed-icon')).toBe(null);
-
- done();
- });
- });
- });
-
- describe('merge request icon', () => {
- it('hides when not a merge request change', () => {
- expect(vm.$el.querySelector('.ic-git-merge')).toBe(null);
- });
-
- it('shows when a merge request change', done => {
- vm.file.mrChange = true;
-
- vm.$nextTick(() => {
- expect(vm.$el.querySelector('.ic-git-merge')).not.toBe(null);
-
- done();
- });
- });
- });
-});
diff --git a/spec/javascripts/ide/components/file_templates/bar_spec.js b/spec/javascripts/ide/components/file_templates/bar_spec.js
deleted file mode 100644
index 5399ada94ae..00000000000
--- a/spec/javascripts/ide/components/file_templates/bar_spec.js
+++ /dev/null
@@ -1,117 +0,0 @@
-import Vue from 'vue';
-import { mountComponentWithStore } from 'spec/helpers/vue_mount_component_helper';
-import { createStore } from '~/ide/stores';
-import Bar from '~/ide/components/file_templates/bar.vue';
-import { resetStore, file } from '../../helpers';
-
-describe('IDE file templates bar component', () => {
- let Component;
- let vm;
-
- beforeAll(() => {
- Component = Vue.extend(Bar);
- });
-
- beforeEach(() => {
- const store = createStore();
-
- store.state.openFiles.push({
- ...file('file'),
- opened: true,
- active: true,
- });
-
- vm = mountComponentWithStore(Component, { store });
- });
-
- afterEach(() => {
- vm.$destroy();
- resetStore(vm.$store);
- });
-
- describe('template type dropdown', () => {
- it('renders dropdown component', () => {
- expect(vm.$el.querySelector('.dropdown').textContent).toContain('Choose a type');
- });
-
- it('calls setSelectedTemplateType when clicking item', () => {
- spyOn(vm, 'setSelectedTemplateType').and.stub();
-
- vm.$el.querySelector('.dropdown-content button').click();
-
- expect(vm.setSelectedTemplateType).toHaveBeenCalledWith({
- name: '.gitlab-ci.yml',
- key: 'gitlab_ci_ymls',
- });
- });
- });
-
- describe('template dropdown', () => {
- beforeEach(done => {
- vm.$store.state.fileTemplates.templates = [
- {
- name: 'test',
- },
- ];
- vm.$store.state.fileTemplates.selectedTemplateType = {
- name: '.gitlab-ci.yml',
- key: 'gitlab_ci_ymls',
- };
-
- vm.$nextTick(done);
- });
-
- it('renders dropdown component', () => {
- expect(vm.$el.querySelectorAll('.dropdown')[1].textContent).toContain('Choose a template');
- });
-
- it('calls fetchTemplate on click', () => {
- spyOn(vm, 'fetchTemplate').and.stub();
-
- vm.$el
- .querySelectorAll('.dropdown-content')[1]
- .querySelector('button')
- .click();
-
- expect(vm.fetchTemplate).toHaveBeenCalledWith({
- name: 'test',
- });
- });
- });
-
- it('shows undo button if updateSuccess is true', done => {
- vm.$store.state.fileTemplates.updateSuccess = true;
-
- vm.$nextTick(() => {
- expect(vm.$el.querySelector('.btn-default').style.display).not.toBe('none');
-
- done();
- });
- });
-
- it('calls undoFileTemplate when clicking undo button', () => {
- spyOn(vm, 'undoFileTemplate').and.stub();
-
- vm.$el.querySelector('.btn-default').click();
-
- expect(vm.undoFileTemplate).toHaveBeenCalled();
- });
-
- it('calls setSelectedTemplateType if activeFile name matches a template', done => {
- const fileName = '.gitlab-ci.yml';
-
- spyOn(vm, 'setSelectedTemplateType');
- vm.$store.state.openFiles[0].name = fileName;
-
- vm.setInitialType();
-
- vm.$nextTick(() => {
- expect(vm.setSelectedTemplateType).toHaveBeenCalledWith({
- name: fileName,
- key: 'gitlab_ci_ymls',
- });
-
- done();
- });
- });
-});
diff --git a/spec/javascripts/ide/components/ide_review_spec.js b/spec/javascripts/ide/components/ide_review_spec.js
deleted file mode 100644
index 396c5d282d4..00000000000
--- a/spec/javascripts/ide/components/ide_review_spec.js
+++ /dev/null
@@ -1,69 +0,0 @@
-import Vue from 'vue';
-import IdeReview from '~/ide/components/ide_review.vue';
-import store from '~/ide/stores';
-import { createComponentWithStore } from '../../helpers/vue_mount_component_helper';
-import { trimText } from '../../helpers/text_helper';
-import { resetStore, file } from '../helpers';
-import { projectData } from '../mock_data';
-
-describe('IDE review mode', () => {
- const Component = Vue.extend(IdeReview);
- let vm;
-
- beforeEach(() => {
- store.state.currentProjectId = 'abcproject';
- store.state.currentBranchId = 'master';
- store.state.projects.abcproject = Object.assign({}, projectData);
- Vue.set(store.state.trees, 'abcproject/master', {
- tree: [file('fileName')],
- loading: false,
- });
-
- vm = createComponentWithStore(Component, store).$mount();
- });
-
- afterEach(() => {
- vm.$destroy();
-
- resetStore(vm.$store);
- });
-
- it('renders list of files', () => {
- expect(vm.$el.textContent).toContain('fileName');
- });
-
- describe('merge request', () => {
- beforeEach(done => {
- store.state.currentMergeRequestId = '1';
- store.state.projects.abcproject.mergeRequests['1'] = {
- iid: 123,
- web_url: 'testing123',
- };
-
- vm.$nextTick(done);
- });
-
- it('renders edit dropdown', () => {
- expect(vm.$el.querySelector('.btn')).not.toBe(null);
- });
-
- it('renders merge request link & IID', () => {
- const link = vm.$el.querySelector('.ide-review-sub-header');
-
- expect(link.querySelector('a').getAttribute('href')).toBe('testing123');
- expect(trimText(link.textContent)).toBe('Merge request (!123)');
- });
-
- it('changes text to latest changes when viewer is not mrdiff', done => {
- store.state.viewer = 'diff';
-
- vm.$nextTick(() => {
- expect(trimText(vm.$el.querySelector('.ide-review-sub-header').textContent)).toBe(
- 'Latest changes',
- );
-
- done();
- });
- });
- });
-});
diff --git a/spec/javascripts/ide/components/ide_side_bar_spec.js b/spec/javascripts/ide/components/ide_side_bar_spec.js
deleted file mode 100644
index 28f127a61c0..00000000000
--- a/spec/javascripts/ide/components/ide_side_bar_spec.js
+++ /dev/null
@@ -1,57 +0,0 @@
-import Vue from 'vue';
-import { createComponentWithStore } from 'spec/helpers/vue_mount_component_helper';
-import store from '~/ide/stores';
-import ideSidebar from '~/ide/components/ide_side_bar.vue';
-import { leftSidebarViews } from '~/ide/constants';
-import { resetStore } from '../helpers';
-import { projectData } from '../mock_data';
-
-describe('IdeSidebar', () => {
- let vm;
-
- beforeEach(() => {
- const Component = Vue.extend(ideSidebar);
-
- store.state.currentProjectId = 'abcproject';
- store.state.projects.abcproject = projectData;
-
- vm = createComponentWithStore(Component, store).$mount();
- });
-
- afterEach(() => {
- vm.$destroy();
-
- resetStore(vm.$store);
- });
-
- it('renders a sidebar', () => {
- expect(vm.$el.querySelector('.multi-file-commit-panel-inner')).not.toBeNull();
- });
-
- it('renders loading icon component', done => {
- vm.$store.state.loading = true;
-
- vm.$nextTick(() => {
- expect(vm.$el.querySelector('.multi-file-loading-container')).not.toBeNull();
- expect(vm.$el.querySelectorAll('.multi-file-loading-container').length).toBe(3);
-
- done();
- });
- });
-
- describe('activityBarComponent', () => {
- it('renders tree component', () => {
- expect(vm.$el.querySelector('.ide-file-list')).not.toBeNull();
- });
-
- it('renders commit component', done => {
- vm.$store.state.currentActivityView = leftSidebarViews.commit.name;
-
- vm.$nextTick(() => {
- expect(vm.$el.querySelector('.multi-file-commit-panel-section')).not.toBeNull();
-
- done();
- });
- });
- });
-});
diff --git a/spec/javascripts/ide/components/ide_spec.js b/spec/javascripts/ide/components/ide_spec.js
deleted file mode 100644
index 4241b994cba..00000000000
--- a/spec/javascripts/ide/components/ide_spec.js
+++ /dev/null
@@ -1,125 +0,0 @@
-import Vue from 'vue';
-import { createComponentWithStore } from 'spec/helpers/vue_mount_component_helper';
-import store from '~/ide/stores';
-import ide from '~/ide/components/ide.vue';
-import { file, resetStore } from '../helpers';
-import { projectData } from '../mock_data';
-
-function bootstrap(projData) {
- const Component = Vue.extend(ide);
-
- store.state.currentProjectId = 'abcproject';
- store.state.currentBranchId = 'master';
- store.state.projects.abcproject = Object.assign({}, projData);
- Vue.set(store.state.trees, 'abcproject/master', {
- tree: [],
- loading: false,
- });
-
- return createComponentWithStore(Component, store, {
- emptyStateSvgPath: 'svg',
- noChangesStateSvgPath: 'svg',
- committedStateSvgPath: 'svg',
- });
-}
-
-describe('ide component, empty repo', () => {
- let vm;
-
- beforeEach(() => {
- const emptyProjData = Object.assign({}, projectData, { empty_repo: true, branches: {} });
- vm = bootstrap(emptyProjData);
- vm.$mount();
- });
-
- afterEach(() => {
- vm.$destroy();
-
- resetStore(vm.$store);
- });
-
- it('renders "New file" button in empty repo', done => {
- vm.$nextTick(() => {
- expect(vm.$el.querySelector('.ide-empty-state button[title="New file"]')).not.toBeNull();
- done();
- });
- });
-});
-
-describe('ide component, non-empty repo', () => {
- let vm;
-
- beforeEach(() => {
- vm = bootstrap(projectData);
- vm.$mount();
- });
-
- afterEach(() => {
- vm.$destroy();
-
- resetStore(vm.$store);
- });
-
- it('shows error message when set', done => {
- expect(vm.$el.querySelector('.gl-alert')).toBe(null);
-
- vm.$store.state.errorMessage = {
- text: 'error',
- };
-
- vm.$nextTick(() => {
- expect(vm.$el.querySelector('.gl-alert')).not.toBe(null);
-
- done();
- });
- });
-
- describe('onBeforeUnload', () => {
- it('returns undefined when no staged files or changed files', () => {
- expect(vm.onBeforeUnload()).toBe(undefined);
- });
-
- it('returns warning text when their are changed files', () => {
- vm.$store.state.changedFiles.push(file());
-
- expect(vm.onBeforeUnload()).toBe('Are you sure you want to lose unsaved changes?');
- });
-
- it('returns warning text when their are staged files', () => {
- vm.$store.state.stagedFiles.push(file());
-
- expect(vm.onBeforeUnload()).toBe('Are you sure you want to lose unsaved changes?');
- });
-
- it('updates event object', () => {
- const event = {};
- vm.$store.state.stagedFiles.push(file());
-
- vm.onBeforeUnload(event);
-
- expect(event.returnValue).toBe('Are you sure you want to lose unsaved changes?');
- });
- });
-
- describe('non-existent branch', () => {
- it('does not render "New file" button for non-existent branch when repo is not empty', done => {
- vm.$nextTick(() => {
- expect(vm.$el.querySelector('.ide-empty-state button[title="New file"]')).toBeNull();
- done();
- });
- });
- });
-
- describe('branch with files', () => {
- beforeEach(() => {
- store.state.trees['abcproject/master'].tree = [file()];
- });
-
- it('does not render "New file" button', done => {
- vm.$nextTick(() => {
- expect(vm.$el.querySelector('.ide-empty-state button[title="New file"]')).toBeNull();
- done();
- });
- });
- });
-});
diff --git a/spec/javascripts/ide/components/ide_status_bar_spec.js b/spec/javascripts/ide/components/ide_status_bar_spec.js
deleted file mode 100644
index 3facf1c266a..00000000000
--- a/spec/javascripts/ide/components/ide_status_bar_spec.js
+++ /dev/null
@@ -1,129 +0,0 @@
-import Vue from 'vue';
-import _ from 'lodash';
-import { createComponentWithStore } from 'spec/helpers/vue_mount_component_helper';
-import { TEST_HOST } from 'spec/test_constants';
-import { createStore } from '~/ide/stores';
-import IdeStatusBar from '~/ide/components/ide_status_bar.vue';
-import { rightSidebarViews } from '~/ide/constants';
-import { projectData } from '../mock_data';
-
-const TEST_PROJECT_ID = 'abcproject';
-const TEST_MERGE_REQUEST_ID = '9001';
-const TEST_MERGE_REQUEST_URL = `${TEST_HOST}merge-requests/${TEST_MERGE_REQUEST_ID}`;
-
-describe('ideStatusBar', () => {
- let store;
- let vm;
-
- const createComponent = () => {
- vm = createComponentWithStore(Vue.extend(IdeStatusBar), store).$mount();
- };
- const findMRStatus = () => vm.$el.querySelector('.js-ide-status-mr');
-
- beforeEach(() => {
- store = createStore();
- store.state.currentProjectId = TEST_PROJECT_ID;
- store.state.projects[TEST_PROJECT_ID] = _.clone(projectData);
- store.state.currentBranchId = 'master';
- });
-
- afterEach(() => {
- vm.$destroy();
- });
-
- describe('default', () => {
- beforeEach(() => {
- createComponent();
- });
-
- it('triggers a setInterval', () => {
- expect(vm.intervalId).not.toBe(null);
- });
-
- it('renders the statusbar', () => {
- expect(vm.$el.className).toBe('ide-status-bar');
- });
-
- describe('commitAgeUpdate', () => {
- beforeEach(function() {
- jasmine.clock().install();
- spyOn(vm, 'commitAgeUpdate').and.callFake(() => {});
- vm.startTimer();
- });
-
- afterEach(function() {
- jasmine.clock().uninstall();
- });
-
- it('gets called every second', () => {
- expect(vm.commitAgeUpdate).not.toHaveBeenCalled();
-
- jasmine.clock().tick(1100);
-
- expect(vm.commitAgeUpdate.calls.count()).toEqual(1);
-
- jasmine.clock().tick(1000);
-
- expect(vm.commitAgeUpdate.calls.count()).toEqual(2);
- });
- });
-
- describe('getCommitPath', () => {
- it('returns the path to the commit details', () => {
- expect(vm.getCommitPath('abc123de')).toBe('/commit/abc123de');
- });
- });
-
- describe('pipeline status', () => {
- it('opens right sidebar on clicking icon', done => {
- spyOn(vm, 'openRightPane');
- Vue.set(vm.$store.state.pipelines, 'latestPipeline', {
- details: {
- status: {
- text: 'success',
- details_path: 'test',
- icon: 'status_success',
- },
- },
- commit: {
- author_gravatar_url: 'www',
- },
- });
-
- vm.$nextTick()
- .then(() => {
- vm.$el.querySelector('.ide-status-pipeline button').click();
-
- expect(vm.openRightPane).toHaveBeenCalledWith(rightSidebarViews.pipelines);
- })
- .then(done)
- .catch(done.fail);
- });
- });
-
- it('does not show merge request status', () => {
- expect(findMRStatus()).toBe(null);
- });
- });
-
- describe('with merge request in store', () => {
- beforeEach(() => {
- store.state.projects[TEST_PROJECT_ID].mergeRequests = {
- [TEST_MERGE_REQUEST_ID]: {
- web_url: TEST_MERGE_REQUEST_URL,
- references: {
- short: `!${TEST_MERGE_REQUEST_ID}`,
- },
- },
- };
- store.state.currentMergeRequestId = TEST_MERGE_REQUEST_ID;
-
- createComponent();
- });
-
- it('shows merge request status', () => {
- expect(findMRStatus().textContent.trim()).toEqual(`Merge request !${TEST_MERGE_REQUEST_ID}`);
- expect(findMRStatus().querySelector('a').href).toEqual(TEST_MERGE_REQUEST_URL);
- });
- });
-});
diff --git a/spec/javascripts/ide/components/ide_tree_list_spec.js b/spec/javascripts/ide/components/ide_tree_list_spec.js
deleted file mode 100644
index f63007c7dd2..00000000000
--- a/spec/javascripts/ide/components/ide_tree_list_spec.js
+++ /dev/null
@@ -1,77 +0,0 @@
-import Vue from 'vue';
-import IdeTreeList from '~/ide/components/ide_tree_list.vue';
-import store from '~/ide/stores';
-import { createComponentWithStore } from '../../helpers/vue_mount_component_helper';
-import { resetStore, file } from '../helpers';
-import { projectData } from '../mock_data';
-
-describe('IDE tree list', () => {
- const Component = Vue.extend(IdeTreeList);
- const normalBranchTree = [file('fileName')];
- const emptyBranchTree = [];
- let vm;
-
- const bootstrapWithTree = (tree = normalBranchTree) => {
- store.state.currentProjectId = 'abcproject';
- store.state.currentBranchId = 'master';
- store.state.projects.abcproject = Object.assign({}, projectData);
- Vue.set(store.state.trees, 'abcproject/master', {
- tree,
- loading: false,
- });
-
- vm = createComponentWithStore(Component, store, {
- viewerType: 'edit',
- });
- };
-
- afterEach(() => {
- vm.$destroy();
-
- resetStore(vm.$store);
- });
-
- describe('normal branch', () => {
- beforeEach(() => {
- bootstrapWithTree();
-
- spyOn(vm, 'updateViewer').and.callThrough();
-
- vm.$mount();
- });
-
- it('updates viewer on mount', () => {
- expect(vm.updateViewer).toHaveBeenCalledWith('edit');
- });
-
- it('renders loading indicator', done => {
- store.state.trees['abcproject/master'].loading = true;
-
- vm.$nextTick(() => {
- expect(vm.$el.querySelector('.multi-file-loading-container')).not.toBeNull();
- expect(vm.$el.querySelectorAll('.multi-file-loading-container').length).toBe(3);
-
- done();
- });
- });
-
- it('renders list of files', () => {
- expect(vm.$el.textContent).toContain('fileName');
- });
- });
-
- describe('empty-branch state', () => {
- beforeEach(() => {
- bootstrapWithTree(emptyBranchTree);
-
- spyOn(vm, 'updateViewer').and.callThrough();
-
- vm.$mount();
- });
-
- it('does not load files if the branch is empty', () => {
- expect(vm.$el.textContent).not.toContain('fileName');
- expect(vm.$el.textContent).toContain('No files');
- });
- });
-});
diff --git a/spec/javascripts/ide/components/ide_tree_spec.js b/spec/javascripts/ide/components/ide_tree_spec.js
deleted file mode 100644
index 97a0a2432f1..00000000000
--- a/spec/javascripts/ide/components/ide_tree_spec.js
+++ /dev/null
@@ -1,34 +0,0 @@
-import Vue from 'vue';
-import IdeTree from '~/ide/components/ide_tree.vue';
-import store from '~/ide/stores';
-import { createComponentWithStore } from '../../helpers/vue_mount_component_helper';
-import { resetStore, file } from '../helpers';
-import { projectData } from '../mock_data';
-
-describe('IdeRepoTree', () => {
- let vm;
-
- beforeEach(() => {
- const IdeRepoTree = Vue.extend(IdeTree);
-
- store.state.currentProjectId = 'abcproject';
- store.state.currentBranchId = 'master';
- store.state.projects.abcproject = Object.assign({}, projectData);
- Vue.set(store.state.trees, 'abcproject/master', {
- tree: [file('fileName')],
- loading: false,
- });
-
- vm = createComponentWithStore(IdeRepoTree, store).$mount();
- });
-
- afterEach(() => {
- vm.$destroy();
-
- resetStore(vm.$store);
- });
-
- it('renders list of files', () => {
- expect(vm.$el.textContent).toContain('fileName');
- });
-});
diff --git a/spec/javascripts/ide/components/merge_requests/item_spec.js b/spec/javascripts/ide/components/merge_requests/item_spec.js
deleted file mode 100644
index 155a247defb..00000000000
--- a/spec/javascripts/ide/components/merge_requests/item_spec.js
+++ /dev/null
@@ -1,63 +0,0 @@
-import Vue from 'vue';
-import router from '~/ide/ide_router';
-import Item from '~/ide/components/merge_requests/item.vue';
-import mountCompontent from '../../../helpers/vue_mount_component_helper';
-
-describe('IDE merge request item', () => {
- const Component = Vue.extend(Item);
- let vm;
-
- beforeEach(() => {
- vm = mountCompontent(Component, {
- item: {
- iid: 1,
- projectPathWithNamespace: 'gitlab-org/gitlab-ce',
- title: 'Merge request title',
- },
- currentId: '1',
- currentProjectId: 'gitlab-org/gitlab-ce',
- });
- });
-
- afterEach(() => {
- vm.$destroy();
- });
-
- it('renders merge requests data', () => {
- expect(vm.$el.textContent).toContain('Merge request title');
- expect(vm.$el.textContent).toContain('gitlab-org/gitlab-ce!1');
- });
-
- it('renders link with href', () => {
- const expectedHref = router.resolve(
- `/project/${vm.item.projectPathWithNamespace}/merge_requests/${vm.item.iid}`,
- ).href;
-
- expect(vm.$el).toMatch('a');
- expect(vm.$el).toHaveAttr('href', expectedHref);
- });
-
- it('renders icon if ID matches currentId', () => {
- expect(vm.$el.querySelector('.ic-mobile-issue-close')).not.toBe(null);
- });
-
- it('does not render icon if ID does not match currentId', done => {
- vm.currentId = '2';
-
- vm.$nextTick(() => {
- expect(vm.$el.querySelector('.ic-mobile-issue-close')).toBe(null);
-
- done();
- });
- });
-
- it('does not render icon if project ID does not match', done => {
- vm.currentProjectId = 'test/test';
-
- vm.$nextTick(() => {
- expect(vm.$el.querySelector('.ic-mobile-issue-close')).toBe(null);
-
- done();
- });
- });
-});
diff --git a/spec/javascripts/ide/components/nav_dropdown_button_spec.js b/spec/javascripts/ide/components/nav_dropdown_button_spec.js
deleted file mode 100644
index bbaf97164ea..00000000000
--- a/spec/javascripts/ide/components/nav_dropdown_button_spec.js
+++ /dev/null
@@ -1,93 +0,0 @@
-import Vue from 'vue';
-import { trimText } from 'spec/helpers/text_helper';
-import { mountComponentWithStore } from 'spec/helpers/vue_mount_component_helper';
-import NavDropdownButton from '~/ide/components/nav_dropdown_button.vue';
-import { createStore } from '~/ide/stores';
-
-describe('NavDropdown', () => {
- const TEST_BRANCH_ID = 'lorem-ipsum-dolar';
- const TEST_MR_ID = '12345';
- let store;
- let vm;
-
- beforeEach(() => {
- store = createStore();
- });
-
- afterEach(() => {
- vm.$destroy();
- });
-
- const createComponent = (props = {}) => {
- vm = mountComponentWithStore(Vue.extend(NavDropdownButton), { props, store });
- vm.$mount();
- };
-
- const findIcon = name => vm.$el.querySelector(`.ic-${name}`);
- const findMRIcon = () => findIcon('merge-request');
- const findBranchIcon = () => findIcon('branch');
-
- describe('normal', () => {
- beforeEach(() => {
- createComponent();
- });
-
- it('renders empty placeholders, if state is falsey', () => {
- expect(trimText(vm.$el.textContent)).toEqual('- -');
- });
-
- it('renders branch name, if state has currentBranchId', done => {
- vm.$store.state.currentBranchId = TEST_BRANCH_ID;
-
- vm.$nextTick()
- .then(() => {
- expect(trimText(vm.$el.textContent)).toEqual(`${TEST_BRANCH_ID} -`);
- })
- .then(done)
- .catch(done.fail);
- });
-
- it('renders mr id, if state has currentMergeRequestId', done => {
- vm.$store.state.currentMergeRequestId = TEST_MR_ID;
-
- vm.$nextTick()
- .then(() => {
- expect(trimText(vm.$el.textContent)).toEqual(`- !${TEST_MR_ID}`);
- })
- .then(done)
- .catch(done.fail);
- });
-
- it('renders branch and mr, if state has both', done => {
- vm.$store.state.currentBranchId = TEST_BRANCH_ID;
- vm.$store.state.currentMergeRequestId = TEST_MR_ID;
-
- vm.$nextTick()
- .then(() => {
- expect(trimText(vm.$el.textContent)).toEqual(`${TEST_BRANCH_ID} !${TEST_MR_ID}`);
- })
- .then(done)
- .catch(done.fail);
- });
-
- it('shows icons', () => {
- expect(findBranchIcon()).toBeTruthy();
- expect(findMRIcon()).toBeTruthy();
- });
- });
-
- describe('with showMergeRequests false', () => {
- beforeEach(() => {
- createComponent({ showMergeRequests: false });
- });
-
- it('shows single empty placeholder, if state is falsey', () => {
- expect(trimText(vm.$el.textContent)).toEqual('-');
- });
-
- it('shows only branch icon', () => {
- expect(findBranchIcon()).toBeTruthy();
- expect(findMRIcon()).toBe(null);
- });
- });
-});
diff --git a/spec/javascripts/ide/components/nav_dropdown_spec.js b/spec/javascripts/ide/components/nav_dropdown_spec.js
deleted file mode 100644
index dfb4d03540f..00000000000
--- a/spec/javascripts/ide/components/nav_dropdown_spec.js
+++ /dev/null
@@ -1,80 +0,0 @@
-import $ from 'jquery';
-import Vue from 'vue';
-import { mountComponentWithStore } from 'spec/helpers/vue_mount_component_helper';
-import store from '~/ide/stores';
-import NavDropdown from '~/ide/components/nav_dropdown.vue';
-import { PERMISSION_READ_MR } from '~/ide/constants';
-
-const TEST_PROJECT_ID = 'lorem-ipsum';
-
-describe('IDE NavDropdown', () => {
- const Component = Vue.extend(NavDropdown);
- let vm;
- let $dropdown;
-
- beforeEach(() => {
- store.state.currentProjectId = TEST_PROJECT_ID;
- Vue.set(store.state.projects, TEST_PROJECT_ID, {
- userPermissions: {
- [PERMISSION_READ_MR]: true,
- },
- });
- vm = mountComponentWithStore(Component, { store });
- $dropdown = $(vm.$el);
-
- // block dispatch from doing anything
- spyOn(vm.$store, 'dispatch');
- });
-
- afterEach(() => {
- vm.$destroy();
- });
-
- const findIcon = name => vm.$el.querySelector(`.ic-${name}`);
- const findMRIcon = () => findIcon('merge-request');
-
- it('renders nothing initially', () => {
- expect(vm.$el).not.toContainElement('.ide-nav-form');
- });
-
- it('renders nav form when show.bs.dropdown', done => {
- $dropdown.trigger('show.bs.dropdown');
-
- vm.$nextTick()
- .then(() => {
- expect(vm.$el).toContainElement('.ide-nav-form');
- })
- .then(done)
- .catch(done.fail);
- });
-
- it('destroys nav form when closed', done => {
- $dropdown.trigger('show.bs.dropdown');
- $dropdown.trigger('hide.bs.dropdown');
-
- vm.$nextTick()
- .then(() => {
- expect(vm.$el).not.toContainElement('.ide-nav-form');
- })
- .then(done)
- .catch(done.fail);
- });
-
- it('renders merge request icon', () => {
- expect(findMRIcon()).not.toBeNull();
- });
-
- describe('when user cannot read merge requests', () => {
- beforeEach(done => {
- store.state.projects[TEST_PROJECT_ID].userPermissions = {};
-
- vm.$nextTick()
- .then(done)
- .catch(done.fail);
- });
-
- it('does not render merge requests', () => {
- expect(findMRIcon()).toBeNull();
- });
- });
-});
diff --git a/spec/javascripts/ide/components/new_dropdown/button_spec.js b/spec/javascripts/ide/components/new_dropdown/button_spec.js
deleted file mode 100644
index 6a326b5bd92..00000000000
--- a/spec/javascripts/ide/components/new_dropdown/button_spec.js
+++ /dev/null
@@ -1,65 +0,0 @@
-import Vue from 'vue';
-import mountComponent from 'spec/helpers/vue_mount_component_helper';
-import Button from '~/ide/components/new_dropdown/button.vue';
-
-describe('IDE new entry dropdown button component', () => {
- let Component;
- let vm;
-
- beforeAll(() => {
- Component = Vue.extend(Button);
- });
-
- beforeEach(() => {
- vm = mountComponent(Component, {
- label: 'Testing',
- icon: 'doc-new',
- });
-
- spyOn(vm, '$emit');
- });
-
- afterEach(() => {
- vm.$destroy();
- });
-
- it('renders button with label', () => {
- expect(vm.$el.textContent).toContain('Testing');
- });
-
- it('renders icon', () => {
- expect(vm.$el.querySelector('.ic-doc-new')).not.toBe(null);
- });
-
- it('emits click event', () => {
- vm.$el.click();
-
- expect(vm.$emit).toHaveBeenCalledWith('click');
- });
-
- it('hides label if showLabel is false', done => {
- vm.showLabel = false;
-
- vm.$nextTick(() => {
- expect(vm.$el.textContent).not.toContain('Testing');
-
- done();
- });
- });
-
- describe('tooltipTitle', () => {
- it('returns empty string when showLabel is true', () => {
- expect(vm.tooltipTitle).toBe('');
- });
-
- it('returns label', done => {
- vm.showLabel = false;
-
- vm.$nextTick(() => {
- expect(vm.tooltipTitle).toBe('Testing');
-
- done();
- });
- });
- });
-});
diff --git a/spec/javascripts/ide/components/new_dropdown/index_spec.js b/spec/javascripts/ide/components/new_dropdown/index_spec.js
deleted file mode 100644
index 03afe997fed..00000000000
--- a/spec/javascripts/ide/components/new_dropdown/index_spec.js
+++ /dev/null
@@ -1,84 +0,0 @@
-import Vue from 'vue';
-import { createComponentWithStore } from 'spec/helpers/vue_mount_component_helper';
-import store from '~/ide/stores';
-import newDropdown from '~/ide/components/new_dropdown/index.vue';
-import { resetStore } from '../../helpers';
-
-describe('new dropdown component', () => {
- let vm;
-
- beforeEach(() => {
- const component = Vue.extend(newDropdown);
-
- vm = createComponentWithStore(component, store, {
- branch: 'master',
- path: '',
- mouseOver: false,
- type: 'tree',
- });
-
- vm.$store.state.currentProjectId = 'abcproject';
- vm.$store.state.path = '';
- vm.$store.state.trees['abcproject/mybranch'] = {
- tree: [],
- };
-
- spyOn(vm, 'openNewEntryModal');
-
- vm.$mount();
- });
-
- afterEach(() => {
- vm.$destroy();
-
- resetStore(vm.$store);
- });
-
- it('renders new file, upload and new directory links', () => {
- const buttons = vm.$el.querySelectorAll('.dropdown-menu button');
-
- expect(buttons[0].textContent.trim()).toBe('New file');
- expect(buttons[1].textContent.trim()).toBe('Upload file');
- expect(buttons[2].textContent.trim()).toBe('New directory');
- });
-
- describe('createNewItem', () => {
- it('sets modalType to blob when new file is clicked', () => {
- vm.$el.querySelectorAll('.dropdown-menu button')[0].click();
-
- expect(vm.openNewEntryModal).toHaveBeenCalledWith({ type: 'blob', path: '' });
- });
-
- it('sets modalType to tree when new directory is clicked', () => {
- vm.$el.querySelectorAll('.dropdown-menu button')[2].click();
-
- expect(vm.openNewEntryModal).toHaveBeenCalledWith({ type: 'tree', path: '' });
- });
- });
-
- describe('isOpen', () => {
- it('scrolls dropdown into view', done => {
- spyOn(vm.$refs.dropdownMenu, 'scrollIntoView');
-
- vm.isOpen = true;
-
- setTimeout(() => {
- expect(vm.$refs.dropdownMenu.scrollIntoView).toHaveBeenCalledWith({
- block: 'nearest',
- });
-
- done();
- });
- });
- });
-
- describe('delete entry', () => {
- it('calls delete action', () => {
- spyOn(vm, 'deleteEntry');
-
- vm.$el.querySelectorAll('.dropdown-menu button')[4].click();
-
- expect(vm.deleteEntry).toHaveBeenCalledWith('');
- });
- });
-});
diff --git a/spec/javascripts/ide/components/new_dropdown/modal_spec.js b/spec/javascripts/ide/components/new_dropdown/modal_spec.js
deleted file mode 100644
index 0ea767e087d..00000000000
--- a/spec/javascripts/ide/components/new_dropdown/modal_spec.js
+++ /dev/null
@@ -1,150 +0,0 @@
-import Vue from 'vue';
-import { createComponentWithStore } from 'spec/helpers/vue_mount_component_helper';
-import { createStore } from '~/ide/stores';
-import modal from '~/ide/components/new_dropdown/modal.vue';
-
-describe('new file modal component', () => {
- const Component = Vue.extend(modal);
- let vm;
-
- afterEach(() => {
- vm.$destroy();
- });
-
- ['tree', 'blob'].forEach(type => {
- describe(type, () => {
- beforeEach(() => {
- const store = createStore();
- store.state.entryModal = {
- type,
- path: '',
- entry: {
- path: '',
- },
- };
-
- vm = createComponentWithStore(Component, store).$mount();
-
- vm.name = 'testing';
- });
-
- it(`sets modal title as ${type}`, () => {
- const title = type === 'tree' ? 'directory' : 'file';
-
- expect(vm.$el.querySelector('.modal-title').textContent.trim()).toBe(`Create new ${title}`);
- });
-
- it(`sets button label as ${type}`, () => {
- const title = type === 'tree' ? 'directory' : 'file';
-
- expect(vm.$el.querySelector('.btn-success').textContent.trim()).toBe(`Create ${title}`);
- });
-
- it(`sets form label as ${type}`, () => {
- expect(vm.$el.querySelector('.label-bold').textContent.trim()).toBe('Name');
- });
-
- it(`${type === 'tree' ? 'does not show' : 'shows'} file templates`, () => {
- const templateFilesEl = vm.$el.querySelector('.file-templates');
- if (type === 'tree') {
- expect(templateFilesEl).toBeNull();
- } else {
- expect(templateFilesEl instanceof Element).toBeTruthy();
- }
- });
- });
- });
-
- describe('rename entry', () => {
- beforeEach(() => {
- const store = createStore();
- store.state.entryModal = {
- type: 'rename',
- path: '',
- entry: {
- name: 'test',
- type: 'blob',
- path: 'test-path',
- },
- };
-
- vm = createComponentWithStore(Component, store).$mount();
- });
-
- ['tree', 'blob'].forEach(type => {
- it(`renders title and button for renaming ${type}`, done => {
- const text = type === 'tree' ? 'folder' : 'file';
-
- vm.$store.state.entryModal.entry.type = type;
-
- vm.$nextTick(() => {
- expect(vm.$el.querySelector('.modal-title').textContent.trim()).toBe(`Rename ${text}`);
- expect(vm.$el.querySelector('.btn-success').textContent.trim()).toBe(`Rename ${text}`);
-
- done();
- });
- });
- });
-
- describe('entryName', () => {
- it('returns entries name', () => {
- expect(vm.entryName).toBe('test-path');
- });
-
- it('updated name', () => {
- vm.name = 'index.js';
-
- expect(vm.entryName).toBe('index.js');
- });
-
- it('removes leading/trailing spaces when found in the new name', () => {
- vm.entryName = ' index.js ';
-
- expect(vm.entryName).toBe('index.js');
- });
-
- it('does not remove internal spaces in the file name', () => {
- vm.entryName = ' In Praise of Idleness.txt ';
-
- expect(vm.entryName).toBe('In Praise of Idleness.txt');
- });
- });
- });
-
- describe('submitForm', () => {
- it('throws an error when target entry exists', () => {
- const store = createStore();
- store.state.entryModal = {
- type: 'rename',
- path: 'test-path/test',
- entry: {
- name: 'test',
- type: 'blob',
- path: 'test-path/test',
- },
- };
- store.state.entries = {
- 'test-path/test': {
- name: 'test',
- deleted: false,
- },
- };
-
- vm = createComponentWithStore(Component, store).$mount();
- const flashSpy = spyOnDependency(modal, 'flash');
-
- expect(flashSpy).not.toHaveBeenCalled();
-
- vm.submitForm();
-
- expect(flashSpy).toHaveBeenCalledWith(
- 'The name "test-path/test" is already taken in this directory.',
- 'alert',
- jasmine.anything(),
- null,
- false,
- true,
- );
- });
- });
-});
diff --git a/spec/javascripts/ide/components/new_dropdown/upload_spec.js b/spec/javascripts/ide/components/new_dropdown/upload_spec.js
deleted file mode 100644
index 66ddf6c0ee6..00000000000
--- a/spec/javascripts/ide/components/new_dropdown/upload_spec.js
+++ /dev/null
@@ -1,112 +0,0 @@
-import Vue from 'vue';
-import createComponent from 'spec/helpers/vue_mount_component_helper';
-import upload from '~/ide/components/new_dropdown/upload.vue';
-
-describe('new dropdown upload', () => {
- let vm;
-
- beforeEach(() => {
- const Component = Vue.extend(upload);
-
- vm = createComponent(Component, {
- path: '',
- });
-
- vm.entryName = 'testing';
-
- spyOn(vm, '$emit').and.callThrough();
- });
-
- afterEach(() => {
- vm.$destroy();
- });
-
- describe('openFile', () => {
- it('calls for each file', () => {
- const files = ['test', 'test2', 'test3'];
-
- spyOn(vm, 'readFile');
- spyOnProperty(vm.$refs.fileUpload, 'files').and.returnValue(files);
-
- vm.openFile();
-
- expect(vm.readFile.calls.count()).toBe(3);
-
- files.forEach((file, i) => {
- expect(vm.readFile.calls.argsFor(i)).toEqual([file]);
- });
- });
- });
-
- describe('readFile', () => {
- beforeEach(() => {
- spyOn(FileReader.prototype, 'readAsDataURL');
- });
-
- it('calls readAsDataURL for all files', () => {
- const file = {
- type: 'images/png',
- };
-
- vm.readFile(file);
-
- expect(FileReader.prototype.readAsDataURL).toHaveBeenCalledWith(file);
- });
- });
-
- describe('createFile', () => {
- const textTarget = {
- result: 'base64,cGxhaW4gdGV4dA==',
- };
- const binaryTarget = {
- result: 'base64,w4I=',
- };
- const textFile = new File(['plain text'], 'textFile');
-
- const binaryFile = {
- name: 'binaryFile',
- type: 'image/png',
- };
-
- beforeEach(() => {
- spyOn(FileReader.prototype, 'readAsText').and.callThrough();
- });
-
- it('calls readAsText and creates file in plain text (without encoding) if the file content is plain text', done => {
- const waitForCreate = new Promise(resolve => vm.$on('create', resolve));
-
- vm.createFile(textTarget, textFile);
-
- expect(FileReader.prototype.readAsText).toHaveBeenCalledWith(textFile);
-
- waitForCreate
- .then(() => {
- expect(vm.$emit).toHaveBeenCalledWith('create', {
- name: textFile.name,
- type: 'blob',
- content: 'plain text',
- base64: false,
- binary: false,
- rawPath: '',
- });
- })
- .then(done)
- .catch(done.fail);
- });
-
- it('splits content on base64 if binary', () => {
- vm.createFile(binaryTarget, binaryFile);
-
- expect(FileReader.prototype.readAsText).not.toHaveBeenCalledWith(textFile);
-
- expect(vm.$emit).toHaveBeenCalledWith('create', {
- name: binaryFile.name,
- type: 'blob',
- content: binaryTarget.result.split('base64,')[1],
- base64: true,
- binary: true,
- rawPath: binaryTarget.result,
- });
- });
- });
-});
diff --git a/spec/javascripts/ide/components/repo_editor_spec.js b/spec/javascripts/ide/components/repo_editor_spec.js
index ef0299f0d56..8db29011da7 100644
--- a/spec/javascripts/ide/components/repo_editor_spec.js
+++ b/spec/javascripts/ide/components/repo_editor_spec.js
@@ -26,7 +26,23 @@ describe('RepoEditor', () => {
f.active = true;
f.tempFile = true;
+
vm.$store.state.openFiles.push(f);
+ vm.$store.state.projects = {
+ 'gitlab-org/gitlab': {
+ branches: {
+ master: {
+ name: 'master',
+ commit: {
+ id: 'abcdefgh',
+ },
+ },
+ },
+ },
+ };
+ vm.$store.state.currentProjectId = 'gitlab-org/gitlab';
+ vm.$store.state.currentBranchId = 'master';
+
Vue.set(vm.$store.state.entries, f.path, f);
spyOn(vm, 'getFileData').and.returnValue(Promise.resolve());
@@ -46,11 +62,6 @@ describe('RepoEditor', () => {
});
const findEditor = () => vm.$el.querySelector('.multi-file-editor-holder');
- const changeRightPanelCollapsed = () => {
- const { state } = vm.$store;
-
- state.rightPanelCollapsed = !state.rightPanelCollapsed;
- };
it('sets renderWhitespace to `all`', () => {
vm.$store.state.renderWhitespaceInCode = true;
@@ -303,17 +314,6 @@ describe('RepoEditor', () => {
spyOn(vm.editor, 'updateDiffView');
});
- it('calls updateDimensions when rightPanelCollapsed is changed', done => {
- changeRightPanelCollapsed();
-
- vm.$nextTick(() => {
- expect(vm.editor.updateDimensions).toHaveBeenCalled();
- expect(vm.editor.updateDiffView).toHaveBeenCalled();
-
- done();
- });
- });
-
it('calls updateDimensions when panelResizing is false', done => {
vm.$store.state.panelResizing = true;
@@ -391,17 +391,6 @@ describe('RepoEditor', () => {
expect(findEditor()).toHaveCss({ display: 'none' });
});
- it('should not update dimensions', done => {
- changeRightPanelCollapsed();
-
- vm.$nextTick()
- .then(() => {
- expect(vm.editor.updateDimensions).not.toHaveBeenCalled();
- })
- .then(done)
- .catch(done.fail);
- });
-
describe('when file view mode changes to editor', () => {
beforeEach(done => {
vm.file.viewMode = FILE_VIEW_MODE_EDITOR;
diff --git a/spec/javascripts/ide/components/repo_tab_spec.js b/spec/javascripts/ide/components/repo_tab_spec.js
deleted file mode 100644
index 3b52f279bf2..00000000000
--- a/spec/javascripts/ide/components/repo_tab_spec.js
+++ /dev/null
@@ -1,185 +0,0 @@
-import Vue from 'vue';
-import store from '~/ide/stores';
-import repoTab from '~/ide/components/repo_tab.vue';
-import router from '~/ide/ide_router';
-import { file, resetStore } from '../helpers';
-
-describe('RepoTab', () => {
- let vm;
-
- function createComponent(propsData) {
- const RepoTab = Vue.extend(repoTab);
-
- return new RepoTab({
- store,
- propsData,
- }).$mount();
- }
-
- beforeEach(() => {
- spyOn(router, 'push');
- });
-
- afterEach(() => {
- vm.$destroy();
-
- resetStore(vm.$store);
- });
-
- it('renders a close link and a name link', () => {
- vm = createComponent({
- tab: file(),
- });
- vm.$store.state.openFiles.push(vm.tab);
- const close = vm.$el.querySelector('.multi-file-tab-close');
- const name = vm.$el.querySelector(`[title="${vm.tab.url}"]`);
-
- expect(close.innerHTML).toContain('#close');
- expect(name.textContent.trim()).toEqual(vm.tab.name);
- });
-
- it('does not call openPendingTab when tab is active', done => {
- vm = createComponent({
- tab: {
- ...file(),
- pending: true,
- active: true,
- },
- });
-
- spyOn(vm, 'openPendingTab');
-
- vm.$el.click();
-
- vm.$nextTick(() => {
- expect(vm.openPendingTab).not.toHaveBeenCalled();
-
- done();
- });
- });
-
- it('fires clickFile when the link is clicked', () => {
- vm = createComponent({
- tab: file(),
- });
-
- spyOn(vm, 'clickFile');
-
- vm.$el.click();
-
- expect(vm.clickFile).toHaveBeenCalledWith(vm.tab);
- });
-
- it('calls closeFile when clicking close button', () => {
- vm = createComponent({
- tab: file(),
- });
-
- spyOn(vm, 'closeFile');
-
- vm.$el.querySelector('.multi-file-tab-close').click();
-
- expect(vm.closeFile).toHaveBeenCalledWith(vm.tab);
- });
-
- it('changes icon on hover', done => {
- const tab = file();
- tab.changed = true;
- vm = createComponent({
- tab,
- });
-
- vm.$el.dispatchEvent(new Event('mouseover'));
-
- Vue.nextTick()
- .then(() => {
- expect(vm.$el.querySelector('.file-modified')).toBeNull();
-
- vm.$el.dispatchEvent(new Event('mouseout'));
- })
- .then(Vue.nextTick)
- .then(() => {
- expect(vm.$el.querySelector('.file-modified')).not.toBeNull();
-
- done();
- })
- .catch(done.fail);
- });
-
- describe('locked file', () => {
- let f;
-
- beforeEach(() => {
- f = file('locked file');
- f.file_lock = {
- user: {
- name: 'testuser',
- updated_at: new Date(),
- },
- };
-
- vm = createComponent({
- tab: f,
- });
- });
-
- afterEach(() => {
- vm.$destroy();
- });
-
- it('renders lock icon', () => {
- expect(vm.$el.querySelector('.file-status-icon')).not.toBeNull();
- });
-
- it('renders a tooltip', () => {
- expect(vm.$el.querySelector('span:nth-child(2)').dataset.originalTitle).toContain(
- 'Locked by testuser',
- );
- });
- });
-
- describe('methods', () => {
- describe('closeTab', () => {
- it('closes tab if file has changed', done => {
- const tab = file();
- tab.changed = true;
- tab.opened = true;
- vm = createComponent({
- tab,
- });
- vm.$store.state.openFiles.push(tab);
- vm.$store.state.changedFiles.push(tab);
- vm.$store.state.entries[tab.path] = tab;
- vm.$store.dispatch('setFileActive', tab.path);
-
- vm.$el.querySelector('.multi-file-tab-close').click();
-
- vm.$nextTick(() => {
- expect(tab.opened).toBeFalsy();
- expect(vm.$store.state.changedFiles.length).toBe(1);
-
- done();
- });
- });
-
- it('closes tab when clicking close btn', done => {
- const tab = file('lose');
- tab.opened = true;
- vm = createComponent({
- tab,
- });
- vm.$store.state.openFiles.push(tab);
- vm.$store.state.entries[tab.path] = tab;
- vm.$store.dispatch('setFileActive', tab.path);
-
- vm.$el.querySelector('.multi-file-tab-close').click();
-
- vm.$nextTick(() => {
- expect(tab.opened).toBeFalsy();
-
- done();
- });
- });
- });
- });
-});
diff --git a/spec/javascripts/ide/components/shared/tokened_input_spec.js b/spec/javascripts/ide/components/shared/tokened_input_spec.js
deleted file mode 100644
index 885fd976655..00000000000
--- a/spec/javascripts/ide/components/shared/tokened_input_spec.js
+++ /dev/null
@@ -1,133 +0,0 @@
-import Vue from 'vue';
-import mountComponent from 'spec/helpers/vue_mount_component_helper';
-import TokenedInput from '~/ide/components/shared/tokened_input.vue';
-
-const TEST_PLACEHOLDER = 'Searching in test';
-const TEST_TOKENS = [
- { label: 'lorem', id: 1 },
- { label: 'ipsum', id: 2 },
- { label: 'dolar', id: 3 },
-];
-const TEST_VALUE = 'lorem';
-
-function getTokenElements(vm) {
- return Array.from(vm.$el.querySelectorAll('.filtered-search-token button'));
-}
-
-function createBackspaceEvent() {
- const e = new Event('keyup');
- e.keyCode = 8;
- e.which = e.keyCode;
- e.altKey = false;
- e.ctrlKey = true;
- e.shiftKey = false;
- e.metaKey = false;
- return e;
-}
-
-describe('IDE shared/TokenedInput', () => {
- const Component = Vue.extend(TokenedInput);
- let vm;
-
- beforeEach(() => {
- vm = mountComponent(Component, {
- tokens: TEST_TOKENS,
- placeholder: TEST_PLACEHOLDER,
- value: TEST_VALUE,
- });
-
- spyOn(vm, '$emit');
- });
-
- afterEach(() => {
- vm.$destroy();
- });
-
- it('renders tokens', () => {
- const renderedTokens = getTokenElements(vm).map(x => x.textContent.trim());
-
- expect(renderedTokens).toEqual(TEST_TOKENS.map(x => x.label));
- });
-
- it('renders input', () => {
- expect(vm.$refs.input).toBeTruthy();
- expect(vm.$refs.input).toHaveValue(TEST_VALUE);
- });
-
- it('renders placeholder, when tokens are empty', done => {
- vm.tokens = [];
-
- vm.$nextTick()
- .then(() => {
- expect(vm.$refs.input).toHaveAttr('placeholder', TEST_PLACEHOLDER);
- })
- .then(done)
- .catch(done.fail);
- });
-
- it('triggers "removeToken" on token click', () => {
- getTokenElements(vm)[0].click();
-
- expect(vm.$emit).toHaveBeenCalledWith('removeToken', TEST_TOKENS[0]);
- });
-
- it('when input triggers backspace event, it calls "onBackspace"', () => {
- spyOn(vm, 'onBackspace');
-
- vm.$refs.input.dispatchEvent(createBackspaceEvent());
- vm.$refs.input.dispatchEvent(createBackspaceEvent());
-
- expect(vm.onBackspace).toHaveBeenCalledTimes(2);
- });
-
- it('triggers "removeToken" on backspaces when value is empty', () => {
- vm.value = '';
-
- vm.onBackspace();
-
- expect(vm.$emit).not.toHaveBeenCalled();
- expect(vm.backspaceCount).toEqual(1);
-
- vm.onBackspace();
-
- expect(vm.$emit).toHaveBeenCalledWith('removeToken', TEST_TOKENS[TEST_TOKENS.length - 1]);
- expect(vm.backspaceCount).toEqual(0);
- });
-
- it('does not trigger "removeToken" on backspaces when value is not empty', () => {
- vm.onBackspace();
- vm.onBackspace();
-
- expect(vm.backspaceCount).toEqual(0);
- expect(vm.$emit).not.toHaveBeenCalled();
- });
-
- it('does not trigger "removeToken" on backspaces when tokens are empty', () => {
- vm.tokens = [];
-
- vm.onBackspace();
- vm.onBackspace();
-
- expect(vm.backspaceCount).toEqual(0);
- expect(vm.$emit).not.toHaveBeenCalled();
- });
-
- it('triggers "focus" on input focus', () => {
- vm.$refs.input.dispatchEvent(new Event('focus'));
-
- expect(vm.$emit).toHaveBeenCalledWith('focus');
- });
-
- it('triggers "blur" on input blur', () => {
- vm.$refs.input.dispatchEvent(new Event('blur'));
-
- expect(vm.$emit).toHaveBeenCalledWith('blur');
- });
-
- it('triggers "input" with value on input change', () => {
- vm.$refs.input.value = 'something-else';
- vm.$refs.input.dispatchEvent(new Event('input'));
-
- expect(vm.$emit).toHaveBeenCalledWith('input', 'something-else');
- });
-});
diff --git a/spec/javascripts/ide/lib/common/model_manager_spec.js b/spec/javascripts/ide/lib/common/model_manager_spec.js
deleted file mode 100644
index 38ffa317e8e..00000000000
--- a/spec/javascripts/ide/lib/common/model_manager_spec.js
+++ /dev/null
@@ -1,126 +0,0 @@
-import eventHub from '~/ide/eventhub';
-import ModelManager from '~/ide/lib/common/model_manager';
-import { file } from '../../helpers';
-
-describe('Multi-file editor library model manager', () => {
- let instance;
-
- beforeEach(() => {
- instance = new ModelManager();
- });
-
- afterEach(() => {
- instance.dispose();
- });
-
- describe('addModel', () => {
- it('caches model', () => {
- instance.addModel(file());
-
- expect(instance.models.size).toBe(1);
- });
-
- it('caches model by file path', () => {
- const f = file('path-name');
- instance.addModel(f);
-
- expect(instance.models.keys().next().value).toBe(f.key);
- });
-
- it('adds model into disposable', () => {
- spyOn(instance.disposable, 'add').and.callThrough();
-
- instance.addModel(file());
-
- expect(instance.disposable.add).toHaveBeenCalled();
- });
-
- it('returns cached model', () => {
- spyOn(instance.models, 'get').and.callThrough();
-
- instance.addModel(file());
- instance.addModel(file());
-
- expect(instance.models.get).toHaveBeenCalled();
- });
-
- it('adds eventHub listener', () => {
- const f = file();
- spyOn(eventHub, '$on').and.callThrough();
-
- instance.addModel(f);
-
- expect(eventHub.$on).toHaveBeenCalledWith(
- `editor.update.model.dispose.${f.key}`,
- jasmine.anything(),
- );
- });
- });
-
- describe('hasCachedModel', () => {
- it('returns false when no models exist', () => {
- expect(instance.hasCachedModel('path')).toBeFalsy();
- });
-
- it('returns true when model exists', () => {
- const f = file('path-name');
-
- instance.addModel(f);
-
- expect(instance.hasCachedModel(f.key)).toBeTruthy();
- });
- });
-
- describe('getModel', () => {
- it('returns cached model', () => {
- instance.addModel(file('path-name'));
-
- expect(instance.getModel('path-name')).not.toBeNull();
- });
- });
-
- describe('removeCachedModel', () => {
- let f;
-
- beforeEach(() => {
- f = file();
-
- instance.addModel(f);
- });
-
- it('clears cached model', () => {
- instance.removeCachedModel(f);
-
- expect(instance.models.size).toBe(0);
- });
-
- it('removes eventHub listener', () => {
- spyOn(eventHub, '$off').and.callThrough();
-
- instance.removeCachedModel(f);
-
- expect(eventHub.$off).toHaveBeenCalledWith(
- `editor.update.model.dispose.${f.key}`,
- jasmine.anything(),
- );
- });
- });
-
- describe('dispose', () => {
- it('clears cached models', () => {
- instance.addModel(file());
-
- instance.dispose();
-
- expect(instance.models.size).toBe(0);
- });
-
- it('calls disposable dispose', () => {
- spyOn(instance.disposable, 'dispose').and.callThrough();
-
- instance.dispose();
-
- expect(instance.disposable.dispose).toHaveBeenCalled();
- });
- });
-});
diff --git a/spec/javascripts/ide/lib/common/model_spec.js b/spec/javascripts/ide/lib/common/model_spec.js
deleted file mode 100644
index f096e06f43c..00000000000
--- a/spec/javascripts/ide/lib/common/model_spec.js
+++ /dev/null
@@ -1,137 +0,0 @@
-import eventHub from '~/ide/eventhub';
-import Model from '~/ide/lib/common/model';
-import { file } from '../../helpers';
-
-describe('Multi-file editor library model', () => {
- let model;
-
- beforeEach(() => {
- spyOn(eventHub, '$on').and.callThrough();
-
- const f = file('path');
- f.mrChange = { diff: 'ABC' };
- f.baseRaw = 'test';
- model = new Model(f);
- });
-
- afterEach(() => {
- model.dispose();
- });
-
- it('creates original model & base model & new model', () => {
- expect(model.originalModel).not.toBeNull();
- expect(model.model).not.toBeNull();
- expect(model.baseModel).not.toBeNull();
-
- expect(model.originalModel.uri.path).toBe('original/path--path');
- expect(model.model.uri.path).toBe('path--path');
- expect(model.baseModel.uri.path).toBe('target/path--path');
- });
-
- it('creates model with head file to compare against', () => {
- const f = file('path');
- model.dispose();
-
- model = new Model(f, {
- ...f,
- content: '123 testing',
- });
-
- expect(model.head).not.toBeNull();
- expect(model.getOriginalModel().getValue()).toBe('123 testing');
- });
-
- it('adds eventHub listener', () => {
- expect(eventHub.$on).toHaveBeenCalledWith(
- `editor.update.model.dispose.${model.file.key}`,
- jasmine.anything(),
- );
- });
-
- describe('path', () => {
- it('returns file path', () => {
- expect(model.path).toBe(model.file.key);
- });
- });
-
- describe('getModel', () => {
- it('returns model', () => {
- expect(model.getModel()).toBe(model.model);
- });
- });
-
- describe('getOriginalModel', () => {
- it('returns original model', () => {
- expect(model.getOriginalModel()).toBe(model.originalModel);
- });
- });
-
- describe('getBaseModel', () => {
- it('returns base model', () => {
- expect(model.getBaseModel()).toBe(model.baseModel);
- });
- });
-
- describe('setValue', () => {
- it('updates models value', () => {
- model.setValue('testing 123');
-
- expect(model.getModel().getValue()).toBe('testing 123');
- });
- });
-
- describe('onChange', () => {
- it('calls callback on change', done => {
- const spy = jasmine.createSpy();
- model.onChange(spy);
-
- model.getModel().setValue('123');
-
- setTimeout(() => {
- expect(spy).toHaveBeenCalledWith(model, jasmine.anything());
- done();
- });
- });
- });
-
- describe('dispose', () => {
- it('calls disposable dispose', () => {
- spyOn(model.disposable, 'dispose').and.callThrough();
-
- model.dispose();
-
- expect(model.disposable.dispose).toHaveBeenCalled();
- });
-
- it('clears events', () => {
- model.onChange(() => {});
-
- expect(model.events.size).toBe(1);
-
- model.dispose();
-
- expect(model.events.size).toBe(0);
- });
-
- it('removes eventHub listener', () => {
- spyOn(eventHub, '$off').and.callThrough();
-
- model.dispose();
-
- expect(eventHub.$off).toHaveBeenCalledWith(
- `editor.update.model.dispose.${model.file.key}`,
- jasmine.anything(),
- );
- });
-
- it('calls onDispose callback', () => {
- const disposeSpy = jasmine.createSpy();
-
- model.onDispose(disposeSpy);
-
- model.dispose();
-
- expect(disposeSpy).toHaveBeenCalled();
- });
- });
-});
diff --git a/spec/javascripts/ide/lib/decorations/controller_spec.js b/spec/javascripts/ide/lib/decorations/controller_spec.js
deleted file mode 100644
index 4118774cca3..00000000000
--- a/spec/javascripts/ide/lib/decorations/controller_spec.js
+++ /dev/null
@@ -1,143 +0,0 @@
-import Editor from '~/ide/lib/editor';
-import DecorationsController from '~/ide/lib/decorations/controller';
-import Model from '~/ide/lib/common/model';
-import { file } from '../../helpers';
-
-describe('Multi-file editor library decorations controller', () => {
- let editorInstance;
- let controller;
- let model;
-
- beforeEach(() => {
- editorInstance = Editor.create();
- editorInstance.createInstance(document.createElement('div'));
-
- controller = new DecorationsController(editorInstance);
- model = new Model(file('path'));
- });
-
- afterEach(() => {
- model.dispose();
- editorInstance.dispose();
- controller.dispose();
- });
-
- describe('getAllDecorationsForModel', () => {
- it('returns empty array when no decorations exist for model', () => {
- const decorations = controller.getAllDecorationsForModel(model);
-
- expect(decorations).toEqual([]);
- });
-
- it('returns decorations by model URL', () => {
- controller.addDecorations(model, 'key', [{ decoration: 'decorationValue' }]);
-
- const decorations = controller.getAllDecorationsForModel(model);
-
- expect(decorations[0]).toEqual({ decoration: 'decorationValue' });
- });
- });
-
- describe('addDecorations', () => {
- it('caches decorations in a new map', () => {
- controller.addDecorations(model, 'key', [{ decoration: 'decorationValue' }]);
-
- expect(controller.decorations.size).toBe(1);
- });
-
- it('does not create new cache model', () => {
- controller.addDecorations(model, 'key', [{ decoration: 'decorationValue' }]);
- controller.addDecorations(model, 'key', [{ decoration: 'decorationValue2' }]);
-
- expect(controller.decorations.size).toBe(1);
- });
-
- it('caches decorations by model URL', () => {
- controller.addDecorations(model, 'key', [{ decoration: 'decorationValue' }]);
-
- expect(controller.decorations.size).toBe(1);
- expect(controller.decorations.keys().next().value).toBe('gitlab:path--path');
- });
-
- it('calls decorate method', () => {
- spyOn(controller, 'decorate');
-
- controller.addDecorations(model, 'key', [{ decoration: 'decorationValue' }]);
-
- expect(controller.decorate).toHaveBeenCalled();
- });
- });
-
- describe('decorate', () => {
- it('sets decorations on editor instance', () => {
- spyOn(controller.editor.instance, 'deltaDecorations');
-
- controller.decorate(model);
-
- expect(controller.editor.instance.deltaDecorations).toHaveBeenCalledWith([], []);
- });
-
- it('caches decorations', () => {
- spyOn(controller.editor.instance, 'deltaDecorations').and.returnValue([]);
-
- controller.decorate(model);
-
- expect(controller.editorDecorations.size).toBe(1);
- });
-
- it('caches decorations by model URL', () => {
- spyOn(controller.editor.instance, 'deltaDecorations').and.returnValue([]);
-
- controller.decorate(model);
-
- expect(controller.editorDecorations.keys().next().value).toBe('gitlab:path--path');
- });
- });
-
- describe('dispose', () => {
- it('clears cached decorations', () => {
- controller.addDecorations(model, 'key', [{ decoration: 'decorationValue' }]);
-
- controller.dispose();
-
- expect(controller.decorations.size).toBe(0);
- });
-
- it('clears cached editorDecorations', () => {
- controller.addDecorations(model, 'key', [{ decoration: 'decorationValue' }]);
-
- controller.dispose();
-
- expect(controller.editorDecorations.size).toBe(0);
- });
- });
-
- describe('hasDecorations', () => {
- it('returns true when decorations are cached', () => {
- controller.addDecorations(model, 'key', [{ decoration: 'decorationValue' }]);
-
- expect(controller.hasDecorations(model)).toBe(true);
- });
-
- it('returns false when no model decorations exist', () => {
- expect(controller.hasDecorations(model)).toBe(false);
- });
- });
-
- describe('removeDecorations', () => {
- beforeEach(() => {
- controller.addDecorations(model, 'key', [{ decoration: 'decorationValue' }]);
- controller.decorate(model);
- });
-
- it('removes cached decorations', () => {
- expect(controller.decorations.size).not.toBe(0);
- expect(controller.editorDecorations.size).not.toBe(0);
-
- controller.removeDecorations(model);
-
- expect(controller.decorations.size).toBe(0);
- expect(controller.editorDecorations.size).toBe(0);
- });
- });
-});
diff --git a/spec/javascripts/ide/lib/diff/controller_spec.js b/spec/javascripts/ide/lib/diff/controller_spec.js
deleted file mode 100644
index 90ebb95b687..00000000000
--- a/spec/javascripts/ide/lib/diff/controller_spec.js
+++ /dev/null
@@ -1,215 +0,0 @@
-import { Range } from 'monaco-editor';
-import Editor from '~/ide/lib/editor';
-import ModelManager from '~/ide/lib/common/model_manager';
-import DecorationsController from '~/ide/lib/decorations/controller';
-import DirtyDiffController, { getDiffChangeType, getDecorator } from '~/ide/lib/diff/controller';
-import { computeDiff } from '~/ide/lib/diff/diff';
-import { file } from '../../helpers';
-
-describe('Multi-file editor library dirty diff controller', () => {
- let editorInstance;
- let controller;
- let modelManager;
- let decorationsController;
- let model;
-
- beforeEach(() => {
- editorInstance = Editor.create();
- editorInstance.createInstance(document.createElement('div'));
-
- modelManager = new ModelManager();
- decorationsController = new DecorationsController(editorInstance);
-
- model = modelManager.addModel(file('path'));
-
- controller = new DirtyDiffController(modelManager, decorationsController);
- });
-
- afterEach(() => {
- controller.dispose();
- model.dispose();
- decorationsController.dispose();
- editorInstance.dispose();
- });
-
- describe('getDiffChangeType', () => {
- ['added', 'removed', 'modified'].forEach(type => {
- it(`returns ${type}`, () => {
- const change = {
- [type]: true,
- };
-
- expect(getDiffChangeType(change)).toBe(type);
- });
- });
- });
-
- describe('getDecorator', () => {
- ['added', 'removed', 'modified'].forEach(type => {
- it(`returns with linesDecorationsClassName for ${type}`, () => {
- const change = {
- [type]: true,
- };
-
- expect(getDecorator(change).options.linesDecorationsClassName).toBe(
- `dirty-diff dirty-diff-${type}`,
- );
- });
-
- it('returns with line numbers', () => {
- const change = {
- lineNumber: 1,
- endLineNumber: 2,
- [type]: true,
- };
-
- const { range } = getDecorator(change);
-
- expect(range.startLineNumber).toBe(1);
- expect(range.endLineNumber).toBe(2);
- expect(range.startColumn).toBe(1);
- expect(range.endColumn).toBe(1);
- });
- });
- });
-
- describe('attachModel', () => {
- it('adds change event callback', () => {
- spyOn(model, 'onChange');
-
- controller.attachModel(model);
-
- expect(model.onChange).toHaveBeenCalled();
- });
-
- it('adds dispose event callback', () => {
- spyOn(model, 'onDispose');
-
- controller.attachModel(model);
-
- expect(model.onDispose).toHaveBeenCalled();
- });
-
- it('calls throttledComputeDiff on change', () => {
- spyOn(controller, 'throttledComputeDiff');
-
- controller.attachModel(model);
-
- model.getModel().setValue('123');
-
- expect(controller.throttledComputeDiff).toHaveBeenCalled();
- });
-
- it('caches model', () => {
- controller.attachModel(model);
-
- expect(controller.models.has(model.url)).toBe(true);
- });
- });
-
- describe('computeDiff', () => {
- it('posts to worker', () => {
- spyOn(controller.dirtyDiffWorker, 'postMessage');
-
- controller.computeDiff(model);
-
- expect(controller.dirtyDiffWorker.postMessage).toHaveBeenCalledWith({
- path: model.path,
- originalContent: '',
- newContent: '',
- });
- });
- });
-
- describe('reDecorate', () => {
- it('calls computeDiff when no decorations are cached', () => {
- spyOn(controller, 'computeDiff');
-
- controller.reDecorate(model);
-
- expect(controller.computeDiff).toHaveBeenCalledWith(model);
- });
-
- it('calls decorate when decorations are cached', () => {
- spyOn(controller.decorationsController, 'decorate');
-
- controller.decorationsController.decorations.set(model.url, 'test');
-
- controller.reDecorate(model);
-
- expect(controller.decorationsController.decorate).toHaveBeenCalledWith(model);
- });
- });
-
- describe('decorate', () => {
- it('adds decorations into decorations controller', () => {
- spyOn(controller.decorationsController, 'addDecorations');
-
- controller.decorate({ data: { changes: [], path: model.path } });
-
- expect(controller.decorationsController.addDecorations).toHaveBeenCalledWith(
- model,
- 'dirtyDiff',
- jasmine.anything(),
- );
- });
-
- it('adds decorations into editor', () => {
- const spy = spyOn(controller.decorationsController.editor.instance, 'deltaDecorations');
-
- controller.decorate({
- data: { changes: computeDiff('123', '1234'), path: model.path },
- });
-
- expect(spy).toHaveBeenCalledWith(
- [],
- [
- {
- range: new Range(1, 1, 1, 1),
- options: {
- isWholeLine: true,
- linesDecorationsClassName: 'dirty-diff dirty-diff-modified',
- },
- },
- ],
- );
- });
- });
-
- describe('dispose', () => {
- it('calls disposable dispose', () => {
- spyOn(controller.disposable, 'dispose').and.callThrough();
-
- controller.dispose();
-
- expect(controller.disposable.dispose).toHaveBeenCalled();
- });
-
- it('terminates worker', () => {
- spyOn(controller.dirtyDiffWorker, 'terminate').and.callThrough();
-
- controller.dispose();
-
- expect(controller.dirtyDiffWorker.terminate).toHaveBeenCalled();
- });
-
- it('removes worker event listener', () => {
- spyOn(controller.dirtyDiffWorker, 'removeEventListener').and.callThrough();
-
- controller.dispose();
-
- expect(controller.dirtyDiffWorker.removeEventListener).toHaveBeenCalledWith(
- 'message',
- jasmine.anything(),
- );
- });
-
- it('clears cached models', () => {
- controller.attachModel(model);
-
- model.dispose();
-
- expect(controller.models.size).toBe(0);
- });
- });
-});
diff --git a/spec/javascripts/ide/lib/editor_spec.js b/spec/javascripts/ide/lib/editor_spec.js
deleted file mode 100644
index 556bd45d3a5..00000000000
--- a/spec/javascripts/ide/lib/editor_spec.js
+++ /dev/null
@@ -1,287 +0,0 @@
-import { editor as monacoEditor } from 'monaco-editor';
-import Editor from '~/ide/lib/editor';
-import { file } from '../helpers';
-
-describe('Multi-file editor library', () => {
- let instance;
- let el;
- let holder;
-
- beforeEach(() => {
- el = document.createElement('div');
- holder = document.createElement('div');
- el.appendChild(holder);
-
- document.body.appendChild(el);
-
- instance = Editor.create();
- });
-
- afterEach(() => {
- instance.dispose();
-
- el.remove();
- });
-
- it('creates instance of editor', () => {
- expect(Editor.editorInstance).not.toBeNull();
- });
-
- it('creates instance returns cached instance', () => {
- expect(Editor.create()).toEqual(instance);
- });
-
- describe('createInstance', () => {
- it('creates editor instance', () => {
- spyOn(monacoEditor, 'create').and.callThrough();
-
- instance.createInstance(holder);
-
- expect(monacoEditor.create).toHaveBeenCalled();
- });
-
- it('creates dirty diff controller', () => {
- instance.createInstance(holder);
-
- expect(instance.dirtyDiffController).not.toBeNull();
- });
-
- it('creates model manager', () => {
- instance.createInstance(holder);
-
- expect(instance.modelManager).not.toBeNull();
- });
- });
-
- describe('createDiffInstance', () => {
- it('creates editor instance', () => {
- spyOn(monacoEditor, 'createDiffEditor').and.callThrough();
-
- instance.createDiffInstance(holder);
-
- expect(monacoEditor.createDiffEditor).toHaveBeenCalledWith(holder, {
- model: null,
- contextmenu: true,
- minimap: {
- enabled: false,
- },
- readOnly: true,
- scrollBeyondLastLine: false,
- renderWhitespace: 'none',
- quickSuggestions: false,
- occurrencesHighlight: false,
- wordWrap: 'on',
- renderSideBySide: true,
- renderLineHighlight: 'all',
- hideCursorInOverviewRuler: false,
- theme: 'vs white',
- });
- });
- });
-
- describe('createModel', () => {
- it('calls model manager addModel', () => {
- spyOn(instance.modelManager, 'addModel');
-
- instance.createModel('FILE');
-
- expect(instance.modelManager.addModel).toHaveBeenCalledWith('FILE', null);
- });
- });
-
- describe('attachModel', () => {
- let model;
-
- beforeEach(() => {
- instance.createInstance(document.createElement('div'));
-
- model = instance.createModel(file());
- });
-
- it('sets the current model on the instance', () => {
- instance.attachModel(model);
-
- expect(instance.currentModel).toBe(model);
- });
-
- it('attaches the model to the current instance', () => {
- spyOn(instance.instance, 'setModel');
-
- instance.attachModel(model);
-
- expect(instance.instance.setModel).toHaveBeenCalledWith(model.getModel());
- });
-
- it('sets original & modified when diff editor', () => {
- spyOn(instance.instance, 'getEditorType').and.returnValue('vs.editor.IDiffEditor');
- spyOn(instance.instance, 'setModel');
-
- instance.attachModel(model);
-
- expect(instance.instance.setModel).toHaveBeenCalledWith({
- original: model.getOriginalModel(),
- modified: model.getModel(),
- });
- });
-
- it('attaches the model to the dirty diff controller', () => {
- spyOn(instance.dirtyDiffController, 'attachModel');
-
- instance.attachModel(model);
-
- expect(instance.dirtyDiffController.attachModel).toHaveBeenCalledWith(model);
- });
-
- it('re-decorates with the dirty diff controller', () => {
- spyOn(instance.dirtyDiffController, 'reDecorate');
-
- instance.attachModel(model);
-
- expect(instance.dirtyDiffController.reDecorate).toHaveBeenCalledWith(model);
- });
- });
-
- describe('attachMergeRequestModel', () => {
- let model;
-
- beforeEach(() => {
- instance.createDiffInstance(document.createElement('div'));
-
- const f = file();
- f.mrChanges = { diff: 'ABC' };
- f.baseRaw = 'testing';
-
- model = instance.createModel(f);
- });
-
- it('sets original & modified', () => {
- spyOn(instance.instance, 'setModel');
-
- instance.attachMergeRequestModel(model);
-
- expect(instance.instance.setModel).toHaveBeenCalledWith({
- original: model.getBaseModel(),
- modified: model.getModel(),
- });
- });
- });
-
- describe('clearEditor', () => {
- it('resets the editor model', () => {
- instance.createInstance(document.createElement('div'));
-
- spyOn(instance.instance, 'setModel');
-
- instance.clearEditor();
-
- expect(instance.instance.setModel).toHaveBeenCalledWith(null);
- });
- });
-
- describe('dispose', () => {
- it('calls disposble dispose method', () => {
- spyOn(instance.disposable, 'dispose').and.callThrough();
-
- instance.dispose();
-
- expect(instance.disposable.dispose).toHaveBeenCalled();
- });
-
- it('resets instance', () => {
- instance.createInstance(document.createElement('div'));
-
- expect(instance.instance).not.toBeNull();
-
- instance.dispose();
-
- expect(instance.instance).toBeNull();
- });
-
- it('does not dispose modelManager', () => {
- spyOn(instance.modelManager, 'dispose');
-
- instance.dispose();
-
- expect(instance.modelManager.dispose).not.toHaveBeenCalled();
- });
-
- it('does not dispose decorationsController', () => {
- spyOn(instance.decorationsController, 'dispose');
-
- instance.dispose();
-
- expect(instance.decorationsController.dispose).not.toHaveBeenCalled();
- });
- });
-
- describe('updateDiffView', () => {
- describe('edit mode', () => {
- it('does not update options', () => {
- instance.createInstance(holder);
-
- spyOn(instance.instance, 'updateOptions');
-
- instance.updateDiffView();
-
- expect(instance.instance.updateOptions).not.toHaveBeenCalled();
- });
- });
-
- describe('diff mode', () => {
- beforeEach(() => {
- instance.createDiffInstance(holder);
-
- spyOn(instance.instance, 'updateOptions').and.callThrough();
- });
-
- it('sets renderSideBySide to false if el is less than 700 pixels', () => {
- spyOnProperty(instance.instance.getDomNode(), 'offsetWidth').and.returnValue(600);
-
- expect(instance.instance.updateOptions).not.toHaveBeenCalledWith({
- renderSideBySide: false,
- });
- });
-
- it('sets renderSideBySide to false if el is more than 700 pixels', () => {
- spyOnProperty(instance.instance.getDomNode(), 'offsetWidth').and.returnValue(800);
-
- expect(instance.instance.updateOptions).not.toHaveBeenCalledWith({
- renderSideBySide: true,
- });
- });
- });
- });
-
- describe('isDiffEditorType', () => {
- it('returns true when diff editor', () => {
- instance.createDiffInstance(holder);
-
- expect(instance.isDiffEditorType).toBe(true);
- });
-
- it('returns false when not diff editor', () => {
- instance.createInstance(holder);
-
- expect(instance.isDiffEditorType).toBe(false);
- });
- });
-
- it('sets quickSuggestions to false when language is markdown', () => {
- instance.createInstance(holder);
-
- spyOn(instance.instance, 'updateOptions').and.callThrough();
-
- const model = instance.createModel({
- ...file(),
- key: 'index.md',
- path: 'index.md',
- });
-
- instance.attachModel(model);
-
- expect(instance.instance.updateOptions).toHaveBeenCalledWith({
- readOnly: false,
- quickSuggestions: false,
- });
- });
-});
diff --git a/spec/javascripts/ide/stores/actions/tree_spec.js b/spec/javascripts/ide/stores/actions/tree_spec.js
index fabe44ce333..2201a3b4b57 100644
--- a/spec/javascripts/ide/stores/actions/tree_spec.js
+++ b/spec/javascripts/ide/stores/actions/tree_spec.js
@@ -30,6 +30,7 @@ describe('Multi-file store tree actions', () => {
store.state.currentBranchId = 'master';
store.state.projects.abcproject = {
web_url: '',
+ path_with_namespace: 'foo/abcproject',
};
});
@@ -57,7 +58,7 @@ describe('Multi-file store tree actions', () => {
store
.dispatch('getFiles', basicCallParameters)
.then(() => {
- expect(service.getFiles).toHaveBeenCalledWith('', '12345678');
+ expect(service.getFiles).toHaveBeenCalledWith('foo/abcproject', '12345678');
done();
})
diff --git a/spec/javascripts/image_diff/helpers/badge_helper_spec.js b/spec/javascripts/image_diff/helpers/badge_helper_spec.js
deleted file mode 100644
index b3001d45e3c..00000000000
--- a/spec/javascripts/image_diff/helpers/badge_helper_spec.js
+++ /dev/null
@@ -1,130 +0,0 @@
-import * as badgeHelper from '~/image_diff/helpers/badge_helper';
-import * as mockData from '../mock_data';
-
-describe('badge helper', () => {
- const { coordinate, noteId, badgeText, badgeNumber } = mockData;
- let containerEl;
- let buttonEl;
-
- beforeEach(() => {
- containerEl = document.createElement('div');
- });
-
- describe('createImageBadge', () => {
- beforeEach(() => {
- buttonEl = badgeHelper.createImageBadge(noteId, coordinate);
- });
-
- it('should create button', () => {
- expect(buttonEl.tagName).toEqual('BUTTON');
- expect(buttonEl.getAttribute('type')).toEqual('button');
- });
-
- it('should set disabled attribute', () => {
- expect(buttonEl.hasAttribute('disabled')).toEqual(true);
- });
-
- it('should set noteId', () => {
- expect(buttonEl.dataset.noteId).toEqual(noteId);
- });
-
- it('should set coordinate', () => {
- expect(buttonEl.style.left).toEqual(`${coordinate.x}px`);
- expect(buttonEl.style.top).toEqual(`${coordinate.y}px`);
- });
-
- describe('classNames', () => {
- it('should set .js-image-badge by default', () => {
- expect(buttonEl.className).toEqual('js-image-badge');
- });
-
- it('should add additional class names if parameter is passed', () => {
- const classNames = ['first-class', 'second-class'];
- buttonEl = badgeHelper.createImageBadge(noteId, coordinate, classNames);
-
- expect(buttonEl.className).toEqual(classNames.concat('js-image-badge').join(' '));
- });
- });
- });
-
- describe('addImageBadge', () => {
- beforeEach(() => {
- badgeHelper.addImageBadge(containerEl, {
- coordinate,
- badgeText,
- noteId,
- });
- buttonEl = containerEl.querySelector('button');
- });
-
- it('should appends button to container', () => {
- expect(buttonEl).toBeDefined();
- });
-
- it('should add badge classes', () => {
- expect(buttonEl.className).toContain('badge badge-pill');
- });
-
- it('should set the badge text', () => {
- expect(buttonEl.innerText).toEqual(badgeText);
- });
-
- it('should set the button coordinates', () => {
- expect(buttonEl.style.left).toEqual(`${coordinate.x}px`);
- expect(buttonEl.style.top).toEqual(`${coordinate.y}px`);
- });
-
- it('should set the button noteId', () => {
- expect(buttonEl.dataset.noteId).toEqual(noteId);
- });
- });
-
- describe('addImageCommentBadge', () => {
- beforeEach(() => {
- badgeHelper.addImageCommentBadge(containerEl, {
- coordinate,
- noteId,
- });
- buttonEl = containerEl.querySelector('button');
- });
-
- it('should append icon button to container', () => {
- expect(buttonEl).toBeDefined();
- });
-
- it('should create icon comment button', () => {
- const iconEl = buttonEl.querySelector('svg');
-
- expect(iconEl).toBeDefined();
- });
- });
-
- describe('addAvatarBadge', () => {
- let avatarBadgeEl;
-
- beforeEach(() => {
- containerEl.innerHTML = `
- <div id="${noteId}">
- <div class="badge hidden">
- </div>
- </div>
- `;
-
- badgeHelper.addAvatarBadge(containerEl, {
- detail: {
- noteId,
- badgeNumber,
- },
- });
- avatarBadgeEl = containerEl.querySelector(`#${noteId} .badge`);
- });
-
- it('should update badge number', () => {
- expect(avatarBadgeEl.innerText).toEqual(badgeNumber.toString());
- });
-
- it('should remove hidden class', () => {
- expect(avatarBadgeEl.classList.contains('hidden')).toEqual(false);
- });
- });
-});
diff --git a/spec/javascripts/image_diff/helpers/comment_indicator_helper_spec.js b/spec/javascripts/image_diff/helpers/comment_indicator_helper_spec.js
deleted file mode 100644
index 8e3e7f1222e..00000000000
--- a/spec/javascripts/image_diff/helpers/comment_indicator_helper_spec.js
+++ /dev/null
@@ -1,144 +0,0 @@
-import * as commentIndicatorHelper from '~/image_diff/helpers/comment_indicator_helper';
-import * as mockData from '../mock_data';
-
-describe('commentIndicatorHelper', () => {
- const { coordinate } = mockData;
- let containerEl;
-
- beforeEach(() => {
- containerEl = document.createElement('div');
- });
-
- describe('addCommentIndicator', () => {
- let buttonEl;
-
- beforeEach(() => {
- commentIndicatorHelper.addCommentIndicator(containerEl, coordinate);
- buttonEl = containerEl.querySelector('button');
- });
-
- it('should append button to container', () => {
- expect(buttonEl).toBeDefined();
- });
-
- describe('button', () => {
- it('should set coordinate', () => {
- expect(buttonEl.style.left).toEqual(`${coordinate.x}px`);
- expect(buttonEl.style.top).toEqual(`${coordinate.y}px`);
- });
-
- it('should contain image-comment-dark svg', () => {
- const svgEl = buttonEl.querySelector('svg');
-
- expect(svgEl).toBeDefined();
-
- const svgLink = svgEl.querySelector('use').getAttribute('xlink:href');
-
- expect(svgLink.indexOf('image-comment-dark')).not.toBe(-1);
- });
- });
- });
-
- describe('removeCommentIndicator', () => {
- it('should return removed false if there is no comment-indicator', () => {
- const result = commentIndicatorHelper.removeCommentIndicator(containerEl);
-
- expect(result.removed).toEqual(false);
- });
-
- describe('has comment indicator', () => {
- let result;
-
- beforeEach(() => {
- containerEl.innerHTML = `
- <div class="comment-indicator" style="left:${coordinate.x}px; top: ${coordinate.y}px;">
- <img src="${gl.TEST_HOST}/image.png">
- </div>
- `;
- result = commentIndicatorHelper.removeCommentIndicator(containerEl);
- });
-
- it('should remove comment indicator', () => {
- expect(containerEl.querySelector('.comment-indicator')).toBeNull();
- });
-
- it('should return removed true', () => {
- expect(result.removed).toEqual(true);
- });
-
- it('should return indicator meta', () => {
- expect(result.x).toEqual(coordinate.x);
- expect(result.y).toEqual(coordinate.y);
- expect(result.image).toBeDefined();
- expect(result.image.width).toBeDefined();
- expect(result.image.height).toBeDefined();
- });
- });
- });
-
- describe('showCommentIndicator', () => {
- describe('commentIndicator exists', () => {
- beforeEach(() => {
- containerEl.innerHTML = `
- <button class="comment-indicator"></button>
- `;
- commentIndicatorHelper.showCommentIndicator(containerEl, coordinate);
- });
-
- it('should set commentIndicator coordinates', () => {
- const commentIndicatorEl = containerEl.querySelector('.comment-indicator');
-
- expect(commentIndicatorEl.style.left).toEqual(`${coordinate.x}px`);
- expect(commentIndicatorEl.style.top).toEqual(`${coordinate.y}px`);
- });
- });
-
- describe('commentIndicator does not exist', () => {
- beforeEach(() => {
- commentIndicatorHelper.showCommentIndicator(containerEl, coordinate);
- });
-
- it('should addCommentIndicator', () => {
- const buttonEl = containerEl.querySelector('.comment-indicator');
-
- expect(buttonEl).toBeDefined();
- expect(buttonEl.style.left).toEqual(`${coordinate.x}px`);
- expect(buttonEl.style.top).toEqual(`${coordinate.y}px`);
- });
- });
- });
-
- describe('commentIndicatorOnClick', () => {
- let event;
- let textAreaEl;
-
- beforeEach(() => {
- containerEl.innerHTML = `
- <div class="diff-viewer">
- <button></button>
- <div class="note-container">
- <textarea class="note-textarea"></textarea>
- </div>
- </div>
- `;
- textAreaEl = containerEl.querySelector('textarea');
-
- event = {
- stopPropagation: () => {},
- currentTarget: containerEl.querySelector('button'),
- };
-
- spyOn(event, 'stopPropagation');
- spyOn(textAreaEl, 'focus');
- commentIndicatorHelper.commentIndicatorOnClick(event);
- });
-
- it('should stopPropagation', () => {
- expect(event.stopPropagation).toHaveBeenCalled();
- });
-
- it('should focus textAreaEl', () => {
- expect(textAreaEl.focus).toHaveBeenCalled();
- });
- });
-});
diff --git a/spec/javascripts/image_diff/helpers/dom_helper_spec.js b/spec/javascripts/image_diff/helpers/dom_helper_spec.js
deleted file mode 100644
index ffe712af2dd..00000000000
--- a/spec/javascripts/image_diff/helpers/dom_helper_spec.js
+++ /dev/null
@@ -1,120 +0,0 @@
-import * as domHelper from '~/image_diff/helpers/dom_helper';
-import * as mockData from '../mock_data';
-
-describe('domHelper', () => {
- const { imageMeta, badgeNumber } = mockData;
-
- describe('setPositionDataAttribute', () => {
- let containerEl;
- let attributeAfterCall;
- const position = {
- myProperty: 'myProperty',
- };
-
- beforeEach(() => {
- containerEl = document.createElement('div');
- containerEl.dataset.position = JSON.stringify(position);
- domHelper.setPositionDataAttribute(containerEl, imageMeta);
- attributeAfterCall = JSON.parse(containerEl.dataset.position);
- });
-
- it('should set x, y, width, height', () => {
- expect(attributeAfterCall.x).toEqual(imageMeta.x);
- expect(attributeAfterCall.y).toEqual(imageMeta.y);
- expect(attributeAfterCall.width).toEqual(imageMeta.width);
- expect(attributeAfterCall.height).toEqual(imageMeta.height);
- });
-
- it('should not override other properties', () => {
- expect(attributeAfterCall.myProperty).toEqual('myProperty');
- });
- });
-
- describe('updateDiscussionAvatarBadgeNumber', () => {
- let discussionEl;
-
- beforeEach(() => {
- discussionEl = document.createElement('div');
- discussionEl.innerHTML = `
- <a href="#" class="image-diff-avatar-link">
- <div class="badge"></div>
- </a>
- `;
- domHelper.updateDiscussionAvatarBadgeNumber(discussionEl, badgeNumber);
- });
-
- it('should update avatar badge number', () => {
- expect(discussionEl.querySelector('.badge').innerText).toEqual(badgeNumber.toString());
- });
- });
-
- describe('updateDiscussionBadgeNumber', () => {
- let discussionEl;
-
- beforeEach(() => {
- discussionEl = document.createElement('div');
- discussionEl.innerHTML = `
- <div class="badge"></div>
- `;
- domHelper.updateDiscussionBadgeNumber(discussionEl, badgeNumber);
- });
-
- it('should update discussion badge number', () => {
- expect(discussionEl.querySelector('.badge').innerText).toEqual(badgeNumber.toString());
- });
- });
-
- describe('toggleCollapsed', () => {
- let element;
- let discussionNotesEl;
-
- beforeEach(() => {
- element = document.createElement('div');
- element.innerHTML = `
- <div class="discussion-notes">
- <button></button>
- <form class="discussion-form"></form>
- </div>
- `;
- discussionNotesEl = element.querySelector('.discussion-notes');
- });
-
- describe('not collapsed', () => {
- beforeEach(() => {
- domHelper.toggleCollapsed({
- currentTarget: element.querySelector('button'),
- });
- });
-
- it('should add collapsed class', () => {
- expect(discussionNotesEl.classList.contains('collapsed')).toEqual(true);
- });
-
- it('should force formEl to display none', () => {
- const formEl = element.querySelector('.discussion-form');
-
- expect(formEl.style.display).toEqual('none');
- });
- });
-
- describe('collapsed', () => {
- beforeEach(() => {
- discussionNotesEl.classList.add('collapsed');
-
- domHelper.toggleCollapsed({
- currentTarget: element.querySelector('button'),
- });
- });
-
- it('should remove collapsed class', () => {
- expect(discussionNotesEl.classList.contains('collapsed')).toEqual(false);
- });
-
- it('should force formEl to display block', () => {
- const formEl = element.querySelector('.discussion-form');
-
- expect(formEl.style.display).toEqual('block');
- });
- });
- });
-});
diff --git a/spec/javascripts/image_diff/image_badge_spec.js b/spec/javascripts/image_diff/image_badge_spec.js
deleted file mode 100644
index 2b23dce5d30..00000000000
--- a/spec/javascripts/image_diff/image_badge_spec.js
+++ /dev/null
@@ -1,96 +0,0 @@
-import ImageBadge from '~/image_diff/image_badge';
-import imageDiffHelper from '~/image_diff/helpers/index';
-import * as mockData from './mock_data';
-
-describe('ImageBadge', () => {
- const { noteId, discussionId, imageMeta } = mockData;
- const options = {
- noteId,
- discussionId,
- };
-
- it('should save actual property', () => {
- const imageBadge = new ImageBadge(
- Object.assign({}, options, {
- actual: imageMeta,
- }),
- );
-
- const { actual } = imageBadge;
-
- expect(actual.x).toEqual(imageMeta.x);
- expect(actual.y).toEqual(imageMeta.y);
- expect(actual.width).toEqual(imageMeta.width);
- expect(actual.height).toEqual(imageMeta.height);
- });
-
- it('should save browser property', () => {
- const imageBadge = new ImageBadge(
- Object.assign({}, options, {
- browser: imageMeta,
- }),
- );
-
- const { browser } = imageBadge;
-
- expect(browser.x).toEqual(imageMeta.x);
- expect(browser.y).toEqual(imageMeta.y);
- expect(browser.width).toEqual(imageMeta.width);
- expect(browser.height).toEqual(imageMeta.height);
- });
-
- it('should save noteId', () => {
- const imageBadge = new ImageBadge(options);
-
- expect(imageBadge.noteId).toEqual(noteId);
- });
-
- it('should save discussionId', () => {
- const imageBadge = new ImageBadge(options);
-
- expect(imageBadge.discussionId).toEqual(discussionId);
- });
-
- describe('default values', () => {
- let imageBadge;
-
- beforeEach(() => {
- imageBadge = new ImageBadge(options);
- });
-
- it('should return defaultimageMeta if actual property is not provided', () => {
- const { actual } = imageBadge;
-
- expect(actual.x).toEqual(0);
- expect(actual.y).toEqual(0);
- expect(actual.width).toEqual(0);
- expect(actual.height).toEqual(0);
- });
-
- it('should return defaultimageMeta if browser property is not provided', () => {
- const { browser } = imageBadge;
-
- expect(browser.x).toEqual(0);
- expect(browser.y).toEqual(0);
- expect(browser.width).toEqual(0);
- expect(browser.height).toEqual(0);
- });
- });
-
- describe('imageEl property is provided and not browser property', () => {
- beforeEach(() => {
- spyOn(imageDiffHelper, 'resizeCoordinatesToImageElement').and.returnValue(true);
- });
-
- it('should generate browser property', () => {
- const imageBadge = new ImageBadge(
- Object.assign({}, options, {
- imageEl: document.createElement('img'),
- }),
- );
-
- expect(imageDiffHelper.resizeCoordinatesToImageElement).toHaveBeenCalled();
- expect(imageBadge.browser).toEqual(true);
- });
- });
-});
diff --git a/spec/javascripts/image_diff/image_diff_spec.js b/spec/javascripts/image_diff/image_diff_spec.js
deleted file mode 100644
index 21e7b8e2e9b..00000000000
--- a/spec/javascripts/image_diff/image_diff_spec.js
+++ /dev/null
@@ -1,361 +0,0 @@
-import ImageDiff from '~/image_diff/image_diff';
-import * as imageUtility from '~/lib/utils/image_utility';
-import imageDiffHelper from '~/image_diff/helpers/index';
-import * as mockData from './mock_data';
-
-describe('ImageDiff', () => {
- let element;
- let imageDiff;
-
- beforeEach(() => {
- setFixtures(`
- <div id="element">
- <div class="diff-file">
- <div class="js-image-frame">
- <img src="${gl.TEST_HOST}/image.png">
- <div class="comment-indicator"></div>
- <div id="badge-1" class="badge">1</div>
- <div id="badge-2" class="badge">2</div>
- <div id="badge-3" class="badge">3</div>
- </div>
- <div class="note-container">
- <div class="discussion-notes">
- <div class="js-diff-notes-toggle"></div>
- <div class="notes"></div>
- </div>
- <div class="discussion-notes">
- <div class="js-diff-notes-toggle"></div>
- <div class="notes"></div>
- </div>
- </div>
- </div>
- </div>
- `);
- element = document.getElementById('element');
- });
-
- describe('constructor', () => {
- beforeEach(() => {
- imageDiff = new ImageDiff(element, {
- canCreateNote: true,
- renderCommentBadge: true,
- });
- });
-
- it('should set el', () => {
- expect(imageDiff.el).toEqual(element);
- });
-
- it('should set canCreateNote', () => {
- expect(imageDiff.canCreateNote).toEqual(true);
- });
-
- it('should set renderCommentBadge', () => {
- expect(imageDiff.renderCommentBadge).toEqual(true);
- });
-
- it('should set $noteContainer', () => {
- expect(imageDiff.$noteContainer[0]).toEqual(element.querySelector('.note-container'));
- });
-
- describe('default', () => {
- beforeEach(() => {
- imageDiff = new ImageDiff(element);
- });
-
- it('should set canCreateNote as false', () => {
- expect(imageDiff.canCreateNote).toEqual(false);
- });
-
- it('should set renderCommentBadge as false', () => {
- expect(imageDiff.renderCommentBadge).toEqual(false);
- });
- });
- });
-
- describe('init', () => {
- beforeEach(() => {
- spyOn(ImageDiff.prototype, 'bindEvents').and.callFake(() => {});
- imageDiff = new ImageDiff(element);
- imageDiff.init();
- });
-
- it('should set imageFrameEl', () => {
- expect(imageDiff.imageFrameEl).toEqual(element.querySelector('.diff-file .js-image-frame'));
- });
-
- it('should set imageEl', () => {
- expect(imageDiff.imageEl).toEqual(element.querySelector('.diff-file .js-image-frame img'));
- });
-
- it('should call bindEvents', () => {
- expect(imageDiff.bindEvents).toHaveBeenCalled();
- });
- });
-
- describe('bindEvents', () => {
- let imageEl;
-
- beforeEach(() => {
- spyOn(imageDiffHelper, 'toggleCollapsed').and.callFake(() => {});
- spyOn(imageDiffHelper, 'commentIndicatorOnClick').and.callFake(() => {});
- spyOn(imageDiffHelper, 'removeCommentIndicator').and.callFake(() => {});
- spyOn(ImageDiff.prototype, 'imageClicked').and.callFake(() => {});
- spyOn(ImageDiff.prototype, 'addBadge').and.callFake(() => {});
- spyOn(ImageDiff.prototype, 'removeBadge').and.callFake(() => {});
- spyOn(ImageDiff.prototype, 'renderBadges').and.callFake(() => {});
- imageEl = element.querySelector('.diff-file .js-image-frame img');
- });
-
- describe('default', () => {
- beforeEach(() => {
- spyOn(imageUtility, 'isImageLoaded').and.returnValue(false);
- imageDiff = new ImageDiff(element);
- imageDiff.imageEl = imageEl;
- imageDiff.bindEvents();
- });
-
- it('should register click event delegation to js-diff-notes-toggle', () => {
- element.querySelector('.js-diff-notes-toggle').click();
-
- expect(imageDiffHelper.toggleCollapsed).toHaveBeenCalled();
- });
-
- it('should register click event delegation to comment-indicator', () => {
- element.querySelector('.comment-indicator').click();
-
- expect(imageDiffHelper.commentIndicatorOnClick).toHaveBeenCalled();
- });
- });
-
- describe('image not loaded', () => {
- beforeEach(() => {
- spyOn(imageUtility, 'isImageLoaded').and.returnValue(false);
- imageDiff = new ImageDiff(element);
- imageDiff.imageEl = imageEl;
- imageDiff.bindEvents();
- });
-
- it('should registers load eventListener', () => {
- const loadEvent = new Event('load');
- imageEl.dispatchEvent(loadEvent);
-
- expect(imageDiff.renderBadges).toHaveBeenCalled();
- });
- });
-
- describe('canCreateNote', () => {
- beforeEach(() => {
- spyOn(imageUtility, 'isImageLoaded').and.returnValue(false);
- imageDiff = new ImageDiff(element, {
- canCreateNote: true,
- });
- imageDiff.imageEl = imageEl;
- imageDiff.bindEvents();
- });
-
- it('should register click.imageDiff event', () => {
- const event = new CustomEvent('click.imageDiff');
- element.dispatchEvent(event);
-
- expect(imageDiff.imageClicked).toHaveBeenCalled();
- });
-
- it('should register blur.imageDiff event', () => {
- const event = new CustomEvent('blur.imageDiff');
- element.dispatchEvent(event);
-
- expect(imageDiffHelper.removeCommentIndicator).toHaveBeenCalled();
- });
-
- it('should register addBadge.imageDiff event', () => {
- const event = new CustomEvent('addBadge.imageDiff');
- element.dispatchEvent(event);
-
- expect(imageDiff.addBadge).toHaveBeenCalled();
- });
-
- it('should register removeBadge.imageDiff event', () => {
- const event = new CustomEvent('removeBadge.imageDiff');
- element.dispatchEvent(event);
-
- expect(imageDiff.removeBadge).toHaveBeenCalled();
- });
- });
-
- describe('canCreateNote is false', () => {
- beforeEach(() => {
- spyOn(imageUtility, 'isImageLoaded').and.returnValue(false);
- imageDiff = new ImageDiff(element);
- imageDiff.imageEl = imageEl;
- imageDiff.bindEvents();
- });
-
- it('should not register click.imageDiff event', () => {
- const event = new CustomEvent('click.imageDiff');
- element.dispatchEvent(event);
-
- expect(imageDiff.imageClicked).not.toHaveBeenCalled();
- });
- });
- });
-
- describe('imageClicked', () => {
- beforeEach(() => {
- spyOn(imageDiffHelper, 'getTargetSelection').and.returnValue({
- actual: {},
- browser: {},
- });
- spyOn(imageDiffHelper, 'setPositionDataAttribute').and.callFake(() => {});
- spyOn(imageDiffHelper, 'showCommentIndicator').and.callFake(() => {});
- imageDiff = new ImageDiff(element);
- imageDiff.imageClicked({
- detail: {
- currentTarget: {},
- },
- });
- });
-
- it('should call getTargetSelection', () => {
- expect(imageDiffHelper.getTargetSelection).toHaveBeenCalled();
- });
-
- it('should call setPositionDataAttribute', () => {
- expect(imageDiffHelper.setPositionDataAttribute).toHaveBeenCalled();
- });
-
- it('should call showCommentIndicator', () => {
- expect(imageDiffHelper.showCommentIndicator).toHaveBeenCalled();
- });
- });
-
- describe('renderBadges', () => {
- beforeEach(() => {
- spyOn(ImageDiff.prototype, 'renderBadge').and.callFake(() => {});
- imageDiff = new ImageDiff(element);
- imageDiff.renderBadges();
- });
-
- it('should call renderBadge for each discussionEl', () => {
- const discussionEls = element.querySelectorAll('.note-container .discussion-notes .notes');
-
- expect(imageDiff.renderBadge.calls.count()).toEqual(discussionEls.length);
- });
- });
-
- describe('renderBadge', () => {
- let discussionEls;
-
- beforeEach(() => {
- spyOn(imageDiffHelper, 'addImageBadge').and.callFake(() => {});
- spyOn(imageDiffHelper, 'addImageCommentBadge').and.callFake(() => {});
- spyOn(imageDiffHelper, 'generateBadgeFromDiscussionDOM').and.returnValue({
- browser: {},
- noteId: 'noteId',
- });
- discussionEls = element.querySelectorAll('.note-container .discussion-notes .notes');
- imageDiff = new ImageDiff(element);
- imageDiff.renderBadge(discussionEls[0], 0);
- });
-
- it('should populate imageBadges', () => {
- expect(imageDiff.imageBadges.length).toEqual(1);
- });
-
- describe('renderCommentBadge', () => {
- beforeEach(() => {
- imageDiff.renderCommentBadge = true;
- imageDiff.renderBadge(discussionEls[0], 0);
- });
-
- it('should call addImageCommentBadge', () => {
- expect(imageDiffHelper.addImageCommentBadge).toHaveBeenCalled();
- });
- });
-
- describe('renderCommentBadge is false', () => {
- it('should call addImageBadge', () => {
- expect(imageDiffHelper.addImageBadge).toHaveBeenCalled();
- });
- });
- });
-
- describe('addBadge', () => {
- beforeEach(() => {
- spyOn(imageDiffHelper, 'addImageBadge').and.callFake(() => {});
- spyOn(imageDiffHelper, 'addAvatarBadge').and.callFake(() => {});
- spyOn(imageDiffHelper, 'updateDiscussionBadgeNumber').and.callFake(() => {});
- imageDiff = new ImageDiff(element);
- imageDiff.imageFrameEl = element.querySelector('.diff-file .js-image-frame');
- imageDiff.addBadge({
- detail: {
- x: 0,
- y: 1,
- width: 25,
- height: 50,
- noteId: 'noteId',
- discussionId: 'discussionId',
- },
- });
- });
-
- it('should add imageBadge to imageBadges', () => {
- expect(imageDiff.imageBadges.length).toEqual(1);
- });
-
- it('should call addImageBadge', () => {
- expect(imageDiffHelper.addImageBadge).toHaveBeenCalled();
- });
-
- it('should call addAvatarBadge', () => {
- expect(imageDiffHelper.addAvatarBadge).toHaveBeenCalled();
- });
-
- it('should call updateDiscussionBadgeNumber', () => {
- expect(imageDiffHelper.updateDiscussionBadgeNumber).toHaveBeenCalled();
- });
- });
-
- describe('removeBadge', () => {
- beforeEach(() => {
- const { imageMeta } = mockData;
-
- spyOn(imageDiffHelper, 'updateDiscussionBadgeNumber').and.callFake(() => {});
- spyOn(imageDiffHelper, 'updateDiscussionAvatarBadgeNumber').and.callFake(() => {});
- imageDiff = new ImageDiff(element);
- imageDiff.imageBadges = [imageMeta, imageMeta, imageMeta];
- imageDiff.imageFrameEl = element.querySelector('.diff-file .js-image-frame');
- imageDiff.removeBadge({
- detail: {
- badgeNumber: 2,
- },
- });
- });
-
- describe('cascade badge count', () => {
- it('should update next imageBadgeEl value', () => {
- const imageBadgeEls = imageDiff.imageFrameEl.querySelectorAll('.badge');
-
- expect(imageBadgeEls[0].innerText).toEqual('1');
- expect(imageBadgeEls[1].innerText).toEqual('2');
- expect(imageBadgeEls.length).toEqual(2);
- });
-
- it('should call updateDiscussionBadgeNumber', () => {
- expect(imageDiffHelper.updateDiscussionBadgeNumber).toHaveBeenCalled();
- });
-
- it('should call updateDiscussionAvatarBadgeNumber', () => {
- expect(imageDiffHelper.updateDiscussionAvatarBadgeNumber).toHaveBeenCalled();
- });
- });
-
- it('should remove badge from imageBadges', () => {
- expect(imageDiff.imageBadges.length).toEqual(2);
- });
-
- it('should remove imageBadgeEl', () => {
- expect(imageDiff.imageFrameEl.querySelector('#badge-2')).toBeNull();
- });
- });
-});
diff --git a/spec/javascripts/image_diff/replaced_image_diff_spec.js b/spec/javascripts/image_diff/replaced_image_diff_spec.js
deleted file mode 100644
index 62e7c8b6c6a..00000000000
--- a/spec/javascripts/image_diff/replaced_image_diff_spec.js
+++ /dev/null
@@ -1,355 +0,0 @@
-import ReplacedImageDiff from '~/image_diff/replaced_image_diff';
-import ImageDiff from '~/image_diff/image_diff';
-import { viewTypes } from '~/image_diff/view_types';
-import imageDiffHelper from '~/image_diff/helpers/index';
-
-describe('ReplacedImageDiff', () => {
- let element;
- let replacedImageDiff;
-
- beforeEach(() => {
- setFixtures(`
- <div id="element">
- <div class="two-up">
- <div class="js-image-frame">
- <img src="${gl.TEST_HOST}/image.png">
- </div>
- </div>
- <div class="swipe">
- <div class="js-image-frame">
- <img src="${gl.TEST_HOST}/image.png">
- </div>
- </div>
- <div class="onion-skin">
- <div class="js-image-frame">
- <img src="${gl.TEST_HOST}/image.png">
- </div>
- </div>
- <div class="view-modes-menu">
- <div class="two-up">2-up</div>
- <div class="swipe">Swipe</div>
- <div class="onion-skin">Onion skin</div>
- </div>
- </div>
- `);
- element = document.getElementById('element');
- });
-
- function setupImageFrameEls() {
- replacedImageDiff.imageFrameEls = [];
- replacedImageDiff.imageFrameEls[viewTypes.TWO_UP] = element.querySelector(
- '.two-up .js-image-frame',
- );
- replacedImageDiff.imageFrameEls[viewTypes.SWIPE] = element.querySelector(
- '.swipe .js-image-frame',
- );
- replacedImageDiff.imageFrameEls[viewTypes.ONION_SKIN] = element.querySelector(
- '.onion-skin .js-image-frame',
- );
- }
-
- function setupViewModesEls() {
- replacedImageDiff.viewModesEls = [];
- replacedImageDiff.viewModesEls[viewTypes.TWO_UP] = element.querySelector(
- '.view-modes-menu .two-up',
- );
- replacedImageDiff.viewModesEls[viewTypes.SWIPE] = element.querySelector(
- '.view-modes-menu .swipe',
- );
- replacedImageDiff.viewModesEls[viewTypes.ONION_SKIN] = element.querySelector(
- '.view-modes-menu .onion-skin',
- );
- }
-
- function setupImageEls() {
- replacedImageDiff.imageEls = [];
- replacedImageDiff.imageEls[viewTypes.TWO_UP] = element.querySelector('.two-up img');
- replacedImageDiff.imageEls[viewTypes.SWIPE] = element.querySelector('.swipe img');
- replacedImageDiff.imageEls[viewTypes.ONION_SKIN] = element.querySelector('.onion-skin img');
- }
-
- it('should extend ImageDiff', () => {
- replacedImageDiff = new ReplacedImageDiff(element);
-
- expect(replacedImageDiff instanceof ImageDiff).toEqual(true);
- });
-
- describe('init', () => {
- beforeEach(() => {
- spyOn(ReplacedImageDiff.prototype, 'bindEvents').and.callFake(() => {});
- spyOn(ReplacedImageDiff.prototype, 'generateImageEls').and.callFake(() => {});
-
- replacedImageDiff = new ReplacedImageDiff(element);
- replacedImageDiff.init();
- });
-
- it('should set imageFrameEls', () => {
- const { imageFrameEls } = replacedImageDiff;
-
- expect(imageFrameEls).toBeDefined();
- expect(imageFrameEls[viewTypes.TWO_UP]).toEqual(
- element.querySelector('.two-up .js-image-frame'),
- );
-
- expect(imageFrameEls[viewTypes.SWIPE]).toEqual(
- element.querySelector('.swipe .js-image-frame'),
- );
-
- expect(imageFrameEls[viewTypes.ONION_SKIN]).toEqual(
- element.querySelector('.onion-skin .js-image-frame'),
- );
- });
-
- it('should set viewModesEls', () => {
- const { viewModesEls } = replacedImageDiff;
-
- expect(viewModesEls).toBeDefined();
- expect(viewModesEls[viewTypes.TWO_UP]).toEqual(
- element.querySelector('.view-modes-menu .two-up'),
- );
-
- expect(viewModesEls[viewTypes.SWIPE]).toEqual(
- element.querySelector('.view-modes-menu .swipe'),
- );
-
- expect(viewModesEls[viewTypes.ONION_SKIN]).toEqual(
- element.querySelector('.view-modes-menu .onion-skin'),
- );
- });
-
- it('should generateImageEls', () => {
- expect(ReplacedImageDiff.prototype.generateImageEls).toHaveBeenCalled();
- });
-
- it('should bindEvents', () => {
- expect(ReplacedImageDiff.prototype.bindEvents).toHaveBeenCalled();
- });
-
- describe('currentView', () => {
- it('should set currentView', () => {
- replacedImageDiff.init(viewTypes.ONION_SKIN);
-
- expect(replacedImageDiff.currentView).toEqual(viewTypes.ONION_SKIN);
- });
-
- it('should default to viewTypes.TWO_UP', () => {
- expect(replacedImageDiff.currentView).toEqual(viewTypes.TWO_UP);
- });
- });
- });
-
- describe('generateImageEls', () => {
- beforeEach(() => {
- spyOn(ReplacedImageDiff.prototype, 'bindEvents').and.callFake(() => {});
-
- replacedImageDiff = new ReplacedImageDiff(element, {
- canCreateNote: false,
- renderCommentBadge: false,
- });
-
- setupImageFrameEls();
- });
-
- it('should set imageEls', () => {
- replacedImageDiff.generateImageEls();
- const { imageEls } = replacedImageDiff;
-
- expect(imageEls).toBeDefined();
- expect(imageEls[viewTypes.TWO_UP]).toEqual(element.querySelector('.two-up img'));
- expect(imageEls[viewTypes.SWIPE]).toEqual(element.querySelector('.swipe img'));
- expect(imageEls[viewTypes.ONION_SKIN]).toEqual(element.querySelector('.onion-skin img'));
- });
- });
-
- describe('bindEvents', () => {
- beforeEach(() => {
- spyOn(ImageDiff.prototype, 'bindEvents').and.callFake(() => {});
- replacedImageDiff = new ReplacedImageDiff(element);
-
- setupViewModesEls();
- });
-
- it('should call super.bindEvents', () => {
- replacedImageDiff.bindEvents();
-
- expect(ImageDiff.prototype.bindEvents).toHaveBeenCalled();
- });
-
- it('should register click eventlistener to 2-up view mode', done => {
- spyOn(ReplacedImageDiff.prototype, 'changeView').and.callFake(viewMode => {
- expect(viewMode).toEqual(viewTypes.TWO_UP);
- done();
- });
-
- replacedImageDiff.bindEvents();
- replacedImageDiff.viewModesEls[viewTypes.TWO_UP].click();
- });
-
- it('should register click eventlistener to swipe view mode', done => {
- spyOn(ReplacedImageDiff.prototype, 'changeView').and.callFake(viewMode => {
- expect(viewMode).toEqual(viewTypes.SWIPE);
- done();
- });
-
- replacedImageDiff.bindEvents();
- replacedImageDiff.viewModesEls[viewTypes.SWIPE].click();
- });
-
- it('should register click eventlistener to onion skin view mode', done => {
- spyOn(ReplacedImageDiff.prototype, 'changeView').and.callFake(viewMode => {
- expect(viewMode).toEqual(viewTypes.SWIPE);
- done();
- });
-
- replacedImageDiff.bindEvents();
- replacedImageDiff.viewModesEls[viewTypes.SWIPE].click();
- });
- });
-
- describe('getters', () => {
- describe('imageEl', () => {
- beforeEach(() => {
- replacedImageDiff = new ReplacedImageDiff(element);
- replacedImageDiff.currentView = viewTypes.TWO_UP;
- setupImageEls();
- });
-
- it('should return imageEl based on currentView', () => {
- expect(replacedImageDiff.imageEl).toEqual(element.querySelector('.two-up img'));
-
- replacedImageDiff.currentView = viewTypes.SWIPE;
-
- expect(replacedImageDiff.imageEl).toEqual(element.querySelector('.swipe img'));
- });
- });
-
- describe('imageFrameEl', () => {
- beforeEach(() => {
- replacedImageDiff = new ReplacedImageDiff(element);
- replacedImageDiff.currentView = viewTypes.TWO_UP;
- setupImageFrameEls();
- });
-
- it('should return imageFrameEl based on currentView', () => {
- expect(replacedImageDiff.imageFrameEl).toEqual(
- element.querySelector('.two-up .js-image-frame'),
- );
-
- replacedImageDiff.currentView = viewTypes.ONION_SKIN;
-
- expect(replacedImageDiff.imageFrameEl).toEqual(
- element.querySelector('.onion-skin .js-image-frame'),
- );
- });
- });
- });
-
- describe('changeView', () => {
- beforeEach(() => {
- replacedImageDiff = new ReplacedImageDiff(element);
- spyOn(imageDiffHelper, 'removeCommentIndicator').and.returnValue({
- removed: false,
- });
- setupImageFrameEls();
- });
-
- describe('invalid viewType', () => {
- beforeEach(() => {
- replacedImageDiff.changeView('some-view-name');
- });
-
- it('should not call removeCommentIndicator', () => {
- expect(imageDiffHelper.removeCommentIndicator).not.toHaveBeenCalled();
- });
- });
-
- describe('valid viewType', () => {
- beforeEach(() => {
- jasmine.clock().install();
- spyOn(ReplacedImageDiff.prototype, 'renderNewView').and.callFake(() => {});
- replacedImageDiff.changeView(viewTypes.ONION_SKIN);
- });
-
- afterEach(() => {
- jasmine.clock().uninstall();
- });
-
- it('should call removeCommentIndicator', () => {
- expect(imageDiffHelper.removeCommentIndicator).toHaveBeenCalled();
- });
-
- it('should update currentView to newView', () => {
- expect(replacedImageDiff.currentView).toEqual(viewTypes.ONION_SKIN);
- });
-
- it('should clear imageBadges', () => {
- expect(replacedImageDiff.imageBadges.length).toEqual(0);
- });
-
- it('should call renderNewView', () => {
- jasmine.clock().tick(251);
-
- expect(replacedImageDiff.renderNewView).toHaveBeenCalled();
- });
- });
- });
-
- describe('renderNewView', () => {
- beforeEach(() => {
- replacedImageDiff = new ReplacedImageDiff(element);
- });
-
- it('should call renderBadges', () => {
- spyOn(ReplacedImageDiff.prototype, 'renderBadges').and.callFake(() => {});
-
- replacedImageDiff.renderNewView({
- removed: false,
- });
-
- expect(replacedImageDiff.renderBadges).toHaveBeenCalled();
- });
-
- describe('removeIndicator', () => {
- const indicator = {
- removed: true,
- x: 0,
- y: 1,
- image: {
- width: 50,
- height: 100,
- },
- };
-
- beforeEach(() => {
- setupImageEls();
- setupImageFrameEls();
- });
-
- it('should pass showCommentIndicator normalized indicator values', done => {
- spyOn(imageDiffHelper, 'showCommentIndicator').and.callFake(() => {});
- spyOn(imageDiffHelper, 'resizeCoordinatesToImageElement').and.callFake((imageEl, meta) => {
- expect(meta.x).toEqual(indicator.x);
- expect(meta.y).toEqual(indicator.y);
- expect(meta.width).toEqual(indicator.image.width);
- expect(meta.height).toEqual(indicator.image.height);
- done();
- });
- replacedImageDiff.renderNewView(indicator);
- });
-
- it('should call showCommentIndicator', done => {
- const normalized = {
- normalized: true,
- };
- spyOn(imageDiffHelper, 'resizeCoordinatesToImageElement').and.returnValue(normalized);
- spyOn(imageDiffHelper, 'showCommentIndicator').and.callFake(
- (imageFrameEl, normalizedIndicator) => {
- expect(normalizedIndicator).toEqual(normalized);
- done();
- },
- );
- replacedImageDiff.renderNewView(indicator);
- });
- });
- });
-});
diff --git a/spec/javascripts/integrations/integration_settings_form_spec.js b/spec/javascripts/integrations/integration_settings_form_spec.js
deleted file mode 100644
index 72d04be822f..00000000000
--- a/spec/javascripts/integrations/integration_settings_form_spec.js
+++ /dev/null
@@ -1,301 +0,0 @@
-import $ from 'jquery';
-import MockAdaptor from 'axios-mock-adapter';
-import axios from '~/lib/utils/axios_utils';
-import IntegrationSettingsForm from '~/integrations/integration_settings_form';
-
-describe('IntegrationSettingsForm', () => {
- const FIXTURE = 'services/edit_service.html';
- preloadFixtures(FIXTURE);
-
- beforeEach(() => {
- loadFixtures(FIXTURE);
- });
-
- describe('contructor', () => {
- let integrationSettingsForm;
-
- beforeEach(() => {
- integrationSettingsForm = new IntegrationSettingsForm('.js-integration-settings-form');
- spyOn(integrationSettingsForm, 'init');
- });
-
- it('should initialize form element refs on class object', () => {
- // Form Reference
- expect(integrationSettingsForm.$form).toBeDefined();
- expect(integrationSettingsForm.$form.prop('nodeName')).toEqual('FORM');
- expect(integrationSettingsForm.formActive).toBeDefined();
-
- // Form Child Elements
- expect(integrationSettingsForm.$submitBtn).toBeDefined();
- expect(integrationSettingsForm.$submitBtnLoader).toBeDefined();
- expect(integrationSettingsForm.$submitBtnLabel).toBeDefined();
- });
-
- it('should initialize form metadata on class object', () => {
- expect(integrationSettingsForm.testEndPoint).toBeDefined();
- expect(integrationSettingsForm.canTestService).toBeDefined();
- });
- });
-
- describe('toggleServiceState', () => {
- let integrationSettingsForm;
-
- beforeEach(() => {
- integrationSettingsForm = new IntegrationSettingsForm('.js-integration-settings-form');
- });
-
- it('should remove `novalidate` attribute to form when called with `true`', () => {
- integrationSettingsForm.formActive = true;
- integrationSettingsForm.toggleServiceState();
-
- expect(integrationSettingsForm.$form.attr('novalidate')).not.toBeDefined();
- });
-
- it('should set `novalidate` attribute to form when called with `false`', () => {
- integrationSettingsForm.formActive = false;
- integrationSettingsForm.toggleServiceState();
-
- expect(integrationSettingsForm.$form.attr('novalidate')).toBeDefined();
- });
- });
-
- describe('toggleSubmitBtnLabel', () => {
- let integrationSettingsForm;
-
- beforeEach(() => {
- integrationSettingsForm = new IntegrationSettingsForm('.js-integration-settings-form');
- });
-
- it('should set Save button label to "Test settings and save changes" when serviceActive & canTestService are `true`', () => {
- integrationSettingsForm.canTestService = true;
- integrationSettingsForm.formActive = true;
-
- integrationSettingsForm.toggleSubmitBtnLabel();
-
- expect(integrationSettingsForm.$submitBtnLabel.text()).toEqual(
- 'Test settings and save changes',
- );
- });
-
- it('should set Save button label to "Save changes" when either serviceActive or canTestService (or both) is `false`', () => {
- integrationSettingsForm.canTestService = false;
- integrationSettingsForm.formActive = false;
-
- integrationSettingsForm.toggleSubmitBtnLabel();
-
- expect(integrationSettingsForm.$submitBtnLabel.text()).toEqual('Save changes');
-
- integrationSettingsForm.formActive = true;
-
- integrationSettingsForm.toggleSubmitBtnLabel();
-
- expect(integrationSettingsForm.$submitBtnLabel.text()).toEqual('Save changes');
-
- integrationSettingsForm.canTestService = true;
- integrationSettingsForm.formActive = false;
-
- integrationSettingsForm.toggleSubmitBtnLabel();
-
- expect(integrationSettingsForm.$submitBtnLabel.text()).toEqual('Save changes');
- });
- });
-
- describe('toggleSubmitBtnState', () => {
- let integrationSettingsForm;
-
- beforeEach(() => {
- integrationSettingsForm = new IntegrationSettingsForm('.js-integration-settings-form');
- });
-
- it('should disable Save button and show loader animation when called with `true`', () => {
- integrationSettingsForm.toggleSubmitBtnState(true);
-
- expect(integrationSettingsForm.$submitBtn.is(':disabled')).toBeTruthy();
- expect(integrationSettingsForm.$submitBtnLoader.hasClass('hidden')).toBeFalsy();
- });
-
- it('should enable Save button and hide loader animation when called with `false`', () => {
- integrationSettingsForm.toggleSubmitBtnState(false);
-
- expect(integrationSettingsForm.$submitBtn.is(':disabled')).toBeFalsy();
- expect(integrationSettingsForm.$submitBtnLoader.hasClass('hidden')).toBeTruthy();
- });
- });
-
- describe('testSettings', () => {
- let integrationSettingsForm;
- let formData;
- let mock;
-
- beforeEach(() => {
- mock = new MockAdaptor(axios);
-
- spyOn(axios, 'put').and.callThrough();
-
- integrationSettingsForm = new IntegrationSettingsForm('.js-integration-settings-form');
- // eslint-disable-next-line no-jquery/no-serialize
- formData = integrationSettingsForm.$form.serialize();
- });
-
- afterEach(() => {
- mock.restore();
- });
-
- it('should make an ajax request with provided `formData`', done => {
- integrationSettingsForm
- .testSettings(formData)
- .then(() => {
- expect(axios.put).toHaveBeenCalledWith(integrationSettingsForm.testEndPoint, formData);
-
- done();
- })
- .catch(done.fail);
- });
-
- it('should show error Flash with `Save anyway` action if ajax request responds with error in test', done => {
- const errorMessage = 'Test failed.';
- mock.onPut(integrationSettingsForm.testEndPoint).reply(200, {
- error: true,
- message: errorMessage,
- service_response: 'some error',
- test_failed: true,
- });
-
- integrationSettingsForm
- .testSettings(formData)
- .then(() => {
- const $flashContainer = $('.flash-container');
-
- expect(
- $flashContainer
- .find('.flash-text')
- .text()
- .trim(),
- ).toEqual('Test failed. some error');
-
- expect($flashContainer.find('.flash-action')).toBeDefined();
- expect(
- $flashContainer
- .find('.flash-action')
- .text()
- .trim(),
- ).toEqual('Save anyway');
-
- done();
- })
- .catch(done.fail);
- });
-
- it('should not show error Flash with `Save anyway` action if ajax request responds with error in validation', done => {
- const errorMessage = 'Validations failed.';
- mock.onPut(integrationSettingsForm.testEndPoint).reply(200, {
- error: true,
- message: errorMessage,
- service_response: 'some error',
- test_failed: false,
- });
-
- integrationSettingsForm
- .testSettings(formData)
- .then(() => {
- const $flashContainer = $('.flash-container');
-
- expect(
- $flashContainer
- .find('.flash-text')
- .text()
- .trim(),
- ).toEqual('Validations failed. some error');
-
- expect($flashContainer.find('.flash-action')).toBeDefined();
- expect(
- $flashContainer
- .find('.flash-action')
- .text()
- .trim(),
- ).toEqual('');
-
- done();
- })
- .catch(done.fail);
- });
-
- it('should submit form if ajax request responds without any error in test', done => {
- spyOn(integrationSettingsForm.$form, 'submit');
-
- mock.onPut(integrationSettingsForm.testEndPoint).reply(200, {
- error: false,
- });
-
- integrationSettingsForm
- .testSettings(formData)
- .then(() => {
- expect(integrationSettingsForm.$form.submit).toHaveBeenCalled();
-
- done();
- })
- .catch(done.fail);
- });
-
- it('should submit form when clicked on `Save anyway` action of error Flash', done => {
- spyOn(integrationSettingsForm.$form, 'submit');
-
- const errorMessage = 'Test failed.';
- mock.onPut(integrationSettingsForm.testEndPoint).reply(200, {
- error: true,
- message: errorMessage,
- test_failed: true,
- });
-
- integrationSettingsForm
- .testSettings(formData)
- .then(() => {
- const $flashAction = $('.flash-container .flash-action');
-
- expect($flashAction).toBeDefined();
-
- $flashAction.get(0).click();
- })
- .then(() => {
- expect(integrationSettingsForm.$form.submit).toHaveBeenCalled();
-
- done();
- })
- .catch(done.fail);
- });
-
- it('should show error Flash if ajax request failed', done => {
- const errorMessage = 'Something went wrong on our end.';
-
- mock.onPut(integrationSettingsForm.testEndPoint).networkError();
-
- integrationSettingsForm
- .testSettings(formData)
- .then(() => {
- expect(
- $('.flash-container .flash-text')
- .text()
- .trim(),
- ).toEqual(errorMessage);
-
- done();
- })
- .catch(done.fail);
- });
-
- it('should always call `toggleSubmitBtnState` with `false` once request is completed', done => {
- mock.onPut(integrationSettingsForm.testEndPoint).networkError();
-
- spyOn(integrationSettingsForm, 'toggleSubmitBtnState');
-
- integrationSettingsForm
- .testSettings(formData)
- .then(() => {
- expect(integrationSettingsForm.toggleSubmitBtnState).toHaveBeenCalledWith(false);
-
- done();
- })
- .catch(done.fail);
- });
- });
-});
diff --git a/spec/javascripts/issuable_spec.js b/spec/javascripts/issuable_spec.js
deleted file mode 100644
index 4d57bfb1b33..00000000000
--- a/spec/javascripts/issuable_spec.js
+++ /dev/null
@@ -1,64 +0,0 @@
-import $ from 'jquery';
-import MockAdaptor from 'axios-mock-adapter';
-import axios from '~/lib/utils/axios_utils';
-import IssuableIndex from '~/issuable_index';
-import issuableInitBulkUpdateSidebar from '~/issuable_init_bulk_update_sidebar';
-
-describe('Issuable', () => {
- describe('initBulkUpdate', () => {
- it('should not set bulkUpdateSidebar', () => {
- new IssuableIndex('issue_'); // eslint-disable-line no-new
-
- expect(issuableInitBulkUpdateSidebar.bulkUpdateSidebar).toBeNull();
- });
-
- it('should set bulkUpdateSidebar', () => {
- const element = document.createElement('div');
- element.classList.add('issues-bulk-update');
- document.body.appendChild(element);
-
- new IssuableIndex('issue_'); // eslint-disable-line no-new
-
- expect(issuableInitBulkUpdateSidebar.bulkUpdateSidebar).toBeDefined();
- });
- });
-
- describe('resetIncomingEmailToken', () => {
- let mock;
-
- beforeEach(() => {
- const element = document.createElement('a');
- element.classList.add('incoming-email-token-reset');
- element.setAttribute('href', 'foo');
- document.body.appendChild(element);
-
- const input = document.createElement('input');
- input.setAttribute('id', 'issuable_email');
- document.body.appendChild(input);
-
- new IssuableIndex('issue_'); // eslint-disable-line no-new
-
- mock = new MockAdaptor(axios);
-
- mock.onPut('foo').reply(200, {
- new_address: 'testing123',
- });
- });
-
- afterEach(() => {
- mock.restore();
- });
-
- it('should send request to reset email token', done => {
- spyOn(axios, 'put').and.callThrough();
- document.querySelector('.incoming-email-token-reset').click();
-
- setTimeout(() => {
- expect(axios.put).toHaveBeenCalledWith('foo');
- expect($('#issuable_email').val()).toBe('testing123');
-
- done();
- });
- });
- });
-});
diff --git a/spec/javascripts/issue_show/components/app_spec.js b/spec/javascripts/issue_show/components/app_spec.js
deleted file mode 100644
index f11d4f5ac33..00000000000
--- a/spec/javascripts/issue_show/components/app_spec.js
+++ /dev/null
@@ -1,568 +0,0 @@
-/* eslint-disable no-unused-vars */
-import Vue from 'vue';
-import MockAdapter from 'axios-mock-adapter';
-import setTimeoutPromise from 'spec/helpers/set_timeout_promise_helper';
-import GLDropdown from '~/gl_dropdown';
-import axios from '~/lib/utils/axios_utils';
-import '~/behaviors/markdown/render_gfm';
-import issuableApp from '~/issue_show/components/app.vue';
-import eventHub from '~/issue_show/event_hub';
-import { initialRequest, secondRequest } from '../mock_data';
-
-function formatText(text) {
- return text.trim().replace(/\s\s+/g, ' ');
-}
-
-const REALTIME_REQUEST_STACK = [initialRequest, secondRequest];
-
-describe('Issuable output', () => {
- let mock;
- let realtimeRequestCount = 0;
- let vm;
-
- beforeEach(done => {
- setFixtures(`
- <div>
- <div class="detail-page-description content-block">
- <details open>
- <summary>One</summary>
- </details>
- <details>
- <summary>Two</summary>
- </details>
- </div>
- <div class="flash-container"></div>
- <span id="task_status"></span>
- </div>
- `);
- spyOn(eventHub, '$emit');
-
- const IssuableDescriptionComponent = Vue.extend(issuableApp);
-
- mock = new MockAdapter(axios);
- mock
- .onGet('/gitlab-org/gitlab-shell/-/issues/9/realtime_changes/realtime_changes')
- .reply(() => {
- const res = Promise.resolve([200, REALTIME_REQUEST_STACK[realtimeRequestCount]]);
- realtimeRequestCount += 1;
- return res;
- });
-
- vm = new IssuableDescriptionComponent({
- propsData: {
- canUpdate: true,
- canDestroy: true,
- endpoint: '/gitlab-org/gitlab-shell/-/issues/9/realtime_changes',
- updateEndpoint: gl.TEST_HOST,
- issuableRef: '#1',
- initialTitleHtml: '',
- initialTitleText: '',
- initialDescriptionHtml: 'test',
- initialDescriptionText: 'test',
- lockVersion: 1,
- markdownPreviewPath: '/',
- markdownDocsPath: '/',
- projectNamespace: '/',
- projectPath: '/',
- issuableTemplateNamesPath: '/issuable-templates-path',
- },
- }).$mount();
-
- setTimeout(done);
- });
-
- afterEach(() => {
- mock.restore();
- realtimeRequestCount = 0;
-
- vm.poll.stop();
- vm.$destroy();
- });
-
- it('should render a title/description/edited and update title/description/edited on update', done => {
- let editedText;
- Vue.nextTick()
- .then(() => {
- editedText = vm.$el.querySelector('.edited-text');
- })
- .then(() => {
- expect(document.querySelector('title').innerText).toContain('this is a title (#1)');
- expect(vm.$el.querySelector('.title').innerHTML).toContain('<p>this is a title</p>');
- expect(vm.$el.querySelector('.md').innerHTML).toContain('<p>this is a description!</p>');
- expect(vm.$el.querySelector('.js-task-list-field').value).toContain(
- 'this is a description',
- );
-
- expect(formatText(editedText.innerText)).toMatch(/Edited[\s\S]+?by Some User/);
- expect(editedText.querySelector('.author-link').href).toMatch(/\/some_user$/);
- expect(editedText.querySelector('time')).toBeTruthy();
- expect(vm.state.lock_version).toEqual(1);
- })
- .then(() => {
- vm.poll.makeRequest();
- })
- .then(() => new Promise(resolve => setTimeout(resolve)))
- .then(() => {
- expect(document.querySelector('title').innerText).toContain('2 (#1)');
- expect(vm.$el.querySelector('.title').innerHTML).toContain('<p>2</p>');
- expect(vm.$el.querySelector('.md').innerHTML).toContain('<p>42</p>');
- expect(vm.$el.querySelector('.js-task-list-field').value).toContain('42');
- expect(vm.$el.querySelector('.edited-text')).toBeTruthy();
- expect(formatText(vm.$el.querySelector('.edited-text').innerText)).toMatch(
- /Edited[\s\S]+?by Other User/,
- );
-
- expect(editedText.querySelector('.author-link').href).toMatch(/\/other_user$/);
- expect(editedText.querySelector('time')).toBeTruthy();
- expect(vm.state.lock_version).toEqual(2);
- })
- .then(done)
- .catch(done.fail);
- });
-
- it('shows actions if permissions are correct', done => {
- vm.showForm = true;
-
- Vue.nextTick(() => {
- expect(vm.$el.querySelector('.btn')).not.toBeNull();
-
- done();
- });
- });
-
- it('does not show actions if permissions are incorrect', done => {
- vm.showForm = true;
- vm.canUpdate = false;
-
- Vue.nextTick(() => {
- expect(vm.$el.querySelector('.btn')).toBeNull();
-
- done();
- });
- });
-
- it('does not update formState if form is already open', done => {
- vm.updateAndShowForm();
-
- vm.state.titleText = 'testing 123';
-
- vm.updateAndShowForm();
-
- Vue.nextTick(() => {
- expect(vm.store.formState.title).not.toBe('testing 123');
-
- done();
- });
- });
-
- describe('updateIssuable', () => {
- it('fetches new data after update', done => {
- spyOn(vm, 'updateStoreState').and.callThrough();
- spyOn(vm.service, 'getData').and.callThrough();
- spyOn(vm.service, 'updateIssuable').and.returnValue(
- Promise.resolve({
- data: { web_url: window.location.pathname },
- }),
- );
-
- vm.updateIssuable()
- .then(() => {
- expect(vm.updateStoreState).toHaveBeenCalled();
- expect(vm.service.getData).toHaveBeenCalled();
- })
- .then(done)
- .catch(done.fail);
- });
-
- it('correctly updates issuable data', done => {
- spyOn(vm.service, 'updateIssuable').and.returnValue(
- Promise.resolve({
- data: { web_url: window.location.pathname },
- }),
- );
-
- vm.updateIssuable()
- .then(() => {
- expect(vm.service.updateIssuable).toHaveBeenCalledWith(vm.formState);
- expect(eventHub.$emit).toHaveBeenCalledWith('close.form');
- })
- .then(done)
- .catch(done.fail);
- });
-
- it('does not redirect if issue has not moved', done => {
- const visitUrl = spyOnDependency(issuableApp, 'visitUrl');
- spyOn(vm.service, 'updateIssuable').and.returnValue(
- Promise.resolve({
- data: {
- web_url: window.location.pathname,
- confidential: vm.isConfidential,
- },
- }),
- );
-
- vm.updateIssuable();
-
- setTimeout(() => {
- expect(visitUrl).not.toHaveBeenCalled();
- done();
- });
- });
-
- it('redirects if returned web_url has changed', done => {
- const visitUrl = spyOnDependency(issuableApp, 'visitUrl');
- spyOn(vm.service, 'updateIssuable').and.returnValue(
- Promise.resolve({
- data: {
- web_url: '/testing-issue-move',
- confidential: vm.isConfidential,
- },
- }),
- );
-
- vm.updateIssuable();
-
- setTimeout(() => {
- expect(visitUrl).toHaveBeenCalledWith('/testing-issue-move');
- done();
- });
- });
-
- describe('shows dialog when issue has unsaved changed', () => {
- it('confirms on title change', done => {
- vm.showForm = true;
- vm.state.titleText = 'title has changed';
- const e = { returnValue: null };
- vm.handleBeforeUnloadEvent(e);
- Vue.nextTick(() => {
- expect(e.returnValue).not.toBeNull();
-
- done();
- });
- });
-
- it('confirms on description change', done => {
- vm.showForm = true;
- vm.state.descriptionText = 'description has changed';
- const e = { returnValue: null };
- vm.handleBeforeUnloadEvent(e);
- Vue.nextTick(() => {
- expect(e.returnValue).not.toBeNull();
-
- done();
- });
- });
-
- it('does nothing when nothing has changed', done => {
- const e = { returnValue: null };
- vm.handleBeforeUnloadEvent(e);
- Vue.nextTick(() => {
- expect(e.returnValue).toBeNull();
-
- done();
- });
- });
- });
-
- describe('error when updating', () => {
- it('closes form on error', done => {
- spyOn(vm.service, 'updateIssuable').and.callFake(() => Promise.reject());
- vm.updateIssuable();
-
- setTimeout(() => {
- expect(eventHub.$emit).not.toHaveBeenCalledWith('close.form');
- expect(document.querySelector('.flash-container .flash-text').innerText.trim()).toBe(
- `Error updating issue`,
- );
-
- done();
- });
- });
-
- it('returns the correct error message for issuableType', done => {
- spyOn(vm.service, 'updateIssuable').and.callFake(() => Promise.reject());
- vm.issuableType = 'merge request';
-
- Vue.nextTick(() => {
- vm.updateIssuable();
-
- setTimeout(() => {
- expect(eventHub.$emit).not.toHaveBeenCalledWith('close.form');
- expect(document.querySelector('.flash-container .flash-text').innerText.trim()).toBe(
- `Error updating merge request`,
- );
-
- done();
- });
- });
- });
-
- it('shows error message from backend if exists', done => {
- const msg = 'Custom error message from backend';
- spyOn(vm.service, 'updateIssuable').and.callFake(
- // eslint-disable-next-line prefer-promise-reject-errors
- () => Promise.reject({ response: { data: { errors: [msg] } } }),
- );
-
- vm.updateIssuable();
- setTimeout(() => {
- expect(document.querySelector('.flash-container .flash-text').innerText.trim()).toBe(
- `${vm.defaultErrorMessage}. ${msg}`,
- );
-
- done();
- });
- });
- });
- });
-
- it('opens recaptcha modal if update rejected as spam', done => {
- function mockScriptSrc() {
- const recaptchaChild = vm.$children.find(
- // eslint-disable-next-line no-underscore-dangle
- child => child.$options._componentTag === 'recaptcha-modal',
- );
-
- recaptchaChild.scriptSrc = '//scriptsrc';
- }
-
- let modal;
- const promise = new Promise(resolve => {
- resolve({
- data: {
- recaptcha_html: '<div class="g-recaptcha">recaptcha_html</div>',
- },
- });
- });
-
- spyOn(vm.service, 'updateIssuable').and.returnValue(promise);
-
- vm.canUpdate = true;
- vm.showForm = true;
-
- vm.$nextTick()
- .then(() => mockScriptSrc())
- .then(() => vm.updateIssuable())
- .then(promise)
- .then(() => setTimeoutPromise())
- .then(() => {
- modal = vm.$el.querySelector('.js-recaptcha-modal');
-
- expect(modal.style.display).not.toEqual('none');
- expect(modal.querySelector('.g-recaptcha').textContent).toEqual('recaptcha_html');
- expect(document.body.querySelector('.js-recaptcha-script').src).toMatch('//scriptsrc');
- })
- .then(() => modal.querySelector('.close').click())
- .then(() => vm.$nextTick())
- .then(() => {
- expect(modal.style.display).toEqual('none');
- expect(document.body.querySelector('.js-recaptcha-script')).toBeNull();
- })
- .then(done)
- .catch(done.fail);
- });
-
- describe('deleteIssuable', () => {
- it('changes URL when deleted', done => {
- const visitUrl = spyOnDependency(issuableApp, 'visitUrl');
- spyOn(vm.service, 'deleteIssuable').and.returnValue(
- Promise.resolve({
- data: {
- web_url: '/test',
- },
- }),
- );
-
- vm.deleteIssuable();
-
- setTimeout(() => {
- expect(visitUrl).toHaveBeenCalledWith('/test');
-
- done();
- });
- });
-
- it('stops polling when deleting', done => {
- spyOnDependency(issuableApp, 'visitUrl');
- spyOn(vm.poll, 'stop').and.callThrough();
- spyOn(vm.service, 'deleteIssuable').and.returnValue(
- Promise.resolve({
- data: {
- web_url: '/test',
- },
- }),
- );
-
- vm.deleteIssuable();
-
- setTimeout(() => {
- expect(vm.poll.stop).toHaveBeenCalledWith();
-
- done();
- });
- });
-
- it('closes form on error', done => {
- spyOn(vm.service, 'deleteIssuable').and.returnValue(Promise.reject());
-
- vm.deleteIssuable();
-
- setTimeout(() => {
- expect(eventHub.$emit).not.toHaveBeenCalledWith('close.form');
- expect(document.querySelector('.flash-container .flash-text').innerText.trim()).toBe(
- 'Error deleting issue',
- );
-
- done();
- });
- });
- });
-
- describe('updateAndShowForm', () => {
- it('shows locked warning if form is open & data is different', done => {
- vm.$nextTick()
- .then(() => {
- vm.updateAndShowForm();
-
- vm.poll.makeRequest();
-
- return new Promise(resolve => {
- vm.$watch('formState.lockedWarningVisible', value => {
- if (value) resolve();
- });
- });
- })
- .then(() => {
- expect(vm.formState.lockedWarningVisible).toEqual(true);
- expect(vm.formState.lock_version).toEqual(1);
- expect(vm.$el.querySelector('.alert')).not.toBeNull();
- })
- .then(done)
- .catch(done.fail);
- });
- });
-
- describe('requestTemplatesAndShowForm', () => {
- beforeEach(() => {
- spyOn(vm, 'updateAndShowForm');
- });
-
- it('shows the form if template names request is successful', done => {
- const mockData = [{ name: 'Bug' }];
- mock.onGet('/issuable-templates-path').reply(() => Promise.resolve([200, mockData]));
-
- vm.requestTemplatesAndShowForm()
- .then(() => {
- expect(vm.updateAndShowForm).toHaveBeenCalledWith(mockData);
- })
- .then(done)
- .catch(done.fail);
- });
-
- it('shows the form if template names request failed', done => {
- mock
- .onGet('/issuable-templates-path')
- .reply(() => Promise.reject(new Error('something went wrong')));
-
- vm.requestTemplatesAndShowForm()
- .then(() => {
- expect(document.querySelector('.flash-container .flash-text').textContent).toContain(
- 'Error updating issue',
- );
-
- expect(vm.updateAndShowForm).toHaveBeenCalledWith();
- })
- .then(done)
- .catch(done.fail);
- });
- });
-
- describe('show inline edit button', () => {
- it('should not render by default', () => {
- expect(vm.$el.querySelector('.title-container .note-action-button')).toBeDefined();
- });
-
- it('should render if showInlineEditButton', () => {
- vm.showInlineEditButton = true;
-
- expect(vm.$el.querySelector('.title-container .note-action-button')).toBeDefined();
- });
- });
-
- describe('updateStoreState', () => {
- it('should make a request and update the state of the store', done => {
- const data = { foo: 1 };
- spyOn(vm.store, 'updateState');
- spyOn(vm.service, 'getData').and.returnValue(Promise.resolve({ data }));
-
- vm.updateStoreState()
- .then(() => {
- expect(vm.service.getData).toHaveBeenCalled();
- expect(vm.store.updateState).toHaveBeenCalledWith(data);
- })
- .then(done)
- .catch(done.fail);
- });
-
- it('should show error message if store update fails', done => {
- spyOn(vm.service, 'getData').and.returnValue(Promise.reject());
- vm.issuableType = 'merge request';
-
- vm.updateStoreState()
- .then(() => {
- expect(document.querySelector('.flash-container .flash-text').innerText.trim()).toBe(
- `Error updating ${vm.issuableType}`,
- );
- })
- .then(done)
- .catch(done.fail);
- });
- });
-
- describe('issueChanged', () => {
- beforeEach(() => {
- vm.store.formState.title = '';
- vm.store.formState.description = '';
- vm.initialDescriptionText = '';
- vm.initialTitleText = '';
- });
-
- it('returns true when title is changed', () => {
- vm.store.formState.title = 'RandomText';
-
- expect(vm.issueChanged).toBe(true);
- });
-
- it('returns false when title is empty null', () => {
- vm.store.formState.title = null;
-
- expect(vm.issueChanged).toBe(false);
- });
-
- it('returns false when `initialTitleText` is null and `formState.title` is empty string', () => {
- vm.store.formState.title = '';
- vm.initialTitleText = null;
-
- expect(vm.issueChanged).toBe(false);
- });
-
- it('returns true when description is changed', () => {
- vm.store.formState.description = 'RandomText';
-
- expect(vm.issueChanged).toBe(true);
- });
-
- it('returns false when description is empty null', () => {
- vm.store.formState.title = null;
-
- expect(vm.issueChanged).toBe(false);
- });
-
- it('returns false when `initialDescriptionText` is null and `formState.description` is empty string', () => {
- vm.store.formState.description = '';
- vm.initialDescriptionText = null;
-
- expect(vm.issueChanged).toBe(false);
- });
- });
-});
diff --git a/spec/javascripts/issue_show/components/description_spec.js b/spec/javascripts/issue_show/components/description_spec.js
deleted file mode 100644
index 83e498347f7..00000000000
--- a/spec/javascripts/issue_show/components/description_spec.js
+++ /dev/null
@@ -1,210 +0,0 @@
-import $ from 'jquery';
-import Vue from 'vue';
-import '~/behaviors/markdown/render_gfm';
-import mountComponent from 'spec/helpers/vue_mount_component_helper';
-import Description from '~/issue_show/components/description.vue';
-
-describe('Description component', () => {
- let vm;
- let DescriptionComponent;
- const props = {
- canUpdate: true,
- descriptionHtml: 'test',
- descriptionText: 'test',
- updatedAt: new Date().toString(),
- taskStatus: '',
- updateUrl: gl.TEST_HOST,
- };
-
- beforeEach(() => {
- DescriptionComponent = Vue.extend(Description);
-
- if (!document.querySelector('.issuable-meta')) {
- const metaData = document.createElement('div');
- metaData.classList.add('issuable-meta');
- metaData.innerHTML =
- '<div class="flash-container"></div><span id="task_status"></span><span id="task_status_short"></span>';
-
- document.body.appendChild(metaData);
- }
-
- vm = mountComponent(DescriptionComponent, props);
- });
-
- afterEach(() => {
- vm.$destroy();
- });
-
- afterAll(() => {
- $('.issuable-meta .flash-container').remove();
- });
-
- it('animates description changes', done => {
- vm.descriptionHtml = 'changed';
-
- Vue.nextTick(() => {
- expect(
- vm.$el.querySelector('.md').classList.contains('issue-realtime-pre-pulse'),
- ).toBeTruthy();
-
- setTimeout(() => {
- expect(
- vm.$el.querySelector('.md').classList.contains('issue-realtime-trigger-pulse'),
- ).toBeTruthy();
-
- done();
- });
- });
- });
-
- it('opens recaptcha dialog if update rejected as spam', done => {
- let modal;
- const recaptchaChild = vm.$children.find(
- // eslint-disable-next-line no-underscore-dangle
- child => child.$options._componentTag === 'recaptcha-modal',
- );
-
- recaptchaChild.scriptSrc = '//scriptsrc';
-
- vm.taskListUpdateSuccess({
- recaptcha_html: '<div class="g-recaptcha">recaptcha_html</div>',
- });
-
- vm.$nextTick()
- .then(() => {
- modal = vm.$el.querySelector('.js-recaptcha-modal');
-
- expect(modal.style.display).not.toEqual('none');
- expect(modal.querySelector('.g-recaptcha').textContent).toEqual('recaptcha_html');
- expect(document.body.querySelector('.js-recaptcha-script').src).toMatch('//scriptsrc');
- })
- .then(() => modal.querySelector('.close').click())
- .then(() => vm.$nextTick())
- .then(() => {
- expect(modal.style.display).toEqual('none');
- expect(document.body.querySelector('.js-recaptcha-script')).toBeNull();
- })
- .then(done)
- .catch(done.fail);
- });
-
- describe('TaskList', () => {
- let TaskList;
-
- beforeEach(() => {
- vm.$destroy();
- vm = mountComponent(
- DescriptionComponent,
- Object.assign({}, props, {
- issuableType: 'issuableType',
- }),
- );
- TaskList = spyOnDependency(Description, 'TaskList');
- });
-
- it('re-inits the TaskList when description changed', done => {
- vm.descriptionHtml = 'changed';
-
- setTimeout(() => {
- expect(TaskList).toHaveBeenCalled();
- done();
- });
- });
-
- it('does not re-init the TaskList when canUpdate is false', done => {
- vm.canUpdate = false;
- vm.descriptionHtml = 'changed';
-
- setTimeout(() => {
- expect(TaskList).not.toHaveBeenCalled();
- done();
- });
- });
-
- it('calls with issuableType dataType', done => {
- vm.descriptionHtml = 'changed';
-
- setTimeout(() => {
- expect(TaskList).toHaveBeenCalledWith({
- dataType: 'issuableType',
- fieldName: 'description',
- selector: '.detail-page-description',
- onSuccess: jasmine.any(Function),
- onError: jasmine.any(Function),
- lockVersion: 0,
- });
-
- done();
- });
- });
- });
-
- describe('taskStatus', () => {
- it('adds full taskStatus', done => {
- vm.taskStatus = '1 of 1';
-
- setTimeout(() => {
- expect(document.querySelector('.issuable-meta #task_status').textContent.trim()).toBe(
- '1 of 1',
- );
-
- done();
- });
- });
-
- it('adds short taskStatus', done => {
- vm.taskStatus = '1 of 1';
-
- setTimeout(() => {
- expect(document.querySelector('.issuable-meta #task_status_short').textContent.trim()).toBe(
- '1/1 task',
- );
-
- done();
- });
- });
-
- it('clears task status text when no tasks are present', done => {
- vm.taskStatus = '0 of 0';
-
- setTimeout(() => {
- expect(document.querySelector('.issuable-meta #task_status').textContent.trim()).toBe('');
-
- done();
- });
- });
- });
-
- it('applies syntax highlighting and math when description changed', done => {
- spyOn(vm, 'renderGFM').and.callThrough();
- spyOn($.prototype, 'renderGFM').and.callThrough();
- vm.descriptionHtml = 'changed';
-
- Vue.nextTick(() => {
- setTimeout(() => {
- expect(vm.$refs['gfm-content']).toBeDefined();
- expect(vm.renderGFM).toHaveBeenCalled();
- expect($.prototype.renderGFM).toHaveBeenCalled();
-
- done();
- });
- });
- });
-
- it('sets data-update-url', () => {
- expect(vm.$el.querySelector('textarea').dataset.updateUrl).toEqual(gl.TEST_HOST);
- });
-
- describe('taskListUpdateError', () => {
- it('should create flash notification and emit an event to parent', () => {
- const msg =
- 'Someone edited this issue at the same time you did. The description has been updated and you will need to make your changes again.';
- spyOn(vm, '$emit');
-
- vm.taskListUpdateError();
-
- expect(document.querySelector('.flash-container .flash-text').innerText.trim()).toBe(msg);
- expect(vm.$emit).toHaveBeenCalledWith('taskListUpdateFailed');
- });
- });
-});
diff --git a/spec/javascripts/issue_show/components/fields/description_template_spec.js b/spec/javascripts/issue_show/components/fields/description_template_spec.js
deleted file mode 100644
index 8d77a620d76..00000000000
--- a/spec/javascripts/issue_show/components/fields/description_template_spec.js
+++ /dev/null
@@ -1,43 +0,0 @@
-import Vue from 'vue';
-import descriptionTemplate from '~/issue_show/components/fields/description_template.vue';
-
-describe('Issue description template component', () => {
- let vm;
- let formState;
-
- beforeEach(done => {
- const Component = Vue.extend(descriptionTemplate);
- formState = {
- description: 'test',
- };
-
- vm = new Component({
- propsData: {
- formState,
- issuableTemplates: [{ name: 'test' }],
- projectPath: '/',
- projectNamespace: '/',
- },
- }).$mount();
-
- Vue.nextTick(done);
- });
-
- it('renders templates as JSON array in data attribute', () => {
- expect(vm.$el.querySelector('.js-issuable-selector').getAttribute('data-data')).toBe(
- '[{"name":"test"}]',
- );
- });
-
- it('updates formState when changing template', () => {
- vm.issuableTemplate.editor.setValue('test new template');
-
- expect(formState.description).toBe('test new template');
- });
-
- it('returns formState description with editor getValue', () => {
- formState.description = 'testing new template';
-
- expect(vm.issuableTemplate.editor.getValue()).toBe('testing new template');
- });
-});
diff --git a/spec/javascripts/issue_show/components/form_spec.js b/spec/javascripts/issue_show/components/form_spec.js
deleted file mode 100644
index a111333ac80..00000000000
--- a/spec/javascripts/issue_show/components/form_spec.js
+++ /dev/null
@@ -1,98 +0,0 @@
-import Vue from 'vue';
-import mountComponent from 'spec/helpers/vue_mount_component_helper';
-import formComponent from '~/issue_show/components/form.vue';
-import eventHub from '~/issue_show/event_hub';
-
-describe('Inline edit form component', () => {
- let vm;
- const defaultProps = {
- canDestroy: true,
- formState: {
- title: 'b',
- description: 'a',
- lockedWarningVisible: false,
- },
- issuableType: 'issue',
- markdownPreviewPath: '/',
- markdownDocsPath: '/',
- projectPath: '/',
- projectNamespace: '/',
- };
-
- afterEach(() => {
- vm.$destroy();
- });
-
- const createComponent = props => {
- const Component = Vue.extend(formComponent);
-
- vm = mountComponent(Component, {
- ...defaultProps,
- ...props,
- });
- };
-
- it('does not render template selector if no templates exist', () => {
- createComponent();
-
- expect(vm.$el.querySelector('.js-issuable-selector-wrap')).toBeNull();
- });
-
- it('renders template selector when templates exists', () => {
- createComponent({ issuableTemplates: ['test'] });
-
- expect(vm.$el.querySelector('.js-issuable-selector-wrap')).not.toBeNull();
- });
-
- it('hides locked warning by default', () => {
- createComponent();
-
- expect(vm.$el.querySelector('.alert')).toBeNull();
- });
-
- it('shows locked warning if formState is different', () => {
- createComponent({ formState: { ...defaultProps.formState, lockedWarningVisible: true } });
-
- expect(vm.$el.querySelector('.alert')).not.toBeNull();
- });
-
- it('hides locked warning when currently saving', () => {
- createComponent({
- formState: { ...defaultProps.formState, updateLoading: true, lockedWarningVisible: true },
- });
-
- expect(vm.$el.querySelector('.alert')).toBeNull();
- });
-
- describe('autosave', () => {
- let autosaveObj;
- let autosave;
-
- beforeEach(() => {
- autosaveObj = { reset: jasmine.createSpy() };
- autosave = spyOnDependency(formComponent, 'Autosave').and.returnValue(autosaveObj);
- });
-
- it('initialized Autosave on mount', () => {
- createComponent();
-
- expect(autosave).toHaveBeenCalledTimes(2);
- });
-
- it('calls reset on autosave when eventHub emits appropriate events', () => {
- createComponent();
-
- eventHub.$emit('close.form');
-
- expect(autosaveObj.reset).toHaveBeenCalledTimes(2);
-
- eventHub.$emit('delete.issuable');
-
- expect(autosaveObj.reset).toHaveBeenCalledTimes(4);
-
- eventHub.$emit('update.issuable');
-
- expect(autosaveObj.reset).toHaveBeenCalledTimes(6);
- });
- });
-});
diff --git a/spec/javascripts/issue_show/components/title_spec.js b/spec/javascripts/issue_show/components/title_spec.js
deleted file mode 100644
index 9754c8a6755..00000000000
--- a/spec/javascripts/issue_show/components/title_spec.js
+++ /dev/null
@@ -1,105 +0,0 @@
-import Vue from 'vue';
-import Store from '~/issue_show/stores';
-import titleComponent from '~/issue_show/components/title.vue';
-import eventHub from '~/issue_show/event_hub';
-
-describe('Title component', () => {
- let vm;
-
- beforeEach(() => {
- const Component = Vue.extend(titleComponent);
- const store = new Store({
- titleHtml: '',
- descriptionHtml: '',
- issuableRef: '',
- });
- vm = new Component({
- propsData: {
- issuableRef: '#1',
- titleHtml: 'Testing <img />',
- titleText: 'Testing',
- showForm: false,
- formState: store.formState,
- },
- }).$mount();
- });
-
- it('renders title HTML', () => {
- expect(vm.$el.querySelector('.title').innerHTML.trim()).toBe('Testing <img>');
- });
-
- it('updates page title when changing titleHtml', done => {
- spyOn(vm, 'setPageTitle');
- vm.titleHtml = 'test';
-
- Vue.nextTick(() => {
- expect(vm.setPageTitle).toHaveBeenCalled();
-
- done();
- });
- });
-
- it('animates title changes', done => {
- vm.titleHtml = 'test';
-
- Vue.nextTick(() => {
- expect(
- vm.$el.querySelector('.title').classList.contains('issue-realtime-pre-pulse'),
- ).toBeTruthy();
-
- setTimeout(() => {
- expect(
- vm.$el.querySelector('.title').classList.contains('issue-realtime-trigger-pulse'),
- ).toBeTruthy();
-
- done();
- });
- });
- });
-
- it('updates page title after changing title', done => {
- vm.titleHtml = 'changed';
- vm.titleText = 'changed';
-
- Vue.nextTick(() => {
- expect(document.querySelector('title').textContent.trim()).toContain('changed');
-
- done();
- });
- });
-
- describe('inline edit button', () => {
- beforeEach(() => {
- spyOn(eventHub, '$emit');
- });
-
- it('should not show by default', () => {
- expect(vm.$el.querySelector('.btn-edit')).toBeNull();
- });
-
- it('should not show if canUpdate is false', () => {
- vm.showInlineEditButton = true;
- vm.canUpdate = false;
-
- expect(vm.$el.querySelector('.btn-edit')).toBeNull();
- });
-
- it('should show if showInlineEditButton and canUpdate', () => {
- vm.showInlineEditButton = true;
- vm.canUpdate = true;
-
- expect(vm.$el.querySelector('.btn-edit')).toBeDefined();
- });
-
- it('should trigger open.form event when clicked', () => {
- vm.showInlineEditButton = true;
- vm.canUpdate = true;
-
- Vue.nextTick(() => {
- vm.$el.querySelector('.btn-edit').click();
-
- expect(eventHub.$emit).toHaveBeenCalledWith('open.form');
- });
- });
- });
-});
diff --git a/spec/javascripts/issue_show/helpers.js b/spec/javascripts/issue_show/helpers.js
deleted file mode 100644
index 951acfd4e10..00000000000
--- a/spec/javascripts/issue_show/helpers.js
+++ /dev/null
@@ -1 +0,0 @@
-export * from '../../frontend/issue_show/helpers.js';
diff --git a/spec/javascripts/issue_show/mock_data.js b/spec/javascripts/issue_show/mock_data.js
deleted file mode 100644
index 1b391bd1588..00000000000
--- a/spec/javascripts/issue_show/mock_data.js
+++ /dev/null
@@ -1 +0,0 @@
-export * from '../../frontend/issue_show/mock_data';
diff --git a/spec/javascripts/jobs/components/commit_block_spec.js b/spec/javascripts/jobs/components/commit_block_spec.js
deleted file mode 100644
index c02f564d01a..00000000000
--- a/spec/javascripts/jobs/components/commit_block_spec.js
+++ /dev/null
@@ -1,89 +0,0 @@
-import Vue from 'vue';
-import component from '~/jobs/components/commit_block.vue';
-import mountComponent from '../../helpers/vue_mount_component_helper';
-
-describe('Commit block', () => {
- const Component = Vue.extend(component);
- let vm;
-
- const props = {
- commit: {
- short_id: '1f0fb84f',
- id: '1f0fb84fb6770d74d97eee58118fd3909cd4f48c',
- commit_path: 'commit/1f0fb84fb6770d74d97eee58118fd3909cd4f48c',
- title: 'Update README.md',
- },
- mergeRequest: {
- iid: '!21244',
- path: 'merge_requests/21244',
- },
- isLastBlock: true,
- };
-
- afterEach(() => {
- vm.$destroy();
- });
-
- describe('pipeline short sha', () => {
- beforeEach(() => {
- vm = mountComponent(Component, {
- ...props,
- });
- });
-
- it('renders pipeline short sha link', () => {
- expect(vm.$el.querySelector('.js-commit-sha').getAttribute('href')).toEqual(
- props.commit.commit_path,
- );
-
- expect(vm.$el.querySelector('.js-commit-sha').textContent.trim()).toEqual(
- props.commit.short_id,
- );
- });
-
- it('renders clipboard button', () => {
- expect(vm.$el.querySelector('button').getAttribute('data-clipboard-text')).toEqual(
- props.commit.id,
- );
- });
- });
-
- describe('with merge request', () => {
- it('renders merge request link and reference', () => {
- vm = mountComponent(Component, {
- ...props,
- });
-
- expect(vm.$el.querySelector('.js-link-commit').getAttribute('href')).toEqual(
- props.mergeRequest.path,
- );
-
- expect(vm.$el.querySelector('.js-link-commit').textContent.trim()).toEqual(
- `!${props.mergeRequest.iid}`,
- );
- });
- });
-
- describe('without merge request', () => {
- it('does not render merge request', () => {
- const copyProps = Object.assign({}, props);
- delete copyProps.mergeRequest;
-
- vm = mountComponent(Component, {
- ...copyProps,
- });
-
- expect(vm.$el.querySelector('.js-link-commit')).toBeNull();
- });
- });
-
- describe('git commit title', () => {
- it('renders git commit title', () => {
- vm = mountComponent(Component, {
- ...props,
- });
-
- expect(vm.$el.textContent).toContain(props.commit.title);
- });
- });
-});
diff --git a/spec/javascripts/jobs/components/job_container_item_spec.js b/spec/javascripts/jobs/components/job_container_item_spec.js
deleted file mode 100644
index 99f6d9a14d9..00000000000
--- a/spec/javascripts/jobs/components/job_container_item_spec.js
+++ /dev/null
@@ -1,99 +0,0 @@
-import Vue from 'vue';
-import mountComponent from 'spec/helpers/vue_mount_component_helper';
-import JobContainerItem from '~/jobs/components/job_container_item.vue';
-import job from '../mock_data';
-
-describe('JobContainerItem', () => {
- const delayedJobFixture = getJSONFixture('jobs/delayed.json');
- const Component = Vue.extend(JobContainerItem);
- let vm;
-
- afterEach(() => {
- vm.$destroy();
- });
-
- const sharedTests = () => {
- it('displays a status icon', () => {
- expect(vm.$el).toHaveSpriteIcon(job.status.icon);
- });
-
- it('displays the job name', () => {
- expect(vm.$el).toContainText(job.name);
- });
-
- it('displays a link to the job', () => {
- const link = vm.$el.querySelector('.js-job-link');
-
- expect(link.href).toBe(job.status.details_path);
- });
- };
-
- describe('when a job is not active and not retied', () => {
- beforeEach(() => {
- vm = mountComponent(Component, {
- job,
- isActive: false,
- });
- });
-
- sharedTests();
- });
-
- describe('when a job is active', () => {
- beforeEach(() => {
- vm = mountComponent(Component, {
- job,
- isActive: true,
- });
- });
-
- sharedTests();
-
- it('displays an arrow', () => {
- expect(vm.$el).toHaveSpriteIcon('arrow-right');
- });
- });
-
- describe('when a job is retried', () => {
- beforeEach(() => {
- vm = mountComponent(Component, {
- job: {
- ...job,
- retried: true,
- },
- isActive: false,
- });
- });
-
- sharedTests();
-
- it('displays an icon', () => {
- expect(vm.$el).toHaveSpriteIcon('retry');
- });
- });
-
- describe('for delayed job', () => {
- beforeEach(() => {
- const remainingMilliseconds = 1337000;
- spyOn(Date, 'now').and.callFake(
- () => new Date(delayedJobFixture.scheduled_at).getTime() - remainingMilliseconds,
- );
- });
-
- it('displays remaining time in tooltip', done => {
- vm = mountComponent(Component, {
- job: delayedJobFixture,
- isActive: false,
- });
-
- Vue.nextTick()
- .then(() => {
- expect(vm.$el.querySelector('.js-job-link').getAttribute('data-original-title')).toEqual(
- 'delayed job - delayed manual action (00:22:17)',
- );
- })
- .then(done)
- .catch(done.fail);
- });
- });
-});
diff --git a/spec/javascripts/jobs/components/job_log_spec.js b/spec/javascripts/jobs/components/job_log_spec.js
deleted file mode 100644
index fcaf2b3bb64..00000000000
--- a/spec/javascripts/jobs/components/job_log_spec.js
+++ /dev/null
@@ -1,65 +0,0 @@
-import Vue from 'vue';
-import { mountComponentWithStore } from 'spec/helpers/vue_mount_component_helper';
-import component from '~/jobs/components/job_log.vue';
-import createStore from '~/jobs/store';
-import { resetStore } from '../store/helpers';
-
-describe('Job Log', () => {
- const Component = Vue.extend(component);
- let store;
- let vm;
-
- const trace =
- '<span>Running with gitlab-runner 12.1.0 (de7731dd)<br/></span><span> on docker-auto-scale-com d5ae8d25<br/></span><div class="append-right-8" data-timestamp="1565502765" data-section="prepare-executor" role="button"></div><span class="section section-header js-s-prepare-executor">Using Docker executor with image ruby:2.6 ...<br/></span>';
-
- beforeEach(() => {
- store = createStore();
- });
-
- afterEach(() => {
- resetStore(store);
- vm.$destroy();
- });
-
- it('renders provided trace', () => {
- vm = mountComponentWithStore(Component, {
- props: {
- trace,
- isComplete: true,
- },
- store,
- });
-
- expect(vm.$el.querySelector('code').textContent).toContain(
- 'Running with gitlab-runner 12.1.0 (de7731dd)',
- );
- });
-
- describe('while receiving trace', () => {
- it('renders animation', () => {
- vm = mountComponentWithStore(Component, {
- props: {
- trace,
- isComplete: false,
- },
- store,
- });
-
- expect(vm.$el.querySelector('.js-log-animation')).not.toBeNull();
- });
- });
-
- describe('when build trace has finishes', () => {
- it('does not render animation', () => {
- vm = mountComponentWithStore(Component, {
- props: {
- trace,
- isComplete: true,
- },
- store,
- });
-
- expect(vm.$el.querySelector('.js-log-animation')).toBeNull();
- });
- });
-});
diff --git a/spec/javascripts/jobs/components/sidebar_spec.js b/spec/javascripts/jobs/components/sidebar_spec.js
deleted file mode 100644
index 740bc3d0491..00000000000
--- a/spec/javascripts/jobs/components/sidebar_spec.js
+++ /dev/null
@@ -1,169 +0,0 @@
-import Vue from 'vue';
-import sidebarDetailsBlock from '~/jobs/components/sidebar.vue';
-import createStore from '~/jobs/store';
-import job, { jobsInStage } from '../mock_data';
-import { mountComponentWithStore } from '../../helpers/vue_mount_component_helper';
-import { trimText } from '../../helpers/text_helper';
-
-describe('Sidebar details block', () => {
- const SidebarComponent = Vue.extend(sidebarDetailsBlock);
- let vm;
- let store;
-
- beforeEach(() => {
- store = createStore();
- });
-
- afterEach(() => {
- vm.$destroy();
- });
-
- describe('when there is no retry path retry', () => {
- it('should not render a retry button', () => {
- const copy = Object.assign({}, job);
- delete copy.retry_path;
-
- store.dispatch('receiveJobSuccess', copy);
- vm = mountComponentWithStore(SidebarComponent, {
- store,
- });
-
- expect(vm.$el.querySelector('.js-retry-button')).toBeNull();
- });
- });
-
- describe('without terminal path', () => {
- it('does not render terminal link', () => {
- store.dispatch('receiveJobSuccess', job);
- vm = mountComponentWithStore(SidebarComponent, { store });
-
- expect(vm.$el.querySelector('.js-terminal-link')).toBeNull();
- });
- });
-
- describe('with terminal path', () => {
- it('renders terminal link', () => {
- store.dispatch(
- 'receiveJobSuccess',
- Object.assign({}, job, { terminal_path: 'job/43123/terminal' }),
- );
- vm = mountComponentWithStore(SidebarComponent, {
- store,
- });
-
- expect(vm.$el.querySelector('.js-terminal-link')).not.toBeNull();
- });
- });
-
- beforeEach(() => {
- store.dispatch('receiveJobSuccess', job);
- vm = mountComponentWithStore(SidebarComponent, { store });
- });
-
- describe('actions', () => {
- it('should render link to new issue', () => {
- expect(vm.$el.querySelector('.js-new-issue').getAttribute('href')).toEqual(
- job.new_issue_path,
- );
-
- expect(vm.$el.querySelector('.js-new-issue').textContent.trim()).toEqual('New issue');
- });
-
- it('should render link to retry job', () => {
- expect(vm.$el.querySelector('.js-retry-button').getAttribute('href')).toEqual(job.retry_path);
- });
-
- it('should render link to cancel job', () => {
- expect(vm.$el.querySelector('.js-cancel-job').getAttribute('href')).toEqual(job.cancel_path);
- });
- });
-
- describe('information', () => {
- it('should render job duration', () => {
- expect(trimText(vm.$el.querySelector('.js-job-duration').textContent)).toEqual(
- 'Duration: 6 seconds',
- );
- });
-
- it('should render erased date', () => {
- expect(trimText(vm.$el.querySelector('.js-job-erased').textContent)).toEqual(
- 'Erased: 3 weeks ago',
- );
- });
-
- it('should render finished date', () => {
- expect(trimText(vm.$el.querySelector('.js-job-finished').textContent)).toEqual(
- 'Finished: 3 weeks ago',
- );
- });
-
- it('should render queued date', () => {
- expect(trimText(vm.$el.querySelector('.js-job-queued').textContent)).toEqual(
- 'Queued: 9 seconds',
- );
- });
-
- it('should render runner ID', () => {
- expect(trimText(vm.$el.querySelector('.js-job-runner').textContent)).toEqual(
- 'Runner: local ci runner (#1)',
- );
- });
-
- it('should render timeout information', () => {
- expect(trimText(vm.$el.querySelector('.js-job-timeout').textContent)).toEqual(
- 'Timeout: 1m 40s (from runner)',
- );
- });
-
- it('should render coverage', () => {
- expect(trimText(vm.$el.querySelector('.js-job-coverage').textContent)).toEqual(
- 'Coverage: 20%',
- );
- });
-
- it('should render tags', () => {
- expect(trimText(vm.$el.querySelector('.js-job-tags').textContent)).toEqual('Tags: tag');
- });
- });
-
- describe('stages dropdown', () => {
- beforeEach(() => {
- store.dispatch('receiveJobSuccess', job);
- });
-
- describe('with stages', () => {
- beforeEach(() => {
- vm = mountComponentWithStore(SidebarComponent, { store });
- });
-
- it('renders value provided as selectedStage as selected', () => {
- expect(vm.$el.querySelector('.js-selected-stage').textContent.trim()).toEqual(
- vm.selectedStage,
- );
- });
- });
-
- describe('without jobs for stages', () => {
- beforeEach(() => {
- store.dispatch('receiveJobSuccess', job);
- vm = mountComponentWithStore(SidebarComponent, { store });
- });
-
- it('does not render job container', () => {
- expect(vm.$el.querySelector('.js-jobs-container')).toBeNull();
- });
- });
-
- describe('with jobs for stages', () => {
- beforeEach(() => {
- store.dispatch('receiveJobSuccess', job);
- store.dispatch('receiveJobsForStageSuccess', jobsInStage.latest_statuses);
- vm = mountComponentWithStore(SidebarComponent, { store });
- });
-
- it('renders list of jobs', () => {
- expect(vm.$el.querySelector('.js-jobs-container')).not.toBeNull();
- });
- });
- });
-});
diff --git a/spec/javascripts/jobs/components/stages_dropdown_spec.js b/spec/javascripts/jobs/components/stages_dropdown_spec.js
deleted file mode 100644
index f1a01530104..00000000000
--- a/spec/javascripts/jobs/components/stages_dropdown_spec.js
+++ /dev/null
@@ -1,163 +0,0 @@
-import Vue from 'vue';
-import { trimText } from 'spec/helpers/text_helper';
-import component from '~/jobs/components/stages_dropdown.vue';
-import mountComponent from '../../helpers/vue_mount_component_helper';
-
-describe('Stages Dropdown', () => {
- const Component = Vue.extend(component);
- let vm;
-
- const mockPipelineData = {
- id: 28029444,
- details: {
- status: {
- details_path: '/gitlab-org/gitlab-foss/pipelines/28029444',
- group: 'success',
- has_details: true,
- icon: 'status_success',
- label: 'passed',
- text: 'passed',
- tooltip: 'passed',
- },
- },
- path: 'pipeline/28029444',
- flags: {
- merge_request_pipeline: true,
- detached_merge_request_pipeline: false,
- },
- merge_request: {
- iid: 1234,
- path: '/root/detached-merge-request-pipelines/-/merge_requests/1',
- title: 'Update README.md',
- source_branch: 'feature-1234',
- source_branch_path: '/root/detached-merge-request-pipelines/branches/feature-1234',
- target_branch: 'master',
- target_branch_path: '/root/detached-merge-request-pipelines/branches/master',
- },
- ref: {
- name: 'test-branch',
- },
- };
-
- describe('without a merge request pipeline', () => {
- let pipeline;
-
- beforeEach(() => {
- pipeline = JSON.parse(JSON.stringify(mockPipelineData));
- delete pipeline.merge_request;
- delete pipeline.flags.merge_request_pipeline;
- delete pipeline.flags.detached_merge_request_pipeline;
-
- vm = mountComponent(Component, {
- pipeline,
- stages: [{ name: 'build' }, { name: 'test' }],
- selectedStage: 'deploy',
- });
- });
-
- afterEach(() => {
- vm.$destroy();
- });
-
- it('renders pipeline status', () => {
- expect(vm.$el.querySelector('.js-ci-status-icon-success')).not.toBeNull();
- });
-
- it('renders pipeline link', () => {
- expect(vm.$el.querySelector('.js-pipeline-path').getAttribute('href')).toEqual(
- 'pipeline/28029444',
- );
- });
-
- it('renders dropdown with stages', () => {
- expect(vm.$el.querySelector('.dropdown .js-stage-item').textContent).toContain('build');
- });
-
- it('rendes selected stage', () => {
- expect(vm.$el.querySelector('.dropdown .js-selected-stage').textContent).toContain('deploy');
- });
-
- it(`renders the pipeline info text like "Pipeline #123 for source_branch"`, () => {
- const expected = `Pipeline #${pipeline.id} for ${pipeline.ref.name}`;
- const actual = trimText(vm.$el.querySelector('.js-pipeline-info').innerText);
-
- expect(actual).toBe(expected);
- });
- });
-
- describe('with an "attached" merge request pipeline', () => {
- let pipeline;
-
- beforeEach(() => {
- pipeline = JSON.parse(JSON.stringify(mockPipelineData));
- pipeline.flags.merge_request_pipeline = true;
- pipeline.flags.detached_merge_request_pipeline = false;
-
- vm = mountComponent(Component, {
- pipeline,
- stages: [],
- selectedStage: 'deploy',
- });
- });
-
- it(`renders the pipeline info text like "Pipeline #123 for !456 with source_branch into target_branch"`, () => {
- const expected = `Pipeline #${pipeline.id} for !${pipeline.merge_request.iid} with ${pipeline.merge_request.source_branch} into ${pipeline.merge_request.target_branch}`;
- const actual = trimText(vm.$el.querySelector('.js-pipeline-info').innerText);
-
- expect(actual).toBe(expected);
- });
-
- it(`renders the correct merge request link`, () => {
- const actual = vm.$el.querySelector('.js-mr-link').href;
-
- expect(actual).toContain(pipeline.merge_request.path);
- });
-
- it(`renders the correct source branch link`, () => {
- const actual = vm.$el.querySelector('.js-source-branch-link').href;
-
- expect(actual).toContain(pipeline.merge_request.source_branch_path);
- });
-
- it(`renders the correct target branch link`, () => {
- const actual = vm.$el.querySelector('.js-target-branch-link').href;
-
- expect(actual).toContain(pipeline.merge_request.target_branch_path);
- });
- });
-
- describe('with a detached merge request pipeline', () => {
- let pipeline;
-
- beforeEach(() => {
- pipeline = JSON.parse(JSON.stringify(mockPipelineData));
- pipeline.flags.merge_request_pipeline = false;
- pipeline.flags.detached_merge_request_pipeline = true;
-
- vm = mountComponent(Component, {
- pipeline,
- stages: [],
- selectedStage: 'deploy',
- });
- });
-
- it(`renders the pipeline info like "Pipeline #123 for !456 with source_branch"`, () => {
- const expected = `Pipeline #${pipeline.id} for !${pipeline.merge_request.iid} with ${pipeline.merge_request.source_branch}`;
- const actual = trimText(vm.$el.querySelector('.js-pipeline-info').innerText);
-
- expect(actual).toBe(expected);
- });
-
- it(`renders the correct merge request link`, () => {
- const actual = vm.$el.querySelector('.js-mr-link').href;
-
- expect(actual).toContain(pipeline.merge_request.path);
- });
-
- it(`renders the correct source branch link`, () => {
- const actual = vm.$el.querySelector('.js-source-branch-link').href;
-
- expect(actual).toContain(pipeline.merge_request.source_branch_path);
- });
- });
-});
diff --git a/spec/javascripts/jobs/mixins/delayed_job_mixin_spec.js b/spec/javascripts/jobs/mixins/delayed_job_mixin_spec.js
deleted file mode 100644
index b67187f1d50..00000000000
--- a/spec/javascripts/jobs/mixins/delayed_job_mixin_spec.js
+++ /dev/null
@@ -1,93 +0,0 @@
-import Vue from 'vue';
-import mountComponent from 'spec/helpers/vue_mount_component_helper';
-import delayedJobMixin from '~/jobs/mixins/delayed_job_mixin';
-
-describe('DelayedJobMixin', () => {
- const delayedJobFixture = getJSONFixture('jobs/delayed.json');
- const dummyComponent = Vue.extend({
- mixins: [delayedJobMixin],
- props: {
- job: {
- type: Object,
- required: true,
- },
- },
- template: '<div>{{ remainingTime }}</div>',
- });
-
- let vm;
-
- beforeEach(() => {
- jasmine.clock().install();
- });
-
- afterEach(() => {
- vm.$destroy();
- jasmine.clock().uninstall();
- });
-
- describe('if job is empty object', () => {
- beforeEach(() => {
- vm = mountComponent(dummyComponent, {
- job: {},
- });
- });
-
- it('sets remaining time to 00:00:00', () => {
- expect(vm.$el.innerText).toBe('00:00:00');
- });
-
- describe('after mounting', () => {
- beforeEach(done => {
- Vue.nextTick()
- .then(done)
- .catch(done.fail);
- });
-
- it('doe not update remaining time', () => {
- expect(vm.$el.innerText).toBe('00:00:00');
- });
- });
- });
-
- describe('if job is delayed job', () => {
- let remainingTimeInMilliseconds = 42000;
-
- beforeEach(() => {
- spyOn(Date, 'now').and.callFake(
- () => new Date(delayedJobFixture.scheduled_at).getTime() - remainingTimeInMilliseconds,
- );
- vm = mountComponent(dummyComponent, {
- job: delayedJobFixture,
- });
- });
-
- it('sets remaining time to 00:00:00', () => {
- expect(vm.$el.innerText).toBe('00:00:00');
- });
-
- describe('after mounting', () => {
- beforeEach(done => {
- Vue.nextTick()
- .then(done)
- .catch(done.fail);
- });
-
- it('sets remaining time', () => {
- expect(vm.$el.innerText).toBe('00:00:42');
- });
-
- it('updates remaining time', done => {
- remainingTimeInMilliseconds = 41000;
- jasmine.clock().tick(1000);
-
- Vue.nextTick()
- .then(() => {
- expect(vm.$el.innerText).toBe('00:00:41');
- })
- .then(done)
- .catch(done.fail);
- });
- });
- });
-});
diff --git a/spec/javascripts/jobs/store/actions_spec.js b/spec/javascripts/jobs/store/actions_spec.js
deleted file mode 100644
index 47257688bd5..00000000000
--- a/spec/javascripts/jobs/store/actions_spec.js
+++ /dev/null
@@ -1,512 +0,0 @@
-import MockAdapter from 'axios-mock-adapter';
-import testAction from 'spec/helpers/vuex_action_helper';
-import { TEST_HOST } from 'spec/test_constants';
-import axios from '~/lib/utils/axios_utils';
-import {
- setJobEndpoint,
- setTraceOptions,
- clearEtagPoll,
- stopPolling,
- requestJob,
- fetchJob,
- receiveJobSuccess,
- receiveJobError,
- scrollTop,
- scrollBottom,
- requestTrace,
- fetchTrace,
- startPollingTrace,
- stopPollingTrace,
- receiveTraceSuccess,
- receiveTraceError,
- toggleCollapsibleLine,
- requestJobsForStage,
- fetchJobsForStage,
- receiveJobsForStageSuccess,
- receiveJobsForStageError,
- hideSidebar,
- showSidebar,
- toggleSidebar,
-} from '~/jobs/store/actions';
-import state from '~/jobs/store/state';
-import * as types from '~/jobs/store/mutation_types';
-
-describe('Job State actions', () => {
- let mockedState;
-
- beforeEach(() => {
- mockedState = state();
- });
-
- describe('setJobEndpoint', () => {
- it('should commit SET_JOB_ENDPOINT mutation', done => {
- testAction(
- setJobEndpoint,
- 'job/872324.json',
- mockedState,
- [{ type: types.SET_JOB_ENDPOINT, payload: 'job/872324.json' }],
- [],
- done,
- );
- });
- });
-
- describe('setTraceOptions', () => {
- it('should commit SET_TRACE_OPTIONS mutation', done => {
- testAction(
- setTraceOptions,
- { pagePath: 'job/872324/trace.json' },
- mockedState,
- [{ type: types.SET_TRACE_OPTIONS, payload: { pagePath: 'job/872324/trace.json' } }],
- [],
- done,
- );
- });
- });
-
- describe('hideSidebar', () => {
- it('should commit HIDE_SIDEBAR mutation', done => {
- testAction(hideSidebar, null, mockedState, [{ type: types.HIDE_SIDEBAR }], [], done);
- });
- });
-
- describe('showSidebar', () => {
- it('should commit HIDE_SIDEBAR mutation', done => {
- testAction(showSidebar, null, mockedState, [{ type: types.SHOW_SIDEBAR }], [], done);
- });
- });
-
- describe('toggleSidebar', () => {
- describe('when isSidebarOpen is true', () => {
- it('should dispatch hideSidebar', done => {
- testAction(toggleSidebar, null, mockedState, [], [{ type: 'hideSidebar' }], done);
- });
- });
-
- describe('when isSidebarOpen is false', () => {
- it('should dispatch showSidebar', done => {
- mockedState.isSidebarOpen = false;
-
- testAction(toggleSidebar, null, mockedState, [], [{ type: 'showSidebar' }], done);
- });
- });
- });
-
- describe('requestJob', () => {
- it('should commit REQUEST_JOB mutation', done => {
- testAction(requestJob, null, mockedState, [{ type: types.REQUEST_JOB }], [], done);
- });
- });
-
- describe('fetchJob', () => {
- let mock;
-
- beforeEach(() => {
- mockedState.jobEndpoint = `${TEST_HOST}/endpoint.json`;
- mock = new MockAdapter(axios);
- });
-
- afterEach(() => {
- mock.restore();
- stopPolling();
- clearEtagPoll();
- });
-
- describe('success', () => {
- it('dispatches requestJob and receiveJobSuccess ', done => {
- mock.onGet(`${TEST_HOST}/endpoint.json`).replyOnce(200, { id: 121212, name: 'karma' });
-
- testAction(
- fetchJob,
- null,
- mockedState,
- [],
- [
- {
- type: 'requestJob',
- },
- {
- payload: { id: 121212, name: 'karma' },
- type: 'receiveJobSuccess',
- },
- ],
- done,
- );
- });
- });
-
- describe('error', () => {
- beforeEach(() => {
- mock.onGet(`${TEST_HOST}/endpoint.json`).reply(500);
- });
-
- it('dispatches requestJob and receiveJobError ', done => {
- testAction(
- fetchJob,
- null,
- mockedState,
- [],
- [
- {
- type: 'requestJob',
- },
- {
- type: 'receiveJobError',
- },
- ],
- done,
- );
- });
- });
- });
-
- describe('receiveJobSuccess', () => {
- it('should commit RECEIVE_JOB_SUCCESS mutation', done => {
- testAction(
- receiveJobSuccess,
- { id: 121232132 },
- mockedState,
- [{ type: types.RECEIVE_JOB_SUCCESS, payload: { id: 121232132 } }],
- [],
- done,
- );
- });
- });
-
- describe('receiveJobError', () => {
- it('should commit RECEIVE_JOB_ERROR mutation', done => {
- testAction(receiveJobError, null, mockedState, [{ type: types.RECEIVE_JOB_ERROR }], [], done);
- });
- });
-
- describe('scrollTop', () => {
- it('should dispatch toggleScrollButtons action', done => {
- testAction(scrollTop, null, mockedState, [], [{ type: 'toggleScrollButtons' }], done);
- });
- });
-
- describe('scrollBottom', () => {
- it('should dispatch toggleScrollButtons action', done => {
- testAction(scrollBottom, null, mockedState, [], [{ type: 'toggleScrollButtons' }], done);
- });
- });
-
- describe('requestTrace', () => {
- it('should commit REQUEST_TRACE mutation', done => {
- testAction(requestTrace, null, mockedState, [{ type: types.REQUEST_TRACE }], [], done);
- });
- });
-
- describe('fetchTrace', () => {
- let mock;
-
- beforeEach(() => {
- mockedState.traceEndpoint = `${TEST_HOST}/endpoint`;
- mock = new MockAdapter(axios);
- });
-
- afterEach(() => {
- mock.restore();
- stopPolling();
- clearEtagPoll();
- });
-
- describe('success', () => {
- it('dispatches requestTrace, receiveTraceSuccess and stopPollingTrace when job is complete', done => {
- mock.onGet(`${TEST_HOST}/endpoint/trace.json`).replyOnce(200, {
- html: 'I, [2018-08-17T22:57:45.707325 #1841] INFO -- :',
- complete: true,
- });
-
- testAction(
- fetchTrace,
- null,
- mockedState,
- [],
- [
- {
- type: 'toggleScrollisInBottom',
- payload: true,
- },
- {
- payload: {
- html: 'I, [2018-08-17T22:57:45.707325 #1841] INFO -- :',
- complete: true,
- },
- type: 'receiveTraceSuccess',
- },
- {
- type: 'stopPollingTrace',
- },
- ],
- done,
- );
- });
-
- describe('when job is incomplete', () => {
- let tracePayload;
-
- beforeEach(() => {
- tracePayload = {
- html: 'I, [2018-08-17T22:57:45.707325 #1841] INFO -- :',
- complete: false,
- };
-
- mock.onGet(`${TEST_HOST}/endpoint/trace.json`).replyOnce(200, tracePayload);
- });
-
- it('dispatches startPollingTrace', done => {
- testAction(
- fetchTrace,
- null,
- mockedState,
- [],
- [
- { type: 'toggleScrollisInBottom', payload: true },
- { type: 'receiveTraceSuccess', payload: tracePayload },
- { type: 'startPollingTrace' },
- ],
- done,
- );
- });
-
- it('does not dispatch startPollingTrace when timeout is non-empty', done => {
- mockedState.traceTimeout = 1;
-
- testAction(
- fetchTrace,
- null,
- mockedState,
- [],
- [
- { type: 'toggleScrollisInBottom', payload: true },
- { type: 'receiveTraceSuccess', payload: tracePayload },
- ],
- done,
- );
- });
- });
- });
-
- describe('error', () => {
- beforeEach(() => {
- mock.onGet(`${TEST_HOST}/endpoint/trace.json`).reply(500);
- });
-
- it('dispatches requestTrace and receiveTraceError ', done => {
- testAction(
- fetchTrace,
- null,
- mockedState,
- [],
- [
- {
- type: 'receiveTraceError',
- },
- ],
- done,
- );
- });
- });
- });
-
- describe('startPollingTrace', () => {
- let dispatch;
- let commit;
-
- beforeEach(() => {
- jasmine.clock().install();
-
- dispatch = jasmine.createSpy();
- commit = jasmine.createSpy();
-
- startPollingTrace({ dispatch, commit });
- });
-
- afterEach(() => {
- jasmine.clock().uninstall();
- });
-
- it('should save the timeout id but not call fetchTrace', () => {
- expect(commit).toHaveBeenCalledWith(types.SET_TRACE_TIMEOUT, 1);
- expect(dispatch).not.toHaveBeenCalledWith('fetchTrace');
- });
-
- describe('after timeout has passed', () => {
- beforeEach(() => {
- jasmine.clock().tick(4000);
- });
-
- it('should clear the timeout id and fetchTrace', () => {
- expect(commit).toHaveBeenCalledWith(types.SET_TRACE_TIMEOUT, 0);
- expect(dispatch).toHaveBeenCalledWith('fetchTrace');
- });
- });
- });
-
- describe('stopPollingTrace', () => {
- let origTimeout;
-
- beforeEach(() => {
- // Can't use spyOn(window, 'clearTimeout') because this caused unrelated specs to timeout
- // https://gitlab.com/gitlab-org/gitlab/-/merge_requests/23838#note_280277727
- origTimeout = window.clearTimeout;
- window.clearTimeout = jasmine.createSpy();
- });
-
- afterEach(() => {
- window.clearTimeout = origTimeout;
- });
-
- it('should commit STOP_POLLING_TRACE mutation ', done => {
- const traceTimeout = 7;
-
- testAction(
- stopPollingTrace,
- null,
- { ...mockedState, traceTimeout },
- [{ type: types.SET_TRACE_TIMEOUT, payload: 0 }, { type: types.STOP_POLLING_TRACE }],
- [],
- )
- .then(() => {
- expect(window.clearTimeout).toHaveBeenCalledWith(traceTimeout);
- })
- .then(done)
- .catch(done.fail);
- });
- });
-
- describe('receiveTraceSuccess', () => {
- it('should commit RECEIVE_TRACE_SUCCESS mutation ', done => {
- testAction(
- receiveTraceSuccess,
- 'hello world',
- mockedState,
- [{ type: types.RECEIVE_TRACE_SUCCESS, payload: 'hello world' }],
- [],
- done,
- );
- });
- });
-
- describe('receiveTraceError', () => {
- it('should commit stop polling trace', done => {
- testAction(receiveTraceError, null, mockedState, [], [{ type: 'stopPollingTrace' }], done);
- });
- });
-
- describe('toggleCollapsibleLine', () => {
- it('should commit TOGGLE_COLLAPSIBLE_LINE mutation ', done => {
- testAction(
- toggleCollapsibleLine,
- { isClosed: true },
- mockedState,
- [{ type: types.TOGGLE_COLLAPSIBLE_LINE, payload: { isClosed: true } }],
- [],
- done,
- );
- });
- });
-
- describe('requestJobsForStage', () => {
- it('should commit REQUEST_JOBS_FOR_STAGE mutation ', done => {
- testAction(
- requestJobsForStage,
- { name: 'deploy' },
- mockedState,
- [{ type: types.REQUEST_JOBS_FOR_STAGE, payload: { name: 'deploy' } }],
- [],
- done,
- );
- });
- });
-
- describe('fetchJobsForStage', () => {
- let mock;
-
- beforeEach(() => {
- mock = new MockAdapter(axios);
- });
-
- afterEach(() => {
- mock.restore();
- });
-
- describe('success', () => {
- it('dispatches requestJobsForStage and receiveJobsForStageSuccess ', done => {
- mock
- .onGet(`${TEST_HOST}/jobs.json`)
- .replyOnce(200, { latest_statuses: [{ id: 121212, name: 'build' }], retried: [] });
-
- testAction(
- fetchJobsForStage,
- { dropdown_path: `${TEST_HOST}/jobs.json` },
- mockedState,
- [],
- [
- {
- type: 'requestJobsForStage',
- payload: { dropdown_path: `${TEST_HOST}/jobs.json` },
- },
- {
- payload: [{ id: 121212, name: 'build' }],
- type: 'receiveJobsForStageSuccess',
- },
- ],
- done,
- );
- });
- });
-
- describe('error', () => {
- beforeEach(() => {
- mock.onGet(`${TEST_HOST}/jobs.json`).reply(500);
- });
-
- it('dispatches requestJobsForStage and receiveJobsForStageError', done => {
- testAction(
- fetchJobsForStage,
- { dropdown_path: `${TEST_HOST}/jobs.json` },
- mockedState,
- [],
- [
- {
- type: 'requestJobsForStage',
- payload: { dropdown_path: `${TEST_HOST}/jobs.json` },
- },
- {
- type: 'receiveJobsForStageError',
- },
- ],
- done,
- );
- });
- });
- });
-
- describe('receiveJobsForStageSuccess', () => {
- it('should commit RECEIVE_JOBS_FOR_STAGE_SUCCESS mutation ', done => {
- testAction(
- receiveJobsForStageSuccess,
- [{ id: 121212, name: 'karma' }],
- mockedState,
- [{ type: types.RECEIVE_JOBS_FOR_STAGE_SUCCESS, payload: [{ id: 121212, name: 'karma' }] }],
- [],
- done,
- );
- });
- });
-
- describe('receiveJobsForStageError', () => {
- it('should commit RECEIVE_JOBS_FOR_STAGE_ERROR mutation ', done => {
- testAction(
- receiveJobsForStageError,
- null,
- mockedState,
- [{ type: types.RECEIVE_JOBS_FOR_STAGE_ERROR }],
- [],
- done,
- );
- });
- });
-});
diff --git a/spec/javascripts/landing_spec.js b/spec/javascripts/landing_spec.js
deleted file mode 100644
index bffef8fc64f..00000000000
--- a/spec/javascripts/landing_spec.js
+++ /dev/null
@@ -1,166 +0,0 @@
-import Cookies from 'js-cookie';
-import Landing from '~/landing';
-
-describe('Landing', function() {
- describe('class constructor', function() {
- beforeEach(function() {
- this.landingElement = {};
- this.dismissButton = {};
- this.cookieName = 'cookie_name';
-
- this.landing = new Landing(this.landingElement, this.dismissButton, this.cookieName);
- });
-
- it('should set .landing', function() {
- expect(this.landing.landingElement).toBe(this.landingElement);
- });
-
- it('should set .cookieName', function() {
- expect(this.landing.cookieName).toBe(this.cookieName);
- });
-
- it('should set .dismissButton', function() {
- expect(this.landing.dismissButton).toBe(this.dismissButton);
- });
-
- it('should set .eventWrapper', function() {
- expect(this.landing.eventWrapper).toEqual({});
- });
- });
-
- describe('toggle', function() {
- beforeEach(function() {
- this.isDismissed = false;
- this.landingElement = { classList: jasmine.createSpyObj('classList', ['toggle']) };
- this.landing = {
- isDismissed: () => {},
- addEvents: () => {},
- landingElement: this.landingElement,
- };
-
- spyOn(this.landing, 'isDismissed').and.returnValue(this.isDismissed);
- spyOn(this.landing, 'addEvents');
-
- Landing.prototype.toggle.call(this.landing);
- });
-
- it('should call .isDismissed', function() {
- expect(this.landing.isDismissed).toHaveBeenCalled();
- });
-
- it('should call .classList.toggle', function() {
- expect(this.landingElement.classList.toggle).toHaveBeenCalledWith('hidden', this.isDismissed);
- });
-
- it('should call .addEvents', function() {
- expect(this.landing.addEvents).toHaveBeenCalled();
- });
-
- describe('if isDismissed is true', function() {
- beforeEach(function() {
- this.isDismissed = true;
- this.landingElement = { classList: jasmine.createSpyObj('classList', ['toggle']) };
- this.landing = {
- isDismissed: () => {},
- addEvents: () => {},
- landingElement: this.landingElement,
- };
-
- spyOn(this.landing, 'isDismissed').and.returnValue(this.isDismissed);
- spyOn(this.landing, 'addEvents');
-
- this.landing.isDismissed.calls.reset();
-
- Landing.prototype.toggle.call(this.landing);
- });
-
- it('should not call .addEvents', function() {
- expect(this.landing.addEvents).not.toHaveBeenCalled();
- });
- });
- });
-
- describe('addEvents', function() {
- beforeEach(function() {
- this.dismissButton = jasmine.createSpyObj('dismissButton', ['addEventListener']);
- this.eventWrapper = {};
- this.landing = {
- eventWrapper: this.eventWrapper,
- dismissButton: this.dismissButton,
- dismissLanding: () => {},
- };
-
- Landing.prototype.addEvents.call(this.landing);
- });
-
- it('should set .eventWrapper.dismissLanding', function() {
- expect(this.eventWrapper.dismissLanding).toEqual(jasmine.any(Function));
- });
-
- it('should call .addEventListener', function() {
- expect(this.dismissButton.addEventListener).toHaveBeenCalledWith(
- 'click',
- this.eventWrapper.dismissLanding,
- );
- });
- });
-
- describe('removeEvents', function() {
- beforeEach(function() {
- this.dismissButton = jasmine.createSpyObj('dismissButton', ['removeEventListener']);
- this.eventWrapper = { dismissLanding: () => {} };
- this.landing = {
- eventWrapper: this.eventWrapper,
- dismissButton: this.dismissButton,
- };
-
- Landing.prototype.removeEvents.call(this.landing);
- });
-
- it('should call .removeEventListener', function() {
- expect(this.dismissButton.removeEventListener).toHaveBeenCalledWith(
- 'click',
- this.eventWrapper.dismissLanding,
- );
- });
- });
-
- describe('dismissLanding', function() {
- beforeEach(function() {
- this.landingElement = { classList: jasmine.createSpyObj('classList', ['add']) };
- this.cookieName = 'cookie_name';
- this.landing = { landingElement: this.landingElement, cookieName: this.cookieName };
-
- spyOn(Cookies, 'set');
-
- Landing.prototype.dismissLanding.call(this.landing);
- });
-
- it('should call .classList.add', function() {
- expect(this.landingElement.classList.add).toHaveBeenCalledWith('hidden');
- });
-
- it('should call Cookies.set', function() {
- expect(Cookies.set).toHaveBeenCalledWith(this.cookieName, 'true', { expires: 365 });
- });
- });
-
- describe('isDismissed', function() {
- beforeEach(function() {
- this.cookieName = 'cookie_name';
- this.landing = { cookieName: this.cookieName };
-
- spyOn(Cookies, 'get').and.returnValue('true');
-
- this.isDismissed = Landing.prototype.isDismissed.call(this.landing);
- });
-
- it('should call Cookies.get', function() {
- expect(Cookies.get).toHaveBeenCalledWith(this.cookieName);
- });
-
- it('should return a boolean', function() {
- expect(typeof this.isDismissed).toEqual('boolean');
- });
- });
-});
diff --git a/spec/javascripts/lib/utils/csrf_token_spec.js b/spec/javascripts/lib/utils/csrf_token_spec.js
deleted file mode 100644
index 867bee34ee5..00000000000
--- a/spec/javascripts/lib/utils/csrf_token_spec.js
+++ /dev/null
@@ -1,50 +0,0 @@
-import csrf from '~/lib/utils/csrf';
-
-describe('csrf', function() {
- beforeEach(() => {
- this.tokenKey = 'X-CSRF-Token';
- this.token =
- 'pH1cvjnP9grx2oKlhWEDvUZnJ8x2eXsIs1qzyHkF3DugSG5yTxR76CWeEZRhML2D1IeVB7NEW0t5l/axE4iJpQ==';
- });
-
- it('returns the correct headerKey', () => {
- expect(csrf.headerKey).toBe(this.tokenKey);
- });
-
- describe('when csrf token is in the DOM', () => {
- beforeEach(() => {
- setFixtures(`
- <meta name="csrf-token" content="${this.token}">
- `);
-
- csrf.init();
- });
-
- it('returns the csrf token', () => {
- expect(csrf.token).toBe(this.token);
- });
-
- it('returns the csrf headers object', () => {
- expect(csrf.headers[this.tokenKey]).toBe(this.token);
- });
- });
-
- describe('when csrf token is not in the DOM', () => {
- beforeEach(() => {
- setFixtures(`
- <meta name="some-other-token">
- `);
-
- csrf.init();
- });
-
- it('returns null for token', () => {
- expect(csrf.token).toBeNull();
- });
-
- it('returns empty object for headers', () => {
- expect(typeof csrf.headers).toBe('object');
- expect(Object.keys(csrf.headers).length).toBe(0);
- });
- });
-});
diff --git a/spec/javascripts/lib/utils/navigation_utility_spec.js b/spec/javascripts/lib/utils/navigation_utility_spec.js
deleted file mode 100644
index be620e4a27c..00000000000
--- a/spec/javascripts/lib/utils/navigation_utility_spec.js
+++ /dev/null
@@ -1,23 +0,0 @@
-import findAndFollowLink from '~/lib/utils/navigation_utility';
-
-describe('findAndFollowLink', () => {
- it('visits a link when the selector exists', () => {
- const href = '/some/path';
- const visitUrl = spyOnDependency(findAndFollowLink, 'visitUrl');
-
- setFixtures(`<a class="my-shortcut" href="${href}">link</a>`);
-
- findAndFollowLink('.my-shortcut');
-
- expect(visitUrl).toHaveBeenCalledWith(href);
- });
-
- it('does not throw an exception when the selector does not exist', () => {
- const visitUrl = spyOnDependency(findAndFollowLink, 'visitUrl');
-
- // this should not throw an exception
- findAndFollowLink('.this-selector-does-not-exist');
-
- expect(visitUrl).not.toHaveBeenCalled();
- });
-});
diff --git a/spec/javascripts/lib/utils/poll_spec.js b/spec/javascripts/lib/utils/poll_spec.js
deleted file mode 100644
index 138041a349f..00000000000
--- a/spec/javascripts/lib/utils/poll_spec.js
+++ /dev/null
@@ -1,222 +0,0 @@
-/* eslint-disable jasmine/no-unsafe-spy */
-
-import Poll from '~/lib/utils/poll';
-import { successCodes } from '~/lib/utils/http_status';
-
-const waitForAllCallsToFinish = (service, waitForCount, successCallback) => {
- const timer = () => {
- setTimeout(() => {
- if (service.fetch.calls.count() === waitForCount) {
- successCallback();
- } else {
- timer();
- }
- }, 0);
- };
-
- timer();
-};
-
-function mockServiceCall(service, response, shouldFail = false) {
- const action = shouldFail ? Promise.reject : Promise.resolve;
- const responseObject = response;
-
- if (!responseObject.headers) responseObject.headers = {};
-
- service.fetch.and.callFake(action.bind(Promise, responseObject));
-}
-
-describe('Poll', () => {
- const service = jasmine.createSpyObj('service', ['fetch']);
- const callbacks = jasmine.createSpyObj('callbacks', ['success', 'error', 'notification']);
-
- function setup() {
- return new Poll({
- resource: service,
- method: 'fetch',
- successCallback: callbacks.success,
- errorCallback: callbacks.error,
- notificationCallback: callbacks.notification,
- }).makeRequest();
- }
-
- afterEach(() => {
- callbacks.success.calls.reset();
- callbacks.error.calls.reset();
- callbacks.notification.calls.reset();
- service.fetch.calls.reset();
- });
-
- it('calls the success callback when no header for interval is provided', done => {
- mockServiceCall(service, { status: 200 });
- setup();
-
- waitForAllCallsToFinish(service, 1, () => {
- expect(callbacks.success).toHaveBeenCalled();
- expect(callbacks.error).not.toHaveBeenCalled();
-
- done();
- });
- });
-
- it('calls the error callback when the http request returns an error', done => {
- mockServiceCall(service, { status: 500 }, true);
- setup();
-
- waitForAllCallsToFinish(service, 1, () => {
- expect(callbacks.success).not.toHaveBeenCalled();
- expect(callbacks.error).toHaveBeenCalled();
-
- done();
- });
- });
-
- it('skips the error callback when request is aborted', done => {
- mockServiceCall(service, { status: 0 }, true);
- setup();
-
- waitForAllCallsToFinish(service, 1, () => {
- expect(callbacks.success).not.toHaveBeenCalled();
- expect(callbacks.error).not.toHaveBeenCalled();
- expect(callbacks.notification).toHaveBeenCalled();
-
- done();
- });
- });
-
- it('should call the success callback when the interval header is -1', done => {
- mockServiceCall(service, { status: 200, headers: { 'poll-interval': -1 } });
- setup()
- .then(() => {
- expect(callbacks.success).toHaveBeenCalled();
- expect(callbacks.error).not.toHaveBeenCalled();
-
- done();
- })
- .catch(done.fail);
- });
-
- describe('for 2xx status code', () => {
- successCodes.forEach(httpCode => {
- it(`starts polling when http status is ${httpCode} and interval header is provided`, done => {
- mockServiceCall(service, { status: httpCode, headers: { 'poll-interval': 1 } });
-
- const Polling = new Poll({
- resource: service,
- method: 'fetch',
- data: { page: 1 },
- successCallback: callbacks.success,
- errorCallback: callbacks.error,
- });
-
- Polling.makeRequest();
-
- waitForAllCallsToFinish(service, 2, () => {
- Polling.stop();
-
- expect(service.fetch.calls.count()).toEqual(2);
- expect(service.fetch).toHaveBeenCalledWith({ page: 1 });
- expect(callbacks.success).toHaveBeenCalled();
- expect(callbacks.error).not.toHaveBeenCalled();
-
- done();
- });
- });
- });
- });
-
- describe('stop', () => {
- it('stops polling when method is called', done => {
- mockServiceCall(service, { status: 200, headers: { 'poll-interval': 1 } });
-
- const Polling = new Poll({
- resource: service,
- method: 'fetch',
- data: { page: 1 },
- successCallback: () => {
- Polling.stop();
- },
- errorCallback: callbacks.error,
- });
-
- spyOn(Polling, 'stop').and.callThrough();
-
- Polling.makeRequest();
-
- waitForAllCallsToFinish(service, 1, () => {
- expect(service.fetch.calls.count()).toEqual(1);
- expect(service.fetch).toHaveBeenCalledWith({ page: 1 });
- expect(Polling.stop).toHaveBeenCalled();
-
- done();
- });
- });
- });
-
- describe('enable', () => {
- it('should enable polling upon a response', done => {
- jasmine.clock().install();
-
- const Polling = new Poll({
- resource: service,
- method: 'fetch',
- data: { page: 1 },
- successCallback: () => {},
- });
-
- Polling.enable({
- data: { page: 4 },
- response: { status: 200, headers: { 'poll-interval': 1 } },
- });
-
- jasmine.clock().tick(1);
- jasmine.clock().uninstall();
-
- waitForAllCallsToFinish(service, 1, () => {
- Polling.stop();
-
- expect(service.fetch.calls.count()).toEqual(1);
- expect(service.fetch).toHaveBeenCalledWith({ page: 4 });
- expect(Polling.options.data).toEqual({ page: 4 });
- done();
- });
- });
- });
-
- describe('restart', () => {
- it('should restart polling when its called', done => {
- mockServiceCall(service, { status: 200, headers: { 'poll-interval': 1 } });
-
- const Polling = new Poll({
- resource: service,
- method: 'fetch',
- data: { page: 1 },
- successCallback: () => {
- Polling.stop();
- setTimeout(() => {
- Polling.restart({ data: { page: 4 } });
- }, 0);
- },
- errorCallback: callbacks.error,
- });
-
- spyOn(Polling, 'stop').and.callThrough();
- spyOn(Polling, 'enable').and.callThrough();
- spyOn(Polling, 'restart').and.callThrough();
-
- Polling.makeRequest();
-
- waitForAllCallsToFinish(service, 2, () => {
- Polling.stop();
-
- expect(service.fetch.calls.count()).toEqual(2);
- expect(service.fetch).toHaveBeenCalledWith({ page: 4 });
- expect(Polling.stop).toHaveBeenCalled();
- expect(Polling.enable).toHaveBeenCalled();
- expect(Polling.restart).toHaveBeenCalled();
- expect(Polling.options.data).toEqual({ page: 4 });
- done();
- });
- });
- });
-});
diff --git a/spec/javascripts/lib/utils/sticky_spec.js b/spec/javascripts/lib/utils/sticky_spec.js
deleted file mode 100644
index 1b1e7da1ed3..00000000000
--- a/spec/javascripts/lib/utils/sticky_spec.js
+++ /dev/null
@@ -1,66 +0,0 @@
-import { isSticky } from '~/lib/utils/sticky';
-
-describe('sticky', () => {
- let el;
-
- beforeEach(() => {
- document.body.innerHTML += `
- <div class="parent">
- <div id="js-sticky"></div>
- </div>
- `;
-
- el = document.getElementById('js-sticky');
- });
-
- afterEach(() => {
- el.parentNode.remove();
- });
-
- describe('when stuck', () => {
- it('does not remove is-stuck class', () => {
- isSticky(el, 0, el.offsetTop);
- isSticky(el, 0, el.offsetTop);
-
- expect(el.classList.contains('is-stuck')).toBeTruthy();
- });
-
- it('adds is-stuck class', () => {
- isSticky(el, 0, el.offsetTop);
-
- expect(el.classList.contains('is-stuck')).toBeTruthy();
- });
-
- it('inserts placeholder element', () => {
- isSticky(el, 0, el.offsetTop, true);
-
- expect(document.querySelector('.sticky-placeholder')).not.toBeNull();
- });
- });
-
- describe('when not stuck', () => {
- it('removes is-stuck class', () => {
- spyOn(el.classList, 'remove').and.callThrough();
-
- isSticky(el, 0, el.offsetTop);
- isSticky(el, 0, 0);
-
- expect(el.classList.remove).toHaveBeenCalledWith('is-stuck');
-
- expect(el.classList.contains('is-stuck')).toBeFalsy();
- });
-
- it('does not add is-stuck class', () => {
- isSticky(el, 0, 0);
-
- expect(el.classList.contains('is-stuck')).toBeFalsy();
- });
-
- it('removes placeholder', () => {
- isSticky(el, 0, el.offsetTop, true);
- isSticky(el, 0, 0, true);
-
- expect(document.querySelector('.sticky-placeholder')).toBeNull();
- });
- });
-});
diff --git a/spec/javascripts/line_highlighter_spec.js b/spec/javascripts/line_highlighter_spec.js
index 45b10fc3bd8..bedab0fd003 100644
--- a/spec/javascripts/line_highlighter_spec.js
+++ b/spec/javascripts/line_highlighter_spec.js
@@ -1,4 +1,4 @@
-/* eslint-disable no-else-return, dot-notation, no-return-assign, no-new, no-underscore-dangle */
+/* eslint-disable dot-notation, no-return-assign, no-new, no-underscore-dangle */
import $ from 'jquery';
import LineHighlighter from '~/line_highlighter';
@@ -8,10 +8,9 @@ describe('LineHighlighter', function() {
const clickLine = function(number, eventData = {}) {
if ($.isEmptyObject(eventData)) {
return $(`#L${number}`).click();
- } else {
- const e = $.Event('click', eventData);
- return $(`#L${number}`).trigger(e);
}
+ const e = $.Event('click', eventData);
+ return $(`#L${number}`).trigger(e);
};
beforeEach(function() {
loadFixtures('static/line_highlighter.html');
@@ -67,6 +66,16 @@ describe('LineHighlighter', function() {
expect(func).not.toThrow();
});
+
+ it('handles hashchange event', () => {
+ const highlighter = new LineHighlighter();
+
+ spyOn(highlighter, 'highlightHash');
+
+ window.dispatchEvent(new Event('hashchange'), 'L15');
+
+ expect(highlighter.highlightHash).toHaveBeenCalled();
+ });
});
describe('clickHandler', function() {
diff --git a/spec/javascripts/monitoring/components/dashboard_resize_browser_spec.js b/spec/javascripts/monitoring/components/dashboard_resize_browser_spec.js
new file mode 100644
index 00000000000..4416dbd014a
--- /dev/null
+++ b/spec/javascripts/monitoring/components/dashboard_resize_browser_spec.js
@@ -0,0 +1,94 @@
+/**
+ * This file should only contain browser specific specs.
+ * If you need to add or update a spec, please see spec/frontend/monitoring/components/*.js
+ * https://gitlab.com/gitlab-org/gitlab/-/issues/194244#note_343427737
+ * https://gitlab.com/groups/gitlab-org/-/epics/895#what-if-theres-a-karma-spec-which-is-simply-unmovable-to-jest-ie-it-is-dependent-on-a-running-browser-environment
+ */
+
+import Vue from 'vue';
+import { createLocalVue } from '@vue/test-utils';
+import MockAdapter from 'axios-mock-adapter';
+import Dashboard from '~/monitoring/components/dashboard.vue';
+import { createStore } from '~/monitoring/stores';
+import axios from '~/lib/utils/axios_utils';
+import { mockApiEndpoint, propsData } from '../mock_data';
+import { metricsDashboardPayload } from '../fixture_data';
+import { setupStoreWithData } from '../store_utils';
+
+const localVue = createLocalVue();
+
+describe('Dashboard', () => {
+ let DashboardComponent;
+ let mock;
+ let store;
+ let component;
+ let wrapper;
+
+ beforeEach(() => {
+ setFixtures(`
+ <div class="prometheus-graphs"></div>
+ <div class="layout-page"></div>
+ `);
+
+ store = createStore();
+ mock = new MockAdapter(axios);
+ DashboardComponent = localVue.extend(Dashboard);
+ });
+
+ afterEach(() => {
+ if (component) {
+ component.$destroy();
+ }
+ if (wrapper) {
+ wrapper.destroy();
+ }
+ mock.restore();
+ });
+
+ describe('responds to window resizes', () => {
+ let promPanel;
+ let promGroup;
+ let panelToggle;
+ let chart;
+ beforeEach(() => {
+ mock.onGet(mockApiEndpoint).reply(200, metricsDashboardPayload);
+
+ component = new DashboardComponent({
+ el: document.querySelector('.prometheus-graphs'),
+ propsData: {
+ ...propsData,
+ hasMetrics: true,
+ showPanels: true,
+ },
+ store,
+ });
+
+ setupStoreWithData(component.$store);
+
+ return Vue.nextTick().then(() => {
+ [promPanel] = component.$el.querySelectorAll('.prometheus-panel');
+ promGroup = promPanel.querySelector('.prometheus-graph-group');
+ panelToggle = promPanel.querySelector('.js-graph-group-toggle');
+ chart = promGroup.querySelector('.position-relative svg');
+ });
+ });
+
+ it('setting chart size to zero when panel group is hidden', () => {
+ expect(promGroup.style.display).toBe('');
+ expect(chart.clientWidth).toBeGreaterThan(0);
+
+ panelToggle.click();
+ return Vue.nextTick().then(() => {
+ expect(promGroup.style.display).toBe('none');
+ expect(chart.clientWidth).toBe(0);
+ promPanel.style.width = '500px';
+ });
+ });
+
+ it('expanding chart panel group after resize displays chart', () => {
+ panelToggle.click();
+
+ expect(chart.clientWidth).toBeGreaterThan(0);
+ });
+ });
+});
diff --git a/spec/javascripts/monitoring/components/dashboard_resize_spec.js b/spec/javascripts/monitoring/components/dashboard_resize_spec.js
deleted file mode 100644
index 0c3193940e6..00000000000
--- a/spec/javascripts/monitoring/components/dashboard_resize_spec.js
+++ /dev/null
@@ -1,87 +0,0 @@
-import Vue from 'vue';
-import { createLocalVue } from '@vue/test-utils';
-import MockAdapter from 'axios-mock-adapter';
-import Dashboard from '~/monitoring/components/dashboard.vue';
-import { createStore } from '~/monitoring/stores';
-import axios from '~/lib/utils/axios_utils';
-import { mockApiEndpoint, propsData } from '../mock_data';
-import { metricsDashboardPayload } from '../fixture_data';
-import { setupStoreWithData } from '../store_utils';
-
-const localVue = createLocalVue();
-
-describe('Dashboard', () => {
- let DashboardComponent;
- let mock;
- let store;
- let component;
- let wrapper;
-
- beforeEach(() => {
- setFixtures(`
- <div class="prometheus-graphs"></div>
- <div class="layout-page"></div>
- `);
-
- store = createStore();
- mock = new MockAdapter(axios);
- DashboardComponent = localVue.extend(Dashboard);
- });
-
- afterEach(() => {
- if (component) {
- component.$destroy();
- }
- if (wrapper) {
- wrapper.destroy();
- }
- mock.restore();
- });
-
- describe('responds to window resizes', () => {
- let promPanel;
- let promGroup;
- let panelToggle;
- let chart;
- beforeEach(() => {
- mock.onGet(mockApiEndpoint).reply(200, metricsDashboardPayload);
-
- component = new DashboardComponent({
- el: document.querySelector('.prometheus-graphs'),
- propsData: {
- ...propsData,
- hasMetrics: true,
- showPanels: true,
- },
- store,
- });
-
- setupStoreWithData(component.$store);
-
- return Vue.nextTick().then(() => {
- [promPanel] = component.$el.querySelectorAll('.prometheus-panel');
- promGroup = promPanel.querySelector('.prometheus-graph-group');
- panelToggle = promPanel.querySelector('.js-graph-group-toggle');
- chart = promGroup.querySelector('.position-relative svg');
- });
- });
-
- it('setting chart size to zero when panel group is hidden', () => {
- expect(promGroup.style.display).toBe('');
- expect(chart.clientWidth).toBeGreaterThan(0);
-
- panelToggle.click();
- return Vue.nextTick().then(() => {
- expect(promGroup.style.display).toBe('none');
- expect(chart.clientWidth).toBe(0);
- promPanel.style.width = '500px';
- });
- });
-
- it('expanding chart panel group after resize displays chart', () => {
- panelToggle.click();
-
- expect(chart.clientWidth).toBeGreaterThan(0);
- });
- });
-});
diff --git a/spec/javascripts/notebook/cells/code_spec.js b/spec/javascripts/notebook/cells/code_spec.js
deleted file mode 100644
index f3f97145ad3..00000000000
--- a/spec/javascripts/notebook/cells/code_spec.js
+++ /dev/null
@@ -1,74 +0,0 @@
-import Vue from 'vue';
-import CodeComponent from '~/notebook/cells/code.vue';
-
-const Component = Vue.extend(CodeComponent);
-
-describe('Code component', () => {
- let vm;
- let json;
-
- beforeEach(() => {
- json = getJSONFixture('blob/notebook/basic.json');
- });
-
- const setupComponent = cell => {
- const comp = new Component({
- propsData: {
- cell,
- },
- });
- comp.$mount();
- return comp;
- };
-
- describe('without output', () => {
- beforeEach(done => {
- vm = setupComponent(json.cells[0]);
-
- setTimeout(() => {
- done();
- });
- });
-
- it('does not render output prompt', () => {
- expect(vm.$el.querySelectorAll('.prompt').length).toBe(1);
- });
- });
-
- describe('with output', () => {
- beforeEach(done => {
- vm = setupComponent(json.cells[2]);
-
- setTimeout(() => {
- done();
- });
- });
-
- it('does not render output prompt', () => {
- expect(vm.$el.querySelectorAll('.prompt').length).toBe(2);
- });
-
- it('renders output cell', () => {
- expect(vm.$el.querySelector('.output')).toBeDefined();
- });
- });
-
- describe('with string for cell.source', () => {
- beforeEach(done => {
- const cell = json.cells[0];
- cell.source = cell.source.join('');
-
- vm = setupComponent(cell);
-
- setTimeout(() => {
- done();
- });
- });
-
- it('renders the same input as when cell.source is an array', () => {
- const expected = "console.log('test')";
-
- expect(vm.$el.querySelector('.input').innerText).toContain(expected);
- });
- });
-});
diff --git a/spec/javascripts/notebook/cells/markdown_spec.js b/spec/javascripts/notebook/cells/markdown_spec.js
deleted file mode 100644
index 07b18d97cd9..00000000000
--- a/spec/javascripts/notebook/cells/markdown_spec.js
+++ /dev/null
@@ -1,105 +0,0 @@
-import Vue from 'vue';
-import katex from 'katex';
-import MarkdownComponent from '~/notebook/cells/markdown.vue';
-
-const Component = Vue.extend(MarkdownComponent);
-
-window.katex = katex;
-
-describe('Markdown component', () => {
- let vm;
- let cell;
- let json;
-
- beforeEach(done => {
- json = getJSONFixture('blob/notebook/basic.json');
-
- // eslint-disable-next-line prefer-destructuring
- cell = json.cells[1];
-
- vm = new Component({
- propsData: {
- cell,
- },
- });
- vm.$mount();
-
- setTimeout(() => {
- done();
- });
- });
-
- it('does not render promot', () => {
- expect(vm.$el.querySelector('.prompt span')).toBeNull();
- });
-
- it('does not render the markdown text', () => {
- expect(vm.$el.querySelector('.markdown').innerHTML.trim()).not.toEqual(cell.source.join(''));
- });
-
- it('renders the markdown HTML', () => {
- expect(vm.$el.querySelector('.markdown h1')).not.toBeNull();
- });
-
- it('sanitizes output', done => {
- Object.assign(cell, {
- source: [
- '[XSS](data:text/html;base64,PHNjcmlwdD5hbGVydChkb2N1bWVudC5kb21haW4pPC9zY3JpcHQ+Cg==)\n',
- ],
- });
-
- Vue.nextTick(() => {
- expect(vm.$el.querySelector('a').getAttribute('href')).toBeNull();
-
- done();
- });
- });
-
- describe('katex', () => {
- beforeEach(() => {
- json = getJSONFixture('blob/notebook/math.json');
- });
-
- it('renders multi-line katex', done => {
- vm = new Component({
- propsData: {
- cell: json.cells[0],
- },
- }).$mount();
-
- Vue.nextTick(() => {
- expect(vm.$el.querySelector('.katex')).not.toBeNull();
-
- done();
- });
- });
-
- it('renders inline katex', done => {
- vm = new Component({
- propsData: {
- cell: json.cells[1],
- },
- }).$mount();
-
- Vue.nextTick(() => {
- expect(vm.$el.querySelector('p:first-child .katex')).not.toBeNull();
-
- done();
- });
- });
-
- it('renders multiple inline katex', done => {
- vm = new Component({
- propsData: {
- cell: json.cells[1],
- },
- }).$mount();
-
- Vue.nextTick(() => {
- expect(vm.$el.querySelectorAll('p:nth-child(2) .katex').length).toBe(4);
-
- done();
- });
- });
- });
-});
diff --git a/spec/javascripts/notebook/cells/output/index_spec.js b/spec/javascripts/notebook/cells/output/index_spec.js
deleted file mode 100644
index 005569f1c2d..00000000000
--- a/spec/javascripts/notebook/cells/output/index_spec.js
+++ /dev/null
@@ -1,115 +0,0 @@
-import Vue from 'vue';
-import CodeComponent from '~/notebook/cells/output/index.vue';
-
-const Component = Vue.extend(CodeComponent);
-
-describe('Output component', () => {
- let vm;
- let json;
-
- const createComponent = output => {
- vm = new Component({
- propsData: {
- outputs: [].concat(output),
- count: 1,
- },
- });
- vm.$mount();
- };
-
- beforeEach(() => {
- json = getJSONFixture('blob/notebook/basic.json');
- });
-
- describe('text output', () => {
- beforeEach(done => {
- createComponent(json.cells[2].outputs[0]);
-
- setTimeout(() => {
- done();
- });
- });
-
- it('renders as plain text', () => {
- expect(vm.$el.querySelector('pre')).not.toBeNull();
- });
-
- it('renders promot', () => {
- expect(vm.$el.querySelector('.prompt span')).not.toBeNull();
- });
- });
-
- describe('image output', () => {
- beforeEach(done => {
- createComponent(json.cells[3].outputs[0]);
-
- setTimeout(() => {
- done();
- });
- });
-
- it('renders as an image', () => {
- expect(vm.$el.querySelector('img')).not.toBeNull();
- });
- });
-
- describe('html output', () => {
- it('renders raw HTML', () => {
- createComponent(json.cells[4].outputs[0]);
-
- expect(vm.$el.querySelector('p')).not.toBeNull();
- expect(vm.$el.querySelectorAll('p').length).toBe(1);
- expect(vm.$el.textContent.trim()).toContain('test');
- });
-
- it('renders multiple raw HTML outputs', () => {
- createComponent([json.cells[4].outputs[0], json.cells[4].outputs[0]]);
-
- expect(vm.$el.querySelectorAll('p').length).toBe(2);
- });
- });
-
- describe('svg output', () => {
- beforeEach(done => {
- createComponent(json.cells[5].outputs[0]);
-
- setTimeout(() => {
- done();
- });
- });
-
- it('renders as an svg', () => {
- expect(vm.$el.querySelector('svg')).not.toBeNull();
- });
- });
-
- describe('default to plain text', () => {
- beforeEach(done => {
- createComponent(json.cells[6].outputs[0]);
-
- setTimeout(() => {
- done();
- });
- });
-
- it('renders as plain text', () => {
- expect(vm.$el.querySelector('pre')).not.toBeNull();
- expect(vm.$el.textContent.trim()).toContain('testing');
- });
-
- it('renders promot', () => {
- expect(vm.$el.querySelector('.prompt span')).not.toBeNull();
- });
-
- it("renders as plain text when doesn't recognise other types", done => {
- createComponent(json.cells[7].outputs[0]);
-
- setTimeout(() => {
- expect(vm.$el.querySelector('pre')).not.toBeNull();
- expect(vm.$el.textContent.trim()).toContain('testing');
-
- done();
- });
- });
- });
-});
diff --git a/spec/javascripts/notebook/cells/prompt_spec.js b/spec/javascripts/notebook/cells/prompt_spec.js
deleted file mode 100644
index cbbcb1e68e3..00000000000
--- a/spec/javascripts/notebook/cells/prompt_spec.js
+++ /dev/null
@@ -1,56 +0,0 @@
-import Vue from 'vue';
-import PromptComponent from '~/notebook/cells/prompt.vue';
-
-const Component = Vue.extend(PromptComponent);
-
-describe('Prompt component', () => {
- let vm;
-
- describe('input', () => {
- beforeEach(done => {
- vm = new Component({
- propsData: {
- type: 'In',
- count: 1,
- },
- });
- vm.$mount();
-
- setTimeout(() => {
- done();
- });
- });
-
- it('renders in label', () => {
- expect(vm.$el.textContent.trim()).toContain('In');
- });
-
- it('renders count', () => {
- expect(vm.$el.textContent.trim()).toContain('1');
- });
- });
-
- describe('output', () => {
- beforeEach(done => {
- vm = new Component({
- propsData: {
- type: 'Out',
- count: 1,
- },
- });
- vm.$mount();
-
- setTimeout(() => {
- done();
- });
- });
-
- it('renders in label', () => {
- expect(vm.$el.textContent.trim()).toContain('Out');
- });
-
- it('renders count', () => {
- expect(vm.$el.textContent.trim()).toContain('1');
- });
- });
-});
diff --git a/spec/javascripts/notebook/index_spec.js b/spec/javascripts/notebook/index_spec.js
deleted file mode 100644
index 2e2ea5ad8af..00000000000
--- a/spec/javascripts/notebook/index_spec.js
+++ /dev/null
@@ -1,100 +0,0 @@
-import Vue from 'vue';
-import Notebook from '~/notebook/index.vue';
-
-const Component = Vue.extend(Notebook);
-
-describe('Notebook component', () => {
- let vm;
- let json;
- let jsonWithWorksheet;
-
- beforeEach(() => {
- json = getJSONFixture('blob/notebook/basic.json');
- jsonWithWorksheet = getJSONFixture('blob/notebook/worksheets.json');
- });
-
- describe('without JSON', () => {
- beforeEach(done => {
- vm = new Component({
- propsData: {
- notebook: {},
- },
- });
- vm.$mount();
-
- setTimeout(() => {
- done();
- });
- });
-
- it('does not render', () => {
- expect(vm.$el.tagName).toBeUndefined();
- });
- });
-
- describe('with JSON', () => {
- beforeEach(done => {
- vm = new Component({
- propsData: {
- notebook: json,
- codeCssClass: 'js-code-class',
- },
- });
- vm.$mount();
-
- setTimeout(() => {
- done();
- });
- });
-
- it('renders cells', () => {
- expect(vm.$el.querySelectorAll('.cell').length).toBe(json.cells.length);
- });
-
- it('renders markdown cell', () => {
- expect(vm.$el.querySelector('.markdown')).not.toBeNull();
- });
-
- it('renders code cell', () => {
- expect(vm.$el.querySelector('pre')).not.toBeNull();
- });
-
- it('add code class to code blocks', () => {
- expect(vm.$el.querySelector('.js-code-class')).not.toBeNull();
- });
- });
-
- describe('with worksheets', () => {
- beforeEach(done => {
- vm = new Component({
- propsData: {
- notebook: jsonWithWorksheet,
- codeCssClass: 'js-code-class',
- },
- });
- vm.$mount();
-
- setTimeout(() => {
- done();
- });
- });
-
- it('renders cells', () => {
- expect(vm.$el.querySelectorAll('.cell').length).toBe(
- jsonWithWorksheet.worksheets[0].cells.length,
- );
- });
-
- it('renders markdown cell', () => {
- expect(vm.$el.querySelector('.markdown')).not.toBeNull();
- });
-
- it('renders code cell', () => {
- expect(vm.$el.querySelector('pre')).not.toBeNull();
- });
-
- it('add code class to code blocks', () => {
- expect(vm.$el.querySelector('.js-code-class')).not.toBeNull();
- });
- });
-});
diff --git a/spec/javascripts/pages/admin/jobs/index/components/stop_jobs_modal_spec.js b/spec/javascripts/pages/admin/jobs/index/components/stop_jobs_modal_spec.js
deleted file mode 100644
index 9ad72e0b043..00000000000
--- a/spec/javascripts/pages/admin/jobs/index/components/stop_jobs_modal_spec.js
+++ /dev/null
@@ -1,61 +0,0 @@
-import Vue from 'vue';
-
-import mountComponent from 'spec/helpers/vue_mount_component_helper';
-import axios from '~/lib/utils/axios_utils';
-import stopJobsModal from '~/pages/admin/jobs/index/components/stop_jobs_modal.vue';
-
-describe('stop_jobs_modal.vue', () => {
- const props = {
- url: `${gl.TEST_HOST}/stop_jobs_modal.vue/stopAll`,
- };
- let vm;
-
- afterEach(() => {
- vm.$destroy();
- });
-
- beforeEach(() => {
- const Component = Vue.extend(stopJobsModal);
- vm = mountComponent(Component, props);
- });
-
- describe('onSubmit', () => {
- it('stops jobs and redirects to overview page', done => {
- const responseURL = `${gl.TEST_HOST}/stop_jobs_modal.vue/jobs`;
- const redirectSpy = spyOnDependency(stopJobsModal, 'redirectTo');
- spyOn(axios, 'post').and.callFake(url => {
- expect(url).toBe(props.url);
- return Promise.resolve({
- request: {
- responseURL,
- },
- });
- });
-
- vm.onSubmit()
- .then(() => {
- expect(redirectSpy).toHaveBeenCalledWith(responseURL);
- })
- .then(done)
- .catch(done.fail);
- });
-
- it('displays error if stopping jobs failed', done => {
- const dummyError = new Error('stopping jobs failed');
- const redirectSpy = spyOnDependency(stopJobsModal, 'redirectTo');
- spyOn(axios, 'post').and.callFake(url => {
- expect(url).toBe(props.url);
- return Promise.reject(dummyError);
- });
-
- vm.onSubmit()
- .then(done.fail)
- .catch(error => {
- expect(error).toBe(dummyError);
- expect(redirectSpy).not.toHaveBeenCalled();
- })
- .then(done)
- .catch(done.fail);
- });
- });
-});
diff --git a/spec/javascripts/pages/labels/components/promote_label_modal_spec.js b/spec/javascripts/pages/labels/components/promote_label_modal_spec.js
deleted file mode 100644
index 5bad13c1ef2..00000000000
--- a/spec/javascripts/pages/labels/components/promote_label_modal_spec.js
+++ /dev/null
@@ -1,103 +0,0 @@
-import Vue from 'vue';
-import mountComponent from 'spec/helpers/vue_mount_component_helper';
-import promoteLabelModal from '~/pages/projects/labels/components/promote_label_modal.vue';
-import eventHub from '~/pages/projects/labels/event_hub';
-import axios from '~/lib/utils/axios_utils';
-
-describe('Promote label modal', () => {
- let vm;
- const Component = Vue.extend(promoteLabelModal);
- const labelMockData = {
- labelTitle: 'Documentation',
- labelColor: '#5cb85c',
- labelTextColor: '#ffffff',
- url: `${gl.TEST_HOST}/dummy/promote/labels`,
- groupName: 'group',
- };
-
- describe('Modal title and description', () => {
- beforeEach(() => {
- vm = mountComponent(Component, labelMockData);
- });
-
- afterEach(() => {
- vm.$destroy();
- });
-
- it('contains the proper description', () => {
- expect(vm.text).toContain(
- `Promoting ${labelMockData.labelTitle} will make it available for all projects inside ${labelMockData.groupName}`,
- );
- });
-
- it('contains a label span with the color', () => {
- const labelFromTitle = vm.$el.querySelector('.modal-header .label.color-label');
-
- expect(labelFromTitle.style.backgroundColor).not.toBe(null);
- expect(labelFromTitle.textContent).toContain(vm.labelTitle);
- });
- });
-
- describe('When requesting a label promotion', () => {
- beforeEach(() => {
- vm = mountComponent(Component, {
- ...labelMockData,
- });
- spyOn(eventHub, '$emit');
- });
-
- afterEach(() => {
- vm.$destroy();
- });
-
- it('redirects when a label is promoted', done => {
- const responseURL = `${gl.TEST_HOST}/dummy/endpoint`;
- spyOn(axios, 'post').and.callFake(url => {
- expect(url).toBe(labelMockData.url);
- expect(eventHub.$emit).toHaveBeenCalledWith(
- 'promoteLabelModal.requestStarted',
- labelMockData.url,
- );
- return Promise.resolve({
- request: {
- responseURL,
- },
- });
- });
-
- vm.onSubmit()
- .then(() => {
- expect(eventHub.$emit).toHaveBeenCalledWith('promoteLabelModal.requestFinished', {
- labelUrl: labelMockData.url,
- successful: true,
- });
- })
- .then(done)
- .catch(done.fail);
- });
-
- it('displays an error if promoting a label failed', done => {
- const dummyError = new Error('promoting label failed');
- dummyError.response = { status: 500 };
- spyOn(axios, 'post').and.callFake(url => {
- expect(url).toBe(labelMockData.url);
- expect(eventHub.$emit).toHaveBeenCalledWith(
- 'promoteLabelModal.requestStarted',
- labelMockData.url,
- );
- return Promise.reject(dummyError);
- });
-
- vm.onSubmit()
- .catch(error => {
- expect(error).toBe(dummyError);
- expect(eventHub.$emit).toHaveBeenCalledWith('promoteLabelModal.requestFinished', {
- labelUrl: labelMockData.url,
- successful: false,
- });
- })
- .then(done)
- .catch(done.fail);
- });
- });
-});
diff --git a/spec/javascripts/pages/milestones/shared/components/delete_milestone_modal_spec.js b/spec/javascripts/pages/milestones/shared/components/delete_milestone_modal_spec.js
deleted file mode 100644
index 9075c8aa97a..00000000000
--- a/spec/javascripts/pages/milestones/shared/components/delete_milestone_modal_spec.js
+++ /dev/null
@@ -1,106 +0,0 @@
-import Vue from 'vue';
-
-import mountComponent from 'spec/helpers/vue_mount_component_helper';
-import axios from '~/lib/utils/axios_utils';
-import deleteMilestoneModal from '~/pages/milestones/shared/components/delete_milestone_modal.vue';
-import eventHub from '~/pages/milestones/shared/event_hub';
-
-describe('delete_milestone_modal.vue', () => {
- const Component = Vue.extend(deleteMilestoneModal);
- const props = {
- issueCount: 1,
- mergeRequestCount: 2,
- milestoneId: 3,
- milestoneTitle: 'my milestone title',
- milestoneUrl: `${gl.TEST_HOST}/delete_milestone_modal.vue/milestone`,
- };
- let vm;
-
- afterEach(() => {
- vm.$destroy();
- });
-
- describe('onSubmit', () => {
- beforeEach(() => {
- vm = mountComponent(Component, props);
- spyOn(eventHub, '$emit');
- });
-
- it('deletes milestone and redirects to overview page', done => {
- const responseURL = `${gl.TEST_HOST}/delete_milestone_modal.vue/milestoneOverview`;
- spyOn(axios, 'delete').and.callFake(url => {
- expect(url).toBe(props.milestoneUrl);
- expect(eventHub.$emit).toHaveBeenCalledWith(
- 'deleteMilestoneModal.requestStarted',
- props.milestoneUrl,
- );
- eventHub.$emit.calls.reset();
- return Promise.resolve({
- request: {
- responseURL,
- },
- });
- });
- const redirectSpy = spyOnDependency(deleteMilestoneModal, 'redirectTo');
-
- vm.onSubmit()
- .then(() => {
- expect(redirectSpy).toHaveBeenCalledWith(responseURL);
- expect(eventHub.$emit).toHaveBeenCalledWith('deleteMilestoneModal.requestFinished', {
- milestoneUrl: props.milestoneUrl,
- successful: true,
- });
- })
- .then(done)
- .catch(done.fail);
- });
-
- it('displays error if deleting milestone failed', done => {
- const dummyError = new Error('deleting milestone failed');
- dummyError.response = { status: 418 };
- spyOn(axios, 'delete').and.callFake(url => {
- expect(url).toBe(props.milestoneUrl);
- expect(eventHub.$emit).toHaveBeenCalledWith(
- 'deleteMilestoneModal.requestStarted',
- props.milestoneUrl,
- );
- eventHub.$emit.calls.reset();
- return Promise.reject(dummyError);
- });
- const redirectSpy = spyOnDependency(deleteMilestoneModal, 'redirectTo');
-
- vm.onSubmit()
- .catch(error => {
- expect(error).toBe(dummyError);
- expect(redirectSpy).not.toHaveBeenCalled();
- expect(eventHub.$emit).toHaveBeenCalledWith('deleteMilestoneModal.requestFinished', {
- milestoneUrl: props.milestoneUrl,
- successful: false,
- });
- })
- .then(done)
- .catch(done.fail);
- });
- });
-
- describe('text', () => {
- it('contains the issue and milestone count', () => {
- vm = mountComponent(Component, props);
- const value = vm.text;
-
- expect(value).toContain('remove it from 1 issue and 2 merge requests');
- });
-
- it('contains neither issue nor milestone count', () => {
- vm = mountComponent(Component, {
- ...props,
- issueCount: 0,
- mergeRequestCount: 0,
- });
-
- const value = vm.text;
-
- expect(value).toContain('is not currently used');
- });
- });
-});
diff --git a/spec/javascripts/pages/milestones/shared/components/promote_milestone_modal_spec.js b/spec/javascripts/pages/milestones/shared/components/promote_milestone_modal_spec.js
deleted file mode 100644
index 78c0070187c..00000000000
--- a/spec/javascripts/pages/milestones/shared/components/promote_milestone_modal_spec.js
+++ /dev/null
@@ -1,98 +0,0 @@
-import Vue from 'vue';
-import mountComponent from 'spec/helpers/vue_mount_component_helper';
-import promoteMilestoneModal from '~/pages/milestones/shared/components/promote_milestone_modal.vue';
-import eventHub from '~/pages/milestones/shared/event_hub';
-import axios from '~/lib/utils/axios_utils';
-
-describe('Promote milestone modal', () => {
- let vm;
- const Component = Vue.extend(promoteMilestoneModal);
- const milestoneMockData = {
- milestoneTitle: 'v1.0',
- url: `${gl.TEST_HOST}/dummy/promote/milestones`,
- groupName: 'group',
- };
-
- describe('Modal title and description', () => {
- beforeEach(() => {
- vm = mountComponent(Component, milestoneMockData);
- });
-
- afterEach(() => {
- vm.$destroy();
- });
-
- it('contains the proper description', () => {
- expect(vm.text).toContain(
- `Promoting ${milestoneMockData.milestoneTitle} will make it available for all projects inside ${milestoneMockData.groupName}.`,
- );
- });
-
- it('contains the correct title', () => {
- expect(vm.title).toEqual('Promote v1.0 to group milestone?');
- });
- });
-
- describe('When requesting a milestone promotion', () => {
- beforeEach(() => {
- vm = mountComponent(Component, {
- ...milestoneMockData,
- });
- spyOn(eventHub, '$emit');
- });
-
- afterEach(() => {
- vm.$destroy();
- });
-
- it('redirects when a milestone is promoted', done => {
- const responseURL = `${gl.TEST_HOST}/dummy/endpoint`;
- spyOn(axios, 'post').and.callFake(url => {
- expect(url).toBe(milestoneMockData.url);
- expect(eventHub.$emit).toHaveBeenCalledWith(
- 'promoteMilestoneModal.requestStarted',
- milestoneMockData.url,
- );
- return Promise.resolve({
- request: {
- responseURL,
- },
- });
- });
-
- vm.onSubmit()
- .then(() => {
- expect(eventHub.$emit).toHaveBeenCalledWith('promoteMilestoneModal.requestFinished', {
- milestoneUrl: milestoneMockData.url,
- successful: true,
- });
- })
- .then(done)
- .catch(done.fail);
- });
-
- it('displays an error if promoting a milestone failed', done => {
- const dummyError = new Error('promoting milestone failed');
- dummyError.response = { status: 500 };
- spyOn(axios, 'post').and.callFake(url => {
- expect(url).toBe(milestoneMockData.url);
- expect(eventHub.$emit).toHaveBeenCalledWith(
- 'promoteMilestoneModal.requestStarted',
- milestoneMockData.url,
- );
- return Promise.reject(dummyError);
- });
-
- vm.onSubmit()
- .catch(error => {
- expect(error).toBe(dummyError);
- expect(eventHub.$emit).toHaveBeenCalledWith('promoteMilestoneModal.requestFinished', {
- milestoneUrl: milestoneMockData.url,
- successful: false,
- });
- })
- .then(done)
- .catch(done.fail);
- });
- });
-});
diff --git a/spec/javascripts/pages/projects/pipeline_schedules/shared/components/interval_pattern_input_spec.js b/spec/javascripts/pages/projects/pipeline_schedules/shared/components/interval_pattern_input_spec.js
deleted file mode 100644
index b20bc96f9be..00000000000
--- a/spec/javascripts/pages/projects/pipeline_schedules/shared/components/interval_pattern_input_spec.js
+++ /dev/null
@@ -1,192 +0,0 @@
-import Vue from 'vue';
-import Translate from '~/vue_shared/translate';
-import IntervalPatternInput from '~/pages/projects/pipeline_schedules/shared/components/interval_pattern_input.vue';
-
-Vue.use(Translate);
-
-const IntervalPatternInputComponent = Vue.extend(IntervalPatternInput);
-const inputNameAttribute = 'schedule[cron]';
-
-const cronIntervalPresets = {
- everyDay: '0 4 * * *',
- everyWeek: '0 4 * * 0',
- everyMonth: '0 4 1 * *',
-};
-
-window.gl = window.gl || {};
-
-window.gl.pipelineScheduleFieldErrors = {
- updateFormValidityState: () => {},
-};
-
-describe('Interval Pattern Input Component', function() {
- describe('when prop initialCronInterval is passed (edit)', function() {
- describe('when prop initialCronInterval is custom', function() {
- beforeEach(function() {
- this.initialCronInterval = '1 2 3 4 5';
- this.intervalPatternComponent = new IntervalPatternInputComponent({
- propsData: {
- initialCronInterval: this.initialCronInterval,
- },
- }).$mount();
- });
-
- it('is initialized as a Vue component', function() {
- expect(this.intervalPatternComponent).toBeDefined();
- });
-
- it('prop initialCronInterval is set', function() {
- expect(this.intervalPatternComponent.initialCronInterval).toBe(this.initialCronInterval);
- });
-
- it('sets isEditable to true', function(done) {
- Vue.nextTick(() => {
- expect(this.intervalPatternComponent.isEditable).toBe(true);
- done();
- });
- });
- });
-
- describe('when prop initialCronInterval is preset', function() {
- beforeEach(function() {
- this.intervalPatternComponent = new IntervalPatternInputComponent({
- propsData: {
- inputNameAttribute,
- initialCronInterval: '0 4 * * *',
- },
- }).$mount();
- });
-
- it('is initialized as a Vue component', function() {
- expect(this.intervalPatternComponent).toBeDefined();
- });
-
- it('sets isEditable to false', function(done) {
- Vue.nextTick(() => {
- expect(this.intervalPatternComponent.isEditable).toBe(false);
- done();
- });
- });
- });
- });
-
- describe('when prop initialCronInterval is not passed (new)', function() {
- beforeEach(function() {
- this.intervalPatternComponent = new IntervalPatternInputComponent({
- propsData: {
- inputNameAttribute,
- },
- }).$mount();
- });
-
- it('is initialized as a Vue component', function() {
- expect(this.intervalPatternComponent).toBeDefined();
- });
-
- it('prop initialCronInterval is set', function() {
- const defaultInitialCronInterval = '';
-
- expect(this.intervalPatternComponent.initialCronInterval).toBe(defaultInitialCronInterval);
- });
-
- it('sets isEditable to true', function(done) {
- Vue.nextTick(() => {
- expect(this.intervalPatternComponent.isEditable).toBe(true);
- done();
- });
- });
- });
-
- describe('User Actions', function() {
- beforeEach(function() {
- // For an unknown reason, some browsers do not propagate click events
- // on radio buttons in a way Vue can register. So, we have to mount
- // to a fixture.
- setFixtures('<div id="my-mount"></div>');
-
- this.initialCronInterval = '1 2 3 4 5';
- this.intervalPatternComponent = new IntervalPatternInputComponent({
- propsData: {
- initialCronInterval: this.initialCronInterval,
- },
- }).$mount('#my-mount');
- });
-
- it('cronInterval is updated when everyday preset interval is selected', function(done) {
- this.intervalPatternComponent.$el.querySelector('#every-day').click();
-
- Vue.nextTick(() => {
- expect(this.intervalPatternComponent.cronInterval).toBe(cronIntervalPresets.everyDay);
- expect(this.intervalPatternComponent.$el.querySelector('.cron-interval-input').value).toBe(
- cronIntervalPresets.everyDay,
- );
- done();
- });
- });
-
- it('cronInterval is updated when everyweek preset interval is selected', function(done) {
- this.intervalPatternComponent.$el.querySelector('#every-week').click();
-
- Vue.nextTick(() => {
- expect(this.intervalPatternComponent.cronInterval).toBe(cronIntervalPresets.everyWeek);
- expect(this.intervalPatternComponent.$el.querySelector('.cron-interval-input').value).toBe(
- cronIntervalPresets.everyWeek,
- );
-
- done();
- });
- });
-
- it('cronInterval is updated when everymonth preset interval is selected', function(done) {
- this.intervalPatternComponent.$el.querySelector('#every-month').click();
-
- Vue.nextTick(() => {
- expect(this.intervalPatternComponent.cronInterval).toBe(cronIntervalPresets.everyMonth);
- expect(this.intervalPatternComponent.$el.querySelector('.cron-interval-input').value).toBe(
- cronIntervalPresets.everyMonth,
- );
- done();
- });
- });
-
- it('only a space is added to cronInterval (trimmed later) when custom radio is selected', function(done) {
- this.intervalPatternComponent.$el.querySelector('#every-month').click();
- this.intervalPatternComponent.$el.querySelector('#custom').click();
-
- Vue.nextTick(() => {
- const intervalWithSpaceAppended = `${cronIntervalPresets.everyMonth} `;
-
- expect(this.intervalPatternComponent.cronInterval).toBe(intervalWithSpaceAppended);
- expect(this.intervalPatternComponent.$el.querySelector('.cron-interval-input').value).toBe(
- intervalWithSpaceAppended,
- );
- done();
- });
- });
-
- it('text input is disabled when preset interval is selected', function(done) {
- this.intervalPatternComponent.$el.querySelector('#every-month').click();
-
- Vue.nextTick(() => {
- expect(this.intervalPatternComponent.isEditable).toBe(false);
- expect(
- this.intervalPatternComponent.$el.querySelector('.cron-interval-input').disabled,
- ).toBe(true);
- done();
- });
- });
-
- it('text input is enabled when custom is selected', function(done) {
- this.intervalPatternComponent.$el.querySelector('#every-month').click();
- this.intervalPatternComponent.$el.querySelector('#custom').click();
-
- Vue.nextTick(() => {
- expect(this.intervalPatternComponent.isEditable).toBe(true);
- expect(
- this.intervalPatternComponent.$el.querySelector('.cron-interval-input').disabled,
- ).toBe(false);
- done();
- });
- });
- });
-});
diff --git a/spec/javascripts/pages/projects/pipeline_schedules/shared/components/pipeline_schedule_callout_spec.js b/spec/javascripts/pages/projects/pipeline_schedules/shared/components/pipeline_schedule_callout_spec.js
deleted file mode 100644
index ea809e1f170..00000000000
--- a/spec/javascripts/pages/projects/pipeline_schedules/shared/components/pipeline_schedule_callout_spec.js
+++ /dev/null
@@ -1,106 +0,0 @@
-import Vue from 'vue';
-import Cookies from 'js-cookie';
-import PipelineSchedulesCallout from '~/pages/projects/pipeline_schedules/shared/components/pipeline_schedules_callout.vue';
-
-const PipelineSchedulesCalloutComponent = Vue.extend(PipelineSchedulesCallout);
-const cookieKey = 'pipeline_schedules_callout_dismissed';
-const docsUrl = 'help/ci/scheduled_pipelines';
-
-describe('Pipeline Schedule Callout', function() {
- beforeEach(() => {
- setFixtures(`
- <div id='pipeline-schedules-callout' data-docs-url=${docsUrl}></div>
- `);
- });
-
- describe('independent of cookies', () => {
- beforeEach(() => {
- this.calloutComponent = new PipelineSchedulesCalloutComponent().$mount();
- });
-
- it('the component can be initialized', () => {
- expect(this.calloutComponent).toBeDefined();
- });
-
- it('correctly sets illustrationSvg', () => {
- expect(this.calloutComponent.illustrationSvg).toContain('<svg');
- });
-
- it('correctly sets docsUrl', () => {
- expect(this.calloutComponent.docsUrl).toContain(docsUrl);
- });
- });
-
- describe(`when ${cookieKey} cookie is set`, () => {
- beforeEach(() => {
- Cookies.set(cookieKey, true);
- this.calloutComponent = new PipelineSchedulesCalloutComponent().$mount();
- });
-
- it('correctly sets calloutDismissed to true', () => {
- expect(this.calloutComponent.calloutDismissed).toBe(true);
- });
-
- it('does not render the callout', () => {
- expect(this.calloutComponent.$el.childNodes.length).toBe(0);
- });
- });
-
- describe('when cookie is not set', () => {
- beforeEach(() => {
- Cookies.remove(cookieKey);
- this.calloutComponent = new PipelineSchedulesCalloutComponent().$mount();
- });
-
- it('correctly sets calloutDismissed to false', () => {
- expect(this.calloutComponent.calloutDismissed).toBe(false);
- });
-
- it('renders the callout container', () => {
- expect(this.calloutComponent.$el.querySelector('.bordered-box')).not.toBeNull();
- });
-
- it('renders the callout svg', () => {
- expect(this.calloutComponent.$el.outerHTML).toContain('<svg');
- });
-
- it('renders the callout title', () => {
- expect(this.calloutComponent.$el.outerHTML).toContain('Scheduling Pipelines');
- });
-
- it('renders the callout text', () => {
- expect(this.calloutComponent.$el.outerHTML).toContain('runs pipelines in the future');
- });
-
- it('renders the documentation url', () => {
- expect(this.calloutComponent.$el.outerHTML).toContain(docsUrl);
- });
-
- it('updates calloutDismissed when close button is clicked', done => {
- this.calloutComponent.$el.querySelector('#dismiss-callout-btn').click();
-
- Vue.nextTick(() => {
- expect(this.calloutComponent.calloutDismissed).toBe(true);
- done();
- });
- });
-
- it('#dismissCallout updates calloutDismissed', done => {
- this.calloutComponent.dismissCallout();
-
- Vue.nextTick(() => {
- expect(this.calloutComponent.calloutDismissed).toBe(true);
- done();
- });
- });
-
- it('is hidden when close button is clicked', done => {
- this.calloutComponent.$el.querySelector('#dismiss-callout-btn').click();
-
- Vue.nextTick(() => {
- expect(this.calloutComponent.$el.childNodes.length).toBe(0);
- done();
- });
- });
- });
-});
diff --git a/spec/javascripts/pipelines/header_component_spec.js b/spec/javascripts/pipelines/header_component_spec.js
deleted file mode 100644
index 9043f30397d..00000000000
--- a/spec/javascripts/pipelines/header_component_spec.js
+++ /dev/null
@@ -1,108 +0,0 @@
-import Vue from 'vue';
-import headerComponent from '~/pipelines/components/header_component.vue';
-import eventHub from '~/pipelines/event_hub';
-
-describe('Pipeline details header', () => {
- let HeaderComponent;
- let vm;
- let props;
-
- beforeEach(() => {
- spyOn(eventHub, '$emit');
- HeaderComponent = Vue.extend(headerComponent);
-
- const threeWeeksAgo = new Date();
- threeWeeksAgo.setDate(threeWeeksAgo.getDate() - 21);
-
- props = {
- pipeline: {
- details: {
- status: {
- group: 'failed',
- icon: 'status_failed',
- label: 'failed',
- text: 'failed',
- details_path: 'path',
- },
- },
- id: 123,
- created_at: threeWeeksAgo.toISOString(),
- user: {
- web_url: 'path',
- name: 'Foo',
- username: 'foobar',
- email: 'foo@bar.com',
- avatar_url: 'link',
- },
- retry_path: 'retry',
- cancel_path: 'cancel',
- delete_path: 'delete',
- },
- isLoading: false,
- };
-
- vm = new HeaderComponent({ propsData: props }).$mount();
- });
-
- afterEach(() => {
- eventHub.$off();
- vm.$destroy();
- });
-
- const findDeleteModal = () => document.getElementById(headerComponent.DELETE_MODAL_ID);
- const findDeleteModalSubmit = () =>
- [...findDeleteModal().querySelectorAll('.btn')].find(x => x.textContent === 'Delete pipeline');
-
- it('should render provided pipeline info', () => {
- expect(
- vm.$el
- .querySelector('.header-main-content')
- .textContent.replace(/\s+/g, ' ')
- .trim(),
- ).toContain('failed Pipeline #123 triggered 3 weeks ago by Foo');
- });
-
- describe('action buttons', () => {
- it('should not trigger eventHub when nothing happens', () => {
- expect(eventHub.$emit).not.toHaveBeenCalled();
- });
-
- it('should call postAction when retry button action is clicked', () => {
- vm.$el.querySelector('.js-retry-button').click();
-
- expect(eventHub.$emit).toHaveBeenCalledWith('headerPostAction', 'retry');
- });
-
- it('should call postAction when cancel button action is clicked', () => {
- vm.$el.querySelector('.js-btn-cancel-pipeline').click();
-
- expect(eventHub.$emit).toHaveBeenCalledWith('headerPostAction', 'cancel');
- });
-
- it('does not show delete modal', () => {
- expect(findDeleteModal()).not.toBeVisible();
- });
-
- describe('when delete button action is clicked', () => {
- beforeEach(done => {
- vm.$el.querySelector('.js-btn-delete-pipeline').click();
-
- // Modal needs two ticks to show
- vm.$nextTick()
- .then(() => vm.$nextTick())
- .then(done)
- .catch(done.fail);
- });
-
- it('should show delete modal', () => {
- expect(findDeleteModal()).toBeVisible();
- });
-
- it('should call delete when modal is submitted', () => {
- findDeleteModalSubmit().click();
-
- expect(eventHub.$emit).toHaveBeenCalledWith('headerDeleteAction', 'delete');
- });
- });
- });
-});
diff --git a/spec/javascripts/pipelines/linked_pipelines_mock.json b/spec/javascripts/pipelines/linked_pipelines_mock.json
deleted file mode 100644
index 60e214ddc32..00000000000
--- a/spec/javascripts/pipelines/linked_pipelines_mock.json
+++ /dev/null
@@ -1,3535 +0,0 @@
-{
- "id": 23211253,
- "user": {
- "id": 3585,
- "name": "Achilleas Pipinellis",
- "username": "axil",
- "state": "active",
- "avatar_url": "https://assets.gitlab-static.net/uploads/-/system/user/avatar/3585/avatar.png",
- "web_url": "https://gitlab.com/axil",
- "status_tooltip_html": "\u003cspan class=\"user-status-emoji has-tooltip\" title=\"I like pizza\" data-html=\"true\" data-placement=\"top\"\u003e\u003cgl-emoji title=\"slice of pizza\" data-name=\"pizza\" data-unicode-version=\"6.0\"\u003eðŸ•\u003c/gl-emoji\u003e\u003c/span\u003e",
- "path": "/axil"
- },
- "active": false,
- "coverage": null,
- "source": "push",
- "created_at": "2018-06-05T11:31:30.452Z",
- "updated_at": "2018-10-31T16:35:31.305Z",
- "path": "/gitlab-org/gitlab-runner/pipelines/23211253",
- "flags": {
- "latest": false,
- "stuck": false,
- "auto_devops": false,
- "merge_request": false,
- "yaml_errors": false,
- "retryable": false,
- "cancelable": false,
- "failure_reason": false
- },
- "details": {
- "status": {
- "icon": "status_success",
- "text": "passed",
- "label": "passed",
- "group": "success",
- "tooltip": "passed",
- "has_details": true,
- "details_path": "/gitlab-org/gitlab-runner/pipelines/23211253",
- "illustration": null,
- "favicon": "https://gitlab.com/assets/ci_favicons/favicon_status_success-8451333011eee8ce9f2ab25dc487fe24a8758c694827a582f17f42b0a90446a2.png"
- },
- "duration": 53,
- "finished_at": "2018-10-31T16:35:31.299Z",
- "stages": [
- {
- "name": "prebuild",
- "title": "prebuild: passed",
- "groups": [
- {
- "name": "review-docs-deploy",
- "size": 1,
- "status": {
- "icon": "status_success",
- "text": "passed",
- "label": "manual play action",
- "group": "success",
- "tooltip": "passed",
- "has_details": true,
- "details_path": "/gitlab-org/gitlab-runner/-/jobs/72469032",
- "illustration": {
- "image": "https://assets.gitlab-static.net/assets/illustrations/manual_action-2b4ca0d1bcfd92aebf33d484e36cbf7a102d007f76b5a0cfea636033a629d601.svg",
- "size": "svg-394",
- "title": "This job requires a manual action",
- "content": "This job depends on a user to trigger its process. Often they are used to deploy code to production environments"
- },
- "favicon": "https://gitlab.com/assets/ci_favicons/favicon_status_success-8451333011eee8ce9f2ab25dc487fe24a8758c694827a582f17f42b0a90446a2.png",
- "action": {
- "icon": "play",
- "title": "Play",
- "path": "/gitlab-org/gitlab-runner/-/jobs/72469032/play",
- "method": "post",
- "button_title": "Trigger this manual action"
- }
- },
- "jobs": [
- {
- "id": 72469032,
- "name": "review-docs-deploy",
- "started": "2018-10-31T16:34:58.778Z",
- "archived": false,
- "build_path": "/gitlab-org/gitlab-runner/-/jobs/72469032",
- "retry_path": "/gitlab-org/gitlab-runner/-/jobs/72469032/retry",
- "play_path": "/gitlab-org/gitlab-runner/-/jobs/72469032/play",
- "playable": true,
- "scheduled": false,
- "created_at": "2018-06-05T11:31:30.495Z",
- "updated_at": "2018-10-31T16:35:31.251Z",
- "status": {
- "icon": "status_success",
- "text": "passed",
- "label": "manual play action",
- "group": "success",
- "tooltip": "passed",
- "has_details": true,
- "details_path": "/gitlab-org/gitlab-runner/-/jobs/72469032",
- "illustration": {
- "image": "https://assets.gitlab-static.net/assets/illustrations/manual_action-2b4ca0d1bcfd92aebf33d484e36cbf7a102d007f76b5a0cfea636033a629d601.svg",
- "size": "svg-394",
- "title": "This job requires a manual action",
- "content": "This job depends on a user to trigger its process. Often they are used to deploy code to production environments"
- },
- "favicon": "https://gitlab.com/assets/ci_favicons/favicon_status_success-8451333011eee8ce9f2ab25dc487fe24a8758c694827a582f17f42b0a90446a2.png",
- "action": {
- "icon": "play",
- "title": "Play",
- "path": "/gitlab-org/gitlab-runner/-/jobs/72469032/play",
- "method": "post",
- "button_title": "Trigger this manual action"
- }
- }
- }
- ]
- }
- ],
- "status": {
- "icon": "status_success",
- "text": "passed",
- "label": "passed",
- "group": "success",
- "tooltip": "passed",
- "has_details": true,
- "details_path": "/gitlab-org/gitlab-runner/pipelines/23211253#prebuild",
- "illustration": null,
- "favicon": "https://gitlab.com/assets/ci_favicons/favicon_status_success-8451333011eee8ce9f2ab25dc487fe24a8758c694827a582f17f42b0a90446a2.png"
- },
- "path": "/gitlab-org/gitlab-runner/pipelines/23211253#prebuild",
- "dropdown_path": "/gitlab-org/gitlab-runner/pipelines/23211253/stage.json?stage=prebuild"
- },
- {
- "name": "test",
- "title": "test: passed",
- "groups": [
- {
- "name": "docs check links",
- "size": 1,
- "status": {
- "icon": "status_success",
- "text": "passed",
- "label": "passed",
- "group": "success",
- "tooltip": "passed",
- "has_details": true,
- "details_path": "/gitlab-org/gitlab-runner/-/jobs/72469033",
- "illustration": {
- "image": "https://assets.gitlab-static.net/assets/illustrations/skipped-job_empty-8b877955fbf175e42ae65b6cb95346e15282c6fc5b682756c329af3a0055225e.svg",
- "size": "svg-430",
- "title": "This job does not have a trace."
- },
- "favicon": "https://gitlab.com/assets/ci_favicons/favicon_status_success-8451333011eee8ce9f2ab25dc487fe24a8758c694827a582f17f42b0a90446a2.png",
- "action": {
- "icon": "retry",
- "title": "Retry",
- "path": "/gitlab-org/gitlab-runner/-/jobs/72469033/retry",
- "method": "post",
- "button_title": "Retry this job"
- }
- },
- "jobs": [
- {
- "id": 72469033,
- "name": "docs check links",
- "started": "2018-06-05T11:31:33.240Z",
- "archived": false,
- "build_path": "/gitlab-org/gitlab-runner/-/jobs/72469033",
- "retry_path": "/gitlab-org/gitlab-runner/-/jobs/72469033/retry",
- "playable": false,
- "scheduled": false,
- "created_at": "2018-06-05T11:31:30.627Z",
- "updated_at": "2018-06-05T11:31:54.363Z",
- "status": {
- "icon": "status_success",
- "text": "passed",
- "label": "passed",
- "group": "success",
- "tooltip": "passed",
- "has_details": true,
- "details_path": "/gitlab-org/gitlab-runner/-/jobs/72469033",
- "illustration": {
- "image": "https://assets.gitlab-static.net/assets/illustrations/skipped-job_empty-8b877955fbf175e42ae65b6cb95346e15282c6fc5b682756c329af3a0055225e.svg",
- "size": "svg-430",
- "title": "This job does not have a trace."
- },
- "favicon": "https://gitlab.com/assets/ci_favicons/favicon_status_success-8451333011eee8ce9f2ab25dc487fe24a8758c694827a582f17f42b0a90446a2.png",
- "action": {
- "icon": "retry",
- "title": "Retry",
- "path": "/gitlab-org/gitlab-runner/-/jobs/72469033/retry",
- "method": "post",
- "button_title": "Retry this job"
- }
- }
- }
- ]
- }
- ],
- "status": {
- "icon": "status_success",
- "text": "passed",
- "label": "passed",
- "group": "success",
- "tooltip": "passed",
- "has_details": true,
- "details_path": "/gitlab-org/gitlab-runner/pipelines/23211253#test",
- "illustration": null,
- "favicon": "https://gitlab.com/assets/ci_favicons/favicon_status_success-8451333011eee8ce9f2ab25dc487fe24a8758c694827a582f17f42b0a90446a2.png"
- },
- "path": "/gitlab-org/gitlab-runner/pipelines/23211253#test",
- "dropdown_path": "/gitlab-org/gitlab-runner/pipelines/23211253/stage.json?stage=test"
- },
- {
- "name": "cleanup",
- "title": "cleanup: skipped",
- "groups": [
- {
- "name": "review-docs-cleanup",
- "size": 1,
- "status": {
- "icon": "status_manual",
- "text": "manual",
- "label": "manual stop action",
- "group": "manual",
- "tooltip": "manual action",
- "has_details": true,
- "details_path": "/gitlab-org/gitlab-runner/-/jobs/72469034",
- "illustration": {
- "image": "https://assets.gitlab-static.net/assets/illustrations/manual_action-2b4ca0d1bcfd92aebf33d484e36cbf7a102d007f76b5a0cfea636033a629d601.svg",
- "size": "svg-394",
- "title": "This job requires a manual action",
- "content": "This job depends on a user to trigger its process. Often they are used to deploy code to production environments"
- },
- "favicon": "https://gitlab.com/assets/ci_favicons/favicon_status_manual-829a0804612cef47d9efc1618dba38325483657c847dba0546c3b9f0295bb36c.png",
- "action": {
- "icon": "stop",
- "title": "Stop",
- "path": "/gitlab-org/gitlab-runner/-/jobs/72469034/play",
- "method": "post",
- "button_title": "Stop this environment"
- }
- },
- "jobs": [
- {
- "id": 72469034,
- "name": "review-docs-cleanup",
- "started": null,
- "archived": false,
- "build_path": "/gitlab-org/gitlab-runner/-/jobs/72469034",
- "play_path": "/gitlab-org/gitlab-runner/-/jobs/72469034/play",
- "playable": true,
- "scheduled": false,
- "created_at": "2018-06-05T11:31:30.760Z",
- "updated_at": "2018-06-05T11:31:56.037Z",
- "status": {
- "icon": "status_manual",
- "text": "manual",
- "label": "manual stop action",
- "group": "manual",
- "tooltip": "manual action",
- "has_details": true,
- "details_path": "/gitlab-org/gitlab-runner/-/jobs/72469034",
- "illustration": {
- "image": "https://assets.gitlab-static.net/assets/illustrations/manual_action-2b4ca0d1bcfd92aebf33d484e36cbf7a102d007f76b5a0cfea636033a629d601.svg",
- "size": "svg-394",
- "title": "This job requires a manual action",
- "content": "This job depends on a user to trigger its process. Often they are used to deploy code to production environments"
- },
- "favicon": "https://gitlab.com/assets/ci_favicons/favicon_status_manual-829a0804612cef47d9efc1618dba38325483657c847dba0546c3b9f0295bb36c.png",
- "action": {
- "icon": "stop",
- "title": "Stop",
- "path": "/gitlab-org/gitlab-runner/-/jobs/72469034/play",
- "method": "post",
- "button_title": "Stop this environment"
- }
- }
- }
- ]
- }
- ],
- "status": {
- "icon": "status_skipped",
- "text": "skipped",
- "label": "skipped",
- "group": "skipped",
- "tooltip": "skipped",
- "has_details": true,
- "details_path": "/gitlab-org/gitlab-runner/pipelines/23211253#cleanup",
- "illustration": null,
- "favicon": "https://gitlab.com/assets/ci_favicons/favicon_status_skipped-0b9c5e543588945e8c4ca57786bbf2d0c56631959c9f853300392d0315be829b.png"
- },
- "path": "/gitlab-org/gitlab-runner/pipelines/23211253#cleanup",
- "dropdown_path": "/gitlab-org/gitlab-runner/pipelines/23211253/stage.json?stage=cleanup"
- }
- ],
- "artifacts": [],
- "manual_actions": [
- {
- "name": "review-docs-cleanup",
- "path": "/gitlab-org/gitlab-runner/-/jobs/72469034/play",
- "playable": true,
- "scheduled": false
- },
- {
- "name": "review-docs-deploy",
- "path": "/gitlab-org/gitlab-runner/-/jobs/72469032/play",
- "playable": true,
- "scheduled": false
- }
- ],
- "scheduled_actions": []
- },
- "ref": {
- "name": "docs/add-development-guide-to-readme",
- "path": "/gitlab-org/gitlab-runner/commits/docs/add-development-guide-to-readme",
- "tag": false,
- "branch": true,
- "merge_request": false
- },
- "commit": {
- "id": "8083eb0a920572214d0dccedd7981f05d535ad46",
- "short_id": "8083eb0a",
- "title": "Add link to development guide in readme",
- "created_at": "2018-06-05T11:30:48.000Z",
- "parent_ids": ["1d7cf79b5a1a2121b9474ac20d61c1b8f621289d"],
- "message": "Add link to development guide in readme\n\nCloses https://gitlab.com/gitlab-org/gitlab-runner/issues/3122\n",
- "author_name": "Achilleas Pipinellis",
- "author_email": "axil@gitlab.com",
- "authored_date": "2018-06-05T11:30:48.000Z",
- "committer_name": "Achilleas Pipinellis",
- "committer_email": "axil@gitlab.com",
- "committed_date": "2018-06-05T11:30:48.000Z",
- "author": {
- "id": 3585,
- "name": "Achilleas Pipinellis",
- "username": "axil",
- "state": "active",
- "avatar_url": "https://assets.gitlab-static.net/uploads/-/system/user/avatar/3585/avatar.png",
- "web_url": "https://gitlab.com/axil",
- "status_tooltip_html": null,
- "path": "/axil"
- },
- "author_gravatar_url": "https://secure.gravatar.com/avatar/1d37af00eec153a8333a4ce18e9aea41?s=80\u0026d=identicon",
- "commit_url": "https://gitlab.com/gitlab-org/gitlab-runner/commit/8083eb0a920572214d0dccedd7981f05d535ad46",
- "commit_path": "/gitlab-org/gitlab-runner/commit/8083eb0a920572214d0dccedd7981f05d535ad46"
- },
- "project": {
- "id": 1794617
- },
- "triggered_by": {
- "id": 12,
- "user": {
- "id": 376774,
- "name": "Alessio Caiazza",
- "username": "nolith",
- "state": "active",
- "avatar_url": "https://assets.gitlab-static.net/uploads/-/system/user/avatar/376774/avatar.png",
- "web_url": "https://gitlab.com/nolith",
- "status_tooltip_html": null,
- "path": "/nolith"
- },
- "active": false,
- "coverage": null,
- "source": "pipeline",
- "path": "/gitlab-com/gitlab-docs/pipelines/34993051",
- "details": {
- "status": {
- "icon": "status_failed",
- "text": "failed",
- "label": "failed",
- "group": "failed",
- "tooltip": "failed",
- "has_details": true,
- "details_path": "/gitlab-com/gitlab-docs/pipelines/34993051",
- "illustration": null,
- "favicon": "https://gitlab.com/assets/ci_favicons/favicon_status_failed-41304d7f7e3828808b0c26771f0309e55296819a9beea3ea9fbf6689d9857c12.png"
- },
- "duration": 118,
- "finished_at": "2018-10-31T16:41:40.615Z",
- "stages": [
- {
- "name": "build-images",
- "title": "build-images: skipped",
- "groups": [
- {
- "name": "image:bootstrap",
- "size": 1,
- "status": {
- "icon": "status_manual",
- "text": "manual",
- "label": "manual play action",
- "group": "manual",
- "tooltip": "manual action",
- "has_details": true,
- "details_path": "/gitlab-com/gitlab-docs/-/jobs/114982853",
- "illustration": {
- "image": "https://assets.gitlab-static.net/assets/illustrations/manual_action-2b4ca0d1bcfd92aebf33d484e36cbf7a102d007f76b5a0cfea636033a629d601.svg",
- "size": "svg-394",
- "title": "This job requires a manual action",
- "content": "This job depends on a user to trigger its process. Often they are used to deploy code to production environments"
- },
- "favicon": "https://gitlab.com/assets/ci_favicons/favicon_status_manual-829a0804612cef47d9efc1618dba38325483657c847dba0546c3b9f0295bb36c.png",
- "action": {
- "icon": "play",
- "title": "Play",
- "path": "/gitlab-com/gitlab-docs/-/jobs/114982853/play",
- "method": "post",
- "button_title": "Trigger this manual action"
- }
- },
- "jobs": [
- {
- "id": 11421321982853,
- "name": "image:bootstrap",
- "started": null,
- "archived": false,
- "build_path": "/gitlab-com/gitlab-docs/-/jobs/114982853",
- "play_path": "/gitlab-com/gitlab-docs/-/jobs/114982853/play",
- "playable": true,
- "scheduled": false,
- "created_at": "2018-10-31T16:35:23.704Z",
- "updated_at": "2018-10-31T16:35:24.118Z",
- "status": {
- "icon": "status_manual",
- "text": "manual",
- "label": "manual play action",
- "group": "manual",
- "tooltip": "manual action",
- "has_details": true,
- "details_path": "/gitlab-com/gitlab-docs/-/jobs/114982853",
- "illustration": {
- "image": "https://assets.gitlab-static.net/assets/illustrations/manual_action-2b4ca0d1bcfd92aebf33d484e36cbf7a102d007f76b5a0cfea636033a629d601.svg",
- "size": "svg-394",
- "title": "This job requires a manual action",
- "content": "This job depends on a user to trigger its process. Often they are used to deploy code to production environments"
- },
- "favicon": "https://gitlab.com/assets/ci_favicons/favicon_status_manual-829a0804612cef47d9efc1618dba38325483657c847dba0546c3b9f0295bb36c.png",
- "action": {
- "icon": "play",
- "title": "Play",
- "path": "/gitlab-com/gitlab-docs/-/jobs/114982853/play",
- "method": "post",
- "button_title": "Trigger this manual action"
- }
- }
- }
- ]
- },
- {
- "name": "image:builder-onbuild",
- "size": 1,
- "status": {
- "icon": "status_manual",
- "text": "manual",
- "label": "manual play action",
- "group": "manual",
- "tooltip": "manual action",
- "has_details": true,
- "details_path": "/gitlab-com/gitlab-docs/-/jobs/114982854",
- "illustration": {
- "image": "https://assets.gitlab-static.net/assets/illustrations/manual_action-2b4ca0d1bcfd92aebf33d484e36cbf7a102d007f76b5a0cfea636033a629d601.svg",
- "size": "svg-394",
- "title": "This job requires a manual action",
- "content": "This job depends on a user to trigger its process. Often they are used to deploy code to production environments"
- },
- "favicon": "https://gitlab.com/assets/ci_favicons/favicon_status_manual-829a0804612cef47d9efc1618dba38325483657c847dba0546c3b9f0295bb36c.png",
- "action": {
- "icon": "play",
- "title": "Play",
- "path": "/gitlab-com/gitlab-docs/-/jobs/114982854/play",
- "method": "post",
- "button_title": "Trigger this manual action"
- }
- },
- "jobs": [
- {
- "id": 1149822131854,
- "name": "image:builder-onbuild",
- "started": null,
- "archived": false,
- "build_path": "/gitlab-com/gitlab-docs/-/jobs/114982854",
- "play_path": "/gitlab-com/gitlab-docs/-/jobs/114982854/play",
- "playable": true,
- "scheduled": false,
- "created_at": "2018-10-31T16:35:23.728Z",
- "updated_at": "2018-10-31T16:35:24.070Z",
- "status": {
- "icon": "status_manual",
- "text": "manual",
- "label": "manual play action",
- "group": "manual",
- "tooltip": "manual action",
- "has_details": true,
- "details_path": "/gitlab-com/gitlab-docs/-/jobs/114982854",
- "illustration": {
- "image": "https://assets.gitlab-static.net/assets/illustrations/manual_action-2b4ca0d1bcfd92aebf33d484e36cbf7a102d007f76b5a0cfea636033a629d601.svg",
- "size": "svg-394",
- "title": "This job requires a manual action",
- "content": "This job depends on a user to trigger its process. Often they are used to deploy code to production environments"
- },
- "favicon": "https://gitlab.com/assets/ci_favicons/favicon_status_manual-829a0804612cef47d9efc1618dba38325483657c847dba0546c3b9f0295bb36c.png",
- "action": {
- "icon": "play",
- "title": "Play",
- "path": "/gitlab-com/gitlab-docs/-/jobs/114982854/play",
- "method": "post",
- "button_title": "Trigger this manual action"
- }
- }
- }
- ]
- },
- {
- "name": "image:nginx-onbuild",
- "size": 1,
- "status": {
- "icon": "status_manual",
- "text": "manual",
- "label": "manual play action",
- "group": "manual",
- "tooltip": "manual action",
- "has_details": true,
- "details_path": "/gitlab-com/gitlab-docs/-/jobs/114982855",
- "illustration": {
- "image": "https://assets.gitlab-static.net/assets/illustrations/manual_action-2b4ca0d1bcfd92aebf33d484e36cbf7a102d007f76b5a0cfea636033a629d601.svg",
- "size": "svg-394",
- "title": "This job requires a manual action",
- "content": "This job depends on a user to trigger its process. Often they are used to deploy code to production environments"
- },
- "favicon": "https://gitlab.com/assets/ci_favicons/favicon_status_manual-829a0804612cef47d9efc1618dba38325483657c847dba0546c3b9f0295bb36c.png",
- "action": {
- "icon": "play",
- "title": "Play",
- "path": "/gitlab-com/gitlab-docs/-/jobs/114982855/play",
- "method": "post",
- "button_title": "Trigger this manual action"
- }
- },
- "jobs": [
- {
- "id": 11498285523424,
- "name": "image:nginx-onbuild",
- "started": null,
- "archived": false,
- "build_path": "/gitlab-com/gitlab-docs/-/jobs/114982855",
- "play_path": "/gitlab-com/gitlab-docs/-/jobs/114982855/play",
- "playable": true,
- "scheduled": false,
- "created_at": "2018-10-31T16:35:23.753Z",
- "updated_at": "2018-10-31T16:35:24.033Z",
- "status": {
- "icon": "status_manual",
- "text": "manual",
- "label": "manual play action",
- "group": "manual",
- "tooltip": "manual action",
- "has_details": true,
- "details_path": "/gitlab-com/gitlab-docs/-/jobs/114982855",
- "illustration": {
- "image": "https://assets.gitlab-static.net/assets/illustrations/manual_action-2b4ca0d1bcfd92aebf33d484e36cbf7a102d007f76b5a0cfea636033a629d601.svg",
- "size": "svg-394",
- "title": "This job requires a manual action",
- "content": "This job depends on a user to trigger its process. Often they are used to deploy code to production environments"
- },
- "favicon": "https://gitlab.com/assets/ci_favicons/favicon_status_manual-829a0804612cef47d9efc1618dba38325483657c847dba0546c3b9f0295bb36c.png",
- "action": {
- "icon": "play",
- "title": "Play",
- "path": "/gitlab-com/gitlab-docs/-/jobs/114982855/play",
- "method": "post",
- "button_title": "Trigger this manual action"
- }
- }
- }
- ]
- }
- ],
- "status": {
- "icon": "status_skipped",
- "text": "skipped",
- "label": "skipped",
- "group": "skipped",
- "tooltip": "skipped",
- "has_details": true,
- "details_path": "/gitlab-com/gitlab-docs/pipelines/34993051#build-images",
- "illustration": null,
- "favicon": "https://gitlab.com/assets/ci_favicons/favicon_status_skipped-0b9c5e543588945e8c4ca57786bbf2d0c56631959c9f853300392d0315be829b.png"
- },
- "path": "/gitlab-com/gitlab-docs/pipelines/34993051#build-images",
- "dropdown_path": "/gitlab-com/gitlab-docs/pipelines/34993051/stage.json?stage=build-images"
- },
- {
- "name": "build",
- "title": "build: failed",
- "groups": [
- {
- "name": "compile_dev",
- "size": 1,
- "status": {
- "icon": "status_failed",
- "text": "failed",
- "label": "failed",
- "group": "failed",
- "tooltip": "failed - (script failure)",
- "has_details": true,
- "details_path": "/gitlab-com/gitlab-docs/-/jobs/114984694",
- "illustration": {
- "image": "https://assets.gitlab-static.net/assets/illustrations/skipped-job_empty-8b877955fbf175e42ae65b6cb95346e15282c6fc5b682756c329af3a0055225e.svg",
- "size": "svg-430",
- "title": "This job does not have a trace."
- },
- "favicon": "https://gitlab.com/assets/ci_favicons/favicon_status_failed-41304d7f7e3828808b0c26771f0309e55296819a9beea3ea9fbf6689d9857c12.png",
- "action": {
- "icon": "retry",
- "title": "Retry",
- "path": "/h5bp/html5-boilerplate/-/jobs/528/retry",
- "method": "post",
- "button_title": "Retry this job"
- }
- },
- "jobs": [
- {
- "id": 1149846949786,
- "name": "compile_dev",
- "started": "2018-10-31T16:39:41.598Z",
- "archived": false,
- "build_path": "/gitlab-com/gitlab-docs/-/jobs/114984694",
- "retry_path": "/gitlab-com/gitlab-docs/-/jobs/114984694/retry",
- "playable": false,
- "scheduled": false,
- "created_at": "2018-10-31T16:39:41.138Z",
- "updated_at": "2018-10-31T16:41:40.072Z",
- "status": {
- "icon": "status_failed",
- "text": "failed",
- "label": "failed",
- "group": "failed",
- "tooltip": "failed - (script failure)",
- "has_details": true,
- "details_path": "/gitlab-com/gitlab-docs/-/jobs/114984694",
- "illustration": {
- "image": "https://assets.gitlab-static.net/assets/illustrations/skipped-job_empty-8b877955fbf175e42ae65b6cb95346e15282c6fc5b682756c329af3a0055225e.svg",
- "size": "svg-430",
- "title": "This job does not have a trace."
- },
- "favicon": "https://gitlab.com/assets/ci_favicons/favicon_status_failed-41304d7f7e3828808b0c26771f0309e55296819a9beea3ea9fbf6689d9857c12.png",
- "action": {
- "icon": "retry",
- "title": "Retry",
- "path": "/h5bp/html5-boilerplate/-/jobs/528/retry",
- "method": "post",
- "button_title": "Retry this job"
- }
- },
- "recoverable": false
- }
- ]
- }
- ],
- "status": {
- "icon": "status_failed",
- "text": "failed",
- "label": "failed",
- "group": "failed",
- "tooltip": "failed",
- "has_details": true,
- "details_path": "/gitlab-com/gitlab-docs/pipelines/34993051#build",
- "illustration": null,
- "favicon": "https://gitlab.com/assets/ci_favicons/favicon_status_failed-41304d7f7e3828808b0c26771f0309e55296819a9beea3ea9fbf6689d9857c12.png"
- },
- "path": "/gitlab-com/gitlab-docs/pipelines/34993051#build",
- "dropdown_path": "/gitlab-com/gitlab-docs/pipelines/34993051/stage.json?stage=build"
- },
- {
- "name": "deploy",
- "title": "deploy: skipped",
- "groups": [
- {
- "name": "review",
- "size": 1,
- "status": {
- "icon": "status_skipped",
- "text": "skipped",
- "label": "skipped",
- "group": "skipped",
- "tooltip": "skipped",
- "has_details": true,
- "details_path": "/gitlab-com/gitlab-docs/-/jobs/114982857",
- "illustration": {
- "image": "https://assets.gitlab-static.net/assets/illustrations/skipped-job_empty-8b877955fbf175e42ae65b6cb95346e15282c6fc5b682756c329af3a0055225e.svg",
- "size": "svg-430",
- "title": "This job has been skipped"
- },
- "favicon": "https://gitlab.com/assets/ci_favicons/favicon_status_skipped-0b9c5e543588945e8c4ca57786bbf2d0c56631959c9f853300392d0315be829b.png"
- },
- "jobs": [
- {
- "id": 11498282342357,
- "name": "review",
- "started": null,
- "archived": false,
- "build_path": "/gitlab-com/gitlab-docs/-/jobs/114982857",
- "playable": false,
- "scheduled": false,
- "created_at": "2018-10-31T16:35:23.805Z",
- "updated_at": "2018-10-31T16:41:40.569Z",
- "status": {
- "icon": "status_skipped",
- "text": "skipped",
- "label": "skipped",
- "group": "skipped",
- "tooltip": "skipped",
- "has_details": true,
- "details_path": "/gitlab-com/gitlab-docs/-/jobs/114982857",
- "illustration": {
- "image": "https://assets.gitlab-static.net/assets/illustrations/skipped-job_empty-8b877955fbf175e42ae65b6cb95346e15282c6fc5b682756c329af3a0055225e.svg",
- "size": "svg-430",
- "title": "This job has been skipped"
- },
- "favicon": "https://gitlab.com/assets/ci_favicons/favicon_status_skipped-0b9c5e543588945e8c4ca57786bbf2d0c56631959c9f853300392d0315be829b.png"
- }
- }
- ]
- },
- {
- "name": "review_stop",
- "size": 1,
- "status": {
- "icon": "status_skipped",
- "text": "skipped",
- "label": "skipped",
- "group": "skipped",
- "tooltip": "skipped",
- "has_details": true,
- "details_path": "/gitlab-com/gitlab-docs/-/jobs/114982858",
- "illustration": {
- "image": "https://assets.gitlab-static.net/assets/illustrations/skipped-job_empty-8b877955fbf175e42ae65b6cb95346e15282c6fc5b682756c329af3a0055225e.svg",
- "size": "svg-430",
- "title": "This job has been skipped"
- },
- "favicon": "https://gitlab.com/assets/ci_favicons/favicon_status_skipped-0b9c5e543588945e8c4ca57786bbf2d0c56631959c9f853300392d0315be829b.png"
- },
- "jobs": [
- {
- "id": 114982858,
- "name": "review_stop",
- "started": null,
- "archived": false,
- "build_path": "/gitlab-com/gitlab-docs/-/jobs/114982858",
- "playable": false,
- "scheduled": false,
- "created_at": "2018-10-31T16:35:23.840Z",
- "updated_at": "2018-10-31T16:41:40.480Z",
- "status": {
- "icon": "status_skipped",
- "text": "skipped",
- "label": "skipped",
- "group": "skipped",
- "tooltip": "skipped",
- "has_details": true,
- "details_path": "/gitlab-com/gitlab-docs/-/jobs/114982858",
- "illustration": {
- "image": "https://assets.gitlab-static.net/assets/illustrations/skipped-job_empty-8b877955fbf175e42ae65b6cb95346e15282c6fc5b682756c329af3a0055225e.svg",
- "size": "svg-430",
- "title": "This job has been skipped"
- },
- "favicon": "https://gitlab.com/assets/ci_favicons/favicon_status_skipped-0b9c5e543588945e8c4ca57786bbf2d0c56631959c9f853300392d0315be829b.png"
- }
- }
- ]
- }
- ],
- "status": {
- "icon": "status_skipped",
- "text": "skipped",
- "label": "skipped",
- "group": "skipped",
- "tooltip": "skipped",
- "has_details": true,
- "details_path": "/gitlab-com/gitlab-docs/pipelines/34993051#deploy",
- "illustration": null,
- "favicon": "https://gitlab.com/assets/ci_favicons/favicon_status_skipped-0b9c5e543588945e8c4ca57786bbf2d0c56631959c9f853300392d0315be829b.png"
- },
- "path": "/gitlab-com/gitlab-docs/pipelines/34993051#deploy",
- "dropdown_path": "/gitlab-com/gitlab-docs/pipelines/34993051/stage.json?stage=deploy"
- }
- ],
- "artifacts": [],
- "manual_actions": [
- {
- "name": "image:bootstrap",
- "path": "/gitlab-com/gitlab-docs/-/jobs/114982853/play",
- "playable": true,
- "scheduled": false
- },
- {
- "name": "image:builder-onbuild",
- "path": "/gitlab-com/gitlab-docs/-/jobs/114982854/play",
- "playable": true,
- "scheduled": false
- },
- {
- "name": "image:nginx-onbuild",
- "path": "/gitlab-com/gitlab-docs/-/jobs/114982855/play",
- "playable": true,
- "scheduled": false
- },
- {
- "name": "review_stop",
- "path": "/gitlab-com/gitlab-docs/-/jobs/114982858/play",
- "playable": false,
- "scheduled": false
- }
- ],
- "scheduled_actions": []
- },
- "project": {
- "id": 1794617,
- "name": "Test",
- "full_path": "/gitlab-com/gitlab-docs",
- "full_name": "GitLab.com / GitLab Docs"
- },
- "triggered_by": {
- "id": 349932310342451,
- "user": {
- "id": 376774,
- "name": "Alessio Caiazza",
- "username": "nolith",
- "state": "active",
- "avatar_url": "https://assets.gitlab-static.net/uploads/-/system/user/avatar/376774/avatar.png",
- "web_url": "https://gitlab.com/nolith",
- "status_tooltip_html": null,
- "path": "/nolith"
- },
- "active": false,
- "coverage": null,
- "source": "pipeline",
- "path": "/gitlab-com/gitlab-docs/pipelines/34993051",
- "details": {
- "status": {
- "icon": "status_failed",
- "text": "failed",
- "label": "failed",
- "group": "failed",
- "tooltip": "failed",
- "has_details": true,
- "details_path": "/gitlab-com/gitlab-docs/pipelines/34993051",
- "illustration": null,
- "favicon": "https://gitlab.com/assets/ci_favicons/favicon_status_failed-41304d7f7e3828808b0c26771f0309e55296819a9beea3ea9fbf6689d9857c12.png"
- },
- "duration": 118,
- "finished_at": "2018-10-31T16:41:40.615Z",
- "stages": [
- {
- "name": "build-images",
- "title": "build-images: skipped",
- "groups": [
- {
- "name": "image:bootstrap",
- "size": 1,
- "status": {
- "icon": "status_manual",
- "text": "manual",
- "label": "manual play action",
- "group": "manual",
- "tooltip": "manual action",
- "has_details": true,
- "details_path": "/gitlab-com/gitlab-docs/-/jobs/114982853",
- "illustration": {
- "image": "https://assets.gitlab-static.net/assets/illustrations/manual_action-2b4ca0d1bcfd92aebf33d484e36cbf7a102d007f76b5a0cfea636033a629d601.svg",
- "size": "svg-394",
- "title": "This job requires a manual action",
- "content": "This job depends on a user to trigger its process. Often they are used to deploy code to production environments"
- },
- "favicon": "https://gitlab.com/assets/ci_favicons/favicon_status_manual-829a0804612cef47d9efc1618dba38325483657c847dba0546c3b9f0295bb36c.png",
- "action": {
- "icon": "play",
- "title": "Play",
- "path": "/gitlab-com/gitlab-docs/-/jobs/114982853/play",
- "method": "post",
- "button_title": "Trigger this manual action"
- }
- },
- "jobs": [
- {
- "id": 11421321982853,
- "name": "image:bootstrap",
- "started": null,
- "archived": false,
- "build_path": "/gitlab-com/gitlab-docs/-/jobs/114982853",
- "play_path": "/gitlab-com/gitlab-docs/-/jobs/114982853/play",
- "playable": true,
- "scheduled": false,
- "created_at": "2018-10-31T16:35:23.704Z",
- "updated_at": "2018-10-31T16:35:24.118Z",
- "status": {
- "icon": "status_manual",
- "text": "manual",
- "label": "manual play action",
- "group": "manual",
- "tooltip": "manual action",
- "has_details": true,
- "details_path": "/gitlab-com/gitlab-docs/-/jobs/114982853",
- "illustration": {
- "image": "https://assets.gitlab-static.net/assets/illustrations/manual_action-2b4ca0d1bcfd92aebf33d484e36cbf7a102d007f76b5a0cfea636033a629d601.svg",
- "size": "svg-394",
- "title": "This job requires a manual action",
- "content": "This job depends on a user to trigger its process. Often they are used to deploy code to production environments"
- },
- "favicon": "https://gitlab.com/assets/ci_favicons/favicon_status_manual-829a0804612cef47d9efc1618dba38325483657c847dba0546c3b9f0295bb36c.png",
- "action": {
- "icon": "play",
- "title": "Play",
- "path": "/gitlab-com/gitlab-docs/-/jobs/114982853/play",
- "method": "post",
- "button_title": "Trigger this manual action"
- }
- }
- }
- ]
- },
- {
- "name": "image:builder-onbuild",
- "size": 1,
- "status": {
- "icon": "status_manual",
- "text": "manual",
- "label": "manual play action",
- "group": "manual",
- "tooltip": "manual action",
- "has_details": true,
- "details_path": "/gitlab-com/gitlab-docs/-/jobs/114982854",
- "illustration": {
- "image": "https://assets.gitlab-static.net/assets/illustrations/manual_action-2b4ca0d1bcfd92aebf33d484e36cbf7a102d007f76b5a0cfea636033a629d601.svg",
- "size": "svg-394",
- "title": "This job requires a manual action",
- "content": "This job depends on a user to trigger its process. Often they are used to deploy code to production environments"
- },
- "favicon": "https://gitlab.com/assets/ci_favicons/favicon_status_manual-829a0804612cef47d9efc1618dba38325483657c847dba0546c3b9f0295bb36c.png",
- "action": {
- "icon": "play",
- "title": "Play",
- "path": "/gitlab-com/gitlab-docs/-/jobs/114982854/play",
- "method": "post",
- "button_title": "Trigger this manual action"
- }
- },
- "jobs": [
- {
- "id": 1149822131854,
- "name": "image:builder-onbuild",
- "started": null,
- "archived": false,
- "build_path": "/gitlab-com/gitlab-docs/-/jobs/114982854",
- "play_path": "/gitlab-com/gitlab-docs/-/jobs/114982854/play",
- "playable": true,
- "scheduled": false,
- "created_at": "2018-10-31T16:35:23.728Z",
- "updated_at": "2018-10-31T16:35:24.070Z",
- "status": {
- "icon": "status_manual",
- "text": "manual",
- "label": "manual play action",
- "group": "manual",
- "tooltip": "manual action",
- "has_details": true,
- "details_path": "/gitlab-com/gitlab-docs/-/jobs/114982854",
- "illustration": {
- "image": "https://assets.gitlab-static.net/assets/illustrations/manual_action-2b4ca0d1bcfd92aebf33d484e36cbf7a102d007f76b5a0cfea636033a629d601.svg",
- "size": "svg-394",
- "title": "This job requires a manual action",
- "content": "This job depends on a user to trigger its process. Often they are used to deploy code to production environments"
- },
- "favicon": "https://gitlab.com/assets/ci_favicons/favicon_status_manual-829a0804612cef47d9efc1618dba38325483657c847dba0546c3b9f0295bb36c.png",
- "action": {
- "icon": "play",
- "title": "Play",
- "path": "/gitlab-com/gitlab-docs/-/jobs/114982854/play",
- "method": "post",
- "button_title": "Trigger this manual action"
- }
- }
- }
- ]
- },
- {
- "name": "image:nginx-onbuild",
- "size": 1,
- "status": {
- "icon": "status_manual",
- "text": "manual",
- "label": "manual play action",
- "group": "manual",
- "tooltip": "manual action",
- "has_details": true,
- "details_path": "/gitlab-com/gitlab-docs/-/jobs/114982855",
- "illustration": {
- "image": "https://assets.gitlab-static.net/assets/illustrations/manual_action-2b4ca0d1bcfd92aebf33d484e36cbf7a102d007f76b5a0cfea636033a629d601.svg",
- "size": "svg-394",
- "title": "This job requires a manual action",
- "content": "This job depends on a user to trigger its process. Often they are used to deploy code to production environments"
- },
- "favicon": "https://gitlab.com/assets/ci_favicons/favicon_status_manual-829a0804612cef47d9efc1618dba38325483657c847dba0546c3b9f0295bb36c.png",
- "action": {
- "icon": "play",
- "title": "Play",
- "path": "/gitlab-com/gitlab-docs/-/jobs/114982855/play",
- "method": "post",
- "button_title": "Trigger this manual action"
- }
- },
- "jobs": [
- {
- "id": 11498285523424,
- "name": "image:nginx-onbuild",
- "started": null,
- "archived": false,
- "build_path": "/gitlab-com/gitlab-docs/-/jobs/114982855",
- "play_path": "/gitlab-com/gitlab-docs/-/jobs/114982855/play",
- "playable": true,
- "scheduled": false,
- "created_at": "2018-10-31T16:35:23.753Z",
- "updated_at": "2018-10-31T16:35:24.033Z",
- "status": {
- "icon": "status_manual",
- "text": "manual",
- "label": "manual play action",
- "group": "manual",
- "tooltip": "manual action",
- "has_details": true,
- "details_path": "/gitlab-com/gitlab-docs/-/jobs/114982855",
- "illustration": {
- "image": "https://assets.gitlab-static.net/assets/illustrations/manual_action-2b4ca0d1bcfd92aebf33d484e36cbf7a102d007f76b5a0cfea636033a629d601.svg",
- "size": "svg-394",
- "title": "This job requires a manual action",
- "content": "This job depends on a user to trigger its process. Often they are used to deploy code to production environments"
- },
- "favicon": "https://gitlab.com/assets/ci_favicons/favicon_status_manual-829a0804612cef47d9efc1618dba38325483657c847dba0546c3b9f0295bb36c.png",
- "action": {
- "icon": "play",
- "title": "Play",
- "path": "/gitlab-com/gitlab-docs/-/jobs/114982855/play",
- "method": "post",
- "button_title": "Trigger this manual action"
- }
- }
- }
- ]
- }
- ],
- "status": {
- "icon": "status_skipped",
- "text": "skipped",
- "label": "skipped",
- "group": "skipped",
- "tooltip": "skipped",
- "has_details": true,
- "details_path": "/gitlab-com/gitlab-docs/pipelines/34993051#build-images",
- "illustration": null,
- "favicon": "https://gitlab.com/assets/ci_favicons/favicon_status_skipped-0b9c5e543588945e8c4ca57786bbf2d0c56631959c9f853300392d0315be829b.png"
- },
- "path": "/gitlab-com/gitlab-docs/pipelines/34993051#build-images",
- "dropdown_path": "/gitlab-com/gitlab-docs/pipelines/34993051/stage.json?stage=build-images"
- },
- {
- "name": "build",
- "title": "build: failed",
- "groups": [
- {
- "name": "compile_dev",
- "size": 1,
- "status": {
- "icon": "status_failed",
- "text": "failed",
- "label": "failed",
- "group": "failed",
- "tooltip": "failed - (script failure)",
- "has_details": true,
- "details_path": "/gitlab-com/gitlab-docs/-/jobs/114984694",
- "illustration": {
- "image": "https://assets.gitlab-static.net/assets/illustrations/skipped-job_empty-8b877955fbf175e42ae65b6cb95346e15282c6fc5b682756c329af3a0055225e.svg",
- "size": "svg-430",
- "title": "This job does not have a trace."
- },
- "favicon": "https://gitlab.com/assets/ci_favicons/favicon_status_failed-41304d7f7e3828808b0c26771f0309e55296819a9beea3ea9fbf6689d9857c12.png",
- "action": {
- "icon": "retry",
- "title": "Retry",
- "path": "/gitlab-com/gitlab-docs/-/jobs/114984694/retry",
- "method": "post",
- "button_title": "Retry this job"
- }
- },
- "jobs": [
- {
- "id": 1149846949786,
- "name": "compile_dev",
- "started": "2018-10-31T16:39:41.598Z",
- "archived": false,
- "build_path": "/gitlab-com/gitlab-docs/-/jobs/114984694",
- "retry_path": "/gitlab-com/gitlab-docs/-/jobs/114984694/retry",
- "playable": false,
- "scheduled": false,
- "created_at": "2018-10-31T16:39:41.138Z",
- "updated_at": "2018-10-31T16:41:40.072Z",
- "status": {
- "icon": "status_failed",
- "text": "failed",
- "label": "failed",
- "group": "failed",
- "tooltip": "failed - (script failure)",
- "has_details": true,
- "details_path": "/gitlab-com/gitlab-docs/-/jobs/114984694",
- "illustration": {
- "image": "https://assets.gitlab-static.net/assets/illustrations/skipped-job_empty-8b877955fbf175e42ae65b6cb95346e15282c6fc5b682756c329af3a0055225e.svg",
- "size": "svg-430",
- "title": "This job does not have a trace."
- },
- "favicon": "https://gitlab.com/assets/ci_favicons/favicon_status_failed-41304d7f7e3828808b0c26771f0309e55296819a9beea3ea9fbf6689d9857c12.png",
- "action": {
- "icon": "retry",
- "title": "Retry",
- "path": "/gitlab-com/gitlab-docs/-/jobs/114984694/retry",
- "method": "post",
- "button_title": "Retry this job"
- }
- },
- "recoverable": false
- }
- ]
- }
- ],
- "status": {
- "icon": "status_failed",
- "text": "failed",
- "label": "failed",
- "group": "failed",
- "tooltip": "failed",
- "has_details": true,
- "details_path": "/gitlab-com/gitlab-docs/pipelines/34993051#build",
- "illustration": null,
- "favicon": "https://gitlab.com/assets/ci_favicons/favicon_status_failed-41304d7f7e3828808b0c26771f0309e55296819a9beea3ea9fbf6689d9857c12.png"
- },
- "path": "/gitlab-com/gitlab-docs/pipelines/34993051#build",
- "dropdown_path": "/gitlab-com/gitlab-docs/pipelines/34993051/stage.json?stage=build"
- },
- {
- "name": "deploy",
- "title": "deploy: skipped",
- "groups": [
- {
- "name": "review",
- "size": 1,
- "status": {
- "icon": "status_skipped",
- "text": "skipped",
- "label": "skipped",
- "group": "skipped",
- "tooltip": "skipped",
- "has_details": true,
- "details_path": "/gitlab-com/gitlab-docs/-/jobs/114982857",
- "illustration": {
- "image": "https://assets.gitlab-static.net/assets/illustrations/skipped-job_empty-8b877955fbf175e42ae65b6cb95346e15282c6fc5b682756c329af3a0055225e.svg",
- "size": "svg-430",
- "title": "This job has been skipped"
- },
- "favicon": "https://gitlab.com/assets/ci_favicons/favicon_status_skipped-0b9c5e543588945e8c4ca57786bbf2d0c56631959c9f853300392d0315be829b.png"
- },
- "jobs": [
- {
- "id": 11498282342357,
- "name": "review",
- "started": null,
- "archived": false,
- "build_path": "/gitlab-com/gitlab-docs/-/jobs/114982857",
- "playable": false,
- "scheduled": false,
- "created_at": "2018-10-31T16:35:23.805Z",
- "updated_at": "2018-10-31T16:41:40.569Z",
- "status": {
- "icon": "status_skipped",
- "text": "skipped",
- "label": "skipped",
- "group": "skipped",
- "tooltip": "skipped",
- "has_details": true,
- "details_path": "/gitlab-com/gitlab-docs/-/jobs/114982857",
- "illustration": {
- "image": "https://assets.gitlab-static.net/assets/illustrations/skipped-job_empty-8b877955fbf175e42ae65b6cb95346e15282c6fc5b682756c329af3a0055225e.svg",
- "size": "svg-430",
- "title": "This job has been skipped"
- },
- "favicon": "https://gitlab.com/assets/ci_favicons/favicon_status_skipped-0b9c5e543588945e8c4ca57786bbf2d0c56631959c9f853300392d0315be829b.png"
- }
- }
- ]
- },
- {
- "name": "review_stop",
- "size": 1,
- "status": {
- "icon": "status_skipped",
- "text": "skipped",
- "label": "skipped",
- "group": "skipped",
- "tooltip": "skipped",
- "has_details": true,
- "details_path": "/gitlab-com/gitlab-docs/-/jobs/114982858",
- "illustration": {
- "image": "https://assets.gitlab-static.net/assets/illustrations/skipped-job_empty-8b877955fbf175e42ae65b6cb95346e15282c6fc5b682756c329af3a0055225e.svg",
- "size": "svg-430",
- "title": "This job has been skipped"
- },
- "favicon": "https://gitlab.com/assets/ci_favicons/favicon_status_skipped-0b9c5e543588945e8c4ca57786bbf2d0c56631959c9f853300392d0315be829b.png"
- },
- "jobs": [
- {
- "id": 114982858,
- "name": "review_stop",
- "started": null,
- "archived": false,
- "build_path": "/gitlab-com/gitlab-docs/-/jobs/114982858",
- "playable": false,
- "scheduled": false,
- "created_at": "2018-10-31T16:35:23.840Z",
- "updated_at": "2018-10-31T16:41:40.480Z",
- "status": {
- "icon": "status_skipped",
- "text": "skipped",
- "label": "skipped",
- "group": "skipped",
- "tooltip": "skipped",
- "has_details": true,
- "details_path": "/gitlab-com/gitlab-docs/-/jobs/114982858",
- "illustration": {
- "image": "https://assets.gitlab-static.net/assets/illustrations/skipped-job_empty-8b877955fbf175e42ae65b6cb95346e15282c6fc5b682756c329af3a0055225e.svg",
- "size": "svg-430",
- "title": "This job has been skipped"
- },
- "favicon": "https://gitlab.com/assets/ci_favicons/favicon_status_skipped-0b9c5e543588945e8c4ca57786bbf2d0c56631959c9f853300392d0315be829b.png"
- }
- }
- ]
- }
- ],
- "status": {
- "icon": "status_skipped",
- "text": "skipped",
- "label": "skipped",
- "group": "skipped",
- "tooltip": "skipped",
- "has_details": true,
- "details_path": "/gitlab-com/gitlab-docs/pipelines/34993051#deploy",
- "illustration": null,
- "favicon": "https://gitlab.com/assets/ci_favicons/favicon_status_skipped-0b9c5e543588945e8c4ca57786bbf2d0c56631959c9f853300392d0315be829b.png"
- },
- "path": "/gitlab-com/gitlab-docs/pipelines/34993051#deploy",
- "dropdown_path": "/gitlab-com/gitlab-docs/pipelines/34993051/stage.json?stage=deploy"
- }
- ],
- "artifacts": [],
- "manual_actions": [
- {
- "name": "image:bootstrap",
- "path": "/gitlab-com/gitlab-docs/-/jobs/114982853/play",
- "playable": true,
- "scheduled": false
- },
- {
- "name": "image:builder-onbuild",
- "path": "/gitlab-com/gitlab-docs/-/jobs/114982854/play",
- "playable": true,
- "scheduled": false
- },
- {
- "name": "image:nginx-onbuild",
- "path": "/gitlab-com/gitlab-docs/-/jobs/114982855/play",
- "playable": true,
- "scheduled": false
- },
- {
- "name": "review_stop",
- "path": "/gitlab-com/gitlab-docs/-/jobs/114982858/play",
- "playable": false,
- "scheduled": false
- }
- ],
- "scheduled_actions": []
- },
- "project": {
- "id": 1794617,
- "name": "GitLab Docs",
- "full_path": "/gitlab-com/gitlab-docs",
- "full_name": "GitLab.com / GitLab Docs"
- }
- },
- "triggered": []
- },
- "triggered": [
- {
- "id": 34993051,
- "user": {
- "id": 376774,
- "name": "Alessio Caiazza",
- "username": "nolith",
- "state": "active",
- "avatar_url": "https://assets.gitlab-static.net/uploads/-/system/user/avatar/376774/avatar.png",
- "web_url": "https://gitlab.com/nolith",
- "status_tooltip_html": null,
- "path": "/nolith"
- },
- "active": false,
- "coverage": null,
- "source": "pipeline",
- "path": "/gitlab-com/gitlab-docs/pipelines/34993051",
- "details": {
- "status": {
- "icon": "status_failed",
- "text": "failed",
- "label": "failed",
- "group": "failed",
- "tooltip": "failed",
- "has_details": true,
- "details_path": "/gitlab-com/gitlab-docs/pipelines/34993051",
- "illustration": null,
- "favicon": "https://gitlab.com/assets/ci_favicons/favicon_status_failed-41304d7f7e3828808b0c26771f0309e55296819a9beea3ea9fbf6689d9857c12.png"
- },
- "duration": 118,
- "finished_at": "2018-10-31T16:41:40.615Z",
- "stages": [
- {
- "name": "build-images",
- "title": "build-images: skipped",
- "groups": [
- {
- "name": "image:bootstrap",
- "size": 1,
- "status": {
- "icon": "status_manual",
- "text": "manual",
- "label": "manual play action",
- "group": "manual",
- "tooltip": "manual action",
- "has_details": true,
- "details_path": "/gitlab-com/gitlab-docs/-/jobs/114982853",
- "illustration": {
- "image": "https://assets.gitlab-static.net/assets/illustrations/manual_action-2b4ca0d1bcfd92aebf33d484e36cbf7a102d007f76b5a0cfea636033a629d601.svg",
- "size": "svg-394",
- "title": "This job requires a manual action",
- "content": "This job depends on a user to trigger its process. Often they are used to deploy code to production environments"
- },
- "favicon": "https://gitlab.com/assets/ci_favicons/favicon_status_manual-829a0804612cef47d9efc1618dba38325483657c847dba0546c3b9f0295bb36c.png",
- "action": {
- "icon": "play",
- "title": "Play",
- "path": "/gitlab-com/gitlab-docs/-/jobs/114982853/play",
- "method": "post",
- "button_title": "Trigger this manual action"
- }
- },
- "jobs": [
- {
- "id": 114982853,
- "name": "image:bootstrap",
- "started": null,
- "archived": false,
- "build_path": "/gitlab-com/gitlab-docs/-/jobs/114982853",
- "play_path": "/gitlab-com/gitlab-docs/-/jobs/114982853/play",
- "playable": true,
- "scheduled": false,
- "created_at": "2018-10-31T16:35:23.704Z",
- "updated_at": "2018-10-31T16:35:24.118Z",
- "status": {
- "icon": "status_manual",
- "text": "manual",
- "label": "manual play action",
- "group": "manual",
- "tooltip": "manual action",
- "has_details": true,
- "details_path": "/gitlab-com/gitlab-docs/-/jobs/114982853",
- "illustration": {
- "image": "https://assets.gitlab-static.net/assets/illustrations/manual_action-2b4ca0d1bcfd92aebf33d484e36cbf7a102d007f76b5a0cfea636033a629d601.svg",
- "size": "svg-394",
- "title": "This job requires a manual action",
- "content": "This job depends on a user to trigger its process. Often they are used to deploy code to production environments"
- },
- "favicon": "https://gitlab.com/assets/ci_favicons/favicon_status_manual-829a0804612cef47d9efc1618dba38325483657c847dba0546c3b9f0295bb36c.png",
- "action": {
- "icon": "play",
- "title": "Play",
- "path": "/gitlab-com/gitlab-docs/-/jobs/114982853/play",
- "method": "post",
- "button_title": "Trigger this manual action"
- }
- }
- }
- ]
- },
- {
- "name": "image:builder-onbuild",
- "size": 1,
- "status": {
- "icon": "status_manual",
- "text": "manual",
- "label": "manual play action",
- "group": "manual",
- "tooltip": "manual action",
- "has_details": true,
- "details_path": "/gitlab-com/gitlab-docs/-/jobs/114982854",
- "illustration": {
- "image": "https://assets.gitlab-static.net/assets/illustrations/manual_action-2b4ca0d1bcfd92aebf33d484e36cbf7a102d007f76b5a0cfea636033a629d601.svg",
- "size": "svg-394",
- "title": "This job requires a manual action",
- "content": "This job depends on a user to trigger its process. Often they are used to deploy code to production environments"
- },
- "favicon": "https://gitlab.com/assets/ci_favicons/favicon_status_manual-829a0804612cef47d9efc1618dba38325483657c847dba0546c3b9f0295bb36c.png",
- "action": {
- "icon": "play",
- "title": "Play",
- "path": "/gitlab-com/gitlab-docs/-/jobs/114982854/play",
- "method": "post",
- "button_title": "Trigger this manual action"
- }
- },
- "jobs": [
- {
- "id": 114982854,
- "name": "image:builder-onbuild",
- "started": null,
- "archived": false,
- "build_path": "/gitlab-com/gitlab-docs/-/jobs/114982854",
- "play_path": "/gitlab-com/gitlab-docs/-/jobs/114982854/play",
- "playable": true,
- "scheduled": false,
- "created_at": "2018-10-31T16:35:23.728Z",
- "updated_at": "2018-10-31T16:35:24.070Z",
- "status": {
- "icon": "status_manual",
- "text": "manual",
- "label": "manual play action",
- "group": "manual",
- "tooltip": "manual action",
- "has_details": true,
- "details_path": "/gitlab-com/gitlab-docs/-/jobs/114982854",
- "illustration": {
- "image": "https://assets.gitlab-static.net/assets/illustrations/manual_action-2b4ca0d1bcfd92aebf33d484e36cbf7a102d007f76b5a0cfea636033a629d601.svg",
- "size": "svg-394",
- "title": "This job requires a manual action",
- "content": "This job depends on a user to trigger its process. Often they are used to deploy code to production environments"
- },
- "favicon": "https://gitlab.com/assets/ci_favicons/favicon_status_manual-829a0804612cef47d9efc1618dba38325483657c847dba0546c3b9f0295bb36c.png",
- "action": {
- "icon": "play",
- "title": "Play",
- "path": "/gitlab-com/gitlab-docs/-/jobs/114982854/play",
- "method": "post",
- "button_title": "Trigger this manual action"
- }
- }
- }
- ]
- },
- {
- "name": "image:nginx-onbuild",
- "size": 1,
- "status": {
- "icon": "status_manual",
- "text": "manual",
- "label": "manual play action",
- "group": "manual",
- "tooltip": "manual action",
- "has_details": true,
- "details_path": "/gitlab-com/gitlab-docs/-/jobs/114982855",
- "illustration": {
- "image": "https://assets.gitlab-static.net/assets/illustrations/manual_action-2b4ca0d1bcfd92aebf33d484e36cbf7a102d007f76b5a0cfea636033a629d601.svg",
- "size": "svg-394",
- "title": "This job requires a manual action",
- "content": "This job depends on a user to trigger its process. Often they are used to deploy code to production environments"
- },
- "favicon": "https://gitlab.com/assets/ci_favicons/favicon_status_manual-829a0804612cef47d9efc1618dba38325483657c847dba0546c3b9f0295bb36c.png",
- "action": {
- "icon": "play",
- "title": "Play",
- "path": "/gitlab-com/gitlab-docs/-/jobs/114982855/play",
- "method": "post",
- "button_title": "Trigger this manual action"
- }
- },
- "jobs": [
- {
- "id": 114982855,
- "name": "image:nginx-onbuild",
- "started": null,
- "archived": false,
- "build_path": "/gitlab-com/gitlab-docs/-/jobs/114982855",
- "play_path": "/gitlab-com/gitlab-docs/-/jobs/114982855/play",
- "playable": true,
- "scheduled": false,
- "created_at": "2018-10-31T16:35:23.753Z",
- "updated_at": "2018-10-31T16:35:24.033Z",
- "status": {
- "icon": "status_manual",
- "text": "manual",
- "label": "manual play action",
- "group": "manual",
- "tooltip": "manual action",
- "has_details": true,
- "details_path": "/gitlab-com/gitlab-docs/-/jobs/114982855",
- "illustration": {
- "image": "https://assets.gitlab-static.net/assets/illustrations/manual_action-2b4ca0d1bcfd92aebf33d484e36cbf7a102d007f76b5a0cfea636033a629d601.svg",
- "size": "svg-394",
- "title": "This job requires a manual action",
- "content": "This job depends on a user to trigger its process. Often they are used to deploy code to production environments"
- },
- "favicon": "https://gitlab.com/assets/ci_favicons/favicon_status_manual-829a0804612cef47d9efc1618dba38325483657c847dba0546c3b9f0295bb36c.png",
- "action": {
- "icon": "play",
- "title": "Play",
- "path": "/gitlab-com/gitlab-docs/-/jobs/114982855/play",
- "method": "post",
- "button_title": "Trigger this manual action"
- }
- }
- }
- ]
- }
- ],
- "status": {
- "icon": "status_skipped",
- "text": "skipped",
- "label": "skipped",
- "group": "skipped",
- "tooltip": "skipped",
- "has_details": true,
- "details_path": "/gitlab-com/gitlab-docs/pipelines/34993051#build-images",
- "illustration": null,
- "favicon": "https://gitlab.com/assets/ci_favicons/favicon_status_skipped-0b9c5e543588945e8c4ca57786bbf2d0c56631959c9f853300392d0315be829b.png"
- },
- "path": "/gitlab-com/gitlab-docs/pipelines/34993051#build-images",
- "dropdown_path": "/gitlab-com/gitlab-docs/pipelines/34993051/stage.json?stage=build-images"
- },
- {
- "name": "build",
- "title": "build: failed",
- "groups": [
- {
- "name": "compile_dev",
- "size": 1,
- "status": {
- "icon": "status_failed",
- "text": "failed",
- "label": "failed",
- "group": "failed",
- "tooltip": "failed - (script failure)",
- "has_details": true,
- "details_path": "/gitlab-com/gitlab-docs/-/jobs/114984694",
- "illustration": {
- "image": "https://assets.gitlab-static.net/assets/illustrations/skipped-job_empty-8b877955fbf175e42ae65b6cb95346e15282c6fc5b682756c329af3a0055225e.svg",
- "size": "svg-430",
- "title": "This job does not have a trace."
- },
- "favicon": "https://gitlab.com/assets/ci_favicons/favicon_status_failed-41304d7f7e3828808b0c26771f0309e55296819a9beea3ea9fbf6689d9857c12.png",
- "action": {
- "icon": "retry",
- "title": "Retry",
- "path": "/h5bp/html5-boilerplate/-/jobs/528/retry",
- "method": "post",
- "button_title": "Retry this job"
- }
- },
- "jobs": [
- {
- "id": 114984694,
- "name": "compile_dev",
- "started": "2018-10-31T16:39:41.598Z",
- "archived": false,
- "build_path": "/gitlab-com/gitlab-docs/-/jobs/114984694",
- "retry_path": "/gitlab-com/gitlab-docs/-/jobs/114984694/retry",
- "playable": false,
- "scheduled": false,
- "created_at": "2018-10-31T16:39:41.138Z",
- "updated_at": "2018-10-31T16:41:40.072Z",
- "status": {
- "icon": "status_failed",
- "text": "failed",
- "label": "failed",
- "group": "failed",
- "tooltip": "failed - (script failure)",
- "has_details": true,
- "details_path": "/gitlab-com/gitlab-docs/-/jobs/114984694",
- "illustration": {
- "image": "https://assets.gitlab-static.net/assets/illustrations/skipped-job_empty-8b877955fbf175e42ae65b6cb95346e15282c6fc5b682756c329af3a0055225e.svg",
- "size": "svg-430",
- "title": "This job does not have a trace."
- },
- "favicon": "https://gitlab.com/assets/ci_favicons/favicon_status_failed-41304d7f7e3828808b0c26771f0309e55296819a9beea3ea9fbf6689d9857c12.png",
- "action": {
- "icon": "retry",
- "title": "Retry",
- "path": "/h5bp/html5-boilerplate/-/jobs/528/retry",
- "method": "post",
- "button_title": "Retry this job"
- }
- },
- "recoverable": false
- }
- ]
- }
- ],
- "status": {
- "icon": "status_failed",
- "text": "failed",
- "label": "failed",
- "group": "failed",
- "tooltip": "failed",
- "has_details": true,
- "details_path": "/gitlab-com/gitlab-docs/pipelines/34993051#build",
- "illustration": null,
- "favicon": "https://gitlab.com/assets/ci_favicons/favicon_status_failed-41304d7f7e3828808b0c26771f0309e55296819a9beea3ea9fbf6689d9857c12.png"
- },
- "path": "/gitlab-com/gitlab-docs/pipelines/34993051#build",
- "dropdown_path": "/gitlab-com/gitlab-docs/pipelines/34993051/stage.json?stage=build"
- },
- {
- "name": "deploy",
- "title": "deploy: skipped",
- "groups": [
- {
- "name": "review",
- "size": 1,
- "status": {
- "icon": "status_skipped",
- "text": "skipped",
- "label": "skipped",
- "group": "skipped",
- "tooltip": "skipped",
- "has_details": true,
- "details_path": "/gitlab-com/gitlab-docs/-/jobs/114982857",
- "illustration": {
- "image": "https://assets.gitlab-static.net/assets/illustrations/skipped-job_empty-8b877955fbf175e42ae65b6cb95346e15282c6fc5b682756c329af3a0055225e.svg",
- "size": "svg-430",
- "title": "This job has been skipped"
- },
- "favicon": "https://gitlab.com/assets/ci_favicons/favicon_status_skipped-0b9c5e543588945e8c4ca57786bbf2d0c56631959c9f853300392d0315be829b.png"
- },
- "jobs": [
- {
- "id": 114982857,
- "name": "review",
- "started": null,
- "archived": false,
- "build_path": "/gitlab-com/gitlab-docs/-/jobs/114982857",
- "playable": false,
- "scheduled": false,
- "created_at": "2018-10-31T16:35:23.805Z",
- "updated_at": "2018-10-31T16:41:40.569Z",
- "status": {
- "icon": "status_skipped",
- "text": "skipped",
- "label": "skipped",
- "group": "skipped",
- "tooltip": "skipped",
- "has_details": true,
- "details_path": "/gitlab-com/gitlab-docs/-/jobs/114982857",
- "illustration": {
- "image": "https://assets.gitlab-static.net/assets/illustrations/skipped-job_empty-8b877955fbf175e42ae65b6cb95346e15282c6fc5b682756c329af3a0055225e.svg",
- "size": "svg-430",
- "title": "This job has been skipped"
- },
- "favicon": "https://gitlab.com/assets/ci_favicons/favicon_status_skipped-0b9c5e543588945e8c4ca57786bbf2d0c56631959c9f853300392d0315be829b.png"
- }
- }
- ]
- },
- {
- "name": "review_stop",
- "size": 1,
- "status": {
- "icon": "status_skipped",
- "text": "skipped",
- "label": "skipped",
- "group": "skipped",
- "tooltip": "skipped",
- "has_details": true,
- "details_path": "/gitlab-com/gitlab-docs/-/jobs/114982858",
- "illustration": {
- "image": "https://assets.gitlab-static.net/assets/illustrations/skipped-job_empty-8b877955fbf175e42ae65b6cb95346e15282c6fc5b682756c329af3a0055225e.svg",
- "size": "svg-430",
- "title": "This job has been skipped"
- },
- "favicon": "https://gitlab.com/assets/ci_favicons/favicon_status_skipped-0b9c5e543588945e8c4ca57786bbf2d0c56631959c9f853300392d0315be829b.png"
- },
- "jobs": [
- {
- "id": 114982858,
- "name": "review_stop",
- "started": null,
- "archived": false,
- "build_path": "/gitlab-com/gitlab-docs/-/jobs/114982858",
- "playable": false,
- "scheduled": false,
- "created_at": "2018-10-31T16:35:23.840Z",
- "updated_at": "2018-10-31T16:41:40.480Z",
- "status": {
- "icon": "status_skipped",
- "text": "skipped",
- "label": "skipped",
- "group": "skipped",
- "tooltip": "skipped",
- "has_details": true,
- "details_path": "/gitlab-com/gitlab-docs/-/jobs/114982858",
- "illustration": {
- "image": "https://assets.gitlab-static.net/assets/illustrations/skipped-job_empty-8b877955fbf175e42ae65b6cb95346e15282c6fc5b682756c329af3a0055225e.svg",
- "size": "svg-430",
- "title": "This job has been skipped"
- },
- "favicon": "https://gitlab.com/assets/ci_favicons/favicon_status_skipped-0b9c5e543588945e8c4ca57786bbf2d0c56631959c9f853300392d0315be829b.png"
- }
- }
- ]
- }
- ],
- "status": {
- "icon": "status_skipped",
- "text": "skipped",
- "label": "skipped",
- "group": "skipped",
- "tooltip": "skipped",
- "has_details": true,
- "details_path": "/gitlab-com/gitlab-docs/pipelines/34993051#deploy",
- "illustration": null,
- "favicon": "https://gitlab.com/assets/ci_favicons/favicon_status_skipped-0b9c5e543588945e8c4ca57786bbf2d0c56631959c9f853300392d0315be829b.png"
- },
- "path": "/gitlab-com/gitlab-docs/pipelines/34993051#deploy",
- "dropdown_path": "/gitlab-com/gitlab-docs/pipelines/34993051/stage.json?stage=deploy"
- }
- ],
- "artifacts": [],
- "manual_actions": [
- {
- "name": "image:bootstrap",
- "path": "/gitlab-com/gitlab-docs/-/jobs/114982853/play",
- "playable": true,
- "scheduled": false
- },
- {
- "name": "image:builder-onbuild",
- "path": "/gitlab-com/gitlab-docs/-/jobs/114982854/play",
- "playable": true,
- "scheduled": false
- },
- {
- "name": "image:nginx-onbuild",
- "path": "/gitlab-com/gitlab-docs/-/jobs/114982855/play",
- "playable": true,
- "scheduled": false
- },
- {
- "name": "review_stop",
- "path": "/gitlab-com/gitlab-docs/-/jobs/114982858/play",
- "playable": false,
- "scheduled": false
- }
- ],
- "scheduled_actions": []
- },
- "project": {
- "id": 1794617,
- "name": "GitLab Docs",
- "full_path": "/gitlab-com/gitlab-docs",
- "full_name": "GitLab.com / GitLab Docs"
- }
- },
- {
- "id": 34993052,
- "user": {
- "id": 376774,
- "name": "Alessio Caiazza",
- "username": "nolith",
- "state": "active",
- "avatar_url": "https://assets.gitlab-static.net/uploads/-/system/user/avatar/376774/avatar.png",
- "web_url": "https://gitlab.com/nolith",
- "status_tooltip_html": null,
- "path": "/nolith"
- },
- "active": false,
- "coverage": null,
- "source": "pipeline",
- "path": "/gitlab-com/gitlab-docs/pipelines/34993051",
- "details": {
- "status": {
- "icon": "status_failed",
- "text": "failed",
- "label": "failed",
- "group": "failed",
- "tooltip": "failed",
- "has_details": true,
- "details_path": "/gitlab-com/gitlab-docs/pipelines/34993051",
- "illustration": null,
- "favicon": "https://gitlab.com/assets/ci_favicons/favicon_status_failed-41304d7f7e3828808b0c26771f0309e55296819a9beea3ea9fbf6689d9857c12.png"
- },
- "duration": 118,
- "finished_at": "2018-10-31T16:41:40.615Z",
- "stages": [
- {
- "name": "build-images",
- "title": "build-images: skipped",
- "groups": [
- {
- "name": "image:bootstrap",
- "size": 1,
- "status": {
- "icon": "status_manual",
- "text": "manual",
- "label": "manual play action",
- "group": "manual",
- "tooltip": "manual action",
- "has_details": true,
- "details_path": "/gitlab-com/gitlab-docs/-/jobs/114982853",
- "illustration": {
- "image": "https://assets.gitlab-static.net/assets/illustrations/manual_action-2b4ca0d1bcfd92aebf33d484e36cbf7a102d007f76b5a0cfea636033a629d601.svg",
- "size": "svg-394",
- "title": "This job requires a manual action",
- "content": "This job depends on a user to trigger its process. Often they are used to deploy code to production environments"
- },
- "favicon": "https://gitlab.com/assets/ci_favicons/favicon_status_manual-829a0804612cef47d9efc1618dba38325483657c847dba0546c3b9f0295bb36c.png",
- "action": {
- "icon": "play",
- "title": "Play",
- "path": "/gitlab-com/gitlab-docs/-/jobs/114982853/play",
- "method": "post",
- "button_title": "Trigger this manual action"
- }
- },
- "jobs": [
- {
- "id": 114982853,
- "name": "image:bootstrap",
- "started": null,
- "archived": false,
- "build_path": "/gitlab-com/gitlab-docs/-/jobs/114982853",
- "play_path": "/gitlab-com/gitlab-docs/-/jobs/114982853/play",
- "playable": true,
- "scheduled": false,
- "created_at": "2018-10-31T16:35:23.704Z",
- "updated_at": "2018-10-31T16:35:24.118Z",
- "status": {
- "icon": "status_manual",
- "text": "manual",
- "label": "manual play action",
- "group": "manual",
- "tooltip": "manual action",
- "has_details": true,
- "details_path": "/gitlab-com/gitlab-docs/-/jobs/114982853",
- "illustration": {
- "image": "https://assets.gitlab-static.net/assets/illustrations/manual_action-2b4ca0d1bcfd92aebf33d484e36cbf7a102d007f76b5a0cfea636033a629d601.svg",
- "size": "svg-394",
- "title": "This job requires a manual action",
- "content": "This job depends on a user to trigger its process. Often they are used to deploy code to production environments"
- },
- "favicon": "https://gitlab.com/assets/ci_favicons/favicon_status_manual-829a0804612cef47d9efc1618dba38325483657c847dba0546c3b9f0295bb36c.png",
- "action": {
- "icon": "play",
- "title": "Play",
- "path": "/gitlab-com/gitlab-docs/-/jobs/114982853/play",
- "method": "post",
- "button_title": "Trigger this manual action"
- }
- }
- }
- ]
- },
- {
- "name": "image:builder-onbuild",
- "size": 1,
- "status": {
- "icon": "status_manual",
- "text": "manual",
- "label": "manual play action",
- "group": "manual",
- "tooltip": "manual action",
- "has_details": true,
- "details_path": "/gitlab-com/gitlab-docs/-/jobs/114982854",
- "illustration": {
- "image": "https://assets.gitlab-static.net/assets/illustrations/manual_action-2b4ca0d1bcfd92aebf33d484e36cbf7a102d007f76b5a0cfea636033a629d601.svg",
- "size": "svg-394",
- "title": "This job requires a manual action",
- "content": "This job depends on a user to trigger its process. Often they are used to deploy code to production environments"
- },
- "favicon": "https://gitlab.com/assets/ci_favicons/favicon_status_manual-829a0804612cef47d9efc1618dba38325483657c847dba0546c3b9f0295bb36c.png",
- "action": {
- "icon": "play",
- "title": "Play",
- "path": "/gitlab-com/gitlab-docs/-/jobs/114982854/play",
- "method": "post",
- "button_title": "Trigger this manual action"
- }
- },
- "jobs": [
- {
- "id": 114982854,
- "name": "image:builder-onbuild",
- "started": null,
- "archived": false,
- "build_path": "/gitlab-com/gitlab-docs/-/jobs/114982854",
- "play_path": "/gitlab-com/gitlab-docs/-/jobs/114982854/play",
- "playable": true,
- "scheduled": false,
- "created_at": "2018-10-31T16:35:23.728Z",
- "updated_at": "2018-10-31T16:35:24.070Z",
- "status": {
- "icon": "status_manual",
- "text": "manual",
- "label": "manual play action",
- "group": "manual",
- "tooltip": "manual action",
- "has_details": true,
- "details_path": "/gitlab-com/gitlab-docs/-/jobs/114982854",
- "illustration": {
- "image": "https://assets.gitlab-static.net/assets/illustrations/manual_action-2b4ca0d1bcfd92aebf33d484e36cbf7a102d007f76b5a0cfea636033a629d601.svg",
- "size": "svg-394",
- "title": "This job requires a manual action",
- "content": "This job depends on a user to trigger its process. Often they are used to deploy code to production environments"
- },
- "favicon": "https://gitlab.com/assets/ci_favicons/favicon_status_manual-829a0804612cef47d9efc1618dba38325483657c847dba0546c3b9f0295bb36c.png",
- "action": {
- "icon": "play",
- "title": "Play",
- "path": "/gitlab-com/gitlab-docs/-/jobs/114982854/play",
- "method": "post",
- "button_title": "Trigger this manual action"
- }
- }
- }
- ]
- },
- {
- "name": "image:nginx-onbuild",
- "size": 1,
- "status": {
- "icon": "status_manual",
- "text": "manual",
- "label": "manual play action",
- "group": "manual",
- "tooltip": "manual action",
- "has_details": true,
- "details_path": "/gitlab-com/gitlab-docs/-/jobs/114982855",
- "illustration": {
- "image": "https://assets.gitlab-static.net/assets/illustrations/manual_action-2b4ca0d1bcfd92aebf33d484e36cbf7a102d007f76b5a0cfea636033a629d601.svg",
- "size": "svg-394",
- "title": "This job requires a manual action",
- "content": "This job depends on a user to trigger its process. Often they are used to deploy code to production environments"
- },
- "favicon": "https://gitlab.com/assets/ci_favicons/favicon_status_manual-829a0804612cef47d9efc1618dba38325483657c847dba0546c3b9f0295bb36c.png",
- "action": {
- "icon": "play",
- "title": "Play",
- "path": "/gitlab-com/gitlab-docs/-/jobs/114982855/play",
- "method": "post",
- "button_title": "Trigger this manual action"
- }
- },
- "jobs": [
- {
- "id": 1224982855,
- "name": "image:nginx-onbuild",
- "started": null,
- "archived": false,
- "build_path": "/gitlab-com/gitlab-docs/-/jobs/114982855",
- "play_path": "/gitlab-com/gitlab-docs/-/jobs/114982855/play",
- "playable": true,
- "scheduled": false,
- "created_at": "2018-10-31T16:35:23.753Z",
- "updated_at": "2018-10-31T16:35:24.033Z",
- "status": {
- "icon": "status_manual",
- "text": "manual",
- "label": "manual play action",
- "group": "manual",
- "tooltip": "manual action",
- "has_details": true,
- "details_path": "/gitlab-com/gitlab-docs/-/jobs/114982855",
- "illustration": {
- "image": "https://assets.gitlab-static.net/assets/illustrations/manual_action-2b4ca0d1bcfd92aebf33d484e36cbf7a102d007f76b5a0cfea636033a629d601.svg",
- "size": "svg-394",
- "title": "This job requires a manual action",
- "content": "This job depends on a user to trigger its process. Often they are used to deploy code to production environments"
- },
- "favicon": "https://gitlab.com/assets/ci_favicons/favicon_status_manual-829a0804612cef47d9efc1618dba38325483657c847dba0546c3b9f0295bb36c.png",
- "action": {
- "icon": "play",
- "title": "Play",
- "path": "/gitlab-com/gitlab-docs/-/jobs/114982855/play",
- "method": "post",
- "button_title": "Trigger this manual action"
- }
- }
- }
- ]
- }
- ],
- "status": {
- "icon": "status_skipped",
- "text": "skipped",
- "label": "skipped",
- "group": "skipped",
- "tooltip": "skipped",
- "has_details": true,
- "details_path": "/gitlab-com/gitlab-docs/pipelines/34993051#build-images",
- "illustration": null,
- "favicon": "https://gitlab.com/assets/ci_favicons/favicon_status_skipped-0b9c5e543588945e8c4ca57786bbf2d0c56631959c9f853300392d0315be829b.png"
- },
- "path": "/gitlab-com/gitlab-docs/pipelines/34993051#build-images",
- "dropdown_path": "/gitlab-com/gitlab-docs/pipelines/34993051/stage.json?stage=build-images"
- },
- {
- "name": "build",
- "title": "build: failed",
- "groups": [
- {
- "name": "compile_dev",
- "size": 1,
- "status": {
- "icon": "status_failed",
- "text": "failed",
- "label": "failed",
- "group": "failed",
- "tooltip": "failed - (script failure)",
- "has_details": true,
- "details_path": "/gitlab-com/gitlab-docs/-/jobs/114984694",
- "illustration": {
- "image": "https://assets.gitlab-static.net/assets/illustrations/skipped-job_empty-8b877955fbf175e42ae65b6cb95346e15282c6fc5b682756c329af3a0055225e.svg",
- "size": "svg-430",
- "title": "This job does not have a trace."
- },
- "favicon": "https://gitlab.com/assets/ci_favicons/favicon_status_failed-41304d7f7e3828808b0c26771f0309e55296819a9beea3ea9fbf6689d9857c12.png",
- "action": {
- "icon": "retry",
- "title": "Retry",
- "path": "/gitlab-com/gitlab-docs/-/jobs/114984694/retry",
- "method": "post",
- "button_title": "Retry this job"
- }
- },
- "jobs": [
- {
- "id": 1123984694,
- "name": "compile_dev",
- "started": "2018-10-31T16:39:41.598Z",
- "archived": false,
- "build_path": "/gitlab-com/gitlab-docs/-/jobs/114984694",
- "retry_path": "/gitlab-com/gitlab-docs/-/jobs/114984694/retry",
- "playable": false,
- "scheduled": false,
- "created_at": "2018-10-31T16:39:41.138Z",
- "updated_at": "2018-10-31T16:41:40.072Z",
- "status": {
- "icon": "status_failed",
- "text": "failed",
- "label": "failed",
- "group": "failed",
- "tooltip": "failed - (script failure)",
- "has_details": true,
- "details_path": "/gitlab-com/gitlab-docs/-/jobs/114984694",
- "illustration": {
- "image": "https://assets.gitlab-static.net/assets/illustrations/skipped-job_empty-8b877955fbf175e42ae65b6cb95346e15282c6fc5b682756c329af3a0055225e.svg",
- "size": "svg-430",
- "title": "This job does not have a trace."
- },
- "favicon": "https://gitlab.com/assets/ci_favicons/favicon_status_failed-41304d7f7e3828808b0c26771f0309e55296819a9beea3ea9fbf6689d9857c12.png",
- "action": {
- "icon": "retry",
- "title": "Retry",
- "path": "/gitlab-com/gitlab-docs/-/jobs/114984694/retry",
- "method": "post",
- "button_title": "Retry this job"
- }
- },
- "recoverable": false
- }
- ]
- }
- ],
- "status": {
- "icon": "status_failed",
- "text": "failed",
- "label": "failed",
- "group": "failed",
- "tooltip": "failed",
- "has_details": true,
- "details_path": "/gitlab-com/gitlab-docs/pipelines/34993051#build",
- "illustration": null,
- "favicon": "https://gitlab.com/assets/ci_favicons/favicon_status_failed-41304d7f7e3828808b0c26771f0309e55296819a9beea3ea9fbf6689d9857c12.png"
- },
- "path": "/gitlab-com/gitlab-docs/pipelines/34993051#build",
- "dropdown_path": "/gitlab-com/gitlab-docs/pipelines/34993051/stage.json?stage=build"
- },
- {
- "name": "deploy",
- "title": "deploy: skipped",
- "groups": [
- {
- "name": "review",
- "size": 1,
- "status": {
- "icon": "status_skipped",
- "text": "skipped",
- "label": "skipped",
- "group": "skipped",
- "tooltip": "skipped",
- "has_details": true,
- "details_path": "/gitlab-com/gitlab-docs/-/jobs/114982857",
- "illustration": {
- "image": "https://assets.gitlab-static.net/assets/illustrations/skipped-job_empty-8b877955fbf175e42ae65b6cb95346e15282c6fc5b682756c329af3a0055225e.svg",
- "size": "svg-430",
- "title": "This job has been skipped"
- },
- "favicon": "https://gitlab.com/assets/ci_favicons/favicon_status_skipped-0b9c5e543588945e8c4ca57786bbf2d0c56631959c9f853300392d0315be829b.png"
- },
- "jobs": [
- {
- "id": 1143232982857,
- "name": "review",
- "started": null,
- "archived": false,
- "build_path": "/gitlab-com/gitlab-docs/-/jobs/114982857",
- "playable": false,
- "scheduled": false,
- "created_at": "2018-10-31T16:35:23.805Z",
- "updated_at": "2018-10-31T16:41:40.569Z",
- "status": {
- "icon": "status_skipped",
- "text": "skipped",
- "label": "skipped",
- "group": "skipped",
- "tooltip": "skipped",
- "has_details": true,
- "details_path": "/gitlab-com/gitlab-docs/-/jobs/114982857",
- "illustration": {
- "image": "https://assets.gitlab-static.net/assets/illustrations/skipped-job_empty-8b877955fbf175e42ae65b6cb95346e15282c6fc5b682756c329af3a0055225e.svg",
- "size": "svg-430",
- "title": "This job has been skipped"
- },
- "favicon": "https://gitlab.com/assets/ci_favicons/favicon_status_skipped-0b9c5e543588945e8c4ca57786bbf2d0c56631959c9f853300392d0315be829b.png"
- }
- }
- ]
- },
- {
- "name": "review_stop",
- "size": 1,
- "status": {
- "icon": "status_skipped",
- "text": "skipped",
- "label": "skipped",
- "group": "skipped",
- "tooltip": "skipped",
- "has_details": true,
- "details_path": "/gitlab-com/gitlab-docs/-/jobs/114982858",
- "illustration": {
- "image": "https://assets.gitlab-static.net/assets/illustrations/skipped-job_empty-8b877955fbf175e42ae65b6cb95346e15282c6fc5b682756c329af3a0055225e.svg",
- "size": "svg-430",
- "title": "This job has been skipped"
- },
- "favicon": "https://gitlab.com/assets/ci_favicons/favicon_status_skipped-0b9c5e543588945e8c4ca57786bbf2d0c56631959c9f853300392d0315be829b.png"
- },
- "jobs": [
- {
- "id": 114921313182858,
- "name": "review_stop",
- "started": null,
- "archived": false,
- "build_path": "/gitlab-com/gitlab-docs/-/jobs/114982858",
- "playable": false,
- "scheduled": false,
- "created_at": "2018-10-31T16:35:23.840Z",
- "updated_at": "2018-10-31T16:41:40.480Z",
- "status": {
- "icon": "status_skipped",
- "text": "skipped",
- "label": "skipped",
- "group": "skipped",
- "tooltip": "skipped",
- "has_details": true,
- "details_path": "/gitlab-com/gitlab-docs/-/jobs/114982858",
- "illustration": {
- "image": "https://assets.gitlab-static.net/assets/illustrations/skipped-job_empty-8b877955fbf175e42ae65b6cb95346e15282c6fc5b682756c329af3a0055225e.svg",
- "size": "svg-430",
- "title": "This job has been skipped"
- },
- "favicon": "https://gitlab.com/assets/ci_favicons/favicon_status_skipped-0b9c5e543588945e8c4ca57786bbf2d0c56631959c9f853300392d0315be829b.png"
- }
- }
- ]
- }
- ],
- "status": {
- "icon": "status_skipped",
- "text": "skipped",
- "label": "skipped",
- "group": "skipped",
- "tooltip": "skipped",
- "has_details": true,
- "details_path": "/gitlab-com/gitlab-docs/pipelines/34993051#deploy",
- "illustration": null,
- "favicon": "https://gitlab.com/assets/ci_favicons/favicon_status_skipped-0b9c5e543588945e8c4ca57786bbf2d0c56631959c9f853300392d0315be829b.png"
- },
- "path": "/gitlab-com/gitlab-docs/pipelines/34993051#deploy",
- "dropdown_path": "/gitlab-com/gitlab-docs/pipelines/34993051/stage.json?stage=deploy"
- }
- ],
- "artifacts": [],
- "manual_actions": [
- {
- "name": "image:bootstrap",
- "path": "/gitlab-com/gitlab-docs/-/jobs/114982853/play",
- "playable": true,
- "scheduled": false
- },
- {
- "name": "image:builder-onbuild",
- "path": "/gitlab-com/gitlab-docs/-/jobs/114982854/play",
- "playable": true,
- "scheduled": false
- },
- {
- "name": "image:nginx-onbuild",
- "path": "/gitlab-com/gitlab-docs/-/jobs/114982855/play",
- "playable": true,
- "scheduled": false
- },
- {
- "name": "review_stop",
- "path": "/gitlab-com/gitlab-docs/-/jobs/114982858/play",
- "playable": false,
- "scheduled": false
- }
- ],
- "scheduled_actions": []
- },
- "project": {
- "id": 1794617,
- "name": "GitLab Docs",
- "full_path": "/gitlab-com/gitlab-docs",
- "full_name": "GitLab.com / GitLab Docs"
- },
- "triggered": [
- {
- "id": 26,
- "user": null,
- "active": false,
- "coverage": null,
- "source": "push",
- "created_at": "2019-01-06T17:48:37.599Z",
- "updated_at": "2019-01-06T17:48:38.371Z",
- "path": "/h5bp/html5-boilerplate/pipelines/26",
- "flags": {
- "latest": true,
- "stuck": false,
- "auto_devops": false,
- "merge_request": false,
- "yaml_errors": false,
- "retryable": true,
- "cancelable": false,
- "failure_reason": false
- },
- "details": {
- "status": {
- "icon": "status_warning",
- "text": "passed",
- "label": "passed with warnings",
- "group": "success-with-warnings",
- "tooltip": "passed",
- "has_details": true,
- "details_path": "/h5bp/html5-boilerplate/pipelines/26",
- "illustration": null,
- "favicon": "/assets/ci_favicons/favicon_status_success-8451333011eee8ce9f2ab25dc487fe24a8758c694827a582f17f42b0a90446a2.png"
- },
- "duration": null,
- "finished_at": "2019-01-06T17:48:38.370Z",
- "stages": [
- {
- "name": "build",
- "title": "build: passed",
- "groups": [
- {
- "name": "build:linux",
- "size": 1,
- "status": {
- "icon": "status_success",
- "text": "passed",
- "label": "passed",
- "group": "success",
- "tooltip": "passed",
- "has_details": true,
- "details_path": "/h5bp/html5-boilerplate/-/jobs/526",
- "illustration": {
- "image": "/assets/illustrations/skipped-job_empty-8b877955fbf175e42ae65b6cb95346e15282c6fc5b682756c329af3a0055225e.svg",
- "size": "svg-430",
- "title": "This job does not have a trace."
- },
- "favicon": "/assets/ci_favicons/favicon_status_success-8451333011eee8ce9f2ab25dc487fe24a8758c694827a582f17f42b0a90446a2.png",
- "action": {
- "icon": "retry",
- "title": "Retry",
- "path": "/h5bp/html5-boilerplate/-/jobs/526/retry",
- "method": "post",
- "button_title": "Retry this job"
- }
- },
- "jobs": [
- {
- "id": 526,
- "name": "build:linux",
- "started": "2019-01-06T08:48:20.236Z",
- "archived": false,
- "build_path": "/h5bp/html5-boilerplate/-/jobs/526",
- "retry_path": "/h5bp/html5-boilerplate/-/jobs/526/retry",
- "playable": false,
- "scheduled": false,
- "created_at": "2019-01-06T17:48:37.806Z",
- "updated_at": "2019-01-06T17:48:37.806Z",
- "status": {
- "icon": "status_success",
- "text": "passed",
- "label": "passed",
- "group": "success",
- "tooltip": "passed",
- "has_details": true,
- "details_path": "/h5bp/html5-boilerplate/-/jobs/526",
- "illustration": {
- "image": "/assets/illustrations/skipped-job_empty-8b877955fbf175e42ae65b6cb95346e15282c6fc5b682756c329af3a0055225e.svg",
- "size": "svg-430",
- "title": "This job does not have a trace."
- },
- "favicon": "/assets/ci_favicons/favicon_status_success-8451333011eee8ce9f2ab25dc487fe24a8758c694827a582f17f42b0a90446a2.png",
- "action": {
- "icon": "retry",
- "title": "Retry",
- "path": "/h5bp/html5-boilerplate/-/jobs/526/retry",
- "method": "post",
- "button_title": "Retry this job"
- }
- }
- }
- ]
- },
- {
- "name": "build:osx",
- "size": 1,
- "status": {
- "icon": "status_success",
- "text": "passed",
- "label": "passed",
- "group": "success",
- "tooltip": "passed",
- "has_details": true,
- "details_path": "/h5bp/html5-boilerplate/-/jobs/527",
- "illustration": {
- "image": "/assets/illustrations/skipped-job_empty-8b877955fbf175e42ae65b6cb95346e15282c6fc5b682756c329af3a0055225e.svg",
- "size": "svg-430",
- "title": "This job does not have a trace."
- },
- "favicon": "/assets/ci_favicons/favicon_status_success-8451333011eee8ce9f2ab25dc487fe24a8758c694827a582f17f42b0a90446a2.png",
- "action": {
- "icon": "retry",
- "title": "Retry",
- "path": "/h5bp/html5-boilerplate/-/jobs/527/retry",
- "method": "post",
- "button_title": "Retry this job"
- }
- },
- "jobs": [
- {
- "id": 527,
- "name": "build:osx",
- "started": "2019-01-06T07:48:20.237Z",
- "archived": false,
- "build_path": "/h5bp/html5-boilerplate/-/jobs/527",
- "retry_path": "/h5bp/html5-boilerplate/-/jobs/527/retry",
- "playable": false,
- "scheduled": false,
- "created_at": "2019-01-06T17:48:37.846Z",
- "updated_at": "2019-01-06T17:48:37.846Z",
- "status": {
- "icon": "status_success",
- "text": "passed",
- "label": "passed",
- "group": "success",
- "tooltip": "passed",
- "has_details": true,
- "details_path": "/h5bp/html5-boilerplate/-/jobs/527",
- "illustration": {
- "image": "/assets/illustrations/skipped-job_empty-8b877955fbf175e42ae65b6cb95346e15282c6fc5b682756c329af3a0055225e.svg",
- "size": "svg-430",
- "title": "This job does not have a trace."
- },
- "favicon": "/assets/ci_favicons/favicon_status_success-8451333011eee8ce9f2ab25dc487fe24a8758c694827a582f17f42b0a90446a2.png",
- "action": {
- "icon": "retry",
- "title": "Retry",
- "path": "/h5bp/html5-boilerplate/-/jobs/527/retry",
- "method": "post",
- "button_title": "Retry this job"
- }
- }
- }
- ]
- }
- ],
- "status": {
- "icon": "status_success",
- "text": "passed",
- "label": "passed",
- "group": "success",
- "tooltip": "passed",
- "has_details": true,
- "details_path": "/h5bp/html5-boilerplate/pipelines/26#build",
- "illustration": null,
- "favicon": "/assets/ci_favicons/favicon_status_success-8451333011eee8ce9f2ab25dc487fe24a8758c694827a582f17f42b0a90446a2.png"
- },
- "path": "/h5bp/html5-boilerplate/pipelines/26#build",
- "dropdown_path": "/h5bp/html5-boilerplate/pipelines/26/stage.json?stage=build"
- },
- {
- "name": "test",
- "title": "test: passed with warnings",
- "groups": [
- {
- "name": "jenkins",
- "size": 1,
- "status": {
- "icon": "status_success",
- "text": "passed",
- "label": null,
- "group": "success",
- "tooltip": null,
- "has_details": false,
- "details_path": null,
- "illustration": null,
- "favicon": "/assets/ci_favicons/favicon_status_success-8451333011eee8ce9f2ab25dc487fe24a8758c694827a582f17f42b0a90446a2.png"
- },
- "jobs": [
- {
- "id": 546,
- "name": "jenkins",
- "started": "2019-01-06T11:48:20.237Z",
- "archived": false,
- "build_path": "/h5bp/html5-boilerplate/-/jobs/546",
- "playable": false,
- "scheduled": false,
- "created_at": "2019-01-06T17:48:38.359Z",
- "updated_at": "2019-01-06T17:48:38.359Z",
- "status": {
- "icon": "status_success",
- "text": "passed",
- "label": null,
- "group": "success",
- "tooltip": null,
- "has_details": false,
- "details_path": null,
- "illustration": null,
- "favicon": "/assets/ci_favicons/favicon_status_success-8451333011eee8ce9f2ab25dc487fe24a8758c694827a582f17f42b0a90446a2.png"
- }
- }
- ]
- },
- {
- "name": "rspec:linux",
- "size": 3,
- "status": {
- "icon": "status_success",
- "text": "passed",
- "label": "passed",
- "group": "success",
- "tooltip": "passed",
- "has_details": false,
- "details_path": null,
- "illustration": null,
- "favicon": "/assets/ci_favicons/favicon_status_success-8451333011eee8ce9f2ab25dc487fe24a8758c694827a582f17f42b0a90446a2.png"
- },
- "jobs": [
- {
- "id": 528,
- "name": "rspec:linux 0 3",
- "started": "2019-01-06T09:48:20.237Z",
- "archived": false,
- "build_path": "/h5bp/html5-boilerplate/-/jobs/528",
- "retry_path": "/h5bp/html5-boilerplate/-/jobs/528/retry",
- "playable": false,
- "scheduled": false,
- "created_at": "2019-01-06T17:48:37.885Z",
- "updated_at": "2019-01-06T17:48:37.885Z",
- "status": {
- "icon": "status_success",
- "text": "passed",
- "label": "passed",
- "group": "success",
- "tooltip": "passed",
- "has_details": true,
- "details_path": "/h5bp/html5-boilerplate/-/jobs/528",
- "illustration": {
- "image": "/assets/illustrations/skipped-job_empty-8b877955fbf175e42ae65b6cb95346e15282c6fc5b682756c329af3a0055225e.svg",
- "size": "svg-430",
- "title": "This job does not have a trace."
- },
- "favicon": "/assets/ci_favicons/favicon_status_success-8451333011eee8ce9f2ab25dc487fe24a8758c694827a582f17f42b0a90446a2.png",
- "action": {
- "icon": "retry",
- "title": "Retry",
- "path": "/h5bp/html5-boilerplate/-/jobs/528/retry",
- "method": "post",
- "button_title": "Retry this job"
- }
- }
- },
- {
- "id": 529,
- "name": "rspec:linux 1 3",
- "started": "2019-01-06T09:48:20.237Z",
- "archived": false,
- "build_path": "/h5bp/html5-boilerplate/-/jobs/529",
- "retry_path": "/h5bp/html5-boilerplate/-/jobs/529/retry",
- "playable": false,
- "scheduled": false,
- "created_at": "2019-01-06T17:48:37.907Z",
- "updated_at": "2019-01-06T17:48:37.907Z",
- "status": {
- "icon": "status_success",
- "text": "passed",
- "label": "passed",
- "group": "success",
- "tooltip": "passed",
- "has_details": true,
- "details_path": "/h5bp/html5-boilerplate/-/jobs/529",
- "illustration": {
- "image": "/assets/illustrations/skipped-job_empty-8b877955fbf175e42ae65b6cb95346e15282c6fc5b682756c329af3a0055225e.svg",
- "size": "svg-430",
- "title": "This job does not have a trace."
- },
- "favicon": "/assets/ci_favicons/favicon_status_success-8451333011eee8ce9f2ab25dc487fe24a8758c694827a582f17f42b0a90446a2.png",
- "action": {
- "icon": "retry",
- "title": "Retry",
- "path": "/h5bp/html5-boilerplate/-/jobs/529/retry",
- "method": "post",
- "button_title": "Retry this job"
- }
- }
- },
- {
- "id": 530,
- "name": "rspec:linux 2 3",
- "started": "2019-01-06T09:48:20.237Z",
- "archived": false,
- "build_path": "/h5bp/html5-boilerplate/-/jobs/530",
- "retry_path": "/h5bp/html5-boilerplate/-/jobs/530/retry",
- "playable": false,
- "scheduled": false,
- "created_at": "2019-01-06T17:48:37.927Z",
- "updated_at": "2019-01-06T17:48:37.927Z",
- "status": {
- "icon": "status_success",
- "text": "passed",
- "label": "passed",
- "group": "success",
- "tooltip": "passed",
- "has_details": true,
- "details_path": "/h5bp/html5-boilerplate/-/jobs/530",
- "illustration": {
- "image": "/assets/illustrations/skipped-job_empty-8b877955fbf175e42ae65b6cb95346e15282c6fc5b682756c329af3a0055225e.svg",
- "size": "svg-430",
- "title": "This job does not have a trace."
- },
- "favicon": "/assets/ci_favicons/favicon_status_success-8451333011eee8ce9f2ab25dc487fe24a8758c694827a582f17f42b0a90446a2.png",
- "action": {
- "icon": "retry",
- "title": "Retry",
- "path": "/h5bp/html5-boilerplate/-/jobs/530/retry",
- "method": "post",
- "button_title": "Retry this job"
- }
- }
- }
- ]
- },
- {
- "name": "rspec:osx",
- "size": 1,
- "status": {
- "icon": "status_success",
- "text": "passed",
- "label": "passed",
- "group": "success",
- "tooltip": "passed",
- "has_details": true,
- "details_path": "/h5bp/html5-boilerplate/-/jobs/535",
- "illustration": {
- "image": "/assets/illustrations/skipped-job_empty-8b877955fbf175e42ae65b6cb95346e15282c6fc5b682756c329af3a0055225e.svg",
- "size": "svg-430",
- "title": "This job does not have a trace."
- },
- "favicon": "/assets/ci_favicons/favicon_status_success-8451333011eee8ce9f2ab25dc487fe24a8758c694827a582f17f42b0a90446a2.png",
- "action": {
- "icon": "retry",
- "title": "Retry",
- "path": "/h5bp/html5-boilerplate/-/jobs/535/retry",
- "method": "post",
- "button_title": "Retry this job"
- }
- },
- "jobs": [
- {
- "id": 535,
- "name": "rspec:osx",
- "started": "2019-01-06T09:48:20.237Z",
- "archived": false,
- "build_path": "/h5bp/html5-boilerplate/-/jobs/535",
- "retry_path": "/h5bp/html5-boilerplate/-/jobs/535/retry",
- "playable": false,
- "scheduled": false,
- "created_at": "2019-01-06T17:48:38.018Z",
- "updated_at": "2019-01-06T17:48:38.018Z",
- "status": {
- "icon": "status_success",
- "text": "passed",
- "label": "passed",
- "group": "success",
- "tooltip": "passed",
- "has_details": true,
- "details_path": "/h5bp/html5-boilerplate/-/jobs/535",
- "illustration": {
- "image": "/assets/illustrations/skipped-job_empty-8b877955fbf175e42ae65b6cb95346e15282c6fc5b682756c329af3a0055225e.svg",
- "size": "svg-430",
- "title": "This job does not have a trace."
- },
- "favicon": "/assets/ci_favicons/favicon_status_success-8451333011eee8ce9f2ab25dc487fe24a8758c694827a582f17f42b0a90446a2.png",
- "action": {
- "icon": "retry",
- "title": "Retry",
- "path": "/h5bp/html5-boilerplate/-/jobs/535/retry",
- "method": "post",
- "button_title": "Retry this job"
- }
- }
- }
- ]
- },
- {
- "name": "rspec:windows",
- "size": 3,
- "status": {
- "icon": "status_success",
- "text": "passed",
- "label": "passed",
- "group": "success",
- "tooltip": "passed",
- "has_details": false,
- "details_path": null,
- "illustration": null,
- "favicon": "/assets/ci_favicons/favicon_status_success-8451333011eee8ce9f2ab25dc487fe24a8758c694827a582f17f42b0a90446a2.png"
- },
- "jobs": [
- {
- "id": 531,
- "name": "rspec:windows 0 3",
- "started": "2019-01-06T09:48:20.237Z",
- "archived": false,
- "build_path": "/h5bp/html5-boilerplate/-/jobs/531",
- "retry_path": "/h5bp/html5-boilerplate/-/jobs/531/retry",
- "playable": false,
- "scheduled": false,
- "created_at": "2019-01-06T17:48:37.944Z",
- "updated_at": "2019-01-06T17:48:37.944Z",
- "status": {
- "icon": "status_success",
- "text": "passed",
- "label": "passed",
- "group": "success",
- "tooltip": "passed",
- "has_details": true,
- "details_path": "/h5bp/html5-boilerplate/-/jobs/531",
- "illustration": {
- "image": "/assets/illustrations/skipped-job_empty-8b877955fbf175e42ae65b6cb95346e15282c6fc5b682756c329af3a0055225e.svg",
- "size": "svg-430",
- "title": "This job does not have a trace."
- },
- "favicon": "/assets/ci_favicons/favicon_status_success-8451333011eee8ce9f2ab25dc487fe24a8758c694827a582f17f42b0a90446a2.png",
- "action": {
- "icon": "retry",
- "title": "Retry",
- "path": "/h5bp/html5-boilerplate/-/jobs/531/retry",
- "method": "post",
- "button_title": "Retry this job"
- }
- }
- },
- {
- "id": 532,
- "name": "rspec:windows 1 3",
- "started": "2019-01-06T09:48:20.237Z",
- "archived": false,
- "build_path": "/h5bp/html5-boilerplate/-/jobs/532",
- "retry_path": "/h5bp/html5-boilerplate/-/jobs/532/retry",
- "playable": false,
- "scheduled": false,
- "created_at": "2019-01-06T17:48:37.962Z",
- "updated_at": "2019-01-06T17:48:37.962Z",
- "status": {
- "icon": "status_success",
- "text": "passed",
- "label": "passed",
- "group": "success",
- "tooltip": "passed",
- "has_details": true,
- "details_path": "/h5bp/html5-boilerplate/-/jobs/532",
- "illustration": {
- "image": "/assets/illustrations/skipped-job_empty-8b877955fbf175e42ae65b6cb95346e15282c6fc5b682756c329af3a0055225e.svg",
- "size": "svg-430",
- "title": "This job does not have a trace."
- },
- "favicon": "/assets/ci_favicons/favicon_status_success-8451333011eee8ce9f2ab25dc487fe24a8758c694827a582f17f42b0a90446a2.png",
- "action": {
- "icon": "retry",
- "title": "Retry",
- "path": "/h5bp/html5-boilerplate/-/jobs/532/retry",
- "method": "post",
- "button_title": "Retry this job"
- }
- }
- },
- {
- "id": 534,
- "name": "rspec:windows 2 3",
- "started": "2019-01-06T09:48:20.237Z",
- "archived": false,
- "build_path": "/h5bp/html5-boilerplate/-/jobs/534",
- "retry_path": "/h5bp/html5-boilerplate/-/jobs/534/retry",
- "playable": false,
- "scheduled": false,
- "created_at": "2019-01-06T17:48:37.999Z",
- "updated_at": "2019-01-06T17:48:37.999Z",
- "status": {
- "icon": "status_success",
- "text": "passed",
- "label": "passed",
- "group": "success",
- "tooltip": "passed",
- "has_details": true,
- "details_path": "/h5bp/html5-boilerplate/-/jobs/534",
- "illustration": {
- "image": "/assets/illustrations/skipped-job_empty-8b877955fbf175e42ae65b6cb95346e15282c6fc5b682756c329af3a0055225e.svg",
- "size": "svg-430",
- "title": "This job does not have a trace."
- },
- "favicon": "/assets/ci_favicons/favicon_status_success-8451333011eee8ce9f2ab25dc487fe24a8758c694827a582f17f42b0a90446a2.png",
- "action": {
- "icon": "retry",
- "title": "Retry",
- "path": "/h5bp/html5-boilerplate/-/jobs/534/retry",
- "method": "post",
- "button_title": "Retry this job"
- }
- }
- }
- ]
- },
- {
- "name": "spinach:linux",
- "size": 1,
- "status": {
- "icon": "status_success",
- "text": "passed",
- "label": "passed",
- "group": "success",
- "tooltip": "passed",
- "has_details": true,
- "details_path": "/h5bp/html5-boilerplate/-/jobs/536",
- "illustration": {
- "image": "/assets/illustrations/skipped-job_empty-8b877955fbf175e42ae65b6cb95346e15282c6fc5b682756c329af3a0055225e.svg",
- "size": "svg-430",
- "title": "This job does not have a trace."
- },
- "favicon": "/assets/ci_favicons/favicon_status_success-8451333011eee8ce9f2ab25dc487fe24a8758c694827a582f17f42b0a90446a2.png",
- "action": {
- "icon": "retry",
- "title": "Retry",
- "path": "/h5bp/html5-boilerplate/-/jobs/536/retry",
- "method": "post",
- "button_title": "Retry this job"
- }
- },
- "jobs": [
- {
- "id": 536,
- "name": "spinach:linux",
- "started": "2019-01-06T09:48:20.237Z",
- "archived": false,
- "build_path": "/h5bp/html5-boilerplate/-/jobs/536",
- "retry_path": "/h5bp/html5-boilerplate/-/jobs/536/retry",
- "playable": false,
- "scheduled": false,
- "created_at": "2019-01-06T17:48:38.050Z",
- "updated_at": "2019-01-06T17:48:38.050Z",
- "status": {
- "icon": "status_success",
- "text": "passed",
- "label": "passed",
- "group": "success",
- "tooltip": "passed",
- "has_details": true,
- "details_path": "/h5bp/html5-boilerplate/-/jobs/536",
- "illustration": {
- "image": "/assets/illustrations/skipped-job_empty-8b877955fbf175e42ae65b6cb95346e15282c6fc5b682756c329af3a0055225e.svg",
- "size": "svg-430",
- "title": "This job does not have a trace."
- },
- "favicon": "/assets/ci_favicons/favicon_status_success-8451333011eee8ce9f2ab25dc487fe24a8758c694827a582f17f42b0a90446a2.png",
- "action": {
- "icon": "retry",
- "title": "Retry",
- "path": "/h5bp/html5-boilerplate/-/jobs/536/retry",
- "method": "post",
- "button_title": "Retry this job"
- }
- }
- }
- ]
- },
- {
- "name": "spinach:osx",
- "size": 1,
- "status": {
- "icon": "status_warning",
- "text": "failed",
- "label": "failed (allowed to fail)",
- "group": "failed-with-warnings",
- "tooltip": "failed - (unknown failure) (allowed to fail)",
- "has_details": true,
- "details_path": "/h5bp/html5-boilerplate/-/jobs/537",
- "illustration": {
- "image": "/assets/illustrations/skipped-job_empty-8b877955fbf175e42ae65b6cb95346e15282c6fc5b682756c329af3a0055225e.svg",
- "size": "svg-430",
- "title": "This job does not have a trace."
- },
- "favicon": "/assets/ci_favicons/favicon_status_failed-41304d7f7e3828808b0c26771f0309e55296819a9beea3ea9fbf6689d9857c12.png",
- "action": {
- "icon": "retry",
- "title": "Retry",
- "path": "/h5bp/html5-boilerplate/-/jobs/537/retry",
- "method": "post",
- "button_title": "Retry this job"
- }
- },
- "jobs": [
- {
- "id": 537,
- "name": "spinach:osx",
- "started": "2019-01-06T09:48:20.237Z",
- "archived": false,
- "build_path": "/h5bp/html5-boilerplate/-/jobs/537",
- "retry_path": "/h5bp/html5-boilerplate/-/jobs/537/retry",
- "playable": false,
- "scheduled": false,
- "created_at": "2019-01-06T17:48:38.069Z",
- "updated_at": "2019-01-06T17:48:38.069Z",
- "status": {
- "icon": "status_warning",
- "text": "failed",
- "label": "failed (allowed to fail)",
- "group": "failed-with-warnings",
- "tooltip": "failed - (unknown failure) (allowed to fail)",
- "has_details": true,
- "details_path": "/h5bp/html5-boilerplate/-/jobs/537",
- "illustration": {
- "image": "/assets/illustrations/skipped-job_empty-8b877955fbf175e42ae65b6cb95346e15282c6fc5b682756c329af3a0055225e.svg",
- "size": "svg-430",
- "title": "This job does not have a trace."
- },
- "favicon": "/assets/ci_favicons/favicon_status_failed-41304d7f7e3828808b0c26771f0309e55296819a9beea3ea9fbf6689d9857c12.png",
- "action": {
- "icon": "retry",
- "title": "Retry",
- "path": "/h5bp/html5-boilerplate/-/jobs/537/retry",
- "method": "post",
- "button_title": "Retry this job"
- }
- },
- "callout_message": "There is an unknown failure, please try again",
- "recoverable": true
- }
- ]
- }
- ],
- "status": {
- "icon": "status_warning",
- "text": "passed",
- "label": "passed with warnings",
- "group": "success-with-warnings",
- "tooltip": "passed",
- "has_details": true,
- "details_path": "/h5bp/html5-boilerplate/pipelines/26#test",
- "illustration": null,
- "favicon": "/assets/ci_favicons/favicon_status_success-8451333011eee8ce9f2ab25dc487fe24a8758c694827a582f17f42b0a90446a2.png"
- },
- "path": "/h5bp/html5-boilerplate/pipelines/26#test",
- "dropdown_path": "/h5bp/html5-boilerplate/pipelines/26/stage.json?stage=test"
- },
- {
- "name": "security",
- "title": "security: passed",
- "groups": [
- {
- "name": "container_scanning",
- "size": 1,
- "status": {
- "icon": "status_success",
- "text": "passed",
- "label": "passed",
- "group": "success",
- "tooltip": "passed",
- "has_details": true,
- "details_path": "/h5bp/html5-boilerplate/-/jobs/541",
- "illustration": {
- "image": "/assets/illustrations/skipped-job_empty-8b877955fbf175e42ae65b6cb95346e15282c6fc5b682756c329af3a0055225e.svg",
- "size": "svg-430",
- "title": "This job does not have a trace."
- },
- "favicon": "/assets/ci_favicons/favicon_status_success-8451333011eee8ce9f2ab25dc487fe24a8758c694827a582f17f42b0a90446a2.png",
- "action": {
- "icon": "retry",
- "title": "Retry",
- "path": "/h5bp/html5-boilerplate/-/jobs/541/retry",
- "method": "post",
- "button_title": "Retry this job"
- }
- },
- "jobs": [
- {
- "id": 541,
- "name": "container_scanning",
- "started": "2019-01-06T09:48:20.237Z",
- "archived": false,
- "build_path": "/h5bp/html5-boilerplate/-/jobs/541",
- "retry_path": "/h5bp/html5-boilerplate/-/jobs/541/retry",
- "playable": false,
- "scheduled": false,
- "created_at": "2019-01-06T17:48:38.186Z",
- "updated_at": "2019-01-06T17:48:38.186Z",
- "status": {
- "icon": "status_success",
- "text": "passed",
- "label": "passed",
- "group": "success",
- "tooltip": "passed",
- "has_details": true,
- "details_path": "/h5bp/html5-boilerplate/-/jobs/541",
- "illustration": {
- "image": "/assets/illustrations/skipped-job_empty-8b877955fbf175e42ae65b6cb95346e15282c6fc5b682756c329af3a0055225e.svg",
- "size": "svg-430",
- "title": "This job does not have a trace."
- },
- "favicon": "/assets/ci_favicons/favicon_status_success-8451333011eee8ce9f2ab25dc487fe24a8758c694827a582f17f42b0a90446a2.png",
- "action": {
- "icon": "retry",
- "title": "Retry",
- "path": "/h5bp/html5-boilerplate/-/jobs/541/retry",
- "method": "post",
- "button_title": "Retry this job"
- }
- }
- }
- ]
- },
- {
- "name": "dast",
- "size": 1,
- "status": {
- "icon": "status_success",
- "text": "passed",
- "label": "passed",
- "group": "success",
- "tooltip": "passed",
- "has_details": true,
- "details_path": "/h5bp/html5-boilerplate/-/jobs/538",
- "illustration": {
- "image": "/assets/illustrations/skipped-job_empty-8b877955fbf175e42ae65b6cb95346e15282c6fc5b682756c329af3a0055225e.svg",
- "size": "svg-430",
- "title": "This job does not have a trace."
- },
- "favicon": "/assets/ci_favicons/favicon_status_success-8451333011eee8ce9f2ab25dc487fe24a8758c694827a582f17f42b0a90446a2.png",
- "action": {
- "icon": "retry",
- "title": "Retry",
- "path": "/h5bp/html5-boilerplate/-/jobs/538/retry",
- "method": "post",
- "button_title": "Retry this job"
- }
- },
- "jobs": [
- {
- "id": 538,
- "name": "dast",
- "started": "2019-01-06T09:48:20.237Z",
- "archived": false,
- "build_path": "/h5bp/html5-boilerplate/-/jobs/538",
- "retry_path": "/h5bp/html5-boilerplate/-/jobs/538/retry",
- "playable": false,
- "scheduled": false,
- "created_at": "2019-01-06T17:48:38.087Z",
- "updated_at": "2019-01-06T17:48:38.087Z",
- "status": {
- "icon": "status_success",
- "text": "passed",
- "label": "passed",
- "group": "success",
- "tooltip": "passed",
- "has_details": true,
- "details_path": "/h5bp/html5-boilerplate/-/jobs/538",
- "illustration": {
- "image": "/assets/illustrations/skipped-job_empty-8b877955fbf175e42ae65b6cb95346e15282c6fc5b682756c329af3a0055225e.svg",
- "size": "svg-430",
- "title": "This job does not have a trace."
- },
- "favicon": "/assets/ci_favicons/favicon_status_success-8451333011eee8ce9f2ab25dc487fe24a8758c694827a582f17f42b0a90446a2.png",
- "action": {
- "icon": "retry",
- "title": "Retry",
- "path": "/h5bp/html5-boilerplate/-/jobs/538/retry",
- "method": "post",
- "button_title": "Retry this job"
- }
- }
- }
- ]
- },
- {
- "name": "dependency_scanning",
- "size": 1,
- "status": {
- "icon": "status_success",
- "text": "passed",
- "label": "passed",
- "group": "success",
- "tooltip": "passed",
- "has_details": true,
- "details_path": "/h5bp/html5-boilerplate/-/jobs/540",
- "illustration": {
- "image": "/assets/illustrations/skipped-job_empty-8b877955fbf175e42ae65b6cb95346e15282c6fc5b682756c329af3a0055225e.svg",
- "size": "svg-430",
- "title": "This job does not have a trace."
- },
- "favicon": "/assets/ci_favicons/favicon_status_success-8451333011eee8ce9f2ab25dc487fe24a8758c694827a582f17f42b0a90446a2.png",
- "action": {
- "icon": "retry",
- "title": "Retry",
- "path": "/h5bp/html5-boilerplate/-/jobs/540/retry",
- "method": "post",
- "button_title": "Retry this job"
- }
- },
- "jobs": [
- {
- "id": 540,
- "name": "dependency_scanning",
- "started": "2019-01-06T09:48:20.237Z",
- "archived": false,
- "build_path": "/h5bp/html5-boilerplate/-/jobs/540",
- "retry_path": "/h5bp/html5-boilerplate/-/jobs/540/retry",
- "playable": false,
- "scheduled": false,
- "created_at": "2019-01-06T17:48:38.153Z",
- "updated_at": "2019-01-06T17:48:38.153Z",
- "status": {
- "icon": "status_success",
- "text": "passed",
- "label": "passed",
- "group": "success",
- "tooltip": "passed",
- "has_details": true,
- "details_path": "/h5bp/html5-boilerplate/-/jobs/540",
- "illustration": {
- "image": "/assets/illustrations/skipped-job_empty-8b877955fbf175e42ae65b6cb95346e15282c6fc5b682756c329af3a0055225e.svg",
- "size": "svg-430",
- "title": "This job does not have a trace."
- },
- "favicon": "/assets/ci_favicons/favicon_status_success-8451333011eee8ce9f2ab25dc487fe24a8758c694827a582f17f42b0a90446a2.png",
- "action": {
- "icon": "retry",
- "title": "Retry",
- "path": "/h5bp/html5-boilerplate/-/jobs/540/retry",
- "method": "post",
- "button_title": "Retry this job"
- }
- }
- }
- ]
- },
- {
- "name": "sast",
- "size": 1,
- "status": {
- "icon": "status_success",
- "text": "passed",
- "label": "passed",
- "group": "success",
- "tooltip": "passed",
- "has_details": true,
- "details_path": "/h5bp/html5-boilerplate/-/jobs/539",
- "illustration": {
- "image": "/assets/illustrations/skipped-job_empty-8b877955fbf175e42ae65b6cb95346e15282c6fc5b682756c329af3a0055225e.svg",
- "size": "svg-430",
- "title": "This job does not have a trace."
- },
- "favicon": "/assets/ci_favicons/favicon_status_success-8451333011eee8ce9f2ab25dc487fe24a8758c694827a582f17f42b0a90446a2.png",
- "action": {
- "icon": "retry",
- "title": "Retry",
- "path": "/h5bp/html5-boilerplate/-/jobs/539/retry",
- "method": "post",
- "button_title": "Retry this job"
- }
- },
- "jobs": [
- {
- "id": 539,
- "name": "sast",
- "started": "2019-01-06T09:48:20.237Z",
- "archived": false,
- "build_path": "/h5bp/html5-boilerplate/-/jobs/539",
- "retry_path": "/h5bp/html5-boilerplate/-/jobs/539/retry",
- "playable": false,
- "scheduled": false,
- "created_at": "2019-01-06T17:48:38.121Z",
- "updated_at": "2019-01-06T17:48:38.121Z",
- "status": {
- "icon": "status_success",
- "text": "passed",
- "label": "passed",
- "group": "success",
- "tooltip": "passed",
- "has_details": true,
- "details_path": "/h5bp/html5-boilerplate/-/jobs/539",
- "illustration": {
- "image": "/assets/illustrations/skipped-job_empty-8b877955fbf175e42ae65b6cb95346e15282c6fc5b682756c329af3a0055225e.svg",
- "size": "svg-430",
- "title": "This job does not have a trace."
- },
- "favicon": "/assets/ci_favicons/favicon_status_success-8451333011eee8ce9f2ab25dc487fe24a8758c694827a582f17f42b0a90446a2.png",
- "action": {
- "icon": "retry",
- "title": "Retry",
- "path": "/h5bp/html5-boilerplate/-/jobs/539/retry",
- "method": "post",
- "button_title": "Retry this job"
- }
- }
- }
- ]
- }
- ],
- "status": {
- "icon": "status_success",
- "text": "passed",
- "label": "passed",
- "group": "success",
- "tooltip": "passed",
- "has_details": true,
- "details_path": "/h5bp/html5-boilerplate/pipelines/26#security",
- "illustration": null,
- "favicon": "/assets/ci_favicons/favicon_status_success-8451333011eee8ce9f2ab25dc487fe24a8758c694827a582f17f42b0a90446a2.png"
- },
- "path": "/h5bp/html5-boilerplate/pipelines/26#security",
- "dropdown_path": "/h5bp/html5-boilerplate/pipelines/26/stage.json?stage=security"
- },
- {
- "name": "deploy",
- "title": "deploy: passed",
- "groups": [
- {
- "name": "production",
- "size": 1,
- "status": {
- "icon": "status_skipped",
- "text": "skipped",
- "label": "skipped",
- "group": "skipped",
- "tooltip": "skipped",
- "has_details": true,
- "details_path": "/h5bp/html5-boilerplate/-/jobs/544",
- "illustration": {
- "image": "/assets/illustrations/skipped-job_empty-8b877955fbf175e42ae65b6cb95346e15282c6fc5b682756c329af3a0055225e.svg",
- "size": "svg-430",
- "title": "This job has been skipped"
- },
- "favicon": "/assets/ci_favicons/favicon_status_skipped-0b9c5e543588945e8c4ca57786bbf2d0c56631959c9f853300392d0315be829b.png"
- },
- "jobs": [
- {
- "id": 544,
- "name": "production",
- "started": null,
- "archived": false,
- "build_path": "/h5bp/html5-boilerplate/-/jobs/544",
- "playable": false,
- "scheduled": false,
- "created_at": "2019-01-06T17:48:38.313Z",
- "updated_at": "2019-01-06T17:48:38.313Z",
- "status": {
- "icon": "status_skipped",
- "text": "skipped",
- "label": "skipped",
- "group": "skipped",
- "tooltip": "skipped",
- "has_details": true,
- "details_path": "/h5bp/html5-boilerplate/-/jobs/544",
- "illustration": {
- "image": "/assets/illustrations/skipped-job_empty-8b877955fbf175e42ae65b6cb95346e15282c6fc5b682756c329af3a0055225e.svg",
- "size": "svg-430",
- "title": "This job has been skipped"
- },
- "favicon": "/assets/ci_favicons/favicon_status_skipped-0b9c5e543588945e8c4ca57786bbf2d0c56631959c9f853300392d0315be829b.png"
- }
- }
- ]
- },
- {
- "name": "staging",
- "size": 1,
- "status": {
- "icon": "status_success",
- "text": "passed",
- "label": "passed",
- "group": "success",
- "tooltip": "passed",
- "has_details": true,
- "details_path": "/h5bp/html5-boilerplate/-/jobs/542",
- "illustration": {
- "image": "/assets/illustrations/skipped-job_empty-8b877955fbf175e42ae65b6cb95346e15282c6fc5b682756c329af3a0055225e.svg",
- "size": "svg-430",
- "title": "This job does not have a trace."
- },
- "favicon": "/assets/ci_favicons/favicon_status_success-8451333011eee8ce9f2ab25dc487fe24a8758c694827a582f17f42b0a90446a2.png",
- "action": {
- "icon": "retry",
- "title": "Retry",
- "path": "/h5bp/html5-boilerplate/-/jobs/542/retry",
- "method": "post",
- "button_title": "Retry this job"
- }
- },
- "jobs": [
- {
- "id": 542,
- "name": "staging",
- "started": "2019-01-06T11:48:20.237Z",
- "archived": false,
- "build_path": "/h5bp/html5-boilerplate/-/jobs/542",
- "retry_path": "/h5bp/html5-boilerplate/-/jobs/542/retry",
- "playable": false,
- "scheduled": false,
- "created_at": "2019-01-06T17:48:38.219Z",
- "updated_at": "2019-01-06T17:48:38.219Z",
- "status": {
- "icon": "status_success",
- "text": "passed",
- "label": "passed",
- "group": "success",
- "tooltip": "passed",
- "has_details": true,
- "details_path": "/h5bp/html5-boilerplate/-/jobs/542",
- "illustration": {
- "image": "/assets/illustrations/skipped-job_empty-8b877955fbf175e42ae65b6cb95346e15282c6fc5b682756c329af3a0055225e.svg",
- "size": "svg-430",
- "title": "This job does not have a trace."
- },
- "favicon": "/assets/ci_favicons/favicon_status_success-8451333011eee8ce9f2ab25dc487fe24a8758c694827a582f17f42b0a90446a2.png",
- "action": {
- "icon": "retry",
- "title": "Retry",
- "path": "/h5bp/html5-boilerplate/-/jobs/542/retry",
- "method": "post",
- "button_title": "Retry this job"
- }
- }
- }
- ]
- },
- {
- "name": "stop staging",
- "size": 1,
- "status": {
- "icon": "status_skipped",
- "text": "skipped",
- "label": "skipped",
- "group": "skipped",
- "tooltip": "skipped",
- "has_details": true,
- "details_path": "/h5bp/html5-boilerplate/-/jobs/543",
- "illustration": {
- "image": "/assets/illustrations/skipped-job_empty-8b877955fbf175e42ae65b6cb95346e15282c6fc5b682756c329af3a0055225e.svg",
- "size": "svg-430",
- "title": "This job has been skipped"
- },
- "favicon": "/assets/ci_favicons/favicon_status_skipped-0b9c5e543588945e8c4ca57786bbf2d0c56631959c9f853300392d0315be829b.png"
- },
- "jobs": [
- {
- "id": 543,
- "name": "stop staging",
- "started": null,
- "archived": false,
- "build_path": "/h5bp/html5-boilerplate/-/jobs/543",
- "playable": false,
- "scheduled": false,
- "created_at": "2019-01-06T17:48:38.283Z",
- "updated_at": "2019-01-06T17:48:38.283Z",
- "status": {
- "icon": "status_skipped",
- "text": "skipped",
- "label": "skipped",
- "group": "skipped",
- "tooltip": "skipped",
- "has_details": true,
- "details_path": "/h5bp/html5-boilerplate/-/jobs/543",
- "illustration": {
- "image": "/assets/illustrations/skipped-job_empty-8b877955fbf175e42ae65b6cb95346e15282c6fc5b682756c329af3a0055225e.svg",
- "size": "svg-430",
- "title": "This job has been skipped"
- },
- "favicon": "/assets/ci_favicons/favicon_status_skipped-0b9c5e543588945e8c4ca57786bbf2d0c56631959c9f853300392d0315be829b.png"
- }
- }
- ]
- }
- ],
- "status": {
- "icon": "status_success",
- "text": "passed",
- "label": "passed",
- "group": "success",
- "tooltip": "passed",
- "has_details": true,
- "details_path": "/h5bp/html5-boilerplate/pipelines/26#deploy",
- "illustration": null,
- "favicon": "/assets/ci_favicons/favicon_status_success-8451333011eee8ce9f2ab25dc487fe24a8758c694827a582f17f42b0a90446a2.png"
- },
- "path": "/h5bp/html5-boilerplate/pipelines/26#deploy",
- "dropdown_path": "/h5bp/html5-boilerplate/pipelines/26/stage.json?stage=deploy"
- },
- {
- "name": "notify",
- "title": "notify: passed",
- "groups": [
- {
- "name": "slack",
- "size": 1,
- "status": {
- "icon": "status_success",
- "text": "passed",
- "label": "manual play action",
- "group": "success",
- "tooltip": "passed",
- "has_details": true,
- "details_path": "/h5bp/html5-boilerplate/-/jobs/545",
- "illustration": {
- "image": "/assets/illustrations/manual_action-2b4ca0d1bcfd92aebf33d484e36cbf7a102d007f76b5a0cfea636033a629d601.svg",
- "size": "svg-394",
- "title": "This job requires a manual action",
- "content": "This job depends on a user to trigger its process. Often they are used to deploy code to production environments"
- },
- "favicon": "/assets/ci_favicons/favicon_status_success-8451333011eee8ce9f2ab25dc487fe24a8758c694827a582f17f42b0a90446a2.png",
- "action": {
- "icon": "play",
- "title": "Play",
- "path": "/h5bp/html5-boilerplate/-/jobs/545/play",
- "method": "post",
- "button_title": "Trigger this manual action"
- }
- },
- "jobs": [
- {
- "id": 545,
- "name": "slack",
- "started": null,
- "archived": false,
- "build_path": "/h5bp/html5-boilerplate/-/jobs/545",
- "retry_path": "/h5bp/html5-boilerplate/-/jobs/545/retry",
- "play_path": "/h5bp/html5-boilerplate/-/jobs/545/play",
- "playable": true,
- "scheduled": false,
- "created_at": "2019-01-06T17:48:38.341Z",
- "updated_at": "2019-01-06T17:48:38.341Z",
- "status": {
- "icon": "status_success",
- "text": "passed",
- "label": "manual play action",
- "group": "success",
- "tooltip": "passed",
- "has_details": true,
- "details_path": "/h5bp/html5-boilerplate/-/jobs/545",
- "illustration": {
- "image": "/assets/illustrations/manual_action-2b4ca0d1bcfd92aebf33d484e36cbf7a102d007f76b5a0cfea636033a629d601.svg",
- "size": "svg-394",
- "title": "This job requires a manual action",
- "content": "This job depends on a user to trigger its process. Often they are used to deploy code to production environments"
- },
- "favicon": "/assets/ci_favicons/favicon_status_success-8451333011eee8ce9f2ab25dc487fe24a8758c694827a582f17f42b0a90446a2.png",
- "action": {
- "icon": "play",
- "title": "Play",
- "path": "/h5bp/html5-boilerplate/-/jobs/545/play",
- "method": "post",
- "button_title": "Trigger this manual action"
- }
- }
- }
- ]
- }
- ],
- "status": {
- "icon": "status_success",
- "text": "passed",
- "label": "passed",
- "group": "success",
- "tooltip": "passed",
- "has_details": true,
- "details_path": "/h5bp/html5-boilerplate/pipelines/26#notify",
- "illustration": null,
- "favicon": "/assets/ci_favicons/favicon_status_success-8451333011eee8ce9f2ab25dc487fe24a8758c694827a582f17f42b0a90446a2.png"
- },
- "path": "/h5bp/html5-boilerplate/pipelines/26#notify",
- "dropdown_path": "/h5bp/html5-boilerplate/pipelines/26/stage.json?stage=notify"
- }
- ],
- "artifacts": [
- {
- "name": "build:linux",
- "expired": null,
- "expire_at": null,
- "path": "/h5bp/html5-boilerplate/-/jobs/526/artifacts/download",
- "browse_path": "/h5bp/html5-boilerplate/-/jobs/526/artifacts/browse"
- },
- {
- "name": "build:osx",
- "expired": null,
- "expire_at": null,
- "path": "/h5bp/html5-boilerplate/-/jobs/527/artifacts/download",
- "browse_path": "/h5bp/html5-boilerplate/-/jobs/527/artifacts/browse"
- }
- ],
- "manual_actions": [
- {
- "name": "stop staging",
- "path": "/h5bp/html5-boilerplate/-/jobs/543/play",
- "playable": false,
- "scheduled": false
- },
- {
- "name": "production",
- "path": "/h5bp/html5-boilerplate/-/jobs/544/play",
- "playable": false,
- "scheduled": false
- },
- {
- "name": "slack",
- "path": "/h5bp/html5-boilerplate/-/jobs/545/play",
- "playable": true,
- "scheduled": false
- }
- ],
- "scheduled_actions": []
- },
- "ref": {
- "name": "master",
- "path": "/h5bp/html5-boilerplate/commits/master",
- "tag": false,
- "branch": true,
- "merge_request": false
- },
- "commit": {
- "id": "bad98c453eab56d20057f3929989251d45cd1a8b",
- "short_id": "bad98c45",
- "title": "remove instances of shrink-to-fit=no (#2103)",
- "created_at": "2018-12-17T20:52:18.000Z",
- "parent_ids": ["49130f6cfe9ff1f749015d735649a2bc6f66cf3a"],
- "message": "remove instances of shrink-to-fit=no (#2103)\n\ncloses #2102\r\n\r\nPer my findings, the need for it as a default was rectified with the release of iOS 9.3, where the viewport no longer shrunk to accommodate overflow, as was introduced in iOS 9.",
- "author_name": "Scott O'Hara",
- "author_email": "scottaohara@users.noreply.github.com",
- "authored_date": "2018-12-17T20:52:18.000Z",
- "committer_name": "Rob Larsen",
- "committer_email": "rob@drunkenfist.com",
- "committed_date": "2018-12-17T20:52:18.000Z",
- "author": null,
- "author_gravatar_url": "https://www.gravatar.com/avatar/6d597df7cf998d16cbe00ccac063b31e?s=80\u0026d=identicon",
- "commit_url": "http://localhost:3001/h5bp/html5-boilerplate/commit/bad98c453eab56d20057f3929989251d45cd1a8b",
- "commit_path": "/h5bp/html5-boilerplate/commit/bad98c453eab56d20057f3929989251d45cd1a8b"
- },
- "retry_path": "/h5bp/html5-boilerplate/pipelines/26/retry",
- "triggered_by": {
- "id": 4,
- "user": null,
- "active": false,
- "coverage": null,
- "source": "push",
- "path": "/gitlab-org/gitlab-test/pipelines/4",
- "details": {
- "status": {
- "icon": "status_warning",
- "text": "passed",
- "label": "passed with warnings",
- "group": "success-with-warnings",
- "tooltip": "passed",
- "has_details": true,
- "details_path": "/gitlab-org/gitlab-test/pipelines/4",
- "illustration": null,
- "favicon": "/assets/ci_favicons/favicon_status_success-8451333011eee8ce9f2ab25dc487fe24a8758c694827a582f17f42b0a90446a2.png"
- }
- },
- "project": {
- "id": 1,
- "name": "Gitlab Test",
- "full_path": "/gitlab-org/gitlab-test",
- "full_name": "Gitlab Org / Gitlab Test"
- }
- },
- "triggered": [],
- "project": {
- "id": 1794617,
- "name": "GitLab Docs",
- "full_path": "/gitlab-com/gitlab-docs",
- "full_name": "GitLab.com / GitLab Docs"
- }
- }
- ]
- }
- ]
-}
diff --git a/spec/javascripts/pipelines/mock_data.js b/spec/javascripts/pipelines/mock_data.js
deleted file mode 100644
index f876987cd88..00000000000
--- a/spec/javascripts/pipelines/mock_data.js
+++ /dev/null
@@ -1,423 +0,0 @@
-export const pipelineWithStages = {
- id: 20333396,
- user: {
- id: 128633,
- name: 'Rémy Coutable',
- username: 'rymai',
- state: 'active',
- avatar_url:
- 'https://secure.gravatar.com/avatar/263da227929cc0035cb0eba512bcf81a?s=80\u0026d=identicon',
- web_url: 'https://gitlab.com/rymai',
- path: '/rymai',
- },
- active: true,
- coverage: '58.24',
- source: 'push',
- created_at: '2018-04-11T14:04:53.881Z',
- updated_at: '2018-04-11T14:05:00.792Z',
- path: '/gitlab-org/gitlab/pipelines/20333396',
- flags: {
- latest: true,
- stuck: false,
- auto_devops: false,
- yaml_errors: false,
- retryable: false,
- cancelable: true,
- failure_reason: false,
- },
- details: {
- status: {
- icon: 'status_running',
- text: 'running',
- label: 'running',
- group: 'running',
- has_details: true,
- details_path: '/gitlab-org/gitlab/pipelines/20333396',
- favicon:
- 'https://assets.gitlab-static.net/assets/ci_favicons/favicon_status_running-2eb56be2871937954b2ba6d6f4ee9fdf7e5e1c146ac45f7be98119ccaca1aca9.ico',
- },
- duration: null,
- finished_at: null,
- stages: [
- {
- name: 'build',
- title: 'build: skipped',
- status: {
- icon: 'status_skipped',
- text: 'skipped',
- label: 'skipped',
- group: 'skipped',
- has_details: true,
- details_path: '/gitlab-org/gitlab/pipelines/20333396#build',
- favicon:
- 'https://assets.gitlab-static.net/assets/ci_favicons/favicon_status_skipped-a2eee568a5bffdb494050c7b62dde241de9189280836288ac8923d369f16222d.ico',
- },
- path: '/gitlab-org/gitlab/pipelines/20333396#build',
- dropdown_path: '/gitlab-org/gitlab/pipelines/20333396/stage.json?stage=build',
- },
- {
- name: 'prepare',
- title: 'prepare: passed',
- status: {
- icon: 'status_success',
- text: 'passed',
- label: 'passed',
- group: 'success',
- has_details: true,
- details_path: '/gitlab-org/gitlab/pipelines/20333396#prepare',
- favicon:
- 'https://assets.gitlab-static.net/assets/ci_favicons/favicon_status_success-26f59841becbef8c6fe414e9e74471d8bfd6a91b5855c19fe7f5923a40a7da47.ico',
- },
- path: '/gitlab-org/gitlab/pipelines/20333396#prepare',
- dropdown_path: '/gitlab-org/gitlab/pipelines/20333396/stage.json?stage=prepare',
- },
- {
- name: 'test',
- title: 'test: running',
- status: {
- icon: 'status_running',
- text: 'running',
- label: 'running',
- group: 'running',
- has_details: true,
- details_path: '/gitlab-org/gitlab/pipelines/20333396#test',
- favicon:
- 'https://assets.gitlab-static.net/assets/ci_favicons/favicon_status_running-2eb56be2871937954b2ba6d6f4ee9fdf7e5e1c146ac45f7be98119ccaca1aca9.ico',
- },
- path: '/gitlab-org/gitlab/pipelines/20333396#test',
- dropdown_path: '/gitlab-org/gitlab/pipelines/20333396/stage.json?stage=test',
- },
- {
- name: 'post-test',
- title: 'post-test: created',
- status: {
- icon: 'status_created',
- text: 'created',
- label: 'created',
- group: 'created',
- has_details: true,
- details_path: '/gitlab-org/gitlab/pipelines/20333396#post-test',
- favicon:
- 'https://assets.gitlab-static.net/assets/ci_favicons/favicon_status_created-e997aa0b7db73165df8a9d6803932b18d7b7cc37d604d2d96e378fea2dba9c5f.ico',
- },
- path: '/gitlab-org/gitlab/pipelines/20333396#post-test',
- dropdown_path: '/gitlab-org/gitlab/pipelines/20333396/stage.json?stage=post-test',
- },
- {
- name: 'pages',
- title: 'pages: created',
- status: {
- icon: 'status_created',
- text: 'created',
- label: 'created',
- group: 'created',
- has_details: true,
- details_path: '/gitlab-org/gitlab/pipelines/20333396#pages',
- favicon:
- 'https://assets.gitlab-static.net/assets/ci_favicons/favicon_status_created-e997aa0b7db73165df8a9d6803932b18d7b7cc37d604d2d96e378fea2dba9c5f.ico',
- },
- path: '/gitlab-org/gitlab/pipelines/20333396#pages',
- dropdown_path: '/gitlab-org/gitlab/pipelines/20333396/stage.json?stage=pages',
- },
- {
- name: 'post-cleanup',
- title: 'post-cleanup: created',
- status: {
- icon: 'status_created',
- text: 'created',
- label: 'created',
- group: 'created',
- has_details: true,
- details_path: '/gitlab-org/gitlab/pipelines/20333396#post-cleanup',
- favicon:
- 'https://assets.gitlab-static.net/assets/ci_favicons/favicon_status_created-e997aa0b7db73165df8a9d6803932b18d7b7cc37d604d2d96e378fea2dba9c5f.ico',
- },
- path: '/gitlab-org/gitlab/pipelines/20333396#post-cleanup',
- dropdown_path: '/gitlab-org/gitlab/pipelines/20333396/stage.json?stage=post-cleanup',
- },
- ],
- artifacts: [
- {
- name: 'gitlab:assets:compile',
- expired: false,
- expire_at: '2018-05-12T14:22:54.730Z',
- path: '/gitlab-org/gitlab/-/jobs/62411438/artifacts/download',
- keep_path: '/gitlab-org/gitlab/-/jobs/62411438/artifacts/keep',
- browse_path: '/gitlab-org/gitlab/-/jobs/62411438/artifacts/browse',
- },
- {
- name: 'rspec-mysql 12 28',
- expired: false,
- expire_at: '2018-05-12T14:22:45.136Z',
- path: '/gitlab-org/gitlab/-/jobs/62411397/artifacts/download',
- keep_path: '/gitlab-org/gitlab/-/jobs/62411397/artifacts/keep',
- browse_path: '/gitlab-org/gitlab/-/jobs/62411397/artifacts/browse',
- },
- {
- name: 'rspec-mysql 6 28',
- expired: false,
- expire_at: '2018-05-12T14:22:41.523Z',
- path: '/gitlab-org/gitlab/-/jobs/62411391/artifacts/download',
- keep_path: '/gitlab-org/gitlab/-/jobs/62411391/artifacts/keep',
- browse_path: '/gitlab-org/gitlab/-/jobs/62411391/artifacts/browse',
- },
- {
- name: 'rspec-pg geo 0 1',
- expired: false,
- expire_at: '2018-05-12T14:22:13.287Z',
- path: '/gitlab-org/gitlab/-/jobs/62411353/artifacts/download',
- keep_path: '/gitlab-org/gitlab/-/jobs/62411353/artifacts/keep',
- browse_path: '/gitlab-org/gitlab/-/jobs/62411353/artifacts/browse',
- },
- {
- name: 'rspec-mysql 0 28',
- expired: false,
- expire_at: '2018-05-12T14:22:06.834Z',
- path: '/gitlab-org/gitlab/-/jobs/62411385/artifacts/download',
- keep_path: '/gitlab-org/gitlab/-/jobs/62411385/artifacts/keep',
- browse_path: '/gitlab-org/gitlab/-/jobs/62411385/artifacts/browse',
- },
- {
- name: 'spinach-mysql 0 2',
- expired: false,
- expire_at: '2018-05-12T14:21:51.409Z',
- path: '/gitlab-org/gitlab/-/jobs/62411423/artifacts/download',
- keep_path: '/gitlab-org/gitlab/-/jobs/62411423/artifacts/keep',
- browse_path: '/gitlab-org/gitlab/-/jobs/62411423/artifacts/browse',
- },
- {
- name: 'karma',
- expired: false,
- expire_at: '2018-05-12T14:21:20.934Z',
- path: '/gitlab-org/gitlab/-/jobs/62411440/artifacts/download',
- keep_path: '/gitlab-org/gitlab/-/jobs/62411440/artifacts/keep',
- browse_path: '/gitlab-org/gitlab/-/jobs/62411440/artifacts/browse',
- },
- {
- name: 'spinach-pg 0 2',
- expired: false,
- expire_at: '2018-05-12T14:20:01.028Z',
- path: '/gitlab-org/gitlab/-/jobs/62411419/artifacts/download',
- keep_path: '/gitlab-org/gitlab/-/jobs/62411419/artifacts/keep',
- browse_path: '/gitlab-org/gitlab/-/jobs/62411419/artifacts/browse',
- },
- {
- name: 'spinach-pg 1 2',
- expired: false,
- expire_at: '2018-05-12T14:19:04.336Z',
- path: '/gitlab-org/gitlab/-/jobs/62411421/artifacts/download',
- keep_path: '/gitlab-org/gitlab/-/jobs/62411421/artifacts/keep',
- browse_path: '/gitlab-org/gitlab/-/jobs/62411421/artifacts/browse',
- },
- {
- name: 'sast',
- expired: null,
- expire_at: null,
- path: '/gitlab-org/gitlab/-/jobs/62411442/artifacts/download',
- browse_path: '/gitlab-org/gitlab/-/jobs/62411442/artifacts/browse',
- },
- {
- name: 'code_quality',
- expired: false,
- expire_at: '2018-04-18T14:16:24.484Z',
- path: '/gitlab-org/gitlab/-/jobs/62411441/artifacts/download',
- keep_path: '/gitlab-org/gitlab/-/jobs/62411441/artifacts/keep',
- browse_path: '/gitlab-org/gitlab/-/jobs/62411441/artifacts/browse',
- },
- {
- name: 'cache gems',
- expired: null,
- expire_at: null,
- path: '/gitlab-org/gitlab/-/jobs/62411447/artifacts/download',
- browse_path: '/gitlab-org/gitlab/-/jobs/62411447/artifacts/browse',
- },
- {
- name: 'dependency_scanning',
- expired: null,
- expire_at: null,
- path: '/gitlab-org/gitlab/-/jobs/62411443/artifacts/download',
- browse_path: '/gitlab-org/gitlab/-/jobs/62411443/artifacts/browse',
- },
- {
- name: 'compile-assets',
- expired: false,
- expire_at: '2018-04-18T14:12:07.638Z',
- path: '/gitlab-org/gitlab/-/jobs/62411334/artifacts/download',
- keep_path: '/gitlab-org/gitlab/-/jobs/62411334/artifacts/keep',
- browse_path: '/gitlab-org/gitlab/-/jobs/62411334/artifacts/browse',
- },
- {
- name: 'setup-test-env',
- expired: false,
- expire_at: '2018-04-18T14:10:27.024Z',
- path: '/gitlab-org/gitlab/-/jobs/62411336/artifacts/download',
- keep_path: '/gitlab-org/gitlab/-/jobs/62411336/artifacts/keep',
- browse_path: '/gitlab-org/gitlab/-/jobs/62411336/artifacts/browse',
- },
- {
- name: 'retrieve-tests-metadata',
- expired: false,
- expire_at: '2018-05-12T14:06:35.926Z',
- path: '/gitlab-org/gitlab/-/jobs/62411333/artifacts/download',
- keep_path: '/gitlab-org/gitlab/-/jobs/62411333/artifacts/keep',
- browse_path: '/gitlab-org/gitlab/-/jobs/62411333/artifacts/browse',
- },
- ],
- manual_actions: [
- {
- name: 'package-and-qa',
- path: '/gitlab-org/gitlab/-/jobs/62411330/play',
- playable: true,
- },
- {
- name: 'review-docs-deploy',
- path: '/gitlab-org/gitlab/-/jobs/62411332/play',
- playable: true,
- },
- ],
- },
- ref: {
- name: 'master',
- path: '/gitlab-org/gitlab/commits/master',
- tag: false,
- branch: true,
- },
- commit: {
- id: 'e6a2885c503825792cb8a84a8731295e361bd059',
- short_id: 'e6a2885c',
- title: "Merge branch 'ce-to-ee-2018-04-11' into 'master'",
- created_at: '2018-04-11T14:04:39.000Z',
- parent_ids: [
- '5d9b5118f6055f72cff1a82b88133609912f2c1d',
- '6fdc6ee76a8062fe41b1a33f7c503334a6ebdc02',
- ],
- message:
- "Merge branch 'ce-to-ee-2018-04-11' into 'master'\n\nCE upstream - 2018-04-11 12:26 UTC\n\nSee merge request gitlab-org/gitlab-ee!5326",
- author_name: 'Rémy Coutable',
- author_email: 'remy@rymai.me',
- authored_date: '2018-04-11T14:04:39.000Z',
- committer_name: 'Rémy Coutable',
- committer_email: 'remy@rymai.me',
- committed_date: '2018-04-11T14:04:39.000Z',
- author: {
- id: 128633,
- name: 'Rémy Coutable',
- username: 'rymai',
- state: 'active',
- avatar_url:
- 'https://secure.gravatar.com/avatar/263da227929cc0035cb0eba512bcf81a?s=80\u0026d=identicon',
- web_url: 'https://gitlab.com/rymai',
- path: '/rymai',
- },
- author_gravatar_url:
- 'https://secure.gravatar.com/avatar/263da227929cc0035cb0eba512bcf81a?s=80\u0026d=identicon',
- commit_url:
- 'https://gitlab.com/gitlab-org/gitlab/commit/e6a2885c503825792cb8a84a8731295e361bd059',
- commit_path: '/gitlab-org/gitlab/commit/e6a2885c503825792cb8a84a8731295e361bd059',
- },
- cancel_path: '/gitlab-org/gitlab/pipelines/20333396/cancel',
- triggered_by: null,
- triggered: [],
-};
-
-export const stageReply = {
- name: 'deploy',
- title: 'deploy: running',
- latest_statuses: [
- {
- id: 928,
- name: 'stop staging',
- started: false,
- build_path: '/twitter/flight/-/jobs/928',
- cancel_path: '/twitter/flight/-/jobs/928/cancel',
- playable: false,
- created_at: '2018-04-04T20:02:02.728Z',
- updated_at: '2018-04-04T20:02:02.766Z',
- status: {
- icon: 'status_pending',
- text: 'pending',
- label: 'pending',
- group: 'pending',
- tooltip: 'pending',
- has_details: true,
- details_path: '/twitter/flight/-/jobs/928',
- favicon:
- '/assets/ci_favicons/dev/favicon_status_pending-db32e1faf94b9f89530ac519790920d1f18ea8f6af6cd2e0a26cd6840cacf101.ico',
- action: {
- icon: 'cancel',
- title: 'Cancel',
- path: '/twitter/flight/-/jobs/928/cancel',
- method: 'post',
- },
- },
- },
- {
- id: 926,
- name: 'production',
- started: false,
- build_path: '/twitter/flight/-/jobs/926',
- retry_path: '/twitter/flight/-/jobs/926/retry',
- play_path: '/twitter/flight/-/jobs/926/play',
- playable: true,
- created_at: '2018-04-04T20:00:57.202Z',
- updated_at: '2018-04-04T20:11:13.110Z',
- status: {
- icon: 'status_canceled',
- text: 'canceled',
- label: 'manual play action',
- group: 'canceled',
- tooltip: 'canceled',
- has_details: true,
- details_path: '/twitter/flight/-/jobs/926',
- favicon:
- '/assets/ci_favicons/dev/favicon_status_canceled-5491840b9b6feafba0bc599cbd49ee9580321dc809683856cf1b0d51532b1af6.ico',
- action: {
- icon: 'play',
- title: 'Play',
- path: '/twitter/flight/-/jobs/926/play',
- method: 'post',
- },
- },
- },
- {
- id: 217,
- name: 'staging',
- started: '2018-03-07T08:41:46.234Z',
- build_path: '/twitter/flight/-/jobs/217',
- retry_path: '/twitter/flight/-/jobs/217/retry',
- playable: false,
- created_at: '2018-03-07T14:41:58.093Z',
- updated_at: '2018-03-07T14:41:58.093Z',
- status: {
- icon: 'status_success',
- text: 'passed',
- label: 'passed',
- group: 'success',
- tooltip: 'passed',
- has_details: true,
- details_path: '/twitter/flight/-/jobs/217',
- favicon:
- '/assets/ci_favicons/dev/favicon_status_success-308b4fc054cdd1b68d0865e6cfb7b02e92e3472f201507418f8eddb74ac11a59.ico',
- action: {
- icon: 'retry',
- title: 'Retry',
- path: '/twitter/flight/-/jobs/217/retry',
- method: 'post',
- },
- },
- },
- ],
- status: {
- icon: 'status_running',
- text: 'running',
- label: 'running',
- group: 'running',
- tooltip: 'running',
- has_details: true,
- details_path: '/twitter/flight/pipelines/13#deploy',
- favicon:
- '/assets/ci_favicons/dev/favicon_status_running-c3ad2fc53ea6079c174e5b6c1351ff349e99ec3af5a5622fb77b0fe53ea279c1.ico',
- },
- path: '/twitter/flight/pipelines/13#deploy',
- dropdown_path: '/twitter/flight/pipelines/13/stage.json?stage=deploy',
-};
diff --git a/spec/javascripts/pipelines/pipeline_details_mediator_spec.js b/spec/javascripts/pipelines/pipeline_details_mediator_spec.js
deleted file mode 100644
index 61ee2dc13ca..00000000000
--- a/spec/javascripts/pipelines/pipeline_details_mediator_spec.js
+++ /dev/null
@@ -1,36 +0,0 @@
-import MockAdapter from 'axios-mock-adapter';
-import axios from '~/lib/utils/axios_utils';
-import PipelineMediator from '~/pipelines/pipeline_details_mediator';
-
-describe('PipelineMdediator', () => {
- let mediator;
- let mock;
-
- beforeEach(() => {
- mock = new MockAdapter(axios);
- mediator = new PipelineMediator({ endpoint: 'foo.json' });
- });
-
- afterEach(() => {
- mock.restore();
- });
-
- it('should set defaults', () => {
- expect(mediator.options).toEqual({ endpoint: 'foo.json' });
- expect(mediator.state.isLoading).toEqual(false);
- expect(mediator.store).toBeDefined();
- expect(mediator.service).toBeDefined();
- });
-
- describe('request and store data', () => {
- it('should store received data', done => {
- mock.onGet('foo.json').reply(200, { id: '121123' });
- mediator.fetchPipeline();
-
- setTimeout(() => {
- expect(mediator.store.state.pipeline).toEqual({ id: '121123' });
- done();
- }, 0);
- });
- });
-});
diff --git a/spec/javascripts/pipelines/pipelines_actions_spec.js b/spec/javascripts/pipelines/pipelines_actions_spec.js
deleted file mode 100644
index 91f7d2167cc..00000000000
--- a/spec/javascripts/pipelines/pipelines_actions_spec.js
+++ /dev/null
@@ -1,128 +0,0 @@
-import Vue from 'vue';
-import MockAdapter from 'axios-mock-adapter';
-import mountComponent from 'spec/helpers/vue_mount_component_helper';
-import { TEST_HOST } from 'spec/test_constants';
-import axios from '~/lib/utils/axios_utils';
-import PipelinesActions from '~/pipelines/components/pipelines_actions.vue';
-
-describe('Pipelines Actions dropdown', () => {
- const Component = Vue.extend(PipelinesActions);
- let vm;
- let mock;
-
- afterEach(() => {
- vm.$destroy();
- mock.restore();
- });
-
- beforeEach(() => {
- mock = new MockAdapter(axios);
- });
-
- describe('manual actions', () => {
- const actions = [
- {
- name: 'stop_review',
- path: `${TEST_HOST}/root/review-app/builds/1893/play`,
- },
- {
- name: 'foo',
- path: `${TEST_HOST}/disabled/pipeline/action`,
- playable: false,
- },
- ];
-
- beforeEach(() => {
- vm = mountComponent(Component, { actions });
- });
-
- it('renders a dropdown with the provided actions', () => {
- const dropdownItems = vm.$el.querySelectorAll('.dropdown-menu li');
-
- expect(dropdownItems.length).toEqual(actions.length);
- });
-
- it("renders a disabled action when it's not playable", () => {
- const dropdownItem = vm.$el.querySelector('.dropdown-menu li:last-child button');
-
- expect(dropdownItem).toBeDisabled();
- });
-
- describe('on click', () => {
- it('makes a request and toggles the loading state', done => {
- mock.onPost(actions.path).reply(200);
-
- vm.$el.querySelector('.dropdown-menu li button').click();
-
- expect(vm.isLoading).toEqual(true);
-
- setTimeout(() => {
- expect(vm.isLoading).toEqual(false);
-
- done();
- });
- });
- });
- });
-
- describe('scheduled jobs', () => {
- const scheduledJobAction = {
- name: 'scheduled action',
- path: `${TEST_HOST}/scheduled/job/action`,
- playable: true,
- scheduled_at: '2063-04-05T00:42:00Z',
- };
- const expiredJobAction = {
- name: 'expired action',
- path: `${TEST_HOST}/expired/job/action`,
- playable: true,
- scheduled_at: '2018-10-05T08:23:00Z',
- };
- const findDropdownItem = action => {
- const buttons = vm.$el.querySelectorAll('.dropdown-menu li button');
- return Array.prototype.find.call(buttons, element =>
- element.innerText.trim().startsWith(action.name),
- );
- };
-
- beforeEach(done => {
- spyOn(Date, 'now').and.callFake(() => new Date('2063-04-04T00:42:00Z').getTime());
- vm = mountComponent(Component, { actions: [scheduledJobAction, expiredJobAction] });
-
- Vue.nextTick()
- .then(done)
- .catch(done.fail);
- });
-
- it('makes post request after confirming', done => {
- mock.onPost(scheduledJobAction.path).reply(200);
- spyOn(window, 'confirm').and.callFake(() => true);
-
- findDropdownItem(scheduledJobAction).click();
-
- expect(window.confirm).toHaveBeenCalled();
- setTimeout(() => {
- expect(mock.history.post.length).toBe(1);
- done();
- });
- });
-
- it('does not make post request if confirmation is cancelled', () => {
- mock.onPost(scheduledJobAction.path).reply(200);
- spyOn(window, 'confirm').and.callFake(() => false);
-
- findDropdownItem(scheduledJobAction).click();
-
- expect(window.confirm).toHaveBeenCalled();
- expect(mock.history.post.length).toBe(0);
- });
-
- it('displays the remaining time in the dropdown', () => {
- expect(findDropdownItem(scheduledJobAction)).toContainText('24:00:00');
- });
-
- it('displays 00:00:00 for expired jobs in the dropdown', () => {
- expect(findDropdownItem(expiredJobAction)).toContainText('00:00:00');
- });
- });
-});
diff --git a/spec/javascripts/pipelines/pipelines_artifacts_spec.js b/spec/javascripts/pipelines/pipelines_artifacts_spec.js
deleted file mode 100644
index 7705d5a19bf..00000000000
--- a/spec/javascripts/pipelines/pipelines_artifacts_spec.js
+++ /dev/null
@@ -1,38 +0,0 @@
-import Vue from 'vue';
-import artifactsComp from '~/pipelines/components/pipelines_artifacts.vue';
-
-describe('Pipelines Artifacts dropdown', () => {
- let component;
- let artifacts;
-
- beforeEach(() => {
- const ArtifactsComponent = Vue.extend(artifactsComp);
-
- artifacts = [
- {
- name: 'artifact',
- path: '/download/path',
- },
- ];
-
- component = new ArtifactsComponent({
- propsData: {
- artifacts,
- },
- }).$mount();
- });
-
- it('should render a dropdown with the provided artifacts', () => {
- expect(component.$el.querySelectorAll('.dropdown-menu li').length).toEqual(artifacts.length);
- });
-
- it('should render a link with the provided path', () => {
- expect(component.$el.querySelector('.dropdown-menu li a').getAttribute('href')).toEqual(
- artifacts[0].path,
- );
-
- expect(component.$el.querySelector('.dropdown-menu li a').textContent).toContain(
- artifacts[0].name,
- );
- });
-});
diff --git a/spec/javascripts/pipelines/pipelines_spec.js b/spec/javascripts/pipelines/pipelines_spec.js
deleted file mode 100644
index 5cd91413c5f..00000000000
--- a/spec/javascripts/pipelines/pipelines_spec.js
+++ /dev/null
@@ -1,783 +0,0 @@
-import Vue from 'vue';
-import MockAdapter from 'axios-mock-adapter';
-import mountComponent from 'spec/helpers/vue_mount_component_helper';
-import axios from '~/lib/utils/axios_utils';
-import pipelinesComp from '~/pipelines/components/pipelines.vue';
-import Store from '~/pipelines/stores/pipelines_store';
-import { pipelineWithStages, stageReply } from './mock_data';
-
-describe('Pipelines', () => {
- const jsonFixtureName = 'pipelines/pipelines.json';
-
- preloadFixtures(jsonFixtureName);
-
- let PipelinesComponent;
- let pipelines;
- let vm;
- let mock;
-
- const paths = {
- endpoint: 'twitter/flight/pipelines.json',
- autoDevopsPath: '/help/topics/autodevops/index.md',
- helpPagePath: '/help/ci/quick_start/README',
- emptyStateSvgPath: '/assets/illustrations/pipelines_empty.svg',
- errorStateSvgPath: '/assets/illustrations/pipelines_failed.svg',
- noPipelinesSvgPath: '/assets/illustrations/pipelines_pending.svg',
- ciLintPath: '/ci/lint',
- resetCachePath: '/twitter/flight/settings/ci_cd/reset_cache',
- newPipelinePath: '/twitter/flight/pipelines/new',
- };
-
- const noPermissions = {
- endpoint: 'twitter/flight/pipelines.json',
- autoDevopsPath: '/help/topics/autodevops/index.md',
- helpPagePath: '/help/ci/quick_start/README',
- emptyStateSvgPath: '/assets/illustrations/pipelines_empty.svg',
- errorStateSvgPath: '/assets/illustrations/pipelines_failed.svg',
- noPipelinesSvgPath: '/assets/illustrations/pipelines_pending.svg',
- };
-
- beforeEach(() => {
- mock = new MockAdapter(axios);
-
- pipelines = getJSONFixture(jsonFixtureName);
-
- PipelinesComponent = Vue.extend(pipelinesComp);
- });
-
- afterEach(() => {
- vm.$destroy();
- mock.restore();
- });
-
- describe('With permission', () => {
- describe('With pipelines in main tab', () => {
- beforeEach(done => {
- mock.onGet('twitter/flight/pipelines.json').reply(200, pipelines);
-
- vm = mountComponent(PipelinesComponent, {
- store: new Store(),
- hasGitlabCi: true,
- canCreatePipeline: true,
- ...paths,
- });
-
- setTimeout(() => {
- done();
- });
- });
-
- it('renders tabs', () => {
- expect(vm.$el.querySelector('.js-pipelines-tab-all').textContent.trim()).toContain('All');
- });
-
- it('renders Run Pipeline link', () => {
- expect(vm.$el.querySelector('.js-run-pipeline').getAttribute('href')).toEqual(
- paths.newPipelinePath,
- );
- });
-
- it('renders CI Lint link', () => {
- expect(vm.$el.querySelector('.js-ci-lint').getAttribute('href')).toEqual(paths.ciLintPath);
- });
-
- it('renders Clear Runner Cache button', () => {
- expect(vm.$el.querySelector('.js-clear-cache').textContent.trim()).toEqual(
- 'Clear Runner Caches',
- );
- });
-
- it('renders pipelines table', () => {
- expect(vm.$el.querySelectorAll('.gl-responsive-table-row').length).toEqual(
- pipelines.pipelines.length + 1,
- );
- });
- });
-
- describe('Without pipelines on main tab with CI', () => {
- beforeEach(done => {
- mock.onGet('twitter/flight/pipelines.json').reply(200, {
- pipelines: [],
- count: {
- all: 0,
- pending: 0,
- running: 0,
- finished: 0,
- },
- });
- vm = mountComponent(PipelinesComponent, {
- store: new Store(),
- hasGitlabCi: true,
- canCreatePipeline: true,
- ...paths,
- });
-
- setTimeout(() => {
- done();
- });
- });
-
- it('renders tabs', () => {
- expect(vm.$el.querySelector('.js-pipelines-tab-all').textContent.trim()).toContain('All');
- });
-
- it('renders Run Pipeline link', () => {
- expect(vm.$el.querySelector('.js-run-pipeline').getAttribute('href')).toEqual(
- paths.newPipelinePath,
- );
- });
-
- it('renders CI Lint link', () => {
- expect(vm.$el.querySelector('.js-ci-lint').getAttribute('href')).toEqual(paths.ciLintPath);
- });
-
- it('renders Clear Runner Cache button', () => {
- expect(vm.$el.querySelector('.js-clear-cache').textContent.trim()).toEqual(
- 'Clear Runner Caches',
- );
- });
-
- it('renders tab empty state', () => {
- expect(vm.$el.querySelector('.empty-state h4').textContent.trim()).toEqual(
- 'There are currently no pipelines.',
- );
- });
- });
-
- describe('Without pipelines nor CI', () => {
- beforeEach(done => {
- mock.onGet('twitter/flight/pipelines.json').reply(200, {
- pipelines: [],
- count: {
- all: 0,
- pending: 0,
- running: 0,
- finished: 0,
- },
- });
- vm = mountComponent(PipelinesComponent, {
- store: new Store(),
- hasGitlabCi: false,
- canCreatePipeline: true,
- ...paths,
- });
-
- setTimeout(() => {
- done();
- });
- });
-
- it('renders empty state', () => {
- expect(vm.$el.querySelector('.js-empty-state h4').textContent.trim()).toEqual(
- 'Build with confidence',
- );
-
- expect(vm.$el.querySelector('.js-get-started-pipelines').getAttribute('href')).toEqual(
- paths.helpPagePath,
- );
- });
-
- it('does not render tabs nor buttons', () => {
- expect(vm.$el.querySelector('.js-pipelines-tab-all')).toBeNull();
- expect(vm.$el.querySelector('.js-run-pipeline')).toBeNull();
- expect(vm.$el.querySelector('.js-ci-lint')).toBeNull();
- expect(vm.$el.querySelector('.js-clear-cache')).toBeNull();
- });
- });
-
- describe('When API returns error', () => {
- beforeEach(done => {
- mock.onGet('twitter/flight/pipelines.json').reply(500, {});
- vm = mountComponent(PipelinesComponent, {
- store: new Store(),
- hasGitlabCi: false,
- canCreatePipeline: true,
- ...paths,
- });
-
- setTimeout(() => {
- done();
- });
- });
-
- it('renders tabs', () => {
- expect(vm.$el.querySelector('.js-pipelines-tab-all').textContent.trim()).toContain('All');
- });
-
- it('renders buttons', () => {
- expect(vm.$el.querySelector('.js-run-pipeline').getAttribute('href')).toEqual(
- paths.newPipelinePath,
- );
-
- expect(vm.$el.querySelector('.js-ci-lint').getAttribute('href')).toEqual(paths.ciLintPath);
- expect(vm.$el.querySelector('.js-clear-cache').textContent.trim()).toEqual(
- 'Clear Runner Caches',
- );
- });
-
- it('renders error state', () => {
- expect(vm.$el.querySelector('.empty-state').textContent.trim()).toContain(
- 'There was an error fetching the pipelines.',
- );
- });
- });
- });
-
- describe('Without permission', () => {
- describe('With pipelines in main tab', () => {
- beforeEach(done => {
- mock.onGet('twitter/flight/pipelines.json').reply(200, pipelines);
-
- vm = mountComponent(PipelinesComponent, {
- store: new Store(),
- hasGitlabCi: false,
- canCreatePipeline: false,
- ...noPermissions,
- });
-
- setTimeout(() => {
- done();
- });
- });
-
- it('renders tabs', () => {
- expect(vm.$el.querySelector('.js-pipelines-tab-all').textContent.trim()).toContain('All');
- });
-
- it('does not render buttons', () => {
- expect(vm.$el.querySelector('.js-run-pipeline')).toBeNull();
- expect(vm.$el.querySelector('.js-ci-lint')).toBeNull();
- expect(vm.$el.querySelector('.js-clear-cache')).toBeNull();
- });
-
- it('renders pipelines table', () => {
- expect(vm.$el.querySelectorAll('.gl-responsive-table-row').length).toEqual(
- pipelines.pipelines.length + 1,
- );
- });
- });
-
- describe('Without pipelines on main tab with CI', () => {
- beforeEach(done => {
- mock.onGet('twitter/flight/pipelines.json').reply(200, {
- pipelines: [],
- count: {
- all: 0,
- pending: 0,
- running: 0,
- finished: 0,
- },
- });
-
- vm = mountComponent(PipelinesComponent, {
- store: new Store(),
- hasGitlabCi: true,
- canCreatePipeline: false,
- ...noPermissions,
- });
-
- setTimeout(() => {
- done();
- });
- });
-
- it('renders tabs', () => {
- expect(vm.$el.querySelector('.js-pipelines-tab-all').textContent.trim()).toContain('All');
- });
-
- it('does not render buttons', () => {
- expect(vm.$el.querySelector('.js-run-pipeline')).toBeNull();
- expect(vm.$el.querySelector('.js-ci-lint')).toBeNull();
- expect(vm.$el.querySelector('.js-clear-cache')).toBeNull();
- });
-
- it('renders tab empty state', () => {
- expect(vm.$el.querySelector('.empty-state h4').textContent.trim()).toEqual(
- 'There are currently no pipelines.',
- );
- });
- });
-
- describe('Without pipelines nor CI', () => {
- beforeEach(done => {
- mock.onGet('twitter/flight/pipelines.json').reply(200, {
- pipelines: [],
- count: {
- all: 0,
- pending: 0,
- running: 0,
- finished: 0,
- },
- });
-
- vm = mountComponent(PipelinesComponent, {
- store: new Store(),
- hasGitlabCi: false,
- canCreatePipeline: false,
- ...noPermissions,
- });
-
- setTimeout(() => {
- done();
- });
- });
-
- it('renders empty state without button to set CI', () => {
- expect(vm.$el.querySelector('.js-empty-state').textContent.trim()).toEqual(
- 'This project is not currently set up to run pipelines.',
- );
-
- expect(vm.$el.querySelector('.js-get-started-pipelines')).toBeNull();
- });
-
- it('does not render tabs or buttons', () => {
- expect(vm.$el.querySelector('.js-pipelines-tab-all')).toBeNull();
- expect(vm.$el.querySelector('.js-run-pipeline')).toBeNull();
- expect(vm.$el.querySelector('.js-ci-lint')).toBeNull();
- expect(vm.$el.querySelector('.js-clear-cache')).toBeNull();
- });
- });
-
- describe('When API returns error', () => {
- beforeEach(done => {
- mock.onGet('twitter/flight/pipelines.json').reply(500, {});
-
- vm = mountComponent(PipelinesComponent, {
- store: new Store(),
- hasGitlabCi: false,
- canCreatePipeline: true,
- ...noPermissions,
- });
-
- setTimeout(() => {
- done();
- });
- });
-
- it('renders tabs', () => {
- expect(vm.$el.querySelector('.js-pipelines-tab-all').textContent.trim()).toContain('All');
- });
-
- it('does not renders buttons', () => {
- expect(vm.$el.querySelector('.js-run-pipeline')).toBeNull();
- expect(vm.$el.querySelector('.js-ci-lint')).toBeNull();
- expect(vm.$el.querySelector('.js-clear-cache')).toBeNull();
- });
-
- it('renders error state', () => {
- expect(vm.$el.querySelector('.empty-state').textContent.trim()).toContain(
- 'There was an error fetching the pipelines.',
- );
- });
- });
- });
-
- describe('successful request', () => {
- describe('with pipelines', () => {
- beforeEach(() => {
- mock.onGet('twitter/flight/pipelines.json').reply(200, pipelines);
-
- vm = mountComponent(PipelinesComponent, {
- store: new Store(),
- hasGitlabCi: true,
- canCreatePipeline: true,
- ...paths,
- });
- });
-
- it('should render table', done => {
- setTimeout(() => {
- expect(vm.$el.querySelector('.table-holder')).toBeDefined();
- expect(vm.$el.querySelectorAll('.gl-responsive-table-row').length).toEqual(
- pipelines.pipelines.length + 1,
- );
- done();
- });
- });
-
- it('should render navigation tabs', done => {
- setTimeout(() => {
- expect(vm.$el.querySelector('.js-pipelines-tab-pending').textContent.trim()).toContain(
- 'Pending',
- );
-
- expect(vm.$el.querySelector('.js-pipelines-tab-all').textContent.trim()).toContain('All');
-
- expect(vm.$el.querySelector('.js-pipelines-tab-running').textContent.trim()).toContain(
- 'Running',
- );
-
- expect(vm.$el.querySelector('.js-pipelines-tab-finished').textContent.trim()).toContain(
- 'Finished',
- );
-
- expect(vm.$el.querySelector('.js-pipelines-tab-branches').textContent.trim()).toContain(
- 'Branches',
- );
-
- expect(vm.$el.querySelector('.js-pipelines-tab-tags').textContent.trim()).toContain(
- 'Tags',
- );
- done();
- });
- });
-
- it('should make an API request when using tabs', done => {
- setTimeout(() => {
- spyOn(vm, 'updateContent');
- vm.$el.querySelector('.js-pipelines-tab-finished').click();
-
- expect(vm.updateContent).toHaveBeenCalledWith({ scope: 'finished', page: '1' });
- done();
- });
- });
-
- describe('with pagination', () => {
- it('should make an API request when using pagination', done => {
- setTimeout(() => {
- spyOn(vm, 'updateContent');
- // Mock pagination
- vm.store.state.pageInfo = {
- page: 1,
- total: 10,
- perPage: 2,
- nextPage: 2,
- totalPages: 5,
- };
-
- vm.$nextTick(() => {
- vm.$el.querySelector('.next-page-item').click();
-
- expect(vm.updateContent).toHaveBeenCalledWith({ scope: 'all', page: '2' });
-
- done();
- });
- });
- });
- });
- });
- });
-
- describe('methods', () => {
- beforeEach(() => {
- spyOn(window.history, 'pushState').and.stub();
- });
-
- describe('updateContent', () => {
- it('should set given parameters', () => {
- vm = mountComponent(PipelinesComponent, {
- store: new Store(),
- hasGitlabCi: true,
- canCreatePipeline: true,
- ...paths,
- });
- vm.updateContent({ scope: 'finished', page: '4' });
-
- expect(vm.page).toEqual('4');
- expect(vm.scope).toEqual('finished');
- expect(vm.requestData.scope).toEqual('finished');
- expect(vm.requestData.page).toEqual('4');
- });
- });
-
- describe('onChangeTab', () => {
- it('should set page to 1', () => {
- vm = mountComponent(PipelinesComponent, {
- store: new Store(),
- hasGitlabCi: true,
- canCreatePipeline: true,
- ...paths,
- });
- spyOn(vm, 'updateContent');
-
- vm.onChangeTab('running');
-
- expect(vm.updateContent).toHaveBeenCalledWith({ scope: 'running', page: '1' });
- });
- });
-
- describe('onChangePage', () => {
- it('should update page and keep scope', () => {
- vm = mountComponent(PipelinesComponent, {
- store: new Store(),
- hasGitlabCi: true,
- canCreatePipeline: true,
- ...paths,
- });
- spyOn(vm, 'updateContent');
-
- vm.onChangePage(4);
-
- expect(vm.updateContent).toHaveBeenCalledWith({ scope: vm.scope, page: '4' });
- });
- });
- });
-
- describe('computed properties', () => {
- beforeEach(() => {
- vm = mountComponent(PipelinesComponent, {
- store: new Store(),
- hasGitlabCi: true,
- canCreatePipeline: true,
- ...paths,
- });
- });
-
- describe('tabs', () => {
- it('returns default tabs', () => {
- expect(vm.tabs).toEqual([
- { name: 'All', scope: 'all', count: undefined, isActive: true },
- { name: 'Pending', scope: 'pending', count: undefined, isActive: false },
- { name: 'Running', scope: 'running', count: undefined, isActive: false },
- { name: 'Finished', scope: 'finished', count: undefined, isActive: false },
- { name: 'Branches', scope: 'branches', isActive: false },
- { name: 'Tags', scope: 'tags', isActive: false },
- ]);
- });
- });
-
- describe('emptyTabMessage', () => {
- it('returns message with scope', done => {
- vm.scope = 'pending';
-
- vm.$nextTick(() => {
- expect(vm.emptyTabMessage).toEqual('There are currently no pending pipelines.');
- done();
- });
- });
-
- it('returns message without scope when scope is `all`', () => {
- expect(vm.emptyTabMessage).toEqual('There are currently no pipelines.');
- });
- });
-
- describe('stateToRender', () => {
- it('returns loading state when the app is loading', () => {
- expect(vm.stateToRender).toEqual('loading');
- });
-
- it('returns error state when app has error', done => {
- vm.hasError = true;
- vm.isLoading = false;
-
- vm.$nextTick(() => {
- expect(vm.stateToRender).toEqual('error');
- done();
- });
- });
-
- it('returns table list when app has pipelines', done => {
- vm.isLoading = false;
- vm.hasError = false;
- vm.state.pipelines = pipelines.pipelines;
-
- vm.$nextTick(() => {
- expect(vm.stateToRender).toEqual('tableList');
-
- done();
- });
- });
-
- it('returns empty tab when app does not have pipelines but project has pipelines', done => {
- vm.state.count.all = 10;
- vm.isLoading = false;
-
- vm.$nextTick(() => {
- expect(vm.stateToRender).toEqual('emptyTab');
-
- done();
- });
- });
-
- it('returns empty tab when project has CI', done => {
- vm.isLoading = false;
- vm.$nextTick(() => {
- expect(vm.stateToRender).toEqual('emptyTab');
-
- done();
- });
- });
-
- it('returns empty state when project does not have pipelines nor CI', done => {
- vm.isLoading = false;
- vm.hasGitlabCi = false;
- vm.$nextTick(() => {
- expect(vm.stateToRender).toEqual('emptyState');
-
- done();
- });
- });
- });
-
- describe('shouldRenderTabs', () => {
- it('returns true when state is loading & has already made the first request', done => {
- vm.isLoading = true;
- vm.hasMadeRequest = true;
-
- vm.$nextTick(() => {
- expect(vm.shouldRenderTabs).toEqual(true);
-
- done();
- });
- });
-
- it('returns true when state is tableList & has already made the first request', done => {
- vm.isLoading = false;
- vm.state.pipelines = pipelines.pipelines;
- vm.hasMadeRequest = true;
-
- vm.$nextTick(() => {
- expect(vm.shouldRenderTabs).toEqual(true);
-
- done();
- });
- });
-
- it('returns true when state is error & has already made the first request', done => {
- vm.isLoading = false;
- vm.hasError = true;
- vm.hasMadeRequest = true;
-
- vm.$nextTick(() => {
- expect(vm.shouldRenderTabs).toEqual(true);
-
- done();
- });
- });
-
- it('returns true when state is empty tab & has already made the first request', done => {
- vm.isLoading = false;
- vm.state.count.all = 10;
- vm.hasMadeRequest = true;
-
- vm.$nextTick(() => {
- expect(vm.shouldRenderTabs).toEqual(true);
-
- done();
- });
- });
-
- it('returns false when has not made first request', done => {
- vm.hasMadeRequest = false;
-
- vm.$nextTick(() => {
- expect(vm.shouldRenderTabs).toEqual(false);
-
- done();
- });
- });
-
- it('returns false when state is empty state', done => {
- vm.isLoading = false;
- vm.hasMadeRequest = true;
- vm.hasGitlabCi = false;
-
- vm.$nextTick(() => {
- expect(vm.shouldRenderTabs).toEqual(false);
-
- done();
- });
- });
- });
-
- describe('shouldRenderButtons', () => {
- it('returns true when it has paths & has made the first request', done => {
- vm.hasMadeRequest = true;
-
- vm.$nextTick(() => {
- expect(vm.shouldRenderButtons).toEqual(true);
-
- done();
- });
- });
-
- it('returns false when it has not made the first request', done => {
- vm.hasMadeRequest = false;
-
- vm.$nextTick(() => {
- expect(vm.shouldRenderButtons).toEqual(false);
-
- done();
- });
- });
- });
- });
-
- describe('updates results when a staged is clicked', () => {
- beforeEach(() => {
- const copyPipeline = Object.assign({}, pipelineWithStages);
- copyPipeline.id += 1;
- mock
- .onGet('twitter/flight/pipelines.json')
- .reply(
- 200,
- {
- pipelines: [pipelineWithStages],
- count: {
- all: 1,
- finished: 1,
- pending: 0,
- running: 0,
- },
- },
- {
- 'POLL-INTERVAL': 100,
- },
- )
- .onGet(pipelineWithStages.details.stages[0].dropdown_path)
- .reply(200, stageReply);
-
- vm = mountComponent(PipelinesComponent, {
- store: new Store(),
- hasGitlabCi: true,
- canCreatePipeline: true,
- ...paths,
- });
- });
-
- describe('when a request is being made', () => {
- it('stops polling, cancels the request, & restarts polling', done => {
- spyOn(vm.poll, 'stop');
- spyOn(vm.poll, 'restart');
- spyOn(vm.service.cancelationSource, 'cancel').and.callThrough();
-
- setTimeout(() => {
- vm.isMakingRequest = true;
- return vm
- .$nextTick()
- .then(() => {
- vm.$el.querySelector('.js-builds-dropdown-button').click();
- })
- .then(() => {
- expect(vm.service.cancelationSource.cancel).toHaveBeenCalled();
- expect(vm.poll.stop).toHaveBeenCalled();
-
- setTimeout(() => {
- expect(vm.poll.restart).toHaveBeenCalled();
- done();
- }, 0);
- })
- .catch(done.fail);
- }, 0);
- });
- });
-
- describe('when no request is being made', () => {
- it('stops polling & restarts polling', done => {
- spyOn(vm.poll, 'stop');
- spyOn(vm.poll, 'restart');
-
- setTimeout(() => {
- vm.$el.querySelector('.js-builds-dropdown-button').click();
-
- expect(vm.poll.stop).toHaveBeenCalled();
-
- setTimeout(() => {
- expect(vm.poll.restart).toHaveBeenCalled();
- done();
- }, 0);
- }, 0);
- });
- });
- });
-});
diff --git a/spec/javascripts/pipelines/pipelines_table_spec.js b/spec/javascripts/pipelines/pipelines_table_spec.js
deleted file mode 100644
index 5c3387190ab..00000000000
--- a/spec/javascripts/pipelines/pipelines_table_spec.js
+++ /dev/null
@@ -1,86 +0,0 @@
-import Vue from 'vue';
-import pipelinesTableComp from '~/pipelines/components/pipelines_table.vue';
-import '~/lib/utils/datetime_utility';
-
-describe('Pipelines Table', () => {
- const jsonFixtureName = 'pipelines/pipelines.json';
-
- let pipeline;
- let PipelinesTableComponent;
-
- preloadFixtures(jsonFixtureName);
-
- beforeEach(() => {
- const { pipelines } = getJSONFixture(jsonFixtureName);
-
- PipelinesTableComponent = Vue.extend(pipelinesTableComp);
- pipeline = pipelines.find(p => p.user !== null && p.commit !== null);
- });
-
- describe('table', () => {
- let component;
- beforeEach(() => {
- component = new PipelinesTableComponent({
- propsData: {
- pipelines: [],
- autoDevopsHelpPath: 'foo',
- viewType: 'root',
- },
- }).$mount();
- });
-
- afterEach(() => {
- component.$destroy();
- });
-
- it('should render a table', () => {
- expect(component.$el.getAttribute('class')).toContain('ci-table');
- });
-
- it('should render table head with correct columns', () => {
- expect(
- component.$el.querySelector('.table-section.js-pipeline-status').textContent.trim(),
- ).toEqual('Status');
-
- expect(
- component.$el.querySelector('.table-section.js-pipeline-info').textContent.trim(),
- ).toEqual('Pipeline');
-
- expect(
- component.$el.querySelector('.table-section.js-pipeline-commit').textContent.trim(),
- ).toEqual('Commit');
-
- expect(
- component.$el.querySelector('.table-section.js-pipeline-stages').textContent.trim(),
- ).toEqual('Stages');
- });
- });
-
- describe('without data', () => {
- it('should render an empty table', () => {
- const component = new PipelinesTableComponent({
- propsData: {
- pipelines: [],
- autoDevopsHelpPath: 'foo',
- viewType: 'root',
- },
- }).$mount();
-
- expect(component.$el.querySelectorAll('.commit.gl-responsive-table-row').length).toEqual(0);
- });
- });
-
- describe('with data', () => {
- it('should render rows', () => {
- const component = new PipelinesTableComponent({
- propsData: {
- pipelines: [pipeline],
- autoDevopsHelpPath: 'foo',
- viewType: 'root',
- },
- }).$mount();
-
- expect(component.$el.querySelectorAll('.commit.gl-responsive-table-row').length).toEqual(1);
- });
- });
-});
diff --git a/spec/javascripts/pipelines/stage_spec.js b/spec/javascripts/pipelines/stage_spec.js
deleted file mode 100644
index b99688ec371..00000000000
--- a/spec/javascripts/pipelines/stage_spec.js
+++ /dev/null
@@ -1,136 +0,0 @@
-import Vue from 'vue';
-import MockAdapter from 'axios-mock-adapter';
-import mountComponent from 'spec/helpers/vue_mount_component_helper';
-import axios from '~/lib/utils/axios_utils';
-import stage from '~/pipelines/components/stage.vue';
-import eventHub from '~/pipelines/event_hub';
-import { stageReply } from './mock_data';
-
-describe('Pipelines stage component', () => {
- let StageComponent;
- let component;
- let mock;
-
- beforeEach(() => {
- mock = new MockAdapter(axios);
-
- StageComponent = Vue.extend(stage);
-
- component = mountComponent(StageComponent, {
- stage: {
- status: {
- group: 'success',
- icon: 'status_success',
- title: 'success',
- },
- dropdown_path: 'path.json',
- },
- updateDropdown: false,
- });
- });
-
- afterEach(() => {
- component.$destroy();
- mock.restore();
- });
-
- it('should render a dropdown with the status icon', () => {
- expect(component.$el.getAttribute('class')).toEqual('dropdown');
- expect(component.$el.querySelector('svg')).toBeDefined();
- expect(component.$el.querySelector('button').getAttribute('data-toggle')).toEqual('dropdown');
- });
-
- describe('with successful request', () => {
- beforeEach(() => {
- mock.onGet('path.json').reply(200, stageReply);
- });
-
- it('should render the received data and emit `clickedDropdown` event', done => {
- spyOn(eventHub, '$emit');
- component.$el.querySelector('button').click();
-
- setTimeout(() => {
- expect(
- component.$el.querySelector('.js-builds-dropdown-container ul').textContent.trim(),
- ).toContain(stageReply.latest_statuses[0].name);
-
- expect(eventHub.$emit).toHaveBeenCalledWith('clickedDropdown');
- done();
- }, 0);
- });
- });
-
- describe('when request fails', () => {
- beforeEach(() => {
- mock.onGet('path.json').reply(500);
- });
-
- it('should close the dropdown', () => {
- component.$el.click();
-
- setTimeout(() => {
- expect(component.$el.classList.contains('open')).toEqual(false);
- }, 0);
- });
- });
-
- describe('update endpoint correctly', () => {
- beforeEach(() => {
- const copyStage = Object.assign({}, stageReply);
- copyStage.latest_statuses[0].name = 'this is the updated content';
- mock.onGet('bar.json').reply(200, copyStage);
- });
-
- it('should update the stage to request the new endpoint provided', done => {
- component.stage = {
- status: {
- group: 'running',
- icon: 'status_running',
- title: 'running',
- },
- dropdown_path: 'bar.json',
- };
-
- Vue.nextTick(() => {
- component.$el.querySelector('button').click();
-
- setTimeout(() => {
- expect(
- component.$el.querySelector('.js-builds-dropdown-container ul').textContent.trim(),
- ).toContain('this is the updated content');
- done();
- });
- });
- });
- });
-
- describe('pipelineActionRequestComplete', () => {
- beforeEach(() => {
- mock.onGet('path.json').reply(200, stageReply);
-
- mock.onPost(`${stageReply.latest_statuses[0].status.action.path}.json`).reply(200);
- });
-
- describe('within pipeline table', () => {
- it('emits `refreshPipelinesTable` event when `pipelineActionRequestComplete` is triggered', done => {
- spyOn(eventHub, '$emit');
-
- component.type = 'PIPELINES_TABLE';
- component.$el.querySelector('button').click();
-
- setTimeout(() => {
- component.$el.querySelector('.js-ci-action').click();
- setTimeout(() => {
- component
- .$nextTick()
- .then(() => {
- expect(eventHub.$emit).toHaveBeenCalledWith('refreshPipelinesTable');
- })
- .then(done)
- .catch(done.fail);
- }, 0);
- }, 0);
- });
- });
- });
-});
diff --git a/spec/javascripts/pipelines/stores/pipeline.json b/spec/javascripts/pipelines/stores/pipeline.json
deleted file mode 100644
index 7d5891d3d52..00000000000
--- a/spec/javascripts/pipelines/stores/pipeline.json
+++ /dev/null
@@ -1,167 +0,0 @@
-{
- "id": 37232567,
- "user": {
- "id": 113870,
- "name": "Phil Hughes",
- "username": "iamphill",
- "state": "active",
- "avatar_url": "https://secure.gravatar.com/avatar/533a51534470a11062df393543eab649?s=80\u0026d=identicon",
- "web_url": "https://gitlab.com/iamphill",
- "status_tooltip_html": null,
- "path": "/iamphill"
- },
- "active": false,
- "coverage": null,
- "source": "push",
- "created_at": "2018-11-20T10:22:52.617Z",
- "updated_at": "2018-11-20T10:24:09.511Z",
- "path": "/gitlab-org/gl-vue-cli/pipelines/37232567",
- "flags": {
- "latest": true,
- "stuck": false,
- "auto_devops": false,
- "yaml_errors": false,
- "retryable": false,
- "cancelable": false,
- "failure_reason": false
- },
- "details": {
- "status": {
- "icon": "status_success",
- "text": "passed",
- "label": "passed",
- "group": "success",
- "tooltip": "passed",
- "has_details": true,
- "details_path": "/gitlab-org/gl-vue-cli/pipelines/37232567",
- "illustration": null,
- "favicon": "https://gitlab.com/assets/ci_favicons/favicon_status_success-8451333011eee8ce9f2ab25dc487fe24a8758c694827a582f17f42b0a90446a2.png"
- },
- "duration": 65,
- "finished_at": "2018-11-20T10:24:09.483Z",
- "stages": [
- {
- "name": "test",
- "title": "test: passed",
- "groups": [
- {
- "name": "eslint",
- "size": 1,
- "status": {
- "icon": "status_success",
- "text": "passed",
- "label": "passed",
- "group": "success",
- "tooltip": "passed",
- "has_details": true,
- "details_path": "/gitlab-org/gl-vue-cli/-/jobs/122845352",
- "illustration": {
- "image": "https://assets.gitlab-static.net/assets/illustrations/skipped-job_empty-8b877955fbf175e42ae65b6cb95346e15282c6fc5b682756c329af3a0055225e.svg",
- "size": "svg-430",
- "title": "This job does not have a trace."
- },
- "favicon": "https://gitlab.com/assets/ci_favicons/favicon_status_success-8451333011eee8ce9f2ab25dc487fe24a8758c694827a582f17f42b0a90446a2.png",
- "action": {
- "icon": "retry",
- "title": "Retry",
- "path": "/gitlab-org/gl-vue-cli/-/jobs/122845352/retry",
- "method": "post",
- "button_title": "Retry this job"
- }
- },
- "jobs": [
- {
- "id": 122845352,
- "name": "eslint",
- "started": "2018-11-20T10:22:53.369Z",
- "archived": false,
- "build_path": "/gitlab-org/gl-vue-cli/-/jobs/122845352",
- "retry_path": "/gitlab-org/gl-vue-cli/-/jobs/122845352/retry",
- "playable": false,
- "scheduled": false,
- "created_at": "2018-11-20T10:22:52.630Z",
- "updated_at": "2018-11-20T10:23:58.948Z",
- "status": {
- "icon": "status_success",
- "text": "passed",
- "label": "passed",
- "group": "success",
- "tooltip": "passed",
- "has_details": true,
- "details_path": "/gitlab-org/gl-vue-cli/-/jobs/122845352",
- "illustration": {
- "image": "https://assets.gitlab-static.net/assets/illustrations/skipped-job_empty-8b877955fbf175e42ae65b6cb95346e15282c6fc5b682756c329af3a0055225e.svg",
- "size": "svg-430",
- "title": "This job does not have a trace."
- },
- "favicon": "https://gitlab.com/assets/ci_favicons/favicon_status_success-8451333011eee8ce9f2ab25dc487fe24a8758c694827a582f17f42b0a90446a2.png",
- "action": {
- "icon": "retry",
- "title": "Retry",
- "path": "/gitlab-org/gl-vue-cli/-/jobs/122845352/retry",
- "method": "post",
- "button_title": "Retry this job"
- }
- }
- }
- ]
- }
- ],
- "status": {
- "icon": "status_success",
- "text": "passed",
- "label": "passed",
- "group": "success",
- "tooltip": "passed",
- "has_details": true,
- "details_path": "/gitlab-org/gl-vue-cli/pipelines/37232567#test",
- "illustration": null,
- "favicon": "https://gitlab.com/assets/ci_favicons/favicon_status_success-8451333011eee8ce9f2ab25dc487fe24a8758c694827a582f17f42b0a90446a2.png"
- },
- "path": "/gitlab-org/gl-vue-cli/pipelines/37232567#test",
- "dropdown_path": "/gitlab-org/gl-vue-cli/pipelines/37232567/stage.json?stage=test"
- }
- ],
- "artifacts": [],
- "manual_actions": [],
- "scheduled_actions": []
- },
- "ref": {
- "name": "master",
- "path": "/gitlab-org/gl-vue-cli/commits/master",
- "tag": false,
- "branch": true
- },
- "commit": {
- "id": "8f179601d481950bcb67032caeb33d1c24dde6bd",
- "short_id": "8f179601",
- "title": "Merge branch 'gl-cli-startt' into 'master'",
- "created_at": "2018-11-20T10:22:51.000Z",
- "parent_ids": [
- "781d78fcd3d6c17ccf208f0cf0ab47c3e5397118",
- "d227a0bb858c48eeee7393fcade1a33748f35183"
- ],
- "message": "Merge branch 'gl-cli-startt' into 'master'\n\nFirst iteration of the CLI\n\nCloses gitlab-foss#53657\n\nSee merge request gitlab-org/gl-vue-cli!2",
- "author_name": "Phil Hughes",
- "author_email": "me@iamphill.com",
- "authored_date": "2018-11-20T10:22:51.000Z",
- "committer_name": "Phil Hughes",
- "committer_email": "me@iamphill.com",
- "committed_date": "2018-11-20T10:22:51.000Z",
- "author": {
- "id": 113870,
- "name": "Phil Hughes",
- "username": "iamphill",
- "state": "active",
- "avatar_url": "https://secure.gravatar.com/avatar/533a51534470a11062df393543eab649?s=80\u0026d=identicon",
- "web_url": "https://gitlab.com/iamphill",
- "status_tooltip_html": null,
- "path": "/iamphill"
- },
- "author_gravatar_url": "https://secure.gravatar.com/avatar/533a51534470a11062df393543eab649?s=80\u0026d=identicon",
- "commit_url": "https://gitlab.com/gitlab-org/gl-vue-cli/commit/8f179601d481950bcb67032caeb33d1c24dde6bd",
- "commit_path": "/gitlab-org/gl-vue-cli/commit/8f179601d481950bcb67032caeb33d1c24dde6bd"
- },
- "triggered_by": null,
- "triggered": []
-}
diff --git a/spec/javascripts/pipelines/stores/pipeline_store.js b/spec/javascripts/pipelines/stores/pipeline_store.js
deleted file mode 100644
index 4a0b3bf4c02..00000000000
--- a/spec/javascripts/pipelines/stores/pipeline_store.js
+++ /dev/null
@@ -1,165 +0,0 @@
-import PipelineStore from '~/pipelines/stores/pipeline_store';
-import LinkedPipelines from '../linked_pipelines_mock.json';
-
-describe('EE Pipeline store', () => {
- let store;
- let data;
-
- beforeEach(() => {
- store = new PipelineStore();
- data = Object.assign({}, LinkedPipelines);
- });
-
- describe('storePipeline', () => {
- beforeAll(() => {
- store.storePipeline(data);
- });
-
- describe('triggered_by', () => {
- it('sets triggered_by as an array', () => {
- expect(store.state.pipeline.triggered_by.length).toEqual(1);
- });
-
- it('adds isExpanding & isLoading keys set to false', () => {
- expect(store.state.pipeline.triggered_by[0].isExpanded).toEqual(false);
- expect(store.state.pipeline.triggered_by[0].isLoading).toEqual(false);
- });
-
- it('parses nested triggered_by', () => {
- expect(store.state.pipeline.triggered_by[0].triggered_by.length).toEqual(1);
- expect(store.state.pipeline.triggered_by[0].triggered_by[0].isExpanded).toEqual(false);
- expect(store.state.pipeline.triggered_by[0].triggered_by[0].isLoading).toEqual(false);
- });
- });
-
- describe('triggered', () => {
- it('adds isExpanding & isLoading keys set to false for each triggered pipeline', () => {
- store.state.pipeline.triggered.forEach(pipeline => {
- expect(pipeline.isExpanded).toEqual(false);
- expect(pipeline.isLoading).toEqual(false);
- });
- });
-
- it('parses nested triggered pipelines', () => {
- store.state.pipeline.triggered[1].triggered.forEach(pipeline => {
- expect(pipeline.isExpanded).toEqual(false);
- expect(pipeline.isLoading).toEqual(false);
- });
- });
- });
- });
-
- describe('resetTriggeredByPipeline', () => {
- beforeEach(() => {
- store.storePipeline(data);
- });
-
- it('closes the pipeline & nested ones', () => {
- store.state.pipeline.triggered_by[0].isExpanded = true;
- store.state.pipeline.triggered_by[0].triggered_by[0].isExpanded = true;
-
- store.resetTriggeredByPipeline(store.state.pipeline, store.state.pipeline.triggered_by[0]);
-
- expect(store.state.pipeline.triggered_by[0].isExpanded).toEqual(false);
- expect(store.state.pipeline.triggered_by[0].triggered_by[0].isExpanded).toEqual(false);
- });
- });
-
- describe('openTriggeredByPipeline', () => {
- beforeEach(() => {
- store.storePipeline(data);
- });
-
- it('opens the given pipeline', () => {
- store.openTriggeredByPipeline(store.state.pipeline, store.state.pipeline.triggered_by[0]);
-
- expect(store.state.pipeline.triggered_by[0].isExpanded).toEqual(true);
- });
- });
-
- describe('closeTriggeredByPipeline', () => {
- beforeEach(() => {
- store.storePipeline(data);
- });
-
- it('closes the given pipeline', () => {
- // open it first
- store.openTriggeredByPipeline(store.state.pipeline, store.state.pipeline.triggered_by[0]);
-
- store.closeTriggeredByPipeline(store.state.pipeline, store.state.pipeline.triggered_by[0]);
-
- expect(store.state.pipeline.triggered_by[0].isExpanded).toEqual(false);
- });
- });
-
- describe('resetTriggeredPipelines', () => {
- beforeEach(() => {
- store.storePipeline(data);
- });
-
- it('closes the pipeline & nested ones', () => {
- store.state.pipeline.triggered[0].isExpanded = true;
- store.state.pipeline.triggered[0].triggered[0].isExpanded = true;
-
- store.resetTriggeredPipeline(store.state.pipeline, store.state.pipeline.triggered[0]);
-
- expect(store.state.pipeline.triggered[0].isExpanded).toEqual(false);
- expect(store.state.pipeline.triggered[0].triggered[0].isExpanded).toEqual(false);
- });
- });
-
- describe('openTriggeredPipeline', () => {
- beforeEach(() => {
- store.storePipeline(data);
- });
-
- it('opens the given pipeline', () => {
- store.openTriggeredPipeline(store.state.pipeline, store.state.pipeline.triggered[0]);
-
- expect(store.state.pipeline.triggered[0].isExpanded).toEqual(true);
- });
- });
-
- describe('closeTriggeredPipeline', () => {
- beforeEach(() => {
- store.storePipeline(data);
- });
-
- it('closes the given pipeline', () => {
- // open it first
- store.openTriggeredPipeline(store.state.pipeline, store.state.pipeline.triggered[0]);
-
- store.closeTriggeredPipeline(store.state.pipeline, store.state.pipeline.triggered[0]);
-
- expect(store.state.pipeline.triggered[0].isExpanded).toEqual(false);
- });
- });
-
- describe('toggleLoading', () => {
- beforeEach(() => {
- store.storePipeline(data);
- });
-
- it('toggles the isLoading property for the given pipeline', () => {
- store.togglePipeline(store.state.pipeline.triggered[0]);
-
- expect(store.state.pipeline.triggered[0].isLoading).toEqual(true);
- });
- });
-
- describe('addExpandedPipelineToRequestData', () => {
- it('pushes the given id to expandedPipelines array', () => {
- store.addExpandedPipelineToRequestData('213231');
-
- expect(store.state.expandedPipelines).toEqual(['213231']);
- });
- });
-
- describe('removeExpandedPipelineToRequestData', () => {
- it('pushes the given id to expandedPipelines array', () => {
- store.removeExpandedPipelineToRequestData('213231');
-
- expect(store.state.expandedPipelines).toEqual([]);
- });
- });
-});
diff --git a/spec/javascripts/pipelines/stores/pipeline_with_triggered.json b/spec/javascripts/pipelines/stores/pipeline_with_triggered.json
deleted file mode 100644
index 1fa15e45792..00000000000
--- a/spec/javascripts/pipelines/stores/pipeline_with_triggered.json
+++ /dev/null
@@ -1,381 +0,0 @@
-{
- "id": 23211253,
- "user": {
- "id": 3585,
- "name": "Achilleas Pipinellis",
- "username": "axil",
- "state": "active",
- "avatar_url": "https://assets.gitlab-static.net/uploads/-/system/user/avatar/3585/avatar.png",
- "web_url": "https://gitlab.com/axil",
- "status_tooltip_html": "\u003cspan class=\"user-status-emoji has-tooltip\" title=\"\" data-html=\"true\" data-placement=\"top\"\u003e\u003cgl-emoji title=\"trumpet\" data-name=\"trumpet\" data-unicode-version=\"6.0\"\u003e🎺\u003c/gl-emoji\u003e\u003c/span\u003e",
- "path": "/axil"
- },
- "active": false,
- "coverage": null,
- "source": "push",
- "created_at": "2018-06-05T11:31:30.452Z",
- "updated_at": "2018-10-31T16:35:31.305Z",
- "path": "/gitlab-org/gitlab-runner/pipelines/23211253",
- "flags": {
- "latest": false,
- "stuck": false,
- "auto_devops": false,
- "yaml_errors": false,
- "retryable": false,
- "cancelable": false,
- "failure_reason": false
- },
- "details": {
- "status": {
- "icon": "status_success",
- "text": "passed",
- "label": "passed",
- "group": "success",
- "tooltip": "passed",
- "has_details": true,
- "details_path": "/gitlab-org/gitlab-runner/pipelines/23211253",
- "illustration": null,
- "favicon": "https://gitlab.com/assets/ci_favicons/favicon_status_success-8451333011eee8ce9f2ab25dc487fe24a8758c694827a582f17f42b0a90446a2.png"
- },
- "duration": 53,
- "finished_at": "2018-10-31T16:35:31.299Z",
- "stages": [
- {
- "name": "prebuild",
- "title": "prebuild: passed",
- "groups": [
- {
- "name": "review-docs-deploy",
- "size": 1,
- "status": {
- "icon": "status_success",
- "text": "passed",
- "label": "manual play action",
- "group": "success",
- "tooltip": "passed",
- "has_details": true,
- "details_path": "/gitlab-org/gitlab-runner/-/jobs/72469032",
- "illustration": {
- "image": "https://assets.gitlab-static.net/assets/illustrations/manual_action-2b4ca0d1bcfd92aebf33d484e36cbf7a102d007f76b5a0cfea636033a629d601.svg",
- "size": "svg-394",
- "title": "This job requires a manual action",
- "content": "This job depends on a user to trigger its process. Often they are used to deploy code to production environments"
- },
- "favicon": "https://gitlab.com/assets/ci_favicons/favicon_status_success-8451333011eee8ce9f2ab25dc487fe24a8758c694827a582f17f42b0a90446a2.png",
- "action": {
- "icon": "play",
- "title": "Play",
- "path": "/gitlab-org/gitlab-runner/-/jobs/72469032/play",
- "method": "post",
- "button_title": "Trigger this manual action"
- }
- },
- "jobs": [
- {
- "id": 72469032,
- "name": "review-docs-deploy",
- "started": "2018-10-31T16:34:58.778Z",
- "archived": false,
- "build_path": "/gitlab-org/gitlab-runner/-/jobs/72469032",
- "retry_path": "/gitlab-org/gitlab-runner/-/jobs/72469032/retry",
- "play_path": "/gitlab-org/gitlab-runner/-/jobs/72469032/play",
- "playable": true,
- "scheduled": false,
- "created_at": "2018-06-05T11:31:30.495Z",
- "updated_at": "2018-10-31T16:35:31.251Z",
- "status": {
- "icon": "status_success",
- "text": "passed",
- "label": "manual play action",
- "group": "success",
- "tooltip": "passed",
- "has_details": true,
- "details_path": "/gitlab-org/gitlab-runner/-/jobs/72469032",
- "illustration": {
- "image": "https://assets.gitlab-static.net/assets/illustrations/manual_action-2b4ca0d1bcfd92aebf33d484e36cbf7a102d007f76b5a0cfea636033a629d601.svg",
- "size": "svg-394",
- "title": "This job requires a manual action",
- "content": "This job depends on a user to trigger its process. Often they are used to deploy code to production environments"
- },
- "favicon": "https://gitlab.com/assets/ci_favicons/favicon_status_success-8451333011eee8ce9f2ab25dc487fe24a8758c694827a582f17f42b0a90446a2.png",
- "action": {
- "icon": "play",
- "title": "Play",
- "path": "/gitlab-org/gitlab-runner/-/jobs/72469032/play",
- "method": "post",
- "button_title": "Trigger this manual action"
- }
- }
- }
- ]
- }
- ],
- "status": {
- "icon": "status_success",
- "text": "passed",
- "label": "passed",
- "group": "success",
- "tooltip": "passed",
- "has_details": true,
- "details_path": "/gitlab-org/gitlab-runner/pipelines/23211253#prebuild",
- "illustration": null,
- "favicon": "https://gitlab.com/assets/ci_favicons/favicon_status_success-8451333011eee8ce9f2ab25dc487fe24a8758c694827a582f17f42b0a90446a2.png"
- },
- "path": "/gitlab-org/gitlab-runner/pipelines/23211253#prebuild",
- "dropdown_path": "/gitlab-org/gitlab-runner/pipelines/23211253/stage.json?stage=prebuild"
- },
- {
- "name": "test",
- "title": "test: passed",
- "groups": [
- {
- "name": "docs check links",
- "size": 1,
- "status": {
- "icon": "status_success",
- "text": "passed",
- "label": "passed",
- "group": "success",
- "tooltip": "passed",
- "has_details": true,
- "details_path": "/gitlab-org/gitlab-runner/-/jobs/72469033",
- "illustration": {
- "image": "https://assets.gitlab-static.net/assets/illustrations/skipped-job_empty-8b877955fbf175e42ae65b6cb95346e15282c6fc5b682756c329af3a0055225e.svg",
- "size": "svg-430",
- "title": "This job does not have a trace."
- },
- "favicon": "https://gitlab.com/assets/ci_favicons/favicon_status_success-8451333011eee8ce9f2ab25dc487fe24a8758c694827a582f17f42b0a90446a2.png",
- "action": {
- "icon": "retry",
- "title": "Retry",
- "path": "/gitlab-org/gitlab-runner/-/jobs/72469033/retry",
- "method": "post",
- "button_title": "Retry this job"
- }
- },
- "jobs": [
- {
- "id": 72469033,
- "name": "docs check links",
- "started": "2018-06-05T11:31:33.240Z",
- "archived": false,
- "build_path": "/gitlab-org/gitlab-runner/-/jobs/72469033",
- "retry_path": "/gitlab-org/gitlab-runner/-/jobs/72469033/retry",
- "playable": false,
- "scheduled": false,
- "created_at": "2018-06-05T11:31:30.627Z",
- "updated_at": "2018-06-05T11:31:54.363Z",
- "status": {
- "icon": "status_success",
- "text": "passed",
- "label": "passed",
- "group": "success",
- "tooltip": "passed",
- "has_details": true,
- "details_path": "/gitlab-org/gitlab-runner/-/jobs/72469033",
- "illustration": {
- "image": "https://assets.gitlab-static.net/assets/illustrations/skipped-job_empty-8b877955fbf175e42ae65b6cb95346e15282c6fc5b682756c329af3a0055225e.svg",
- "size": "svg-430",
- "title": "This job does not have a trace."
- },
- "favicon": "https://gitlab.com/assets/ci_favicons/favicon_status_success-8451333011eee8ce9f2ab25dc487fe24a8758c694827a582f17f42b0a90446a2.png",
- "action": {
- "icon": "retry",
- "title": "Retry",
- "path": "/gitlab-org/gitlab-runner/-/jobs/72469033/retry",
- "method": "post",
- "button_title": "Retry this job"
- }
- }
- }
- ]
- }
- ],
- "status": {
- "icon": "status_success",
- "text": "passed",
- "label": "passed",
- "group": "success",
- "tooltip": "passed",
- "has_details": true,
- "details_path": "/gitlab-org/gitlab-runner/pipelines/23211253#test",
- "illustration": null,
- "favicon": "https://gitlab.com/assets/ci_favicons/favicon_status_success-8451333011eee8ce9f2ab25dc487fe24a8758c694827a582f17f42b0a90446a2.png"
- },
- "path": "/gitlab-org/gitlab-runner/pipelines/23211253#test",
- "dropdown_path": "/gitlab-org/gitlab-runner/pipelines/23211253/stage.json?stage=test"
- },
- {
- "name": "cleanup",
- "title": "cleanup: skipped",
- "groups": [
- {
- "name": "review-docs-cleanup",
- "size": 1,
- "status": {
- "icon": "status_manual",
- "text": "manual",
- "label": "manual stop action",
- "group": "manual",
- "tooltip": "manual action",
- "has_details": true,
- "details_path": "/gitlab-org/gitlab-runner/-/jobs/72469034",
- "illustration": {
- "image": "https://assets.gitlab-static.net/assets/illustrations/manual_action-2b4ca0d1bcfd92aebf33d484e36cbf7a102d007f76b5a0cfea636033a629d601.svg",
- "size": "svg-394",
- "title": "This job requires a manual action",
- "content": "This job depends on a user to trigger its process. Often they are used to deploy code to production environments"
- },
- "favicon": "https://gitlab.com/assets/ci_favicons/favicon_status_manual-829a0804612cef47d9efc1618dba38325483657c847dba0546c3b9f0295bb36c.png",
- "action": {
- "icon": "stop",
- "title": "Stop",
- "path": "/gitlab-org/gitlab-runner/-/jobs/72469034/play",
- "method": "post",
- "button_title": "Stop this environment"
- }
- },
- "jobs": [
- {
- "id": 72469034,
- "name": "review-docs-cleanup",
- "started": null,
- "archived": false,
- "build_path": "/gitlab-org/gitlab-runner/-/jobs/72469034",
- "play_path": "/gitlab-org/gitlab-runner/-/jobs/72469034/play",
- "playable": true,
- "scheduled": false,
- "created_at": "2018-06-05T11:31:30.760Z",
- "updated_at": "2018-06-05T11:31:56.037Z",
- "status": {
- "icon": "status_manual",
- "text": "manual",
- "label": "manual stop action",
- "group": "manual",
- "tooltip": "manual action",
- "has_details": true,
- "details_path": "/gitlab-org/gitlab-runner/-/jobs/72469034",
- "illustration": {
- "image": "https://assets.gitlab-static.net/assets/illustrations/manual_action-2b4ca0d1bcfd92aebf33d484e36cbf7a102d007f76b5a0cfea636033a629d601.svg",
- "size": "svg-394",
- "title": "This job requires a manual action",
- "content": "This job depends on a user to trigger its process. Often they are used to deploy code to production environments"
- },
- "favicon": "https://gitlab.com/assets/ci_favicons/favicon_status_manual-829a0804612cef47d9efc1618dba38325483657c847dba0546c3b9f0295bb36c.png",
- "action": {
- "icon": "stop",
- "title": "Stop",
- "path": "/gitlab-org/gitlab-runner/-/jobs/72469034/play",
- "method": "post",
- "button_title": "Stop this environment"
- }
- }
- }
- ]
- }
- ],
- "status": {
- "icon": "status_skipped",
- "text": "skipped",
- "label": "skipped",
- "group": "skipped",
- "tooltip": "skipped",
- "has_details": true,
- "details_path": "/gitlab-org/gitlab-runner/pipelines/23211253#cleanup",
- "illustration": null,
- "favicon": "https://gitlab.com/assets/ci_favicons/favicon_status_skipped-0b9c5e543588945e8c4ca57786bbf2d0c56631959c9f853300392d0315be829b.png"
- },
- "path": "/gitlab-org/gitlab-runner/pipelines/23211253#cleanup",
- "dropdown_path": "/gitlab-org/gitlab-runner/pipelines/23211253/stage.json?stage=cleanup"
- }
- ],
- "artifacts": [],
- "manual_actions": [
- {
- "name": "review-docs-cleanup",
- "path": "/gitlab-org/gitlab-runner/-/jobs/72469034/play",
- "playable": true,
- "scheduled": false
- },
- {
- "name": "review-docs-deploy",
- "path": "/gitlab-org/gitlab-runner/-/jobs/72469032/play",
- "playable": true,
- "scheduled": false
- }
- ],
- "scheduled_actions": []
- },
- "ref": {
- "name": "docs/add-development-guide-to-readme",
- "path": "/gitlab-org/gitlab-runner/commits/docs/add-development-guide-to-readme",
- "tag": false,
- "branch": true
- },
- "commit": {
- "id": "8083eb0a920572214d0dccedd7981f05d535ad46",
- "short_id": "8083eb0a",
- "title": "Add link to development guide in readme",
- "created_at": "2018-06-05T11:30:48.000Z",
- "parent_ids": ["1d7cf79b5a1a2121b9474ac20d61c1b8f621289d"],
- "message": "Add link to development guide in readme\n\nCloses https://gitlab.com/gitlab-org/gitlab-runner/issues/3122\n",
- "author_name": "Achilleas Pipinellis",
- "author_email": "axil@gitlab.com",
- "authored_date": "2018-06-05T11:30:48.000Z",
- "committer_name": "Achilleas Pipinellis",
- "committer_email": "axil@gitlab.com",
- "committed_date": "2018-06-05T11:30:48.000Z",
- "author": {
- "id": 3585,
- "name": "Achilleas Pipinellis",
- "username": "axil",
- "state": "active",
- "avatar_url": "https://assets.gitlab-static.net/uploads/-/system/user/avatar/3585/avatar.png",
- "web_url": "https://gitlab.com/axil",
- "status_tooltip_html": null,
- "path": "/axil"
- },
- "author_gravatar_url": "https://secure.gravatar.com/avatar/1d37af00eec153a8333a4ce18e9aea41?s=80\u0026d=identicon",
- "commit_url": "https://gitlab.com/gitlab-org/gitlab-runner/commit/8083eb0a920572214d0dccedd7981f05d535ad46",
- "commit_path": "/gitlab-org/gitlab-runner/commit/8083eb0a920572214d0dccedd7981f05d535ad46"
- },
- "triggered_by": null,
- "triggered": [
- {
- "id": 34993051,
- "user": {
- "id": 376774,
- "name": "Alessio Caiazza",
- "username": "nolith",
- "state": "active",
- "avatar_url": "https://assets.gitlab-static.net/uploads/-/system/user/avatar/376774/avatar.png",
- "web_url": "https://gitlab.com/nolith",
- "status_tooltip_html": null,
- "path": "/nolith"
- },
- "active": false,
- "coverage": null,
- "source": "pipeline",
- "path": "/gitlab-com/gitlab-docs/pipelines/34993051",
- "details": {
- "status": {
- "icon": "status_failed",
- "text": "failed",
- "label": "failed",
- "group": "failed",
- "tooltip": "failed",
- "has_details": true,
- "details_path": "/gitlab-com/gitlab-docs/pipelines/34993051",
- "illustration": null,
- "favicon": "https://gitlab.com/assets/ci_favicons/favicon_status_failed-41304d7f7e3828808b0c26771f0309e55296819a9beea3ea9fbf6689d9857c12.png"
- }
- },
- "project": {
- "id": 1794617,
- "name": "GitLab Docs",
- "full_path": "/gitlab-com/gitlab-docs",
- "full_name": "GitLab.com / GitLab Docs"
- }
- }
- ]
-}
diff --git a/spec/javascripts/pipelines/stores/pipeline_with_triggered_by.json b/spec/javascripts/pipelines/stores/pipeline_with_triggered_by.json
deleted file mode 100644
index 7aeea6f3ebb..00000000000
--- a/spec/javascripts/pipelines/stores/pipeline_with_triggered_by.json
+++ /dev/null
@@ -1,379 +0,0 @@
-{
- "id": 23211253,
- "user": {
- "id": 3585,
- "name": "Achilleas Pipinellis",
- "username": "axil",
- "state": "active",
- "avatar_url": "https://assets.gitlab-static.net/uploads/-/system/user/avatar/3585/avatar.png",
- "web_url": "https://gitlab.com/axil",
- "status_tooltip_html": "\u003cspan class=\"user-status-emoji has-tooltip\" title=\"\" data-html=\"true\" data-placement=\"top\"\u003e\u003cgl-emoji title=\"trumpet\" data-name=\"trumpet\" data-unicode-version=\"6.0\"\u003e🎺\u003c/gl-emoji\u003e\u003c/span\u003e",
- "path": "/axil"
- },
- "active": false,
- "coverage": null,
- "source": "push",
- "created_at": "2018-06-05T11:31:30.452Z",
- "updated_at": "2018-10-31T16:35:31.305Z",
- "path": "/gitlab-org/gitlab-runner/pipelines/23211253",
- "flags": {
- "latest": false,
- "stuck": false,
- "auto_devops": false,
- "yaml_errors": false,
- "retryable": false,
- "cancelable": false,
- "failure_reason": false
- },
- "details": {
- "status": {
- "icon": "status_success",
- "text": "passed",
- "label": "passed",
- "group": "success",
- "tooltip": "passed",
- "has_details": true,
- "details_path": "/gitlab-org/gitlab-runner/pipelines/23211253",
- "illustration": null,
- "favicon": "https://gitlab.com/assets/ci_favicons/favicon_status_success-8451333011eee8ce9f2ab25dc487fe24a8758c694827a582f17f42b0a90446a2.png"
- },
- "duration": 53,
- "finished_at": "2018-10-31T16:35:31.299Z",
- "stages": [
- {
- "name": "prebuild",
- "title": "prebuild: passed",
- "groups": [
- {
- "name": "review-docs-deploy",
- "size": 1,
- "status": {
- "icon": "status_success",
- "text": "passed",
- "label": "manual play action",
- "group": "success",
- "tooltip": "passed",
- "has_details": true,
- "details_path": "/gitlab-org/gitlab-runner/-/jobs/72469032",
- "illustration": {
- "image": "https://assets.gitlab-static.net/assets/illustrations/manual_action-2b4ca0d1bcfd92aebf33d484e36cbf7a102d007f76b5a0cfea636033a629d601.svg",
- "size": "svg-394",
- "title": "This job requires a manual action",
- "content": "This job depends on a user to trigger its process. Often they are used to deploy code to production environments"
- },
- "favicon": "https://gitlab.com/assets/ci_favicons/favicon_status_success-8451333011eee8ce9f2ab25dc487fe24a8758c694827a582f17f42b0a90446a2.png",
- "action": {
- "icon": "play",
- "title": "Play",
- "path": "/gitlab-org/gitlab-runner/-/jobs/72469032/play",
- "method": "post",
- "button_title": "Trigger this manual action"
- }
- },
- "jobs": [
- {
- "id": 72469032,
- "name": "review-docs-deploy",
- "started": "2018-10-31T16:34:58.778Z",
- "archived": false,
- "build_path": "/gitlab-org/gitlab-runner/-/jobs/72469032",
- "retry_path": "/gitlab-org/gitlab-runner/-/jobs/72469032/retry",
- "play_path": "/gitlab-org/gitlab-runner/-/jobs/72469032/play",
- "playable": true,
- "scheduled": false,
- "created_at": "2018-06-05T11:31:30.495Z",
- "updated_at": "2018-10-31T16:35:31.251Z",
- "status": {
- "icon": "status_success",
- "text": "passed",
- "label": "manual play action",
- "group": "success",
- "tooltip": "passed",
- "has_details": true,
- "details_path": "/gitlab-org/gitlab-runner/-/jobs/72469032",
- "illustration": {
- "image": "https://assets.gitlab-static.net/assets/illustrations/manual_action-2b4ca0d1bcfd92aebf33d484e36cbf7a102d007f76b5a0cfea636033a629d601.svg",
- "size": "svg-394",
- "title": "This job requires a manual action",
- "content": "This job depends on a user to trigger its process. Often they are used to deploy code to production environments"
- },
- "favicon": "https://gitlab.com/assets/ci_favicons/favicon_status_success-8451333011eee8ce9f2ab25dc487fe24a8758c694827a582f17f42b0a90446a2.png",
- "action": {
- "icon": "play",
- "title": "Play",
- "path": "/gitlab-org/gitlab-runner/-/jobs/72469032/play",
- "method": "post",
- "button_title": "Trigger this manual action"
- }
- }
- }
- ]
- }
- ],
- "status": {
- "icon": "status_success",
- "text": "passed",
- "label": "passed",
- "group": "success",
- "tooltip": "passed",
- "has_details": true,
- "details_path": "/gitlab-org/gitlab-runner/pipelines/23211253#prebuild",
- "illustration": null,
- "favicon": "https://gitlab.com/assets/ci_favicons/favicon_status_success-8451333011eee8ce9f2ab25dc487fe24a8758c694827a582f17f42b0a90446a2.png"
- },
- "path": "/gitlab-org/gitlab-runner/pipelines/23211253#prebuild",
- "dropdown_path": "/gitlab-org/gitlab-runner/pipelines/23211253/stage.json?stage=prebuild"
- },
- {
- "name": "test",
- "title": "test: passed",
- "groups": [
- {
- "name": "docs check links",
- "size": 1,
- "status": {
- "icon": "status_success",
- "text": "passed",
- "label": "passed",
- "group": "success",
- "tooltip": "passed",
- "has_details": true,
- "details_path": "/gitlab-org/gitlab-runner/-/jobs/72469033",
- "illustration": {
- "image": "https://assets.gitlab-static.net/assets/illustrations/skipped-job_empty-8b877955fbf175e42ae65b6cb95346e15282c6fc5b682756c329af3a0055225e.svg",
- "size": "svg-430",
- "title": "This job does not have a trace."
- },
- "favicon": "https://gitlab.com/assets/ci_favicons/favicon_status_success-8451333011eee8ce9f2ab25dc487fe24a8758c694827a582f17f42b0a90446a2.png",
- "action": {
- "icon": "retry",
- "title": "Retry",
- "path": "/gitlab-org/gitlab-runner/-/jobs/72469033/retry",
- "method": "post",
- "button_title": "Retry this job"
- }
- },
- "jobs": [
- {
- "id": 72469033,
- "name": "docs check links",
- "started": "2018-06-05T11:31:33.240Z",
- "archived": false,
- "build_path": "/gitlab-org/gitlab-runner/-/jobs/72469033",
- "retry_path": "/gitlab-org/gitlab-runner/-/jobs/72469033/retry",
- "playable": false,
- "scheduled": false,
- "created_at": "2018-06-05T11:31:30.627Z",
- "updated_at": "2018-06-05T11:31:54.363Z",
- "status": {
- "icon": "status_success",
- "text": "passed",
- "label": "passed",
- "group": "success",
- "tooltip": "passed",
- "has_details": true,
- "details_path": "/gitlab-org/gitlab-runner/-/jobs/72469033",
- "illustration": {
- "image": "https://assets.gitlab-static.net/assets/illustrations/skipped-job_empty-8b877955fbf175e42ae65b6cb95346e15282c6fc5b682756c329af3a0055225e.svg",
- "size": "svg-430",
- "title": "This job does not have a trace."
- },
- "favicon": "https://gitlab.com/assets/ci_favicons/favicon_status_success-8451333011eee8ce9f2ab25dc487fe24a8758c694827a582f17f42b0a90446a2.png",
- "action": {
- "icon": "retry",
- "title": "Retry",
- "path": "/gitlab-org/gitlab-runner/-/jobs/72469033/retry",
- "method": "post",
- "button_title": "Retry this job"
- }
- }
- }
- ]
- }
- ],
- "status": {
- "icon": "status_success",
- "text": "passed",
- "label": "passed",
- "group": "success",
- "tooltip": "passed",
- "has_details": true,
- "details_path": "/gitlab-org/gitlab-runner/pipelines/23211253#test",
- "illustration": null,
- "favicon": "https://gitlab.com/assets/ci_favicons/favicon_status_success-8451333011eee8ce9f2ab25dc487fe24a8758c694827a582f17f42b0a90446a2.png"
- },
- "path": "/gitlab-org/gitlab-runner/pipelines/23211253#test",
- "dropdown_path": "/gitlab-org/gitlab-runner/pipelines/23211253/stage.json?stage=test"
- },
- {
- "name": "cleanup",
- "title": "cleanup: skipped",
- "groups": [
- {
- "name": "review-docs-cleanup",
- "size": 1,
- "status": {
- "icon": "status_manual",
- "text": "manual",
- "label": "manual stop action",
- "group": "manual",
- "tooltip": "manual action",
- "has_details": true,
- "details_path": "/gitlab-org/gitlab-runner/-/jobs/72469034",
- "illustration": {
- "image": "https://assets.gitlab-static.net/assets/illustrations/manual_action-2b4ca0d1bcfd92aebf33d484e36cbf7a102d007f76b5a0cfea636033a629d601.svg",
- "size": "svg-394",
- "title": "This job requires a manual action",
- "content": "This job depends on a user to trigger its process. Often they are used to deploy code to production environments"
- },
- "favicon": "https://gitlab.com/assets/ci_favicons/favicon_status_manual-829a0804612cef47d9efc1618dba38325483657c847dba0546c3b9f0295bb36c.png",
- "action": {
- "icon": "stop",
- "title": "Stop",
- "path": "/gitlab-org/gitlab-runner/-/jobs/72469034/play",
- "method": "post",
- "button_title": "Stop this environment"
- }
- },
- "jobs": [
- {
- "id": 72469034,
- "name": "review-docs-cleanup",
- "started": null,
- "archived": false,
- "build_path": "/gitlab-org/gitlab-runner/-/jobs/72469034",
- "play_path": "/gitlab-org/gitlab-runner/-/jobs/72469034/play",
- "playable": true,
- "scheduled": false,
- "created_at": "2018-06-05T11:31:30.760Z",
- "updated_at": "2018-06-05T11:31:56.037Z",
- "status": {
- "icon": "status_manual",
- "text": "manual",
- "label": "manual stop action",
- "group": "manual",
- "tooltip": "manual action",
- "has_details": true,
- "details_path": "/gitlab-org/gitlab-runner/-/jobs/72469034",
- "illustration": {
- "image": "https://assets.gitlab-static.net/assets/illustrations/manual_action-2b4ca0d1bcfd92aebf33d484e36cbf7a102d007f76b5a0cfea636033a629d601.svg",
- "size": "svg-394",
- "title": "This job requires a manual action",
- "content": "This job depends on a user to trigger its process. Often they are used to deploy code to production environments"
- },
- "favicon": "https://gitlab.com/assets/ci_favicons/favicon_status_manual-829a0804612cef47d9efc1618dba38325483657c847dba0546c3b9f0295bb36c.png",
- "action": {
- "icon": "stop",
- "title": "Stop",
- "path": "/gitlab-org/gitlab-runner/-/jobs/72469034/play",
- "method": "post",
- "button_title": "Stop this environment"
- }
- }
- }
- ]
- }
- ],
- "status": {
- "icon": "status_skipped",
- "text": "skipped",
- "label": "skipped",
- "group": "skipped",
- "tooltip": "skipped",
- "has_details": true,
- "details_path": "/gitlab-org/gitlab-runner/pipelines/23211253#cleanup",
- "illustration": null,
- "favicon": "https://gitlab.com/assets/ci_favicons/favicon_status_skipped-0b9c5e543588945e8c4ca57786bbf2d0c56631959c9f853300392d0315be829b.png"
- },
- "path": "/gitlab-org/gitlab-runner/pipelines/23211253#cleanup",
- "dropdown_path": "/gitlab-org/gitlab-runner/pipelines/23211253/stage.json?stage=cleanup"
- }
- ],
- "artifacts": [],
- "manual_actions": [
- {
- "name": "review-docs-cleanup",
- "path": "/gitlab-org/gitlab-runner/-/jobs/72469034/play",
- "playable": true,
- "scheduled": false
- },
- {
- "name": "review-docs-deploy",
- "path": "/gitlab-org/gitlab-runner/-/jobs/72469032/play",
- "playable": true,
- "scheduled": false
- }
- ],
- "scheduled_actions": []
- },
- "ref": {
- "name": "docs/add-development-guide-to-readme",
- "path": "/gitlab-org/gitlab-runner/commits/docs/add-development-guide-to-readme",
- "tag": false,
- "branch": true
- },
- "commit": {
- "id": "8083eb0a920572214d0dccedd7981f05d535ad46",
- "short_id": "8083eb0a",
- "title": "Add link to development guide in readme",
- "created_at": "2018-06-05T11:30:48.000Z",
- "parent_ids": ["1d7cf79b5a1a2121b9474ac20d61c1b8f621289d"],
- "message": "Add link to development guide in readme\n\nCloses https://gitlab.com/gitlab-org/gitlab-runner/issues/3122\n",
- "author_name": "Achilleas Pipinellis",
- "author_email": "axil@gitlab.com",
- "authored_date": "2018-06-05T11:30:48.000Z",
- "committer_name": "Achilleas Pipinellis",
- "committer_email": "axil@gitlab.com",
- "committed_date": "2018-06-05T11:30:48.000Z",
- "author": {
- "id": 3585,
- "name": "Achilleas Pipinellis",
- "username": "axil",
- "state": "active",
- "avatar_url": "https://assets.gitlab-static.net/uploads/-/system/user/avatar/3585/avatar.png",
- "web_url": "https://gitlab.com/axil",
- "status_tooltip_html": null,
- "path": "/axil"
- },
- "author_gravatar_url": "https://secure.gravatar.com/avatar/1d37af00eec153a8333a4ce18e9aea41?s=80\u0026d=identicon",
- "commit_url": "https://gitlab.com/gitlab-org/gitlab-runner/commit/8083eb0a920572214d0dccedd7981f05d535ad46",
- "commit_path": "/gitlab-org/gitlab-runner/commit/8083eb0a920572214d0dccedd7981f05d535ad46"
- },
- "triggered_by": {
- "id": 34993051,
- "user": {
- "id": 376774,
- "name": "Alessio Caiazza",
- "username": "nolith",
- "state": "active",
- "avatar_url": "https://assets.gitlab-static.net/uploads/-/system/user/avatar/376774/avatar.png",
- "web_url": "https://gitlab.com/nolith",
- "status_tooltip_html": null,
- "path": "/nolith"
- },
- "active": false,
- "coverage": null,
- "source": "pipeline",
- "path": "/gitlab-com/gitlab-docs/pipelines/34993051",
- "details": {
- "status": {
- "icon": "status_failed",
- "text": "failed",
- "label": "failed",
- "group": "failed",
- "tooltip": "failed",
- "has_details": true,
- "details_path": "/gitlab-com/gitlab-docs/pipelines/34993051",
- "illustration": null,
- "favicon": "https://gitlab.com/assets/ci_favicons/favicon_status_failed-41304d7f7e3828808b0c26771f0309e55296819a9beea3ea9fbf6689d9857c12.png"
- }
- },
- "project": {
- "id": 1794617,
- "name": "GitLab Docs",
- "full_path": "/gitlab-com/gitlab-docs",
- "full_name": "GitLab.com / GitLab Docs"
- }
- },
- "triggered": []
-}
diff --git a/spec/javascripts/pipelines/stores/pipeline_with_triggered_triggered_by.json b/spec/javascripts/pipelines/stores/pipeline_with_triggered_triggered_by.json
deleted file mode 100644
index 2402cbae6c8..00000000000
--- a/spec/javascripts/pipelines/stores/pipeline_with_triggered_triggered_by.json
+++ /dev/null
@@ -1,452 +0,0 @@
-{
- "id": 23211253,
- "user": {
- "id": 3585,
- "name": "Achilleas Pipinellis",
- "username": "axil",
- "state": "active",
- "avatar_url": "https://assets.gitlab-static.net/uploads/-/system/user/avatar/3585/avatar.png",
- "web_url": "https://gitlab.com/axil",
- "status_tooltip_html": "\u003cspan class=\"user-status-emoji has-tooltip\" title=\"\" data-html=\"true\" data-placement=\"top\"\u003e\u003cgl-emoji title=\"trumpet\" data-name=\"trumpet\" data-unicode-version=\"6.0\"\u003e🎺\u003c/gl-emoji\u003e\u003c/span\u003e",
- "path": "/axil"
- },
- "active": false,
- "coverage": null,
- "source": "push",
- "created_at": "2018-06-05T11:31:30.452Z",
- "updated_at": "2018-10-31T16:35:31.305Z",
- "path": "/gitlab-org/gitlab-runner/pipelines/23211253",
- "flags": {
- "latest": false,
- "stuck": false,
- "auto_devops": false,
- "yaml_errors": false,
- "retryable": false,
- "cancelable": false,
- "failure_reason": false
- },
- "details": {
- "status": {
- "icon": "status_success",
- "text": "passed",
- "label": "passed",
- "group": "success",
- "tooltip": "passed",
- "has_details": true,
- "details_path": "/gitlab-org/gitlab-runner/pipelines/23211253",
- "illustration": null,
- "favicon": "https://gitlab.com/assets/ci_favicons/favicon_status_success-8451333011eee8ce9f2ab25dc487fe24a8758c694827a582f17f42b0a90446a2.png"
- },
- "duration": 53,
- "finished_at": "2018-10-31T16:35:31.299Z",
- "stages": [
- {
- "name": "prebuild",
- "title": "prebuild: passed",
- "groups": [
- {
- "name": "review-docs-deploy",
- "size": 1,
- "status": {
- "icon": "status_success",
- "text": "passed",
- "label": "manual play action",
- "group": "success",
- "tooltip": "passed",
- "has_details": true,
- "details_path": "/gitlab-org/gitlab-runner/-/jobs/72469032",
- "illustration": {
- "image": "https://assets.gitlab-static.net/assets/illustrations/manual_action-2b4ca0d1bcfd92aebf33d484e36cbf7a102d007f76b5a0cfea636033a629d601.svg",
- "size": "svg-394",
- "title": "This job requires a manual action",
- "content": "This job depends on a user to trigger its process. Often they are used to deploy code to production environments"
- },
- "favicon": "https://gitlab.com/assets/ci_favicons/favicon_status_success-8451333011eee8ce9f2ab25dc487fe24a8758c694827a582f17f42b0a90446a2.png",
- "action": {
- "icon": "play",
- "title": "Play",
- "path": "/gitlab-org/gitlab-runner/-/jobs/72469032/play",
- "method": "post",
- "button_title": "Trigger this manual action"
- }
- },
- "jobs": [
- {
- "id": 72469032,
- "name": "review-docs-deploy",
- "started": "2018-10-31T16:34:58.778Z",
- "archived": false,
- "build_path": "/gitlab-org/gitlab-runner/-/jobs/72469032",
- "retry_path": "/gitlab-org/gitlab-runner/-/jobs/72469032/retry",
- "play_path": "/gitlab-org/gitlab-runner/-/jobs/72469032/play",
- "playable": true,
- "scheduled": false,
- "created_at": "2018-06-05T11:31:30.495Z",
- "updated_at": "2018-10-31T16:35:31.251Z",
- "status": {
- "icon": "status_success",
- "text": "passed",
- "label": "manual play action",
- "group": "success",
- "tooltip": "passed",
- "has_details": true,
- "details_path": "/gitlab-org/gitlab-runner/-/jobs/72469032",
- "illustration": {
- "image": "https://assets.gitlab-static.net/assets/illustrations/manual_action-2b4ca0d1bcfd92aebf33d484e36cbf7a102d007f76b5a0cfea636033a629d601.svg",
- "size": "svg-394",
- "title": "This job requires a manual action",
- "content": "This job depends on a user to trigger its process. Often they are used to deploy code to production environments"
- },
- "favicon": "https://gitlab.com/assets/ci_favicons/favicon_status_success-8451333011eee8ce9f2ab25dc487fe24a8758c694827a582f17f42b0a90446a2.png",
- "action": {
- "icon": "play",
- "title": "Play",
- "path": "/gitlab-org/gitlab-runner/-/jobs/72469032/play",
- "method": "post",
- "button_title": "Trigger this manual action"
- }
- }
- }
- ]
- }
- ],
- "status": {
- "icon": "status_success",
- "text": "passed",
- "label": "passed",
- "group": "success",
- "tooltip": "passed",
- "has_details": true,
- "details_path": "/gitlab-org/gitlab-runner/pipelines/23211253#prebuild",
- "illustration": null,
- "favicon": "https://gitlab.com/assets/ci_favicons/favicon_status_success-8451333011eee8ce9f2ab25dc487fe24a8758c694827a582f17f42b0a90446a2.png"
- },
- "path": "/gitlab-org/gitlab-runner/pipelines/23211253#prebuild",
- "dropdown_path": "/gitlab-org/gitlab-runner/pipelines/23211253/stage.json?stage=prebuild"
- },
- {
- "name": "test",
- "title": "test: passed",
- "groups": [
- {
- "name": "docs check links",
- "size": 1,
- "status": {
- "icon": "status_success",
- "text": "passed",
- "label": "passed",
- "group": "success",
- "tooltip": "passed",
- "has_details": true,
- "details_path": "/gitlab-org/gitlab-runner/-/jobs/72469033",
- "illustration": {
- "image": "https://assets.gitlab-static.net/assets/illustrations/skipped-job_empty-8b877955fbf175e42ae65b6cb95346e15282c6fc5b682756c329af3a0055225e.svg",
- "size": "svg-430",
- "title": "This job does not have a trace."
- },
- "favicon": "https://gitlab.com/assets/ci_favicons/favicon_status_success-8451333011eee8ce9f2ab25dc487fe24a8758c694827a582f17f42b0a90446a2.png",
- "action": {
- "icon": "retry",
- "title": "Retry",
- "path": "/gitlab-org/gitlab-runner/-/jobs/72469033/retry",
- "method": "post",
- "button_title": "Retry this job"
- }
- },
- "jobs": [
- {
- "id": 72469033,
- "name": "docs check links",
- "started": "2018-06-05T11:31:33.240Z",
- "archived": false,
- "build_path": "/gitlab-org/gitlab-runner/-/jobs/72469033",
- "retry_path": "/gitlab-org/gitlab-runner/-/jobs/72469033/retry",
- "playable": false,
- "scheduled": false,
- "created_at": "2018-06-05T11:31:30.627Z",
- "updated_at": "2018-06-05T11:31:54.363Z",
- "status": {
- "icon": "status_success",
- "text": "passed",
- "label": "passed",
- "group": "success",
- "tooltip": "passed",
- "has_details": true,
- "details_path": "/gitlab-org/gitlab-runner/-/jobs/72469033",
- "illustration": {
- "image": "https://assets.gitlab-static.net/assets/illustrations/skipped-job_empty-8b877955fbf175e42ae65b6cb95346e15282c6fc5b682756c329af3a0055225e.svg",
- "size": "svg-430",
- "title": "This job does not have a trace."
- },
- "favicon": "https://gitlab.com/assets/ci_favicons/favicon_status_success-8451333011eee8ce9f2ab25dc487fe24a8758c694827a582f17f42b0a90446a2.png",
- "action": {
- "icon": "retry",
- "title": "Retry",
- "path": "/gitlab-org/gitlab-runner/-/jobs/72469033/retry",
- "method": "post",
- "button_title": "Retry this job"
- }
- }
- }
- ]
- }
- ],
- "status": {
- "icon": "status_success",
- "text": "passed",
- "label": "passed",
- "group": "success",
- "tooltip": "passed",
- "has_details": true,
- "details_path": "/gitlab-org/gitlab-runner/pipelines/23211253#test",
- "illustration": null,
- "favicon": "https://gitlab.com/assets/ci_favicons/favicon_status_success-8451333011eee8ce9f2ab25dc487fe24a8758c694827a582f17f42b0a90446a2.png"
- },
- "path": "/gitlab-org/gitlab-runner/pipelines/23211253#test",
- "dropdown_path": "/gitlab-org/gitlab-runner/pipelines/23211253/stage.json?stage=test"
- },
- {
- "name": "cleanup",
- "title": "cleanup: skipped",
- "groups": [
- {
- "name": "review-docs-cleanup",
- "size": 1,
- "status": {
- "icon": "status_manual",
- "text": "manual",
- "label": "manual stop action",
- "group": "manual",
- "tooltip": "manual action",
- "has_details": true,
- "details_path": "/gitlab-org/gitlab-runner/-/jobs/72469034",
- "illustration": {
- "image": "https://assets.gitlab-static.net/assets/illustrations/manual_action-2b4ca0d1bcfd92aebf33d484e36cbf7a102d007f76b5a0cfea636033a629d601.svg",
- "size": "svg-394",
- "title": "This job requires a manual action",
- "content": "This job depends on a user to trigger its process. Often they are used to deploy code to production environments"
- },
- "favicon": "https://gitlab.com/assets/ci_favicons/favicon_status_manual-829a0804612cef47d9efc1618dba38325483657c847dba0546c3b9f0295bb36c.png",
- "action": {
- "icon": "stop",
- "title": "Stop",
- "path": "/gitlab-org/gitlab-runner/-/jobs/72469034/play",
- "method": "post",
- "button_title": "Stop this environment"
- }
- },
- "jobs": [
- {
- "id": 72469034,
- "name": "review-docs-cleanup",
- "started": null,
- "archived": false,
- "build_path": "/gitlab-org/gitlab-runner/-/jobs/72469034",
- "play_path": "/gitlab-org/gitlab-runner/-/jobs/72469034/play",
- "playable": true,
- "scheduled": false,
- "created_at": "2018-06-05T11:31:30.760Z",
- "updated_at": "2018-06-05T11:31:56.037Z",
- "status": {
- "icon": "status_manual",
- "text": "manual",
- "label": "manual stop action",
- "group": "manual",
- "tooltip": "manual action",
- "has_details": true,
- "details_path": "/gitlab-org/gitlab-runner/-/jobs/72469034",
- "illustration": {
- "image": "https://assets.gitlab-static.net/assets/illustrations/manual_action-2b4ca0d1bcfd92aebf33d484e36cbf7a102d007f76b5a0cfea636033a629d601.svg",
- "size": "svg-394",
- "title": "This job requires a manual action",
- "content": "This job depends on a user to trigger its process. Often they are used to deploy code to production environments"
- },
- "favicon": "https://gitlab.com/assets/ci_favicons/favicon_status_manual-829a0804612cef47d9efc1618dba38325483657c847dba0546c3b9f0295bb36c.png",
- "action": {
- "icon": "stop",
- "title": "Stop",
- "path": "/gitlab-org/gitlab-runner/-/jobs/72469034/play",
- "method": "post",
- "button_title": "Stop this environment"
- }
- }
- }
- ]
- }
- ],
- "status": {
- "icon": "status_skipped",
- "text": "skipped",
- "label": "skipped",
- "group": "skipped",
- "tooltip": "skipped",
- "has_details": true,
- "details_path": "/gitlab-org/gitlab-runner/pipelines/23211253#cleanup",
- "illustration": null,
- "favicon": "https://gitlab.com/assets/ci_favicons/favicon_status_skipped-0b9c5e543588945e8c4ca57786bbf2d0c56631959c9f853300392d0315be829b.png"
- },
- "path": "/gitlab-org/gitlab-runner/pipelines/23211253#cleanup",
- "dropdown_path": "/gitlab-org/gitlab-runner/pipelines/23211253/stage.json?stage=cleanup"
- }
- ],
- "artifacts": [],
- "manual_actions": [
- {
- "name": "review-docs-cleanup",
- "path": "/gitlab-org/gitlab-runner/-/jobs/72469034/play",
- "playable": true,
- "scheduled": false
- },
- {
- "name": "review-docs-deploy",
- "path": "/gitlab-org/gitlab-runner/-/jobs/72469032/play",
- "playable": true,
- "scheduled": false
- }
- ],
- "scheduled_actions": []
- },
- "ref": {
- "name": "docs/add-development-guide-to-readme",
- "path": "/gitlab-org/gitlab-runner/commits/docs/add-development-guide-to-readme",
- "tag": false,
- "branch": true
- },
- "commit": {
- "id": "8083eb0a920572214d0dccedd7981f05d535ad46",
- "short_id": "8083eb0a",
- "title": "Add link to development guide in readme",
- "created_at": "2018-06-05T11:30:48.000Z",
- "parent_ids": ["1d7cf79b5a1a2121b9474ac20d61c1b8f621289d"],
- "message": "Add link to development guide in readme\n\nCloses https://gitlab.com/gitlab-org/gitlab-runner/issues/3122\n",
- "author_name": "Achilleas Pipinellis",
- "author_email": "axil@gitlab.com",
- "authored_date": "2018-06-05T11:30:48.000Z",
- "committer_name": "Achilleas Pipinellis",
- "committer_email": "axil@gitlab.com",
- "committed_date": "2018-06-05T11:30:48.000Z",
- "author": {
- "id": 3585,
- "name": "Achilleas Pipinellis",
- "username": "axil",
- "state": "active",
- "avatar_url": "https://assets.gitlab-static.net/uploads/-/system/user/avatar/3585/avatar.png",
- "web_url": "https://gitlab.com/axil",
- "status_tooltip_html": null,
- "path": "/axil"
- },
- "author_gravatar_url": "https://secure.gravatar.com/avatar/1d37af00eec153a8333a4ce18e9aea41?s=80\u0026d=identicon",
- "commit_url": "https://gitlab.com/gitlab-org/gitlab-runner/commit/8083eb0a920572214d0dccedd7981f05d535ad46",
- "commit_path": "/gitlab-org/gitlab-runner/commit/8083eb0a920572214d0dccedd7981f05d535ad46"
- },
- "triggered_by": {
- "id": 34993051,
- "user": {
- "id": 376774,
- "name": "Alessio Caiazza",
- "username": "nolith",
- "state": "active",
- "avatar_url": "https://assets.gitlab-static.net/uploads/-/system/user/avatar/376774/avatar.png",
- "web_url": "https://gitlab.com/nolith",
- "status_tooltip_html": null,
- "path": "/nolith"
- },
- "active": false,
- "coverage": null,
- "source": "pipeline",
- "path": "/gitlab-com/gitlab-docs/pipelines/34993051",
- "details": {
- "status": {
- "icon": "status_failed",
- "text": "failed",
- "label": "failed",
- "group": "failed",
- "tooltip": "failed",
- "has_details": true,
- "details_path": "/gitlab-com/gitlab-docs/pipelines/34993051",
- "illustration": null,
- "favicon": "https://gitlab.com/assets/ci_favicons/favicon_status_failed-41304d7f7e3828808b0c26771f0309e55296819a9beea3ea9fbf6689d9857c12.png"
- }
- },
- "project": {
- "id": 1794617,
- "name": "GitLab Docs",
- "full_path": "/gitlab-com/gitlab-docs",
- "full_name": "GitLab.com / GitLab Docs"
- }
- },
- "triggered": [
- {
- "id": 349233051,
- "user": {
- "id": 376774,
- "name": "Alessio Caiazza",
- "username": "nolith",
- "state": "active",
- "avatar_url": "https://assets.gitlab-static.net/uploads/-/system/user/avatar/376774/avatar.png",
- "web_url": "https://gitlab.com/nolith",
- "status_tooltip_html": null,
- "path": "/nolith"
- },
- "active": false,
- "coverage": null,
- "source": "pipeline",
- "path": "/gitlab-com/gitlab-docs/pipelines/34993051",
- "details": {
- "status": {
- "icon": "status_failed",
- "text": "failed",
- "label": "failed",
- "group": "failed",
- "tooltip": "failed",
- "has_details": true,
- "details_path": "/gitlab-com/gitlab-docs/pipelines/349233051",
- "illustration": null,
- "favicon": "https://gitlab.com/assets/ci_favicons/favicon_status_failed-41304d7f7e3828808b0c26771f0309e55296819a9beea3ea9fbf6689d9857c12.png"
- }
- },
- "project": {
- "id": 1794617,
- "name": "GitLab Docs",
- "full_path": "/gitlab-com/gitlab-docs",
- "full_name": "GitLab.com / GitLab Docs"
- }
- },
- {
- "id": 34993023,
- "user": {
- "id": 376774,
- "name": "Alessio Caiazza",
- "username": "nolith",
- "state": "active",
- "avatar_url": "https://assets.gitlab-static.net/uploads/-/system/user/avatar/376774/avatar.png",
- "web_url": "https://gitlab.com/nolith",
- "status_tooltip_html": null,
- "path": "/nolith"
- },
- "active": false,
- "coverage": null,
- "source": "pipeline",
- "path": "/gitlab-com/gitlab-docs/pipelines/34993023",
- "details": {
- "status": {
- "icon": "status_failed",
- "text": "failed",
- "label": "failed",
- "group": "failed",
- "tooltip": "failed",
- "has_details": true,
- "details_path": "/gitlab-com/gitlab-docs/pipelines/34993051",
- "illustration": null,
- "favicon": "https://gitlab.com/assets/ci_favicons/favicon_status_failed-41304d7f7e3828808b0c26771f0309e55296819a9beea3ea9fbf6689d9857c12.png"
- }
- },
- "project": {
- "id": 1794617,
- "name": "GitLab Docs",
- "full_path": "/gitlab-com/gitlab-docs",
- "full_name": "GitLab.com / GitLab Docs"
- }
- }
- ]
-}
diff --git a/spec/javascripts/pipelines/time_ago_spec.js b/spec/javascripts/pipelines/time_ago_spec.js
deleted file mode 100644
index 42b34c82f89..00000000000
--- a/spec/javascripts/pipelines/time_ago_spec.js
+++ /dev/null
@@ -1,64 +0,0 @@
-import Vue from 'vue';
-import timeAgo from '~/pipelines/components/time_ago.vue';
-
-describe('Timeago component', () => {
- let TimeAgo;
- beforeEach(() => {
- TimeAgo = Vue.extend(timeAgo);
- });
-
- describe('with duration', () => {
- it('should render duration and timer svg', () => {
- const component = new TimeAgo({
- propsData: {
- duration: 10,
- finishedTime: '',
- },
- }).$mount();
-
- expect(component.$el.querySelector('.duration')).toBeDefined();
- expect(component.$el.querySelector('.duration svg')).toBeDefined();
- });
- });
-
- describe('without duration', () => {
- it('should not render duration and timer svg', () => {
- const component = new TimeAgo({
- propsData: {
- duration: 0,
- finishedTime: '',
- },
- }).$mount();
-
- expect(component.$el.querySelector('.duration')).toBe(null);
- });
- });
-
- describe('with finishedTime', () => {
- it('should render time and calendar icon', () => {
- const component = new TimeAgo({
- propsData: {
- duration: 0,
- finishedTime: '2017-04-26T12:40:23.277Z',
- },
- }).$mount();
-
- expect(component.$el.querySelector('.finished-at')).toBeDefined();
- expect(component.$el.querySelector('.finished-at i.fa-calendar')).toBeDefined();
- expect(component.$el.querySelector('.finished-at time')).toBeDefined();
- });
- });
-
- describe('without finishedTime', () => {
- it('should not render time and calendar icon', () => {
- const component = new TimeAgo({
- propsData: {
- duration: 0,
- finishedTime: '',
- },
- }).$mount();
-
- expect(component.$el.querySelector('.finished-at')).toBe(null);
- });
- });
-});
diff --git a/spec/javascripts/prometheus_metrics/mock_data.js b/spec/javascripts/prometheus_metrics/mock_data.js
deleted file mode 100644
index 3af56df92e2..00000000000
--- a/spec/javascripts/prometheus_metrics/mock_data.js
+++ /dev/null
@@ -1,41 +0,0 @@
-export const metrics = [
- {
- group: 'Kubernetes',
- priority: 1,
- active_metrics: 4,
- metrics_missing_requirements: 0,
- },
- {
- group: 'HAProxy',
- priority: 2,
- active_metrics: 3,
- metrics_missing_requirements: 0,
- },
- {
- group: 'Apache',
- priority: 3,
- active_metrics: 5,
- metrics_missing_requirements: 0,
- },
-];
-
-export const missingVarMetrics = [
- {
- group: 'Kubernetes',
- priority: 1,
- active_metrics: 4,
- metrics_missing_requirements: 0,
- },
- {
- group: 'HAProxy',
- priority: 2,
- active_metrics: 3,
- metrics_missing_requirements: 1,
- },
- {
- group: 'Apache',
- priority: 3,
- active_metrics: 5,
- metrics_missing_requirements: 3,
- },
-];
diff --git a/spec/javascripts/prometheus_metrics/prometheus_metrics_spec.js b/spec/javascripts/prometheus_metrics/prometheus_metrics_spec.js
deleted file mode 100644
index dca3e1553b9..00000000000
--- a/spec/javascripts/prometheus_metrics/prometheus_metrics_spec.js
+++ /dev/null
@@ -1,178 +0,0 @@
-import MockAdapter from 'axios-mock-adapter';
-import axios from '~/lib/utils/axios_utils';
-import PrometheusMetrics from '~/prometheus_metrics/prometheus_metrics';
-import PANEL_STATE from '~/prometheus_metrics/constants';
-import { metrics, missingVarMetrics } from './mock_data';
-
-describe('PrometheusMetrics', () => {
- const FIXTURE = 'services/prometheus/prometheus_service.html';
- preloadFixtures(FIXTURE);
-
- beforeEach(() => {
- loadFixtures(FIXTURE);
- });
-
- describe('constructor', () => {
- let prometheusMetrics;
-
- beforeEach(() => {
- prometheusMetrics = new PrometheusMetrics('.js-prometheus-metrics-monitoring');
- });
-
- it('should initialize wrapper element refs on class object', () => {
- expect(prometheusMetrics.$wrapper).toBeDefined();
- expect(prometheusMetrics.$monitoredMetricsPanel).toBeDefined();
- expect(prometheusMetrics.$monitoredMetricsCount).toBeDefined();
- expect(prometheusMetrics.$monitoredMetricsLoading).toBeDefined();
- expect(prometheusMetrics.$monitoredMetricsEmpty).toBeDefined();
- expect(prometheusMetrics.$monitoredMetricsList).toBeDefined();
- expect(prometheusMetrics.$missingEnvVarPanel).toBeDefined();
- expect(prometheusMetrics.$panelToggle).toBeDefined();
- expect(prometheusMetrics.$missingEnvVarMetricCount).toBeDefined();
- expect(prometheusMetrics.$missingEnvVarMetricsList).toBeDefined();
- });
-
- it('should initialize metadata on class object', () => {
- expect(prometheusMetrics.backOffRequestCounter).toEqual(0);
- expect(prometheusMetrics.activeMetricsEndpoint).toContain('/test');
- });
- });
-
- describe('showMonitoringMetricsPanelState', () => {
- let prometheusMetrics;
-
- beforeEach(() => {
- prometheusMetrics = new PrometheusMetrics('.js-prometheus-metrics-monitoring');
- });
-
- it('should show loading state when called with `loading`', () => {
- prometheusMetrics.showMonitoringMetricsPanelState(PANEL_STATE.LOADING);
-
- expect(prometheusMetrics.$monitoredMetricsLoading.hasClass('hidden')).toBeFalsy();
- expect(prometheusMetrics.$monitoredMetricsEmpty.hasClass('hidden')).toBeTruthy();
- expect(prometheusMetrics.$monitoredMetricsList.hasClass('hidden')).toBeTruthy();
- });
-
- it('should show metrics list when called with `list`', () => {
- prometheusMetrics.showMonitoringMetricsPanelState(PANEL_STATE.LIST);
-
- expect(prometheusMetrics.$monitoredMetricsLoading.hasClass('hidden')).toBeTruthy();
- expect(prometheusMetrics.$monitoredMetricsEmpty.hasClass('hidden')).toBeTruthy();
- expect(prometheusMetrics.$monitoredMetricsList.hasClass('hidden')).toBeFalsy();
- });
-
- it('should show empty state when called with `empty`', () => {
- prometheusMetrics.showMonitoringMetricsPanelState(PANEL_STATE.EMPTY);
-
- expect(prometheusMetrics.$monitoredMetricsLoading.hasClass('hidden')).toBeTruthy();
- expect(prometheusMetrics.$monitoredMetricsEmpty.hasClass('hidden')).toBeFalsy();
- expect(prometheusMetrics.$monitoredMetricsList.hasClass('hidden')).toBeTruthy();
- });
- });
-
- describe('populateActiveMetrics', () => {
- let prometheusMetrics;
-
- beforeEach(() => {
- prometheusMetrics = new PrometheusMetrics('.js-prometheus-metrics-monitoring');
- });
-
- it('should show monitored metrics list', () => {
- prometheusMetrics.populateActiveMetrics(metrics);
-
- const $metricsListLi = prometheusMetrics.$monitoredMetricsList.find('li');
-
- expect(prometheusMetrics.$monitoredMetricsLoading.hasClass('hidden')).toBeTruthy();
- expect(prometheusMetrics.$monitoredMetricsList.hasClass('hidden')).toBeFalsy();
-
- expect(prometheusMetrics.$monitoredMetricsCount.text()).toEqual(
- '3 exporters with 12 metrics were found',
- );
-
- expect($metricsListLi.length).toEqual(metrics.length);
- expect(
- $metricsListLi
- .first()
- .find('.badge')
- .text(),
- ).toEqual(`${metrics[0].active_metrics}`);
- });
-
- it('should show missing environment variables list', () => {
- prometheusMetrics.populateActiveMetrics(missingVarMetrics);
-
- expect(prometheusMetrics.$monitoredMetricsLoading.hasClass('hidden')).toBeTruthy();
- expect(prometheusMetrics.$missingEnvVarPanel.hasClass('hidden')).toBeFalsy();
-
- expect(prometheusMetrics.$missingEnvVarMetricCount.text()).toEqual('2');
- expect(prometheusMetrics.$missingEnvVarPanel.find('li').length).toEqual(2);
- expect(prometheusMetrics.$missingEnvVarPanel.find('.flash-container')).toBeDefined();
- });
- });
-
- describe('loadActiveMetrics', () => {
- let prometheusMetrics;
- let mock;
-
- function mockSuccess() {
- mock.onGet(prometheusMetrics.activeMetricsEndpoint).reply(200, {
- data: metrics,
- success: true,
- });
- }
-
- function mockError() {
- mock.onGet(prometheusMetrics.activeMetricsEndpoint).networkError();
- }
-
- beforeEach(() => {
- spyOn(axios, 'get').and.callThrough();
-
- prometheusMetrics = new PrometheusMetrics('.js-prometheus-metrics-monitoring');
-
- mock = new MockAdapter(axios);
- });
-
- afterEach(() => {
- mock.restore();
- });
-
- it('should show loader animation while response is being loaded and hide it when request is complete', done => {
- mockSuccess();
-
- prometheusMetrics.loadActiveMetrics();
-
- expect(prometheusMetrics.$monitoredMetricsLoading.hasClass('hidden')).toBeFalsy();
- expect(axios.get).toHaveBeenCalledWith(prometheusMetrics.activeMetricsEndpoint);
-
- setTimeout(() => {
- expect(prometheusMetrics.$monitoredMetricsLoading.hasClass('hidden')).toBeTruthy();
- done();
- });
- });
-
- it('should show empty state if response failed to load', done => {
- mockError();
-
- prometheusMetrics.loadActiveMetrics();
-
- setTimeout(() => {
- expect(prometheusMetrics.$monitoredMetricsLoading.hasClass('hidden')).toBeTruthy();
- expect(prometheusMetrics.$monitoredMetricsEmpty.hasClass('hidden')).toBeFalsy();
- done();
- });
- });
-
- it('should populate metrics list once response is loaded', done => {
- spyOn(prometheusMetrics, 'populateActiveMetrics');
- mockSuccess();
-
- prometheusMetrics.loadActiveMetrics();
-
- setTimeout(() => {
- expect(prometheusMetrics.populateActiveMetrics).toHaveBeenCalledWith(metrics);
- done();
- });
- });
- });
-});
diff --git a/spec/javascripts/related_merge_requests/components/related_merge_requests_spec.js b/spec/javascripts/related_merge_requests/components/related_merge_requests_spec.js
deleted file mode 100644
index d8bdf69dfee..00000000000
--- a/spec/javascripts/related_merge_requests/components/related_merge_requests_spec.js
+++ /dev/null
@@ -1,88 +0,0 @@
-import { mount, createLocalVue } from '@vue/test-utils';
-import MockAdapter from 'axios-mock-adapter';
-import axios from '~/lib/utils/axios_utils';
-import RelatedIssuableItem from '~/vue_shared/components/issue/related_issuable_item.vue';
-import RelatedMergeRequests from '~/related_merge_requests/components/related_merge_requests.vue';
-import createStore from '~/related_merge_requests/store/index';
-
-const FIXTURE_PATH = 'issues/related_merge_requests.json';
-const API_ENDPOINT = '/api/v4/projects/2/issues/33/related_merge_requests';
-const localVue = createLocalVue();
-
-describe('RelatedMergeRequests', () => {
- let wrapper;
- let mock;
- let mockData;
-
- beforeEach(done => {
- loadFixtures(FIXTURE_PATH);
- mockData = getJSONFixture(FIXTURE_PATH);
- mock = new MockAdapter(axios);
- mock.onGet(`${API_ENDPOINT}?per_page=100`).reply(200, mockData, { 'x-total': 2 });
-
- wrapper = mount(localVue.extend(RelatedMergeRequests), {
- localVue,
- store: createStore(),
- propsData: {
- endpoint: API_ENDPOINT,
- projectNamespace: 'gitlab-org',
- projectPath: 'gitlab-ce',
- },
- });
-
- setTimeout(done);
- });
-
- afterEach(() => {
- wrapper.destroy();
- mock.restore();
- });
-
- describe('methods', () => {
- describe('getAssignees', () => {
- const assignees = [{ name: 'foo' }, { name: 'bar' }];
-
- describe('when there is assignees array', () => {
- it('should return assignees array', () => {
- const mr = { assignees };
-
- expect(wrapper.vm.getAssignees(mr)).toEqual(assignees);
- });
- });
-
- it('should return an array with single assingee', () => {
- const mr = { assignee: assignees[0] };
-
- expect(wrapper.vm.getAssignees(mr)).toEqual([assignees[0]]);
- });
-
- it('should return empty array when assignee is not set', () => {
- expect(wrapper.vm.getAssignees({})).toEqual([]);
- expect(wrapper.vm.getAssignees({ assignee: null })).toEqual([]);
- });
- });
- });
-
- describe('template', () => {
- it('should render related merge request items', () => {
- expect(wrapper.find('.js-items-count').text()).toEqual('2');
- expect(wrapper.findAll(RelatedIssuableItem).length).toEqual(2);
-
- const props = wrapper
- .findAll(RelatedIssuableItem)
- .at(1)
- .props();
- const data = mockData[1];
-
- expect(props.idKey).toEqual(data.id);
- expect(props.pathIdSeparator).toEqual('!');
- expect(props.pipelineStatus).toBe(data.head_pipeline.detailed_status);
- expect(props.assignees).toEqual([data.assignee]);
- expect(props.isMergeRequest).toBe(true);
- expect(props.confidential).toEqual(false);
- expect(props.title).toEqual(data.title);
- expect(props.state).toEqual(data.state);
- expect(props.createdAt).toEqual(data.created_at);
- });
- });
-});
diff --git a/spec/javascripts/related_merge_requests/store/actions_spec.js b/spec/javascripts/related_merge_requests/store/actions_spec.js
deleted file mode 100644
index c4cd9f5f803..00000000000
--- a/spec/javascripts/related_merge_requests/store/actions_spec.js
+++ /dev/null
@@ -1,110 +0,0 @@
-import MockAdapter from 'axios-mock-adapter';
-import testAction from 'spec/helpers/vuex_action_helper';
-import axios from '~/lib/utils/axios_utils';
-import * as types from '~/related_merge_requests/store/mutation_types';
-import actionsModule, * as actions from '~/related_merge_requests/store/actions';
-
-describe('RelatedMergeRequest store actions', () => {
- let state;
- let flashSpy;
- let mock;
-
- beforeEach(() => {
- state = {
- apiEndpoint: '/api/related_merge_requests',
- };
- flashSpy = spyOnDependency(actionsModule, 'createFlash');
- mock = new MockAdapter(axios);
- });
-
- afterEach(() => {
- mock.restore();
- });
-
- describe('setInitialState', () => {
- it('commits types.SET_INITIAL_STATE with given props', done => {
- const props = { a: 1, b: 2 };
-
- testAction(
- actions.setInitialState,
- props,
- {},
- [{ type: types.SET_INITIAL_STATE, payload: props }],
- [],
- done,
- );
- });
- });
-
- describe('requestData', () => {
- it('commits types.REQUEST_DATA', done => {
- testAction(actions.requestData, null, {}, [{ type: types.REQUEST_DATA }], [], done);
- });
- });
-
- describe('receiveDataSuccess', () => {
- it('commits types.RECEIVE_DATA_SUCCESS with data', done => {
- const data = { a: 1, b: 2 };
-
- testAction(
- actions.receiveDataSuccess,
- data,
- {},
- [{ type: types.RECEIVE_DATA_SUCCESS, payload: data }],
- [],
- done,
- );
- });
- });
-
- describe('receiveDataError', () => {
- it('commits types.RECEIVE_DATA_ERROR', done => {
- testAction(
- actions.receiveDataError,
- null,
- {},
- [{ type: types.RECEIVE_DATA_ERROR }],
- [],
- done,
- );
- });
- });
-
- describe('fetchMergeRequests', () => {
- describe('for a successful request', () => {
- it('should dispatch success action', done => {
- const data = { a: 1 };
- mock.onGet(`${state.apiEndpoint}?per_page=100`).replyOnce(200, data, { 'x-total': 2 });
-
- testAction(
- actions.fetchMergeRequests,
- null,
- state,
- [],
- [{ type: 'requestData' }, { type: 'receiveDataSuccess', payload: { data, total: 2 } }],
- done,
- );
- });
- });
-
- describe('for a failing request', () => {
- it('should dispatch error action', done => {
- mock.onGet(`${state.apiEndpoint}?per_page=100`).replyOnce(400);
-
- testAction(
- actions.fetchMergeRequests,
- null,
- state,
- [],
- [{ type: 'requestData' }, { type: 'receiveDataError' }],
- () => {
- expect(flashSpy).toHaveBeenCalledTimes(1);
- expect(flashSpy).toHaveBeenCalledWith(jasmine.stringMatching('Something went wrong'));
-
- done();
- },
- );
- });
- });
- });
-});
diff --git a/spec/javascripts/reports/components/grouped_test_reports_app_spec.js b/spec/javascripts/reports/components/grouped_test_reports_app_spec.js
deleted file mode 100644
index bafc47c952a..00000000000
--- a/spec/javascripts/reports/components/grouped_test_reports_app_spec.js
+++ /dev/null
@@ -1,239 +0,0 @@
-import Vue from 'vue';
-import MockAdapter from 'axios-mock-adapter';
-import axios from '~/lib/utils/axios_utils';
-import state from '~/reports/store/state';
-import component from '~/reports/components/grouped_test_reports_app.vue';
-import mountComponent from '../../helpers/vue_mount_component_helper';
-import newFailedTestReports from '../mock_data/new_failures_report.json';
-import newErrorsTestReports from '../mock_data/new_errors_report.json';
-import successTestReports from '../mock_data/no_failures_report.json';
-import mixedResultsTestReports from '../mock_data/new_and_fixed_failures_report.json';
-import resolvedFailures from '../mock_data/resolved_failures.json';
-
-describe('Grouped Test Reports App', () => {
- let vm;
- let mock;
- const Component = Vue.extend(component);
-
- beforeEach(() => {
- mock = new MockAdapter(axios);
- });
-
- afterEach(() => {
- vm.$store.replaceState(state());
- vm.$destroy();
- mock.restore();
- });
-
- describe('with success result', () => {
- beforeEach(() => {
- mock.onGet('test_results.json').reply(200, successTestReports, {});
- vm = mountComponent(Component, {
- endpoint: 'test_results.json',
- });
- });
-
- it('renders success summary text', done => {
- setTimeout(() => {
- expect(vm.$el.querySelector('.gl-spinner')).toBeNull();
- expect(vm.$el.querySelector('.js-code-text').textContent.trim()).toEqual(
- 'Test summary contained no changed test results out of 11 total tests',
- );
-
- expect(vm.$el.textContent).toContain(
- 'rspec:pg found no changed test results out of 8 total tests',
- );
-
- expect(vm.$el.textContent).toContain(
- 'java ant found no changed test results out of 3 total tests',
- );
- done();
- }, 0);
- });
- });
-
- describe('with 204 result', () => {
- beforeEach(() => {
- mock.onGet('test_results.json').reply(204, {}, {});
- vm = mountComponent(Component, {
- endpoint: 'test_results.json',
- });
- });
-
- it('renders success summary text', done => {
- setTimeout(() => {
- expect(vm.$el.querySelector('.gl-spinner')).not.toBeNull();
- expect(vm.$el.querySelector('.js-code-text').textContent.trim()).toEqual(
- 'Test summary results are being parsed',
- );
-
- done();
- }, 0);
- });
- });
-
- describe('with new failed result', () => {
- beforeEach(() => {
- mock.onGet('test_results.json').reply(200, newFailedTestReports, {});
- vm = mountComponent(Component, {
- endpoint: 'test_results.json',
- });
- });
-
- it('renders failed summary text + new badge', done => {
- setTimeout(() => {
- expect(vm.$el.querySelector('.gl-spinner')).toBeNull();
- expect(vm.$el.querySelector('.js-code-text').textContent.trim()).toEqual(
- 'Test summary contained 2 failed out of 11 total tests',
- );
-
- expect(vm.$el.textContent).toContain('rspec:pg found 2 failed out of 8 total tests');
-
- expect(vm.$el.textContent).toContain('New');
- expect(vm.$el.textContent).toContain(
- 'java ant found no changed test results out of 3 total tests',
- );
- done();
- }, 0);
- });
- });
-
- describe('with new error result', () => {
- beforeEach(() => {
- mock.onGet('test_results.json').reply(200, newErrorsTestReports, {});
- vm = mountComponent(Component, {
- endpoint: 'test_results.json',
- });
- });
-
- it('renders error summary text + new badge', done => {
- setTimeout(() => {
- expect(vm.$el.querySelector('.gl-spinner')).toBeNull();
- expect(vm.$el.querySelector('.js-code-text').textContent.trim()).toEqual(
- 'Test summary contained 2 errors out of 11 total tests',
- );
-
- expect(vm.$el.textContent).toContain('karma found 2 errors out of 3 total tests');
-
- expect(vm.$el.textContent).toContain('New');
- expect(vm.$el.textContent).toContain(
- 'rspec:pg found no changed test results out of 8 total tests',
- );
- done();
- }, 0);
- });
- });
-
- describe('with mixed results', () => {
- beforeEach(() => {
- mock.onGet('test_results.json').reply(200, mixedResultsTestReports, {});
- vm = mountComponent(Component, {
- endpoint: 'test_results.json',
- });
- });
-
- it('renders summary text', done => {
- setTimeout(() => {
- expect(vm.$el.querySelector('.gl-spinner')).toBeNull();
- expect(vm.$el.querySelector('.js-code-text').textContent.trim()).toEqual(
- 'Test summary contained 2 failed and 2 fixed test results out of 11 total tests',
- );
-
- expect(vm.$el.textContent).toContain(
- 'rspec:pg found 1 failed and 2 fixed test results out of 8 total tests',
- );
-
- expect(vm.$el.textContent).toContain('New');
- expect(vm.$el.textContent).toContain(' java ant found 1 failed out of 3 total tests');
- done();
- }, 0);
- });
- });
-
- describe('with resolved failures and resolved errors', () => {
- beforeEach(() => {
- mock.onGet('test_results.json').reply(200, resolvedFailures, {});
- vm = mountComponent(Component, {
- endpoint: 'test_results.json',
- });
- });
-
- it('renders summary text', done => {
- setTimeout(() => {
- expect(vm.$el.querySelector('.gl-spinner')).toBeNull();
- expect(vm.$el.querySelector('.js-code-text').textContent.trim()).toEqual(
- 'Test summary contained 4 fixed test results out of 11 total tests',
- );
-
- expect(vm.$el.textContent).toContain(
- 'rspec:pg found 4 fixed test results out of 8 total tests',
- );
- done();
- }, 0);
- });
-
- it('renders resolved failures', done => {
- setTimeout(() => {
- expect(vm.$el.querySelector('.report-block-container').textContent).toContain(
- resolvedFailures.suites[0].resolved_failures[0].name,
- );
-
- expect(vm.$el.querySelector('.report-block-container').textContent).toContain(
- resolvedFailures.suites[0].resolved_failures[1].name,
- );
- done();
- }, 0);
- });
-
- it('renders resolved errors', done => {
- setTimeout(() => {
- expect(vm.$el.querySelector('.report-block-container').textContent).toContain(
- resolvedFailures.suites[0].resolved_errors[0].name,
- );
-
- expect(vm.$el.querySelector('.report-block-container').textContent).toContain(
- resolvedFailures.suites[0].resolved_errors[1].name,
- );
- done();
- }, 0);
- });
- });
-
- describe('with error', () => {
- beforeEach(() => {
- mock.onGet('test_results.json').reply(500, {}, {});
- vm = mountComponent(Component, {
- endpoint: 'test_results.json',
- });
- });
-
- it('renders loading summary text with loading icon', done => {
- setTimeout(() => {
- expect(vm.$el.querySelector('.js-code-text').textContent.trim()).toEqual(
- 'Test summary failed loading results',
- );
- done();
- }, 0);
- });
- });
-
- describe('while loading', () => {
- beforeEach(() => {
- mock.onGet('test_results.json').reply(200, {}, {});
- vm = mountComponent(Component, {
- endpoint: 'test_results.json',
- });
- });
-
- it('renders loading summary text with loading icon', done => {
- expect(vm.$el.querySelector('.gl-spinner')).not.toBeNull();
- expect(vm.$el.querySelector('.js-code-text').textContent.trim()).toEqual(
- 'Test summary results are being parsed',
- );
-
- setTimeout(() => {
- done();
- }, 0);
- });
- });
-});
diff --git a/spec/javascripts/reports/components/modal_open_name_spec.js b/spec/javascripts/reports/components/modal_open_name_spec.js
deleted file mode 100644
index ae1fb2bf187..00000000000
--- a/spec/javascripts/reports/components/modal_open_name_spec.js
+++ /dev/null
@@ -1,47 +0,0 @@
-import Vue from 'vue';
-import Vuex from 'vuex';
-import { mountComponentWithStore } from 'spec/helpers/vue_mount_component_helper';
-import component from '~/reports/components/modal_open_name.vue';
-
-Vue.use(Vuex);
-
-describe('Modal open name', () => {
- const Component = Vue.extend(component);
- let vm;
-
- const store = new Vuex.Store({
- actions: {
- openModal: () => {},
- },
- state: {},
- mutations: {},
- });
-
- beforeEach(() => {
- vm = mountComponentWithStore(Component, {
- store,
- props: {
- issue: {
- title: 'Issue',
- },
- status: 'failed',
- },
- });
- });
-
- afterEach(() => {
- vm.$destroy();
- });
-
- it('renders the issue name', () => {
- expect(vm.$el.textContent.trim()).toEqual('Issue');
- });
-
- it('calls openModal actions when button is clicked', () => {
- spyOn(vm, 'openModal');
-
- vm.$el.click();
-
- expect(vm.openModal).toHaveBeenCalled();
- });
-});
diff --git a/spec/javascripts/reports/components/summary_row_spec.js b/spec/javascripts/reports/components/summary_row_spec.js
deleted file mode 100644
index a19fbad403c..00000000000
--- a/spec/javascripts/reports/components/summary_row_spec.js
+++ /dev/null
@@ -1,37 +0,0 @@
-import Vue from 'vue';
-import mountComponent from 'spec/helpers/vue_mount_component_helper';
-import component from '~/reports/components/summary_row.vue';
-
-describe('Summary row', () => {
- const Component = Vue.extend(component);
- let vm;
-
- const props = {
- summary: 'SAST detected 1 new vulnerability and 1 fixed vulnerability',
- popoverOptions: {
- title: 'Static Application Security Testing (SAST)',
- content: '<a>Learn more about SAST</a>',
- },
- statusIcon: 'warning',
- };
-
- beforeEach(() => {
- vm = mountComponent(Component, props);
- });
-
- afterEach(() => {
- vm.$destroy();
- });
-
- it('renders provided summary', () => {
- expect(
- vm.$el.querySelector('.report-block-list-issue-description-text').textContent.trim(),
- ).toEqual(props.summary);
- });
-
- it('renders provided icon', () => {
- expect(vm.$el.querySelector('.report-block-list-icon span').classList).toContain(
- 'js-ci-status-icon-warning',
- );
- });
-});
diff --git a/spec/javascripts/reports/components/test_issue_body_spec.js b/spec/javascripts/reports/components/test_issue_body_spec.js
deleted file mode 100644
index 9c1cec4c9bc..00000000000
--- a/spec/javascripts/reports/components/test_issue_body_spec.js
+++ /dev/null
@@ -1,72 +0,0 @@
-import Vue from 'vue';
-import component from '~/reports/components/test_issue_body.vue';
-import createStore from '~/reports/store';
-import { mountComponentWithStore } from '../../helpers/vue_mount_component_helper';
-import { trimText } from '../../helpers/text_helper';
-import { issue } from '../mock_data/mock_data';
-
-describe('Test Issue body', () => {
- let vm;
- const Component = Vue.extend(component);
- const store = createStore();
-
- const commonProps = {
- issue,
- status: 'failed',
- };
-
- afterEach(() => {
- vm.$destroy();
- });
-
- describe('on click', () => {
- it('calls openModal action', () => {
- vm = mountComponentWithStore(Component, {
- store,
- props: commonProps,
- });
-
- spyOn(vm, 'openModal');
-
- vm.$el.querySelector('button').click();
-
- expect(vm.openModal).toHaveBeenCalledWith({
- issue: commonProps.issue,
- });
- });
- });
-
- describe('is new', () => {
- beforeEach(() => {
- vm = mountComponentWithStore(Component, {
- store,
- props: Object.assign({}, commonProps, { isNew: true }),
- });
- });
-
- it('renders issue name', () => {
- expect(vm.$el.textContent).toContain(commonProps.issue.name);
- });
-
- it('renders new badge', () => {
- expect(trimText(vm.$el.querySelector('.badge').textContent)).toEqual('New');
- });
- });
-
- describe('not new', () => {
- beforeEach(() => {
- vm = mountComponentWithStore(Component, {
- store,
- props: commonProps,
- });
- });
-
- it('renders issue name', () => {
- expect(vm.$el.textContent).toContain(commonProps.issue.name);
- });
-
- it('does not renders new badge', () => {
- expect(vm.$el.querySelector('.badge')).toEqual(null);
- });
- });
-});
diff --git a/spec/javascripts/reports/mock_data/mock_data.js b/spec/javascripts/reports/mock_data/mock_data.js
deleted file mode 100644
index 0d90253bad2..00000000000
--- a/spec/javascripts/reports/mock_data/mock_data.js
+++ /dev/null
@@ -1,8 +0,0 @@
-// eslint-disable-next-line import/prefer-default-export
-export const issue = {
- result: 'failure',
- name: 'Test#sum when a is 1 and b is 2 returns summary',
- execution_time: 0.009411,
- system_output:
- "Failure/Error: is_expected.to eq(3)\n\n expected: 3\n got: -1\n\n (compared using ==)\n./spec/test_spec.rb:12:in `block (4 levels) in \u003ctop (required)\u003e'",
-};
diff --git a/spec/javascripts/reports/store/actions_spec.js b/spec/javascripts/reports/store/actions_spec.js
deleted file mode 100644
index 18fdb179597..00000000000
--- a/spec/javascripts/reports/store/actions_spec.js
+++ /dev/null
@@ -1,171 +0,0 @@
-import MockAdapter from 'axios-mock-adapter';
-import testAction from 'spec/helpers/vuex_action_helper';
-import { TEST_HOST } from 'spec/test_constants';
-import axios from '~/lib/utils/axios_utils';
-import {
- setEndpoint,
- requestReports,
- fetchReports,
- stopPolling,
- clearEtagPoll,
- receiveReportsSuccess,
- receiveReportsError,
- openModal,
- setModalData,
-} from '~/reports/store/actions';
-import state from '~/reports/store/state';
-import * as types from '~/reports/store/mutation_types';
-
-describe('Reports Store Actions', () => {
- let mockedState;
-
- beforeEach(() => {
- mockedState = state();
- });
-
- describe('setEndpoint', () => {
- it('should commit SET_ENDPOINT mutation', done => {
- testAction(
- setEndpoint,
- 'endpoint.json',
- mockedState,
- [{ type: types.SET_ENDPOINT, payload: 'endpoint.json' }],
- [],
- done,
- );
- });
- });
-
- describe('requestReports', () => {
- it('should commit REQUEST_REPORTS mutation', done => {
- testAction(requestReports, null, mockedState, [{ type: types.REQUEST_REPORTS }], [], done);
- });
- });
-
- describe('fetchReports', () => {
- let mock;
-
- beforeEach(() => {
- mockedState.endpoint = `${TEST_HOST}/endpoint.json`;
- mock = new MockAdapter(axios);
- });
-
- afterEach(() => {
- mock.restore();
- stopPolling();
- clearEtagPoll();
- });
-
- describe('success', () => {
- it('dispatches requestReports and receiveReportsSuccess ', done => {
- mock
- .onGet(`${TEST_HOST}/endpoint.json`)
- .replyOnce(200, { summary: {}, suites: [{ name: 'rspec' }] });
-
- testAction(
- fetchReports,
- null,
- mockedState,
- [],
- [
- {
- type: 'requestReports',
- },
- {
- payload: { data: { summary: {}, suites: [{ name: 'rspec' }] }, status: 200 },
- type: 'receiveReportsSuccess',
- },
- ],
- done,
- );
- });
- });
-
- describe('error', () => {
- beforeEach(() => {
- mock.onGet(`${TEST_HOST}/endpoint.json`).reply(500);
- });
-
- it('dispatches requestReports and receiveReportsError ', done => {
- testAction(
- fetchReports,
- null,
- mockedState,
- [],
- [
- {
- type: 'requestReports',
- },
- {
- type: 'receiveReportsError',
- },
- ],
- done,
- );
- });
- });
- });
-
- describe('receiveReportsSuccess', () => {
- it('should commit RECEIVE_REPORTS_SUCCESS mutation with 200', done => {
- testAction(
- receiveReportsSuccess,
- { data: { summary: {} }, status: 200 },
- mockedState,
- [{ type: types.RECEIVE_REPORTS_SUCCESS, payload: { summary: {} } }],
- [],
- done,
- );
- });
-
- it('should not commit RECEIVE_REPORTS_SUCCESS mutation with 204', done => {
- testAction(
- receiveReportsSuccess,
- { data: { summary: {} }, status: 204 },
- mockedState,
- [],
- [],
- done,
- );
- });
- });
-
- describe('receiveReportsError', () => {
- it('should commit RECEIVE_REPORTS_ERROR mutation', done => {
- testAction(
- receiveReportsError,
- null,
- mockedState,
- [{ type: types.RECEIVE_REPORTS_ERROR }],
- [],
- done,
- );
- });
- });
-
- describe('openModal', () => {
- it('should dispatch setModalData', done => {
- testAction(
- openModal,
- { name: 'foo' },
- mockedState,
- [],
- [{ type: 'setModalData', payload: { name: 'foo' } }],
- done,
- );
- });
- });
-
- describe('setModalData', () => {
- it('should commit SET_ISSUE_MODAL_DATA', done => {
- testAction(
- setModalData,
- { name: 'foo' },
- mockedState,
- [{ type: types.SET_ISSUE_MODAL_DATA, payload: { name: 'foo' } }],
- [],
- done,
- );
- });
- });
-});
diff --git a/spec/javascripts/search_autocomplete_spec.js b/spec/javascripts/search_autocomplete_spec.js
index e9bc1fc51e8..4f42d4880e8 100644
--- a/spec/javascripts/search_autocomplete_spec.js
+++ b/spec/javascripts/search_autocomplete_spec.js
@@ -188,4 +188,28 @@ describe('Search autocomplete dropdown', () => {
// example) on JavaScript-created keypresses.
expect(submitSpy).not.toHaveBeenTriggered();
});
+
+ describe('disableAutocomplete', function() {
+ beforeEach(function() {
+ widget.enableAutocomplete();
+ });
+
+ it('should close the Dropdown', function() {
+ const toggleSpy = spyOn(widget.dropdownToggle, 'dropdown');
+
+ widget.dropdown.addClass('show');
+ widget.disableAutocomplete();
+
+ expect(toggleSpy).toHaveBeenCalledWith('toggle');
+ });
+ });
+
+ describe('enableAutocomplete', function() {
+ it('should open the Dropdown', function() {
+ const toggleSpy = spyOn(widget.dropdownToggle, 'dropdown');
+ widget.enableAutocomplete();
+
+ expect(toggleSpy).toHaveBeenCalledWith('toggle');
+ });
+ });
});
diff --git a/spec/javascripts/sidebar/components/time_tracking/time_tracker_spec.js b/spec/javascripts/sidebar/components/time_tracking/time_tracker_spec.js
deleted file mode 100644
index 1580f32cfca..00000000000
--- a/spec/javascripts/sidebar/components/time_tracking/time_tracker_spec.js
+++ /dev/null
@@ -1,278 +0,0 @@
-import Vue from 'vue';
-
-import mountComponent from 'spec/helpers/vue_mount_component_helper';
-import TimeTracker from '~/sidebar/components/time_tracking/time_tracker.vue';
-
-describe('Issuable Time Tracker', () => {
- let initialData;
- let vm;
-
- const initTimeTrackingComponent = ({
- timeEstimate,
- timeSpent,
- timeEstimateHumanReadable,
- timeSpentHumanReadable,
- limitToHours,
- }) => {
- setFixtures(`
- <div>
- <div id="mock-container"></div>
- </div>
- `);
-
- initialData = {
- timeEstimate,
- timeSpent,
- humanTimeEstimate: timeEstimateHumanReadable,
- humanTimeSpent: timeSpentHumanReadable,
- limitToHours: Boolean(limitToHours),
- rootPath: '/',
- };
-
- const TimeTrackingComponent = Vue.extend({
- ...TimeTracker,
- components: {
- ...TimeTracker.components,
- transition: {
- // disable animations
- template: '<div><slot></slot></div>',
- },
- },
- });
- vm = mountComponent(TimeTrackingComponent, initialData, '#mock-container');
- };
-
- afterEach(() => {
- vm.$destroy();
- });
-
- describe('Initialization', () => {
- beforeEach(() => {
- initTimeTrackingComponent({
- timeEstimate: 10000, // 2h 46m
- timeSpent: 5000, // 1h 23m
- timeEstimateHumanReadable: '2h 46m',
- timeSpentHumanReadable: '1h 23m',
- });
- });
-
- it('should return something defined', () => {
- expect(vm).toBeDefined();
- });
-
- it('should correctly set timeEstimate', done => {
- Vue.nextTick(() => {
- expect(vm.timeEstimate).toBe(initialData.timeEstimate);
- done();
- });
- });
-
- it('should correctly set time_spent', done => {
- Vue.nextTick(() => {
- expect(vm.timeSpent).toBe(initialData.timeSpent);
- done();
- });
- });
- });
-
- describe('Content Display', () => {
- describe('Panes', () => {
- describe('Comparison pane', () => {
- beforeEach(() => {
- initTimeTrackingComponent({
- timeEstimate: 100000, // 1d 3h
- timeSpent: 5000, // 1h 23m
- timeEstimateHumanReadable: '1d 3h',
- timeSpentHumanReadable: '1h 23m',
- });
- });
-
- it('should show the "Comparison" pane when timeEstimate and time_spent are truthy', done => {
- Vue.nextTick(() => {
- expect(vm.showComparisonState).toBe(true);
- const $comparisonPane = vm.$el.querySelector('.time-tracking-comparison-pane');
-
- expect($comparisonPane).toBeVisible();
- done();
- });
- });
-
- it('should show full times when the sidebar is collapsed', done => {
- Vue.nextTick(() => {
- const timeTrackingText = vm.$el.querySelector('.time-tracking-collapsed-summary span')
- .innerText;
-
- expect(timeTrackingText).toBe('1h 23m / 1d 3h');
- done();
- });
- });
-
- describe('Remaining meter', () => {
- it('should display the remaining meter with the correct width', done => {
- Vue.nextTick(() => {
- expect(
- vm.$el.querySelector('.time-tracking-comparison-pane .progress[value="5"]'),
- ).not.toBeNull();
- done();
- });
- });
-
- it('should display the remaining meter with the correct background color when within estimate', done => {
- Vue.nextTick(() => {
- expect(
- vm.$el.querySelector('.time-tracking-comparison-pane .progress[variant="primary"]'),
- ).not.toBeNull();
- done();
- });
- });
-
- it('should display the remaining meter with the correct background color when over estimate', done => {
- vm.timeEstimate = 10000; // 2h 46m
- vm.timeSpent = 20000000; // 231 days
- Vue.nextTick(() => {
- expect(
- vm.$el.querySelector('.time-tracking-comparison-pane .progress[variant="danger"]'),
- ).not.toBeNull();
- done();
- });
- });
- });
- });
-
- describe('Comparison pane when limitToHours is true', () => {
- beforeEach(() => {
- initTimeTrackingComponent({
- timeEstimate: 100000, // 1d 3h
- timeSpent: 5000, // 1h 23m
- timeEstimateHumanReadable: '',
- timeSpentHumanReadable: '',
- limitToHours: true,
- });
- });
-
- it('should show the correct tooltip text', done => {
- Vue.nextTick(() => {
- expect(vm.showComparisonState).toBe(true);
- const $title = vm.$el.querySelector('.time-tracking-content .compare-meter').dataset
- .originalTitle;
-
- expect($title).toBe('Time remaining: 26h 23m');
- done();
- });
- });
- });
-
- describe('Estimate only pane', () => {
- beforeEach(() => {
- initTimeTrackingComponent({
- timeEstimate: 10000, // 2h 46m
- timeSpent: 0,
- timeEstimateHumanReadable: '2h 46m',
- timeSpentHumanReadable: '',
- });
- });
-
- it('should display the human readable version of time estimated', done => {
- Vue.nextTick(() => {
- const estimateText = vm.$el.querySelector('.time-tracking-estimate-only-pane')
- .innerText;
- const correctText = 'Estimated: 2h 46m';
-
- expect(estimateText).toBe(correctText);
- done();
- });
- });
- });
-
- describe('Spent only pane', () => {
- beforeEach(() => {
- initTimeTrackingComponent({
- timeEstimate: 0,
- timeSpent: 5000, // 1h 23m
- timeEstimateHumanReadable: '2h 46m',
- timeSpentHumanReadable: '1h 23m',
- });
- });
-
- it('should display the human readable version of time spent', done => {
- Vue.nextTick(() => {
- const spentText = vm.$el.querySelector('.time-tracking-spend-only-pane').innerText;
- const correctText = 'Spent: 1h 23m';
-
- expect(spentText).toBe(correctText);
- done();
- });
- });
- });
-
- describe('No time tracking pane', () => {
- beforeEach(() => {
- initTimeTrackingComponent({
- timeEstimate: 0,
- timeSpent: 0,
- timeEstimateHumanReadable: '',
- timeSpentHumanReadable: '',
- });
- });
-
- it('should only show the "No time tracking" pane when both timeEstimate and time_spent are falsey', done => {
- Vue.nextTick(() => {
- const $noTrackingPane = vm.$el.querySelector('.time-tracking-no-tracking-pane');
- const noTrackingText = $noTrackingPane.innerText;
- const correctText = 'No estimate or time spent';
-
- expect(vm.showNoTimeTrackingState).toBe(true);
- expect($noTrackingPane).toBeVisible();
- expect(noTrackingText).toBe(correctText);
- done();
- });
- });
- });
-
- describe('Help pane', () => {
- const helpButton = () => vm.$el.querySelector('.help-button');
- const closeHelpButton = () => vm.$el.querySelector('.close-help-button');
- const helpPane = () => vm.$el.querySelector('.time-tracking-help-state');
-
- beforeEach(done => {
- initTimeTrackingComponent({ timeEstimate: 0, timeSpent: 0 });
-
- Vue.nextTick()
- .then(done)
- .catch(done.fail);
- });
-
- it('should not show the "Help" pane by default', () => {
- expect(vm.showHelpState).toBe(false);
- expect(helpPane()).toBeNull();
- });
-
- it('should show the "Help" pane when help button is clicked', done => {
- helpButton().click();
-
- Vue.nextTick()
- .then(() => {
- expect(vm.showHelpState).toBe(true);
- expect(helpPane()).toBeVisible();
- })
- .then(done)
- .catch(done.fail);
- });
-
- it('should not show the "Help" pane when help button is clicked and then closed', done => {
- helpButton().click();
-
- Vue.nextTick()
- .then(() => closeHelpButton().click())
- .then(() => Vue.nextTick())
- .then(() => {
- expect(vm.showHelpState).toBe(false);
- expect(helpPane()).toBeNull();
- })
- .then(done)
- .catch(done.fail);
- });
- });
- });
- });
-});
diff --git a/spec/javascripts/sidebar/lock/edit_form_buttons_spec.js b/spec/javascripts/sidebar/lock/edit_form_buttons_spec.js
deleted file mode 100644
index c532554efb4..00000000000
--- a/spec/javascripts/sidebar/lock/edit_form_buttons_spec.js
+++ /dev/null
@@ -1,32 +0,0 @@
-import Vue from 'vue';
-import mountComponent from 'spec/helpers/vue_mount_component_helper';
-import editFormButtons from '~/sidebar/components/lock/edit_form_buttons.vue';
-
-describe('EditFormButtons', () => {
- let vm1;
- let vm2;
-
- beforeEach(() => {
- const Component = Vue.extend(editFormButtons);
- const toggleForm = () => {};
- const updateLockedAttribute = () => {};
-
- vm1 = mountComponent(Component, {
- isLocked: true,
- toggleForm,
- updateLockedAttribute,
- });
-
- vm2 = mountComponent(Component, {
- isLocked: false,
- toggleForm,
- updateLockedAttribute,
- });
- });
-
- it('renders unlock or lock text based on locked state', () => {
- expect(vm1.$el.innerHTML.includes('Unlock')).toBe(true);
-
- expect(vm2.$el.innerHTML.includes('Lock')).toBe(true);
- });
-});
diff --git a/spec/javascripts/sidebar/lock/lock_issue_sidebar_spec.js b/spec/javascripts/sidebar/lock/lock_issue_sidebar_spec.js
deleted file mode 100644
index 5296908afe2..00000000000
--- a/spec/javascripts/sidebar/lock/lock_issue_sidebar_spec.js
+++ /dev/null
@@ -1,99 +0,0 @@
-import Vue from 'vue';
-import { mockTracking, triggerEvent } from 'spec/helpers/tracking_helper';
-import lockIssueSidebar from '~/sidebar/components/lock/lock_issue_sidebar.vue';
-
-describe('LockIssueSidebar', () => {
- let vm1;
- let vm2;
-
- beforeEach(() => {
- const Component = Vue.extend(lockIssueSidebar);
-
- const mediator = {
- service: {
- update: Promise.resolve(true),
- },
-
- store: {
- isLockDialogOpen: false,
- },
- };
-
- vm1 = new Component({
- propsData: {
- isLocked: true,
- isEditable: true,
- mediator,
- issuableType: 'issue',
- },
- }).$mount();
-
- vm2 = new Component({
- propsData: {
- isLocked: false,
- isEditable: false,
- mediator,
- issuableType: 'merge_request',
- },
- }).$mount();
- });
-
- it('shows if locked and/or editable', () => {
- expect(vm1.$el.innerHTML.includes('Edit')).toBe(true);
-
- expect(vm1.$el.innerHTML.includes('Locked')).toBe(true);
-
- expect(vm2.$el.innerHTML.includes('Unlocked')).toBe(true);
- });
-
- it('displays the edit form when editable', done => {
- expect(vm1.isLockDialogOpen).toBe(false);
-
- vm1.$el.querySelector('.lock-edit').click();
-
- expect(vm1.isLockDialogOpen).toBe(true);
-
- vm1.$nextTick(() => {
- expect(vm1.$el.innerHTML.includes('Unlock this issue?')).toBe(true);
-
- done();
- });
- });
-
- it('tracks an event when "Edit" is clicked', () => {
- const spy = mockTracking('_category_', vm1.$el, spyOn);
- triggerEvent('.lock-edit');
-
- expect(spy).toHaveBeenCalledWith('_category_', 'click_edit_button', {
- label: 'right_sidebar',
- property: 'lock_issue',
- });
- });
-
- it('displays the edit form when opened from collapsed state', done => {
- expect(vm1.isLockDialogOpen).toBe(false);
-
- vm1.$el.querySelector('.sidebar-collapsed-icon').click();
-
- expect(vm1.isLockDialogOpen).toBe(true);
-
- setTimeout(() => {
- expect(vm1.$el.innerHTML.includes('Unlock this issue?')).toBe(true);
-
- done();
- });
- });
-
- it('does not display the edit form when opened from collapsed state if not editable', done => {
- expect(vm2.isLockDialogOpen).toBe(false);
-
- vm2.$el.querySelector('.sidebar-collapsed-icon').click();
-
- Vue.nextTick()
- .then(() => {
- expect(vm2.isLockDialogOpen).toBe(false);
- })
- .then(done)
- .catch(done.fail);
- });
-});
diff --git a/spec/javascripts/sidebar/mock_data.js b/spec/javascripts/sidebar/mock_data.js
deleted file mode 100644
index c869ff96933..00000000000
--- a/spec/javascripts/sidebar/mock_data.js
+++ /dev/null
@@ -1,7 +0,0 @@
-// No new code should be added to this file. Instead, modify the
-// file this one re-exports from. For more detail about why, see:
-// https://gitlab.com/gitlab-org/gitlab-foss/merge_requests/31349
-
-import mockData from '../../../spec/frontend/sidebar/mock_data';
-
-export default mockData;
diff --git a/spec/javascripts/sidebar/participants_spec.js b/spec/javascripts/sidebar/participants_spec.js
deleted file mode 100644
index 7e80e86f8ca..00000000000
--- a/spec/javascripts/sidebar/participants_spec.js
+++ /dev/null
@@ -1,202 +0,0 @@
-import Vue from 'vue';
-import mountComponent from 'spec/helpers/vue_mount_component_helper';
-import participants from '~/sidebar/components/participants/participants.vue';
-
-const PARTICIPANT = {
- id: 1,
- state: 'active',
- username: 'marcene',
- name: 'Allie Will',
- web_url: 'foo.com',
- avatar_url: 'gravatar.com/avatar/xxx',
-};
-
-const PARTICIPANT_LIST = [PARTICIPANT, { ...PARTICIPANT, id: 2 }, { ...PARTICIPANT, id: 3 }];
-
-describe('Participants', function() {
- let vm;
- let Participants;
-
- beforeEach(() => {
- Participants = Vue.extend(participants);
- });
-
- afterEach(() => {
- vm.$destroy();
- });
-
- describe('collapsed sidebar state', () => {
- it('shows loading spinner when loading', () => {
- vm = mountComponent(Participants, {
- loading: true,
- });
-
- expect(vm.$el.querySelector('.js-participants-collapsed-loading-icon')).toBeDefined();
- });
-
- it('shows participant count when given', () => {
- vm = mountComponent(Participants, {
- loading: false,
- participants: PARTICIPANT_LIST,
- });
- const countEl = vm.$el.querySelector('.js-participants-collapsed-count');
-
- expect(countEl.textContent.trim()).toBe(`${PARTICIPANT_LIST.length}`);
- });
-
- it('shows full participant count when there are hidden participants', () => {
- vm = mountComponent(Participants, {
- loading: false,
- participants: PARTICIPANT_LIST,
- numberOfLessParticipants: 1,
- });
- const countEl = vm.$el.querySelector('.js-participants-collapsed-count');
-
- expect(countEl.textContent.trim()).toBe(`${PARTICIPANT_LIST.length}`);
- });
- });
-
- describe('expanded sidebar state', () => {
- it('shows loading spinner when loading', () => {
- vm = mountComponent(Participants, {
- loading: true,
- });
-
- expect(vm.$el.querySelector('.js-participants-expanded-loading-icon')).toBeDefined();
- });
-
- it('when only showing visible participants, shows an avatar only for each participant under the limit', done => {
- const numberOfLessParticipants = 2;
- vm = mountComponent(Participants, {
- loading: false,
- participants: PARTICIPANT_LIST,
- numberOfLessParticipants,
- });
- vm.isShowingMoreParticipants = false;
-
- Vue.nextTick()
- .then(() => {
- const participantEls = vm.$el.querySelectorAll('.js-participants-author');
-
- expect(participantEls.length).toBe(numberOfLessParticipants);
- })
- .then(done)
- .catch(done.fail);
- });
-
- it('when only showing all participants, each has an avatar', done => {
- const numberOfLessParticipants = 2;
- vm = mountComponent(Participants, {
- loading: false,
- participants: PARTICIPANT_LIST,
- numberOfLessParticipants,
- });
- vm.isShowingMoreParticipants = true;
-
- Vue.nextTick()
- .then(() => {
- const participantEls = vm.$el.querySelectorAll('.js-participants-author');
-
- expect(participantEls.length).toBe(PARTICIPANT_LIST.length);
- })
- .then(done)
- .catch(done.fail);
- });
-
- it('does not have more participants link when they can all be shown', () => {
- const numberOfLessParticipants = 100;
- vm = mountComponent(Participants, {
- loading: false,
- participants: PARTICIPANT_LIST,
- numberOfLessParticipants,
- });
- const moreParticipantLink = vm.$el.querySelector('.js-toggle-participants-button');
-
- expect(PARTICIPANT_LIST.length).toBeLessThan(numberOfLessParticipants);
- expect(moreParticipantLink).toBeNull();
- });
-
- it('when too many participants, has more participants link to show more', done => {
- vm = mountComponent(Participants, {
- loading: false,
- participants: PARTICIPANT_LIST,
- numberOfLessParticipants: 2,
- });
- vm.isShowingMoreParticipants = false;
-
- Vue.nextTick()
- .then(() => {
- const moreParticipantLink = vm.$el.querySelector('.js-toggle-participants-button');
-
- expect(moreParticipantLink.textContent.trim()).toBe('+ 1 more');
- })
- .then(done)
- .catch(done.fail);
- });
-
- it('when too many participants and already showing them, has more participants link to show less', done => {
- vm = mountComponent(Participants, {
- loading: false,
- participants: PARTICIPANT_LIST,
- numberOfLessParticipants: 2,
- });
- vm.isShowingMoreParticipants = true;
-
- Vue.nextTick()
- .then(() => {
- const moreParticipantLink = vm.$el.querySelector('.js-toggle-participants-button');
-
- expect(moreParticipantLink.textContent.trim()).toBe('- show less');
- })
- .then(done)
- .catch(done.fail);
- });
-
- it('clicking more participants link emits event', () => {
- vm = mountComponent(Participants, {
- loading: false,
- participants: PARTICIPANT_LIST,
- numberOfLessParticipants: 2,
- });
- const moreParticipantLink = vm.$el.querySelector('.js-toggle-participants-button');
-
- expect(vm.isShowingMoreParticipants).toBe(false);
-
- moreParticipantLink.click();
-
- expect(vm.isShowingMoreParticipants).toBe(true);
- });
-
- it('clicking on participants icon emits `toggleSidebar` event', () => {
- vm = mountComponent(Participants, {
- loading: false,
- participants: PARTICIPANT_LIST,
- numberOfLessParticipants: 2,
- });
- spyOn(vm, '$emit');
-
- const participantsIconEl = vm.$el.querySelector('.sidebar-collapsed-icon');
-
- participantsIconEl.click();
-
- expect(vm.$emit).toHaveBeenCalledWith('toggleSidebar');
- });
- });
-
- describe('when not showing participants label', () => {
- beforeEach(() => {
- vm = mountComponent(Participants, {
- participants: PARTICIPANT_LIST,
- showParticipantLabel: false,
- });
- });
-
- it('does not show sidebar collapsed icon', () => {
- expect(vm.$el.querySelector('.sidebar-collapsed-icon')).not.toBeTruthy();
- });
-
- it('does not show participants label title', () => {
- expect(vm.$el.querySelector('.title')).not.toBeTruthy();
- });
- });
-});
diff --git a/spec/javascripts/sidebar/sidebar_mediator_spec.js b/spec/javascripts/sidebar/sidebar_mediator_spec.js
deleted file mode 100644
index 2aa30fd1cc6..00000000000
--- a/spec/javascripts/sidebar/sidebar_mediator_spec.js
+++ /dev/null
@@ -1,134 +0,0 @@
-import MockAdapter from 'axios-mock-adapter';
-import axios from '~/lib/utils/axios_utils';
-import SidebarMediator from '~/sidebar/sidebar_mediator';
-import SidebarStore from '~/sidebar/stores/sidebar_store';
-import SidebarService, { gqClient } from '~/sidebar/services/sidebar_service';
-import Mock from './mock_data';
-
-const { mediator: mediatorMockData } = Mock;
-
-describe('Sidebar mediator', function() {
- let mock;
-
- beforeEach(() => {
- mock = new MockAdapter(axios);
-
- this.mediator = new SidebarMediator(mediatorMockData);
- });
-
- afterEach(() => {
- SidebarService.singleton = null;
- SidebarStore.singleton = null;
- SidebarMediator.singleton = null;
- mock.restore();
- });
-
- it('assigns yourself ', () => {
- this.mediator.assignYourself();
-
- expect(this.mediator.store.currentUser).toEqual(mediatorMockData.currentUser);
- expect(this.mediator.store.assignees[0]).toEqual(mediatorMockData.currentUser);
- });
-
- it('saves assignees', done => {
- mock.onPut(mediatorMockData.endpoint).reply(200, {});
- this.mediator
- .saveAssignees('issue[assignee_ids]')
- .then(resp => {
- expect(resp.status).toEqual(200);
- done();
- })
- .catch(done.fail);
- });
-
- it('fetches the data', done => {
- const mockData = Mock.responseMap.GET[mediatorMockData.endpoint];
- mock.onGet(mediatorMockData.endpoint).reply(200, mockData);
-
- const mockGraphQlData = Mock.graphQlResponseData;
- spyOn(gqClient, 'query').and.returnValue({
- data: mockGraphQlData,
- });
-
- spyOn(this.mediator, 'processFetchedData').and.callThrough();
-
- this.mediator
- .fetch()
- .then(() => {
- expect(this.mediator.processFetchedData).toHaveBeenCalledWith(mockData, mockGraphQlData);
- })
- .then(done)
- .catch(done.fail);
- });
-
- it('processes fetched data', () => {
- const mockData = Mock.responseMap.GET[mediatorMockData.endpoint];
- this.mediator.processFetchedData(mockData);
-
- expect(this.mediator.store.assignees).toEqual(mockData.assignees);
- expect(this.mediator.store.humanTimeEstimate).toEqual(mockData.human_time_estimate);
- expect(this.mediator.store.humanTotalTimeSpent).toEqual(mockData.human_total_time_spent);
- expect(this.mediator.store.participants).toEqual(mockData.participants);
- expect(this.mediator.store.subscribed).toEqual(mockData.subscribed);
- expect(this.mediator.store.timeEstimate).toEqual(mockData.time_estimate);
- expect(this.mediator.store.totalTimeSpent).toEqual(mockData.total_time_spent);
- });
-
- it('sets moveToProjectId', () => {
- const projectId = 7;
- spyOn(this.mediator.store, 'setMoveToProjectId').and.callThrough();
-
- this.mediator.setMoveToProjectId(projectId);
-
- expect(this.mediator.store.setMoveToProjectId).toHaveBeenCalledWith(projectId);
- });
-
- it('fetches autocomplete projects', done => {
- const searchTerm = 'foo';
- mock.onGet(mediatorMockData.projectsAutocompleteEndpoint).reply(200, {});
- spyOn(this.mediator.service, 'getProjectsAutocomplete').and.callThrough();
- spyOn(this.mediator.store, 'setAutocompleteProjects').and.callThrough();
-
- this.mediator
- .fetchAutocompleteProjects(searchTerm)
- .then(() => {
- expect(this.mediator.service.getProjectsAutocomplete).toHaveBeenCalledWith(searchTerm);
- expect(this.mediator.store.setAutocompleteProjects).toHaveBeenCalled();
- })
- .then(done)
- .catch(done.fail);
- });
-
- it('moves issue', done => {
- const mockData = Mock.responseMap.POST[mediatorMockData.moveIssueEndpoint];
- const moveToProjectId = 7;
- mock.onPost(mediatorMockData.moveIssueEndpoint).reply(200, mockData);
- this.mediator.store.setMoveToProjectId(moveToProjectId);
- spyOn(this.mediator.service, 'moveIssue').and.callThrough();
- const visitUrl = spyOnDependency(SidebarMediator, 'visitUrl');
-
- this.mediator
- .moveIssue()
- .then(() => {
- expect(this.mediator.service.moveIssue).toHaveBeenCalledWith(moveToProjectId);
- expect(visitUrl).toHaveBeenCalledWith(mockData.web_url);
- })
- .then(done)
- .catch(done.fail);
- });
-
- it('toggle subscription', done => {
- this.mediator.store.setSubscribedState(false);
- mock.onPost(mediatorMockData.toggleSubscriptionEndpoint).reply(200, {});
- spyOn(this.mediator.service, 'toggleSubscription').and.callThrough();
-
- this.mediator
- .toggleSubscription()
- .then(() => {
- expect(this.mediator.service.toggleSubscription).toHaveBeenCalled();
- expect(this.mediator.store.subscribed).toEqual(true);
- })
- .then(done)
- .catch(done.fail);
- });
-});
diff --git a/spec/javascripts/sidebar/sidebar_move_issue_spec.js b/spec/javascripts/sidebar/sidebar_move_issue_spec.js
deleted file mode 100644
index ec712450f2e..00000000000
--- a/spec/javascripts/sidebar/sidebar_move_issue_spec.js
+++ /dev/null
@@ -1,166 +0,0 @@
-import $ from 'jquery';
-import MockAdapter from 'axios-mock-adapter';
-import axios from '~/lib/utils/axios_utils';
-import SidebarMediator from '~/sidebar/sidebar_mediator';
-import SidebarStore from '~/sidebar/stores/sidebar_store';
-import SidebarService from '~/sidebar/services/sidebar_service';
-import SidebarMoveIssue from '~/sidebar/lib/sidebar_move_issue';
-import Mock from './mock_data';
-
-describe('SidebarMoveIssue', function() {
- let mock;
-
- beforeEach(() => {
- mock = new MockAdapter(axios);
- const mockData = Mock.responseMap.GET['/autocomplete/projects?project_id=15'];
- mock.onGet('/autocomplete/projects?project_id=15').reply(200, mockData);
- this.mediator = new SidebarMediator(Mock.mediator);
- this.$content = $(`
- <div class="dropdown">
- <div class="js-toggle"></div>
- <div class="dropdown-menu">
- <div class="dropdown-content"></div>
- </div>
- <div class="js-confirm-button"></div>
- </div>
- `);
- this.$toggleButton = this.$content.find('.js-toggle');
- this.$confirmButton = this.$content.find('.js-confirm-button');
-
- this.sidebarMoveIssue = new SidebarMoveIssue(
- this.mediator,
- this.$toggleButton,
- this.$confirmButton,
- );
- this.sidebarMoveIssue.init();
- });
-
- afterEach(() => {
- SidebarService.singleton = null;
- SidebarStore.singleton = null;
- SidebarMediator.singleton = null;
-
- this.sidebarMoveIssue.destroy();
- mock.restore();
- });
-
- describe('init', () => {
- it('should initialize the dropdown and listeners', () => {
- spyOn(this.sidebarMoveIssue, 'initDropdown');
- spyOn(this.sidebarMoveIssue, 'addEventListeners');
-
- this.sidebarMoveIssue.init();
-
- expect(this.sidebarMoveIssue.initDropdown).toHaveBeenCalled();
- expect(this.sidebarMoveIssue.addEventListeners).toHaveBeenCalled();
- });
- });
-
- describe('destroy', () => {
- it('should remove the listeners', () => {
- spyOn(this.sidebarMoveIssue, 'removeEventListeners');
-
- this.sidebarMoveIssue.destroy();
-
- expect(this.sidebarMoveIssue.removeEventListeners).toHaveBeenCalled();
- });
- });
-
- describe('initDropdown', () => {
- it('should initialize the gl_dropdown', () => {
- spyOn($.fn, 'glDropdown');
-
- this.sidebarMoveIssue.initDropdown();
-
- expect($.fn.glDropdown).toHaveBeenCalled();
- });
-
- it('escapes html from project name', done => {
- this.$toggleButton.dropdown('toggle');
-
- setTimeout(() => {
- expect(this.$content.find('.js-move-issue-dropdown-item')[1].innerHTML.trim()).toEqual(
- '&lt;img src=x onerror=alert(document.domain)&gt; foo / bar',
- );
- done();
- });
- });
- });
-
- describe('onConfirmClicked', () => {
- it('should move the issue with valid project ID', () => {
- spyOn(this.mediator, 'moveIssue').and.returnValue(Promise.resolve());
- this.mediator.setMoveToProjectId(7);
-
- this.sidebarMoveIssue.onConfirmClicked();
-
- expect(this.mediator.moveIssue).toHaveBeenCalled();
- expect(this.$confirmButton.prop('disabled')).toBeTruthy();
- expect(this.$confirmButton.hasClass('is-loading')).toBe(true);
- });
-
- it('should remove loading state from confirm button on failure', done => {
- spyOn(window, 'Flash');
- spyOn(this.mediator, 'moveIssue').and.returnValue(Promise.reject());
- this.mediator.setMoveToProjectId(7);
-
- this.sidebarMoveIssue.onConfirmClicked();
-
- expect(this.mediator.moveIssue).toHaveBeenCalled();
- // Wait for the move issue request to fail
- setTimeout(() => {
- expect(window.Flash).toHaveBeenCalled();
- expect(this.$confirmButton.prop('disabled')).toBeFalsy();
- expect(this.$confirmButton.hasClass('is-loading')).toBe(false);
- done();
- });
- });
-
- it('should not move the issue with id=0', () => {
- spyOn(this.mediator, 'moveIssue');
- this.mediator.setMoveToProjectId(0);
-
- this.sidebarMoveIssue.onConfirmClicked();
-
- expect(this.mediator.moveIssue).not.toHaveBeenCalled();
- });
- });
-
- it('should set moveToProjectId on dropdown item "No project" click', done => {
- spyOn(this.mediator, 'setMoveToProjectId');
-
- // Open the dropdown
- this.$toggleButton.dropdown('toggle');
-
- // Wait for the autocomplete request to finish
- setTimeout(() => {
- this.$content
- .find('.js-move-issue-dropdown-item')
- .eq(0)
- .trigger('click');
-
- expect(this.mediator.setMoveToProjectId).toHaveBeenCalledWith(0);
- expect(this.$confirmButton.prop('disabled')).toBeTruthy();
- done();
- }, 0);
- });
-
- it('should set moveToProjectId on dropdown item click', done => {
- spyOn(this.mediator, 'setMoveToProjectId');
-
- // Open the dropdown
- this.$toggleButton.dropdown('toggle');
-
- // Wait for the autocomplete request to finish
- setTimeout(() => {
- this.$content
- .find('.js-move-issue-dropdown-item')
- .eq(1)
- .trigger('click');
-
- expect(this.mediator.setMoveToProjectId).toHaveBeenCalledWith(20);
- expect(this.$confirmButton.attr('disabled')).toBe(undefined);
- done();
- }, 0);
- });
-});
diff --git a/spec/javascripts/sidebar/sidebar_subscriptions_spec.js b/spec/javascripts/sidebar/sidebar_subscriptions_spec.js
deleted file mode 100644
index ee4516f3bcd..00000000000
--- a/spec/javascripts/sidebar/sidebar_subscriptions_spec.js
+++ /dev/null
@@ -1,38 +0,0 @@
-import Vue from 'vue';
-import mountComponent from 'spec/helpers/vue_mount_component_helper';
-import sidebarSubscriptions from '~/sidebar/components/subscriptions/sidebar_subscriptions.vue';
-import SidebarMediator from '~/sidebar/sidebar_mediator';
-import SidebarService from '~/sidebar/services/sidebar_service';
-import SidebarStore from '~/sidebar/stores/sidebar_store';
-import Mock from './mock_data';
-
-describe('Sidebar Subscriptions', function() {
- let vm;
- let SidebarSubscriptions;
-
- beforeEach(() => {
- SidebarSubscriptions = Vue.extend(sidebarSubscriptions);
- // Set up the stores, services, etc
- // eslint-disable-next-line no-new
- new SidebarMediator(Mock.mediator);
- });
-
- afterEach(() => {
- vm.$destroy();
- SidebarService.singleton = null;
- SidebarStore.singleton = null;
- SidebarMediator.singleton = null;
- });
-
- it('calls the mediator toggleSubscription on event', () => {
- const mediator = new SidebarMediator();
- spyOn(mediator, 'toggleSubscription').and.returnValue(Promise.resolve());
- vm = mountComponent(SidebarSubscriptions, {
- mediator,
- });
-
- vm.onToggleSubscription();
-
- expect(mediator.toggleSubscription).toHaveBeenCalled();
- });
-});
diff --git a/spec/javascripts/sidebar/subscriptions_spec.js b/spec/javascripts/sidebar/subscriptions_spec.js
deleted file mode 100644
index cdb39efbef8..00000000000
--- a/spec/javascripts/sidebar/subscriptions_spec.js
+++ /dev/null
@@ -1,100 +0,0 @@
-import Vue from 'vue';
-import mountComponent from 'spec/helpers/vue_mount_component_helper';
-import { mockTracking } from 'spec/helpers/tracking_helper';
-import subscriptions from '~/sidebar/components/subscriptions/subscriptions.vue';
-import eventHub from '~/sidebar/event_hub';
-
-describe('Subscriptions', function() {
- let vm;
- let Subscriptions;
-
- beforeEach(() => {
- Subscriptions = Vue.extend(subscriptions);
- });
-
- afterEach(() => {
- vm.$destroy();
- });
-
- it('shows loading spinner when loading', () => {
- vm = mountComponent(Subscriptions, {
- loading: true,
- subscribed: undefined,
- });
-
- expect(vm.$refs.toggleButton.isLoading).toBe(true);
- expect(vm.$refs.toggleButton.$el.querySelector('.project-feature-toggle')).toHaveClass(
- 'is-loading',
- );
- });
-
- it('is toggled "off" when currently not subscribed', () => {
- vm = mountComponent(Subscriptions, {
- subscribed: false,
- });
-
- expect(vm.$refs.toggleButton.$el.querySelector('.project-feature-toggle')).not.toHaveClass(
- 'is-checked',
- );
- });
-
- it('is toggled "on" when currently subscribed', () => {
- vm = mountComponent(Subscriptions, {
- subscribed: true,
- });
-
- expect(vm.$refs.toggleButton.$el.querySelector('.project-feature-toggle')).toHaveClass(
- 'is-checked',
- );
- });
-
- it('toggleSubscription method emits `toggleSubscription` event on eventHub and Component', () => {
- vm = mountComponent(Subscriptions, { subscribed: true });
- spyOn(eventHub, '$emit');
- spyOn(vm, '$emit');
- spyOn(vm, 'track');
-
- vm.toggleSubscription();
-
- expect(eventHub.$emit).toHaveBeenCalledWith('toggleSubscription', jasmine.any(Object));
- expect(vm.$emit).toHaveBeenCalledWith('toggleSubscription', jasmine.any(Object));
- });
-
- it('tracks the event when toggled', () => {
- vm = mountComponent(Subscriptions, { subscribed: true });
- const spy = mockTracking('_category_', vm.$el, spyOn);
- vm.toggleSubscription();
-
- expect(spy).toHaveBeenCalled();
- });
-
- it('onClickCollapsedIcon method emits `toggleSidebar` event on component', () => {
- vm = mountComponent(Subscriptions, { subscribed: true });
- spyOn(vm, '$emit');
-
- vm.onClickCollapsedIcon();
-
- expect(vm.$emit).toHaveBeenCalledWith('toggleSidebar');
- });
-
- describe('given project emails are disabled', () => {
- const subscribeDisabledDescription = 'Notifications have been disabled';
-
- beforeEach(() => {
- vm = mountComponent(Subscriptions, {
- subscribed: false,
- projectEmailsDisabled: true,
- subscribeDisabledDescription,
- });
- });
-
- it('sets the correct display text', () => {
- expect(vm.$el.textContent).toContain(subscribeDisabledDescription);
- expect(vm.$refs.tooltip.dataset.originalTitle).toBe(subscribeDisabledDescription);
- });
-
- it('does not render the toggle button', () => {
- expect(vm.$refs.toggleButton).toBeUndefined();
- });
- });
-});
diff --git a/spec/javascripts/vue_mr_widget/components/mr_widget_header_spec.js b/spec/javascripts/vue_mr_widget/components/mr_widget_header_spec.js
index de1d351677c..3cbaa47c832 100644
--- a/spec/javascripts/vue_mr_widget/components/mr_widget_header_spec.js
+++ b/spec/javascripts/vue_mr_widget/components/mr_widget_header_spec.js
@@ -153,7 +153,7 @@ describe('MRWidgetHeader', () => {
beforeEach(() => {
vm = mountComponent(Component, {
- mr: Object.assign({}, mrDefaultOptions),
+ mr: { ...mrDefaultOptions },
});
});
@@ -176,7 +176,7 @@ describe('MRWidgetHeader', () => {
});
it('renders web ide button in disabled state with no href', () => {
- const mr = Object.assign({}, mrDefaultOptions, { canPushToSourceBranch: false });
+ const mr = { ...mrDefaultOptions, canPushToSourceBranch: false };
vm = mountComponent(Component, { mr });
const link = vm.$el.querySelector('.js-web-ide');
diff --git a/spec/javascripts/vue_mr_widget/components/mr_widget_pipeline_container_spec.js b/spec/javascripts/vue_mr_widget/components/mr_widget_pipeline_container_spec.js
deleted file mode 100644
index 76827cde093..00000000000
--- a/spec/javascripts/vue_mr_widget/components/mr_widget_pipeline_container_spec.js
+++ /dev/null
@@ -1,99 +0,0 @@
-import { mount, createLocalVue } from '@vue/test-utils';
-import MrWidgetPipelineContainer from '~/vue_merge_request_widget/components/mr_widget_pipeline_container.vue';
-import MrWidgetPipeline from '~/vue_merge_request_widget/components/mr_widget_pipeline.vue';
-import ArtifactsApp from '~/vue_merge_request_widget/components/artifacts_list_app.vue';
-import { mockStore } from '../mock_data';
-
-const localVue = createLocalVue();
-
-describe('MrWidgetPipelineContainer', () => {
- let wrapper;
-
- const factory = (props = {}) => {
- wrapper = mount(localVue.extend(MrWidgetPipelineContainer), {
- propsData: {
- mr: Object.assign({}, mockStore),
- ...props,
- },
- localVue,
- });
- };
-
- afterEach(() => {
- wrapper.destroy();
- });
-
- describe('when pre merge', () => {
- beforeEach(() => {
- factory();
- });
-
- it('renders pipeline', () => {
- expect(wrapper.find(MrWidgetPipeline).exists()).toBe(true);
- expect(wrapper.find(MrWidgetPipeline).props()).toEqual(
- jasmine.objectContaining({
- pipeline: mockStore.pipeline,
- pipelineCoverageDelta: mockStore.pipelineCoverageDelta,
- ciStatus: mockStore.ciStatus,
- hasCi: mockStore.hasCI,
- sourceBranch: mockStore.sourceBranch,
- sourceBranchLink: mockStore.sourceBranchLink,
- }),
- );
- });
-
- it('renders deployments', () => {
- const expectedProps = mockStore.deployments.map(dep =>
- jasmine.objectContaining({
- deployment: dep,
- showMetrics: false,
- }),
- );
-
- const deployments = wrapper.findAll('.mr-widget-extension .js-pre-deployment');
-
- expect(deployments.wrappers.map(x => x.props())).toEqual(expectedProps);
- });
- });
-
- describe('when post merge', () => {
- beforeEach(() => {
- factory({
- isPostMerge: true,
- });
- });
-
- it('renders pipeline', () => {
- expect(wrapper.find(MrWidgetPipeline).exists()).toBe(true);
- expect(wrapper.find(MrWidgetPipeline).props()).toEqual(
- jasmine.objectContaining({
- pipeline: mockStore.mergePipeline,
- pipelineCoverageDelta: mockStore.pipelineCoverageDelta,
- ciStatus: mockStore.ciStatus,
- hasCi: mockStore.hasCI,
- sourceBranch: mockStore.targetBranch,
- sourceBranchLink: mockStore.targetBranch,
- }),
- );
- });
-
- it('renders deployments', () => {
- const expectedProps = mockStore.postMergeDeployments.map(dep =>
- jasmine.objectContaining({
- deployment: dep,
- showMetrics: true,
- }),
- );
-
- const deployments = wrapper.findAll('.mr-widget-extension .js-post-deployment');
-
- expect(deployments.wrappers.map(x => x.props())).toEqual(expectedProps);
- });
- });
-
- describe('with artifacts path', () => {
- it('renders the artifacts app', () => {
- expect(wrapper.find(ArtifactsApp).isVisible()).toBe(true);
- });
- });
-});
diff --git a/spec/javascripts/vue_mr_widget/components/mr_widget_pipeline_spec.js b/spec/javascripts/vue_mr_widget/components/mr_widget_pipeline_spec.js
index 5997c93105e..883c41085fa 100644
--- a/spec/javascripts/vue_mr_widget/components/mr_widget_pipeline_spec.js
+++ b/spec/javascripts/vue_mr_widget/components/mr_widget_pipeline_spec.js
@@ -122,6 +122,19 @@ describe('MRWidgetPipeline', () => {
);
});
+ it('should render CI error when no CI is provided and pipeline must succeed is turned on', () => {
+ vm = mountComponent(Component, {
+ pipeline: {},
+ hasCi: false,
+ pipelineMustSucceed: true,
+ troubleshootingDocsPath: 'help',
+ });
+
+ expect(vm.$el.querySelector('.media-body').textContent.trim()).toContain(
+ 'No pipeline has been run for this commit.',
+ );
+ });
+
describe('with a pipeline', () => {
beforeEach(() => {
vm = mountComponent(Component, {
diff --git a/spec/javascripts/vue_mr_widget/components/states/mr_widget_ready_to_merge_spec.js b/spec/javascripts/vue_mr_widget/components/states/mr_widget_ready_to_merge_spec.js
index d396f2d9271..9ba429c3d20 100644
--- a/spec/javascripts/vue_mr_widget/components/states/mr_widget_ready_to_merge_spec.js
+++ b/spec/javascripts/vue_mr_widget/components/states/mr_widget_ready_to_merge_spec.js
@@ -18,6 +18,7 @@ const createTestMr = customConfig => {
isPipelineFailed: false,
isPipelinePassing: false,
isMergeAllowed: true,
+ isApproved: true,
onlyAllowMergeIfPipelineSucceeds: false,
ffOnlyEnabled: false,
hasCI: false,
@@ -919,8 +920,8 @@ describe('ReadyToMerge', () => {
});
});
- describe('Commit message area', () => {
- describe('when using merge commits', () => {
+ describe('Merge request project settings', () => {
+ describe('when the merge commit merge method is enabled', () => {
beforeEach(() => {
vm = createComponent({
mr: { ffOnlyEnabled: false },
@@ -936,7 +937,7 @@ describe('ReadyToMerge', () => {
});
});
- describe('when fast-forward merge is enabled', () => {
+ describe('when the fast-forward merge method is enabled', () => {
beforeEach(() => {
vm = createComponent({
mr: { ffOnlyEnabled: true },
diff --git a/spec/javascripts/vue_mr_widget/stores/artifacts_list/actions_spec.js b/spec/javascripts/vue_mr_widget/stores/artifacts_list/actions_spec.js
deleted file mode 100644
index 5070e74b5d2..00000000000
--- a/spec/javascripts/vue_mr_widget/stores/artifacts_list/actions_spec.js
+++ /dev/null
@@ -1,165 +0,0 @@
-import MockAdapter from 'axios-mock-adapter';
-import testAction from 'spec/helpers/vuex_action_helper';
-import { TEST_HOST } from 'spec/test_constants';
-import axios from '~/lib/utils/axios_utils';
-import {
- setEndpoint,
- requestArtifacts,
- clearEtagPoll,
- stopPolling,
- fetchArtifacts,
- receiveArtifactsSuccess,
- receiveArtifactsError,
-} from '~/vue_merge_request_widget/stores/artifacts_list/actions';
-import state from '~/vue_merge_request_widget/stores/artifacts_list/state';
-import * as types from '~/vue_merge_request_widget/stores/artifacts_list/mutation_types';
-
-describe('Artifacts App Store Actions', () => {
- let mockedState;
-
- beforeEach(() => {
- mockedState = state();
- });
-
- describe('setEndpoint', () => {
- it('should commit SET_ENDPOINT mutation', done => {
- testAction(
- setEndpoint,
- 'endpoint.json',
- mockedState,
- [{ type: types.SET_ENDPOINT, payload: 'endpoint.json' }],
- [],
- done,
- );
- });
- });
-
- describe('requestArtifacts', () => {
- it('should commit REQUEST_ARTIFACTS mutation', done => {
- testAction(
- requestArtifacts,
- null,
- mockedState,
- [{ type: types.REQUEST_ARTIFACTS }],
- [],
- done,
- );
- });
- });
-
- describe('fetchArtifacts', () => {
- let mock;
-
- beforeEach(() => {
- mockedState.endpoint = `${TEST_HOST}/endpoint.json`;
- mock = new MockAdapter(axios);
- });
-
- afterEach(() => {
- mock.restore();
- stopPolling();
- clearEtagPoll();
- });
-
- describe('success', () => {
- it('dispatches requestArtifacts and receiveArtifactsSuccess ', done => {
- mock.onGet(`${TEST_HOST}/endpoint.json`).replyOnce(200, [
- {
- text: 'result.txt',
- url: 'asda',
- job_name: 'generate-artifact',
- job_path: 'asda',
- },
- ]);
-
- testAction(
- fetchArtifacts,
- null,
- mockedState,
- [],
- [
- {
- type: 'requestArtifacts',
- },
- {
- payload: {
- data: [
- {
- text: 'result.txt',
- url: 'asda',
- job_name: 'generate-artifact',
- job_path: 'asda',
- },
- ],
- status: 200,
- },
- type: 'receiveArtifactsSuccess',
- },
- ],
- done,
- );
- });
- });
-
- describe('error', () => {
- beforeEach(() => {
- mock.onGet(`${TEST_HOST}/endpoint.json`).reply(500);
- });
-
- it('dispatches requestArtifacts and receiveArtifactsError ', done => {
- testAction(
- fetchArtifacts,
- null,
- mockedState,
- [],
- [
- {
- type: 'requestArtifacts',
- },
- {
- type: 'receiveArtifactsError',
- },
- ],
- done,
- );
- });
- });
- });
-
- describe('receiveArtifactsSuccess', () => {
- it('should commit RECEIVE_ARTIFACTS_SUCCESS mutation with 200', done => {
- testAction(
- receiveArtifactsSuccess,
- { data: { summary: {} }, status: 200 },
- mockedState,
- [{ type: types.RECEIVE_ARTIFACTS_SUCCESS, payload: { summary: {} } }],
- [],
- done,
- );
- });
-
- it('should not commit RECEIVE_ARTIFACTS_SUCCESS mutation with 204', done => {
- testAction(
- receiveArtifactsSuccess,
- { data: { summary: {} }, status: 204 },
- mockedState,
- [],
- [],
- done,
- );
- });
- });
-
- describe('receiveArtifactsError', () => {
- it('should commit RECEIVE_ARTIFACTS_ERROR mutation', done => {
- testAction(
- receiveArtifactsError,
- null,
- mockedState,
- [{ type: types.RECEIVE_ARTIFACTS_ERROR }],
- [],
- done,
- );
- });
- });
-});
diff --git a/spec/javascripts/vue_mr_widget/stores/mr_widget_store_spec.js b/spec/javascripts/vue_mr_widget/stores/mr_widget_store_spec.js
deleted file mode 100644
index 1906585af7b..00000000000
--- a/spec/javascripts/vue_mr_widget/stores/mr_widget_store_spec.js
+++ /dev/null
@@ -1,112 +0,0 @@
-import MergeRequestStore from '~/vue_merge_request_widget/stores/mr_widget_store';
-import { stateKey } from '~/vue_merge_request_widget/stores/state_maps';
-import mockData from '../mock_data';
-
-describe('MergeRequestStore', () => {
- let store;
-
- beforeEach(() => {
- store = new MergeRequestStore(mockData);
- });
-
- describe('setData', () => {
- it('should set isSHAMismatch when the diff SHA changes', () => {
- store.setData({ ...mockData, diff_head_sha: 'a-different-string' });
-
- expect(store.isSHAMismatch).toBe(true);
- });
-
- it('should not set isSHAMismatch when other data changes', () => {
- store.setData({ ...mockData, work_in_progress: !mockData.work_in_progress });
-
- expect(store.isSHAMismatch).toBe(false);
- });
-
- it('should update cached sha after rebasing', () => {
- store.setData({ ...mockData, diff_head_sha: 'abc123' }, true);
-
- expect(store.isSHAMismatch).toBe(false);
- expect(store.sha).toBe('abc123');
- });
-
- describe('isPipelinePassing', () => {
- it('is true when the CI status is `success`', () => {
- store.setData({ ...mockData, ci_status: 'success' });
-
- expect(store.isPipelinePassing).toBe(true);
- });
-
- it('is true when the CI status is `success-with-warnings`', () => {
- store.setData({ ...mockData, ci_status: 'success-with-warnings' });
-
- expect(store.isPipelinePassing).toBe(true);
- });
-
- it('is false when the CI status is `failed`', () => {
- store.setData({ ...mockData, ci_status: 'failed' });
-
- expect(store.isPipelinePassing).toBe(false);
- });
-
- it('is false when the CI status is anything except `success`', () => {
- store.setData({ ...mockData, ci_status: 'foobarbaz' });
-
- expect(store.isPipelinePassing).toBe(false);
- });
- });
-
- describe('isPipelineSkipped', () => {
- it('should set isPipelineSkipped=true when the CI status is `skipped`', () => {
- store.setData({ ...mockData, ci_status: 'skipped' });
-
- expect(store.isPipelineSkipped).toBe(true);
- });
-
- it('should set isPipelineSkipped=false when the CI status is anything except `skipped`', () => {
- store.setData({ ...mockData, ci_status: 'foobarbaz' });
-
- expect(store.isPipelineSkipped).toBe(false);
- });
- });
-
- describe('isNothingToMergeState', () => {
- it('returns true when nothingToMerge', () => {
- store.state = stateKey.nothingToMerge;
-
- expect(store.isNothingToMergeState).toEqual(true);
- });
-
- it('returns false when not nothingToMerge', () => {
- store.state = 'state';
-
- expect(store.isNothingToMergeState).toEqual(false);
- });
- });
- });
-
- describe('setPaths', () => {
- it('should set the add ci config path', () => {
- store.setData({ ...mockData });
-
- expect(store.mergeRequestAddCiConfigPath).toEqual('/group2/project2/new/pipeline');
- });
-
- it('should set humanAccess=Maintainer when user has that role', () => {
- store.setData({ ...mockData });
-
- expect(store.humanAccess).toEqual('Maintainer');
- });
-
- it('should set pipelinesEmptySvgPath', () => {
- store.setData({ ...mockData });
-
- expect(store.pipelinesEmptySvgPath).toBe('/path/to/svg');
- });
-
- it('should set newPipelinePath', () => {
- store.setData({ ...mockData });
-
- expect(store.newPipelinePath).toBe('/group2/project2/pipelines/new');
- });
- });
-});
diff --git a/spec/javascripts/vue_shared/components/ci_badge_link_spec.js b/spec/javascripts/vue_shared/components/ci_badge_link_spec.js
deleted file mode 100644
index 367e07d3ad3..00000000000
--- a/spec/javascripts/vue_shared/components/ci_badge_link_spec.js
+++ /dev/null
@@ -1,100 +0,0 @@
-import Vue from 'vue';
-import mountComponent from 'spec/helpers/vue_mount_component_helper';
-import ciBadge from '~/vue_shared/components/ci_badge_link.vue';
-
-describe('CI Badge Link Component', () => {
- let CIBadge;
- let vm;
-
- const statuses = {
- canceled: {
- text: 'canceled',
- label: 'canceled',
- group: 'canceled',
- icon: 'status_canceled',
- details_path: 'status/canceled',
- },
- created: {
- text: 'created',
- label: 'created',
- group: 'created',
- icon: 'status_created',
- details_path: 'status/created',
- },
- failed: {
- text: 'failed',
- label: 'failed',
- group: 'failed',
- icon: 'status_failed',
- details_path: 'status/failed',
- },
- manual: {
- text: 'manual',
- label: 'manual action',
- group: 'manual',
- icon: 'status_manual',
- details_path: 'status/manual',
- },
- pending: {
- text: 'pending',
- label: 'pending',
- group: 'pending',
- icon: 'status_pending',
- details_path: 'status/pending',
- },
- running: {
- text: 'running',
- label: 'running',
- group: 'running',
- icon: 'status_running',
- details_path: 'status/running',
- },
- skipped: {
- text: 'skipped',
- label: 'skipped',
- group: 'skipped',
- icon: 'status_skipped',
- details_path: 'status/skipped',
- },
- success_warining: {
- text: 'passed',
- label: 'passed',
- group: 'success-with-warnings',
- icon: 'status_warning',
- details_path: 'status/warning',
- },
- success: {
- text: 'passed',
- label: 'passed',
- group: 'passed',
- icon: 'status_success',
- details_path: 'status/passed',
- },
- };
-
- beforeEach(() => {
- CIBadge = Vue.extend(ciBadge);
- });
-
- afterEach(() => {
- vm.$destroy();
- });
-
- it('should render each status badge', () => {
- Object.keys(statuses).map(status => {
- vm = mountComponent(CIBadge, { status: statuses[status] });
-
- expect(vm.$el.getAttribute('href')).toEqual(statuses[status].details_path);
- expect(vm.$el.textContent.trim()).toEqual(statuses[status].text);
- expect(vm.$el.getAttribute('class')).toContain(`ci-status ci-${statuses[status].group}`);
- expect(vm.$el.querySelector('svg')).toBeDefined();
- return vm;
- });
- });
-
- it('should not render label', () => {
- vm = mountComponent(CIBadge, { status: statuses.canceled, showText: false });
-
- expect(vm.$el.textContent.trim()).toEqual('');
- });
-});
diff --git a/spec/javascripts/vue_shared/components/ci_icon_spec.js b/spec/javascripts/vue_shared/components/ci_icon_spec.js
deleted file mode 100644
index 9486d7d4f23..00000000000
--- a/spec/javascripts/vue_shared/components/ci_icon_spec.js
+++ /dev/null
@@ -1,122 +0,0 @@
-import Vue from 'vue';
-import mountComponent from 'spec/helpers/vue_mount_component_helper';
-import ciIcon from '~/vue_shared/components/ci_icon.vue';
-
-describe('CI Icon component', () => {
- const Component = Vue.extend(ciIcon);
- let vm;
-
- afterEach(() => {
- vm.$destroy();
- });
-
- it('should render a span element with an svg', () => {
- vm = mountComponent(Component, {
- status: {
- icon: 'status_success',
- },
- });
-
- expect(vm.$el.tagName).toEqual('SPAN');
- expect(vm.$el.querySelector('span > svg')).toBeDefined();
- });
-
- it('should render a success status', () => {
- vm = mountComponent(Component, {
- status: {
- icon: 'status_success',
- group: 'success',
- },
- });
-
- expect(vm.$el.classList.contains('ci-status-icon-success')).toEqual(true);
- });
-
- it('should render a failed status', () => {
- vm = mountComponent(Component, {
- status: {
- icon: 'status_failed',
- group: 'failed',
- },
- });
-
- expect(vm.$el.classList.contains('ci-status-icon-failed')).toEqual(true);
- });
-
- it('should render success with warnings status', () => {
- vm = mountComponent(Component, {
- status: {
- icon: 'status_warning',
- group: 'warning',
- },
- });
-
- expect(vm.$el.classList.contains('ci-status-icon-warning')).toEqual(true);
- });
-
- it('should render pending status', () => {
- vm = mountComponent(Component, {
- status: {
- icon: 'status_pending',
- group: 'pending',
- },
- });
-
- expect(vm.$el.classList.contains('ci-status-icon-pending')).toEqual(true);
- });
-
- it('should render running status', () => {
- vm = mountComponent(Component, {
- status: {
- icon: 'status_running',
- group: 'running',
- },
- });
-
- expect(vm.$el.classList.contains('ci-status-icon-running')).toEqual(true);
- });
-
- it('should render created status', () => {
- vm = mountComponent(Component, {
- status: {
- icon: 'status_created',
- group: 'created',
- },
- });
-
- expect(vm.$el.classList.contains('ci-status-icon-created')).toEqual(true);
- });
-
- it('should render skipped status', () => {
- vm = mountComponent(Component, {
- status: {
- icon: 'status_skipped',
- group: 'skipped',
- },
- });
-
- expect(vm.$el.classList.contains('ci-status-icon-skipped')).toEqual(true);
- });
-
- it('should render canceled status', () => {
- vm = mountComponent(Component, {
- status: {
- icon: 'status_canceled',
- group: 'canceled',
- },
- });
-
- expect(vm.$el.classList.contains('ci-status-icon-canceled')).toEqual(true);
- });
-
- it('should render status for manual action', () => {
- vm = mountComponent(Component, {
- status: {
- icon: 'status_manual',
- group: 'manual',
- },
- });
-
- expect(vm.$el.classList.contains('ci-status-icon-manual')).toEqual(true);
- });
-});
diff --git a/spec/javascripts/vue_shared/components/content_viewer/content_viewer_spec.js b/spec/javascripts/vue_shared/components/content_viewer/content_viewer_spec.js
deleted file mode 100644
index fbe9337ecf4..00000000000
--- a/spec/javascripts/vue_shared/components/content_viewer/content_viewer_spec.js
+++ /dev/null
@@ -1,123 +0,0 @@
-import Vue from 'vue';
-import MockAdapter from 'axios-mock-adapter';
-import mountComponent from 'spec/helpers/vue_mount_component_helper';
-import waitForPromises from 'spec/helpers/wait_for_promises';
-import { GREEN_BOX_IMAGE_URL } from 'spec/test_constants';
-import axios from '~/lib/utils/axios_utils';
-import contentViewer from '~/vue_shared/components/content_viewer/content_viewer.vue';
-import '~/behaviors/markdown/render_gfm';
-
-describe('ContentViewer', () => {
- let vm;
- let mock;
-
- function createComponent(props) {
- const ContentViewer = Vue.extend(contentViewer);
- vm = mountComponent(ContentViewer, props);
- }
-
- afterEach(() => {
- vm.$destroy();
- if (mock) mock.restore();
- });
-
- it('markdown preview renders + loads rendered markdown from server', done => {
- mock = new MockAdapter(axios);
- mock.onPost(`${gon.relative_url_root}/testproject/preview_markdown`).replyOnce(200, {
- body: '<b>testing</b>',
- });
-
- createComponent({
- path: 'test.md',
- content: '* Test',
- projectPath: 'testproject',
- type: 'markdown',
- });
-
- waitForPromises()
- .then(() => {
- expect(vm.$el.querySelector('.md-previewer').textContent).toContain('testing');
- })
- .then(done)
- .catch(done.fail);
- });
-
- it('renders image preview', done => {
- createComponent({
- path: GREEN_BOX_IMAGE_URL,
- fileSize: 1024,
- type: 'image',
- });
-
- vm.$nextTick()
- .then(() => {
- expect(vm.$el.querySelector('img').getAttribute('src')).toBe(GREEN_BOX_IMAGE_URL);
- })
- .then(done)
- .catch(done.fail);
- });
-
- it('renders fallback download control', done => {
- createComponent({
- path: 'somepath/test.abc',
- fileSize: 1024,
- });
-
- vm.$nextTick()
- .then(() => {
- expect(
- vm.$el
- .querySelector('.file-info')
- .textContent.trim()
- .replace(/\s+/, ' '),
- ).toEqual('test.abc (1.00 KiB)');
-
- expect(vm.$el.querySelector('.btn.btn-default').textContent.trim()).toEqual('Download');
- })
- .then(done)
- .catch(done.fail);
- });
-
- it('renders fallback download control for file with a data URL path properly', done => {
- createComponent({
- path: 'data:application/octet-stream;base64,U0VMRUNUICfEhHNnc2cnIGZyb20gVGFibGVuYW1lOwoK',
- filePath: 'somepath/test.abc',
- });
-
- vm.$nextTick()
- .then(() => {
- expect(vm.$el.querySelector('.file-info').textContent.trim()).toEqual('test.abc');
- expect(vm.$el.querySelector('.btn.btn-default')).toHaveAttr('download', 'test.abc');
- expect(vm.$el.querySelector('.btn.btn-default').textContent.trim()).toEqual('Download');
- })
- .then(done)
- .catch(done.fail);
- });
-
- it('markdown preview receives the file path as a parameter', done => {
- mock = new MockAdapter(axios);
- spyOn(axios, 'post').and.callThrough();
- mock.onPost(`${gon.relative_url_root}/testproject/preview_markdown`).reply(200, {
- body: '<b>testing</b>',
- });
-
- createComponent({
- path: 'test.md',
- content: '* Test',
- projectPath: 'testproject',
- type: 'markdown',
- filePath: 'foo/test.md',
- });
-
- vm.$nextTick()
- .then(() => {
- expect(axios.post).toHaveBeenCalledWith(
- `${gon.relative_url_root}/testproject/preview_markdown`,
- { path: 'foo/test.md', text: '* Test' },
- jasmine.any(Object),
- );
- })
- .then(done)
- .catch(done.fail);
- });
-});
diff --git a/spec/javascripts/vue_shared/components/diff_viewer/diff_viewer_spec.js b/spec/javascripts/vue_shared/components/diff_viewer/diff_viewer_spec.js
deleted file mode 100644
index 6a83790093a..00000000000
--- a/spec/javascripts/vue_shared/components/diff_viewer/diff_viewer_spec.js
+++ /dev/null
@@ -1,105 +0,0 @@
-import Vue from 'vue';
-
-import mountComponent from 'spec/helpers/vue_mount_component_helper';
-import { GREEN_BOX_IMAGE_URL, RED_BOX_IMAGE_URL } from 'spec/test_constants';
-import diffViewer from '~/vue_shared/components/diff_viewer/diff_viewer.vue';
-
-describe('DiffViewer', () => {
- const requiredProps = {
- diffMode: 'replaced',
- diffViewerMode: 'image',
- newPath: GREEN_BOX_IMAGE_URL,
- newSha: 'ABC',
- oldPath: RED_BOX_IMAGE_URL,
- oldSha: 'DEF',
- };
- let vm;
-
- function createComponent(props) {
- const DiffViewer = Vue.extend(diffViewer);
-
- vm = mountComponent(DiffViewer, props);
- }
-
- afterEach(() => {
- vm.$destroy();
- });
-
- it('renders image diff', done => {
- window.gon = {
- relative_url_root: '',
- };
-
- createComponent(
- Object.assign({}, requiredProps, {
- projectPath: '',
- }),
- );
-
- setTimeout(() => {
- expect(vm.$el.querySelector('.deleted img').getAttribute('src')).toBe(
- `//-/raw/DEF/${RED_BOX_IMAGE_URL}`,
- );
-
- expect(vm.$el.querySelector('.added img').getAttribute('src')).toBe(
- `//-/raw/ABC/${GREEN_BOX_IMAGE_URL}`,
- );
-
- done();
- });
- });
-
- it('renders fallback download diff display', done => {
- createComponent(
- Object.assign({}, requiredProps, {
- diffViewerMode: 'added',
- newPath: 'test.abc',
- oldPath: 'testold.abc',
- }),
- );
-
- setTimeout(() => {
- expect(vm.$el.querySelector('.deleted .file-info').textContent.trim()).toContain(
- 'testold.abc',
- );
-
- expect(vm.$el.querySelector('.deleted .btn.btn-default').textContent.trim()).toContain(
- 'Download',
- );
-
- expect(vm.$el.querySelector('.added .file-info').textContent.trim()).toContain('test.abc');
- expect(vm.$el.querySelector('.added .btn.btn-default').textContent.trim()).toContain(
- 'Download',
- );
-
- done();
- });
- });
-
- it('renders renamed component', () => {
- createComponent(
- Object.assign({}, requiredProps, {
- diffMode: 'renamed',
- diffViewerMode: 'renamed',
- newPath: 'test.abc',
- oldPath: 'testold.abc',
- }),
- );
-
- expect(vm.$el.textContent).toContain('File moved');
- });
-
- it('renders mode changed component', () => {
- createComponent(
- Object.assign({}, requiredProps, {
- diffMode: 'mode_changed',
- newPath: 'test.abc',
- oldPath: 'testold.abc',
- aMode: '123',
- bMode: '321',
- }),
- );
-
- expect(vm.$el.textContent).toContain('File mode changed from 123 to 321');
- });
-});
diff --git a/spec/javascripts/vue_shared/components/dropdown/dropdown_button_spec.js b/spec/javascripts/vue_shared/components/dropdown/dropdown_button_spec.js
deleted file mode 100644
index b00fa785a0e..00000000000
--- a/spec/javascripts/vue_shared/components/dropdown/dropdown_button_spec.js
+++ /dev/null
@@ -1,81 +0,0 @@
-import Vue from 'vue';
-
-import { mountComponentWithSlots } from 'spec/helpers/vue_mount_component_helper';
-import dropdownButtonComponent from '~/vue_shared/components/dropdown/dropdown_button.vue';
-
-const defaultLabel = 'Select';
-const customLabel = 'Select project';
-
-const createComponent = (props, slots = {}) => {
- const Component = Vue.extend(dropdownButtonComponent);
-
- return mountComponentWithSlots(Component, { props, slots });
-};
-
-describe('DropdownButtonComponent', () => {
- let vm;
-
- beforeEach(() => {
- vm = createComponent();
- });
-
- afterEach(() => {
- vm.$destroy();
- });
-
- describe('computed', () => {
- describe('dropdownToggleText', () => {
- it('returns default toggle text', () => {
- expect(vm.toggleText).toBe(defaultLabel);
- });
-
- it('returns custom toggle text when provided via props', () => {
- const vmEmptyLabels = createComponent({ toggleText: customLabel });
-
- expect(vmEmptyLabels.toggleText).toBe(customLabel);
- vmEmptyLabels.$destroy();
- });
- });
- });
-
- describe('template', () => {
- it('renders component container element of type `button`', () => {
- expect(vm.$el.nodeName).toBe('BUTTON');
- });
-
- it('renders component container element with required data attributes', () => {
- expect(vm.$el.dataset.abilityName).toBe(vm.abilityName);
- expect(vm.$el.dataset.fieldName).toBe(vm.fieldName);
- expect(vm.$el.dataset.issueUpdate).toBe(vm.updatePath);
- expect(vm.$el.dataset.labels).toBe(vm.labelsPath);
- expect(vm.$el.dataset.namespacePath).toBe(vm.namespace);
- expect(vm.$el.dataset.showAny).not.toBeDefined();
- });
-
- it('renders dropdown toggle text element', () => {
- const dropdownToggleTextEl = vm.$el.querySelector('.dropdown-toggle-text');
-
- expect(dropdownToggleTextEl).not.toBeNull();
- expect(dropdownToggleTextEl.innerText.trim()).toBe(defaultLabel);
- });
-
- it('renders dropdown button icon', () => {
- const dropdownIconEl = vm.$el.querySelector('.dropdown-toggle-icon i.fa');
-
- expect(dropdownIconEl).not.toBeNull();
- expect(dropdownIconEl.classList.contains('fa-chevron-down')).toBe(true);
- });
-
- it('renders slot, if default slot exists', () => {
- vm = createComponent(
- {},
- {
- default: ['Lorem Ipsum Dolar'],
- },
- );
-
- expect(vm.$el).not.toContainElement('.dropdown-toggle-text');
- expect(vm.$el).toHaveText('Lorem Ipsum Dolar');
- });
- });
-});
diff --git a/spec/javascripts/vue_shared/components/dropdown/dropdown_hidden_input_spec.js b/spec/javascripts/vue_shared/components/dropdown/dropdown_hidden_input_spec.js
deleted file mode 100644
index 402de2a8788..00000000000
--- a/spec/javascripts/vue_shared/components/dropdown/dropdown_hidden_input_spec.js
+++ /dev/null
@@ -1,36 +0,0 @@
-import Vue from 'vue';
-
-import mountComponent from 'spec/helpers/vue_mount_component_helper';
-import dropdownHiddenInputComponent from '~/vue_shared/components/dropdown/dropdown_hidden_input.vue';
-
-import { mockLabels } from './mock_data';
-
-const createComponent = (name = 'label_id[]', value = mockLabels[0].id) => {
- const Component = Vue.extend(dropdownHiddenInputComponent);
-
- return mountComponent(Component, {
- name,
- value,
- });
-};
-
-describe('DropdownHiddenInputComponent', () => {
- let vm;
-
- beforeEach(() => {
- vm = createComponent();
- });
-
- afterEach(() => {
- vm.$destroy();
- });
-
- describe('template', () => {
- it('renders input element of type `hidden`', () => {
- expect(vm.$el.nodeName).toBe('INPUT');
- expect(vm.$el.getAttribute('type')).toBe('hidden');
- expect(vm.$el.getAttribute('name')).toBe(vm.name);
- expect(vm.$el.getAttribute('value')).toBe(`${vm.value}`);
- });
- });
-});
diff --git a/spec/javascripts/vue_shared/components/file_finder/item_spec.js b/spec/javascripts/vue_shared/components/file_finder/item_spec.js
deleted file mode 100644
index e18d0a46223..00000000000
--- a/spec/javascripts/vue_shared/components/file_finder/item_spec.js
+++ /dev/null
@@ -1,140 +0,0 @@
-import Vue from 'vue';
-import { file } from 'spec/ide/helpers';
-import ItemComponent from '~/vue_shared/components/file_finder/item.vue';
-import createComponent from '../../../helpers/vue_mount_component_helper';
-
-describe('File finder item spec', () => {
- const Component = Vue.extend(ItemComponent);
- let vm;
- let localFile;
-
- beforeEach(() => {
- localFile = {
- ...file(),
- name: 'test file',
- path: 'test/file',
- };
-
- vm = createComponent(Component, {
- file: localFile,
- focused: true,
- searchText: '',
- index: 0,
- });
- });
-
- afterEach(() => {
- vm.$destroy();
- });
-
- it('renders file name & path', () => {
- expect(vm.$el.textContent).toContain('test file');
- expect(vm.$el.textContent).toContain('test/file');
- });
-
- describe('focused', () => {
- it('adds is-focused class', () => {
- expect(vm.$el.classList).toContain('is-focused');
- });
-
- it('does not have is-focused class when not focused', done => {
- vm.focused = false;
-
- vm.$nextTick(() => {
- expect(vm.$el.classList).not.toContain('is-focused');
-
- done();
- });
- });
- });
-
- describe('changed file icon', () => {
- it('does not render when not a changed or temp file', () => {
- expect(vm.$el.querySelector('.diff-changed-stats')).toBe(null);
- });
-
- it('renders when a changed file', done => {
- vm.file.changed = true;
-
- vm.$nextTick(() => {
- expect(vm.$el.querySelector('.diff-changed-stats')).not.toBe(null);
-
- done();
- });
- });
-
- it('renders when a temp file', done => {
- vm.file.tempFile = true;
-
- vm.$nextTick(() => {
- expect(vm.$el.querySelector('.diff-changed-stats')).not.toBe(null);
-
- done();
- });
- });
- });
-
- it('emits event when clicked', () => {
- spyOn(vm, '$emit');
-
- vm.$el.click();
-
- expect(vm.$emit).toHaveBeenCalledWith('click', vm.file);
- });
-
- describe('path', () => {
- let el;
-
- beforeEach(done => {
- vm.searchText = 'file';
-
- el = vm.$el.querySelector('.diff-changed-file-path');
-
- vm.$nextTick(done);
- });
-
- it('highlights text', () => {
- expect(el.querySelectorAll('.highlighted').length).toBe(4);
- });
-
- it('adds ellipsis to long text', done => {
- vm.file.path = new Array(70)
- .fill()
- .map((_, i) => `${i}-`)
- .join('');
-
- vm.$nextTick(() => {
- expect(el.textContent).toBe(`...${vm.file.path.substr(vm.file.path.length - 60)}`);
- done();
- });
- });
- });
-
- describe('name', () => {
- let el;
-
- beforeEach(done => {
- vm.searchText = 'file';
-
- el = vm.$el.querySelector('.diff-changed-file-name');
-
- vm.$nextTick(done);
- });
-
- it('highlights text', () => {
- expect(el.querySelectorAll('.highlighted').length).toBe(4);
- });
-
- it('does not add ellipsis to long text', done => {
- vm.file.name = new Array(70)
- .fill()
- .map((_, i) => `${i}-`)
- .join('');
-
- vm.$nextTick(() => {
- expect(el.textContent).not.toBe(`...${vm.file.name.substr(vm.file.name.length - 60)}`);
- done();
- });
- });
- });
-});
diff --git a/spec/javascripts/vue_shared/components/filtered_search_dropdown_spec.js b/spec/javascripts/vue_shared/components/filtered_search_dropdown_spec.js
deleted file mode 100644
index 0bb4a04557b..00000000000
--- a/spec/javascripts/vue_shared/components/filtered_search_dropdown_spec.js
+++ /dev/null
@@ -1,190 +0,0 @@
-import Vue from 'vue';
-import mountComponent from 'spec/helpers/vue_mount_component_helper';
-import component from '~/vue_shared/components/filtered_search_dropdown.vue';
-
-describe('Filtered search dropdown', () => {
- const Component = Vue.extend(component);
- let vm;
-
- afterEach(() => {
- vm.$destroy();
- });
-
- describe('with an empty array of items', () => {
- beforeEach(() => {
- vm = mountComponent(Component, {
- items: [],
- filterKey: '',
- });
- });
-
- it('renders empty list', () => {
- expect(vm.$el.querySelectorAll('.js-filtered-dropdown-result').length).toEqual(0);
- });
-
- it('renders filter input', () => {
- expect(vm.$el.querySelector('.js-filtered-dropdown-input')).not.toBeNull();
- });
- });
-
- describe('when visible numbers is less than the items length', () => {
- beforeEach(() => {
- vm = mountComponent(Component, {
- items: [{ title: 'One' }, { title: 'Two' }, { title: 'Three' }],
- visibleItems: 2,
- filterKey: 'title',
- });
- });
-
- it('it renders only the maximum number provided', () => {
- expect(vm.$el.querySelectorAll('.js-filtered-dropdown-result').length).toEqual(2);
- });
- });
-
- describe('when visible number is bigger than the items length', () => {
- beforeEach(() => {
- vm = mountComponent(Component, {
- items: [{ title: 'One' }, { title: 'Two' }, { title: 'Three' }],
- filterKey: 'title',
- });
- });
-
- it('it renders the full list of items the maximum number provided', () => {
- expect(vm.$el.querySelectorAll('.js-filtered-dropdown-result').length).toEqual(3);
- });
- });
-
- describe('while filtering', () => {
- beforeEach(() => {
- vm = mountComponent(Component, {
- items: [
- { title: 'One' },
- { title: 'Two/three' },
- { title: 'Three four' },
- { title: 'Five' },
- ],
- filterKey: 'title',
- });
- });
-
- it('updates the results to match the typed value', done => {
- vm.$el.querySelector('.js-filtered-dropdown-input').value = 'three';
- vm.$el.querySelector('.js-filtered-dropdown-input').dispatchEvent(new Event('input'));
- vm.$nextTick(() => {
- expect(vm.$el.querySelectorAll('.js-filtered-dropdown-result').length).toEqual(2);
- done();
- });
- });
-
- describe('when no value matches the typed one', () => {
- it('does not render any result', done => {
- vm.$el.querySelector('.js-filtered-dropdown-input').value = 'six';
- vm.$el.querySelector('.js-filtered-dropdown-input').dispatchEvent(new Event('input'));
-
- vm.$nextTick(() => {
- expect(vm.$el.querySelectorAll('.js-filtered-dropdown-result').length).toEqual(0);
- done();
- });
- });
- });
- });
-
- describe('with create mode enabled', () => {
- describe('when there are no matches', () => {
- beforeEach(() => {
- vm = mountComponent(Component, {
- items: [
- { title: 'One' },
- { title: 'Two/three' },
- { title: 'Three four' },
- { title: 'Five' },
- ],
- filterKey: 'title',
- showCreateMode: true,
- });
-
- vm.$el.querySelector('.js-filtered-dropdown-input').value = 'eleven';
- vm.$el.querySelector('.js-filtered-dropdown-input').dispatchEvent(new Event('input'));
- });
-
- it('renders a create button', done => {
- vm.$nextTick(() => {
- expect(vm.$el.querySelector('.js-dropdown-create-button')).not.toBeNull();
- done();
- });
- });
-
- it('renders computed button text', done => {
- vm.$nextTick(() => {
- expect(vm.$el.querySelector('.js-dropdown-create-button').textContent.trim()).toEqual(
- 'Create eleven',
- );
- done();
- });
- });
-
- describe('on click create button', () => {
- it('emits createItem event with the filter', done => {
- spyOn(vm, '$emit');
- vm.$nextTick(() => {
- vm.$el.querySelector('.js-dropdown-create-button').click();
-
- expect(vm.$emit).toHaveBeenCalledWith('createItem', 'eleven');
- done();
- });
- });
- });
- });
-
- describe('when there are matches', () => {
- beforeEach(() => {
- vm = mountComponent(Component, {
- items: [
- { title: 'One' },
- { title: 'Two/three' },
- { title: 'Three four' },
- { title: 'Five' },
- ],
- filterKey: 'title',
- showCreateMode: true,
- });
-
- vm.$el.querySelector('.js-filtered-dropdown-input').value = 'one';
- vm.$el.querySelector('.js-filtered-dropdown-input').dispatchEvent(new Event('input'));
- });
-
- it('does not render a create button', done => {
- vm.$nextTick(() => {
- expect(vm.$el.querySelector('.js-dropdown-create-button')).toBeNull();
- done();
- });
- });
- });
- });
-
- describe('with create mode disabled', () => {
- describe('when there are no matches', () => {
- beforeEach(() => {
- vm = mountComponent(Component, {
- items: [
- { title: 'One' },
- { title: 'Two/three' },
- { title: 'Three four' },
- { title: 'Five' },
- ],
- filterKey: 'title',
- });
-
- vm.$el.querySelector('.js-filtered-dropdown-input').value = 'eleven';
- vm.$el.querySelector('.js-filtered-dropdown-input').dispatchEvent(new Event('input'));
- });
-
- it('does not render a create button', done => {
- vm.$nextTick(() => {
- expect(vm.$el.querySelector('.js-dropdown-create-button')).toBeNull();
- done();
- });
- });
- });
- });
-});
diff --git a/spec/javascripts/vue_shared/components/gl_countdown_spec.js b/spec/javascripts/vue_shared/components/gl_countdown_spec.js
deleted file mode 100644
index 929ffe219f4..00000000000
--- a/spec/javascripts/vue_shared/components/gl_countdown_spec.js
+++ /dev/null
@@ -1,77 +0,0 @@
-import mountComponent from 'spec/helpers/vue_mount_component_helper';
-import Vue from 'vue';
-import GlCountdown from '~/vue_shared/components/gl_countdown.vue';
-
-describe('GlCountdown', () => {
- const Component = Vue.extend(GlCountdown);
- let vm;
- let now = '2000-01-01T00:00:00Z';
-
- beforeEach(() => {
- spyOn(Date, 'now').and.callFake(() => new Date(now).getTime());
- jasmine.clock().install();
- });
-
- afterEach(() => {
- vm.$destroy();
- jasmine.clock().uninstall();
- });
-
- describe('when there is time remaining', () => {
- beforeEach(done => {
- vm = mountComponent(Component, {
- endDateString: '2000-01-01T01:02:03Z',
- });
-
- Vue.nextTick()
- .then(done)
- .catch(done.fail);
- });
-
- it('displays remaining time', () => {
- expect(vm.$el).toContainText('01:02:03');
- });
-
- it('updates remaining time', done => {
- now = '2000-01-01T00:00:01Z';
- jasmine.clock().tick(1000);
-
- Vue.nextTick()
- .then(() => {
- expect(vm.$el).toContainText('01:02:02');
- done();
- })
- .catch(done.fail);
- });
- });
-
- describe('when there is no time remaining', () => {
- beforeEach(done => {
- vm = mountComponent(Component, {
- endDateString: '1900-01-01T00:00:00Z',
- });
-
- Vue.nextTick()
- .then(done)
- .catch(done.fail);
- });
-
- it('displays 00:00:00', () => {
- expect(vm.$el).toContainText('00:00:00');
- });
- });
-
- describe('when an invalid date is passed', () => {
- it('throws a validation error', () => {
- spyOn(Vue.config, 'warnHandler').and.stub();
- vm = mountComponent(Component, {
- endDateString: 'this is invalid',
- });
-
- expect(Vue.config.warnHandler).toHaveBeenCalledTimes(1);
- const [errorMessage] = Vue.config.warnHandler.calls.argsFor(0);
-
- expect(errorMessage).toMatch(/^Invalid prop: .* "endDateString"/);
- });
- });
-});
diff --git a/spec/javascripts/vue_shared/components/header_ci_component_spec.js b/spec/javascripts/vue_shared/components/header_ci_component_spec.js
deleted file mode 100644
index b1abc972e1d..00000000000
--- a/spec/javascripts/vue_shared/components/header_ci_component_spec.js
+++ /dev/null
@@ -1,93 +0,0 @@
-import Vue from 'vue';
-import mountComponent, { mountComponentWithSlots } from 'spec/helpers/vue_mount_component_helper';
-import headerCi from '~/vue_shared/components/header_ci_component.vue';
-
-describe('Header CI Component', () => {
- let HeaderCi;
- let vm;
- let props;
-
- beforeEach(() => {
- HeaderCi = Vue.extend(headerCi);
- props = {
- status: {
- group: 'failed',
- icon: 'status_failed',
- label: 'failed',
- text: 'failed',
- details_path: 'path',
- },
- itemName: 'job',
- itemId: 123,
- time: '2017-05-08T14:57:39.781Z',
- user: {
- web_url: 'path',
- name: 'Foo',
- username: 'foobar',
- email: 'foo@bar.com',
- avatar_url: 'link',
- },
- hasSidebarButton: true,
- };
- });
-
- afterEach(() => {
- vm.$destroy();
- });
-
- const findActionButtons = () => vm.$el.querySelector('.header-action-buttons');
-
- describe('render', () => {
- beforeEach(() => {
- vm = mountComponent(HeaderCi, props);
- });
-
- it('should render status badge', () => {
- expect(vm.$el.querySelector('.ci-failed')).toBeDefined();
- expect(vm.$el.querySelector('.ci-status-icon-failed svg')).toBeDefined();
- expect(vm.$el.querySelector('.ci-failed').getAttribute('href')).toEqual(
- props.status.details_path,
- );
- });
-
- it('should render item name and id', () => {
- expect(vm.$el.querySelector('strong').textContent.trim()).toEqual('job #123');
- });
-
- it('should render timeago date', () => {
- expect(vm.$el.querySelector('time')).toBeDefined();
- });
-
- it('should render user icon and name', () => {
- expect(vm.$el.querySelector('.js-user-link').innerText.trim()).toContain(props.user.name);
- });
-
- it('should render sidebar toggle button', () => {
- expect(vm.$el.querySelector('.js-sidebar-build-toggle')).not.toBeNull();
- });
-
- it('should not render header action buttons when empty', () => {
- expect(findActionButtons()).toBeNull();
- });
- });
-
- describe('slot', () => {
- it('should render header action buttons', () => {
- vm = mountComponentWithSlots(HeaderCi, { props, slots: { default: 'Test Actions' } });
-
- const buttons = findActionButtons();
-
- expect(buttons).not.toBeNull();
- expect(buttons.textContent).toEqual('Test Actions');
- });
- });
-
- describe('shouldRenderTriggeredLabel', () => {
- it('should rendered created keyword when the shouldRenderTriggeredLabel is false', () => {
- vm = mountComponent(HeaderCi, { ...props, shouldRenderTriggeredLabel: false });
-
- expect(vm.$el.textContent).toContain('created');
- expect(vm.$el.textContent).not.toContain('triggered');
- });
- });
-});
diff --git a/spec/javascripts/vue_shared/components/markdown/suggestions_spec.js b/spec/javascripts/vue_shared/components/markdown/suggestions_spec.js
deleted file mode 100644
index b7de40b4831..00000000000
--- a/spec/javascripts/vue_shared/components/markdown/suggestions_spec.js
+++ /dev/null
@@ -1,106 +0,0 @@
-import Vue from 'vue';
-import SuggestionsComponent from '~/vue_shared/components/markdown/suggestions.vue';
-
-const MOCK_DATA = {
- suggestions: [
- {
- id: 1,
- appliable: true,
- applied: false,
- current_user: {
- can_apply: true,
- },
- diff_lines: [
- {
- can_receive_suggestion: false,
- line_code: null,
- meta_data: null,
- new_line: null,
- old_line: 5,
- rich_text: '-test',
- text: '-test',
- type: 'old',
- },
- {
- can_receive_suggestion: true,
- line_code: null,
- meta_data: null,
- new_line: 5,
- old_line: null,
- rich_text: '+new test',
- text: '+new test',
- type: 'new',
- },
- ],
- },
- ],
- noteHtml: `
- <div class="suggestion">
- <div class="line">-oldtest</div>
- </div>
- <div class="suggestion">
- <div class="line">+newtest</div>
- </div>
- `,
- isApplied: false,
- helpPagePath: 'path_to_docs',
-};
-
-describe('Suggestion component', () => {
- let vm;
- let diffTable;
-
- beforeEach(done => {
- const Component = Vue.extend(SuggestionsComponent);
-
- vm = new Component({
- propsData: MOCK_DATA,
- }).$mount();
-
- diffTable = vm.generateDiff(0).$mount().$el;
-
- spyOn(vm, 'renderSuggestions');
- vm.renderSuggestions();
- Vue.nextTick(done);
- });
-
- describe('mounted', () => {
- it('renders a flash container', () => {
- expect(vm.$el.querySelector('.js-suggestions-flash')).not.toBeNull();
- });
-
- it('renders a container for suggestions', () => {
- expect(vm.$refs.container).not.toBeNull();
- });
-
- it('renders suggestions', () => {
- expect(vm.renderSuggestions).toHaveBeenCalled();
- expect(vm.$el.innerHTML.includes('oldtest')).toBe(true);
- expect(vm.$el.innerHTML.includes('newtest')).toBe(true);
- });
- });
-
- describe('generateDiff', () => {
- it('generates a diff table', () => {
- expect(diffTable.querySelector('.md-suggestion-diff')).not.toBeNull();
- });
-
- it('generates a diff table that contains contents of `oldLineContent`', () => {
- expect(diffTable.innerHTML.includes(vm.fromContent)).toBe(true);
- });
-
- it('generates a diff table that contains contents the suggested lines', () => {
- MOCK_DATA.suggestions[0].diff_lines.forEach(line => {
- const text = line.text.substring(1);
-
- expect(diffTable.innerHTML.includes(text)).toBe(true);
- });
- });
-
- it('generates a diff table with the correct line number for each suggested line', () => {
- const lines = diffTable.querySelectorAll('.old_line');
-
- expect(parseInt([...lines][0].innerHTML, 10)).toBe(5);
- });
- });
-});
diff --git a/spec/javascripts/vue_shared/components/markdown/toolbar_spec.js b/spec/javascripts/vue_shared/components/markdown/toolbar_spec.js
deleted file mode 100644
index 288eb40cc76..00000000000
--- a/spec/javascripts/vue_shared/components/markdown/toolbar_spec.js
+++ /dev/null
@@ -1,40 +0,0 @@
-import Vue from 'vue';
-import mountComponent from 'spec/helpers/vue_mount_component_helper';
-import toolbar from '~/vue_shared/components/markdown/toolbar.vue';
-
-describe('toolbar', () => {
- let vm;
- const Toolbar = Vue.extend(toolbar);
- const props = {
- markdownDocsPath: '',
- };
-
- afterEach(() => {
- vm.$destroy();
- });
-
- describe('user can attach file', () => {
- beforeEach(() => {
- vm = mountComponent(Toolbar, props);
- });
-
- it('should render uploading-container', () => {
- expect(vm.$el.querySelector('.uploading-container')).not.toBeNull();
- });
- });
-
- describe('user cannot attach file', () => {
- beforeEach(() => {
- vm = mountComponent(
- Toolbar,
- Object.assign({}, props, {
- canAttachFile: false,
- }),
- );
- });
-
- it('should not render uploading-container', () => {
- expect(vm.$el.querySelector('.uploading-container')).toBeNull();
- });
- });
-});
diff --git a/spec/javascripts/vue_shared/components/navigation_tabs_spec.js b/spec/javascripts/vue_shared/components/navigation_tabs_spec.js
deleted file mode 100644
index beb980a6556..00000000000
--- a/spec/javascripts/vue_shared/components/navigation_tabs_spec.js
+++ /dev/null
@@ -1,64 +0,0 @@
-import Vue from 'vue';
-import mountComponent from 'spec/helpers/vue_mount_component_helper';
-import navigationTabs from '~/vue_shared/components/navigation_tabs.vue';
-
-describe('navigation tabs component', () => {
- let vm;
- let Component;
- let data;
-
- beforeEach(() => {
- data = [
- {
- name: 'All',
- scope: 'all',
- count: 1,
- isActive: true,
- },
- {
- name: 'Pending',
- scope: 'pending',
- count: 0,
- isActive: false,
- },
- {
- name: 'Running',
- scope: 'running',
- isActive: false,
- },
- ];
-
- Component = Vue.extend(navigationTabs);
- vm = mountComponent(Component, { tabs: data, scope: 'pipelines' });
- });
-
- afterEach(() => {
- vm.$destroy();
- });
-
- it('should render tabs', () => {
- expect(vm.$el.querySelectorAll('li').length).toEqual(data.length);
- });
-
- it('should render active tab', () => {
- expect(vm.$el.querySelector('.active .js-pipelines-tab-all')).toBeDefined();
- });
-
- it('should render badge', () => {
- expect(vm.$el.querySelector('.js-pipelines-tab-all .badge').textContent.trim()).toEqual('1');
- expect(vm.$el.querySelector('.js-pipelines-tab-pending .badge').textContent.trim()).toEqual(
- '0',
- );
- });
-
- it('should not render badge', () => {
- expect(vm.$el.querySelector('.js-pipelines-tab-running .badge')).toEqual(null);
- });
-
- it('should trigger onTabClick', () => {
- spyOn(vm, '$emit');
- vm.$el.querySelector('.js-pipelines-tab-pending').click();
-
- expect(vm.$emit).toHaveBeenCalledWith('onChangeTab', 'pending');
- });
-});
diff --git a/spec/javascripts/vue_shared/components/pikaday_spec.js b/spec/javascripts/vue_shared/components/pikaday_spec.js
deleted file mode 100644
index b787ba7596f..00000000000
--- a/spec/javascripts/vue_shared/components/pikaday_spec.js
+++ /dev/null
@@ -1,30 +0,0 @@
-import Vue from 'vue';
-import mountComponent from 'spec/helpers/vue_mount_component_helper';
-import datePicker from '~/vue_shared/components/pikaday.vue';
-
-describe('datePicker', () => {
- let vm;
- beforeEach(() => {
- const DatePicker = Vue.extend(datePicker);
- vm = mountComponent(DatePicker, {
- label: 'label',
- });
- });
-
- it('should render label text', () => {
- expect(vm.$el.querySelector('.dropdown-toggle-text').innerText.trim()).toEqual('label');
- });
-
- it('should show calendar', () => {
- expect(vm.$el.querySelector('.pika-single')).toBeDefined();
- });
-
- it('should toggle when dropdown is clicked', () => {
- const hidePicker = jasmine.createSpy();
- vm.$on('hidePicker', hidePicker);
-
- vm.$el.querySelector('.dropdown-menu-toggle').click();
-
- expect(hidePicker).toHaveBeenCalled();
- });
-});
diff --git a/spec/javascripts/vue_shared/components/project_avatar/default_spec.js b/spec/javascripts/vue_shared/components/project_avatar/default_spec.js
deleted file mode 100644
index 2ec19ebf80e..00000000000
--- a/spec/javascripts/vue_shared/components/project_avatar/default_spec.js
+++ /dev/null
@@ -1,58 +0,0 @@
-import Vue from 'vue';
-import mountComponent from 'spec/helpers/vue_mount_component_helper';
-import { projectData } from 'spec/ide/mock_data';
-import { TEST_HOST } from 'spec/test_constants';
-import { getFirstCharacterCapitalized } from '~/lib/utils/text_utility';
-import ProjectAvatarDefault from '~/vue_shared/components/project_avatar/default.vue';
-
-describe('ProjectAvatarDefault component', () => {
- const Component = Vue.extend(ProjectAvatarDefault);
- let vm;
-
- beforeEach(() => {
- vm = mountComponent(Component, {
- project: projectData,
- });
- });
-
- afterEach(() => {
- vm.$destroy();
- });
-
- it('renders identicon if project has no avatar_url', done => {
- const expectedText = getFirstCharacterCapitalized(projectData.name);
-
- vm.project = {
- ...vm.project,
- avatar_url: null,
- };
-
- vm.$nextTick()
- .then(() => {
- const identiconEl = vm.$el.querySelector('.identicon');
-
- expect(identiconEl).not.toBe(null);
- expect(identiconEl.textContent.trim()).toEqual(expectedText);
- })
- .then(done)
- .catch(done.fail);
- });
-
- it('renders avatar image if project has avatar_url', done => {
- const avatarUrl = `${TEST_HOST}/images/home/nasa.svg`;
-
- vm.project = {
- ...vm.project,
- avatar_url: avatarUrl,
- };
-
- vm.$nextTick()
- .then(() => {
- expect(vm.$el).toContainElement('.avatar');
- expect(vm.$el).not.toContainElement('.identicon');
- expect(vm.$el.querySelector('img')).toHaveAttr('src', avatarUrl);
- })
- .then(done)
- .catch(done.fail);
- });
-});
diff --git a/spec/javascripts/vue_shared/components/project_selector/project_list_item_spec.js b/spec/javascripts/vue_shared/components/project_selector/project_list_item_spec.js
deleted file mode 100644
index e73fb97b741..00000000000
--- a/spec/javascripts/vue_shared/components/project_selector/project_list_item_spec.js
+++ /dev/null
@@ -1,109 +0,0 @@
-import { shallowMount, createLocalVue } from '@vue/test-utils';
-import { trimText } from 'spec/helpers/text_helper';
-import ProjectListItem from '~/vue_shared/components/project_selector/project_list_item.vue';
-
-const localVue = createLocalVue();
-
-describe('ProjectListItem component', () => {
- const Component = localVue.extend(ProjectListItem);
- let wrapper;
- let vm;
- let options;
- loadJSONFixtures('static/projects.json');
- const project = getJSONFixture('static/projects.json')[0];
-
- beforeEach(() => {
- options = {
- propsData: {
- project,
- selected: false,
- },
- localVue,
- };
- });
-
- afterEach(() => {
- wrapper.vm.$destroy();
- });
-
- it('does not render a check mark icon if selected === false', () => {
- wrapper = shallowMount(Component, options);
-
- expect(wrapper.contains('.js-selected-icon.js-unselected')).toBe(true);
- });
-
- it('renders a check mark icon if selected === true', () => {
- options.propsData.selected = true;
-
- wrapper = shallowMount(Component, options);
-
- expect(wrapper.contains('.js-selected-icon.js-selected')).toBe(true);
- });
-
- it(`emits a "clicked" event when clicked`, () => {
- wrapper = shallowMount(Component, options);
- ({ vm } = wrapper);
-
- spyOn(vm, '$emit');
- wrapper.vm.onClick();
-
- expect(wrapper.vm.$emit).toHaveBeenCalledWith('click');
- });
-
- it(`renders the project avatar`, () => {
- wrapper = shallowMount(Component, options);
-
- expect(wrapper.contains('.js-project-avatar')).toBe(true);
- });
-
- it(`renders a simple namespace name with a trailing slash`, () => {
- options.propsData.project.name_with_namespace = 'a / b';
-
- wrapper = shallowMount(Component, options);
- const renderedNamespace = trimText(wrapper.find('.js-project-namespace').text());
-
- expect(renderedNamespace).toBe('a /');
- });
-
- it(`renders a properly truncated namespace with a trailing slash`, () => {
- options.propsData.project.name_with_namespace = 'a / b / c / d / e / f';
-
- wrapper = shallowMount(Component, options);
- const renderedNamespace = trimText(wrapper.find('.js-project-namespace').text());
-
- expect(renderedNamespace).toBe('a / ... / e /');
- });
-
- it(`renders the project name`, () => {
- options.propsData.project.name = 'my-test-project';
-
- wrapper = shallowMount(Component, options);
- const renderedName = trimText(wrapper.find('.js-project-name').text());
-
- expect(renderedName).toBe('my-test-project');
- });
-
- it(`renders the project name with highlighting in the case of a search query match`, () => {
- options.propsData.project.name = 'my-test-project';
- options.propsData.matcher = 'pro';
-
- wrapper = shallowMount(Component, options);
- const renderedName = trimText(wrapper.find('.js-project-name').html());
- const expected = 'my-test-<b>p</b><b>r</b><b>o</b>ject';
-
- expect(renderedName).toContain(expected);
- });
-
- it('prevents search query and project name XSS', () => {
- const alertSpy = spyOn(window, 'alert');
- options.propsData.project.name = "my-xss-pro<script>alert('XSS');</script>ject";
- options.propsData.matcher = "pro<script>alert('XSS');</script>";
-
- wrapper = shallowMount(Component, options);
- const renderedName = trimText(wrapper.find('.js-project-name').html());
- const expected = 'my-xss-project';
-
- expect(renderedName).toContain(expected);
- expect(alertSpy).not.toHaveBeenCalled();
- });
-});
diff --git a/spec/javascripts/vue_shared/components/project_selector/project_selector_spec.js b/spec/javascripts/vue_shared/components/project_selector/project_selector_spec.js
deleted file mode 100644
index 5d995f06abb..00000000000
--- a/spec/javascripts/vue_shared/components/project_selector/project_selector_spec.js
+++ /dev/null
@@ -1,142 +0,0 @@
-import Vue from 'vue';
-import { head } from 'lodash';
-
-import { GlSearchBoxByType, GlInfiniteScroll } from '@gitlab/ui';
-import { mount, createLocalVue } from '@vue/test-utils';
-import { trimText } from 'spec/helpers/text_helper';
-import ProjectListItem from '~/vue_shared/components/project_selector/project_list_item.vue';
-import ProjectSelector from '~/vue_shared/components/project_selector/project_selector.vue';
-
-const localVue = createLocalVue();
-
-describe('ProjectSelector component', () => {
- let wrapper;
- let vm;
- loadJSONFixtures('static/projects.json');
- const allProjects = getJSONFixture('static/projects.json');
- const searchResults = allProjects.slice(0, 5);
- let selected = [];
- selected = selected.concat(allProjects.slice(0, 3)).concat(allProjects.slice(5, 8));
-
- const findSearchInput = () => wrapper.find(GlSearchBoxByType).find('input');
-
- beforeEach(() => {
- jasmine.clock().install();
- jasmine.clock().mockDate();
-
- wrapper = mount(Vue.extend(ProjectSelector), {
- localVue,
- propsData: {
- projectSearchResults: searchResults,
- selectedProjects: selected,
- showNoResultsMessage: false,
- showMinimumSearchQueryMessage: false,
- showLoadingIndicator: false,
- showSearchErrorMessage: false,
- },
- attachToDocument: true,
- });
-
- ({ vm } = wrapper);
- });
-
- afterEach(() => {
- jasmine.clock().uninstall();
- vm.$destroy();
- });
-
- it('renders the search results', () => {
- expect(wrapper.findAll('.js-project-list-item').length).toBe(5);
- });
-
- it(`triggers a (debounced) search when the search input value changes`, () => {
- spyOn(vm, '$emit');
- const query = 'my test query!';
- const searchInput = findSearchInput();
-
- searchInput.setValue(query);
- searchInput.trigger('input');
-
- expect(vm.$emit).not.toHaveBeenCalledWith();
- jasmine.clock().tick(501);
-
- expect(vm.$emit).toHaveBeenCalledWith('searched', query);
- });
-
- it(`debounces the search input`, () => {
- spyOn(vm, '$emit');
- const searchInput = findSearchInput();
-
- const updateSearchQuery = (count = 0) => {
- if (count === 10) {
- jasmine.clock().tick(101);
-
- expect(vm.$emit).toHaveBeenCalledTimes(1);
- expect(vm.$emit).toHaveBeenCalledWith('searched', `search query #9`);
- } else {
- searchInput.setValue(`search query #${count}`);
- searchInput.trigger('input');
-
- jasmine.clock().tick(400);
- updateSearchQuery(count + 1);
- }
- };
-
- updateSearchQuery();
- });
-
- it(`includes a placeholder in the search box`, () => {
- const searchInput = findSearchInput();
-
- expect(searchInput.attributes('placeholder')).toBe('Search your projects');
- });
-
- it(`triggers a "bottomReached" event when user has scrolled to the bottom of the list`, () => {
- spyOn(vm, '$emit');
- wrapper.find(GlInfiniteScroll).vm.$emit('bottomReached');
-
- expect(vm.$emit).toHaveBeenCalledWith('bottomReached');
- });
-
- it(`triggers a "projectClicked" event when a project is clicked`, () => {
- spyOn(vm, '$emit');
- wrapper.find(ProjectListItem).vm.$emit('click', head(searchResults));
-
- expect(vm.$emit).toHaveBeenCalledWith('projectClicked', head(searchResults));
- });
-
- it(`shows a "no results" message if showNoResultsMessage === true`, () => {
- wrapper.setProps({ showNoResultsMessage: true });
-
- return vm.$nextTick().then(() => {
- const noResultsEl = wrapper.find('.js-no-results-message');
-
- expect(noResultsEl.exists()).toBe(true);
- expect(trimText(noResultsEl.text())).toEqual('Sorry, no projects matched your search');
- });
- });
-
- it(`shows a "minimum search query" message if showMinimumSearchQueryMessage === true`, () => {
- wrapper.setProps({ showMinimumSearchQueryMessage: true });
-
- return vm.$nextTick().then(() => {
- const minimumSearchEl = wrapper.find('.js-minimum-search-query-message');
-
- expect(minimumSearchEl.exists()).toBe(true);
- expect(trimText(minimumSearchEl.text())).toEqual('Enter at least three characters to search');
- });
- });
-
- it(`shows a error message if showSearchErrorMessage === true`, () => {
- wrapper.setProps({ showSearchErrorMessage: true });
-
- return vm.$nextTick().then(() => {
- const errorMessageEl = wrapper.find('.js-search-error-message');
-
- expect(errorMessageEl.exists()).toBe(true);
- expect(trimText(errorMessageEl.text())).toEqual(
- 'Something went wrong, unable to search projects',
- );
- });
- });
-});
diff --git a/spec/javascripts/vue_shared/components/stacked_progress_bar_spec.js b/spec/javascripts/vue_shared/components/stacked_progress_bar_spec.js
deleted file mode 100644
index c062ee13231..00000000000
--- a/spec/javascripts/vue_shared/components/stacked_progress_bar_spec.js
+++ /dev/null
@@ -1,107 +0,0 @@
-import Vue from 'vue';
-
-import mountComponent from 'spec/helpers/vue_mount_component_helper';
-import stackedProgressBarComponent from '~/vue_shared/components/stacked_progress_bar.vue';
-
-const createComponent = config => {
- const Component = Vue.extend(stackedProgressBarComponent);
- const defaultConfig = Object.assign(
- {},
- {
- successLabel: 'Synced',
- failureLabel: 'Failed',
- neutralLabel: 'Out of sync',
- successCount: 25,
- failureCount: 10,
- totalCount: 5000,
- },
- config,
- );
-
- return mountComponent(Component, defaultConfig);
-};
-
-describe('StackedProgressBarComponent', () => {
- let vm;
-
- beforeEach(() => {
- vm = createComponent();
- });
-
- afterEach(() => {
- vm.$destroy();
- });
-
- describe('computed', () => {
- describe('neutralCount', () => {
- it('returns neutralCount based on totalCount, successCount and failureCount', () => {
- expect(vm.neutralCount).toBe(4965); // 5000 - 25 - 10
- });
- });
- });
-
- describe('methods', () => {
- describe('getPercent', () => {
- it('returns percentage from provided count based on `totalCount`', () => {
- expect(vm.getPercent(500)).toBe(10);
- });
-
- it('returns percentage with decimal place from provided count based on `totalCount`', () => {
- expect(vm.getPercent(67)).toBe(1.3);
- });
-
- it('returns percentage as `< 1` from provided count based on `totalCount` when evaluated value is less than 1', () => {
- expect(vm.getPercent(10)).toBe('< 1');
- });
-
- it('returns 0 if totalCount is falsy', () => {
- vm = createComponent({ totalCount: 0 });
-
- expect(vm.getPercent(100)).toBe(0);
- });
- });
-
- describe('barStyle', () => {
- it('returns style string based on percentage provided', () => {
- expect(vm.barStyle(50)).toBe('width: 50%;');
- });
- });
-
- describe('getTooltip', () => {
- describe('when hideTooltips is false', () => {
- it('returns label string based on label and count provided', () => {
- expect(vm.getTooltip('Synced', 10)).toBe('Synced: 10');
- });
- });
-
- describe('when hideTooltips is true', () => {
- beforeEach(() => {
- vm = createComponent({ hideTooltips: true });
- });
-
- it('returns an empty string', () => {
- expect(vm.getTooltip('Synced', 10)).toBe('');
- });
- });
- });
- });
-
- describe('template', () => {
- it('renders container element', () => {
- expect(vm.$el.classList.contains('stacked-progress-bar')).toBeTruthy();
- });
-
- it('renders empty state when count is unavailable', () => {
- const vmX = createComponent({ totalCount: 0, successCount: 0, failureCount: 0 });
-
- expect(vmX.$el.querySelectorAll('.status-unavailable').length).not.toBe(0);
- vmX.$destroy();
- });
-
- it('renders bar elements when count is available', () => {
- expect(vm.$el.querySelectorAll('.status-green').length).not.toBe(0);
- expect(vm.$el.querySelectorAll('.status-neutral').length).not.toBe(0);
- expect(vm.$el.querySelectorAll('.status-red').length).not.toBe(0);
- });
- });
-});
diff --git a/spec/javascripts/vue_shared/components/tabs/tab_spec.js b/spec/javascripts/vue_shared/components/tabs/tab_spec.js
deleted file mode 100644
index 8437fe37738..00000000000
--- a/spec/javascripts/vue_shared/components/tabs/tab_spec.js
+++ /dev/null
@@ -1,32 +0,0 @@
-import Vue from 'vue';
-import mountComponent from 'spec/helpers/vue_mount_component_helper';
-import Tab from '~/vue_shared/components/tabs/tab.vue';
-
-describe('Tab component', () => {
- const Component = Vue.extend(Tab);
- let vm;
-
- beforeEach(() => {
- vm = mountComponent(Component);
- });
-
- it('sets localActive to equal active', done => {
- vm.active = true;
-
- vm.$nextTick(() => {
- expect(vm.localActive).toBe(true);
-
- done();
- });
- });
-
- it('sets active class', done => {
- vm.active = true;
-
- vm.$nextTick(() => {
- expect(vm.$el.classList).toContain('active');
-
- done();
- });
- });
-});
diff --git a/spec/javascripts/vue_shared/components/tabs/tabs_spec.js b/spec/javascripts/vue_shared/components/tabs/tabs_spec.js
deleted file mode 100644
index 50ba18cd338..00000000000
--- a/spec/javascripts/vue_shared/components/tabs/tabs_spec.js
+++ /dev/null
@@ -1,68 +0,0 @@
-import Vue from 'vue';
-import Tabs from '~/vue_shared/components/tabs/tabs';
-import Tab from '~/vue_shared/components/tabs/tab.vue';
-
-describe('Tabs component', () => {
- let vm;
-
- beforeEach(done => {
- vm = new Vue({
- components: {
- Tabs,
- Tab,
- },
- template: `
- <div>
- <tabs>
- <tab title="Testing" active>
- First tab
- </tab>
- <tab>
- <template slot="title">Test slot</template>
- Second tab
- </tab>
- </tabs>
- </div>
- `,
- }).$mount();
-
- setTimeout(done);
- });
-
- describe('tab links', () => {
- it('renders links for tabs', () => {
- expect(vm.$el.querySelectorAll('a').length).toBe(2);
- });
-
- it('renders link titles from props', () => {
- expect(vm.$el.querySelector('a').textContent).toContain('Testing');
- });
-
- it('renders link titles from slot', () => {
- expect(vm.$el.querySelectorAll('a')[1].textContent).toContain('Test slot');
- });
-
- it('renders active class', () => {
- expect(vm.$el.querySelector('a').classList).toContain('active');
- });
-
- it('updates active class on click', done => {
- vm.$el.querySelectorAll('a')[1].click();
-
- setTimeout(() => {
- expect(vm.$el.querySelector('a').classList).not.toContain('active');
- expect(vm.$el.querySelectorAll('a')[1].classList).toContain('active');
-
- done();
- });
- });
- });
-
- describe('content', () => {
- it('renders content panes', () => {
- expect(vm.$el.querySelectorAll('.tab-pane').length).toBe(2);
- expect(vm.$el.querySelectorAll('.tab-pane')[0].textContent).toContain('First tab');
- expect(vm.$el.querySelectorAll('.tab-pane')[1].textContent).toContain('Second tab');
- });
- });
-});
diff --git a/spec/javascripts/vue_shared/components/toggle_button_spec.js b/spec/javascripts/vue_shared/components/toggle_button_spec.js
deleted file mode 100644
index ea0a89a3ab5..00000000000
--- a/spec/javascripts/vue_shared/components/toggle_button_spec.js
+++ /dev/null
@@ -1,101 +0,0 @@
-import Vue from 'vue';
-import mountComponent from 'spec/helpers/vue_mount_component_helper';
-import toggleButton from '~/vue_shared/components/toggle_button.vue';
-
-describe('Toggle Button', () => {
- let vm;
- let Component;
-
- beforeEach(() => {
- Component = Vue.extend(toggleButton);
- });
-
- afterEach(() => {
- vm.$destroy();
- });
-
- describe('render output', () => {
- beforeEach(() => {
- vm = mountComponent(Component, {
- value: true,
- name: 'foo',
- });
- });
-
- it('renders input with provided name', () => {
- expect(vm.$el.querySelector('input').getAttribute('name')).toEqual('foo');
- });
-
- it('renders input with provided value', () => {
- expect(vm.$el.querySelector('input').getAttribute('value')).toEqual('true');
- });
-
- it('renders input status icon', () => {
- expect(vm.$el.querySelectorAll('span.toggle-icon').length).toEqual(1);
- expect(vm.$el.querySelectorAll('svg.s16.toggle-icon-svg').length).toEqual(1);
- });
- });
-
- describe('is-checked', () => {
- beforeEach(() => {
- vm = mountComponent(Component, {
- value: true,
- });
-
- spyOn(vm, '$emit');
- });
-
- it('renders is checked class', () => {
- expect(vm.$el.querySelector('button').classList.contains('is-checked')).toEqual(true);
- });
-
- it('sets aria-label representing toggle state', () => {
- vm.value = true;
-
- expect(vm.ariaLabel).toEqual('Toggle Status: ON');
-
- vm.value = false;
-
- expect(vm.ariaLabel).toEqual('Toggle Status: OFF');
- });
-
- it('emits change event when clicked', () => {
- vm.$el.querySelector('button').click();
-
- expect(vm.$emit).toHaveBeenCalledWith('change', false);
- });
- });
-
- describe('is-disabled', () => {
- beforeEach(() => {
- vm = mountComponent(Component, {
- value: true,
- disabledInput: true,
- });
- spyOn(vm, '$emit');
- });
-
- it('renders disabled button', () => {
- expect(vm.$el.querySelector('button').classList.contains('is-disabled')).toEqual(true);
- });
-
- it('does not emit change event when clicked', () => {
- vm.$el.querySelector('button').click();
-
- expect(vm.$emit).not.toHaveBeenCalled();
- });
- });
-
- describe('is-loading', () => {
- beforeEach(() => {
- vm = mountComponent(Component, {
- value: true,
- isLoading: true,
- });
- });
-
- it('renders loading class', () => {
- expect(vm.$el.querySelector('button').classList.contains('is-loading')).toEqual(true);
- });
- });
-});
diff --git a/spec/javascripts/vue_shared/components/user_avatar/user_avatar_svg_spec.js b/spec/javascripts/vue_shared/components/user_avatar/user_avatar_svg_spec.js
deleted file mode 100644
index 31644416439..00000000000
--- a/spec/javascripts/vue_shared/components/user_avatar/user_avatar_svg_spec.js
+++ /dev/null
@@ -1,29 +0,0 @@
-import Vue from 'vue';
-import avatarSvg from 'icons/_icon_random.svg';
-import UserAvatarSvg from '~/vue_shared/components/user_avatar/user_avatar_svg.vue';
-
-const UserAvatarSvgComponent = Vue.extend(UserAvatarSvg);
-
-describe('User Avatar Svg Component', function() {
- describe('Initialization', function() {
- beforeEach(function() {
- this.propsData = {
- size: 99,
- svg: avatarSvg,
- };
-
- this.userAvatarSvg = new UserAvatarSvgComponent({
- propsData: this.propsData,
- }).$mount();
- });
-
- it('should return a defined Vue component', function() {
- expect(this.userAvatarSvg).toBeDefined();
- });
-
- it('should have <svg> as a child element', function() {
- expect(this.userAvatarSvg.$el.tagName).toEqual('svg');
- expect(this.userAvatarSvg.$el.innerHTML).toContain('<path');
- });
- });
-});
diff --git a/spec/lib/api/entities/branch_spec.rb b/spec/lib/api/entities/branch_spec.rb
new file mode 100644
index 00000000000..604f56c0cb2
--- /dev/null
+++ b/spec/lib/api/entities/branch_spec.rb
@@ -0,0 +1,28 @@
+# frozen_string_literal: true
+
+require 'spec_helper'
+
+describe API::Entities::Branch do
+ describe '#as_json' do
+ subject { entity.as_json }
+
+ let(:project) { create(:project, :public, :repository) }
+ let(:repository) { project.repository }
+ let(:branch) { repository.find_branch('master') }
+ let(:entity) { described_class.new(branch, project: project) }
+
+ it 'includes basic fields', :aggregate_failures do
+ is_expected.to include(
+ name: 'master',
+ commit: a_kind_of(Hash),
+ merged: false,
+ protected: false,
+ developers_can_push: false,
+ developers_can_merge: false,
+ can_push: false,
+ default: true,
+ web_url: Gitlab::Routing.url_helpers.project_tree_url(project, 'master')
+ )
+ end
+ end
+end
diff --git a/spec/lib/api/entities/design_management/design_spec.rb b/spec/lib/api/entities/design_management/design_spec.rb
new file mode 100644
index 00000000000..50ca3b43c6a
--- /dev/null
+++ b/spec/lib/api/entities/design_management/design_spec.rb
@@ -0,0 +1,19 @@
+# frozen_string_literal: true
+
+require 'spec_helper'
+
+describe API::Entities::DesignManagement::Design do
+ let_it_be(:design) { create(:design) }
+ let(:entity) { described_class.new(design, request: double) }
+
+ subject { entity.as_json }
+
+ it 'has the correct attributes' do
+ expect(subject).to eq({
+ id: design.id,
+ project_id: design.project_id,
+ filename: design.filename,
+ image_url: ::Gitlab::UrlBuilder.build(design)
+ })
+ end
+end
diff --git a/spec/lib/api/entities/project_repository_storage_move_spec.rb b/spec/lib/api/entities/project_repository_storage_move_spec.rb
new file mode 100644
index 00000000000..1c38c8231d4
--- /dev/null
+++ b/spec/lib/api/entities/project_repository_storage_move_spec.rb
@@ -0,0 +1,21 @@
+# frozen_string_literal: true
+
+require 'spec_helper'
+
+describe API::Entities::ProjectRepositoryStorageMove do
+ describe '#as_json' do
+ subject { entity.as_json }
+
+ let(:storage_move) { build(:project_repository_storage_move, :scheduled, destination_storage_name: 'test_second_storage') }
+ let(:entity) { described_class.new(storage_move) }
+
+ it 'includes basic fields' do
+ is_expected.to include(
+ state: 'scheduled',
+ source_storage_name: 'default',
+ destination_storage_name: 'test_second_storage',
+ project: a_kind_of(Hash)
+ )
+ end
+ end
+end
diff --git a/spec/lib/api/entities/snippet_spec.rb b/spec/lib/api/entities/snippet_spec.rb
new file mode 100644
index 00000000000..dada0942e49
--- /dev/null
+++ b/spec/lib/api/entities/snippet_spec.rb
@@ -0,0 +1,94 @@
+# frozen_string_literal: true
+
+require 'spec_helper'
+
+describe ::API::Entities::Snippet do
+ let_it_be(:user) { create(:user) }
+ let_it_be(:personal_snippet) { create(:personal_snippet, :repository, author: user ) }
+ let_it_be(:project_snippet) { create(:project_snippet, :repository, author: user) }
+
+ let(:entity) { described_class.new(snippet) }
+
+ subject { entity.as_json }
+
+ shared_examples 'common attributes' do
+ it { expect(subject[:id]).to eq snippet.id }
+ it { expect(subject[:title]).to eq snippet.title }
+ it { expect(subject[:description]).to eq snippet.description }
+ it { expect(subject[:updated_at]).to eq snippet.updated_at }
+ it { expect(subject[:created_at]).to eq snippet.created_at }
+ it { expect(subject[:project_id]).to eq snippet.project_id }
+ it { expect(subject[:visibility]).to eq snippet.visibility }
+ it { expect(subject).to include(:author) }
+
+ describe 'file_name' do
+ it 'returns attribute from repository' do
+ expect(subject[:file_name]).to eq snippet.blobs.first.path
+ end
+
+ context 'when repository is empty' do
+ it 'returns attribute from db' do
+ allow(snippet.repository).to receive(:empty?).and_return(true)
+
+ expect(subject[:file_name]).to eq snippet.file_name
+ end
+ end
+ end
+
+ describe 'ssh_url_to_repo' do
+ it 'returns attribute' do
+ expect(subject[:ssh_url_to_repo]).to eq snippet.ssh_url_to_repo
+ end
+
+ context 'when repository does not exist' do
+ it 'does not include attribute' do
+ allow(snippet).to receive(:repository_exists?).and_return(false)
+
+ expect(subject).not_to include(:ssh_url_to_repo)
+ end
+ end
+ end
+
+ describe 'http_url_to_repo' do
+ it 'returns attribute' do
+ expect(subject[:http_url_to_repo]).to eq snippet.http_url_to_repo
+ end
+
+ context 'when repository does not exist' do
+ it 'does not include attribute' do
+ allow(snippet).to receive(:repository_exists?).and_return(false)
+
+ expect(subject).not_to include(:http_url_to_repo)
+ end
+ end
+ end
+ end
+
+ context 'with PersonalSnippet' do
+ let(:snippet) { personal_snippet }
+
+ it_behaves_like 'common attributes'
+
+ it 'returns snippet web_url attribute' do
+ expect(subject[:web_url]).to match("/snippets/#{snippet.id}")
+ end
+
+ it 'returns snippet raw_url attribute' do
+ expect(subject[:raw_url]).to match("/snippets/#{snippet.id}/raw")
+ end
+ end
+
+ context 'with ProjectSnippet' do
+ let(:snippet) { project_snippet }
+
+ it_behaves_like 'common attributes'
+
+ it 'returns snippet web_url attribute' do
+ expect(subject[:web_url]).to match("#{snippet.project.full_path}/snippets/#{snippet.id}")
+ end
+
+ it 'returns snippet raw_url attribute' do
+ expect(subject[:raw_url]).to match("#{snippet.project.full_path}/snippets/#{snippet.id}/raw")
+ end
+ end
+end
diff --git a/spec/lib/api/helpers/pagination_strategies_spec.rb b/spec/lib/api/helpers/pagination_strategies_spec.rb
index a418c09a824..eaa71159714 100644
--- a/spec/lib/api/helpers/pagination_strategies_spec.rb
+++ b/spec/lib/api/helpers/pagination_strategies_spec.rb
@@ -6,7 +6,7 @@ describe API::Helpers::PaginationStrategies do
subject { Class.new.include(described_class).new }
let(:expected_result) { double("result") }
- let(:relation) { double("relation") }
+ let(:relation) { double("relation", klass: "SomeClass") }
let(:params) { {} }
before do
@@ -17,18 +17,18 @@ describe API::Helpers::PaginationStrategies do
let(:paginator) { double("paginator", paginate: expected_result, finalize: nil) }
before do
- allow(subject).to receive(:paginator).with(relation).and_return(paginator)
+ allow(subject).to receive(:paginator).with(relation, nil).and_return(paginator)
end
it 'yields paginated relation' do
- expect { |b| subject.paginate_with_strategies(relation, &b) }.to yield_with_args(expected_result)
+ expect { |b| subject.paginate_with_strategies(relation, nil, &b) }.to yield_with_args(expected_result)
end
it 'calls #finalize with first value returned from block' do
return_value = double
expect(paginator).to receive(:finalize).with(return_value)
- subject.paginate_with_strategies(relation) do |records|
+ subject.paginate_with_strategies(relation, nil) do |records|
some_options = {}
[return_value, some_options]
end
@@ -37,7 +37,7 @@ describe API::Helpers::PaginationStrategies do
it 'returns whatever the block returns' do
return_value = [double, double]
- result = subject.paginate_with_strategies(relation) do |records|
+ result = subject.paginate_with_strategies(relation, nil) do |records|
return_value
end
@@ -47,16 +47,77 @@ describe API::Helpers::PaginationStrategies do
describe '#paginator' do
context 'offset pagination' do
+ let(:plan_limits) { Plan.default.actual_limits }
+ let(:offset_limit) { plan_limits.offset_pagination_limit }
let(:paginator) { double("paginator") }
before do
allow(subject).to receive(:keyset_pagination_enabled?).and_return(false)
end
- it 'delegates to OffsetPagination' do
- expect(Gitlab::Pagination::OffsetPagination).to receive(:new).with(subject).and_return(paginator)
+ context 'when keyset pagination is available for the relation' do
+ before do
+ allow(Gitlab::Pagination::Keyset).to receive(:available_for_type?).and_return(true)
+ end
+
+ context 'when a request scope is given' do
+ let(:params) { { per_page: 100, page: offset_limit / 100 + 1 } }
+ let(:request_scope) { double("scope", actual_limits: plan_limits) }
+
+ context 'when the scope limit is exceeded' do
+ it 'renders a 405 error' do
+ expect(subject).to receive(:error!).with(/maximum allowed offset/, 405)
+
+ subject.paginator(relation, request_scope)
+ end
+ end
+
+ context 'when the scope limit is not exceeded' do
+ let(:params) { { per_page: 100, page: offset_limit / 100 } }
+
+ it 'delegates to OffsetPagination' do
+ expect(Gitlab::Pagination::OffsetPagination).to receive(:new).with(subject).and_return(paginator)
+
+ expect(subject.paginator(relation, request_scope)).to eq(paginator)
+ end
+ end
+ end
+
+ context 'when a request scope is not given' do
+ context 'when the default limits are exceeded' do
+ let(:params) { { per_page: 100, page: offset_limit / 100 + 1 } }
+
+ it 'renders a 405 error' do
+ expect(subject).to receive(:error!).with(/maximum allowed offset/, 405)
+
+ subject.paginator(relation)
+ end
+ end
- expect(subject.paginator(relation)).to eq(paginator)
+ context 'when the default limits are not exceeded' do
+ let(:params) { { per_page: 100, page: offset_limit / 100 } }
+
+ it 'delegates to OffsetPagination' do
+ expect(Gitlab::Pagination::OffsetPagination).to receive(:new).with(subject).and_return(paginator)
+
+ expect(subject.paginator(relation)).to eq(paginator)
+ end
+ end
+ end
+ end
+
+ context 'when keyset pagination is not available for the relation' do
+ let(:params) { { per_page: 100, page: offset_limit / 100 + 1 } }
+
+ before do
+ allow(Gitlab::Pagination::Keyset).to receive(:available_for_type?).and_return(false)
+ end
+
+ it 'delegates to OffsetPagination' do
+ expect(Gitlab::Pagination::OffsetPagination).to receive(:new).with(subject).and_return(paginator)
+
+ expect(subject.paginator(relation)).to eq(paginator)
+ end
end
end
diff --git a/spec/lib/banzai/filter/issue_reference_filter_spec.rb b/spec/lib/banzai/filter/issue_reference_filter_spec.rb
index 4a412da27a7..61c59162a30 100644
--- a/spec/lib/banzai/filter/issue_reference_filter_spec.rb
+++ b/spec/lib/banzai/filter/issue_reference_filter_spec.rb
@@ -4,6 +4,7 @@ require 'spec_helper'
describe Banzai::Filter::IssueReferenceFilter do
include FilterSpecHelper
+ include DesignManagementTestHelpers
def helper
IssuesHelper
@@ -358,6 +359,23 @@ describe Banzai::Filter::IssueReferenceFilter do
end
end
+ context 'when processing a link to the designs tab' do
+ let(:designs_tab_url) { url_for_designs(issue) }
+ let(:input_text) { "See #{designs_tab_url}" }
+
+ subject(:link) { reference_filter(input_text).css('a').first }
+
+ before do
+ enable_design_management
+ end
+
+ it 'includes the word "designs" after the reference in the text content', :aggregate_failures do
+ expect(link.attr('title')).to eq(issue.title)
+ expect(link.attr('href')).to eq(designs_tab_url)
+ expect(link.text).to eq("#{issue.to_reference} (designs)")
+ end
+ end
+
context 'group context' do
let(:group) { create(:group) }
let(:context) { { project: nil, group: group } }
@@ -467,4 +485,41 @@ describe Banzai::Filter::IssueReferenceFilter do
end.not_to yield_control
end
end
+
+ describe '#object_link_text_extras' do
+ before do
+ enable_design_management(enabled)
+ end
+
+ let(:current_user) { project.owner }
+ let(:enabled) { true }
+ let(:matches) { Issue.link_reference_pattern.match(input_text) }
+ let(:extras) { subject.object_link_text_extras(issue, matches) }
+
+ subject { filter_instance }
+
+ context 'the link does not go to the designs tab' do
+ let(:input_text) { Gitlab::Routing.url_helpers.project_issue_url(issue.project, issue) }
+
+ it 'does not include designs' do
+ expect(extras).not_to include('designs')
+ end
+ end
+
+ context 'the link goes to the designs tab' do
+ let(:input_text) { url_for_designs(issue) }
+
+ it 'includes designs' do
+ expect(extras).to include('designs')
+ end
+
+ context 'design management is disabled' do
+ let(:enabled) { false }
+
+ it 'does not include designs in the extras' do
+ expect(extras).not_to include('designs')
+ end
+ end
+ end
+ end
end
diff --git a/spec/lib/banzai/filter/upload_link_filter_spec.rb b/spec/lib/banzai/filter/upload_link_filter_spec.rb
index 3f181dce7bc..c366f774895 100644
--- a/spec/lib/banzai/filter/upload_link_filter_spec.rb
+++ b/spec/lib/banzai/filter/upload_link_filter_spec.rb
@@ -51,6 +51,7 @@ describe Banzai::Filter::UploadLinkFilter do
expect(doc.at_css('a')['href']).to eq(absolute_path)
expect(doc.at_css('a').classes).to include('gfm')
+ expect(doc.at_css('a')['data-link']).to eq('true')
end
end
@@ -59,11 +60,13 @@ describe Banzai::Filter::UploadLinkFilter do
expect(doc.at_css('a')['href']).to eq(relative_path)
expect(doc.at_css('a').classes).to include('gfm')
+ expect(doc.at_css('a')['data-link']).to eq('true')
doc = filter(nested(link(upload_path)))
expect(doc.at_css('a')['href']).to eq(relative_path)
expect(doc.at_css('a').classes).to include('gfm')
+ expect(doc.at_css('a')['data-link']).to eq('true')
end
it 'rebuilds relative URL for an image' do
@@ -71,11 +74,13 @@ describe Banzai::Filter::UploadLinkFilter do
expect(doc.at_css('img')['src']).to eq(relative_path)
expect(doc.at_css('img').classes).to include('gfm')
+ expect(doc.at_css('img')['data-link']).not_to eq('true')
doc = filter(nested(image(upload_path)))
expect(doc.at_css('img')['src']).to eq(relative_path)
expect(doc.at_css('img').classes).to include('gfm')
+ expect(doc.at_css('img')['data-link']).not_to eq('true')
end
it 'does not modify absolute URL' do
@@ -83,6 +88,7 @@ describe Banzai::Filter::UploadLinkFilter do
expect(doc.at_css('a')['href']).to eq 'http://example.com'
expect(doc.at_css('a').classes).not_to include('gfm')
+ expect(doc.at_css('a')['data-link']).not_to eq('true')
end
it 'supports unescaped Unicode filenames' do
@@ -91,6 +97,7 @@ describe Banzai::Filter::UploadLinkFilter do
expect(doc.at_css('a')['href']).to eq("/#{project.full_path}/uploads/%ED%95%9C%EA%B8%80.png")
expect(doc.at_css('a').classes).to include('gfm')
+ expect(doc.at_css('a')['data-link']).to eq('true')
end
it 'supports escaped Unicode filenames' do
@@ -100,6 +107,7 @@ describe Banzai::Filter::UploadLinkFilter do
expect(doc.at_css('img')['src']).to eq("/#{project.full_path}/uploads/%ED%95%9C%EA%B8%80.png")
expect(doc.at_css('img').classes).to include('gfm')
+ expect(doc.at_css('img')['data-link']).not_to eq('true')
end
end
@@ -118,6 +126,7 @@ describe Banzai::Filter::UploadLinkFilter do
expect(doc.at_css('a')['href']).to eq(absolute_path)
expect(doc.at_css('a').classes).to include('gfm')
+ expect(doc.at_css('a')['data-link']).to eq('true')
end
end
@@ -126,6 +135,7 @@ describe Banzai::Filter::UploadLinkFilter do
expect(doc.at_css('a')['href']).to eq(relative_path)
expect(doc.at_css('a').classes).to include('gfm')
+ expect(doc.at_css('a')['data-link']).to eq('true')
end
it 'rewrites the link correctly for subgroup' do
@@ -135,6 +145,7 @@ describe Banzai::Filter::UploadLinkFilter do
expect(doc.at_css('a')['href']).to eq(relative_path)
expect(doc.at_css('a').classes).to include('gfm')
+ expect(doc.at_css('a')['data-link']).to eq('true')
end
it 'does not modify absolute URL' do
@@ -142,6 +153,7 @@ describe Banzai::Filter::UploadLinkFilter do
expect(doc.at_css('a')['href']).to eq 'http://example.com'
expect(doc.at_css('a').classes).not_to include('gfm')
+ expect(doc.at_css('a')['data-link']).not_to eq('true')
end
end
@@ -159,6 +171,7 @@ describe Banzai::Filter::UploadLinkFilter do
expect(doc.at_css('a')['href']).to eq(absolute_path)
expect(doc.at_css('a').classes).to include('gfm')
+ expect(doc.at_css('a')['data-link']).to eq('true')
end
end
@@ -178,6 +191,7 @@ describe Banzai::Filter::UploadLinkFilter do
expect(doc.at_css('a')['href']).to eq(absolute_path)
expect(doc.at_css('a').classes).to include('gfm')
+ expect(doc.at_css('a')['data-link']).to eq('true')
end
end
@@ -186,6 +200,7 @@ describe Banzai::Filter::UploadLinkFilter do
expect(doc.at_css('a')['href']).to eq(gitlab_root + relative_path)
expect(doc.at_css('a').classes).to include('gfm')
+ expect(doc.at_css('a')['data-link']).to eq('true')
end
end
@@ -194,6 +209,7 @@ describe Banzai::Filter::UploadLinkFilter do
expect(doc.at_css('a')['href']).to eq(relative_path)
expect(doc.at_css('a').classes).to include('gfm')
+ expect(doc.at_css('a')['data-link']).to eq('true')
end
it 'does not modify absolute URL' do
@@ -201,6 +217,7 @@ describe Banzai::Filter::UploadLinkFilter do
expect(doc.at_css('a')['href']).to eq 'http://example.com'
expect(doc.at_css('a').classes).not_to include('gfm')
+ expect(doc.at_css('a')['data-link']).not_to eq('true')
end
end
diff --git a/spec/lib/banzai/filter/user_reference_filter_spec.rb b/spec/lib/banzai/filter/user_reference_filter_spec.rb
index a09aeb7d7f6..cd6b68343b5 100644
--- a/spec/lib/banzai/filter/user_reference_filter_spec.rb
+++ b/spec/lib/banzai/filter/user_reference_filter_spec.rb
@@ -123,7 +123,7 @@ describe Banzai::Filter::UserReferenceFilter do
it 'includes default classes' do
doc = reference_filter("Hey #{reference}")
- expect(doc.css('a').first.attr('class')).to eq 'gfm gfm-project_member'
+ expect(doc.css('a').first.attr('class')).to eq 'gfm gfm-project_member js-user-link'
end
context 'when a project is not specified' do
diff --git a/spec/lib/banzai/pipeline/wiki_pipeline_spec.rb b/spec/lib/banzai/pipeline/wiki_pipeline_spec.rb
index 8c009bc409b..4d16c568c13 100644
--- a/spec/lib/banzai/pipeline/wiki_pipeline_spec.rb
+++ b/spec/lib/banzai/pipeline/wiki_pipeline_spec.rb
@@ -57,10 +57,10 @@ describe Banzai::Pipeline::WikiPipeline do
let(:namespace) { create(:namespace, name: "wiki_link_ns") }
let(:project) { create(:project, :public, name: "wiki_link_project", namespace: namespace) }
let(:project_wiki) { ProjectWiki.new(project, double(:user)) }
- let(:page) { build(:wiki_page, wiki: project_wiki, page: OpenStruct.new(url_path: 'nested/twice/start-page')) }
+ let(:page) { build(:wiki_page, wiki: project_wiki, title: 'nested/twice/start-page') }
- { "when GitLab is hosted at a root URL" => '/',
- "when GitLab is hosted at a relative URL" => '/nested/relative/gitlab' }.each do |test_name, relative_url_root|
+ { 'when GitLab is hosted at a root URL' => '',
+ 'when GitLab is hosted at a relative URL' => '/nested/relative/gitlab' }.each do |test_name, relative_url_root|
context test_name do
before do
allow(Gitlab.config.gitlab).to receive(:relative_url_root).and_return(relative_url_root)
@@ -264,7 +264,7 @@ describe Banzai::Pipeline::WikiPipeline do
let_it_be(:namespace) { create(:namespace, name: "wiki_link_ns") }
let_it_be(:project) { create(:project, :public, name: "wiki_link_project", namespace: namespace) }
let_it_be(:project_wiki) { ProjectWiki.new(project, double(:user)) }
- let_it_be(:page) { build(:wiki_page, wiki: project_wiki, page: OpenStruct.new(url_path: 'nested/twice/start-page')) }
+ let_it_be(:page) { build(:wiki_page, wiki: project_wiki, title: 'nested/twice/start-page') }
it 'generates video html structure' do
markdown = "![video_file](video_file_name.mp4)"
diff --git a/spec/lib/banzai/reference_parser/design_parser_spec.rb b/spec/lib/banzai/reference_parser/design_parser_spec.rb
new file mode 100644
index 00000000000..76708acf887
--- /dev/null
+++ b/spec/lib/banzai/reference_parser/design_parser_spec.rb
@@ -0,0 +1,91 @@
+# frozen_string_literal: true
+
+require 'spec_helper'
+
+describe Banzai::ReferenceParser::DesignParser do
+ include ReferenceParserHelpers
+ include DesignManagementTestHelpers
+
+ let_it_be(:issue) { create(:issue) }
+ let_it_be(:design) { create(:design, :with_versions, issue: issue) }
+ let_it_be(:user) { create(:user, developer_projects: [issue.project]) }
+
+ subject(:instance) { described_class.new(Banzai::RenderContext.new(issue.project, user)) }
+
+ let(:link) { design_link(design) }
+
+ before do
+ enable_design_management
+ end
+
+ describe '#nodes_visible_to_user' do
+ it_behaves_like 'referenced feature visibility', 'issues' do
+ let(:project) { issue.project }
+ end
+
+ describe 'specific states' do
+ let_it_be(:public_project) { create(:project, :public) }
+
+ let_it_be(:other_project_link) do
+ design_link(create(:design, :with_versions))
+ end
+ let_it_be(:public_link) do
+ design_link(create(:design, :with_versions, issue: create(:issue, project: public_project)))
+ end
+ let_it_be(:public_but_confidential_link) do
+ design_link(create(:design, :with_versions, issue: create(:issue, :confidential, project: public_project)))
+ end
+
+ subject(:visible_nodes) do
+ nodes = [link,
+ other_project_link,
+ public_link,
+ public_but_confidential_link]
+
+ instance.nodes_visible_to_user(user, nodes)
+ end
+
+ it 'redacts links we should not have access to' do
+ expect(visible_nodes).to contain_exactly(link, public_link)
+ end
+
+ context 'design management is not available' do
+ before do
+ enable_design_management(false)
+ end
+
+ it 'redacts all nodes' do
+ expect(visible_nodes).to be_empty
+ end
+ end
+ end
+ end
+
+ describe '#process' do
+ it 'returns the correct designs' do
+ frag = document([design, create(:design, :with_versions)])
+
+ expect(subject.process([frag])[:visible]).to contain_exactly(design)
+ end
+ end
+
+ def design_link(design)
+ node = empty_html_link
+ node['class'] = 'gfm'
+ node['data-reference-type'] = 'design'
+ node['data-project'] = design.project.id.to_s
+ node['data-issue'] = design.issue.id.to_s
+ node['data-design'] = design.id.to_s
+
+ node
+ end
+
+ def document(designs)
+ frag = Nokogiri::HTML.fragment('')
+ designs.each do |design|
+ frag.add_child(design_link(design))
+ end
+
+ frag
+ end
+end
diff --git a/spec/lib/banzai/renderer_spec.rb b/spec/lib/banzai/renderer_spec.rb
index 0d329b47aa3..b540a76face 100644
--- a/spec/lib/banzai/renderer_spec.rb
+++ b/spec/lib/banzai/renderer_spec.rb
@@ -3,6 +3,8 @@
require 'spec_helper'
describe Banzai::Renderer do
+ let(:renderer) { described_class }
+
def fake_object(fresh:)
object = double('object')
@@ -40,8 +42,6 @@ describe Banzai::Renderer do
end
describe '#render_field' do
- let(:renderer) { described_class }
-
context 'without cache' do
let(:commit) { fake_cacheless_object }
@@ -83,4 +83,57 @@ describe Banzai::Renderer do
end
end
end
+
+ describe '#post_process' do
+ let(:context_options) { {} }
+ let(:html) { 'Consequatur aperiam et nesciunt modi aut assumenda quo id. '}
+ let(:post_processed_html) { double(html_safe: 'safe doc') }
+ let(:doc) { double(to_html: post_processed_html) }
+
+ subject { renderer.post_process(html, context_options) }
+
+ context 'when xhtml' do
+ let(:context_options) { { xhtml: ' ' } }
+
+ context 'without :post_process_pipeline key' do
+ it 'uses PostProcessPipeline' do
+ expect(::Banzai::Pipeline::PostProcessPipeline).to receive(:to_document).and_return(doc)
+
+ subject
+ end
+ end
+
+ context 'with :post_process_pipeline key' do
+ let(:context_options) { { post_process_pipeline: Object, xhtml: ' ' } }
+
+ it 'uses passed post process pipeline' do
+ expect(Object).to receive(:to_document).and_return(doc)
+
+ subject
+ end
+ end
+ end
+
+ context 'when not xhtml' do
+ context 'without :post_process_pipeline key' do
+ it 'uses PostProcessPipeline' do
+ expect(::Banzai::Pipeline::PostProcessPipeline).to receive(:to_html)
+ .with(html, { only_path: true, disable_asset_proxy: true })
+ .and_return(post_processed_html)
+
+ subject
+ end
+ end
+
+ context 'with :post_process_pipeline key' do
+ let(:context_options) { { post_process_pipeline: Object } }
+
+ it 'uses passed post process pipeline' do
+ expect(Object).to receive(:to_html).and_return(post_processed_html)
+
+ subject
+ end
+ end
+ end
+ end
end
diff --git a/spec/lib/bitbucket_server/representation/activity_spec.rb b/spec/lib/bitbucket_server/representation/activity_spec.rb
index b548dedadfb..6988e77ad25 100644
--- a/spec/lib/bitbucket_server/representation/activity_spec.rb
+++ b/spec/lib/bitbucket_server/representation/activity_spec.rb
@@ -3,7 +3,7 @@
require 'spec_helper'
describe BitbucketServer::Representation::Activity do
- let(:activities) { JSON.parse(fixture_file('importers/bitbucket_server/activities.json'))['values'] }
+ let(:activities) { Gitlab::Json.parse(fixture_file('importers/bitbucket_server/activities.json'))['values'] }
let(:inline_comment) { activities.first }
let(:comment) { activities[3] }
let(:merge_event) { activities[4] }
diff --git a/spec/lib/bitbucket_server/representation/comment_spec.rb b/spec/lib/bitbucket_server/representation/comment_spec.rb
index f8c73c3da35..ecaf6a843ae 100644
--- a/spec/lib/bitbucket_server/representation/comment_spec.rb
+++ b/spec/lib/bitbucket_server/representation/comment_spec.rb
@@ -3,7 +3,7 @@
require 'spec_helper'
describe BitbucketServer::Representation::Comment do
- let(:activities) { JSON.parse(fixture_file('importers/bitbucket_server/activities.json'))['values'] }
+ let(:activities) { Gitlab::Json.parse(fixture_file('importers/bitbucket_server/activities.json'))['values'] }
let(:comment) { activities.first }
subject { described_class.new(comment) }
diff --git a/spec/lib/bitbucket_server/representation/pull_request_comment_spec.rb b/spec/lib/bitbucket_server/representation/pull_request_comment_spec.rb
index db43e990812..aa3eddf305a 100644
--- a/spec/lib/bitbucket_server/representation/pull_request_comment_spec.rb
+++ b/spec/lib/bitbucket_server/representation/pull_request_comment_spec.rb
@@ -3,7 +3,7 @@
require 'spec_helper'
describe BitbucketServer::Representation::PullRequestComment do
- let(:activities) { JSON.parse(fixture_file('importers/bitbucket_server/activities.json'))['values'] }
+ let(:activities) { Gitlab::Json.parse(fixture_file('importers/bitbucket_server/activities.json'))['values'] }
let(:comment) { activities.second }
subject { described_class.new(comment) }
diff --git a/spec/lib/bitbucket_server/representation/pull_request_spec.rb b/spec/lib/bitbucket_server/representation/pull_request_spec.rb
index e091890041e..7e72da05cb1 100644
--- a/spec/lib/bitbucket_server/representation/pull_request_spec.rb
+++ b/spec/lib/bitbucket_server/representation/pull_request_spec.rb
@@ -3,7 +3,7 @@
require 'spec_helper'
describe BitbucketServer::Representation::PullRequest do
- let(:sample_data) { JSON.parse(fixture_file('importers/bitbucket_server/pull_request.json')) }
+ let(:sample_data) { Gitlab::Json.parse(fixture_file('importers/bitbucket_server/pull_request.json')) }
subject { described_class.new(sample_data) }
diff --git a/spec/lib/bitbucket_server/representation/repo_spec.rb b/spec/lib/bitbucket_server/representation/repo_spec.rb
index 801de247d73..429b6d36c59 100644
--- a/spec/lib/bitbucket_server/representation/repo_spec.rb
+++ b/spec/lib/bitbucket_server/representation/repo_spec.rb
@@ -50,7 +50,7 @@ describe BitbucketServer::Representation::Repo do
DATA
end
- subject { described_class.new(JSON.parse(sample_data)) }
+ subject { described_class.new(Gitlab::Json.parse(sample_data)) }
describe '#project_key' do
it { expect(subject.project_key).to eq('TEST') }
diff --git a/spec/lib/container_registry/client_spec.rb b/spec/lib/container_registry/client_spec.rb
index 0aad6568793..18bcff65f41 100644
--- a/spec/lib/container_registry/client_spec.rb
+++ b/spec/lib/container_registry/client_spec.rb
@@ -85,7 +85,7 @@ describe ContainerRegistry::Client do
it 'follows 307 redirect for GET /v2/:name/blobs/:digest' do
stub_request(:get, "http://container-registry/v2/group/test/blobs/sha256:0123456789012345")
.with(headers: blob_headers)
- .to_return(status: 307, body: "", headers: { Location: 'http://redirected' })
+ .to_return(status: 307, body: '', headers: { Location: 'http://redirected' })
# We should probably use hash_excluding here, but that requires an update to WebMock:
# https://github.com/bblimke/webmock/blob/master/lib/webmock/matchers/hash_excluding_matcher.rb
stub_request(:get, "http://redirected/")
@@ -238,4 +238,54 @@ describe ContainerRegistry::Client do
it { is_expected.to be_falsey }
end
end
+
+ def stub_registry_info(headers: {}, status: 200)
+ stub_request(:get, 'http://container-registry/v2/')
+ .to_return(status: status, body: "", headers: headers)
+ end
+
+ describe '#registry_info' do
+ subject { client.registry_info }
+
+ context 'when the check is successful' do
+ context 'when using the GitLab container registry' do
+ before do
+ stub_registry_info(headers: {
+ 'GitLab-Container-Registry-Version' => '2.9.1-gitlab',
+ 'GitLab-Container-Registry-Features' => 'a,b,c'
+ })
+ end
+
+ it 'identifies the vendor as "gitlab"' do
+ expect(subject).to include(vendor: 'gitlab')
+ end
+
+ it 'identifies version and features' do
+ expect(subject).to include(version: '2.9.1-gitlab', features: %w[a b c])
+ end
+ end
+
+ context 'when using a third-party container registry' do
+ before do
+ stub_registry_info
+ end
+
+ it 'identifies the vendor as "other"' do
+ expect(subject).to include(vendor: 'other')
+ end
+
+ it 'does not identify version or features' do
+ expect(subject).to include(version: nil, features: [])
+ end
+ end
+ end
+
+ context 'when the check is not successful' do
+ it 'does not identify vendor, version or features' do
+ stub_registry_info(status: 500)
+
+ expect(subject).to eq({})
+ end
+ end
+ end
end
diff --git a/spec/lib/declarative_policy_spec.rb b/spec/lib/declarative_policy_spec.rb
new file mode 100644
index 00000000000..5fdb3c27738
--- /dev/null
+++ b/spec/lib/declarative_policy_spec.rb
@@ -0,0 +1,38 @@
+# frozen_string_literal: true
+
+require 'spec_helper'
+
+describe DeclarativePolicy do
+ describe '.class_for' do
+ it 'uses declarative_policy_class if present' do
+ instance = Gitlab::ErrorTracking::ErrorEvent.new
+
+ expect(described_class.class_for(instance)).to eq(ErrorTracking::BasePolicy)
+ end
+
+ it 'infers policy class from name' do
+ instance = PersonalSnippet.new
+
+ expect(described_class.class_for(instance)).to eq(PersonalSnippetPolicy)
+ end
+
+ it 'raises error if not found' do
+ instance = Object.new
+
+ expect { described_class.class_for(instance) }.to raise_error('no policy for Object')
+ end
+
+ context 'when found policy class does not inherit base' do
+ before do
+ stub_const('Foo', Class.new)
+ stub_const('FooPolicy', Class.new)
+ end
+
+ it 'raises error if inferred class does not inherit Base' do
+ instance = Foo.new
+
+ expect { described_class.class_for(instance) }.to raise_error('no policy for Foo')
+ end
+ end
+ end
+end
diff --git a/spec/lib/feature_spec.rb b/spec/lib/feature_spec.rb
index 2890b8d4f3b..81fa2dc5cad 100644
--- a/spec/lib/feature_spec.rb
+++ b/spec/lib/feature_spec.rb
@@ -146,14 +146,6 @@ describe Feature do
expect(described_class.enabled?(:enabled_feature_flag)).to be_truthy
end
- context 'with USE_THREAD_MEMORY_CACHE defined' do
- before do
- stub_env('USE_THREAD_MEMORY_CACHE', '1')
- end
-
- it { expect(described_class.l1_cache_backend).to eq(Gitlab::ThreadMemoryCache.cache_backend) }
- end
-
it { expect(described_class.l1_cache_backend).to eq(Gitlab::ProcessMemoryCache.cache_backend) }
it { expect(described_class.l2_cache_backend).to eq(Rails.cache) }
diff --git a/spec/lib/gitlab/alert_management/alert_params_spec.rb b/spec/lib/gitlab/alert_management/alert_params_spec.rb
new file mode 100644
index 00000000000..5cf34038f68
--- /dev/null
+++ b/spec/lib/gitlab/alert_management/alert_params_spec.rb
@@ -0,0 +1,94 @@
+# frozen_string_literal: true
+
+require 'spec_helper'
+
+describe Gitlab::AlertManagement::AlertParams do
+ let_it_be(:project) { create(:project, :repository, :private) }
+
+ describe '.from_generic_alert' do
+ let(:started_at) { Time.current.change(usec: 0).rfc3339 }
+ let(:default_payload) do
+ {
+ 'title' => 'Alert title',
+ 'description' => 'Description',
+ 'monitoring_tool' => 'Monitoring tool name',
+ 'service' => 'Service',
+ 'hosts' => ['gitlab.com'],
+ 'start_time' => started_at,
+ 'some' => { 'extra' => { 'payload' => 'here' } }
+ }
+ end
+ let(:payload) { default_payload }
+
+ subject { described_class.from_generic_alert(project: project, payload: payload) }
+
+ it 'returns Alert compatible parameters' do
+ is_expected.to eq(
+ project_id: project.id,
+ title: 'Alert title',
+ description: 'Description',
+ monitoring_tool: 'Monitoring tool name',
+ service: 'Service',
+ severity: 'critical',
+ hosts: ['gitlab.com'],
+ payload: payload,
+ started_at: started_at
+ )
+ end
+
+ context 'when severity given' do
+ let(:payload) { default_payload.merge(severity: 'low') }
+
+ it 'returns Alert compatible parameters' do
+ expect(subject[:severity]).to eq('low')
+ end
+ end
+
+ context 'when there are no hosts in the payload' do
+ let(:payload) { {} }
+
+ it 'hosts param is an empty array' do
+ expect(subject[:hosts]).to be_empty
+ end
+ end
+ end
+
+ describe '.from_prometheus_alert' do
+ let(:payload) do
+ {
+ 'status' => 'firing',
+ 'labels' => {
+ 'alertname' => 'GitalyFileServerDown',
+ 'channel' => 'gitaly',
+ 'pager' => 'pagerduty',
+ 'severity' => 's1'
+ },
+ 'annotations' => {
+ 'description' => 'Alert description',
+ 'runbook' => 'troubleshooting/gitaly-down.md',
+ 'title' => 'Alert title'
+ },
+ 'startsAt' => '2020-04-27T10:10:22.265949279Z',
+ 'endsAt' => '0001-01-01T00:00:00Z',
+ 'generatorURL' => 'http://8d467bd4607a:9090/graph?g0.expr=vector%281%29&g0.tab=1',
+ 'fingerprint' => 'b6ac4d42057c43c1'
+ }
+ end
+ let(:parsed_alert) { Gitlab::Alerting::Alert.new(project: project, payload: payload) }
+
+ subject { described_class.from_prometheus_alert(project: project, parsed_alert: parsed_alert) }
+
+ it 'returns Alert-compatible params' do
+ is_expected.to eq(
+ project_id: project.id,
+ title: 'Alert title',
+ description: 'Alert description',
+ monitoring_tool: 'Prometheus',
+ payload: payload,
+ started_at: parsed_alert.starts_at,
+ ended_at: parsed_alert.ends_at,
+ fingerprint: parsed_alert.gitlab_fingerprint
+ )
+ end
+ end
+end
diff --git a/spec/lib/gitlab/alert_management/alert_status_counts_spec.rb b/spec/lib/gitlab/alert_management/alert_status_counts_spec.rb
new file mode 100644
index 00000000000..816ed918fe8
--- /dev/null
+++ b/spec/lib/gitlab/alert_management/alert_status_counts_spec.rb
@@ -0,0 +1,55 @@
+# frozen_string_literal: true
+
+require 'spec_helper'
+
+describe Gitlab::AlertManagement::AlertStatusCounts do
+ let_it_be(:current_user) { create(:user) }
+ let_it_be(:project) { create(:project) }
+ let_it_be(:alert_1) { create(:alert_management_alert, :resolved, project: project) }
+ let_it_be(:alert_2) { create(:alert_management_alert, :ignored, project: project) }
+ let_it_be(:alert_3) { create(:alert_management_alert) }
+ let(:params) { {} }
+
+ describe '#execute' do
+ subject(:counts) { described_class.new(current_user, project, params) }
+
+ context 'for an unauthorized user' do
+ it 'returns zero for all statuses' do
+ expect(counts.open).to eq(0)
+ expect(counts.all).to eq(0)
+
+ AlertManagement::Alert::STATUSES.each_key do |status|
+ expect(counts.send(status)).to eq(0)
+ end
+ end
+ end
+
+ context 'for an authorized user' do
+ before do
+ project.add_developer(current_user)
+ end
+
+ it 'returns the correct counts for each status' do
+ expect(counts.open).to eq(0)
+ expect(counts.all).to eq(2)
+ expect(counts.resolved).to eq(1)
+ expect(counts.ignored).to eq(1)
+ expect(counts.triggered).to eq(0)
+ expect(counts.acknowledged).to eq(0)
+ end
+
+ context 'when filtering params are included' do
+ let(:params) { { status: AlertManagement::Alert::STATUSES[:resolved] } }
+
+ it 'returns the correct counts for each status' do
+ expect(counts.open).to eq(0)
+ expect(counts.all).to eq(1)
+ expect(counts.resolved).to eq(1)
+ expect(counts.ignored).to eq(0)
+ expect(counts.triggered).to eq(0)
+ expect(counts.acknowledged).to eq(0)
+ end
+ end
+ end
+ end
+end
diff --git a/spec/lib/gitlab/alerting/alert_spec.rb b/spec/lib/gitlab/alerting/alert_spec.rb
index 6d97f08af91..a0582515f3d 100644
--- a/spec/lib/gitlab/alerting/alert_spec.rb
+++ b/spec/lib/gitlab/alerting/alert_spec.rb
@@ -246,6 +246,30 @@ describe Gitlab::Alerting::Alert do
it_behaves_like 'parse payload', 'annotations/gitlab_incident_markdown'
end
+ describe '#gitlab_fingerprint' do
+ subject { alert.gitlab_fingerprint }
+
+ context 'when the alert is a GitLab managed alert' do
+ include_context 'gitlab alert'
+
+ it 'returns a fingerprint' do
+ plain_fingerprint = [alert.metric_id, alert.starts_at].join('/')
+
+ is_expected.to eq(Digest::SHA1.hexdigest(plain_fingerprint))
+ end
+ end
+
+ context 'when the alert is from self managed Prometheus' do
+ include_context 'full query'
+
+ it 'returns a fingerprint' do
+ plain_fingerprint = [alert.starts_at, alert.title, alert.full_query].join('/')
+
+ is_expected.to eq(Digest::SHA1.hexdigest(plain_fingerprint))
+ end
+ end
+ end
+
describe '#valid?' do
before do
payload.update(
diff --git a/spec/lib/gitlab/alerting/notification_payload_parser_spec.rb b/spec/lib/gitlab/alerting/notification_payload_parser_spec.rb
index a38aea7b972..f32095b3c86 100644
--- a/spec/lib/gitlab/alerting/notification_payload_parser_spec.rb
+++ b/spec/lib/gitlab/alerting/notification_payload_parser_spec.rb
@@ -12,7 +12,8 @@ describe Gitlab::Alerting::NotificationPayloadParser do
'description' => 'Description',
'monitoring_tool' => 'Monitoring tool name',
'service' => 'Service',
- 'hosts' => ['gitlab.com']
+ 'hosts' => ['gitlab.com'],
+ 'severity' => 'low'
}
end
@@ -26,7 +27,8 @@ describe Gitlab::Alerting::NotificationPayloadParser do
'description' => 'Description',
'monitoring_tool' => 'Monitoring tool name',
'service' => 'Service',
- 'hosts' => ['gitlab.com']
+ 'hosts' => ['gitlab.com'],
+ 'severity' => 'low'
},
'startsAt' => starts_at.rfc3339
}
@@ -67,11 +69,24 @@ describe Gitlab::Alerting::NotificationPayloadParser do
let(:payload) { {} }
it 'returns default parameters' do
- is_expected.to eq(
- 'annotations' => { 'title' => 'New: Incident' },
+ is_expected.to match(
+ 'annotations' => {
+ 'title' => described_class::DEFAULT_TITLE,
+ 'severity' => described_class::DEFAULT_SEVERITY
+ },
'startsAt' => starts_at.rfc3339
)
end
+
+ context 'when severity is blank' do
+ before do
+ payload[:severity] = ''
+ end
+
+ it 'sets severity to the default ' do
+ expect(subject.dig('annotations', 'severity')).to eq(described_class::DEFAULT_SEVERITY)
+ end
+ end
end
context 'when payload attributes have blank lines' do
@@ -88,7 +103,10 @@ describe Gitlab::Alerting::NotificationPayloadParser do
it 'returns default parameters' do
is_expected.to eq(
- 'annotations' => { 'title' => 'New: Incident' },
+ 'annotations' => {
+ 'title' => 'New: Incident',
+ 'severity' => described_class::DEFAULT_SEVERITY
+ },
'startsAt' => starts_at.rfc3339
)
end
@@ -112,6 +130,7 @@ describe Gitlab::Alerting::NotificationPayloadParser do
is_expected.to eq(
'annotations' => {
'title' => 'New: Incident',
+ 'severity' => described_class::DEFAULT_SEVERITY,
'description' => 'Description',
'additional.params.1' => 'Some value 1',
'additional.params.2' => 'Some value 2'
diff --git a/spec/lib/gitlab/analytics/cycle_analytics/median_spec.rb b/spec/lib/gitlab/analytics/cycle_analytics/median_spec.rb
new file mode 100644
index 00000000000..92ecec350ae
--- /dev/null
+++ b/spec/lib/gitlab/analytics/cycle_analytics/median_spec.rb
@@ -0,0 +1,42 @@
+# frozen_string_literal: true
+
+require 'spec_helper'
+
+describe Gitlab::Analytics::CycleAnalytics::Median do
+ let_it_be(:project) { create(:project, :repository) }
+ let(:query) { Project.joins(merge_requests: :metrics) }
+
+ let(:stage) do
+ build(
+ :cycle_analytics_project_stage,
+ start_event_identifier: Gitlab::Analytics::CycleAnalytics::StageEvents::MergeRequestCreated.identifier,
+ end_event_identifier: Gitlab::Analytics::CycleAnalytics::StageEvents::MergeRequestMerged.identifier,
+ project: project
+ )
+ end
+
+ subject { described_class.new(stage: stage, query: query).seconds }
+
+ around do |example|
+ Timecop.freeze { example.run }
+ end
+
+ it 'retruns nil when no results' do
+ expect(subject).to eq(nil)
+ end
+
+ it 'returns median duration seconds as float' do
+ merge_request1 = create(:merge_request, source_branch: '1', target_project: project, source_project: project)
+ merge_request2 = create(:merge_request, source_branch: '2', target_project: project, source_project: project)
+
+ Timecop.travel(5.minutes.from_now) do
+ merge_request1.metrics.update!(merged_at: Time.zone.now)
+ end
+
+ Timecop.travel(10.minutes.from_now) do
+ merge_request2.metrics.update!(merged_at: Time.zone.now)
+ end
+
+ expect(subject).to be_within(0.5).of(7.5.minutes.seconds)
+ end
+end
diff --git a/spec/lib/gitlab/app_json_logger_spec.rb b/spec/lib/gitlab/app_json_logger_spec.rb
index 22a398f8bca..d11456236cc 100644
--- a/spec/lib/gitlab/app_json_logger_spec.rb
+++ b/spec/lib/gitlab/app_json_logger_spec.rb
@@ -9,10 +9,10 @@ describe Gitlab::AppJsonLogger do
let(:string_message) { 'Information' }
it 'logs a hash as a JSON' do
- expect(JSON.parse(subject.format_message('INFO', Time.now, nil, hash_message))).to include(hash_message)
+ expect(Gitlab::Json.parse(subject.format_message('INFO', Time.now, nil, hash_message))).to include(hash_message)
end
it 'logs a string as a JSON' do
- expect(JSON.parse(subject.format_message('INFO', Time.now, nil, string_message))).to include('message' => string_message)
+ expect(Gitlab::Json.parse(subject.format_message('INFO', Time.now, nil, string_message))).to include('message' => string_message)
end
end
diff --git a/spec/lib/gitlab/application_context_spec.rb b/spec/lib/gitlab/application_context_spec.rb
index 6674ea059a0..3be967ac8a4 100644
--- a/spec/lib/gitlab/application_context_spec.rb
+++ b/spec/lib/gitlab/application_context_spec.rb
@@ -55,10 +55,10 @@ describe Gitlab::ApplicationContext do
end
describe '#to_lazy_hash' do
- let(:user) { build(:user) }
- let(:project) { build(:project) }
- let(:namespace) { create(:group) }
- let(:subgroup) { create(:group, parent: namespace) }
+ let_it_be(:user) { create(:user) }
+ let_it_be(:project) { create(:project) }
+ let_it_be(:namespace) { create(:group) }
+ let_it_be(:subgroup) { create(:group, parent: namespace) }
def result(context)
context.to_lazy_hash.transform_values { |v| v.respond_to?(:call) ? v.call : v }
@@ -106,5 +106,11 @@ describe Gitlab::ApplicationContext do
context.use {}
end
+
+ it 'does not cause queries' do
+ context = described_class.new(project: create(:project), namespace: create(:group, :nested), user: create(:user))
+
+ expect { context.use { Labkit::Context.current.to_h } }.not_to exceed_query_limit(0)
+ end
end
end
diff --git a/spec/lib/gitlab/auth/auth_finders_spec.rb b/spec/lib/gitlab/auth/auth_finders_spec.rb
index 0b6fda31d7b..774a87752b9 100644
--- a/spec/lib/gitlab/auth/auth_finders_spec.rb
+++ b/spec/lib/gitlab/auth/auth_finders_spec.rb
@@ -17,6 +17,17 @@ describe Gitlab::Auth::AuthFinders do
request.update_param(key, value)
end
+ def set_header(key, value)
+ env[key] = value
+ end
+
+ def set_basic_auth_header(username, password)
+ set_header(
+ 'HTTP_AUTHORIZATION',
+ ActionController::HttpAuthentication::Basic.encode_credentials(username, password)
+ )
+ end
+
describe '#find_user_from_warden' do
context 'with CSRF token' do
before do
@@ -31,7 +42,7 @@ describe Gitlab::Auth::AuthFinders do
context 'with valid credentials' do
it 'returns the user' do
- env['warden'] = double("warden", authenticate: user)
+ set_header('warden', double("warden", authenticate: user))
expect(find_user_from_warden).to eq user
end
@@ -41,7 +52,7 @@ describe Gitlab::Auth::AuthFinders do
context 'without CSRF token' do
it 'returns nil' do
allow(Gitlab::RequestForgeryProtection).to receive(:verified?).and_return(false)
- env['warden'] = double("warden", authenticate: user)
+ set_header('warden', double("warden", authenticate: user))
expect(find_user_from_warden).to be_nil
end
@@ -51,8 +62,8 @@ describe Gitlab::Auth::AuthFinders do
describe '#find_user_from_feed_token' do
context 'when the request format is atom' do
before do
- env['SCRIPT_NAME'] = 'url.atom'
- env['HTTP_ACCEPT'] = 'application/atom+xml'
+ set_header('SCRIPT_NAME', 'url.atom')
+ set_header('HTTP_ACCEPT', 'application/atom+xml')
end
context 'when feed_token param is provided' do
@@ -94,7 +105,7 @@ describe Gitlab::Auth::AuthFinders do
context 'when the request format is not atom' do
it 'returns nil' do
- env['SCRIPT_NAME'] = 'json'
+ set_header('SCRIPT_NAME', 'json')
set_param(:feed_token, user.feed_token)
@@ -104,7 +115,7 @@ describe Gitlab::Auth::AuthFinders do
context 'when the request format is empty' do
it 'the method call does not modify the original value' do
- env['SCRIPT_NAME'] = 'url.atom'
+ set_header('SCRIPT_NAME', 'url.atom')
env.delete('action_dispatch.request.formats')
@@ -118,7 +129,7 @@ describe Gitlab::Auth::AuthFinders do
describe '#find_user_from_static_object_token' do
shared_examples 'static object request' do
before do
- env['SCRIPT_NAME'] = path
+ set_header('SCRIPT_NAME', path)
end
context 'when token header param is present' do
@@ -174,7 +185,7 @@ describe Gitlab::Auth::AuthFinders do
context 'when request format is not archive nor blob' do
before do
- env['script_name'] = 'url'
+ set_header('script_name', 'url')
end
it 'returns nil' do
@@ -183,11 +194,82 @@ describe Gitlab::Auth::AuthFinders do
end
end
+ describe '#deploy_token_from_request' do
+ let_it_be(:deploy_token) { create(:deploy_token) }
+ let_it_be(:route_authentication_setting) { { deploy_token_allowed: true } }
+
+ subject { deploy_token_from_request }
+
+ it { is_expected.to be_nil }
+
+ shared_examples 'an unauthenticated route' do
+ context 'when route is not allowed to use deploy_tokens' do
+ let(:route_authentication_setting) { { deploy_token_allowed: false } }
+
+ it { is_expected.to be_nil }
+ end
+ end
+
+ context 'with deploy token headers' do
+ before do
+ set_header(described_class::DEPLOY_TOKEN_HEADER, deploy_token.token)
+ end
+
+ it { is_expected.to eq deploy_token }
+
+ it_behaves_like 'an unauthenticated route'
+
+ context 'with incorrect token' do
+ before do
+ set_header(described_class::DEPLOY_TOKEN_HEADER, 'invalid_token')
+ end
+
+ it { is_expected.to be_nil }
+ end
+ end
+
+ context 'with oauth headers' do
+ before do
+ set_header('HTTP_AUTHORIZATION', "Bearer #{deploy_token.token}")
+ end
+
+ it { is_expected.to eq deploy_token }
+
+ it_behaves_like 'an unauthenticated route'
+
+ context 'with invalid token' do
+ before do
+ set_header('HTTP_AUTHORIZATION', "Bearer invalid_token")
+ end
+
+ it { is_expected.to be_nil }
+ end
+ end
+
+ context 'with basic auth headers' do
+ before do
+ set_basic_auth_header(deploy_token.username, deploy_token.token)
+ end
+
+ it { is_expected.to eq deploy_token }
+
+ it_behaves_like 'an unauthenticated route'
+
+ context 'with incorrect token' do
+ before do
+ set_basic_auth_header(deploy_token.username, 'invalid')
+ end
+
+ it { is_expected.to be_nil }
+ end
+ end
+ end
+
describe '#find_user_from_access_token' do
let(:personal_access_token) { create(:personal_access_token, user: user) }
before do
- env['SCRIPT_NAME'] = 'url.atom'
+ set_header('SCRIPT_NAME', 'url.atom')
end
it 'returns nil if no access_token present' do
@@ -196,13 +278,13 @@ describe Gitlab::Auth::AuthFinders do
context 'when validate_access_token! returns valid' do
it 'returns user' do
- env[described_class::PRIVATE_TOKEN_HEADER] = personal_access_token.token
+ set_header(described_class::PRIVATE_TOKEN_HEADER, personal_access_token.token)
expect(find_user_from_access_token).to eq user
end
it 'returns exception if token has no user' do
- env[described_class::PRIVATE_TOKEN_HEADER] = personal_access_token.token
+ set_header(described_class::PRIVATE_TOKEN_HEADER, personal_access_token.token)
allow_any_instance_of(PersonalAccessToken).to receive(:user).and_return(nil)
expect { find_user_from_access_token }.to raise_error(Gitlab::Auth::UnauthorizedError)
@@ -211,7 +293,7 @@ describe Gitlab::Auth::AuthFinders do
context 'with OAuth headers' do
it 'returns user' do
- env['HTTP_AUTHORIZATION'] = "Bearer #{personal_access_token.token}"
+ set_header('HTTP_AUTHORIZATION', "Bearer #{personal_access_token.token}")
expect(find_user_from_access_token).to eq user
end
@@ -228,7 +310,7 @@ describe Gitlab::Auth::AuthFinders do
let(:personal_access_token) { create(:personal_access_token, user: user) }
before do
- env[described_class::PRIVATE_TOKEN_HEADER] = personal_access_token.token
+ set_header(described_class::PRIVATE_TOKEN_HEADER, personal_access_token.token)
end
it 'returns exception if token has no user' do
@@ -252,19 +334,19 @@ describe Gitlab::Auth::AuthFinders do
end
it 'returns the user for RSS requests' do
- env['SCRIPT_NAME'] = 'url.atom'
+ set_header('SCRIPT_NAME', 'url.atom')
expect(find_user_from_web_access_token(:rss)).to eq(user)
end
it 'returns the user for ICS requests' do
- env['SCRIPT_NAME'] = 'url.ics'
+ set_header('SCRIPT_NAME', 'url.ics')
expect(find_user_from_web_access_token(:ics)).to eq(user)
end
it 'returns the user for API requests' do
- env['SCRIPT_NAME'] = '/api/endpoint'
+ set_header('SCRIPT_NAME', '/api/endpoint')
expect(find_user_from_web_access_token(:api)).to eq(user)
end
@@ -274,12 +356,12 @@ describe Gitlab::Auth::AuthFinders do
let(:personal_access_token) { create(:personal_access_token, user: user) }
before do
- env['SCRIPT_NAME'] = 'url.atom'
+ set_header('SCRIPT_NAME', 'url.atom')
end
context 'passed as header' do
it 'returns token if valid personal_access_token' do
- env[described_class::PRIVATE_TOKEN_HEADER] = personal_access_token.token
+ set_header(described_class::PRIVATE_TOKEN_HEADER, personal_access_token.token)
expect(find_personal_access_token).to eq personal_access_token
end
@@ -298,7 +380,7 @@ describe Gitlab::Auth::AuthFinders do
end
it 'returns exception if invalid personal_access_token' do
- env[described_class::PRIVATE_TOKEN_HEADER] = 'invalid_token'
+ set_header(described_class::PRIVATE_TOKEN_HEADER, 'invalid_token')
expect { find_personal_access_token }.to raise_error(Gitlab::Auth::UnauthorizedError)
end
@@ -310,7 +392,7 @@ describe Gitlab::Auth::AuthFinders do
context 'passed as header' do
it 'returns token if valid oauth_access_token' do
- env['HTTP_AUTHORIZATION'] = "Bearer #{token.token}"
+ set_header('HTTP_AUTHORIZATION', "Bearer #{token.token}")
expect(find_oauth_access_token.token).to eq token.token
end
@@ -329,7 +411,7 @@ describe Gitlab::Auth::AuthFinders do
end
it 'returns exception if invalid oauth_access_token' do
- env['HTTP_AUTHORIZATION'] = "Bearer invalid_token"
+ set_header('HTTP_AUTHORIZATION', "Bearer invalid_token")
expect { find_oauth_access_token }.to raise_error(Gitlab::Auth::UnauthorizedError)
end
@@ -337,7 +419,7 @@ describe Gitlab::Auth::AuthFinders do
describe '#find_personal_access_token_from_http_basic_auth' do
def auth_header_with(token)
- env['HTTP_AUTHORIZATION'] = ActionController::HttpAuthentication::Basic.encode_credentials('username', token)
+ set_basic_auth_header('username', token)
end
context 'access token is valid' do
@@ -384,14 +466,6 @@ describe Gitlab::Auth::AuthFinders do
end
describe '#find_user_from_basic_auth_job' do
- def basic_http_auth(username, password)
- ActionController::HttpAuthentication::Basic.encode_credentials(username, password)
- end
-
- def set_auth(username, password)
- env['HTTP_AUTHORIZATION'] = basic_http_auth(username, password)
- end
-
subject { find_user_from_basic_auth_job }
context 'when the request does not have AUTHORIZATION header' do
@@ -400,25 +474,25 @@ describe Gitlab::Auth::AuthFinders do
context 'with wrong credentials' do
it 'returns nil without user and password' do
- set_auth(nil, nil)
+ set_basic_auth_header(nil, nil)
is_expected.to be_nil
end
it 'returns nil without password' do
- set_auth('some-user', nil)
+ set_basic_auth_header('some-user', nil)
is_expected.to be_nil
end
it 'returns nil without user' do
- set_auth(nil, 'password')
+ set_basic_auth_header(nil, 'password')
is_expected.to be_nil
end
it 'returns nil without CI username' do
- set_auth('user', 'password')
+ set_basic_auth_header('user', 'password')
is_expected.to be_nil
end
@@ -430,19 +504,19 @@ describe Gitlab::Auth::AuthFinders do
let(:build) { create(:ci_build, user: user) }
it 'returns nil without password' do
- set_auth(username, nil)
+ set_basic_auth_header(username, nil)
is_expected.to be_nil
end
it 'returns user with valid token' do
- set_auth(username, build.token)
+ set_basic_auth_header(username, build.token)
is_expected.to eq user
end
it 'raises error with invalid token' do
- set_auth(username, 'token')
+ set_basic_auth_header(username, 'token')
expect { subject }.to raise_error(Gitlab::Auth::UnauthorizedError)
end
@@ -502,20 +576,20 @@ describe Gitlab::Auth::AuthFinders do
context 'when the job token is in the headers' do
it 'returns the user if valid job token' do
- env[described_class::JOB_TOKEN_HEADER] = job.token
+ set_header(described_class::JOB_TOKEN_HEADER, job.token)
is_expected.to eq(user)
expect(@current_authenticated_job).to eq(job)
end
it 'returns nil without job token' do
- env[described_class::JOB_TOKEN_HEADER] = ''
+ set_header(described_class::JOB_TOKEN_HEADER, '')
is_expected.to be_nil
end
it 'returns exception if invalid job token' do
- env[described_class::JOB_TOKEN_HEADER] = 'invalid token'
+ set_header(described_class::JOB_TOKEN_HEADER, 'invalid token')
expect { subject }.to raise_error(Gitlab::Auth::UnauthorizedError)
end
@@ -524,7 +598,7 @@ describe Gitlab::Auth::AuthFinders do
let(:route_authentication_setting) { { job_token_allowed: false } }
it 'sets current_user to nil' do
- env[described_class::JOB_TOKEN_HEADER] = job.token
+ set_header(described_class::JOB_TOKEN_HEADER, job.token)
allow_any_instance_of(Gitlab::UserAccess).to receive(:allowed?).and_return(true)
@@ -586,7 +660,7 @@ describe Gitlab::Auth::AuthFinders do
context 'with API requests' do
before do
- env['SCRIPT_NAME'] = '/api/endpoint'
+ set_header('SCRIPT_NAME', '/api/endpoint')
end
it 'returns the runner if token is valid' do
@@ -614,7 +688,7 @@ describe Gitlab::Auth::AuthFinders do
context 'without API requests' do
before do
- env['SCRIPT_NAME'] = 'url.ics'
+ set_header('SCRIPT_NAME', 'url.ics')
end
it 'returns nil if token is valid' do
diff --git a/spec/lib/gitlab/auth/o_auth/provider_spec.rb b/spec/lib/gitlab/auth/o_auth/provider_spec.rb
index f46f9d76a1e..8b0d4d786cd 100644
--- a/spec/lib/gitlab/auth/o_auth/provider_spec.rb
+++ b/spec/lib/gitlab/auth/o_auth/provider_spec.rb
@@ -63,7 +63,7 @@ describe Gitlab::Auth::OAuth::Provider do
context 'for an OmniAuth provider' do
before do
provider = OpenStruct.new(
- name: 'google',
+ name: 'google_oauth2',
app_id: 'asd123',
app_secret: 'asd123'
)
@@ -71,8 +71,16 @@ describe Gitlab::Auth::OAuth::Provider do
end
context 'when the provider exists' do
+ subject { described_class.config_for('google_oauth2') }
+
it 'returns the config' do
- expect(described_class.config_for('google')).to be_a(OpenStruct)
+ expect(subject).to be_a(OpenStruct)
+ end
+
+ it 'merges defaults with the given configuration' do
+ defaults = Gitlab::OmniauthInitializer.default_arguments_for('google_oauth2').deep_stringify_keys
+
+ expect(subject['args']).to include(defaults)
end
end
diff --git a/spec/lib/gitlab/auth_spec.rb b/spec/lib/gitlab/auth_spec.rb
index a0a8767637e..870f02b6933 100644
--- a/spec/lib/gitlab/auth_spec.rb
+++ b/spec/lib/gitlab/auth_spec.rb
@@ -715,6 +715,14 @@ describe Gitlab::Auth, :use_clean_rails_memory_store_caching do
end
end
+ describe ".resource_bot_scopes" do
+ subject { described_class.resource_bot_scopes }
+
+ it { is_expected.to include(*described_class::API_SCOPES - [:read_user]) }
+ it { is_expected.to include(*described_class::REPOSITORY_SCOPES) }
+ it { is_expected.to include(*described_class.registry_scopes) }
+ end
+
private
def expect_results_with_abilities(personal_access_token, abilities, success = true)
diff --git a/spec/lib/gitlab/background_migration/backfill_environment_id_deployment_merge_requests_spec.rb b/spec/lib/gitlab/background_migration/backfill_environment_id_deployment_merge_requests_spec.rb
new file mode 100644
index 00000000000..34ac70071bb
--- /dev/null
+++ b/spec/lib/gitlab/background_migration/backfill_environment_id_deployment_merge_requests_spec.rb
@@ -0,0 +1,46 @@
+# frozen_string_literal: true
+
+require 'spec_helper'
+
+describe Gitlab::BackgroundMigration::BackfillEnvironmentIdDeploymentMergeRequests, schema: 20200312134637 do
+ let(:environments) { table(:environments) }
+ let(:merge_requests) { table(:merge_requests) }
+ let(:deployments) { table(:deployments) }
+ let(:deployment_merge_requests) { table(:deployment_merge_requests) }
+ let(:namespaces) { table(:namespaces) }
+ let(:projects) { table(:projects) }
+
+ subject(:migration) { described_class.new }
+
+ it 'correctly backfills environment_id column' do
+ namespace = namespaces.create!(name: 'foo', path: 'foo')
+ project = projects.create!(namespace_id: namespace.id)
+
+ production = environments.create!(project_id: project.id, name: 'production', slug: 'production')
+ staging = environments.create!(project_id: project.id, name: 'staging', slug: 'staging')
+
+ mr = merge_requests.create!(source_branch: 'x', target_branch: 'master', target_project_id: project.id)
+
+ deployment1 = deployments.create!(environment_id: staging.id, iid: 1, project_id: project.id, ref: 'master', tag: false, sha: '123abcdef', status: 1)
+ deployment2 = deployments.create!(environment_id: production.id, iid: 2, project_id: project.id, ref: 'master', tag: false, sha: '123abcdef', status: 1)
+ deployment3 = deployments.create!(environment_id: production.id, iid: 3, project_id: project.id, ref: 'master', tag: false, sha: '123abcdef', status: 1)
+
+ # mr is tracked twice in production through deployment2 and deployment3
+ deployment_merge_requests.create!(deployment_id: deployment1.id, merge_request_id: mr.id)
+ deployment_merge_requests.create!(deployment_id: deployment2.id, merge_request_id: mr.id)
+ deployment_merge_requests.create!(deployment_id: deployment3.id, merge_request_id: mr.id)
+
+ expect(deployment_merge_requests.where(environment_id: nil).count).to eq(3)
+
+ migration.backfill_range(1, mr.id)
+
+ expect(deployment_merge_requests.where(environment_id: nil).count).to be_zero
+ expect(deployment_merge_requests.count).to eq(2)
+
+ production_deployments = deployment_merge_requests.where(environment_id: production.id)
+ expect(production_deployments.count).to eq(1)
+ expect(production_deployments.first.deployment_id).to eq(deployment2.id)
+
+ expect(deployment_merge_requests.where(environment_id: staging.id).count).to eq(1)
+ end
+end
diff --git a/spec/lib/gitlab/background_migration/backfill_snippet_repositories_spec.rb b/spec/lib/gitlab/background_migration/backfill_snippet_repositories_spec.rb
index 08d3b7bec6a..27ae60eb278 100644
--- a/spec/lib/gitlab/background_migration/backfill_snippet_repositories_spec.rb
+++ b/spec/lib/gitlab/background_migration/backfill_snippet_repositories_spec.rb
@@ -2,13 +2,31 @@
require 'spec_helper'
-describe Gitlab::BackgroundMigration::BackfillSnippetRepositories, :migration, schema: 2020_02_26_162723 do
+describe Gitlab::BackgroundMigration::BackfillSnippetRepositories, :migration, schema: 2020_04_20_094444 do
let(:gitlab_shell) { Gitlab::Shell.new }
let(:users) { table(:users) }
let(:snippets) { table(:snippets) }
let(:snippet_repositories) { table(:snippet_repositories) }
- let(:user) { users.create(id: 1, email: 'user@example.com', projects_limit: 10, username: 'test', name: 'Test') }
+ let(:user_state) { 'active' }
+ let(:ghost) { false }
+ let(:user_type) { nil }
+ let(:user_name) { 'Test' }
+
+ let!(:user) do
+ users.create(id: 1,
+ email: 'user@example.com',
+ projects_limit: 10,
+ username: 'test',
+ name: user_name,
+ state: user_state,
+ ghost: ghost,
+ last_activity_on: 1.minute.ago,
+ user_type: user_type,
+ confirmed_at: 1.day.ago)
+ end
+
+ let(:migration_bot) { User.migration_bot }
let!(:snippet_with_repo) { snippets.create(id: 1, type: 'PersonalSnippet', author_id: user.id, file_name: file_name, content: content) }
let!(:snippet_with_empty_repo) { snippets.create(id: 2, type: 'PersonalSnippet', author_id: user.id, file_name: file_name, content: content) }
let!(:snippet_without_repo) { snippets.create(id: 3, type: 'PersonalSnippet', author_id: user.id, file_name: file_name, content: content) }
@@ -53,15 +71,52 @@ describe Gitlab::BackgroundMigration::BackfillSnippetRepositories, :migration, s
end
end
- shared_examples 'commits the file to the repository' do
+ shared_examples 'migration_bot user commits files' do
it do
subject
- blob = blob_at(snippet, file_name)
+ last_commit = raw_repository(snippet).commit
- aggregate_failures do
- expect(blob).to be
- expect(blob.data).to eq content
+ expect(last_commit.author_name).to eq migration_bot.name
+ expect(last_commit.author_email).to eq migration_bot.email
+ end
+ end
+
+ shared_examples 'commits the file to the repository' do
+ context 'when author can update snippet and use git' do
+ it 'creates the repository and commit the file' do
+ subject
+
+ blob = blob_at(snippet, file_name)
+ last_commit = raw_repository(snippet).commit
+
+ aggregate_failures do
+ expect(blob).to be
+ expect(blob.data).to eq content
+ expect(last_commit.author_name).to eq user.name
+ expect(last_commit.author_email).to eq user.email
+ end
+ end
+ end
+
+ context 'when author cannot update snippet or use git' do
+ context 'when user is blocked' do
+ let(:user_state) { 'blocked' }
+
+ it_behaves_like 'migration_bot user commits files'
+ end
+
+ context 'when user is deactivated' do
+ let(:user_state) { 'deactivated' }
+
+ it_behaves_like 'migration_bot user commits files'
+ end
+
+ context 'when user is a ghost' do
+ let(:ghost) { true }
+ let(:user_type) { 'ghost' }
+
+ it_behaves_like 'migration_bot user commits files'
end
end
end
@@ -123,6 +178,124 @@ describe Gitlab::BackgroundMigration::BackfillSnippetRepositories, :migration, s
end
end
end
+
+ context 'with invalid file names' do
+ using RSpec::Parameterized::TableSyntax
+
+ where(:invalid_file_name, :converted_file_name) do
+ 'filename.js // with comment' | 'filename-js-with-comment'
+ '.git/hooks/pre-commit' | 'git-hooks-pre-commit'
+ 'https://gitlab.com' | 'https-gitlab-com'
+ 'html://web.title%mp4/mpg/mpeg.net' | 'html-web-title-mp4-mpg-mpeg-net'
+ '../../etc/passwd' | 'etc-passwd'
+ '.' | 'snippetfile1.txt'
+ end
+
+ with_them do
+ let!(:snippet_with_invalid_path) { snippets.create(id: 4, type: 'PersonalSnippet', author_id: user.id, file_name: invalid_file_name, content: content) }
+ let!(:snippet_with_valid_path) { snippets.create(id: 5, type: 'PersonalSnippet', author_id: user.id, file_name: file_name, content: content) }
+ let(:ids) { [4, 5] }
+
+ after do
+ raw_repository(snippet_with_invalid_path).remove
+ raw_repository(snippet_with_valid_path).remove
+ end
+
+ it 'checks for file path errors when errors are raised' do
+ expect(service).to receive(:set_file_path_error).once.and_call_original
+
+ subject
+ end
+
+ it 'converts invalid filenames' do
+ subject
+
+ expect(blob_at(snippet_with_invalid_path, converted_file_name)).to be
+ end
+
+ it 'does not convert valid filenames on subsequent migrations' do
+ subject
+
+ expect(blob_at(snippet_with_valid_path, file_name)).to be
+ end
+ end
+ end
+
+ context 'when snippet content size is higher than the existing limit' do
+ let(:limit) { 15 }
+ let(:content) { 'a' * (limit + 1) }
+ let(:snippet) { snippet_without_repo }
+ let(:ids) { [snippet.id, snippet.id] }
+
+ before do
+ allow(Gitlab::CurrentSettings).to receive(:snippet_size_limit).and_return(limit)
+ end
+
+ it_behaves_like 'migration_bot user commits files'
+ end
+
+ context 'when user name is invalid' do
+ let(:user_name) { '.' }
+ let!(:snippet) { snippets.create(id: 4, type: 'PersonalSnippet', author_id: user.id, file_name: file_name, content: content) }
+ let(:ids) { [4, 4] }
+
+ after do
+ raw_repository(snippet).remove
+ end
+
+ it_behaves_like 'migration_bot user commits files'
+ end
+
+ context 'when both user name and snippet file_name are invalid' do
+ let(:user_name) { '.' }
+ let!(:other_user) do
+ users.create(id: 2,
+ email: 'user2@example.com',
+ projects_limit: 10,
+ username: 'test2',
+ name: 'Test2',
+ state: user_state,
+ ghost: ghost,
+ last_activity_on: 1.minute.ago,
+ user_type: user_type,
+ confirmed_at: 1.day.ago)
+ end
+ let!(:invalid_snippet) { snippets.create(id: 4, type: 'PersonalSnippet', author_id: user.id, file_name: '.', content: content) }
+ let!(:snippet) { snippets.create(id: 5, type: 'PersonalSnippet', author_id: other_user.id, file_name: file_name, content: content) }
+ let(:ids) { [4, 5] }
+
+ after do
+ raw_repository(snippet).remove
+ raw_repository(invalid_snippet).remove
+ end
+
+ it 'updates the file_name only when it is invalid' do
+ subject
+
+ expect(blob_at(invalid_snippet, 'snippetfile1.txt')).to be
+ expect(blob_at(snippet, file_name)).to be
+ end
+
+ it_behaves_like 'migration_bot user commits files' do
+ let(:snippet) { invalid_snippet }
+ end
+
+ it 'does not alter the commit author in subsequent migrations' do
+ subject
+
+ last_commit = raw_repository(snippet).commit
+
+ expect(last_commit.author_name).to eq other_user.name
+ expect(last_commit.author_email).to eq other_user.email
+ end
+
+ it "increases the number of retries temporarily from #{described_class::MAX_RETRIES} to #{described_class::MAX_RETRIES + 1}" do
+ expect(service).to receive(:create_commit).with(Snippet.find(invalid_snippet.id)).exactly(described_class::MAX_RETRIES + 1).times.and_call_original
+ expect(service).to receive(:create_commit).with(Snippet.find(snippet.id)).once.and_call_original
+
+ subject
+ end
+ end
end
def blob_at(snippet, path)
diff --git a/spec/lib/gitlab/background_migration/migrate_issue_trackers_sensitive_data_spec.rb b/spec/lib/gitlab/background_migration/migrate_issue_trackers_sensitive_data_spec.rb
index 7dae28f72a5..4411dca3fd9 100644
--- a/spec/lib/gitlab/background_migration/migrate_issue_trackers_sensitive_data_spec.rb
+++ b/spec/lib/gitlab/background_migration/migrate_issue_trackers_sensitive_data_spec.rb
@@ -4,40 +4,45 @@ require 'spec_helper'
describe Gitlab::BackgroundMigration::MigrateIssueTrackersSensitiveData, schema: 20200130145430 do
let(:services) { table(:services) }
- # we need to define the classes due to encryption
- class IssueTrackerData < ApplicationRecord
- self.table_name = 'issue_tracker_data'
-
- def self.encryption_options
- {
- key: Settings.attr_encrypted_db_key_base_32,
- encode: true,
- mode: :per_attribute_iv,
- algorithm: 'aes-256-gcm'
- }
+ before do
+ # we need to define the classes due to encryption
+ issue_tracker_data = Class.new(ApplicationRecord) do
+ self.table_name = 'issue_tracker_data'
+
+ def self.encryption_options
+ {
+ key: Settings.attr_encrypted_db_key_base_32,
+ encode: true,
+ mode: :per_attribute_iv,
+ algorithm: 'aes-256-gcm'
+ }
+ end
+
+ attr_encrypted :project_url, encryption_options
+ attr_encrypted :issues_url, encryption_options
+ attr_encrypted :new_issue_url, encryption_options
end
- attr_encrypted :project_url, encryption_options
- attr_encrypted :issues_url, encryption_options
- attr_encrypted :new_issue_url, encryption_options
- end
+ jira_tracker_data = Class.new(ApplicationRecord) do
+ self.table_name = 'jira_tracker_data'
- class JiraTrackerData < ApplicationRecord
- self.table_name = 'jira_tracker_data'
+ def self.encryption_options
+ {
+ key: Settings.attr_encrypted_db_key_base_32,
+ encode: true,
+ mode: :per_attribute_iv,
+ algorithm: 'aes-256-gcm'
+ }
+ end
- def self.encryption_options
- {
- key: Settings.attr_encrypted_db_key_base_32,
- encode: true,
- mode: :per_attribute_iv,
- algorithm: 'aes-256-gcm'
- }
+ attr_encrypted :url, encryption_options
+ attr_encrypted :api_url, encryption_options
+ attr_encrypted :username, encryption_options
+ attr_encrypted :password, encryption_options
end
- attr_encrypted :url, encryption_options
- attr_encrypted :api_url, encryption_options
- attr_encrypted :username, encryption_options
- attr_encrypted :password, encryption_options
+ stub_const('IssueTrackerData', issue_tracker_data)
+ stub_const('JiraTrackerData', jira_tracker_data)
end
let(:url) { 'http://base-url.tracker.com' }
@@ -90,7 +95,7 @@ describe Gitlab::BackgroundMigration::MigrateIssueTrackersSensitiveData, schema:
end
end
- context 'with jira service' do
+ context 'with Jira service' do
let!(:service) do
services.create(id: 10, type: 'JiraService', title: nil, properties: jira_properties.to_json, category: 'issue_tracker')
end
@@ -202,7 +207,7 @@ describe Gitlab::BackgroundMigration::MigrateIssueTrackersSensitiveData, schema:
end
end
- context 'with jira service which has data fields record inconsistent with properties field' do
+ context 'with Jira service which has data fields record inconsistent with properties field' do
let!(:service) do
services.create(id: 16, type: 'CustomIssueTrackerService', description: 'Existing description', properties: jira_properties.to_json, category: 'issue_tracker').tap do |service|
JiraTrackerData.create!(service_id: service.id, url: 'http://other_jira_url')
@@ -241,7 +246,7 @@ describe Gitlab::BackgroundMigration::MigrateIssueTrackersSensitiveData, schema:
end
end
- context 'jira service with empty properties' do
+ context 'Jira service with empty properties' do
let!(:service) do
services.create(id: 18, type: 'JiraService', properties: '', category: 'issue_tracker')
end
@@ -253,7 +258,7 @@ describe Gitlab::BackgroundMigration::MigrateIssueTrackersSensitiveData, schema:
end
end
- context 'jira service with nil properties' do
+ context 'Jira service with nil properties' do
let!(:service) do
services.create(id: 18, type: 'JiraService', properties: nil, category: 'issue_tracker')
end
@@ -265,7 +270,7 @@ describe Gitlab::BackgroundMigration::MigrateIssueTrackersSensitiveData, schema:
end
end
- context 'jira service with invalid properties' do
+ context 'Jira service with invalid properties' do
let!(:service) do
services.create(id: 18, type: 'JiraService', properties: 'invalid data', category: 'issue_tracker')
end
@@ -277,7 +282,7 @@ describe Gitlab::BackgroundMigration::MigrateIssueTrackersSensitiveData, schema:
end
end
- context 'with jira service with invalid properties, valid jira service and valid bugzilla service' do
+ context 'with Jira service with invalid properties, valid Jira service and valid bugzilla service' do
let!(:jira_service_invalid) do
services.create(id: 19, title: 'invalid - title', description: 'invalid - description', type: 'JiraService', properties: 'invalid data', category: 'issue_tracker')
end
diff --git a/spec/lib/gitlab/chat/responder/mattermost_spec.rb b/spec/lib/gitlab/chat/responder/mattermost_spec.rb
new file mode 100644
index 00000000000..f3480dfef06
--- /dev/null
+++ b/spec/lib/gitlab/chat/responder/mattermost_spec.rb
@@ -0,0 +1,117 @@
+# frozen_string_literal: true
+
+require 'spec_helper'
+
+describe Gitlab::Chat::Responder::Mattermost do
+ let(:chat_name) { create(:chat_name, chat_id: 'U123') }
+
+ let(:pipeline) do
+ pipeline = create(:ci_pipeline)
+
+ pipeline.create_chat_data!(
+ response_url: 'http://example.com',
+ chat_name_id: chat_name.id
+ )
+
+ pipeline
+ end
+
+ let(:build) { create(:ci_build, pipeline: pipeline) }
+ let(:responder) { described_class.new(build) }
+
+ describe '#send_response' do
+ it 'sends a response back to Slack' do
+ expect(Gitlab::HTTP).to receive(:post).with(
+ 'http://example.com',
+ { headers: { 'Content-Type': 'application/json' }, body: 'hello'.to_json }
+ )
+
+ responder.send_response('hello')
+ end
+ end
+
+ describe '#success' do
+ it 'returns the output for a successful build' do
+ expect(responder)
+ .to receive(:send_response)
+ .with(
+ hash_including(
+ response_type: :in_channel,
+ attachments: array_including(
+ a_hash_including(
+ text: /#{pipeline.chat_data.chat_name.user.name}.*completed successfully/,
+ fields: array_including(
+ a_hash_including(value: /##{build.id}/),
+ a_hash_including(value: build.name),
+ a_hash_including(value: "```shell\nscript output\n```")
+ )
+ )
+ )
+ )
+ )
+
+ responder.success('script output')
+ end
+
+ it 'limits the output to a fixed size' do
+ expect(responder)
+ .to receive(:send_response)
+ .with(
+ hash_including(
+ response_type: :in_channel,
+ attachments: array_including(
+ a_hash_including(
+ fields: array_including(
+ a_hash_including(value: /The output is too large/)
+ )
+ )
+ )
+ )
+ )
+
+ responder.success('a' * 4000)
+ end
+
+ it 'does not send a response if the output is empty' do
+ expect(responder).not_to receive(:send_response)
+
+ responder.success('')
+ end
+ end
+
+ describe '#failure' do
+ it 'returns the output for a failed build' do
+ expect(responder)
+ .to receive(:send_response)
+ .with(
+ hash_including(
+ response_type: :in_channel,
+ attachments: array_including(
+ a_hash_including(
+ text: /#{pipeline.chat_data.chat_name.user.name}.*failed/,
+ fields: array_including(
+ a_hash_including(value: /##{build.id}/),
+ a_hash_including(value: build.name)
+ )
+ )
+ )
+ )
+ )
+
+ responder.failure
+ end
+ end
+
+ describe '#scheduled_output' do
+ it 'returns the output for a scheduled build' do
+ output = responder.scheduled_output
+
+ expect(output).to match(
+ hash_including(
+ response_type: :ephemeral,
+ text: /##{build.id}/
+ )
+ )
+ end
+ end
+end
diff --git a/spec/lib/gitlab/checks/push_file_count_check_spec.rb b/spec/lib/gitlab/checks/push_file_count_check_spec.rb
index 58ba7d579a3..e05102a9ce8 100644
--- a/spec/lib/gitlab/checks/push_file_count_check_spec.rb
+++ b/spec/lib/gitlab/checks/push_file_count_check_spec.rb
@@ -8,7 +8,7 @@ describe Gitlab::Checks::PushFileCountCheck do
let(:timeout) { Gitlab::GitAccess::INTERNAL_TIMEOUT }
let(:logger) { Gitlab::Checks::TimedLogger.new(timeout: timeout) }
- subject { described_class.new(changes, repository: snippet.repository, limit: 1, logger: logger) }
+ subject { described_class.new(changes, repository: snippet.repository, limit: 2, logger: logger) }
describe '#validate!' do
using RSpec::Parameterized::TableSyntax
@@ -31,7 +31,7 @@ describe Gitlab::Checks::PushFileCountCheck do
where(:old, :new, :valid, :message) do
'single-file' | 'edit-file' | true | nil
- 'single-file' | 'multiple-files' | false | 'The repository can contain at most 1 file(s).'
+ 'single-file' | 'multiple-files' | false | 'The repository can contain at most 2 file(s).'
'single-file' | 'no-files' | false | 'The repository must contain at least 1 file.'
'edit-file' | 'rename-and-edit-file' | true | nil
end
diff --git a/spec/lib/gitlab/ci/config/entry/artifacts_spec.rb b/spec/lib/gitlab/ci/config/entry/artifacts_spec.rb
index 513a9b8f2b4..8cfd07df777 100644
--- a/spec/lib/gitlab/ci/config/entry/artifacts_spec.rb
+++ b/spec/lib/gitlab/ci/config/entry/artifacts_spec.rb
@@ -123,25 +123,53 @@ describe Gitlab::Ci::Config::Entry::Artifacts do
end
end
end
+ end
- context 'when feature flag :ci_expose_arbitrary_artifacts_in_mr is disabled' do
+ describe 'excluded artifacts' do
+ context 'when configuration is valid and the feature is enabled' do
before do
- stub_feature_flags(ci_expose_arbitrary_artifacts_in_mr: false)
+ stub_feature_flags(ci_artifacts_exclude: true)
end
- context 'when syntax is correct' do
- let(:config) { { expose_as: 'Test results', paths: ['test.txt'] } }
+ context 'when configuration is valid' do
+ let(:config) { { untracked: true, exclude: ['some/directory/'] } }
- it 'is valid' do
- expect(entry.errors).to be_empty
+ it 'correctly parses the configuration' do
+ expect(entry).to be_valid
+ expect(entry.value).to eq config
end
end
- context 'when syntax for :expose_as is incorrect' do
- let(:config) { { paths: %w[results.txt], expose_as: '' } }
+ context 'when configuration is not valid' do
+ let(:config) { { untracked: true, exclude: 1234 } }
+
+ it 'returns an error' do
+ expect(entry).not_to be_valid
+ expect(entry.errors)
+ .to include 'artifacts exclude should be an array of strings'
+ end
+ end
+ end
+
+ context 'when artifacts/exclude feature is disabled' do
+ before do
+ stub_feature_flags(ci_artifacts_exclude: false)
+ end
+
+ context 'when configuration has been provided' do
+ let(:config) { { untracked: true, exclude: ['some/directory/'] } }
+
+ it 'returns an error' do
+ expect(entry).not_to be_valid
+ expect(entry.errors).to include 'artifacts exclude feature is disabled'
+ end
+ end
+
+ context 'when configuration is not present' do
+ let(:config) { { untracked: true } }
- it 'is valid' do
- expect(entry.errors).to be_empty
+ it 'is a valid configuration' do
+ expect(entry).to be_valid
end
end
end
diff --git a/spec/lib/gitlab/ci/config/entry/reports_spec.rb b/spec/lib/gitlab/ci/config/entry/reports_spec.rb
index 9bba3eb2b77..8c6c91d919e 100644
--- a/spec/lib/gitlab/ci/config/entry/reports_spec.rb
+++ b/spec/lib/gitlab/ci/config/entry/reports_spec.rb
@@ -47,6 +47,8 @@ describe Gitlab::Ci::Config::Entry::Reports do
:dotenv | 'build.dotenv'
:cobertura | 'cobertura-coverage.xml'
:terraform | 'tfplan.json'
+ :accessibility | 'gl-accessibility.json'
+ :cluster_applications | 'gl-cluster-applications.json'
end
with_them do
diff --git a/spec/lib/gitlab/ci/config/entry/trigger_spec.rb b/spec/lib/gitlab/ci/config/entry/trigger_spec.rb
index 752c3f59a95..dfd9807583c 100644
--- a/spec/lib/gitlab/ci/config/entry/trigger_spec.rb
+++ b/spec/lib/gitlab/ci/config/entry/trigger_spec.rb
@@ -114,19 +114,6 @@ describe Gitlab::Ci::Config::Entry::Trigger do
.to match /config contains unknown keys: branch/
end
end
-
- context 'when feature flag is off' do
- before do
- stub_feature_flags(ci_parent_child_pipeline: false)
- end
-
- let(:config) { { include: 'path/to/config.yml' } }
-
- it 'is returns an error if include is used' do
- expect(subject.errors.first)
- .to match /config must specify project/
- end
- end
end
context 'when config contains unknown keys' do
diff --git a/spec/lib/gitlab/ci/cron_parser_spec.rb b/spec/lib/gitlab/ci/cron_parser_spec.rb
index 385df72fa41..8f9f3d7fa37 100644
--- a/spec/lib/gitlab/ci/cron_parser_spec.rb
+++ b/spec/lib/gitlab/ci/cron_parser_spec.rb
@@ -7,198 +7,240 @@ describe Gitlab::Ci::CronParser do
it { is_expected.to be > Time.now }
end
- describe '#next_time_from' do
- subject { described_class.new(cron, cron_timezone).next_time_from(Time.now) }
+ shared_examples_for "returns time in the past" do
+ it { is_expected.to be < Time.now }
+ end
- context 'when cron and cron_timezone are valid' do
- context 'when specific time' do
- let(:cron) { '3 4 5 6 *' }
- let(:cron_timezone) { 'UTC' }
+ shared_examples_for 'when cron and cron_timezone are valid' do |returns_time_for_epoch|
+ context 'when specific time' do
+ let(:cron) { '3 4 5 6 *' }
+ let(:cron_timezone) { 'UTC' }
- it_behaves_like "returns time in the future"
+ it_behaves_like returns_time_for_epoch
- it 'returns exact time' do
- expect(subject.min).to eq(3)
- expect(subject.hour).to eq(4)
- expect(subject.day).to eq(5)
- expect(subject.month).to eq(6)
- end
+ it 'returns exact time' do
+ expect(subject.min).to eq(3)
+ expect(subject.hour).to eq(4)
+ expect(subject.day).to eq(5)
+ expect(subject.month).to eq(6)
end
+ end
- context 'when specific day of week' do
- let(:cron) { '* * * * 0' }
- let(:cron_timezone) { 'UTC' }
+ context 'when specific day of week' do
+ let(:cron) { '* * * * 0' }
+ let(:cron_timezone) { 'UTC' }
- it_behaves_like "returns time in the future"
+ it_behaves_like returns_time_for_epoch
- it 'returns exact day of week' do
- expect(subject.wday).to eq(0)
- end
+ it 'returns exact day of week' do
+ expect(subject.wday).to eq(0)
end
+ end
- context 'when slash used' do
- let(:cron) { '*/10 */6 */10 */10 *' }
- let(:cron_timezone) { 'UTC' }
+ context 'when slash used' do
+ let(:cron) { '*/10 */6 */10 */10 *' }
+ let(:cron_timezone) { 'UTC' }
- it_behaves_like "returns time in the future"
+ it_behaves_like returns_time_for_epoch
- it 'returns specific time' do
- expect(subject.min).to be_in([0, 10, 20, 30, 40, 50])
- expect(subject.hour).to be_in([0, 6, 12, 18])
- expect(subject.day).to be_in([1, 11, 21, 31])
- expect(subject.month).to be_in([1, 11])
- end
+ it 'returns specific time' do
+ expect(subject.min).to be_in([0, 10, 20, 30, 40, 50])
+ expect(subject.hour).to be_in([0, 6, 12, 18])
+ expect(subject.day).to be_in([1, 11, 21, 31])
+ expect(subject.month).to be_in([1, 11])
end
+ end
- context 'when range used' do
- let(:cron) { '0,20,40 * 1-5 * *' }
- let(:cron_timezone) { 'UTC' }
+ context 'when range used' do
+ let(:cron) { '0,20,40 * 1-5 * *' }
+ let(:cron_timezone) { 'UTC' }
- it_behaves_like "returns time in the future"
+ it_behaves_like returns_time_for_epoch
- it 'returns specific time' do
- expect(subject.min).to be_in([0, 20, 40])
- expect(subject.day).to be_in((1..5).to_a)
- end
+ it 'returns specific time' do
+ expect(subject.min).to be_in([0, 20, 40])
+ expect(subject.day).to be_in((1..5).to_a)
end
+ end
- context 'when cron_timezone is TZInfo format' do
- before do
- allow(Time).to receive(:zone)
- .and_return(ActiveSupport::TimeZone['UTC'])
- end
+ context 'when cron_timezone is TZInfo format' do
+ before do
+ allow(Time).to receive(:zone)
+ .and_return(ActiveSupport::TimeZone['UTC'])
+ end
- let(:hour_in_utc) do
- ActiveSupport::TimeZone[cron_timezone]
- .now.change(hour: 0).in_time_zone('UTC').hour
- end
+ let(:hour_in_utc) do
+ ActiveSupport::TimeZone[cron_timezone]
+ .now.change(hour: 0).in_time_zone('UTC').hour
+ end
- context 'when cron_timezone is US/Pacific' do
- let(:cron) { '* 0 * * *' }
- let(:cron_timezone) { 'US/Pacific' }
+ context 'when cron_timezone is US/Pacific' do
+ let(:cron) { '* 0 * * *' }
+ let(:cron_timezone) { 'US/Pacific' }
- it_behaves_like "returns time in the future"
+ it_behaves_like returns_time_for_epoch
- context 'when PST (Pacific Standard Time)' do
- it 'converts time in server time zone' do
- Timecop.freeze(Time.utc(2017, 1, 1)) do
- expect(subject.hour).to eq(hour_in_utc)
- end
+ context 'when PST (Pacific Standard Time)' do
+ it 'converts time in server time zone' do
+ Timecop.freeze(Time.utc(2017, 1, 1)) do
+ expect(subject.hour).to eq(hour_in_utc)
end
end
+ end
- context 'when PDT (Pacific Daylight Time)' do
- it 'converts time in server time zone' do
- Timecop.freeze(Time.utc(2017, 6, 1)) do
- expect(subject.hour).to eq(hour_in_utc)
- end
+ context 'when PDT (Pacific Daylight Time)' do
+ it 'converts time in server time zone' do
+ Timecop.freeze(Time.utc(2017, 6, 1)) do
+ expect(subject.hour).to eq(hour_in_utc)
end
end
end
end
+ end
- context 'when cron_timezone is ActiveSupport::TimeZone format' do
- before do
- allow(Time).to receive(:zone)
- .and_return(ActiveSupport::TimeZone['UTC'])
- end
+ context 'when cron_timezone is ActiveSupport::TimeZone format' do
+ before do
+ allow(Time).to receive(:zone)
+ .and_return(ActiveSupport::TimeZone['UTC'])
+ end
- let(:hour_in_utc) do
- ActiveSupport::TimeZone[cron_timezone]
- .now.change(hour: 0).in_time_zone('UTC').hour
- end
+ let(:hour_in_utc) do
+ ActiveSupport::TimeZone[cron_timezone]
+ .now.change(hour: 0).in_time_zone('UTC').hour
+ end
- context 'when cron_timezone is Berlin' do
- let(:cron) { '* 0 * * *' }
- let(:cron_timezone) { 'Berlin' }
+ context 'when cron_timezone is Berlin' do
+ let(:cron) { '* 0 * * *' }
+ let(:cron_timezone) { 'Berlin' }
- it_behaves_like "returns time in the future"
+ it_behaves_like returns_time_for_epoch
- context 'when CET (Central European Time)' do
- it 'converts time in server time zone' do
- Timecop.freeze(Time.utc(2017, 1, 1)) do
- expect(subject.hour).to eq(hour_in_utc)
- end
+ context 'when CET (Central European Time)' do
+ it 'converts time in server time zone' do
+ Timecop.freeze(Time.utc(2017, 1, 1)) do
+ expect(subject.hour).to eq(hour_in_utc)
end
end
+ end
- context 'when CEST (Central European Summer Time)' do
- it 'converts time in server time zone' do
- Timecop.freeze(Time.utc(2017, 6, 1)) do
- expect(subject.hour).to eq(hour_in_utc)
- end
+ context 'when CEST (Central European Summer Time)' do
+ it 'converts time in server time zone' do
+ Timecop.freeze(Time.utc(2017, 6, 1)) do
+ expect(subject.hour).to eq(hour_in_utc)
end
end
end
+ end
+ end
+ end
- context 'when cron_timezone is Eastern Time (US & Canada)' do
- let(:cron) { '* 0 * * *' }
- let(:cron_timezone) { 'Eastern Time (US & Canada)' }
+ shared_examples_for 'when cron_timezone is Eastern Time (US & Canada)' do |returns_time_for_epoch, year|
+ let(:cron) { '* 0 * * *' }
+ let(:cron_timezone) { 'Eastern Time (US & Canada)' }
- it_behaves_like "returns time in the future"
+ before do
+ allow(Time).to receive(:zone)
+ .and_return(ActiveSupport::TimeZone['UTC'])
+ end
- context 'when EST (Eastern Standard Time)' do
- it 'converts time in server time zone' do
- Timecop.freeze(Time.utc(2017, 1, 1)) do
- expect(subject.hour).to eq(hour_in_utc)
- end
- end
- end
+ let(:hour_in_utc) do
+ ActiveSupport::TimeZone[cron_timezone]
+ .now.change(hour: 0).in_time_zone('UTC').hour
+ end
- context 'when EDT (Eastern Daylight Time)' do
- it 'converts time in server time zone' do
- Timecop.freeze(Time.utc(2017, 6, 1)) do
- expect(subject.hour).to eq(hour_in_utc)
- end
- end
- end
+ it_behaves_like returns_time_for_epoch
- context 'when time crosses a Daylight Savings boundary' do
- let(:cron) { '* 0 1 12 *'}
-
- # Note this previously only failed if the time zone is set
- # to a zone that observes Daylight Savings
- # (e.g. America/Chicago) at the start of the test. Stubbing
- # TZ doesn't appear to be enough.
- it 'generates day without TZInfo::AmbiguousTime error' do
- Timecop.freeze(Time.utc(2020, 1, 1)) do
- expect(subject.year).to eq(2020)
- expect(subject.month).to eq(12)
- expect(subject.day).to eq(1)
- end
- end
- end
+ context 'when EST (Eastern Standard Time)' do
+ it 'converts time in server time zone' do
+ Timecop.freeze(Time.utc(2017, 1, 1)) do
+ expect(subject.hour).to eq(hour_in_utc)
end
end
end
- context 'when cron and cron_timezone are invalid' do
- let(:cron) { 'invalid_cron' }
- let(:cron_timezone) { 'invalid_cron_timezone' }
+ context 'when EDT (Eastern Daylight Time)' do
+ it 'converts time in server time zone' do
+ Timecop.freeze(Time.utc(2017, 6, 1)) do
+ expect(subject.hour).to eq(hour_in_utc)
+ end
+ end
+ end
- it { is_expected.to be_nil }
+ context 'when time crosses a Daylight Savings boundary' do
+ let(:cron) { '* 0 1 12 *'}
+
+ # Note this previously only failed if the time zone is set
+ # to a zone that observes Daylight Savings
+ # (e.g. America/Chicago) at the start of the test. Stubbing
+ # TZ doesn't appear to be enough.
+ it 'generates day without TZInfo::AmbiguousTime error' do
+ Timecop.freeze(Time.utc(2020, 1, 1)) do
+ expect(subject.year).to eq(year)
+ expect(subject.month).to eq(12)
+ expect(subject.day).to eq(1)
+ end
+ end
end
+ end
- context 'when cron syntax is quoted' do
- let(:cron) { "'0 * * * *'" }
- let(:cron_timezone) { 'UTC' }
+ shared_examples_for 'when cron and cron_timezone are invalid' do
+ let(:cron) { 'invalid_cron' }
+ let(:cron_timezone) { 'invalid_cron_timezone' }
- it { expect(subject).to be_nil }
- end
+ it { is_expected.to be_nil }
+ end
- context 'when cron syntax is rufus-scheduler syntax' do
- let(:cron) { 'every 3h' }
- let(:cron_timezone) { 'UTC' }
+ shared_examples_for 'when cron syntax is quoted' do
+ let(:cron) { "'0 * * * *'" }
+ let(:cron_timezone) { 'UTC' }
- it { expect(subject).to be_nil }
- end
+ it { expect(subject).to be_nil }
+ end
- context 'when cron is scheduled to a non existent day' do
- let(:cron) { '0 12 31 2 *' }
- let(:cron_timezone) { 'UTC' }
+ shared_examples_for 'when cron syntax is rufus-scheduler syntax' do
+ let(:cron) { 'every 3h' }
+ let(:cron_timezone) { 'UTC' }
- it { expect(subject).to be_nil }
- end
+ it { expect(subject).to be_nil }
+ end
+
+ shared_examples_for 'when cron is scheduled to a non existent day' do
+ let(:cron) { '0 12 31 2 *' }
+ let(:cron_timezone) { 'UTC' }
+
+ it { expect(subject).to be_nil }
+ end
+
+ describe '#next_time_from' do
+ subject { described_class.new(cron, cron_timezone).next_time_from(Time.now) }
+
+ it_behaves_like 'when cron and cron_timezone are valid', 'returns time in the future'
+
+ it_behaves_like 'when cron_timezone is Eastern Time (US & Canada)', 'returns time in the future', 2020
+
+ it_behaves_like 'when cron and cron_timezone are invalid'
+
+ it_behaves_like 'when cron syntax is quoted'
+
+ it_behaves_like 'when cron syntax is rufus-scheduler syntax'
+
+ it_behaves_like 'when cron is scheduled to a non existent day'
+ end
+
+ describe '#previous_time_from' do
+ subject { described_class.new(cron, cron_timezone).previous_time_from(Time.now) }
+
+ it_behaves_like 'when cron and cron_timezone are valid', 'returns time in the past'
+
+ it_behaves_like 'when cron_timezone is Eastern Time (US & Canada)', 'returns time in the past', 2019
+
+ it_behaves_like 'when cron and cron_timezone are invalid'
+
+ it_behaves_like 'when cron syntax is quoted'
+
+ it_behaves_like 'when cron syntax is rufus-scheduler syntax'
+
+ it_behaves_like 'when cron is scheduled to a non existent day'
end
describe '#cron_valid?' do
diff --git a/spec/lib/gitlab/ci/parsers/accessibility/pa11y_spec.rb b/spec/lib/gitlab/ci/parsers/accessibility/pa11y_spec.rb
new file mode 100644
index 00000000000..4d87e3b201a
--- /dev/null
+++ b/spec/lib/gitlab/ci/parsers/accessibility/pa11y_spec.rb
@@ -0,0 +1,118 @@
+# frozen_string_literal: true
+
+require 'fast_spec_helper'
+
+describe Gitlab::Ci::Parsers::Accessibility::Pa11y do
+ describe '#parse!' do
+ subject { described_class.new.parse!(pa11y, accessibility_report) }
+
+ let(:accessibility_report) { Gitlab::Ci::Reports::AccessibilityReports.new }
+
+ context "when data is pa11y style JSON" do
+ context "when there are no URLs provided" do
+ let(:pa11y) do
+ {
+ "total": 1,
+ "passes": 0,
+ "errors": 0,
+ "results": {
+ "": [
+ {
+ "message": "Protocol error (Page.navigate): Cannot navigate to invalid URL"
+ }
+ ]
+ }
+ }.to_json
+ end
+
+ it "returns an accessibility report" do
+ expect { subject }.not_to raise_error
+
+ expect(accessibility_report.errors_count).to eq(0)
+ expect(accessibility_report.passes_count).to eq(0)
+ expect(accessibility_report.scans_count).to eq(0)
+ expect(accessibility_report.urls).to be_empty
+ expect(accessibility_report.error_message).to eq("Empty URL detected in gl-accessibility.json")
+ end
+ end
+
+ context "when there are no errors" do
+ let(:pa11y) do
+ {
+ "total": 1,
+ "passes": 1,
+ "errors": 0,
+ "results": {
+ "http://pa11y.org/": []
+ }
+ }.to_json
+ end
+
+ it "returns an accessibility report" do
+ expect { subject }.not_to raise_error
+
+ expect(accessibility_report.urls['http://pa11y.org/']).to be_empty
+ expect(accessibility_report.errors_count).to eq(0)
+ expect(accessibility_report.passes_count).to eq(1)
+ expect(accessibility_report.scans_count).to eq(1)
+ end
+ end
+
+ context "when there are errors" do
+ let(:pa11y) do
+ {
+ "total": 1,
+ "passes": 0,
+ "errors": 1,
+ "results": {
+ "https://about.gitlab.com/": [
+ {
+ "code": "WCAG2AA.Principle4.Guideline4_1.4_1_2.H91.A.NoContent",
+ "type": "error",
+ "typeCode": 1,
+ "message": "Anchor element found with a valid href attribute, but no link content has been supplied.",
+ "context": "<a href=\"/\" class=\"navbar-brand animated\"><svg height=\"36\" viewBox=\"0 0 1...</a>",
+ "selector": "#main-nav > div:nth-child(1) > a",
+ "runner": "htmlcs",
+ "runnerExtras": {}
+ }
+ ]
+ }
+ }.to_json
+ end
+
+ it "returns an accessibility report" do
+ expect { subject }.not_to raise_error
+
+ expect(accessibility_report.errors_count).to eq(1)
+ expect(accessibility_report.passes_count).to eq(0)
+ expect(accessibility_report.scans_count).to eq(1)
+ expect(accessibility_report.urls['https://about.gitlab.com/']).to be_present
+ expect(accessibility_report.urls['https://about.gitlab.com/'].first[:code]).to be_present
+ end
+ end
+ end
+
+ context "when data is not a valid JSON string" do
+ let(:pa11y) do
+ {
+ "total": 1,
+ "passes": 1,
+ "errors": 0,
+ "results": {
+ "http://pa11y.org/": []
+ }
+ }
+ end
+
+ it "sets error_message" do
+ expect { subject }.not_to raise_error
+
+ expect(accessibility_report.error_message).to include('Pa11y parsing failed')
+ expect(accessibility_report.errors_count).to eq(0)
+ expect(accessibility_report.passes_count).to eq(0)
+ expect(accessibility_report.scans_count).to eq(0)
+ end
+ end
+ end
+end
diff --git a/spec/lib/gitlab/ci/parsers/terraform/tfplan_spec.rb b/spec/lib/gitlab/ci/parsers/terraform/tfplan_spec.rb
new file mode 100644
index 00000000000..19cd75e586c
--- /dev/null
+++ b/spec/lib/gitlab/ci/parsers/terraform/tfplan_spec.rb
@@ -0,0 +1,51 @@
+# frozen_string_literal: true
+
+require 'spec_helper'
+
+describe Gitlab::Ci::Parsers::Terraform::Tfplan do
+ describe '#parse!' do
+ let_it_be(:artifact) { create(:ci_job_artifact, :terraform) }
+
+ let(:reports) { Gitlab::Ci::Reports::TerraformReports.new }
+
+ context 'when data is tfplan.json' do
+ context 'when there is no data' do
+ it 'raises an error' do
+ plan = '{}'
+
+ expect { subject.parse!(plan, reports, artifact: artifact) }.to raise_error(
+ described_class::TfplanParserError
+ )
+ end
+ end
+
+ context 'when there is data' do
+ it 'parses JSON and returns a report' do
+ plan = '{ "create": 0, "update": 1, "delete": 0 }'
+
+ expect { subject.parse!(plan, reports, artifact: artifact) }.not_to raise_error
+
+ expect(reports.plans).to match(
+ a_hash_including(
+ 'tfplan.json' => a_hash_including(
+ 'create' => 0,
+ 'update' => 1,
+ 'delete' => 0
+ )
+ )
+ )
+ end
+ end
+ end
+
+ context 'when data is not tfplan.json' do
+ it 'raises an error' do
+ plan = { 'create' => 0, 'update' => 1, 'delete' => 0 }.to_s
+
+ expect { subject.parse!(plan, reports, artifact: artifact) }.to raise_error(
+ described_class::TfplanParserError
+ )
+ end
+ end
+ end
+end
diff --git a/spec/lib/gitlab/ci/parsers/test/junit_spec.rb b/spec/lib/gitlab/ci/parsers/test/junit_spec.rb
index b4be5a41cd7..7b7ace02bba 100644
--- a/spec/lib/gitlab/ci/parsers/test/junit_spec.rb
+++ b/spec/lib/gitlab/ci/parsers/test/junit_spec.rb
@@ -215,8 +215,64 @@ describe Gitlab::Ci::Parsers::Test::Junit do
context 'when data is not JUnit style XML' do
let(:junit) { { testsuite: 'abc' }.to_json }
- it 'raises an error' do
- expect { subject }.to raise_error(described_class::JunitParserError)
+ it 'attaches an error to the TestSuite object' do
+ expect { subject }.not_to raise_error
+ expect(test_cases).to be_empty
+ end
+ end
+
+ context 'when data is malformed JUnit XML' do
+ let(:junit) do
+ <<-EOF.strip_heredoc
+ <testsuite>
+ <testcase classname='Calculator' name='sumTest1' time='0.01'></testcase>
+ <testcase classname='Calculator' name='sumTest2' time='0.02'></testcase
+ </testsuite>
+ EOF
+ end
+
+ it 'attaches an error to the TestSuite object' do
+ expect { subject }.not_to raise_error
+ expect(test_suite.suite_error).to eq("JUnit XML parsing failed: 4:1: FATAL: expected '>'")
+ end
+
+ it 'returns 0 tests cases' do
+ subject
+
+ expect(test_cases).to be_empty
+ expect(test_suite.total_count).to eq(0)
+ expect(test_suite.success_count).to eq(0)
+ expect(test_suite.error_count).to eq(0)
+ end
+
+ it 'returns a failure status' do
+ subject
+
+ expect(test_suite.total_status).to eq(Gitlab::Ci::Reports::TestCase::STATUS_ERROR)
+ end
+ end
+
+ context 'when data is not XML' do
+ let(:junit) { double(:random_trash) }
+
+ it 'attaches an error to the TestSuite object' do
+ expect { subject }.not_to raise_error
+ expect(test_suite.suite_error).to eq('JUnit data parsing failed: no implicit conversion of RSpec::Mocks::Double into String')
+ end
+
+ it 'returns 0 tests cases' do
+ subject
+
+ expect(test_cases).to be_empty
+ expect(test_suite.total_count).to eq(0)
+ expect(test_suite.success_count).to eq(0)
+ expect(test_suite.error_count).to eq(0)
+ end
+
+ it 'returns a failure status' do
+ subject
+
+ expect(test_suite.total_status).to eq(Gitlab::Ci::Reports::TestCase::STATUS_ERROR)
end
end
diff --git a/spec/lib/gitlab/ci/parsers_spec.rb b/spec/lib/gitlab/ci/parsers_spec.rb
index 9d6896b3cb4..0a266e7a206 100644
--- a/spec/lib/gitlab/ci/parsers_spec.rb
+++ b/spec/lib/gitlab/ci/parsers_spec.rb
@@ -22,6 +22,22 @@ describe Gitlab::Ci::Parsers do
end
end
+ context 'when file_type is accessibility' do
+ let(:file_type) { 'accessibility' }
+
+ it 'fabricates the class' do
+ is_expected.to be_a(described_class::Accessibility::Pa11y)
+ end
+ end
+
+ context 'when file_type is terraform' do
+ let(:file_type) { 'terraform' }
+
+ it 'fabricates the class' do
+ is_expected.to be_a(described_class::Terraform::Tfplan)
+ end
+ end
+
context 'when file_type does not exist' do
let(:file_type) { 'undefined' }
diff --git a/spec/lib/gitlab/ci/pipeline/chain/sequence_spec.rb b/spec/lib/gitlab/ci/pipeline/chain/sequence_spec.rb
index 9033b71b19f..f82e49f9323 100644
--- a/spec/lib/gitlab/ci/pipeline/chain/sequence_spec.rb
+++ b/spec/lib/gitlab/ci/pipeline/chain/sequence_spec.rb
@@ -5,11 +5,13 @@ require 'spec_helper'
describe Gitlab::Ci::Pipeline::Chain::Sequence do
let_it_be(:project) { create(:project) }
let_it_be(:user) { create(:user) }
+
let(:pipeline) { build_stubbed(:ci_pipeline) }
let(:command) { Gitlab::Ci::Pipeline::Chain::Command.new }
let(:first_step) { spy('first step') }
let(:second_step) { spy('second step') }
let(:sequence) { [first_step, second_step] }
+ let(:histogram) { spy('prometheus metric') }
subject do
described_class.new(pipeline, command, sequence)
@@ -52,5 +54,13 @@ describe Gitlab::Ci::Pipeline::Chain::Sequence do
it 'returns a pipeline object' do
expect(subject.build!).to eq pipeline
end
+
+ it 'adds sequence duration to duration histogram' do
+ allow(command).to receive(:duration_histogram).and_return(histogram)
+
+ subject.build!
+
+ expect(histogram).to have_received(:observe)
+ end
end
end
diff --git a/spec/lib/gitlab/ci/reports/accessibility_reports_comparer_spec.rb b/spec/lib/gitlab/ci/reports/accessibility_reports_comparer_spec.rb
new file mode 100644
index 00000000000..31a330f46b1
--- /dev/null
+++ b/spec/lib/gitlab/ci/reports/accessibility_reports_comparer_spec.rb
@@ -0,0 +1,270 @@
+# frozen_string_literal: true
+
+require 'spec_helper'
+
+describe Gitlab::Ci::Reports::AccessibilityReportsComparer do
+ let(:comparer) { described_class.new(base_reports, head_reports) }
+ let(:base_reports) { Gitlab::Ci::Reports::AccessibilityReports.new }
+ let(:head_reports) { Gitlab::Ci::Reports::AccessibilityReports.new }
+ let(:url) { "https://gitlab.com" }
+ let(:single_error) do
+ [
+ {
+ "code" => "WCAG2AA.Principle4.Guideline4_1.4_1_2.H91.A.NoContent",
+ "type" => "error",
+ "typeCode" => 1,
+ "message" => "Anchor element found with a valid href attribute, but no link content has been supplied.",
+ "context" => %{<a href="/" class="navbar-brand animated"><svg height="36" viewBox="0 0 1...</a>},
+ "selector" => "#main-nav > div:nth-child(1) > a",
+ "runner" => "htmlcs",
+ "runnerExtras" => {}
+ }
+ ]
+ end
+ let(:different_error) do
+ [
+ {
+ "code" => "WCAG2AA.Principle1.Guideline1_4.1_4_3.G18.Fail",
+ "type" => "error",
+ "typeCode" => 1,
+ "message" => "This element has insufficient contrast at this conformance level.",
+ "context" => %{<a href="/stages-devops-lifecycle/" class="main-nav-link">Product</a>},
+ "selector" => "#main-nav > div:nth-child(2) > ul > li:nth-child(1) > a",
+ "runner" => "htmlcs",
+ "runnerExtras" => {}
+ }
+ ]
+ end
+
+ describe '#status' do
+ subject { comparer.status }
+
+ context 'when head report has an error' do
+ before do
+ head_reports.add_url(url, single_error)
+ end
+
+ it 'returns status failed' do
+ expect(subject).to eq(described_class::STATUS_FAILED)
+ end
+ end
+
+ context 'when head reports does not have errors' do
+ before do
+ head_reports.add_url(url, [])
+ end
+
+ it 'returns status success' do
+ expect(subject).to eq(described_class::STATUS_SUCCESS)
+ end
+ end
+ end
+
+ describe '#errors_count' do
+ subject { comparer.errors_count }
+
+ context 'when head report has an error' do
+ before do
+ head_reports.add_url(url, single_error)
+ end
+
+ it 'returns the number of new errors' do
+ expect(subject).to eq(1)
+ end
+ end
+
+ context 'when head reports does not have an error' do
+ before do
+ head_reports.add_url(url, [])
+ end
+
+ it 'returns the number new errors' do
+ expect(subject).to eq(0)
+ end
+ end
+ end
+
+ describe '#resolved_count' do
+ subject { comparer.resolved_count }
+
+ context 'when base reports has an error and head has a different error' do
+ before do
+ base_reports.add_url(url, single_error)
+ head_reports.add_url(url, different_error)
+ end
+
+ it 'returns the resolved count' do
+ expect(subject).to eq(1)
+ end
+ end
+
+ context 'when base reports has errors head has no errors' do
+ before do
+ base_reports.add_url(url, single_error)
+ head_reports.add_url(url, [])
+ end
+
+ it 'returns the resolved count' do
+ expect(subject).to eq(1)
+ end
+ end
+
+ context 'when base reports has errors and head has the same error' do
+ before do
+ base_reports.add_url(url, single_error)
+ head_reports.add_url(url, single_error)
+ end
+
+ it 'returns zero' do
+ expect(subject).to eq(0)
+ end
+ end
+
+ context 'when base reports does not have errors and head has errors' do
+ before do
+ head_reports.add_url(url, single_error)
+ end
+
+ it 'returns the number of resolved errors' do
+ expect(subject).to eq(0)
+ end
+ end
+ end
+
+ describe '#total_count' do
+ subject { comparer.total_count }
+
+ context 'when base reports has an error' do
+ before do
+ base_reports.add_url(url, single_error)
+ end
+
+ it 'returns the error count' do
+ expect(subject).to eq(1)
+ end
+ end
+
+ context 'when head report has an error' do
+ before do
+ head_reports.add_url(url, single_error)
+ end
+
+ it 'returns the error count' do
+ expect(subject).to eq(1)
+ end
+ end
+
+ context 'when base report has errors and head report has errors' do
+ before do
+ base_reports.add_url(url, single_error)
+ head_reports.add_url(url, different_error)
+ end
+
+ it 'returns the error count' do
+ expect(subject).to eq(2)
+ end
+ end
+ end
+
+ describe '#existing_errors' do
+ subject { comparer.existing_errors }
+
+ context 'when base report has errors and head has a different error' do
+ before do
+ base_reports.add_url(url, single_error)
+ head_reports.add_url(url, different_error)
+ end
+
+ it 'returns the existing errors' do
+ expect(subject.size).to eq(1)
+ expect(subject.first["code"]).to eq("WCAG2AA.Principle4.Guideline4_1.4_1_2.H91.A.NoContent")
+ end
+ end
+
+ context 'when base report does not have errors and head has errors' do
+ before do
+ base_reports.add_url(url, [])
+ head_reports.add_url(url, single_error)
+ end
+
+ it 'returns an empty array' do
+ expect(subject).to be_empty
+ end
+ end
+ end
+
+ describe '#new_errors' do
+ subject { comparer.new_errors }
+
+ context 'when base reports has errors and head has more errors' do
+ before do
+ base_reports.add_url(url, single_error)
+ head_reports.add_url(url, single_error + different_error)
+ end
+
+ it 'returns new errors between base and head reports' do
+ expect(subject.size).to eq(1)
+ expect(subject.first["code"]).to eq("WCAG2AA.Principle1.Guideline1_4.1_4_3.G18.Fail")
+ end
+ end
+
+ context 'when base reports has an error and head has no errors' do
+ before do
+ base_reports.add_url(url, single_error)
+ head_reports.add_url(url, [])
+ end
+
+ it 'returns an empty array' do
+ expect(subject).to be_empty
+ end
+ end
+
+ context 'when base reports does not have errors and head has errors' do
+ before do
+ head_reports.add_url(url, single_error)
+ end
+
+ it 'returns the new error' do
+ expect(subject.size).to eq(1)
+ expect(subject.first["code"]).to eq("WCAG2AA.Principle4.Guideline4_1.4_1_2.H91.A.NoContent")
+ end
+ end
+ end
+
+ describe '#resolved_errors' do
+ subject { comparer.resolved_errors }
+
+ context 'when base report has errors and head has more errors' do
+ before do
+ base_reports.add_url(url, single_error)
+ head_reports.add_url(url, single_error + different_error)
+ end
+
+ it 'returns an empty array' do
+ expect(subject).to be_empty
+ end
+ end
+
+ context 'when base reports has errors and head has a different error' do
+ before do
+ base_reports.add_url(url, single_error)
+ head_reports.add_url(url, different_error)
+ end
+
+ it 'returns the resolved errors' do
+ expect(subject.size).to eq(1)
+ expect(subject.first["code"]).to eq("WCAG2AA.Principle4.Guideline4_1.4_1_2.H91.A.NoContent")
+ end
+ end
+
+ context 'when base reports does not have errors and head has errors' do
+ before do
+ head_reports.add_url(url, single_error)
+ end
+
+ it 'returns an empty array' do
+ expect(subject).to be_empty
+ end
+ end
+ end
+end
diff --git a/spec/lib/gitlab/ci/reports/accessibility_reports_spec.rb b/spec/lib/gitlab/ci/reports/accessibility_reports_spec.rb
new file mode 100644
index 00000000000..0dc13b464b1
--- /dev/null
+++ b/spec/lib/gitlab/ci/reports/accessibility_reports_spec.rb
@@ -0,0 +1,232 @@
+# frozen_string_literal: true
+
+require 'spec_helper'
+
+describe Gitlab::Ci::Reports::AccessibilityReports do
+ let(:accessibility_report) { described_class.new }
+ let(:url) { 'https://gitlab.com' }
+ let(:data) do
+ [
+ {
+ "code": "WCAG2AA.Principle4.Guideline4_1.4_1_2.H91.A.NoContent",
+ "type": "error",
+ "typeCode": 1,
+ "message": "Anchor element found with a valid href attribute, but no link content has been supplied.",
+ "context": %{<a href="/customers/worldline"><svg viewBox="0 0 509 89" xmln...</a>},
+ "selector": "html > body > div:nth-child(9) > div:nth-child(2) > a:nth-child(17)",
+ "runner": "htmlcs",
+ "runnerExtras": {}
+ },
+ {
+ "code": "WCAG2AA.Principle4.Guideline4_1.4_1_2.H91.A.NoContent",
+ "type": "error",
+ "typeCode": 1,
+ "message": "Anchor element found with a valid href attribute, but no link content has been supplied.",
+ "context": %{<a href="/customers/equinix"><svg xmlns="http://www.w3.org/...</a>},
+ "selector": "html > body > div:nth-child(9) > div:nth-child(2) > a:nth-child(18)",
+ "runner": "htmlcs",
+ "runnerExtras": {}
+ }
+ ]
+ end
+
+ describe '#scans_count' do
+ subject { accessibility_report.scans_count }
+
+ context 'when data has errors' do
+ let(:different_url) { 'https://about.gitlab.com' }
+
+ before do
+ accessibility_report.add_url(url, data)
+ accessibility_report.add_url(different_url, data)
+ end
+
+ it 'returns the scans_count' do
+ expect(subject).to eq(2)
+ end
+ end
+
+ context 'when data has no errors' do
+ before do
+ accessibility_report.add_url(url, [])
+ end
+
+ it 'returns the scans_count' do
+ expect(subject).to eq(1)
+ end
+ end
+
+ context 'when data has no url' do
+ before do
+ accessibility_report.add_url("", [])
+ end
+
+ it 'returns the scans_count' do
+ expect(subject).to eq(0)
+ end
+ end
+ end
+
+ describe '#passes_count' do
+ subject { accessibility_report.passes_count }
+
+ context 'when data has errors' do
+ before do
+ accessibility_report.add_url(url, data)
+ end
+
+ it 'returns the passes_count' do
+ expect(subject).to eq(0)
+ end
+ end
+
+ context 'when data has no errors' do
+ before do
+ accessibility_report.add_url(url, [])
+ end
+
+ it 'returns the passes_count' do
+ expect(subject).to eq(1)
+ end
+ end
+
+ context 'when data has no url' do
+ before do
+ accessibility_report.add_url("", [])
+ end
+
+ it 'returns the scans_count' do
+ expect(subject).to eq(0)
+ end
+ end
+ end
+
+ describe '#errors_count' do
+ subject { accessibility_report.errors_count }
+
+ context 'when data has errors' do
+ let(:different_url) { 'https://about.gitlab.com' }
+
+ before do
+ accessibility_report.add_url(url, data)
+ accessibility_report.add_url(different_url, data)
+ end
+
+ it 'returns the errors_count' do
+ expect(subject).to eq(4)
+ end
+ end
+
+ context 'when data has no errors' do
+ before do
+ accessibility_report.add_url(url, [])
+ end
+
+ it 'returns the errors_count' do
+ expect(subject).to eq(0)
+ end
+ end
+
+ context 'when data has no url' do
+ before do
+ accessibility_report.add_url("", [])
+ end
+
+ it 'returns the errors_count' do
+ expect(subject).to eq(0)
+ end
+ end
+ end
+
+ describe '#add_url' do
+ subject { accessibility_report.add_url(url, data) }
+
+ context 'when data has errors' do
+ it 'adds urls and data to accessibility report' do
+ expect { subject }.not_to raise_error
+
+ expect(accessibility_report.urls.keys).to eq([url])
+ expect(accessibility_report.urls.values.flatten.size).to eq(2)
+ end
+ end
+
+ context 'when data does not have errors' do
+ let(:data) { [] }
+
+ it 'adds data to accessibility report' do
+ expect { subject }.not_to raise_error
+
+ expect(accessibility_report.urls.keys).to eq([url])
+ expect(accessibility_report.urls.values.flatten.size).to eq(0)
+ end
+ end
+
+ context 'when url does not exist' do
+ let(:url) { '' }
+ let(:data) { [{ message: "Protocol error (Page.navigate): Cannot navigate to invalid URL" }] }
+
+ it 'sets error_message and decreases total' do
+ expect { subject }.not_to raise_error
+
+ expect(accessibility_report.scans_count).to eq(0)
+ expect(accessibility_report.error_message).to eq('Empty URL detected in gl-accessibility.json')
+ end
+ end
+ end
+
+ describe '#set_error_message' do
+ let(:set_accessibility_error) { accessibility_report.set_error_message('error') }
+
+ context 'when error is nil' do
+ it 'returns the error' do
+ expect(set_accessibility_error).to eq('error')
+ end
+
+ it 'sets the error' do
+ set_accessibility_error
+
+ expect(accessibility_report.error_message).to eq('error')
+ end
+ end
+
+ context 'when a error has already been set' do
+ before do
+ accessibility_report.set_error_message('old error')
+ end
+
+ it 'overwrites the existing message' do
+ expect { set_accessibility_error }.to change(accessibility_report, :error_message).from('old error').to('error')
+ end
+ end
+ end
+
+ describe '#all_errors' do
+ subject { accessibility_report.all_errors }
+
+ context 'when data has errors' do
+ before do
+ accessibility_report.add_url(url, data)
+ end
+
+ it 'returns all errors' do
+ expect(subject.size).to eq(2)
+ end
+ end
+
+ context 'when data has no errors' do
+ before do
+ accessibility_report.add_url(url, [])
+ end
+
+ it 'returns an empty array' do
+ expect(subject).to eq([])
+ end
+ end
+
+ context 'when accessibility report has no data' do
+ it 'returns an empty array' do
+ expect(subject).to eq([])
+ end
+ end
+ end
+end
diff --git a/spec/lib/gitlab/ci/reports/terraform_reports_spec.rb b/spec/lib/gitlab/ci/reports/terraform_reports_spec.rb
new file mode 100644
index 00000000000..061029299ac
--- /dev/null
+++ b/spec/lib/gitlab/ci/reports/terraform_reports_spec.rb
@@ -0,0 +1,34 @@
+# frozen_string_literal: true
+
+require 'spec_helper'
+
+describe Gitlab::Ci::Reports::TerraformReports do
+ it 'initializes plans with and empty hash' do
+ expect(subject.plans).to eq({})
+ end
+
+ describe '#add_plan' do
+ context 'when providing two unique plans' do
+ it 'returns two plans' do
+ subject.add_plan('a/tfplan.json', { 'create' => 0, 'update' => 1, 'delete' => 0 })
+ subject.add_plan('b/tfplan.json', { 'create' => 0, 'update' => 1, 'delete' => 0 })
+
+ expect(subject.plans).to eq({
+ 'a/tfplan.json' => { 'create' => 0, 'update' => 1, 'delete' => 0 },
+ 'b/tfplan.json' => { 'create' => 0, 'update' => 1, 'delete' => 0 }
+ })
+ end
+ end
+
+ context 'when providing the same plan twice' do
+ it 'returns the last added plan' do
+ subject.add_plan('tfplan.json', { 'create' => 0, 'update' => 0, 'delete' => 0 })
+ subject.add_plan('tfplan.json', { 'create' => 0, 'update' => 1, 'delete' => 0 })
+
+ expect(subject.plans).to eq({
+ 'tfplan.json' => { 'create' => 0, 'update' => 1, 'delete' => 0 }
+ })
+ end
+ end
+ end
+end
diff --git a/spec/lib/gitlab/ci/reports/test_case_spec.rb b/spec/lib/gitlab/ci/reports/test_case_spec.rb
index c0652288cca..b5883867983 100644
--- a/spec/lib/gitlab/ci/reports/test_case_spec.rb
+++ b/spec/lib/gitlab/ci/reports/test_case_spec.rb
@@ -62,7 +62,7 @@ describe Gitlab::Ci::Reports::TestCase do
end
context 'when attachment is present' do
- let(:attachment_test_case) { build(:test_case, :with_attachment) }
+ let(:attachment_test_case) { build(:test_case, :failed_with_attachment) }
it "initializes the attachment if present" do
expect(attachment_test_case.attachment).to eq("some/path.png")
diff --git a/spec/lib/gitlab/ci/reports/test_reports_spec.rb b/spec/lib/gitlab/ci/reports/test_reports_spec.rb
index 638acde69eb..e51728496e1 100644
--- a/spec/lib/gitlab/ci/reports/test_reports_spec.rb
+++ b/spec/lib/gitlab/ci/reports/test_reports_spec.rb
@@ -127,7 +127,7 @@ describe Gitlab::Ci::Reports::TestReports do
context 'when test suites contain an attachment' do
let(:test_case_succes) { build(:test_case) }
- let(:test_case_with_attachment) { build(:test_case, :with_attachment) }
+ let(:test_case_with_attachment) { build(:test_case, :failed_with_attachment) }
before do
test_reports.get_suite('rspec').add_test_case(test_case_succes)
@@ -141,6 +141,29 @@ describe Gitlab::Ci::Reports::TestReports do
end
end
+ describe '#suite_errors' do
+ subject { test_reports.suite_errors }
+
+ context 'when a suite has normal spec errors or failures' do
+ before do
+ test_reports.get_suite('junit').add_test_case(create_test_case_java_success)
+ test_reports.get_suite('junit').add_test_case(create_test_case_java_failed)
+ test_reports.get_suite('junit').add_test_case(create_test_case_java_error)
+ end
+
+ it { is_expected.to be_empty }
+ end
+
+ context 'when there is an error test case' do
+ before do
+ test_reports.get_suite('rspec').add_test_case(create_test_case_rspec_success)
+ test_reports.get_suite('junit').set_suite_error('Existential parsing error')
+ end
+
+ it { is_expected.to eq({ 'junit' => 'Existential parsing error' }) }
+ end
+ end
+
Gitlab::Ci::Reports::TestCase::STATUS_TYPES.each do |status_type|
describe "##{status_type}_count" do
subject { test_reports.public_send("#{status_type}_count") }
diff --git a/spec/lib/gitlab/ci/reports/test_suite_spec.rb b/spec/lib/gitlab/ci/reports/test_suite_spec.rb
index 9d9774afc82..e0b2593353a 100644
--- a/spec/lib/gitlab/ci/reports/test_suite_spec.rb
+++ b/spec/lib/gitlab/ci/reports/test_suite_spec.rb
@@ -101,7 +101,7 @@ describe Gitlab::Ci::Reports::TestSuite do
end
context 'when test cases contain an attachment' do
- let(:test_case_with_attachment) { build(:test_case, :with_attachment)}
+ let(:test_case_with_attachment) { build(:test_case, :failed_with_attachment)}
before do
test_suite.add_test_case(test_case_with_attachment)
@@ -114,6 +114,31 @@ describe Gitlab::Ci::Reports::TestSuite do
end
end
+ describe '#set_suite_error' do
+ let(:set_suite_error) { test_suite.set_suite_error('message') }
+
+ context 'when @suite_error is nil' do
+ it 'returns message' do
+ expect(set_suite_error).to eq('message')
+ end
+
+ it 'sets the new message' do
+ set_suite_error
+ expect(test_suite.suite_error).to eq('message')
+ end
+ end
+
+ context 'when a suite_error has already been set' do
+ before do
+ test_suite.set_suite_error('old message')
+ end
+
+ it 'overwrites the existing message' do
+ expect { set_suite_error }.to change(test_suite, :suite_error).from('old message').to('message')
+ end
+ end
+ end
+
Gitlab::Ci::Reports::TestCase::STATUS_TYPES.each do |status_type|
describe "##{status_type}" do
subject { test_suite.public_send("#{status_type}") }
diff --git a/spec/lib/gitlab/ci/templates/Jobs/browser_performance_testing_gitlab_ci_yaml_spec.rb b/spec/lib/gitlab/ci/templates/Jobs/browser_performance_testing_gitlab_ci_yaml_spec.rb
new file mode 100644
index 00000000000..54c3500b0a0
--- /dev/null
+++ b/spec/lib/gitlab/ci/templates/Jobs/browser_performance_testing_gitlab_ci_yaml_spec.rb
@@ -0,0 +1,85 @@
+# frozen_string_literal: true
+
+require 'spec_helper'
+
+describe 'Jobs/Browser-Performance-Testing.gitlab-ci.yml' do
+ subject(:template) do
+ <<~YAML
+ stages:
+ - test
+ - performance
+
+ include:
+ - template: 'Jobs/Browser-Performance-Testing.gitlab-ci.yml'
+
+ placeholder:
+ script:
+ - keep pipeline validator happy by having a job when stages are intentionally empty
+ YAML
+ end
+
+ describe 'the created pipeline' do
+ let(:user) { create(:admin) }
+ let(:project) do
+ create(:project, :repository, variables: [
+ build(:ci_variable, key: 'CI_KUBERNETES_ACTIVE', value: 'true')
+ ])
+ end
+
+ let(:default_branch) { 'master' }
+ let(:pipeline_ref) { default_branch }
+ let(:service) { Ci::CreatePipelineService.new(project, user, ref: pipeline_ref) }
+ let(:pipeline) { service.execute!(:push) }
+ let(:build_names) { pipeline.builds.pluck(:name) }
+
+ before do
+ stub_ci_pipeline_yaml_file(template)
+
+ allow_any_instance_of(Ci::BuildScheduleWorker).to receive(:perform).and_return(true)
+ allow(project).to receive(:default_branch).and_return(default_branch)
+ end
+
+ it 'has no errors' do
+ expect(pipeline.errors).to be_empty
+ end
+
+ shared_examples_for 'performance job on tag or branch' do
+ it 'by default' do
+ expect(build_names).to include('performance')
+ end
+
+ it 'when PERFORMANCE_DISABLED' do
+ create(:ci_variable, project: project, key: 'PERFORMANCE_DISABLED', value: '1')
+
+ expect(build_names).not_to include('performance')
+ end
+ end
+
+ context 'on master' do
+ it_behaves_like 'performance job on tag or branch'
+ end
+
+ context 'on another branch' do
+ let(:pipeline_ref) { 'feature' }
+
+ it_behaves_like 'performance job on tag or branch'
+ end
+
+ context 'on tag' do
+ let(:pipeline_ref) { 'v1.0.0' }
+
+ it_behaves_like 'performance job on tag or branch'
+ end
+
+ context 'on merge request' do
+ let(:service) { MergeRequests::CreatePipelineService.new(project, user) }
+ let(:merge_request) { create(:merge_request, :simple, source_project: project) }
+ let(:pipeline) { service.execute(merge_request) }
+
+ it 'has no jobs' do
+ expect(pipeline).to be_merge_request_event
+ expect(build_names).to be_empty
+ end
+ end
+ end
+end
diff --git a/spec/lib/gitlab/ci/templates/Jobs/build_gitlab_ci_yaml_spec.rb b/spec/lib/gitlab/ci/templates/Jobs/build_gitlab_ci_yaml_spec.rb
new file mode 100644
index 00000000000..b2a9e3f5cf4
--- /dev/null
+++ b/spec/lib/gitlab/ci/templates/Jobs/build_gitlab_ci_yaml_spec.rb
@@ -0,0 +1,58 @@
+# frozen_string_literal: true
+
+require 'spec_helper'
+
+describe 'Jobs/Build.gitlab-ci.yml' do
+ subject(:template) { Gitlab::Template::GitlabCiYmlTemplate.find('Jobs/Build') }
+
+ describe 'the created pipeline' do
+ let_it_be(:user) { create(:admin) }
+ let_it_be(:project) { create(:project, :repository) }
+
+ let(:default_branch) { 'master' }
+ let(:pipeline_ref) { default_branch }
+ let(:service) { Ci::CreatePipelineService.new(project, user, ref: pipeline_ref) }
+ let(:pipeline) { service.execute!(:push) }
+ let(:build_names) { pipeline.builds.pluck(:name) }
+
+ before do
+ stub_ci_pipeline_yaml_file(template.content)
+ allow_any_instance_of(Ci::BuildScheduleWorker).to receive(:perform).and_return(true)
+ allow(project).to receive(:default_branch).and_return(default_branch)
+ end
+
+ context 'on master' do
+ it 'creates the build job' do
+ expect(build_names).to contain_exactly('build')
+ end
+ end
+
+ context 'on another branch' do
+ let(:pipeline_ref) { 'feature' }
+
+ it 'creates the build job' do
+ expect(build_names).to contain_exactly('build')
+ end
+ end
+
+ context 'on tag' do
+ let(:pipeline_ref) { 'v1.0.0' }
+
+ it 'creates the build job' do
+ expect(pipeline).to be_tag
+ expect(build_names).to contain_exactly('build')
+ end
+ end
+
+ context 'on merge request' do
+ let(:service) { MergeRequests::CreatePipelineService.new(project, user) }
+ let(:merge_request) { create(:merge_request, :simple, source_project: project) }
+ let(:pipeline) { service.execute(merge_request) }
+
+ it 'has no jobs' do
+ expect(pipeline).to be_merge_request_event
+ expect(build_names).to be_empty
+ end
+ end
+ end
+end
diff --git a/spec/lib/gitlab/ci/templates/Jobs/code_quality_gitlab_ci_yaml_spec.rb b/spec/lib/gitlab/ci/templates/Jobs/code_quality_gitlab_ci_yaml_spec.rb
new file mode 100644
index 00000000000..9c5b2fd5099
--- /dev/null
+++ b/spec/lib/gitlab/ci/templates/Jobs/code_quality_gitlab_ci_yaml_spec.rb
@@ -0,0 +1,86 @@
+# frozen_string_literal: true
+
+require 'spec_helper'
+
+describe 'Jobs/Code-Quality.gitlab-ci.yml' do
+ subject(:template) { Gitlab::Template::GitlabCiYmlTemplate.find('Jobs/Code-Quality') }
+
+ describe 'the created pipeline' do
+ let_it_be(:user) { create(:admin) }
+ let_it_be(:project) { create(:project, :repository) }
+
+ let(:default_branch) { 'master' }
+ let(:pipeline_ref) { default_branch }
+ let(:service) { Ci::CreatePipelineService.new(project, user, ref: pipeline_ref) }
+ let(:pipeline) { service.execute!(:push) }
+ let(:build_names) { pipeline.builds.pluck(:name) }
+
+ before do
+ stub_ci_pipeline_yaml_file(template.content)
+ allow_any_instance_of(Ci::BuildScheduleWorker).to receive(:perform).and_return(true)
+ allow(project).to receive(:default_branch).and_return(default_branch)
+ end
+
+ context 'on master' do
+ it 'creates the code_quality job' do
+ expect(build_names).to contain_exactly('code_quality')
+ end
+ end
+
+ context 'on another branch' do
+ let(:pipeline_ref) { 'feature' }
+
+ it 'creates the code_quality job' do
+ expect(build_names).to contain_exactly('code_quality')
+ end
+ end
+
+ context 'on tag' do
+ let(:pipeline_ref) { 'v1.0.0' }
+
+ it 'creates the code_quality job' do
+ expect(pipeline).to be_tag
+ expect(build_names).to contain_exactly('code_quality')
+ end
+ end
+
+ context 'on merge request' do
+ let(:service) { MergeRequests::CreatePipelineService.new(project, user) }
+ let(:merge_request) { create(:merge_request, :simple, source_project: project) }
+ let(:pipeline) { service.execute(merge_request) }
+
+ it 'has no jobs' do
+ expect(pipeline).to be_merge_request_event
+ expect(build_names).to be_empty
+ end
+ end
+
+ context 'CODE_QUALITY_DISABLED is set' do
+ before do
+ create(:ci_variable, key: 'CODE_QUALITY_DISABLED', value: 'true', project: project)
+ end
+
+ context 'on master' do
+ it 'has no jobs' do
+ expect { pipeline }.to raise_error(Ci::CreatePipelineService::CreateError)
+ end
+ end
+
+ context 'on another branch' do
+ let(:pipeline_ref) { 'feature' }
+
+ it 'has no jobs' do
+ expect { pipeline }.to raise_error(Ci::CreatePipelineService::CreateError)
+ end
+ end
+
+ context 'on tag' do
+ let(:pipeline_ref) { 'v1.0.0' }
+
+ it 'has no jobs' do
+ expect { pipeline }.to raise_error(Ci::CreatePipelineService::CreateError)
+ end
+ end
+ end
+ end
+end
diff --git a/spec/lib/gitlab/ci/templates/Jobs/deploy_gitlab_ci_yaml_spec.rb b/spec/lib/gitlab/ci/templates/Jobs/deploy_gitlab_ci_yaml_spec.rb
new file mode 100644
index 00000000000..a6ae23c85d3
--- /dev/null
+++ b/spec/lib/gitlab/ci/templates/Jobs/deploy_gitlab_ci_yaml_spec.rb
@@ -0,0 +1,222 @@
+# frozen_string_literal: true
+
+require 'spec_helper'
+
+describe 'Jobs/Deploy.gitlab-ci.yml' do
+ subject(:template) do
+ <<~YAML
+ stages:
+ - test
+ - review
+ - staging
+ - canary
+ - production
+ - incremental rollout 10%
+ - incremental rollout 25%
+ - incremental rollout 50%
+ - incremental rollout 100%
+ - cleanup
+
+ include:
+ - template: Jobs/Deploy.gitlab-ci.yml
+
+ placeholder:
+ script:
+ - echo "Ensure at least one job to keep pipeline validator happy"
+ YAML
+ end
+
+ describe 'the created pipeline' do
+ let(:user) { create(:admin) }
+ let(:project) { create(:project, :repository) }
+
+ let(:default_branch) { 'master' }
+ let(:pipeline_ref) { default_branch }
+ let(:service) { Ci::CreatePipelineService.new(project, user, ref: pipeline_ref) }
+ let(:pipeline) { service.execute!(:push) }
+ let(:build_names) { pipeline.builds.pluck(:name) }
+
+ before do
+ stub_ci_pipeline_yaml_file(template)
+
+ allow_any_instance_of(Ci::BuildScheduleWorker).to receive(:perform).and_return(true)
+ allow(project).to receive(:default_branch).and_return(default_branch)
+ end
+
+ context 'with no cluster' do
+ it 'does not create any kubernetes deployment jobs' do
+ expect(build_names).to eq %w(placeholder)
+ end
+ end
+
+ context 'with only a disabled cluster' do
+ let!(:cluster) { create(:cluster, :project, :provided_by_gcp, enabled: false, projects: [project]) }
+
+ it 'does not create any kubernetes deployment jobs' do
+ expect(build_names).to eq %w(placeholder)
+ end
+ end
+
+ context 'with an active cluster' do
+ let!(:cluster) { create(:cluster, :project, :provided_by_gcp, projects: [project]) }
+
+ context 'on master' do
+ it 'by default' do
+ expect(build_names).to include('production')
+ expect(build_names).not_to include('review')
+ end
+
+ it 'when CANARY_ENABLED' do
+ create(:ci_variable, project: project, key: 'CANARY_ENABLED', value: 'true')
+
+ expect(build_names).to include('production_manual')
+ expect(build_names).to include('canary')
+ expect(build_names).not_to include('production')
+ end
+
+ it 'when STAGING_ENABLED' do
+ create(:ci_variable, project: project, key: 'STAGING_ENABLED', value: 'true')
+
+ expect(build_names).to include('production_manual')
+ expect(build_names).to include('staging')
+ expect(build_names).not_to include('production')
+ end
+
+ it 'when INCREMENTAL_ROLLOUT_MODE == timed' do
+ create(:ci_variable, project: project, key: 'INCREMENTAL_ROLLOUT_ENABLED', value: 'true')
+ create(:ci_variable, project: project, key: 'INCREMENTAL_ROLLOUT_MODE', value: 'timed')
+
+ expect(build_names).not_to include('production_manual')
+ expect(build_names).not_to include('production')
+ expect(build_names).not_to include(
+ 'rollout 10%',
+ 'rollout 25%',
+ 'rollout 50%',
+ 'rollout 100%'
+ )
+ expect(build_names).to include(
+ 'timed rollout 10%',
+ 'timed rollout 25%',
+ 'timed rollout 50%',
+ 'timed rollout 100%'
+ )
+ end
+
+ it 'when INCREMENTAL_ROLLOUT_ENABLED' do
+ create(:ci_variable, project: project, key: 'INCREMENTAL_ROLLOUT_ENABLED', value: 'true')
+
+ expect(build_names).not_to include('production_manual')
+ expect(build_names).not_to include('production')
+ expect(build_names).not_to include(
+ 'timed rollout 10%',
+ 'timed rollout 25%',
+ 'timed rollout 50%',
+ 'timed rollout 100%'
+ )
+ expect(build_names).to include(
+ 'rollout 10%',
+ 'rollout 25%',
+ 'rollout 50%',
+ 'rollout 100%'
+ )
+ end
+
+ it 'when INCREMENTAL_ROLLOUT_MODE == manual' do
+ create(:ci_variable, project: project, key: 'INCREMENTAL_ROLLOUT_MODE', value: 'manual')
+
+ expect(build_names).not_to include('production_manual')
+ expect(build_names).not_to include('production')
+ expect(build_names).not_to include(
+ 'timed rollout 10%',
+ 'timed rollout 25%',
+ 'timed rollout 50%',
+ 'timed rollout 100%'
+ )
+ expect(build_names).to include(
+ 'rollout 10%',
+ 'rollout 25%',
+ 'rollout 50%',
+ 'rollout 100%'
+ )
+ end
+ end
+
+ shared_examples_for 'review app deployment' do
+ it 'creates the review and stop_review jobs but no production jobs' do
+ expect(build_names).to include('review')
+ expect(build_names).to include('stop_review')
+ expect(build_names).not_to include('production')
+ expect(build_names).not_to include('production_manual')
+ expect(build_names).not_to include('staging')
+ expect(build_names).not_to include('canary')
+ expect(build_names).not_to include('timed rollout 10%')
+ expect(build_names).not_to include('timed rollout 25%')
+ expect(build_names).not_to include('timed rollout 50%')
+ expect(build_names).not_to include('timed rollout 100%')
+ expect(build_names).not_to include('rollout 10%')
+ expect(build_names).not_to include('rollout 25%')
+ expect(build_names).not_to include('rollout 50%')
+ expect(build_names).not_to include('rollout 100%')
+ end
+
+ it 'does not include review when REVIEW_DISABLED' do
+ create(:ci_variable, project: project, key: 'REVIEW_DISABLED', value: 'true')
+
+ expect(build_names).not_to include('review')
+ expect(build_names).not_to include('stop_review')
+ end
+ end
+
+ context 'on branch' do
+ let(:pipeline_ref) { 'feature' }
+
+ before do
+ allow_any_instance_of(Gitlab::Ci::Pipeline::Chain::Validate::Repository).to receive(:perform!).and_return(true)
+ end
+
+ it_behaves_like 'review app deployment'
+
+ context 'when INCREMENTAL_ROLLOUT_ENABLED' do
+ before do
+ create(:ci_variable, project: project, key: 'INCREMENTAL_ROLLOUT_ENABLED', value: 'true')
+ end
+
+ it_behaves_like 'review app deployment'
+ end
+
+ context 'when INCREMENTAL_ROLLOUT_MODE == "timed"' do
+ before do
+ create(:ci_variable, project: project, key: 'INCREMENTAL_ROLLOUT_MODE', value: 'timed')
+ end
+
+ it_behaves_like 'review app deployment'
+ end
+
+ context 'when INCREMENTAL_ROLLOUT_MODE == "manual"' do
+ before do
+ create(:ci_variable, project: project, key: 'INCREMENTAL_ROLLOUT_MODE', value: 'manual')
+ end
+
+ it_behaves_like 'review app deployment'
+ end
+ end
+
+ context 'on tag' do
+ let(:pipeline_ref) { 'v1.0.0' }
+
+ it_behaves_like 'review app deployment'
+ end
+
+ context 'on merge request' do
+ let(:service) { MergeRequests::CreatePipelineService.new(project, user) }
+ let(:merge_request) { create(:merge_request, :simple, source_project: project) }
+ let(:pipeline) { service.execute(merge_request) }
+
+ it 'has no jobs' do
+ expect(pipeline).to be_merge_request_event
+ expect(build_names).to be_empty
+ end
+ end
+ end
+ end
+end
diff --git a/spec/lib/gitlab/ci/templates/Jobs/test_gitlab_ci_yaml_spec.rb b/spec/lib/gitlab/ci/templates/Jobs/test_gitlab_ci_yaml_spec.rb
new file mode 100644
index 00000000000..2186bf038eb
--- /dev/null
+++ b/spec/lib/gitlab/ci/templates/Jobs/test_gitlab_ci_yaml_spec.rb
@@ -0,0 +1,86 @@
+# frozen_string_literal: true
+
+require 'spec_helper'
+
+describe 'Jobs/Test.gitlab-ci.yml' do
+ subject(:template) { Gitlab::Template::GitlabCiYmlTemplate.find('Jobs/Test') }
+
+ describe 'the created pipeline' do
+ let_it_be(:user) { create(:admin) }
+ let_it_be(:project) { create(:project, :repository) }
+
+ let(:default_branch) { 'master' }
+ let(:pipeline_ref) { default_branch }
+ let(:service) { Ci::CreatePipelineService.new(project, user, ref: pipeline_ref) }
+ let(:pipeline) { service.execute!(:push) }
+ let(:build_names) { pipeline.builds.pluck(:name) }
+
+ before do
+ stub_ci_pipeline_yaml_file(template.content)
+ allow_any_instance_of(Ci::BuildScheduleWorker).to receive(:perform).and_return(true)
+ allow(project).to receive(:default_branch).and_return(default_branch)
+ end
+
+ context 'on master' do
+ it 'creates the test job' do
+ expect(build_names).to contain_exactly('test')
+ end
+ end
+
+ context 'on another branch' do
+ let(:pipeline_ref) { 'feature' }
+
+ it 'creates the test job' do
+ expect(build_names).to contain_exactly('test')
+ end
+ end
+
+ context 'on tag' do
+ let(:pipeline_ref) { 'v1.0.0' }
+
+ it 'creates the test job' do
+ expect(pipeline).to be_tag
+ expect(build_names).to contain_exactly('test')
+ end
+ end
+
+ context 'on merge request' do
+ let(:service) { MergeRequests::CreatePipelineService.new(project, user) }
+ let(:merge_request) { create(:merge_request, :simple, source_project: project) }
+ let(:pipeline) { service.execute(merge_request) }
+
+ it 'has no jobs' do
+ expect(pipeline).to be_merge_request_event
+ expect(build_names).to be_empty
+ end
+ end
+
+ context 'TEST_DISABLED is set' do
+ before do
+ create(:ci_variable, key: 'TEST_DISABLED', value: 'true', project: project)
+ end
+
+ context 'on master' do
+ it 'has no jobs' do
+ expect { pipeline }.to raise_error(Ci::CreatePipelineService::CreateError)
+ end
+ end
+
+ context 'on another branch' do
+ let(:pipeline_ref) { 'feature' }
+
+ it 'has no jobs' do
+ expect { pipeline }.to raise_error(Ci::CreatePipelineService::CreateError)
+ end
+ end
+
+ context 'on tag' do
+ let(:pipeline_ref) { 'v1.0.0' }
+
+ it 'has no jobs' do
+ expect { pipeline }.to raise_error(Ci::CreatePipelineService::CreateError)
+ end
+ end
+ end
+ end
+end
diff --git a/spec/lib/gitlab/ci/templates/auto_devops_gitlab_ci_yaml_spec.rb b/spec/lib/gitlab/ci/templates/auto_devops_gitlab_ci_yaml_spec.rb
index 0c5d172f17c..af6ec25b9d6 100644
--- a/spec/lib/gitlab/ci/templates/auto_devops_gitlab_ci_yaml_spec.rb
+++ b/spec/lib/gitlab/ci/templates/auto_devops_gitlab_ci_yaml_spec.rb
@@ -20,16 +20,8 @@ describe 'Auto-DevOps.gitlab-ci.yml' do
allow(project).to receive(:default_branch).and_return(default_branch)
end
- it 'creates a build and a test job' do
- expect(build_names).to include('build', 'test')
- end
-
- context 'when the project has no active cluster' do
- it 'only creates a build and a test stage' do
- expect(pipeline.stages_names).to eq(%w(build test))
- end
-
- it 'does not create any deployment-related builds' do
+ shared_examples 'no Kubernetes deployment job' do
+ it 'does not create any Kubernetes deployment-related builds' do
expect(build_names).not_to include('production')
expect(build_names).not_to include('production_manual')
expect(build_names).not_to include('staging')
@@ -39,13 +31,95 @@ describe 'Auto-DevOps.gitlab-ci.yml' do
end
end
- context 'when the project has an active cluster' do
- let(:cluster) { create(:cluster, :project, :provided_by_gcp, projects: [project]) }
+ it 'creates a build and a test job' do
+ expect(build_names).to include('build', 'test')
+ end
+
+ context 'when the project is set for deployment to AWS' do
+ let(:platform_value) { 'ECS' }
before do
- allow(cluster).to receive(:active?).and_return(true)
+ create(:ci_variable, project: project, key: 'AUTO_DEVOPS_PLATFORM_TARGET', value: platform_value)
+ end
+
+ shared_examples 'no ECS job when AUTO_DEVOPS_PLATFORM_TARGET is not present' do |job_name|
+ context 'when AUTO_DEVOPS_PLATFORM_TARGET is nil' do
+ let(:platform_value) { nil }
+
+ it 'does not trigger the job' do
+ expect(build_names).not_to include(job_name)
+ end
+ end
+
+ context 'when AUTO_DEVOPS_PLATFORM_TARGET is empty' do
+ let(:platform_value) { '' }
+
+ it 'does not trigger the job' do
+ expect(build_names).not_to include(job_name)
+ end
+ end
+ end
+
+ it_behaves_like 'no Kubernetes deployment job'
+
+ it_behaves_like 'no ECS job when AUTO_DEVOPS_PLATFORM_TARGET is not present' do
+ let(:job_name) { 'production_ecs' }
+ end
+
+ it 'creates an ECS deployment job for production only' do
+ expect(build_names).not_to include('review_ecs')
+ expect(build_names).to include('production_ecs')
end
+ context 'and we are not on the default branch' do
+ let(:platform_value) { 'ECS' }
+ let(:pipeline_branch) { 'patch-1' }
+
+ before do
+ project.repository.create_branch(pipeline_branch)
+ end
+
+ it_behaves_like 'no ECS job when AUTO_DEVOPS_PLATFORM_TARGET is not present' do
+ let(:job_name) { 'review_ecs' }
+ end
+
+ it 'creates an ECS deployment job for review only' do
+ expect(build_names).to include('review_ecs')
+ expect(build_names).not_to include('production_ecs')
+ expect(build_names).not_to include('review')
+ expect(build_names).not_to include('production')
+ end
+ end
+
+ context 'and when the project has an active cluster' do
+ let(:cluster) { create(:cluster, :project, :provided_by_gcp, projects: [project]) }
+
+ before do
+ allow(cluster).to receive(:active?).and_return(true)
+ end
+
+ context 'on default branch' do
+ it 'triggers the deployment to Kubernetes, not to ECS' do
+ expect(build_names).not_to include('review')
+ expect(build_names).to include('production')
+ expect(build_names).not_to include('production_ecs')
+ expect(build_names).not_to include('review_ecs')
+ end
+ end
+ end
+ end
+
+ context 'when the project has no active cluster' do
+ it 'only creates a build and a test stage' do
+ expect(pipeline.stages_names).to eq(%w(build test))
+ end
+
+ it_behaves_like 'no Kubernetes deployment job'
+ end
+
+ context 'when the project has an active cluster' do
+ let!(:cluster) { create(:cluster, :project, :provided_by_gcp, projects: [project]) }
+
describe 'deployment-related builds' do
context 'on default branch' do
it 'does not include rollout jobs besides production' do
diff --git a/spec/lib/gitlab/ci/yaml_processor_spec.rb b/spec/lib/gitlab/ci/yaml_processor_spec.rb
index 70c3c5ab339..c93bb901981 100644
--- a/spec/lib/gitlab/ci/yaml_processor_spec.rb
+++ b/spec/lib/gitlab/ci/yaml_processor_spec.rb
@@ -1364,6 +1364,24 @@ module Gitlab
expect { described_class.new(config) }.to raise_error(described_class::ValidationError)
end
+
+ it 'populates a build options with complete artifacts configuration' do
+ stub_feature_flags(ci_artifacts_exclude: true)
+
+ config = <<~YAML
+ test:
+ script: echo "Hello World"
+ artifacts:
+ paths:
+ - my/test
+ exclude:
+ - my/test/something
+ YAML
+
+ attributes = Gitlab::Ci::YamlProcessor.new(config).build_attributes('test')
+
+ expect(attributes.dig(*%i[options artifacts exclude])).to eq(%w[my/test/something])
+ end
end
describe "release" do
@@ -2264,14 +2282,14 @@ module Gitlab
config = YAML.dump({ rspec: { script: "test", type: "acceptance" } })
expect do
Gitlab::Ci::YamlProcessor.new(config)
- end.to raise_error(Gitlab::Ci::YamlProcessor::ValidationError, "rspec job: stage parameter should be .pre, build, test, deploy, .post")
+ end.to raise_error(Gitlab::Ci::YamlProcessor::ValidationError, "rspec job: chosen stage does not exist; available stages are .pre, build, test, deploy, .post")
end
it "returns errors if job stage is not a defined stage" do
config = YAML.dump({ types: %w(build test), rspec: { script: "test", type: "acceptance" } })
expect do
Gitlab::Ci::YamlProcessor.new(config)
- end.to raise_error(Gitlab::Ci::YamlProcessor::ValidationError, "rspec job: stage parameter should be .pre, build, test, .post")
+ end.to raise_error(Gitlab::Ci::YamlProcessor::ValidationError, "rspec job: chosen stage does not exist; available stages are .pre, build, test, .post")
end
it "returns errors if stages is not an array" do
diff --git a/spec/lib/gitlab/code_navigation_path_spec.rb b/spec/lib/gitlab/code_navigation_path_spec.rb
index cafe362c8c7..938a2f821fd 100644
--- a/spec/lib/gitlab/code_navigation_path_spec.rb
+++ b/spec/lib/gitlab/code_navigation_path_spec.rb
@@ -4,18 +4,29 @@ require 'spec_helper'
describe Gitlab::CodeNavigationPath do
context 'when there is an artifact with code navigation data' do
- let(:project) { create(:project, :repository) }
- let(:sha) { project.commit.id }
- let(:build_name) { Gitlab::CodeNavigationPath::CODE_NAVIGATION_JOB_NAME }
+ let_it_be(:project) { create(:project, :repository) }
+ let_it_be(:sha) { project.repository.commits('master', limit: 5).last.id }
+ let_it_be(:pipeline) { create(:ci_pipeline, project: project, sha: sha) }
+ let_it_be(:job) { create(:ci_build, pipeline: pipeline) }
+ let_it_be(:artifact) { create(:ci_job_artifact, :lsif, job: job) }
+
+ let(:commit_sha) { sha }
let(:path) { 'lib/app.rb' }
- let!(:pipeline) { create(:ci_pipeline, project: project, sha: sha) }
- let!(:job) { create(:ci_build, pipeline: pipeline, name: build_name) }
- let!(:artifact) { create(:ci_job_artifact, :lsif, job: job) }
- subject { described_class.new(project, sha).full_json_path_for(path) }
+ subject { described_class.new(project, commit_sha).full_json_path_for(path) }
+
+ context 'when a pipeline exist for a sha' do
+ it 'returns path to a file in the artifact' do
+ expect(subject).to eq("/#{project.full_path}/-/jobs/#{job.id}/artifacts/raw/lsif/#{path}.json?file_type=lsif")
+ end
+ end
+
+ context 'when a pipeline exist for the latest commits' do
+ let(:commit_sha) { project.commit.id }
- it 'assigns code_navigation_build variable' do
- expect(subject).to eq("/#{project.full_path}/-/jobs/#{job.id}/artifacts/raw/lsif/#{path}.json")
+ it 'returns path to a file in the artifact' do
+ expect(subject).to eq("/#{project.full_path}/-/jobs/#{job.id}/artifacts/raw/lsif/#{path}.json?file_type=lsif")
+ end
end
context 'when code_navigation feature is disabled' do
@@ -23,7 +34,7 @@ describe Gitlab::CodeNavigationPath do
stub_feature_flags(code_navigation: false)
end
- it 'does not assign code_navigation_build variable' do
+ it 'returns nil' do
expect(subject).to be_nil
end
end
diff --git a/spec/lib/gitlab/config_checker/external_database_checker_spec.rb b/spec/lib/gitlab/config_checker/external_database_checker_spec.rb
new file mode 100644
index 00000000000..d86d132c237
--- /dev/null
+++ b/spec/lib/gitlab/config_checker/external_database_checker_spec.rb
@@ -0,0 +1,56 @@
+# frozen_string_literal: true
+
+require 'spec_helper'
+
+describe Gitlab::ConfigChecker::ExternalDatabaseChecker do
+ describe '#check' do
+ subject { described_class.check }
+
+ context 'database version is not deprecated' do
+ before do
+ allow(described_class).to receive(:db_version_deprecated?).and_return(false)
+ end
+
+ it { is_expected.to be_empty }
+ end
+
+ context 'database version is deprecated' do
+ before do
+ allow(described_class).to receive(:db_version_deprecated?).and_return(true)
+ end
+
+ let(:notice_deprecated_database) do
+ {
+ type: 'warning',
+ message: _('Note that PostgreSQL 11 will become the minimum required PostgreSQL version in GitLab 13.0 (May 2020). '\
+ 'PostgreSQL 9.6 and PostgreSQL 10 will no longer be supported in GitLab 13.0. '\
+ 'Please consider upgrading your PostgreSQL version (%{db_version}) soon.') % { db_version: Gitlab::Database.version.to_s }
+ }
+ end
+
+ it 'reports deprecated database notices' do
+ is_expected.to contain_exactly(notice_deprecated_database)
+ end
+ end
+ end
+
+ describe '#db_version_deprecated' do
+ subject { described_class.db_version_deprecated? }
+
+ context 'database version is not deprecated' do
+ before do
+ allow(Gitlab::Database).to receive(:version).and_return(11)
+ end
+
+ it { is_expected.to be false }
+ end
+
+ context 'database version is deprecated' do
+ before do
+ allow(Gitlab::Database).to receive(:version).and_return(10)
+ end
+
+ it { is_expected.to be true }
+ end
+ end
+end
diff --git a/spec/lib/gitlab/cycle_analytics/group_stage_summary_spec.rb b/spec/lib/gitlab/cycle_analytics/group_stage_summary_spec.rb
deleted file mode 100644
index 2242895f8ea..00000000000
--- a/spec/lib/gitlab/cycle_analytics/group_stage_summary_spec.rb
+++ /dev/null
@@ -1,176 +0,0 @@
-# frozen_string_literal: true
-require 'spec_helper'
-
-describe Gitlab::CycleAnalytics::GroupStageSummary do
- let(:group) { create(:group) }
- let(:project) { create(:project, :repository, namespace: group) }
- let(:project_2) { create(:project, :repository, namespace: group) }
- let(:from) { 1.day.ago }
- let(:user) { create(:user, :admin) }
-
- subject { described_class.new(group, options: { from: Time.now, current_user: user }).data }
-
- describe "#new_issues" do
- context 'with from date' do
- before do
- Timecop.freeze(5.days.ago) { create(:issue, project: project) }
- Timecop.freeze(5.days.ago) { create(:issue, project: project_2) }
- Timecop.freeze(5.days.from_now) { create(:issue, project: project) }
- Timecop.freeze(5.days.from_now) { create(:issue, project: project_2) }
- end
-
- it "finds the number of issues created after it" do
- expect(subject.first[:value]).to eq('2')
- end
-
- context 'with subgroups' do
- before do
- Timecop.freeze(5.days.from_now) { create(:issue, project: create(:project, namespace: create(:group, parent: group))) }
- end
-
- it "finds issues from them" do
- expect(subject.first[:value]).to eq('3')
- end
- end
-
- context 'with projects specified in options' do
- before do
- Timecop.freeze(5.days.from_now) { create(:issue, project: create(:project, namespace: group)) }
- end
-
- subject { described_class.new(group, options: { from: Time.now, current_user: user, projects: [project.id, project_2.id] }).data }
-
- it 'finds issues from those projects' do
- expect(subject.first[:value]).to eq('2')
- end
- end
-
- context 'when `from` and `to` parameters are provided' do
- subject { described_class.new(group, options: { from: 10.days.ago, to: Time.now, current_user: user }).data }
-
- it 'finds issues from 5 days ago' do
- expect(subject.first[:value]).to eq('2')
- end
- end
- end
-
- context 'with other projects' do
- before do
- Timecop.freeze(5.days.from_now) { create(:issue, project: create(:project, namespace: create(:group))) }
- Timecop.freeze(5.days.from_now) { create(:issue, project: project) }
- Timecop.freeze(5.days.from_now) { create(:issue, project: project_2) }
- end
-
- it "doesn't find issues from them" do
- expect(subject.first[:value]).to eq('2')
- end
- end
- end
-
- describe "#deploys" do
- context 'with from date' do
- before do
- Timecop.freeze(5.days.ago) { create(:deployment, :success, project: project) }
- Timecop.freeze(5.days.from_now) { create(:deployment, :success, project: project) }
- Timecop.freeze(5.days.ago) { create(:deployment, :success, project: project_2) }
- Timecop.freeze(5.days.from_now) { create(:deployment, :success, project: project_2) }
- end
-
- it "finds the number of deploys made created after it" do
- expect(subject.second[:value]).to eq('2')
- end
-
- context 'with subgroups' do
- before do
- Timecop.freeze(5.days.from_now) do
- create(:deployment, :success, project: create(:project, :repository, namespace: create(:group, parent: group)))
- end
- end
-
- it "finds deploys from them" do
- expect(subject.second[:value]).to eq('3')
- end
- end
-
- context 'with projects specified in options' do
- before do
- Timecop.freeze(5.days.from_now) do
- create(:deployment, :success, project: create(:project, :repository, namespace: group, name: 'not_applicable'))
- end
- end
-
- subject { described_class.new(group, options: { from: Time.now, current_user: user, projects: [project.id, project_2.id] }).data }
-
- it 'shows deploys from those projects' do
- expect(subject.second[:value]).to eq('2')
- end
- end
-
- context 'when `from` and `to` parameters are provided' do
- subject { described_class.new(group, options: { from: 10.days.ago, to: Time.now, current_user: user }).data }
-
- it 'finds deployments from 5 days ago' do
- expect(subject.second[:value]).to eq('2')
- end
- end
- end
-
- context 'with other projects' do
- before do
- Timecop.freeze(5.days.from_now) do
- create(:deployment, :success, project: create(:project, :repository, namespace: create(:group)))
- end
- end
-
- it "doesn't find deploys from them" do
- expect(subject.second[:value]).to eq('-')
- end
- end
- end
-
- describe '#deployment_frequency' do
- let(:from) { 6.days.ago }
- let(:to) { nil }
-
- subject do
- described_class.new(group, options: {
- from: from,
- to: to,
- current_user: user
- }).data.third
- end
-
- it 'includes the unit: `per day`' do
- expect(subject[:unit]).to eq(_('per day'))
- end
-
- before do
- Timecop.freeze(5.days.ago) do
- create(:deployment, :success, project: project)
- end
- end
-
- context 'when `to` is nil' do
- it 'includes range until now' do
- # 1 deployment over 7 days
- expect(subject[:value]).to eq('0.1')
- end
- end
-
- context 'when `to` is given' do
- let(:from) { 10.days.ago }
- let(:to) { 10.days.from_now }
-
- before do
- Timecop.freeze(5.days.from_now) do
- create(:deployment, :success, project: project)
- end
- end
-
- it 'returns deployment frequency within `from` and `to` range' do
- # 2 deployments over 20 days
- expect(subject[:value]).to eq('0.1')
- end
- end
- end
-end
diff --git a/spec/lib/gitlab/cycle_analytics/summary/value_spec.rb b/spec/lib/gitlab/cycle_analytics/summary/value_spec.rb
new file mode 100644
index 00000000000..d9bdfa92a04
--- /dev/null
+++ b/spec/lib/gitlab/cycle_analytics/summary/value_spec.rb
@@ -0,0 +1,33 @@
+# frozen_string_literal: true
+
+require 'spec_helper'
+
+describe Gitlab::CycleAnalytics::Summary::Value do
+ describe Gitlab::CycleAnalytics::Summary::Value::None do
+ it 'returns `-`' do
+ expect(described_class.new.to_s).to eq('-')
+ end
+ end
+
+ describe Gitlab::CycleAnalytics::Summary::Value::Numeric do
+ it 'returns the string representation of the number' do
+ expect(described_class.new(3.2).to_s).to eq('3.2')
+ end
+ end
+
+ describe Gitlab::CycleAnalytics::Summary::Value::PrettyNumeric do
+ describe '#to_s' do
+ it 'returns `-` when the number is 0' do
+ expect(described_class.new(0).to_s).to eq('-')
+ end
+
+ it 'returns `-` when the number is nil' do
+ expect(described_class.new(nil).to_s).to eq('-')
+ end
+
+ it 'returns the string representation of the number' do
+ expect(described_class.new(100).to_s).to eq('100')
+ end
+ end
+ end
+end
diff --git a/spec/lib/gitlab/danger/changelog_spec.rb b/spec/lib/gitlab/danger/changelog_spec.rb
index ba23c3828de..8929374fb87 100644
--- a/spec/lib/gitlab/danger/changelog_spec.rb
+++ b/spec/lib/gitlab/danger/changelog_spec.rb
@@ -86,14 +86,6 @@ describe Gitlab::Danger::Changelog do
end
end
- describe '#presented_no_changelog_labels' do
- subject { changelog.presented_no_changelog_labels }
-
- it 'returns the labels formatted' do
- is_expected.to eq('~backstage, ~ci-build, ~meta')
- end
- end
-
describe '#ee_changelog?' do
subject { changelog.ee_changelog? }
diff --git a/spec/lib/gitlab/danger/helper_spec.rb b/spec/lib/gitlab/danger/helper_spec.rb
index d5d582d7d6c..c2c881fd589 100644
--- a/spec/lib/gitlab/danger/helper_spec.rb
+++ b/spec/lib/gitlab/danger/helper_spec.rb
@@ -363,4 +363,69 @@ describe Gitlab::Danger::Helper do
expect(helper).to be_security_mr
end
end
+
+ describe '#mr_has_label?' do
+ it 'returns false when `gitlab_helper` is unavailable' do
+ expect(helper).to receive(:gitlab_helper).and_return(nil)
+
+ expect(helper.mr_has_labels?('telemetry')).to be_falsey
+ end
+
+ context 'when mr has labels' do
+ before do
+ mr_labels = ['telemetry', 'telemetry::reviewed']
+ expect(fake_gitlab).to receive(:mr_labels).and_return(mr_labels)
+ end
+
+ it 'returns true with a matched label' do
+ expect(helper.mr_has_labels?('telemetry')).to be_truthy
+ end
+
+ it 'returns false with unmatched label' do
+ expect(helper.mr_has_labels?('database')).to be_falsey
+ end
+
+ it 'returns true with an array of labels' do
+ expect(helper.mr_has_labels?(['telemetry', 'telemetry::reviewed'])).to be_truthy
+ end
+
+ it 'returns true with multi arguments with matched labels' do
+ expect(helper.mr_has_labels?('telemetry', 'telemetry::reviewed')).to be_truthy
+ end
+
+ it 'returns false with multi arguments with unmatched labels' do
+ expect(helper.mr_has_labels?('telemetry', 'telemetry::non existing')).to be_falsey
+ end
+ end
+ end
+
+ describe '#labels_list' do
+ let(:labels) { ['telemetry', 'telemetry::reviewed'] }
+
+ it 'composes the labels string' do
+ expect(helper.labels_list(labels)).to eq('~"telemetry", ~"telemetry::reviewed"')
+ end
+
+ context 'when passing a separator' do
+ it 'composes the labels string with the given separator' do
+ expect(helper.labels_list(labels, sep: ' ')).to eq('~"telemetry" ~"telemetry::reviewed"')
+ end
+ end
+
+ it 'returns empty string for empty array' do
+ expect(helper.labels_list([])).to eq('')
+ end
+ end
+
+ describe '#prepare_labels_for_mr' do
+ it 'composes the labels string' do
+ mr_labels = ['telemetry', 'telemetry::reviewed']
+
+ expect(helper.prepare_labels_for_mr(mr_labels)).to eq('/label ~"telemetry" ~"telemetry::reviewed"')
+ end
+
+ it 'returns empty string for empty array' do
+ expect(helper.prepare_labels_for_mr([])).to eq('')
+ end
+ end
end
diff --git a/spec/lib/gitlab/danger/teammate_spec.rb b/spec/lib/gitlab/danger/teammate_spec.rb
index 570f4bd27cc..ea5aecbc597 100644
--- a/spec/lib/gitlab/danger/teammate_spec.rb
+++ b/spec/lib/gitlab/danger/teammate_spec.rb
@@ -163,6 +163,13 @@ describe Gitlab::Danger::Teammate do
{ message: 'OOO: massage' } | false
{ message: 'love it SOOO much' } | false
{ emoji: 'red_circle' } | false
+ { emoji: 'palm_tree' } | false
+ { emoji: 'beach' } | false
+ { emoji: 'beach_umbrella' } | false
+ { emoji: 'beach_with_umbrella' } | false
+ { emoji: nil } | true
+ { emoji: '' } | true
+ { emoji: 'dancer' } | true
end
with_them do
@@ -175,9 +182,9 @@ describe Gitlab::Danger::Teammate do
end
it 'returns true if request fails' do
- expect(Gitlab::Danger::RequestHelper).to receive(:http_get_json)
- .twice
- .and_raise(Gitlab::Danger::RequestHelper::HTTPError.new)
+ expect(Gitlab::Danger::RequestHelper)
+ .to receive(:http_get_json)
+ .and_raise(Gitlab::Danger::RequestHelper::HTTPError.new)
expect(subject.available?).to be true
end
diff --git a/spec/lib/gitlab/database/batch_count_spec.rb b/spec/lib/gitlab/database/batch_count_spec.rb
index 7be84b8f980..e7cb53f2dbd 100644
--- a/spec/lib/gitlab/database/batch_count_spec.rb
+++ b/spec/lib/gitlab/database/batch_count_spec.rb
@@ -35,6 +35,10 @@ describe Gitlab::Database::BatchCount do
expect(described_class.batch_count(model, "#{model.table_name}.id")).to eq(5)
end
+ it 'counts with Arel column' do
+ expect(described_class.batch_count(model, model.arel_table[:id])).to eq(5)
+ end
+
it 'counts table with batch_size 50K' do
expect(described_class.batch_count(model, batch_size: 50_000)).to eq(5)
end
@@ -98,6 +102,10 @@ describe Gitlab::Database::BatchCount do
expect(described_class.batch_distinct_count(model, "#{model.table_name}.#{column}")).to eq(2)
end
+ it 'counts with Arel column' do
+ expect(described_class.batch_distinct_count(model, model.arel_table[column])).to eq(2)
+ end
+
it 'counts with :column field with batch_size of 50K' do
expect(described_class.batch_distinct_count(model, column, batch_size: 50_000)).to eq(2)
end
diff --git a/spec/lib/gitlab/database/migration_helpers_spec.rb b/spec/lib/gitlab/database/migration_helpers_spec.rb
index 3a0148615b9..203d39be22b 100644
--- a/spec/lib/gitlab/database/migration_helpers_spec.rb
+++ b/spec/lib/gitlab/database/migration_helpers_spec.rb
@@ -217,9 +217,10 @@ describe Gitlab::Database::MigrationHelpers do
it 'appends ON DELETE SET NULL statement' do
expect(model).to receive(:with_lock_retries).and_call_original
expect(model).to receive(:disable_statement_timeout).and_call_original
+ expect(model).to receive(:statement_timeout_disabled?).and_return(false)
expect(model).to receive(:execute).with(/statement_timeout/)
expect(model).to receive(:execute).ordered.with(/VALIDATE CONSTRAINT/)
- expect(model).to receive(:execute).with(/RESET ALL/)
+ expect(model).to receive(:execute).ordered.with(/RESET ALL/)
expect(model).to receive(:execute).with(/ON DELETE SET NULL/)
@@ -233,9 +234,10 @@ describe Gitlab::Database::MigrationHelpers do
it 'appends ON DELETE CASCADE statement' do
expect(model).to receive(:with_lock_retries).and_call_original
expect(model).to receive(:disable_statement_timeout).and_call_original
+ expect(model).to receive(:statement_timeout_disabled?).and_return(false)
expect(model).to receive(:execute).with(/statement_timeout/)
expect(model).to receive(:execute).ordered.with(/VALIDATE CONSTRAINT/)
- expect(model).to receive(:execute).with(/RESET ALL/)
+ expect(model).to receive(:execute).ordered.with(/RESET ALL/)
expect(model).to receive(:execute).with(/ON DELETE CASCADE/)
@@ -249,9 +251,10 @@ describe Gitlab::Database::MigrationHelpers do
it 'appends no ON DELETE statement' do
expect(model).to receive(:with_lock_retries).and_call_original
expect(model).to receive(:disable_statement_timeout).and_call_original
+ expect(model).to receive(:statement_timeout_disabled?).and_return(false)
expect(model).to receive(:execute).with(/statement_timeout/)
expect(model).to receive(:execute).ordered.with(/VALIDATE CONSTRAINT/)
- expect(model).to receive(:execute).with(/RESET ALL/)
+ expect(model).to receive(:execute).ordered.with(/RESET ALL/)
expect(model).not_to receive(:execute).with(/ON DELETE/)
@@ -266,10 +269,11 @@ describe Gitlab::Database::MigrationHelpers do
it 'creates a concurrent foreign key and validates it' do
expect(model).to receive(:with_lock_retries).and_call_original
expect(model).to receive(:disable_statement_timeout).and_call_original
+ expect(model).to receive(:statement_timeout_disabled?).and_return(false)
expect(model).to receive(:execute).with(/statement_timeout/)
expect(model).to receive(:execute).ordered.with(/NOT VALID/)
expect(model).to receive(:execute).ordered.with(/VALIDATE CONSTRAINT/)
- expect(model).to receive(:execute).with(/RESET ALL/)
+ expect(model).to receive(:execute).ordered.with(/RESET ALL/)
model.add_concurrent_foreign_key(:projects, :users, column: :user_id)
end
@@ -293,10 +297,11 @@ describe Gitlab::Database::MigrationHelpers do
it 'creates a new foreign key' do
expect(model).to receive(:with_lock_retries).and_call_original
expect(model).to receive(:disable_statement_timeout).and_call_original
+ expect(model).to receive(:statement_timeout_disabled?).and_return(false)
expect(model).to receive(:execute).with(/statement_timeout/)
expect(model).to receive(:execute).ordered.with(/NOT VALID/)
expect(model).to receive(:execute).ordered.with(/VALIDATE CONSTRAINT.+foo/)
- expect(model).to receive(:execute).with(/RESET ALL/)
+ expect(model).to receive(:execute).ordered.with(/RESET ALL/)
model.add_concurrent_foreign_key(:projects, :users, column: :user_id, name: :foo)
end
@@ -321,10 +326,11 @@ describe Gitlab::Database::MigrationHelpers do
it 'creates a new foreign key' do
expect(model).to receive(:with_lock_retries).and_call_original
expect(model).to receive(:disable_statement_timeout).and_call_original
+ expect(model).to receive(:statement_timeout_disabled?).and_return(false)
expect(model).to receive(:execute).with(/statement_timeout/)
expect(model).to receive(:execute).ordered.with(/NOT VALID/)
expect(model).to receive(:execute).ordered.with(/VALIDATE CONSTRAINT.+bar/)
- expect(model).to receive(:execute).with(/RESET ALL/)
+ expect(model).to receive(:execute).ordered.with(/RESET ALL/)
model.add_concurrent_foreign_key(:projects, :users, column: :user_id, name: :bar)
end
@@ -361,6 +367,7 @@ describe Gitlab::Database::MigrationHelpers do
aggregate_failures do
expect(model).not_to receive(:concurrent_foreign_key_name)
expect(model).to receive(:disable_statement_timeout).and_call_original
+ expect(model).to receive(:statement_timeout_disabled?).and_return(false)
expect(model).to receive(:execute).with(/statement_timeout/)
expect(model).to receive(:execute).ordered.with(/ALTER TABLE projects VALIDATE CONSTRAINT/)
expect(model).to receive(:execute).ordered.with(/RESET ALL/)
@@ -377,6 +384,7 @@ describe Gitlab::Database::MigrationHelpers do
aggregate_failures do
expect(model).to receive(:concurrent_foreign_key_name)
expect(model).to receive(:disable_statement_timeout).and_call_original
+ expect(model).to receive(:statement_timeout_disabled?).and_return(false)
expect(model).to receive(:execute).with(/statement_timeout/)
expect(model).to receive(:execute).ordered.with(/ALTER TABLE projects VALIDATE CONSTRAINT/)
expect(model).to receive(:execute).ordered.with(/RESET ALL/)
@@ -527,6 +535,26 @@ describe Gitlab::Database::MigrationHelpers do
end
end
end
+
+ # This spec runs without an enclosing transaction (:delete truncation method for db_cleaner)
+ context 'when the statement_timeout is already disabled', :delete do
+ before do
+ ActiveRecord::Base.connection.execute('SET statement_timeout TO 0')
+ end
+
+ after do
+ # Use ActiveRecord::Base.connection instead of model.execute
+ # so that this call is not counted below
+ ActiveRecord::Base.connection.execute('RESET ALL')
+ end
+
+ it 'yields control without disabling the timeout or resetting' do
+ expect(model).not_to receive(:execute).with('SET statement_timeout TO 0')
+ expect(model).not_to receive(:execute).with('RESET ALL')
+
+ expect { |block| model.disable_statement_timeout(&block) }.to yield_control
+ end
+ end
end
describe '#true_value' do
@@ -596,140 +624,12 @@ describe Gitlab::Database::MigrationHelpers do
describe '#add_column_with_default' do
let(:column) { Project.columns.find { |c| c.name == "id" } }
- context 'outside of a transaction' do
- context 'when a column limit is not set' do
- before do
- expect(model).to receive(:transaction_open?)
- .and_return(false)
- .at_least(:once)
-
- expect(model).to receive(:transaction).and_yield
-
- expect(model).to receive(:add_column)
- .with(:projects, :foo, :integer, default: nil)
-
- expect(model).to receive(:change_column_default)
- .with(:projects, :foo, 10)
-
- expect(model).to receive(:column_for)
- .with(:projects, :foo).and_return(column)
- end
-
- it 'adds the column while allowing NULL values' do
- expect(model).to receive(:update_column_in_batches)
- .with(:projects, :foo, 10)
-
- expect(model).not_to receive(:change_column_null)
-
- model.add_column_with_default(:projects, :foo, :integer,
- default: 10,
- allow_null: true)
- end
-
- it 'adds the column while not allowing NULL values' do
- expect(model).to receive(:update_column_in_batches)
- .with(:projects, :foo, 10)
-
- expect(model).to receive(:change_column_null)
- .with(:projects, :foo, false)
-
- model.add_column_with_default(:projects, :foo, :integer, default: 10)
- end
-
- it 'removes the added column whenever updating the rows fails' do
- expect(model).to receive(:update_column_in_batches)
- .with(:projects, :foo, 10)
- .and_raise(RuntimeError)
-
- expect(model).to receive(:remove_column)
- .with(:projects, :foo)
-
- expect do
- model.add_column_with_default(:projects, :foo, :integer, default: 10)
- end.to raise_error(RuntimeError)
- end
-
- it 'removes the added column whenever changing a column NULL constraint fails' do
- expect(model).to receive(:change_column_null)
- .with(:projects, :foo, false)
- .and_raise(RuntimeError)
-
- expect(model).to receive(:remove_column)
- .with(:projects, :foo)
-
- expect do
- model.add_column_with_default(:projects, :foo, :integer, default: 10)
- end.to raise_error(RuntimeError)
- end
- end
-
- context 'when `update_column_in_batches_args` is given' do
- let(:column) { UserDetail.columns.find { |c| c.name == "user_id" } }
-
- it 'uses `user_id` for `update_column_in_batches`' do
- allow(model).to receive(:transaction_open?).and_return(false)
- allow(model).to receive(:transaction).and_yield
- allow(model).to receive(:column_for).with(:user_details, :foo).and_return(column)
- allow(model).to receive(:update_column_in_batches).with(:user_details, :foo, 10, batch_column_name: :user_id)
- allow(model).to receive(:change_column_null).with(:user_details, :foo, false)
- allow(model).to receive(:change_column_default).with(:user_details, :foo, 10)
+ it 'delegates to #add_column' do
+ expect(model).to receive(:add_column).with(:projects, :foo, :integer, default: 10, limit: nil, null: true)
- expect(model).to receive(:add_column)
- .with(:user_details, :foo, :integer, default: nil)
-
- model.add_column_with_default(
- :user_details,
- :foo,
- :integer,
- default: 10,
- update_column_in_batches_args: { batch_column_name: :user_id }
- )
- end
- end
-
- context 'when a column limit is set' do
- it 'adds the column with a limit' do
- allow(model).to receive(:transaction_open?).and_return(false)
- allow(model).to receive(:transaction).and_yield
- allow(model).to receive(:column_for).with(:projects, :foo).and_return(column)
- allow(model).to receive(:update_column_in_batches).with(:projects, :foo, 10)
- allow(model).to receive(:change_column_null).with(:projects, :foo, false)
- allow(model).to receive(:change_column_default).with(:projects, :foo, 10)
-
- expect(model).to receive(:add_column)
- .with(:projects, :foo, :integer, default: nil, limit: 8)
-
- model.add_column_with_default(:projects, :foo, :integer, default: 10, limit: 8)
- end
- end
-
- it 'adds a column with an array default value for a jsonb type' do
- create(:project)
- allow(model).to receive(:transaction_open?).and_return(false)
- allow(model).to receive(:transaction).and_yield
- expect(model).to receive(:update_column_in_batches).with(:projects, :foo, '[{"foo":"json"}]').and_call_original
-
- model.add_column_with_default(:projects, :foo, :jsonb, default: [{ foo: "json" }])
- end
-
- it 'adds a column with an object default value for a jsonb type' do
- create(:project)
- allow(model).to receive(:transaction_open?).and_return(false)
- allow(model).to receive(:transaction).and_yield
- expect(model).to receive(:update_column_in_batches).with(:projects, :foo, '{"foo":"json"}').and_call_original
-
- model.add_column_with_default(:projects, :foo, :jsonb, default: { foo: "json" })
- end
- end
-
- context 'inside a transaction' do
- it 'raises RuntimeError' do
- expect(model).to receive(:transaction_open?).and_return(true)
-
- expect do
- model.add_column_with_default(:projects, :foo, :integer, default: 10)
- end.to raise_error(RuntimeError)
- end
+ model.add_column_with_default(:projects, :foo, :integer,
+ default: 10,
+ allow_null: true)
end
end
@@ -782,7 +682,7 @@ describe Gitlab::Database::MigrationHelpers do
expect(model).to receive(:update_column_in_batches)
- expect(model).to receive(:change_column_null).with(:users, :new, false)
+ expect(model).to receive(:add_not_null_constraint).with(:users, :new)
expect(model).to receive(:copy_indexes).with(:users, :old, :new)
expect(model).to receive(:copy_foreign_keys).with(:users, :old, :new)
@@ -790,6 +690,25 @@ describe Gitlab::Database::MigrationHelpers do
model.rename_column_concurrently(:users, :old, :new)
end
+ it 'passes the batch_column_name' do
+ expect(model).to receive(:column_exists?).with(:users, :other_batch_column).and_return(true)
+ expect(model).to receive(:check_trigger_permissions!).and_return(true)
+
+ expect(model).to receive(:create_column_from).with(
+ :users, :old, :new, type: nil, batch_column_name: :other_batch_column
+ ).and_return(true)
+
+ expect(model).to receive(:install_rename_triggers).and_return(true)
+
+ model.rename_column_concurrently(:users, :old, :new, batch_column_name: :other_batch_column)
+ end
+
+ it 'raises an error with invalid batch_column_name' do
+ expect do
+ model.rename_column_concurrently(:users, :old, :new, batch_column_name: :invalid)
+ end.to raise_error(RuntimeError, /Column invalid does not exist on users/)
+ end
+
context 'when default is false' do
let(:old_column) do
double(:column,
@@ -896,7 +815,7 @@ describe Gitlab::Database::MigrationHelpers do
expect(model).to receive(:update_column_in_batches)
- expect(model).to receive(:change_column_null).with(:users, :old, false)
+ expect(model).to receive(:add_not_null_constraint).with(:users, :old)
expect(model).to receive(:copy_indexes).with(:users, :new, :old)
expect(model).to receive(:copy_foreign_keys).with(:users, :new, :old)
@@ -904,6 +823,25 @@ describe Gitlab::Database::MigrationHelpers do
model.undo_cleanup_concurrent_column_rename(:users, :old, :new)
end
+ it 'passes the batch_column_name' do
+ expect(model).to receive(:column_exists?).with(:users, :other_batch_column).and_return(true)
+ expect(model).to receive(:check_trigger_permissions!).and_return(true)
+
+ expect(model).to receive(:create_column_from).with(
+ :users, :new, :old, type: nil, batch_column_name: :other_batch_column
+ ).and_return(true)
+
+ expect(model).to receive(:install_rename_triggers).and_return(true)
+
+ model.undo_cleanup_concurrent_column_rename(:users, :old, :new, batch_column_name: :other_batch_column)
+ end
+
+ it 'raises an error with invalid batch_column_name' do
+ expect do
+ model.undo_cleanup_concurrent_column_rename(:users, :old, :new, batch_column_name: :invalid)
+ end.to raise_error(RuntimeError, /Column invalid does not exist on users/)
+ end
+
context 'when default is false' do
let(:new_column) do
double(:column,
@@ -1365,6 +1303,22 @@ describe Gitlab::Database::MigrationHelpers do
end
end
+ it 'returns the final expected delay' do
+ Sidekiq::Testing.fake! do
+ final_delay = model.queue_background_migration_jobs_by_range_at_intervals(User, 'FooJob', 10.minutes, batch_size: 2)
+
+ expect(final_delay.to_f).to eq(20.minutes.to_f)
+ end
+ end
+
+ it 'returns zero when nothing gets queued' do
+ Sidekiq::Testing.fake! do
+ final_delay = model.queue_background_migration_jobs_by_range_at_intervals(User.none, 'FooJob', 10.minutes)
+
+ expect(final_delay).to eq(0)
+ end
+ end
+
context 'with batch_size option' do
it 'queues jobs correctly' do
Sidekiq::Testing.fake! do
@@ -1389,12 +1343,25 @@ describe Gitlab::Database::MigrationHelpers do
end
end
- context 'with other_arguments option' do
+ context 'with other_job_arguments option' do
+ it 'queues jobs correctly' do
+ Sidekiq::Testing.fake! do
+ model.queue_background_migration_jobs_by_range_at_intervals(User, 'FooJob', 10.minutes, other_job_arguments: [1, 2])
+
+ expect(BackgroundMigrationWorker.jobs[0]['args']).to eq(['FooJob', [id1, id3, 1, 2]])
+ expect(BackgroundMigrationWorker.jobs[0]['at']).to eq(10.minutes.from_now.to_f)
+ end
+ end
+ end
+
+ context 'with initial_delay option' do
it 'queues jobs correctly' do
- model.queue_background_migration_jobs_by_range_at_intervals(User, 'FooJob', 10.minutes, other_arguments: [1, 2])
+ Sidekiq::Testing.fake! do
+ model.queue_background_migration_jobs_by_range_at_intervals(User, 'FooJob', 10.minutes, other_job_arguments: [1, 2], initial_delay: 10.minutes)
- expect(BackgroundMigrationWorker.jobs[0]['args']).to eq(['FooJob', [id1, id3, 1, 2]])
- expect(BackgroundMigrationWorker.jobs[0]['at']).to eq(10.minutes.from_now.to_f)
+ expect(BackgroundMigrationWorker.jobs[0]['args']).to eq(['FooJob', [id1, id3, 1, 2]])
+ expect(BackgroundMigrationWorker.jobs[0]['at']).to eq(20.minutes.from_now.to_f)
+ end
end
end
end
@@ -2158,6 +2125,7 @@ describe Gitlab::Database::MigrationHelpers do
.and_return(false).exactly(1)
expect(model).to receive(:disable_statement_timeout).and_call_original
+ expect(model).to receive(:statement_timeout_disabled?).and_return(false)
expect(model).to receive(:execute).with(/statement_timeout/)
expect(model).to receive(:with_lock_retries).and_call_original
expect(model).to receive(:execute).with(/ADD CONSTRAINT check_name_not_null/)
@@ -2201,6 +2169,7 @@ describe Gitlab::Database::MigrationHelpers do
.and_return(false).exactly(1)
expect(model).to receive(:disable_statement_timeout).and_call_original
+ expect(model).to receive(:statement_timeout_disabled?).and_return(false)
expect(model).to receive(:execute).with(/statement_timeout/)
expect(model).to receive(:with_lock_retries).and_call_original
expect(model).to receive(:execute).with(/ADD CONSTRAINT check_name_not_null/)
@@ -2242,6 +2211,7 @@ describe Gitlab::Database::MigrationHelpers do
expect(model).to receive(:check_constraint_exists?).and_return(true)
expect(model).to receive(:disable_statement_timeout).and_call_original
+ expect(model).to receive(:statement_timeout_disabled?).and_return(false)
expect(model).to receive(:execute).with(/statement_timeout/)
expect(model).to receive(:execute).ordered.with(validate_sql)
expect(model).to receive(:execute).ordered.with(/RESET ALL/)
@@ -2381,4 +2351,135 @@ describe Gitlab::Database::MigrationHelpers do
end
end
end
+
+ describe '#add_not_null_constraint' do
+ context 'when it is called with the default options' do
+ it 'calls add_check_constraint with an infered constraint name and validate: true' do
+ constraint_name = model.check_constraint_name(:test_table,
+ :name,
+ 'not_null')
+ check = "name IS NOT NULL"
+
+ expect(model).to receive(:column_is_nullable?).and_return(true)
+ expect(model).to receive(:check_constraint_name).and_call_original
+ expect(model).to receive(:add_check_constraint)
+ .with(:test_table, check, constraint_name, validate: true)
+
+ model.add_not_null_constraint(:test_table, :name)
+ end
+ end
+
+ context 'when all parameters are provided' do
+ it 'calls add_check_constraint with the correct parameters' do
+ constraint_name = 'check_name_not_null'
+ check = "name IS NOT NULL"
+
+ expect(model).to receive(:column_is_nullable?).and_return(true)
+ expect(model).not_to receive(:check_constraint_name)
+ expect(model).to receive(:add_check_constraint)
+ .with(:test_table, check, constraint_name, validate: false)
+
+ model.add_not_null_constraint(
+ :test_table,
+ :name,
+ constraint_name: constraint_name,
+ validate: false
+ )
+ end
+ end
+
+ context 'when the column is defined as NOT NULL' do
+ it 'does not add a check constraint' do
+ expect(model).to receive(:column_is_nullable?).and_return(false)
+ expect(model).not_to receive(:check_constraint_name)
+ expect(model).not_to receive(:add_check_constraint)
+
+ model.add_not_null_constraint(:test_table, :name)
+ end
+ end
+ end
+
+ describe '#validate_not_null_constraint' do
+ context 'when constraint_name is not provided' do
+ it 'calls validate_check_constraint with an infered constraint name' do
+ constraint_name = model.check_constraint_name(:test_table,
+ :name,
+ 'not_null')
+
+ expect(model).to receive(:check_constraint_name).and_call_original
+ expect(model).to receive(:validate_check_constraint)
+ .with(:test_table, constraint_name)
+
+ model.validate_not_null_constraint(:test_table, :name)
+ end
+ end
+
+ context 'when constraint_name is provided' do
+ it 'calls validate_check_constraint with the correct parameters' do
+ constraint_name = 'check_name_not_null'
+
+ expect(model).not_to receive(:check_constraint_name)
+ expect(model).to receive(:validate_check_constraint)
+ .with(:test_table, constraint_name)
+
+ model.validate_not_null_constraint(:test_table, :name, constraint_name: constraint_name)
+ end
+ end
+ end
+
+ describe '#remove_not_null_constraint' do
+ context 'when constraint_name is not provided' do
+ it 'calls remove_check_constraint with an infered constraint name' do
+ constraint_name = model.check_constraint_name(:test_table,
+ :name,
+ 'not_null')
+
+ expect(model).to receive(:check_constraint_name).and_call_original
+ expect(model).to receive(:remove_check_constraint)
+ .with(:test_table, constraint_name)
+
+ model.remove_not_null_constraint(:test_table, :name)
+ end
+ end
+
+ context 'when constraint_name is provided' do
+ it 'calls remove_check_constraint with the correct parameters' do
+ constraint_name = 'check_name_not_null'
+
+ expect(model).not_to receive(:check_constraint_name)
+ expect(model).to receive(:remove_check_constraint)
+ .with(:test_table, constraint_name)
+
+ model.remove_not_null_constraint(:test_table, :name, constraint_name: constraint_name)
+ end
+ end
+ end
+
+ describe '#check_not_null_constraint_exists?' do
+ context 'when constraint_name is not provided' do
+ it 'calls check_constraint_exists? with an infered constraint name' do
+ constraint_name = model.check_constraint_name(:test_table,
+ :name,
+ 'not_null')
+
+ expect(model).to receive(:check_constraint_name).and_call_original
+ expect(model).to receive(:check_constraint_exists?)
+ .with(:test_table, constraint_name)
+
+ model.check_not_null_constraint_exists?(:test_table, :name)
+ end
+ end
+
+ context 'when constraint_name is provided' do
+ it 'calls check_constraint_exists? with the correct parameters' do
+ constraint_name = 'check_name_not_null'
+
+ expect(model).not_to receive(:check_constraint_name)
+ expect(model).to receive(:check_constraint_exists?)
+ .with(:test_table, constraint_name)
+
+ model.check_not_null_constraint_exists?(:test_table, :name, constraint_name: constraint_name)
+ end
+ end
+ end
end
diff --git a/spec/lib/gitlab/database/partitioning_migration_helpers/partitioned_foreign_key_spec.rb b/spec/lib/gitlab/database/partitioning_migration_helpers/partitioned_foreign_key_spec.rb
new file mode 100644
index 00000000000..77f71676252
--- /dev/null
+++ b/spec/lib/gitlab/database/partitioning_migration_helpers/partitioned_foreign_key_spec.rb
@@ -0,0 +1,48 @@
+# frozen_string_literal: true
+
+require 'spec_helper'
+
+describe Gitlab::Database::PartitioningMigrationHelpers::PartitionedForeignKey do
+ let(:foreign_key) do
+ described_class.new(
+ to_table: 'issues',
+ from_table: 'issue_assignees',
+ from_column: 'issue_id',
+ to_column: 'id',
+ cascade_delete: true)
+ end
+
+ describe 'validations' do
+ it 'allows keys that reference valid tables and columns' do
+ expect(foreign_key).to be_valid
+ end
+
+ it 'does not allow keys without a valid to_table' do
+ foreign_key.to_table = 'this_is_not_a_real_table'
+
+ expect(foreign_key).not_to be_valid
+ expect(foreign_key.errors[:to_table].first).to eq('must be a valid table')
+ end
+
+ it 'does not allow keys without a valid from_table' do
+ foreign_key.from_table = 'this_is_not_a_real_table'
+
+ expect(foreign_key).not_to be_valid
+ expect(foreign_key.errors[:from_table].first).to eq('must be a valid table')
+ end
+
+ it 'does not allow keys without a valid to_column' do
+ foreign_key.to_column = 'this_is_not_a_real_fk'
+
+ expect(foreign_key).not_to be_valid
+ expect(foreign_key.errors[:to_column].first).to eq('must be a valid column')
+ end
+
+ it 'does not allow keys without a valid from_column' do
+ foreign_key.from_column = 'this_is_not_a_real_pk'
+
+ expect(foreign_key).not_to be_valid
+ expect(foreign_key.errors[:from_column].first).to eq('must be a valid column')
+ end
+ end
+end
diff --git a/spec/lib/gitlab/database/partitioning_migration_helpers_spec.rb b/spec/lib/gitlab/database/partitioning_migration_helpers_spec.rb
new file mode 100644
index 00000000000..0e2fb047469
--- /dev/null
+++ b/spec/lib/gitlab/database/partitioning_migration_helpers_spec.rb
@@ -0,0 +1,230 @@
+# frozen_string_literal: true
+
+require 'spec_helper'
+
+describe Gitlab::Database::PartitioningMigrationHelpers do
+ let(:model) do
+ ActiveRecord::Migration.new.extend(described_class)
+ end
+ let_it_be(:connection) { ActiveRecord::Base.connection }
+ let(:referenced_table) { :issues }
+ let(:function_name) { model.fk_function_name(referenced_table) }
+ let(:trigger_name) { model.fk_trigger_name(referenced_table) }
+
+ before do
+ allow(model).to receive(:puts)
+ end
+
+ describe 'adding a foreign key' do
+ before do
+ allow(model).to receive(:transaction_open?).and_return(false)
+ end
+
+ context 'when the table has no foreign keys' do
+ it 'creates a trigger function to handle the single cascade' do
+ model.add_partitioned_foreign_key :issue_assignees, referenced_table
+
+ expect_function_to_contain(function_name, 'delete from issue_assignees where issue_id = old.id')
+ expect_valid_function_trigger(trigger_name, function_name)
+ end
+ end
+
+ context 'when the table already has foreign keys' do
+ context 'when the foreign key is from a different table' do
+ before do
+ model.add_partitioned_foreign_key :issue_assignees, referenced_table
+ end
+
+ it 'creates a trigger function to handle the multiple cascades' do
+ model.add_partitioned_foreign_key :epic_issues, referenced_table
+
+ expect_function_to_contain(function_name,
+ 'delete from issue_assignees where issue_id = old.id',
+ 'delete from epic_issues where issue_id = old.id')
+ expect_valid_function_trigger(trigger_name, function_name)
+ end
+ end
+
+ context 'when the foreign key is from the same table' do
+ before do
+ model.add_partitioned_foreign_key :issues, referenced_table, column: :moved_to_id
+ end
+
+ context 'when the foreign key is from a different column' do
+ it 'creates a trigger function to handle the multiple cascades' do
+ model.add_partitioned_foreign_key :issues, referenced_table, column: :duplicated_to_id
+
+ expect_function_to_contain(function_name,
+ 'delete from issues where moved_to_id = old.id',
+ 'delete from issues where duplicated_to_id = old.id')
+ expect_valid_function_trigger(trigger_name, function_name)
+ end
+ end
+
+ context 'when the foreign key is from the same column' do
+ it 'ignores the duplicate and properly recreates the trigger function' do
+ model.add_partitioned_foreign_key :issues, referenced_table, column: :moved_to_id
+
+ expect_function_to_contain(function_name, 'delete from issues where moved_to_id = old.id')
+ expect_valid_function_trigger(trigger_name, function_name)
+ end
+ end
+ end
+ end
+
+ context 'when the foreign key is set to nullify' do
+ it 'creates a trigger function that nullifies the foreign key' do
+ model.add_partitioned_foreign_key :issue_assignees, referenced_table, on_delete: :nullify
+
+ expect_function_to_contain(function_name, 'update issue_assignees set issue_id = null where issue_id = old.id')
+ expect_valid_function_trigger(trigger_name, function_name)
+ end
+ end
+
+ context 'when the referencing column is a custom value' do
+ it 'creates a trigger function with the correct column name' do
+ model.add_partitioned_foreign_key :issues, referenced_table, column: :duplicated_to_id
+
+ expect_function_to_contain(function_name, 'delete from issues where duplicated_to_id = old.id')
+ expect_valid_function_trigger(trigger_name, function_name)
+ end
+ end
+
+ context 'when the referenced column is a custom value' do
+ let(:referenced_table) { :user_details }
+
+ it 'creates a trigger function with the correct column name' do
+ model.add_partitioned_foreign_key :user_preferences, referenced_table, column: :user_id, primary_key: :user_id
+
+ expect_function_to_contain(function_name, 'delete from user_preferences where user_id = old.user_id')
+ expect_valid_function_trigger(trigger_name, function_name)
+ end
+ end
+
+ context 'when the given key definition is invalid' do
+ it 'raises an error with the appropriate message' do
+ expect do
+ model.add_partitioned_foreign_key :issue_assignees, referenced_table, column: :not_a_real_issue_id
+ end.to raise_error(/From column must be a valid column/)
+ end
+ end
+
+ context 'when run inside a transaction' do
+ it 'raises an error' do
+ expect(model).to receive(:transaction_open?).and_return(true)
+
+ expect do
+ model.add_partitioned_foreign_key :issue_assignees, referenced_table
+ end.to raise_error(/can not be run inside a transaction/)
+ end
+ end
+ end
+
+ context 'removing a foreign key' do
+ before do
+ allow(model).to receive(:transaction_open?).and_return(false)
+ end
+
+ context 'when the table has multiple foreign keys' do
+ before do
+ model.add_partitioned_foreign_key :issue_assignees, referenced_table
+ model.add_partitioned_foreign_key :epic_issues, referenced_table
+ end
+
+ it 'creates a trigger function without the removed cascade' do
+ expect_function_to_contain(function_name,
+ 'delete from issue_assignees where issue_id = old.id',
+ 'delete from epic_issues where issue_id = old.id')
+ expect_valid_function_trigger(trigger_name, function_name)
+
+ model.remove_partitioned_foreign_key :issue_assignees, referenced_table
+
+ expect_function_to_contain(function_name, 'delete from epic_issues where issue_id = old.id')
+ expect_valid_function_trigger(trigger_name, function_name)
+ end
+ end
+
+ context 'when the table has only one remaining foreign key' do
+ before do
+ model.add_partitioned_foreign_key :issue_assignees, referenced_table
+ end
+
+ it 'removes the trigger function altogether' do
+ expect_function_to_contain(function_name, 'delete from issue_assignees where issue_id = old.id')
+ expect_valid_function_trigger(trigger_name, function_name)
+
+ model.remove_partitioned_foreign_key :issue_assignees, referenced_table
+
+ expect(find_function_def(function_name)).to be_nil
+ expect(find_trigger_def(trigger_name)).to be_nil
+ end
+ end
+
+ context 'when the foreign key does not exist' do
+ before do
+ model.add_partitioned_foreign_key :issue_assignees, referenced_table
+ end
+
+ it 'ignores the invalid key and properly recreates the trigger function' do
+ expect_function_to_contain(function_name, 'delete from issue_assignees where issue_id = old.id')
+ expect_valid_function_trigger(trigger_name, function_name)
+
+ model.remove_partitioned_foreign_key :issues, referenced_table, column: :moved_to_id
+
+ expect_function_to_contain(function_name, 'delete from issue_assignees where issue_id = old.id')
+ expect_valid_function_trigger(trigger_name, function_name)
+ end
+ end
+
+ context 'when run outside a transaction' do
+ it 'raises an error' do
+ expect(model).to receive(:transaction_open?).and_return(true)
+
+ expect do
+ model.remove_partitioned_foreign_key :issue_assignees, referenced_table
+ end.to raise_error(/can not be run inside a transaction/)
+ end
+ end
+ end
+
+ def expect_function_to_contain(name, *statements)
+ return_stmt, *body_stmts = parsed_function_statements(name).reverse
+
+ expect(return_stmt).to eq('return old')
+ expect(body_stmts).to contain_exactly(*statements)
+ end
+
+ def expect_valid_function_trigger(name, fn_name)
+ event, activation, definition = cleaned_trigger_def(name)
+
+ expect(event).to eq('delete')
+ expect(activation).to eq('after')
+ expect(definition).to eq("execute procedure #{fn_name}()")
+ end
+
+ def parsed_function_statements(name)
+ cleaned_definition = find_function_def(name)['fn_body'].downcase.gsub(/\s+/, ' ')
+ statements = cleaned_definition.sub(/\A\s*begin\s*(.*)\s*end\s*\Z/, "\\1")
+ statements.split(';').map! { |stmt| stmt.strip.presence }.compact!
+ end
+
+ def find_function_def(name)
+ connection.execute("select prosrc as fn_body from pg_proc where proname = '#{name}';").first
+ end
+
+ def cleaned_trigger_def(name)
+ find_trigger_def(name).values_at('event', 'activation', 'definition').map!(&:downcase)
+ end
+
+ def find_trigger_def(name)
+ connection.execute(<<~SQL).first
+ select
+ string_agg(event_manipulation, ',') as event,
+ action_timing as activation,
+ action_statement as definition
+ from information_schema.triggers
+ where trigger_name = '#{name}'
+ group by 2, 3
+ SQL
+ end
+end
diff --git a/spec/lib/gitlab/database/rename_reserved_paths_migration/v1/rename_base_spec.rb b/spec/lib/gitlab/database/rename_reserved_paths_migration/v1/rename_base_spec.rb
index 7b8437e4874..fae57996fb6 100644
--- a/spec/lib/gitlab/database/rename_reserved_paths_migration/v1/rename_base_spec.rb
+++ b/spec/lib/gitlab/database/rename_reserved_paths_migration/v1/rename_base_spec.rb
@@ -242,7 +242,7 @@ describe Gitlab::Database::RenameReservedPathsMigration::V1::RenameBase, :delete
old_path, new_path = [nil, nil]
Gitlab::Redis::SharedState.with do |redis|
rename_info = redis.lpop(key)
- old_path, new_path = JSON.parse(rename_info)
+ old_path, new_path = Gitlab::Json.parse(rename_info)
end
expect(old_path).to eq('path/to/namespace')
@@ -278,7 +278,7 @@ describe Gitlab::Database::RenameReservedPathsMigration::V1::RenameBase, :delete
end
expect(rename_count).to eq(1)
- expect(JSON.parse(stored_renames.first)).to eq(%w(old_path new_path))
+ expect(Gitlab::Json.parse(stored_renames.first)).to eq(%w(old_path new_path))
end
end
end
diff --git a/spec/lib/gitlab/database/with_lock_retries_spec.rb b/spec/lib/gitlab/database/with_lock_retries_spec.rb
index b6321f2eab1..9c8c9749125 100644
--- a/spec/lib/gitlab/database/with_lock_retries_spec.rb
+++ b/spec/lib/gitlab/database/with_lock_retries_spec.rb
@@ -84,7 +84,7 @@ describe Gitlab::Database::WithLockRetries do
subject.run do
lock_attempts += 1
- if lock_attempts == retry_count # we reached the last retry iteration, if we kill the thread, the last try (no lock_timeout) will succeed)
+ if lock_attempts == retry_count # we reached the last retry iteration, if we kill the thread, the last try (no lock_timeout) will succeed
lock_fiber.resume
end
@@ -106,9 +106,13 @@ describe Gitlab::Database::WithLockRetries do
end
context 'after the retries, without setting lock_timeout' do
- let(:retry_count) { timing_configuration.size }
+ let(:retry_count) { timing_configuration.size + 1 }
- it_behaves_like 'retriable exclusive lock on `projects`'
+ it_behaves_like 'retriable exclusive lock on `projects`' do
+ before do
+ expect(subject).to receive(:run_block_without_lock_timeout).and_call_original
+ end
+ end
end
context 'when statement timeout is reached' do
@@ -129,11 +133,22 @@ describe Gitlab::Database::WithLockRetries do
end
end
+ context 'restore local database variables' do
+ it do
+ expect { subject.run {} }.not_to change { ActiveRecord::Base.connection.execute("SHOW lock_timeout").to_a }
+ end
+
+ it do
+ expect { subject.run {} }.not_to change { ActiveRecord::Base.connection.execute("SHOW idle_in_transaction_session_timeout").to_a }
+ end
+ end
+
context 'casting durations correctly' do
let(:timing_configuration) { [[0.015.seconds, 0.025.seconds], [0.015.seconds, 0.025.seconds]] } # 15ms, 25ms
it 'executes `SET LOCAL lock_timeout` using the configured timeout value in milliseconds' do
expect(ActiveRecord::Base.connection).to receive(:execute).with("SAVEPOINT active_record_1").and_call_original
+ expect(ActiveRecord::Base.connection).to receive(:execute).with('RESET idle_in_transaction_session_timeout; RESET lock_timeout').and_call_original
expect(ActiveRecord::Base.connection).to receive(:execute).with("SET LOCAL lock_timeout TO '15ms'").and_call_original
expect(ActiveRecord::Base.connection).to receive(:execute).with("RELEASE SAVEPOINT active_record_1").and_call_original
diff --git a/spec/lib/gitlab/diff/file_spec.rb b/spec/lib/gitlab/diff/file_spec.rb
index 61d7400b95e..d1592e60d3d 100644
--- a/spec/lib/gitlab/diff/file_spec.rb
+++ b/spec/lib/gitlab/diff/file_spec.rb
@@ -567,6 +567,61 @@ describe Gitlab::Diff::File do
end
end
+ describe '#alternate_viewer' do
+ subject { diff_file.alternate_viewer }
+
+ where(:viewer_class) do
+ [
+ DiffViewer::Image,
+ DiffViewer::Collapsed,
+ DiffViewer::NotDiffable,
+ DiffViewer::Text,
+ DiffViewer::NoPreview,
+ DiffViewer::Added,
+ DiffViewer::Deleted,
+ DiffViewer::ModeChanged,
+ DiffViewer::ModeChanged,
+ DiffViewer::NoPreview
+ ]
+ end
+
+ with_them do
+ let(:viewer) { viewer_class.new(diff_file) }
+
+ before do
+ allow(diff_file).to receive(:viewer).and_return(viewer)
+ end
+
+ it { is_expected.to be_nil }
+ end
+
+ context 'when viewer is DiffViewer::Renamed' do
+ let(:viewer) { DiffViewer::Renamed.new(diff_file) }
+
+ before do
+ allow(diff_file).to receive(:viewer).and_return(viewer)
+ end
+
+ context 'when it can be rendered as text' do
+ it { is_expected.to be_a(DiffViewer::Text) }
+ end
+
+ context 'when it can be rendered as image' do
+ let(:commit) { project.commit('2f63565e7aac07bcdadb654e253078b727143ec4') }
+ let(:diff_file) { commit.diffs.diff_file_with_new_path('files/images/6049019_460s.jpg') }
+
+ it { is_expected.to be_a(DiffViewer::Image) }
+ end
+
+ context 'when it is something else' do
+ let(:commit) { project.commit('ae73cb07c9eeaf35924a10f713b364d32b2dd34f') }
+ let(:diff_file) { commit.diffs.diff_file_with_new_path('Gemfile.zip') }
+
+ it { is_expected.to be_nil }
+ end
+ end
+ end
+
describe '#rendered_as_text?' do
context 'when the simple viewer is text-based' do
let(:commit) { project.commit('570e7b2abdd848b95f2f578043fc23bd6f6fd24d') }
diff --git a/spec/lib/gitlab/diff/formatters/text_formatter_spec.rb b/spec/lib/gitlab/diff/formatters/text_formatter_spec.rb
index e275ebef2c9..fa129a20e58 100644
--- a/spec/lib/gitlab/diff/formatters/text_formatter_spec.rb
+++ b/spec/lib/gitlab/diff/formatters/text_formatter_spec.rb
@@ -26,6 +26,7 @@ describe Gitlab::Diff::Formatters::TextFormatter do
# Specific text formatter examples
let!(:formatter) { described_class.new(attrs) }
+ let(:attrs) { base }
describe '#line_age' do
subject { formatter.line_age }
@@ -42,4 +43,21 @@ describe Gitlab::Diff::Formatters::TextFormatter do
it { is_expected.to eq('old') }
end
end
+
+ describe "#==" do
+ it "is false when the line_range changes" do
+ formatter_1 = described_class.new(base.merge(line_range: { start_line_code: "foo", end_line_code: "bar" }))
+ formatter_2 = described_class.new(base.merge(line_range: { start_line_code: "foo", end_line_code: "baz" }))
+
+ expect(formatter_1).not_to eq(formatter_2)
+ end
+
+ it "is true when the line_range doesn't change" do
+ attrs = base.merge({ line_range: { start_line_code: "foo", end_line_code: "baz" } })
+ formatter_1 = described_class.new(attrs)
+ formatter_2 = described_class.new(attrs)
+
+ expect(formatter_1).to eq(formatter_2)
+ end
+ end
end
diff --git a/spec/lib/gitlab/diff/position_spec.rb b/spec/lib/gitlab/diff/position_spec.rb
index a83c0f35d92..10749ec024d 100644
--- a/spec/lib/gitlab/diff/position_spec.rb
+++ b/spec/lib/gitlab/diff/position_spec.rb
@@ -639,11 +639,11 @@ describe Gitlab::Diff::Position do
let(:diff_position) { described_class.new(args) }
it "returns the position as JSON" do
- expect(JSON.parse(diff_position.to_json)).to eq(args.stringify_keys)
+ expect(Gitlab::Json.parse(diff_position.to_json)).to eq(args.stringify_keys)
end
it "works when nested under another hash" do
- expect(JSON.parse(JSON.generate(pos: diff_position))).to eq('pos' => args.stringify_keys)
+ expect(Gitlab::Json.parse(Gitlab::Json.generate(pos: diff_position))).to eq('pos' => args.stringify_keys)
end
end
diff --git a/spec/lib/gitlab/elasticsearch/logs/lines_spec.rb b/spec/lib/gitlab/elasticsearch/logs/lines_spec.rb
index 8b6a19fa2c5..45a262c0e77 100644
--- a/spec/lib/gitlab/elasticsearch/logs/lines_spec.rb
+++ b/spec/lib/gitlab/elasticsearch/logs/lines_spec.rb
@@ -10,7 +10,7 @@ describe Gitlab::Elasticsearch::Logs::Lines do
let(:es_message_3) { { timestamp: "2019-12-13T14:35:36.034Z", pod: "production-6866bc8974-m4sk4", message: "10.8.2.1 - - [04/Nov/2019:23:09:24 UTC] \"GET / HTTP/1.1\" 200 13" } }
let(:es_message_4) { { timestamp: "2019-12-13T14:35:37.034Z", pod: "production-6866bc8974-m4sk4", message: "- -\u003e /" } }
- let(:es_response) { JSON.parse(fixture_file('lib/elasticsearch/logs_response.json')) }
+ let(:es_response) { Gitlab::Json.parse(fixture_file('lib/elasticsearch/logs_response.json')) }
subject { described_class.new(client) }
@@ -22,13 +22,14 @@ describe Gitlab::Elasticsearch::Logs::Lines do
let(:end_time) { "2019-12-13T14:35:34.034Z" }
let(:cursor) { "9999934,1572449784442" }
- let(:body) { JSON.parse(fixture_file('lib/elasticsearch/query.json')) }
- let(:body_with_container) { JSON.parse(fixture_file('lib/elasticsearch/query_with_container.json')) }
- let(:body_with_search) { JSON.parse(fixture_file('lib/elasticsearch/query_with_search.json')) }
- let(:body_with_times) { JSON.parse(fixture_file('lib/elasticsearch/query_with_times.json')) }
- let(:body_with_start_time) { JSON.parse(fixture_file('lib/elasticsearch/query_with_start_time.json')) }
- let(:body_with_end_time) { JSON.parse(fixture_file('lib/elasticsearch/query_with_end_time.json')) }
- let(:body_with_cursor) { JSON.parse(fixture_file('lib/elasticsearch/query_with_cursor.json')) }
+ let(:body) { Gitlab::Json.parse(fixture_file('lib/elasticsearch/query.json')) }
+ let(:body_with_container) { Gitlab::Json.parse(fixture_file('lib/elasticsearch/query_with_container.json')) }
+ let(:body_with_search) { Gitlab::Json.parse(fixture_file('lib/elasticsearch/query_with_search.json')) }
+ let(:body_with_times) { Gitlab::Json.parse(fixture_file('lib/elasticsearch/query_with_times.json')) }
+ let(:body_with_start_time) { Gitlab::Json.parse(fixture_file('lib/elasticsearch/query_with_start_time.json')) }
+ let(:body_with_end_time) { Gitlab::Json.parse(fixture_file('lib/elasticsearch/query_with_end_time.json')) }
+ let(:body_with_cursor) { Gitlab::Json.parse(fixture_file('lib/elasticsearch/query_with_cursor.json')) }
+ let(:body_with_filebeat_6) { Gitlab::Json.parse(fixture_file('lib/elasticsearch/query_with_filebeat_6.json')) }
RSpec::Matchers.define :a_hash_equal_to_json do |expected|
match do |actual|
@@ -85,5 +86,12 @@ describe Gitlab::Elasticsearch::Logs::Lines do
result = subject.pod_logs(namespace, pod_name: pod_name, cursor: cursor)
expect(result).to eq(logs: [es_message_4, es_message_3, es_message_2, es_message_1], cursor: cursor)
end
+
+ it 'can search on filebeat 6' do
+ expect(client).to receive(:search).with(body: a_hash_equal_to_json(body_with_filebeat_6)).and_return(es_response)
+
+ result = subject.pod_logs(namespace, pod_name: pod_name, chart_above_v2: false)
+ expect(result).to eq(logs: [es_message_4, es_message_3, es_message_2, es_message_1], cursor: cursor)
+ end
end
end
diff --git a/spec/lib/gitlab/elasticsearch/logs/pods_spec.rb b/spec/lib/gitlab/elasticsearch/logs/pods_spec.rb
index 0a4ab0780c5..c2c3074e965 100644
--- a/spec/lib/gitlab/elasticsearch/logs/pods_spec.rb
+++ b/spec/lib/gitlab/elasticsearch/logs/pods_spec.rb
@@ -5,8 +5,8 @@ require 'spec_helper'
describe Gitlab::Elasticsearch::Logs::Pods do
let(:client) { Elasticsearch::Transport::Client }
- let(:es_query) { JSON.parse(fixture_file('lib/elasticsearch/pods_query.json'), symbolize_names: true) }
- let(:es_response) { JSON.parse(fixture_file('lib/elasticsearch/pods_response.json')) }
+ let(:es_query) { Gitlab::Json.parse(fixture_file('lib/elasticsearch/pods_query.json'), symbolize_names: true) }
+ let(:es_response) { Gitlab::Json.parse(fixture_file('lib/elasticsearch/pods_response.json')) }
let(:namespace) { "autodevops-deploy-9-production" }
subject { described_class.new(client) }
diff --git a/spec/lib/gitlab/email/handler_spec.rb b/spec/lib/gitlab/email/handler_spec.rb
index 5014e4c22ce..6dbf069f07c 100644
--- a/spec/lib/gitlab/email/handler_spec.rb
+++ b/spec/lib/gitlab/email/handler_spec.rb
@@ -6,6 +6,18 @@ describe Gitlab::Email::Handler do
let(:email) { Mail.new { body 'email' } }
describe '.for' do
+ context 'key matches the reply_key of a notification' do
+ it 'picks note handler' do
+ expect(described_class.for(email, '1234567890abcdef1234567890abcdef')).to be_an_instance_of(Gitlab::Email::Handler::CreateNoteHandler)
+ end
+ end
+
+ context 'key matches the reply_key of a notification, along with an unsubscribe suffix' do
+ it 'picks unsubscribe handler' do
+ expect(described_class.for(email, '1234567890abcdef1234567890abcdef-unsubscribe')).to be_an_instance_of(Gitlab::Email::Handler::UnsubscribeHandler)
+ end
+ end
+
it 'picks issue handler if there is no merge request prefix' do
expect(described_class.for(email, 'project+key')).to be_an_instance_of(Gitlab::Email::Handler::CreateIssueHandler)
end
diff --git a/spec/lib/gitlab/email/hook/smime_signature_interceptor_spec.rb b/spec/lib/gitlab/email/hook/smime_signature_interceptor_spec.rb
index 36954252b6b..31ba48e9df1 100644
--- a/spec/lib/gitlab/email/hook/smime_signature_interceptor_spec.rb
+++ b/spec/lib/gitlab/email/hook/smime_signature_interceptor_spec.rb
@@ -5,19 +5,24 @@ require 'spec_helper'
describe Gitlab::Email::Hook::SmimeSignatureInterceptor do
include SmimeHelper
- # cert generation is an expensive operation and they are used read-only,
+ # certs generation is an expensive operation and they are used read-only,
# so we share them as instance variables in all tests
before :context do
@root_ca = generate_root
- @cert = generate_cert(root_ca: @root_ca)
+ @intermediate_ca = generate_intermediate(signer_ca: @root_ca)
+ @cert = generate_cert(signer_ca: @intermediate_ca)
end
let(:root_certificate) do
Gitlab::Email::Smime::Certificate.new(@root_ca[:key], @root_ca[:cert])
end
+ let(:intermediate_certificate) do
+ Gitlab::Email::Smime::Certificate.new(@intermediate_ca[:key], @intermediate_ca[:cert])
+ end
+
let(:certificate) do
- Gitlab::Email::Smime::Certificate.new(@cert[:key], @cert[:cert])
+ Gitlab::Email::Smime::Certificate.new(@cert[:key], @cert[:cert], [intermediate_certificate.cert])
end
let(:mail_body) { "signed hello with Unicode €áø and\r\n newlines\r\n" }
@@ -48,17 +53,19 @@ describe Gitlab::Email::Hook::SmimeSignatureInterceptor do
# verify signature and obtain pkcs7 encoded content
p7enc = Gitlab::Email::Smime::Signer.verify_signature(
- cert: certificate.cert,
- ca_cert: root_certificate.cert,
+ ca_certs: root_certificate.cert,
signed_data: mail.encoded)
+ expect(p7enc).not_to be_nil
+
# re-verify signature from a new Mail object content
# See https://gitlab.com/gitlab-org/gitlab/issues/197386
- Gitlab::Email::Smime::Signer.verify_signature(
- cert: certificate.cert,
- ca_cert: root_certificate.cert,
+ p7_re_enc = Gitlab::Email::Smime::Signer.verify_signature(
+ ca_certs: root_certificate.cert,
signed_data: Mail.new(mail).encoded)
+ expect(p7_re_enc).not_to be_nil
+
# envelope in a Mail object and obtain the body
decoded_mail = Mail.new(p7enc.data)
diff --git a/spec/lib/gitlab/email/smime/certificate_spec.rb b/spec/lib/gitlab/email/smime/certificate_spec.rb
index 90b27602413..07b8c1e4de1 100644
--- a/spec/lib/gitlab/email/smime/certificate_spec.rb
+++ b/spec/lib/gitlab/email/smime/certificate_spec.rb
@@ -9,7 +9,8 @@ describe Gitlab::Email::Smime::Certificate do
# so we share them as instance variables in all tests
before :context do
@root_ca = generate_root
- @cert = generate_cert(root_ca: @root_ca)
+ @intermediate_ca = generate_intermediate(signer_ca: @root_ca)
+ @cert = generate_cert(signer_ca: @intermediate_ca)
end
describe 'testing environment setup' do
@@ -21,11 +22,23 @@ describe Gitlab::Email::Smime::Certificate do
end
end
+ describe 'generate_intermediate' do
+ subject { @intermediate_ca }
+
+ it 'generates an intermediate CA that expires a long way in the future' do
+ expect(subject[:cert].not_after).to be > 999.years.from_now
+ end
+
+ it 'generates an intermediate CA properly signed by the root CA' do
+ expect(subject[:cert].issuer).to eq(@root_ca[:cert].subject)
+ end
+ end
+
describe 'generate_cert' do
subject { @cert }
- it 'generates a cert properly signed by the root CA' do
- expect(subject[:cert].issuer).to eq(@root_ca[:cert].subject)
+ it 'generates a cert properly signed by the intermediate CA' do
+ expect(subject[:cert].issuer).to eq(@intermediate_ca[:cert].subject)
end
it 'generates a cert that expires soon' do
@@ -37,7 +50,7 @@ describe Gitlab::Email::Smime::Certificate do
end
context 'passing in INFINITE_EXPIRY' do
- subject { generate_cert(root_ca: @root_ca, expires_in: SmimeHelper::INFINITE_EXPIRY) }
+ subject { generate_cert(signer_ca: @intermediate_ca, expires_in: SmimeHelper::INFINITE_EXPIRY) }
it 'generates a cert that expires a long way in the future' do
expect(subject[:cert].not_after).to be > 999.years.from_now
@@ -50,7 +63,7 @@ describe Gitlab::Email::Smime::Certificate do
it 'parses correctly a certificate and key' do
parsed_cert = described_class.from_strings(@cert[:key].to_s, @cert[:cert].to_pem)
- common_cert_tests(parsed_cert, @cert, @root_ca)
+ common_cert_tests(parsed_cert, @cert, @intermediate_ca)
end
end
@@ -61,17 +74,43 @@ describe Gitlab::Email::Smime::Certificate do
parsed_cert = described_class.from_files('a_key', 'a_cert')
- common_cert_tests(parsed_cert, @cert, @root_ca)
+ common_cert_tests(parsed_cert, @cert, @intermediate_ca)
+ end
+
+ context 'with optional ca_certs' do
+ it 'parses correctly certificate, key and ca_certs' do
+ allow(File).to receive(:read).with('a_key').and_return(@cert[:key].to_s)
+ allow(File).to receive(:read).with('a_cert').and_return(@cert[:cert].to_pem)
+ allow(File).to receive(:read).with('a_ca_cert').and_return(@intermediate_ca[:cert].to_pem)
+
+ parsed_cert = described_class.from_files('a_key', 'a_cert', 'a_ca_cert')
+
+ common_cert_tests(parsed_cert, @cert, @intermediate_ca, with_ca_certs: [@intermediate_ca[:cert]])
+ end
+ end
+ end
+
+ context 'with no intermediate CA' do
+ it 'parses correctly a certificate and key' do
+ cert = generate_cert(signer_ca: @root_ca)
+
+ allow(File).to receive(:read).with('a_key').and_return(cert[:key].to_s)
+ allow(File).to receive(:read).with('a_cert').and_return(cert[:cert].to_pem)
+
+ parsed_cert = described_class.from_files('a_key', 'a_cert')
+
+ common_cert_tests(parsed_cert, cert, @root_ca)
end
end
- def common_cert_tests(parsed_cert, cert, root_ca)
+ def common_cert_tests(parsed_cert, cert, signer_ca, with_ca_certs: nil)
expect(parsed_cert.cert).to be_a(OpenSSL::X509::Certificate)
expect(parsed_cert.cert.subject).to eq(cert[:cert].subject)
- expect(parsed_cert.cert.issuer).to eq(root_ca[:cert].subject)
+ expect(parsed_cert.cert.issuer).to eq(signer_ca[:cert].subject)
expect(parsed_cert.cert.not_before).to eq(cert[:cert].not_before)
expect(parsed_cert.cert.not_after).to eq(cert[:cert].not_after)
expect(parsed_cert.cert.extensions).to include(an_object_having_attributes(oid: 'extendedKeyUsage', value: match('E-mail Protection')))
expect(parsed_cert.key).to be_a(OpenSSL::PKey::RSA)
+ expect(parsed_cert.ca_certs).to match_array(Array.wrap(with_ca_certs)) if with_ca_certs
end
end
diff --git a/spec/lib/gitlab/email/smime/signer_spec.rb b/spec/lib/gitlab/email/smime/signer_spec.rb
index 56048b7148c..d891b86da08 100644
--- a/spec/lib/gitlab/email/smime/signer_spec.rb
+++ b/spec/lib/gitlab/email/smime/signer_spec.rb
@@ -5,22 +5,39 @@ require 'spec_helper'
describe Gitlab::Email::Smime::Signer do
include SmimeHelper
- it 'signs data appropriately with SMIME' do
- root_certificate = generate_root
- certificate = generate_cert(root_ca: root_certificate)
+ let_it_be(:root_ca) { generate_root }
+ let_it_be(:intermediate_ca) { generate_intermediate(signer_ca: root_ca) }
+ context 'when using an intermediate CA' do
+ it 'signs data appropriately with SMIME' do
+ cert = generate_cert(signer_ca: intermediate_ca)
+
+ sign_and_verify('signed content', cert[:cert], cert[:key], root_ca[:cert], ca_certs: intermediate_ca[:cert])
+ end
+ end
+
+ context 'when not using an intermediate CA' do
+ it 'signs data appropriately with SMIME' do
+ cert = generate_cert(signer_ca: root_ca)
+
+ sign_and_verify('signed content', cert[:cert], cert[:key], root_ca[:cert])
+ end
+ end
+
+ def sign_and_verify(data, cert, key, root_ca_cert, ca_certs: nil)
signed_content = described_class.sign(
- cert: certificate[:cert],
- key: certificate[:key],
- data: 'signed content')
+ cert: cert,
+ key: key,
+ ca_certs: ca_certs,
+ data: data)
+
expect(signed_content).not_to be_nil
p7enc = described_class.verify_signature(
- cert: certificate[:cert],
- ca_cert: root_certificate[:cert],
+ ca_certs: root_ca_cert,
signed_data: signed_content)
expect(p7enc).not_to be_nil
- expect(p7enc.data).to eq('signed content')
+ expect(p7enc.data).to eq(data)
end
end
diff --git a/spec/lib/gitlab/exclusive_lease_helpers/sleeping_lock_spec.rb b/spec/lib/gitlab/exclusive_lease_helpers/sleeping_lock_spec.rb
new file mode 100644
index 00000000000..8917eeec56f
--- /dev/null
+++ b/spec/lib/gitlab/exclusive_lease_helpers/sleeping_lock_spec.rb
@@ -0,0 +1,102 @@
+# frozen_string_literal: true
+
+require 'spec_helper'
+
+describe Gitlab::ExclusiveLeaseHelpers::SleepingLock, :clean_gitlab_redis_shared_state do
+ include ::ExclusiveLeaseHelpers
+
+ let(:timeout) { 1.second }
+ let(:delay) { 0.1.seconds }
+ let(:key) { SecureRandom.hex(10) }
+
+ subject { described_class.new(key, timeout: timeout, delay: delay) }
+
+ describe '#retried?' do
+ before do
+ stub_exclusive_lease(key, 'uuid')
+ end
+
+ context 'we have not made any attempts' do
+ it { is_expected.not_to be_retried }
+ end
+
+ context 'we just made a single (initial) attempt' do
+ it 'is not considered a retry' do
+ subject.send(:try_obtain)
+
+ is_expected.not_to be_retried
+ end
+ end
+
+ context 'made multiple attempts' do
+ it 'is considered a retry' do
+ 2.times { subject.send(:try_obtain) }
+
+ is_expected.to be_retried
+ end
+ end
+ end
+
+ describe '#obtain' do
+ context 'when the lease is not held' do
+ before do
+ stub_exclusive_lease(key, 'uuid')
+ end
+
+ it 'obtains the lease on the first attempt, without sleeping' do
+ expect(subject).not_to receive(:sleep)
+
+ subject.obtain(10)
+
+ expect(subject).not_to be_retried
+ end
+ end
+
+ context 'when the lease is held elsewhere' do
+ let!(:lease) { stub_exclusive_lease_taken(key) }
+ let(:max_attempts) { 7 }
+
+ it 'retries to obtain a lease and raises an error' do
+ expect(subject).to receive(:sleep).with(delay).exactly(max_attempts - 1).times
+ expect(lease).to receive(:try_obtain).exactly(max_attempts).times
+
+ expect { subject.obtain(max_attempts) }.to raise_error('Failed to obtain a lock')
+ end
+
+ context 'when the delay is computed from the attempt number' do
+ let(:delay) { ->(n) { 3 * n } }
+
+ it 'uses the computation to determine the sleep length' do
+ expect(subject).to receive(:sleep).with(3).once
+ expect(subject).to receive(:sleep).with(6).once
+ expect(subject).to receive(:sleep).with(9).once
+ expect(lease).to receive(:try_obtain).exactly(4).times
+
+ expect { subject.obtain(4) }.to raise_error('Failed to obtain a lock')
+ end
+ end
+
+ context 'when lease is granted after retry' do
+ it 'knows that it retried' do
+ expect(subject).to receive(:sleep).with(delay).exactly(3).times
+ expect(lease).to receive(:try_obtain).exactly(3).times { nil }
+ expect(lease).to receive(:try_obtain).once { 'obtained' }
+
+ subject.obtain(max_attempts)
+
+ expect(subject).to be_retried
+ end
+ end
+ end
+
+ describe 'cancel' do
+ let!(:lease) { stub_exclusive_lease(key, 'uuid') }
+
+ it 'cancels the lease' do
+ expect(lease).to receive(:cancel)
+
+ subject.cancel
+ end
+ end
+ end
+end
diff --git a/spec/lib/gitlab/exclusive_lease_helpers_spec.rb b/spec/lib/gitlab/exclusive_lease_helpers_spec.rb
index 747fe369c78..9914518cda5 100644
--- a/spec/lib/gitlab/exclusive_lease_helpers_spec.rb
+++ b/spec/lib/gitlab/exclusive_lease_helpers_spec.rb
@@ -22,9 +22,7 @@ describe Gitlab::ExclusiveLeaseHelpers, :clean_gitlab_redis_shared_state do
end
context 'when the lease is not obtained yet' do
- before do
- stub_exclusive_lease(unique_key, 'uuid')
- end
+ let!(:lease) { stub_exclusive_lease(unique_key, 'uuid') }
it 'calls the given block' do
expect { |b| class_instance.in_lock(unique_key, &b) }.to yield_with_args(false)
@@ -37,7 +35,7 @@ describe Gitlab::ExclusiveLeaseHelpers, :clean_gitlab_redis_shared_state do
end
it 'cancels the exclusive lease after the block' do
- expect_to_cancel_exclusive_lease(unique_key, 'uuid')
+ expect(lease).to receive(:cancel).once
subject
end
@@ -81,11 +79,32 @@ describe Gitlab::ExclusiveLeaseHelpers, :clean_gitlab_redis_shared_state do
end
end
+ context 'when we specify no retries' do
+ let(:options) { { retries: 0 } }
+
+ it 'never sleeps' do
+ expect(class_instance).not_to receive(:sleep)
+
+ expect { subject }.to raise_error('Failed to obtain a lock')
+ end
+ end
+
context 'when sleep second is specified' do
- let(:options) { { retries: 0, sleep_sec: 0.05.seconds } }
+ let(:options) { { retries: 1, sleep_sec: 0.05.seconds } }
+
+ it 'receives the specified argument' do
+ expect_any_instance_of(Object).to receive(:sleep).with(0.05.seconds).once
+
+ expect { subject }.to raise_error('Failed to obtain a lock')
+ end
+ end
+
+ context 'when sleep second is specified as a lambda' do
+ let(:options) { { retries: 2, sleep_sec: ->(num) { 0.1 + num } } }
it 'receives the specified argument' do
- expect(class_instance).to receive(:sleep).with(0.05.seconds).once
+ expect_any_instance_of(Object).to receive(:sleep).with(1.1.seconds).once
+ expect_any_instance_of(Object).to receive(:sleep).with(2.1.seconds).once
expect { subject }.to raise_error('Failed to obtain a lock')
end
diff --git a/spec/lib/gitlab/exclusive_lease_spec.rb b/spec/lib/gitlab/exclusive_lease_spec.rb
index 0739f622af5..2c0bb23a0b6 100644
--- a/spec/lib/gitlab/exclusive_lease_spec.rb
+++ b/spec/lib/gitlab/exclusive_lease_spec.rb
@@ -21,6 +21,27 @@ describe Gitlab::ExclusiveLease, :clean_gitlab_redis_shared_state do
end
end
+ describe '.redis_shared_state_key' do
+ it 'provides a namespaced key' do
+ expect(described_class.redis_shared_state_key(unique_key))
+ .to start_with(described_class::PREFIX)
+ .and include(unique_key)
+ end
+ end
+
+ describe '.ensure_prefixed_key' do
+ it 'does not double prefix a key' do
+ prefixed = described_class.redis_shared_state_key(unique_key)
+
+ expect(described_class.ensure_prefixed_key(unique_key))
+ .to eq(described_class.ensure_prefixed_key(prefixed))
+ end
+
+ it 'raises errors when there is no key' do
+ expect { described_class.ensure_prefixed_key(nil) }.to raise_error(described_class::NoKey)
+ end
+ end
+
describe '#renew' do
it 'returns true when we have the existing lease' do
lease = described_class.new(unique_key, timeout: 3600)
@@ -61,18 +82,61 @@ describe Gitlab::ExclusiveLease, :clean_gitlab_redis_shared_state do
end
end
- describe '.cancel' do
- it 'can cancel a lease' do
- uuid = new_lease(unique_key)
- expect(uuid).to be_present
- expect(new_lease(unique_key)).to eq(false)
+ describe 'cancellation' do
+ def new_lease(key)
+ described_class.new(key, timeout: 3600)
+ end
- described_class.cancel(unique_key, uuid)
- expect(new_lease(unique_key)).to be_present
+ shared_examples 'cancelling a lease' do
+ let(:lease) { new_lease(unique_key) }
+
+ it 'releases the held lease' do
+ uuid = lease.try_obtain
+ expect(uuid).to be_present
+ expect(new_lease(unique_key).try_obtain).to eq(false)
+
+ cancel_lease(uuid)
+
+ expect(new_lease(unique_key).try_obtain).to be_present
+ end
end
- def new_lease(key)
- described_class.new(key, timeout: 3600).try_obtain
+ describe '.cancel' do
+ def cancel_lease(uuid)
+ described_class.cancel(release_key, uuid)
+ end
+
+ context 'when called with the unprefixed key' do
+ it_behaves_like 'cancelling a lease' do
+ let(:release_key) { unique_key }
+ end
+ end
+
+ context 'when called with the prefixed key' do
+ it_behaves_like 'cancelling a lease' do
+ let(:release_key) { described_class.redis_shared_state_key(unique_key) }
+ end
+ end
+
+ it 'does not raise errors when given a nil key' do
+ expect { described_class.cancel(nil, nil) }.not_to raise_error
+ end
+ end
+
+ describe '#cancel' do
+ def cancel_lease(_uuid)
+ lease.cancel
+ end
+
+ it_behaves_like 'cancelling a lease'
+
+ it 'is safe to call even if the lease was never obtained' do
+ lease = new_lease(unique_key)
+
+ lease.cancel
+
+ expect(new_lease(unique_key).try_obtain).to be_present
+ end
end
end
diff --git a/spec/lib/gitlab/experimentation_spec.rb b/spec/lib/gitlab/experimentation_spec.rb
index a39c50ab038..99442cb0ca6 100644
--- a/spec/lib/gitlab/experimentation_spec.rb
+++ b/spec/lib/gitlab/experimentation_spec.rb
@@ -6,19 +6,16 @@ describe Gitlab::Experimentation do
before do
stub_const('Gitlab::Experimentation::EXPERIMENTS', {
test_experiment: {
- feature_toggle: feature_toggle,
environment: environment,
- enabled_ratio: enabled_ratio,
tracking_category: 'Team'
}
})
- stub_feature_flags(feature_toggle => true)
+ allow(Feature).to receive(:get).with(:test_experiment_experiment_percentage).and_return double(percentage_of_time_value: enabled_percentage)
end
- let(:feature_toggle) { :test_experiment_toggle }
let(:environment) { Rails.env.test? }
- let(:enabled_ratio) { 0.1 }
+ let(:enabled_percentage) { 10 }
describe Gitlab::Experimentation::ControllerConcern, type: :controller do
controller(ApplicationController) do
@@ -251,44 +248,16 @@ describe Gitlab::Experimentation do
end
end
- describe 'feature toggle' do
- context 'feature toggle is not set' do
- let(:feature_toggle) { nil }
+ describe 'experiment is disabled' do
+ let(:enabled_percentage) { 0 }
- it { is_expected.to be_truthy }
- end
-
- context 'feature toggle is not set, but a feature with the experiment key as name does exist' do
- before do
- stub_feature_flags(test_experiment: false)
- end
-
- let(:feature_toggle) { nil }
-
- it { is_expected.to be_falsey }
- end
-
- context 'feature toggle is disabled' do
- before do
- stub_feature_flags(feature_toggle => false)
- end
-
- it { is_expected.to be_falsey }
- end
+ it { is_expected.to be_falsey }
end
- describe 'environment' do
- context 'environment is not set' do
- let(:environment) { nil }
-
- it { is_expected.to be_truthy }
- end
-
- context 'we are on the wrong environment' do
- let(:environment) { ::Gitlab.com? }
+ describe 'we are on the wrong environment' do
+ let(:environment) { ::Gitlab.com? }
- it { is_expected.to be_falsey }
- end
+ it { is_expected.to be_falsey }
end
end
@@ -312,12 +281,6 @@ describe Gitlab::Experimentation do
it { is_expected.to be_truthy }
- context 'enabled ratio is not set' do
- let(:enabled_ratio) { nil }
-
- it { is_expected.to be_falsey }
- end
-
describe 'experimentation_subject_index' do
context 'experimentation_subject_index is not set' do
let(:experimentation_subject_index) { nil }
diff --git a/spec/lib/gitlab/gfm/reference_rewriter_spec.rb b/spec/lib/gitlab/gfm/reference_rewriter_spec.rb
index 084dde1f93f..335135696ef 100644
--- a/spec/lib/gitlab/gfm/reference_rewriter_spec.rb
+++ b/spec/lib/gitlab/gfm/reference_rewriter_spec.rb
@@ -147,6 +147,18 @@ describe Gitlab::Gfm::ReferenceRewriter do
it { is_expected.to eq text }
end
+ context 'when referring to a group' do
+ let(:text) { "group @#{group.full_path}" }
+
+ it { is_expected.to eq text }
+ end
+
+ context 'when referring to a user' do
+ let(:text) { "user @#{user.full_path}" }
+
+ it { is_expected.to eq text }
+ end
+
context 'when referable has a nil reference' do
before do
create(:milestone, title: '9.0', project: old_project)
diff --git a/spec/lib/gitlab/git/attributes_parser_spec.rb b/spec/lib/gitlab/git/attributes_parser_spec.rb
index 94b7a086e59..45db4acd3ac 100644
--- a/spec/lib/gitlab/git/attributes_parser_spec.rb
+++ b/spec/lib/gitlab/git/attributes_parser_spec.rb
@@ -75,6 +75,14 @@ describe Gitlab::Git::AttributesParser, :seed_helper do
expect(subject.attributes('test.foo')).to eq({})
end
end
+
+ context 'when attributes data has binary data' do
+ let(:data) { "\xFF\xFE*\u0000.\u0000c\u0000s".b }
+
+ it 'returns an empty Hash' do
+ expect(subject.attributes('test.foo')).to eq({})
+ end
+ end
end
describe '#patterns' do
diff --git a/spec/lib/gitlab/git/blob_spec.rb b/spec/lib/gitlab/git/blob_spec.rb
index 06f9767d58b..46d9b78c14b 100644
--- a/spec/lib/gitlab/git/blob_spec.rb
+++ b/spec/lib/gitlab/git/blob_spec.rb
@@ -652,4 +652,16 @@ describe Gitlab::Git::Blob, :seed_helper do
expect(described_class).to respond_to(:gitlab_blob_size)
end
end
+
+ describe '#lines' do
+ context 'when the encoding cannot be detected' do
+ it 'successfully splits the data' do
+ data = "test\nblob"
+ blob = Gitlab::Git::Blob.new(name: 'test', size: data.bytesize, data: data)
+ expect(blob).to receive(:ruby_encoding) { nil }
+
+ expect(blob.lines).to eq(data.split("\n"))
+ end
+ end
+ end
end
diff --git a/spec/lib/gitlab/git/commit_spec.rb b/spec/lib/gitlab/git/commit_spec.rb
index c2fc228d34a..edd367673fb 100644
--- a/spec/lib/gitlab/git/commit_spec.rb
+++ b/spec/lib/gitlab/git/commit_spec.rb
@@ -161,6 +161,26 @@ describe Gitlab::Git::Commit, :seed_helper do
expect(described_class.find(repository, "+123_4532530XYZ")).to be_nil
end
+ it "returns nil for id started with dash" do
+ expect(described_class.find(repository, "-HEAD")).to be_nil
+ end
+
+ it "returns nil for id containing colon" do
+ expect(described_class.find(repository, "HEAD:")).to be_nil
+ end
+
+ it "returns nil for id containing space" do
+ expect(described_class.find(repository, "HE AD")).to be_nil
+ end
+
+ it "returns nil for id containing tab" do
+ expect(described_class.find(repository, "HE\tAD")).to be_nil
+ end
+
+ it "returns nil for id containing NULL" do
+ expect(described_class.find(repository, "HE\x00AD")).to be_nil
+ end
+
context 'with broken repo' do
let(:repository) { Gitlab::Git::Repository.new('default', TEST_BROKEN_REPO_PATH, '', 'group/project') }
diff --git a/spec/lib/gitlab/git/tag_spec.rb b/spec/lib/gitlab/git/tag_spec.rb
index 87db3f588ad..6d3b239c38f 100644
--- a/spec/lib/gitlab/git/tag_spec.rb
+++ b/spec/lib/gitlab/git/tag_spec.rb
@@ -13,6 +13,13 @@ describe Gitlab::Git::Tag, :seed_helper do
it { expect(tag.target).to eq("f4e6814c3e4e7a0de82a9e7cd20c626cc963a2f8") }
it { expect(tag.dereferenced_target.sha).to eq("6f6d7e7ed97bb5f0054f2b1df789b39ca89b6ff9") }
it { expect(tag.message).to eq("Release") }
+ it { expect(tag.has_signature?).to be_falsey }
+ it { expect(tag.signature_type).to eq(:NONE) }
+ it { expect(tag.signature).to be_nil }
+ it { expect(tag.tagger.name).to eq("Dmitriy Zaporozhets") }
+ it { expect(tag.tagger.email).to eq("dmitriy.zaporozhets@gmail.com") }
+ it { expect(tag.tagger.date).to eq(Google::Protobuf::Timestamp.new(seconds: 1393491299)) }
+ it { expect(tag.tagger.timezone).to eq("+0200") }
end
describe 'last tag' do
@@ -22,6 +29,29 @@ describe Gitlab::Git::Tag, :seed_helper do
it { expect(tag.target).to eq("2ac1f24e253e08135507d0830508febaaccf02ee") }
it { expect(tag.dereferenced_target.sha).to eq("fa1b1e6c004a68b7d8763b86455da9e6b23e36d6") }
it { expect(tag.message).to eq("Version 1.2.1") }
+ it { expect(tag.has_signature?).to be_falsey }
+ it { expect(tag.signature_type).to eq(:NONE) }
+ it { expect(tag.signature).to be_nil }
+ it { expect(tag.tagger.name).to eq("Douwe Maan") }
+ it { expect(tag.tagger.email).to eq("douwe@selenight.nl") }
+ it { expect(tag.tagger.date).to eq(Google::Protobuf::Timestamp.new(seconds: 1427789449)) }
+ it { expect(tag.tagger.timezone).to eq("+0200") }
+ end
+
+ describe 'signed tag' do
+ let(:project) { create(:project, :repository) }
+ let(:tag) { project.repository.find_tag('v1.1.1') }
+
+ it { expect(tag.target).to eq("8f03acbcd11c53d9c9468078f32a2622005a4841") }
+ it { expect(tag.dereferenced_target.sha).to eq("189a6c924013fc3fe40d6f1ec1dc20214183bc97") }
+ it { expect(tag.message).to eq("x509 signed tag" + "\n" + X509Helpers::User1.signed_tag_signature.chomp) }
+ it { expect(tag.has_signature?).to be_truthy }
+ it { expect(tag.signature_type).to eq(:X509) }
+ it { expect(tag.signature).not_to be_nil }
+ it { expect(tag.tagger.name).to eq("Roger Meier") }
+ it { expect(tag.tagger.email).to eq("r.meier@siemens.com") }
+ it { expect(tag.tagger.date).to eq(Google::Protobuf::Timestamp.new(seconds: 1574261780)) }
+ it { expect(tag.tagger.timezone).to eq("+0100") }
end
it { expect(repository.tags.size).to eq(SeedRepo::Repo::TAGS.size) }
diff --git a/spec/lib/gitlab/git_access_design_spec.rb b/spec/lib/gitlab/git_access_design_spec.rb
new file mode 100644
index 00000000000..d816608f7e5
--- /dev/null
+++ b/spec/lib/gitlab/git_access_design_spec.rb
@@ -0,0 +1,45 @@
+# frozen_string_literal: true
+require 'spec_helper'
+
+describe Gitlab::GitAccessDesign do
+ include DesignManagementTestHelpers
+
+ let_it_be(:project) { create(:project) }
+ let_it_be(:user) { project.owner }
+ let(:protocol) { 'web' }
+ let(:actor) { user }
+
+ subject(:access) do
+ described_class.new(actor, project, protocol, authentication_abilities: [:read_project, :download_code, :push_code])
+ end
+
+ describe '#check' do
+ subject { access.check('git-receive-pack', ::Gitlab::GitAccess::ANY) }
+
+ before do
+ enable_design_management
+ end
+
+ context 'when the user is allowed to manage designs' do
+ it do
+ is_expected.to be_a(::Gitlab::GitAccessResult::Success)
+ end
+ end
+
+ context 'when the user is not allowed to manage designs' do
+ let_it_be(:user) { create(:user) }
+
+ it 'raises an error' do
+ expect { subject }.to raise_error(::Gitlab::GitAccess::ForbiddenError)
+ end
+ end
+
+ context 'when the protocol is not web' do
+ let(:protocol) { 'https' }
+
+ it 'raises an error' do
+ expect { subject }.to raise_error(::Gitlab::GitAccess::ForbiddenError)
+ end
+ end
+ end
+end
diff --git a/spec/lib/gitlab/git_access_snippet_spec.rb b/spec/lib/gitlab/git_access_snippet_spec.rb
index bbc3808df12..48b425a8ec5 100644
--- a/spec/lib/gitlab/git_access_snippet_spec.rb
+++ b/spec/lib/gitlab/git_access_snippet_spec.rb
@@ -11,8 +11,9 @@ describe Gitlab::GitAccessSnippet do
let_it_be(:user) { create(:user) }
let_it_be(:project) { create(:project, :public) }
let_it_be(:snippet) { create(:project_snippet, :public, :repository, project: project) }
- let(:repository) { snippet.repository }
+ let_it_be(:migration_bot) { User.migration_bot }
+ let(:repository) { snippet.repository }
let(:actor) { user }
let(:protocol) { 'ssh' }
let(:changes) { Gitlab::GitAccess::ANY }
@@ -27,20 +28,19 @@ describe Gitlab::GitAccessSnippet do
let(:actor) { build(:deploy_key) }
it 'does not allow push and pull access' do
+ expect { push_access_check }.to raise_forbidden(described_class::ERROR_MESSAGES[:authentication_mechanism])
expect { pull_access_check }.to raise_forbidden(described_class::ERROR_MESSAGES[:authentication_mechanism])
end
end
- describe 'when feature flag :version_snippets is disabled' do
- let(:user) { snippet.author }
-
- before do
- stub_feature_flags(version_snippets: false)
- end
+ shared_examples 'actor is migration bot' do
+ context 'when user is the migration bot' do
+ let(:user) { migration_bot }
- it 'allows push and pull access' do
- expect { pull_access_check }.not_to raise_error
- expect { push_access_check }.not_to raise_error
+ it 'can perform git operations' do
+ expect { push_access_check }.not_to raise_error
+ expect { pull_access_check }.not_to raise_error
+ end
end
end
@@ -90,6 +90,12 @@ describe Gitlab::GitAccessSnippet do
expect { push_access_check }.not_to raise_error
expect { pull_access_check }.not_to raise_error
end
+
+ it_behaves_like 'actor is migration bot' do
+ before do
+ expect(migration_bot.required_terms_not_accepted?).to be_truthy
+ end
+ end
end
context 'project snippet accessibility', :aggregate_failures do
@@ -120,6 +126,7 @@ describe Gitlab::GitAccessSnippet do
context 'when project is public' do
it_behaves_like 'checks accessibility'
+ it_behaves_like 'actor is migration bot'
end
context 'when project is public but snippet feature is private' do
@@ -130,6 +137,7 @@ describe Gitlab::GitAccessSnippet do
end
it_behaves_like 'checks accessibility'
+ it_behaves_like 'actor is migration bot'
end
context 'when project is not accessible' do
@@ -140,11 +148,58 @@ describe Gitlab::GitAccessSnippet do
let(:membership) { membership }
it 'respects accessibility' do
- expect { push_access_check }.to raise_error(described_class::NotFoundError)
- expect { pull_access_check }.to raise_error(described_class::NotFoundError)
+ expect { push_access_check }.to raise_snippet_not_found
+ expect { pull_access_check }.to raise_snippet_not_found
+ end
+ end
+ end
+
+ it_behaves_like 'actor is migration bot'
+ end
+
+ context 'when project is archived' do
+ let(:project) { create(:project, :public, :archived) }
+
+ [:anonymous, :non_member].each do |membership|
+ context membership.to_s do
+ let(:membership) { membership }
+
+ it 'cannot perform git operations' do
+ expect { push_access_check }.to raise_error(described_class::ForbiddenError)
+ expect { pull_access_check }.to raise_error(described_class::ForbiddenError)
+ end
+ end
+ end
+
+ [:guest, :reporter, :maintainer, :author, :admin].each do |membership|
+ context membership.to_s do
+ let(:membership) { membership }
+
+ it 'cannot perform git pushes' do
+ expect { push_access_check }.to raise_error(described_class::ForbiddenError)
+ expect { pull_access_check }.not_to raise_error
+ end
+ end
+ end
+
+ it_behaves_like 'actor is migration bot'
+ end
+
+ context 'when snippet feature is disabled' do
+ let(:project) { create(:project, :public, :snippets_disabled) }
+
+ [:anonymous, :non_member, :author, :admin].each do |membership|
+ context membership.to_s do
+ let(:membership) { membership }
+
+ it 'cannot perform git operations' do
+ expect { push_access_check }.to raise_error(described_class::ForbiddenError)
+ expect { pull_access_check }.to raise_error(described_class::ForbiddenError)
end
end
end
+
+ it_behaves_like 'actor is migration bot'
end
end
@@ -172,6 +227,8 @@ describe Gitlab::GitAccessSnippet do
expect { pull_access_check }.to raise_error(error_class)
end
end
+
+ it_behaves_like 'actor is migration bot'
end
end
@@ -179,36 +236,66 @@ describe Gitlab::GitAccessSnippet do
let(:user) { snippet.author }
let!(:primary_node) { FactoryBot.create(:geo_node, :primary) }
- # Without override, push access would return Gitlab::GitAccessResult::CustomAction
- it 'skips geo for snippet' do
+ before do
allow(::Gitlab::Database).to receive(:read_only?).and_return(true)
allow(::Gitlab::Geo).to receive(:secondary_with_primary?).and_return(true)
+ end
+ # Without override, push access would return Gitlab::GitAccessResult::CustomAction
+ it 'skips geo for snippet' do
expect { push_access_check }.to raise_forbidden(/You can't push code to a read-only GitLab instance/)
end
+
+ context 'when user is migration bot' do
+ let(:user) { migration_bot }
+
+ it 'skips geo for snippet' do
+ expect { push_access_check }.to raise_forbidden(/You can't push code to a read-only GitLab instance/)
+ end
+ end
end
context 'when changes are specific' do
let(:changes) { "2d1db523e11e777e49377cfb22d368deec3f0793 ddd0f15ae83993f5cb66a927a28673882e99100b master" }
let(:user) { snippet.author }
- it 'does not raise error if SnippetCheck does not raise error' do
- expect_next_instance_of(Gitlab::Checks::SnippetCheck) do |check|
- expect(check).to receive(:validate!).and_call_original
+ shared_examples 'snippet checks' do
+ it 'does not raise error if SnippetCheck does not raise error' do
+ expect_next_instance_of(Gitlab::Checks::SnippetCheck) do |check|
+ expect(check).to receive(:validate!).and_call_original
+ end
+ expect_next_instance_of(Gitlab::Checks::PushFileCountCheck) do |check|
+ expect(check).to receive(:validate!)
+ end
+
+ expect { push_access_check }.not_to raise_error
end
- expect_next_instance_of(Gitlab::Checks::PushFileCountCheck) do |check|
- expect(check).to receive(:validate!)
+
+ it 'raises error if SnippetCheck raises error' do
+ expect_next_instance_of(Gitlab::Checks::SnippetCheck) do |check|
+ allow(check).to receive(:validate!).and_raise(Gitlab::GitAccess::ForbiddenError, 'foo')
+ end
+
+ expect { push_access_check }.to raise_forbidden('foo')
end
- expect { push_access_check }.not_to raise_error
- end
+ it 'sets the file count limit from Snippet class' do
+ service = double
- it 'raises error if SnippetCheck raises error' do
- expect_next_instance_of(Gitlab::Checks::SnippetCheck) do |check|
- allow(check).to receive(:validate!).and_raise(Gitlab::GitAccess::ForbiddenError, 'foo')
+ expect(service).to receive(:validate!).and_return(nil)
+ expect(Snippet).to receive(:max_file_limit).with(user).and_return(5)
+ expect(Gitlab::Checks::PushFileCountCheck).to receive(:new).with(anything, hash_including(limit: 5)).and_return(service)
+
+ push_access_check
end
+ end
+
+ it_behaves_like 'snippet checks'
- expect { push_access_check }.to raise_forbidden('foo')
+ context 'when user is migration bot' do
+ let(:user) { migration_bot }
+
+ it_behaves_like 'snippet checks'
end
end
@@ -221,6 +308,16 @@ describe Gitlab::GitAccessSnippet do
let(:ref) { "refs/heads/snippet/edit-file" }
let(:changes) { "#{oldrev} #{newrev} #{ref}" }
+ shared_examples 'migration bot does not err' do
+ let(:actor) { migration_bot }
+
+ it 'does not err' do
+ expect(snippet.repository_size_checker).not_to receive(:above_size_limit?)
+
+ expect { push_access_check }.not_to raise_error
+ end
+ end
+
shared_examples_for 'a push to repository already over the limit' do
it 'errs' do
expect(snippet.repository_size_checker).to receive(:above_size_limit?).and_return(true)
@@ -229,6 +326,8 @@ describe Gitlab::GitAccessSnippet do
push_access_check
end.to raise_error(described_class::ForbiddenError, /Your push has been rejected/)
end
+
+ it_behaves_like 'migration bot does not err'
end
shared_examples_for 'a push to repository below the limit' do
@@ -241,6 +340,8 @@ describe Gitlab::GitAccessSnippet do
expect { push_access_check }.not_to raise_error
end
+
+ it_behaves_like 'migration bot does not err'
end
shared_examples_for 'a push to repository to make it over the limit' do
@@ -255,6 +356,8 @@ describe Gitlab::GitAccessSnippet do
push_access_check
end.to raise_error(described_class::ForbiddenError, /Your push to this repository would cause it to exceed the size limit/)
end
+
+ it_behaves_like 'migration bot does not err'
end
context 'when GIT_OBJECT_DIRECTORY_RELATIVE env var is set' do
diff --git a/spec/lib/gitlab/git_access_wiki_spec.rb b/spec/lib/gitlab/git_access_wiki_spec.rb
index b5e673c9e79..e42570804a8 100644
--- a/spec/lib/gitlab/git_access_wiki_spec.rb
+++ b/spec/lib/gitlab/git_access_wiki_spec.rb
@@ -52,14 +52,10 @@ describe Gitlab::GitAccessWiki do
end
context 'when the wiki repository does not exist' do
- it 'returns not found' do
- wiki_repo = project.wiki.repository
- Gitlab::GitalyClient::StorageSettings.allow_disk_access do
- FileUtils.rm_rf(wiki_repo.path)
- end
+ let(:project) { create(:project) }
- # Sanity check for rm_rf
- expect(wiki_repo.exists?).to eq(false)
+ it 'returns not found' do
+ expect(project.wiki_repository_exists?).to eq(false)
expect { subject }.to raise_error(Gitlab::GitAccess::NotFoundError, 'A repository for this project does not exist yet.')
end
diff --git a/spec/lib/gitlab/gl_repository/repo_type_spec.rb b/spec/lib/gitlab/gl_repository/repo_type_spec.rb
index 6185b068d4c..bf6df55b71e 100644
--- a/spec/lib/gitlab/gl_repository/repo_type_spec.rb
+++ b/spec/lib/gitlab/gl_repository/repo_type_spec.rb
@@ -7,6 +7,7 @@ describe Gitlab::GlRepository::RepoType do
let_it_be(:project_snippet) { create(:project_snippet, project: project, author: project.owner) }
let(:project_path) { project.repository.full_path }
let(:wiki_path) { project.wiki.repository.full_path }
+ let(:design_path) { project.design_repository.full_path }
let(:personal_snippet_path) { "snippets/#{personal_snippet.id}" }
let(:project_snippet_path) { "#{project.full_path}/snippets/#{project_snippet.id}" }
@@ -24,6 +25,7 @@ describe Gitlab::GlRepository::RepoType do
expect(described_class).not_to be_wiki
expect(described_class).to be_project
expect(described_class).not_to be_snippet
+ expect(described_class).not_to be_design
end
end
@@ -33,6 +35,7 @@ describe Gitlab::GlRepository::RepoType do
expect(described_class.valid?(wiki_path)).to be_truthy
expect(described_class.valid?(personal_snippet_path)).to be_truthy
expect(described_class.valid?(project_snippet_path)).to be_truthy
+ expect(described_class.valid?(design_path)).to be_truthy
end
end
end
@@ -51,6 +54,7 @@ describe Gitlab::GlRepository::RepoType do
expect(described_class).to be_wiki
expect(described_class).not_to be_project
expect(described_class).not_to be_snippet
+ expect(described_class).not_to be_design
end
end
@@ -60,6 +64,7 @@ describe Gitlab::GlRepository::RepoType do
expect(described_class.valid?(wiki_path)).to be_truthy
expect(described_class.valid?(personal_snippet_path)).to be_falsey
expect(described_class.valid?(project_snippet_path)).to be_falsey
+ expect(described_class.valid?(design_path)).to be_falsey
end
end
end
@@ -79,6 +84,7 @@ describe Gitlab::GlRepository::RepoType do
expect(described_class).to be_snippet
expect(described_class).not_to be_wiki
expect(described_class).not_to be_project
+ expect(described_class).not_to be_design
end
end
@@ -88,6 +94,7 @@ describe Gitlab::GlRepository::RepoType do
expect(described_class.valid?(wiki_path)).to be_falsey
expect(described_class.valid?(personal_snippet_path)).to be_truthy
expect(described_class.valid?(project_snippet_path)).to be_truthy
+ expect(described_class.valid?(design_path)).to be_falsey
end
end
end
@@ -115,8 +122,38 @@ describe Gitlab::GlRepository::RepoType do
expect(described_class.valid?(wiki_path)).to be_falsey
expect(described_class.valid?(personal_snippet_path)).to be_truthy
expect(described_class.valid?(project_snippet_path)).to be_truthy
+ expect(described_class.valid?(design_path)).to be_falsey
end
end
end
end
+
+ describe Gitlab::GlRepository::DESIGN do
+ it_behaves_like 'a repo type' do
+ let(:expected_identifier) { "design-#{project.id}" }
+ let(:expected_id) { project.id.to_s }
+ let(:expected_suffix) { '.design' }
+ let(:expected_repository) { project.design_repository }
+ let(:expected_container) { project }
+ end
+
+ it 'knows its type' do
+ aggregate_failures do
+ expect(described_class).to be_design
+ expect(described_class).not_to be_project
+ expect(described_class).not_to be_wiki
+ expect(described_class).not_to be_snippet
+ end
+ end
+
+ it 'checks if repository path is valid' do
+ aggregate_failures do
+ expect(described_class.valid?(design_path)).to be_truthy
+ expect(described_class.valid?(project_path)).to be_falsey
+ expect(described_class.valid?(wiki_path)).to be_falsey
+ expect(described_class.valid?(personal_snippet_path)).to be_falsey
+ expect(described_class.valid?(project_snippet_path)).to be_falsey
+ end
+ end
+ end
end
diff --git a/spec/lib/gitlab/gl_repository_spec.rb b/spec/lib/gitlab/gl_repository_spec.rb
index 858f436047e..5f5244b7116 100644
--- a/spec/lib/gitlab/gl_repository_spec.rb
+++ b/spec/lib/gitlab/gl_repository_spec.rb
@@ -19,6 +19,10 @@ describe ::Gitlab::GlRepository do
expect(described_class.parse("snippet-#{snippet.id}")).to eq([snippet, nil, Gitlab::GlRepository::SNIPPET])
end
+ it 'parses a design gl_repository' do
+ expect(described_class.parse("design-#{project.id}")).to eq([project, project, Gitlab::GlRepository::DESIGN])
+ end
+
it 'throws an argument error on an invalid gl_repository type' do
expect { described_class.parse("badformat-#{project.id}") }.to raise_error(ArgumentError)
end
@@ -27,4 +31,15 @@ describe ::Gitlab::GlRepository do
expect { described_class.parse("project-foo") }.to raise_error(ArgumentError)
end
end
+
+ describe 'DESIGN' do
+ it 'uses the design access checker' do
+ expect(described_class::DESIGN.access_checker_class).to eq(::Gitlab::GitAccessDesign)
+ end
+
+ it 'builds a design repository' do
+ expect(described_class::DESIGN.repository_resolver.call(create(:project)))
+ .to be_a(::DesignManagement::Repository)
+ end
+ end
end
diff --git a/spec/lib/gitlab/google_code_import/client_spec.rb b/spec/lib/gitlab/google_code_import/client_spec.rb
index 2e929a62ebc..fb1c7085017 100644
--- a/spec/lib/gitlab/google_code_import/client_spec.rb
+++ b/spec/lib/gitlab/google_code_import/client_spec.rb
@@ -3,7 +3,7 @@
require "spec_helper"
describe Gitlab::GoogleCodeImport::Client do
- let(:raw_data) { JSON.parse(fixture_file("GoogleCodeProjectHosting.json")) }
+ let(:raw_data) { Gitlab::Json.parse(fixture_file("GoogleCodeProjectHosting.json")) }
subject { described_class.new(raw_data) }
diff --git a/spec/lib/gitlab/google_code_import/importer_spec.rb b/spec/lib/gitlab/google_code_import/importer_spec.rb
index 7055df89c09..3118671bb5e 100644
--- a/spec/lib/gitlab/google_code_import/importer_spec.rb
+++ b/spec/lib/gitlab/google_code_import/importer_spec.rb
@@ -4,7 +4,7 @@ require "spec_helper"
describe Gitlab::GoogleCodeImport::Importer do
let(:mapped_user) { create(:user, username: "thilo123") }
- let(:raw_data) { JSON.parse(fixture_file("GoogleCodeProjectHosting.json")) }
+ let(:raw_data) { Gitlab::Json.parse(fixture_file("GoogleCodeProjectHosting.json")) }
let(:client) { Gitlab::GoogleCodeImport::Client.new(raw_data) }
let(:import_data) do
{
diff --git a/spec/lib/gitlab/grape_logging/formatters/lograge_with_timestamp_spec.rb b/spec/lib/gitlab/grape_logging/formatters/lograge_with_timestamp_spec.rb
index d3b108f60ff..84f23bb2ad9 100644
--- a/spec/lib/gitlab/grape_logging/formatters/lograge_with_timestamp_spec.rb
+++ b/spec/lib/gitlab/grape_logging/formatters/lograge_with_timestamp_spec.rb
@@ -30,7 +30,7 @@ describe Gitlab::GrapeLogging::Formatters::LogrageWithTimestamp do
}
end
let(:time) { Time.now }
- let(:result) { JSON.parse(subject) }
+ let(:result) { Gitlab::Json.parse(subject) }
subject { described_class.new.call(:info, time, nil, log_entry) }
diff --git a/spec/lib/gitlab/grape_logging/loggers/cloudflare_logger_spec.rb b/spec/lib/gitlab/grape_logging/loggers/cloudflare_logger_spec.rb
new file mode 100644
index 00000000000..922a433d7ac
--- /dev/null
+++ b/spec/lib/gitlab/grape_logging/loggers/cloudflare_logger_spec.rb
@@ -0,0 +1,31 @@
+# frozen_string_literal: true
+
+require 'spec_helper'
+
+describe Gitlab::GrapeLogging::Loggers::CloudflareLogger do
+ subject { described_class.new }
+
+ describe "#parameters" do
+ let(:mock_request) { ActionDispatch::Request.new({}) }
+ let(:start_time) { Time.new(2018, 01, 01) }
+
+ describe 'with no Cloudflare headers' do
+ it 'returns an empty hash' do
+ expect(subject.parameters(mock_request, nil)).to eq({})
+ end
+ end
+
+ describe 'with Cloudflare headers' do
+ before do
+ mock_request.headers['Cf-Ray'] = SecureRandom.hex
+ mock_request.headers['Cf-Request-Id'] = SecureRandom.hex
+ end
+
+ it 'returns the correct duration in seconds' do
+ data = subject.parameters(mock_request, nil)
+
+ expect(data.keys).to contain_exactly(:cf_ray, :cf_request_id)
+ end
+ end
+ end
+end
diff --git a/spec/lib/gitlab/grape_logging/loggers/exception_logger_spec.rb b/spec/lib/gitlab/grape_logging/loggers/exception_logger_spec.rb
index c9021e2f436..cc9535d4d2c 100644
--- a/spec/lib/gitlab/grape_logging/loggers/exception_logger_spec.rb
+++ b/spec/lib/gitlab/grape_logging/loggers/exception_logger_spec.rb
@@ -3,14 +3,73 @@
require 'spec_helper'
describe Gitlab::GrapeLogging::Loggers::ExceptionLogger do
- subject { described_class.new }
-
let(:mock_request) { OpenStruct.new(env: {}) }
+ let(:response_body) { nil }
describe ".parameters" do
+ subject { described_class.new.parameters(mock_request, response_body) }
+
describe 'when no exception is available' do
it 'returns an empty hash' do
- expect(subject.parameters(mock_request, nil)).to eq({})
+ expect(subject).to eq({})
+ end
+ end
+
+ describe 'with a response' do
+ before do
+ mock_request.env[::API::Helpers::API_RESPONSE_STATUS_CODE] = code
+ end
+
+ context 'with a String response' do
+ let(:response_body) { { message: "something went wrong" }.to_json }
+ let(:code) { 400 }
+ let(:expected) { { api_error: [response_body.to_s] } }
+
+ it 'logs the response body' do
+ expect(subject).to eq(expected)
+ end
+ end
+
+ context 'with an Array response' do
+ let(:response_body) { ["hello world", 1] }
+ let(:code) { 400 }
+ let(:expected) { { api_error: ["hello world", "1"] } }
+
+ it 'casts all elements to strings' do
+ expect(subject).to eq(expected)
+ end
+ end
+
+ # Rack v2.0.9 can return a BodyProxy. This was changed in later versions:
+ # https://github.com/rack/rack/blob/2.0.9/lib/rack/response.rb#L69
+ context 'with a Rack BodyProxy response' do
+ let(:message) { { message: "something went wrong" }.to_json }
+ let(:response) { Rack::Response.new(message, code, {}) }
+ let(:response_body) { Rack::BodyProxy.new(response) }
+ let(:code) { 400 }
+ let(:expected) { { api_error: [message] } }
+
+ it 'logs the response body' do
+ expect(subject).to eq(expected)
+ end
+ end
+
+ context 'unauthorized error' do
+ let(:response_body) { 'unauthorized' }
+ let(:code) { 401 }
+
+ it 'does not log an api_error field' do
+ expect(subject).not_to have_key(:api_error)
+ end
+ end
+
+ context 'HTTP success' do
+ let(:response_body) { 'success' }
+ let(:code) { 200 }
+
+ it 'does not log an api_error field' do
+ expect(subject).not_to have_key(:api_error)
+ end
end
end
@@ -32,7 +91,7 @@ describe Gitlab::GrapeLogging::Loggers::ExceptionLogger do
end
it 'returns the correct fields' do
- expect(subject.parameters(mock_request, nil)).to eq(expected)
+ expect(subject).to eq(expected)
end
context 'with backtrace' do
@@ -43,7 +102,7 @@ describe Gitlab::GrapeLogging::Loggers::ExceptionLogger do
end
it 'includes the backtrace' do
- expect(subject.parameters(mock_request, nil)).to eq(expected)
+ expect(subject).to eq(expected)
end
end
end
diff --git a/spec/lib/gitlab/graphql/authorize/authorize_field_service_spec.rb b/spec/lib/gitlab/graphql/authorize/authorize_field_service_spec.rb
index 98659dbed57..c1dab5feb91 100644
--- a/spec/lib/gitlab/graphql/authorize/authorize_field_service_spec.rb
+++ b/spec/lib/gitlab/graphql/authorize/authorize_field_service_spec.rb
@@ -84,6 +84,16 @@ describe Gitlab::Graphql::Authorize::AuthorizeFieldService do
end
end
+ context 'when the field is a connection' do
+ context 'when it resolves to nil' do
+ let(:field) { type_with_field(Types::QueryType.connection_type, :read_field, nil).fields['testField'].to_graphql }
+
+ it 'does not fail when authorizing' do
+ expect(resolved).to be_nil
+ end
+ end
+ end
+
context 'when the field is a specific type' do
let(:custom_type) { type(:read_type) }
let(:object_in_field) { double('presented in field') }
diff --git a/spec/lib/gitlab/graphql/pagination/keyset/connection_spec.rb b/spec/lib/gitlab/graphql/pagination/keyset/connection_spec.rb
index fdacecbaca6..ba77bc95bb5 100644
--- a/spec/lib/gitlab/graphql/pagination/keyset/connection_spec.rb
+++ b/spec/lib/gitlab/graphql/pagination/keyset/connection_spec.rb
@@ -9,6 +9,14 @@ describe Gitlab::Graphql::Pagination::Keyset::Connection do
let(:schema) { GraphQL::Schema.define(query: query_type, mutation: nil)}
let(:context) { GraphQL::Query::Context.new(query: OpenStruct.new(schema: schema), values: nil, object: nil) }
+ before do
+ stub_const('NoPrimaryKey', Class.new(ActiveRecord::Base))
+ NoPrimaryKey.class_eval do
+ self.table_name = 'no_primary_key'
+ self.primary_key = nil
+ end
+ end
+
subject(:connection) do
described_class.new(nodes, { context: context, max_page_size: 3 }.merge(arguments))
end
@@ -18,7 +26,7 @@ describe Gitlab::Graphql::Pagination::Keyset::Connection do
end
def decoded_cursor(cursor)
- JSON.parse(Base64Bp.urlsafe_decode64(cursor))
+ Gitlab::Json.parse(Base64Bp.urlsafe_decode64(cursor))
end
describe '#cursor_for' do
@@ -303,9 +311,4 @@ describe Gitlab::Graphql::Pagination::Keyset::Connection do
end
end
end
-
- class NoPrimaryKey < ActiveRecord::Base
- self.table_name = 'no_primary_key'
- self.primary_key = nil
- end
end
diff --git a/spec/lib/gitlab/graphql_logger_spec.rb b/spec/lib/gitlab/graphql_logger_spec.rb
index 4977f98b83e..12cb56c78c1 100644
--- a/spec/lib/gitlab/graphql_logger_spec.rb
+++ b/spec/lib/gitlab/graphql_logger_spec.rb
@@ -23,18 +23,18 @@ describe Gitlab::GraphqlLogger do
variables: {},
complexity: 181,
depth: 0,
- duration: 7
+ duration_s: 7
}
output = subject.format_message('INFO', now, 'test', analyzer_memo)
- data = JSON.parse(output)
+ data = Gitlab::Json.parse(output)
expect(data['severity']).to eq('INFO')
expect(data['time']).to eq(now.utc.iso8601(3))
expect(data['complexity']).to eq(181)
expect(data['variables']).to eq({})
expect(data['depth']).to eq(0)
- expect(data['duration']).to eq(7)
+ expect(data['duration_s']).to eq(7)
end
end
end
diff --git a/spec/lib/gitlab/health_checks/master_check_spec.rb b/spec/lib/gitlab/health_checks/master_check_spec.rb
index cb20c1188af..dcfc733d5ad 100644
--- a/spec/lib/gitlab/health_checks/master_check_spec.rb
+++ b/spec/lib/gitlab/health_checks/master_check_spec.rb
@@ -6,10 +6,9 @@ require_relative './simple_check_shared'
describe Gitlab::HealthChecks::MasterCheck do
let(:result_class) { Gitlab::HealthChecks::Result }
- SUCCESS_CODE = 100
- FAILURE_CODE = 101
-
before do
+ stub_const('SUCCESS_CODE', 100)
+ stub_const('FAILURE_CODE', 101)
described_class.register_master
end
diff --git a/spec/lib/gitlab/hook_data/issuable_builder_spec.rb b/spec/lib/gitlab/hook_data/issuable_builder_spec.rb
index cff489e0f3b..afbc48e9ca2 100644
--- a/spec/lib/gitlab/hook_data/issuable_builder_spec.rb
+++ b/spec/lib/gitlab/hook_data/issuable_builder_spec.rb
@@ -12,6 +12,7 @@ describe Gitlab::HookData::IssuableBuilder do
include_examples 'project hook data' do
let(:project) { builder.issuable.project }
end
+
include_examples 'deprecated repository hook data'
context "with a #{kind}" do
diff --git a/spec/lib/gitlab/import_export/all_models.yml b/spec/lib/gitlab/import_export/all_models.yml
index 5d5e2fe2a33..c78b4501310 100644
--- a/spec/lib/gitlab/import_export/all_models.yml
+++ b/spec/lib/gitlab/import_export/all_models.yml
@@ -6,10 +6,12 @@ issues:
- assignees
- updated_by
- milestone
+- iteration
- notes
- resource_label_events
- resource_weight_events
- resource_milestone_events
+- resource_state_events
- sent_notifications
- sentry_issue
- label_links
@@ -18,6 +20,7 @@ issues:
- todos
- user_agent_detail
- moved_to
+- moved_from
- duplicated_to
- promoted_to_epic
- events
@@ -39,6 +42,8 @@ issues:
- related_vulnerabilities
- user_mentions
- system_note_metadata
+- alert_management_alert
+- status_page_published_incident
events:
- author
- project
@@ -111,9 +116,11 @@ merge_requests:
- assignee
- updated_by
- milestone
+- iteration
- notes
- resource_label_events
- resource_milestone_events
+- resource_state_events
- label_links
- labels
- last_edited_by
@@ -212,7 +219,7 @@ ci_pipelines:
- vulnerability_findings
- pipeline_config
- security_scans
-- daily_report_results
+- daily_build_group_report_results
pipeline_variables:
- pipeline
stages:
@@ -222,6 +229,7 @@ stages:
- processables
- builds
- bridges
+- latest_statuses
statuses:
- project
- pipeline
@@ -343,6 +351,7 @@ project:
- labels
- events
- milestones
+- iterations
- notes
- snippets
- hooks
@@ -420,7 +429,6 @@ project:
- mirror_user
- push_rule
- jenkins_service
-- jenkins_deprecated_service
- index_status
- feature_usage
- approval_rules
@@ -443,6 +451,7 @@ project:
- vulnerability_scanners
- operations_feature_flags
- operations_feature_flags_client
+- operations_feature_flags_user_lists
- prometheus_alerts
- prometheus_alert_events
- self_managed_prometheus_alert_events
@@ -477,9 +486,14 @@ project:
- status_page_setting
- requirements
- export_jobs
-- daily_report_results
+- daily_build_group_report_results
- jira_imports
- compliance_framework_setting
+- metrics_users_starred_dashboards
+- alert_management_alerts
+- repository_storage_moves
+- freeze_periods
+- webex_teams_service
award_emoji:
- awardable
- user
@@ -631,3 +645,5 @@ epic_issue:
system_note_metadata:
- note
- description_version
+status_page_published_incident:
+- issue
diff --git a/spec/lib/gitlab/import_export/attribute_configuration_spec.rb b/spec/lib/gitlab/import_export/attribute_configuration_spec.rb
index 58da25bbedb..f97dafc6bf9 100644
--- a/spec/lib/gitlab/import_export/attribute_configuration_spec.rb
+++ b/spec/lib/gitlab/import_export/attribute_configuration_spec.rb
@@ -43,7 +43,4 @@ describe 'Import/Export attribute configuration' do
IMPORT_EXPORT_CONFIG: #{Gitlab::ImportExport.config_file}
MSG
end
-
- class Author < User
- end
end
diff --git a/spec/lib/gitlab/import_export/design_repo_restorer_spec.rb b/spec/lib/gitlab/import_export/design_repo_restorer_spec.rb
new file mode 100644
index 00000000000..5662b8af280
--- /dev/null
+++ b/spec/lib/gitlab/import_export/design_repo_restorer_spec.rb
@@ -0,0 +1,42 @@
+# frozen_string_literal: true
+
+require 'spec_helper'
+
+describe Gitlab::ImportExport::DesignRepoRestorer do
+ include GitHelpers
+
+ describe 'bundle a design Git repo' do
+ let(:user) { create(:user) }
+ let!(:project_with_design_repo) { create(:project, :design_repo) }
+ let!(:project) { create(:project) }
+ let(:export_path) { "#{Dir.tmpdir}/project_tree_saver_spec" }
+ let(:shared) { project.import_export_shared }
+ let(:bundler) { Gitlab::ImportExport::DesignRepoSaver.new(project: project_with_design_repo, shared: shared) }
+ let(:bundle_path) { File.join(shared.export_path, Gitlab::ImportExport.design_repo_bundle_filename) }
+ let(:restorer) do
+ described_class.new(path_to_bundle: bundle_path,
+ shared: shared,
+ project: project)
+ end
+
+ before do
+ allow_next_instance_of(Gitlab::ImportExport) do |instance|
+ allow(instance).to receive(:storage_path).and_return(export_path)
+ end
+
+ bundler.save
+ end
+
+ after do
+ FileUtils.rm_rf(export_path)
+ Gitlab::GitalyClient::StorageSettings.allow_disk_access do
+ FileUtils.rm_rf(project_with_design_repo.design_repository.path_to_repo)
+ FileUtils.rm_rf(project.design_repository.path_to_repo)
+ end
+ end
+
+ it 'restores the repo successfully' do
+ expect(restorer.restore).to eq(true)
+ end
+ end
+end
diff --git a/spec/lib/gitlab/import_export/design_repo_saver_spec.rb b/spec/lib/gitlab/import_export/design_repo_saver_spec.rb
new file mode 100644
index 00000000000..bff48e8b52a
--- /dev/null
+++ b/spec/lib/gitlab/import_export/design_repo_saver_spec.rb
@@ -0,0 +1,37 @@
+# frozen_string_literal: true
+
+require 'spec_helper'
+
+describe Gitlab::ImportExport::DesignRepoSaver do
+ describe 'bundle a design Git repo' do
+ let_it_be(:user) { create(:user) }
+ let_it_be(:design) { create(:design, :with_file, versions_count: 1) }
+ let!(:project) { create(:project, :design_repo) }
+ let(:export_path) { "#{Dir.tmpdir}/project_tree_saver_spec" }
+ let(:shared) { project.import_export_shared }
+ let(:design_bundler) { described_class.new(project: project, shared: shared) }
+
+ before do
+ project.add_maintainer(user)
+ allow_next_instance_of(Gitlab::ImportExport) do |instance|
+ allow(instance).to receive(:storage_path).and_return(export_path)
+ end
+ end
+
+ after do
+ FileUtils.rm_rf(export_path)
+ end
+
+ it 'bundles the repo successfully' do
+ expect(design_bundler.save).to be true
+ end
+
+ context 'when the repo is empty' do
+ let!(:project) { create(:project) }
+
+ it 'bundles the repo successfully' do
+ expect(design_bundler.save).to be true
+ end
+ end
+ end
+end
diff --git a/spec/lib/gitlab/import_export/fast_hash_serializer_spec.rb b/spec/lib/gitlab/import_export/fast_hash_serializer_spec.rb
index 15058684229..916ed692a05 100644
--- a/spec/lib/gitlab/import_export/fast_hash_serializer_spec.rb
+++ b/spec/lib/gitlab/import_export/fast_hash_serializer_spec.rb
@@ -8,7 +8,7 @@ describe Gitlab::ImportExport::FastHashSerializer do
# Wrapping the result into JSON generating/parsing is for making
# the testing more convenient. Doing this, we can check that
# all items are properly serialized while traversing the simple hash.
- subject { JSON.parse(JSON.generate(described_class.new(project, tree).execute)) }
+ subject { Gitlab::Json.parse(Gitlab::Json.generate(described_class.new(project, tree).execute)) }
let!(:project) { setup_project }
let(:user) { create(:user) }
diff --git a/spec/lib/gitlab/import_export/group/legacy_tree_restorer_spec.rb b/spec/lib/gitlab/import_export/group/legacy_tree_restorer_spec.rb
index 3030cdf4cf8..4c926da1436 100644
--- a/spec/lib/gitlab/import_export/group/legacy_tree_restorer_spec.rb
+++ b/spec/lib/gitlab/import_export/group/legacy_tree_restorer_spec.rb
@@ -141,7 +141,7 @@ describe Gitlab::ImportExport::Group::LegacyTreeRestorer do
let(:filepath) { "group_exports/visibility_levels/#{visibility_level}" }
it "imports all subgroups as #{visibility_level}" do
- expect(group.children.map(&:visibility_level)).to eq(expected_visibilities)
+ expect(group.children.map(&:visibility_level)).to match_array(expected_visibilities)
end
end
end
diff --git a/spec/lib/gitlab/import_export/group/tree_restorer_spec.rb b/spec/lib/gitlab/import_export/group/tree_restorer_spec.rb
new file mode 100644
index 00000000000..327f36c664e
--- /dev/null
+++ b/spec/lib/gitlab/import_export/group/tree_restorer_spec.rb
@@ -0,0 +1,184 @@
+# frozen_string_literal: true
+
+require 'spec_helper'
+
+describe Gitlab::ImportExport::Group::TreeRestorer do
+ include ImportExport::CommonUtil
+
+ describe 'restore group tree' do
+ before_all do
+ # Using an admin for import, so we can check assignment of existing members
+ user = create(:admin, email: 'root@gitlabexample.com')
+ create(:user, email: 'adriene.mcclure@gitlabexample.com')
+ create(:user, email: 'gwendolyn_robel@gitlabexample.com')
+
+ RSpec::Mocks.with_temporary_scope do
+ @group = create(:group, name: 'group', path: 'group')
+ @shared = Gitlab::ImportExport::Shared.new(@group)
+
+ setup_import_export_config('group_exports/complex')
+
+ group_tree_restorer = described_class.new(user: user, shared: @shared, group: @group)
+
+ expect(group_tree_restorer.restore).to be_truthy
+ end
+ end
+
+ it 'has the group description' do
+ expect(Group.find_by_path('group').description).to eq('Group Description')
+ end
+
+ it 'has group labels' do
+ expect(@group.labels.count).to eq(10)
+ end
+
+ context 'issue boards' do
+ it 'has issue boards' do
+ expect(@group.boards.count).to eq(1)
+ end
+
+ it 'has board label lists' do
+ lists = @group.boards.find_by(name: 'first board').lists
+
+ expect(lists.count).to eq(3)
+ expect(lists.first.label.title).to eq('TSL')
+ expect(lists.second.label.title).to eq('Sosync')
+ end
+ end
+
+ it 'has badges' do
+ expect(@group.badges.count).to eq(1)
+ end
+
+ it 'has milestones' do
+ expect(@group.milestones.count).to eq(5)
+ end
+
+ it 'has group children' do
+ expect(@group.children.count).to eq(2)
+ end
+
+ it 'has group members' do
+ expect(@group.members.map(&:user).map(&:email)).to contain_exactly(
+ 'root@gitlabexample.com',
+ 'adriene.mcclure@gitlabexample.com',
+ 'gwendolyn_robel@gitlabexample.com'
+ )
+ end
+ end
+
+ context 'child with no parent' do
+ let(:user) { create(:user) }
+ let(:group) { create(:group) }
+ let(:shared) { Gitlab::ImportExport::Shared.new(group) }
+ let(:group_tree_restorer) { described_class.new(user: user, shared: shared, group: group) }
+
+ before do
+ setup_import_export_config('group_exports/child_with_no_parent')
+
+ expect(group_tree_restorer.restore).to be_falsey
+ end
+
+ it 'fails when a child group does not have a valid parent_id' do
+ expect(shared.errors).to include('Parent group not found')
+ end
+ end
+
+ context 'excluded attributes' do
+ let!(:source_user) { create(:user, id: 123) }
+ let!(:importer_user) { create(:user) }
+ let(:group) { create(:group, name: 'user-inputed-name', path: 'user-inputed-path') }
+ let(:shared) { Gitlab::ImportExport::Shared.new(group) }
+ let(:group_tree_restorer) { described_class.new(user: importer_user, shared: shared, group: group) }
+ let(:exported_file) { File.join(shared.export_path, 'tree/groups/4352.json') }
+ let(:group_json) { ActiveSupport::JSON.decode(IO.read(exported_file)) }
+
+ shared_examples 'excluded attributes' do
+ excluded_attributes = %w[
+ id
+ parent_id
+ owner_id
+ created_at
+ updated_at
+ runners_token
+ runners_token_encrypted
+ saml_discovery_token
+ ]
+
+ before do
+ group.add_owner(importer_user)
+
+ setup_import_export_config('group_exports/complex')
+
+ expect(File.exist?(exported_file)).to be_truthy
+
+ group_tree_restorer.restore
+ group.reload
+ end
+
+ it 'does not import root group name' do
+ expect(group.name).to eq('user-inputed-name')
+ end
+
+ it 'does not import root group path' do
+ expect(group.path).to eq('user-inputed-path')
+ end
+
+ excluded_attributes.each do |excluded_attribute|
+ it 'does not allow override of excluded attributes' do
+ unless group.public_send(excluded_attribute).nil?
+ expect(group_json[excluded_attribute]).not_to eq(group.public_send(excluded_attribute))
+ end
+ end
+ end
+ end
+
+ include_examples 'excluded attributes'
+ end
+
+ context 'group.json file access check' do
+ let(:user) { create(:user) }
+ let!(:group) { create(:group, name: 'group2', path: 'group2') }
+ let(:shared) { Gitlab::ImportExport::Shared.new(group) }
+ let(:group_tree_restorer) { described_class.new(user: user, shared: shared, group: group) }
+
+ it 'does not read a symlink' do
+ Dir.mktmpdir do |tmpdir|
+ FileUtils.mkdir_p(File.join(tmpdir, 'tree', 'groups'))
+ setup_symlink(tmpdir, 'tree/groups/_all.ndjson')
+
+ allow(shared).to receive(:export_path).and_return(tmpdir)
+
+ expect(group_tree_restorer.restore).to eq(false)
+ expect(shared.errors).to include('Incorrect JSON format')
+ end
+ end
+ end
+
+ context 'group visibility levels' do
+ let(:user) { create(:user) }
+ let(:shared) { Gitlab::ImportExport::Shared.new(group) }
+ let(:group_tree_restorer) { described_class.new(user: user, shared: shared, group: group) }
+
+ before do
+ setup_import_export_config(filepath)
+
+ group_tree_restorer.restore
+ end
+
+ shared_examples 'with visibility level' do |visibility_level, expected_visibilities|
+ context "when visibility level is #{visibility_level}" do
+ let(:group) { create(:group, visibility_level) }
+ let(:filepath) { "group_exports/visibility_levels/#{visibility_level}" }
+
+ it "imports all subgroups as #{visibility_level}" do
+ expect(group.children.map(&:visibility_level)).to eq(expected_visibilities)
+ end
+ end
+ end
+
+ include_examples 'with visibility level', :public, [20, 10, 0]
+ include_examples 'with visibility level', :private, [0, 0, 0]
+ include_examples 'with visibility level', :internal, [10, 10, 0]
+ end
+end
diff --git a/spec/lib/gitlab/import_export/group/tree_saver_spec.rb b/spec/lib/gitlab/import_export/group/tree_saver_spec.rb
new file mode 100644
index 00000000000..06e8484a3cb
--- /dev/null
+++ b/spec/lib/gitlab/import_export/group/tree_saver_spec.rb
@@ -0,0 +1,140 @@
+# frozen_string_literal: true
+
+require 'spec_helper'
+
+describe Gitlab::ImportExport::Group::TreeSaver do
+ describe 'saves the group tree into a json object' do
+ let_it_be(:user) { create(:user) }
+ let_it_be(:group) { setup_groups }
+
+ let(:shared) { Gitlab::ImportExport::Shared.new(group) }
+ let(:export_path) { "#{Dir.tmpdir}/group_tree_saver_spec" }
+
+ subject(:group_tree_saver) { described_class.new(group: group, current_user: user, shared: shared) }
+
+ before_all do
+ group.add_maintainer(user)
+ end
+
+ before do
+ allow_next_instance_of(Gitlab::ImportExport) do |import_export|
+ allow(import_export).to receive(:storage_path).and_return(export_path)
+ end
+ end
+
+ after do
+ FileUtils.rm_rf(export_path)
+ end
+
+ it 'saves the group successfully' do
+ expect(group_tree_saver.save).to be true
+ end
+
+ it 'fails to export a group' do
+ allow_next_instance_of(Gitlab::ImportExport::JSON::NdjsonWriter) do |ndjson_writer|
+ allow(ndjson_writer).to receive(:write_relation_array).and_raise(RuntimeError, 'exception')
+ end
+
+ expect(shared).to receive(:error).with(RuntimeError).and_call_original
+
+ expect(group_tree_saver.save).to be false
+ end
+
+ context 'exported files' do
+ before do
+ group_tree_saver.save
+ end
+
+ it 'has one group per line' do
+ groups_catalog =
+ File.readlines(exported_path_for('_all.ndjson'))
+ .map { |line| Integer(line) }
+
+ expect(groups_catalog.size).to eq(3)
+ expect(groups_catalog).to eq([
+ group.id,
+ group.descendants.first.id,
+ group.descendants.first.descendants.first.id
+ ])
+ end
+
+ it 'has a file per group' do
+ group.self_and_descendants.pluck(:id).each do |id|
+ group_attributes_file = exported_path_for("#{id}.json")
+
+ expect(File.exist?(group_attributes_file)).to be(true)
+ end
+ end
+
+ context 'group attributes file' do
+ let(:group_attributes_file) { exported_path_for("#{group.id}.json") }
+ let(:group_attributes) { ::JSON.parse(File.read(group_attributes_file)) }
+
+ it 'has a file for each group with its attributes' do
+ expect(group_attributes['description']).to eq(group.description)
+ expect(group_attributes['parent_id']).to eq(group.parent_id)
+ end
+
+ shared_examples 'excluded attributes' do
+ excluded_attributes = %w[
+ owner_id
+ created_at
+ updated_at
+ runners_token
+ runners_token_encrypted
+ saml_discovery_token
+ ]
+
+ excluded_attributes.each do |excluded_attribute|
+ it 'does not contain excluded attribute' do
+ expect(group_attributes).not_to include(excluded_attribute => group.public_send(excluded_attribute))
+ end
+ end
+ end
+
+ include_examples 'excluded attributes'
+ end
+
+ it 'has a file for each group association' do
+ group.self_and_descendants do |g|
+ %w[
+ badges
+ boards
+ epics
+ labels
+ members
+ milestones
+ ].each do |association|
+ path = exported_path_for("#{g.id}", "#{association}.ndjson")
+ expect(File.exist?(path)).to eq(true), "#{path} does not exist"
+ end
+ end
+ end
+ end
+ end
+
+ def exported_path_for(*file)
+ File.join(group_tree_saver.full_path, 'groups', *file)
+ end
+
+ def setup_groups
+ root = setup_group
+ subgroup = setup_group(parent: root)
+ setup_group(parent: subgroup)
+
+ root
+ end
+
+ def setup_group(parent: nil)
+ group = create(:group, description: 'description', parent: parent)
+ create(:milestone, group: group)
+ create(:group_badge, group: group)
+ group_label = create(:group_label, group: group)
+ board = create(:board, group: group, milestone_id: Milestone::Upcoming.id)
+ create(:list, board: board, label: group_label)
+ create(:group_badge, group: group)
+ create(:label_priority, label: group_label, priority: 1)
+
+ group
+ end
+end
diff --git a/spec/lib/gitlab/import_export/import_export_equivalence_spec.rb b/spec/lib/gitlab/import_export/import_export_equivalence_spec.rb
index 707975f20b6..95df9cd0e6e 100644
--- a/spec/lib/gitlab/import_export/import_export_equivalence_spec.rb
+++ b/spec/lib/gitlab/import_export/import_export_equivalence_spec.rb
@@ -46,8 +46,8 @@ describe Gitlab::ImportExport do
export_path: test_tmp_path)
).to be true
- imported_json = JSON.parse(File.read("#{test_fixture_path}/project.json"))
- exported_json = JSON.parse(File.read("#{test_tmp_path}/project.json"))
+ imported_json = Gitlab::Json.parse(File.read("#{test_fixture_path}/project.json"))
+ exported_json = Gitlab::Json.parse(File.read("#{test_tmp_path}/project.json"))
assert_relations_match(imported_json, exported_json)
end
diff --git a/spec/lib/gitlab/import_export/import_test_coverage_spec.rb b/spec/lib/gitlab/import_export/import_test_coverage_spec.rb
index 335b0031147..038b95809b4 100644
--- a/spec/lib/gitlab/import_export/import_test_coverage_spec.rb
+++ b/spec/lib/gitlab/import_export/import_test_coverage_spec.rb
@@ -53,22 +53,15 @@ describe 'Test coverage of the Project Import' do
].freeze
# A list of JSON fixture files we use to test Import.
- # Note that we use separate fixture to test ee-only features.
# Most of the relations are present in `complex/project.json`
# which is our main fixture.
- PROJECT_JSON_FIXTURES_EE =
- if Gitlab.ee?
- ['ee/spec/fixtures/lib/gitlab/import_export/designs/project.json'].freeze
- else
- []
- end
-
PROJECT_JSON_FIXTURES = [
'spec/fixtures/lib/gitlab/import_export/complex/project.json',
'spec/fixtures/lib/gitlab/import_export/group/project.json',
'spec/fixtures/lib/gitlab/import_export/light/project.json',
- 'spec/fixtures/lib/gitlab/import_export/milestone-iid/project.json'
- ].freeze + PROJECT_JSON_FIXTURES_EE
+ 'spec/fixtures/lib/gitlab/import_export/milestone-iid/project.json',
+ 'spec/fixtures/lib/gitlab/import_export/designs/project.json'
+ ].freeze
it 'ensures that all imported/exported relations are present in test JSONs' do
not_tested_relations = (relations_from_config - tested_relations) - MUTED_RELATIONS
diff --git a/spec/lib/gitlab/import_export/importer_spec.rb b/spec/lib/gitlab/import_export/importer_spec.rb
index e03c95525df..60179146416 100644
--- a/spec/lib/gitlab/import_export/importer_spec.rb
+++ b/spec/lib/gitlab/import_export/importer_spec.rb
@@ -51,7 +51,8 @@ describe Gitlab::ImportExport::Importer do
Gitlab::ImportExport::UploadsRestorer,
Gitlab::ImportExport::LfsRestorer,
Gitlab::ImportExport::StatisticsRestorer,
- Gitlab::ImportExport::SnippetsRepoRestorer
+ Gitlab::ImportExport::SnippetsRepoRestorer,
+ Gitlab::ImportExport::DesignRepoRestorer
].each do |restorer|
it "calls the #{restorer}" do
fake_restorer = double(restorer.to_s)
@@ -89,36 +90,74 @@ describe Gitlab::ImportExport::Importer do
end
context 'when project successfully restored' do
- let!(:existing_project) { create(:project, namespace: user.namespace) }
- let(:project) { create(:project, namespace: user.namespace, name: 'whatever', path: 'whatever') }
+ context "with a project in a user's namespace" do
+ let!(:existing_project) { create(:project, namespace: user.namespace) }
+ let(:project) { create(:project, namespace: user.namespace, name: 'whatever', path: 'whatever') }
- before do
- restorers = double(:restorers, all?: true)
+ before do
+ restorers = double(:restorers, all?: true)
- allow(subject).to receive(:import_file).and_return(true)
- allow(subject).to receive(:check_version!).and_return(true)
- allow(subject).to receive(:restorers).and_return(restorers)
- allow(project).to receive(:import_data).and_return(double(data: { 'original_path' => existing_project.path }))
+ allow(subject).to receive(:import_file).and_return(true)
+ allow(subject).to receive(:check_version!).and_return(true)
+ allow(subject).to receive(:restorers).and_return(restorers)
+ allow(project).to receive(:import_data).and_return(double(data: { 'original_path' => existing_project.path }))
+ end
+
+ context 'when import_data' do
+ context 'has original_path' do
+ it 'overwrites existing project' do
+ expect_next_instance_of(::Projects::OverwriteProjectService) do |service|
+ expect(service).to receive(:execute).with(existing_project)
+ end
+
+ subject.execute
+ end
+ end
+
+ context 'has not original_path' do
+ before do
+ allow(project).to receive(:import_data).and_return(double(data: {}))
+ end
+
+ it 'does not call the overwrite service' do
+ expect(::Projects::OverwriteProjectService).not_to receive(:new)
+
+ subject.execute
+ end
+ end
+ end
end
- context 'when import_data' do
+ context "with a project in a group namespace" do
+ let(:group) { create(:group) }
+ let!(:existing_project) { create(:project, group: group) }
+ let(:project) { create(:project, creator: user, group: group, name: 'whatever', path: 'whatever') }
+
+ before do
+ restorers = double(:restorers, all?: true)
+
+ allow(subject).to receive(:import_file).and_return(true)
+ allow(subject).to receive(:check_version!).and_return(true)
+ allow(subject).to receive(:restorers).and_return(restorers)
+ allow(project).to receive(:import_data).and_return(double(data: { 'original_path' => existing_project.path }))
+ end
+
context 'has original_path' do
it 'overwrites existing project' do
- expect_any_instance_of(::Projects::OverwriteProjectService).to receive(:execute).with(existing_project)
+ group.add_owner(user)
- subject.execute
- end
- end
+ expect_next_instance_of(::Projects::OverwriteProjectService) do |service|
+ expect(service).to receive(:execute).with(existing_project)
+ end
- context 'has not original_path' do
- before do
- allow(project).to receive(:import_data).and_return(double(data: {}))
+ subject.execute
end
- it 'does not call the overwrite service' do
- expect_any_instance_of(::Projects::OverwriteProjectService).not_to receive(:execute).with(existing_project)
+ it 'does not allow user to overwrite existing project' do
+ expect(::Projects::OverwriteProjectService).not_to receive(:new)
- subject.execute
+ expect { subject.execute }.to raise_error(Projects::ImportService::Error,
+ "User #{user.username} (#{user.id}) cannot overwrite a project in #{group.path}")
end
end
end
diff --git a/spec/lib/gitlab/import_export/json/legacy_reader/file_spec.rb b/spec/lib/gitlab/import_export/json/legacy_reader/file_spec.rb
index 1021ce3cd50..99932404fd9 100644
--- a/spec/lib/gitlab/import_export/json/legacy_reader/file_spec.rb
+++ b/spec/lib/gitlab/import_export/json/legacy_reader/file_spec.rb
@@ -7,7 +7,7 @@ describe Gitlab::ImportExport::JSON::LegacyReader::File do
it_behaves_like 'import/export json legacy reader' do
let(:valid_path) { 'spec/fixtures/lib/gitlab/import_export/light/project.json' }
let(:data) { valid_path }
- let(:json_data) { JSON.parse(File.read(valid_path)) }
+ let(:json_data) { Gitlab::Json.parse(File.read(valid_path)) }
end
describe '#exist?' do
diff --git a/spec/lib/gitlab/import_export/json/legacy_reader/hash_spec.rb b/spec/lib/gitlab/import_export/json/legacy_reader/hash_spec.rb
index 8c4dfd2f356..e793dc7339d 100644
--- a/spec/lib/gitlab/import_export/json/legacy_reader/hash_spec.rb
+++ b/spec/lib/gitlab/import_export/json/legacy_reader/hash_spec.rb
@@ -9,8 +9,8 @@ describe Gitlab::ImportExport::JSON::LegacyReader::Hash do
# the hash is modified by the `LegacyReader`
# we need to deep-dup it
- let(:json_data) { JSON.parse(File.read(path)) }
- let(:data) { JSON.parse(File.read(path)) }
+ let(:json_data) { Gitlab::Json.parse(File.read(path)) }
+ let(:data) { Gitlab::Json.parse(File.read(path)) }
end
describe '#exist?' do
diff --git a/spec/lib/gitlab/import_export/json/ndjson_reader_spec.rb b/spec/lib/gitlab/import_export/json/ndjson_reader_spec.rb
index 40b784fdb87..34e8b1ddd59 100644
--- a/spec/lib/gitlab/import_export/json/ndjson_reader_spec.rb
+++ b/spec/lib/gitlab/import_export/json/ndjson_reader_spec.rb
@@ -6,18 +6,10 @@ describe Gitlab::ImportExport::JSON::NdjsonReader do
include ImportExport::CommonUtil
let(:fixture) { 'spec/fixtures/lib/gitlab/import_export/light/tree' }
- let(:root_tree) { JSON.parse(File.read(File.join(fixture, 'project.json'))) }
+ let(:root_tree) { Gitlab::Json.parse(File.read(File.join(fixture, 'project.json'))) }
let(:ndjson_reader) { described_class.new(dir_path) }
let(:importable_path) { 'project' }
- before :all do
- extract_archive('spec/fixtures/lib/gitlab/import_export/light', 'tree.tar.gz')
- end
-
- after :all do
- cleanup_artifacts_from_extract_archive('light')
- end
-
describe '#exist?' do
subject { ndjson_reader.exist? }
@@ -101,8 +93,8 @@ describe Gitlab::ImportExport::JSON::NdjsonReader do
context 'relation file contains multiple lines' do
let(:key) { 'custom_attributes' }
- let(:attr_1) { JSON.parse('{"id":201,"project_id":5,"created_at":"2016-06-14T15:01:51.315Z","updated_at":"2016-06-14T15:01:51.315Z","key":"color","value":"red"}') }
- let(:attr_2) { JSON.parse('{"id":202,"project_id":5,"created_at":"2016-06-14T15:01:51.315Z","updated_at":"2016-06-14T15:01:51.315Z","key":"size","value":"small"}') }
+ let(:attr_1) { Gitlab::Json.parse('{"id":201,"project_id":5,"created_at":"2016-06-14T15:01:51.315Z","updated_at":"2016-06-14T15:01:51.315Z","key":"color","value":"red"}') }
+ let(:attr_2) { Gitlab::Json.parse('{"id":202,"project_id":5,"created_at":"2016-06-14T15:01:51.315Z","updated_at":"2016-06-14T15:01:51.315Z","key":"size","value":"small"}') }
it 'yields every relation value to the Enumerator' do
expect(subject.to_a).to eq([[attr_1, 0], [attr_2, 1]])
diff --git a/spec/lib/gitlab/import_export/lfs_saver_spec.rb b/spec/lib/gitlab/import_export/lfs_saver_spec.rb
index a8ff7867410..e9d06573e70 100644
--- a/spec/lib/gitlab/import_export/lfs_saver_spec.rb
+++ b/spec/lib/gitlab/import_export/lfs_saver_spec.rb
@@ -26,7 +26,7 @@ describe Gitlab::ImportExport::LfsSaver do
let(:lfs_json_file) { File.join(shared.export_path, Gitlab::ImportExport.lfs_objects_filename) }
def lfs_json
- JSON.parse(IO.read(lfs_json_file))
+ Gitlab::Json.parse(IO.read(lfs_json_file))
end
before do
diff --git a/spec/lib/gitlab/import_export/project/export_task_spec.rb b/spec/lib/gitlab/import_export/project/export_task_spec.rb
index cf11a1df33c..dc8eb54dc14 100644
--- a/spec/lib/gitlab/import_export/project/export_task_spec.rb
+++ b/spec/lib/gitlab/import_export/project/export_task_spec.rb
@@ -3,13 +3,14 @@
require 'rake_helper'
describe Gitlab::ImportExport::Project::ExportTask do
- let(:username) { 'root' }
+ let_it_be(:username) { 'root' }
let(:namespace_path) { username }
- let!(:user) { create(:user, username: username) }
+ let_it_be(:user) { create(:user, username: username) }
let(:measurement_enabled) { false }
let(:file_path) { 'spec/fixtures/gitlab/import_export/test_project_export.tar.gz' }
let(:project) { create(:project, creator: user, namespace: user.namespace) }
let(:project_name) { project.name }
+ let(:rake_task) { described_class.new(task_params) }
let(:task_params) do
{
@@ -21,7 +22,7 @@ describe Gitlab::ImportExport::Project::ExportTask do
}
end
- subject { described_class.new(task_params).export }
+ subject { rake_task.export }
context 'when project is found' do
let(:project) { create(:project, creator: user, namespace: user.namespace) }
@@ -29,9 +30,13 @@ describe Gitlab::ImportExport::Project::ExportTask do
around do |example|
example.run
ensure
- File.delete(file_path)
+ File.delete(file_path) if File.exist?(file_path)
end
+ include_context 'rake task object storage shared context'
+
+ it_behaves_like 'rake task with disabled object_storage', ::Projects::ImportExport::ExportService, :success
+
it 'performs project export successfully' do
expect { subject }.to output(/Done!/).to_stdout
@@ -39,8 +44,6 @@ describe Gitlab::ImportExport::Project::ExportTask do
expect(File).to exist(file_path)
end
-
- it_behaves_like 'measurable'
end
context 'when project is not found' do
@@ -66,4 +69,32 @@ describe Gitlab::ImportExport::Project::ExportTask do
expect(subject).to eq(false)
end
end
+
+ context 'when after export strategy fails' do
+ before do
+ allow_next_instance_of(Gitlab::ImportExport::AfterExportStrategies::MoveFileStrategy) do |after_export_strategy|
+ allow(after_export_strategy).to receive(:strategy_execute).and_raise(Gitlab::ImportExport::AfterExportStrategies::BaseAfterExportStrategy::StrategyError)
+ end
+ end
+
+ it 'error is logged' do
+ expect(rake_task).to receive(:error).and_call_original
+
+ expect(subject).to eq(false)
+ end
+ end
+
+ context 'when saving services fail' do
+ before do
+ allow_next_instance_of(::Projects::ImportExport::ExportService) do |service|
+ allow(service).to receive(:execute).and_raise(Gitlab::ImportExport::Error)
+ end
+ end
+
+ it 'error is logged' do
+ expect(rake_task).to receive(:error).and_call_original
+
+ expect(subject).to eq(false)
+ end
+ end
end
diff --git a/spec/lib/gitlab/import_export/project/import_task_spec.rb b/spec/lib/gitlab/import_export/project/import_task_spec.rb
index 4f4fcd3ad8a..7c11161aaa7 100644
--- a/spec/lib/gitlab/import_export/project/import_task_spec.rb
+++ b/spec/lib/gitlab/import_export/project/import_task_spec.rb
@@ -8,7 +8,7 @@ describe Gitlab::ImportExport::Project::ImportTask, :request_store do
let!(:user) { create(:user, username: username) }
let(:measurement_enabled) { false }
let(:project) { Project.find_by_full_path("#{namespace_path}/#{project_name}") }
- let(:import_task) { described_class.new(task_params) }
+ let(:rake_task) { described_class.new(task_params) }
let(:task_params) do
{
username: username,
@@ -19,29 +19,16 @@ describe Gitlab::ImportExport::Project::ImportTask, :request_store do
}
end
- before do
- allow(Settings.uploads.object_store).to receive(:[]=).and_call_original
- end
-
- around do |example|
- old_direct_upload_setting = Settings.uploads.object_store['direct_upload']
- old_background_upload_setting = Settings.uploads.object_store['background_upload']
-
- Settings.uploads.object_store['direct_upload'] = true
- Settings.uploads.object_store['background_upload'] = true
-
- example.run
-
- Settings.uploads.object_store['direct_upload'] = old_direct_upload_setting
- Settings.uploads.object_store['background_upload'] = old_background_upload_setting
- end
-
- subject { import_task.import }
+ subject { rake_task.import }
context 'when project import is valid' do
let(:project_name) { 'import_rake_test_project' }
let(:file_path) { 'spec/fixtures/gitlab/import_export/lightweight_project_export.tar.gz' }
+ include_context 'rake task object storage shared context'
+
+ it_behaves_like 'rake task with disabled object_storage', ::Projects::GitlabProjectsImportService, :execute_sidekiq_job
+
it 'performs project import successfully' do
expect { subject }.to output(/Done!/).to_stdout
expect { subject }.not_to raise_error
@@ -52,30 +39,6 @@ describe Gitlab::ImportExport::Project::ImportTask, :request_store do
expect(project.milestones.count).to be > 0
expect(project.import_state.status).to eq('finished')
end
-
- it 'disables direct & background upload only during project creation' do
- expect_next_instance_of(Projects::GitlabProjectsImportService) do |service|
- expect(service).to receive(:execute).and_wrap_original do |m|
- expect(Settings.uploads.object_store['background_upload']).to eq(false)
- expect(Settings.uploads.object_store['direct_upload']).to eq(false)
-
- m.call
- end
- end
-
- expect(import_task).to receive(:execute_sidekiq_job).and_wrap_original do |m|
- expect(Settings.uploads.object_store['background_upload']).to eq(true)
- expect(Settings.uploads.object_store['direct_upload']).to eq(true)
- expect(Settings.uploads.object_store).not_to receive(:[]=).with('backgroud_upload', false)
- expect(Settings.uploads.object_store).not_to receive(:[]=).with('direct_upload', false)
-
- m.call
- end
-
- subject
- end
-
- it_behaves_like 'measurable'
end
context 'when project import is invalid' do
diff --git a/spec/lib/gitlab/import_export/project/tree_restorer_spec.rb b/spec/lib/gitlab/import_export/project/tree_restorer_spec.rb
index 04e8bd05666..58589a7bbbe 100644
--- a/spec/lib/gitlab/import_export/project/tree_restorer_spec.rb
+++ b/spec/lib/gitlab/import_export/project/tree_restorer_spec.rb
@@ -8,6 +8,7 @@ end
describe Gitlab::ImportExport::Project::TreeRestorer do
include ImportExport::CommonUtil
+ using RSpec::Parameterized::TableSyntax
let(:shared) { project.import_export_shared }
@@ -44,10 +45,6 @@ describe Gitlab::ImportExport::Project::TreeRestorer do
end
end
- after(:context) do
- cleanup_artifacts_from_extract_archive('complex')
- end
-
context 'JSON' do
it 'restores models based on JSON' do
expect(@restored_project_json).to be_truthy
@@ -536,10 +533,6 @@ describe Gitlab::ImportExport::Project::TreeRestorer do
expect(restored_project_json).to eq(true)
end
- after do
- cleanup_artifacts_from_extract_archive('light')
- end
-
it 'issue system note metadata restored successfully' do
note_content = 'created merge request !1 to address this issue'
note = project.issues.first.notes.select { |n| n.note.match(/#{note_content}/)}.first
@@ -586,10 +579,6 @@ describe Gitlab::ImportExport::Project::TreeRestorer do
expect(restored_project_json).to eq(true)
end
- after do
- cleanup_artifacts_from_extract_archive('multi_pipeline_ref_one_external_pr')
- end
-
it_behaves_like 'restores project successfully',
issues: 0,
labels: 0,
@@ -620,10 +609,6 @@ describe Gitlab::ImportExport::Project::TreeRestorer do
.and_raise(exception)
end
- after do
- cleanup_artifacts_from_extract_archive('light')
- end
-
it 'report post import error' do
expect(restored_project_json).to eq(false)
expect(shared.errors).to include('post_import_error')
@@ -646,10 +631,6 @@ describe Gitlab::ImportExport::Project::TreeRestorer do
expect(restored_project_json).to eq(true)
end
- after do
- cleanup_artifacts_from_extract_archive('light')
- end
-
it_behaves_like 'restores project successfully',
issues: 1,
labels: 2,
@@ -678,10 +659,6 @@ describe Gitlab::ImportExport::Project::TreeRestorer do
setup_reader(reader)
end
- after do
- cleanup_artifacts_from_extract_archive('light')
- end
-
it 'handles string versions of visibility_level' do
# Project needs to be in a group for visibility level comparison
# to happen
@@ -747,10 +724,6 @@ describe Gitlab::ImportExport::Project::TreeRestorer do
expect(restored_project_json).to eq(true)
end
- after do
- cleanup_artifacts_from_extract_archive('group')
- end
-
it_behaves_like 'restores project successfully',
issues: 3,
labels: 2,
@@ -784,10 +757,6 @@ describe Gitlab::ImportExport::Project::TreeRestorer do
setup_reader(reader)
end
- after do
- cleanup_artifacts_from_extract_archive('light')
- end
-
it 'does not import any templated services' do
expect(restored_project_json).to eq(true)
@@ -835,10 +804,6 @@ describe Gitlab::ImportExport::Project::TreeRestorer do
setup_reader(reader)
end
- after do
- cleanup_artifacts_from_extract_archive('milestone-iid')
- end
-
it 'preserves the project milestone IID' do
expect_any_instance_of(Gitlab::ImportExport::Shared).not_to receive(:error)
@@ -855,10 +820,6 @@ describe Gitlab::ImportExport::Project::TreeRestorer do
setup_reader(reader)
end
- after do
- cleanup_artifacts_from_extract_archive('light')
- end
-
it 'converts empty external classification authorization labels to nil' do
project.create_import_data(data: { override_params: { external_authorization_classification_label: "" } })
@@ -1004,10 +965,6 @@ describe Gitlab::ImportExport::Project::TreeRestorer do
subject
end
- after do
- cleanup_artifacts_from_extract_archive('with_invalid_records')
- end
-
context 'when failures occur because a relation fails to be processed' do
it_behaves_like 'restores project successfully',
issues: 0,
@@ -1031,6 +988,69 @@ describe Gitlab::ImportExport::Project::TreeRestorer do
end
end
end
+
+ context 'JSON with design management data' do
+ let_it_be(:user) { create(:admin, email: 'user_1@gitlabexample.com') }
+ let_it_be(:second_user) { create(:user, email: 'user_2@gitlabexample.com') }
+ let_it_be(:project) do
+ create(:project, :builds_disabled, :issues_disabled,
+ { name: 'project', path: 'project' })
+ end
+ let(:shared) { project.import_export_shared }
+ let(:project_tree_restorer) { described_class.new(user: user, shared: shared, project: project) }
+
+ subject(:restored_project_json) { project_tree_restorer.restore }
+
+ before do
+ setup_import_export_config('designs')
+ restored_project_json
+ end
+
+ it_behaves_like 'restores project successfully', issues: 2
+
+ it 'restores project associations correctly' do
+ expect(project.designs.size).to eq(7)
+ end
+
+ describe 'restores issue associations correctly' do
+ let(:issue) { project.issues.offset(index).first }
+
+ where(:index, :design_filenames, :version_shas, :events, :author_emails) do
+ 0 | %w[chirrido3.jpg jonathan_richman.jpg mariavontrap.jpeg] | %w[27702d08f5ee021ae938737f84e8fe7c38599e85 9358d1bac8ff300d3d2597adaa2572a20f7f8703 e1a4a501bcb42f291f84e5d04c8f927821542fb6] | %w[creation creation creation modification modification deletion] | %w[user_1@gitlabexample.com user_1@gitlabexample.com user_2@gitlabexample.com]
+ 1 | ['1 (1).jpeg', '2099743.jpg', 'a screenshot (1).jpg', 'chirrido3.jpg'] | %w[73f871b4c8c1d65c62c460635e023179fb53abc4 8587e78ab6bda3bc820a9f014c3be4a21ad4fcc8 c9b5f067f3e892122a4b12b0a25a8089192f3ac8] | %w[creation creation creation creation modification] | %w[user_1@gitlabexample.com user_2@gitlabexample.com user_2@gitlabexample.com]
+ end
+
+ with_them do
+ it do
+ expect(issue.designs.pluck(:filename)).to contain_exactly(*design_filenames)
+ expect(issue.design_versions.pluck(:sha)).to contain_exactly(*version_shas)
+ expect(issue.design_versions.flat_map(&:actions).map(&:event)).to contain_exactly(*events)
+ expect(issue.design_versions.map(&:author).map(&:email)).to contain_exactly(*author_emails)
+ end
+ end
+ end
+
+ describe 'restores design version associations correctly' do
+ let(:project_designs) { project.designs.reorder(:filename, :issue_id) }
+ let(:design) { project_designs.offset(index).first }
+
+ where(:index, :version_shas) do
+ 0 | %w[73f871b4c8c1d65c62c460635e023179fb53abc4 c9b5f067f3e892122a4b12b0a25a8089192f3ac8]
+ 1 | %w[73f871b4c8c1d65c62c460635e023179fb53abc4]
+ 2 | %w[c9b5f067f3e892122a4b12b0a25a8089192f3ac8]
+ 3 | %w[27702d08f5ee021ae938737f84e8fe7c38599e85 9358d1bac8ff300d3d2597adaa2572a20f7f8703 e1a4a501bcb42f291f84e5d04c8f927821542fb6]
+ 4 | %w[8587e78ab6bda3bc820a9f014c3be4a21ad4fcc8]
+ 5 | %w[27702d08f5ee021ae938737f84e8fe7c38599e85 e1a4a501bcb42f291f84e5d04c8f927821542fb6]
+ 6 | %w[27702d08f5ee021ae938737f84e8fe7c38599e85]
+ end
+
+ with_them do
+ it do
+ expect(design.versions.pluck(:sha)).to contain_exactly(*version_shas)
+ end
+ end
+ end
+ end
end
context 'enable ndjson import' do
diff --git a/spec/lib/gitlab/import_export/project/tree_saver_spec.rb b/spec/lib/gitlab/import_export/project/tree_saver_spec.rb
index 8adc360026d..b9bfe253f10 100644
--- a/spec/lib/gitlab/import_export/project/tree_saver_spec.rb
+++ b/spec/lib/gitlab/import_export/project/tree_saver_spec.rb
@@ -168,6 +168,28 @@ describe Gitlab::ImportExport::Project::TreeSaver do
it 'has issue resource label events' do
expect(subject.first['resource_label_events']).not_to be_empty
end
+
+ it 'saves the issue designs correctly' do
+ expect(subject.first['designs'].size).to eq(1)
+ end
+
+ it 'saves the issue design notes correctly' do
+ expect(subject.first['designs'].first['notes']).not_to be_empty
+ end
+
+ it 'saves the issue design versions correctly' do
+ issue_json = subject.first
+ actions = issue_json['design_versions'].flat_map { |v| v['actions'] }
+
+ expect(issue_json['design_versions'].size).to eq(2)
+ issue_json['design_versions'].each do |version|
+ expect(version['author_id']).to be_kind_of(Integer)
+ end
+ expect(actions.size).to eq(2)
+ actions.each do |action|
+ expect(action['design']).to be_present
+ end
+ end
end
context 'with ci_pipelines' do
@@ -442,6 +464,9 @@ describe Gitlab::ImportExport::Project::TreeSaver do
board = create(:board, project: project, name: 'TestBoard')
create(:list, board: board, position: 0, label: project_label)
+ design = create(:design, :with_file, versions_count: 2, issue: issue)
+ create(:diff_note_on_design, noteable: design, project: project, author: user)
+
project
end
end
diff --git a/spec/lib/gitlab/import_export/relation_tree_restorer_spec.rb b/spec/lib/gitlab/import_export/relation_tree_restorer_spec.rb
index 0b58a75220d..8fe419da450 100644
--- a/spec/lib/gitlab/import_export/relation_tree_restorer_spec.rb
+++ b/spec/lib/gitlab/import_export/relation_tree_restorer_spec.rb
@@ -64,7 +64,7 @@ describe Gitlab::ImportExport::RelationTreeRestorer do
shared_examples 'logging of relations creation' do
context 'when log_import_export_relation_creation feature flag is enabled' do
before do
- stub_feature_flags(log_import_export_relation_creation: { enabled: true, thing: group })
+ stub_feature_flags(log_import_export_relation_creation: group)
end
it 'logs top-level relation creation' do
@@ -79,7 +79,7 @@ describe Gitlab::ImportExport::RelationTreeRestorer do
context 'when log_import_export_relation_creation feature flag is disabled' do
before do
- stub_feature_flags(log_import_export_relation_creation: { enabled: false, thing: group })
+ stub_feature_flags(log_import_export_relation_creation: false)
end
it 'does not log top-level relation creation' do
@@ -126,14 +126,6 @@ describe Gitlab::ImportExport::RelationTreeRestorer do
let(:path) { 'spec/fixtures/lib/gitlab/import_export/complex/tree' }
let(:relation_reader) { Gitlab::ImportExport::JSON::NdjsonReader.new(path) }
- before :all do
- extract_archive('spec/fixtures/lib/gitlab/import_export/complex', 'tree.tar.gz')
- end
-
- after :all do
- cleanup_artifacts_from_extract_archive('complex')
- end
-
it_behaves_like 'import project successfully'
end
end
@@ -156,7 +148,7 @@ describe Gitlab::ImportExport::RelationTreeRestorer do
let(:reader) do
Gitlab::ImportExport::Reader.new(
shared: shared,
- config: Gitlab::ImportExport::Config.new(config: Gitlab::ImportExport.group_config_file).to_h
+ config: Gitlab::ImportExport::Config.new(config: Gitlab::ImportExport.legacy_group_config_file).to_h
)
end
diff --git a/spec/lib/gitlab/import_export/safe_model_attributes.yml b/spec/lib/gitlab/import_export/safe_model_attributes.yml
index 88d7fdaef36..c29a85ce624 100644
--- a/spec/lib/gitlab/import_export/safe_model_attributes.yml
+++ b/spec/lib/gitlab/import_export/safe_model_attributes.yml
@@ -480,6 +480,7 @@ Service:
- pipeline_events
- job_events
- comment_on_event_enabled
+- comment_detail
- category
- default
- wiki_page_events
@@ -487,6 +488,7 @@ Service:
- confidential_note_events
- deployment_events
- description
+- inherit_from_id
ProjectHook:
- id
- url
diff --git a/spec/lib/gitlab/instrumentation_helper_spec.rb b/spec/lib/gitlab/instrumentation_helper_spec.rb
index 858fa044a52..fdb842dac0f 100644
--- a/spec/lib/gitlab/instrumentation_helper_spec.rb
+++ b/spec/lib/gitlab/instrumentation_helper_spec.rb
@@ -49,12 +49,12 @@ describe Gitlab::InstrumentationHelper do
describe '.queue_duration_for_job' do
where(:enqueued_at, :created_at, :time_now, :expected_duration) do
"2019-06-01T00:00:00.000+0000" | nil | "2019-06-01T02:00:00.000+0000" | 2.hours.to_f
- "2019-06-01T02:00:00.000+0000" | nil | "2019-06-01T02:00:00.001+0000" | 0.0
+ "2019-06-01T02:00:00.000+0000" | nil | "2019-06-01T02:00:00.001+0000" | 0.001
"2019-06-01T02:00:00.000+0000" | "2019-05-01T02:00:00.000+0000" | "2019-06-01T02:00:01.000+0000" | 1
- nil | "2019-06-01T02:00:00.000+0000" | "2019-06-01T02:00:00.001+0000" | 0.0
+ nil | "2019-06-01T02:00:00.000+0000" | "2019-06-01T02:00:00.001+0000" | 0.001
nil | nil | "2019-06-01T02:00:00.001+0000" | nil
"2019-06-01T02:00:00.000+0200" | nil | "2019-06-01T02:00:00.000-0200" | 4.hours.to_f
- 1571825569.998168 | nil | "2019-10-23T12:13:16.000+0200" | 26.00
+ 1571825569.998168 | nil | "2019-10-23T12:13:16.000+0200" | 26.001832
1571825569 | nil | "2019-10-23T12:13:16.000+0200" | 27
"invalid_date" | nil | "2019-10-23T12:13:16.000+0200" | nil
"" | nil | "2019-10-23T12:13:16.000+0200" | nil
diff --git a/spec/lib/gitlab/jira_import/base_importer_spec.rb b/spec/lib/gitlab/jira_import/base_importer_spec.rb
index f22efcb8743..ecaf3def589 100644
--- a/spec/lib/gitlab/jira_import/base_importer_spec.rb
+++ b/spec/lib/gitlab/jira_import/base_importer_spec.rb
@@ -3,12 +3,17 @@
require 'spec_helper'
describe Gitlab::JiraImport::BaseImporter do
+ include JiraServiceHelper
+
let(:project) { create(:project) }
describe 'with any inheriting class' do
- context 'when feature flag disabled' do
+ context 'when an error is returned from the project validation' do
before do
stub_feature_flags(jira_issue_import: false)
+
+ allow(project).to receive(:validate_jira_import_settings!)
+ .and_raise(Projects::ImportService::Error, 'Jira import feature is disabled.')
end
it 'raises exception' do
@@ -16,20 +21,17 @@ describe Gitlab::JiraImport::BaseImporter do
end
end
- context 'when feature flag enabled' do
+ context 'when project validation is ok' do
+ let!(:jira_service) { create(:jira_service, project: project) }
+
before do
stub_feature_flags(jira_issue_import: true)
- end
+ stub_jira_service_test
- context 'when Jira service was not setup' do
- it 'raises exception' do
- expect { described_class.new(project) }.to raise_error(Projects::ImportService::Error, 'Jira integration not configured.')
- end
+ allow(project).to receive(:validate_jira_import_settings!)
end
context 'when Jira service exists' do
- let!(:jira_service) { create(:jira_service, project: project) }
-
context 'when Jira import data is not present' do
it 'raises exception' do
expect { described_class.new(project) }.to raise_error(Projects::ImportService::Error, 'Unable to find Jira project to import data from.')
diff --git a/spec/lib/gitlab/jira_import/handle_labels_service_spec.rb b/spec/lib/gitlab/jira_import/handle_labels_service_spec.rb
new file mode 100644
index 00000000000..0eeff180575
--- /dev/null
+++ b/spec/lib/gitlab/jira_import/handle_labels_service_spec.rb
@@ -0,0 +1,53 @@
+# frozen_string_literal: true
+
+require 'spec_helper'
+
+describe Gitlab::JiraImport::HandleLabelsService do
+ describe '#execute' do
+ let_it_be(:group) { create(:group) }
+ let_it_be(:project) { create(:project, group: group) }
+
+ let_it_be(:project_label) { create(:label, project: project, title: 'bug') }
+ let_it_be(:other_project_label) { create(:label, title: 'feature') }
+ let_it_be(:group_label) { create(:group_label, group: group, title: 'dev') }
+ let(:jira_labels) { %w(bug feature dev group::new) }
+
+ subject { described_class.new(project, jira_labels).execute }
+
+ context 'when some provided jira labels are missing' do
+ def created_labels
+ project.labels.reorder(id: :desc).first(2)
+ end
+
+ it 'creates the missing labels on the project level' do
+ expect { subject }.to change { Label.count }.from(3).to(5)
+
+ expect(created_labels.map(&:title)).to match_array(%w(feature group::new))
+ end
+
+ it 'returns the id of all labels matching the title' do
+ expect(subject).to match_array([project_label.id, group_label.id] + created_labels.map(&:id))
+ end
+ end
+
+ context 'when no provided jira labels are missing' do
+ let(:jira_labels) { %w(bug dev) }
+
+ it 'does not create any new labels' do
+ expect { subject }.not_to change { Label.count }.from(3)
+ end
+
+ it 'returns the id of all labels matching the title' do
+ expect(subject).to match_array([project_label.id, group_label.id])
+ end
+ end
+
+ context 'when no labels are provided' do
+ let(:jira_labels) { [] }
+
+ it 'does not create any new labels' do
+ expect { subject }.not_to change { Label.count }.from(3)
+ end
+ end
+ end
+end
diff --git a/spec/lib/gitlab/jira_import/issue_serializer_spec.rb b/spec/lib/gitlab/jira_import/issue_serializer_spec.rb
index 808ed6ee2fa..ce38a1234cf 100644
--- a/spec/lib/gitlab/jira_import/issue_serializer_spec.rb
+++ b/spec/lib/gitlab/jira_import/issue_serializer_spec.rb
@@ -4,7 +4,12 @@ require 'spec_helper'
describe Gitlab::JiraImport::IssueSerializer do
describe '#execute' do
- let_it_be(:project) { create(:project) }
+ let_it_be(:group) { create(:group) }
+ let_it_be(:project) { create(:project, group: group) }
+ let_it_be(:project_label) { create(:label, project: project, title: 'bug') }
+ let_it_be(:other_project_label) { create(:label, project: project, title: 'feature') }
+ let_it_be(:group_label) { create(:group_label, group: group, title: 'dev') }
+ let_it_be(:current_user) { create(:user) }
let(:iid) { 5 }
let(:key) { 'PROJECT-5' }
@@ -12,28 +17,21 @@ describe Gitlab::JiraImport::IssueSerializer do
let(:description) { 'basic description' }
let(:created_at) { '2020-01-01 20:00:00' }
let(:updated_at) { '2020-01-10 20:00:00' }
- let(:assignee) { double(displayName: 'Solver') }
+ let(:assignee) { double(attrs: { 'displayName' => 'Solver', 'emailAddress' => 'assignee@example.com' }) }
+ let(:reporter) { double(attrs: { 'displayName' => 'Reporter', 'emailAddress' => 'reporter@example.com' }) }
let(:jira_status) { 'new' }
let(:parent_field) do
{ 'key' => 'FOO-2', 'id' => '1050', 'fields' => { 'summary' => 'parent issue FOO' } }
end
- let(:issue_type_field) { { 'name' => 'Task' } }
- let(:fix_versions_field) { [{ 'name' => '1.0' }, { 'name' => '1.1' }] }
let(:priority_field) { { 'name' => 'Medium' } }
- let(:labels_field) { %w(bug backend) }
- let(:environment_field) { 'staging' }
- let(:duedate_field) { '2020-03-01' }
+ let(:labels_field) { %w(bug dev backend frontend) }
let(:fields) do
{
'parent' => parent_field,
- 'issuetype' => issue_type_field,
- 'fixVersions' => fix_versions_field,
'priority' => priority_field,
- 'labels' => labels_field,
- 'environment' => environment_field,
- 'duedate' => duedate_field
+ 'labels' => labels_field
}
end
@@ -46,7 +44,7 @@ describe Gitlab::JiraImport::IssueSerializer do
created: created_at,
updated: updated_at,
assignee: assignee,
- reporter: double(displayName: 'Reporter'),
+ reporter: reporter,
status: double(statusCategory: { 'key' => jira_status }),
fields: fields
)
@@ -54,27 +52,18 @@ describe Gitlab::JiraImport::IssueSerializer do
let(:params) { { iid: iid } }
- subject { described_class.new(project, jira_issue, params).execute }
+ subject { described_class.new(project, jira_issue, current_user.id, params).execute }
let(:expected_description) do
<<~MD
- *Created by: Reporter*
-
- *Assigned to: Solver*
-
basic description
---
**Issue metadata**
- - Issue type: Task
- Priority: Medium
- - Labels: bug, backend
- - Environment: staging
- - Due date: 2020-03-01
- Parent issue: [FOO-2] parent issue FOO
- - Fix versions: 1.0, 1.1
MD
end
@@ -88,55 +77,102 @@ describe Gitlab::JiraImport::IssueSerializer do
state_id: 1,
updated_at: updated_at,
created_at: created_at,
- author_id: project.creator_id
+ author_id: current_user.id,
+ assignee_ids: nil,
+ label_ids: [project_label.id, group_label.id] + Label.reorder(id: :asc).last(2).pluck(:id)
)
end
- context 'when some metadata fields are missing' do
- let(:assignee) { nil }
- let(:parent_field) { nil }
- let(:fix_versions_field) { [] }
- let(:labels_field) { [] }
- let(:environment_field) { nil }
- let(:duedate_field) { '2020-03-01' }
+ it 'creates a hash for valid issue' do
+ expect(Issue.new(subject)).to be_valid
+ end
+
+ context 'labels' do
+ it 'creates all missing labels (on project level)' do
+ expect { subject }.to change { Label.count }.from(3).to(5)
+
+ expect(Label.find_by(title: 'frontend').project).to eq(project)
+ expect(Label.find_by(title: 'backend').project).to eq(project)
+ end
+
+ context 'when there are no new labels' do
+ let(:labels_field) { %w(bug dev) }
- it 'skips the missing fields' do
- expected_description = <<~MD
- *Created by: Reporter*
+ it 'assigns the labels to the Issue hash' do
+ expect(subject[:label_ids]).to match_array([project_label.id, group_label.id])
+ end
- basic description
+ it 'does not create new labels' do
+ expect { subject }.not_to change { Label.count }.from(3)
+ end
+ end
+ end
- ---
+ context 'author' do
+ context 'when reporter maps to a valid GitLab user' do
+ let!(:user) { create(:user, email: 'reporter@example.com') }
- **Issue metadata**
+ it 'sets the issue author to the mapped user' do
+ project.add_developer(user)
- - Issue type: Task
- - Priority: Medium
- - Due date: 2020-03-01
- MD
+ expect(subject[:author_id]).to eq(user.id)
+ end
+ end
- expect(subject[:description]).to eq(expected_description.strip)
+ context 'when reporter does not map to a valid Gitlab user' do
+ it 'defaults the issue author to project creator' do
+ expect(subject[:author_id]).to eq(current_user.id)
+ end
+ end
+
+ context 'when reporter field is empty' do
+ let(:reporter) { nil }
+
+ it 'defaults the issue author to project creator' do
+ expect(subject[:author_id]).to eq(current_user.id)
+ end
+ end
+
+ context 'when reporter field is missing email address' do
+ let(:reporter) { double(attrs: { 'displayName' => 'Reporter' }) }
+
+ it 'defaults the issue author to project creator' do
+ expect(subject[:author_id]).to eq(current_user.id)
+ end
end
end
- context 'when all metadata fields are missing' do
- let(:assignee) { nil }
- let(:parent_field) { nil }
- let(:issue_type_field) { nil }
- let(:fix_versions_field) { [] }
- let(:priority_field) { nil }
- let(:labels_field) { [] }
- let(:environment_field) { nil }
- let(:duedate_field) { nil }
+ context 'assignee' do
+ context 'when assignee maps to a valid GitLab user' do
+ let!(:user) { create(:user, email: 'assignee@example.com') }
+
+ it 'sets the issue assignees to the mapped user' do
+ project.add_developer(user)
- it 'skips the whole metadata secction' do
- expected_description = <<~MD
- *Created by: Reporter*
+ expect(subject[:assignee_ids]).to eq([user.id])
+ end
+ end
+
+ context 'when assignee does not map to a valid GitLab user' do
+ it 'leaves the assignee empty' do
+ expect(subject[:assignee_ids]).to be_nil
+ end
+ end
+
+ context 'when assginee field is empty' do
+ let(:assignee) { nil }
+
+ it 'leaves the assignee empty' do
+ expect(subject[:assignee_ids]).to be_nil
+ end
+ end
- basic description
- MD
+ context 'when assginee field is missing email address' do
+ let(:assignee) { double(attrs: { 'displayName' => 'Reporter' }) }
- expect(subject[:description]).to eq(expected_description.strip)
+ it 'leaves the assignee empty' do
+ expect(subject[:assignee_ids]).to be_nil
+ end
end
end
end
diff --git a/spec/lib/gitlab/jira_import/issues_importer_spec.rb b/spec/lib/gitlab/jira_import/issues_importer_spec.rb
index 8e16fd3e978..6cf06c20e19 100644
--- a/spec/lib/gitlab/jira_import/issues_importer_spec.rb
+++ b/spec/lib/gitlab/jira_import/issues_importer_spec.rb
@@ -3,15 +3,19 @@
require 'spec_helper'
describe Gitlab::JiraImport::IssuesImporter do
+ include JiraServiceHelper
+
let_it_be(:user) { create(:user) }
+ let_it_be(:current_user) { create(:user) }
let_it_be(:project) { create(:project) }
- let_it_be(:jira_import) { create(:jira_import_state, project: project) }
+ let_it_be(:jira_import) { create(:jira_import_state, project: project, user: current_user) }
let_it_be(:jira_service) { create(:jira_service, project: project) }
subject { described_class.new(project) }
before do
stub_feature_flags(jira_issue_import: true)
+ stub_jira_service_test
end
describe '#imported_items_cache_key' do
@@ -36,8 +40,16 @@ describe Gitlab::JiraImport::IssuesImporter do
context 'with results returned' do
JiraIssue = Struct.new(:id)
- let_it_be(:jira_issue1) { JiraIssue.new(1) }
- let_it_be(:jira_issue2) { JiraIssue.new(2) }
+ let_it_be(:jira_issues) { [JiraIssue.new(1), JiraIssue.new(2)] }
+
+ def mock_issue_serializer(count)
+ serializer = instance_double(Gitlab::JiraImport::IssueSerializer, execute: { key: 'data' })
+
+ count.times do |i|
+ expect(Gitlab::JiraImport::IssueSerializer).to receive(:new)
+ .with(project, jira_issues[i], current_user.id, { iid: i + 1 }).and_return(serializer)
+ end
+ end
context 'when single page of results is returned' do
before do
@@ -45,13 +57,11 @@ describe Gitlab::JiraImport::IssuesImporter do
end
it 'schedules 2 import jobs' do
- expect(subject).to receive(:fetch_issues).and_return([jira_issue1, jira_issue2])
+ expect(subject).to receive(:fetch_issues).with(0).and_return([jira_issues[0], jira_issues[1]])
expect(Gitlab::JiraImport::ImportIssueWorker).to receive(:perform_async).twice
expect(Gitlab::Cache::Import::Caching).to receive(:set_add).twice.and_call_original
expect(Gitlab::Cache::Import::Caching).to receive(:set_includes?).twice.and_call_original
- allow_next_instance_of(Gitlab::JiraImport::IssueSerializer) do |instance|
- allow(instance).to receive(:execute).and_return({ key: 'data' })
- end
+ mock_issue_serializer(2)
job_waiter = subject.execute
@@ -66,13 +76,11 @@ describe Gitlab::JiraImport::IssuesImporter do
end
it 'schedules 3 import jobs' do
- expect(subject).to receive(:fetch_issues).with(0).and_return([jira_issue1, jira_issue2])
+ expect(subject).to receive(:fetch_issues).with(0).and_return([jira_issues[0], jira_issues[1]])
expect(Gitlab::JiraImport::ImportIssueWorker).to receive(:perform_async).twice.times
expect(Gitlab::Cache::Import::Caching).to receive(:set_add).twice.times.and_call_original
expect(Gitlab::Cache::Import::Caching).to receive(:set_includes?).twice.times.and_call_original
- allow_next_instance_of(Gitlab::JiraImport::IssueSerializer) do |instance|
- allow(instance).to receive(:execute).and_return({ key: 'data' })
- end
+ mock_issue_serializer(2)
job_waiter = subject.execute
@@ -87,13 +95,11 @@ describe Gitlab::JiraImport::IssuesImporter do
end
it 'schedules 2 import jobs' do
- expect(subject).to receive(:fetch_issues).with(0).and_return([jira_issue1, jira_issue1])
+ expect(subject).to receive(:fetch_issues).with(0).and_return([jira_issues[0], jira_issues[0]])
expect(Gitlab::JiraImport::ImportIssueWorker).to receive(:perform_async).once
expect(Gitlab::Cache::Import::Caching).to receive(:set_add).once.and_call_original
expect(Gitlab::Cache::Import::Caching).to receive(:set_includes?).twice.times.and_call_original
- allow_next_instance_of(Gitlab::JiraImport::IssueSerializer) do |instance|
- allow(instance).to receive(:execute).and_return({ key: 'data' })
- end
+ mock_issue_serializer(1)
job_waiter = subject.execute
diff --git a/spec/lib/gitlab/jira_import/labels_importer_spec.rb b/spec/lib/gitlab/jira_import/labels_importer_spec.rb
index 3eb4666a74f..67eb541d376 100644
--- a/spec/lib/gitlab/jira_import/labels_importer_spec.rb
+++ b/spec/lib/gitlab/jira_import/labels_importer_spec.rb
@@ -3,35 +3,100 @@
require 'spec_helper'
describe Gitlab::JiraImport::LabelsImporter do
+ include JiraServiceHelper
+
let_it_be(:user) { create(:user) }
- let_it_be(:project) { create(:project) }
+ let_it_be(:group) { create(:group) }
+ let_it_be(:project) { create(:project, group: group) }
let_it_be(:jira_service) { create(:jira_service, project: project) }
- subject { described_class.new(project).execute }
+ let(:importer) { described_class.new(project) }
+
+ subject { importer.execute }
before do
stub_feature_flags(jira_issue_import: true)
+ stub_const('Gitlab::JiraImport::LabelsImporter::MAX_LABELS', 2)
end
describe '#execute', :clean_gitlab_redis_cache do
+ before do
+ stub_jira_service_test
+ end
+
context 'when label is missing from jira import' do
let_it_be(:no_label_jira_import) { create(:jira_import_state, label: nil, project: project) }
it 'raises error' do
- expect { subject }.to raise_error(Projects::ImportService::Error, 'Failed to find import label for jira import.')
+ expect { subject }.to raise_error(Projects::ImportService::Error, 'Failed to find import label for Jira import.')
end
end
- context 'when label exists' do
- let_it_be(:label) { create(:label) }
+ context 'when jira import label exists' do
+ let_it_be(:label) { create(:label) }
let_it_be(:jira_import_with_label) { create(:jira_import_state, label: label, project: project) }
+ let_it_be(:issue_label) { create(:label, project: project, title: 'bug') }
+
+ let(:jira_labels_1) { { "maxResults" => 2, "startAt" => 0, "total" => 3, "isLast" => false, "values" => %w(backend bug) } }
+ let(:jira_labels_2) { { "maxResults" => 2, "startAt" => 2, "total" => 3, "isLast" => true, "values" => %w(feature) } }
+
+ context 'when labels are returned from jira' do
+ before do
+ client = double
+ expect(importer).to receive(:client).twice.and_return(client)
+ allow(client).to receive(:get).twice.and_return(jira_labels_1, jira_labels_2)
+ end
+
+ it 'caches import label' do
+ expect(Gitlab::Cache::Import::Caching.read(Gitlab::JiraImport.import_label_cache_key(project.id))).to be nil
+
+ subject
+
+ expect(Gitlab::JiraImport.get_import_label_id(project.id).to_i).to eq(label.id)
+ end
+
+ it 'calls Gitlab::JiraImport::HandleLabelsService' do
+ expect(Gitlab::JiraImport::HandleLabelsService).to receive(:new).with(project, %w(backend bug)).and_return(double(execute: [1, 2]))
+ expect(Gitlab::JiraImport::HandleLabelsService).to receive(:new).with(project, %w(feature)).and_return(double(execute: [3]))
+
+ subject
+ end
+ end
+
+ context 'when there are no labels to be handled' do
+ shared_examples 'no labels handling' do
+ it 'does not call Gitlab::JiraImport::HandleLabelsService' do
+ expect(Gitlab::JiraImport::HandleLabelsService).not_to receive(:new)
+
+ subject
+ end
+ end
+
+ let(:jira_labels) { { "maxResults" => 2, "startAt" => 0, "total" => 3, "values" => [] } }
+
+ before do
+ client = double
+ expect(importer).to receive(:client).and_return(client)
+ allow(client).to receive(:get).and_return(jira_labels)
+ end
+
+ context 'when the labels field is empty' do
+ let(:jira_labels) { { "maxResults" => 2, "startAt" => 0, "isLast" => true, "total" => 3, "values" => [] } }
+
+ it_behaves_like 'no labels handling'
+ end
+
+ context 'when the labels field is missing' do
+ let(:jira_labels) { { "maxResults" => 2, "startAt" => 0, "isLast" => true, "total" => 3 } }
- it 'caches import label' do
- expect(Gitlab::Cache::Import::Caching.read(Gitlab::JiraImport.import_label_cache_key(project.id))).to be nil
+ it_behaves_like 'no labels handling'
+ end
- subject
+ context 'when the isLast argument is missing' do
+ let(:jira_labels) { { "maxResults" => 2, "startAt" => 0, "total" => 3, "values" => %w(bug dev) } }
- expect(Gitlab::JiraImport.get_import_label_id(project.id).to_i).to eq(label.id)
+ it_behaves_like 'no labels handling'
+ end
end
end
end
diff --git a/spec/lib/gitlab/jira_import/metadata_collector_spec.rb b/spec/lib/gitlab/jira_import/metadata_collector_spec.rb
new file mode 100644
index 00000000000..af479810df0
--- /dev/null
+++ b/spec/lib/gitlab/jira_import/metadata_collector_spec.rb
@@ -0,0 +1,178 @@
+# frozen_string_literal: true
+
+require 'spec_helper'
+
+describe Gitlab::JiraImport::MetadataCollector do
+ describe '#execute' do
+ let(:key) { 'PROJECT-5' }
+ let(:summary) { 'some title' }
+ let(:description) { 'basic description' }
+ let(:created_at) { '2020-01-01 20:00:00' }
+ let(:updated_at) { '2020-01-10 20:00:00' }
+ let(:jira_status) { 'new' }
+
+ let(:parent_field) do
+ { 'key' => 'FOO-2', 'id' => '1050', 'fields' => { 'summary' => 'parent issue FOO' } }
+ end
+ let(:issue_type_field) { { 'name' => 'Task' } }
+ let(:fix_versions_field) { [{ 'name' => '1.0' }, { 'name' => '1.1' }] }
+ let(:priority_field) { { 'name' => 'Medium' } }
+ let(:environment_field) { 'staging' }
+ let(:duedate_field) { '2020-03-01' }
+
+ let(:fields) do
+ {
+ 'parent' => parent_field,
+ 'issuetype' => issue_type_field,
+ 'fixVersions' => fix_versions_field,
+ 'priority' => priority_field,
+ 'environment' => environment_field,
+ 'duedate' => duedate_field
+ }
+ end
+ let(:jira_issue) do
+ double(
+ id: '1234',
+ key: key,
+ summary: summary,
+ description: description,
+ created: created_at,
+ updated: updated_at,
+ status: double(statusCategory: { 'key' => jira_status }),
+ fields: fields
+ )
+ end
+
+ subject { described_class.new(jira_issue).execute }
+
+ context 'when all metadata fields are present' do
+ it 'writes all fields' do
+ expected_result = <<~MD
+ ---
+
+ **Issue metadata**
+
+ - Issue type: Task
+ - Priority: Medium
+ - Environment: staging
+ - Due date: 2020-03-01
+ - Parent issue: [FOO-2] parent issue FOO
+ - Fix versions: 1.0, 1.1
+ MD
+
+ expect(subject.strip).to eq(expected_result.strip)
+ end
+ end
+
+ context 'when some fields are in incorrect format' do
+ let(:parent_field) { nil }
+ let(:fix_versions_field) { [] }
+ let(:priority_field) { nil }
+ let(:environment_field) { nil }
+ let(:duedate_field) { nil }
+
+ context 'when fixVersions field is not an array' do
+ let(:fix_versions_field) { { 'title' => '1.0', 'name' => '1.1' } }
+
+ it 'skips these fields' do
+ expected_result = <<~MD
+ ---
+
+ **Issue metadata**
+
+ - Issue type: Task
+ MD
+
+ expect(subject.strip).to eq(expected_result.strip)
+ end
+ end
+
+ context 'when a fixVersions element is in incorrect format' do
+ let(:fix_versions_field) { [{ 'title' => '1.0' }, { 'name' => '1.1' }] }
+
+ it 'skips the element' do
+ expected_result = <<~MD
+ ---
+
+ **Issue metadata**
+
+ - Issue type: Task
+ - Fix versions: 1.1
+ MD
+
+ expect(subject.strip).to eq(expected_result.strip)
+ end
+ end
+
+ context 'when a parent field has incorrectly formatted summary' do
+ let(:parent_field) do
+ { 'key' => 'FOO-2', 'id' => '1050', 'other_field' => { 'summary' => 'parent issue FOO' } }
+ end
+
+ it 'skips the summary' do
+ expected_result = <<~MD
+ ---
+
+ **Issue metadata**
+
+ - Issue type: Task
+ - Parent issue: [FOO-2]
+ MD
+
+ expect(subject.strip).to eq(expected_result.strip)
+ end
+ end
+
+ context 'when a parent field is missing the key' do
+ let(:parent_field) do
+ { 'not_key' => 'FOO-2', 'id' => '1050', 'other_field' => { 'summary' => 'parent issue FOO' } }
+ end
+
+ it 'skips the field' do
+ expected_result = <<~MD
+ ---
+
+ **Issue metadata**
+
+ - Issue type: Task
+ MD
+
+ expect(subject.strip).to eq(expected_result.strip)
+ end
+ end
+ end
+
+ context 'when some metadata fields are missing' do
+ let(:parent_field) { nil }
+ let(:fix_versions_field) { [] }
+ let(:environment_field) { nil }
+
+ it 'skips the missing fields' do
+ expected_result = <<~MD
+ ---
+
+ **Issue metadata**
+
+ - Issue type: Task
+ - Priority: Medium
+ - Due date: 2020-03-01
+ MD
+
+ expect(subject.strip).to eq(expected_result.strip)
+ end
+ end
+
+ context 'when all metadata fields are missing' do
+ let(:parent_field) { nil }
+ let(:issue_type_field) { nil }
+ let(:fix_versions_field) { [] }
+ let(:priority_field) { nil }
+ let(:environment_field) { nil }
+ let(:duedate_field) { nil }
+
+ it 'returns nil' do
+ expect(subject).to be_nil
+ end
+ end
+ end
+end
diff --git a/spec/lib/gitlab/jira_import/user_mapper_spec.rb b/spec/lib/gitlab/jira_import/user_mapper_spec.rb
new file mode 100644
index 00000000000..c8c8bd3c5b0
--- /dev/null
+++ b/spec/lib/gitlab/jira_import/user_mapper_spec.rb
@@ -0,0 +1,80 @@
+# frozen_string_literal: true
+
+require 'spec_helper'
+
+describe Gitlab::JiraImport::UserMapper do
+ let_it_be(:group) { create(:group) }
+ let_it_be(:project) { create(:project, group: group) }
+ let_it_be(:user) { create(:user, email: 'user@example.com') }
+ let_it_be(:email) { create(:email, user: user, email: 'second_email@example.com', confirmed_at: nil) }
+
+ let(:jira_user) { { 'acountId' => '1a2b', 'emailAddress' => 'user@example.com' } }
+
+ describe '#execute' do
+ subject { described_class.new(project, jira_user).execute }
+
+ context 'when jira_user is nil' do
+ let(:jira_user) { nil }
+
+ it 'returns nil' do
+ expect(subject).to be_nil
+ end
+ end
+
+ context 'when Gitlab user is not found by email' do
+ let(:jira_user) { { 'acountId' => '1a2b', 'emailAddress' => 'other@example.com' } }
+
+ it 'returns nil' do
+ expect(subject).to be_nil
+ end
+ end
+
+ context 'when jira_user emailAddress is nil' do
+ let(:jira_user) { { 'acountId' => '1a2b', 'emailAddress' => nil } }
+
+ it 'returns nil' do
+ expect(subject).to be_nil
+ end
+ end
+
+ context 'when jira_user emailAddress key is missing' do
+ let(:jira_user) { { 'acountId' => '1a2b' } }
+
+ it 'returns nil' do
+ expect(subject).to be_nil
+ end
+ end
+
+ context 'when found user is not a project member' do
+ it 'returns nil' do
+ expect(subject).to be_nil
+ end
+ end
+
+ context 'when found user is a project member' do
+ it 'returns the found user' do
+ project.add_developer(user)
+
+ expect(subject).to eq(user)
+ end
+ end
+
+ context 'when user found by unconfirmd secondary address is a project member' do
+ let(:jira_user) { { 'acountId' => '1a2b', 'emailAddress' => 'second_email@example.com' } }
+
+ it 'returns the found user' do
+ project.add_developer(user)
+
+ expect(subject).to eq(user)
+ end
+ end
+
+ context 'when user is a group member' do
+ it 'returns the found user' do
+ group.add_developer(user)
+
+ expect(subject).to eq(user)
+ end
+ end
+ end
+end
diff --git a/spec/lib/gitlab/json_logger_spec.rb b/spec/lib/gitlab/json_logger_spec.rb
index 5d544198c40..41dafc84ef2 100644
--- a/spec/lib/gitlab/json_logger_spec.rb
+++ b/spec/lib/gitlab/json_logger_spec.rb
@@ -14,7 +14,7 @@ describe Gitlab::JsonLogger do
it 'formats strings' do
output = subject.format_message('INFO', now, 'test', 'Hello world')
- data = JSON.parse(output)
+ data = Gitlab::Json.parse(output)
expect(data['severity']).to eq('INFO')
expect(data['time']).to eq(now.utc.iso8601(3))
@@ -24,7 +24,7 @@ describe Gitlab::JsonLogger do
it 'formats hashes' do
output = subject.format_message('INFO', now, 'test', { hello: 1 })
- data = JSON.parse(output)
+ data = Gitlab::Json.parse(output)
expect(data['severity']).to eq('INFO')
expect(data['time']).to eq(now.utc.iso8601(3))
diff --git a/spec/lib/gitlab/json_spec.rb b/spec/lib/gitlab/json_spec.rb
index 5186ab041da..ee7c98a5a54 100644
--- a/spec/lib/gitlab/json_spec.rb
+++ b/spec/lib/gitlab/json_spec.rb
@@ -3,47 +3,151 @@
require "spec_helper"
RSpec.describe Gitlab::Json do
+ before do
+ stub_feature_flags(json_wrapper_legacy_mode: true)
+ end
+
describe ".parse" do
- it "parses an object" do
- expect(subject.parse('{ "foo": "bar" }')).to eq({ "foo" => "bar" })
- end
+ context "legacy_mode is disabled by default" do
+ it "parses an object" do
+ expect(subject.parse('{ "foo": "bar" }')).to eq({ "foo" => "bar" })
+ end
- it "parses an array" do
- expect(subject.parse('[{ "foo": "bar" }]')).to eq([{ "foo" => "bar" }])
- end
+ it "parses an array" do
+ expect(subject.parse('[{ "foo": "bar" }]')).to eq([{ "foo" => "bar" }])
+ end
- it "raises an error on a string" do
- expect { subject.parse('"foo"') }.to raise_error(JSON::ParserError)
+ it "parses a string" do
+ expect(subject.parse('"foo"', legacy_mode: false)).to eq("foo")
+ end
+
+ it "parses a true bool" do
+ expect(subject.parse("true", legacy_mode: false)).to be(true)
+ end
+
+ it "parses a false bool" do
+ expect(subject.parse("false", legacy_mode: false)).to be(false)
+ end
end
- it "raises an error on a true bool" do
- expect { subject.parse("true") }.to raise_error(JSON::ParserError)
+ context "legacy_mode is enabled" do
+ it "parses an object" do
+ expect(subject.parse('{ "foo": "bar" }', legacy_mode: true)).to eq({ "foo" => "bar" })
+ end
+
+ it "parses an array" do
+ expect(subject.parse('[{ "foo": "bar" }]', legacy_mode: true)).to eq([{ "foo" => "bar" }])
+ end
+
+ it "raises an error on a string" do
+ expect { subject.parse('"foo"', legacy_mode: true) }.to raise_error(JSON::ParserError)
+ end
+
+ it "raises an error on a true bool" do
+ expect { subject.parse("true", legacy_mode: true) }.to raise_error(JSON::ParserError)
+ end
+
+ it "raises an error on a false bool" do
+ expect { subject.parse("false", legacy_mode: true) }.to raise_error(JSON::ParserError)
+ end
end
- it "raises an error on a false bool" do
- expect { subject.parse("false") }.to raise_error(JSON::ParserError)
+ context "feature flag is disabled" do
+ before do
+ stub_feature_flags(json_wrapper_legacy_mode: false)
+ end
+
+ it "parses an object" do
+ expect(subject.parse('{ "foo": "bar" }', legacy_mode: true)).to eq({ "foo" => "bar" })
+ end
+
+ it "parses an array" do
+ expect(subject.parse('[{ "foo": "bar" }]', legacy_mode: true)).to eq([{ "foo" => "bar" }])
+ end
+
+ it "parses a string" do
+ expect(subject.parse('"foo"', legacy_mode: true)).to eq("foo")
+ end
+
+ it "parses a true bool" do
+ expect(subject.parse("true", legacy_mode: true)).to be(true)
+ end
+
+ it "parses a false bool" do
+ expect(subject.parse("false", legacy_mode: true)).to be(false)
+ end
end
end
describe ".parse!" do
- it "parses an object" do
- expect(subject.parse!('{ "foo": "bar" }')).to eq({ "foo" => "bar" })
- end
+ context "legacy_mode is disabled by default" do
+ it "parses an object" do
+ expect(subject.parse!('{ "foo": "bar" }')).to eq({ "foo" => "bar" })
+ end
- it "parses an array" do
- expect(subject.parse!('[{ "foo": "bar" }]')).to eq([{ "foo" => "bar" }])
- end
+ it "parses an array" do
+ expect(subject.parse!('[{ "foo": "bar" }]')).to eq([{ "foo" => "bar" }])
+ end
+
+ it "parses a string" do
+ expect(subject.parse!('"foo"', legacy_mode: false)).to eq("foo")
+ end
- it "raises an error on a string" do
- expect { subject.parse!('"foo"') }.to raise_error(JSON::ParserError)
+ it "parses a true bool" do
+ expect(subject.parse!("true", legacy_mode: false)).to be(true)
+ end
+
+ it "parses a false bool" do
+ expect(subject.parse!("false", legacy_mode: false)).to be(false)
+ end
end
- it "raises an error on a true bool" do
- expect { subject.parse!("true") }.to raise_error(JSON::ParserError)
+ context "legacy_mode is enabled" do
+ it "parses an object" do
+ expect(subject.parse!('{ "foo": "bar" }', legacy_mode: true)).to eq({ "foo" => "bar" })
+ end
+
+ it "parses an array" do
+ expect(subject.parse!('[{ "foo": "bar" }]', legacy_mode: true)).to eq([{ "foo" => "bar" }])
+ end
+
+ it "raises an error on a string" do
+ expect { subject.parse!('"foo"', legacy_mode: true) }.to raise_error(JSON::ParserError)
+ end
+
+ it "raises an error on a true bool" do
+ expect { subject.parse!("true", legacy_mode: true) }.to raise_error(JSON::ParserError)
+ end
+
+ it "raises an error on a false bool" do
+ expect { subject.parse!("false", legacy_mode: true) }.to raise_error(JSON::ParserError)
+ end
end
- it "raises an error on a false bool" do
- expect { subject.parse!("false") }.to raise_error(JSON::ParserError)
+ context "feature flag is disabled" do
+ before do
+ stub_feature_flags(json_wrapper_legacy_mode: false)
+ end
+
+ it "parses an object" do
+ expect(subject.parse!('{ "foo": "bar" }', legacy_mode: true)).to eq({ "foo" => "bar" })
+ end
+
+ it "parses an array" do
+ expect(subject.parse!('[{ "foo": "bar" }]', legacy_mode: true)).to eq([{ "foo" => "bar" }])
+ end
+
+ it "parses a string" do
+ expect(subject.parse!('"foo"', legacy_mode: true)).to eq("foo")
+ end
+
+ it "parses a true bool" do
+ expect(subject.parse!("true", legacy_mode: true)).to be(true)
+ end
+
+ it "parses a false bool" do
+ expect(subject.parse!("false", legacy_mode: true)).to be(false)
+ end
end
end
diff --git a/spec/lib/gitlab/kubernetes/helm/api_spec.rb b/spec/lib/gitlab/kubernetes/helm/api_spec.rb
index 8147990ecc3..1f925fd45af 100644
--- a/spec/lib/gitlab/kubernetes/helm/api_spec.rb
+++ b/spec/lib/gitlab/kubernetes/helm/api_spec.rb
@@ -92,7 +92,6 @@ describe Gitlab::Kubernetes::Helm::API do
allow(client).to receive(:get_config_map).and_return(nil)
allow(client).to receive(:create_config_map).and_return(nil)
allow(client).to receive(:create_service_account).and_return(nil)
- allow(client).to receive(:create_cluster_role_binding).and_return(nil)
allow(client).to receive(:delete_pod).and_return(nil)
allow(namespace).to receive(:ensure_exists!).once
end
@@ -136,7 +135,7 @@ describe Gitlab::Kubernetes::Helm::API do
context 'without a service account' do
it 'does not create a service account on kubeclient' do
expect(client).not_to receive(:create_service_account)
- expect(client).not_to receive(:create_cluster_role_binding)
+ expect(client).not_to receive(:update_cluster_role_binding)
subject.install(command)
end
@@ -160,15 +159,14 @@ describe Gitlab::Kubernetes::Helm::API do
)
end
- context 'service account and cluster role binding does not exist' do
+ context 'service account does not exist' do
before do
expect(client).to receive(:get_service_account).with('tiller', 'gitlab-managed-apps').and_raise(Kubeclient::ResourceNotFoundError.new(404, 'Not found', nil))
- expect(client).to receive(:get_cluster_role_binding).with('tiller-admin').and_raise(Kubeclient::ResourceNotFoundError.new(404, 'Not found', nil))
end
it 'creates a service account, followed the cluster role binding on kubeclient' do
expect(client).to receive(:create_service_account).with(service_account_resource).once.ordered
- expect(client).to receive(:create_cluster_role_binding).with(cluster_role_binding_resource).once.ordered
+ expect(client).to receive(:update_cluster_role_binding).with(cluster_role_binding_resource).once.ordered
subject.install(command)
end
@@ -177,21 +175,6 @@ describe Gitlab::Kubernetes::Helm::API do
context 'service account already exists' do
before do
expect(client).to receive(:get_service_account).with('tiller', 'gitlab-managed-apps').and_return(service_account_resource)
- expect(client).to receive(:get_cluster_role_binding).with('tiller-admin').and_raise(Kubeclient::ResourceNotFoundError.new(404, 'Not found', nil))
- end
-
- it 'updates the service account, followed by creating the cluster role binding' do
- expect(client).to receive(:update_service_account).with(service_account_resource).once.ordered
- expect(client).to receive(:create_cluster_role_binding).with(cluster_role_binding_resource).once.ordered
-
- subject.install(command)
- end
- end
-
- context 'service account and cluster role binding already exists' do
- before do
- expect(client).to receive(:get_service_account).with('tiller', 'gitlab-managed-apps').and_return(service_account_resource)
- expect(client).to receive(:get_cluster_role_binding).with('tiller-admin').and_return(cluster_role_binding_resource)
end
it 'updates the service account, followed by creating the cluster role binding' do
@@ -216,7 +199,7 @@ describe Gitlab::Kubernetes::Helm::API do
context 'legacy abac cluster' do
it 'does not create a service account on kubeclient' do
expect(client).not_to receive(:create_service_account)
- expect(client).not_to receive(:create_cluster_role_binding)
+ expect(client).not_to receive(:update_cluster_role_binding)
subject.install(command)
end
diff --git a/spec/lib/gitlab/kubernetes/helm/base_command_spec.rb b/spec/lib/gitlab/kubernetes/helm/base_command_spec.rb
index a11a9d08503..2a4a911cf38 100644
--- a/spec/lib/gitlab/kubernetes/helm/base_command_spec.rb
+++ b/spec/lib/gitlab/kubernetes/helm/base_command_spec.rb
@@ -3,6 +3,10 @@
require 'spec_helper'
describe Gitlab::Kubernetes::Helm::BaseCommand do
+ subject(:base_command) do
+ test_class.new(rbac)
+ end
+
let(:application) { create(:clusters_applications_helm) }
let(:rbac) { false }
@@ -30,87 +34,17 @@ describe Gitlab::Kubernetes::Helm::BaseCommand do
end
end
- let(:base_command) do
- test_class.new(rbac)
- end
-
- subject { base_command }
-
- it_behaves_like 'helm commands' do
+ it_behaves_like 'helm command generator' do
let(:commands) { '' }
end
- describe '#pod_resource' do
- subject { base_command.pod_resource }
-
- it 'returns a kubeclient resoure with pod content for application' do
- is_expected.to be_an_instance_of ::Kubeclient::Resource
- end
-
- context 'when rbac is true' do
- let(:rbac) { true }
-
- it 'also returns a kubeclient resource' do
- is_expected.to be_an_instance_of ::Kubeclient::Resource
- end
- end
- end
-
describe '#pod_name' do
subject { base_command.pod_name }
it { is_expected.to eq('install-test-class-name') }
end
- describe '#service_account_resource' do
- let(:resource) do
- Kubeclient::Resource.new(metadata: { name: 'tiller', namespace: 'gitlab-managed-apps' })
- end
-
- subject { base_command.service_account_resource }
-
- context 'rbac is enabled' do
- let(:rbac) { true }
-
- it 'generates a Kubeclient resource for the tiller ServiceAccount' do
- is_expected.to eq(resource)
- end
- end
-
- context 'rbac is not enabled' do
- let(:rbac) { false }
-
- it 'generates nothing' do
- is_expected.to be_nil
- end
- end
- end
-
- describe '#cluster_role_binding_resource' do
- let(:resource) do
- Kubeclient::Resource.new(
- metadata: { name: 'tiller-admin' },
- roleRef: { apiGroup: 'rbac.authorization.k8s.io', kind: 'ClusterRole', name: 'cluster-admin' },
- subjects: [{ kind: 'ServiceAccount', name: 'tiller', namespace: 'gitlab-managed-apps' }]
- )
- end
-
- subject { base_command.cluster_role_binding_resource }
-
- context 'rbac is enabled' do
- let(:rbac) { true }
-
- it 'generates a Kubeclient resource for the ClusterRoleBinding for tiller' do
- is_expected.to eq(resource)
- end
- end
-
- context 'rbac is not enabled' do
- let(:rbac) { false }
-
- it 'generates nothing' do
- is_expected.to be_nil
- end
- end
+ it_behaves_like 'helm command' do
+ let(:command) { base_command }
end
end
diff --git a/spec/lib/gitlab/kubernetes/helm/delete_command_spec.rb b/spec/lib/gitlab/kubernetes/helm/delete_command_spec.rb
index 82e15864687..95d60c18d56 100644
--- a/spec/lib/gitlab/kubernetes/helm/delete_command_spec.rb
+++ b/spec/lib/gitlab/kubernetes/helm/delete_command_spec.rb
@@ -3,14 +3,13 @@
require 'spec_helper'
describe Gitlab::Kubernetes::Helm::DeleteCommand do
+ subject(:delete_command) { described_class.new(name: app_name, rbac: rbac, files: files) }
+
let(:app_name) { 'app-name' }
let(:rbac) { true }
let(:files) { {} }
- let(:delete_command) { described_class.new(name: app_name, rbac: rbac, files: files) }
-
- subject { delete_command }
- it_behaves_like 'helm commands' do
+ it_behaves_like 'helm command generator' do
let(:commands) do
<<~EOS
export HELM_HOST="localhost:44134"
@@ -26,7 +25,7 @@ describe Gitlab::Kubernetes::Helm::DeleteCommand do
stub_feature_flags(managed_apps_local_tiller: false)
end
- it_behaves_like 'helm commands' do
+ it_behaves_like 'helm command generator' do
let(:commands) do
<<~EOS
helm init --upgrade
@@ -48,7 +47,7 @@ describe Gitlab::Kubernetes::Helm::DeleteCommand do
EOS
end
- it_behaves_like 'helm commands' do
+ it_behaves_like 'helm command generator' do
let(:commands) do
<<~EOS
helm init --upgrade
@@ -67,29 +66,19 @@ describe Gitlab::Kubernetes::Helm::DeleteCommand do
end
end
- describe '#pod_resource' do
- subject { delete_command.pod_resource }
-
- context 'rbac is enabled' do
- let(:rbac) { true }
-
- it 'generates a pod that uses the tiller serviceAccountName' do
- expect(subject.spec.serviceAccountName).to eq('tiller')
- end
- end
-
- context 'rbac is not enabled' do
- let(:rbac) { false }
-
- it 'generates a pod that uses the default serviceAccountName' do
- expect(subject.spec.serviceAcccountName).to be_nil
- end
- end
- end
-
describe '#pod_name' do
subject { delete_command.pod_name }
it { is_expected.to eq('uninstall-app-name') }
end
+
+ it_behaves_like 'helm command' do
+ let(:command) { delete_command }
+ end
+
+ describe '#delete_command' do
+ it 'deletes the release' do
+ expect(subject.delete_command).to eq('helm delete --purge app-name')
+ end
+ end
end
diff --git a/spec/lib/gitlab/kubernetes/helm/init_command_spec.rb b/spec/lib/gitlab/kubernetes/helm/init_command_spec.rb
index 13021a08f9f..05d9b63d12b 100644
--- a/spec/lib/gitlab/kubernetes/helm/init_command_spec.rb
+++ b/spec/lib/gitlab/kubernetes/helm/init_command_spec.rb
@@ -3,25 +3,24 @@
require 'spec_helper'
describe Gitlab::Kubernetes::Helm::InitCommand do
+ subject(:init_command) { described_class.new(name: application.name, files: files, rbac: rbac) }
+
let(:application) { create(:clusters_applications_helm) }
let(:rbac) { false }
let(:files) { {} }
- let(:init_command) { described_class.new(name: application.name, files: files, rbac: rbac) }
- let(:commands) do
- <<~EOS
- helm init --tiller-tls --tiller-tls-verify --tls-ca-cert /data/helm/helm/config/ca.pem --tiller-tls-cert /data/helm/helm/config/cert.pem --tiller-tls-key /data/helm/helm/config/key.pem
- EOS
+ it_behaves_like 'helm command generator' do
+ let(:commands) do
+ <<~EOS
+ helm init --tiller-tls --tiller-tls-verify --tls-ca-cert /data/helm/helm/config/ca.pem --tiller-tls-cert /data/helm/helm/config/cert.pem --tiller-tls-key /data/helm/helm/config/key.pem
+ EOS
+ end
end
- subject { init_command }
-
- it_behaves_like 'helm commands'
-
context 'on a rbac-enabled cluster' do
let(:rbac) { true }
- it_behaves_like 'helm commands' do
+ it_behaves_like 'helm command generator' do
let(:commands) do
<<~EOS
helm init --tiller-tls --tiller-tls-verify --tls-ca-cert /data/helm/helm/config/ca.pem --tiller-tls-cert /data/helm/helm/config/cert.pem --tiller-tls-key /data/helm/helm/config/key.pem --service-account tiller
@@ -30,57 +29,7 @@ describe Gitlab::Kubernetes::Helm::InitCommand do
end
end
- describe '#rbac?' do
- subject { init_command.rbac? }
-
- context 'rbac is enabled' do
- let(:rbac) { true }
-
- it { is_expected.to be_truthy }
- end
-
- context 'rbac is not enabled' do
- let(:rbac) { false }
-
- it { is_expected.to be_falsey }
- end
- end
-
- describe '#config_map_resource' do
- let(:metadata) do
- {
- name: 'values-content-configuration-helm',
- namespace: 'gitlab-managed-apps',
- labels: { name: 'values-content-configuration-helm' }
- }
- end
-
- let(:resource) { ::Kubeclient::Resource.new(metadata: metadata, data: files) }
-
- subject { init_command.config_map_resource }
-
- it 'returns a KubeClient resource with config map content for the application' do
- is_expected.to eq(resource)
- end
- end
-
- describe '#pod_resource' do
- subject { init_command.pod_resource }
-
- context 'rbac is enabled' do
- let(:rbac) { true }
-
- it 'generates a pod that uses the tiller serviceAccountName' do
- expect(subject.spec.serviceAccountName).to eq('tiller')
- end
- end
-
- context 'rbac is not enabled' do
- let(:rbac) { false }
-
- it 'generates a pod that uses the default serviceAccountName' do
- expect(subject.spec.serviceAcccountName).to be_nil
- end
- end
+ it_behaves_like 'helm command' do
+ let(:command) { init_command }
end
end
diff --git a/spec/lib/gitlab/kubernetes/helm/install_command_spec.rb b/spec/lib/gitlab/kubernetes/helm/install_command_spec.rb
index a5ed8f57bf3..abd29e97505 100644
--- a/spec/lib/gitlab/kubernetes/helm/install_command_spec.rb
+++ b/spec/lib/gitlab/kubernetes/helm/install_command_spec.rb
@@ -3,14 +3,7 @@
require 'spec_helper'
describe Gitlab::Kubernetes::Helm::InstallCommand do
- let(:files) { { 'ca.pem': 'some file content' } }
- let(:repository) { 'https://repository.example.com' }
- let(:rbac) { false }
- let(:version) { '1.2.3' }
- let(:preinstall) { nil }
- let(:postinstall) { nil }
-
- let(:install_command) do
+ subject(:install_command) do
described_class.new(
name: 'app-name',
chart: 'chart-name',
@@ -23,9 +16,14 @@ describe Gitlab::Kubernetes::Helm::InstallCommand do
)
end
- subject { install_command }
+ let(:files) { { 'ca.pem': 'some file content' } }
+ let(:repository) { 'https://repository.example.com' }
+ let(:rbac) { false }
+ let(:version) { '1.2.3' }
+ let(:preinstall) { nil }
+ let(:postinstall) { nil }
- it_behaves_like 'helm commands' do
+ it_behaves_like 'helm command generator' do
let(:commands) do
<<~EOS
export HELM_HOST="localhost:44134"
@@ -66,7 +64,7 @@ describe Gitlab::Kubernetes::Helm::InstallCommand do
EOS
end
- it_behaves_like 'helm commands' do
+ it_behaves_like 'helm command generator' do
let(:commands) do
<<~EOS
helm init --upgrade
@@ -97,7 +95,7 @@ describe Gitlab::Kubernetes::Helm::InstallCommand do
context 'when rbac is true' do
let(:rbac) { true }
- it_behaves_like 'helm commands' do
+ it_behaves_like 'helm command generator' do
let(:commands) do
<<~EOS
export HELM_HOST="localhost:44134"
@@ -128,7 +126,7 @@ describe Gitlab::Kubernetes::Helm::InstallCommand do
context 'when there is a pre-install script' do
let(:preinstall) { ['/bin/date', '/bin/true'] }
- it_behaves_like 'helm commands' do
+ it_behaves_like 'helm command generator' do
let(:commands) do
<<~EOS
export HELM_HOST="localhost:44134"
@@ -161,7 +159,7 @@ describe Gitlab::Kubernetes::Helm::InstallCommand do
context 'when there is a post-install script' do
let(:postinstall) { ['/bin/date', "/bin/false\n"] }
- it_behaves_like 'helm commands' do
+ it_behaves_like 'helm command generator' do
let(:commands) do
<<~EOS
export HELM_HOST="localhost:44134"
@@ -194,7 +192,7 @@ describe Gitlab::Kubernetes::Helm::InstallCommand do
context 'when there is no ca.pem file' do
let(:files) { { 'file.txt': 'some content' } }
- it_behaves_like 'helm commands' do
+ it_behaves_like 'helm command generator' do
let(:commands) do
<<~EOS
export HELM_HOST="localhost:44134"
@@ -225,7 +223,7 @@ describe Gitlab::Kubernetes::Helm::InstallCommand do
context 'when there is no version' do
let(:version) { nil }
- it_behaves_like 'helm commands' do
+ it_behaves_like 'helm command generator' do
let(:commands) do
<<~EOS
export HELM_HOST="localhost:44134"
@@ -252,57 +250,7 @@ describe Gitlab::Kubernetes::Helm::InstallCommand do
end
end
- describe '#rbac?' do
- subject { install_command.rbac? }
-
- context 'rbac is enabled' do
- let(:rbac) { true }
-
- it { is_expected.to be_truthy }
- end
-
- context 'rbac is not enabled' do
- let(:rbac) { false }
-
- it { is_expected.to be_falsey }
- end
- end
-
- describe '#pod_resource' do
- subject { install_command.pod_resource }
-
- context 'rbac is enabled' do
- let(:rbac) { true }
-
- it 'generates a pod that uses the tiller serviceAccountName' do
- expect(subject.spec.serviceAccountName).to eq('tiller')
- end
- end
-
- context 'rbac is not enabled' do
- let(:rbac) { false }
-
- it 'generates a pod that uses the default serviceAccountName' do
- expect(subject.spec.serviceAcccountName).to be_nil
- end
- end
- end
-
- describe '#config_map_resource' do
- let(:metadata) do
- {
- name: "values-content-configuration-app-name",
- namespace: 'gitlab-managed-apps',
- labels: { name: "values-content-configuration-app-name" }
- }
- end
-
- let(:resource) { ::Kubeclient::Resource.new(metadata: metadata, data: files) }
-
- subject { install_command.config_map_resource }
-
- it 'returns a KubeClient resource with config map content for the application' do
- is_expected.to eq(resource)
- end
+ it_behaves_like 'helm command' do
+ let(:command) { install_command }
end
end
diff --git a/spec/lib/gitlab/kubernetes/helm/parsers/list_v2_spec.rb b/spec/lib/gitlab/kubernetes/helm/parsers/list_v2_spec.rb
new file mode 100644
index 00000000000..0ad5dc189c0
--- /dev/null
+++ b/spec/lib/gitlab/kubernetes/helm/parsers/list_v2_spec.rb
@@ -0,0 +1,100 @@
+# frozen_string_literal: true
+
+require 'fast_spec_helper'
+
+describe Gitlab::Kubernetes::Helm::Parsers::ListV2 do
+ let(:valid_file_contents) do
+ <<~EOF
+ {
+ "Next": "",
+ "Releases": [
+ {
+ "Name": "certmanager",
+ "Revision": 2,
+ "Updated": "Sun Mar 29 06:55:42 2020",
+ "Status": "DEPLOYED",
+ "Chart": "cert-manager-v0.10.1",
+ "AppVersion": "v0.10.1",
+ "Namespace": "gitlab-managed-apps"
+ },
+ {
+ "Name": "certmanager-crds",
+ "Revision": 2,
+ "Updated": "Sun Mar 29 06:55:32 2020",
+ "Status": "DEPLOYED",
+ "Chart": "cert-manager-crds-v0.2.0",
+ "AppVersion": "release-0.10",
+ "Namespace": "gitlab-managed-apps"
+ },
+ {
+ "Name": "certmanager-issuer",
+ "Revision": 1,
+ "Updated": "Tue Feb 18 10:04:04 2020",
+ "Status": "FAILED",
+ "Chart": "cert-manager-issuer-v0.1.0",
+ "AppVersion": "",
+ "Namespace": "gitlab-managed-apps"
+ },
+ {
+ "Name": "runner",
+ "Revision": 2,
+ "Updated": "Sun Mar 29 07:01:01 2020",
+ "Status": "DEPLOYED",
+ "Chart": "gitlab-runner-0.14.0",
+ "AppVersion": "12.8.0",
+ "Namespace": "gitlab-managed-apps"
+ }
+ ]
+ }
+ EOF
+ end
+
+ describe '#initialize' do
+ it 'initializes without error' do
+ expect do
+ described_class.new(valid_file_contents)
+ end.not_to raise_error
+ end
+
+ it 'raises an error on invalid JSON' do
+ expect do
+ described_class.new('')
+ end.to raise_error(described_class::ParserError)
+ end
+ end
+
+ describe '#releases' do
+ subject(:list_v2) { described_class.new(valid_file_contents) }
+
+ it 'returns list of releases' do
+ expect(list_v2.releases).to match([
+ a_hash_including('Name' => 'certmanager', 'Status' => 'DEPLOYED'),
+ a_hash_including('Name' => 'certmanager-crds', 'Status' => 'DEPLOYED'),
+ a_hash_including('Name' => 'certmanager-issuer', 'Status' => 'FAILED'),
+ a_hash_including('Name' => 'runner', 'Status' => 'DEPLOYED')
+ ])
+ end
+
+ context 'empty Releases' do
+ let(:valid_file_contents) { '{}' }
+
+ it 'returns an empty array' do
+ expect(list_v2.releases).to eq([])
+ end
+ end
+
+ context 'invalid Releases' do
+ let(:invalid_file_contents) do
+ '{ "Releases" : ["a", "b"] }'
+ end
+
+ subject(:list_v2) { described_class.new(invalid_file_contents) }
+
+ it 'raises an error' do
+ expect do
+ list_v2.releases
+ end.to raise_error(described_class::ParserError, 'Invalid format for Releases')
+ end
+ end
+ end
+end
diff --git a/spec/lib/gitlab/kubernetes/helm/patch_command_spec.rb b/spec/lib/gitlab/kubernetes/helm/patch_command_spec.rb
index e69570f5371..eee842fa7d6 100644
--- a/spec/lib/gitlab/kubernetes/helm/patch_command_spec.rb
+++ b/spec/lib/gitlab/kubernetes/helm/patch_command_spec.rb
@@ -33,7 +33,7 @@ describe Gitlab::Kubernetes::Helm::PatchCommand do
EOS
end
- it_behaves_like 'helm commands' do
+ it_behaves_like 'helm command generator' do
let(:commands) do
<<~EOS
helm init --upgrade
@@ -57,7 +57,7 @@ describe Gitlab::Kubernetes::Helm::PatchCommand do
end
end
- it_behaves_like 'helm commands' do
+ it_behaves_like 'helm command generator' do
let(:commands) do
<<~EOS
export HELM_HOST="localhost:44134"
@@ -83,7 +83,7 @@ describe Gitlab::Kubernetes::Helm::PatchCommand do
context 'when rbac is true' do
let(:rbac) { true }
- it_behaves_like 'helm commands' do
+ it_behaves_like 'helm command generator' do
let(:commands) do
<<~EOS
export HELM_HOST="localhost:44134"
@@ -110,7 +110,7 @@ describe Gitlab::Kubernetes::Helm::PatchCommand do
context 'when there is no ca.pem file' do
let(:files) { { 'file.txt': 'some content' } }
- it_behaves_like 'helm commands' do
+ it_behaves_like 'helm command generator' do
let(:commands) do
<<~EOS
export HELM_HOST="localhost:44134"
@@ -134,69 +134,19 @@ describe Gitlab::Kubernetes::Helm::PatchCommand do
end
end
- describe '#pod_name' do
- subject { patch_command.pod_name }
-
- it { is_expected.to eq 'install-app-name' }
- end
-
context 'when there is no version' do
let(:version) { nil }
it { expect { patch_command }.to raise_error(ArgumentError, 'version is required') }
end
- describe '#rbac?' do
- subject { patch_command.rbac? }
-
- context 'rbac is enabled' do
- let(:rbac) { true }
-
- it { is_expected.to be_truthy }
- end
-
- context 'rbac is not enabled' do
- let(:rbac) { false }
-
- it { is_expected.to be_falsey }
- end
- end
-
- describe '#pod_resource' do
- subject { patch_command.pod_resource }
-
- context 'rbac is enabled' do
- let(:rbac) { true }
-
- it 'generates a pod that uses the tiller serviceAccountName' do
- expect(subject.spec.serviceAccountName).to eq('tiller')
- end
- end
-
- context 'rbac is not enabled' do
- let(:rbac) { false }
+ describe '#pod_name' do
+ subject { patch_command.pod_name }
- it 'generates a pod that uses the default serviceAccountName' do
- expect(subject.spec.serviceAcccountName).to be_nil
- end
- end
+ it { is_expected.to eq 'install-app-name' }
end
- describe '#config_map_resource' do
- let(:metadata) do
- {
- name: "values-content-configuration-app-name",
- namespace: 'gitlab-managed-apps',
- labels: { name: "values-content-configuration-app-name" }
- }
- end
-
- let(:resource) { ::Kubeclient::Resource.new(metadata: metadata, data: files) }
-
- subject { patch_command.config_map_resource }
-
- it 'returns a KubeClient resource with config map content for the application' do
- is_expected.to eq(resource)
- end
+ it_behaves_like 'helm command' do
+ let(:command) { patch_command }
end
end
diff --git a/spec/lib/gitlab/kubernetes/helm/pod_spec.rb b/spec/lib/gitlab/kubernetes/helm/pod_spec.rb
index 3c62219a9a5..ea32ac96213 100644
--- a/spec/lib/gitlab/kubernetes/helm/pod_spec.rb
+++ b/spec/lib/gitlab/kubernetes/helm/pod_spec.rb
@@ -32,7 +32,7 @@ describe Gitlab::Kubernetes::Helm::Pod do
it 'generates the appropriate specifications for the container' do
container = subject.generate.spec.containers.first
expect(container.name).to eq('helm')
- expect(container.image).to eq('registry.gitlab.com/gitlab-org/cluster-integration/helm-install-image/releases/2.16.3-kube-1.13.12')
+ expect(container.image).to eq('registry.gitlab.com/gitlab-org/cluster-integration/helm-install-image/releases/2.16.6-kube-1.13.12')
expect(container.env.count).to eq(3)
expect(container.env.map(&:name)).to match_array([:HELM_VERSION, :TILLER_NAMESPACE, :COMMAND_SCRIPT])
expect(container.command).to match_array(["/bin/sh"])
diff --git a/spec/lib/gitlab/kubernetes/helm/reset_command_spec.rb b/spec/lib/gitlab/kubernetes/helm/reset_command_spec.rb
index 2a89b04723d..981bb4e4abf 100644
--- a/spec/lib/gitlab/kubernetes/helm/reset_command_spec.rb
+++ b/spec/lib/gitlab/kubernetes/helm/reset_command_spec.rb
@@ -3,14 +3,13 @@
require 'spec_helper'
describe Gitlab::Kubernetes::Helm::ResetCommand do
+ subject(:reset_command) { described_class.new(name: name, rbac: rbac, files: files) }
+
let(:rbac) { true }
let(:name) { 'helm' }
let(:files) { {} }
- let(:reset_command) { described_class.new(name: name, rbac: rbac, files: files) }
-
- subject { reset_command }
- it_behaves_like 'helm commands' do
+ it_behaves_like 'helm command generator' do
let(:commands) do
<<~EOS
helm reset
@@ -23,7 +22,7 @@ describe Gitlab::Kubernetes::Helm::ResetCommand do
context 'when there is a ca.pem file' do
let(:files) { { 'ca.pem': 'some file content' } }
- it_behaves_like 'helm commands' do
+ it_behaves_like 'helm command generator' do
let(:commands) do
<<~EOS1.squish + "\n" + <<~EOS2
helm reset
@@ -39,29 +38,13 @@ describe Gitlab::Kubernetes::Helm::ResetCommand do
end
end
- describe '#pod_resource' do
- subject { reset_command.pod_resource }
-
- context 'rbac is enabled' do
- let(:rbac) { true }
-
- it 'generates a pod that uses the tiller serviceAccountName' do
- expect(subject.spec.serviceAccountName).to eq('tiller')
- end
- end
-
- context 'rbac is not enabled' do
- let(:rbac) { false }
-
- it 'generates a pod that uses the default serviceAccountName' do
- expect(subject.spec.serviceAcccountName).to be_nil
- end
- end
- end
-
describe '#pod_name' do
subject { reset_command.pod_name }
it { is_expected.to eq('uninstall-helm') }
end
+
+ it_behaves_like 'helm command' do
+ let(:command) { reset_command }
+ end
end
diff --git a/spec/lib/gitlab/kubernetes/kube_client_spec.rb b/spec/lib/gitlab/kubernetes/kube_client_spec.rb
index 1959fbca33b..32597aa4f5a 100644
--- a/spec/lib/gitlab/kubernetes/kube_client_spec.rb
+++ b/spec/lib/gitlab/kubernetes/kube_client_spec.rb
@@ -64,6 +64,45 @@ describe Gitlab::Kubernetes::KubeClient do
end
end
+ describe '.graceful_request' do
+ context 'successful' do
+ before do
+ allow(client).to receive(:foo).and_return(true)
+ end
+
+ it 'returns connected status and foo response' do
+ result = described_class.graceful_request(1) { client.foo }
+
+ expect(result).to eq({ status: :connected, response: true })
+ end
+ end
+
+ context 'errored' do
+ using RSpec::Parameterized::TableSyntax
+
+ where(:error, :error_status) do
+ SocketError | :unreachable
+ OpenSSL::X509::CertificateError | :authentication_failure
+ StandardError | :unknown_failure
+ Kubeclient::HttpError.new(408, "timed out", nil) | :unreachable
+ Kubeclient::HttpError.new(408, "timeout", nil) | :unreachable
+ Kubeclient::HttpError.new(408, "", nil) | :authentication_failure
+ end
+
+ with_them do
+ before do
+ allow(client).to receive(:foo).and_raise(error)
+ end
+
+ it 'returns error status' do
+ result = described_class.graceful_request(1) { client.foo }
+
+ expect(result).to eq({ status: error_status })
+ end
+ end
+ end
+ end
+
describe '#initialize' do
shared_examples 'local address' do
it 'blocks local addresses' do
@@ -174,10 +213,39 @@ describe Gitlab::Kubernetes::KubeClient do
end
end
+ describe '#networking_client' do
+ subject { client.networking_client }
+
+ it_behaves_like 'a Kubeclient'
+
+ it 'has the networking API group endpoint' do
+ expect(subject.api_endpoint.to_s).to match(%r{\/apis\/networking.k8s.io\Z})
+ end
+
+ it 'has the api_version' do
+ expect(subject.instance_variable_get(:@api_version)).to eq('v1')
+ end
+ end
+
+ describe '#metrics_client' do
+ subject { client.metrics_client }
+
+ it_behaves_like 'a Kubeclient'
+
+ it 'has the metrics API group endpoint' do
+ expect(subject.api_endpoint.to_s).to match(%r{\/apis\/metrics.k8s.io\Z})
+ end
+
+ it 'has the api_version' do
+ expect(subject.instance_variable_get(:@api_version)).to eq('v1beta1')
+ end
+ end
+
describe 'core API' do
let(:core_client) { client.core_client }
[
+ :get_nodes,
:get_pods,
:get_secrets,
:get_config_map,
@@ -220,8 +288,6 @@ describe Gitlab::Kubernetes::KubeClient do
:create_role,
:get_role,
:update_role,
- :create_cluster_role_binding,
- :get_cluster_role_binding,
:update_cluster_role_binding
].each do |method|
describe "##{method}" do
@@ -290,6 +356,30 @@ describe Gitlab::Kubernetes::KubeClient do
end
end
+ describe 'networking API group' do
+ let(:networking_client) { client.networking_client }
+
+ [
+ :create_network_policy,
+ :get_network_policies,
+ :update_network_policy,
+ :delete_network_policy
+ ].each do |method|
+ describe "##{method}" do
+ include_examples 'redirection not allowed', method
+ include_examples 'dns rebinding not allowed', method
+
+ it 'delegates to the networking client' do
+ expect(client).to delegate_method(method).to(:networking_client)
+ end
+
+ it 'responds to the method' do
+ expect(client).to respond_to method
+ end
+ end
+ end
+ end
+
describe 'non-entity methods' do
it 'does not proxy for non-entity methods' do
expect(client).not_to respond_to :proxy_url
@@ -316,6 +406,16 @@ describe Gitlab::Kubernetes::KubeClient do
end
end
+ shared_examples 'create_or_update method using put' do
+ let(:update_method) { "update_#{resource_type}" }
+
+ it 'calls the update method' do
+ expect(client).to receive(update_method).with(resource)
+
+ subject
+ end
+ end
+
shared_examples 'create_or_update method' do
let(:get_method) { "get_#{resource_type}" }
let(:update_method) { "update_#{resource_type}" }
@@ -355,7 +455,7 @@ describe Gitlab::Kubernetes::KubeClient do
subject { client.create_or_update_cluster_role_binding(resource) }
- it_behaves_like 'create_or_update method'
+ it_behaves_like 'create_or_update method using put'
end
describe '#create_or_update_role_binding' do
@@ -367,7 +467,7 @@ describe Gitlab::Kubernetes::KubeClient do
subject { client.create_or_update_role_binding(resource) }
- it_behaves_like 'create_or_update method'
+ it_behaves_like 'create_or_update method using put'
end
describe '#create_or_update_service_account' do
diff --git a/spec/lib/gitlab/kubernetes/network_policy_spec.rb b/spec/lib/gitlab/kubernetes/network_policy_spec.rb
new file mode 100644
index 00000000000..f23d215a9a1
--- /dev/null
+++ b/spec/lib/gitlab/kubernetes/network_policy_spec.rb
@@ -0,0 +1,224 @@
+# frozen_string_literal: true
+
+require 'spec_helper'
+
+describe Gitlab::Kubernetes::NetworkPolicy do
+ let(:policy) do
+ described_class.new(
+ name: name,
+ namespace: namespace,
+ creation_timestamp: '2020-04-14T00:08:30Z',
+ pod_selector: pod_selector,
+ policy_types: %w(Ingress Egress),
+ ingress: ingress,
+ egress: egress
+ )
+ end
+
+ let(:name) { 'example-name' }
+ let(:namespace) { 'example-namespace' }
+ let(:pod_selector) { { matchLabels: { role: 'db' } } }
+
+ let(:ingress) do
+ [
+ {
+ from: [
+ { namespaceSelector: { matchLabels: { project: 'myproject' } } }
+ ]
+ }
+ ]
+ end
+
+ let(:egress) do
+ [
+ {
+ ports: [{ port: 5978 }]
+ }
+ ]
+ end
+
+ describe '.from_yaml' do
+ let(:manifest) do
+ <<-POLICY
+apiVersion: networking.k8s.io/v1
+kind: NetworkPolicy
+metadata:
+ name: example-name
+ namespace: example-namespace
+spec:
+ podSelector:
+ matchLabels:
+ role: db
+ policyTypes:
+ - Ingress
+ ingress:
+ - from:
+ - namespaceSelector:
+ matchLabels:
+ project: myproject
+ POLICY
+ end
+ let(:resource) do
+ ::Kubeclient::Resource.new(
+ metadata: { name: name, namespace: namespace },
+ spec: { podSelector: pod_selector, policyTypes: %w(Ingress), ingress: ingress, egress: nil }
+ )
+ end
+
+ subject { Gitlab::Kubernetes::NetworkPolicy.from_yaml(manifest)&.generate }
+
+ it { is_expected.to eq(resource) }
+
+ context 'with nil manifest' do
+ let(:manifest) { nil }
+
+ it { is_expected.to be_nil }
+ end
+
+ context 'with invalid manifest' do
+ let(:manifest) { "\tfoo: bar" }
+
+ it { is_expected.to be_nil }
+ end
+
+ context 'with manifest without metadata' do
+ let(:manifest) do
+ <<-POLICY
+apiVersion: networking.k8s.io/v1
+kind: NetworkPolicy
+spec:
+ podSelector:
+ matchLabels:
+ role: db
+ policyTypes:
+ - Ingress
+ ingress:
+ - from:
+ - namespaceSelector:
+ matchLabels:
+ project: myproject
+ POLICY
+ end
+
+ it { is_expected.to be_nil }
+ end
+
+ context 'with manifest without spec' do
+ let(:manifest) do
+ <<-POLICY
+apiVersion: networking.k8s.io/v1
+kind: NetworkPolicy
+metadata:
+ name: example-name
+ namespace: example-namespace
+ POLICY
+ end
+
+ it { is_expected.to be_nil }
+ end
+
+ context 'with disallowed class' do
+ let(:manifest) do
+ <<-POLICY
+apiVersion: networking.k8s.io/v1
+kind: NetworkPolicy
+metadata:
+ name: example-name
+ namespace: example-namespace
+ creationTimestamp: 2020-04-14T00:08:30Z
+spec:
+ podSelector:
+ matchLabels:
+ role: db
+ policyTypes:
+ - Ingress
+ ingress:
+ - from:
+ - namespaceSelector:
+ matchLabels:
+ project: myproject
+ POLICY
+ end
+
+ it { is_expected.to be_nil }
+ end
+ end
+
+ describe '.from_resource' do
+ let(:resource) do
+ ::Kubeclient::Resource.new(
+ metadata: { name: name, namespace: namespace, creationTimestamp: '2020-04-14T00:08:30Z', resourceVersion: '4990' },
+ spec: { podSelector: pod_selector, policyTypes: %w(Ingress), ingress: ingress, egress: nil }
+ )
+ end
+ let(:generated_resource) do
+ ::Kubeclient::Resource.new(
+ metadata: { name: name, namespace: namespace },
+ spec: { podSelector: pod_selector, policyTypes: %w(Ingress), ingress: ingress, egress: nil }
+ )
+ end
+
+ subject { Gitlab::Kubernetes::NetworkPolicy.from_resource(resource)&.generate }
+
+ it { is_expected.to eq(generated_resource) }
+
+ context 'with nil resource' do
+ let(:resource) { nil }
+
+ it { is_expected.to be_nil }
+ end
+
+ context 'with resource without metadata' do
+ let(:resource) do
+ ::Kubeclient::Resource.new(
+ spec: { podSelector: pod_selector, policyTypes: %w(Ingress), ingress: ingress, egress: nil }
+ )
+ end
+
+ it { is_expected.to be_nil }
+ end
+
+ context 'with resource without spec' do
+ let(:resource) do
+ ::Kubeclient::Resource.new(
+ metadata: { name: name, namespace: namespace, uid: '128cf288-7de4-11ea-aceb-42010a800089', resourceVersion: '4990' }
+ )
+ end
+
+ it { is_expected.to be_nil }
+ end
+ end
+
+ describe '#generate' do
+ let(:resource) do
+ ::Kubeclient::Resource.new(
+ metadata: { name: name, namespace: namespace },
+ spec: { podSelector: pod_selector, policyTypes: %w(Ingress Egress), ingress: ingress, egress: egress }
+ )
+ end
+
+ subject { policy.generate }
+
+ it { is_expected.to eq(resource) }
+ end
+
+ describe '#as_json' do
+ let(:json_policy) do
+ {
+ name: name,
+ namespace: namespace,
+ creation_timestamp: '2020-04-14T00:08:30Z',
+ manifest: YAML.dump(
+ {
+ metadata: { name: name, namespace: namespace },
+ spec: { podSelector: pod_selector, policyTypes: %w(Ingress Egress), ingress: ingress, egress: egress }
+ }.deep_stringify_keys
+ )
+ }
+ end
+
+ subject { policy.as_json }
+
+ it { is_expected.to eq(json_policy) }
+ end
+end
diff --git a/spec/lib/gitlab/legacy_github_import/importer_spec.rb b/spec/lib/gitlab/legacy_github_import/importer_spec.rb
index af0bffa91a5..8cc3fd8efbd 100644
--- a/spec/lib/gitlab/legacy_github_import/importer_spec.rb
+++ b/spec/lib/gitlab/legacy_github_import/importer_spec.rb
@@ -294,6 +294,7 @@ describe Gitlab::LegacyGithubImport::Importer do
it_behaves_like 'Gitlab::LegacyGithubImport::Importer#execute' do
let(:expected_not_called) { [:import_releases, [:import_comments, :pull_requests]] }
end
+
it_behaves_like 'Gitlab::LegacyGithubImport::Importer#execute an error occurs'
it_behaves_like 'Gitlab::LegacyGithubImport unit-testing'
diff --git a/spec/lib/gitlab/logging/cloudflare_helper_spec.rb b/spec/lib/gitlab/logging/cloudflare_helper_spec.rb
new file mode 100644
index 00000000000..2b73fb7bc1c
--- /dev/null
+++ b/spec/lib/gitlab/logging/cloudflare_helper_spec.rb
@@ -0,0 +1,52 @@
+# frozen_string_literal: true
+
+require 'spec_helper'
+
+describe Gitlab::Logging::CloudflareHelper do
+ let(:helper) do
+ Class.new do
+ include Gitlab::Logging::CloudflareHelper
+ end.new
+ end
+
+ describe '#store_cloudflare_headers!' do
+ let(:payload) { {} }
+ let(:env) { {} }
+ let(:request) { ActionDispatch::Request.new(env) }
+
+ before do
+ request.headers.merge!(headers)
+ end
+
+ context 'with normal headers' do
+ let(:headers) { { 'Cf-Ray' => '592f0aa22b3dea38-IAD', 'Cf-Request-Id' => SecureRandom.hex } }
+
+ it 'adds Cf-Ray-Id and Cf-Request-Id' do
+ helper.store_cloudflare_headers!(payload, request)
+
+ expect(payload[:cf_ray]).to eq(headers['Cf-Ray'])
+ expect(payload[:cf_request_id]).to eq(headers['Cf-Request-Id'])
+ end
+ end
+
+ context 'with header values with long strings' do
+ let(:headers) { { 'Cf-Ray' => SecureRandom.hex(33), 'Cf-Request-Id' => SecureRandom.hex(33) } }
+
+ it 'filters invalid header values' do
+ helper.store_cloudflare_headers!(payload, request)
+
+ expect(payload.keys).not_to include(:cf_ray, :cf_request_id)
+ end
+ end
+
+ context 'with header values with non-alphanumeric characters' do
+ let(:headers) { { 'Cf-Ray' => "Bad\u0000ray", 'Cf-Request-Id' => "Bad\u0000req" } }
+
+ it 'filters invalid header values' do
+ helper.store_cloudflare_headers!(payload, request)
+
+ expect(payload.keys).not_to include(:cf_ray, :cf_request_id)
+ end
+ end
+ end
+end
diff --git a/spec/lib/gitlab/lograge/custom_options_spec.rb b/spec/lib/gitlab/lograge/custom_options_spec.rb
index 48d06283b7a..7ae8baa31b5 100644
--- a/spec/lib/gitlab/lograge/custom_options_spec.rb
+++ b/spec/lib/gitlab/lograge/custom_options_spec.rb
@@ -19,7 +19,13 @@ describe Gitlab::Lograge::CustomOptions do
1,
2,
'transaction_id',
- { params: params, user_id: 'test' }
+ {
+ params: params,
+ user_id: 'test',
+ cf_ray: SecureRandom.hex,
+ cf_request_id: SecureRandom.hex,
+ metadata: { 'meta.user' => 'jane.doe' }
+ }
)
end
@@ -46,5 +52,30 @@ describe Gitlab::Lograge::CustomOptions do
it 'adds the user id' do
expect(subject[:user_id]).to eq('test')
end
+
+ it 'adds Cloudflare headers' do
+ expect(subject[:cf_ray]).to eq(event.payload[:cf_ray])
+ expect(subject[:cf_request_id]).to eq(event.payload[:cf_request_id])
+ end
+
+ it 'adds the metadata' do
+ expect(subject['meta.user']).to eq('jane.doe')
+ end
+
+ context 'when metadata is missing' do
+ let(:event) do
+ ActiveSupport::Notifications::Event.new(
+ 'test',
+ 1,
+ 2,
+ 'transaction_id',
+ { params: {} }
+ )
+ end
+
+ it 'does not break' do
+ expect { subject }.not_to raise_error
+ end
+ end
end
end
diff --git a/spec/lib/gitlab/mail_room/mail_room_spec.rb b/spec/lib/gitlab/mail_room/mail_room_spec.rb
index 5d41ee06263..4b09205a181 100644
--- a/spec/lib/gitlab/mail_room/mail_room_spec.rb
+++ b/spec/lib/gitlab/mail_room/mail_room_spec.rb
@@ -13,7 +13,8 @@ describe Gitlab::MailRoom do
start_tls: false,
mailbox: 'inbox',
idle_timeout: 60,
- log_path: Rails.root.join('log', 'mail_room_json.log').to_s
+ log_path: Rails.root.join('log', 'mail_room_json.log').to_s,
+ expunge_deleted: false
}
end
diff --git a/spec/lib/gitlab/metrics/background_transaction_spec.rb b/spec/lib/gitlab/metrics/background_transaction_spec.rb
index d87d2c839ad..84f405d7369 100644
--- a/spec/lib/gitlab/metrics/background_transaction_spec.rb
+++ b/spec/lib/gitlab/metrics/background_transaction_spec.rb
@@ -7,12 +7,6 @@ describe Gitlab::Metrics::BackgroundTransaction do
subject { described_class.new(test_worker_class) }
- describe '#action' do
- it 'returns transaction action name' do
- expect(subject.action).to eq('TestWorker#perform')
- end
- end
-
describe '#label' do
it 'returns labels based on class name' do
expect(subject.labels).to eq(controller: 'TestWorker', action: 'perform')
diff --git a/spec/lib/gitlab/metrics/dashboard/stages/grafana_formatter_spec.rb b/spec/lib/gitlab/metrics/dashboard/stages/grafana_formatter_spec.rb
index e41004bb57e..5d4bd4512e3 100644
--- a/spec/lib/gitlab/metrics/dashboard/stages/grafana_formatter_spec.rb
+++ b/spec/lib/gitlab/metrics/dashboard/stages/grafana_formatter_spec.rb
@@ -9,9 +9,9 @@ describe Gitlab::Metrics::Dashboard::Stages::GrafanaFormatter do
let_it_be(:project) { create(:project, namespace: namespace, name: 'bar') }
describe '#transform!' do
- let(:grafana_dashboard) { JSON.parse(fixture_file('grafana/simplified_dashboard_response.json'), symbolize_names: true) }
- let(:datasource) { JSON.parse(fixture_file('grafana/datasource_response.json'), symbolize_names: true) }
- let(:expected_dashboard) { JSON.parse(fixture_file('grafana/expected_grafana_embed.json'), symbolize_names: true) }
+ let(:grafana_dashboard) { Gitlab::Json.parse(fixture_file('grafana/simplified_dashboard_response.json'), symbolize_names: true) }
+ let(:datasource) { Gitlab::Json.parse(fixture_file('grafana/datasource_response.json'), symbolize_names: true) }
+ let(:expected_dashboard) { Gitlab::Json.parse(fixture_file('grafana/expected_grafana_embed.json'), symbolize_names: true) }
subject(:dashboard) { described_class.new(project, {}, params).transform! }
diff --git a/spec/lib/gitlab/metrics/dashboard/url_spec.rb b/spec/lib/gitlab/metrics/dashboard/url_spec.rb
index 9ccd1c06d6b..75f9f99c8a6 100644
--- a/spec/lib/gitlab/metrics/dashboard/url_spec.rb
+++ b/spec/lib/gitlab/metrics/dashboard/url_spec.rb
@@ -3,17 +3,21 @@
require 'spec_helper'
describe Gitlab::Metrics::Dashboard::Url do
+ include Gitlab::Routing.url_helpers
+
describe '#metrics_regex' do
- let(:url) do
- Gitlab::Routing.url_helpers.metrics_namespace_project_environment_url(
+ let(:url_params) do
+ [
'foo',
'bar',
1,
- start: '2019-08-02T05:43:09.000Z',
- dashboard: 'config/prometheus/common_metrics.yml',
- group: 'awesome group',
- anchor: 'title'
- )
+ {
+ start: '2019-08-02T05:43:09.000Z',
+ dashboard: 'config/prometheus/common_metrics.yml',
+ group: 'awesome group',
+ anchor: 'title'
+ }
+ ]
end
let(:expected_params) do
@@ -29,12 +33,22 @@ describe Gitlab::Metrics::Dashboard::Url do
subject { described_class.metrics_regex }
- it_behaves_like 'regex which matches url when expected'
+ context 'for metrics route' do
+ let(:url) { metrics_namespace_project_environment_url(*url_params) }
+
+ it_behaves_like 'regex which matches url when expected'
+ end
+
+ context 'for metrics_dashboard route' do
+ let(:url) { metrics_dashboard_namespace_project_environment_url(*url_params) }
+
+ it_behaves_like 'regex which matches url when expected'
+ end
end
describe '#grafana_regex' do
let(:url) do
- Gitlab::Routing.url_helpers.namespace_project_grafana_api_metrics_dashboard_url(
+ namespace_project_grafana_api_metrics_dashboard_url(
'foo',
'bar',
start: '2019-08-02T05:43:09.000Z',
diff --git a/spec/lib/gitlab/metrics/exporter/sidekiq_exporter_spec.rb b/spec/lib/gitlab/metrics/exporter/sidekiq_exporter_spec.rb
index a415b6407d5..0b820fdbde9 100644
--- a/spec/lib/gitlab/metrics/exporter/sidekiq_exporter_spec.rb
+++ b/spec/lib/gitlab/metrics/exporter/sidekiq_exporter_spec.rb
@@ -53,7 +53,7 @@ describe Gitlab::Metrics::Exporter::SidekiqExporter do
.with(
class: described_class.to_s,
message: 'Cannot start sidekiq_exporter',
- exception: anything)
+ 'exception.message' => anything)
exporter.start
end
diff --git a/spec/lib/gitlab/metrics/method_call_spec.rb b/spec/lib/gitlab/metrics/method_call_spec.rb
index 3b5e04e2df5..229db67ec88 100644
--- a/spec/lib/gitlab/metrics/method_call_spec.rb
+++ b/spec/lib/gitlab/metrics/method_call_spec.rb
@@ -76,25 +76,6 @@ describe Gitlab::Metrics::MethodCall do
end
end
- describe '#to_metric' do
- it 'returns a Metric instance' do
- expect(method_call).to receive(:real_time).and_return(4.0001).twice
- expect(method_call).to receive(:cpu_time).and_return(3.0001)
-
- method_call.measure { 'foo' }
- metric = method_call.to_metric
-
- expect(metric).to be_an_instance_of(Gitlab::Metrics::Metric)
- expect(metric.series).to eq('rails_method_calls')
-
- expect(metric.values[:duration]).to eq(4000)
- expect(metric.values[:cpu_duration]).to eq(3000)
- expect(metric.values[:call_count]).to be_an(Integer)
-
- expect(metric.tags).to eq({ method: 'Foo#bar' })
- end
- end
-
describe '#above_threshold?' do
before do
allow(Gitlab::Metrics).to receive(:method_call_threshold).and_return(100)
diff --git a/spec/lib/gitlab/metrics/metric_spec.rb b/spec/lib/gitlab/metrics/metric_spec.rb
deleted file mode 100644
index 611b59231ba..00000000000
--- a/spec/lib/gitlab/metrics/metric_spec.rb
+++ /dev/null
@@ -1,71 +0,0 @@
-# frozen_string_literal: true
-
-require 'spec_helper'
-
-describe Gitlab::Metrics::Metric do
- let(:metric) do
- described_class.new('foo', { number: 10 }, { host: 'localtoast' })
- end
-
- describe '#series' do
- subject { metric.series }
-
- it { is_expected.to eq('foo') }
- end
-
- describe '#values' do
- subject { metric.values }
-
- it { is_expected.to eq({ number: 10 }) }
- end
-
- describe '#tags' do
- subject { metric.tags }
-
- it { is_expected.to eq({ host: 'localtoast' }) }
- end
-
- describe '#type' do
- subject { metric.type }
-
- it { is_expected.to eq(:metric) }
- end
-
- describe '#event?' do
- it 'returns false for a regular metric' do
- expect(metric.event?).to eq(false)
- end
-
- it 'returns true for an event metric' do
- expect(metric).to receive(:type).and_return(:event)
-
- expect(metric.event?).to eq(true)
- end
- end
-
- describe '#to_hash' do
- it 'returns a Hash' do
- expect(metric.to_hash).to be_an_instance_of(Hash)
- end
-
- describe 'the returned Hash' do
- let(:hash) { metric.to_hash }
-
- it 'includes the series' do
- expect(hash[:series]).to eq('foo')
- end
-
- it 'includes the tags' do
- expect(hash[:tags]).to be_an_instance_of(Hash)
- end
-
- it 'includes the values' do
- expect(hash[:values]).to eq({ number: 10 })
- end
-
- it 'includes the timestamp' do
- expect(hash[:timestamp]).to be_an(Integer)
- end
- end
- end
-end
diff --git a/spec/lib/gitlab/metrics/rack_middleware_spec.rb b/spec/lib/gitlab/metrics/rack_middleware_spec.rb
index 1c1681cc5ab..dd1dbf7a1f4 100644
--- a/spec/lib/gitlab/metrics/rack_middleware_spec.rb
+++ b/spec/lib/gitlab/metrics/rack_middleware_spec.rb
@@ -10,10 +10,6 @@ describe Gitlab::Metrics::RackMiddleware do
let(:env) { { 'REQUEST_METHOD' => 'GET', 'REQUEST_URI' => '/foo' } }
describe '#call' do
- before do
- expect_any_instance_of(Gitlab::Metrics::Transaction).to receive(:finish)
- end
-
it 'tracks a transaction' do
expect(app).to receive(:call).with(env).and_return('yay')
@@ -36,26 +32,5 @@ describe Gitlab::Metrics::RackMiddleware do
it 'returns a Transaction' do
expect(transaction).to be_an_instance_of(Gitlab::Metrics::WebTransaction)
end
-
- it 'stores the request method and URI in the transaction as values' do
- expect(transaction.values[:request_method]).to eq('GET')
- expect(transaction.values[:request_uri]).to eq('/foo')
- end
-
- context "when URI includes sensitive parameters" do
- let(:env) do
- {
- 'REQUEST_METHOD' => 'GET',
- 'REQUEST_URI' => '/foo?private_token=my-token',
- 'PATH_INFO' => '/foo',
- 'QUERY_STRING' => 'private_token=my_token',
- 'action_dispatch.parameter_filter' => [:private_token]
- }
- end
-
- it 'stores the request URI with the sensitive parameters filtered' do
- expect(transaction.values[:request_uri]).to eq('/foo?private_token=[FILTERED]')
- end
- end
end
end
diff --git a/spec/lib/gitlab/metrics/samplers/database_sampler_spec.rb b/spec/lib/gitlab/metrics/samplers/database_sampler_spec.rb
new file mode 100644
index 00000000000..fdf3b5bd045
--- /dev/null
+++ b/spec/lib/gitlab/metrics/samplers/database_sampler_spec.rb
@@ -0,0 +1,49 @@
+# frozen_string_literal: true
+
+require 'spec_helper'
+
+describe Gitlab::Metrics::Samplers::DatabaseSampler do
+ subject { described_class.new(described_class::SAMPLING_INTERVAL_SECONDS) }
+
+ describe '#sample' do
+ before do
+ described_class::METRIC_DESCRIPTIONS.each_key do |metric|
+ allow(subject.metrics[metric]).to receive(:set)
+ end
+ end
+
+ context 'for ActiveRecord::Base' do
+ let(:labels) do
+ {
+ class: 'ActiveRecord::Base',
+ host: Gitlab::Database.config['host'],
+ port: Gitlab::Database.config['port']
+ }
+ end
+
+ context 'when the database is connected' do
+ it 'samples connection pool statistics' do
+ expect(subject.metrics[:size]).to receive(:set).with(labels, a_value >= 1)
+ expect(subject.metrics[:connections]).to receive(:set).with(labels, a_value >= 1)
+ expect(subject.metrics[:busy]).to receive(:set).with(labels, a_value >= 1)
+ expect(subject.metrics[:dead]).to receive(:set).with(labels, a_value >= 0)
+ expect(subject.metrics[:waiting]).to receive(:set).with(labels, a_value >= 0)
+
+ subject.sample
+ end
+ end
+
+ context 'when the database is not connected' do
+ before do
+ allow(ActiveRecord::Base).to receive(:connected?).and_return(false)
+ end
+
+ it 'records no samples' do
+ expect(subject.metrics[:size]).not_to receive(:set).with(labels, anything)
+
+ subject.sample
+ end
+ end
+ end
+ end
+end
diff --git a/spec/lib/gitlab/metrics/samplers/influx_sampler_spec.rb b/spec/lib/gitlab/metrics/samplers/influx_sampler_spec.rb
deleted file mode 100644
index 939c057c342..00000000000
--- a/spec/lib/gitlab/metrics/samplers/influx_sampler_spec.rb
+++ /dev/null
@@ -1,105 +0,0 @@
-# frozen_string_literal: true
-
-require 'spec_helper'
-
-describe Gitlab::Metrics::Samplers::InfluxSampler do
- let(:sampler) { described_class.new(5) }
-
- describe '#start' do
- it 'runs once and gathers a sample at a given interval' do
- expect(sampler).to receive(:sleep).with(a_kind_of(Numeric)).twice
- expect(sampler).to receive(:sample).once
- expect(sampler).to receive(:running).and_return(true, false)
-
- sampler.start.join
- end
- end
-
- describe '#sample' do
- it 'samples various statistics' do
- expect(sampler).to receive(:sample_memory_usage)
- expect(sampler).to receive(:sample_file_descriptors)
- expect(sampler).to receive(:flush)
-
- sampler.sample
- end
- end
-
- describe '#flush' do
- it 'schedules the metrics using Sidekiq' do
- expect(Gitlab::Metrics).to receive(:submit_metrics)
- .with([an_instance_of(Hash)])
-
- sampler.sample_memory_usage
- sampler.flush
- end
- end
-
- describe '#sample_memory_usage' do
- it 'adds a metric containing the memory usage' do
- expect(Gitlab::Metrics::System).to receive(:memory_usage)
- .and_return(9000)
-
- expect(sampler).to receive(:add_metric)
- .with(/memory_usage/, value: 9000)
- .and_call_original
-
- sampler.sample_memory_usage
- end
- end
-
- describe '#sample_file_descriptors' do
- it 'adds a metric containing the amount of open file descriptors' do
- expect(Gitlab::Metrics::System).to receive(:file_descriptor_count)
- .and_return(4)
-
- expect(sampler).to receive(:add_metric)
- .with(/file_descriptors/, value: 4)
- .and_call_original
-
- sampler.sample_file_descriptors
- end
- end
-
- describe '#add_metric' do
- it 'prefixes the series name for a Rails process' do
- expect(Gitlab::Runtime).to receive(:sidekiq?).and_return(false)
-
- expect(Gitlab::Metrics::Metric).to receive(:new)
- .with('rails_cats', { value: 10 }, {})
- .and_call_original
-
- sampler.add_metric('cats', value: 10)
- end
-
- it 'prefixes the series name for a Sidekiq process' do
- expect(Gitlab::Runtime).to receive(:sidekiq?).and_return(true)
-
- expect(Gitlab::Metrics::Metric).to receive(:new)
- .with('sidekiq_cats', { value: 10 }, {})
- .and_call_original
-
- sampler.add_metric('cats', value: 10)
- end
- end
-
- describe '#sleep_interval' do
- it 'returns a Numeric' do
- expect(sampler.sleep_interval).to be_a_kind_of(Numeric)
- end
-
- # Testing random behaviour is very hard, so treat this test as a basic smoke
- # test instead of a very accurate behaviour/unit test.
- it 'does not return the same interval twice in a row' do
- last = nil
-
- 100.times do
- interval = sampler.sleep_interval
-
- expect(interval).not_to eq(last)
-
- last = interval
- end
- end
- end
-end
diff --git a/spec/lib/gitlab/metrics/samplers/ruby_sampler_spec.rb b/spec/lib/gitlab/metrics/samplers/ruby_sampler_spec.rb
index 8c4071a7ed1..ead650a27f0 100644
--- a/spec/lib/gitlab/metrics/samplers/ruby_sampler_spec.rb
+++ b/spec/lib/gitlab/metrics/samplers/ruby_sampler_spec.rb
@@ -19,24 +19,38 @@ describe Gitlab::Metrics::Samplers::RubySampler do
end
describe '#sample' do
- it 'samples various statistics' do
- expect(Gitlab::Metrics::System).to receive(:cpu_time)
- expect(Gitlab::Metrics::System).to receive(:file_descriptor_count)
- expect(Gitlab::Metrics::System).to receive(:memory_usage)
- expect(Gitlab::Metrics::System).to receive(:max_open_file_descriptors)
- expect(sampler).to receive(:sample_gc)
+ it 'adds a metric containing the process resident memory bytes' do
+ expect(Gitlab::Metrics::System).to receive(:memory_usage_rss).and_return(9000)
+
+ expect(sampler.metrics[:process_resident_memory_bytes]).to receive(:set).with({}, 9000)
sampler.sample
end
- it 'adds a metric containing the process resident memory bytes' do
- expect(Gitlab::Metrics::System).to receive(:memory_usage).and_return(9000)
+ it 'adds a metric containing the process unique and proportional memory bytes' do
+ expect(Gitlab::Metrics::System).to receive(:memory_usage_uss_pss).and_return(uss: 9000, pss: 10_000)
- expect(sampler.metrics[:process_resident_memory_bytes]).to receive(:set).with({}, 9000)
+ expect(sampler.metrics[:process_unique_memory_bytes]).to receive(:set).with({}, 9000)
+ expect(sampler.metrics[:process_proportional_memory_bytes]).to receive(:set).with({}, 10_000)
sampler.sample
end
+ context 'when USS+PSS sampling is disabled via environment' do
+ before do
+ stub_env('enable_memory_uss_pss', "0")
+ end
+
+ it 'does not sample USS or PSS' do
+ expect(Gitlab::Metrics::System).not_to receive(:memory_usage_uss_pss)
+
+ expect(sampler.metrics[:process_unique_memory_bytes]).not_to receive(:set)
+ expect(sampler.metrics[:process_proportional_memory_bytes]).not_to receive(:set)
+
+ sampler.sample
+ end
+ end
+
it 'adds a metric containing the amount of open file descriptors' do
expect(Gitlab::Metrics::System).to receive(:file_descriptor_count)
.and_return(4)
diff --git a/spec/lib/gitlab/metrics/sidekiq_middleware_spec.rb b/spec/lib/gitlab/metrics/sidekiq_middleware_spec.rb
index bb95d5ab2ad..67336cf83e6 100644
--- a/spec/lib/gitlab/metrics/sidekiq_middleware_spec.rb
+++ b/spec/lib/gitlab/metrics/sidekiq_middleware_spec.rb
@@ -17,8 +17,6 @@ describe Gitlab::Metrics::SidekiqMiddleware do
expect_any_instance_of(Gitlab::Metrics::Transaction).to receive(:set)
.with(:sidekiq_queue_duration, instance_of(Float))
- expect_any_instance_of(Gitlab::Metrics::Transaction).to receive(:finish)
-
middleware.call(worker, message, :test) { nil }
end
@@ -32,8 +30,6 @@ describe Gitlab::Metrics::SidekiqMiddleware do
expect_any_instance_of(Gitlab::Metrics::Transaction).to receive(:set)
.with(:sidekiq_queue_duration, instance_of(Float))
- expect_any_instance_of(Gitlab::Metrics::Transaction).to receive(:finish)
-
middleware.call(worker, {}, :test) { nil }
end
@@ -46,9 +42,6 @@ describe Gitlab::Metrics::SidekiqMiddleware do
expect_any_instance_of(Gitlab::Metrics::Transaction)
.to receive(:add_event).with(:sidekiq_exception)
- expect_any_instance_of(Gitlab::Metrics::Transaction)
- .to receive(:finish)
-
expect { middleware.call(worker, message, :test) }
.to raise_error(RuntimeError)
end
diff --git a/spec/lib/gitlab/metrics/subscribers/action_view_spec.rb b/spec/lib/gitlab/metrics/subscribers/action_view_spec.rb
index 25c0e7b695a..857e54d3432 100644
--- a/spec/lib/gitlab/metrics/subscribers/action_view_spec.rb
+++ b/spec/lib/gitlab/metrics/subscribers/action_view_spec.rb
@@ -21,15 +21,9 @@ describe Gitlab::Metrics::Subscribers::ActionView do
describe '#render_template' do
it 'tracks rendering of a template' do
- values = { duration: 2.1 }
- tags = { view: 'app/views/x.html.haml' }
-
expect(transaction).to receive(:increment)
.with(:view_duration, 2.1)
- expect(transaction).to receive(:add_metric)
- .with(described_class::SERIES, values, tags)
-
subscriber.render_template(event)
end
diff --git a/spec/lib/gitlab/metrics/system_spec.rb b/spec/lib/gitlab/metrics/system_spec.rb
index a5aa80686fd..abb6a0096d6 100644
--- a/spec/lib/gitlab/metrics/system_spec.rb
+++ b/spec/lib/gitlab/metrics/system_spec.rb
@@ -3,33 +3,122 @@
require 'spec_helper'
describe Gitlab::Metrics::System do
- if File.exist?('/proc')
- describe '.memory_usage' do
- it "returns the process' memory usage in bytes" do
- expect(described_class.memory_usage).to be > 0
+ context 'when /proc files exist' do
+ # Fixtures pulled from:
+ # Linux carbon 5.3.0-7648-generic #41~1586789791~19.10~9593806-Ubuntu SMP Mon Apr 13 17:50:40 UTC x86_64 x86_64 x86_64 GNU/Linux
+ let(:proc_status) do
+ # most rows omitted for brevity
+ <<~SNIP
+ Name: less
+ VmHWM: 2468 kB
+ VmRSS: 2468 kB
+ RssAnon: 260 kB
+ SNIP
+ end
+
+ let(:proc_smaps_rollup) do
+ # full snapshot
+ <<~SNIP
+ Rss: 2564 kB
+ Pss: 503 kB
+ Pss_Anon: 312 kB
+ Pss_File: 191 kB
+ Pss_Shmem: 0 kB
+ Shared_Clean: 2100 kB
+ Shared_Dirty: 0 kB
+ Private_Clean: 152 kB
+ Private_Dirty: 312 kB
+ Referenced: 2564 kB
+ Anonymous: 312 kB
+ LazyFree: 0 kB
+ AnonHugePages: 0 kB
+ ShmemPmdMapped: 0 kB
+ Shared_Hugetlb: 0 kB
+ Private_Hugetlb: 0 kB
+ Swap: 0 kB
+ SwapPss: 0 kB
+ Locked: 0 kB
+ SNIP
+ end
+
+ let(:proc_limits) do
+ # full snapshot
+ <<~SNIP
+ Limit Soft Limit Hard Limit Units
+ Max cpu time unlimited unlimited seconds
+ Max file size unlimited unlimited bytes
+ Max data size unlimited unlimited bytes
+ Max stack size 8388608 unlimited bytes
+ Max core file size 0 unlimited bytes
+ Max resident set unlimited unlimited bytes
+ Max processes 126519 126519 processes
+ Max open files 1024 1048576 files
+ Max locked memory 67108864 67108864 bytes
+ Max address space unlimited unlimited bytes
+ Max file locks unlimited unlimited locks
+ Max pending signals 126519 126519 signals
+ Max msgqueue size 819200 819200 bytes
+ Max nice priority 0 0
+ Max realtime priority 0 0
+ Max realtime timeout unlimited unlimited us
+ SNIP
+ end
+
+ describe '.memory_usage_rss' do
+ it "returns the process' resident set size (RSS) in bytes" do
+ mock_existing_proc_file('/proc/self/status', proc_status)
+
+ expect(described_class.memory_usage_rss).to eq(2527232)
end
end
describe '.file_descriptor_count' do
it 'returns the amount of open file descriptors' do
- expect(described_class.file_descriptor_count).to be > 0
+ expect(Dir).to receive(:glob).and_return(['/some/path', '/some/other/path'])
+
+ expect(described_class.file_descriptor_count).to eq(2)
end
end
describe '.max_open_file_descriptors' do
it 'returns the max allowed open file descriptors' do
- expect(described_class.max_open_file_descriptors).to be > 0
+ mock_existing_proc_file('/proc/self/limits', proc_limits)
+
+ expect(described_class.max_open_file_descriptors).to eq(1024)
+ end
+ end
+
+ describe '.memory_usage_uss_pss' do
+ it "returns the process' unique and porportional set size (USS/PSS) in bytes" do
+ mock_existing_proc_file('/proc/self/smaps_rollup', proc_smaps_rollup)
+
+ # (Private_Clean (152 kB) + Private_Dirty (312 kB) + Private_Hugetlb (0 kB)) * 1024
+ expect(described_class.memory_usage_uss_pss).to eq(uss: 475136, pss: 515072)
end
end
- else
- describe '.memory_usage' do
- it 'returns 0.0' do
- expect(described_class.memory_usage).to eq(0.0)
+ end
+
+ context 'when /proc files do not exist' do
+ before do
+ mock_missing_proc_file
+ end
+
+ describe '.memory_usage_rss' do
+ it 'returns 0' do
+ expect(described_class.memory_usage_rss).to eq(0)
+ end
+ end
+
+ describe '.memory_usage_uss_pss' do
+ it "returns 0 for all components" do
+ expect(described_class.memory_usage_uss_pss).to eq(uss: 0, pss: 0)
end
end
describe '.file_descriptor_count' do
it 'returns 0' do
+ expect(Dir).to receive(:glob).and_return([])
+
expect(described_class.file_descriptor_count).to eq(0)
end
end
@@ -98,4 +187,12 @@ describe Gitlab::Metrics::System do
expect(described_class.thread_cpu_duration(start_time)).to be_nil
end
end
+
+ def mock_existing_proc_file(path, content)
+ allow(File).to receive(:foreach).with(path) { |_path, &block| content.each_line(&block) }
+ end
+
+ def mock_missing_proc_file
+ allow(File).to receive(:foreach).and_raise(Errno::ENOENT)
+ end
end
diff --git a/spec/lib/gitlab/metrics/transaction_spec.rb b/spec/lib/gitlab/metrics/transaction_spec.rb
index 08de2426c5a..cf46fa3e91c 100644
--- a/spec/lib/gitlab/metrics/transaction_spec.rb
+++ b/spec/lib/gitlab/metrics/transaction_spec.rb
@@ -4,7 +4,6 @@ require 'spec_helper'
describe Gitlab::Metrics::Transaction do
let(:transaction) { described_class.new }
- let(:metric) { transaction.metrics[0] }
let(:sensitive_tags) do
{
@@ -13,12 +12,6 @@ describe Gitlab::Metrics::Transaction do
}
end
- shared_examples 'tag filter' do |sane_tags|
- it 'filters potentially sensitive tags' do
- expect(metric.tags).to eq(sane_tags)
- end
- end
-
describe '#duration' do
it 'returns the duration of a transaction in seconds' do
transaction.run { }
@@ -61,25 +54,6 @@ describe Gitlab::Metrics::Transaction do
end
end
- describe '#add_metric' do
- it 'adds a metric to the transaction' do
- transaction.add_metric('foo', value: 1)
-
- expect(metric.series).to eq('rails_foo')
- expect(metric.tags).to eq({})
- expect(metric.values).to eq(value: 1)
- end
-
- context 'with sensitive tags' do
- before do
- transaction
- .add_metric('foo', { value: 1 }, **sensitive_tags.merge(sane: 'yes'))
- end
-
- it_behaves_like 'tag filter', sane: 'yes'
- end
- end
-
describe '#method_call_for' do
it 'returns a MethodCall' do
method = transaction.method_call_for('Foo#bar', :Foo, '#bar')
@@ -88,133 +62,23 @@ describe Gitlab::Metrics::Transaction do
end
end
- describe '#increment' do
- it 'increments a counter' do
- transaction.increment(:time, 1)
- transaction.increment(:time, 2)
-
- values = metric_values(time: 3)
-
- expect(transaction).to receive(:add_metric)
- .with('transactions', values, {})
-
- transaction.track_self
- end
- end
-
- describe '#set' do
- it 'sets a value' do
- transaction.set(:number, 10)
-
- values = metric_values(number: 10)
-
- expect(transaction).to receive(:add_metric)
- .with('transactions', values, {})
-
- transaction.track_self
- end
- end
-
- describe '#finish' do
- it 'tracks the transaction details and submits them to Sidekiq' do
- expect(transaction).to receive(:track_self)
- expect(transaction).to receive(:submit)
-
- transaction.finish
- end
- end
-
- describe '#track_self' do
- it 'adds a metric for the transaction itself' do
- values = metric_values
-
- expect(transaction).to receive(:add_metric)
- .with('transactions', values, {})
-
- transaction.track_self
- end
- end
-
- describe '#submit' do
- it 'submits the metrics to Sidekiq' do
- transaction.track_self
-
- expect(Gitlab::Metrics).to receive(:submit_metrics)
- .with([an_instance_of(Hash)])
-
- transaction.submit
- end
-
- it 'adds the action as a tag for every metric' do
- allow(transaction)
- .to receive(:labels)
- .and_return(controller: 'Foo', action: 'bar')
-
- transaction.track_self
-
- hash = {
- series: 'rails_transactions',
- tags: { action: 'Foo#bar' },
- values: metric_values,
- timestamp: a_kind_of(Integer)
- }
-
- expect(Gitlab::Metrics).to receive(:submit_metrics)
- .with([hash])
-
- transaction.submit
- end
-
- it 'does not add an action tag for events' do
- allow(transaction)
- .to receive(:labels)
- .and_return(controller: 'Foo', action: 'bar')
-
- transaction.add_event(:meow)
-
- hash = {
- series: 'events',
- tags: { event: :meow },
- values: { count: 1 },
- timestamp: a_kind_of(Integer)
- }
-
- expect(Gitlab::Metrics).to receive(:submit_metrics)
- .with([hash])
-
- transaction.submit
- end
- end
-
describe '#add_event' do
- it 'adds a metric' do
- transaction.add_event(:meow)
+ let(:prometheus_metric) { instance_double(Prometheus::Client::Counter, increment: nil) }
- expect(metric).to be_an_instance_of(Gitlab::Metrics::Metric)
+ before do
+ allow(described_class).to receive(:transaction_metric).and_return(prometheus_metric)
end
- it "does not prefix the metric's series name" do
- transaction.add_event(:meow)
-
- expect(metric.series).to eq(described_class::EVENT_SERIES)
- end
-
- it 'tracks a counter for every event' do
- transaction.add_event(:meow)
-
- expect(metric.values).to eq(count: 1)
- end
+ it 'adds a metric' do
+ expect(prometheus_metric).to receive(:increment)
- it 'tracks the event name' do
transaction.add_event(:meow)
-
- expect(metric.tags).to eq(event: :meow)
end
it 'allows tracking of custom tags' do
- transaction.add_event(:bau, animal: 'dog')
+ expect(prometheus_metric).to receive(:increment).with(hash_including(animal: "dog"))
- expect(metric.tags).to eq(event: :bau, animal: 'dog')
+ transaction.add_event(:bau, animal: 'dog')
end
context 'with sensitive tags' do
@@ -222,16 +86,11 @@ describe Gitlab::Metrics::Transaction do
transaction.add_event(:baubau, **sensitive_tags.merge(sane: 'yes'))
end
- it_behaves_like 'tag filter', event: :baubau, sane: 'yes'
- end
- end
-
- private
+ it 'filters tags' do
+ expect(prometheus_metric).not_to receive(:increment).with(hash_including(sensitive_tags))
- def metric_values(opts = {})
- {
- duration: 0.0,
- allocated_memory: a_kind_of(Numeric)
- }.merge(opts)
+ transaction.add_event(:baubau, **sensitive_tags.merge(sane: 'yes'))
+ end
+ end
end
end
diff --git a/spec/lib/gitlab/metrics/web_transaction_spec.rb b/spec/lib/gitlab/metrics/web_transaction_spec.rb
index 21a762dbf25..47f1bd3bd10 100644
--- a/spec/lib/gitlab/metrics/web_transaction_spec.rb
+++ b/spec/lib/gitlab/metrics/web_transaction_spec.rb
@@ -5,6 +5,11 @@ require 'spec_helper'
describe Gitlab::Metrics::WebTransaction do
let(:env) { {} }
let(:transaction) { described_class.new(env) }
+ let(:prometheus_metric) { double("prometheus metric") }
+
+ before do
+ allow(described_class).to receive(:transaction_metric).and_return(prometheus_metric)
+ end
describe '#duration' do
it 'returns the duration of a transaction in seconds' do
@@ -40,15 +45,6 @@ describe Gitlab::Metrics::WebTransaction do
end
end
- describe '#add_metric' do
- it 'adds a metric to the transaction' do
- expect(Gitlab::Metrics::Metric).to receive(:new)
- .with('rails_foo', { number: 10 }, {})
-
- transaction.add_metric('foo', number: 10)
- end
- end
-
describe '#method_call_for' do
it 'returns a MethodCall' do
method = transaction.method_call_for('Foo#bar', :Foo, '#bar')
@@ -59,101 +55,17 @@ describe Gitlab::Metrics::WebTransaction do
describe '#increment' do
it 'increments a counter' do
- transaction.increment(:time, 1)
- transaction.increment(:time, 2)
-
- values = { duration: 0.0, time: 3, allocated_memory: a_kind_of(Numeric) }
+ expect(prometheus_metric).to receive(:increment).with({}, 1)
- expect(transaction).to receive(:add_metric)
- .with('transactions', values, {})
-
- transaction.track_self
+ transaction.increment(:time, 1)
end
end
describe '#set' do
it 'sets a value' do
- transaction.set(:number, 10)
-
- values = {
- duration: 0.0,
- number: 10,
- allocated_memory: a_kind_of(Numeric)
- }
-
- expect(transaction).to receive(:add_metric)
- .with('transactions', values, {})
-
- transaction.track_self
- end
- end
-
- describe '#finish' do
- it 'tracks the transaction details and submits them to Sidekiq' do
- expect(transaction).to receive(:track_self)
- expect(transaction).to receive(:submit)
-
- transaction.finish
- end
- end
-
- describe '#track_self' do
- it 'adds a metric for the transaction itself' do
- values = {
- duration: transaction.duration,
- allocated_memory: a_kind_of(Numeric)
- }
-
- expect(transaction).to receive(:add_metric)
- .with('transactions', values, {})
-
- transaction.track_self
- end
- end
-
- describe '#submit' do
- it 'submits the metrics to Sidekiq' do
- transaction.track_self
-
- expect(Gitlab::Metrics).to receive(:submit_metrics)
- .with([an_instance_of(Hash)])
-
- transaction.submit
- end
+ expect(prometheus_metric).to receive(:set).with({}, 10)
- it 'adds the action as a tag for every metric' do
- allow(transaction).to receive(:labels).and_return(controller: 'Foo', action: 'bar')
- transaction.track_self
-
- hash = {
- series: 'rails_transactions',
- tags: { action: 'Foo#bar' },
- values: { duration: 0.0, allocated_memory: a_kind_of(Numeric) },
- timestamp: a_kind_of(Integer)
- }
-
- expect(Gitlab::Metrics).to receive(:submit_metrics)
- .with([hash])
-
- transaction.submit
- end
-
- it 'does not add an action tag for events' do
- allow(transaction).to receive(:labels).and_return(controller: 'Foo', action: 'bar')
-
- transaction.add_event(:meow)
-
- hash = {
- series: 'events',
- tags: { event: :meow },
- values: { count: 1 },
- timestamp: a_kind_of(Integer)
- }
-
- expect(Gitlab::Metrics).to receive(:submit_metrics)
- .with([hash])
-
- transaction.submit
+ transaction.set(:number, 10)
end
end
@@ -167,7 +79,6 @@ describe Gitlab::Metrics::WebTransaction do
end
it 'provides labels with the method and path of the route in the grape endpoint' do
expect(transaction.labels).to eq({ controller: 'Grape', action: 'GET /projects/:id/archive' })
- expect(transaction.action).to eq('Grape#GET /projects/:id/archive')
end
it 'does not provide labels if route infos are missing' do
@@ -177,7 +88,6 @@ describe Gitlab::Metrics::WebTransaction do
env['api.endpoint'] = endpoint
expect(transaction.labels).to eq({})
- expect(transaction.action).to be_nil
end
end
@@ -193,7 +103,6 @@ describe Gitlab::Metrics::WebTransaction do
it 'tags a transaction with the name and action of a controller' do
expect(transaction.labels).to eq({ controller: 'TestController', action: 'show' })
- expect(transaction.action).to eq('TestController#show')
end
context 'when the request content type is not :html' do
@@ -201,7 +110,6 @@ describe Gitlab::Metrics::WebTransaction do
it 'appends the mime type to the transaction action' do
expect(transaction.labels).to eq({ controller: 'TestController', action: 'show.json' })
- expect(transaction.action).to eq('TestController#show.json')
end
end
@@ -210,54 +118,26 @@ describe Gitlab::Metrics::WebTransaction do
it 'does not append the MIME type to the transaction action' do
expect(transaction.labels).to eq({ controller: 'TestController', action: 'show' })
- expect(transaction.action).to eq('TestController#show')
end
end
end
it 'returns no labels when no route information is present in env' do
expect(transaction.labels).to eq({})
- expect(transaction.action).to eq(nil)
end
end
describe '#add_event' do
it 'adds a metric' do
- transaction.add_event(:meow)
+ expect(prometheus_metric).to receive(:increment)
- expect(transaction.metrics[0]).to be_an_instance_of(Gitlab::Metrics::Metric)
- end
-
- it "does not prefix the metric's series name" do
transaction.add_event(:meow)
-
- metric = transaction.metrics[0]
-
- expect(metric.series).to eq(described_class::EVENT_SERIES)
- end
-
- it 'tracks a counter for every event' do
- transaction.add_event(:meow)
-
- metric = transaction.metrics[0]
-
- expect(metric.values).to eq(count: 1)
- end
-
- it 'tracks the event name' do
- transaction.add_event(:meow)
-
- metric = transaction.metrics[0]
-
- expect(metric.tags).to eq(event: :meow)
end
it 'allows tracking of custom tags' do
- transaction.add_event(:bau, animal: 'dog')
-
- metric = transaction.metrics[0]
+ expect(prometheus_metric).to receive(:increment).with(animal: "dog")
- expect(metric.tags).to eq(event: :bau, animal: 'dog')
+ transaction.add_event(:bau, animal: 'dog')
end
end
end
diff --git a/spec/lib/gitlab/metrics_spec.rb b/spec/lib/gitlab/metrics_spec.rb
index f0ba12c1cd0..2ebe1958487 100644
--- a/spec/lib/gitlab/metrics_spec.rb
+++ b/spec/lib/gitlab/metrics_spec.rb
@@ -53,60 +53,6 @@ describe Gitlab::Metrics do
end
end
- describe '.influx_metrics_enabled?' do
- it 'returns a boolean' do
- expect(described_class.influx_metrics_enabled?).to be_in([true, false])
- end
- end
-
- describe '.submit_metrics' do
- it 'prepares and writes the metrics to InfluxDB' do
- connection = double(:connection)
- pool = double(:pool)
-
- expect(pool).to receive(:with).and_yield(connection)
- expect(connection).to receive(:write_points).with(an_instance_of(Array))
- expect(described_class).to receive(:pool).and_return(pool)
-
- described_class.submit_metrics([{ 'series' => 'kittens', 'tags' => {} }])
- end
- end
-
- describe '.prepare_metrics' do
- it 'returns a Hash with the keys as Symbols' do
- metrics = described_class
- .prepare_metrics([{ 'values' => {}, 'tags' => {} }])
-
- expect(metrics).to eq([{ values: {}, tags: {} }])
- end
-
- it 'escapes tag values' do
- metrics = described_class.prepare_metrics([
- { 'values' => {}, 'tags' => { 'foo' => 'bar=' } }
- ])
-
- expect(metrics).to eq([{ values: {}, tags: { 'foo' => 'bar\\=' } }])
- end
-
- it 'drops empty tags' do
- metrics = described_class.prepare_metrics([
- { 'values' => {}, 'tags' => { 'cats' => '', 'dogs' => nil } }
- ])
-
- expect(metrics).to eq([{ values: {}, tags: {} }])
- end
- end
-
- describe '.escape_value' do
- it 'escapes an equals sign' do
- expect(described_class.escape_value('foo=')).to eq('foo\\=')
- end
-
- it 'casts values to Strings' do
- expect(described_class.escape_value(10)).to eq('10')
- end
- end
-
describe '.measure' do
context 'without a transaction' do
it 'returns the return value of the block' do
@@ -145,30 +91,6 @@ describe Gitlab::Metrics do
end
end
- describe '.action=' do
- context 'without a transaction' do
- it 'does nothing' do
- expect_any_instance_of(Gitlab::Metrics::Transaction)
- .not_to receive(:action=)
-
- described_class.action = 'foo'
- end
- end
-
- context 'with a transaction' do
- it 'sets the action of a transaction' do
- trans = Gitlab::Metrics::WebTransaction.new({})
-
- expect(described_class).to receive(:current_transaction)
- .and_return(trans)
-
- expect(trans).to receive(:action=).with('foo')
-
- described_class.action = 'foo'
- end
- end
- end
-
describe '#series_prefix' do
it 'returns a String' do
expect(described_class.series_prefix).to be_an_instance_of(String)
diff --git a/spec/lib/gitlab/middleware/multipart_spec.rb b/spec/lib/gitlab/middleware/multipart_spec.rb
index c99281ee12c..705164d5445 100644
--- a/spec/lib/gitlab/middleware/multipart_spec.rb
+++ b/spec/lib/gitlab/middleware/multipart_spec.rb
@@ -195,6 +195,17 @@ describe Gitlab::Middleware::Multipart do
end
end
+ it 'allows files in the lfs upload path' do
+ with_tmp_dir('lfs-objects') do |dir, env|
+ expect(LfsObjectUploader).to receive(:workhorse_upload_path).and_return(File.join(dir, 'lfs-objects'))
+ expect(app).to receive(:call) do |env|
+ expect(get_params(env)['file']).to be_a(::UploadedFile)
+ end
+
+ middleware.call(env)
+ end
+ end
+
it 'allows symlinks for uploads dir' do
Tempfile.open('two-levels') do |tempfile|
symlinked_dir = '/some/dir/uploads'
diff --git a/spec/lib/gitlab/omniauth_initializer_spec.rb b/spec/lib/gitlab/omniauth_initializer_spec.rb
index 99684bb2ab2..4afe4545891 100644
--- a/spec/lib/gitlab/omniauth_initializer_spec.rb
+++ b/spec/lib/gitlab/omniauth_initializer_spec.rb
@@ -86,6 +86,22 @@ describe Gitlab::OmniauthInitializer do
subject.execute([cas3_config])
end
+ it 'configures defaults for google_oauth2' do
+ google_config = {
+ 'name' => 'google_oauth2',
+ "args" => { "access_type" => "offline", "approval_prompt" => '' }
+ }
+
+ expect(devise_config).to receive(:omniauth).with(
+ :google_oauth2,
+ access_type: "offline",
+ approval_prompt: "",
+ client_options: { connection_opts: { request: { timeout: Gitlab::OmniauthInitializer::OAUTH2_TIMEOUT_SECONDS } } }
+ )
+
+ subject.execute([google_config])
+ end
+
it 'converts client_auth_method to a Symbol for openid_connect' do
openid_connect_config = {
'name' => 'openid_connect',
diff --git a/spec/lib/gitlab/pagination/keyset_spec.rb b/spec/lib/gitlab/pagination/keyset_spec.rb
index bde280c5fca..0ac40080872 100644
--- a/spec/lib/gitlab/pagination/keyset_spec.rb
+++ b/spec/lib/gitlab/pagination/keyset_spec.rb
@@ -3,6 +3,18 @@
require 'spec_helper'
describe Gitlab::Pagination::Keyset do
+ describe '.available_for_type?' do
+ subject { described_class }
+
+ it 'returns true for Project' do
+ expect(subject.available_for_type?(Project.all)).to be_truthy
+ end
+
+ it 'return false for other types of relations' do
+ expect(subject.available_for_type?(User.all)).to be_falsey
+ end
+ end
+
describe '.available?' do
subject { described_class }
diff --git a/spec/lib/gitlab/path_regex_spec.rb b/spec/lib/gitlab/path_regex_spec.rb
index 8dabe5a756b..50b045c6aad 100644
--- a/spec/lib/gitlab/path_regex_spec.rb
+++ b/spec/lib/gitlab/path_regex_spec.rb
@@ -170,6 +170,11 @@ describe Gitlab::PathRegex do
expect(described_class::TOP_LEVEL_ROUTES)
.to contain_exactly(*top_level_words), failure_block
end
+
+ # We ban new items in this list, see https://gitlab.com/gitlab-org/gitlab/-/issues/215362
+ it 'does not allow expansion' do
+ expect(described_class::TOP_LEVEL_ROUTES.size).to eq(41)
+ end
end
describe 'GROUP_ROUTES' do
@@ -184,6 +189,11 @@ describe Gitlab::PathRegex do
expect(described_class::GROUP_ROUTES)
.to contain_exactly(*paths_after_group_id), failure_block
end
+
+ # We ban new items in this list, see https://gitlab.com/gitlab-org/gitlab/-/issues/215362
+ it 'does not allow expansion' do
+ expect(described_class::GROUP_ROUTES.size).to eq(1)
+ end
end
describe 'PROJECT_WILDCARD_ROUTES' do
@@ -195,6 +205,11 @@ describe Gitlab::PathRegex do
end
end
end
+
+ # We ban new items in this list, see https://gitlab.com/gitlab-org/gitlab/-/issues/215362
+ it 'does not allow expansion' do
+ expect(described_class::PROJECT_WILDCARD_ROUTES.size).to eq(21)
+ end
end
describe '.root_namespace_route_regex' do
diff --git a/spec/lib/gitlab/performance_bar_spec.rb b/spec/lib/gitlab/performance_bar_spec.rb
index 816db49d94a..7b79cc82816 100644
--- a/spec/lib/gitlab/performance_bar_spec.rb
+++ b/spec/lib/gitlab/performance_bar_spec.rb
@@ -3,42 +3,7 @@
require 'spec_helper'
describe Gitlab::PerformanceBar do
- shared_examples 'allowed user IDs are cached' do
- before do
- # Warm the caches
- described_class.enabled_for_user?(user)
- end
-
- it 'caches the allowed user IDs in cache', :use_clean_rails_memory_store_caching do
- expect do
- expect(described_class.l1_cache_backend).to receive(:fetch).and_call_original
- expect(described_class.l2_cache_backend).not_to receive(:fetch)
- expect(described_class.enabled_for_user?(user)).to be_truthy
- end.not_to exceed_query_limit(0)
- end
-
- it 'caches the allowed user IDs in L1 cache for 1 minute', :use_clean_rails_memory_store_caching do
- Timecop.travel 2.minutes do
- expect do
- expect(described_class.l1_cache_backend).to receive(:fetch).and_call_original
- expect(described_class.l2_cache_backend).to receive(:fetch).and_call_original
- expect(described_class.enabled_for_user?(user)).to be_truthy
- end.not_to exceed_query_limit(0)
- end
- end
-
- it 'caches the allowed user IDs in L2 cache for 5 minutes', :use_clean_rails_memory_store_caching do
- Timecop.travel 6.minutes do
- expect do
- expect(described_class.l1_cache_backend).to receive(:fetch).and_call_original
- expect(described_class.l2_cache_backend).to receive(:fetch).and_call_original
- expect(described_class.enabled_for_user?(user)).to be_truthy
- end.not_to exceed_query_limit(2)
- end
- end
- end
-
- it { expect(described_class.l1_cache_backend).to eq(Gitlab::ThreadMemoryCache.cache_backend) }
+ it { expect(described_class.l1_cache_backend).to eq(Gitlab::ProcessMemoryCache.cache_backend) }
it { expect(described_class.l2_cache_backend).to eq(Rails.cache) }
describe '.enabled_for_user?' do
@@ -82,7 +47,16 @@ describe Gitlab::PerformanceBar do
expect(described_class.enabled_for_user?(user)).to be_falsy
end
- it_behaves_like 'allowed user IDs are cached'
+ context 'caching of allowed user IDs' do
+ subject { described_class.enabled_for_user?(user) }
+
+ before do
+ # Warm the caches
+ described_class.enabled_for_user?(user)
+ end
+
+ it_behaves_like 'allowed user IDs are cached'
+ end
end
context 'when user is a member of the allowed group' do
@@ -94,7 +68,16 @@ describe Gitlab::PerformanceBar do
expect(described_class.enabled_for_user?(user)).to be_truthy
end
- it_behaves_like 'allowed user IDs are cached'
+ context 'caching of allowed user IDs' do
+ subject { described_class.enabled_for_user?(user) }
+
+ before do
+ # Warm the caches
+ described_class.enabled_for_user?(user)
+ end
+
+ it_behaves_like 'allowed user IDs are cached'
+ end
end
end
diff --git a/spec/lib/gitlab/phabricator_import/conduit/response_spec.rb b/spec/lib/gitlab/phabricator_import/conduit/response_spec.rb
index a8596968f14..1ffb811cbc1 100644
--- a/spec/lib/gitlab/phabricator_import/conduit/response_spec.rb
+++ b/spec/lib/gitlab/phabricator_import/conduit/response_spec.rb
@@ -2,8 +2,8 @@
require 'spec_helper'
describe Gitlab::PhabricatorImport::Conduit::Response do
- let(:response) { described_class.new(JSON.parse(fixture_file('phabricator_responses/maniphest.search.json')))}
- let(:error_response) { described_class.new(JSON.parse(fixture_file('phabricator_responses/auth_failed.json'))) }
+ let(:response) { described_class.new(Gitlab::Json.parse(fixture_file('phabricator_responses/maniphest.search.json')))}
+ let(:error_response) { described_class.new(Gitlab::Json.parse(fixture_file('phabricator_responses/auth_failed.json'))) }
describe '.parse!' do
it 'raises a ResponseError if the http response was not successfull' do
diff --git a/spec/lib/gitlab/phabricator_import/conduit/tasks_response_spec.rb b/spec/lib/gitlab/phabricator_import/conduit/tasks_response_spec.rb
index 4b4c2a6276e..2cc12ee0165 100644
--- a/spec/lib/gitlab/phabricator_import/conduit/tasks_response_spec.rb
+++ b/spec/lib/gitlab/phabricator_import/conduit/tasks_response_spec.rb
@@ -4,7 +4,7 @@ require 'spec_helper'
describe Gitlab::PhabricatorImport::Conduit::TasksResponse do
let(:conduit_response) do
Gitlab::PhabricatorImport::Conduit::Response
- .new(JSON.parse(fixture_file('phabricator_responses/maniphest.search.json')))
+ .new(Gitlab::Json.parse(fixture_file('phabricator_responses/maniphest.search.json')))
end
subject(:response) { described_class.new(conduit_response) }
diff --git a/spec/lib/gitlab/phabricator_import/conduit/users_response_spec.rb b/spec/lib/gitlab/phabricator_import/conduit/users_response_spec.rb
index 00778ad90fd..999a986b73c 100644
--- a/spec/lib/gitlab/phabricator_import/conduit/users_response_spec.rb
+++ b/spec/lib/gitlab/phabricator_import/conduit/users_response_spec.rb
@@ -4,7 +4,7 @@ require 'spec_helper'
describe Gitlab::PhabricatorImport::Conduit::UsersResponse do
let(:conduit_response) do
Gitlab::PhabricatorImport::Conduit::Response
- .new(JSON.parse(fixture_file('phabricator_responses/user.search.json')))
+ .new(Gitlab::Json.parse(fixture_file('phabricator_responses/user.search.json')))
end
subject(:response) { described_class.new(conduit_response) }
diff --git a/spec/lib/gitlab/phabricator_import/issues/importer_spec.rb b/spec/lib/gitlab/phabricator_import/issues/importer_spec.rb
index 667321409da..02dafd4bb3b 100644
--- a/spec/lib/gitlab/phabricator_import/issues/importer_spec.rb
+++ b/spec/lib/gitlab/phabricator_import/issues/importer_spec.rb
@@ -7,7 +7,7 @@ describe Gitlab::PhabricatorImport::Issues::Importer do
let(:response) do
Gitlab::PhabricatorImport::Conduit::TasksResponse.new(
Gitlab::PhabricatorImport::Conduit::Response
- .new(JSON.parse(fixture_file('phabricator_responses/maniphest.search.json')))
+ .new(Gitlab::Json.parse(fixture_file('phabricator_responses/maniphest.search.json')))
)
end
diff --git a/spec/lib/gitlab/project_search_results_spec.rb b/spec/lib/gitlab/project_search_results_spec.rb
index d206d31eb96..64f80b5d736 100644
--- a/spec/lib/gitlab/project_search_results_spec.rb
+++ b/spec/lib/gitlab/project_search_results_spec.rb
@@ -45,22 +45,36 @@ describe Gitlab::ProjectSearchResults do
expect(results.formatted_count(scope)).to eq(expected)
end
end
+
+ context 'blobs' do
+ it "limits the search to #{described_class::COUNT_LIMIT} items" do
+ expect(results).to receive(:blobs).with(limit: described_class::COUNT_LIMIT).and_call_original
+ expect(results.formatted_count('blobs')).to eq('0')
+ end
+ end
+
+ context 'wiki_blobs' do
+ it "limits the search to #{described_class::COUNT_LIMIT} items" do
+ expect(results).to receive(:wiki_blobs).with(limit: described_class::COUNT_LIMIT).and_call_original
+ expect(results.formatted_count('wiki_blobs')).to eq('0')
+ end
+ end
end
- shared_examples 'general blob search' do |entity_type, blob_kind|
+ shared_examples 'general blob search' do |entity_type, blob_type|
let(:query) { 'files' }
subject(:results) { described_class.new(user, project, query).objects(blob_type) }
context "when #{entity_type} is disabled" do
let(:project) { disabled_project }
- it "hides #{blob_kind} from members" do
+ it "hides #{blob_type} from members" do
project.add_reporter(user)
is_expected.to be_empty
end
- it "hides #{blob_kind} from non-members" do
+ it "hides #{blob_type} from non-members" do
is_expected.to be_empty
end
end
@@ -68,13 +82,13 @@ describe Gitlab::ProjectSearchResults do
context "when #{entity_type} is internal" do
let(:project) { private_project }
- it "finds #{blob_kind} for members" do
+ it "finds #{blob_type} for members" do
project.add_reporter(user)
is_expected.not_to be_empty
end
- it "hides #{blob_kind} from non-members" do
+ it "hides #{blob_type} from non-members" do
is_expected.to be_empty
end
end
@@ -96,7 +110,7 @@ describe Gitlab::ProjectSearchResults do
end
end
- shared_examples 'blob search repository ref' do |entity_type|
+ shared_examples 'blob search repository ref' do |entity_type, blob_type|
let(:query) { 'files' }
let(:file_finder) { double }
let(:project_branch) { 'project_branch' }
@@ -139,9 +153,41 @@ describe Gitlab::ProjectSearchResults do
end
end
+ shared_examples 'blob search pagination' do |blob_type|
+ let(:per_page) { 20 }
+ let(:count_limit) { described_class::COUNT_LIMIT }
+ let(:file_finder) { instance_double('Gitlab::FileFinder') }
+ let(:results) { described_class.new(user, project, query) }
+ let(:repository_ref) { 'master' }
+
+ before do
+ allow(file_finder).to receive(:find).and_return([])
+ expect(Gitlab::FileFinder).to receive(:new).with(project, repository_ref).and_return(file_finder)
+ end
+
+ it 'limits search results based on the first page' do
+ expect(file_finder).to receive(:find).with(query, content_match_cutoff: count_limit)
+ results.objects(blob_type, page: 1, per_page: per_page)
+ end
+
+ it 'limits search results based on the second page' do
+ expect(file_finder).to receive(:find).with(query, content_match_cutoff: count_limit + per_page)
+ results.objects(blob_type, page: 2, per_page: per_page)
+ end
+
+ it 'limits search results based on the third page' do
+ expect(file_finder).to receive(:find).with(query, content_match_cutoff: count_limit + per_page * 2)
+ results.objects(blob_type, page: 3, per_page: per_page)
+ end
+
+ it 'uses the per_page value when passed' do
+ expect(file_finder).to receive(:find).with(query, content_match_cutoff: count_limit + 10 * 2)
+ results.objects(blob_type, page: 3, per_page: 10)
+ end
+ end
+
describe 'blob search' do
let(:project) { create(:project, :public, :repository) }
- let(:blob_type) { 'blobs' }
it_behaves_like 'general blob search', 'repository', 'blobs' do
let(:disabled_project) { create(:project, :public, :repository, :repository_disabled) }
@@ -150,37 +196,11 @@ describe Gitlab::ProjectSearchResults do
let(:expected_file_by_content) { 'CHANGELOG' }
end
- it_behaves_like 'blob search repository ref', 'project' do
+ it_behaves_like 'blob search repository ref', 'project', 'blobs' do
let(:entity) { project }
end
- context 'pagination' do
- let(:per_page) { 20 }
- let(:count_limit) { described_class::COUNT_LIMIT }
- let(:file_finder) { instance_double('Gitlab::FileFinder') }
- let(:results) { described_class.new(user, project, query, per_page: per_page) }
- let(:repository_ref) { 'master' }
-
- before do
- allow(file_finder).to receive(:find).and_return([])
- expect(Gitlab::FileFinder).to receive(:new).with(project, repository_ref).and_return(file_finder)
- end
-
- it 'limits search results based on the first page' do
- expect(file_finder).to receive(:find).with(query, content_match_cutoff: count_limit)
- results.objects(blob_type, 1)
- end
-
- it 'limits search results based on the second page' do
- expect(file_finder).to receive(:find).with(query, content_match_cutoff: count_limit + per_page)
- results.objects(blob_type, 2)
- end
-
- it 'limits search results based on the third page' do
- expect(file_finder).to receive(:find).with(query, content_match_cutoff: count_limit + per_page * 2)
- results.objects(blob_type, 3)
- end
- end
+ it_behaves_like 'blob search pagination', 'blobs'
end
describe 'wiki search' do
@@ -192,7 +212,7 @@ describe Gitlab::ProjectSearchResults do
wiki.create_page('CHANGELOG', 'Files example')
end
- it_behaves_like 'general blob search', 'wiki', 'wiki blobs' do
+ it_behaves_like 'general blob search', 'wiki', 'wiki_blobs' do
let(:blob_type) { 'wiki_blobs' }
let(:disabled_project) { create(:project, :public, :wiki_repo, :wiki_disabled) }
let(:private_project) { create(:project, :public, :wiki_repo, :wiki_private) }
@@ -200,10 +220,27 @@ describe Gitlab::ProjectSearchResults do
let(:expected_file_by_content) { 'CHANGELOG.md' }
end
- it_behaves_like 'blob search repository ref', 'wiki' do
- let(:blob_type) { 'wiki_blobs' }
+ it_behaves_like 'blob search repository ref', 'wiki', 'wiki_blobs' do
let(:entity) { project.wiki }
end
+
+ it_behaves_like 'blob search pagination', 'wiki_blobs'
+
+ context 'return type' do
+ let(:blobs) { [Gitlab::Search::FoundBlob.new(project: project)] }
+ let(:results) { described_class.new(user, project, "Files", per_page: 20) }
+
+ before do
+ allow(results).to receive(:wiki_blobs).and_return(blobs)
+ end
+
+ it 'returns list of FoundWikiPage type object' do
+ objects = results.objects('wiki_blobs')
+
+ expect(objects).to be_present
+ expect(objects).to all(be_a(Gitlab::Search::FoundWikiPage))
+ end
+ end
end
it 'does not list issues on private projects' do
diff --git a/spec/lib/gitlab/prometheus_client_spec.rb b/spec/lib/gitlab/prometheus_client_spec.rb
index e869a384b29..4ff53b50a50 100644
--- a/spec/lib/gitlab/prometheus_client_spec.rb
+++ b/spec/lib/gitlab/prometheus_client_spec.rb
@@ -313,7 +313,7 @@ describe Gitlab::PrometheusClient do
req_stub = stub_prometheus_request(query_url, body: prometheus_value_body('vector'))
response = subject.proxy('query', { query: prometheus_query })
- json_response = JSON.parse(response.body)
+ json_response = Gitlab::Json.parse(response.body)
expect(response.code).to eq(200)
expect(json_response).to eq({
@@ -332,7 +332,7 @@ describe Gitlab::PrometheusClient do
req_stub = stub_prometheus_request(query_url, status: 400, body: { error: 'error' })
response = subject.proxy('query', { query: prometheus_query })
- json_response = JSON.parse(response.body)
+ json_response = Gitlab::Json.parse(response.body)
expect(req_stub).to have_been_requested
expect(response.code).to eq(400)
diff --git a/spec/lib/gitlab/regex_spec.rb b/spec/lib/gitlab/regex_spec.rb
index 5a2cf2eda8b..9e596400904 100644
--- a/spec/lib/gitlab/regex_spec.rb
+++ b/spec/lib/gitlab/regex_spec.rb
@@ -130,4 +130,37 @@ describe Gitlab::Regex do
it { is_expected.not_to match('aa-1234-cc') }
it { is_expected.not_to match('9/9/2018') }
end
+
+ describe '.kubernetes_namespace_regex' do
+ subject { described_class.kubernetes_namespace_regex }
+
+ it { is_expected.to match('foo') }
+ it { is_expected.to match('foo-bar') }
+ it { is_expected.to match('1foo-bar') }
+ it { is_expected.to match('foo-bar2') }
+ it { is_expected.to match('foo-1bar') }
+ it { is_expected.not_to match('foo.bar') }
+ it { is_expected.not_to match('Foo') }
+ it { is_expected.not_to match('FoO') }
+ it { is_expected.not_to match('FoO-') }
+ it { is_expected.not_to match('-foo-') }
+ it { is_expected.not_to match('foo/bar') }
+ end
+
+ describe '.kubernetes_dns_subdomain_regex' do
+ subject { described_class.kubernetes_dns_subdomain_regex }
+
+ it { is_expected.to match('foo') }
+ it { is_expected.to match('foo-bar') }
+ it { is_expected.to match('foo.bar') }
+ it { is_expected.to match('foo1.bar') }
+ it { is_expected.to match('foo1.2bar') }
+ it { is_expected.to match('foo.bar1') }
+ it { is_expected.to match('1foo.bar1') }
+ it { is_expected.not_to match('Foo') }
+ it { is_expected.not_to match('FoO') }
+ it { is_expected.not_to match('FoO-') }
+ it { is_expected.not_to match('-foo-') }
+ it { is_expected.not_to match('foo/bar') }
+ end
end
diff --git a/spec/lib/gitlab/repository_url_builder_spec.rb b/spec/lib/gitlab/repository_url_builder_spec.rb
index 3d8870ecb53..a5797146cc5 100644
--- a/spec/lib/gitlab/repository_url_builder_spec.rb
+++ b/spec/lib/gitlab/repository_url_builder_spec.rb
@@ -9,7 +9,7 @@ describe Gitlab::RepositoryUrlBuilder do
where(:factory, :path_generator) do
:project | ->(project) { project.full_path }
:project_snippet | ->(snippet) { "#{snippet.project.full_path}/snippets/#{snippet.id}" }
- :project_wiki | ->(wiki) { "#{wiki.project.full_path}.wiki" }
+ :project_wiki | ->(wiki) { "#{wiki.container.full_path}.wiki" }
:personal_snippet | ->(snippet) { "snippets/#{snippet.id}" }
end
diff --git a/spec/lib/gitlab/request_context_spec.rb b/spec/lib/gitlab/request_context_spec.rb
index 7e2e05c9f1b..d7af0765d53 100644
--- a/spec/lib/gitlab/request_context_spec.rb
+++ b/spec/lib/gitlab/request_context_spec.rb
@@ -5,6 +5,10 @@ require 'spec_helper'
describe Gitlab::RequestContext, :request_store do
subject { described_class.instance }
+ before do
+ allow(subject).to receive(:enabled?).and_return(true)
+ end
+
it { is_expected.to have_attributes(client_ip: nil, start_thread_cpu_time: nil, request_start_time: nil) }
describe '#request_deadline' do
diff --git a/spec/lib/gitlab/runtime_spec.rb b/spec/lib/gitlab/runtime_spec.rb
index 34a775fc206..8f920bb2e01 100644
--- a/spec/lib/gitlab/runtime_spec.rb
+++ b/spec/lib/gitlab/runtime_spec.rb
@@ -105,4 +105,17 @@ describe Gitlab::Runtime do
it_behaves_like "valid runtime", :rails_runner, 1
end
+
+ context "action_cable" do
+ before do
+ stub_const('ACTION_CABLE_SERVER', true)
+ stub_const('::Puma', Module.new)
+
+ allow(Gitlab::Application).to receive_message_chain(:config, :action_cable, :worker_pool_size).and_return(8)
+ end
+
+ it "reports its maximum concurrency based on ActionCable's worker pool size" do
+ expect(subject.max_threads).to eq(9)
+ end
+ end
end
diff --git a/spec/lib/gitlab/search_results_spec.rb b/spec/lib/gitlab/search_results_spec.rb
index 86dde15cc8a..ab14602a468 100644
--- a/spec/lib/gitlab/search_results_spec.rb
+++ b/spec/lib/gitlab/search_results_spec.rb
@@ -28,7 +28,15 @@ describe Gitlab::SearchResults do
end
it 'returns with counts collection when requested' do
- expect(results.objects('projects', 1, false)).not_to be_kind_of(Kaminari::PaginatableWithoutCount)
+ expect(results.objects('projects', page: 1, per_page: 1, without_count: false)).not_to be_kind_of(Kaminari::PaginatableWithoutCount)
+ end
+
+ it 'uses page and per_page to paginate results' do
+ project2 = create(:project, name: 'foo')
+
+ expect(results.objects('projects', page: 1, per_page: 1).to_a).to eq([project])
+ expect(results.objects('projects', page: 2, per_page: 1).to_a).to eq([project2])
+ expect(results.objects('projects', page: 1, per_page: 2).count).to eq(2)
end
end
diff --git a/spec/lib/gitlab/sidekiq_config/cli_methods_spec.rb b/spec/lib/gitlab/sidekiq_config/cli_methods_spec.rb
index 0aaff12f278..80e8da58f23 100644
--- a/spec/lib/gitlab/sidekiq_config/cli_methods_spec.rb
+++ b/spec/lib/gitlab/sidekiq_config/cli_methods_spec.rb
@@ -54,14 +54,6 @@ describe Gitlab::SidekiqConfig::CliMethods do
end
end
- context 'when the file contains an array of strings' do
- before do
- stub_contents(['queue_a'], ['queue_b'])
- end
-
- include_examples 'valid file contents'
- end
-
context 'when the file contains an array of hashes' do
before do
stub_contents([{ name: 'queue_a' }], [{ name: 'queue_b' }])
diff --git a/spec/lib/gitlab/sidekiq_logging/json_formatter_spec.rb b/spec/lib/gitlab/sidekiq_logging/json_formatter_spec.rb
index 2f5343627d8..283140d7fdf 100644
--- a/spec/lib/gitlab/sidekiq_logging/json_formatter_spec.rb
+++ b/spec/lib/gitlab/sidekiq_logging/json_formatter_spec.rb
@@ -9,7 +9,7 @@ describe Gitlab::SidekiqLogging::JSONFormatter do
let(:timestamp_iso8601) { now.iso8601(3) }
describe 'with a Hash' do
- subject { JSON.parse(described_class.new.call('INFO', now, 'my program', hash_input)) }
+ subject { Gitlab::Json.parse(described_class.new.call('INFO', now, 'my program', hash_input)) }
let(:hash_input) do
{
@@ -34,7 +34,8 @@ describe Gitlab::SidekiqLogging::JSONFormatter do
'started_at' => timestamp_iso8601,
'retried_at' => timestamp_iso8601,
'failed_at' => timestamp_iso8601,
- 'completed_at' => timestamp_iso8601
+ 'completed_at' => timestamp_iso8601,
+ 'retry' => 0
}
)
@@ -57,13 +58,33 @@ describe Gitlab::SidekiqLogging::JSONFormatter do
expect(subject['args']).to eq(["1", "test", "2", %({"test"=>1})])
end
+
+ context 'when the job has a non-integer value for retry' do
+ using RSpec::Parameterized::TableSyntax
+
+ where(:retry_in_job, :retry_in_logs) do
+ 3 | 3
+ true | 25
+ false | 0
+ nil | 0
+ 'string' | -1
+ end
+
+ with_them do
+ it 'logs as the correct integer' do
+ hash_input['retry'] = retry_in_job
+
+ expect(subject['retry']).to eq(retry_in_logs)
+ end
+ end
+ end
end
describe 'with a String' do
it 'accepts strings with no changes' do
result = subject.call('DEBUG', now, 'my string', message)
- data = JSON.parse(result)
+ data = Gitlab::Json.parse(result)
expected_output = {
severity: 'DEBUG',
time: timestamp_iso8601,
diff --git a/spec/lib/gitlab/sidekiq_logging/structured_logger_spec.rb b/spec/lib/gitlab/sidekiq_logging/structured_logger_spec.rb
index f4b939c3013..a4bbb51baae 100644
--- a/spec/lib/gitlab/sidekiq_logging/structured_logger_spec.rb
+++ b/spec/lib/gitlab/sidekiq_logging/structured_logger_spec.rb
@@ -44,7 +44,7 @@ describe Gitlab::SidekiqLogging::StructuredLogger do
'job_status' => 'done',
'duration_s' => 0.0,
'completed_at' => timestamp.to_f,
- 'cpu_s' => 1.11,
+ 'cpu_s' => 1.111112,
'db_duration_s' => 0.0
)
end
@@ -218,13 +218,34 @@ describe Gitlab::SidekiqLogging::StructuredLogger do
subject.call(job, 'test_queue') { }
end
end
+
+ context 'when there is extra metadata set for the done log' do
+ let(:expected_start_payload) { start_payload.except('args') }
+
+ let(:expected_end_payload) do
+ end_payload.except('args').merge("#{ApplicationWorker::LOGGING_EXTRA_KEY}.key1" => 15, "#{ApplicationWorker::LOGGING_EXTRA_KEY}.key2" => 16)
+ end
+
+ it 'logs it in the done log' do
+ Timecop.freeze(timestamp) do
+ expect(logger).to receive(:info).with(expected_start_payload).ordered
+ expect(logger).to receive(:info).with(expected_end_payload).ordered
+
+ subject.call(job, 'test_queue') do
+ job["#{ApplicationWorker::LOGGING_EXTRA_KEY}.key1"] = 15
+ job["#{ApplicationWorker::LOGGING_EXTRA_KEY}.key2"] = 16
+ job['key that will be ignored because it does not start with extra.'] = 17
+ end
+ end
+ end
+ end
end
describe '#add_time_keys!' do
let(:time) { { duration: 0.1231234, cputime: 1.2342345 } }
let(:payload) { { 'class' => 'my-class', 'message' => 'my-message', 'job_status' => 'my-job-status' } }
let(:current_utc_time) { Time.now.utc }
- let(:payload_with_time_keys) { { 'class' => 'my-class', 'message' => 'my-message', 'job_status' => 'my-job-status', 'duration_s' => 0.12, 'cpu_s' => 1.23, 'completed_at' => current_utc_time.to_f } }
+ let(:payload_with_time_keys) { { 'class' => 'my-class', 'message' => 'my-message', 'job_status' => 'my-job-status', 'duration_s' => 0.123123, 'cpu_s' => 1.234235, 'completed_at' => current_utc_time.to_f } }
subject { described_class.new }
diff --git a/spec/lib/gitlab/sidekiq_middleware/duplicate_jobs/duplicate_job_spec.rb b/spec/lib/gitlab/sidekiq_middleware/duplicate_jobs/duplicate_job_spec.rb
index 6e8a8c03aad..929df0a7ffb 100644
--- a/spec/lib/gitlab/sidekiq_middleware/duplicate_jobs/duplicate_job_spec.rb
+++ b/spec/lib/gitlab/sidekiq_middleware/duplicate_jobs/duplicate_job_spec.rb
@@ -113,22 +113,27 @@ describe Gitlab::SidekiqMiddleware::DuplicateJobs::DuplicateJob, :clean_gitlab_r
end
describe 'droppable?' do
- where(:idempotent, :duplicate) do
- # [true, false].repeated_permutation(2)
- [[true, true],
- [true, false],
- [false, true],
- [false, false]]
+ where(:idempotent, :duplicate, :prevent_deduplication) do
+ # [true, false].repeated_permutation(3)
+ [[true, true, true],
+ [true, true, false],
+ [true, false, true],
+ [true, false, false],
+ [false, true, true],
+ [false, true, false],
+ [false, false, true],
+ [false, false, false]]
end
with_them do
before do
allow(AuthorizedProjectsWorker).to receive(:idempotent?).and_return(idempotent)
allow(duplicate_job).to receive(:duplicate?).and_return(duplicate)
+ stub_feature_flags("disable_#{queue}_deduplication" => prevent_deduplication)
end
it 'is droppable when all conditions are met' do
- if idempotent && duplicate
+ if idempotent && duplicate && !prevent_deduplication
expect(duplicate_job).to be_droppable
else
expect(duplicate_job).not_to be_droppable
diff --git a/spec/lib/gitlab/sidekiq_middleware/extra_done_log_metadata_spec.rb b/spec/lib/gitlab/sidekiq_middleware/extra_done_log_metadata_spec.rb
new file mode 100644
index 00000000000..98847885e62
--- /dev/null
+++ b/spec/lib/gitlab/sidekiq_middleware/extra_done_log_metadata_spec.rb
@@ -0,0 +1,35 @@
+# frozen_string_literal: true
+
+require 'spec_helper'
+
+describe Gitlab::SidekiqMiddleware::ExtraDoneLogMetadata do
+ # Cannot use Class.new for this as ApplicationWorker will need the class to
+ # have a name during `included do`.
+ let(:worker) { AdminEmailWorker.new }
+
+ let(:worker_without_application_worker) do
+ Class.new do
+ end.new
+ end
+
+ subject { described_class.new }
+
+ let(:job) { { 'jid' => 123 } }
+ let(:queue) { 'test_queue' }
+
+ describe '#call' do
+ it 'merges Application#logging_extras in to job' do
+ worker.log_extra_metadata_on_done(:key1, 15)
+ worker.log_extra_metadata_on_done(:key2, 16)
+ expect { |b| subject.call(worker, job, queue, &b) }.to yield_control
+
+ expect(job).to eq({ 'jid' => 123, 'extra.admin_email_worker.key1' => 15, 'extra.admin_email_worker.key2' => 16 })
+ end
+
+ it 'does not raise when the worker does not respond to #done_log_extra_metadata' do
+ expect { |b| subject.call(worker_without_application_worker, job, queue, &b) }.to yield_control
+
+ expect(job).to eq({ 'jid' => 123 })
+ end
+ end
+end
diff --git a/spec/lib/gitlab/sidekiq_middleware_spec.rb b/spec/lib/gitlab/sidekiq_middleware_spec.rb
index 752ec6a0a3f..6fe61fb42a5 100644
--- a/spec/lib/gitlab/sidekiq_middleware_spec.rb
+++ b/spec/lib/gitlab/sidekiq_middleware_spec.rb
@@ -4,12 +4,17 @@ require 'spec_helper'
require 'sidekiq/testing'
describe Gitlab::SidekiqMiddleware do
- class TestWorker
- include Sidekiq::Worker
+ before do
+ stub_const('TestWorker', Class.new)
- def perform(_arg)
- Gitlab::SafeRequestStore['gitaly_call_actual'] = 1
- Gitlab::GitalyClient.query_time = 5
+ TestWorker.class_eval do
+ include Sidekiq::Worker
+ include ApplicationWorker
+
+ def perform(_arg)
+ Gitlab::SafeRequestStore['gitaly_call_actual'] = 1
+ Gitlab::GitalyClient.query_time = 5
+ end
end
end
@@ -32,8 +37,7 @@ describe Gitlab::SidekiqMiddleware do
described_class.server_configurator(
metrics: metrics,
arguments_logger: arguments_logger,
- memory_killer: memory_killer,
- request_store: request_store
+ memory_killer: memory_killer
).call(chain)
example.run
@@ -52,6 +56,7 @@ describe Gitlab::SidekiqMiddleware do
Gitlab::SidekiqMiddleware::ArgumentsLogger,
Gitlab::SidekiqMiddleware::MemoryKiller,
Gitlab::SidekiqMiddleware::RequestStoreMiddleware,
+ Gitlab::SidekiqMiddleware::ExtraDoneLogMetadata,
Gitlab::SidekiqMiddleware::WorkerContext::Server,
Gitlab::SidekiqMiddleware::AdminMode::Server,
Gitlab::SidekiqMiddleware::DuplicateJobs::Server
@@ -77,13 +82,11 @@ describe Gitlab::SidekiqMiddleware do
let(:metrics) { false }
let(:arguments_logger) { false }
let(:memory_killer) { false }
- let(:request_store) { false }
let(:disabled_sidekiq_middlewares) do
[
Gitlab::SidekiqMiddleware::ServerMetrics,
Gitlab::SidekiqMiddleware::ArgumentsLogger,
- Gitlab::SidekiqMiddleware::MemoryKiller,
- Gitlab::SidekiqMiddleware::RequestStoreMiddleware
+ Gitlab::SidekiqMiddleware::MemoryKiller
]
end
@@ -94,7 +97,6 @@ describe Gitlab::SidekiqMiddleware do
let(:metrics) { true }
let(:arguments_logger) { true }
let(:memory_killer) { true }
- let(:request_store) { true }
let(:disabled_sidekiq_middlewares) { [] }
it_behaves_like "a server middleware chain"
diff --git a/spec/lib/gitlab/snippet_search_results_spec.rb b/spec/lib/gitlab/snippet_search_results_spec.rb
index 47f26fdebe2..a41be0eaa95 100644
--- a/spec/lib/gitlab/snippet_search_results_spec.rb
+++ b/spec/lib/gitlab/snippet_search_results_spec.rb
@@ -5,7 +5,7 @@ require 'spec_helper'
describe Gitlab::SnippetSearchResults do
include SearchHelpers
- let!(:snippet) { create(:snippet, content: 'foo', file_name: 'foo') }
+ let_it_be(:snippet) { create(:snippet, content: 'foo', file_name: 'foo') }
let(:results) { described_class.new(snippet.author, 'foo') }
describe '#snippet_titles_count' do
@@ -14,27 +14,20 @@ describe Gitlab::SnippetSearchResults do
end
end
- describe '#snippet_blobs_count' do
- it 'returns the amount of matched snippet blobs' do
- expect(results.limited_snippet_blobs_count).to eq(1)
+ describe '#formatted_count' do
+ it 'returns the expected formatted count' do
+ expect(results).to receive(:limited_snippet_titles_count).and_return(1234)
+ expect(results.formatted_count('snippet_titles')).to eq(max_limited_count)
end
end
- describe '#formatted_count' do
- using RSpec::Parameterized::TableSyntax
-
- where(:scope, :count_method, :expected) do
- 'snippet_titles' | :limited_snippet_titles_count | max_limited_count
- 'snippet_blobs' | :limited_snippet_blobs_count | max_limited_count
- 'projects' | :limited_projects_count | max_limited_count
- 'unknown' | nil | nil
- end
+ describe '#objects' do
+ it 'uses page and per_page to paginate results' do
+ snippet2 = create(:snippet, :public, content: 'foo', file_name: 'foo')
- with_them do
- it 'returns the expected formatted count' do
- expect(results).to receive(count_method).and_return(1234) if count_method
- expect(results.formatted_count(scope)).to eq(expected)
- end
+ expect(results.objects('snippet_titles', page: 1, per_page: 1).to_a).to eq([snippet2])
+ expect(results.objects('snippet_titles', page: 2, per_page: 1).to_a).to eq([snippet])
+ expect(results.objects('snippet_titles', page: 1, per_page: 2).count).to eq(2)
end
end
end
diff --git a/spec/lib/gitlab/static_site_editor/config_spec.rb b/spec/lib/gitlab/static_site_editor/config_spec.rb
index 8f61476722d..a1db567db1a 100644
--- a/spec/lib/gitlab/static_site_editor/config_spec.rb
+++ b/spec/lib/gitlab/static_site_editor/config_spec.rb
@@ -5,9 +5,10 @@ require 'spec_helper'
describe Gitlab::StaticSiteEditor::Config do
subject(:config) { described_class.new(repository, ref, file_path, return_url) }
- let(:project) { create(:project, :public, :repository, name: 'project', namespace: namespace) }
- let(:namespace) { create(:namespace, name: 'namespace') }
- let(:repository) { project.repository }
+ let_it_be(:namespace) { create(:namespace, name: 'namespace') }
+ let_it_be(:project) { create(:project, :public, :repository, name: 'project', namespace: namespace) }
+ let_it_be(:repository) { project.repository }
+
let(:ref) { 'master' }
let(:file_path) { 'README.md' }
let(:return_url) { 'http://example.com' }
@@ -24,38 +25,45 @@ describe Gitlab::StaticSiteEditor::Config do
project: 'project',
project_id: project.id,
return_url: 'http://example.com',
- is_supported_content: true
+ is_supported_content: 'true',
+ base_url: '/namespace/project/-/sse/master%2FREADME.md'
)
end
+ context 'when file path is nested' do
+ let(:file_path) { 'lib/README.md' }
+
+ it { is_expected.to include(base_url: '/namespace/project/-/sse/master%2Flib%2FREADME.md') }
+ end
+
context 'when branch is not master' do
let(:ref) { 'my-branch' }
- it { is_expected.to include(is_supported_content: false) }
+ it { is_expected.to include(is_supported_content: 'false') }
end
context 'when file does not have a markdown extension' do
let(:file_path) { 'README.txt' }
- it { is_expected.to include(is_supported_content: false) }
+ it { is_expected.to include(is_supported_content: 'false') }
end
context 'when file does not have an extension' do
let(:file_path) { 'README' }
- it { is_expected.to include(is_supported_content: false) }
+ it { is_expected.to include(is_supported_content: 'false') }
end
context 'when file does not exist' do
let(:file_path) { 'UNKNOWN.md' }
- it { is_expected.to include(is_supported_content: false) }
+ it { is_expected.to include(is_supported_content: 'false') }
end
context 'when repository is empty' do
- let(:project) { create(:project_empty_repo) }
+ let(:repository) { create(:project_empty_repo).repository }
- it { is_expected.to include(is_supported_content: false) }
+ it { is_expected.to include(is_supported_content: 'false') }
end
end
end
diff --git a/spec/lib/gitlab/throttle_spec.rb b/spec/lib/gitlab/throttle_spec.rb
index 674646a5f06..e3679a1a721 100644
--- a/spec/lib/gitlab/throttle_spec.rb
+++ b/spec/lib/gitlab/throttle_spec.rb
@@ -6,82 +6,10 @@ describe Gitlab::Throttle do
describe '.protected_paths_enabled?' do
subject { described_class.protected_paths_enabled? }
- context 'when omnibus protected paths throttle should be used' do
- before do
- expect(described_class).to receive(:should_use_omnibus_protected_paths?).and_return(true)
- end
+ it 'returns Application Settings throttle_protected_paths_enabled?' do
+ expect(Gitlab::CurrentSettings.current_application_settings).to receive(:throttle_protected_paths_enabled?)
- it { is_expected.to be_falsey }
- end
-
- context 'when omnibus protected paths throttle should not be used' do
- before do
- expect(described_class).to receive(:should_use_omnibus_protected_paths?).and_return(false)
- end
-
- it 'returns Application Settings throttle_protected_paths_enabled?' do
- expect(Gitlab::CurrentSettings.current_application_settings).to receive(:throttle_protected_paths_enabled?)
-
- subject
- end
- end
- end
-
- describe '.should_use_omnibus_protected_paths?' do
- subject { described_class.should_use_omnibus_protected_paths? }
-
- context 'when rack_attack.admin_area_protected_paths_enabled config is unspecified' do
- context 'when the omnibus protected paths throttle has been recently used (it has data)' do
- before do
- expect(described_class).to receive(:omnibus_protected_paths_present?).and_return(true)
- end
-
- it { is_expected.to be_truthy }
- end
-
- context 'when the omnibus protected paths throttle has not been recently used' do
- before do
- expect(described_class).to receive(:omnibus_protected_paths_present?).and_return(false)
- end
-
- it { is_expected.to be_falsey }
- end
- end
-
- context 'when rack_attack.admin_area_protected_paths_enabled config is false' do
- before do
- stub_config(rack_attack: {
- admin_area_protected_paths_enabled: false
- })
- end
-
- context 'when the omnibus protected paths throttle has been recently used (it has data)' do
- before do
- expect(described_class).to receive(:omnibus_protected_paths_present?).and_return(true)
- end
-
- it { is_expected.to be_truthy }
- end
-
- context 'when the omnibus protected paths throttle has not been recently used' do
- before do
- expect(described_class).to receive(:omnibus_protected_paths_present?).and_return(false)
- end
-
- it { is_expected.to be_falsey }
- end
- end
-
- context 'when rack_attack.admin_area_protected_paths_enabled config is true' do
- before do
- stub_config(rack_attack: {
- admin_area_protected_paths_enabled: true
- })
-
- expect(described_class).not_to receive(:omnibus_protected_paths_present?)
- end
-
- it { is_expected.to be_falsey }
+ subject
end
end
end
diff --git a/spec/lib/gitlab/tracking_spec.rb b/spec/lib/gitlab/tracking_spec.rb
index efb07d9dc95..2e65f98a085 100644
--- a/spec/lib/gitlab/tracking_spec.rb
+++ b/spec/lib/gitlab/tracking_spec.rb
@@ -28,17 +28,13 @@ describe Gitlab::Tracking do
end
it 'enables features using feature flags' do
- stub_feature_flags(additional_snowplow_tracking: true)
- allow(Feature).to receive(:enabled?).with(
- :additional_snowplow_tracking,
- '_group_'
- ).and_return(false)
+ stub_feature_flags(additional_snowplow_tracking: :__group__)
addition_feature_fields = {
formTracking: false,
linkClickTracking: false
}
- expect(subject.snowplow_options('_group_')).to include(addition_feature_fields)
+ expect(subject.snowplow_options(:_group_)).to include(addition_feature_fields)
end
end
diff --git a/spec/lib/gitlab/tree_summary_spec.rb b/spec/lib/gitlab/tree_summary_spec.rb
index d64b826ba9b..593b8655e80 100644
--- a/spec/lib/gitlab/tree_summary_spec.rb
+++ b/spec/lib/gitlab/tree_summary_spec.rb
@@ -8,12 +8,13 @@ describe Gitlab::TreeSummary do
let(:project) { create(:project, :empty_repo) }
let(:repo) { project.repository }
let(:commit) { repo.head_commit }
+ let_it_be(:user) { create(:user) }
let(:path) { nil }
let(:offset) { nil }
let(:limit) { nil }
- subject(:summary) { described_class.new(commit, project, path: path, offset: offset, limit: limit) }
+ subject(:summary) { described_class.new(commit, project, user, path: path, offset: offset, limit: limit) }
describe '#initialize' do
it 'defaults offset to 0' do
@@ -72,7 +73,8 @@ describe Gitlab::TreeSummary do
expected_commit_path = Gitlab::Routing.url_helpers.project_commit_path(project, commit)
expect(entry[:commit]).to be_a(::Commit)
- expect(entry[:commit_path]).to eq expected_commit_path
+ expect(entry[:commit_path]).to eq(expected_commit_path)
+ expect(entry[:commit_title_html]).to eq(commit.message)
end
context 'in a good subdirectory' do
@@ -140,6 +142,16 @@ describe Gitlab::TreeSummary do
expect(entry).to include(:commit)
end
end
+
+ context 'rendering commits' do
+ it 'does not perform N + 1 request' do
+ summary
+
+ queries = ActiveRecord::QueryRecorder.new { summary.summarize }
+
+ expect(queries.count).to be <= 3
+ end
+ end
end
describe '#more?' do
diff --git a/spec/lib/gitlab/url_builder_spec.rb b/spec/lib/gitlab/url_builder_spec.rb
index 1b23f331b89..66826bcb3b1 100644
--- a/spec/lib/gitlab/url_builder_spec.rb
+++ b/spec/lib/gitlab/url_builder_spec.rb
@@ -23,8 +23,9 @@ describe Gitlab::UrlBuilder do
:merge_request | ->(merge_request) { "/#{merge_request.project.full_path}/-/merge_requests/#{merge_request.iid}" }
:project_milestone | ->(milestone) { "/#{milestone.project.full_path}/-/milestones/#{milestone.iid}" }
:project_snippet | ->(snippet) { "/#{snippet.project.full_path}/snippets/#{snippet.id}" }
- :project_wiki | ->(wiki) { "/#{wiki.project.full_path}/-/wikis/home" }
+ :project_wiki | ->(wiki) { "/#{wiki.container.full_path}/-/wikis/home" }
:ci_build | ->(build) { "/#{build.project.full_path}/-/jobs/#{build.id}" }
+ :design | ->(design) { "/#{design.project.full_path}/-/design_management/designs/#{design.id}/raw_image" }
:group | ->(group) { "/groups/#{group.full_path}" }
:group_milestone | ->(milestone) { "/groups/#{milestone.group.full_path}/-/milestones/#{milestone.iid}" }
@@ -95,6 +96,16 @@ describe Gitlab::UrlBuilder do
end
end
+ context 'when passing a DesignManagement::Design' do
+ let(:design) { build_stubbed(:design) }
+
+ it 'uses the given ref and size in the URL' do
+ url = subject.build(design, ref: 'feature', size: 'small')
+
+ expect(url).to eq "#{Settings.gitlab['url']}/#{design.project.full_path}/-/design_management/designs/#{design.id}/feature/resized_image/small"
+ end
+ end
+
context 'when passing an unsupported class' do
let(:object) { Object.new }
diff --git a/spec/lib/gitlab/usage_data_counters/designs_counter_spec.rb b/spec/lib/gitlab/usage_data_counters/designs_counter_spec.rb
new file mode 100644
index 00000000000..deaf7ebc7f3
--- /dev/null
+++ b/spec/lib/gitlab/usage_data_counters/designs_counter_spec.rb
@@ -0,0 +1,14 @@
+# frozen_string_literal: true
+
+require 'spec_helper'
+
+describe Gitlab::UsageDataCounters::DesignsCounter do
+ it_behaves_like 'a redis usage counter', 'Designs', :create
+ it_behaves_like 'a redis usage counter', 'Designs', :update
+ it_behaves_like 'a redis usage counter', 'Designs', :delete
+
+ it_behaves_like 'a redis usage counter with totals', :design_management_designs,
+ create: 5,
+ update: 3,
+ delete: 2
+end
diff --git a/spec/lib/gitlab/usage_data_counters/web_ide_counter_spec.rb b/spec/lib/gitlab/usage_data_counters/web_ide_counter_spec.rb
index 96ebeb8ff76..42abbecead0 100644
--- a/spec/lib/gitlab/usage_data_counters/web_ide_counter_spec.rb
+++ b/spec/lib/gitlab/usage_data_counters/web_ide_counter_spec.rb
@@ -3,35 +3,35 @@
require 'spec_helper'
describe Gitlab::UsageDataCounters::WebIdeCounter, :clean_gitlab_redis_shared_state do
- shared_examples 'counter examples' do
+ shared_examples 'counter examples' do |event|
it 'increments counter and return the total count' do
- expect(described_class.public_send(total_counter_method)).to eq(0)
+ expect(described_class.public_send(:total_count, event)).to eq(0)
- 2.times { described_class.public_send(increment_counter_method) }
+ 2.times { described_class.public_send(:"increment_#{event}_count") }
- expect(described_class.public_send(total_counter_method)).to eq(2)
+ redis_key = "web_ide_#{event}_count".upcase
+ expect(described_class.public_send(:total_count, redis_key)).to eq(2)
end
end
describe 'commits counter' do
- let(:increment_counter_method) { :increment_commits_count }
- let(:total_counter_method) { :total_commits_count }
-
- it_behaves_like 'counter examples'
+ it_behaves_like 'counter examples', 'commits'
end
describe 'merge requests counter' do
- let(:increment_counter_method) { :increment_merge_requests_count }
- let(:total_counter_method) { :total_merge_requests_count }
-
- it_behaves_like 'counter examples'
+ it_behaves_like 'counter examples', 'merge_requests'
end
describe 'views counter' do
- let(:increment_counter_method) { :increment_views_count }
- let(:total_counter_method) { :total_views_count }
+ it_behaves_like 'counter examples', 'views'
+ end
- it_behaves_like 'counter examples'
+ describe 'terminals counter' do
+ it_behaves_like 'counter examples', 'terminals'
+ end
+
+ describe 'pipelines counter' do
+ it_behaves_like 'counter examples', 'pipelines'
end
describe 'previews counter' do
@@ -42,21 +42,19 @@ describe Gitlab::UsageDataCounters::WebIdeCounter, :clean_gitlab_redis_shared_st
end
context 'when web ide clientside preview is enabled' do
- let(:increment_counter_method) { :increment_previews_count }
- let(:total_counter_method) { :total_previews_count }
-
- it_behaves_like 'counter examples'
+ it_behaves_like 'counter examples', 'previews'
end
context 'when web ide clientside preview is not enabled' do
let(:setting_enabled) { false }
it 'does not increment the counter' do
- expect(described_class.total_previews_count).to eq(0)
+ redis_key = 'WEB_IDE_PREVIEWS_COUNT'
+ expect(described_class.total_count(redis_key)).to eq(0)
2.times { described_class.increment_previews_count }
- expect(described_class.total_previews_count).to eq(0)
+ expect(described_class.total_count(redis_key)).to eq(0)
end
end
end
@@ -66,6 +64,8 @@ describe Gitlab::UsageDataCounters::WebIdeCounter, :clean_gitlab_redis_shared_st
merge_requests = 3
views = 2
previews = 4
+ terminals = 1
+ pipelines = 2
before do
stub_application_setting(web_ide_clientside_preview_enabled: true)
@@ -74,6 +74,8 @@ describe Gitlab::UsageDataCounters::WebIdeCounter, :clean_gitlab_redis_shared_st
merge_requests.times { described_class.increment_merge_requests_count }
views.times { described_class.increment_views_count }
previews.times { described_class.increment_previews_count }
+ terminals.times { described_class.increment_terminals_count }
+ pipelines.times { described_class.increment_pipelines_count }
end
it 'can report all totals' do
@@ -81,7 +83,8 @@ describe Gitlab::UsageDataCounters::WebIdeCounter, :clean_gitlab_redis_shared_st
web_ide_commits: commits,
web_ide_views: views,
web_ide_merge_requests: merge_requests,
- web_ide_previews: previews
+ web_ide_previews: previews,
+ web_ide_terminals: terminals
)
end
end
diff --git a/spec/lib/gitlab/usage_data_spec.rb b/spec/lib/gitlab/usage_data_spec.rb
index a46778bb6c3..9c6aab10083 100644
--- a/spec/lib/gitlab/usage_data_spec.rb
+++ b/spec/lib/gitlab/usage_data_spec.rb
@@ -7,6 +7,8 @@ describe Gitlab::UsageData, :aggregate_failures do
before do
allow(ActiveRecord::Base.connection).to receive(:transaction_open?).and_return(false)
+
+ stub_object_store_settings
end
shared_examples "usage data execution" do
@@ -42,6 +44,9 @@ describe Gitlab::UsageData, :aggregate_failures do
expect(count_data[:projects_jira_active]).to eq(4)
expect(count_data[:projects_jira_server_active]).to eq(2)
expect(count_data[:projects_jira_cloud_active]).to eq(2)
+ expect(count_data[:jira_imports_projects_count]).to eq(2)
+ expect(count_data[:jira_imports_total_imported_count]).to eq(3)
+ expect(count_data[:jira_imports_total_imported_issues_count]).to eq(13)
expect(count_data[:projects_slack_notifications_active]).to eq(2)
expect(count_data[:projects_slack_slash_active]).to eq(1)
expect(count_data[:projects_slack_active]).to eq(2)
@@ -57,6 +62,9 @@ describe Gitlab::UsageData, :aggregate_failures do
expect(count_data[:issues_using_zoom_quick_actions]).to eq(3)
expect(count_data[:issues_with_embedded_grafana_charts_approx]).to eq(2)
expect(count_data[:incident_issues]).to eq(4)
+ expect(count_data[:issues_created_gitlab_alerts]).to eq(1)
+ expect(count_data[:alert_bot_incident_issues]).to eq(4)
+ expect(count_data[:incident_labeled_issues]).to eq(3)
expect(count_data[:clusters_enabled]).to eq(6)
expect(count_data[:project_clusters_enabled]).to eq(4)
@@ -82,6 +90,56 @@ describe Gitlab::UsageData, :aggregate_failures do
expect(count_data[:clusters_management_project]).to eq(1)
end
+ it 'gathers object store usage correctly' do
+ expect(subject[:object_store]).to eq(
+ { artifacts: { enabled: true, object_store: { enabled: true, direct_upload: true, background_upload: false, provider: "AWS" } },
+ external_diffs: { enabled: false },
+ lfs: { enabled: true, object_store: { enabled: false, direct_upload: true, background_upload: false, provider: "AWS" } },
+ uploads: { enabled: nil, object_store: { enabled: false, direct_upload: true, background_upload: false, provider: "AWS" } },
+ packages: { enabled: true, object_store: { enabled: false, direct_upload: false, background_upload: true, provider: "AWS" } } }
+ )
+ end
+
+ context 'with existing container expiration policies' do
+ let_it_be(:disabled) { create(:container_expiration_policy, enabled: false) }
+ let_it_be(:enabled) { create(:container_expiration_policy, enabled: true) }
+
+ %i[keep_n cadence older_than].each do |attribute|
+ ContainerExpirationPolicy.send("#{attribute}_options").keys.each do |value|
+ let_it_be("container_expiration_policy_with_#{attribute}_set_to_#{value}") { create(:container_expiration_policy, attribute => value) }
+ end
+ end
+
+ let(:inactive_policies) { ::ContainerExpirationPolicy.where(enabled: false) }
+ let(:active_policies) { ::ContainerExpirationPolicy.active }
+
+ subject { described_class.data[:counts] }
+
+ it 'gathers usage data' do
+ expect(subject[:projects_with_expiration_policy_enabled]).to eq 20
+ expect(subject[:projects_with_expiration_policy_disabled]).to eq 1
+
+ expect(subject[:projects_with_expiration_policy_enabled_with_keep_n_unset]).to eq 14
+ expect(subject[:projects_with_expiration_policy_enabled_with_keep_n_set_to_1]).to eq 1
+ expect(subject[:projects_with_expiration_policy_enabled_with_keep_n_set_to_5]).to eq 1
+ expect(subject[:projects_with_expiration_policy_enabled_with_keep_n_set_to_10]).to eq 1
+ expect(subject[:projects_with_expiration_policy_enabled_with_keep_n_set_to_25]).to eq 1
+ expect(subject[:projects_with_expiration_policy_enabled_with_keep_n_set_to_50]).to eq 1
+
+ expect(subject[:projects_with_expiration_policy_enabled_with_older_than_unset]).to eq 16
+ expect(subject[:projects_with_expiration_policy_enabled_with_older_than_set_to_7d]).to eq 1
+ expect(subject[:projects_with_expiration_policy_enabled_with_older_than_set_to_14d]).to eq 1
+ expect(subject[:projects_with_expiration_policy_enabled_with_older_than_set_to_30d]).to eq 1
+ expect(subject[:projects_with_expiration_policy_enabled_with_older_than_set_to_90d]).to eq 1
+
+ expect(subject[:projects_with_expiration_policy_enabled_with_cadence_set_to_1d]).to eq 12
+ expect(subject[:projects_with_expiration_policy_enabled_with_cadence_set_to_7d]).to eq 5
+ expect(subject[:projects_with_expiration_policy_enabled_with_cadence_set_to_14d]).to eq 1
+ expect(subject[:projects_with_expiration_policy_enabled_with_cadence_set_to_1month]).to eq 1
+ expect(subject[:projects_with_expiration_policy_enabled_with_cadence_set_to_3month]).to eq 1
+ end
+ end
+
it 'works when queries time out' do
allow_any_instance_of(ActiveRecord::Relation)
.to receive(:count).and_raise(ActiveRecord::StatementInvalid.new(''))
@@ -101,6 +159,7 @@ describe Gitlab::UsageData, :aggregate_failures do
subject { described_class.usage_data_counters }
it { is_expected.to all(respond_to :totals) }
+ it { is_expected.to all(respond_to :fallback_totals) }
describe 'the results of calling #totals on all objects in the array' do
subject { described_class.usage_data_counters.map(&:totals) }
@@ -109,6 +168,13 @@ describe Gitlab::UsageData, :aggregate_failures do
it { is_expected.to all(have_attributes(keys: all(be_a Symbol), values: all(be_a Integer))) }
end
+ describe 'the results of calling #fallback_totals on all objects in the array' do
+ subject { described_class.usage_data_counters.map(&:fallback_totals) }
+
+ it { is_expected.to all(be_a Hash) }
+ it { is_expected.to all(have_attributes(keys: all(be_a Symbol), values: all(eq(-1)))) }
+ end
+
it 'does not have any conflicts' do
all_keys = subject.flat_map { |counter| counter.totals.keys }
@@ -128,6 +194,14 @@ describe Gitlab::UsageData, :aggregate_failures do
end
end
+ describe '.recording_ce_finished_at' do
+ subject { described_class.recording_ce_finish_data }
+
+ it 'gathers time ce recording finishes at' do
+ expect(subject[:recording_ce_finished_at]).to be_a(Time)
+ end
+ end
+
context 'when not relying on database records' do
describe '#features_usage_data_ce' do
subject { described_class.features_usage_data_ce }
@@ -143,42 +217,20 @@ describe Gitlab::UsageData, :aggregate_failures do
expect(subject[:dependency_proxy_enabled]).to eq(Gitlab.config.dependency_proxy.enabled)
expect(subject[:gitlab_shared_runners_enabled]).to eq(Gitlab.config.gitlab_ci.shared_runners_enabled)
expect(subject[:web_ide_clientside_preview_enabled]).to eq(Gitlab::CurrentSettings.web_ide_clientside_preview_enabled?)
+ expect(subject[:grafana_link_enabled]).to eq(Gitlab::CurrentSettings.grafana_enabled?)
end
- context 'with existing container expiration policies' do
- let_it_be(:disabled) { create(:container_expiration_policy, enabled: false) }
- let_it_be(:enabled) { create(:container_expiration_policy, enabled: true) }
- %i[keep_n cadence older_than].each do |attribute|
- ContainerExpirationPolicy.send("#{attribute}_options").keys.each do |value|
- let_it_be("container_expiration_policy_with_#{attribute}_set_to_#{value}") { create(:container_expiration_policy, attribute => value) }
- end
+ context 'with embedded grafana' do
+ it 'returns true when embedded grafana is enabled' do
+ stub_application_setting(grafana_enabled: true)
+
+ expect(subject[:grafana_link_enabled]).to eq(true)
end
- let(:inactive_policies) { ::ContainerExpirationPolicy.where(enabled: false) }
- let(:active_policies) { ::ContainerExpirationPolicy.active }
-
- it 'gathers usage data' do
- expect(subject[:projects_with_expiration_policy_enabled]).to eq 16
- expect(subject[:projects_with_expiration_policy_disabled]).to eq 1
-
- expect(subject[:projects_with_expiration_policy_enabled_with_keep_n_unset]).to eq 10
- expect(subject[:projects_with_expiration_policy_enabled_with_keep_n_set_to_1]).to eq 1
- expect(subject[:projects_with_expiration_policy_enabled_with_keep_n_set_to_5]).to eq 1
- expect(subject[:projects_with_expiration_policy_enabled_with_keep_n_set_to_10]).to eq 1
- expect(subject[:projects_with_expiration_policy_enabled_with_keep_n_set_to_25]).to eq 1
- expect(subject[:projects_with_expiration_policy_enabled_with_keep_n_set_to_50]).to eq 1
-
- expect(subject[:projects_with_expiration_policy_enabled_with_older_than_unset]).to eq 12
- expect(subject[:projects_with_expiration_policy_enabled_with_older_than_set_to_7d]).to eq 1
- expect(subject[:projects_with_expiration_policy_enabled_with_older_than_set_to_14d]).to eq 1
- expect(subject[:projects_with_expiration_policy_enabled_with_older_than_set_to_30d]).to eq 1
- expect(subject[:projects_with_expiration_policy_enabled_with_older_than_set_to_90d]).to eq 1
-
- expect(subject[:projects_with_expiration_policy_enabled_with_cadence_set_to_1d]).to eq 12
- expect(subject[:projects_with_expiration_policy_enabled_with_cadence_set_to_7d]).to eq 1
- expect(subject[:projects_with_expiration_policy_enabled_with_cadence_set_to_14d]).to eq 1
- expect(subject[:projects_with_expiration_policy_enabled_with_cadence_set_to_1month]).to eq 1
- expect(subject[:projects_with_expiration_policy_enabled_with_cadence_set_to_3month]).to eq 1
+ it 'returns false when embedded grafana is disabled' do
+ stub_application_setting(grafana_enabled: false)
+
+ expect(subject[:grafana_link_enabled]).to eq(false)
end
end
end
@@ -223,6 +275,66 @@ describe Gitlab::UsageData, :aggregate_failures do
end
end
+ describe '#object_store_config' do
+ let(:component) { 'lfs' }
+
+ subject { described_class.object_store_config(component) }
+
+ context 'when object_store is not configured' do
+ it 'returns component enable status only' do
+ allow(Settings).to receive(:[]).with(component).and_return({ 'enabled' => false })
+
+ expect(subject).to eq({ enabled: false })
+ end
+ end
+
+ context 'when object_store is configured' do
+ it 'returns filtered object store config' do
+ allow(Settings).to receive(:[]).with(component)
+ .and_return(
+ { 'enabled' => true,
+ 'object_store' =>
+ { 'enabled' => true,
+ 'remote_directory' => component,
+ 'direct_upload' => true,
+ 'connection' =>
+ { 'provider' => 'AWS', 'aws_access_key_id' => 'minio', 'aws_secret_access_key' => 'gdk-minio', 'region' => 'gdk', 'endpoint' => 'http://127.0.0.1:9000', 'path_style' => true },
+ 'background_upload' => false,
+ 'proxy_download' => false } })
+
+ expect(subject).to eq(
+ { enabled: true, object_store: { enabled: true, direct_upload: true, background_upload: false, provider: "AWS" } })
+ end
+ end
+
+ context 'when retrieve component setting meets exception' do
+ it 'returns -1 for component enable status' do
+ allow(Settings).to receive(:[]).with(component).and_raise(StandardError)
+
+ expect(subject).to eq({ enabled: -1 })
+ end
+ end
+ end
+
+ describe '#object_store_usage_data' do
+ subject { described_class.object_store_usage_data }
+
+ it 'fetches object store config of five components' do
+ %w(artifacts external_diffs lfs uploads packages).each do |component|
+ expect(described_class).to receive(:object_store_config).with(component).and_return("#{component}_object_store_config")
+ end
+
+ expect(subject).to eq(
+ object_store: {
+ artifacts: 'artifacts_object_store_config',
+ external_diffs: 'external_diffs_object_store_config',
+ lfs: 'lfs_object_store_config',
+ uploads: 'uploads_object_store_config',
+ packages: 'packages_object_store_config'
+ })
+ end
+ end
+
describe '#cycle_analytics_usage_data' do
subject { described_class.cycle_analytics_usage_data }
@@ -244,18 +356,132 @@ describe Gitlab::UsageData, :aggregate_failures do
describe '#ingress_modsecurity_usage' do
subject { described_class.ingress_modsecurity_usage }
- it 'gathers variable data' do
- allow_any_instance_of(
- ::Clusters::Applications::IngressModsecurityUsageService
- ).to receive(:execute).and_return(
- {
- ingress_modsecurity_blocking: 1,
- ingress_modsecurity_disabled: 2
- }
- )
-
- expect(subject[:ingress_modsecurity_blocking]).to eq(1)
- expect(subject[:ingress_modsecurity_disabled]).to eq(2)
+ let(:environment) { create(:environment) }
+ let(:project) { environment.project }
+ let(:environment_scope) { '*' }
+ let(:deployment) { create(:deployment, :success, environment: environment, project: project, cluster: cluster) }
+ let(:cluster) { create(:cluster, environment_scope: environment_scope, projects: [project]) }
+ let(:ingress_mode) { :modsecurity_blocking }
+ let!(:ingress) { create(:clusters_applications_ingress, ingress_mode, cluster: cluster) }
+
+ context 'when cluster is disabled' do
+ let(:cluster) { create(:cluster, :disabled, projects: [project]) }
+
+ it 'gathers ingress data' do
+ expect(subject[:ingress_modsecurity_logging]).to eq(0)
+ expect(subject[:ingress_modsecurity_blocking]).to eq(0)
+ expect(subject[:ingress_modsecurity_disabled]).to eq(0)
+ expect(subject[:ingress_modsecurity_not_installed]).to eq(0)
+ end
+ end
+
+ context 'when deployment is unsuccessful' do
+ let!(:deployment) { create(:deployment, :failed, environment: environment, project: project, cluster: cluster) }
+
+ it 'gathers ingress data' do
+ expect(subject[:ingress_modsecurity_logging]).to eq(0)
+ expect(subject[:ingress_modsecurity_blocking]).to eq(0)
+ expect(subject[:ingress_modsecurity_disabled]).to eq(0)
+ expect(subject[:ingress_modsecurity_not_installed]).to eq(0)
+ end
+ end
+
+ context 'when deployment is successful' do
+ let!(:deployment) { create(:deployment, :success, environment: environment, project: project, cluster: cluster) }
+
+ context 'when modsecurity is in blocking mode' do
+ it 'gathers ingress data' do
+ expect(subject[:ingress_modsecurity_logging]).to eq(0)
+ expect(subject[:ingress_modsecurity_blocking]).to eq(1)
+ expect(subject[:ingress_modsecurity_disabled]).to eq(0)
+ expect(subject[:ingress_modsecurity_not_installed]).to eq(0)
+ end
+ end
+
+ context 'when modsecurity is in logging mode' do
+ let(:ingress_mode) { :modsecurity_logging }
+
+ it 'gathers ingress data' do
+ expect(subject[:ingress_modsecurity_logging]).to eq(1)
+ expect(subject[:ingress_modsecurity_blocking]).to eq(0)
+ expect(subject[:ingress_modsecurity_disabled]).to eq(0)
+ expect(subject[:ingress_modsecurity_not_installed]).to eq(0)
+ end
+ end
+
+ context 'when modsecurity is disabled' do
+ let(:ingress_mode) { :modsecurity_disabled }
+
+ it 'gathers ingress data' do
+ expect(subject[:ingress_modsecurity_logging]).to eq(0)
+ expect(subject[:ingress_modsecurity_blocking]).to eq(0)
+ expect(subject[:ingress_modsecurity_disabled]).to eq(1)
+ expect(subject[:ingress_modsecurity_not_installed]).to eq(0)
+ end
+ end
+
+ context 'when modsecurity is not installed' do
+ let(:ingress_mode) { :modsecurity_not_installed }
+
+ it 'gathers ingress data' do
+ expect(subject[:ingress_modsecurity_logging]).to eq(0)
+ expect(subject[:ingress_modsecurity_blocking]).to eq(0)
+ expect(subject[:ingress_modsecurity_disabled]).to eq(0)
+ expect(subject[:ingress_modsecurity_not_installed]).to eq(1)
+ end
+ end
+
+ context 'with multiple projects' do
+ let(:environment_2) { create(:environment) }
+ let(:project_2) { environment_2.project }
+ let(:cluster_2) { create(:cluster, environment_scope: environment_scope, projects: [project_2]) }
+ let!(:ingress_2) { create(:clusters_applications_ingress, :modsecurity_logging, cluster: cluster_2) }
+ let!(:deployment_2) { create(:deployment, :success, environment: environment_2, project: project_2, cluster: cluster_2) }
+
+ it 'gathers non-duplicated ingress data' do
+ expect(subject[:ingress_modsecurity_logging]).to eq(1)
+ expect(subject[:ingress_modsecurity_blocking]).to eq(1)
+ expect(subject[:ingress_modsecurity_disabled]).to eq(0)
+ expect(subject[:ingress_modsecurity_not_installed]).to eq(0)
+ end
+ end
+
+ context 'with multiple deployments' do
+ let!(:deployment_2) { create(:deployment, :success, environment: environment, project: project, cluster: cluster) }
+
+ it 'gathers non-duplicated ingress data' do
+ expect(subject[:ingress_modsecurity_logging]).to eq(0)
+ expect(subject[:ingress_modsecurity_blocking]).to eq(1)
+ expect(subject[:ingress_modsecurity_disabled]).to eq(0)
+ expect(subject[:ingress_modsecurity_not_installed]).to eq(0)
+ end
+ end
+
+ context 'with multiple projects' do
+ let(:environment_2) { create(:environment) }
+ let(:project_2) { environment_2.project }
+ let!(:deployment_2) { create(:deployment, :success, environment: environment_2, project: project_2, cluster: cluster) }
+ let(:cluster) { create(:cluster, environment_scope: environment_scope, projects: [project, project_2]) }
+
+ it 'gathers ingress data' do
+ expect(subject[:ingress_modsecurity_logging]).to eq(0)
+ expect(subject[:ingress_modsecurity_blocking]).to eq(2)
+ expect(subject[:ingress_modsecurity_disabled]).to eq(0)
+ expect(subject[:ingress_modsecurity_not_installed]).to eq(0)
+ end
+ end
+
+ context 'with multiple environments' do
+ let!(:environment_2) { create(:environment, project: project) }
+ let!(:deployment_2) { create(:deployment, :success, environment: environment_2, project: project, cluster: cluster) }
+
+ it 'gathers ingress data' do
+ expect(subject[:ingress_modsecurity_logging]).to eq(0)
+ expect(subject[:ingress_modsecurity_blocking]).to eq(2)
+ expect(subject[:ingress_modsecurity_disabled]).to eq(0)
+ expect(subject[:ingress_modsecurity_not_installed]).to eq(0)
+ end
+ end
end
end
@@ -334,9 +560,10 @@ describe Gitlab::UsageData, :aggregate_failures do
end
it 'returns the fallback value when counting fails' do
+ stub_const("Gitlab::UsageData::FALLBACK", 15)
allow(relation).to receive(:count).and_raise(ActiveRecord::StatementInvalid.new(''))
- expect(described_class.count(relation, fallback: 15, batch: false)).to eq(15)
+ expect(described_class.count(relation, batch: false)).to eq(15)
end
end
@@ -350,9 +577,10 @@ describe Gitlab::UsageData, :aggregate_failures do
end
it 'returns the fallback value when counting fails' do
+ stub_const("Gitlab::UsageData::FALLBACK", 15)
allow(relation).to receive(:distinct_count_by).and_raise(ActiveRecord::StatementInvalid.new(''))
- expect(described_class.distinct_count(relation, fallback: 15, batch: false)).to eq(15)
+ expect(described_class.distinct_count(relation, batch: false)).to eq(15)
end
end
end
@@ -387,4 +615,28 @@ describe Gitlab::UsageData, :aggregate_failures do
expect(described_class.alt_usage_data(1)).to eq 1
end
end
+
+ describe '#redis_usage_data' do
+ context 'with block given' do
+ it 'returns the fallback when it gets an error' do
+ expect(described_class.redis_usage_data { raise ::Redis::CommandError } ).to eq(-1)
+ end
+
+ it 'returns the evaluated block when given' do
+ expect(described_class.redis_usage_data { 1 }).to eq(1)
+ end
+ end
+
+ context 'with counter given' do
+ it 'returns the falback values for all counter keys when it gets an error' do
+ allow(::Gitlab::UsageDataCounters::WikiPageCounter).to receive(:totals).and_raise(::Redis::CommandError)
+ expect(described_class.redis_usage_data(::Gitlab::UsageDataCounters::WikiPageCounter)).to eql(::Gitlab::UsageDataCounters::WikiPageCounter.fallback_totals)
+ end
+
+ it 'returns the totals when couter is given' do
+ allow(::Gitlab::UsageDataCounters::WikiPageCounter).to receive(:totals).and_return({ wiki_pages_create: 2 })
+ expect(described_class.redis_usage_data(::Gitlab::UsageDataCounters::WikiPageCounter)).to eql({ wiki_pages_create: 2 })
+ end
+ end
+ end
end
diff --git a/spec/lib/gitlab/user_access_snippet_spec.rb b/spec/lib/gitlab/user_access_snippet_spec.rb
index 57e52e2e93d..2e8a0a49a76 100644
--- a/spec/lib/gitlab/user_access_snippet_spec.rb
+++ b/spec/lib/gitlab/user_access_snippet_spec.rb
@@ -7,6 +7,8 @@ describe Gitlab::UserAccessSnippet do
let_it_be(:project) { create(:project, :private) }
let_it_be(:snippet) { create(:project_snippet, :private, project: project) }
+ let_it_be(:migration_bot) { User.migration_bot }
+
let(:user) { create(:user) }
describe '#can_do_action?' do
@@ -36,6 +38,14 @@ describe Gitlab::UserAccessSnippet do
expect(access.can_do_action?(:ability)).to eq(false)
end
end
+
+ context 'when user is migration bot' do
+ let(:user) { migration_bot }
+
+ it 'allows access' do
+ expect(access.can_do_action?(:ability)).to eq(true)
+ end
+ end
end
describe '#can_push_to_branch?' do
@@ -65,6 +75,16 @@ describe Gitlab::UserAccessSnippet do
end
end
+ context 'when user is migration bot' do
+ let(:user) { migration_bot }
+
+ it 'allows access' do
+ allow(Ability).to receive(:allowed?).and_return(false)
+
+ expect(access.can_push_to_branch?('random_branch')).to eq(true)
+ end
+ end
+
context 'when snippet is nil' do
let(:user) { create_user_from_membership(project, :admin) }
let(:snippet) { nil }
@@ -72,6 +92,14 @@ describe Gitlab::UserAccessSnippet do
it 'disallows access' do
expect(access.can_push_to_branch?('random_branch')).to eq(false)
end
+
+ context 'when user is migration bot' do
+ let(:user) { migration_bot }
+
+ it 'disallows access' do
+ expect(access.can_push_to_branch?('random_branch')).to eq(false)
+ end
+ end
end
end
@@ -79,17 +107,41 @@ describe Gitlab::UserAccessSnippet do
it 'returns false' do
expect(access.can_create_tag?('random_tag')).to be_falsey
end
+
+ context 'when user is migration bot' do
+ let(:user) { migration_bot }
+
+ it 'returns false' do
+ expect(access.can_create_tag?('random_tag')).to be_falsey
+ end
+ end
end
describe '#can_delete_branch?' do
it 'returns false' do
expect(access.can_delete_branch?('random_branch')).to be_falsey
end
+
+ context 'when user is migration bot' do
+ let(:user) { migration_bot }
+
+ it 'returns false' do
+ expect(access.can_delete_branch?('random_branch')).to be_falsey
+ end
+ end
end
describe '#can_merge_to_branch?' do
it 'returns false' do
expect(access.can_merge_to_branch?('random_branch')).to be_falsey
end
+
+ context 'when user is migration bot' do
+ let(:user) { migration_bot }
+
+ it 'returns false' do
+ expect(access.can_merge_to_branch?('random_branch')).to be_falsey
+ end
+ end
end
end
diff --git a/spec/lib/gitlab/utils/measuring_spec.rb b/spec/lib/gitlab/utils/measuring_spec.rb
new file mode 100644
index 00000000000..254f53f7da3
--- /dev/null
+++ b/spec/lib/gitlab/utils/measuring_spec.rb
@@ -0,0 +1,40 @@
+# frozen_string_literal: true
+
+require 'fast_spec_helper'
+
+describe Gitlab::Utils::Measuring do
+ describe '#with_measuring' do
+ let(:base_log_data) { {} }
+ let(:result) { "result" }
+
+ before do
+ allow(ActiveSupport::Logger).to receive(:logger_outputs_to?).with(Gitlab::Utils::Measuring.logger, STDOUT).and_return(false)
+ end
+
+ let(:measurement) { described_class.new(base_log_data) }
+
+ subject do
+ measurement.with_measuring { result }
+ end
+
+ it 'measures and logs data', :aggregate_failure do
+ expect(measurement).to receive(:with_measure_time).and_call_original
+ expect(measurement).to receive(:with_count_queries).and_call_original
+ expect(measurement).to receive(:with_gc_stats).and_call_original
+
+ expect(described_class.logger).to receive(:info).with(include(:gc_stats, :time_to_finish, :number_of_sql_calls, :memory_usage, :label))
+
+ is_expected.to eq(result)
+ end
+
+ context 'with base_log_data provided' do
+ let(:base_log_data) { { test: "data" } }
+
+ it 'logs includes base data' do
+ expect(described_class.logger).to receive(:info).with(include(:test, :gc_stats, :time_to_finish, :number_of_sql_calls, :memory_usage, :label))
+
+ subject
+ end
+ end
+ end
+end
diff --git a/spec/lib/gitlab/utils_spec.rb b/spec/lib/gitlab/utils_spec.rb
index e34367cbbf9..0f0d6a93c97 100644
--- a/spec/lib/gitlab/utils_spec.rb
+++ b/spec/lib/gitlab/utils_spec.rb
@@ -59,9 +59,10 @@ describe Gitlab::Utils do
using RSpec::Parameterized::TableSyntax
where(:original, :expected) do
- 1999.8999 | 2
- 12384 | 12.38
- 333 | 0.33
+ 1999.8999 | 1.9999
+ 12384 | 12.384
+ 333 | 0.333
+ 1333.33333333 | 1.333333
end
with_them do
@@ -129,7 +130,7 @@ describe Gitlab::Utils do
expect(to_boolean(false)).to be(false)
end
- it 'converts a valid string to a boolean' do
+ it 'converts a valid value to a boolean' do
expect(to_boolean(true)).to be(true)
expect(to_boolean('true')).to be(true)
expect(to_boolean('YeS')).to be(true)
@@ -145,12 +146,35 @@ describe Gitlab::Utils do
expect(to_boolean('oFF')).to be(false)
end
- it 'converts an invalid string to nil' do
+ it 'converts an invalid value to nil' do
expect(to_boolean('fals')).to be_nil
expect(to_boolean('yeah')).to be_nil
expect(to_boolean('')).to be_nil
expect(to_boolean(nil)).to be_nil
end
+
+ it 'accepts a default value, and does not return it when a valid value is given' do
+ expect(to_boolean(true, default: false)).to be(true)
+ expect(to_boolean('true', default: false)).to be(true)
+ expect(to_boolean('YeS', default: false)).to be(true)
+ expect(to_boolean('t', default: false)).to be(true)
+ expect(to_boolean('1', default: 'any value')).to be(true)
+ expect(to_boolean('ON', default: 42)).to be(true)
+
+ expect(to_boolean('FaLse', default: true)).to be(false)
+ expect(to_boolean('F', default: true)).to be(false)
+ expect(to_boolean('NO', default: true)).to be(false)
+ expect(to_boolean('n', default: true)).to be(false)
+ expect(to_boolean('0', default: 'any value')).to be(false)
+ expect(to_boolean('oFF', default: 42)).to be(false)
+ end
+
+ it 'accepts a default value, and returns it when an invalid value is given' do
+ expect(to_boolean('fals', default: true)).to eq(true)
+ expect(to_boolean('yeah', default: false)).to eq(false)
+ expect(to_boolean('', default: 'any value')).to eq('any value')
+ expect(to_boolean(nil, default: 42)).to eq(42)
+ end
end
describe '.boolean_to_yes_no' do
diff --git a/spec/lib/gitlab/view/presenter/factory_spec.rb b/spec/lib/gitlab/view/presenter/factory_spec.rb
index 515a1b0a8e4..7bf3c325019 100644
--- a/spec/lib/gitlab/view/presenter/factory_spec.rb
+++ b/spec/lib/gitlab/view/presenter/factory_spec.rb
@@ -31,11 +31,11 @@ describe Gitlab::View::Presenter::Factory do
end
it 'uses the presenter_class if given on #initialize' do
- MyCustomPresenter = Class.new(described_class)
+ my_custom_presenter = Class.new(described_class)
- presenter = described_class.new(build, presenter_class: MyCustomPresenter).fabricate!
+ presenter = described_class.new(build, presenter_class: my_custom_presenter).fabricate!
- expect(presenter).to be_a(MyCustomPresenter)
+ expect(presenter).to be_a(my_custom_presenter)
end
end
end
diff --git a/spec/lib/gitlab/wiki_pages/front_matter_parser_spec.rb b/spec/lib/gitlab/wiki_pages/front_matter_parser_spec.rb
index c606ba11b9c..f9ed769f2d9 100644
--- a/spec/lib/gitlab/wiki_pages/front_matter_parser_spec.rb
+++ b/spec/lib/gitlab/wiki_pages/front_matter_parser_spec.rb
@@ -76,11 +76,7 @@ describe Gitlab::WikiPages::FrontMatterParser do
let(:raw_content) { with_front_matter }
before do
- stub_feature_flags(Gitlab::WikiPages::FrontMatterParser::FEATURE_FLAG => false)
- stub_feature_flags(Gitlab::WikiPages::FrontMatterParser::FEATURE_FLAG => {
- enabled: true,
- thing: gate
- })
+ stub_feature_flags(Gitlab::WikiPages::FrontMatterParser::FEATURE_FLAG => gate)
end
it do
diff --git a/spec/lib/gitlab/with_request_store_spec.rb b/spec/lib/gitlab/with_request_store_spec.rb
new file mode 100644
index 00000000000..1ef8d986f96
--- /dev/null
+++ b/spec/lib/gitlab/with_request_store_spec.rb
@@ -0,0 +1,30 @@
+# frozen_string_literal: true
+
+require 'fast_spec_helper'
+require 'request_store'
+
+describe Gitlab::WithRequestStore do
+ let(:fake_class) { Class.new { include Gitlab::WithRequestStore } }
+
+ subject(:object) { fake_class.new }
+
+ describe "#with_request_store" do
+ it 'starts a request store and yields control' do
+ expect(RequestStore).to receive(:begin!).ordered
+ expect(RequestStore).to receive(:end!).ordered
+ expect(RequestStore).to receive(:clear!).ordered
+
+ expect { |b| object.with_request_store(&b) }.to yield_control
+ end
+
+ it 'only starts a request store once when nested' do
+ expect(RequestStore).to receive(:begin!).ordered.once.and_call_original
+ expect(RequestStore).to receive(:end!).ordered.once.and_call_original
+ expect(RequestStore).to receive(:clear!).ordered.once.and_call_original
+
+ object.with_request_store do
+ expect { |b| object.with_request_store(&b) }.to yield_control
+ end
+ end
+ end
+end
diff --git a/spec/lib/gitlab/workhorse_spec.rb b/spec/lib/gitlab/workhorse_spec.rb
index 921ed568b71..53b6f461a48 100644
--- a/spec/lib/gitlab/workhorse_spec.rb
+++ b/spec/lib/gitlab/workhorse_spec.rb
@@ -9,7 +9,7 @@ describe Gitlab::Workhorse do
def decode_workhorse_header(array)
key, value = array
command, encoded_params = value.split(":")
- params = JSON.parse(Base64.urlsafe_decode64(encoded_params))
+ params = Gitlab::Json.parse(Base64.urlsafe_decode64(encoded_params))
[key, command, params]
end
@@ -24,7 +24,7 @@ describe Gitlab::Workhorse do
let(:ref) { 'master' }
let(:format) { 'zip' }
let(:storage_path) { Gitlab.config.gitlab.repository_downloads_path }
- let(:path) { 'some/path' if Feature.enabled?(:git_archive_path, default_enabled: true) }
+ let(:path) { 'some/path' }
let(:metadata) { repository.archive_metadata(ref, storage_path, format, append_sha: nil, path: path) }
let(:cache_disabled) { false }
@@ -36,70 +36,36 @@ describe Gitlab::Workhorse do
allow(described_class).to receive(:git_archive_cache_disabled?).and_return(cache_disabled)
end
- context 'feature flag disabled' do
- before do
- stub_feature_flags(git_archive_path: false)
- end
-
- it 'sets the header correctly' do
- key, command, params = decode_workhorse_header(subject)
+ it 'sets the header correctly' do
+ key, command, params = decode_workhorse_header(subject)
- expected_params = metadata.merge(
- 'GitalyRepository' => repository.gitaly_repository.to_h,
- 'GitalyServer' => {
- features: { 'gitaly-feature-foobar' => 'true' },
- address: Gitlab::GitalyClient.address(project.repository_storage),
- token: Gitlab::GitalyClient.token(project.repository_storage)
- }
+ expect(key).to eq('Gitlab-Workhorse-Send-Data')
+ expect(command).to eq('git-archive')
+ expect(params).to eq({
+ 'GitalyServer' => {
+ features: { 'gitaly-feature-foobar' => 'true' },
+ address: Gitlab::GitalyClient.address(project.repository_storage),
+ token: Gitlab::GitalyClient.token(project.repository_storage)
+ },
+ 'ArchivePath' => metadata['ArchivePath'],
+ 'GetArchiveRequest' => Base64.encode64(
+ Gitaly::GetArchiveRequest.new(
+ repository: repository.gitaly_repository,
+ commit_id: metadata['CommitId'],
+ prefix: metadata['ArchivePrefix'],
+ format: Gitaly::GetArchiveRequest::Format::ZIP,
+ path: path
+ ).to_proto
)
-
- expect(key).to eq('Gitlab-Workhorse-Send-Data')
- expect(command).to eq('git-archive')
- expect(params).to eq(expected_params.deep_stringify_keys)
- end
-
- context 'when archive caching is disabled' do
- let(:cache_disabled) { true }
-
- it 'tells workhorse not to use the cache' do
- _, _, params = decode_workhorse_header(subject)
- expect(params).to include({ 'DisableCache' => true })
- end
- end
+ }.deep_stringify_keys)
end
- context 'feature flag enabled' do
- it 'sets the header correctly' do
- key, command, params = decode_workhorse_header(subject)
-
- expect(key).to eq('Gitlab-Workhorse-Send-Data')
- expect(command).to eq('git-archive')
- expect(params).to eq({
- 'GitalyServer' => {
- features: { 'gitaly-feature-foobar' => 'true' },
- address: Gitlab::GitalyClient.address(project.repository_storage),
- token: Gitlab::GitalyClient.token(project.repository_storage)
- },
- 'ArchivePath' => metadata['ArchivePath'],
- 'GetArchiveRequest' => Base64.encode64(
- Gitaly::GetArchiveRequest.new(
- repository: repository.gitaly_repository,
- commit_id: metadata['CommitId'],
- prefix: metadata['ArchivePrefix'],
- format: Gitaly::GetArchiveRequest::Format::ZIP,
- path: path
- ).to_proto
- )
- }.deep_stringify_keys)
- end
-
- context 'when archive caching is disabled' do
- let(:cache_disabled) { true }
+ context 'when archive caching is disabled' do
+ let(:cache_disabled) { true }
- it 'tells workhorse not to use the cache' do
- _, _, params = decode_workhorse_header(subject)
- expect(params).to include({ 'DisableCache' => true })
- end
+ it 'tells workhorse not to use the cache' do
+ _, _, params = decode_workhorse_header(subject)
+ expect(params).to include({ 'DisableCache' => true })
end
end
diff --git a/spec/lib/gitlab/x509/signature_spec.rb b/spec/lib/gitlab/x509/signature_spec.rb
index 6c585acd5cd..cff2fd7748b 100644
--- a/spec/lib/gitlab/x509/signature_spec.rb
+++ b/spec/lib/gitlab/x509/signature_spec.rb
@@ -229,4 +229,164 @@ describe Gitlab::X509::Signature do
end
end
end
+
+ describe '#user' do
+ signature = described_class.new(
+ X509Helpers::User1.signed_tag_signature,
+ X509Helpers::User1.signed_tag_base_data,
+ X509Helpers::User1.certificate_email,
+ X509Helpers::User1.signed_commit_time
+ )
+
+ context 'if email is assigned to a user' do
+ let!(:user) { create(:user, email: X509Helpers::User1.certificate_email) }
+
+ it 'returns user' do
+ expect(signature.user).to eq(user)
+ end
+ end
+
+ it 'if email is not assigned to a user, return nil' do
+ expect(signature.user).to be_nil
+ end
+ end
+
+ context 'tag signature' do
+ let(:certificate_attributes) do
+ {
+ subject_key_identifier: X509Helpers::User1.tag_certificate_subject_key_identifier,
+ subject: X509Helpers::User1.certificate_subject,
+ email: X509Helpers::User1.certificate_email,
+ serial_number: X509Helpers::User1.tag_certificate_serial
+ }
+ end
+
+ let(:issuer_attributes) do
+ {
+ subject_key_identifier: X509Helpers::User1.tag_issuer_subject_key_identifier,
+ subject: X509Helpers::User1.tag_certificate_issuer,
+ crl_url: X509Helpers::User1.tag_certificate_crl
+ }
+ end
+
+ context 'verified signature' do
+ context 'with trusted certificate store' do
+ before do
+ store = OpenSSL::X509::Store.new
+ certificate = OpenSSL::X509::Certificate.new X509Helpers::User1.trust_cert
+ store.add_cert(certificate)
+ allow(OpenSSL::X509::Store).to receive(:new).and_return(store)
+ end
+
+ it 'returns a verified signature if email does match' do
+ signature = described_class.new(
+ X509Helpers::User1.signed_tag_signature,
+ X509Helpers::User1.signed_tag_base_data,
+ X509Helpers::User1.certificate_email,
+ X509Helpers::User1.signed_commit_time
+ )
+
+ expect(signature.x509_certificate).to have_attributes(certificate_attributes)
+ expect(signature.x509_certificate.x509_issuer).to have_attributes(issuer_attributes)
+ expect(signature.verified_signature).to be_truthy
+ expect(signature.verification_status).to eq(:verified)
+ end
+
+ it 'returns an unverified signature if email does not match' do
+ signature = described_class.new(
+ X509Helpers::User1.signed_tag_signature,
+ X509Helpers::User1.signed_tag_base_data,
+ "gitlab@example.com",
+ X509Helpers::User1.signed_commit_time
+ )
+
+ expect(signature.x509_certificate).to have_attributes(certificate_attributes)
+ expect(signature.x509_certificate.x509_issuer).to have_attributes(issuer_attributes)
+ expect(signature.verified_signature).to be_truthy
+ expect(signature.verification_status).to eq(:unverified)
+ end
+
+ it 'returns an unverified signature if email does match and time is wrong' do
+ signature = described_class.new(
+ X509Helpers::User1.signed_tag_signature,
+ X509Helpers::User1.signed_tag_base_data,
+ X509Helpers::User1.certificate_email,
+ Time.new(2020, 2, 22)
+ )
+
+ expect(signature.x509_certificate).to have_attributes(certificate_attributes)
+ expect(signature.x509_certificate.x509_issuer).to have_attributes(issuer_attributes)
+ expect(signature.verified_signature).to be_falsey
+ expect(signature.verification_status).to eq(:unverified)
+ end
+
+ it 'returns an unverified signature if certificate is revoked' do
+ signature = described_class.new(
+ X509Helpers::User1.signed_tag_signature,
+ X509Helpers::User1.signed_tag_base_data,
+ X509Helpers::User1.certificate_email,
+ X509Helpers::User1.signed_commit_time
+ )
+
+ expect(signature.verification_status).to eq(:verified)
+
+ signature.x509_certificate.revoked!
+
+ expect(signature.verification_status).to eq(:unverified)
+ end
+ end
+
+ context 'without trusted certificate within store' do
+ before do
+ store = OpenSSL::X509::Store.new
+ allow(OpenSSL::X509::Store).to receive(:new)
+ .and_return(
+ store
+ )
+ end
+
+ it 'returns an unverified signature' do
+ signature = described_class.new(
+ X509Helpers::User1.signed_tag_signature,
+ X509Helpers::User1.signed_tag_base_data,
+ X509Helpers::User1.certificate_email,
+ X509Helpers::User1.signed_commit_time
+ )
+
+ expect(signature.x509_certificate).to have_attributes(certificate_attributes)
+ expect(signature.x509_certificate.x509_issuer).to have_attributes(issuer_attributes)
+ expect(signature.verified_signature).to be_falsey
+ expect(signature.verification_status).to eq(:unverified)
+ end
+ end
+ end
+
+ context 'invalid signature' do
+ it 'returns nil' do
+ signature = described_class.new(
+ X509Helpers::User1.signed_tag_signature.tr('A', 'B'),
+ X509Helpers::User1.signed_tag_base_data,
+ X509Helpers::User1.certificate_email,
+ X509Helpers::User1.signed_commit_time
+ )
+ expect(signature.x509_certificate).to be_nil
+ expect(signature.verified_signature).to be_falsey
+ expect(signature.verification_status).to eq(:unverified)
+ end
+ end
+
+ context 'invalid message' do
+ it 'returns nil' do
+ signature = described_class.new(
+ X509Helpers::User1.signed_tag_signature,
+ 'x',
+ X509Helpers::User1.certificate_email,
+ X509Helpers::User1.signed_commit_time
+ )
+ expect(signature.x509_certificate).to be_nil
+ expect(signature.verified_signature).to be_falsey
+ expect(signature.verification_status).to eq(:unverified)
+ end
+ end
+ end
end
diff --git a/spec/lib/gitlab/x509/tag_spec.rb b/spec/lib/gitlab/x509/tag_spec.rb
new file mode 100644
index 00000000000..4bc9723bd0d
--- /dev/null
+++ b/spec/lib/gitlab/x509/tag_spec.rb
@@ -0,0 +1,42 @@
+# frozen_string_literal: true
+require 'spec_helper'
+
+describe Gitlab::X509::Tag do
+ subject(:signature) { described_class.new(tag).signature }
+
+ describe '#signature' do
+ let(:repository) { Gitlab::Git::Repository.new('default', TEST_REPO_PATH, '', 'group/project') }
+ let(:project) { create(:project, :repository) }
+
+ describe 'signed tag' do
+ let(:tag) { project.repository.find_tag('v1.1.1') }
+ let(:certificate_attributes) do
+ {
+ subject_key_identifier: X509Helpers::User1.tag_certificate_subject_key_identifier,
+ subject: X509Helpers::User1.certificate_subject,
+ email: X509Helpers::User1.certificate_email,
+ serial_number: X509Helpers::User1.tag_certificate_serial
+ }
+ end
+
+ let(:issuer_attributes) do
+ {
+ subject_key_identifier: X509Helpers::User1.tag_issuer_subject_key_identifier,
+ subject: X509Helpers::User1.tag_certificate_issuer,
+ crl_url: X509Helpers::User1.tag_certificate_crl
+ }
+ end
+
+ it { expect(signature).not_to be_nil }
+ it { expect(signature.verification_status).to eq(:unverified) }
+ it { expect(signature.x509_certificate).to have_attributes(certificate_attributes) }
+ it { expect(signature.x509_certificate.x509_issuer).to have_attributes(issuer_attributes) }
+ end
+
+ context 'unsigned tag' do
+ let(:tag) { project.repository.find_tag('v1.0.0') }
+
+ it { expect(signature).to be_nil }
+ end
+ end
+end
diff --git a/spec/lib/gitlab_danger_spec.rb b/spec/lib/gitlab_danger_spec.rb
index f4620e54979..8115fbca5e0 100644
--- a/spec/lib/gitlab_danger_spec.rb
+++ b/spec/lib/gitlab_danger_spec.rb
@@ -9,7 +9,7 @@ describe GitlabDanger do
describe '.local_warning_message' do
it 'returns an informational message with rules that can run' do
- expect(described_class.local_warning_message).to eq('==> Only the following Danger rules can be run locally: changes_size, documentation, frozen_string, duplicate_yarn_dependencies, prettier, eslint, karma, database, commit_messages, telemetry')
+ expect(described_class.local_warning_message).to eq('==> Only the following Danger rules can be run locally: changes_size, documentation, frozen_string, duplicate_yarn_dependencies, prettier, eslint, karma, database, commit_messages, telemetry, utility_css')
end
end
diff --git a/spec/lib/google_api/auth_spec.rb b/spec/lib/google_api/auth_spec.rb
index 719e98c5fdf..fa4e6288681 100644
--- a/spec/lib/google_api/auth_spec.rb
+++ b/spec/lib/google_api/auth_spec.rb
@@ -40,5 +40,19 @@ describe GoogleApi::Auth do
expect(token).to eq('token')
expect(expires_at).to eq('expires_at')
end
+
+ it 'expects the client to receive default options' do
+ config = Gitlab::Auth::OAuth::Provider.config_for('google_oauth2')
+
+ expect(OAuth2::Client).to receive(:new).with(
+ config.app_id,
+ config.app_secret,
+ hash_including(
+ **config.args.client_options.deep_symbolize_keys
+ )
+ ).and_call_original
+
+ client.get_token('xxx')
+ end
end
end
diff --git a/spec/lib/grafana/validator_spec.rb b/spec/lib/grafana/validator_spec.rb
index 603e27fd0c0..a048a1f3470 100644
--- a/spec/lib/grafana/validator_spec.rb
+++ b/spec/lib/grafana/validator_spec.rb
@@ -3,8 +3,8 @@
require 'spec_helper'
describe Grafana::Validator do
- let(:grafana_dashboard) { JSON.parse(fixture_file('grafana/simplified_dashboard_response.json'), symbolize_names: true) }
- let(:datasource) { JSON.parse(fixture_file('grafana/datasource_response.json'), symbolize_names: true) }
+ let(:grafana_dashboard) { Gitlab::Json.parse(fixture_file('grafana/simplified_dashboard_response.json'), symbolize_names: true) }
+ let(:datasource) { Gitlab::Json.parse(fixture_file('grafana/datasource_response.json'), symbolize_names: true) }
let(:panel) { grafana_dashboard[:dashboard][:panels].first }
let(:query_params) do
diff --git a/spec/lib/omni_auth/strategies/jwt_spec.rb b/spec/lib/omni_auth/strategies/jwt_spec.rb
index f2b682850e3..302329cf198 100644
--- a/spec/lib/omni_auth/strategies/jwt_spec.rb
+++ b/spec/lib/omni_auth/strategies/jwt_spec.rb
@@ -35,7 +35,7 @@ describe OmniAuth::Strategies::Jwt do
end
end
- ECDSA_NAMED_CURVES = {
+ ecdsa_named_curves = {
'ES256' => 'prime256v1',
'ES384' => 'secp384r1',
'ES512' => 'secp521r1'
@@ -54,7 +54,7 @@ describe OmniAuth::Strategies::Jwt do
private_key_class.generate(2048)
.to_pem
elsif private_key_class == OpenSSL::PKey::EC
- private_key_class.new(ECDSA_NAMED_CURVES[algorithm])
+ private_key_class.new(ecdsa_named_curves[algorithm])
.tap { |key| key.generate_key! }
.to_pem
else
diff --git a/spec/lib/quality/helm_client_spec.rb b/spec/lib/quality/helm_client_spec.rb
deleted file mode 100644
index 8d199fe3531..00000000000
--- a/spec/lib/quality/helm_client_spec.rb
+++ /dev/null
@@ -1,132 +0,0 @@
-# frozen_string_literal: true
-
-require 'fast_spec_helper'
-
-RSpec.describe Quality::HelmClient do
- let(:tiller_namespace) { 'review-apps-ee' }
- let(:namespace) { tiller_namespace }
- let(:release_name) { 'my-release' }
- let(:raw_helm_list_page1) do
- <<~OUTPUT
- {"Next":"review-6709-group-t40qbv",
- "Releases":[
- {"Name":"review-qa-60-reor-1mugd1", "Revision":1,"Updated":"Thu Oct 4 17:52:31 2018","Status":"FAILED", "Chart":"gitlab-1.1.3","AppVersion":"master","Namespace":"#{namespace}"},
- {"Name":"review-7846-fix-s-261vd6","Revision":1,"Updated":"Thu Oct 4 17:33:29 2018","Status":"FAILED","Chart":"gitlab-1.1.3","AppVersion":"master","Namespace":"#{namespace}"},
- {"Name":"review-7867-snowp-lzo3iy","Revision":1,"Updated":"Thu Oct 4 17:22:14 2018","Status":"DEPLOYED","Chart":"gitlab-1.1.3","AppVersion":"master","Namespace":"#{namespace}"},
- {"Name":"review-rename-geo-o4a780","Revision":1,"Updated":"Thu Oct 4 17:14:57 2018","Status":"DEPLOYED","Chart":"gitlab-1.1.3","AppVersion":"master","Namespace":"#{namespace}"},
- {"Name":"review-5781-opera-0k93fx","Revision":1,"Updated":"Thu Oct 4 17:06:15 2018","Status":"FAILED","Chart":"gitlab-1.1.3","AppVersion":"master","Namespace":"#{namespace}"},
- {"Name":"review-6709-group-2pzeec","Revision":1,"Updated":"Thu Oct 4 16:36:59 2018","Status":"FAILED","Chart":"gitlab-1.1.3","AppVersion":"master","Namespace":"#{namespace}"},
- {"Name":"review-ce-to-ee-2-l554mn","Revision":1,"Updated":"Thu Oct 4 16:27:02 2018","Status":"FAILED","Chart":"gitlab-1.1.3","AppVersion":"master","Namespace":"#{namespace}"},
- {"Name":"review-epics-e2e-m690eb","Revision":1,"Updated":"Thu Oct 4 16:08:26 2018","Status":"DEPLOYED","Chart":"gitlab-1.1.3","AppVersion":"master","Namespace":"#{namespace}"},
- {"Name":"review-7126-admin-06fae2","Revision":1,"Updated":"Thu Oct 4 15:56:35 2018","Status":"FAILED","Chart":"gitlab-1.1.3","AppVersion":"master","Namespace":"#{namespace}"},
- {"Name":"review-6983-promo-xyou11","Revision":1,"Updated":"Thu Oct 4 15:15:34 2018","Status":"FAILED","Chart":"gitlab-1.1.3","AppVersion":"master","Namespace":"#{namespace}"}
- ]}
- OUTPUT
- end
- let(:raw_helm_list_page2) do
- <<~OUTPUT
- {"Releases":[
- {"Name":"review-6709-group-t40qbv","Revision":1,"Updated":"Thu Oct 4 17:52:31 2018","Status":"FAILED","Chart":"gitlab-1.1.3","AppVersion":"master","Namespace":"#{namespace}"}
- ]}
- OUTPUT
- end
-
- subject { described_class.new(tiller_namespace: tiller_namespace, namespace: namespace) }
-
- describe '#releases' do
- it 'raises an error if the Helm command fails' do
- expect(Gitlab::Popen).to receive(:popen_with_detail)
- .with([%(helm list --namespace "#{namespace}" --tiller-namespace "#{tiller_namespace}" --output json)])
- .and_return(Gitlab::Popen::Result.new([], '', '', double(success?: false)))
-
- expect { subject.releases.to_a }.to raise_error(described_class::CommandFailedError)
- end
-
- it 'calls helm list with default arguments' do
- expect(Gitlab::Popen).to receive(:popen_with_detail)
- .with([%(helm list --namespace "#{namespace}" --tiller-namespace "#{tiller_namespace}" --output json)])
- .and_return(Gitlab::Popen::Result.new([], '', '', double(success?: true)))
-
- subject.releases.to_a
- end
-
- it 'calls helm list with extra arguments' do
- expect(Gitlab::Popen).to receive(:popen_with_detail)
- .with([%(helm list --namespace "#{namespace}" --tiller-namespace "#{tiller_namespace}" --output json --deployed)])
- .and_return(Gitlab::Popen::Result.new([], '', '', double(success?: true)))
-
- subject.releases(args: ['--deployed']).to_a
- end
-
- it 'returns a list of Release objects' do
- expect(Gitlab::Popen).to receive(:popen_with_detail)
- .with([%(helm list --namespace "#{namespace}" --tiller-namespace "#{tiller_namespace}" --output json --deployed)])
- .and_return(Gitlab::Popen::Result.new([], raw_helm_list_page2, '', double(success?: true)))
-
- releases = subject.releases(args: ['--deployed']).to_a
-
- expect(releases.size).to eq(1)
- expect(releases[0]).to have_attributes(
- name: 'review-6709-group-t40qbv',
- revision: 1,
- last_update: Time.parse('Thu Oct 4 17:52:31 2018'),
- status: 'FAILED',
- chart: 'gitlab-1.1.3',
- app_version: 'master',
- namespace: namespace
- )
- end
-
- it 'automatically paginates releases' do
- expect(Gitlab::Popen).to receive(:popen_with_detail).ordered
- .with([%(helm list --namespace "#{namespace}" --tiller-namespace "#{tiller_namespace}" --output json)])
- .and_return(Gitlab::Popen::Result.new([], raw_helm_list_page1, '', double(success?: true)))
- expect(Gitlab::Popen).to receive(:popen_with_detail).ordered
- .with([%(helm list --namespace "#{namespace}" --tiller-namespace "#{tiller_namespace}" --output json --offset review-6709-group-t40qbv)])
- .and_return(Gitlab::Popen::Result.new([], raw_helm_list_page2, '', double(success?: true)))
-
- releases = subject.releases.to_a
-
- expect(releases.size).to eq(11)
- expect(releases.last.name).to eq('review-6709-group-t40qbv')
- end
- end
-
- describe '#delete' do
- it 'raises an error if the Helm command fails' do
- expect(Gitlab::Popen).to receive(:popen_with_detail)
- .with([%(helm delete --tiller-namespace "#{tiller_namespace}" --purge #{release_name})])
- .and_return(Gitlab::Popen::Result.new([], '', '', double(success?: false)))
-
- expect { subject.delete(release_name: release_name) }.to raise_error(described_class::CommandFailedError)
- end
-
- it 'calls helm delete with default arguments' do
- expect(Gitlab::Popen).to receive(:popen_with_detail)
- .with([%(helm delete --tiller-namespace "#{tiller_namespace}" --purge #{release_name})])
- .and_return(Gitlab::Popen::Result.new([], '', '', double(success?: true)))
-
- expect(subject.delete(release_name: release_name)).to eq('')
- end
-
- context 'with multiple release names' do
- let(:release_name) { %w[my-release my-release-2] }
-
- it 'raises an error if the Helm command fails' do
- expect(Gitlab::Popen).to receive(:popen_with_detail)
- .with([%(helm delete --tiller-namespace "#{tiller_namespace}" --purge #{release_name.join(' ')})])
- .and_return(Gitlab::Popen::Result.new([], '', '', double(success?: false)))
-
- expect { subject.delete(release_name: release_name) }.to raise_error(described_class::CommandFailedError)
- end
-
- it 'calls helm delete with multiple release names' do
- expect(Gitlab::Popen).to receive(:popen_with_detail)
- .with([%(helm delete --tiller-namespace "#{tiller_namespace}" --purge #{release_name.join(' ')})])
- .and_return(Gitlab::Popen::Result.new([], '', '', double(success?: true)))
-
- expect(subject.delete(release_name: release_name)).to eq('')
- end
- end
- end
-end
diff --git a/spec/lib/quality/test_level_spec.rb b/spec/lib/quality/test_level_spec.rb
index 6042ab24787..b784a92fa85 100644
--- a/spec/lib/quality/test_level_spec.rb
+++ b/spec/lib/quality/test_level_spec.rb
@@ -21,7 +21,7 @@ RSpec.describe Quality::TestLevel do
context 'when level is unit' do
it 'returns a pattern' do
expect(subject.pattern(:unit))
- .to eq("spec/{bin,config,db,dependencies,factories,finders,frontend,graphql,haml_lint,helpers,initializers,javascripts,lib,models,policies,presenters,rack_servers,replicators,routing,rubocop,serializers,services,sidekiq,support_specs,tasks,uploaders,validators,views,workers,elastic_integration}{,/**/}*_spec.rb")
+ .to eq("spec/{bin,channels,config,db,dependencies,factories,finders,frontend,graphql,haml_lint,helpers,initializers,javascripts,lib,models,policies,presenters,rack_servers,replicators,routing,rubocop,serializers,services,sidekiq,support_specs,tasks,uploaders,validators,views,workers,elastic_integration}{,/**/}*_spec.rb")
end
end
@@ -89,7 +89,7 @@ RSpec.describe Quality::TestLevel do
context 'when level is unit' do
it 'returns a regexp' do
expect(subject.regexp(:unit))
- .to eq(%r{spec/(bin|config|db|dependencies|factories|finders|frontend|graphql|haml_lint|helpers|initializers|javascripts|lib|models|policies|presenters|rack_servers|replicators|routing|rubocop|serializers|services|sidekiq|support_specs|tasks|uploaders|validators|views|workers|elastic_integration)})
+ .to eq(%r{spec/(bin|channels|config|db|dependencies|factories|finders|frontend|graphql|haml_lint|helpers|initializers|javascripts|lib|models|policies|presenters|rack_servers|replicators|routing|rubocop|serializers|services|sidekiq|support_specs|tasks|uploaders|validators|views|workers|elastic_integration)})
end
end
diff --git a/spec/lib/rspec_flaky/flaky_example_spec.rb b/spec/lib/rspec_flaky/flaky_example_spec.rb
index 47c88e053e1..d4a1d6c882a 100644
--- a/spec/lib/rspec_flaky/flaky_example_spec.rb
+++ b/spec/lib/rspec_flaky/flaky_example_spec.rb
@@ -77,7 +77,7 @@ describe RspecFlaky::FlakyExample, :aggregate_failures do
it 'updates the first_flaky_at' do
now = Time.now
- expected_first_flaky_at = flaky_example.first_flaky_at ? flaky_example.first_flaky_at : now
+ expected_first_flaky_at = flaky_example.first_flaky_at || now
Timecop.freeze(now) { flaky_example.update_flakiness! }
expect(flaky_example.first_flaky_at).to eq(expected_first_flaky_at)
diff --git a/spec/lib/rspec_flaky/report_spec.rb b/spec/lib/rspec_flaky/report_spec.rb
index 1f0eff83db0..37330f39e1c 100644
--- a/spec/lib/rspec_flaky/report_spec.rb
+++ b/spec/lib/rspec_flaky/report_spec.rb
@@ -31,7 +31,7 @@ describe RspecFlaky::Report, :aggregate_failures do
describe '.load' do
let!(:report_file) do
Tempfile.new(%w[rspec_flaky_report .json]).tap do |f|
- f.write(JSON.pretty_generate(suite_flaky_example_report))
+ f.write(Gitlab::Json.pretty_generate(suite_flaky_example_report))
f.rewind
end
end
@@ -48,7 +48,7 @@ describe RspecFlaky::Report, :aggregate_failures do
describe '.load_json' do
let(:report_json) do
- JSON.pretty_generate(suite_flaky_example_report)
+ Gitlab::Json.pretty_generate(suite_flaky_example_report)
end
it 'loads the report file' do
@@ -103,7 +103,7 @@ describe RspecFlaky::Report, :aggregate_failures do
expect(File.exist?(report_file_path)).to be(true)
expect(File.read(report_file_path))
- .to eq(JSON.pretty_generate(report.flaky_examples.to_h))
+ .to eq(Gitlab::Json.pretty_generate(report.flaky_examples.to_h))
end
end
end
diff --git a/spec/lib/sentry/client/event_spec.rb b/spec/lib/sentry/client/event_spec.rb
index c8604d72ada..58891895bfa 100644
--- a/spec/lib/sentry/client/event_spec.rb
+++ b/spec/lib/sentry/client/event_spec.rb
@@ -18,7 +18,7 @@ describe Sentry::Client do
describe '#issue_latest_event' do
let(:sample_response) do
Gitlab::Utils.deep_indifferent_access(
- JSON.parse(fixture_file('sentry/issue_latest_event_sample_response.json'))
+ Gitlab::Json.parse(fixture_file('sentry/issue_latest_event_sample_response.json'))
)
end
let(:issue_id) { '1234' }
diff --git a/spec/lib/sentry/client/issue_link_spec.rb b/spec/lib/sentry/client/issue_link_spec.rb
index 3434e93365e..293937f6100 100644
--- a/spec/lib/sentry/client/issue_link_spec.rb
+++ b/spec/lib/sentry/client/issue_link_spec.rb
@@ -16,7 +16,7 @@ describe Sentry::Client::IssueLink do
let(:sentry_issue_link_url) { "https://sentrytest.gitlab.com/api/0/groups/#{sentry_issue_id}/integrations/#{integration_id}/" }
let(:integration_id) { 44444 }
- let(:issue_link_sample_response) { JSON.parse(fixture_file('sentry/global_integration_link_sample_response.json')) }
+ let(:issue_link_sample_response) { Gitlab::Json.parse(fixture_file('sentry/global_integration_link_sample_response.json')) }
let(:sentry_api_response) { issue_link_sample_response }
let!(:sentry_api_request) { stub_sentry_request(sentry_issue_link_url, :put, body: sentry_api_response, status: 201) }
@@ -42,7 +42,7 @@ describe Sentry::Client::IssueLink do
let(:sentry_issue_link_url) { "https://sentrytest.gitlab.com/api/0/issues/#{sentry_issue_id}/plugins/gitlab/link/" }
let(:integration_id) { nil }
- let(:issue_link_sample_response) { JSON.parse(fixture_file('sentry/plugin_link_sample_response.json')) }
+ let(:issue_link_sample_response) { Gitlab::Json.parse(fixture_file('sentry/plugin_link_sample_response.json')) }
let!(:sentry_api_request) { stub_sentry_request(sentry_issue_link_url, :post, body: sentry_api_response) }
it_behaves_like 'calls sentry api'
diff --git a/spec/lib/sentry/client/issue_spec.rb b/spec/lib/sentry/client/issue_spec.rb
index 0f57d38d290..b683ad6d4a9 100644
--- a/spec/lib/sentry/client/issue_spec.rb
+++ b/spec/lib/sentry/client/issue_spec.rb
@@ -23,7 +23,7 @@ describe Sentry::Client::Issue do
let(:issues_sample_response) do
Gitlab::Utils.deep_indifferent_access(
- JSON.parse(fixture_file('sentry/issues_sample_response.json'))
+ Gitlab::Json.parse(fixture_file('sentry/issues_sample_response.json'))
)
end
@@ -201,7 +201,7 @@ describe Sentry::Client::Issue do
describe '#issue_details' do
let(:issue_sample_response) do
Gitlab::Utils.deep_indifferent_access(
- JSON.parse(fixture_file('sentry/issue_sample_response.json'))
+ Gitlab::Json.parse(fixture_file('sentry/issue_sample_response.json'))
)
end
diff --git a/spec/lib/sentry/client/projects_spec.rb b/spec/lib/sentry/client/projects_spec.rb
index 6183d4c5816..1b5bbb8f81a 100644
--- a/spec/lib/sentry/client/projects_spec.rb
+++ b/spec/lib/sentry/client/projects_spec.rb
@@ -10,7 +10,7 @@ describe Sentry::Client::Projects do
let(:client) { Sentry::Client.new(sentry_url, token) }
let(:projects_sample_response) do
Gitlab::Utils.deep_indifferent_access(
- JSON.parse(fixture_file('sentry/list_projects_sample_response.json'))
+ Gitlab::Json.parse(fixture_file('sentry/list_projects_sample_response.json'))
)
end
diff --git a/spec/lib/sentry/client/repo_spec.rb b/spec/lib/sentry/client/repo_spec.rb
index 7bc2811ef03..524dca8dcf6 100644
--- a/spec/lib/sentry/client/repo_spec.rb
+++ b/spec/lib/sentry/client/repo_spec.rb
@@ -8,7 +8,7 @@ describe Sentry::Client::Repo do
let(:sentry_url) { 'https://sentrytest.gitlab.com/api/0/projects/sentry-org/sentry-project' }
let(:token) { 'test-token' }
let(:client) { Sentry::Client.new(sentry_url, token) }
- let(:repos_sample_response) { JSON.parse(fixture_file('sentry/repos_sample_response.json')) }
+ let(:repos_sample_response) { Gitlab::Json.parse(fixture_file('sentry/repos_sample_response.json')) }
describe '#repos' do
let(:organization_slug) { 'gitlab' }
diff --git a/spec/lib/serializers/json_spec.rb b/spec/lib/serializers/json_spec.rb
index a8d82d70e89..dfe85d3f362 100644
--- a/spec/lib/serializers/json_spec.rb
+++ b/spec/lib/serializers/json_spec.rb
@@ -15,7 +15,7 @@ describe Serializers::JSON do
describe '.load' do
let(:data_string) { '{"key":"value","variables":[{"key":"VAR1","value":"VALUE1"}]}' }
- let(:data_hash) { JSON.parse(data_string) }
+ let(:data_hash) { Gitlab::Json.parse(data_string) }
context 'when loading a hash' do
subject { described_class.load(data_hash) }
diff --git a/spec/lib/system_check/app/hashed_storage_all_projects_check_spec.rb b/spec/lib/system_check/app/hashed_storage_all_projects_check_spec.rb
new file mode 100644
index 00000000000..e5e7f6a4450
--- /dev/null
+++ b/spec/lib/system_check/app/hashed_storage_all_projects_check_spec.rb
@@ -0,0 +1,24 @@
+# frozen_string_literal: true
+
+require 'spec_helper'
+require 'rake_helper'
+
+describe SystemCheck::App::HashedStorageAllProjectsCheck do
+ before do
+ silence_output
+ end
+
+ describe '#check?' do
+ it 'fails when at least one project is in legacy storage' do
+ create(:project, :legacy_storage)
+
+ expect(subject.check?).to be_falsey
+ end
+
+ it 'succeeds when all projects are in hashed storage' do
+ create(:project)
+
+ expect(subject.check?).to be_truthy
+ end
+ end
+end
diff --git a/spec/lib/system_check/app/hashed_storage_enabled_check_spec.rb b/spec/lib/system_check/app/hashed_storage_enabled_check_spec.rb
new file mode 100644
index 00000000000..d5a0014b791
--- /dev/null
+++ b/spec/lib/system_check/app/hashed_storage_enabled_check_spec.rb
@@ -0,0 +1,24 @@
+# frozen_string_literal: true
+
+require 'spec_helper'
+require 'rake_helper'
+
+describe SystemCheck::App::HashedStorageEnabledCheck do
+ before do
+ silence_output
+ end
+
+ describe '#check?' do
+ it 'fails when hashed storage is disabled' do
+ stub_application_setting(hashed_storage_enabled: false)
+
+ expect(subject.check?).to be_falsey
+ end
+
+ it 'succeeds when hashed storage is enabled' do
+ stub_application_setting(hashed_storage_enabled: true)
+
+ expect(subject.check?).to be_truthy
+ end
+ end
+end
diff --git a/spec/lib/system_check/simple_executor_spec.rb b/spec/lib/system_check/simple_executor_spec.rb
index 94094343ec6..58f3a7df197 100644
--- a/spec/lib/system_check/simple_executor_spec.rb
+++ b/spec/lib/system_check/simple_executor_spec.rb
@@ -4,99 +4,109 @@ require 'spec_helper'
require 'rake_helper'
describe SystemCheck::SimpleExecutor do
- class SimpleCheck < SystemCheck::BaseCheck
- set_name 'my simple check'
-
- def check?
- true
+ before do
+ stub_const('SimpleCheck', Class.new(SystemCheck::BaseCheck))
+ stub_const('OtherCheck', Class.new(SystemCheck::BaseCheck))
+ stub_const('SkipCheck', Class.new(SystemCheck::BaseCheck))
+ stub_const('DynamicSkipCheck', Class.new(SystemCheck::BaseCheck))
+ stub_const('MultiCheck', Class.new(SystemCheck::BaseCheck))
+ stub_const('SkipMultiCheck', Class.new(SystemCheck::BaseCheck))
+ stub_const('RepairCheck', Class.new(SystemCheck::BaseCheck))
+ stub_const('BugousCheck', Class.new(SystemCheck::BaseCheck))
+
+ SimpleCheck.class_eval do
+ set_name 'my simple check'
+
+ def check?
+ true
+ end
end
- end
- class OtherCheck < SystemCheck::BaseCheck
- set_name 'other check'
+ OtherCheck.class_eval do
+ set_name 'other check'
- def check?
- false
- end
+ def check?
+ false
+ end
- def show_error
- $stdout.puts 'this is an error text'
+ def show_error
+ $stdout.puts 'this is an error text'
+ end
end
- end
- class SkipCheck < SystemCheck::BaseCheck
- set_name 'skip check'
- set_skip_reason 'this is a skip reason'
+ SkipCheck.class_eval do
+ set_name 'skip check'
+ set_skip_reason 'this is a skip reason'
- def skip?
- true
- end
+ def skip?
+ true
+ end
- def check?
- raise 'should not execute this'
+ def check?
+ raise 'should not execute this'
+ end
end
- end
- class DynamicSkipCheck < SystemCheck::BaseCheck
- set_name 'dynamic skip check'
- set_skip_reason 'this is a skip reason'
+ DynamicSkipCheck.class_eval do
+ set_name 'dynamic skip check'
+ set_skip_reason 'this is a skip reason'
- def skip?
- self.skip_reason = 'this is a dynamic skip reason'
- true
- end
+ def skip?
+ self.skip_reason = 'this is a dynamic skip reason'
+ true
+ end
- def check?
- raise 'should not execute this'
+ def check?
+ raise 'should not execute this'
+ end
end
- end
- class MultiCheck < SystemCheck::BaseCheck
- set_name 'multi check'
+ MultiCheck.class_eval do
+ set_name 'multi check'
- def multi_check
- $stdout.puts 'this is a multi output check'
- end
+ def multi_check
+ $stdout.puts 'this is a multi output check'
+ end
- def check?
- raise 'should not execute this'
+ def check?
+ raise 'should not execute this'
+ end
end
- end
- class SkipMultiCheck < SystemCheck::BaseCheck
- set_name 'skip multi check'
+ SkipMultiCheck.class_eval do
+ set_name 'skip multi check'
- def skip?
- true
- end
+ def skip?
+ true
+ end
- def multi_check
- raise 'should not execute this'
+ def multi_check
+ raise 'should not execute this'
+ end
end
- end
- class RepairCheck < SystemCheck::BaseCheck
- set_name 'repair check'
+ RepairCheck.class_eval do
+ set_name 'repair check'
- def check?
- false
- end
+ def check?
+ false
+ end
- def repair!
- true
- end
+ def repair!
+ true
+ end
- def show_error
- $stdout.puts 'this is an error message'
+ def show_error
+ $stdout.puts 'this is an error message'
+ end
end
- end
- class BugousCheck < SystemCheck::BaseCheck
- CustomError = Class.new(StandardError)
- set_name 'my bugous check'
+ BugousCheck.class_eval do
+ set_name 'my bugous check'
- def check?
- raise CustomError, 'omg'
+ def check?
+ raise StandardError, 'omg'
+ end
end
end
diff --git a/spec/lib/system_check_spec.rb b/spec/lib/system_check_spec.rb
index f3ed6ca31c9..da1916455ba 100644
--- a/spec/lib/system_check_spec.rb
+++ b/spec/lib/system_check_spec.rb
@@ -4,19 +4,22 @@ require 'spec_helper'
require 'rake_helper'
describe SystemCheck do
- class SimpleCheck < SystemCheck::BaseCheck
- def check?
- true
+ before do
+ stub_const('SimpleCheck', Class.new(SystemCheck::BaseCheck))
+ stub_const('OtherCheck', Class.new(SystemCheck::BaseCheck))
+
+ SimpleCheck.class_eval do
+ def check?
+ true
+ end
end
- end
- class OtherCheck < SystemCheck::BaseCheck
- def check?
- false
+ OtherCheck.class_eval do
+ def check?
+ false
+ end
end
- end
- before do
silence_output
end
diff --git a/spec/mailers/emails/groups_spec.rb b/spec/mailers/emails/groups_spec.rb
new file mode 100644
index 00000000000..b4746e120e0
--- /dev/null
+++ b/spec/mailers/emails/groups_spec.rb
@@ -0,0 +1,41 @@
+# frozen_string_literal: true
+
+require 'spec_helper'
+require 'email_spec'
+
+describe Emails::Groups do
+ include EmailSpec::Matchers
+
+ let(:group) { create(:group) }
+ let(:user) { create(:user) }
+
+ before do
+ group.add_owner(user)
+ end
+
+ describe '#group_was_exported_email' do
+ subject { Notify.group_was_exported_email(user, group) }
+
+ it 'sends success email' do
+ expect(subject).to have_subject "#{group.name} | Group was exported"
+ expect(subject).to have_body_text 'The download link will expire in 24 hours.'
+ expect(subject).to have_body_text "groups/#{group.path}/-/download_export"
+ end
+ end
+
+ describe '#group_was_not_exported_email' do
+ let(:shared) { Gitlab::ImportExport::Shared.new(group) }
+ let(:error) { Gitlab::ImportExport::Error.new('Error!') }
+
+ before do
+ shared.error(error)
+ end
+
+ subject { Notify.group_was_not_exported_email(user, group, shared.errors) }
+
+ it 'sends failure email' do
+ expect(subject).to have_subject "#{group.name} | Group export error"
+ expect(subject).to have_body_text "Group #{group.name} couldn't be exported."
+ end
+ end
+end
diff --git a/spec/mailers/emails/profile_spec.rb b/spec/mailers/emails/profile_spec.rb
index 58c04fb4834..f84bf43b9c4 100644
--- a/spec/mailers/emails/profile_spec.rb
+++ b/spec/mailers/emails/profile_spec.rb
@@ -156,4 +156,44 @@ describe Emails::Profile do
it { expect { Notify.access_token_about_to_expire_email('foo') }.not_to raise_error }
end
end
+
+ describe 'user unknown sign in email' do
+ let_it_be(:user) { create(:user) }
+ let_it_be(:ip) { '169.0.0.1' }
+
+ subject { Notify.unknown_sign_in_email(user, ip) }
+
+ it_behaves_like 'an email sent from GitLab'
+ it_behaves_like 'it should not have Gmail Actions links'
+ it_behaves_like 'a user cannot unsubscribe through footer link'
+
+ it 'is sent to the user' do
+ expect(subject).to deliver_to user.email
+ end
+
+ it 'has the correct subject' do
+ expect(subject).to have_subject /^Unknown sign-in from new location$/
+ end
+
+ it 'mentions the unknown sign-in IP' do
+ expect(subject).to have_body_text /A sign-in to your account has been made from the following IP address: #{ip}./
+ end
+
+ it 'includes a link to the change password page' do
+ expect(subject).to have_body_text /#{edit_profile_password_path}/
+ end
+
+ it 'mentions two factor authentication when two factor is not enabled' do
+ expect(subject).to have_body_text /two-factor authentication/
+ end
+
+ context 'when two factor authentication is enabled' do
+ it 'does not mention two factor authentication' do
+ two_factor_user = create(:user, :two_factor)
+
+ expect( Notify.unknown_sign_in_email(two_factor_user, ip) )
+ .not_to have_body_text /two-factor authentication/
+ end
+ end
+ end
end
diff --git a/spec/mailers/notify_spec.rb b/spec/mailers/notify_spec.rb
index d21efe2e1fe..3c66902bb2e 100644
--- a/spec/mailers/notify_spec.rb
+++ b/spec/mailers/notify_spec.rb
@@ -70,6 +70,7 @@ describe Notify do
it_behaves_like 'an email starting a new thread with reply-by-email enabled' do
let(:model) { issue }
end
+
it_behaves_like 'it should show Gmail Actions View Issue link'
it_behaves_like 'an unsubscribeable thread'
it_behaves_like 'appearance header and footer enabled'
@@ -116,6 +117,7 @@ describe Notify do
it_behaves_like 'an answer to an existing thread with reply-by-email enabled' do
let(:model) { issue }
end
+
it_behaves_like 'it should show Gmail Actions View Issue link'
it_behaves_like 'an unsubscribeable thread'
it_behaves_like 'appearance header and footer enabled'
@@ -155,6 +157,7 @@ describe Notify do
it_behaves_like 'an answer to an existing thread with reply-by-email enabled' do
let(:model) { issue }
end
+
it_behaves_like 'it should show Gmail Actions View Issue link'
it_behaves_like 'a user cannot unsubscribe through footer link'
it_behaves_like 'an email with a labels subscriptions link in its footer'
@@ -200,6 +203,7 @@ describe Notify do
it_behaves_like 'an answer to an existing thread with reply-by-email enabled' do
let(:model) { issue }
end
+
it_behaves_like 'it should show Gmail Actions View Issue link'
it_behaves_like 'an unsubscribeable thread'
it_behaves_like 'appearance header and footer enabled'
@@ -214,6 +218,7 @@ describe Notify do
it_behaves_like 'an answer to an existing thread with reply-by-email enabled' do
let(:model) { issue }
end
+
it_behaves_like 'it should show Gmail Actions View Issue link'
it_behaves_like 'an unsubscribeable thread'
it_behaves_like 'appearance header and footer enabled'
@@ -248,6 +253,7 @@ describe Notify do
it_behaves_like 'an answer to an existing thread with reply-by-email enabled' do
let(:model) { issue }
end
+
it_behaves_like 'it should show Gmail Actions View Issue link'
it_behaves_like 'an unsubscribeable thread'
@@ -300,6 +306,7 @@ describe Notify do
it_behaves_like 'an email starting a new thread with reply-by-email enabled' do
let(:model) { merge_request }
end
+
it_behaves_like 'it should show Gmail Actions View Merge request link'
it_behaves_like 'an unsubscribeable thread'
it_behaves_like 'appearance header and footer enabled'
@@ -344,6 +351,7 @@ describe Notify do
it_behaves_like 'an answer to an existing thread with reply-by-email enabled' do
let(:model) { merge_request }
end
+
it_behaves_like 'it should show Gmail Actions View Merge request link'
it_behaves_like "an unsubscribeable thread"
it_behaves_like 'appearance header and footer enabled'
@@ -409,6 +417,7 @@ describe Notify do
it_behaves_like 'an answer to an existing thread with reply-by-email enabled' do
let(:model) { merge_request }
end
+
it_behaves_like 'it should show Gmail Actions View Merge request link'
it_behaves_like 'a user cannot unsubscribe through footer link'
it_behaves_like 'an email with a labels subscriptions link in its footer'
@@ -436,6 +445,7 @@ describe Notify do
it_behaves_like 'an answer to an existing thread with reply-by-email enabled' do
let(:model) { merge_request }
end
+
it_behaves_like 'it should show Gmail Actions View Merge request link'
it_behaves_like 'an unsubscribeable thread'
it_behaves_like 'appearance header and footer enabled'
@@ -466,6 +476,7 @@ describe Notify do
it_behaves_like 'an answer to an existing thread with reply-by-email enabled' do
let(:model) { merge_request }
end
+
it_behaves_like 'it should show Gmail Actions View Merge request link'
it_behaves_like 'an unsubscribeable thread'
it_behaves_like 'appearance header and footer enabled'
@@ -502,6 +513,7 @@ describe Notify do
it_behaves_like 'an answer to an existing thread with reply-by-email enabled' do
let(:model) { merge_request }
end
+
it_behaves_like 'it should show Gmail Actions View Merge request link'
it_behaves_like 'an unsubscribeable thread'
it_behaves_like 'appearance header and footer enabled'
@@ -533,6 +545,7 @@ describe Notify do
it_behaves_like 'an answer to an existing thread with reply-by-email enabled' do
let(:model) { merge_request }
end
+
it_behaves_like 'it should show Gmail Actions View Merge request link'
it_behaves_like 'an unsubscribeable thread'
it_behaves_like 'appearance header and footer enabled'
@@ -694,6 +707,7 @@ describe Notify do
it_behaves_like 'an answer to an existing thread with reply-by-email enabled' do
let(:model) { project_snippet }
end
+
it_behaves_like 'a user cannot unsubscribe through footer link'
it 'has the correct subject' do
@@ -712,6 +726,29 @@ describe Notify do
end
end
+ describe 'for design notes' do
+ let_it_be(:design) { create(:design, :with_file) }
+ let_it_be(:recipient) { create(:user) }
+ let_it_be(:note) do
+ create(:diff_note_on_design,
+ noteable: design,
+ note: "Hello #{recipient.to_reference}")
+ end
+
+ let(:header_name) { 'X-Gitlab-DesignManagement-Design-ID' }
+ let(:refer_to_design) do
+ have_attributes(subject: a_string_including(design.filename))
+ end
+
+ subject { described_class.note_design_email(recipient.id, note.id) }
+
+ it { is_expected.to have_header(header_name, design.id.to_s) }
+
+ it { is_expected.to have_body_text(design.filename) }
+
+ it { is_expected.to refer_to_design }
+ end
+
describe 'project was moved' do
let(:recipient) { user }
@@ -913,6 +950,7 @@ describe Notify do
it_behaves_like 'an answer to an existing thread with reply-by-email enabled' do
let(:model) { commit }
end
+
it_behaves_like 'it should show Gmail Actions View Commit link'
it_behaves_like 'a user cannot unsubscribe through footer link'
it_behaves_like 'appearance header and footer enabled'
@@ -939,6 +977,7 @@ describe Notify do
it_behaves_like 'an answer to an existing thread with reply-by-email enabled' do
let(:model) { merge_request }
end
+
it_behaves_like 'it should show Gmail Actions View Merge request link'
it_behaves_like 'an unsubscribeable thread'
it_behaves_like 'appearance header and footer enabled'
@@ -965,6 +1004,7 @@ describe Notify do
it_behaves_like 'an answer to an existing thread with reply-by-email enabled' do
let(:model) { issue }
end
+
it_behaves_like 'it should show Gmail Actions View Issue link'
it_behaves_like 'an unsubscribeable thread'
it_behaves_like 'appearance header and footer enabled'
@@ -1037,6 +1077,7 @@ describe Notify do
it_behaves_like 'an answer to an existing thread with reply-by-email enabled' do
let(:model) { commit }
end
+
it_behaves_like 'it should show Gmail Actions View Commit link'
it_behaves_like 'a user cannot unsubscribe through footer link'
it_behaves_like 'appearance header and footer enabled'
@@ -1069,6 +1110,7 @@ describe Notify do
it_behaves_like 'an answer to an existing thread with reply-by-email enabled' do
let(:model) { merge_request }
end
+
it_behaves_like 'it should show Gmail Actions View Merge request link'
it_behaves_like 'an unsubscribeable thread'
it_behaves_like 'appearance header and footer enabled'
@@ -1101,6 +1143,7 @@ describe Notify do
it_behaves_like 'an answer to an existing thread with reply-by-email enabled' do
let(:model) { issue }
end
+
it_behaves_like 'it should show Gmail Actions View Issue link'
it_behaves_like 'an unsubscribeable thread'
it_behaves_like 'appearance header and footer enabled'
diff --git a/spec/migrations/20200511145545_change_variable_interpolation_format_in_common_metrics_spec.rb b/spec/migrations/20200511145545_change_variable_interpolation_format_in_common_metrics_spec.rb
new file mode 100644
index 00000000000..f9e8a7ee6e9
--- /dev/null
+++ b/spec/migrations/20200511145545_change_variable_interpolation_format_in_common_metrics_spec.rb
@@ -0,0 +1,34 @@
+# frozen_string_literal: true
+
+require 'spec_helper'
+require Rails.root.join('db', 'post_migrate', '20200511145545_change_variable_interpolation_format_in_common_metrics')
+
+describe ChangeVariableInterpolationFormatInCommonMetrics, :migration do
+ let(:prometheus_metrics) { table(:prometheus_metrics) }
+
+ let!(:common_metric) do
+ prometheus_metrics.create!(
+ identifier: 'system_metrics_kubernetes_container_memory_total',
+ query: 'avg(sum(container_memory_usage_bytes{container_name!="POD",' \
+ 'pod_name=~"^%{ci_environment_slug}-(.*)",namespace="%{kube_namespace}"})' \
+ ' by (job)) without (job) /1024/1024/1024',
+ project_id: nil,
+ title: 'Memory Usage (Total)',
+ y_label: 'Total Memory Used (GB)',
+ unit: 'GB',
+ legend: 'Total (GB)',
+ group: -5,
+ common: true
+ )
+ end
+
+ it 'updates query to use {{}}' do
+ expected_query = 'avg(sum(container_memory_usage_bytes{container_name!="POD",' \
+ 'pod_name=~"^{{ci_environment_slug}}-(.*)",namespace="{{kube_namespace}}"})' \
+ ' by (job)) without (job) /1024/1024/1024'
+
+ migrate!
+
+ expect(common_metric.reload.query).to eq(expected_query)
+ end
+end
diff --git a/spec/migrations/backfill_snippet_repositories_spec.rb b/spec/migrations/backfill_snippet_repositories_spec.rb
new file mode 100644
index 00000000000..e87bf7376dd
--- /dev/null
+++ b/spec/migrations/backfill_snippet_repositories_spec.rb
@@ -0,0 +1,44 @@
+# frozen_string_literal: true
+
+require 'spec_helper'
+require Rails.root.join('db', 'post_migrate', '20200420094444_backfill_snippet_repositories.rb')
+
+describe BackfillSnippetRepositories do
+ let(:users) { table(:users) }
+ let(:snippets) { table(:snippets) }
+ let(:user) { users.create(id: 1, email: 'user@example.com', projects_limit: 10, username: 'test', name: 'Test', state: 'active') }
+
+ def create_snippet(id)
+ params = {
+ id: id,
+ type: 'PersonalSnippet',
+ author_id: user.id,
+ file_name: 'foo',
+ content: 'bar'
+ }
+
+ snippets.create!(params)
+ end
+
+ it 'correctly schedules background migrations' do
+ create_snippet(1)
+ create_snippet(2)
+ create_snippet(3)
+
+ stub_const("#{described_class.name}::BATCH_SIZE", 2)
+
+ Sidekiq::Testing.fake! do
+ Timecop.freeze do
+ migrate!
+
+ expect(described_class::MIGRATION)
+ .to be_scheduled_delayed_migration(3.minutes, 1, 2)
+
+ expect(described_class::MIGRATION)
+ .to be_scheduled_delayed_migration(6.minutes, 3, 3)
+
+ expect(BackgroundMigrationWorker.jobs.size).to eq(2)
+ end
+ end
+ end
+end
diff --git a/spec/migrations/cleanup_optimistic_locking_nulls_pt2_fixed_spec.rb b/spec/migrations/cleanup_optimistic_locking_nulls_pt2_fixed_spec.rb
new file mode 100644
index 00000000000..2e5e450afc7
--- /dev/null
+++ b/spec/migrations/cleanup_optimistic_locking_nulls_pt2_fixed_spec.rb
@@ -0,0 +1,45 @@
+# frozen_string_literal: true
+
+require 'spec_helper'
+require Rails.root.join('db', 'post_migrate', '20200427064130_cleanup_optimistic_locking_nulls_pt2_fixed.rb')
+
+describe CleanupOptimisticLockingNullsPt2Fixed, :migration do
+ test_tables = %w(ci_stages ci_builds ci_pipelines).freeze
+ test_tables.each do |table|
+ let(table.to_sym) { table(table.to_sym) }
+ end
+ let(:tables) { test_tables.map { |t| method(t.to_sym).call } }
+
+ before do
+ # Create necessary rows
+ ci_stages.create!
+ ci_builds.create!
+ ci_pipelines.create!
+
+ # Nullify `lock_version` column for all rows
+ # Needs to be done with a SQL fragment, otherwise Rails will coerce it to 0
+ tables.each do |table|
+ table.update_all('lock_version = NULL')
+ end
+ end
+
+ it 'correctly migrates nullified lock_version column', :sidekiq_might_not_need_inline do
+ tables.each do |table|
+ expect(table.where(lock_version: nil).count).to eq(1)
+ end
+
+ tables.each do |table|
+ expect(table.where(lock_version: 0).count).to eq(0)
+ end
+
+ migrate!
+
+ tables.each do |table|
+ expect(table.where(lock_version: nil).count).to eq(0)
+ end
+
+ tables.each do |table|
+ expect(table.where(lock_version: 0).count).to eq(1)
+ end
+ end
+end
diff --git a/spec/migrations/cleanup_optimistic_locking_nulls_spec.rb b/spec/migrations/cleanup_optimistic_locking_nulls_spec.rb
index d32a374b914..6e541c903ff 100644
--- a/spec/migrations/cleanup_optimistic_locking_nulls_spec.rb
+++ b/spec/migrations/cleanup_optimistic_locking_nulls_spec.rb
@@ -4,11 +4,10 @@ require 'spec_helper'
require Rails.root.join('db', 'post_migrate', '20200128210353_cleanup_optimistic_locking_nulls')
describe CleanupOptimisticLockingNulls do
- TABLES = %w(epics merge_requests issues).freeze
- TABLES.each do |table|
- let(table.to_sym) { table(table.to_sym) }
- end
- let(:tables) { TABLES.map { |t| method(t.to_sym).call } }
+ let(:epics) { table(:epics) }
+ let(:merge_requests) { table(:merge_requests) }
+ let(:issues) { table(:issues) }
+ let(:tables) { [epics, merge_requests, issues] }
let(:namespaces) { table(:namespaces) }
let(:projects) { table(:projects) }
diff --git a/spec/migrations/cleanup_projects_with_missing_namespace_spec.rb b/spec/migrations/cleanup_projects_with_missing_namespace_spec.rb
new file mode 100644
index 00000000000..06b6d5e3b46
--- /dev/null
+++ b/spec/migrations/cleanup_projects_with_missing_namespace_spec.rb
@@ -0,0 +1,134 @@
+# frozen_string_literal: true
+
+require 'spec_helper'
+
+require Rails.root.join('db', 'post_migrate', '20200511080113_add_projects_foreign_key_to_namespaces.rb')
+require Rails.root.join('db', 'post_migrate', '20200511083541_cleanup_projects_with_missing_namespace.rb')
+
+LOST_AND_FOUND_GROUP = 'lost-and-found'
+USER_TYPE_GHOST = 5
+ACCESS_LEVEL_OWNER = 50
+
+# In order to test the CleanupProjectsWithMissingNamespace migration, we need
+# to first create an orphaned project (one with an invalid namespace_id)
+# and then run the migration to check that the project was properly cleaned up
+#
+# The problem is that the CleanupProjectsWithMissingNamespace migration comes
+# after the FK has been added with a previous migration (AddProjectsForeignKeyToNamespaces)
+# That means that while testing the current class we can not insert projects with an
+# invalid namespace_id as the existing FK is correctly blocking us from doing so
+#
+# The approach that solves that problem is to:
+# - Set the schema of this test to the one prior to AddProjectsForeignKeyToNamespaces
+# - We could hardcode it to `20200508091106` (which currently is the previous
+# migration before adding the FK) but that would mean that this test depends
+# on migration 20200508091106 not being reverted or deleted
+# - So, we use SchemaVersionFinder that finds the previous migration and returns
+# its schema, which we then use in the describe
+#
+# That means that we lock the schema version to the one returned by
+# SchemaVersionFinder.previous_migration and only test the cleanup migration
+# *without* the migration that adds the Foreign Key ever running
+# That's acceptable as the cleanup script should not be affected in any way
+# by the migration that adds the Foreign Key
+class SchemaVersionFinder
+ def self.migrations_paths
+ ActiveRecord::Migrator.migrations_paths
+ end
+
+ def self.migration_context
+ ActiveRecord::MigrationContext.new(migrations_paths, ActiveRecord::SchemaMigration)
+ end
+
+ def self.migrations
+ migration_context.migrations
+ end
+
+ def self.previous_migration
+ migrations.each_cons(2) do |previous, migration|
+ break previous.version if migration.name == AddProjectsForeignKeyToNamespaces.name
+ end
+ end
+end
+
+describe CleanupProjectsWithMissingNamespace, :migration, schema: SchemaVersionFinder.previous_migration do
+ let(:projects) { table(:projects) }
+ let(:namespaces) { table(:namespaces) }
+ let(:users) { table(:users) }
+
+ before do
+ namespace = namespaces.create!(name: 'existing_namespace', path: 'existing_namespace')
+
+ projects.create!(
+ name: 'project_with_existing_namespace',
+ path: 'project_with_existing_namespace',
+ visibility_level: 20,
+ archived: false,
+ namespace_id: namespace.id
+ )
+
+ projects.create!(
+ name: 'project_with_non_existing_namespace',
+ path: 'project_with_non_existing_namespace',
+ visibility_level: 20,
+ archived: false,
+ namespace_id: non_existing_record_id
+ )
+ end
+
+ it 'creates the ghost user' do
+ expect(users.where(user_type: USER_TYPE_GHOST).count).to eq(0)
+
+ disable_migrations_output { migrate! }
+
+ expect(users.where(user_type: USER_TYPE_GHOST).count).to eq(1)
+ end
+
+ it 'creates the lost-and-found group, owned by the ghost user' do
+ expect(
+ Group.where(Group.arel_table[:name].matches("#{LOST_AND_FOUND_GROUP}%")).count
+ ).to eq(0)
+
+ disable_migrations_output { migrate! }
+
+ ghost_user = users.find_by(user_type: USER_TYPE_GHOST)
+ expect(
+ Group
+ .joins('INNER JOIN members ON namespaces.id = members.source_id')
+ .where('namespaces.type = ?', 'Group')
+ .where('members.type = ?', 'GroupMember')
+ .where('members.source_type = ?', 'Namespace')
+ .where('members.user_id = ?', ghost_user.id)
+ .where('members.requested_at IS NULL')
+ .where('members.access_level = ?', ACCESS_LEVEL_OWNER)
+ .where(Group.arel_table[:name].matches("#{LOST_AND_FOUND_GROUP}%"))
+ .count
+ ).to eq(1)
+ end
+
+ it 'moves the orphaned project to the lost-and-found group' do
+ orphaned_project = projects.find_by(name: 'project_with_non_existing_namespace')
+ expect(orphaned_project.visibility_level).to eq(20)
+ expect(orphaned_project.archived).to eq(false)
+ expect(orphaned_project.namespace_id).to eq(non_existing_record_id)
+
+ disable_migrations_output { migrate! }
+
+ lost_and_found_group = Group.find_by(Group.arel_table[:name].matches("#{LOST_AND_FOUND_GROUP}%"))
+ orphaned_project = projects.find_by(id: orphaned_project.id)
+
+ expect(orphaned_project.visibility_level).to eq(0)
+ expect(orphaned_project.namespace_id).to eq(lost_and_found_group.id)
+ expect(orphaned_project.name).to eq("project_with_non_existing_namespace_#{orphaned_project.id}")
+ expect(orphaned_project.path).to eq("project_with_non_existing_namespace_#{orphaned_project.id}")
+ expect(orphaned_project.archived).to eq(true)
+
+ valid_project = projects.find_by(name: 'project_with_existing_namespace')
+ existing_namespace = namespaces.find_by(name: 'existing_namespace')
+
+ expect(valid_project.visibility_level).to eq(20)
+ expect(valid_project.namespace_id).to eq(existing_namespace.id)
+ expect(valid_project.path).to eq('project_with_existing_namespace')
+ expect(valid_project.archived).to eq(false)
+ end
+end
diff --git a/spec/migrations/encrypt_plaintext_attributes_on_application_settings_spec.rb b/spec/migrations/encrypt_plaintext_attributes_on_application_settings_spec.rb
index 87a72ed0cf5..fda810d1da9 100644
--- a/spec/migrations/encrypt_plaintext_attributes_on_application_settings_spec.rb
+++ b/spec/migrations/encrypt_plaintext_attributes_on_application_settings_spec.rb
@@ -8,7 +8,7 @@ describe EncryptPlaintextAttributesOnApplicationSettings do
let(:application_settings) { table(:application_settings) }
let(:plaintext) { 'secret-token' }
- PLAINTEXT_ATTRIBUTES = %w[
+ plaintext_attributes = %w[
akismet_api_key
elasticsearch_aws_secret_access_key
recaptcha_private_key
@@ -21,7 +21,7 @@ describe EncryptPlaintextAttributesOnApplicationSettings do
it 'encrypts token and saves it' do
application_setting = application_settings.create
application_setting.update_columns(
- PLAINTEXT_ATTRIBUTES.each_with_object({}) do |plaintext_attribute, attributes|
+ plaintext_attributes.each_with_object({}) do |plaintext_attribute, attributes|
attributes[plaintext_attribute] = plaintext
end
)
@@ -29,7 +29,7 @@ describe EncryptPlaintextAttributesOnApplicationSettings do
migration.up
application_setting.reload
- PLAINTEXT_ATTRIBUTES.each do |plaintext_attribute|
+ plaintext_attributes.each do |plaintext_attribute|
expect(application_setting[plaintext_attribute]).not_to be_nil
expect(application_setting["encrypted_#{plaintext_attribute}"]).not_to be_nil
expect(application_setting["encrypted_#{plaintext_attribute}_iv"]).not_to be_nil
@@ -40,7 +40,7 @@ describe EncryptPlaintextAttributesOnApplicationSettings do
describe '#down' do
it 'decrypts encrypted token and saves it' do
application_setting = application_settings.create(
- PLAINTEXT_ATTRIBUTES.each_with_object({}) do |plaintext_attribute, attributes|
+ plaintext_attributes.each_with_object({}) do |plaintext_attribute, attributes|
attributes[plaintext_attribute] = plaintext
end
)
@@ -48,7 +48,7 @@ describe EncryptPlaintextAttributesOnApplicationSettings do
migration.down
application_setting.reload
- PLAINTEXT_ATTRIBUTES.each do |plaintext_attribute|
+ plaintext_attributes.each do |plaintext_attribute|
expect(application_setting[plaintext_attribute]).to eq(plaintext)
expect(application_setting["encrypted_#{plaintext_attribute}"]).to be_nil
expect(application_setting["encrypted_#{plaintext_attribute}_iv"]).to be_nil
diff --git a/spec/migrations/fill_file_store_ci_job_artifacts_spec.rb b/spec/migrations/fill_file_store_ci_job_artifacts_spec.rb
new file mode 100644
index 00000000000..5435a438824
--- /dev/null
+++ b/spec/migrations/fill_file_store_ci_job_artifacts_spec.rb
@@ -0,0 +1,44 @@
+# frozen_string_literal: true
+
+require 'spec_helper'
+require Rails.root.join('db', 'migrate', '20200513235532_fill_file_store_ci_job_artifacts.rb')
+
+describe FillFileStoreCiJobArtifacts do
+ let(:namespaces) { table(:namespaces) }
+ let(:projects) { table(:projects) }
+ let(:builds) { table(:ci_builds) }
+ let(:job_artifacts) { table(:ci_job_artifacts) }
+
+ before do
+ namespaces.create!(id: 123, name: 'sample', path: 'sample')
+ projects.create!(id: 123, name: 'sample', path: 'sample', namespace_id: 123)
+ builds.create!(id: 1)
+ end
+
+ context 'when file_store is nil' do
+ it 'updates file_store to local' do
+ job_artifacts.create!(project_id: 123, job_id: 1, file_type: 1, file_store: nil)
+ job_artifact = job_artifacts.find_by(project_id: 123, job_id: 1)
+
+ expect { migrate! }.to change { job_artifact.reload.file_store }.from(nil).to(1)
+ end
+ end
+
+ context 'when file_store is set to local' do
+ it 'does not update file_store' do
+ job_artifacts.create!(project_id: 123, job_id: 1, file_type: 1, file_store: 1)
+ job_artifact = job_artifacts.find_by(project_id: 123, job_id: 1)
+
+ expect { migrate! }.not_to change { job_artifact.reload.file_store }
+ end
+ end
+
+ context 'when file_store is set to object storage' do
+ it 'does not update file_store' do
+ job_artifacts.create!(project_id: 123, job_id: 1, file_type: 1, file_store: 2)
+ job_artifact = job_artifacts.find_by(project_id: 123, job_id: 1)
+
+ expect { migrate! }.not_to change { job_artifact.reload.file_store }
+ end
+ end
+end
diff --git a/spec/migrations/fill_file_store_lfs_objects_spec.rb b/spec/migrations/fill_file_store_lfs_objects_spec.rb
new file mode 100644
index 00000000000..e574eacca35
--- /dev/null
+++ b/spec/migrations/fill_file_store_lfs_objects_spec.rb
@@ -0,0 +1,36 @@
+# frozen_string_literal: true
+
+require 'spec_helper'
+require Rails.root.join('db', 'migrate', '20200513234502_fill_file_store_lfs_objects.rb')
+
+describe FillFileStoreLfsObjects do
+ let(:lfs_objects) { table(:lfs_objects) }
+ let(:oid) { 'b804383982bb89b00e828e3f44c038cc991d3d1768009fc39ba8e2c081b9fb75' }
+
+ context 'when file_store is nil' do
+ it 'updates file_store to local' do
+ lfs_objects.create(oid: oid, size: 1062, file_store: nil)
+ lfs_object = lfs_objects.find_by(oid: oid)
+
+ expect { migrate! }.to change { lfs_object.reload.file_store }.from(nil).to(1)
+ end
+ end
+
+ context 'when file_store is set to local' do
+ it 'does not update file_store' do
+ lfs_objects.create(oid: oid, size: 1062, file_store: 1)
+ lfs_object = lfs_objects.find_by(oid: oid)
+
+ expect { migrate! }.not_to change { lfs_object.reload.file_store }
+ end
+ end
+
+ context 'when file_store is set to object storage' do
+ it 'does not update file_store' do
+ lfs_objects.create(oid: oid, size: 1062, file_store: 2)
+ lfs_object = lfs_objects.find_by(oid: oid)
+
+ expect { migrate! }.not_to change { lfs_object.reload.file_store }
+ end
+ end
+end
diff --git a/spec/migrations/fill_store_uploads_spec.rb b/spec/migrations/fill_store_uploads_spec.rb
new file mode 100644
index 00000000000..6a2a3c4ea8e
--- /dev/null
+++ b/spec/migrations/fill_store_uploads_spec.rb
@@ -0,0 +1,48 @@
+# frozen_string_literal: true
+
+require 'spec_helper'
+require Rails.root.join('db', 'migrate', '20200513235347_fill_store_uploads.rb')
+
+describe FillStoreUploads do
+ let(:uploads) { table(:uploads) }
+ let(:path) { 'uploads/-/system/avatar.jpg' }
+
+ context 'when store is nil' do
+ it 'updates store to local' do
+ uploads.create(size: 100.kilobytes,
+ uploader: 'AvatarUploader',
+ path: path,
+ store: nil)
+
+ upload = uploads.find_by(path: path)
+
+ expect { migrate! }.to change { upload.reload.store }.from(nil).to(1)
+ end
+ end
+
+ context 'when store is set to local' do
+ it 'does not update store' do
+ uploads.create(size: 100.kilobytes,
+ uploader: 'AvatarUploader',
+ path: path,
+ store: 1)
+
+ upload = uploads.find_by(path: path)
+
+ expect { migrate! }.not_to change { upload.reload.store }
+ end
+ end
+
+ context 'when store is set to object storage' do
+ it 'does not update store' do
+ uploads.create(size: 100.kilobytes,
+ uploader: 'AvatarUploader',
+ path: path,
+ store: 2)
+
+ upload = uploads.find_by(path: path)
+
+ expect { migrate! }.not_to change { upload.reload.store }
+ end
+ end
+end
diff --git a/spec/migrations/remove_additional_application_settings_rows_spec.rb b/spec/migrations/remove_additional_application_settings_rows_spec.rb
new file mode 100644
index 00000000000..379fa385b8e
--- /dev/null
+++ b/spec/migrations/remove_additional_application_settings_rows_spec.rb
@@ -0,0 +1,27 @@
+# frozen_string_literal: true
+
+require 'spec_helper'
+
+require Rails.root.join('db', 'post_migrate', '20200420162730_remove_additional_application_settings_rows.rb')
+
+describe RemoveAdditionalApplicationSettingsRows do
+ let(:application_settings) { table(:application_settings) }
+
+ it 'removes additional rows from application settings' do
+ 3.times { application_settings.create! }
+ latest_settings = application_settings.create!
+
+ disable_migrations_output { migrate! }
+
+ expect(application_settings.count).to eq(1)
+ expect(application_settings.first).to eq(latest_settings)
+ end
+
+ it 'leaves only row in application_settings' do
+ latest_settings = application_settings.create!
+
+ disable_migrations_output { migrate! }
+
+ expect(application_settings.first).to eq(latest_settings)
+ end
+end
diff --git a/spec/migrations/remove_deprecated_jenkins_service_records_spec.rb b/spec/migrations/remove_deprecated_jenkins_service_records_spec.rb
new file mode 100644
index 00000000000..9c9abd36203
--- /dev/null
+++ b/spec/migrations/remove_deprecated_jenkins_service_records_spec.rb
@@ -0,0 +1,28 @@
+# frozen_string_literal: true
+
+require 'spec_helper'
+require Rails.root.join('db', 'migrate', '20200511130129_remove_deprecated_jenkins_service_records.rb')
+require Rails.root.join('db', 'post_migrate', '20200511130130_ensure_deprecated_jenkins_service_records_removal.rb')
+
+shared_examples 'remove DeprecatedJenkinsService records' do
+ let(:services) { table(:services) }
+
+ before do
+ services.create!(type: 'JenkinsDeprecatedService')
+ services.create!(type: 'JenkinsService')
+ end
+
+ it 'deletes services when template and attached to a project' do
+ expect { migrate! }
+ .to change { services.where(type: 'JenkinsDeprecatedService').count }.from(1).to(0)
+ .and not_change { services.where(type: 'JenkinsService').count }
+ end
+end
+
+describe RemoveDeprecatedJenkinsServiceRecords, :migration do
+ it_behaves_like 'remove DeprecatedJenkinsService records'
+end
+
+describe EnsureDeprecatedJenkinsServiceRecordsRemoval, :migration do
+ it_behaves_like 'remove DeprecatedJenkinsService records'
+end
diff --git a/spec/migrations/remove_orphaned_invited_members_spec.rb b/spec/migrations/remove_orphaned_invited_members_spec.rb
new file mode 100644
index 00000000000..0ed4c15428a
--- /dev/null
+++ b/spec/migrations/remove_orphaned_invited_members_spec.rb
@@ -0,0 +1,55 @@
+# frozen_string_literal: true
+
+require 'spec_helper'
+require Rails.root.join('db', 'migrate', '20200424050250_remove_orphaned_invited_members.rb')
+
+describe RemoveOrphanedInvitedMembers do
+ let(:members_table) { table(:members) }
+ let(:users_table) { table(:users) }
+ let(:namespaces_table) { table(:namespaces) }
+ let(:projects_table) { table(:projects) }
+
+ let!(:user1) { users_table.create!(name: 'user1', email: 'user1@example.com', projects_limit: 1) }
+ let!(:user2) { users_table.create!(name: 'user2', email: 'user2@example.com', projects_limit: 1) }
+ let!(:group) { namespaces_table.create!(type: 'Group', name: 'group', path: 'group') }
+ let!(:project) { projects_table.create!(name: 'project', path: 'project', namespace_id: group.id) }
+
+ let!(:member1) { create_member(user_id: user1.id, source_type: 'Project', source_id: project.id, access_level: 10) }
+ let!(:member2) { create_member(user_id: user2.id, source_type: 'Group', source_id: group.id, access_level: 20) }
+
+ let!(:invited_member1) do
+ create_member(user_id: nil, source_type: 'Project', source_id: project.id,
+ invite_token: SecureRandom.hex, invite_accepted_at: Time.now,
+ access_level: 20)
+ end
+ let!(:invited_member2) do
+ create_member(user_id: nil, source_type: 'Group', source_id: group.id,
+ invite_token: SecureRandom.hex, invite_accepted_at: Time.now,
+ access_level: 20)
+ end
+
+ let!(:orphaned_member1) do
+ create_member(user_id: nil, source_type: 'Project', source_id: project.id,
+ invite_accepted_at: Time.now, access_level: 30)
+ end
+ let!(:orphaned_member2) do
+ create_member(user_id: nil, source_type: 'Group', source_id: group.id,
+ invite_accepted_at: Time.now, access_level: 20)
+ end
+
+ it 'removes orphaned invited members but keeps current members' do
+ expect { migrate! }.to change { members_table.count }.from(6).to(4)
+
+ expect(members_table.all.pluck(:id)).to contain_exactly(member1.id, member2.id, invited_member1.id, invited_member2.id)
+ end
+
+ def create_member(options)
+ members_table.create!(
+ {
+ notification_level: 0,
+ ldap: false,
+ override: false
+ }.merge(options)
+ )
+ end
+end
diff --git a/spec/models/ability_spec.rb b/spec/models/ability_spec.rb
index 2bf971f553f..9ef77da6f43 100644
--- a/spec/models/ability_spec.rb
+++ b/spec/models/ability_spec.rb
@@ -74,13 +74,20 @@ describe Ability do
context 'using a private project' do
let(:project) { create(:project, :private) }
- it 'returns users that are administrators' do
+ it 'returns users that are administrators when admin mode is enabled', :enable_admin_mode do
user = build(:user, admin: true)
expect(described_class.users_that_can_read_project([user], project))
.to eq([user])
end
+ it 'does not return users that are administrators when admin mode is disabled' do
+ user = build(:user, admin: true)
+
+ expect(described_class.users_that_can_read_project([user], project))
+ .to eq([])
+ end
+
it 'returns external users if they are the project owner' do
user1 = build(:user, external: true)
user2 = build(:user, external: true)
@@ -145,7 +152,7 @@ describe Ability do
end
describe '.merge_requests_readable_by_user' do
- context 'with an admin' do
+ context 'with an admin when admin mode is enabled', :enable_admin_mode do
it 'returns all merge requests' do
user = build(:user, admin: true)
merge_request = build(:merge_request)
@@ -155,6 +162,19 @@ describe Ability do
end
end
+ context 'with an admin when admin mode is disabled' do
+ it 'returns merge_requests that are publicly visible' do
+ user = build(:user, admin: true)
+ hidden_merge_request = build(:merge_request)
+ visible_merge_request = build(:merge_request, source_project: build(:project, :public))
+
+ merge_requests = described_class
+ .merge_requests_readable_by_user([hidden_merge_request, visible_merge_request], user)
+
+ expect(merge_requests).to eq([visible_merge_request])
+ end
+ end
+
context 'without a user' do
it 'returns merge_requests that are publicly visible' do
hidden_merge_request = build(:merge_request)
@@ -217,7 +237,7 @@ describe Ability do
end
describe '.issues_readable_by_user' do
- context 'with an admin user' do
+ context 'with an admin when admin mode is enabled', :enable_admin_mode do
it 'returns all given issues' do
user = build(:user, admin: true)
issue = build(:issue)
@@ -227,6 +247,26 @@ describe Ability do
end
end
+ context 'with an admin when admin mode is disabled' do
+ it 'returns the issues readable by the admin' do
+ user = build(:user, admin: true)
+ issue = build(:issue)
+
+ expect(issue).to receive(:readable_by?).with(user).and_return(true)
+
+ expect(described_class.issues_readable_by_user([issue], user))
+ .to eq([issue])
+ end
+
+ it 'returns no issues when not given access' do
+ user = build(:user, admin: true)
+ issue = build(:issue)
+
+ expect(described_class.issues_readable_by_user([issue], user))
+ .to be_empty
+ end
+ end
+
context 'with a regular user' do
it 'returns the issues readable by the user' do
user = build(:user)
diff --git a/spec/models/alert_management/alert_spec.rb b/spec/models/alert_management/alert_spec.rb
new file mode 100644
index 00000000000..1da0c6d4071
--- /dev/null
+++ b/spec/models/alert_management/alert_spec.rb
@@ -0,0 +1,320 @@
+# frozen_string_literal: true
+
+require 'spec_helper'
+
+describe AlertManagement::Alert do
+ describe 'associations' do
+ it { is_expected.to belong_to(:project) }
+ it { is_expected.to belong_to(:issue) }
+ end
+
+ describe 'validations' do
+ it { is_expected.to validate_presence_of(:title) }
+ it { is_expected.to validate_presence_of(:events) }
+ it { is_expected.to validate_presence_of(:severity) }
+ it { is_expected.to validate_presence_of(:status) }
+ it { is_expected.to validate_presence_of(:started_at) }
+
+ it { is_expected.to validate_length_of(:title).is_at_most(200) }
+ it { is_expected.to validate_length_of(:description).is_at_most(1000) }
+ it { is_expected.to validate_length_of(:service).is_at_most(100) }
+ it { is_expected.to validate_length_of(:monitoring_tool).is_at_most(100) }
+
+ context 'when status is triggered' do
+ context 'when ended_at is blank' do
+ subject { build(:alert_management_alert) }
+
+ it { is_expected.to be_valid }
+ end
+
+ context 'when ended_at is present' do
+ subject { build(:alert_management_alert, ended_at: Time.current) }
+
+ it { is_expected.to be_invalid }
+ end
+ end
+
+ context 'when status is acknowledged' do
+ context 'when ended_at is blank' do
+ subject { build(:alert_management_alert, :acknowledged) }
+
+ it { is_expected.to be_valid }
+ end
+
+ context 'when ended_at is present' do
+ subject { build(:alert_management_alert, :acknowledged, ended_at: Time.current) }
+
+ it { is_expected.to be_invalid }
+ end
+ end
+
+ context 'when status is resolved' do
+ context 'when ended_at is blank' do
+ subject { build(:alert_management_alert, :resolved, ended_at: nil) }
+
+ it { is_expected.to be_invalid }
+ end
+
+ context 'when ended_at is present' do
+ subject { build(:alert_management_alert, :resolved, ended_at: Time.current) }
+
+ it { is_expected.to be_valid }
+ end
+ end
+
+ context 'when status is ignored' do
+ context 'when ended_at is blank' do
+ subject { build(:alert_management_alert, :ignored) }
+
+ it { is_expected.to be_valid }
+ end
+
+ context 'when ended_at is present' do
+ subject { build(:alert_management_alert, :ignored, ended_at: Time.current) }
+
+ it { is_expected.to be_invalid }
+ end
+ end
+
+ describe 'fingerprint' do
+ let_it_be(:fingerprint) { 'fingerprint' }
+ let_it_be(:existing_alert) { create(:alert_management_alert, fingerprint: fingerprint) }
+ let(:new_alert) { build(:alert_management_alert, fingerprint: fingerprint, project: project) }
+
+ subject { new_alert }
+
+ context 'adding an alert with the same fingerprint' do
+ context 'same project' do
+ let(:project) { existing_alert.project }
+
+ it { is_expected.not_to be_valid }
+ end
+
+ context 'different project' do
+ let(:project) { create(:project) }
+
+ it { is_expected.to be_valid }
+ end
+ end
+ end
+
+ describe 'hosts' do
+ subject(:alert) { build(:alert_management_alert, hosts: hosts) }
+
+ context 'over 255 total chars' do
+ let(:hosts) { ['111.111.111.111'] * 18 }
+
+ it { is_expected.not_to be_valid }
+ end
+
+ context 'under 255 chars' do
+ let(:hosts) { ['111.111.111.111'] * 17 }
+
+ it { is_expected.to be_valid }
+ end
+ end
+ end
+
+ describe 'enums' do
+ let(:severity_values) do
+ { critical: 0, high: 1, medium: 2, low: 3, info: 4, unknown: 5 }
+ end
+
+ it { is_expected.to define_enum_for(:severity).with_values(severity_values) }
+ end
+
+ describe 'scopes' do
+ let_it_be(:project) { create(:project) }
+ let_it_be(:triggered_alert) { create(:alert_management_alert, project: project) }
+ let_it_be(:resolved_alert) { create(:alert_management_alert, :resolved, project: project) }
+ let_it_be(:ignored_alert) { create(:alert_management_alert, :ignored, project: project) }
+
+ describe '.for_iid' do
+ subject { AlertManagement::Alert.for_iid(triggered_alert.iid) }
+
+ it { is_expected.to match_array(triggered_alert) }
+ end
+
+ describe '.for_status' do
+ let(:status) { AlertManagement::Alert::STATUSES[:resolved] }
+
+ subject { AlertManagement::Alert.for_status(status) }
+
+ it { is_expected.to match_array(resolved_alert) }
+
+ context 'with multiple statuses' do
+ let(:status) { AlertManagement::Alert::STATUSES.values_at(:resolved, :ignored) }
+
+ it { is_expected.to match_array([resolved_alert, ignored_alert]) }
+ end
+ end
+
+ describe '.for_fingerprint' do
+ let_it_be(:fingerprint) { SecureRandom.hex }
+ let_it_be(:alert_with_fingerprint) { create(:alert_management_alert, project: project, fingerprint: fingerprint) }
+ let_it_be(:unrelated_alert_with_finger_print) { create(:alert_management_alert, fingerprint: fingerprint) }
+
+ subject { described_class.for_fingerprint(project, fingerprint) }
+
+ it { is_expected.to contain_exactly(alert_with_fingerprint) }
+ end
+
+ describe '.counts_by_status' do
+ subject { described_class.counts_by_status }
+
+ it do
+ is_expected.to eq(
+ triggered_alert.status => 1,
+ resolved_alert.status => 1,
+ ignored_alert.status => 1
+ )
+ end
+ end
+ end
+
+ describe '.search' do
+ let_it_be(:alert) do
+ create(:alert_management_alert,
+ title: 'Title',
+ description: 'Desc',
+ service: 'Service',
+ monitoring_tool: 'Monitor'
+ )
+ end
+
+ subject { AlertManagement::Alert.search(query) }
+
+ context 'does not contain search string' do
+ let(:query) { 'something else' }
+
+ it { is_expected.to be_empty }
+ end
+
+ context 'title includes query' do
+ let(:query) { alert.title.upcase }
+
+ it { is_expected.to contain_exactly(alert) }
+ end
+
+ context 'description includes query' do
+ let(:query) { alert.description.upcase }
+
+ it { is_expected.to contain_exactly(alert) }
+ end
+
+ context 'service includes query' do
+ let(:query) { alert.service.upcase }
+
+ it { is_expected.to contain_exactly(alert) }
+ end
+
+ context 'monitoring tool includes query' do
+ let(:query) { alert.monitoring_tool.upcase }
+
+ it { is_expected.to contain_exactly(alert) }
+ end
+ end
+
+ describe '#details' do
+ let(:payload) do
+ {
+ 'title' => 'Details title',
+ 'custom' => {
+ 'alert' => {
+ 'fields' => %w[one two]
+ }
+ },
+ 'yet' => {
+ 'another' => 'field'
+ }
+ }
+ end
+ let(:alert) { build(:alert_management_alert, title: 'Details title', payload: payload) }
+
+ subject { alert.details }
+
+ it 'renders the payload as inline hash' do
+ is_expected.to eq(
+ 'custom.alert.fields' => %w[one two],
+ 'yet.another' => 'field'
+ )
+ end
+ end
+
+ describe '#trigger' do
+ subject { alert.trigger }
+
+ context 'when alert is in triggered state' do
+ let(:alert) { create(:alert_management_alert) }
+
+ it 'does not change the alert status' do
+ expect { subject }.not_to change { alert.reload.status }
+ end
+ end
+
+ context 'when alert not in triggered state' do
+ let(:alert) { create(:alert_management_alert, :resolved) }
+
+ it 'changes the alert status to triggered' do
+ expect { subject }.to change { alert.triggered? }.to(true)
+ end
+
+ it 'resets ended at' do
+ expect { subject }.to change { alert.reload.ended_at }.to nil
+ end
+ end
+ end
+
+ describe '#acknowledge' do
+ subject { alert.acknowledge }
+
+ let(:alert) { create(:alert_management_alert, :resolved) }
+
+ it 'changes the alert status to acknowledged' do
+ expect { subject }.to change { alert.acknowledged? }.to(true)
+ end
+
+ it 'resets ended at' do
+ expect { subject }.to change { alert.reload.ended_at }.to nil
+ end
+ end
+
+ describe '#resolve' do
+ let!(:ended_at) { Time.current }
+
+ subject do
+ alert.ended_at = ended_at
+ alert.resolve
+ end
+
+ context 'when alert already resolved' do
+ let(:alert) { create(:alert_management_alert, :resolved) }
+
+ it 'does not change the alert status' do
+ expect { subject }.not_to change { alert.reload.status }
+ end
+ end
+
+ context 'when alert is not resolved' do
+ let(:alert) { create(:alert_management_alert) }
+
+ it 'changes alert status to "resolved"' do
+ expect { subject }.to change { alert.resolved? }.to(true)
+ end
+ end
+ end
+
+ describe '#ignore' do
+ subject { alert.ignore }
+
+ let(:alert) { create(:alert_management_alert, :resolved) }
+
+ it 'changes the alert status to ignored' do
+ expect { subject }.to change { alert.ignored? }.to(true)
+ end
+
+ it 'resets ended at' do
+ expect { subject }.to change { alert.reload.ended_at }.to nil
+ end
+ end
+end
diff --git a/spec/models/application_setting_spec.rb b/spec/models/application_setting_spec.rb
index 523e17f82c1..64308af38f9 100644
--- a/spec/models/application_setting_spec.rb
+++ b/spec/models/application_setting_spec.rb
@@ -91,6 +91,20 @@ describe ApplicationSetting do
it { is_expected.not_to allow_value(nil).for(:namespace_storage_size_limit) }
it { is_expected.not_to allow_value(-1).for(:namespace_storage_size_limit) }
+ it { is_expected.to allow_value(300).for(:issues_create_limit) }
+ it { is_expected.not_to allow_value('three').for(:issues_create_limit) }
+ it { is_expected.not_to allow_value(nil).for(:issues_create_limit) }
+ it { is_expected.not_to allow_value(10.5).for(:issues_create_limit) }
+ it { is_expected.not_to allow_value(-1).for(:issues_create_limit) }
+
+ it { is_expected.to allow_value(0).for(:raw_blob_request_limit) }
+ it { is_expected.not_to allow_value('abc').for(:raw_blob_request_limit) }
+ it { is_expected.not_to allow_value(nil).for(:raw_blob_request_limit) }
+ it { is_expected.not_to allow_value(10.5).for(:raw_blob_request_limit) }
+ it { is_expected.not_to allow_value(-1).for(:raw_blob_request_limit) }
+
+ it { is_expected.not_to allow_value(false).for(:hashed_storage_enabled) }
+
context 'grafana_url validations' do
before do
subject.instance_variable_set(:@parsed_grafana_url, nil)
diff --git a/spec/models/blob_spec.rb b/spec/models/blob_spec.rb
index a0193b29bb3..c2d6406c3fb 100644
--- a/spec/models/blob_spec.rb
+++ b/spec/models/blob_spec.rb
@@ -5,12 +5,17 @@ require 'spec_helper'
describe Blob do
include FakeBlobHelpers
- let(:project) { build(:project, lfs_enabled: true) }
+ using RSpec::Parameterized::TableSyntax
+
+ let(:project) { build(:project) }
let(:personal_snippet) { build(:personal_snippet) }
let(:project_snippet) { build(:project_snippet, project: project) }
+ let(:repository) { project.repository }
+ let(:lfs_enabled) { true }
+
before do
- allow(Gitlab.config.lfs).to receive(:enabled).and_return(true)
+ allow(repository).to receive(:lfs_enabled?) { lfs_enabled }
end
describe '.decorate' do
@@ -27,7 +32,7 @@ describe Blob do
it 'does not fetch blobs when none are accessed' do
expect(container.repository).not_to receive(:blobs_at)
- described_class.lazy(container, commit_id, 'CHANGELOG')
+ described_class.lazy(container.repository, commit_id, 'CHANGELOG')
end
it 'fetches all blobs for the same repository when one is accessed' do
@@ -36,10 +41,10 @@ describe Blob do
.once.and_call_original
expect(other_container.repository).not_to receive(:blobs_at)
- changelog = described_class.lazy(container, commit_id, 'CHANGELOG')
- contributing = described_class.lazy(same_container, commit_id, 'CONTRIBUTING.md')
+ changelog = described_class.lazy(container.repository, commit_id, 'CHANGELOG')
+ contributing = described_class.lazy(same_container.repository, commit_id, 'CONTRIBUTING.md')
- described_class.lazy(other_container, commit_id, 'CHANGELOG')
+ described_class.lazy(other_container.repository, commit_id, 'CHANGELOG')
# Access property so the values are loaded
changelog.id
@@ -47,14 +52,14 @@ describe Blob do
end
it 'does not include blobs from previous requests in later requests' do
- changelog = described_class.lazy(container, commit_id, 'CHANGELOG')
- contributing = described_class.lazy(same_container, commit_id, 'CONTRIBUTING.md')
+ changelog = described_class.lazy(container.repository, commit_id, 'CHANGELOG')
+ contributing = described_class.lazy(same_container.repository, commit_id, 'CONTRIBUTING.md')
# Access property so the values are loaded
changelog.id
contributing.id
- readme = described_class.lazy(container, commit_id, 'README.md')
+ readme = described_class.lazy(container.repository, commit_id, 'README.md')
expect(container.repository).to receive(:blobs_at)
.with([[commit_id, 'README.md']], blob_size_limit: blob_size_limit).once.and_call_original
@@ -128,399 +133,84 @@ describe Blob do
end
describe '#external_storage_error?' do
- shared_examples 'no error' do
- it do
- expect(blob.external_storage_error?).to be_falsey
- end
- end
-
- shared_examples 'returns error' do
- it do
- expect(blob.external_storage_error?).to be_truthy
- end
- end
+ subject { blob.external_storage_error? }
context 'if the blob is stored in LFS' do
- let(:blob) { fake_blob(path: 'file.pdf', lfs: true, container: container) }
-
- context 'when the project has LFS enabled' do
- context 'with project' do
- let(:container) { project }
-
- it_behaves_like 'no error'
- end
-
- context 'with personal snippet' do
- let(:container) { personal_snippet }
-
- it_behaves_like 'returns error'
- end
+ let(:blob) { fake_blob(path: 'file.pdf', lfs: true) }
- context 'with project snippet' do
- let(:container) { project_snippet }
+ context 'when LFS is enabled' do
+ let(:lfs_enabled) { true }
- it_behaves_like 'no error'
- end
+ it { is_expected.to be_falsy }
end
- context 'when the project does not have LFS enabled' do
- before do
- project.lfs_enabled = false
- end
-
- context 'with project' do
- let(:container) { project }
+ context 'when LFS is not enabled' do
+ let(:lfs_enabled) { false }
- it_behaves_like 'returns error'
- end
-
- context 'with project snippet' do
- let(:container) { project_snippet }
-
- it_behaves_like 'returns error'
- end
+ it { is_expected.to be_truthy }
end
end
context 'if the blob is not stored in LFS' do
- let(:blob) { fake_blob(path: 'file.md', container: container) }
-
- context 'with project' do
- let(:container) { project }
-
- it_behaves_like 'no error'
- end
-
- context 'with personal snippet' do
- let(:container) { personal_snippet }
-
- it_behaves_like 'no error'
- end
-
- context 'with project snippet' do
- let(:container) { project_snippet }
+ let(:blob) { fake_blob(path: 'file.md') }
- it_behaves_like 'no error'
- end
+ it { is_expected.to be_falsy }
end
end
describe '#stored_externally?' do
+ subject { blob.stored_externally? }
+
context 'if the blob is stored in LFS' do
let(:blob) { fake_blob(path: 'file.pdf', lfs: true) }
- shared_examples 'returns true' do
- it do
- expect(blob.stored_externally?).to be_truthy
- end
- end
-
- shared_examples 'returns false' do
- it do
- expect(blob.stored_externally?).to be_falsey
- end
- end
-
- context 'when the project has LFS enabled' do
- context 'with project' do
- let(:container) { project }
-
- it_behaves_like 'returns true'
- end
+ context 'when LFS is enabled' do
+ let(:lfs_enabled) { true }
- context 'with personal snippet' do
- let(:container) { personal_snippet }
-
- it_behaves_like 'returns true'
- end
-
- context 'with project snippet' do
- let(:container) { project_snippet }
-
- it_behaves_like 'returns true'
- end
+ it { is_expected.to be_truthy }
end
- context 'when the project does not have LFS enabled' do
- before do
- project.lfs_enabled = false
- end
-
- context 'with project' do
- let(:container) { project }
-
- it_behaves_like 'returns false'
- end
-
- context 'with personal snippet' do
- let(:container) { personal_snippet }
+ context 'when LFS is not enabled' do
+ let(:lfs_enabled) { false }
- it_behaves_like 'returns false'
- end
-
- context 'with project snippet' do
- let(:container) { project_snippet }
-
- it_behaves_like 'returns false'
- end
+ it { is_expected.to be_falsy }
end
end
context 'if the blob is not stored in LFS' do
let(:blob) { fake_blob(path: 'file.md') }
- it 'returns false' do
- expect(blob.stored_externally?).to be_falsey
- end
+ it { is_expected.to be_falsy }
end
end
describe '#binary?' do
- shared_examples 'returns true' do
- it do
- expect(blob.binary?).to be_truthy
- end
- end
-
- shared_examples 'returns false' do
- it do
- expect(blob.binary?).to be_falsey
- end
- end
-
- context 'if the blob is stored externally' do
- let(:blob) { fake_blob(path: file, lfs: true) }
-
- context 'if the extension has a rich viewer' do
- context 'if the viewer is binary' do
- let(:file) { 'file.pdf' }
-
- context 'with project' do
- let(:container) { project }
-
- it_behaves_like 'returns true'
- end
-
- context 'with personal snippet' do
- let(:container) { personal_snippet }
-
- it_behaves_like 'returns true'
- end
-
- context 'with project snippet' do
- let(:container) { project_snippet }
-
- it_behaves_like 'returns true'
- end
- end
-
- context 'if the viewer is text-based' do
- let(:file) { 'file.md' }
-
- context 'with project' do
- let(:container) { project }
-
- it_behaves_like 'returns false'
- end
-
- context 'with personal snippet' do
- let(:container) { personal_snippet }
-
- it_behaves_like 'returns false'
- end
-
- context 'with project snippet' do
- let(:container) { project_snippet }
-
- it_behaves_like 'returns false'
- end
- end
+ context 'an lfs object' do
+ where(:filename, :is_binary) do
+ 'file.pdf' | true
+ 'file.md' | false
+ 'file.txt' | false
+ 'file.ics' | false
+ 'file.rb' | false
+ 'file.exe' | true
+ 'file.ini' | false
+ 'file.wtf' | true
end
- context "if the extension doesn't have a rich viewer" do
- context 'if the extension has a text mime type' do
- context 'if the extension is for a programming language' do
- let(:file) { 'file.txt' }
-
- context 'with project' do
- let(:container) { project }
-
- it_behaves_like 'returns false'
- end
-
- context 'with personal snippet' do
- let(:container) { personal_snippet }
-
- it_behaves_like 'returns false'
- end
-
- context 'with project snippet' do
- let(:container) { project_snippet }
-
- it_behaves_like 'returns false'
- end
- end
-
- context 'if the extension is not for a programming language' do
- let(:file) { 'file.ics' }
-
- context 'with project' do
- let(:container) { project }
-
- it_behaves_like 'returns false'
- end
-
- context 'with personal snippet' do
- let(:container) { personal_snippet }
-
- it_behaves_like 'returns false'
- end
-
- context 'with project snippet' do
- let(:container) { project_snippet }
+ with_them do
+ let(:blob) { fake_blob(path: filename, lfs: true, container: project) }
- it_behaves_like 'returns false'
- end
- end
- end
-
- context 'if the extension has a binary mime type' do
- context 'if the extension is for a programming language' do
- let(:file) { 'file.rb' }
-
- context 'with project' do
- let(:container) { project }
-
- it_behaves_like 'returns false'
- end
-
- context 'with personal snippet' do
- let(:container) { personal_snippet }
-
- it_behaves_like 'returns false'
- end
-
- context 'with project snippet' do
- let(:container) { project_snippet }
-
- it_behaves_like 'returns false'
- end
- end
-
- context 'if the extension is not for a programming language' do
- let(:file) { 'file.exe' }
-
- context 'with project' do
- let(:container) { project }
-
- it_behaves_like 'returns true'
- end
-
- context 'with personal snippet' do
- let(:container) { personal_snippet }
-
- it_behaves_like 'returns true'
- end
-
- context 'with project snippet' do
- let(:container) { project_snippet }
-
- it_behaves_like 'returns true'
- end
- end
- end
-
- context 'if the extension has an unknown mime type' do
- context 'if the extension is for a programming language' do
- let(:file) { 'file.ini' }
-
- context 'with project' do
- let(:container) { project }
-
- it_behaves_like 'returns false'
- end
-
- context 'with personal snippet' do
- let(:container) { personal_snippet }
-
- it_behaves_like 'returns false'
- end
-
- context 'with project snippet' do
- let(:container) { project_snippet }
-
- it_behaves_like 'returns false'
- end
- end
-
- context 'if the extension is not for a programming language' do
- let(:file) { 'file.wtf' }
-
- context 'with project' do
- let(:container) { project }
-
- it_behaves_like 'returns true'
- end
-
- context 'with personal snippet' do
- let(:container) { personal_snippet }
-
- it_behaves_like 'returns true'
- end
-
- context 'with project snippet' do
- let(:container) { project_snippet }
-
- it_behaves_like 'returns true'
- end
- end
- end
+ it { expect(blob.binary?).to eq(is_binary) }
end
end
- context 'if the blob is not stored externally' do
- context 'if the blob is binary' do
- let(:blob) { fake_blob(path: 'file.pdf', binary: true, container: container) }
-
- context 'with project' do
- let(:container) { project }
-
- it_behaves_like 'returns true'
- end
-
- context 'with personal snippet' do
- let(:container) { personal_snippet }
-
- it_behaves_like 'returns true'
- end
-
- context 'with project snippet' do
- let(:container) { project_snippet }
-
- it_behaves_like 'returns true'
- end
- end
-
- context 'if the blob is text-based' do
- let(:blob) { fake_blob(path: 'file.md', container: container) }
-
- context 'with project' do
- let(:container) { project }
-
- it_behaves_like 'returns false'
- end
-
- context 'with personal snippet' do
- let(:container) { personal_snippet }
-
- it_behaves_like 'returns false'
- end
+ context 'a non-lfs object' do
+ let(:blob) { fake_blob(path: 'anything', container: project) }
- context 'with project snippet' do
- let(:container) { project_snippet }
+ it 'delegates to binary_in_repo?' do
+ expect(blob).to receive(:binary_in_repo?) { :result }
- it_behaves_like 'returns false'
- end
+ expect(blob.binary?).to eq(:result)
end
end
end
@@ -569,9 +259,7 @@ describe Blob do
describe '#rich_viewer' do
context 'when the blob has an external storage error' do
- before do
- project.lfs_enabled = false
- end
+ let(:lfs_enabled) { false }
it 'returns nil' do
blob = fake_blob(path: 'file.pdf', lfs: true)
@@ -631,9 +319,7 @@ describe Blob do
describe '#auxiliary_viewer' do
context 'when the blob has an external storage error' do
- before do
- project.lfs_enabled = false
- end
+ let(:lfs_enabled) { false }
it 'returns nil' do
blob = fake_blob(path: 'LICENSE', lfs: true)
@@ -676,63 +362,21 @@ describe Blob do
end
describe '#rendered_as_text?' do
- shared_examples 'returns true' do
- it do
- expect(blob.rendered_as_text?(ignore_errors: ignore_errors)).to be_truthy
- end
- end
-
- shared_examples 'returns false' do
- it do
- expect(blob.rendered_as_text?(ignore_errors: ignore_errors)).to be_falsey
- end
- end
+ subject { blob.rendered_as_text?(ignore_errors: ignore_errors) }
context 'when ignoring errors' do
let(:ignore_errors) { true }
context 'when the simple viewer is text-based' do
- let(:blob) { fake_blob(path: 'file.md', size: 100.megabytes, container: container) }
-
- context 'with project' do
- let(:container) { project }
-
- it_behaves_like 'returns true'
- end
-
- context 'with personal snippet' do
- let(:container) { personal_snippet }
+ let(:blob) { fake_blob(path: 'file.md', size: 100.megabytes) }
- it_behaves_like 'returns true'
- end
-
- context 'with project snippet' do
- let(:container) { project_snippet }
-
- it_behaves_like 'returns true'
- end
+ it { is_expected.to be_truthy }
end
context 'when the simple viewer is binary' do
- let(:blob) { fake_blob(path: 'file.pdf', binary: true, size: 100.megabytes, container: container) }
-
- context 'with project' do
- let(:container) { project }
-
- it_behaves_like 'returns false'
- end
-
- context 'with personal snippet' do
- let(:container) { personal_snippet }
-
- it_behaves_like 'returns false'
- end
-
- context 'with project snippet' do
- let(:container) { project_snippet }
+ let(:blob) { fake_blob(path: 'file.pdf', binary: true, size: 100.megabytes) }
- it_behaves_like 'returns false'
- end
+ it { is_expected.to be_falsy }
end
end
@@ -740,47 +384,15 @@ describe Blob do
let(:ignore_errors) { false }
context 'when the viewer has render errors' do
- let(:blob) { fake_blob(path: 'file.md', size: 100.megabytes, container: container) }
-
- context 'with project' do
- let(:container) { project }
-
- it_behaves_like 'returns false'
- end
-
- context 'with personal snippet' do
- let(:container) { personal_snippet }
-
- it_behaves_like 'returns false'
- end
-
- context 'with project snippet' do
- let(:container) { project_snippet }
+ let(:blob) { fake_blob(path: 'file.md', size: 100.megabytes) }
- it_behaves_like 'returns false'
- end
+ it { is_expected.to be_falsy }
end
context "when the viewer doesn't have render errors" do
- let(:blob) { fake_blob(path: 'file.md', container: container) }
-
- context 'with project' do
- let(:container) { project }
-
- it_behaves_like 'returns true'
- end
+ let(:blob) { fake_blob(path: 'file.md') }
- context 'with personal snippet' do
- let(:container) { personal_snippet }
-
- it_behaves_like 'returns true'
- end
-
- context 'with project snippet' do
- let(:container) { project_snippet }
-
- it_behaves_like 'returns true'
- end
+ it { is_expected.to be_truthy }
end
end
end
diff --git a/spec/models/blob_viewer/readme_spec.rb b/spec/models/blob_viewer/readme_spec.rb
index 6586adbc373..89bc5be94fb 100644
--- a/spec/models/blob_viewer/readme_spec.rb
+++ b/spec/models/blob_viewer/readme_spec.rb
@@ -40,7 +40,7 @@ describe BlobViewer::Readme do
context 'when the wiki is not empty' do
before do
- create(:wiki_page, wiki: project.wiki, attrs: { title: 'home', content: 'Home page' })
+ create(:wiki_page, wiki: project.wiki, title: 'home', content: 'Home page')
end
it 'returns nil' do
diff --git a/spec/models/broadcast_message_spec.rb b/spec/models/broadcast_message_spec.rb
index 6cef81d6e44..127faa5e8e2 100644
--- a/spec/models/broadcast_message_spec.rb
+++ b/spec/models/broadcast_message_spec.rb
@@ -143,6 +143,24 @@ describe BroadcastMessage do
expect(subject.call('/group/groupname/issues').length).to eq(0)
end
+
+ it 'does not return message if target path has no wild card at the end' do
+ create(:broadcast_message, target_path: "*/issues", broadcast_type: broadcast_type)
+
+ expect(subject.call('/group/issues/test').length).to eq(0)
+ end
+
+ it 'does not return message if target path has wild card at the end' do
+ create(:broadcast_message, target_path: "/issues/*", broadcast_type: broadcast_type)
+
+ expect(subject.call('/group/issues/test').length).to eq(0)
+ end
+
+ it 'does return message if target path has wild card at the beginning and the end' do
+ create(:broadcast_message, target_path: "*/issues/*", broadcast_type: broadcast_type)
+
+ expect(subject.call('/group/issues/test').length).to eq(1)
+ end
end
describe '.current', :use_clean_rails_memory_store_caching do
diff --git a/spec/models/ci/build_spec.rb b/spec/models/ci/build_spec.rb
index a4f3fa518c6..6605866d9c0 100644
--- a/spec/models/ci/build_spec.rb
+++ b/spec/models/ci/build_spec.rb
@@ -106,10 +106,14 @@ describe Ci::Build do
end
end
- describe '.with_artifacts_archive' do
- subject { described_class.with_artifacts_archive }
+ describe '.with_downloadable_artifacts' do
+ subject { described_class.with_downloadable_artifacts }
- context 'when job does not have an archive' do
+ before do
+ stub_feature_flags(drop_license_management_artifact: false)
+ end
+
+ context 'when job does not have a downloadable artifact' do
let!(:job) { create(:ci_build) }
it 'does not return the job' do
@@ -117,15 +121,23 @@ describe Ci::Build do
end
end
- context 'when job has a job artifact archive' do
- let!(:job) { create(:ci_build, :artifacts) }
+ ::Ci::JobArtifact::DOWNLOADABLE_TYPES.each do |type|
+ context "when job has a #{type} artifact" do
+ it 'returns the job' do
+ job = create(:ci_build)
+ create(
+ :ci_job_artifact,
+ file_format: ::Ci::JobArtifact::TYPE_AND_FORMAT_PAIRS[type.to_sym],
+ file_type: type,
+ job: job
+ )
- it 'returns the job' do
- is_expected.to include(job)
+ is_expected.to include(job)
+ end
end
end
- context 'when job has a job artifact trace' do
+ context 'when job has a non-downloadable artifact' do
let!(:job) { create(:ci_build, :trace_artifact) }
it 'does not return the job' do
@@ -1419,6 +1431,8 @@ describe Ci::Build do
subject { build.erase_erasable_artifacts! }
before do
+ stub_feature_flags(drop_license_management_artifact: false)
+
Ci::JobArtifact.file_types.keys.each do |file_type|
create(:ci_job_artifact, job: build, file_type: file_type, file_format: Ci::JobArtifact::TYPE_AND_FORMAT_PAIRS[file_type.to_sym])
end
@@ -2367,12 +2381,14 @@ describe Ci::Build do
let(:pipeline_pre_var) { { key: 'pipeline', value: 'value', public: true, masked: false } }
let(:build_yaml_var) { { key: 'yaml', value: 'value', public: true, masked: false } }
let(:job_jwt_var) { { key: 'CI_JOB_JWT', value: 'ci.job.jwt', public: false, masked: true } }
+ let(:job_dependency_var) { { key: 'job_dependency', value: 'value', public: true, masked: false } }
before do
allow(build).to receive(:predefined_variables) { [build_pre_var] }
allow(build).to receive(:yaml_variables) { [build_yaml_var] }
allow(build).to receive(:persisted_variables) { [] }
allow(build).to receive(:job_jwt_variables) { [job_jwt_var] }
+ allow(build).to receive(:dependency_variables) { [job_dependency_var] }
allow_any_instance_of(Project)
.to receive(:predefined_variables) { [project_pre_var] }
@@ -2390,6 +2406,7 @@ describe Ci::Build do
project_pre_var,
pipeline_pre_var,
build_yaml_var,
+ job_dependency_var,
{ key: 'secret', value: 'value', public: false, masked: false }])
end
end
@@ -2884,6 +2901,19 @@ describe Ci::Build do
it { is_expected.to include(deployment_variable) }
end
+ context 'when build has a freeze period' do
+ let(:freeze_variable) { { key: 'CI_DEPLOY_FREEZE', value: 'true', masked: false, public: true } }
+
+ before do
+ expect_next_instance_of(Ci::FreezePeriodStatus) do |freeze_period|
+ expect(freeze_period).to receive(:execute)
+ .and_return(true)
+ end
+ end
+
+ it { is_expected.to include(freeze_variable) }
+ end
+
context 'when project has default CI config path' do
let(:ci_config_path) { { key: 'CI_CONFIG_PATH', value: '.gitlab-ci.yml', public: true, masked: false } }
@@ -2987,6 +3017,15 @@ describe Ci::Build do
end
end
end
+
+ context 'when build has dependency which has dotenv variable' do
+ let!(:prepare) { create(:ci_build, pipeline: pipeline, stage_idx: 0) }
+ let!(:build) { create(:ci_build, pipeline: pipeline, stage_idx: 1, options: { dependencies: [prepare.name] }) }
+
+ let!(:job_variable) { create(:ci_job_variable, :dotenv_source, job: prepare) }
+
+ it { is_expected.to include(key: job_variable.key, value: job_variable.value, public: false, masked: false) }
+ end
end
describe '#scoped_variables' do
@@ -3049,71 +3088,36 @@ describe Ci::Build do
end
end
end
- end
- describe '#secret_group_variables' do
- subject { build.secret_group_variables }
-
- let!(:variable) { create(:ci_group_variable, protected: true, group: group) }
+ context 'with dependency variables' do
+ let!(:prepare) { create(:ci_build, name: 'prepare', pipeline: pipeline, stage_idx: 0) }
+ let!(:build) { create(:ci_build, pipeline: pipeline, stage_idx: 1, options: { dependencies: ['prepare'] }) }
- context 'when ref is branch' do
- let(:build) { create(:ci_build, ref: 'master', tag: false, project: project) }
+ let!(:job_variable) { create(:ci_job_variable, :dotenv_source, job: prepare) }
- context 'when ref is protected' do
+ context 'FF ci_dependency_variables is enabled' do
before do
- create(:protected_branch, :developers_can_merge, name: 'master', project: project)
+ stub_feature_flags(ci_dependency_variables: true)
end
- it { is_expected.to include(variable) }
- end
-
- context 'when ref is not protected' do
- it { is_expected.not_to include(variable) }
- end
- end
-
- context 'when ref is tag' do
- let(:build) { create(:ci_build, ref: 'v1.1.0', tag: true, project: project) }
-
- context 'when ref is protected' do
- before do
- create(:protected_tag, project: project, name: 'v*')
+ it 'inherits dependent variables' do
+ expect(build.scoped_variables.to_hash).to include(job_variable.key => job_variable.value)
end
-
- it { is_expected.to include(variable) }
- end
-
- context 'when ref is not protected' do
- it { is_expected.not_to include(variable) }
end
- end
- context 'when ref is merge request' do
- let(:merge_request) { create(:merge_request, :with_detached_merge_request_pipeline) }
- let(:pipeline) { merge_request.pipelines_for_merge_request.first }
- let(:build) { create(:ci_build, ref: merge_request.source_branch, tag: false, pipeline: pipeline, project: project) }
-
- context 'when ref is protected' do
+ context 'FF ci_dependency_variables is disabled' do
before do
- create(:protected_branch, :developers_can_merge, name: merge_request.source_branch, project: project)
+ stub_feature_flags(ci_dependency_variables: false)
end
- it 'does not return protected variables as it is not supported for merge request pipelines' do
- is_expected.not_to include(variable)
+ it 'does not inherit dependent variables' do
+ expect(build.scoped_variables.to_hash).not_to include(job_variable.key => job_variable.value)
end
end
-
- context 'when ref is not protected' do
- it { is_expected.not_to include(variable) }
- end
end
end
- describe '#secret_project_variables' do
- subject { build.secret_project_variables }
-
- let!(:variable) { create(:ci_variable, protected: true, project: project) }
-
+ shared_examples "secret CI variables" do
context 'when ref is branch' do
let(:build) { create(:ci_build, ref: 'master', tag: false, project: project) }
@@ -3167,6 +3171,30 @@ describe Ci::Build do
end
end
+ describe '#secret_instance_variables' do
+ subject { build.secret_instance_variables }
+
+ let_it_be(:variable) { create(:ci_instance_variable, protected: true) }
+
+ include_examples "secret CI variables"
+ end
+
+ describe '#secret_group_variables' do
+ subject { build.secret_group_variables }
+
+ let_it_be(:variable) { create(:ci_group_variable, protected: true, group: group) }
+
+ include_examples "secret CI variables"
+ end
+
+ describe '#secret_project_variables' do
+ subject { build.secret_project_variables }
+
+ let_it_be(:variable) { create(:ci_variable, protected: true, project: project) }
+
+ include_examples "secret CI variables"
+ end
+
describe '#deployment_variables' do
let(:build) { create(:ci_build, environment: environment) }
let(:environment) { 'production' }
@@ -3217,6 +3245,29 @@ describe Ci::Build do
expect(build.scoped_variables_hash).not_to include('MY_VAR': 'myvar')
end
end
+
+ context 'when overriding CI instance variables' do
+ before do
+ create(:ci_instance_variable, key: 'MY_VAR', value: 'my value 1')
+ group.variables.create!(key: 'MY_VAR', value: 'my value 2')
+ end
+
+ it 'returns a regular hash created using valid ordering' do
+ expect(build.scoped_variables_hash).to include('MY_VAR': 'my value 2')
+ expect(build.scoped_variables_hash).not_to include('MY_VAR': 'my value 1')
+ end
+ end
+
+ context 'when CI instance variables are disabled' do
+ before do
+ create(:ci_instance_variable, key: 'MY_VAR', value: 'my value 1')
+ stub_feature_flags(ci_instance_level_variables: false)
+ end
+
+ it 'does not include instance level variables' do
+ expect(build.scoped_variables_hash).not_to include('MY_VAR': 'my value 1')
+ end
+ end
end
describe '#any_unmet_prerequisites?' do
@@ -3293,6 +3344,41 @@ describe Ci::Build do
end
end
+ describe '#dependency_variables' do
+ subject { build.dependency_variables }
+
+ context 'when using dependencies' do
+ let!(:prepare1) { create(:ci_build, name: 'prepare1', pipeline: pipeline, stage_idx: 0) }
+ let!(:prepare2) { create(:ci_build, name: 'prepare2', pipeline: pipeline, stage_idx: 0) }
+ let!(:build) { create(:ci_build, pipeline: pipeline, stage_idx: 1, options: { dependencies: ['prepare1'] }) }
+
+ let!(:job_variable_1) { create(:ci_job_variable, :dotenv_source, job: prepare1) }
+ let!(:job_variable_2) { create(:ci_job_variable, job: prepare1) }
+ let!(:job_variable_3) { create(:ci_job_variable, :dotenv_source, job: prepare2) }
+
+ it 'inherits only dependent variables' do
+ expect(subject.to_hash).to eq(job_variable_1.key => job_variable_1.value)
+ end
+ end
+
+ context 'when using needs' do
+ let!(:prepare1) { create(:ci_build, name: 'prepare1', pipeline: pipeline, stage_idx: 0) }
+ let!(:prepare2) { create(:ci_build, name: 'prepare2', pipeline: pipeline, stage_idx: 0) }
+ let!(:prepare3) { create(:ci_build, name: 'prepare3', pipeline: pipeline, stage_idx: 0) }
+ let!(:build) { create(:ci_build, pipeline: pipeline, stage_idx: 1, scheduling_type: 'dag') }
+ let!(:build_needs_prepare1) { create(:ci_build_need, build: build, name: 'prepare1', artifacts: true) }
+ let!(:build_needs_prepare2) { create(:ci_build_need, build: build, name: 'prepare2', artifacts: false) }
+
+ let!(:job_variable_1) { create(:ci_job_variable, :dotenv_source, job: prepare1) }
+ let!(:job_variable_2) { create(:ci_job_variable, :dotenv_source, job: prepare2) }
+ let!(:job_variable_3) { create(:ci_job_variable, :dotenv_source, job: prepare3) }
+
+ it 'inherits only needs with artifacts variables' do
+ expect(subject.to_hash).to eq(job_variable_1.key => job_variable_1.value)
+ end
+ end
+ end
+
describe 'state transition: any => [:preparing]' do
let(:build) { create(:ci_build, :created) }
@@ -3822,8 +3908,68 @@ describe Ci::Build do
create(:ci_job_artifact, :junit_with_corrupted_data, job: build, project: build.project)
end
- it 'raises an error' do
- expect { subject }.to raise_error(Gitlab::Ci::Parsers::Test::Junit::JunitParserError)
+ it 'returns no test data and includes a suite_error message' do
+ expect { subject }.not_to raise_error
+
+ expect(test_reports.get_suite(build.name).total_count).to eq(0)
+ expect(test_reports.get_suite(build.name).success_count).to eq(0)
+ expect(test_reports.get_suite(build.name).failed_count).to eq(0)
+ expect(test_reports.get_suite(build.name).suite_error).to eq('JUnit XML parsing failed: 1:1: FATAL: Document is empty')
+ end
+ end
+ end
+ end
+
+ describe '#collect_accessibility_reports!' do
+ subject { build.collect_accessibility_reports!(accessibility_report) }
+
+ let(:accessibility_report) { Gitlab::Ci::Reports::AccessibilityReports.new }
+
+ it { expect(accessibility_report.urls).to eq({}) }
+
+ context 'when build has an accessibility report' do
+ context 'when there is an accessibility report with errors' do
+ before do
+ create(:ci_job_artifact, :accessibility, job: build, project: build.project)
+ end
+
+ it 'parses blobs and add the results to the accessibility report' do
+ expect { subject }.not_to raise_error
+
+ expect(accessibility_report.urls.keys).to match_array(['https://about.gitlab.com/'])
+ expect(accessibility_report.errors_count).to eq(10)
+ expect(accessibility_report.scans_count).to eq(1)
+ expect(accessibility_report.passes_count).to eq(0)
+ end
+ end
+
+ context 'when there is an accessibility report without errors' do
+ before do
+ create(:ci_job_artifact, :accessibility_without_errors, job: build, project: build.project)
+ end
+
+ it 'parses blobs and add the results to the accessibility report' do
+ expect { subject }.not_to raise_error
+
+ expect(accessibility_report.urls.keys).to match_array(['https://pa11y.org/'])
+ expect(accessibility_report.errors_count).to eq(0)
+ expect(accessibility_report.scans_count).to eq(1)
+ expect(accessibility_report.passes_count).to eq(1)
+ end
+ end
+
+ context 'when there is an accessibility report with an invalid url' do
+ before do
+ create(:ci_job_artifact, :accessibility_with_invalid_url, job: build, project: build.project)
+ end
+
+ it 'parses blobs and add the results to the accessibility report' do
+ expect { subject }.not_to raise_error
+
+ expect(accessibility_report.urls).to be_empty
+ expect(accessibility_report.errors_count).to eq(0)
+ expect(accessibility_report.scans_count).to eq(0)
+ expect(accessibility_report.passes_count).to eq(0)
end
end
end
@@ -3876,6 +4022,48 @@ describe Ci::Build do
end
end
+ describe '#collect_terraform_reports!' do
+ let(:terraform_reports) { Gitlab::Ci::Reports::TerraformReports.new }
+
+ it 'returns an empty hash' do
+ expect(build.collect_terraform_reports!(terraform_reports).plans).to eq({})
+ end
+
+ context 'when build has a terraform report' do
+ context 'when there is a valid tfplan.json' do
+ before do
+ create(:ci_job_artifact, :terraform, job: build, project: build.project)
+ end
+
+ it 'parses blobs and add the results to the terraform report' do
+ expect { build.collect_terraform_reports!(terraform_reports) }.not_to raise_error
+
+ expect(terraform_reports.plans).to match(
+ a_hash_including(
+ 'tfplan.json' => a_hash_including(
+ 'create' => 0,
+ 'update' => 1,
+ 'delete' => 0
+ )
+ )
+ )
+ end
+ end
+
+ context 'when there is an invalid tfplan.json' do
+ before do
+ create(:ci_job_artifact, :terraform_with_corrupted_data, job: build, project: build.project)
+ end
+
+ it 'raises an error' do
+ expect { build.collect_terraform_reports!(terraform_reports) }.to raise_error(
+ Gitlab::Ci::Parsers::Terraform::Tfplan::TfplanParserError
+ )
+ end
+ end
+ end
+ end
+
describe '#report_artifacts' do
subject { build.report_artifacts }
@@ -3986,6 +4174,28 @@ describe Ci::Build do
it { is_expected.to include(:upload_multiple_artifacts) }
end
+
+ context 'when artifacts exclude is defined and the is feature enabled' do
+ let(:options) do
+ { artifacts: { exclude: %w[something] } }
+ end
+
+ context 'when a feature flag is enabled' do
+ before do
+ stub_feature_flags(ci_artifacts_exclude: true)
+ end
+
+ it { is_expected.to include(:artifacts_exclude) }
+ end
+
+ context 'when a feature flag is disabled' do
+ before do
+ stub_feature_flags(ci_artifacts_exclude: false)
+ end
+
+ it { is_expected.not_to include(:artifacts_exclude) }
+ end
+ end
end
describe '#supported_runner?' do
@@ -4312,4 +4522,31 @@ describe Ci::Build do
it { is_expected.to be_nil }
end
end
+
+ describe '#degradation_threshold' do
+ subject { build.degradation_threshold }
+
+ context 'when threshold variable is defined' do
+ before do
+ build.yaml_variables = [
+ { key: 'SOME_VAR_1', value: 'SOME_VAL_1' },
+ { key: 'DEGRADATION_THRESHOLD', value: '5' },
+ { key: 'SOME_VAR_2', value: 'SOME_VAL_2' }
+ ]
+ end
+
+ it { is_expected.to eq(5) }
+ end
+
+ context 'when threshold variable is not defined' do
+ before do
+ build.yaml_variables = [
+ { key: 'SOME_VAR_1', value: 'SOME_VAL_1' },
+ { key: 'SOME_VAR_2', value: 'SOME_VAL_2' }
+ ]
+ end
+
+ it { is_expected.to be_nil }
+ end
+ end
end
diff --git a/spec/models/ci/daily_build_group_report_result_spec.rb b/spec/models/ci/daily_build_group_report_result_spec.rb
new file mode 100644
index 00000000000..d4c305c649a
--- /dev/null
+++ b/spec/models/ci/daily_build_group_report_result_spec.rb
@@ -0,0 +1,59 @@
+# frozen_string_literal: true
+
+require 'spec_helper'
+
+describe Ci::DailyBuildGroupReportResult do
+ describe '.upsert_reports' do
+ let!(:rspec_coverage) do
+ create(
+ :ci_daily_build_group_report_result,
+ group_name: 'rspec',
+ date: '2020-03-09',
+ data: { coverage: 71.2 }
+ )
+ end
+ let!(:new_pipeline) { create(:ci_pipeline) }
+
+ it 'creates or updates matching report results' do
+ described_class.upsert_reports([
+ {
+ project_id: rspec_coverage.project_id,
+ ref_path: rspec_coverage.ref_path,
+ last_pipeline_id: new_pipeline.id,
+ date: rspec_coverage.date,
+ group_name: 'rspec',
+ data: { 'coverage' => 81.0 }
+ },
+ {
+ project_id: rspec_coverage.project_id,
+ ref_path: rspec_coverage.ref_path,
+ last_pipeline_id: new_pipeline.id,
+ date: rspec_coverage.date,
+ group_name: 'karma',
+ data: { 'coverage' => 87.0 }
+ }
+ ])
+
+ rspec_coverage.reload
+
+ expect(rspec_coverage).to have_attributes(
+ last_pipeline_id: new_pipeline.id,
+ data: { 'coverage' => 81.0 }
+ )
+
+ expect(described_class.find_by_group_name('karma')).to have_attributes(
+ project_id: rspec_coverage.project_id,
+ ref_path: rspec_coverage.ref_path,
+ last_pipeline_id: new_pipeline.id,
+ date: rspec_coverage.date,
+ data: { 'coverage' => 87.0 }
+ )
+ end
+
+ context 'when given data is empty' do
+ it 'does nothing' do
+ expect { described_class.upsert_reports([]) }.not_to raise_error
+ end
+ end
+ end
+end
diff --git a/spec/models/ci/daily_report_result_spec.rb b/spec/models/ci/daily_report_result_spec.rb
deleted file mode 100644
index 61aa58c6692..00000000000
--- a/spec/models/ci/daily_report_result_spec.rb
+++ /dev/null
@@ -1,62 +0,0 @@
-# frozen_string_literal: true
-
-require 'spec_helper'
-
-describe Ci::DailyReportResult do
- describe '.upsert_reports' do
- let!(:rspec_coverage) do
- create(
- :ci_daily_report_result,
- title: 'rspec',
- date: '2020-03-09',
- value: 71.2
- )
- end
- let!(:new_pipeline) { create(:ci_pipeline) }
-
- it 'creates or updates matching report results' do
- described_class.upsert_reports([
- {
- project_id: rspec_coverage.project_id,
- ref_path: rspec_coverage.ref_path,
- param_type: described_class.param_types[rspec_coverage.param_type],
- last_pipeline_id: new_pipeline.id,
- date: rspec_coverage.date,
- title: 'rspec',
- value: 81.0
- },
- {
- project_id: rspec_coverage.project_id,
- ref_path: rspec_coverage.ref_path,
- param_type: described_class.param_types[rspec_coverage.param_type],
- last_pipeline_id: new_pipeline.id,
- date: rspec_coverage.date,
- title: 'karma',
- value: 87.0
- }
- ])
-
- rspec_coverage.reload
-
- expect(rspec_coverage).to have_attributes(
- last_pipeline_id: new_pipeline.id,
- value: 81.0
- )
-
- expect(described_class.find_by_title('karma')).to have_attributes(
- project_id: rspec_coverage.project_id,
- ref_path: rspec_coverage.ref_path,
- param_type: rspec_coverage.param_type,
- last_pipeline_id: new_pipeline.id,
- date: rspec_coverage.date,
- value: 87.0
- )
- end
-
- context 'when given data is empty' do
- it 'does nothing' do
- expect { described_class.upsert_reports([]) }.not_to raise_error
- end
- end
- end
-end
diff --git a/spec/models/ci/freeze_period_spec.rb b/spec/models/ci/freeze_period_spec.rb
new file mode 100644
index 00000000000..f7f840c6696
--- /dev/null
+++ b/spec/models/ci/freeze_period_spec.rb
@@ -0,0 +1,50 @@
+# frozen_string_literal: true
+
+require 'spec_helper'
+
+RSpec.describe Ci::FreezePeriod, type: :model do
+ subject { build(:ci_freeze_period) }
+
+ let(:invalid_cron) { '0 0 0 * *' }
+
+ it { is_expected.to belong_to(:project) }
+
+ it { is_expected.to respond_to(:freeze_start) }
+ it { is_expected.to respond_to(:freeze_end) }
+ it { is_expected.to respond_to(:cron_timezone) }
+
+ describe 'cron validations' do
+ it 'allows valid cron patterns' do
+ freeze_period = build(:ci_freeze_period)
+
+ expect(freeze_period).to be_valid
+ end
+
+ it 'does not allow invalid cron patterns on freeze_start' do
+ freeze_period = build(:ci_freeze_period, freeze_start: invalid_cron)
+
+ expect(freeze_period).not_to be_valid
+ end
+
+ it 'does not allow invalid cron patterns on freeze_end' do
+ freeze_period = build(:ci_freeze_period, freeze_end: invalid_cron)
+
+ expect(freeze_period).not_to be_valid
+ end
+
+ it 'does not allow an invalid timezone' do
+ freeze_period = build(:ci_freeze_period, cron_timezone: 'invalid')
+
+ expect(freeze_period).not_to be_valid
+ end
+
+ context 'when cron contains trailing whitespaces' do
+ it 'strips the attribute' do
+ freeze_period = build(:ci_freeze_period, freeze_start: ' 0 0 * * * ')
+
+ expect(freeze_period).to be_valid
+ expect(freeze_period.freeze_start).to eq('0 0 * * *')
+ end
+ end
+ end
+end
diff --git a/spec/models/ci/freeze_period_status_spec.rb b/spec/models/ci/freeze_period_status_spec.rb
new file mode 100644
index 00000000000..b700ec8c45f
--- /dev/null
+++ b/spec/models/ci/freeze_period_status_spec.rb
@@ -0,0 +1,62 @@
+# frozen_string_literal: true
+require 'spec_helper'
+
+describe Ci::FreezePeriodStatus do
+ let(:project) { create :project }
+ # '0 23 * * 5' == "At 23:00 on Friday."", '0 7 * * 1' == "At 07:00 on Monday.""
+ let(:friday_2300) { '0 23 * * 5' }
+ let(:monday_0700) { '0 7 * * 1' }
+
+ subject { described_class.new(project: project).execute }
+
+ shared_examples 'within freeze period' do |time|
+ it 'is frozen' do
+ Timecop.freeze(time) do
+ expect(subject).to be_truthy
+ end
+ end
+ end
+
+ shared_examples 'outside freeze period' do |time|
+ it 'is not frozen' do
+ Timecop.freeze(time) do
+ expect(subject).to be_falsy
+ end
+ end
+ end
+
+ describe 'single freeze period' do
+ let!(:freeze_period) { create(:ci_freeze_period, project: project, freeze_start: friday_2300, freeze_end: monday_0700) }
+
+ it_behaves_like 'outside freeze period', Time.utc(2020, 4, 10, 22, 59)
+
+ it_behaves_like 'within freeze period', Time.utc(2020, 4, 10, 23, 1)
+
+ it_behaves_like 'within freeze period', Time.utc(2020, 4, 13, 6, 59)
+
+ it_behaves_like 'outside freeze period', Time.utc(2020, 4, 13, 7, 1)
+ end
+
+ describe 'multiple freeze periods' do
+ # '30 23 * * 5' == "At 23:30 on Friday."", '0 8 * * 1' == "At 08:00 on Monday.""
+ let(:friday_2330) { '30 23 * * 5' }
+ let(:monday_0800) { '0 8 * * 1' }
+
+ let!(:freeze_period_1) { create(:ci_freeze_period, project: project, freeze_start: friday_2300, freeze_end: monday_0700) }
+ let!(:freeze_period_2) { create(:ci_freeze_period, project: project, freeze_start: friday_2330, freeze_end: monday_0800) }
+
+ it_behaves_like 'outside freeze period', Time.utc(2020, 4, 10, 22, 59)
+
+ it_behaves_like 'within freeze period', Time.utc(2020, 4, 10, 23, 29)
+
+ it_behaves_like 'within freeze period', Time.utc(2020, 4, 11, 10, 0)
+
+ it_behaves_like 'within freeze period', Time.utc(2020, 4, 10, 23, 1)
+
+ it_behaves_like 'within freeze period', Time.utc(2020, 4, 13, 6, 59)
+
+ it_behaves_like 'within freeze period', Time.utc(2020, 4, 13, 7, 59)
+
+ it_behaves_like 'outside freeze period', Time.utc(2020, 4, 13, 8, 1)
+ end
+end
diff --git a/spec/models/ci/instance_variable_spec.rb b/spec/models/ci/instance_variable_spec.rb
new file mode 100644
index 00000000000..ff8676e1424
--- /dev/null
+++ b/spec/models/ci/instance_variable_spec.rb
@@ -0,0 +1,93 @@
+# frozen_string_literal: true
+
+require 'spec_helper'
+
+describe Ci::InstanceVariable do
+ subject { build(:ci_instance_variable) }
+
+ it_behaves_like "CI variable"
+
+ it { is_expected.to include_module(Ci::Maskable) }
+ it { is_expected.to validate_uniqueness_of(:key).with_message(/\(\w+\) has already been taken/) }
+
+ describe '.unprotected' do
+ subject { described_class.unprotected }
+
+ context 'when variable is protected' do
+ before do
+ create(:ci_instance_variable, :protected)
+ end
+
+ it 'returns nothing' do
+ is_expected.to be_empty
+ end
+ end
+
+ context 'when variable is not protected' do
+ let(:variable) { create(:ci_instance_variable, protected: false) }
+
+ it 'returns the variable' do
+ is_expected.to contain_exactly(variable)
+ end
+ end
+ end
+
+ describe '.all_cached', :use_clean_rails_memory_store_caching do
+ let_it_be(:unprotected_variable) { create(:ci_instance_variable, protected: false) }
+ let_it_be(:protected_variable) { create(:ci_instance_variable, protected: true) }
+
+ it { expect(described_class.all_cached).to contain_exactly(protected_variable, unprotected_variable) }
+
+ it 'memoizes the result' do
+ expect(described_class).to receive(:store_cache).with(:ci_instance_variable_data).once.and_call_original
+
+ 2.times do
+ expect(described_class.all_cached).to contain_exactly(protected_variable, unprotected_variable)
+ end
+ end
+
+ it 'removes scopes' do
+ expect(described_class.unprotected.all_cached).to contain_exactly(protected_variable, unprotected_variable)
+ end
+
+ it 'resets the cache when records are deleted' do
+ expect(described_class.all_cached).to contain_exactly(protected_variable, unprotected_variable)
+
+ protected_variable.destroy
+
+ expect(described_class.all_cached).to contain_exactly(unprotected_variable)
+ end
+
+ it 'resets the cache when records are inserted' do
+ expect(described_class.all_cached).to contain_exactly(protected_variable, unprotected_variable)
+
+ variable = create(:ci_instance_variable, protected: true)
+
+ expect(described_class.all_cached).to contain_exactly(protected_variable, unprotected_variable, variable)
+ end
+
+ it 'resets the cache when the shared key is missing' do
+ expect(Rails.cache).to receive(:read).with(:ci_instance_variable_changed_at).twice.and_return(nil)
+ expect(described_class).to receive(:store_cache).with(:ci_instance_variable_data).thrice.and_call_original
+
+ 3.times do
+ expect(described_class.all_cached).to contain_exactly(protected_variable, unprotected_variable)
+ end
+ end
+ end
+
+ describe '.unprotected_cached', :use_clean_rails_memory_store_caching do
+ let_it_be(:unprotected_variable) { create(:ci_instance_variable, protected: false) }
+ let_it_be(:protected_variable) { create(:ci_instance_variable, protected: true) }
+
+ it { expect(described_class.unprotected_cached).to contain_exactly(unprotected_variable) }
+
+ it 'memoizes the result' do
+ expect(described_class).to receive(:store_cache).with(:ci_instance_variable_data).once.and_call_original
+
+ 2.times do
+ expect(described_class.unprotected_cached).to contain_exactly(unprotected_variable)
+ end
+ end
+ end
+end
diff --git a/spec/models/ci/job_artifact_spec.rb b/spec/models/ci/job_artifact_spec.rb
index 6f6ff3704b4..4cdc74d7a41 100644
--- a/spec/models/ci/job_artifact_spec.rb
+++ b/spec/models/ci/job_artifact_spec.rb
@@ -19,24 +19,8 @@ describe Ci::JobArtifact do
it_behaves_like 'having unique enum values'
- context 'with update_project_statistics_after_commit enabled' do
- before do
- stub_feature_flags(update_project_statistics_after_commit: true)
- end
-
- it_behaves_like 'UpdateProjectStatistics' do
- subject { build(:ci_job_artifact, :archive, size: 107464) }
- end
- end
-
- context 'with update_project_statistics_after_commit disabled' do
- before do
- stub_feature_flags(update_project_statistics_after_commit: false)
- end
-
- it_behaves_like 'UpdateProjectStatistics' do
- subject { build(:ci_job_artifact, :archive, size: 107464) }
- end
+ it_behaves_like 'UpdateProjectStatistics' do
+ subject { build(:ci_job_artifact, :archive, size: 107464) }
end
describe '.with_reports' do
@@ -70,6 +54,22 @@ describe Ci::JobArtifact do
end
end
+ describe '.accessibility_reports' do
+ subject { described_class.accessibility_reports }
+
+ context 'when there is an accessibility report' do
+ let(:artifact) { create(:ci_job_artifact, :accessibility) }
+
+ it { is_expected.to eq([artifact]) }
+ end
+
+ context 'when there are no accessibility report' do
+ let(:artifact) { create(:ci_job_artifact, :archive) }
+
+ it { is_expected.to be_empty }
+ end
+ end
+
describe '.coverage_reports' do
subject { described_class.coverage_reports }
@@ -86,6 +86,22 @@ describe Ci::JobArtifact do
end
end
+ describe '.terraform_reports' do
+ context 'when there is a terraform report' do
+ it 'return the job artifact' do
+ artifact = create(:ci_job_artifact, :terraform)
+
+ expect(described_class.terraform_reports).to eq([artifact])
+ end
+ end
+
+ context 'when there are no terraform reports' do
+ it 'return the an empty array' do
+ expect(described_class.terraform_reports).to eq([])
+ end
+ end
+ end
+
describe '.erasable' do
subject { described_class.erasable }
@@ -128,15 +144,26 @@ describe Ci::JobArtifact do
end
describe '.for_sha' do
+ let(:first_pipeline) { create(:ci_pipeline) }
+ let(:second_pipeline) { create(:ci_pipeline, project: first_pipeline.project, sha: Digest::SHA1.hexdigest(SecureRandom.hex)) }
+ let!(:first_artifact) { create(:ci_job_artifact, job: create(:ci_build, pipeline: first_pipeline)) }
+ let!(:second_artifact) { create(:ci_job_artifact, job: create(:ci_build, pipeline: second_pipeline)) }
+
it 'returns job artifacts for a given pipeline sha' do
- project = create(:project)
- first_pipeline = create(:ci_pipeline, project: project)
- second_pipeline = create(:ci_pipeline, project: project, sha: Digest::SHA1.hexdigest(SecureRandom.hex))
- first_artifact = create(:ci_job_artifact, job: create(:ci_build, pipeline: first_pipeline))
- second_artifact = create(:ci_job_artifact, job: create(:ci_build, pipeline: second_pipeline))
+ expect(described_class.for_sha(first_pipeline.sha, first_pipeline.project.id)).to eq([first_artifact])
+ expect(described_class.for_sha(second_pipeline.sha, first_pipeline.project.id)).to eq([second_artifact])
+ end
+ end
- expect(described_class.for_sha(first_pipeline.sha, project.id)).to eq([first_artifact])
- expect(described_class.for_sha(second_pipeline.sha, project.id)).to eq([second_artifact])
+ describe '.for_ref' do
+ let(:first_pipeline) { create(:ci_pipeline, ref: 'first_ref') }
+ let(:second_pipeline) { create(:ci_pipeline, ref: 'second_ref', project: first_pipeline.project) }
+ let!(:first_artifact) { create(:ci_job_artifact, job: create(:ci_build, pipeline: first_pipeline)) }
+ let!(:second_artifact) { create(:ci_job_artifact, job: create(:ci_build, pipeline: second_pipeline)) }
+
+ it 'returns job artifacts for a given pipeline ref' do
+ expect(described_class.for_ref(first_pipeline.ref, first_pipeline.project.id)).to eq([first_artifact])
+ expect(described_class.for_ref(second_pipeline.ref, first_pipeline.project.id)).to eq([second_artifact])
end
end
@@ -153,9 +180,9 @@ describe Ci::JobArtifact do
end
describe 'callbacks' do
- subject { create(:ci_job_artifact, :archive) }
-
describe '#schedule_background_upload' do
+ subject { create(:ci_job_artifact, :archive) }
+
context 'when object storage is disabled' do
before do
stub_artifacts_object_storage(enabled: false)
@@ -212,9 +239,35 @@ describe Ci::JobArtifact do
end
end
+ describe 'validates if file format is supported' do
+ subject { artifact }
+
+ let(:artifact) { build(:ci_job_artifact, file_type: :license_management, file_format: :raw) }
+
+ context 'when license_management is supported' do
+ before do
+ stub_feature_flags(drop_license_management_artifact: false)
+ end
+
+ it { is_expected.to be_valid }
+ end
+
+ context 'when license_management is not supported' do
+ before do
+ stub_feature_flags(drop_license_management_artifact: true)
+ end
+
+ it { is_expected.not_to be_valid }
+ end
+ end
+
describe 'validates file format' do
subject { artifact }
+ before do
+ stub_feature_flags(drop_license_management_artifact: false)
+ end
+
described_class::TYPE_AND_FORMAT_PAIRS.except(:trace).each do |file_type, file_format|
context "when #{file_type} type with #{file_format} format" do
let(:artifact) { build(:ci_job_artifact, file_type: file_type, file_format: file_format) }
@@ -351,19 +404,6 @@ describe Ci::JobArtifact do
describe 'file is being stored' do
subject { create(:ci_job_artifact, :archive) }
- context 'when object has nil store' do
- before do
- subject.update_column(:file_store, nil)
- subject.reload
- end
-
- it 'is stored locally' do
- expect(subject.file_store).to be(nil)
- expect(subject.file).to be_file_storage
- expect(subject.file.object_store).to eq(ObjectStorage::Store::LOCAL)
- end
- end
-
context 'when existing object has local store' do
it 'is stored locally' do
expect(subject.file_store).to be(ObjectStorage::Store::LOCAL)
diff --git a/spec/models/ci/persistent_ref_spec.rb b/spec/models/ci/persistent_ref_spec.rb
index 4cece0664cf..89dd9b05331 100644
--- a/spec/models/ci/persistent_ref_spec.rb
+++ b/spec/models/ci/persistent_ref_spec.rb
@@ -45,18 +45,6 @@ describe Ci::PersistentRef do
expect(pipeline.persistent_ref).to be_exist
end
- context 'when depend_on_persistent_pipeline_ref feature flag is disabled' do
- before do
- stub_feature_flags(depend_on_persistent_pipeline_ref: false)
- end
-
- it 'does not create a persistent ref' do
- expect(project.repository).not_to receive(:create_ref)
-
- subject
- end
- end
-
context 'when sha does not exist in the repository' do
let(:sha) { 'not-exist' }
diff --git a/spec/models/ci/pipeline_schedule_spec.rb b/spec/models/ci/pipeline_schedule_spec.rb
index 4ed4b7e38d8..9a10c7629b2 100644
--- a/spec/models/ci/pipeline_schedule_spec.rb
+++ b/spec/models/ci/pipeline_schedule_spec.rb
@@ -17,14 +17,18 @@ describe Ci::PipelineSchedule do
it { is_expected.to respond_to(:description) }
it { is_expected.to respond_to(:next_run_at) }
+ it_behaves_like 'includes Limitable concern' do
+ subject { build(:ci_pipeline_schedule) }
+ end
+
describe 'validations' do
- it 'does not allow invalid cron patters' do
+ it 'does not allow invalid cron patterns' do
pipeline_schedule = build(:ci_pipeline_schedule, cron: '0 0 0 * *')
expect(pipeline_schedule).not_to be_valid
end
- it 'does not allow invalid cron patters' do
+ it 'does not allow invalid cron patterns' do
pipeline_schedule = build(:ci_pipeline_schedule, cron_timezone: 'invalid')
expect(pipeline_schedule).not_to be_valid
diff --git a/spec/models/ci/pipeline_spec.rb b/spec/models/ci/pipeline_spec.rb
index 90412136c1d..4f53b6b4418 100644
--- a/spec/models/ci/pipeline_spec.rb
+++ b/spec/models/ci/pipeline_spec.rb
@@ -53,6 +53,29 @@ describe Ci::Pipeline, :mailer do
end
end
+ describe '#set_status' do
+ where(:from_status, :to_status) do
+ from_status_names = described_class.state_machines[:status].states.map(&:name)
+ to_status_names = from_status_names - [:created] # we never want to transition into created
+
+ from_status_names.product(to_status_names)
+ end
+
+ with_them do
+ it do
+ pipeline.status = from_status.to_s
+
+ if from_status != to_status
+ expect(pipeline.set_status(to_status.to_s))
+ .to eq(true)
+ else
+ expect(pipeline.set_status(to_status.to_s))
+ .to eq(false), "loopback transitions are not allowed"
+ end
+ end
+ end
+ end
+
describe '.processables' do
before do
create(:ci_build, name: 'build', pipeline: pipeline)
@@ -364,6 +387,26 @@ describe Ci::Pipeline, :mailer do
end
end
+ context 'when pipeline has an accessibility report' do
+ subject { described_class.with_reports(Ci::JobArtifact.accessibility_reports) }
+
+ let(:pipeline_with_report) { create(:ci_pipeline, :with_accessibility_reports) }
+
+ it 'selects the pipeline' do
+ is_expected.to eq([pipeline_with_report])
+ end
+ end
+
+ context 'when pipeline has a terraform report' do
+ it 'selects the pipeline' do
+ pipeline_with_report = create(:ci_pipeline, :with_terraform_reports)
+
+ expect(described_class.with_reports(Ci::JobArtifact.terraform_reports)).to eq(
+ [pipeline_with_report]
+ )
+ end
+ end
+
context 'when pipeline does not have metrics reports' do
subject { described_class.with_reports(Ci::JobArtifact.test_reports) }
@@ -699,6 +742,28 @@ describe Ci::Pipeline, :mailer do
)
end
end
+
+ describe 'variable CI_KUBERNETES_ACTIVE' do
+ context 'when pipeline.has_kubernetes_active? is true' do
+ before do
+ allow(pipeline).to receive(:has_kubernetes_active?).and_return(true)
+ end
+
+ it "is included with value 'true'" do
+ expect(subject.to_hash).to include('CI_KUBERNETES_ACTIVE' => 'true')
+ end
+ end
+
+ context 'when pipeline.has_kubernetes_active? is false' do
+ before do
+ allow(pipeline).to receive(:has_kubernetes_active?).and_return(false)
+ end
+
+ it 'is not included' do
+ expect(subject.to_hash).not_to have_key('CI_KUBERNETES_ACTIVE')
+ end
+ end
+ end
end
describe '#protected_ref?' do
@@ -944,7 +1009,10 @@ describe Ci::Pipeline, :mailer do
context 'when using legacy stages' do
before do
- stub_feature_flags(ci_pipeline_persisted_stages: false)
+ stub_feature_flags(
+ ci_pipeline_persisted_stages: false,
+ ci_atomic_processing: false
+ )
end
it 'returns legacy stages in valid order' do
@@ -952,9 +1020,40 @@ describe Ci::Pipeline, :mailer do
end
end
+ context 'when using atomic processing' do
+ before do
+ stub_feature_flags(
+ ci_atomic_processing: true
+ )
+ end
+
+ context 'when pipelines is not complete' do
+ it 'returns stages in valid order' do
+ expect(subject).to all(be_a Ci::Stage)
+ expect(subject.map(&:name))
+ .to eq %w[sanity build test deploy cleanup]
+ end
+ end
+
+ context 'when pipeline is complete' do
+ before do
+ pipeline.succeed!
+ end
+
+ it 'returns stages in valid order' do
+ expect(subject).to all(be_a Ci::Stage)
+ expect(subject.map(&:name))
+ .to eq %w[sanity build test deploy cleanup]
+ end
+ end
+ end
+
context 'when using persisted stages' do
before do
- stub_feature_flags(ci_pipeline_persisted_stages: true)
+ stub_feature_flags(
+ ci_pipeline_persisted_stages: true,
+ ci_atomic_processing: false
+ )
end
context 'when pipelines is not complete' do
@@ -1119,8 +1218,8 @@ describe Ci::Pipeline, :mailer do
context "from #{status}" do
let(:from_status) { status }
- it 'schedules pipeline success worker' do
- expect(Ci::DailyReportResultsWorker).to receive(:perform_in).with(10.minutes, pipeline.id)
+ it 'schedules daily build group report results worker' do
+ expect(Ci::DailyBuildGroupReportResultsWorker).to receive(:perform_in).with(10.minutes, pipeline.id)
pipeline.succeed
end
@@ -2307,7 +2406,7 @@ describe Ci::Pipeline, :mailer do
def have_requested_pipeline_hook(status)
have_requested(:post, stubbed_hostname(hook.url)).with do |req|
- json_body = JSON.parse(req.body)
+ json_body = Gitlab::Json.parse(req.body)
json_body['object_attributes']['status'] == status &&
json_body['builds'].length == 2
end
@@ -2755,6 +2854,42 @@ describe Ci::Pipeline, :mailer do
end
end
+ describe '#accessibility_reports' do
+ subject { pipeline.accessibility_reports }
+
+ context 'when pipeline has multiple builds with accessibility reports' do
+ let(:build_rspec) { create(:ci_build, :success, name: 'rspec', pipeline: pipeline, project: project) }
+ let(:build_golang) { create(:ci_build, :success, name: 'golang', pipeline: pipeline, project: project) }
+
+ before do
+ create(:ci_job_artifact, :accessibility, job: build_rspec, project: project)
+ create(:ci_job_artifact, :accessibility_without_errors, job: build_golang, project: project)
+ end
+
+ it 'returns accessibility report with collected data' do
+ expect(subject.urls.keys).to match_array([
+ "https://pa11y.org/",
+ "https://about.gitlab.com/"
+ ])
+ end
+
+ context 'when builds are retried' do
+ let(:build_rspec) { create(:ci_build, :retried, :success, name: 'rspec', pipeline: pipeline, project: project) }
+ let(:build_golang) { create(:ci_build, :retried, :success, name: 'golang', pipeline: pipeline, project: project) }
+
+ it 'returns empty urls for accessibility reports' do
+ expect(subject.urls).to be_empty
+ end
+ end
+ end
+
+ context 'when pipeline does not have any builds with accessibility reports' do
+ it 'returns empty urls for accessibility reports' do
+ expect(subject.urls).to be_empty
+ end
+ end
+ end
+
describe '#coverage_reports' do
subject { pipeline.coverage_reports }
diff --git a/spec/models/ci/processable_spec.rb b/spec/models/ci/processable_spec.rb
index 4490371bde5..e67f740279b 100644
--- a/spec/models/ci/processable_spec.rb
+++ b/spec/models/ci/processable_spec.rb
@@ -6,16 +6,12 @@ describe Ci::Processable do
let_it_be(:project) { create(:project) }
let_it_be(:pipeline) { create(:ci_pipeline, project: project) }
- let_it_be(:detached_merge_request_pipeline) do
- create(:ci_pipeline, :detached_merge_request_pipeline, :with_job, project: project)
- end
-
- let_it_be(:legacy_detached_merge_request_pipeline) do
- create(:ci_pipeline, :legacy_detached_merge_request_pipeline, :with_job, project: project)
- end
+ describe 'delegations' do
+ subject { Ci::Processable.new }
- let_it_be(:merged_result_pipeline) do
- create(:ci_pipeline, :merged_result_pipeline, :with_job, project: project)
+ it { is_expected.to delegate_method(:merge_request?).to(:pipeline) }
+ it { is_expected.to delegate_method(:merge_request_ref?).to(:pipeline) }
+ it { is_expected.to delegate_method(:legacy_detached_merge_request_pipeline?).to(:pipeline) }
end
describe '#aggregated_needs_names' do
@@ -52,69 +48,28 @@ describe Ci::Processable do
end
describe 'validate presence of scheduling_type' do
- context 'on create' do
- let(:processable) do
- build(
- :ci_build, :created, project: project, pipeline: pipeline,
- importing: importing, scheduling_type: nil
- )
- end
-
- context 'when importing' do
- let(:importing) { true }
-
- context 'when validate_scheduling_type_of_processables is true' do
- before do
- stub_feature_flags(validate_scheduling_type_of_processables: true)
- end
+ using RSpec::Parameterized::TableSyntax
- it 'does not validate' do
- expect(processable).to be_valid
- end
- end
-
- context 'when validate_scheduling_type_of_processables is false' do
- before do
- stub_feature_flags(validate_scheduling_type_of_processables: false)
- end
-
- it 'does not validate' do
- expect(processable).to be_valid
- end
- end
- end
+ subject { build(:ci_build, project: project, pipeline: pipeline, importing: importing) }
- context 'when not importing' do
- let(:importing) { false }
-
- context 'when validate_scheduling_type_of_processables is true' do
- before do
- stub_feature_flags(validate_scheduling_type_of_processables: true)
- end
-
- it 'validates' do
- expect(processable).not_to be_valid
- end
- end
-
- context 'when validate_scheduling_type_of_processables is false' do
- before do
- stub_feature_flags(validate_scheduling_type_of_processables: false)
- end
+ where(:importing, :should_validate) do
+ false | true
+ true | false
+ end
- it 'does not validate' do
- expect(processable).to be_valid
+ with_them do
+ context 'on create' do
+ it 'validates presence' do
+ if should_validate
+ is_expected.to validate_presence_of(:scheduling_type).on(:create)
+ else
+ is_expected.not_to validate_presence_of(:scheduling_type).on(:create)
end
end
end
- end
-
- context 'on update' do
- let(:processable) { create(:ci_build, :created, project: project, pipeline: pipeline) }
- it 'does not validate' do
- processable.scheduling_type = nil
- expect(processable).to be_valid
+ context 'on update' do
+ it { is_expected.not_to validate_presence_of(:scheduling_type).on(:update) }
end
end
end
@@ -147,6 +102,8 @@ describe Ci::Processable do
describe '#needs_attributes' do
let(:build) { create(:ci_build, :created, project: project, pipeline: pipeline) }
+ subject { build.needs_attributes }
+
context 'with needs' do
before do
create(:ci_build_need, build: build, name: 'test1')
@@ -154,7 +111,7 @@ describe Ci::Processable do
end
it 'returns all needs attributes' do
- expect(build.needs_attributes).to contain_exactly(
+ is_expected.to contain_exactly(
{ 'artifacts' => true, 'name' => 'test1' },
{ 'artifacts' => true, 'name' => 'test2' }
)
@@ -162,75 +119,7 @@ describe Ci::Processable do
end
context 'without needs' do
- it 'returns all needs attributes' do
- expect(build.needs_attributes).to be_empty
- end
- end
- end
-
- describe '#merge_request?' do
- subject { pipeline.processables.first.merge_request? }
-
- context 'in a detached merge request pipeline' do
- let(:pipeline) { detached_merge_request_pipeline }
-
- it { is_expected.to eq(pipeline.merge_request?) }
- end
-
- context 'in a legacy detached merge_request_pipeline' do
- let(:pipeline) { legacy_detached_merge_request_pipeline }
-
- it { is_expected.to eq(pipeline.merge_request?) }
- end
-
- context 'in a pipeline for merged results' do
- let(:pipeline) { merged_result_pipeline }
-
- it { is_expected.to eq(pipeline.merge_request?) }
- end
- end
-
- describe '#merge_request_ref?' do
- subject { pipeline.processables.first.merge_request_ref? }
-
- context 'in a detached merge request pipeline' do
- let(:pipeline) { detached_merge_request_pipeline }
-
- it { is_expected.to eq(pipeline.merge_request_ref?) }
- end
-
- context 'in a legacy detached merge_request_pipeline' do
- let(:pipeline) { legacy_detached_merge_request_pipeline }
-
- it { is_expected.to eq(pipeline.merge_request_ref?) }
- end
-
- context 'in a pipeline for merged results' do
- let(:pipeline) { merged_result_pipeline }
-
- it { is_expected.to eq(pipeline.merge_request_ref?) }
- end
- end
-
- describe '#legacy_detached_merge_request_pipeline?' do
- subject { pipeline.processables.first.legacy_detached_merge_request_pipeline? }
-
- context 'in a detached merge request pipeline' do
- let(:pipeline) { detached_merge_request_pipeline }
-
- it { is_expected.to eq(pipeline.legacy_detached_merge_request_pipeline?) }
- end
-
- context 'in a legacy detached merge_request_pipeline' do
- let(:pipeline) { legacy_detached_merge_request_pipeline }
-
- it { is_expected.to eq(pipeline.legacy_detached_merge_request_pipeline?) }
- end
-
- context 'in a pipeline for merged results' do
- let(:pipeline) { merged_result_pipeline }
-
- it { is_expected.to eq(pipeline.legacy_detached_merge_request_pipeline?) }
+ it { is_expected.to be_empty }
end
end
end
diff --git a/spec/models/ci/runner_spec.rb b/spec/models/ci/runner_spec.rb
index 2dedff7f15b..8b6a4fa6ade 100644
--- a/spec/models/ci/runner_spec.rb
+++ b/spec/models/ci/runner_spec.rb
@@ -270,7 +270,7 @@ describe Ci::Runner do
it { is_expected.to eq([@runner2])}
end
- describe '#online?' do
+ describe '#online?', :clean_gitlab_redis_cache do
let(:runner) { create(:ci_runner, :instance) }
subject { runner.online? }
@@ -332,7 +332,7 @@ describe Ci::Runner do
end
def stub_redis_runner_contacted_at(value)
- Gitlab::Redis::SharedState.with do |redis|
+ Gitlab::Redis::Cache.with do |redis|
cache_key = runner.send(:cache_attribute_key)
expect(redis).to receive(:get).with(cache_key)
.and_return({ contacted_at: value }.to_json).at_least(:once)
@@ -640,7 +640,7 @@ describe Ci::Runner do
end
def expect_redis_update
- Gitlab::Redis::SharedState.with do |redis|
+ Gitlab::Redis::Cache.with do |redis|
redis_key = runner.send(:cache_attribute_key)
expect(redis).to receive(:set).with(redis_key, anything, any_args)
end
@@ -664,7 +664,7 @@ describe Ci::Runner do
end
it 'cleans up the queue' do
- Gitlab::Redis::SharedState.with do |redis|
+ Gitlab::Redis::Cache.with do |redis|
expect(redis.get(queue_key)).to be_nil
end
end
diff --git a/spec/models/ci/stage_spec.rb b/spec/models/ci/stage_spec.rb
index 3aeaa27abce..a1549532559 100644
--- a/spec/models/ci/stage_spec.rb
+++ b/spec/models/ci/stage_spec.rb
@@ -3,7 +3,8 @@
require 'spec_helper'
describe Ci::Stage, :models do
- let(:stage) { create(:ci_stage_entity) }
+ let_it_be(:pipeline) { create(:ci_empty_pipeline) }
+ let(:stage) { create(:ci_stage_entity, pipeline: pipeline, project: pipeline.project) }
it_behaves_like 'having unique enum values'
@@ -55,6 +56,29 @@ describe Ci::Stage, :models do
end
end
+ describe '#set_status' do
+ where(:from_status, :to_status) do
+ from_status_names = described_class.state_machines[:status].states.map(&:name)
+ to_status_names = from_status_names - [:created] # we never want to transition into created
+
+ from_status_names.product(to_status_names)
+ end
+
+ with_them do
+ it do
+ stage.status = from_status.to_s
+
+ if from_status != to_status
+ expect(stage.set_status(to_status.to_s))
+ .to eq(true)
+ else
+ expect(stage.set_status(to_status.to_s))
+ .to eq(false), "loopback transitions are not allowed"
+ end
+ end
+ end
+ end
+
describe '#update_status' do
context 'when stage objects needs to be updated' do
before do
diff --git a/spec/models/clusters/applications/elastic_stack_spec.rb b/spec/models/clusters/applications/elastic_stack_spec.rb
index b0992c43d11..02ada219e32 100644
--- a/spec/models/clusters/applications/elastic_stack_spec.rb
+++ b/spec/models/clusters/applications/elastic_stack_spec.rb
@@ -19,10 +19,12 @@ describe Clusters::Applications::ElasticStack do
it 'is initialized with elastic stack arguments' do
expect(subject.name).to eq('elastic-stack')
- expect(subject.chart).to eq('stable/elastic-stack')
- expect(subject.version).to eq('1.9.0')
+ expect(subject.chart).to eq('elastic-stack/elastic-stack')
+ expect(subject.version).to eq('3.0.0')
+ expect(subject.repository).to eq('https://charts.gitlab.io')
expect(subject).to be_rbac
expect(subject.files).to eq(elastic_stack.files)
+ expect(subject.preinstall).to be_empty
end
context 'on a non rbac enabled cluster' do
@@ -33,15 +35,75 @@ describe Clusters::Applications::ElasticStack do
it { is_expected.not_to be_rbac }
end
+ context 'on versions older than 2' do
+ before do
+ elastic_stack.status = elastic_stack.status_states[:updating]
+ elastic_stack.version = "1.9.0"
+ end
+
+ it 'includes a preinstall script' do
+ expect(subject.preinstall).not_to be_empty
+ expect(subject.preinstall.first).to include("delete")
+ end
+ end
+
+ context 'on versions older than 3' do
+ before do
+ elastic_stack.status = elastic_stack.status_states[:updating]
+ elastic_stack.version = "2.9.0"
+ end
+
+ it 'includes a preinstall script' do
+ expect(subject.preinstall).not_to be_empty
+ expect(subject.preinstall.first).to include("delete")
+ end
+ end
+
context 'application failed to install previously' do
let(:elastic_stack) { create(:clusters_applications_elastic_stack, :errored, version: '0.0.1') }
it 'is initialized with the locked version' do
- expect(subject.version).to eq('1.9.0')
+ expect(subject.version).to eq('3.0.0')
end
end
end
+ describe '#chart_above_v2?' do
+ let(:elastic_stack) { create(:clusters_applications_elastic_stack, version: version) }
+
+ subject { elastic_stack.chart_above_v2? }
+
+ context 'on v1.9.0' do
+ let(:version) { '1.9.0' }
+
+ it { is_expected.to be_falsy }
+ end
+
+ context 'on v2.0.0' do
+ let(:version) { '2.0.0' }
+
+ it { is_expected.to be_truthy }
+ end
+ end
+
+ describe '#chart_above_v3?' do
+ let(:elastic_stack) { create(:clusters_applications_elastic_stack, version: version) }
+
+ subject { elastic_stack.chart_above_v3? }
+
+ context 'on v1.9.0' do
+ let(:version) { '1.9.0' }
+
+ it { is_expected.to be_falsy }
+ end
+
+ context 'on v3.0.0' do
+ let(:version) { '3.0.0' }
+
+ it { is_expected.to be_truthy }
+ end
+ end
+
describe '#uninstall_command' do
let!(:elastic_stack) { create(:clusters_applications_elastic_stack) }
@@ -57,7 +119,7 @@ describe Clusters::Applications::ElasticStack do
it 'specifies a post delete command to remove custom resource definitions' do
expect(subject.postdelete).to eq([
- 'kubectl delete pvc --selector release\\=elastic-stack'
+ 'kubectl delete pvc --selector app\\=elastic-stack-elasticsearch-master --namespace gitlab-managed-apps'
])
end
end
diff --git a/spec/models/clusters/applications/fluentd_spec.rb b/spec/models/clusters/applications/fluentd_spec.rb
index 7e9680b0ab4..4e9548990ed 100644
--- a/spec/models/clusters/applications/fluentd_spec.rb
+++ b/spec/models/clusters/applications/fluentd_spec.rb
@@ -3,7 +3,9 @@
require 'spec_helper'
describe Clusters::Applications::Fluentd do
- let(:fluentd) { create(:clusters_applications_fluentd) }
+ let(:waf_log_enabled) { true }
+ let(:cilium_log_enabled) { true }
+ let(:fluentd) { create(:clusters_applications_fluentd, waf_log_enabled: waf_log_enabled, cilium_log_enabled: cilium_log_enabled) }
include_examples 'cluster application core specs', :clusters_applications_fluentd
include_examples 'cluster application status specs', :clusters_applications_fluentd
@@ -47,4 +49,36 @@ describe Clusters::Applications::Fluentd do
expect(values).to include('output.conf', 'general.conf')
end
end
+
+ describe '#values' do
+ let(:modsecurity_log_path) { "/var/log/containers/*#{Clusters::Applications::Ingress::MODSECURITY_LOG_CONTAINER_NAME}*.log" }
+ let(:cilium_log_path) { "/var/log/containers/*#{described_class::CILIUM_CONTAINER_NAME}*.log" }
+
+ subject { fluentd.values }
+
+ context 'with both logs variables set to false' do
+ let(:waf_log_enabled) { false }
+ let(:cilium_log_enabled) { false }
+
+ it "raises ActiveRecord::RecordInvalid" do
+ expect {subject}.to raise_error(ActiveRecord::RecordInvalid)
+ end
+ end
+
+ context 'with both logs variables set to true' do
+ it { is_expected.to include("#{modsecurity_log_path},#{cilium_log_path}") }
+ end
+
+ context 'with waf_log_enabled set to true' do
+ let(:cilium_log_enabled) { false }
+
+ it { is_expected.to include(modsecurity_log_path) }
+ end
+
+ context 'with cilium_log_enabled set to true' do
+ let(:waf_log_enabled) { false }
+
+ it { is_expected.to include(cilium_log_path) }
+ end
+ end
end
diff --git a/spec/models/clusters/applications/ingress_spec.rb b/spec/models/clusters/applications/ingress_spec.rb
index b070729ccac..8aee4eec0d3 100644
--- a/spec/models/clusters/applications/ingress_spec.rb
+++ b/spec/models/clusters/applications/ingress_spec.rb
@@ -220,6 +220,12 @@ describe Clusters::Applications::Ingress do
expect(subject.values).to include('extraContainers')
end
+ it 'executes command to tail modsecurity logs with -F option' do
+ args = YAML.safe_load(subject.values).dig('controller', 'extraContainers', 0, 'args')
+
+ expect(args).to eq(['/bin/sh', '-c', 'tail -F /var/log/modsec/audit.log'])
+ end
+
it 'includes livenessProbe for modsecurity sidecar container' do
probe_config = YAML.safe_load(subject.values).dig('controller', 'extraContainers', 0, 'livenessProbe')
diff --git a/spec/models/clusters/applications/jupyter_spec.rb b/spec/models/clusters/applications/jupyter_spec.rb
index 3bc5088d1ab..937db9217f3 100644
--- a/spec/models/clusters/applications/jupyter_spec.rb
+++ b/spec/models/clusters/applications/jupyter_spec.rb
@@ -57,7 +57,7 @@ describe Clusters::Applications::Jupyter do
it 'is initialized with 4 arguments' do
expect(subject.name).to eq('jupyter')
expect(subject.chart).to eq('jupyter/jupyterhub')
- expect(subject.version).to eq('0.9.0-beta.2')
+ expect(subject.version).to eq('0.9.0')
expect(subject).to be_rbac
expect(subject.repository).to eq('https://jupyterhub.github.io/helm-chart/')
@@ -76,7 +76,7 @@ describe Clusters::Applications::Jupyter do
let(:jupyter) { create(:clusters_applications_jupyter, :errored, version: '0.0.1') }
it 'is initialized with the locked version' do
- expect(subject.version).to eq('0.9.0-beta.2')
+ expect(subject.version).to eq('0.9.0')
end
end
end
diff --git a/spec/models/clusters/cluster_spec.rb b/spec/models/clusters/cluster_spec.rb
index db1d8672d1e..521ed98f637 100644
--- a/spec/models/clusters/cluster_spec.rb
+++ b/spec/models/clusters/cluster_spec.rb
@@ -590,6 +590,60 @@ describe Clusters::Cluster, :use_clean_rails_memory_store_caching do
end
end
+ describe '#find_or_build_application' do
+ let_it_be(:cluster, reload: true) { create(:cluster) }
+
+ it 'rejects classes that are not applications' do
+ expect do
+ cluster.find_or_build_application(Project)
+ end.to raise_error(ArgumentError)
+ end
+
+ context 'when none of applications are created' do
+ it 'returns the new application', :aggregate_failures do
+ described_class::APPLICATIONS.values.each do |application_class|
+ application = cluster.find_or_build_application(application_class)
+
+ expect(application).to be_a(application_class)
+ expect(application).not_to be_persisted
+ end
+ end
+ end
+
+ context 'when application is persisted' do
+ let!(:helm) { create(:clusters_applications_helm, cluster: cluster) }
+ let!(:ingress) { create(:clusters_applications_ingress, cluster: cluster) }
+ let!(:cert_manager) { create(:clusters_applications_cert_manager, cluster: cluster) }
+ let!(:crossplane) { create(:clusters_applications_crossplane, cluster: cluster) }
+ let!(:prometheus) { create(:clusters_applications_prometheus, cluster: cluster) }
+ let!(:runner) { create(:clusters_applications_runner, cluster: cluster) }
+ let!(:jupyter) { create(:clusters_applications_jupyter, cluster: cluster) }
+ let!(:knative) { create(:clusters_applications_knative, cluster: cluster) }
+ let!(:elastic_stack) { create(:clusters_applications_elastic_stack, cluster: cluster) }
+ let!(:fluentd) { create(:clusters_applications_fluentd, cluster: cluster) }
+
+ it 'returns the persisted application', :aggregate_failures do
+ {
+ Clusters::Applications::Helm => helm,
+ Clusters::Applications::Ingress => ingress,
+ Clusters::Applications::CertManager => cert_manager,
+ Clusters::Applications::Crossplane => crossplane,
+ Clusters::Applications::Prometheus => prometheus,
+ Clusters::Applications::Runner => runner,
+ Clusters::Applications::Jupyter => jupyter,
+ Clusters::Applications::Knative => knative,
+ Clusters::Applications::ElasticStack => elastic_stack,
+ Clusters::Applications::Fluentd => fluentd
+ }.each do |application_class, expected_object|
+ application = cluster.find_or_build_application(application_class)
+
+ expect(application).to eq(expected_object)
+ expect(application).to be_persisted
+ end
+ end
+ end
+ end
+
describe '#allow_user_defined_namespace?' do
subject { cluster.allow_user_defined_namespace? }
@@ -889,9 +943,9 @@ describe Clusters::Cluster, :use_clean_rails_memory_store_caching do
end
describe '#make_cleanup_errored!' do
- NON_ERRORED_STATES = Clusters::Cluster.state_machines[:cleanup_status].states.keys - [:cleanup_errored]
+ non_errored_states = Clusters::Cluster.state_machines[:cleanup_status].states.keys - [:cleanup_errored]
- NON_ERRORED_STATES.each do |state|
+ non_errored_states.each do |state|
it "transitions cleanup_status from #{state} to cleanup_errored" do
cluster = create(:cluster, state)
@@ -948,6 +1002,22 @@ describe Clusters::Cluster, :use_clean_rails_memory_store_caching do
end
end
+ describe '#nodes' do
+ let(:cluster) { create(:cluster) }
+
+ subject { cluster.nodes }
+
+ it { is_expected.to be_nil }
+
+ context 'with a cached status' do
+ before do
+ stub_reactive_cache(cluster, nodes: [kube_node])
+ end
+
+ it { is_expected.to eq([kube_node]) }
+ end
+ end
+
describe '#calculate_reactive_cache' do
subject { cluster.calculate_reactive_cache }
@@ -956,6 +1026,7 @@ describe Clusters::Cluster, :use_clean_rails_memory_store_caching do
it 'does not populate the cache' do
expect(cluster).not_to receive(:retrieve_connection_status)
+ expect(cluster).not_to receive(:retrieve_nodes)
is_expected.to be_nil
end
@@ -964,12 +1035,12 @@ describe Clusters::Cluster, :use_clean_rails_memory_store_caching do
context 'cluster is enabled' do
let(:cluster) { create(:cluster, :provided_by_user, :group) }
- context 'connection to the cluster is successful' do
- before do
- stub_kubeclient_discover(cluster.platform.api_url)
- end
+ before do
+ stub_kubeclient_nodes_and_nodes_metrics(cluster.platform.api_url)
+ end
- it { is_expected.to eq(connection_status: :connected) }
+ context 'connection to the cluster is successful' do
+ it { is_expected.to eq(connection_status: :connected, nodes: [kube_node.merge(kube_node_metrics)]) }
end
context 'cluster cannot be reached' do
@@ -978,7 +1049,7 @@ describe Clusters::Cluster, :use_clean_rails_memory_store_caching do
.and_raise(SocketError)
end
- it { is_expected.to eq(connection_status: :unreachable) }
+ it { is_expected.to eq(connection_status: :unreachable, nodes: []) }
end
context 'cluster cannot be authenticated to' do
@@ -987,7 +1058,7 @@ describe Clusters::Cluster, :use_clean_rails_memory_store_caching do
.and_raise(OpenSSL::X509::CertificateError.new("Certificate error"))
end
- it { is_expected.to eq(connection_status: :authentication_failure) }
+ it { is_expected.to eq(connection_status: :authentication_failure, nodes: []) }
end
describe 'Kubeclient::HttpError' do
@@ -999,18 +1070,18 @@ describe Clusters::Cluster, :use_clean_rails_memory_store_caching do
.and_raise(Kubeclient::HttpError.new(error_code, error_message, nil))
end
- it { is_expected.to eq(connection_status: :authentication_failure) }
+ it { is_expected.to eq(connection_status: :authentication_failure, nodes: []) }
context 'generic timeout' do
let(:error_message) { 'Timed out connecting to server'}
- it { is_expected.to eq(connection_status: :unreachable) }
+ it { is_expected.to eq(connection_status: :unreachable, nodes: []) }
end
context 'gateway timeout' do
let(:error_message) { '504 Gateway Timeout for GET https://kubernetes.example.com/api/v1'}
- it { is_expected.to eq(connection_status: :unreachable) }
+ it { is_expected.to eq(connection_status: :unreachable, nodes: []) }
end
end
@@ -1020,11 +1091,12 @@ describe Clusters::Cluster, :use_clean_rails_memory_store_caching do
.and_raise(StandardError)
end
- it { is_expected.to eq(connection_status: :unknown_failure) }
+ it { is_expected.to eq(connection_status: :unknown_failure, nodes: []) }
it 'notifies Sentry' do
expect(Gitlab::ErrorTracking).to receive(:track_exception)
.with(instance_of(StandardError), hash_including(cluster_id: cluster.id))
+ .twice
subject
end
diff --git a/spec/models/commit_status_spec.rb b/spec/models/commit_status_spec.rb
index 73b81b2225a..05d3329215a 100644
--- a/spec/models/commit_status_spec.rb
+++ b/spec/models/commit_status_spec.rb
@@ -751,4 +751,48 @@ describe CommitStatus do
it { is_expected.to be_a(CommitStatusPresenter) }
end
+
+ describe '#recoverable?' do
+ using RSpec::Parameterized::TableSyntax
+
+ let(:commit_status) { create(:commit_status, :pending) }
+
+ subject(:recoverable?) { commit_status.recoverable? }
+
+ context 'when commit status is failed' do
+ before do
+ commit_status.drop!
+ end
+
+ where(:failure_reason, :recoverable) do
+ :script_failure | false
+ :missing_dependency_failure | false
+ :archived_failure | false
+ :scheduler_failure | false
+ :data_integrity_failure | false
+ :unknown_failure | true
+ :api_failure | true
+ :stuck_or_timeout_failure | true
+ :runner_system_failure | true
+ end
+
+ with_them do
+ context "when failure reason is #{params[:failure_reason]}" do
+ before do
+ commit_status.update_attribute(:failure_reason, failure_reason)
+ end
+
+ it { is_expected.to eq(recoverable) }
+ end
+ end
+ end
+
+ context 'when commit status is not failed' do
+ before do
+ commit_status.success!
+ end
+
+ it { is_expected.to eq(false) }
+ end
+ end
end
diff --git a/spec/models/concerns/awardable_spec.rb b/spec/models/concerns/awardable_spec.rb
index 76da42cf243..29f911fcb04 100644
--- a/spec/models/concerns/awardable_spec.rb
+++ b/spec/models/concerns/awardable_spec.rb
@@ -91,4 +91,45 @@ describe Awardable do
expect(issue.award_emoji).to eq issue.award_emoji.sort_by(&:id)
end
end
+
+ describe "#grouped_awards" do
+ context 'default award emojis' do
+ let(:issue_without_downvote) { create(:issue) }
+ let(:issue_with_downvote) do
+ issue_with_downvote = create(:issue)
+ create(:award_emoji, :downvote, awardable: issue_with_downvote)
+ issue_with_downvote
+ end
+
+ it "includes unused thumbs buttons by default" do
+ expect(issue_without_downvote.grouped_awards.keys.sort).to eq %w(thumbsdown thumbsup)
+ end
+
+ it "doesn't include unused thumbs buttons when disabled in project" do
+ issue_without_downvote.project.show_default_award_emojis = false
+
+ expect(issue_without_downvote.grouped_awards.keys.sort).to eq []
+ end
+
+ it "includes unused thumbs buttons when enabled in project" do
+ issue_without_downvote.project.show_default_award_emojis = true
+
+ expect(issue_without_downvote.grouped_awards.keys.sort).to eq %w(thumbsdown thumbsup)
+ end
+
+ it "doesn't include unused thumbs buttons in summary" do
+ expect(issue_without_downvote.grouped_awards(with_thumbs: false).keys).to eq []
+ end
+
+ it "includes used thumbs buttons when disabled in project" do
+ issue_with_downvote.project.show_default_award_emojis = false
+
+ expect(issue_with_downvote.grouped_awards.keys).to eq %w(thumbsdown)
+ end
+
+ it "includes used thumbs buttons in summary" do
+ expect(issue_with_downvote.grouped_awards(with_thumbs: false).keys).to eq %w(thumbsdown)
+ end
+ end
+ end
end
diff --git a/spec/models/concerns/blocks_json_serialization_spec.rb b/spec/models/concerns/blocks_json_serialization_spec.rb
index 0ef5be3cb61..32870461019 100644
--- a/spec/models/concerns/blocks_json_serialization_spec.rb
+++ b/spec/models/concerns/blocks_json_serialization_spec.rb
@@ -3,8 +3,11 @@
require 'spec_helper'
describe BlocksJsonSerialization do
- DummyModel = Class.new do
- include BlocksJsonSerialization
+ before do
+ stub_const('DummyModel', Class.new)
+ DummyModel.class_eval do
+ include BlocksJsonSerialization
+ end
end
it 'blocks as_json' do
diff --git a/spec/models/concerns/cache_markdown_field_spec.rb b/spec/models/concerns/cache_markdown_field_spec.rb
index 697a9e98505..193144fcb0e 100644
--- a/spec/models/concerns/cache_markdown_field_spec.rb
+++ b/spec/models/concerns/cache_markdown_field_spec.rb
@@ -223,6 +223,10 @@ describe CacheMarkdownField, :clean_gitlab_redis_cache do
end
context 'when the markdown cache is up to date' do
+ before do
+ thing.try(:save)
+ end
+
it 'does not call #refresh_markdown_cache' do
expect(thing).not_to receive(:refresh_markdown_cache)
@@ -256,6 +260,54 @@ describe CacheMarkdownField, :clean_gitlab_redis_cache do
let(:klass) { ar_class }
it_behaves_like 'a class with cached markdown fields'
+
+ describe '#attribute_invalidated?' do
+ let(:thing) { klass.create(description: markdown, description_html: html, cached_markdown_version: cache_version) }
+
+ it 'returns true when cached_markdown_version is different' do
+ thing.cached_markdown_version += 1
+
+ expect(thing.attribute_invalidated?(:description_html)).to eq(true)
+ end
+
+ it 'returns true when markdown is changed' do
+ thing.description = updated_markdown
+
+ expect(thing.attribute_invalidated?(:description_html)).to eq(true)
+ end
+
+ it 'returns true when both markdown and HTML are changed' do
+ thing.description = updated_markdown
+ thing.description_html = updated_html
+
+ expect(thing.attribute_invalidated?(:description_html)).to eq(true)
+ end
+
+ it 'returns false when there are no changes' do
+ expect(thing.attribute_invalidated?(:description_html)).to eq(false)
+ end
+ end
+
+ context 'when cache version is updated' do
+ let(:old_version) { cache_version - 1 }
+ let(:old_html) { '<p data-sourcepos="1:1-1:5" dir="auto" class="some-old-class"><code>Foo</code></p>' }
+
+ let(:thing) do
+ # This forces the record to have outdated HTML. We can't use `create` because the `before_create` hook
+ # would re-render the HTML to the latest version
+ klass.create.tap do |thing|
+ thing.update_columns(description: markdown, description_html: old_html, cached_markdown_version: old_version)
+ end
+ end
+
+ it 'correctly updates cached HTML even if refresh_markdown_cache is called before updating the attribute' do
+ thing.refresh_markdown_cache
+
+ thing.update(description: updated_markdown)
+
+ expect(thing.description_html).to eq(updated_html)
+ end
+ end
end
context 'for other classes' do
diff --git a/spec/models/concerns/cacheable_attributes_spec.rb b/spec/models/concerns/cacheable_attributes_spec.rb
index d8f940a808e..56e0d044247 100644
--- a/spec/models/concerns/cacheable_attributes_spec.rb
+++ b/spec/models/concerns/cacheable_attributes_spec.rb
@@ -205,11 +205,11 @@ describe CacheableAttributes do
end
end
- it 'uses RequestStore in addition to Thread memory cache', :request_store do
+ it 'uses RequestStore in addition to process memory cache', :request_store do
# Warm up the cache
create(:application_setting).cache!
- expect(ApplicationSetting.cache_backend).to eq(Gitlab::ThreadMemoryCache.cache_backend)
+ expect(ApplicationSetting.cache_backend).to eq(Gitlab::ProcessMemoryCache.cache_backend)
expect(ApplicationSetting.cache_backend).to receive(:read).with(ApplicationSetting.cache_key).once.and_call_original
2.times { ApplicationSetting.current }
diff --git a/spec/models/concerns/has_user_type_spec.rb b/spec/models/concerns/has_user_type_spec.rb
new file mode 100644
index 00000000000..f12eee414f9
--- /dev/null
+++ b/spec/models/concerns/has_user_type_spec.rb
@@ -0,0 +1,86 @@
+# frozen_string_literal: true
+
+require 'spec_helper'
+
+describe User do
+ specify 'types consistency checks', :aggregate_failures do
+ expect(described_class::USER_TYPES.keys)
+ .to match_array(%w[human ghost alert_bot project_bot support_bot service_user visual_review_bot migration_bot])
+ expect(described_class::USER_TYPES).to include(*described_class::BOT_USER_TYPES)
+ expect(described_class::USER_TYPES).to include(*described_class::NON_INTERNAL_USER_TYPES)
+ expect(described_class::USER_TYPES).to include(*described_class::INTERNAL_USER_TYPES)
+ end
+
+ describe 'scopes & predicates' do
+ User::USER_TYPES.keys.each do |type|
+ let_it_be(type) { create(:user, username: type, user_type: type) }
+ end
+ let(:bots) { User::BOT_USER_TYPES.map { |type| public_send(type) } }
+ let(:non_internal) { User::NON_INTERNAL_USER_TYPES.map { |type| public_send(type) } }
+ let(:everyone) { User::USER_TYPES.keys.map { |type| public_send(type) } }
+
+ describe '.humans' do
+ it 'includes humans only' do
+ expect(described_class.humans).to match_array([human])
+ end
+ end
+
+ describe '.bots' do
+ it 'includes all bots' do
+ expect(described_class.bots).to match_array(bots)
+ end
+ end
+
+ describe '.bots_without_project_bot' do
+ it 'includes all bots except project_bot' do
+ expect(described_class.bots_without_project_bot).to match_array(bots - [project_bot])
+ end
+ end
+
+ describe '.non_internal' do
+ it 'includes all non_internal users' do
+ expect(described_class.non_internal).to match_array(non_internal)
+ end
+ end
+
+ describe '.without_ghosts' do
+ it 'includes everyone except ghosts' do
+ expect(described_class.without_ghosts).to match_array(everyone - [ghost])
+ end
+ end
+
+ describe '.without_project_bot' do
+ it 'includes everyone except project_bot' do
+ expect(described_class.without_project_bot).to match_array(everyone - [project_bot])
+ end
+ end
+
+ describe '#bot?' do
+ it 'is true for all bot user types and false for others' do
+ expect(bots).to all(be_bot)
+
+ (everyone - bots).each do |user|
+ expect(user).not_to be_bot
+ end
+ end
+ end
+
+ describe '#human?' do
+ it 'is true for humans only' do
+ expect(human).to be_human
+ expect(alert_bot).not_to be_human
+ expect(User.new).to be_human
+ end
+ end
+
+ describe '#internal?' do
+ it 'is true for all internal user types and false for others' do
+ expect(everyone - non_internal).to all(be_internal)
+
+ non_internal.each do |user|
+ expect(user).not_to be_internal
+ end
+ end
+ end
+ end
+end
diff --git a/spec/models/concerns/mentionable_spec.rb b/spec/models/concerns/mentionable_spec.rb
index 13a3d1cdd82..03fd1c69654 100644
--- a/spec/models/concerns/mentionable_spec.rb
+++ b/spec/models/concerns/mentionable_spec.rb
@@ -3,14 +3,17 @@
require 'spec_helper'
describe Mentionable do
- class Example
- include Mentionable
+ before do
+ stub_const('Example', Class.new)
+ Example.class_eval do
+ include Mentionable
- attr_accessor :project, :message
- attr_mentionable :message
+ attr_accessor :project, :message
+ attr_mentionable :message
- def author
- nil
+ def author
+ nil
+ end
end
end
@@ -28,11 +31,11 @@ describe Mentionable do
end
describe '#any_mentionable_attributes_changed?' do
- Message = Struct.new(:text)
+ message = Struct.new(:text)
let(:mentionable) { Example.new }
let(:changes) do
- msg = Message.new('test')
+ msg = message.new('test')
changes = {}
changes[msg] = ['', 'some message']
@@ -325,3 +328,36 @@ describe Snippet, 'Mentionable' do
end
end
end
+
+describe PersonalSnippet, 'Mentionable' do
+ describe '#store_mentions!' do
+ it_behaves_like 'mentions in description', :personal_snippet
+ it_behaves_like 'mentions in notes', :personal_snippet do
+ let(:note) { create(:note_on_personal_snippet) }
+ let(:mentionable) { note.noteable }
+ end
+ end
+
+ describe 'load mentions' do
+ it_behaves_like 'load mentions from DB', :personal_snippet do
+ let(:note) { create(:note_on_personal_snippet) }
+ let(:mentionable) { note.noteable }
+ end
+ end
+end
+
+describe DesignManagement::Design do
+ describe '#store_mentions!' do
+ it_behaves_like 'mentions in notes', :design do
+ let(:note) { create(:diff_note_on_design) }
+ let(:mentionable) { note.noteable }
+ end
+ end
+
+ describe 'load mentions' do
+ it_behaves_like 'load mentions from DB', :design do
+ let(:note) { create(:diff_note_on_design) }
+ let(:mentionable) { note.noteable }
+ end
+ end
+end
diff --git a/spec/models/concerns/noteable_spec.rb b/spec/models/concerns/noteable_spec.rb
index 097bc24d90f..5c8c5425ca7 100644
--- a/spec/models/concerns/noteable_spec.rb
+++ b/spec/models/concerns/noteable_spec.rb
@@ -241,7 +241,7 @@ describe Noteable do
describe '.resolvable_types' do
it 'exposes the replyable types' do
- expect(described_class.resolvable_types).to include('MergeRequest')
+ expect(described_class.resolvable_types).to include('MergeRequest', 'DesignManagement::Design')
end
end
diff --git a/spec/models/concerns/reactive_caching_spec.rb b/spec/models/concerns/reactive_caching_spec.rb
index 96a9c317fb8..cfca383e0b0 100644
--- a/spec/models/concerns/reactive_caching_spec.rb
+++ b/spec/models/concerns/reactive_caching_spec.rb
@@ -6,39 +6,47 @@ describe ReactiveCaching, :use_clean_rails_memory_store_caching do
include ExclusiveLeaseHelpers
include ReactiveCachingHelpers
- class CacheTest
- include ReactiveCaching
+ let(:cache_class_test) do
+ Class.new do
+ include ReactiveCaching
- self.reactive_cache_key = ->(thing) { ["foo", thing.id] }
+ self.reactive_cache_key = ->(thing) { ["foo", thing.id] }
- self.reactive_cache_lifetime = 5.minutes
- self.reactive_cache_refresh_interval = 15.seconds
+ self.reactive_cache_lifetime = 5.minutes
+ self.reactive_cache_refresh_interval = 15.seconds
- attr_reader :id
+ attr_reader :id
- def self.primary_key
- :id
- end
+ def self.primary_key
+ :id
+ end
- def initialize(id, &blk)
- @id = id
- @calculator = blk
- end
+ def initialize(id, &blk)
+ @id = id
+ @calculator = blk
+ end
- def calculate_reactive_cache
- @calculator.call
- end
+ def calculate_reactive_cache
+ @calculator.call
+ end
- def result
- with_reactive_cache do |data|
- data
+ def result
+ with_reactive_cache do |data|
+ data
+ end
end
end
end
+ let(:external_dependency_cache_class_test) do
+ Class.new(cache_class_test) do
+ self.reactive_cache_work_type = :external_dependency
+ end
+ end
+
let(:calculation) { -> { 2 + 2 } }
let(:cache_key) { "foo:666" }
- let(:instance) { CacheTest.new(666, &calculation) }
+ let(:instance) { cache_class_test.new(666, &calculation) }
describe '#with_reactive_cache' do
before do
@@ -47,6 +55,18 @@ describe ReactiveCaching, :use_clean_rails_memory_store_caching do
subject(:go!) { instance.result }
+ shared_examples 'reactive worker call' do |worker_class|
+ let(:instance) do
+ test_class.new(666, &calculation)
+ end
+
+ it 'performs caching with correct worker' do
+ expect(worker_class).to receive(:perform_async).with(test_class, 666)
+
+ go!
+ end
+ end
+
shared_examples 'a cacheable value' do |cached_value|
before do
stub_reactive_cache(instance, cached_value)
@@ -73,10 +93,12 @@ describe ReactiveCaching, :use_clean_rails_memory_store_caching do
it { is_expected.to be_nil }
- it 'refreshes cache' do
- expect(ReactiveCachingWorker).to receive(:perform_async).with(CacheTest, 666)
+ it_behaves_like 'reactive worker call', ReactiveCachingWorker do
+ let(:test_class) { cache_class_test }
+ end
- instance.with_reactive_cache { raise described_class::InvalidateReactiveCache }
+ it_behaves_like 'reactive worker call', ExternalServiceReactiveCachingWorker do
+ let(:test_class) { external_dependency_cache_class_test }
end
end
end
@@ -84,10 +106,12 @@ describe ReactiveCaching, :use_clean_rails_memory_store_caching do
context 'when cache is empty' do
it { is_expected.to be_nil }
- it 'enqueues a background worker to bootstrap the cache' do
- expect(ReactiveCachingWorker).to receive(:perform_async).with(CacheTest, 666)
+ it_behaves_like 'reactive worker call', ReactiveCachingWorker do
+ let(:test_class) { cache_class_test }
+ end
- go!
+ it_behaves_like 'reactive worker call', ExternalServiceReactiveCachingWorker do
+ let(:test_class) { external_dependency_cache_class_test }
end
it 'updates the cache lifespan' do
@@ -168,12 +192,14 @@ describe ReactiveCaching, :use_clean_rails_memory_store_caching do
context 'with custom reactive_cache_worker_finder' do
let(:args) { %w(arg1 arg2) }
- let(:instance) { CustomFinderCacheTest.new(666, &calculation) }
+ let(:instance) { custom_finder_cache_test.new(666, &calculation) }
- class CustomFinderCacheTest < CacheTest
- self.reactive_cache_worker_finder = ->(_id, *args) { from_cache(*args) }
+ let(:custom_finder_cache_test) do
+ Class.new(cache_class_test) do
+ self.reactive_cache_worker_finder = ->(_id, *args) { from_cache(*args) }
- def self.from_cache(*args); end
+ def self.from_cache(*args); end
+ end
end
before do
@@ -234,6 +260,18 @@ describe ReactiveCaching, :use_clean_rails_memory_store_caching do
go!
end
+ context 'when :external_dependency cache' do
+ let(:instance) do
+ external_dependency_cache_class_test.new(666, &calculation)
+ end
+
+ it 'enqueues a repeat worker' do
+ expect_reactive_cache_update_queued(instance, worker_klass: ExternalServiceReactiveCachingWorker)
+
+ go!
+ end
+ end
+
it 'calls a reactive_cache_updated only once if content did not change on subsequent update' do
expect(instance).to receive(:calculate_reactive_cache).twice
expect(instance).to receive(:reactive_cache_updated).once
@@ -262,7 +300,7 @@ describe ReactiveCaching, :use_clean_rails_memory_store_caching do
it_behaves_like 'ExceededReactiveCacheLimit'
context 'when reactive_cache_hard_limit is overridden' do
- let(:test_class) { Class.new(CacheTest) { self.reactive_cache_hard_limit = 3.megabytes } }
+ let(:test_class) { Class.new(cache_class_test) { self.reactive_cache_hard_limit = 3.megabytes } }
let(:instance) { test_class.new(666, &calculation) }
it_behaves_like 'successful cache'
diff --git a/spec/models/concerns/redis_cacheable_spec.rb b/spec/models/concerns/redis_cacheable_spec.rb
index f88d64e2013..1cf6afcc167 100644
--- a/spec/models/concerns/redis_cacheable_spec.rb
+++ b/spec/models/concerns/redis_cacheable_spec.rb
@@ -31,7 +31,7 @@ describe RedisCacheable do
subject { instance.cached_attribute(payload.each_key.first) }
it 'gets the cache attribute' do
- Gitlab::Redis::SharedState.with do |redis|
+ Gitlab::Redis::Cache.with do |redis|
expect(redis).to receive(:get).with(cache_key)
.and_return(payload.to_json)
end
@@ -44,7 +44,7 @@ describe RedisCacheable do
subject { instance.cache_attributes(payload) }
it 'sets the cache attributes' do
- Gitlab::Redis::SharedState.with do |redis|
+ Gitlab::Redis::Cache.with do |redis|
expect(redis).to receive(:set).with(cache_key, payload.to_json, anything)
end
@@ -52,7 +52,7 @@ describe RedisCacheable do
end
end
- describe '#cached_attr_reader', :clean_gitlab_redis_shared_state do
+ describe '#cached_attr_reader', :clean_gitlab_redis_cache do
subject { instance.name }
before do
diff --git a/spec/models/concerns/spammable_spec.rb b/spec/models/concerns/spammable_spec.rb
index b8537dd39f6..a8d27e174b7 100644
--- a/spec/models/concerns/spammable_spec.rb
+++ b/spec/models/concerns/spammable_spec.rb
@@ -39,43 +39,100 @@ describe Spammable do
describe '#invalidate_if_spam' do
using RSpec::Parameterized::TableSyntax
+ before do
+ stub_application_setting(recaptcha_enabled: true)
+ end
+
context 'when the model is spam' do
- where(:recaptcha_enabled, :error) do
- true | /solve the reCAPTCHA to proceed/
- false | /has been discarded/
+ subject { invalidate_if_spam(is_spam: true) }
+
+ it 'has an error related to spam on the model' do
+ expect(subject.errors.messages[:base]).to match_array /has been discarded/
end
+ end
- with_them do
- subject { invalidate_if_spam(true, recaptcha_enabled) }
+ context 'when the model needs recaptcha' do
+ subject { invalidate_if_spam(needs_recaptcha: true) }
- it 'has an error related to spam on the model' do
- expect(subject.errors.messages[:base]).to match_array error
- end
+ it 'has an error related to spam on the model' do
+ expect(subject.errors.messages[:base]).to match_array /solve the reCAPTCHA/
end
end
- context 'when the model is not spam' do
- [true, false].each do |enabled|
- let(:recaptcha_enabled) { enabled }
+ context 'if the model is spam and also needs recaptcha' do
+ subject { invalidate_if_spam(is_spam: true, needs_recaptcha: true) }
+
+ it 'has an error related to spam on the model' do
+ expect(subject.errors.messages[:base]).to match_array /solve the reCAPTCHA/
+ end
+ end
- subject { invalidate_if_spam(false, recaptcha_enabled) }
+ context 'when the model is not spam nor needs recaptcha' do
+ subject { invalidate_if_spam }
- it 'returns no error' do
- expect(subject.errors.messages[:base]).to be_empty
- end
+ it 'returns no error' do
+ expect(subject.errors.messages[:base]).to be_empty
end
end
- def invalidate_if_spam(is_spam, recaptcha_enabled)
- stub_application_setting(recaptcha_enabled: recaptcha_enabled)
+ context 'if recaptcha is not enabled and the model needs recaptcha' do
+ before do
+ stub_application_setting(recaptcha_enabled: false)
+ end
+
+ subject { invalidate_if_spam(needs_recaptcha: true) }
+ it 'has no errors' do
+ expect(subject.errors.messages[:base]).to match_array /has been discarded/
+ end
+ end
+
+ def invalidate_if_spam(is_spam: false, needs_recaptcha: false)
issue.tap do |i|
i.spam = is_spam
+ i.needs_recaptcha = needs_recaptcha
i.invalidate_if_spam
end
end
end
+ describe 'spam flags' do
+ before do
+ issue.spam = false
+ issue.needs_recaptcha = false
+ end
+
+ describe '#spam!' do
+ it 'adds only `spam` flag' do
+ issue.spam!
+
+ expect(issue.spam).to be_truthy
+ expect(issue.needs_recaptcha).to be_falsey
+ end
+ end
+
+ describe '#needs_recaptcha!' do
+ it 'adds `needs_recaptcha` flag' do
+ issue.needs_recaptcha!
+
+ expect(issue.spam).to be_falsey
+ expect(issue.needs_recaptcha).to be_truthy
+ end
+ end
+
+ describe '#clear_spam_flags!' do
+ it 'clears spam and recaptcha flags' do
+ issue.spam = true
+ issue.needs_recaptcha = true
+
+ issue.clear_spam_flags!
+
+ expect(issue).not_to be_spam
+ expect(issue.needs_recaptcha).to be_falsey
+ end
+ end
+ end
+
describe '#submittable_as_spam_by?' do
let(:admin) { build(:admin) }
let(:user) { build(:user) }
diff --git a/spec/models/container_repository_spec.rb b/spec/models/container_repository_spec.rb
index 5bcd9dfd396..1eecefe5d4a 100644
--- a/spec/models/container_repository_spec.rb
+++ b/spec/models/container_repository_spec.rb
@@ -19,7 +19,7 @@ describe ContainerRepository do
.with(headers: { 'Accept' => ContainerRegistry::Client::ACCEPTED_TYPES.join(', ') })
.to_return(
status: 200,
- body: JSON.dump(tags: ['test_tag']),
+ body: Gitlab::Json.dump(tags: ['test_tag']),
headers: { 'Content-Type' => 'application/json' })
end
@@ -309,4 +309,14 @@ describe ContainerRepository do
it { is_expected.to eq([]) }
end
end
+
+ describe '.search_by_name' do
+ let!(:another_repository) do
+ create(:container_repository, name: 'my_foo_bar', project: project)
+ end
+
+ subject { described_class.search_by_name('my_image') }
+
+ it { is_expected.to contain_exactly(repository) }
+ end
end
diff --git a/spec/models/cycle_analytics/code_spec.rb b/spec/models/cycle_analytics/code_spec.rb
index 441f8265629..f6ab8e0ece6 100644
--- a/spec/models/cycle_analytics/code_spec.rb
+++ b/spec/models/cycle_analytics/code_spec.rb
@@ -7,7 +7,7 @@ describe 'CycleAnalytics#code' do
let_it_be(:project) { create(:project, :repository) }
let_it_be(:from_date) { 10.days.ago }
- let_it_be(:user) { create(:user, :admin) }
+ let_it_be(:user) { project.owner }
let_it_be(:project_level) { CycleAnalytics::ProjectLevel.new(project, options: { from: from_date }) }
subject { project_level }
diff --git a/spec/models/cycle_analytics/group_level_spec.rb b/spec/models/cycle_analytics/group_level_spec.rb
deleted file mode 100644
index ac169ebc0cf..00000000000
--- a/spec/models/cycle_analytics/group_level_spec.rb
+++ /dev/null
@@ -1,44 +0,0 @@
-# frozen_string_literal: true
-
-require 'spec_helper'
-
-describe CycleAnalytics::GroupLevel do
- let_it_be(:group) { create(:group)}
- let_it_be(:project) { create(:project, :repository, namespace: group) }
- let_it_be(:from_date) { 10.days.ago }
- let_it_be(:user) { create(:user, :admin) }
- let(:issue) { create(:issue, project: project, created_at: 2.days.ago) }
- let_it_be(:milestone) { create(:milestone, project: project) }
- let(:mr) { create_merge_request_closing_issue(user, project, issue, commit_message: "References #{issue.to_reference}") }
- let(:pipeline) { create(:ci_empty_pipeline, status: 'created', project: project, ref: mr.source_branch, sha: mr.source_branch_sha, head_pipeline_of: mr) }
-
- subject { described_class.new(group: group, options: { from: from_date, current_user: user }) }
-
- describe '#permissions' do
- it 'returns true for all stages' do
- expect(subject.permissions.values.uniq).to eq([true])
- end
- end
-
- describe '#stats' do
- before do
- create_cycle(user, project, issue, mr, milestone, pipeline)
- deploy_master(user, project)
- end
-
- it 'returns medians for each stage for a specific group' do
- expect(subject.no_stats?).to eq(false)
- end
- end
-
- describe '#summary' do
- before do
- create_cycle(user, project, issue, mr, milestone, pipeline)
- deploy_master(user, project)
- end
-
- it 'returns medians for each stage for a specific group' do
- expect(subject.summary.map { |summary| summary[:value] }).to contain_exactly('0.1', '1', '1')
- end
- end
-end
diff --git a/spec/models/cycle_analytics/issue_spec.rb b/spec/models/cycle_analytics/issue_spec.rb
index 726f2f8b018..b4ab763e0e6 100644
--- a/spec/models/cycle_analytics/issue_spec.rb
+++ b/spec/models/cycle_analytics/issue_spec.rb
@@ -7,7 +7,7 @@ describe 'CycleAnalytics#issue' do
let_it_be(:project) { create(:project, :repository) }
let_it_be(:from_date) { 10.days.ago }
- let_it_be(:user) { create(:user, :admin) }
+ let_it_be(:user) { project.owner }
let_it_be(:project_level) { CycleAnalytics::ProjectLevel.new(project, options: { from: from_date }) }
subject { project_level }
diff --git a/spec/models/cycle_analytics/plan_spec.rb b/spec/models/cycle_analytics/plan_spec.rb
index 3bd9f317ca7..6765b2e2cbc 100644
--- a/spec/models/cycle_analytics/plan_spec.rb
+++ b/spec/models/cycle_analytics/plan_spec.rb
@@ -7,7 +7,7 @@ describe 'CycleAnalytics#plan' do
let_it_be(:project) { create(:project, :repository) }
let_it_be(:from_date) { 10.days.ago }
- let_it_be(:user) { create(:user, :admin) }
+ let_it_be(:user) { project.owner }
let_it_be(:project_level) { CycleAnalytics::ProjectLevel.new(project, options: { from: from_date }) }
subject { project_level }
diff --git a/spec/models/cycle_analytics/production_spec.rb b/spec/models/cycle_analytics/production_spec.rb
index 01d88bbeec9..2f2bcd63acd 100644
--- a/spec/models/cycle_analytics/production_spec.rb
+++ b/spec/models/cycle_analytics/production_spec.rb
@@ -7,7 +7,7 @@ describe 'CycleAnalytics#production' do
let_it_be(:project) { create(:project, :repository) }
let_it_be(:from_date) { 10.days.ago }
- let_it_be(:user) { create(:user, :admin) }
+ let_it_be(:user) { project.owner }
let_it_be(:project_level) { CycleAnalytics::ProjectLevel.new(project, options: { from: from_date }) }
subject { project_level }
diff --git a/spec/models/cycle_analytics/project_level_spec.rb b/spec/models/cycle_analytics/project_level_spec.rb
index 2fc81777746..bb296351a29 100644
--- a/spec/models/cycle_analytics/project_level_spec.rb
+++ b/spec/models/cycle_analytics/project_level_spec.rb
@@ -5,7 +5,7 @@ require 'spec_helper'
describe CycleAnalytics::ProjectLevel do
let_it_be(:project) { create(:project, :repository) }
let_it_be(:from_date) { 10.days.ago }
- let_it_be(:user) { create(:user, :admin) }
+ let_it_be(:user) { project.owner }
let_it_be(:issue) { create(:issue, project: project, created_at: 2.days.ago) }
let_it_be(:milestone) { create(:milestone, project: project) }
let(:mr) { create_merge_request_closing_issue(user, project, issue, commit_message: "References #{issue.to_reference}") }
diff --git a/spec/models/cycle_analytics/review_spec.rb b/spec/models/cycle_analytics/review_spec.rb
index 50670188e85..25e8f1441d3 100644
--- a/spec/models/cycle_analytics/review_spec.rb
+++ b/spec/models/cycle_analytics/review_spec.rb
@@ -7,7 +7,7 @@ describe 'CycleAnalytics#review' do
let_it_be(:project) { create(:project, :repository) }
let_it_be(:from_date) { 10.days.ago }
- let_it_be(:user) { create(:user, :admin) }
+ let_it_be(:user) { project.owner }
subject { CycleAnalytics::ProjectLevel.new(project, options: { from: from_date }) }
diff --git a/spec/models/cycle_analytics/staging_spec.rb b/spec/models/cycle_analytics/staging_spec.rb
index cf0695f175a..effbc7056cc 100644
--- a/spec/models/cycle_analytics/staging_spec.rb
+++ b/spec/models/cycle_analytics/staging_spec.rb
@@ -7,7 +7,7 @@ describe 'CycleAnalytics#staging' do
let_it_be(:project) { create(:project, :repository) }
let_it_be(:from_date) { 10.days.ago }
- let_it_be(:user) { create(:user, :admin) }
+ let_it_be(:user) { project.owner }
let_it_be(:project_level) { CycleAnalytics::ProjectLevel.new(project, options: { from: from_date }) }
subject { project_level }
diff --git a/spec/models/cycle_analytics/test_spec.rb b/spec/models/cycle_analytics/test_spec.rb
index 24800aafca7..7e7ba4d9994 100644
--- a/spec/models/cycle_analytics/test_spec.rb
+++ b/spec/models/cycle_analytics/test_spec.rb
@@ -7,7 +7,7 @@ describe 'CycleAnalytics#test' do
let_it_be(:project) { create(:project, :repository) }
let_it_be(:from_date) { 10.days.ago }
- let_it_be(:user) { create(:user, :admin) }
+ let_it_be(:user) { project.owner }
let_it_be(:issue) { create(:issue, project: project) }
let_it_be(:project_level) { CycleAnalytics::ProjectLevel.new(project, options: { from: from_date }) }
let!(:merge_request) { create_merge_request_closing_issue(user, project, issue) }
diff --git a/spec/models/deploy_token_spec.rb b/spec/models/deploy_token_spec.rb
index a2d4c046d46..819e2850644 100644
--- a/spec/models/deploy_token_spec.rb
+++ b/spec/models/deploy_token_spec.rb
@@ -72,8 +72,10 @@ describe DeployToken do
describe '#scopes' do
context 'with all the scopes' do
+ let_it_be(:deploy_token) { create(:deploy_token, :all_scopes) }
+
it 'returns scopes assigned to DeployToken' do
- expect(deploy_token.scopes).to eq([:read_repository, :read_registry])
+ expect(deploy_token.scopes).to eq(DeployToken::AVAILABLE_SCOPES)
end
end
diff --git a/spec/models/design_management/action_spec.rb b/spec/models/design_management/action_spec.rb
new file mode 100644
index 00000000000..753c31b1549
--- /dev/null
+++ b/spec/models/design_management/action_spec.rb
@@ -0,0 +1,105 @@
+# frozen_string_literal: true
+require 'spec_helper'
+
+describe DesignManagement::Action do
+ describe 'relations' do
+ it { is_expected.to belong_to(:design) }
+ it { is_expected.to belong_to(:version) }
+ end
+
+ describe 'scopes' do
+ describe '.most_recent' do
+ let_it_be(:design_a) { create(:design) }
+ let_it_be(:design_b) { create(:design) }
+ let_it_be(:design_c) { create(:design) }
+
+ let(:designs) { [design_a, design_b, design_c] }
+
+ before_all do
+ create(:design_version, designs: [design_a, design_b, design_c])
+ create(:design_version, designs: [design_a, design_b])
+ create(:design_version, designs: [design_a])
+ end
+
+ it 'finds the correct version for each design' do
+ dvs = described_class.where(design: designs)
+
+ expected = designs
+ .map(&:id)
+ .zip(dvs.order("version_id DESC").pluck(:version_id).uniq)
+
+ actual = dvs.most_recent.map { |dv| [dv.design_id, dv.version_id] }
+
+ expect(actual).to eq(expected)
+ end
+ end
+
+ describe '.up_to_version' do
+ let_it_be(:issue) { create(:issue) }
+ let_it_be(:design_a) { create(:design, issue: issue) }
+ let_it_be(:design_b) { create(:design, issue: issue) }
+
+ # let bindings are not available in before(:all) contexts,
+ # so we need to redefine the array on each construction.
+ let_it_be(:oldest) { create(:design_version, designs: [design_a, design_b]) }
+ let_it_be(:middle) { create(:design_version, designs: [design_a, design_b]) }
+ let_it_be(:newest) { create(:design_version, designs: [design_a, design_b]) }
+
+ subject { described_class.where(design: issue.designs).up_to_version(version) }
+
+ context 'the version is nil' do
+ let(:version) { nil }
+
+ it 'returns all design_versions' do
+ is_expected.to have_attributes(size: 6)
+ end
+ end
+
+ context 'when given a Version instance' do
+ context 'the version is the most current' do
+ let(:version) { newest }
+
+ it { is_expected.to have_attributes(size: 6) }
+ end
+
+ context 'the version is the oldest' do
+ let(:version) { oldest }
+
+ it { is_expected.to have_attributes(size: 2) }
+ end
+
+ context 'the version is the middle one' do
+ let(:version) { middle }
+
+ it { is_expected.to have_attributes(size: 4) }
+ end
+ end
+
+ context 'when given a commit SHA' do
+ context 'the version is the most current' do
+ let(:version) { newest.sha }
+
+ it { is_expected.to have_attributes(size: 6) }
+ end
+
+ context 'the version is the oldest' do
+ let(:version) { oldest.sha }
+
+ it { is_expected.to have_attributes(size: 2) }
+ end
+
+ context 'the version is the middle one' do
+ let(:version) { middle.sha }
+
+ it { is_expected.to have_attributes(size: 4) }
+ end
+ end
+
+ context 'when given a String that is not a commit SHA' do
+ let(:version) { 'foo' }
+
+ it { expect { subject }.to raise_error(ArgumentError) }
+ end
+ end
+ end
+end
diff --git a/spec/models/design_management/design_action_spec.rb b/spec/models/design_management/design_action_spec.rb
new file mode 100644
index 00000000000..da4ad41dfcb
--- /dev/null
+++ b/spec/models/design_management/design_action_spec.rb
@@ -0,0 +1,98 @@
+# frozen_string_literal: true
+require 'spec_helper'
+
+describe DesignManagement::DesignAction do
+ describe 'validations' do
+ describe 'the design' do
+ let(:fail_validation) { raise_error(/design/i) }
+
+ it 'must not be nil' do
+ expect { described_class.new(nil, :create, :foo) }.to fail_validation
+ end
+ end
+
+ describe 'the action' do
+ let(:fail_validation) { raise_error(/action/i) }
+
+ it 'must not be nil' do
+ expect { described_class.new(double, nil, :foo) }.to fail_validation
+ end
+
+ it 'must be a known action' do
+ expect { described_class.new(double, :wibble, :foo) }.to fail_validation
+ end
+ end
+
+ describe 'the content' do
+ context 'content is necesary' do
+ let(:fail_validation) { raise_error(/needs content/i) }
+
+ %i[create update].each do |action|
+ it "must not be nil if the action is #{action}" do
+ expect { described_class.new(double, action, nil) }.to fail_validation
+ end
+ end
+ end
+
+ context 'content is forbidden' do
+ let(:fail_validation) { raise_error(/forbids content/i) }
+
+ it "must not be nil if the action is delete" do
+ expect { described_class.new(double, :delete, :foo) }.to fail_validation
+ end
+ end
+ end
+ end
+
+ describe '#gitaly_action' do
+ let(:path) { 'some/path/somewhere' }
+ let(:design) { OpenStruct.new(full_path: path) }
+
+ subject { described_class.new(design, action, content) }
+
+ context 'the action needs content' do
+ let(:action) { :create }
+ let(:content) { :foo }
+
+ it 'produces a good gitaly action' do
+ expect(subject.gitaly_action).to eq(
+ action: action,
+ file_path: path,
+ content: content
+ )
+ end
+ end
+
+ context 'the action forbids content' do
+ let(:action) { :delete }
+ let(:content) { nil }
+
+ it 'produces a good gitaly action' do
+ expect(subject.gitaly_action).to eq(action: action, file_path: path)
+ end
+ end
+ end
+
+ describe '#issue_id' do
+ let(:issue_id) { :foo }
+ let(:design) { OpenStruct.new(issue_id: issue_id) }
+
+ subject { described_class.new(design, :delete) }
+
+ it 'delegates to the design' do
+ expect(subject.issue_id).to eq(issue_id)
+ end
+ end
+
+ describe '#performed' do
+ let(:design) { double }
+
+ subject { described_class.new(design, :delete) }
+
+ it 'calls design#clear_version_cache when the action has been performed' do
+ expect(design).to receive(:clear_version_cache)
+
+ subject.performed
+ end
+ end
+end
diff --git a/spec/models/design_management/design_at_version_spec.rb b/spec/models/design_management/design_at_version_spec.rb
new file mode 100644
index 00000000000..f6fa8df243c
--- /dev/null
+++ b/spec/models/design_management/design_at_version_spec.rb
@@ -0,0 +1,426 @@
+# frozen_string_literal: true
+
+require 'spec_helper'
+
+describe DesignManagement::DesignAtVersion do
+ include DesignManagementTestHelpers
+
+ let_it_be(:issue, reload: true) { create(:issue) }
+ let_it_be(:issue_b, reload: true) { create(:issue) }
+ let_it_be(:design, reload: true) { create(:design, issue: issue) }
+ let_it_be(:version) { create(:design_version, designs: [design]) }
+
+ describe '#id' do
+ subject { described_class.new(design: design, version: version) }
+
+ it 'combines design.id and version.id' do
+ expect(subject.id).to include(design.id.to_s, version.id.to_s)
+ end
+ end
+
+ describe '#==' do
+ it 'identifies objects created with the same parameters as equal' do
+ design = build_stubbed(:design, issue: issue)
+ version = build_stubbed(:design_version, designs: [design], issue: issue)
+
+ this = build_stubbed(:design_at_version, design: design, version: version)
+ other = build_stubbed(:design_at_version, design: design, version: version)
+
+ expect(this).to eq(other)
+ expect(other).to eq(this)
+ end
+
+ it 'identifies unequal objects as unequal, by virtue of their version' do
+ design = build_stubbed(:design, issue: issue)
+ version_a = build_stubbed(:design_version, designs: [design])
+ version_b = build_stubbed(:design_version, designs: [design])
+
+ this = build_stubbed(:design_at_version, design: design, version: version_a)
+ other = build_stubbed(:design_at_version, design: design, version: version_b)
+
+ expect(this).not_to eq(nil)
+ expect(this).not_to eq(design)
+ expect(this).not_to eq(other)
+ expect(other).not_to eq(this)
+ end
+
+ it 'identifies unequal objects as unequal, by virtue of their design' do
+ design_a = build_stubbed(:design, issue: issue)
+ design_b = build_stubbed(:design, issue: issue)
+ version = build_stubbed(:design_version, designs: [design_a, design_b])
+
+ this = build_stubbed(:design_at_version, design: design_a, version: version)
+ other = build_stubbed(:design_at_version, design: design_b, version: version)
+
+ expect(this).not_to eq(other)
+ expect(other).not_to eq(this)
+ end
+
+ it 'rejects objects with the same id and the wrong class' do
+ dav = build_stubbed(:design_at_version)
+
+ expect(dav).not_to eq(OpenStruct.new(id: dav.id))
+ end
+
+ it 'expects objects to be of the same type, not subtypes' do
+ subtype = Class.new(described_class)
+ dav = build_stubbed(:design_at_version)
+ other = subtype.new(design: dav.design, version: dav.version)
+
+ expect(dav).not_to eq(other)
+ end
+ end
+
+ describe 'status methods' do
+ let!(:design_a) { create(:design, issue: issue) }
+ let!(:design_b) { create(:design, issue: issue) }
+
+ let!(:version_a) do
+ create(:design_version, designs: [design_a])
+ end
+ let!(:version_b) do
+ create(:design_version, designs: [design_b])
+ end
+ let!(:version_mod) do
+ create(:design_version, modified_designs: [design_a, design_b])
+ end
+ let!(:version_c) do
+ create(:design_version, deleted_designs: [design_a])
+ end
+ let!(:version_d) do
+ create(:design_version, deleted_designs: [design_b])
+ end
+ let!(:version_e) do
+ create(:design_version, designs: [design_a])
+ end
+
+ describe 'a design before it has been created' do
+ subject { build(:design_at_version, design: design_b, version: version_a) }
+
+ it 'is not deleted' do
+ expect(subject).not_to be_deleted
+ end
+
+ it 'has the status :not_created_yet' do
+ expect(subject).to have_attributes(status: :not_created_yet)
+ end
+ end
+
+ describe 'a design as of its creation' do
+ subject { build(:design_at_version, design: design_a, version: version_a) }
+
+ it 'is not deleted' do
+ expect(subject).not_to be_deleted
+ end
+
+ it 'has the status :current' do
+ expect(subject).to have_attributes(status: :current)
+ end
+ end
+
+ describe 'a design after it has been created, but before deletion' do
+ subject { build(:design_at_version, design: design_b, version: version_c) }
+
+ it 'is not deleted' do
+ expect(subject).not_to be_deleted
+ end
+
+ it 'has the status :current' do
+ expect(subject).to have_attributes(status: :current)
+ end
+ end
+
+ describe 'a design as of its modification' do
+ subject { build(:design_at_version, design: design_a, version: version_mod) }
+
+ it 'is not deleted' do
+ expect(subject).not_to be_deleted
+ end
+
+ it 'has the status :current' do
+ expect(subject).to have_attributes(status: :current)
+ end
+ end
+
+ describe 'a design as of its deletion' do
+ subject { build(:design_at_version, design: design_a, version: version_c) }
+
+ it 'is deleted' do
+ expect(subject).to be_deleted
+ end
+
+ it 'has the status :deleted' do
+ expect(subject).to have_attributes(status: :deleted)
+ end
+ end
+
+ describe 'a design after its deletion' do
+ subject { build(:design_at_version, design: design_b, version: version_e) }
+
+ it 'is deleted' do
+ expect(subject).to be_deleted
+ end
+
+ it 'has the status :deleted' do
+ expect(subject).to have_attributes(status: :deleted)
+ end
+ end
+
+ describe 'a design on its recreation' do
+ subject { build(:design_at_version, design: design_a, version: version_e) }
+
+ it 'is not deleted' do
+ expect(subject).not_to be_deleted
+ end
+
+ it 'has the status :current' do
+ expect(subject).to have_attributes(status: :current)
+ end
+ end
+ end
+
+ describe 'validations' do
+ subject(:design_at_version) { build(:design_at_version) }
+
+ it { is_expected.to be_valid }
+
+ describe 'a design-at-version without a design' do
+ subject { described_class.new(design: nil, version: build(:design_version)) }
+
+ it { is_expected.to be_invalid }
+
+ it 'mentions the design in the errors' do
+ subject.valid?
+
+ expect(subject.errors[:design]).to be_present
+ end
+ end
+
+ describe 'a design-at-version without a version' do
+ subject { described_class.new(design: build(:design), version: nil) }
+
+ it { is_expected.to be_invalid }
+
+ it 'mentions the version in the errors' do
+ subject.valid?
+
+ expect(subject.errors[:version]).to be_present
+ end
+ end
+
+ describe 'design_and_version_belong_to_the_same_issue' do
+ context 'both design and version are supplied' do
+ subject(:design_at_version) { build(:design_at_version, design: design, version: version) }
+
+ context 'the design belongs to the same issue as the version' do
+ it { is_expected.to be_valid }
+ end
+
+ context 'the design does not belong to the same issue as the version' do
+ let(:design) { create(:design) }
+ let(:version) { create(:design_version) }
+
+ it { is_expected.to be_invalid }
+ end
+ end
+
+ context 'the factory is just supplied with a design' do
+ let(:design) { create(:design) }
+
+ subject(:design_at_version) { build(:design_at_version, design: design) }
+
+ it { is_expected.to be_valid }
+ end
+
+ context 'the factory is just supplied with a version' do
+ let(:version) { create(:design_version) }
+
+ subject(:design_at_version) { build(:design_at_version, version: version) }
+
+ it { is_expected.to be_valid }
+ end
+ end
+
+ describe 'design_and_version_have_issue_id' do
+ subject(:design_at_version) { build(:design_at_version, design: design, version: version) }
+
+ context 'the design has no issue_id, because it is being imported' do
+ let(:design) { create(:design, :importing) }
+
+ it { is_expected.to be_invalid }
+ end
+
+ context 'the version has no issue_id, because it is being imported' do
+ let(:version) { create(:design_version, :importing) }
+
+ it { is_expected.to be_invalid }
+ end
+
+ context 'both the design and the version are being imported' do
+ let(:version) { create(:design_version, :importing) }
+ let(:design) { create(:design, :importing) }
+
+ it { is_expected.to be_invalid }
+ end
+ end
+ end
+
+ def id_of(design, version)
+ build(:design_at_version, design: design, version: version).id
+ end
+
+ describe '.instantiate' do
+ context 'when attrs are valid' do
+ subject do
+ described_class.instantiate(design: design, version: version)
+ end
+
+ it { is_expected.to be_a(described_class).and(be_valid) }
+ end
+
+ context 'when attrs are invalid' do
+ subject do
+ described_class.instantiate(
+ design: create(:design),
+ version: create(:design_version)
+ )
+ end
+
+ it 'raises a validation error' do
+ expect { subject }.to raise_error(ActiveModel::ValidationError)
+ end
+ end
+ end
+
+ describe '.lazy_find' do
+ let!(:version_a) do
+ create(:design_version, designs: create_list(:design, 3, issue: issue))
+ end
+ let!(:version_b) do
+ create(:design_version, designs: create_list(:design, 1, issue: issue))
+ end
+ let!(:version_c) do
+ create(:design_version, designs: create_list(:design, 1, issue: issue_b))
+ end
+
+ let(:id_a) { id_of(version_a.designs.first, version_a) }
+ let(:id_b) { id_of(version_a.designs.second, version_a) }
+ let(:id_c) { id_of(version_a.designs.last, version_a) }
+ let(:id_d) { id_of(version_b.designs.first, version_b) }
+ let(:id_e) { id_of(version_c.designs.first, version_c) }
+ let(:bad_id) { id_of(version_c.designs.first, version_a) }
+
+ def find(the_id)
+ described_class.lazy_find(the_id)
+ end
+
+ let(:db_calls) { 2 }
+
+ it 'issues fewer queries than the naive approach would' do
+ expect do
+ dav_a = find(id_a)
+ dav_b = find(id_b)
+ dav_c = find(id_c)
+ dav_d = find(id_d)
+ dav_e = find(id_e)
+ should_not_exist = find(bad_id)
+
+ expect(dav_a.version).to eq(version_a)
+ expect(dav_b.version).to eq(version_a)
+ expect(dav_c.version).to eq(version_a)
+ expect(dav_d.version).to eq(version_b)
+ expect(dav_e.version).to eq(version_c)
+ expect(should_not_exist).not_to be_present
+
+ expect(version_a.designs).to include(dav_a.design, dav_b.design, dav_c.design)
+ expect(version_b.designs).to include(dav_d.design)
+ expect(version_c.designs).to include(dav_e.design)
+ end.not_to exceed_query_limit(db_calls)
+ end
+ end
+
+ describe '.find' do
+ let(:results) { described_class.find(ids) }
+
+ # 2 versions, with 5 total designs on issue A, so 2*5 = 10
+ let!(:version_a) do
+ create(:design_version, designs: create_list(:design, 3, issue: issue))
+ end
+ let!(:version_b) do
+ create(:design_version, designs: create_list(:design, 2, issue: issue))
+ end
+ # 1 version, with 3 designs on issue B, so 1*3 = 3
+ let!(:version_c) do
+ create(:design_version, designs: create_list(:design, 3, issue: issue_b))
+ end
+
+ context 'invalid ids' do
+ let(:ids) do
+ version_b.designs.map { |d| id_of(d, version_c) }
+ end
+
+ describe '#count' do
+ it 'counts 0 records' do
+ expect(results.count).to eq(0)
+ end
+ end
+
+ describe '#empty?' do
+ it 'is empty' do
+ expect(results).to be_empty
+ end
+ end
+
+ describe '#to_a' do
+ it 'finds no records' do
+ expect(results.to_a).to eq([])
+ end
+ end
+ end
+
+ context 'valid ids' do
+ let(:red_herrings) { issue_b.designs.sample(2).map { |d| id_of(d, version_a) } }
+
+ let(:ids) do
+ a_ids = issue.designs.sample(2).map { |d| id_of(d, version_a) }
+ b_ids = issue.designs.sample(2).map { |d| id_of(d, version_b) }
+ c_ids = issue_b.designs.sample(2).map { |d| id_of(d, version_c) }
+
+ a_ids + b_ids + c_ids + red_herrings
+ end
+
+ before do
+ ids.size # force IDs
+ end
+
+ describe '#count' do
+ it 'counts 2 records' do
+ expect(results.count).to eq(6)
+ end
+
+ it 'issues at most two queries' do
+ expect { results.count }.not_to exceed_query_limit(2)
+ end
+ end
+
+ describe '#to_a' do
+ it 'finds 6 records' do
+ expect(results.size).to eq(6)
+ expect(results).to all(be_a(described_class))
+ end
+
+ it 'only returns records with matching IDs' do
+ expect(results.map(&:id)).to match_array(ids - red_herrings)
+ end
+
+ it 'only returns valid records' do
+ expect(results).to all(be_valid)
+ end
+
+ it 'issues at most two queries' do
+ expect { results.to_a }.not_to exceed_query_limit(2)
+ end
+ end
+ end
+ end
+end
diff --git a/spec/models/design_management/design_collection_spec.rb b/spec/models/design_management/design_collection_spec.rb
new file mode 100644
index 00000000000..bd48f742042
--- /dev/null
+++ b/spec/models/design_management/design_collection_spec.rb
@@ -0,0 +1,82 @@
+# frozen_string_literal: true
+require 'spec_helper'
+
+describe DesignManagement::DesignCollection do
+ include DesignManagementTestHelpers
+
+ let_it_be(:issue, reload: true) { create(:issue) }
+
+ subject(:collection) { described_class.new(issue) }
+
+ describe ".find_or_create_design!" do
+ it "finds an existing design" do
+ design = create(:design, issue: issue, filename: 'world.png')
+
+ expect(collection.find_or_create_design!(filename: 'world.png')).to eq(design)
+ end
+
+ it "creates a new design if one didn't exist" do
+ expect(issue.designs.size).to eq(0)
+
+ new_design = collection.find_or_create_design!(filename: 'world.png')
+
+ expect(issue.designs.size).to eq(1)
+ expect(new_design.filename).to eq('world.png')
+ expect(new_design.issue).to eq(issue)
+ end
+
+ it "only queries the designs once" do
+ create(:design, issue: issue, filename: 'hello.png')
+ create(:design, issue: issue, filename: 'world.jpg')
+
+ expect do
+ collection.find_or_create_design!(filename: 'hello.png')
+ collection.find_or_create_design!(filename: 'world.jpg')
+ end.not_to exceed_query_limit(1)
+ end
+ end
+
+ describe "#versions" do
+ it "includes versions for all designs" do
+ version_1 = create(:design_version)
+ version_2 = create(:design_version)
+ other_version = create(:design_version)
+ create(:design, issue: issue, versions: [version_1])
+ create(:design, issue: issue, versions: [version_2])
+ create(:design, versions: [other_version])
+
+ expect(collection.versions).to contain_exactly(version_1, version_2)
+ end
+ end
+
+ describe "#repository" do
+ it "builds a design repository" do
+ expect(collection.repository).to be_a(DesignManagement::Repository)
+ end
+ end
+
+ describe '#designs_by_filename' do
+ let(:designs) { create_list(:design, 5, :with_file, issue: issue) }
+ let(:filenames) { designs.map(&:filename) }
+ let(:query) { subject.designs_by_filename(filenames) }
+
+ it 'finds all the designs with those filenames on this issue' do
+ expect(query).to have_attributes(size: 5)
+ end
+
+ it 'only makes a single query' do
+ designs.each(&:id)
+ expect { query }.not_to exceed_query_limit(1)
+ end
+
+ context 'some are deleted' do
+ before do
+ delete_designs(*designs.sample(2))
+ end
+
+ it 'takes deletion into account' do
+ expect(query).to have_attributes(size: 3)
+ end
+ end
+ end
+end
diff --git a/spec/models/design_management/design_spec.rb b/spec/models/design_management/design_spec.rb
new file mode 100644
index 00000000000..95782c1f674
--- /dev/null
+++ b/spec/models/design_management/design_spec.rb
@@ -0,0 +1,575 @@
+# frozen_string_literal: true
+
+require 'spec_helper'
+
+describe DesignManagement::Design do
+ include DesignManagementTestHelpers
+
+ let_it_be(:issue) { create(:issue) }
+ let_it_be(:design1) { create(:design, :with_versions, issue: issue, versions_count: 1) }
+ let_it_be(:design2) { create(:design, :with_versions, issue: issue, versions_count: 1) }
+ let_it_be(:design3) { create(:design, :with_versions, issue: issue, versions_count: 1) }
+ let_it_be(:deleted_design) { create(:design, :with_versions, deleted: true) }
+
+ describe 'relations' do
+ it { is_expected.to belong_to(:project) }
+ it { is_expected.to belong_to(:issue) }
+ it { is_expected.to have_many(:actions) }
+ it { is_expected.to have_many(:versions) }
+ it { is_expected.to have_many(:notes).dependent(:delete_all) }
+ it { is_expected.to have_many(:user_mentions) }
+ end
+
+ describe 'validations' do
+ subject(:design) { build(:design) }
+
+ it { is_expected.to be_valid }
+ it { is_expected.to validate_presence_of(:project) }
+ it { is_expected.to validate_presence_of(:issue) }
+ it { is_expected.to validate_presence_of(:filename) }
+ it { is_expected.to validate_uniqueness_of(:filename).scoped_to(:issue_id) }
+
+ it "validates that the extension is an image" do
+ design.filename = "thing.txt"
+ extensions = described_class::SAFE_IMAGE_EXT + described_class::DANGEROUS_IMAGE_EXT
+
+ expect(design).not_to be_valid
+ expect(design.errors[:filename].first).to eq(
+ "does not have a supported extension. Only #{extensions.to_sentence} are supported"
+ )
+ end
+
+ describe 'validating files with .svg extension' do
+ before do
+ design.filename = "thing.svg"
+ end
+
+ it "allows .svg files when feature flag is enabled" do
+ stub_feature_flags(design_management_allow_dangerous_images: true)
+
+ expect(design).to be_valid
+ end
+
+ it "does not allow .svg files when feature flag is disabled" do
+ stub_feature_flags(design_management_allow_dangerous_images: false)
+
+ expect(design).not_to be_valid
+ expect(design.errors[:filename].first).to eq(
+ "does not have a supported extension. Only #{described_class::SAFE_IMAGE_EXT.to_sentence} are supported"
+ )
+ end
+ end
+ end
+
+ describe 'scopes' do
+ describe '.visible_at_version' do
+ let(:versions) { DesignManagement::Version.where(issue: issue).ordered }
+ let(:found) { described_class.visible_at_version(version) }
+
+ context 'at oldest version' do
+ let(:version) { versions.last }
+
+ it 'finds the first design only' do
+ expect(found).to contain_exactly(design1)
+ end
+ end
+
+ context 'at version 2' do
+ let(:version) { versions.second }
+
+ it 'finds the first and second designs' do
+ expect(found).to contain_exactly(design1, design2)
+ end
+ end
+
+ context 'at latest version' do
+ let(:version) { versions.first }
+
+ it 'finds designs' do
+ expect(found).to contain_exactly(design1, design2, design3)
+ end
+ end
+
+ context 'when the argument is nil' do
+ let(:version) { nil }
+
+ it 'finds all undeleted designs' do
+ expect(found).to contain_exactly(design1, design2, design3)
+ end
+ end
+
+ describe 'one of the designs was deleted before the given version' do
+ before do
+ delete_designs(design2)
+ end
+
+ it 'is not returned' do
+ current_version = versions.first
+
+ expect(described_class.visible_at_version(current_version)).to contain_exactly(design1, design3)
+ end
+ end
+
+ context 'a re-created history' do
+ before do
+ delete_designs(design1, design2)
+ restore_designs(design1)
+ end
+
+ it 'is returned, though other deleted events are not' do
+ expect(described_class.visible_at_version(nil)).to contain_exactly(design1, design3)
+ end
+ end
+
+ # test that a design that has been modified at various points
+ # can be queried for correctly at different points in its history
+ describe 'dead or alive' do
+ let(:versions) { DesignManagement::Version.where(issue: issue).map { |v| [v, :alive] } }
+
+ before do
+ versions << [delete_designs(design1), :dead]
+ versions << [modify_designs(design2), :dead]
+ versions << [restore_designs(design1), :alive]
+ versions << [modify_designs(design3), :alive]
+ versions << [delete_designs(design1), :dead]
+ versions << [modify_designs(design2, design3), :dead]
+ versions << [restore_designs(design1), :alive]
+ end
+
+ it 'can establish the history at any point' do
+ history = versions.map(&:first).map do |v|
+ described_class.visible_at_version(v).include?(design1) ? :alive : :dead
+ end
+
+ expect(history).to eq(versions.map(&:second))
+ end
+ end
+ end
+
+ describe '.with_filename' do
+ it 'returns correct design when passed a single filename' do
+ expect(described_class.with_filename(design1.filename)).to eq([design1])
+ end
+
+ it 'returns correct designs when passed an Array of filenames' do
+ expect(
+ described_class.with_filename([design1, design2].map(&:filename))
+ ).to contain_exactly(design1, design2)
+ end
+ end
+
+ describe '.on_issue' do
+ it 'returns correct designs when passed a single issue' do
+ expect(described_class.on_issue(issue)).to match_array(issue.designs)
+ end
+
+ it 'returns correct designs when passed an Array of issues' do
+ expect(
+ described_class.on_issue([issue, deleted_design.issue])
+ ).to contain_exactly(design1, design2, design3, deleted_design)
+ end
+ end
+
+ describe '.current' do
+ it 'returns just the undeleted designs' do
+ delete_designs(design3)
+
+ expect(described_class.current).to contain_exactly(design1, design2)
+ end
+ end
+ end
+
+ describe '#visible_in?' do
+ let_it_be(:issue) { create(:issue) }
+
+ # It is expensive to re-create complex histories, so we do it once, and then
+ # assert that we can establish visibility at any given version.
+ it 'tells us when a design is visible' do
+ expected = []
+
+ first_design = create(:design, :with_versions, issue: issue, versions_count: 1)
+ prior_to_creation = first_design.versions.first
+ expected << [prior_to_creation, :not_created_yet, false]
+
+ v = modify_designs(first_design)
+ expected << [v, :not_created_yet, false]
+
+ design = create(:design, :with_versions, issue: issue, versions_count: 1)
+ created_in = design.versions.first
+ expected << [created_in, :created, true]
+
+ # The future state should not affect the result for any state, so we
+ # ensure that most states have a long future as well as a rich past
+ 2.times do
+ v = modify_designs(first_design)
+ expected << [v, :unaffected_visible, true]
+
+ v = modify_designs(design)
+ expected << [v, :modified, true]
+
+ v = modify_designs(first_design)
+ expected << [v, :unaffected_visible, true]
+
+ v = delete_designs(design)
+ expected << [v, :deleted, false]
+
+ v = modify_designs(first_design)
+ expected << [v, :unaffected_nv, false]
+
+ v = restore_designs(design)
+ expected << [v, :restored, true]
+ end
+
+ delete_designs(design) # ensure visibility is not corelated with current state
+
+ got = expected.map do |(v, sym, _)|
+ [v, sym, design.visible_in?(v)]
+ end
+
+ expect(got).to eq(expected)
+ end
+ end
+
+ describe '#to_ability_name' do
+ it { expect(described_class.new.to_ability_name).to eq('design') }
+ end
+
+ describe '#status' do
+ context 'the design is new' do
+ subject { build(:design) }
+
+ it { is_expected.to have_attributes(status: :new) }
+ end
+
+ context 'the design is current' do
+ subject { design1 }
+
+ it { is_expected.to have_attributes(status: :current) }
+ end
+
+ context 'the design has been deleted' do
+ subject { deleted_design }
+
+ it { is_expected.to have_attributes(status: :deleted) }
+ end
+ end
+
+ describe '#deleted?' do
+ context 'the design is new' do
+ let(:design) { build(:design) }
+
+ it 'is falsy' do
+ expect(design).not_to be_deleted
+ end
+ end
+
+ context 'the design is current' do
+ let(:design) { design1 }
+
+ it 'is falsy' do
+ expect(design).not_to be_deleted
+ end
+ end
+
+ context 'the design has been deleted' do
+ let(:design) { deleted_design }
+
+ it 'is truthy' do
+ expect(design).to be_deleted
+ end
+ end
+
+ context 'the design has been deleted, but was then re-created' do
+ let(:design) { create(:design, :with_versions, versions_count: 1, deleted: true) }
+
+ it 'is falsy' do
+ restore_designs(design)
+
+ expect(design).not_to be_deleted
+ end
+ end
+ end
+
+ describe "#new_design?" do
+ let(:design) { design1 }
+
+ it "is false when there are versions" do
+ expect(design1).not_to be_new_design
+ end
+
+ it "is true when there are no versions" do
+ expect(build(:design)).to be_new_design
+ end
+
+ it 'is false for deleted designs' do
+ expect(deleted_design).not_to be_new_design
+ end
+
+ it "does not cause extra queries when actions are loaded" do
+ design.actions.map(&:id)
+
+ expect { design.new_design? }.not_to exceed_query_limit(0)
+ end
+
+ it "implicitly caches values" do
+ expect do
+ design.new_design?
+ design.new_design?
+ end.not_to exceed_query_limit(1)
+ end
+
+ it "queries again when the clear_version_cache trigger has been called" do
+ expect do
+ design.new_design?
+ design.clear_version_cache
+ design.new_design?
+ end.not_to exceed_query_limit(2)
+ end
+
+ it "causes a single query when there versions are not loaded" do
+ design.reload
+
+ expect { design.new_design? }.not_to exceed_query_limit(1)
+ end
+ end
+
+ describe "#full_path" do
+ it "builds the full path for a design" do
+ design = build(:design, filename: "hello.jpg")
+ expected_path = "#{DesignManagement.designs_directory}/issue-#{design.issue.iid}/hello.jpg"
+
+ expect(design.full_path).to eq(expected_path)
+ end
+ end
+
+ describe '#diff_refs' do
+ let(:design) { create(:design, :with_file, versions_count: versions_count) }
+
+ context 'there are several versions' do
+ let(:versions_count) { 3 }
+
+ it "builds diff refs based on the first commit and it's for the design" do
+ expect(design.diff_refs.base_sha).to eq(design.versions.ordered.second.sha)
+ expect(design.diff_refs.head_sha).to eq(design.versions.ordered.first.sha)
+ end
+ end
+
+ context 'there is just one version' do
+ let(:versions_count) { 1 }
+
+ it 'builds diff refs based on the empty tree if there was only one version' do
+ design = create(:design, :with_file, versions_count: 1)
+
+ expect(design.diff_refs.base_sha).to eq(Gitlab::Git::BLANK_SHA)
+ expect(design.diff_refs.head_sha).to eq(design.diff_refs.head_sha)
+ end
+ end
+
+ it 'has no diff ref if new' do
+ design = build(:design)
+
+ expect(design.diff_refs).to be_nil
+ end
+ end
+
+ describe '#repository' do
+ it 'is a design repository' do
+ design = build(:design)
+
+ expect(design.repository).to be_a(DesignManagement::Repository)
+ end
+ end
+
+ describe '#note_etag_key' do
+ it 'returns a correct etag key' do
+ design = create(:design)
+
+ expect(design.note_etag_key).to eq(
+ ::Gitlab::Routing.url_helpers.designs_project_issue_path(design.project, design.issue, { vueroute: design.filename })
+ )
+ end
+ end
+
+ describe '#user_notes_count', :use_clean_rails_memory_store_caching do
+ let_it_be(:design) { create(:design, :with_file) }
+
+ subject { design.user_notes_count }
+
+ # Note: Cache invalidation tests are in `design_user_notes_count_service_spec.rb`
+
+ it 'returns a count of user-generated notes' do
+ create(:diff_note_on_design, noteable: design)
+
+ is_expected.to eq(1)
+ end
+
+ it 'does not count notes on other designs' do
+ second_design = create(:design, :with_file)
+ create(:diff_note_on_design, noteable: second_design)
+
+ is_expected.to eq(0)
+ end
+
+ it 'does not count system notes' do
+ create(:diff_note_on_design, system: true, noteable: design)
+
+ is_expected.to eq(0)
+ end
+ end
+
+ describe '#after_note_changed' do
+ subject { build(:design) }
+
+ it 'calls #delete_cache on DesignUserNotesCountService' do
+ expect_next_instance_of(DesignManagement::DesignUserNotesCountService) do |service|
+ expect(service).to receive(:delete_cache)
+ end
+
+ subject.after_note_changed(build(:note))
+ end
+
+ it 'does not call #delete_cache on DesignUserNotesCountService when passed a system note' do
+ expect(DesignManagement::DesignUserNotesCountService).not_to receive(:new)
+
+ subject.after_note_changed(build(:note, :system))
+ end
+ end
+
+ describe '.for_reference' do
+ let_it_be(:design_a) { create(:design) }
+ let_it_be(:design_b) { create(:design) }
+
+ it 'avoids extra queries when calling to_reference' do
+ designs = described_class.for_reference.where(id: [design_a.id, design_b.id]).to_a
+
+ expect { designs.map(&:to_reference) }.not_to exceed_query_limit(0)
+ end
+ end
+
+ describe '#to_reference' do
+ let(:namespace) { build(:namespace, path: 'sample-namespace') }
+ let(:project) { build(:project, name: 'sample-project', namespace: namespace) }
+ let(:group) { create(:group, name: 'Group', path: 'sample-group') }
+ let(:issue) { build(:issue, iid: 1, project: project) }
+ let(:filename) { 'homescreen.jpg' }
+ let(:design) { build(:design, filename: filename, issue: issue, project: project) }
+
+ context 'when nil argument' do
+ let(:reference) { design.to_reference }
+
+ it 'uses the simple format' do
+ expect(reference).to eq "#1[homescreen.jpg]"
+ end
+
+ context 'when the filename contains spaces, hyphens, periods, single-quotes, underscores and colons' do
+ let(:filename) { %q{a complex filename: containing - _ : etc., but still 'simple'.gif} }
+
+ it 'uses the simple format' do
+ expect(reference).to eq "#1[#{filename}]"
+ end
+ end
+
+ context 'when the filename contains HTML angle brackets' do
+ let(:filename) { 'a <em>great</em> filename.jpg' }
+
+ it 'uses Base64 encoding' do
+ expect(reference).to eq "#1[base64:#{Base64.strict_encode64(filename)}]"
+ end
+ end
+
+ context 'when the filename contains quotation marks' do
+ let(:filename) { %q{a "great" filename.jpg} }
+
+ it 'uses enclosing quotes, with backslash encoding' do
+ expect(reference).to eq %q{#1["a \"great\" filename.jpg"]}
+ end
+ end
+
+ context 'when the filename contains square brackets' do
+ let(:filename) { %q{a [great] filename.jpg} }
+
+ it 'uses enclosing quotes' do
+ expect(reference).to eq %q{#1["a [great] filename.jpg"]}
+ end
+ end
+ end
+
+ context 'when full is true' do
+ it 'returns complete path to the issue' do
+ refs = [
+ design.to_reference(full: true),
+ design.to_reference(project, full: true),
+ design.to_reference(group, full: true)
+ ]
+
+ expect(refs).to all(eq 'sample-namespace/sample-project#1/designs[homescreen.jpg]')
+ end
+ end
+
+ context 'when full is false' do
+ it 'returns complete path to the issue' do
+ refs = [
+ design.to_reference(build(:project), full: false),
+ design.to_reference(group, full: false)
+ ]
+
+ expect(refs).to all(eq 'sample-namespace/sample-project#1[homescreen.jpg]')
+ end
+ end
+
+ context 'when same project argument' do
+ it 'returns bare reference' do
+ expect(design.to_reference(project)).to eq("#1[homescreen.jpg]")
+ end
+ end
+ end
+
+ describe 'reference_pattern' do
+ let(:match) { described_class.reference_pattern.match(ref) }
+ let(:ref) { design.to_reference }
+ let(:design) { build(:design, filename: filename) }
+
+ context 'simple_file_name' do
+ let(:filename) { 'simple-file-name.jpg' }
+
+ it 'matches :simple_file_name' do
+ expect(match[:simple_file_name]).to eq(filename)
+ end
+ end
+
+ context 'quoted_file_name' do
+ let(:filename) { 'simple "file" name.jpg' }
+
+ it 'matches :simple_file_name' do
+ expect(match[:escaped_filename].gsub(/\\"/, '"')).to eq(filename)
+ end
+ end
+
+ context 'Base64 name' do
+ let(:filename) { '<>.png' }
+
+ it 'matches base_64_encoded_name' do
+ expect(Base64.decode64(match[:base_64_encoded_name])).to eq(filename)
+ end
+ end
+ end
+
+ describe '.by_issue_id_and_filename' do
+ let_it_be(:issue_a) { create(:issue) }
+ let_it_be(:issue_b) { create(:issue) }
+
+ let_it_be(:design_a) { create(:design, issue: issue_a) }
+ let_it_be(:design_b) { create(:design, issue: issue_a) }
+ let_it_be(:design_c) { create(:design, issue: issue_b, filename: design_a.filename) }
+ let_it_be(:design_d) { create(:design, issue: issue_b, filename: design_b.filename) }
+
+ it_behaves_like 'a where_composite scope', :by_issue_id_and_filename do
+ let(:all_results) { [design_a, design_b, design_c, design_d] }
+ let(:first_result) { design_a }
+
+ let(:composite_ids) do
+ all_results.map { |design| { issue_id: design.issue_id, filename: design.filename } }
+ end
+ end
+ end
+end
diff --git a/spec/models/design_management/repository_spec.rb b/spec/models/design_management/repository_spec.rb
new file mode 100644
index 00000000000..996316eeec9
--- /dev/null
+++ b/spec/models/design_management/repository_spec.rb
@@ -0,0 +1,58 @@
+# frozen_string_literal: true
+
+require 'spec_helper'
+
+describe DesignManagement::Repository do
+ let(:project) { create(:project) }
+ let(:repository) { described_class.new(project) }
+
+ shared_examples 'returns parsed git attributes that enable LFS for all file types' do
+ it do
+ expect(subject.patterns).to be_a_kind_of(Hash)
+ expect(subject.patterns).to have_key('/designs/*')
+ expect(subject.patterns['/designs/*']).to eql(
+ { "filter" => "lfs", "diff" => "lfs", "merge" => "lfs", "text" => false }
+ )
+ end
+ end
+
+ describe "#info_attributes" do
+ subject { repository.info_attributes }
+
+ include_examples 'returns parsed git attributes that enable LFS for all file types'
+ end
+
+ describe '#attributes_at' do
+ subject { repository.attributes_at }
+
+ include_examples 'returns parsed git attributes that enable LFS for all file types'
+ end
+
+ describe '#gitattribute' do
+ it 'returns a gitattribute when path has gitattributes' do
+ expect(repository.gitattribute('/designs/file.txt', 'filter')).to eq('lfs')
+ end
+
+ it 'returns nil when path has no gitattributes' do
+ expect(repository.gitattribute('/invalid/file.txt', 'filter')).to be_nil
+ end
+ end
+
+ describe '#copy_gitattributes' do
+ it 'always returns regardless of whether given a valid or invalid ref' do
+ expect(repository.copy_gitattributes('master')).to be true
+ expect(repository.copy_gitattributes('invalid')).to be true
+ end
+ end
+
+ describe '#attributes' do
+ it 'confirms that all files are LFS enabled' do
+ %w(png zip anything).each do |filetype|
+ path = "/#{DesignManagement.designs_directory}/file.#{filetype}"
+ attributes = repository.attributes(path)
+
+ expect(attributes['filter']).to eq('lfs')
+ end
+ end
+ end
+end
diff --git a/spec/models/design_management/version_spec.rb b/spec/models/design_management/version_spec.rb
new file mode 100644
index 00000000000..ab6958ea94a
--- /dev/null
+++ b/spec/models/design_management/version_spec.rb
@@ -0,0 +1,342 @@
+# frozen_string_literal: true
+require 'spec_helper'
+
+describe DesignManagement::Version do
+ let_it_be(:issue) { create(:issue) }
+
+ describe 'relations' do
+ it { is_expected.to have_many(:actions) }
+ it { is_expected.to have_many(:designs).through(:actions) }
+
+ it 'constrains the designs relation correctly' do
+ design = create(:design)
+ version = create(:design_version, designs: [design])
+
+ expect { version.designs << design }.to raise_error(ActiveRecord::RecordNotUnique)
+ end
+
+ it 'allows adding multiple versions to a single design' do
+ design = create(:design)
+ versions = create_list(:design_version, 2)
+
+ expect { versions.each { |v| design.versions << v } }
+ .not_to raise_error
+ end
+ end
+
+ describe 'validations' do
+ subject(:design_version) { build(:design_version) }
+
+ it { is_expected.to be_valid }
+ it { is_expected.to validate_presence_of(:author) }
+ it { is_expected.to validate_presence_of(:sha) }
+ it { is_expected.to validate_presence_of(:designs) }
+ it { is_expected.to validate_presence_of(:issue_id) }
+ it { is_expected.to validate_uniqueness_of(:sha).scoped_to(:issue_id).case_insensitive }
+ end
+
+ describe "scopes" do
+ let_it_be(:version_1) { create(:design_version) }
+ let_it_be(:version_2) { create(:design_version) }
+
+ describe ".for_designs" do
+ it "only returns versions related to the specified designs" do
+ _other_version = create(:design_version)
+ designs = [create(:design, versions: [version_1]),
+ create(:design, versions: [version_2])]
+
+ expect(described_class.for_designs(designs))
+ .to contain_exactly(version_1, version_2)
+ end
+ end
+
+ describe '.earlier_or_equal_to' do
+ it 'only returns versions created earlier or later than the given version' do
+ expect(described_class.earlier_or_equal_to(version_1)).to eq([version_1])
+ expect(described_class.earlier_or_equal_to(version_2)).to contain_exactly(version_1, version_2)
+ end
+
+ it 'can be passed either a DesignManagement::Version or an ID' do
+ [version_1, version_1.id].each do |arg|
+ expect(described_class.earlier_or_equal_to(arg)).to eq([version_1])
+ end
+ end
+ end
+
+ describe '.by_sha' do
+ it 'can find versions by sha' do
+ [version_1, version_2].each do |version|
+ expect(described_class.by_sha(version.sha)).to contain_exactly(version)
+ end
+ end
+ end
+ end
+
+ describe ".create_for_designs" do
+ def current_version_id(design)
+ design.send(:head_version).try(:id)
+ end
+
+ def as_actions(designs, action = :create)
+ designs.map do |d|
+ DesignManagement::DesignAction.new(d, action, action == :delete ? nil : :content)
+ end
+ end
+
+ let_it_be(:author) { create(:user) }
+ let_it_be(:design_a) { create(:design, issue: issue) }
+ let_it_be(:design_b) { create(:design, issue: issue) }
+ let_it_be(:designs) { [design_a, design_b] }
+
+ describe 'the error raised when there are no actions' do
+ let_it_be(:sha) { 'f00' }
+
+ def call_with_empty_actions
+ described_class.create_for_designs([], sha, author)
+ end
+
+ it 'raises CouldNotCreateVersion' do
+ expect { call_with_empty_actions }
+ .to raise_error(described_class::CouldNotCreateVersion)
+ end
+
+ it 'has an appropriate cause' do
+ expect { call_with_empty_actions }
+ .to raise_error(have_attributes(cause: ActiveRecord::RecordInvalid))
+ end
+
+ it 'provides extra data sentry can consume' do
+ extra_info = a_hash_including(sha: sha)
+
+ expect { call_with_empty_actions }
+ .to raise_error(have_attributes(sentry_extra_data: extra_info))
+ end
+ end
+
+ describe 'the error raised when the designs come from different issues' do
+ let_it_be(:sha) { 'f00' }
+ let_it_be(:designs) { create_list(:design, 2) }
+ let_it_be(:actions) { as_actions(designs) }
+
+ def call_with_mismatched_designs
+ described_class.create_for_designs(actions, sha, author)
+ end
+
+ it 'raises CouldNotCreateVersion' do
+ expect { call_with_mismatched_designs }
+ .to raise_error(described_class::CouldNotCreateVersion)
+ end
+
+ it 'has an appropriate cause' do
+ expect { call_with_mismatched_designs }
+ .to raise_error(have_attributes(cause: described_class::NotSameIssue))
+ end
+
+ it 'provides extra data sentry can consume' do
+ extra_info = a_hash_including(design_ids: designs.map(&:id))
+
+ expect { call_with_mismatched_designs }
+ .to raise_error(have_attributes(sentry_extra_data: extra_info))
+ end
+ end
+
+ it 'does not leave invalid versions around if creation fails' do
+ expect do
+ described_class.create_for_designs([], 'abcdef', author) rescue nil
+ end.not_to change { described_class.count }
+ end
+
+ it 'does not leave orphaned design-versions around if creation fails' do
+ actions = as_actions(designs)
+ expect do
+ described_class.create_for_designs(actions, '', author) rescue nil
+ end.not_to change { DesignManagement::Action.count }
+ end
+
+ it 'creates a version and links it to multiple designs' do
+ actions = as_actions(designs, :create)
+
+ version = described_class.create_for_designs(actions, 'abc', author)
+
+ expect(version.designs).to contain_exactly(*designs)
+ expect(designs.map(&method(:current_version_id))).to all(eq version.id)
+ end
+
+ it 'creates designs if they are new to git' do
+ actions = as_actions(designs, :create)
+
+ described_class.create_for_designs(actions, 'abc', author)
+
+ expect(designs.map(&:most_recent_action)).to all(be_creation)
+ end
+
+ it 'correctly associates the version with the issue' do
+ actions = as_actions(designs)
+
+ version = described_class.create_for_designs(actions, 'abc', author)
+
+ expect(version.issue).to eq(issue)
+ end
+
+ it 'correctly associates the version with the author' do
+ actions = as_actions(designs)
+
+ version = described_class.create_for_designs(actions, 'abc', author)
+
+ expect(version.author).to eq(author)
+ end
+
+ it 'modifies designs if git updated them' do
+ actions = as_actions(designs, :update)
+
+ described_class.create_for_designs(actions, 'abc', author)
+
+ expect(designs.map(&:most_recent_action)).to all(be_modification)
+ end
+
+ it 'deletes designs when the git action was delete' do
+ actions = as_actions(designs, :delete)
+
+ described_class.create_for_designs(actions, 'def', author)
+
+ expect(designs).to all(be_deleted)
+ end
+
+ it 're-creates designs if they are deleted' do
+ described_class.create_for_designs(as_actions(designs, :create), 'abc', author)
+ described_class.create_for_designs(as_actions(designs, :delete), 'def', author)
+
+ expect(designs).to all(be_deleted)
+
+ described_class.create_for_designs(as_actions(designs, :create), 'ghi', author)
+
+ expect(designs.map(&:most_recent_action)).to all(be_creation)
+ expect(designs).not_to include(be_deleted)
+ end
+
+ it 'changes the version of the designs' do
+ actions = as_actions([design_a])
+ described_class.create_for_designs(actions, 'before', author)
+
+ expect do
+ described_class.create_for_designs(actions, 'after', author)
+ end.to change { current_version_id(design_a) }
+ end
+ end
+
+ describe '#designs_by_event' do
+ context 'there is a single design' do
+ let_it_be(:design) { create(:design) }
+
+ shared_examples :a_correctly_categorised_design do |kind, category|
+ let_it_be(:version) { create(:design_version, kind => [design]) }
+
+ it 'returns a hash with a single key and the single design in that bucket' do
+ expect(version.designs_by_event).to eq(category => [design])
+ end
+ end
+
+ it_behaves_like :a_correctly_categorised_design, :created_designs, 'creation'
+ it_behaves_like :a_correctly_categorised_design, :modified_designs, 'modification'
+ it_behaves_like :a_correctly_categorised_design, :deleted_designs, 'deletion'
+ end
+
+ context 'there are a bunch of different designs in a variety of states' do
+ let_it_be(:version) do
+ create(:design_version,
+ created_designs: create_list(:design, 3),
+ modified_designs: create_list(:design, 4),
+ deleted_designs: create_list(:design, 5))
+ end
+
+ it 'puts them in the right buckets' do
+ expect(version.designs_by_event).to match(
+ a_hash_including(
+ 'creation' => have_attributes(size: 3),
+ 'modification' => have_attributes(size: 4),
+ 'deletion' => have_attributes(size: 5)
+ )
+ )
+ end
+
+ it 'does not suffer from N+1 queries' do
+ version.designs.map(&:id) # we don't care about the set-up queries
+ expect { version.designs_by_event }.not_to exceed_query_limit(2)
+ end
+ end
+ end
+
+ describe '#author' do
+ it 'returns the author' do
+ author = build(:user)
+ version = build(:design_version, author: author)
+
+ expect(version.author).to eq(author)
+ end
+
+ it 'returns nil if author_id is nil and version is not persisted' do
+ version = build(:design_version, author: nil)
+
+ expect(version.author).to eq(nil)
+ end
+
+ it 'retrieves author from the Commit if author_id is nil and version has been persisted' do
+ author = create(:user)
+ version = create(:design_version, :committed, author: author)
+ author.destroy
+ version.reload
+ commit = version.issue.project.design_repository.commit(version.sha)
+ commit_user = create(:user, email: commit.author_email, name: commit.author_name)
+
+ expect(version.author_id).to eq(nil)
+ expect(version.author).to eq(commit_user)
+ end
+ end
+
+ describe '#diff_refs' do
+ let(:project) { issue.project }
+
+ before do
+ expect(project.design_repository).to receive(:commit)
+ .once
+ .with(sha)
+ .and_return(commit)
+ end
+
+ subject { create(:design_version, issue: issue, sha: sha) }
+
+ context 'there is a commit in the repo by the SHA' do
+ let(:commit) { build(:commit) }
+ let(:sha) { commit.id }
+
+ it { is_expected.to have_attributes(diff_refs: commit.diff_refs) }
+
+ it 'memoizes calls to #diff_refs' do
+ expect(subject.diff_refs).to eq(subject.diff_refs)
+ end
+ end
+
+ context 'there is no commit in the repo by the SHA' do
+ let(:commit) { nil }
+ let(:sha) { Digest::SHA1.hexdigest("points to nothing") }
+
+ it { is_expected.to have_attributes(diff_refs: be_nil) }
+ end
+ end
+
+ describe '#reset' do
+ subject { create(:design_version, issue: issue) }
+
+ it 'removes memoized values' do
+ expect(subject).to receive(:commit).twice.and_return(nil)
+
+ subject.diff_refs
+ subject.diff_refs
+
+ subject.reset
+
+ subject.diff_refs
+ subject.diff_refs
+ end
+ end
+end
diff --git a/spec/models/design_user_mention_spec.rb b/spec/models/design_user_mention_spec.rb
new file mode 100644
index 00000000000..03c77c73c8d
--- /dev/null
+++ b/spec/models/design_user_mention_spec.rb
@@ -0,0 +1,12 @@
+# frozen_string_literal: true
+
+require 'spec_helper'
+
+describe DesignUserMention do
+ describe 'associations' do
+ it { is_expected.to belong_to(:design) }
+ it { is_expected.to belong_to(:note) }
+ end
+
+ it_behaves_like 'has user mentions'
+end
diff --git a/spec/models/diff_note_spec.rb b/spec/models/diff_note_spec.rb
index b802c8ba506..65f06a5b270 100644
--- a/spec/models/diff_note_spec.rb
+++ b/spec/models/diff_note_spec.rb
@@ -287,6 +287,24 @@ describe DiffNote do
reply_diff_note.reload.diff_file
end
end
+
+ context 'when noteable is a Design' do
+ it 'does not return a diff file' do
+ diff_note = create(:diff_note_on_design)
+
+ expect(diff_note.diff_file).to be_nil
+ end
+ end
+ end
+
+ describe '#latest_diff_file' do
+ context 'when noteable is a Design' do
+ it 'does not return a diff file' do
+ diff_note = create(:diff_note_on_design)
+
+ expect(diff_note.latest_diff_file).to be_nil
+ end
+ end
end
describe "#diff_line" do
diff --git a/spec/models/email_spec.rb b/spec/models/email_spec.rb
index aa3a60b867a..f7b194abcee 100644
--- a/spec/models/email_spec.rb
+++ b/spec/models/email_spec.rb
@@ -3,8 +3,14 @@
require 'spec_helper'
describe Email do
+ describe 'modules' do
+ subject { described_class }
+
+ it { is_expected.to include_module(AsyncDeviseEmail) }
+ end
+
describe 'validations' do
- it_behaves_like 'an object with email-formated attributes', :email do
+ it_behaves_like 'an object with RFC3696 compliant email-formated attributes', :email do
subject { build(:email) }
end
end
@@ -45,4 +51,16 @@ describe Email do
expect(build(:email, user: user).username).to eq user.username
end
end
+
+ describe 'Devise emails' do
+ let!(:user) { create(:user) }
+
+ describe 'behaviour' do
+ it 'sends emails asynchronously' do
+ expect do
+ user.emails.create!(email: 'hello@hello.com')
+ end.to have_enqueued_job.on_queue('mailers')
+ end
+ end
+ end
end
diff --git a/spec/models/environment_spec.rb b/spec/models/environment_spec.rb
index d0305d878e3..c0b2a4ae984 100644
--- a/spec/models/environment_spec.rb
+++ b/spec/models/environment_spec.rb
@@ -1311,4 +1311,25 @@ describe Environment, :use_clean_rails_memory_store_caching do
expect { environment.destroy }.to change { project.commit(deployment.ref_path) }.to(nil)
end
end
+
+ describe '.count_by_state' do
+ context 'when environments are not empty' do
+ let!(:environment1) { create(:environment, project: project, state: 'stopped') }
+ let!(:environment2) { create(:environment, project: project, state: 'available') }
+ let!(:environment3) { create(:environment, project: project, state: 'stopped') }
+
+ it 'returns the environments count grouped by state' do
+ expect(project.environments.count_by_state).to eq({ stopped: 2, available: 1 })
+ end
+
+ it 'returns the environments count grouped by state with zero value' do
+ environment2.update(state: 'stopped')
+ expect(project.environments.count_by_state).to eq({ stopped: 3, available: 0 })
+ end
+ end
+
+ it 'returns zero state counts when environments are empty' do
+ expect(project.environments.count_by_state).to eq({ stopped: 0, available: 0 })
+ end
+ end
end
diff --git a/spec/models/event_spec.rb b/spec/models/event_spec.rb
index 3239c7a843a..ac89f8fe9e1 100644
--- a/spec/models/event_spec.rb
+++ b/spec/models/event_spec.rb
@@ -84,6 +84,21 @@ describe Event do
end
end
+ describe 'scopes' do
+ describe 'created_at' do
+ it 'can find the right event' do
+ time = 1.day.ago
+ event = create(:event, created_at: time)
+ false_positive = create(:event, created_at: 2.days.ago)
+
+ found = described_class.created_at(time)
+
+ expect(found).to include(event)
+ expect(found).not_to include(false_positive)
+ end
+ end
+ end
+
describe "Push event" do
let(:project) { create(:project, :private) }
let(:user) { project.owner }
@@ -195,11 +210,13 @@ describe Event do
let(:confidential_issue) { create(:issue, :confidential, project: project, author: author, assignees: [assignee]) }
let(:project_snippet) { create(:project_snippet, :public, project: project, author: author) }
let(:personal_snippet) { create(:personal_snippet, :public, author: author) }
+ let(:design) { create(:design, issue: issue, project: project) }
let(:note_on_commit) { create(:note_on_commit, project: project) }
let(:note_on_issue) { create(:note_on_issue, noteable: issue, project: project) }
let(:note_on_confidential_issue) { create(:note_on_issue, noteable: confidential_issue, project: project) }
let(:note_on_project_snippet) { create(:note_on_project_snippet, author: author, noteable: project_snippet, project: project) }
let(:note_on_personal_snippet) { create(:note_on_personal_snippet, author: author, noteable: personal_snippet, project: nil) }
+ let(:note_on_design) { create(:note_on_design, author: author, noteable: design, project: project) }
let(:milestone_on_project) { create(:milestone, project: project) }
let(:event) do
described_class.new(project: project,
@@ -270,8 +287,16 @@ describe Event do
context 'private project' do
let(:project) { create(:project, :private, :repository) }
- include_examples 'visibility examples' do
- let(:visibility) { visible_to_none_except(:member, :admin) }
+ context 'when admin mode enabled', :enable_admin_mode do
+ include_examples 'visibility examples' do
+ let(:visibility) { visible_to_none_except(:member, :admin) }
+ end
+ end
+
+ context 'when admin mode disabled' do
+ include_examples 'visibility examples' do
+ let(:visibility) { visible_to_none_except(:member) }
+ end
end
end
end
@@ -283,6 +308,7 @@ describe Event do
include_examples 'visibility examples' do
let(:visibility) { visible_to_all }
end
+
include_examples 'visible to assignee and author', true
end
@@ -292,6 +318,7 @@ describe Event do
include_examples 'visibility examples' do
let(:visibility) { visible_to_none_except(:member, :admin) }
end
+
include_examples 'visible to assignee and author', true
end
end
@@ -303,6 +330,7 @@ describe Event do
include_examples 'visibility examples' do
let(:visibility) { visible_to_all }
end
+
include_examples 'visible to assignee and author', true
end
@@ -312,6 +340,7 @@ describe Event do
include_examples 'visibility examples' do
let(:visibility) { visible_to_none_except(:member, :admin) }
end
+
include_examples 'visible to assignee and author', true
end
@@ -319,8 +348,16 @@ describe Event do
let(:project) { private_project }
let(:target) { note_on_issue }
- include_examples 'visibility examples' do
- let(:visibility) { visible_to_none_except(:guest, :member, :admin) }
+ context 'when admin mode enabled', :enable_admin_mode do
+ include_examples 'visibility examples' do
+ let(:visibility) { visible_to_none_except(:guest, :member, :admin) }
+ end
+ end
+
+ context 'when admin mode disabled' do
+ include_examples 'visibility examples' do
+ let(:visibility) { visible_to_none_except(:guest, :member) }
+ end
end
include_examples 'visible to assignee and author', false
@@ -345,8 +382,16 @@ describe Event do
context 'private project' do
let(:project) { private_project }
- include_examples 'visibility examples' do
- let(:visibility) { visible_to_none_except(:member, :admin) }
+ context 'when admin mode enabled', :enable_admin_mode do
+ include_examples 'visibility examples' do
+ let(:visibility) { visible_to_none_except(:member, :admin) }
+ end
+ end
+
+ context 'when admin mode disabled' do
+ include_examples 'visibility examples' do
+ let(:visibility) { visible_to_none_except(:member) }
+ end
end
include_examples 'visible to assignee', false
@@ -363,16 +408,32 @@ describe Event do
context 'on public project with private issue tracker and merge requests' do
let(:project) { create(:project, :public, :issues_private, :merge_requests_private) }
- include_examples 'visibility examples' do
- let(:visibility) { visible_to_all_except(:logged_out, :non_member) }
+ context 'when admin mode enabled', :enable_admin_mode do
+ include_examples 'visibility examples' do
+ let(:visibility) { visible_to_all_except(:logged_out, :non_member) }
+ end
+ end
+
+ context 'when admin mode disabled' do
+ include_examples 'visibility examples' do
+ let(:visibility) { visible_to_all_except(:logged_out, :non_member, :admin) }
+ end
end
end
context 'on private project' do
let(:project) { create(:project, :private) }
- include_examples 'visibility examples' do
- let(:visibility) { visible_to_all_except(:logged_out, :non_member) }
+ context 'when admin mode enabled', :enable_admin_mode do
+ include_examples 'visibility examples' do
+ let(:visibility) { visible_to_all_except(:logged_out, :non_member) }
+ end
+ end
+
+ context 'when admin mode disabled' do
+ include_examples 'visibility examples' do
+ let(:visibility) { visible_to_all_except(:logged_out, :non_member, :admin) }
+ end
end
end
end
@@ -383,8 +444,16 @@ describe Event do
context 'on private project', :aggregate_failures do
let(:project) { create(:project, :wiki_repo) }
- include_examples 'visibility examples' do
- let(:visibility) { visible_to_all_except(:logged_out, :non_member) }
+ context 'when admin mode enabled', :enable_admin_mode do
+ include_examples 'visibility examples' do
+ let(:visibility) { visible_to_all_except(:logged_out, :non_member) }
+ end
+ end
+
+ context 'when admin mode disabled' do
+ include_examples 'visibility examples' do
+ let(:visibility) { visible_to_all_except(:logged_out, :non_member, :admin) }
+ end
end
end
@@ -407,22 +476,42 @@ describe Event do
context 'on public project with private snippets' do
let(:project) { create(:project, :public, :snippets_private) }
- include_examples 'visibility examples' do
- let(:visibility) { visible_to_none_except(:guest, :member, :admin) }
+ context 'when admin mode enabled', :enable_admin_mode do
+ include_examples 'visibility examples' do
+ let(:visibility) { visible_to_none_except(:guest, :member, :admin) }
+ end
+ end
+
+ context 'when admin mode disabled' do
+ include_examples 'visibility examples' do
+ let(:visibility) { visible_to_none_except(:guest, :member) }
+ end
end
+
# Normally, we'd expect the author of a comment to be able to view it.
# However, this doesn't seem to be the case for comments on snippets.
+
include_examples 'visible to author', false
end
context 'on private project' do
let(:project) { create(:project, :private) }
- include_examples 'visibility examples' do
- let(:visibility) { visible_to_none_except(:guest, :member, :admin) }
+ context 'when admin mode enabled', :enable_admin_mode do
+ include_examples 'visibility examples' do
+ let(:visibility) { visible_to_none_except(:guest, :member, :admin) }
+ end
end
+
+ context 'when admin mode disabled' do
+ include_examples 'visibility examples' do
+ let(:visibility) { visible_to_none_except(:guest, :member) }
+ end
+ end
+
# Normally, we'd expect the author of a comment to be able to view it.
# However, this doesn't seem to be the case for comments on snippets.
+
include_examples 'visible to author', false
end
end
@@ -433,6 +522,7 @@ describe Event do
include_examples 'visibility examples' do
let(:visibility) { visible_to_all }
end
+
include_examples 'visible to author', true
context 'on internal snippet' do
@@ -446,12 +536,47 @@ describe Event do
context 'on private snippet' do
let(:personal_snippet) { create(:personal_snippet, :private, author: author) }
- include_examples 'visibility examples' do
- let(:visibility) { visible_to_none_except(:admin) }
+ context 'when admin mode enabled', :enable_admin_mode do
+ include_examples 'visibility examples' do
+ let(:visibility) { visible_to_none_except(:admin) }
+ end
end
+
+ context 'when admin mode disabled' do
+ include_examples 'visibility examples' do
+ let(:visibility) { visible_to_none }
+ end
+ end
+
include_examples 'visible to author', true
end
end
+
+ context 'design event' do
+ include DesignManagementTestHelpers
+
+ let(:target) { note_on_design }
+
+ before do
+ enable_design_management
+ end
+
+ include_examples 'visibility examples' do
+ let(:visibility) { visible_to_all }
+ end
+
+ include_examples 'visible to assignee and author', true
+
+ context 'the event refers to a design on a confidential issue' do
+ let(:design) { create(:design, issue: confidential_issue, project: project) }
+
+ include_examples 'visibility examples' do
+ let(:visibility) { visible_to_none_except(:member, :admin) }
+ end
+
+ include_examples 'visible to assignee and author', true
+ end
+ end
end
describe 'wiki_page predicate scopes' do
@@ -483,6 +608,14 @@ describe Event do
expect(described_class.not_wiki_page).to match_array(non_wiki_events)
end
end
+
+ describe '.for_wiki_meta' do
+ it 'finds events for a given wiki page metadata object' do
+ event = events.select(&:wiki_page?).first
+
+ expect(described_class.for_wiki_meta(event.target)).to contain_exactly(event)
+ end
+ end
end
describe '#wiki_page and #wiki_page?' do
@@ -490,7 +623,7 @@ describe Event do
context 'for a wiki page event' do
let(:wiki_page) do
- create(:wiki_page, :with_real_page, project: project)
+ create(:wiki_page, project: project)
end
subject(:event) { create(:wiki_page_event, project: project, wiki_page: wiki_page) }
diff --git a/spec/models/group_spec.rb b/spec/models/group_spec.rb
index 576ac880fca..a4e49f88115 100644
--- a/spec/models/group_spec.rb
+++ b/spec/models/group_spec.rb
@@ -24,6 +24,8 @@ describe Group do
it { is_expected.to have_many(:cluster_groups).class_name('Clusters::Group') }
it { is_expected.to have_many(:clusters).class_name('Clusters::Cluster') }
it { is_expected.to have_many(:container_repositories) }
+ it { is_expected.to have_many(:milestones) }
+ it { is_expected.to have_many(:iterations) }
describe '#members & #requesters' do
let(:requester) { create(:user) }
@@ -553,114 +555,72 @@ describe Group do
group_access: GroupMember::DEVELOPER })
end
- context 'when feature flag share_group_with_group is enabled' do
- before do
- stub_feature_flags(share_group_with_group: true)
- end
-
- context 'with user in the group' do
- let(:user) { group_user }
-
- it 'returns correct access level' do
- expect(shared_group_parent.max_member_access_for_user(user)).to eq(Gitlab::Access::NO_ACCESS)
- expect(shared_group.max_member_access_for_user(user)).to eq(Gitlab::Access::DEVELOPER)
- expect(shared_group_child.max_member_access_for_user(user)).to eq(Gitlab::Access::DEVELOPER)
- end
+ context 'with user in the group' do
+ let(:user) { group_user }
- context 'with lower group access level than max access level for share' do
- let(:user) { create(:user) }
-
- it 'returns correct access level' do
- group.add_reporter(user)
-
- expect(shared_group_parent.max_member_access_for_user(user)).to eq(Gitlab::Access::NO_ACCESS)
- expect(shared_group.max_member_access_for_user(user)).to eq(Gitlab::Access::REPORTER)
- expect(shared_group_child.max_member_access_for_user(user)).to eq(Gitlab::Access::REPORTER)
- end
- end
+ it 'returns correct access level' do
+ expect(shared_group_parent.max_member_access_for_user(user)).to eq(Gitlab::Access::NO_ACCESS)
+ expect(shared_group.max_member_access_for_user(user)).to eq(Gitlab::Access::DEVELOPER)
+ expect(shared_group_child.max_member_access_for_user(user)).to eq(Gitlab::Access::DEVELOPER)
end
- context 'with user in the parent group' do
- let(:user) { parent_group_user }
+ context 'with lower group access level than max access level for share' do
+ let(:user) { create(:user) }
it 'returns correct access level' do
- expect(shared_group_parent.max_member_access_for_user(user)).to eq(Gitlab::Access::NO_ACCESS)
- expect(shared_group.max_member_access_for_user(user)).to eq(Gitlab::Access::NO_ACCESS)
- expect(shared_group_child.max_member_access_for_user(user)).to eq(Gitlab::Access::NO_ACCESS)
- end
- end
-
- context 'with user in the child group' do
- let(:user) { child_group_user }
+ group.add_reporter(user)
- it 'returns correct access level' do
expect(shared_group_parent.max_member_access_for_user(user)).to eq(Gitlab::Access::NO_ACCESS)
- expect(shared_group.max_member_access_for_user(user)).to eq(Gitlab::Access::NO_ACCESS)
- expect(shared_group_child.max_member_access_for_user(user)).to eq(Gitlab::Access::NO_ACCESS)
+ expect(shared_group.max_member_access_for_user(user)).to eq(Gitlab::Access::REPORTER)
+ expect(shared_group_child.max_member_access_for_user(user)).to eq(Gitlab::Access::REPORTER)
end
end
+ end
- context 'unrelated project owner' do
- let(:common_id) { [Project.maximum(:id).to_i, Namespace.maximum(:id).to_i].max + 999 }
- let!(:group) { create(:group, id: common_id) }
- let!(:unrelated_project) { create(:project, id: common_id) }
- let(:user) { unrelated_project.owner }
+ context 'with user in the parent group' do
+ let(:user) { parent_group_user }
- it 'returns correct access level' do
- expect(shared_group_parent.max_member_access_for_user(user)).to eq(Gitlab::Access::NO_ACCESS)
- expect(shared_group.max_member_access_for_user(user)).to eq(Gitlab::Access::NO_ACCESS)
- expect(shared_group_child.max_member_access_for_user(user)).to eq(Gitlab::Access::NO_ACCESS)
- end
+ it 'returns correct access level' do
+ expect(shared_group_parent.max_member_access_for_user(user)).to eq(Gitlab::Access::NO_ACCESS)
+ expect(shared_group.max_member_access_for_user(user)).to eq(Gitlab::Access::NO_ACCESS)
+ expect(shared_group_child.max_member_access_for_user(user)).to eq(Gitlab::Access::NO_ACCESS)
end
+ end
- context 'user without accepted access request' do
- let!(:user) { create(:user) }
-
- before do
- create(:group_member, :developer, :access_request, user: user, group: group)
- end
+ context 'with user in the child group' do
+ let(:user) { child_group_user }
- it 'returns correct access level' do
- expect(shared_group_parent.max_member_access_for_user(user)).to eq(Gitlab::Access::NO_ACCESS)
- expect(shared_group.max_member_access_for_user(user)).to eq(Gitlab::Access::NO_ACCESS)
- expect(shared_group_child.max_member_access_for_user(user)).to eq(Gitlab::Access::NO_ACCESS)
- end
+ it 'returns correct access level' do
+ expect(shared_group_parent.max_member_access_for_user(user)).to eq(Gitlab::Access::NO_ACCESS)
+ expect(shared_group.max_member_access_for_user(user)).to eq(Gitlab::Access::NO_ACCESS)
+ expect(shared_group_child.max_member_access_for_user(user)).to eq(Gitlab::Access::NO_ACCESS)
end
end
- context 'when feature flag share_group_with_group is disabled' do
- before do
- stub_feature_flags(share_group_with_group: false)
- end
-
- context 'with user in the group' do
- let(:user) { group_user }
+ context 'unrelated project owner' do
+ let(:common_id) { [Project.maximum(:id).to_i, Namespace.maximum(:id).to_i].max + 999 }
+ let!(:group) { create(:group, id: common_id) }
+ let!(:unrelated_project) { create(:project, id: common_id) }
+ let(:user) { unrelated_project.owner }
- it 'returns correct access level' do
- expect(shared_group_parent.max_member_access_for_user(user)).to eq(Gitlab::Access::NO_ACCESS)
- expect(shared_group.max_member_access_for_user(user)).to eq(Gitlab::Access::NO_ACCESS)
- expect(shared_group_child.max_member_access_for_user(user)).to eq(Gitlab::Access::NO_ACCESS)
- end
+ it 'returns correct access level' do
+ expect(shared_group_parent.max_member_access_for_user(user)).to eq(Gitlab::Access::NO_ACCESS)
+ expect(shared_group.max_member_access_for_user(user)).to eq(Gitlab::Access::NO_ACCESS)
+ expect(shared_group_child.max_member_access_for_user(user)).to eq(Gitlab::Access::NO_ACCESS)
end
+ end
- context 'with user in the parent group' do
- let(:user) { parent_group_user }
+ context 'user without accepted access request' do
+ let!(:user) { create(:user) }
- it 'returns correct access level' do
- expect(shared_group_parent.max_member_access_for_user(user)).to eq(Gitlab::Access::NO_ACCESS)
- expect(shared_group.max_member_access_for_user(user)).to eq(Gitlab::Access::NO_ACCESS)
- expect(shared_group_child.max_member_access_for_user(user)).to eq(Gitlab::Access::NO_ACCESS)
- end
+ before do
+ create(:group_member, :developer, :access_request, user: user, group: group)
end
- context 'with user in the child group' do
- let(:user) { child_group_user }
-
- it 'returns correct access level' do
- expect(shared_group_parent.max_member_access_for_user(user)).to eq(Gitlab::Access::NO_ACCESS)
- expect(shared_group.max_member_access_for_user(user)).to eq(Gitlab::Access::NO_ACCESS)
- expect(shared_group_child.max_member_access_for_user(user)).to eq(Gitlab::Access::NO_ACCESS)
- end
+ it 'returns correct access level' do
+ expect(shared_group_parent.max_member_access_for_user(user)).to eq(Gitlab::Access::NO_ACCESS)
+ expect(shared_group.max_member_access_for_user(user)).to eq(Gitlab::Access::NO_ACCESS)
+ expect(shared_group_child.max_member_access_for_user(user)).to eq(Gitlab::Access::NO_ACCESS)
end
end
end
@@ -672,8 +632,6 @@ describe Group do
let(:shared_group) { create(:group, :private, parent: shared_group_parent) }
before do
- stub_feature_flags(share_group_with_group: true)
-
group.add_owner(user)
create(:group_group_link, { shared_with_group: group,
@@ -701,6 +659,42 @@ describe Group do
end
end
+ describe '#members_from_self_and_ancestors_with_effective_access_level' do
+ let!(:group_parent) { create(:group, :private) }
+ let!(:group) { create(:group, :private, parent: group_parent) }
+ let!(:group_child) { create(:group, :private, parent: group) }
+
+ let!(:user) { create(:user) }
+
+ let(:parent_group_access_level) { Gitlab::Access::REPORTER }
+ let(:group_access_level) { Gitlab::Access::DEVELOPER }
+ let(:child_group_access_level) { Gitlab::Access::MAINTAINER }
+
+ before do
+ create(:group_member, user: user, group: group_parent, access_level: parent_group_access_level)
+ create(:group_member, user: user, group: group, access_level: group_access_level)
+ create(:group_member, user: user, group: group_child, access_level: child_group_access_level)
+ end
+
+ it 'returns effective access level for user' do
+ expect(group_parent.members_from_self_and_ancestors_with_effective_access_level.as_json).to(
+ contain_exactly(
+ hash_including('user_id' => user.id, 'access_level' => parent_group_access_level)
+ )
+ )
+ expect(group.members_from_self_and_ancestors_with_effective_access_level.as_json).to(
+ contain_exactly(
+ hash_including('user_id' => user.id, 'access_level' => group_access_level)
+ )
+ )
+ expect(group_child.members_from_self_and_ancestors_with_effective_access_level.as_json).to(
+ contain_exactly(
+ hash_including('user_id' => user.id, 'access_level' => child_group_access_level)
+ )
+ )
+ end
+ end
+
describe '#direct_and_indirect_members' do
let!(:group) { create(:group, :nested) }
let!(:sub_group) { create(:group, parent: group) }
diff --git a/spec/models/hooks/project_hook_spec.rb b/spec/models/hooks/project_hook_spec.rb
index a945f0d1516..ccf8171049d 100644
--- a/spec/models/hooks/project_hook_spec.rb
+++ b/spec/models/hooks/project_hook_spec.rb
@@ -11,6 +11,10 @@ describe ProjectHook do
it { is_expected.to validate_presence_of(:project) }
end
+ it_behaves_like 'includes Limitable concern' do
+ subject { build(:project_hook, project: create(:project)) }
+ end
+
describe '.push_hooks' do
it 'returns hooks for push events only' do
hook = create(:project_hook, push_events: true)
diff --git a/spec/models/issue_spec.rb b/spec/models/issue_spec.rb
index e8103be0682..dd5ff3dbdde 100644
--- a/spec/models/issue_spec.rb
+++ b/spec/models/issue_spec.rb
@@ -7,13 +7,30 @@ describe Issue do
describe "Associations" do
it { is_expected.to belong_to(:milestone) }
+ it { is_expected.to belong_to(:iteration) }
it { is_expected.to belong_to(:project) }
it { is_expected.to belong_to(:moved_to).class_name('Issue') }
+ it { is_expected.to have_one(:moved_from).class_name('Issue') }
it { is_expected.to belong_to(:duplicated_to).class_name('Issue') }
it { is_expected.to belong_to(:closed_by).class_name('User') }
it { is_expected.to have_many(:assignees) }
it { is_expected.to have_many(:user_mentions).class_name("IssueUserMention") }
+ it { is_expected.to have_many(:designs) }
+ it { is_expected.to have_many(:design_versions) }
it { is_expected.to have_one(:sentry_issue) }
+ it { is_expected.to have_one(:alert_management_alert) }
+ it { is_expected.to have_many(:resource_milestone_events) }
+ it { is_expected.to have_many(:resource_state_events) }
+
+ describe 'versions.most_recent' do
+ it 'returns the most recent version' do
+ issue = create(:issue)
+ create_list(:design_version, 2, issue: issue)
+ last_version = create(:design_version, issue: issue)
+
+ expect(issue.design_versions.most_recent).to eq(last_version)
+ end
+ end
end
describe 'modules' do
@@ -23,6 +40,8 @@ describe Issue do
it { is_expected.to include_module(Referable) }
it { is_expected.to include_module(Sortable) }
it { is_expected.to include_module(Taskable) }
+ it { is_expected.to include_module(MilestoneEventable) }
+ it { is_expected.to include_module(StateEventable) }
it_behaves_like 'AtomicInternalId' do
let(:internal_id_attribute) { :iid }
@@ -61,6 +80,18 @@ describe Issue do
end
end
+ describe '.with_alert_management_alerts' do
+ subject { described_class.with_alert_management_alerts }
+
+ it 'gets only issues with alerts' do
+ alert = create(:alert_management_alert, issue: create(:issue))
+ issue = create(:issue)
+
+ expect(subject).to contain_exactly(alert.issue)
+ expect(subject).not_to include(issue)
+ end
+ end
+
describe 'locking' do
using RSpec::Parameterized::TableSyntax
@@ -593,8 +624,15 @@ describe Issue do
context 'with an admin user' do
let(:user) { build(:admin) }
- it_behaves_like 'issue readable by user'
- it_behaves_like 'confidential issue readable by user'
+ context 'when admin mode is enabled', :enable_admin_mode do
+ it_behaves_like 'issue readable by user'
+ it_behaves_like 'confidential issue readable by user'
+ end
+
+ context 'when admin mode is disabled' do
+ it_behaves_like 'issue not readable by user'
+ it_behaves_like 'confidential issue not readable by user'
+ end
end
context 'with an owner' do
@@ -713,13 +751,29 @@ describe Issue do
expect(issue.visible_to_user?(user)).to be_falsy
end
- it 'does not check the external webservice for admins' do
- issue = build(:issue)
- user = build(:admin)
+ context 'with an admin' do
+ context 'when admin mode is enabled', :enable_admin_mode do
+ it 'does not check the external webservice' do
+ issue = build(:issue)
+ user = build(:admin)
- expect(::Gitlab::ExternalAuthorization).not_to receive(:access_allowed?)
+ expect(::Gitlab::ExternalAuthorization).not_to receive(:access_allowed?)
- issue.visible_to_user?(user)
+ issue.visible_to_user?(user)
+ end
+ end
+
+ context 'when admin mode is disabled' do
+ it 'checks the external service to determine if an issue is readable by the admin' do
+ project = build(:project, :public,
+ external_authorization_classification_label: 'a-label')
+ issue = build(:issue, project: project)
+ user = build(:admin)
+
+ expect(::Gitlab::ExternalAuthorization).to receive(:access_allowed?).with(user, 'a-label') { false }
+ expect(issue.visible_to_user?(user)).to be_falsy
+ end
+ end
end
end
@@ -967,4 +1021,68 @@ describe Issue do
expect(issue.previous_updated_at).to eq(Time.new(2013, 02, 06))
end
end
+
+ describe '#design_collection' do
+ it 'returns a design collection' do
+ issue = build(:issue)
+ collection = issue.design_collection
+
+ expect(collection).to be_a(DesignManagement::DesignCollection)
+ expect(collection.issue).to eq(issue)
+ end
+ end
+
+ describe 'current designs' do
+ let(:issue) { create(:issue) }
+
+ subject { issue.designs.current }
+
+ context 'an issue has no designs' do
+ it { is_expected.to be_empty }
+ end
+
+ context 'an issue only has current designs' do
+ let!(:design_a) { create(:design, :with_file, issue: issue) }
+ let!(:design_b) { create(:design, :with_file, issue: issue) }
+ let!(:design_c) { create(:design, :with_file, issue: issue) }
+
+ it { is_expected.to include(design_a, design_b, design_c) }
+ end
+
+ context 'an issue only has deleted designs' do
+ let!(:design_a) { create(:design, :with_file, issue: issue, deleted: true) }
+ let!(:design_b) { create(:design, :with_file, issue: issue, deleted: true) }
+ let!(:design_c) { create(:design, :with_file, issue: issue, deleted: true) }
+
+ it { is_expected.to be_empty }
+ end
+
+ context 'an issue has a mixture of current and deleted designs' do
+ let!(:design_a) { create(:design, :with_file, issue: issue) }
+ let!(:design_b) { create(:design, :with_file, issue: issue, deleted: true) }
+ let!(:design_c) { create(:design, :with_file, issue: issue) }
+
+ it { is_expected.to contain_exactly(design_a, design_c) }
+ end
+ end
+
+ describe '.with_label_attributes' do
+ subject { described_class.with_label_attributes(label_attributes) }
+
+ let(:label_attributes) { { title: 'hello world', description: 'hi' } }
+
+ it 'gets issues with given label attributes' do
+ label = create(:label, **label_attributes)
+ labeled_issue = create(:labeled_issue, project: label.project, labels: [label])
+
+ expect(subject).to include(labeled_issue)
+ end
+
+ it 'excludes issues without given label attributes' do
+ label = create(:label, title: 'GitLab', description: 'tanuki')
+ labeled_issue = create(:labeled_issue, project: label.project, labels: [label])
+
+ expect(subject).not_to include(labeled_issue)
+ end
+ end
end
diff --git a/spec/models/iteration_spec.rb b/spec/models/iteration_spec.rb
new file mode 100644
index 00000000000..e5b7b746639
--- /dev/null
+++ b/spec/models/iteration_spec.rb
@@ -0,0 +1,170 @@
+# frozen_string_literal: true
+
+require 'spec_helper'
+
+describe Iteration do
+ let_it_be(:project) { create(:project) }
+ let_it_be(:group) { create(:group) }
+
+ it_behaves_like 'a timebox', :iteration do
+ let(:timebox_table_name) { described_class.table_name.to_sym }
+ end
+
+ describe "#iid" do
+ it "is properly scoped on project and group" do
+ iteration1 = create(:iteration, project: project)
+ iteration2 = create(:iteration, project: project)
+ iteration3 = create(:iteration, group: group)
+ iteration4 = create(:iteration, group: group)
+ iteration5 = create(:iteration, project: project)
+
+ want = {
+ iteration1: 1,
+ iteration2: 2,
+ iteration3: 1,
+ iteration4: 2,
+ iteration5: 3
+ }
+ got = {
+ iteration1: iteration1.iid,
+ iteration2: iteration2.iid,
+ iteration3: iteration3.iid,
+ iteration4: iteration4.iid,
+ iteration5: iteration5.iid
+ }
+ expect(got).to eq(want)
+ end
+ end
+
+ context 'Validations' do
+ subject { build(:iteration, group: group, start_date: start_date, due_date: due_date) }
+
+ describe '#dates_do_not_overlap' do
+ let_it_be(:existing_iteration) { create(:iteration, group: group, start_date: 4.days.from_now, due_date: 1.week.from_now) }
+
+ context 'when no Iteration dates overlap' do
+ let(:start_date) { 2.weeks.from_now }
+ let(:due_date) { 3.weeks.from_now }
+
+ it { is_expected.to be_valid }
+ end
+
+ context 'when dates overlap' do
+ context 'same group' do
+ context 'when start_date is in range' do
+ let(:start_date) { 5.days.from_now }
+ let(:due_date) { 3.weeks.from_now }
+
+ it 'is not valid' do
+ expect(subject).not_to be_valid
+ expect(subject.errors[:base]).to include('Dates cannot overlap with other existing Iterations')
+ end
+ end
+
+ context 'when end_date is in range' do
+ let(:start_date) { Time.now }
+ let(:due_date) { 6.days.from_now }
+
+ it 'is not valid' do
+ expect(subject).not_to be_valid
+ expect(subject.errors[:base]).to include('Dates cannot overlap with other existing Iterations')
+ end
+ end
+
+ context 'when both overlap' do
+ let(:start_date) { 5.days.from_now }
+ let(:due_date) { 6.days.from_now }
+
+ it 'is not valid' do
+ expect(subject).not_to be_valid
+ expect(subject.errors[:base]).to include('Dates cannot overlap with other existing Iterations')
+ end
+ end
+ end
+
+ context 'different group' do
+ let(:start_date) { 5.days.from_now }
+ let(:due_date) { 6.days.from_now }
+ let(:group) { create(:group) }
+
+ it { is_expected.to be_valid }
+ end
+ end
+ end
+
+ describe '#future_date' do
+ context 'when dates are in the future' do
+ let(:start_date) { Time.now }
+ let(:due_date) { 1.week.from_now }
+
+ it { is_expected.to be_valid }
+ end
+
+ context 'when start_date is in the past' do
+ let(:start_date) { 1.week.ago }
+ let(:due_date) { 1.week.from_now }
+
+ it 'is not valid' do
+ expect(subject).not_to be_valid
+ expect(subject.errors[:start_date]).to include('cannot be in the past')
+ end
+ end
+
+ context 'when due_date is in the past' do
+ let(:start_date) { Time.now }
+ let(:due_date) { 1.week.ago }
+
+ it 'is not valid' do
+ expect(subject).not_to be_valid
+ expect(subject.errors[:due_date]).to include('cannot be in the past')
+ end
+ end
+
+ context 'when start_date is over 500 years in the future' do
+ let(:start_date) { 501.years.from_now }
+ let(:due_date) { Time.now }
+
+ it 'is not valid' do
+ expect(subject).not_to be_valid
+ expect(subject.errors[:start_date]).to include('cannot be more than 500 years in the future')
+ end
+ end
+
+ context 'when due_date is over 500 years in the future' do
+ let(:start_date) { Time.now }
+ let(:due_date) { 501.years.from_now }
+
+ it 'is not valid' do
+ expect(subject).not_to be_valid
+ expect(subject.errors[:due_date]).to include('cannot be more than 500 years in the future')
+ end
+ end
+ end
+ end
+
+ describe '.within_timeframe' do
+ let_it_be(:now) { Time.now }
+ let_it_be(:project) { create(:project, :empty_repo) }
+ let_it_be(:iteration_1) { create(:iteration, project: project, start_date: now, due_date: 1.day.from_now) }
+ let_it_be(:iteration_2) { create(:iteration, project: project, start_date: 2.days.from_now, due_date: 3.days.from_now) }
+ let_it_be(:iteration_3) { create(:iteration, project: project, start_date: 4.days.from_now, due_date: 1.week.from_now) }
+
+ it 'returns iterations with start_date and/or end_date between timeframe' do
+ iterations = described_class.within_timeframe(2.days.from_now, 3.days.from_now)
+
+ expect(iterations).to match_array([iteration_2])
+ end
+
+ it 'returns iterations which starts before the timeframe' do
+ iterations = described_class.within_timeframe(1.day.from_now, 3.days.from_now)
+
+ expect(iterations).to match_array([iteration_1, iteration_2])
+ end
+
+ it 'returns iterations which ends after the timeframe' do
+ iterations = described_class.within_timeframe(3.days.from_now, 5.days.from_now)
+
+ expect(iterations).to match_array([iteration_2, iteration_3])
+ end
+ end
+end
diff --git a/spec/models/jira_import_state_spec.rb b/spec/models/jira_import_state_spec.rb
index 4d91bf25b5e..99f9e035205 100644
--- a/spec/models/jira_import_state_spec.rb
+++ b/spec/models/jira_import_state_spec.rb
@@ -124,6 +124,7 @@ describe JiraImportState do
jira_import.schedule
expect(jira_import.jid).to eq('some-job-id')
+ expect(jira_import.scheduled_at).to be_within(1.second).of(Time.now)
end
end
diff --git a/spec/models/member_spec.rb b/spec/models/member_spec.rb
index eeb2350359c..a8d864ad3f0 100644
--- a/spec/models/member_spec.rb
+++ b/spec/models/member_spec.rb
@@ -241,10 +241,22 @@ describe Member do
expect(member).to be_persisted
end
- it 'sets members.created_by to the given current_user' do
- member = described_class.add_user(source, user, :maintainer, current_user: admin)
+ context 'when admin mode is enabled', :enable_admin_mode do
+ it 'sets members.created_by to the given admin current_user' do
+ member = described_class.add_user(source, user, :maintainer, current_user: admin)
- expect(member.created_by).to eq(admin)
+ expect(member.created_by).to eq(admin)
+ end
+ end
+
+ context 'when admin mode is disabled' do
+ # Skipped because `Group#max_member_access_for_user` needs to be migrated to use admin mode
+ # https://gitlab.com/gitlab-org/gitlab/-/issues/207950
+ xit 'rejects setting members.created_by to the given admin current_user' do
+ member = described_class.add_user(source, user, :maintainer, current_user: admin)
+
+ expect(member.created_by).not_to be_persisted
+ end
end
it 'sets members.expires_at to the given expires_at' do
@@ -353,7 +365,7 @@ describe Member do
end
end
- context 'when current_user can update member' do
+ context 'when current_user can update member', :enable_admin_mode do
it 'creates the member' do
expect(source.users).not_to include(user)
@@ -421,7 +433,7 @@ describe Member do
end
end
- context 'when current_user can update member' do
+ context 'when current_user can update member', :enable_admin_mode do
it 'updates the member' do
expect(source.users).to include(user)
diff --git a/spec/models/merge_request_diff_spec.rb b/spec/models/merge_request_diff_spec.rb
index 016af4f269b..0839dde696a 100644
--- a/spec/models/merge_request_diff_spec.rb
+++ b/spec/models/merge_request_diff_spec.rb
@@ -3,6 +3,8 @@
require 'spec_helper'
describe MergeRequestDiff do
+ using RSpec::Parameterized::TableSyntax
+
include RepoHelpers
let(:diff_with_commits) { create(:merge_request).merge_request_diff }
@@ -125,18 +127,71 @@ describe MergeRequestDiff do
end
end
+ describe '#update_external_diff_store' do
+ let_it_be(:merge_request) { create(:merge_request) }
+
+ let(:diff) { merge_request.merge_request_diff }
+ let(:store) { diff.external_diff.object_store }
+
+ where(:change_stored_externally, :change_external_diff) do
+ false | false
+ false | true
+ true | false
+ true | true
+ end
+
+ with_them do
+ it do
+ diff.stored_externally = true if change_stored_externally
+ diff.external_diff = "new-filename" if change_external_diff
+
+ update_store = receive(:update_column).with(:external_diff_store, store)
+
+ if change_stored_externally || change_external_diff
+ expect(diff).to update_store
+ else
+ expect(diff).not_to update_store
+ end
+
+ diff.save!
+ end
+ end
+ end
+
describe '#migrate_files_to_external_storage!' do
+ let(:uploader) { ExternalDiffUploader }
+ let(:file_store) { uploader::Store::LOCAL }
+ let(:remote_store) { uploader::Store::REMOTE }
let(:diff) { create(:merge_request).merge_request_diff }
- it 'converts from in-database to external storage' do
+ it 'converts from in-database to external file storage' do
expect(diff).not_to be_stored_externally
stub_external_diffs_setting(enabled: true)
- expect(diff).to receive(:save!)
+
+ expect(diff).to receive(:save!).and_call_original
+
+ diff.migrate_files_to_external_storage!
+
+ expect(diff).to be_stored_externally
+ expect(diff.external_diff_store).to eq(file_store)
+ end
+
+ it 'converts from in-database to external object storage' do
+ expect(diff).not_to be_stored_externally
+
+ stub_external_diffs_setting(enabled: true)
+
+ # Without direct_upload: true, the files would be saved to disk, and a
+ # background job would be enqueued to move the file to object storage
+ stub_external_diffs_object_storage(uploader, direct_upload: true)
+
+ expect(diff).to receive(:save!).and_call_original
diff.migrate_files_to_external_storage!
expect(diff).to be_stored_externally
+ expect(diff.external_diff_store).to eq(remote_store)
end
it 'does nothing with an external diff' do
diff --git a/spec/models/merge_request_spec.rb b/spec/models/merge_request_spec.rb
index cbb837c139e..fc4590f7b22 100644
--- a/spec/models/merge_request_spec.rb
+++ b/spec/models/merge_request_spec.rb
@@ -18,6 +18,10 @@ describe MergeRequest do
it { is_expected.to have_many(:assignees).through(:merge_request_assignees) }
it { is_expected.to have_many(:merge_request_diffs) }
it { is_expected.to have_many(:user_mentions).class_name("MergeRequestUserMention") }
+ it { is_expected.to belong_to(:milestone) }
+ it { is_expected.to belong_to(:iteration) }
+ it { is_expected.to have_many(:resource_milestone_events) }
+ it { is_expected.to have_many(:resource_state_events) }
context 'for forks' do
let!(:project) { create(:project) }
@@ -176,6 +180,8 @@ describe MergeRequest do
it { is_expected.to include_module(Referable) }
it { is_expected.to include_module(Sortable) }
it { is_expected.to include_module(Taskable) }
+ it { is_expected.to include_module(MilestoneEventable) }
+ it { is_expected.to include_module(StateEventable) }
it_behaves_like 'AtomicInternalId' do
let(:internal_id_attribute) { :iid }
@@ -1610,6 +1616,32 @@ describe MergeRequest do
end
end
+ describe '#has_accessibility_reports?' do
+ subject { merge_request.has_accessibility_reports? }
+
+ let(:project) { create(:project, :repository) }
+
+ context 'when head pipeline has an accessibility reports' do
+ let(:merge_request) { create(:merge_request, :with_accessibility_reports, source_project: project) }
+
+ it { is_expected.to be_truthy }
+
+ context 'when feature flag is disabled' do
+ before do
+ stub_feature_flags(accessibility_report_view: false)
+ end
+
+ it { is_expected.to be_falsey }
+ end
+ end
+
+ context 'when head pipeline does not have accessibility reports' do
+ let(:merge_request) { create(:merge_request, source_project: project) }
+
+ it { is_expected.to be_falsey }
+ end
+ end
+
describe '#has_coverage_reports?' do
subject { merge_request.has_coverage_reports? }
@@ -1628,6 +1660,26 @@ describe MergeRequest do
end
end
+ describe '#has_terraform_reports?' do
+ let_it_be(:project) { create(:project, :repository) }
+
+ context 'when head pipeline has terraform reports' do
+ it 'returns true' do
+ merge_request = create(:merge_request, :with_terraform_reports, source_project: project)
+
+ expect(merge_request.has_terraform_reports?).to be_truthy
+ end
+ end
+
+ context 'when head pipeline does not have terraform reports' do
+ it 'returns false' do
+ merge_request = create(:merge_request, source_project: project)
+
+ expect(merge_request.has_terraform_reports?).to be_falsey
+ end
+ end
+ end
+
describe '#calculate_reactive_cache' do
let(:project) { create(:project, :repository) }
let(:merge_request) { create(:merge_request, source_project: project) }
@@ -1837,6 +1889,62 @@ describe MergeRequest do
end
end
+ describe '#compare_accessibility_reports' do
+ let_it_be(:project) { create(:project, :repository) }
+ let_it_be(:merge_request, reload: true) { create(:merge_request, :with_accessibility_reports, source_project: project) }
+ let_it_be(:pipeline) { merge_request.head_pipeline }
+
+ subject { merge_request.compare_accessibility_reports }
+
+ context 'when head pipeline has accessibility reports' do
+ let(:job) do
+ create(:ci_build, options: { artifacts: { reports: { pa11y: ['accessibility.json'] } } }, pipeline: pipeline)
+ end
+
+ let(:artifacts_metadata) { create(:ci_job_artifact, :metadata, job: job) }
+
+ context 'when reactive cache worker is parsing results asynchronously' do
+ it 'returns parsing status' do
+ expect(subject[:status]).to eq(:parsing)
+ end
+ end
+
+ context 'when reactive cache worker is inline' do
+ before do
+ synchronous_reactive_cache(merge_request)
+ end
+
+ it 'returns parsed status' do
+ expect(subject[:status]).to eq(:parsed)
+ expect(subject[:data]).to be_present
+ end
+
+ context 'when an error occurrs' do
+ before do
+ merge_request.update!(head_pipeline: nil)
+ end
+
+ it 'returns an error status' do
+ expect(subject[:status]).to eq(:error)
+ expect(subject[:status_reason]).to eq("This merge request does not have accessibility reports")
+ end
+ end
+
+ context 'when cached result is not latest' do
+ before do
+ allow_next_instance_of(Ci::CompareAccessibilityReportsService) do |service|
+ allow(service).to receive(:latest?).and_return(false)
+ end
+ end
+
+ it 'raises an InvalidateReactiveCache error' do
+ expect { subject }.to raise_error(ReactiveCaching::InvalidateReactiveCache)
+ end
+ end
+ end
+ end
+ end
+
describe '#all_commit_shas' do
context 'when merge request is persisted' do
let(:all_commit_shas) do
@@ -3678,41 +3786,41 @@ describe MergeRequest do
describe '#recent_visible_deployments' do
let(:merge_request) { create(:merge_request) }
- let(:environment) do
- create(:environment, project: merge_request.target_project)
- end
-
it 'returns visible deployments' do
+ envs = create_list(:environment, 3, project: merge_request.target_project)
+
created = create(
:deployment,
:created,
project: merge_request.target_project,
- environment: environment
+ environment: envs[0]
)
success = create(
:deployment,
:success,
project: merge_request.target_project,
- environment: environment
+ environment: envs[1]
)
failed = create(
:deployment,
:failed,
project: merge_request.target_project,
- environment: environment
+ environment: envs[2]
)
- merge_request.deployment_merge_requests.create!(deployment: created)
- merge_request.deployment_merge_requests.create!(deployment: success)
- merge_request.deployment_merge_requests.create!(deployment: failed)
+ merge_request_relation = MergeRequest.where(id: merge_request.id)
+ created.link_merge_requests(merge_request_relation)
+ success.link_merge_requests(merge_request_relation)
+ failed.link_merge_requests(merge_request_relation)
expect(merge_request.recent_visible_deployments).to eq([failed, success])
end
it 'only returns a limited number of deployments' do
20.times do
+ environment = create(:environment, project: merge_request.target_project)
deploy = create(
:deployment,
:success,
@@ -3720,7 +3828,7 @@ describe MergeRequest do
environment: environment
)
- merge_request.deployment_merge_requests.create!(deployment: deploy)
+ deploy.link_merge_requests(MergeRequest.where(id: merge_request.id))
end
expect(merge_request.recent_visible_deployments.count).to eq(10)
@@ -3728,40 +3836,28 @@ describe MergeRequest do
end
describe '#diffable_merge_ref?' do
- context 'diff_compare_with_head enabled' do
- context 'merge request can be merged' do
- context 'merge_to_ref is not calculated' do
- it 'returns true' do
- expect(subject.diffable_merge_ref?).to eq(false)
- end
- end
-
- context 'merge_to_ref is calculated' do
- before do
- MergeRequests::MergeToRefService.new(subject.project, subject.author).execute(subject)
- end
-
- it 'returns true' do
- expect(subject.diffable_merge_ref?).to eq(true)
- end
+ context 'merge request can be merged' do
+ context 'merge_to_ref is not calculated' do
+ it 'returns true' do
+ expect(subject.diffable_merge_ref?).to eq(false)
end
end
- context 'merge request cannot be merged' do
- it 'returns false' do
- subject.mark_as_unchecked!
+ context 'merge_to_ref is calculated' do
+ before do
+ MergeRequests::MergeToRefService.new(subject.project, subject.author).execute(subject)
+ end
- expect(subject.diffable_merge_ref?).to eq(false)
+ it 'returns true' do
+ expect(subject.diffable_merge_ref?).to eq(true)
end
end
end
- context 'diff_compare_with_head disabled' do
- before do
- stub_feature_flags(diff_compare_with_head: { enabled: false, thing: subject.target_project })
- end
-
+ context 'merge request cannot be merged' do
it 'returns false' do
+ subject.mark_as_unchecked!
+
expect(subject.diffable_merge_ref?).to eq(false)
end
end
diff --git a/spec/models/metrics/users_starred_dashboard_spec.rb b/spec/models/metrics/users_starred_dashboard_spec.rb
new file mode 100644
index 00000000000..6cb14ae569e
--- /dev/null
+++ b/spec/models/metrics/users_starred_dashboard_spec.rb
@@ -0,0 +1,39 @@
+# frozen_string_literal: true
+
+require 'spec_helper'
+
+describe Metrics::UsersStarredDashboard do
+ describe 'associations' do
+ it { is_expected.to belong_to(:project).inverse_of(:metrics_users_starred_dashboards) }
+ it { is_expected.to belong_to(:user).inverse_of(:metrics_users_starred_dashboards) }
+ end
+
+ describe 'validation' do
+ subject { build(:metrics_users_starred_dashboard) }
+
+ it { is_expected.to validate_presence_of(:user_id) }
+ it { is_expected.to validate_presence_of(:project_id) }
+ it { is_expected.to validate_presence_of(:dashboard_path) }
+ it { is_expected.to validate_length_of(:dashboard_path).is_at_most(255) }
+ it { is_expected.to validate_uniqueness_of(:dashboard_path).scoped_to(%i[user_id project_id]) }
+ end
+
+ context 'scopes' do
+ let_it_be(:project) { create(:project) }
+ let_it_be(:starred_dashboard_a) { create(:metrics_users_starred_dashboard, project: project, dashboard_path: 'path_a') }
+ let_it_be(:starred_dashboard_b) { create(:metrics_users_starred_dashboard, project: project, dashboard_path: 'path_b') }
+ let_it_be(:starred_dashboard_c) { create(:metrics_users_starred_dashboard, dashboard_path: 'path_b') }
+
+ describe '#for_project' do
+ it 'selects only starred dashboards belonging to project' do
+ expect(described_class.for_project(project)).to contain_exactly starred_dashboard_a, starred_dashboard_b
+ end
+ end
+
+ describe '#for_project_dashboard' do
+ it 'selects only starred dashboards belonging to project with given dashboard path' do
+ expect(described_class.for_project_dashboard(project, 'path_b')).to contain_exactly starred_dashboard_b
+ end
+ end
+ end
+end
diff --git a/spec/models/milestone_note_spec.rb b/spec/models/milestone_note_spec.rb
index 9e77ef91bb2..aad65cf0346 100644
--- a/spec/models/milestone_note_spec.rb
+++ b/spec/models/milestone_note_spec.rb
@@ -14,5 +14,15 @@ describe MilestoneNote do
it_behaves_like 'a system note', exclude_project: true do
let(:action) { 'milestone' }
end
+
+ context 'with a remove milestone event' do
+ let(:milestone) { create(:milestone) }
+ let(:event) { create(:resource_milestone_event, action: :remove, issue: noteable, milestone: milestone) }
+
+ it 'creates the expected note' do
+ expect(subject.note_html).to include('removed milestone')
+ expect(subject.note_html).not_to include('changed milestone to')
+ end
+ end
end
end
diff --git a/spec/models/milestone_spec.rb b/spec/models/milestone_spec.rb
index ee4c35ebddd..e51108947a7 100644
--- a/spec/models/milestone_spec.rb
+++ b/spec/models/milestone_spec.rb
@@ -3,8 +3,10 @@
require 'spec_helper'
describe Milestone do
+ it_behaves_like 'a timebox', :milestone
+
describe 'MilestoneStruct#serializable_hash' do
- let(:predefined_milestone) { described_class::MilestoneStruct.new('Test Milestone', '#test', 1) }
+ let(:predefined_milestone) { described_class::TimeboxStruct.new('Test Milestone', '#test', 1) }
it 'presents the predefined milestone as a hash' do
expect(predefined_milestone.serializable_hash).to eq(
@@ -15,69 +17,11 @@ describe Milestone do
end
end
- describe 'modules' do
- context 'with a project' do
- it_behaves_like 'AtomicInternalId' do
- let(:internal_id_attribute) { :iid }
- let(:instance) { build(:milestone, project: build(:project), group: nil) }
- let(:scope) { :project }
- let(:scope_attrs) { { project: instance.project } }
- let(:usage) { :milestones }
- end
- end
-
- context 'with a group' do
- it_behaves_like 'AtomicInternalId' do
- let(:internal_id_attribute) { :iid }
- let(:instance) { build(:milestone, project: nil, group: build(:group)) }
- let(:scope) { :group }
- let(:scope_attrs) { { namespace: instance.group } }
- let(:usage) { :milestones }
- end
- end
- end
-
describe "Validation" do
before do
allow(subject).to receive(:set_iid).and_return(false)
end
- describe 'start_date' do
- it 'adds an error when start_date is greater then due_date' do
- milestone = build(:milestone, start_date: Date.tomorrow, due_date: Date.yesterday)
-
- expect(milestone).not_to be_valid
- expect(milestone.errors[:due_date]).to include("must be greater than start date")
- end
-
- it 'adds an error when start_date is greater than 9999-12-31' do
- milestone = build(:milestone, start_date: Date.new(10000, 1, 1))
-
- expect(milestone).not_to be_valid
- expect(milestone.errors[:start_date]).to include("date must not be after 9999-12-31")
- end
- end
-
- describe 'due_date' do
- it 'adds an error when due_date is greater than 9999-12-31' do
- milestone = build(:milestone, due_date: Date.new(10000, 1, 1))
-
- expect(milestone).not_to be_valid
- expect(milestone.errors[:due_date]).to include("date must not be after 9999-12-31")
- end
- end
-
- describe 'title' do
- it { is_expected.to validate_presence_of(:title) }
-
- it 'is invalid if title would be empty after sanitation' do
- milestone = build(:milestone, project: project, title: '<img src=x onerror=prompt(1)>')
-
- expect(milestone).not_to be_valid
- expect(milestone.errors[:title]).to include("can't be blank")
- end
- end
-
describe 'milestone_releases' do
let(:milestone) { build(:milestone, project: project) }
@@ -99,8 +43,6 @@ describe Milestone do
end
describe "Associations" do
- it { is_expected.to belong_to(:project) }
- it { is_expected.to have_many(:issues) }
it { is_expected.to have_many(:releases) }
it { is_expected.to have_many(:milestone_releases) }
end
@@ -110,87 +52,6 @@ describe Milestone do
let(:issue) { create(:issue, project: project) }
let(:user) { create(:user) }
- describe "#title" do
- let(:milestone) { create(:milestone, title: "<b>foo & bar -> 2.2</b>") }
-
- it "sanitizes title" do
- expect(milestone.title).to eq("foo & bar -> 2.2")
- end
- end
-
- describe '#merge_requests_enabled?' do
- context "per project" do
- it "is true for projects with MRs enabled" do
- project = create(:project, :merge_requests_enabled)
- milestone = create(:milestone, project: project)
-
- expect(milestone.merge_requests_enabled?).to be(true)
- end
-
- it "is false for projects with MRs disabled" do
- project = create(:project, :repository_enabled, :merge_requests_disabled)
- milestone = create(:milestone, project: project)
-
- expect(milestone.merge_requests_enabled?).to be(false)
- end
-
- it "is false for projects with repository disabled" do
- project = create(:project, :repository_disabled)
- milestone = create(:milestone, project: project)
-
- expect(milestone.merge_requests_enabled?).to be(false)
- end
- end
-
- context "per group" do
- let(:group) { create(:group) }
- let(:milestone) { create(:milestone, group: group) }
-
- it "is always true for groups, for performance reasons" do
- expect(milestone.merge_requests_enabled?).to be(true)
- end
- end
- end
-
- describe "unique milestone title" do
- context "per project" do
- it "does not accept the same title in a project twice" do
- new_milestone = described_class.new(project: milestone.project, title: milestone.title)
- expect(new_milestone).not_to be_valid
- end
-
- it "accepts the same title in another project" do
- project = create(:project)
- new_milestone = described_class.new(project: project, title: milestone.title)
-
- expect(new_milestone).to be_valid
- end
- end
-
- context "per group" do
- let(:group) { create(:group) }
- let(:milestone) { create(:milestone, group: group) }
-
- before do
- project.update(group: group)
- end
-
- it "does not accept the same title in a group twice" do
- new_milestone = described_class.new(group: group, title: milestone.title)
-
- expect(new_milestone).not_to be_valid
- end
-
- it "does not accept the same title of a child project milestone" do
- create(:milestone, project: group.projects.first)
-
- new_milestone = described_class.new(group: group, title: milestone.title)
-
- expect(new_milestone).not_to be_valid
- end
- end
- end
-
describe '.predefined_id?' do
it 'returns true for a predefined Milestone ID' do
expect(Milestone.predefined_id?(described_class::Upcoming.id)).to be true
@@ -619,4 +480,22 @@ describe Milestone do
it { is_expected.not_to match("#{Gitlab.config.gitlab.url}/gitlab-org/gitlab-foss/issues/123") }
it { is_expected.not_to match("gitlab-org/gitlab-ce/milestones/123") }
end
+
+ describe '#parent' do
+ context 'with group' do
+ it 'returns the expected parent' do
+ group = create(:group)
+
+ expect(build(:milestone, group: group).parent).to eq(group)
+ end
+ end
+
+ context 'with project' do
+ it 'returns the expected parent' do
+ project = create(:project)
+
+ expect(build(:milestone, project: project).parent).to eq(project)
+ end
+ end
+ end
end
diff --git a/spec/models/namespace/root_storage_size_spec.rb b/spec/models/namespace/root_storage_size_spec.rb
new file mode 100644
index 00000000000..a8048b7f637
--- /dev/null
+++ b/spec/models/namespace/root_storage_size_spec.rb
@@ -0,0 +1,67 @@
+# frozen_string_literal: true
+
+require 'spec_helper'
+
+RSpec.describe Namespace::RootStorageSize, type: :model do
+ let(:namespace) { create(:namespace) }
+ let(:current_size) { 50.megabytes }
+ let(:limit) { 100 }
+ let(:model) { described_class.new(namespace) }
+ let(:create_statistics) { create(:namespace_root_storage_statistics, namespace: namespace, storage_size: current_size)}
+
+ before do
+ create_statistics
+
+ stub_application_setting(namespace_storage_size_limit: limit)
+ end
+
+ describe '#above_size_limit?' do
+ subject { model.above_size_limit? }
+
+ context 'when limit is 0' do
+ let(:limit) { 0 }
+
+ it { is_expected.to eq(false) }
+ end
+
+ context 'when below limit' do
+ it { is_expected.to eq(false) }
+ end
+
+ context 'when above limit' do
+ let(:current_size) { 101.megabytes }
+
+ it { is_expected.to eq(true) }
+ end
+ end
+
+ describe '#usage_ratio' do
+ subject { model.usage_ratio }
+
+ it { is_expected.to eq(0.5) }
+
+ context 'when limit is 0' do
+ let(:limit) { 0 }
+
+ it { is_expected.to eq(0) }
+ end
+
+ context 'when there are no root_storage_statistics' do
+ let(:create_statistics) { nil }
+
+ it { is_expected.to eq(0) }
+ end
+ end
+
+ describe '#current_size' do
+ subject { model.current_size }
+
+ it { is_expected.to eq(current_size) }
+ end
+
+ describe '#limit' do
+ subject { model.limit }
+
+ it { is_expected.to eq(limit.megabytes) }
+ end
+end
diff --git a/spec/models/note_spec.rb b/spec/models/note_spec.rb
index 74ec74e0def..6dd295ca915 100644
--- a/spec/models/note_spec.rb
+++ b/spec/models/note_spec.rb
@@ -105,6 +105,38 @@ describe Note do
end
end
+ describe 'callbacks' do
+ describe '#notify_after_create' do
+ it 'calls #after_note_created on the noteable' do
+ note = build(:note)
+
+ expect(note).to receive(:notify_after_create).and_call_original
+ expect(note.noteable).to receive(:after_note_created).with(note)
+
+ note.save!
+ end
+ end
+
+ describe '#notify_after_destroy' do
+ it 'calls #after_note_destroyed on the noteable' do
+ note = create(:note)
+
+ expect(note).to receive(:notify_after_destroy).and_call_original
+ expect(note.noteable).to receive(:after_note_destroyed).with(note)
+
+ note.destroy
+ end
+
+ it 'does not error if noteable is nil' do
+ note = create(:note)
+
+ expect(note).to receive(:notify_after_destroy).and_call_original
+ expect(note).to receive(:noteable).at_least(:once).and_return(nil)
+ expect { note.destroy }.not_to raise_error
+ end
+ end
+ end
+
describe "Commit notes" do
before do
allow(Gitlab::Git::KeepAround).to receive(:execute).and_call_original
@@ -751,6 +783,14 @@ describe Note do
end
end
+ describe '#for_design' do
+ it 'is true when the noteable is a design' do
+ note = build(:note, noteable: build(:design))
+
+ expect(note).to be_for_design
+ end
+ end
+
describe '#to_ability_name' do
it 'returns note' do
expect(build(:note).to_ability_name).to eq('note')
diff --git a/spec/models/pages_domain_spec.rb b/spec/models/pages_domain_spec.rb
index fa2648979e9..54747ddf525 100644
--- a/spec/models/pages_domain_spec.rb
+++ b/spec/models/pages_domain_spec.rb
@@ -620,7 +620,11 @@ describe PagesDomain do
create(:pages_domain, :letsencrypt, :with_expired_certificate)
end
- it 'contains only domains needing verification' do
+ let!(:domain_with_failed_auto_ssl) do
+ create(:pages_domain, auto_ssl_enabled: true, auto_ssl_failed: true)
+ end
+
+ it 'contains only domains needing ssl renewal' do
is_expected.to(
contain_exactly(
domain_with_user_provided_certificate_and_auto_ssl,
diff --git a/spec/models/performance_monitoring/prometheus_dashboard_spec.rb b/spec/models/performance_monitoring/prometheus_dashboard_spec.rb
new file mode 100644
index 00000000000..e6fc03a0fb6
--- /dev/null
+++ b/spec/models/performance_monitoring/prometheus_dashboard_spec.rb
@@ -0,0 +1,102 @@
+# frozen_string_literal: true
+
+require 'spec_helper'
+
+describe PerformanceMonitoring::PrometheusDashboard do
+ let(:json_content) do
+ {
+ "dashboard" => "Dashboard Title",
+ "templating" => {
+ "variables" => {
+ "variable1" => %w(value1 value2 value3)
+ }
+ },
+ "panel_groups" => [{
+ "group" => "Group Title",
+ "panels" => [{
+ "type" => "area-chart",
+ "title" => "Chart Title",
+ "y_label" => "Y-Axis",
+ "metrics" => [{
+ "id" => "metric_of_ages",
+ "unit" => "count",
+ "label" => "Metric of Ages",
+ "query_range" => "http_requests_total"
+ }]
+ }]
+ }]
+ }
+ end
+
+ describe '.from_json' do
+ subject { described_class.from_json(json_content) }
+
+ it 'creates a PrometheusDashboard object' do
+ expect(subject).to be_a PerformanceMonitoring::PrometheusDashboard
+ expect(subject.dashboard).to eq(json_content['dashboard'])
+ expect(subject.panel_groups).to all(be_a PerformanceMonitoring::PrometheusPanelGroup)
+ end
+
+ describe 'validations' do
+ context 'when dashboard is missing' do
+ before do
+ json_content['dashboard'] = nil
+ end
+
+ subject { described_class.from_json(json_content) }
+
+ it { expect { subject }.to raise_error(ActiveModel::ValidationError) }
+ end
+
+ context 'when panel groups are missing' do
+ before do
+ json_content['panel_groups'] = []
+ end
+
+ subject { described_class.from_json(json_content) }
+
+ it { expect { subject }.to raise_error(ActiveModel::ValidationError) }
+ end
+ end
+ end
+
+ describe '.find_for' do
+ let(:project) { build_stubbed(:project) }
+ let(:user) { build_stubbed(:user) }
+ let(:environment) { build_stubbed(:environment) }
+ let(:path) { ::Metrics::Dashboard::SystemDashboardService::DASHBOARD_PATH }
+
+ context 'dashboard has been found' do
+ it 'uses dashboard finder to find and load dashboard data and returns dashboard instance', :aggregate_failures do
+ expect(Gitlab::Metrics::Dashboard::Finder).to receive(:find).with(project, user, environment: environment, dashboard_path: path).and_return(status: :success, dashboard: json_content)
+
+ dashboard_instance = described_class.find_for(project: project, user: user, path: path, options: { environment: environment })
+
+ expect(dashboard_instance).to be_instance_of described_class
+ expect(dashboard_instance.environment).to be environment
+ expect(dashboard_instance.path).to be path
+ end
+ end
+
+ context 'dashboard has NOT been found' do
+ it 'returns nil' do
+ allow(Gitlab::Metrics::Dashboard::Finder).to receive(:find).and_return(status: :error)
+
+ dashboard_instance = described_class.find_for(project: project, user: user, path: path, options: { environment: environment })
+
+ expect(dashboard_instance).to be_nil
+ end
+ end
+ end
+
+ describe '#to_yaml' do
+ subject { prometheus_dashboard.to_yaml }
+
+ let(:prometheus_dashboard) { described_class.from_json(json_content) }
+ let(:expected_yaml) do
+ "---\npanel_groups:\n- panels:\n - metrics:\n - id: metric_of_ages\n unit: count\n label: Metric of Ages\n query: \n query_range: http_requests_total\n type: area-chart\n title: Chart Title\n y_label: Y-Axis\n weight: \n group: Group Title\n priority: \ndashboard: Dashboard Title\n"
+ end
+
+ it { is_expected.to eq(expected_yaml) }
+ end
+end
diff --git a/spec/models/performance_monitoring/prometheus_metric_spec.rb b/spec/models/performance_monitoring/prometheus_metric_spec.rb
new file mode 100644
index 00000000000..08288e5d993
--- /dev/null
+++ b/spec/models/performance_monitoring/prometheus_metric_spec.rb
@@ -0,0 +1,59 @@
+# frozen_string_literal: true
+
+require 'spec_helper'
+
+describe PerformanceMonitoring::PrometheusMetric do
+ let(:json_content) do
+ {
+ "id" => "metric_of_ages",
+ "unit" => "count",
+ "label" => "Metric of Ages",
+ "query_range" => "http_requests_total"
+ }
+ end
+
+ describe '.from_json' do
+ subject { described_class.from_json(json_content) }
+
+ it 'creates a PrometheusMetric object' do
+ expect(subject).to be_a PerformanceMonitoring::PrometheusMetric
+ expect(subject.id).to eq(json_content['id'])
+ expect(subject.unit).to eq(json_content['unit'])
+ expect(subject.label).to eq(json_content['label'])
+ expect(subject.query_range).to eq(json_content['query_range'])
+ end
+
+ describe 'validations' do
+ context 'when unit is missing' do
+ before do
+ json_content['unit'] = nil
+ end
+
+ subject { described_class.from_json(json_content) }
+
+ it { expect { subject }.to raise_error(ActiveModel::ValidationError) }
+ end
+
+ context 'when query and query_range is missing' do
+ before do
+ json_content['query_range'] = nil
+ end
+
+ subject { described_class.from_json(json_content) }
+
+ it { expect { subject }.to raise_error(ActiveModel::ValidationError) }
+ end
+
+ context 'when query_range is missing but query is available' do
+ before do
+ json_content['query_range'] = nil
+ json_content['query'] = 'http_requests_total'
+ end
+
+ subject { described_class.from_json(json_content) }
+
+ it { is_expected.to be_valid }
+ end
+ end
+ end
+end
diff --git a/spec/models/performance_monitoring/prometheus_panel_group_spec.rb b/spec/models/performance_monitoring/prometheus_panel_group_spec.rb
new file mode 100644
index 00000000000..2447bb5df94
--- /dev/null
+++ b/spec/models/performance_monitoring/prometheus_panel_group_spec.rb
@@ -0,0 +1,54 @@
+# frozen_string_literal: true
+
+require 'spec_helper'
+
+describe PerformanceMonitoring::PrometheusPanelGroup do
+ let(:json_content) do
+ {
+ "group" => "Group Title",
+ "panels" => [{
+ "type" => "area-chart",
+ "title" => "Chart Title",
+ "y_label" => "Y-Axis",
+ "metrics" => [{
+ "id" => "metric_of_ages",
+ "unit" => "count",
+ "label" => "Metric of Ages",
+ "query_range" => "http_requests_total"
+ }]
+ }]
+ }
+ end
+
+ describe '.from_json' do
+ subject { described_class.from_json(json_content) }
+
+ it 'creates a PrometheusPanelGroup object' do
+ expect(subject).to be_a PerformanceMonitoring::PrometheusPanelGroup
+ expect(subject.group).to eq(json_content['group'])
+ expect(subject.panels).to all(be_a PerformanceMonitoring::PrometheusPanel)
+ end
+
+ describe 'validations' do
+ context 'when group is missing' do
+ before do
+ json_content['group'] = nil
+ end
+
+ subject { described_class.from_json(json_content) }
+
+ it { expect { subject }.to raise_error(ActiveModel::ValidationError) }
+ end
+
+ context 'when panels are missing' do
+ before do
+ json_content['panels'] = []
+ end
+
+ subject { described_class.from_json(json_content) }
+
+ it { expect { subject }.to raise_error(ActiveModel::ValidationError) }
+ end
+ end
+ end
+end
diff --git a/spec/models/performance_monitoring/prometheus_panel_spec.rb b/spec/models/performance_monitoring/prometheus_panel_spec.rb
new file mode 100644
index 00000000000..f5e04ec91e2
--- /dev/null
+++ b/spec/models/performance_monitoring/prometheus_panel_spec.rb
@@ -0,0 +1,77 @@
+# frozen_string_literal: true
+
+require 'spec_helper'
+
+describe PerformanceMonitoring::PrometheusPanel do
+ let(:json_content) do
+ {
+ "max_value" => 1,
+ "type" => "area-chart",
+ "title" => "Chart Title",
+ "y_label" => "Y-Axis",
+ "weight" => 1,
+ "metrics" => [{
+ "id" => "metric_of_ages",
+ "unit" => "count",
+ "label" => "Metric of Ages",
+ "query_range" => "http_requests_total"
+ }]
+ }
+ end
+
+ describe '#new' do
+ it 'accepts old schema format' do
+ expect { described_class.new(json_content) }.not_to raise_error
+ end
+
+ it 'accepts new schema format' do
+ expect { described_class.new(json_content.merge("y_axis" => { "precision" => 0 })) }.not_to raise_error
+ end
+ end
+
+ describe '.from_json' do
+ subject { described_class.from_json(json_content) }
+
+ it 'creates a PrometheusPanelGroup object' do
+ expect(subject).to be_a PerformanceMonitoring::PrometheusPanel
+ expect(subject.type).to eq(json_content['type'])
+ expect(subject.title).to eq(json_content['title'])
+ expect(subject.y_label).to eq(json_content['y_label'])
+ expect(subject.weight).to eq(json_content['weight'])
+ expect(subject.metrics).to all(be_a PerformanceMonitoring::PrometheusMetric)
+ end
+
+ describe 'validations' do
+ context 'when title is missing' do
+ before do
+ json_content['title'] = nil
+ end
+
+ subject { described_class.from_json(json_content) }
+
+ it { expect { subject }.to raise_error(ActiveModel::ValidationError) }
+ end
+
+ context 'when metrics are missing' do
+ before do
+ json_content['metrics'] = []
+ end
+
+ subject { described_class.from_json(json_content) }
+
+ it { expect { subject }.to raise_error(ActiveModel::ValidationError) }
+ end
+ end
+ end
+
+ describe '.id' do
+ it 'returns hexdigest of group_title, type and title as the panel id' do
+ group_title = 'Business Group'
+ panel_type = 'area-chart'
+ panel_title = 'New feature requests made'
+
+ expect(Digest::SHA2).to receive(:hexdigest).with("#{group_title}#{panel_type}#{panel_title}").and_return('hexdigest')
+ expect(described_class.new(title: panel_title, type: panel_type).id(group_title)).to eql 'hexdigest'
+ end
+ end
+end
diff --git a/spec/models/personal_access_token_spec.rb b/spec/models/personal_access_token_spec.rb
index b16d1f58be5..596b11613b3 100644
--- a/spec/models/personal_access_token_spec.rb
+++ b/spec/models/personal_access_token_spec.rb
@@ -179,4 +179,27 @@ describe PersonalAccessToken do
end
end
end
+
+ describe '.simple_sorts' do
+ it 'includes overriden keys' do
+ expect(described_class.simple_sorts.keys).to include(*%w(expires_at_asc expires_at_desc))
+ end
+ end
+
+ describe 'ordering by expires_at' do
+ let_it_be(:earlier_token) { create(:personal_access_token, expires_at: 2.days.ago) }
+ let_it_be(:later_token) { create(:personal_access_token, expires_at: 1.day.ago) }
+
+ describe '.order_expires_at_asc' do
+ it 'returns ordered list in asc order of expiry date' do
+ expect(described_class.order_expires_at_asc).to match [earlier_token, later_token]
+ end
+ end
+
+ describe '.order_expires_at_desc' do
+ it 'returns ordered list in desc order of expiry date' do
+ expect(described_class.order_expires_at_desc).to match [later_token, earlier_token]
+ end
+ end
+ end
end
diff --git a/spec/models/personal_snippet_spec.rb b/spec/models/personal_snippet_spec.rb
index a055f107e33..fb96d6e8bc3 100644
--- a/spec/models/personal_snippet_spec.rb
+++ b/spec/models/personal_snippet_spec.rb
@@ -22,5 +22,6 @@ describe PersonalSnippet do
let(:stubbed_container) { build_stubbed(:personal_snippet) }
let(:expected_full_path) { "@snippets/#{container.id}" }
let(:expected_web_url_path) { "snippets/#{container.id}" }
+ let(:expected_repo_url_path) { expected_web_url_path }
end
end
diff --git a/spec/models/plan_limits_spec.rb b/spec/models/plan_limits_spec.rb
new file mode 100644
index 00000000000..1366f088623
--- /dev/null
+++ b/spec/models/plan_limits_spec.rb
@@ -0,0 +1,74 @@
+# frozen_string_literal: true
+
+require 'spec_helper'
+
+describe PlanLimits do
+ let(:plan_limits) { create(:plan_limits) }
+ let(:model) { ProjectHook }
+ let(:count) { model.count }
+
+ before do
+ create(:project_hook)
+ end
+
+ context 'without plan limits configured' do
+ describe '#exceeded?' do
+ it 'does not exceed any relation offset' do
+ expect(plan_limits.exceeded?(:project_hooks, model)).to be false
+ expect(plan_limits.exceeded?(:project_hooks, count)).to be false
+ end
+ end
+ end
+
+ context 'with plan limits configured' do
+ before do
+ plan_limits.update!(project_hooks: 2)
+ end
+
+ describe '#exceeded?' do
+ it 'does not exceed the relation offset' do
+ expect(plan_limits.exceeded?(:project_hooks, model)).to be false
+ expect(plan_limits.exceeded?(:project_hooks, count)).to be false
+ end
+ end
+
+ context 'with boundary values' do
+ before do
+ create(:project_hook)
+ end
+
+ describe '#exceeded?' do
+ it 'does exceed the relation offset' do
+ expect(plan_limits.exceeded?(:project_hooks, model)).to be true
+ expect(plan_limits.exceeded?(:project_hooks, count)).to be true
+ end
+ end
+ end
+ end
+
+ context 'validates default values' do
+ let(:columns_with_zero) do
+ %w[
+ ci_active_pipelines
+ ci_pipeline_size
+ ci_active_jobs
+ ]
+ end
+
+ it "has positive values for enabled limits" do
+ attributes = plan_limits.attributes
+ attributes = attributes.except(described_class.primary_key)
+ attributes = attributes.except(described_class.reflections.values.map(&:foreign_key))
+ attributes = attributes.except(*columns_with_zero)
+
+ expect(attributes).to all(include(be_positive))
+ end
+
+ it "has zero values for disabled limits" do
+ attributes = plan_limits.attributes
+ attributes = attributes.slice(*columns_with_zero)
+
+ expect(attributes).to all(include(be_zero))
+ end
+ end
+end
diff --git a/spec/models/plan_spec.rb b/spec/models/plan_spec.rb
new file mode 100644
index 00000000000..3f3b8046232
--- /dev/null
+++ b/spec/models/plan_spec.rb
@@ -0,0 +1,17 @@
+# frozen_string_literal: true
+
+require 'spec_helper'
+
+describe Plan do
+ describe '#default?' do
+ subject { plan.default? }
+
+ Plan.default_plans.each do |plan|
+ context "when '#{plan}'" do
+ let(:plan) { build("#{plan}_plan".to_sym) }
+
+ it { is_expected.to be_truthy }
+ end
+ end
+ end
+end
diff --git a/spec/models/project_ci_cd_setting_spec.rb b/spec/models/project_ci_cd_setting_spec.rb
index 312cbbb0948..86115a61aa7 100644
--- a/spec/models/project_ci_cd_setting_spec.rb
+++ b/spec/models/project_ci_cd_setting_spec.rb
@@ -54,17 +54,5 @@ describe ProjectCiCdSetting do
expect(project.reload.ci_cd_settings.default_git_depth).to eq(0)
end
-
- context 'when feature flag :ci_set_project_default_git_depth is disabled' do
- let(:project) { create(:project) }
-
- before do
- stub_feature_flags(ci_set_project_default_git_depth: { enabled: false } )
- end
-
- it 'does not set default value for new records' do
- expect(project.ci_cd_settings.default_git_depth).to eq(nil)
- end
- end
end
end
diff --git a/spec/models/project_feature_spec.rb b/spec/models/project_feature_spec.rb
index 38fba5ea071..e072cc21b38 100644
--- a/spec/models/project_feature_spec.rb
+++ b/spec/models/project_feature_spec.rb
@@ -31,27 +31,30 @@ describe ProjectFeature do
context 'when features are disabled' do
it "returns false" do
+ update_all_project_features(project, features, ProjectFeature::DISABLED)
+
features.each do |feature|
- project.project_feature.update_attribute("#{feature}_access_level".to_sym, ProjectFeature::DISABLED)
- expect(project.feature_available?(:issues, user)).to eq(false)
+ expect(project.feature_available?(feature.to_sym, user)).to eq(false), "#{feature} failed"
end
end
end
context 'when features are enabled only for team members' do
it "returns false when user is not a team member" do
+ update_all_project_features(project, features, ProjectFeature::PRIVATE)
+
features.each do |feature|
- project.project_feature.update_attribute("#{feature}_access_level".to_sym, ProjectFeature::PRIVATE)
- expect(project.feature_available?(:issues, user)).to eq(false)
+ expect(project.feature_available?(feature.to_sym, user)).to eq(false), "#{feature} failed"
end
end
it "returns true when user is a team member" do
project.add_developer(user)
+ update_all_project_features(project, features, ProjectFeature::PRIVATE)
+
features.each do |feature|
- project.project_feature.update_attribute("#{feature}_access_level".to_sym, ProjectFeature::PRIVATE)
- expect(project.feature_available?(:issues, user)).to eq(true)
+ expect(project.feature_available?(feature.to_sym, user)).to eq(true), "#{feature} failed"
end
end
@@ -60,27 +63,41 @@ describe ProjectFeature do
project = create(:project, namespace: group)
group.add_developer(user)
+ update_all_project_features(project, features, ProjectFeature::PRIVATE)
+
features.each do |feature|
- project.project_feature.update_attribute("#{feature}_access_level".to_sym, ProjectFeature::PRIVATE)
- expect(project.feature_available?(:issues, user)).to eq(true)
+ expect(project.feature_available?(feature.to_sym, user)).to eq(true), "#{feature} failed"
end
end
- it "returns true if user is an admin" do
- user.update_attribute(:admin, true)
+ context 'when admin mode is enabled', :enable_admin_mode do
+ it "returns true if user is an admin" do
+ user.update_attribute(:admin, true)
- features.each do |feature|
- project.project_feature.update_attribute("#{feature}_access_level".to_sym, ProjectFeature::PRIVATE)
- expect(project.feature_available?(:issues, user)).to eq(true)
+ update_all_project_features(project, features, ProjectFeature::PRIVATE)
+
+ features.each do |feature|
+ expect(project.feature_available?(feature.to_sym, user)).to eq(true), "#{feature} failed"
+ end
+ end
+ end
+
+ context 'when admin mode is disabled' do
+ it "returns false when user is an admin" do
+ user.update_attribute(:admin, true)
+
+ update_all_project_features(project, features, ProjectFeature::PRIVATE)
+
+ features.each do |feature|
+ expect(project.feature_available?(feature.to_sym, user)).to eq(false), "#{feature} failed"
+ end
end
end
end
context 'when feature is enabled for everyone' do
it "returns true" do
- features.each do |feature|
- expect(project.feature_available?(:issues, user)).to eq(true)
- end
+ expect(project.feature_available?(:issues, user)).to eq(true)
end
end
@@ -117,7 +134,7 @@ describe ProjectFeature do
features.each do |feature|
field = "#{feature}_access_level".to_sym
project_feature.update_attribute(field, ProjectFeature::ENABLED)
- expect(project_feature.valid?).to be_falsy
+ expect(project_feature.valid?).to be_falsy, "#{field} failed"
end
end
end
@@ -131,7 +148,7 @@ describe ProjectFeature do
field = "#{feature}_access_level".to_sym
project_feature.update_attribute(field, ProjectFeature::PUBLIC)
- expect(project_feature.valid?).to be_falsy
+ expect(project_feature.valid?).to be_falsy, "#{field} failed"
end
end
end
@@ -140,22 +157,24 @@ describe ProjectFeature do
let(:features) { %w(wiki builds merge_requests) }
it "returns false when feature is disabled" do
+ update_all_project_features(project, features, ProjectFeature::DISABLED)
+
features.each do |feature|
- project.project_feature.update_attribute("#{feature}_access_level".to_sym, ProjectFeature::DISABLED)
- expect(project.public_send("#{feature}_enabled?")).to eq(false)
+ expect(project.public_send("#{feature}_enabled?")).to eq(false), "#{feature} failed"
end
end
it "returns true when feature is enabled only for team members" do
+ update_all_project_features(project, features, ProjectFeature::PRIVATE)
+
features.each do |feature|
- project.project_feature.update_attribute("#{feature}_access_level".to_sym, ProjectFeature::PRIVATE)
- expect(project.public_send("#{feature}_enabled?")).to eq(true)
+ expect(project.public_send("#{feature}_enabled?")).to eq(true), "#{feature} failed"
end
end
it "returns true when feature is enabled for everyone" do
features.each do |feature|
- expect(project.public_send("#{feature}_enabled?")).to eq(true)
+ expect(project.public_send("#{feature}_enabled?")).to eq(true), "#{feature} failed"
end
end
end
@@ -198,7 +217,7 @@ describe ProjectFeature do
end
describe '#public_pages?' do
- it 'returns true if Pages access controll is not enabled' do
+ it 'returns true if Pages access control is not enabled' do
stub_config(pages: { access_control: false })
project_feature = described_class.new(pages_access_level: described_class::PRIVATE)
@@ -281,7 +300,7 @@ describe ProjectFeature do
it 'raises error if feature is invalid' do
expect do
described_class.required_minimum_access_level(:foos)
- end.to raise_error
+ end.to raise_error(ArgumentError)
end
end
@@ -294,4 +313,9 @@ describe ProjectFeature do
expect(described_class.required_minimum_access_level_for_private_project(:issues)).to eq(Gitlab::Access::GUEST)
end
end
+
+ def update_all_project_features(project, features, value)
+ project_feature_attributes = features.map { |f| ["#{f}_access_level", value] }.to_h
+ project.project_feature.update(project_feature_attributes)
+ end
end
diff --git a/spec/models/project_repository_storage_move_spec.rb b/spec/models/project_repository_storage_move_spec.rb
new file mode 100644
index 00000000000..146fc13bee0
--- /dev/null
+++ b/spec/models/project_repository_storage_move_spec.rb
@@ -0,0 +1,63 @@
+# frozen_string_literal: true
+
+require 'spec_helper'
+
+RSpec.describe ProjectRepositoryStorageMove, type: :model do
+ describe 'associations' do
+ it { is_expected.to belong_to(:project) }
+ end
+
+ describe 'validations' do
+ it { is_expected.to validate_presence_of(:project) }
+ it { is_expected.to validate_presence_of(:state) }
+ it { is_expected.to validate_presence_of(:source_storage_name) }
+ it { is_expected.to validate_presence_of(:destination_storage_name) }
+
+ context 'source_storage_name inclusion' do
+ subject { build(:project_repository_storage_move, source_storage_name: 'missing') }
+
+ it "does not allow repository storages that don't match a label in the configuration" do
+ expect(subject).not_to be_valid
+ expect(subject.errors[:source_storage_name].first).to match(/is not included in the list/)
+ end
+ end
+
+ context 'destination_storage_name inclusion' do
+ subject { build(:project_repository_storage_move, destination_storage_name: 'missing') }
+
+ it "does not allow repository storages that don't match a label in the configuration" do
+ expect(subject).not_to be_valid
+ expect(subject.errors[:destination_storage_name].first).to match(/is not included in the list/)
+ end
+ end
+ end
+
+ describe 'state transitions' do
+ using RSpec::Parameterized::TableSyntax
+
+ context 'when in the default state' do
+ subject(:storage_move) { create(:project_repository_storage_move, project: project, destination_storage_name: 'test_second_storage') }
+
+ let(:project) { create(:project) }
+
+ before do
+ stub_storage_settings('test_second_storage' => { 'path' => 'tmp/tests/extra_storage' })
+ end
+
+ context 'and transits to scheduled' do
+ it 'triggers ProjectUpdateRepositoryStorageWorker' do
+ expect(ProjectUpdateRepositoryStorageWorker).to receive(:perform_async).with(project.id, 'test_second_storage', storage_move.id)
+
+ storage_move.schedule!
+ end
+ end
+
+ context 'and transits to started' do
+ it 'does not allow the transition' do
+ expect { storage_move.start! }
+ .to raise_error(StateMachines::InvalidTransition)
+ end
+ end
+ end
+ end
+end
diff --git a/spec/models/project_services/chat_message/pipeline_message_spec.rb b/spec/models/project_services/chat_message/pipeline_message_spec.rb
index e99148d1d1f..7c3e48f572a 100644
--- a/spec/models/project_services/chat_message/pipeline_message_spec.rb
+++ b/spec/models/project_services/chat_message/pipeline_message_spec.rb
@@ -55,475 +55,324 @@ describe ChatMessage::PipelineMessage do
allow(Gitlab::UrlBuilder).to receive(:build).with(args[:user]).and_return("http://example.gitlab.com/hacker")
end
- context 'when the fancy_pipeline_slack_notifications feature flag is disabled' do
- before do
- stub_feature_flags(fancy_pipeline_slack_notifications: false)
- end
+ it 'returns an empty pretext' do
+ expect(subject.pretext).to be_empty
+ end
+
+ it "returns the pipeline summary in the activity's title" do
+ expect(subject.activity[:title]).to eq(
+ "Pipeline [#123](http://example.gitlab.com/pipelines/123)" \
+ " of branch [develop](http://example.gitlab.com/commits/develop)" \
+ " by The Hacker (hacker) has passed"
+ )
+ end
- it 'returns an empty pretext' do
- expect(subject.pretext).to be_empty
+ context "when the pipeline failed" do
+ before do
+ args[:object_attributes][:status] = 'failed'
end
- it "returns the pipeline summary in the activity's title" do
+ it "returns the summary with a 'failed' status" do
expect(subject.activity[:title]).to eq(
"Pipeline [#123](http://example.gitlab.com/pipelines/123)" \
" of branch [develop](http://example.gitlab.com/commits/develop)" \
- " by The Hacker (hacker) passed"
+ " by The Hacker (hacker) has failed"
)
end
+ end
- context "when the pipeline failed" do
- before do
- args[:object_attributes][:status] = 'failed'
- end
-
- it "returns the summary with a 'failed' status" do
- expect(subject.activity[:title]).to eq(
- "Pipeline [#123](http://example.gitlab.com/pipelines/123)" \
- " of branch [develop](http://example.gitlab.com/commits/develop)" \
- " by The Hacker (hacker) failed"
- )
- end
- end
-
- context 'when no user is provided because the pipeline was triggered by the API' do
- before do
- args[:user] = nil
- end
-
- it "returns the summary with 'API' as the username" do
- expect(subject.activity[:title]).to eq(
- "Pipeline [#123](http://example.gitlab.com/pipelines/123)" \
- " of branch [develop](http://example.gitlab.com/commits/develop)" \
- " by API passed"
- )
- end
- end
-
- it "returns a link to the project in the activity's subtitle" do
- expect(subject.activity[:subtitle]).to eq("in [project_name](http://example.gitlab.com)")
- end
-
- it "returns the build duration in the activity's text property" do
- expect(subject.activity[:text]).to eq("in 02:00:10")
- end
-
- it "returns the user's avatar image URL in the activity's image property" do
- expect(subject.activity[:image]).to eq("http://example.com/avatar")
- end
-
- context 'when the user does not have an avatar' do
- before do
- args[:user][:avatar_url] = nil
- end
-
- it "returns an empty string in the activity's image property" do
- expect(subject.activity[:image]).to be_empty
- end
+ context "when the pipeline passed with warnings" do
+ before do
+ args[:object_attributes][:detailed_status] = 'passed with warnings'
end
- it "returns the pipeline summary as the attachment's text property" do
- expect(subject.attachments.first[:text]).to eq(
- "<http://example.gitlab.com|project_name>:" \
- " Pipeline <http://example.gitlab.com/pipelines/123|#123>" \
- " of branch <http://example.gitlab.com/commits/develop|develop>" \
- " by The Hacker (hacker) passed in 02:00:10"
+ it "returns the summary with a 'passed with warnings' status" do
+ expect(subject.activity[:title]).to eq(
+ "Pipeline [#123](http://example.gitlab.com/pipelines/123)" \
+ " of branch [develop](http://example.gitlab.com/commits/develop)" \
+ " by The Hacker (hacker) has passed with warnings"
)
end
-
- it "returns 'good' as the attachment's color property" do
- expect(subject.attachments.first[:color]).to eq('good')
- end
-
- context "when the pipeline failed" do
- before do
- args[:object_attributes][:status] = 'failed'
- end
-
- it "returns 'danger' as the attachment's color property" do
- expect(subject.attachments.first[:color]).to eq('danger')
- end
- end
-
- context 'when rendering markdown' do
- before do
- args[:markdown] = true
- end
-
- it 'returns the pipeline summary as the attachments in markdown format' do
- expect(subject.attachments).to eq(
- "[project_name](http://example.gitlab.com):" \
- " Pipeline [#123](http://example.gitlab.com/pipelines/123)" \
- " of branch [develop](http://example.gitlab.com/commits/develop)" \
- " by The Hacker (hacker) passed in 02:00:10"
- )
- end
- end
-
- context 'when ref type is tag' do
- before do
- args[:object_attributes][:tag] = true
- args[:object_attributes][:ref] = 'new_tag'
- end
-
- it "returns the pipeline summary in the activity's title" do
- expect(subject.activity[:title]).to eq(
- "Pipeline [#123](http://example.gitlab.com/pipelines/123)" \
- " of tag [new_tag](http://example.gitlab.com/-/tags/new_tag)" \
- " by The Hacker (hacker) passed"
- )
- end
-
- it "returns the pipeline summary as the attachment's text property" do
- expect(subject.attachments.first[:text]).to eq(
- "<http://example.gitlab.com|project_name>:" \
- " Pipeline <http://example.gitlab.com/pipelines/123|#123>" \
- " of tag <http://example.gitlab.com/-/tags/new_tag|new_tag>" \
- " by The Hacker (hacker) passed in 02:00:10"
- )
- end
-
- context 'when rendering markdown' do
- before do
- args[:markdown] = true
- end
-
- it 'returns the pipeline summary as the attachments in markdown format' do
- expect(subject.attachments).to eq(
- "[project_name](http://example.gitlab.com):" \
- " Pipeline [#123](http://example.gitlab.com/pipelines/123)" \
- " of tag [new_tag](http://example.gitlab.com/-/tags/new_tag)" \
- " by The Hacker (hacker) passed in 02:00:10"
- )
- end
- end
- end
end
- context 'when the fancy_pipeline_slack_notifications feature flag is enabled' do
+ context 'when no user is provided because the pipeline was triggered by the API' do
before do
- stub_feature_flags(fancy_pipeline_slack_notifications: true)
- end
-
- it 'returns an empty pretext' do
- expect(subject.pretext).to be_empty
+ args[:user] = nil
end
- it "returns the pipeline summary in the activity's title" do
+ it "returns the summary with 'API' as the username" do
expect(subject.activity[:title]).to eq(
"Pipeline [#123](http://example.gitlab.com/pipelines/123)" \
" of branch [develop](http://example.gitlab.com/commits/develop)" \
- " by The Hacker (hacker) has passed"
+ " by API has passed"
)
end
+ end
- context "when the pipeline failed" do
- before do
- args[:object_attributes][:status] = 'failed'
- end
+ it "returns a link to the project in the activity's subtitle" do
+ expect(subject.activity[:subtitle]).to eq("in [project_name](http://example.gitlab.com)")
+ end
- it "returns the summary with a 'failed' status" do
- expect(subject.activity[:title]).to eq(
- "Pipeline [#123](http://example.gitlab.com/pipelines/123)" \
- " of branch [develop](http://example.gitlab.com/commits/develop)" \
- " by The Hacker (hacker) has failed"
- )
- end
- end
+ it "returns the build duration in the activity's text property" do
+ expect(subject.activity[:text]).to eq("in 02:00:10")
+ end
- context "when the pipeline passed with warnings" do
- before do
- args[:object_attributes][:detailed_status] = 'passed with warnings'
- end
+ it "returns the user's avatar image URL in the activity's image property" do
+ expect(subject.activity[:image]).to eq("http://example.com/avatar")
+ end
- it "returns the summary with a 'passed with warnings' status" do
- expect(subject.activity[:title]).to eq(
- "Pipeline [#123](http://example.gitlab.com/pipelines/123)" \
- " of branch [develop](http://example.gitlab.com/commits/develop)" \
- " by The Hacker (hacker) has passed with warnings"
- )
- end
+ context 'when the user does not have an avatar' do
+ before do
+ args[:user][:avatar_url] = nil
end
- context 'when no user is provided because the pipeline was triggered by the API' do
- before do
- args[:user] = nil
- end
-
- it "returns the summary with 'API' as the username" do
- expect(subject.activity[:title]).to eq(
- "Pipeline [#123](http://example.gitlab.com/pipelines/123)" \
- " of branch [develop](http://example.gitlab.com/commits/develop)" \
- " by API has passed"
- )
- end
+ it "returns an empty string in the activity's image property" do
+ expect(subject.activity[:image]).to be_empty
end
+ end
- it "returns a link to the project in the activity's subtitle" do
- expect(subject.activity[:subtitle]).to eq("in [project_name](http://example.gitlab.com)")
- end
+ it "returns the pipeline summary as the attachment's fallback property" do
+ expect(subject.attachments.first[:fallback]).to eq(
+ "<http://example.gitlab.com|project_name>:" \
+ " Pipeline <http://example.gitlab.com/pipelines/123|#123>" \
+ " of branch <http://example.gitlab.com/commits/develop|develop>" \
+ " by The Hacker (hacker) has passed in 02:00:10"
+ )
+ end
- it "returns the build duration in the activity's text property" do
- expect(subject.activity[:text]).to eq("in 02:00:10")
- end
+ it "returns 'good' as the attachment's color property" do
+ expect(subject.attachments.first[:color]).to eq('good')
+ end
- it "returns the user's avatar image URL in the activity's image property" do
- expect(subject.activity[:image]).to eq("http://example.com/avatar")
+ context "when the pipeline failed" do
+ before do
+ args[:object_attributes][:status] = 'failed'
end
- context 'when the user does not have an avatar' do
- before do
- args[:user][:avatar_url] = nil
- end
-
- it "returns an empty string in the activity's image property" do
- expect(subject.activity[:image]).to be_empty
- end
+ it "returns 'danger' as the attachment's color property" do
+ expect(subject.attachments.first[:color]).to eq('danger')
end
+ end
- it "returns the pipeline summary as the attachment's fallback property" do
- expect(subject.attachments.first[:fallback]).to eq(
- "<http://example.gitlab.com|project_name>:" \
- " Pipeline <http://example.gitlab.com/pipelines/123|#123>" \
- " of branch <http://example.gitlab.com/commits/develop|develop>" \
- " by The Hacker (hacker) has passed in 02:00:10"
- )
+ context "when the pipeline passed with warnings" do
+ before do
+ args[:object_attributes][:detailed_status] = 'passed with warnings'
end
- it "returns 'good' as the attachment's color property" do
- expect(subject.attachments.first[:color]).to eq('good')
+ it "returns 'warning' as the attachment's color property" do
+ expect(subject.attachments.first[:color]).to eq('warning')
end
+ end
- context "when the pipeline failed" do
- before do
- args[:object_attributes][:status] = 'failed'
- end
+ it "returns the committer's name and username as the attachment's author_name property" do
+ expect(subject.attachments.first[:author_name]).to eq('The Hacker (hacker)')
+ end
- it "returns 'danger' as the attachment's color property" do
- expect(subject.attachments.first[:color]).to eq('danger')
- end
- end
+ it "returns the committer's avatar URL as the attachment's author_icon property" do
+ expect(subject.attachments.first[:author_icon]).to eq('http://example.com/avatar')
+ end
- context "when the pipeline passed with warnings" do
- before do
- args[:object_attributes][:detailed_status] = 'passed with warnings'
- end
+ it "returns the committer's GitLab profile URL as the attachment's author_link property" do
+ expect(subject.attachments.first[:author_link]).to eq('http://example.gitlab.com/hacker')
+ end
- it "returns 'warning' as the attachment's color property" do
- expect(subject.attachments.first[:color]).to eq('warning')
- end
+ context 'when no user is provided because the pipeline was triggered by the API' do
+ before do
+ args[:user] = nil
end
it "returns the committer's name and username as the attachment's author_name property" do
- expect(subject.attachments.first[:author_name]).to eq('The Hacker (hacker)')
+ expect(subject.attachments.first[:author_name]).to eq('API')
end
- it "returns the committer's avatar URL as the attachment's author_icon property" do
- expect(subject.attachments.first[:author_icon]).to eq('http://example.com/avatar')
+ it "returns nil as the attachment's author_icon property" do
+ expect(subject.attachments.first[:author_icon]).to be_nil
end
- it "returns the committer's GitLab profile URL as the attachment's author_link property" do
- expect(subject.attachments.first[:author_link]).to eq('http://example.gitlab.com/hacker')
+ it "returns nil as the attachment's author_link property" do
+ expect(subject.attachments.first[:author_link]).to be_nil
end
+ end
- context 'when no user is provided because the pipeline was triggered by the API' do
- before do
- args[:user] = nil
- end
+ it "returns the pipeline ID, status, and duration as the attachment's title property" do
+ expect(subject.attachments.first[:title]).to eq("Pipeline #123 has passed in 02:00:10")
+ end
- it "returns the committer's name and username as the attachment's author_name property" do
- expect(subject.attachments.first[:author_name]).to eq('API')
- end
+ it "returns the pipeline URL as the attachment's title_link property" do
+ expect(subject.attachments.first[:title_link]).to eq("http://example.gitlab.com/pipelines/123")
+ end
- it "returns nil as the attachment's author_icon property" do
- expect(subject.attachments.first[:author_icon]).to be_nil
- end
+ it "returns two attachment fields" do
+ expect(subject.attachments.first[:fields].count).to eq(2)
+ end
- it "returns nil as the attachment's author_link property" do
- expect(subject.attachments.first[:author_link]).to be_nil
- end
- end
+ it "returns the commit message as the attachment's second field property" do
+ expect(subject.attachments.first[:fields][0]).to eq({
+ title: "Branch",
+ value: "<http://example.gitlab.com/commits/develop|develop>",
+ short: true
+ })
+ end
- it "returns the pipeline ID, status, and duration as the attachment's title property" do
- expect(subject.attachments.first[:title]).to eq("Pipeline #123 has passed in 02:00:10")
- end
+ it "returns the ref name and link as the attachment's second field property" do
+ expect(subject.attachments.first[:fields][1]).to eq({
+ title: "Commit",
+ value: "<http://example.com/commit|A test commit message>",
+ short: true
+ })
+ end
- it "returns the pipeline URL as the attachment's title_link property" do
- expect(subject.attachments.first[:title_link]).to eq("http://example.gitlab.com/pipelines/123")
+ context "when a job in the pipeline fails" do
+ before do
+ args[:builds] = [
+ { id: 1, name: "rspec", status: "failed", stage: "test" },
+ { id: 2, name: "karma", status: "success", stage: "test" }
+ ]
end
- it "returns two attachment fields" do
- expect(subject.attachments.first[:fields].count).to eq(2)
+ it "returns four attachment fields" do
+ expect(subject.attachments.first[:fields].count).to eq(4)
end
- it "returns the commit message as the attachment's second field property" do
- expect(subject.attachments.first[:fields][0]).to eq({
- title: "Branch",
- value: "<http://example.gitlab.com/commits/develop|develop>",
+ it "returns the stage name and link to the 'Failed jobs' tab on the pipeline's page as the attachment's third field property" do
+ expect(subject.attachments.first[:fields][2]).to eq({
+ title: "Failed stage",
+ value: "<http://example.gitlab.com/pipelines/123/failures|test>",
short: true
})
end
- it "returns the ref name and link as the attachment's second field property" do
- expect(subject.attachments.first[:fields][1]).to eq({
- title: "Commit",
- value: "<http://example.com/commit|A test commit message>",
+ it "returns the job name and link as the attachment's fourth field property" do
+ expect(subject.attachments.first[:fields][3]).to eq({
+ title: "Failed job",
+ value: "<http://example.gitlab.com/-/jobs/1|rspec>",
short: true
})
end
+ end
- context "when a job in the pipeline fails" do
- before do
- args[:builds] = [
- { id: 1, name: "rspec", status: "failed", stage: "test" },
- { id: 2, name: "karma", status: "success", stage: "test" }
- ]
- end
-
- it "returns four attachment fields" do
- expect(subject.attachments.first[:fields].count).to eq(4)
- end
-
- it "returns the stage name and link to the 'Failed jobs' tab on the pipeline's page as the attachment's third field property" do
- expect(subject.attachments.first[:fields][2]).to eq({
- title: "Failed stage",
- value: "<http://example.gitlab.com/pipelines/123/failures|test>",
- short: true
- })
- end
-
- it "returns the job name and link as the attachment's fourth field property" do
- expect(subject.attachments.first[:fields][3]).to eq({
- title: "Failed job",
- value: "<http://example.gitlab.com/-/jobs/1|rspec>",
- short: true
- })
+ context "when lots of jobs across multiple stages fail" do
+ before do
+ args[:builds] = (1..25).map do |i|
+ { id: i, name: "job-#{i}", status: "failed", stage: "stage-" + ((i % 3) + 1).to_s }
end
end
- context "when lots of jobs across multiple stages fail" do
- before do
- args[:builds] = (1..25).map do |i|
- { id: i, name: "job-#{i}", status: "failed", stage: "stage-" + ((i % 3) + 1).to_s }
- end
- end
+ it "returns the stage names and links to the 'Failed jobs' tab on the pipeline's page as the attachment's third field property" do
+ expect(subject.attachments.first[:fields][2]).to eq({
+ title: "Failed stages",
+ value: "<http://example.gitlab.com/pipelines/123/failures|stage-2>, <http://example.gitlab.com/pipelines/123/failures|stage-1>, <http://example.gitlab.com/pipelines/123/failures|stage-3>",
+ short: true
+ })
+ end
- it "returns the stage names and links to the 'Failed jobs' tab on the pipeline's page as the attachment's third field property" do
- expect(subject.attachments.first[:fields][2]).to eq({
- title: "Failed stages",
- value: "<http://example.gitlab.com/pipelines/123/failures|stage-2>, <http://example.gitlab.com/pipelines/123/failures|stage-1>, <http://example.gitlab.com/pipelines/123/failures|stage-3>",
- short: true
- })
+ it "returns the job names and links as the attachment's fourth field property" do
+ expected_jobs = 25.downto(16).map do |i|
+ "<http://example.gitlab.com/-/jobs/#{i}|job-#{i}>"
end
- it "returns the job names and links as the attachment's fourth field property" do
- expected_jobs = 25.downto(16).map do |i|
- "<http://example.gitlab.com/-/jobs/#{i}|job-#{i}>"
- end
+ expected_jobs << "and <http://example.gitlab.com/pipelines/123/failures|15 more>"
- expected_jobs << "and <http://example.gitlab.com/pipelines/123/failures|15 more>"
-
- expect(subject.attachments.first[:fields][3]).to eq({
- title: "Failed jobs",
- value: expected_jobs.join(", "),
- short: true
- })
- end
+ expect(subject.attachments.first[:fields][3]).to eq({
+ title: "Failed jobs",
+ value: expected_jobs.join(", "),
+ short: true
+ })
end
+ end
- context "when jobs succeed on retries" do
- before do
- args[:builds] = [
- { id: 1, name: "job-1", status: "failed", stage: "stage-1" },
- { id: 2, name: "job-2", status: "failed", stage: "stage-2" },
- { id: 3, name: "job-3", status: "failed", stage: "stage-3" },
- { id: 7, name: "job-1", status: "failed", stage: "stage-1" },
- { id: 8, name: "job-1", status: "success", stage: "stage-1" }
- ]
- end
-
- it "do not return a job which succeeded on retry" do
- expected_jobs = [
- "<http://example.gitlab.com/-/jobs/3|job-3>",
- "<http://example.gitlab.com/-/jobs/2|job-2>"
- ]
-
- expect(subject.attachments.first[:fields][3]).to eq(
- title: "Failed jobs",
- value: expected_jobs.join(", "),
- short: true
- )
- end
+ context "when jobs succeed on retries" do
+ before do
+ args[:builds] = [
+ { id: 1, name: "job-1", status: "failed", stage: "stage-1" },
+ { id: 2, name: "job-2", status: "failed", stage: "stage-2" },
+ { id: 3, name: "job-3", status: "failed", stage: "stage-3" },
+ { id: 7, name: "job-1", status: "failed", stage: "stage-1" },
+ { id: 8, name: "job-1", status: "success", stage: "stage-1" }
+ ]
+ end
+
+ it "do not return a job which succeeded on retry" do
+ expected_jobs = [
+ "<http://example.gitlab.com/-/jobs/3|job-3>",
+ "<http://example.gitlab.com/-/jobs/2|job-2>"
+ ]
+
+ expect(subject.attachments.first[:fields][3]).to eq(
+ title: "Failed jobs",
+ value: expected_jobs.join(", "),
+ short: true
+ )
end
+ end
- context "when jobs failed even on retries" do
- before do
- args[:builds] = [
- { id: 1, name: "job-1", status: "failed", stage: "stage-1" },
- { id: 2, name: "job-2", status: "failed", stage: "stage-2" },
- { id: 3, name: "job-3", status: "failed", stage: "stage-3" },
- { id: 7, name: "job-1", status: "failed", stage: "stage-1" },
- { id: 8, name: "job-1", status: "failed", stage: "stage-1" }
- ]
- end
-
- it "returns only first instance of the failed job" do
- expected_jobs = [
- "<http://example.gitlab.com/-/jobs/3|job-3>",
- "<http://example.gitlab.com/-/jobs/2|job-2>",
- "<http://example.gitlab.com/-/jobs/1|job-1>"
- ]
-
- expect(subject.attachments.first[:fields][3]).to eq(
- title: "Failed jobs",
- value: expected_jobs.join(", "),
- short: true
- )
- end
+ context "when jobs failed even on retries" do
+ before do
+ args[:builds] = [
+ { id: 1, name: "job-1", status: "failed", stage: "stage-1" },
+ { id: 2, name: "job-2", status: "failed", stage: "stage-2" },
+ { id: 3, name: "job-3", status: "failed", stage: "stage-3" },
+ { id: 7, name: "job-1", status: "failed", stage: "stage-1" },
+ { id: 8, name: "job-1", status: "failed", stage: "stage-1" }
+ ]
+ end
+
+ it "returns only first instance of the failed job" do
+ expected_jobs = [
+ "<http://example.gitlab.com/-/jobs/3|job-3>",
+ "<http://example.gitlab.com/-/jobs/2|job-2>",
+ "<http://example.gitlab.com/-/jobs/1|job-1>"
+ ]
+
+ expect(subject.attachments.first[:fields][3]).to eq(
+ title: "Failed jobs",
+ value: expected_jobs.join(", "),
+ short: true
+ )
end
+ end
- context "when the CI config file contains a YAML error" do
- let(:has_yaml_errors) { true }
-
- it "returns three attachment fields" do
- expect(subject.attachments.first[:fields].count).to eq(3)
- end
+ context "when the CI config file contains a YAML error" do
+ let(:has_yaml_errors) { true }
- it "returns the YAML error deatils as the attachment's third field property" do
- expect(subject.attachments.first[:fields][2]).to eq({
- title: "Invalid CI config YAML file",
- value: "yaml error description here",
- short: false
- })
- end
+ it "returns three attachment fields" do
+ expect(subject.attachments.first[:fields].count).to eq(3)
end
- it "returns the project's name as the attachment's footer property" do
- expect(subject.attachments.first[:footer]).to eq("project_name")
+ it "returns the YAML error deatils as the attachment's third field property" do
+ expect(subject.attachments.first[:fields][2]).to eq({
+ title: "Invalid CI config YAML file",
+ value: "yaml error description here",
+ short: false
+ })
end
+ end
- it "returns the project's avatar URL as the attachment's footer_icon property" do
- expect(subject.attachments.first[:footer_icon]).to eq("http://example.com/project_avatar")
- end
+ it "returns the project's name as the attachment's footer property" do
+ expect(subject.attachments.first[:footer]).to eq("project_name")
+ end
- it "returns the pipeline's timestamp as the attachment's ts property" do
- expected_ts = Time.parse(args[:object_attributes][:finished_at]).to_i
- expect(subject.attachments.first[:ts]).to eq(expected_ts)
- end
+ it "returns the project's avatar URL as the attachment's footer_icon property" do
+ expect(subject.attachments.first[:footer_icon]).to eq("http://example.com/project_avatar")
+ end
- context 'when rendering markdown' do
- before do
- args[:markdown] = true
- end
+ it "returns the pipeline's timestamp as the attachment's ts property" do
+ expected_ts = Time.parse(args[:object_attributes][:finished_at]).to_i
+ expect(subject.attachments.first[:ts]).to eq(expected_ts)
+ end
- it 'returns the pipeline summary as the attachments in markdown format' do
- expect(subject.attachments).to eq(
- "[project_name](http://example.gitlab.com):" \
- " Pipeline [#123](http://example.gitlab.com/pipelines/123)" \
- " of branch [develop](http://example.gitlab.com/commits/develop)" \
- " by The Hacker (hacker) has passed in 02:00:10"
- )
- end
+ context 'when rendering markdown' do
+ before do
+ args[:markdown] = true
+ end
+
+ it 'returns the pipeline summary as the attachments in markdown format' do
+ expect(subject.attachments).to eq(
+ "[project_name](http://example.gitlab.com):" \
+ " Pipeline [#123](http://example.gitlab.com/pipelines/123)" \
+ " of branch [develop](http://example.gitlab.com/commits/develop)" \
+ " by The Hacker (hacker) has passed in 02:00:10"
+ )
end
end
end
diff --git a/spec/models/project_services/irker_service_spec.rb b/spec/models/project_services/irker_service_spec.rb
index badc964db16..88a93eef214 100644
--- a/spec/models/project_services/irker_service_spec.rb
+++ b/spec/models/project_services/irker_service_spec.rb
@@ -65,7 +65,7 @@ describe IrkerService do
conn = @irker_server.accept
conn.each_line do |line|
- msg = JSON.parse(line.chomp("\n"))
+ msg = Gitlab::Json.parse(line.chomp("\n"))
expect(msg.keys).to match_array(%w(to privmsg))
expect(msg['to']).to match_array(["irc://chat.freenode.net/#commits",
"irc://test.net/#test"])
diff --git a/spec/models/project_services/jira_service_spec.rb b/spec/models/project_services/jira_service_spec.rb
index 32e6b5afce5..a0d36f0a238 100644
--- a/spec/models/project_services/jira_service_spec.rb
+++ b/spec/models/project_services/jira_service_spec.rb
@@ -69,11 +69,23 @@ describe JiraService do
end
describe '.reference_pattern' do
- it_behaves_like 'allows project key on reference pattern'
+ using RSpec::Parameterized::TableSyntax
- it 'does not allow # on the code' do
- expect(described_class.reference_pattern.match('#123')).to be_nil
- expect(described_class.reference_pattern.match('1#23#12')).to be_nil
+ where(:key, :result) do
+ '#123' | ''
+ '1#23#12' | ''
+ 'JIRA-1234A' | 'JIRA-1234'
+ 'JIRA-1234-some_tag' | 'JIRA-1234'
+ 'JIRA-1234_some_tag' | 'JIRA-1234'
+ 'EXT_EXT-1234' | 'EXT_EXT-1234'
+ 'EXT3_EXT-1234' | 'EXT3_EXT-1234'
+ '3EXT_EXT-1234' | ''
+ end
+
+ with_them do
+ specify do
+ expect(described_class.reference_pattern.match(key).to_s).to eq(result)
+ end
end
end
@@ -570,6 +582,79 @@ describe JiraService do
end
end
+ describe '#create_cross_reference_note' do
+ let_it_be(:user) { build_stubbed(:user) }
+ let_it_be(:project) { create(:project, :repository) }
+ let(:jira_service) do
+ described_class.new(
+ project: project,
+ url: url,
+ username: username,
+ password: password
+ )
+ end
+ let(:jira_issue) { ExternalIssue.new('JIRA-123', project) }
+
+ subject { jira_service.create_cross_reference_note(jira_issue, resource, user) }
+
+ shared_examples 'creates a comment on Jira' do
+ let(:issue_url) { "#{url}/rest/api/2/issue/JIRA-123" }
+ let(:comment_url) { "#{issue_url}/comment" }
+ let(:remote_link_url) { "#{issue_url}/remotelink" }
+
+ before do
+ allow(JIRA::Resource::Remotelink).to receive(:all).and_return([])
+ stub_request(:get, issue_url).with(basic_auth: [username, password])
+ stub_request(:post, comment_url).with(basic_auth: [username, password])
+ stub_request(:post, remote_link_url).with(basic_auth: [username, password])
+ end
+
+ it 'creates a comment on Jira' do
+ subject
+
+ expect(WebMock).to have_requested(:post, comment_url).with(
+ body: /mentioned this issue in/
+ ).once
+ end
+ end
+
+ context 'when resource is a commit' do
+ let(:resource) { project.commit('master') }
+
+ context 'when disabled' do
+ before do
+ allow_next_instance_of(JiraService) do |instance|
+ allow(instance).to receive(:commit_events) { false }
+ end
+ end
+
+ it { is_expected.to eq('Events for commits are disabled.') }
+ end
+
+ context 'when enabled' do
+ it_behaves_like 'creates a comment on Jira'
+ end
+ end
+
+ context 'when resource is a merge request' do
+ let(:resource) { build_stubbed(:merge_request, source_project: project) }
+
+ context 'when disabled' do
+ before do
+ allow_next_instance_of(JiraService) do |instance|
+ allow(instance).to receive(:merge_requests_events) { false }
+ end
+ end
+
+ it { is_expected.to eq('Events for merge requests are disabled.') }
+ end
+
+ context 'when enabled' do
+ it_behaves_like 'creates a comment on Jira'
+ end
+ end
+ end
+
describe '#test' do
let(:jira_service) do
described_class.new(
diff --git a/spec/models/project_services/mattermost_slash_commands_service_spec.rb b/spec/models/project_services/mattermost_slash_commands_service_spec.rb
index 87e482059f2..836181929e3 100644
--- a/spec/models/project_services/mattermost_slash_commands_service_spec.rb
+++ b/spec/models/project_services/mattermost_slash_commands_service_spec.rb
@@ -121,5 +121,12 @@ describe MattermostSlashCommandsService do
end
end
end
+
+ describe '#chat_responder' do
+ it 'returns the responder to use for Mattermost' do
+ expect(described_class.new.chat_responder)
+ .to eq(Gitlab::Chat::Responder::Mattermost)
+ end
+ end
end
end
diff --git a/spec/models/project_services/microsoft_teams_service_spec.rb b/spec/models/project_services/microsoft_teams_service_spec.rb
index d93b8a2cb40..425599c73d4 100644
--- a/spec/models/project_services/microsoft_teams_service_spec.rb
+++ b/spec/models/project_services/microsoft_teams_service_spec.rb
@@ -121,7 +121,7 @@ describe MicrosoftTeamsService do
message: "user created page: Awesome wiki_page"
}
end
- let(:wiki_page) { create(:wiki_page, wiki: project.wiki, attrs: opts) }
+ let(:wiki_page) { create(:wiki_page, wiki: project.wiki, **opts) }
let(:wiki_page_sample_data) { Gitlab::DataBuilder::WikiPage.build(wiki_page, user, 'create') }
it "calls Microsoft Teams API" do
diff --git a/spec/models/project_services/webex_teams_service_spec.rb b/spec/models/project_services/webex_teams_service_spec.rb
new file mode 100644
index 00000000000..38977ef3b7d
--- /dev/null
+++ b/spec/models/project_services/webex_teams_service_spec.rb
@@ -0,0 +1,10 @@
+# frozen_string_literal: true
+
+require "spec_helper"
+
+describe WebexTeamsService do
+ it_behaves_like "chat service", "Webex Teams" do
+ let(:client_arguments) { webhook_url }
+ let(:content_key) { :markdown }
+ end
+end
diff --git a/spec/models/project_snippet_spec.rb b/spec/models/project_snippet_spec.rb
index 719a74f995d..c17a24dc7cf 100644
--- a/spec/models/project_snippet_spec.rb
+++ b/spec/models/project_snippet_spec.rb
@@ -38,5 +38,6 @@ describe ProjectSnippet do
let(:stubbed_container) { build_stubbed(:project_snippet) }
let(:expected_full_path) { "#{container.project.full_path}/@snippets/#{container.id}" }
let(:expected_web_url_path) { "#{container.project.full_path}/snippets/#{container.id}" }
+ let(:expected_repo_url_path) { expected_web_url_path }
end
end
diff --git a/spec/models/project_spec.rb b/spec/models/project_spec.rb
index 4e75ef4fc87..5f8b51c250d 100644
--- a/spec/models/project_spec.rb
+++ b/spec/models/project_spec.rb
@@ -6,6 +6,7 @@ describe Project do
include ProjectForksHelper
include GitHelpers
include ExternalAuthorizationServiceHelpers
+ using RSpec::Parameterized::TableSyntax
it_behaves_like 'having unique enum values'
@@ -20,6 +21,7 @@ describe Project do
it { is_expected.to have_many(:merge_requests) }
it { is_expected.to have_many(:issues) }
it { is_expected.to have_many(:milestones) }
+ it { is_expected.to have_many(:iterations) }
it { is_expected.to have_many(:project_members).dependent(:delete_all) }
it { is_expected.to have_many(:users).through(:project_members) }
it { is_expected.to have_many(:requesters).dependent(:delete_all) }
@@ -34,6 +36,7 @@ describe Project do
it { is_expected.to have_one(:mattermost_service) }
it { is_expected.to have_one(:hangouts_chat_service) }
it { is_expected.to have_one(:unify_circuit_service) }
+ it { is_expected.to have_one(:webex_teams_service) }
it { is_expected.to have_one(:packagist_service) }
it { is_expected.to have_one(:pushover_service) }
it { is_expected.to have_one(:asana_service) }
@@ -110,7 +113,10 @@ describe Project do
it { is_expected.to have_many(:source_pipelines) }
it { is_expected.to have_many(:prometheus_alert_events) }
it { is_expected.to have_many(:self_managed_prometheus_alert_events) }
+ it { is_expected.to have_many(:alert_management_alerts) }
it { is_expected.to have_many(:jira_imports) }
+ it { is_expected.to have_many(:metrics_users_starred_dashboards).inverse_of(:project) }
+ it { is_expected.to have_many(:repository_storage_moves) }
it_behaves_like 'model with repository' do
let_it_be(:container) { create(:project, :repository, path: 'somewhere') }
@@ -118,6 +124,11 @@ describe Project do
let(:expected_full_path) { "#{container.namespace.full_path}/somewhere" }
end
+ it_behaves_like 'model with wiki' do
+ let(:container) { create(:project, :wiki_repo) }
+ let(:container_without_wiki) { create(:project) }
+ end
+
it 'has an inverse relationship with merge requests' do
expect(described_class.reflect_on_association(:merge_requests).has_inverse?).to eq(:target_project)
end
@@ -263,27 +274,6 @@ describe Project do
create(:project)
end
- describe 'wiki path conflict' do
- context "when the new path has been used by the wiki of other Project" do
- it 'has an error on the name attribute' do
- new_project = build_stubbed(:project, namespace_id: project.namespace_id, path: "#{project.path}.wiki")
-
- expect(new_project).not_to be_valid
- expect(new_project.errors[:name].first).to eq(_('has already been taken'))
- end
- end
-
- context "when the new wiki path has been used by the path of other Project" do
- it 'has an error on the name attribute' do
- project_with_wiki_suffix = create(:project, path: 'foo.wiki')
- new_project = build_stubbed(:project, namespace_id: project_with_wiki_suffix.namespace_id, path: 'foo')
-
- expect(new_project).not_to be_valid
- expect(new_project.errors[:name].first).to eq(_('has already been taken'))
- end
- end
- end
-
context 'repository storages inclusion' do
let(:project2) { build(:project, repository_storage: 'missing') }
@@ -1791,6 +1781,7 @@ describe Project do
let(:project) { create(:project, :repository) }
let(:repo) { double(:repo, exists?: true) }
let(:wiki) { double(:wiki, exists?: true) }
+ let(:design) { double(:design, exists?: true) }
it 'expires the caches of the repository and wiki' do
# In EE, there are design repositories as well
@@ -1804,8 +1795,13 @@ describe Project do
.with('foo.wiki', project, shard: project.repository_storage, repo_type: Gitlab::GlRepository::WIKI)
.and_return(wiki)
+ allow(Repository).to receive(:new)
+ .with('foo.design', project, shard: project.repository_storage, repo_type: Gitlab::GlRepository::DESIGN)
+ .and_return(design)
+
expect(repo).to receive(:before_delete)
expect(wiki).to receive(:before_delete)
+ expect(design).to receive(:before_delete)
project.expire_caches_before_rename('foo')
end
@@ -2849,12 +2845,16 @@ describe Project do
end
it 'schedules the transfer of the repository to the new storage and locks the project' do
- expect(ProjectUpdateRepositoryStorageWorker).to receive(:perform_async).with(project.id, 'test_second_storage')
+ expect(ProjectUpdateRepositoryStorageWorker).to receive(:perform_async).with(project.id, 'test_second_storage', anything)
project.change_repository_storage('test_second_storage')
project.save!
expect(project).to be_repository_read_only
+ expect(project.repository_storage_moves.last).to have_attributes(
+ source_storage_name: "default",
+ destination_storage_name: "test_second_storage"
+ )
end
it "doesn't schedule the transfer if the repository is already read-only" do
@@ -3139,6 +3139,45 @@ describe Project do
end
end
+ describe '#ci_instance_variables_for' do
+ let(:project) { create(:project) }
+
+ let!(:instance_variable) do
+ create(:ci_instance_variable, value: 'secret')
+ end
+
+ let!(:protected_instance_variable) do
+ create(:ci_instance_variable, :protected, value: 'protected')
+ end
+
+ subject { project.ci_instance_variables_for(ref: 'ref') }
+
+ before do
+ stub_application_setting(
+ default_branch_protection: Gitlab::Access::PROTECTION_NONE)
+ end
+
+ context 'when the ref is not protected' do
+ before do
+ allow(project).to receive(:protected_for?).with('ref').and_return(false)
+ end
+
+ it 'contains only the CI variables' do
+ is_expected.to contain_exactly(instance_variable)
+ end
+ end
+
+ context 'when the ref is protected' do
+ before do
+ allow(project).to receive(:protected_for?).with('ref').and_return(true)
+ end
+
+ it 'contains all the variables' do
+ is_expected.to contain_exactly(instance_variable, protected_instance_variable)
+ end
+ end
+ end
+
describe '#any_lfs_file_locks?', :request_store do
let_it_be(:project) { create(:project) }
@@ -3637,6 +3676,24 @@ describe Project do
expect(projects).to contain_exactly(public_project)
end
end
+
+ context 'with deploy token users' do
+ let_it_be(:private_project) { create(:project, :private) }
+
+ subject { described_class.all.public_or_visible_to_user(user) }
+
+ context 'deploy token user without project' do
+ let_it_be(:user) { create(:deploy_token) }
+
+ it { is_expected.to eq [] }
+ end
+
+ context 'deploy token user with project' do
+ let_it_be(:user) { create(:deploy_token, projects: [private_project]) }
+
+ it { is_expected.to include(private_project) }
+ end
+ end
end
describe '.ids_with_issuables_available_for' do
@@ -3760,7 +3817,7 @@ describe Project do
end
end
- describe '.filter_by_feature_visibility' do
+ describe '.filter_by_feature_visibility', :enable_admin_mode do
include_context 'ProjectPolicyTable context'
include ProjectHelpers
using RSpec::Parameterized::TableSyntax
@@ -3955,16 +4012,6 @@ describe Project do
expect { project.remove_pages }.to change { pages_metadatum.reload.deployed }.from(true).to(false)
end
- it 'is a no-op when there is no namespace' do
- project.namespace.delete
- project.reload
-
- expect_any_instance_of(Projects::UpdatePagesConfigurationService).not_to receive(:execute)
- expect_any_instance_of(Gitlab::PagesTransfer).not_to receive(:rename_project)
-
- expect { project.remove_pages }.not_to change { pages_metadatum.reload.deployed }
- end
-
it 'is run when the project is destroyed' do
expect(project).to receive(:remove_pages).and_call_original
@@ -4716,20 +4763,6 @@ describe Project do
end
end
- describe '#wiki_repository_exists?' do
- it 'returns true when the wiki repository exists' do
- project = create(:project, :wiki_repo)
-
- expect(project.wiki_repository_exists?).to eq(true)
- end
-
- it 'returns false when the wiki repository does not exist' do
- project = create(:project)
-
- expect(project.wiki_repository_exists?).to eq(false)
- end
- end
-
describe '#write_repository_config' do
let_it_be(:project) { create(:project, :repository) }
@@ -5972,6 +6005,158 @@ describe Project do
end
end
+ describe '#validate_jira_import_settings!' do
+ include JiraServiceHelper
+
+ let_it_be(:project, reload: true) { create(:project) }
+
+ shared_examples 'raise Jira import error' do |message|
+ it 'returns error' do
+ expect { subject }.to raise_error(Projects::ImportService::Error, message)
+ end
+ end
+
+ shared_examples 'jira configuration base checks' do
+ context 'when feature flag is disabled' do
+ before do
+ stub_feature_flags(jira_issue_import: false)
+ end
+
+ it_behaves_like 'raise Jira import error', 'Jira import feature is disabled.'
+ end
+
+ context 'when feature flag is enabled' do
+ before do
+ stub_feature_flags(jira_issue_import: true)
+ end
+
+ context 'when Jira service was not setup' do
+ it_behaves_like 'raise Jira import error', 'Jira integration not configured.'
+ end
+
+ context 'when Jira service exists' do
+ let!(:jira_service) { create(:jira_service, project: project, active: true) }
+
+ context 'when Jira connection is not valid' do
+ before do
+ WebMock.stub_request(:get, 'https://jira.example.com/rest/api/2/serverInfo')
+ .to_raise(JIRA::HTTPError.new(double(message: 'Some failure.')))
+ end
+
+ it_behaves_like 'raise Jira import error', 'Unable to connect to the Jira instance. Please check your Jira integration configuration.'
+ end
+ end
+ end
+ end
+
+ before do
+ stub_jira_service_test
+ end
+
+ context 'without user param' do
+ subject { project.validate_jira_import_settings! }
+
+ it_behaves_like 'jira configuration base checks'
+
+ context 'when jira connection is valid' do
+ let!(:jira_service) { create(:jira_service, project: project, active: true) }
+
+ it 'does not return any error' do
+ expect { subject }.not_to raise_error
+ end
+ end
+ end
+
+ context 'with user param provided' do
+ let_it_be(:user) { create(:user) }
+
+ subject { project.validate_jira_import_settings!(user: user) }
+
+ context 'when user has permission to run import' do
+ before do
+ project.add_maintainer(user)
+ end
+
+ it_behaves_like 'jira configuration base checks'
+ end
+
+ context 'when feature flag is enabled' do
+ before do
+ stub_feature_flags(jira_issue_import: true)
+ end
+
+ context 'when user does not have permissions to run the import' do
+ before do
+ create(:jira_service, project: project, active: true)
+
+ project.add_developer(user)
+ end
+
+ it_behaves_like 'raise Jira import error', 'You do not have permissions to run the import.'
+ end
+
+ context 'when user has permission to run import' do
+ before do
+ project.add_maintainer(user)
+ end
+
+ let!(:jira_service) { create(:jira_service, project: project, active: true) }
+
+ context 'when issues feature is disabled' do
+ let_it_be(:project, reload: true) { create(:project, :issues_disabled) }
+
+ it_behaves_like 'raise Jira import error', 'Cannot import because issues are not available in this project.'
+ end
+
+ context 'when everything is ok' do
+ it 'does not return any error' do
+ expect { subject }.not_to raise_error
+ end
+ end
+ end
+ end
+ end
+ end
+
+ describe '#design_management_enabled?' do
+ let(:project) { build(:project) }
+
+ where(:lfs_enabled, :hashed_storage_enabled, :expectation) do
+ false | false | false
+ true | false | false
+ false | true | false
+ true | true | true
+ end
+
+ with_them do
+ before do
+ expect(project).to receive(:lfs_enabled?).and_return(lfs_enabled)
+ allow(project).to receive(:hashed_storage?).with(:repository).and_return(hashed_storage_enabled)
+ end
+
+ it do
+ expect(project.design_management_enabled?).to be(expectation)
+ end
+ end
+ end
+
+ describe '#bots' do
+ subject { project.bots }
+
+ let_it_be(:project) { create(:project) }
+ let_it_be(:project_bot) { create(:user, :project_bot) }
+ let_it_be(:user) { create(:user) }
+
+ before_all do
+ [project_bot, user].each do |member|
+ project.add_maintainer(member)
+ end
+ end
+
+ it { is_expected.to contain_exactly(project_bot) }
+ it { is_expected.not_to include(user) }
+ end
+
def finish_job(export_job)
export_job.start
export_job.finish
diff --git a/spec/models/project_wiki_spec.rb b/spec/models/project_wiki_spec.rb
index 1b121b7dee1..a4181e3be9a 100644
--- a/spec/models/project_wiki_spec.rb
+++ b/spec/models/project_wiki_spec.rb
@@ -1,448 +1,35 @@
# frozen_string_literal: true
-require "spec_helper"
+require 'spec_helper'
describe ProjectWiki do
- let(:user) { create(:user, :commit_email) }
- let(:project) { create(:project, :wiki_repo, namespace: user.namespace) }
- let(:repository) { project.repository }
- let(:gitlab_shell) { Gitlab::Shell.new }
- let(:project_wiki) { described_class.new(project, user) }
- let(:raw_repository) { Gitlab::Git::Repository.new(project.repository_storage, subject.disk_path + '.git', 'foo', 'group/project.wiki') }
- let(:commit) { project_wiki.repository.head_commit }
+ it_behaves_like 'wiki model' do
+ let(:wiki_container) { create(:project, :wiki_repo, namespace: user.namespace) }
+ let(:wiki_container_without_repo) { create(:project, namespace: user.namespace) }
- subject { project_wiki }
+ it { is_expected.to delegate_method(:storage).to(:container) }
+ it { is_expected.to delegate_method(:repository_storage).to(:container) }
+ it { is_expected.to delegate_method(:hashed_storage?).to(:container) }
- it { is_expected.to delegate_method(:repository_storage).to :project }
- it { is_expected.to delegate_method(:hashed_storage?).to :project }
-
- describe "#full_path" do
- it "returns the project path with namespace with the .wiki extension" do
- expect(subject.full_path).to eq(project.full_path + '.wiki')
- end
-
- it 'returns the same value as #full_path' do
- expect(subject.full_path).to eq(subject.full_path)
- end
- end
-
- describe '#web_url' do
- it 'returns the full web URL to the wiki' do
- expect(subject.web_url).to eq(Gitlab::UrlBuilder.build(subject))
- end
- end
-
- describe "#url_to_repo" do
- it "returns the correct ssh url to the repo" do
- expect(subject.url_to_repo).to eq(Gitlab::RepositoryUrlBuilder.build(subject.repository.full_path, protocol: :ssh))
- end
- end
-
- describe "#ssh_url_to_repo" do
- it "equals #url_to_repo" do
- expect(subject.ssh_url_to_repo).to eq(subject.url_to_repo)
- end
- end
-
- describe "#http_url_to_repo" do
- it "returns the correct http url to the repo" do
- expect(subject.http_url_to_repo).to eq(Gitlab::RepositoryUrlBuilder.build(subject.repository.full_path, protocol: :http))
- end
- end
-
- describe "#wiki_base_path" do
- it "returns the wiki base path" do
- wiki_base_path = "#{Gitlab.config.gitlab.relative_url_root}/#{project.full_path}/-/wikis"
-
- expect(subject.wiki_base_path).to eq(wiki_base_path)
- end
- end
-
- describe "#wiki" do
- it "contains a Gitlab::Git::Wiki instance" do
- expect(subject.wiki).to be_a Gitlab::Git::Wiki
- end
-
- it "creates a new wiki repo if one does not yet exist" do
- expect(project_wiki.create_page("index", "test content")).to be_truthy
- end
-
- it "creates a new wiki repo with a default commit message" do
- expect(project_wiki.create_page("index", "test content", :markdown, "")).to be_truthy
-
- page = project_wiki.find_page('index')
-
- expect(page.last_version.message).to eq("#{user.username} created page: index")
- end
-
- it "raises CouldNotCreateWikiError if it can't create the wiki repository" do
- # Create a fresh project which will not have a wiki
- project_wiki = described_class.new(create(:project), user)
- expect(project_wiki.repository).to receive(:create_if_not_exists) { false }
-
- expect { project_wiki.send(:wiki) }.to raise_exception(ProjectWiki::CouldNotCreateWikiError)
- end
- end
-
- describe "#empty?" do
- context "when the wiki repository is empty" do
- describe '#empty?' do
- subject { super().empty? }
-
- it { is_expected.to be_truthy }
- end
- end
-
- context "when the wiki has pages" do
- before do
- project_wiki.create_page("index", "This is an awesome new Gollum Wiki")
- project_wiki.create_page("another-page", "This is another page")
- end
-
- describe '#empty?' do
- subject { super().empty? }
-
- it { is_expected.to be_falsey }
-
- it 'only instantiates a Wiki page once' do
- expect(WikiPage).to receive(:new).once.and_call_original
-
- subject
- end
- end
- end
- end
-
- describe "#list_pages" do
- let(:wiki_pages) { subject.list_pages }
-
- before do
- create_page("index", "This is an index")
- create_page("index2", "This is an index2")
- create_page("an index3", "This is an index3")
- end
-
- after do
- wiki_pages.each do |wiki_page|
- destroy_page(wiki_page.page)
- end
- end
-
- it "returns an array of WikiPage instances" do
- expect(wiki_pages.first).to be_a WikiPage
- end
-
- it 'does not load WikiPage content by default' do
- wiki_pages.each do |page|
- expect(page.content).to be_empty
- end
- end
-
- it 'returns all pages by default' do
- expect(wiki_pages.count).to eq(3)
- end
-
- context "with limit option" do
- it 'returns limited set of pages' do
- expect(subject.list_pages(limit: 1).count).to eq(1)
- end
- end
-
- context "with sorting options" do
- it 'returns pages sorted by title by default' do
- pages = ['an index3', 'index', 'index2']
-
- expect(subject.list_pages.map(&:title)).to eq(pages)
- expect(subject.list_pages(direction: "desc").map(&:title)).to eq(pages.reverse)
- end
-
- it 'returns pages sorted by created_at' do
- pages = ['index', 'index2', 'an index3']
-
- expect(subject.list_pages(sort: 'created_at').map(&:title)).to eq(pages)
- expect(subject.list_pages(sort: 'created_at', direction: "desc").map(&:title)).to eq(pages.reverse)
- end
- end
-
- context "with load_content option" do
- let(:pages) { subject.list_pages(load_content: true) }
-
- it 'loads WikiPage content' do
- expect(pages.first.content).to eq("This is an index3")
- expect(pages.second.content).to eq("This is an index")
- expect(pages.third.content).to eq("This is an index2")
- end
- end
- end
-
- describe "#find_page" do
- before do
- create_page("index page", "This is an awesome Gollum Wiki")
- end
-
- after do
- subject.list_pages.each { |page| destroy_page(page.page) }
- end
-
- it "returns the latest version of the page if it exists" do
- page = subject.find_page("index page")
- expect(page.title).to eq("index page")
- end
-
- it "returns nil if the page does not exist" do
- expect(subject.find_page("non-existent")).to eq(nil)
- end
-
- it "can find a page by slug" do
- page = subject.find_page("index-page")
- expect(page.title).to eq("index page")
- end
-
- it "returns a WikiPage instance" do
- page = subject.find_page("index page")
- expect(page).to be_a WikiPage
- end
-
- context 'pages with multibyte-character title' do
- before do
- create_page("autre pagé", "C'est un génial Gollum Wiki")
- end
-
- it "can find a page by slug" do
- page = subject.find_page("autre pagé")
- expect(page.title).to eq("autre pagé")
- end
- end
-
- context 'pages with invalidly-encoded content' do
- before do
- create_page("encoding is fun", "f\xFCr".b)
- end
-
- it "can find the page" do
- page = subject.find_page("encoding is fun")
- expect(page.content).to eq("fr")
+ describe '#disk_path' do
+ it 'returns the repository storage path' do
+ expect(subject.disk_path).to eq("#{subject.container.disk_path}.wiki")
end
end
- end
-
- describe '#find_sidebar' do
- before do
- create_page(described_class::SIDEBAR, 'This is an awesome Sidebar')
- end
-
- after do
- subject.list_pages.each { |page| destroy_page(page.page) }
- end
-
- it 'finds the page defined as _sidebar' do
- page = subject.find_page('_sidebar')
-
- expect(page.content).to eq('This is an awesome Sidebar')
- end
- end
- describe '#find_file' do
- let(:image) { File.open(Rails.root.join('spec', 'fixtures', 'big-image.png')) }
+ describe '#update_container_activity' do
+ it 'updates project activity' do
+ wiki_container.update!(
+ last_activity_at: nil,
+ last_repository_updated_at: nil
+ )
- before do
- subject.wiki # Make sure the wiki repo exists
+ subject.create_page('Test Page', 'This is content')
+ wiki_container.reload
- repo_path = Gitlab::GitalyClient::StorageSettings.allow_disk_access do
- subject.repository.path_to_repo
+ expect(wiki_container.last_activity_at).to be_within(1.minute).of(Time.now)
+ expect(wiki_container.last_repository_updated_at).to be_within(1.minute).of(Time.now)
end
-
- BareRepoOperations.new(repo_path).commit_file(image, 'image.png')
- end
-
- it 'returns the latest version of the file if it exists' do
- file = subject.find_file('image.png')
- expect(file.mime_type).to eq('image/png')
- end
-
- it 'returns nil if the page does not exist' do
- expect(subject.find_file('non-existent')).to eq(nil)
- end
-
- it 'returns a Gitlab::Git::WikiFile instance' do
- file = subject.find_file('image.png')
- expect(file).to be_a Gitlab::Git::WikiFile
- end
-
- it 'returns the whole file' do
- file = subject.find_file('image.png')
- image.rewind
-
- expect(file.raw_data.b).to eq(image.read.b)
- end
- end
-
- describe "#create_page" do
- after do
- destroy_page(subject.list_pages.first.page)
- end
-
- it "creates a new wiki page" do
- expect(subject.create_page("test page", "this is content")).not_to eq(false)
- expect(subject.list_pages.count).to eq(1)
- end
-
- it "returns false when a duplicate page exists" do
- subject.create_page("test page", "content")
- expect(subject.create_page("test page", "content")).to eq(false)
end
-
- it "stores an error message when a duplicate page exists" do
- 2.times { subject.create_page("test page", "content") }
- expect(subject.error_message).to match(/Duplicate page:/)
- end
-
- it "sets the correct commit message" do
- subject.create_page("test page", "some content", :markdown, "commit message")
- expect(subject.list_pages.first.page.version.message).to eq("commit message")
- end
-
- it 'sets the correct commit email' do
- subject.create_page('test page', 'content')
-
- expect(user.commit_email).not_to eq(user.email)
- expect(commit.author_email).to eq(user.commit_email)
- expect(commit.committer_email).to eq(user.commit_email)
- end
-
- it 'updates project activity' do
- subject.create_page('Test Page', 'This is content')
-
- project.reload
-
- expect(project.last_activity_at).to be_within(1.minute).of(Time.now)
- expect(project.last_repository_updated_at).to be_within(1.minute).of(Time.now)
- end
- end
-
- describe "#update_page" do
- before do
- create_page("update-page", "some content")
- @gitlab_git_wiki_page = subject.wiki.page(title: "update-page")
- subject.update_page(
- @gitlab_git_wiki_page,
- content: "some other content",
- format: :markdown,
- message: "updated page"
- )
- @page = subject.list_pages(load_content: true).first.page
- end
-
- after do
- destroy_page(@page)
- end
-
- it "updates the content of the page" do
- expect(@page.raw_data).to eq("some other content")
- end
-
- it "sets the correct commit message" do
- expect(@page.version.message).to eq("updated page")
- end
-
- it 'sets the correct commit email' do
- expect(user.commit_email).not_to eq(user.email)
- expect(commit.author_email).to eq(user.commit_email)
- expect(commit.committer_email).to eq(user.commit_email)
- end
-
- it 'updates project activity' do
- subject.update_page(
- @gitlab_git_wiki_page,
- content: 'Yet more content',
- format: :markdown,
- message: 'Updated page again'
- )
-
- project.reload
-
- expect(project.last_activity_at).to be_within(1.minute).of(Time.now)
- expect(project.last_repository_updated_at).to be_within(1.minute).of(Time.now)
- end
- end
-
- describe "#delete_page" do
- before do
- create_page("index", "some content")
- @page = subject.wiki.page(title: "index")
- end
-
- it "deletes the page" do
- subject.delete_page(@page)
- expect(subject.list_pages.count).to eq(0)
- end
-
- it 'sets the correct commit email' do
- subject.delete_page(@page)
-
- expect(user.commit_email).not_to eq(user.email)
- expect(commit.author_email).to eq(user.commit_email)
- expect(commit.committer_email).to eq(user.commit_email)
- end
-
- it 'updates project activity' do
- subject.delete_page(@page)
-
- project.reload
-
- expect(project.last_activity_at).to be_within(1.minute).of(Time.now)
- expect(project.last_repository_updated_at).to be_within(1.minute).of(Time.now)
- end
- end
-
- describe '#ensure_repository' do
- let(:project) { create(:project) }
-
- it 'creates the repository if it not exist' do
- expect(raw_repository.exists?).to eq(false)
-
- subject.ensure_repository
-
- expect(raw_repository.exists?).to eq(true)
- end
-
- it 'does not create the repository if it exists' do
- subject.wiki
- expect(raw_repository.exists?).to eq(true)
-
- expect(subject).not_to receive(:create_repo!)
-
- subject.ensure_repository
- end
- end
-
- describe '#hook_attrs' do
- it 'returns a hash with values' do
- expect(subject.hook_attrs).to be_a Hash
- expect(subject.hook_attrs.keys).to contain_exactly(:web_url, :git_ssh_url, :git_http_url, :path_with_namespace, :default_branch)
- end
- end
-
- private
-
- def create_temp_repo(path)
- FileUtils.mkdir_p path
- system(*%W(#{Gitlab.config.git.bin_path} init --quiet --bare -- #{path}))
- end
-
- def remove_temp_repo(path)
- FileUtils.rm_rf path
- end
-
- def commit_details
- Gitlab::Git::Wiki::CommitDetails.new(user.id, user.username, user.name, user.commit_email, "test commit")
- end
-
- def create_page(name, content)
- subject.wiki.write_page(name, :markdown, content, commit_details)
- end
-
- def destroy_page(page)
- subject.delete_page(page, "test commit")
end
end
diff --git a/spec/models/release_spec.rb b/spec/models/release_spec.rb
index 8b1b738ab58..d72fd137f3f 100644
--- a/spec/models/release_spec.rb
+++ b/spec/models/release_spec.rb
@@ -111,26 +111,6 @@ RSpec.describe Release do
end
end
- describe '#notify_new_release' do
- context 'when a release is created' do
- it 'instantiates NewReleaseWorker to send notifications' do
- expect(NewReleaseWorker).to receive(:perform_async)
-
- create(:release)
- end
- end
-
- context 'when a release is updated' do
- let!(:release) { create(:release) }
-
- it 'does not send any new notification' do
- expect(NewReleaseWorker).not_to receive(:perform_async)
-
- release.update!(description: 'new description')
- end
- end
- end
-
describe '#name' do
context 'name is nil' do
before do
@@ -143,38 +123,6 @@ RSpec.describe Release do
end
end
- describe '#evidence_sha' do
- subject { release.evidence_sha }
-
- context 'when a release was created before evidence collection existed' do
- let!(:release) { create(:release) }
-
- it { is_expected.to be_nil }
- end
-
- context 'when a release was created with evidence collection' do
- let!(:release) { create(:release, :with_evidence) }
-
- it { is_expected.to eq(release.evidences.first.summary_sha) }
- end
- end
-
- describe '#evidence_summary' do
- subject { release.evidence_summary }
-
- context 'when a release was created before evidence collection existed' do
- let!(:release) { create(:release) }
-
- it { is_expected.to eq({}) }
- end
-
- context 'when a release was created with evidence collection' do
- let!(:release) { create(:release, :with_evidence) }
-
- it { is_expected.to eq(release.evidences.first.summary) }
- end
- end
-
describe '#milestone_titles' do
let(:release) { create(:release, :with_milestones) }
diff --git a/spec/models/remote_mirror_spec.rb b/spec/models/remote_mirror_spec.rb
index 15b162ae87a..a87cdcf9344 100644
--- a/spec/models/remote_mirror_spec.rb
+++ b/spec/models/remote_mirror_spec.rb
@@ -143,22 +143,54 @@ describe RemoteMirror, :mailer do
end
describe '#update_repository' do
- let(:git_remote_mirror) { spy }
+ it 'performs update including options' do
+ git_remote_mirror = stub_const('Gitlab::Git::RemoteMirror', spy)
+ mirror = build(:remote_mirror)
- before do
- stub_const('Gitlab::Git::RemoteMirror', git_remote_mirror)
+ expect(mirror).to receive(:options_for_update).and_return(keep_divergent_refs: true)
+ mirror.update_repository
+
+ expect(git_remote_mirror).to have_received(:new).with(
+ mirror.project.repository.raw,
+ mirror.remote_name,
+ keep_divergent_refs: true
+ )
+ expect(git_remote_mirror).to have_received(:update)
end
+ end
- it 'includes the `keep_divergent_refs` setting' do
+ describe '#options_for_update' do
+ it 'includes the `keep_divergent_refs` option' do
mirror = build_stubbed(:remote_mirror, keep_divergent_refs: true)
- mirror.update_repository({})
+ options = mirror.options_for_update
- expect(git_remote_mirror).to have_received(:new).with(
- anything,
- mirror.remote_name,
- hash_including(keep_divergent_refs: true)
- )
+ expect(options).to include(keep_divergent_refs: true)
+ end
+
+ it 'includes the `only_branches_matching` option' do
+ branch = create(:protected_branch)
+ mirror = build_stubbed(:remote_mirror, project: branch.project, only_protected_branches: true)
+
+ options = mirror.options_for_update
+
+ expect(options).to include(only_branches_matching: [branch.name])
+ end
+
+ it 'includes the `ssh_key` option' do
+ mirror = build(:remote_mirror, :ssh, ssh_private_key: 'private-key')
+
+ options = mirror.options_for_update
+
+ expect(options).to include(ssh_key: 'private-key')
+ end
+
+ it 'includes the `known_hosts` option' do
+ mirror = build(:remote_mirror, :ssh, ssh_known_hosts: 'known-hosts')
+
+ options = mirror.options_for_update
+
+ expect(options).to include(known_hosts: 'known-hosts')
end
end
diff --git a/spec/models/repository_spec.rb b/spec/models/repository_spec.rb
index ca04bd7a28a..be626dd6e32 100644
--- a/spec/models/repository_spec.rb
+++ b/spec/models/repository_spec.rb
@@ -2874,4 +2874,80 @@ describe Repository do
expect(repository.submodule_links).to be_a(Gitlab::SubmoduleLinks)
end
end
+
+ describe '#lfs_enabled?' do
+ let_it_be(:project) { create(:project, :repository, :design_repo, lfs_enabled: true) }
+
+ subject { repository.lfs_enabled? }
+
+ context 'for a project repository' do
+ let(:repository) { project.repository }
+
+ it 'returns true when LFS is enabled' do
+ stub_lfs_setting(enabled: true)
+
+ is_expected.to be_truthy
+ end
+
+ it 'returns false when LFS is disabled' do
+ stub_lfs_setting(enabled: false)
+
+ is_expected.to be_falsy
+ end
+ end
+
+ context 'for a project wiki repository' do
+ let(:repository) { project.wiki.repository }
+
+ it 'returns true when LFS is enabled' do
+ stub_lfs_setting(enabled: true)
+
+ is_expected.to be_truthy
+ end
+
+ it 'returns false when LFS is disabled' do
+ stub_lfs_setting(enabled: false)
+
+ is_expected.to be_falsy
+ end
+ end
+
+ context 'for a project snippet repository' do
+ let(:snippet) { create(:project_snippet, project: project) }
+ let(:repository) { snippet.repository }
+
+ it 'returns false when LFS is enabled' do
+ stub_lfs_setting(enabled: true)
+
+ is_expected.to be_falsy
+ end
+ end
+
+ context 'for a personal snippet repository' do
+ let(:snippet) { create(:personal_snippet) }
+ let(:repository) { snippet.repository }
+
+ it 'returns false when LFS is enabled' do
+ stub_lfs_setting(enabled: true)
+
+ is_expected.to be_falsy
+ end
+ end
+
+ context 'for a design repository' do
+ let(:repository) { project.design_repository }
+
+ it 'returns true when LFS is enabled' do
+ stub_lfs_setting(enabled: true)
+
+ is_expected.to be_truthy
+ end
+
+ it 'returns false when LFS is disabled' do
+ stub_lfs_setting(enabled: false)
+
+ is_expected.to be_falsy
+ end
+ end
+ end
end
diff --git a/spec/models/resource_label_event_spec.rb b/spec/models/resource_label_event_spec.rb
index ca887b485a2..a1a2150f461 100644
--- a/spec/models/resource_label_event_spec.rb
+++ b/spec/models/resource_label_event_spec.rb
@@ -15,9 +15,6 @@ RSpec.describe ResourceLabelEvent, type: :model do
it_behaves_like 'a resource event for merge requests'
describe 'associations' do
- it { is_expected.to belong_to(:user) }
- it { is_expected.to belong_to(:issue) }
- it { is_expected.to belong_to(:merge_request) }
it { is_expected.to belong_to(:label) }
end
diff --git a/spec/models/resource_milestone_event_spec.rb b/spec/models/resource_milestone_event_spec.rb
index bf8672f95c9..3f8d8b4c1df 100644
--- a/spec/models/resource_milestone_event_spec.rb
+++ b/spec/models/resource_milestone_event_spec.rb
@@ -78,4 +78,21 @@ describe ResourceMilestoneEvent, type: :model do
let(:query_method) { :remove? }
end
end
+
+ describe '#milestone_title' do
+ let(:milestone) { create(:milestone, title: 'v2.3') }
+ let(:event) { create(:resource_milestone_event, milestone: milestone) }
+
+ it 'returns the expected title' do
+ expect(event.milestone_title).to eq('v2.3')
+ end
+
+ context 'when milestone is nil' do
+ let(:event) { create(:resource_milestone_event, milestone: nil) }
+
+ it 'returns nil' do
+ expect(event.milestone_title).to be_nil
+ end
+ end
+ end
end
diff --git a/spec/models/resource_state_event_spec.rb b/spec/models/resource_state_event_spec.rb
new file mode 100644
index 00000000000..986a13cbd0d
--- /dev/null
+++ b/spec/models/resource_state_event_spec.rb
@@ -0,0 +1,14 @@
+# frozen_string_literal: true
+
+require 'spec_helper'
+
+describe ResourceStateEvent, type: :model do
+ subject { build(:resource_state_event, issue: issue) }
+
+ let(:issue) { create(:issue) }
+ let(:merge_request) { create(:merge_request) }
+
+ it_behaves_like 'a resource event'
+ it_behaves_like 'a resource event for issues'
+ it_behaves_like 'a resource event for merge requests'
+end
diff --git a/spec/models/sent_notification_spec.rb b/spec/models/sent_notification_spec.rb
index fedaae372c4..087bc957373 100644
--- a/spec/models/sent_notification_spec.rb
+++ b/spec/models/sent_notification_spec.rb
@@ -326,4 +326,26 @@ describe SentNotification do
end
end
end
+
+ describe "#position=" do
+ subject { build(:sent_notification, noteable: create(:issue)) }
+
+ it "doesn't accept non-hash JSON passed as a string" do
+ subject.position = "true"
+
+ expect(subject.attributes_before_type_cast["position"]).to be(nil)
+ end
+
+ it "does accept a position hash as a string" do
+ subject.position = '{ "base_sha": "test" }'
+
+ expect(subject.position.base_sha).to eq("test")
+ end
+
+ it "does accept a hash" do
+ subject.position = { "base_sha" => "test" }
+
+ expect(subject.position.base_sha).to eq("test")
+ end
+ end
end
diff --git a/spec/models/service_spec.rb b/spec/models/service_spec.rb
index cb8122b6573..106f8def42d 100644
--- a/spec/models/service_spec.rb
+++ b/spec/models/service_spec.rb
@@ -87,6 +87,20 @@ describe Service do
end
end
+ describe '#operating?' do
+ it 'is false when the service is not active' do
+ expect(build(:service).operating?).to eq(false)
+ end
+
+ it 'is false when the service is not persisted' do
+ expect(build(:service, active: true).operating?).to eq(false)
+ end
+
+ it 'is true when the service is active and persisted' do
+ expect(create(:service, active: true).operating?).to eq(true)
+ end
+ end
+
describe '.confidential_note_hooks' do
it 'includes services where confidential_note_events is true' do
create(:service, active: true, confidential_note_events: true)
@@ -523,24 +537,6 @@ describe Service do
end
end
- describe "#deprecated?" do
- let(:project) { create(:project, :repository) }
-
- it 'returns false by default' do
- service = create(:service, project: project)
- expect(service.deprecated?).to be_falsy
- end
- end
-
- describe "#deprecation_message" do
- let(:project) { create(:project, :repository) }
-
- it 'is empty by default' do
- service = create(:service, project: project)
- expect(service.deprecation_message).to be_nil
- end
- end
-
describe '#api_field_names' do
let(:fake_service) do
Class.new(Service) do
diff --git a/spec/models/snippet_repository_spec.rb b/spec/models/snippet_repository_spec.rb
index dc9f9a95d24..255f07ebfa5 100644
--- a/spec/models/snippet_repository_spec.rb
+++ b/spec/models/snippet_repository_spec.rb
@@ -202,6 +202,38 @@ describe SnippetRepository do
it_behaves_like 'snippet repository with file names', 'snippetfile10.txt', 'snippetfile11.txt'
end
+
+ shared_examples 'snippet repository with git errors' do |path, error|
+ let(:new_file) { { file_path: path, content: 'bar' } }
+
+ it 'raises a path specific error' do
+ expect do
+ snippet_repository.multi_files_action(user, data, commit_opts)
+ end.to raise_error(error)
+ end
+ end
+
+ context 'with git errors' do
+ it_behaves_like 'snippet repository with git errors', 'invalid://path/here', described_class::InvalidPathError
+ it_behaves_like 'snippet repository with git errors', '../../path/traversal/here', described_class::InvalidPathError
+ it_behaves_like 'snippet repository with git errors', 'README', described_class::CommitError
+
+ context 'when user name is invalid' do
+ let(:user) { create(:user, name: '.') }
+
+ it_behaves_like 'snippet repository with git errors', 'non_existing_file', described_class::InvalidSignatureError
+ end
+
+ context 'when user email is empty' do
+ let(:user) { create(:user) }
+
+ before do
+ user.update_column(:email, '')
+ end
+
+ it_behaves_like 'snippet repository with git errors', 'non_existing_file', described_class::InvalidSignatureError
+ end
+ end
end
def blob_at(snippet, path)
diff --git a/spec/models/snippet_spec.rb b/spec/models/snippet_spec.rb
index 2061084d5ea..4d6586c1df4 100644
--- a/spec/models/snippet_spec.rb
+++ b/spec/models/snippet_spec.rb
@@ -180,22 +180,6 @@ describe Snippet do
end
end
- describe '.search_code' do
- let(:snippet) { create(:snippet, content: 'class Foo; end') }
-
- it 'returns snippets with matching content' do
- expect(described_class.search_code(snippet.content)).to eq([snippet])
- end
-
- it 'returns snippets with partially matching content' do
- expect(described_class.search_code('class')).to eq([snippet])
- end
-
- it 'returns snippets with matching content regardless of the casing' do
- expect(described_class.search_code('FOO')).to eq([snippet])
- end
- end
-
describe 'when default snippet visibility set to internal' do
using RSpec::Parameterized::TableSyntax
@@ -545,11 +529,11 @@ describe Snippet do
let(:snippet) { build(:snippet) }
it 'excludes secret_token from generated json' do
- expect(JSON.parse(to_json).keys).not_to include("secret_token")
+ expect(Gitlab::Json.parse(to_json).keys).not_to include("secret_token")
end
it 'does not override existing exclude option value' do
- expect(JSON.parse(to_json(except: [:id])).keys).not_to include("secret_token", "id")
+ expect(Gitlab::Json.parse(to_json(except: [:id])).keys).not_to include("secret_token", "id")
end
def to_json(params = {})
@@ -735,31 +719,35 @@ describe Snippet do
end
end
- describe '#versioned_enabled_for?' do
- let_it_be(:user) { create(:user) }
+ describe '#url_to_repo' do
+ subject { snippet.url_to_repo }
+
+ context 'with personal snippet' do
+ let(:snippet) { create(:personal_snippet) }
- subject { snippet.versioned_enabled_for?(user) }
+ it { is_expected.to eq(Gitlab.config.gitlab_shell.ssh_path_prefix + "snippets/#{snippet.id}.git") }
+ end
- context 'with repository and version_snippets enabled' do
- let!(:snippet) { create(:personal_snippet, :repository, author: user) }
+ context 'with project snippet' do
+ let(:snippet) { create(:project_snippet) }
- it { is_expected.to be_truthy }
+ it { is_expected.to eq(Gitlab.config.gitlab_shell.ssh_path_prefix + "#{snippet.project.full_path}/snippets/#{snippet.id}.git") }
end
+ end
- context 'without repository' do
- let!(:snippet) { create(:personal_snippet, author: user) }
+ describe '.max_file_limit' do
+ subject { described_class.max_file_limit(nil) }
- it { is_expected.to be_falsy }
+ it "returns #{Snippet::MAX_FILE_COUNT}" do
+ expect(subject).to eq Snippet::MAX_FILE_COUNT
end
- context 'without version_snippets feature disabled' do
- let!(:snippet) { create(:personal_snippet, :repository, author: user) }
+ context 'when feature flag :snippet_multiple_files is disabled' do
+ it "returns #{described_class::MAX_SINGLE_FILE_COUNT}" do
+ stub_feature_flags(snippet_multiple_files: false)
- before do
- stub_feature_flags(version_snippets: false)
+ expect(subject).to eq described_class::MAX_SINGLE_FILE_COUNT
end
-
- it { is_expected.to be_falsy }
end
end
end
diff --git a/spec/models/spam_log_spec.rb b/spec/models/spam_log_spec.rb
index 8ebd97de9ff..8d0f247b5d6 100644
--- a/spec/models/spam_log_spec.rb
+++ b/spec/models/spam_log_spec.rb
@@ -20,15 +20,30 @@ describe SpamLog do
expect { spam_log.remove_user(deleted_by: admin) }.to change { spam_log.user.blocked? }.to(true)
end
- it 'removes the user', :sidekiq_might_not_need_inline do
- spam_log = build(:spam_log)
- user = spam_log.user
+ context 'when admin mode is enabled', :enable_admin_mode do
+ it 'removes the user', :sidekiq_might_not_need_inline do
+ spam_log = build(:spam_log)
+ user = spam_log.user
+
+ perform_enqueued_jobs do
+ spam_log.remove_user(deleted_by: admin)
+ end
- perform_enqueued_jobs do
- spam_log.remove_user(deleted_by: admin)
+ expect { User.find(user.id) }.to raise_error(ActiveRecord::RecordNotFound)
end
+ end
- expect { User.find(user.id) }.to raise_error(ActiveRecord::RecordNotFound)
+ context 'when admin mode is disabled' do
+ it 'does not allow to remove the user', :sidekiq_might_not_need_inline do
+ spam_log = build(:spam_log)
+ user = spam_log.user
+
+ perform_enqueued_jobs do
+ spam_log.remove_user(deleted_by: admin)
+ end
+
+ expect(User.exists?(user.id)).to be(true)
+ end
end
end
diff --git a/spec/models/state_note_spec.rb b/spec/models/state_note_spec.rb
new file mode 100644
index 00000000000..d3409315e41
--- /dev/null
+++ b/spec/models/state_note_spec.rb
@@ -0,0 +1,29 @@
+# frozen_string_literal: true
+
+require 'spec_helper'
+
+describe StateNote do
+ describe '.from_event' do
+ let_it_be(:author) { create(:user) }
+ let_it_be(:project) { create(:project, :repository) }
+ let_it_be(:noteable) { create(:issue, author: author, project: project) }
+
+ ResourceStateEvent.states.each do |state, _value|
+ context "with event state #{state}" do
+ let_it_be(:event) { create(:resource_state_event, issue: noteable, state: state, created_at: '2020-02-05') }
+
+ subject { described_class.from_event(event, resource: noteable, resource_parent: project) }
+
+ it_behaves_like 'a system note', exclude_project: true do
+ let(:action) { state.to_s }
+ end
+
+ it 'contains the expected values' do
+ expect(subject.author).to eq(author)
+ expect(subject.created_at).to eq(event.created_at)
+ expect(subject.note_html).to eq("<p dir=\"auto\">#{state}</p>")
+ end
+ end
+ end
+ end
+end
diff --git a/spec/models/timelog_spec.rb b/spec/models/timelog_spec.rb
index 33c1afad59f..ae1697fb7e6 100644
--- a/spec/models/timelog_spec.rb
+++ b/spec/models/timelog_spec.rb
@@ -56,12 +56,12 @@ RSpec.describe Timelog do
end
end
- describe 'between_dates' do
- it 'returns collection of timelogs within given dates' do
+ describe 'between_times' do
+ it 'returns collection of timelogs within given times' do
create(:timelog, spent_at: 65.days.ago)
timelog1 = create(:timelog, spent_at: 15.days.ago)
timelog2 = create(:timelog, spent_at: 5.days.ago)
- timelogs = described_class.between_dates(20.days.ago, 1.day.ago)
+ timelogs = described_class.between_times(20.days.ago, 1.day.ago)
expect(timelogs).to contain_exactly(timelog1, timelog2)
end
diff --git a/spec/models/todo_spec.rb b/spec/models/todo_spec.rb
index 3f0c95b2513..e125f58399e 100644
--- a/spec/models/todo_spec.rb
+++ b/spec/models/todo_spec.rb
@@ -61,11 +61,13 @@ describe Todo do
describe '#done' do
it 'changes state to done' do
todo = create(:todo, state: :pending)
+
expect { todo.done }.to change(todo, :state).from('pending').to('done')
end
it 'does not raise error when is already done' do
todo = create(:todo, state: :done)
+
expect { todo.done }.not_to raise_error
end
end
@@ -73,15 +75,31 @@ describe Todo do
describe '#for_commit?' do
it 'returns true when target is a commit' do
subject.target_type = 'Commit'
+
expect(subject.for_commit?).to eq true
end
it 'returns false when target is an issuable' do
subject.target_type = 'Issue'
+
expect(subject.for_commit?).to eq false
end
end
+ describe '#for_design?' do
+ it 'returns true when target is a Design' do
+ subject.target_type = 'DesignManagement::Design'
+
+ expect(subject.for_design?).to eq(true)
+ end
+
+ it 'returns false when target is not a Design' do
+ subject.target_type = 'Issue'
+
+ expect(subject.for_design?).to eq(false)
+ end
+ end
+
describe '#target' do
context 'for commits' do
let(:project) { create(:project, :repository) }
@@ -108,6 +126,7 @@ describe Todo do
it 'returns the issuable for issuables' do
subject.target_id = issue.id
subject.target_type = issue.class.name
+
expect(subject.target).to eq issue
end
end
@@ -126,6 +145,7 @@ describe Todo do
it 'returns full reference for issuables' do
subject.target = issue
+
expect(subject.target_reference).to eq issue.to_reference(full: false)
end
end
@@ -389,5 +409,17 @@ describe Todo do
expect(described_class.update_state(:pending)).to be_empty
end
+
+ it 'updates updated_at' do
+ create(:todo, :pending)
+
+ Timecop.freeze(1.day.from_now) do
+ expected_update_date = Time.now.utc
+
+ ids = described_class.update_state(:done)
+
+ expect(Todo.where(id: ids).map(&:updated_at)).to all(be_like_time(expected_update_date))
+ end
+ end
end
end
diff --git a/spec/models/tree_spec.rb b/spec/models/tree_spec.rb
index c2d5dfdf9c4..7dde8459f9a 100644
--- a/spec/models/tree_spec.rb
+++ b/spec/models/tree_spec.rb
@@ -9,15 +9,18 @@ describe Tree do
subject { described_class.new(repository, '54fcc214') }
describe '#readme' do
- class FakeBlob
- attr_reader :name
-
- def initialize(name)
- @name = name
- end
-
- def readme?
- name =~ /^readme/i
+ before do
+ stub_const('FakeBlob', Class.new)
+ FakeBlob.class_eval do
+ attr_reader :name
+
+ def initialize(name)
+ @name = name
+ end
+
+ def readme?
+ name =~ /^readme/i
+ end
end
end
diff --git a/spec/models/user_spec.rb b/spec/models/user_spec.rb
index 8597397c3c6..94a3f6bafea 100644
--- a/spec/models/user_spec.rb
+++ b/spec/models/user_spec.rb
@@ -2,7 +2,7 @@
require 'spec_helper'
-describe User, :do_not_mock_admin_mode do
+describe User do
include ProjectForksHelper
include TermsHelper
include ExclusiveLeaseHelpers
@@ -17,6 +17,7 @@ describe User, :do_not_mock_admin_mode do
it { is_expected.to include_module(Sortable) }
it { is_expected.to include_module(TokenAuthenticatable) }
it { is_expected.to include_module(BlocksJsonSerialization) }
+ it { is_expected.to include_module(AsyncDeviseEmail) }
end
describe 'delegations' do
@@ -54,6 +55,7 @@ describe User, :do_not_mock_admin_mode do
it { is_expected.to have_many(:reported_abuse_reports).dependent(:destroy).class_name('AbuseReport') }
it { is_expected.to have_many(:custom_attributes).class_name('UserCustomAttribute') }
it { is_expected.to have_many(:releases).dependent(:nullify) }
+ it { is_expected.to have_many(:metrics_users_starred_dashboards).inverse_of(:user) }
describe "#bio" do
it 'syncs bio with `user_details.bio` on create' do
@@ -164,6 +166,18 @@ describe User, :do_not_mock_admin_mode do
end
end
+ describe 'Devise emails' do
+ let!(:user) { create(:user) }
+
+ describe 'behaviour' do
+ it 'sends emails asynchronously' do
+ expect do
+ user.update!(email: 'hello@hello.com')
+ end.to have_enqueued_job.on_queue('mailers').exactly(:twice)
+ end
+ end
+ end
+
describe 'validations' do
describe 'password' do
let!(:user) { create(:user) }
@@ -295,7 +309,7 @@ describe User, :do_not_mock_admin_mode do
subject { build(:user) }
end
- it_behaves_like 'an object with email-formated attributes', :public_email, :notification_email do
+ it_behaves_like 'an object with RFC3696 compliant email-formated attributes', :public_email, :notification_email do
subject { build(:user).tap { |user| user.emails << build(:email, email: email_value) } }
end
@@ -538,18 +552,6 @@ describe User, :do_not_mock_admin_mode do
expect(user).to be_valid
end
- context 'when feature flag is turned off' do
- before do
- stub_feature_flags(email_restrictions: false)
- end
-
- it 'does accept the email address' do
- user = build(:user, email: 'info+1@test.com')
-
- expect(user).to be_valid
- end
- end
-
context 'when created_by_id is set' do
it 'does accept the email address' do
user = build(:user, email: 'info+1@test.com', created_by_id: 1)
@@ -813,7 +815,7 @@ describe User, :do_not_mock_admin_mode do
describe '.active_without_ghosts' do
let_it_be(:user1) { create(:user, :external) }
let_it_be(:user2) { create(:user, state: 'blocked') }
- let_it_be(:user3) { create(:user, ghost: true) }
+ let_it_be(:user3) { create(:user, :ghost) }
let_it_be(:user4) { create(:user) }
it 'returns all active users but ghost users' do
@@ -824,7 +826,7 @@ describe User, :do_not_mock_admin_mode do
describe '.without_ghosts' do
let_it_be(:user1) { create(:user, :external) }
let_it_be(:user2) { create(:user, state: 'blocked') }
- let_it_be(:user3) { create(:user, ghost: true) }
+ let_it_be(:user3) { create(:user, :ghost) }
it 'returns users without ghosts users' do
expect(described_class.without_ghosts).to match_array([user1, user2])
@@ -927,7 +929,6 @@ describe User, :do_not_mock_admin_mode do
user.tap { |u| u.update!(email: new_email) }.reload
end.to change(user, :unconfirmed_email).to(new_email)
end
-
it 'does not change :notification_email' do
expect do
user.tap { |u| u.update!(email: new_email) }.reload
@@ -3275,7 +3276,6 @@ describe User, :do_not_mock_admin_mode do
expect(ghost.namespace).not_to be_nil
expect(ghost.namespace).to be_persisted
expect(ghost.user_type).to eq 'ghost'
- expect(ghost.ghost).to eq true
end
it "does not create a second ghost user if one is already present" do
@@ -4077,7 +4077,7 @@ describe User, :do_not_mock_admin_mode do
context 'in single-user environment' do
it 'requires user consent after one week' do
- create(:user, ghost: true)
+ create(:user, :ghost)
expect(user.requires_usage_stats_consent?).to be true
end
@@ -4355,31 +4355,15 @@ describe User, :do_not_mock_admin_mode do
end
end
- describe 'internal methods' do
- let_it_be(:user) { create(:user) }
- let_it_be(:ghost) { described_class.ghost }
- let_it_be(:alert_bot) { described_class.alert_bot }
- let_it_be(:project_bot) { create(:user, :project_bot) }
- let_it_be(:non_internal) { [user, project_bot] }
- let_it_be(:internal) { [ghost, alert_bot] }
+ describe '.active_without_ghosts' do
+ let_it_be(:user1) { create(:user, :external) }
+ let_it_be(:user2) { create(:user, state: 'blocked') }
+ let_it_be(:user3) { create(:user, :ghost) }
+ let_it_be(:user4) { create(:user, user_type: :support_bot) }
+ let_it_be(:user5) { create(:user, state: 'blocked', user_type: :support_bot) }
- it 'returns internal users' do
- expect(described_class.internal).to match_array(internal)
- expect(internal.all?(&:internal?)).to eq(true)
- end
-
- it 'returns non internal users' do
- expect(described_class.non_internal).to match_array(non_internal)
- expect(non_internal.all?(&:internal?)).to eq(false)
- end
-
- describe '#bot?' do
- it 'marks bot users' do
- expect(user.bot?).to eq(false)
- expect(ghost.bot?).to eq(false)
-
- expect(alert_bot.bot?).to eq(true)
- end
+ it 'returns all active users including active bots but ghost users' do
+ expect(described_class.active_without_ghosts).to match_array([user1, user4])
end
end
@@ -4417,19 +4401,6 @@ describe User, :do_not_mock_admin_mode do
end
end
- describe 'bots & humans' do
- it 'returns corresponding users' do
- human = create(:user)
- bot = create(:user, :bot)
- project_bot = create(:user, :project_bot)
-
- expect(described_class.humans).to match_array([human])
- expect(described_class.bots).to match_array([bot, project_bot])
- expect(described_class.bots_without_project_bot).to match_array([bot])
- expect(described_class.with_project_bots).to match_array([human, project_bot])
- end
- end
-
describe '#hook_attrs' do
it 'includes name, username, avatar_url, and email' do
user = create(:user)
@@ -4458,45 +4429,6 @@ describe User, :do_not_mock_admin_mode do
end
end
- describe '#gitlab_employee?' do
- using RSpec::Parameterized::TableSyntax
-
- subject { user.gitlab_employee? }
-
- where(:email, :is_com, :expected_result) do
- 'test@gitlab.com' | true | true
- 'test@example.com' | true | false
- 'test@gitlab.com' | false | false
- 'test@example.com' | false | false
- end
-
- with_them do
- let(:user) { build(:user, email: email) }
-
- before do
- allow(Gitlab).to receive(:com?).and_return(is_com)
- end
-
- it { is_expected.to be expected_result }
- end
-
- context 'when email is of Gitlab and is not confirmed' do
- let(:user) { build(:user, email: 'test@gitlab.com', confirmed_at: nil) }
-
- it { is_expected.to be false }
- end
-
- context 'when `:gitlab_employee_badge` feature flag is disabled' do
- let(:user) { build(:user, email: 'test@gitlab.com') }
-
- before do
- stub_feature_flags(gitlab_employee_badge: false)
- end
-
- it { is_expected.to be false }
- end
- end
-
describe '#current_highest_access_level' do
let_it_be(:user) { create(:user) }
@@ -4517,27 +4449,6 @@ describe User, :do_not_mock_admin_mode do
end
end
- describe '#organization' do
- using RSpec::Parameterized::TableSyntax
-
- let(:user) { build(:user, organization: 'ACME') }
-
- subject { user.organization }
-
- where(:gitlab_employee?, :expected_result) do
- true | 'GitLab'
- false | 'ACME'
- end
-
- with_them do
- before do
- allow(user).to receive(:gitlab_employee?).and_return(gitlab_employee?)
- end
-
- it { is_expected.to eql(expected_result) }
- end
- end
-
context 'when after_commit :update_highest_role' do
describe 'create user' do
subject { create(:user) }
@@ -4563,7 +4474,7 @@ describe User, :do_not_mock_admin_mode do
where(:attributes) do
[
{ state: 'blocked' },
- { ghost: true },
+ { user_type: :ghost },
{ user_type: :alert_bot }
]
end
@@ -4606,7 +4517,7 @@ describe User, :do_not_mock_admin_mode do
context 'when user is a ghost user' do
before do
- user.update(ghost: true)
+ user.update(user_type: :ghost)
end
it { is_expected.to be false }
@@ -4645,7 +4556,7 @@ describe User, :do_not_mock_admin_mode do
context 'when user is an internal user' do
before do
- user.update(ghost: true)
+ user.update(user_type: :ghost)
end
it { is_expected.to be User::LOGIN_FORBIDDEN }
@@ -4685,4 +4596,20 @@ describe User, :do_not_mock_admin_mode do
it_behaves_like 'does not require password to be present'
end
end
+
+ describe '#migration_bot' do
+ it 'creates the user if it does not exist' do
+ expect do
+ described_class.migration_bot
+ end.to change { User.where(user_type: :migration_bot).count }.by(1)
+ end
+
+ it 'does not create a new user if it already exists' do
+ described_class.migration_bot
+
+ expect do
+ described_class.migration_bot
+ end.not_to change { User.count }
+ end
+ end
end
diff --git a/spec/models/user_type_enums_spec.rb b/spec/models/user_type_enums_spec.rb
deleted file mode 100644
index 4f56e6ea96e..00000000000
--- a/spec/models/user_type_enums_spec.rb
+++ /dev/null
@@ -1,13 +0,0 @@
-# frozen_string_literal: true
-
-require 'spec_helper'
-
-describe UserTypeEnums do
- it '.types' do
- expect(described_class.types.keys).to include('alert_bot', 'project_bot', 'human', 'ghost')
- end
-
- it '.bots' do
- expect(described_class.bots.keys).to include('alert_bot', 'project_bot')
- end
-end
diff --git a/spec/models/wiki_page/meta_spec.rb b/spec/models/wiki_page/meta_spec.rb
index f9bfc31ba64..0255dd802cf 100644
--- a/spec/models/wiki_page/meta_spec.rb
+++ b/spec/models/wiki_page/meta_spec.rb
@@ -3,7 +3,7 @@
require 'spec_helper'
describe WikiPage::Meta do
- let_it_be(:project) { create(:project) }
+ let_it_be(:project) { create(:project, :wiki_repo) }
let_it_be(:other_project) { create(:project) }
describe 'Associations' do
@@ -169,8 +169,11 @@ describe WikiPage::Meta do
described_class.find_or_create(last_known_slug, wiki_page)
end
- def create_previous_version(title = old_title, slug = last_known_slug)
- create(:wiki_page_meta, title: title, project: project, canonical_slug: slug)
+ def create_previous_version(title: old_title, slug: last_known_slug, date: wiki_page.version.commit.committed_date)
+ create(:wiki_page_meta,
+ title: title, project: project,
+ created_at: date, updated_at: date,
+ canonical_slug: slug)
end
def create_context
@@ -198,6 +201,8 @@ describe WikiPage::Meta do
title: wiki_page.title,
project: wiki_page.wiki.project
)
+ expect(meta.updated_at).to eq(wiki_page.version.commit.committed_date)
+ expect(meta.created_at).not_to be_after(meta.updated_at)
expect(meta.slugs.where(slug: last_known_slug)).to exist
expect(meta.slugs.canonical.where(slug: wiki_page.slug)).to exist
end
@@ -209,22 +214,32 @@ describe WikiPage::Meta do
end
end
- context 'the slug is too long' do
- let(:last_known_slug) { FFaker::Lorem.characters(2050) }
+ context 'there are problems' do
+ context 'the slug is too long' do
+ let(:last_known_slug) { FFaker::Lorem.characters(2050) }
- it 'raises an error' do
- expect { find_record }.to raise_error ActiveRecord::ValueTooLong
+ it 'raises an error' do
+ expect { find_record }.to raise_error ActiveRecord::ValueTooLong
+ end
end
- end
- context 'a conflicting record exists' do
- before do
- create(:wiki_page_meta, project: project, canonical_slug: last_known_slug)
- create(:wiki_page_meta, project: project, canonical_slug: current_slug)
+ context 'a conflicting record exists' do
+ before do
+ create(:wiki_page_meta, project: project, canonical_slug: last_known_slug)
+ create(:wiki_page_meta, project: project, canonical_slug: current_slug)
+ end
+
+ it 'raises an error' do
+ expect { find_record }.to raise_error(ActiveRecord::RecordInvalid)
+ end
end
- it 'raises an error' do
- expect { find_record }.to raise_error(ActiveRecord::RecordInvalid)
+ context 'the wiki page is not valid' do
+ let(:wiki_page) { build(:wiki_page, project: project, title: nil) }
+
+ it 'raises an error' do
+ expect { find_record }.to raise_error(described_class::WikiPageInvalid)
+ end
end
end
@@ -258,6 +273,17 @@ describe WikiPage::Meta do
end
end
+ context 'the commit happened a day ago' do
+ before do
+ allow(wiki_page.version.commit).to receive(:committed_date).and_return(1.day.ago)
+ end
+
+ include_examples 'metadata examples' do
+ # Identical to the base case.
+ let(:query_limit) { 5 }
+ end
+ end
+
context 'the last_known_slug is the same as the current slug, as on creation' do
let(:last_known_slug) { current_slug }
@@ -292,6 +318,33 @@ describe WikiPage::Meta do
end
end
+ context 'a record exists in the DB, but we need to update timestamps' do
+ let(:last_known_slug) { current_slug }
+ let(:old_title) { title }
+
+ before do
+ create_previous_version(date: 1.week.ago)
+ end
+
+ include_examples 'metadata examples' do
+ # We need the query, and the update
+ # SAVEPOINT active_record_2
+ #
+ # SELECT * FROM wiki_page_meta
+ # INNER JOIN wiki_page_slugs
+ # ON wiki_page_slugs.wiki_page_meta_id = wiki_page_meta.id
+ # WHERE wiki_page_meta.project_id = ?
+ # AND wiki_page_slugs.canonical = TRUE
+ # AND wiki_page_slugs.slug = ?
+ # LIMIT 2
+ #
+ # UPDATE wiki_page_meta SET updated_at = ?date WHERE id = ?id
+ #
+ # RELEASE SAVEPOINT active_record_2
+ let(:query_limit) { 4 }
+ end
+ end
+
context 'we need to update the slug, but not the title' do
let(:old_title) { title }
@@ -359,14 +412,14 @@ describe WikiPage::Meta do
end
context 'we want to change the slug back to a previous version' do
- let(:slug_1) { 'foo' }
- let(:slug_2) { 'bar' }
+ let(:slug_1) { generate(:sluggified_title) }
+ let(:slug_2) { generate(:sluggified_title) }
let(:wiki_page) { create(:wiki_page, title: slug_1, project: project) }
let(:last_known_slug) { slug_2 }
before do
- meta = create_previous_version(title, slug_1)
+ meta = create_previous_version(title: title, slug: slug_1)
meta.canonical_slug = slug_2
end
diff --git a/spec/models/wiki_page_spec.rb b/spec/models/wiki_page_spec.rb
index 718b386b3fd..201dc85daf8 100644
--- a/spec/models/wiki_page_spec.rb
+++ b/spec/models/wiki_page_spec.rb
@@ -3,20 +3,11 @@
require "spec_helper"
describe WikiPage do
- let(:project) { create(:project, :wiki_repo) }
- let(:user) { project.owner }
- let(:wiki) { ProjectWiki.new(project, user) }
-
- let(:new_page) do
- described_class.new(wiki).tap do |page|
- page.attributes = { title: 'test page', content: 'test content' }
- end
- end
-
- let(:existing_page) do
- create_page('test page', 'test content')
- wiki.find_page('test page')
- end
+ let_it_be(:user) { create(:user) }
+ let(:container) { create(:project, :wiki_repo) }
+ let(:wiki) { Wiki.for_container(container, user) }
+ let(:new_page) { build(:wiki_page, wiki: wiki, title: 'test page', content: 'test content') }
+ let(:existing_page) { create(:wiki_page, wiki: wiki, title: 'test page', content: 'test content', message: 'test commit') }
subject { new_page }
@@ -24,11 +15,8 @@ describe WikiPage do
stub_feature_flags(Gitlab::WikiPages::FrontMatterParser::FEATURE_FLAG => false)
end
- def enable_front_matter_for_project
- stub_feature_flags(Gitlab::WikiPages::FrontMatterParser::FEATURE_FLAG => {
- thing: project,
- enabled: true
- })
+ def enable_front_matter_for(thing)
+ stub_feature_flags(Gitlab::WikiPages::FrontMatterParser::FEATURE_FLAG => thing)
end
describe '.group_by_directory' do
@@ -41,13 +29,13 @@ describe WikiPage do
context 'when there are pages' do
before do
- create_page('dir_1/dir_1_1/page_3', 'content')
- create_page('page_1', 'content')
- create_page('dir_1/page_2', 'content')
- create_page('dir_2', 'page with dir name')
- create_page('dir_2/page_5', 'content')
- create_page('page_6', 'content')
- create_page('dir_2/page_4', 'content')
+ wiki.create_page('dir_1/dir_1_1/page_3', 'content')
+ wiki.create_page('page_1', 'content')
+ wiki.create_page('dir_1/page_2', 'content')
+ wiki.create_page('dir_2', 'page with dir name')
+ wiki.create_page('dir_2/page_5', 'content')
+ wiki.create_page('page_6', 'content')
+ wiki.create_page('dir_2/page_4', 'content')
end
let(:page_1) { wiki.find_page('page_1') }
@@ -114,7 +102,8 @@ describe WikiPage do
describe '#front_matter' do
let_it_be(:project) { create(:project) }
- let(:wiki_page) { create(:wiki_page, project: project, content: content) }
+ let(:container) { project }
+ let(:wiki_page) { create(:wiki_page, container: container, content: content) }
shared_examples 'a page without front-matter' do
it { expect(wiki_page).to have_attributes(front_matter: {}, content: content) }
@@ -153,9 +142,9 @@ describe WikiPage do
it_behaves_like 'a page without front-matter'
- context 'but enabled for the project' do
+ context 'but enabled for the container' do
before do
- enable_front_matter_for_project
+ enable_front_matter_for(container)
end
it_behaves_like 'a page with front-matter'
@@ -344,7 +333,7 @@ describe WikiPage do
context 'with an existing page title exceeding the limit' do
subject do
title = 'a' * (max_title + 1)
- create_page(title, 'content')
+ wiki.create_page(title, 'content')
wiki.find_page(title)
end
@@ -388,6 +377,20 @@ describe WikiPage do
expect(wiki.find_page("Index").message).to eq 'Custom Commit Message'
end
+
+ it 'if the title is preceded by a / it is removed' do
+ subject.create(attributes.merge(title: '/New Page'))
+
+ expect(wiki.find_page('New Page')).not_to be_nil
+ end
+ end
+
+ context "with invalid attributes" do
+ it 'does not create the page' do
+ subject.create(title: '')
+
+ expect(wiki.find_page('New Page')).to be_nil
+ end
end
end
@@ -410,14 +413,11 @@ describe WikiPage do
end
end
- describe "#update" do
- subject do
- create_page(title, "content")
- wiki.find_page(title)
- end
+ describe '#update' do
+ subject { create(:wiki_page, wiki: wiki, title: title) }
- it "updates the content of the page" do
- subject.update(content: "new content")
+ it 'updates the content of the page' do
+ subject.update(content: 'new content')
page = wiki.find_page(title)
expect([subject.content, page.content]).to all(eq('new content'))
@@ -429,24 +429,6 @@ describe WikiPage do
end
end
- describe '#create' do
- context 'with valid attributes' do
- it 'raises an error if a page with the same path already exists' do
- create_page('New Page', 'content')
- create_page('foo/bar', 'content')
-
- expect { create_page('New Page', 'other content') }.to raise_error Gitlab::Git::Wiki::DuplicatePageError
- expect { create_page('foo/bar', 'other content') }.to raise_error Gitlab::Git::Wiki::DuplicatePageError
- end
-
- it 'if the title is preceded by a / it is removed' do
- create_page('/New Page', 'content')
-
- expect(wiki.find_page('New Page')).not_to be_nil
- end
- end
- end
-
describe "#update" do
subject { existing_page }
@@ -514,9 +496,9 @@ describe WikiPage do
expect([subject, page]).to all(have_attributes(front_matter: be_empty, content: content))
end
- context 'but it is enabled for the project' do
+ context 'but it is enabled for the container' do
before do
- enable_front_matter_for_project
+ enable_front_matter_for(container)
end
it_behaves_like 'able to update front-matter'
@@ -556,7 +538,7 @@ describe WikiPage do
context 'when renaming a page' do
it 'raises an error if the page already exists' do
- create_page('Existing Page', 'content')
+ wiki.create_page('Existing Page', 'content')
expect { subject.update(title: 'Existing Page', content: 'new_content') }.to raise_error(WikiPage::PageRenameError)
expect(subject.title).to eq 'test page'
@@ -578,7 +560,7 @@ describe WikiPage do
context 'when moving a page' do
it 'raises an error if the page already exists' do
- create_page('foo/Existing Page', 'content')
+ wiki.create_page('foo/Existing Page', 'content')
expect { subject.update(title: 'foo/Existing Page', content: 'new_content') }.to raise_error(WikiPage::PageRenameError)
expect(subject.title).to eq 'test page'
@@ -598,10 +580,7 @@ describe WikiPage do
end
context 'in subdir' do
- subject do
- create_page('foo/Existing Page', 'content')
- wiki.find_page('foo/Existing Page')
- end
+ subject { create(:wiki_page, wiki: wiki, title: 'foo/Existing Page') }
it 'moves the page to the root folder if the title is preceded by /' do
expect(subject.slug).to eq 'foo/Existing-Page'
@@ -639,7 +618,7 @@ describe WikiPage do
end
end
- describe "#destroy" do
+ describe "#delete" do
subject { existing_page }
it "deletes the page" do
@@ -671,10 +650,7 @@ describe WikiPage do
using RSpec::Parameterized::TableSyntax
let(:untitled_page) { described_class.new(wiki) }
- let(:directory_page) do
- create_page('parent directory/child page', 'test content')
- wiki.find_page('parent directory/child page')
- end
+ let(:directory_page) { create(:wiki_page, title: 'parent directory/child page') }
where(:page, :title, :changed) do
:untitled_page | nil | false
@@ -737,10 +713,7 @@ describe WikiPage do
end
context 'when the page is inside an actual directory' do
- subject do
- create_page('dir_1/dir_1_1/file', 'content')
- wiki.find_page('dir_1/dir_1_1/file')
- end
+ subject { create(:wiki_page, title: 'dir_1/dir_1_1/file') }
it 'returns the full directory hierarchy' do
expect(subject.directory).to eq('dir_1/dir_1_1')
@@ -787,6 +760,16 @@ describe WikiPage do
end
end
+ describe '#persisted?' do
+ it 'returns true for a persisted page' do
+ expect(existing_page).to be_persisted
+ end
+
+ it 'returns false for an unpersisted page' do
+ expect(new_page).not_to be_persisted
+ end
+ end
+
describe '#to_partial_path' do
it 'returns the relative path to the partial to be used' do
expect(subject.to_partial_path).to eq('projects/wikis/wiki_page')
@@ -812,23 +795,23 @@ describe WikiPage do
other_page = create(:wiki_page)
expect(subject.slug).not_to eq(other_page.slug)
- expect(subject.project).not_to eq(other_page.project)
+ expect(subject.container).not_to eq(other_page.container)
expect(subject).not_to eq(other_page)
end
- it 'returns false for page with different slug on same project' do
- other_page = create(:wiki_page, project: subject.project)
+ it 'returns false for page with different slug on same container' do
+ other_page = create(:wiki_page, container: subject.container)
expect(subject.slug).not_to eq(other_page.slug)
- expect(subject.project).to eq(other_page.project)
+ expect(subject.container).to eq(other_page.container)
expect(subject).not_to eq(other_page)
end
- it 'returns false for page with the same slug on a different project' do
+ it 'returns false for page with the same slug on a different container' do
other_page = create(:wiki_page, title: existing_page.slug)
expect(subject.slug).to eq(other_page.slug)
- expect(subject.project).not_to eq(other_page.project)
+ expect(subject.container).not_to eq(other_page.container)
expect(subject).not_to eq(other_page)
end
end
@@ -858,19 +841,21 @@ describe WikiPage do
end
end
- private
-
- def remove_temp_repo(path)
- FileUtils.rm_rf path
- end
+ describe '#version_commit_timestamp' do
+ context 'for a new page' do
+ it 'returns nil' do
+ expect(new_page.version_commit_timestamp).to be_nil
+ end
+ end
- def commit_details
- Gitlab::Git::Wiki::CommitDetails.new(user.id, user.username, user.name, user.email, "test commit")
+ context 'for page that exists' do
+ it 'returns the timestamp of the commit' do
+ expect(existing_page.version_commit_timestamp).to eq(existing_page.version.commit.committed_date)
+ end
+ end
end
- def create_page(name, content)
- wiki.wiki.write_page(name, :markdown, content, commit_details)
- end
+ private
def get_slugs(page_or_dir)
if page_or_dir.is_a? WikiPage
diff --git a/spec/models/x509_commit_signature_spec.rb b/spec/models/x509_commit_signature_spec.rb
index a2f72228a86..2efb77c96ad 100644
--- a/spec/models/x509_commit_signature_spec.rb
+++ b/spec/models/x509_commit_signature_spec.rb
@@ -9,6 +9,15 @@ RSpec.describe X509CommitSignature do
let(:x509_certificate) { create(:x509_certificate) }
let(:x509_signature) { create(:x509_commit_signature, commit_sha: commit_sha) }
+ let(:attributes) do
+ {
+ commit_sha: commit_sha,
+ project: project,
+ x509_certificate_id: x509_certificate.id,
+ verification_status: "verified"
+ }
+ end
+
it_behaves_like 'having unique enum values'
describe 'validation' do
@@ -23,15 +32,6 @@ RSpec.describe X509CommitSignature do
end
describe '.safe_create!' do
- let(:attributes) do
- {
- commit_sha: commit_sha,
- project: project,
- x509_certificate_id: x509_certificate.id,
- verification_status: "verified"
- }
- end
-
it 'finds a signature by commit sha if it existed' do
x509_signature
@@ -50,4 +50,18 @@ RSpec.describe X509CommitSignature do
expect(signature.x509_certificate_id).to eq(x509_certificate.id)
end
end
+
+ describe '#user' do
+ context 'if email is assigned to a user' do
+ let!(:user) { create(:user, email: X509Helpers::User1.certificate_email) }
+
+ it 'returns user' do
+ expect(described_class.safe_create!(attributes).user).to eq(user)
+ end
+ end
+
+ it 'if email is not assigned to a user, return nil' do
+ expect(described_class.safe_create!(attributes).user).to be_nil
+ end
+ end
end
diff --git a/spec/policies/alert_management/alert_policy_spec.rb b/spec/policies/alert_management/alert_policy_spec.rb
new file mode 100644
index 00000000000..0d7624a0142
--- /dev/null
+++ b/spec/policies/alert_management/alert_policy_spec.rb
@@ -0,0 +1,25 @@
+# frozen_string_literal: true
+
+require 'spec_helper'
+
+describe AlertManagement::AlertPolicy, :models do
+ let(:alert) { create(:alert_management_alert) }
+ let(:project) { alert.project }
+ let(:user) { create(:user) }
+
+ subject(:policy) { described_class.new(user, alert) }
+
+ describe 'rules' do
+ it { is_expected.to be_disallowed :read_alert_management_alert }
+ it { is_expected.to be_disallowed :update_alert_management_alert }
+
+ context 'when developer' do
+ before do
+ project.add_developer(user)
+ end
+
+ it { is_expected.to be_allowed :read_alert_management_alert }
+ it { is_expected.to be_allowed :update_alert_management_alert }
+ end
+ end
+end
diff --git a/spec/policies/base_policy_spec.rb b/spec/policies/base_policy_spec.rb
index e15221492c3..67f7452528a 100644
--- a/spec/policies/base_policy_spec.rb
+++ b/spec/policies/base_policy_spec.rb
@@ -2,7 +2,7 @@
require 'spec_helper'
-describe BasePolicy, :do_not_mock_admin_mode do
+describe BasePolicy do
include ExternalAuthorizationServiceHelpers
include AdminModeHelper
diff --git a/spec/policies/blob_policy_spec.rb b/spec/policies/blob_policy_spec.rb
index 20c8a55f437..e48dd751a8f 100644
--- a/spec/policies/blob_policy_spec.rb
+++ b/spec/policies/blob_policy_spec.rb
@@ -2,7 +2,7 @@
require 'spec_helper'
-describe BlobPolicy do
+describe BlobPolicy, :enable_admin_mode do
include_context 'ProjectPolicyTable context'
include ProjectHelpers
using RSpec::Parameterized::TableSyntax
diff --git a/spec/policies/ci/build_policy_spec.rb b/spec/policies/ci/build_policy_spec.rb
index 333f4e560cf..f29ed26f2aa 100644
--- a/spec/policies/ci/build_policy_spec.rb
+++ b/spec/policies/ci/build_policy_spec.rb
@@ -176,15 +176,21 @@ describe Ci::BuildPolicy do
end
context 'when developers can push to the branch' do
- before do
- create(:protected_branch, :developers_can_push,
- name: build.ref, project: project)
- end
-
context 'when the build was created by the developer' do
let(:owner) { user }
- it { expect(policy).to be_allowed :erase_build }
+ context 'when the build was created for a protected ref' do
+ before do
+ create(:protected_branch, :developers_can_push,
+ name: build.ref, project: project)
+ end
+
+ it { expect(policy).to be_disallowed :erase_build }
+ end
+
+ context 'when the build was created for an unprotected ref' do
+ it { expect(policy).to be_allowed :erase_build }
+ end
end
context 'when the build was created by the other' do
diff --git a/spec/policies/clusters/cluster_policy_spec.rb b/spec/policies/clusters/cluster_policy_spec.rb
index 55c3351a171..26cfc19862a 100644
--- a/spec/policies/clusters/cluster_policy_spec.rb
+++ b/spec/policies/clusters/cluster_policy_spec.rb
@@ -80,8 +80,15 @@ describe Clusters::ClusterPolicy, :models do
context 'when admin' do
let(:user) { create(:admin) }
- it { expect(policy).to be_allowed :update_cluster }
- it { expect(policy).to be_allowed :admin_cluster }
+ context 'when admin mode is enabled', :enable_admin_mode do
+ it { expect(policy).to be_allowed :update_cluster }
+ it { expect(policy).to be_allowed :admin_cluster }
+ end
+
+ context 'when admin mode is disabled' do
+ it { expect(policy).to be_disallowed :update_cluster }
+ it { expect(policy).to be_disallowed :admin_cluster }
+ end
end
end
end
diff --git a/spec/policies/clusters/instance_policy_spec.rb b/spec/policies/clusters/instance_policy_spec.rb
index 2373fef8aa6..dfe480d7fa4 100644
--- a/spec/policies/clusters/instance_policy_spec.rb
+++ b/spec/policies/clusters/instance_policy_spec.rb
@@ -18,11 +18,21 @@ describe Clusters::InstancePolicy do
context 'when admin' do
let(:user) { create(:admin) }
- it { expect(policy).to be_allowed :read_cluster }
- it { expect(policy).to be_allowed :add_cluster }
- it { expect(policy).to be_allowed :create_cluster }
- it { expect(policy).to be_allowed :update_cluster }
- it { expect(policy).to be_allowed :admin_cluster }
+ context 'when admin mode is enabled', :enable_admin_mode do
+ it { expect(policy).to be_allowed :read_cluster }
+ it { expect(policy).to be_allowed :add_cluster }
+ it { expect(policy).to be_allowed :create_cluster }
+ it { expect(policy).to be_allowed :update_cluster }
+ it { expect(policy).to be_allowed :admin_cluster }
+ end
+
+ context 'when admin mode is disabled' do
+ it { expect(policy).to be_disallowed :read_cluster }
+ it { expect(policy).to be_disallowed :add_cluster }
+ it { expect(policy).to be_disallowed :create_cluster }
+ it { expect(policy).to be_disallowed :update_cluster }
+ it { expect(policy).to be_disallowed :admin_cluster }
+ end
end
end
end
diff --git a/spec/policies/deploy_key_policy_spec.rb b/spec/policies/deploy_key_policy_spec.rb
index aca93d8fe85..545647e2c67 100644
--- a/spec/policies/deploy_key_policy_spec.rb
+++ b/spec/policies/deploy_key_policy_spec.rb
@@ -42,16 +42,28 @@ describe DeployKeyPolicy do
context 'when an admin user' do
let(:current_user) { create(:user, :admin) }
- context ' tries to update private deploy key' do
+ context 'tries to update private deploy key' do
let(:deploy_key) { create(:deploy_key, public: false) }
- it { is_expected.to be_allowed(:update_deploy_key) }
+ context 'when admin mode enabled', :enable_admin_mode do
+ it { is_expected.to be_allowed(:update_deploy_key) }
+ end
+
+ context 'when admin mode disabled' do
+ it { is_expected.to be_disallowed(:update_deploy_key) }
+ end
end
context 'when an admin user tries to update public deploy key' do
let(:deploy_key) { create(:another_deploy_key, public: true) }
- it { is_expected.to be_allowed(:update_deploy_key) }
+ context 'when admin mode enabled', :enable_admin_mode do
+ it { is_expected.to be_allowed(:update_deploy_key) }
+ end
+
+ context 'when admin mode disabled' do
+ it { is_expected.to be_disallowed(:update_deploy_key) }
+ end
end
end
end
diff --git a/spec/policies/design_management/design_policy_spec.rb b/spec/policies/design_management/design_policy_spec.rb
new file mode 100644
index 00000000000..a566aecc4b7
--- /dev/null
+++ b/spec/policies/design_management/design_policy_spec.rb
@@ -0,0 +1,181 @@
+# frozen_string_literal: true
+require 'spec_helper'
+
+describe DesignManagement::DesignPolicy do
+ include DesignManagementTestHelpers
+
+ include_context 'ProjectPolicy context'
+
+ let(:guest_design_abilities) { %i[read_design] }
+ let(:developer_design_abilities) do
+ %i[create_design destroy_design]
+ end
+ let(:design_abilities) { guest_design_abilities + developer_design_abilities }
+
+ let(:issue) { create(:issue, project: project) }
+ let(:design) { create(:design, issue: issue) }
+
+ subject(:design_policy) { described_class.new(current_user, design) }
+
+ shared_examples_for "design abilities not available" do
+ context "for owners" do
+ let(:current_user) { owner }
+
+ it { is_expected.to be_disallowed(*design_abilities) }
+ end
+
+ context "for admins" do
+ let(:current_user) { admin }
+
+ it { is_expected.to be_disallowed(*design_abilities) }
+ end
+
+ context "for maintainers" do
+ let(:current_user) { maintainer }
+
+ it { is_expected.to be_disallowed(*design_abilities) }
+ end
+
+ context "for developers" do
+ let(:current_user) { developer }
+
+ it { is_expected.to be_disallowed(*design_abilities) }
+ end
+
+ context "for reporters" do
+ let(:current_user) { reporter }
+
+ it { is_expected.to be_disallowed(*design_abilities) }
+ end
+
+ context "for guests" do
+ let(:current_user) { guest }
+
+ it { is_expected.to be_disallowed(*design_abilities) }
+ end
+
+ context "for anonymous users" do
+ let(:current_user) { nil }
+
+ it { is_expected.to be_disallowed(*design_abilities) }
+ end
+ end
+
+ shared_examples_for "design abilities available for members" do
+ context "for owners" do
+ let(:current_user) { owner }
+
+ it { is_expected.to be_allowed(*design_abilities) }
+ end
+
+ context "for admins" do
+ let(:current_user) { admin }
+
+ context 'when admin mode enabled', :enable_admin_mode do
+ it { is_expected.to be_allowed(*design_abilities) }
+ end
+
+ context 'when admin mode disabled' do
+ it { is_expected.to be_allowed(*guest_design_abilities) }
+ it { is_expected.to be_disallowed(*developer_design_abilities) }
+ end
+ end
+
+ context "for maintainers" do
+ let(:current_user) { maintainer }
+
+ it { is_expected.to be_allowed(*design_abilities) }
+ end
+
+ context "for developers" do
+ let(:current_user) { developer }
+
+ it { is_expected.to be_allowed(*design_abilities) }
+ end
+
+ context "for reporters" do
+ let(:current_user) { reporter }
+
+ it { is_expected.to be_allowed(*guest_design_abilities) }
+ it { is_expected.to be_disallowed(*developer_design_abilities) }
+ end
+ end
+
+ shared_examples_for "read-only design abilities" do
+ it { is_expected.to be_allowed(:read_design) }
+ it { is_expected.to be_disallowed(:create_design, :destroy_design) }
+ end
+
+ context "when DesignManagement is not enabled" do
+ before do
+ enable_design_management(false)
+ end
+
+ it_behaves_like "design abilities not available"
+ end
+
+ context "when the feature is available" do
+ before do
+ enable_design_management
+ end
+
+ it_behaves_like "design abilities available for members"
+
+ context "for guests in private projects" do
+ let(:project) { create(:project, :private) }
+ let(:current_user) { guest }
+
+ it { is_expected.to be_allowed(*guest_design_abilities) }
+ it { is_expected.to be_disallowed(*developer_design_abilities) }
+ end
+
+ context "for anonymous users in public projects" do
+ let(:current_user) { nil }
+
+ it { is_expected.to be_allowed(*guest_design_abilities) }
+ it { is_expected.to be_disallowed(*developer_design_abilities) }
+ end
+
+ context "when the issue is confidential" do
+ let(:issue) { create(:issue, :confidential, project: project) }
+
+ it_behaves_like "design abilities available for members"
+
+ context "for guests" do
+ let(:current_user) { guest }
+
+ it { is_expected.to be_disallowed(*design_abilities) }
+ end
+
+ context "for anonymous users" do
+ let(:current_user) { nil }
+
+ it { is_expected.to be_disallowed(*design_abilities) }
+ end
+ end
+
+ context "when the issue is locked" do
+ let(:current_user) { owner }
+ let(:issue) { create(:issue, :locked, project: project) }
+
+ it_behaves_like "read-only design abilities"
+ end
+
+ context "when the issue has moved" do
+ let(:current_user) { owner }
+ let(:issue) { create(:issue, project: project, moved_to: create(:issue)) }
+
+ it_behaves_like "read-only design abilities"
+ end
+
+ context "when the project is archived" do
+ let(:current_user) { owner }
+
+ before do
+ project.update!(archived: true)
+ end
+
+ it_behaves_like "read-only design abilities"
+ end
+ end
+end
diff --git a/spec/policies/environment_policy_spec.rb b/spec/policies/environment_policy_spec.rb
index a098b52023d..75fca464ec8 100644
--- a/spec/policies/environment_policy_spec.rb
+++ b/spec/policies/environment_policy_spec.rb
@@ -37,7 +37,13 @@ describe EnvironmentPolicy do
context 'when an admin user' do
let(:user) { create(:user, :admin) }
- it { expect(policy).to be_allowed :stop_environment }
+ context 'when admin mode is enabled', :enable_admin_mode do
+ it { expect(policy).to be_allowed :stop_environment }
+ end
+
+ context 'when admin mode is disabled' do
+ it { expect(policy).to be_disallowed :stop_environment }
+ end
end
context 'with protected branch' do
@@ -54,7 +60,13 @@ describe EnvironmentPolicy do
context 'when an admin user' do
let(:user) { create(:user, :admin) }
- it { expect(policy).to be_allowed :stop_environment }
+ context 'when admin mode is enabled', :enable_admin_mode do
+ it { expect(policy).to be_allowed :stop_environment }
+ end
+
+ context 'when admin mode is disabled' do
+ it { expect(policy).to be_disallowed :stop_environment }
+ end
end
end
end
@@ -83,7 +95,13 @@ describe EnvironmentPolicy do
context 'when an admin user' do
let(:user) { create(:user, :admin) }
- it { expect(policy).to be_allowed :stop_environment }
+ context 'when admin mode is enabled', :enable_admin_mode do
+ it { expect(policy).to be_allowed :stop_environment }
+ end
+
+ context 'when admin mode is disabled' do
+ it { expect(policy).to be_disallowed :stop_environment }
+ end
end
end
@@ -126,7 +144,13 @@ describe EnvironmentPolicy do
environment.stop!
end
- it { expect(policy).to be_allowed :destroy_environment }
+ context 'when admin mode is enabled', :enable_admin_mode do
+ it { expect(policy).to be_allowed :destroy_environment }
+ end
+
+ context 'when admin mode is disabled' do
+ it { expect(policy).to be_disallowed :destroy_environment }
+ end
end
end
end
diff --git a/spec/policies/global_policy_spec.rb b/spec/policies/global_policy_spec.rb
index 5e77b64a408..e8ba4eed4ec 100644
--- a/spec/policies/global_policy_spec.rb
+++ b/spec/policies/global_policy_spec.rb
@@ -6,6 +6,7 @@ describe GlobalPolicy do
include TermsHelper
let_it_be(:project_bot) { create(:user, :project_bot) }
+ let_it_be(:migration_bot) { create(:user, :migration_bot) }
let(:current_user) { create(:user) }
let(:user) { create(:user) }
@@ -80,6 +81,34 @@ describe GlobalPolicy do
end
end
+ describe 'create group' do
+ context 'when user has the ability to create group' do
+ let(:current_user) { create(:user, can_create_group: true) }
+
+ it { is_expected.to be_allowed(:create_group) }
+ end
+
+ context 'when user does not have the ability to create group' do
+ let(:current_user) { create(:user, can_create_group: false) }
+
+ it { is_expected.not_to be_allowed(:create_group) }
+ end
+ end
+
+ describe 'create group with default branch protection' do
+ context 'when user has the ability to create group' do
+ let(:current_user) { create(:user, can_create_group: true) }
+
+ it { is_expected.to be_allowed(:create_group_with_default_branch_protection) }
+ end
+
+ context 'when user does not have the ability to create group' do
+ let(:current_user) { create(:user, can_create_group: false) }
+
+ it { is_expected.not_to be_allowed(:create_group_with_default_branch_protection) }
+ end
+ end
+
describe 'custom attributes' do
context 'regular user' do
it { is_expected.not_to be_allowed(:read_custom_attribute) }
@@ -89,8 +118,15 @@ describe GlobalPolicy do
context 'admin' do
let(:current_user) { create(:user, :admin) }
- it { is_expected.to be_allowed(:read_custom_attribute) }
- it { is_expected.to be_allowed(:update_custom_attribute) }
+ context 'when admin mode is enabled', :enable_admin_mode do
+ it { is_expected.to be_allowed(:read_custom_attribute) }
+ it { is_expected.to be_allowed(:update_custom_attribute) }
+ end
+
+ context 'when admin mode is disabled' do
+ it { is_expected.to be_disallowed(:read_custom_attribute) }
+ it { is_expected.to be_disallowed(:update_custom_attribute) }
+ end
end
end
@@ -127,6 +163,12 @@ describe GlobalPolicy do
it { is_expected.to be_allowed(:access_api) }
end
+ context 'migration bot' do
+ let(:current_user) { migration_bot }
+
+ it { is_expected.not_to be_allowed(:access_api) }
+ end
+
context 'when terms are enforced' do
before do
enforce_terms
@@ -216,6 +258,12 @@ describe GlobalPolicy do
it { is_expected.not_to be_allowed(:receive_notifications) }
end
+
+ context 'migration bot' do
+ let(:current_user) { migration_bot }
+
+ it { is_expected.not_to be_allowed(:receive_notifications) }
+ end
end
describe 'git access' do
@@ -235,6 +283,12 @@ describe GlobalPolicy do
it { is_expected.to be_allowed(:access_git) }
end
+ context 'migration bot' do
+ let(:current_user) { migration_bot }
+
+ it { is_expected.to be_allowed(:access_git) }
+ end
+
describe 'deactivated user' do
before do
current_user.deactivate
@@ -321,7 +375,13 @@ describe GlobalPolicy do
stub_application_setting(instance_statistics_visibility_private: true)
end
- it { is_expected.to be_allowed(:read_instance_statistics) }
+ context 'when admin mode is enabled', :enable_admin_mode do
+ it { is_expected.to be_allowed(:read_instance_statistics) }
+ end
+
+ context 'when admin mode is disabled' do
+ it { is_expected.to be_disallowed(:read_instance_statistics) }
+ end
end
end
@@ -386,6 +446,12 @@ describe GlobalPolicy do
it { is_expected.to be_allowed(:use_slash_commands) }
end
+
+ context 'migration bot' do
+ let(:current_user) { migration_bot }
+
+ it { is_expected.not_to be_allowed(:use_slash_commands) }
+ end
end
describe 'create_snippet' do
@@ -412,5 +478,11 @@ describe GlobalPolicy do
it { is_expected.not_to be_allowed(:log_in) }
end
+
+ context 'migration bot' do
+ let(:current_user) { migration_bot }
+
+ it { is_expected.not_to be_allowed(:log_in) }
+ end
end
end
diff --git a/spec/policies/group_policy_spec.rb b/spec/policies/group_policy_spec.rb
index 13f1bcb389a..9faddfd00e5 100644
--- a/spec/policies/group_policy_spec.rb
+++ b/spec/policies/group_policy_spec.rb
@@ -644,7 +644,13 @@ describe GroupPolicy do
context 'admin' do
let(:current_user) { admin }
- it { expect_allowed(:update_max_artifacts_size) }
+ context 'when admin mode is enabled', :enable_admin_mode do
+ it { expect_allowed(:update_max_artifacts_size) }
+ end
+
+ context 'when admin mode is enabled' do
+ it { expect_disallowed(:update_max_artifacts_size) }
+ end
end
%w(guest reporter developer maintainer owner).each do |role|
@@ -655,26 +661,4 @@ describe GroupPolicy do
end
end
end
-
- it_behaves_like 'model with wiki policies' do
- let(:container) { create(:group) }
-
- def set_access_level(access_level)
- allow(container).to receive(:wiki_access_level).and_return(access_level)
- end
-
- before do
- stub_feature_flags(group_wiki: true)
- end
-
- context 'when the feature flag is disabled' do
- before do
- stub_feature_flags(group_wiki: false)
- end
-
- it 'does not include the wiki permissions' do
- expect_disallowed(*permissions)
- end
- end
- end
end
diff --git a/spec/policies/issue_policy_spec.rb b/spec/policies/issue_policy_spec.rb
index 242a002bc23..9d52079e4be 100644
--- a/spec/policies/issue_policy_spec.rb
+++ b/spec/policies/issue_policy_spec.rb
@@ -206,11 +206,25 @@ describe IssuePolicy do
it 'allows guests to comment' do
expect(permissions(guest, issue)).to be_allowed(:create_note)
end
- it 'allows admins to view' do
- expect(permissions(admin, issue)).to be_allowed(:read_issue)
+
+ context 'when admin mode is enabled', :enable_admin_mode do
+ it 'allows admins to view' do
+ expect(permissions(admin, issue)).to be_allowed(:read_issue)
+ end
+
+ it 'allows admins to comment' do
+ expect(permissions(admin, issue)).to be_allowed(:create_note)
+ end
end
- it 'allows admins to comment' do
- expect(permissions(admin, issue)).to be_allowed(:create_note)
+
+ context 'when admin mode is disabled' do
+ it 'forbids admins to view' do
+ expect(permissions(admin, issue)).to be_disallowed(:read_issue)
+ end
+
+ it 'forbids admins to comment' do
+ expect(permissions(admin, issue)).to be_disallowed(:create_note)
+ end
end
end
diff --git a/spec/policies/merge_request_policy_spec.rb b/spec/policies/merge_request_policy_spec.rb
index 287325e96df..31ced5db953 100644
--- a/spec/policies/merge_request_policy_spec.rb
+++ b/spec/policies/merge_request_policy_spec.rb
@@ -21,7 +21,7 @@ describe MergeRequestPolicy do
project.add_developer(developer)
end
- MR_PERMS = %i[create_merge_request_in
+ mr_perms = %i[create_merge_request_in
create_merge_request_from
read_merge_request
create_note].freeze
@@ -29,7 +29,7 @@ describe MergeRequestPolicy do
shared_examples_for 'a denied user' do
let(:perms) { permissions(subject, merge_request) }
- MR_PERMS.each do |thing|
+ mr_perms.each do |thing|
it "cannot #{thing}" do
expect(perms).to be_disallowed(thing)
end
@@ -39,7 +39,7 @@ describe MergeRequestPolicy do
shared_examples_for 'a user with access' do
let(:perms) { permissions(subject, merge_request) }
- MR_PERMS.each do |thing|
+ mr_perms.each do |thing|
it "can #{thing}" do
expect(perms).to be_allowed(thing)
end
diff --git a/spec/policies/namespace_policy_spec.rb b/spec/policies/namespace_policy_spec.rb
index c0a5119c550..01162dc0fc4 100644
--- a/spec/policies/namespace_policy_spec.rb
+++ b/spec/policies/namespace_policy_spec.rb
@@ -40,6 +40,12 @@ describe NamespacePolicy do
context 'admin' do
let(:current_user) { admin }
- it { is_expected.to be_allowed(*owner_permissions) }
+ context 'when admin mode is enabled', :enable_admin_mode do
+ it { is_expected.to be_allowed(*owner_permissions) }
+ end
+
+ context 'when admin mode is disabled' do
+ it { is_expected.to be_disallowed(*owner_permissions) }
+ end
end
end
diff --git a/spec/policies/note_policy_spec.rb b/spec/policies/note_policy_spec.rb
index e9dd5ee1c51..1e3bd0d9147 100644
--- a/spec/policies/note_policy_spec.rb
+++ b/spec/policies/note_policy_spec.rb
@@ -295,8 +295,16 @@ describe NotePolicy do
expect(permissions(maintainer, confidential_note)).to be_allowed(:read_note, :admin_note, :resolve_note, :award_emoji)
end
- it 'allows admins to read all notes and admin them' do
- expect(permissions(admin, confidential_note)).to be_allowed(:read_note, :admin_note, :resolve_note, :award_emoji)
+ context 'when admin mode is enabled', :enable_admin_mode do
+ it 'allows admins to read all notes and admin them' do
+ expect(permissions(admin, confidential_note)).to be_allowed(:read_note, :admin_note, :resolve_note, :award_emoji)
+ end
+ end
+
+ context 'when admin mode is disabled' do
+ it 'does not allow non members to read confidential notes and replies' do
+ expect(permissions(admin, confidential_note)).to be_disallowed(:read_note, :admin_note, :resolve_note, :award_emoji)
+ end
end
it 'allows noteable author to read and resolve all notes' do
diff --git a/spec/policies/personal_snippet_policy_spec.rb b/spec/policies/personal_snippet_policy_spec.rb
index a6b76620c29..5fc48717d86 100644
--- a/spec/policies/personal_snippet_policy_spec.rb
+++ b/spec/policies/personal_snippet_policy_spec.rb
@@ -19,8 +19,8 @@ describe PersonalSnippetPolicy do
described_class.new(user, snippet)
end
- shared_examples 'admin access' do
- context 'admin user' do
+ shared_examples 'admin access with admin mode' do
+ context 'admin user', :enable_admin_mode do
subject { permissions(admin_user) }
it do
@@ -68,7 +68,7 @@ describe PersonalSnippetPolicy do
end
end
- it_behaves_like 'admin access'
+ it_behaves_like 'admin access with admin mode'
end
context 'internal snippet' do
@@ -118,7 +118,7 @@ describe PersonalSnippetPolicy do
end
end
- it_behaves_like 'admin access'
+ it_behaves_like 'admin access with admin mode'
end
context 'private snippet' do
@@ -168,6 +168,6 @@ describe PersonalSnippetPolicy do
end
end
- it_behaves_like 'admin access'
+ it_behaves_like 'admin access with admin mode'
end
end
diff --git a/spec/policies/project_policy_spec.rb b/spec/policies/project_policy_spec.rb
index db643e3a31f..09d54eb9df6 100644
--- a/spec/policies/project_policy_spec.rb
+++ b/spec/policies/project_policy_spec.rb
@@ -29,6 +29,7 @@ describe ProjectPolicy do
admin_issue admin_label admin_list read_commit_status read_build
read_container_image read_pipeline read_environment read_deployment
read_merge_request download_wiki_code read_sentry_issue read_metrics_dashboard_annotation
+ metrics_dashboard
]
end
@@ -41,7 +42,7 @@ describe ProjectPolicy do
admin_tag admin_milestone admin_merge_request update_merge_request create_commit_status
update_commit_status create_build update_build create_pipeline
update_pipeline create_merge_request_from create_wiki push_code
- resolve_note create_container_image update_container_image destroy_container_image
+ resolve_note create_container_image update_container_image destroy_container_image daily_statistics
create_environment update_environment create_deployment update_deployment create_release update_release
create_metrics_dashboard_annotation delete_metrics_dashboard_annotation update_metrics_dashboard_annotation
]
@@ -53,7 +54,7 @@ describe ProjectPolicy do
admin_snippet admin_project_member admin_note admin_wiki admin_project
admin_commit_status admin_build admin_container_image
admin_pipeline admin_environment admin_deployment destroy_release add_cluster
- daily_statistics read_deploy_token create_deploy_token destroy_deploy_token
+ read_deploy_token create_deploy_token destroy_deploy_token
admin_terraform_state
]
end
@@ -123,6 +124,7 @@ describe ProjectPolicy do
it_behaves_like 'model with wiki policies' do
let(:container) { project }
+ let_it_be(:user) { owner }
def set_access_level(access_level)
project.project_feature.update_attribute(:wiki_access_level, access_level)
@@ -216,16 +218,41 @@ describe ProjectPolicy do
project.project_feature.update(builds_access_level: ProjectFeature::DISABLED)
end
- it 'disallows all permissions except pipeline when the feature is disabled' do
- builds_permissions = [
- :create_build, :read_build, :update_build, :admin_build, :destroy_build,
- :create_pipeline_schedule, :read_pipeline_schedule, :update_pipeline_schedule, :admin_pipeline_schedule, :destroy_pipeline_schedule,
- :create_environment, :read_environment, :update_environment, :admin_environment, :destroy_environment,
- :create_cluster, :read_cluster, :update_cluster, :admin_cluster, :destroy_cluster,
- :create_deployment, :read_deployment, :update_deployment, :admin_deployment, :destroy_deployment
- ]
+ context 'without metrics_dashboard_allowed' do
+ before do
+ project.project_feature.update(metrics_dashboard_access_level: ProjectFeature::DISABLED)
+ end
- expect_disallowed(*builds_permissions)
+ it 'disallows all permissions except pipeline when the feature is disabled' do
+ builds_permissions = [
+ :create_build, :read_build, :update_build, :admin_build, :destroy_build,
+ :create_pipeline_schedule, :read_pipeline_schedule, :update_pipeline_schedule, :admin_pipeline_schedule, :destroy_pipeline_schedule,
+ :create_environment, :read_environment, :update_environment, :admin_environment, :destroy_environment,
+ :create_cluster, :read_cluster, :update_cluster, :admin_cluster, :destroy_cluster,
+ :create_deployment, :read_deployment, :update_deployment, :admin_deployment, :destroy_deployment
+ ]
+
+ expect_disallowed(*builds_permissions)
+ end
+ end
+
+ context 'with metrics_dashboard_allowed' do
+ before do
+ project.project_feature.update(metrics_dashboard_access_level: ProjectFeature::ENABLED)
+ end
+
+ it 'disallows all permissions except pipeline and read_environment when the feature is disabled' do
+ builds_permissions = [
+ :create_build, :read_build, :update_build, :admin_build, :destroy_build,
+ :create_pipeline_schedule, :read_pipeline_schedule, :update_pipeline_schedule, :admin_pipeline_schedule, :destroy_pipeline_schedule,
+ :create_environment, :update_environment, :admin_environment, :destroy_environment,
+ :create_cluster, :read_cluster, :update_cluster, :admin_cluster, :destroy_cluster,
+ :create_deployment, :read_deployment, :update_deployment, :admin_deployment, :destroy_deployment
+ ]
+
+ expect_disallowed(*builds_permissions)
+ expect_allowed(:read_environment)
+ end
end
end
@@ -250,20 +277,49 @@ describe ProjectPolicy do
context 'repository feature' do
subject { described_class.new(owner, project) }
- it 'disallows all permissions when the feature is disabled' do
+ before do
project.project_feature.update(repository_access_level: ProjectFeature::DISABLED)
+ end
- repository_permissions = [
- :create_pipeline, :update_pipeline, :admin_pipeline, :destroy_pipeline,
- :create_build, :read_build, :update_build, :admin_build, :destroy_build,
- :create_pipeline_schedule, :read_pipeline_schedule, :update_pipeline_schedule, :admin_pipeline_schedule, :destroy_pipeline_schedule,
- :create_environment, :read_environment, :update_environment, :admin_environment, :destroy_environment,
- :create_cluster, :read_cluster, :update_cluster, :admin_cluster,
- :create_deployment, :read_deployment, :update_deployment, :admin_deployment, :destroy_deployment,
- :destroy_release
- ]
+ context 'without metrics_dashboard_allowed' do
+ before do
+ project.project_feature.update(metrics_dashboard_access_level: ProjectFeature::DISABLED)
+ end
- expect_disallowed(*repository_permissions)
+ it 'disallows all permissions when the feature is disabled' do
+ repository_permissions = [
+ :create_pipeline, :update_pipeline, :admin_pipeline, :destroy_pipeline,
+ :create_build, :read_build, :update_build, :admin_build, :destroy_build,
+ :create_pipeline_schedule, :read_pipeline_schedule, :update_pipeline_schedule, :admin_pipeline_schedule, :destroy_pipeline_schedule,
+ :create_environment, :read_environment, :update_environment, :admin_environment, :destroy_environment,
+ :create_cluster, :read_cluster, :update_cluster, :admin_cluster,
+ :create_deployment, :read_deployment, :update_deployment, :admin_deployment, :destroy_deployment,
+ :destroy_release
+ ]
+
+ expect_disallowed(*repository_permissions)
+ end
+ end
+
+ context 'with metrics_dashboard_allowed' do
+ before do
+ project.project_feature.update(metrics_dashboard_access_level: ProjectFeature::ENABLED)
+ end
+
+ it 'disallows all permissions when the feature is disabled' do
+ repository_permissions = [
+ :create_pipeline, :update_pipeline, :admin_pipeline, :destroy_pipeline,
+ :create_build, :read_build, :update_build, :admin_build, :destroy_build,
+ :create_pipeline_schedule, :read_pipeline_schedule, :update_pipeline_schedule, :admin_pipeline_schedule, :destroy_pipeline_schedule,
+ :create_environment, :update_environment, :admin_environment, :destroy_environment,
+ :create_cluster, :read_cluster, :update_cluster, :admin_cluster,
+ :create_deployment, :read_deployment, :update_deployment, :admin_deployment, :destroy_deployment,
+ :destroy_release
+ ]
+
+ expect_disallowed(*repository_permissions)
+ expect_allowed(:read_environment)
+ end
end
end
@@ -273,7 +329,8 @@ describe ProjectPolicy do
it_behaves_like 'project policies as developer'
it_behaves_like 'project policies as maintainer'
it_behaves_like 'project policies as owner'
- it_behaves_like 'project policies as admin'
+ it_behaves_like 'project policies as admin with admin mode'
+ it_behaves_like 'project policies as admin without admin mode'
context 'when a public project has merge requests allowing access' do
include ProjectForksHelper
@@ -304,7 +361,7 @@ describe ProjectPolicy do
expect_allowed(*maintainer_abilities)
end
- it 'dissallows abilities to a maintainer if the merge request was closed' do
+ it 'disallows abilities to a maintainer if the merge request was closed' do
target_project.add_developer(user)
merge_request.close!
@@ -348,10 +405,24 @@ describe ProjectPolicy do
expect(described_class.new(developer, project)).to be_allowed(:read_project)
end
- it 'does not check the external service for admins and allows access' do
- expect(::Gitlab::ExternalAuthorization).not_to receive(:access_allowed?)
+ context 'with an admin' do
+ context 'when admin mode is enabled', :enable_admin_mode do
+ it 'does not check the external service and allows access' do
+ expect(::Gitlab::ExternalAuthorization).not_to receive(:access_allowed?)
- expect(described_class.new(admin, project)).to be_allowed(:read_project)
+ expect(described_class.new(admin, project)).to be_allowed(:read_project)
+ end
+ end
+
+ context 'when admin mode is disabled' do
+ it 'checks the external service and allows access' do
+ external_service_allow_access(admin, project)
+
+ expect(::Gitlab::ExternalAuthorization).to receive(:access_allowed?)
+
+ expect(described_class.new(admin, project)).to be_allowed(:read_project)
+ end
+ end
end
it 'prevents all but seeing a public project in a list when access is denied' do
@@ -414,7 +485,13 @@ describe ProjectPolicy do
context 'admin' do
let(:current_user) { admin }
- it { expect_allowed(:update_max_artifacts_size) }
+ context 'when admin mode is enabled', :enable_admin_mode do
+ it { expect_allowed(:update_max_artifacts_size) }
+ end
+
+ context 'when admin mode is disabled' do
+ it { expect_disallowed(:update_max_artifacts_size) }
+ end
end
%w(guest reporter developer maintainer owner).each do |role|
@@ -446,7 +523,13 @@ describe ProjectPolicy do
context 'with admin' do
let(:current_user) { admin }
- it { is_expected.to be_allowed(:read_prometheus_alerts) }
+ context 'when admin mode is enabled', :enable_admin_mode do
+ it { is_expected.to be_allowed(:read_prometheus_alerts) }
+ end
+
+ context 'when admin mode is disabled' do
+ it { is_expected.to be_disallowed(:read_prometheus_alerts) }
+ end
end
context 'with owner' do
@@ -485,4 +568,232 @@ describe ProjectPolicy do
it { is_expected.to be_disallowed(:read_prometheus_alerts) }
end
end
+
+ describe 'metrics_dashboard feature' do
+ subject { described_class.new(current_user, project) }
+
+ context 'public project' do
+ let(:project) { create(:project, :public) }
+
+ context 'feature private' do
+ context 'with reporter' do
+ let(:current_user) { reporter }
+
+ it { is_expected.to be_allowed(:metrics_dashboard) }
+ it { is_expected.to be_allowed(:read_prometheus) }
+ it { is_expected.to be_allowed(:read_deployment) }
+ it { is_expected.to be_allowed(:read_metrics_user_starred_dashboard) }
+ it { is_expected.to be_allowed(:create_metrics_user_starred_dashboard) }
+ end
+
+ context 'with guest' do
+ let(:current_user) { guest }
+
+ it { is_expected.to be_disallowed(:metrics_dashboard) }
+ end
+
+ context 'with anonymous' do
+ let(:current_user) { nil }
+
+ it { is_expected.to be_disallowed(:metrics_dashboard) }
+ end
+ end
+
+ context 'feature enabled' do
+ before do
+ project.project_feature.update(metrics_dashboard_access_level: ProjectFeature::ENABLED)
+ end
+
+ context 'with reporter' do
+ let(:current_user) { reporter }
+
+ it { is_expected.to be_allowed(:metrics_dashboard) }
+ it { is_expected.to be_allowed(:read_prometheus) }
+ it { is_expected.to be_allowed(:read_deployment) }
+ it { is_expected.to be_allowed(:read_metrics_user_starred_dashboard) }
+ it { is_expected.to be_allowed(:create_metrics_user_starred_dashboard) }
+ end
+
+ context 'with guest' do
+ let(:current_user) { guest }
+
+ it { is_expected.to be_allowed(:metrics_dashboard) }
+ it { is_expected.to be_allowed(:read_prometheus) }
+ it { is_expected.to be_allowed(:read_deployment) }
+ it { is_expected.to be_allowed(:read_metrics_user_starred_dashboard) }
+ it { is_expected.to be_allowed(:create_metrics_user_starred_dashboard) }
+ end
+
+ context 'with anonymous' do
+ let(:current_user) { nil }
+
+ it { is_expected.to be_allowed(:metrics_dashboard) }
+ it { is_expected.to be_allowed(:read_prometheus) }
+ it { is_expected.to be_allowed(:read_deployment) }
+ it { is_expected.to be_disallowed(:read_metrics_user_starred_dashboard) }
+ it { is_expected.to be_disallowed(:create_metrics_user_starred_dashboard) }
+ end
+ end
+ end
+
+ context 'internal project' do
+ let(:project) { create(:project, :internal) }
+
+ context 'feature private' do
+ context 'with reporter' do
+ let(:current_user) { reporter }
+
+ it { is_expected.to be_allowed(:metrics_dashboard) }
+ it { is_expected.to be_allowed(:read_prometheus) }
+ it { is_expected.to be_allowed(:read_deployment) }
+ it { is_expected.to be_allowed(:read_metrics_user_starred_dashboard) }
+ it { is_expected.to be_allowed(:create_metrics_user_starred_dashboard) }
+ end
+
+ context 'with guest' do
+ let(:current_user) { guest }
+
+ it { is_expected.to be_disallowed(:metrics_dashboard) }
+ end
+
+ context 'with anonymous' do
+ let(:current_user) { nil }
+
+ it { is_expected.to be_disallowed(:metrics_dashboard)}
+ end
+ end
+
+ context 'feature enabled' do
+ before do
+ project.project_feature.update(metrics_dashboard_access_level: ProjectFeature::ENABLED)
+ end
+
+ context 'with reporter' do
+ let(:current_user) { reporter }
+
+ it { is_expected.to be_allowed(:metrics_dashboard) }
+ it { is_expected.to be_allowed(:read_prometheus) }
+ it { is_expected.to be_allowed(:read_deployment) }
+ it { is_expected.to be_allowed(:read_metrics_user_starred_dashboard) }
+ it { is_expected.to be_allowed(:create_metrics_user_starred_dashboard) }
+ end
+
+ context 'with guest' do
+ let(:current_user) { guest }
+
+ it { is_expected.to be_allowed(:metrics_dashboard) }
+ it { is_expected.to be_allowed(:read_prometheus) }
+ it { is_expected.to be_allowed(:read_deployment) }
+ it { is_expected.to be_allowed(:read_metrics_user_starred_dashboard) }
+ it { is_expected.to be_allowed(:create_metrics_user_starred_dashboard) }
+ end
+
+ context 'with anonymous' do
+ let(:current_user) { nil }
+
+ it { is_expected.to be_disallowed(:metrics_dashboard) }
+ end
+ end
+ end
+
+ context 'private project' do
+ let(:project) { create(:project, :private) }
+
+ context 'feature private' do
+ context 'with reporter' do
+ let(:current_user) { reporter }
+
+ it { is_expected.to be_allowed(:metrics_dashboard) }
+ it { is_expected.to be_allowed(:read_prometheus) }
+ it { is_expected.to be_allowed(:read_deployment) }
+ it { is_expected.to be_allowed(:read_metrics_user_starred_dashboard) }
+ it { is_expected.to be_allowed(:create_metrics_user_starred_dashboard) }
+ end
+
+ context 'with guest' do
+ let(:current_user) { guest }
+
+ it { is_expected.to be_disallowed(:metrics_dashboard) }
+ end
+
+ context 'with anonymous' do
+ let(:current_user) { nil }
+
+ it { is_expected.to be_disallowed(:metrics_dashboard) }
+ end
+ end
+
+ context 'feature enabled' do
+ context 'with reporter' do
+ let(:current_user) { reporter }
+
+ it { is_expected.to be_allowed(:metrics_dashboard) }
+ it { is_expected.to be_allowed(:read_prometheus) }
+ it { is_expected.to be_allowed(:read_deployment) }
+ it { is_expected.to be_allowed(:read_metrics_user_starred_dashboard) }
+ it { is_expected.to be_allowed(:create_metrics_user_starred_dashboard) }
+ end
+
+ context 'with guest' do
+ let(:current_user) { guest }
+
+ it { is_expected.to be_disallowed(:metrics_dashboard) }
+ end
+
+ context 'with anonymous' do
+ let(:current_user) { nil }
+
+ it { is_expected.to be_disallowed(:metrics_dashboard) }
+ end
+ end
+ end
+
+ context 'feature disabled' do
+ before do
+ project.project_feature.update(metrics_dashboard_access_level: ProjectFeature::DISABLED)
+ end
+
+ context 'with reporter' do
+ let(:current_user) { reporter }
+
+ it { is_expected.to be_disallowed(:metrics_dashboard) }
+ end
+
+ context 'with guest' do
+ let(:current_user) { guest }
+
+ it { is_expected.to be_disallowed(:metrics_dashboard) }
+ end
+
+ context 'with anonymous' do
+ let(:current_user) { nil }
+
+ it { is_expected.to be_disallowed(:metrics_dashboard) }
+ end
+ end
+ end
+
+ context 'deploy token access' do
+ let!(:project_deploy_token) do
+ create(:project_deploy_token, project: project, deploy_token: deploy_token)
+ end
+
+ subject { described_class.new(deploy_token, project) }
+
+ context 'a deploy token with read_package_registry scope' do
+ let(:deploy_token) { create(:deploy_token, read_package_registry: true) }
+
+ it { is_expected.to be_allowed(:read_package) }
+ it { is_expected.to be_allowed(:read_project) }
+ it { is_expected.to be_disallowed(:create_package) }
+ end
+
+ context 'a deploy token with write_package_registry scope' do
+ let(:deploy_token) { create(:deploy_token, write_package_registry: true) }
+
+ it { is_expected.to be_allowed(:create_package) }
+ it { is_expected.to be_allowed(:read_project) }
+ it { is_expected.to be_disallowed(:destroy_package) }
+ end
+ end
end
diff --git a/spec/policies/project_snippet_policy_spec.rb b/spec/policies/project_snippet_policy_spec.rb
index c5077e119bc..3864666f587 100644
--- a/spec/policies/project_snippet_policy_spec.rb
+++ b/spec/policies/project_snippet_policy_spec.rb
@@ -235,9 +235,18 @@ describe ProjectSnippetPolicy do
let(:snippet_visibility) { :private }
let(:current_user) { create(:admin) }
- it do
- expect_allowed(:read_snippet, :create_note)
- expect_allowed(*author_permissions)
+ context 'when admin mode is enabled', :enable_admin_mode do
+ it do
+ expect_allowed(:read_snippet, :create_note)
+ expect_allowed(*author_permissions)
+ end
+ end
+
+ context 'when admin mode is disabled' do
+ it do
+ expect_disallowed(:read_snippet, :create_note)
+ expect_disallowed(*author_permissions)
+ end
end
end
end
diff --git a/spec/policies/user_policy_spec.rb b/spec/policies/user_policy_spec.rb
index 9da9d2ce49b..63c4bd05836 100644
--- a/spec/policies/user_policy_spec.rb
+++ b/spec/policies/user_policy_spec.rb
@@ -26,7 +26,13 @@ describe UserPolicy do
context "when an admin user tries to destroy a regular user" do
let(:current_user) { create(:user, :admin) }
- it { is_expected.to be_allowed(ability) }
+ context 'when admin mode is enabled', :enable_admin_mode do
+ it { is_expected.to be_allowed(ability) }
+ end
+
+ context 'when admin mode is disabled' do
+ it { is_expected.to be_disallowed(ability) }
+ end
end
context "when an admin user tries to destroy a ghost user" do
diff --git a/spec/policies/wiki_page_policy_spec.rb b/spec/policies/wiki_page_policy_spec.rb
index e550ccf6d65..0dedccb6e88 100644
--- a/spec/policies/wiki_page_policy_spec.rb
+++ b/spec/policies/wiki_page_policy_spec.rb
@@ -2,7 +2,7 @@
require 'spec_helper'
-describe WikiPagePolicy do
+describe WikiPagePolicy, :enable_admin_mode do
include_context 'ProjectPolicyTable context'
include ProjectHelpers
using RSpec::Parameterized::TableSyntax
diff --git a/spec/presenters/ci/build_presenter_spec.rb b/spec/presenters/ci/build_presenter_spec.rb
index b6c47f40ceb..9cf6eb45c63 100644
--- a/spec/presenters/ci/build_presenter_spec.rb
+++ b/spec/presenters/ci/build_presenter_spec.rb
@@ -264,30 +264,4 @@ describe Ci::BuildPresenter do
expect(description).to eq('There has been an API failure, please try again')
end
end
-
- describe '#recoverable?' do
- let(:build) { create(:ci_build, :failed, :script_failure) }
-
- context 'when is a script or missing dependency failure' do
- let(:failure_reasons) { %w(script_failure missing_dependency_failure archived_failure scheduler_failure data_integrity_failure) }
-
- it 'returns false' do
- failure_reasons.each do |failure_reason|
- build.update_attribute(:failure_reason, failure_reason)
- expect(presenter.recoverable?).to be_falsy
- end
- end
- end
-
- context 'when is any other failure type' do
- let(:failure_reasons) { %w(unknown_failure api_failure stuck_or_timeout_failure runner_system_failure) }
-
- it 'returns true' do
- failure_reasons.each do |failure_reason|
- build.update_attribute(:failure_reason, failure_reason)
- expect(presenter.recoverable?).to be_truthy
- end
- end
- end
- end
end
diff --git a/spec/presenters/ci/build_runner_presenter_spec.rb b/spec/presenters/ci/build_runner_presenter_spec.rb
index 0635c318942..de199d2bff9 100644
--- a/spec/presenters/ci/build_runner_presenter_spec.rb
+++ b/spec/presenters/ci/build_runner_presenter_spec.rb
@@ -38,6 +38,47 @@ describe Ci::BuildRunnerPresenter do
expect(presenter.artifacts).to be_empty
end
end
+
+ context 'when artifacts exclude is defined' do
+ let(:build) do
+ create(:ci_build, options: { artifacts: { paths: %w[abc], exclude: %w[cde] } })
+ end
+
+ context 'when the feature is enabled' do
+ before do
+ stub_feature_flags(ci_artifacts_exclude: true)
+ end
+
+ it 'includes the list of excluded paths' do
+ expect(presenter.artifacts.first).to include(
+ artifact_type: :archive,
+ artifact_format: :zip,
+ paths: %w[abc],
+ exclude: %w[cde]
+ )
+ end
+ end
+
+ context 'when the feature is disabled' do
+ before do
+ stub_feature_flags(ci_artifacts_exclude: false)
+ end
+
+ it 'does not include the list of excluded paths' do
+ expect(presenter.artifacts.first).not_to have_key(:exclude)
+ end
+ end
+ end
+
+ context 'when artifacts exclude is not defined' do
+ let(:build) do
+ create(:ci_build, options: { artifacts: { paths: %w[abc] } })
+ end
+
+ it 'does not include an empty list of excluded paths' do
+ expect(presenter.artifacts.first).not_to have_key(:exclude)
+ end
+ end
end
context "with reports" do
@@ -138,32 +179,25 @@ describe Ci::BuildRunnerPresenter do
it 'defaults to git depth setting for the project' do
expect(git_depth).to eq(build.project.ci_default_git_depth)
end
-
- context 'when feature flag :ci_project_git_depth is disabled' do
- before do
- stub_feature_flags(ci_project_git_depth: { enabled: false })
- end
-
- it 'defaults to 0' do
- expect(git_depth).to eq(0)
- end
- end
end
describe '#refspecs' do
subject { presenter.refspecs }
let(:build) { create(:ci_build) }
+ let(:pipeline) { build.pipeline }
it 'returns the correct refspecs' do
- is_expected.to contain_exactly("+refs/heads/#{build.ref}:refs/remotes/origin/#{build.ref}")
+ is_expected.to contain_exactly("+refs/heads/#{build.ref}:refs/remotes/origin/#{build.ref}",
+ "+refs/pipelines/#{pipeline.id}:refs/pipelines/#{pipeline.id}")
end
context 'when ref is tag' do
let(:build) { create(:ci_build, :tag) }
it 'returns the correct refspecs' do
- is_expected.to contain_exactly("+refs/tags/#{build.ref}:refs/tags/#{build.ref}")
+ is_expected.to contain_exactly("+refs/tags/#{build.ref}:refs/tags/#{build.ref}",
+ "+refs/pipelines/#{pipeline.id}:refs/pipelines/#{pipeline.id}")
end
context 'when GIT_DEPTH is zero' do
@@ -173,7 +207,8 @@ describe Ci::BuildRunnerPresenter do
it 'returns the correct refspecs' do
is_expected.to contain_exactly('+refs/tags/*:refs/tags/*',
- '+refs/heads/*:refs/remotes/origin/*')
+ '+refs/heads/*:refs/remotes/origin/*',
+ "+refs/pipelines/#{pipeline.id}:refs/pipelines/#{pipeline.id}")
end
end
end
@@ -183,81 +218,34 @@ describe Ci::BuildRunnerPresenter do
let(:pipeline) { merge_request.all_pipelines.first }
let(:build) { create(:ci_build, ref: pipeline.ref, pipeline: pipeline) }
- context 'when depend_on_persistent_pipeline_ref feature flag is enabled' do
- before do
- stub_feature_flags(ci_force_exposing_merge_request_refs: false)
- pipeline.persistent_ref.create
- end
-
- it 'returns the correct refspecs' do
- is_expected
- .to contain_exactly("+refs/pipelines/#{pipeline.id}:refs/pipelines/#{pipeline.id}")
- end
-
- context 'when ci_force_exposing_merge_request_refs feature flag is enabled' do
- before do
- stub_feature_flags(ci_force_exposing_merge_request_refs: true)
- end
-
- it 'returns the correct refspecs' do
- is_expected
- .to contain_exactly("+refs/pipelines/#{pipeline.id}:refs/pipelines/#{pipeline.id}",
- '+refs/merge-requests/1/head:refs/merge-requests/1/head')
- end
- end
-
- context 'when GIT_DEPTH is zero' do
- before do
- create(:ci_pipeline_variable, key: 'GIT_DEPTH', value: 0, pipeline: build.pipeline)
- end
-
- it 'returns the correct refspecs' do
- is_expected
- .to contain_exactly("+refs/pipelines/#{pipeline.id}:refs/pipelines/#{pipeline.id}",
- '+refs/heads/*:refs/remotes/origin/*',
- '+refs/tags/*:refs/tags/*')
- end
- end
-
- context 'when pipeline is legacy detached merge request pipeline' do
- let(:merge_request) { create(:merge_request, :with_legacy_detached_merge_request_pipeline) }
+ before do
+ pipeline.persistent_ref.create
+ end
- it 'returns the correct refspecs' do
- is_expected.to contain_exactly("+refs/pipelines/#{pipeline.id}:refs/pipelines/#{pipeline.id}",
- "+refs/heads/#{build.ref}:refs/remotes/origin/#{build.ref}")
- end
- end
+ it 'returns the correct refspecs' do
+ is_expected
+ .to contain_exactly("+refs/pipelines/#{pipeline.id}:refs/pipelines/#{pipeline.id}")
end
- context 'when depend_on_persistent_pipeline_ref feature flag is disabled' do
+ context 'when GIT_DEPTH is zero' do
before do
- stub_feature_flags(depend_on_persistent_pipeline_ref: false)
+ create(:ci_pipeline_variable, key: 'GIT_DEPTH', value: 0, pipeline: build.pipeline)
end
it 'returns the correct refspecs' do
is_expected
- .to contain_exactly('+refs/merge-requests/1/head:refs/merge-requests/1/head')
- end
-
- context 'when GIT_DEPTH is zero' do
- before do
- create(:ci_pipeline_variable, key: 'GIT_DEPTH', value: 0, pipeline: build.pipeline)
- end
-
- it 'returns the correct refspecs' do
- is_expected
- .to contain_exactly('+refs/merge-requests/1/head:refs/merge-requests/1/head',
- '+refs/heads/*:refs/remotes/origin/*',
- '+refs/tags/*:refs/tags/*')
- end
+ .to contain_exactly("+refs/pipelines/#{pipeline.id}:refs/pipelines/#{pipeline.id}",
+ '+refs/heads/*:refs/remotes/origin/*',
+ '+refs/tags/*:refs/tags/*')
end
+ end
- context 'when pipeline is legacy detached merge request pipeline' do
- let(:merge_request) { create(:merge_request, :with_legacy_detached_merge_request_pipeline) }
+ context 'when pipeline is legacy detached merge request pipeline' do
+ let(:merge_request) { create(:merge_request, :with_legacy_detached_merge_request_pipeline) }
- it 'returns the correct refspecs' do
- is_expected.to contain_exactly("+refs/heads/#{build.ref}:refs/remotes/origin/#{build.ref}")
- end
+ it 'returns the correct refspecs' do
+ is_expected.to contain_exactly("+refs/pipelines/#{pipeline.id}:refs/pipelines/#{pipeline.id}",
+ "+refs/heads/#{build.ref}:refs/remotes/origin/#{build.ref}")
end
end
end
diff --git a/spec/presenters/clusterable_presenter_spec.rb b/spec/presenters/clusterable_presenter_spec.rb
index 47ccc59ae45..2c0a7f3e9b2 100644
--- a/spec/presenters/clusterable_presenter_spec.rb
+++ b/spec/presenters/clusterable_presenter_spec.rb
@@ -87,4 +87,20 @@ describe ClusterablePresenter do
it { is_expected.to be_nil }
end
+
+ describe '#index_path' do
+ let(:clusterable) { create(:group) }
+
+ context 'without options' do
+ subject { described_class.new(clusterable).index_path }
+
+ it { is_expected.to eq(group_clusters_path(clusterable)) }
+ end
+
+ context 'with options' do
+ subject { described_class.new(clusterable).index_path(format: :json) }
+
+ it { is_expected.to eq(group_clusters_path(clusterable, format: :json)) }
+ end
+ end
end
diff --git a/spec/presenters/pages_domain_presenter_spec.rb b/spec/presenters/pages_domain_presenter_spec.rb
index 1cae3a8c9be..30ce59b7bfb 100644
--- a/spec/presenters/pages_domain_presenter_spec.rb
+++ b/spec/presenters/pages_domain_presenter_spec.rb
@@ -45,14 +45,6 @@ describe PagesDomainPresenter do
it { is_expected.to eq(true) }
- context 'when lets_encrypt_error feature flag is disabled' do
- before do
- stub_feature_flags(pages_letsencrypt_errors: false)
- end
-
- it { is_expected.to eq(false) }
- end
-
context "when Let's Encrypt integration is disabled" do
before do
allow(::Gitlab::LetsEncrypt).to receive(:enabled?).and_return false
diff --git a/spec/presenters/projects/prometheus/alert_presenter_spec.rb b/spec/presenters/projects/prometheus/alert_presenter_spec.rb
index 85c73aa3533..967a0fb2c09 100644
--- a/spec/presenters/projects/prometheus/alert_presenter_spec.rb
+++ b/spec/presenters/projects/prometheus/alert_presenter_spec.rb
@@ -152,6 +152,148 @@ describe Projects::Prometheus::AlertPresenter do
end
end
end
+
+ context 'with embedded metrics' do
+ let(:starts_at) { '2018-03-12T09:06:00Z' }
+
+ shared_examples_for 'markdown with metrics embed' do
+ let(:expected_markdown) do
+ <<~MARKDOWN.chomp
+ #### Summary
+
+ **Start time:** #{presenter.starts_at}#{markdown_line_break}
+ **full_query:** `avg(metric) > 1.0`
+
+ [](#{url})
+ MARKDOWN
+ end
+
+ context 'without a starting time available' do
+ around do |example|
+ Timecop.freeze(starts_at) { example.run }
+ end
+
+ it { is_expected.to eq(expected_markdown) }
+ end
+
+ context 'with a starting time available' do
+ before do
+ payload['startsAt'] = starts_at
+ end
+
+ it { is_expected.to eq(expected_markdown) }
+ end
+ end
+
+ context 'for gitlab-managed prometheus alerts' do
+ let(:gitlab_alert) { create(:prometheus_alert, project: project) }
+ let(:metric_id) { gitlab_alert.prometheus_metric_id }
+ let(:env_id) { gitlab_alert.environment_id }
+
+ before do
+ payload['labels'] = { 'gitlab_alert_id' => metric_id }
+ end
+
+ let(:url) { "http://localhost/#{project.full_path}/prometheus/alerts/#{metric_id}/metrics_dashboard?end=2018-03-12T09%3A36%3A00Z&environment_id=#{env_id}&start=2018-03-12T08%3A36%3A00Z" }
+
+ it_behaves_like 'markdown with metrics embed'
+ end
+
+ context 'for alerts from a self-managed prometheus' do
+ let!(:environment) { create(:environment, project: project, name: 'production') }
+ let(:url) { "http://localhost/#{project.full_path}/-/environments/#{environment.id}/metrics_dashboard?embed_json=#{CGI.escape(embed_content.to_json)}&end=2018-03-12T09%3A36%3A00Z&start=2018-03-12T08%3A36%3A00Z" }
+
+ let(:title) { 'title' }
+ let(:y_label) { 'y_label' }
+ let(:query) { 'avg(metric) > 1.0' }
+ let(:embed_content) do
+ {
+ panel_groups: [{
+ panels: [{
+ type: 'line-graph',
+ title: title,
+ y_label: y_label,
+ metrics: [{ query_range: query }]
+ }]
+ }]
+ }
+ end
+
+ before do
+ # Setup embed time range
+ payload['startsAt'] = starts_at
+
+ # Setup query
+ payload['generatorURL'] = "http://host?g0.expr=#{CGI.escape(query)}"
+
+ # Setup environment
+ payload['labels'] ||= {}
+ payload['labels']['gitlab_environment_name'] = 'production'
+
+ # Setup chart title & axis labels
+ payload['annotations'] ||= {}
+ payload['annotations']['title'] = 'title'
+ payload['annotations']['gitlab_y_label'] = 'y_label'
+ end
+
+ it_behaves_like 'markdown with metrics embed'
+
+ context 'without y_label' do
+ let(:y_label) { title }
+
+ before do
+ payload['annotations'].delete('gitlab_y_label')
+ end
+
+ it_behaves_like 'markdown with metrics embed'
+ end
+
+ context 'when not enough information is present for an embed' do
+ let(:expected_markdown) do
+ <<~MARKDOWN.chomp
+ #### Summary
+
+ **Start time:** #{presenter.starts_at}#{markdown_line_break}
+ **full_query:** `avg(metric) > 1.0`
+
+ MARKDOWN
+ end
+
+ context 'without title' do
+ before do
+ payload['annotations'].delete('title')
+ end
+
+ it { is_expected.to eq(expected_markdown) }
+ end
+
+ context 'without environment' do
+ before do
+ payload['labels'].delete('gitlab_environment_name')
+ end
+
+ it { is_expected.to eq(expected_markdown) }
+ end
+
+ context 'without full_query' do
+ let(:expected_markdown) do
+ <<~MARKDOWN.chomp
+ #### Summary
+
+ **Start time:** #{presenter.starts_at}
+
+ MARKDOWN
+ end
+
+ before do
+ payload.delete('generatorURL')
+ end
+
+ it { is_expected.to eq(expected_markdown) }
+ end
+ end
+ end
+ end
end
describe '#show_performance_dashboard_link?' do
diff --git a/spec/requests/api/admin/ci/variables_spec.rb b/spec/requests/api/admin/ci/variables_spec.rb
new file mode 100644
index 00000000000..bc2f0ba50a2
--- /dev/null
+++ b/spec/requests/api/admin/ci/variables_spec.rb
@@ -0,0 +1,210 @@
+# frozen_string_literal: true
+
+require 'spec_helper'
+
+describe ::API::Admin::Ci::Variables do
+ let_it_be(:admin) { create(:admin) }
+ let_it_be(:user) { create(:user) }
+
+ describe 'GET /admin/ci/variables' do
+ let!(:variable) { create(:ci_instance_variable) }
+
+ it 'returns instance-level variables for admins', :aggregate_failures do
+ get api('/admin/ci/variables', admin)
+
+ expect(response).to have_gitlab_http_status(:ok)
+ expect(json_response).to be_a(Array)
+ end
+
+ it 'does not return instance-level variables for regular users' do
+ get api('/admin/ci/variables', user)
+
+ expect(response).to have_gitlab_http_status(:forbidden)
+ end
+
+ it 'does not return instance-level variables for unauthorized users' do
+ get api('/admin/ci/variables')
+
+ expect(response).to have_gitlab_http_status(:unauthorized)
+ end
+ end
+
+ describe 'GET /admin/ci/variables/:key' do
+ let!(:variable) { create(:ci_instance_variable) }
+
+ it 'returns instance-level variable details for admins', :aggregate_failures do
+ get api("/admin/ci/variables/#{variable.key}", admin)
+
+ expect(response).to have_gitlab_http_status(:ok)
+ expect(json_response['value']).to eq(variable.value)
+ expect(json_response['protected']).to eq(variable.protected?)
+ expect(json_response['variable_type']).to eq(variable.variable_type)
+ end
+
+ it 'responds with 404 Not Found if requesting non-existing variable' do
+ get api('/admin/ci/variables/non_existing_variable', admin)
+
+ expect(response).to have_gitlab_http_status(:not_found)
+ end
+
+ it 'does not return instance-level variable details for regular users' do
+ get api("/admin/ci/variables/#{variable.key}", user)
+
+ expect(response).to have_gitlab_http_status(:forbidden)
+ end
+
+ it 'does not return instance-level variable details for unauthorized users' do
+ get api("/admin/ci/variables/#{variable.key}")
+
+ expect(response).to have_gitlab_http_status(:unauthorized)
+ end
+ end
+
+ describe 'POST /admin/ci/variables' do
+ context 'authorized user with proper permissions' do
+ let!(:variable) { create(:ci_instance_variable) }
+
+ it 'creates variable for admins', :aggregate_failures do
+ expect do
+ post api('/admin/ci/variables', admin),
+ params: {
+ key: 'TEST_VARIABLE_2',
+ value: 'PROTECTED_VALUE_2',
+ protected: true,
+ masked: true
+ }
+ end.to change { ::Ci::InstanceVariable.count }.by(1)
+
+ expect(response).to have_gitlab_http_status(:created)
+ expect(json_response['key']).to eq('TEST_VARIABLE_2')
+ expect(json_response['value']).to eq('PROTECTED_VALUE_2')
+ expect(json_response['protected']).to be_truthy
+ expect(json_response['masked']).to be_truthy
+ expect(json_response['variable_type']).to eq('env_var')
+ end
+
+ it 'creates variable with optional attributes', :aggregate_failures do
+ expect do
+ post api('/admin/ci/variables', admin),
+ params: {
+ variable_type: 'file',
+ key: 'TEST_VARIABLE_2',
+ value: 'VALUE_2'
+ }
+ end.to change { ::Ci::InstanceVariable.count }.by(1)
+
+ expect(response).to have_gitlab_http_status(:created)
+ expect(json_response['key']).to eq('TEST_VARIABLE_2')
+ expect(json_response['value']).to eq('VALUE_2')
+ expect(json_response['protected']).to be_falsey
+ expect(json_response['masked']).to be_falsey
+ expect(json_response['variable_type']).to eq('file')
+ end
+
+ it 'does not allow to duplicate variable key' do
+ expect do
+ post api('/admin/ci/variables', admin),
+ params: { key: variable.key, value: 'VALUE_2' }
+ end.not_to change { ::Ci::InstanceVariable.count }
+
+ expect(response).to have_gitlab_http_status(:bad_request)
+ end
+ end
+
+ context 'authorized user with invalid permissions' do
+ it 'does not create variable' do
+ post api('/admin/ci/variables', user)
+
+ expect(response).to have_gitlab_http_status(:forbidden)
+ end
+ end
+
+ context 'unauthorized user' do
+ it 'does not create variable' do
+ post api('/admin/ci/variables')
+
+ expect(response).to have_gitlab_http_status(:unauthorized)
+ end
+ end
+ end
+
+ describe 'PUT /admin/ci/variables/:key' do
+ let!(:variable) { create(:ci_instance_variable) }
+
+ context 'authorized user with proper permissions' do
+ it 'updates variable data', :aggregate_failures do
+ put api("/admin/ci/variables/#{variable.key}", admin),
+ params: {
+ variable_type: 'file',
+ value: 'VALUE_1_UP',
+ protected: true,
+ masked: true
+ }
+
+ expect(response).to have_gitlab_http_status(:ok)
+ expect(variable.reload.value).to eq('VALUE_1_UP')
+ expect(variable.reload).to be_protected
+ expect(json_response['variable_type']).to eq('file')
+ expect(json_response['masked']).to be_truthy
+ end
+
+ it 'responds with 404 Not Found if requesting non-existing variable' do
+ put api('/admin/ci/variables/non_existing_variable', admin)
+
+ expect(response).to have_gitlab_http_status(:not_found)
+ end
+ end
+
+ context 'authorized user with invalid permissions' do
+ it 'does not update variable' do
+ put api("/admin/ci/variables/#{variable.key}", user)
+
+ expect(response).to have_gitlab_http_status(:forbidden)
+ end
+ end
+
+ context 'unauthorized user' do
+ it 'does not update variable' do
+ put api("/admin/ci/variables/#{variable.key}")
+
+ expect(response).to have_gitlab_http_status(:unauthorized)
+ end
+ end
+ end
+
+ describe 'DELETE /admin/ci/variables/:key' do
+ let!(:variable) { create(:ci_instance_variable) }
+
+ context 'authorized user with proper permissions' do
+ it 'deletes variable' do
+ expect do
+ delete api("/admin/ci/variables/#{variable.key}", admin)
+
+ expect(response).to have_gitlab_http_status(:no_content)
+ end.to change { ::Ci::InstanceVariable.count }.by(-1)
+ end
+
+ it 'responds with 404 Not Found if requesting non-existing variable' do
+ delete api('/admin/ci/variables/non_existing_variable', admin)
+
+ expect(response).to have_gitlab_http_status(:not_found)
+ end
+ end
+
+ context 'authorized user with invalid permissions' do
+ it 'does not delete variable' do
+ delete api("/admin/ci/variables/#{variable.key}", user)
+
+ expect(response).to have_gitlab_http_status(:forbidden)
+ end
+ end
+
+ context 'unauthorized user' do
+ it 'does not delete variable' do
+ delete api("/admin/ci/variables/#{variable.key}")
+
+ expect(response).to have_gitlab_http_status(:unauthorized)
+ end
+ end
+ end
+end
diff --git a/spec/requests/api/appearance_spec.rb b/spec/requests/api/appearance_spec.rb
index 70be3adf723..f8c3db70d16 100644
--- a/spec/requests/api/appearance_spec.rb
+++ b/spec/requests/api/appearance_spec.rb
@@ -31,6 +31,7 @@ describe API::Appearance, 'Appearance' do
expect(json_response['message_background_color']).to eq('#E75E40')
expect(json_response['message_font_color']).to eq('#FFFFFF')
expect(json_response['new_project_guidelines']).to eq('')
+ expect(json_response['profile_image_guidelines']).to eq('')
expect(json_response['title']).to eq('')
end
end
@@ -51,7 +52,8 @@ describe API::Appearance, 'Appearance' do
put api("/application/appearance", admin), params: {
title: "GitLab Test Instance",
description: "gitlab-test.example.com",
- new_project_guidelines: "Please read the FAQs for help."
+ new_project_guidelines: "Please read the FAQs for help.",
+ profile_image_guidelines: "Custom profile image guidelines"
}
expect(response).to have_gitlab_http_status(:ok)
@@ -66,6 +68,7 @@ describe API::Appearance, 'Appearance' do
expect(json_response['message_background_color']).to eq('#E75E40')
expect(json_response['message_font_color']).to eq('#FFFFFF')
expect(json_response['new_project_guidelines']).to eq('Please read the FAQs for help.')
+ expect(json_response['profile_image_guidelines']).to eq('Custom profile image guidelines')
expect(json_response['title']).to eq('GitLab Test Instance')
end
end
diff --git a/spec/requests/api/branches_spec.rb b/spec/requests/api/branches_spec.rb
index 97f880dd3cd..f2dc5b1c045 100644
--- a/spec/requests/api/branches_spec.rb
+++ b/spec/requests/api/branches_spec.rb
@@ -16,6 +16,7 @@ describe API::Branches do
before do
project.add_maintainer(user)
+ project.repository.add_branch(user, 'ends-with.txt', branch_sha)
end
describe "GET /projects/:id/repository/branches" do
@@ -240,6 +241,12 @@ describe API::Branches do
it_behaves_like 'repository branch'
end
+ context 'when branch contains dot txt' do
+ let(:branch_name) { project.repository.find_branch('ends-with.txt').name }
+
+ it_behaves_like 'repository branch'
+ end
+
context 'when branch contains a slash' do
let(:branch_name) { branch_with_slash.name }
@@ -623,7 +630,7 @@ describe API::Branches do
post api(route, user), params: { branch: 'new_design3', ref: 'foo' }
expect(response).to have_gitlab_http_status(:bad_request)
- expect(json_response['message']).to eq('Invalid reference name: new_design3')
+ expect(json_response['message']).to eq('Invalid reference name: foo')
end
end
diff --git a/spec/requests/api/deployments_spec.rb b/spec/requests/api/deployments_spec.rb
index b820b227fff..ef2415a0cde 100644
--- a/spec/requests/api/deployments_spec.rb
+++ b/spec/requests/api/deployments_spec.rb
@@ -439,7 +439,7 @@ describe API::Deployments do
let!(:merge_request3) { create(:merge_request, source_project: project2, target_project: project2) }
it 'returns the relevant merge requests linked to a deployment for a project' do
- deployment.merge_requests << [merge_request1, merge_request2]
+ deployment.link_merge_requests(MergeRequest.where(id: [merge_request1.id, merge_request2.id]))
subject
diff --git a/spec/requests/api/features_spec.rb b/spec/requests/api/features_spec.rb
index ce72a416c33..4ad5b4f9d49 100644
--- a/spec/requests/api/features_spec.rb
+++ b/spec/requests/api/features_spec.rb
@@ -198,7 +198,7 @@ describe API::Features do
end
end
- it 'creates a feature with the given percentage if passed an integer' do
+ it 'creates a feature with the given percentage of time if passed an integer' do
post api("/features/#{feature_name}", admin), params: { value: '50' }
expect(response).to have_gitlab_http_status(:created)
@@ -210,6 +210,19 @@ describe API::Features do
{ 'key' => 'percentage_of_time', 'value' => 50 }
])
end
+
+ it 'creates a feature with the given percentage of actors if passed an integer' do
+ post api("/features/#{feature_name}", admin), params: { value: '50', key: 'percentage_of_actors' }
+
+ expect(response).to have_gitlab_http_status(:created)
+ expect(json_response).to eq(
+ 'name' => 'my_feature',
+ 'state' => 'conditional',
+ 'gates' => [
+ { 'key' => 'boolean', 'value' => false },
+ { 'key' => 'percentage_of_actors', 'value' => 50 }
+ ])
+ end
end
context 'when the feature exists' do
@@ -298,7 +311,7 @@ describe API::Features do
end
end
- context 'with a pre-existing percentage value' do
+ context 'with a pre-existing percentage of time value' do
before do
feature.enable_percentage_of_time(50)
end
@@ -316,6 +329,25 @@ describe API::Features do
])
end
end
+
+ context 'with a pre-existing percentage of actors value' do
+ before do
+ feature.enable_percentage_of_actors(42)
+ end
+
+ it 'updates the percentage of actors if passed an integer' do
+ post api("/features/#{feature_name}", admin), params: { value: '74', key: 'percentage_of_actors' }
+
+ expect(response).to have_gitlab_http_status(:created)
+ expect(json_response).to eq(
+ 'name' => 'my_feature',
+ 'state' => 'conditional',
+ 'gates' => [
+ { 'key' => 'boolean', 'value' => false },
+ { 'key' => 'percentage_of_actors', 'value' => 74 }
+ ])
+ end
+ end
end
end
diff --git a/spec/requests/api/freeze_periods_spec.rb b/spec/requests/api/freeze_periods_spec.rb
new file mode 100644
index 00000000000..0b7828ebedf
--- /dev/null
+++ b/spec/requests/api/freeze_periods_spec.rb
@@ -0,0 +1,475 @@
+# frozen_string_literal: true
+
+require 'spec_helper'
+
+describe API::FreezePeriods do
+ let_it_be(:project) { create(:project, :repository, :private) }
+ let_it_be(:user) { create(:user) }
+ let_it_be(:admin) { create(:admin) }
+ let(:api_user) { user }
+ let(:invalid_cron) { '0 0 0 * *' }
+ let(:last_freeze_period) { project.freeze_periods.last }
+
+ describe 'GET /projects/:id/freeze_periods' do
+ context 'when the user is the admin' do
+ let!(:freeze_period) { create(:ci_freeze_period, project: project, created_at: 2.days.ago) }
+
+ it 'returns 200 HTTP status' do
+ get api("/projects/#{project.id}/freeze_periods", admin)
+
+ expect(response).to have_gitlab_http_status(:ok)
+ end
+ end
+
+ context 'when the user is the maintainer' do
+ before do
+ project.add_maintainer(user)
+ end
+
+ context 'when there are two freeze_periods' do
+ let!(:freeze_period_1) { create(:ci_freeze_period, project: project, created_at: 2.days.ago) }
+ let!(:freeze_period_2) { create(:ci_freeze_period, project: project, created_at: 1.day.ago) }
+
+ it 'returns 200 HTTP status' do
+ get api("/projects/#{project.id}/freeze_periods", user)
+
+ expect(response).to have_gitlab_http_status(:ok)
+ end
+
+ it 'returns freeze_periods ordered by created_at ascending' do
+ get api("/projects/#{project.id}/freeze_periods", user)
+
+ expect(json_response.count).to eq(2)
+ expect(freeze_period_ids).to eq([freeze_period_1.id, freeze_period_2.id])
+ end
+
+ it 'matches response schema' do
+ get api("/projects/#{project.id}/freeze_periods", user)
+
+ expect(response).to match_response_schema('public_api/v4/freeze_periods')
+ end
+ end
+
+ context 'when there are no freeze_periods' do
+ it 'returns 200 HTTP status' do
+ get api("/projects/#{project.id}/freeze_periods", user)
+
+ expect(response).to have_gitlab_http_status(:ok)
+ end
+
+ it 'returns an empty response' do
+ get api("/projects/#{project.id}/freeze_periods", user)
+
+ expect(json_response).to be_empty
+ end
+ end
+ end
+
+ context 'when user is a guest' do
+ before do
+ project.add_guest(user)
+ end
+
+ let!(:freeze_period) do
+ create(:ci_freeze_period, project: project)
+ end
+
+ it 'responds 403 Forbidden' do
+ get api("/projects/#{project.id}/freeze_periods", user)
+
+ expect(response).to have_gitlab_http_status(:forbidden)
+ end
+ end
+
+ context 'when user is not a project member' do
+ it 'responds 404 Not Found' do
+ get api("/projects/#{project.id}/freeze_periods", user)
+
+ expect(response).to have_gitlab_http_status(:not_found)
+ end
+
+ context 'when project is public' do
+ let(:project) { create(:project, :public) }
+
+ it 'responds 403 Forbidden' do
+ get api("/projects/#{project.id}/freeze_periods", user)
+
+ expect(response).to have_gitlab_http_status(:forbidden)
+ end
+ end
+ end
+ end
+
+ describe 'GET /projects/:id/freeze_periods/:freeze_period_id' do
+ context 'when there is a freeze period' do
+ let!(:freeze_period) do
+ create(:ci_freeze_period, project: project)
+ end
+
+ context 'when the user is the admin' do
+ let!(:freeze_period) { create(:ci_freeze_period, project: project, created_at: 2.days.ago) }
+
+ it 'responds 200 OK' do
+ get api("/projects/#{project.id}/freeze_periods/#{freeze_period.id}", admin)
+
+ expect(response).to have_gitlab_http_status(:ok)
+ end
+ end
+
+ context 'when the user is the maintainer' do
+ before do
+ project.add_maintainer(user)
+ end
+
+ it 'responds 200 OK' do
+ get api("/projects/#{project.id}/freeze_periods/#{freeze_period.id}", user)
+
+ expect(response).to have_gitlab_http_status(:ok)
+ end
+
+ it 'returns a freeze period' do
+ get api("/projects/#{project.id}/freeze_periods/#{freeze_period.id}", user)
+
+ expect(json_response).to include(
+ 'id' => freeze_period.id,
+ 'freeze_start' => freeze_period.freeze_start,
+ 'freeze_end' => freeze_period.freeze_end,
+ 'cron_timezone' => freeze_period.cron_timezone)
+ end
+
+ it 'matches response schema' do
+ get api("/projects/#{project.id}/freeze_periods/#{freeze_period.id}", user)
+
+ expect(response).to match_response_schema('public_api/v4/freeze_period')
+ end
+ end
+
+ context 'when user is a guest' do
+ before do
+ project.add_guest(user)
+ end
+
+ it 'responds 403 Forbidden' do
+ get api("/projects/#{project.id}/freeze_periods/#{freeze_period.id}", user)
+
+ expect(response).to have_gitlab_http_status(:forbidden)
+ end
+
+ context 'when project is public' do
+ let(:project) { create(:project, :public) }
+
+ context 'when freeze_period exists' do
+ it 'responds 403 Forbidden' do
+ get api("/projects/#{project.id}/freeze_periods/#{freeze_period.id}", user)
+
+ expect(response).to have_gitlab_http_status(:forbidden)
+ end
+ end
+
+ context 'when freeze_period does not exist' do
+ it 'responds 403 Forbidden' do
+ get api("/projects/#{project.id}/freeze_periods/0", user)
+
+ expect(response).to have_gitlab_http_status(:forbidden)
+ end
+ end
+ end
+ end
+ end
+ end
+
+ describe 'POST /projects/:id/freeze_periods' do
+ let(:params) do
+ {
+ freeze_start: '0 23 * * 5',
+ freeze_end: '0 7 * * 1',
+ cron_timezone: 'UTC'
+ }
+ end
+
+ subject { post api("/projects/#{project.id}/freeze_periods", api_user), params: params }
+
+ context 'when the user is the admin' do
+ let(:api_user) { admin }
+
+ it 'accepts the request' do
+ subject
+
+ expect(response).to have_gitlab_http_status(:created)
+ end
+ end
+
+ context 'when user is the maintainer' do
+ before do
+ project.add_maintainer(user)
+ end
+
+ context 'with valid params' do
+ it 'accepts the request' do
+ subject
+
+ expect(response).to have_gitlab_http_status(:created)
+ end
+
+ it 'creates a new freeze period' do
+ expect do
+ subject
+ end.to change { Ci::FreezePeriod.count }.by(1)
+
+ expect(last_freeze_period.freeze_start).to eq('0 23 * * 5')
+ expect(last_freeze_period.freeze_end).to eq('0 7 * * 1')
+ expect(last_freeze_period.cron_timezone).to eq('UTC')
+ end
+
+ it 'matches response schema' do
+ subject
+
+ expect(response).to match_response_schema('public_api/v4/freeze_period')
+ end
+ end
+
+ context 'with incomplete params' do
+ let(:params) do
+ {
+ freeze_start: '0 23 * * 5',
+ cron_timezone: 'UTC'
+ }
+ end
+
+ it 'responds 400 Bad Request' do
+ subject
+
+ expect(response).to have_gitlab_http_status(:bad_request)
+ expect(json_response['error']).to eq("freeze_end is missing")
+ end
+ end
+
+ context 'with invalid params' do
+ let(:params) do
+ {
+ freeze_start: '0 23 * * 5',
+ freeze_end: invalid_cron,
+ cron_timezone: 'UTC'
+ }
+ end
+
+ it 'responds 400 Bad Request' do
+ subject
+
+ expect(response).to have_gitlab_http_status(:bad_request)
+ expect(json_response['message']['freeze_end']).to eq([" is invalid syntax"])
+ end
+ end
+ end
+
+ context 'when user is a developer' do
+ before do
+ project.add_developer(user)
+ end
+
+ it 'responds 403 Forbidden' do
+ subject
+
+ expect(response).to have_gitlab_http_status(:forbidden)
+ end
+ end
+
+ context 'when user is a reporter' do
+ before do
+ project.add_reporter(user)
+ end
+
+ it 'responds 403 Forbidden' do
+ subject
+
+ expect(response).to have_gitlab_http_status(:forbidden)
+ end
+ end
+
+ context 'when user is not a project member' do
+ it 'responds 403 Forbidden' do
+ subject
+
+ expect(response).to have_gitlab_http_status(:not_found)
+ end
+
+ context 'when project is public' do
+ let(:project) { create(:project, :public) }
+
+ it 'responds 403 Forbidden' do
+ subject
+
+ expect(response).to have_gitlab_http_status(:forbidden)
+ end
+ end
+ end
+ end
+
+ describe 'PUT /projects/:id/freeze_periods/:freeze_period_id' do
+ let(:params) { { freeze_start: '0 22 * * 5', freeze_end: '5 4 * * sun' } }
+ let!(:freeze_period) { create :ci_freeze_period, project: project }
+
+ subject { put api("/projects/#{project.id}/freeze_periods/#{freeze_period.id}", api_user), params: params }
+
+ context 'when user is the admin' do
+ let(:api_user) { admin }
+
+ it 'accepts the request' do
+ subject
+
+ expect(response).to have_gitlab_http_status(:ok)
+ end
+ end
+
+ context 'when user is the maintainer' do
+ before do
+ project.add_maintainer(user)
+ end
+
+ context 'with valid params' do
+ it 'accepts the request' do
+ subject
+
+ expect(response).to have_gitlab_http_status(:ok)
+ end
+
+ it 'performs the update' do
+ subject
+
+ freeze_period.reload
+
+ expect(freeze_period.freeze_start).to eq(params[:freeze_start])
+ expect(freeze_period.freeze_end).to eq(params[:freeze_end])
+ end
+
+ it 'matches response schema' do
+ subject
+
+ expect(response).to match_response_schema('public_api/v4/freeze_period')
+ end
+ end
+
+ context 'with invalid params' do
+ let(:params) { { freeze_start: invalid_cron } }
+
+ it 'responds 400 Bad Request' do
+ subject
+
+ expect(response).to have_gitlab_http_status(:bad_request)
+ expect(json_response['message']['freeze_start']).to eq([" is invalid syntax"])
+ end
+ end
+ end
+
+ context 'when user is a reporter' do
+ before do
+ project.add_reporter(user)
+ end
+
+ it 'responds 403 Forbidden' do
+ subject
+
+ expect(response).to have_gitlab_http_status(:forbidden)
+ end
+ end
+
+ context 'when user is not a project member' do
+ it 'responds 404 Not Found' do
+ subject
+
+ expect(response).to have_gitlab_http_status(:not_found)
+ end
+
+ context 'when project is public' do
+ let(:project) { create(:project, :public) }
+
+ it 'responds 403 Forbidden' do
+ subject
+
+ expect(response).to have_gitlab_http_status(:forbidden)
+ end
+ end
+ end
+ end
+
+ describe 'DELETE /projects/:id/freeze_periods/:freeze_period_id' do
+ let!(:freeze_period) { create :ci_freeze_period, project: project }
+ let(:freeze_period_id) { freeze_period.id }
+
+ subject { delete api("/projects/#{project.id}/freeze_periods/#{freeze_period_id}", api_user) }
+
+ context 'when user is the admin' do
+ let(:api_user) { admin }
+
+ it 'accepts the request' do
+ subject
+
+ expect(response).to have_gitlab_http_status(:no_content)
+ end
+ end
+
+ context 'when user is the maintainer' do
+ before do
+ project.add_maintainer(user)
+ end
+
+ it 'accepts the request' do
+ subject
+
+ expect(response).to have_gitlab_http_status(:no_content)
+ end
+
+ it 'destroys the freeze period' do
+ expect do
+ subject
+ end.to change { Ci::FreezePeriod.count }.by(-1)
+ end
+
+ context 'when it is a non-existing freeze period id' do
+ let(:freeze_period_id) { 0 }
+
+ it '404' do
+ subject
+
+ expect(response).to have_gitlab_http_status(:not_found)
+ end
+ end
+ end
+
+ context 'when user is a reporter' do
+ before do
+ project.add_reporter(user)
+ end
+
+ it 'responds 403 Forbidden' do
+ subject
+
+ expect(response).to have_gitlab_http_status(:forbidden)
+ end
+ end
+
+ context 'when user is not a project member' do
+ it 'responds 404 Not Found' do
+ subject
+
+ expect(response).to have_gitlab_http_status(:not_found)
+ end
+
+ context 'when project is public' do
+ let(:project) { create(:project, :public) }
+
+ it 'responds 403 Forbidden' do
+ subject
+
+ expect(response).to have_gitlab_http_status(:forbidden)
+ end
+ end
+ end
+ end
+
+ def freeze_period_ids
+ json_response.map do |freeze_period_hash|
+ freeze_period_hash.fetch('id')&.to_i
+ end
+ end
+end
diff --git a/spec/requests/api/graphql/boards/board_lists_query_spec.rb b/spec/requests/api/graphql/boards/board_lists_query_spec.rb
new file mode 100644
index 00000000000..f0927487f85
--- /dev/null
+++ b/spec/requests/api/graphql/boards/board_lists_query_spec.rb
@@ -0,0 +1,137 @@
+# frozen_string_literal: true
+
+require 'spec_helper'
+
+describe 'get board lists' do
+ include GraphqlHelpers
+
+ let_it_be(:user) { create(:user) }
+ let_it_be(:unauth_user) { create(:user) }
+ let_it_be(:project) { create(:project, creator_id: user.id, namespace: user.namespace ) }
+ let_it_be(:group) { create(:group, :private) }
+ let_it_be(:project_label) { create(:label, project: project, name: 'Development') }
+ let_it_be(:project_label2) { create(:label, project: project, name: 'Testing') }
+ let_it_be(:group_label) { create(:group_label, group: group, name: 'Development') }
+ let_it_be(:group_label2) { create(:group_label, group: group, name: 'Testing') }
+
+ let(:params) { '' }
+ let(:board) { }
+ let(:board_parent_type) { board_parent.class.to_s.downcase }
+ let(:board_data) { graphql_data[board_parent_type]['boards']['edges'].first['node'] }
+ let(:lists_data) { board_data['lists']['edges'] }
+ let(:start_cursor) { board_data['lists']['pageInfo']['startCursor'] }
+ let(:end_cursor) { board_data['lists']['pageInfo']['endCursor'] }
+
+ def query(list_params = params)
+ graphql_query_for(
+ board_parent_type,
+ { 'fullPath' => board_parent.full_path },
+ <<~BOARDS
+ boards(first: 1) {
+ edges {
+ node {
+ #{field_with_params('lists', list_params)} {
+ pageInfo {
+ startCursor
+ endCursor
+ }
+ edges {
+ node {
+ #{all_graphql_fields_for('board_lists'.classify)}
+ }
+ }
+ }
+ }
+ }
+ }
+ BOARDS
+ )
+ end
+
+ shared_examples 'group and project board lists query' do
+ let!(:board) { create(:board, resource_parent: board_parent) }
+
+ context 'when the user does not have access to the board' do
+ it 'returns nil' do
+ post_graphql(query, current_user: unauth_user)
+
+ expect(graphql_data[board_parent_type]).to be_nil
+ end
+ end
+
+ context 'when user can read the board' do
+ before do
+ board_parent.add_reporter(user)
+ end
+
+ describe 'sorting and pagination' do
+ context 'when using default sorting' do
+ let!(:label_list) { create(:list, board: board, label: label, position: 10) }
+ let!(:label_list2) { create(:list, board: board, label: label2, position: 2) }
+ let!(:backlog_list) { create(:backlog_list, board: board) }
+ let(:closed_list) { board.lists.find_by(list_type: :closed) }
+
+ before do
+ post_graphql(query, current_user: user)
+ end
+
+ it_behaves_like 'a working graphql query'
+
+ context 'when ascending' do
+ let(:lists) { [backlog_list, label_list2, label_list, closed_list] }
+ let(:expected_list_gids) do
+ lists.map { |list| list.to_global_id.to_s }
+ end
+
+ it 'sorts lists' do
+ expect(grab_ids).to eq expected_list_gids
+ end
+
+ context 'when paginating' do
+ let(:params) { 'first: 2' }
+
+ it 'sorts boards' do
+ expect(grab_ids).to eq expected_list_gids.first(2)
+
+ cursored_query = query("after: \"#{end_cursor}\"")
+ post_graphql(cursored_query, current_user: user)
+
+ response_data = grab_list_data(response.body)
+
+ expect(grab_ids(response_data)).to eq expected_list_gids.drop(2).first(2)
+ end
+ end
+ end
+ end
+ end
+ end
+ end
+
+ describe 'for a project' do
+ let(:board_parent) { project }
+ let(:label) { project_label }
+ let(:label2) { project_label2 }
+
+ it_behaves_like 'group and project board lists query'
+ end
+
+ describe 'for a group' do
+ let(:board_parent) { group }
+ let(:label) { group_label }
+ let(:label2) { group_label2 }
+
+ before do
+ allow(board_parent).to receive(:multiple_issue_boards_available?).and_return(false)
+ end
+
+ it_behaves_like 'group and project board lists query'
+ end
+
+ def grab_ids(data = lists_data)
+ data.map { |list| list.dig('node', 'id') }
+ end
+
+ def grab_list_data(response_body)
+ Gitlab::Json.parse(response_body)['data'][board_parent_type]['boards']['edges'][0]['node']['lists']['edges']
+ end
+end
diff --git a/spec/requests/api/graphql/current_user/todos_query_spec.rb b/spec/requests/api/graphql/current_user/todos_query_spec.rb
index 82deba0d92c..321e1062a96 100644
--- a/spec/requests/api/graphql/current_user/todos_query_spec.rb
+++ b/spec/requests/api/graphql/current_user/todos_query_spec.rb
@@ -9,6 +9,7 @@ describe 'Query current user todos' do
let_it_be(:commit_todo) { create(:on_commit_todo, user: current_user, project: create(:project, :repository)) }
let_it_be(:issue_todo) { create(:todo, user: current_user, target: create(:issue)) }
let_it_be(:merge_request_todo) { create(:todo, user: current_user, target: create(:merge_request)) }
+ let_it_be(:design_todo) { create(:todo, user: current_user, target: create(:design)) }
let(:fields) do
<<~QUERY
@@ -34,7 +35,8 @@ describe 'Query current user todos' do
is_expected.to include(
a_hash_including('id' => commit_todo.to_global_id.to_s),
a_hash_including('id' => issue_todo.to_global_id.to_s),
- a_hash_including('id' => merge_request_todo.to_global_id.to_s)
+ a_hash_including('id' => merge_request_todo.to_global_id.to_s),
+ a_hash_including('id' => design_todo.to_global_id.to_s)
)
end
@@ -42,7 +44,8 @@ describe 'Query current user todos' do
is_expected.to include(
a_hash_including('targetType' => 'COMMIT'),
a_hash_including('targetType' => 'ISSUE'),
- a_hash_including('targetType' => 'MERGEREQUEST')
+ a_hash_including('targetType' => 'MERGEREQUEST'),
+ a_hash_including('targetType' => 'DESIGN')
)
end
end
diff --git a/spec/requests/api/graphql/gitlab_schema_spec.rb b/spec/requests/api/graphql/gitlab_schema_spec.rb
index cf409ea6c2d..266c98d6f08 100644
--- a/spec/requests/api/graphql/gitlab_schema_spec.rb
+++ b/spec/requests/api/graphql/gitlab_schema_spec.rb
@@ -190,7 +190,7 @@ describe 'GitlabSchema configurations' do
variables: {}.to_s,
complexity: 181,
depth: 13,
- duration: 7
+ duration_s: 7
}
expect_any_instance_of(Gitlab::Graphql::QueryAnalyzers::LoggerAnalyzer).to receive(:duration).and_return(7)
diff --git a/spec/requests/api/graphql/group/milestones_spec.rb b/spec/requests/api/graphql/group/milestones_spec.rb
index f8e3c0026f5..bad0024e7a3 100644
--- a/spec/requests/api/graphql/group/milestones_spec.rb
+++ b/spec/requests/api/graphql/group/milestones_spec.rb
@@ -7,7 +7,7 @@ describe 'Milestones through GroupQuery' do
let_it_be(:user) { create(:user) }
let_it_be(:now) { Time.now }
- let_it_be(:group) { create(:group, :private) }
+ let_it_be(:group) { create(:group) }
let_it_be(:milestone_1) { create(:milestone, group: group) }
let_it_be(:milestone_2) { create(:milestone, group: group, state: :closed, start_date: now, due_date: now + 1.day) }
let_it_be(:milestone_3) { create(:milestone, group: group, start_date: now, due_date: now + 2.days) }
@@ -17,10 +17,6 @@ describe 'Milestones through GroupQuery' do
let(:milestone_data) { graphql_data['group']['milestones']['edges'] }
describe 'Get list of milestones from a group' do
- before do
- group.add_developer(user)
- end
-
context 'when the request is correct' do
before do
fetch_milestones(user)
@@ -51,6 +47,48 @@ describe 'Milestones through GroupQuery' do
end
end
+ context 'when including milestones from decendants' do
+ let_it_be(:accessible_group) { create(:group, :private, parent: group) }
+ let_it_be(:accessible_project) { create(:project, group: accessible_group) }
+ let_it_be(:inaccessible_group) { create(:group, :private, parent: group) }
+ let_it_be(:inaccessible_project) { create(:project, :private, group: group) }
+ let_it_be(:submilestone_1) { create(:milestone, group: accessible_group) }
+ let_it_be(:submilestone_2) { create(:milestone, project: accessible_project) }
+ let_it_be(:submilestone_3) { create(:milestone, group: inaccessible_group) }
+ let_it_be(:submilestone_4) { create(:milestone, project: inaccessible_project) }
+
+ let(:args) { { include_descendants: true } }
+
+ before do
+ accessible_group.add_developer(user)
+ end
+
+ it 'returns milestones also from subgroups and subprojects visible to user' do
+ fetch_milestones(user, args)
+
+ expect_array_response(
+ milestone_1.to_global_id.to_s, milestone_2.to_global_id.to_s,
+ milestone_3.to_global_id.to_s, milestone_4.to_global_id.to_s,
+ submilestone_1.to_global_id.to_s, submilestone_2.to_global_id.to_s
+ )
+ end
+
+ context 'when group_milestone_descendants is disabled' do
+ before do
+ stub_feature_flags(group_milestone_descendants: false)
+ end
+
+ it 'ignores descendant milestones' do
+ fetch_milestones(user, args)
+
+ expect_array_response(
+ milestone_1.to_global_id.to_s, milestone_2.to_global_id.to_s,
+ milestone_3.to_global_id.to_s, milestone_4.to_global_id.to_s
+ )
+ end
+ end
+ end
+
def fetch_milestones(user = nil, args = {})
post_graphql(milestones_query(args), current_user: user)
end
diff --git a/spec/requests/api/graphql/metrics/dashboard/annotations_spec.rb b/spec/requests/api/graphql/metrics/dashboard/annotations_spec.rb
index f5a5f0a9ec2..cb35411b7a5 100644
--- a/spec/requests/api/graphql/metrics/dashboard/annotations_spec.rb
+++ b/spec/requests/api/graphql/metrics/dashboard/annotations_spec.rb
@@ -21,6 +21,7 @@ describe 'Getting Metrics Dashboard Annotations' do
create(:metrics_dashboard_annotation, environment: environment, starting_at: to.advance(minutes: 5), dashboard_path: path)
end
+ let(:args) { "from: \"#{from}\", to: \"#{to}\"" }
let(:fields) do
<<~QUERY
#{all_graphql_fields_for('MetricsDashboardAnnotation'.classify)}
@@ -47,63 +48,40 @@ describe 'Getting Metrics Dashboard Annotations' do
)
end
- context 'feature flag metrics_dashboard_annotations' do
- let(:args) { "from: \"#{from}\", to: \"#{to}\"" }
+ before do
+ project.add_developer(current_user)
+ post_graphql(query, current_user: current_user)
+ end
- before do
- project.add_developer(current_user)
- end
+ it_behaves_like 'a working graphql query'
- context 'is off' do
- before do
- stub_feature_flags(metrics_dashboard_annotations: false)
- post_graphql(query, current_user: current_user)
- end
+ it 'returns annotations' do
+ annotations = graphql_data.dig('project', 'environments', 'nodes')[0].dig('metricsDashboard', 'annotations', 'nodes')
- it 'returns empty nodes array' do
- annotations = graphql_data.dig('project', 'environments', 'nodes')[0].dig('metricsDashboard', 'annotations', 'nodes')
+ expect(annotations).to match_array [{
+ "description" => annotation.description,
+ "id" => annotation.to_global_id.to_s,
+ "panelId" => annotation.panel_xid,
+ "startingAt" => annotation.starting_at.iso8601,
+ "endingAt" => nil
+ }]
+ end
- expect(annotations).to be_empty
- end
- end
+ context 'arguments' do
+ context 'from is missing' do
+ let(:args) { "to: \"#{from}\"" }
- context 'is on' do
- before do
- stub_feature_flags(metrics_dashboard_annotations: true)
+ it 'returns error' do
post_graphql(query, current_user: current_user)
- end
- it_behaves_like 'a working graphql query'
-
- it 'returns annotations' do
- annotations = graphql_data.dig('project', 'environments', 'nodes')[0].dig('metricsDashboard', 'annotations', 'nodes')
-
- expect(annotations).to match_array [{
- "description" => annotation.description,
- "id" => annotation.to_global_id.to_s,
- "panelId" => annotation.panel_xid,
- "startingAt" => annotation.starting_at.to_s,
- "endingAt" => nil
- }]
+ expect(graphql_errors[0]).to include("message" => "Field 'annotations' is missing required arguments: from")
end
+ end
- context 'arguments' do
- context 'from is missing' do
- let(:args) { "to: \"#{from}\"" }
-
- it 'returns error' do
- post_graphql(query, current_user: current_user)
-
- expect(graphql_errors[0]).to include("message" => "Field 'annotations' is missing required arguments: from")
- end
- end
-
- context 'to is missing' do
- let(:args) { "from: \"#{from}\"" }
+ context 'to is missing' do
+ let(:args) { "from: \"#{from}\"" }
- it_behaves_like 'a working graphql query'
- end
- end
+ it_behaves_like 'a working graphql query'
end
end
end
diff --git a/spec/requests/api/graphql/mutations/alert_management/alerts/update_alert_status_spec.rb b/spec/requests/api/graphql/mutations/alert_management/alerts/update_alert_status_spec.rb
new file mode 100644
index 00000000000..fe50468134c
--- /dev/null
+++ b/spec/requests/api/graphql/mutations/alert_management/alerts/update_alert_status_spec.rb
@@ -0,0 +1,42 @@
+# frozen_string_literal: true
+
+require 'spec_helper'
+
+describe 'Setting the status of an alert' do
+ include GraphqlHelpers
+
+ let_it_be(:user) { create(:user) }
+ let_it_be(:project) { create(:project) }
+ let(:alert) { create(:alert_management_alert, project: project) }
+ let(:input) { { status: 'ACKNOWLEDGED' } }
+
+ let(:mutation) do
+ variables = {
+ project_path: project.full_path,
+ iid: alert.iid.to_s
+ }
+ graphql_mutation(:update_alert_status, variables.merge(input),
+ <<~QL
+ clientMutationId
+ errors
+ alert {
+ iid
+ status
+ }
+ QL
+ )
+ end
+
+ let(:mutation_response) { graphql_mutation_response(:update_alert_status) }
+
+ before do
+ project.add_developer(user)
+ end
+
+ it 'updates the status of the alert' do
+ post_graphql_mutation(mutation, current_user: user)
+
+ expect(response).to have_gitlab_http_status(:success)
+ expect(mutation_response['alert']['status']).to eq(input[:status])
+ end
+end
diff --git a/spec/requests/api/graphql/mutations/award_emojis/add_spec.rb b/spec/requests/api/graphql/mutations/award_emojis/add_spec.rb
index 3fdeccc84f9..83dec7dd3e2 100644
--- a/spec/requests/api/graphql/mutations/award_emojis/add_spec.rb
+++ b/spec/requests/api/graphql/mutations/award_emojis/add_spec.rb
@@ -23,7 +23,7 @@ describe 'Adding an AwardEmoji' do
end
shared_examples 'a mutation that does not create an AwardEmoji' do
- it do
+ specify do
expect do
post_graphql_mutation(mutation, current_user: current_user)
end.not_to change { AwardEmoji.count }
diff --git a/spec/requests/api/graphql/mutations/award_emojis/remove_spec.rb b/spec/requests/api/graphql/mutations/award_emojis/remove_spec.rb
index c78f0c7ca27..a2997db6cae 100644
--- a/spec/requests/api/graphql/mutations/award_emojis/remove_spec.rb
+++ b/spec/requests/api/graphql/mutations/award_emojis/remove_spec.rb
@@ -24,7 +24,7 @@ describe 'Removing an AwardEmoji' do
end
shared_examples 'a mutation that does not destroy an AwardEmoji' do
- it do
+ specify do
expect do
post_graphql_mutation(mutation, current_user: current_user)
end.not_to change { AwardEmoji.count }
diff --git a/spec/requests/api/graphql/mutations/award_emojis/toggle_spec.rb b/spec/requests/api/graphql/mutations/award_emojis/toggle_spec.rb
index bc796b34db4..e1180c85c6b 100644
--- a/spec/requests/api/graphql/mutations/award_emojis/toggle_spec.rb
+++ b/spec/requests/api/graphql/mutations/award_emojis/toggle_spec.rb
@@ -23,7 +23,7 @@ describe 'Toggling an AwardEmoji' do
end
shared_examples 'a mutation that does not create or destroy an AwardEmoji' do
- it do
+ specify do
expect do
post_graphql_mutation(mutation, current_user: current_user)
end.not_to change { AwardEmoji.count }
diff --git a/spec/requests/api/graphql/mutations/branches/create_spec.rb b/spec/requests/api/graphql/mutations/branches/create_spec.rb
new file mode 100644
index 00000000000..b3c378ec2bc
--- /dev/null
+++ b/spec/requests/api/graphql/mutations/branches/create_spec.rb
@@ -0,0 +1,45 @@
+# frozen_string_literal: true
+
+require 'spec_helper'
+
+describe 'Creation of a new branch' do
+ include GraphqlHelpers
+
+ let_it_be(:current_user) { create(:user) }
+ let_it_be(:project) { create(:project, :public, :empty_repo) }
+ let(:input) { { project_path: project.full_path, name: new_branch, ref: ref } }
+ let(:new_branch) { 'new_branch' }
+ let(:ref) { 'master' }
+
+ let(:mutation) { graphql_mutation(:create_branch, input) }
+ let(:mutation_response) { graphql_mutation_response(:create_branch) }
+
+ context 'the user is not allowed to create a branch' do
+ it_behaves_like 'a mutation that returns top-level errors',
+ errors: ['The resource that you are attempting to access does not exist or you don\'t have permission to perform this action']
+ end
+
+ context 'when user has permissions to create a branch' do
+ before do
+ project.add_developer(current_user)
+ end
+
+ it 'creates a new branch' do
+ post_graphql_mutation(mutation, current_user: current_user)
+
+ expect(response).to have_gitlab_http_status(:success)
+ expect(mutation_response['branch']).to include(
+ 'name' => new_branch,
+ 'commit' => a_hash_including('id')
+ )
+ end
+
+ context 'when ref is not correct' do
+ let(:new_branch) { 'another_branch' }
+ let(:ref) { 'unknown' }
+
+ it_behaves_like 'a mutation that returns errors in the response',
+ errors: ['Invalid reference name: unknown']
+ end
+ end
+end
diff --git a/spec/requests/api/graphql/mutations/design_management/delete_spec.rb b/spec/requests/api/graphql/mutations/design_management/delete_spec.rb
new file mode 100644
index 00000000000..10376305b3e
--- /dev/null
+++ b/spec/requests/api/graphql/mutations/design_management/delete_spec.rb
@@ -0,0 +1,127 @@
+# frozen_string_literal: true
+
+require "spec_helper"
+
+describe "deleting designs" do
+ include GraphqlHelpers
+ include DesignManagementTestHelpers
+
+ let(:developer) { create(:user) }
+ let(:current_user) { developer }
+ let(:issue) { create(:issue) }
+ let(:project) { issue.project }
+ let(:designs) { create_designs }
+ let(:variables) { {} }
+
+ let(:mutation) do
+ input = {
+ project_path: project.full_path,
+ iid: issue.iid,
+ filenames: designs.map(&:filename)
+ }.merge(variables)
+ graphql_mutation(:design_management_delete, input)
+ end
+
+ let(:mutation_response) { graphql_mutation_response(:design_management_delete) }
+
+ def mutate!
+ post_graphql_mutation(mutation, current_user: current_user)
+ end
+
+ before do
+ enable_design_management
+
+ project.add_developer(developer)
+ end
+
+ shared_examples 'a failed request' do
+ let(:the_error) { be_present }
+
+ it 'reports an error' do
+ mutate!
+
+ expect(graphql_errors).to include(a_hash_including('message' => the_error))
+ end
+ end
+
+ context 'the designs list is empty' do
+ it_behaves_like 'a failed request' do
+ let(:designs) { [] }
+ let(:the_error) { a_string_matching %r/was provided invalid value/ }
+ end
+ end
+
+ context 'the designs list contains filenames we cannot find' do
+ it_behaves_like 'a failed request' do
+ let(:designs) { %w/foo bar baz/.map { |fn| OpenStruct.new(filename: fn) } }
+ let(:the_error) { a_string_matching %r/filenames were not found/ }
+ end
+ end
+
+ context 'the current user does not have developer access' do
+ it_behaves_like 'a failed request' do
+ let(:current_user) { create(:user) }
+ let(:the_error) { a_string_matching %r/you don't have permission/ }
+ end
+ end
+
+ context "when the issue does not exist" do
+ it_behaves_like 'a failed request' do
+ let(:variables) { { iid: "1234567890" } }
+ let(:the_error) { a_string_matching %r/does not exist/ }
+ end
+ end
+
+ context "when saving the designs raises an error" do
+ let(:designs) { create_designs(1) }
+
+ it "responds with errors" do
+ expect_next_instance_of(::DesignManagement::DeleteDesignsService) do |service|
+ expect(service)
+ .to receive(:execute)
+ .and_return({ status: :error, message: "Something went wrong" })
+ end
+
+ mutate!
+
+ expect(mutation_response).to include('errors' => include(eq "Something went wrong"))
+ end
+ end
+
+ context 'one of the designs is already deleted' do
+ let(:designs) do
+ create_designs(2).push(create(:design, :with_file, deleted: true, issue: issue))
+ end
+
+ it 'reports an error' do
+ mutate!
+
+ expect(graphql_errors).to be_present
+ end
+ end
+
+ context 'when the user names designs to delete' do
+ before do
+ create_designs(1)
+ end
+
+ let!(:designs) { create_designs(2) }
+
+ it 'deletes the designs' do
+ expect { mutate! }
+ .to change { issue.reset.designs.current.count }.from(3).to(1)
+ end
+
+ it 'has no errors' do
+ mutate!
+
+ expect(mutation_response).to include('errors' => be_empty)
+ end
+ end
+
+ private
+
+ def create_designs(how_many = 2)
+ create_list(:design, how_many, :with_file, issue: issue)
+ end
+end
diff --git a/spec/requests/api/graphql/mutations/design_management/upload_spec.rb b/spec/requests/api/graphql/mutations/design_management/upload_spec.rb
new file mode 100644
index 00000000000..22adc064406
--- /dev/null
+++ b/spec/requests/api/graphql/mutations/design_management/upload_spec.rb
@@ -0,0 +1,99 @@
+# frozen_string_literal: true
+require "spec_helper"
+
+describe "uploading designs" do
+ include GraphqlHelpers
+ include DesignManagementTestHelpers
+ include WorkhorseHelpers
+
+ let(:current_user) { create(:user) }
+ let(:issue) { create(:issue) }
+ let(:project) { issue.project }
+ let(:files) { [fixture_file_upload("spec/fixtures/dk.png")] }
+ let(:variables) { {} }
+
+ let(:mutation) do
+ input = {
+ project_path: project.full_path,
+ iid: issue.iid,
+ files: files
+ }.merge(variables)
+ graphql_mutation(:design_management_upload, input)
+ end
+
+ let(:mutation_response) { graphql_mutation_response(:design_management_upload) }
+
+ before do
+ enable_design_management
+
+ project.add_developer(current_user)
+ end
+
+ it "returns an error if the user is not allowed to upload designs" do
+ post_graphql_mutation(mutation, current_user: create(:user))
+
+ expect(graphql_errors).to be_present
+ end
+
+ it "succeeds (backward compatibility)" do
+ post_graphql_mutation(mutation, current_user: current_user)
+
+ expect(graphql_errors).not_to be_present
+ end
+
+ it 'succeeds' do
+ file_path_in_params = ['designManagementUploadInput', 'files', 0]
+ params = mutation_to_apollo_uploads_param(mutation, files: [file_path_in_params])
+
+ workhorse_post_with_file(api('/', current_user, version: 'graphql'),
+ params: params,
+ file_key: '1'
+ )
+
+ expect(graphql_errors).not_to be_present
+ end
+
+ it "responds with the created designs" do
+ post_graphql_mutation(mutation, current_user: current_user)
+
+ expect(mutation_response).to include(
+ "designs" => a_collection_containing_exactly(
+ a_hash_including("filename" => "dk.png")
+ )
+ )
+ end
+
+ it "can respond with skipped designs" do
+ 2.times do
+ post_graphql_mutation(mutation, current_user: current_user)
+ files.each(&:rewind)
+ end
+
+ expect(mutation_response).to include(
+ "skippedDesigns" => a_collection_containing_exactly(
+ a_hash_including("filename" => "dk.png")
+ )
+ )
+ end
+
+ context "when the issue does not exist" do
+ let(:variables) { { iid: "123" } }
+
+ it "returns an error" do
+ post_graphql_mutation(mutation, current_user: create(:user))
+
+ expect(graphql_errors).not_to be_empty
+ end
+ end
+
+ context "when saving the designs raises an error" do
+ it "responds with errors" do
+ expect_next_instance_of(::DesignManagement::SaveDesignsService) do |service|
+ expect(service).to receive(:execute).and_return({ status: :error, message: "Something went wrong" })
+ end
+
+ post_graphql_mutation(mutation, current_user: current_user)
+ expect(mutation_response["errors"].first).to eq("Something went wrong")
+ end
+ end
+end
diff --git a/spec/requests/api/graphql/mutations/jira_import/start_spec.rb b/spec/requests/api/graphql/mutations/jira_import/start_spec.rb
index 014da5d1e1a..84110098400 100644
--- a/spec/requests/api/graphql/mutations/jira_import/start_spec.rb
+++ b/spec/requests/api/graphql/mutations/jira_import/start_spec.rb
@@ -3,6 +3,7 @@
require 'spec_helper'
describe 'Starting a Jira Import' do
+ include JiraServiceHelper
include GraphqlHelpers
let_it_be(:user) { create(:user) }
@@ -104,6 +105,8 @@ describe 'Starting a Jira Import' do
before do
project.reload
+
+ stub_jira_service_test
end
context 'when issues feature are disabled' do
@@ -118,7 +121,7 @@ describe 'Starting a Jira Import' do
it_behaves_like 'a mutation that returns errors in the response', errors: ['Unable to find Jira project to import data from.']
end
- context 'when jira import successfully scheduled' do
+ context 'when Jira import successfully scheduled' do
it 'schedules a Jira import' do
post_graphql_mutation(mutation, current_user: current_user)
diff --git a/spec/requests/api/graphql/mutations/metrics/dashboard/annotations/create_spec.rb b/spec/requests/api/graphql/mutations/metrics/dashboard/annotations/create_spec.rb
new file mode 100644
index 00000000000..8568dc8ffc0
--- /dev/null
+++ b/spec/requests/api/graphql/mutations/metrics/dashboard/annotations/create_spec.rb
@@ -0,0 +1,231 @@
+# frozen_string_literal: true
+
+require 'spec_helper'
+
+describe Mutations::Metrics::Dashboard::Annotations::Create do
+ include GraphqlHelpers
+
+ let_it_be(:current_user) { create(:user) }
+ let_it_be(:project) { create(:project, :private, :repository) }
+ let_it_be(:environment) { create(:environment, project: project) }
+ let_it_be(:cluster) { create(:cluster, projects: [project]) }
+ let(:dashboard_path) { 'config/prometheus/common_metrics.yml' }
+ let(:starting_at) { Time.current.iso8601 }
+ let(:ending_at) { 1.hour.from_now.iso8601 }
+ let(:description) { 'test description' }
+
+ def mutation_response
+ graphql_mutation_response(:create_annotation)
+ end
+
+ specify { expect(described_class).to require_graphql_authorizations(:create_metrics_dashboard_annotation) }
+
+ context 'when annotation source is environment' do
+ let(:mutation) do
+ variables = {
+ environment_id: GitlabSchema.id_from_object(environment).to_s,
+ starting_at: starting_at,
+ ending_at: ending_at,
+ dashboard_path: dashboard_path,
+ description: description
+ }
+
+ graphql_mutation(:create_annotation, variables)
+ end
+
+ context 'when the user does not have permission' do
+ before do
+ project.add_reporter(current_user)
+ end
+
+ it_behaves_like 'a mutation that returns top-level errors',
+ errors: [Gitlab::Graphql::Authorize::AuthorizeResource::RESOURCE_ACCESS_ERROR]
+
+ it 'does not create the annotation' do
+ expect do
+ post_graphql_mutation(mutation, current_user: current_user)
+ end.not_to change { Metrics::Dashboard::Annotation.count }
+ end
+ end
+
+ context 'when the user has permission' do
+ before do
+ project.add_developer(current_user)
+ end
+
+ it 'creates the annotation' do
+ expect do
+ post_graphql_mutation(mutation, current_user: current_user)
+ end.to change { Metrics::Dashboard::Annotation.count }.by(1)
+ end
+
+ it 'returns the created annotation' do
+ post_graphql_mutation(mutation, current_user: current_user)
+
+ annotation = Metrics::Dashboard::Annotation.first
+ annotation_id = GitlabSchema.id_from_object(annotation).to_s
+
+ expect(mutation_response['annotation']['description']).to match(description)
+ expect(mutation_response['annotation']['startingAt'].to_time).to match(starting_at.to_time)
+ expect(mutation_response['annotation']['endingAt'].to_time).to match(ending_at.to_time)
+ expect(mutation_response['annotation']['id']).to match(annotation_id)
+ expect(annotation.environment_id).to eq(environment.id)
+ end
+
+ context 'when environment_id is missing' do
+ let(:mutation) do
+ variables = {
+ environment_id: nil,
+ starting_at: starting_at,
+ ending_at: ending_at,
+ dashboard_path: dashboard_path,
+ description: description
+ }
+
+ graphql_mutation(:create_annotation, variables)
+ end
+
+ it_behaves_like 'a mutation that returns top-level errors', errors: [described_class::ANNOTATION_SOURCE_ARGUMENT_ERROR]
+ end
+
+ context 'when environment_id is invalid' do
+ let(:mutation) do
+ variables = {
+ environment_id: 'invalid_id',
+ starting_at: starting_at,
+ ending_at: ending_at,
+ dashboard_path: dashboard_path,
+ description: description
+ }
+
+ graphql_mutation(:create_annotation, variables)
+ end
+
+ it_behaves_like 'a mutation that returns top-level errors', errors: ['invalid_id is not a valid GitLab id.']
+ end
+ end
+ end
+
+ context 'when annotation source is cluster' do
+ let(:mutation) do
+ variables = {
+ cluster_id: GitlabSchema.id_from_object(cluster).to_s,
+ starting_at: starting_at,
+ ending_at: ending_at,
+ dashboard_path: dashboard_path,
+ description: description
+ }
+
+ graphql_mutation(:create_annotation, variables)
+ end
+
+ context 'with permission' do
+ before do
+ project.add_developer(current_user)
+ end
+
+ it 'creates the annotation' do
+ expect do
+ post_graphql_mutation(mutation, current_user: current_user)
+ end.to change { Metrics::Dashboard::Annotation.count }.by(1)
+ end
+
+ it 'returns the created annotation' do
+ post_graphql_mutation(mutation, current_user: current_user)
+
+ annotation = Metrics::Dashboard::Annotation.first
+ annotation_id = GitlabSchema.id_from_object(annotation).to_s
+
+ expect(mutation_response['annotation']['description']).to match(description)
+ expect(mutation_response['annotation']['startingAt'].to_time).to match(starting_at.to_time)
+ expect(mutation_response['annotation']['endingAt'].to_time).to match(ending_at.to_time)
+ expect(mutation_response['annotation']['id']).to match(annotation_id)
+ expect(annotation.cluster_id).to eq(cluster.id)
+ end
+
+ context 'when cluster_id is missing' do
+ let(:mutation) do
+ variables = {
+ cluster_id: nil,
+ starting_at: starting_at,
+ ending_at: ending_at,
+ dashboard_path: dashboard_path,
+ description: description
+ }
+
+ graphql_mutation(:create_annotation, variables)
+ end
+
+ it_behaves_like 'a mutation that returns top-level errors', errors: [described_class::ANNOTATION_SOURCE_ARGUMENT_ERROR]
+ end
+ end
+
+ context 'without permission' do
+ before do
+ project.add_guest(current_user)
+ end
+
+ it_behaves_like 'a mutation that returns top-level errors',
+ errors: [Gitlab::Graphql::Authorize::AuthorizeResource::RESOURCE_ACCESS_ERROR]
+
+ it 'does not create the annotation' do
+ expect do
+ post_graphql_mutation(mutation, current_user: current_user)
+ end.not_to change { Metrics::Dashboard::Annotation.count }
+ end
+ end
+
+ context 'when cluster_id is invalid' do
+ let(:mutation) do
+ variables = {
+ cluster_id: 'invalid_id',
+ starting_at: starting_at,
+ ending_at: ending_at,
+ dashboard_path: dashboard_path,
+ description: description
+ }
+
+ graphql_mutation(:create_annotation, variables)
+ end
+
+ it_behaves_like 'a mutation that returns top-level errors', errors: ['invalid_id is not a valid GitLab id.']
+ end
+ end
+
+ context 'when both environment_id and cluster_id are provided' do
+ let(:mutation) do
+ variables = {
+ environment_id: GitlabSchema.id_from_object(environment).to_s,
+ cluster_id: GitlabSchema.id_from_object(cluster).to_s,
+ starting_at: starting_at,
+ ending_at: ending_at,
+ dashboard_path: dashboard_path,
+ description: description
+ }
+
+ graphql_mutation(:create_annotation, variables)
+ end
+
+ it_behaves_like 'a mutation that returns top-level errors', errors: [described_class::ANNOTATION_SOURCE_ARGUMENT_ERROR]
+ end
+
+ context 'when a non-cluster or environment id is provided' do
+ let(:mutation) do
+ variables = {
+ environment_id: GitlabSchema.id_from_object(project).to_s,
+ starting_at: starting_at,
+ ending_at: ending_at,
+ dashboard_path: dashboard_path,
+ description: description
+ }
+
+ graphql_mutation(:create_annotation, variables)
+ end
+
+ before do
+ project.add_developer(current_user)
+ end
+
+ it_behaves_like 'a mutation that returns top-level errors', errors: [described_class::INVALID_ANNOTATION_SOURCE_ERROR]
+ end
+end
diff --git a/spec/requests/api/graphql/mutations/snippets/create_spec.rb b/spec/requests/api/graphql/mutations/snippets/create_spec.rb
index cef7fc5cbe3..e1e5fe22887 100644
--- a/spec/requests/api/graphql/mutations/snippets/create_spec.rb
+++ b/spec/requests/api/graphql/mutations/snippets/create_spec.rb
@@ -13,6 +13,7 @@ describe 'Creating a Snippet' do
let(:file_name) { 'Initial file_name' }
let(:visibility_level) { 'public' }
let(:project_path) { nil }
+ let(:uploaded_files) { nil }
let(:mutation) do
variables = {
@@ -21,7 +22,8 @@ describe 'Creating a Snippet' do
visibility_level: visibility_level,
file_name: file_name,
title: title,
- project_path: project_path
+ project_path: project_path,
+ uploaded_files: uploaded_files
}
graphql_mutation(:create_snippet, variables)
@@ -31,6 +33,8 @@ describe 'Creating a Snippet' do
graphql_mutation_response(:create_snippet)
end
+ subject { post_graphql_mutation(mutation, current_user: current_user) }
+
context 'when the user does not have permission' do
let(:current_user) { nil }
@@ -39,7 +43,7 @@ describe 'Creating a Snippet' do
it 'does not create the Snippet' do
expect do
- post_graphql_mutation(mutation, current_user: current_user)
+ subject
end.not_to change { Snippet.count }
end
@@ -48,7 +52,7 @@ describe 'Creating a Snippet' do
it 'does not create the snippet when the user is not authorized' do
expect do
- post_graphql_mutation(mutation, current_user: current_user)
+ subject
end.not_to change { Snippet.count }
end
end
@@ -60,12 +64,12 @@ describe 'Creating a Snippet' do
context 'with PersonalSnippet' do
it 'creates the Snippet' do
expect do
- post_graphql_mutation(mutation, current_user: current_user)
+ subject
end.to change { Snippet.count }.by(1)
end
it 'returns the created Snippet' do
- post_graphql_mutation(mutation, current_user: current_user)
+ subject
expect(mutation_response['snippet']['blob']['richData']).to be_nil
expect(mutation_response['snippet']['blob']['plainData']).to match(content)
@@ -86,12 +90,12 @@ describe 'Creating a Snippet' do
it 'creates the Snippet' do
expect do
- post_graphql_mutation(mutation, current_user: current_user)
+ subject
end.to change { Snippet.count }.by(1)
end
it 'returns the created Snippet' do
- post_graphql_mutation(mutation, current_user: current_user)
+ subject
expect(mutation_response['snippet']['blob']['richData']).to be_nil
expect(mutation_response['snippet']['blob']['plainData']).to match(content)
@@ -106,7 +110,7 @@ describe 'Creating a Snippet' do
let(:project_path) { 'foobar' }
it 'returns an an error' do
- post_graphql_mutation(mutation, current_user: current_user)
+ subject
errors = json_response['errors']
expect(errors.first['message']).to eq(Gitlab::Graphql::Authorize::AuthorizeResource::RESOURCE_ACCESS_ERROR)
@@ -117,7 +121,7 @@ describe 'Creating a Snippet' do
it 'returns an an error' do
project.project_feature.update_attribute(:snippets_access_level, ProjectFeature::DISABLED)
- post_graphql_mutation(mutation, current_user: current_user)
+ subject
errors = json_response['errors']
expect(errors.first['message']).to eq(Gitlab::Graphql::Authorize::AuthorizeResource::RESOURCE_ACCESS_ERROR)
@@ -132,15 +136,41 @@ describe 'Creating a Snippet' do
it 'does not create the Snippet' do
expect do
- post_graphql_mutation(mutation, current_user: current_user)
+ subject
end.not_to change { Snippet.count }
end
it 'does not return Snippet' do
- post_graphql_mutation(mutation, current_user: current_user)
+ subject
expect(mutation_response['snippet']).to be_nil
end
end
+
+ context 'when there uploaded files' do
+ shared_examples 'expected files argument' do |file_value, expected_value|
+ let(:uploaded_files) { file_value }
+
+ it do
+ expect(::Snippets::CreateService).to receive(:new).with(nil, user, hash_including(files: expected_value))
+
+ subject
+ end
+ end
+
+ it_behaves_like 'expected files argument', nil, nil
+ it_behaves_like 'expected files argument', %w(foo bar), %w(foo bar)
+ it_behaves_like 'expected files argument', 'foo', %w(foo)
+
+ context 'when files has an invalid value' do
+ let(:uploaded_files) { [1] }
+
+ it 'returns an error' do
+ subject
+
+ expect(json_response['errors']).to be
+ end
+ end
+ end
end
end
diff --git a/spec/requests/api/graphql/mutations/snippets/destroy_spec.rb b/spec/requests/api/graphql/mutations/snippets/destroy_spec.rb
index 351d2db8973..cb9aeea74b2 100644
--- a/spec/requests/api/graphql/mutations/snippets/destroy_spec.rb
+++ b/spec/requests/api/graphql/mutations/snippets/destroy_spec.rb
@@ -6,9 +6,10 @@ describe 'Destroying a Snippet' do
include GraphqlHelpers
let(:current_user) { snippet.author }
+ let(:snippet_gid) { snippet.to_global_id.to_s }
let(:mutation) do
variables = {
- id: snippet.to_global_id.to_s
+ id: snippet_gid
}
graphql_mutation(:destroy_snippet, variables)
@@ -49,9 +50,11 @@ describe 'Destroying a Snippet' do
end
describe 'PersonalSnippet' do
- it_behaves_like 'graphql delete actions' do
- let_it_be(:snippet) { create(:personal_snippet) }
- end
+ let_it_be(:snippet) { create(:personal_snippet) }
+
+ it_behaves_like 'graphql delete actions'
+
+ it_behaves_like 'when the snippet is not found'
end
describe 'ProjectSnippet' do
@@ -85,5 +88,7 @@ describe 'Destroying a Snippet' do
end
end
end
+
+ it_behaves_like 'when the snippet is not found'
end
end
diff --git a/spec/requests/api/graphql/mutations/snippets/mark_as_spam_spec.rb b/spec/requests/api/graphql/mutations/snippets/mark_as_spam_spec.rb
index 05e3f7e6806..6d4dce3f6f1 100644
--- a/spec/requests/api/graphql/mutations/snippets/mark_as_spam_spec.rb
+++ b/spec/requests/api/graphql/mutations/snippets/mark_as_spam_spec.rb
@@ -10,9 +10,11 @@ describe 'Mark snippet as spam', :do_not_mock_admin_mode do
let_it_be(:snippet) { create(:personal_snippet) }
let_it_be(:user_agent_detail) { create(:user_agent_detail, subject: snippet) }
let(:current_user) { snippet.author }
+
+ let(:snippet_gid) { snippet.to_global_id.to_s }
let(:mutation) do
variables = {
- id: snippet.to_global_id.to_s
+ id: snippet_gid
}
graphql_mutation(:mark_as_spam_snippet, variables)
@@ -23,13 +25,15 @@ describe 'Mark snippet as spam', :do_not_mock_admin_mode do
end
shared_examples 'does not mark the snippet as spam' do
- it do
+ specify do
expect do
post_graphql_mutation(mutation, current_user: current_user)
end.not_to change { snippet.reload.user_agent_detail.submitted }
end
end
+ it_behaves_like 'when the snippet is not found'
+
context 'when the user does not have permission' do
let(:current_user) { other_user }
diff --git a/spec/requests/api/graphql/mutations/snippets/update_spec.rb b/spec/requests/api/graphql/mutations/snippets/update_spec.rb
index 1035e3346e1..968ea5aed52 100644
--- a/spec/requests/api/graphql/mutations/snippets/update_spec.rb
+++ b/spec/requests/api/graphql/mutations/snippets/update_spec.rb
@@ -15,9 +15,10 @@ describe 'Updating a Snippet' do
let(:updated_file_name) { 'Updated file_name' }
let(:current_user) { snippet.author }
+ let(:snippet_gid) { GitlabSchema.id_from_object(snippet).to_s }
let(:mutation) do
variables = {
- id: GitlabSchema.id_from_object(snippet).to_s,
+ id: snippet_gid,
content: updated_content,
description: updated_description,
visibility_level: 'public',
@@ -90,16 +91,18 @@ describe 'Updating a Snippet' do
end
describe 'PersonalSnippet' do
- it_behaves_like 'graphql update actions' do
- let(:snippet) do
- create(:personal_snippet,
- :private,
- file_name: original_file_name,
- title: original_title,
- content: original_content,
- description: original_description)
- end
+ let(:snippet) do
+ create(:personal_snippet,
+ :private,
+ file_name: original_file_name,
+ title: original_title,
+ content: original_content,
+ description: original_description)
end
+
+ it_behaves_like 'graphql update actions'
+
+ it_behaves_like 'when the snippet is not found'
end
describe 'ProjectSnippet' do
@@ -142,5 +145,7 @@ describe 'Updating a Snippet' do
end
end
end
+
+ it_behaves_like 'when the snippet is not found'
end
end
diff --git a/spec/requests/api/graphql/project/alert_management/alert_status_counts_spec.rb b/spec/requests/api/graphql/project/alert_management/alert_status_counts_spec.rb
new file mode 100644
index 00000000000..ffd328429ef
--- /dev/null
+++ b/spec/requests/api/graphql/project/alert_management/alert_status_counts_spec.rb
@@ -0,0 +1,61 @@
+# frozen_string_literal: true
+require 'spec_helper'
+
+describe 'getting Alert Management Alert counts by status' do
+ include GraphqlHelpers
+
+ let_it_be(:project) { create(:project, :repository) }
+ let_it_be(:current_user) { create(:user) }
+ let_it_be(:alert_1) { create(:alert_management_alert, :resolved, project: project) }
+ let_it_be(:alert_2) { create(:alert_management_alert, project: project) }
+ let_it_be(:other_project_alert) { create(:alert_management_alert) }
+ let(:params) { {} }
+
+ let(:fields) do
+ <<~QUERY
+ #{all_graphql_fields_for('AlertManagementAlertStatusCountsType'.classify)}
+ QUERY
+ end
+
+ let(:query) do
+ graphql_query_for(
+ 'project',
+ { 'fullPath' => project.full_path },
+ query_graphql_field('alertManagementAlertStatusCounts', params, fields)
+ )
+ end
+
+ context 'with alert data' do
+ let(:alert_counts) { graphql_data.dig('project', 'alertManagementAlertStatusCounts') }
+
+ context 'without project permissions' do
+ let(:user) { create(:user) }
+
+ before do
+ post_graphql(query, current_user: current_user)
+ end
+
+ it_behaves_like 'a working graphql query'
+ it { expect(alert_counts).to be nil }
+ end
+
+ context 'with project permissions' do
+ before do
+ project.add_developer(current_user)
+ post_graphql(query, current_user: current_user)
+ end
+
+ it_behaves_like 'a working graphql query'
+ it 'returns the correct counts for each status' do
+ expect(alert_counts).to eq(
+ 'open' => 1,
+ 'all' => 2,
+ 'triggered' => 1,
+ 'acknowledged' => 0,
+ 'resolved' => 1,
+ 'ignored' => 0
+ )
+ end
+ end
+ end
+end
diff --git a/spec/requests/api/graphql/project/alert_management/alerts_spec.rb b/spec/requests/api/graphql/project/alert_management/alerts_spec.rb
new file mode 100644
index 00000000000..c226e659364
--- /dev/null
+++ b/spec/requests/api/graphql/project/alert_management/alerts_spec.rb
@@ -0,0 +1,139 @@
+# frozen_string_literal: true
+require 'spec_helper'
+
+describe 'getting Alert Management Alerts' do
+ include GraphqlHelpers
+
+ let_it_be(:payload) { { 'custom' => { 'alert' => 'payload' } } }
+ let_it_be(:project) { create(:project, :repository) }
+ let_it_be(:current_user) { create(:user) }
+ let_it_be(:resolved_alert) { create(:alert_management_alert, :all_fields, :resolved, project: project, issue: nil, severity: :low) }
+ let_it_be(:triggered_alert) { create(:alert_management_alert, :all_fields, project: project, severity: :critical, payload: payload) }
+ let_it_be(:other_project_alert) { create(:alert_management_alert, :all_fields) }
+ let(:params) { {} }
+
+ let(:fields) do
+ <<~QUERY
+ nodes {
+ #{all_graphql_fields_for('AlertManagementAlert'.classify)}
+ }
+ QUERY
+ end
+
+ let(:query) do
+ graphql_query_for(
+ 'project',
+ { 'fullPath' => project.full_path },
+ query_graphql_field('alertManagementAlerts', params, fields)
+ )
+ end
+
+ context 'with alert data' do
+ let(:alerts) { graphql_data.dig('project', 'alertManagementAlerts', 'nodes') }
+
+ context 'without project permissions' do
+ let(:user) { create(:user) }
+
+ before do
+ post_graphql(query, current_user: current_user)
+ end
+
+ it_behaves_like 'a working graphql query'
+
+ it { expect(alerts).to be nil }
+ end
+
+ context 'with project permissions' do
+ before do
+ project.add_developer(current_user)
+ post_graphql(query, current_user: current_user)
+ end
+
+ let(:first_alert) { alerts.first }
+ let(:second_alert) { alerts.second }
+
+ it_behaves_like 'a working graphql query'
+
+ it { expect(alerts.size).to eq(2) }
+
+ it 'returns the correct properties of the alerts' do
+ expect(first_alert).to include(
+ 'iid' => triggered_alert.iid.to_s,
+ 'issueIid' => triggered_alert.issue_iid.to_s,
+ 'title' => triggered_alert.title,
+ 'description' => triggered_alert.description,
+ 'severity' => triggered_alert.severity.upcase,
+ 'status' => 'TRIGGERED',
+ 'monitoringTool' => triggered_alert.monitoring_tool,
+ 'service' => triggered_alert.service,
+ 'hosts' => triggered_alert.hosts,
+ 'eventCount' => triggered_alert.events,
+ 'startedAt' => triggered_alert.started_at.strftime('%Y-%m-%dT%H:%M:%SZ'),
+ 'endedAt' => nil,
+ 'details' => { 'custom.alert' => 'payload' },
+ 'createdAt' => triggered_alert.created_at.strftime('%Y-%m-%dT%H:%M:%SZ'),
+ 'updatedAt' => triggered_alert.updated_at.strftime('%Y-%m-%dT%H:%M:%SZ')
+ )
+
+ expect(second_alert).to include(
+ 'iid' => resolved_alert.iid.to_s,
+ 'issueIid' => nil,
+ 'status' => 'RESOLVED',
+ 'endedAt' => resolved_alert.ended_at.strftime('%Y-%m-%dT%H:%M:%SZ')
+ )
+ end
+
+ context 'with iid given' do
+ let(:params) { { iid: resolved_alert.iid.to_s } }
+
+ it_behaves_like 'a working graphql query'
+
+ it { expect(alerts.size).to eq(1) }
+ it { expect(first_alert['iid']).to eq(resolved_alert.iid.to_s) }
+ end
+
+ context 'with statuses given' do
+ let(:params) { 'statuses: [TRIGGERED, ACKNOWLEDGED]' }
+
+ it_behaves_like 'a working graphql query'
+
+ it { expect(alerts.size).to eq(1) }
+ it { expect(first_alert['iid']).to eq(triggered_alert.iid.to_s) }
+ end
+
+ context 'sorting data given' do
+ let(:params) { 'sort: SEVERITY_DESC' }
+ let(:iids) { alerts.map { |a| a['iid'] } }
+
+ it_behaves_like 'a working graphql query'
+
+ it 'sorts in the correct order' do
+ expect(iids).to eq [resolved_alert.iid.to_s, triggered_alert.iid.to_s]
+ end
+
+ context 'ascending order' do
+ let(:params) { 'sort: SEVERITY_ASC' }
+
+ it 'sorts in the correct order' do
+ expect(iids).to eq [triggered_alert.iid.to_s, resolved_alert.iid.to_s]
+ end
+ end
+ end
+
+ context 'searching' do
+ let(:params) { { search: resolved_alert.title } }
+
+ it_behaves_like 'a working graphql query'
+
+ it { expect(alerts.size).to eq(1) }
+ it { expect(first_alert['iid']).to eq(resolved_alert.iid.to_s) }
+
+ context 'unknown criteria' do
+ let(:params) { { search: 'something random' } }
+
+ it { expect(alerts.size).to eq(0) }
+ end
+ end
+ end
+ end
+end
diff --git a/spec/requests/api/graphql/project/grafana_integration_spec.rb b/spec/requests/api/graphql/project/grafana_integration_spec.rb
index e7155934b3a..c9bc6c1a68e 100644
--- a/spec/requests/api/graphql/project/grafana_integration_spec.rb
+++ b/spec/requests/api/graphql/project/grafana_integration_spec.rb
@@ -35,7 +35,7 @@ describe 'Getting Grafana Integration' do
it_behaves_like 'a working graphql query'
- it { expect(integration_data).to be nil }
+ specify { expect(integration_data).to be nil }
end
context 'with project admin permissions' do
@@ -45,16 +45,16 @@ describe 'Getting Grafana Integration' do
it_behaves_like 'a working graphql query'
- it { expect(integration_data['token']).to eql grafana_integration.masked_token }
- it { expect(integration_data['grafanaUrl']).to eql grafana_integration.grafana_url }
+ specify { expect(integration_data['token']).to eql grafana_integration.masked_token }
+ specify { expect(integration_data['grafanaUrl']).to eql grafana_integration.grafana_url }
- it do
+ specify do
expect(
integration_data['createdAt']
).to eql grafana_integration.created_at.strftime('%Y-%m-%dT%H:%M:%SZ')
end
- it do
+ specify do
expect(
integration_data['updatedAt']
).to eql grafana_integration.updated_at.strftime('%Y-%m-%dT%H:%M:%SZ')
diff --git a/spec/requests/api/graphql/project/issue/design_collection/version_spec.rb b/spec/requests/api/graphql/project/issue/design_collection/version_spec.rb
new file mode 100644
index 00000000000..04f445b4318
--- /dev/null
+++ b/spec/requests/api/graphql/project/issue/design_collection/version_spec.rb
@@ -0,0 +1,216 @@
+# frozen_string_literal: true
+
+require 'spec_helper'
+
+describe 'Query.project(fullPath).issue(iid).designCollection.version(sha)' do
+ include GraphqlHelpers
+ include DesignManagementTestHelpers
+
+ let_it_be(:project) { create(:project) }
+ let_it_be(:issue) { create(:issue, project: project) }
+ let_it_be(:developer) { create(:user) }
+ let_it_be(:stranger) { create(:user) }
+ let_it_be(:old_version) do
+ create(:design_version, issue: issue,
+ created_designs: create_list(:design, 3, issue: issue))
+ end
+ let_it_be(:version) do
+ create(:design_version, issue: issue,
+ modified_designs: old_version.designs,
+ created_designs: create_list(:design, 2, issue: issue))
+ end
+
+ let(:current_user) { developer }
+
+ def query(vq = version_fields)
+ graphql_query_for(:project, { fullPath: project.full_path },
+ query_graphql_field(:issue, { iid: issue.iid.to_s },
+ query_graphql_field(:design_collection, nil,
+ query_graphql_field(:version, { sha: version.sha }, vq))))
+ end
+
+ let(:post_query) { post_graphql(query, current_user: current_user) }
+ let(:path_prefix) { %w[project issue designCollection version] }
+
+ let(:data) { graphql_data.dig(*path) }
+
+ before do
+ enable_design_management
+ project.add_developer(developer)
+ end
+
+ describe 'scalar fields' do
+ let(:path) { path_prefix }
+ let(:version_fields) { query_graphql_field(:sha) }
+
+ before do
+ post_query
+ end
+
+ { id: ->(x) { x.to_global_id.to_s }, sha: ->(x) { x.sha } }.each do |field, value|
+ describe ".#{field}" do
+ let(:version_fields) { query_graphql_field(field) }
+
+ it "retrieves the #{field}" do
+ expect(data).to match(a_hash_including(field.to_s => value[version]))
+ end
+ end
+ end
+ end
+
+ describe 'design_at_version' do
+ let(:path) { path_prefix + %w[designAtVersion] }
+ let(:design) { issue.designs.visible_at_version(version).to_a.sample }
+ let(:design_at_version) { build(:design_at_version, design: design, version: version) }
+
+ let(:version_fields) do
+ query_graphql_field(:design_at_version, dav_params, 'id filename')
+ end
+
+ shared_examples :finds_dav do
+ it 'finds all the designs as of the given version' do
+ post_query
+
+ expect(data).to match(
+ a_hash_including(
+ 'id' => global_id_of(design_at_version),
+ 'filename' => design.filename
+ ))
+ end
+
+ context 'when the current_user is not authorized' do
+ let(:current_user) { stranger }
+
+ it 'returns nil' do
+ post_query
+
+ expect(data).to be_nil
+ end
+ end
+ end
+
+ context 'by ID' do
+ let(:dav_params) { { id: global_id_of(design_at_version) } }
+
+ include_examples :finds_dav
+ end
+
+ context 'by filename' do
+ let(:dav_params) { { filename: design.filename } }
+
+ include_examples :finds_dav
+ end
+
+ context 'by design_id' do
+ let(:dav_params) { { design_id: global_id_of(design) } }
+
+ include_examples :finds_dav
+ end
+ end
+
+ describe 'designs_at_version' do
+ let(:path) { path_prefix + %w[designsAtVersion edges] }
+ let(:version_fields) do
+ query_graphql_field(:designs_at_version, dav_params, 'edges { node { id filename } }')
+ end
+
+ let(:dav_params) { nil }
+
+ let(:results) do
+ issue.designs.visible_at_version(version).map do |d|
+ dav = build(:design_at_version, design: d, version: version)
+ { 'id' => global_id_of(dav), 'filename' => d.filename }
+ end
+ end
+
+ it 'finds all the designs as of the given version' do
+ post_query
+
+ expect(data.pluck('node')).to match_array(results)
+ end
+
+ describe 'filtering' do
+ let(:designs) { issue.designs.sample(3) }
+ let(:filenames) { designs.map(&:filename) }
+ let(:ids) do
+ designs.map { |d| global_id_of(build(:design_at_version, design: d, version: version)) }
+ end
+
+ before do
+ post_query
+ end
+
+ describe 'by filename' do
+ let(:dav_params) { { filenames: filenames } }
+
+ it 'finds the designs by filename' do
+ expect(data.map { |e| e.dig('node', 'id') }).to match_array(ids)
+ end
+ end
+
+ describe 'by design-id' do
+ let(:dav_params) { { ids: designs.map { |d| global_id_of(d) } } }
+
+ it 'finds the designs by id' do
+ expect(data.map { |e| e.dig('node', 'filename') }).to match_array(filenames)
+ end
+ end
+ end
+
+ describe 'pagination' do
+ let(:end_cursor) { graphql_data_at(*path_prefix, :designs_at_version, :page_info, :end_cursor) }
+
+ let(:ids) do
+ ::DesignManagement::Design.visible_at_version(version).order(:id).map do |d|
+ global_id_of(build(:design_at_version, design: d, version: version))
+ end
+ end
+
+ let(:version_fields) do
+ query_graphql_field(:designs_at_version, { first: 2 }, fields)
+ end
+
+ let(:cursored_query) do
+ frag = query_graphql_field(:designs_at_version, { after: end_cursor }, fields)
+ query(frag)
+ end
+
+ let(:fields) { ['pageInfo { endCursor }', 'edges { node { id } }'] }
+
+ def response_values(data = graphql_data)
+ data.dig(*path).map { |e| e.dig('node', 'id') }
+ end
+
+ it 'sorts designs for reliable pagination' do
+ post_graphql(query, current_user: current_user)
+
+ expect(response_values).to match_array(ids.take(2))
+
+ post_graphql(cursored_query, current_user: current_user)
+
+ new_data = Gitlab::Json.parse(response.body).fetch('data')
+
+ expect(response_values(new_data)).to match_array(ids.drop(2))
+ end
+ end
+ end
+
+ describe 'designs' do
+ let(:path) { path_prefix + %w[designs edges] }
+ let(:version_fields) do
+ query_graphql_field(:designs, nil, 'edges { node { id filename } }')
+ end
+
+ let(:results) do
+ version.designs.map do |design|
+ { 'id' => global_id_of(design), 'filename' => design.filename }
+ end
+ end
+
+ it 'finds all the designs as of the given version' do
+ post_query
+
+ expect(data.pluck('node')).to match_array(results)
+ end
+ end
+end
diff --git a/spec/requests/api/graphql/project/issue/design_collection/versions_spec.rb b/spec/requests/api/graphql/project/issue/design_collection/versions_spec.rb
new file mode 100644
index 00000000000..18787bf925d
--- /dev/null
+++ b/spec/requests/api/graphql/project/issue/design_collection/versions_spec.rb
@@ -0,0 +1,113 @@
+# frozen_string_literal: true
+
+require 'spec_helper'
+
+describe 'Getting versions related to an issue' do
+ include GraphqlHelpers
+ include DesignManagementTestHelpers
+
+ let_it_be(:issue) { create(:issue) }
+
+ let_it_be(:version_a) do
+ create(:design_version, issue: issue)
+ end
+ let_it_be(:version_b) do
+ create(:design_version, issue: issue)
+ end
+ let_it_be(:version_c) do
+ create(:design_version, issue: issue)
+ end
+ let_it_be(:version_d) do
+ create(:design_version, issue: issue)
+ end
+
+ let_it_be(:owner) { issue.project.owner }
+
+ def version_query(params = version_params)
+ query_graphql_field(:versions, params, version_query_fields)
+ end
+
+ let(:version_params) { nil }
+
+ let(:version_query_fields) { ['edges { node { sha } }'] }
+
+ let(:project) { issue.project }
+ let(:current_user) { owner }
+
+ let(:query) { make_query }
+
+ def make_query(vq = version_query)
+ graphql_query_for(:project, { fullPath: project.full_path },
+ query_graphql_field(:issue, { iid: issue.iid.to_s },
+ query_graphql_field(:design_collection, {}, vq)))
+ end
+
+ let(:design_collection) do
+ graphql_data_at(:project, :issue, :design_collection)
+ end
+
+ def response_values(data = graphql_data, key = 'sha')
+ path = %w[project issue designCollection versions edges]
+ data.dig(*path).map { |e| e.dig('node', key) }
+ end
+
+ before do
+ enable_design_management
+ end
+
+ it 'returns the design filename' do
+ post_graphql(query, current_user: current_user)
+
+ expect(response_values).to match_array([version_a, version_b, version_c, version_d].map(&:sha))
+ end
+
+ describe 'filter by sha' do
+ let(:sha) { version_b.sha }
+
+ let(:version_params) { { earlier_or_equal_to_sha: sha } }
+
+ it 'finds only those versions at or before the given cut-off' do
+ post_graphql(query, current_user: current_user)
+
+ expect(response_values).to contain_exactly(version_a.sha, version_b.sha)
+ end
+ end
+
+ describe 'filter by id' do
+ let(:id) { global_id_of(version_c) }
+
+ let(:version_params) { { earlier_or_equal_to_id: id } }
+
+ it 'finds only those versions at or before the given cut-off' do
+ post_graphql(query, current_user: current_user)
+
+ expect(response_values).to contain_exactly(version_a.sha, version_b.sha, version_c.sha)
+ end
+ end
+
+ describe 'pagination' do
+ let(:end_cursor) { design_collection.dig('versions', 'pageInfo', 'endCursor') }
+
+ let(:ids) { issue.design_collection.versions.ordered.map(&:sha) }
+
+ let(:query) { make_query(version_query(first: 2)) }
+
+ let(:cursored_query) do
+ make_query(version_query(after: end_cursor))
+ end
+
+ let(:version_query_fields) { ['pageInfo { endCursor }', 'edges { node { sha } }'] }
+
+ it 'sorts designs for reliable pagination' do
+ post_graphql(query, current_user: current_user)
+
+ expect(response_values).to match_array(ids.take(2))
+
+ post_graphql(cursored_query, current_user: current_user)
+
+ new_data = Gitlab::Json.parse(response.body).fetch('data')
+
+ expect(response_values(new_data)).to match_array(ids.drop(2))
+ end
+ end
+end
diff --git a/spec/requests/api/graphql/project/issue/designs/designs_spec.rb b/spec/requests/api/graphql/project/issue/designs/designs_spec.rb
new file mode 100644
index 00000000000..b6fd0d91bda
--- /dev/null
+++ b/spec/requests/api/graphql/project/issue/designs/designs_spec.rb
@@ -0,0 +1,388 @@
+# frozen_string_literal: true
+
+require 'spec_helper'
+
+describe 'Getting designs related to an issue' do
+ include GraphqlHelpers
+ include DesignManagementTestHelpers
+
+ let_it_be(:design) { create(:design, :with_smaller_image_versions, versions_count: 1) }
+ let_it_be(:current_user) { design.project.owner }
+ let(:design_query) do
+ <<~NODE
+ designs {
+ edges {
+ node {
+ id
+ filename
+ fullPath
+ event
+ image
+ imageV432x230
+ }
+ }
+ }
+ NODE
+ end
+ let(:issue) { design.issue }
+ let(:project) { issue.project }
+ let(:query) { make_query }
+ let(:design_collection) do
+ graphql_data_at(:project, :issue, :design_collection)
+ end
+ let(:design_response) do
+ design_collection.dig('designs', 'edges').first['node']
+ end
+
+ def make_query(dq = design_query)
+ designs_field = query_graphql_field(:design_collection, {}, dq)
+ issue_field = query_graphql_field(:issue, { iid: issue.iid.to_s }, designs_field)
+
+ graphql_query_for(:project, { fullPath: project.full_path }, issue_field)
+ end
+
+ def design_image_url(design, ref: nil, size: nil)
+ Gitlab::UrlBuilder.build(design, ref: ref, size: size)
+ end
+
+ context 'when the feature is available' do
+ before do
+ enable_design_management
+ end
+
+ it 'returns the design properties correctly' do
+ version_sha = design.versions.first.sha
+
+ post_graphql(query, current_user: current_user)
+
+ expect(design_response).to eq(
+ 'id' => design.to_global_id.to_s,
+ 'event' => 'CREATION',
+ 'fullPath' => design.full_path,
+ 'filename' => design.filename,
+ 'image' => design_image_url(design, ref: version_sha),
+ 'imageV432x230' => design_image_url(design, ref: version_sha, size: :v432x230)
+ )
+ end
+
+ context 'when the v432x230-sized design image has not been processed' do
+ before do
+ allow_next_instance_of(DesignManagement::DesignV432x230Uploader) do |uploader|
+ allow(uploader).to receive(:file).and_return(nil)
+ end
+ end
+
+ it 'returns nil for the v432x230-sized design image' do
+ post_graphql(query, current_user: current_user)
+
+ expect(design_response['imageV432x230']).to be_nil
+ end
+ end
+
+ describe 'pagination' do
+ before do
+ create_list(:design, 5, :with_file, issue: issue)
+ project.add_developer(current_user)
+ post_graphql(query, current_user: current_user)
+ end
+
+ let(:issue) { create(:issue) }
+
+ let(:end_cursor) { design_collection.dig('designs', 'pageInfo', 'endCursor') }
+
+ let(:ids) { issue.designs.order(:id).map { |d| global_id_of(d) } }
+
+ let(:query) { make_query(designs_fragment(first: 2)) }
+
+ let(:design_query_fields) { 'pageInfo { endCursor } edges { node { id } }' }
+
+ let(:cursored_query) do
+ make_query(designs_fragment(after: end_cursor))
+ end
+
+ def designs_fragment(params)
+ query_graphql_field(:designs, params, design_query_fields)
+ end
+
+ def response_ids(data = graphql_data)
+ path = %w[project issue designCollection designs edges]
+ data.dig(*path).map { |e| e.dig('node', 'id') }
+ end
+
+ it 'sorts designs for reliable pagination' do
+ expect(response_ids).to match_array(ids.take(2))
+
+ post_graphql(cursored_query, current_user: current_user)
+
+ new_data = Gitlab::Json.parse(response.body).fetch('data')
+
+ expect(response_ids(new_data)).to match_array(ids.drop(2))
+ end
+ end
+
+ context 'with versions' do
+ let_it_be(:version) { design.versions.take }
+ let(:design_query) do
+ <<~NODE
+ designs {
+ edges {
+ node {
+ filename
+ versions {
+ edges {
+ node {
+ id
+ sha
+ }
+ }
+ }
+ }
+ }
+ }
+ NODE
+ end
+
+ it 'includes the version id' do
+ post_graphql(query, current_user: current_user)
+
+ version_id = design_response['versions']['edges'].first['node']['id']
+
+ expect(version_id).to eq(version.to_global_id.to_s)
+ end
+
+ it 'includes the version sha' do
+ post_graphql(query, current_user: current_user)
+
+ version_sha = design_response['versions']['edges'].first['node']['sha']
+
+ expect(version_sha).to eq(version.sha)
+ end
+ end
+
+ describe 'viewing a design board at a particular version' do
+ let_it_be(:issue) { design.issue }
+ let_it_be(:second_design, reload: true) { create(:design, :with_smaller_image_versions, issue: issue, versions_count: 1) }
+ let_it_be(:deleted_design) { create(:design, :with_versions, issue: issue, deleted: true, versions_count: 1) }
+ let(:all_versions) { issue.design_versions.ordered.reverse }
+ let(:design_query) do
+ <<~NODE
+ designs(atVersion: "#{version.to_global_id}") {
+ edges {
+ node {
+ id
+ image
+ imageV432x230
+ event
+ versions {
+ edges {
+ node {
+ id
+ }
+ }
+ }
+ }
+ }
+ }
+ NODE
+ end
+ let(:design_response) do
+ design_collection['designs']['edges']
+ end
+
+ def global_id(object)
+ object.to_global_id.to_s
+ end
+
+ # Filters just design nodes from the larger `design_response`
+ def design_nodes
+ design_response.map do |response|
+ response['node']
+ end
+ end
+
+ # Filters just version nodes from the larger `design_response`
+ def version_nodes
+ design_response.map do |response|
+ response.dig('node', 'versions', 'edges')
+ end
+ end
+
+ context 'viewing the original version, when one design was created' do
+ let(:version) { all_versions.first }
+
+ before do
+ post_graphql(query, current_user: current_user)
+ end
+
+ it 'only returns the first design' do
+ expect(design_nodes).to contain_exactly(
+ a_hash_including('id' => global_id(design))
+ )
+ end
+
+ it 'returns the correct full-sized design image' do
+ expect(design_nodes).to contain_exactly(
+ a_hash_including('image' => design_image_url(design, ref: version.sha))
+ )
+ end
+
+ it 'returns the correct v432x230-sized design image' do
+ expect(design_nodes).to contain_exactly(
+ a_hash_including('imageV432x230' => design_image_url(design, ref: version.sha, size: :v432x230))
+ )
+ end
+
+ it 'returns the correct event for the design in this version' do
+ expect(design_nodes).to contain_exactly(
+ a_hash_including('event' => 'CREATION')
+ )
+ end
+
+ it 'only returns one version record for the design (the original version)' do
+ expect(version_nodes).to eq([
+ [{ 'node' => { 'id' => global_id(version) } }]
+ ])
+ end
+ end
+
+ context 'viewing the second version, when one design was created' do
+ let(:version) { all_versions.second }
+
+ before do
+ post_graphql(query, current_user: current_user)
+ end
+
+ it 'only returns the first two designs' do
+ expect(design_nodes).to contain_exactly(
+ a_hash_including('id' => global_id(design)),
+ a_hash_including('id' => global_id(second_design))
+ )
+ end
+
+ it 'returns the correct full-sized design images' do
+ expect(design_nodes).to contain_exactly(
+ a_hash_including('image' => design_image_url(design, ref: version.sha)),
+ a_hash_including('image' => design_image_url(second_design, ref: version.sha))
+ )
+ end
+
+ it 'returns the correct v432x230-sized design images' do
+ expect(design_nodes).to contain_exactly(
+ a_hash_including('imageV432x230' => design_image_url(design, ref: version.sha, size: :v432x230)),
+ a_hash_including('imageV432x230' => design_image_url(second_design, ref: version.sha, size: :v432x230))
+ )
+ end
+
+ it 'returns the correct events for the designs in this version' do
+ expect(design_nodes).to contain_exactly(
+ a_hash_including('event' => 'NONE'),
+ a_hash_including('event' => 'CREATION')
+ )
+ end
+
+ it 'returns the correct versions records for both designs' do
+ expect(version_nodes).to eq([
+ [{ 'node' => { 'id' => global_id(design.versions.first) } }],
+ [{ 'node' => { 'id' => global_id(second_design.versions.first) } }]
+ ])
+ end
+ end
+
+ context 'viewing the last version, when one design was deleted and one was updated' do
+ let(:version) { all_versions.last }
+ let!(:second_design_update) do
+ create(:design_action, :with_image_v432x230, design: second_design, version: version, event: 'modification')
+ end
+
+ before do
+ post_graphql(query, current_user: current_user)
+ end
+
+ it 'does not include the deleted design' do
+ # The design does exist in the version
+ expect(version.designs).to include(deleted_design)
+
+ # But the GraphQL API does not include it in these results
+ expect(design_nodes).to contain_exactly(
+ a_hash_including('id' => global_id(design)),
+ a_hash_including('id' => global_id(second_design))
+ )
+ end
+
+ it 'returns the correct full-sized design images' do
+ expect(design_nodes).to contain_exactly(
+ a_hash_including('image' => design_image_url(design, ref: version.sha)),
+ a_hash_including('image' => design_image_url(second_design, ref: version.sha))
+ )
+ end
+
+ it 'returns the correct v432x230-sized design images' do
+ expect(design_nodes).to contain_exactly(
+ a_hash_including('imageV432x230' => design_image_url(design, ref: version.sha, size: :v432x230)),
+ a_hash_including('imageV432x230' => design_image_url(second_design, ref: version.sha, size: :v432x230))
+ )
+ end
+
+ it 'returns the correct events for the designs in this version' do
+ expect(design_nodes).to contain_exactly(
+ a_hash_including('event' => 'NONE'),
+ a_hash_including('event' => 'MODIFICATION')
+ )
+ end
+
+ it 'returns all versions records for the designs' do
+ expect(version_nodes).to eq([
+ [
+ { 'node' => { 'id' => global_id(design.versions.first) } }
+ ],
+ [
+ { 'node' => { 'id' => global_id(second_design.versions.second) } },
+ { 'node' => { 'id' => global_id(second_design.versions.first) } }
+ ]
+ ])
+ end
+ end
+ end
+
+ describe 'a design with note annotations' do
+ let_it_be(:note) { create(:diff_note_on_design, noteable: design) }
+
+ let(:design_query) do
+ <<~NODE
+ designs {
+ edges {
+ node {
+ notesCount
+ notes {
+ edges {
+ node {
+ id
+ }
+ }
+ }
+ }
+ }
+ }
+ NODE
+ end
+
+ let(:design_response) do
+ design_collection['designs']['edges'].first['node']
+ end
+
+ before do
+ post_graphql(query, current_user: current_user)
+ end
+
+ it 'returns the notes for the design' do
+ expect(design_response.dig('notes', 'edges')).to eq(
+ ['node' => { 'id' => note.to_global_id.to_s }]
+ )
+ end
+
+ it 'returns a note_count for the design' do
+ expect(design_response['notesCount']).to eq(1)
+ end
+ end
+ end
+end
diff --git a/spec/requests/api/graphql/project/issue/designs/notes_spec.rb b/spec/requests/api/graphql/project/issue/designs/notes_spec.rb
new file mode 100644
index 00000000000..0207bb9123a
--- /dev/null
+++ b/spec/requests/api/graphql/project/issue/designs/notes_spec.rb
@@ -0,0 +1,70 @@
+# frozen_string_literal: true
+
+require 'spec_helper'
+
+describe 'Getting designs related to an issue' do
+ include GraphqlHelpers
+ include DesignManagementTestHelpers
+
+ let_it_be(:project) { create(:project, :public) }
+ let_it_be(:issue) { create(:issue, project: project) }
+ let_it_be(:design) { create(:design, :with_file, versions_count: 1, issue: issue) }
+ let_it_be(:current_user) { project.owner }
+ let_it_be(:note) { create(:diff_note_on_design, noteable: design, project: project) }
+
+ before do
+ enable_design_management
+
+ note
+ end
+
+ it_behaves_like 'a working graphql query' do
+ before do
+ post_graphql(query, current_user: current_user)
+ end
+ end
+
+ it 'is not too deep for anonymous users' do
+ note_fields = <<~FIELDS
+ id
+ author { name }
+ FIELDS
+
+ post_graphql(query(note_fields), current_user: nil)
+
+ designs_data = graphql_data['project']['issue']['designs']['designs']
+ design_data = designs_data['edges'].first['node']
+ note_data = design_data['notes']['edges'].first['node']
+
+ expect(note_data['id']).to eq(note.to_global_id.to_s)
+ end
+
+ def query(note_fields = all_graphql_fields_for(Note))
+ design_node = <<~NODE
+ designs {
+ edges {
+ node {
+ notes {
+ edges {
+ node {
+ #{note_fields}
+ }
+ }
+ }
+ }
+ }
+ }
+ NODE
+ graphql_query_for(
+ 'project',
+ { 'fullPath' => design.project.full_path },
+ query_graphql_field(
+ 'issue',
+ { iid: design.issue.iid.to_s },
+ query_graphql_field(
+ 'designs', {}, design_node
+ )
+ )
+ )
+ end
+end
diff --git a/spec/requests/api/graphql/project/issue_spec.rb b/spec/requests/api/graphql/project/issue_spec.rb
new file mode 100644
index 00000000000..92d2f9d0d31
--- /dev/null
+++ b/spec/requests/api/graphql/project/issue_spec.rb
@@ -0,0 +1,189 @@
+# frozen_string_literal: true
+
+require 'spec_helper'
+
+describe 'Query.project(fullPath).issue(iid)' do
+ include GraphqlHelpers
+
+ let_it_be(:project) { create(:project) }
+ let_it_be(:issue) { create(:issue, project: project) }
+ let_it_be(:issue_b) { create(:issue, project: project) }
+ let_it_be(:developer) { create(:user) }
+ let(:current_user) { developer }
+
+ let_it_be(:project_params) { { 'fullPath' => project.full_path } }
+ let_it_be(:issue_params) { { 'iid' => issue.iid.to_s } }
+ let_it_be(:issue_fields) { 'title' }
+
+ let(:query) do
+ graphql_query_for('project', project_params, project_fields)
+ end
+
+ let(:project_fields) do
+ query_graphql_field(:issue, issue_params, issue_fields)
+ end
+
+ shared_examples 'being able to fetch a design-like object by ID' do
+ let(:design) { design_a }
+ let(:path) { %w[project issue designCollection] + [GraphqlHelpers.fieldnamerize(object_field_name)] }
+
+ let(:design_fields) do
+ [
+ query_graphql_field(:filename),
+ query_graphql_field(:project, nil, query_graphql_field(:id))
+ ]
+ end
+
+ let(:design_collection_fields) do
+ query_graphql_field(object_field_name, object_params, object_fields)
+ end
+
+ let(:object_fields) { design_fields }
+
+ context 'the ID is passed' do
+ let(:object_params) { { id: global_id_of(object) } }
+ let(:result_fields) { {} }
+
+ let(:expected_fields) do
+ result_fields.merge({ 'filename' => design.filename, 'project' => id_hash(project) })
+ end
+
+ it 'retrieves the object' do
+ post_query
+
+ data = graphql_data.dig(*path)
+
+ expect(data).to match(a_hash_including(expected_fields))
+ end
+
+ context 'the user is unauthorized' do
+ let(:current_user) { create(:user) }
+
+ it_behaves_like 'a failure to find anything'
+ end
+ end
+
+ context 'without parameters' do
+ let(:object_params) { nil }
+
+ it 'raises an error' do
+ post_query
+
+ expect(graphql_errors).to include(no_argument_error)
+ end
+ end
+
+ context 'attempting to retrieve an object from a different issue' do
+ let(:object_params) { { id: global_id_of(object_on_other_issue) } }
+
+ it_behaves_like 'a failure to find anything'
+ end
+ end
+
+ before do
+ project.add_developer(developer)
+ end
+
+ let(:post_query) { post_graphql(query, current_user: current_user) }
+
+ describe '.designCollection' do
+ include DesignManagementTestHelpers
+
+ let_it_be(:design_a) { create(:design, issue: issue) }
+ let_it_be(:version_a) { create(:design_version, issue: issue, created_designs: [design_a]) }
+
+ let(:issue_fields) do
+ query_graphql_field(:design_collection, dc_params, design_collection_fields)
+ end
+
+ let(:dc_params) { nil }
+ let(:design_collection_fields) { nil }
+
+ before do
+ enable_design_management
+ end
+
+ describe '.design' do
+ let(:object) { design }
+ let(:object_field_name) { :design }
+
+ let(:no_argument_error) do
+ custom_graphql_error(path, a_string_matching(%r/id or filename/))
+ end
+
+ let_it_be(:object_on_other_issue) { create(:design, issue: issue_b) }
+
+ it_behaves_like 'being able to fetch a design-like object by ID'
+
+ it_behaves_like 'being able to fetch a design-like object by ID' do
+ let(:object_params) { { filename: design.filename } }
+ end
+ end
+
+ describe '.version' do
+ let(:version) { version_a }
+ let(:path) { %w[project issue designCollection version] }
+
+ let(:design_collection_fields) do
+ query_graphql_field(:version, version_params, 'id sha')
+ end
+
+ context 'no parameters' do
+ let(:version_params) { nil }
+
+ it 'raises an error' do
+ post_query
+
+ expect(graphql_errors).to include(custom_graphql_error(path, a_string_matching(%r/id or sha/)))
+ end
+ end
+
+ shared_examples 'a successful query for a version' do
+ it 'finds the version' do
+ post_query
+
+ data = graphql_data.dig(*path)
+
+ expect(data).to match(
+ a_hash_including('id' => global_id_of(version),
+ 'sha' => version.sha)
+ )
+ end
+ end
+
+ context '(sha: STRING_TYPE)' do
+ let(:version_params) { { sha: version.sha } }
+
+ it_behaves_like 'a successful query for a version'
+ end
+
+ context '(id: ID_TYPE)' do
+ let(:version_params) { { id: global_id_of(version) } }
+
+ it_behaves_like 'a successful query for a version'
+ end
+ end
+
+ describe '.designAtVersion' do
+ it_behaves_like 'being able to fetch a design-like object by ID' do
+ let(:object) { build(:design_at_version, design: design, version: version) }
+ let(:object_field_name) { :design_at_version }
+
+ let(:version) { version_a }
+
+ let(:result_fields) { { 'version' => id_hash(version) } }
+ let(:object_fields) do
+ design_fields + [query_graphql_field(:version, nil, query_graphql_field(:id))]
+ end
+
+ let(:no_argument_error) { missing_required_argument(path, :id) }
+
+ let(:object_on_other_issue) { build(:design_at_version, issue: issue_b) }
+ end
+ end
+ end
+
+ def id_hash(object)
+ a_hash_including('id' => global_id_of(object))
+ end
+end
diff --git a/spec/requests/api/graphql/project/issues_spec.rb b/spec/requests/api/graphql/project/issues_spec.rb
index 4ce7a3912a3..91fce3eed92 100644
--- a/spec/requests/api/graphql/project/issues_spec.rb
+++ b/spec/requests/api/graphql/project/issues_spec.rb
@@ -45,8 +45,8 @@ describe 'getting an issue list for a project' do
it 'includes discussion locked' do
post_graphql(query, current_user: current_user)
- expect(issues_data[0]['node']['discussionLocked']).to eq false
- expect(issues_data[1]['node']['discussionLocked']).to eq true
+ expect(issues_data[0]['node']['discussionLocked']).to eq(false)
+ expect(issues_data[1]['node']['discussionLocked']).to eq(true)
end
context 'when limiting the number of results' do
@@ -79,7 +79,7 @@ describe 'getting an issue list for a project' do
post_graphql(query)
- expect(issues_data).to eq []
+ expect(issues_data).to eq([])
end
end
@@ -118,131 +118,138 @@ describe 'getting an issue list for a project' do
end
describe 'sorting and pagination' do
- let(:start_cursor) { graphql_data['project']['issues']['pageInfo']['startCursor'] }
- let(:end_cursor) { graphql_data['project']['issues']['pageInfo']['endCursor'] }
+ let_it_be(:data_path) { [:project, :issues] }
- context 'when sorting by due date' do
- let(:sort_project) { create(:project, :public) }
-
- let!(:due_issue1) { create(:issue, project: sort_project, due_date: 3.days.from_now) }
- let!(:due_issue2) { create(:issue, project: sort_project, due_date: nil) }
- let!(:due_issue3) { create(:issue, project: sort_project, due_date: 2.days.ago) }
- let!(:due_issue4) { create(:issue, project: sort_project, due_date: nil) }
- let!(:due_issue5) { create(:issue, project: sort_project, due_date: 1.day.ago) }
-
- let(:params) { 'sort: DUE_DATE_ASC' }
-
- def query(issue_params = params)
- graphql_query_for(
- 'project',
- { 'fullPath' => sort_project.full_path },
- <<~ISSUES
- issues(#{issue_params}) {
- pageInfo {
- endCursor
- }
- edges {
- node {
- iid
- dueDate
- }
- }
- }
- ISSUES
- )
- end
+ def pagination_query(params, page_info)
+ graphql_query_for(
+ 'project',
+ { 'fullPath' => sort_project.full_path },
+ "issues(#{params}) { #{page_info} edges { node { iid dueDate } } }"
+ )
+ end
- before do
- post_graphql(query, current_user: current_user)
- end
+ def pagination_results_data(data)
+ data.map { |issue| issue.dig('node', 'iid').to_i }
+ end
- it_behaves_like 'a working graphql query'
+ context 'when sorting by due date' do
+ let_it_be(:sort_project) { create(:project, :public) }
+ let_it_be(:due_issue1) { create(:issue, project: sort_project, due_date: 3.days.from_now) }
+ let_it_be(:due_issue2) { create(:issue, project: sort_project, due_date: nil) }
+ let_it_be(:due_issue3) { create(:issue, project: sort_project, due_date: 2.days.ago) }
+ let_it_be(:due_issue4) { create(:issue, project: sort_project, due_date: nil) }
+ let_it_be(:due_issue5) { create(:issue, project: sort_project, due_date: 1.day.ago) }
context 'when ascending' do
- it 'sorts issues' do
- expect(grab_iids).to eq [due_issue3.iid, due_issue5.iid, due_issue1.iid, due_issue4.iid, due_issue2.iid]
- end
-
- context 'when paginating' do
- let(:params) { 'sort: DUE_DATE_ASC, first: 2' }
-
- it 'sorts issues' do
- expect(grab_iids).to eq [due_issue3.iid, due_issue5.iid]
-
- cursored_query = query("sort: DUE_DATE_ASC, after: \"#{end_cursor}\"")
- post_graphql(cursored_query, current_user: current_user)
- response_data = JSON.parse(response.body)['data']['project']['issues']['edges']
-
- expect(grab_iids(response_data)).to eq [due_issue1.iid, due_issue4.iid, due_issue2.iid]
- end
+ it_behaves_like 'sorted paginated query' do
+ let(:sort_param) { 'DUE_DATE_ASC' }
+ let(:first_param) { 2 }
+ let(:expected_results) { [due_issue3.iid, due_issue5.iid, due_issue1.iid, due_issue4.iid, due_issue2.iid] }
end
end
context 'when descending' do
- let(:params) { 'sort: DUE_DATE_DESC' }
-
- it 'sorts issues' do
- expect(grab_iids).to eq [due_issue1.iid, due_issue5.iid, due_issue3.iid, due_issue4.iid, due_issue2.iid]
+ it_behaves_like 'sorted paginated query' do
+ let(:sort_param) { 'DUE_DATE_DESC' }
+ let(:first_param) { 2 }
+ let(:expected_results) { [due_issue1.iid, due_issue5.iid, due_issue3.iid, due_issue4.iid, due_issue2.iid] }
end
+ end
+ end
- context 'when paginating' do
- let(:params) { 'sort: DUE_DATE_DESC, first: 2' }
-
- it 'sorts issues' do
- expect(grab_iids).to eq [due_issue1.iid, due_issue5.iid]
-
- cursored_query = query("sort: DUE_DATE_DESC, after: \"#{end_cursor}\"")
- post_graphql(cursored_query, current_user: current_user)
- response_data = JSON.parse(response.body)['data']['project']['issues']['edges']
+ context 'when sorting by relative position' do
+ let_it_be(:sort_project) { create(:project, :public) }
+ let_it_be(:relative_issue1) { create(:issue, project: sort_project, relative_position: 2000) }
+ let_it_be(:relative_issue2) { create(:issue, project: sort_project, relative_position: nil) }
+ let_it_be(:relative_issue3) { create(:issue, project: sort_project, relative_position: 1000) }
+ let_it_be(:relative_issue4) { create(:issue, project: sort_project, relative_position: nil) }
+ let_it_be(:relative_issue5) { create(:issue, project: sort_project, relative_position: 500) }
- expect(grab_iids(response_data)).to eq [due_issue3.iid, due_issue4.iid, due_issue2.iid]
- end
+ context 'when ascending' do
+ it_behaves_like 'sorted paginated query' do
+ let(:sort_param) { 'RELATIVE_POSITION_ASC' }
+ let(:first_param) { 2 }
+ let(:expected_results) { [relative_issue5.iid, relative_issue3.iid, relative_issue1.iid, relative_issue4.iid, relative_issue2.iid] }
end
end
end
- context 'when sorting by relative position' do
- let(:sort_project) { create(:project, :public) }
-
- let!(:relative_issue1) { create(:issue, project: sort_project, relative_position: 2000) }
- let!(:relative_issue2) { create(:issue, project: sort_project, relative_position: nil) }
- let!(:relative_issue3) { create(:issue, project: sort_project, relative_position: 1000) }
- let!(:relative_issue4) { create(:issue, project: sort_project, relative_position: nil) }
- let!(:relative_issue5) { create(:issue, project: sort_project, relative_position: 500) }
-
- let(:params) { 'sort: RELATIVE_POSITION_ASC' }
-
- def query(issue_params = params)
- graphql_query_for(
- 'project',
- { 'fullPath' => sort_project.full_path },
- "issues(#{issue_params}) { pageInfo { endCursor} edges { node { iid dueDate } } }"
- )
+ context 'when sorting by priority' do
+ let_it_be(:sort_project) { create(:project, :public) }
+ let_it_be(:early_milestone) { create(:milestone, project: sort_project, due_date: 10.days.from_now) }
+ let_it_be(:late_milestone) { create(:milestone, project: sort_project, due_date: 30.days.from_now) }
+ let_it_be(:priority_label1) { create(:label, project: sort_project, priority: 1) }
+ let_it_be(:priority_label2) { create(:label, project: sort_project, priority: 5) }
+ let_it_be(:priority_issue1) { create(:issue, project: sort_project, labels: [priority_label1], milestone: late_milestone) }
+ let_it_be(:priority_issue2) { create(:issue, project: sort_project, labels: [priority_label2]) }
+ let_it_be(:priority_issue3) { create(:issue, project: sort_project, milestone: early_milestone) }
+ let_it_be(:priority_issue4) { create(:issue, project: sort_project) }
+
+ context 'when ascending' do
+ it_behaves_like 'sorted paginated query' do
+ let(:sort_param) { 'PRIORITY_ASC' }
+ let(:first_param) { 2 }
+ let(:expected_results) { [priority_issue3.iid, priority_issue1.iid, priority_issue2.iid, priority_issue4.iid] }
+ end
end
- before do
- post_graphql(query, current_user: current_user)
+ context 'when descending' do
+ it_behaves_like 'sorted paginated query' do
+ let(:sort_param) { 'PRIORITY_DESC' }
+ let(:first_param) { 2 }
+ let(:expected_results) { [priority_issue1.iid, priority_issue3.iid, priority_issue2.iid, priority_issue4.iid] }
+ end
end
+ end
- it_behaves_like 'a working graphql query'
+ context 'when sorting by label priority' do
+ let_it_be(:sort_project) { create(:project, :public) }
+ let_it_be(:label1) { create(:label, project: sort_project, priority: 1) }
+ let_it_be(:label2) { create(:label, project: sort_project, priority: 5) }
+ let_it_be(:label3) { create(:label, project: sort_project, priority: 10) }
+ let_it_be(:label_issue1) { create(:issue, project: sort_project, labels: [label1]) }
+ let_it_be(:label_issue2) { create(:issue, project: sort_project, labels: [label2]) }
+ let_it_be(:label_issue3) { create(:issue, project: sort_project, labels: [label1, label3]) }
+ let_it_be(:label_issue4) { create(:issue, project: sort_project) }
context 'when ascending' do
- it 'sorts issues' do
- expect(grab_iids).to eq [relative_issue5.iid, relative_issue3.iid, relative_issue1.iid, relative_issue4.iid, relative_issue2.iid]
+ it_behaves_like 'sorted paginated query' do
+ let(:sort_param) { 'LABEL_PRIORITY_ASC' }
+ let(:first_param) { 2 }
+ let(:expected_results) { [label_issue3.iid, label_issue1.iid, label_issue2.iid, label_issue4.iid] }
end
+ end
- context 'when paginating' do
- let(:params) { 'sort: RELATIVE_POSITION_ASC, first: 2' }
+ context 'when descending' do
+ it_behaves_like 'sorted paginated query' do
+ let(:sort_param) { 'LABEL_PRIORITY_DESC' }
+ let(:first_param) { 2 }
+ let(:expected_results) { [label_issue2.iid, label_issue3.iid, label_issue1.iid, label_issue4.iid] }
+ end
+ end
+ end
- it 'sorts issues' do
- expect(grab_iids).to eq [relative_issue5.iid, relative_issue3.iid]
+ context 'when sorting by milestone due date' do
+ let_it_be(:sort_project) { create(:project, :public) }
+ let_it_be(:early_milestone) { create(:milestone, project: sort_project, due_date: 10.days.from_now) }
+ let_it_be(:late_milestone) { create(:milestone, project: sort_project, due_date: 30.days.from_now) }
+ let_it_be(:milestone_issue1) { create(:issue, project: sort_project) }
+ let_it_be(:milestone_issue2) { create(:issue, project: sort_project, milestone: early_milestone) }
+ let_it_be(:milestone_issue3) { create(:issue, project: sort_project, milestone: late_milestone) }
- cursored_query = query("sort: RELATIVE_POSITION_ASC, after: \"#{end_cursor}\"")
- post_graphql(cursored_query, current_user: current_user)
- response_data = JSON.parse(response.body)['data']['project']['issues']['edges']
+ context 'when ascending' do
+ it_behaves_like 'sorted paginated query' do
+ let(:sort_param) { 'MILESTONE_DUE_ASC' }
+ let(:first_param) { 2 }
+ let(:expected_results) { [milestone_issue2.iid, milestone_issue3.iid, milestone_issue1.iid] }
+ end
+ end
- expect(grab_iids(response_data)).to eq [relative_issue1.iid, relative_issue4.iid, relative_issue2.iid]
- end
+ context 'when descending' do
+ it_behaves_like 'sorted paginated query' do
+ let(:sort_param) { 'MILESTONE_DUE_DESC' }
+ let(:first_param) { 2 }
+ let(:expected_results) { [milestone_issue3.iid, milestone_issue2.iid, milestone_issue1.iid] }
end
end
end
diff --git a/spec/requests/api/graphql/project/jira_import_spec.rb b/spec/requests/api/graphql/project/jira_import_spec.rb
index 43e1bb13342..e063068eb1a 100644
--- a/spec/requests/api/graphql/project/jira_import_spec.rb
+++ b/spec/requests/api/graphql/project/jira_import_spec.rb
@@ -2,7 +2,7 @@
require 'spec_helper'
-describe 'query jira import data' do
+describe 'query Jira import data' do
include GraphqlHelpers
let_it_be(:current_user) { create(:user) }
@@ -18,6 +18,7 @@ describe 'query jira import data' do
jiraImports {
nodes {
jiraProjectKey
+ createdAt
scheduledAt
scheduledBy {
username
diff --git a/spec/requests/api/graphql/query_spec.rb b/spec/requests/api/graphql/query_spec.rb
new file mode 100644
index 00000000000..26b4c6eafd7
--- /dev/null
+++ b/spec/requests/api/graphql/query_spec.rb
@@ -0,0 +1,95 @@
+# frozen_string_literal: true
+
+require 'spec_helper'
+
+describe 'Query' do
+ include GraphqlHelpers
+
+ let_it_be(:project) { create(:project) }
+ let_it_be(:issue) { create(:issue, project: project) }
+ let_it_be(:developer) { create(:user) }
+ let(:current_user) { developer }
+
+ describe '.designManagement' do
+ include DesignManagementTestHelpers
+
+ let_it_be(:version) { create(:design_version, issue: issue) }
+ let_it_be(:design) { version.designs.first }
+ let(:query_result) { graphql_data.dig(*path) }
+ let(:query) { graphql_query_for(:design_management, nil, dm_fields) }
+
+ before do
+ enable_design_management
+ project.add_developer(developer)
+ post_graphql(query, current_user: current_user)
+ end
+
+ shared_examples 'a query that needs authorization' do
+ context 'the current user is not able to read designs' do
+ let(:current_user) { create(:user) }
+
+ it 'does not retrieve the record' do
+ expect(query_result).to be_nil
+ end
+
+ it 'raises an error' do
+ expect(graphql_errors).to include(
+ a_hash_including('message' => a_string_matching(%r{you don't have permission}))
+ )
+ end
+ end
+ end
+
+ describe '.version' do
+ let(:path) { %w[designManagement version] }
+
+ let(:dm_fields) do
+ query_graphql_field(:version, { 'id' => global_id_of(version) }, 'id sha')
+ end
+
+ it_behaves_like 'a working graphql query'
+ it_behaves_like 'a query that needs authorization'
+
+ context 'the current user is able to read designs' do
+ it 'fetches the expected data' do
+ expect(query_result).to eq('id' => global_id_of(version), 'sha' => version.sha)
+ end
+ end
+ end
+
+ describe '.designAtVersion' do
+ let_it_be(:design_at_version) do
+ ::DesignManagement::DesignAtVersion.new(design: design, version: version)
+ end
+
+ let(:path) { %w[designManagement designAtVersion] }
+
+ let(:dm_fields) do
+ query_graphql_field(:design_at_version, { 'id' => global_id_of(design_at_version) }, <<~FIELDS)
+ id
+ filename
+ version { id sha }
+ design { id }
+ issue { title iid }
+ project { id fullPath }
+ FIELDS
+ end
+
+ it_behaves_like 'a working graphql query'
+ it_behaves_like 'a query that needs authorization'
+
+ context 'the current user is able to read designs' do
+ it 'fetches the expected data, including the correct associations' do
+ expect(query_result).to eq(
+ 'id' => global_id_of(design_at_version),
+ 'filename' => design_at_version.design.filename,
+ 'version' => { 'id' => global_id_of(version), 'sha' => version.sha },
+ 'design' => { 'id' => global_id_of(design) },
+ 'issue' => { 'title' => issue.title, 'iid' => issue.iid.to_s },
+ 'project' => { 'id' => global_id_of(project), 'fullPath' => project.full_path }
+ )
+ end
+ end
+ end
+ end
+end
diff --git a/spec/requests/api/graphql_spec.rb b/spec/requests/api/graphql_spec.rb
index 783dd730dd9..f5c7a820abe 100644
--- a/spec/requests/api/graphql_spec.rb
+++ b/spec/requests/api/graphql_spec.rb
@@ -9,7 +9,7 @@ describe 'GraphQL' do
context 'logging' do
shared_examples 'logging a graphql query' do
let(:expected_params) do
- { query_string: query, variables: variables.to_s, duration: anything, depth: 1, complexity: 1 }
+ { query_string: query, variables: variables.to_s, duration_s: anything, depth: 1, complexity: 1 }
end
it 'logs a query with the expected params' do
diff --git a/spec/requests/api/groups_spec.rb b/spec/requests/api/groups_spec.rb
index 30c1f99569b..18feff85482 100644
--- a/spec/requests/api/groups_spec.rb
+++ b/spec/requests/api/groups_spec.rb
@@ -6,15 +6,15 @@ describe API::Groups do
include GroupAPIHelpers
include UploadHelpers
- let(:user1) { create(:user, can_create_group: false) }
- let(:user2) { create(:user) }
- let(:user3) { create(:user) }
- let(:admin) { create(:admin) }
- let!(:group1) { create(:group, avatar: File.open(uploaded_image_temp_path)) }
- let!(:group2) { create(:group, :private) }
- let!(:project1) { create(:project, namespace: group1) }
- let!(:project2) { create(:project, namespace: group2) }
- let!(:project3) { create(:project, namespace: group1, path: 'test', visibility_level: Gitlab::VisibilityLevel::PRIVATE) }
+ let_it_be(:user1) { create(:user, can_create_group: false) }
+ let_it_be(:user2) { create(:user) }
+ let_it_be(:user3) { create(:user) }
+ let_it_be(:admin) { create(:admin) }
+ let_it_be(:group1) { create(:group, avatar: File.open(uploaded_image_temp_path)) }
+ let_it_be(:group2) { create(:group, :private) }
+ let_it_be(:project1) { create(:project, namespace: group1) }
+ let_it_be(:project2) { create(:project, namespace: group2) }
+ let_it_be(:project3) { create(:project, namespace: group1, path: 'test', visibility_level: Gitlab::VisibilityLevel::PRIVATE) }
before do
group1.add_owner(user1)
@@ -90,6 +90,17 @@ describe API::Groups do
get api("/groups", admin)
end.not_to exceed_query_limit(control)
end
+
+ context 'when statistics are requested' do
+ it 'does not include statistics' do
+ get api("/groups"), params: { statistics: true }
+
+ expect(response).to have_gitlab_http_status(:ok)
+ expect(response).to include_pagination_headers
+ expect(json_response).to be_an Array
+ expect(json_response.first).not_to include 'statistics'
+ end
+ end
end
context "when authenticated as user" do
@@ -330,7 +341,7 @@ describe API::Groups do
expect(response).to have_gitlab_http_status(:ok)
expect(response).to include_pagination_headers
expect(json_response).to be_an Array
- expect(response_groups).to eq([group2.id, group3.id])
+ expect(response_groups).to contain_exactly(group2.id, group3.id)
end
end
end
@@ -642,6 +653,33 @@ describe API::Groups do
expect(json_response['default_branch_protection']).to eq(::Gitlab::Access::MAINTAINER_PROJECT_ACCESS)
end
+ context 'updating the `default_branch_protection` attribute' do
+ subject do
+ put api("/groups/#{group1.id}", user1), params: { default_branch_protection: ::Gitlab::Access::PROTECTION_NONE }
+ end
+
+ context 'for users who have the ability to update default_branch_protection' do
+ it 'updates the attribute' do
+ subject
+
+ expect(response).to have_gitlab_http_status(:ok)
+ expect(json_response['default_branch_protection']).to eq(Gitlab::Access::PROTECTION_NONE)
+ end
+ end
+
+ context 'for users who does not have the ability to update default_branch_protection`' do
+ it 'does not update the attribute' do
+ allow(Ability).to receive(:allowed?).and_call_original
+ allow(Ability).to receive(:allowed?).with(user1, :update_default_branch_protection, group1) { false }
+
+ subject
+
+ expect(response).to have_gitlab_http_status(:ok)
+ expect(json_response['default_branch_protection']).not_to eq(Gitlab::Access::PROTECTION_NONE)
+ end
+ end
+ end
+
context 'malicious group name' do
subject { put api("/groups/#{group1.id}", user1), params: { name: "<SCRIPT>alert('DOUBLE-ATTACK!')</SCRIPT>" } }
@@ -889,6 +927,181 @@ describe API::Groups do
end
end
+ describe "GET /groups/:id/projects/shared" do
+ let!(:project4) do
+ create(:project, namespace: group2, path: 'test_project', visibility_level: Gitlab::VisibilityLevel::PRIVATE)
+ end
+ let(:path) { "/groups/#{group1.id}/projects/shared" }
+
+ before do
+ create(:project_group_link, project: project2, group: group1)
+ create(:project_group_link, project: project4, group: group1)
+ end
+
+ context 'when authenticated as user' do
+ it 'returns the shared projects in the group' do
+ get api(path, user1)
+
+ expect(response).to have_gitlab_http_status(:ok)
+ expect(response).to include_pagination_headers
+ expect(json_response.length).to eq(2)
+ project_ids = json_response.map { |project| project['id'] }
+ expect(project_ids).to match_array([project2.id, project4.id])
+ expect(json_response.first['visibility']).to be_present
+ end
+
+ it 'returns shared projects with min access level or higher' do
+ user = create(:user)
+
+ project2.add_guest(user)
+ project4.add_reporter(user)
+
+ get api(path, user), params: { min_access_level: Gitlab::Access::REPORTER }
+
+ expect(json_response).to be_an(Array)
+ expect(json_response.length).to eq(1)
+ expect(json_response.first['id']).to eq(project4.id)
+ end
+
+ it 'returns the shared projects of the group with simple representation' do
+ get api(path, user1), params: { simple: true }
+
+ expect(response).to have_gitlab_http_status(:ok)
+ expect(response).to include_pagination_headers
+ expect(json_response.length).to eq(2)
+ project_ids = json_response.map { |project| project['id'] }
+ expect(project_ids).to match_array([project2.id, project4.id])
+ expect(json_response.first['visibility']).not_to be_present
+ end
+
+ it 'filters the shared projects in the group based on visibility' do
+ internal_project = create(:project, :internal, namespace: create(:group))
+
+ create(:project_group_link, project: internal_project, group: group1)
+
+ get api(path, user1), params: { visibility: 'internal' }
+
+ expect(response).to have_gitlab_http_status(:ok)
+ expect(response).to include_pagination_headers
+ expect(json_response).to be_an(Array)
+ expect(json_response.length).to eq(1)
+ expect(json_response.first['id']).to eq(internal_project.id)
+ end
+
+ it 'filters the shared projects in the group based on search params' do
+ get api(path, user1), params: { search: 'test_project' }
+
+ expect(response).to have_gitlab_http_status(:ok)
+ expect(response).to include_pagination_headers
+ expect(json_response).to be_an(Array)
+ expect(json_response.length).to eq(1)
+ expect(json_response.first['id']).to eq(project4.id)
+ end
+
+ it 'does not return the projects owned by the group' do
+ get api(path, user1)
+
+ expect(response).to have_gitlab_http_status(:ok)
+ expect(response).to include_pagination_headers
+ expect(json_response).to be_an(Array)
+ project_ids = json_response.map { |project| project['id'] }
+
+ expect(project_ids).not_to include(project1.id)
+ end
+
+ it 'returns 404 for a non-existing group' do
+ get api("/groups/0000/projects/shared", user1)
+
+ expect(response).to have_gitlab_http_status(:not_found)
+ end
+
+ it 'does not return a group not attached to the user' do
+ group = create(:group, :private)
+
+ get api("/groups/#{group.id}/projects/shared", user1)
+
+ expect(response).to have_gitlab_http_status(:not_found)
+ end
+
+ it 'only returns shared projects to which user has access' do
+ project4.add_developer(user3)
+
+ get api(path, user3)
+
+ expect(response).to have_gitlab_http_status(:ok)
+ expect(response).to include_pagination_headers
+ expect(json_response.length).to eq(1)
+ expect(json_response.first['id']).to eq(project4.id)
+ end
+
+ it 'only returns the projects starred by user' do
+ user1.starred_projects = [project2]
+
+ get api(path, user1), params: { starred: true }
+
+ expect(response).to have_gitlab_http_status(:ok)
+ expect(json_response.length).to eq(1)
+ expect(json_response.first['id']).to eq(project2.id)
+ end
+ end
+
+ context "when authenticated as admin" do
+ subject { get api(path, admin) }
+
+ it "returns shared projects of an existing group" do
+ subject
+
+ expect(response).to have_gitlab_http_status(:ok)
+ expect(response).to include_pagination_headers
+ expect(json_response.length).to eq(2)
+ project_ids = json_response.map { |project| project['id'] }
+ expect(project_ids).to match_array([project2.id, project4.id])
+ end
+
+ context 'for a non-existent group' do
+ let(:path) { "/groups/000/projects/shared" }
+
+ it 'returns 404 for a non-existent group' do
+ subject
+
+ expect(response).to have_gitlab_http_status(:not_found)
+ end
+ end
+
+ it 'avoids N+1 queries' do
+ control_count = ActiveRecord::QueryRecorder.new do
+ subject
+ end.count
+
+ create(:project_group_link, project: create(:project), group: group1)
+
+ expect do
+ subject
+ end.not_to exceed_query_limit(control_count)
+ end
+ end
+
+ context 'when using group path in URL' do
+ let(:path) { "/groups/#{group1.path}/projects/shared" }
+
+ it 'returns the right details' do
+ get api(path, admin)
+
+ expect(response).to have_gitlab_http_status(:ok)
+ expect(response).to include_pagination_headers
+ expect(json_response.length).to eq(2)
+ project_ids = json_response.map { |project| project['id'] }
+ expect(project_ids).to match_array([project2.id, project4.id])
+ end
+
+ it 'returns 404 for a non-existent group' do
+ get api('/groups/unknown/projects/shared', admin)
+
+ expect(response).to have_gitlab_http_status(:not_found)
+ end
+ end
+ end
+
describe 'GET /groups/:id/subgroups' do
let!(:subgroup1) { create(:group, parent: group1) }
let!(:subgroup2) { create(:group, :private, parent: group1) }
@@ -911,6 +1124,17 @@ describe API::Groups do
expect(response).to have_gitlab_http_status(:not_found)
end
+
+ context 'when statistics are requested' do
+ it 'does not include statistics' do
+ get api("/groups/#{group1.id}/subgroups"), params: { statistics: true }
+
+ expect(response).to have_gitlab_http_status(:ok)
+ expect(response).to include_pagination_headers
+ expect(json_response).to be_an Array
+ expect(json_response.first).not_to include 'statistics'
+ end
+ end
end
context 'when authenticated as user' do
@@ -1111,6 +1335,33 @@ describe API::Groups do
it { expect { subject }.not_to change { Group.count } }
end
+ context 'when creating a group with `default_branch_protection` attribute' do
+ let(:params) { attributes_for_group_api default_branch_protection: Gitlab::Access::PROTECTION_NONE }
+
+ subject { post api("/groups", user3), params: params }
+
+ context 'for users who have the ability to create a group with `default_branch_protection`' do
+ it 'creates group with the specified branch protection level' do
+ subject
+
+ expect(response).to have_gitlab_http_status(:created)
+ expect(json_response['default_branch_protection']).to eq(Gitlab::Access::PROTECTION_NONE)
+ end
+ end
+
+ context 'for users who do not have the ability to create a group with `default_branch_protection`' do
+ it 'does not create the group with the specified branch protection level' do
+ allow(Ability).to receive(:allowed?).and_call_original
+ allow(Ability).to receive(:allowed?).with(user3, :create_group_with_default_branch_protection) { false }
+
+ subject
+
+ expect(response).to have_gitlab_http_status(:created)
+ expect(json_response['default_branch_protection']).not_to eq(Gitlab::Access::PROTECTION_NONE)
+ end
+ end
+ end
+
it "does not create group, duplicate" do
post api("/groups", user3), params: { name: 'Duplicate Test', path: group2.path }
diff --git a/spec/requests/api/helpers_spec.rb b/spec/requests/api/helpers_spec.rb
index 98904a4d79f..d65c89f48ea 100644
--- a/spec/requests/api/helpers_spec.rb
+++ b/spec/requests/api/helpers_spec.rb
@@ -328,6 +328,8 @@ describe API::Helpers do
it 'returns a 401 response' do
expect { authenticate! }.to raise_error /401/
+
+ expect(env[described_class::API_RESPONSE_STATUS_CODE]).to eq(401)
end
end
@@ -340,6 +342,8 @@ describe API::Helpers do
it 'does not raise an error' do
expect { authenticate! }.not_to raise_error
+
+ expect(env[described_class::API_RESPONSE_STATUS_CODE]).to be_nil
end
end
end
diff --git a/spec/requests/api/internal/base_spec.rb b/spec/requests/api/internal/base_spec.rb
index 93c2233e021..684f0329909 100644
--- a/spec/requests/api/internal/base_spec.rb
+++ b/spec/requests/api/internal/base_spec.rb
@@ -323,18 +323,6 @@ describe API::Internal::Base do
end
end
- shared_examples 'snippets with disabled feature flag' do
- context 'when feature flag :version_snippets is disabled' do
- it 'returns 401' do
- stub_feature_flags(version_snippets: false)
-
- subject
-
- expect(response).to have_gitlab_http_status(:unauthorized)
- end
- end
- end
-
shared_examples 'snippet success' do
it 'responds with success' do
subject
@@ -344,18 +332,6 @@ describe API::Internal::Base do
end
end
- shared_examples 'snippets with web protocol' do
- it_behaves_like 'snippet success'
-
- context 'with disabled version flag' do
- before do
- stub_feature_flags(version_snippets: false)
- end
-
- it_behaves_like 'snippet success'
- end
- end
-
context 'git push with personal snippet' do
subject { push(key, personal_snippet, env: env.to_json, changes: snippet_changes) }
@@ -369,12 +345,6 @@ describe API::Internal::Base do
expect(user.reload.last_activity_on).to be_nil
end
- it_behaves_like 'snippets with disabled feature flag'
-
- it_behaves_like 'snippets with web protocol' do
- subject { push(key, personal_snippet, 'web', env: env.to_json, changes: snippet_changes) }
- end
-
it_behaves_like 'sets hook env' do
let(:gl_repository) { Gitlab::GlRepository::SNIPPET.identifier_for_container(personal_snippet) }
end
@@ -392,12 +362,6 @@ describe API::Internal::Base do
expect(json_response["gl_repository"]).to eq("snippet-#{personal_snippet.id}")
expect(user.reload.last_activity_on).to eql(Date.today)
end
-
- it_behaves_like 'snippets with disabled feature flag'
-
- it_behaves_like 'snippets with web protocol' do
- subject { pull(key, personal_snippet, 'web') }
- end
end
context 'git push with project snippet' do
@@ -413,12 +377,6 @@ describe API::Internal::Base do
expect(user.reload.last_activity_on).to be_nil
end
- it_behaves_like 'snippets with disabled feature flag'
-
- it_behaves_like 'snippets with web protocol' do
- subject { push(key, project_snippet, 'web', env: env.to_json, changes: snippet_changes) }
- end
-
it_behaves_like 'sets hook env' do
let(:gl_repository) { Gitlab::GlRepository::SNIPPET.identifier_for_container(project_snippet) }
end
@@ -434,14 +392,6 @@ describe API::Internal::Base do
expect(json_response["gl_repository"]).to eq("snippet-#{project_snippet.id}")
expect(user.reload.last_activity_on).to eql(Date.today)
end
-
- it_behaves_like 'snippets with disabled feature flag' do
- subject { pull(key, project_snippet) }
- end
-
- it_behaves_like 'snippets with web protocol' do
- subject { pull(key, project_snippet, 'web') }
- end
end
context "git pull" do
@@ -491,23 +441,25 @@ describe API::Internal::Base do
allow(Gitlab::CurrentSettings).to receive(:receive_max_input_size) { 1 }
end
- it 'returns custom git config' do
+ it 'returns maxInputSize and partial clone git config' do
push(key, project)
expect(json_response["git_config_options"]).to be_present
+ expect(json_response["git_config_options"]).to include("receive.maxInputSize=1048576")
expect(json_response["git_config_options"]).to include("uploadpack.allowFilter=true")
expect(json_response["git_config_options"]).to include("uploadpack.allowAnySHA1InWant=true")
end
context 'when gitaly_upload_pack_filter feature flag is disabled' do
before do
- stub_feature_flags(gitaly_upload_pack_filter: { enabled: false, thing: project })
+ stub_feature_flags(gitaly_upload_pack_filter: false)
end
- it 'does not include allowFilter and allowAnySha1InWant in the git config options' do
+ it 'returns only maxInputSize and not partial clone git config' do
push(key, project)
expect(json_response["git_config_options"]).to be_present
+ expect(json_response["git_config_options"]).to include("receive.maxInputSize=1048576")
expect(json_response["git_config_options"]).not_to include("uploadpack.allowFilter=true")
expect(json_response["git_config_options"]).not_to include("uploadpack.allowAnySHA1InWant=true")
end
@@ -515,12 +467,28 @@ describe API::Internal::Base do
end
context 'when receive_max_input_size is empty' do
- it 'returns an empty git config' do
+ before do
allow(Gitlab::CurrentSettings).to receive(:receive_max_input_size) { nil }
+ end
+ it 'returns partial clone git config' do
push(key, project)
- expect(json_response["git_config_options"]).to be_empty
+ expect(json_response["git_config_options"]).to be_present
+ expect(json_response["git_config_options"]).to include("uploadpack.allowFilter=true")
+ expect(json_response["git_config_options"]).to include("uploadpack.allowAnySHA1InWant=true")
+ end
+
+ context 'when gitaly_upload_pack_filter feature flag is disabled' do
+ before do
+ stub_feature_flags(gitaly_upload_pack_filter: false)
+ end
+
+ it 'returns an empty git config' do
+ push(key, project)
+
+ expect(json_response["git_config_options"]).to be_empty
+ end
end
end
end
@@ -949,6 +917,23 @@ describe API::Internal::Base do
expect(json_response['status']).to be_falsy
end
end
+
+ context 'for design repositories' do
+ let(:gl_repository) { Gitlab::GlRepository::DESIGN.identifier_for_container(project) }
+
+ it 'does not allow access' do
+ post(api('/internal/allowed'),
+ params: {
+ key_id: key.id,
+ project: project.full_path,
+ gl_repository: gl_repository,
+ secret_token: secret_token,
+ protocol: 'ssh'
+ })
+
+ expect(response).to have_gitlab_http_status(:unauthorized)
+ end
+ end
end
describe 'POST /internal/post_receive', :clean_gitlab_redis_shared_state do
diff --git a/spec/requests/api/issues/get_group_issues_spec.rb b/spec/requests/api/issues/get_group_issues_spec.rb
index 3ec5f380390..5c925d2a32e 100644
--- a/spec/requests/api/issues/get_group_issues_spec.rb
+++ b/spec/requests/api/issues/get_group_issues_spec.rb
@@ -3,26 +3,26 @@
require 'spec_helper'
describe API::Issues do
- let_it_be(:user) { create(:user) }
- let(:user2) { create(:user) }
- let(:non_member) { create(:user) }
- let_it_be(:guest) { create(:user) }
- let_it_be(:author) { create(:author) }
- let_it_be(:assignee) { create(:assignee) }
- let(:admin) { create(:user, :admin) }
- let(:issue_title) { 'foo' }
- let(:issue_description) { 'closed' }
- let(:no_milestone_title) { 'None' }
- let(:any_milestone_title) { 'Any' }
+ let_it_be(:user2) { create(:user) }
+ let_it_be(:admin) { create(:user, :admin) }
+ let_it_be(:non_member) { create(:user) }
+ let_it_be(:user) { create(:user) }
+ let_it_be(:guest) { create(:user) }
+ let_it_be(:author) { create(:author) }
+ let_it_be(:assignee) { create(:assignee) }
+ let_it_be(:issue_title) { 'foo' }
+ let_it_be(:issue_description) { 'closed' }
+ let_it_be(:no_milestone_title) { 'None' }
+ let_it_be(:any_milestone_title) { 'Any' }
before do
stub_licensed_features(multiple_issue_assignees: false, issue_weights: false)
end
describe 'GET /groups/:id/issues' do
- let!(:group) { create(:group) }
- let!(:group_project) { create(:project, :public, :repository, creator_id: user.id, namespace: group) }
- let!(:private_mrs_project) do
+ let_it_be(:group) { create(:group) }
+ let_it_be(:group_project) { create(:project, :public, :repository, creator_id: user.id, namespace: group) }
+ let_it_be(:private_mrs_project) do
create(:project, :public, :repository, creator_id: user.id, namespace: group, merge_requests_access_level: ProjectFeature::PRIVATE)
end
@@ -455,6 +455,29 @@ describe API::Issues do
it_behaves_like 'labeled issues with labels and label_name params'
end
+ context 'with archived projects' do
+ let_it_be(:archived_issue) do
+ create(
+ :issue, author: user, assignees: [user],
+ project: create(:project, :public, :archived, creator_id: user.id, namespace: group)
+ )
+ end
+
+ it 'returns only non archived projects issues' do
+ get api(base_url, user)
+
+ expect_paginated_array_response([group_closed_issue.id, group_confidential_issue.id, group_issue.id])
+ end
+
+ it 'returns issues from archived projects if non_archived it set to false' do
+ get api(base_url, user), params: { non_archived: false }
+
+ expect_paginated_array_response(
+ [archived_issue.id, group_closed_issue.id, group_confidential_issue.id, group_issue.id]
+ )
+ end
+ end
+
it 'returns an array of issues found by iids' do
get api(base_url, user), params: { iids: [group_issue.iid] }
diff --git a/spec/requests/api/issues/issues_spec.rb b/spec/requests/api/issues/issues_spec.rb
index 00169c1529f..06878f57d43 100644
--- a/spec/requests/api/issues/issues_spec.rb
+++ b/spec/requests/api/issues/issues_spec.rb
@@ -780,28 +780,20 @@ describe API::Issues do
end
context 'filtering by non_archived' do
- let_it_be(:group1) { create(:group) }
- let_it_be(:archived_project) { create(:project, :archived, namespace: group1) }
- let_it_be(:active_project) { create(:project, namespace: group1) }
- let_it_be(:issue1) { create(:issue, project: active_project) }
- let_it_be(:issue2) { create(:issue, project: active_project) }
- let_it_be(:issue3) { create(:issue, project: archived_project) }
+ let_it_be(:archived_project) { create(:project, :archived, creator_id: user.id, namespace: user.namespace) }
+ let_it_be(:archived_issue) { create(:issue, author: user, project: archived_project) }
+ let_it_be(:active_issue) { create(:issue, author: user, project: project) }
- before do
- archived_project.add_developer(user)
- active_project.add_developer(user)
- end
-
- it 'returns issues from non archived projects only by default' do
- get api("/groups/#{group1.id}/issues", user), params: { scope: 'all' }
+ it 'returns issues from non archived projects by default' do
+ get api('/issues', user)
- expect_paginated_array_response([issue2.id, issue1.id])
+ expect_paginated_array_response(active_issue.id, issue.id, closed_issue.id)
end
- it 'returns issues from archived and non archived projects when non_archived is false' do
- get api("/groups/#{group1.id}/issues", user), params: { non_archived: false, scope: 'all' }
+ it 'returns issues from archived project with non_archived set as false' do
+ get api("/issues", user), params: { non_archived: false }
- expect_paginated_array_response([issue3.id, issue2.id, issue1.id])
+ expect_paginated_array_response(active_issue.id, archived_issue.id, issue.id, closed_issue.id)
end
end
end
diff --git a/spec/requests/api/issues/post_projects_issues_spec.rb b/spec/requests/api/issues/post_projects_issues_spec.rb
index 1444f43003f..2e1e5d3204e 100644
--- a/spec/requests/api/issues/post_projects_issues_spec.rb
+++ b/spec/requests/api/issues/post_projects_issues_spec.rb
@@ -403,7 +403,7 @@ describe API::Issues do
end
before do
- expect_next_instance_of(Spam::SpamCheckService) do |spam_service|
+ expect_next_instance_of(Spam::SpamActionService) do |spam_service|
expect(spam_service).to receive_messages(check_for_spam?: true)
end
expect_next_instance_of(Spam::AkismetService) do |akismet_service|
diff --git a/spec/requests/api/issues/put_projects_issues_spec.rb b/spec/requests/api/issues/put_projects_issues_spec.rb
index ffc5e2b1db8..2ab8b9d7877 100644
--- a/spec/requests/api/issues/put_projects_issues_spec.rb
+++ b/spec/requests/api/issues/put_projects_issues_spec.rb
@@ -182,6 +182,8 @@ describe API::Issues do
end
describe 'PUT /projects/:id/issues/:issue_iid with spam filtering' do
+ include_context 'includes Spam constants'
+
def update_issue
put api("/projects/#{project.id}/issues/#{issue.iid}", user), params: params
end
@@ -195,11 +197,12 @@ describe API::Issues do
end
before do
- expect_next_instance_of(Spam::SpamCheckService) do |spam_service|
+ expect_next_instance_of(Spam::SpamActionService) do |spam_service|
expect(spam_service).to receive_messages(check_for_spam?: true)
end
- expect_next_instance_of(Spam::AkismetService) do |akismet_service|
- expect(akismet_service).to receive_messages(spam?: true)
+
+ expect_next_instance_of(Spam::SpamVerdictService) do |verdict_service|
+ expect(verdict_service).to receive(:execute).and_return(DISALLOW)
end
end
diff --git a/spec/requests/api/merge_requests_spec.rb b/spec/requests/api/merge_requests_spec.rb
index af2ce7f7aef..14b22de9661 100644
--- a/spec/requests/api/merge_requests_spec.rb
+++ b/spec/requests/api/merge_requests_spec.rb
@@ -66,17 +66,36 @@ describe API::MergeRequests do
end
context 'when merge request is unchecked' do
+ let(:check_service_class) { MergeRequests::MergeabilityCheckService }
+ let(:mr_entity) { json_response.find { |mr| mr['id'] == merge_request.id } }
+
before do
merge_request.mark_as_unchecked!
end
- it 'checks mergeability asynchronously' do
- expect_next_instance_of(MergeRequests::MergeabilityCheckService) do |service|
- expect(service).not_to receive(:execute)
- expect(service).to receive(:async_execute)
+ context 'with merge status recheck projection' do
+ it 'checks mergeability asynchronously' do
+ expect_next_instance_of(check_service_class) do |service|
+ expect(service).not_to receive(:execute)
+ expect(service).to receive(:async_execute).and_call_original
+ end
+
+ get(api(endpoint_path, user), params: { with_merge_status_recheck: true })
+
+ expect_successful_response_with_paginated_array
+ expect(mr_entity['merge_status']).to eq('checking')
end
+ end
- get api(endpoint_path, user)
+ context 'without merge status recheck projection' do
+ it 'does not enqueue a merge status recheck' do
+ expect(check_service_class).not_to receive(:new)
+
+ get api(endpoint_path, user)
+
+ expect_successful_response_with_paginated_array
+ expect(mr_entity['merge_status']).to eq('unchecked')
+ end
end
end
@@ -776,8 +795,8 @@ describe API::MergeRequests do
end
describe "GET /groups/:id/merge_requests" do
- let!(:group) { create(:group, :public) }
- let!(:project) { create(:project, :public, :repository, creator: user, namespace: group, only_allow_merge_if_pipeline_succeeds: false) }
+ let_it_be(:group) { create(:group, :public) }
+ let_it_be(:project) { create(:project, :public, :repository, creator: user, namespace: group, only_allow_merge_if_pipeline_succeeds: false) }
let(:endpoint_path) { "/groups/#{group.id}/merge_requests" }
before do
@@ -787,9 +806,9 @@ describe API::MergeRequests do
it_behaves_like 'merge requests list'
context 'when have subgroups' do
- let!(:group) { create(:group, :public) }
- let!(:subgroup) { create(:group, parent: group) }
- let!(:project) { create(:project, :public, :repository, creator: user, namespace: subgroup, only_allow_merge_if_pipeline_succeeds: false) }
+ let_it_be(:group) { create(:group, :public) }
+ let_it_be(:subgroup) { create(:group, parent: group) }
+ let_it_be(:project) { create(:project, :public, :repository, creator: user, namespace: subgroup, only_allow_merge_if_pipeline_succeeds: false) }
it_behaves_like 'merge requests list'
end
@@ -1535,7 +1554,7 @@ describe API::MergeRequests do
end
context 'forked projects', :sidekiq_might_not_need_inline do
- let!(:user2) { create(:user) }
+ let_it_be(:user2) { create(:user) }
let(:project) { create(:project, :public, :repository) }
let!(:forked_project) { fork_project(project, user2, repository: true) }
let!(:unrelated_project) { create(:project, namespace: create(:user).namespace, creator_id: user2.id) }
@@ -2308,6 +2327,33 @@ describe API::MergeRequests do
end
end
+ context 'with labels' do
+ include_context 'with labels'
+
+ let(:api_base) { api("/projects/#{project.id}/merge_requests/#{merge_request.iid}", user) }
+
+ it 'when adding labels, keeps existing labels and adds new' do
+ put api_base, params: { add_labels: '1, 2' }
+
+ expect(response).to have_gitlab_http_status(:ok)
+ expect(json_response['labels']).to contain_exactly(label.title, label2.title, '1', '2')
+ end
+
+ it 'when removing labels, only removes those specified' do
+ put api_base, params: { remove_labels: "#{label.title}" }
+
+ expect(response).to have_gitlab_http_status(:ok)
+ expect(json_response['labels']).to eq([label2.title])
+ end
+
+ it 'when removing all labels, keeps no labels' do
+ put api_base, params: { remove_labels: "#{label.title}, #{label2.title}" }
+
+ expect(response).to have_gitlab_http_status(:ok)
+ expect(json_response['labels']).to be_empty
+ end
+ end
+
it 'does not update state when title is empty' do
put api("/projects/#{project.id}/merge_requests/#{merge_request.iid}", user), params: { state_event: 'close', title: nil }
diff --git a/spec/requests/api/metrics/dashboard/annotations_spec.rb b/spec/requests/api/metrics/dashboard/annotations_spec.rb
index 0b51c46e474..6377ef2435a 100644
--- a/spec/requests/api/metrics/dashboard/annotations_spec.rb
+++ b/spec/requests/api/metrics/dashboard/annotations_spec.rb
@@ -11,77 +11,125 @@ describe API::Metrics::Dashboard::Annotations do
let(:ending_at) { 1.hour.from_now.iso8601 }
let(:params) { attributes_for(:metrics_dashboard_annotation, environment: environment, starting_at: starting_at, ending_at: ending_at, dashboard_path: dashboard)}
- describe 'POST /environments/:environment_id/metrics_dashboard/annotations' do
- before :all do
+ shared_examples 'POST /:source_type/:id/metrics_dashboard/annotations' do |source_type|
+ let(:url) { "/#{source_type.pluralize}/#{source.id}/metrics_dashboard/annotations" }
+
+ before do
project.add_developer(user)
end
- context 'feature flag metrics_dashboard_annotations' do
- context 'is on' do
- before do
- stub_feature_flags(metrics_dashboard_annotations: { enabled: true, thing: project })
- end
- context 'with correct permissions' do
- context 'with valid parameters' do
- it 'creates a new annotation', :aggregate_failures do
- post api("/environments/#{environment.id}/metrics_dashboard/annotations", user), params: params
-
- expect(response).to have_gitlab_http_status(:created)
- expect(json_response['environment_id']).to eq(environment.id)
- expect(json_response['starting_at'].to_time).to eq(starting_at.to_time)
- expect(json_response['ending_at'].to_time).to eq(ending_at.to_time)
- expect(json_response['description']).to eq(params[:description])
- expect(json_response['dashboard_path']).to eq(dashboard)
- end
+ context "with :source_type == #{source_type.pluralize}" do
+ context 'with correct permissions' do
+ context 'with valid parameters' do
+ it 'creates a new annotation', :aggregate_failures do
+ post api(url, user), params: params
+
+ expect(response).to have_gitlab_http_status(:created)
+ expect(json_response["#{source_type}_id"]).to eq(source.id)
+ expect(json_response['starting_at'].to_time).to eq(starting_at.to_time)
+ expect(json_response['ending_at'].to_time).to eq(ending_at.to_time)
+ expect(json_response['description']).to eq(params[:description])
+ expect(json_response['dashboard_path']).to eq(dashboard)
end
+ end
- context 'with invalid parameters' do
- it 'returns error messsage' do
- post api("/environments/#{environment.id}/metrics_dashboard/annotations", user),
- params: { dashboard_path: nil, starting_at: nil, description: nil }
+ context 'with invalid parameters' do
+ it 'returns error messsage' do
+ post api(url, user), params: { dashboard_path: '', starting_at: nil, description: nil }
- expect(response).to have_gitlab_http_status(:bad_request)
- expect(json_response['message']).to include({ "starting_at" => ["can't be blank"], "description" => ["can't be blank"], "dashboard_path" => ["can't be blank"] })
- end
+ expect(response).to have_gitlab_http_status(:bad_request)
+ expect(json_response['message']).to include({ "starting_at" => ["can't be blank"], "description" => ["can't be blank"], "dashboard_path" => ["can't be blank"] })
end
+ end
- context 'with undeclared params' do
- before do
- params[:undeclared_param] = 'xyz'
- end
- it 'filters out undeclared params' do
- expect(::Metrics::Dashboard::Annotations::CreateService).to receive(:new).with(user, hash_excluding(:undeclared_param))
+ context 'with undeclared params' do
+ before do
+ params[:undeclared_param] = 'xyz'
+ end
- post api("/environments/#{environment.id}/metrics_dashboard/annotations", user), params: params
- end
+ it 'filters out undeclared params' do
+ expect(::Metrics::Dashboard::Annotations::CreateService).to receive(:new).with(user, hash_excluding(:undeclared_param))
+
+ post api(url, user), params: params
end
end
- context 'without correct permissions' do
- let_it_be(:guest) { create(:user) }
+ context 'with special characers in dashboard_path in request body' do
+ let(:dashboard_escaped) { 'config/prometheus/common_metrics%26copy.yml' }
+ let(:dashboard_unescaped) { 'config/prometheus/common_metrics&copy.yml' }
- before do
- project.add_guest(guest)
+ shared_examples 'special characters unescaped' do
+ let(:expected_params) do
+ {
+ 'starting_at' => starting_at.to_time,
+ 'ending_at' => ending_at.to_time,
+ "#{source_type}" => source,
+ 'dashboard_path' => dashboard_unescaped,
+ 'description' => params[:description]
+ }
+ end
+
+ it 'unescapes the dashboard_path', :aggregate_failures do
+ expect(::Metrics::Dashboard::Annotations::CreateService).to receive(:new).with(user, expected_params)
+
+ post api(url, user), params: params
+ end
end
- it 'returns error messsage' do
- post api("/environments/#{environment.id}/metrics_dashboard/annotations", guest), params: params
+ context 'with escaped characters' do
+ it_behaves_like 'special characters unescaped' do
+ let(:dashboard) { dashboard_escaped }
+ end
+ end
- expect(response).to have_gitlab_http_status(:forbidden)
+ context 'with unescaped characers' do
+ it_behaves_like 'special characters unescaped' do
+ let(:dashboard) { dashboard_unescaped }
+ end
end
end
end
- context 'is off' do
+
+ context 'without correct permissions' do
+ let_it_be(:guest) { create(:user) }
+
before do
- stub_feature_flags(metrics_dashboard_annotations: { enabled: false, thing: project })
+ project.add_guest(guest)
end
- it 'returns error messsage' do
- post api("/environments/#{environment.id}/metrics_dashboard/annotations", user), params: params
+ it 'returns error message' do
+ post api(url, guest), params: params
- expect(response).to have_gitlab_http_status(:not_found)
+ expect(response).to have_gitlab_http_status(:forbidden)
end
end
end
end
+
+ describe 'environment' do
+ it_behaves_like 'POST /:source_type/:id/metrics_dashboard/annotations', 'environment' do
+ let(:source) { environment }
+ end
+ end
+
+ describe 'group cluster' do
+ it_behaves_like 'POST /:source_type/:id/metrics_dashboard/annotations', 'cluster' do
+ let_it_be(:group) { create(:group) }
+ let_it_be(:cluster) { create(:cluster_for_group, groups: [group]) }
+
+ before do
+ group.add_developer(user)
+ end
+
+ let(:source) { cluster }
+ end
+ end
+
+ describe 'project cluster' do
+ it_behaves_like 'POST /:source_type/:id/metrics_dashboard/annotations', 'cluster' do
+ let_it_be(:cluster) { create(:cluster, projects: [project]) }
+
+ let(:source) { cluster }
+ end
+ end
end
diff --git a/spec/requests/api/metrics/user_starred_dashboards_spec.rb b/spec/requests/api/metrics/user_starred_dashboards_spec.rb
new file mode 100644
index 00000000000..8f9394a0e20
--- /dev/null
+++ b/spec/requests/api/metrics/user_starred_dashboards_spec.rb
@@ -0,0 +1,164 @@
+# frozen_string_literal: true
+
+require 'spec_helper'
+
+describe API::Metrics::UserStarredDashboards do
+ let_it_be(:user) { create(:user) }
+ let_it_be(:dashboard_yml) { fixture_file('lib/gitlab/metrics/dashboard/sample_dashboard.yml') }
+ let_it_be(:dashboard) { '.gitlab/dashboards/find&seek.yml' }
+ let_it_be(:project) { create(:project, :private, :repository, :custom_repo, namespace: user.namespace, files: { dashboard => dashboard_yml }) }
+ let(:url) { "/projects/#{project.id}/metrics/user_starred_dashboards" }
+ let(:params) do
+ {
+ dashboard_path: CGI.escape(dashboard)
+ }
+ end
+
+ describe 'POST /projects/:id/metrics/user_starred_dashboards' do
+ before do
+ project.add_reporter(user)
+ end
+
+ context 'with correct permissions' do
+ context 'with valid parameters' do
+ context 'dashboard_path as url param url escaped' do
+ it 'creates a new user starred metrics dashboard', :aggregate_failures do
+ post api(url, user), params: params
+
+ expect(response).to have_gitlab_http_status(:created)
+ expect(json_response['project_id']).to eq(project.id)
+ expect(json_response['user_id']).to eq(user.id)
+ expect(json_response['dashboard_path']).to eq(dashboard)
+ end
+ end
+
+ context 'dashboard_path in request body unescaped' do
+ let(:params) do
+ {
+ dashboard_path: dashboard
+ }
+ end
+
+ it 'creates a new user starred metrics dashboard', :aggregate_failures do
+ post api(url, user), params: params
+
+ expect(response).to have_gitlab_http_status(:created)
+ expect(json_response['project_id']).to eq(project.id)
+ expect(json_response['user_id']).to eq(user.id)
+ expect(json_response['dashboard_path']).to eq(dashboard)
+ end
+ end
+ end
+
+ context 'with invalid parameters' do
+ it 'returns error message' do
+ post api(url, user), params: { dashboard_path: '' }
+
+ expect(response).to have_gitlab_http_status(:bad_request)
+ expect(json_response['error']).to eq('dashboard_path is empty')
+ end
+
+ context 'user is missing' do
+ it 'returns 404 not found' do
+ post api(url, nil), params: params
+
+ expect(response).to have_gitlab_http_status(:not_found)
+ end
+ end
+
+ context 'project is missing' do
+ it 'returns 404 not found' do
+ post api("/projects/#{project.id + 1}/user_starred_dashboards", user), params: params
+
+ expect(response).to have_gitlab_http_status(:not_found)
+ end
+ end
+ end
+ end
+
+ context 'without correct permissions' do
+ it 'returns 404 not found' do
+ post api(url, create(:user)), params: params
+
+ expect(response).to have_gitlab_http_status(:not_found)
+ end
+ end
+ end
+
+ describe 'DELETE /projects/:id/metrics/user_starred_dashboards' do
+ let_it_be(:user_starred_dashboard_1) { create(:metrics_users_starred_dashboard, user: user, project: project, dashboard_path: dashboard) }
+ let_it_be(:user_starred_dashboard_2) { create(:metrics_users_starred_dashboard, user: user, project: project) }
+ let_it_be(:other_user_starred_dashboard) { create(:metrics_users_starred_dashboard, project: project) }
+ let_it_be(:other_project_starred_dashboard) { create(:metrics_users_starred_dashboard, user: user) }
+
+ before do
+ project.add_reporter(user)
+ end
+
+ context 'with correct permissions' do
+ context 'with valid parameters' do
+ context 'dashboard_path as url param url escaped' do
+ it 'deletes given user starred metrics dashboard', :aggregate_failures do
+ delete api(url, user), params: params
+
+ expect(response).to have_gitlab_http_status(:ok)
+ expect(json_response['deleted_rows']).to eq(1)
+ expect(::Metrics::UsersStarredDashboard.all.pluck(:dashboard_path)).not_to include(dashboard)
+ end
+ end
+
+ context 'dashboard_path in request body unescaped' do
+ let(:params) do
+ {
+ dashboard_path: dashboard
+ }
+ end
+
+ it 'deletes given user starred metrics dashboard', :aggregate_failures do
+ delete api(url, user), params: params
+
+ expect(response).to have_gitlab_http_status(:ok)
+ expect(json_response['deleted_rows']).to eq(1)
+ expect(::Metrics::UsersStarredDashboard.all.pluck(:dashboard_path)).not_to include(dashboard)
+ end
+ end
+
+ context 'dashboard_path has not been specified' do
+ it 'deletes all starred dashboards for that user within given project', :aggregate_failures do
+ delete api(url, user), params: {}
+
+ expect(response).to have_gitlab_http_status(:ok)
+ expect(json_response['deleted_rows']).to eq(2)
+ expect(::Metrics::UsersStarredDashboard.all).to contain_exactly(other_user_starred_dashboard, other_project_starred_dashboard)
+ end
+ end
+ end
+
+ context 'with invalid parameters' do
+ context 'user is missing' do
+ it 'returns 404 not found' do
+ post api(url, nil), params: params
+
+ expect(response).to have_gitlab_http_status(:not_found)
+ end
+ end
+
+ context 'project is missing' do
+ it 'returns 404 not found' do
+ post api("/projects/#{project.id + 1}/user_starred_dashboards", user), params: params
+
+ expect(response).to have_gitlab_http_status(:not_found)
+ end
+ end
+ end
+ end
+
+ context 'without correct permissions' do
+ it 'returns 404 not found' do
+ post api(url, create(:user)), params: params
+
+ expect(response).to have_gitlab_http_status(:not_found)
+ end
+ end
+ end
+end
diff --git a/spec/requests/api/pipeline_schedules_spec.rb b/spec/requests/api/pipeline_schedules_spec.rb
index 14b292db045..98eaf36b14e 100644
--- a/spec/requests/api/pipeline_schedules_spec.rb
+++ b/spec/requests/api/pipeline_schedules_spec.rb
@@ -67,7 +67,7 @@ describe API::PipelineSchedules do
end
def active?(str)
- (str == 'active') ? true : false
+ str == 'active'
end
end
end
diff --git a/spec/requests/api/pipelines_spec.rb b/spec/requests/api/pipelines_spec.rb
index f43fa5b4185..f57223f1de5 100644
--- a/spec/requests/api/pipelines_spec.rb
+++ b/spec/requests/api/pipelines_spec.rb
@@ -72,8 +72,8 @@ describe API::Pipelines do
end
context 'when scope is branches or tags' do
- let!(:pipeline_branch) { create(:ci_pipeline, project: project) }
- let!(:pipeline_tag) { create(:ci_pipeline, project: project, ref: 'v1.0.0', tag: true) }
+ let_it_be(:pipeline_branch) { create(:ci_pipeline, project: project) }
+ let_it_be(:pipeline_tag) { create(:ci_pipeline, project: project, ref: 'v1.0.0', tag: true) }
context 'when scope is branches' do
it 'returns matched pipelines' do
@@ -161,7 +161,7 @@ describe API::Pipelines do
end
context 'when name is specified' do
- let!(:pipeline) { create(:ci_pipeline, project: project, user: user) }
+ let_it_be(:pipeline) { create(:ci_pipeline, project: project, user: user) }
context 'when name exists' do
it 'returns matched pipelines' do
@@ -185,7 +185,7 @@ describe API::Pipelines do
end
context 'when username is specified' do
- let!(:pipeline) { create(:ci_pipeline, project: project, user: user) }
+ let_it_be(:pipeline) { create(:ci_pipeline, project: project, user: user) }
context 'when username exists' do
it 'returns matched pipelines' do
@@ -209,8 +209,8 @@ describe API::Pipelines do
end
context 'when yaml_errors is specified' do
- let!(:pipeline1) { create(:ci_pipeline, project: project, yaml_errors: 'Syntax error') }
- let!(:pipeline2) { create(:ci_pipeline, project: project) }
+ let_it_be(:pipeline1) { create(:ci_pipeline, project: project, yaml_errors: 'Syntax error') }
+ let_it_be(:pipeline2) { create(:ci_pipeline, project: project) }
context 'when yaml_errors is true' do
it 'returns matched pipelines' do
@@ -242,9 +242,9 @@ describe API::Pipelines do
end
context 'when updated_at filters are specified' do
- let!(:pipeline1) { create(:ci_pipeline, project: project, updated_at: 2.days.ago) }
- let!(:pipeline2) { create(:ci_pipeline, project: project, updated_at: 4.days.ago) }
- let!(:pipeline3) { create(:ci_pipeline, project: project, updated_at: 1.hour.ago) }
+ let_it_be(:pipeline1) { create(:ci_pipeline, project: project, updated_at: 2.days.ago) }
+ let_it_be(:pipeline2) { create(:ci_pipeline, project: project, updated_at: 4.days.ago) }
+ let_it_be(:pipeline3) { create(:ci_pipeline, project: project, updated_at: 1.hour.ago) }
it 'returns pipelines with last update date in specified datetime range' do
get api("/projects/#{project.id}/pipelines", user), params: { updated_before: 1.day.ago, updated_after: 3.days.ago }
@@ -614,7 +614,7 @@ describe API::Pipelines do
end
context 'when the pipeline has jobs' do
- let!(:build) { create(:ci_build, project: project, pipeline: pipeline) }
+ let_it_be(:build) { create(:ci_build, project: project, pipeline: pipeline) }
it 'destroys associated jobs' do
delete api("/projects/#{project.id}/pipelines/#{pipeline.id}", owner)
@@ -654,12 +654,12 @@ describe API::Pipelines do
describe 'POST /projects/:id/pipelines/:pipeline_id/retry' do
context 'authorized user' do
- let!(:pipeline) do
+ let_it_be(:pipeline) do
create(:ci_pipeline, project: project, sha: project.commit.id,
ref: project.default_branch)
end
- let!(:build) { create(:ci_build, :failed, pipeline: pipeline) }
+ let_it_be(:build) { create(:ci_build, :failed, pipeline: pipeline) }
it 'retries failed builds' do
expect do
@@ -683,12 +683,12 @@ describe API::Pipelines do
end
describe 'POST /projects/:id/pipelines/:pipeline_id/cancel' do
- let!(:pipeline) do
+ let_it_be(:pipeline) do
create(:ci_empty_pipeline, project: project, sha: project.commit.id,
ref: project.default_branch)
end
- let!(:build) { create(:ci_build, :running, pipeline: pipeline) }
+ let_it_be(:build) { create(:ci_build, :running, pipeline: pipeline) }
context 'authorized user' do
it 'retries failed builds', :sidekiq_might_not_need_inline do
@@ -700,7 +700,7 @@ describe API::Pipelines do
end
context 'user without proper access rights' do
- let!(:reporter) { create(:user) }
+ let_it_be(:reporter) { create(:user) }
before do
project.add_reporter(reporter)
@@ -714,4 +714,73 @@ describe API::Pipelines do
end
end
end
+
+ describe 'GET /projects/:id/pipelines/:pipeline_id/test_report' do
+ context 'authorized user' do
+ subject { get api("/projects/#{project.id}/pipelines/#{pipeline.id}/test_report", user) }
+
+ let(:pipeline) { create(:ci_pipeline, project: project) }
+
+ context 'when feature is enabled' do
+ before do
+ stub_feature_flags(junit_pipeline_view: true)
+ end
+
+ context 'when pipeline does not have a test report' do
+ it 'returns an empty test report' do
+ subject
+
+ expect(response).to have_gitlab_http_status(:ok)
+ expect(json_response['total_count']).to eq(0)
+ end
+ end
+
+ context 'when pipeline has a test report' do
+ let(:pipeline) { create(:ci_pipeline, :with_test_reports, project: project) }
+
+ it 'returns the test report' do
+ subject
+
+ expect(response).to have_gitlab_http_status(:ok)
+ expect(json_response['total_count']).to eq(4)
+ end
+ end
+
+ context 'when pipeline has corrupt test reports' do
+ before do
+ job = create(:ci_build, pipeline: pipeline)
+ create(:ci_job_artifact, :junit_with_corrupted_data, job: job, project: project)
+ end
+
+ it 'returns a suite_error' do
+ subject
+
+ expect(response).to have_gitlab_http_status(:ok)
+ expect(json_response['test_suites'].first['suite_error']).to eq('JUnit XML parsing failed: 1:1: FATAL: Document is empty')
+ end
+ end
+ end
+
+ context 'when feature is disabled' do
+ before do
+ stub_feature_flags(junit_pipeline_view: false)
+ end
+
+ it 'renders empty response' do
+ subject
+
+ expect(response).to have_gitlab_http_status(:not_found)
+ end
+ end
+ end
+
+ context 'unauthorized user' do
+ it 'does not return project pipelines' do
+ get api("/projects/#{project.id}/pipelines/#{pipeline.id}/test_report", non_member)
+
+ expect(response).to have_gitlab_http_status(:not_found)
+ expect(json_response['message']).to eq '404 Project Not Found'
+ end
+ end
+ end
end
diff --git a/spec/requests/api/project_export_spec.rb b/spec/requests/api/project_export_spec.rb
index 859a3cca44f..ad872b88664 100644
--- a/spec/requests/api/project_export_spec.rb
+++ b/spec/requests/api/project_export_spec.rb
@@ -411,7 +411,9 @@ describe API::ProjectExport, :clean_gitlab_redis_cache do
it 'starts', :sidekiq_might_not_need_inline do
params = { description: "Foo" }
- expect_any_instance_of(Projects::ImportExport::ExportService).to receive(:execute)
+ expect_next_instance_of(Projects::ImportExport::ExportService) do |service|
+ expect(service).to receive(:execute)
+ end
post api(path, project.owner), params: params
expect(response).to have_gitlab_http_status(:accepted)
diff --git a/spec/requests/api/project_milestones_spec.rb b/spec/requests/api/project_milestones_spec.rb
index a40878fc807..c5911d51706 100644
--- a/spec/requests/api/project_milestones_spec.rb
+++ b/spec/requests/api/project_milestones_spec.rb
@@ -24,13 +24,13 @@ describe API::ProjectMilestones do
project.add_reporter(reporter)
end
- it 'returns 404 response when the project does not exists' do
+ it 'returns 404 response when the project does not exist' do
delete api("/projects/0/milestones/#{milestone.id}", user)
expect(response).to have_gitlab_http_status(:not_found)
end
- it 'returns 404 response when the milestone does not exists' do
+ it 'returns 404 response when the milestone does not exist' do
delete api("/projects/#{project.id}/milestones/0", user)
expect(response).to have_gitlab_http_status(:not_found)
@@ -44,7 +44,7 @@ describe API::ProjectMilestones do
end
describe 'PUT /projects/:id/milestones/:milestone_id to test observer on close' do
- it 'creates an activity event when an milestone is closed' do
+ it 'creates an activity event when a milestone is closed' do
expect(Event).to receive(:create!)
put api("/projects/#{project.id}/milestones/#{milestone.id}", user),
@@ -91,7 +91,7 @@ describe API::ProjectMilestones do
end
end
- context 'when no such resources' do
+ context 'when no such resource' do
before do
group.add_developer(user)
end
diff --git a/spec/requests/api/project_repository_storage_moves_spec.rb b/spec/requests/api/project_repository_storage_moves_spec.rb
new file mode 100644
index 00000000000..7ceea0178f3
--- /dev/null
+++ b/spec/requests/api/project_repository_storage_moves_spec.rb
@@ -0,0 +1,89 @@
+# frozen_string_literal: true
+
+require 'spec_helper'
+
+describe API::ProjectRepositoryStorageMoves do
+ include AccessMatchersForRequest
+
+ let(:user) { create(:admin) }
+ let!(:storage_move) { create(:project_repository_storage_move, :scheduled) }
+
+ describe 'GET /project_repository_storage_moves' do
+ def get_project_repository_storage_moves
+ get api('/project_repository_storage_moves', user)
+ end
+
+ it 'returns project repository storage moves' do
+ get_project_repository_storage_moves
+
+ expect(response).to have_gitlab_http_status(:ok)
+ expect(response).to include_pagination_headers
+ expect(response).to match_response_schema('public_api/v4/project_repository_storage_moves')
+ expect(json_response.size).to eq(1)
+ expect(json_response.first['id']).to eq(storage_move.id)
+ expect(json_response.first['state']).to eq(storage_move.human_state_name)
+ end
+
+ it 'avoids N+1 queries', :request_store do
+ # prevent `let` from polluting the control
+ get_project_repository_storage_moves
+
+ control = ActiveRecord::QueryRecorder.new { get_project_repository_storage_moves }
+
+ create(:project_repository_storage_move, :scheduled)
+
+ expect { get_project_repository_storage_moves }.not_to exceed_query_limit(control)
+ end
+
+ it 'returns the most recently created first' do
+ storage_move_oldest = create(:project_repository_storage_move, :scheduled, created_at: 2.days.ago)
+ storage_move_middle = create(:project_repository_storage_move, :scheduled, created_at: 1.day.ago)
+
+ get api('/project_repository_storage_moves', user)
+
+ json_ids = json_response.map {|storage_move| storage_move['id'] }
+ expect(json_ids).to eq([
+ storage_move.id,
+ storage_move_middle.id,
+ storage_move_oldest.id
+ ])
+ end
+
+ describe 'permissions' do
+ it { expect { get_project_repository_storage_moves }.to be_allowed_for(:admin) }
+ it { expect { get_project_repository_storage_moves }.to be_denied_for(:user) }
+ end
+ end
+
+ describe 'GET /project_repository_storage_moves/:id' do
+ let(:project_repository_storage_move_id) { storage_move.id }
+
+ def get_project_repository_storage_move
+ get api("/project_repository_storage_moves/#{project_repository_storage_move_id}", user)
+ end
+
+ it 'returns a project repository storage move' do
+ get_project_repository_storage_move
+
+ expect(response).to have_gitlab_http_status(:ok)
+ expect(response).to match_response_schema('public_api/v4/project_repository_storage_move')
+ expect(json_response['id']).to eq(storage_move.id)
+ expect(json_response['state']).to eq(storage_move.human_state_name)
+ end
+
+ context 'non-existent project repository storage move' do
+ let(:project_repository_storage_move_id) { non_existing_record_id }
+
+ it 'returns not found' do
+ get_project_repository_storage_move
+
+ expect(response).to have_gitlab_http_status(:not_found)
+ end
+ end
+
+ describe 'permissions' do
+ it { expect { get_project_repository_storage_move }.to be_allowed_for(:admin) }
+ it { expect { get_project_repository_storage_move }.to be_denied_for(:user) }
+ end
+ end
+end
diff --git a/spec/requests/api/project_snippets_spec.rb b/spec/requests/api/project_snippets_spec.rb
index 89ade15c1f6..22189dc3299 100644
--- a/spec/requests/api/project_snippets_spec.rb
+++ b/spec/requests/api/project_snippets_spec.rb
@@ -94,21 +94,11 @@ describe API::ProjectSnippets do
expect(json_response['title']).to eq(snippet.title)
expect(json_response['description']).to eq(snippet.description)
- expect(json_response['file_name']).to eq(snippet.file_name)
+ expect(json_response['file_name']).to eq(snippet.file_name_on_repo)
expect(json_response['ssh_url_to_repo']).to eq(snippet.ssh_url_to_repo)
expect(json_response['http_url_to_repo']).to eq(snippet.http_url_to_repo)
end
- context 'when feature flag :version_snippets is disabled' do
- before do
- stub_feature_flags(version_snippets: false)
-
- get api("/projects/#{project.id}/snippets/#{snippet.id}", user)
- end
-
- it_behaves_like 'snippet response without repository URLs'
- end
-
it 'returns 404 for invalid snippet id' do
get api("/projects/#{project.id}/snippets/#{non_existing_record_id}", user)
@@ -129,7 +119,7 @@ describe API::ProjectSnippets do
title: 'Test Title',
file_name: 'test.rb',
description: 'test description',
- code: 'puts "hello world"',
+ content: 'puts "hello world"',
visibility: 'public'
}
end
@@ -148,19 +138,7 @@ describe API::ProjectSnippets do
blob = snippet.repository.blob_at('master', params[:file_name])
- expect(blob.data).to eq params[:code]
- end
-
- context 'when feature flag :version_snippets is disabled' do
- it 'does not create snippet repository' do
- stub_feature_flags(version_snippets: false)
-
- expect do
- subject
- end.to change { ProjectSnippet.count }.by(1)
-
- expect(snippet.repository_exists?).to be_falsey
- end
+ expect(blob.data).to eq params[:content]
end
end
@@ -202,7 +180,7 @@ describe API::ProjectSnippets do
expect(response).to have_gitlab_http_status(:created)
snippet = ProjectSnippet.find(json_response['id'])
- expect(snippet.content).to eq(params[:code])
+ expect(snippet.content).to eq(params[:content])
expect(snippet.description).to eq(params[:description])
expect(snippet.title).to eq(params[:title])
expect(snippet.file_name).to eq(params[:file_name])
@@ -219,7 +197,7 @@ describe API::ProjectSnippets do
expect(response).to have_gitlab_http_status(:created)
snippet = ProjectSnippet.find(json_response['id'])
- expect(snippet.content).to eq(params[:code])
+ expect(snippet.content).to eq(params[:content])
expect(snippet.description).to eq(params[:description])
expect(snippet.title).to eq(params[:title])
expect(snippet.file_name).to eq(params[:file_name])
@@ -230,43 +208,44 @@ describe API::ProjectSnippets do
subject { post api("/projects/#{project.id}/snippets/", admin), params: params }
end
- it 'creates a new snippet with content parameter' do
- params[:content] = params.delete(:code)
+ it 'returns 400 for missing parameters' do
+ params.delete(:title)
post api("/projects/#{project.id}/snippets/", admin), params: params
- expect(response).to have_gitlab_http_status(:created)
- snippet = ProjectSnippet.find(json_response['id'])
- expect(snippet.content).to eq(params[:content])
- expect(snippet.description).to eq(params[:description])
- expect(snippet.title).to eq(params[:title])
- expect(snippet.file_name).to eq(params[:file_name])
- expect(snippet.visibility_level).to eq(Snippet::PUBLIC)
+ expect(response).to have_gitlab_http_status(:bad_request)
end
- it 'returns 400 when both code and content parameters specified' do
- params[:content] = params[:code]
+ it 'returns 400 if content is blank' do
+ params[:content] = ''
post api("/projects/#{project.id}/snippets/", admin), params: params
expect(response).to have_gitlab_http_status(:bad_request)
- expect(json_response['error']).to eq('code, content are mutually exclusive')
+ expect(json_response['error']).to eq 'content is empty'
end
- it 'returns 400 for missing parameters' do
- params.delete(:title)
+ it 'returns 400 if title is blank' do
+ params[:title] = ''
post api("/projects/#{project.id}/snippets/", admin), params: params
expect(response).to have_gitlab_http_status(:bad_request)
+ expect(json_response['error']).to eq 'title is empty'
end
- it 'returns 400 for empty code field' do
- params[:code] = ''
+ context 'when save fails because the repository could not be created' do
+ before do
+ allow_next_instance_of(Snippets::CreateService) do |instance|
+ allow(instance).to receive(:create_repository).and_raise(Snippets::CreateService::CreateRepositoryError)
+ end
+ end
- post api("/projects/#{project.id}/snippets/", admin), params: params
+ it 'returns 400' do
+ post api("/projects/#{project.id}/snippets", admin), params: params
- expect(response).to have_gitlab_http_status(:bad_request)
+ expect(response).to have_gitlab_http_status(:bad_request)
+ end
end
context 'when the snippet is spam' do
@@ -320,7 +299,7 @@ describe API::ProjectSnippets do
new_content = 'New content'
new_description = 'New description'
- update_snippet(params: { code: new_content, description: new_description, visibility: 'private' })
+ update_snippet(params: { content: new_content, description: new_description, visibility: 'private' })
expect(response).to have_gitlab_http_status(:ok)
snippet.reload
@@ -341,13 +320,6 @@ describe API::ProjectSnippets do
expect(snippet.description).to eq(new_description)
end
- it 'returns 400 when both code and content parameters specified' do
- update_snippet(params: { code: 'some content', content: 'other content' })
-
- expect(response).to have_gitlab_http_status(:bad_request)
- expect(json_response['error']).to eq('code, content are mutually exclusive')
- end
-
it 'returns 404 for invalid snippet id' do
update_snippet(snippet_id: non_existing_record_id, params: { title: 'foo' })
@@ -361,12 +333,17 @@ describe API::ProjectSnippets do
expect(response).to have_gitlab_http_status(:bad_request)
end
- it 'returns 400 for empty code field' do
- new_content = ''
+ it 'returns 400 if content is blank' do
+ update_snippet(params: { content: '' })
+
+ expect(response).to have_gitlab_http_status(:bad_request)
+ end
- update_snippet(params: { code: new_content })
+ it 'returns 400 if title is blank' do
+ update_snippet(params: { title: '' })
expect(response).to have_gitlab_http_status(:bad_request)
+ expect(json_response['error']).to eq 'title is empty'
end
it_behaves_like 'update with repository actions' do
@@ -460,14 +437,13 @@ describe API::ProjectSnippets do
end
describe 'GET /projects/:project_id/snippets/:id/raw' do
- let(:snippet) { create(:project_snippet, author: admin, project: project) }
+ let_it_be(:snippet) { create(:project_snippet, :repository, author: admin, project: project) }
it 'returns raw text' do
get api("/projects/#{snippet.project.id}/snippets/#{snippet.id}/raw", admin)
expect(response).to have_gitlab_http_status(:ok)
expect(response.content_type).to eq 'text/plain'
- expect(response.body).to eq(snippet.content)
end
it 'returns 404 for invalid snippet id' do
@@ -482,5 +458,11 @@ describe API::ProjectSnippets do
let(:request) { get api("/projects/#{project_no_snippets.id}/snippets/123/raw", admin) }
end
end
+
+ it_behaves_like 'snippet blob content' do
+ let_it_be(:snippet_with_empty_repo) { create(:project_snippet, :empty_repo, author: admin, project: project) }
+
+ subject { get api("/projects/#{snippet.project.id}/snippets/#{snippet.id}/raw", snippet.author) }
+ end
end
end
diff --git a/spec/requests/api/project_statistics_spec.rb b/spec/requests/api/project_statistics_spec.rb
index 1f48c081043..89809a97b96 100644
--- a/spec/requests/api/project_statistics_spec.rb
+++ b/spec/requests/api/project_statistics_spec.rb
@@ -3,23 +3,23 @@
require 'spec_helper'
describe API::ProjectStatistics do
- let(:maintainer) { create(:user) }
- let(:public_project) { create(:project, :public) }
+ let_it_be(:developer) { create(:user) }
+ let_it_be(:public_project) { create(:project, :public) }
before do
- public_project.add_maintainer(maintainer)
+ public_project.add_developer(developer)
end
describe 'GET /projects/:id/statistics' do
- let!(:fetch_statistics1) { create(:project_daily_statistic, project: public_project, fetch_count: 30, date: 29.days.ago) }
- let!(:fetch_statistics2) { create(:project_daily_statistic, project: public_project, fetch_count: 4, date: 3.days.ago) }
- let!(:fetch_statistics3) { create(:project_daily_statistic, project: public_project, fetch_count: 3, date: 2.days.ago) }
- let!(:fetch_statistics4) { create(:project_daily_statistic, project: public_project, fetch_count: 2, date: 1.day.ago) }
- let!(:fetch_statistics5) { create(:project_daily_statistic, project: public_project, fetch_count: 1, date: Date.today) }
- let!(:fetch_statistics_other_project) { create(:project_daily_statistic, project: create(:project), fetch_count: 29, date: 29.days.ago) }
+ let_it_be(:fetch_statistics1) { create(:project_daily_statistic, project: public_project, fetch_count: 30, date: 29.days.ago) }
+ let_it_be(:fetch_statistics2) { create(:project_daily_statistic, project: public_project, fetch_count: 4, date: 3.days.ago) }
+ let_it_be(:fetch_statistics3) { create(:project_daily_statistic, project: public_project, fetch_count: 3, date: 2.days.ago) }
+ let_it_be(:fetch_statistics4) { create(:project_daily_statistic, project: public_project, fetch_count: 2, date: 1.day.ago) }
+ let_it_be(:fetch_statistics5) { create(:project_daily_statistic, project: public_project, fetch_count: 1, date: Date.today) }
+ let_it_be(:fetch_statistics_other_project) { create(:project_daily_statistic, project: create(:project), fetch_count: 29, date: 29.days.ago) }
it 'returns the fetch statistics of the last 30 days' do
- get api("/projects/#{public_project.id}/statistics", maintainer)
+ get api("/projects/#{public_project.id}/statistics", developer)
expect(response).to have_gitlab_http_status(:ok)
fetches = json_response['fetches']
@@ -32,7 +32,7 @@ describe API::ProjectStatistics do
it 'excludes the fetch statistics older than 30 days' do
create(:project_daily_statistic, fetch_count: 31, project: public_project, date: 30.days.ago)
- get api("/projects/#{public_project.id}/statistics", maintainer)
+ get api("/projects/#{public_project.id}/statistics", developer)
expect(response).to have_gitlab_http_status(:ok)
fetches = json_response['fetches']
@@ -41,11 +41,11 @@ describe API::ProjectStatistics do
expect(fetches['days'].last).to eq({ 'count' => fetch_statistics1.fetch_count, 'date' => fetch_statistics1.date.to_s })
end
- it 'responds with 403 when the user is not a maintainer of the repository' do
- developer = create(:user)
- public_project.add_developer(developer)
+ it 'responds with 403 when the user is not a developer of the repository' do
+ guest = create(:user)
+ public_project.add_guest(guest)
- get api("/projects/#{public_project.id}/statistics", developer)
+ get api("/projects/#{public_project.id}/statistics", guest)
expect(response).to have_gitlab_http_status(:forbidden)
expect(json_response['message']).to eq('403 Forbidden')
diff --git a/spec/requests/api/project_templates_spec.rb b/spec/requests/api/project_templates_spec.rb
index 5dabce20043..caeb465080e 100644
--- a/spec/requests/api/project_templates_spec.rb
+++ b/spec/requests/api/project_templates_spec.rb
@@ -3,15 +3,29 @@
require 'spec_helper'
describe API::ProjectTemplates do
- let_it_be(:public_project) { create(:project, :public) }
+ let_it_be(:public_project) { create(:project, :public, path: 'path.with.dot') }
let_it_be(:private_project) { create(:project, :private) }
let_it_be(:developer) { create(:user) }
+ let(:url_encoded_path) { "#{public_project.namespace.path}%2F#{public_project.path}" }
+
before do
private_project.add_developer(developer)
end
+ shared_examples 'accepts project paths with dots' do
+ it do
+ subject
+
+ expect(response).to have_gitlab_http_status(:ok)
+ end
+ end
+
describe 'GET /projects/:id/templates/:type' do
+ it_behaves_like 'accepts project paths with dots' do
+ subject { get api("/projects/#{url_encoded_path}/templates/dockerfiles") }
+ end
+
it 'returns dockerfiles' do
get api("/projects/#{public_project.id}/templates/dockerfiles")
@@ -75,6 +89,10 @@ describe API::ProjectTemplates do
expect(response).to have_gitlab_http_status(:ok)
expect(response).to match_response_schema('public_api/v4/template_list')
end
+
+ it_behaves_like 'accepts project paths with dots' do
+ subject { get api("/projects/#{url_encoded_path}/templates/licenses") }
+ end
end
describe 'GET /projects/:id/templates/:type/:key' do
@@ -144,6 +162,10 @@ describe API::ProjectTemplates do
expect(response).to match_response_schema('public_api/v4/license')
end
+ it_behaves_like 'accepts project paths with dots' do
+ subject { get api("/projects/#{url_encoded_path}/templates/gitlab_ci_ymls/Android") }
+ end
+
shared_examples 'path traversal attempt' do |template_type|
it 'rejects invalid filenames' do
get api("/projects/#{public_project.id}/templates/#{template_type}/%2e%2e%2fPython%2ea")
@@ -173,5 +195,9 @@ describe API::ProjectTemplates do
expect(content).to include('Project Placeholder')
expect(content).to include("Copyright (C) #{Time.now.year} Fullname Placeholder")
end
+
+ it_behaves_like 'accepts project paths with dots' do
+ subject { get api("/projects/#{url_encoded_path}/templates/licenses/mit") }
+ end
end
end
diff --git a/spec/requests/api/projects_spec.rb b/spec/requests/api/projects_spec.rb
index 853155cea7a..0deff138e2e 100644
--- a/spec/requests/api/projects_spec.rb
+++ b/spec/requests/api/projects_spec.rb
@@ -24,7 +24,7 @@ shared_examples 'languages and percentages JSON response' do
get api("/projects/#{project.id}/languages", user)
expect(response).to have_gitlab_http_status(:ok)
- expect(JSON.parse(response.body)).to eq(expected_languages)
+ expect(Gitlab::Json.parse(response.body)).to eq(expected_languages)
end
end
@@ -672,7 +672,7 @@ describe API::Projects do
match[1]
end
- ids += JSON.parse(response.body).map { |p| p['id'] }
+ ids += Gitlab::Json.parse(response.body).map { |p| p['id'] }
end
expect(ids).to contain_exactly(*projects.map(&:id))
@@ -1806,7 +1806,7 @@ describe API::Projects do
first_user = json_response.first
expect(first_user['username']).to eq(user.username)
expect(first_user['name']).to eq(user.name)
- expect(first_user.keys).to contain_exactly(*%w[name username id state avatar_url web_url])
+ expect(first_user.keys).to include(*%w[name username id state avatar_url web_url])
end
end
diff --git a/spec/requests/api/remote_mirrors_spec.rb b/spec/requests/api/remote_mirrors_spec.rb
index 3eaec6e2520..3029b8443b0 100644
--- a/spec/requests/api/remote_mirrors_spec.rb
+++ b/spec/requests/api/remote_mirrors_spec.rb
@@ -78,10 +78,6 @@ describe API::RemoteMirrors do
let(:route) { ->(id) { "/projects/#{project.id}/remote_mirrors/#{id}" } }
let(:mirror) { project.remote_mirrors.first }
- before do
- stub_feature_flags(keep_divergent_refs: false)
- end
-
it 'requires `admin_remote_mirror` permission' do
put api(route[mirror.id], developer)
@@ -100,24 +96,7 @@ describe API::RemoteMirrors do
expect(response).to have_gitlab_http_status(:success)
expect(json_response['enabled']).to eq(false)
expect(json_response['only_protected_branches']).to eq(true)
-
- # Deleted due to lack of feature availability
- expect(json_response['keep_divergent_refs']).to be_nil
- end
-
- context 'with the `keep_divergent_refs` feature enabled' do
- before do
- stub_feature_flags(keep_divergent_refs: { enabled: true, project: project })
- end
-
- it 'updates the `keep_divergent_refs` attribute' do
- project.add_maintainer(user)
-
- put api(route[mirror.id], user), params: { keep_divergent_refs: 'true' }
-
- expect(response).to have_gitlab_http_status(:success)
- expect(json_response['keep_divergent_refs']).to eq(true)
- end
+ expect(json_response['keep_divergent_refs']).to eq(true)
end
end
end
diff --git a/spec/requests/api/runner_spec.rb b/spec/requests/api/runner_spec.rb
index bc2da8a2b9a..7284f33f3af 100644
--- a/spec/requests/api/runner_spec.rb
+++ b/spec/requests/api/runner_spec.rb
@@ -471,7 +471,8 @@ describe API::Runner, :clean_gitlab_redis_shared_state do
'sha' => job.sha,
'before_sha' => job.before_sha,
'ref_type' => 'branch',
- 'refspecs' => ["+refs/heads/#{job.ref}:refs/remotes/origin/#{job.ref}"],
+ 'refspecs' => ["+refs/pipelines/#{pipeline.id}:refs/pipelines/#{pipeline.id}",
+ "+refs/heads/#{job.ref}:refs/remotes/origin/#{job.ref}"],
'depth' => project.ci_default_git_depth }
end
@@ -578,7 +579,9 @@ describe API::Runner, :clean_gitlab_redis_shared_state do
expect(response).to have_gitlab_http_status(:created)
expect(json_response['git_info']['refspecs'])
- .to contain_exactly('+refs/tags/*:refs/tags/*', '+refs/heads/*:refs/remotes/origin/*')
+ .to contain_exactly("+refs/pipelines/#{pipeline.id}:refs/pipelines/#{pipeline.id}",
+ '+refs/tags/*:refs/tags/*',
+ '+refs/heads/*:refs/remotes/origin/*')
end
end
end
@@ -638,7 +641,9 @@ describe API::Runner, :clean_gitlab_redis_shared_state do
expect(response).to have_gitlab_http_status(:created)
expect(json_response['git_info']['refspecs'])
- .to contain_exactly('+refs/tags/*:refs/tags/*', '+refs/heads/*:refs/remotes/origin/*')
+ .to contain_exactly("+refs/pipelines/#{pipeline.id}:refs/pipelines/#{pipeline.id}",
+ '+refs/tags/*:refs/tags/*',
+ '+refs/heads/*:refs/remotes/origin/*')
end
end
end
@@ -998,6 +1003,53 @@ describe API::Runner, :clean_gitlab_redis_shared_state do
end
end
+ describe 'a job with excluded artifacts' do
+ context 'when excluded paths are defined' do
+ let(:job) do
+ create(:ci_build, pipeline: pipeline, token: 'test-job-token', name: 'test',
+ stage: 'deploy', stage_idx: 1,
+ options: { artifacts: { paths: ['abc'], exclude: ['cde'] } })
+ end
+
+ context 'when a runner supports this feature' do
+ it 'exposes excluded paths when the feature is enabled' do
+ stub_feature_flags(ci_artifacts_exclude: true)
+
+ request_job info: { features: { artifacts_exclude: true } }
+
+ expect(response).to have_gitlab_http_status(:created)
+ expect(json_response.dig('artifacts').first).to include('exclude' => ['cde'])
+ end
+
+ it 'does not expose excluded paths when the feature is disabled' do
+ stub_feature_flags(ci_artifacts_exclude: false)
+
+ request_job info: { features: { artifacts_exclude: true } }
+
+ expect(response).to have_gitlab_http_status(:created)
+ expect(json_response.dig('artifacts').first).not_to have_key('exclude')
+ end
+ end
+
+ context 'when a runner does not support this feature' do
+ it 'does not expose the build at all' do
+ stub_feature_flags(ci_artifacts_exclude: true)
+
+ request_job
+
+ expect(response).to have_gitlab_http_status(:no_content)
+ end
+ end
+ end
+
+ it 'does not expose excluded paths when these are empty' do
+ request_job
+
+ expect(response).to have_gitlab_http_status(:created)
+ expect(json_response.dig('artifacts').first).not_to have_key('exclude')
+ end
+ end
+
def request_job(token = runner.token, **params)
new_params = params.merge(token: token, last_update: last_update)
post api('/jobs/request'), params: new_params, headers: { 'User-Agent' => user_agent }
diff --git a/spec/requests/api/runners_spec.rb b/spec/requests/api/runners_spec.rb
index 164be8f0da6..261e54da6a8 100644
--- a/spec/requests/api/runners_spec.rb
+++ b/spec/requests/api/runners_spec.rb
@@ -3,35 +3,34 @@
require 'spec_helper'
describe API::Runners do
- let(:admin) { create(:user, :admin) }
- let(:user) { create(:user) }
- let(:user2) { create(:user) }
- let(:group_guest) { create(:user) }
- let(:group_reporter) { create(:user) }
- let(:group_developer) { create(:user) }
- let(:group_maintainer) { create(:user) }
-
- let(:project) { create(:project, creator_id: user.id) }
- let(:project2) { create(:project, creator_id: user.id) }
-
- let(:group) { create(:group).tap { |group| group.add_owner(user) } }
- let(:subgroup) { create(:group, parent: group) }
-
- let!(:shared_runner) { create(:ci_runner, :instance, description: 'Shared runner') }
- let!(:project_runner) { create(:ci_runner, :project, description: 'Project runner', projects: [project]) }
- let!(:two_projects_runner) { create(:ci_runner, :project, description: 'Two projects runner', projects: [project, project2]) }
- let!(:group_runner_a) { create(:ci_runner, :group, description: 'Group runner A', groups: [group]) }
- let!(:group_runner_b) { create(:ci_runner, :group, description: 'Group runner B', groups: [subgroup]) }
-
- before do
- # Set project access for users
- create(:group_member, :guest, user: group_guest, group: group)
- create(:group_member, :reporter, user: group_reporter, group: group)
- create(:group_member, :developer, user: group_developer, group: group)
- create(:group_member, :maintainer, user: group_maintainer, group: group)
- create(:project_member, :maintainer, user: user, project: project)
- create(:project_member, :maintainer, user: user, project: project2)
- create(:project_member, :reporter, user: user2, project: project)
+ let_it_be(:admin) { create(:user, :admin) }
+ let_it_be(:user) { create(:user) }
+ let_it_be(:user2) { create(:user) }
+ let_it_be(:group_guest) { create(:user) }
+ let_it_be(:group_reporter) { create(:user) }
+ let_it_be(:group_developer) { create(:user) }
+ let_it_be(:group_maintainer) { create(:user) }
+
+ let_it_be(:project) { create(:project, creator_id: user.id) }
+ let_it_be(:project2) { create(:project, creator_id: user.id) }
+
+ let_it_be(:group) { create(:group).tap { |group| group.add_owner(user) } }
+ let_it_be(:subgroup) { create(:group, parent: group) }
+
+ let_it_be(:shared_runner, reload: true) { create(:ci_runner, :instance, description: 'Shared runner') }
+ let_it_be(:project_runner, reload: true) { create(:ci_runner, :project, description: 'Project runner', projects: [project]) }
+ let_it_be(:two_projects_runner) { create(:ci_runner, :project, description: 'Two projects runner', projects: [project, project2]) }
+ let_it_be(:group_runner_a) { create(:ci_runner, :group, description: 'Group runner A', groups: [group]) }
+ let_it_be(:group_runner_b) { create(:ci_runner, :group, description: 'Group runner B', groups: [subgroup]) }
+
+ before_all do
+ group.add_guest(group_guest)
+ group.add_reporter(group_reporter)
+ group.add_developer(group_developer)
+ group.add_maintainer(group_maintainer)
+ project.add_maintainer(user)
+ project2.add_maintainer(user)
+ project.add_reporter(user2)
end
describe 'GET /runners' do
@@ -327,6 +326,32 @@ describe API::Runners do
expect(response).to have_gitlab_http_status(:unauthorized)
end
end
+
+ context 'FF hide_token_from_runners_api is enabled' do
+ before do
+ stub_feature_flags(hide_token_from_runners_api: true)
+ end
+
+ it "does not return runner's token" do
+ get api("/runners/#{shared_runner.id}", admin)
+
+ expect(response).to have_gitlab_http_status(:ok)
+ expect(json_response).not_to have_key('token')
+ end
+ end
+
+ context 'FF hide_token_from_runners_api is disabled' do
+ before do
+ stub_feature_flags(hide_token_from_runners_api: false)
+ end
+
+ it "returns runner's token" do
+ get api("/runners/#{shared_runner.id}", admin)
+
+ expect(response).to have_gitlab_http_status(:ok)
+ expect(json_response).to have_key('token')
+ end
+ end
end
describe 'PUT /runners/:id' do
@@ -603,10 +628,10 @@ describe API::Runners do
describe 'GET /runners/:id/jobs' do
let_it_be(:job_1) { create(:ci_build) }
- let!(:job_2) { create(:ci_build, :running, runner: shared_runner, project: project) }
- let!(:job_3) { create(:ci_build, :failed, runner: shared_runner, project: project) }
- let!(:job_4) { create(:ci_build, :running, runner: project_runner, project: project) }
- let!(:job_5) { create(:ci_build, :failed, runner: project_runner, project: project) }
+ let_it_be(:job_2) { create(:ci_build, :running, runner: shared_runner, project: project) }
+ let_it_be(:job_3) { create(:ci_build, :failed, runner: shared_runner, project: project) }
+ let_it_be(:job_4) { create(:ci_build, :running, runner: project_runner, project: project) }
+ let_it_be(:job_5) { create(:ci_build, :failed, runner: project_runner, project: project) }
context 'admin user' do
context 'when runner exists' do
@@ -952,7 +977,7 @@ describe API::Runners do
describe 'POST /projects/:id/runners' do
context 'authorized user' do
- let(:project_runner2) { create(:ci_runner, :project, projects: [project2]) }
+ let_it_be(:project_runner2) { create(:ci_runner, :project, projects: [project2]) }
it 'enables specific runner' do
expect do
diff --git a/spec/requests/api/search_spec.rb b/spec/requests/api/search_spec.rb
index 6ff5fbd7925..3894e0bf2d1 100644
--- a/spec/requests/api/search_spec.rb
+++ b/spec/requests/api/search_spec.rb
@@ -15,10 +15,36 @@ describe API::Search do
it { expect(json_response.size).to eq(size) }
end
- describe 'GET /search' do
+ shared_examples 'pagination' do |scope:, search: ''|
+ it 'returns a different result for each page' do
+ get api(endpoint, user), params: { scope: scope, search: search, page: 1, per_page: 1 }
+ first = json_response.first
+
+ get api(endpoint, user), params: { scope: scope, search: search, page: 2, per_page: 1 }
+ second = Gitlab::Json.parse(response.body).first
+
+ expect(first).not_to eq(second)
+ end
+
+ it 'returns 1 result when per_page is 1' do
+ get api(endpoint, user), params: { scope: scope, search: search, per_page: 1 }
+
+ expect(json_response.count).to eq(1)
+ end
+
+ it 'returns 2 results when per_page is 2' do
+ get api(endpoint, user), params: { scope: scope, search: search, per_page: 2 }
+
+ expect(Gitlab::Json.parse(response.body).count).to eq(2)
+ end
+ end
+
+ describe 'GET /search' do
+ let(:endpoint) { '/search' }
+
context 'when user is not authenticated' do
it 'returns 401 error' do
- get api('/search'), params: { scope: 'projects', search: 'awesome' }
+ get api(endpoint), params: { scope: 'projects', search: 'awesome' }
expect(response).to have_gitlab_http_status(:unauthorized)
end
@@ -26,7 +52,7 @@ describe API::Search do
context 'when scope is not supported' do
it 'returns 400 error' do
- get api('/search', user), params: { scope: 'unsupported', search: 'awesome' }
+ get api(endpoint, user), params: { scope: 'unsupported', search: 'awesome' }
expect(response).to have_gitlab_http_status(:bad_request)
end
@@ -34,7 +60,7 @@ describe API::Search do
context 'when scope is missing' do
it 'returns 400 error' do
- get api('/search', user), params: { search: 'awesome' }
+ get api(endpoint, user), params: { search: 'awesome' }
expect(response).to have_gitlab_http_status(:bad_request)
end
@@ -43,30 +69,48 @@ describe API::Search do
context 'with correct params' do
context 'for projects scope' do
before do
- get api('/search', user), params: { scope: 'projects', search: 'awesome' }
+ get api(endpoint, user), params: { scope: 'projects', search: 'awesome' }
end
it_behaves_like 'response is correct', schema: 'public_api/v4/projects'
+
+ it_behaves_like 'pagination', scope: :projects
end
context 'for issues scope' do
before do
create(:issue, project: project, title: 'awesome issue')
- get api('/search', user), params: { scope: 'issues', search: 'awesome' }
+ get api(endpoint, user), params: { scope: 'issues', search: 'awesome' }
end
it_behaves_like 'response is correct', schema: 'public_api/v4/issues'
+
+ describe 'pagination' do
+ before do
+ create(:issue, project: project, title: 'another issue')
+ end
+
+ include_examples 'pagination', scope: :issues
+ end
end
context 'for merge_requests scope' do
before do
create(:merge_request, source_project: repo_project, title: 'awesome mr')
- get api('/search', user), params: { scope: 'merge_requests', search: 'awesome' }
+ get api(endpoint, user), params: { scope: 'merge_requests', search: 'awesome' }
end
it_behaves_like 'response is correct', schema: 'public_api/v4/merge_requests'
+
+ describe 'pagination' do
+ before do
+ create(:merge_request, source_project: repo_project, title: 'another mr', target_branch: 'another_branch')
+ end
+
+ include_examples 'pagination', scope: :merge_requests
+ end
end
context 'for milestones scope' do
@@ -76,10 +120,18 @@ describe API::Search do
context 'when user can read project milestones' do
before do
- get api('/search', user), params: { scope: 'milestones', search: 'awesome' }
+ get api(endpoint, user), params: { scope: 'milestones', search: 'awesome' }
end
it_behaves_like 'response is correct', schema: 'public_api/v4/milestones'
+
+ describe 'pagination' do
+ before do
+ create(:milestone, project: project, title: 'another milestone')
+ end
+
+ include_examples 'pagination', scope: :milestones
+ end
end
context 'when user cannot read project milestones' do
@@ -89,7 +141,7 @@ describe API::Search do
end
it 'returns empty array' do
- get api('/search', user), params: { scope: 'milestones', search: 'awesome' }
+ get api(endpoint, user), params: { scope: 'milestones', search: 'awesome' }
milestones = json_response
@@ -102,16 +154,18 @@ describe API::Search do
before do
create(:user, name: 'billy')
- get api('/search', user), params: { scope: 'users', search: 'billy' }
+ get api(endpoint, user), params: { scope: 'users', search: 'billy' }
end
it_behaves_like 'response is correct', schema: 'public_api/v4/user/basics'
+ it_behaves_like 'pagination', scope: :users
+
context 'when users search feature is disabled' do
before do
- allow(Feature).to receive(:disabled?).with(:users_search, default_enabled: true).and_return(true)
+ stub_feature_flags(users_search: false)
- get api('/search', user), params: { scope: 'users', search: 'billy' }
+ get api(endpoint, user), params: { scope: 'users', search: 'billy' }
end
it 'returns 400 error' do
@@ -124,28 +178,28 @@ describe API::Search do
before do
create(:snippet, :public, title: 'awesome snippet', content: 'snippet content')
- get api('/search', user), params: { scope: 'snippet_titles', search: 'awesome' }
+ get api(endpoint, user), params: { scope: 'snippet_titles', search: 'awesome' }
end
it_behaves_like 'response is correct', schema: 'public_api/v4/snippets'
- end
- context 'for snippet_blobs scope' do
- before do
- create(:snippet, :public, title: 'awesome snippet', content: 'snippet content')
+ describe 'pagination' do
+ before do
+ create(:snippet, :public, title: 'another snippet', content: 'snippet content')
+ end
- get api('/search', user), params: { scope: 'snippet_blobs', search: 'content' }
+ include_examples 'pagination', scope: :snippet_titles
end
-
- it_behaves_like 'response is correct', schema: 'public_api/v4/snippets'
end
end
end
describe "GET /groups/:id/search" do
+ let(:endpoint) { "/groups/#{group.id}/-/search" }
+
context 'when user is not authenticated' do
it 'returns 401 error' do
- get api("/groups/#{group.id}/search"), params: { scope: 'projects', search: 'awesome' }
+ get api(endpoint), params: { scope: 'projects', search: 'awesome' }
expect(response).to have_gitlab_http_status(:unauthorized)
end
@@ -153,7 +207,7 @@ describe API::Search do
context 'when scope is not supported' do
it 'returns 400 error' do
- get api("/groups/#{group.id}/search", user), params: { scope: 'unsupported', search: 'awesome' }
+ get api(endpoint, user), params: { scope: 'unsupported', search: 'awesome' }
expect(response).to have_gitlab_http_status(:bad_request)
end
@@ -161,7 +215,7 @@ describe API::Search do
context 'when scope is missing' do
it 'returns 400 error' do
- get api("/groups/#{group.id}/search", user), params: { search: 'awesome' }
+ get api(endpoint, user), params: { search: 'awesome' }
expect(response).to have_gitlab_http_status(:bad_request)
end
@@ -188,40 +242,66 @@ describe API::Search do
context 'with correct params' do
context 'for projects scope' do
before do
- get api("/groups/#{group.id}/search", user), params: { scope: 'projects', search: 'awesome' }
+ get api(endpoint, user), params: { scope: 'projects', search: 'awesome' }
end
it_behaves_like 'response is correct', schema: 'public_api/v4/projects'
+
+ it_behaves_like 'pagination', scope: :projects
end
context 'for issues scope' do
before do
create(:issue, project: project, title: 'awesome issue')
- get api("/groups/#{group.id}/search", user), params: { scope: 'issues', search: 'awesome' }
+ get api(endpoint, user), params: { scope: 'issues', search: 'awesome' }
end
it_behaves_like 'response is correct', schema: 'public_api/v4/issues'
+
+ describe 'pagination' do
+ before do
+ create(:issue, project: project, title: 'another issue')
+ end
+
+ include_examples 'pagination', scope: :issues
+ end
end
context 'for merge_requests scope' do
before do
create(:merge_request, source_project: repo_project, title: 'awesome mr')
- get api("/groups/#{group.id}/search", user), params: { scope: 'merge_requests', search: 'awesome' }
+ get api(endpoint, user), params: { scope: 'merge_requests', search: 'awesome' }
end
it_behaves_like 'response is correct', schema: 'public_api/v4/merge_requests'
+
+ describe 'pagination' do
+ before do
+ create(:merge_request, source_project: repo_project, title: 'another mr', target_branch: 'another_branch')
+ end
+
+ include_examples 'pagination', scope: :merge_requests
+ end
end
context 'for milestones scope' do
before do
create(:milestone, project: project, title: 'awesome milestone')
- get api("/groups/#{group.id}/search", user), params: { scope: 'milestones', search: 'awesome' }
+ get api(endpoint, user), params: { scope: 'milestones', search: 'awesome' }
end
it_behaves_like 'response is correct', schema: 'public_api/v4/milestones'
+
+ describe 'pagination' do
+ before do
+ create(:milestone, project: project, title: 'another milestone')
+ end
+
+ include_examples 'pagination', scope: :milestones
+ end
end
context 'for milestones scope with group path as id' do
@@ -241,16 +321,24 @@ describe API::Search do
user = create(:user, name: 'billy')
create(:group_member, :developer, user: user, group: group)
- get api("/groups/#{group.id}/search", user), params: { scope: 'users', search: 'billy' }
+ get api(endpoint, user), params: { scope: 'users', search: 'billy' }
end
it_behaves_like 'response is correct', schema: 'public_api/v4/user/basics'
+ describe 'pagination' do
+ before do
+ create(:group_member, :developer, group: group)
+ end
+
+ include_examples 'pagination', scope: :users
+ end
+
context 'when users search feature is disabled' do
before do
- allow(Feature).to receive(:disabled?).with(:users_search, default_enabled: true).and_return(true)
+ stub_feature_flags(users_search: false)
- get api("/groups/#{group.id}/search", user), params: { scope: 'users', search: 'billy' }
+ get api(endpoint, user), params: { scope: 'users', search: 'billy' }
end
it 'returns 400 error' do
@@ -273,9 +361,11 @@ describe API::Search do
end
describe "GET /projects/:id/search" do
+ let(:endpoint) { "/projects/#{project.id}/search" }
+
context 'when user is not authenticated' do
it 'returns 401 error' do
- get api("/projects/#{project.id}/search"), params: { scope: 'issues', search: 'awesome' }
+ get api(endpoint), params: { scope: 'issues', search: 'awesome' }
expect(response).to have_gitlab_http_status(:unauthorized)
end
@@ -283,7 +373,7 @@ describe API::Search do
context 'when scope is not supported' do
it 'returns 400 error' do
- get api("/projects/#{project.id}/search", user), params: { scope: 'unsupported', search: 'awesome' }
+ get api(endpoint, user), params: { scope: 'unsupported', search: 'awesome' }
expect(response).to have_gitlab_http_status(:bad_request)
end
@@ -291,7 +381,7 @@ describe API::Search do
context 'when scope is missing' do
it 'returns 400 error' do
- get api("/projects/#{project.id}/search", user), params: { search: 'awesome' }
+ get api(endpoint, user), params: { search: 'awesome' }
expect(response).to have_gitlab_http_status(:bad_request)
end
@@ -309,7 +399,7 @@ describe API::Search do
it 'returns 404 error' do
project.update!(visibility_level: Gitlab::VisibilityLevel::PRIVATE)
- get api("/projects/#{project.id}/search", user), params: { scope: 'issues', search: 'awesome' }
+ get api(endpoint, user), params: { scope: 'issues', search: 'awesome' }
expect(response).to have_gitlab_http_status(:not_found)
end
@@ -320,20 +410,38 @@ describe API::Search do
before do
create(:issue, project: project, title: 'awesome issue')
- get api("/projects/#{project.id}/search", user), params: { scope: 'issues', search: 'awesome' }
+ get api(endpoint, user), params: { scope: 'issues', search: 'awesome' }
end
it_behaves_like 'response is correct', schema: 'public_api/v4/issues'
+
+ describe 'pagination' do
+ before do
+ create(:issue, project: project, title: 'another issue')
+ end
+
+ include_examples 'pagination', scope: :issues
+ end
end
context 'for merge_requests scope' do
+ let(:endpoint) { "/projects/#{repo_project.id}/search" }
+
before do
create(:merge_request, source_project: repo_project, title: 'awesome mr')
- get api("/projects/#{repo_project.id}/search", user), params: { scope: 'merge_requests', search: 'awesome' }
+ get api(endpoint, user), params: { scope: 'merge_requests', search: 'awesome' }
end
it_behaves_like 'response is correct', schema: 'public_api/v4/merge_requests'
+
+ describe 'pagination' do
+ before do
+ create(:merge_request, source_project: repo_project, title: 'another mr', target_branch: 'another_branch')
+ end
+
+ include_examples 'pagination', scope: :merge_requests
+ end
end
context 'for milestones scope' do
@@ -343,10 +451,18 @@ describe API::Search do
context 'when user can read milestones' do
before do
- get api("/projects/#{project.id}/search", user), params: { scope: 'milestones', search: 'awesome' }
+ get api(endpoint, user), params: { scope: 'milestones', search: 'awesome' }
end
it_behaves_like 'response is correct', schema: 'public_api/v4/milestones'
+
+ describe 'pagination' do
+ before do
+ create(:milestone, project: project, title: 'another milestone')
+ end
+
+ include_examples 'pagination', scope: :milestones
+ end
end
context 'when user cannot read project milestones' do
@@ -356,7 +472,7 @@ describe API::Search do
end
it 'returns empty array' do
- get api("/projects/#{project.id}/search", user), params: { scope: 'milestones', search: 'awesome' }
+ get api(endpoint, user), params: { scope: 'milestones', search: 'awesome' }
milestones = json_response
@@ -370,16 +486,24 @@ describe API::Search do
user1 = create(:user, name: 'billy')
create(:project_member, :developer, user: user1, project: project)
- get api("/projects/#{project.id}/search", user), params: { scope: 'users', search: 'billy' }
+ get api(endpoint, user), params: { scope: 'users', search: 'billy' }
end
it_behaves_like 'response is correct', schema: 'public_api/v4/user/basics'
+ describe 'pagination' do
+ before do
+ create(:project_member, :developer, project: project)
+ end
+
+ include_examples 'pagination', scope: :users
+ end
+
context 'when users search feature is disabled' do
before do
- allow(Feature).to receive(:disabled?).with(:users_search, default_enabled: true).and_return(true)
+ stub_feature_flags(users_search: false)
- get api("/projects/#{project.id}/search", user), params: { scope: 'users', search: 'billy' }
+ get api(endpoint, user), params: { scope: 'users', search: 'billy' }
end
it 'returns 400 error' do
@@ -392,29 +516,51 @@ describe API::Search do
before do
create(:note_on_merge_request, project: project, note: 'awesome note')
- get api("/projects/#{project.id}/search", user), params: { scope: 'notes', search: 'awesome' }
+ get api(endpoint, user), params: { scope: 'notes', search: 'awesome' }
end
it_behaves_like 'response is correct', schema: 'public_api/v4/notes'
+
+ describe 'pagination' do
+ before do
+ mr = create(:merge_request, source_project: project, target_branch: 'another_branch')
+ create(:note, project: project, noteable: mr, note: 'another note')
+ end
+
+ include_examples 'pagination', scope: :notes
+ end
end
context 'for wiki_blobs scope' do
+ let(:wiki) { create(:project_wiki, project: project) }
+
before do
- wiki = create(:project_wiki, project: project)
- create(:wiki_page, wiki: wiki, attrs: { title: 'home', content: "Awesome page" })
+ create(:wiki_page, wiki: wiki, title: 'home', content: "Awesome page")
- get api("/projects/#{project.id}/search", user), params: { scope: 'wiki_blobs', search: 'awesome' }
+ get api(endpoint, user), params: { scope: 'wiki_blobs', search: 'awesome' }
end
it_behaves_like 'response is correct', schema: 'public_api/v4/blobs'
+
+ describe 'pagination' do
+ before do
+ create(:wiki_page, wiki: wiki, title: 'home 2', content: 'Another page')
+ end
+
+ include_examples 'pagination', scope: :wiki_blobs, search: 'page'
+ end
end
context 'for commits scope' do
+ let(:endpoint) { "/projects/#{repo_project.id}/search" }
+
before do
- get api("/projects/#{repo_project.id}/search", user), params: { scope: 'commits', search: '498214de67004b1da3d820901307bed2a68a8ef6' }
+ get api(endpoint, user), params: { scope: 'commits', search: '498214de67004b1da3d820901307bed2a68a8ef6' }
end
it_behaves_like 'response is correct', schema: 'public_api/v4/commits_details'
+
+ it_behaves_like 'pagination', scope: :commits, search: 'merge'
end
context 'for commits scope with project path as id' do
@@ -426,15 +572,19 @@ describe API::Search do
end
context 'for blobs scope' do
+ let(:endpoint) { "/projects/#{repo_project.id}/search" }
+
before do
- get api("/projects/#{repo_project.id}/search", user), params: { scope: 'blobs', search: 'monitors' }
+ get api(endpoint, user), params: { scope: 'blobs', search: 'monitors' }
end
it_behaves_like 'response is correct', schema: 'public_api/v4/blobs', size: 2
+ it_behaves_like 'pagination', scope: :blobs, search: 'monitors'
+
context 'filters' do
it 'by filename' do
- get api("/projects/#{repo_project.id}/search", user), params: { scope: 'blobs', search: 'mon filename:PROCESS.md' }
+ get api(endpoint, user), params: { scope: 'blobs', search: 'mon filename:PROCESS.md' }
expect(response).to have_gitlab_http_status(:ok)
expect(json_response.size).to eq(2)
@@ -443,21 +593,21 @@ describe API::Search do
end
it 'by path' do
- get api("/projects/#{repo_project.id}/search", user), params: { scope: 'blobs', search: 'mon path:markdown' }
+ get api(endpoint, user), params: { scope: 'blobs', search: 'mon path:markdown' }
expect(response).to have_gitlab_http_status(:ok)
expect(json_response.size).to eq(8)
end
it 'by extension' do
- get api("/projects/#{repo_project.id}/search", user), params: { scope: 'blobs', search: 'mon extension:md' }
+ get api(endpoint, user), params: { scope: 'blobs', search: 'mon extension:md' }
expect(response).to have_gitlab_http_status(:ok)
expect(json_response.size).to eq(11)
end
it 'by ref' do
- get api("/projects/#{repo_project.id}/search", user), params: { scope: 'blobs', search: 'This file is used in tests for ci_environments_status', ref: 'pages-deploy' }
+ get api(endpoint, user), params: { scope: 'blobs', search: 'This file is used in tests for ci_environments_status', ref: 'pages-deploy' }
expect(response).to have_gitlab_http_status(:ok)
expect(json_response.size).to eq(1)
diff --git a/spec/requests/api/settings_spec.rb b/spec/requests/api/settings_spec.rb
index 4a8b8f70dff..a5b95bc59a5 100644
--- a/spec/requests/api/settings_spec.rb
+++ b/spec/requests/api/settings_spec.rb
@@ -88,7 +88,9 @@ describe API::Settings, 'Settings' do
allow_local_requests_from_system_hooks: false,
push_event_hooks_limit: 2,
push_event_activities_limit: 2,
- snippet_size_limit: 5
+ snippet_size_limit: 5,
+ issues_create_limit: 300,
+ raw_blob_request_limit: 300
}
expect(response).to have_gitlab_http_status(:ok)
@@ -125,6 +127,8 @@ describe API::Settings, 'Settings' do
expect(json_response['push_event_hooks_limit']).to eq(2)
expect(json_response['push_event_activities_limit']).to eq(2)
expect(json_response['snippet_size_limit']).to eq(5)
+ expect(json_response['issues_create_limit']).to eq(300)
+ expect(json_response['raw_blob_request_limit']).to eq(300)
end
end
@@ -155,6 +159,14 @@ describe API::Settings, 'Settings' do
expect(json_response['allow_local_requests_from_hooks_and_services']).to eq(true)
end
+ it 'disables ability to switch to legacy storage' do
+ put api("/application/settings", admin),
+ params: { hashed_storage_enabled: false }
+
+ expect(response).to have_gitlab_http_status(:ok)
+ expect(json_response['hashed_storage_enabled']).to eq(true)
+ end
+
context 'external policy classification settings' do
let(:settings) do
{
diff --git a/spec/requests/api/snippets_spec.rb b/spec/requests/api/snippets_spec.rb
index 3e30dc537e4..c12c95ae2e0 100644
--- a/spec/requests/api/snippets_spec.rb
+++ b/spec/requests/api/snippets_spec.rb
@@ -3,7 +3,7 @@
require 'spec_helper'
describe API::Snippets do
- let!(:user) { create(:user) }
+ let_it_be(:user) { create(:user) }
describe 'GET /snippets/' do
it 'returns snippets available' do
@@ -90,7 +90,7 @@ describe API::Snippets do
describe 'GET /snippets/:id/raw' do
let_it_be(:author) { create(:user) }
- let_it_be(:snippet) { create(:personal_snippet, :private, author: author) }
+ let_it_be(:snippet) { create(:personal_snippet, :repository, :private, author: author) }
it 'requires authentication' do
get api("/snippets/#{snippet.id}", nil)
@@ -103,7 +103,6 @@ describe API::Snippets do
expect(response).to have_gitlab_http_status(:ok)
expect(response.content_type).to eq 'text/plain'
- expect(response.body).to eq(snippet.content)
end
it 'forces attachment content disposition' do
@@ -134,6 +133,12 @@ describe API::Snippets do
expect(response).to have_gitlab_http_status(:ok)
end
+
+ it_behaves_like 'snippet blob content' do
+ let_it_be(:snippet_with_empty_repo) { create(:personal_snippet, :empty_repo, :private, author: author) }
+
+ subject { get api("/snippets/#{snippet.id}/raw", snippet.author) }
+ end
end
describe 'GET /snippets/:id' do
@@ -155,22 +160,12 @@ describe API::Snippets do
expect(json_response['title']).to eq(private_snippet.title)
expect(json_response['description']).to eq(private_snippet.description)
- expect(json_response['file_name']).to eq(private_snippet.file_name)
+ expect(json_response['file_name']).to eq(private_snippet.file_name_on_repo)
expect(json_response['visibility']).to eq(private_snippet.visibility)
expect(json_response['ssh_url_to_repo']).to eq(private_snippet.ssh_url_to_repo)
expect(json_response['http_url_to_repo']).to eq(private_snippet.http_url_to_repo)
end
- context 'when feature flag :version_snippets is disabled' do
- before do
- stub_feature_flags(version_snippets: false)
-
- get api("/snippets/#{private_snippet.id}", author)
- end
-
- it_behaves_like 'snippet response without repository URLs'
- end
-
it 'shows private snippets to an admin' do
get api("/snippets/#{private_snippet.id}", admin)
@@ -200,7 +195,7 @@ describe API::Snippets do
end
describe 'POST /snippets/' do
- let(:params) do
+ let(:base_params) do
{
title: 'Test Title',
file_name: 'test.rb',
@@ -209,12 +204,14 @@ describe API::Snippets do
visibility: 'public'
}
end
+ let(:params) { base_params.merge(extra_params) }
+ let(:extra_params) { {} }
+
+ subject { post api("/snippets/", user), params: params }
shared_examples 'snippet creation' do
let(:snippet) { Snippet.find(json_response["id"]) }
- subject { post api("/snippets/", user), params: params }
-
it 'creates a new snippet' do
expect do
subject
@@ -240,18 +237,6 @@ describe API::Snippets do
expect(blob.data).to eq params[:content]
end
-
- context 'when feature flag :version_snippets is disabled' do
- it 'does not create snippet repository' do
- stub_feature_flags(version_snippets: false)
-
- expect do
- subject
- end.to change { PersonalSnippet.count }.by(1)
-
- expect(snippet.repository_exists?).to be_falsey
- end
- end
end
context 'with restricted visibility settings' do
@@ -270,7 +255,7 @@ describe API::Snippets do
let(:user) { create(:user, :external) }
it 'does not create a new snippet' do
- post api("/snippets/", user), params: params
+ subject
expect(response).to have_gitlab_http_status(:forbidden)
end
@@ -279,16 +264,44 @@ describe API::Snippets do
it 'returns 400 for missing parameters' do
params.delete(:title)
- post api("/snippets/", user), params: params
+ subject
expect(response).to have_gitlab_http_status(:bad_request)
end
- context 'when the snippet is spam' do
- def create_snippet(snippet_params = {})
- post api('/snippets', user), params: params.merge(snippet_params)
+ it 'returns 400 if content is blank' do
+ params[:content] = ''
+
+ subject
+
+ expect(response).to have_gitlab_http_status(:bad_request)
+ expect(json_response['error']).to eq 'content is empty'
+ end
+
+ it 'returns 400 if title is blank' do
+ params[:title] = ''
+
+ subject
+
+ expect(response).to have_gitlab_http_status(:bad_request)
+ expect(json_response['error']).to eq 'title is empty'
+ end
+
+ context 'when save fails because the repository could not be created' do
+ before do
+ allow_next_instance_of(Snippets::CreateService) do |instance|
+ allow(instance).to receive(:create_repository).and_raise(Snippets::CreateService::CreateRepositoryError)
+ end
end
+ it 'returns 400' do
+ subject
+
+ expect(response).to have_gitlab_http_status(:bad_request)
+ end
+ end
+
+ context 'when the snippet is spam' do
before do
allow_next_instance_of(Spam::AkismetService) do |instance|
allow(instance).to receive(:spam?).and_return(true)
@@ -296,23 +309,25 @@ describe API::Snippets do
end
context 'when the snippet is private' do
+ let(:extra_params) { { visibility: 'private' } }
+
it 'creates the snippet' do
- expect { create_snippet(visibility: 'private') }
- .to change { Snippet.count }.by(1)
+ expect { subject }.to change { Snippet.count }.by(1)
end
end
context 'when the snippet is public' do
+ let(:extra_params) { { visibility: 'public' } }
+
it 'rejects the shippet' do
- expect { create_snippet(visibility: 'public') }
- .not_to change { Snippet.count }
+ expect { subject }.not_to change { Snippet.count }
expect(response).to have_gitlab_http_status(:bad_request)
expect(json_response['message']).to eq({ "error" => "Spam detected" })
end
it 'creates a spam log' do
- expect { create_snippet(visibility: 'public') }
+ expect { subject }
.to log_spam(title: 'Test Title', user_id: user.id, noteable_type: 'PersonalSnippet')
end
end
@@ -320,8 +335,9 @@ describe API::Snippets do
end
describe 'PUT /snippets/:id' do
+ let_it_be(:other_user) { create(:user) }
+
let(:visibility_level) { Snippet::PUBLIC }
- let(:other_user) { create(:user) }
let(:snippet) do
create(:personal_snippet, :repository, author: user, visibility_level: visibility_level)
end
@@ -373,6 +389,20 @@ describe API::Snippets do
expect(response).to have_gitlab_http_status(:bad_request)
end
+ it 'returns 400 if content is blank' do
+ update_snippet(params: { content: '' })
+
+ expect(response).to have_gitlab_http_status(:bad_request)
+ expect(json_response['error']).to eq 'content is empty'
+ end
+
+ it 'returns 400 if title is blank' do
+ update_snippet(params: { title: '' })
+
+ expect(response).to have_gitlab_http_status(:bad_request)
+ expect(json_response['error']).to eq 'title is empty'
+ end
+
it_behaves_like 'update with repository actions' do
let(:snippet_without_repo) { create(:personal_snippet, author: user, visibility_level: visibility_level) }
end
@@ -424,6 +454,32 @@ describe API::Snippets do
end
end
+ context "when admin" do
+ let(:admin) { create(:admin) }
+ let(:token) { create(:personal_access_token, user: admin, scopes: [:sudo]) }
+
+ subject do
+ put api("/snippets/#{snippet.id}", admin, personal_access_token: token), params: { visibility: 'private', sudo: user.id }
+ end
+
+ context 'when sudo is defined' do
+ it 'returns 200 and updates snippet visibility' do
+ expect(snippet.visibility).not_to eq('private')
+
+ subject
+
+ expect(response).to have_gitlab_http_status(:success)
+ expect(json_response["visibility"]).to eq 'private'
+ end
+
+ it 'does not commit data' do
+ expect_any_instance_of(SnippetRepository).not_to receive(:multi_files_action)
+
+ subject
+ end
+ end
+ end
+
def update_snippet(snippet_id: snippet.id, params: {}, requester: user)
put api("/snippets/#{snippet_id}", requester), params: params
end
diff --git a/spec/requests/api/statistics_spec.rb b/spec/requests/api/statistics_spec.rb
index f03c1e9ca64..5aea5c225a0 100644
--- a/spec/requests/api/statistics_spec.rb
+++ b/spec/requests/api/statistics_spec.rb
@@ -4,7 +4,7 @@ require 'spec_helper'
describe API::Statistics, 'Statistics' do
include ProjectForksHelper
- TABLES_TO_ANALYZE = %w[
+ tables_to_analyze = %w[
projects
users
namespaces
@@ -62,7 +62,7 @@ describe API::Statistics, 'Statistics' do
# Make sure the reltuples have been updated
# to get a correct count on postgresql
- TABLES_TO_ANALYZE.each do |table|
+ tables_to_analyze.each do |table|
ActiveRecord::Base.connection.execute("ANALYZE #{table}")
end
diff --git a/spec/requests/api/terraform/state_spec.rb b/spec/requests/api/terraform/state_spec.rb
index 88c277f4e08..844cd948411 100644
--- a/spec/requests/api/terraform/state_spec.rb
+++ b/spec/requests/api/terraform/state_spec.rb
@@ -78,6 +78,14 @@ describe API::Terraform::State do
expect(response).to have_gitlab_http_status(:ok)
end
+
+ context 'on Unicorn', :unicorn do
+ it 'updates the state' do
+ expect { request }.to change { Terraform::State.count }.by(0)
+
+ expect(response).to have_gitlab_http_status(:ok)
+ end
+ end
end
context 'without body' do
@@ -112,6 +120,14 @@ describe API::Terraform::State do
expect(response).to have_gitlab_http_status(:ok)
end
+
+ context 'on Unicorn', :unicorn do
+ it 'creates a new state' do
+ expect { request }.to change { Terraform::State.count }.by(1)
+
+ expect(response).to have_gitlab_http_status(:ok)
+ end
+ end
end
context 'without body' do
diff --git a/spec/requests/api/todos_spec.rb b/spec/requests/api/todos_spec.rb
index 1aa5e21dddb..0bdc71a30e9 100644
--- a/spec/requests/api/todos_spec.rb
+++ b/spec/requests/api/todos_spec.rb
@@ -159,6 +159,46 @@ describe API::Todos do
expect { get api('/todos', john_doe) }.not_to exceed_query_limit(control)
expect(response).to have_gitlab_http_status(:ok)
end
+
+ context 'when there is a Design Todo' do
+ let!(:design_todo) { create_todo_for_mentioned_in_design }
+
+ def create_todo_for_mentioned_in_design
+ issue = create(:issue, project: project_1)
+ create(:todo, :mentioned,
+ user: john_doe,
+ project: project_1,
+ target: create(:design, issue: issue),
+ author: create(:user),
+ note: create(:note, project: project_1, note: "I am note, hear me roar"))
+ end
+
+ def api_request
+ get api('/todos', john_doe)
+ end
+
+ before do
+ api_request
+ end
+
+ specify do
+ expect(response).to have_gitlab_http_status(:ok)
+ end
+
+ it 'avoids N+1 queries', :request_store do
+ control = ActiveRecord::QueryRecorder.new { api_request }
+
+ create_todo_for_mentioned_in_design
+
+ expect { api_request }.not_to exceed_query_limit(control)
+ end
+
+ it 'includes the Design Todo in the response' do
+ expect(json_response).to include(
+ a_hash_including('id' => design_todo.id)
+ )
+ end
+ end
end
describe 'POST /todos/:id/mark_as_done' do
@@ -235,6 +275,7 @@ describe API::Todos do
expect(json_response['state']).to eq('pending')
expect(json_response['action_name']).to eq('marked')
expect(json_response['created_at']).to be_present
+ expect(json_response['updated_at']).to be_present
end
it 'returns 304 there already exist a todo on that issuable' do
diff --git a/spec/requests/api/users_spec.rb b/spec/requests/api/users_spec.rb
index 864f6f77f39..4a0f0eea088 100644
--- a/spec/requests/api/users_spec.rb
+++ b/spec/requests/api/users_spec.rb
@@ -734,7 +734,7 @@ describe API::Users, :do_not_mock_admin_mode do
end
describe "PUT /users/:id" do
- let!(:admin_user) { create(:admin) }
+ let_it_be(:admin_user) { create(:admin) }
it "returns 200 OK on success" do
put api("/users/#{user.id}", admin), params: { bio: 'new test bio' }
@@ -2405,8 +2405,8 @@ describe API::Users, :do_not_mock_admin_mode do
end
context "user activities", :clean_gitlab_redis_shared_state do
- let!(:old_active_user) { create(:user, last_activity_on: Time.utc(2000, 1, 1)) }
- let!(:newly_active_user) { create(:user, last_activity_on: 2.days.ago.midday) }
+ let_it_be(:old_active_user) { create(:user, last_activity_on: Time.utc(2000, 1, 1)) }
+ let_it_be(:newly_active_user) { create(:user, last_activity_on: 2.days.ago.midday) }
context 'last activity as normal user' do
it 'has no permission' do
diff --git a/spec/requests/api/wikis_spec.rb b/spec/requests/api/wikis_spec.rb
index 7bd9a178a8d..43a5cb446bb 100644
--- a/spec/requests/api/wikis_spec.rb
+++ b/spec/requests/api/wikis_spec.rb
@@ -25,8 +25,8 @@ describe API::Wikis do
shared_examples_for 'returns list of wiki pages' do
context 'when wiki has pages' do
let!(:pages) do
- [create(:wiki_page, wiki: project_wiki, attrs: { title: 'page1', content: 'content of page1' }),
- create(:wiki_page, wiki: project_wiki, attrs: { title: 'page2.with.dot', content: 'content of page2' })]
+ [create(:wiki_page, wiki: project_wiki, title: 'page1', content: 'content of page1'),
+ create(:wiki_page, wiki: project_wiki, title: 'page2.with.dot', content: 'content of page2')]
end
it 'returns the list of wiki pages without content' do
diff --git a/spec/requests/jwt_controller_spec.rb b/spec/requests/jwt_controller_spec.rb
index 73dc9d8c63e..d860179f0a7 100644
--- a/spec/requests/jwt_controller_spec.rb
+++ b/spec/requests/jwt_controller_spec.rb
@@ -7,11 +7,26 @@ describe JwtController do
let(:service_class) { double(new: service) }
let(:service_name) { 'test' }
let(:parameters) { { service: service_name } }
+ let(:log_output) { StringIO.new }
+ let(:logger) do
+ Logger.new(log_output).tap { |logger| logger.formatter = ->(_, _, _, msg) { msg } }
+ end
+ let(:log_data) { Gitlab::Json.parse(log_output.string) }
before do
+ Lograge.logger = logger
+
stub_const('JwtController::SERVICES', service_name => service_class)
end
+ shared_examples 'user logging' do
+ it 'logs username and ID' do
+ expect(log_data['username']).to eq(user.username)
+ expect(log_data['user_id']).to eq(user.id)
+ expect(log_data['meta.user']).to eq(user.username)
+ end
+ end
+
context 'existing service' do
subject! { get '/jwt/auth', params: parameters }
@@ -37,14 +52,17 @@ describe JwtController do
end
context 'using CI token' do
- let(:build) { create(:ci_build, :running) }
+ let(:user) { create(:user) }
+ let(:build) { create(:ci_build, :running, user: user) }
let(:project) { build.project }
let(:headers) { { authorization: credentials('gitlab-ci-token', build.token) } }
context 'project with enabled CI' do
subject! { get '/jwt/auth', params: parameters, headers: headers }
- it { expect(service_class).to have_received(:new).with(project, nil, ActionController::Parameters.new(parameters).permit!) }
+ it { expect(service_class).to have_received(:new).with(project, user, ActionController::Parameters.new(parameters).permit!) }
+
+ it_behaves_like 'user logging'
end
context 'project with disabled CI' do
@@ -57,8 +75,23 @@ describe JwtController do
it { expect(response).to have_gitlab_http_status(:unauthorized) }
end
+ context 'using deploy tokens' do
+ let(:deploy_token) { create(:deploy_token, read_registry: true, projects: [project]) }
+ let(:headers) { { authorization: credentials(deploy_token.username, deploy_token.token) } }
+
+ subject! { get '/jwt/auth', params: parameters, headers: headers }
+
+ it 'authenticates correctly' do
+ expect(response).to have_gitlab_http_status(:ok)
+ expect(service_class).to have_received(:new).with(nil, deploy_token, ActionController::Parameters.new(parameters).permit!)
+ end
+
+ it 'does not log a user' do
+ expect(log_data.keys).not_to include(%w(username user_id))
+ end
+ end
+
context 'using personal access tokens' do
- let(:user) { create(:user) }
let(:pat) { create(:personal_access_token, user: user, scopes: ['read_registry']) }
let(:headers) { { authorization: credentials('personal_access_token', pat.token) } }
@@ -74,6 +107,7 @@ describe JwtController do
end
it_behaves_like 'rejecting a blocked user'
+ it_behaves_like 'user logging'
end
end
@@ -104,6 +138,8 @@ describe JwtController do
end
it { expect(service_class).to have_received(:new).with(nil, user, service_parameters) }
+
+ it_behaves_like 'user logging'
end
context 'when user has 2FA enabled' do
diff --git a/spec/requests/rack_attack_global_spec.rb b/spec/requests/rack_attack_global_spec.rb
index da0ca4c197a..175c5dd0088 100644
--- a/spec/requests/rack_attack_global_spec.rb
+++ b/spec/requests/rack_attack_global_spec.rb
@@ -262,20 +262,6 @@ describe 'Rack Attack global throttles' do
expect_rejection { post protected_path_that_does_not_require_authentication, params: post_params }
end
-
- context 'when Omnibus throttle should be used' do
- before do
- allow(Gitlab::Throttle)
- .to receive(:should_use_omnibus_protected_paths?).and_return(true)
- end
-
- it 'allows requests over the rate limit' do
- (1 + requests_per_period).times do
- post protected_path_that_does_not_require_authentication, params: post_params
- expect(response).to have_gitlab_http_status(:ok)
- end
- end
- end
end
end
@@ -311,28 +297,6 @@ describe 'Rack Attack global throttles' do
it_behaves_like 'rate-limited token-authenticated requests'
end
-
- context 'when Omnibus throttle should be used' do
- let(:request_args) { [api(api_partial_url, personal_access_token: token)] }
- let(:other_user_request_args) { [api(api_partial_url, personal_access_token: other_user_token)] }
-
- before do
- settings_to_set[:"#{throttle_setting_prefix}_requests_per_period"] = requests_per_period
- settings_to_set[:"#{throttle_setting_prefix}_period_in_seconds"] = period_in_seconds
- settings_to_set[:"#{throttle_setting_prefix}_enabled"] = true
- stub_application_setting(settings_to_set)
-
- allow(Gitlab::Throttle)
- .to receive(:should_use_omnibus_protected_paths?).and_return(true)
- end
-
- it 'allows requests over the rate limit' do
- (1 + requests_per_period).times do
- post(*request_args)
- expect(response).not_to have_gitlab_http_status(:too_many_requests)
- end
- end
- end
end
describe 'web requests authenticated with regular login' do
@@ -352,27 +316,6 @@ describe 'Rack Attack global throttles' do
end
it_behaves_like 'rate-limited web authenticated requests'
-
- context 'when Omnibus throttle should be used' do
- before do
- settings_to_set[:"#{throttle_setting_prefix}_requests_per_period"] = requests_per_period
- settings_to_set[:"#{throttle_setting_prefix}_period_in_seconds"] = period_in_seconds
- settings_to_set[:"#{throttle_setting_prefix}_enabled"] = true
- stub_application_setting(settings_to_set)
-
- allow(Gitlab::Throttle)
- .to receive(:should_use_omnibus_protected_paths?).and_return(true)
-
- login_as(user)
- end
-
- it 'allows requests over the rate limit' do
- (1 + requests_per_period).times do
- post url_that_requires_authentication
- expect(response).not_to have_gitlab_http_status(:too_many_requests)
- end
- end
- end
end
end
end
diff --git a/spec/requests/user_activity_spec.rb b/spec/requests/user_activity_spec.rb
index 3cd4911098a..b24760d475b 100644
--- a/spec/requests/user_activity_spec.rb
+++ b/spec/requests/user_activity_spec.rb
@@ -24,8 +24,8 @@ describe 'Update of user activity' do
'/dashboard/snippets',
'/dashboard/groups',
'/dashboard/todos',
- '/group/project/issues',
- '/group/project/issues/10',
+ '/group/project/-/issues',
+ '/group/project/-/issues/10',
'/group/project/-/merge_requests',
'/group/project/-/merge_requests/15'
]
diff --git a/spec/routing/admin_routing_spec.rb b/spec/routing/admin_routing_spec.rb
index 10cf76b607f..25216b0c712 100644
--- a/spec/routing/admin_routing_spec.rb
+++ b/spec/routing/admin_routing_spec.rb
@@ -113,13 +113,6 @@ describe Admin::HookLogsController, 'routing' do
end
end
-# admin_logs GET /admin/logs(.:format) admin/logs#show
-describe Admin::LogsController, "routing" do
- it "to #show" do
- expect(get("/admin/logs")).to route_to('admin/logs#show')
- end
-end
-
# admin_background_jobs GET /admin/background_jobs(.:format) admin/background_jobs#show
describe Admin::BackgroundJobsController, "routing" do
it "to #show" do
diff --git a/spec/routing/project_routing_spec.rb b/spec/routing/project_routing_spec.rb
index fd6cccba959..f8e1ccac912 100644
--- a/spec/routing/project_routing_spec.rb
+++ b/spec/routing/project_routing_spec.rb
@@ -143,8 +143,6 @@ describe 'project routing' do
expect(get("/gitlab/gitlabhq/-/autocomplete_sources/#{action}")).to route_to("projects/autocomplete_sources##{action}", namespace_id: 'gitlab', project_id: 'gitlabhq')
end
end
-
- it_behaves_like 'redirecting a legacy project path', "/gitlab/gitlabhq/autocomplete_sources/labels", "/gitlab/gitlabhq/-/autocomplete_sources/labels"
end
# pages_project_wikis GET /:project_id/wikis/pages(.:format) projects/wikis#pages
@@ -220,8 +218,6 @@ describe 'project routing' do
expect(delete('/gitlab/gitlabhq/-/branches/feature%2B45/foo/bar/baz')).to route_to('projects/branches#destroy', namespace_id: 'gitlab', project_id: 'gitlabhq', id: 'feature+45/foo/bar/baz')
expect(delete('/gitlab/gitlabhq/-/branches/feature@45/foo/bar/baz')).to route_to('projects/branches#destroy', namespace_id: 'gitlab', project_id: 'gitlabhq', id: 'feature@45/foo/bar/baz')
end
-
- it_behaves_like 'redirecting a legacy project path', "/gitlab/gitlabhq/branches", "/gitlab/gitlabhq/-/branches"
end
describe Projects::TagsController, 'routing' do
@@ -234,6 +230,8 @@ describe 'project routing' do
expect(delete('/gitlab/gitlabhq/-/tags/feature%2B45/foo/bar/baz')).to route_to('projects/tags#destroy', namespace_id: 'gitlab', project_id: 'gitlabhq', id: 'feature+45/foo/bar/baz')
expect(delete('/gitlab/gitlabhq/-/tags/feature@45/foo/bar/baz')).to route_to('projects/tags#destroy', namespace_id: 'gitlab', project_id: 'gitlabhq', id: 'feature@45/foo/bar/baz')
end
+
+ it_behaves_like 'redirecting a legacy project path', "/gitlab/gitlabhq/tags", "/gitlab/gitlabhq/-/tags"
end
# project_deploy_keys GET /:project_id/deploy_keys(.:format) deploy_keys#index
@@ -249,8 +247,6 @@ describe 'project routing' do
let(:controller) { 'deploy_keys' }
let(:controller_path) { '/-/deploy_keys' }
end
-
- it_behaves_like 'redirecting a legacy project path', "/gitlab/gitlabhq/deploy_keys", "/gitlab/gitlabhq/-/deploy_keys"
end
# project_protected_branches GET /:project_id/protected_branches(.:format) protected_branches#index
@@ -487,7 +483,6 @@ describe 'project routing' do
let(:controller_path) { '/-/project_members' }
end
- it_behaves_like 'redirecting a legacy project path', "/gitlab/gitlabhq/project_members", "/gitlab/gitlabhq/-/project_members"
it_behaves_like 'redirecting a legacy project path', "/gitlab/gitlabhq/-/settings/members", "/gitlab/gitlabhq/-/project_members"
end
@@ -509,8 +504,6 @@ describe 'project routing' do
it 'to #promote' do
expect(post('/gitlab/gitlabhq/-/milestones/1/promote')).to route_to('projects/milestones#promote', namespace_id: 'gitlab', project_id: 'gitlabhq', id: "1")
end
-
- it_behaves_like 'redirecting a legacy project path', "/gitlab/gitlabhq/milestones", "/gitlab/gitlabhq/-/milestones"
end
# project_labels GET /:project_id/labels(.:format) labels#index
@@ -518,8 +511,6 @@ describe 'project routing' do
it 'to #index' do
expect(get('/gitlab/gitlabhq/-/labels')).to route_to('projects/labels#index', namespace_id: 'gitlab', project_id: 'gitlabhq')
end
-
- it_behaves_like 'redirecting a legacy project path', "/gitlab/gitlabhq/labels", "/gitlab/gitlabhq/-/labels"
end
# sort_project_issues POST /:project_id/issues/sort(.:format) issues#sort
@@ -534,13 +525,17 @@ describe 'project routing' do
# DELETE /:project_id/issues/:id(.:format) issues#destroy
describe Projects::IssuesController, 'routing' do
it 'to #bulk_update' do
- expect(post('/gitlab/gitlabhq/issues/bulk_update')).to route_to('projects/issues#bulk_update', namespace_id: 'gitlab', project_id: 'gitlabhq')
+ expect(post('/gitlab/gitlabhq/-/issues/bulk_update')).to route_to('projects/issues#bulk_update', namespace_id: 'gitlab', project_id: 'gitlabhq')
end
it_behaves_like 'RESTful project resources' do
let(:controller) { 'issues' }
let(:actions) { [:index, :create, :new, :edit, :show, :update] }
+ let(:controller_path) { '/-/issues' }
end
+
+ it_behaves_like 'redirecting a legacy project path', "/gitlab/gitlabhq/issues", "/gitlab/gitlabhq/-/issues"
+ it_behaves_like 'redirecting a legacy project path', "/gitlab/gitlabhq/issues/1/edit", "/gitlab/gitlabhq/-/issues/1/edit"
end
# project_noteable_notes GET /:project_id/noteable/:target_type/:target_id/notes notes#index
@@ -719,8 +714,6 @@ describe 'project routing' do
expect(get('/gitlab/gitlabhq/-/network/ends-with.json')).to route_to('projects/network#show', namespace_id: 'gitlab', project_id: 'gitlabhq', id: 'ends-with.json')
expect(get('/gitlab/gitlabhq/-/network/master?format=json')).to route_to('projects/network#show', namespace_id: 'gitlab', project_id: 'gitlabhq', id: 'master', format: 'json')
end
-
- it_behaves_like 'redirecting a legacy project path', "/gitlab/gitlabhq/network/master", "/gitlab/gitlabhq/-/network/master"
end
describe Projects::GraphsController, 'routing' do
@@ -729,8 +722,6 @@ describe 'project routing' do
expect(get('/gitlab/gitlabhq/-/graphs/ends-with.json')).to route_to('projects/graphs#show', namespace_id: 'gitlab', project_id: 'gitlabhq', id: 'ends-with.json')
expect(get('/gitlab/gitlabhq/-/graphs/master?format=json')).to route_to('projects/graphs#show', namespace_id: 'gitlab', project_id: 'gitlabhq', id: 'master', format: 'json')
end
-
- it_behaves_like 'redirecting a legacy project path', "/gitlab/gitlabhq/graphs/master", "/gitlab/gitlabhq/-/graphs/master"
end
describe Projects::ForksController, 'routing' do
@@ -741,8 +732,6 @@ describe 'project routing' do
it 'to #create' do
expect(post('/gitlab/gitlabhq/-/forks')).to route_to('projects/forks#create', namespace_id: 'gitlab', project_id: 'gitlabhq')
end
-
- it_behaves_like 'redirecting a legacy project path', "/gitlab/gitlabhq/forks", "/gitlab/gitlabhq/-/forks"
end
# project_avatar DELETE /project/avatar(.:format) projects/avatars#destroy
@@ -751,8 +740,6 @@ describe 'project routing' do
expect(delete('/gitlab/gitlabhq/-/avatar')).to route_to(
'projects/avatars#destroy', namespace_id: 'gitlab', project_id: 'gitlabhq')
end
-
- it_behaves_like 'redirecting a legacy project path', "/gitlab/gitlabhq/avatar", "/gitlab/gitlabhq/-/avatar"
end
describe Projects::PagesDomainsController, 'routing' do
@@ -798,8 +785,6 @@ describe 'project routing' do
expect(get('/gitlab/gitlabhq/-/settings/repository')).to route_to('projects/settings/repository#show', namespace_id: 'gitlab', project_id: 'gitlabhq')
end
- it_behaves_like 'redirecting a legacy project path', "/gitlab/gitlabhq/settings/repository", "/gitlab/gitlabhq/-/settings/repository"
-
it 'to repository#create_deploy_token' do
expect(post('gitlab/gitlabhq/-/settings/ci_cd/deploy_token/create')).to route_to('projects/settings/repository#create_deploy_token', namespace_id: 'gitlab', project_id: 'gitlabhq')
end
@@ -835,12 +820,18 @@ describe 'project routing' do
it 'routes to usage_ping#web_ide_clientside_preview' do
expect(post('/gitlab/gitlabhq/usage_ping/web_ide_clientside_preview')).to route_to('projects/usage_ping#web_ide_clientside_preview', namespace_id: 'gitlab', project_id: 'gitlabhq')
end
+
+ it 'routes to usage_ping#web_ide_pipelines_count' do
+ expect(post('/gitlab/gitlabhq/usage_ping/web_ide_pipelines_count')).to route_to('projects/usage_ping#web_ide_pipelines_count', namespace_id: 'gitlab', project_id: 'gitlabhq')
+ end
end
describe Projects::StaticSiteEditorController, 'routing' do
it 'routes to static_site_editor#show', :aggregate_failures do
- expect(get('/gitlab/gitlabhq/-/sse/master/CONTRIBUTING.md')).to route_to('projects/static_site_editor#show', namespace_id: 'gitlab', project_id: 'gitlabhq', id: 'master/CONTRIBUTING.md')
- expect(get('/gitlab/gitlabhq/-/sse/master/README')).to route_to('projects/static_site_editor#show', namespace_id: 'gitlab', project_id: 'gitlabhq', id: 'master/README')
+ expect(get('/gitlab/gitlabhq/-/sse/master%2FCONTRIBUTING.md')).to route_to('projects/static_site_editor#show', namespace_id: 'gitlab', project_id: 'gitlabhq', id: 'master/CONTRIBUTING.md')
+ expect(get('/gitlab/gitlabhq/-/sse/master%2FCONTRIBUTING.md/')).to route_to('projects/static_site_editor#show', namespace_id: 'gitlab', project_id: 'gitlabhq', id: 'master/CONTRIBUTING.md')
+ expect(get('/gitlab/gitlabhq/-/sse/master%2FREADME/unsupported/error')).to route_to('projects/static_site_editor#show', namespace_id: 'gitlab', project_id: 'gitlabhq', id: 'master/README', vueroute: 'unsupported/error')
+ expect(get('/gitlab/gitlabhq/-/sse/master%2Flib%2FREADME/success')).to route_to('projects/static_site_editor#show', namespace_id: 'gitlab', project_id: 'gitlabhq', id: 'master/lib/README', vueroute: 'success')
end
end
@@ -867,4 +858,20 @@ describe 'project routing' do
it_behaves_like 'redirecting a legacy project path', "/gitlab/gitlabhq/serverless", "/gitlab/gitlabhq/-/serverless"
end
end
+
+ describe Projects::DesignManagement::Designs::RawImagesController, 'routing' do
+ it 'to #show' do
+ expect(get('/gitlab/gitlabhq/-/design_management/designs/1/raw_image')).to route_to('projects/design_management/designs/raw_images#show', namespace_id: 'gitlab', project_id: 'gitlabhq', design_id: '1')
+ expect(get('/gitlab/gitlabhq/-/design_management/designs/1/c6f00aa50b80887ada30a6fe517670be9f8f9ece/raw_image')).to route_to('projects/design_management/designs/raw_images#show', namespace_id: 'gitlab', project_id: 'gitlabhq', design_id: '1', sha: 'c6f00aa50b80887ada30a6fe517670be9f8f9ece')
+ end
+ end
+
+ describe Projects::DesignManagement::Designs::ResizedImageController, 'routing' do
+ it 'to #show' do
+ expect(get('/gitlab/gitlabhq/-/design_management/designs/1/resized_image/v432x230')).to route_to('projects/design_management/designs/resized_image#show', namespace_id: 'gitlab', project_id: 'gitlabhq', design_id: '1', id: 'v432x230')
+ expect(get('/gitlab/gitlabhq/-/design_management/designs/1/c6f00aa50b80887ada30a6fe517670be9f8f9ece/resized_image/v432x230')).to route_to('projects/design_management/designs/resized_image#show', namespace_id: 'gitlab', project_id: 'gitlabhq', design_id: '1', sha: 'c6f00aa50b80887ada30a6fe517670be9f8f9ece', id: 'v432x230')
+ expect(get('/gitlab/gitlabhq/-/design_management/designs/1/invalid/resized_image/v432x230')).to route_to('application#route_not_found', unmatched_route: 'gitlab/gitlabhq/-/design_management/designs/1/invalid/resized_image/v432x230')
+ expect(get('/gitlab/gitlabhq/-/design_management/designs/1/c6f00aa50b80887ada30a6fe517670be9f8f9ece/resized_image/small')).to route_to('application#route_not_found', unmatched_route: 'gitlab/gitlabhq/-/design_management/designs/1/c6f00aa50b80887ada30a6fe517670be9f8f9ece/resized_image/small')
+ end
+ end
end
diff --git a/spec/routing/routing_spec.rb b/spec/routing/routing_spec.rb
index 61599f0876f..9c3d17f7d8f 100644
--- a/spec/routing/routing_spec.rb
+++ b/spec/routing/routing_spec.rb
@@ -344,3 +344,27 @@ describe SentNotificationsController, 'routing' do
.to route_to('sent_notifications#unsubscribe', id: '4bee17d4a63ed60cf5db53417e9aeb4c')
end
end
+
+describe AutocompleteController, 'routing' do
+ it 'to #users' do
+ expect(get("/-/autocomplete/users")).to route_to('autocomplete#users')
+ end
+
+ it 'to #projects' do
+ expect(get("/-/autocomplete/projects")).to route_to('autocomplete#projects')
+ end
+
+ it 'to #award_emojis' do
+ expect(get("/-/autocomplete/award_emojis")).to route_to('autocomplete#award_emojis')
+ end
+
+ it 'to #merge_request_target_branches' do
+ expect(get("/-/autocomplete/merge_request_target_branches")).to route_to('autocomplete#merge_request_target_branches')
+ end
+
+ it 'to legacy route' do
+ expect(get("/autocomplete/users")).to route_to('autocomplete#users')
+ expect(get("/autocomplete/projects")).to route_to('autocomplete#projects')
+ expect(get("/autocomplete/award_emojis")).to route_to('autocomplete#award_emojis')
+ end
+end
diff --git a/spec/rubocop/cop/avoid_keyword_arguments_in_sidekiq_workers_spec.rb b/spec/rubocop/cop/avoid_keyword_arguments_in_sidekiq_workers_spec.rb
new file mode 100644
index 00000000000..11d63d8e0ee
--- /dev/null
+++ b/spec/rubocop/cop/avoid_keyword_arguments_in_sidekiq_workers_spec.rb
@@ -0,0 +1,49 @@
+# frozen_string_literal: true
+
+require 'spec_helper'
+require 'rubocop'
+require 'rubocop/rspec/support'
+require_relative '../../../rubocop/cop/avoid_keyword_arguments_in_sidekiq_workers'
+
+describe RuboCop::Cop::AvoidKeywordArgumentsInSidekiqWorkers do
+ include CopHelper
+
+ subject(:cop) { described_class.new }
+
+ it 'flags violation for keyword arguments usage in perform method signature' do
+ expect_offense(<<~RUBY)
+ def perform(id:)
+ ^^^^^^^^^^^^^^^^ Do not use keyword arguments in Sidekiq workers. For details, check https://github.com/mperham/sidekiq/issues/2372
+ end
+ RUBY
+ end
+
+ it 'flags violation for optional keyword arguments usage in perform method signature' do
+ expect_offense(<<~RUBY)
+ def perform(id: nil)
+ ^^^^^^^^^^^^^^^^^^^^ Do not use keyword arguments in Sidekiq workers. For details, check https://github.com/mperham/sidekiq/issues/2372
+ end
+ RUBY
+ end
+
+ it 'does not flag a violation for standard optional arguments usage in perform method signature' do
+ expect_no_offenses(<<~RUBY)
+ def perform(id = nil)
+ end
+ RUBY
+ end
+
+ it 'does not flag a violation for keyword arguments usage in non-perform method signatures' do
+ expect_no_offenses(<<~RUBY)
+ def helper(id:)
+ end
+ RUBY
+ end
+
+ it 'does not flag a violation for optional keyword arguments usage in non-perform method signatures' do
+ expect_no_offenses(<<~RUBY)
+ def helper(id: nil)
+ end
+ RUBY
+ end
+end
diff --git a/spec/rubocop/cop/gitlab/change_timezone_spec.rb b/spec/rubocop/cop/gitlab/change_timezone_spec.rb
new file mode 100644
index 00000000000..af76559a9fa
--- /dev/null
+++ b/spec/rubocop/cop/gitlab/change_timezone_spec.rb
@@ -0,0 +1,21 @@
+# frozen_string_literal: true
+
+require 'spec_helper'
+require 'rubocop'
+require 'rubocop/rspec/support'
+require_relative '../../../../rubocop/cop/gitlab/change_timzone'
+
+describe RuboCop::Cop::Gitlab::ChangeTimezone do
+ include CopHelper
+
+ subject(:cop) { described_class.new }
+
+ context 'Time.zone=' do
+ it 'registers an offense with no 2nd argument' do
+ expect_offense(<<~PATTERN.strip_indent)
+ Time.zone = 'Awkland'
+ ^^^^^^^^^^^^^^^^^^^^^ Do not change timezone in the runtime (application or rspec), it could result in silently modifying other behavior.
+ PATTERN
+ end
+ end
+end
diff --git a/spec/rubocop/cop/gitlab/json_spec.rb b/spec/rubocop/cop/gitlab/json_spec.rb
new file mode 100644
index 00000000000..d64f60c8583
--- /dev/null
+++ b/spec/rubocop/cop/gitlab/json_spec.rb
@@ -0,0 +1,39 @@
+# frozen_string_literal: true
+
+require 'spec_helper'
+require 'rubocop'
+require 'rubocop/rspec/support'
+require_relative '../../../../rubocop/cop/gitlab/json'
+
+describe RuboCop::Cop::Gitlab::Json do
+ include CopHelper
+
+ subject(:cop) { described_class.new }
+
+ shared_examples('registering call offense') do |options|
+ let(:offending_lines) { options[:offending_lines] }
+
+ it 'registers an offense when the class calls JSON' do
+ inspect_source(source)
+
+ aggregate_failures do
+ expect(cop.offenses.size).to eq(offending_lines.size)
+ expect(cop.offenses.map(&:line)).to eq(offending_lines)
+ end
+ end
+ end
+
+ context 'when JSON is called' do
+ it_behaves_like 'registering call offense', offending_lines: [3] do
+ let(:source) do
+ <<~RUBY
+ class Foo
+ def bar
+ JSON.parse('{ "foo": "bar" }')
+ end
+ end
+ RUBY
+ end
+ end
+ end
+end
diff --git a/spec/rubocop/cop/inject_enterprise_edition_module_spec.rb b/spec/rubocop/cop/inject_enterprise_edition_module_spec.rb
index ce20d494542..3cb1dbbbc2c 100644
--- a/spec/rubocop/cop/inject_enterprise_edition_module_spec.rb
+++ b/spec/rubocop/cop/inject_enterprise_edition_module_spec.rb
@@ -159,6 +159,17 @@ describe RuboCop::Cop::InjectEnterpriseEditionModule do
SOURCE
end
+ it 'does not flag the double use of `X_if_ee` on the last line' do
+ expect_no_offenses(<<~SOURCE)
+ class Foo
+ end
+
+ Foo.extend_if_ee('EE::Foo')
+ Foo.include_if_ee('EE::Foo')
+ Foo.prepend_if_ee('EE::Foo')
+ SOURCE
+ end
+
it 'autocorrects offenses by just disabling the Cop' do
source = <<~SOURCE
class Foo
diff --git a/spec/rubocop/cop/migration/add_column_with_default_spec.rb b/spec/rubocop/cop/migration/add_column_with_default_spec.rb
index a8cf965a3ef..5d4fc59fb95 100644
--- a/spec/rubocop/cop/migration/add_column_with_default_spec.rb
+++ b/spec/rubocop/cop/migration/add_column_with_default_spec.rb
@@ -27,44 +27,15 @@ describe RuboCop::Cop::Migration::AddColumnWithDefault do
allow(cop).to receive(:in_migration?).and_return(true)
end
- let(:offense) { '`add_column_with_default` without `allow_null: true` may cause prolonged lock situations and downtime, see https://gitlab.com/gitlab-org/gitlab/issues/38060' }
+ let(:offense) { '`add_column_with_default` is deprecated, use `add_column` instead' }
- context 'for blacklisted table' do
- it 'registers an offense when specifying allow_null: false' do
- expect_offense(<<~RUBY)
- def up
- add_column_with_default(:merge_request_diff_files, :artifacts, :boolean, default: true, allow_null: false)
- ^^^^^^^^^^^^^^^^^^^^^^^ #{offense}
- end
- RUBY
- end
-
- it 'registers no offense when specifying allow_null: true' do
- expect_no_offenses(<<~RUBY)
- def up
- add_column_with_default(:merge_request_diff_files, :artifacts, :boolean, default: true, allow_null: true)
- end
- RUBY
- end
-
- it 'registers an offense when allow_null is not specified' do
- expect_offense(<<~RUBY)
- def up
- add_column_with_default(:merge_request_diff_files, :artifacts, :boolean, default: true)
- ^^^^^^^^^^^^^^^^^^^^^^^ #{offense}
- end
- RUBY
- end
- end
-
- context 'for tables not on the blacklist' do
- it 'registers no offense for application_settings (not on blacklist)' do
- expect_no_offenses(<<~RUBY)
- def up
- add_column_with_default(:application_settings, :another_column, :boolean, default: true, allow_null: false)
- end
- RUBY
- end
+ it 'registers an offense ' do
+ expect_offense(<<~RUBY)
+ def up
+ add_column_with_default(:merge_request_diff_files, :artifacts, :boolean, default: true, allow_null: false)
+ ^^^^^^^^^^^^^^^^^^^^^^^ #{offense}
+ end
+ RUBY
end
end
end
diff --git a/spec/rubocop/cop/migration/add_columns_to_wide_tables_spec.rb b/spec/rubocop/cop/migration/add_columns_to_wide_tables_spec.rb
index f0c64740e63..5b179168eab 100644
--- a/spec/rubocop/cop/migration/add_columns_to_wide_tables_spec.rb
+++ b/spec/rubocop/cop/migration/add_columns_to_wide_tables_spec.rb
@@ -42,8 +42,8 @@ describe RuboCop::Cop::Migration::AddColumnsToWideTables do
expect_offense(<<~RUBY)
def up
- add_column_with_default(:users, :another_column, :boolean, default: false)
- ^^^^^^^^^^^^^^^^^^^^^^^ #{offense}
+ add_column(:users, :another_column, :boolean, default: false)
+ ^^^^^^^^^^ #{offense}
end
RUBY
end
diff --git a/spec/rubocop/cop/migration/add_concurrent_foreign_key_spec.rb b/spec/rubocop/cop/migration/add_concurrent_foreign_key_spec.rb
index 419d74c298a..dfc3898af24 100644
--- a/spec/rubocop/cop/migration/add_concurrent_foreign_key_spec.rb
+++ b/spec/rubocop/cop/migration/add_concurrent_foreign_key_spec.rb
@@ -33,5 +33,11 @@ describe RuboCop::Cop::Migration::AddConcurrentForeignKey do
expect(cop.offenses.map(&:line)).to eq([1])
end
end
+
+ it 'does not register an offense when a `NOT VALID` foreign key is added' do
+ inspect_source('def up; add_foreign_key(:projects, :users, column: :user_id, validate: false); end')
+
+ expect(cop.offenses).to be_empty
+ end
end
end
diff --git a/spec/rubocop/cop/migration/add_limit_to_string_columns_spec.rb b/spec/rubocop/cop/migration/add_limit_to_string_columns_spec.rb
deleted file mode 100644
index 97a3ae8f2bc..00000000000
--- a/spec/rubocop/cop/migration/add_limit_to_string_columns_spec.rb
+++ /dev/null
@@ -1,268 +0,0 @@
-# frozen_string_literal: true
-
-require 'spec_helper'
-require 'rubocop'
-require 'rubocop/rspec/support'
-
-require_relative '../../../../rubocop/cop/migration/add_limit_to_string_columns'
-
-describe RuboCop::Cop::Migration::AddLimitToStringColumns do
- include CopHelper
-
- subject(:cop) { described_class.new }
-
- context 'in migration' do
- before do
- allow(cop).to receive(:in_migration?).and_return(true)
-
- inspect_source(migration)
- end
-
- context 'when creating a table' do
- context 'with string columns and limit' do
- let(:migration) do
- %q(
- class CreateUsers < ActiveRecord::Migration[5.2]
- DOWNTIME = false
-
- def change
- create_table :users do |t|
- t.string :username, null: false, limit: 255
- t.timestamps_with_timezone null: true
- end
- end
- end
- )
- end
-
- it 'register no offense' do
- expect(cop.offenses.size).to eq(0)
- end
-
- context 'with limit in a different position' do
- let(:migration) do
- %q(
- class CreateUsers < ActiveRecord::Migration[5.2]
- DOWNTIME = false
-
- def change
- create_table :users do |t|
- t.string :username, limit: 255, null: false
- t.timestamps_with_timezone null: true
- end
- end
- end
- )
- end
-
- it 'registers an offense' do
- expect(cop.offenses.size).to eq(0)
- end
- end
- end
-
- context 'with string columns and no limit' do
- let(:migration) do
- %q(
- class CreateUsers < ActiveRecord::Migration[5.2]
- DOWNTIME = false
-
- def change
- create_table :users do |t|
- t.string :username, null: false
- t.timestamps_with_timezone null: true
- end
- end
- end
- )
- end
-
- it 'registers an offense' do
- expect(cop.offenses.size).to eq(1)
- expect(cop.offenses.first.message)
- .to eq('String columns should have a limit constraint. 255 is suggested')
- end
- end
-
- context 'with no string columns' do
- let(:migration) do
- %q(
- class CreateMilestoneReleases < ActiveRecord::Migration[5.2]
- DOWNTIME = false
-
- def change
- create_table :milestone_releases do |t|
- t.integer :milestone_id
- t.integer :release_id
- end
- end
- end
- )
- end
-
- it 'register no offense' do
- expect(cop.offenses.size).to eq(0)
- end
- end
- end
-
- context 'when adding columns' do
- context 'with string columns with limit' do
- let(:migration) do
- %q(
- class AddEmailToUsers < ActiveRecord::Migration[5.2]
- DOWNTIME = false
-
- def change
- add_column :users, :email, :string, limit: 255
- end
- end
- )
- end
-
- it 'registers no offense' do
- expect(cop.offenses.size).to eq(0)
- end
-
- context 'with limit in a different position' do
- let(:migration) do
- %q(
- class AddEmailToUsers < ActiveRecord::Migration[5.2]
- DOWNTIME = false
-
- def change
- add_column :users, :email, :string, limit: 255, default: 'example@email.com'
- end
- end
- )
- end
-
- it 'registers no offense' do
- expect(cop.offenses.size).to eq(0)
- end
- end
- end
-
- context 'with string columns with no limit' do
- let(:migration) do
- %q(
- class AddEmailToUsers < ActiveRecord::Migration[5.2]
- DOWNTIME = false
-
- def change
- add_column :users, :email, :string
- end
- end
- )
- end
-
- it 'registers offense' do
- expect(cop.offenses.size).to eq(1)
- expect(cop.offenses.first.message)
- .to eq('String columns should have a limit constraint. 255 is suggested')
- end
- end
-
- context 'with no string columns' do
- let(:migration) do
- %q(
- class AddEmailToUsers < ActiveRecord::Migration[5.2]
- DOWNTIME = false
-
- def change
- add_column :users, :active, :boolean, default: false
- end
- end
- )
- end
-
- it 'registers no offense' do
- expect(cop.offenses.size).to eq(0)
- end
- end
- end
-
- context 'with add_column_with_default' do
- context 'with a limit' do
- let(:migration) do
- %q(
- class AddRuleTypeToApprovalMergeRequestRules < ActiveRecord::Migration[5.2]
- DOWNTIME = false
-
- def change
- add_column_with_default(:approval_merge_request_rules, :rule_type, :string, limit: 2, default: 1)
- end
- end
- )
- end
-
- it 'registers no offense' do
- expect(cop.offenses.size).to eq(0)
- end
- end
-
- context 'without a limit' do
- let(:migration) do
- %q(
- class AddRuleTypeToApprovalMergeRequestRules < ActiveRecord::Migration[5.2]
- DOWNTIME = false
-
- def change
- add_column_with_default(:approval_merge_request_rules, :rule_type, :string, default: 1)
- end
- end
- )
- end
-
- it 'registers an offense' do
- expect(cop.offenses.size).to eq(1)
- end
- end
- end
-
- context 'with methods' do
- let(:migration) do
- %q(
- class AddEmailToUsers < ActiveRecord::Migration[5.2]
- DOWNTIME = false
-
- def change
- add_column_if_table_not_exists :users, :first_name, :string, limit: 255
- search_namespace(user_name)
- end
-
- def add_column_if_not_exists(table, name, *args)
- add_column(table, name, *args) unless column_exists?(table, name)
- end
-
- def search_namespace(username)
- Uniquify.new.string(username) do |str|
- query = "SELECT id FROM namespaces WHERE parent_id IS NULL AND path='#{str}' LIMIT 1"
- connection.exec_query(query)
- end
- end
- end
- )
- end
-
- it 'registers no offense' do
- expect(cop.offenses.size).to eq(0)
- end
- end
- end
-
- context 'outside of migrations' do
- let(:active_record_model) do
- %q(
- class User < ApplicationRecord
- end
- )
- end
-
- it 'registers no offense' do
- inspect_source(active_record_model)
-
- expect(cop.offenses.size).to eq(0)
- end
- end
-end
diff --git a/spec/rubocop/cop/migration/add_limit_to_text_columns_spec.rb b/spec/rubocop/cop/migration/add_limit_to_text_columns_spec.rb
new file mode 100644
index 00000000000..514260a4306
--- /dev/null
+++ b/spec/rubocop/cop/migration/add_limit_to_text_columns_spec.rb
@@ -0,0 +1,160 @@
+# frozen_string_literal: true
+
+require 'spec_helper'
+require 'rubocop'
+require 'rubocop/rspec/support'
+
+require_relative '../../../../rubocop/cop/migration/add_limit_to_text_columns'
+
+describe RuboCop::Cop::Migration::AddLimitToTextColumns do
+ include CopHelper
+
+ subject(:cop) { described_class.new }
+
+ context 'in migration' do
+ before do
+ allow(cop).to receive(:in_migration?).and_return(true)
+ end
+
+ context 'when text columns are defined without a limit' do
+ it 'registers an offense' do
+ expect_offense(<<~RUBY)
+ class TestTextLimits < ActiveRecord::Migration[6.0]
+ DOWNTIME = false
+ disable_ddl_transaction!
+
+ def up
+ create_table :test_text_limits, id: false do |t|
+ t.integer :test_id, null: false
+ t.text :name
+ ^^^^ #{described_class::MSG}
+ end
+
+ add_column :test_text_limits, :email, :text
+ ^^^^^^^^^^ #{described_class::MSG}
+
+ add_column_with_default :test_text_limits, :role, :text, default: 'default'
+ ^^^^^^^^^^^^^^^^^^^^^^^ #{described_class::MSG}
+
+ change_column_type_concurrently :test_text_limits, :test_id, :text
+ ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ #{described_class::MSG}
+ end
+ end
+ RUBY
+
+ expect(cop.offenses.map(&:cop_name)).to all(eq('Migration/AddLimitToTextColumns'))
+ end
+ end
+
+ context 'when text columns are defined with a limit' do
+ it 'registers no offense' do
+ expect_no_offenses(<<~RUBY)
+ class TestTextLimits < ActiveRecord::Migration[6.0]
+ DOWNTIME = false
+ disable_ddl_transaction!
+
+ def up
+ create_table :test_text_limits, id: false do |t|
+ t.integer :test_id, null: false
+ t.text :name
+ end
+
+ add_column :test_text_limits, :email, :text
+ add_column_with_default :test_text_limits, :role, :text, default: 'default'
+ change_column_type_concurrently :test_text_limits, :test_id, :text
+
+ add_text_limit :test_text_limits, :name, 255
+ add_text_limit :test_text_limits, :email, 255
+ add_text_limit :test_text_limits, :role, 255
+ add_text_limit :test_text_limits, :test_id, 255
+ end
+ end
+ RUBY
+ end
+ end
+
+ # Make sure that the cop is properly checking for an `add_text_limit`
+ # over the same {table, attribute} as the one that triggered the offence
+ context 'when the limit is defined for a same name attribute but different table' do
+ it 'registers an offense' do
+ expect_offense(<<~RUBY)
+ class TestTextLimits < ActiveRecord::Migration[6.0]
+ DOWNTIME = false
+ disable_ddl_transaction!
+
+ def up
+ create_table :test_text_limits, id: false do |t|
+ t.integer :test_id, null: false
+ t.text :name
+ ^^^^ #{described_class::MSG}
+ end
+
+ add_column :test_text_limits, :email, :text
+ ^^^^^^^^^^ #{described_class::MSG}
+
+ add_column_with_default :test_text_limits, :role, :text, default: 'default'
+ ^^^^^^^^^^^^^^^^^^^^^^^ #{described_class::MSG}
+
+ change_column_type_concurrently :test_text_limits, :test_id, :text
+ ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ #{described_class::MSG}
+
+ add_text_limit :wrong_table, :name, 255
+ add_text_limit :wrong_table, :email, 255
+ add_text_limit :wrong_table, :role, 255
+ add_text_limit :wrong_table, :test_id, 255
+ end
+ end
+ RUBY
+
+ expect(cop.offenses.map(&:cop_name)).to all(eq('Migration/AddLimitToTextColumns'))
+ end
+ end
+
+ context 'on down' do
+ it 'registers no offense' do
+ expect_no_offenses(<<~RUBY)
+ class TestTextLimits < ActiveRecord::Migration[6.0]
+ DOWNTIME = false
+
+ def up
+ drop_table :no_offence_on_down
+ end
+
+ def down
+ create_table :no_offence_on_down, id: false do |t|
+ t.integer :test_id, null: false
+ t.text :name
+ end
+
+ add_column :no_offence_on_down, :email, :text
+
+ add_column_with_default :no_offence_on_down, :role, :text, default: 'default'
+ end
+ end
+ RUBY
+ end
+ end
+ end
+
+ context 'outside of migration' do
+ it 'registers no offense' do
+ expect_no_offenses(<<~RUBY)
+ class TestTextLimits < ActiveRecord::Migration[6.0]
+ DOWNTIME = false
+ disable_ddl_transaction!
+
+ def up
+ create_table :test_text_limits, id: false do |t|
+ t.integer :test_id, null: false
+ t.text :name
+ end
+
+ add_column :test_text_limits, :email, :text
+ add_column_with_default :test_text_limits, :role, :text, default: 'default'
+ change_column_type_concurrently :test_text_limits, :test_id, :text
+ end
+ end
+ RUBY
+ end
+ end
+end
diff --git a/spec/rubocop/cop/migration/prevent_strings_spec.rb b/spec/rubocop/cop/migration/prevent_strings_spec.rb
new file mode 100644
index 00000000000..2702ce1c090
--- /dev/null
+++ b/spec/rubocop/cop/migration/prevent_strings_spec.rb
@@ -0,0 +1,143 @@
+# frozen_string_literal: true
+
+require 'spec_helper'
+require 'rubocop'
+require 'rubocop/rspec/support'
+
+require_relative '../../../../rubocop/cop/migration/prevent_strings'
+
+describe RuboCop::Cop::Migration::PreventStrings do
+ include CopHelper
+
+ subject(:cop) { described_class.new }
+
+ context 'in migration' do
+ before do
+ allow(cop).to receive(:in_migration?).and_return(true)
+ end
+
+ context 'when the string data type is used' do
+ it 'registers an offense' do
+ expect_offense(<<~RUBY)
+ class Users < ActiveRecord::Migration[6.0]
+ DOWNTIME = false
+
+ def up
+ create_table :users do |t|
+ t.string :username, null: false
+ ^^^^^^ #{described_class::MSG}
+
+ t.timestamps_with_timezone null: true
+
+ t.string :password
+ ^^^^^^ #{described_class::MSG}
+ end
+
+ add_column(:users, :bio, :string)
+ ^^^^^^^^^^ #{described_class::MSG}
+
+ add_column_with_default(:users, :url, :string, default: '/-/user', allow_null: false, limit: 255)
+ ^^^^^^^^^^^^^^^^^^^^^^^ #{described_class::MSG}
+
+ change_column_type_concurrently :users, :commit_id, :string
+ ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ #{described_class::MSG}
+ end
+ end
+ RUBY
+
+ expect(cop.offenses.map(&:cop_name)).to all(eq('Migration/PreventStrings'))
+ end
+ end
+
+ context 'when the string data type is not used' do
+ it 'registers no offense' do
+ expect_no_offenses(<<~RUBY)
+ class Users < ActiveRecord::Migration[6.0]
+ DOWNTIME = false
+
+ def up
+ create_table :users do |t|
+ t.integer :not_a_string, null: false
+ t.timestamps_with_timezone null: true
+ end
+
+ add_column(:users, :not_a_string, :integer)
+ end
+ end
+ RUBY
+ end
+ end
+
+ context 'when the text data type is used' do
+ it 'registers no offense' do
+ expect_no_offenses(<<~RUBY)
+ class Users < ActiveRecord::Migration[6.0]
+ DOWNTIME = false
+
+ def up
+ create_table :users do |t|
+ t.text :username, null: false
+ t.timestamps_with_timezone null: true
+ t.text :password
+ end
+
+ add_column(:users, :bio, :text)
+ add_column_with_default(:users, :url, :text, default: '/-/user', allow_null: false, limit: 255)
+ change_column_type_concurrently :users, :commit_id, :text
+ end
+ end
+ RUBY
+ end
+ end
+
+ context 'on down' do
+ it 'registers no offense' do
+ expect_no_offenses(<<~RUBY)
+ class Users < ActiveRecord::Migration[6.0]
+ DOWNTIME = false
+
+ def up
+ remove_column :users, :bio
+ remove_column :users, :url
+
+ drop_table :test_strings
+ end
+
+ def down
+ create_table :test_strings, id: false do |t|
+ t.integer :test_id, null: false
+ t.string :name
+ end
+
+ add_column(:users, :bio, :string)
+ add_column_with_default(:users, :url, :string, default: '/-/user', allow_null: false, limit: 255)
+ change_column_type_concurrently :users, :commit_id, :string
+ end
+ end
+ RUBY
+ end
+ end
+ end
+
+ context 'outside of migration' do
+ it 'registers no offense' do
+ expect_no_offenses(<<~RUBY)
+ class Users < ActiveRecord::Migration[6.0]
+ DOWNTIME = false
+
+ def up
+ create_table :users do |t|
+ t.string :username, null: false
+ t.timestamps_with_timezone null: true
+ t.string :password
+ end
+
+ add_column(:users, :bio, :string)
+ add_column_with_default(:users, :url, :string, default: '/-/user', allow_null: false, limit: 255)
+ change_column_type_concurrently :users, :commit_id, :string
+ end
+ end
+ RUBY
+ end
+ end
+end
diff --git a/spec/rubocop/cop/migration/reversible_add_column_with_default_spec.rb b/spec/rubocop/cop/migration/reversible_add_column_with_default_spec.rb
deleted file mode 100644
index b3c5b855004..00000000000
--- a/spec/rubocop/cop/migration/reversible_add_column_with_default_spec.rb
+++ /dev/null
@@ -1,43 +0,0 @@
-# frozen_string_literal: true
-
-require 'spec_helper'
-
-require 'rubocop'
-require 'rubocop/rspec/support'
-
-require_relative '../../../../rubocop/cop/migration/reversible_add_column_with_default'
-
-describe RuboCop::Cop::Migration::ReversibleAddColumnWithDefault do
- include CopHelper
-
- subject(:cop) { described_class.new }
-
- context 'in migration' do
- before do
- allow(cop).to receive(:in_migration?).and_return(true)
- end
-
- it 'registers an offense when add_column_with_default is used inside a change method' do
- inspect_source('def change; add_column_with_default :table, :column, default: false; end')
-
- aggregate_failures do
- expect(cop.offenses.size).to eq(1)
- expect(cop.offenses.map(&:line)).to eq([1])
- end
- end
-
- it 'registers no offense when add_column_with_default is used inside an up method' do
- inspect_source('def up; add_column_with_default :table, :column, default: false; end')
-
- expect(cop.offenses.size).to eq(0)
- end
- end
-
- context 'outside of migration' do
- it 'registers no offense' do
- inspect_source('def change; add_column_with_default :table, :column, default: false; end')
-
- expect(cop.offenses.size).to eq(0)
- end
- end
-end
diff --git a/spec/rubocop/cop/migration/with_lock_retries_disallowed_method_spec.rb b/spec/rubocop/cop/migration/with_lock_retries_disallowed_method_spec.rb
new file mode 100644
index 00000000000..48570c1c8d8
--- /dev/null
+++ b/spec/rubocop/cop/migration/with_lock_retries_disallowed_method_spec.rb
@@ -0,0 +1,68 @@
+# frozen_string_literal: true
+
+require 'spec_helper'
+
+require 'rubocop'
+require 'rubocop/rspec/support'
+
+require_relative '../../../../rubocop/cop/migration/with_lock_retries_disallowed_method'
+
+describe RuboCop::Cop::Migration::WithLockRetriesDisallowedMethod do
+ include CopHelper
+
+ subject(:cop) { described_class.new }
+
+ context 'in migration' do
+ before do
+ allow(cop).to receive(:in_migration?).and_return(true)
+ end
+
+ it 'registers an offense when `with_lock_retries` block has disallowed method' do
+ inspect_source('def change; with_lock_retries { disallowed_method }; end')
+
+ aggregate_failures do
+ expect(cop.offenses.size).to eq(1)
+ expect(cop.offenses.map(&:line)).to eq([1])
+ end
+ end
+
+ it 'registers an offense when `with_lock_retries` block has disallowed methods' do
+ source = <<~HEREDOC
+ def change
+ with_lock_retries do
+ disallowed_method
+
+ create_table do |t|
+ t.text :text
+ end
+
+ other_disallowed_method
+
+ add_column :users, :name
+ end
+ end
+ HEREDOC
+
+ inspect_source(source)
+
+ aggregate_failures do
+ expect(cop.offenses.size).to eq(2)
+ expect(cop.offenses.map(&:line)).to eq([3, 9])
+ end
+ end
+
+ it 'registers no offense when `with_lock_retries` has only allowed method' do
+ inspect_source('def up; with_lock_retries { add_foreign_key :foo, :bar }; end')
+
+ expect(cop.offenses.size).to eq(0)
+ end
+ end
+
+ context 'outside of migration' do
+ it 'registers no offense' do
+ inspect_source('def change; with_lock_retries { disallowed_method }; end')
+
+ expect(cop.offenses.size).to eq(0)
+ end
+ end
+end
diff --git a/spec/rubocop/cop/migration/with_lock_retries_without_ddl_transaction_spec.rb b/spec/rubocop/cop/migration/with_lock_retries_without_ddl_transaction_spec.rb
deleted file mode 100644
index b42a4a14c67..00000000000
--- a/spec/rubocop/cop/migration/with_lock_retries_without_ddl_transaction_spec.rb
+++ /dev/null
@@ -1,46 +0,0 @@
-# frozen_string_literal: true
-
-require 'spec_helper'
-
-require 'rubocop'
-require 'rubocop/rspec/support'
-
-require_relative '../../../../rubocop/cop/migration/with_lock_retries_without_ddl_transaction'
-
-describe RuboCop::Cop::Migration::WithLockRetriesWithoutDdlTransaction do
- include CopHelper
-
- let(:valid_source) { 'class MigrationClass < ActiveRecord::Migration[6.0]; def up; with_lock_retries {}; end; end' }
- let(:invalid_source) { 'class MigrationClass < ActiveRecord::Migration[6.0]; disable_ddl_transaction!; def up; with_lock_retries {}; end; end' }
-
- subject(:cop) { described_class.new }
-
- context 'in migration' do
- before do
- allow(cop).to receive(:in_migration?).and_return(true)
- end
-
- it 'registers an offense when `with_lock_retries` is used with `disable_ddl_transaction!` method' do
- inspect_source(invalid_source)
-
- aggregate_failures do
- expect(cop.offenses.size).to eq(1)
- expect(cop.offenses.map(&:line)).to eq([1])
- end
- end
-
- it 'registers no offense when `with_lock_retries` is used inside an `up` method' do
- inspect_source(valid_source)
-
- expect(cop.offenses.size).to eq(0)
- end
- end
-
- context 'outside of migration' do
- it 'registers no offense' do
- inspect_source(invalid_source)
-
- expect(cop.offenses.size).to eq(0)
- end
- end
-end
diff --git a/spec/rubocop/cop/performance/ar_exists_and_present_blank_spec.rb b/spec/rubocop/cop/performance/ar_exists_and_present_blank_spec.rb
new file mode 100644
index 00000000000..ce4fdac56b0
--- /dev/null
+++ b/spec/rubocop/cop/performance/ar_exists_and_present_blank_spec.rb
@@ -0,0 +1,111 @@
+# frozen_string_literal: true
+
+require 'fast_spec_helper'
+require_relative '../../../support/helpers/expect_offense'
+require_relative '../../../../rubocop/cop/performance/ar_exists_and_present_blank.rb'
+
+describe RuboCop::Cop::Performance::ARExistsAndPresentBlank do
+ include CopHelper
+ include ExpectOffense
+
+ subject(:cop) { described_class.new }
+
+ context 'when it is not haml file' do
+ it 'does not flag it as an offense' do
+ expect(subject).to receive(:in_haml_file?).with(anything).at_least(:once).and_return(false)
+
+ expect_no_offenses <<~SOURCE
+ return unless @users.exists?
+ show @users if @users.present?
+ SOURCE
+ end
+ end
+
+ context 'when it is haml file' do
+ before do
+ expect(subject).to receive(:in_haml_file?).with(anything).at_least(:once).and_return(true)
+ end
+
+ context 'the same object uses exists? and present?' do
+ it 'flags it as an offense' do
+ expect_offense <<~SOURCE
+ return unless @users.exists?
+ show @users if @users.present?
+ ^^^^^^^^^^^^^^^ Avoid `@users.present?`, because it will generate database query 'Select TABLE.*' which is expensive. Suggest to use `@users.any?` to replace `@users.present?`
+ SOURCE
+
+ expect(cop.offenses.map(&:cop_name)).to contain_exactly('Performance/ARExistsAndPresentBlank')
+ end
+ end
+
+ context 'the same object uses exists? and blank?' do
+ it 'flags it as an offense' do
+ expect_offense <<~SOURCE
+ return unless @users.exists?
+ show @users if @users.blank?
+ ^^^^^^^^^^^^^ Avoid `@users.blank?`, because it will generate database query 'Select TABLE.*' which is expensive. Suggest to use `@users.empty?` to replace `@users.blank?`
+ SOURCE
+
+ expect(cop.offenses.map(&:cop_name)).to contain_exactly('Performance/ARExistsAndPresentBlank')
+ end
+ end
+
+ context 'the same object uses exists?, blank? and present?' do
+ it 'flags it as an offense' do
+ expect_offense <<~SOURCE
+ return unless @users.exists?
+ show @users if @users.blank?
+ ^^^^^^^^^^^^^ Avoid `@users.blank?`, because it will generate database query 'Select TABLE.*' which is expensive. Suggest to use `@users.empty?` to replace `@users.blank?`
+ show @users if @users.present?
+ ^^^^^^^^^^^^^^^ Avoid `@users.present?`, because it will generate database query 'Select TABLE.*' which is expensive. Suggest to use `@users.any?` to replace `@users.present?`
+ SOURCE
+
+ expect(cop.offenses.map(&:cop_name)).to contain_exactly('Performance/ARExistsAndPresentBlank', 'Performance/ARExistsAndPresentBlank')
+ end
+ end
+
+ RSpec.shared_examples 'different object uses exists? and present?/blank?' do |another_method|
+ it 'does not flag it as an offense' do
+ expect_no_offenses <<~SOURCE
+ return unless @users.exists?
+ present @emails if @emails.#{another_method}
+ SOURCE
+ end
+ end
+
+ it_behaves_like 'different object uses exists? and present?/blank?', 'present?'
+ it_behaves_like 'different object uses exists? and present?/blank?', 'blank?'
+
+ RSpec.shared_examples 'Only using one present?/blank? without exists?' do |non_exists_method|
+ it 'does not flag it as an offense' do
+ expect_no_offenses "@users.#{non_exists_method}"
+ end
+ end
+
+ it_behaves_like 'Only using one present?/blank? without exists?', 'present?'
+ it_behaves_like 'Only using one present?/blank? without exists?', 'blank?'
+
+ context 'when using many present?/empty? without exists?' do
+ it 'does not flag it as an offense' do
+ expect_no_offenses <<~SOURCE
+ @user.present?
+ @user.blank?
+ @user.present?
+ @user.blank?
+ SOURCE
+ end
+ end
+
+ context 'when just using exists? without present?/blank?' do
+ it 'does not flag it as an offense' do
+ expect_no_offenses '@users.exists?'
+
+ expect_no_offenses <<~SOURCE
+ @users.exists?
+ @users.some_other_method?
+ @users.exists?
+ SOURCE
+ end
+ end
+ end
+end
diff --git a/spec/rubocop/cop/rspec/empty_line_after_shared_example_spec.rb b/spec/rubocop/cop/rspec/empty_line_after_shared_example_spec.rb
new file mode 100644
index 00000000000..cee593fe535
--- /dev/null
+++ b/spec/rubocop/cop/rspec/empty_line_after_shared_example_spec.rb
@@ -0,0 +1,86 @@
+# frozen_string_literal: true
+
+require 'spec_helper'
+
+require_relative '../../../../rubocop/cop/rspec/empty_line_after_shared_example'
+
+describe RuboCop::Cop::RSpec::EmptyLineAfterSharedExample do
+ subject(:cop) { described_class.new }
+
+ it 'flags a missing empty line after `it_behaves_like` block' do
+ expect_offense(<<-RUBY)
+ RSpec.describe Foo do
+ it_behaves_like 'does this' do
+ end
+ ^^^ Add an empty line after `it_behaves_like` block.
+ it_behaves_like 'does that' do
+ end
+ end
+ RUBY
+
+ expect_correction(<<-RUBY)
+ RSpec.describe Foo do
+ it_behaves_like 'does this' do
+ end
+
+ it_behaves_like 'does that' do
+ end
+ end
+ RUBY
+ end
+
+ it 'ignores one-line shared examples before shared example blocks' do
+ expect_no_offenses(<<-RUBY)
+ RSpec.describe Foo do
+ it_behaves_like 'does this'
+ it_behaves_like 'does that' do
+ end
+ end
+ RUBY
+ end
+
+ it 'flags a missing empty line after `shared_examples`' do
+ expect_offense(<<-RUBY)
+ RSpec.context 'foo' do
+ shared_examples do
+ end
+ ^^^ Add an empty line after `shared_examples` block.
+ shared_examples 'something gets done' do
+ end
+ end
+ RUBY
+
+ expect_correction(<<-RUBY)
+ RSpec.context 'foo' do
+ shared_examples do
+ end
+
+ shared_examples 'something gets done' do
+ end
+ end
+ RUBY
+ end
+
+ it 'ignores consecutive one-liners' do
+ expect_no_offenses(<<-RUBY)
+ RSpec.describe Foo do
+ it_behaves_like 'do this'
+ it_behaves_like 'do that'
+ end
+ RUBY
+ end
+
+ it 'flags mixed one-line and multi-line shared examples' do
+ expect_offense(<<-RUBY)
+ RSpec.context 'foo' do
+ it_behaves_like 'do this'
+ it_behaves_like 'do that'
+ it_behaves_like 'does this' do
+ end
+ ^^^ Add an empty line after `it_behaves_like` block.
+ it_behaves_like 'do this'
+ it_behaves_like 'do that'
+ end
+ RUBY
+ end
+end
diff --git a/spec/rubocop/cop/rspec/env_assignment_spec.rb b/spec/rubocop/cop/rspec/env_assignment_spec.rb
index 2a2bd1434d6..1c7cfb5c827 100644
--- a/spec/rubocop/cop/rspec/env_assignment_spec.rb
+++ b/spec/rubocop/cop/rspec/env_assignment_spec.rb
@@ -10,8 +10,8 @@ require_relative '../../../../rubocop/cop/rspec/env_assignment'
describe RuboCop::Cop::RSpec::EnvAssignment do
include CopHelper
- OFFENSE_CALL_SINGLE_QUOTES_KEY = %(ENV['FOO'] = 'bar').freeze
- OFFENSE_CALL_DOUBLE_QUOTES_KEY = %(ENV["FOO"] = 'bar').freeze
+ offense_call_single_quotes_key = %(ENV['FOO'] = 'bar').freeze
+ offense_call_double_quotes_key = %(ENV["FOO"] = 'bar').freeze
let(:source_file) { 'spec/foo_spec.rb' }
@@ -36,12 +36,12 @@ describe RuboCop::Cop::RSpec::EnvAssignment do
end
context 'with a key using single quotes' do
- it_behaves_like 'an offensive ENV#[]= call', OFFENSE_CALL_SINGLE_QUOTES_KEY
- it_behaves_like 'an autocorrected ENV#[]= call', OFFENSE_CALL_SINGLE_QUOTES_KEY, %(stub_env('FOO', 'bar'))
+ it_behaves_like 'an offensive ENV#[]= call', offense_call_single_quotes_key
+ it_behaves_like 'an autocorrected ENV#[]= call', offense_call_single_quotes_key, %(stub_env('FOO', 'bar'))
end
context 'with a key using double quotes' do
- it_behaves_like 'an offensive ENV#[]= call', OFFENSE_CALL_DOUBLE_QUOTES_KEY
- it_behaves_like 'an autocorrected ENV#[]= call', OFFENSE_CALL_DOUBLE_QUOTES_KEY, %(stub_env("FOO", 'bar'))
+ it_behaves_like 'an offensive ENV#[]= call', offense_call_double_quotes_key
+ it_behaves_like 'an autocorrected ENV#[]= call', offense_call_double_quotes_key, %(stub_env("FOO", 'bar'))
end
end
diff --git a/spec/serializers/accessibility_error_entity_spec.rb b/spec/serializers/accessibility_error_entity_spec.rb
new file mode 100644
index 00000000000..e9bfabb7aa8
--- /dev/null
+++ b/spec/serializers/accessibility_error_entity_spec.rb
@@ -0,0 +1,37 @@
+# frozen_string_literal: true
+
+require 'spec_helper'
+
+describe AccessibilityErrorEntity do
+ let(:entity) { described_class.new(accessibility_error) }
+
+ describe '#as_json' do
+ subject { entity.as_json }
+
+ context 'when accessibility contains an error' do
+ let(:accessibility_error) do
+ {
+ code: "WCAG2AA.Principle4.Guideline4_1.4_1_2.H91.A.NoContent",
+ type: "error",
+ typeCode: 1,
+ message: "Anchor element found with a valid href attribute, but no link content has been supplied.",
+ context: "<a href=\"/\" class=\"navbar-brand animated\"><svg height=\"36\" viewBox=\"0 0 1...</a>",
+ selector: "#main-nav > div:nth-child(1) > a",
+ runner: "htmlcs",
+ runnerExtras: {}
+ }
+ end
+
+ it 'contains correct accessibility error details', :aggregate_failures do
+ expect(subject[:code]).to eq("WCAG2AA.Principle4.Guideline4_1.4_1_2.H91.A.NoContent")
+ expect(subject[:type]).to eq("error")
+ expect(subject[:type_code]).to eq(1)
+ expect(subject[:message]).to eq("Anchor element found with a valid href attribute, but no link content has been supplied.")
+ expect(subject[:context]).to eq("<a href=\"/\" class=\"navbar-brand animated\"><svg height=\"36\" viewBox=\"0 0 1...</a>")
+ expect(subject[:selector]).to eq("#main-nav > div:nth-child(1) > a")
+ expect(subject[:runner]).to eq("htmlcs")
+ expect(subject[:runner_extras]).to be_empty
+ end
+ end
+ end
+end
diff --git a/spec/serializers/accessibility_reports_comparer_entity_spec.rb b/spec/serializers/accessibility_reports_comparer_entity_spec.rb
new file mode 100644
index 00000000000..ed2c17de640
--- /dev/null
+++ b/spec/serializers/accessibility_reports_comparer_entity_spec.rb
@@ -0,0 +1,87 @@
+# frozen_string_literal: true
+
+require 'spec_helper'
+
+describe AccessibilityReportsComparerEntity do
+ let(:entity) { described_class.new(comparer) }
+ let(:comparer) { Gitlab::Ci::Reports::AccessibilityReportsComparer.new(base_report, head_report) }
+ let(:base_report) { Gitlab::Ci::Reports::AccessibilityReports.new }
+ let(:head_report) { Gitlab::Ci::Reports::AccessibilityReports.new }
+ let(:url) { "https://gitlab.com" }
+ let(:single_error) do
+ [
+ {
+ code: "WCAG2AA.Principle4.Guideline4_1.4_1_2.H91.A.NoContent",
+ type: "error",
+ typeCode: 1,
+ message: "Anchor element found with a valid href attribute, but no link content has been supplied.",
+ context: "<a href=\"/\" class=\"navbar-brand animated\"><svg height=\"36\" viewBox=\"0 0 1...</a>",
+ selector: "#main-nav > div:nth-child(1) > a",
+ runner: "htmlcs",
+ runnerExtras: {}
+ }
+ ]
+ end
+ let(:different_error) do
+ [
+ {
+ code: "WCAG2AA.Principle1.Guideline1_4.1_4_3.G18.Fail",
+ type: "error",
+ typeCode: 1,
+ message: "This element has insufficient contrast at this conformance level.",
+ context: "<a href=\"/stages-devops-lifecycle/\" class=\"main-nav-link\">Product</a>",
+ selector: "#main-nav > div:nth-child(2) > ul > li:nth-child(1) > a",
+ runner: "htmlcs",
+ runnerExtras: {}
+ }
+ ]
+ end
+
+ describe '#as_json' do
+ subject { entity.as_json }
+
+ context 'when base report has error and head has a different error' do
+ before do
+ base_report.add_url(url, single_error)
+ head_report.add_url(url, different_error)
+ end
+
+ it 'contains correct compared accessibility report details', :aggregate_failures do
+ expect(subject[:status]).to eq(Gitlab::Ci::Reports::AccessibilityReportsComparer::STATUS_FAILED)
+ expect(subject[:resolved_errors].first).to include(:code, :type, :type_code, :message, :context, :selector, :runner, :runner_extras)
+ expect(subject[:new_errors].first).to include(:code, :type, :type_code, :message, :context, :selector, :runner, :runner_extras)
+ expect(subject[:existing_errors].first).to include(:code, :type, :type_code, :message, :context, :selector, :runner, :runner_extras)
+ expect(subject[:summary]).to include(total: 2, resolved: 1, errored: 1)
+ end
+ end
+
+ context 'when base report has error and head has the same error' do
+ before do
+ base_report.add_url(url, single_error)
+ head_report.add_url(url, single_error)
+ end
+
+ it 'contains correct compared accessibility report details', :aggregate_failures do
+ expect(subject[:status]).to eq(Gitlab::Ci::Reports::AccessibilityReportsComparer::STATUS_FAILED)
+ expect(subject[:new_errors]).to be_empty
+ expect(subject[:resolved_errors]).to be_empty
+ expect(subject[:existing_errors].first).to include(:code, :type, :type_code, :message, :context, :selector, :runner, :runner_extras)
+ expect(subject[:summary]).to include(total: 1, resolved: 0, errored: 1)
+ end
+ end
+
+ context 'when base report has no error and head has errors' do
+ before do
+ head_report.add_url(url, single_error)
+ end
+
+ it 'contains correct compared accessibility report details', :aggregate_failures do
+ expect(subject[:status]).to eq(Gitlab::Ci::Reports::AccessibilityReportsComparer::STATUS_FAILED)
+ expect(subject[:resolved_errors]).to be_empty
+ expect(subject[:existing_errors]).to be_empty
+ expect(subject[:new_errors].first).to include(:code, :type, :type_code, :message, :context, :selector, :runner, :runner_extras)
+ expect(subject[:summary]).to include(total: 1, resolved: 0, errored: 1)
+ end
+ end
+ end
+end
diff --git a/spec/serializers/accessibility_reports_comparer_serializer_spec.rb b/spec/serializers/accessibility_reports_comparer_serializer_spec.rb
new file mode 100644
index 00000000000..37dc760fdec
--- /dev/null
+++ b/spec/serializers/accessibility_reports_comparer_serializer_spec.rb
@@ -0,0 +1,65 @@
+# frozen_string_literal: true
+
+require 'spec_helper'
+
+describe AccessibilityReportsComparerSerializer do
+ let(:project) { double(:project) }
+ let(:serializer) { described_class.new(project: project).represent(comparer) }
+ let(:comparer) { Gitlab::Ci::Reports::AccessibilityReportsComparer.new(base_report, head_report) }
+ let(:base_report) { Gitlab::Ci::Reports::AccessibilityReports.new }
+ let(:head_report) { Gitlab::Ci::Reports::AccessibilityReports.new }
+ let(:url) { "https://gitlab.com" }
+ let(:single_error) do
+ [
+ {
+ code: "WCAG2AA.Principle4.Guideline4_1.4_1_2.H91.A.NoContent",
+ type: "error",
+ typeCode: 1,
+ message: "Anchor element found with a valid href attribute, but no link content has been supplied.",
+ context: "<a href=\"/\" class=\"navbar-brand animated\"><svg height=\"36\" viewBox=\"0 0 1...</a>",
+ selector: "#main-nav > divnth-child(1) > a",
+ runner: "htmlcs",
+ runnerExtras: {}
+ }
+ ]
+ end
+ let(:different_error) do
+ [
+ {
+ code: "WCAG2AA.Principle1.Guideline1_4.1_4_3.G18.Fail",
+ type: "error",
+ typeCode: 1,
+ message: "This element has insufficient contrast at this conformance level.",
+ context: "<a href=\"/stages-devops-lifecycle/\" class=\"main-nav-link\">Product</a>",
+ selector: "#main-nav > divnth-child(2) > ul > linth-child(1) > a",
+ runner: "htmlcs",
+ runnerExtras: {}
+ }
+ ]
+ end
+
+ describe '#to_json' do
+ subject { serializer.as_json }
+
+ context 'when base report has error and head has a different error' do
+ before do
+ base_report.add_url(url, single_error)
+ head_report.add_url(url, different_error)
+ end
+
+ it 'matches the schema' do
+ expect(subject).to match_schema('entities/accessibility_reports_comparer')
+ end
+ end
+
+ context 'when base report has no error and head has errors' do
+ before do
+ head_report.add_url(url, single_error)
+ end
+
+ it 'matches the schema' do
+ expect(subject).to match_schema('entities/accessibility_reports_comparer')
+ end
+ end
+ end
+end
diff --git a/spec/serializers/ci/dag_job_entity_spec.rb b/spec/serializers/ci/dag_job_entity_spec.rb
new file mode 100644
index 00000000000..19b849c3879
--- /dev/null
+++ b/spec/serializers/ci/dag_job_entity_spec.rb
@@ -0,0 +1,43 @@
+# frozen_string_literal: true
+
+require 'spec_helper'
+
+describe Ci::DagJobEntity do
+ let_it_be(:request) { double(:request) }
+
+ let(:job) { create(:ci_build, name: 'dag_job') }
+ let(:entity) { described_class.new(job, request: request) }
+
+ describe '#as_json' do
+ subject { entity.as_json }
+
+ it 'contains the name' do
+ expect(subject[:name]).to eq 'dag_job'
+ end
+
+ context 'when job is stage scheduled' do
+ it 'does not expose needs' do
+ expect(subject).not_to include(:needs)
+ end
+ end
+
+ context 'when job is dag scheduled' do
+ context 'when job has needs' do
+ let(:job) { create(:ci_build, scheduling_type: 'dag') }
+ let!(:need) { create(:ci_build_need, build: job, name: 'compile') }
+
+ it 'exposes the array of needs' do
+ expect(subject[:needs]).to eq ['compile']
+ end
+ end
+
+ context 'when job has empty needs' do
+ let(:job) { create(:ci_build, scheduling_type: 'dag') }
+
+ it 'exposes an empty array of needs' do
+ expect(subject[:needs]).to eq []
+ end
+ end
+ end
+ end
+end
diff --git a/spec/serializers/ci/dag_job_group_entity_spec.rb b/spec/serializers/ci/dag_job_group_entity_spec.rb
new file mode 100644
index 00000000000..a25723894fd
--- /dev/null
+++ b/spec/serializers/ci/dag_job_group_entity_spec.rb
@@ -0,0 +1,58 @@
+# frozen_string_literal: true
+
+require 'spec_helper'
+
+describe Ci::DagJobGroupEntity do
+ let_it_be(:request) { double(:request) }
+ let_it_be(:pipeline) { create(:ci_pipeline) }
+ let_it_be(:stage) { create(:ci_stage, pipeline: pipeline) }
+
+ let(:group) { Ci::Group.new(pipeline.project, stage, name: 'test', jobs: jobs) }
+ let(:entity) { described_class.new(group, request: request) }
+
+ describe '#as_json' do
+ subject { entity.as_json }
+
+ context 'when group contains 1 job' do
+ let(:job) { create(:ci_build, stage: stage, pipeline: pipeline, name: 'test') }
+ let(:jobs) { [job] }
+
+ it 'exposes a name' do
+ expect(subject.fetch(:name)).to eq 'test'
+ end
+
+ it 'exposes the size' do
+ expect(subject.fetch(:size)).to eq 1
+ end
+
+ it 'exposes the jobs' do
+ exposed_jobs = subject.fetch(:jobs)
+
+ expect(exposed_jobs.size).to eq 1
+ expect(exposed_jobs.first.fetch(:name)).to eq 'test'
+ end
+ end
+
+ context 'when group contains multiple parallel jobs' do
+ let(:job_1) { create(:ci_build, stage: stage, pipeline: pipeline, name: 'test 1/2') }
+ let(:job_2) { create(:ci_build, stage: stage, pipeline: pipeline, name: 'test 2/2') }
+ let(:jobs) { [job_1, job_2] }
+
+ it 'exposes a name' do
+ expect(subject.fetch(:name)).to eq 'test'
+ end
+
+ it 'exposes the size' do
+ expect(subject.fetch(:size)).to eq 2
+ end
+
+ it 'exposes the jobs' do
+ exposed_jobs = subject.fetch(:jobs)
+
+ expect(exposed_jobs.size).to eq 2
+ expect(exposed_jobs.first.fetch(:name)).to eq 'test 1/2'
+ expect(exposed_jobs.last.fetch(:name)).to eq 'test 2/2'
+ end
+ end
+ end
+end
diff --git a/spec/serializers/ci/dag_pipeline_entity_spec.rb b/spec/serializers/ci/dag_pipeline_entity_spec.rb
new file mode 100644
index 00000000000..4645451e146
--- /dev/null
+++ b/spec/serializers/ci/dag_pipeline_entity_spec.rb
@@ -0,0 +1,112 @@
+# frozen_string_literal: true
+
+require 'spec_helper'
+
+describe Ci::DagPipelineEntity do
+ let_it_be(:request) { double(:request) }
+
+ let(:pipeline) { create(:ci_pipeline) }
+ let(:entity) { described_class.new(pipeline, request: request) }
+
+ describe '#as_json' do
+ subject { entity.as_json }
+
+ context 'when pipeline is empty' do
+ it 'contains stages' do
+ expect(subject).to include(:stages)
+
+ expect(subject[:stages]).to be_empty
+ end
+ end
+
+ context 'when pipeline has jobs' do
+ let!(:build_job) { create(:ci_build, stage: 'build', pipeline: pipeline) }
+ let!(:test_job) { create(:ci_build, stage: 'test', pipeline: pipeline) }
+ let!(:deploy_job) { create(:ci_build, stage: 'deploy', pipeline: pipeline) }
+
+ it 'contains 3 stages' do
+ stages = subject[:stages]
+
+ expect(stages.size).to eq 3
+ expect(stages.map { |s| s[:name] }).to contain_exactly('build', 'test', 'deploy')
+ end
+ end
+
+ context 'when pipeline has parallel jobs and DAG needs' do
+ let!(:stage_build) { create(:ci_stage_entity, name: 'build', position: 1, pipeline: pipeline) }
+ let!(:stage_test) { create(:ci_stage_entity, name: 'test', position: 2, pipeline: pipeline) }
+ let!(:stage_deploy) { create(:ci_stage_entity, name: 'deploy', position: 3, pipeline: pipeline) }
+
+ let!(:job_build_1) { create(:ci_build, name: 'build 1', stage: 'build', pipeline: pipeline) }
+ let!(:job_build_2) { create(:ci_build, name: 'build 2', stage: 'build', pipeline: pipeline) }
+
+ let!(:job_rspec_1) { create(:ci_build, name: 'rspec 1/2', stage: 'test', pipeline: pipeline) }
+ let!(:job_rspec_2) { create(:ci_build, name: 'rspec 2/2', stage: 'test', pipeline: pipeline) }
+
+ let!(:job_jest) do
+ create(:ci_build, name: 'jest', stage: 'test', scheduling_type: 'dag', pipeline: pipeline).tap do |job|
+ create(:ci_build_need, name: 'build 1', build: job)
+ end
+ end
+
+ let!(:job_deploy_ruby) do
+ create(:ci_build, name: 'deploy_ruby', stage: 'deploy', scheduling_type: 'dag', pipeline: pipeline).tap do |job|
+ create(:ci_build_need, name: 'rspec 1/2', build: job)
+ create(:ci_build_need, name: 'rspec 2/2', build: job)
+ end
+ end
+
+ let!(:job_deploy_js) do
+ create(:ci_build, name: 'deploy_js', stage: 'deploy', scheduling_type: 'dag', pipeline: pipeline).tap do |job|
+ create(:ci_build_need, name: 'jest', build: job)
+ end
+ end
+
+ it 'performs the smallest number of queries' do
+ log = ActiveRecord::QueryRecorder.new { subject }
+
+ # stages, project, builds, build_needs
+ expect(log.count).to eq 4
+ end
+
+ it 'contains all the data' do
+ expected_result = {
+ stages: [
+ {
+ name: 'build',
+ groups: [
+ { name: 'build 1', size: 1, jobs: [{ name: 'build 1' }] },
+ { name: 'build 2', size: 1, jobs: [{ name: 'build 2' }] }
+ ]
+ },
+ {
+ name: 'test',
+ groups: [
+ { name: 'jest', size: 1, jobs: [{ name: 'jest', needs: ['build 1'] }] },
+ { name: 'rspec', size: 2, jobs: [{ name: 'rspec 1/2' }, { name: 'rspec 2/2' }] }
+ ]
+ },
+ {
+ name: 'deploy',
+ groups: [
+ { name: 'deploy_js', size: 1, jobs: [{ name: 'deploy_js', needs: ['jest'] }] },
+ { name: 'deploy_ruby', size: 1, jobs: [{ name: 'deploy_ruby', needs: ['rspec 1/2', 'rspec 2/2'] }] }
+ ]
+ }
+ ]
+ }
+
+ expect(subject.fetch(:stages)).not_to be_empty
+
+ expect(subject.fetch(:stages)[0].fetch(:name)).to eq 'build'
+ expect(subject.fetch(:stages)[0]).to eq expected_result.fetch(:stages)[0]
+
+ expect(subject.fetch(:stages)[1].fetch(:name)).to eq 'test'
+ expect(subject.fetch(:stages)[1]).to eq expected_result.fetch(:stages)[1]
+
+ expect(subject.fetch(:stages)[2].fetch(:name)).to eq 'deploy'
+ expect(subject.fetch(:stages)[2]).to eq expected_result.fetch(:stages)[2]
+ end
+ end
+ end
+end
diff --git a/spec/serializers/ci/dag_pipeline_serializer_spec.rb b/spec/serializers/ci/dag_pipeline_serializer_spec.rb
new file mode 100644
index 00000000000..abf895c3e77
--- /dev/null
+++ b/spec/serializers/ci/dag_pipeline_serializer_spec.rb
@@ -0,0 +1,17 @@
+# frozen_string_literal: true
+
+require 'spec_helper'
+
+describe Ci::DagPipelineSerializer do
+ describe '#represent' do
+ subject { described_class.new.represent(pipeline) }
+
+ let(:pipeline) { create(:ci_pipeline) }
+ let!(:job) { create(:ci_build, pipeline: pipeline) }
+
+ it 'includes stages' do
+ expect(subject[:stages]).to be_present
+ expect(subject[:stages].size).to eq 1
+ end
+ end
+end
diff --git a/spec/serializers/ci/dag_stage_entity_spec.rb b/spec/serializers/ci/dag_stage_entity_spec.rb
new file mode 100644
index 00000000000..5c6aa7faee4
--- /dev/null
+++ b/spec/serializers/ci/dag_stage_entity_spec.rb
@@ -0,0 +1,31 @@
+# frozen_string_literal: true
+
+require 'spec_helper'
+
+describe Ci::DagStageEntity do
+ let_it_be(:pipeline) { create(:ci_pipeline) }
+ let_it_be(:request) { double(:request) }
+
+ let(:stage) { build(:ci_stage, pipeline: pipeline, name: 'test') }
+ let(:entity) { described_class.new(stage, request: request) }
+
+ let!(:job) { create(:ci_build, :success, pipeline: pipeline) }
+
+ describe '#as_json' do
+ subject { entity.as_json }
+
+ it 'contains valid name' do
+ expect(subject[:name]).to eq 'test'
+ end
+
+ it 'contains the job groups' do
+ expect(subject).to include :groups
+ expect(subject[:groups]).not_to be_empty
+
+ job_group = subject[:groups].first
+ expect(job_group[:name]).to eq 'test'
+ expect(job_group[:size]).to eq 1
+ expect(job_group[:jobs]).not_to be_empty
+ end
+ end
+end
diff --git a/spec/serializers/cluster_application_entity_spec.rb b/spec/serializers/cluster_application_entity_spec.rb
index 873fbf812cc..b81bdaa0d72 100644
--- a/spec/serializers/cluster_application_entity_spec.rb
+++ b/spec/serializers/cluster_application_entity_spec.rb
@@ -77,5 +77,17 @@ describe ClusterApplicationEntity do
expect(subject[:pages_domain]).to eq(id: pages_domain.id, domain: pages_domain.domain)
end
end
+
+ context 'for fluentd application' do
+ let(:application) { build(:clusters_applications_fluentd, :installed) }
+
+ it 'includes host, port, protocol and log fields' do
+ expect(subject[:port]).to eq(514)
+ expect(subject[:host]).to eq("example.com")
+ expect(subject[:protocol]).to eq("tcp")
+ expect(subject[:waf_log_enabled]).to be true
+ expect(subject[:cilium_log_enabled]).to be true
+ end
+ end
end
end
diff --git a/spec/serializers/cluster_entity_spec.rb b/spec/serializers/cluster_entity_spec.rb
index e3826a7221d..16247eef655 100644
--- a/spec/serializers/cluster_entity_spec.rb
+++ b/spec/serializers/cluster_entity_spec.rb
@@ -7,7 +7,7 @@ describe ClusterEntity do
subject { described_class.new(cluster).as_json }
context 'when provider type is gcp' do
- let(:cluster) { create(:cluster, provider_type: :gcp, provider_gcp: provider) }
+ let(:cluster) { create(:cluster, :instance, provider_type: :gcp, provider_gcp: provider) }
context 'when status is creating' do
let(:provider) { create(:cluster_provider_gcp, :creating) }
@@ -29,7 +29,7 @@ describe ClusterEntity do
end
context 'when provider type is user' do
- let(:cluster) { create(:cluster, provider_type: :user) }
+ let(:cluster) { create(:cluster, :instance, provider_type: :user) }
it 'has corresponded data' do
expect(subject[:status]).to eq(:created)
@@ -38,7 +38,7 @@ describe ClusterEntity do
end
context 'when no application has been installed' do
- let(:cluster) { create(:cluster) }
+ let(:cluster) { create(:cluster, :instance) }
subject { described_class.new(cluster).as_json[:applications]}
diff --git a/spec/serializers/cluster_serializer_spec.rb b/spec/serializers/cluster_serializer_spec.rb
index db0e65ca0fa..39551649ff0 100644
--- a/spec/serializers/cluster_serializer_spec.rb
+++ b/spec/serializers/cluster_serializer_spec.rb
@@ -3,23 +3,41 @@
require 'spec_helper'
describe ClusterSerializer do
+ let(:cluster) { create(:cluster, :project, provider_type: :user) }
+
+ describe '#represent_list' do
+ subject { described_class.new.represent_list(cluster).keys }
+
+ it 'serializes attrs correctly' do
+ is_expected.to contain_exactly(
+ :cluster_type,
+ :enabled,
+ :environment_scope,
+ :name,
+ :nodes,
+ :path,
+ :status)
+ end
+ end
+
describe '#represent_status' do
- subject { described_class.new.represent_status(cluster) }
+ subject { described_class.new.represent_status(cluster).keys }
+
+ context 'when provider type is gcp and cluster is errored' do
+ let(:cluster) do
+ errored_provider = create(:cluster_provider_gcp, :errored)
- context 'when provider type is gcp' do
- let(:cluster) { create(:cluster, provider_type: :gcp, provider_gcp: provider) }
- let(:provider) { create(:cluster_provider_gcp, :errored) }
+ create(:cluster, provider_type: :gcp, provider_gcp: errored_provider)
+ end
- it 'serializes only status' do
- expect(subject.keys).to contain_exactly(:status, :status_reason, :applications)
+ it 'serializes attrs correctly' do
+ is_expected.to contain_exactly(:status, :status_reason, :applications)
end
end
context 'when provider type is user' do
- let(:cluster) { create(:cluster, provider_type: :user) }
-
- it 'serializes only status' do
- expect(subject.keys).to contain_exactly(:status, :status_reason, :applications)
+ it 'serializes attrs correctly' do
+ is_expected.to contain_exactly(:status, :status_reason, :applications)
end
end
end
diff --git a/spec/serializers/diff_file_base_entity_spec.rb b/spec/serializers/diff_file_base_entity_spec.rb
index 80f5bc8f159..1fd697970de 100644
--- a/spec/serializers/diff_file_base_entity_spec.rb
+++ b/spec/serializers/diff_file_base_entity_spec.rb
@@ -34,4 +34,62 @@ describe DiffFileBaseEntity do
expect(entity[:new_size]).to eq(132)
end
end
+
+ context 'edit_path' do
+ let(:diff_file) { merge_request.diffs.diff_files.to_a.last }
+ let(:options) { { request: EntityRequest.new(current_user: create(:user)), merge_request: merge_request } }
+ let(:params) { {} }
+
+ before do
+ stub_feature_flags(web_ide_default: false)
+ end
+
+ shared_examples 'a diff file edit path to the source branch' do
+ it do
+ expect(entity[:edit_path]).to eq(Gitlab::Routing.url_helpers.project_edit_blob_path(project, File.join(merge_request.source_branch, diff_file.new_path), params))
+ end
+ end
+
+ context 'open' do
+ let(:merge_request) { create(:merge_request, source_project: project, target_branch: 'master', source_branch: 'feature') }
+ let(:params) { { from_merge_request_iid: merge_request.iid } }
+
+ it_behaves_like 'a diff file edit path to the source branch'
+
+ context 'removed source branch' do
+ before do
+ allow(merge_request).to receive(:source_branch_exists?).and_return(false)
+ end
+
+ it do
+ expect(entity[:edit_path]).to eq(nil)
+ end
+ end
+ end
+
+ context 'closed' do
+ let(:merge_request) { create(:merge_request, source_project: project, state: :closed, target_branch: 'master', source_branch: 'feature') }
+ let(:params) { { from_merge_request_iid: merge_request.iid } }
+
+ it_behaves_like 'a diff file edit path to the source branch'
+
+ context 'removed source branch' do
+ before do
+ allow(merge_request).to receive(:source_branch_exists?).and_return(false)
+ end
+
+ it do
+ expect(entity[:edit_path]).to eq(nil)
+ end
+ end
+ end
+
+ context 'merged' do
+ let(:merge_request) { create(:merge_request, source_project: project, state: :merged) }
+
+ it do
+ expect(entity[:edit_path]).to eq(Gitlab::Routing.url_helpers.project_edit_blob_path(project, File.join(merge_request.target_branch, diff_file.new_path), {}))
+ end
+ end
+ end
end
diff --git a/spec/serializers/diffs_metadata_entity_spec.rb b/spec/serializers/diffs_metadata_entity_spec.rb
index a6bf9a7700e..3ed2b7c9452 100644
--- a/spec/serializers/diffs_metadata_entity_spec.rb
+++ b/spec/serializers/diffs_metadata_entity_spec.rb
@@ -29,7 +29,7 @@ describe DiffsMetadataEntity do
:added_lines, :removed_lines, :render_overflow_warning,
:email_patch_path, :plain_diff_path,
:merge_request_diffs, :context_commits,
- :definition_path_prefix,
+ :definition_path_prefix, :source_branch_exists,
# Attributes
:diff_files
)
diff --git a/spec/serializers/environment_entity_spec.rb b/spec/serializers/environment_entity_spec.rb
index b4ea90d2141..36e971c467a 100644
--- a/spec/serializers/environment_entity_spec.rb
+++ b/spec/serializers/environment_entity_spec.rb
@@ -10,7 +10,13 @@ describe EnvironmentEntity do
described_class.new(environment, request: spy('request'))
end
- let(:environment) { create(:environment) }
+ let_it_be(:user) { create(:user) }
+ let_it_be(:project) { create(:project) }
+ let_it_be(:environment) { create(:environment, project: project) }
+
+ before do
+ allow(entity).to receive(:current_user).and_return(user)
+ end
subject { entity.as_json }
@@ -67,28 +73,48 @@ describe EnvironmentEntity do
end
context 'with auto_stop_in' do
- let(:environment) { create(:environment, :will_auto_stop) }
+ let(:environment) { create(:environment, :will_auto_stop, project: project) }
it 'exposes auto stop related information' do
+ project.add_maintainer(user)
+
expect(subject).to include(:cancel_auto_stop_path, :auto_stop_at)
end
end
context 'pod_logs' do
- it 'exposes logs keys' do
- expect(subject).to include(:logs_path)
- expect(subject).to include(:logs_api_path)
- expect(subject).to include(:enable_advanced_logs_querying)
- end
+ context 'with developer access' do
+ before do
+ project.add_developer(user)
+ end
- it 'uses k8s api when ES is not available' do
- expect(subject[:logs_api_path]).to eq(k8s_project_logs_path(environment.project, environment_name: environment.name, format: :json))
+ it 'does not expose logs keys' do
+ expect(subject).not_to include(:logs_path)
+ expect(subject).not_to include(:logs_api_path)
+ expect(subject).not_to include(:enable_advanced_logs_querying)
+ end
end
- it 'uses ES api when ES is available' do
- allow(environment).to receive(:elastic_stack_available?).and_return(true)
+ context 'with maintainer access' do
+ before do
+ project.add_maintainer(user)
+ end
+
+ it 'exposes logs keys' do
+ expect(subject).to include(:logs_path)
+ expect(subject).to include(:logs_api_path)
+ expect(subject).to include(:enable_advanced_logs_querying)
+ end
- expect(subject[:logs_api_path]).to eq(elasticsearch_project_logs_path(environment.project, environment_name: environment.name, format: :json))
+ it 'uses k8s api when ES is not available' do
+ expect(subject[:logs_api_path]).to eq(k8s_project_logs_path(project, environment_name: environment.name, format: :json))
+ end
+
+ it 'uses ES api when ES is available' do
+ allow(environment).to receive(:elastic_stack_available?).and_return(true)
+
+ expect(subject[:logs_api_path]).to eq(elasticsearch_project_logs_path(project, environment_name: environment.name, format: :json))
+ end
end
end
end
diff --git a/spec/serializers/merge_request_poll_widget_entity_spec.rb b/spec/serializers/merge_request_poll_widget_entity_spec.rb
index fe0b717ede0..4b3bfc99c88 100644
--- a/spec/serializers/merge_request_poll_widget_entity_spec.rb
+++ b/spec/serializers/merge_request_poll_widget_entity_spec.rb
@@ -71,6 +71,50 @@ describe MergeRequestPollWidgetEntity do
end
end
+ describe 'terraform_reports_path' do
+ context 'when merge request has terraform reports' do
+ before do
+ allow(resource).to receive(:has_terraform_reports?).and_return(true)
+ end
+
+ it 'set the path to poll data' do
+ expect(subject[:terraform_reports_path]).to be_present
+ end
+ end
+
+ context 'when merge request has no terraform reports' do
+ before do
+ allow(resource).to receive(:has_terraform_reports?).and_return(false)
+ end
+
+ it 'set the path to poll data' do
+ expect(subject[:terraform_reports_path]).to be_nil
+ end
+ end
+ end
+
+ describe 'accessibility_report_path' do
+ context 'when merge request has accessibility reports' do
+ before do
+ allow(resource).to receive(:has_accessibility_reports?).and_return(true)
+ end
+
+ it 'set the path to poll data' do
+ expect(subject[:accessibility_report_path]).to be_present
+ end
+ end
+
+ context 'when merge request has no accessibility reports' do
+ before do
+ allow(resource).to receive(:has_accessibility_reports?).and_return(false)
+ end
+
+ it 'set the path to poll data' do
+ expect(subject[:accessibility_report_path]).to be_nil
+ end
+ end
+ end
+
describe 'exposed_artifacts_path' do
context 'when merge request has exposed artifacts' do
before do
diff --git a/spec/serializers/merge_request_sidebar_basic_entity_spec.rb b/spec/serializers/merge_request_sidebar_basic_entity_spec.rb
index b364b1a3306..b2db57801ea 100644
--- a/spec/serializers/merge_request_sidebar_basic_entity_spec.rb
+++ b/spec/serializers/merge_request_sidebar_basic_entity_spec.rb
@@ -13,7 +13,7 @@ describe MergeRequestSidebarBasicEntity do
describe '#current_user' do
it 'contains attributes related to the current user' do
- expect(entity[:current_user].keys).to contain_exactly(
+ expect(entity[:current_user].keys).to include(
:id, :name, :username, :state, :avatar_url, :web_url, :todo,
:can_edit, :can_move, :can_admin_label, :can_merge
)
diff --git a/spec/serializers/service_event_entity_spec.rb b/spec/serializers/service_event_entity_spec.rb
new file mode 100644
index 00000000000..fc11263807b
--- /dev/null
+++ b/spec/serializers/service_event_entity_spec.rb
@@ -0,0 +1,41 @@
+# frozen_string_literal: true
+
+require 'spec_helper'
+
+describe ServiceEventEntity do
+ let(:request) { double('request') }
+
+ subject { described_class.new(event, request: request, service: service).as_json }
+
+ before do
+ allow(request).to receive(:service).and_return(service)
+ end
+
+ describe '#as_json' do
+ context 'service without fields' do
+ let(:service) { create(:emails_on_push_service, push_events: true) }
+ let(:event) { 'push' }
+
+ it 'exposes correct attributes' do
+ expect(subject[:description]).to eq('Event will be triggered by a push to the repository')
+ expect(subject[:name]).to eq('push_events')
+ expect(subject[:title]).to eq('push')
+ expect(subject[:value]).to be(true)
+ end
+ end
+
+ context 'service with fields' do
+ let(:service) { create(:slack_service, note_events: false, note_channel: 'note-channel') }
+ let(:event) { 'note' }
+
+ it 'exposes correct attributes' do
+ expect(subject[:description]).to eq('Event will be triggered when someone adds a comment')
+ expect(subject[:name]).to eq('note_events')
+ expect(subject[:title]).to eq('note')
+ expect(subject[:value]).to eq(false)
+ expect(subject[:field][:name]).to eq('note_channel')
+ expect(subject[:field][:value]).to eq('note-channel')
+ end
+ end
+ end
+end
diff --git a/spec/serializers/test_case_entity_spec.rb b/spec/serializers/test_case_entity_spec.rb
index f16c271be4d..9f1822ff581 100644
--- a/spec/serializers/test_case_entity_spec.rb
+++ b/spec/serializers/test_case_entity_spec.rb
@@ -38,7 +38,7 @@ describe TestCaseEntity do
end
context 'when attachment is present' do
- let(:test_case) { build(:test_case, :with_attachment) }
+ let(:test_case) { build(:test_case, :failed_with_attachment) }
it 'returns the attachment_url' do
expect(subject).to include(:attachment_url)
@@ -60,7 +60,7 @@ describe TestCaseEntity do
end
context 'when attachment is present' do
- let(:test_case) { build(:test_case, :with_attachment) }
+ let(:test_case) { build(:test_case, :failed_with_attachment) }
it 'returns no attachment_url' do
expect(subject).not_to include(:attachment_url)
diff --git a/spec/serializers/test_suite_entity_spec.rb b/spec/serializers/test_suite_entity_spec.rb
index 6a9653954f3..bd88d235013 100644
--- a/spec/serializers/test_suite_entity_spec.rb
+++ b/spec/serializers/test_suite_entity_spec.rb
@@ -3,27 +3,65 @@
require 'spec_helper'
describe TestSuiteEntity do
- let(:pipeline) { create(:ci_pipeline, :with_test_reports) }
- let(:entity) { described_class.new(pipeline.test_reports.test_suites.each_value.first) }
+ let(:pipeline) { create(:ci_pipeline, :with_test_reports) }
+ let(:test_suite) { pipeline.test_reports.test_suites.each_value.first }
+ let(:entity) { described_class.new(test_suite) }
describe '#as_json' do
subject(:as_json) { entity.as_json }
it 'contains the suite name' do
- expect(as_json).to include(:name)
+ expect(as_json[:name]).to be_present
end
it 'contains the total time' do
- expect(as_json).to include(:total_time)
+ expect(as_json[:total_time]).to be_present
end
it 'contains the counts' do
- expect(as_json).to include(:total_count, :success_count, :failed_count, :skipped_count, :error_count)
+ expect(as_json[:total_count]).to eq(4)
+ expect(as_json[:success_count]).to eq(2)
+ expect(as_json[:failed_count]).to eq(2)
+ expect(as_json[:skipped_count]).to eq(0)
+ expect(as_json[:error_count]).to eq(0)
end
it 'contains the test cases' do
- expect(as_json).to include(:test_cases)
expect(as_json[:test_cases].count).to eq(4)
end
+
+ it 'contains an empty error message' do
+ expect(as_json[:suite_error]).to be_nil
+ end
+
+ context 'with a suite error' do
+ before do
+ test_suite.set_suite_error('a really bad error')
+ end
+
+ it 'contains the suite name' do
+ expect(as_json[:name]).to be_present
+ end
+
+ it 'contains the total time' do
+ expect(as_json[:total_time]).to be_present
+ end
+
+ it 'returns all the counts as 0' do
+ expect(as_json[:total_count]).to eq(0)
+ expect(as_json[:success_count]).to eq(0)
+ expect(as_json[:failed_count]).to eq(0)
+ expect(as_json[:skipped_count]).to eq(0)
+ expect(as_json[:error_count]).to eq(0)
+ end
+
+ it 'returns no test cases' do
+ expect(as_json[:test_cases]).to be_empty
+ end
+
+ it 'returns a suite error' do
+ expect(as_json[:suite_error]).to eq('a really bad error')
+ end
+ end
end
end
diff --git a/spec/services/alert_management/create_alert_issue_service_spec.rb b/spec/services/alert_management/create_alert_issue_service_spec.rb
new file mode 100644
index 00000000000..62afe777165
--- /dev/null
+++ b/spec/services/alert_management/create_alert_issue_service_spec.rb
@@ -0,0 +1,152 @@
+# frozen_string_literal: true
+
+require 'spec_helper'
+
+RSpec.describe AlertManagement::CreateAlertIssueService do
+ let_it_be(:user) { create(:user) }
+ let_it_be(:project) { create(:project) }
+ let_it_be(:payload) do
+ {
+ 'annotations' => {
+ 'title' => 'Alert title'
+ },
+ 'startsAt' => '2020-04-27T10:10:22.265949279Z',
+ 'generatorURL' => 'http://8d467bd4607a:9090/graph?g0.expr=vector%281%29&g0.tab=1'
+ }
+ end
+ let_it_be(:generic_alert, reload: true) { create(:alert_management_alert, :triggered, project: project, payload: payload) }
+ let_it_be(:prometheus_alert) { create(:alert_management_alert, :triggered, :prometheus, project: project, payload: payload) }
+ let(:alert) { generic_alert }
+ let(:created_issue) { Issue.last! }
+
+ describe '#execute' do
+ subject(:execute) { described_class.new(alert, user).execute }
+
+ before do
+ allow(user).to receive(:can?).and_call_original
+ allow(user).to receive(:can?)
+ .with(:create_issue, project)
+ .and_return(can_create)
+ end
+
+ shared_examples 'creating an alert' do
+ it 'creates an issue' do
+ expect { execute }.to change { project.issues.count }.by(1)
+ end
+
+ it 'returns a created issue' do
+ expect(execute.payload).to eq(issue: created_issue)
+ end
+
+ it 'has a successful status' do
+ expect(execute).to be_success
+ end
+
+ it 'updates alert.issue_id' do
+ execute
+
+ expect(alert.reload.issue_id).to eq(created_issue.id)
+ end
+
+ it 'sets issue author to the current user' do
+ execute
+
+ expect(created_issue.author).to eq(user)
+ end
+ end
+
+ context 'when a user is allowed to create an issue' do
+ let(:can_create) { true }
+
+ before do
+ project.add_developer(user)
+ end
+
+ it 'checks permissions' do
+ execute
+ expect(user).to have_received(:can?).with(:create_issue, project)
+ end
+
+ context 'when the alert is prometheus alert' do
+ let(:alert) { prometheus_alert }
+
+ it_behaves_like 'creating an alert'
+ end
+
+ context 'when the alert is generic' do
+ let(:alert) { generic_alert }
+
+ it_behaves_like 'creating an alert'
+ end
+
+ context 'when issue cannot be created' do
+ let(:alert) { prometheus_alert }
+
+ before do
+ # set invalid payload for Prometheus alert
+ alert.update!(payload: {})
+ end
+
+ it 'has an unsuccessful status' do
+ expect(execute).to be_error
+ expect(execute.message).to eq('invalid alert')
+ end
+ end
+
+ context 'when alert cannot be updated' do
+ before do
+ # invalidate alert
+ too_many_hosts = Array.new(AlertManagement::Alert::HOSTS_MAX_LENGTH + 1) { |_| 'host' }
+ alert.update_columns(hosts: too_many_hosts)
+ end
+
+ it 'responds with error' do
+ expect(execute).to be_error
+ expect(execute.message).to eq('Hosts hosts array is over 255 chars')
+ end
+ end
+
+ context 'when alert already has an attached issue' do
+ let!(:issue) { create(:issue, project: project) }
+
+ before do
+ alert.update!(issue_id: issue.id)
+ end
+
+ it 'does not create yet another issue' do
+ expect { execute }.not_to change(Issue, :count)
+ end
+
+ it 'responds with error' do
+ expect(execute).to be_error
+ expect(execute.message).to eq(_('An issue already exists'))
+ end
+ end
+
+ context 'when alert_management_create_alert_issue feature flag is disabled' do
+ before do
+ stub_feature_flags(alert_management_create_alert_issue: false)
+ end
+
+ it 'responds with error' do
+ expect(execute).to be_error
+ expect(execute.message).to eq(_('You have no permissions'))
+ end
+ end
+ end
+
+ context 'when a user is not allowed to create an issue' do
+ let(:can_create) { false }
+
+ it 'checks permissions' do
+ execute
+ expect(user).to have_received(:can?).with(:create_issue, project)
+ end
+
+ it 'responds with error' do
+ expect(execute).to be_error
+ expect(execute.message).to eq(_('You have no permissions'))
+ end
+ end
+ end
+end
diff --git a/spec/services/alert_management/process_prometheus_alert_service_spec.rb b/spec/services/alert_management/process_prometheus_alert_service_spec.rb
new file mode 100644
index 00000000000..73f9f103902
--- /dev/null
+++ b/spec/services/alert_management/process_prometheus_alert_service_spec.rb
@@ -0,0 +1,136 @@
+# frozen_string_literal: true
+
+require 'spec_helper'
+
+RSpec.describe AlertManagement::ProcessPrometheusAlertService do
+ let_it_be(:project) { create(:project) }
+
+ describe '#execute' do
+ subject { described_class.new(project, nil, payload).execute }
+
+ context 'when alert payload is valid' do
+ let(:parsed_alert) { Gitlab::Alerting::Alert.new(project: project, payload: payload) }
+ let(:payload) do
+ {
+ 'status' => status,
+ 'labels' => {
+ 'alertname' => 'GitalyFileServerDown',
+ 'channel' => 'gitaly',
+ 'pager' => 'pagerduty',
+ 'severity' => 's1'
+ },
+ 'annotations' => {
+ 'description' => 'Alert description',
+ 'runbook' => 'troubleshooting/gitaly-down.md',
+ 'title' => 'Alert title'
+ },
+ 'startsAt' => '2020-04-27T10:10:22.265949279Z',
+ 'endsAt' => '2020-04-27T10:20:22.265949279Z',
+ 'generatorURL' => 'http://8d467bd4607a:9090/graph?g0.expr=vector%281%29&g0.tab=1',
+ 'fingerprint' => 'b6ac4d42057c43c1'
+ }
+ end
+
+ context 'when Prometheus alert status is firing' do
+ let(:status) { 'firing' }
+
+ context 'when alert with the same fingerprint already exists' do
+ let!(:alert) { create(:alert_management_alert, :resolved, project: project, fingerprint: parsed_alert.gitlab_fingerprint) }
+
+ context 'when status can be changed' do
+ it 'changes status to triggered' do
+ expect { subject }.to change { alert.reload.triggered? }.to(true)
+ end
+ end
+
+ context 'when status change did not succeed' do
+ before do
+ allow(AlertManagement::Alert).to receive(:for_fingerprint).and_return([alert])
+ allow(alert).to receive(:trigger).and_return(false)
+ end
+
+ it 'writes a warning to the log' do
+ expect(Gitlab::AppLogger).to receive(:warn).with(
+ message: 'Unable to update AlertManagement::Alert status to triggered',
+ project_id: project.id,
+ alert_id: alert.id
+ )
+
+ subject
+ end
+ end
+
+ it { is_expected.to be_success }
+ end
+
+ context 'when alert does not exist' do
+ context 'when alert can be created' do
+ it 'creates a new alert' do
+ expect { subject }.to change { AlertManagement::Alert.where(project: project).count }.by(1)
+ end
+ end
+
+ context 'when alert cannot be created' do
+ let(:errors) { double(messages: { hosts: ['hosts array is over 255 chars'] })}
+ let(:am_alert) { instance_double(AlertManagement::Alert, save: false, errors: errors) }
+
+ before do
+ allow(AlertManagement::Alert).to receive(:new).and_return(am_alert)
+ end
+
+ it 'writes a warning to the log' do
+ expect(Gitlab::AppLogger).to receive(:warn).with(
+ message: 'Unable to create AlertManagement::Alert',
+ project_id: project.id,
+ alert_errors: { hosts: ['hosts array is over 255 chars'] }
+ )
+
+ subject
+ end
+ end
+
+ it { is_expected.to be_success }
+ end
+ end
+
+ context 'when Prometheus alert status is resolved' do
+ let(:status) { 'resolved' }
+ let!(:alert) { create(:alert_management_alert, project: project, fingerprint: parsed_alert.gitlab_fingerprint) }
+
+ context 'when status can be changed' do
+ it 'resolves an existing alert' do
+ expect { subject }.to change { alert.reload.resolved? }.to(true)
+ end
+ end
+
+ context 'when status change did not succeed' do
+ before do
+ allow(AlertManagement::Alert).to receive(:for_fingerprint).and_return([alert])
+ allow(alert).to receive(:resolve).and_return(false)
+ end
+
+ it 'writes a warning to the log' do
+ expect(Gitlab::AppLogger).to receive(:warn).with(
+ message: 'Unable to update AlertManagement::Alert status to resolved',
+ project_id: project.id,
+ alert_id: alert.id
+ )
+
+ subject
+ end
+ end
+
+ it { is_expected.to be_success }
+ end
+ end
+
+ context 'when alert payload is invalid' do
+ let(:payload) { {} }
+
+ it 'responds with bad_request' do
+ expect(subject).to be_error
+ expect(subject.http_status).to eq(:bad_request)
+ end
+ end
+ end
+end
diff --git a/spec/services/alert_management/update_alert_status_service_spec.rb b/spec/services/alert_management/update_alert_status_service_spec.rb
new file mode 100644
index 00000000000..b287d0d1614
--- /dev/null
+++ b/spec/services/alert_management/update_alert_status_service_spec.rb
@@ -0,0 +1,66 @@
+# frozen_string_literal: true
+
+require 'spec_helper'
+
+describe AlertManagement::UpdateAlertStatusService do
+ let(:project) { alert.project }
+ let_it_be(:user) { build(:user) }
+
+ let_it_be(:alert, reload: true) do
+ create(:alert_management_alert, :triggered)
+ end
+
+ let(:service) { described_class.new(alert, user, new_status) }
+
+ describe '#execute' do
+ shared_examples 'update failure' do |error_message|
+ it 'returns an error' do
+ expect(response).to be_error
+ expect(response.message).to eq(error_message)
+ expect(response.payload[:alert]).to eq(alert)
+ end
+
+ it 'does not update the status' do
+ expect { response }.not_to change { alert.status }
+ end
+ end
+
+ let(:new_status) { Types::AlertManagement::StatusEnum.values['ACKNOWLEDGED'].value }
+ let(:can_update) { true }
+
+ subject(:response) { service.execute }
+
+ before do
+ allow(user).to receive(:can?)
+ .with(:update_alert_management_alert, project)
+ .and_return(can_update)
+ end
+
+ it 'returns success' do
+ expect(response).to be_success
+ expect(response.payload[:alert]).to eq(alert)
+ end
+
+ it 'updates the status' do
+ expect { response }.to change { alert.acknowledged? }.to(true)
+ end
+
+ context 'when user has no permissions' do
+ let(:can_update) { false }
+
+ include_examples 'update failure', _('You have no permissions')
+ end
+
+ context 'with no status' do
+ let(:new_status) { nil }
+
+ include_examples 'update failure', _('Invalid status')
+ end
+
+ context 'with unknown status' do
+ let(:new_status) { -1 }
+
+ include_examples 'update failure', _('Invalid status')
+ end
+ end
+end
diff --git a/spec/services/application_settings/update_service_spec.rb b/spec/services/application_settings/update_service_spec.rb
index 069572e4dff..3a37cbc3522 100644
--- a/spec/services/application_settings/update_service_spec.rb
+++ b/spec/services/application_settings/update_service_spec.rb
@@ -335,7 +335,7 @@ describe ApplicationSettings::UpdateService do
end
end
- context 'when issues_create_limit is passsed' do
+ context 'when issues_create_limit is passed' do
let(:params) do
{
issues_create_limit: 600
diff --git a/spec/services/auth/container_registry_authentication_service_spec.rb b/spec/services/auth/container_registry_authentication_service_spec.rb
index 8273269c2fb..70eb35f0826 100644
--- a/spec/services/auth/container_registry_authentication_service_spec.rb
+++ b/spec/services/auth/container_registry_authentication_service_spec.rb
@@ -35,11 +35,11 @@ describe Auth::ContainerRegistryAuthenticationService do
it { expect(payload).to include('access') }
context 'a expirable' do
- let(:expires_at) { Time.at(payload['exp']) }
+ let(:expires_at) { Time.zone.at(payload['exp']) }
let(:expire_delay) { 10 }
context 'for default configuration' do
- it { expect(expires_at).not_to be_within(2.seconds).of(Time.now + expire_delay.minutes) }
+ it { expect(expires_at).not_to be_within(2.seconds).of(Time.current + expire_delay.minutes) }
end
context 'for changed configuration' do
@@ -47,7 +47,7 @@ describe Auth::ContainerRegistryAuthenticationService do
stub_application_setting(container_registry_token_expire_delay: expire_delay)
end
- it { expect(expires_at).to be_within(2.seconds).of(Time.now + expire_delay.minutes) }
+ it { expect(expires_at).to be_within(2.seconds).of(Time.current + expire_delay.minutes) }
end
end
end
@@ -205,6 +205,20 @@ describe Auth::ContainerRegistryAuthenticationService do
it_behaves_like 'an inaccessible'
it_behaves_like 'not a container repository factory'
+
+ it 'logs an auth warning' do
+ expect(Gitlab::AuthLogger).to receive(:warn).with(
+ message: 'Denied container registry permissions',
+ scope_type: 'repository',
+ requested_project_path: project.full_path,
+ requested_actions: ['*'],
+ authorized_actions: [],
+ user_id: current_user.id,
+ username: current_user.username
+ )
+
+ subject
+ end
end
context 'disallow developer to delete images since registry 2.7' do
diff --git a/spec/services/authorized_project_update/project_create_service_spec.rb b/spec/services/authorized_project_update/project_create_service_spec.rb
new file mode 100644
index 00000000000..49ea538d909
--- /dev/null
+++ b/spec/services/authorized_project_update/project_create_service_spec.rb
@@ -0,0 +1,142 @@
+# frozen_string_literal: true
+
+require 'spec_helper'
+
+describe AuthorizedProjectUpdate::ProjectCreateService do
+ let_it_be(:group_parent) { create(:group, :private) }
+ let_it_be(:group) { create(:group, :private, parent: group_parent) }
+ let_it_be(:group_child) { create(:group, :private, parent: group) }
+
+ let_it_be(:group_project) { create(:project, group: group) }
+
+ let_it_be(:parent_group_user) { create(:user) }
+ let_it_be(:group_user) { create(:user) }
+ let_it_be(:child_group_user) { create(:user) }
+
+ let(:access_level) { Gitlab::Access::MAINTAINER }
+
+ subject(:service) { described_class.new(group_project) }
+
+ describe '#perform' do
+ context 'direct group members' do
+ before do
+ create(:group_member, access_level: access_level, group: group, user: group_user)
+ ProjectAuthorization.delete_all
+ end
+
+ it 'creates project authorization' do
+ expect { service.execute }.to(
+ change { ProjectAuthorization.count }.from(0).to(1))
+
+ project_authorization = ProjectAuthorization.where(
+ project_id: group_project.id,
+ user_id: group_user.id,
+ access_level: access_level)
+
+ expect(project_authorization).to exist
+ end
+ end
+
+ context 'inherited group members' do
+ before do
+ create(:group_member, access_level: access_level, group: group_parent, user: parent_group_user)
+ ProjectAuthorization.delete_all
+ end
+
+ it 'creates project authorization' do
+ expect { service.execute }.to(
+ change { ProjectAuthorization.count }.from(0).to(1))
+
+ project_authorization = ProjectAuthorization.where(
+ project_id: group_project.id,
+ user_id: parent_group_user.id,
+ access_level: access_level)
+ expect(project_authorization).to exist
+ end
+ end
+
+ context 'membership overrides' do
+ before do
+ create(:group_member, access_level: Gitlab::Access::REPORTER, group: group_parent, user: group_user)
+ create(:group_member, access_level: Gitlab::Access::DEVELOPER, group: group, user: group_user)
+ ProjectAuthorization.delete_all
+ end
+
+ it 'creates project authorization' do
+ expect { service.execute }.to(
+ change { ProjectAuthorization.count }.from(0).to(1))
+
+ project_authorization = ProjectAuthorization.where(
+ project_id: group_project.id,
+ user_id: group_user.id,
+ access_level: Gitlab::Access::DEVELOPER)
+ expect(project_authorization).to exist
+ end
+ end
+
+ context 'no group member' do
+ it 'does not create project authorization' do
+ expect { service.execute }.not_to(
+ change { ProjectAuthorization.count }.from(0))
+ end
+ end
+
+ context 'unapproved access requests' do
+ before do
+ create(:group_member, :guest, :access_request, user: group_user, group: group)
+ end
+
+ it 'does not create project authorization' do
+ expect { service.execute }.not_to(
+ change { ProjectAuthorization.count }.from(0))
+ end
+ end
+
+ context 'project has more user than BATCH_SIZE' do
+ let(:batch_size) { 2 }
+ let(:users) { create_list(:user, batch_size + 1 ) }
+
+ before do
+ stub_const("#{described_class.name}::BATCH_SIZE", batch_size)
+
+ users.each do |user|
+ create(:group_member, access_level: access_level, group: group_parent, user: user)
+ end
+
+ ProjectAuthorization.delete_all
+ end
+
+ it 'bulk creates project authorizations in batches' do
+ users.each_slice(batch_size) do |batch|
+ attributes = batch.map do |user|
+ { user_id: user.id, project_id: group_project.id, access_level: access_level }
+ end
+
+ expect(ProjectAuthorization).to(
+ receive(:insert_all).with(array_including(attributes)).and_call_original)
+ end
+
+ expect { service.execute }.to(
+ change { ProjectAuthorization.count }.from(0).to(batch_size + 1))
+ end
+ end
+
+ context 'ignores existing project authorizations' do
+ before do
+ # ProjectAuthorizations is also created because of an after_commit
+ # callback on Member model
+ create(:group_member, access_level: access_level, group: group, user: group_user)
+ end
+
+ it 'does not create project authorization' do
+ project_authorization = ProjectAuthorization.where(
+ project_id: group_project.id,
+ user_id: group_user.id,
+ access_level: access_level)
+
+ expect { service.execute }.not_to(
+ change { project_authorization.reload.exists? }.from(true))
+ end
+ end
+ end
+end
diff --git a/spec/services/base_container_service_spec.rb b/spec/services/base_container_service_spec.rb
new file mode 100644
index 00000000000..47cfb387e25
--- /dev/null
+++ b/spec/services/base_container_service_spec.rb
@@ -0,0 +1,23 @@
+# frozen_string_literal: true
+
+require 'spec_helper'
+
+describe BaseContainerService do
+ let(:project) { Project.new }
+ let(:user) { User.new }
+
+ describe '#initialize' do
+ it 'accepts container and current_user' do
+ subject = described_class.new(container: project, current_user: user)
+
+ expect(subject.container).to eq(project)
+ expect(subject.current_user).to eq(user)
+ end
+
+ it 'treats current_user as optional' do
+ subject = described_class.new(container: project)
+
+ expect(subject.current_user).to be_nil
+ end
+ end
+end
diff --git a/spec/services/boards/issues/list_service_spec.rb b/spec/services/boards/issues/list_service_spec.rb
index 33538703e92..c46ab004af6 100644
--- a/spec/services/boards/issues/list_service_spec.rb
+++ b/spec/services/boards/issues/list_service_spec.rb
@@ -87,7 +87,7 @@ describe Boards::Issues::ListService do
let!(:opened_issue1) { create(:labeled_issue, project: project, milestone: m1, title: 'Issue 1', labels: [bug]) }
let!(:opened_issue2) { create(:labeled_issue, project: project, milestone: m2, title: 'Issue 2', labels: [p2, p2_project]) }
let!(:opened_issue3) { create(:labeled_issue, project: project_archived, milestone: m1, title: 'Issue 3', labels: [bug]) }
- let!(:reopened_issue1) { create(:issue, state: 'opened', project: project, title: 'Reopened Issue 1', closed_at: Time.now ) }
+ let!(:reopened_issue1) { create(:issue, state: 'opened', project: project, title: 'Reopened Issue 1', closed_at: Time.current ) }
let!(:list1_issue1) { create(:labeled_issue, project: project, milestone: m1, labels: [p2, p2_project, development]) }
let!(:list1_issue2) { create(:labeled_issue, project: project, milestone: m2, labels: [development]) }
diff --git a/spec/services/branches/create_service_spec.rb b/spec/services/branches/create_service_spec.rb
index b0629c5e25a..072a86d17fc 100644
--- a/spec/services/branches/create_service_spec.rb
+++ b/spec/services/branches/create_service_spec.rb
@@ -3,39 +3,45 @@
require 'spec_helper'
describe Branches::CreateService do
- let(:user) { create(:user) }
-
subject(:service) { described_class.new(project, user) }
+ let_it_be(:project) { create(:project_empty_repo) }
+ let_it_be(:user) { create(:user) }
+
describe '#execute' do
context 'when repository is empty' do
- let(:project) { create(:project_empty_repo) }
-
it 'creates master branch' do
service.execute('my-feature', 'master')
expect(project.repository.branch_exists?('master')).to be_truthy
end
- it 'creates my-feature branch' do
- service.execute('my-feature', 'master')
+ it 'creates another-feature branch' do
+ service.execute('another-feature', 'master')
- expect(project.repository.branch_exists?('my-feature')).to be_truthy
+ expect(project.repository.branch_exists?('another-feature')).to be_truthy
end
end
- context 'when creating a branch fails' do
- let(:project) { create(:project_empty_repo) }
+ context 'when branch already exists' do
+ it 'returns an error' do
+ result = service.execute('master', 'master')
+
+ expect(result[:status]).to eq(:error)
+ expect(result[:message]).to eq('Branch already exists')
+ end
+ end
+ context 'when incorrect reference is provided' do
before do
allow(project.repository).to receive(:add_branch).and_return(false)
end
- it 'returns an error with the branch name' do
- result = service.execute('my-feature', 'master')
+ it 'returns an error with a reference name' do
+ result = service.execute('new-feature', 'unknown')
expect(result[:status]).to eq(:error)
- expect(result[:message]).to eq("Invalid reference name: my-feature")
+ expect(result[:message]).to eq('Invalid reference name: unknown')
end
end
end
diff --git a/spec/services/ci/compare_accessibility_reports_service_spec.rb b/spec/services/ci/compare_accessibility_reports_service_spec.rb
new file mode 100644
index 00000000000..aee1fd14bc5
--- /dev/null
+++ b/spec/services/ci/compare_accessibility_reports_service_spec.rb
@@ -0,0 +1,62 @@
+# frozen_string_literal: true
+
+require 'spec_helper'
+
+describe Ci::CompareAccessibilityReportsService do
+ let(:service) { described_class.new(project) }
+ let(:project) { create(:project, :repository) }
+
+ describe '#execute' do
+ subject { service.execute(base_pipeline, head_pipeline) }
+
+ context 'when head pipeline has accessibility reports' do
+ let(:base_pipeline) { nil }
+ let(:head_pipeline) { create(:ci_pipeline, :with_accessibility_reports, project: project) }
+
+ it 'returns status and data' do
+ expect(subject[:status]).to eq(:parsed)
+ expect(subject[:data]).to match_schema('entities/accessibility_reports_comparer')
+ end
+ end
+
+ context 'when base and head pipelines have accessibility reports' do
+ let(:base_pipeline) { create(:ci_pipeline, :with_accessibility_reports, project: project) }
+ let(:head_pipeline) { create(:ci_pipeline, :with_accessibility_reports, project: project) }
+
+ it 'returns status and data' do
+ expect(subject[:status]).to eq(:parsed)
+ expect(subject[:data]).to match_schema('entities/accessibility_reports_comparer')
+ end
+ end
+ end
+
+ describe '#latest?' do
+ subject { service.latest?(base_pipeline, head_pipeline, data) }
+
+ let!(:base_pipeline) { nil }
+ let!(:head_pipeline) { create(:ci_pipeline, :with_accessibility_reports, project: project) }
+ let!(:key) { service.send(:key, base_pipeline, head_pipeline) }
+
+ context 'when cache key is latest' do
+ let(:data) { { key: key } }
+
+ it { is_expected.to be_truthy }
+ end
+
+ context 'when cache key is outdated' do
+ before do
+ head_pipeline.update_column(:updated_at, 10.minutes.ago)
+ end
+
+ let(:data) { { key: key } }
+
+ it { is_expected.to be_falsy }
+ end
+
+ context 'when cache key is empty' do
+ let(:data) { { key: nil } }
+
+ it { is_expected.to be_falsy }
+ end
+ end
+end
diff --git a/spec/services/ci/compare_test_reports_service_spec.rb b/spec/services/ci/compare_test_reports_service_spec.rb
index f5edd3a552d..46f4d2d42ff 100644
--- a/spec/services/ci/compare_test_reports_service_spec.rb
+++ b/spec/services/ci/compare_test_reports_service_spec.rb
@@ -38,9 +38,10 @@ describe Ci::CompareTestReportsService do
create(:ci_job_artifact, :junit_with_corrupted_data, job: build, project: project)
end
- it 'returns status and error message' do
- expect(subject[:status]).to eq(:error)
- expect(subject[:status_reason]).to include('XML parsing failed')
+ it 'returns a parsed TestReports success status and failure on the individual suite' do
+ expect(subject[:status]).to eq(:parsed)
+ expect(subject.dig(:data, 'status')).to eq('success')
+ expect(subject.dig(:data, 'suites', 0, 'status') ).to eq('error')
end
end
end
diff --git a/spec/services/ci/create_job_artifacts_service_spec.rb b/spec/services/ci/create_job_artifacts_service_spec.rb
index fe64a66f322..4d49923a184 100644
--- a/spec/services/ci/create_job_artifacts_service_spec.rb
+++ b/spec/services/ci/create_job_artifacts_service_spec.rb
@@ -30,6 +30,26 @@ describe Ci::CreateJobArtifactsService do
describe '#execute' do
subject { service.execute(job, artifacts_file, params, metadata_file: metadata_file) }
+ context 'locking' do
+ let(:old_job) { create(:ci_build, pipeline: create(:ci_pipeline, project: job.project, ref: job.ref)) }
+ let!(:latest_artifact) { create(:ci_job_artifact, job: old_job, locked: true) }
+ let!(:other_artifact) { create(:ci_job_artifact, locked: true) }
+
+ it 'locks the new artifact' do
+ subject
+
+ expect(Ci::JobArtifact.last).to have_attributes(locked: true)
+ end
+
+ it 'unlocks all other artifacts for the same ref' do
+ expect { subject }.to change { latest_artifact.reload.locked }.from(true).to(false)
+ end
+
+ it 'does not unlock artifacts for other refs' do
+ expect { subject }.not_to change { other_artifact.reload.locked }.from(true)
+ end
+ end
+
context 'when artifacts file is uploaded' do
it 'saves artifact for the given type' do
expect { subject }.to change { Ci::JobArtifact.count }.by(1)
@@ -157,6 +177,53 @@ describe Ci::CreateJobArtifactsService do
end
end
+ context 'when artifact type is cluster_applications' do
+ let(:artifacts_file) do
+ file_to_upload('spec/fixtures/helm/helm_list_v2_prometheus_missing.json.gz', sha256: artifacts_sha256)
+ end
+
+ let(:params) do
+ {
+ 'artifact_type' => 'cluster_applications',
+ 'artifact_format' => 'gzip'
+ }
+ end
+
+ it 'calls cluster applications parse service' do
+ expect_next_instance_of(Clusters::ParseClusterApplicationsArtifactService) do |service|
+ expect(service).to receive(:execute).once.and_call_original
+ end
+
+ subject
+ end
+
+ context 'when there is a deployment cluster' do
+ let(:user) { project.owner }
+
+ before do
+ job.update!(user: user)
+ end
+
+ it 'calls cluster applications parse service with job and job user', :aggregate_failures do
+ expect(Clusters::ParseClusterApplicationsArtifactService).to receive(:new).with(job, user).and_call_original
+
+ subject
+ end
+ end
+
+ context 'when ci_synchronous_artifact_parsing feature flag is disabled' do
+ before do
+ stub_feature_flags(ci_synchronous_artifact_parsing: false)
+ end
+
+ it 'does not call parse service' do
+ expect(Clusters::ParseClusterApplicationsArtifactService).not_to receive(:new)
+
+ expect(subject[:status]).to eq(:success)
+ end
+ end
+ end
+
shared_examples 'rescues object storage error' do |klass, message, expected_message|
it "handles #{klass}" do
allow_next_instance_of(JobArtifactUploader) do |uploader|
diff --git a/spec/services/ci/create_pipeline_service/custom_config_content_spec.rb b/spec/services/ci/create_pipeline_service/custom_config_content_spec.rb
index 112b19fcbc5..5980260a08a 100644
--- a/spec/services/ci/create_pipeline_service/custom_config_content_spec.rb
+++ b/spec/services/ci/create_pipeline_service/custom_config_content_spec.rb
@@ -34,7 +34,7 @@ describe Ci::CreatePipelineService do
it 'creates a pipeline using the content passed in as param' do
expect(subject).to be_persisted
- expect(subject.builds.map(&:name)).to eq %w[rspec custom]
+ expect(subject.builds.pluck(:name)).to match_array %w[rspec custom]
expect(subject.config_source).to eq 'bridge_source'
end
@@ -59,7 +59,7 @@ describe Ci::CreatePipelineService do
it 'created a pipeline using the content passed in as param and download the artifact' do
expect(subject).to be_persisted
- expect(subject.builds.pluck(:name)).to eq %w[rspec time custom]
+ expect(subject.builds.pluck(:name)).to match_array %w[rspec time custom]
expect(subject.config_source).to eq 'bridge_source'
end
end
diff --git a/spec/services/ci/daily_build_group_report_result_service_spec.rb b/spec/services/ci/daily_build_group_report_result_service_spec.rb
new file mode 100644
index 00000000000..f0b72b8fd86
--- /dev/null
+++ b/spec/services/ci/daily_build_group_report_result_service_spec.rb
@@ -0,0 +1,158 @@
+# frozen_string_literal: true
+
+require 'spec_helper'
+
+describe Ci::DailyBuildGroupReportResultService, '#execute' do
+ let!(:pipeline) { create(:ci_pipeline, created_at: '2020-02-06 00:01:10') }
+ let!(:rspec_job) { create(:ci_build, pipeline: pipeline, name: '3/3 rspec', coverage: 80) }
+ let!(:karma_job) { create(:ci_build, pipeline: pipeline, name: '2/2 karma', coverage: 90) }
+ let!(:extra_job) { create(:ci_build, pipeline: pipeline, name: 'extra', coverage: nil) }
+
+ it 'creates daily code coverage record for each job in the pipeline that has coverage value' do
+ described_class.new.execute(pipeline)
+
+ Ci::DailyBuildGroupReportResult.find_by(group_name: 'rspec').tap do |coverage|
+ expect(coverage).to have_attributes(
+ project_id: pipeline.project.id,
+ last_pipeline_id: pipeline.id,
+ ref_path: pipeline.source_ref_path,
+ group_name: rspec_job.group_name,
+ data: { 'coverage' => rspec_job.coverage },
+ date: pipeline.created_at.to_date
+ )
+ end
+
+ Ci::DailyBuildGroupReportResult.find_by(group_name: 'karma').tap do |coverage|
+ expect(coverage).to have_attributes(
+ project_id: pipeline.project.id,
+ last_pipeline_id: pipeline.id,
+ ref_path: pipeline.source_ref_path,
+ group_name: karma_job.group_name,
+ data: { 'coverage' => karma_job.coverage },
+ date: pipeline.created_at.to_date
+ )
+ end
+
+ expect(Ci::DailyBuildGroupReportResult.find_by(group_name: 'extra')).to be_nil
+ end
+
+ context 'when there are multiple builds with the same group name that report coverage' do
+ let!(:test_job_1) { create(:ci_build, pipeline: pipeline, name: '1/2 test', coverage: 70) }
+ let!(:test_job_2) { create(:ci_build, pipeline: pipeline, name: '2/2 test', coverage: 80) }
+
+ it 'creates daily code coverage record with the average as the value' do
+ described_class.new.execute(pipeline)
+
+ Ci::DailyBuildGroupReportResult.find_by(group_name: 'test').tap do |coverage|
+ expect(coverage).to have_attributes(
+ project_id: pipeline.project.id,
+ last_pipeline_id: pipeline.id,
+ ref_path: pipeline.source_ref_path,
+ group_name: test_job_2.group_name,
+ data: { 'coverage' => 75.0 },
+ date: pipeline.created_at.to_date
+ )
+ end
+ end
+ end
+
+ context 'when there is an existing daily code coverage for the matching date, project, ref_path, and group name' do
+ let!(:new_pipeline) do
+ create(
+ :ci_pipeline,
+ project: pipeline.project,
+ ref: pipeline.ref,
+ created_at: '2020-02-06 00:02:20'
+ )
+ end
+ let!(:new_rspec_job) { create(:ci_build, pipeline: new_pipeline, name: '4/4 rspec', coverage: 84) }
+ let!(:new_karma_job) { create(:ci_build, pipeline: new_pipeline, name: '3/3 karma', coverage: 92) }
+
+ before do
+ # Create the existing daily code coverage records
+ described_class.new.execute(pipeline)
+ end
+
+ it "updates the existing record's coverage value and last_pipeline_id" do
+ rspec_coverage = Ci::DailyBuildGroupReportResult.find_by(group_name: 'rspec')
+ karma_coverage = Ci::DailyBuildGroupReportResult.find_by(group_name: 'karma')
+
+ # Bump up the coverage values
+ described_class.new.execute(new_pipeline)
+
+ rspec_coverage.reload
+ karma_coverage.reload
+
+ expect(rspec_coverage).to have_attributes(
+ last_pipeline_id: new_pipeline.id,
+ data: { 'coverage' => new_rspec_job.coverage }
+ )
+
+ expect(karma_coverage).to have_attributes(
+ last_pipeline_id: new_pipeline.id,
+ data: { 'coverage' => new_karma_job.coverage }
+ )
+ end
+ end
+
+ context 'when the ID of the pipeline is older than the last_pipeline_id' do
+ let!(:new_pipeline) do
+ create(
+ :ci_pipeline,
+ project: pipeline.project,
+ ref: pipeline.ref,
+ created_at: '2020-02-06 00:02:20'
+ )
+ end
+ let!(:new_rspec_job) { create(:ci_build, pipeline: new_pipeline, name: '4/4 rspec', coverage: 84) }
+ let!(:new_karma_job) { create(:ci_build, pipeline: new_pipeline, name: '3/3 karma', coverage: 92) }
+
+ before do
+ # Create the existing daily code coverage records
+ # but in this case, for the newer pipeline first.
+ described_class.new.execute(new_pipeline)
+ end
+
+ it 'updates the existing daily code coverage records' do
+ rspec_coverage = Ci::DailyBuildGroupReportResult.find_by(group_name: 'rspec')
+ karma_coverage = Ci::DailyBuildGroupReportResult.find_by(group_name: 'karma')
+
+ # Run another one but for the older pipeline.
+ # This simulates the scenario wherein the success worker
+ # of an older pipeline, for some network hiccup, was delayed
+ # and only got executed right after the newer pipeline's success worker.
+ # Ideally, we don't want to bump the coverage value with an older one
+ # but given this can be a rare edge case and can be remedied by re-running
+ # the pipeline we'll just let it be for now. In return, we are able to use
+ # Rails 6 shiny new method, upsert_all, and simplify the code a lot.
+ described_class.new.execute(pipeline)
+
+ rspec_coverage.reload
+ karma_coverage.reload
+
+ expect(rspec_coverage).to have_attributes(
+ last_pipeline_id: pipeline.id,
+ data: { 'coverage' => rspec_job.coverage }
+ )
+
+ expect(karma_coverage).to have_attributes(
+ last_pipeline_id: pipeline.id,
+ data: { 'coverage' => karma_job.coverage }
+ )
+ end
+ end
+
+ context 'when pipeline has no builds with coverage' do
+ let!(:new_pipeline) do
+ create(
+ :ci_pipeline,
+ created_at: '2020-02-06 00:02:20'
+ )
+ end
+ let!(:some_job) { create(:ci_build, pipeline: new_pipeline, name: 'foo') }
+
+ it 'does nothing' do
+ expect { described_class.new.execute(new_pipeline) }.not_to raise_error
+ end
+ end
+end
diff --git a/spec/services/ci/daily_report_result_service_spec.rb b/spec/services/ci/daily_report_result_service_spec.rb
deleted file mode 100644
index 240709bab0b..00000000000
--- a/spec/services/ci/daily_report_result_service_spec.rb
+++ /dev/null
@@ -1,161 +0,0 @@
-# frozen_string_literal: true
-
-require 'spec_helper'
-
-describe Ci::DailyReportResultService, '#execute' do
- let!(:pipeline) { create(:ci_pipeline, created_at: '2020-02-06 00:01:10') }
- let!(:rspec_job) { create(:ci_build, pipeline: pipeline, name: '3/3 rspec', coverage: 80) }
- let!(:karma_job) { create(:ci_build, pipeline: pipeline, name: '2/2 karma', coverage: 90) }
- let!(:extra_job) { create(:ci_build, pipeline: pipeline, name: 'extra', coverage: nil) }
-
- it 'creates daily code coverage record for each job in the pipeline that has coverage value' do
- described_class.new.execute(pipeline)
-
- Ci::DailyReportResult.find_by(title: 'rspec').tap do |coverage|
- expect(coverage).to have_attributes(
- project_id: pipeline.project.id,
- last_pipeline_id: pipeline.id,
- ref_path: pipeline.source_ref_path,
- param_type: 'coverage',
- title: rspec_job.group_name,
- value: rspec_job.coverage,
- date: pipeline.created_at.to_date
- )
- end
-
- Ci::DailyReportResult.find_by(title: 'karma').tap do |coverage|
- expect(coverage).to have_attributes(
- project_id: pipeline.project.id,
- last_pipeline_id: pipeline.id,
- ref_path: pipeline.source_ref_path,
- param_type: 'coverage',
- title: karma_job.group_name,
- value: karma_job.coverage,
- date: pipeline.created_at.to_date
- )
- end
-
- expect(Ci::DailyReportResult.find_by(title: 'extra')).to be_nil
- end
-
- context 'when there are multiple builds with the same group name that report coverage' do
- let!(:test_job_1) { create(:ci_build, pipeline: pipeline, name: '1/2 test', coverage: 70) }
- let!(:test_job_2) { create(:ci_build, pipeline: pipeline, name: '2/2 test', coverage: 80) }
-
- it 'creates daily code coverage record with the average as the value' do
- described_class.new.execute(pipeline)
-
- Ci::DailyReportResult.find_by(title: 'test').tap do |coverage|
- expect(coverage).to have_attributes(
- project_id: pipeline.project.id,
- last_pipeline_id: pipeline.id,
- ref_path: pipeline.source_ref_path,
- param_type: 'coverage',
- title: test_job_2.group_name,
- value: 75,
- date: pipeline.created_at.to_date
- )
- end
- end
- end
-
- context 'when there is an existing daily code coverage for the matching date, project, ref_path, and group name' do
- let!(:new_pipeline) do
- create(
- :ci_pipeline,
- project: pipeline.project,
- ref: pipeline.ref,
- created_at: '2020-02-06 00:02:20'
- )
- end
- let!(:new_rspec_job) { create(:ci_build, pipeline: new_pipeline, name: '4/4 rspec', coverage: 84) }
- let!(:new_karma_job) { create(:ci_build, pipeline: new_pipeline, name: '3/3 karma', coverage: 92) }
-
- before do
- # Create the existing daily code coverage records
- described_class.new.execute(pipeline)
- end
-
- it "updates the existing record's coverage value and last_pipeline_id" do
- rspec_coverage = Ci::DailyReportResult.find_by(title: 'rspec')
- karma_coverage = Ci::DailyReportResult.find_by(title: 'karma')
-
- # Bump up the coverage values
- described_class.new.execute(new_pipeline)
-
- rspec_coverage.reload
- karma_coverage.reload
-
- expect(rspec_coverage).to have_attributes(
- last_pipeline_id: new_pipeline.id,
- value: new_rspec_job.coverage
- )
-
- expect(karma_coverage).to have_attributes(
- last_pipeline_id: new_pipeline.id,
- value: new_karma_job.coverage
- )
- end
- end
-
- context 'when the ID of the pipeline is older than the last_pipeline_id' do
- let!(:new_pipeline) do
- create(
- :ci_pipeline,
- project: pipeline.project,
- ref: pipeline.ref,
- created_at: '2020-02-06 00:02:20'
- )
- end
- let!(:new_rspec_job) { create(:ci_build, pipeline: new_pipeline, name: '4/4 rspec', coverage: 84) }
- let!(:new_karma_job) { create(:ci_build, pipeline: new_pipeline, name: '3/3 karma', coverage: 92) }
-
- before do
- # Create the existing daily code coverage records
- # but in this case, for the newer pipeline first.
- described_class.new.execute(new_pipeline)
- end
-
- it 'updates the existing daily code coverage records' do
- rspec_coverage = Ci::DailyReportResult.find_by(title: 'rspec')
- karma_coverage = Ci::DailyReportResult.find_by(title: 'karma')
-
- # Run another one but for the older pipeline.
- # This simulates the scenario wherein the success worker
- # of an older pipeline, for some network hiccup, was delayed
- # and only got executed right after the newer pipeline's success worker.
- # Ideally, we don't want to bump the coverage value with an older one
- # but given this can be a rare edge case and can be remedied by re-running
- # the pipeline we'll just let it be for now. In return, we are able to use
- # Rails 6 shiny new method, upsert_all, and simplify the code a lot.
- described_class.new.execute(pipeline)
-
- rspec_coverage.reload
- karma_coverage.reload
-
- expect(rspec_coverage).to have_attributes(
- last_pipeline_id: pipeline.id,
- value: rspec_job.coverage
- )
-
- expect(karma_coverage).to have_attributes(
- last_pipeline_id: pipeline.id,
- value: karma_job.coverage
- )
- end
- end
-
- context 'when pipeline has no builds with coverage' do
- let!(:new_pipeline) do
- create(
- :ci_pipeline,
- created_at: '2020-02-06 00:02:20'
- )
- end
- let!(:some_job) { create(:ci_build, pipeline: new_pipeline, name: 'foo') }
-
- it 'does nothing' do
- expect { described_class.new.execute(new_pipeline) }.not_to raise_error
- end
- end
-end
diff --git a/spec/services/ci/destroy_expired_job_artifacts_service_spec.rb b/spec/services/ci/destroy_expired_job_artifacts_service_spec.rb
index fc5450ab33d..4b9f12d8fdf 100644
--- a/spec/services/ci/destroy_expired_job_artifacts_service_spec.rb
+++ b/spec/services/ci/destroy_expired_job_artifacts_service_spec.rb
@@ -11,8 +11,26 @@ describe Ci::DestroyExpiredJobArtifactsService, :clean_gitlab_redis_shared_state
let(:service) { described_class.new }
let!(:artifact) { create(:ci_job_artifact, expire_at: 1.day.ago) }
- it 'destroys expired job artifacts' do
- expect { subject }.to change { Ci::JobArtifact.count }.by(-1)
+ context 'when artifact is expired' do
+ context 'when artifact is not locked' do
+ before do
+ artifact.update!(locked: false)
+ end
+
+ it 'destroys job artifact' do
+ expect { subject }.to change { Ci::JobArtifact.count }.by(-1)
+ end
+ end
+
+ context 'when artifact is locked' do
+ before do
+ artifact.update!(locked: true)
+ end
+
+ it 'does not destroy job artifact' do
+ expect { subject }.not_to change { Ci::JobArtifact.count }
+ end
+ end
end
context 'when artifact is not expired' do
@@ -72,7 +90,7 @@ describe Ci::DestroyExpiredJobArtifactsService, :clean_gitlab_redis_shared_state
stub_const('Ci::DestroyExpiredJobArtifactsService::BATCH_SIZE', 1)
end
- let!(:artifact) { create_list(:ci_job_artifact, 2, expire_at: 1.day.ago) }
+ let!(:second_artifact) { create(:ci_job_artifact, expire_at: 1.day.ago) }
it 'raises an error and does not continue destroying' do
is_expected.to be_falsy
@@ -96,7 +114,7 @@ describe Ci::DestroyExpiredJobArtifactsService, :clean_gitlab_redis_shared_state
stub_const('Ci::DestroyExpiredJobArtifactsService::BATCH_SIZE', 1)
end
- let!(:artifact) { create_list(:ci_job_artifact, 2, expire_at: 1.day.ago) }
+ let!(:second_artifact) { create(:ci_job_artifact, expire_at: 1.day.ago) }
it 'destroys all expired artifacts' do
expect { subject }.to change { Ci::JobArtifact.count }.by(-2)
diff --git a/spec/services/ci/generate_terraform_reports_service_spec.rb b/spec/services/ci/generate_terraform_reports_service_spec.rb
new file mode 100644
index 00000000000..4d2c60bed2c
--- /dev/null
+++ b/spec/services/ci/generate_terraform_reports_service_spec.rb
@@ -0,0 +1,71 @@
+# frozen_string_literal: true
+
+require 'spec_helper'
+
+describe Ci::GenerateTerraformReportsService do
+ let_it_be(:project) { create(:project, :repository) }
+
+ describe '#execute' do
+ let_it_be(:merge_request) { create(:merge_request, :with_terraform_reports, source_project: project) }
+
+ subject { described_class.new(project, nil, id: merge_request.id) }
+
+ context 'when head pipeline has terraform reports' do
+ it 'returns status and data' do
+ result = subject.execute(nil, merge_request.head_pipeline)
+
+ expect(result).to match(
+ status: :parsed,
+ data: match(
+ a_hash_including('tfplan.json' => a_hash_including('create' => 0, 'update' => 1, 'delete' => 0))
+ ),
+ key: an_instance_of(Array)
+ )
+ end
+ end
+
+ context 'when head pipeline has corrupted terraform reports' do
+ it 'returns status and error message' do
+ build = create(:ci_build, pipeline: merge_request.head_pipeline, project: project)
+ create(:ci_job_artifact, :terraform_with_corrupted_data, job: build, project: project)
+
+ result = subject.execute(nil, merge_request.head_pipeline)
+
+ expect(result).to match(
+ status: :error,
+ status_reason: 'An error occurred while fetching terraform reports.',
+ key: an_instance_of(Array)
+ )
+ end
+ end
+ end
+
+ describe '#latest?' do
+ let_it_be(:head_pipeline) { create(:ci_pipeline, :with_test_reports, project: project) }
+
+ subject { described_class.new(project) }
+
+ it 'returns true when cache key is latest' do
+ cache_key = subject.send(:key, nil, head_pipeline)
+
+ result = subject.latest?(nil, head_pipeline, key: cache_key)
+
+ expect(result).to eq(true)
+ end
+
+ it 'returns false when cache key is outdated' do
+ cache_key = subject.send(:key, nil, head_pipeline)
+ head_pipeline.update_column(:updated_at, 10.minutes.ago)
+
+ result = subject.latest?(nil, head_pipeline, key: cache_key)
+
+ expect(result).to eq(false)
+ end
+
+ it 'returns false when cache key is nil' do
+ result = subject.latest?(nil, head_pipeline, key: nil)
+
+ expect(result).to eq(false)
+ end
+ end
+end
diff --git a/spec/services/ci/pipeline_processing/atomic_processing_service/status_collection_spec.rb b/spec/services/ci/pipeline_processing/atomic_processing_service/status_collection_spec.rb
index b487730d07f..de3c7713ac8 100644
--- a/spec/services/ci/pipeline_processing/atomic_processing_service/status_collection_spec.rb
+++ b/spec/services/ci/pipeline_processing/atomic_processing_service/status_collection_spec.rb
@@ -18,7 +18,7 @@ describe Ci::PipelineProcessing::AtomicProcessingService::StatusCollection do
it 'does update existing status of processable' do
collection.set_processable_status(test_a.id, 'success', 100)
- expect(collection.status_for_names(['test-a'])).to eq('success')
+ expect(collection.status_for_names(['test-a'], dag: false)).to eq('success')
end
it 'ignores a missing processable' do
@@ -33,15 +33,18 @@ describe Ci::PipelineProcessing::AtomicProcessingService::StatusCollection do
end
describe '#status_for_names' do
- where(:names, :status) do
- %w[build-a] | 'success'
- %w[build-a build-b] | 'failed'
- %w[build-a test-a] | 'running'
+ where(:names, :status, :dag) do
+ %w[build-a] | 'success' | false
+ %w[build-a build-b] | 'failed' | false
+ %w[build-a test-a] | 'running' | false
+ %w[build-a] | 'success' | true
+ %w[build-a build-b] | 'failed' | true
+ %w[build-a test-a] | 'pending' | true
end
with_them do
it 'returns composite status of given names' do
- expect(collection.status_for_names(names)).to eq(status)
+ expect(collection.status_for_names(names, dag: dag)).to eq(status)
end
end
end
diff --git a/spec/services/ci/pipeline_processing/atomic_processing_service_spec.rb b/spec/services/ci/pipeline_processing/atomic_processing_service_spec.rb
index cbeb45b92ff..3b66ecff196 100644
--- a/spec/services/ci/pipeline_processing/atomic_processing_service_spec.rb
+++ b/spec/services/ci/pipeline_processing/atomic_processing_service_spec.rb
@@ -2,13 +2,19 @@
require 'spec_helper'
require_relative 'shared_processing_service.rb'
+require_relative 'shared_processing_service_tests_with_yaml.rb'
describe Ci::PipelineProcessing::AtomicProcessingService do
before do
stub_feature_flags(ci_atomic_processing: true)
+
+ # This feature flag is implicit
+ # Atomic Processing does not process statuses differently
+ stub_feature_flags(ci_composite_status: true)
end
it_behaves_like 'Pipeline Processing Service'
+ it_behaves_like 'Pipeline Processing Service Tests With Yaml'
private
diff --git a/spec/services/ci/pipeline_processing/legacy_processing_service_spec.rb b/spec/services/ci/pipeline_processing/legacy_processing_service_spec.rb
index 09b462b7600..fd491bf461b 100644
--- a/spec/services/ci/pipeline_processing/legacy_processing_service_spec.rb
+++ b/spec/services/ci/pipeline_processing/legacy_processing_service_spec.rb
@@ -2,13 +2,30 @@
require 'spec_helper'
require_relative 'shared_processing_service.rb'
+require_relative 'shared_processing_service_tests_with_yaml.rb'
describe Ci::PipelineProcessing::LegacyProcessingService do
before do
stub_feature_flags(ci_atomic_processing: false)
end
- it_behaves_like 'Pipeline Processing Service'
+ context 'when ci_composite_status is enabled' do
+ before do
+ stub_feature_flags(ci_composite_status: true)
+ end
+
+ it_behaves_like 'Pipeline Processing Service'
+ it_behaves_like 'Pipeline Processing Service Tests With Yaml'
+ end
+
+ context 'when ci_composite_status is disabled' do
+ before do
+ stub_feature_flags(ci_composite_status: false)
+ end
+
+ it_behaves_like 'Pipeline Processing Service'
+ it_behaves_like 'Pipeline Processing Service Tests With Yaml'
+ end
private
diff --git a/spec/services/ci/pipeline_processing/shared_processing_service.rb b/spec/services/ci/pipeline_processing/shared_processing_service.rb
index ffe5eacfc48..29fa43001ae 100644
--- a/spec/services/ci/pipeline_processing/shared_processing_service.rb
+++ b/spec/services/ci/pipeline_processing/shared_processing_service.rb
@@ -816,10 +816,10 @@ shared_examples 'Pipeline Processing Service' do
context 'when a needed job is skipped', :sidekiq_inline do
let!(:linux_build) { create_build('linux:build', stage: 'build', stage_idx: 0) }
let!(:linux_rspec) { create_build('linux:rspec', stage: 'test', stage_idx: 1) }
- let!(:deploy) do
- create_build('deploy', stage: 'deploy', stage_idx: 2, scheduling_type: :dag, needs: [
- create(:ci_build_need, name: 'linux:rspec')
- ])
+ let!(:deploy) { create_build('deploy', stage: 'deploy', stage_idx: 2, scheduling_type: :dag) }
+
+ before do
+ create(:ci_build_need, build: deploy, name: 'linux:build')
end
it 'skips the jobs depending on it' do
@@ -836,6 +836,23 @@ shared_examples 'Pipeline Processing Service' do
end
end
+ context 'when a needed job is manual', :sidekiq_inline do
+ let!(:linux_build) { create_build('linux:build', stage: 'build', stage_idx: 0, when: 'manual', allow_failure: true) }
+ let!(:deploy) { create_build('deploy', stage: 'deploy', stage_idx: 1, scheduling_type: :dag) }
+
+ before do
+ create(:ci_build_need, build: deploy, name: 'linux:build')
+ end
+
+ it 'makes deploy DAG to be waiting for optional manual to finish' do
+ expect(process_pipeline).to be_truthy
+
+ expect(stages).to eq(%w(skipped created))
+ expect(all_builds.manual).to contain_exactly(linux_build)
+ expect(all_builds.created).to contain_exactly(deploy)
+ end
+ end
+
private
def all_builds
diff --git a/spec/services/ci/pipeline_processing/shared_processing_service_tests_with_yaml.rb b/spec/services/ci/pipeline_processing/shared_processing_service_tests_with_yaml.rb
new file mode 100644
index 00000000000..93f83f0ea3b
--- /dev/null
+++ b/spec/services/ci/pipeline_processing/shared_processing_service_tests_with_yaml.rb
@@ -0,0 +1,57 @@
+# frozen_string_literal: true
+
+shared_context 'Pipeline Processing Service Tests With Yaml' do
+ where(:test_file_path) do
+ Dir.glob(Rails.root.join('spec/services/ci/pipeline_processing/test_cases/*.yml'))
+ end
+
+ with_them do
+ let(:test_file) { YAML.load_file(test_file_path) }
+
+ let(:user) { create(:user) }
+ let(:project) { create(:project, :repository) }
+ let(:pipeline) { Ci::CreatePipelineService.new(project, user, ref: 'master').execute(:pipeline) }
+
+ before do
+ stub_ci_pipeline_yaml_file(YAML.dump(test_file['config']))
+ stub_not_protect_default_branch
+ project.add_developer(user)
+ end
+
+ it 'follows transitions' do
+ expect(pipeline).to be_persisted
+ Sidekiq::Worker.drain_all # ensure that all async jobs are executed
+ check_expectation(test_file.dig('init', 'expect'), "init")
+
+ test_file['transitions'].each_with_index do |transition, idx|
+ event_on_jobs(transition['event'], transition['jobs'])
+ Sidekiq::Worker.drain_all # ensure that all async jobs are executed
+ check_expectation(transition['expect'], "transition:#{idx}")
+ end
+ end
+
+ private
+
+ def check_expectation(expectation, message)
+ expect(current_state.deep_stringify_keys).to eq(expectation), message
+ end
+
+ def current_state
+ # reload pipeline and all relations
+ pipeline.reload
+
+ {
+ pipeline: pipeline.status,
+ stages: pipeline.ordered_stages.pluck(:name, :status).to_h,
+ jobs: pipeline.statuses.latest.pluck(:name, :status).to_h
+ }
+ end
+
+ def event_on_jobs(event, job_names)
+ statuses = pipeline.statuses.latest.by_name(job_names).to_a
+ expect(statuses.count).to eq(job_names.count) # ensure that we have the same counts
+
+ statuses.each { |status| status.public_send("#{event}!") }
+ end
+ end
+end
diff --git a/spec/services/ci/pipeline_processing/test_cases/dag_build_allow_failure_test_on_failure.yml b/spec/services/ci/pipeline_processing/test_cases/dag_build_allow_failure_test_on_failure.yml
new file mode 100644
index 00000000000..cfc456387ff
--- /dev/null
+++ b/spec/services/ci/pipeline_processing/test_cases/dag_build_allow_failure_test_on_failure.yml
@@ -0,0 +1,47 @@
+config:
+ build:
+ stage: build
+ allow_failure: true
+ script: exit 1
+
+ test:
+ stage: test
+ when: on_failure
+ script: exit 0
+ needs: [build]
+
+ deploy:
+ stage: deploy
+ script: exit 0
+ needs: [test]
+
+init:
+ expect:
+ pipeline: pending
+ stages:
+ build: pending
+ test: created
+ deploy: created
+ jobs:
+ build: pending
+ test: created
+ deploy: created
+
+transitions:
+ - event: drop
+ jobs: [build]
+ expect:
+ pipeline: success
+ stages:
+ build: success
+ test: skipped
+ deploy: skipped
+ jobs:
+ build: failed
+ test: skipped
+ deploy: skipped
+
+# TODO: What is the real expected behavior here?
+# Is `needs` keyword a requirement indicator or just a helper to build dependency tree?
+# How should it behave `when: on_failure` with `needs`?
+# Further discussions: https://gitlab.com/gitlab-org/gitlab/-/issues/213080
diff --git a/spec/services/ci/pipeline_processing/test_cases/dag_build_fails.yml b/spec/services/ci/pipeline_processing/test_cases/dag_build_fails.yml
new file mode 100644
index 00000000000..e71ef194c5f
--- /dev/null
+++ b/spec/services/ci/pipeline_processing/test_cases/dag_build_fails.yml
@@ -0,0 +1,39 @@
+config:
+ build:
+ stage: build
+ script: exit 1
+
+ test:
+ stage: test
+ script: exit 0
+ needs: [build]
+
+ deploy:
+ stage: deploy
+ script: exit 0
+
+init:
+ expect:
+ pipeline: pending
+ stages:
+ build: pending
+ test: created
+ deploy: created
+ jobs:
+ build: pending
+ test: created
+ deploy: created
+
+transitions:
+ - event: drop
+ jobs: [build]
+ expect:
+ pipeline: failed
+ stages:
+ build: failed
+ test: skipped
+ deploy: skipped
+ jobs:
+ build: failed
+ test: skipped
+ deploy: skipped
diff --git a/spec/services/ci/pipeline_processing/test_cases/dag_build_fails_deploy_needs_test.yml b/spec/services/ci/pipeline_processing/test_cases/dag_build_fails_deploy_needs_test.yml
new file mode 100644
index 00000000000..40a80f6f53b
--- /dev/null
+++ b/spec/services/ci/pipeline_processing/test_cases/dag_build_fails_deploy_needs_test.yml
@@ -0,0 +1,39 @@
+config:
+ build:
+ stage: build
+ script: exit 1
+
+ test:
+ stage: test
+ script: exit 0
+
+ deploy:
+ stage: deploy
+ script: exit 0
+ needs: [test]
+
+init:
+ expect:
+ pipeline: pending
+ stages:
+ build: pending
+ test: created
+ deploy: created
+ jobs:
+ build: pending
+ test: created
+ deploy: created
+
+transitions:
+ - event: drop
+ jobs: [build]
+ expect:
+ pipeline: failed
+ stages:
+ build: failed
+ test: skipped
+ deploy: skipped
+ jobs:
+ build: failed
+ test: skipped
+ deploy: skipped
diff --git a/spec/services/ci/pipeline_processing/test_cases/dag_build_fails_deploy_needs_test_when_always.yml b/spec/services/ci/pipeline_processing/test_cases/dag_build_fails_deploy_needs_test_when_always.yml
new file mode 100644
index 00000000000..b0904a027f8
--- /dev/null
+++ b/spec/services/ci/pipeline_processing/test_cases/dag_build_fails_deploy_needs_test_when_always.yml
@@ -0,0 +1,43 @@
+config:
+ build:
+ stage: build
+ script: exit 1
+
+ test:
+ stage: test
+ script: exit 0
+
+ deploy:
+ stage: deploy
+ script: exit 0
+ when: always
+ needs: [test]
+
+init:
+ expect:
+ pipeline: pending
+ stages:
+ build: pending
+ test: created
+ deploy: created
+ jobs:
+ build: pending
+ test: created
+ deploy: created
+
+transitions:
+ - event: drop
+ jobs: [build]
+ expect:
+ pipeline: running
+ stages:
+ build: failed
+ test: skipped
+ deploy: pending
+ jobs:
+ build: failed
+ test: skipped
+ deploy: pending
+
+# TODO: `test` is actually skipped, but we run `deploy`. Should we?
+# Further discussions: https://gitlab.com/gitlab-org/gitlab/-/issues/213080
diff --git a/spec/services/ci/pipeline_processing/test_cases/dag_build_fails_other_build_succeeds.yml b/spec/services/ci/pipeline_processing/test_cases/dag_build_fails_other_build_succeeds.yml
new file mode 100644
index 00000000000..a133023b12d
--- /dev/null
+++ b/spec/services/ci/pipeline_processing/test_cases/dag_build_fails_other_build_succeeds.yml
@@ -0,0 +1,62 @@
+config:
+ build_1:
+ stage: build
+ script: exit 0
+
+ build_2:
+ stage: build
+ script: exit 1
+
+ test:
+ stage: test
+ script: exit 0
+
+ deploy:
+ stage: deploy
+ script: exit 0
+ needs: [build_1, test]
+
+init:
+ expect:
+ pipeline: pending
+ stages:
+ build: pending
+ test: created
+ deploy: created
+ jobs:
+ build_1: pending
+ build_2: pending
+ test: created
+ deploy: created
+
+transitions:
+ - event: success
+ jobs: [build_1]
+ expect:
+ pipeline: running
+ stages:
+ build: running
+ test: created
+ deploy: created
+ jobs:
+ build_1: success
+ build_2: pending
+ test: created
+ deploy: created
+
+ - event: drop
+ jobs: [build_2]
+ expect:
+ pipeline: running
+ stages:
+ build: failed
+ test: skipped
+ deploy: pending
+ jobs:
+ build_1: success
+ build_2: failed
+ test: skipped
+ deploy: pending
+
+# TODO: should we run deploy?
+# Further discussions: https://gitlab.com/gitlab-org/gitlab/-/issues/213080
diff --git a/spec/services/ci/pipeline_processing/test_cases/dag_build_fails_other_build_succeeds_deploy_always.yml b/spec/services/ci/pipeline_processing/test_cases/dag_build_fails_other_build_succeeds_deploy_always.yml
new file mode 100644
index 00000000000..4c676761e5c
--- /dev/null
+++ b/spec/services/ci/pipeline_processing/test_cases/dag_build_fails_other_build_succeeds_deploy_always.yml
@@ -0,0 +1,63 @@
+config:
+ build_1:
+ stage: build
+ script: exit 0
+
+ build_2:
+ stage: build
+ script: exit 1
+
+ test:
+ stage: test
+ script: exit 0
+
+ deploy:
+ stage: deploy
+ script: exit 0
+ when: always
+ needs: [build_1, test]
+
+init:
+ expect:
+ pipeline: pending
+ stages:
+ build: pending
+ test: created
+ deploy: created
+ jobs:
+ build_1: pending
+ build_2: pending
+ test: created
+ deploy: created
+
+transitions:
+ - event: success
+ jobs: [build_1]
+ expect:
+ pipeline: running
+ stages:
+ build: running
+ test: created
+ deploy: created
+ jobs:
+ build_1: success
+ build_2: pending
+ test: created
+ deploy: created
+
+ - event: drop
+ jobs: [build_2]
+ expect:
+ pipeline: running
+ stages:
+ build: failed
+ test: skipped
+ deploy: pending
+ jobs:
+ build_1: success
+ build_2: failed
+ test: skipped
+ deploy: pending
+
+# TODO: what's the actual expected behavior here?
+# Further discussions: https://gitlab.com/gitlab-org/gitlab/-/issues/213080
diff --git a/spec/services/ci/pipeline_processing/test_cases/dag_build_fails_test_allow_failure.yml b/spec/services/ci/pipeline_processing/test_cases/dag_build_fails_test_allow_failure.yml
new file mode 100644
index 00000000000..ea7046262c3
--- /dev/null
+++ b/spec/services/ci/pipeline_processing/test_cases/dag_build_fails_test_allow_failure.yml
@@ -0,0 +1,40 @@
+config:
+ build:
+ stage: build
+ script: exit 1
+
+ test:
+ stage: test
+ allow_failure: true
+ script: exit 1
+
+ deploy:
+ stage: deploy
+ script: exit 0
+ needs: [test]
+
+init:
+ expect:
+ pipeline: pending
+ stages:
+ build: pending
+ test: created
+ deploy: created
+ jobs:
+ build: pending
+ test: created
+ deploy: created
+
+transitions:
+ - event: drop
+ jobs: [build]
+ expect:
+ pipeline: failed
+ stages:
+ build: failed
+ test: skipped
+ deploy: skipped
+ jobs:
+ build: failed
+ test: skipped
+ deploy: skipped
diff --git a/spec/services/ci/pipeline_processing/test_cases/dag_build_fails_test_always.yml b/spec/services/ci/pipeline_processing/test_cases/dag_build_fails_test_always.yml
new file mode 100644
index 00000000000..8860f565cc7
--- /dev/null
+++ b/spec/services/ci/pipeline_processing/test_cases/dag_build_fails_test_always.yml
@@ -0,0 +1,35 @@
+config:
+ build:
+ stage: build
+ script: exit 1
+
+ test:
+ stage: test
+ when: always
+ script: exit 0
+ needs: [build]
+
+init:
+ expect:
+ pipeline: pending
+ stages:
+ build: pending
+ test: created
+ jobs:
+ build: pending
+ test: created
+
+transitions:
+ - event: drop
+ jobs: [build]
+ expect:
+ pipeline: running
+ stages:
+ build: failed
+ test: pending
+ jobs:
+ build: failed
+ test: pending
+
+# TODO: Should we run `test`?
+# Further discussions: https://gitlab.com/gitlab-org/gitlab/-/issues/213080
diff --git a/spec/services/ci/pipeline_processing/test_cases/dag_build_fails_test_on_failure.yml b/spec/services/ci/pipeline_processing/test_cases/dag_build_fails_test_on_failure.yml
new file mode 100644
index 00000000000..3fa5a8034a2
--- /dev/null
+++ b/spec/services/ci/pipeline_processing/test_cases/dag_build_fails_test_on_failure.yml
@@ -0,0 +1,35 @@
+config:
+ build:
+ stage: build
+ script: exit 1
+
+ test:
+ stage: test
+ when: on_failure
+ script: exit 0
+ needs: [build]
+
+init:
+ expect:
+ pipeline: pending
+ stages:
+ build: pending
+ test: created
+ jobs:
+ build: pending
+ test: created
+
+transitions:
+ - event: drop
+ jobs: [build]
+ expect:
+ pipeline: running
+ stages:
+ build: failed
+ test: pending
+ jobs:
+ build: failed
+ test: pending
+
+# TODO: Should we run `test`?
+# Further discussions: https://gitlab.com/gitlab-org/gitlab/-/issues/213080
diff --git a/spec/services/ci/pipeline_processing/test_cases/dag_build_succeeds_test_on_failure.yml b/spec/services/ci/pipeline_processing/test_cases/dag_build_succeeds_test_on_failure.yml
new file mode 100644
index 00000000000..700d4440802
--- /dev/null
+++ b/spec/services/ci/pipeline_processing/test_cases/dag_build_succeeds_test_on_failure.yml
@@ -0,0 +1,35 @@
+config:
+ build:
+ stage: build
+ script: exit 0
+
+ test:
+ stage: test
+ when: on_failure
+ script: exit 0
+ needs: [build]
+
+init:
+ expect:
+ pipeline: pending
+ stages:
+ build: pending
+ test: created
+ jobs:
+ build: pending
+ test: created
+
+transitions:
+ - event: success
+ jobs: [build]
+ expect:
+ pipeline: success
+ stages:
+ build: success
+ test: skipped
+ jobs:
+ build: success
+ test: skipped
+
+# TODO: Should we run `test`?
+# Further discussions: https://gitlab.com/gitlab-org/gitlab/-/issues/213080
diff --git a/spec/services/ci/pipeline_processing/test_cases/dag_builds_succeed_test_on_failure.yml b/spec/services/ci/pipeline_processing/test_cases/dag_builds_succeed_test_on_failure.yml
new file mode 100644
index 00000000000..f324525bd56
--- /dev/null
+++ b/spec/services/ci/pipeline_processing/test_cases/dag_builds_succeed_test_on_failure.yml
@@ -0,0 +1,63 @@
+config:
+ build_1:
+ stage: build
+ script: exit 0
+
+ build_2:
+ stage: build
+ script: exit 0
+
+ test:
+ stage: test
+ script: exit 0
+ when: on_failure
+
+ deploy:
+ stage: deploy
+ script: exit 0
+ needs: [build_1, test]
+
+init:
+ expect:
+ pipeline: pending
+ stages:
+ build: pending
+ test: created
+ deploy: created
+ jobs:
+ build_1: pending
+ build_2: pending
+ test: created
+ deploy: created
+
+transitions:
+ - event: success
+ jobs: [build_1, build_2]
+ expect:
+ pipeline: running
+ stages:
+ build: success
+ test: skipped
+ deploy: pending
+ jobs:
+ build_1: success
+ build_2: success
+ test: skipped
+ deploy: pending
+
+ - event: success
+ jobs: [deploy]
+ expect:
+ pipeline: success
+ stages:
+ build: success
+ test: skipped
+ deploy: success
+ jobs:
+ build_1: success
+ build_2: success
+ test: skipped
+ deploy: success
+
+# TODO: should we run deploy?
+# Further discussions: https://gitlab.com/gitlab-org/gitlab/-/issues/213080
diff --git a/spec/services/ci/pipeline_processing/test_cases/dag_builds_succeed_test_on_failure_deploy_always.yml b/spec/services/ci/pipeline_processing/test_cases/dag_builds_succeed_test_on_failure_deploy_always.yml
new file mode 100644
index 00000000000..9986dbaa215
--- /dev/null
+++ b/spec/services/ci/pipeline_processing/test_cases/dag_builds_succeed_test_on_failure_deploy_always.yml
@@ -0,0 +1,64 @@
+config:
+ build_1:
+ stage: build
+ script: exit 0
+
+ build_2:
+ stage: build
+ script: exit 0
+
+ test:
+ stage: test
+ script: exit 0
+ when: on_failure
+
+ deploy:
+ stage: deploy
+ script: exit 0
+ when: always
+ needs: [build_1, test]
+
+init:
+ expect:
+ pipeline: pending
+ stages:
+ build: pending
+ test: created
+ deploy: created
+ jobs:
+ build_1: pending
+ build_2: pending
+ test: created
+ deploy: created
+
+transitions:
+ - event: success
+ jobs: [build_1, build_2]
+ expect:
+ pipeline: running
+ stages:
+ build: success
+ test: skipped
+ deploy: pending
+ jobs:
+ build_1: success
+ build_2: success
+ test: skipped
+ deploy: pending
+
+ - event: success
+ jobs: [deploy]
+ expect:
+ pipeline: success
+ stages:
+ build: success
+ test: skipped
+ deploy: success
+ jobs:
+ build_1: success
+ build_2: success
+ test: skipped
+ deploy: success
+
+# TODO: should we run deploy?
+# Further discussions: https://gitlab.com/gitlab-org/gitlab/-/issues/213080
diff --git a/spec/services/ci/pipeline_processing/test_cases/dag_test_allow_failure_true.yml b/spec/services/ci/pipeline_processing/test_cases/dag_test_allow_failure_true.yml
new file mode 100644
index 00000000000..8d4d9d403f1
--- /dev/null
+++ b/spec/services/ci/pipeline_processing/test_cases/dag_test_allow_failure_true.yml
@@ -0,0 +1,43 @@
+config:
+ test:
+ stage: test
+ allow_failure: true
+ script: exit 1
+
+ deploy:
+ stage: deploy
+ script: exit 0
+ needs: [test]
+
+init:
+ expect:
+ pipeline: pending
+ stages:
+ test: pending
+ deploy: created
+ jobs:
+ test: pending
+ deploy: created
+
+transitions:
+ - event: drop
+ jobs: [test]
+ expect:
+ pipeline: pending
+ stages:
+ test: success
+ deploy: pending
+ jobs:
+ test: failed
+ deploy: pending
+
+ - event: success
+ jobs: [deploy]
+ expect:
+ pipeline: success
+ stages:
+ test: success
+ deploy: success
+ jobs:
+ test: failed
+ deploy: success
diff --git a/spec/services/ci/pipeline_processing/test_cases/dag_test_manual_allow_failure_false.yml b/spec/services/ci/pipeline_processing/test_cases/dag_test_manual_allow_failure_false.yml
new file mode 100644
index 00000000000..1d61cd24f8c
--- /dev/null
+++ b/spec/services/ci/pipeline_processing/test_cases/dag_test_manual_allow_failure_false.yml
@@ -0,0 +1,66 @@
+config:
+ test:
+ stage: test
+ when: manual
+ allow_failure: false
+ script: exit 0
+
+ deploy:
+ stage: deploy
+ script: exit 0
+ needs: [test]
+
+init:
+ expect:
+ pipeline: manual
+ stages:
+ test: manual
+ deploy: created
+ jobs:
+ test: manual
+ deploy: created
+
+transitions:
+ - event: enqueue
+ jobs: [test]
+ expect:
+ pipeline: pending
+ stages:
+ test: pending
+ deploy: created
+ jobs:
+ test: pending
+ deploy: created
+
+ - event: run
+ jobs: [test]
+ expect:
+ pipeline: running
+ stages:
+ test: running
+ deploy: created
+ jobs:
+ test: running
+ deploy: created
+
+ - event: success
+ jobs: [test]
+ expect:
+ pipeline: running
+ stages:
+ test: success
+ deploy: pending
+ jobs:
+ test: success
+ deploy: pending
+
+ - event: success
+ jobs: [deploy]
+ expect:
+ pipeline: success
+ stages:
+ test: success
+ deploy: success
+ jobs:
+ test: success
+ deploy: success
diff --git a/spec/services/ci/pipeline_processing/test_cases/dag_test_manual_allow_failure_true.yml b/spec/services/ci/pipeline_processing/test_cases/dag_test_manual_allow_failure_true.yml
new file mode 100644
index 00000000000..d8ca563b141
--- /dev/null
+++ b/spec/services/ci/pipeline_processing/test_cases/dag_test_manual_allow_failure_true.yml
@@ -0,0 +1,58 @@
+config:
+ test:
+ stage: test
+ when: manual
+ allow_failure: true
+ script: exit 1
+
+ deploy:
+ stage: deploy
+ script: exit 0
+ needs: [test]
+
+init:
+ expect:
+ pipeline: created
+ stages:
+ test: skipped
+ deploy: created
+ jobs:
+ test: manual
+ deploy: created
+
+transitions:
+ - event: enqueue
+ jobs: [test]
+ expect:
+ pipeline: pending
+ stages:
+ test: pending
+ deploy: created
+ jobs:
+ test: pending
+ deploy: created
+
+ - event: run
+ jobs: [test]
+ expect:
+ pipeline: running
+ stages:
+ test: running
+ deploy: created
+ jobs:
+ test: running
+ deploy: created
+
+ - event: drop
+ jobs: [test]
+ expect:
+ pipeline: running
+ stages:
+ test: success
+ deploy: pending
+ jobs:
+ test: failed
+ deploy: pending
+
+# TOOD: should we run deploy?
+# Further discussions: https://gitlab.com/gitlab-org/gitlab/-/issues/213080
diff --git a/spec/services/ci/pipeline_processing/test_cases/dag_test_manual_allow_failure_true_deploy_always.yml b/spec/services/ci/pipeline_processing/test_cases/dag_test_manual_allow_failure_true_deploy_always.yml
new file mode 100644
index 00000000000..ba0a20f49a7
--- /dev/null
+++ b/spec/services/ci/pipeline_processing/test_cases/dag_test_manual_allow_failure_true_deploy_always.yml
@@ -0,0 +1,27 @@
+config:
+ test:
+ stage: test
+ when: manual
+ allow_failure: true
+ script: exit 1
+
+ deploy:
+ stage: deploy
+ when: always
+ script: exit 0
+ needs: [test]
+
+init:
+ expect:
+ pipeline: created
+ stages:
+ test: skipped
+ deploy: created
+ jobs:
+ test: manual
+ deploy: created
+
+transitions: []
+
+# TODO: should we run `deploy`?
+# Further discussions: https://gitlab.com/gitlab-org/gitlab/-/issues/213080
diff --git a/spec/services/ci/pipeline_processing/test_cases/dag_test_manual_allow_failure_true_deploy_on_failure.yml b/spec/services/ci/pipeline_processing/test_cases/dag_test_manual_allow_failure_true_deploy_on_failure.yml
new file mode 100644
index 00000000000..d375c6a49e0
--- /dev/null
+++ b/spec/services/ci/pipeline_processing/test_cases/dag_test_manual_allow_failure_true_deploy_on_failure.yml
@@ -0,0 +1,48 @@
+config:
+ test:
+ stage: test
+ when: manual
+ allow_failure: true
+ script: exit 1
+
+ deploy:
+ stage: deploy
+ when: on_failure
+ script: exit 0
+ needs: [test]
+
+init:
+ expect:
+ pipeline: created
+ stages:
+ test: skipped
+ deploy: created
+ jobs:
+ test: manual
+ deploy: created
+
+transitions:
+ - event: enqueue
+ jobs: [test]
+ expect:
+ pipeline: pending
+ stages:
+ test: pending
+ deploy: created
+ jobs:
+ test: pending
+ deploy: created
+
+ - event: drop
+ jobs: [test]
+ expect:
+ pipeline: success
+ stages:
+ test: success
+ deploy: skipped
+ jobs:
+ test: failed
+ deploy: skipped
+
+# TODO: should we run `deploy`?
+# Further discussions: https://gitlab.com/gitlab-org/gitlab/-/issues/213080
diff --git a/spec/services/ci/pipeline_processing/test_cases/dag_test_manual_allow_failure_true_other_test_succeeds.yml b/spec/services/ci/pipeline_processing/test_cases/dag_test_manual_allow_failure_true_other_test_succeeds.yml
new file mode 100644
index 00000000000..34073b92ccc
--- /dev/null
+++ b/spec/services/ci/pipeline_processing/test_cases/dag_test_manual_allow_failure_true_other_test_succeeds.yml
@@ -0,0 +1,42 @@
+config:
+ test1:
+ stage: test
+ script: exit 0
+
+ test2:
+ stage: test
+ when: manual
+ allow_failure: true
+ script: exit 1
+
+ deploy:
+ stage: deploy
+ script: exit 0
+ needs: [test1, test2]
+
+init:
+ expect:
+ pipeline: pending
+ stages:
+ test: pending
+ deploy: created
+ jobs:
+ test1: pending
+ test2: manual
+ deploy: created
+
+transitions:
+ - event: success
+ jobs: [test1]
+ expect:
+ pipeline: running
+ stages:
+ test: success
+ deploy: created
+ jobs:
+ test1: success
+ test2: manual
+ deploy: created
+
+# TODO: should deploy run?
+# Further discussions: https://gitlab.com/gitlab-org/gitlab/-/issues/213080
diff --git a/spec/services/ci/pipeline_processing/test_cases/dag_test_on_failure_with_failure.yml b/spec/services/ci/pipeline_processing/test_cases/dag_test_on_failure_with_failure.yml
new file mode 100644
index 00000000000..5ace621e89c
--- /dev/null
+++ b/spec/services/ci/pipeline_processing/test_cases/dag_test_on_failure_with_failure.yml
@@ -0,0 +1,66 @@
+config:
+ build:
+ stage: build
+ script: exit 1
+
+ test:
+ stage: test
+ when: on_failure
+ script: exit 0
+
+ deploy:
+ stage: deploy
+ script: exit 0
+ needs: [test]
+
+init:
+ expect:
+ pipeline: pending
+ stages:
+ build: pending
+ test: created
+ deploy: created
+ jobs:
+ build: pending
+ test: created
+ deploy: created
+
+transitions:
+ - event: drop
+ jobs: [build]
+ expect:
+ pipeline: running
+ stages:
+ build: failed
+ test: pending
+ deploy: created
+ jobs:
+ build: failed
+ test: pending
+ deploy: created
+
+ - event: success
+ jobs: [test]
+ expect:
+ pipeline: running
+ stages:
+ build: failed
+ test: success
+ deploy: pending
+ jobs:
+ build: failed
+ test: success
+ deploy: pending
+
+ - event: success
+ jobs: [deploy]
+ expect:
+ pipeline: failed
+ stages:
+ build: failed
+ test: success
+ deploy: success
+ jobs:
+ build: failed
+ test: success
+ deploy: success
diff --git a/spec/services/ci/pipeline_processing/test_cases/dag_test_on_failure_with_success.yml b/spec/services/ci/pipeline_processing/test_cases/dag_test_on_failure_with_success.yml
new file mode 100644
index 00000000000..19524cfd3e4
--- /dev/null
+++ b/spec/services/ci/pipeline_processing/test_cases/dag_test_on_failure_with_success.yml
@@ -0,0 +1,40 @@
+config:
+ build:
+ stage: build
+ script: exit 0
+
+ test:
+ stage: test
+ when: on_failure
+ script: exit 0
+
+ deploy:
+ stage: deploy
+ script: exit 0
+ needs: [test]
+
+init:
+ expect:
+ pipeline: pending
+ stages:
+ build: pending
+ test: created
+ deploy: created
+ jobs:
+ build: pending
+ test: created
+ deploy: created
+
+transitions:
+ - event: success
+ jobs: [build]
+ expect:
+ pipeline: success
+ stages:
+ build: success
+ test: skipped
+ deploy: skipped
+ jobs:
+ build: success
+ test: skipped
+ deploy: skipped
diff --git a/spec/services/ci/pipeline_processing/test_cases/stage_build_allow_failure_test_on_failure.yml b/spec/services/ci/pipeline_processing/test_cases/stage_build_allow_failure_test_on_failure.yml
new file mode 100644
index 00000000000..3e081d4411b
--- /dev/null
+++ b/spec/services/ci/pipeline_processing/test_cases/stage_build_allow_failure_test_on_failure.yml
@@ -0,0 +1,53 @@
+config:
+ build:
+ stage: build
+ allow_failure: true
+ script: exit 1
+
+ test:
+ stage: test
+ when: on_failure
+ script: exit 0
+
+ deploy:
+ stage: deploy
+ script: exit 0
+
+init:
+ expect:
+ pipeline: pending
+ stages:
+ build: pending
+ test: created
+ deploy: created
+ jobs:
+ build: pending
+ test: created
+ deploy: created
+
+transitions:
+ - event: drop
+ jobs: [build]
+ expect:
+ pipeline: pending
+ stages:
+ build: success
+ test: skipped
+ deploy: pending
+ jobs:
+ build: failed
+ test: skipped
+ deploy: pending
+
+ - event: success
+ jobs: [deploy]
+ expect:
+ pipeline: success
+ stages:
+ build: success
+ test: skipped
+ deploy: success
+ jobs:
+ build: failed
+ test: skipped
+ deploy: success
diff --git a/spec/services/ci/pipeline_processing/test_cases/stage_build_fails.yml b/spec/services/ci/pipeline_processing/test_cases/stage_build_fails.yml
new file mode 100644
index 00000000000..0618abf3524
--- /dev/null
+++ b/spec/services/ci/pipeline_processing/test_cases/stage_build_fails.yml
@@ -0,0 +1,38 @@
+config:
+ build:
+ stage: build
+ script: exit 1
+
+ test:
+ stage: test
+ script: exit 0
+
+ deploy:
+ stage: deploy
+ script: exit 0
+
+init:
+ expect:
+ pipeline: pending
+ stages:
+ build: pending
+ test: created
+ deploy: created
+ jobs:
+ build: pending
+ test: created
+ deploy: created
+
+transitions:
+ - event: drop
+ jobs: [build]
+ expect:
+ pipeline: failed
+ stages:
+ build: failed
+ test: skipped
+ deploy: skipped
+ jobs:
+ build: failed
+ test: skipped
+ deploy: skipped
diff --git a/spec/services/ci/pipeline_processing/test_cases/stage_build_fails_test_allow_failure.yml b/spec/services/ci/pipeline_processing/test_cases/stage_build_fails_test_allow_failure.yml
new file mode 100644
index 00000000000..362ac6e4239
--- /dev/null
+++ b/spec/services/ci/pipeline_processing/test_cases/stage_build_fails_test_allow_failure.yml
@@ -0,0 +1,39 @@
+config:
+ build:
+ stage: build
+ script: exit 1
+
+ test:
+ stage: test
+ allow_failure: true
+ script: exit 1
+
+ deploy:
+ stage: deploy
+ script: exit 0
+
+init:
+ expect:
+ pipeline: pending
+ stages:
+ build: pending
+ test: created
+ deploy: created
+ jobs:
+ build: pending
+ test: created
+ deploy: created
+
+transitions:
+ - event: drop
+ jobs: [build]
+ expect:
+ pipeline: failed
+ stages:
+ build: failed
+ test: skipped
+ deploy: skipped
+ jobs:
+ build: failed
+ test: skipped
+ deploy: skipped
diff --git a/spec/services/ci/pipeline_processing/test_cases/stage_test_manual_allow_failure_false.yml b/spec/services/ci/pipeline_processing/test_cases/stage_test_manual_allow_failure_false.yml
new file mode 100644
index 00000000000..2ffa35b56d7
--- /dev/null
+++ b/spec/services/ci/pipeline_processing/test_cases/stage_test_manual_allow_failure_false.yml
@@ -0,0 +1,65 @@
+config:
+ test:
+ stage: test
+ when: manual
+ allow_failure: false
+ script: exit 0
+
+ deploy:
+ stage: deploy
+ script: exit 0
+
+init:
+ expect:
+ pipeline: manual
+ stages:
+ test: manual
+ deploy: created
+ jobs:
+ test: manual
+ deploy: created
+
+transitions:
+ - event: enqueue
+ jobs: [test]
+ expect:
+ pipeline: pending
+ stages:
+ test: pending
+ deploy: created
+ jobs:
+ test: pending
+ deploy: created
+
+ - event: run
+ jobs: [test]
+ expect:
+ pipeline: running
+ stages:
+ test: running
+ deploy: created
+ jobs:
+ test: running
+ deploy: created
+
+ - event: success
+ jobs: [test]
+ expect:
+ pipeline: running
+ stages:
+ test: success
+ deploy: pending
+ jobs:
+ test: success
+ deploy: pending
+
+ - event: success
+ jobs: [deploy]
+ expect:
+ pipeline: success
+ stages:
+ test: success
+ deploy: success
+ jobs:
+ test: success
+ deploy: success
diff --git a/spec/services/ci/pipeline_processing/test_cases/stage_test_manual_allow_failure_true.yml b/spec/services/ci/pipeline_processing/test_cases/stage_test_manual_allow_failure_true.yml
new file mode 100644
index 00000000000..088fab5ca09
--- /dev/null
+++ b/spec/services/ci/pipeline_processing/test_cases/stage_test_manual_allow_failure_true.yml
@@ -0,0 +1,54 @@
+config:
+ test:
+ stage: test
+ when: manual
+ allow_failure: true
+ script: exit 1
+
+ deploy:
+ stage: deploy
+ script: exit 0
+
+init:
+ expect:
+ pipeline: pending
+ stages:
+ test: skipped
+ deploy: pending
+ jobs:
+ test: manual
+ deploy: pending
+
+transitions:
+ - event: success
+ jobs: [deploy]
+ expect:
+ pipeline: success
+ stages:
+ test: skipped
+ deploy: success
+ jobs:
+ test: manual
+ deploy: success
+
+ - event: enqueue
+ jobs: [test]
+ expect:
+ pipeline: running
+ stages:
+ test: pending
+ deploy: success
+ jobs:
+ test: pending
+ deploy: success
+
+ - event: drop
+ jobs: [test]
+ expect:
+ pipeline: success
+ stages:
+ test: success
+ deploy: success
+ jobs:
+ test: failed
+ deploy: success
diff --git a/spec/services/ci/pipeline_processing/test_cases/stage_test_manual_allow_failure_true_deploy_on_failure.yml b/spec/services/ci/pipeline_processing/test_cases/stage_test_manual_allow_failure_true_deploy_on_failure.yml
new file mode 100644
index 00000000000..2b30316aef6
--- /dev/null
+++ b/spec/services/ci/pipeline_processing/test_cases/stage_test_manual_allow_failure_true_deploy_on_failure.yml
@@ -0,0 +1,44 @@
+config:
+ test:
+ stage: test
+ when: manual
+ allow_failure: true
+ script: exit 1
+
+ deploy:
+ stage: deploy
+ when: on_failure
+ script: exit 0
+
+init:
+ expect:
+ pipeline: skipped
+ stages:
+ test: skipped
+ deploy: skipped
+ jobs:
+ test: manual
+ deploy: skipped
+
+transitions:
+ - event: enqueue
+ jobs: [test]
+ expect:
+ pipeline: pending
+ stages:
+ test: pending
+ deploy: skipped
+ jobs:
+ test: pending
+ deploy: skipped
+
+ - event: drop
+ jobs: [test]
+ expect:
+ pipeline: success
+ stages:
+ test: success
+ deploy: skipped
+ jobs:
+ test: failed
+ deploy: skipped
diff --git a/spec/services/ci/pipeline_processing/test_cases/stage_test_on_failure_with_failure.yml b/spec/services/ci/pipeline_processing/test_cases/stage_test_on_failure_with_failure.yml
new file mode 100644
index 00000000000..1751cbb2023
--- /dev/null
+++ b/spec/services/ci/pipeline_processing/test_cases/stage_test_on_failure_with_failure.yml
@@ -0,0 +1,52 @@
+config:
+ build:
+ stage: build
+ script: exit 1
+
+ test:
+ stage: test
+ when: on_failure
+ script: exit 0
+
+ deploy:
+ stage: deploy
+ script: exit 0
+
+init:
+ expect:
+ pipeline: pending
+ stages:
+ build: pending
+ test: created
+ deploy: created
+ jobs:
+ build: pending
+ test: created
+ deploy: created
+
+transitions:
+ - event: drop
+ jobs: [build]
+ expect:
+ pipeline: running
+ stages:
+ build: failed
+ test: pending
+ deploy: created
+ jobs:
+ build: failed
+ test: pending
+ deploy: created
+
+ - event: success
+ jobs: [test]
+ expect:
+ pipeline: failed
+ stages:
+ build: failed
+ test: success
+ deploy: skipped
+ jobs:
+ build: failed
+ test: success
+ deploy: skipped
diff --git a/spec/services/ci/pipeline_processing/test_cases/stage_test_on_failure_with_success.yml b/spec/services/ci/pipeline_processing/test_cases/stage_test_on_failure_with_success.yml
new file mode 100644
index 00000000000..15afe1ce8e1
--- /dev/null
+++ b/spec/services/ci/pipeline_processing/test_cases/stage_test_on_failure_with_success.yml
@@ -0,0 +1,52 @@
+config:
+ build:
+ stage: build
+ script: exit 0
+
+ test:
+ stage: test
+ when: on_failure
+ script: exit 0
+
+ deploy:
+ stage: deploy
+ script: exit 0
+
+init:
+ expect:
+ pipeline: pending
+ stages:
+ build: pending
+ test: created
+ deploy: created
+ jobs:
+ build: pending
+ test: created
+ deploy: created
+
+transitions:
+ - event: success
+ jobs: [build]
+ expect:
+ pipeline: running
+ stages:
+ build: success
+ test: skipped
+ deploy: pending
+ jobs:
+ build: success
+ test: skipped
+ deploy: pending
+
+ - event: success
+ jobs: [deploy]
+ expect:
+ pipeline: success
+ stages:
+ build: success
+ test: skipped
+ deploy: success
+ jobs:
+ build: success
+ test: skipped
+ deploy: success
diff --git a/spec/services/ci/pipeline_schedule_service_spec.rb b/spec/services/ci/pipeline_schedule_service_spec.rb
index f7590720f66..867ed0acc0d 100644
--- a/spec/services/ci/pipeline_schedule_service_spec.rb
+++ b/spec/services/ci/pipeline_schedule_service_spec.rb
@@ -25,38 +25,6 @@ describe Ci::PipelineScheduleService do
subject
end
- context 'when ci_pipeline_schedule_async feature flag is disabled' do
- before do
- stub_feature_flags(ci_pipeline_schedule_async: false)
- end
-
- it 'runs RunPipelineScheduleWorker synchronously' do
- expect_next_instance_of(RunPipelineScheduleWorker) do |worker|
- expect(worker).to receive(:perform).with(schedule.id, schedule.owner.id)
- end
-
- subject
- end
-
- it 'calls Garbage Collection manually' do
- expect(GC).to receive(:start)
-
- subject
- end
-
- context 'when ci_pipeline_schedule_force_gc feature flag is disabled' do
- before do
- stub_feature_flags(ci_pipeline_schedule_force_gc: false)
- end
-
- it 'does not call Garbage Collection manually' do
- expect(GC).not_to receive(:start)
-
- subject
- end
- end
- end
-
context 'when owner is nil' do
let(:schedule) { create(:ci_pipeline_schedule, project: project, owner: nil) }
diff --git a/spec/services/ci/process_pipeline_service_spec.rb b/spec/services/ci/process_pipeline_service_spec.rb
index 6f5a070d73d..40ae1c4029b 100644
--- a/spec/services/ci/process_pipeline_service_spec.rb
+++ b/spec/services/ci/process_pipeline_service_spec.rb
@@ -33,25 +33,6 @@ describe Ci::ProcessPipelineService do
end
end
- context 'with a pipeline which has processables with nil scheduling_type', :clean_gitlab_redis_shared_state do
- let!(:build1) { create_build('build1') }
- let!(:build2) { create_build('build2') }
- let!(:build3) { create_build('build3', scheduling_type: :dag) }
- let!(:build3_on_build2) { create(:ci_build_need, build: build3, name: 'build2') }
-
- before do
- pipeline.processables.update_all(scheduling_type: nil)
- end
-
- it 'populates scheduling_type before processing' do
- process_pipeline
-
- expect(build1.scheduling_type).to eq('stage')
- expect(build2.scheduling_type).to eq('stage')
- expect(build3.scheduling_type).to eq('dag')
- end
- end
-
def process_pipeline
described_class.new(pipeline).execute
end
diff --git a/spec/services/ci/register_job_service_spec.rb b/spec/services/ci/register_job_service_spec.rb
index 2da1350e2af..c0f854df9b7 100644
--- a/spec/services/ci/register_job_service_spec.rb
+++ b/spec/services/ci/register_job_service_spec.rb
@@ -571,7 +571,7 @@ module Ci
end
describe '#register_success' do
- let!(:current_time) { Time.new(2018, 4, 5, 14, 0, 0) }
+ let!(:current_time) { Time.zone.local(2018, 4, 5, 14, 0, 0) }
let!(:attempt_counter) { double('Gitlab::Metrics::NullMetric') }
let!(:job_queue_duration_seconds) { double('Gitlab::Metrics::NullMetric') }
diff --git a/spec/services/ci/retry_build_service_spec.rb b/spec/services/ci/retry_build_service_spec.rb
index 86b68dc3ade..0aa603b24ae 100644
--- a/spec/services/ci/retry_build_service_spec.rb
+++ b/spec/services/ci/retry_build_service_spec.rb
@@ -22,9 +22,9 @@ describe Ci::RetryBuildService do
described_class.new(project, user)
end
- CLONE_ACCESSORS = described_class::CLONE_ACCESSORS
+ clone_accessors = described_class::CLONE_ACCESSORS
- REJECT_ACCESSORS =
+ reject_accessors =
%i[id status user token token_encrypted coverage trace runner
artifacts_expire_at
created_at updated_at started_at finished_at queued_at erased_by
@@ -34,13 +34,13 @@ describe Ci::RetryBuildService do
job_artifacts_container_scanning job_artifacts_dast
job_artifacts_license_management job_artifacts_license_scanning
job_artifacts_performance job_artifacts_lsif
- job_artifacts_terraform
+ job_artifacts_terraform job_artifacts_cluster_applications
job_artifacts_codequality job_artifacts_metrics scheduled_at
job_variables waiting_for_resource_at job_artifacts_metrics_referee
job_artifacts_network_referee job_artifacts_dotenv
- job_artifacts_cobertura needs].freeze
+ job_artifacts_cobertura needs job_artifacts_accessibility].freeze
- IGNORE_ACCESSORS =
+ ignore_accessors =
%i[type lock_version target_url base_tags trace_sections
commit_id deployment erased_by_id project_id
runner_id tag_taggings taggings tags trigger_request_id
@@ -63,6 +63,9 @@ describe Ci::RetryBuildService do
end
before do
+ # Test correctly behaviour of deprecated artifact because it can be still in use
+ stub_feature_flags(drop_license_management_artifact: false)
+
# Make sure that build has both `stage_id` and `stage` because FactoryBot
# can reset one of the fields when assigning another. We plan to deprecate
# and remove legacy `stage` column in the future.
@@ -88,7 +91,7 @@ describe Ci::RetryBuildService do
end
end
- CLONE_ACCESSORS.each do |attribute|
+ clone_accessors.each do |attribute|
it "clones #{attribute} build attribute" do
expect(attribute).not_to be_in(forbidden_associations), "association #{attribute} must be `belongs_to`"
expect(build.send(attribute)).not_to be_nil
@@ -118,7 +121,7 @@ describe Ci::RetryBuildService do
end
describe 'reject accessors' do
- REJECT_ACCESSORS.each do |attribute|
+ reject_accessors.each do |attribute|
it "does not clone #{attribute} build attribute" do
expect(new_build.send(attribute)).not_to eq build.send(attribute)
end
@@ -126,8 +129,8 @@ describe Ci::RetryBuildService do
end
it 'has correct number of known attributes' do
- processed_accessors = CLONE_ACCESSORS + REJECT_ACCESSORS
- known_accessors = processed_accessors + IGNORE_ACCESSORS
+ processed_accessors = clone_accessors + reject_accessors
+ known_accessors = processed_accessors + ignore_accessors
# :tag_list is a special case, this accessor does not exist
# in reflected associations, comes from `act_as_taggable` and
@@ -190,6 +193,35 @@ describe Ci::RetryBuildService do
expect(subsequent_build.reload).to be_created
end
end
+
+ context 'when pipeline has other builds' do
+ let!(:stage2) { create(:ci_stage_entity, project: project, pipeline: pipeline, name: 'deploy') }
+ let!(:build2) { create(:ci_build, pipeline: pipeline, stage_id: stage.id ) }
+ let!(:deploy) { create(:ci_build, pipeline: pipeline, stage_id: stage2.id) }
+ let!(:deploy_needs_build2) { create(:ci_build_need, build: deploy, name: build2.name) }
+
+ context 'when build has nil scheduling_type' do
+ before do
+ build.pipeline.processables.update_all(scheduling_type: nil)
+ build.reload
+ end
+
+ it 'populates scheduling_type of processables' do
+ expect(new_build.scheduling_type).to eq('stage')
+ expect(build.reload.scheduling_type).to eq('stage')
+ expect(build2.reload.scheduling_type).to eq('stage')
+ expect(deploy.reload.scheduling_type).to eq('dag')
+ end
+ end
+
+ context 'when build has scheduling_type' do
+ it 'does not call populate_scheduling_type!' do
+ expect_any_instance_of(Ci::Pipeline).not_to receive(:ensure_scheduling_type!)
+
+ expect(new_build.scheduling_type).to eq('stage')
+ end
+ end
+ end
end
context 'when user does not have ability to execute build' do
diff --git a/spec/services/ci/retry_pipeline_service_spec.rb b/spec/services/ci/retry_pipeline_service_spec.rb
index 81a0b05f2c7..8e85e68d4fc 100644
--- a/spec/services/ci/retry_pipeline_service_spec.rb
+++ b/spec/services/ci/retry_pipeline_service_spec.rb
@@ -261,6 +261,25 @@ describe Ci::RetryPipelineService, '#execute' do
service.execute(pipeline)
end
+
+ context 'when pipeline has processables with nil scheduling_type' do
+ let!(:build1) { create_build('build1', :success, 0) }
+ let!(:build2) { create_build('build2', :failed, 0) }
+ let!(:build3) { create_build('build3', :failed, 1) }
+ let!(:build3_needs_build1) { create(:ci_build_need, build: build3, name: build1.name) }
+
+ before do
+ statuses.update_all(scheduling_type: nil)
+ end
+
+ it 'populates scheduling_type of processables' do
+ service.execute(pipeline)
+
+ expect(build1.reload.scheduling_type).to eq('stage')
+ expect(build2.reload.scheduling_type).to eq('stage')
+ expect(build3.reload.scheduling_type).to eq('dag')
+ end
+ end
end
context 'when user is not allowed to retry pipeline' do
diff --git a/spec/services/ci/update_instance_variables_service_spec.rb b/spec/services/ci/update_instance_variables_service_spec.rb
new file mode 100644
index 00000000000..93f6e5d3ea8
--- /dev/null
+++ b/spec/services/ci/update_instance_variables_service_spec.rb
@@ -0,0 +1,230 @@
+# frozen_string_literal: true
+
+require 'spec_helper'
+
+describe Ci::UpdateInstanceVariablesService do
+ let(:params) { { variables_attributes: variables_attributes } }
+
+ subject { described_class.new(params) }
+
+ describe '#execute' do
+ context 'without variables' do
+ let(:variables_attributes) { [] }
+
+ it { expect(subject.execute).to be_truthy }
+ end
+
+ context 'with insert only variables' do
+ let(:variables_attributes) do
+ [
+ { key: 'var_a', secret_value: 'dummy_value_for_a', protected: true },
+ { key: 'var_b', secret_value: 'dummy_value_for_b', protected: false }
+ ]
+ end
+
+ it { expect(subject.execute).to be_truthy }
+
+ it 'persists all the records' do
+ expect { subject.execute }
+ .to change { Ci::InstanceVariable.count }
+ .by variables_attributes.size
+ end
+
+ it 'persists attributes' do
+ subject.execute
+
+ expect(Ci::InstanceVariable.all).to contain_exactly(
+ have_attributes(key: 'var_a', secret_value: 'dummy_value_for_a', protected: true),
+ have_attributes(key: 'var_b', secret_value: 'dummy_value_for_b', protected: false)
+ )
+ end
+ end
+
+ context 'with update only variables' do
+ let!(:var_a) { create(:ci_instance_variable) }
+ let!(:var_b) { create(:ci_instance_variable, protected: false) }
+
+ let(:variables_attributes) do
+ [
+ {
+ id: var_a.id,
+ key: var_a.key,
+ secret_value: 'new_dummy_value_for_a',
+ protected: var_a.protected?.to_s
+ },
+ {
+ id: var_b.id,
+ key: 'var_b_key',
+ secret_value: 'new_dummy_value_for_b',
+ protected: 'true'
+ }
+ ]
+ end
+
+ it { expect(subject.execute).to be_truthy }
+
+ it 'does not change the count' do
+ expect { subject.execute }
+ .not_to change { Ci::InstanceVariable.count }
+ end
+
+ it 'updates the records in place', :aggregate_failures do
+ subject.execute
+
+ expect(var_a.reload).to have_attributes(secret_value: 'new_dummy_value_for_a')
+
+ expect(var_b.reload).to have_attributes(
+ key: 'var_b_key', secret_value: 'new_dummy_value_for_b', protected: true)
+ end
+ end
+
+ context 'with insert and update variables' do
+ let!(:var_a) { create(:ci_instance_variable) }
+
+ let(:variables_attributes) do
+ [
+ {
+ id: var_a.id,
+ key: var_a.key,
+ secret_value: 'new_dummy_value_for_a',
+ protected: var_a.protected?.to_s
+ },
+ {
+ key: 'var_b',
+ secret_value: 'dummy_value_for_b',
+ protected: true
+ }
+ ]
+ end
+
+ it { expect(subject.execute).to be_truthy }
+
+ it 'inserts only one record' do
+ expect { subject.execute }
+ .to change { Ci::InstanceVariable.count }.by 1
+ end
+
+ it 'persists all the records', :aggregate_failures do
+ subject.execute
+ var_b = Ci::InstanceVariable.find_by(key: 'var_b')
+
+ expect(var_a.reload.secret_value).to eq('new_dummy_value_for_a')
+ expect(var_b.secret_value).to eq('dummy_value_for_b')
+ end
+ end
+
+ context 'with insert, update, and destroy variables' do
+ let!(:var_a) { create(:ci_instance_variable) }
+ let!(:var_b) { create(:ci_instance_variable) }
+
+ let(:variables_attributes) do
+ [
+ {
+ id: var_a.id,
+ key: var_a.key,
+ secret_value: 'new_dummy_value_for_a',
+ protected: var_a.protected?.to_s
+ },
+ {
+ id: var_b.id,
+ key: var_b.key,
+ secret_value: 'dummy_value_for_b',
+ protected: var_b.protected?.to_s,
+ '_destroy' => 'true'
+ },
+ {
+ key: 'var_c',
+ secret_value: 'dummy_value_for_c',
+ protected: true
+ }
+ ]
+ end
+
+ it { expect(subject.execute).to be_truthy }
+
+ it 'persists all the records', :aggregate_failures do
+ subject.execute
+ var_c = Ci::InstanceVariable.find_by(key: 'var_c')
+
+ expect(var_a.reload.secret_value).to eq('new_dummy_value_for_a')
+ expect { var_b.reload }.to raise_error(ActiveRecord::RecordNotFound)
+ expect(var_c.secret_value).to eq('dummy_value_for_c')
+ end
+ end
+
+ context 'with invalid variables' do
+ let!(:var_a) { create(:ci_instance_variable, secret_value: 'dummy_value_for_a') }
+
+ let(:variables_attributes) do
+ [
+ {
+ key: '...?',
+ secret_value: 'nice_value'
+ },
+ {
+ id: var_a.id,
+ key: var_a.key,
+ secret_value: 'new_dummy_value_for_a',
+ protected: var_a.protected?.to_s
+ },
+ {
+ key: var_a.key,
+ secret_value: 'other_value'
+ }
+ ]
+ end
+
+ it { expect(subject.execute).to be_falsey }
+
+ it 'does not insert any records' do
+ expect { subject.execute }
+ .not_to change { Ci::InstanceVariable.count }
+ end
+
+ it 'does not update existing records' do
+ subject.execute
+
+ expect(var_a.reload.secret_value).to eq('dummy_value_for_a')
+ end
+
+ it 'returns errors' do
+ subject.execute
+
+ expect(subject.errors).to match_array(
+ [
+ "Key (#{var_a.key}) has already been taken",
+ "Key can contain only letters, digits and '_'."
+ ])
+ end
+ end
+
+ context 'when deleting non existing variables' do
+ let(:variables_attributes) do
+ [
+ {
+ id: 'some-id',
+ key: 'some_key',
+ secret_value: 'other_value',
+ '_destroy' => 'true'
+ }
+ ]
+ end
+
+ it { expect { subject.execute }.to raise_error(ActiveRecord::RecordNotFound) }
+ end
+
+ context 'when updating non existing variables' do
+ let(:variables_attributes) do
+ [
+ {
+ id: 'some-id',
+ key: 'some_key',
+ secret_value: 'other_value'
+ }
+ ]
+ end
+
+ it { expect { subject.execute }.to raise_error(ActiveRecord::RecordNotFound) }
+ end
+ end
+end
diff --git a/spec/services/clusters/applications/check_upgrade_progress_service_spec.rb b/spec/services/clusters/applications/check_upgrade_progress_service_spec.rb
index c08b618fe6a..29ee897454a 100644
--- a/spec/services/clusters/applications/check_upgrade_progress_service_spec.rb
+++ b/spec/services/clusters/applications/check_upgrade_progress_service_spec.rb
@@ -3,7 +3,7 @@
require 'spec_helper'
describe Clusters::Applications::CheckUpgradeProgressService do
- RESCHEDULE_PHASES = ::Gitlab::Kubernetes::Pod::PHASES -
+ reschedule_phashes = ::Gitlab::Kubernetes::Pod::PHASES -
[::Gitlab::Kubernetes::Pod::SUCCEEDED, ::Gitlab::Kubernetes::Pod::FAILED, ::Gitlab].freeze
let(:application) { create(:clusters_applications_prometheus, :updating) }
@@ -89,6 +89,6 @@ describe Clusters::Applications::CheckUpgradeProgressService do
end
end
- RESCHEDULE_PHASES.each { |phase| it_behaves_like 'a not yet terminated upgrade', phase }
+ reschedule_phashes.each { |phase| it_behaves_like 'a not yet terminated upgrade', phase }
end
end
diff --git a/spec/services/clusters/applications/ingress_modsecurity_usage_service_spec.rb b/spec/services/clusters/applications/ingress_modsecurity_usage_service_spec.rb
deleted file mode 100644
index d456284f76a..00000000000
--- a/spec/services/clusters/applications/ingress_modsecurity_usage_service_spec.rb
+++ /dev/null
@@ -1,196 +0,0 @@
-# frozen_string_literal: true
-
-require 'spec_helper'
-
-describe Clusters::Applications::IngressModsecurityUsageService do
- describe '#execute' do
- ADO_MODSEC_KEY = Clusters::Applications::IngressModsecurityUsageService::ADO_MODSEC_KEY
-
- let(:project_with_ci_var) { create(:environment).project }
- let(:project_with_pipeline_var) { create(:environment).project }
-
- subject { described_class.new.execute }
-
- context 'with multiple projects' do
- let(:pipeline1) { create(:ci_pipeline, :with_job, project: project_with_pipeline_var) }
- let(:pipeline2) { create(:ci_pipeline, :with_job, project: project_with_ci_var) }
-
- let!(:deployment_with_pipeline_var) do
- create(
- :deployment,
- :success,
- environment: project_with_pipeline_var.environments.first,
- project: project_with_pipeline_var,
- deployable: pipeline1.builds.last
- )
- end
- let!(:deployment_with_project_var) do
- create(
- :deployment,
- :success,
- environment: project_with_ci_var.environments.first,
- project: project_with_ci_var,
- deployable: pipeline2.builds.last
- )
- end
-
- context 'mixed data' do
- let!(:ci_variable) { create(:ci_variable, project: project_with_ci_var, key: ADO_MODSEC_KEY, value: "On") }
- let!(:pipeline_variable) { create(:ci_pipeline_variable, pipeline: pipeline1, key: ADO_MODSEC_KEY, value: "Off") }
-
- it 'gathers variable data' do
- expect(subject[:ingress_modsecurity_blocking]).to eq(1)
- expect(subject[:ingress_modsecurity_disabled]).to eq(1)
- end
- end
-
- context 'blocking' do
- let(:modsec_values) { { key: ADO_MODSEC_KEY, value: "On" } }
-
- let!(:ci_variable) { create(:ci_variable, project: project_with_ci_var, **modsec_values) }
- let!(:pipeline_variable) { create(:ci_pipeline_variable, pipeline: pipeline1, **modsec_values) }
-
- it 'gathers variable data' do
- expect(subject[:ingress_modsecurity_blocking]).to eq(2)
- expect(subject[:ingress_modsecurity_disabled]).to eq(0)
- end
- end
-
- context 'disabled' do
- let(:modsec_values) { { key: ADO_MODSEC_KEY, value: "Off" } }
-
- let!(:ci_variable) { create(:ci_variable, project: project_with_ci_var, **modsec_values) }
- let!(:pipeline_variable) { create(:ci_pipeline_variable, pipeline: pipeline1, **modsec_values) }
-
- it 'gathers variable data' do
- expect(subject[:ingress_modsecurity_blocking]).to eq(0)
- expect(subject[:ingress_modsecurity_disabled]).to eq(2)
- end
- end
- end
-
- context 'when set as both ci and pipeline variables' do
- let(:modsec_values) { { key: ADO_MODSEC_KEY, value: "Off" } }
-
- let(:pipeline) { create(:ci_pipeline, :with_job, project: project_with_ci_var) }
- let!(:deployment) do
- create(
- :deployment,
- :success,
- environment: project_with_ci_var.environments.first,
- project: project_with_ci_var,
- deployable: pipeline.builds.last
- )
- end
-
- let!(:ci_variable) { create(:ci_variable, project: project_with_ci_var, **modsec_values) }
- let!(:pipeline_variable) { create(:ci_pipeline_variable, pipeline: pipeline, **modsec_values) }
-
- it 'wont double-count projects' do
- expect(subject[:ingress_modsecurity_blocking]).to eq(0)
- expect(subject[:ingress_modsecurity_disabled]).to eq(1)
- end
-
- it 'gives precedence to pipeline variable' do
- pipeline_variable.update(value: "On")
-
- expect(subject[:ingress_modsecurity_blocking]).to eq(1)
- expect(subject[:ingress_modsecurity_disabled]).to eq(0)
- end
- end
-
- context 'when a project has multiple environments' do
- let(:modsec_values) { { key: ADO_MODSEC_KEY, value: "On" } }
-
- let!(:env1) { project_with_pipeline_var.environments.first }
- let!(:env2) { create(:environment, project: project_with_pipeline_var) }
-
- let!(:pipeline_with_2_deployments) do
- create(:ci_pipeline, :with_job, project: project_with_ci_var).tap do |pip|
- pip.builds << build(:ci_build, pipeline: pip, project: project_with_pipeline_var)
- end
- end
-
- let!(:deployment1) do
- create(
- :deployment,
- :success,
- environment: env1,
- project: project_with_pipeline_var,
- deployable: pipeline_with_2_deployments.builds.last
- )
- end
- let!(:deployment2) do
- create(
- :deployment,
- :success,
- environment: env2,
- project: project_with_pipeline_var,
- deployable: pipeline_with_2_deployments.builds.last
- )
- end
-
- context 'when set as ci variable' do
- let!(:ci_variable) { create(:ci_variable, project: project_with_pipeline_var, **modsec_values) }
-
- it 'gathers variable data' do
- expect(subject[:ingress_modsecurity_blocking]).to eq(2)
- expect(subject[:ingress_modsecurity_disabled]).to eq(0)
- end
- end
-
- context 'when set as pipeline variable' do
- let!(:pipeline_variable) { create(:ci_pipeline_variable, pipeline: pipeline_with_2_deployments, **modsec_values) }
-
- it 'gathers variable data' do
- expect(subject[:ingress_modsecurity_blocking]).to eq(2)
- expect(subject[:ingress_modsecurity_disabled]).to eq(0)
- end
- end
- end
-
- context 'when an environment has multiple deployments' do
- let!(:env) { project_with_pipeline_var.environments.first }
-
- let!(:pipeline_first) do
- create(:ci_pipeline, :with_job, project: project_with_pipeline_var).tap do |pip|
- pip.builds << build(:ci_build, pipeline: pip, project: project_with_pipeline_var)
- end
- end
- let!(:pipeline_last) do
- create(:ci_pipeline, :with_job, project: project_with_pipeline_var).tap do |pip|
- pip.builds << build(:ci_build, pipeline: pip, project: project_with_pipeline_var)
- end
- end
-
- let!(:deployment_first) do
- create(
- :deployment,
- :success,
- environment: env,
- project: project_with_pipeline_var,
- deployable: pipeline_first.builds.last
- )
- end
- let!(:deployment_last) do
- create(
- :deployment,
- :success,
- environment: env,
- project: project_with_pipeline_var,
- deployable: pipeline_last.builds.last
- )
- end
-
- context 'when set as pipeline variable' do
- let!(:first_pipeline_variable) { create(:ci_pipeline_variable, pipeline: pipeline_first, key: ADO_MODSEC_KEY, value: "On") }
- let!(:last_pipeline_variable) { create(:ci_pipeline_variable, pipeline: pipeline_last, key: ADO_MODSEC_KEY, value: "Off") }
-
- it 'gives precedence to latest deployment' do
- expect(subject[:ingress_modsecurity_blocking]).to eq(0)
- expect(subject[:ingress_modsecurity_disabled]).to eq(1)
- end
- end
- end
- end
-end
diff --git a/spec/services/clusters/applications/schedule_update_service_spec.rb b/spec/services/clusters/applications/schedule_update_service_spec.rb
index 0764f5b6a97..eb1006ce8e0 100644
--- a/spec/services/clusters/applications/schedule_update_service_spec.rb
+++ b/spec/services/clusters/applications/schedule_update_service_spec.rb
@@ -13,10 +13,10 @@ describe Clusters::Applications::ScheduleUpdateService do
context 'when application is able to be updated' do
context 'when the application was recently scheduled' do
it 'schedules worker with a backoff delay' do
- application = create(:clusters_applications_prometheus, :installed, last_update_started_at: Time.now + 5.minutes)
+ application = create(:clusters_applications_prometheus, :installed, last_update_started_at: Time.current + 5.minutes)
service = described_class.new(application, project)
- expect(::ClusterUpdateAppWorker).to receive(:perform_in).with(described_class::BACKOFF_DELAY, application.name, application.id, project.id, Time.now).once
+ expect(::ClusterUpdateAppWorker).to receive(:perform_in).with(described_class::BACKOFF_DELAY, application.name, application.id, project.id, Time.current).once
service.execute
end
@@ -27,7 +27,7 @@ describe Clusters::Applications::ScheduleUpdateService do
application = create(:clusters_applications_prometheus, :installed)
service = described_class.new(application, project)
- expect(::ClusterUpdateAppWorker).to receive(:perform_async).with(application.name, application.id, project.id, Time.now).once
+ expect(::ClusterUpdateAppWorker).to receive(:perform_async).with(application.name, application.id, project.id, Time.current).once
service.execute
end
diff --git a/spec/services/clusters/gcp/finalize_creation_service_spec.rb b/spec/services/clusters/gcp/finalize_creation_service_spec.rb
index 43dbea959a2..4d1548c9786 100644
--- a/spec/services/clusters/gcp/finalize_creation_service_spec.rb
+++ b/spec/services/clusters/gcp/finalize_creation_service_spec.rb
@@ -108,8 +108,7 @@ describe Clusters::Gcp::FinalizeCreationService, '#execute' do
}
)
- stub_kubeclient_get_cluster_role_binding_error(api_url, 'gitlab-admin')
- stub_kubeclient_create_cluster_role_binding(api_url)
+ stub_kubeclient_put_cluster_role_binding(api_url, 'gitlab-admin')
end
end
diff --git a/spec/services/clusters/kubernetes/configure_istio_ingress_service_spec.rb b/spec/services/clusters/kubernetes/configure_istio_ingress_service_spec.rb
index 9238f7debd0..e9f7f015293 100644
--- a/spec/services/clusters/kubernetes/configure_istio_ingress_service_spec.rb
+++ b/spec/services/clusters/kubernetes/configure_istio_ingress_service_spec.rb
@@ -120,8 +120,8 @@ describe Clusters::Kubernetes::ConfigureIstioIngressService, '#execute' do
expect(certificate.subject.to_s).to include(serverless_domain_cluster.knative.hostname)
- expect(certificate.not_before).to be_within(1.minute).of(Time.now)
- expect(certificate.not_after).to be_within(1.minute).of(Time.now + 1000.years)
+ expect(certificate.not_before).to be_within(1.minute).of(Time.current)
+ expect(certificate.not_after).to be_within(1.minute).of(Time.current + 1000.years)
expect(WebMock).to have_requested(:put, api_url + '/api/v1/namespaces/istio-system/secrets/istio-ingressgateway-ca-certs').with(
body: hash_including(
diff --git a/spec/services/clusters/kubernetes/create_or_update_namespace_service_spec.rb b/spec/services/clusters/kubernetes/create_or_update_namespace_service_spec.rb
index 3982d2310d8..6d8b1617c17 100644
--- a/spec/services/clusters/kubernetes/create_or_update_namespace_service_spec.rb
+++ b/spec/services/clusters/kubernetes/create_or_update_namespace_service_spec.rb
@@ -28,7 +28,6 @@ describe Clusters::Kubernetes::CreateOrUpdateNamespaceService, '#execute' do
stub_kubeclient_get_secret_error(api_url, 'gitlab-token')
stub_kubeclient_create_secret(api_url)
- stub_kubeclient_get_role_binding(api_url, "gitlab-#{namespace}", namespace: namespace)
stub_kubeclient_put_role_binding(api_url, "gitlab-#{namespace}", namespace: namespace)
stub_kubeclient_get_namespace(api_url, namespace: namespace)
stub_kubeclient_get_service_account_error(api_url, "#{namespace}-service-account", namespace: namespace)
diff --git a/spec/services/clusters/kubernetes/create_or_update_service_account_service_spec.rb b/spec/services/clusters/kubernetes/create_or_update_service_account_service_spec.rb
index 8fa22422074..4bcd5c6933e 100644
--- a/spec/services/clusters/kubernetes/create_or_update_service_account_service_spec.rb
+++ b/spec/services/clusters/kubernetes/create_or_update_service_account_service_spec.rb
@@ -83,8 +83,7 @@ describe Clusters::Kubernetes::CreateOrUpdateServiceAccountService do
before do
cluster.platform_kubernetes.rbac!
- stub_kubeclient_get_cluster_role_binding_error(api_url, cluster_role_binding_name)
- stub_kubeclient_create_cluster_role_binding(api_url)
+ stub_kubeclient_put_cluster_role_binding(api_url, cluster_role_binding_name)
end
it_behaves_like 'creates service account and token'
@@ -92,9 +91,8 @@ describe Clusters::Kubernetes::CreateOrUpdateServiceAccountService do
it 'creates a cluster role binding with cluster-admin access' do
subject
- expect(WebMock).to have_requested(:post, api_url + "/apis/rbac.authorization.k8s.io/v1/clusterrolebindings").with(
+ expect(WebMock).to have_requested(:put, api_url + "/apis/rbac.authorization.k8s.io/v1/clusterrolebindings/gitlab-admin").with(
body: hash_including(
- kind: 'ClusterRoleBinding',
metadata: { name: 'gitlab-admin' },
roleRef: {
apiGroup: 'rbac.authorization.k8s.io',
@@ -143,8 +141,7 @@ describe Clusters::Kubernetes::CreateOrUpdateServiceAccountService do
before do
cluster.platform_kubernetes.rbac!
- stub_kubeclient_get_role_binding_error(api_url, role_binding_name, namespace: namespace)
- stub_kubeclient_create_role_binding(api_url, namespace: namespace)
+ stub_kubeclient_put_role_binding(api_url, role_binding_name, namespace: namespace)
stub_kubeclient_put_role(api_url, Clusters::Kubernetes::GITLAB_KNATIVE_SERVING_ROLE_NAME, namespace: namespace)
stub_kubeclient_put_role_binding(api_url, Clusters::Kubernetes::GITLAB_KNATIVE_SERVING_ROLE_BINDING_NAME, namespace: namespace)
stub_kubeclient_put_role(api_url, Clusters::Kubernetes::GITLAB_CROSSPLANE_DATABASE_ROLE_NAME, namespace: namespace)
@@ -166,9 +163,8 @@ describe Clusters::Kubernetes::CreateOrUpdateServiceAccountService do
it 'creates a namespaced role binding with edit access' do
subject
- expect(WebMock).to have_requested(:post, api_url + "/apis/rbac.authorization.k8s.io/v1/namespaces/#{namespace}/rolebindings").with(
+ expect(WebMock).to have_requested(:put, api_url + "/apis/rbac.authorization.k8s.io/v1/namespaces/#{namespace}/rolebindings/#{role_binding_name}").with(
body: hash_including(
- kind: 'RoleBinding',
metadata: { name: "gitlab-#{namespace}", namespace: "#{namespace}" },
roleRef: {
apiGroup: 'rbac.authorization.k8s.io',
diff --git a/spec/services/clusters/parse_cluster_applications_artifact_service_spec.rb b/spec/services/clusters/parse_cluster_applications_artifact_service_spec.rb
new file mode 100644
index 00000000000..f14c929554a
--- /dev/null
+++ b/spec/services/clusters/parse_cluster_applications_artifact_service_spec.rb
@@ -0,0 +1,200 @@
+# frozen_string_literal: true
+
+require 'spec_helper'
+
+describe Clusters::ParseClusterApplicationsArtifactService do
+ let_it_be(:project) { create(:project) }
+ let_it_be(:user) { create(:user) }
+
+ before do
+ project.add_maintainer(user)
+ end
+
+ describe 'RELEASE_NAMES' do
+ it 'is included in Cluster application names', :aggregate_failures do
+ described_class::RELEASE_NAMES.each do |release_name|
+ expect(Clusters::Cluster::APPLICATIONS).to include(release_name)
+ end
+ end
+ end
+
+ describe '.new' do
+ let(:job) { build(:ci_build) }
+
+ it 'sets the project and current user', :aggregate_failures do
+ service = described_class.new(job, user)
+
+ expect(service.project).to eq(job.project)
+ expect(service.current_user).to eq(user)
+ end
+ end
+
+ describe '#execute' do
+ let_it_be(:cluster, reload: true) { create(:cluster, projects: [project]) }
+ let_it_be(:deployment, reload: true) { create(:deployment, cluster: cluster) }
+
+ let(:job) { deployment.deployable }
+ let(:artifact) { create(:ci_job_artifact, :cluster_applications, job: job) }
+
+ context 'when cluster_applications_artifact feature flag is disabled' do
+ before do
+ stub_feature_flags(cluster_applications_artifact: false)
+ end
+
+ it 'does not call Gitlab::Kubernetes::Helm::Parsers::ListV2 and returns success immediately' do
+ expect(Gitlab::Kubernetes::Helm::Parsers::ListV2).not_to receive(:new)
+
+ result = described_class.new(job, user).execute(artifact)
+
+ expect(result[:status]).to eq(:success)
+ end
+ end
+
+ context 'when cluster_applications_artifact feature flag is enabled for project' do
+ before do
+ stub_feature_flags(cluster_applications_artifact: job.project)
+ end
+
+ it 'calls Gitlab::Kubernetes::Helm::Parsers::ListV2' do
+ expect(Gitlab::Kubernetes::Helm::Parsers::ListV2).to receive(:new).and_call_original
+
+ result = described_class.new(job, user).execute(artifact)
+
+ expect(result[:status]).to eq(:success)
+ end
+
+ context 'artifact is not of cluster_applications type' do
+ let(:artifact) { create(:ci_job_artifact, :archive) }
+ let(:job) { artifact.job }
+
+ it 'raise ArgumentError' do
+ expect do
+ described_class.new(job, user).execute(artifact)
+ end.to raise_error(ArgumentError, 'Artifact is not cluster_applications file type')
+ end
+ end
+
+ context 'artifact exceeds acceptable size' do
+ it 'returns an error' do
+ stub_const("#{described_class}::MAX_ACCEPTABLE_ARTIFACT_SIZE", 1.byte)
+
+ result = described_class.new(job, user).execute(artifact)
+
+ expect(result[:status]).to eq(:error)
+ expect(result[:message]).to eq('Cluster_applications artifact too big. Maximum allowable size: 1 Byte')
+ end
+ end
+
+ context 'job has no deployment cluster' do
+ let(:job) { build(:ci_build) }
+
+ it 'returns an error' do
+ result = described_class.new(job, user).execute(artifact)
+
+ expect(result[:status]).to eq(:error)
+ expect(result[:message]).to eq('No deployment cluster found for this job')
+ end
+ end
+
+ context 'job has deployment cluster' do
+ context 'current user does not have access to deployment cluster' do
+ let(:other_user) { create(:user) }
+
+ it 'returns an error' do
+ result = described_class.new(job, other_user).execute(artifact)
+
+ expect(result[:status]).to eq(:error)
+ expect(result[:message]).to eq('No deployment cluster found for this job')
+ end
+ end
+
+ context 'release is missing' do
+ let(:fixture) { 'spec/fixtures/helm/helm_list_v2_prometheus_missing.json.gz' }
+ let(:file) { fixture_file_upload(Rails.root.join(fixture)) }
+ let(:artifact) { create(:ci_job_artifact, :cluster_applications, job: job, file: file) }
+
+ context 'application does not exist' do
+ it 'does not create or destroy an application' do
+ expect do
+ described_class.new(job, user).execute(artifact)
+ end.not_to change(Clusters::Applications::Prometheus, :count)
+ end
+ end
+
+ context 'application exists' do
+ before do
+ create(:clusters_applications_prometheus, :installed, cluster: cluster)
+ end
+
+ it 'marks the application as uninstalled' do
+ described_class.new(job, user).execute(artifact)
+
+ cluster.application_prometheus.reload
+ expect(cluster.application_prometheus).to be_uninstalled
+ end
+ end
+ end
+
+ context 'release is deployed' do
+ let(:fixture) { 'spec/fixtures/helm/helm_list_v2_prometheus_deployed.json.gz' }
+ let(:file) { fixture_file_upload(Rails.root.join(fixture)) }
+ let(:artifact) { create(:ci_job_artifact, :cluster_applications, job: job, file: file) }
+
+ context 'application does not exist' do
+ it 'creates an application and marks it as installed' do
+ expect do
+ described_class.new(job, user).execute(artifact)
+ end.to change(Clusters::Applications::Prometheus, :count)
+
+ expect(cluster.application_prometheus).to be_persisted
+ expect(cluster.application_prometheus).to be_installed
+ end
+ end
+
+ context 'application exists' do
+ before do
+ create(:clusters_applications_prometheus, :errored, cluster: cluster)
+ end
+
+ it 'marks the application as installed' do
+ described_class.new(job, user).execute(artifact)
+
+ expect(cluster.application_prometheus).to be_installed
+ end
+ end
+ end
+
+ context 'release is failed' do
+ let(:fixture) { 'spec/fixtures/helm/helm_list_v2_prometheus_failed.json.gz' }
+ let(:file) { fixture_file_upload(Rails.root.join(fixture)) }
+ let(:artifact) { create(:ci_job_artifact, :cluster_applications, job: job, file: file) }
+
+ context 'application does not exist' do
+ it 'creates an application and marks it as errored' do
+ expect do
+ described_class.new(job, user).execute(artifact)
+ end.to change(Clusters::Applications::Prometheus, :count)
+
+ expect(cluster.application_prometheus).to be_persisted
+ expect(cluster.application_prometheus).to be_errored
+ expect(cluster.application_prometheus.status_reason).to eq('Helm release failed to install')
+ end
+ end
+
+ context 'application exists' do
+ before do
+ create(:clusters_applications_prometheus, :installed, cluster: cluster)
+ end
+
+ it 'marks the application as errored' do
+ described_class.new(job, user).execute(artifact)
+
+ expect(cluster.application_prometheus).to be_errored
+ expect(cluster.application_prometheus.status_reason).to eq('Helm release failed to install')
+ end
+ end
+ end
+ end
+ end
+ end
+end
diff --git a/spec/services/cohorts_service_spec.rb b/spec/services/cohorts_service_spec.rb
index 38f441fbc4d..b2f82a1153c 100644
--- a/spec/services/cohorts_service_spec.rb
+++ b/spec/services/cohorts_service_spec.rb
@@ -13,7 +13,7 @@ describe CohortsService do
6.times do |months_ago|
months_ago_time = (months_ago * 2).months.ago
- create(:user, created_at: months_ago_time, last_activity_on: Time.now)
+ create(:user, created_at: months_ago_time, last_activity_on: Time.current)
create(:user, created_at: months_ago_time, last_activity_on: months_ago_time)
end
diff --git a/spec/services/deployments/older_deployments_drop_service_spec.rb b/spec/services/deployments/older_deployments_drop_service_spec.rb
index 44e9af07e46..4c9bcf90533 100644
--- a/spec/services/deployments/older_deployments_drop_service_spec.rb
+++ b/spec/services/deployments/older_deployments_drop_service_spec.rb
@@ -66,6 +66,43 @@ describe Deployments::OlderDeploymentsDropService do
expect(deployable.reload.failed?).to be_truthy
end
+ context 'when older deployable is a manual job' do
+ let(:older_deployment) { create(:deployment, :created, environment: environment, deployable: build) }
+ let(:build) { create(:ci_build, :manual) }
+
+ it 'does not drop any builds nor track the exception' do
+ expect(Gitlab::ErrorTracking).not_to receive(:track_exception)
+
+ expect { subject }.not_to change { Ci::Build.failed.count }
+ end
+ end
+
+ context 'when deployable.drop raises RuntimeError' do
+ before do
+ allow_any_instance_of(Ci::Build).to receive(:drop).and_raise(RuntimeError)
+ end
+
+ it 'does not drop an older deployment and tracks the exception' do
+ expect(Gitlab::ErrorTracking).to receive(:track_exception)
+ .with(kind_of(RuntimeError), subject_id: deployment.id, deployment_id: older_deployment.id)
+
+ expect { subject }.not_to change { Ci::Build.failed.count }
+ end
+ end
+
+ context 'when ActiveRecord::StaleObjectError is raised' do
+ before do
+ allow_any_instance_of(Ci::Build)
+ .to receive(:drop).and_raise(ActiveRecord::StaleObjectError)
+ end
+
+ it 'resets the object via Gitlab::OptimisticLocking' do
+ allow_any_instance_of(Ci::Build).to receive(:reset).at_least(:once)
+
+ subject
+ end
+ end
+
context 'and there is no deployable for that older deployment' do
let(:older_deployment) { create(:deployment, :running, environment: environment, deployable: nil) }
diff --git a/spec/services/design_management/delete_designs_service_spec.rb b/spec/services/design_management/delete_designs_service_spec.rb
new file mode 100644
index 00000000000..2c0c1570cb4
--- /dev/null
+++ b/spec/services/design_management/delete_designs_service_spec.rb
@@ -0,0 +1,195 @@
+# frozen_string_literal: true
+require 'spec_helper'
+
+describe DesignManagement::DeleteDesignsService do
+ include DesignManagementTestHelpers
+
+ let_it_be(:project) { create(:project) }
+ let_it_be(:issue) { create(:issue, project: project) }
+ let_it_be(:user) { create(:user) }
+ let(:designs) { create_designs }
+
+ subject(:service) { described_class.new(project, user, issue: issue, designs: designs) }
+
+ # Defined as a method so that the reponse is not cached. We also construct
+ # a new service executor each time to avoid the intermediate cached values
+ # it constructs during its execution.
+ def run_service(delenda = nil)
+ service = described_class.new(project, user, issue: issue, designs: delenda || designs)
+ service.execute
+ end
+
+ let(:response) { run_service }
+
+ shared_examples 'a service error' do
+ it 'returns an error', :aggregate_failures do
+ expect(response).to include(status: :error)
+ end
+ end
+
+ shared_examples 'a top-level error' do
+ let(:expected_error) { StandardError }
+ it 'raises an en expected error', :aggregate_failures do
+ expect { run_service }.to raise_error(expected_error)
+ end
+ end
+
+ shared_examples 'a success' do
+ it 'returns successfully', :aggregate_failures do
+ expect(response).to include(status: :success)
+ end
+
+ it 'saves the user as the author' do
+ version = response[:version]
+
+ expect(version.author).to eq(user)
+ end
+ end
+
+ before do
+ enable_design_management(enabled)
+ project.add_developer(user)
+ end
+
+ describe "#execute" do
+ context "when the feature is not available" do
+ let(:enabled) { false }
+
+ it_behaves_like "a service error"
+ end
+
+ context "when the feature is available" do
+ let(:enabled) { true }
+
+ it 'is able to delete designs' do
+ expect(service.send(:can_delete_designs?)).to be true
+ end
+
+ context 'no designs were passed' do
+ let(:designs) { [] }
+
+ it_behaves_like "a top-level error"
+
+ it 'does not log any events' do
+ counter = ::Gitlab::UsageDataCounters::DesignsCounter
+ expect { run_service rescue nil }.not_to change { counter.totals }
+ end
+ end
+
+ context 'one design is passed' do
+ before do
+ create_designs(2)
+ end
+
+ let!(:designs) { create_designs(1) }
+
+ it 'removes that design' do
+ expect { run_service }.to change { issue.designs.current.count }.from(3).to(2)
+ end
+
+ it 'logs a deletion event' do
+ counter = ::Gitlab::UsageDataCounters::DesignsCounter
+ expect { run_service }.to change { counter.read(:delete) }.by(1)
+ end
+
+ it 'informs the new-version-worker' do
+ expect(::DesignManagement::NewVersionWorker).to receive(:perform_async).with(Integer)
+
+ run_service
+ end
+
+ it 'creates a new version' do
+ expect { run_service }.to change { DesignManagement::Version.where(issue: issue).count }.by(1)
+ end
+
+ it 'returns the new version' do
+ version = response[:version]
+
+ expect(version).to eq(DesignManagement::Version.for_issue(issue).ordered.first)
+ end
+
+ it_behaves_like "a success"
+
+ it 'removes the design from the current design list' do
+ run_service
+
+ expect(issue.designs.current).not_to include(designs.first)
+ end
+
+ it 'marks the design as deleted' do
+ expect { run_service }
+ .to change { designs.first.deleted? }.from(false).to(true)
+ end
+ end
+
+ context 'more than one design is passed' do
+ before do
+ create_designs(1)
+ end
+
+ let!(:designs) { create_designs(2) }
+
+ it 'removes those designs' do
+ expect { run_service }
+ .to change { issue.designs.current.count }.from(3).to(1)
+ end
+
+ it 'logs the correct number of deletion events' do
+ counter = ::Gitlab::UsageDataCounters::DesignsCounter
+ expect { run_service }.to change { counter.read(:delete) }.by(2)
+ end
+
+ it_behaves_like "a success"
+
+ context 'after executing the service' do
+ let(:deleted_designs) { designs.map(&:reset) }
+
+ let!(:version) { run_service[:version] }
+
+ it 'removes the removed designs from the current design list' do
+ expect(issue.designs.current).not_to include(*deleted_designs)
+ end
+
+ it 'does not make the designs impossible to find' do
+ expect(issue.designs).to include(*deleted_designs)
+ end
+
+ it 'associates the new version with all the designs' do
+ current_versions = deleted_designs.map { |d| d.most_recent_action.version }
+ expect(current_versions).to all(eq version)
+ end
+
+ it 'marks all deleted designs as deleted' do
+ expect(deleted_designs).to all(be_deleted)
+ end
+
+ it 'marks all deleted designs with the same deletion version' do
+ expect(deleted_designs.map { |d| d.most_recent_action.version_id }.uniq)
+ .to have_attributes(size: 1)
+ end
+ end
+ end
+
+ describe 'scalability' do
+ before do
+ run_service(create_designs(1)) # ensure project, issue, etc are created
+ end
+
+ it 'makes the same number of DB requests for one design as for several' do
+ one = create_designs(1)
+ many = create_designs(5)
+
+ baseline = ActiveRecord::QueryRecorder.new { run_service(one) }
+
+ expect { run_service(many) }.not_to exceed_query_limit(baseline)
+ end
+ end
+ end
+ end
+
+ private
+
+ def create_designs(how_many = 2)
+ create_list(:design, how_many, :with_lfs_file, issue: issue)
+ end
+end
diff --git a/spec/services/design_management/design_user_notes_count_service_spec.rb b/spec/services/design_management/design_user_notes_count_service_spec.rb
new file mode 100644
index 00000000000..62211a4dd0f
--- /dev/null
+++ b/spec/services/design_management/design_user_notes_count_service_spec.rb
@@ -0,0 +1,43 @@
+# frozen_string_literal: true
+
+require 'spec_helper'
+
+describe DesignManagement::DesignUserNotesCountService, :use_clean_rails_memory_store_caching do
+ let_it_be(:design) { create(:design, :with_file) }
+
+ subject { described_class.new(design) }
+
+ it_behaves_like 'a counter caching service'
+
+ describe '#count' do
+ it 'returns the count of notes' do
+ create_list(:diff_note_on_design, 3, noteable: design)
+
+ expect(subject.count).to eq(3)
+ end
+ end
+
+ describe '#cache_key' do
+ it 'contains the `VERSION` and `design.id`' do
+ expect(subject.cache_key).to eq(['designs', 'notes_count', DesignManagement::DesignUserNotesCountService::VERSION, design.id])
+ end
+ end
+
+ describe 'cache invalidation' do
+ it 'changes when a new note is created' do
+ new_note_attrs = attributes_for(:diff_note_on_design, noteable: design)
+
+ expect do
+ Notes::CreateService.new(design.project, create(:user), new_note_attrs).execute
+ end.to change { subject.count }.by(1)
+ end
+
+ it 'changes when a note is destroyed' do
+ note = create(:diff_note_on_design, noteable: design)
+
+ expect do
+ Notes::DestroyService.new(note.project, note.author).execute(note)
+ end.to change { subject.count }.by(-1)
+ end
+ end
+end
diff --git a/spec/services/design_management/generate_image_versions_service_spec.rb b/spec/services/design_management/generate_image_versions_service_spec.rb
new file mode 100644
index 00000000000..cd021c8d7d3
--- /dev/null
+++ b/spec/services/design_management/generate_image_versions_service_spec.rb
@@ -0,0 +1,77 @@
+# frozen_string_literal: true
+
+require 'spec_helper'
+
+describe DesignManagement::GenerateImageVersionsService do
+ let_it_be(:project) { create(:project) }
+ let_it_be(:issue) { create(:issue, project: project) }
+ let_it_be(:version) { create(:design, :with_lfs_file, issue: issue).versions.first }
+ let_it_be(:action) { version.actions.first }
+
+ describe '#execute' do
+ it 'generates the image' do
+ expect { described_class.new(version).execute }
+ .to change { action.reload.image_v432x230.file }
+ .from(nil).to(CarrierWave::SanitizedFile)
+ end
+
+ it 'skips generating image versions if the mime type is not whitelisted' do
+ stub_const('DesignManagement::DesignV432x230Uploader::MIME_TYPE_WHITELIST', [])
+
+ described_class.new(version).execute
+
+ expect(action.reload.image_v432x230.file).to eq(nil)
+ end
+
+ it 'skips generating image versions if the design file size is too large' do
+ stub_const("#{described_class.name}::MAX_DESIGN_SIZE", 1.byte)
+
+ described_class.new(version).execute
+
+ expect(action.reload.image_v432x230.file).to eq(nil)
+ end
+
+ it 'returns the status' do
+ result = described_class.new(version).execute
+
+ expect(result[:status]).to eq(:success)
+ end
+
+ it 'returns the version' do
+ result = described_class.new(version).execute
+
+ expect(result[:version]).to eq(version)
+ end
+
+ it 'logs if the raw image cannot be found' do
+ version.designs.first.update(filename: 'foo.png')
+
+ expect(Gitlab::AppLogger).to receive(:error).with("No design file found for Action: #{action.id}")
+
+ described_class.new(version).execute
+ end
+
+ context 'when an error is encountered when generating the image versions' do
+ before do
+ expect_next_instance_of(DesignManagement::DesignV432x230Uploader) do |uploader|
+ expect(uploader).to receive(:cache!).and_raise(CarrierWave::DownloadError, 'foo')
+ end
+ end
+
+ it 'logs the error' do
+ expect(Gitlab::AppLogger).to receive(:error).with('foo')
+
+ described_class.new(version).execute
+ end
+
+ it 'tracks the error' do
+ expect(Gitlab::ErrorTracking).to receive(:track_exception).with(
+ instance_of(CarrierWave::DownloadError),
+ project_id: project.id, version_id: version.id, design_id: version.designs.first.id
+ )
+
+ described_class.new(version).execute
+ end
+ end
+ end
+end
diff --git a/spec/services/design_management/save_designs_service_spec.rb b/spec/services/design_management/save_designs_service_spec.rb
new file mode 100644
index 00000000000..013d5473860
--- /dev/null
+++ b/spec/services/design_management/save_designs_service_spec.rb
@@ -0,0 +1,356 @@
+# frozen_string_literal: true
+require 'spec_helper'
+
+describe DesignManagement::SaveDesignsService do
+ include DesignManagementTestHelpers
+ include ConcurrentHelpers
+
+ let_it_be(:developer) { create(:user) }
+ let(:project) { issue.project }
+ let(:issue) { create(:issue) }
+ let(:user) { developer }
+ let(:files) { [rails_sample] }
+ let(:design_repository) { ::Gitlab::GlRepository::DESIGN.repository_resolver.call(project) }
+ let(:rails_sample_name) { 'rails_sample.jpg' }
+ let(:rails_sample) { sample_image(rails_sample_name) }
+ let(:dk_png) { sample_image('dk.png') }
+
+ def sample_image(filename)
+ fixture_file_upload("spec/fixtures/#{filename}")
+ end
+
+ before do
+ project.add_developer(developer)
+ end
+
+ def run_service(files_to_upload = nil)
+ design_files = files_to_upload || files
+ design_files.each(&:rewind)
+
+ service = described_class.new(project, user,
+ issue: issue,
+ files: design_files)
+ service.execute
+ end
+
+ # Randomly alter the content of files.
+ # This allows the files to be updated by the service, as unmodified
+ # files are rejected.
+ def touch_files(files_to_touch = nil)
+ design_files = files_to_touch || files
+
+ design_files.each do |f|
+ f.tempfile.write(SecureRandom.random_bytes)
+ end
+ end
+
+ let(:response) { run_service }
+
+ shared_examples 'a service error' do
+ it 'returns an error', :aggregate_failures do
+ expect(response).to match(a_hash_including(status: :error))
+ end
+ end
+
+ shared_examples 'an execution error' do
+ it 'returns an error', :aggregate_failures do
+ expect { service.execute }.to raise_error(some_error)
+ end
+ end
+
+ describe '#execute' do
+ context 'when the feature is not available' do
+ before do
+ enable_design_management(false)
+ end
+
+ it_behaves_like 'a service error'
+ end
+
+ context 'when the feature is available' do
+ before do
+ enable_design_management(true)
+ end
+
+ describe 'repository existence' do
+ def repository_exists
+ # Expire the memoized value as the service creates it's own instance
+ design_repository.expire_exists_cache
+ design_repository.exists?
+ end
+
+ it 'creates a design repository when it did not exist' do
+ expect { run_service }.to change { repository_exists }.from(false).to(true)
+ end
+ end
+
+ it 'updates the creation count' do
+ counter = Gitlab::UsageDataCounters::DesignsCounter
+ expect { run_service }.to change { counter.read(:create) }.by(1)
+ end
+
+ it 'creates a commit in the repository' do
+ run_service
+
+ expect(design_repository.commit).to have_attributes(
+ author: user,
+ message: include(rails_sample_name)
+ )
+ end
+
+ it 'can run the same command in parallel' do
+ blocks = Array.new(10).map do
+ unique_files = %w(rails_sample.jpg dk.png)
+ .map { |name| RenameableUpload.unique_file(name) }
+
+ -> { run_service(unique_files) }
+ end
+
+ expect { run_parallel(blocks) }.to change(DesignManagement::Version, :count).by(10)
+ end
+
+ it 'causes diff_refs not to be nil' do
+ expect(response).to include(
+ designs: all(have_attributes(diff_refs: be_present))
+ )
+ end
+
+ it 'creates a design & a version for the filename if it did not exist' do
+ expect(issue.designs.size).to eq(0)
+
+ updated_designs = response[:designs]
+
+ expect(updated_designs.size).to eq(1)
+ expect(updated_designs.first.versions.size).to eq(1)
+ end
+
+ it 'saves the user as the author' do
+ updated_designs = response[:designs]
+
+ expect(updated_designs.first.versions.first.author).to eq(user)
+ end
+
+ describe 'saving the file to LFS' do
+ before do
+ expect_next_instance_of(Lfs::FileTransformer) do |transformer|
+ expect(transformer).to receive(:lfs_file?).and_return(true)
+ end
+ end
+
+ it 'saves the design to LFS' do
+ expect { run_service }.to change { LfsObject.count }.by(1)
+ end
+
+ it 'saves the repository_type of the LfsObjectsProject as design' do
+ expect do
+ run_service
+ end.to change { project.lfs_objects_projects.count }.from(0).to(1)
+
+ expect(project.lfs_objects_projects.first.repository_type).to eq('design')
+ end
+ end
+
+ context 'when a design is being updated' do
+ before do
+ run_service
+ touch_files
+ end
+
+ it 'creates a new version for the existing design and updates the file' do
+ expect(issue.designs.size).to eq(1)
+ expect(DesignManagement::Version.for_designs(issue.designs).size).to eq(1)
+
+ updated_designs = response[:designs]
+
+ expect(updated_designs.size).to eq(1)
+ expect(updated_designs.first.versions.size).to eq(2)
+ end
+
+ it 'increments the update counter' do
+ counter = Gitlab::UsageDataCounters::DesignsCounter
+ expect { run_service }.to change { counter.read(:update) }.by 1
+ end
+
+ context 'when uploading a new design' do
+ it 'does not link the new version to the existing design' do
+ existing_design = issue.designs.first
+
+ updated_designs = run_service([dk_png])[:designs]
+
+ expect(existing_design.versions.reload.size).to eq(1)
+ expect(updated_designs.size).to eq(1)
+ expect(updated_designs.first.versions.size).to eq(1)
+ end
+ end
+ end
+
+ context 'when a design has not changed since its previous version' do
+ before do
+ run_service
+ end
+
+ it 'does not create a new version' do
+ expect { run_service }.not_to change { issue.design_versions.count }
+ end
+
+ it 'returns the design in `skipped_designs` instead of `designs`' do
+ response = run_service
+
+ expect(response[:designs]).to be_empty
+ expect(response[:skipped_designs].size).to eq(1)
+ end
+ end
+
+ context 'when doing a mixture of updates and creations' do
+ let(:files) { [rails_sample, dk_png] }
+
+ before do
+ # Create just the first one, which we will later update.
+ run_service([files.first])
+ touch_files([files.first])
+ end
+
+ it 'counts one creation and one update' do
+ counter = Gitlab::UsageDataCounters::DesignsCounter
+ expect { run_service }
+ .to change { counter.read(:create) }.by(1)
+ .and change { counter.read(:update) }.by(1)
+ end
+
+ it 'creates a single commit' do
+ commit_count = -> do
+ design_repository.expire_all_method_caches
+ design_repository.commit_count
+ end
+
+ expect { run_service }.to change { commit_count.call }.by(1)
+ end
+
+ it 'enqueues just one new version worker' do
+ expect(::DesignManagement::NewVersionWorker)
+ .to receive(:perform_async).once.with(Integer)
+
+ run_service
+ end
+ end
+
+ context 'when uploading multiple files' do
+ let(:files) { [rails_sample, dk_png] }
+
+ it 'returns information about both designs in the response' do
+ expect(response).to include(designs: have_attributes(size: 2), status: :success)
+ end
+
+ it 'creates 2 designs with a single version' do
+ expect { run_service }.to change { issue.designs.count }.from(0).to(2)
+
+ expect(DesignManagement::Version.for_designs(issue.designs).size).to eq(1)
+ end
+
+ it 'increments the creation count by 2' do
+ counter = Gitlab::UsageDataCounters::DesignsCounter
+ expect { run_service }.to change { counter.read(:create) }.by 2
+ end
+
+ it 'enqueues a new version worker' do
+ expect(::DesignManagement::NewVersionWorker)
+ .to receive(:perform_async).once.with(Integer)
+
+ run_service
+ end
+
+ it 'creates a single commit' do
+ commit_count = -> do
+ design_repository.expire_all_method_caches
+ design_repository.commit_count
+ end
+
+ expect { run_service }.to change { commit_count.call }.by(1)
+ end
+
+ it 'only does 5 gitaly calls', :request_store, :sidekiq_might_not_need_inline do
+ allow(::DesignManagement::NewVersionWorker).to receive(:perform_async).with(Integer)
+ service = described_class.new(project, user, issue: issue, files: files)
+ # Some unrelated calls that are usually cached or happen only once
+ service.__send__(:repository).create_if_not_exists
+ service.__send__(:repository).has_visible_content?
+
+ request_count = -> { Gitlab::GitalyClient.get_request_count }
+
+ # An exists?, a check for existing blobs, default branch, an after_commit
+ # callback on LfsObjectsProject
+ expect { service.execute }.to change(&request_count).by(4)
+ end
+
+ context 'when uploading too many files' do
+ let(:files) { Array.new(DesignManagement::SaveDesignsService::MAX_FILES + 1) { dk_png } }
+
+ it 'returns the correct error' do
+ expect(response[:message]).to match(/only \d+ files are allowed simultaneously/i)
+ end
+ end
+ end
+
+ context 'when the user is not allowed to upload designs' do
+ let(:user) { create(:user) }
+
+ it_behaves_like 'a service error'
+ end
+
+ describe 'failure modes' do
+ let(:service) { described_class.new(project, user, issue: issue, files: files) }
+ let(:response) { service.execute }
+
+ before do
+ expect(service).to receive(:run_actions).and_raise(some_error)
+ end
+
+ context 'when creating the commit fails' do
+ let(:some_error) { Gitlab::Git::BaseError }
+
+ it_behaves_like 'an execution error'
+ end
+
+ context 'when creating the versions fails' do
+ let(:some_error) { ActiveRecord::RecordInvalid }
+
+ it_behaves_like 'a service error'
+ end
+ end
+
+ context "when a design already existed in the repo but we didn't know about it in the database" do
+ let(:filename) { rails_sample_name }
+
+ before do
+ path = File.join(build(:design, issue: issue, filename: filename).full_path)
+ design_repository.create_if_not_exists
+ design_repository.create_file(user, path, 'something fake',
+ branch_name: 'master',
+ message: 'Somehow created without being tracked in db')
+ end
+
+ it 'creates the design and a new version for it' do
+ first_updated_design = response[:designs].first
+
+ expect(first_updated_design.filename).to eq(filename)
+ expect(first_updated_design.versions.size).to eq(1)
+ end
+ end
+
+ describe 'scalability', skip: 'See: https://gitlab.com/gitlab-org/gitlab/-/issues/213169' do
+ before do
+ run_service([sample_image('banana_sample.gif')]) # ensure project, issue, etc are created
+ end
+
+ it 'runs the same queries for all requests, regardless of number of files' do
+ one = [dk_png]
+ two = [rails_sample, dk_png]
+
+ baseline = ActiveRecord::QueryRecorder.new { run_service(one) }
+
+ expect { run_service(two) }.not_to exceed_query_limit(baseline)
+ end
+ end
+ end
+ end
+end
diff --git a/spec/services/emails/confirm_service_spec.rb b/spec/services/emails/confirm_service_spec.rb
index 6a274ca9dfe..973d2731b2f 100644
--- a/spec/services/emails/confirm_service_spec.rb
+++ b/spec/services/emails/confirm_service_spec.rb
@@ -8,10 +8,10 @@ describe Emails::ConfirmService do
subject(:service) { described_class.new(user) }
describe '#execute' do
- it 'sends a confirmation email again' do
+ it 'enqueues a background job to send confirmation email again' do
email = user.emails.create(email: 'new@email.com')
- mail = service.execute(email)
- expect(mail.subject).to eq('Confirmation instructions')
+
+ expect { service.execute(email) }.to have_enqueued_job.on_queue('mailers')
end
end
end
diff --git a/spec/services/event_create_service_spec.rb b/spec/services/event_create_service_spec.rb
index 0a8a4d5bf58..987b4ad68f7 100644
--- a/spec/services/event_create_service_spec.rb
+++ b/spec/services/event_create_service_spec.rb
@@ -162,16 +162,25 @@ describe EventCreateService do
context "The action is #{action}" do
let(:event) { service.wiki_event(meta, user, action) }
- it 'creates the event' do
+ it 'creates the event', :aggregate_failures do
expect(event).to have_attributes(
wiki_page?: true,
valid?: true,
persisted?: true,
action: action,
- wiki_page: wiki_page
+ wiki_page: wiki_page,
+ author: user
)
end
+ it 'is idempotent', :aggregate_failures do
+ expect { event }.to change(Event, :count).by(1)
+ duplicate = nil
+ expect { duplicate = service.wiki_event(meta, user, action) }.not_to change(Event, :count)
+
+ expect(duplicate).to eq(event)
+ end
+
context 'the feature is disabled' do
before do
stub_feature_flags(wiki_events: false)
diff --git a/spec/services/git/branch_push_service_spec.rb b/spec/services/git/branch_push_service_spec.rb
index acd14005c69..6ecc1a62ff3 100644
--- a/spec/services/git/branch_push_service_spec.rb
+++ b/spec/services/git/branch_push_service_spec.rb
@@ -291,7 +291,7 @@ describe Git::BranchPushService, services: true do
execute_service(project, user, oldrev: oldrev, newrev: newrev, ref: ref)
end
- it "defaults to the pushing user if the commit's author is not known", :sidekiq_might_not_need_inline do
+ it "defaults to the pushing user if the commit's author is not known", :sidekiq_inline, :use_clean_rails_redis_caching do
allow(commit).to receive_messages(
author_name: 'unknown name',
author_email: 'unknown@email.com'
@@ -315,7 +315,7 @@ describe Git::BranchPushService, services: true do
let(:issue) { create :issue, project: project }
let(:commit_author) { create :user }
let(:commit) { project.commit }
- let(:commit_time) { Time.now }
+ let(:commit_time) { Time.current }
before do
project.add_developer(commit_author)
@@ -336,7 +336,7 @@ describe Git::BranchPushService, services: true do
end
context "while saving the 'first_mentioned_in_commit_at' metric for an issue" do
- it 'sets the metric for referenced issues', :sidekiq_might_not_need_inline do
+ it 'sets the metric for referenced issues', :sidekiq_inline, :use_clean_rails_redis_caching do
execute_service(project, user, oldrev: oldrev, newrev: newrev, ref: ref)
expect(issue.reload.metrics.first_mentioned_in_commit_at).to be_like_time(commit_time)
@@ -397,7 +397,7 @@ describe Git::BranchPushService, services: true do
allow(project).to receive(:default_branch).and_return('not-master')
end
- it "creates cross-reference notes", :sidekiq_might_not_need_inline do
+ it "creates cross-reference notes", :sidekiq_inline, :use_clean_rails_redis_caching do
expect(SystemNoteService).to receive(:cross_reference).with(issue, closing_commit, commit_author)
execute_service(project, user, oldrev: oldrev, newrev: newrev, ref: ref)
end
@@ -438,7 +438,7 @@ describe Git::BranchPushService, services: true do
context "mentioning an issue" do
let(:message) { "this is some work.\n\nrelated to JIRA-1" }
- it "initiates one api call to jira server to mention the issue", :sidekiq_might_not_need_inline do
+ it "initiates one api call to jira server to mention the issue", :sidekiq_inline, :use_clean_rails_redis_caching do
execute_service(project, user, oldrev: oldrev, newrev: newrev, ref: ref)
expect(WebMock).to have_requested(:post, jira_api_comment_url('JIRA-1')).with(
diff --git a/spec/services/git/wiki_push_service/change_spec.rb b/spec/services/git/wiki_push_service/change_spec.rb
new file mode 100644
index 00000000000..547874270ab
--- /dev/null
+++ b/spec/services/git/wiki_push_service/change_spec.rb
@@ -0,0 +1,109 @@
+# frozen_string_literal: true
+
+require 'spec_helper'
+
+describe Git::WikiPushService::Change do
+ subject { described_class.new(project_wiki, change, raw_change) }
+
+ let(:project_wiki) { double('ProjectWiki') }
+ let(:raw_change) { double('RawChange', new_path: new_path, old_path: old_path, operation: operation) }
+ let(:change) { { oldrev: generate(:sha), newrev: generate(:sha) } }
+
+ let(:new_path) do
+ case operation
+ when :deleted
+ nil
+ else
+ generate(:wiki_filename)
+ end
+ end
+
+ let(:old_path) do
+ case operation
+ when :added
+ nil
+ when :deleted, :renamed
+ generate(:wiki_filename)
+ else
+ new_path
+ end
+ end
+
+ describe '#page' do
+ context 'the page does not exist' do
+ before do
+ expect(project_wiki).to receive(:find_page).with(String, String).and_return(nil)
+ end
+
+ %i[added deleted renamed modified].each do |op|
+ context "the operation is #{op}" do
+ let(:operation) { op }
+
+ it { is_expected.to have_attributes(page: be_nil) }
+ end
+ end
+ end
+
+ context 'the page can be found' do
+ let(:wiki_page) { double('WikiPage') }
+
+ before do
+ expect(project_wiki).to receive(:find_page).with(slug, revision).and_return(wiki_page)
+ end
+
+ context 'the page has been deleted' do
+ let(:operation) { :deleted }
+ let(:slug) { old_path.chomp('.md') }
+ let(:revision) { change[:oldrev] }
+
+ it { is_expected.to have_attributes(page: wiki_page) }
+ end
+
+ %i[added renamed modified].each do |op|
+ let(:operation) { op }
+ let(:slug) { new_path.chomp('.md') }
+ let(:revision) { change[:newrev] }
+
+ it { is_expected.to have_attributes(page: wiki_page) }
+ end
+ end
+ end
+
+ describe '#last_known_slug' do
+ context 'the page has been created' do
+ let(:operation) { :added }
+
+ it { is_expected.to have_attributes(last_known_slug: new_path.chomp('.md')) }
+ end
+
+ %i[renamed modified deleted].each do |op|
+ context "the operation is #{op}" do
+ let(:operation) { op }
+
+ it { is_expected.to have_attributes(last_known_slug: old_path.chomp('.md')) }
+ end
+ end
+ end
+
+ describe '#event_action' do
+ context 'the page is deleted' do
+ let(:operation) { :deleted }
+
+ it { is_expected.to have_attributes(event_action: Event::DESTROYED) }
+ end
+
+ context 'the page is added' do
+ let(:operation) { :added }
+
+ it { is_expected.to have_attributes(event_action: Event::CREATED) }
+ end
+
+ %i[renamed modified].each do |op|
+ context "the page is #{op}" do
+ let(:operation) { op }
+
+ it { is_expected.to have_attributes(event_action: Event::UPDATED) }
+ end
+ end
+ end
+end
diff --git a/spec/services/git/wiki_push_service_spec.rb b/spec/services/git/wiki_push_service_spec.rb
new file mode 100644
index 00000000000..cdb1dc5a435
--- /dev/null
+++ b/spec/services/git/wiki_push_service_spec.rb
@@ -0,0 +1,338 @@
+# frozen_string_literal: true
+
+require 'spec_helper'
+
+describe Git::WikiPushService, services: true do
+ include RepoHelpers
+
+ let_it_be(:key_id) { create(:key, user: current_user).shell_id }
+ let_it_be(:project) { create(:project, :wiki_repo) }
+ let_it_be(:current_user) { create(:user) }
+ let_it_be(:git_wiki) { project.wiki.wiki }
+ let_it_be(:repository) { git_wiki.repository }
+
+ describe '#execute' do
+ context 'the push contains more than the permitted number of changes' do
+ def run_service
+ process_changes { described_class::MAX_CHANGES.succ.times { write_new_page } }
+ end
+
+ it 'creates only MAX_CHANGES events' do
+ expect { run_service }.to change(Event, :count).by(described_class::MAX_CHANGES)
+ end
+ end
+
+ context 'default_branch collides with a tag' do
+ it 'creates only one event' do
+ base_sha = current_sha
+ write_new_page
+
+ service = create_service(base_sha, ['refs/heads/master', 'refs/tags/master'])
+
+ expect { service.execute }.to change(Event, :count).by(1)
+ end
+ end
+
+ describe 'successfully creating events' do
+ let(:count) { Event::WIKI_ACTIONS.size }
+
+ def run_service
+ wiki_page_a = create(:wiki_page, project: project)
+ wiki_page_b = create(:wiki_page, project: project)
+
+ process_changes do
+ write_new_page
+ update_page(wiki_page_a.title)
+ delete_page(wiki_page_b.page.path)
+ end
+ end
+
+ it 'creates one event for every wiki action' do
+ expect { run_service }.to change(Event, :count).by(count)
+ end
+
+ it 'handles all known actions' do
+ run_service
+
+ expect(Event.last(count).pluck(:action)).to match_array(Event::WIKI_ACTIONS)
+ end
+ end
+
+ context 'two pages have been created' do
+ def run_service
+ process_changes do
+ write_new_page
+ write_new_page
+ end
+ end
+
+ it 'creates two events' do
+ expect { run_service }.to change(Event, :count).by(2)
+ end
+
+ it 'creates two metadata records' do
+ expect { run_service }.to change(WikiPage::Meta, :count).by(2)
+ end
+
+ it 'creates appropriate events' do
+ run_service
+
+ expect(Event.last(2)).to all(have_attributes(wiki_page?: true, action: Event::CREATED))
+ end
+ end
+
+ context 'a non-page file as been added' do
+ it 'does not create events, or WikiPage metadata' do
+ expect do
+ process_changes { write_non_page }
+ end.not_to change { [Event.count, WikiPage::Meta.count] }
+ end
+ end
+
+ context 'one page, and one non-page have been created' do
+ def run_service
+ process_changes do
+ write_new_page
+ write_non_page
+ end
+ end
+
+ it 'creates a wiki page creation event' do
+ expect { run_service }.to change(Event, :count).by(1)
+
+ expect(Event.last).to have_attributes(wiki_page?: true, action: Event::CREATED)
+ end
+
+ it 'creates one metadata record' do
+ expect { run_service }.to change(WikiPage::Meta, :count).by(1)
+ end
+ end
+
+ context 'one page has been added, and then updated' do
+ def run_service
+ process_changes do
+ title = write_new_page
+ update_page(title)
+ end
+ end
+
+ it 'creates just a single event' do
+ expect { run_service }.to change(Event, :count).by(1)
+ end
+
+ it 'creates just one metadata record' do
+ expect { run_service }.to change(WikiPage::Meta, :count).by(1)
+ end
+
+ it 'creates a new wiki page creation event' do
+ run_service
+
+ expect(Event.last).to have_attributes(
+ wiki_page?: true,
+ action: Event::CREATED
+ )
+ end
+ end
+
+ context 'when a page we already know about has been updated' do
+ let(:wiki_page) { create(:wiki_page, project: project) }
+
+ before do
+ create(:wiki_page_meta, :for_wiki_page, wiki_page: wiki_page)
+ end
+
+ def run_service
+ process_changes { update_page(wiki_page.title) }
+ end
+
+ it 'does not create a new meta-data record' do
+ expect { run_service }.not_to change(WikiPage::Meta, :count)
+ end
+
+ it 'creates a new event' do
+ expect { run_service }.to change(Event, :count).by(1)
+ end
+
+ it 'adds an update event' do
+ run_service
+
+ expect(Event.last).to have_attributes(
+ wiki_page?: true,
+ action: Event::UPDATED
+ )
+ end
+ end
+
+ context 'when a page we do not know about has been updated' do
+ def run_service
+ wiki_page = create(:wiki_page, project: project)
+ process_changes { update_page(wiki_page.title) }
+ end
+
+ it 'creates a new meta-data record' do
+ expect { run_service }.to change(WikiPage::Meta, :count).by(1)
+ end
+
+ it 'creates a new event' do
+ expect { run_service }.to change(Event, :count).by(1)
+ end
+
+ it 'adds an update event' do
+ run_service
+
+ expect(Event.last).to have_attributes(
+ wiki_page?: true,
+ action: Event::UPDATED
+ )
+ end
+ end
+
+ context 'when a page we do not know about has been deleted' do
+ def run_service
+ wiki_page = create(:wiki_page, project: project)
+ process_changes { delete_page(wiki_page.page.path) }
+ end
+
+ it 'create a new meta-data record' do
+ expect { run_service }.to change(WikiPage::Meta, :count).by(1)
+ end
+
+ it 'creates a new event' do
+ expect { run_service }.to change(Event, :count).by(1)
+ end
+
+ it 'adds an update event' do
+ run_service
+
+ expect(Event.last).to have_attributes(
+ wiki_page?: true,
+ action: Event::DESTROYED
+ )
+ end
+ end
+
+ it 'calls log_error for every event we cannot create' do
+ base_sha = current_sha
+ count = 3
+ count.times { write_new_page }
+ message = 'something went very very wrong'
+ allow_next_instance_of(WikiPages::EventCreateService, current_user) do |service|
+ allow(service).to receive(:execute)
+ .with(String, WikiPage, Integer)
+ .and_return(ServiceResponse.error(message: message))
+ end
+
+ service = create_service(base_sha)
+
+ expect(service).to receive(:log_error).exactly(count).times.with(message)
+
+ service.execute
+ end
+
+ describe 'feature flags' do
+ shared_examples 'a no-op push' do
+ it 'does not create any events' do
+ expect { process_changes { write_new_page } }.not_to change(Event, :count)
+ end
+
+ it 'does not even look for events to process' do
+ base_sha = current_sha
+ write_new_page
+
+ service = create_service(base_sha)
+
+ expect(service).not_to receive(:changed_files)
+
+ service.execute
+ end
+ end
+
+ context 'the wiki_events feature is disabled' do
+ before do
+ stub_feature_flags(wiki_events: false)
+ end
+
+ it_behaves_like 'a no-op push'
+ end
+
+ context 'the wiki_events_on_git_push feature is disabled' do
+ before do
+ stub_feature_flags(wiki_events_on_git_push: false)
+ end
+
+ it_behaves_like 'a no-op push'
+
+ context 'but is enabled for a given project' do
+ before do
+ stub_feature_flags(wiki_events_on_git_push: project)
+ end
+
+ it 'creates events' do
+ expect { process_changes { write_new_page } }.to change(Event, :count).by(1)
+ end
+ end
+ end
+ end
+ end
+
+ # In order to construct the correct GitPostReceive object that represents the
+ # changes we are applying, we need to describe the changes between old-ref and
+ # new-ref. Old ref (the base sha) we have to capture before we perform any
+ # changes. Once the changes have been applied, we can execute the service to
+ # process them.
+ def process_changes(&block)
+ base_sha = current_sha
+ yield
+ create_service(base_sha).execute
+ end
+
+ def create_service(base, refs = ['refs/heads/master'])
+ changes = post_received(base, refs).changes
+ described_class.new(project, current_user, changes: changes)
+ end
+
+ def post_received(base, refs)
+ change_str = refs.map { |ref| +"#{base} #{current_sha} #{ref}" }.join("\n")
+ post_received = ::Gitlab::GitPostReceive.new(project, key_id, change_str, {})
+ allow(post_received).to receive(:identify).with(key_id).and_return(current_user)
+
+ post_received
+ end
+
+ def current_sha
+ repository.gitaly_ref_client.find_branch('master')&.dereferenced_target&.id || Gitlab::Git::BLANK_SHA
+ end
+
+ # It is important not to re-use the WikiPage services here, since they create
+ # events - these helper methods below are intended to simulate actions on the repo
+ # that have not gone through our services.
+
+ def write_new_page
+ generate(:wiki_page_title).tap { |t| git_wiki.write_page(t, 'markdown', 'Hello', commit_details) }
+ end
+
+ # We write something to the wiki-repo that is not a page - as, for example, an
+ # attachment. This will appear as a raw-diff change, but wiki.find_page will
+ # return nil.
+ def write_non_page
+ params = {
+ file_name: 'attachment.log',
+ file_content: 'some stuff',
+ branch_name: 'master'
+ }
+ ::Wikis::CreateAttachmentService.new(container: project, current_user: project.owner, params: params).execute
+ end
+
+ def update_page(title)
+ page = git_wiki.page(title: title)
+ git_wiki.update_page(page.path, title, 'markdown', 'Hey', commit_details)
+ end
+
+ def delete_page(path)
+ git_wiki.delete_page(path, commit_details)
+ end
+
+ def commit_details
+ create(:git_wiki_commit_details, author: current_user)
+ end
+end
diff --git a/spec/services/grafana/proxy_service_spec.rb b/spec/services/grafana/proxy_service_spec.rb
index 694d531c9fc..8cb7210524a 100644
--- a/spec/services/grafana/proxy_service_spec.rb
+++ b/spec/services/grafana/proxy_service_spec.rb
@@ -66,7 +66,7 @@ describe Grafana::ProxyService do
context 'with caching', :use_clean_rails_memory_store_caching do
context 'when value not present in cache' do
it 'returns nil' do
- expect(ReactiveCachingWorker)
+ expect(ExternalServiceReactiveCachingWorker)
.to receive(:perform_async)
.with(service.class, service.id, *cache_params)
diff --git a/spec/services/groups/create_service_spec.rb b/spec/services/groups/create_service_spec.rb
index 5cde9a3ed45..c0e876cce33 100644
--- a/spec/services/groups/create_service_spec.rb
+++ b/spec/services/groups/create_service_spec.rb
@@ -24,6 +24,27 @@ describe Groups::CreateService, '#execute' do
end
end
+ context 'creating a group with `default_branch_protection` attribute' do
+ let(:params) { group_params.merge(default_branch_protection: Gitlab::Access::PROTECTION_NONE) }
+ let(:service) { described_class.new(user, params) }
+ let(:created_group) { service.execute }
+
+ context 'for users who have the ability to create a group with `default_branch_protection`' do
+ it 'creates group with the specified branch protection level' do
+ expect(created_group.default_branch_protection).to eq(Gitlab::Access::PROTECTION_NONE)
+ end
+ end
+
+ context 'for users who do not have the ability to create a group with `default_branch_protection`' do
+ it 'does not create the group with the specified branch protection level' do
+ allow(Ability).to receive(:allowed?).and_call_original
+ allow(Ability).to receive(:allowed?).with(user, :create_group_with_default_branch_protection) { false }
+
+ expect(created_group.default_branch_protection).not_to eq(Gitlab::Access::PROTECTION_NONE)
+ end
+ end
+ end
+
describe 'creating a top level group' do
let(:service) { described_class.new(user, group_params) }
diff --git a/spec/services/groups/import_export/export_service_spec.rb b/spec/services/groups/import_export/export_service_spec.rb
index 56c7121cc34..7bad68b4e00 100644
--- a/spec/services/groups/import_export/export_service_spec.rb
+++ b/spec/services/groups/import_export/export_service_spec.rb
@@ -11,7 +11,7 @@ describe Groups::ImportExport::ExportService do
let(:export_service) { described_class.new(group: group, user: user) }
it 'enqueues an export job' do
- expect(GroupExportWorker).to receive(:perform_async).with(user.id, group.id, {})
+ allow(GroupExportWorker).to receive(:perform_async).with(user.id, group.id, {})
export_service.async_execute
end
@@ -49,12 +49,36 @@ describe Groups::ImportExport::ExportService do
FileUtils.rm_rf(archive_path)
end
- it 'saves the models' do
+ it 'saves the version' do
+ expect(Gitlab::ImportExport::VersionSaver).to receive(:new).and_call_original
+
+ service.execute
+ end
+
+ it 'saves the models using ndjson tree saver' do
+ stub_feature_flags(group_export_ndjson: true)
+
+ expect(Gitlab::ImportExport::Group::TreeSaver).to receive(:new).and_call_original
+
+ service.execute
+ end
+
+ it 'saves the models using legacy tree saver' do
+ stub_feature_flags(group_export_ndjson: false)
+
expect(Gitlab::ImportExport::Group::LegacyTreeSaver).to receive(:new).and_call_original
service.execute
end
+ it 'notifies the user' do
+ expect_next_instance_of(NotificationService) do |instance|
+ expect(instance).to receive(:group_was_exported)
+ end
+
+ service.execute
+ end
+
context 'when saver succeeds' do
it 'saves the group in the file system' do
service.execute
@@ -98,16 +122,26 @@ describe Groups::ImportExport::ExportService do
context 'when export fails' do
context 'when file saver fails' do
- it 'removes the remaining exported data' do
+ before do
allow_next_instance_of(Gitlab::ImportExport::Saver) do |saver|
allow(saver).to receive(:save).and_return(false)
end
+ end
+ it 'removes the remaining exported data' do
expect { service.execute }.to raise_error(Gitlab::ImportExport::Error)
expect(group.import_export_upload).to be_nil
expect(File.exist?(shared.archive_path)).to eq(false)
end
+
+ it 'notifies the user about failed group export' do
+ expect_next_instance_of(NotificationService) do |instance|
+ expect(instance).to receive(:group_was_not_exported)
+ end
+
+ expect { service.execute }.to raise_error(Gitlab::ImportExport::Error)
+ end
end
context 'when file compression fails' do
diff --git a/spec/services/groups/import_export/import_service_spec.rb b/spec/services/groups/import_export/import_service_spec.rb
index d95bba38b3e..256e0a1b3c5 100644
--- a/spec/services/groups/import_export/import_service_spec.rb
+++ b/spec/services/groups/import_export/import_service_spec.rb
@@ -3,17 +3,16 @@
require 'spec_helper'
describe Groups::ImportExport::ImportService do
- describe '#execute' do
+ context 'with group_import_ndjson feature flag disabled' do
let(:user) { create(:admin) }
let(:group) { create(:group) }
- let(:service) { described_class.new(group: group, user: user) }
- let(:import_file) { fixture_file_upload('spec/fixtures/group_export.tar.gz') }
-
let(:import_logger) { instance_double(Gitlab::Import::Logger) }
- subject { service.execute }
+ subject(:service) { described_class.new(group: group, user: user) }
before do
+ stub_feature_flags(group_import_ndjson: false)
+
ImportExportUpload.create(group: group, import_file: import_file)
allow(Gitlab::Import::Logger).to receive(:build).and_return(import_logger)
@@ -21,84 +20,227 @@ describe Groups::ImportExport::ImportService do
allow(import_logger).to receive(:info)
end
- context 'when user has correct permissions' do
- it 'imports group structure successfully' do
- expect(subject).to be_truthy
- end
+ context 'with a json file' do
+ let(:import_file) { fixture_file_upload('spec/fixtures/legacy_group_export.tar.gz') }
- it 'removes import file' do
- subject
+ it 'uses LegacyTreeRestorer to import the file' do
+ expect(Gitlab::ImportExport::Group::LegacyTreeRestorer).to receive(:new).and_call_original
- expect(group.import_export_upload.import_file.file).to be_nil
+ service.execute
end
+ end
- it 'logs the import success' do
- expect(import_logger).to receive(:info).with(
- group_id: group.id,
- group_name: group.name,
- message: 'Group Import/Export: Import succeeded'
- ).once
+ context 'with a ndjson file' do
+ let(:import_file) { fixture_file_upload('spec/fixtures/group_export.tar.gz') }
- subject
+ it 'fails to import' do
+ expect { service.execute }.to raise_error(Gitlab::ImportExport::Error, 'Incorrect JSON format')
end
end
+ end
+
+ context 'with group_import_ndjson feature flag enabled' do
+ before do
+ stub_feature_flags(group_import_ndjson: true)
+ end
+
+ context 'when importing a ndjson export' do
+ let(:user) { create(:admin) }
+ let(:group) { create(:group) }
+ let(:service) { described_class.new(group: group, user: user) }
+ let(:import_file) { fixture_file_upload('spec/fixtures/group_export.tar.gz') }
- context 'when user does not have correct permissions' do
- let(:user) { create(:user) }
+ let(:import_logger) { instance_double(Gitlab::Import::Logger) }
- it 'logs the error and raises an exception' do
- expect(import_logger).to receive(:error).with(
- group_id: group.id,
- group_name: group.name,
- message: a_string_including('Errors occurred')
- )
+ subject { service.execute }
- expect { subject }.to raise_error(Gitlab::ImportExport::Error)
+ before do
+ ImportExportUpload.create(group: group, import_file: import_file)
+
+ allow(Gitlab::Import::Logger).to receive(:build).and_return(import_logger)
+ allow(import_logger).to receive(:error)
+ allow(import_logger).to receive(:info)
end
- it 'tracks the error' do
- shared = Gitlab::ImportExport::Shared.new(group)
- allow(Gitlab::ImportExport::Shared).to receive(:new).and_return(shared)
+ context 'when user has correct permissions' do
+ it 'imports group structure successfully' do
+ expect(subject).to be_truthy
+ end
+
+ it 'removes import file' do
+ subject
- expect(shared).to receive(:error) do |param|
- expect(param.message).to include 'does not have required permissions for'
+ expect(group.import_export_upload.import_file.file).to be_nil
end
- expect { subject }.to raise_error(Gitlab::ImportExport::Error)
+ it 'logs the import success' do
+ expect(import_logger).to receive(:info).with(
+ group_id: group.id,
+ group_name: group.name,
+ message: 'Group Import/Export: Import succeeded'
+ ).once
+
+ subject
+ end
end
- end
- context 'when there are errors with the import file' do
- let(:import_file) { fixture_file_upload('spec/fixtures/symlink_export.tar.gz') }
+ context 'when user does not have correct permissions' do
+ let(:user) { create(:user) }
- it 'logs the error and raises an exception' do
- expect(import_logger).to receive(:error).with(
- group_id: group.id,
- group_name: group.name,
- message: a_string_including('Errors occurred')
- ).once
+ it 'logs the error and raises an exception' do
+ expect(import_logger).to receive(:error).with(
+ group_id: group.id,
+ group_name: group.name,
+ message: a_string_including('Errors occurred')
+ )
- expect { subject }.to raise_error(Gitlab::ImportExport::Error)
+ expect { subject }.to raise_error(Gitlab::ImportExport::Error)
+ end
+
+ it 'tracks the error' do
+ shared = Gitlab::ImportExport::Shared.new(group)
+ allow(Gitlab::ImportExport::Shared).to receive(:new).and_return(shared)
+
+ expect(shared).to receive(:error) do |param|
+ expect(param.message).to include 'does not have required permissions for'
+ end
+
+ expect { subject }.to raise_error(Gitlab::ImportExport::Error)
+ end
end
- end
- context 'when there are errors with the sub-relations' do
- let(:import_file) { fixture_file_upload('spec/fixtures/group_export_invalid_subrelations.tar.gz') }
+ context 'when there are errors with the import file' do
+ let(:import_file) { fixture_file_upload('spec/fixtures/symlink_export.tar.gz') }
+
+ it 'logs the error and raises an exception' do
+ expect(import_logger).to receive(:error).with(
+ group_id: group.id,
+ group_name: group.name,
+ message: a_string_including('Errors occurred')
+ ).once
+
+ expect { subject }.to raise_error(Gitlab::ImportExport::Error)
+ end
+ end
+
+ context 'when there are errors with the sub-relations' do
+ let(:import_file) { fixture_file_upload('spec/fixtures/group_export_invalid_subrelations.tar.gz') }
+
+ it 'successfully imports the group' do
+ expect(subject).to be_truthy
+ end
+
+ it 'logs the import success' do
+ allow(Gitlab::Import::Logger).to receive(:build).and_return(import_logger)
+
+ expect(import_logger).to receive(:info).with(
+ group_id: group.id,
+ group_name: group.name,
+ message: 'Group Import/Export: Import succeeded'
+ )
- it 'successfully imports the group' do
- expect(subject).to be_truthy
+ subject
+ end
end
+ end
+
+ context 'when importing a json export' do
+ let(:user) { create(:admin) }
+ let(:group) { create(:group) }
+ let(:service) { described_class.new(group: group, user: user) }
+ let(:import_file) { fixture_file_upload('spec/fixtures/legacy_group_export.tar.gz') }
+
+ let(:import_logger) { instance_double(Gitlab::Import::Logger) }
+
+ subject { service.execute }
+
+ before do
+ ImportExportUpload.create(group: group, import_file: import_file)
- it 'logs the import success' do
allow(Gitlab::Import::Logger).to receive(:build).and_return(import_logger)
+ allow(import_logger).to receive(:error)
+ allow(import_logger).to receive(:info)
+ end
- expect(import_logger).to receive(:info).with(
- group_id: group.id,
- group_name: group.name,
- message: 'Group Import/Export: Import succeeded'
- )
+ context 'when user has correct permissions' do
+ it 'imports group structure successfully' do
+ expect(subject).to be_truthy
+ end
+
+ it 'removes import file' do
+ subject
+
+ expect(group.import_export_upload.import_file.file).to be_nil
+ end
+
+ it 'logs the import success' do
+ expect(import_logger).to receive(:info).with(
+ group_id: group.id,
+ group_name: group.name,
+ message: 'Group Import/Export: Import succeeded'
+ ).once
+
+ subject
+ end
+ end
+
+ context 'when user does not have correct permissions' do
+ let(:user) { create(:user) }
+
+ it 'logs the error and raises an exception' do
+ expect(import_logger).to receive(:error).with(
+ group_id: group.id,
+ group_name: group.name,
+ message: a_string_including('Errors occurred')
+ )
+
+ expect { subject }.to raise_error(Gitlab::ImportExport::Error)
+ end
- subject
+ it 'tracks the error' do
+ shared = Gitlab::ImportExport::Shared.new(group)
+ allow(Gitlab::ImportExport::Shared).to receive(:new).and_return(shared)
+
+ expect(shared).to receive(:error) do |param|
+ expect(param.message).to include 'does not have required permissions for'
+ end
+
+ expect { subject }.to raise_error(Gitlab::ImportExport::Error)
+ end
+ end
+
+ context 'when there are errors with the import file' do
+ let(:import_file) { fixture_file_upload('spec/fixtures/legacy_symlink_export.tar.gz') }
+
+ it 'logs the error and raises an exception' do
+ expect(import_logger).to receive(:error).with(
+ group_id: group.id,
+ group_name: group.name,
+ message: a_string_including('Errors occurred')
+ ).once
+
+ expect { subject }.to raise_error(Gitlab::ImportExport::Error)
+ end
+ end
+
+ context 'when there are errors with the sub-relations' do
+ let(:import_file) { fixture_file_upload('spec/fixtures/legacy_group_export_invalid_subrelations.tar.gz') }
+
+ it 'successfully imports the group' do
+ expect(subject).to be_truthy
+ end
+
+ it 'logs the import success' do
+ allow(Gitlab::Import::Logger).to receive(:build).and_return(import_logger)
+
+ expect(import_logger).to receive(:info).with(
+ group_id: group.id,
+ group_name: group.name,
+ message: 'Group Import/Export: Import succeeded'
+ )
+
+ subject
+ end
end
end
end
diff --git a/spec/services/groups/update_service_spec.rb b/spec/services/groups/update_service_spec.rb
index 1aa7e06182b..b17d78505d1 100644
--- a/spec/services/groups/update_service_spec.rb
+++ b/spec/services/groups/update_service_spec.rb
@@ -148,6 +148,26 @@ describe Groups::UpdateService do
end
end
+ context 'updating default_branch_protection' do
+ let(:service) do
+ described_class.new(internal_group, user, default_branch_protection: Gitlab::Access::PROTECTION_NONE)
+ end
+
+ context 'for users who have the ability to update default_branch_protection' do
+ it 'updates the attribute' do
+ internal_group.add_owner(user)
+
+ expect { service.execute }.to change { internal_group.default_branch_protection }.to(Gitlab::Access::PROTECTION_NONE)
+ end
+ end
+
+ context 'for users who do not have the ability to update default_branch_protection' do
+ it 'does not update the attribute' do
+ expect { service.execute }.not_to change { internal_group.default_branch_protection }
+ end
+ end
+ end
+
context 'rename group' do
let!(:service) { described_class.new(internal_group, user, path: SecureRandom.hex) }
diff --git a/spec/services/incident_management/create_issue_service_spec.rb b/spec/services/incident_management/create_issue_service_spec.rb
index 4c7fb682193..5a3721f00b8 100644
--- a/spec/services/incident_management/create_issue_service_spec.rb
+++ b/spec/services/incident_management/create_issue_service_spec.rb
@@ -6,7 +6,7 @@ describe IncidentManagement::CreateIssueService do
let(:project) { create(:project, :repository, :private) }
let_it_be(:user) { User.alert_bot }
let(:service) { described_class.new(project, alert_payload) }
- let(:alert_starts_at) { Time.now }
+ let(:alert_starts_at) { Time.current }
let(:alert_title) { 'TITLE' }
let(:alert_annotations) { { title: alert_title } }
@@ -281,18 +281,28 @@ describe IncidentManagement::CreateIssueService do
setting.update!(create_issue: false)
end
- it 'returns an error' do
- expect(service)
- .to receive(:log_error)
- .with(error_message('setting disabled'))
+ context 'when skip_settings_check is false (default)' do
+ it 'returns an error' do
+ expect(service)
+ .to receive(:log_error)
+ .with(error_message('setting disabled'))
+
+ expect(subject).to eq(status: :error, message: 'setting disabled')
+ end
+ end
+
+ context 'when skip_settings_check is true' do
+ subject { service.execute(skip_settings_check: true) }
- expect(subject).to eq(status: :error, message: 'setting disabled')
+ it 'creates an issue' do
+ expect { subject }.to change(Issue, :count).by(1)
+ end
end
end
private
- def build_alert_payload(annotations: {}, starts_at: Time.now)
+ def build_alert_payload(annotations: {}, starts_at: Time.current)
{
'annotations' => annotations.stringify_keys
}.tap do |payload|
diff --git a/spec/services/issuable/clone/attributes_rewriter_spec.rb b/spec/services/issuable/clone/attributes_rewriter_spec.rb
index 9111b19d7b7..fb520f828fa 100644
--- a/spec/services/issuable/clone/attributes_rewriter_spec.rb
+++ b/spec/services/issuable/clone/attributes_rewriter_spec.rb
@@ -89,7 +89,7 @@ describe Issuable::Clone::AttributesRewriter do
create_event(milestone1_project1)
create_event(milestone2_project1)
- create_event(milestone1_project1, 'remove')
+ create_event(nil, 'remove')
create_event(milestone3_project1)
end
@@ -101,7 +101,7 @@ describe Issuable::Clone::AttributesRewriter do
expect_milestone_event(new_issue_milestone_events.first, milestone: milestone1_project2, action: 'add', state: 'opened')
expect_milestone_event(new_issue_milestone_events.second, milestone: milestone2_project2, action: 'add', state: 'opened')
- expect_milestone_event(new_issue_milestone_events.third, milestone: milestone1_project2, action: 'remove', state: 'opened')
+ expect_milestone_event(new_issue_milestone_events.third, milestone: nil, action: 'remove', state: 'opened')
end
def create_event(milestone, action = 'add')
@@ -109,10 +109,32 @@ describe Issuable::Clone::AttributesRewriter do
end
def expect_milestone_event(event, expected_attrs)
- expect(event.milestone_id).to eq(expected_attrs[:milestone].id)
+ expect(event.milestone_id).to eq(expected_attrs[:milestone]&.id)
expect(event.action).to eq(expected_attrs[:action])
expect(event.state).to eq(expected_attrs[:state])
end
end
+
+ context 'with existing state events' do
+ let!(:event1) { create(:resource_state_event, issue: original_issue, state: 'opened') }
+ let!(:event2) { create(:resource_state_event, issue: original_issue, state: 'closed') }
+ let!(:event3) { create(:resource_state_event, issue: original_issue, state: 'reopened') }
+
+ it 'copies existing state events as expected' do
+ subject.execute
+
+ state_events = new_issue.reload.resource_state_events
+ expect(state_events.size).to eq(3)
+
+ expect_state_event(state_events.first, issue: new_issue, state: 'opened')
+ expect_state_event(state_events.second, issue: new_issue, state: 'closed')
+ expect_state_event(state_events.third, issue: new_issue, state: 'reopened')
+ end
+
+ def expect_state_event(event, expected_attrs)
+ expect(event.issue_id).to eq(expected_attrs[:issue]&.id)
+ expect(event.state).to eq(expected_attrs[:state])
+ end
+ end
end
end
diff --git a/spec/services/issues/close_service_spec.rb b/spec/services/issues/close_service_spec.rb
index 86377e054c1..6fc1928d47b 100644
--- a/spec/services/issues/close_service_spec.rb
+++ b/spec/services/issues/close_service_spec.rb
@@ -146,7 +146,7 @@ describe Issues::CloseService do
context 'when `metrics.first_mentioned_in_commit_at` is already set' do
before do
- issue.metrics.update!(first_mentioned_in_commit_at: Time.now)
+ issue.metrics.update!(first_mentioned_in_commit_at: Time.current)
end
it 'does not update the metrics' do
diff --git a/spec/services/issues/create_service_spec.rb b/spec/services/issues/create_service_spec.rb
index bd50d6b1001..7a251e03e51 100644
--- a/spec/services/issues/create_service_spec.rb
+++ b/spec/services/issues/create_service_spec.rb
@@ -110,6 +110,31 @@ describe Issues::CreateService do
end
end
+ context 'when labels is nil' do
+ let(:opts) do
+ { title: 'Title',
+ description: 'Description',
+ labels: nil }
+ end
+
+ it 'does not assign label' do
+ expect(issue.labels).to be_empty
+ end
+ end
+
+ context 'when labels is nil and label_ids is present' do
+ let(:opts) do
+ { title: 'Title',
+ description: 'Description',
+ labels: nil,
+ label_ids: labels.map(&:id) }
+ end
+
+ it 'assigns group labels' do
+ expect(issue.labels).to match_array labels
+ end
+ end
+
context 'when milestone belongs to different project' do
let(:milestone) { create(:milestone) }
@@ -368,6 +393,8 @@ describe Issues::CreateService do
end
context 'checking spam' do
+ include_context 'includes Spam constants'
+
let(:title) { 'Legit issue' }
let(:description) { 'please fix' }
let(:opts) do
@@ -378,11 +405,13 @@ describe Issues::CreateService do
}
end
+ subject { described_class.new(project, user, opts) }
+
before do
stub_feature_flags(allow_possible_spam: false)
end
- context 'when recaptcha was verified' do
+ context 'when reCAPTCHA was verified' do
let(:log_user) { user }
let(:spam_logs) { create_list(:spam_log, 2, user: log_user, title: title) }
let(:target_spam_log) { spam_logs.last }
@@ -391,7 +420,7 @@ describe Issues::CreateService do
opts[:recaptcha_verified] = true
opts[:spam_log_id] = target_spam_log.id
- expect(Spam::AkismetService).not_to receive(:new)
+ expect(Spam::SpamVerdictService).not_to receive(:new)
end
it 'does not mark an issue as spam' do
@@ -402,7 +431,7 @@ describe Issues::CreateService do
expect(issue).to be_valid
end
- it 'does not assign a spam_log to an issue' do
+ it 'does not assign a spam_log to the issue' do
expect(issue.spam_log).to be_nil
end
@@ -419,17 +448,42 @@ describe Issues::CreateService do
end
end
- context 'when recaptcha was not verified' do
+ context 'when reCAPTCHA was not verified' do
before do
- expect_next_instance_of(Spam::SpamCheckService) do |spam_service|
+ expect_next_instance_of(Spam::SpamActionService) do |spam_service|
expect(spam_service).to receive_messages(check_for_spam?: true)
end
end
- context 'when akismet detects spam' do
+ context 'when SpamVerdictService requires reCAPTCHA' do
before do
- expect_next_instance_of(Spam::AkismetService) do |akismet_service|
- expect(akismet_service).to receive_messages(spam?: true)
+ expect_next_instance_of(Spam::SpamVerdictService) do |verdict_service|
+ expect(verdict_service).to receive(:execute).and_return(REQUIRE_RECAPTCHA)
+ end
+ end
+
+ it 'does not mark the issue as spam' do
+ expect(issue).not_to be_spam
+ end
+
+ it 'marks the issue as needing reCAPTCHA' do
+ expect(issue.needs_recaptcha?).to be_truthy
+ end
+
+ it 'invalidates the issue' do
+ expect(issue).to be_invalid
+ end
+
+ it 'creates a new spam_log' do
+ expect { issue }
+ .to have_spam_log(title: title, description: description, user_id: user.id, noteable_type: 'Issue')
+ end
+ end
+
+ context 'when SpamVerdictService disallows creation' do
+ before do
+ expect_next_instance_of(Spam::SpamVerdictService) do |verdict_service|
+ expect(verdict_service).to receive(:execute).and_return(DISALLOW)
end
end
@@ -438,6 +492,10 @@ describe Issues::CreateService do
expect(issue).to be_spam
end
+ it 'does not mark the issue as needing reCAPTCHA' do
+ expect(issue.needs_recaptcha?).to be_falsey
+ end
+
it 'invalidates the issue' do
expect(issue).to be_invalid
end
@@ -457,7 +515,11 @@ describe Issues::CreateService do
expect(issue).not_to be_spam
end
- it '​creates a valid issue' do
+ it 'does not mark the issue as needing reCAPTCHA' do
+ expect(issue.needs_recaptcha?).to be_falsey
+ end
+
+ it 'creates a valid issue' do
expect(issue).to be_valid
end
@@ -468,10 +530,10 @@ describe Issues::CreateService do
end
end
- context 'when akismet does not detect spam' do
+ context 'when the SpamVerdictService allows creation' do
before do
- expect_next_instance_of(Spam::AkismetService) do |akismet_service|
- expect(akismet_service).to receive_messages(spam?: false)
+ expect_next_instance_of(Spam::SpamVerdictService) do |verdict_service|
+ expect(verdict_service).to receive(:execute).and_return(ALLOW)
end
end
diff --git a/spec/services/issues/related_branches_service_spec.rb b/spec/services/issues/related_branches_service_spec.rb
index eae35f12560..9f72e499414 100644
--- a/spec/services/issues/related_branches_service_spec.rb
+++ b/spec/services/issues/related_branches_service_spec.rb
@@ -3,39 +3,103 @@
require 'spec_helper'
describe Issues::RelatedBranchesService do
- let(:user) { create(:admin) }
- let(:issue) { create(:issue) }
+ let_it_be(:developer) { create(:user) }
+ let_it_be(:issue) { create(:issue) }
+ let(:user) { developer }
subject { described_class.new(issue.project, user) }
+ before do
+ issue.project.add_developer(developer)
+ end
+
describe '#execute' do
+ let(:sha) { 'abcdef' }
+ let(:repo) { issue.project.repository }
+ let(:project) { issue.project }
+ let(:branch_info) { subject.execute(issue) }
+
+ def make_branch
+ double('Branch', dereferenced_target: double('Target', sha: sha))
+ end
+
before do
- allow(issue.project.repository).to receive(:branch_names).and_return(["mpempe", "#{issue.iid}mepmep", issue.to_branch_name, "#{issue.iid}-branch"])
+ allow(repo).to receive(:branch_names).and_return(branch_names)
end
- it "selects the right branches when there are no referenced merge requests" do
- expect(subject.execute(issue)).to eq([issue.to_branch_name, "#{issue.iid}-branch"])
+ context 'no branches are available' do
+ let(:branch_names) { [] }
+
+ it 'returns an empty array' do
+ expect(branch_info).to be_empty
+ end
end
- it "selects the right branches when there is a referenced merge request" do
- merge_request = create(:merge_request, { description: "Closes ##{issue.iid}",
- source_project: issue.project,
- source_branch: "#{issue.iid}-branch" })
- merge_request.create_cross_references!(user)
+ context 'branches are available' do
+ let(:missing_branch) { "#{issue.to_branch_name}-missing" }
+ let(:unreadable_branch_name) { "#{issue.to_branch_name}-unreadable" }
+ let(:pipeline) { build(:ci_pipeline, :success, project: project) }
+ let(:unreadable_pipeline) { build(:ci_pipeline, :running) }
+
+ let(:branch_names) do
+ [
+ generate(:branch),
+ "#{issue.iid}doesnt-match",
+ issue.to_branch_name,
+ missing_branch,
+ unreadable_branch_name
+ ]
+ end
+
+ before do
+ {
+ issue.to_branch_name => pipeline,
+ unreadable_branch_name => unreadable_pipeline
+ }.each do |name, pipeline|
+ allow(repo).to receive(:find_branch).with(name).and_return(make_branch)
+ allow(project).to receive(:pipeline_for).with(name, sha).and_return(pipeline)
+ end
+
+ allow(repo).to receive(:find_branch).with(missing_branch).and_return(nil)
+ end
+
+ it 'selects relevant branches, along with pipeline status where available' do
+ expect(branch_info).to contain_exactly(
+ { name: issue.to_branch_name, pipeline_status: an_instance_of(Gitlab::Ci::Status::Success) },
+ { name: missing_branch, pipeline_status: be_nil },
+ { name: unreadable_branch_name, pipeline_status: be_nil }
+ )
+ end
+
+ context 'the user has access to otherwise unreadable pipelines' do
+ let(:user) { create(:admin) }
+
+ it 'returns info a developer could not see' do
+ expect(branch_info.pluck(:pipeline_status)).to include(an_instance_of(Gitlab::Ci::Status::Running))
+ end
+ end
+
+ it 'excludes branches referenced in merge requests' do
+ merge_request = create(:merge_request, { description: "Closes #{issue.to_reference}",
+ source_project: issue.project,
+ source_branch: issue.to_branch_name })
+ merge_request.create_cross_references!(user)
- referenced_merge_requests = Issues::ReferencedMergeRequestsService
- .new(issue.project, user)
- .referenced_merge_requests(issue)
+ referenced_merge_requests = Issues::ReferencedMergeRequestsService
+ .new(issue.project, user)
+ .referenced_merge_requests(issue)
- expect(referenced_merge_requests).not_to be_empty
- expect(subject.execute(issue)).to eq([issue.to_branch_name])
+ expect(referenced_merge_requests).not_to be_empty
+ expect(branch_info.pluck(:name)).not_to include(merge_request.source_branch)
+ end
end
- it 'excludes stable branches from the related branches' do
- allow(issue.project.repository).to receive(:branch_names)
- .and_return(["#{issue.iid}-0-stable"])
+ context 'one of the branches is stable' do
+ let(:branch_names) { ["#{issue.iid}-0-stable"] }
- expect(subject.execute(issue)).to eq []
+ it 'is excluded' do
+ expect(branch_info).to be_empty
+ end
end
end
end
diff --git a/spec/services/issues/resolve_discussions_spec.rb b/spec/services/issues/resolve_discussions_spec.rb
index f12a3820b8d..ec6624db6fc 100644
--- a/spec/services/issues/resolve_discussions_spec.rb
+++ b/spec/services/issues/resolve_discussions_spec.rb
@@ -3,19 +3,20 @@
require 'spec_helper.rb'
describe Issues::ResolveDiscussions do
- class DummyService < Issues::BaseService
- include ::Issues::ResolveDiscussions
-
- def initialize(*args)
- super
- filter_resolve_discussion_params
- end
- end
-
let(:project) { create(:project, :repository) }
let(:user) { create(:user) }
before do
+ stub_const('DummyService', Class.new(Issues::BaseService))
+ DummyService.class_eval do
+ include ::Issues::ResolveDiscussions
+
+ def initialize(*args)
+ super
+ filter_resolve_discussion_params
+ end
+ end
+
project.add_developer(user)
end
diff --git a/spec/services/issues/update_service_spec.rb b/spec/services/issues/update_service_spec.rb
index c32bef5a1a5..80039049bc3 100644
--- a/spec/services/issues/update_service_spec.rb
+++ b/spec/services/issues/update_service_spec.rb
@@ -510,7 +510,7 @@ describe Issues::UpdateService, :mailer do
end
it 'updates updated_at' do
- expect(issue.reload.updated_at).to be > Time.now
+ expect(issue.reload.updated_at).to be > Time.current
end
end
end
@@ -842,5 +842,33 @@ describe Issues::UpdateService, :mailer do
let(:open_issuable) { issue }
let(:closed_issuable) { create(:closed_issue, project: project) }
end
+
+ context 'real-time updates' do
+ let(:update_params) { { assignee_ids: [user2.id] } }
+
+ context 'when broadcast_issue_updates is enabled' do
+ before do
+ stub_feature_flags(broadcast_issue_updates: true)
+ end
+
+ it 'broadcasts to the issues channel' do
+ expect(IssuesChannel).to receive(:broadcast_to).with(issue, event: 'updated')
+
+ update_issue(update_params)
+ end
+ end
+
+ context 'when broadcast_issue_updates is disabled' do
+ before do
+ stub_feature_flags(broadcast_issue_updates: false)
+ end
+
+ it 'does not broadcast to the issues channel' do
+ expect(IssuesChannel).not_to receive(:broadcast_to)
+
+ update_issue(update_params)
+ end
+ end
+ end
end
end
diff --git a/spec/services/jira_import/start_import_service_spec.rb b/spec/services/jira_import/start_import_service_spec.rb
index 90f38945a9f..759e4f3363f 100644
--- a/spec/services/jira_import/start_import_service_spec.rb
+++ b/spec/services/jira_import/start_import_service_spec.rb
@@ -3,113 +3,89 @@
require 'spec_helper'
describe JiraImport::StartImportService do
+ include JiraServiceHelper
+
let_it_be(:user) { create(:user) }
let_it_be(:project, reload: true) { create(:project) }
let(:key) { 'KEY' }
subject { described_class.new(user, project, key).execute }
- context 'when feature flag disabled' do
+ context 'when an error is returned from the project validation' do
before do
- stub_feature_flags(jira_issue_import: false)
+ allow(project).to receive(:validate_jira_import_settings!)
+ .and_raise(Projects::ImportService::Error, 'Jira import feature is disabled.')
end
it_behaves_like 'responds with error', 'Jira import feature is disabled.'
end
- context 'when feature flag enabled' do
+ context 'when project validation is ok' do
+ let!(:jira_service) { create(:jira_service, project: project, active: true) }
+
before do
- stub_feature_flags(jira_issue_import: true)
+ stub_jira_service_test
+ allow(project).to receive(:validate_jira_import_settings!)
end
- context 'when user does not have permissions to run the import' do
- before do
- create(:jira_service, project: project, active: true)
+ context 'when Jira project key is not provided' do
+ let(:key) { '' }
- project.add_developer(user)
- end
-
- it_behaves_like 'responds with error', 'You do not have permissions to run the import.'
+ it_behaves_like 'responds with error', 'Unable to find Jira project to import data from.'
end
- context 'when user has permission to run import' do
- before do
- project.add_maintainer(user)
- end
+ context 'when correct data provided' do
+ let(:fake_key) { 'some-key' }
- context 'when Jira service was not setup' do
- it_behaves_like 'responds with error', 'Jira integration not configured.'
- end
+ subject { described_class.new(user, project, fake_key).execute }
- context 'when Jira service exists' do
- let!(:jira_service) { create(:jira_service, project: project, active: true) }
+ context 'when import is already running' do
+ let_it_be(:jira_import_state) { create(:jira_import_state, :started, project: project) }
- context 'when Jira project key is not provided' do
- let(:key) { '' }
+ it_behaves_like 'responds with error', 'Jira import is already running.'
+ end
- it_behaves_like 'responds with error', 'Unable to find Jira project to import data from.'
+ context 'when everything is ok' do
+ it 'returns success response' do
+ expect(subject).to be_a(ServiceResponse)
+ expect(subject).to be_success
end
- context 'when issues feature are disabled' do
- let_it_be(:project, reload: true) { create(:project, :issues_disabled) }
+ it 'schedules Jira import' do
+ subject
- it_behaves_like 'responds with error', 'Cannot import because issues are not available in this project.'
+ expect(project.latest_jira_import).to be_scheduled
end
- context 'when correct data provided' do
- let(:fake_key) { 'some-key' }
-
- subject { described_class.new(user, project, fake_key).execute }
-
- context 'when import is already running' do
- let_it_be(:jira_import_state) { create(:jira_import_state, :started, project: project) }
+ it 'creates Jira import data' do
+ jira_import = subject.payload[:import_data]
- it_behaves_like 'responds with error', 'Jira import is already running.'
- end
-
- context 'when everything is ok' do
- it 'returns success response' do
- expect(subject).to be_a(ServiceResponse)
- expect(subject).to be_success
- end
-
- it 'schedules jira import' do
- subject
-
- expect(project.latest_jira_import).to be_scheduled
- end
- end
-
- it 'creates jira import data' do
- jira_import = subject.payload[:import_data]
-
- expect(jira_import.jira_project_xid).to eq(0)
- expect(jira_import.jira_project_name).to eq(fake_key)
- expect(jira_import.jira_project_key).to eq(fake_key)
- expect(jira_import.user).to eq(user)
- end
+ expect(jira_import.jira_project_xid).to eq(0)
+ expect(jira_import.jira_project_name).to eq(fake_key)
+ expect(jira_import.jira_project_key).to eq(fake_key)
+ expect(jira_import.user).to eq(user)
+ end
- it 'creates jira import label' do
- expect { subject }.to change { Label.count }.by(1)
- end
+ it 'creates Jira import label' do
+ expect { subject }.to change { Label.count }.by(1)
+ end
- it 'creates jira label title with correct number' do
- jira_import = subject.payload[:import_data]
+ it 'creates Jira label title with correct number' do
+ jira_import = subject.payload[:import_data]
- label_title = "jira-import::#{jira_import.jira_project_key}-1"
- expect(jira_import.label.title).to eq(label_title)
- end
+ label_title = "jira-import::#{jira_import.jira_project_key}-1"
+ expect(jira_import.label.title).to eq(label_title)
+ end
+ end
- context 'when multiple jira imports for same jira project' do
- let!(:jira_imports) { create_list(:jira_import_state, 3, :finished, project: project, jira_project_key: fake_key)}
+ context 'when multiple Jira imports for same Jira project' do
+ let!(:jira_imports) { create_list(:jira_import_state, 3, :finished, project: project, jira_project_key: fake_key)}
- it 'creates jira label title with correct number' do
- jira_import = subject.payload[:import_data]
+ it 'creates Jira label title with correct number' do
+ jira_import = subject.payload[:import_data]
- label_title = "jira-import::#{jira_import.jira_project_key}-4"
- expect(jira_import.label.title).to eq(label_title)
- end
- end
+ label_title = "jira-import::#{jira_import.jira_project_key}-4"
+ expect(jira_import.label.title).to eq(label_title)
end
end
end
diff --git a/spec/services/lfs/file_transformer_spec.rb b/spec/services/lfs/file_transformer_spec.rb
index 9973d64930b..13d9c369c42 100644
--- a/spec/services/lfs/file_transformer_spec.rb
+++ b/spec/services/lfs/file_transformer_spec.rb
@@ -81,6 +81,23 @@ describe Lfs::FileTransformer do
expect(LfsObject.last.file.read).to eq file_content
end
+
+ context 'when repository is a design repository' do
+ let(:file_path) { "/#{DesignManagement.designs_directory}/test_file.lfs" }
+ let(:repository) { project.design_repository }
+
+ it "creates an LfsObject with the file's content" do
+ subject.new_file(file_path, file)
+
+ expect(LfsObject.last.file.read).to eq(file_content)
+ end
+
+ it 'saves the correct repository_type to LfsObjectsProject' do
+ subject.new_file(file_path, file)
+
+ expect(project.lfs_objects_projects.first.repository_type).to eq('design')
+ end
+ end
end
context "when doesn't use LFS" do
diff --git a/spec/services/merge_requests/create_service_spec.rb b/spec/services/merge_requests/create_service_spec.rb
index dc34546a599..9155db16d17 100644
--- a/spec/services/merge_requests/create_service_spec.rb
+++ b/spec/services/merge_requests/create_service_spec.rb
@@ -224,19 +224,6 @@ describe MergeRequests::CreateService, :clean_gitlab_redis_shared_state do
end
end
- context 'when ci_use_merge_request_ref feature flag is false' do
- before do
- stub_feature_flags(ci_use_merge_request_ref: false)
- end
-
- it 'create legacy detached merge request pipeline for non-fork merge request' do
- merge_request.reload
-
- expect(merge_request.actual_head_pipeline)
- .to be_legacy_detached_merge_request_pipeline
- end
- end
-
context 'when there are no commits between source branch and target branch' do
let(:opts) do
{
diff --git a/spec/services/merge_requests/merge_service_spec.rb b/spec/services/merge_requests/merge_service_spec.rb
index fa7f745d8a0..bcad822b1dc 100644
--- a/spec/services/merge_requests/merge_service_spec.rb
+++ b/spec/services/merge_requests/merge_service_spec.rb
@@ -118,7 +118,7 @@ describe MergeRequests::MergeService do
it 'closes GitLab issue tracker issues' do
issue = create :issue, project: project
- commit = instance_double('commit', safe_message: "Fixes #{issue.to_reference}", date: Time.now, authored_date: Time.now)
+ commit = instance_double('commit', safe_message: "Fixes #{issue.to_reference}", date: Time.current, authored_date: Time.current)
allow(merge_request).to receive(:commits).and_return([commit])
merge_request.cache_merge_request_closes_issues!
diff --git a/spec/services/merge_requests/rebase_service_spec.rb b/spec/services/merge_requests/rebase_service_spec.rb
index 22df3b84243..69d555f838d 100644
--- a/spec/services/merge_requests/rebase_service_spec.rb
+++ b/spec/services/merge_requests/rebase_service_spec.rb
@@ -72,12 +72,15 @@ describe MergeRequests::RebaseService do
it_behaves_like 'sequence of failure and success'
context 'when unexpected error occurs' do
+ let(:exception) { RuntimeError.new('Something went wrong') }
+ let(:merge_request_ref) { merge_request.to_reference(full: true) }
+
before do
- allow(repository).to receive(:gitaly_operation_client).and_raise('Something went wrong')
+ allow(repository).to receive(:gitaly_operation_client).and_raise(exception)
end
it 'saves a generic error message' do
- subject.execute(merge_request)
+ service.execute(merge_request)
expect(merge_request.reload.merge_error).to eq(described_class::REBASE_ERROR)
end
@@ -86,6 +89,18 @@ describe MergeRequests::RebaseService do
expect(service.execute(merge_request)).to match(status: :error,
message: described_class::REBASE_ERROR)
end
+
+ it 'logs the error' do
+ expect(service).to receive(:log_error).with(exception: exception, message: described_class::REBASE_ERROR, save_message_on_model: true).and_call_original
+ expect(Gitlab::ErrorTracking).to receive(:track_exception).with(exception,
+ class: described_class.to_s,
+ merge_request: merge_request_ref,
+ merge_request_id: merge_request.id,
+ message: described_class::REBASE_ERROR,
+ save_message_on_model: true).and_call_original
+
+ service.execute(merge_request)
+ end
end
context 'with git command failure' do
diff --git a/spec/services/merge_requests/refresh_service_spec.rb b/spec/services/merge_requests/refresh_service_spec.rb
index 4f052fa3edb..94e65d895ac 100644
--- a/spec/services/merge_requests/refresh_service_spec.rb
+++ b/spec/services/merge_requests/refresh_service_spec.rb
@@ -94,6 +94,31 @@ describe MergeRequests::RefreshService do
expect(@fork_build_failed_todo).to be_done
end
+ context 'when a merge error exists' do
+ let(:error_message) { 'This is a merge error' }
+
+ before do
+ @merge_request = create(:merge_request,
+ source_project: @project,
+ source_branch: 'feature',
+ target_branch: 'master',
+ target_project: @project,
+ merge_error: error_message)
+ end
+
+ it 'clears merge errors when pushing to the source branch' do
+ expect { refresh_service.execute(@oldrev, @newrev, 'refs/heads/feature') }
+ .to change { @merge_request.reload.merge_error }
+ .from(error_message)
+ .to(nil)
+ end
+
+ it 'does not clear merge errors when pushing to the target branch' do
+ expect { refresh_service.execute(@oldrev, @newrev, 'refs/heads/master') }
+ .not_to change { @merge_request.reload.merge_error }
+ end
+ end
+
it 'reloads source branch MRs memoization' do
refresh_service.execute(@oldrev, @newrev, 'refs/heads/master')
@@ -209,19 +234,6 @@ describe MergeRequests::RefreshService do
end
end
- context 'when ci_use_merge_request_ref feature flag is false' do
- before do
- stub_feature_flags(ci_use_merge_request_ref: false)
- end
-
- it 'create legacy detached merge request pipeline for non-fork merge request' do
- subject
-
- expect(@merge_request.pipelines_for_merge_request.first)
- .to be_legacy_detached_merge_request_pipeline
- end
- end
-
context "when branch pipeline was created before a detaced merge request pipeline has been created" do
before do
create(:ci_pipeline, project: @merge_request.source_project,
@@ -623,7 +635,7 @@ describe MergeRequests::RefreshService do
references: [issue],
author_name: commit_author.name,
author_email: commit_author.email,
- committed_date: Time.now
+ committed_date: Time.current
)
allow_any_instance_of(MergeRequest).to receive(:commits).and_return(CommitCollection.new(@project, [commit], 'feature'))
diff --git a/spec/services/merge_requests/squash_service_spec.rb b/spec/services/merge_requests/squash_service_spec.rb
index cb278eec692..a53314ed737 100644
--- a/spec/services/merge_requests/squash_service_spec.rb
+++ b/spec/services/merge_requests/squash_service_spec.rb
@@ -141,15 +141,14 @@ describe MergeRequests::SquashService do
let(:merge_request) { merge_request_with_only_new_files }
let(:error) { 'A test error' }
- context 'with gitaly enabled' do
+ context 'with an error in Gitaly UserSquash RPC' do
before do
allow(repository.gitaly_operation_client).to receive(:user_squash)
.and_raise(Gitlab::Git::Repository::GitError, error)
end
- it 'logs the stage and output' do
- expect(service).to receive(:log_error).with(log_error)
- expect(service).to receive(:log_error).with(error)
+ it 'logs the error' do
+ expect(service).to receive(:log_error).with(exception: an_instance_of(Gitlab::Git::Repository::GitError), message: 'Failed to squash merge request')
service.execute
end
@@ -158,19 +157,42 @@ describe MergeRequests::SquashService do
expect(service.execute).to match(status: :error, message: a_string_including('squash'))
end
end
+
+ context 'with an error in squash in progress check' do
+ before do
+ allow(repository).to receive(:squash_in_progress?)
+ .and_raise(Gitlab::Git::Repository::GitError, error)
+ end
+
+ it 'logs the stage and output' do
+ expect(service).to receive(:log_error).with(exception: an_instance_of(Gitlab::Git::Repository::GitError), message: 'Failed to check squash in progress')
+
+ service.execute
+ end
+
+ it 'returns an error' do
+ expect(service.execute).to match(status: :error, message: 'An error occurred while checking whether another squash is in progress.')
+ end
+ end
end
context 'when any other exception is thrown' do
let(:merge_request) { merge_request_with_only_new_files }
- let(:error) { 'A test error' }
+ let(:merge_request_ref) { merge_request.to_reference(full: true) }
+ let(:exception) { RuntimeError.new('A test error') }
before do
- allow(merge_request.target_project.repository).to receive(:squash).and_raise(error)
+ allow(merge_request.target_project.repository).to receive(:squash).and_raise(exception)
end
- it 'logs the MR reference and exception' do
- expect(service).to receive(:log_error).with(a_string_including("#{project.full_path}#{merge_request.to_reference}"))
- expect(service).to receive(:log_error).with(error)
+ it 'logs the error' do
+ expect(service).to receive(:log_error).with(exception: exception, message: 'Failed to squash merge request').and_call_original
+ expect(Gitlab::ErrorTracking).to receive(:track_exception).with(exception,
+ class: described_class.to_s,
+ merge_request: merge_request_ref,
+ merge_request_id: merge_request.id,
+ message: 'Failed to squash merge request',
+ save_message_on_model: false).and_call_original
service.execute
end
diff --git a/spec/services/merge_requests/update_service_spec.rb b/spec/services/merge_requests/update_service_spec.rb
index 8c1800c495f..2b934b24757 100644
--- a/spec/services/merge_requests/update_service_spec.rb
+++ b/spec/services/merge_requests/update_service_spec.rb
@@ -92,6 +92,7 @@ describe MergeRequests::UpdateService, :mailer do
labels: [],
mentioned_users: [user2],
assignees: [user3],
+ milestone: nil,
total_time_spent: 0,
description: "FYI #{user2.to_reference}"
}
@@ -452,7 +453,7 @@ describe MergeRequests::UpdateService, :mailer do
end
it 'updates updated_at' do
- expect(merge_request.reload.updated_at).to be > Time.now
+ expect(merge_request.reload.updated_at).to be > Time.current
end
end
diff --git a/spec/services/metrics/dashboard/clone_dashboard_service_spec.rb b/spec/services/metrics/dashboard/clone_dashboard_service_spec.rb
index b386159541a..3d26ab2ede5 100644
--- a/spec/services/metrics/dashboard/clone_dashboard_service_spec.rb
+++ b/spec/services/metrics/dashboard/clone_dashboard_service_spec.rb
@@ -5,8 +5,6 @@ require 'spec_helper'
describe Metrics::Dashboard::CloneDashboardService, :use_clean_rails_memory_store_caching do
include MetricsDashboardHelpers
- STAGES = ::Gitlab::Metrics::Dashboard::Stages
-
let_it_be(:user) { create(:user) }
let_it_be(:project) { create(:project, :repository) }
let_it_be(:environment) { create(:environment, project: project) }
@@ -83,7 +81,7 @@ describe Metrics::Dashboard::CloneDashboardService, :use_clean_rails_memory_stor
allow(::Gitlab::Metrics::Dashboard::Processor).to receive(:new).and_return(double(process: file_content_hash))
end
- it_behaves_like 'valid dashboard cloning process', ::Metrics::Dashboard::SystemDashboardService::DASHBOARD_PATH, [STAGES::CommonMetricsInserter, STAGES::CustomMetricsInserter, STAGES::Sorter]
+ it_behaves_like 'valid dashboard cloning process', ::Metrics::Dashboard::SystemDashboardService::DASHBOARD_PATH, [::Gitlab::Metrics::Dashboard::Stages::CommonMetricsInserter, ::Gitlab::Metrics::Dashboard::Stages::CustomMetricsInserter, ::Gitlab::Metrics::Dashboard::Stages::Sorter]
context 'selected branch already exists' do
let(:branch) { 'existing_branch' }
diff --git a/spec/services/metrics/dashboard/grafana_metric_embed_service_spec.rb b/spec/services/metrics/dashboard/grafana_metric_embed_service_spec.rb
index 034d6aba5d6..3547e1f0f8c 100644
--- a/spec/services/metrics/dashboard/grafana_metric_embed_service_spec.rb
+++ b/spec/services/metrics/dashboard/grafana_metric_embed_service_spec.rb
@@ -154,7 +154,7 @@ describe Metrics::Dashboard::GrafanaMetricEmbedService do
context 'when value not present in cache' do
it 'returns nil' do
- expect(ReactiveCachingWorker)
+ expect(ExternalServiceReactiveCachingWorker)
.to receive(:perform_async)
.with(service.class, service.id, *cache_params)
@@ -217,7 +217,7 @@ describe Metrics::Dashboard::DatasourceNameParser do
include GrafanaApiHelpers
let(:grafana_url) { valid_grafana_dashboard_link('https://gitlab.grafana.net') }
- let(:grafana_dashboard) { JSON.parse(fixture_file('grafana/dashboard_response.json'), symbolize_names: true) }
+ let(:grafana_dashboard) { Gitlab::Json.parse(fixture_file('grafana/dashboard_response.json'), symbolize_names: true) }
subject { described_class.new(grafana_url, grafana_dashboard).parse }
diff --git a/spec/services/metrics/dashboard/transient_embed_service_spec.rb b/spec/services/metrics/dashboard/transient_embed_service_spec.rb
index 4982f56cddc..125fff7c23c 100644
--- a/spec/services/metrics/dashboard/transient_embed_service_spec.rb
+++ b/spec/services/metrics/dashboard/transient_embed_service_spec.rb
@@ -67,6 +67,12 @@ describe Metrics::Dashboard::TransientEmbedService, :use_clean_rails_memory_stor
expect(get_type_for_embed(alt_embed)).to eq('area-chart')
end
+ context 'when embed_json cannot be parsed as json' do
+ let(:embed_json) { '' }
+
+ it_behaves_like 'misconfigured dashboard service response', :unprocessable_entity
+ end
+
private
def get_embed_json(type = 'line-graph')
diff --git a/spec/services/metrics/users_starred_dashboards/create_service_spec.rb b/spec/services/metrics/users_starred_dashboards/create_service_spec.rb
new file mode 100644
index 00000000000..eac4965ba44
--- /dev/null
+++ b/spec/services/metrics/users_starred_dashboards/create_service_spec.rb
@@ -0,0 +1,72 @@
+# frozen_string_literal: true
+
+require 'spec_helper'
+
+describe Metrics::UsersStarredDashboards::CreateService do
+ let_it_be(:user) { create(:user) }
+ let(:dashboard_path) { 'config/prometheus/common_metrics.yml' }
+ let(:service_instance) { described_class.new(user, project, dashboard_path) }
+ let(:project) { create(:project) }
+ let(:starred_dashboard_params) do
+ {
+ user: user,
+ project: project,
+ dashboard_path: dashboard_path
+ }
+ end
+
+ shared_examples 'prevented starred dashboard creation' do |message|
+ it 'returns error response', :aggregate_failures do
+ expect(Metrics::UsersStarredDashboard).not_to receive(:new)
+
+ response = service_instance.execute
+
+ expect(response.status).to be :error
+ expect(response.message).to eql message
+ end
+ end
+
+ describe '.execute' do
+ context 'with anonymous user' do
+ it_behaves_like 'prevented starred dashboard creation', 'You are not authorized to add star to this dashboard'
+ end
+
+ context 'with reporter user' do
+ before do
+ project.add_reporter(user)
+ end
+
+ context 'incorrect dashboard_path' do
+ let(:dashboard_path) { 'something_incorrect.yml' }
+
+ it_behaves_like 'prevented starred dashboard creation', 'Dashboard with requested path can not be found'
+ end
+
+ context 'with valid dashboard path' do
+ it 'creates starred dashboard and returns success response', :aggregate_failures do
+ expect_next_instance_of(Metrics::UsersStarredDashboard, starred_dashboard_params) do |starred_dashboard|
+ expect(starred_dashboard).to receive(:save).and_return true
+ end
+
+ response = service_instance.execute
+
+ expect(response.status).to be :success
+ end
+
+ context 'Metrics::UsersStarredDashboard has validation errors' do
+ it 'returns error response', :aggregate_failures do
+ expect_next_instance_of(Metrics::UsersStarredDashboard, starred_dashboard_params) do |starred_dashboard|
+ expect(starred_dashboard).to receive(:save).and_return(false)
+ expect(starred_dashboard).to receive(:errors).and_return(double(messages: { base: ['Model validation error'] }))
+ end
+
+ response = service_instance.execute
+
+ expect(response.status).to be :error
+ expect(response.message).to eql(base: ['Model validation error'])
+ end
+ end
+ end
+ end
+ end
+end
diff --git a/spec/services/metrics/users_starred_dashboards/delete_service_spec.rb b/spec/services/metrics/users_starred_dashboards/delete_service_spec.rb
new file mode 100644
index 00000000000..68a2fef5931
--- /dev/null
+++ b/spec/services/metrics/users_starred_dashboards/delete_service_spec.rb
@@ -0,0 +1,41 @@
+# frozen_string_literal: true
+
+require 'spec_helper'
+
+describe Metrics::UsersStarredDashboards::DeleteService do
+ subject(:service_instance) { described_class.new(user, project, dashboard_path) }
+
+ let_it_be(:user) { create(:user) }
+ let_it_be(:project) { create(:project) }
+
+ describe '#execute' do
+ let_it_be(:user_starred_dashboard_1) { create(:metrics_users_starred_dashboard, user: user, project: project, dashboard_path: 'dashboard_1') }
+ let_it_be(:user_starred_dashboard_2) { create(:metrics_users_starred_dashboard, user: user, project: project) }
+ let_it_be(:other_user_starred_dashboard) { create(:metrics_users_starred_dashboard, project: project) }
+ let_it_be(:other_project_starred_dashboard) { create(:metrics_users_starred_dashboard, user: user) }
+
+ context 'without dashboard_path' do
+ let(:dashboard_path) { nil }
+
+ it 'does not scope user starred dashboards by dashboard path' do
+ result = service_instance.execute
+
+ expect(result.success?).to be_truthy
+ expect(result.payload[:deleted_rows]).to be(2)
+ expect(Metrics::UsersStarredDashboard.all).to contain_exactly(other_user_starred_dashboard, other_project_starred_dashboard)
+ end
+ end
+
+ context 'with dashboard_path' do
+ let(:dashboard_path) { 'dashboard_1' }
+
+ it 'does scope user starred dashboards by dashboard path' do
+ result = service_instance.execute
+
+ expect(result.success?).to be_truthy
+ expect(result.payload[:deleted_rows]).to be(1)
+ expect(Metrics::UsersStarredDashboard.all).to contain_exactly(user_starred_dashboard_2, other_user_starred_dashboard, other_project_starred_dashboard)
+ end
+ end
+ end
+end
diff --git a/spec/services/namespaces/check_storage_size_service_spec.rb b/spec/services/namespaces/check_storage_size_service_spec.rb
new file mode 100644
index 00000000000..50359ef90ab
--- /dev/null
+++ b/spec/services/namespaces/check_storage_size_service_spec.rb
@@ -0,0 +1,159 @@
+# frozen_string_literal: true
+
+require 'spec_helper'
+
+describe Namespaces::CheckStorageSizeService, '#execute' do
+ let(:namespace) { build_stubbed(:namespace) }
+ let(:user) { build(:user, namespace: namespace) }
+ let(:service) { described_class.new(namespace, user) }
+ let(:current_size) { 150.megabytes }
+ let(:limit) { 100.megabytes }
+
+ subject(:response) { service.execute }
+
+ before do
+ allow(namespace).to receive(:root_ancestor).and_return(namespace)
+
+ root_storage_size = instance_double("RootStorageSize",
+ current_size: current_size,
+ limit: limit,
+ usage_ratio: limit == 0 ? 0 : current_size.to_f / limit.to_f,
+ above_size_limit?: current_size > limit
+ )
+
+ expect(Namespace::RootStorageSize).to receive(:new).and_return(root_storage_size)
+ end
+
+ context 'feature flag' do
+ it 'is successful when disabled' do
+ stub_feature_flags(namespace_storage_limit: false)
+
+ expect(response).to be_success
+ end
+
+ it 'errors when enabled' do
+ stub_feature_flags(namespace_storage_limit: true)
+
+ expect(response).to be_error
+ end
+
+ it 'is successful when feature flag is activated for another namespace' do
+ stub_feature_flags(namespace_storage_limit: build(:namespace))
+
+ expect(response).to be_success
+ end
+
+ it 'errors when feature flag is activated for the current namespace' do
+ stub_feature_flags(namespace_storage_limit: namespace )
+
+ expect(response).to be_error
+ expect(response.message).to be_present
+ end
+ end
+
+ context 'when limit is set to 0' do
+ let(:limit) { 0 }
+
+ it 'is successful and has no payload' do
+ expect(response).to be_success
+ expect(response.payload).to be_empty
+ end
+ end
+
+ context 'when current size is below threshold' do
+ let(:current_size) { 10.megabytes }
+
+ it 'is successful and has no payload' do
+ expect(response).to be_success
+ expect(response.payload).to be_empty
+ end
+ end
+
+ context 'when not admin of the namespace' do
+ let(:other_namespace) { build_stubbed(:namespace) }
+
+ subject(:response) { described_class.new(other_namespace, user).execute }
+
+ before do
+ allow(other_namespace).to receive(:root_ancestor).and_return(other_namespace)
+ end
+
+ it 'errors and has no payload' do
+ expect(response).to be_error
+ expect(response.payload).to be_empty
+ end
+ end
+
+ context 'when providing the child namespace' do
+ let(:namespace) { build_stubbed(:group) }
+ let(:child_namespace) { build_stubbed(:group, parent: namespace) }
+
+ subject(:response) { described_class.new(child_namespace, user).execute }
+
+ before do
+ allow(child_namespace).to receive(:root_ancestor).and_return(namespace)
+ namespace.add_owner(user)
+ end
+
+ it 'uses the root namespace' do
+ expect(response).to be_error
+ end
+ end
+
+ describe 'payload alert_level' do
+ subject { service.execute.payload[:alert_level] }
+
+ context 'when above info threshold' do
+ let(:current_size) { 50.megabytes }
+
+ it { is_expected.to eq(:info) }
+ end
+
+ context 'when above warning threshold' do
+ let(:current_size) { 75.megabytes }
+
+ it { is_expected.to eq(:warning) }
+ end
+
+ context 'when above alert threshold' do
+ let(:current_size) { 95.megabytes }
+
+ it { is_expected.to eq(:alert) }
+ end
+
+ context 'when above error threshold' do
+ let(:current_size) { 100.megabytes }
+
+ it { is_expected.to eq(:error) }
+ end
+ end
+
+ describe 'payload explanation_message' do
+ subject(:response) { service.execute.payload[:explanation_message] }
+
+ context 'when above limit' do
+ let(:current_size) { 110.megabytes }
+
+ it 'returns message with read-only warning' do
+ expect(response).to include("#{namespace.name} is now read-only")
+ end
+ end
+
+ context 'when below limit' do
+ let(:current_size) { 60.megabytes }
+
+ it { is_expected.to include('If you reach 100% storage capacity') }
+ end
+ end
+
+ describe 'payload usage_message' do
+ let(:current_size) { 60.megabytes }
+
+ subject(:response) { service.execute.payload[:usage_message] }
+
+ it 'returns current usage information' do
+ expect(response).to include("60 MB of 100 MB")
+ expect(response).to include("60%")
+ end
+ end
+end
diff --git a/spec/services/note_summary_spec.rb b/spec/services/note_summary_spec.rb
index aa4e41f4d8c..038e0cdb703 100644
--- a/spec/services/note_summary_spec.rb
+++ b/spec/services/note_summary_spec.rb
@@ -25,18 +25,18 @@ describe NoteSummary do
it 'returns note hash' do
Timecop.freeze do
expect(create_note_summary.note).to eq(noteable: noteable, project: project, author: user, note: 'note',
- created_at: Time.now)
+ created_at: Time.current)
end
end
context 'when noteable is a commit' do
- let(:noteable) { build(:commit, system_note_timestamp: Time.at(43)) }
+ let(:noteable) { build(:commit, system_note_timestamp: Time.zone.at(43)) }
it 'returns note hash specific to commit' do
expect(create_note_summary.note).to eq(
noteable: nil, project: project, author: user, note: 'note',
noteable_type: 'Commit', commit_id: noteable.id,
- created_at: Time.at(43)
+ created_at: Time.zone.at(43)
)
end
end
diff --git a/spec/services/notes/create_service_spec.rb b/spec/services/notes/create_service_spec.rb
index c461dd700ec..39d6fd26e31 100644
--- a/spec/services/notes/create_service_spec.rb
+++ b/spec/services/notes/create_service_spec.rb
@@ -342,6 +342,60 @@ describe Notes::CreateService do
end
end
+ context 'design note' do
+ subject(:service) { described_class.new(project, user, params) }
+
+ let_it_be(:design) { create(:design, :with_file) }
+ let_it_be(:project) { design.project }
+ let_it_be(:user) { project.owner }
+ let_it_be(:params) do
+ {
+ type: 'DiffNote',
+ noteable: design,
+ note: "A message",
+ position: {
+ old_path: design.full_path,
+ new_path: design.full_path,
+ position_type: 'image',
+ width: '100',
+ height: '100',
+ x: '50',
+ y: '50',
+ base_sha: design.diff_refs.base_sha,
+ start_sha: design.diff_refs.base_sha,
+ head_sha: design.diff_refs.head_sha
+ }
+ }
+ end
+
+ it 'can create diff notes for designs' do
+ note = service.execute
+
+ expect(note).to be_a(DiffNote)
+ expect(note).to be_persisted
+ expect(note.noteable).to eq(design)
+ end
+
+ it 'sends a notification about this note', :sidekiq_might_not_need_inline do
+ notifier = double
+ allow(::NotificationService).to receive(:new).and_return(notifier)
+
+ expect(notifier)
+ .to receive(:new_note)
+ .with have_attributes(noteable: design)
+
+ service.execute
+ end
+
+ it 'correctly builds the position of the note' do
+ note = service.execute
+
+ expect(note.position.new_path).to eq(design.full_path)
+ expect(note.position.old_path).to eq(design.full_path)
+ expect(note.position.diff_refs).to eq(design.diff_refs)
+ end
+ end
+
context 'note with emoji only' do
it 'creates regular note' do
opts = {
@@ -371,7 +425,7 @@ describe Notes::CreateService do
expect do
existing_note
- Timecop.freeze(Time.now + 1.minute) { subject }
+ Timecop.freeze(Time.current + 1.minute) { subject }
existing_note.reload
end.to change { existing_note.type }.from(nil).to('DiscussionNote')
diff --git a/spec/services/notes/post_process_service_spec.rb b/spec/services/notes/post_process_service_spec.rb
index 99db7897664..d564cacd2d8 100644
--- a/spec/services/notes/post_process_service_spec.rb
+++ b/spec/services/notes/post_process_service_spec.rb
@@ -43,5 +43,32 @@ describe Notes::PostProcessService do
described_class.new(@note).execute
end
end
+
+ context 'when the noteable is a design' do
+ let_it_be(:noteable) { create(:design, :with_file) }
+ let_it_be(:discussion_note) { create_note }
+
+ subject { described_class.new(note).execute }
+
+ def create_note(in_reply_to: nil)
+ create(:diff_note_on_design, noteable: noteable, in_reply_to: in_reply_to)
+ end
+
+ context 'when the note is the start of a new discussion' do
+ let(:note) { discussion_note }
+
+ it 'creates a new system note' do
+ expect { subject }.to change { Note.system.count }.by(1)
+ end
+ end
+
+ context 'when the note is a reply within a discussion' do
+ let_it_be(:note) { create_note(in_reply_to: discussion_note) }
+
+ it 'does not create a new system note' do
+ expect { subject }.not_to change { Note.system.count }
+ end
+ end
+ end
end
end
diff --git a/spec/services/notification_service_spec.rb b/spec/services/notification_service_spec.rb
index 163ca0b9bc3..2a7166e3895 100644
--- a/spec/services/notification_service_spec.rb
+++ b/spec/services/notification_service_spec.rb
@@ -240,6 +240,17 @@ describe NotificationService, :mailer do
end
end
+ describe '#unknown_sign_in' do
+ let_it_be(:user) { create(:user) }
+ let_it_be(:ip) { '127.0.0.1' }
+
+ subject { notification.unknown_sign_in(user, ip) }
+
+ it 'sends email to the user' do
+ expect { subject }.to have_enqueued_email(user, ip, mail: 'unknown_sign_in_email')
+ end
+ end
+
describe 'Notes' do
context 'issue note' do
let(:project) { create(:project, :private) }
@@ -698,9 +709,60 @@ describe NotificationService, :mailer do
end
end
end
+
+ context 'when notified of a new design diff note' do
+ include DesignManagementTestHelpers
+
+ let_it_be(:design) { create(:design, :with_file) }
+ let_it_be(:project) { design.project }
+ let_it_be(:dev) { create(:user) }
+ let_it_be(:stranger) { create(:user) }
+ let_it_be(:note) do
+ create(:diff_note_on_design,
+ noteable: design,
+ note: "Hello #{dev.to_reference}, G'day #{stranger.to_reference}")
+ end
+ let(:mailer) { double(deliver_later: true) }
+
+ context 'design management is enabled' do
+ before do
+ enable_design_management
+ project.add_developer(dev)
+ allow(Notify).to receive(:note_design_email) { mailer }
+ end
+
+ it 'sends new note notifications' do
+ expect(subject).to receive(:send_new_note_notifications).with(note)
+
+ subject.new_note(note)
+ end
+
+ it 'sends a mail to the developer' do
+ expect(Notify)
+ .to receive(:note_design_email).with(dev.id, note.id, 'mentioned')
+
+ subject.new_note(note)
+ end
+
+ it 'does not notify non-developers' do
+ expect(Notify)
+ .not_to receive(:note_design_email).with(stranger.id, note.id)
+
+ subject.new_note(note)
+ end
+ end
+
+ context 'design management is disabled' do
+ it 'does not notify the user' do
+ expect(Notify).not_to receive(:note_design_email)
+
+ subject.new_note(note)
+ end
+ end
+ end
end
- describe '#send_new_release_notifications', :deliver_mails_inline, :sidekiq_inline do
+ describe '#send_new_release_notifications', :deliver_mails_inline do
context 'when recipients for a new release exist' do
let(:release) { create(:release) }
@@ -712,7 +774,7 @@ describe NotificationService, :mailer do
recipient_2 = NotificationRecipient.new(user_2, :custom, custom_action: :new_release)
allow(NotificationRecipients::BuildService).to receive(:build_new_release_recipients).and_return([recipient_1, recipient_2])
- release
+ notification.send_new_release_notifications(release)
should_email(user_1)
should_email(user_2)
diff --git a/spec/services/pages_domains/obtain_lets_encrypt_certificate_service_spec.rb b/spec/services/pages_domains/obtain_lets_encrypt_certificate_service_spec.rb
index 63fd0978c97..22fcc6b9a79 100644
--- a/spec/services/pages_domains/obtain_lets_encrypt_certificate_service_spec.rb
+++ b/spec/services/pages_domains/obtain_lets_encrypt_certificate_service_spec.rb
@@ -119,7 +119,7 @@ describe PagesDomains::ObtainLetsEncryptCertificateService do
cert = OpenSSL::X509::Certificate.new
cert.subject = cert.issuer = OpenSSL::X509::Name.parse(subject)
- cert.not_before = Time.now
+ cert.not_before = Time.current
cert.not_after = 1.year.from_now
cert.public_key = key.public_key
cert.serial = 0x0
diff --git a/spec/services/pod_logs/base_service_spec.rb b/spec/services/pod_logs/base_service_spec.rb
index 3ec5dc68c60..bc4989b59d9 100644
--- a/spec/services/pod_logs/base_service_spec.rb
+++ b/spec/services/pod_logs/base_service_spec.rb
@@ -103,6 +103,36 @@ describe ::PodLogs::BaseService do
expect(result[:container_name]).to eq(container_name)
end
end
+
+ context 'when pod_name is not a string' do
+ let(:params) do
+ {
+ 'pod_name' => { something_that_is: :not_a_string }
+ }
+ end
+
+ it 'returns error' do
+ result = subject.send(:check_arguments, {})
+
+ expect(result[:status]).to eq(:error)
+ expect(result[:message]).to eq('Invalid pod_name')
+ end
+ end
+
+ context 'when container_name is not a string' do
+ let(:params) do
+ {
+ 'container_name' => { something_that_is: :not_a_string }
+ }
+ end
+
+ it 'returns error' do
+ result = subject.send(:check_arguments, {})
+
+ expect(result[:status]).to eq(:error)
+ expect(result[:message]).to eq('Invalid container_name')
+ end
+ end
end
describe '#get_pod_names' do
diff --git a/spec/services/pod_logs/elasticsearch_service_spec.rb b/spec/services/pod_logs/elasticsearch_service_spec.rb
index e3efce1134b..8060d07461a 100644
--- a/spec/services/pod_logs/elasticsearch_service_spec.rb
+++ b/spec/services/pod_logs/elasticsearch_service_spec.rb
@@ -158,6 +158,21 @@ describe ::PodLogs::ElasticsearchService do
end
end
+ context 'with search provided and invalid' do
+ let(:params) do
+ {
+ 'search' => { term: "foo-bar" }
+ }
+ end
+
+ it 'returns error' do
+ result = subject.send(:check_search, {})
+
+ expect(result[:status]).to eq(:error)
+ expect(result[:message]).to eq("Invalid search parameter")
+ end
+ end
+
context 'with search not provided' do
let(:params) do
{}
@@ -188,6 +203,21 @@ describe ::PodLogs::ElasticsearchService do
end
end
+ context 'with cursor provided and invalid' do
+ let(:params) do
+ {
+ 'cursor' => { term: "foo-bar" }
+ }
+ end
+
+ it 'returns error' do
+ result = subject.send(:check_cursor, {})
+
+ expect(result[:status]).to eq(:error)
+ expect(result[:message]).to eq("Invalid cursor parameter")
+ end
+ end
+
context 'with cursor not provided' do
let(:params) do
{}
@@ -225,7 +255,7 @@ describe ::PodLogs::ElasticsearchService do
.and_return(Elasticsearch::Transport::Client.new)
allow_any_instance_of(::Gitlab::Elasticsearch::Logs::Lines)
.to receive(:pod_logs)
- .with(namespace, pod_name: pod_name, container_name: container_name, search: search, start_time: start_time, end_time: end_time, cursor: cursor)
+ .with(namespace, pod_name: pod_name, container_name: container_name, search: search, start_time: start_time, end_time: end_time, cursor: cursor, chart_above_v2: true)
.and_return({ logs: expected_logs, cursor: expected_cursor })
result = subject.send(:pod_logs, result_arg)
diff --git a/spec/services/pod_logs/kubernetes_service_spec.rb b/spec/services/pod_logs/kubernetes_service_spec.rb
index da89c7ee117..a1f7645323b 100644
--- a/spec/services/pod_logs/kubernetes_service_spec.rb
+++ b/spec/services/pod_logs/kubernetes_service_spec.rb
@@ -218,7 +218,7 @@ describe ::PodLogs::KubernetesService do
end
it 'returns error if pod_name was specified but does not exist' do
- result = subject.send(:check_pod_name, pod_name: 'another_pod', pods: [pod_name])
+ result = subject.send(:check_pod_name, pod_name: 'another-pod', pods: [pod_name])
expect(result[:status]).to eq(:error)
expect(result[:message]).to eq('Pod does not exist')
@@ -230,6 +230,13 @@ describe ::PodLogs::KubernetesService do
expect(result[:status]).to eq(:error)
expect(result[:message]).to eq('pod_name cannot be larger than 253 chars')
end
+
+ it 'returns error if pod_name is in invalid format' do
+ result = subject.send(:check_pod_name, pod_name: "Invalid_pod_name", pods: [pod_name])
+
+ expect(result[:status]).to eq(:error)
+ expect(result[:message]).to eq('pod_name can contain only lowercase letters, digits, \'-\', and \'.\' and must start and end with an alphanumeric character')
+ end
end
describe '#check_container_name' do
@@ -287,5 +294,16 @@ describe ::PodLogs::KubernetesService do
expect(result[:status]).to eq(:error)
expect(result[:message]).to eq('container_name cannot be larger than 253 chars')
end
+
+ it 'returns error if container_name is in invalid format' do
+ result = subject.send(:check_container_name,
+ container_name: "Invalid_container_name",
+ pod_name: pod_name,
+ raw_pods: raw_pods
+ )
+
+ expect(result[:status]).to eq(:error)
+ expect(result[:message]).to eq('container_name can contain only lowercase letters, digits, \'-\', and \'.\' and must start and end with an alphanumeric character')
+ end
end
end
diff --git a/spec/services/post_receive_service_spec.rb b/spec/services/post_receive_service_spec.rb
index b4f48696b15..25f4122f134 100644
--- a/spec/services/post_receive_service_spec.rb
+++ b/spec/services/post_receive_service_spec.rb
@@ -166,6 +166,41 @@ describe PostReceiveService do
expect(subject).to include(build_alert_message(message))
end
end
+
+ context 'storage size limit alerts' do
+ let(:check_storage_size_response) { ServiceResponse.success }
+
+ before do
+ expect_next_instance_of(Namespaces::CheckStorageSizeService, project.namespace, user) do |check_storage_size_service|
+ expect(check_storage_size_service).to receive(:execute).and_return(check_storage_size_response)
+ end
+ end
+
+ context 'when there is no payload' do
+ it 'adds no alert' do
+ expect(subject.size).to eq(1)
+ end
+ end
+
+ context 'when there is payload' do
+ let(:check_storage_size_response) do
+ ServiceResponse.success(
+ payload: {
+ alert_level: :info,
+ usage_message: "Usage",
+ explanation_message: "Explanation"
+ }
+ )
+ end
+
+ it 'adds an alert' do
+ response = subject
+
+ expect(response.size).to eq(2)
+ expect(response).to include(build_alert_message("##### INFO #####\nUsage\nExplanation"))
+ end
+ end
+ end
end
context 'with PersonalSnippet' do
diff --git a/spec/services/projects/alerting/notify_service_spec.rb b/spec/services/projects/alerting/notify_service_spec.rb
index f08ecd397ec..b88f0ef5149 100644
--- a/spec/services/projects/alerting/notify_service_spec.rb
+++ b/spec/services/projects/alerting/notify_service_spec.rb
@@ -12,11 +12,16 @@ describe Projects::Alerting::NotifyService do
shared_examples 'processes incident issues' do |amount|
let(:create_incident_service) { spy }
+ let(:new_alert) { instance_double(AlertManagement::Alert, id: 503, persisted?: true) }
it 'processes issues' do
+ expect(AlertManagement::Alert)
+ .to receive(:create)
+ .and_return(new_alert)
+
expect(IncidentManagement::ProcessAlertWorker)
.to receive(:perform_async)
- .with(project.id, kind_of(Hash))
+ .with(project.id, kind_of(Hash), new_alert.id)
.exactly(amount).times
Sidekiq::Testing.inline! do
@@ -59,15 +64,26 @@ describe Projects::Alerting::NotifyService do
end
end
+ shared_examples 'NotifyService does not create alert' do
+ it 'does not create alert' do
+ expect { subject }.not_to change(AlertManagement::Alert, :count)
+ end
+ end
+
describe '#execute' do
let(:token) { 'invalid-token' }
- let(:starts_at) { Time.now.change(usec: 0) }
+ let(:starts_at) { Time.current.change(usec: 0) }
let(:service) { described_class.new(project, nil, payload) }
let(:payload_raw) do
{
- 'title' => 'alert title',
- 'start_time' => starts_at.rfc3339
- }
+ title: 'alert title',
+ start_time: starts_at.rfc3339,
+ severity: 'low',
+ monitoring_tool: 'GitLab RSpec',
+ service: 'GitLab Test Suite',
+ description: 'Very detailed description',
+ hosts: ['1.1.1.1', '2.2.2.2']
+ }.with_indifferent_access
end
let(:payload) { ActionController::Parameters.new(payload_raw).permit! }
@@ -88,6 +104,73 @@ describe Projects::Alerting::NotifyService do
.and_return(incident_management_setting)
end
+ context 'with valid payload' do
+ let(:last_alert_attributes) do
+ AlertManagement::Alert.last.attributes
+ .except('id', 'iid', 'created_at', 'updated_at')
+ .with_indifferent_access
+ end
+
+ it 'creates AlertManagement::Alert' do
+ expect { subject }.to change(AlertManagement::Alert, :count).by(1)
+ end
+
+ it 'created alert has all data properly assigned' do
+ subject
+
+ expect(last_alert_attributes).to match(
+ project_id: project.id,
+ title: payload_raw.fetch(:title),
+ started_at: Time.zone.parse(payload_raw.fetch(:start_time)),
+ severity: payload_raw.fetch(:severity),
+ status: AlertManagement::Alert::STATUSES[:triggered],
+ events: 1,
+ hosts: payload_raw.fetch(:hosts),
+ payload: payload_raw.with_indifferent_access,
+ issue_id: nil,
+ description: payload_raw.fetch(:description),
+ monitoring_tool: payload_raw.fetch(:monitoring_tool),
+ service: payload_raw.fetch(:service),
+ fingerprint: nil,
+ ended_at: nil
+ )
+ end
+
+ context 'with a minimal payload' do
+ let(:payload_raw) do
+ {
+ title: 'alert title',
+ start_time: starts_at.rfc3339
+ }
+ end
+
+ it 'creates AlertManagement::Alert' do
+ expect { subject }.to change(AlertManagement::Alert, :count).by(1)
+ end
+
+ it 'created alert has all data properly assigned' do
+ subject
+
+ expect(last_alert_attributes).to match(
+ project_id: project.id,
+ title: payload_raw.fetch(:title),
+ started_at: Time.zone.parse(payload_raw.fetch(:start_time)),
+ severity: 'critical',
+ status: AlertManagement::Alert::STATUSES[:triggered],
+ events: 1,
+ hosts: [],
+ payload: payload_raw.with_indifferent_access,
+ issue_id: nil,
+ description: nil,
+ monitoring_tool: nil,
+ service: nil,
+ fingerprint: nil,
+ ended_at: nil
+ )
+ end
+ end
+ end
+
it_behaves_like 'does not process incident issues'
context 'issue enabled' do
@@ -103,6 +186,7 @@ describe Projects::Alerting::NotifyService do
end
it_behaves_like 'does not process incident issues due to error', http_status: :bad_request
+ it_behaves_like 'NotifyService does not create alert'
end
end
@@ -115,12 +199,14 @@ describe Projects::Alerting::NotifyService do
context 'with invalid token' do
it_behaves_like 'does not process incident issues due to error', http_status: :unauthorized
+ it_behaves_like 'NotifyService does not create alert'
end
context 'with deactivated Alerts Service' do
let!(:alerts_service) { create(:alerts_service, :inactive, project: project) }
it_behaves_like 'does not process incident issues due to error', http_status: :forbidden
+ it_behaves_like 'NotifyService does not create alert'
end
end
end
diff --git a/spec/services/projects/create_service_spec.rb b/spec/services/projects/create_service_spec.rb
index 1feea27eebc..e542f1e9108 100644
--- a/spec/services/projects/create_service_spec.rb
+++ b/spec/services/projects/create_service_spec.rb
@@ -489,6 +489,104 @@ describe Projects::CreateService, '#execute' do
end
end
+ it_behaves_like 'measurable service' do
+ before do
+ opts.merge!(
+ current_user: user,
+ path: 'foo'
+ )
+ end
+
+ let(:base_log_data) do
+ {
+ class: Projects::CreateService.name,
+ current_user: user.name,
+ project_full_path: "#{user.namespace.full_path}/#{opts[:path]}"
+ }
+ end
+
+ after do
+ create_project(user, opts)
+ end
+ end
+
+ context 'with specialized_project_authorization_workers' do
+ let_it_be(:other_user) { create(:user) }
+ let_it_be(:group) { create(:group) }
+
+ let(:opts) do
+ {
+ name: 'GitLab',
+ namespace_id: group.id
+ }
+ end
+
+ before do
+ group.add_maintainer(user)
+ group.add_developer(other_user)
+ end
+
+ it 'updates authorization for current_user' do
+ expect(Users::RefreshAuthorizedProjectsService).to(
+ receive(:new).with(user).and_call_original
+ )
+
+ project = create_project(user, opts)
+
+ expect(
+ Ability.allowed?(user, :read_project, project)
+ ).to be_truthy
+ end
+
+ it 'schedules authorization update for users with access to group' do
+ expect(AuthorizedProjectsWorker).not_to(
+ receive(:bulk_perform_async)
+ )
+ expect(AuthorizedProjectUpdate::ProjectCreateWorker).to(
+ receive(:perform_async).and_call_original
+ )
+ expect(AuthorizedProjectUpdate::UserRefreshWithLowUrgencyWorker).to(
+ receive(:bulk_perform_in)
+ .with(1.hour, array_including([user.id], [other_user.id]))
+ .and_call_original
+ )
+
+ create_project(user, opts)
+ end
+
+ context 'when feature is disabled' do
+ before do
+ stub_feature_flags(specialized_project_authorization_workers: false)
+ end
+
+ it 'updates authorization for current_user' do
+ expect(Users::RefreshAuthorizedProjectsService).to(
+ receive(:new).with(user).and_call_original
+ )
+
+ project = create_project(user, opts)
+
+ expect(
+ Ability.allowed?(user, :read_project, project)
+ ).to be_truthy
+ end
+
+ it 'uses AuthorizedProjectsWorker' do
+ expect(AuthorizedProjectsWorker).to(
+ receive(:bulk_perform_async).with(array_including([user.id], [other_user.id])).and_call_original
+ )
+ expect(AuthorizedProjectUpdate::ProjectCreateWorker).not_to(
+ receive(:perform_async)
+ )
+ expect(AuthorizedProjectUpdate::UserRefreshWithLowUrgencyWorker).not_to(
+ receive(:bulk_perform_in)
+ )
+
+ create_project(user, opts)
+ end
+ end
+ end
+
def create_project(user, opts)
Projects::CreateService.new(user, opts).execute
end
diff --git a/spec/services/projects/fork_service_spec.rb b/spec/services/projects/fork_service_spec.rb
index c8354f6ba4e..112a41c773b 100644
--- a/spec/services/projects/fork_service_spec.rb
+++ b/spec/services/projects/fork_service_spec.rb
@@ -320,7 +320,13 @@ describe Projects::ForkService do
allow_any_instance_of(Gitlab::Git::Repository).to receive(:checksum)
.and_return(::Gitlab::Git::BLANK_SHA)
- Projects::UpdateRepositoryStorageService.new(project).execute('test_second_storage')
+ storage_move = create(
+ :project_repository_storage_move,
+ :scheduled,
+ project: project,
+ destination_storage_name: 'test_second_storage'
+ )
+ Projects::UpdateRepositoryStorageService.new(storage_move).execute
fork_after_move = fork_project(project)
pool_repository_before_move = PoolRepository.joins(:shard)
.find_by(source_project: project, shards: { name: 'default' })
diff --git a/spec/services/projects/hashed_storage/base_attachment_service_spec.rb b/spec/services/projects/hashed_storage/base_attachment_service_spec.rb
index 34c37be6703..070dd5fc1b8 100644
--- a/spec/services/projects/hashed_storage/base_attachment_service_spec.rb
+++ b/spec/services/projects/hashed_storage/base_attachment_service_spec.rb
@@ -31,7 +31,7 @@ describe Projects::HashedStorage::BaseAttachmentService do
expect(Dir.exist?(target_path)).to be_truthy
Timecop.freeze do
- suffix = Time.now.utc.to_i
+ suffix = Time.current.utc.to_i
subject.send(:discard_path!, target_path)
expected_renamed_path = "#{target_path}-#{suffix}"
diff --git a/spec/services/projects/hashed_storage/migrate_repository_service_spec.rb b/spec/services/projects/hashed_storage/migrate_repository_service_spec.rb
index 71be335c11d..f1eaf8324e0 100644
--- a/spec/services/projects/hashed_storage/migrate_repository_service_spec.rb
+++ b/spec/services/projects/hashed_storage/migrate_repository_service_spec.rb
@@ -6,7 +6,7 @@ describe Projects::HashedStorage::MigrateRepositoryService do
include GitHelpers
let(:gitlab_shell) { Gitlab::Shell.new }
- let(:project) { create(:project, :legacy_storage, :repository, :wiki_repo) }
+ let(:project) { create(:project, :legacy_storage, :repository, :wiki_repo, :design_repo) }
let(:legacy_storage) { Storage::LegacyProject.new(project) }
let(:hashed_storage) { Storage::Hashed.new(project) }
@@ -45,11 +45,12 @@ describe Projects::HashedStorage::MigrateRepositoryService do
end
context 'when succeeds' do
- it 'renames project and wiki repositories' do
+ it 'renames project, wiki and design repositories' do
service.execute
expect(gitlab_shell.repository_exists?(project.repository_storage, "#{new_disk_path}.git")).to be_truthy
expect(gitlab_shell.repository_exists?(project.repository_storage, "#{new_disk_path}.wiki.git")).to be_truthy
+ expect(gitlab_shell.repository_exists?(project.repository_storage, "#{new_disk_path}.design.git")).to be_truthy
end
it 'updates project to be hashed and not read-only' do
@@ -59,9 +60,10 @@ describe Projects::HashedStorage::MigrateRepositoryService do
expect(project.repository_read_only).to be_falsey
end
- it 'move operation is called for both repositories' do
+ it 'move operation is called for all repositories' do
expect_move_repository(old_disk_path, new_disk_path)
expect_move_repository("#{old_disk_path}.wiki", "#{new_disk_path}.wiki")
+ expect_move_repository("#{old_disk_path}.design", "#{new_disk_path}.design")
service.execute
end
@@ -86,6 +88,7 @@ describe Projects::HashedStorage::MigrateRepositoryService do
expect(gitlab_shell.repository_exists?(project.repository_storage, "#{new_disk_path}.git")).to be_falsey
expect(gitlab_shell.repository_exists?(project.repository_storage, "#{new_disk_path}.wiki.git")).to be_falsey
+ expect(gitlab_shell.repository_exists?(project.repository_storage, "#{new_disk_path}.design.git")).to be_falsey
expect(project.repository_read_only?).to be_falsey
end
@@ -97,6 +100,7 @@ describe Projects::HashedStorage::MigrateRepositoryService do
it 'does not try to move nil repository over existing' do
expect(gitlab_shell).not_to receive(:mv_repository).with(project.repository_storage, old_disk_path, new_disk_path)
expect_move_repository("#{old_disk_path}.wiki", "#{new_disk_path}.wiki")
+ expect_move_repository("#{old_disk_path}.design", "#{new_disk_path}.design")
service.execute
end
diff --git a/spec/services/projects/hashed_storage/rollback_repository_service_spec.rb b/spec/services/projects/hashed_storage/rollback_repository_service_spec.rb
index 6dcd2ff4555..1c0f446d9cf 100644
--- a/spec/services/projects/hashed_storage/rollback_repository_service_spec.rb
+++ b/spec/services/projects/hashed_storage/rollback_repository_service_spec.rb
@@ -6,7 +6,7 @@ describe Projects::HashedStorage::RollbackRepositoryService, :clean_gitlab_redis
include GitHelpers
let(:gitlab_shell) { Gitlab::Shell.new }
- let(:project) { create(:project, :repository, :wiki_repo, storage_version: ::Project::HASHED_STORAGE_FEATURES[:repository]) }
+ let(:project) { create(:project, :repository, :wiki_repo, :design_repo, storage_version: ::Project::HASHED_STORAGE_FEATURES[:repository]) }
let(:legacy_storage) { Storage::LegacyProject.new(project) }
let(:hashed_storage) { Storage::Hashed.new(project) }
@@ -45,11 +45,12 @@ describe Projects::HashedStorage::RollbackRepositoryService, :clean_gitlab_redis
end
context 'when succeeds' do
- it 'renames project and wiki repositories' do
+ it 'renames project, wiki and design repositories' do
service.execute
expect(gitlab_shell.repository_exists?(project.repository_storage, "#{new_disk_path}.git")).to be_truthy
expect(gitlab_shell.repository_exists?(project.repository_storage, "#{new_disk_path}.wiki.git")).to be_truthy
+ expect(gitlab_shell.repository_exists?(project.repository_storage, "#{new_disk_path}.design.git")).to be_truthy
end
it 'updates project to be legacy and not read-only' do
@@ -62,6 +63,7 @@ describe Projects::HashedStorage::RollbackRepositoryService, :clean_gitlab_redis
it 'move operation is called for both repositories' do
expect_move_repository(old_disk_path, new_disk_path)
expect_move_repository("#{old_disk_path}.wiki", "#{new_disk_path}.wiki")
+ expect_move_repository("#{old_disk_path}.design", "#{new_disk_path}.design")
service.execute
end
@@ -86,6 +88,7 @@ describe Projects::HashedStorage::RollbackRepositoryService, :clean_gitlab_redis
expect(gitlab_shell.repository_exists?(project.repository_storage, "#{new_disk_path}.git")).to be_falsey
expect(gitlab_shell.repository_exists?(project.repository_storage, "#{new_disk_path}.wiki.git")).to be_falsey
+ expect(gitlab_shell.repository_exists?(project.repository_storage, "#{new_disk_path}.design.git")).to be_falsey
expect(project.repository_read_only?).to be_falsey
end
@@ -97,6 +100,7 @@ describe Projects::HashedStorage::RollbackRepositoryService, :clean_gitlab_redis
it 'does not try to move nil repository over existing' do
expect(gitlab_shell).not_to receive(:mv_repository).with(project.repository_storage, old_disk_path, new_disk_path)
expect_move_repository("#{old_disk_path}.wiki", "#{new_disk_path}.wiki")
+ expect_move_repository("#{old_disk_path}.design", "#{new_disk_path}.design")
service.execute
end
diff --git a/spec/services/projects/import_export/export_service_spec.rb b/spec/services/projects/import_export/export_service_spec.rb
index e00507d1827..5f496cb1e56 100644
--- a/spec/services/projects/import_export/export_service_spec.rb
+++ b/spec/services/projects/import_export/export_service_spec.rb
@@ -7,9 +7,10 @@ describe Projects::ImportExport::ExportService do
let!(:user) { create(:user) }
let(:project) { create(:project) }
let(:shared) { project.import_export_shared }
- let(:service) { described_class.new(project, user) }
let!(:after_export_strategy) { Gitlab::ImportExport::AfterExportStrategies::DownloadNotificationStrategy.new }
+ subject(:service) { described_class.new(project, user) }
+
before do
project.add_maintainer(user)
end
@@ -46,8 +47,8 @@ describe Projects::ImportExport::ExportService do
# in the corresponding EE spec.
skip if Gitlab.ee?
- # once for the normal repo, once for the wiki
- expect(Gitlab::ImportExport::RepoSaver).to receive(:new).twice.and_call_original
+ # once for the normal repo, once for the wiki repo, and once for the design repo
+ expect(Gitlab::ImportExport::RepoSaver).to receive(:new).exactly(3).times.and_call_original
service.execute
end
@@ -58,6 +59,12 @@ describe Projects::ImportExport::ExportService do
service.execute
end
+ it 'saves the design repo' do
+ expect(Gitlab::ImportExport::DesignRepoSaver).to receive(:new).and_call_original
+
+ service.execute
+ end
+
it 'saves the lfs objects' do
expect(Gitlab::ImportExport::LfsSaver).to receive(:new).and_call_original
@@ -177,5 +184,20 @@ describe Projects::ImportExport::ExportService do
expect { service.execute }.to raise_error(Gitlab::ImportExport::Error).with_message(expected_message)
end
end
+
+ it_behaves_like 'measurable service' do
+ let(:base_log_data) do
+ {
+ class: described_class.name,
+ current_user: user.name,
+ project_full_path: project.full_path,
+ file_path: shared.export_path
+ }
+ end
+
+ after do
+ service.execute(after_export_strategy)
+ end
+ end
end
end
diff --git a/spec/services/projects/import_service_spec.rb b/spec/services/projects/import_service_spec.rb
index af8118f9b11..ca6750b373d 100644
--- a/spec/services/projects/import_service_spec.rb
+++ b/spec/services/projects/import_service_spec.rb
@@ -264,13 +264,33 @@ describe Projects::ImportService do
it 'fails with port 25' do
project.import_url = "https://github.com:25/vim/vim.git"
- result = described_class.new(project, user).execute
+ result = subject.execute
expect(result[:status]).to eq :error
expect(result[:message]).to include('Only allowed ports are 80, 443')
end
end
+ it_behaves_like 'measurable service' do
+ let(:base_log_data) do
+ {
+ class: described_class.name,
+ current_user: user.name,
+ project_full_path: project.full_path,
+ import_type: project.import_type,
+ file_path: project.import_source
+ }
+ end
+
+ before do
+ project.import_type = 'github'
+ end
+
+ after do
+ subject.execute
+ end
+ end
+
def stub_github_omniauth_provider
provider = OpenStruct.new(
'name' => 'github',
diff --git a/spec/services/projects/prometheus/alerts/create_events_service_spec.rb b/spec/services/projects/prometheus/alerts/create_events_service_spec.rb
index 1d726db6ce3..35f23afd7a2 100644
--- a/spec/services/projects/prometheus/alerts/create_events_service_spec.rb
+++ b/spec/services/projects/prometheus/alerts/create_events_service_spec.rb
@@ -50,7 +50,7 @@ describe Projects::Prometheus::Alerts::CreateEventsService do
let(:events) { service.execute }
context 'with a firing payload' do
- let(:started_at) { truncate_to_second(Time.now) }
+ let(:started_at) { truncate_to_second(Time.current) }
let(:firing_event) { alert_payload(status: 'firing', started_at: started_at) }
let(:alerts_payload) { { 'alerts' => [firing_event] } }
@@ -87,7 +87,7 @@ describe Projects::Prometheus::Alerts::CreateEventsService do
end
context 'with a resolved payload' do
- let(:started_at) { truncate_to_second(Time.now) }
+ let(:started_at) { truncate_to_second(Time.current) }
let(:ended_at) { started_at + 1 }
let(:payload_key) { PrometheusAlertEvent.payload_key_for(alert.prometheus_metric_id, utc_rfc3339(started_at)) }
let(:resolved_event) { alert_payload(status: 'resolved', started_at: started_at, ended_at: ended_at) }
@@ -285,7 +285,7 @@ describe Projects::Prometheus::Alerts::CreateEventsService do
private
- def alert_payload(status: 'firing', started_at: Time.now, ended_at: Time.now, gitlab_alert_id: alert.prometheus_metric_id, title: nil, environment: nil)
+ def alert_payload(status: 'firing', started_at: Time.current, ended_at: Time.current, gitlab_alert_id: alert.prometheus_metric_id, title: nil, environment: nil)
payload = {}
payload['status'] = status if status
diff --git a/spec/services/projects/prometheus/alerts/notify_service_spec.rb b/spec/services/projects/prometheus/alerts/notify_service_spec.rb
index dce96dda1e3..009543f9016 100644
--- a/spec/services/projects/prometheus/alerts/notify_service_spec.rb
+++ b/spec/services/projects/prometheus/alerts/notify_service_spec.rb
@@ -217,6 +217,32 @@ describe Projects::Prometheus::Alerts::NotifyService do
end
end
+ context 'process Alert Management alerts' do
+ let(:process_service) { instance_double(AlertManagement::ProcessPrometheusAlertService) }
+
+ before do
+ create(:prometheus_service, project: project)
+ create(:project_alerting_setting, project: project, token: token)
+ end
+
+ context 'with multiple firing alerts and resolving alerts' do
+ let(:payload_raw) do
+ payload_for(firing: [alert_firing, alert_firing], resolved: [alert_resolved])
+ end
+
+ it 'processes Prometheus alerts' do
+ expect(AlertManagement::ProcessPrometheusAlertService)
+ .to receive(:new)
+ .with(project, nil, kind_of(Hash))
+ .exactly(3).times
+ .and_return(process_service)
+ expect(process_service).to receive(:execute).exactly(3).times
+
+ subject
+ end
+ end
+ end
+
context 'process incident issues' do
before do
create(:prometheus_service, project: project)
@@ -286,6 +312,13 @@ describe Projects::Prometheus::Alerts::NotifyService do
it_behaves_like 'no notifications', http_status: :bad_request
+ it 'does not process Prometheus alerts' do
+ expect(AlertManagement::ProcessPrometheusAlertService)
+ .not_to receive(:new)
+
+ subject
+ end
+
it 'does not process issues' do
expect(IncidentManagement::ProcessPrometheusAlertWorker)
.not_to receive(:perform_async)
diff --git a/spec/services/projects/propagate_service_template_spec.rb b/spec/services/projects/propagate_service_template_spec.rb
index 2c3effec617..7188ac5f733 100644
--- a/spec/services/projects/propagate_service_template_spec.rb
+++ b/spec/services/projects/propagate_service_template_spec.rb
@@ -8,16 +8,19 @@ describe Projects::PropagateServiceTemplate do
PushoverService.create(
template: true,
active: true,
+ push_events: false,
properties: {
device: 'MyDevice',
sound: 'mic',
priority: 4,
user_key: 'asdf',
api_key: '123456789'
- })
+ }
+ )
end
let!(:project) { create(:project) }
+ let(:excluded_attributes) { %w[id project_id template created_at updated_at title description] }
it 'creates services for projects' do
expect(project.pushover_service).to be_nil
@@ -35,7 +38,7 @@ describe Projects::PropagateServiceTemplate do
properties: {
bamboo_url: 'http://gitlab.com',
username: 'mic',
- password: "password",
+ password: 'password',
build_key: 'build'
}
)
@@ -54,7 +57,7 @@ describe Projects::PropagateServiceTemplate do
properties: {
bamboo_url: 'http://gitlab.com',
username: 'mic',
- password: "password",
+ password: 'password',
build_key: 'build'
}
)
@@ -70,6 +73,33 @@ describe Projects::PropagateServiceTemplate do
described_class.propagate(service_template)
expect(project.pushover_service.properties).to eq(service_template.properties)
+
+ expect(project.pushover_service.attributes.except(*excluded_attributes))
+ .to eq(service_template.attributes.except(*excluded_attributes))
+ end
+
+ context 'service with data fields' do
+ let(:service_template) do
+ JiraService.create!(
+ template: true,
+ active: true,
+ push_events: false,
+ url: 'http://jira.instance.com',
+ username: 'user',
+ password: 'secret'
+ )
+ end
+
+ it 'creates the service containing the template attributes' do
+ described_class.propagate(service_template)
+
+ expect(project.jira_service.attributes.except(*excluded_attributes))
+ .to eq(service_template.attributes.except(*excluded_attributes))
+
+ excluded_attributes = %w[id service_id created_at updated_at]
+ expect(project.jira_service.data_fields.attributes.except(*excluded_attributes))
+ .to eq(service_template.data_fields.attributes.except(*excluded_attributes))
+ end
end
describe 'bulk update', :use_sql_query_cache do
diff --git a/spec/services/projects/transfer_service_spec.rb b/spec/services/projects/transfer_service_spec.rb
index f17ddb22d22..0e2431c0e44 100644
--- a/spec/services/projects/transfer_service_spec.rb
+++ b/spec/services/projects/transfer_service_spec.rb
@@ -9,18 +9,26 @@ describe Projects::TransferService do
let(:group) { create(:group) }
let(:project) { create(:project, :repository, :legacy_storage, namespace: user.namespace) }
+ subject(:execute_transfer) { described_class.new(project, user).execute(group) }
+
context 'namespace -> namespace' do
before do
- allow_any_instance_of(Gitlab::UploadsTransfer)
- .to receive(:move_project).and_return(true)
- allow_any_instance_of(Gitlab::PagesTransfer)
- .to receive(:move_project).and_return(true)
+ allow_next_instance_of(Gitlab::UploadsTransfer) do |service|
+ allow(service).to receive(:move_project).and_return(true)
+ end
+ allow_next_instance_of(Gitlab::PagesTransfer) do |service|
+ allow(service).to receive(:move_project).and_return(true)
+ end
+
group.add_owner(user)
- @result = transfer_project(project, user, group)
end
- it { expect(@result).to be_truthy }
- it { expect(project.namespace).to eq(group) }
+ it 'updates the namespace' do
+ transfer_result = execute_transfer
+
+ expect(transfer_result).to be_truthy
+ expect(project.namespace).to eq(group)
+ end
end
context 'when transfer succeeds' do
@@ -31,26 +39,29 @@ describe Projects::TransferService do
it 'sends notifications' do
expect_any_instance_of(NotificationService).to receive(:project_was_moved)
- transfer_project(project, user, group)
+ execute_transfer
end
it 'invalidates the user\'s personal_project_count cache' do
expect(user).to receive(:invalidate_personal_projects_count)
- transfer_project(project, user, group)
+ execute_transfer
end
it 'executes system hooks' do
- transfer_project(project, user, group) do |service|
+ expect_next_instance_of(described_class) do |service|
expect(service).to receive(:execute_system_hooks)
end
+
+ execute_transfer
end
it 'moves the disk path', :aggregate_failures do
old_path = project.repository.disk_path
old_full_path = project.repository.full_path
- transfer_project(project, user, group)
+ execute_transfer
+
project.reload_repository!
expect(project.repository.disk_path).not_to eq(old_path)
@@ -60,13 +71,13 @@ describe Projects::TransferService do
end
it 'updates project full path in .git/config' do
- transfer_project(project, user, group)
+ execute_transfer
expect(rugged_config['gitlab.fullpath']).to eq "#{group.full_path}/#{project.path}"
end
it 'updates storage location' do
- transfer_project(project, user, group)
+ execute_transfer
expect(project.project_repository).to have_attributes(
disk_path: "#{group.full_path}/#{project.path}",
@@ -80,7 +91,7 @@ describe Projects::TransferService do
def attempt_project_transfer(&block)
expect do
- transfer_project(project, user, group, &block)
+ execute_transfer
end.to raise_error(ActiveRecord::ActiveRecordError)
end
@@ -138,13 +149,15 @@ describe Projects::TransferService do
end
context 'namespace -> no namespace' do
- before do
- @result = transfer_project(project, user, nil)
- end
+ let(:group) { nil }
+
+ it 'does not allow the project transfer' do
+ transfer_result = execute_transfer
- it { expect(@result).to eq false }
- it { expect(project.namespace).to eq(user.namespace) }
- it { expect(project.errors.messages[:new_namespace].first).to eq 'Please select a new namespace for your project.' }
+ expect(transfer_result).to eq false
+ expect(project.namespace).to eq(user.namespace)
+ expect(project.errors.messages[:new_namespace].first).to eq 'Please select a new namespace for your project.'
+ end
end
context 'disallow transferring of project with tags' do
@@ -156,18 +169,18 @@ describe Projects::TransferService do
project.container_repositories << container_repository
end
- subject { transfer_project(project, user, group) }
-
- it { is_expected.to be_falsey }
+ it 'does not allow the project transfer' do
+ expect(execute_transfer).to eq false
+ end
end
context 'namespace -> not allowed namespace' do
- before do
- @result = transfer_project(project, user, group)
- end
+ it 'does not allow the project transfer' do
+ transfer_result = execute_transfer
- it { expect(@result).to eq false }
- it { expect(project.namespace).to eq(user.namespace) }
+ expect(transfer_result).to eq false
+ expect(project.namespace).to eq(user.namespace)
+ end
end
context 'namespace which contains orphan repository with same projects path name' do
@@ -177,99 +190,94 @@ describe Projects::TransferService do
group.add_owner(user)
TestEnv.create_bare_repository(fake_repo_path)
-
- @result = transfer_project(project, user, group)
end
after do
FileUtils.rm_rf(fake_repo_path)
end
- it { expect(@result).to eq false }
- it { expect(project.namespace).to eq(user.namespace) }
- it { expect(project.errors[:new_namespace]).to include('Cannot move project') }
+ it 'does not allow the project transfer' do
+ transfer_result = execute_transfer
+
+ expect(transfer_result).to eq false
+ expect(project.namespace).to eq(user.namespace)
+ expect(project.errors[:new_namespace]).to include('Cannot move project')
+ end
end
context 'target namespace containing the same project name' do
before do
group.add_owner(user)
- project.update(name: 'new_name')
+ create(:project, name: project.name, group: group, path: 'other')
+ end
- create(:project, name: 'new_name', group: group, path: 'other')
+ it 'does not allow the project transfer' do
+ transfer_result = execute_transfer
- @result = transfer_project(project, user, group)
+ expect(transfer_result).to eq false
+ expect(project.namespace).to eq(user.namespace)
+ expect(project.errors[:new_namespace]).to include('Project with same name or path in target namespace already exists')
end
-
- it { expect(@result).to eq false }
- it { expect(project.namespace).to eq(user.namespace) }
- it { expect(project.errors[:new_namespace]).to include('Project with same name or path in target namespace already exists') }
end
context 'target namespace containing the same project path' do
before do
group.add_owner(user)
-
create(:project, name: 'other-name', path: project.path, group: group)
-
- @result = transfer_project(project, user, group)
end
- it { expect(@result).to eq false }
- it { expect(project.namespace).to eq(user.namespace) }
- it { expect(project.errors[:new_namespace]).to include('Project with same name or path in target namespace already exists') }
+ it 'does not allow the project transfer' do
+ transfer_result = execute_transfer
+
+ expect(transfer_result).to eq false
+ expect(project.namespace).to eq(user.namespace)
+ expect(project.errors[:new_namespace]).to include('Project with same name or path in target namespace already exists')
+ end
end
context 'target namespace allows developers to create projects' do
let(:group) { create(:group, project_creation_level: ::Gitlab::Access::DEVELOPER_MAINTAINER_PROJECT_ACCESS) }
context 'the user is a member of the target namespace with developer permissions' do
- subject(:transfer_project_result) { transfer_project(project, user, group) }
-
before do
group.add_developer(user)
end
it 'does not allow project transfer to the target namespace' do
- expect(transfer_project_result).to eq false
+ transfer_result = execute_transfer
+
+ expect(transfer_result).to eq false
expect(project.namespace).to eq(user.namespace)
expect(project.errors[:new_namespace]).to include('Transfer failed, please contact an admin.')
end
end
end
- def transfer_project(project, user, new_namespace)
- service = Projects::TransferService.new(project, user)
-
- yield(service) if block_given?
-
- service.execute(new_namespace)
- end
-
context 'visibility level' do
- let(:internal_group) { create(:group, :internal) }
+ let(:group) { create(:group, :internal) }
before do
- internal_group.add_owner(user)
+ group.add_owner(user)
end
context 'when namespace visibility level < project visibility level' do
- let(:public_project) { create(:project, :public, :repository, namespace: user.namespace) }
+ let(:project) { create(:project, :public, :repository, namespace: user.namespace) }
before do
- transfer_project(public_project, user, internal_group)
+ execute_transfer
end
- it { expect(public_project.visibility_level).to eq(internal_group.visibility_level) }
+ it { expect(project.visibility_level).to eq(group.visibility_level) }
end
context 'when namespace visibility level > project visibility level' do
- let(:private_project) { create(:project, :private, :repository, namespace: user.namespace) }
+ let(:project) { create(:project, :private, :repository, namespace: user.namespace) }
before do
- transfer_project(private_project, user, internal_group)
+ execute_transfer
end
- it { expect(private_project.visibility_level).to eq(Gitlab::VisibilityLevel::PRIVATE) }
+ it { expect(project.visibility_level).to eq(Gitlab::VisibilityLevel::PRIVATE) }
end
end
@@ -277,9 +285,11 @@ describe Projects::TransferService do
it 'delegates transfer to Labels::TransferService' do
group.add_owner(user)
- expect_any_instance_of(Labels::TransferService).to receive(:execute).once.and_call_original
+ expect_next_instance_of(Labels::TransferService, user, project.group, project) do |labels_transfer_service|
+ expect(labels_transfer_service).to receive(:execute).once.and_call_original
+ end
- transfer_project(project, user, group)
+ execute_transfer
end
end
@@ -287,49 +297,52 @@ describe Projects::TransferService do
it 'delegates transfer to Milestones::TransferService' do
group.add_owner(user)
- expect(Milestones::TransferService).to receive(:new).with(user, project.group, project).and_call_original
- expect_any_instance_of(Milestones::TransferService).to receive(:execute).once
+ expect_next_instance_of(Milestones::TransferService, user, project.group, project) do |milestones_transfer_service|
+ expect(milestones_transfer_service).to receive(:execute).once.and_call_original
+ end
- transfer_project(project, user, group)
+ execute_transfer
end
end
context 'when hashed storage in use' do
- let!(:hashed_project) { create(:project, :repository, namespace: user.namespace) }
- let!(:old_disk_path) { hashed_project.repository.disk_path }
+ let!(:project) { create(:project, :repository, namespace: user.namespace) }
+ let!(:old_disk_path) { project.repository.disk_path }
before do
group.add_owner(user)
end
it 'does not move the disk path', :aggregate_failures do
- new_full_path = "#{group.full_path}/#{hashed_project.path}"
+ new_full_path = "#{group.full_path}/#{project.path}"
- transfer_project(hashed_project, user, group)
- hashed_project.reload_repository!
+ execute_transfer
- expect(hashed_project.repository).to have_attributes(
+ project.reload_repository!
+
+ expect(project.repository).to have_attributes(
disk_path: old_disk_path,
full_path: new_full_path
)
- expect(hashed_project.disk_path).to eq(old_disk_path)
+ expect(project.disk_path).to eq(old_disk_path)
end
it 'does not move the disk path when the transfer fails', :aggregate_failures do
- old_full_path = hashed_project.full_path
+ old_full_path = project.full_path
expect_next_instance_of(described_class) do |service|
allow(service).to receive(:execute_system_hooks).and_raise('foo')
end
- expect { transfer_project(hashed_project, user, group) }.to raise_error('foo')
- hashed_project.reload_repository!
+ expect { execute_transfer }.to raise_error('foo')
+
+ project.reload_repository!
- expect(hashed_project.repository).to have_attributes(
+ expect(project.repository).to have_attributes(
disk_path: old_disk_path,
full_path: old_full_path
)
- expect(hashed_project.disk_path).to eq(old_disk_path)
+ expect(project.disk_path).to eq(old_disk_path)
end
end
@@ -344,18 +357,102 @@ describe Projects::TransferService do
end
it 'refreshes the permissions of the old and new namespace' do
- transfer_project(project, owner, group)
+ execute_transfer
expect(group_member.authorized_projects).to include(project)
expect(owner.authorized_projects).to include(project)
end
it 'only schedules a single job for every user' do
- expect(UserProjectAccessChangedService).to receive(:new)
- .with([owner.id, group_member.id])
- .and_call_original
+ expect_next_instance_of(UserProjectAccessChangedService, [owner.id, group_member.id]) do |service|
+ expect(service).to receive(:execute).once.and_call_original
+ end
+
+ execute_transfer
+ end
+ end
+
+ describe 'transferring a design repository' do
+ subject { described_class.new(project, user) }
+
+ before do
+ group.add_owner(user)
+ end
+
+ def design_repository
+ project.design_repository
+ end
+
+ it 'does not create a design repository' do
+ expect(subject.execute(group)).to be true
+
+ project.clear_memoization(:design_repository)
+
+ expect(design_repository.exists?).to be false
+ end
- transfer_project(project, owner, group)
+ describe 'when the project has a design repository' do
+ let(:project_repo_path) { "#{project.path}#{::Gitlab::GlRepository::DESIGN.path_suffix}" }
+ let(:old_full_path) { "#{user.namespace.full_path}/#{project_repo_path}" }
+ let(:new_full_path) { "#{group.full_path}/#{project_repo_path}" }
+
+ context 'with legacy storage' do
+ let(:project) { create(:project, :repository, :legacy_storage, :design_repo, namespace: user.namespace) }
+
+ it 'moves the repository' do
+ expect(subject.execute(group)).to be true
+
+ project.clear_memoization(:design_repository)
+
+ expect(design_repository).to have_attributes(
+ disk_path: new_full_path,
+ full_path: new_full_path
+ )
+ end
+
+ it 'does not move the repository when an error occurs', :aggregate_failures do
+ allow(subject).to receive(:execute_system_hooks).and_raise('foo')
+ expect { subject.execute(group) }.to raise_error('foo')
+
+ project.clear_memoization(:design_repository)
+
+ expect(design_repository).to have_attributes(
+ disk_path: old_full_path,
+ full_path: old_full_path
+ )
+ end
+ end
+
+ context 'with hashed storage' do
+ let(:project) { create(:project, :repository, namespace: user.namespace) }
+
+ it 'does not move the repository' do
+ old_disk_path = design_repository.disk_path
+
+ expect(subject.execute(group)).to be true
+
+ project.clear_memoization(:design_repository)
+
+ expect(design_repository).to have_attributes(
+ disk_path: old_disk_path,
+ full_path: new_full_path
+ )
+ end
+
+ it 'does not move the repository when an error occurs' do
+ old_disk_path = design_repository.disk_path
+
+ allow(subject).to receive(:execute_system_hooks).and_raise('foo')
+ expect { subject.execute(group) }.to raise_error('foo')
+
+ project.clear_memoization(:design_repository)
+
+ expect(design_repository).to have_attributes(
+ disk_path: old_disk_path,
+ full_path: old_full_path
+ )
+ end
+ end
end
end
diff --git a/spec/services/projects/update_remote_mirror_service_spec.rb b/spec/services/projects/update_remote_mirror_service_spec.rb
index 4396ccab584..38c2dc0780e 100644
--- a/spec/services/projects/update_remote_mirror_service_spec.rb
+++ b/spec/services/projects/update_remote_mirror_service_spec.rb
@@ -5,7 +5,7 @@ require 'spec_helper'
describe Projects::UpdateRemoteMirrorService do
let(:project) { create(:project, :repository) }
let(:remote_project) { create(:forked_project_with_submodules) }
- let(:remote_mirror) { project.remote_mirrors.create!(url: remote_project.http_url_to_repo, enabled: true, only_protected_branches: false) }
+ let(:remote_mirror) { create(:remote_mirror, project: project, enabled: true) }
let(:remote_name) { remote_mirror.remote_name }
subject(:service) { described_class.new(project, project.creator) }
@@ -16,7 +16,9 @@ describe Projects::UpdateRemoteMirrorService do
before do
project.repository.add_branch(project.owner, 'existing-branch', 'master')
- allow(remote_mirror).to receive(:update_repository).and_return(true)
+ allow(remote_mirror)
+ .to receive(:update_repository)
+ .and_return(double(divergent_refs: []))
end
it 'ensures the remote exists' do
@@ -53,7 +55,7 @@ describe Projects::UpdateRemoteMirrorService do
it 'marks the mirror as failed and raises the error when an unexpected error occurs' do
allow(project.repository).to receive(:fetch_remote).and_raise('Badly broken')
- expect { execute! }.to raise_error /Badly broken/
+ expect { execute! }.to raise_error(/Badly broken/)
expect(remote_mirror).to be_failed
expect(remote_mirror.last_error).to include('Badly broken')
@@ -83,32 +85,21 @@ describe Projects::UpdateRemoteMirrorService do
end
end
- context 'when syncing all branches' do
- it 'push all the branches the first time' do
+ context 'when there are divergent refs' do
+ before do
stub_fetch_remote(project, remote_name: remote_name, ssh_auth: remote_mirror)
-
- expect(remote_mirror).to receive(:update_repository).with({})
-
- execute!
end
- end
- context 'when only syncing protected branches' do
- it 'sync updated protected branches' do
- stub_fetch_remote(project, remote_name: remote_name, ssh_auth: remote_mirror)
- protected_branch = create_protected_branch(project)
- remote_mirror.only_protected_branches = true
-
- expect(remote_mirror)
- .to receive(:update_repository)
- .with(only_branches_matching: [protected_branch.name])
+ it 'marks the mirror as failed and sets an error message' do
+ response = double(divergent_refs: %w[refs/heads/master refs/heads/develop])
+ expect(remote_mirror).to receive(:update_repository).and_return(response)
execute!
- end
- def create_protected_branch(project)
- branch_name = project.repository.branch_names.find { |n| n != 'existing-branch' }
- create(:protected_branch, project: project, name: branch_name)
+ expect(remote_mirror).to be_failed
+ expect(remote_mirror.last_error).to include("Some refs have diverged")
+ expect(remote_mirror.last_error).to include("refs/heads/master\n")
+ expect(remote_mirror.last_error).to include("refs/heads/develop")
end
end
end
diff --git a/spec/services/projects/update_repository_storage_service_spec.rb b/spec/services/projects/update_repository_storage_service_spec.rb
index 05555fa76f7..28b79bc61d9 100644
--- a/spec/services/projects/update_repository_storage_service_spec.rb
+++ b/spec/services/projects/update_repository_storage_service_spec.rb
@@ -5,17 +5,20 @@ require 'spec_helper'
describe Projects::UpdateRepositoryStorageService do
include Gitlab::ShellAdapter
- subject { described_class.new(project) }
+ subject { described_class.new(repository_storage_move) }
describe "#execute" do
- let(:time) { Time.now }
+ let(:time) { Time.current }
before do
allow(Time).to receive(:now).and_return(time)
+ allow(Gitlab.config.repositories.storages).to receive(:keys).and_return(%w[default test_second_storage])
end
context 'without wiki and design repository' do
let(:project) { create(:project, :repository, repository_read_only: true, wiki_enabled: false) }
+ let(:destination) { 'test_second_storage' }
+ let(:repository_storage_move) { create(:project_repository_storage_move, :scheduled, project: project, destination_storage_name: destination) }
let!(:checksum) { project.repository.checksum }
let(:project_repository_double) { double(:repository) }
@@ -41,9 +44,9 @@ describe Projects::UpdateRepositoryStorageService do
expect(project_repository_double).to receive(:checksum)
.and_return(checksum)
- result = subject.execute('test_second_storage')
+ result = subject.execute
- expect(result[:status]).to eq(:success)
+ expect(result).to be_success
expect(project).not_to be_repository_read_only
expect(project.repository_storage).to eq('test_second_storage')
expect(gitlab_shell.repository_exists?('default', old_path)).to be(false)
@@ -52,11 +55,13 @@ describe Projects::UpdateRepositoryStorageService do
end
context 'when the filesystems are the same' do
+ let(:destination) { project.repository_storage }
+
it 'bails out and does nothing' do
- result = subject.execute(project.repository_storage)
+ result = subject.execute
- expect(result[:status]).to eq(:error)
- expect(result[:message]).to match(/SameFilesystemError/)
+ expect(result).to be_error
+ expect(result.message).to match(/SameFilesystemError/)
end
end
@@ -72,9 +77,9 @@ describe Projects::UpdateRepositoryStorageService do
.and_raise(Gitlab::Git::CommandError)
expect(GitlabShellWorker).not_to receive(:perform_async)
- result = subject.execute('test_second_storage')
+ result = subject.execute
- expect(result[:status]).to eq(:error)
+ expect(result).to be_error
expect(project).not_to be_repository_read_only
expect(project.repository_storage).to eq('default')
end
@@ -93,9 +98,9 @@ describe Projects::UpdateRepositoryStorageService do
.and_return('not matching checksum')
expect(GitlabShellWorker).not_to receive(:perform_async)
- result = subject.execute('test_second_storage')
+ result = subject.execute
- expect(result[:status]).to eq(:error)
+ expect(result).to be_error
expect(project).not_to be_repository_read_only
expect(project.repository_storage).to eq('default')
end
@@ -115,9 +120,9 @@ describe Projects::UpdateRepositoryStorageService do
expect(project_repository_double).to receive(:checksum)
.and_return(checksum)
- result = subject.execute('test_second_storage')
+ result = subject.execute
- expect(result[:status]).to eq(:success)
+ expect(result).to be_success
expect(project.repository_storage).to eq('test_second_storage')
expect(project.reload_pool_repository).to be_nil
end
@@ -128,11 +133,26 @@ describe Projects::UpdateRepositoryStorageService do
include_examples 'moves repository to another storage', 'wiki' do
let(:project) { create(:project, :repository, repository_read_only: true, wiki_enabled: true) }
let(:repository) { project.wiki.repository }
+ let(:destination) { 'test_second_storage' }
+ let(:repository_storage_move) { create(:project_repository_storage_move, :scheduled, project: project, destination_storage_name: destination) }
before do
project.create_wiki
end
end
end
+
+ context 'with design repository' do
+ include_examples 'moves repository to another storage', 'design' do
+ let(:project) { create(:project, :repository, repository_read_only: true) }
+ let(:repository) { project.design_repository }
+ let(:destination) { 'test_second_storage' }
+ let(:repository_storage_move) { create(:project_repository_storage_move, :scheduled, project: project, destination_storage_name: destination) }
+
+ before do
+ project.design_repository.create_if_not_exists
+ end
+ end
+ end
end
end
diff --git a/spec/services/prometheus/proxy_service_spec.rb b/spec/services/prometheus/proxy_service_spec.rb
index 5a036194d01..656ccea10de 100644
--- a/spec/services/prometheus/proxy_service_spec.rb
+++ b/spec/services/prometheus/proxy_service_spec.rb
@@ -117,7 +117,7 @@ describe Prometheus::ProxyService do
context 'when value not present in cache' do
it 'returns nil' do
- expect(ReactiveCachingWorker)
+ expect(ExternalServiceReactiveCachingWorker)
.to receive(:perform_async)
.with(subject.class, subject.id, *opts)
diff --git a/spec/services/prometheus/proxy_variable_substitution_service_spec.rb b/spec/services/prometheus/proxy_variable_substitution_service_spec.rb
index 9978c631366..82ea356d599 100644
--- a/spec/services/prometheus/proxy_variable_substitution_service_spec.rb
+++ b/spec/services/prometheus/proxy_variable_substitution_service_spec.rb
@@ -6,7 +6,7 @@ describe Prometheus::ProxyVariableSubstitutionService do
describe '#execute' do
let_it_be(:environment) { create(:environment) }
- let(:params_keys) { { query: 'up{environment="%{ci_environment_slug}"}' } }
+ let(:params_keys) { { query: 'up{environment="{{ci_environment_slug}}"}' } }
let(:params) { ActionController::Parameters.new(params_keys).permit! }
let(:result) { subject.execute }
@@ -32,21 +32,13 @@ describe Prometheus::ProxyVariableSubstitutionService do
expect(params).to eq(
ActionController::Parameters.new(
- query: 'up{environment="%{ci_environment_slug}"}'
+ query: 'up{environment="{{ci_environment_slug}}"}'
).permit!
)
end
end
context 'with predefined variables' do
- let(:params_keys) { { query: 'up{%{environment_filter}}' } }
-
- it_behaves_like 'success' do
- let(:expected_query) do
- %Q[up{container_name!="POD",environment="#{environment.slug}"}]
- end
- end
-
context 'with nil query' do
let(:params_keys) { {} }
@@ -64,18 +56,6 @@ describe Prometheus::ProxyVariableSubstitutionService do
let(:expected_query) { %Q[up{environment="#{environment.slug}"}] }
end
end
-
- context 'with ruby and liquid formats' do
- let(:params_keys) do
- { query: 'up{%{environment_filter},env2="{{ci_environment_slug}}"}' }
- end
-
- it_behaves_like 'success' do
- let(:expected_query) do
- %Q[up{container_name!="POD",environment="#{environment.slug}",env2="#{environment.slug}"}]
- end
- end
- end
end
context 'with custom variables' do
@@ -92,20 +72,6 @@ describe Prometheus::ProxyVariableSubstitutionService do
let(:expected_query) { %q[up{pod_name="pod1"}] }
end
- context 'with ruby variable interpolation format' do
- let(:params_keys) do
- {
- query: 'up{pod_name="%{pod_name}"}',
- variables: ['pod_name', pod_name]
- }
- end
-
- it_behaves_like 'success' do
- # Custom variables cannot be used with the Ruby interpolation format.
- let(:expected_query) { "up{pod_name=\"%{pod_name}\"}" }
- end
- end
-
context 'with predefined variables in variables parameter' do
let(:params_keys) do
{
@@ -142,62 +108,47 @@ describe Prometheus::ProxyVariableSubstitutionService do
end
it_behaves_like 'success' do
- let(:expected_query) { 'up{pod_name=""}' }
+ let(:expected_query) { 'up{pod_name="{{pod_name}}"}' }
end
end
+ end
- context 'with ruby and liquid variables' do
+ context 'gsub variable substitution tolerance for weirdness' do
+ context 'with whitespace around variable' do
let(:params_keys) do
{
- query: 'up{env1="%{ruby_variable}",env2="{{ liquid_variable }}"}',
- variables: %w(ruby_variable value liquid_variable env_slug)
+ query: 'up{' \
+ "env1={{ ci_environment_slug}}," \
+ "env2={{ci_environment_slug }}," \
+ "{{ environment_filter }}" \
+ '}'
}
end
it_behaves_like 'success' do
- # It should replace only liquid variables with their values
- let(:expected_query) { %q[up{env1="%{ruby_variable}",env2="env_slug"}] }
+ let(:expected_query) do
+ 'up{' \
+ "env1=#{environment.slug}," \
+ "env2=#{environment.slug}," \
+ "container_name!=\"POD\",environment=\"#{environment.slug}\"" \
+ '}'
+ end
end
end
- end
-
- context 'with liquid tags and ruby format variables' do
- let(:params_keys) do
- {
- query: 'up{ {% if true %}env1="%{ci_environment_slug}",' \
- 'env2="{{ci_environment_slug}}"{% endif %} }'
- }
- end
-
- # The following spec will fail and should be changed to a 'success' spec
- # once we remove support for the Ruby interpolation format.
- # https://gitlab.com/gitlab-org/gitlab/issues/37990
- #
- # Liquid tags `{% %}` cannot be used currently because the Ruby `%`
- # operator raises an error when it encounters a Liquid `{% %}` tag in the
- # string.
- #
- # Once we remove support for the Ruby format, users can start using
- # Liquid tags.
-
- it_behaves_like 'error', 'Malformed string'
- end
- context 'ruby template rendering' do
- let(:params_keys) do
- { query: 'up{env=%{ci_environment_slug},%{environment_filter}}' }
- end
+ context 'with empty variables' do
+ let(:params_keys) do
+ { query: "up{env1={{}},env2={{ }}}" }
+ end
- it_behaves_like 'success' do
- let(:expected_query) do
- "up{env=#{environment.slug},container_name!=\"POD\"," \
- "environment=\"#{environment.slug}\"}"
+ it_behaves_like 'success' do
+ let(:expected_query) { "up{env1={{}},env2={{ }}}" }
end
end
context 'with multiple occurrences of variable in string' do
let(:params_keys) do
- { query: 'up{env1=%{ci_environment_slug},env2=%{ci_environment_slug}}' }
+ { query: "up{env1={{ci_environment_slug}},env2={{ci_environment_slug}}}" }
end
it_behaves_like 'success' do
@@ -207,7 +158,7 @@ describe Prometheus::ProxyVariableSubstitutionService do
context 'with multiple variables in string' do
let(:params_keys) do
- { query: 'up{env=%{ci_environment_slug},%{environment_filter}}' }
+ { query: "up{env={{ci_environment_slug}},{{environment_filter}}}" }
end
it_behaves_like 'success' do
@@ -219,69 +170,22 @@ describe Prometheus::ProxyVariableSubstitutionService do
end
context 'with unknown variables in string' do
- let(:params_keys) { { query: 'up{env=%{env_slug}}' } }
-
- it_behaves_like 'success' do
- let(:expected_query) { 'up{env=%{env_slug}}' }
- end
- end
-
- # This spec is needed if there are multiple keys in the context provided
- # by `Gitlab::Prometheus::QueryVariables.call(environment)` which is
- # passed to the Ruby `%` operator.
- # If the number of keys in the context is one, there is no need for
- # this spec.
- context 'with extra variables in context' do
- let(:params_keys) { { query: 'up{env=%{ci_environment_slug}}' } }
+ let(:params_keys) { { query: "up{env={{env_slug}}}" } }
it_behaves_like 'success' do
- let(:expected_query) { "up{env=#{environment.slug}}" }
- end
-
- it 'has more than one variable in context' do
- expect(Gitlab::Prometheus::QueryVariables.call(environment).size).to be > 1
+ let(:expected_query) { "up{env={{env_slug}}}" }
end
end
- # The ruby % operator will not replace known variables if there are unknown
- # variables also in the string. It doesn't raise an error
- # (though the `sprintf` and `format` methods do).
context 'with unknown and known variables in string' do
let(:params_keys) do
- { query: 'up{env=%{ci_environment_slug},other_env=%{env_slug}}' }
+ { query: "up{env={{ci_environment_slug}},other_env={{env_slug}}}" }
end
it_behaves_like 'success' do
- let(:expected_query) { 'up{env=%{ci_environment_slug},other_env=%{env_slug}}' }
+ let(:expected_query) { "up{env=#{environment.slug},other_env={{env_slug}}}" }
end
end
-
- context 'when rendering raises error' do
- context 'when TypeError is raised' do
- let(:params_keys) { { query: '{% a %}' } }
-
- it_behaves_like 'error', 'Malformed string'
- end
-
- context 'when ArgumentError is raised' do
- let(:params_keys) { { query: '%<' } }
-
- it_behaves_like 'error', 'Malformed string'
- end
- end
- end
-
- context 'when liquid template rendering raises error' do
- before do
- liquid_service = instance_double(TemplateEngines::LiquidService)
-
- allow(TemplateEngines::LiquidService).to receive(:new).and_return(liquid_service)
- allow(liquid_service).to receive(:render).and_raise(
- TemplateEngines::LiquidService::RenderError, 'error message'
- )
- end
-
- it_behaves_like 'error', 'error message'
end
end
end
diff --git a/spec/services/quick_actions/interpret_service_spec.rb b/spec/services/quick_actions/interpret_service_spec.rb
index 36f9966c0ef..a9de0a747f6 100644
--- a/spec/services/quick_actions/interpret_service_spec.rb
+++ b/spec/services/quick_actions/interpret_service_spec.rb
@@ -361,7 +361,7 @@ describe QuickActions::InterpretService do
expect(updates).to eq(spend_time: {
duration: 3600,
user_id: developer.id,
- spent_at: DateTime.now.to_date
+ spent_at: DateTime.current.to_date
})
end
@@ -379,7 +379,7 @@ describe QuickActions::InterpretService do
expect(updates).to eq(spend_time: {
duration: -1800,
user_id: developer.id,
- spent_at: DateTime.now.to_date
+ spent_at: DateTime.current.to_date
})
end
end
diff --git a/spec/services/releases/create_service_spec.rb b/spec/services/releases/create_service_spec.rb
index 255f044db90..d0859500440 100644
--- a/spec/services/releases/create_service_spec.rb
+++ b/spec/services/releases/create_service_spec.rb
@@ -20,6 +20,8 @@ describe Releases::CreateService do
describe '#execute' do
shared_examples 'a successful release creation' do
it 'creates a new release' do
+ expected_job_count = MailScheduler::NotificationServiceWorker.jobs.size + 1
+
result = service.execute
expect(project.releases.count).to eq(1)
@@ -30,6 +32,7 @@ describe Releases::CreateService do
expect(result[:release].name).to eq(name)
expect(result[:release].author).to eq(user)
expect(result[:release].sha).to eq(tag_sha)
+ expect(MailScheduler::NotificationServiceWorker.jobs.size).to eq(expected_job_count)
end
end
diff --git a/spec/services/repository_archive_clean_up_service_spec.rb b/spec/services/repository_archive_clean_up_service_spec.rb
index c0b286ac675..80b177a0174 100644
--- a/spec/services/repository_archive_clean_up_service_spec.rb
+++ b/spec/services/repository_archive_clean_up_service_spec.rb
@@ -110,6 +110,8 @@ describe RepositoryArchiveCleanUpService do
def create_temporary_files(dir, extensions, mtime)
FileUtils.mkdir_p(dir)
+ # rubocop: disable Rails/TimeZone
FileUtils.touch(extensions.map { |ext| File.join(dir, "sample.#{ext}") }, mtime: Time.now - mtime)
+ # rubocop: enable Rails/TimeZone
end
end
diff --git a/spec/services/resource_access_tokens/create_service_spec.rb b/spec/services/resource_access_tokens/create_service_spec.rb
new file mode 100644
index 00000000000..57e7e4e66de
--- /dev/null
+++ b/spec/services/resource_access_tokens/create_service_spec.rb
@@ -0,0 +1,163 @@
+# frozen_string_literal: true
+
+require 'spec_helper'
+
+describe ResourceAccessTokens::CreateService do
+ subject { described_class.new(user, resource, params).execute }
+
+ let_it_be(:user) { create(:user) }
+ let_it_be(:project) { create(:project, :private) }
+ let_it_be(:params) { {} }
+
+ describe '#execute' do
+ # Created shared_examples as it will easy to include specs for group bots in https://gitlab.com/gitlab-org/gitlab/-/issues/214046
+ shared_examples 'fails when user does not have the permission to create a Resource Bot' do
+ before_all do
+ resource.add_developer(user)
+ end
+
+ it 'returns error' do
+ response = subject
+
+ expect(response.error?).to be true
+ expect(response.message).to eq("User does not have permission to create #{resource_type} Access Token")
+ end
+ end
+
+ shared_examples 'fails when flag is disabled' do
+ before do
+ stub_feature_flags(resource_access_token: false)
+ end
+
+ it 'returns nil' do
+ expect(subject).to be nil
+ end
+ end
+
+ shared_examples 'allows creation of bot with valid params' do
+ it { expect { subject }.to change { User.count }.by(1) }
+
+ it 'creates resource bot user' do
+ response = subject
+
+ access_token = response.payload[:access_token]
+
+ expect(access_token.user.reload.user_type).to eq("#{resource_type}_bot")
+ end
+
+ context 'bot name' do
+ context 'when no value is passed' do
+ it 'uses default value' do
+ response = subject
+ access_token = response.payload[:access_token]
+
+ expect(access_token.user.name).to eq("#{resource.name.to_s.humanize} bot")
+ end
+ end
+
+ context 'when user provides value' do
+ let_it_be(:params) { { name: 'Random bot' } }
+
+ it 'overrides the default value' do
+ response = subject
+ access_token = response.payload[:access_token]
+
+ expect(access_token.user.name).to eq(params[:name])
+ end
+ end
+ end
+
+ it 'adds the bot user as a maintainer in the resource' do
+ response = subject
+ access_token = response.payload[:access_token]
+ bot_user = access_token.user
+
+ expect(resource.members.maintainers.map(&:user_id)).to include(bot_user.id)
+ end
+
+ context 'personal access token' do
+ it { expect { subject }.to change { PersonalAccessToken.count }.by(1) }
+
+ context 'when user does not provide scope' do
+ it 'has default scopes' do
+ response = subject
+ access_token = response.payload[:access_token]
+
+ expect(access_token.scopes).to eq(Gitlab::Auth.resource_bot_scopes)
+ end
+ end
+
+ context 'when user provides scope explicitly' do
+ let_it_be(:params) { { scopes: Gitlab::Auth::REPOSITORY_SCOPES } }
+
+ it 'overrides the default value' do
+ response = subject
+ access_token = response.payload[:access_token]
+
+ expect(access_token.scopes).to eq(Gitlab::Auth::REPOSITORY_SCOPES)
+ end
+ end
+
+ context 'expires_at' do
+ context 'when no value is passed' do
+ it 'uses default value' do
+ response = subject
+ access_token = response.payload[:access_token]
+
+ expect(access_token.expires_at).to eq(nil)
+ end
+ end
+
+ context 'when user provides value' do
+ let_it_be(:params) { { expires_at: Date.today + 1.month } }
+
+ it 'overrides the default value' do
+ response = subject
+ access_token = response.payload[:access_token]
+
+ expect(access_token.expires_at).to eq(params[:expires_at])
+ end
+ end
+
+ context 'when invalid scope is passed' do
+ let_it_be(:params) { { scopes: [:invalid_scope] } }
+
+ it 'returns error' do
+ response = subject
+
+ expect(response.error?).to be true
+ end
+ end
+ end
+ end
+
+ context 'when access provisioning fails' do
+ before do
+ allow(resource).to receive(:add_maintainer).and_return(nil)
+ end
+
+ it 'returns error' do
+ response = subject
+
+ expect(response.error?).to be true
+ end
+ end
+ end
+
+ context 'when resource is a project' do
+ let_it_be(:resource_type) { 'project' }
+ let_it_be(:resource) { project }
+
+ it_behaves_like 'fails when user does not have the permission to create a Resource Bot'
+ it_behaves_like 'fails when flag is disabled'
+
+ context 'user with valid permission' do
+ before_all do
+ resource.add_maintainer(user)
+ end
+
+ it_behaves_like 'allows creation of bot with valid params'
+ end
+ end
+ end
+end
diff --git a/spec/services/resource_access_tokens/revoke_service_spec.rb b/spec/services/resource_access_tokens/revoke_service_spec.rb
new file mode 100644
index 00000000000..3ce82745b9e
--- /dev/null
+++ b/spec/services/resource_access_tokens/revoke_service_spec.rb
@@ -0,0 +1,111 @@
+# frozen_string_literal: true
+
+require 'spec_helper'
+
+describe ResourceAccessTokens::RevokeService do
+ subject { described_class.new(user, resource, access_token).execute }
+
+ let_it_be(:user) { create(:user) }
+ let(:access_token) { create(:personal_access_token, user: resource_bot) }
+
+ describe '#execute' do
+ # Created shared_examples as it will easy to include specs for group bots in https://gitlab.com/gitlab-org/gitlab/-/issues/214046
+ shared_examples 'revokes access token' do
+ it { expect(subject.success?).to be true }
+
+ it { expect(subject.message).to eq("Revoked access token: #{access_token.name}") }
+
+ it 'revokes token access' do
+ subject
+
+ expect(access_token.reload.revoked?).to be true
+ end
+
+ it 'removes membership of bot user' do
+ subject
+
+ expect(resource.reload.users).not_to include(resource_bot)
+ end
+
+ it 'transfer issuables of bot user to ghost user' do
+ issue = create(:issue, author: resource_bot)
+
+ subject
+
+ expect(issue.reload.author.ghost?).to be true
+ end
+ end
+
+ shared_examples 'rollback revoke steps' do
+ it 'does not revoke the access token' do
+ subject
+
+ expect(access_token.reload.revoked?).to be false
+ end
+
+ it 'does not remove bot from member list' do
+ subject
+
+ expect(resource.reload.users).to include(resource_bot)
+ end
+
+ it 'does not transfer issuables of bot user to ghost user' do
+ issue = create(:issue, author: resource_bot)
+
+ subject
+
+ expect(issue.reload.author.ghost?).to be false
+ end
+ end
+
+ context 'when resource is a project' do
+ let_it_be(:resource) { create(:project, :private) }
+ let_it_be(:resource_bot) { create(:user, :project_bot) }
+
+ before_all do
+ resource.add_maintainer(user)
+ resource.add_maintainer(resource_bot)
+ end
+
+ it_behaves_like 'revokes access token'
+
+ context 'when revoke fails' do
+ context 'invalid resource type' do
+ subject { described_class.new(user, resource, access_token).execute }
+
+ let_it_be(:resource) { double }
+ let_it_be(:resource_bot) { create(:user, :project_bot) }
+
+ it 'returns error response' do
+ response = subject
+
+ expect(response.success?).to be false
+ expect(response.message).to eq("Failed to find bot user")
+ end
+
+ it { expect { subject }.not_to change(access_token.reload, :revoked) }
+ end
+
+ context 'when migration to ghost user fails' do
+ before do
+ allow_next_instance_of(::Members::DestroyService) do |service|
+ allow(service).to receive(:execute).and_return(false)
+ end
+ end
+
+ it_behaves_like 'rollback revoke steps'
+ end
+
+ context 'when migration to ghost user fails' do
+ before do
+ allow_next_instance_of(::Users::MigrateToGhostUserService) do |service|
+ allow(service).to receive(:execute).and_return(false)
+ end
+ end
+
+ it_behaves_like 'rollback revoke steps'
+ end
+ end
+ end
+ end
+end
diff --git a/spec/services/resource_events/change_milestone_service_spec.rb b/spec/services/resource_events/change_milestone_service_spec.rb
index bc634fadadd..dec01d0db8d 100644
--- a/spec/services/resource_events/change_milestone_service_spec.rb
+++ b/spec/services/resource_events/change_milestone_service_spec.rb
@@ -3,11 +3,9 @@
require 'spec_helper'
describe ResourceEvents::ChangeMilestoneService do
- it_behaves_like 'a milestone events creator' do
- let(:resource) { create(:issue) }
- end
-
- it_behaves_like 'a milestone events creator' do
- let(:resource) { create(:merge_request) }
+ [:issue, :merge_request].each do |issuable|
+ it_behaves_like 'a milestone events creator' do
+ let(:resource) { create(issuable) }
+ end
end
end
diff --git a/spec/services/resource_events/merge_into_notes_service_spec.rb b/spec/services/resource_events/merge_into_notes_service_spec.rb
index 6bad1b86fca..2664a27244d 100644
--- a/spec/services/resource_events/merge_into_notes_service_spec.rb
+++ b/spec/services/resource_events/merge_into_notes_service_spec.rb
@@ -21,7 +21,7 @@ describe ResourceEvents::MergeIntoNotesService do
let_it_be(:resource) { create(:issue, project: project) }
let_it_be(:label) { create(:label, project: project) }
let_it_be(:label2) { create(:label, project: project) }
- let(:time) { Time.now }
+ let(:time) { Time.current }
describe '#execute' do
it 'merges label events into notes in order of created_at' do
diff --git a/spec/services/resources/create_access_token_service_spec.rb b/spec/services/resources/create_access_token_service_spec.rb
deleted file mode 100644
index 8c108d9937a..00000000000
--- a/spec/services/resources/create_access_token_service_spec.rb
+++ /dev/null
@@ -1,163 +0,0 @@
-# frozen_string_literal: true
-
-require 'spec_helper'
-
-describe Resources::CreateAccessTokenService do
- subject { described_class.new(resource_type, resource, user, params).execute }
-
- let_it_be(:user) { create(:user) }
- let_it_be(:project) { create(:project, :private) }
- let_it_be(:params) { {} }
-
- describe '#execute' do
- # Created shared_examples as it will easy to include specs for group bots in https://gitlab.com/gitlab-org/gitlab/-/issues/214046
- shared_examples 'fails when user does not have the permission to create a Resource Bot' do
- before do
- resource.add_developer(user)
- end
-
- it 'returns error' do
- response = subject
-
- expect(response.error?).to be true
- expect(response.message).to eq("User does not have permission to create #{resource_type} Access Token")
- end
- end
-
- shared_examples 'fails when flag is disabled' do
- before do
- stub_feature_flags(resource_access_token: false)
- end
-
- it 'returns nil' do
- expect(subject).to be nil
- end
- end
-
- shared_examples 'allows creation of bot with valid params' do
- it { expect { subject }.to change { User.count }.by(1) }
-
- it 'creates resource bot user' do
- response = subject
-
- access_token = response.payload[:access_token]
-
- expect(access_token.user.reload.user_type).to eq("#{resource_type}_bot")
- end
-
- context 'bot name' do
- context 'when no value is passed' do
- it 'uses default value' do
- response = subject
- access_token = response.payload[:access_token]
-
- expect(access_token.user.name).to eq("#{resource.name.to_s.humanize} bot")
- end
- end
-
- context 'when user provides value' do
- let(:params) { { name: 'Random bot' } }
-
- it 'overrides the default value' do
- response = subject
- access_token = response.payload[:access_token]
-
- expect(access_token.user.name).to eq(params[:name])
- end
- end
- end
-
- it 'adds the bot user as a maintainer in the resource' do
- response = subject
- access_token = response.payload[:access_token]
- bot_user = access_token.user
-
- expect(resource.members.maintainers.map(&:user_id)).to include(bot_user.id)
- end
-
- context 'personal access token' do
- it { expect { subject }.to change { PersonalAccessToken.count }.by(1) }
-
- context 'when user does not provide scope' do
- it 'has default scopes' do
- response = subject
- access_token = response.payload[:access_token]
-
- expect(access_token.scopes).to eq(Gitlab::Auth::API_SCOPES + Gitlab::Auth::REPOSITORY_SCOPES + Gitlab::Auth.registry_scopes - [:read_user])
- end
- end
-
- context 'when user provides scope explicitly' do
- let(:params) { { scopes: Gitlab::Auth::REPOSITORY_SCOPES } }
-
- it 'overrides the default value' do
- response = subject
- access_token = response.payload[:access_token]
-
- expect(access_token.scopes).to eq(Gitlab::Auth::REPOSITORY_SCOPES)
- end
- end
-
- context 'expires_at' do
- context 'when no value is passed' do
- it 'uses default value' do
- response = subject
- access_token = response.payload[:access_token]
-
- expect(access_token.expires_at).to eq(nil)
- end
- end
-
- context 'when user provides value' do
- let(:params) { { expires_at: Date.today + 1.month } }
-
- it 'overrides the default value' do
- response = subject
- access_token = response.payload[:access_token]
-
- expect(access_token.expires_at).to eq(params[:expires_at])
- end
- end
-
- context 'when invalid scope is passed' do
- let(:params) { { scopes: [:invalid_scope] } }
-
- it 'returns error' do
- response = subject
-
- expect(response.error?).to be true
- end
- end
- end
- end
-
- context 'when access provisioning fails' do
- before do
- allow(resource).to receive(:add_maintainer).and_return(nil)
- end
-
- it 'returns error' do
- response = subject
-
- expect(response.error?).to be true
- end
- end
- end
-
- context 'when resource is a project' do
- let(:resource_type) { 'project' }
- let(:resource) { project }
-
- it_behaves_like 'fails when user does not have the permission to create a Resource Bot'
- it_behaves_like 'fails when flag is disabled'
-
- context 'user with valid permission' do
- before do
- resource.add_maintainer(user)
- end
-
- it_behaves_like 'allows creation of bot with valid params'
- end
- end
- end
-end
diff --git a/spec/services/search/snippet_service_spec.rb b/spec/services/search/snippet_service_spec.rb
index 430c71880a3..cb2bb0c43fd 100644
--- a/spec/services/search/snippet_service_spec.rb
+++ b/spec/services/search/snippet_service_spec.rb
@@ -3,59 +3,67 @@
require 'spec_helper'
describe Search::SnippetService do
- let(:author) { create(:author) }
- let(:project) { create(:project, :public) }
+ let_it_be(:author) { create(:author) }
+ let_it_be(:project) { create(:project, :public) }
- let!(:public_snippet) { create(:snippet, :public, content: 'password: XXX') }
- let!(:internal_snippet) { create(:snippet, :internal, content: 'password: XXX') }
- let!(:private_snippet) { create(:snippet, :private, content: 'password: XXX', author: author) }
+ let_it_be(:public_snippet) { create(:snippet, :public, title: 'Foo Bar Title') }
+ let_it_be(:internal_snippet) { create(:snippet, :internal, title: 'Foo Bar Title') }
+ let_it_be(:private_snippet) { create(:snippet, :private, title: 'Foo Bar Title', author: author) }
- let!(:project_public_snippet) { create(:snippet, :public, project: project, content: 'password: XXX') }
- let!(:project_internal_snippet) { create(:snippet, :internal, project: project, content: 'password: XXX') }
- let!(:project_private_snippet) { create(:snippet, :private, project: project, content: 'password: XXX') }
+ let_it_be(:project_public_snippet) { create(:snippet, :public, project: project, title: 'Foo Bar Title') }
+ let_it_be(:project_internal_snippet) { create(:snippet, :internal, project: project, title: 'Foo Bar Title') }
+ let_it_be(:project_private_snippet) { create(:snippet, :private, project: project, title: 'Foo Bar Title') }
+
+ let_it_be(:user) { create(:user) }
describe '#execute' do
context 'unauthenticated' do
it 'returns public snippets only' do
- search = described_class.new(nil, search: 'password')
+ search = described_class.new(nil, search: 'bar')
results = search.execute
- expect(results.objects('snippet_blobs')).to match_array [public_snippet, project_public_snippet]
+ expect(results.objects('snippet_titles')).to match_array [public_snippet, project_public_snippet]
end
end
context 'authenticated' do
it 'returns only public & internal snippets for regular users' do
- user = create(:user)
- search = described_class.new(user, search: 'password')
+ search = described_class.new(user, search: 'bar')
results = search.execute
- expect(results.objects('snippet_blobs')).to match_array [public_snippet, internal_snippet, project_public_snippet, project_internal_snippet]
+ expect(results.objects('snippet_titles')).to match_array [public_snippet, internal_snippet, project_public_snippet, project_internal_snippet]
end
it 'returns public, internal snippets and project private snippets for project members' do
- member = create(:user)
- project.add_developer(member)
- search = described_class.new(member, search: 'password')
+ project.add_developer(user)
+ search = described_class.new(user, search: 'bar')
results = search.execute
- expect(results.objects('snippet_blobs')).to match_array [public_snippet, internal_snippet, project_public_snippet, project_internal_snippet, project_private_snippet]
+ expect(results.objects('snippet_titles')).to match_array [public_snippet, internal_snippet, project_public_snippet, project_internal_snippet, project_private_snippet]
end
it 'returns public, internal and private snippets where user is the author' do
- search = described_class.new(author, search: 'password')
+ search = described_class.new(author, search: 'bar')
results = search.execute
- expect(results.objects('snippet_blobs')).to match_array [public_snippet, internal_snippet, private_snippet, project_public_snippet, project_internal_snippet]
+ expect(results.objects('snippet_titles')).to match_array [public_snippet, internal_snippet, private_snippet, project_public_snippet, project_internal_snippet]
end
it 'returns all snippets when user is admin' do
admin = create(:admin)
- search = described_class.new(admin, search: 'password')
+ search = described_class.new(admin, search: 'bar')
results = search.execute
- expect(results.objects('snippet_blobs')).to match_array [public_snippet, internal_snippet, private_snippet, project_public_snippet, project_internal_snippet, project_private_snippet]
+ expect(results.objects('snippet_titles')).to match_array [public_snippet, internal_snippet, private_snippet, project_public_snippet, project_internal_snippet, project_private_snippet]
end
end
end
+
+ describe '#scope' do
+ it 'always scopes to snippet_titles' do
+ search = described_class.new(user, search: 'bar')
+
+ expect(search.scope).to eq 'snippet_titles'
+ end
+ end
end
diff --git a/spec/services/search_service_spec.rb b/spec/services/search_service_spec.rb
index 97d7ca6e1ad..0333eb85fb6 100644
--- a/spec/services/search_service_spec.rb
+++ b/spec/services/search_service_spec.rb
@@ -18,7 +18,9 @@ describe SearchService do
let(:group_project) { create(:project, group: accessible_group, name: 'group_project') }
let(:public_project) { create(:project, :public, name: 'public_project') }
- subject(:search_service) { described_class.new(user, search: search, scope: scope, page: 1) }
+ let(:per_page) { described_class::DEFAULT_PER_PAGE }
+
+ subject(:search_service) { described_class.new(user, search: search, scope: scope, page: 1, per_page: per_page) }
before do
accessible_project.add_maintainer(user)
@@ -151,7 +153,7 @@ describe SearchService do
it 'returns the default scope' do
scope = described_class.new(user, snippets: 'true', scope: 'projects').scope
- expect(scope).to eq 'snippet_blobs'
+ expect(scope).to eq 'snippet_titles'
end
end
@@ -159,7 +161,7 @@ describe SearchService do
it 'returns the default scope' do
scope = described_class.new(user, snippets: 'true').scope
- expect(scope).to eq 'snippet_blobs'
+ expect(scope).to eq 'snippet_titles'
end
end
end
@@ -222,7 +224,7 @@ describe SearchService do
search_results = described_class.new(
user,
snippets: 'true',
- search: snippet.content).search_results
+ search: snippet.title).search_results
expect(search_results).to be_a Gitlab::SnippetSearchResults
end
@@ -240,6 +242,76 @@ describe SearchService do
end
describe '#search_objects' do
+ context 'handling per_page param' do
+ let(:search) { '' }
+ let(:scope) { nil }
+
+ context 'when nil' do
+ let(:per_page) { nil }
+
+ it "defaults to #{described_class::DEFAULT_PER_PAGE}" do
+ expect_any_instance_of(Gitlab::SearchResults)
+ .to receive(:objects)
+ .with(anything, hash_including(per_page: described_class::DEFAULT_PER_PAGE))
+ .and_call_original
+
+ subject.search_objects
+ end
+ end
+
+ context 'when empty string' do
+ let(:per_page) { '' }
+
+ it "defaults to #{described_class::DEFAULT_PER_PAGE}" do
+ expect_any_instance_of(Gitlab::SearchResults)
+ .to receive(:objects)
+ .with(anything, hash_including(per_page: described_class::DEFAULT_PER_PAGE))
+ .and_call_original
+
+ subject.search_objects
+ end
+ end
+
+ context 'when negative' do
+ let(:per_page) { '-1' }
+
+ it "defaults to #{described_class::DEFAULT_PER_PAGE}" do
+ expect_any_instance_of(Gitlab::SearchResults)
+ .to receive(:objects)
+ .with(anything, hash_including(per_page: described_class::DEFAULT_PER_PAGE))
+ .and_call_original
+
+ subject.search_objects
+ end
+ end
+
+ context 'when present' do
+ let(:per_page) { '50' }
+
+ it "converts to integer and passes to search results" do
+ expect_any_instance_of(Gitlab::SearchResults)
+ .to receive(:objects)
+ .with(anything, hash_including(per_page: 50))
+ .and_call_original
+
+ subject.search_objects
+ end
+ end
+
+ context "when greater than #{described_class::MAX_PER_PAGE}" do
+ let(:per_page) { described_class::MAX_PER_PAGE + 1 }
+
+ it "passes #{described_class::MAX_PER_PAGE}" do
+ expect_any_instance_of(Gitlab::SearchResults)
+ .to receive(:objects)
+ .with(anything, hash_including(per_page: described_class::MAX_PER_PAGE))
+ .and_call_original
+
+ subject.search_objects
+ end
+ end
+ end
+
context 'with accessible project_id' do
it 'returns objects in the project' do
search_objects = described_class.new(
@@ -270,7 +342,7 @@ describe SearchService do
search_objects = described_class.new(
user,
snippets: 'true',
- search: snippet.content).search_objects
+ search: snippet.title).search_objects
expect(search_objects.first).to eq snippet
end
@@ -383,7 +455,7 @@ describe SearchService do
let(:readable) { create(:project_snippet, project: accessible_project) }
let(:unreadable) { create(:project_snippet, project: inaccessible_project) }
let(:unredacted_results) { ar_relation(ProjectSnippet, readable, unreadable) }
- let(:scope) { 'snippet_blobs' }
+ let(:scope) { 'snippet_titles' }
it 'redacts the inaccessible snippet' do
expect(result).to contain_exactly(readable)
@@ -394,7 +466,7 @@ describe SearchService do
let(:readable) { create(:personal_snippet, :private, author: user) }
let(:unreadable) { create(:personal_snippet, :private) }
let(:unredacted_results) { ar_relation(PersonalSnippet, readable, unreadable) }
- let(:scope) { 'snippet_blobs' }
+ let(:scope) { 'snippet_titles' }
it 'redacts the inaccessible snippet' do
expect(result).to contain_exactly(readable)
diff --git a/spec/services/snippets/create_service_spec.rb b/spec/services/snippets/create_service_spec.rb
index c1a8a026b90..786fc3ec8dd 100644
--- a/spec/services/snippets/create_service_spec.rb
+++ b/spec/services/snippets/create_service_spec.rb
@@ -74,47 +74,6 @@ describe Snippets::CreateService do
end
end
- shared_examples 'spam check is performed' do
- shared_examples 'marked as spam' do
- it 'marks a snippet as spam' do
- expect(snippet).to be_spam
- end
-
- it 'invalidates the snippet' do
- expect(snippet).to be_invalid
- end
-
- it 'creates a new spam_log' do
- expect { snippet }
- .to have_spam_log(title: snippet.title, noteable_type: snippet.class.name)
- end
-
- it 'assigns a spam_log to an issue' do
- expect(snippet.spam_log).to eq(SpamLog.last)
- end
- end
-
- let(:extra_opts) do
- { visibility_level: Gitlab::VisibilityLevel::PUBLIC, request: double(:request, env: {}) }
- end
-
- before do
- expect_next_instance_of(Spam::AkismetService) do |akismet_service|
- expect(akismet_service).to receive_messages(spam?: true)
- end
- end
-
- [true, false, nil].each do |allow_possible_spam|
- context "when recaptcha_disabled flag is #{allow_possible_spam.inspect}" do
- before do
- stub_feature_flags(allow_possible_spam: allow_possible_spam) unless allow_possible_spam.nil?
- end
-
- it_behaves_like 'marked as spam'
- end
- end
- end
-
shared_examples 'snippet create data is tracked' do
let(:counter) { Gitlab::UsageDataCounters::SnippetCounter }
@@ -169,8 +128,8 @@ describe Snippets::CreateService do
expect { subject }.not_to change { Snippet.count }
end
- it 'returns the error' do
- expect(snippet.errors.full_messages).to include('Repository could not be created')
+ it 'returns a generic creation error' do
+ expect(snippet.errors[:repository]).to eq ['Error creating the snippet - Repository could not be created']
end
it 'does not return a snippet with an id' do
@@ -178,6 +137,14 @@ describe Snippets::CreateService do
end
end
+ context 'when repository creation fails with invalid file name' do
+ let(:extra_opts) { { file_name: 'invalid://file/name/here' } }
+
+ it 'returns an appropriate error' do
+ expect(snippet.errors[:repository]).to eq ['Error creating the snippet - Invalid file name']
+ end
+ end
+
context 'when the commit action fails' do
before do
allow_next_instance_of(SnippetRepository) do |instance|
@@ -209,11 +176,11 @@ describe Snippets::CreateService do
subject
end
- it 'returns the error' do
+ it 'returns a generic error' do
response = subject
expect(response).to be_error
- expect(response.payload[:snippet].errors.full_messages).to eq ['foobar']
+ expect(response.payload[:snippet].errors[:repository]).to eq ['Error creating the snippet']
end
end
@@ -228,36 +195,14 @@ describe Snippets::CreateService do
expect(snippet.repository_exists?).to be_falsey
end
end
-
- context 'when feature flag :version_snippets is disabled' do
- before do
- stub_feature_flags(version_snippets: false)
- end
-
- it 'does not create snippet repository' do
- expect do
- subject
- end.to change(Snippet, :count).by(1)
-
- expect(snippet.repository_exists?).to be_falsey
- end
-
- it 'does not try to commit files' do
- expect_next_instance_of(described_class) do |instance|
- expect(instance).not_to receive(:create_commit)
- end
-
- subject
- end
- end
end
- shared_examples 'after_save callback to store_mentions' do
+ shared_examples 'after_save callback to store_mentions' do |mentionable_class|
context 'when mentionable attributes change' do
let(:extra_opts) { { description: "Description with #{user.to_reference}" } }
it 'saves mentions' do
- expect_next_instance_of(Snippet) do |instance|
+ expect_next_instance_of(mentionable_class) do |instance|
expect(instance).to receive(:store_mentions!).and_call_original
end
expect(snippet.user_mentions.count).to eq 1
@@ -266,7 +211,7 @@ describe Snippets::CreateService do
context 'when mentionable attributes do not change' do
it 'does not call store_mentions' do
- expect_next_instance_of(Snippet) do |instance|
+ expect_next_instance_of(mentionable_class) do |instance|
expect(instance).not_to receive(:store_mentions!)
end
expect(snippet.user_mentions.count).to eq 0
@@ -277,7 +222,7 @@ describe Snippets::CreateService do
it 'does not call store_mentions' do
base_opts.delete(:title)
- expect_next_instance_of(Snippet) do |instance|
+ expect_next_instance_of(mentionable_class) do |instance|
expect(instance).not_to receive(:store_mentions!)
end
expect(snippet.valid?).to be false
@@ -294,11 +239,25 @@ describe Snippets::CreateService do
it_behaves_like 'a service that creates a snippet'
it_behaves_like 'public visibility level restrictions apply'
- it_behaves_like 'spam check is performed'
+ it_behaves_like 'snippets spam check is performed'
it_behaves_like 'snippet create data is tracked'
it_behaves_like 'an error service response when save fails'
it_behaves_like 'creates repository and files'
- it_behaves_like 'after_save callback to store_mentions'
+ it_behaves_like 'after_save callback to store_mentions', ProjectSnippet
+
+ context 'when uploaded files are passed to the service' do
+ let(:extra_opts) { { files: ['foo'] } }
+
+ it 'does not move uploaded files to the snippet' do
+ expect_next_instance_of(described_class) do |instance|
+ expect(instance).to receive(:move_temporary_files).and_call_original
+ end
+
+ expect_any_instance_of(FileMover).not_to receive(:execute)
+
+ subject
+ end
+ end
end
context 'when PersonalSnippet' do
@@ -306,12 +265,55 @@ describe Snippets::CreateService do
it_behaves_like 'a service that creates a snippet'
it_behaves_like 'public visibility level restrictions apply'
- it_behaves_like 'spam check is performed'
+ it_behaves_like 'snippets spam check is performed'
it_behaves_like 'snippet create data is tracked'
it_behaves_like 'an error service response when save fails'
it_behaves_like 'creates repository and files'
- pending('See https://gitlab.com/gitlab-org/gitlab/issues/30742') do
- it_behaves_like 'after_save callback to store_mentions'
+ it_behaves_like 'after_save callback to store_mentions', PersonalSnippet
+
+ context 'when the snippet description contains files' do
+ include FileMoverHelpers
+
+ let(:title) { 'Title' }
+ let(:picture_secret) { SecureRandom.hex }
+ let(:text_secret) { SecureRandom.hex }
+ let(:picture_file) { "/-/system/user/#{creator.id}/#{picture_secret}/picture.jpg" }
+ let(:text_file) { "/-/system/user/#{creator.id}/#{text_secret}/text.txt" }
+ let(:files) { [picture_file, text_file] }
+ let(:description) do
+ "Description with picture: ![picture](/uploads#{picture_file}) and "\
+ "text: [text.txt](/uploads#{text_file})"
+ end
+
+ before do
+ allow(FileUtils).to receive(:mkdir_p)
+ allow(FileUtils).to receive(:move)
+ end
+
+ let(:extra_opts) { { description: description, title: title, files: files } }
+
+ it 'stores the snippet description correctly' do
+ stub_file_mover(text_file)
+ stub_file_mover(picture_file)
+
+ snippet = subject.payload[:snippet]
+
+ expected_description = "Description with picture: "\
+ "![picture](/uploads/-/system/personal_snippet/#{snippet.id}/#{picture_secret}/picture.jpg) and "\
+ "text: [text.txt](/uploads/-/system/personal_snippet/#{snippet.id}/#{text_secret}/text.txt)"
+
+ expect(snippet.description).to eq(expected_description)
+ end
+
+ context 'when there is a validation error' do
+ let(:title) { nil }
+
+ it 'does not move uploaded files to the snippet' do
+ expect_any_instance_of(described_class).not_to receive(:move_temporary_files)
+
+ subject
+ end
+ end
end
end
end
diff --git a/spec/services/snippets/update_service_spec.rb b/spec/services/snippets/update_service_spec.rb
index 05fb725c065..38747ae907f 100644
--- a/spec/services/snippets/update_service_spec.rb
+++ b/spec/services/snippets/update_service_spec.rb
@@ -7,7 +7,7 @@ describe Snippets::UpdateService do
let_it_be(:user) { create(:user) }
let_it_be(:admin) { create :user, admin: true }
let(:visibility_level) { Gitlab::VisibilityLevel::PRIVATE }
- let(:options) do
+ let(:base_opts) do
{
title: 'Test snippet',
file_name: 'snippet.rb',
@@ -15,6 +15,8 @@ describe Snippets::UpdateService do
visibility_level: visibility_level
}
end
+ let(:extra_opts) { {} }
+ let(:options) { base_opts.merge(extra_opts) }
let(:updater) { user }
let(:service) { Snippets::UpdateService.new(project, updater, options) }
@@ -85,7 +87,7 @@ describe Snippets::UpdateService do
end
context 'when update fails' do
- let(:options) { { title: '' } }
+ let(:extra_opts) { { title: '' } }
it 'does not increment count' do
expect { subject }.not_to change { counter.read(:update) }
@@ -112,25 +114,16 @@ describe Snippets::UpdateService do
expect(blob.data).to eq options[:content]
end
- context 'when the repository does not exist' do
- it 'does not try to commit file' do
- allow(snippet).to receive(:repository_exists?).and_return(false)
-
- expect(service).not_to receive(:create_commit)
-
- subject
- end
- end
-
- context 'when feature flag is disabled' do
+ context 'when the repository creation fails' do
before do
- stub_feature_flags(version_snippets: false)
+ allow(snippet).to receive(:repository_exists?).and_return(false)
end
- it 'does not create repository' do
- subject
+ it 'raise an error' do
+ response = subject
- expect(snippet.repository).not_to exist
+ expect(response).to be_error
+ expect(response.payload[:snippet].errors[:repository].to_sentence).to eq 'Error updating the snippet - Repository could not be created'
end
it 'does not try to commit file' do
@@ -205,14 +198,24 @@ describe Snippets::UpdateService do
end
end
- it 'rolls back any snippet modifications' do
- option_keys = options.stringify_keys.keys
- orig_attrs = snippet.attributes.select { |k, v| k.in?(option_keys) }
+ context 'with snippet modifications' do
+ let(:option_keys) { options.stringify_keys.keys }
- subject
+ it 'rolls back any snippet modifications' do
+ orig_attrs = snippet.attributes.select { |k, v| k.in?(option_keys) }
+
+ subject
+
+ persisted_attrs = snippet.reload.attributes.select { |k, v| k.in?(option_keys) }
+ expect(orig_attrs).to eq persisted_attrs
+ end
+
+ it 'keeps any snippet modifications' do
+ subject
- current_attrs = snippet.attributes.select { |k, v| k.in?(option_keys) }
- expect(orig_attrs).to eq current_attrs
+ instance_attrs = snippet.attributes.select { |k, v| k.in?(option_keys) }
+ expect(options.stringify_keys).to eq instance_attrs
+ end
end
end
@@ -270,6 +273,35 @@ describe Snippets::UpdateService do
end
end
+ shared_examples 'committable attributes' do
+ context 'when file_name is updated' do
+ let(:extra_opts) { { file_name: 'snippet.rb' } }
+
+ it 'commits to repository' do
+ expect(service).to receive(:create_commit)
+ expect(subject).to be_success
+ end
+ end
+
+ context 'when content is updated' do
+ let(:extra_opts) { { content: 'puts "hello world"' } }
+
+ it 'commits to repository' do
+ expect(service).to receive(:create_commit)
+ expect(subject).to be_success
+ end
+ end
+
+ context 'when content or file_name is not updated' do
+ let(:options) { { title: 'Test snippet' } }
+
+ it 'does not perform any commit' do
+ expect(service).not_to receive(:create_commit)
+ expect(subject).to be_success
+ end
+ end
+ end
+
context 'when Project Snippet' do
let_it_be(:project) { create(:project) }
let!(:snippet) { create(:project_snippet, :repository, author: user, project: project) }
@@ -283,6 +315,12 @@ describe Snippets::UpdateService do
it_behaves_like 'snippet update data is tracked'
it_behaves_like 'updates repository content'
it_behaves_like 'commit operation fails'
+ it_behaves_like 'committable attributes'
+ it_behaves_like 'snippets spam check is performed' do
+ before do
+ subject
+ end
+ end
context 'when snippet does not have a repository' do
let!(:snippet) { create(:project_snippet, author: user, project: project) }
@@ -301,6 +339,12 @@ describe Snippets::UpdateService do
it_behaves_like 'snippet update data is tracked'
it_behaves_like 'updates repository content'
it_behaves_like 'commit operation fails'
+ it_behaves_like 'committable attributes'
+ it_behaves_like 'snippets spam check is performed' do
+ before do
+ subject
+ end
+ end
context 'when snippet does not have a repository' do
let!(:snippet) { create(:personal_snippet, author: user, project: project) }
diff --git a/spec/services/spam/spam_action_service_spec.rb b/spec/services/spam/spam_action_service_spec.rb
new file mode 100644
index 00000000000..560833aba97
--- /dev/null
+++ b/spec/services/spam/spam_action_service_spec.rb
@@ -0,0 +1,215 @@
+# frozen_string_literal: true
+
+require 'spec_helper'
+
+describe Spam::SpamActionService do
+ include_context 'includes Spam constants'
+
+ let(:fake_ip) { '1.2.3.4' }
+ let(:fake_user_agent) { 'fake-user-agent' }
+ let(:fake_referrer) { 'fake-http-referrer' }
+ let(:env) do
+ { 'action_dispatch.remote_ip' => fake_ip,
+ 'HTTP_USER_AGENT' => fake_user_agent,
+ 'HTTP_REFERRER' => fake_referrer }
+ end
+ let(:request) { double(:request, env: env) }
+
+ let_it_be(:project) { create(:project, :public) }
+ let_it_be(:user) { create(:user) }
+ let(:issue) { create(:issue, project: project, author: user) }
+
+ before do
+ issue.spam = false
+ end
+
+ describe '#initialize' do
+ subject { described_class.new(spammable: issue, request: request) }
+
+ context 'when the request is nil' do
+ let(:request) { nil }
+
+ it 'assembles the options with information from the spammable' do
+ aggregate_failures do
+ expect(subject.options[:ip_address]).to eq(issue.ip_address)
+ expect(subject.options[:user_agent]).to eq(issue.user_agent)
+ expect(subject.options.key?(:referrer)).to be_falsey
+ end
+ end
+ end
+
+ context 'when the request is present' do
+ let(:request) { double(:request, env: env) }
+
+ it 'assembles the options with information from the spammable' do
+ aggregate_failures do
+ expect(subject.options[:ip_address]).to eq(fake_ip)
+ expect(subject.options[:user_agent]).to eq(fake_user_agent)
+ expect(subject.options[:referrer]).to eq(fake_referrer)
+ end
+ end
+ end
+ end
+
+ shared_examples 'only checks for spam if a request is provided' do
+ context 'when request is missing' do
+ subject { described_class.new(spammable: issue, request: nil) }
+
+ it "doesn't check as spam" do
+ subject
+
+ expect(issue).not_to be_spam
+ end
+ end
+
+ context 'when request exists' do
+ it 'creates a spam log' do
+ expect { subject }
+ .to log_spam(title: issue.title, description: issue.description, noteable_type: 'Issue')
+ end
+ end
+ end
+
+ describe '#execute' do
+ let(:request) { double(:request, env: env) }
+ let(:fake_verdict_service) { double(:spam_verdict_service) }
+ let(:allowlisted) { false }
+
+ let_it_be(:existing_spam_log) { create(:spam_log, user: user, recaptcha_verified: false) }
+
+ subject do
+ described_service = described_class.new(spammable: issue, request: request)
+ allow(described_service).to receive(:allowlisted?).and_return(allowlisted)
+ described_service.execute(user: user, api: nil, recaptcha_verified: recaptcha_verified, spam_log_id: existing_spam_log.id)
+ end
+
+ before do
+ allow(Spam::SpamVerdictService).to receive(:new).and_return(fake_verdict_service)
+ end
+
+ context 'when reCAPTCHA was already verified' do
+ let(:recaptcha_verified) { true }
+
+ it "doesn't check with the SpamVerdictService" do
+ aggregate_failures do
+ expect(SpamLog).to receive(:verify_recaptcha!)
+ expect(fake_verdict_service).not_to receive(:execute)
+ end
+
+ subject
+ end
+
+ it 'updates spam log' do
+ expect { subject }.to change { existing_spam_log.reload.recaptcha_verified }.from(false).to(true)
+ end
+ end
+
+ context 'when reCAPTCHA was not verified' do
+ let(:recaptcha_verified) { false }
+
+ context 'when spammable attributes have not changed' do
+ before do
+ issue.closed_at = Time.zone.now
+ end
+
+ it 'does not create a spam log' do
+ expect { subject }
+ .not_to change { SpamLog.count }
+ end
+ end
+
+ context 'when spammable attributes have changed' do
+ before do
+ issue.description = 'SPAM!'
+ end
+
+ context 'if allowlisted' do
+ let(:allowlisted) { true }
+
+ it 'does not perform spam check' do
+ expect(Spam::SpamVerdictService).not_to receive(:new)
+
+ subject
+ end
+ end
+
+ context 'when disallowed by the spam verdict service' do
+ before do
+ allow(fake_verdict_service).to receive(:execute).and_return(DISALLOW)
+ end
+
+ context 'when allow_possible_spam feature flag is false' do
+ before do
+ stub_feature_flags(allow_possible_spam: false)
+ end
+
+ it_behaves_like 'only checks for spam if a request is provided'
+
+ it 'marks as spam' do
+ subject
+
+ expect(issue).to be_spam
+ end
+ end
+
+ context 'when allow_possible_spam feature flag is true' do
+ it_behaves_like 'only checks for spam if a request is provided'
+
+ it 'does not mark as spam' do
+ subject
+
+ expect(issue).not_to be_spam
+ end
+ end
+ end
+
+ context 'when spam verdict service requires reCAPTCHA' do
+ before do
+ allow(fake_verdict_service).to receive(:execute).and_return(REQUIRE_RECAPTCHA)
+ end
+
+ context 'when allow_possible_spam feature flag is false' do
+ before do
+ stub_feature_flags(allow_possible_spam: false)
+ end
+
+ it_behaves_like 'only checks for spam if a request is provided'
+
+ it 'does not mark as spam' do
+ subject
+
+ expect(issue).not_to be_spam
+ end
+
+ it 'marks as needing reCAPTCHA' do
+ subject
+
+ expect(issue.needs_recaptcha?).to be_truthy
+ end
+ end
+
+ context 'when allow_possible_spam feature flag is true' do
+ it_behaves_like 'only checks for spam if a request is provided'
+
+ it 'does not mark as needing reCAPTCHA' do
+ subject
+
+ expect(issue.needs_recaptcha).to be_falsey
+ end
+ end
+ end
+
+ context 'when spam verdict service allows creation' do
+ before do
+ allow(fake_verdict_service).to receive(:execute).and_return(ALLOW)
+ end
+
+ it 'does not create a spam log' do
+ expect { subject }
+ .not_to change { SpamLog.count }
+ end
+ end
+ end
+ end
+ end
+end
diff --git a/spec/services/spam/spam_check_service_spec.rb b/spec/services/spam/spam_check_service_spec.rb
deleted file mode 100644
index 3d0cb1447bd..00000000000
--- a/spec/services/spam/spam_check_service_spec.rb
+++ /dev/null
@@ -1,170 +0,0 @@
-# frozen_string_literal: true
-
-require 'spec_helper'
-
-describe Spam::SpamCheckService do
- let(:fake_ip) { '1.2.3.4' }
- let(:fake_user_agent) { 'fake-user-agent' }
- let(:fake_referrer) { 'fake-http-referrer' }
- let(:env) do
- { 'action_dispatch.remote_ip' => fake_ip,
- 'HTTP_USER_AGENT' => fake_user_agent,
- 'HTTP_REFERRER' => fake_referrer }
- end
- let(:request) { double(:request, env: env) }
-
- let_it_be(:project) { create(:project, :public) }
- let_it_be(:user) { create(:user) }
- let_it_be(:issue) { create(:issue, project: project, author: user) }
-
- before do
- issue.spam = false
- end
-
- describe '#initialize' do
- subject { described_class.new(spammable: issue, request: request) }
-
- context 'when the request is nil' do
- let(:request) { nil }
-
- it 'assembles the options with information from the spammable' do
- aggregate_failures do
- expect(subject.options[:ip_address]).to eq(issue.ip_address)
- expect(subject.options[:user_agent]).to eq(issue.user_agent)
- expect(subject.options.key?(:referrer)).to be_falsey
- end
- end
- end
-
- context 'when the request is present' do
- let(:request) { double(:request, env: env) }
-
- it 'assembles the options with information from the spammable' do
- aggregate_failures do
- expect(subject.options[:ip_address]).to eq(fake_ip)
- expect(subject.options[:user_agent]).to eq(fake_user_agent)
- expect(subject.options[:referrer]).to eq(fake_referrer)
- end
- end
- end
- end
-
- shared_examples 'only checks for spam if a request is provided' do
- context 'when request is missing' do
- let(:request) { nil }
-
- it "doesn't check as spam" do
- subject
-
- expect(issue).not_to be_spam
- end
- end
-
- context 'when request exists' do
- it 'creates a spam log' do
- expect { subject }
- .to log_spam(title: issue.title, description: issue.description, noteable_type: 'Issue')
- end
- end
- end
-
- describe '#execute' do
- let(:request) { double(:request, env: env) }
-
- let_it_be(:existing_spam_log) { create(:spam_log, user: user, recaptcha_verified: false) }
-
- subject do
- described_service = described_class.new(spammable: issue, request: request)
- described_service.execute(user_id: user.id, api: nil, recaptcha_verified: recaptcha_verified, spam_log_id: existing_spam_log.id)
- end
-
- context 'when recaptcha was already verified' do
- let(:recaptcha_verified) { true }
-
- it "updates spam log and doesn't check Akismet" do
- aggregate_failures do
- expect(SpamLog).not_to receive(:create!)
- expect(an_instance_of(described_class)).not_to receive(:check)
- end
-
- subject
- end
-
- it 'updates spam log' do
- expect { subject }.to change { existing_spam_log.reload.recaptcha_verified }.from(false).to(true)
- end
- end
-
- context 'when recaptcha was not verified' do
- let(:recaptcha_verified) { false }
-
- context 'when spammable attributes have not changed' do
- before do
- issue.closed_at = Time.zone.now
-
- allow(Spam::AkismetService).to receive(:new).and_return(double(spam?: true))
- end
-
- it 'returns false' do
- expect(subject).to be_falsey
- end
-
- it 'does not create a spam log' do
- expect { subject }
- .not_to change { SpamLog.count }
- end
- end
-
- context 'when spammable attributes have changed' do
- before do
- issue.description = 'SPAM!'
- end
-
- context 'when indicated as spam by Akismet' do
- before do
- allow(Spam::AkismetService).to receive(:new).and_return(double(spam?: true))
- end
-
- context 'when allow_possible_spam feature flag is false' do
- before do
- stub_feature_flags(allow_possible_spam: false)
- end
-
- it_behaves_like 'only checks for spam if a request is provided'
-
- it 'marks as spam' do
- subject
-
- expect(issue).to be_spam
- end
- end
-
- context 'when allow_possible_spam feature flag is true' do
- it_behaves_like 'only checks for spam if a request is provided'
-
- it 'does not mark as spam' do
- subject
-
- expect(issue).not_to be_spam
- end
- end
- end
-
- context 'when not indicated as spam by Akismet' do
- before do
- allow(Spam::AkismetService).to receive(:new).and_return(double(spam?: false))
- end
-
- it 'returns false' do
- expect(subject).to be_falsey
- end
-
- it 'does not create a spam log' do
- expect { subject }
- .not_to change { SpamLog.count }
- end
- end
- end
- end
- end
-end
diff --git a/spec/services/spam/spam_verdict_service_spec.rb b/spec/services/spam/spam_verdict_service_spec.rb
new file mode 100644
index 00000000000..93460a5e7d7
--- /dev/null
+++ b/spec/services/spam/spam_verdict_service_spec.rb
@@ -0,0 +1,65 @@
+# frozen_string_literal: true
+
+require 'spec_helper'
+
+describe Spam::SpamVerdictService do
+ include_context 'includes Spam constants'
+
+ let(:fake_ip) { '1.2.3.4' }
+ let(:fake_user_agent) { 'fake-user-agent' }
+ let(:fake_referrer) { 'fake-http-referrer' }
+ let(:env) do
+ { 'action_dispatch.remote_ip' => fake_ip,
+ 'HTTP_USER_AGENT' => fake_user_agent,
+ 'HTTP_REFERRER' => fake_referrer }
+ end
+ let(:request) { double(:request, env: env) }
+
+ let(:check_for_spam) { true }
+ let(:issue) { build(:issue) }
+ let(:service) do
+ described_class.new(target: issue, request: request, options: {})
+ end
+
+ describe '#execute' do
+ subject { service.execute }
+
+ before do
+ allow_next_instance_of(Spam::AkismetService) do |service|
+ allow(service).to receive(:spam?).and_return(spam_verdict)
+ end
+ end
+
+ context 'if Akismet considers it spam' do
+ let(:spam_verdict) { true }
+
+ context 'if reCAPTCHA is enabled' do
+ before do
+ stub_application_setting(recaptcha_enabled: true)
+ end
+
+ it 'requires reCAPTCHA' do
+ expect(subject).to eq REQUIRE_RECAPTCHA
+ end
+ end
+
+ context 'if reCAPTCHA is not enabled' do
+ before do
+ stub_application_setting(recaptcha_enabled: false)
+ end
+
+ it 'disallows the change' do
+ expect(subject).to eq DISALLOW
+ end
+ end
+ end
+
+ context 'if Akismet does not consider it spam' do
+ let(:spam_verdict) { false }
+
+ it 'allows the change' do
+ expect(subject).to eq ALLOW
+ end
+ end
+ end
+end
diff --git a/spec/services/system_note_service_spec.rb b/spec/services/system_note_service_spec.rb
index 5b87ec022ae..66f9b5d092f 100644
--- a/spec/services/system_note_service_spec.rb
+++ b/spec/services/system_note_service_spec.rb
@@ -6,6 +6,7 @@ describe SystemNoteService do
include Gitlab::Routing
include RepoHelpers
include AssetsHelpers
+ include DesignManagementTestHelpers
let_it_be(:group) { create(:group) }
let_it_be(:project) { create(:project, :repository, group: group) }
@@ -462,7 +463,8 @@ describe SystemNoteService do
describe "existing reference" do
before do
allow(JIRA::Resource::Remotelink).to receive(:all).and_return([])
- message = "[#{author.name}|http://localhost/#{author.username}] mentioned this issue in [a commit of #{project.full_path}|http://localhost/#{project.full_path}/-/commit/#{commit.id}]:\n'#{commit.title.chomp}'"
+ message = double('message')
+ allow(message).to receive(:include?) { true }
allow_next_instance_of(JIRA::Resource::Issue) do |instance|
allow(instance).to receive(:comments).and_return([OpenStruct.new(body: message)])
end
@@ -635,4 +637,28 @@ describe SystemNoteService do
described_class.auto_resolve_prometheus_alert(noteable, project, author)
end
end
+
+ describe '.design_version_added' do
+ let(:version) { create(:design_version) }
+
+ it 'calls DesignManagementService' do
+ expect_next_instance_of(SystemNotes::DesignManagementService) do |service|
+ expect(service).to receive(:design_version_added).with(version)
+ end
+
+ described_class.design_version_added(version)
+ end
+ end
+
+ describe '.design_discussion_added' do
+ let(:discussion_note) { create(:diff_note_on_design) }
+
+ it 'calls DesignManagementService' do
+ expect_next_instance_of(SystemNotes::DesignManagementService) do |service|
+ expect(service).to receive(:design_discussion_added).with(discussion_note)
+ end
+
+ described_class.design_discussion_added(discussion_note)
+ end
+ end
end
diff --git a/spec/services/system_notes/design_management_service_spec.rb b/spec/services/system_notes/design_management_service_spec.rb
new file mode 100644
index 00000000000..08511e62341
--- /dev/null
+++ b/spec/services/system_notes/design_management_service_spec.rb
@@ -0,0 +1,155 @@
+# frozen_string_literal: true
+
+require 'spec_helper'
+
+describe SystemNotes::DesignManagementService do
+ let(:project) { create(:project) }
+ let(:issue) { create(:issue, project: project) }
+
+ let(:instance) { described_class.new(noteable: instance_noteable, project: instance_project, author: instance_author) }
+
+ describe '#design_version_added' do
+ let(:instance_noteable) { version.issue }
+ let(:instance_project) { version.issue.project }
+ let(:instance_author) { version.author }
+
+ subject { instance.design_version_added(version) }
+
+ # default (valid) parameters:
+ let(:n_designs) { 3 }
+ let(:designs) { create_list(:design, n_designs, issue: issue) }
+ let(:user) { build(:user) }
+ let(:version) do
+ create(:design_version, issue: issue, designs: designs)
+ end
+
+ before do
+ # Avoid needing to call into gitaly
+ allow(version).to receive(:author).and_return(user)
+ end
+
+ context 'with one kind of event' do
+ before do
+ DesignManagement::Action
+ .where(design: designs).update_all(event: :modification)
+ end
+
+ it 'makes just one note' do
+ expect(subject).to contain_exactly(Note)
+ end
+
+ it 'adds a new system note' do
+ expect { subject }.to change { Note.system.count }.by(1)
+ end
+ end
+
+ context 'with a mixture of events' do
+ let(:n_designs) { DesignManagement::Action.events.size }
+
+ before do
+ designs.each_with_index do |design, i|
+ design.actions.update_all(event: i)
+ end
+ end
+
+ it 'makes one note for each kind of event' do
+ expect(subject).to have_attributes(size: n_designs)
+ end
+
+ it 'adds a system note for each kind of event' do
+ expect { subject }.to change { Note.system.count }.by(n_designs)
+ end
+ end
+
+ describe 'icons' do
+ where(:action) do
+ [
+ [:creation],
+ [:modification],
+ [:deletion]
+ ]
+ end
+
+ with_them do
+ before do
+ version.actions.update_all(event: action)
+ end
+
+ subject(:metadata) do
+ instance.design_version_added(version)
+ .first.system_note_metadata
+ end
+
+ it 'has a valid action' do
+ expect(::SystemNoteHelper::ICON_NAMES_BY_ACTION)
+ .to include(metadata.action)
+ end
+ end
+ end
+
+ context 'it succeeds' do
+ where(:action, :icon, :human_description) do
+ [
+ [:creation, 'designs_added', 'added'],
+ [:modification, 'designs_modified', 'updated'],
+ [:deletion, 'designs_removed', 'removed']
+ ]
+ end
+
+ with_them do
+ before do
+ version.actions.update_all(event: action)
+ end
+
+ let(:anchor_tag) { %r{ <a[^>]*>#{link}</a>} }
+ let(:href) { instance.send(:designs_path, { version: version.id }) }
+ let(:link) { "#{n_designs} designs" }
+
+ subject(:note) { instance.design_version_added(version).first }
+
+ it 'has the correct data' do
+ expect(note)
+ .to be_system
+ .and have_attributes(
+ system_note_metadata: have_attributes(action: icon),
+ note: include(human_description)
+ .and(include link)
+ .and(include href),
+ note_html: a_string_matching(anchor_tag)
+ )
+ end
+ end
+ end
+ end
+
+ describe '#design_discussion_added' do
+ let(:instance_noteable) { design.issue }
+ let(:instance_project) { design.issue.project }
+ let(:instance_author) { discussion_note.author }
+
+ subject { instance.design_discussion_added(discussion_note) }
+
+ let(:design) { create(:design, :with_file, issue: issue) }
+ let(:author) { create(:user) }
+ let(:discussion_note) do
+ create(:diff_note_on_design, noteable: design, author: author)
+ end
+ let(:action) { 'designs_discussion_added' }
+
+ it_behaves_like 'a system note' do
+ let(:noteable) { discussion_note.noteable.issue }
+ end
+
+ it 'adds a new system note' do
+ expect { subject }.to change { Note.system.count }.by(1)
+ end
+
+ it 'has the correct note text' do
+ href = instance.send(:designs_path,
+ { vueroute: design.filename, anchor: ActionView::RecordIdentifier.dom_id(discussion_note) }
+ )
+
+ expect(subject.note).to eq("started a discussion on [#{design.filename}](#{href})")
+ end
+ end
+end
diff --git a/spec/services/template_engines/liquid_service_spec.rb b/spec/services/template_engines/liquid_service_spec.rb
deleted file mode 100644
index 7c5262bc264..00000000000
--- a/spec/services/template_engines/liquid_service_spec.rb
+++ /dev/null
@@ -1,126 +0,0 @@
-# frozen_string_literal: true
-
-require 'spec_helper'
-
-describe TemplateEngines::LiquidService do
- describe '#render' do
- let(:template) { 'up{env={{ci_environment_slug}}}' }
- let(:result) { subject }
-
- let_it_be(:slug) { 'env_slug' }
-
- let_it_be(:context) do
- {
- ci_environment_slug: slug,
- environment_filter: "container_name!=\"POD\",environment=\"#{slug}\""
- }
- end
-
- subject { described_class.new(template).render(context) }
-
- it 'with symbol keys in context it substitutes variables' do
- expect(result).to include("up{env=#{slug}")
- end
-
- context 'with multiple occurrences of variable in template' do
- let(:template) do
- 'up{env1={{ci_environment_slug}},env2={{ci_environment_slug}}}'
- end
-
- it 'substitutes variables' do
- expect(result).to eq("up{env1=#{slug},env2=#{slug}}")
- end
- end
-
- context 'with multiple variables in template' do
- let(:template) do
- 'up{env={{ci_environment_slug}},' \
- '{{environment_filter}}}'
- end
-
- it 'substitutes all variables' do
- expect(result).to eq(
- "up{env=#{slug}," \
- "container_name!=\"POD\",environment=\"#{slug}\"}"
- )
- end
- end
-
- context 'with unknown variables in template' do
- let(:template) { 'up{env={{env_slug}}}' }
-
- it 'does not substitute unknown variables' do
- expect(result).to eq("up{env=}")
- end
- end
-
- context 'with extra variables in context' do
- let(:template) { 'up{env={{ci_environment_slug}}}' }
-
- it 'substitutes variables' do
- # If context has only 1 key, there is no need for this spec.
- expect(context.count).to be > 1
- expect(result).to eq("up{env=#{slug}}")
- end
- end
-
- context 'with unknown and known variables in template' do
- let(:template) { 'up{env={{ci_environment_slug}},other_env={{env_slug}}}' }
-
- it 'substitutes known variables' do
- expect(result).to eq("up{env=#{slug},other_env=}")
- end
- end
-
- context 'Liquid errors' do
- shared_examples 'raises RenderError' do |message|
- it do
- expect { result }.to raise_error(described_class::RenderError, message)
- end
- end
-
- context 'when liquid raises error' do
- let(:template) { 'up{env={{ci_environment_slug}}' }
- let(:liquid_template) { Liquid::Template.new }
-
- before do
- allow(Liquid::Template).to receive(:parse).with(template).and_return(liquid_template)
- allow(liquid_template).to receive(:render!).and_raise(exception, message)
- end
-
- context 'raises Liquid::MemoryError' do
- let(:exception) { Liquid::MemoryError }
- let(:message) { 'Liquid error: Memory limits exceeded' }
-
- it_behaves_like 'raises RenderError', 'Memory limit exceeded while rendering template'
- end
-
- context 'raises Liquid::Error' do
- let(:exception) { Liquid::Error }
- let(:message) { 'Liquid error: Generic error message' }
-
- it_behaves_like 'raises RenderError', 'Error rendering query'
- end
- end
-
- context 'with template that is expensive to render' do
- let(:template) do
- '{% assign loop_count = 1000 %}'\
- '{% assign padStr = "0" %}'\
- '{% assign number_to_pad = "1" %}'\
- '{% assign strLength = number_to_pad | size %}'\
- '{% assign padLength = loop_count | minus: strLength %}'\
- '{% if padLength > 0 %}'\
- ' {% assign padded = number_to_pad %}'\
- ' {% for position in (1..padLength) %}'\
- ' {% assign padded = padded | prepend: padStr %}'\
- ' {% endfor %}'\
- ' {{ padded }}'\
- '{% endif %}'
- end
-
- it_behaves_like 'raises RenderError', 'Memory limit exceeded while rendering template'
- end
- end
- end
-end
diff --git a/spec/services/todo_service_spec.rb b/spec/services/todo_service_spec.rb
index 9b92590cb63..4894cf12372 100644
--- a/spec/services/todo_service_spec.rb
+++ b/spec/services/todo_service_spec.rb
@@ -895,6 +895,36 @@ describe TodoService do
end
end
+ describe 'Designs' do
+ include DesignManagementTestHelpers
+
+ let(:issue) { create(:issue, project: project) }
+ let(:design) { create(:design, issue: issue) }
+
+ before do
+ enable_design_management
+
+ project.add_guest(author)
+ project.add_developer(john_doe)
+ end
+
+ let(:note) do
+ build(:diff_note_on_design,
+ noteable: design,
+ author: author,
+ note: "Hey #{john_doe.to_reference}")
+ end
+
+ it 'creates a todo for mentioned user on new diff note' do
+ service.new_note(note, author)
+
+ should_create_todo(user: john_doe,
+ target: design,
+ action: Todo::MENTIONED,
+ note: note)
+ end
+ end
+
describe '#update_note' do
let(:noteable) { create(:issue, project: project) }
let(:note) { create(:note, project: project, note: mentions, noteable: noteable) }
diff --git a/spec/services/update_merge_request_metrics_service_spec.rb b/spec/services/update_merge_request_metrics_service_spec.rb
index bb07dfa1a0e..1aaf5e712f9 100644
--- a/spec/services/update_merge_request_metrics_service_spec.rb
+++ b/spec/services/update_merge_request_metrics_service_spec.rb
@@ -9,7 +9,7 @@ describe MergeRequestMetricsService do
it 'updates metrics' do
user = create(:user)
service = described_class.new(metrics)
- event = double(Event, author_id: user.id, created_at: Time.now)
+ event = double(Event, author_id: user.id, created_at: Time.current)
service.merge(event)
@@ -22,7 +22,7 @@ describe MergeRequestMetricsService do
it 'updates metrics' do
user = create(:user)
service = described_class.new(metrics)
- event = double(Event, author_id: user.id, created_at: Time.now)
+ event = double(Event, author_id: user.id, created_at: Time.current)
service.close(event)
diff --git a/spec/services/user_project_access_changed_service_spec.rb b/spec/services/user_project_access_changed_service_spec.rb
index 902ed723e09..f27eeb74265 100644
--- a/spec/services/user_project_access_changed_service_spec.rb
+++ b/spec/services/user_project_access_changed_service_spec.rb
@@ -17,5 +17,14 @@ describe UserProjectAccessChangedService do
described_class.new([1, 2]).execute(blocking: false)
end
+
+ it 'permits low-priority operation' do
+ expect(AuthorizedProjectUpdate::UserRefreshWithLowUrgencyWorker).to(
+ receive(:bulk_perform_in).with(described_class::DELAY, [[1], [2]])
+ )
+
+ described_class.new([1, 2]).execute(blocking: false,
+ priority: described_class::LOW_PRIORITY)
+ end
end
end
diff --git a/spec/services/users/destroy_service_spec.rb b/spec/services/users/destroy_service_spec.rb
index 216d9170274..6e4b293286b 100644
--- a/spec/services/users/destroy_service_spec.rb
+++ b/spec/services/users/destroy_service_spec.rb
@@ -42,13 +42,11 @@ describe Users::DestroyService do
it 'calls the bulk snippet destroy service for the user personal snippets' do
repo1 = create(:personal_snippet, :repository, author: user).snippet_repository
- repo2 = create(:project_snippet, :repository, author: user).snippet_repository
- repo3 = create(:project_snippet, :repository, project: project, author: user).snippet_repository
+ repo2 = create(:project_snippet, :repository, project: project, author: user).snippet_repository
aggregate_failures do
expect(gitlab_shell.repository_exists?(repo1.shard_name, repo1.disk_path + '.git')).to be_truthy
expect(gitlab_shell.repository_exists?(repo2.shard_name, repo2.disk_path + '.git')).to be_truthy
- expect(gitlab_shell.repository_exists?(repo3.shard_name, repo3.disk_path + '.git')).to be_truthy
end
# Call made when destroying user personal projects
@@ -59,17 +57,23 @@ describe Users::DestroyService do
# project snippets where projects are not user personal
# ones
expect(Snippets::BulkDestroyService).to receive(:new)
- .with(admin, user.snippets).and_call_original
+ .with(admin, user.snippets.only_personal_snippets).and_call_original
service.execute(user)
aggregate_failures do
expect(gitlab_shell.repository_exists?(repo1.shard_name, repo1.disk_path + '.git')).to be_falsey
expect(gitlab_shell.repository_exists?(repo2.shard_name, repo2.disk_path + '.git')).to be_falsey
- expect(gitlab_shell.repository_exists?(repo3.shard_name, repo3.disk_path + '.git')).to be_falsey
end
end
+ it 'does not delete project snippets that the user is the author of' do
+ repo = create(:project_snippet, :repository, author: user).snippet_repository
+ service.execute(user)
+ expect(gitlab_shell.repository_exists?(repo.shard_name, repo.disk_path + '.git')).to be_truthy
+ expect(User.ghost.snippets).to include(repo.snippet)
+ end
+
context 'when an error is raised deleting snippets' do
it 'does not delete user' do
snippet = create(:personal_snippet, :repository, author: user)
diff --git a/spec/services/users/migrate_to_ghost_user_service_spec.rb b/spec/services/users/migrate_to_ghost_user_service_spec.rb
index 40206775aed..a7d7c16a66f 100644
--- a/spec/services/users/migrate_to_ghost_user_service_spec.rb
+++ b/spec/services/users/migrate_to_ghost_user_service_spec.rb
@@ -78,6 +78,12 @@ describe Users::MigrateToGhostUserService do
end
end
+ context 'snippets' do
+ include_examples "migrating a deleted user's associated records to the ghost user", Snippet do
+ let(:created_record) { create(:snippet, project: project, author: user) }
+ end
+ end
+
context "when record migration fails with a rollback exception" do
before do
expect_any_instance_of(ActiveRecord::Associations::CollectionProxy)
diff --git a/spec/services/verify_pages_domain_service_spec.rb b/spec/services/verify_pages_domain_service_spec.rb
index f2b3b44d223..3f08ae84c14 100644
--- a/spec/services/verify_pages_domain_service_spec.rb
+++ b/spec/services/verify_pages_domain_service_spec.rb
@@ -348,7 +348,7 @@ describe VerifyPagesDomainService do
end
it 'does not shorten any grace period' do
- grace = Time.now + 1.year
+ grace = Time.current + 1.year
domain.update!(enabled_until: grace)
disallow_resolver!
diff --git a/spec/services/wiki_pages/base_service_spec.rb b/spec/services/wiki_pages/base_service_spec.rb
index 4c44c195ac8..fede86a5192 100644
--- a/spec/services/wiki_pages/base_service_spec.rb
+++ b/spec/services/wiki_pages/base_service_spec.rb
@@ -10,7 +10,7 @@ describe WikiPages::BaseService do
counter = Gitlab::UsageDataCounters::WikiPageCounter
error = counter::UnknownEvent
- let(:subject) { bad_service_class.new(project, user, {}) }
+ let(:subject) { bad_service_class.new(container: project, current_user: user) }
context 'the class implements usage_counter_action incorrectly' do
let(:bad_service_class) do
diff --git a/spec/services/wiki_pages/create_service_spec.rb b/spec/services/wiki_pages/create_service_spec.rb
index d63d62e9492..2a17805110e 100644
--- a/spec/services/wiki_pages/create_service_spec.rb
+++ b/spec/services/wiki_pages/create_service_spec.rb
@@ -3,96 +3,5 @@
require 'spec_helper'
describe WikiPages::CreateService do
- let(:project) { create(:project, :wiki_repo) }
- let(:user) { create(:user) }
- let(:page_title) { 'Title' }
-
- let(:opts) do
- {
- title: page_title,
- content: 'Content for wiki page',
- format: 'markdown'
- }
- end
-
- subject(:service) { described_class.new(project, user, opts) }
-
- before do
- project.add_developer(user)
- end
-
- describe '#execute' do
- it 'creates wiki page with valid attributes' do
- page = service.execute
-
- expect(page).to be_valid
- expect(page.title).to eq(opts[:title])
- expect(page.content).to eq(opts[:content])
- expect(page.format).to eq(opts[:format].to_sym)
- end
-
- it 'executes webhooks' do
- expect(service).to receive(:execute_hooks).once.with(WikiPage)
-
- service.execute
- end
-
- it 'counts wiki page creation' do
- counter = Gitlab::UsageDataCounters::WikiPageCounter
-
- expect { service.execute }.to change { counter.read(:create) }.by 1
- end
-
- shared_examples 'correct event created' do
- it 'creates appropriate events' do
- expect { service.execute }.to change { Event.count }.by 1
-
- expect(Event.recent.first).to have_attributes(
- action: Event::CREATED,
- target: have_attributes(canonical_slug: page_title)
- )
- end
- end
-
- context 'the new page is at the top level' do
- let(:page_title) { 'root-level-page' }
-
- include_examples 'correct event created'
- end
-
- context 'the new page is in a subsection' do
- let(:page_title) { 'subsection/page' }
-
- include_examples 'correct event created'
- end
-
- context 'the feature is disabled' do
- before do
- stub_feature_flags(wiki_events: false)
- end
-
- it 'does not record the activity' do
- expect { service.execute }.not_to change(Event, :count)
- end
- end
-
- context 'when the options are bad' do
- let(:page_title) { '' }
-
- it 'does not count a creation event' do
- counter = Gitlab::UsageDataCounters::WikiPageCounter
-
- expect { service.execute }.not_to change { counter.read(:create) }
- end
-
- it 'does not record the activity' do
- expect { service.execute }.not_to change(Event, :count)
- end
-
- it 'reports the error' do
- expect(service.execute).to be_invalid
- .and have_attributes(errors: be_present)
- end
- end
- end
+ it_behaves_like 'WikiPages::CreateService#execute', :project
end
diff --git a/spec/services/wiki_pages/destroy_service_spec.rb b/spec/services/wiki_pages/destroy_service_spec.rb
index e205bedfdb9..b6fee1fd896 100644
--- a/spec/services/wiki_pages/destroy_service_spec.rb
+++ b/spec/services/wiki_pages/destroy_service_spec.rb
@@ -3,52 +3,5 @@
require 'spec_helper'
describe WikiPages::DestroyService do
- let(:project) { create(:project) }
- let(:user) { create(:user) }
- let(:page) { create(:wiki_page) }
-
- subject(:service) { described_class.new(project, user) }
-
- before do
- project.add_developer(user)
- end
-
- describe '#execute' do
- it 'executes webhooks' do
- expect(service).to receive(:execute_hooks).once.with(page)
-
- service.execute(page)
- end
-
- it 'increments the delete count' do
- counter = Gitlab::UsageDataCounters::WikiPageCounter
-
- expect { service.execute(page) }.to change { counter.read(:delete) }.by 1
- end
-
- it 'creates a new wiki page deletion event' do
- expect { service.execute(page) }.to change { Event.count }.by 1
-
- expect(Event.recent.first).to have_attributes(
- action: Event::DESTROYED,
- target: have_attributes(canonical_slug: page.slug)
- )
- end
-
- it 'does not increment the delete count if the deletion failed' do
- counter = Gitlab::UsageDataCounters::WikiPageCounter
-
- expect { service.execute(nil) }.not_to change { counter.read(:delete) }
- end
- end
-
- context 'the feature is disabled' do
- before do
- stub_feature_flags(wiki_events: false)
- end
-
- it 'does not record the activity' do
- expect { service.execute(page) }.not_to change(Event, :count)
- end
- end
+ it_behaves_like 'WikiPages::DestroyService#execute', :project
end
diff --git a/spec/services/wiki_pages/event_create_service_spec.rb b/spec/services/wiki_pages/event_create_service_spec.rb
new file mode 100644
index 00000000000..cf971b0a02c
--- /dev/null
+++ b/spec/services/wiki_pages/event_create_service_spec.rb
@@ -0,0 +1,87 @@
+# frozen_string_literal: true
+
+require 'spec_helper'
+
+describe WikiPages::EventCreateService do
+ let_it_be(:project) { create(:project) }
+ let_it_be(:user) { create(:user) }
+
+ subject { described_class.new(user) }
+
+ describe '#execute' do
+ let_it_be(:page) { create(:wiki_page, project: project) }
+ let(:slug) { generate(:sluggified_title) }
+ let(:action) { Event::CREATED }
+ let(:response) { subject.execute(slug, page, action) }
+
+ context 'feature flag is not enabled' do
+ before do
+ stub_feature_flags(wiki_events: false)
+ end
+
+ it 'does not error' do
+ expect(response).to be_success
+ .and have_attributes(message: /No event created/)
+ end
+
+ it 'does not create an event' do
+ expect { response }.not_to change(Event, :count)
+ end
+ end
+
+ context 'the user is nil' do
+ subject { described_class.new(nil) }
+
+ it 'raises an error on construction' do
+ expect { subject }.to raise_error ArgumentError
+ end
+ end
+
+ context 'the action is illegal' do
+ let(:action) { Event::WIKI_ACTIONS.max + 1 }
+
+ it 'returns an error' do
+ expect(response).to be_error
+ end
+
+ it 'does not create an event' do
+ expect { response }.not_to change(Event, :count)
+ end
+
+ it 'does not create a metadata record' do
+ expect { response }.not_to change(WikiPage::Meta, :count)
+ end
+ end
+
+ it 'returns a successful response' do
+ expect(response).to be_success
+ end
+
+ context 'the action is a deletion' do
+ let(:action) { Event::DESTROYED }
+
+ it 'does not synchronize the wiki metadata timestamps with the git commit' do
+ expect_next_instance_of(WikiPage::Meta) do |instance|
+ expect(instance).not_to receive(:synch_times_with_page)
+ end
+
+ response
+ end
+ end
+
+ it 'creates a wiki page event' do
+ expect { response }.to change(Event, :count).by(1)
+ end
+
+ it 'returns an event in the payload' do
+ expect(response.payload).to include(event: have_attributes(author: user, wiki_page?: true, action: action))
+ end
+
+ it 'records the slug for the page' do
+ response
+ meta = WikiPage::Meta.find_or_create(page.slug, page)
+
+ expect(meta.slugs.pluck(:slug)).to include(slug)
+ end
+ end
+end
diff --git a/spec/services/wiki_pages/update_service_spec.rb b/spec/services/wiki_pages/update_service_spec.rb
index ece714ee8e5..ac629a96f9a 100644
--- a/spec/services/wiki_pages/update_service_spec.rb
+++ b/spec/services/wiki_pages/update_service_spec.rb
@@ -3,100 +3,5 @@
require 'spec_helper'
describe WikiPages::UpdateService do
- let(:project) { create(:project) }
- let(:user) { create(:user) }
- let(:page) { create(:wiki_page) }
- let(:page_title) { 'New Title' }
-
- let(:opts) do
- {
- content: 'New content for wiki page',
- format: 'markdown',
- message: 'New wiki message',
- title: page_title
- }
- end
-
- subject(:service) { described_class.new(project, user, opts) }
-
- before do
- project.add_developer(user)
- end
-
- describe '#execute' do
- it 'updates the wiki page' do
- updated_page = service.execute(page)
-
- expect(updated_page).to be_valid
- expect(updated_page.message).to eq(opts[:message])
- expect(updated_page.content).to eq(opts[:content])
- expect(updated_page.format).to eq(opts[:format].to_sym)
- expect(updated_page.title).to eq(page_title)
- end
-
- it 'executes webhooks' do
- expect(service).to receive(:execute_hooks).once.with(WikiPage)
-
- service.execute(page)
- end
-
- it 'counts edit events' do
- counter = Gitlab::UsageDataCounters::WikiPageCounter
-
- expect { service.execute page }.to change { counter.read(:update) }.by 1
- end
-
- shared_examples 'adds activity event' do
- it 'adds a new wiki page activity event' do
- expect { service.execute(page) }.to change { Event.count }.by 1
-
- expect(Event.recent.first).to have_attributes(
- action: Event::UPDATED,
- wiki_page: page,
- target_title: page.title
- )
- end
- end
-
- context 'the page is at the top level' do
- let(:page_title) { 'Top level page' }
-
- include_examples 'adds activity event'
- end
-
- context 'the page is in a subsection' do
- let(:page_title) { 'Subsection / secondary page' }
-
- include_examples 'adds activity event'
- end
-
- context 'the feature is disabled' do
- before do
- stub_feature_flags(wiki_events: false)
- end
-
- it 'does not record the activity' do
- expect { service.execute(page) }.not_to change(Event, :count)
- end
- end
-
- context 'when the options are bad' do
- let(:page_title) { '' }
-
- it 'does not count an edit event' do
- counter = Gitlab::UsageDataCounters::WikiPageCounter
-
- expect { service.execute page }.not_to change { counter.read(:update) }
- end
-
- it 'does not record the activity' do
- expect { service.execute page }.not_to change(Event, :count)
- end
-
- it 'reports the error' do
- expect(service.execute(page)).to be_invalid
- .and have_attributes(errors: be_present)
- end
- end
- end
+ it_behaves_like 'WikiPages::UpdateService#execute', :project
end
diff --git a/spec/services/wikis/create_attachment_service_spec.rb b/spec/services/wikis/create_attachment_service_spec.rb
index 7a73a0a555f..4adfaa24874 100644
--- a/spec/services/wikis/create_attachment_service_spec.rb
+++ b/spec/services/wikis/create_attachment_service_spec.rb
@@ -2,7 +2,7 @@
require 'spec_helper'
describe Wikis::CreateAttachmentService do
- let(:project) { create(:project, :wiki_repo) }
+ let(:container) { create(:project, :wiki_repo) }
let(:user) { create(:user) }
let(:file_name) { 'filename.txt' }
let(:file_path_regex) { %r{#{described_class::ATTACHMENT_PATH}/\h{32}/#{file_name}} }
@@ -15,25 +15,21 @@ describe Wikis::CreateAttachmentService do
end
let(:opts) { file_opts }
- subject(:service) { described_class.new(project, user, opts) }
+ subject(:service) { described_class.new(container: container, current_user: user, params: opts) }
before do
- project.add_developer(user)
+ container.add_developer(user)
end
describe 'initialization' do
context 'author commit info' do
it 'does not raise error if user is nil' do
- service = described_class.new(project, nil, opts)
+ service = described_class.new(container: container, current_user: nil, params: opts)
expect(service.instance_variable_get(:@author_email)).to be_nil
expect(service.instance_variable_get(:@author_name)).to be_nil
end
- it 'fills file_path from the repository uploads folder' do
- expect(service.instance_variable_get(:@file_path)).to match(file_path_regex)
- end
-
context 'when no author info provided' do
it 'fills author_email and author_name from current_user info' do
expect(service.instance_variable_get(:@author_email)).to eq user.email
@@ -73,7 +69,7 @@ describe Wikis::CreateAttachmentService do
context 'branch name' do
context 'when no branch provided' do
it 'sets the branch from the wiki default_branch' do
- expect(service.instance_variable_get(:@branch_name)).to eq project.wiki.default_branch
+ expect(service.instance_variable_get(:@branch_name)).to eq container.wiki.default_branch
end
end
@@ -151,7 +147,7 @@ describe Wikis::CreateAttachmentService do
context 'when user' do
shared_examples 'wiki attachment user validations' do
it 'returns error' do
- result = described_class.new(project, user2, opts).execute
+ result = described_class.new(container: container, current_user: user2, params: opts).execute
expect(result[:status]).to eq :error
expect(result[:message]).to eq 'You are not allowed to push to the wiki'
@@ -172,54 +168,5 @@ describe Wikis::CreateAttachmentService do
end
end
- describe '#execute' do
- let(:wiki) { project.wiki }
-
- subject(:service_execute) { service.execute[:result] }
-
- context 'creates branch if it does not exists' do
- let(:branch_name) { 'new_branch' }
- let(:opts) { file_opts.merge(branch_name: branch_name) }
-
- it do
- expect(wiki.repository.branches).to be_empty
- expect { service.execute }.to change { wiki.repository.branches.count }.by(1)
- expect(wiki.repository.branches.first.name).to eq branch_name
- end
- end
-
- it 'adds file to the repository' do
- expect(wiki.repository.ls_files('HEAD')).to be_empty
-
- service.execute
-
- files = wiki.repository.ls_files('HEAD')
- expect(files.count).to eq 1
- expect(files.first).to match(file_path_regex)
- end
-
- context 'returns' do
- before do
- allow(SecureRandom).to receive(:hex).and_return('fixed_hex')
-
- service_execute
- end
-
- it 'returns the file name' do
- expect(service_execute[:file_name]).to eq file_name
- end
-
- it 'returns the path where file was stored' do
- expect(service_execute[:file_path]).to eq 'uploads/fixed_hex/filename.txt'
- end
-
- it 'returns the branch where the file was pushed' do
- expect(service_execute[:branch]).to eq wiki.default_branch
- end
-
- it 'returns the commit id' do
- expect(service_execute[:commit]).not_to be_empty
- end
- end
- end
+ it_behaves_like 'Wikis::CreateAttachmentService#execute', :project
end
diff --git a/spec/spec_helper.rb b/spec/spec_helper.rb
index fe03621b9bf..80dfa20a2f1 100644
--- a/spec/spec_helper.rb
+++ b/spec/spec_helper.rb
@@ -8,10 +8,12 @@ ENV["IN_MEMORY_APPLICATION_SETTINGS"] = 'true'
ENV["RSPEC_ALLOW_INVALID_URLS"] = 'true'
require File.expand_path('../config/environment', __dir__)
+
+require 'rspec/mocks'
require 'rspec/rails'
-require 'shoulda/matchers'
require 'rspec/retry'
require 'rspec-parameterized'
+require 'shoulda/matchers'
require 'test_prof/recipes/rspec/let_it_be'
rspec_profiling_is_configured =
@@ -173,21 +175,19 @@ RSpec.configure do |config|
# Enable all features by default for testing
allow(Feature).to receive(:enabled?) { true }
- enabled = example.metadata[:enable_rugged].present?
+ enable_rugged = example.metadata[:enable_rugged].present?
# Disable Rugged features by default
Gitlab::Git::RuggedImpl::Repository::FEATURE_FLAGS.each do |flag|
- allow(Feature).to receive(:enabled?).with(flag).and_return(enabled)
+ stub_feature_flags(flag => enable_rugged)
end
- allow(Gitlab::GitalyClient).to receive(:can_use_disk?).and_return(enabled)
+ allow(Gitlab::GitalyClient).to receive(:can_use_disk?).and_return(enable_rugged)
# The following can be removed when we remove the staged rollout strategy
# and we can just enable it using instance wide settings
# (ie. ApplicationSetting#auto_devops_enabled)
- allow(Feature).to receive(:enabled?)
- .with(:force_autodevops_on_by_default, anything)
- .and_return(false)
+ stub_feature_flags(force_autodevops_on_by_default: false)
# Enable Marginalia feature for all specs in the test suite.
allow(Gitlab::Marginalia).to receive(:cached_feature_enabled?).and_return(true)
@@ -196,11 +196,11 @@ RSpec.configure do |config|
# is feature-complete and can be made default in place
# of older sidebar.
# See https://gitlab.com/groups/gitlab-org/-/epics/1863
+ stub_feature_flags(vue_issuable_sidebar: false)
+ stub_feature_flags(vue_issuable_epic_sidebar: false)
+
allow(Feature).to receive(:enabled?)
- .with(:vue_issuable_sidebar, anything)
- .and_return(false)
- allow(Feature).to receive(:enabled?)
- .with(:vue_issuable_epic_sidebar, anything)
+ .with(/\Apromo_\w+\z/, default_enabled: false)
.and_return(false)
# Stub these calls due to being expensive operations
@@ -209,9 +209,7 @@ RSpec.configure do |config|
# expect(Gitlab::Git::KeepAround).to receive(:execute).and_call_original
allow(Gitlab::Git::KeepAround).to receive(:execute)
- [Gitlab::ThreadMemoryCache, Gitlab::ProcessMemoryCache].each do |cache|
- cache.cache_backend.clear
- end
+ Gitlab::ProcessMemoryCache.cache_backend.clear
Sidekiq::Worker.clear_all
@@ -235,26 +233,25 @@ RSpec.configure do |config|
./ee/spec/features
./ee/spec/finders
./ee/spec/lib
- ./ee/spec/models
- ./ee/spec/policies
./ee/spec/requests/admin
./ee/spec/serializers
./ee/spec/services
./ee/spec/support/protected_tags
- ./ee/spec/support/shared_examples
+ ./ee/spec/support/shared_examples/features
+ ./ee/spec/support/shared_examples/finders/geo
+ ./ee/spec/support/shared_examples/graphql/geo
+ ./ee/spec/support/shared_examples/services
./spec/features
./spec/finders
./spec/frontend
./spec/helpers
./spec/lib
- ./spec/models
- ./spec/policies
./spec/requests
./spec/serializers
./spec/services
- ./spec/support/cycle_analytics_helpers
./spec/support/protected_tags
- ./spec/support/shared_examples
+ ./spec/support/shared_examples/features
+ ./spec/support/shared_examples/requests
./spec/views
./spec/workers
)
@@ -286,12 +283,7 @@ RSpec.configure do |config|
end
config.around(:example, :request_store) do |example|
- RequestStore.begin!
-
- example.run
-
- RequestStore.end!
- RequestStore.clear!
+ Gitlab::WithRequestStore.with_request_store { example.run }
end
config.around do |example|
@@ -305,12 +297,10 @@ RSpec.configure do |config|
Gitlab::SidekiqMiddleware.server_configurator(
metrics: false, # The metrics don't go anywhere in tests
arguments_logger: false, # We're not logging the regular messages for inline jobs
- memory_killer: false, # This is not a thing we want to do inline in tests
- # Don't enable this if the request store is active in the spec itself
- # This needs to run within the `request_store` around block defined above
- request_store: !RequestStore.active?
+ memory_killer: false # This is not a thing we want to do inline in tests
).call(chain)
chain.add DisableQueryLimit
+ chain.insert_after ::Gitlab::SidekiqMiddleware::RequestStoreMiddleware, IsolatedRequestStore
example.run
end
diff --git a/spec/support/capybara.rb b/spec/support/capybara.rb
index 90adfb1a2ee..38f9ccf23f5 100644
--- a/spec/support/capybara.rb
+++ b/spec/support/capybara.rb
@@ -7,7 +7,7 @@ require 'capybara-screenshot/rspec'
require 'selenium-webdriver'
# Give CI some extra time
-timeout = (ENV['CI'] || ENV['CI_SERVER']) ? 60 : 30
+timeout = ENV['CI'] || ENV['CI_SERVER'] ? 60 : 30
# Define an error class for JS console messages
JSConsoleError = Class.new(StandardError)
@@ -95,12 +95,23 @@ RSpec.configure do |config|
config.include CapybaraHelpers, type: :feature
config.before(:context, :js) do
+ # This prevents Selenium from creating thousands of connections while waiting for
+ # an element to appear
+ webmock_enable_with_http_connect_on_start!
+
next if $capybara_server_already_started
TestEnv.eager_load_driver_server
$capybara_server_already_started = true
end
+ config.after(:context, :js) do
+ # WebMock doesn't stub connections, so we need to restore the original behavior
+ # to prevent many specs from failing:
+ # https://github.com/bblimke/webmock/blob/master/README.md#connecting-on-nethttpstart
+ webmock_enable!
+ end
+
config.before(:example, :js) do
session = Capybara.current_session
diff --git a/spec/support/cycle_analytics_helpers/test_generation.rb b/spec/support/cycle_analytics_helpers/test_generation.rb
index 34018263339..c577e5cc665 100644
--- a/spec/support/cycle_analytics_helpers/test_generation.rb
+++ b/spec/support/cycle_analytics_helpers/test_generation.rb
@@ -29,6 +29,10 @@ module CycleAnalyticsHelpers
scenarios.each do |start_time_conditions, end_time_conditions|
let_it_be(:other_project) { create(:project, :repository) }
+ before do
+ other_project.add_developer(self.user)
+ end
+
context "start condition: #{start_time_conditions.map(&:first).to_sentence}" do
context "end condition: #{end_time_conditions.map(&:first).to_sentence}" do
it "finds the median of available durations between the two conditions", :sidekiq_might_not_need_inline do
diff --git a/spec/support/database_cleaner.rb b/spec/support/database_cleaner.rb
index f6339d7343c..60d82f7e92a 100644
--- a/spec/support/database_cleaner.rb
+++ b/spec/support/database_cleaner.rb
@@ -35,6 +35,8 @@ RSpec.configure do |config|
puts "Recreating the database"
start = Gitlab::Metrics::System.monotonic_time
+ ActiveRecord::AdvisoryLockBase.clear_all_connections!
+
ActiveRecord::Tasks::DatabaseTasks.drop_current
ActiveRecord::Tasks::DatabaseTasks.create_current
ActiveRecord::Tasks::DatabaseTasks.load_schema_current
diff --git a/spec/support/helpers/admin_mode_helpers.rb b/spec/support/helpers/admin_mode_helpers.rb
index e995a7d4f5e..36ed262f8ae 100644
--- a/spec/support/helpers/admin_mode_helpers.rb
+++ b/spec/support/helpers/admin_mode_helpers.rb
@@ -7,6 +7,9 @@ module AdminModeHelper
# mode for accessing any administrative functionality. This helper lets a user
# be in admin mode without requiring a second authentication step (provided
# the user is an admin)
+ #
+ # See also tag :enable_admin_mode in spec/spec_helper.rb for a spec-wide
+ # alternative
def enable_admin_mode!(user)
fake_user_mode = instance_double(Gitlab::Auth::CurrentUserMode)
diff --git a/spec/support/helpers/concurrent_helpers.rb b/spec/support/helpers/concurrent_helpers.rb
new file mode 100644
index 00000000000..4eecc2133e7
--- /dev/null
+++ b/spec/support/helpers/concurrent_helpers.rb
@@ -0,0 +1,40 @@
+# frozen_string_literal: true
+
+module ConcurrentHelpers
+ Cancelled = Class.new(StandardError)
+
+ # To test for contention, we may need to run some actions in parallel. This
+ # helper takes an array of blocks and schedules them all on different threads
+ # in a fixed-size thread pool.
+ #
+ # @param [Array[Proc]] blocks
+ # @param [Integer] task_wait_time: time to wait for each task (upper bound on
+ # reasonable task execution time)
+ # @param [Integer] max_concurrency: maximum number of tasks to run at once
+ #
+ def run_parallel(blocks, task_wait_time: 20.seconds, max_concurrency: Concurrent.processor_count - 1)
+ thread_pool = Concurrent::FixedThreadPool.new(
+ [2, max_concurrency].max, { max_queue: blocks.size }
+ )
+ opts = { executor: thread_pool }
+
+ error = Concurrent::MVar.new
+
+ blocks.map { |block| Concurrent::Future.execute(opts, &block) }.each do |future|
+ future.wait(task_wait_time)
+
+ if future.complete?
+ error.put(future.reason) if future.reason && error.empty?
+ else
+ future.cancel
+ error.put(Cancelled.new) if error.empty?
+ end
+ end
+
+ raise error.take if error.full?
+ ensure
+ thread_pool.shutdown
+ thread_pool.wait_for_termination(10)
+ thread_pool.kill if thread_pool.running?
+ end
+end
diff --git a/spec/support/helpers/design_management_test_helpers.rb b/spec/support/helpers/design_management_test_helpers.rb
new file mode 100644
index 00000000000..bf41e2f5079
--- /dev/null
+++ b/spec/support/helpers/design_management_test_helpers.rb
@@ -0,0 +1,45 @@
+# frozen_string_literal: true
+
+module DesignManagementTestHelpers
+ def enable_design_management(enabled = true, ref_filter = true)
+ stub_lfs_setting(enabled: enabled)
+ stub_feature_flags(design_management_reference_filter_gfm_pipeline: ref_filter)
+ end
+
+ def delete_designs(*designs)
+ act_on_designs(designs) { ::DesignManagement::Action.deletion }
+ end
+
+ def restore_designs(*designs)
+ act_on_designs(designs) { ::DesignManagement::Action.creation }
+ end
+
+ def modify_designs(*designs)
+ act_on_designs(designs) { ::DesignManagement::Action.modification }
+ end
+
+ def path_for_design(design)
+ path_options = { vueroute: design.filename }
+ Gitlab::Routing.url_helpers.designs_project_issue_path(design.project, design.issue, path_options)
+ end
+
+ def url_for_design(design)
+ path_options = { vueroute: design.filename }
+ Gitlab::Routing.url_helpers.designs_project_issue_url(design.project, design.issue, path_options)
+ end
+
+ def url_for_designs(issue)
+ Gitlab::Routing.url_helpers.designs_project_issue_url(issue.project, issue)
+ end
+
+ private
+
+ def act_on_designs(designs, &block)
+ issue = designs.first.issue
+ version = build(:design_version, :empty, issue: issue).tap { |v| v.save(validate: false) }
+ designs.each do |d|
+ yield.create(design: d, version: version)
+ end
+ version
+ end
+end
diff --git a/spec/support/helpers/exclusive_lease_helpers.rb b/spec/support/helpers/exclusive_lease_helpers.rb
index 77703e20602..95cfc56c273 100644
--- a/spec/support/helpers/exclusive_lease_helpers.rb
+++ b/spec/support/helpers/exclusive_lease_helpers.rb
@@ -9,7 +9,9 @@ module ExclusiveLeaseHelpers
Gitlab::ExclusiveLease,
try_obtain: uuid,
exists?: true,
- renew: renew
+ renew: renew,
+ cancel: nil,
+ ttl: timeout
)
allow(Gitlab::ExclusiveLease)
diff --git a/spec/support/helpers/fake_blob_helpers.rb b/spec/support/helpers/fake_blob_helpers.rb
index a7eafb0fd23..6c8866deac4 100644
--- a/spec/support/helpers/fake_blob_helpers.rb
+++ b/spec/support/helpers/fake_blob_helpers.rb
@@ -22,7 +22,11 @@ module FakeBlobHelpers
alias_method :name, :path
def id
- 0
+ "00000000"
+ end
+
+ def commit_id
+ "11111111"
end
def binary_in_repo?
diff --git a/spec/support/helpers/graphql_helpers.rb b/spec/support/helpers/graphql_helpers.rb
index fc543186b08..b3d7f7bcece 100644
--- a/spec/support/helpers/graphql_helpers.rb
+++ b/spec/support/helpers/graphql_helpers.rb
@@ -246,12 +246,19 @@ module GraphqlHelpers
# Raises an error if no data is found
def graphql_data
+ # Note that `json_response` is defined as `let(:json_response)` and
+ # therefore, in a spec with multiple queries, will only contain data
+ # from the _first_ query, not subsequent ones
json_response['data'] || (raise NoData, graphql_errors)
end
def graphql_data_at(*path)
+ graphql_dig_at(graphql_data, *path)
+ end
+
+ def graphql_dig_at(data, *path)
keys = path.map { |segment| GraphqlHelpers.fieldnamerize(segment) }
- graphql_data.dig(*keys)
+ data.dig(*keys)
end
def graphql_errors
diff --git a/spec/support/helpers/jira_service_helper.rb b/spec/support/helpers/jira_service_helper.rb
index c23a8d52c84..198bedfe3bc 100644
--- a/spec/support/helpers/jira_service_helper.rb
+++ b/spec/support/helpers/jira_service_helper.rb
@@ -78,6 +78,11 @@ module JiraServiceHelper
JIRA_API + "/issue/#{issue_id}"
end
+ def stub_jira_service_test
+ WebMock.stub_request(:get, 'https://jira.example.com/rest/api/2/serverInfo')
+ .to_return(body: { url: 'http://url' }.to_json)
+ end
+
def stub_jira_urls(issue_id)
WebMock.stub_request(:get, jira_project_url)
WebMock.stub_request(:get, jira_api_comment_url(issue_id)).to_return(body: jira_issue_comments)
diff --git a/spec/support/helpers/kubernetes_helpers.rb b/spec/support/helpers/kubernetes_helpers.rb
index ca910e47695..8882f31e2f4 100644
--- a/spec/support/helpers/kubernetes_helpers.rb
+++ b/spec/support/helpers/kubernetes_helpers.rb
@@ -3,6 +3,8 @@
module KubernetesHelpers
include Gitlab::Kubernetes
+ NODE_NAME = "gke-cluster-applications-default-pool-49b7f225-v527"
+
def kube_response(body)
{ body: body.to_json }
end
@@ -11,6 +13,14 @@ module KubernetesHelpers
kube_response(kube_pods_body)
end
+ def nodes_response
+ kube_response(nodes_body)
+ end
+
+ def nodes_metrics_response
+ kube_response(nodes_metrics_body)
+ end
+
def kube_pod_response
kube_response(kube_pod)
end
@@ -34,6 +44,9 @@ module KubernetesHelpers
WebMock
.stub_request(:get, api_url + '/apis/rbac.authorization.k8s.io/v1')
.to_return(kube_response(kube_v1_rbac_authorization_discovery_body))
+ WebMock
+ .stub_request(:get, api_url + '/apis/metrics.k8s.io/v1beta1')
+ .to_return(kube_response(kube_metrics_v1beta1_discovery_body))
end
def stub_kubeclient_discover_istio(api_url)
@@ -76,6 +89,22 @@ module KubernetesHelpers
WebMock.stub_request(:get, pods_url).to_return(response || kube_pods_response)
end
+ def stub_kubeclient_nodes(api_url)
+ stub_kubeclient_discover_base(api_url)
+
+ nodes_url = api_url + "/api/v1/nodes"
+
+ WebMock.stub_request(:get, nodes_url).to_return(nodes_response)
+ end
+
+ def stub_kubeclient_nodes_and_nodes_metrics(api_url)
+ stub_kubeclient_nodes(api_url)
+
+ nodes_url = api_url + "/apis/metrics.k8s.io/v1beta1/nodes"
+
+ WebMock.stub_request(:get, nodes_url).to_return(nodes_metrics_response)
+ end
+
def stub_kubeclient_pods(namespace, status: nil)
stub_kubeclient_discover(service.api_url)
pods_url = service.api_url + "/api/v1/namespaces/#{namespace}/pods"
@@ -201,28 +230,8 @@ module KubernetesHelpers
.to_return(kube_response({}))
end
- def stub_kubeclient_get_cluster_role_binding_error(api_url, name, status: 404)
- WebMock.stub_request(:get, api_url + "/apis/rbac.authorization.k8s.io/v1/clusterrolebindings/#{name}")
- .to_return(status: [status, "Internal Server Error"])
- end
-
- def stub_kubeclient_create_cluster_role_binding(api_url)
- WebMock.stub_request(:post, api_url + '/apis/rbac.authorization.k8s.io/v1/clusterrolebindings')
- .to_return(kube_response({}))
- end
-
- def stub_kubeclient_get_role_binding(api_url, name, namespace: 'default')
- WebMock.stub_request(:get, api_url + "/apis/rbac.authorization.k8s.io/v1/namespaces/#{namespace}/rolebindings/#{name}")
- .to_return(kube_response({}))
- end
-
- def stub_kubeclient_get_role_binding_error(api_url, name, namespace: 'default', status: 404)
- WebMock.stub_request(:get, api_url + "/apis/rbac.authorization.k8s.io/v1/namespaces/#{namespace}/rolebindings/#{name}")
- .to_return(status: [status, "Internal Server Error"])
- end
-
- def stub_kubeclient_create_role_binding(api_url, namespace: 'default')
- WebMock.stub_request(:post, api_url + "/apis/rbac.authorization.k8s.io/v1/namespaces/#{namespace}/rolebindings")
+ def stub_kubeclient_put_cluster_role_binding(api_url, name)
+ WebMock.stub_request(:put, api_url + "/apis/rbac.authorization.k8s.io/v1/clusterrolebindings/#{name}")
.to_return(kube_response({}))
end
@@ -274,6 +283,7 @@ module KubernetesHelpers
{
"kind" => "APIResourceList",
"resources" => [
+ { "name" => "nodes", "namespaced" => false, "kind" => "Node" },
{ "name" => "pods", "namespaced" => true, "kind" => "Pod" },
{ "name" => "deployments", "namespaced" => true, "kind" => "Deployment" },
{ "name" => "secrets", "namespaced" => true, "kind" => "Secret" },
@@ -334,6 +344,16 @@ module KubernetesHelpers
}
end
+ def kube_metrics_v1beta1_discovery_body
+ {
+ "kind" => "APIResourceList",
+ "resources" => [
+ { "name" => "nodes", "namespaced" => false, "kind" => "NodeMetrics" },
+ { "name" => "pods", "namespaced" => true, "kind" => "PodMetrics" }
+ ]
+ }
+ end
+
def kube_istio_discovery_body
{
"kind" => "APIResourceList",
@@ -462,6 +482,20 @@ module KubernetesHelpers
}
end
+ def nodes_body
+ {
+ "kind" => "NodeList",
+ "items" => [kube_node]
+ }
+ end
+
+ def nodes_metrics_body
+ {
+ "kind" => "List",
+ "items" => [kube_node_metrics]
+ }
+ end
+
def kube_logs_body
"2019-12-13T14:04:22.123456Z Log 1\n2019-12-13T14:04:23.123456Z Log 2\n2019-12-13T14:04:24.123456Z Log 3"
end
@@ -514,6 +548,40 @@ module KubernetesHelpers
}
end
+ # This is a partial response, it will have many more elements in reality but
+ # these are the ones we care about at the moment
+ def kube_node
+ {
+ "metadata" => {
+ "name" => NODE_NAME
+ },
+ "status" => {
+ "capacity" => {
+ "cpu" => "2",
+ "memory" => "7657228Ki"
+ },
+ "allocatable" => {
+ "cpu" => "1930m",
+ "memory" => "5777164Ki"
+ }
+ }
+ }
+ end
+
+ # This is a partial response, it will have many more elements in reality but
+ # these are the ones we care about at the moment
+ def kube_node_metrics
+ {
+ "metadata" => {
+ "name" => NODE_NAME
+ },
+ "usage" => {
+ "cpu" => "144208668n",
+ "memory" => "1789048Ki"
+ }
+ }
+ end
+
# Similar to a kube_pod, but should contain a running service
def kube_knative_pod(name: "kube-pod", namespace: "default", status: "Running")
{
diff --git a/spec/support/helpers/login_helpers.rb b/spec/support/helpers/login_helpers.rb
index 6a4dcfcdb1e..cb880939b1c 100644
--- a/spec/support/helpers/login_helpers.rb
+++ b/spec/support/helpers/login_helpers.rb
@@ -50,9 +50,7 @@ module LoginHelpers
def gitlab_enable_admin_mode_sign_in(user)
visit new_admin_session_path
-
fill_in 'user_password', with: user.password
-
click_button 'Enter Admin Mode'
end
diff --git a/spec/support/helpers/query_recorder.rb b/spec/support/helpers/query_recorder.rb
index fd200a1abf3..61634813a1c 100644
--- a/spec/support/helpers/query_recorder.rb
+++ b/spec/support/helpers/query_recorder.rb
@@ -19,7 +19,9 @@ module ActiveRecord
def show_backtrace(values)
Rails.logger.debug("QueryRecorder SQL: #{values[:sql]}")
- Gitlab::BacktraceCleaner.clean_backtrace(caller).each { |line| Rails.logger.debug(" --> #{line}") }
+ Gitlab::BacktraceCleaner.clean_backtrace(caller).each do |line|
+ Rails.logger.debug("QueryRecorder backtrace: --> #{line}")
+ end
end
def get_sql_source(sql)
diff --git a/spec/support/helpers/reactive_caching_helpers.rb b/spec/support/helpers/reactive_caching_helpers.rb
index aa9d3b3a199..0b0b0622696 100644
--- a/spec/support/helpers/reactive_caching_helpers.rb
+++ b/spec/support/helpers/reactive_caching_helpers.rb
@@ -10,8 +10,11 @@ module ReactiveCachingHelpers
end
def stub_reactive_cache(subject = nil, data = nil, *qualifiers)
- allow(ReactiveCachingWorker).to receive(:perform_async)
- allow(ReactiveCachingWorker).to receive(:perform_in)
+ ReactiveCaching::WORK_TYPE.values.each do |worker|
+ allow(worker).to receive(:perform_async)
+ allow(worker).to receive(:perform_in)
+ end
+
write_reactive_cache(subject, data, *qualifiers) unless subject.nil?
end
@@ -42,8 +45,8 @@ module ReactiveCachingHelpers
Rails.cache.write(alive_reactive_cache_key(subject, *qualifiers), true)
end
- def expect_reactive_cache_update_queued(subject)
- expect(ReactiveCachingWorker)
+ def expect_reactive_cache_update_queued(subject, worker_klass: ReactiveCachingWorker)
+ expect(worker_klass)
.to receive(:perform_in)
.with(subject.class.reactive_cache_refresh_interval, subject.class, subject.id)
end
diff --git a/spec/support/helpers/smime_helper.rb b/spec/support/helpers/smime_helper.rb
index 96da3d81708..261aef9518e 100644
--- a/spec/support/helpers/smime_helper.rb
+++ b/spec/support/helpers/smime_helper.rb
@@ -5,20 +5,24 @@ module SmimeHelper
SHORT_EXPIRY = 30.minutes
def generate_root
- issue(signed_by: nil, expires_in: INFINITE_EXPIRY, certificate_authority: true)
+ issue(cn: 'RootCA', signed_by: nil, expires_in: INFINITE_EXPIRY, certificate_authority: true)
end
- def generate_cert(root_ca:, expires_in: SHORT_EXPIRY)
- issue(signed_by: root_ca, expires_in: expires_in, certificate_authority: false)
+ def generate_intermediate(signer_ca:)
+ issue(cn: 'IntermediateCA', signed_by: signer_ca, expires_in: INFINITE_EXPIRY, certificate_authority: true)
+ end
+
+ def generate_cert(signer_ca:, expires_in: SHORT_EXPIRY)
+ issue(signed_by: signer_ca, expires_in: expires_in, certificate_authority: false)
end
# returns a hash { key:, cert: } containing a generated key, cert pair
- def issue(email_address: 'test@example.com', signed_by:, expires_in:, certificate_authority:)
+ def issue(email_address: 'test@example.com', cn: nil, signed_by:, expires_in:, certificate_authority:)
key = OpenSSL::PKey::RSA.new(4096)
public_key = key.public_key
subject = if certificate_authority
- OpenSSL::X509::Name.parse("/CN=EU")
+ OpenSSL::X509::Name.parse("/CN=#{cn}")
else
OpenSSL::X509::Name.parse("/CN=#{email_address}")
end
diff --git a/spec/support/helpers/stub_feature_flags.rb b/spec/support/helpers/stub_feature_flags.rb
index 6c3efff7262..5b8a85b206f 100644
--- a/spec/support/helpers/stub_feature_flags.rb
+++ b/spec/support/helpers/stub_feature_flags.rb
@@ -9,23 +9,27 @@ module StubFeatureFlags
# Examples
# - `stub_feature_flags(ci_live_trace: false)` ... Disable `ci_live_trace`
# feature flag globally.
- # - `stub_feature_flags(ci_live_trace: { enabled: false, thing: project })` ...
- # Disable `ci_live_trace` feature flag on the specified project.
+ # - `stub_feature_flags(ci_live_trace: project)` ...
+ # - `stub_feature_flags(ci_live_trace: [project1, project2])` ...
+ # Enable `ci_live_trace` feature flag only on the specified projects.
def stub_feature_flags(features)
- features.each do |feature_name, option|
- if option.is_a?(Hash)
- enabled, thing = option.values_at(:enabled, :thing)
- else
- enabled = option
- thing = nil
- end
+ features.each do |feature_name, actors|
+ allow(Feature).to receive(:enabled?).with(feature_name, any_args).and_return(false)
+ allow(Feature).to receive(:enabled?).with(feature_name.to_s, any_args).and_return(false)
+
+ Array(actors).each do |actor|
+ raise ArgumentError, "actor cannot be Hash" if actor.is_a?(Hash)
- if thing
- allow(Feature).to receive(:enabled?).with(feature_name, thing, any_args) { enabled }
- allow(Feature).to receive(:enabled?).with(feature_name.to_s, thing, any_args) { enabled }
- else
- allow(Feature).to receive(:enabled?).with(feature_name, any_args) { enabled }
- allow(Feature).to receive(:enabled?).with(feature_name.to_s, any_args) { enabled }
+ case actor
+ when false, true
+ allow(Feature).to receive(:enabled?).with(feature_name, any_args).and_return(actor)
+ allow(Feature).to receive(:enabled?).with(feature_name.to_s, any_args).and_return(actor)
+ when nil, ActiveRecord::Base, Symbol, RSpec::Mocks::Double
+ allow(Feature).to receive(:enabled?).with(feature_name, actor, any_args).and_return(true)
+ allow(Feature).to receive(:enabled?).with(feature_name.to_s, actor, any_args).and_return(true)
+ else
+ raise ArgumentError, "#stub_feature_flags accepts only `nil`, `true`, `false`, `ActiveRecord::Base` or `Symbol` as actors"
+ end
end
end
end
diff --git a/spec/support/helpers/stub_gitlab_calls.rb b/spec/support/helpers/stub_gitlab_calls.rb
index 40f4151c0fb..120d432655b 100644
--- a/spec/support/helpers/stub_gitlab_calls.rb
+++ b/spec/support/helpers/stub_gitlab_calls.rb
@@ -86,7 +86,7 @@ module StubGitlabCalls
def stub_container_registry_tag_manifest_content
fixture_path = 'spec/fixtures/container_registry/tag_manifest.json'
- JSON.parse(File.read(Rails.root + fixture_path))
+ Gitlab::Json.parse(File.read(Rails.root + fixture_path))
end
def stub_container_registry_blob_content
@@ -113,12 +113,12 @@ module StubGitlabCalls
def stub_project_8
data = File.read(Rails.root.join('spec/support/gitlab_stubs/project_8.json'))
- allow_any_instance_of(Network).to receive(:project).and_return(JSON.parse(data))
+ allow_any_instance_of(Network).to receive(:project).and_return(Gitlab::Json.parse(data))
end
def stub_project_8_hooks
data = File.read(Rails.root.join('spec/support/gitlab_stubs/project_8_hooks.json'))
- allow_any_instance_of(Network).to receive(:project_hooks).and_return(JSON.parse(data))
+ allow_any_instance_of(Network).to receive(:project_hooks).and_return(Gitlab::Json.parse(data))
end
def stub_projects
@@ -143,7 +143,7 @@ module StubGitlabCalls
def project_hash_array
f = File.read(Rails.root.join('spec/support/gitlab_stubs/projects.json'))
- JSON.parse f
+ Gitlab::Json.parse(f)
end
end
diff --git a/spec/support/helpers/stub_object_storage.rb b/spec/support/helpers/stub_object_storage.rb
index d4ac286e959..b473cdaefc1 100644
--- a/spec/support/helpers/stub_object_storage.rb
+++ b/spec/support/helpers/stub_object_storage.rb
@@ -45,7 +45,7 @@ module StubObjectStorage
def stub_external_diffs_object_storage(uploader = described_class, **params)
stub_object_storage_uploader(config: Gitlab.config.external_diffs.object_store,
uploader: uploader,
- remote_directory: 'external_diffs',
+ remote_directory: 'external-diffs',
**params)
end
diff --git a/spec/support/helpers/test_env.rb b/spec/support/helpers/test_env.rb
index 47d69ca1f6a..130650b7e2e 100644
--- a/spec/support/helpers/test_env.rb
+++ b/spec/support/helpers/test_env.rb
@@ -1,7 +1,5 @@
# frozen_string_literal: true
-require 'rspec/mocks'
-
module TestEnv
extend ActiveSupport::Concern
extend self
@@ -61,7 +59,7 @@ module TestEnv
'merge-commit-analyze-side-branch' => '8a99451',
'merge-commit-analyze-after' => '646ece5',
'snippet/single-file' => '43e4080aaa14fc7d4b77ee1f5c9d067d5a7df10e',
- 'snippet/multiple-files' => 'b80faa8c5b2b62f6489a0d84755580e927e1189b',
+ 'snippet/multiple-files' => '40232f7eb98b3f221886432def6e8bab2432add9',
'snippet/rename-and-edit-file' => '220a1e4b4dff37feea0625a7947a4c60fbe78365',
'snippet/edit-file' => 'c2f074f4f26929c92795a75775af79a6ed6d8430',
'snippet/no-files' => '671aaa842a4875e5f30082d1ab6feda345fdb94d',
@@ -284,29 +282,33 @@ module TestEnv
end
def setup_factory_repo
- setup_repo(factory_repo_path, factory_repo_path_bare, factory_repo_name,
- BRANCH_SHA)
+ setup_repo(factory_repo_path, factory_repo_path_bare, factory_repo_name, BRANCH_SHA)
end
# This repo has a submodule commit that is not present in the main test
# repository.
def setup_forked_repo
- setup_repo(forked_repo_path, forked_repo_path_bare, forked_repo_name,
- FORKED_BRANCH_SHA)
+ setup_repo(forked_repo_path, forked_repo_path_bare, forked_repo_name, FORKED_BRANCH_SHA)
end
def setup_repo(repo_path, repo_path_bare, repo_name, refs)
clone_url = "https://gitlab.com/gitlab-org/#{repo_name}.git"
unless File.directory?(repo_path)
- system(*%W(#{Gitlab.config.git.bin_path} clone -q #{clone_url} #{repo_path}))
+ puts "\n==> Setting up #{repo_name} repository in #{repo_path}..."
+ start = Time.now
+ system(*%W(#{Gitlab.config.git.bin_path} clone --quiet -- #{clone_url} #{repo_path}))
+ puts " #{repo_path} set up in #{Time.now - start} seconds...\n"
end
set_repo_refs(repo_path, refs)
unless File.directory?(repo_path_bare)
+ puts "\n==> Setting up #{repo_name} bare repository in #{repo_path_bare}..."
+ start = Time.now
# We must copy bare repositories because we will push to them.
- system(git_env, *%W(#{Gitlab.config.git.bin_path} clone -q --bare #{repo_path} #{repo_path_bare}))
+ system(git_env, *%W(#{Gitlab.config.git.bin_path} clone --quiet --bare -- #{repo_path} #{repo_path_bare}))
+ puts " #{repo_path_bare} set up in #{Time.now - start} seconds...\n"
end
end
diff --git a/spec/support/helpers/usage_data_helpers.rb b/spec/support/helpers/usage_data_helpers.rb
index 1f1e686fb21..382e4f6a1a4 100644
--- a/spec/support/helpers/usage_data_helpers.rb
+++ b/spec/support/helpers/usage_data_helpers.rb
@@ -19,6 +19,9 @@ module UsageDataHelpers
cycle_analytics_views
productivity_analytics_views
source_code_pushes
+ design_management_designs_create
+ design_management_designs_update
+ design_management_designs_delete
).freeze
COUNTS_KEYS = %i(
@@ -96,6 +99,24 @@ module UsageDataHelpers
projects_with_error_tracking_enabled
projects_with_alerts_service_enabled
projects_with_prometheus_alerts
+ projects_with_expiration_policy_enabled
+ projects_with_expiration_policy_disabled
+ projects_with_expiration_policy_enabled_with_keep_n_unset
+ projects_with_expiration_policy_enabled_with_keep_n_set_to_1
+ projects_with_expiration_policy_enabled_with_keep_n_set_to_5
+ projects_with_expiration_policy_enabled_with_keep_n_set_to_10
+ projects_with_expiration_policy_enabled_with_keep_n_set_to_25
+ projects_with_expiration_policy_enabled_with_keep_n_set_to_50
+ projects_with_expiration_policy_enabled_with_older_than_unset
+ projects_with_expiration_policy_enabled_with_older_than_set_to_7d
+ projects_with_expiration_policy_enabled_with_older_than_set_to_14d
+ projects_with_expiration_policy_enabled_with_older_than_set_to_30d
+ projects_with_expiration_policy_enabled_with_older_than_set_to_90d
+ projects_with_expiration_policy_enabled_with_cadence_set_to_1d
+ projects_with_expiration_policy_enabled_with_cadence_set_to_7d
+ projects_with_expiration_policy_enabled_with_cadence_set_to_14d
+ projects_with_expiration_policy_enabled_with_cadence_set_to_1month
+ projects_with_expiration_policy_enabled_with_cadence_set_to_3month
pages_domains
protected_branches
releases
@@ -130,28 +151,62 @@ module UsageDataHelpers
gitaly
database
avg_cycle_analytics
- influxdb_metrics_enabled
prometheus_metrics_enabled
web_ide_clientside_preview_enabled
ingress_modsecurity_enabled
- projects_with_expiration_policy_disabled
- projects_with_expiration_policy_enabled
- projects_with_expiration_policy_enabled_with_keep_n_unset
- projects_with_expiration_policy_enabled_with_older_than_unset
- projects_with_expiration_policy_enabled_with_keep_n_set_to_1
- projects_with_expiration_policy_enabled_with_keep_n_set_to_5
- projects_with_expiration_policy_enabled_with_keep_n_set_to_10
- projects_with_expiration_policy_enabled_with_keep_n_set_to_25
- projects_with_expiration_policy_enabled_with_keep_n_set_to_50
- projects_with_expiration_policy_enabled_with_keep_n_set_to_100
- projects_with_expiration_policy_enabled_with_cadence_set_to_1d
- projects_with_expiration_policy_enabled_with_cadence_set_to_7d
- projects_with_expiration_policy_enabled_with_cadence_set_to_14d
- projects_with_expiration_policy_enabled_with_cadence_set_to_1month
- projects_with_expiration_policy_enabled_with_cadence_set_to_3month
- projects_with_expiration_policy_enabled_with_older_than_set_to_7d
- projects_with_expiration_policy_enabled_with_older_than_set_to_14d
- projects_with_expiration_policy_enabled_with_older_than_set_to_30d
- projects_with_expiration_policy_enabled_with_older_than_set_to_90d
+ object_store
).freeze
+
+ def stub_object_store_settings
+ allow(Settings).to receive(:[]).with('artifacts')
+ .and_return(
+ { 'enabled' => true,
+ 'object_store' =>
+ { 'enabled' => true,
+ 'remote_directory' => 'artifacts',
+ 'direct_upload' => true,
+ 'connection' =>
+ { 'provider' => 'AWS', 'aws_access_key_id' => 'minio', 'aws_secret_access_key' => 'gdk-minio', 'region' => 'gdk', 'endpoint' => 'http://127.0.0.1:9000', 'path_style' => true },
+ 'background_upload' => false,
+ 'proxy_download' => false } }
+ )
+
+ allow(Settings).to receive(:[]).with('external_diffs').and_return({ 'enabled' => false })
+
+ allow(Settings).to receive(:[]).with('lfs')
+ .and_return(
+ { 'enabled' => true,
+ 'object_store' =>
+ { 'enabled' => false,
+ 'remote_directory' => 'lfs-objects',
+ 'direct_upload' => true,
+ 'connection' =>
+ { 'provider' => 'AWS', 'aws_access_key_id' => 'minio', 'aws_secret_access_key' => 'gdk-minio', 'region' => 'gdk', 'endpoint' => 'http://127.0.0.1:9000', 'path_style' => true },
+ 'background_upload' => false,
+ 'proxy_download' => false } }
+ )
+ allow(Settings).to receive(:[]).with('uploads')
+ .and_return(
+ { 'object_store' =>
+ { 'enabled' => false,
+ 'remote_directory' => 'uploads',
+ 'direct_upload' => true,
+ 'connection' =>
+ { 'provider' => 'AWS', 'aws_access_key_id' => 'minio', 'aws_secret_access_key' => 'gdk-minio', 'region' => 'gdk', 'endpoint' => 'http://127.0.0.1:9000', 'path_style' => true },
+ 'background_upload' => false,
+ 'proxy_download' => false } }
+ )
+ allow(Settings).to receive(:[]).with('packages')
+ .and_return(
+ { 'enabled' => true,
+ 'object_store' =>
+ { 'enabled' => false,
+ 'remote_directory' => 'packages',
+ 'direct_upload' => false,
+ 'connection' =>
+ { 'provider' => 'AWS', 'aws_access_key_id' => 'minio', 'aws_secret_access_key' => 'gdk-minio', 'region' => 'gdk', 'endpoint' => 'http://127.0.0.1:9000', 'path_style' => true },
+ 'background_upload' => true,
+ 'proxy_download' => false } }
+ )
+ end
end
diff --git a/spec/support/helpers/wiki_helpers.rb b/spec/support/helpers/wiki_helpers.rb
index 86eb1793707..e6818ff8f0c 100644
--- a/spec/support/helpers/wiki_helpers.rb
+++ b/spec/support/helpers/wiki_helpers.rb
@@ -14,7 +14,10 @@ module WikiHelpers
file_content: File.read(expand_fixture_path(file_name))
}
- ::Wikis::CreateAttachmentService.new(project, user, opts)
- .execute[:result][:file_path]
+ ::Wikis::CreateAttachmentService.new(
+ container: project,
+ current_user: user,
+ params: opts
+ ).execute[:result][:file_path]
end
end
diff --git a/spec/support/helpers/workhorse_helpers.rb b/spec/support/helpers/workhorse_helpers.rb
index 53b36b3dd45..f16b6c1e910 100644
--- a/spec/support/helpers/workhorse_helpers.rb
+++ b/spec/support/helpers/workhorse_helpers.rb
@@ -11,7 +11,7 @@ module WorkhorseHelpers
header = split_header.join(':')
[
type,
- JSON.parse(Base64.urlsafe_decode64(header))
+ Gitlab::Json.parse(Base64.urlsafe_decode64(header))
]
end
end
diff --git a/spec/support/helpers/x509_helpers.rb b/spec/support/helpers/x509_helpers.rb
index 9ea997bf5f4..ce0fa268ace 100644
--- a/spec/support/helpers/x509_helpers.rb
+++ b/spec/support/helpers/x509_helpers.rb
@@ -173,22 +173,155 @@ module X509Helpers
Time.at(1561027326)
end
+ def signed_tag_signature
+ <<~SIGNATURE
+ -----BEGIN SIGNED MESSAGE-----
+ MIISfwYJKoZIhvcNAQcCoIIScDCCEmwCAQExDTALBglghkgBZQMEAgEwCwYJKoZI
+ hvcNAQcBoIIP8zCCB3QwggVcoAMCAQICBBXXLOIwDQYJKoZIhvcNAQELBQAwgbYx
+ CzAJBgNVBAYTAkRFMQ8wDQYDVQQIDAZCYXllcm4xETAPBgNVBAcMCE11ZW5jaGVu
+ MRAwDgYDVQQKDAdTaWVtZW5zMREwDwYDVQQFEwhaWlpaWlpBNjEdMBsGA1UECwwU
+ U2llbWVucyBUcnVzdCBDZW50ZXIxPzA9BgNVBAMMNlNpZW1lbnMgSXNzdWluZyBD
+ QSBNZWRpdW0gU3RyZW5ndGggQXV0aGVudGljYXRpb24gMjAxNjAeFw0xNzAyMDMw
+ NjU4MzNaFw0yMDAyMDMwNjU4MzNaMFsxETAPBgNVBAUTCFowMDBOV0RIMQ4wDAYD
+ VQQqDAVSb2dlcjEOMAwGA1UEBAwFTWVpZXIxEDAOBgNVBAoMB1NpZW1lbnMxFDAS
+ BgNVBAMMC01laWVyIFJvZ2VyMIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKC
+ AQEAuBNea/68ZCnHYQjpm/k3ZBG0wBpEKSwG6lk9CEQlSxsqVLQHAoAKBIlJm1in
+ YVLcK/Sq1yhYJ/qWcY/M53DhK2rpPuhtrWJUdOUy8EBWO20F4bd4Fw9pO7jt8bme
+ u33TSrK772vKjuppzB6SeG13Cs08H+BIeD106G27h7ufsO00pvsxoSDL+uc4slnr
+ pBL+2TAL7nSFnB9QHWmRIK27SPqJE+lESdb0pse11x1wjvqKy2Q7EjL9fpqJdHzX
+ NLKHXd2r024TOORTa05DFTNR+kQEKKV96XfpYdtSBomXNQ44cisiPBJjFtYvfnFE
+ wgrHa8fogn/b0C+A+HAoICN12wIDAQABo4IC4jCCAt4wHQYDVR0OBBYEFCF+gkUp
+ XQ6xGc0kRWXuDFxzA14zMEMGA1UdEQQ8MDqgIwYKKwYBBAGCNxQCA6AVDBNyLm1l
+ aWVyQHNpZW1lbnMuY29tgRNyLm1laWVyQHNpZW1lbnMuY29tMA4GA1UdDwEB/wQE
+ AwIHgDAdBgNVHSUEFjAUBggrBgEFBQcDAgYIKwYBBQUHAwQwgcoGA1UdHwSBwjCB
+ vzCBvKCBuaCBtoYmaHR0cDovL2NoLnNpZW1lbnMuY29tL3BraT9aWlpaWlpBNi5j
+ cmyGQWxkYXA6Ly9jbC5zaWVtZW5zLm5ldC9DTj1aWlpaWlpBNixMPVBLST9jZXJ0
+ aWZpY2F0ZVJldm9jYXRpb25MaXN0hklsZGFwOi8vY2wuc2llbWVucy5jb20vQ049
+ WlpaWlpaQTYsbz1UcnVzdGNlbnRlcj9jZXJ0aWZpY2F0ZVJldm9jYXRpb25MaXN0
+ MEUGA1UdIAQ+MDwwOgYNKwYBBAGhaQcCAgMBAzApMCcGCCsGAQUFBwIBFhtodHRw
+ Oi8vd3d3LnNpZW1lbnMuY29tL3BraS8wDAYDVR0TAQH/BAIwADAfBgNVHSMEGDAW
+ gBT4FV1HDGx3e3LEAheRaKK292oJRDCCAQQGCCsGAQUFBwEBBIH3MIH0MDIGCCsG
+ AQUFBzAChiZodHRwOi8vYWguc2llbWVucy5jb20vcGtpP1paWlpaWkE2LmNydDBB
+ BggrBgEFBQcwAoY1bGRhcDovL2FsLnNpZW1lbnMubmV0L0NOPVpaWlpaWkE2LEw9
+ UEtJP2NBQ2VydGlmaWNhdGUwSQYIKwYBBQUHMAKGPWxkYXA6Ly9hbC5zaWVtZW5z
+ LmNvbS9DTj1aWlpaWlpBNixvPVRydXN0Y2VudGVyP2NBQ2VydGlmaWNhdGUwMAYI
+ KwYBBQUHMAGGJGh0dHA6Ly9vY3NwLnBraS1zZXJ2aWNlcy5zaWVtZW5zLmNvbTAN
+ BgkqhkiG9w0BAQsFAAOCAgEAXPVcX6vaEcszJqg5IemF9aFTlwTrX5ITNIpzcqG+
+ kD5haOf2mZYLjl+MKtLC1XfmIsGCUZNb8bjP6QHQEI+2d6x/ZOqPq7Kd7PwVu6x6
+ xZrkDjUyhUbUntT5+RBy++l3Wf6Cq6Kx+K8ambHBP/bu90/p2U8KfFAG3Kr2gI2q
+ fZrnNMOxmJfZ3/sXxssgLkhbZ7hRa+MpLfQ6uFsSiat3vlawBBvTyHnoZ/7oRc8y
+ qi6QzWcd76CPpMElYWibl+hJzKbBZUWvc71AzHR6i1QeZ6wubYz7vr+FF5Y7tnxB
+ Vz6omPC9XAg0F+Dla6Zlz3Awj5imCzVXa+9SjtnsidmJdLcKzTAKyDewewoxYOOJ
+ j3cJU7VSjJPl+2fVmDBaQwcNcUcu/TPAKApkegqO7tRF9IPhjhW8QkRnkqMetO3D
+ OXmAFVIsEI0Hvb2cdb7B6jSpjGUuhaFm9TCKhQtCk2p8JCDTuaENLm1x34rrJKbT
+ 2vzyYN0CZtSkUdgD4yQxK9VWXGEzexRisWb4AnZjD2NAquLPpXmw8N0UwFD7MSpC
+ dpaX7FktdvZmMXsnGiAdtLSbBgLVWOD1gmJFDjrhNbI8NOaOaNk4jrfGqNh5lhGU
+ 4DnBT2U6Cie1anLmFH/oZooAEXR2o3Nu+1mNDJChnJp0ovs08aa3zZvBdcloOvfU
+ qdowggh3MIIGX6ADAgECAgQtyi/nMA0GCSqGSIb3DQEBCwUAMIGZMQswCQYDVQQG
+ EwJERTEPMA0GA1UECAwGQmF5ZXJuMREwDwYDVQQHDAhNdWVuY2hlbjEQMA4GA1UE
+ CgwHU2llbWVuczERMA8GA1UEBRMIWlpaWlpaQTExHTAbBgNVBAsMFFNpZW1lbnMg
+ VHJ1c3QgQ2VudGVyMSIwIAYDVQQDDBlTaWVtZW5zIFJvb3QgQ0EgVjMuMCAyMDE2
+ MB4XDTE2MDcyMDEzNDYxMFoXDTIyMDcyMDEzNDYxMFowgbYxCzAJBgNVBAYTAkRF
+ MQ8wDQYDVQQIDAZCYXllcm4xETAPBgNVBAcMCE11ZW5jaGVuMRAwDgYDVQQKDAdT
+ aWVtZW5zMREwDwYDVQQFEwhaWlpaWlpBNjEdMBsGA1UECwwUU2llbWVucyBUcnVz
+ dCBDZW50ZXIxPzA9BgNVBAMMNlNpZW1lbnMgSXNzdWluZyBDQSBNZWRpdW0gU3Ry
+ ZW5ndGggQXV0aGVudGljYXRpb24gMjAxNjCCAiIwDQYJKoZIhvcNAQEBBQADggIP
+ ADCCAgoCggIBAL9UfK+JAZEqVMVvECdYF9IK4KSw34AqyNl3rYP5x03dtmKaNu+2
+ 0fQqNESA1NGzw3s6LmrKLh1cR991nB2cvKOXu7AvEGpSuxzIcOROd4NpvRx+Ej1p
+ JIPeqf+ScmVK7lMSO8QL/QzjHOpGV3is9sG+ZIxOW9U1ESooy4Hal6ZNs4DNItsz
+ piCKqm6G3et4r2WqCy2RRuSqvnmMza7Y8BZsLy0ZVo5teObQ37E/FxqSrbDI8nxn
+ B7nVUve5ZjrqoIGSkEOtyo11003dVO1vmWB9A0WQGDqE/q3w178hGhKfxzRaqzyi
+ SoADUYS2sD/CglGTUxVq6u0pGLLsCFjItcCWqW+T9fPYfJ2CEd5b3hvqdCn+pXjZ
+ /gdX1XAcdUF5lRnGWifaYpT9n4s4adzX8q6oHSJxTppuAwLRKH6eXALbGQ1I9lGQ
+ DSOipD/09xkEsPw6HOepmf2U3YxZK1VU2sHqugFJboeLcHMzp6E1n2ctlNG1GKE9
+ FDHmdyFzDi0Nnxtf/GgVjnHF68hByEE1MYdJ4nJLuxoT9hyjYdRW9MpeNNxxZnmz
+ W3zh7QxIqP0ZfIz6XVhzrI9uZiqwwojDiM5tEOUkQ7XyW6grNXe75yt6mTj89LlB
+ H5fOW2RNmCy/jzBXDjgyskgK7kuCvUYTuRv8ITXbBY5axFA+CpxZqokpAgMBAAGj
+ ggKmMIICojCCAQUGCCsGAQUFBwEBBIH4MIH1MEEGCCsGAQUFBzAChjVsZGFwOi8v
+ YWwuc2llbWVucy5uZXQvQ049WlpaWlpaQTEsTD1QS0k/Y0FDZXJ0aWZpY2F0ZTAy
+ BggrBgEFBQcwAoYmaHR0cDovL2FoLnNpZW1lbnMuY29tL3BraT9aWlpaWlpBMS5j
+ cnQwSgYIKwYBBQUHMAKGPmxkYXA6Ly9hbC5zaWVtZW5zLmNvbS91aWQ9WlpaWlpa
+ QTEsbz1UcnVzdGNlbnRlcj9jQUNlcnRpZmljYXRlMDAGCCsGAQUFBzABhiRodHRw
+ Oi8vb2NzcC5wa2ktc2VydmljZXMuc2llbWVucy5jb20wHwYDVR0jBBgwFoAUcG2g
+ UOyp0CxnnRkV/v0EczXD4tQwEgYDVR0TAQH/BAgwBgEB/wIBADBABgNVHSAEOTA3
+ MDUGCCsGAQQBoWkHMCkwJwYIKwYBBQUHAgEWG2h0dHA6Ly93d3cuc2llbWVucy5j
+ b20vcGtpLzCBxwYDVR0fBIG/MIG8MIG5oIG2oIGzhj9sZGFwOi8vY2wuc2llbWVu
+ cy5uZXQvQ049WlpaWlpaQTEsTD1QS0k/YXV0aG9yaXR5UmV2b2NhdGlvbkxpc3SG
+ Jmh0dHA6Ly9jaC5zaWVtZW5zLmNvbS9wa2k/WlpaWlpaQTEuY3JshkhsZGFwOi8v
+ Y2wuc2llbWVucy5jb20vdWlkPVpaWlpaWkExLG89VHJ1c3RjZW50ZXI/YXV0aG9y
+ aXR5UmV2b2NhdGlvbkxpc3QwJwYDVR0lBCAwHgYIKwYBBQUHAwIGCCsGAQUFBwME
+ BggrBgEFBQcDCTAOBgNVHQ8BAf8EBAMCAQYwHQYDVR0OBBYEFPgVXUcMbHd7csQC
+ F5Foorb3aglEMA0GCSqGSIb3DQEBCwUAA4ICAQBw+sqMp3SS7DVKcILEmXbdRAg3
+ lLO1r457KY+YgCT9uX4VG5EdRKcGfWXK6VHGCi4Dos5eXFV34Mq/p8nu1sqMuoGP
+ YjHn604eWDprhGy6GrTYdxzcE/GGHkpkuE3Ir/45UcmZlOU41SJ9SNjuIVrSHMOf
+ ccSY42BCspR/Q1Z/ykmIqQecdT3/Kkx02GzzSN2+HlW6cEO4GBW5RMqsvd2n0h2d
+ fe2zcqOgkLtx7u2JCR/U77zfyxG3qXtcymoz0wgSHcsKIl+GUjITLkHfS9Op8V7C
+ Gr/dX437sIg5pVHmEAWadjkIzqdHux+EF94Z6kaHywohc1xG0KvPYPX7iSNjkvhz
+ 4NY53DHmxl4YEMLffZnaS/dqyhe1GTpcpyN8WiR4KuPfxrkVDOsuzWFtMSvNdlOV
+ gdI0MXcLMP+EOeANZWX6lGgJ3vWyemo58nzgshKd24MY3w3i6masUkxJH2KvI7UH
+ /1Db3SC8oOUjInvSRej6M3ZhYWgugm6gbpUgFoDw/o9Cg6Qm71hY0JtcaPC13rzm
+ N8a2Br0+Fa5e2VhwLmAxyfe1JKzqPwuHT0S5u05SQghL5VdzqfA8FCL/j4XC9yI6
+ csZTAQi73xFQYVjZt3+aoSz84lOlTmVo/jgvGMY/JzH9I4mETGgAJRNj34Z/0meh
+ M+pKWCojNH/dgyJSwDGCAlIwggJOAgEBMIG/MIG2MQswCQYDVQQGEwJERTEPMA0G
+ A1UECAwGQmF5ZXJuMREwDwYDVQQHDAhNdWVuY2hlbjEQMA4GA1UECgwHU2llbWVu
+ czERMA8GA1UEBRMIWlpaWlpaQTYxHTAbBgNVBAsMFFNpZW1lbnMgVHJ1c3QgQ2Vu
+ dGVyMT8wPQYDVQQDDDZTaWVtZW5zIElzc3VpbmcgQ0EgTWVkaXVtIFN0cmVuZ3Ro
+ IEF1dGhlbnRpY2F0aW9uIDIwMTYCBBXXLOIwCwYJYIZIAWUDBAIBoGkwHAYJKoZI
+ hvcNAQkFMQ8XDTE5MTEyMDE0NTYyMFowLwYJKoZIhvcNAQkEMSIEIJDnZUpcVLzC
+ OdtpkH8gtxwLPIDE0NmAmFC9uM8q2z+OMBgGCSqGSIb3DQEJAzELBgkqhkiG9w0B
+ BwEwCwYJKoZIhvcNAQEBBIIBAH/Pqv2xp3a0jSPkwU1K3eGA/1lfoNJMUny4d/PS
+ LVWlkgrmedXdLmuBzAGEaaZOJS0lEpNd01pR/reHs7xxZ+RZ0olTs2ufM0CijQSx
+ OL9HDl2O3OoD77NWx4tl3Wy1yJCeV3XH/cEI7AkKHCmKY9QMoMYWh16ORBtr+YcS
+ YK+gONOjpjgcgTJgZ3HSFgQ50xiD4WT1kFBHsuYsLqaOSbTfTN6Ayyg4edjrPQqa
+ VcVf1OQcIrfWA3yMQrnEZfOYfN/D4EPjTfxBV+VCi/F2bdZmMbJ7jNk1FbewSwWO
+ SDH1i0K32NyFbnh0BSos7njq7ELqKlYBsoB/sZfaH2vKy5U=
+ -----END SIGNED MESSAGE-----
+ SIGNATURE
+ end
+
+ def signed_tag_base_data
+ <<~SIGNEDDATA
+ object 189a6c924013fc3fe40d6f1ec1dc20214183bc97
+ type commit
+ tag v1.1.1
+ tagger Roger Meier <r.meier@siemens.com> 1574261780 +0100
+
+ x509 signed tag
+ SIGNEDDATA
+ end
+
def certificate_crl
'http://ch.siemens.com/pki?ZZZZZZA2.crl'
end
+ def tag_certificate_crl
+ 'http://ch.siemens.com/pki?ZZZZZZA6.crl'
+ end
+
def certificate_serial
1810356222
end
+ def tag_certificate_serial
+ 3664232660
+ end
+
def certificate_subject_key_identifier
'EC:00:B5:28:02:5C:D3:A5:A1:AB:C2:A1:34:81:84:AA:BF:9B:CF:F8'
end
+ def tag_certificate_subject_key_identifier
+ '21:7E:82:45:29:5D:0E:B1:19:CD:24:45:65:EE:0C:5C:73:03:5E:33'
+ end
+
def issuer_subject_key_identifier
'BD:BD:2A:43:22:3D:48:4A:57:7E:98:31:17:A9:70:9D:EE:9F:A8:99'
end
+ def tag_issuer_subject_key_identifier
+ 'F8:15:5D:47:0C:6C:77:7B:72:C4:02:17:91:68:A2:B6:F7:6A:09:44'
+ end
+
def certificate_email
'r.meier@siemens.com'
end
@@ -197,6 +330,10 @@ module X509Helpers
'CN=Siemens Issuing CA EE Auth 2016,OU=Siemens Trust Center,serialNumber=ZZZZZZA2,O=Siemens,L=Muenchen,ST=Bayern,C=DE'
end
+ def tag_certificate_issuer
+ 'CN=Siemens Issuing CA Medium Strength Authentication 2016,OU=Siemens Trust Center,serialNumber=ZZZZZZA6,O=Siemens,L=Muenchen,ST=Bayern,C=DE'
+ end
+
def certificate_subject
'CN=Meier Roger,O=Siemens,SN=Meier,GN=Roger,serialNumber=Z000NWDH'
end
diff --git a/spec/support/import_export/common_util.rb b/spec/support/import_export/common_util.rb
index 1a5668946c6..0069ae81b76 100644
--- a/spec/support/import_export/common_util.rb
+++ b/spec/support/import_export/common_util.rb
@@ -15,28 +15,9 @@ module ImportExport
export_path = [prefix, 'spec', 'fixtures', 'lib', 'gitlab', 'import_export', name].compact
export_path = File.join(*export_path)
- if File.exist?(File.join(export_path, 'tree.tar.gz'))
- extract_archive(export_path, 'tree.tar.gz')
- end
-
allow_any_instance_of(Gitlab::ImportExport).to receive(:export_path) { export_path }
end
- def extract_archive(path, archive)
- output, exit_status = Gitlab::Popen.popen(["cd #{path}; tar xzf #{archive}"])
-
- raise "Failed to extract archive. Output: #{output}" unless exit_status.zero?
- end
-
- def cleanup_artifacts_from_extract_archive(name, prefix = nil)
- export_path = [prefix, 'spec', 'fixtures', 'lib', 'gitlab', 'import_export', name].compact
- export_path = File.join(*export_path)
-
- if File.exist?(File.join(export_path, 'tree.tar.gz'))
- system("cd #{export_path}; rm -fr tree")
- end
- end
-
def setup_reader(reader)
if reader == :ndjson_reader && Feature.enabled?(:project_import_ndjson)
allow_any_instance_of(Gitlab::ImportExport::JSON::LegacyReader::File).to receive(:exist?).and_return(false)
diff --git a/spec/support/import_export/configuration_helper.rb b/spec/support/import_export/configuration_helper.rb
index 4330c4314a8..6f67b0f3dd7 100644
--- a/spec/support/import_export/configuration_helper.rb
+++ b/spec/support/import_export/configuration_helper.rb
@@ -44,8 +44,8 @@ module ConfigurationHelper
import_export_config = config_hash(config)
excluded_attributes = import_export_config[:excluded_attributes][relation_name.to_sym]
included_attributes = import_export_config[:included_attributes][relation_name.to_sym]
- attributes = attributes - JSON.parse(excluded_attributes.to_json) if excluded_attributes
- attributes = attributes & JSON.parse(included_attributes.to_json) if included_attributes
+ attributes = attributes - Gitlab::Json.parse(excluded_attributes.to_json) if excluded_attributes
+ attributes = attributes & Gitlab::Json.parse(included_attributes.to_json) if included_attributes
attributes
end
diff --git a/spec/support/kubeclient.rb b/spec/support/kubeclient.rb
new file mode 100644
index 00000000000..56c5800c801
--- /dev/null
+++ b/spec/support/kubeclient.rb
@@ -0,0 +1,10 @@
+# frozen_string_literal: true
+
+RSpec.configure do |config|
+ # Feature specs call webmock_enable_with_http_connect_on_start! by
+ # default. This is needed to prevent Kubeclient from connecting to a
+ # host before the request is stubbed.
+ config.before(:each, :kubeclient) do
+ webmock_enable!
+ end
+end
diff --git a/spec/support/matchers/disallow_request_matchers.rb b/spec/support/matchers/disallow_request_matchers.rb
index a161e3660cd..cb6f4bedbd5 100644
--- a/spec/support/matchers/disallow_request_matchers.rb
+++ b/spec/support/matchers/disallow_request_matchers.rb
@@ -11,7 +11,7 @@ end
RSpec::Matchers.define :disallow_request_in_json do
match do |response|
- json_response = JSON.parse(response.body)
+ json_response = Gitlab::Json.parse(response.body)
response.body.include?('You cannot perform write operations') && json_response.key?('message')
end
end
diff --git a/spec/support/matchers/graphql_matchers.rb b/spec/support/matchers/graphql_matchers.rb
index 6439b68764e..3e2193a9069 100644
--- a/spec/support/matchers/graphql_matchers.rb
+++ b/spec/support/matchers/graphql_matchers.rb
@@ -1,8 +1,14 @@
# frozen_string_literal: true
RSpec::Matchers.define :require_graphql_authorizations do |*expected|
- match do |field|
- expect(field.to_graphql.metadata[:authorize]).to eq(*expected)
+ match do |klass|
+ permissions = if klass.respond_to?(:required_permissions)
+ klass.required_permissions
+ else
+ [klass.to_graphql.metadata[:authorize]]
+ end
+
+ expect(permissions).to eq(expected)
end
end
diff --git a/spec/support/rails/test_case_patch.rb b/spec/support/rails/test_case_patch.rb
deleted file mode 100644
index 161e1ef2a4c..00000000000
--- a/spec/support/rails/test_case_patch.rb
+++ /dev/null
@@ -1,53 +0,0 @@
-# frozen_string_literal: true
-#
-# This file pulls in the changes in https://github.com/rails/rails/pull/38063
-# to fix controller specs updated with the latest Rack versions.
-#
-# This file should be removed after that change ships. It is not
-# present in Rails 6.0.2.2.
-module ActionController
- class TestRequest < ActionDispatch::TestRequest #:nodoc:
- def self.new_session
- TestSessionPatched.new
- end
- end
-
- # Methods #destroy and #load! are overridden to avoid calling methods on the
- # @store object, which does not exist for the TestSession class.
- class TestSessionPatched < Rack::Session::Abstract::PersistedSecure::SecureSessionHash #:nodoc:
- DEFAULT_OPTIONS = Rack::Session::Abstract::Persisted::DEFAULT_OPTIONS
-
- def initialize(session = {})
- super(nil, nil)
- @id = Rack::Session::SessionId.new(SecureRandom.hex(16))
- @data = stringify_keys(session)
- @loaded = true
- end
-
- def exists?
- true
- end
-
- def keys
- @data.keys
- end
-
- def values
- @data.values
- end
-
- def destroy
- clear
- end
-
- def fetch(key, *args, &block)
- @data.fetch(key.to_s, *args, &block)
- end
-
- private
-
- def load!
- @id
- end
- end
-end
diff --git a/spec/support/redis/redis_shared_examples.rb b/spec/support/redis/redis_shared_examples.rb
index 1e2d11a66cb..f5f6a69738b 100644
--- a/spec/support/redis/redis_shared_examples.rb
+++ b/spec/support/redis/redis_shared_examples.rb
@@ -150,7 +150,7 @@ RSpec.shared_examples "redis_shared_examples" do
it 'returns an array of hashes with host and port keys' do
is_expected.to include(host: 'localhost', port: sentinel_port)
- is_expected.to include(host: 'slave2', port: sentinel_port)
+ is_expected.to include(host: 'replica2', port: sentinel_port)
end
end
diff --git a/spec/support/renameable_upload.rb b/spec/support/renameable_upload.rb
new file mode 100644
index 00000000000..f7f00181605
--- /dev/null
+++ b/spec/support/renameable_upload.rb
@@ -0,0 +1,15 @@
+# frozen_string_literal: true
+
+class RenameableUpload < SimpleDelegator
+ attr_accessor :original_filename
+
+ # Get a fixture file with a new unique name, and the same extension
+ def self.unique_file(name)
+ upload = new(fixture_file_upload("spec/fixtures/#{name}"))
+ ext = File.extname(name)
+ new_name = File.basename(FactoryBot.generate(:filename), '.*')
+ upload.original_filename = new_name + ext
+
+ upload
+ end
+end
diff --git a/spec/support/shared_contexts/cache_allowed_users_in_namespace_shared_context.rb b/spec/support/shared_contexts/cache_allowed_users_in_namespace_shared_context.rb
new file mode 100644
index 00000000000..04f49e94647
--- /dev/null
+++ b/spec/support/shared_contexts/cache_allowed_users_in_namespace_shared_context.rb
@@ -0,0 +1,31 @@
+# frozen_string_literal: true
+
+shared_examples 'allowed user IDs are cached' do
+ it 'caches the allowed user IDs in cache', :use_clean_rails_memory_store_caching do
+ expect do
+ expect(described_class.l1_cache_backend).to receive(:fetch).and_call_original
+ expect(described_class.l2_cache_backend).not_to receive(:fetch)
+ expect(subject).to be_truthy
+ end.not_to exceed_query_limit(0)
+ end
+
+ it 'caches the allowed user IDs in L1 cache for 1 minute', :use_clean_rails_memory_store_caching do
+ Timecop.travel 2.minutes do
+ expect do
+ expect(described_class.l1_cache_backend).to receive(:fetch).and_call_original
+ expect(described_class.l2_cache_backend).to receive(:fetch).and_call_original
+ expect(subject).to be_truthy
+ end.not_to exceed_query_limit(0)
+ end
+ end
+
+ it 'caches the allowed user IDs in L2 cache for 5 minutes', :use_clean_rails_memory_store_caching do
+ Timecop.travel 6.minutes do
+ expect do
+ expect(described_class.l1_cache_backend).to receive(:fetch).and_call_original
+ expect(described_class.l2_cache_backend).to receive(:fetch).and_call_original
+ expect(subject).to be_truthy
+ end.not_to exceed_query_limit(2)
+ end
+ end
+end
diff --git a/spec/support/shared_contexts/design_management_shared_contexts.rb b/spec/support/shared_contexts/design_management_shared_contexts.rb
new file mode 100644
index 00000000000..2866effb3a8
--- /dev/null
+++ b/spec/support/shared_contexts/design_management_shared_contexts.rb
@@ -0,0 +1,38 @@
+# frozen_string_literal: true
+
+shared_context 'four designs in three versions' do
+ include DesignManagementTestHelpers
+
+ let_it_be(:issue) { create(:issue) }
+ let_it_be(:project) { issue.project }
+ let_it_be(:authorized_user) { create(:user) }
+
+ let_it_be(:design_a) { create(:design, issue: issue) }
+ let_it_be(:design_b) { create(:design, issue: issue) }
+ let_it_be(:design_c) { create(:design, issue: issue) }
+ let_it_be(:design_d) { create(:design, issue: issue) }
+
+ let_it_be(:first_version) do
+ create(:design_version, issue: issue,
+ created_designs: [design_a],
+ modified_designs: [],
+ deleted_designs: [])
+ end
+ let_it_be(:second_version) do
+ create(:design_version, issue: issue,
+ created_designs: [design_b, design_c, design_d],
+ modified_designs: [design_a],
+ deleted_designs: [])
+ end
+ let_it_be(:third_version) do
+ create(:design_version, issue: issue,
+ created_designs: [],
+ modified_designs: [design_a],
+ deleted_designs: [design_d])
+ end
+
+ before do
+ enable_design_management
+ project.add_developer(authorized_user)
+ end
+end
diff --git a/spec/support/shared_contexts/features/error_tracking_shared_context.rb b/spec/support/shared_contexts/features/error_tracking_shared_context.rb
index cbd33dd109b..102cf7c9b11 100644
--- a/spec/support/shared_contexts/features/error_tracking_shared_context.rb
+++ b/spec/support/shared_contexts/features/error_tracking_shared_context.rb
@@ -6,9 +6,9 @@ shared_context 'sentry error tracking context feature' do
let_it_be(:project) { create(:project) }
let_it_be(:project_error_tracking_settings) { create(:project_error_tracking_setting, project: project) }
let_it_be(:issue_response_body) { fixture_file('sentry/issue_sample_response.json') }
- let_it_be(:issue_response) { JSON.parse(issue_response_body) }
+ let_it_be(:issue_response) { Gitlab::Json.parse(issue_response_body) }
let_it_be(:event_response_body) { fixture_file('sentry/issue_latest_event_sample_response.json') }
- let_it_be(:event_response) { JSON.parse(event_response_body) }
+ let_it_be(:event_response) { Gitlab::Json.parse(event_response_body) }
let(:sentry_api_urls) { Sentry::ApiUrls.new(project_error_tracking_settings.api_url) }
let(:issue_id) { issue_response['id'] }
let(:issue_seen) { 1.year.ago.utc }
diff --git a/spec/support/shared_contexts/issuable/merge_request_shared_context.rb b/spec/support/shared_contexts/issuable/merge_request_shared_context.rb
new file mode 100644
index 00000000000..05ffb934c34
--- /dev/null
+++ b/spec/support/shared_contexts/issuable/merge_request_shared_context.rb
@@ -0,0 +1,49 @@
+# frozen_string_literal: true
+
+shared_context 'merge request show action' do
+ include Devise::Test::ControllerHelpers
+ include ProjectForksHelper
+
+ let(:user) { create(:user) }
+ let(:project) { create(:project, :public, :repository) }
+ let(:forked_project) { fork_project(project, user, repository: true) }
+ let(:unlink_project) { Projects::UnlinkForkService.new(forked_project, user) }
+ let(:note) { create(:note_on_merge_request, project: project, noteable: closed_merge_request) }
+
+ let(:closed_merge_request) do
+ create(:closed_merge_request,
+ source_project: forked_project,
+ target_project: project,
+ author: user)
+ end
+
+ def preload_view_requirements
+ # This will load the status fields of the author of the note and merge request
+ # to avoid queries in when rendering the view being tested.
+ closed_merge_request.author.status
+ note.author.status
+ end
+
+ before do
+ assign(:project, project)
+ assign(:merge_request, closed_merge_request)
+ assign(:commits_count, 0)
+ assign(:note, note)
+ assign(:noteable, closed_merge_request)
+ assign(:notes, [])
+ assign(:pipelines, Ci::Pipeline.none)
+ assign(:issuable_sidebar, serialize_issuable_sidebar(user, project, closed_merge_request))
+
+ preload_view_requirements
+
+ allow(view).to receive_messages(current_user: user,
+ can?: true,
+ current_application_settings: Gitlab::CurrentSettings.current_application_settings)
+ end
+
+ def serialize_issuable_sidebar(user, project, merge_request)
+ MergeRequestSerializer
+ .new(current_user: user, project: project)
+ .represent(closed_merge_request, serializer: 'sidebar')
+ end
+end
diff --git a/spec/support/shared_contexts/issuable/project_shared_context.rb b/spec/support/shared_contexts/issuable/project_shared_context.rb
new file mode 100644
index 00000000000..6dbf3154977
--- /dev/null
+++ b/spec/support/shared_contexts/issuable/project_shared_context.rb
@@ -0,0 +1,16 @@
+# frozen_string_literal: true
+
+shared_context 'project show action' do
+ let(:project) { create(:project, :repository) }
+ let(:issue) { create(:issue, project: project, author: user) }
+ let(:user) { create(:user) }
+
+ before do
+ assign(:project, project)
+ assign(:issue, issue)
+ assign(:noteable, issue)
+ stub_template 'shared/issuable/_sidebar' => ''
+ stub_template 'projects/issues/_discussion' => ''
+ allow(view).to receive(:user_status).and_return('')
+ end
+end
diff --git a/spec/support/shared_contexts/json_response_shared_context.rb b/spec/support/shared_contexts/json_response_shared_context.rb
index 6a0734decd5..2f0a564d2bc 100644
--- a/spec/support/shared_contexts/json_response_shared_context.rb
+++ b/spec/support/shared_contexts/json_response_shared_context.rb
@@ -1,5 +1,5 @@
# frozen_string_literal: true
RSpec.shared_context 'JSON response' do
- let(:json_response) { JSON.parse(response.body) }
+ let(:json_response) { Gitlab::Json.parse(response.body) }
end
diff --git a/spec/support/shared_contexts/lib/gitlab/import_export/project/rake_task_object_storage_shared_context.rb b/spec/support/shared_contexts/lib/gitlab/import_export/project/rake_task_object_storage_shared_context.rb
new file mode 100644
index 00000000000..dc1a52e3629
--- /dev/null
+++ b/spec/support/shared_contexts/lib/gitlab/import_export/project/rake_task_object_storage_shared_context.rb
@@ -0,0 +1,17 @@
+# frozen_string_literal: true
+
+RSpec.shared_context 'rake task object storage shared context' do
+ before do
+ allow(Settings.uploads.object_store).to receive(:[]=).and_call_original
+ end
+
+ around do |example|
+ old_object_store_setting = Settings.uploads.object_store['enabled']
+
+ Settings.uploads.object_store['enabled'] = true
+
+ example.run
+
+ Settings.uploads.object_store['enabled'] = old_object_store_setting
+ end
+end
diff --git a/spec/support/shared_contexts/navbar_structure_context.rb b/spec/support/shared_contexts/navbar_structure_context.rb
index 4606608866a..fe3c32ec0c5 100644
--- a/spec/support/shared_contexts/navbar_structure_context.rb
+++ b/spec/support/shared_contexts/navbar_structure_context.rb
@@ -62,6 +62,7 @@ RSpec.shared_context 'project navbar structure' do
nav_item: _('Operations'),
nav_sub_items: [
_('Metrics'),
+ _('Alerts'),
_('Environments'),
_('Error Tracking'),
_('Serverless'),
@@ -85,6 +86,7 @@ RSpec.shared_context 'project navbar structure' do
_('Members'),
_('Integrations'),
_('Webhooks'),
+ _('Access Tokens'),
_('Repository'),
_('CI / CD'),
_('Operations'),
diff --git a/spec/support/shared_contexts/policies/group_policy_shared_context.rb b/spec/support/shared_contexts/policies/group_policy_shared_context.rb
index c2797c49c02..a0d54666dff 100644
--- a/spec/support/shared_contexts/policies/group_policy_shared_context.rb
+++ b/spec/support/shared_contexts/policies/group_policy_shared_context.rb
@@ -14,17 +14,16 @@ RSpec.shared_context 'GroupPolicy context' do
%i[
read_label read_group upload_file read_namespace read_group_activity
read_group_issues read_group_boards read_group_labels read_group_milestones
- read_group_merge_requests read_wiki
+ read_group_merge_requests
]
end
let(:read_group_permissions) { %i[read_label read_list read_milestone read_board] }
- let(:reporter_permissions) { %i[admin_label read_container_image read_metrics_dashboard_annotation download_wiki_code] }
- let(:developer_permissions) { %i[admin_milestone create_metrics_dashboard_annotation delete_metrics_dashboard_annotation update_metrics_dashboard_annotation create_wiki] }
+ let(:reporter_permissions) { %i[admin_label read_container_image read_metrics_dashboard_annotation] }
+ let(:developer_permissions) { %i[admin_milestone create_metrics_dashboard_annotation delete_metrics_dashboard_annotation update_metrics_dashboard_annotation] }
let(:maintainer_permissions) do
%i[
create_projects
read_cluster create_cluster update_cluster admin_cluster add_cluster
- admin_wiki
]
end
let(:owner_permissions) do
@@ -35,7 +34,8 @@ RSpec.shared_context 'GroupPolicy context' do
:change_visibility_level,
:set_note_created_at,
:create_subgroup,
- :read_statistics
+ :read_statistics,
+ :update_default_branch_protection
].compact
end
diff --git a/spec/support/shared_contexts/policies/project_policy_shared_context.rb b/spec/support/shared_contexts/policies/project_policy_shared_context.rb
index 055164ec38e..5339fa003b9 100644
--- a/spec/support/shared_contexts/policies/project_policy_shared_context.rb
+++ b/spec/support/shared_contexts/policies/project_policy_shared_context.rb
@@ -39,7 +39,7 @@ RSpec.shared_context 'ProjectPolicy context' do
update_pipeline create_merge_request_from create_wiki push_code
resolve_note create_container_image update_container_image
create_environment create_deployment update_deployment create_release update_release
- update_environment
+ update_environment daily_statistics
]
end
@@ -49,7 +49,6 @@ RSpec.shared_context 'ProjectPolicy context' do
admin_snippet admin_project_member admin_note admin_wiki admin_project
admin_commit_status admin_build admin_container_image
admin_pipeline admin_environment admin_deployment destroy_release add_cluster
- daily_statistics
]
end
diff --git a/spec/support/shared_contexts/project_service_shared_context.rb b/spec/support/shared_contexts/project_service_shared_context.rb
index 89b196e7039..21d67ea71a8 100644
--- a/spec/support/shared_contexts/project_service_shared_context.rb
+++ b/spec/support/shared_contexts/project_service_shared_context.rb
@@ -5,6 +5,7 @@ shared_context 'project service activation' do
let(:user) { create(:user) }
before do
+ stub_feature_flags(integration_form_refactor: false)
project.add_maintainer(user)
sign_in(user)
end
@@ -18,6 +19,10 @@ shared_context 'project service activation' do
click_link(name)
end
+ def click_active_toggle
+ find('input[name="service[active]"] + button').click
+ end
+
def click_test_integration
click_button('Test settings and save changes')
end
diff --git a/spec/support/shared_contexts/services_shared_context.rb b/spec/support/shared_contexts/services_shared_context.rb
index 21bc0651c44..bf4eac8f324 100644
--- a/spec/support/shared_contexts/services_shared_context.rb
+++ b/spec/support/shared_contexts/services_shared_context.rb
@@ -31,8 +31,7 @@ Service.available_services_names.each do |service|
let(:licensed_features) do
{
'github' => :github_project_service_integration,
- 'jenkins' => :jenkins_integration,
- 'jenkins_deprecated' => :jenkins_integration
+ 'jenkins' => :jenkins_integration
}
end
diff --git a/spec/support/shared_contexts/spam_constants.rb b/spec/support/shared_contexts/spam_constants.rb
new file mode 100644
index 00000000000..b6e92ea3050
--- /dev/null
+++ b/spec/support/shared_contexts/spam_constants.rb
@@ -0,0 +1,7 @@
+# frozen_string_literal: true
+
+shared_context 'includes Spam constants' do
+ REQUIRE_RECAPTCHA = Spam::SpamConstants::REQUIRE_RECAPTCHA
+ DISALLOW = Spam::SpamConstants::DISALLOW
+ ALLOW = Spam::SpamConstants::ALLOW
+end
diff --git a/spec/support/shared_examples/controllers/instance_statistics_controllers_shared_examples.rb b/spec/support/shared_examples/controllers/instance_statistics_controllers_shared_examples.rb
index e4d59463d93..17087456720 100644
--- a/spec/support/shared_examples/controllers/instance_statistics_controllers_shared_examples.rb
+++ b/spec/support/shared_examples/controllers/instance_statistics_controllers_shared_examples.rb
@@ -27,12 +27,24 @@ RSpec.shared_examples 'instance statistics availability' do
context 'for admins' do
let(:user) { create(:admin) }
- it 'allows access when the feature is not available publicly' do
- stub_application_setting(instance_statistics_visibility_private: true)
+ context 'when admin mode disabled' do
+ it 'forbids access when the feature is not available publicly' do
+ stub_application_setting(instance_statistics_visibility_private: true)
- get :index
+ get :index
- expect(response).to have_gitlab_http_status(:success)
+ expect(response).to have_gitlab_http_status(:not_found)
+ end
+ end
+
+ context 'when admin mode enabled', :enable_admin_mode do
+ it 'allows access when the feature is not available publicly' do
+ stub_application_setting(instance_statistics_visibility_private: true)
+
+ get :index
+
+ expect(response).to have_gitlab_http_status(:success)
+ end
end
end
end
diff --git a/spec/support/shared_examples/controllers/known_sign_in_shared_examples.rb b/spec/support/shared_examples/controllers/known_sign_in_shared_examples.rb
new file mode 100644
index 00000000000..60abb76acec
--- /dev/null
+++ b/spec/support/shared_examples/controllers/known_sign_in_shared_examples.rb
@@ -0,0 +1,59 @@
+# frozen_string_literal: true
+
+RSpec.shared_examples 'known sign in' do
+ def stub_remote_ip(ip)
+ request.remote_ip = ip
+ end
+
+ def stub_user_ip(ip)
+ user.update!(current_sign_in_ip: ip)
+ end
+
+ context 'with a valid post' do
+ context 'when remote IP does not match user last sign in IP' do
+ before do
+ stub_user_ip('127.0.0.1')
+ stub_remote_ip('169.0.0.1')
+ end
+
+ it 'notifies the user' do
+ expect_next_instance_of(NotificationService) do |instance|
+ expect(instance).to receive(:unknown_sign_in)
+ end
+
+ post_action
+ end
+ end
+
+ context 'when remote IP matches an active session' do
+ before do
+ existing_sessions = ActiveSession.session_ids_for_user(user.id)
+ existing_sessions.each { |sessions| ActiveSession.destroy(user, sessions) }
+
+ stub_user_ip('169.0.0.1')
+ stub_remote_ip('127.0.0.1')
+
+ ActiveSession.set(user, request)
+ end
+
+ it 'does not notify the user' do
+ expect_any_instance_of(NotificationService).not_to receive(:unknown_sign_in)
+
+ post_action
+ end
+ end
+
+ context 'when remote IP address matches last sign in IP' do
+ before do
+ stub_user_ip('127.0.0.1')
+ stub_remote_ip('127.0.0.1')
+ end
+
+ it 'does not notify the user' do
+ expect_any_instance_of(NotificationService).not_to receive(:unknown_sign_in)
+
+ post_action
+ end
+ end
+ end
+end
diff --git a/spec/support/shared_examples/controllers/variables_shared_examples.rb b/spec/support/shared_examples/controllers/variables_shared_examples.rb
index 752bdc47851..9ff0bc3d217 100644
--- a/spec/support/shared_examples/controllers/variables_shared_examples.rb
+++ b/spec/support/shared_examples/controllers/variables_shared_examples.rb
@@ -27,6 +27,9 @@ RSpec.shared_examples 'PATCH #update updates variables' do
protected: 'false' }
end
+ let(:variables_scope) { owner.variables }
+ let(:file_variables_scope) { owner.variables.file }
+
context 'with invalid new variable parameters' do
let(:variables_attributes) do
[
@@ -40,7 +43,7 @@ RSpec.shared_examples 'PATCH #update updates variables' do
end
it 'does not create the new variable' do
- expect { subject }.not_to change { owner.variables.count }
+ expect { subject }.not_to change { variables_scope.count }
end
it 'returns a bad request response' do
@@ -63,7 +66,7 @@ RSpec.shared_examples 'PATCH #update updates variables' do
end
it 'does not create the new variable' do
- expect { subject }.not_to change { owner.variables.count }
+ expect { subject }.not_to change { variables_scope.count }
end
it 'returns a bad request response' do
@@ -86,7 +89,7 @@ RSpec.shared_examples 'PATCH #update updates variables' do
end
it 'creates the new variable' do
- expect { subject }.to change { owner.variables.count }.by(1)
+ expect { subject }.to change { variables_scope.count }.by(1)
end
it 'returns a successful response' do
@@ -106,7 +109,7 @@ RSpec.shared_examples 'PATCH #update updates variables' do
let(:variables_attributes) { [variable_attributes.merge(_destroy: 'true')] }
it 'destroys the variable' do
- expect { subject }.to change { owner.variables.count }.by(-1)
+ expect { subject }.to change { variables_scope.count }.by(-1)
expect { variable.reload }.to raise_error ActiveRecord::RecordNotFound
end
@@ -123,6 +126,18 @@ RSpec.shared_examples 'PATCH #update updates variables' do
end
end
+ context 'with missing variable' do
+ let(:variables_attributes) do
+ [variable_attributes.merge(_destroy: 'true', id: 'some-id')]
+ end
+
+ it 'returns not found response' do
+ subject
+
+ expect(response).to have_gitlab_http_status(:not_found)
+ end
+ end
+
context 'for variables of type file' do
let(:variables_attributes) do
[
@@ -131,7 +146,7 @@ RSpec.shared_examples 'PATCH #update updates variables' do
end
it 'creates new variable of type file' do
- expect { subject }.to change { owner.variables.file.count }.by(1)
+ expect { subject }.to change { file_variables_scope.count }.by(1)
end
end
end
diff --git a/spec/support/shared_examples/features/error_tracking_shared_example.rb b/spec/support/shared_examples/features/error_tracking_shared_example.rb
index 922d2627bce..1cd05b22ae9 100644
--- a/spec/support/shared_examples/features/error_tracking_shared_example.rb
+++ b/spec/support/shared_examples/features/error_tracking_shared_example.rb
@@ -1,7 +1,7 @@
# frozen_string_literal: true
shared_examples 'error tracking index page' do
- it 'renders the error index page' do
+ it 'renders the error index page', quarantine: { issue: 'https://gitlab.com/gitlab-org/gitlab/-/issues/217810' } do
within('div.js-title-container') do
expect(page).to have_content(project.namespace.name)
expect(page).to have_content(project.name)
@@ -15,7 +15,7 @@ shared_examples 'error tracking index page' do
end
end
- it 'loads the error show page on click' do
+ it 'loads the error show page on click', quarantine: { issue: 'https://gitlab.com/gitlab-org/gitlab/-/issues/217810' } do
click_on issues_response[0]['title']
wait_for_requests
@@ -23,7 +23,7 @@ shared_examples 'error tracking index page' do
expect(page).to have_content('Error Details')
end
- it 'renders the error index data' do
+ it 'renders the error index data', quarantine: { issue: 'https://gitlab.com/gitlab-org/gitlab/-/issues/217810' } do
within('div.error-list') do
expect(page).to have_content(issues_response[0]['title'])
expect(page).to have_content(issues_response[0]['count'].to_s)
@@ -34,7 +34,7 @@ shared_examples 'error tracking index page' do
end
shared_examples 'expanded stack trace context' do |selected_line: nil, expected_line: 1|
- it 'expands the stack trace context' do
+ it 'expands the stack trace context', quarantine: { issue: 'https://gitlab.com/gitlab-org/gitlab/-/issues/217810' } do
within('div.stacktrace') do
find("div.file-holder:nth-child(#{selected_line}) svg.ic-chevron-right").click if selected_line
@@ -49,7 +49,7 @@ shared_examples 'expanded stack trace context' do |selected_line: nil, expected_
end
shared_examples 'error tracking show page' do
- it 'renders the error details' do
+ it 'renders the error details', quarantine: { issue: 'https://gitlab.com/gitlab-org/gitlab/-/issues/217810' } do
content = page.find(".content")
nav = page.find("nav.breadcrumbs")
header = page.find(".error-details-header")
@@ -67,11 +67,11 @@ shared_examples 'error tracking show page' do
expect(content).to have_content('Users: 0')
end
- it 'renders the stack trace heading' do
+ it 'renders the stack trace heading', quarantine: { issue: 'https://gitlab.com/gitlab-org/gitlab/-/issues/217810' } do
expect(page).to have_content('Stack trace')
end
- it 'renders the stack trace' do
+ it 'renders the stack trace', quarantine: { issue: 'https://gitlab.com/gitlab-org/gitlab/-/issues/217810' } do
event_response['entries'][0]['data']['values'][0]['stacktrace']['frames'].each do |frame|
expect(frame['filename']).not_to be_nil
expect(page).to have_content(frame['filename'])
diff --git a/spec/support/shared_examples/features/variable_list_shared_examples.rb b/spec/support/shared_examples/features/variable_list_shared_examples.rb
index 4fd4d42003f..218ef070221 100644
--- a/spec/support/shared_examples/features/variable_list_shared_examples.rb
+++ b/spec/support/shared_examples/features/variable_list_shared_examples.rb
@@ -29,7 +29,6 @@ RSpec.shared_examples 'variable list' do
page.within('.js-ci-variable-list-section .js-row:last-child') do
find('.js-ci-variable-input-key').set('key')
find('.js-ci-variable-input-value').set('key_value')
- find('.ci-variable-protected-item .js-project-feature-toggle').click
expect(find('.js-ci-variable-input-protected', visible: false).value).to eq('true')
end
@@ -173,6 +172,7 @@ RSpec.shared_examples 'variable list' do
page.within('.js-ci-variable-list-section .js-row:last-child') do
find('.js-ci-variable-input-key').set('unprotected_key')
find('.js-ci-variable-input-value').set('unprotected_value')
+ find('.ci-variable-protected-item .js-project-feature-toggle').click
expect(find('.js-ci-variable-input-protected', visible: false).value).to eq('false')
end
@@ -207,7 +207,6 @@ RSpec.shared_examples 'variable list' do
page.within('.js-ci-variable-list-section .js-row:last-child') do
find('.js-ci-variable-input-key').set('protected_key')
find('.js-ci-variable-input-value').set('protected_value')
- find('.ci-variable-protected-item .js-project-feature-toggle').click
expect(find('.js-ci-variable-input-protected', visible: false).value).to eq('true')
end
diff --git a/spec/support/shared_examples/graphql/design_fields_shared_examples.rb b/spec/support/shared_examples/graphql/design_fields_shared_examples.rb
new file mode 100644
index 00000000000..029d7e677da
--- /dev/null
+++ b/spec/support/shared_examples/graphql/design_fields_shared_examples.rb
@@ -0,0 +1,80 @@
+# frozen_string_literal: true
+
+# To use these shared examples, you may define a value in scope named
+# `extra_design_fields`, to pass any extra fields in addition to the
+# standard design fields.
+RSpec.shared_examples 'a GraphQL type with design fields' do
+ let(:extra_design_fields) { [] }
+
+ it { expect(described_class).to require_graphql_authorizations(:read_design) }
+
+ it 'exposes the expected design fields' do
+ expected_fields = %i[
+ id
+ project
+ issue
+ filename
+ full_path
+ image
+ image_v432x230
+ diff_refs
+ event
+ notes_count
+ ] + extra_design_fields
+
+ expect(described_class).to have_graphql_fields(*expected_fields).only
+ end
+
+ describe '#image' do
+ let(:schema) { GitlabSchema }
+ let(:query) { GraphQL::Query.new(schema) }
+ let(:context) { double('Context', schema: schema, query: query, parent: nil) }
+ let(:field) { described_class.fields['image'] }
+ let(:args) { GraphQL::Query::Arguments::NO_ARGS }
+ let(:instance) do
+ object = GitlabSchema.sync_lazy(GitlabSchema.object_from_id(object_id))
+ object_type.authorized_new(object, query.context)
+ end
+ let(:instance_b) do
+ object_b = GitlabSchema.sync_lazy(GitlabSchema.object_from_id(object_id_b))
+ object_type.authorized_new(object_b, query.context)
+ end
+
+ it 'resolves to the design image URL' do
+ image = field.resolve_field(instance, args, context)
+ sha = design.versions.first.sha
+ url = ::Gitlab::Routing.url_helpers.project_design_management_designs_raw_image_url(design.project, design, sha)
+
+ expect(image).to eq(url)
+ end
+
+ it 'has better than O(N) peformance', :request_store do
+ # Assuming designs have been loaded (as they must be), the following
+ # queries are required:
+ # For each distinct version:
+ # - design_management_versions
+ # (Request store is needed so that each version is fetched only once.)
+ # For each distinct issue
+ # - issues
+ # For each distinct project
+ # - projects
+ # - routes
+ # - namespaces
+ # Here that total is:
+ # - 2 x issues
+ # - 2 x versions
+ # - 2 x (projects + routes + namespaces)
+ # = 10
+ expect(instance).not_to eq(instance_b) # preload designs themselves.
+ expect do
+ image_a = field.resolve_field(instance, args, context)
+ image_b = field.resolve_field(instance, args, context)
+ image_c = field.resolve_field(instance_b, args, context)
+ image_d = field.resolve_field(instance_b, args, context)
+ expect(image_a).to eq(image_b)
+ expect(image_c).not_to eq(image_b)
+ expect(image_c).to eq(image_d)
+ end.not_to exceed_query_limit(10)
+ end
+ end
+end
diff --git a/spec/support/shared_examples/graphql/jira_import/jira_import_resolver_shared_examples.rb b/spec/support/shared_examples/graphql/jira_import/jira_import_resolver_shared_examples.rb
index 3d97fe10a47..2b96010477c 100644
--- a/spec/support/shared_examples/graphql/jira_import/jira_import_resolver_shared_examples.rb
+++ b/spec/support/shared_examples/graphql/jira_import/jira_import_resolver_shared_examples.rb
@@ -1,12 +1,12 @@
# frozen_string_literal: true
-shared_examples 'no jira import data present' do
+shared_examples 'no Jira import data present' do
it 'returns none' do
expect(resolve_imports).to eq JiraImportState.none
end
end
-shared_examples 'no jira import access' do
+shared_examples 'no Jira import access' do
it 'raises error' do
expect do
resolve_imports
diff --git a/spec/support/shared_examples/graphql/sorted_paginated_query_shared_examples.rb b/spec/support/shared_examples/graphql/sorted_paginated_query_shared_examples.rb
new file mode 100644
index 00000000000..fb7e24eecf2
--- /dev/null
+++ b/spec/support/shared_examples/graphql/sorted_paginated_query_shared_examples.rb
@@ -0,0 +1,92 @@
+# frozen_string_literal: true
+
+# Use this for testing how a GraphQL query handles sorting and pagination.
+# This is particularly important when using keyset pagination connection,
+# which is the default for ActiveRecord relations, as certain sort keys
+# might not be supportable.
+#
+# sort_param: the value to specify the sort
+# data_path: the keys necessary to dig into the return GraphQL data to get the
+# returned results
+# first_param: number of items expected (like a page size)
+# expected_results: array of comparison data of all items sorted correctly
+# pagination_query: method that specifies the GraphQL query
+# pagination_results_data: method that extracts the sorted data used to compare against
+# the expected results
+#
+# Example:
+# describe 'sorting and pagination' do
+# let(:sort_project) { create(:project, :public) }
+# let(:data_path) { [:project, :issues] }
+#
+# def pagination_query(params, page_info)
+# graphql_query_for(
+# 'project',
+# { 'fullPath' => sort_project.full_path },
+# "issues(#{params}) { #{page_info} edges { node { iid weight } } }"
+# )
+# end
+#
+# def pagination_results_data(data)
+# data.map { |issue| issue.dig('node', 'iid').to_i }
+# end
+#
+# context 'when sorting by weight' do
+# ...
+# context 'when ascending' do
+# it_behaves_like 'sorted paginated query' do
+# let(:sort_param) { 'WEIGHT_ASC' }
+# let(:first_param) { 2 }
+# let(:expected_results) { [weight_issue3.iid, weight_issue5.iid, weight_issue1.iid, weight_issue4.iid, weight_issue2.iid] }
+# end
+# end
+#
+RSpec.shared_examples 'sorted paginated query' do
+ it_behaves_like 'requires variables' do
+ let(:required_variables) { [:sort_param, :first_param, :expected_results, :data_path, :current_user] }
+ end
+
+ describe do
+ let(:params) { "sort: #{sort_param}" }
+ let(:start_cursor) { graphql_data_at(*data_path, :pageInfo, :startCursor) }
+ let(:end_cursor) { graphql_data_at(*data_path, :pageInfo, :endCursor) }
+ let(:sorted_edges) { graphql_data_at(*data_path, :edges) }
+ let(:page_info) { "pageInfo { startCursor endCursor }" }
+
+ def pagination_query(params, page_info)
+ raise('pagination_query(params, page_info) must be defined in the test, see example in comment') unless defined?(super)
+
+ super
+ end
+
+ def pagination_results_data(data)
+ raise('pagination_results_data(data) must be defined in the test, see example in comment') unless defined?(super)
+
+ super(data)
+ end
+
+ before do
+ post_graphql(pagination_query(params, page_info), current_user: current_user)
+ end
+
+ context 'when sorting' do
+ it 'sorts correctly' do
+ expect(pagination_results_data(sorted_edges)).to eq expected_results
+ end
+
+ context 'when paginating' do
+ let(:params) { "sort: #{sort_param}, first: #{first_param}" }
+
+ it 'paginates correctly' do
+ expect(pagination_results_data(sorted_edges)).to eq expected_results.first(first_param)
+
+ cursored_query = pagination_query("sort: #{sort_param}, after: \"#{end_cursor}\"", page_info)
+ post_graphql(cursored_query, current_user: current_user)
+ response_data = graphql_dig_at(Gitlab::Json.parse(response.body), :data, *data_path, :edges)
+
+ expect(pagination_results_data(response_data)).to eq expected_results.drop(first_param)
+ end
+ end
+ end
+ end
+end
diff --git a/spec/support/shared_examples/helm_commands_shared_examples.rb b/spec/support/shared_examples/helm_commands_shared_examples.rb
new file mode 100644
index 00000000000..f0624fbf29f
--- /dev/null
+++ b/spec/support/shared_examples/helm_commands_shared_examples.rb
@@ -0,0 +1,131 @@
+# frozen_string_literal: true
+
+shared_examples 'helm command generator' do
+ describe '#generate_script' do
+ let(:helm_setup) do
+ <<~EOS
+ set -xeo pipefail
+ EOS
+ end
+
+ it 'returns appropriate command' do
+ expect(subject.generate_script.strip).to eq((helm_setup + commands).strip)
+ end
+ end
+end
+
+shared_examples 'helm command' do
+ describe '#rbac?' do
+ subject { command.rbac? }
+
+ context 'rbac is enabled' do
+ let(:rbac) { true }
+
+ it { is_expected.to be_truthy }
+ end
+
+ context 'rbac is not enabled' do
+ let(:rbac) { false }
+
+ it { is_expected.to be_falsey }
+ end
+ end
+
+ describe '#pod_resource' do
+ subject { command.pod_resource }
+
+ context 'rbac is enabled' do
+ let(:rbac) { true }
+
+ it { is_expected.to be_an_instance_of ::Kubeclient::Resource }
+
+ it 'generates a pod that uses the tiller serviceAccountName' do
+ expect(subject.spec.serviceAccountName).to eq('tiller')
+ end
+ end
+
+ context 'rbac is not enabled' do
+ let(:rbac) { false }
+
+ it { is_expected.to be_an_instance_of ::Kubeclient::Resource }
+
+ it 'generates a pod that uses the default serviceAccountName' do
+ expect(subject.spec.serviceAcccountName).to be_nil
+ end
+ end
+ end
+
+ describe '#config_map_resource' do
+ subject { command.config_map_resource }
+
+ let(:metadata) do
+ {
+ name: "values-content-configuration-#{command.name}",
+ namespace: 'gitlab-managed-apps',
+ labels: { name: "values-content-configuration-#{command.name}" }
+ }
+ end
+
+ let(:resource) { ::Kubeclient::Resource.new(metadata: metadata, data: command.files) }
+
+ it 'returns a KubeClient resource with config map content for the application' do
+ is_expected.to eq(resource)
+ end
+ end
+
+ describe '#service_account_resource' do
+ let(:resource) do
+ Kubeclient::Resource.new(metadata: { name: 'tiller', namespace: 'gitlab-managed-apps' })
+ end
+
+ subject { command.service_account_resource }
+
+ context 'rbac is enabled' do
+ let(:rbac) { true }
+
+ it 'generates a Kubeclient resource for the tiller ServiceAccount' do
+ is_expected.to eq(resource)
+ end
+ end
+
+ context 'rbac is not enabled' do
+ let(:rbac) { false }
+
+ it 'generates nothing' do
+ is_expected.to be_nil
+ end
+ end
+ end
+
+ describe '#cluster_role_binding_resource' do
+ let(:resource) do
+ Kubeclient::Resource.new(
+ metadata: { name: 'tiller-admin' },
+ roleRef: { apiGroup: 'rbac.authorization.k8s.io', kind: 'ClusterRole', name: 'cluster-admin' },
+ subjects: [{ kind: 'ServiceAccount', name: 'tiller', namespace: 'gitlab-managed-apps' }]
+ )
+ end
+
+ subject(:cluster_role_binding_resource) { command.cluster_role_binding_resource }
+
+ context 'rbac is enabled' do
+ let(:rbac) { true }
+
+ it 'generates a Kubeclient resource for the ClusterRoleBinding for tiller' do
+ is_expected.to eq(resource)
+ end
+
+ it 'binds the account in #service_account_resource' do
+ expect(cluster_role_binding_resource.subjects.first.name).to eq(command.service_account_resource.metadata.name)
+ end
+ end
+
+ context 'rbac is not enabled' do
+ let(:rbac) { false }
+
+ it 'generates nothing' do
+ is_expected.to be_nil
+ end
+ end
+ end
+end
diff --git a/spec/support/shared_examples/lib/gitlab/cycle_analytics/base_stage_shared_examples.rb b/spec/support/shared_examples/lib/gitlab/cycle_analytics/base_stage_shared_examples.rb
index 851ed9c65a3..14292f70228 100644
--- a/spec/support/shared_examples/lib/gitlab/cycle_analytics/base_stage_shared_examples.rb
+++ b/spec/support/shared_examples/lib/gitlab/cycle_analytics/base_stage_shared_examples.rb
@@ -63,7 +63,7 @@ shared_examples 'Gitlab::Analytics::CycleAnalytics::DataCollector backend exampl
context 'provides the same results as the old implementation' do
it 'for the median' do
- expect(data_collector.median.seconds).to eq(ISSUES_MEDIAN)
+ expect(data_collector.median.seconds).to be_within(0.5).of(ISSUES_MEDIAN)
end
it 'for the list of event records' do
diff --git a/spec/support/shared_examples/lib/gitlab/helm_generated_script_shared_examples.rb b/spec/support/shared_examples/lib/gitlab/helm_generated_script_shared_examples.rb
deleted file mode 100644
index bbf8a946f8b..00000000000
--- a/spec/support/shared_examples/lib/gitlab/helm_generated_script_shared_examples.rb
+++ /dev/null
@@ -1,15 +0,0 @@
-# frozen_string_literal: true
-
-RSpec.shared_examples 'helm commands' do
- describe '#generate_script' do
- let(:helm_setup) do
- <<~EOS
- set -xeo pipefail
- EOS
- end
-
- it 'returns appropriate command' do
- expect(subject.generate_script.strip).to eq((helm_setup + commands).strip)
- end
- end
-end
diff --git a/spec/support/shared_examples/lib/gitlab/import_export/project/rake_task_object_storage_shared_examples.rb b/spec/support/shared_examples/lib/gitlab/import_export/project/rake_task_object_storage_shared_examples.rb
new file mode 100644
index 00000000000..d6dc89a2c15
--- /dev/null
+++ b/spec/support/shared_examples/lib/gitlab/import_export/project/rake_task_object_storage_shared_examples.rb
@@ -0,0 +1,22 @@
+# frozen_string_literal: true
+
+RSpec.shared_examples 'rake task with disabled object_storage' do |service_class, method|
+ it 'disables direct & background upload only for service call' do
+ expect_next_instance_of(service_class) do |service|
+ expect(service).to receive(:execute).and_wrap_original do |m|
+ expect(Settings.uploads.object_store['enabled']).to eq(false)
+
+ m.call
+ end
+ end
+
+ expect(rake_task).to receive(method).and_wrap_original do |m, *args|
+ expect(Settings.uploads.object_store['enabled']).to eq(true)
+ expect(Settings.uploads.object_store).not_to receive(:[]=).with('enabled', false)
+
+ m.call(*args)
+ end
+
+ subject
+ end
+end
diff --git a/spec/support/shared_examples/lib/gitlab/migration_helpers_shared_examples.rb b/spec/support/shared_examples/lib/gitlab/migration_helpers_shared_examples.rb
index 8893ed5504b..72d672fd36c 100644
--- a/spec/support/shared_examples/lib/gitlab/migration_helpers_shared_examples.rb
+++ b/spec/support/shared_examples/lib/gitlab/migration_helpers_shared_examples.rb
@@ -13,10 +13,11 @@ end
RSpec.shared_examples 'performs validation' do |validation_option|
it 'performs validation' do
expect(model).to receive(:disable_statement_timeout).and_call_original
+ expect(model).to receive(:statement_timeout_disabled?).and_return(false)
expect(model).to receive(:execute).with(/statement_timeout/)
expect(model).to receive(:execute).ordered.with(/NOT VALID/)
expect(model).to receive(:execute).ordered.with(/VALIDATE CONSTRAINT/)
- expect(model).to receive(:execute).with(/RESET ALL/)
+ expect(model).to receive(:execute).ordered.with(/RESET ALL/)
model.add_concurrent_foreign_key(*args, **options.merge(validation_option))
end
diff --git a/spec/support/shared_examples/models/chat_service_shared_examples.rb b/spec/support/shared_examples/models/chat_service_shared_examples.rb
index 1cc1a1c8176..0a1c27b32db 100644
--- a/spec/support/shared_examples/models/chat_service_shared_examples.rb
+++ b/spec/support/shared_examples/models/chat_service_shared_examples.rb
@@ -198,7 +198,7 @@ RSpec.shared_examples "chat service" do |service_name|
message: "user created page: Awesome wiki_page"
}
end
- let(:wiki_page) { create(:wiki_page, wiki: project.wiki, attrs: opts) }
+ let(:wiki_page) { create(:wiki_page, wiki: project.wiki, **opts) }
let(:sample_data) { Gitlab::DataBuilder::WikiPage.build(wiki_page, user, "create") }
it_behaves_like "triggered #{service_name} service"
diff --git a/spec/support/shared_examples/models/cluster_application_status_shared_examples.rb b/spec/support/shared_examples/models/cluster_application_status_shared_examples.rb
index 37f1b33d455..c2fd04d648b 100644
--- a/spec/support/shared_examples/models/cluster_application_status_shared_examples.rb
+++ b/spec/support/shared_examples/models/cluster_application_status_shared_examples.rb
@@ -194,6 +194,66 @@ RSpec.shared_examples 'cluster application status specs' do |application_name|
end
end
+ describe '#make_externally_installed' do
+ subject { create(application_name, :installing) }
+
+ it 'is installed' do
+ subject.make_externally_installed
+
+ expect(subject).to be_installed
+ end
+
+ context 'application is updated' do
+ subject { create(application_name, :updated) }
+
+ it 'is installed' do
+ subject.make_externally_installed
+
+ expect(subject).to be_installed
+ end
+ end
+
+ context 'application is errored' do
+ subject { create(application_name, :errored) }
+
+ it 'is installed' do
+ subject.make_externally_installed
+
+ expect(subject).to be_installed
+ end
+ end
+ end
+
+ describe '#make_externally_uninstalled' do
+ subject { create(application_name, :installed) }
+
+ it 'is uninstalled' do
+ subject.make_externally_uninstalled
+
+ expect(subject).to be_uninstalled
+ end
+
+ context 'application is updated' do
+ subject { create(application_name, :updated) }
+
+ it 'is uninstalled' do
+ subject.make_externally_uninstalled
+
+ expect(subject).to be_uninstalled
+ end
+ end
+
+ context 'application is errored' do
+ subject { create(application_name, :errored) }
+
+ it 'is uninstalled' do
+ subject.make_externally_uninstalled
+
+ expect(subject).to be_uninstalled
+ end
+ end
+ end
+
describe '#make_scheduled' do
subject { create(application_name, :installable) }
@@ -278,6 +338,7 @@ RSpec.shared_examples 'cluster application status specs' do |application_name|
:update_errored | false
:uninstalling | false
:uninstall_errored | false
+ :uninstalled | false
:timed_out | false
end
diff --git a/spec/support/shared_examples/models/concerns/blob_replicator_strategy_shared_examples.rb b/spec/support/shared_examples/models/concerns/blob_replicator_strategy_shared_examples.rb
index 8372ee9ac4a..76339837351 100644
--- a/spec/support/shared_examples/models/concerns/blob_replicator_strategy_shared_examples.rb
+++ b/spec/support/shared_examples/models/concerns/blob_replicator_strategy_shared_examples.rb
@@ -76,7 +76,7 @@ RSpec.shared_examples 'a blob replicator' do
expect(service).to receive(:execute)
expect(::Geo::BlobDownloadService).to receive(:new).with(replicator: replicator).and_return(service)
- replicator.consume_created_event
+ replicator.consume_event_created
end
end
@@ -89,7 +89,7 @@ RSpec.shared_examples 'a blob replicator' do
end
describe '#model' do
- let(:invoke_model) { replicator.send(:model) }
+ let(:invoke_model) { replicator.class.model }
it 'is implemented' do
expect do
diff --git a/spec/support/shared_examples/models/concerns/has_repository_shared_examples.rb b/spec/support/shared_examples/models/concerns/has_repository_shared_examples.rb
index 41de499f590..30c8c7d0fe5 100644
--- a/spec/support/shared_examples/models/concerns/has_repository_shared_examples.rb
+++ b/spec/support/shared_examples/models/concerns/has_repository_shared_examples.rb
@@ -5,6 +5,7 @@ RSpec.shared_examples 'model with repository' do
let(:stubbed_container) { raise NotImplementedError }
let(:expected_full_path) { raise NotImplementedError }
let(:expected_web_url_path) { expected_full_path }
+ let(:expected_repo_url_path) { expected_full_path }
describe '#commits_by' do
let(:commits) { container.repository.commits('HEAD', limit: 3).commits }
@@ -53,19 +54,19 @@ RSpec.shared_examples 'model with repository' do
describe '#url_to_repo' do
it 'returns the SSH URL to the repository' do
- expect(container.url_to_repo).to eq("#{Gitlab.config.gitlab_shell.ssh_path_prefix}#{expected_web_url_path}.git")
+ expect(container.url_to_repo).to eq(container.ssh_url_to_repo)
end
end
describe '#ssh_url_to_repo' do
it 'returns the SSH URL to the repository' do
- expect(container.ssh_url_to_repo).to eq(container.url_to_repo)
+ expect(container.ssh_url_to_repo).to eq("#{Gitlab.config.gitlab_shell.ssh_path_prefix}#{expected_repo_url_path}.git")
end
end
describe '#http_url_to_repo' do
it 'returns the HTTP URL to the repository' do
- expect(container.http_url_to_repo).to eq("#{Gitlab.config.gitlab.url}/#{expected_web_url_path}.git")
+ expect(container.http_url_to_repo).to eq("#{Gitlab.config.gitlab.url}/#{expected_repo_url_path}.git")
end
end
@@ -95,12 +96,8 @@ RSpec.shared_examples 'model with repository' do
end
context 'when the repo exists' do
- it { expect(container.empty_repo?).to be(false) }
-
- it 'returns true when repository is empty' do
- allow(container.repository).to receive(:empty?).and_return(true)
-
- expect(container.empty_repo?).to be(true)
+ it 'returns the empty state of the repository' do
+ expect(container.empty_repo?).to be(container.repository.empty?)
end
end
end
@@ -146,15 +143,14 @@ RSpec.shared_examples 'model with repository' do
end
it 'picks storage from ApplicationSetting' do
- expect_next_instance_of(ApplicationSetting) do |instance|
- expect(instance).to receive(:pick_repository_storage).and_return('picked')
- end
+ expect(Gitlab::CurrentSettings).to receive(:pick_repository_storage).and_return('picked')
expect(subject).to eq('picked')
end
it 'picks from the latest available storage', :request_store do
stub_env('IN_MEMORY_APPLICATION_SETTINGS', 'false')
+ Gitlab::CurrentSettings.expire_current_application_settings
Gitlab::CurrentSettings.current_application_settings
settings = ApplicationSetting.last
diff --git a/spec/support/shared_examples/models/concerns/has_wiki_shared_examples.rb b/spec/support/shared_examples/models/concerns/has_wiki_shared_examples.rb
new file mode 100644
index 00000000000..0357b7462fb
--- /dev/null
+++ b/spec/support/shared_examples/models/concerns/has_wiki_shared_examples.rb
@@ -0,0 +1,79 @@
+# frozen_string_literal: true
+
+RSpec.shared_examples 'model with wiki' do
+ describe '#create_wiki' do
+ it 'returns true if the wiki repository already exists' do
+ expect(container.wiki_repository_exists?).to be(true)
+ expect(container.create_wiki).to be(true)
+ end
+
+ it 'returns true if the wiki repository was created' do
+ expect(container_without_wiki.wiki_repository_exists?).to be(false)
+ expect(container_without_wiki.create_wiki).to be(true)
+ expect(container_without_wiki.wiki_repository_exists?).to be(true)
+ end
+
+ context 'when the repository cannot be created' do
+ before do
+ expect(container.wiki).to receive(:wiki) { raise Wiki::CouldNotCreateWikiError }
+ end
+
+ it 'returns false and adds a validation error' do
+ expect(container.create_wiki).to be(false)
+ expect(container.errors[:base]).to contain_exactly('Failed to create wiki')
+ end
+ end
+ end
+
+ describe '#wiki_repository_exists?' do
+ it 'returns true when the wiki repository exists' do
+ expect(container.wiki_repository_exists?).to eq(true)
+ end
+
+ it 'returns false when the wiki repository does not exist' do
+ expect(container_without_wiki.wiki_repository_exists?).to eq(false)
+ end
+ end
+
+ describe 'wiki path conflict' do
+ context 'when the new path has been used by the wiki of other Project' do
+ it 'has an error on the name attribute' do
+ create(:project, namespace: container.parent, path: 'existing')
+ container.path = 'existing.wiki'
+
+ expect(container).not_to be_valid
+ expect(container.errors[:name].first).to eq(_('has already been taken'))
+ end
+ end
+
+ context 'when the new wiki path has been used by the path of other Project' do
+ it 'has an error on the name attribute' do
+ create(:project, namespace: container.parent, path: 'existing.wiki')
+ container.path = 'existing'
+
+ expect(container).not_to be_valid
+ expect(container.errors[:name].first).to eq(_('has already been taken'))
+ end
+ end
+
+ context 'when the new path has been used by the wiki of other Group' do
+ it 'has an error on the name attribute' do
+ create(:group, parent: container.parent, path: 'existing')
+ container.path = 'existing.wiki'
+
+ expect(container).not_to be_valid
+ expect(container.errors[:name].first).to eq(_('has already been taken'))
+ end
+ end
+
+ context 'when the new wiki path has been used by the path of other Group' do
+ it 'has an error on the name attribute' do
+ create(:group, parent: container.parent, path: 'existing.wiki')
+ container.path = 'existing'
+
+ expect(container).not_to be_valid
+ expect(container.errors[:name].first).to eq(_('has already been taken'))
+ end
+ end
+ end
+end
diff --git a/spec/support/shared_examples/models/concerns/limitable_shared_examples.rb b/spec/support/shared_examples/models/concerns/limitable_shared_examples.rb
new file mode 100644
index 00000000000..4bcea36fd42
--- /dev/null
+++ b/spec/support/shared_examples/models/concerns/limitable_shared_examples.rb
@@ -0,0 +1,36 @@
+# frozen_string_literal: true
+
+RSpec.shared_examples 'includes Limitable concern' do
+ describe 'validations' do
+ let(:plan_limits) { create(:plan_limits, :default_plan) }
+
+ it { is_expected.to be_a(Limitable) }
+
+ context 'without plan limits configured' do
+ it 'can create new models' do
+ expect { subject.save }.to change { described_class.count }
+ end
+ end
+
+ context 'with plan limits configured' do
+ before do
+ plan_limits.update(subject.class.limit_name => 1)
+ end
+
+ it 'can create new models' do
+ expect { subject.save }.to change { described_class.count }
+ end
+
+ context 'with an existing model' do
+ before do
+ subject.dup.save
+ end
+
+ it 'cannot create new models exceding the plan limits' do
+ expect { subject.save }.not_to change { described_class.count }
+ expect(subject.errors[:base]).to contain_exactly("Maximum number of #{subject.class.limit_name.humanize(capitalize: false)} (1) exceeded")
+ end
+ end
+ end
+ end
+end
diff --git a/spec/support/shared_examples/models/concerns/timebox_shared_examples.rb b/spec/support/shared_examples/models/concerns/timebox_shared_examples.rb
new file mode 100644
index 00000000000..32d502af5a2
--- /dev/null
+++ b/spec/support/shared_examples/models/concerns/timebox_shared_examples.rb
@@ -0,0 +1,242 @@
+# frozen_string_literal: true
+
+RSpec.shared_examples 'a timebox' do |timebox_type|
+ let(:project) { create(:project, :public) }
+ let(:group) { create(:group) }
+ let(:timebox) { create(timebox_type, project: project) }
+ let(:issue) { create(:issue, project: project) }
+ let(:user) { create(:user) }
+ let(:timebox_table_name) { timebox_type.to_s.pluralize.to_sym }
+
+ describe 'modules' do
+ context 'with a project' do
+ it_behaves_like 'AtomicInternalId' do
+ let(:internal_id_attribute) { :iid }
+ let(:instance) { build(timebox_type, project: build(:project), group: nil) }
+ let(:scope) { :project }
+ let(:scope_attrs) { { project: instance.project } }
+ let(:usage) { timebox_table_name }
+ end
+ end
+
+ context 'with a group' do
+ it_behaves_like 'AtomicInternalId' do
+ let(:internal_id_attribute) { :iid }
+ let(:instance) { build(timebox_type, project: nil, group: build(:group)) }
+ let(:scope) { :group }
+ let(:scope_attrs) { { namespace: instance.group } }
+ let(:usage) { timebox_table_name }
+ end
+ end
+ end
+
+ describe "Validation" do
+ before do
+ allow(subject).to receive(:set_iid).and_return(false)
+ end
+
+ describe 'start_date' do
+ it 'adds an error when start_date is greater then due_date' do
+ timebox = build(timebox_type, start_date: Date.tomorrow, due_date: Date.yesterday)
+
+ expect(timebox).not_to be_valid
+ expect(timebox.errors[:due_date]).to include("must be greater than start date")
+ end
+
+ it 'adds an error when start_date is greater than 9999-12-31' do
+ timebox = build(timebox_type, start_date: Date.new(10000, 1, 1))
+
+ expect(timebox).not_to be_valid
+ expect(timebox.errors[:start_date]).to include("date must not be after 9999-12-31")
+ end
+ end
+
+ describe 'due_date' do
+ it 'adds an error when due_date is greater than 9999-12-31' do
+ timebox = build(timebox_type, due_date: Date.new(10000, 1, 1))
+
+ expect(timebox).not_to be_valid
+ expect(timebox.errors[:due_date]).to include("date must not be after 9999-12-31")
+ end
+ end
+
+ describe 'title' do
+ it { is_expected.to validate_presence_of(:title) }
+
+ it 'is invalid if title would be empty after sanitation' do
+ timebox = build(timebox_type, project: project, title: '<img src=x onerror=prompt(1)>')
+
+ expect(timebox).not_to be_valid
+ expect(timebox.errors[:title]).to include("can't be blank")
+ end
+ end
+
+ describe '#timebox_type_check' do
+ it 'is invalid if it has both project_id and group_id' do
+ timebox = build(timebox_type, group: group)
+ timebox.project = project
+
+ expect(timebox).not_to be_valid
+ expect(timebox.errors[:project_id]).to include("#{timebox_type} should belong either to a project or a group.")
+ end
+ end
+
+ describe "#uniqueness_of_title" do
+ context "per project" do
+ it "does not accept the same title in a project twice" do
+ new_timebox = timebox.dup
+ expect(new_timebox).not_to be_valid
+ end
+
+ it "accepts the same title in another project" do
+ project = create(:project)
+ new_timebox = timebox.dup
+ new_timebox.project = project
+
+ expect(new_timebox).to be_valid
+ end
+ end
+
+ context "per group" do
+ let(:timebox) { create(timebox_type, group: group) }
+
+ before do
+ project.update(group: group)
+ end
+
+ it "does not accept the same title in a group twice" do
+ new_timebox = described_class.new(group: group, title: timebox.title)
+
+ expect(new_timebox).not_to be_valid
+ end
+
+ it "does not accept the same title of a child project timebox" do
+ create(timebox_type, project: group.projects.first)
+
+ new_timebox = described_class.new(group: group, title: timebox.title)
+
+ expect(new_timebox).not_to be_valid
+ end
+ end
+ end
+ end
+
+ describe "Associations" do
+ it { is_expected.to belong_to(:project) }
+ it { is_expected.to belong_to(:group) }
+ it { is_expected.to have_many(:issues) }
+ it { is_expected.to have_many(:merge_requests) }
+ it { is_expected.to have_many(:labels) }
+ end
+
+ describe '#timebox_name' do
+ it 'returns the name of the model' do
+ expect(timebox.timebox_name).to eq(timebox_type.to_s)
+ end
+ end
+
+ describe '#project_timebox?' do
+ context 'when project_id is present' do
+ it 'returns true' do
+ expect(timebox.project_timebox?).to be_truthy
+ end
+ end
+
+ context 'when project_id is not present' do
+ let(:timebox) { build(timebox_type, group: group) }
+
+ it 'returns false' do
+ expect(timebox.project_timebox?).to be_falsey
+ end
+ end
+ end
+
+ describe '#group_timebox?' do
+ context 'when group_id is present' do
+ let(:timebox) { build(timebox_type, group: group) }
+
+ it 'returns true' do
+ expect(timebox.group_timebox?).to be_truthy
+ end
+ end
+
+ context 'when group_id is not present' do
+ it 'returns false' do
+ expect(timebox.group_timebox?).to be_falsey
+ end
+ end
+ end
+
+ describe '#safe_title' do
+ let(:timebox) { create(timebox_type, title: "<b>foo & bar -> 2.2</b>") }
+
+ it 'normalizes the title for use as a slug' do
+ expect(timebox.safe_title).to eq('foo-bar-22')
+ end
+ end
+
+ describe '#resource_parent' do
+ context 'when group is present' do
+ let(:timebox) { build(timebox_type, group: group) }
+
+ it 'returns the group' do
+ expect(timebox.resource_parent).to eq(group)
+ end
+ end
+
+ context 'when project is present' do
+ it 'returns the project' do
+ expect(timebox.resource_parent).to eq(project)
+ end
+ end
+ end
+
+ describe "#title" do
+ let(:timebox) { create(timebox_type, title: "<b>foo & bar -> 2.2</b>") }
+
+ it "sanitizes title" do
+ expect(timebox.title).to eq("foo & bar -> 2.2")
+ end
+ end
+
+ describe '#merge_requests_enabled?' do
+ context "per project" do
+ it "is true for projects with MRs enabled" do
+ project = create(:project, :merge_requests_enabled)
+ timebox = create(timebox_type, project: project)
+
+ expect(timebox.merge_requests_enabled?).to be_truthy
+ end
+
+ it "is false for projects with MRs disabled" do
+ project = create(:project, :repository_enabled, :merge_requests_disabled)
+ timebox = create(timebox_type, project: project)
+
+ expect(timebox.merge_requests_enabled?).to be_falsey
+ end
+
+ it "is false for projects with repository disabled" do
+ project = create(:project, :repository_disabled)
+ timebox = create(timebox_type, project: project)
+
+ expect(timebox.merge_requests_enabled?).to be_falsey
+ end
+ end
+
+ context "per group" do
+ let(:timebox) { create(timebox_type, group: group) }
+
+ it "is always true for groups, for performance reasons" do
+ expect(timebox.merge_requests_enabled?).to be_truthy
+ end
+ end
+ end
+
+ describe '#to_ability_name' do
+ it 'returns timebox' do
+ timebox = build(timebox_type)
+
+ expect(timebox.to_ability_name).to eq(timebox_type.to_s)
+ end
+ end
+end
diff --git a/spec/support/shared_examples/models/diff_positionable_note_shared_examples.rb b/spec/support/shared_examples/models/diff_positionable_note_shared_examples.rb
index aa8979603b6..b0cdc77a378 100644
--- a/spec/support/shared_examples/models/diff_positionable_note_shared_examples.rb
+++ b/spec/support/shared_examples/models/diff_positionable_note_shared_examples.rb
@@ -49,5 +49,29 @@ RSpec.shared_examples 'a valid diff positionable note' do |factory_on_commit|
expect(subject.errors).to have_key(:commit_id)
end
end
+
+ %i(original_position position change_position).each do |method|
+ describe "#{method}=" do
+ it "doesn't accept non-hash JSON passed as a string" do
+ subject.send(:"#{method}=", "true")
+ expect(subject.attributes_before_type_cast[method.to_s]).to be(nil)
+ end
+
+ it "does accept a position hash as a string" do
+ subject.send(:"#{method}=", position.to_json)
+ expect(subject.position).to eq(position)
+ end
+
+ it "doesn't accept an array" do
+ subject.send(:"#{method}=", ["test"])
+ expect(subject.attributes_before_type_cast[method.to_s]).to be(nil)
+ end
+
+ it "does accept a hash" do
+ subject.send(:"#{method}=", position.to_h)
+ expect(subject.position).to eq(position)
+ end
+ end
+ end
end
end
diff --git a/spec/support/shared_examples/models/email_format_shared_examples.rb b/spec/support/shared_examples/models/email_format_shared_examples.rb
index 6797836e383..a8115e440a4 100644
--- a/spec/support/shared_examples/models/email_format_shared_examples.rb
+++ b/spec/support/shared_examples/models/email_format_shared_examples.rb
@@ -44,3 +44,44 @@ RSpec.shared_examples 'an object with email-formated attributes' do |*attributes
end
end
end
+
+RSpec.shared_examples 'an object with RFC3696 compliant email-formated attributes' do |*attributes|
+ attributes.each do |attribute|
+ describe "specifically its :#{attribute} attribute" do
+ %w[
+ info@example.com
+ info+test@example.com
+ o'reilly@example.com
+ ].each do |valid_email|
+ context "with a value of '#{valid_email}'" do
+ let(:email_value) { valid_email }
+
+ it 'is valid' do
+ subject.send("#{attribute}=", valid_email)
+
+ expect(subject).to be_valid
+ end
+ end
+ end
+
+ %w[
+ foobar
+ test@test@example.com
+ test.test.@example.com
+ .test.test@example.com
+ mailto:test@example.com
+ lol!'+=?><#$%^&*()@gmail.com
+ ].each do |invalid_email|
+ context "with a value of '#{invalid_email}'" do
+ let(:email_value) { invalid_email }
+
+ it 'is invalid' do
+ subject.send("#{attribute}=", invalid_email)
+
+ expect(subject).to be_invalid
+ end
+ end
+ end
+ end
+ end
+end
diff --git a/spec/support/shared_examples/models/issuable_hook_data_shared_examples.rb b/spec/support/shared_examples/models/issuable_hook_data_shared_examples.rb
index ecf1640ef5d..21ab9b06c33 100644
--- a/spec/support/shared_examples/models/issuable_hook_data_shared_examples.rb
+++ b/spec/support/shared_examples/models/issuable_hook_data_shared_examples.rb
@@ -7,6 +7,7 @@ RSpec.shared_examples 'issuable hook data' do |kind|
include_examples 'project hook data' do
let(:project) { builder.issuable.project }
end
+
include_examples 'deprecated repository hook data'
context "with a #{kind}" do
diff --git a/spec/support/shared_examples/models/mentionable_shared_examples.rb b/spec/support/shared_examples/models/mentionable_shared_examples.rb
index ba6aa4e1d89..d3e9035393f 100644
--- a/spec/support/shared_examples/models/mentionable_shared_examples.rb
+++ b/spec/support/shared_examples/models/mentionable_shared_examples.rb
@@ -210,6 +210,10 @@ RSpec.shared_examples 'mentions in description' do |mentionable_type|
it 'stores no mentions' do
expect(mentionable.user_mentions.count).to eq 0
end
+
+ it 'renders description_html correctly' do
+ expect(mentionable.description_html).to include("<a href=\"/#{user.username}\" data-user=\"#{user.id}\"")
+ end
end
end
diff --git a/spec/support/shared_examples/models/slack_mattermost_notifications_shared_examples.rb b/spec/support/shared_examples/models/slack_mattermost_notifications_shared_examples.rb
index 24ff57c8517..a5228c43f6f 100644
--- a/spec/support/shared_examples/models/slack_mattermost_notifications_shared_examples.rb
+++ b/spec/support/shared_examples/models/slack_mattermost_notifications_shared_examples.rb
@@ -112,7 +112,7 @@ RSpec.shared_examples 'slack or mattermost notifications' do |service_name|
message: "user created page: Awesome wiki_page"
}
- @wiki_page = create(:wiki_page, wiki: project.wiki, attrs: opts)
+ @wiki_page = create(:wiki_page, wiki: project.wiki, **opts)
@wiki_page_sample_data = Gitlab::DataBuilder::WikiPage.build(@wiki_page, user, 'create')
end
diff --git a/spec/support/shared_examples/models/wiki_shared_examples.rb b/spec/support/shared_examples/models/wiki_shared_examples.rb
new file mode 100644
index 00000000000..84569e95e11
--- /dev/null
+++ b/spec/support/shared_examples/models/wiki_shared_examples.rb
@@ -0,0 +1,423 @@
+# frozen_string_literal: true
+
+RSpec.shared_examples 'wiki model' do
+ let_it_be(:user) { create(:user, :commit_email) }
+ let(:wiki_container) { raise NotImplementedError }
+ let(:wiki_container_without_repo) { raise NotImplementedError }
+ let(:wiki) { described_class.new(wiki_container, user) }
+ let(:commit) { subject.repository.head_commit }
+
+ subject { wiki }
+
+ it_behaves_like 'model with repository' do
+ let(:container) { wiki }
+ let(:stubbed_container) { described_class.new(wiki_container_without_repo, user) }
+ let(:expected_full_path) { "#{container.container.full_path}.wiki" }
+ let(:expected_web_url_path) { "#{container.container.web_url(only_path: true).sub(%r{^/}, '')}/-/wikis/home" }
+ end
+
+ describe '#repository' do
+ it 'returns a wiki repository' do
+ expect(subject.repository.repo_type).to be_wiki
+ end
+ end
+
+ describe '#full_path' do
+ it 'returns the container path with the .wiki extension' do
+ expect(subject.full_path).to eq(wiki_container.full_path + '.wiki')
+ end
+ end
+
+ describe '#wiki_base_path' do
+ it 'returns the wiki base path' do
+ expect(subject.wiki_base_path).to eq("#{wiki_container.web_url(only_path: true)}/-/wikis")
+ end
+ end
+
+ describe '#wiki' do
+ it 'contains a Gitlab::Git::Wiki instance' do
+ expect(subject.wiki).to be_a Gitlab::Git::Wiki
+ end
+
+ it 'creates a new wiki repo if one does not yet exist' do
+ expect(subject.create_page('index', 'test content')).to be_truthy
+ end
+
+ it 'creates a new wiki repo with a default commit message' do
+ expect(subject.create_page('index', 'test content', :markdown, '')).to be_truthy
+
+ page = subject.find_page('index')
+
+ expect(page.last_version.message).to eq("#{user.username} created page: index")
+ end
+
+ context 'when the repository cannot be created' do
+ let(:wiki_container) { wiki_container_without_repo }
+
+ before do
+ expect(subject.repository).to receive(:create_if_not_exists) { false }
+ end
+
+ it 'raises CouldNotCreateWikiError' do
+ expect { subject.wiki }.to raise_exception(Wiki::CouldNotCreateWikiError)
+ end
+ end
+ end
+
+ describe '#empty?' do
+ context 'when the wiki repository is empty' do
+ it 'returns true' do
+ expect(subject.empty?).to be(true)
+ end
+ end
+
+ context 'when the wiki has pages' do
+ before do
+ subject.create_page('index', 'This is an awesome new Gollum Wiki')
+ subject.create_page('another-page', 'This is another page')
+ end
+
+ describe '#empty?' do
+ it 'returns false' do
+ expect(subject.empty?).to be(false)
+ end
+
+ it 'only instantiates a Wiki page once' do
+ expect(WikiPage).to receive(:new).once.and_call_original
+
+ subject.empty?
+ end
+ end
+ end
+ end
+
+ describe '#list_pages' do
+ let(:wiki_pages) { subject.list_pages }
+
+ before do
+ subject.create_page('index', 'This is an index')
+ subject.create_page('index2', 'This is an index2')
+ subject.create_page('an index3', 'This is an index3')
+ end
+
+ it 'returns an array of WikiPage instances' do
+ expect(wiki_pages).to be_present
+ expect(wiki_pages).to all(be_a(WikiPage))
+ end
+
+ it 'does not load WikiPage content by default' do
+ wiki_pages.each do |page|
+ expect(page.content).to be_empty
+ end
+ end
+
+ it 'returns all pages by default' do
+ expect(wiki_pages.count).to eq(3)
+ end
+
+ context 'with limit option' do
+ it 'returns limited set of pages' do
+ expect(subject.list_pages(limit: 1).count).to eq(1)
+ end
+ end
+
+ context 'with sorting options' do
+ it 'returns pages sorted by title by default' do
+ pages = ['an index3', 'index', 'index2']
+
+ expect(subject.list_pages.map(&:title)).to eq(pages)
+ expect(subject.list_pages(direction: 'desc').map(&:title)).to eq(pages.reverse)
+ end
+
+ it 'returns pages sorted by created_at' do
+ pages = ['index', 'index2', 'an index3']
+
+ expect(subject.list_pages(sort: 'created_at').map(&:title)).to eq(pages)
+ expect(subject.list_pages(sort: 'created_at', direction: 'desc').map(&:title)).to eq(pages.reverse)
+ end
+ end
+
+ context 'with load_content option' do
+ let(:pages) { subject.list_pages(load_content: true) }
+
+ it 'loads WikiPage content' do
+ expect(pages.first.content).to eq('This is an index3')
+ expect(pages.second.content).to eq('This is an index')
+ expect(pages.third.content).to eq('This is an index2')
+ end
+ end
+ end
+
+ describe '#sidebar_entries' do
+ before do
+ (1..5).each { |i| create(:wiki_page, wiki: wiki, title: "my page #{i}") }
+ (6..10).each { |i| create(:wiki_page, wiki: wiki, title: "parent/my page #{i}") }
+ (11..15).each { |i| create(:wiki_page, wiki: wiki, title: "grandparent/parent/my page #{i}") }
+ end
+
+ def total_pages(entries)
+ entries.sum do |entry|
+ entry.is_a?(WikiDirectory) ? entry.pages.size : 1
+ end
+ end
+
+ context 'when the number of pages does not exceed the limit' do
+ it 'returns all pages grouped by directory and limited is false' do
+ entries, limited = subject.sidebar_entries
+
+ expect(entries.size).to be(7)
+ expect(total_pages(entries)).to be(15)
+ expect(limited).to be(false)
+ end
+ end
+
+ context 'when the number of pages exceeds the limit' do
+ before do
+ create(:wiki_page, wiki: wiki, title: 'my page 16')
+ end
+
+ it 'returns 15 pages grouped by directory and limited is true' do
+ entries, limited = subject.sidebar_entries
+
+ expect(entries.size).to be(8)
+ expect(total_pages(entries)).to be(15)
+ expect(limited).to be(true)
+ end
+ end
+ end
+
+ describe '#find_page' do
+ before do
+ subject.create_page('index page', 'This is an awesome Gollum Wiki')
+ end
+
+ it 'returns the latest version of the page if it exists' do
+ page = subject.find_page('index page')
+
+ expect(page.title).to eq('index page')
+ end
+
+ it 'returns nil if the page does not exist' do
+ expect(subject.find_page('non-existent')).to eq(nil)
+ end
+
+ it 'can find a page by slug' do
+ page = subject.find_page('index-page')
+
+ expect(page.title).to eq('index page')
+ end
+
+ it 'returns a WikiPage instance' do
+ page = subject.find_page('index page')
+
+ expect(page).to be_a WikiPage
+ end
+
+ context 'pages with multibyte-character title' do
+ before do
+ subject.create_page('autre pagé', "C'est un génial Gollum Wiki")
+ end
+
+ it 'can find a page by slug' do
+ page = subject.find_page('autre pagé')
+
+ expect(page.title).to eq('autre pagé')
+ end
+ end
+
+ context 'pages with invalidly-encoded content' do
+ before do
+ subject.create_page('encoding is fun', "f\xFCr".b)
+ end
+
+ it 'can find the page' do
+ page = subject.find_page('encoding is fun')
+
+ expect(page.content).to eq('fr')
+ end
+ end
+ end
+
+ describe '#find_sidebar' do
+ before do
+ subject.create_page(described_class::SIDEBAR, 'This is an awesome Sidebar')
+ end
+
+ it 'finds the page defined as _sidebar' do
+ page = subject.find_sidebar
+
+ expect(page.content).to eq('This is an awesome Sidebar')
+ end
+ end
+
+ describe '#find_file' do
+ let(:image) { File.open(Rails.root.join('spec', 'fixtures', 'big-image.png')) }
+
+ before do
+ subject.wiki # Make sure the wiki repo exists
+
+ subject.repository.create_file(user, 'image.png', image, branch_name: subject.default_branch, message: 'add image')
+ end
+
+ it 'returns the latest version of the file if it exists' do
+ file = subject.find_file('image.png')
+
+ expect(file.mime_type).to eq('image/png')
+ end
+
+ it 'returns nil if the page does not exist' do
+ expect(subject.find_file('non-existent')).to eq(nil)
+ end
+
+ it 'returns a Gitlab::Git::WikiFile instance' do
+ file = subject.find_file('image.png')
+
+ expect(file).to be_a Gitlab::Git::WikiFile
+ end
+
+ it 'returns the whole file' do
+ file = subject.find_file('image.png')
+ image.rewind
+
+ expect(file.raw_data.b).to eq(image.read.b)
+ end
+ end
+
+ describe '#create_page' do
+ it 'creates a new wiki page' do
+ expect(subject.create_page('test page', 'this is content')).not_to eq(false)
+ expect(subject.list_pages.count).to eq(1)
+ end
+
+ it 'returns false when a duplicate page exists' do
+ subject.create_page('test page', 'content')
+
+ expect(subject.create_page('test page', 'content')).to eq(false)
+ end
+
+ it 'stores an error message when a duplicate page exists' do
+ 2.times { subject.create_page('test page', 'content') }
+
+ expect(subject.error_message).to match(/Duplicate page:/)
+ end
+
+ it 'sets the correct commit message' do
+ subject.create_page('test page', 'some content', :markdown, 'commit message')
+
+ expect(subject.list_pages.first.page.version.message).to eq('commit message')
+ end
+
+ it 'sets the correct commit email' do
+ subject.create_page('test page', 'content')
+
+ expect(user.commit_email).not_to eq(user.email)
+ expect(commit.author_email).to eq(user.commit_email)
+ expect(commit.committer_email).to eq(user.commit_email)
+ end
+
+ it 'updates container activity' do
+ expect(subject).to receive(:update_container_activity)
+
+ subject.create_page('Test Page', 'This is content')
+ end
+ end
+
+ describe '#update_page' do
+ let(:page) { create(:wiki_page, wiki: subject, title: 'update-page') }
+
+ def update_page
+ subject.update_page(
+ page.page,
+ content: 'some other content',
+ format: :markdown,
+ message: 'updated page'
+ )
+ end
+
+ it 'updates the content of the page' do
+ update_page
+ page = subject.find_page('update-page')
+
+ expect(page.raw_content).to eq('some other content')
+ end
+
+ it 'sets the correct commit message' do
+ update_page
+ page = subject.find_page('update-page')
+
+ expect(page.version.message).to eq('updated page')
+ end
+
+ it 'sets the correct commit email' do
+ update_page
+
+ expect(user.commit_email).not_to eq(user.email)
+ expect(commit.author_email).to eq(user.commit_email)
+ expect(commit.committer_email).to eq(user.commit_email)
+ end
+
+ it 'updates container activity' do
+ page
+
+ expect(subject).to receive(:update_container_activity)
+
+ update_page
+ end
+ end
+
+ describe '#delete_page' do
+ let(:page) { create(:wiki_page, wiki: wiki) }
+
+ it 'deletes the page' do
+ subject.delete_page(page)
+
+ expect(subject.list_pages.count).to eq(0)
+ end
+
+ it 'sets the correct commit email' do
+ subject.delete_page(page)
+
+ expect(user.commit_email).not_to eq(user.email)
+ expect(commit.author_email).to eq(user.commit_email)
+ expect(commit.committer_email).to eq(user.commit_email)
+ end
+
+ it 'updates container activity' do
+ page
+
+ expect(subject).to receive(:update_container_activity)
+
+ subject.delete_page(page)
+ end
+ end
+
+ describe '#ensure_repository' do
+ context 'if the repository exists' do
+ it 'does not create the repository' do
+ expect(subject.repository.exists?).to eq(true)
+ expect(subject.repository.raw).not_to receive(:create_repository)
+
+ subject.ensure_repository
+ end
+ end
+
+ context 'if the repository does not exist' do
+ let(:wiki_container) { wiki_container_without_repo }
+
+ it 'creates the repository' do
+ expect(subject.repository.exists?).to eq(false)
+
+ subject.ensure_repository
+
+ expect(subject.repository.exists?).to eq(true)
+ end
+ end
+ end
+
+ describe '#hook_attrs' do
+ it 'returns a hash with values' do
+ expect(subject.hook_attrs).to be_a Hash
+ expect(subject.hook_attrs.keys).to contain_exactly(:web_url, :git_ssh_url, :git_http_url, :path_with_namespace, :default_branch)
+ end
+ end
+end
diff --git a/spec/support/shared_examples/policies/project_policy_shared_examples.rb b/spec/support/shared_examples/policies/project_policy_shared_examples.rb
index 1831fc10628..4dd0152e3d1 100644
--- a/spec/support/shared_examples/policies/project_policy_shared_examples.rb
+++ b/spec/support/shared_examples/policies/project_policy_shared_examples.rb
@@ -212,8 +212,8 @@ RSpec.shared_examples 'project policies as owner' do
end
end
-RSpec.shared_examples 'project policies as admin' do
- context 'abilities for non-public projects' do
+RSpec.shared_examples 'project policies as admin with admin mode' do
+ context 'abilities for non-public projects', :enable_admin_mode do
let(:project) { create(:project, namespace: owner.namespace) }
subject { described_class.new(admin, project) }
@@ -232,3 +232,13 @@ RSpec.shared_examples 'project policies as admin' do
end
end
end
+
+RSpec.shared_examples 'project policies as admin without admin mode' do
+ context 'abilities for non-public projects' do
+ let(:project) { create(:project, namespace: owner.namespace) }
+
+ subject { described_class.new(admin, project) }
+
+ it { is_expected.to be_banned }
+ end
+end
diff --git a/spec/support/shared_examples/policies/wiki_policies_shared_examples.rb b/spec/support/shared_examples/policies/wiki_policies_shared_examples.rb
index b91500ffd9c..58822f4309b 100644
--- a/spec/support/shared_examples/policies/wiki_policies_shared_examples.rb
+++ b/spec/support/shared_examples/policies/wiki_policies_shared_examples.rb
@@ -1,152 +1,116 @@
# frozen_string_literal: true
RSpec.shared_examples 'model with wiki policies' do
- let(:container) { raise NotImplementedError }
- let(:permissions) { %i(read_wiki create_wiki update_wiki admin_wiki download_wiki_code) }
-
- # TODO: Remove this helper once we implement group features
- # https://gitlab.com/gitlab-org/gitlab/-/issues/208412
- def set_access_level(access_level)
- raise NotImplementedError
- end
-
- subject { described_class.new(owner, container) }
-
- context 'when the feature is disabled' do
- before do
- set_access_level(ProjectFeature::DISABLED)
- end
+ include ProjectHelpers
+ include AdminModeHelper
- it 'does not include the wiki permissions' do
- expect_disallowed(*permissions)
- end
+ let(:container) { raise NotImplementedError }
+ let(:user) { raise NotImplementedError }
- context 'when there is an external wiki' do
- it 'does not include the wiki permissions' do
- allow(container).to receive(:has_external_wiki?).and_return(true)
+ subject { described_class.new(user, container) }
- expect_disallowed(*permissions)
- end
+ let_it_be(:wiki_permissions) do
+ {}.tap do |permissions|
+ permissions[:guest] = %i[read_wiki]
+ permissions[:reporter] = permissions[:guest] + %i[download_wiki_code]
+ permissions[:developer] = permissions[:reporter] + %i[create_wiki]
+ permissions[:maintainer] = permissions[:developer] + %i[admin_wiki]
+ permissions[:all] = permissions[:maintainer]
end
end
- describe 'read_wiki' do
- subject { described_class.new(user, container) }
-
- member_roles = %i[guest developer]
- stranger_roles = %i[anonymous non_member]
-
- user_roles = stranger_roles + member_roles
+ using RSpec::Parameterized::TableSyntax
+
+ where(:container_level, :access_level, :membership, :access) do
+ :public | :enabled | :admin | :all
+ :public | :enabled | :maintainer | :maintainer
+ :public | :enabled | :developer | :developer
+ :public | :enabled | :reporter | :reporter
+ :public | :enabled | :guest | :guest
+ :public | :enabled | :non_member | :guest
+ :public | :enabled | :anonymous | :guest
+
+ :public | :private | :admin | :all
+ :public | :private | :maintainer | :maintainer
+ :public | :private | :developer | :developer
+ :public | :private | :reporter | :reporter
+ :public | :private | :guest | :guest
+ :public | :private | :non_member | nil
+ :public | :private | :anonymous | nil
+
+ :public | :disabled | :admin | nil
+ :public | :disabled | :maintainer | nil
+ :public | :disabled | :developer | nil
+ :public | :disabled | :reporter | nil
+ :public | :disabled | :guest | nil
+ :public | :disabled | :non_member | nil
+ :public | :disabled | :anonymous | nil
+
+ :internal | :enabled | :admin | :all
+ :internal | :enabled | :maintainer | :maintainer
+ :internal | :enabled | :developer | :developer
+ :internal | :enabled | :reporter | :reporter
+ :internal | :enabled | :guest | :guest
+ :internal | :enabled | :non_member | :guest
+ :internal | :enabled | :anonymous | nil
+
+ :internal | :private | :admin | :all
+ :internal | :private | :maintainer | :maintainer
+ :internal | :private | :developer | :developer
+ :internal | :private | :reporter | :reporter
+ :internal | :private | :guest | :guest
+ :internal | :private | :non_member | nil
+ :internal | :private | :anonymous | nil
+
+ :internal | :disabled | :admin | nil
+ :internal | :disabled | :maintainer | nil
+ :internal | :disabled | :developer | nil
+ :internal | :disabled | :reporter | nil
+ :internal | :disabled | :guest | nil
+ :internal | :disabled | :non_member | nil
+ :internal | :disabled | :anonymous | nil
+
+ :private | :private | :admin | :all
+ :private | :private | :maintainer | :maintainer
+ :private | :private | :developer | :developer
+ :private | :private | :reporter | :reporter
+ :private | :private | :guest | :guest
+ :private | :private | :non_member | nil
+ :private | :private | :anonymous | nil
+
+ :private | :disabled | :admin | nil
+ :private | :disabled | :maintainer | nil
+ :private | :disabled | :developer | nil
+ :private | :disabled | :reporter | nil
+ :private | :disabled | :guest | nil
+ :private | :disabled | :non_member | nil
+ :private | :disabled | :anonymous | nil
+ end
- # When a user is anonymous, their `current_user == nil`
- let(:user) { create(:user) unless user_role == :anonymous }
+ with_them do
+ let(:user) { create_user_from_membership(container, membership) }
+ let(:allowed_permissions) { wiki_permissions[access].dup || [] }
+ let(:disallowed_permissions) { wiki_permissions[:all] - allowed_permissions }
before do
- container.visibility = container_visibility
- set_access_level(wiki_access_level)
- container.add_user(user, user_role) if member_roles.include?(user_role)
- end
-
- title = ->(container_visibility, wiki_access_level, user_role) do
- [
- "container is #{Gitlab::VisibilityLevel.level_name container_visibility}",
- "wiki is #{ProjectFeature.str_from_access_level wiki_access_level}",
- "user is #{user_role}"
- ].join(', ')
- end
-
- describe 'Situations where :read_wiki is always false' do
- where(case_names: title,
- container_visibility: Gitlab::VisibilityLevel.options.values,
- wiki_access_level: [ProjectFeature::DISABLED],
- user_role: user_roles)
-
- with_them do
- it { is_expected.to be_disallowed(:read_wiki) }
- end
- end
-
- describe 'Situations where :read_wiki is always true' do
- where(case_names: title,
- container_visibility: [Gitlab::VisibilityLevel::PUBLIC],
- wiki_access_level: [ProjectFeature::ENABLED],
- user_role: user_roles)
+ container.visibility = container_level.to_s
+ set_access_level(ProjectFeature.access_level_from_str(access_level.to_s))
+ enable_admin_mode!(user) if user&.admin?
- with_them do
- it { is_expected.to be_allowed(:read_wiki) }
+ if allowed_permissions.any? && [container_level, access_level, membership] != [:private, :private, :guest]
+ allowed_permissions << :download_wiki_code
end
end
- describe 'Situations where :read_wiki requires membership' do
- context 'the wiki is private, and the user is a member' do
- where(case_names: title,
- container_visibility: [Gitlab::VisibilityLevel::PUBLIC,
- Gitlab::VisibilityLevel::INTERNAL],
- wiki_access_level: [ProjectFeature::PRIVATE],
- user_role: member_roles)
-
- with_them do
- it { is_expected.to be_allowed(:read_wiki) }
- end
- end
-
- context 'the wiki is private, and the user is not member' do
- where(case_names: title,
- container_visibility: [Gitlab::VisibilityLevel::PUBLIC,
- Gitlab::VisibilityLevel::INTERNAL],
- wiki_access_level: [ProjectFeature::PRIVATE],
- user_role: stranger_roles)
-
- with_them do
- it { is_expected.to be_disallowed(:read_wiki) }
- end
- end
-
- context 'the wiki is enabled, and the user is a member' do
- where(case_names: title,
- container_visibility: [Gitlab::VisibilityLevel::PRIVATE],
- wiki_access_level: [ProjectFeature::ENABLED],
- user_role: member_roles)
-
- with_them do
- it { is_expected.to be_allowed(:read_wiki) }
- end
- end
-
- context 'the wiki is enabled, and the user is not a member' do
- where(case_names: title,
- container_visibility: [Gitlab::VisibilityLevel::PRIVATE],
- wiki_access_level: [ProjectFeature::ENABLED],
- user_role: stranger_roles)
-
- with_them do
- it { is_expected.to be_disallowed(:read_wiki) }
- end
- end
+ it 'allows actions based on membership' do
+ expect_allowed(*allowed_permissions)
+ expect_disallowed(*disallowed_permissions)
end
+ end
- describe 'Situations where :read_wiki prohibits anonymous access' do
- context 'the user is not anonymous' do
- where(case_names: title,
- container_visibility: [Gitlab::VisibilityLevel::INTERNAL],
- wiki_access_level: [ProjectFeature::ENABLED, ProjectFeature::PUBLIC],
- user_role: user_roles.reject { |u| u == :anonymous })
-
- with_them do
- it { is_expected.to be_allowed(:read_wiki) }
- end
- end
-
- context 'the user is anonymous' do
- where(case_names: title,
- container_visibility: [Gitlab::VisibilityLevel::INTERNAL],
- wiki_access_level: [ProjectFeature::ENABLED, ProjectFeature::PUBLIC],
- user_role: %i[anonymous])
-
- with_them do
- it { is_expected.to be_disallowed(:read_wiki) }
- end
- end
- end
+ # TODO: Remove this helper once we implement group features
+ # https://gitlab.com/gitlab-org/gitlab/-/issues/208412
+ def set_access_level(access_level)
+ raise NotImplementedError
end
end
diff --git a/spec/support/shared_examples/quick_actions/issuable/issuable_quick_actions_shared_examples.rb b/spec/support/shared_examples/quick_actions/issuable/issuable_quick_actions_shared_examples.rb
index b03da4471bc..50a8b81b518 100644
--- a/spec/support/shared_examples/quick_actions/issuable/issuable_quick_actions_shared_examples.rb
+++ b/spec/support/shared_examples/quick_actions/issuable/issuable_quick_actions_shared_examples.rb
@@ -18,6 +18,16 @@ RSpec.shared_examples 'issuable quick actions' do
end
end
+ let(:unlabel_expectation) do
+ ->(noteable, can_use_quick_action) {
+ if can_use_quick_action
+ expect(noteable.labels).to be_empty
+ else
+ expect(noteable.labels).not_to be_empty
+ end
+ }
+ end
+
# Quick actions shared by issues and merge requests
let(:issuable_quick_actions) do
[
@@ -136,13 +146,11 @@ RSpec.shared_examples 'issuable quick actions' do
),
QuickAction.new(
action_text: "/unlabel",
- expectation: ->(noteable, can_use_quick_action) {
- if can_use_quick_action
- expect(noteable.labels).to be_empty
- else
- expect(noteable.labels).not_to be_empty
- end
- }
+ expectation: unlabel_expectation
+ ),
+ QuickAction.new(
+ action_text: "/remove_label",
+ expectation: unlabel_expectation
),
QuickAction.new(
action_text: "/award :100:",
diff --git a/spec/support/shared_examples/requests/api/award_emoji_todo_shared_examples.rb b/spec/support/shared_examples/requests/api/award_emoji_todo_shared_examples.rb
index 88ad37d232f..9bfd1e6faa0 100644
--- a/spec/support/shared_examples/requests/api/award_emoji_todo_shared_examples.rb
+++ b/spec/support/shared_examples/requests/api/award_emoji_todo_shared_examples.rb
@@ -25,7 +25,7 @@ RSpec.shared_examples 'creating award emojis marks Todos as done' do
let(:awardable) { create(type) }
let!(:todo) { create(:todo, target: awardable, project: project, user: user) }
- it do
+ specify do
subject
expect(todo.reload.done?).to eq(expectation)
diff --git a/spec/support/shared_examples/requests/api/graphql/group_and_project_boards_query_shared_examples.rb b/spec/support/shared_examples/requests/api/graphql/group_and_project_boards_query_shared_examples.rb
index 90ac60a6fe7..feb3ba46353 100644
--- a/spec/support/shared_examples/requests/api/graphql/group_and_project_boards_query_shared_examples.rb
+++ b/spec/support/shared_examples/requests/api/graphql/group_and_project_boards_query_shared_examples.rb
@@ -80,7 +80,7 @@ RSpec.shared_examples 'group and project boards query' do
cursored_query = query("after: \"#{end_cursor}\"")
post_graphql(cursored_query, current_user: current_user)
- response_data = JSON.parse(response.body)['data'][board_parent_type]['boards']['edges']
+ response_data = Gitlab::Json.parse(response.body)['data'][board_parent_type]['boards']['edges']
expect(grab_names(response_data)).to eq expected_boards.drop(2).first(2).map(&:name)
end
diff --git a/spec/support/shared_examples/requests/api/graphql/mutations/snippets_shared_examples.rb b/spec/support/shared_examples/requests/api/graphql/mutations/snippets_shared_examples.rb
new file mode 100644
index 00000000000..48824a4b0d2
--- /dev/null
+++ b/spec/support/shared_examples/requests/api/graphql/mutations/snippets_shared_examples.rb
@@ -0,0 +1,10 @@
+# frozen_string_literal: true
+
+RSpec.shared_examples 'when the snippet is not found' do
+ let(:snippet_gid) do
+ "gid://gitlab/#{snippet.class.name}/#{non_existing_record_id}"
+ end
+
+ it_behaves_like 'a mutation that returns top-level errors',
+ errors: [Gitlab::Graphql::Authorize::AuthorizeResource::RESOURCE_ACCESS_ERROR]
+end
diff --git a/spec/support/shared_examples/requests/snippet_shared_examples.rb b/spec/support/shared_examples/requests/snippet_shared_examples.rb
index aa7f57ae903..f830f957174 100644
--- a/spec/support/shared_examples/requests/snippet_shared_examples.rb
+++ b/spec/support/shared_examples/requests/snippet_shared_examples.rb
@@ -23,21 +23,53 @@ RSpec.shared_examples 'update with repository actions' do
context 'when the repository does not exist' do
let(:snippet) { snippet_without_repo }
- it 'creates the repository' do
- update_snippet(snippet_id: snippet.id, params: { title: 'foo' })
+ context 'when update attributes does not include file_name or content' do
+ it 'does not create the repository' do
+ update_snippet(snippet_id: snippet.id, params: { title: 'foo' })
- expect(snippet.repository).to exist
+ expect(snippet.repository).not_to exist
+ end
end
- it 'commits the file to the repository' do
- content = 'New Content'
- file_name = 'file_name.rb'
+ context 'when update attributes include file_name or content' do
+ it 'creates the repository' do
+ update_snippet(snippet_id: snippet.id, params: { title: 'foo', file_name: 'foo' })
+
+ expect(snippet.repository).to exist
+ end
+
+ it 'commits the file to the repository' do
+ content = 'New Content'
+ file_name = 'file_name.rb'
+
+ update_snippet(snippet_id: snippet.id, params: { content: content, file_name: file_name })
+
+ blob = snippet.repository.blob_at('master', file_name)
+ expect(blob).not_to be_nil
+ expect(blob.data).to eq content
+ end
+
+ context 'when save fails due to a repository creation error' do
+ let(:content) { 'File content' }
+ let(:file_name) { 'test.md' }
+
+ before do
+ allow_next_instance_of(Snippets::UpdateService) do |instance|
+ allow(instance).to receive(:create_repository_for).with(snippet).and_raise(Snippets::UpdateService::CreateRepositoryError)
+ end
+
+ update_snippet(snippet_id: snippet.id, params: { content: content, file_name: file_name })
+ end
- update_snippet(snippet_id: snippet.id, params: { content: content, file_name: file_name })
+ it 'returns 400' do
+ expect(response).to have_gitlab_http_status(:bad_request)
+ end
- blob = snippet.repository.blob_at('master', file_name)
- expect(blob).not_to be_nil
- expect(blob.data).to eq content
+ it 'does not save the changes to the snippet object' do
+ expect(snippet.content).not_to eq(content)
+ expect(snippet.file_name).not_to eq(file_name)
+ end
+ end
end
end
end
@@ -48,3 +80,21 @@ RSpec.shared_examples 'snippet response without repository URLs' do
expect(json_response).not_to have_key('http_url_to_repo')
end
end
+
+RSpec.shared_examples 'snippet blob content' do
+ it 'returns content from repository' do
+ subject
+
+ expect(response.body).to eq(snippet.blobs.first.data)
+ end
+
+ context 'when snippet repository is empty' do
+ let(:snippet) { snippet_with_empty_repo }
+
+ it 'returns content from database' do
+ subject
+
+ expect(response.body).to eq(snippet.content)
+ end
+ end
+end
diff --git a/spec/support/shared_examples/requires_variables_shared_example.rb b/spec/support/shared_examples/requires_variables_shared_example.rb
new file mode 100644
index 00000000000..2921fccf87a
--- /dev/null
+++ b/spec/support/shared_examples/requires_variables_shared_example.rb
@@ -0,0 +1,13 @@
+# frozen_string_literal: true
+
+RSpec.shared_examples 'requires variables' do
+ it 'shared example requires variables to be set', :aggregate_failures do
+ variables = Array.wrap(required_variables)
+
+ variables.each do |variable_name|
+ expect { send(variable_name) }.not_to(
+ raise_error, "The following variable must be set to use this shared example: #{variable_name}"
+ )
+ end
+ end
+end
diff --git a/spec/support/shared_examples/resource_events.rb b/spec/support/shared_examples/resource_events.rb
index 963453666c9..66f5e760c37 100644
--- a/spec/support/shared_examples/resource_events.rb
+++ b/spec/support/shared_examples/resource_events.rb
@@ -83,6 +83,24 @@ shared_examples 'a resource event for issues' do
expect(events).to be_empty
end
end
+
+ describe '.by_issue_ids_and_created_at_earlier_or_equal_to' do
+ let_it_be(:event1) { create(described_class.name.underscore.to_sym, issue: issue1, created_at: '2020-03-10') }
+ let_it_be(:event2) { create(described_class.name.underscore.to_sym, issue: issue2, created_at: '2020-03-10') }
+ let_it_be(:event3) { create(described_class.name.underscore.to_sym, issue: issue1, created_at: '2020-03-12') }
+
+ it 'returns the expected records for an issue with events' do
+ events = described_class.by_issue_ids_and_created_at_earlier_or_equal_to([issue1.id, issue2.id], '2020-03-11 23:59:59')
+
+ expect(events).to contain_exactly(event1, event2)
+ end
+
+ it 'returns the expected records for an issue with no events' do
+ events = described_class.by_issue_ids_and_created_at_earlier_or_equal_to(issue3, '2020-03-12')
+
+ expect(events).to be_empty
+ end
+ end
end
shared_examples 'a resource event for merge requests' do
diff --git a/spec/support/shared_examples/serializers/diff_file_entity_shared_examples.rb b/spec/support/shared_examples/serializers/diff_file_entity_shared_examples.rb
index b6c4841dbd4..db5c4b45b70 100644
--- a/spec/support/shared_examples/serializers/diff_file_entity_shared_examples.rb
+++ b/spec/support/shared_examples/serializers/diff_file_entity_shared_examples.rb
@@ -60,7 +60,7 @@ RSpec.shared_examples 'diff file entity' do
context 'when the `single_mr_diff_view` feature is disabled' do
before do
- stub_feature_flags(single_mr_diff_view: { enabled: false, thing: project })
+ stub_feature_flags(single_mr_diff_view: false)
end
it 'contains both kinds of diffs' do
diff --git a/spec/support/shared_examples/services/boards/lists_list_service_shared_examples.rb b/spec/support/shared_examples/services/boards/lists_list_service_shared_examples.rb
index 1b7fe626aea..07a6353296d 100644
--- a/spec/support/shared_examples/services/boards/lists_list_service_shared_examples.rb
+++ b/spec/support/shared_examples/services/boards/lists_list_service_shared_examples.rb
@@ -18,6 +18,10 @@ RSpec.shared_examples 'lists list service' do
expect { service.execute(board) }.to change(board.lists, :count).by(1)
end
+ it 'does not create a backlog list when create_default_lists is false' do
+ expect { service.execute(board, create_default_lists: false) }.not_to change(board.lists, :count)
+ end
+
it "returns board's lists" do
expect(service.execute(board)).to eq [board.backlog_list, list, board.closed_list]
end
diff --git a/spec/support/shared_examples/services/measurable_service_shared_examples.rb b/spec/support/shared_examples/services/measurable_service_shared_examples.rb
new file mode 100644
index 00000000000..206c25e49af
--- /dev/null
+++ b/spec/support/shared_examples/services/measurable_service_shared_examples.rb
@@ -0,0 +1,32 @@
+# frozen_string_literal: true
+
+RSpec.shared_examples 'measurable service' do
+ context 'when measurement is enabled' do
+ let!(:measuring) { Gitlab::Utils::Measuring.new(base_log_data) }
+
+ before do
+ stub_feature_flags(feature_flag => true)
+ end
+
+ it 'measure service execution with Gitlab::Utils::Measuring', :aggregate_failures do
+ expect(Gitlab::Utils::Measuring).to receive(:new).with(base_log_data).and_return(measuring)
+ expect(measuring).to receive(:with_measuring).and_call_original
+ end
+ end
+
+ context 'when measurement is disabled' do
+ it 'does not measure service execution' do
+ stub_feature_flags(feature_flag => false)
+
+ expect(Gitlab::Utils::Measuring).not_to receive(:new)
+ end
+ end
+
+ def feature_flag
+ "gitlab_service_measuring_#{described_class_name}"
+ end
+
+ def described_class_name
+ described_class.name.underscore.tr('/', '_')
+ end
+end
diff --git a/spec/support/shared_examples/services/metrics/dashboard_shared_examples.rb b/spec/support/shared_examples/services/metrics/dashboard_shared_examples.rb
index 90fcac0e55c..5dd1badbefc 100644
--- a/spec/support/shared_examples/services/metrics/dashboard_shared_examples.rb
+++ b/spec/support/shared_examples/services/metrics/dashboard_shared_examples.rb
@@ -23,7 +23,7 @@ RSpec.shared_examples 'valid dashboard service response for schema' do
end
RSpec.shared_examples 'valid dashboard service response' do
- let(:dashboard_schema) { JSON.parse(fixture_file('lib/gitlab/metrics/dashboard/schemas/dashboard.json')) }
+ let(:dashboard_schema) { Gitlab::Json.parse(fixture_file('lib/gitlab/metrics/dashboard/schemas/dashboard.json')) }
it_behaves_like 'valid dashboard service response for schema'
end
@@ -38,7 +38,7 @@ RSpec.shared_examples 'caches the unprocessed dashboard for subsequent calls' do
end
RSpec.shared_examples 'valid embedded dashboard service response' do
- let(:dashboard_schema) { JSON.parse(fixture_file('lib/gitlab/metrics/dashboard/schemas/embedded_dashboard.json')) }
+ let(:dashboard_schema) { Gitlab::Json.parse(fixture_file('lib/gitlab/metrics/dashboard/schemas/embedded_dashboard.json')) }
it_behaves_like 'valid dashboard service response for schema'
end
diff --git a/spec/support/shared_examples/services/projects/update_repository_storage_service_shared_examples.rb b/spec/support/shared_examples/services/projects/update_repository_storage_service_shared_examples.rb
index d6166ac8188..0e6ecf49cd0 100644
--- a/spec/support/shared_examples/services/projects/update_repository_storage_service_shared_examples.rb
+++ b/spec/support/shared_examples/services/projects/update_repository_storage_service_shared_examples.rb
@@ -47,9 +47,9 @@ RSpec.shared_examples 'moves repository to another storage' do |repository_type|
old_repository_path = repository.full_path
- result = subject.execute('test_second_storage')
+ result = subject.execute
- expect(result[:status]).to eq(:success)
+ expect(result).to be_success
expect(project).not_to be_repository_read_only
expect(project.repository_storage).to eq('test_second_storage')
expect(gitlab_shell.repository_exists?('default', old_project_repository_path)).to be(false)
@@ -62,7 +62,7 @@ RSpec.shared_examples 'moves repository to another storage' do |repository_type|
end
it 'does not enqueue a GC run' do
- expect { subject.execute('test_second_storage') }
+ expect { subject.execute }
.not_to change(GitGarbageCollectWorker.jobs, :count)
end
end
@@ -75,23 +75,25 @@ RSpec.shared_examples 'moves repository to another storage' do |repository_type|
it 'does not enqueue a GC run if housekeeping is disabled' do
stub_application_setting(housekeeping_enabled: false)
- expect { subject.execute('test_second_storage') }
+ expect { subject.execute }
.not_to change(GitGarbageCollectWorker.jobs, :count)
end
it 'enqueues a GC run' do
- expect { subject.execute('test_second_storage') }
+ expect { subject.execute }
.to change(GitGarbageCollectWorker.jobs, :count).by(1)
end
end
end
context 'when the filesystems are the same' do
+ let(:destination) { project.repository_storage }
+
it 'bails out and does nothing' do
- result = subject.execute(project.repository_storage)
+ result = subject.execute
- expect(result[:status]).to eq(:error)
- expect(result[:message]).to match(/SameFilesystemError/)
+ expect(result).to be_error
+ expect(result.message).to match(/SameFilesystemError/)
end
end
@@ -114,9 +116,9 @@ RSpec.shared_examples 'moves repository to another storage' do |repository_type|
expect(GitlabShellWorker).not_to receive(:perform_async)
- result = subject.execute('test_second_storage')
+ result = subject.execute
- expect(result[:status]).to eq(:error)
+ expect(result).to be_error
expect(project).not_to be_repository_read_only
expect(project.repository_storage).to eq('default')
end
@@ -142,9 +144,9 @@ RSpec.shared_examples 'moves repository to another storage' do |repository_type|
expect(GitlabShellWorker).not_to receive(:perform_async)
- result = subject.execute('test_second_storage')
+ result = subject.execute
- expect(result[:status]).to eq(:error)
+ expect(result).to be_error
expect(project).not_to be_repository_read_only
expect(project.repository_storage).to eq('default')
end
diff --git a/spec/support/shared_examples/services/resource_events/change_milestone_service_shared_examples.rb b/spec/support/shared_examples/services/resource_events/change_milestone_service_shared_examples.rb
index 77f64e5e8f8..c5f84e205cf 100644
--- a/spec/support/shared_examples/services/resource_events/change_milestone_service_shared_examples.rb
+++ b/spec/support/shared_examples/services/resource_events/change_milestone_service_shared_examples.rb
@@ -4,7 +4,7 @@ shared_examples 'a milestone events creator' do
let_it_be(:user) { create(:user) }
let(:created_at_time) { Time.utc(2019, 12, 30) }
- let(:service) { described_class.new(resource, user, created_at: created_at_time) }
+ let(:service) { described_class.new(resource, user, created_at: created_at_time, old_milestone: nil) }
context 'when milestone is present' do
let_it_be(:milestone) { create(:milestone) }
@@ -25,10 +25,13 @@ shared_examples 'a milestone events creator' do
resource.milestone = nil
end
+ let(:old_milestone) { create(:milestone, project: resource.project) }
+ let(:service) { described_class.new(resource, user, created_at: created_at_time, old_milestone: old_milestone) }
+
it 'creates the expected event records' do
expect { service.execute }.to change { ResourceMilestoneEvent.count }.by(1)
- expect_event_record(ResourceMilestoneEvent.last, action: 'remove', milestone: nil, state: 'opened')
+ expect_event_record(ResourceMilestoneEvent.last, action: 'remove', milestone: old_milestone, state: 'opened')
end
end
diff --git a/spec/support/shared_examples/services/snippets_shared_examples.rb b/spec/support/shared_examples/services/snippets_shared_examples.rb
new file mode 100644
index 00000000000..51a4a8b1cd9
--- /dev/null
+++ b/spec/support/shared_examples/services/snippets_shared_examples.rb
@@ -0,0 +1,42 @@
+# frozen_string_literal: true
+
+RSpec.shared_examples 'snippets spam check is performed' do
+ shared_examples 'marked as spam' do
+ it 'marks a snippet as spam' do
+ expect(snippet).to be_spam
+ end
+
+ it 'invalidates the snippet' do
+ expect(snippet).to be_invalid
+ end
+
+ it 'creates a new spam_log' do
+ expect { snippet }
+ .to have_spam_log(title: snippet.title, noteable_type: snippet.class.name)
+ end
+
+ it 'assigns a spam_log to an issue' do
+ expect(snippet.spam_log).to eq(SpamLog.last)
+ end
+ end
+
+ let(:extra_opts) do
+ { visibility_level: Gitlab::VisibilityLevel::PUBLIC, request: double(:request, env: {}) }
+ end
+
+ before do
+ expect_next_instance_of(Spam::AkismetService) do |akismet_service|
+ expect(akismet_service).to receive_messages(spam?: true)
+ end
+ end
+
+ [true, false, nil].each do |allow_possible_spam|
+ context "when allow_possible_spam flag is #{allow_possible_spam.inspect}" do
+ before do
+ stub_feature_flags(allow_possible_spam: allow_possible_spam) unless allow_possible_spam.nil?
+ end
+
+ it_behaves_like 'marked as spam'
+ end
+ end
+end
diff --git a/spec/support/shared_examples/services/wiki_pages/create_service_shared_examples.rb b/spec/support/shared_examples/services/wiki_pages/create_service_shared_examples.rb
new file mode 100644
index 00000000000..71bdd46572f
--- /dev/null
+++ b/spec/support/shared_examples/services/wiki_pages/create_service_shared_examples.rb
@@ -0,0 +1,94 @@
+# frozen_string_literal: true
+
+RSpec.shared_examples 'WikiPages::CreateService#execute' do |container_type|
+ let(:container) { create(container_type, :wiki_repo) }
+ let(:user) { create(:user) }
+ let(:page_title) { 'Title' }
+
+ let(:opts) do
+ {
+ title: page_title,
+ content: 'Content for wiki page',
+ format: 'markdown'
+ }
+ end
+
+ subject(:service) { described_class.new(container: container, current_user: user, params: opts) }
+
+ it 'creates wiki page with valid attributes' do
+ page = service.execute
+
+ expect(page).to be_valid
+ expect(page).to be_persisted
+ expect(page.title).to eq(opts[:title])
+ expect(page.content).to eq(opts[:content])
+ expect(page.format).to eq(opts[:format].to_sym)
+ end
+
+ it 'executes webhooks' do
+ expect(service).to receive(:execute_hooks).once.with(WikiPage)
+
+ service.execute
+ end
+
+ it 'counts wiki page creation' do
+ counter = Gitlab::UsageDataCounters::WikiPageCounter
+
+ expect { service.execute }.to change { counter.read(:create) }.by 1
+ end
+
+ shared_examples 'correct event created' do
+ it 'creates appropriate events' do
+ # TODO: https://gitlab.com/gitlab-org/gitlab/-/issues/216904
+ pending('group wiki support') if container_type == :group
+
+ expect { service.execute }.to change { Event.count }.by 1
+
+ expect(Event.recent.first).to have_attributes(
+ action: Event::CREATED,
+ target: have_attributes(canonical_slug: page_title)
+ )
+ end
+ end
+
+ context 'the new page is at the top level' do
+ let(:page_title) { 'root-level-page' }
+
+ include_examples 'correct event created'
+ end
+
+ context 'the new page is in a subsection' do
+ let(:page_title) { 'subsection/page' }
+
+ include_examples 'correct event created'
+ end
+
+ context 'the feature is disabled' do
+ before do
+ stub_feature_flags(wiki_events: false)
+ end
+
+ it 'does not record the activity' do
+ expect { service.execute }.not_to change(Event, :count)
+ end
+ end
+
+ context 'when the options are bad' do
+ let(:page_title) { '' }
+
+ it 'does not count a creation event' do
+ counter = Gitlab::UsageDataCounters::WikiPageCounter
+
+ expect { service.execute }.not_to change { counter.read(:create) }
+ end
+
+ it 'does not record the activity' do
+ expect { service.execute }.not_to change(Event, :count)
+ end
+
+ it 'reports the error' do
+ expect(service.execute).to be_invalid
+ .and have_attributes(errors: be_present)
+ end
+ end
+end
diff --git a/spec/support/shared_examples/services/wiki_pages/destroy_service_shared_examples.rb b/spec/support/shared_examples/services/wiki_pages/destroy_service_shared_examples.rb
new file mode 100644
index 00000000000..62541eb3da9
--- /dev/null
+++ b/spec/support/shared_examples/services/wiki_pages/destroy_service_shared_examples.rb
@@ -0,0 +1,50 @@
+# frozen_string_literal: true
+
+RSpec.shared_examples 'WikiPages::DestroyService#execute' do |container_type|
+ let(:container) { create(container_type) }
+
+ let(:user) { create(:user) }
+ let(:page) { create(:wiki_page) }
+
+ subject(:service) { described_class.new(container: container, current_user: user) }
+
+ it 'executes webhooks' do
+ expect(service).to receive(:execute_hooks).once.with(page)
+
+ service.execute(page)
+ end
+
+ it 'increments the delete count' do
+ counter = Gitlab::UsageDataCounters::WikiPageCounter
+
+ expect { service.execute(page) }.to change { counter.read(:delete) }.by 1
+ end
+
+ it 'creates a new wiki page deletion event' do
+ # TODO: https://gitlab.com/gitlab-org/gitlab/-/issues/216904
+ pending('group wiki support') if container_type == :group
+
+ expect { service.execute(page) }.to change { Event.count }.by 1
+
+ expect(Event.recent.first).to have_attributes(
+ action: Event::DESTROYED,
+ target: have_attributes(canonical_slug: page.slug)
+ )
+ end
+
+ it 'does not increment the delete count if the deletion failed' do
+ counter = Gitlab::UsageDataCounters::WikiPageCounter
+
+ expect { service.execute(nil) }.not_to change { counter.read(:delete) }
+ end
+
+ context 'the feature is disabled' do
+ before do
+ stub_feature_flags(wiki_events: false)
+ end
+
+ it 'does not record the activity' do
+ expect { service.execute(page) }.not_to change(Event, :count)
+ end
+ end
+end
diff --git a/spec/support/shared_examples/services/wiki_pages/update_service_shared_examples.rb b/spec/support/shared_examples/services/wiki_pages/update_service_shared_examples.rb
new file mode 100644
index 00000000000..0dfc99d043b
--- /dev/null
+++ b/spec/support/shared_examples/services/wiki_pages/update_service_shared_examples.rb
@@ -0,0 +1,98 @@
+# frozen_string_literal: true
+
+RSpec.shared_examples 'WikiPages::UpdateService#execute' do |container_type|
+ let(:container) { create(container_type, :wiki_repo) }
+
+ let(:user) { create(:user) }
+ let(:page) { create(:wiki_page) }
+ let(:page_title) { 'New Title' }
+
+ let(:opts) do
+ {
+ content: 'New content for wiki page',
+ format: 'markdown',
+ message: 'New wiki message',
+ title: page_title
+ }
+ end
+
+ subject(:service) { described_class.new(container: container, current_user: user, params: opts) }
+
+ it 'updates the wiki page' do
+ updated_page = service.execute(page)
+
+ expect(updated_page).to be_valid
+ expect(updated_page.message).to eq(opts[:message])
+ expect(updated_page.content).to eq(opts[:content])
+ expect(updated_page.format).to eq(opts[:format].to_sym)
+ expect(updated_page.title).to eq(page_title)
+ end
+
+ it 'executes webhooks' do
+ expect(service).to receive(:execute_hooks).once.with(WikiPage)
+
+ service.execute(page)
+ end
+
+ it 'counts edit events' do
+ counter = Gitlab::UsageDataCounters::WikiPageCounter
+
+ expect { service.execute page }.to change { counter.read(:update) }.by 1
+ end
+
+ shared_examples 'adds activity event' do
+ it 'adds a new wiki page activity event' do
+ # TODO: https://gitlab.com/gitlab-org/gitlab/-/issues/216904
+ pending('group wiki support') if container_type == :group
+
+ expect { service.execute(page) }.to change { Event.count }.by 1
+
+ expect(Event.recent.first).to have_attributes(
+ action: Event::UPDATED,
+ wiki_page: page,
+ target_title: page.title
+ )
+ end
+ end
+
+ context 'the page is at the top level' do
+ let(:page_title) { 'Top level page' }
+
+ include_examples 'adds activity event'
+ end
+
+ context 'the page is in a subsection' do
+ let(:page_title) { 'Subsection / secondary page' }
+
+ include_examples 'adds activity event'
+ end
+
+ context 'the feature is disabled' do
+ before do
+ stub_feature_flags(wiki_events: false)
+ end
+
+ it 'does not record the activity' do
+ expect { service.execute(page) }.not_to change(Event, :count)
+ end
+ end
+
+ context 'when the options are bad' do
+ let(:page_title) { '' }
+
+ it 'does not count an edit event' do
+ counter = Gitlab::UsageDataCounters::WikiPageCounter
+
+ expect { service.execute page }.not_to change { counter.read(:update) }
+ end
+
+ it 'does not record the activity' do
+ expect { service.execute page }.not_to change(Event, :count)
+ end
+
+ it 'reports the error' do
+ expect(service.execute(page)).to be_invalid
+ .and have_attributes(errors: be_present)
+ end
+ end
+end
diff --git a/spec/support/shared_examples/services/wikis/create_attachment_service_shared_examples.rb b/spec/support/shared_examples/services/wikis/create_attachment_service_shared_examples.rb
new file mode 100644
index 00000000000..541e332e3a1
--- /dev/null
+++ b/spec/support/shared_examples/services/wikis/create_attachment_service_shared_examples.rb
@@ -0,0 +1,62 @@
+# frozen_string_literal: true
+
+RSpec.shared_examples 'Wikis::CreateAttachmentService#execute' do |container_type|
+ let(:container) { create(container_type, :wiki_repo) }
+ let(:wiki) { container.wiki }
+
+ let(:user) { create(:user) }
+ let(:file_name) { 'filename.txt' }
+ let(:file_path_regex) { %r{#{described_class::ATTACHMENT_PATH}/\h{32}/#{file_name}} }
+
+ let(:file_opts) do
+ {
+ file_name: file_name,
+ file_content: 'Content of attachment'
+ }
+ end
+ let(:opts) { file_opts }
+
+ let(:service) { Wikis::CreateAttachmentService.new(container: container, current_user: user, params: opts) }
+
+ subject(:service_execute) { service.execute[:result] }
+
+ before do
+ container.add_developer(user)
+ end
+
+ context 'creates branch if it does not exists' do
+ let(:branch_name) { 'new_branch' }
+ let(:opts) { file_opts.merge(branch_name: branch_name) }
+
+ it do
+ expect(wiki.repository.branches).to be_empty
+ expect { service.execute }.to change { wiki.repository.branches.count }.by(1)
+ expect(wiki.repository.branches.first.name).to eq branch_name
+ end
+ end
+
+ it 'adds file to the repository' do
+ expect(wiki.repository.ls_files('HEAD')).to be_empty
+
+ service.execute
+
+ files = wiki.repository.ls_files('HEAD')
+ expect(files.count).to eq 1
+ expect(files.first).to match(file_path_regex)
+ end
+
+ context 'returns' do
+ before do
+ allow(SecureRandom).to receive(:hex).and_return('fixed_hex')
+
+ service_execute
+ end
+
+ it 'returns related information', :aggregate_failures do
+ expect(service_execute[:file_name]).to eq file_name
+ expect(service_execute[:file_path]).to eq 'uploads/fixed_hex/filename.txt'
+ expect(service_execute[:branch]).to eq wiki.default_branch
+ expect(service_execute[:commit]).not_to be_empty
+ end
+ end
+end
diff --git a/spec/support/shared_examples/tasks/gitlab/import_export/measurable_shared_examples.rb b/spec/support/shared_examples/tasks/gitlab/import_export/measurable_shared_examples.rb
deleted file mode 100644
index 5950a1a53e2..00000000000
--- a/spec/support/shared_examples/tasks/gitlab/import_export/measurable_shared_examples.rb
+++ /dev/null
@@ -1,31 +0,0 @@
-# frozen_string_literal: true
-
-RSpec.shared_examples 'measurable' do
- context 'when measurement is enabled' do
- let(:measurement_enabled) { true }
-
- it 'prints measurement results' do
- expect { subject }.to output(including('Measuring enabled...', 'Number of sql calls:', 'GC stats:')).to_stdout
- end
- end
-
- context 'when measurement is not enabled' do
- let(:measurement_enabled) { false }
-
- it 'does not output measurement results' do
- expect { subject }.not_to output(/Measuring enabled.../).to_stdout
- end
- end
-
- context 'when measurement is not provided' do
- let(:measurement_enabled) { nil }
-
- it 'does not output measurement results' do
- expect { subject }.not_to output(/Measuring enabled.../).to_stdout
- end
-
- it 'does not raise any exception' do
- expect { subject }.not_to raise_error
- end
- end
-end
diff --git a/spec/support/shared_examples/workers/authorized_projects_worker_shared_example.rb b/spec/support/shared_examples/workers/authorized_projects_worker_shared_example.rb
new file mode 100644
index 00000000000..fba8b4aadbb
--- /dev/null
+++ b/spec/support/shared_examples/workers/authorized_projects_worker_shared_example.rb
@@ -0,0 +1,42 @@
+# frozen_string_literal: true
+
+require 'spec_helper'
+
+RSpec.shared_examples "refreshes user's project authorizations" do
+ describe '#perform' do
+ let(:user) { create(:user) }
+
+ subject(:job) { described_class.new }
+
+ it "refreshes user's authorized projects" do
+ expect_any_instance_of(User).to receive(:refresh_authorized_projects)
+
+ job.perform(user.id)
+ end
+
+ context "when the user is not found" do
+ it "does nothing" do
+ expect_any_instance_of(User).not_to receive(:refresh_authorized_projects)
+
+ job.perform(-1)
+ end
+ end
+
+ it_behaves_like "an idempotent worker" do
+ let(:job_args) { user.id }
+
+ it "does not change authorizations when run twice" do
+ group = create(:group)
+ create(:project, namespace: group)
+ group.add_developer(user)
+
+ # Delete the authorization created by the after save hook of the member
+ # created above.
+ user.project_authorizations.delete_all
+
+ expect { job.perform(user.id) }.to change { user.project_authorizations.reload.size }.by(1)
+ expect { job.perform(user.id) }.not_to change { user.project_authorizations.reload.size }
+ end
+ end
+ end
+end
diff --git a/spec/support/shared_examples/workers/gitlab/jira_import/jira_import_workers_shared_examples.rb b/spec/support/shared_examples/workers/gitlab/jira_import/jira_import_workers_shared_examples.rb
index c0d17d6853d..ae8c82cb67c 100644
--- a/spec/support/shared_examples/workers/gitlab/jira_import/jira_import_workers_shared_examples.rb
+++ b/spec/support/shared_examples/workers/gitlab/jira_import/jira_import_workers_shared_examples.rb
@@ -20,7 +20,7 @@ shared_examples 'does not advance to next stage' do
end
end
-shared_examples 'cannot do jira import' do
+shared_examples 'cannot do Jira import' do
it 'does not advance to next stage' do
worker = described_class.new
expect(worker).not_to receive(:import)
diff --git a/spec/support/shared_examples/workers/pages_domain_cron_worker_shared_examples.rb b/spec/support/shared_examples/workers/pages_domain_cron_worker_shared_examples.rb
index 9e8102aea53..c79e3ed7d21 100644
--- a/spec/support/shared_examples/workers/pages_domain_cron_worker_shared_examples.rb
+++ b/spec/support/shared_examples/workers/pages_domain_cron_worker_shared_examples.rb
@@ -3,12 +3,14 @@
RSpec.shared_examples 'a pages cronjob scheduling jobs with context' do |scheduled_worker_class|
let(:worker) { described_class.new }
- it 'does not cause extra queries for multiple domains' do
- control = ActiveRecord::QueryRecorder.new { worker.perform }
+ context 'with RequestStore enabled', :request_store do
+ it 'does not cause extra queries for multiple domains' do
+ control = ActiveRecord::QueryRecorder.new { worker.perform }
- extra_domain
+ extra_domain
- expect { worker.perform }.not_to exceed_query_limit(control)
+ expect { worker.perform }.not_to exceed_query_limit(control)
+ end
end
it 'schedules the renewal with a context' do
diff --git a/spec/support/shared_examples/workers/reactive_cacheable_shared_examples.rb b/spec/support/shared_examples/workers/reactive_cacheable_shared_examples.rb
new file mode 100644
index 00000000000..0bbd0e2a90d
--- /dev/null
+++ b/spec/support/shared_examples/workers/reactive_cacheable_shared_examples.rb
@@ -0,0 +1,59 @@
+# frozen_string_literal: true
+
+RSpec.shared_examples 'reactive cacheable worker' do
+ describe '#perform' do
+ context 'when reactive cache worker class is found' do
+ let!(:cluster) { create(:cluster, :project, :provided_by_gcp) }
+ let(:project) { cluster.project }
+ let!(:environment) { create(:environment, project: project) }
+
+ it 'calls #exclusively_update_reactive_cache!' do
+ expect_any_instance_of(Environment).to receive(:exclusively_update_reactive_cache!)
+
+ described_class.new.perform("Environment", environment.id)
+ end
+
+ context 'when ReactiveCaching::ExceededReactiveCacheLimit is raised' do
+ it 'avoids failing the job and tracks via Gitlab::ErrorTracking' do
+ allow_any_instance_of(Environment).to receive(:exclusively_update_reactive_cache!)
+ .and_raise(ReactiveCaching::ExceededReactiveCacheLimit)
+
+ expect(Gitlab::ErrorTracking).to receive(:track_exception)
+ .with(kind_of(ReactiveCaching::ExceededReactiveCacheLimit))
+
+ described_class.new.perform("Environment", environment.id)
+ end
+ end
+ end
+
+ context 'when reactive cache worker class is not found' do
+ it 'raises no error' do
+ expect { described_class.new.perform("Environment", -1) }.not_to raise_error
+ end
+ end
+
+ context 'when reactive cache worker class is invalid' do
+ it 'raises no error' do
+ expect { described_class.new.perform("FooBarKux", -1) }.not_to raise_error
+ end
+ end
+ end
+
+ describe 'worker context' do
+ it 'sets the related class on the job' do
+ described_class.perform_async('Environment', 1, 'other', 'argument')
+
+ scheduled_job = described_class.jobs.first
+
+ expect(scheduled_job).to include('meta.related_class' => 'Environment')
+ end
+
+ it 'sets the related class on the job when it was passed as a class' do
+ described_class.perform_async(Project, 1, 'other', 'argument')
+
+ scheduled_job = described_class.jobs.first
+
+ expect(scheduled_job).to include('meta.related_class' => 'Project')
+ end
+ end
+end
diff --git a/spec/support/sidekiq.rb b/spec/support/sidekiq.rb
index 9fa8df39019..374997af1ec 100644
--- a/spec/support/sidekiq.rb
+++ b/spec/support/sidekiq.rb
@@ -1,14 +1,23 @@
# frozen_string_literal: true
RSpec.configure do |config|
+ def gitlab_sidekiq_inline(&block)
+ # We need to cleanup the queues before running jobs in specs because the
+ # middleware might have written to redis
+ redis_queues_cleanup!
+ Sidekiq::Testing.inline!(&block)
+ ensure
+ redis_queues_cleanup!
+ end
+
# As we'll review the examples with this tag, we should either:
# - fix the example to not require Sidekiq inline mode (and remove this tag)
# - explicitly keep the inline mode and change the tag for `:sidekiq_inline` instead
config.around(:example, :sidekiq_might_not_need_inline) do |example|
- Sidekiq::Testing.inline! { example.run }
+ gitlab_sidekiq_inline { example.run }
end
config.around(:example, :sidekiq_inline) do |example|
- Sidekiq::Testing.inline! { example.run }
+ gitlab_sidekiq_inline { example.run }
end
end
diff --git a/spec/support/sidekiq_middleware.rb b/spec/support/sidekiq_middleware.rb
index 1380f4394d8..62f81ef1669 100644
--- a/spec/support/sidekiq_middleware.rb
+++ b/spec/support/sidekiq_middleware.rb
@@ -31,3 +31,16 @@ class DisableQueryLimit
end
end
end
+
+# When running `Sidekiq::Testing.inline!` each job is using a request-store.
+# This middleware makes sure the values don't leak into eachother.
+class IsolatedRequestStore
+ def call(_worker, msg, queue)
+ old_store = RequestStore.store.dup
+ RequestStore.clear!
+
+ yield
+
+ RequestStore.store = old_store
+ end
+end
diff --git a/spec/support/unicorn.rb b/spec/support/unicorn.rb
new file mode 100644
index 00000000000..0b01fc9e26c
--- /dev/null
+++ b/spec/support/unicorn.rb
@@ -0,0 +1,27 @@
+# frozen_string_literal: true
+
+REQUEST_CLASSES = [
+ ::Grape::Request,
+ ::Rack::Request
+].freeze
+
+def request_body_class
+ return ::Unicorn::TeeInput if defined?(::Unicorn)
+
+ Class.new(StringIO) do
+ def string
+ raise NotImplementedError, '#string is only valid under Puma which uses StringIO, use #read instead'
+ end
+ end
+end
+
+RSpec.configure do |config|
+ config.before(:each, :unicorn) do
+ REQUEST_CLASSES.each do |request_class|
+ allow_any_instance_of(request_class)
+ .to receive(:body).and_wrap_original do |m, *args|
+ request_body_class.new(m.call(*args).read)
+ end
+ end
+ end
+end
diff --git a/spec/support/webmock.rb b/spec/support/webmock.rb
index 57acc3b63b1..f952f7f0985 100644
--- a/spec/support/webmock.rb
+++ b/spec/support/webmock.rb
@@ -15,4 +15,20 @@ def webmock_allowed_hosts
end.compact.uniq
end
-WebMock.disable_net_connect!(allow_localhost: true, allow: webmock_allowed_hosts)
+# This prevents Selenium/WebMock from spawning thousands of connections
+# while waiting for an element to appear via Capybara's find:
+# https://github.com/teamcapybara/capybara/issues/2322#issuecomment-619321520
+def webmock_enable_with_http_connect_on_start!
+ webmock_enable!(net_http_connect_on_start: true)
+end
+
+def webmock_enable!(options = {})
+ WebMock.disable_net_connect!(
+ {
+ allow_localhost: true,
+ allow: webmock_allowed_hosts
+ }.merge(options)
+ )
+end
+
+webmock_enable!
diff --git a/spec/support_specs/helpers/active_record/query_recorder_spec.rb b/spec/support_specs/helpers/active_record/query_recorder_spec.rb
index 0827ce37b07..d15fbb5d4c3 100644
--- a/spec/support_specs/helpers/active_record/query_recorder_spec.rb
+++ b/spec/support_specs/helpers/active_record/query_recorder_spec.rb
@@ -3,8 +3,12 @@
require 'spec_helper'
describe ActiveRecord::QueryRecorder do
- class TestQueries < ActiveRecord::Base
- self.table_name = 'schema_migrations'
+ before do
+ stub_const('TestQueries', Class.new(ActiveRecord::Base))
+
+ TestQueries.class_eval do
+ self.table_name = 'schema_migrations'
+ end
end
describe 'detecting the right number of calls and their origin' do
diff --git a/spec/support_specs/helpers/stub_feature_flags_spec.rb b/spec/support_specs/helpers/stub_feature_flags_spec.rb
new file mode 100644
index 00000000000..b6e230075f2
--- /dev/null
+++ b/spec/support_specs/helpers/stub_feature_flags_spec.rb
@@ -0,0 +1,130 @@
+# frozen_string_literal: true
+
+require 'spec_helper'
+
+describe StubFeatureFlags do
+ before do
+ # reset stub introduced by `stub_feature_flags`
+ allow(Feature).to receive(:enabled?).and_call_original
+ end
+
+ context 'if not stubbed' do
+ it 'features are disabled by default' do
+ expect(Feature.enabled?(:test_feature)).to eq(false)
+ end
+ end
+
+ describe '#stub_feature_flags' do
+ using RSpec::Parameterized::TableSyntax
+
+ let(:feature_name) { :test_feature }
+
+ context 'when checking global state' do
+ where(:feature_actors, :expected_result) do
+ false | false
+ true | true
+ :A | false
+ %i[A] | false
+ %i[A B] | false
+ end
+
+ with_them do
+ before do
+ stub_feature_flags(feature_name => feature_actors)
+ end
+
+ it { expect(Feature.enabled?(feature_name)).to eq(expected_result) }
+ it { expect(Feature.disabled?(feature_name)).not_to eq(expected_result) }
+
+ context 'default_enabled does not impact feature state' do
+ it { expect(Feature.enabled?(feature_name, default_enabled: true)).to eq(expected_result) }
+ it { expect(Feature.disabled?(feature_name, default_enabled: true)).not_to eq(expected_result) }
+ end
+ end
+ end
+
+ context 'when checking scoped state' do
+ where(:feature_actors, :tested_actor, :expected_result) do
+ false | nil | false
+ true | nil | true
+ false | :A | false
+ true | :A | true
+ :A | nil | false
+ :A | :A | true
+ :A | :B | false
+ %i[A] | nil | false
+ %i[A] | :A | true
+ %i[A] | :B | false
+ %i[A B] | nil | false
+ %i[A B] | :A | true
+ %i[A B] | :B | true
+ end
+
+ with_them do
+ before do
+ stub_feature_flags(feature_name => feature_actors)
+ end
+
+ it { expect(Feature.enabled?(feature_name, tested_actor)).to eq(expected_result) }
+ it { expect(Feature.disabled?(feature_name, tested_actor)).not_to eq(expected_result) }
+
+ context 'default_enabled does not impact feature state' do
+ it { expect(Feature.enabled?(feature_name, tested_actor, default_enabled: true)).to eq(expected_result) }
+ it { expect(Feature.disabled?(feature_name, tested_actor, default_enabled: true)).not_to eq(expected_result) }
+ end
+ end
+ end
+
+ context 'type handling' do
+ context 'raises error' do
+ where(:feature_actors) do
+ ['string', 1, 1.0, OpenStruct.new]
+ end
+
+ with_them do
+ subject { stub_feature_flags(feature_name => feature_actors) }
+
+ it { expect { subject }.to raise_error(ArgumentError, /accepts only/) }
+ end
+ end
+
+ context 'does not raise error' do
+ where(:feature_actors) do
+ [true, false, nil, :symbol, double, User.new]
+ end
+
+ with_them do
+ subject { stub_feature_flags(feature_name => feature_actors) }
+
+ it { expect { subject }.not_to raise_error }
+ end
+ end
+ end
+
+ it 'subsquent run changes state' do
+ # enable FF only on A
+ stub_feature_flags(test_feature: %i[A])
+ expect(Feature.enabled?(:test_feature)).to eq(false)
+ expect(Feature.enabled?(:test_feature, :A)).to eq(true)
+ expect(Feature.enabled?(:test_feature, :B)).to eq(false)
+
+ # enable FF only on B
+ stub_feature_flags(test_feature: %i[B])
+ expect(Feature.enabled?(:test_feature)).to eq(false)
+ expect(Feature.enabled?(:test_feature, :A)).to eq(false)
+ expect(Feature.enabled?(:test_feature, :B)).to eq(true)
+
+ # enable FF on all
+ stub_feature_flags(test_feature: true)
+ expect(Feature.enabled?(:test_feature)).to eq(true)
+ expect(Feature.enabled?(:test_feature, :A)).to eq(true)
+ expect(Feature.enabled?(:test_feature, :B)).to eq(true)
+
+ # disable FF on all
+ stub_feature_flags(test_feature: false)
+ expect(Feature.enabled?(:test_feature)).to eq(false)
+ expect(Feature.enabled?(:test_feature, :A)).to eq(false)
+ expect(Feature.enabled?(:test_feature, :B)).to eq(false)
+ end
+ end
+end
diff --git a/spec/tasks/gitlab/artifacts/migrate_rake_spec.rb b/spec/tasks/gitlab/artifacts/migrate_rake_spec.rb
index 55bfb7acd9d..9ee00b4297b 100644
--- a/spec/tasks/gitlab/artifacts/migrate_rake_spec.rb
+++ b/spec/tasks/gitlab/artifacts/migrate_rake_spec.rb
@@ -22,18 +22,6 @@ describe 'gitlab:artifacts namespace rake task' do
context 'when local storage is used' do
let(:store) { ObjectStorage::Store::LOCAL }
- context 'and job does not have file store defined' do
- let(:object_storage_enabled) { true }
- let(:store) { nil }
-
- it "migrates file to remote storage" do
- subject
-
- expect(artifact.reload.file_store).to eq(ObjectStorage::Store::REMOTE)
- expect(job_trace.reload.file_store).to eq(ObjectStorage::Store::REMOTE)
- end
- end
-
context 'and remote storage is defined' do
let(:object_storage_enabled) { true }
diff --git a/spec/tasks/gitlab/gitaly_rake_spec.rb b/spec/tasks/gitlab/gitaly_rake_spec.rb
index 0cc92680582..d9fdc183bfe 100644
--- a/spec/tasks/gitlab/gitaly_rake_spec.rb
+++ b/spec/tasks/gitlab/gitaly_rake_spec.rb
@@ -46,15 +46,13 @@ describe 'gitlab:gitaly namespace rake task' do
it 'calls checkout_or_clone_version with the right arguments' do
expect(main_object)
- .to receive(:checkout_or_clone_version).with(version: version, repo: repo, target_dir: clone_path)
+ .to receive(:checkout_or_clone_version).with(version: version, repo: repo, target_dir: clone_path, clone_opts: %w[--depth 1])
subject
end
end
describe 'gmake/make' do
- let(:command_preamble) { %w[/usr/bin/env -u RUBYOPT -u BUNDLE_GEMFILE] }
-
before do
stub_env('CI', false)
FileUtils.mkdir_p(clone_path)
@@ -69,7 +67,7 @@ describe 'gitlab:gitaly namespace rake task' do
it 'calls gmake in the gitaly directory' do
expect(Gitlab::Popen).to receive(:popen).with(%w[which gmake]).and_return(['/usr/bin/gmake', 0])
- expect(main_object).to receive(:run_command!).with(command_preamble + %w[gmake]).and_return(true)
+ expect(Gitlab::Popen).to receive(:popen).with(%w[gmake], nil, { "BUNDLE_GEMFILE" => nil, "RUBYOPT" => nil }).and_return(true)
subject
end
@@ -82,7 +80,7 @@ describe 'gitlab:gitaly namespace rake task' do
end
it 'calls make in the gitaly directory' do
- expect(main_object).to receive(:run_command!).with(command_preamble + %w[make]).and_return(true)
+ expect(Gitlab::Popen).to receive(:popen).with(%w[make], nil, { "BUNDLE_GEMFILE" => nil, "RUBYOPT" => nil }).and_return(true)
subject
end
@@ -99,7 +97,7 @@ describe 'gitlab:gitaly namespace rake task' do
end
it 'calls make in the gitaly directory with --no-deployment flag for bundle' do
- expect(main_object).to receive(:run_command!).with(command_preamble + command).and_return(true)
+ expect(Gitlab::Popen).to receive(:popen).with(command, nil, { "BUNDLE_GEMFILE" => nil, "RUBYOPT" => nil }).and_return(true)
subject
end
diff --git a/spec/tasks/gitlab/snippets_rake_spec.rb b/spec/tasks/gitlab/snippets_rake_spec.rb
new file mode 100644
index 00000000000..c4bb8d7897c
--- /dev/null
+++ b/spec/tasks/gitlab/snippets_rake_spec.rb
@@ -0,0 +1,114 @@
+# frozen_string_literal: true
+
+require 'rake_helper'
+
+describe 'gitlab:snippets namespace rake task' do
+ let_it_be(:user) { create(:user)}
+ let_it_be(:migrated) { create(:personal_snippet, :repository, author: user) }
+ let(:non_migrated) { create_list(:personal_snippet, 3, author: user) }
+ let(:non_migrated_ids) { non_migrated.pluck(:id) }
+
+ before :all do
+ Rake.application.rake_require 'tasks/gitlab/snippets'
+ end
+
+ describe 'migrate' do
+ subject { run_rake_task('gitlab:snippets:migrate') }
+
+ before do
+ stub_env('SNIPPET_IDS' => non_migrated_ids.join(','))
+ end
+
+ it 'can migrate specific snippets passing ids' do
+ expect_next_instance_of(Gitlab::BackgroundMigration::BackfillSnippetRepositories) do |instance|
+ expect(instance).to receive(:perform_by_ids).with(non_migrated_ids).and_call_original
+ end
+
+ expect { subject }.to output(/All snippets were migrated successfully/).to_stdout
+ end
+
+ it 'returns the ids of those snippet that failed the migration' do
+ expect_next_instance_of(Gitlab::BackgroundMigration::BackfillSnippetRepositories) do |instance|
+ expect(instance).to receive(:perform_by_ids).with(non_migrated_ids)
+ end
+
+ expect { subject }.to output(/The following snippets couldn't be migrated:\n#{non_migrated_ids.join(',')}/).to_stdout
+ end
+
+ it 'fails if the SNIPPET_IDS env var is not set' do
+ stub_env('SNIPPET_IDS' => nil)
+
+ expect { subject }.to raise_error(RuntimeError, 'Please supply the list of ids through the SNIPPET_IDS env var')
+ end
+
+ it 'fails if the number of ids provided is higher than the limit' do
+ stub_env('LIMIT' => 2)
+
+ expect { subject }.to raise_error(RuntimeError, /The number of ids provided is higher than/)
+ end
+
+ it 'fails if the env var LIMIT is invalid' do
+ stub_env('LIMIT' => 'foo')
+
+ expect { subject }.to raise_error(RuntimeError, 'Invalid limit value')
+ end
+
+ it 'fails if the ids are invalid' do
+ stub_env('SNIPPET_IDS' => '1,2,a')
+
+ expect { subject }.to raise_error(RuntimeError, 'Invalid id provided')
+ end
+
+ it 'fails if the snippet background migration is running' do
+ Sidekiq::Testing.disable! do
+ BackgroundMigrationWorker.perform_in(180, 'BackfillSnippetRepositories', [non_migrated.first.id, non_migrated.last.id])
+ expect(Sidekiq::ScheduledSet.new).to be_one
+
+ expect { subject }.to raise_error(RuntimeError, /There are already snippet migrations running/)
+
+ Sidekiq::ScheduledSet.new.clear
+ end
+ end
+ end
+
+ describe 'migration_status' do
+ subject { run_rake_task('gitlab:snippets:migration_status') }
+
+ it 'returns a message when the background migration is not running' do
+ expect { subject }.to output("There are no snippet migrations running\n").to_stdout
+ end
+
+ it 'returns a message saying that the background migration is running' do
+ Sidekiq::Testing.disable! do
+ BackgroundMigrationWorker.perform_in(180, 'BackfillSnippetRepositories', [non_migrated.first.id, non_migrated.last.id])
+ expect(Sidekiq::ScheduledSet.new).to be_one
+
+ expect { subject }.to output("There are snippet migrations running\n").to_stdout
+
+ Sidekiq::ScheduledSet.new.clear
+ end
+ end
+ end
+
+ describe 'list_non_migrated' do
+ subject { run_rake_task('gitlab:snippets:list_non_migrated') }
+
+ it 'returns a message if all snippets are migrated' do
+ expect { subject }.to output("All snippets have been successfully migrated\n").to_stdout
+ end
+
+ context 'when there are still non migrated snippets' do
+ let!(:non_migrated) { create_list(:personal_snippet, 3, author: user) }
+
+ it 'returns a message returning the non migrated snippets ids' do
+ expect { subject }.to output(/#{non_migrated_ids}/).to_stdout
+ end
+
+ it 'returns as many snippet ids as the limit set' do
+ stub_env('LIMIT' => 1)
+
+ expect { subject }.to output(/#{non_migrated_ids[0]}/).to_stdout
+ end
+ end
+ end
+end
diff --git a/spec/tasks/gitlab/task_helpers_spec.rb b/spec/tasks/gitlab/task_helpers_spec.rb
index 4546d3bdfaf..8e6872f4d6f 100644
--- a/spec/tasks/gitlab/task_helpers_spec.rb
+++ b/spec/tasks/gitlab/task_helpers_spec.rb
@@ -28,7 +28,7 @@ describe Gitlab::TaskHelpers do
context "target_dir doesn't exist" do
it 'clones the repo' do
- expect(subject).to receive(:clone_repo).with(repo, clone_path)
+ expect(subject).to receive(:clone_repo).with(repo, clone_path, clone_opts: [])
subject.checkout_or_clone_version(version: version, repo: repo, target_dir: clone_path)
end
@@ -45,6 +45,12 @@ describe Gitlab::TaskHelpers do
subject.checkout_or_clone_version(version: version, repo: repo, target_dir: clone_path)
end
end
+
+ it 'accepts clone_opts' do
+ expect(subject).to receive(:clone_repo).with(repo, clone_path, clone_opts: %w[--depth 1])
+
+ subject.checkout_or_clone_version(version: version, repo: repo, target_dir: clone_path, clone_opts: %w[--depth 1])
+ end
end
describe '#clone_repo' do
@@ -54,6 +60,13 @@ describe Gitlab::TaskHelpers do
subject.clone_repo(repo, clone_path)
end
+
+ it 'accepts clone_opts' do
+ expect(subject)
+ .to receive(:run_command!).with(%W[#{Gitlab.config.git.bin_path} clone --depth 1 -- #{repo} #{clone_path}])
+
+ subject.clone_repo(repo, clone_path, clone_opts: %w[--depth 1])
+ end
end
describe '#checkout_version' do
diff --git a/spec/tasks/gitlab/uploads/migrate_rake_spec.rb b/spec/tasks/gitlab/uploads/migrate_rake_spec.rb
index 8ea0a98a1dc..49026cd74f9 100644
--- a/spec/tasks/gitlab/uploads/migrate_rake_spec.rb
+++ b/spec/tasks/gitlab/uploads/migrate_rake_spec.rb
@@ -119,4 +119,16 @@ describe 'gitlab:uploads:migrate and migrate_to_local rake tasks' do
it_behaves_like 'enqueue upload migration jobs in batch', batch: 4
end
+
+ context 'for DesignManagement::DesignV432x230Uploader' do
+ let(:uploader_class) { DesignManagement::DesignV432x230Uploader }
+ let(:model_class) { DesignManagement::Action }
+ let(:mounted_as) { :image_v432x230 }
+
+ before do
+ create_list(:design_action, 10, :with_image_v432x230)
+ end
+
+ it_behaves_like 'enqueue upload migration jobs in batch', batch: 4
+ end
end
diff --git a/spec/tasks/gitlab/workhorse_rake_spec.rb b/spec/tasks/gitlab/workhorse_rake_spec.rb
index b7877a84185..139652ac258 100644
--- a/spec/tasks/gitlab/workhorse_rake_spec.rb
+++ b/spec/tasks/gitlab/workhorse_rake_spec.rb
@@ -36,7 +36,7 @@ describe 'gitlab:workhorse namespace rake task' do
it 'calls checkout_or_clone_version with the right arguments' do
expect(main_object)
- .to receive(:checkout_or_clone_version).with(version: version, repo: repo, target_dir: clone_path)
+ .to receive(:checkout_or_clone_version).with(version: version, repo: repo, target_dir: clone_path, clone_opts: %w[--depth 1])
run_rake_task('gitlab:workhorse:install', clone_path)
end
diff --git a/spec/uploaders/content_type_whitelist_spec.rb b/spec/uploaders/content_type_whitelist_spec.rb
index 4689f83759d..32d030cdfee 100644
--- a/spec/uploaders/content_type_whitelist_spec.rb
+++ b/spec/uploaders/content_type_whitelist_spec.rb
@@ -3,16 +3,20 @@
require 'spec_helper'
describe ContentTypeWhitelist do
- class DummyUploader < CarrierWave::Uploader::Base
- include ContentTypeWhitelist::Concern
+ let_it_be(:model) { build_stubbed(:user) }
+ let!(:uploader) do
+ stub_const('DummyUploader', Class.new(CarrierWave::Uploader::Base))
+
+ DummyUploader.class_eval do
+ include ContentTypeWhitelist::Concern
- def content_type_whitelist
- %w[image/png image/jpeg]
+ def content_type_whitelist
+ %w[image/png image/jpeg]
+ end
end
- end
- let_it_be(:model) { build_stubbed(:user) }
- let_it_be(:uploader) { DummyUploader.new(model, :dummy) }
+ DummyUploader.new(model, :dummy)
+ end
context 'upload whitelisted file content type' do
let(:path) { File.join('spec', 'fixtures', 'rails_sample.jpg') }
diff --git a/spec/uploaders/design_management/design_v432x230_uploader_spec.rb b/spec/uploaders/design_management/design_v432x230_uploader_spec.rb
new file mode 100644
index 00000000000..8c62b6ad6a8
--- /dev/null
+++ b/spec/uploaders/design_management/design_v432x230_uploader_spec.rb
@@ -0,0 +1,86 @@
+# frozen_string_literal: true
+
+require 'spec_helper'
+
+describe DesignManagement::DesignV432x230Uploader do
+ include CarrierWave::Test::Matchers
+
+ let(:model) { create(:design_action, :with_image_v432x230) }
+ let(:upload) { create(:upload, :design_action_image_v432x230_upload, model: model) }
+
+ subject(:uploader) { described_class.new(model, :image_v432x230) }
+
+ it_behaves_like 'builds correct paths',
+ store_dir: %r[uploads/-/system/design_management/action/image_v432x230/],
+ upload_path: %r[uploads/-/system/design_management/action/image_v432x230/],
+ relative_path: %r[uploads/-/system/design_management/action/image_v432x230/],
+ absolute_path: %r[#{CarrierWave.root}/uploads/-/system/design_management/action/image_v432x230/]
+
+ context 'object_store is REMOTE' do
+ before do
+ stub_uploads_object_storage
+ end
+
+ include_context 'with storage', described_class::Store::REMOTE
+
+ it_behaves_like 'builds correct paths',
+ store_dir: %r[design_management/action/image_v432x230/],
+ upload_path: %r[design_management/action/image_v432x230/],
+ relative_path: %r[design_management/action/image_v432x230/]
+ end
+
+ describe "#migrate!" do
+ before do
+ uploader.store!(fixture_file_upload('spec/fixtures/dk.png'))
+ stub_uploads_object_storage
+ end
+
+ it_behaves_like 'migrates', to_store: described_class::Store::REMOTE
+ it_behaves_like 'migrates', from_store: described_class::Store::REMOTE, to_store: described_class::Store::LOCAL
+ end
+
+ it 'resizes images', :aggregate_failures do
+ image_loader = CarrierWave::Test::Matchers::ImageLoader
+ original_file = fixture_file_upload('spec/fixtures/dk.png')
+ uploader.store!(original_file)
+
+ expect(
+ image_loader.load_image(original_file.tempfile.path)
+ ).to have_attributes(
+ width: 460,
+ height: 322
+ )
+ expect(
+ image_loader.load_image(uploader.file.file)
+ ).to have_attributes(
+ width: 329,
+ height: 230
+ )
+ end
+
+ context 'accept whitelist file content type' do
+ # We need to feed through a valid path, but we force the parsed mime type
+ # in a stub below so we can set any path.
+ let_it_be(:path) { File.join('spec', 'fixtures', 'dk.png') }
+
+ where(:mime_type) { described_class::MIME_TYPE_WHITELIST }
+
+ with_them do
+ include_context 'force content type detection to mime_type'
+
+ it_behaves_like 'accepted carrierwave upload'
+ end
+ end
+
+ context 'upload non-whitelisted file content type' do
+ let_it_be(:path) { File.join('spec', 'fixtures', 'logo_sample.svg') }
+
+ it_behaves_like 'denied carrierwave upload'
+ end
+
+ context 'upload misnamed non-whitelisted file content type' do
+ let_it_be(:path) { File.join('spec', 'fixtures', 'not_a_png.png') }
+
+ it_behaves_like 'denied carrierwave upload'
+ end
+end
diff --git a/spec/uploaders/records_uploads_spec.rb b/spec/uploaders/records_uploads_spec.rb
index 71eff23c77c..1a3c416c74a 100644
--- a/spec/uploaders/records_uploads_spec.rb
+++ b/spec/uploaders/records_uploads_spec.rb
@@ -4,7 +4,9 @@ require 'spec_helper'
describe RecordsUploads do
let!(:uploader) do
- class RecordsUploadsExampleUploader < GitlabUploader
+ stub_const('RecordsUploadsExampleUploader', Class.new(GitlabUploader))
+
+ RecordsUploadsExampleUploader.class_eval do
include RecordsUploads::Concern
storage :file
diff --git a/spec/uploaders/workers/object_storage/migrate_uploads_worker_spec.rb b/spec/uploaders/workers/object_storage/migrate_uploads_worker_spec.rb
index fcb8f4e51b5..7bf8512a6fd 100644
--- a/spec/uploaders/workers/object_storage/migrate_uploads_worker_spec.rb
+++ b/spec/uploaders/workers/object_storage/migrate_uploads_worker_spec.rb
@@ -65,4 +65,16 @@ describe ObjectStorage::MigrateUploadsWorker do
end
end
end
+
+ context 'for DesignManagement::DesignV432x230Uploader' do
+ let(:model_class) { DesignManagement::Action }
+ let!(:design_actions) { create_list(:design_action, 10, :with_image_v432x230) }
+ let(:mounted_as) { :image_v432x230 }
+
+ before do
+ stub_uploads_object_storage(DesignManagement::DesignV432x230Uploader)
+ end
+
+ it_behaves_like 'uploads migration worker'
+ end
end
diff --git a/spec/validators/cron_freeze_period_timezone_validator_spec.rb b/spec/validators/cron_freeze_period_timezone_validator_spec.rb
new file mode 100644
index 00000000000..d283b89fa54
--- /dev/null
+++ b/spec/validators/cron_freeze_period_timezone_validator_spec.rb
@@ -0,0 +1,24 @@
+# frozen_string_literal: true
+
+require 'spec_helper'
+
+describe CronFreezePeriodTimezoneValidator do
+ using RSpec::Parameterized::TableSyntax
+
+ subject { create :ci_freeze_period }
+
+ where(:freeze_start, :freeze_end, :is_valid) do
+ '0 23 * * 5' | '0 7 * * 1' | true
+ '0 23 * * 5' | 'invalid' | false
+ 'invalid' | '0 7 * * 1' | false
+ end
+
+ with_them do
+ it 'crontab validation' do
+ subject.freeze_start = freeze_start
+ subject.freeze_end = freeze_end
+
+ expect(subject.valid?).to eq(is_valid)
+ end
+ end
+end
diff --git a/spec/validators/cron_validator_spec.rb b/spec/validators/cron_validator_spec.rb
new file mode 100644
index 00000000000..d6605610402
--- /dev/null
+++ b/spec/validators/cron_validator_spec.rb
@@ -0,0 +1,47 @@
+# frozen_string_literal: true
+
+require 'spec_helper'
+
+describe CronValidator do
+ subject do
+ Class.new do
+ include ActiveModel::Model
+ include ActiveModel::Validations
+ attr_accessor :cron
+ validates :cron, cron: true
+
+ def cron_timezone
+ 'UTC'
+ end
+ end.new
+ end
+
+ it 'validates valid crontab' do
+ subject.cron = '0 23 * * 5'
+
+ expect(subject.valid?).to be_truthy
+ end
+
+ it 'validates invalid crontab' do
+ subject.cron = 'not a cron'
+
+ expect(subject.valid?).to be_falsy
+ end
+
+ context 'cron field is not whitelisted' do
+ subject do
+ Class.new do
+ include ActiveModel::Model
+ include ActiveModel::Validations
+ attr_accessor :cron_partytime
+ validates :cron_partytime, cron: true
+ end.new
+ end
+
+ it 'raises an error' do
+ subject.cron_partytime = '0 23 * * 5'
+
+ expect { subject.valid? }.to raise_error(StandardError, "Non-whitelisted attribute")
+ end
+ end
+end
diff --git a/spec/views/admin/sessions/new.html.haml_spec.rb b/spec/views/admin/sessions/new.html.haml_spec.rb
index 05601e5471e..b52ad0f9505 100644
--- a/spec/views/admin/sessions/new.html.haml_spec.rb
+++ b/spec/views/admin/sessions/new.html.haml_spec.rb
@@ -6,20 +6,26 @@ describe 'admin/sessions/new.html.haml' do
let(:user) { create(:admin) }
before do
+ disable_all_signin_methods
+
allow(view).to receive(:current_user).and_return(user)
- allow(view).to receive(:omniauth_enabled?).and_return(false)
end
context 'internal admin user' do
+ before do
+ allow(view).to receive(:allow_admin_mode_password_authentication_for_web?).and_return(true)
+ end
+
it 'shows enter password form' do
render
+ expect(rendered).to have_selector('[data-qa-selector="sign_in_tab"]')
expect(rendered).to have_css('#login-pane.active')
- expect(rendered).to have_selector('input[name="user[password]"]')
+ expect(rendered).to have_selector('[data-qa-selector="password_field"]')
end
it 'warns authentication not possible if password not set' do
- allow(user).to receive(:require_password_creation_for_web?).and_return(true)
+ allow(view).to receive(:allow_admin_mode_password_authentication_for_web?).and_return(false)
render
@@ -39,8 +45,53 @@ describe 'admin/sessions/new.html.haml' do
expect(rendered).to have_css('.omniauth-container')
expect(rendered).to have_content _('Sign in with')
-
expect(rendered).not_to have_content _('No authentication methods configured.')
end
end
+
+ context 'ldap authentication' do
+ let(:user) { create(:omniauth_user, :admin, extern_uid: 'my-uid', provider: 'ldapmain') }
+ let(:server) { { provider_name: 'ldapmain', label: 'LDAP' }.with_indifferent_access }
+
+ before do
+ enable_ldap
+ end
+
+ it 'is shown when enabled' do
+ render
+
+ expect(rendered).to have_selector('[data-qa-selector="ldap_tab"]')
+ expect(rendered).to have_css('.login-box#ldapmain')
+ expect(rendered).to have_field('LDAP Username')
+ expect(rendered).not_to have_content('No authentication methods configured')
+ end
+
+ it 'is not shown when LDAP sign in is disabled' do
+ disable_ldap_sign_in
+
+ render
+
+ expect(rendered).not_to have_selector('[data-qa-selector="ldap_tab"]')
+ expect(rendered).not_to have_field('LDAP Username')
+ expect(rendered).to have_content('No authentication methods configured')
+ end
+
+ def enable_ldap
+ allow(view).to receive(:ldap_servers).and_return([server])
+ allow(view).to receive(:form_based_providers).and_return([:ldapmain])
+ allow(view).to receive(:omniauth_callback_path).with(:user, 'ldapmain').and_return('/ldapmain')
+ allow(view).to receive(:ldap_sign_in_enabled?).and_return(true)
+ end
+
+ def disable_ldap_sign_in
+ allow(view).to receive(:ldap_sign_in_enabled?).and_return(false)
+ allow(view).to receive(:ldap_servers).and_return([])
+ end
+ end
+
+ def disable_all_signin_methods
+ allow(view).to receive(:password_authentication_enabled_for_web?).and_return(false)
+ allow(view).to receive(:omniauth_enabled?).and_return(false)
+ allow(view).to receive(:ldap_sign_in_enabled?).and_return(false)
+ end
end
diff --git a/spec/views/admin/users/_user.html.haml_spec.rb b/spec/views/admin/users/_user.html.haml_spec.rb
index 96d84229d94..de5a291a6f8 100644
--- a/spec/views/admin/users/_user.html.haml_spec.rb
+++ b/spec/views/admin/users/_user.html.haml_spec.rb
@@ -9,7 +9,7 @@ describe 'admin/users/_user.html.haml' do
context 'internal users' do
context 'when showing a `Ghost User`' do
- let(:user) { create(:user, ghost: true) }
+ let(:user) { create(:user, :ghost) }
it 'does not render action buttons' do
render
@@ -27,6 +27,16 @@ describe 'admin/users/_user.html.haml' do
expect(rendered).not_to have_selector('.table-action-buttons')
end
end
+
+ context 'when showing a `Migration User`' do
+ let(:user) { create(:user, user_type: :migration_bot) }
+
+ it 'does not render action buttons' do
+ render
+
+ expect(rendered).not_to have_selector('.table-action-buttons')
+ end
+ end
end
context 'when showing an external user' do
diff --git a/spec/views/devise/sessions/new.html.haml_spec.rb b/spec/views/devise/sessions/new.html.haml_spec.rb
index 66afc2af7ce..27bd683bbf0 100644
--- a/spec/views/devise/sessions/new.html.haml_spec.rb
+++ b/spec/views/devise/sessions/new.html.haml_spec.rb
@@ -54,14 +54,14 @@ describe 'devise/sessions/new' do
def enable_ldap
stub_ldap_setting(enabled: true)
- assign(:ldap_servers, [server])
+ allow(view).to receive(:ldap_servers).and_return([server])
allow(view).to receive(:form_based_providers).and_return([:ldapmain])
allow(view).to receive(:omniauth_callback_path).with(:user, 'ldapmain').and_return('/ldapmain')
end
def disable_ldap_sign_in
allow(view).to receive(:ldap_sign_in_enabled?).and_return(false)
- assign(:ldap_servers, [])
+ allow(view).to receive(:ldap_servers).and_return([])
end
def disable_captcha
diff --git a/spec/views/devise/shared/_signin_box.html.haml_spec.rb b/spec/views/devise/shared/_signin_box.html.haml_spec.rb
index f8867477603..dfd8c315e50 100644
--- a/spec/views/devise/shared/_signin_box.html.haml_spec.rb
+++ b/spec/views/devise/shared/_signin_box.html.haml_spec.rb
@@ -6,7 +6,7 @@ describe 'devise/shared/_signin_box' do
describe 'Crowd form' do
before do
stub_devise
- assign(:ldap_servers, [])
+ allow(view).to receive(:ldap_servers).and_return([])
allow(view).to receive(:current_application_settings).and_return(Gitlab::CurrentSettings.current_application_settings)
allow(view).to receive(:captcha_enabled?).and_return(false)
allow(view).to receive(:captcha_on_login_required?).and_return(false)
diff --git a/spec/views/help/index.html.haml_spec.rb b/spec/views/help/index.html.haml_spec.rb
index 98040da9d2c..3831ddacb72 100644
--- a/spec/views/help/index.html.haml_spec.rb
+++ b/spec/views/help/index.html.haml_spec.rb
@@ -53,6 +53,18 @@ describe 'help/index' do
end
end
+ describe 'Markdown rendering' do
+ before do
+ assign(:help_index, 'Welcome to [GitLab](https://about.gitlab.com/) Documentation.')
+ end
+
+ it 'renders Markdown' do
+ render
+
+ expect(rendered).to have_link('GitLab', href: 'https://about.gitlab.com/')
+ end
+ end
+
def stub_user(user = double)
allow(view).to receive(:user_signed_in?).and_return(user)
end
diff --git a/spec/views/help/show.html.haml_spec.rb b/spec/views/help/show.html.haml_spec.rb
new file mode 100644
index 00000000000..539c647c1d3
--- /dev/null
+++ b/spec/views/help/show.html.haml_spec.rb
@@ -0,0 +1,18 @@
+# frozen_string_literal: true
+
+require 'spec_helper'
+
+describe 'help/show' do
+ describe 'Markdown rendering' do
+ before do
+ assign(:path, 'ssh/README')
+ assign(:markdown, 'Welcome to [GitLab](https://about.gitlab.com/) Documentation.')
+ end
+
+ it 'renders Markdown' do
+ render
+
+ expect(rendered).to have_link('GitLab', href: 'https://about.gitlab.com/')
+ end
+ end
+end
diff --git a/spec/views/layouts/nav/sidebar/_admin.html.haml_spec.rb b/spec/views/layouts/nav/sidebar/_admin.html.haml_spec.rb
index aee2b0baf92..2f8a75a81c8 100644
--- a/spec/views/layouts/nav/sidebar/_admin.html.haml_spec.rb
+++ b/spec/views/layouts/nav/sidebar/_admin.html.haml_spec.rb
@@ -58,15 +58,6 @@ describe 'layouts/nav/sidebar/_admin' do
it_behaves_like 'page has active sub tab', 'Users'
end
- context 'on logs' do
- before do
- allow(controller).to receive(:controller_name).and_return('logs')
- end
-
- it_behaves_like 'page has active tab', 'Monitoring'
- it_behaves_like 'page has active sub tab', 'Logs'
- end
-
context 'on messages' do
before do
allow(controller).to receive(:controller_name).and_return('broadcast_messages')
diff --git a/spec/views/layouts/nav/sidebar/_project.html.haml_spec.rb b/spec/views/layouts/nav/sidebar/_project.html.haml_spec.rb
index cd622807c09..3d5c34ae1e0 100644
--- a/spec/views/layouts/nav/sidebar/_project.html.haml_spec.rb
+++ b/spec/views/layouts/nav/sidebar/_project.html.haml_spec.rb
@@ -136,27 +136,37 @@ describe 'layouts/nav/sidebar/_project' do
end
describe 'operations settings tab' do
- before do
- project.update!(archived: project_archived)
- end
+ describe 'archive projects' do
+ before do
+ project.update!(archived: project_archived)
+ end
- context 'when project is archived' do
- let(:project_archived) { true }
+ context 'when project is archived' do
+ let(:project_archived) { true }
- it 'does not show the operations settings tab' do
- render
+ it 'does not show the operations settings tab' do
+ render
- expect(rendered).not_to have_link('Operations', href: project_settings_operations_path(project))
+ expect(rendered).not_to have_link('Operations', href: project_settings_operations_path(project))
+ end
end
- end
- context 'when project is active' do
- let(:project_archived) { false }
+ context 'when project is active' do
+ let(:project_archived) { false }
- it 'shows the operations settings tab' do
+ it 'shows the operations settings tab' do
+ render
+
+ expect(rendered).to have_link('Operations', href: project_settings_operations_path(project))
+ end
+ end
+ end
+
+ describe 'Alert Management' do
+ it 'shows the Alerts sidebar entry' do
render
- expect(rendered).to have_link('Operations', href: project_settings_operations_path(project))
+ expect(rendered).to have_css('a[title="Alerts"]')
end
end
end
@@ -186,4 +196,30 @@ describe 'layouts/nav/sidebar/_project' do
end
end
end
+
+ describe 'project access tokens' do
+ context 'self-managed instance' do
+ before do
+ allow(Gitlab).to receive(:com?).and_return(false)
+ end
+
+ it 'displays "Access Tokens" nav item' do
+ render
+
+ expect(rendered).to have_link('Access Tokens', href: project_settings_access_tokens_path(project))
+ end
+ end
+
+ context 'gitlab.com' do
+ before do
+ allow(Gitlab).to receive(:com?).and_return(true)
+ end
+
+ it 'does not display "Access Tokens" nav item' do
+ render
+
+ expect(rendered).not_to have_link('Access Tokens', href: project_settings_access_tokens_path(project))
+ end
+ end
+ end
end
diff --git a/spec/views/profiles/show.html.haml_spec.rb b/spec/views/profiles/show.html.haml_spec.rb
index e1c21f87780..14e6feed3ab 100644
--- a/spec/views/profiles/show.html.haml_spec.rb
+++ b/spec/views/profiles/show.html.haml_spec.rb
@@ -19,48 +19,4 @@ describe 'profiles/show' do
expect(rendered).to have_field('user_id', with: user.id)
end
end
-
- context 'gitlab.com organization field' do
- before do
- allow(Gitlab).to receive(:com?).and_return(true)
- end
-
- context 'when `:gitlab_employee_badge` feature flag is enabled' do
- context 'and when user has an `@gitlab.com` email address' do
- let(:user) { create(:user, email: 'test@gitlab.com') }
-
- it 'displays the organization field as `readonly` with a `value` of `GitLab`' do
- render
-
- expect(rendered).to have_selector('#user_organization[readonly][value="GitLab"]')
- end
- end
-
- context 'and when a user does not have an `@gitlab.com` email' do
- let(:user) { create(:user, email: 'test@example.com') }
-
- it 'displays an editable organization field' do
- render
-
- expect(rendered).to have_selector('#user_organization:not([readonly]):not([value="GitLab"])')
- end
- end
- end
-
- context 'when `:gitlab_employee_badge` feature flag is disabled' do
- before do
- stub_feature_flags(gitlab_employee_badge: false)
- end
-
- context 'and when a user has an `@gitlab.com` email' do
- let(:user) { create(:user, email: 'test@gitlab.com') }
-
- it 'displays an editable organization field' do
- render
-
- expect(rendered).to have_selector('#user_organization:not([readonly]):not([value="GitLab"])')
- end
- end
- end
- end
end
diff --git a/spec/views/projects/issues/_related_branches.html.haml_spec.rb b/spec/views/projects/issues/_related_branches.html.haml_spec.rb
index a6817e3fdbf..6c9bbaea38c 100644
--- a/spec/views/projects/issues/_related_branches.html.haml_spec.rb
+++ b/spec/views/projects/issues/_related_branches.html.haml_spec.rb
@@ -5,23 +5,25 @@ require 'spec_helper'
describe 'projects/issues/_related_branches' do
include Devise::Test::ControllerHelpers
- let(:user) { create(:user) }
- let(:project) { create(:project, :repository) }
- let(:branch) { project.repository.find_branch('feature') }
- let!(:pipeline) { create(:ci_pipeline, project: project, sha: branch.dereferenced_target.id, ref: 'feature') }
+ let(:pipeline) { build(:ci_pipeline, :success) }
+ let(:status) { pipeline.detailed_status(build(:user)) }
before do
- assign(:project, project)
- assign(:related_branches, ['feature'])
-
- project.add_developer(user)
- allow(view).to receive(:current_user).and_return(user)
+ assign(:related_branches, [
+ { name: 'other', link: 'link-to-other', pipeline_status: nil },
+ { name: 'feature', link: 'link-to-feature', pipeline_status: status }
+ ])
render
end
- it 'shows the related branches with their build status' do
- expect(rendered).to match('feature')
+ it 'shows the related branches with their build status', :aggregate_failures do
+ expect(rendered).to have_text('feature')
+ expect(rendered).to have_text('other')
+ expect(rendered).to have_link(href: 'link-to-feature')
+ expect(rendered).to have_link(href: 'link-to-other')
expect(rendered).to have_css('.related-branch-ci-status')
+ expect(rendered).to have_css('.ci-status-icon')
+ expect(rendered).to have_css('.related-branch-info')
end
end
diff --git a/spec/views/projects/issues/show.html.haml_spec.rb b/spec/views/projects/issues/show.html.haml_spec.rb
index fb09840c8f4..60a541916e9 100644
--- a/spec/views/projects/issues/show.html.haml_spec.rb
+++ b/spec/views/projects/issues/show.html.haml_spec.rb
@@ -3,18 +3,7 @@
require 'spec_helper'
describe 'projects/issues/show' do
- let(:project) { create(:project, :repository) }
- let(:issue) { create(:issue, project: project, author: user) }
- let(:user) { create(:user) }
-
- before do
- assign(:project, project)
- assign(:issue, issue)
- assign(:noteable, issue)
- stub_template 'shared/issuable/_sidebar' => ''
- stub_template 'projects/issues/_discussion' => ''
- allow(view).to receive(:user_status).and_return('')
- end
+ include_context 'project show action'
context 'when the issue is closed' do
before do
@@ -152,18 +141,4 @@ describe 'projects/issues/show' do
expect(rendered).not_to have_selector('#js-sentry-error-stack-trace')
end
end
-
- context 'when issue is created by a GitLab team member' do
- let(:user) { create(:user, email: 'test@gitlab.com') }
-
- before do
- allow(Gitlab).to receive(:com?).and_return(true)
- end
-
- it 'renders an employee badge next to their name' do
- render
-
- expect(rendered).to have_selector('[aria-label="GitLab Team Member"]')
- end
- end
end
diff --git a/spec/views/projects/merge_requests/show.html.haml_spec.rb b/spec/views/projects/merge_requests/show.html.haml_spec.rb
index 67e7c3cf2fb..665003d137a 100644
--- a/spec/views/projects/merge_requests/show.html.haml_spec.rb
+++ b/spec/views/projects/merge_requests/show.html.haml_spec.rb
@@ -3,45 +3,7 @@
require 'spec_helper'
describe 'projects/merge_requests/show.html.haml' do
- include Devise::Test::ControllerHelpers
- include ProjectForksHelper
-
- let(:user) { create(:user) }
- let(:project) { create(:project, :public, :repository) }
- let(:forked_project) { fork_project(project, user, repository: true) }
- let(:unlink_project) { Projects::UnlinkForkService.new(forked_project, user) }
- let(:note) { create(:note_on_merge_request, project: project, noteable: closed_merge_request) }
-
- let(:closed_merge_request) do
- create(:closed_merge_request,
- source_project: forked_project,
- target_project: project,
- author: user)
- end
-
- def preload_view_requirements
- # This will load the status fields of the author of the note and merge request
- # to avoid queries in when rendering the view being tested.
- closed_merge_request.author.status
- note.author.status
- end
-
- before do
- assign(:project, project)
- assign(:merge_request, closed_merge_request)
- assign(:commits_count, 0)
- assign(:note, note)
- assign(:noteable, closed_merge_request)
- assign(:notes, [])
- assign(:pipelines, Ci::Pipeline.none)
- assign(:issuable_sidebar, serialize_issuable_sidebar(user, project, closed_merge_request))
-
- preload_view_requirements
-
- allow(view).to receive_messages(current_user: user,
- can?: true,
- current_application_settings: Gitlab::CurrentSettings.current_application_settings)
- end
+ include_context 'merge request show action'
describe 'merge request assignee sidebar' do
context 'when assignee is allowed to merge' do
@@ -92,24 +54,4 @@ describe 'projects/merge_requests/show.html.haml' do
expect(rendered).to have_css('a', visible: false, text: 'Close')
end
end
-
- context 'when merge request is created by a GitLab team member' do
- let(:user) { create(:user, email: 'test@gitlab.com') }
-
- before do
- allow(Gitlab).to receive(:com?).and_return(true)
- end
-
- it 'renders an employee badge next to their name' do
- render
-
- expect(rendered).to have_selector('[aria-label="GitLab Team Member"]')
- end
- end
-
- def serialize_issuable_sidebar(user, project, merge_request)
- MergeRequestSerializer
- .new(current_user: user, project: project)
- .represent(closed_merge_request, serializer: 'sidebar')
- end
end
diff --git a/spec/views/projects/services/_form.haml_spec.rb b/spec/views/projects/services/_form.haml_spec.rb
index 272ac97604a..a3faa92b50e 100644
--- a/spec/views/projects/services/_form.haml_spec.rb
+++ b/spec/views/projects/services/_form.haml_spec.rb
@@ -7,6 +7,8 @@ describe 'projects/services/_form' do
let(:user) { create(:admin) }
before do
+ stub_feature_flags(integration_form_refactor: false)
+
assign(:project, project)
allow(controller).to receive(:current_user).and_return(user)
@@ -29,20 +31,5 @@ describe 'projects/services/_form' do
expect(rendered).to have_content('Event will be triggered when a commit is created/updated')
expect(rendered).to have_content('Event will be triggered when a merge request is created/updated/merged')
end
-
- context 'when service is Jira' do
- let(:project) { create(:jira_project) }
-
- before do
- assign(:service, project.jira_service)
- end
-
- it 'display merge_request_events and commit_events descriptions' do
- render
-
- expect(rendered).to have_content('Jira comments will be created when an issue gets referenced in a commit.')
- expect(rendered).to have_content('Jira comments will be created when an issue gets referenced in a merge request.')
- end
- end
end
end
diff --git a/spec/views/projects/settings/ci_cd/_autodevops_form.html.haml_spec.rb b/spec/views/projects/settings/ci_cd/_autodevops_form.html.haml_spec.rb
index 94a85486cfa..d25860ab301 100644
--- a/spec/views/projects/settings/ci_cd/_autodevops_form.html.haml_spec.rb
+++ b/spec/views/projects/settings/ci_cd/_autodevops_form.html.haml_spec.rb
@@ -13,7 +13,7 @@ describe 'projects/settings/ci_cd/_autodevops_form' do
it 'shows a warning message about Kubernetes cluster' do
render
- expect(rendered).to have_text('You must add a Kubernetes cluster integration to this project with a domain in order for your deployment strategy to work correctly.')
+ expect(rendered).to have_text('Add a Kubernetes cluster integration with a domain or create an AUTO_DEVOPS_PLATFORM_TARGET CI variable')
end
context 'when the project has an available kubernetes cluster' do
diff --git a/spec/workers/authorized_project_update/project_create_worker_spec.rb b/spec/workers/authorized_project_update/project_create_worker_spec.rb
new file mode 100644
index 00000000000..5ebfb60bc79
--- /dev/null
+++ b/spec/workers/authorized_project_update/project_create_worker_spec.rb
@@ -0,0 +1,50 @@
+# frozen_string_literal: true
+
+require 'spec_helper'
+
+describe AuthorizedProjectUpdate::ProjectCreateWorker do
+ let_it_be(:group) { create(:group, :private) }
+ let_it_be(:group_project) { create(:project, group: group) }
+ let_it_be(:group_user) { create(:user) }
+
+ let(:access_level) { Gitlab::Access::MAINTAINER }
+
+ subject(:worker) { described_class.new }
+
+ it 'calls AuthorizedProjectUpdate::ProjectCreateService' do
+ expect_next_instance_of(AuthorizedProjectUpdate::ProjectCreateService) do |service|
+ expect(service).to(receive(:execute))
+ end
+
+ worker.perform(group_project.id)
+ end
+
+ it 'returns ServiceResponse.success' do
+ result = worker.perform(group_project.id)
+
+ expect(result.success?).to be_truthy
+ end
+
+ context 'idempotence' do
+ before do
+ create(:group_member, access_level: Gitlab::Access::MAINTAINER, group: group, user: group_user)
+ ProjectAuthorization.delete_all
+ end
+
+ include_examples 'an idempotent worker' do
+ let(:job_args) { group_project.id }
+
+ it 'creates project authorization' do
+ subject
+
+ project_authorization = ProjectAuthorization.where(
+ project_id: group_project.id,
+ user_id: group_user.id,
+ access_level: access_level)
+
+ expect(project_authorization).to exist
+ expect(ProjectAuthorization.count).to eq(1)
+ end
+ end
+ end
+end
diff --git a/spec/workers/authorized_project_update/user_refresh_with_low_urgency_worker_spec.rb b/spec/workers/authorized_project_update/user_refresh_with_low_urgency_worker_spec.rb
new file mode 100644
index 00000000000..fa029dae0fa
--- /dev/null
+++ b/spec/workers/authorized_project_update/user_refresh_with_low_urgency_worker_spec.rb
@@ -0,0 +1,11 @@
+# frozen_string_literal: true
+
+require 'spec_helper'
+
+describe AuthorizedProjectUpdate::UserRefreshWithLowUrgencyWorker do
+ it 'is labeled as low urgency' do
+ expect(described_class.get_urgency).to eq(:low)
+ end
+
+ it_behaves_like "refreshes user's project authorizations"
+end
diff --git a/spec/workers/authorized_projects_worker_spec.rb b/spec/workers/authorized_projects_worker_spec.rb
index 8ce0d4edd4f..93f22471c56 100644
--- a/spec/workers/authorized_projects_worker_spec.rb
+++ b/spec/workers/authorized_projects_worker_spec.rb
@@ -3,40 +3,5 @@
require 'spec_helper'
describe AuthorizedProjectsWorker do
- describe '#perform' do
- let(:user) { create(:user) }
-
- subject(:job) { described_class.new }
-
- it "refreshes user's authorized projects" do
- expect_any_instance_of(User).to receive(:refresh_authorized_projects)
-
- job.perform(user.id)
- end
-
- context "when the user is not found" do
- it "does nothing" do
- expect_any_instance_of(User).not_to receive(:refresh_authorized_projects)
-
- job.perform(-1)
- end
- end
-
- it_behaves_like "an idempotent worker" do
- let(:job_args) { user.id }
-
- it "does not change authorizations when run twice" do
- group = create(:group)
- create(:project, namespace: group)
- group.add_developer(user)
-
- # Delete the authorization created by the after save hook of the member
- # created above.
- user.project_authorizations.delete_all
-
- expect { job.perform(user.id) }.to change { user.project_authorizations.reload.size }.by(1)
- expect { job.perform(user.id) }.not_to change { user.project_authorizations.reload.size }
- end
- end
- end
+ it_behaves_like "refreshes user's project authorizations"
end
diff --git a/spec/workers/ci/daily_build_group_report_results_worker_spec.rb b/spec/workers/ci/daily_build_group_report_results_worker_spec.rb
new file mode 100644
index 00000000000..d9706982a62
--- /dev/null
+++ b/spec/workers/ci/daily_build_group_report_results_worker_spec.rb
@@ -0,0 +1,34 @@
+# frozen_string_literal: true
+
+require 'spec_helper'
+
+describe Ci::DailyBuildGroupReportResultsWorker do
+ describe '#perform' do
+ let!(:pipeline) { create(:ci_pipeline) }
+
+ subject { described_class.new.perform(pipeline_id) }
+
+ context 'when pipeline is found' do
+ let(:pipeline_id) { pipeline.id }
+
+ it 'executes service' do
+ expect_any_instance_of(Ci::DailyBuildGroupReportResultService)
+ .to receive(:execute).with(pipeline)
+
+ subject
+ end
+ end
+
+ context 'when pipeline is not found' do
+ let(:pipeline_id) { 123 }
+
+ it 'does not execute service' do
+ expect_any_instance_of(Ci::DailyBuildGroupReportResultService)
+ .not_to receive(:execute)
+
+ expect { subject }
+ .not_to raise_error
+ end
+ end
+ end
+end
diff --git a/spec/workers/ci/daily_report_results_worker_spec.rb b/spec/workers/ci/daily_report_results_worker_spec.rb
deleted file mode 100644
index b6543b32b09..00000000000
--- a/spec/workers/ci/daily_report_results_worker_spec.rb
+++ /dev/null
@@ -1,34 +0,0 @@
-# frozen_string_literal: true
-
-require 'spec_helper'
-
-describe Ci::DailyReportResultsWorker do
- describe '#perform' do
- let!(:pipeline) { create(:ci_pipeline) }
-
- subject { described_class.new.perform(pipeline_id) }
-
- context 'when pipeline is found' do
- let(:pipeline_id) { pipeline.id }
-
- it 'executes service' do
- expect_any_instance_of(Ci::DailyReportResultService)
- .to receive(:execute).with(pipeline)
-
- subject
- end
- end
-
- context 'when pipeline is not found' do
- let(:pipeline_id) { 123 }
-
- it 'does not execute service' do
- expect_any_instance_of(Ci::DailyReportResultService)
- .not_to receive(:execute)
-
- expect { subject }
- .not_to raise_error
- end
- end
- end
-end
diff --git a/spec/workers/concerns/application_worker_spec.rb b/spec/workers/concerns/application_worker_spec.rb
index 2fbaaf1131f..ae311a54cd1 100644
--- a/spec/workers/concerns/application_worker_spec.rb
+++ b/spec/workers/concerns/application_worker_spec.rb
@@ -21,6 +21,21 @@ describe ApplicationWorker do
end
end
+ describe '#logging_extras' do
+ it 'returns extra data to be logged that was set from #log_extra_metadata_on_done' do
+ instance.log_extra_metadata_on_done(:key1, "value1")
+ instance.log_extra_metadata_on_done(:key2, "value2")
+
+ expect(instance.logging_extras).to eq({ 'extra.gitlab_foo_bar_dummy_worker.key1' => "value1", 'extra.gitlab_foo_bar_dummy_worker.key2' => "value2" })
+ end
+
+ context 'when nothing is set' do
+ it 'returns {}' do
+ expect(instance.logging_extras).to eq({})
+ end
+ end
+ end
+
describe '#structured_payload' do
let(:payload) { {} }
diff --git a/spec/workers/create_commit_signature_worker_spec.rb b/spec/workers/create_commit_signature_worker_spec.rb
index f40482f2361..fd5d99b3265 100644
--- a/spec/workers/create_commit_signature_worker_spec.rb
+++ b/spec/workers/create_commit_signature_worker_spec.rb
@@ -17,6 +17,25 @@ describe CreateCommitSignatureWorker do
subject { described_class.new.perform(commit_shas, project.id) }
context 'when a signature is found' do
+ it_behaves_like 'an idempotent worker' do
+ let(:job_args) { [commit_shas, project.id] }
+
+ before do
+ # Removing the stub which can cause bugs for multiple calls to
+ # Project#commits_by.
+ allow(project).to receive(:commits_by).and_call_original
+
+ # Making sure it still goes through all the perform execution.
+ allow_next_instance_of(::Commit) do |commit|
+ allow(commit).to receive(:signature_type).and_return(:PGP)
+ end
+
+ allow_next_instance_of(::Gitlab::Gpg::Commit) do |gpg|
+ expect(gpg).to receive(:signature).once.and_call_original
+ end
+ end
+ end
+
it 'calls Gitlab::Gpg::Commit#signature' do
commits.each do |commit|
allow(commit).to receive(:signature_type).and_return(:PGP)
diff --git a/spec/workers/design_management/new_version_worker_spec.rb b/spec/workers/design_management/new_version_worker_spec.rb
new file mode 100644
index 00000000000..ef7cd8de108
--- /dev/null
+++ b/spec/workers/design_management/new_version_worker_spec.rb
@@ -0,0 +1,72 @@
+# frozen_string_literal: true
+
+require 'spec_helper'
+
+describe DesignManagement::NewVersionWorker do
+ describe '#perform' do
+ let(:worker) { described_class.new }
+
+ context 'the id is wrong or out-of-date' do
+ let(:version_id) { -1 }
+
+ it 'does not create system notes' do
+ expect(SystemNoteService).not_to receive(:design_version_added)
+
+ worker.perform(version_id)
+ end
+
+ it 'does not invoke GenerateImageVersionsService' do
+ expect(DesignManagement::GenerateImageVersionsService).not_to receive(:new)
+
+ worker.perform(version_id)
+ end
+
+ it 'logs the reason for this failure' do
+ expect(Sidekiq.logger).to receive(:warn)
+ .with(an_instance_of(ActiveRecord::RecordNotFound))
+
+ worker.perform(version_id)
+ end
+ end
+
+ context 'the version id is valid' do
+ let_it_be(:version) { create(:design_version, :with_lfs_file, designs_count: 2) }
+
+ it 'creates a system note' do
+ expect { worker.perform(version.id) }.to change { Note.system.count }.by(1)
+ end
+
+ it 'invokes GenerateImageVersionsService' do
+ expect_next_instance_of(DesignManagement::GenerateImageVersionsService) do |service|
+ expect(service).to receive(:execute)
+ end
+
+ worker.perform(version.id)
+ end
+
+ it 'does not log anything' do
+ expect(Sidekiq.logger).not_to receive(:warn)
+
+ worker.perform(version.id)
+ end
+ end
+
+ context 'the version includes multiple types of action' do
+ let_it_be(:version) do
+ create(:design_version, :with_lfs_file,
+ created_designs: create_list(:design, 1, :with_lfs_file),
+ modified_designs: create_list(:design, 1))
+ end
+
+ it 'creates two system notes' do
+ expect { worker.perform(version.id) }.to change { Note.system.count }.by(2)
+ end
+
+ it 'calls design_version_added' do
+ expect(SystemNoteService).to receive(:design_version_added).with(version)
+
+ worker.perform(version.id)
+ end
+ end
+ end
+end
diff --git a/spec/workers/external_service_reactive_caching_worker_spec.rb b/spec/workers/external_service_reactive_caching_worker_spec.rb
new file mode 100644
index 00000000000..45cce71b75b
--- /dev/null
+++ b/spec/workers/external_service_reactive_caching_worker_spec.rb
@@ -0,0 +1,7 @@
+# frozen_string_literal: true
+
+require 'spec_helper'
+
+describe ExternalServiceReactiveCachingWorker do
+ it_behaves_like 'reactive cacheable worker'
+end
diff --git a/spec/workers/gitlab/jira_import/import_issue_worker_spec.rb b/spec/workers/gitlab/jira_import/import_issue_worker_spec.rb
index 80629cb875e..2de609761e2 100644
--- a/spec/workers/gitlab/jira_import/import_issue_worker_spec.rb
+++ b/spec/workers/gitlab/jira_import/import_issue_worker_spec.rb
@@ -5,6 +5,8 @@ require 'spec_helper'
describe Gitlab::JiraImport::ImportIssueWorker do
let_it_be(:user) { create(:user) }
let_it_be(:project) { create(:project) }
+ let_it_be(:jira_issue_label_1) { create(:label, project: project) }
+ let_it_be(:jira_issue_label_2) { create(:label, project: project) }
let(:some_key) { 'some-key' }
describe 'modules' do
@@ -17,7 +19,13 @@ describe Gitlab::JiraImport::ImportIssueWorker do
subject { described_class.new }
describe '#perform', :clean_gitlab_redis_cache do
- let(:issue_attrs) { build(:issue, project_id: project.id).as_json.compact }
+ let(:assignee_ids) { [user.id] }
+ let(:issue_attrs) do
+ build(:issue, project_id: project.id, title: 'jira issue')
+ .as_json.merge(
+ 'label_ids' => [jira_issue_label_1.id, jira_issue_label_2.id], 'assignee_ids' => assignee_ids
+ ).compact
+ end
context 'when any exception raised while inserting to DB' do
before do
@@ -47,14 +55,39 @@ describe Gitlab::JiraImport::ImportIssueWorker do
context 'when import label exists' do
before do
Gitlab::JiraImport.cache_import_label_id(project.id, label.id)
- end
- it 'does not record import failure' do
subject.perform(project.id, 123, issue_attrs, some_key)
+ end
+ it 'does not record import failure' do
expect(label.issues.count).to eq(1)
expect(Gitlab::Cache::Import::Caching.read(Gitlab::JiraImport.failed_issues_counter_cache_key(project.id)).to_i).to eq(0)
end
+
+ it 'creates an issue with the correct attributes' do
+ issue = Issue.last
+
+ expect(issue.title).to eq('jira issue')
+ expect(issue.project).to eq(project)
+ expect(issue.labels).to match_array([label, jira_issue_label_1, jira_issue_label_2])
+ expect(issue.assignees).to eq([user])
+ end
+
+ context 'when assignee_ids is nil' do
+ let(:assignee_ids) { nil }
+
+ it 'creates an issue without assignee' do
+ expect(Issue.last.assignees).to be_empty
+ end
+ end
+
+ context 'when assignee_ids is an empty array' do
+ let(:assignee_ids) { [] }
+
+ it 'creates an issue without assignee' do
+ expect(Issue.last.assignees).to be_empty
+ end
+ end
end
end
end
diff --git a/spec/workers/gitlab/jira_import/stage/finish_import_worker_spec.rb b/spec/workers/gitlab/jira_import/stage/finish_import_worker_spec.rb
index 5c3c7dcccc1..4cb6f5e28b8 100644
--- a/spec/workers/gitlab/jira_import/stage/finish_import_worker_spec.rb
+++ b/spec/workers/gitlab/jira_import/stage/finish_import_worker_spec.rb
@@ -16,7 +16,7 @@ describe Gitlab::JiraImport::Stage::FinishImportWorker do
stub_feature_flags(jira_issue_import: false)
end
- it_behaves_like 'cannot do jira import'
+ it_behaves_like 'cannot do Jira import'
end
context 'when feature flag enabled' do
@@ -27,7 +27,7 @@ describe Gitlab::JiraImport::Stage::FinishImportWorker do
end
context 'when import did not start' do
- it_behaves_like 'cannot do jira import'
+ it_behaves_like 'cannot do Jira import'
end
context 'when import started' do
diff --git a/spec/workers/gitlab/jira_import/stage/import_attachments_worker_spec.rb b/spec/workers/gitlab/jira_import/stage/import_attachments_worker_spec.rb
index 478cb447dc5..e6d41ae8bb4 100644
--- a/spec/workers/gitlab/jira_import/stage/import_attachments_worker_spec.rb
+++ b/spec/workers/gitlab/jira_import/stage/import_attachments_worker_spec.rb
@@ -15,7 +15,7 @@ describe Gitlab::JiraImport::Stage::ImportAttachmentsWorker do
stub_feature_flags(jira_issue_import: false)
end
- it_behaves_like 'cannot do jira import'
+ it_behaves_like 'cannot do Jira import'
it_behaves_like 'does not advance to next stage'
end
@@ -27,7 +27,7 @@ describe Gitlab::JiraImport::Stage::ImportAttachmentsWorker do
end
context 'when import did not start' do
- it_behaves_like 'cannot do jira import'
+ it_behaves_like 'cannot do Jira import'
it_behaves_like 'does not advance to next stage'
end
diff --git a/spec/workers/gitlab/jira_import/stage/import_issues_worker_spec.rb b/spec/workers/gitlab/jira_import/stage/import_issues_worker_spec.rb
index 6470a293461..f2067522af4 100644
--- a/spec/workers/gitlab/jira_import/stage/import_issues_worker_spec.rb
+++ b/spec/workers/gitlab/jira_import/stage/import_issues_worker_spec.rb
@@ -3,6 +3,8 @@
require 'spec_helper'
describe Gitlab::JiraImport::Stage::ImportIssuesWorker do
+ include JiraServiceHelper
+
let_it_be(:user) { create(:user) }
let_it_be(:project) { create(:project, import_type: 'jira') }
@@ -16,7 +18,7 @@ describe Gitlab::JiraImport::Stage::ImportIssuesWorker do
stub_feature_flags(jira_issue_import: false)
end
- it_behaves_like 'cannot do jira import'
+ it_behaves_like 'cannot do Jira import'
it_behaves_like 'does not advance to next stage'
end
@@ -25,10 +27,11 @@ describe Gitlab::JiraImport::Stage::ImportIssuesWorker do
before do
stub_feature_flags(jira_issue_import: true)
+ stub_jira_service_test
end
context 'when import did not start' do
- it_behaves_like 'cannot do jira import'
+ it_behaves_like 'cannot do Jira import'
it_behaves_like 'does not advance to next stage'
end
diff --git a/spec/workers/gitlab/jira_import/stage/import_labels_worker_spec.rb b/spec/workers/gitlab/jira_import/stage/import_labels_worker_spec.rb
index f1562395546..7f289de5422 100644
--- a/spec/workers/gitlab/jira_import/stage/import_labels_worker_spec.rb
+++ b/spec/workers/gitlab/jira_import/stage/import_labels_worker_spec.rb
@@ -3,6 +3,8 @@
require 'spec_helper'
describe Gitlab::JiraImport::Stage::ImportLabelsWorker do
+ include JiraServiceHelper
+
let_it_be(:user) { create(:user) }
let_it_be(:project) { create(:project, import_type: 'jira') }
@@ -16,7 +18,7 @@ describe Gitlab::JiraImport::Stage::ImportLabelsWorker do
stub_feature_flags(jira_issue_import: false)
end
- it_behaves_like 'cannot do jira import'
+ it_behaves_like 'cannot do Jira import'
it_behaves_like 'does not advance to next stage'
end
@@ -28,7 +30,7 @@ describe Gitlab::JiraImport::Stage::ImportLabelsWorker do
end
context 'when import did not start' do
- it_behaves_like 'cannot do jira import'
+ it_behaves_like 'cannot do Jira import'
it_behaves_like 'does not advance to next stage'
end
@@ -36,7 +38,12 @@ describe Gitlab::JiraImport::Stage::ImportLabelsWorker do
let!(:jira_service) { create(:jira_service, project: project) }
before do
+ stub_jira_service_test
+
jira_import.start!
+
+ WebMock.stub_request(:get, 'https://jira.example.com/rest/api/2/label?maxResults=500&startAt=0')
+ .to_return(body: {}.to_json )
end
it_behaves_like 'advance to next stage', :issues
diff --git a/spec/workers/gitlab/jira_import/stage/import_notes_worker_spec.rb b/spec/workers/gitlab/jira_import/stage/import_notes_worker_spec.rb
index 956898c1abc..f9bdbd669d8 100644
--- a/spec/workers/gitlab/jira_import/stage/import_notes_worker_spec.rb
+++ b/spec/workers/gitlab/jira_import/stage/import_notes_worker_spec.rb
@@ -15,7 +15,7 @@ describe Gitlab::JiraImport::Stage::ImportNotesWorker do
stub_feature_flags(jira_issue_import: false)
end
- it_behaves_like 'cannot do jira import'
+ it_behaves_like 'cannot do Jira import'
it_behaves_like 'does not advance to next stage'
end
@@ -27,7 +27,7 @@ describe Gitlab::JiraImport::Stage::ImportNotesWorker do
end
context 'when import did not start' do
- it_behaves_like 'cannot do jira import'
+ it_behaves_like 'cannot do Jira import'
it_behaves_like 'does not advance to next stage'
end
diff --git a/spec/workers/group_import_worker_spec.rb b/spec/workers/group_import_worker_spec.rb
index 641aa45c9b0..bb7dc116a08 100644
--- a/spec/workers/group_import_worker_spec.rb
+++ b/spec/workers/group_import_worker_spec.rb
@@ -8,13 +8,52 @@ describe GroupImportWorker do
subject { described_class.new }
+ before do
+ allow_next_instance_of(described_class) do |job|
+ allow(job).to receive(:jid).and_return(SecureRandom.hex(8))
+ end
+ end
+
describe '#perform' do
context 'when it succeeds' do
- it 'calls the ImportService' do
- expect_any_instance_of(::Groups::ImportExport::ImportService).to receive(:execute)
+ before do
+ expect_next_instance_of(::Groups::ImportExport::ImportService) do |service|
+ expect(service).to receive(:execute)
+ end
+ end
+ it 'calls the ImportService' do
subject.perform(user.id, group.id)
end
+
+ context 'import state' do
+ it 'creates group import' do
+ expect(group.import_state).to be_nil
+
+ subject.perform(user.id, group.id)
+ import_state = group.reload.import_state
+
+ expect(import_state).to be_instance_of(GroupImportState)
+ expect(import_state.status_name).to eq(:finished)
+ expect(import_state.jid).not_to be_empty
+ end
+
+ it 'sets the group import status to started' do
+ expect_next_instance_of(GroupImportState) do |import|
+ expect(import).to receive(:start!).and_call_original
+ end
+
+ subject.perform(user.id, group.id)
+ end
+
+ it 'sets the group import status to finished' do
+ expect_next_instance_of(GroupImportState) do |import|
+ expect(import).to receive(:finish!).and_call_original
+ end
+
+ subject.perform(user.id, group.id)
+ end
+ end
end
context 'when it fails' do
@@ -24,6 +63,22 @@ describe GroupImportWorker do
expect { subject.perform(non_existing_record_id, group.id) }.to raise_exception(ActiveRecord::RecordNotFound)
expect { subject.perform(user.id, non_existing_record_id) }.to raise_exception(ActiveRecord::RecordNotFound)
end
+
+ context 'import state' do
+ before do
+ expect_next_instance_of(::Groups::ImportExport::ImportService) do |service|
+ expect(service).to receive(:execute).and_raise(Gitlab::ImportExport::Error)
+ end
+ end
+
+ it 'sets the group import status to failed' do
+ expect_next_instance_of(GroupImportState) do |import|
+ expect(import).to receive(:fail_op).and_call_original
+ end
+
+ expect { subject.perform(user.id, group.id) }.to raise_exception(Gitlab::ImportExport::Error)
+ end
+ end
end
end
end
diff --git a/spec/workers/incident_management/process_alert_worker_spec.rb b/spec/workers/incident_management/process_alert_worker_spec.rb
index 9f40833dfd7..938e72aa0f0 100644
--- a/spec/workers/incident_management/process_alert_worker_spec.rb
+++ b/spec/workers/incident_management/process_alert_worker_spec.rb
@@ -6,16 +6,24 @@ describe IncidentManagement::ProcessAlertWorker do
let_it_be(:project) { create(:project) }
describe '#perform' do
- let(:alert) { :alert }
- let(:create_issue_service) { spy(:create_issue_service) }
+ let(:alert_management_alert_id) { nil }
+ let(:alert_payload) { { alert: 'payload' } }
+ let(:new_issue) { create(:issue, project: project) }
+ let(:create_issue_service) { instance_double(IncidentManagement::CreateIssueService, execute: new_issue) }
- subject { described_class.new.perform(project.id, alert) }
+ subject { described_class.new.perform(project.id, alert_payload, alert_management_alert_id) }
+
+ before do
+ allow(IncidentManagement::CreateIssueService)
+ .to receive(:new).with(project, alert_payload)
+ .and_return(create_issue_service)
+ end
it 'calls create issue service' do
expect(Project).to receive(:find_by_id).and_call_original
expect(IncidentManagement::CreateIssueService)
- .to receive(:new).with(project, :alert)
+ .to receive(:new).with(project, alert_payload)
.and_return(create_issue_service)
expect(create_issue_service).to receive(:execute)
@@ -26,7 +34,7 @@ describe IncidentManagement::ProcessAlertWorker do
context 'with invalid project' do
let(:invalid_project_id) { 0 }
- subject { described_class.new.perform(invalid_project_id, alert) }
+ subject { described_class.new.perform(invalid_project_id, alert_payload) }
it 'does not create issues' do
expect(Project).to receive(:find_by_id).and_call_original
@@ -35,5 +43,54 @@ describe IncidentManagement::ProcessAlertWorker do
subject
end
end
+
+ context 'when alert_management_alert_id is present' do
+ let!(:alert) { create(:alert_management_alert, project: project) }
+ let(:alert_management_alert_id) { alert.id }
+
+ before do
+ allow(AlertManagement::Alert)
+ .to receive(:find_by_id)
+ .with(alert_management_alert_id)
+ .and_return(alert)
+
+ allow(Gitlab::AppLogger).to receive(:warn).and_call_original
+ end
+
+ context 'when alert can be updated' do
+ it 'updates AlertManagement::Alert#issue_id' do
+ expect { subject }.to change { alert.reload.issue_id }.to(new_issue.id)
+ end
+
+ it 'does not write a warning to log' do
+ subject
+
+ expect(Gitlab::AppLogger).not_to have_received(:warn)
+ end
+ end
+
+ context 'when alert cannot be updated' do
+ before do
+ # invalidate alert
+ too_many_hosts = Array.new(AlertManagement::Alert::HOSTS_MAX_LENGTH + 1) { |_| 'host' }
+ alert.update_columns(hosts: too_many_hosts)
+ end
+
+ it 'updates AlertManagement::Alert#issue_id' do
+ expect { subject }.not_to change { alert.reload.issue_id }
+ end
+
+ it 'writes a worning to log' do
+ subject
+
+ expect(Gitlab::AppLogger).to have_received(:warn).with(
+ message: 'Cannot link an Issue with Alert',
+ issue_id: new_issue.id,
+ alert_id: alert_management_alert_id,
+ alert_errors: { hosts: ['hosts array is over 255 chars'] }
+ )
+ end
+ end
+ end
end
end
diff --git a/spec/workers/merge_request_mergeability_check_worker_spec.rb b/spec/workers/merge_request_mergeability_check_worker_spec.rb
index 2331664215f..8909af1f685 100644
--- a/spec/workers/merge_request_mergeability_check_worker_spec.rb
+++ b/spec/workers/merge_request_mergeability_check_worker_spec.rb
@@ -25,5 +25,16 @@ describe MergeRequestMergeabilityCheckWorker do
subject.perform(merge_request.id)
end
end
+
+ it_behaves_like 'an idempotent worker' do
+ let(:merge_request) { create(:merge_request) }
+ let(:job_args) { [merge_request.id] }
+
+ it 'is mergeable' do
+ subject
+
+ expect(merge_request).to be_mergeable
+ end
+ end
end
end
diff --git a/spec/workers/namespaceless_project_destroy_worker_spec.rb b/spec/workers/namespaceless_project_destroy_worker_spec.rb
index b97a44c714d..ceea7c8d8f5 100644
--- a/spec/workers/namespaceless_project_destroy_worker_spec.rb
+++ b/spec/workers/namespaceless_project_destroy_worker_spec.rb
@@ -79,19 +79,5 @@ describe NamespacelessProjectDestroyWorker do
end
end
end
-
- context 'project has non-existing namespace' do
- let!(:project) do
- project = build(:project, namespace_id: non_existing_record_id)
- project.save(validate: false)
- project
- end
-
- it 'deletes the project' do
- subject.perform(project.id)
-
- expect(Project.unscoped.all).not_to include(project)
- end
- end
end
end
diff --git a/spec/workers/new_release_worker_spec.rb b/spec/workers/new_release_worker_spec.rb
index 9d8c5bbf919..de4e1bac48f 100644
--- a/spec/workers/new_release_worker_spec.rb
+++ b/spec/workers/new_release_worker_spec.rb
@@ -1,5 +1,7 @@
# frozen_string_literal: true
+# TODO: Worker can be removed in 13.2:
+# https://gitlab.com/gitlab-org/gitlab/-/issues/218231
require 'spec_helper'
describe NewReleaseWorker do
diff --git a/spec/workers/post_receive_spec.rb b/spec/workers/post_receive_spec.rb
index 3d24b5f753a..aab7a36189a 100644
--- a/spec/workers/post_receive_spec.rb
+++ b/spec/workers/post_receive_spec.rb
@@ -299,6 +299,31 @@ describe PostReceive do
end
end
+ context "master" do
+ let(:default_branch) { 'master' }
+ let(:oldrev) { '012345' }
+ let(:newrev) { '6789ab' }
+ let(:changes) do
+ <<~EOF
+ #{oldrev} #{newrev} refs/heads/#{default_branch}
+ 123456 789012 refs/heads/tést2
+ EOF
+ end
+
+ let(:raw_repo) { double('RawRepo') }
+
+ it 'processes the changes on the master branch' do
+ expect_next_instance_of(Git::WikiPushService) do |service|
+ expect(service).to receive(:process_changes).and_call_original
+ end
+ expect(project.wiki).to receive(:default_branch).twice.and_return(default_branch)
+ expect(project.wiki.repository).to receive(:raw).and_return(raw_repo)
+ expect(raw_repo).to receive(:raw_changes_between).once.with(oldrev, newrev).and_return([])
+
+ perform
+ end
+ end
+
context "branches" do
let(:changes) do
<<~EOF
@@ -307,6 +332,12 @@ describe PostReceive do
EOF
end
+ before do
+ allow_next_instance_of(Git::WikiPushService) do |service|
+ allow(service).to receive(:process_changes)
+ end
+ end
+
it 'expires the branches cache' do
expect(project.wiki.repository).to receive(:expire_branches_cache).once
@@ -440,4 +471,17 @@ describe PostReceive do
it_behaves_like 'snippet changes actions'
end
end
+
+ describe 'processing design changes' do
+ let(:gl_repository) { "design-#{project.id}" }
+
+ it 'does not do anything' do
+ worker = described_class.new
+
+ expect(worker).not_to receive(:process_wiki_changes)
+ expect(worker).not_to receive(:process_project_changes)
+
+ described_class.new.perform(gl_repository, key_id, base64_changes)
+ end
+ end
end
diff --git a/spec/workers/process_commit_worker_spec.rb b/spec/workers/process_commit_worker_spec.rb
index 21c300af7ac..d247668ac76 100644
--- a/spec/workers/process_commit_worker_spec.rb
+++ b/spec/workers/process_commit_worker_spec.rb
@@ -22,16 +22,26 @@ describe ProcessCommitWorker do
worker.perform(project.id, -1, commit.to_hash)
end
- it 'processes the commit message' do
- expect(worker).to receive(:process_commit_message).and_call_original
+ include_examples 'an idempotent worker' do
+ subject do
+ perform_multiple([project.id, user.id, commit.to_hash], worker: worker)
+ end
- worker.perform(project.id, user.id, commit.to_hash)
- end
+ it 'processes the commit message' do
+ expect(worker).to receive(:process_commit_message)
+ .exactly(IdempotentWorkerHelper::WORKER_EXEC_TIMES)
+ .and_call_original
- it 'updates the issue metrics' do
- expect(worker).to receive(:update_issue_metrics).and_call_original
+ subject
+ end
- worker.perform(project.id, user.id, commit.to_hash)
+ it 'updates the issue metrics' do
+ expect(worker).to receive(:update_issue_metrics)
+ .exactly(IdempotentWorkerHelper::WORKER_EXEC_TIMES)
+ .and_call_original
+
+ subject
+ end
end
end
diff --git a/spec/workers/project_export_worker_spec.rb b/spec/workers/project_export_worker_spec.rb
index 373e7f32530..4c49939d34e 100644
--- a/spec/workers/project_export_worker_spec.rb
+++ b/spec/workers/project_export_worker_spec.rb
@@ -17,14 +17,18 @@ describe ProjectExportWorker do
context 'when it succeeds' do
it 'calls the ExportService' do
- expect_any_instance_of(::Projects::ImportExport::ExportService).to receive(:execute)
+ expect_next_instance_of(::Projects::ImportExport::ExportService) do |service|
+ expect(service).to receive(:execute)
+ end
subject.perform(user.id, project.id, { 'klass' => 'Gitlab::ImportExport::AfterExportStrategies::DownloadNotificationStrategy' })
end
context 'export job' do
before do
- allow_any_instance_of(::Projects::ImportExport::ExportService).to receive(:execute)
+ allow_next_instance_of(::Projects::ImportExport::ExportService) do |service|
+ allow(service).to receive(:execute)
+ end
end
it 'creates an export job record for the project' do
@@ -51,7 +55,7 @@ describe ProjectExportWorker do
context 'when it fails' do
it 'does not raise an exception when strategy is invalid' do
- expect_any_instance_of(::Projects::ImportExport::ExportService).not_to receive(:execute)
+ expect(::Projects::ImportExport::ExportService).not_to receive(:new)
expect { subject.perform(user.id, project.id, { 'klass' => 'Whatever' }) }.not_to raise_error
end
diff --git a/spec/workers/project_update_repository_storage_worker_spec.rb b/spec/workers/project_update_repository_storage_worker_spec.rb
index 57a4c2128b3..98856480b21 100644
--- a/spec/workers/project_update_repository_storage_worker_spec.rb
+++ b/spec/workers/project_update_repository_storage_worker_spec.rb
@@ -9,12 +9,40 @@ describe ProjectUpdateRepositoryStorageWorker do
subject { described_class.new }
describe "#perform" do
- it "calls the update repository storage service" do
- expect_next_instance_of(Projects::UpdateRepositoryStorageService) do |instance|
- expect(instance).to receive(:execute).with('new_storage')
+ let(:service) { double(:update_repository_storage_service) }
+
+ before do
+ allow(Gitlab.config.repositories.storages).to receive(:keys).and_return(%w[default test_second_storage])
+ end
+
+ context 'without repository storage move' do
+ it "calls the update repository storage service" do
+ expect(Projects::UpdateRepositoryStorageService).to receive(:new).and_return(service)
+ expect(service).to receive(:execute)
+
+ expect do
+ subject.perform(project.id, 'test_second_storage')
+ end.to change(ProjectRepositoryStorageMove, :count).by(1)
+
+ storage_move = project.repository_storage_moves.last
+ expect(storage_move).to have_attributes(
+ source_storage_name: "default",
+ destination_storage_name: "test_second_storage"
+ )
end
+ end
+
+ context 'with repository storage move' do
+ let!(:repository_storage_move) { create(:project_repository_storage_move) }
- subject.perform(project.id, 'new_storage')
+ it "calls the update repository storage service" do
+ expect(Projects::UpdateRepositoryStorageService).to receive(:new).and_return(service)
+ expect(service).to receive(:execute)
+
+ expect do
+ subject.perform(nil, nil, repository_storage_move.id)
+ end.not_to change(ProjectRepositoryStorageMove, :count)
+ end
end
end
end
diff --git a/spec/workers/reactive_caching_worker_spec.rb b/spec/workers/reactive_caching_worker_spec.rb
index 603ce6160ce..dcb804a7e6e 100644
--- a/spec/workers/reactive_caching_worker_spec.rb
+++ b/spec/workers/reactive_caching_worker_spec.rb
@@ -3,47 +3,5 @@
require 'spec_helper'
describe ReactiveCachingWorker do
- describe '#perform' do
- context 'when user configured kubernetes from CI/CD > Clusters' do
- let!(:cluster) { create(:cluster, :project, :provided_by_gcp) }
- let(:project) { cluster.project }
- let!(:environment) { create(:environment, project: project) }
-
- it 'calls #exclusively_update_reactive_cache!' do
- expect_any_instance_of(Environment).to receive(:exclusively_update_reactive_cache!)
-
- described_class.new.perform("Environment", environment.id)
- end
-
- context 'when ReactiveCaching::ExceededReactiveCacheLimit is raised' do
- it 'avoids failing the job and tracks via Gitlab::ErrorTracking' do
- allow_any_instance_of(Environment).to receive(:exclusively_update_reactive_cache!)
- .and_raise(ReactiveCaching::ExceededReactiveCacheLimit)
-
- expect(Gitlab::ErrorTracking).to receive(:track_exception)
- .with(kind_of(ReactiveCaching::ExceededReactiveCacheLimit))
-
- described_class.new.perform("Environment", environment.id)
- end
- end
- end
- end
-
- describe 'worker context' do
- it 'sets the related class on the job' do
- described_class.perform_async('Environment', 1, 'other', 'argument')
-
- scheduled_job = described_class.jobs.first
-
- expect(scheduled_job).to include('meta.related_class' => 'Environment')
- end
-
- it 'sets the related class on the job when it was passed as a class' do
- described_class.perform_async(Project, 1, 'other', 'argument')
-
- scheduled_job = described_class.jobs.first
-
- expect(scheduled_job).to include('meta.related_class' => 'Project')
- end
- end
+ it_behaves_like 'reactive cacheable worker'
end
diff --git a/spec/workers/stage_update_worker_spec.rb b/spec/workers/stage_update_worker_spec.rb
index 8a57cc6bbff..dc7158cfd2f 100644
--- a/spec/workers/stage_update_worker_spec.rb
+++ b/spec/workers/stage_update_worker_spec.rb
@@ -12,6 +12,15 @@ describe StageUpdateWorker do
described_class.new.perform(stage.id)
end
+
+ it_behaves_like 'an idempotent worker' do
+ let(:job_args) { [stage.id] }
+
+ it 'results in the stage getting the skipped status' do
+ expect { subject }.to change { stage.reload.status }.from('pending').to('skipped')
+ expect { subject }.not_to change { stage.reload.status }
+ end
+ end
end
context 'when stage does not exist' do
diff --git a/spec/workers/update_head_pipeline_for_merge_request_worker_spec.rb b/spec/workers/update_head_pipeline_for_merge_request_worker_spec.rb
index c4af829a5e2..8fe3f27c8b1 100644
--- a/spec/workers/update_head_pipeline_for_merge_request_worker_spec.rb
+++ b/spec/workers/update_head_pipeline_for_merge_request_worker_spec.rb
@@ -4,18 +4,27 @@ require 'spec_helper'
describe UpdateHeadPipelineForMergeRequestWorker do
describe '#perform' do
- let(:user) { create(:user) }
- let(:project) { create(:project, :repository) }
- let(:merge_request) { create(:merge_request, source_project: project) }
- let(:latest_sha) { 'b83d6e391c22777fca1ed3012fce84f633d7fed0' }
+ let_it_be(:user) { create(:user) }
+ let_it_be(:project) { create(:project, :repository) }
+ let_it_be(:merge_request) { create(:merge_request, source_project: project) }
+ let_it_be(:latest_sha) { 'b83d6e391c22777fca1ed3012fce84f633d7fed0' }
context 'when pipeline exists for the source project and branch' do
- before do
- create(:ci_empty_pipeline, project: project, ref: merge_request.source_branch, sha: latest_sha)
- end
+ let_it_be(:pipeline) { create(:ci_empty_pipeline, project: project, ref: merge_request.source_branch, sha: latest_sha) }
it 'updates the head_pipeline_id of the merge_request' do
- expect { subject.perform(merge_request.id) }.to change { merge_request.reload.head_pipeline_id }
+ expect { subject.perform(merge_request.id) }
+ .to change { merge_request.reload.head_pipeline_id }.from(nil).to(pipeline.id)
+ end
+
+ it_behaves_like 'an idempotent worker' do
+ let(:job_args) { merge_request.id }
+
+ it 'sets the pipeline as the head pipeline when run multiple times' do
+ subject
+
+ expect(merge_request.reload.head_pipeline_id).to eq(pipeline.id)
+ end
end
context 'when merge request sha does not equal pipeline sha' do
@@ -27,6 +36,15 @@ describe UpdateHeadPipelineForMergeRequestWorker do
expect { subject.perform(merge_request.id) }
.not_to change { merge_request.reload.head_pipeline_id }
end
+
+ it_behaves_like 'an idempotent worker' do
+ let(:job_args) { merge_request.id }
+
+ it 'does not update the head_pipeline_id when run multiple times' do
+ expect { subject }
+ .not_to change { merge_request.reload.head_pipeline_id }
+ end
+ end
end
end
@@ -35,10 +53,19 @@ describe UpdateHeadPipelineForMergeRequestWorker do
expect { subject.perform(merge_request.id) }
.not_to change { merge_request.reload.head_pipeline_id }
end
+
+ it_behaves_like 'an idempotent worker' do
+ let(:job_args) { merge_request.id }
+
+ it 'does not update the head_pipeline_id when run multiple times' do
+ expect { subject }
+ .not_to change { merge_request.reload.head_pipeline_id }
+ end
+ end
end
context 'when a merge request pipeline exists' do
- let!(:merge_request_pipeline) do
+ let_it_be(:merge_request_pipeline) do
create(:ci_pipeline,
project: project,
source: :merge_request_event,
@@ -52,6 +79,16 @@ describe UpdateHeadPipelineForMergeRequestWorker do
.from(nil).to(merge_request_pipeline.id)
end
+ it_behaves_like 'an idempotent worker' do
+ let(:job_args) { merge_request.id }
+
+ it 'sets the merge request pipeline as the head pipeline when run multiple times' do
+ subject
+
+ expect(merge_request.reload.head_pipeline_id).to eq(merge_request_pipeline.id)
+ end
+ end
+
context 'when branch pipeline exists' do
let!(:branch_pipeline) do
create(:ci_pipeline, project: project, source: :push, sha: latest_sha)
@@ -62,6 +99,16 @@ describe UpdateHeadPipelineForMergeRequestWorker do
.to change { merge_request.reload.head_pipeline_id }
.from(nil).to(merge_request_pipeline.id)
end
+
+ it_behaves_like 'an idempotent worker' do
+ let(:job_args) { merge_request.id }
+
+ it 'sets the merge request pipeline as the head pipeline when run multiple times' do
+ subject
+
+ expect(merge_request.reload.head_pipeline_id).to eq(merge_request_pipeline.id)
+ end
+ end
end
end
end
diff --git a/spec/workers/update_highest_role_worker_spec.rb b/spec/workers/update_highest_role_worker_spec.rb
index 1e378a5a61e..3f377208a62 100644
--- a/spec/workers/update_highest_role_worker_spec.rb
+++ b/spec/workers/update_highest_role_worker_spec.rb
@@ -18,7 +18,6 @@ describe UpdateHighestRoleWorker, :clean_gitlab_redis_shared_state do
let(:active_attributes) do
{
state: 'active',
- ghost: false,
user_type: nil
}
end
@@ -54,7 +53,6 @@ describe UpdateHighestRoleWorker, :clean_gitlab_redis_shared_state do
where(:additional_attributes) do
[
{ state: 'blocked' },
- { ghost: true },
{ user_type: :alert_bot }
]
end
diff --git a/spec/workers/x509_issuer_crl_check_worker_spec.rb b/spec/workers/x509_issuer_crl_check_worker_spec.rb
new file mode 100644
index 00000000000..f052812b86b
--- /dev/null
+++ b/spec/workers/x509_issuer_crl_check_worker_spec.rb
@@ -0,0 +1,90 @@
+# frozen_string_literal: true
+
+require 'spec_helper'
+
+describe X509IssuerCrlCheckWorker do
+ subject(:worker) { described_class.new }
+
+ let(:project) { create(:project, :public, :repository) }
+ let(:x509_signed_commit) { project.commit_by(oid: '189a6c924013fc3fe40d6f1ec1dc20214183bc97') }
+ let(:revoked_x509_signed_commit) { project.commit_by(oid: 'ed775cc81e5477df30c2abba7b6fdbb5d0baadae') }
+
+ describe '#perform' do
+ context 'valid crl' do
+ before do
+ stub_request(:get, "http://ch.siemens.com/pki?ZZZZZZA6.crl")
+ .to_return(status: 200, body: File.read('spec/fixtures/x509/ZZZZZZA6.crl'), headers: {})
+ end
+
+ it 'changes certificate status for revoked certificates' do
+ revoked_x509_commit = Gitlab::X509::Commit.new(revoked_x509_signed_commit)
+ x509_commit = Gitlab::X509::Commit.new(x509_signed_commit)
+ issuer = revoked_x509_commit.signature.x509_certificate.x509_issuer
+
+ expect(issuer).to eq(x509_commit.signature.x509_certificate.x509_issuer)
+ expect(revoked_x509_commit.signature.x509_certificate.good?).to be_truthy
+ expect(x509_commit.signature.x509_certificate.good?).to be_truthy
+
+ worker.perform
+ revoked_x509_commit.signature.reload
+
+ expect(revoked_x509_commit.signature.x509_certificate.revoked?).to be_truthy
+ expect(x509_commit.signature.x509_certificate.revoked?).to be_falsey
+ end
+ end
+
+ context 'invalid crl' do
+ before do
+ stub_request(:get, "http://ch.siemens.com/pki?ZZZZZZA6.crl")
+ .to_return(status: 200, body: "trash", headers: {})
+ end
+
+ it 'does not change certificate status' do
+ revoked_x509_commit = Gitlab::X509::Commit.new(revoked_x509_signed_commit)
+
+ expect(revoked_x509_commit.signature.x509_certificate.good?).to be_truthy
+
+ worker.perform
+ revoked_x509_commit.signature.reload
+
+ expect(revoked_x509_commit.signature.x509_certificate.revoked?).to be_falsey
+ end
+ end
+
+ context 'not found crl' do
+ before do
+ stub_request(:get, "http://ch.siemens.com/pki?ZZZZZZA6.crl")
+ .to_return(status: 404, body: "not found", headers: {})
+ end
+
+ it 'does not change certificate status' do
+ revoked_x509_commit = Gitlab::X509::Commit.new(revoked_x509_signed_commit)
+
+ expect(revoked_x509_commit.signature.x509_certificate.good?).to be_truthy
+
+ worker.perform
+ revoked_x509_commit.signature.reload
+
+ expect(revoked_x509_commit.signature.x509_certificate.revoked?).to be_falsey
+ end
+ end
+
+ context 'unreachable crl' do
+ before do
+ stub_request(:get, "http://ch.siemens.com/pki?ZZZZZZA6.crl")
+ .to_raise(SocketError.new('Some HTTP error'))
+ end
+
+ it 'does not change certificate status' do
+ revoked_x509_commit = Gitlab::X509::Commit.new(revoked_x509_signed_commit)
+
+ expect(revoked_x509_commit.signature.x509_certificate.good?).to be_truthy
+
+ worker.perform
+ revoked_x509_commit.signature.reload
+
+ expect(revoked_x509_commit.signature.x509_certificate.revoked?).to be_falsey
+ end
+ end
+ end
+end
diff --git a/vendor/elastic_stack/values.yaml b/vendor/elastic_stack/values.yaml
index 48950ed8bbb..21352dd35e2 100644
--- a/vendor/elastic_stack/values.yaml
+++ b/vendor/elastic_stack/values.yaml
@@ -1,60 +1,41 @@
+# Default values for elastic-stack.
+# This is a YAML-formatted file.
+# Declare variables to be passed into your templates.
+
elasticsearch:
enabled: true
- cluster:
- env:
- MINIMUM_MASTER_NODES: "1"
- master:
- replicas: 2
- client:
- replicas: 1
- data:
- replicas: 2
-
-kibana:
- enabled: false
-
-logstash:
- enabled: false
+ # prefix elasticsearch resources with the name of the releases
+ # looks like we can't use {{ .Release.Name }}-elasticsearch
+ # https://github.com/helm/helm/issues/2133
+ clusterName: "elastic-stack-elasticsearch"
filebeat:
enabled: true
- config:
- output.file.enabled: false
- output.elasticsearch:
- enabled: true
- hosts: ["http://elastic-stack-elasticsearch-client:9200"]
- filebeat.prospectors:
- - type: log
- enabled: true
- paths:
- - /var/log/*.log
- - /var/log/messages
- - /var/log/syslog
- - type: docker
- containers.ids:
- - "*"
- json.keys_under_root: true
- json.ignore_decoding_error: true
- processors:
- - add_kubernetes_metadata:
- - drop_event:
- when:
- equals:
- kubernetes.container.name: "filebeat"
- - decode_json_fields:
- fields: ["message"]
- when:
- equals:
- kubernetes.container.name: "modsecurity-log"
-
-fluentd:
- enabled: false
-
-fluent-bit:
- enabled: false
-
-nginx-ldapauth-proxy:
+ filebeatConfig:
+ filebeat.yml: |
+ output.file.enabled: false
+ output.elasticsearch:
+ hosts: ["http://elastic-stack-elasticsearch-master:9200"]
+ filebeat.inputs:
+ - type: container
+ paths:
+ - '/var/lib/docker/containers/*/*.log'
+ json.keys_under_root: true
+ json.ignore_decoding_error: true
+ processors:
+ - add_id:
+ target_field: tie_breaker_id
+ - add_cloud_metadata: ~
+ - add_kubernetes_metadata: ~
+ - decode_json_fields:
+ fields: ["message"]
+ when:
+ equals:
+ kubernetes.container.namespace: "gitlab-managed-apps"
+ kubernetes.container.name: "modsecurity-log"
+kibana:
enabled: false
+ elasticsearchHosts: "http://elastic-stack-elasticsearch-master:9200"
elasticsearch-curator:
enabled: true
@@ -63,7 +44,7 @@ elasticsearch-curator:
---
client:
hosts:
- - elastic-stack-elasticsearch-client
+ - elastic-stack-elasticsearch-master
port: 9200
action_file_yml: |-
---
@@ -76,6 +57,7 @@ elasticsearch-curator:
actionable list of indices (ignore_empty_list) and exit cleanly.
options:
ignore_empty_list: True
+ allow_ilm_indices: True
filters:
- filtertype: pattern
kind: prefix
@@ -86,6 +68,3 @@ elasticsearch-curator:
timestring: '%Y.%m.%d'
unit: days
unit_count: 30
-
-elasticsearch-exporter:
- enabled: false
diff --git a/vendor/elastic_stack/wait-for-elasticsearch.sh b/vendor/elastic_stack/wait-for-elasticsearch.sh
index 1423af2e10b..33c5eaae9ef 100755
--- a/vendor/elastic_stack/wait-for-elasticsearch.sh
+++ b/vendor/elastic_stack/wait-for-elasticsearch.sh
@@ -1,4 +1,4 @@
-#!/bin/bash
+#!/usr/bin/env bash
# http://redsymbol.net/articles/unofficial-bash-strict-mode/
IFS=$'\n\t'
set -euo pipefail
diff --git a/vendor/project_templates/android.tar.gz b/vendor/project_templates/android.tar.gz
index 277aedaa1ca..fff7a7e45a6 100644
--- a/vendor/project_templates/android.tar.gz
+++ b/vendor/project_templates/android.tar.gz
Binary files differ
diff --git a/vendor/project_templates/sse_middleman.tar.gz b/vendor/project_templates/sse_middleman.tar.gz
index 62126e8f80d..30d38fbb43d 100644
--- a/vendor/project_templates/sse_middleman.tar.gz
+++ b/vendor/project_templates/sse_middleman.tar.gz
Binary files differ
diff --git a/yarn.lock b/yarn.lock
index 5eea3d8202a..f6672fc96c8 100644
--- a/yarn.lock
+++ b/yarn.lock
@@ -766,10 +766,10 @@
resolved "https://registry.yarnpkg.com/@gitlab/at.js/-/at.js-1.5.5.tgz#5f6bfe6baaef360daa9b038fa78798d7a6a916b4"
integrity sha512-282Dn3SPVsUHVDhMsXgfnv+Rzog0uxecjttxGRQvxh25es1+xvkGQFsvJfkSKJ3X1kHVkSjKf+Tt5Rra+Jhp9g==
-"@gitlab/eslint-plugin@2.2.1":
- version "2.2.1"
- resolved "https://registry.yarnpkg.com/@gitlab/eslint-plugin/-/eslint-plugin-2.2.1.tgz#7033030787981ded5ae24f4051109d069c98fcc0"
- integrity sha512-OM0gU2wfUeFZU5MP4Qaj1QHeJwpi9Ps35dRo8V1BP1lFskn2nDl6dEINAgqpdpjRV33LJ5/TZLfcGIswad0UtQ==
+"@gitlab/eslint-plugin@3.1.0":
+ version "3.1.0"
+ resolved "https://registry.yarnpkg.com/@gitlab/eslint-plugin/-/eslint-plugin-3.1.0.tgz#18e03630d10788defbb4c2d746620aec09517295"
+ integrity sha512-M5bCk5vD0d65COeYtWoc7p43bvvsT9885t6DONI7q5aQVg7GBk3J4on8XjnWTLI4dFZNQGS6aw8+PkRD8NqByQ==
dependencies:
babel-eslint "^10.0.3"
eslint-config-airbnb-base "^14.0.0"
@@ -777,19 +777,20 @@
eslint-plugin-babel "^5.3.0"
eslint-plugin-filenames "^1.3.2"
eslint-plugin-import "^2.20.1"
+ eslint-plugin-jest "^23.8.2"
eslint-plugin-promise "^4.2.1"
eslint-plugin-vue "^6.2.1"
vue-eslint-parser "^7.0.0"
-"@gitlab/svgs@1.121.0":
- version "1.121.0"
- resolved "https://registry.yarnpkg.com/@gitlab/svgs/-/svgs-1.121.0.tgz#77083a68f72e9aa0e294da7715f378eef13b839e"
- integrity sha512-scz/6Y/eED7RMFLAlhT6PwXwe0Wj8ivnRsyulk9NXKoqUmAqZliNmBmzYsHy5bFf9NB6xVV/rOk1/92nbi/Yaw==
+"@gitlab/svgs@1.127.0":
+ version "1.127.0"
+ resolved "https://registry.yarnpkg.com/@gitlab/svgs/-/svgs-1.127.0.tgz#1f7ffdffe44d6a82b372535f93d78f3a895d1960"
+ integrity sha512-Uv52DqkG2KwCB0VRlXUEHFZxJ/7Ql0t1YTdzICpXmDjltuUBrysFcdmWPVO6PgXQxk2ahryNsUjSOziMYTeSiw==
-"@gitlab/ui@12.1.0":
- version "12.1.0"
- resolved "https://registry.yarnpkg.com/@gitlab/ui/-/ui-12.1.0.tgz#b97c7898410767c85cf1768f9b9e36329e59a7ec"
- integrity sha512-EYUYg1n7ByuFFuODvnjTUxBQUIduKK4pm3n7Jb+MXNf2VvcufO9QwlvL6iCV8dqXX/u4HsuUgZfEidLsDnrltQ==
+"@gitlab/ui@14.10.0":
+ version "14.10.0"
+ resolved "https://registry.yarnpkg.com/@gitlab/ui/-/ui-14.10.0.tgz#39c04d62c914fcefe96c7ec32fdf31b1f98f1119"
+ integrity sha512-k9w6z3/QBeUas++cH5BaozjxY4fVu+AggjGoh9QMKN5hpiGTiTPx5aQJIlOv8UX/kpUmgc4pHSWAbw30YVGGFw==
dependencies:
"@babel/standalone" "^7.0.0"
"@gitlab/vue-toasted" "^1.3.0"
@@ -802,8 +803,6 @@
portal-vue "^2.1.6"
resize-observer-polyfill "^1.5.1"
url-search-params-polyfill "^5.0.0"
- vue "^2.6.10"
- vue-loader "^15.4.2"
vue-runtime-helpers "^1.1.2"
"@gitlab/visual-review-tools@1.6.1":
@@ -984,6 +983,11 @@
consola "^2.10.1"
node-fetch "^2.6.0"
+"@rails/actioncable@^6.0.3":
+ version "6.0.3"
+ resolved "https://registry.yarnpkg.com/@rails/actioncable/-/actioncable-6.0.3.tgz#722b4b639936129307ddbab3a390f6bcacf3e7bc"
+ integrity sha512-I01hgqxxnOgOtJTGlq0ZsGJYiTEEiSGVEGQn3vimZSqEP1HqzyFNbzGTq14Xdyeow2yGJjygjoFF1pmtE+SQaw==
+
"@sentry/browser@^5.10.2":
version "5.10.2"
resolved "https://registry.yarnpkg.com/@sentry/browser/-/browser-5.10.2.tgz#0bbb05505c58ea998c833cffec3f922fe4b4fa58"
@@ -1036,10 +1040,25 @@
"@sentry/types" "5.10.0"
tslib "^1.9.3"
-"@sourcegraph/code-host-integration@0.0.36":
- version "0.0.36"
- resolved "https://registry.yarnpkg.com/@sourcegraph/code-host-integration/-/code-host-integration-0.0.36.tgz#2f4d287840ac2944c78ef92f10f0db0ef8a077fa"
- integrity sha512-Hpj1xiVhPxMsjLNre9MrYYAM1SPOWPE9yG9SPtz4dqYzc6/ycaPGyr+ljcaWEclS9hZCvkk4+qVC5WONpYVjyA==
+"@sourcegraph/code-host-integration@0.0.46":
+ version "0.0.46"
+ resolved "https://registry.yarnpkg.com/@sourcegraph/code-host-integration/-/code-host-integration-0.0.46.tgz#05e4cda671ed00450be12461e6a3caff473675aa"
+ integrity sha512-ghzfaV5ydSWTamLPIDLl5tRvTtM2MBDRmQbWTPg9ZoCP/eHk61uCTAFEq02lsefDQHISZHldeClqRYhZvGDZfw==
+
+"@toast-ui/editor@^2.0.1":
+ version "2.0.1"
+ resolved "https://registry.yarnpkg.com/@toast-ui/editor/-/editor-2.0.1.tgz#749e5be1f02f42ded51488d1575ab1c19ca59952"
+ integrity sha512-TC481O/zP37boY6H6oVN6KLVMY7yrU8zQu+3xqZ71V3Sr6D2XyaGb2Xub9XqTdqzBmzsf7y4Gi+EXO0IQ3rGVA==
+ dependencies:
+ "@types/codemirror" "0.0.71"
+ codemirror "^5.48.4"
+
+"@toast-ui/vue-editor@^2.0.1":
+ version "2.0.1"
+ resolved "https://registry.yarnpkg.com/@toast-ui/vue-editor/-/vue-editor-2.0.1.tgz#c9c8c8da4c0a67b9fbc4240464388c67d72a0c22"
+ integrity sha512-sGsApl0n+GVAZbmPA+tTrq9rmmyh2mRgCgg2/mu1/lN7S4vPv/nQH8KXxLG9Y6hG2+kgelqz6wvbOCdzlM/HmQ==
+ dependencies:
+ "@toast-ui/editor" "^2.0.1"
"@types/anymatch@*":
version "1.3.0"
@@ -1079,6 +1098,18 @@
dependencies:
"@babel/types" "^7.3.0"
+"@types/codemirror@0.0.71":
+ version "0.0.71"
+ resolved "https://registry.yarnpkg.com/@types/codemirror/-/codemirror-0.0.71.tgz#861f1bcb3100c0a064567c5400f2981cf4ae8ca7"
+ integrity sha512-b2oEEnno1LIGKMR7uBEsr40al1UijF1HEpRn0+Yf1xOLl24iQgB7DBpZVMM7y54G5wCNoclDrRO65E6KHPNO2w==
+ dependencies:
+ "@types/tern" "*"
+
+"@types/estree@*":
+ version "0.0.44"
+ resolved "https://registry.yarnpkg.com/@types/estree/-/estree-0.0.44.tgz#980cc5a29a3ef3bea6ff1f7d021047d7ea575e21"
+ integrity sha512-iaIVzr+w2ZJ5HkidlZ3EJM8VTZb2MJLCjw3V+505yVts0gRC4UMvjw0d1HPtGqI/HQC/KdsYtayfzl+AXY2R8g==
+
"@types/events@*":
version "1.2.0"
resolved "https://registry.yarnpkg.com/@types/events/-/events-1.2.0.tgz#81a6731ce4df43619e5c8c945383b3e62a89ea86"
@@ -1113,6 +1144,11 @@
"@types/istanbul-lib-coverage" "*"
"@types/istanbul-lib-report" "*"
+"@types/json-schema@^7.0.3":
+ version "7.0.4"
+ resolved "https://registry.yarnpkg.com/@types/json-schema/-/json-schema-7.0.4.tgz#38fd73ddfd9b55abb1e1b2ed578cb55bd7b7d339"
+ integrity sha512-8+KAKzEvSUdeo+kmqnKrqgeE+LcA0tjYWFY7RPProVYwnqDjukzO+3b6dLD56rYX5TdWejnEOLJYOIeh4CXKuA==
+
"@types/minimatch@*":
version "3.0.3"
resolved "https://registry.yarnpkg.com/@types/minimatch/-/minimatch-3.0.3.tgz#3dca0e3f33b200fc7d1139c0cd96c1268cadfd9d"
@@ -1138,6 +1174,13 @@
resolved "https://registry.yarnpkg.com/@types/tapable/-/tapable-1.0.4.tgz#b4ffc7dc97b498c969b360a41eee247f82616370"
integrity sha512-78AdXtlhpCHT0K3EytMpn4JNxaf5tbqbLcbIRoQIHzpTIyjpxLQKRoxU55ujBXAtg3Nl2h/XWvfDa9dsMOd0pQ==
+"@types/tern@*":
+ version "0.23.3"
+ resolved "https://registry.yarnpkg.com/@types/tern/-/tern-0.23.3.tgz#4b54538f04a88c9ff79de1f6f94f575a7f339460"
+ integrity sha512-imDtS4TAoTcXk0g7u4kkWqedB3E4qpjXzCpD2LU5M5NAXHzCDsypyvXSaG7mM8DKYkCRa7tFp4tS/lp/Wo7Q3w==
+ dependencies:
+ "@types/estree" "*"
+
"@types/uglify-js@*":
version "3.0.4"
resolved "https://registry.yarnpkg.com/@types/uglify-js/-/uglify-js-3.0.4.tgz#96beae23df6f561862a830b4288a49e86baac082"
@@ -1188,6 +1231,29 @@
resolved "https://registry.yarnpkg.com/@types/zen-observable/-/zen-observable-0.8.0.tgz#8b63ab7f1aa5321248aad5ac890a485656dcea4d"
integrity sha512-te5lMAWii1uEJ4FwLjzdlbw3+n0FZNOvFXHxQDKeT0dilh7HOzdMzV2TrJVUzq8ep7J4Na8OUYPRLSQkJHAlrg==
+"@typescript-eslint/experimental-utils@^2.5.0":
+ version "2.30.0"
+ resolved "https://registry.yarnpkg.com/@typescript-eslint/experimental-utils/-/experimental-utils-2.30.0.tgz#9845e868c01f3aed66472c561d4b6bac44809dd0"
+ integrity sha512-L3/tS9t+hAHksy8xuorhOzhdefN0ERPDWmR9CclsIGOUqGKy6tqc/P+SoXeJRye5gazkuPO0cK9MQRnolykzkA==
+ dependencies:
+ "@types/json-schema" "^7.0.3"
+ "@typescript-eslint/typescript-estree" "2.30.0"
+ eslint-scope "^5.0.0"
+ eslint-utils "^2.0.0"
+
+"@typescript-eslint/typescript-estree@2.30.0":
+ version "2.30.0"
+ resolved "https://registry.yarnpkg.com/@typescript-eslint/typescript-estree/-/typescript-estree-2.30.0.tgz#1b8e848b55144270255ffbfe4c63291f8f766615"
+ integrity sha512-nI5WOechrA0qAhnr+DzqwmqHsx7Ulr/+0H7bWCcClDhhWkSyZR5BmTvnBEyONwJCTWHfc5PAQExX24VD26IAVw==
+ dependencies:
+ debug "^4.1.1"
+ eslint-visitor-keys "^1.1.0"
+ glob "^7.1.6"
+ is-glob "^4.0.1"
+ lodash "^4.17.15"
+ semver "^6.3.0"
+ tsutils "^3.17.1"
+
"@vue/component-compiler-utils@^2.4.0":
version "2.6.0"
resolved "https://registry.yarnpkg.com/@vue/component-compiler-utils/-/component-compiler-utils-2.6.0.tgz#aa46d2a6f7647440b0b8932434d22f12371e543b"
@@ -1891,15 +1957,6 @@ axios@^0.19.0:
follow-redirects "1.5.10"
is-buffer "^2.0.2"
-babel-code-frame@^6.26.0:
- version "6.26.0"
- resolved "https://registry.yarnpkg.com/babel-code-frame/-/babel-code-frame-6.26.0.tgz#63fd43f7dc1e3bb7ce35947db8fe369a3f58c74b"
- integrity sha1-Y/1D99weO7fONZR9uP42mj9Yx0s=
- dependencies:
- chalk "^1.1.3"
- esutils "^2.0.2"
- js-tokens "^3.0.2"
-
babel-eslint@^10.0.3:
version "10.0.3"
resolved "https://registry.yarnpkg.com/babel-eslint/-/babel-eslint-10.0.3.tgz#81a2c669be0f205e19462fed2482d33e4687a88a"
@@ -2477,7 +2534,7 @@ camelcase@^4.0.0, camelcase@^4.1.0:
resolved "https://registry.yarnpkg.com/camelcase/-/camelcase-4.1.0.tgz#d545635be1e33c542649c69173e5de6acfae34dd"
integrity sha1-1UVjW+HjPFQmScaRc+Xeas+uNN0=
-camelcase@^5.0.0:
+camelcase@^5.0.0, camelcase@^5.2.0:
version "5.3.1"
resolved "https://registry.yarnpkg.com/camelcase/-/camelcase-5.3.1.tgz#e3c9b31569e106811df242f715725a1f4c494320"
integrity sha512-L28STB170nwWS63UjtlEOE3dldQApaJXZkOI1uMFfzf3rRuPegHaHesyee+YxQ+W6SvRDQV6UrdOdRiR153wJg==
@@ -2525,7 +2582,7 @@ chalk@2.4.2, chalk@^2.0.0, chalk@^2.0.1, chalk@^2.1.0, chalk@^2.4.1, chalk@^2.4.
escape-string-regexp "^1.0.5"
supports-color "^5.3.0"
-chalk@^1.1.1, chalk@^1.1.3:
+chalk@^1.1.1:
version "1.1.3"
resolved "https://registry.yarnpkg.com/chalk/-/chalk-1.1.3.tgz#a8115c55e4a702fe4d150abd3872822a7e09fc98"
integrity sha1-qBFcVeSnAv5NFQq9OHKCKn4J/Jg=
@@ -2764,6 +2821,11 @@ code-point-at@^1.0.0:
resolved "https://registry.yarnpkg.com/code-point-at/-/code-point-at-1.1.0.tgz#0d070b4d043a5bea33a2f1a40e2edb3d9a4ccf77"
integrity sha1-DQcLTQQ6W+ozovGkDi7bPZpMz3c=
+codemirror@^5.48.4:
+ version "5.53.2"
+ resolved "https://registry.yarnpkg.com/codemirror/-/codemirror-5.53.2.tgz#9799121cf8c50809cca487304e9de3a74d33f428"
+ integrity sha512-wvSQKS4E+P8Fxn/AQ+tQtJnF1qH5UOlxtugFLpubEZ5jcdH2iXTVinb+Xc/4QjshuOxRm4fUsU2QPF1JJKiyXA==
+
codesandbox-api@0.0.23:
version "0.0.23"
resolved "https://registry.yarnpkg.com/codesandbox-api/-/codesandbox-api-0.0.23.tgz#bf650a21b5f3c2369e03f0c19d10b4e2ba255b4f"
@@ -3212,38 +3274,28 @@ css-b64-images@~0.2.5:
resolved "https://registry.yarnpkg.com/css-b64-images/-/css-b64-images-0.2.5.tgz#42005d83204b2b4a5d93b6b1a5644133b5927a02"
integrity sha1-QgBdgyBLK0pdk7axpWRBM7WSegI=
-css-loader@^1.0.0:
- version "1.0.1"
- resolved "https://registry.yarnpkg.com/css-loader/-/css-loader-1.0.1.tgz#6885bb5233b35ec47b006057da01cc640b6b79fe"
- integrity sha512-+ZHAZm/yqvJ2kDtPne3uX0C+Vr3Zn5jFn2N4HywtS5ujwvsVkyg0VArEXpl3BgczDA8anieki1FIzhchX4yrDw==
+css-loader@^2.1.1:
+ version "2.1.1"
+ resolved "https://registry.yarnpkg.com/css-loader/-/css-loader-2.1.1.tgz#d8254f72e412bb2238bb44dd674ffbef497333ea"
+ integrity sha512-OcKJU/lt232vl1P9EEDamhoO9iKY3tIjY5GU+XDLblAykTdgs6Ux9P1hTHve8nFKy5KPpOXOsVI/hIwi3841+w==
dependencies:
- babel-code-frame "^6.26.0"
- css-selector-tokenizer "^0.7.0"
- icss-utils "^2.1.0"
- loader-utils "^1.0.2"
- lodash "^4.17.11"
- postcss "^6.0.23"
- postcss-modules-extract-imports "^1.2.0"
- postcss-modules-local-by-default "^1.2.0"
- postcss-modules-scope "^1.1.0"
- postcss-modules-values "^1.3.0"
+ camelcase "^5.2.0"
+ icss-utils "^4.1.0"
+ loader-utils "^1.2.3"
+ normalize-path "^3.0.0"
+ postcss "^7.0.14"
+ postcss-modules-extract-imports "^2.0.0"
+ postcss-modules-local-by-default "^2.0.6"
+ postcss-modules-scope "^2.1.0"
+ postcss-modules-values "^2.0.0"
postcss-value-parser "^3.3.0"
- source-list-map "^2.0.0"
+ schema-utils "^1.0.0"
css-selector-parser@^1.3:
version "1.3.0"
resolved "https://registry.yarnpkg.com/css-selector-parser/-/css-selector-parser-1.3.0.tgz#5f1ad43e2d8eefbfdc304fcd39a521664943e3eb"
integrity sha1-XxrUPi2O77/cME/NOaUhZklD4+s=
-css-selector-tokenizer@^0.7.0:
- version "0.7.2"
- resolved "https://registry.yarnpkg.com/css-selector-tokenizer/-/css-selector-tokenizer-0.7.2.tgz#11e5e27c9a48d90284f22d45061c303d7a25ad87"
- integrity sha512-yj856NGuAymN6r8bn8/Jl46pR+OC3eEvAhfGYDUe7YPtTPAYrSSw4oAniZ9Y8T5B92hjhwTBLUen0/vKPxf6pw==
- dependencies:
- cssesc "^3.0.0"
- fastparse "^1.1.2"
- regexpu-core "^4.6.0"
-
css@^2.1.0:
version "2.2.4"
resolved "https://registry.yarnpkg.com/css/-/css-2.2.4.tgz#c646755c73971f2bba6a601e2cf2fd71b1298929"
@@ -4314,10 +4366,12 @@ eslint-plugin-jasmine@^4.1.0:
resolved "https://registry.yarnpkg.com/eslint-plugin-jasmine/-/eslint-plugin-jasmine-4.1.0.tgz#4f6d41b1a8622348c97559cbcd29badffa74dbfa"
integrity sha512-Vfuk2Sm1ULR7MqGjVIOOEdQWyoFBfSwvwUeo9MrajVGJB3C24c9Mmj1Cgf8Qwmf3aS2bezPt1sckpKXWpd74Dw==
-eslint-plugin-jest@^22.3.0:
- version "22.3.0"
- resolved "https://registry.yarnpkg.com/eslint-plugin-jest/-/eslint-plugin-jest-22.3.0.tgz#a10f10dedfc92def774ec9bb5bfbd2fb8e1c96d2"
- integrity sha512-P1mYVRNlOEoO5T9yTqOfucjOYf1ktmJ26NjwjH8sxpCFQa6IhBGr5TpKl3hcAAT29hOsRJVuMWmTsHoUVo9FoA==
+eslint-plugin-jest@^23.8.2:
+ version "23.8.2"
+ resolved "https://registry.yarnpkg.com/eslint-plugin-jest/-/eslint-plugin-jest-23.8.2.tgz#6f28b41c67ef635f803ebd9e168f6b73858eb8d4"
+ integrity sha512-xwbnvOsotSV27MtAe7s8uGWOori0nUsrXh2f1EnpmXua8sDfY6VZhHAhHg2sqK7HBNycRQExF074XSZ7DvfoFg==
+ dependencies:
+ "@typescript-eslint/experimental-utils" "^2.5.0"
eslint-plugin-no-jquery@^2.3.0:
version "2.3.1"
@@ -4366,6 +4420,13 @@ eslint-utils@^1.4.3:
dependencies:
eslint-visitor-keys "^1.1.0"
+eslint-utils@^2.0.0:
+ version "2.0.0"
+ resolved "https://registry.yarnpkg.com/eslint-utils/-/eslint-utils-2.0.0.tgz#7be1cc70f27a72a76cd14aa698bcabed6890e1cd"
+ integrity sha512-0HCPuJv+7Wv1bACm8y5/ECVfYdfsAm9xmVb7saeFlxjPYALefjhbYoCkBjPdPzGH8wWyTpAez82Fh3VKYEZ8OA==
+ dependencies:
+ eslint-visitor-keys "^1.1.0"
+
eslint-visitor-keys@^1.0.0, eslint-visitor-keys@^1.1.0:
version "1.1.0"
resolved "https://registry.yarnpkg.com/eslint-visitor-keys/-/eslint-visitor-keys-1.1.0.tgz#e2a82cea84ff246ad6fb57f9bde5b46621459ec2"
@@ -4703,11 +4764,6 @@ fast-levenshtein@~2.0.6:
resolved "https://registry.yarnpkg.com/fast-levenshtein/-/fast-levenshtein-2.0.6.tgz#3d8a5c66883a16a30ca8643e851f19baa7797917"
integrity sha1-PYpcZog6FqMMqGQ+hR8Zuqd5eRc=
-fastparse@^1.1.2:
- version "1.1.2"
- resolved "https://registry.yarnpkg.com/fastparse/-/fastparse-1.1.2.tgz#91728c5a5942eced8531283c79441ee4122c35a9"
- integrity sha512-483XLLxTVIwWK3QTrMGRqUfUpoOs/0hbQrl2oz4J0pAcm3A3bu84wxTFqGqkJzewCLdME38xJLJAxBABfQT8sQ==
-
fault@^1.0.2:
version "1.0.2"
resolved "https://registry.yarnpkg.com/fault/-/fault-1.0.2.tgz#c3d0fec202f172a3a4d414042ad2bb5e2a3ffbaa"
@@ -5334,10 +5390,10 @@ graphlib@^2.1.7, graphlib@^2.1.8:
dependencies:
lodash "^4.17.15"
-graphql-tag@^2.10.0:
- version "2.10.0"
- resolved "https://registry.yarnpkg.com/graphql-tag/-/graphql-tag-2.10.0.tgz#87da024be863e357551b2b8700e496ee2d4353ae"
- integrity sha512-9FD6cw976TLLf9WYIUPCaaTpniawIjHWZSwIRZSjrfufJamcXbVVYfN2TWvJYbw0Xf2JjYbl1/f2+wDnBVw3/w==
+graphql-tag@^2.10.1:
+ version "2.10.3"
+ resolved "https://registry.yarnpkg.com/graphql-tag/-/graphql-tag-2.10.3.tgz#ea1baba5eb8fc6339e4c4cf049dabe522b0edf03"
+ integrity sha512-4FOv3ZKfA4WdOKJeHdz6B3F/vxBLSgmBcGeAFPf4n1F64ltJUvOOerNj0rsJxONQGdhUMynQIvd6LzB+1J5oKA==
graphql@^14.0.2:
version "14.0.2"
@@ -5667,12 +5723,12 @@ icss-replace-symbols@^1.1.0:
resolved "https://registry.yarnpkg.com/icss-replace-symbols/-/icss-replace-symbols-1.1.0.tgz#06ea6f83679a7749e386cfe1fe812ae5db223ded"
integrity sha1-Bupvg2ead0njhs/h/oEq5dsiPe0=
-icss-utils@^2.1.0:
- version "2.1.0"
- resolved "https://registry.yarnpkg.com/icss-utils/-/icss-utils-2.1.0.tgz#83f0a0ec378bf3246178b6c2ad9136f135b1c962"
- integrity sha1-g/Cg7DeL8yRheLbCrZE28TWxyWI=
+icss-utils@^4.1.0:
+ version "4.1.1"
+ resolved "https://registry.yarnpkg.com/icss-utils/-/icss-utils-4.1.1.tgz#21170b53789ee27447c2f47dd683081403f9a467"
+ integrity sha512-4aFq7wvWyMHKgxsH8QQtGpvbASCf+eM3wPRLI6R+MgAnTCZ6STYsRvttLvRWK0Nfif5piF394St3HeJDaljGPA==
dependencies:
- postcss "^6.0.1"
+ postcss "^7.0.14"
ieee754@1.1.13, ieee754@^1.1.4:
version "1.1.13"
@@ -6830,11 +6886,6 @@ js-cookie@^2.2.1:
resolved "https://registry.yarnpkg.com/js-tokens/-/js-tokens-4.0.0.tgz#19203fb59991df98e3a287050d4647cdeaf32499"
integrity sha512-RdJUflcE3cUzKiMqQgsCu06FPu9UdIJO0beYbPhHN4k6apgJtifcoCtT9bcxOpYBtpD2kCM6Sbzg4CausW/PKQ==
-js-tokens@^3.0.2:
- version "3.0.2"
- resolved "https://registry.yarnpkg.com/js-tokens/-/js-tokens-3.0.2.tgz#9866df395102130e38f7f996bceb65443209c25b"
- integrity sha1-mGbfOVECEw449/mWvOtlRDIJwls=
-
js-yaml@^3.13.1:
version "3.13.1"
resolved "https://registry.yarnpkg.com/js-yaml/-/js-yaml-3.13.1.tgz#aff151b30bfdfa8e49e05da22e7415e9dfa37847"
@@ -7884,6 +7935,11 @@ mississippi@^3.0.0:
stream-each "^1.1.0"
through2 "^2.0.0"
+mitt@^1.2.0:
+ version "1.2.0"
+ resolved "https://registry.yarnpkg.com/mitt/-/mitt-1.2.0.tgz#cb24e6569c806e31bd4e3995787fe38a04fdf90d"
+ integrity sha512-r6lj77KlwqLhIUku9UWYes7KJtsczvolZkzp8hbaDPPaE24OmWl5s539Mytlj22siEQKosZ26qCBgda2PKwoJw==
+
mixin-deep@^1.2.0:
version "1.3.2"
resolved "https://registry.yarnpkg.com/mixin-deep/-/mixin-deep-1.3.2.tgz#1120b43dc359a785dce65b55b82e257ccf479566"
@@ -8932,36 +8988,37 @@ postcss-media-query-parser@^0.2.3:
resolved "https://registry.yarnpkg.com/postcss-media-query-parser/-/postcss-media-query-parser-0.2.3.tgz#27b39c6f4d94f81b1a73b8f76351c609e5cef244"
integrity sha1-J7Ocb02U+Bsac7j3Y1HGCeXO8kQ=
-postcss-modules-extract-imports@^1.2.0:
- version "1.2.1"
- resolved "https://registry.yarnpkg.com/postcss-modules-extract-imports/-/postcss-modules-extract-imports-1.2.1.tgz#dc87e34148ec7eab5f791f7cd5849833375b741a"
- integrity sha512-6jt9XZwUhwmRUhb/CkyJY020PYaPJsCyt3UjbaWo6XEbH/94Hmv6MP7fG2C5NDU/BcHzyGYxNtHvM+LTf9HrYw==
+postcss-modules-extract-imports@^2.0.0:
+ version "2.0.0"
+ resolved "https://registry.yarnpkg.com/postcss-modules-extract-imports/-/postcss-modules-extract-imports-2.0.0.tgz#818719a1ae1da325f9832446b01136eeb493cd7e"
+ integrity sha512-LaYLDNS4SG8Q5WAWqIJgdHPJrDDr/Lv775rMBFUbgjTz6j34lUznACHcdRWroPvXANP2Vj7yNK57vp9eFqzLWQ==
dependencies:
- postcss "^6.0.1"
+ postcss "^7.0.5"
-postcss-modules-local-by-default@^1.2.0:
- version "1.2.0"
- resolved "https://registry.yarnpkg.com/postcss-modules-local-by-default/-/postcss-modules-local-by-default-1.2.0.tgz#f7d80c398c5a393fa7964466bd19500a7d61c069"
- integrity sha1-99gMOYxaOT+nlkRmvRlQCn1hwGk=
+postcss-modules-local-by-default@^2.0.6:
+ version "2.0.6"
+ resolved "https://registry.yarnpkg.com/postcss-modules-local-by-default/-/postcss-modules-local-by-default-2.0.6.tgz#dd9953f6dd476b5fd1ef2d8830c8929760b56e63"
+ integrity sha512-oLUV5YNkeIBa0yQl7EYnxMgy4N6noxmiwZStaEJUSe2xPMcdNc8WmBQuQCx18H5psYbVxz8zoHk0RAAYZXP9gA==
dependencies:
- css-selector-tokenizer "^0.7.0"
- postcss "^6.0.1"
+ postcss "^7.0.6"
+ postcss-selector-parser "^6.0.0"
+ postcss-value-parser "^3.3.1"
-postcss-modules-scope@^1.1.0:
- version "1.1.0"
- resolved "https://registry.yarnpkg.com/postcss-modules-scope/-/postcss-modules-scope-1.1.0.tgz#d6ea64994c79f97b62a72b426fbe6056a194bb90"
- integrity sha1-1upkmUx5+XtipytCb75gVqGUu5A=
+postcss-modules-scope@^2.1.0:
+ version "2.2.0"
+ resolved "https://registry.yarnpkg.com/postcss-modules-scope/-/postcss-modules-scope-2.2.0.tgz#385cae013cc7743f5a7d7602d1073a89eaae62ee"
+ integrity sha512-YyEgsTMRpNd+HmyC7H/mh3y+MeFWevy7V1evVhJWewmMbjDHIbZbOXICC2y+m1xI1UVfIT1HMW/O04Hxyu9oXQ==
dependencies:
- css-selector-tokenizer "^0.7.0"
- postcss "^6.0.1"
+ postcss "^7.0.6"
+ postcss-selector-parser "^6.0.0"
-postcss-modules-values@^1.3.0:
- version "1.3.0"
- resolved "https://registry.yarnpkg.com/postcss-modules-values/-/postcss-modules-values-1.3.0.tgz#ecffa9d7e192518389f42ad0e83f72aec456ea20"
- integrity sha1-7P+p1+GSUYOJ9CrQ6D9yrsRW6iA=
+postcss-modules-values@^2.0.0:
+ version "2.0.0"
+ resolved "https://registry.yarnpkg.com/postcss-modules-values/-/postcss-modules-values-2.0.0.tgz#479b46dc0c5ca3dc7fa5270851836b9ec7152f64"
+ integrity sha512-Ki7JZa7ff1N3EIMlPnGTZfUMe69FFwiQPnVSXC9mnn3jozCRBYIxiZd44yJOV2AmabOo4qFf8s0dC/+lweG7+w==
dependencies:
icss-replace-symbols "^1.1.0"
- postcss "^6.0.1"
+ postcss "^7.0.6"
postcss-reporter@^6.0.1:
version "6.0.1"
@@ -9018,7 +9075,7 @@ postcss-selector-parser@^5.0.0:
indexes-of "^1.0.1"
uniq "^1.0.1"
-postcss-selector-parser@^6.0.2:
+postcss-selector-parser@^6.0.0, postcss-selector-parser@^6.0.2:
version "6.0.2"
resolved "https://registry.yarnpkg.com/postcss-selector-parser/-/postcss-selector-parser-6.0.2.tgz#934cf799d016c83411859e09dcecade01286ec5c"
integrity sha512-36P2QR59jDTOAiIkqEprfJDsoNrvwFei3eCqKd1Y0tUsBimsq39BLp7RD+JWny3WgB1zGhJX8XVePwm9k4wdBg==
@@ -9042,15 +9099,6 @@ postcss-value-parser@^4.0.0:
resolved "https://registry.yarnpkg.com/postcss-value-parser/-/postcss-value-parser-4.0.0.tgz#99a983d365f7b2ad8d0f9b8c3094926eab4b936d"
integrity sha512-ESPktioptiSUchCKgggAkzdmkgzKfmp0EU8jXH+5kbIUB+unr0Y4CY9SRMvibuvYUBjNh1ACLbxqYNpdTQOteQ==
-postcss@^6.0.1, postcss@^6.0.23:
- version "6.0.23"
- resolved "https://registry.yarnpkg.com/postcss/-/postcss-6.0.23.tgz#61c82cc328ac60e677645f979054eb98bc0e3324"
- integrity sha512-soOk1h6J3VMTZtVeVpv15/Hpdl2cBLX3CAw4TAbkpTJiNPk9YP/zWcD1ND+xEtvyuuvKzbxliTOIyvkSeSJ6ag==
- dependencies:
- chalk "^2.4.1"
- source-map "^0.6.1"
- supports-color "^5.4.0"
-
postcss@^7.0.0, postcss@^7.0.1, postcss@^7.0.14, postcss@^7.0.17, postcss@^7.0.2, postcss@^7.0.27, postcss@^7.0.7:
version "7.0.27"
resolved "https://registry.yarnpkg.com/postcss/-/postcss-7.0.27.tgz#cc67cdc6b0daa375105b7c424a85567345fc54d9"
@@ -9060,6 +9108,15 @@ postcss@^7.0.0, postcss@^7.0.1, postcss@^7.0.14, postcss@^7.0.17, postcss@^7.0.2
source-map "^0.6.1"
supports-color "^6.1.0"
+postcss@^7.0.5, postcss@^7.0.6:
+ version "7.0.30"
+ resolved "https://registry.yarnpkg.com/postcss/-/postcss-7.0.30.tgz#cc9378beffe46a02cbc4506a0477d05fcea9a8e2"
+ integrity sha512-nu/0m+NtIzoubO+xdAlwZl/u5S5vi/y6BCsoL8D+8IxsD3XvBS8X4YEADNIVXKVuQvduiucnRv+vPIqj56EGMQ==
+ dependencies:
+ chalk "^2.4.2"
+ source-map "^0.6.1"
+ supports-color "^6.1.0"
+
prelude-ls@~1.1.2:
version "1.1.2"
resolved "https://registry.yarnpkg.com/prelude-ls/-/prelude-ls-1.1.2.tgz#21932a549f5e52ffd9a827f570e04be62a97da54"
@@ -10117,7 +10174,7 @@ send@0.17.1:
range-parser "~1.2.1"
statuses "~1.5.0"
-serialize-javascript@^2.1.2:
+serialize-javascript@^2.1.0, serialize-javascript@^2.1.2:
version "2.1.2"
resolved "https://registry.yarnpkg.com/serialize-javascript/-/serialize-javascript-2.1.2.tgz#ecec53b0e0317bdc95ef76ab7074b7384785fa61"
integrity sha512-rs9OggEUF0V4jUSecXazOYsLfu7OGK2qIn3c7IPBiffz32XniEp/TX9Xmc9LQfK2nQ2QKHvZ2oygKUGU0lG4jQ==
@@ -10852,7 +10909,7 @@ supports-color@^2.0.0:
resolved "https://registry.yarnpkg.com/supports-color/-/supports-color-2.0.0.tgz#535d045ce6b6363fa40117084629995e9df324c7"
integrity sha1-U10EXOa2Nj+kARcIRimZXp3zJMc=
-supports-color@^5.2.0, supports-color@^5.3.0, supports-color@^5.4.0:
+supports-color@^5.2.0, supports-color@^5.3.0:
version "5.5.0"
resolved "https://registry.yarnpkg.com/supports-color/-/supports-color-5.5.0.tgz#e2e69a44ac8772f78a1ec0b35b689df6530efc8f"
integrity sha512-QjVjwdXIt408MIiAqCX4oUKsgU2EqAGzs2Ppkm4aQYbjm+ZEWEcW4SfFNTr4uMNZma0ey4f5lgLrkB0aX0QMow==
@@ -11009,10 +11066,10 @@ throat@^4.0.0:
resolved "https://registry.yarnpkg.com/throat/-/throat-4.1.0.tgz#89037cbc92c56ab18926e6ba4cbb200e15672a6a"
integrity sha1-iQN8vJLFarGJJua6TLsgDhVnKmo=
-throttle-debounce@^2.0.0:
- version "2.0.1"
- resolved "https://registry.yarnpkg.com/throttle-debounce/-/throttle-debounce-2.0.1.tgz#7307ddd6cd9acadb349132fbf6c18d78c88a5e62"
- integrity sha512-Sr6jZBlWShsAaSXKyNXyNicOrJW/KtkDqIEwHt4wYwWA2wa/q67Luhqoujg48V8hTk60wB56tYrJJn6jc2R7VA==
+throttle-debounce@^2.1.0:
+ version "2.1.0"
+ resolved "https://registry.yarnpkg.com/throttle-debounce/-/throttle-debounce-2.1.0.tgz#257e648f0a56bd9e54fe0f132c4ab8611df4e1d5"
+ integrity sha512-AOvyNahXQuU7NN+VVvOOX+uW6FPaWdAOdRP5HfwYxAfCzXTFKRMoIMk+n+po318+ktcChx+F1Dd91G3YHeMKyg==
through2@^2.0.0:
version "2.0.5"
@@ -11282,10 +11339,17 @@ ts-jest@24.0.0, ts-jest@^23.10.5:
semver "^5.5"
yargs-parser "10.x"
-tslib@^1.9.0, tslib@^1.9.3:
- version "1.9.3"
- resolved "https://registry.yarnpkg.com/tslib/-/tslib-1.9.3.tgz#d7e4dd79245d85428c4d7e4822a79917954ca286"
- integrity sha512-4krF8scpejhaOgqzBEcGM7yDIEfi0/8+8zDRZhNZZ2kjmHJ4hv3zCbQWxoJGz1iw5U0Jl0nma13xzHXcncMavQ==
+tslib@^1.8.1, tslib@^1.9.0, tslib@^1.9.3:
+ version "1.11.1"
+ resolved "https://registry.yarnpkg.com/tslib/-/tslib-1.11.1.tgz#eb15d128827fbee2841549e171f45ed338ac7e35"
+ integrity sha512-aZW88SY8kQbU7gpV19lN24LtXh/yD4ZZg6qieAJDDg+YBsJcSmLGK9QpnUjAKVG/xefmvJGd1WUmfpT/g6AJGA==
+
+tsutils@^3.17.1:
+ version "3.17.1"
+ resolved "https://registry.yarnpkg.com/tsutils/-/tsutils-3.17.1.tgz#ed719917f11ca0dee586272b2ac49e015a2dd759"
+ integrity sha512-kzeQ5B8H3w60nFY2g8cJIuH7JDpsALXySGtwGJ0p2LSjLgay3NdIpqq5SoOBe46bKDW2iq25irHCr8wjomUS2g==
+ dependencies:
+ tslib "^1.8.1"
tty-browserify@0.0.0:
version "0.0.0"
@@ -11376,11 +11440,6 @@ underscore@1.6.0, underscore@~1.6.0:
resolved "https://registry.yarnpkg.com/underscore/-/underscore-1.6.0.tgz#8b38b10cacdef63337b8b24e4ff86d45aea529a8"
integrity sha1-izixDKze9jM3uLJOT/htRa6lKag=
-underscore@^1.9.2:
- version "1.9.2"
- resolved "https://registry.yarnpkg.com/underscore/-/underscore-1.9.2.tgz#0c8d6f536d6f378a5af264a72f7bec50feb7cf2f"
- integrity sha512-D39qtimx0c1fI3ya1Lnhk3E9nONswSKhnffBI0gME9C99fYOkNi04xs8K6pePLhvl1frbDemkaBQ5ikWllR2HQ==
-
underscore@~1.8.3:
version "1.8.3"
resolved "https://registry.yarnpkg.com/underscore/-/underscore-1.8.3.tgz#4f3fb53b106e6097fcf9cb4109f2a5e9bdfa5022"
@@ -11730,13 +11789,14 @@ void-elements@^2.0.0:
resolved "https://registry.yarnpkg.com/void-elements/-/void-elements-2.0.1.tgz#c066afb582bb1cb4128d60ea92392e94d5e9dbec"
integrity sha1-wGavtYK7HLQSjWDqkjkulNXp2+w=
-vue-apollo@^3.0.0-beta.28:
- version "3.0.0-beta.28"
- resolved "https://registry.yarnpkg.com/vue-apollo/-/vue-apollo-3.0.0-beta.28.tgz#be6a3a1504be2096cbfb23996537e2fc95c8c239"
- integrity sha512-ceCc1xTyxpNtiSeJMQgSkfgJue6pnv+TIvp75CwZlwMxRtNZjITj4MGvBWFwnoIEhVrUAw45ff/5udgJ8z9sdQ==
+vue-apollo@^3.0.3:
+ version "3.0.3"
+ resolved "https://registry.yarnpkg.com/vue-apollo/-/vue-apollo-3.0.3.tgz#7f29558df76eec0f03251847eef153816a261827"
+ integrity sha512-WJaQ1v/i46/oIPlKv7J0Tx6tTlbuaeCdhrAbL06h+Zca2gzr5ywjUFpl8ijMTGJsQ+Ph/U4xEpBFBOMxQmL+7g==
dependencies:
- chalk "^2.4.1"
- throttle-debounce "^2.0.0"
+ chalk "^2.4.2"
+ serialize-javascript "^2.1.0"
+ throttle-debounce "^2.1.0"
vue-eslint-parser@^7.0.0:
version "7.0.0"
@@ -11772,7 +11832,7 @@ vue-jest@^4.0.0-beta.2:
source-map "^0.5.6"
ts-jest "^23.10.5"
-vue-loader@^15.4.2, vue-loader@^15.9.0:
+vue-loader@^15.9.0:
version "15.9.0"
resolved "https://registry.yarnpkg.com/vue-loader/-/vue-loader-15.9.0.tgz#5d4b0378a4606188fc83e587ed23c94bc3a10998"
integrity sha512-FeDHvTSpwyLeF7LIV1PYkvqUQgTJ8UmOxhSlCyRSxaXCKk+M6NF4tDQsLsPPNeDPyR7TfRQ8MLg6v+8PsDV9xQ==